Initial import with GitLab CI/CD and registry deploy flow
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
FROM ubuntu:24.04
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
asterisk \
|
||||
ca-certificates \
|
||||
curl \
|
||||
openssl \
|
||||
python3 \
|
||||
procps \
|
||||
uuid-runtime \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY deployment/asterisk/entrypoint.sh /usr/local/bin/mvpcc-asterisk-entrypoint.sh
|
||||
|
||||
RUN chmod +x /usr/local/bin/mvpcc-asterisk-entrypoint.sh \
|
||||
&& mkdir -p /config-template /assets /etc/asterisk/keys /var/spool/asterisk/monitor/mvpcc
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/mvpcc-asterisk-entrypoint.sh"]
|
||||
@@ -0,0 +1,74 @@
|
||||
# Asterisk Lab Config
|
||||
|
||||
This folder contains the lab-grade configuration set for Track 9, Track 11, and Track 14:
|
||||
|
||||
- `pjsip.conf`
|
||||
- `extensions.conf`
|
||||
- `manager.conf`
|
||||
- `http.conf`
|
||||
- `rtp.conf`
|
||||
|
||||
These files are templates for a dedicated Linux VM running Asterisk 20 LTS.
|
||||
|
||||
Before using them:
|
||||
|
||||
1. Replace default passwords and secrets.
|
||||
2. Confirm the softphone source network.
|
||||
3. Create the recording directory:
|
||||
- `/var/spool/asterisk/monitor/mvpcc`
|
||||
4. Point `ASTERISK_QUEUE_MAP_JSON` in the platform to the matching queue code:
|
||||
- `voice_lab`
|
||||
- `voice_lab_ai` for the dedicated AI-first staging entrypoint
|
||||
- `voice_lab_ivr` for the dedicated IVR staging entrypoint
|
||||
5. Configure operator extension mapping in bridge env for Track 11 call control:
|
||||
- `ASTERISK_OPERATOR_EXTENSION_MAP_JSON={"operator_a":"2001","operator_b":"2002"}`
|
||||
6. Optional transfer map for queue-code based blind transfer:
|
||||
- `ASTERISK_TRANSFER_TARGET_MAP_JSON={"voice_lab":"2001","voice_lab_ai":"2001","voice_lab_ivr":"7200"}`
|
||||
7. For IVR-over-Asterisk staging:
|
||||
- enable `ASTERISK_IVR_FASTAGI_ENABLED=1`
|
||||
- set `ASTERISK_IVR_FASTAGI_HOST=<bridge-host>`
|
||||
- set `ASTERISK_IVR_FASTAGI_PORT=4573`
|
||||
- set `IVR_RUNTIME_TRUSTED_SERVICE_SUBJECTS=svc:asterisk-bridge`
|
||||
- provision the referenced `prompt_audio_key` files under the Asterisk sounds directory
|
||||
- if you want a higher-quality prompt from OpenAI TTS, generate it once and keep it on the server:
|
||||
- `python scripts/materialize_tts_prompt.py --text "Здравствуйте! Добро пожаловать в контакт-центр. Для русского языка нажмите 1. Қазақ тілі үшін 2 басыңыз." --output /usr/share/asterisk/sounds/en/mvpcc_ivr_language.wav --language ru`
|
||||
- `python scripts/materialize_tts_prompt.py --text "Для отдела продаж нажмите 1. Для службы поддержки нажмите 2." --output /usr/share/asterisk/sounds/en/mvpcc_ivr_menu_ru.wav --language ru`
|
||||
- `python scripts/materialize_tts_prompt.py --text "Сату бөлімі үшін 1 басыңыз. Қолдау қызметі үшін 2 басыңыз." --output /usr/share/asterisk/sounds/en/mvpcc_ivr_menu_kz.wav --language kk`
|
||||
- repeated runs reuse the server cache from `AI_VOICE_TTS_CACHE_DIR` instead of calling OpenAI again for the same text
|
||||
8. For Track 14 browser softphone QA:
|
||||
- enable `ASTERISK_WEBRTC_ENABLED=1`
|
||||
- set `ASTERISK_WEBRTC_WS_URL=wss://<asterisk-host>:8089/ws`
|
||||
- configure `ASTERISK_BROWSER_SIP_MAP_JSON`
|
||||
- replace TLS placeholders in `http.conf` with a trusted cert/key pair
|
||||
9. For Voice AI V1 lab wiring:
|
||||
- enable `AI_VOICE_ENABLED=1`
|
||||
- set `AI_VOICE_QUEUE_CONFIG_JSON={"voice_lab_ai":{"mode":"ai_first","agent_profile":"voice_support","handoff_queue_code":"voice_lab","language":"ru"}}`
|
||||
- set `AI_VOICE_RUNTIME_SERVICE_URL=http://ai-voice-runtime-service:8000`
|
||||
- keep `AI_VOICE_RUNTIME_TRUSTED_SERVICE_SUBJECTS=svc:ai-voice-runtime`
|
||||
- load `app_audiosocket.so` on the Asterisk VM before enabling the AI media path
|
||||
- set `MVPCC_AI_AUDIOSOCKET_SERVICE=<media-host>:<port>` in `extensions.conf` only after the AudioSocket listener is reachable
|
||||
|
||||
Entrypoints in `extensions.conf`:
|
||||
|
||||
- `7000`: human-only baseline. This path preserves the original Track 9/11 behavior.
|
||||
- `7100`: AI-first staging path. It emits the normal `MVPCCCallStarted` event with `QueueCode=voice_lab_ai`, then:
|
||||
- falls back to human dialing immediately if `MVPCC_AI_AUDIOSOCKET_SERVICE` is blank
|
||||
- otherwise starts `AudioSocket(<uuid>,<host>:<port>)` for the media bridge
|
||||
- `7200`: IVR staging path. It emits the normal `MVPCCCallStarted` event with `QueueCode=voice_lab_ivr`, then:
|
||||
- hands control to FastAGI at `MVPCC_IVR_FASTAGI_HOSTPORT`
|
||||
- routes completed IVR sessions through `mvpcc-transfer`
|
||||
- falls back to the human baseline if FastAGI does not return a transfer target
|
||||
|
||||
Important Voice AI note:
|
||||
|
||||
- `AI_VOICE_RUNTIME_SERVICE_URL` is the HTTP control-plane endpoint for platform services.
|
||||
- `MVPCC_AI_AUDIOSOCKET_SERVICE` is a separate raw AudioSocket address for Asterisk media.
|
||||
- Do not point `MVPCC_AI_AUDIOSOCKET_SERVICE` at the runtime HTTP port.
|
||||
|
||||
Default lab SIP accounts from `pjsip.conf`:
|
||||
- `1001` (caller)
|
||||
- `2001` (operator A)
|
||||
- `2002` (operator B)
|
||||
|
||||
Track 14 note:
|
||||
- `2001` and `2002` are allowed to keep multiple contacts so browser registration can coexist with `MicroSIP/Zoiper` in QA.
|
||||
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
template_dir="/config-template"
|
||||
monitor_dir="/var/spool/asterisk/monitor/mvpcc"
|
||||
primary_sounds_dir="/usr/share/asterisk/sounds"
|
||||
legacy_sounds_dir="/var/lib/asterisk/sounds"
|
||||
keys_dir="/etc/asterisk/keys"
|
||||
|
||||
password_1001="${ASTERISK_ENDPOINT_1001_PASSWORD:-${ASTERISK_AMI_SECRET:-ChangeMe-1001}}"
|
||||
password_2001="${ASTERISK_ENDPOINT_2001_PASSWORD:-${ASTERISK_AMI_SECRET:-ChangeMe-2001}}"
|
||||
password_2002="${ASTERISK_ENDPOINT_2002_PASSWORD:-${ASTERISK_AMI_SECRET:-ChangeMe-2002}}"
|
||||
ami_secret="${ASTERISK_AMI_SECRET:-ChangeMe-AmiSecret}"
|
||||
external_address="${ASTERISK_EXTERNAL_ADDRESS:-92.38.48.166}"
|
||||
|
||||
mkdir -p /etc/asterisk "$monitor_dir" "$primary_sounds_dir" "$legacy_sounds_dir" "$keys_dir"
|
||||
|
||||
for file in extensions.conf pjsip.conf manager.conf http.conf rtp.conf; do
|
||||
cp "${template_dir}/${file}" "/etc/asterisk/${file}"
|
||||
done
|
||||
|
||||
export ASTERISK_PASSWORD_1001="$password_1001"
|
||||
export ASTERISK_PASSWORD_2001="$password_2001"
|
||||
export ASTERISK_PASSWORD_2002="$password_2002"
|
||||
export ASTERISK_MANAGER_SECRET="$ami_secret"
|
||||
export ASTERISK_EXTERNAL_ADDRESS_VALUE="$external_address"
|
||||
|
||||
python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
||||
replacements = {
|
||||
"ChangeMe-1001": os.environ["ASTERISK_PASSWORD_1001"],
|
||||
"ChangeMe-2001": os.environ["ASTERISK_PASSWORD_2001"],
|
||||
"ChangeMe-2002": os.environ["ASTERISK_PASSWORD_2002"],
|
||||
"ChangeMe-AmiSecret": os.environ["ASTERISK_MANAGER_SECRET"],
|
||||
"92.38.48.166": os.environ["ASTERISK_EXTERNAL_ADDRESS_VALUE"],
|
||||
}
|
||||
|
||||
for path in (
|
||||
Path("/etc/asterisk/pjsip.conf"),
|
||||
Path("/etc/asterisk/manager.conf"),
|
||||
):
|
||||
text = path.read_text(encoding="utf-8")
|
||||
for old, new in replacements.items():
|
||||
text = text.replace(old, new)
|
||||
path.write_text(text, encoding="utf-8")
|
||||
|
||||
modules_path = Path("/etc/asterisk/modules.conf")
|
||||
if modules_path.exists():
|
||||
text = modules_path.read_text(encoding="utf-8")
|
||||
if "noload => chan_sip.so" not in text:
|
||||
stanza = (
|
||||
"; MVPCC uses PJSIP/WebRTC for browser softphones.\n"
|
||||
"noload => chan_sip.so\n\n"
|
||||
)
|
||||
if "[global]" in text:
|
||||
text = text.replace("[global]", f"{stanza}[global]", 1)
|
||||
elif "[modules]" in text:
|
||||
text = text.rstrip() + "\n\n" + stanza
|
||||
else:
|
||||
text = "[modules]\nautoload=yes\n\n" + stanza + text
|
||||
modules_path.write_text(text, encoding="utf-8")
|
||||
PY
|
||||
|
||||
if [ ! -f "${keys_dir}/asterisk.pem" ] || [ ! -f "${keys_dir}/asterisk.key" ]; then
|
||||
openssl req \
|
||||
-x509 \
|
||||
-nodes \
|
||||
-newkey rsa:2048 \
|
||||
-keyout "${keys_dir}/asterisk.key" \
|
||||
-out "${keys_dir}/asterisk.pem" \
|
||||
-days 3650 \
|
||||
-subj "/CN=${external_address}"
|
||||
fi
|
||||
|
||||
if [ -f /assets/mvpcc_ivr_prompt.wav ]; then
|
||||
for sounds_dir in "$primary_sounds_dir" "$legacy_sounds_dir"; do
|
||||
cp /assets/mvpcc_ivr_prompt.wav "${sounds_dir}/mvpcc_ivr_prompt.wav"
|
||||
mkdir -p "${sounds_dir}/en" "${sounds_dir}/ru" "${sounds_dir}/ru_RU"
|
||||
cp /assets/mvpcc_ivr_prompt.wav "${sounds_dir}/en/mvpcc_ivr_prompt.wav"
|
||||
cp /assets/mvpcc_ivr_prompt.wav "${sounds_dir}/ru/mvpcc_ivr_prompt.wav"
|
||||
cp /assets/mvpcc_ivr_prompt.wav "${sounds_dir}/ru_RU/mvpcc_ivr_prompt.wav"
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -d /assets/ivr ]; then
|
||||
for sounds_dir in "$primary_sounds_dir" "$legacy_sounds_dir"; do
|
||||
mkdir -p "${sounds_dir}/ivr"
|
||||
cp -a /assets/ivr/. "${sounds_dir}/ivr/"
|
||||
done
|
||||
fi
|
||||
|
||||
chown -R asterisk:asterisk /etc/asterisk "$keys_dir" /var/spool/asterisk/monitor
|
||||
|
||||
for sounds_dir in "$primary_sounds_dir" "$legacy_sounds_dir"; do
|
||||
if [ -w "$sounds_dir" ]; then
|
||||
chown -R asterisk:asterisk "$sounds_dir"
|
||||
fi
|
||||
done
|
||||
|
||||
chmod 0755 /var/spool/asterisk /var/spool/asterisk/monitor "$monitor_dir"
|
||||
|
||||
exec /usr/sbin/asterisk -f -U asterisk -G asterisk -vvv
|
||||
@@ -0,0 +1,119 @@
|
||||
[globals]
|
||||
MVPCC_QUEUE_CODE=voice_lab
|
||||
MVPCC_AI_QUEUE_CODE=voice_lab_ai
|
||||
MVPCC_IVR_QUEUE_CODE=voice_lab_ivr
|
||||
MVPCC_IVR_FALLBACK_QUEUE_CODE=voice_lab
|
||||
MVPCC_MONITOR_DIR=/var/spool/asterisk/monitor/mvpcc
|
||||
MVPCC_HUMAN_DIAL_TARGETS=PJSIP/2001&PJSIP/2002
|
||||
MVPCC_AI_AUDIOSOCKET_SERVICE=127.0.0.1:9019
|
||||
MVPCC_IVR_FASTAGI_HOSTPORT=127.0.0.1:4573
|
||||
|
||||
[from-softphones]
|
||||
exten => 7000,1,NoOp(MVPCC human baseline entrypoint)
|
||||
same => n,Answer()
|
||||
same => n,Set(MVPCC_QUEUE=${MVPCC_QUEUE_CODE})
|
||||
same => n,Gosub(mvpcc-call-init,s,1(${EXTEN}))
|
||||
same => n,Dial(${MVPCC_HUMAN_DIAL_TARGETS},40,U(mvpcc-connected^${MVPCC_CALL_ID}^${MVPCC_LINKED_ID}))
|
||||
same => n,Hangup()
|
||||
|
||||
exten => 7100,1,NoOp(MVPCC AI-first voice lab entrypoint)
|
||||
same => n,Set(MVPCC_QUEUE=${IF($["${MVPCC_AI_QUEUE_OVERRIDE}" != ""]?${MVPCC_AI_QUEUE_OVERRIDE}:${MVPCC_AI_QUEUE_CODE})})
|
||||
same => n,GotoIf($["${MVPCC_AI_REUSE_CALL}" = "1"]?ai-reuse,1)
|
||||
same => n,Answer()
|
||||
same => n,Gosub(mvpcc-call-init,s,1(${EXTEN}))
|
||||
same => n,Goto(ai-audiosocket,1)
|
||||
|
||||
exten => ai-reuse,1,NoOp(MVPCC AI handoff reusing active IVR call)
|
||||
same => n,Goto(ai-audiosocket,1)
|
||||
|
||||
exten => ai-audiosocket,1,NoOp(MVPCC AI audio bridge start)
|
||||
same => n,GotoIf($["${MVPCC_AI_AUDIOSOCKET_SERVICE}" = ""]?ai-human-fallback,1)
|
||||
same => n,Set(MVPCC_MEDIA_UUID=${SHELL(uuidgen | tr -d '\r\n')})
|
||||
same => n,UserEvent(MVPCCAIAudioBridgeRequested,CallID:${MVPCC_CALL_ID},LinkedID:${MVPCC_LINKED_ID},QueueCode:${MVPCC_QUEUE},Extension:${MVPCC_ENTRY_EXTENSION},Context:${CONTEXT},MediaUUID:${MVPCC_MEDIA_UUID},UuidLen:${LEN(${MVPCC_MEDIA_UUID})},AudioSocketService:${MVPCC_AI_AUDIOSOCKET_SERVICE},Channel:${CHANNEL})
|
||||
same => n,UserEvent(MVPCCAIAudioSocketAttempt,CallID:${MVPCC_CALL_ID},LinkedID:${MVPCC_LINKED_ID},QueueCode:${MVPCC_QUEUE},Extension:${MVPCC_ENTRY_EXTENSION},Context:${CONTEXT},MediaUUID:${MVPCC_MEDIA_UUID},UuidLen:${LEN(${MVPCC_MEDIA_UUID})},AudioSocketService:${MVPCC_AI_AUDIOSOCKET_SERVICE},Channel:${CHANNEL})
|
||||
same => n,TryExec(AudioSocket(${MVPCC_MEDIA_UUID},${MVPCC_AI_AUDIOSOCKET_SERVICE}))
|
||||
same => n,UserEvent(MVPCCAIAudioSocketResult,CallID:${MVPCC_CALL_ID},LinkedID:${MVPCC_LINKED_ID},QueueCode:${MVPCC_QUEUE},Extension:${MVPCC_ENTRY_EXTENSION},Context:${CONTEXT},MediaUUID:${MVPCC_MEDIA_UUID},UuidLen:${LEN(${MVPCC_MEDIA_UUID})},AudioSocketService:${MVPCC_AI_AUDIOSOCKET_SERVICE},TryExecStatus:${TRYSTATUS},Channel:${CHANNEL})
|
||||
same => n,GotoIf($["${TRYSTATUS}" = "SUCCESS"]?ai-bridge-ended,1)
|
||||
same => n,UserEvent(MVPCCAIFallbackToHuman,CallID:${MVPCC_CALL_ID},LinkedID:${MVPCC_LINKED_ID},QueueCode:${MVPCC_QUEUE},Extension:${MVPCC_ENTRY_EXTENSION},Context:${CONTEXT},Reason:audiosocket_failed,TryExecStatus:${TRYSTATUS},Channel:${CHANNEL})
|
||||
same => n,Dial(${MVPCC_HUMAN_DIAL_TARGETS},40,U(mvpcc-connected^${MVPCC_CALL_ID}^${MVPCC_LINKED_ID}))
|
||||
same => n,Hangup()
|
||||
|
||||
exten => ai-bridge-ended,1,UserEvent(MVPCCAIAudioBridgeEnded,CallID:${MVPCC_CALL_ID},LinkedID:${MVPCC_LINKED_ID},QueueCode:${MVPCC_QUEUE},Extension:${MVPCC_ENTRY_EXTENSION},Context:${CONTEXT},MediaUUID:${MVPCC_MEDIA_UUID},AudioSocketService:${MVPCC_AI_AUDIOSOCKET_SERVICE},Channel:${CHANNEL})
|
||||
same => n,Hangup()
|
||||
|
||||
exten => ai-human-fallback,1,UserEvent(MVPCCAIFallbackToHuman,CallID:${MVPCC_CALL_ID},LinkedID:${MVPCC_LINKED_ID},QueueCode:${MVPCC_QUEUE},Extension:${MVPCC_ENTRY_EXTENSION},Context:${CONTEXT},Reason:audiosocket_service_missing,Channel:${CHANNEL})
|
||||
same => n,Dial(${MVPCC_HUMAN_DIAL_TARGETS},40,U(mvpcc-connected^${MVPCC_CALL_ID}^${MVPCC_LINKED_ID}))
|
||||
same => n,Hangup()
|
||||
|
||||
exten => 7200,1,NoOp(MVPCC IVR staging entrypoint)
|
||||
same => n,Answer()
|
||||
same => n,Set(MVPCC_QUEUE=${MVPCC_IVR_QUEUE_CODE})
|
||||
same => n,Set(MVPCC_IVR_FALLBACK_QUEUE_CODE=${GLOBAL(MVPCC_IVR_FALLBACK_QUEUE_CODE)})
|
||||
same => n,Gosub(mvpcc-call-init,s,1(${EXTEN}))
|
||||
same => n,Set(MVPCC_IVR_RESULT=started)
|
||||
same => n,Set(MVPCC_IVR_TARGET_QUEUE_CODE=)
|
||||
same => n,Set(MVPCC_IVR_TARGET_EXTENSION=)
|
||||
same => n,Set(MVPCC_IVR_SESSION_ID=)
|
||||
same => n,GotoIf($["${MVPCC_IVR_FASTAGI_HOSTPORT}" = ""]?ivr-human-fallback,1)
|
||||
same => n,AGI(agi://${MVPCC_IVR_FASTAGI_HOSTPORT}/ivr,${MVPCC_CALL_ID},${MVPCC_LINKED_ID},${MVPCC_QUEUE},${MVPCC_IVR_FALLBACK_QUEUE_CODE},${EXTEN})
|
||||
same => n,GotoIf($["${MVPCC_IVR_TARGET_EXTENSION}" = ""]?ivr-human-fallback,1)
|
||||
same => n,Goto(mvpcc-transfer,${MVPCC_IVR_TARGET_EXTENSION},1)
|
||||
|
||||
exten => ivr-human-fallback,1,UserEvent(MVPCCIVRFallbackToHuman,CallID:${MVPCC_CALL_ID},LinkedID:${MVPCC_LINKED_ID},QueueCode:${MVPCC_QUEUE},FallbackQueueCode:${MVPCC_IVR_FALLBACK_QUEUE_CODE},Extension:${MVPCC_ENTRY_EXTENSION},Result:${MVPCC_IVR_RESULT},TargetQueueCode:${MVPCC_IVR_TARGET_QUEUE_CODE},TargetExtension:${MVPCC_IVR_TARGET_EXTENSION},Channel:${CHANNEL})
|
||||
same => n,Dial(${MVPCC_HUMAN_DIAL_TARGETS},40,U(mvpcc-connected^${MVPCC_CALL_ID}^${MVPCC_LINKED_ID}))
|
||||
same => n,Hangup()
|
||||
|
||||
exten => h,1,StopMixMonitor()
|
||||
same => n,UserEvent(MVPCCCallEnded,CallID:${MVPCC_CALL_ID},LinkedID:${MVPCC_LINKED_ID},HangupCause:${HANGUPCAUSE},DurationSeconds:${CDR(duration)})
|
||||
same => n,UserEvent(MVPCCRecordingReady,CallID:${MVPCC_CALL_ID},LinkedID:${MVPCC_LINKED_ID},RemotePath:${MONITOR_FILENAME},FileName:${MVPCC_RECORD_FILE},MimeType:audio/wav,DurationSeconds:${CDR(duration)})
|
||||
|
||||
[mvpcc-call-init]
|
||||
exten => s,1,Set(MVPCC_ENTRY_EXTENSION=${ARG1})
|
||||
same => n,Set(MVPCC_CALL_ID=${UNIQUEID})
|
||||
same => n,Set(MVPCC_LINKED_ID=${CHANNEL(linkedid)})
|
||||
same => n,Set(MVPCC_RECORD_FILE=${STRFTIME(${EPOCH},,%Y%m%d-%H%M%S)}-${UNIQUEID}.wav)
|
||||
same => n,Set(MONITOR_FILENAME=${MVPCC_MONITOR_DIR}/${MVPCC_RECORD_FILE})
|
||||
same => n,UserEvent(MVPCCCallStarted,CallID:${MVPCC_CALL_ID},LinkedID:${MVPCC_LINKED_ID},CallerNumber:${CALLERID(num)},CallerName:${CALLERID(name)},QueueCode:${MVPCC_QUEUE},Extension:${MVPCC_ENTRY_EXTENSION},Context:${CONTEXT},Direction:inbound,Channel:${CHANNEL})
|
||||
same => n,MixMonitor(${MONITOR_FILENAME},a)
|
||||
same => n,Return()
|
||||
|
||||
[mvpcc-connected]
|
||||
exten => s,1,Set(MVPCC_OPERATOR=${IF($["${ARG3}" != ""]?${ARG3}:${CHANNEL(peername)})})
|
||||
same => n,Set(MVPCC_OPERATOR=${IF($["${MVPCC_OPERATOR}" = ""]?${EXTEN}:${MVPCC_OPERATOR})})
|
||||
same => n,UserEvent(MVPCCOperatorConnected,CallID:${ARG1},LinkedID:${ARG2},OperatorExtension:${MVPCC_OPERATOR},Context:${CONTEXT},Channel:${CHANNEL},ClaimedByUser:${MVPCC_OPERATOR})
|
||||
same => n,Return()
|
||||
|
||||
[mvpcc-claim]
|
||||
exten => _X.,1,NoOp(Claimed call routed to operator ${EXTEN})
|
||||
same => n,Set(MVPCC_CALL_ID=${CHANNEL(linkedid)})
|
||||
same => n,Set(MVPCC_LINKED_ID=${CHANNEL(linkedid)})
|
||||
same => n,Dial(PJSIP/${EXTEN},40,U(mvpcc-connected^${MVPCC_CALL_ID}^${MVPCC_LINKED_ID}^${EXTEN}))
|
||||
same => n,Hangup()
|
||||
|
||||
[mvpcc-transfer]
|
||||
exten => 7000,1,NoOp(Queue transfer target ${EXTEN})
|
||||
same => n,Dial(${MVPCC_HUMAN_DIAL_TARGETS},40)
|
||||
same => n,Hangup()
|
||||
|
||||
exten => 7100,1,NoOp(Queue transfer target ${EXTEN})
|
||||
same => n,Goto(from-softphones,7100,1)
|
||||
|
||||
exten => 7200,1,NoOp(Queue transfer target ${EXTEN})
|
||||
same => n,Goto(from-softphones,7200,1)
|
||||
|
||||
exten => _X.,1,NoOp(Blind transfer target ${EXTEN})
|
||||
same => n,Dial(PJSIP/${EXTEN},40,U(mvpcc-connected^${MVPCC_CALL_ID}^${MVPCC_LINKED_ID}^${EXTEN}))
|
||||
same => n,GotoIf($["${DIALSTATUS}" = "ANSWER"]?transfer-finished,1)
|
||||
same => n,GotoIf($["${MVPCC_IVR_FALLBACK_QUEUE_CODE}" = ""]?transfer-finished,1)
|
||||
same => n,Set(MVPCC_IVR_RESULT=fallback_transfer_unavailable)
|
||||
same => n,Goto(from-softphones,ivr-human-fallback,1)
|
||||
|
||||
exten => transfer-finished,1,Hangup()
|
||||
|
||||
exten => h,1,StopMixMonitor()
|
||||
same => n,UserEvent(MVPCCCallEnded,CallID:${MVPCC_CALL_ID},LinkedID:${MVPCC_LINKED_ID},HangupCause:${HANGUPCAUSE},DurationSeconds:${CDR(duration)})
|
||||
same => n,UserEvent(MVPCCRecordingReady,CallID:${MVPCC_CALL_ID},LinkedID:${MVPCC_LINKED_ID},RemotePath:${MONITOR_FILENAME},FileName:${MVPCC_RECORD_FILE},MimeType:audio/wav,DurationSeconds:${CDR(duration)})
|
||||
|
||||
[mvpcc-operators]
|
||||
exten => _X.,1,Dial(PJSIP/${EXTEN},30)
|
||||
same => n,Hangup()
|
||||
@@ -0,0 +1,12 @@
|
||||
[general]
|
||||
enabled=yes
|
||||
enablestatic=yes
|
||||
bindaddr=0.0.0.0
|
||||
bindport=8088
|
||||
|
||||
; QA-only TLS placeholders for direct browser SIP over WebSocket.
|
||||
; Replace with your actual trusted certificate and key paths before enabling WSS.
|
||||
tlsenable=yes
|
||||
tlsbindaddr=0.0.0.0:8089
|
||||
tlscertfile=/etc/asterisk/keys/asterisk.pem
|
||||
tlsprivatekey=/etc/asterisk/keys/asterisk.key
|
||||
@@ -0,0 +1,11 @@
|
||||
[general]
|
||||
enabled = yes
|
||||
port = 5038
|
||||
bindaddr = 0.0.0.0
|
||||
|
||||
[mvpcc]
|
||||
secret = ChangeMe-AmiSecret
|
||||
deny = 0.0.0.0/0.0.0.0
|
||||
permit = 0.0.0.0/0.0.0.0
|
||||
read = system,call,log,verbose,command,agent,user,dtmf,reporting,cdr,dialplan
|
||||
write = command,call,originate,agent,user
|
||||
@@ -0,0 +1,84 @@
|
||||
[transport-udp]
|
||||
type=transport
|
||||
protocol=udp
|
||||
bind=0.0.0.0
|
||||
external_signaling_address=92.38.48.166
|
||||
external_media_address=92.38.48.166
|
||||
|
||||
[transport-wss]
|
||||
type=transport
|
||||
protocol=wss
|
||||
bind=0.0.0.0
|
||||
external_signaling_address=92.38.48.166
|
||||
external_media_address=92.38.48.166
|
||||
|
||||
[softphone-endpoint](!)
|
||||
type=endpoint
|
||||
disallow=all
|
||||
allow=opus,ulaw,alaw
|
||||
dtmf_mode=auto_info
|
||||
direct_media=no
|
||||
rtp_symmetric=yes
|
||||
force_rport=yes
|
||||
rewrite_contact=yes
|
||||
webrtc=yes
|
||||
ice_support=yes
|
||||
rtcp_mux=yes
|
||||
use_avpf=yes
|
||||
media_encryption=dtls
|
||||
dtls_auto_generate_cert=yes
|
||||
dtls_setup=actpass
|
||||
media_use_received_transport=yes
|
||||
media_address=92.38.48.166
|
||||
|
||||
[caller-endpoint](!,softphone-endpoint)
|
||||
context=from-softphones
|
||||
|
||||
[operator-endpoint](!,softphone-endpoint)
|
||||
context=mvpcc-operators
|
||||
|
||||
[1001](caller-endpoint)
|
||||
auth=1001-auth
|
||||
aors=1001
|
||||
|
||||
[1001-auth]
|
||||
type=auth
|
||||
auth_type=userpass
|
||||
username=1001
|
||||
password=ChangeMe-1001
|
||||
|
||||
[1001]
|
||||
type=aor
|
||||
max_contacts=2
|
||||
|
||||
[2001](operator-endpoint)
|
||||
auth=2001-auth
|
||||
aors=2001
|
||||
|
||||
[2001-auth]
|
||||
type=auth
|
||||
auth_type=userpass
|
||||
username=2001
|
||||
password=ChangeMe-2001
|
||||
|
||||
[2001]
|
||||
type=aor
|
||||
max_contacts=1
|
||||
remove_existing=yes
|
||||
support_path=yes
|
||||
|
||||
[2002](operator-endpoint)
|
||||
auth=2002-auth
|
||||
aors=2002
|
||||
|
||||
[2002-auth]
|
||||
type=auth
|
||||
auth_type=userpass
|
||||
username=2002
|
||||
password=ChangeMe-2002
|
||||
|
||||
[2002]
|
||||
type=aor
|
||||
max_contacts=1
|
||||
remove_existing=yes
|
||||
support_path=yes
|
||||
@@ -0,0 +1,5 @@
|
||||
[general]
|
||||
rtpstart=10000
|
||||
rtpend=20000
|
||||
icesupport=true
|
||||
stunaddr=stun.l.google.com:19302
|
||||
Reference in New Issue
Block a user