跳到正文

部署网络拓扑与配置手册

本文是跨项目部署网络的主文档,面向部署工程师、运维人员和排查跨主机链路的开发人员。 示例使用符号化主机名和占位地址;附录单独记录当前 aliyun + debian12-show + FRP 拓扑。本文只给出网络相关片段,不替代完整配置文件,也不保存真实密码、token、证书私钥或 shared secret。

默认端口以服务目录为唯一来源,字段语义以各模块配置文档为准:

1. 网络模型

1.1 三类地址不能混用

地址类型含义典型字段判断方法
监听 / bind进程实际绑定的本机网卡和端口server.hostlistenHostlisten-ipsip-iprtp-ip在服务所在主机执行 ss -lntup 能看到
连接 / upstream调用方用于建立连接的地址freeswitch.hostaudioFork.wsUrl、TTS endpoint、数据库 URL从调用方主机能够解析、路由并完成握手
公开 / advertised返回给另一节点或写入 SIP/SDP、ICE、媒体响应的地址callbackHost、TTS publicBaseUrlexternal_sip_ipexternal_rtp_ip、录音 URL、TURN external-ip从最终使用该地址的节点能够访问

0.0.0.0 只适合作为监听地址,不能作为 upstream 或公开地址。127.0.0.1 永远指向当前 进程所在主机:只有调用方和目标服务在同一网络命名空间时才可使用。NAT 主机通常用私网 IP 绑定,用公网 IP 或域名广告。

1.2 完整拓扑

1.3 呼叫控制时序

外呼由 callout-server 或管理 API 发起时,控制方向是调用方先连接 FreeSWITCH inbound ESL; 接通后 FreeSWITCH 再连接该 cell 配对的 callflow-esl outbound socket。两条 ESL 连接用途不同, 不能只开放其中一条。

1.4 WebRTC 媒体流

WSS 只承载 SIP 信令,不承载 RTP。浏览器显示 WSS 已连接不代表媒体可用;必须继续检查 ICE candidate、DTLS 状态和双方 RTP 计数。

1.5 逐连接矩阵

下表的自研服务端口均引用服务目录。生产部署可以覆盖端口, 但源、目标、防火墙和反向代理必须作为一组同步修改。

目标协议 / 默认端口长连接主要配置公网开放检查
浏览器Nginx Web/APIHTTPS 443VITE_*_API_BASE_URL、Nginx location页面与 GET /api/...
浏览器Nginx → FreeSWITCHSIP over WSS 4437443Sofia wss-binding、Nginx Upgrade仅 Nginx 入口SIP REGISTER
浏览器FreeSWITCH VertoJSON-RPC over WS/WSS 8081 / 8082verto.conf.xml、WebRTC 视频通话可按需代理开放Verto Login 与视讯流
SIP trunk / 终端FreeSWITCHSIP UDP/TCP/TLS,现场定义Sofia profile、gateway、realm按来源限制sofia status
浏览器coturnSTUN/TURN UDP+TCP 3478会话期两个后端 turn.*authenticated allocation
浏览器 / coturnFreeSWITCHDTLS-SRTP / RTP,现场范围会话期external_rtp_ipswitch.conf.xml是,UDP 范围getStats()、RTP 计数
coturn浏览器 / FreeSWITCHTURN relay UDP,独立范围会话期min-port / max-port是,UDP 范围relay address
FreeSWITCHcallflow-esloutbound ESL TCP 9911通话期dialplan socketesl-server.*建立真实呼叫
callflow-eslFreeSWITCHinbound ESL TCP 8021,部署样例另有覆盖常驻 / 命令期freeswitch.*event_socket.conf.xmlauthapi status
callout-server每个 FreeSWITCHinbound ESL TCP常驻 / 命令期cells[].freeswitch.*事件监听与 originate
FreeSWITCHSherpa ASR (流式)WebSocket 10096/audio识别期audioFork.wsUrl、ASR wsPathWS 握手、partial/final
FreeSWITCHFunASR ASR (2-pass 流式)WebSocket 10099/audio识别期audioFork.wsUrl、ASR wsPathWS 握手、partial/final
callout-server / 业务方FunASR ASR (离线)HTTP 10094/asrasrPath/v1/audio/transcriptionsPOST /asr 转写
callout-server / 业务方Sherpa ASR (离线)HTTP 10095/asrasrPath/v1/audio/transcriptionsPOST /asr 转写
callout-serverASRWebSocket 10096/audio10099/audio回铃检测期ringbackDetection.wsUrl回铃检测日志
callflow-eslTTSHTTP 9080请求或流式响应tts.profiles.*.endpointPOST /ttsPOST /tts-stream
FreeSWITCHTTSHTTP(S) 媒体 URL播放期TTS server.publicBaseUrl仅 FS 可达即可curl 返回 URL
callflow-servercallflow-eslHTTP 9912callflowEsl.baseUrl/tokenGET /health、runtime API
callout-servercallflow-eslHTTP 9912cells[].callflowEsl.*软电话会话代理
callflow-eslcallout-serverHTTP 9920callout.calloutServer.*进度 / 结果回写
callflow-eslcallai-server (NLU)HTTP 9930callout.nlu.*/health/api/v1/nlu/capabilities
callflow-eslcallai-server (Chat)HTTP/SSE 9930单轮流式callout.chat.*capabilities、SSE complete
callout-servercallai-serverHTTP/SSE 9930话术生成流式callai.*capabilities、/api/v1/script/*
callflow-esl 业务 / callai-serverOpenAI 兼容推理后端HTTP /v1/chat/completions单轮流式businessConfig.*.endpointllm.openaiUrl 或 DB callai.ai_llm_configs/v1/models、一次推理
callout-serverEmotionHTTP 9090/analyzeemotionAnalysis.endpoint/healthPOST /analyze
Bun 服务 / FreeSWITCHPostgreSQLTCP 5432连接池db.urldatabases.*.url、DSNpg_isready / SQL
callflow-esl / callout-serverRedisTCP 6379常驻redis.urlPING
浏览器Nginx 录音 / 情绪音频HTTPS 443media.*BaseUrl、Nginx alias可只读开放HEAD / GET 文件

2. 部署场景

2.1 通用主机名

以下名称用于所有通用示例。内部 DNS 应返回私网地址,公网域名只用于浏览器、SIP/SDP、TURN 和确实需要跨边界访问的媒体。

名称角色
edge.example.com前端、API 与媒体 HTTPS 入口
sip.example.comWebRTC WSS、SIP realm 与 SIP/SDP 广告
turn.example.comTURN control 与 relay 广告
fs-a.internalFreeSWITCH cell A
callflow-esl-a.internalcell A 配对 callflow-esl
asr.internal / tts.internal / emotion.internal本地语音服务
callai.internal / openai.internal统一 AI 决策中台与 OpenAI 兼容推理后端
postgres.internal / redis.internal数据服务

2.2 单机部署

所有进程位于同一主机时,内部 upstream 可使用 127.0.0.1。监听地址仍按访问者选择:只由 同机进程访问的服务绑定 127.0.0.1;需要接收外部 SIP、RTP、HTTPS 或 TURN 的组件绑定实际 网卡或 0.0.0.0,并用防火墙限制。

纯内网联调的关键地址:

json
{
  "esl-server": { "host": "127.0.0.1", "port": 9911 },
  "http-server": { "host": "127.0.0.1", "port": 9912, "authToken": ["<ESL_HTTP_TOKEN>"] },
  "freeswitch": {
    "host": "127.0.0.1",
    "port": 8021,
    "password": "<ESL_PASSWORD>",
    "callbackHost": "127.0.0.1",
    "callbackPort": 9911
  },
  "audioFork": { "wsUrl": "ws://127.0.0.1:10096/audio" },
  "tts": {
    "profiles": {
      "wav": { "endpoint": "http://127.0.0.1:9080/tts" },
      "shout": { "endpoint": "http://127.0.0.1:9080/tts-stream", "playbackPrefix": "shout://" }
    }
  }
}

TTS 同机时仍需区分控制地址与返回地址。若 FreeSWITCH 同机,TTS 可广告 http://127.0.0.1:9080;只要任一播放者跨主机,就改为其可达的主机地址或 HTTPS 域名。

公网 WebRTC 在上述基础上增加:

  • sip.example.com:443 终止 TLS 并代理到本机 Sofia WSS;
  • external_sip_ipexternal_rtp_ip 广告公网 IP;
  • FreeSWITCH RTP 与 coturn relay 使用两个不重叠的 UDP 范围;
  • 两个管理后端启用相同 TURN realm、host 和 shared secret,浏览器只获得短期凭据;
  • 录音与情绪音频通过 Nginx 只读发布,目录不可执行、不可列目录。

内网联调不需要 TURN 时保持 turn.enabled=false。公网浏览器跨运营商网络时不能把 TURN 当作可选优化,必须完成真实 allocation 与呼叫验收。

2.3 分层多机部署

推荐最小放行规则:

边界允许方向最小端口
公网 → 边缘入站80/443 TCP、SIP 现场端口、3478 TCP/UDP、FS RTP UDP 范围、TURN relay UDP 范围
边缘 → 应用出站callflow / callout API 端口;Web upstream 仅 Nginx 需要
边缘 → AI出站ASR WS、TTS HTTP、callai-server、OpenAI 兼容推理后端(仅业务确实直连时)
应用 → 边缘出站callflow-esl HTTP、每 cell inbound ESL、只读录音 HTTPS
应用 → AI出站Emotion、callai-server
边缘 / 应用 → 数据出站PostgreSQL、Redis
AI → 数据出站默认无;只有新增明确依赖时开放

主机防火墙规则应同时限制源网段或源主机。禁止用“整个内部网段到所有端口”代替连接矩阵。 数据库和 Redis 若支持 TLS,跨不可信网络分别使用 postgresql TLS 参数与 rediss://

2.4 每服务独立部署

完整内部地址表:

服务DNS 与端口调用者
FreeSWITCH Afs-a.internal:<ESL_PORT>callflow-esl A、callout-server
callflow-esl A outboundcallflow-esl-a.internal:9911FreeSWITCH A
callflow-esl A HTTPcallflow-esl-a.internal:9912两个 API
callflow-servercallflow-api.internal:9913Nginx
callout-servercallout-api.internal:9920Nginx、callflow-esl
ASRasr.internal:10096FreeSWITCH、callout-server
TTStts.internal:9080callflow-esl、FreeSWITCH
Emotionemotion.internal:9090callout-server
统一 AI 决策中台 (callai-server)callai.internal:9930callflow-esl、callout-server
OpenAI 兼容推理后端openai.internal:8000callai-server
PostgreSQLpostgres.internal:5432FreeSWITCH 与各自研后端
Redisredis.internal:6379callflow-esl、callout-server

调用方配置替换表:

调用方字段目标示例
FreeSWITCH Adialplan socketcallflow-esl-a.internal:9911
callflow-esl Afreeswitch.host/portfs-a.internal:<ESL_PORT>
callflow-esl Afreeswitch.callbackHost/Portcallflow-esl-a.internal:9911
callflow-esl AaudioFork.wsUrlws://asr.internal:10096/audio
callflow-esl Atts.profiles.*.endpointhttp://tts.internal:9080/...
callflow-esl AbusinessConfig.*.endpointOpenAI 兼容后端 /v1/chat/completions 或 callai-server 完整 API URL
callflow-esl Acallout.*.baseUrl对应 API 内部基址
callflow-esl Adb.url / redis.url数据层 DNS
callflow-serverdatabases.*.urlPostgreSQL DNS
callflow-servercallflowEsl.baseUrlhttp://callflow-esl-a.internal:9912
callout-servercells[].freeswitch.*对应 cell FreeSWITCH
callout-servercells[].callflowEsl.baseUrl对应 cell callflow-esl
callout-serveremotionAnalysis.endpointhttp://emotion.internal:9090/analyze
callout-servercallai.baseUrlhttp://callai.internal:9930
AI 决策中台 (callai-server)llm.openaiUrlhttp://openai.internal:8000/v1/chat/completions

跨机部署还必须满足:

  • ASR 和 TTS 流式请求的代理关闭响应缓冲,读超时覆盖最长通话轮次;
  • WebSocket 代理传递 Upgrade / Connection,并保留 ASR 子协议;
  • FreeSWITCH 录音目录不能假设与 callout-server 共用文件系统,应由 cell 所在主机发布只读 URL;
  • TTS publicBaseUrl 从 FreeSWITCH 主机测试,而不是只从 callflow-esl 测试;
  • 内部 DNS 设置合理 TTL,证书 SAN 覆盖实际域名;长连接不会因 DNS 变化自动迁移;
  • 避免链路中重复 NAT。出现 WebSocket 随机断开或大包丢失时检查 MTU、MSS clamp 和代理超时;
  • 健康检查只证明新连接可建立,部署切换前还要评估已有 ESL、WS 和流式媒体连接的排空。

2.5 多 FreeSWITCH cell 与高可用

一个 cell 固定由一台 FreeSWITCH 和配对的 callflow-esl 服务构成。cellId 会写入业务 记录并决定事件、录音与并发归属,必须稳定、唯一,投产后不要重命名。

网络相关模板:

json
{
  "redis": {
    "url": "rediss://<USER>:<PASSWORD>@redis.internal:6379/0",
    "keyPrefix": "callout",
    "lockTtlMs": 60000
  },
  "cells": [
    {
      "id": "cell-a",
      "freeswitch": {
        "host": "fs-a.internal",
        "port": 8021,
        "password": "<CELL_A_ESL_PASSWORD>",
        "callbackHost": "callflow-esl-a.internal",
        "callbackPort": 9911,
        "originateDialStringTemplate": "user/{{destinationNumber}}",
        "agentDialStringTemplate": "user/{{agentExtension}}",
        "commandTimeoutMs": 15000
      },
      "callflowEsl": {
        "baseUrl": "http://callflow-esl-a.internal:9912",
        "token": "<CELL_A_HTTP_TOKEN>"
      },
      "media": {
        "freeswitchRecordingBaseUrl": "https://media.example.com/recordings/cell-a"
      },
      "maxConcurrency": 100
    },
    {
      "id": "cell-b",
      "freeswitch": {
        "host": "fs-b.internal",
        "port": 8021,
        "password": "<CELL_B_ESL_PASSWORD>",
        "callbackHost": "callflow-esl-b.internal",
        "callbackPort": 9911
      },
      "callflowEsl": {
        "baseUrl": "http://callflow-esl-b.internal:9912",
        "token": "<CELL_B_HTTP_TOKEN>"
      },
      "media": {
        "freeswitchRecordingBaseUrl": "https://media.example.com/recordings/cell-b"
      },
      "maxConcurrency": 100
    }
  ]
}

多个 callout-server 可共享 PostgreSQL 和 Redis,并置于 HTTP 负载均衡后。每 cell inbound ESL 事件监听由 Redis 租约协调,同一时刻只应有一个消费者;这提供消费者接管能力,但不等于呼叫 无损自动迁移。至少监控租约续期、数据库连接、各 cell ESL 状态和可用并发。

以下边界需要连接归属或配对,不能直接套用普通无状态 HTTP 轮询:

  • FreeSWITCH outbound socket 必须进入本 cell 的 callflow-esl;在途呼叫不能迁移到另一实例;
  • inbound ESL 命令和事件必须命中承载目标 UUID 的 FreeSWITCH;
  • ASR WebSocket 属于当前媒体 bug 与识别轮次,断线后需要业务恢复,不会透明切换;
  • TTS /tts-stream 产生的资源和响应由创建它的 TTS 实例持有,负载均衡需粘性、共享资源存储, 或让返回 URL 精确回到原实例;
  • 录音 URL 必须按 attempt.cellId 选择 cell 对应的媒体源;
  • 扩容 callflow-esl 时,共享 Redis 只协调部分运行态,不会把既有 outbound ESL socket 搬迁。

2.6 可选语音后端

主线配置使用本地 sherpa ASR/TTS。替换为云兼容网关时,只改变调用地址和 TTS 广告地址,不 改变 FreeSWITCH 到兼容接口的协议:

后端ASRTTS替换规则
本地 sherpa100969080audioFork.wsUrl 与两个 TTS endpoint 指向本地服务
阿里云网关100979081指向网关 /audio/tts/tts-stream,校准 tts.publicBaseUrl
Azure 网关100989082同上,使用 Azure 网关端口

网关 tts.publicBaseUrl 同样必须从 FreeSWITCH 可达。阿里云的 appKey/token 和 Azure 的 key、 region、endpoint 只能用相应环境变量或密钥系统注入,不能写入本文、仓库配置样例或前端构建 变量。详见阿里云网关Azure 网关

3. 配置调整手册

3.1 callflow-esl

下面只展示网络字段;合并时保留完整配置中的超时、业务和运行参数。

json
{
  "esl-server": { "host": "0.0.0.0", "port": 9911 },
  "http-server": {
    "host": "0.0.0.0",
    "port": 9912,
    "authToken": ["<ESL_HTTP_TOKEN>"]
  },
  "freeswitch": {
    "host": "fs-a.internal",
    "port": 8021,
    "password": "<ESL_PASSWORD>",
    "callbackHost": "callflow-esl-a.internal",
    "callbackPort": 9911
  },
  "audioFork": { "wsUrl": "ws://asr.internal:10096/audio" },
  "tts": {
    "profiles": {
      "wav": {
        "endpoint": "http://tts.internal:9080/tts",
        "playbackTarget": "wav-url",
        "fsPlaybackBaseDir": ""
      },
      "shout": {
        "endpoint": "http://tts.internal:9080/tts-stream",
        "playbackPrefix": "shout://"
      }
    }
  },
  "businessConfig": {
    "llm-chat-business": {
      "endpoint": "http://openai.internal:8000/v1/chat/completions",
      "apiKey": "<OPENAI_API_KEY>",
      "model": "<MODEL_NAME>"
    },
    "kb-audio-chat-business": {
      "endpoint": "http://callai.internal:9930/api/v1/conversation",
      "token": "<CHAT_TOKEN>"
    }
  },
  "callout": {
    "calloutServer": { "baseUrl": "http://callout-api.internal:9920", "token": "<CALLOUT_TOKEN>" },
    "nlu": { "baseUrl": "http://llm.internal:9930", "token": "<NLU_TOKEN>" },
    "chat": { "baseUrl": "http://llm.internal:9930", "token": "<CHAT_TOKEN>" }
  },
  "db": { "url": "postgres://<USER>:<PASSWORD>@postgres.internal:5432/freeswitch" },
  "redis": { "url": "redis://<USER>:<PASSWORD>@redis.internal:6379/0" },
  "recording": { "directory": "/srv/freeswitch/recordings" }
}

逐字段按访问者判断:

字段实际访问者不能误填为
esl-server.host/portFreeSWITCH → callflow-eslFreeSWITCH 自己的回环地址
http-server.host/port两个 API / Nginx → callflow-esl只监听回环但调用者跨机
freeswitch.host/port/passwordcallflow-esl → FreeSWITCH inbound ESLoutbound ESL 9911
freeswitch.callbackHost/PortFreeSWITCH → callflow-esl outbound ESLcallflow-esl 所在机的 127.0.0.1(跨机时)
audioFork.wsUrlFreeSWITCH 的 mod_audio_fork → ASR仅 callflow-esl 可达的地址
tts.profiles.*.endpointcallflow-esl → TTSTTS 返回给 FreeSWITCH 的媒体基址
recording.directoryFreeSWITCH 写文件callflow-esl 本机但未共享的目录
db.url / redis.urlcallflow-esl → 数据层调用方跨机时的 localhost

audioFork.wsUrl 虽然位于 callflow-esl 配置中,真正建立 WebSocket 的是 FreeSWITCH。录音路径 也由 FreeSWITCH 写入;跨机时要使用 FreeSWITCH 本地路径或双方真实共享的挂载。

3.2 callflow-server 与前端

json
{
  "server": { "host": "0.0.0.0", "port": 9913 },
  "databases": {
    "freeswitch": { "url": "postgres://<RO_USER>:<PASSWORD>@postgres.internal:5432/freeswitch" },
    "callflow": { "url": "postgres://<USER>:<PASSWORD>@postgres.internal:5432/freeswitch?options=-c%20search_path=callflow" }
  },
  "callflowEsl": {
    "baseUrl": "http://callflow-esl-a.internal:9912",
    "token": "<ESL_HTTP_TOKEN>"
  },
  "turn": {
    "enabled": true,
    "host": "turn.example.com",
    "port": 3478,
    "realm": "sip.example.com",
    "sharedSecret": "<INJECT_AT_RUNTIME>",
    "credentialTtlSeconds": 3600
  }
}

callflow-server 当前没有 Redis 网络配置。前端 API 根地址在构建期注入:

bash
VITE_CALLFLOW_API_BASE_URL=/api bun run --cwd apps/callflow-webpage build
VITE_CALLOUT_API_BASE_URL=/api bun run --cwd apps/callout-webpage build
VITE_CALLAI_API_BASE_URL=/api bun run --cwd apps/callai-webpage build

同域 /api 时,应为三个前端使用不同 origin,并分别代理到对应 API;同一 origin 的同一个 /api 不能同时指向多个后端。使用绝对 URL 时必须是浏览器可访问地址,并配置 HTTPS 与所需 CORS 策略。

3.3 callout-server

单 cell 可使用 cells 的单元素形式,避免从顶层兼容字段迁移到多 cell 时改变语义:

json
{
  "server": {
    "host": "0.0.0.0",
    "port": 9920,
    "serviceApi": { "authToken": "<CALLOUT_TOKEN>" }
  },
  "db": { "url": "postgres://<USER>:<PASSWORD>@postgres.internal:5432/freeswitch" },
  "redis": {
    "url": "redis://<USER>:<PASSWORD>@redis.internal:6379/0",
    "keyPrefix": "callout",
    "lockTtlMs": 60000
  },
  "emotionAnalysis": { "endpoint": "http://emotion.internal:9090/analyze" },
  "nlu": {
    "capabilitiesEndpoint": "http://llm.internal:9930/api/v1/nlu/capabilities",
    "token": "<NLU_TOKEN>"
  },
  "chat": {
    "capabilitiesEndpoint": "http://llm.internal:9930/api/v1/chat/capabilities",
    "token": "<CHAT_TOKEN>"
  },
  "media": {
    "emotionAudioBaseUrl": "https://media.example.com/emotion-audio"
  },
  "turn": {
    "enabled": true,
    "host": "turn.example.com",
    "port": 3478,
    "realm": "sip.example.com",
    "sharedSecret": "<INJECT_AT_RUNTIME>"
  },
  "cells": [
    {
      "id": "cell-a",
      "freeswitch": {
        "host": "fs-a.internal",
        "port": 8021,
        "password": "<ESL_PASSWORD>",
        "callbackHost": "callflow-esl-a.internal",
        "callbackPort": 9911
      },
      "callflowEsl": {
        "baseUrl": "http://callflow-esl-a.internal:9912",
        "token": "<ESL_HTTP_TOKEN>"
      },
      "media": {
        "freeswitchRecordingBaseUrl": "https://media.example.com/recordings/cell-a"
      },
      "maxConcurrency": 100
    }
  ]
}

ringbackDetection.wsUrl 也由 callout-server 主机直接连接 ASR。两个后端的 TURN host、port、 realm 和 shared secret 必须与 coturn 一致;shared secret 优先通过 CALLFLOW_SERVER_TURN_SHARED_SECRETCALLOUT_TURN_SHARED_SECRET 注入。

3.4 ASR、TTS、Emotion、NLU 与 Chat

json
{
  "server": {
    "host": "0.0.0.0",
    "port": 10096,
    "healthPath": "/health",
    "wsPath": "/audio",
    "wsSubprotocol": "audio.drachtio.org"
  }
}

ASR 的调用者是 FreeSWITCH 和可选的 callout 回铃检测器。跨机时监听地址需覆盖目标私网网卡, 只向这些源开放。

json
{
  "server": {
    "listenHost": "0.0.0.0",
    "listenPort": 9080,
    "publicBaseUrl": "https://media.example.com/tts"
  }
}

TTS listenHost/listenPort 供 callflow-esl 发送控制请求;publicBaseUrl 出现在合成响应中, 由 FreeSWITCH 发起 GET。两者可以是不同地址。

json
{
  "server": { "host": "0.0.0.0", "port": 9090 },
  "audio": { "processedDir": "/srv/ai-voice/emotion-audio" }
}

Emotion 把处理后的音频写入 audio.processedDir 并返回短文件名。Nginx 必须把该目录映射到 callout-server.media.emotionAudioBaseUrl 对应路径;Emotion 本身没有公开基址字段。

json
{
  "server": { "host": "0.0.0.0", "port": 9930, "authToken": ["<CALLAI_TOKEN>"] },
  "db": { "url": "postgres://postgres:postgres@postgres.internal:5432/freeswitch" },
  "llm": {
    "openaiUrl": "http://openai.internal:8000/v1/chat/completions",
    "apiKey": "<API_KEY>",
    "nlu": { "model": "qwen3.5-2b", "timeoutMs": 30000, "maxTokens": 300 },
    "chat": { "model": "qwen3.5-2b", "timeoutMs": 30000 },
    "script": { "model": "qwen3.6-27b-fp8", "timeoutMs": 60000, "maxTokens": 3000 }
  }
}

callai-server 基于 PostgreSQL callai 模式持久化维护 AI 策略与用量记录。/health 可用于探活,业务与策略管理接口支持内部 Token 鉴权(X-Callai-Internal-Token / X-Callout-Internal-Token / Authorization)以及统一用户中心会话校验。模型后端与 callai-server 不得直接暴露公网。

3.5 FreeSWITCH

inbound ESL

xml
<configuration name="event_socket.conf" description="Socket Client">
  <settings>
    <param name="listen-ip" value="<FS_PRIVATE_IP>"/>
    <param name="listen-port" value="8021"/>
    <param name="password" value="<ESL_PASSWORD>"/>
    <param name="apply-inbound-acl" value="callflow-esl-inbound"/>
  </settings>
</configuration>
xml
<list name="callflow-esl-inbound" default="deny">
  <node type="allow" cidr="<CALLFLOW_ESL_CIDR>"/>
  <node type="allow" cidr="<CALLOUT_SERVER_CIDR>"/>
</list>

<list name="domains" default="deny">
  <node type="allow" cidr="<NGINX_TO_SOFIA_CIDR>"/>
</list>

<list name="softphone-turn-relay" default="deny">
  <node type="allow" cidr="<TURN_PUBLIC_IP>/32"/>
</list>

密码不能替代网络 ACL;端口不得直接面向公网。配置中的 listen-port 必须与两个调用方的 freeswitch.port / cells[].freeswitch.port 一致。Sofia 使用的来源 ACL 必须允许实际 Nginx 上游地址,candidate ACL 必须与后端下发的 TURN 公网地址一致;不要放宽为任意公网来源。

outbound ESL 与模块

xml
<extension name="callflow-esl-socket">
  <condition field="destination_number" expression="^<SERVICE_PREFIX>">
    <action application="socket" data="callflow-esl-a.internal:9911 async full"/>
  </condition>
</extension>
xml
<load module="mod_event_socket"/>
<load module="mod_audio_fork"/>
<load module="mod_shout"/>

mod_shout 仅在使用 shout:// MP3 流播放时必需;WAV 播放还应保留现场所需的 HTTP / 文件 模块。模块是否已加载可用 show modules 核对。

SIP、SDP 与 RTP

xml
<!-- vars.xml:绑定地址与广告地址分离 -->
<X-PRE-PROCESS cmd="set" data="domain=sip.example.com"/>
<X-PRE-PROCESS cmd="set" data="external_sip_ip=<PUBLIC_IP>"/>
<X-PRE-PROCESS cmd="set" data="external_rtp_ip=<PUBLIC_IP>"/>
xml
<!-- Sofia profile -->
<param name="sip-ip" value="<FS_PRIVATE_IP>"/>
<param name="rtp-ip" value="<FS_PRIVATE_IP>"/>
<param name="ext-sip-ip" value="$${external_sip_ip}"/>
<param name="ext-rtp-ip" value="$${external_rtp_ip}"/>
<param name="ws-binding" value=":5066"/>
<param name="wss-binding" value=":7443"/>
<param name="local-network-acl" value="none"/>
<param name="apply-candidate-acl" value="softphone-turn-relay"/>
xml
<!-- switch.conf.xml;范围按容量规划,且不与 TURN relay 重叠 -->
<param name="rtp-start-port" value="<FS_RTP_START>"/>
<param name="rtp-end-port" value="<FS_RTP_END>"/>

sip-ip / rtp-ip 必须是本机可绑定地址,external_* 是写入 SIP/SDP 的公网广告地址。 NAT 后的浏览器场景应同时检查 Sofia local-network-acl、candidate ACL 和公网防火墙,不能只 修改 external_rtp_ip

录音、数据库与证书

  • record-path、会议 auto-record 和 callflow-esl recording.directory 应落到同一套可写目录 约定;按 cell 从对应 FreeSWITCH 主机只读发布。
  • FreeSWITCH PostgreSQL DSN 的 host 必须从 FreeSWITCH 主机可达。不要把完整含密码 DSN 放进 公开文档、日志或命令历史。
  • WSS、SIP TLS 和 DTLS 证书必须属于目标域名且处于有效期,私钥仅允许运行用户读取。仓库内 参考证书不能用于生产。

生效边界

变更最小操作备注
dialplan、directory、普通 XML 变量reloadxml只影响后续匹配;用新呼叫验证
ACL 内容reloadacl新连接 / 新 candidate 生效
Sofia profile 的绑定、WSS、NAT 参数重启对应 Sofia profile会影响注册与在途 SIP 会话,安排窗口
模块加载、event socket 核心监听、RTP 全局范围、底层库完整重启 FreeSWITCH先排空通话并准备回滚

3.6 Nginx

以下模板展示路径关系,证书路径和安全策略按现场管理:

nginx
map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
}

upstream callflow_api { server callflow-api.internal:9913; }
upstream callout_api { server callout-api.internal:9920; }
upstream asr_ws { server asr.internal:10096; }
upstream tts_http { server tts.internal:9080; }

server {
    listen 443 ssl;
    server_name flow.example.com;

    location /api/ {
        proxy_pass http://callflow_api;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
    location / {
        root /srv/www/callflow;
        try_files $uri $uri/ /index.html;
    }
}

server {
    listen 443 ssl;
    server_name sip.example.com;

    location / {
        proxy_pass https://<FS_PRIVATE_IP>:7443;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_set_header Host $host;
        proxy_read_timeout 3600s;
    }
}

server {
    listen 443 ssl;
    server_name media.example.com;

    location /asr/ {
        proxy_pass http://asr_ws/;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_read_timeout 3600s;
        proxy_buffering off;
    }
    location /tts/ {
        proxy_pass http://tts_http/;
        proxy_buffering off;
        proxy_read_timeout 120s;
    }
    location /recordings/cell-a/ {
        alias /srv/freeswitch/recordings/;
        limit_except GET { deny all; }
        autoindex off;
    }
    location /emotion-audio/ {
        alias /srv/ai-voice/emotion-audio/;
        limit_except GET { deny all; }
        autoindex off;
    }
}

ASR 并不需要默认公开给浏览器;只有确实跨边界且无法使用私网时才增加公网 WSS,并加入鉴权、 来源限制和容量保护。不要通过 Nginx 暴露 ESL、数据库、Redis 或 OpenAI 兼容推理后端。

3.7 coturn

ini
listening-ip=0.0.0.0
listening-port=3478
external-ip=<TURN_PUBLIC_IP>/<TURN_PRIVATE_IP>
min-port=<TURN_RELAY_START>
max-port=<TURN_RELAY_END>
realm=sip.example.com
use-auth-secret
static-auth-secret=<INJECT_IN_PRIVATE_CONFIG>
fingerprint
no-multicast-peers

公网/私网映射写法用于一对一 NAT;直接绑定公网 IP 时按 coturn 版本与网络模型调整。控制端口和 整个 relay 范围都需放行,relay 范围不得与 FreeSWITCH RTP 范围重叠。生产优先配置 TLS/TURNS; 若现场暂未启用,必须至少提供 TURN/UDP 与 TURN/TCP,并使用短期 HMAC 凭据。

3.8 FRP

FRP 只应用于无法建立私网路由的应用 / AI TCP upstream。FRPS 代理端口应绑定回环或专用内网, 再由同机 Nginx / 应用消费:

toml
bindPort = 7000
proxyBindAddr = "127.0.0.1"

auth.method = "token"
auth.token = "<INJECT_FROM_SECRET_STORE>"
toml
serverAddr = "<FRPS_PUBLIC_OR_PRIVATE_IP>"
serverPort = 7000
auth.method = "token"
auth.token = "<INJECT_FROM_SECRET_STORE>"

[[proxies]]
name = "asr"
type = "tcp"
localIP = "127.0.0.1"
localPort = 10096
remotePort = <LOOPBACK_UPSTREAM_PORT>

不得用 FRP 承载高带宽、低抖动敏感的 FreeSWITCH RTP 或 TURN relay;公网 SIP WSS 也应在 FreeSWITCH 所在边缘主机就近终止。FRP token 只存在于私有配置,先运行配置校验,再滚动重启。

4. 安全边界

4.1 端口分区

分区允许的典型入口
公网入口HTTPS/WSS、明确的 SIP 入口、TURN control、FreeSWITCH RTP、TURN relay
跨主机内部outbound/inbound ESL、应用 API、ASR/TTS/Emotion、callai-server、数据库、Redis、OpenAI 兼容推理后端
仅回环同机服务间 HTTP、FRPS 代理落点、未启用远程管理的诊断端口

以下服务不得直接暴露公网:FreeSWITCH inbound ESL、callflow-esl outbound ESL、PostgreSQL、 Redis、OpenAI 兼容推理后端、callai-server 和内部 callflow-esl HTTP API。跨主机时使用安全组、主机防火墙、ACL、 应用 token 和必要的 TLS 形成多层限制。

还需满足:

  • 浏览器入口只用 HTTPS/WSS,HTTP 仅用于跳转或受控健康检查;
  • SIP/SDP 的 NAT 广告地址与实际公网映射一致;
  • TURN shared secret、ESL 密码、内部 token、数据库口令均由密钥系统或受限环境变量注入;
  • 录音和情绪音频只读发布,禁用目录索引、上传、脚本执行和任意路径穿越;
  • 公开 ASR/TTS 时配置限流、连接上限、超时和访问控制;
  • 日志不得记录 Authorization、内部 token、TURN shared secret 或完整数据库 URL。

5. 部署与验收

5.1 部署前检查表

  • 为每条连接填完源 IP、目标 DNS/IP、端口、协议、TLS、鉴权和负责人。
  • 从最终调用者主机验证 DNS 解析,不以运维电脑的解析结果代替。
  • 确认所有 127.0.0.1 都是有意的同机访问,所有 0.0.0.0 只出现在监听字段。
  • 检查 TTS、SIP/SDP、TURN、录音和情绪音频的公开地址。
  • 检查 FreeSWITCH RTP 与 TURN relay 范围不重叠,安全组和主机防火墙同时放行。
  • 校验证书域名、完整链、有效期和私钥权限。
  • 校准代理 WebSocket Upgrade、读写超时、缓冲和请求体限制。
  • 检查数据库 / Redis 备份、连接上限和跨机加密。
  • 确认配置片段中的所有 <PLACEHOLDER> 已在私有配置中替换,且仓库无真实凭据。
  • 记录生效命令、维护窗口和逐组件回滚版本。

各场景在通用清单之外还需完成以下检查:

场景上线前增量检查
单机纯内网确认所有回环地址确实在同一网络命名空间;TURN 保持关闭;无管理端口意外绑定公网网卡
单机公网 WebRTC核对 NAT 公网映射、WSS 证书、SIP/SDP 广告、RTP 与 TURN relay 两段防火墙规则
分层多机按层逐条审批源 → 目标规则;从边缘和应用层分别测试 AI / 数据层;确认没有反向开放无调用需求的端口
每服务独立核对内部 DNS、证书 SAN、代理超时、MTU、录音跨机发布和 TTS 回源;准备 DNS 变更时的长连接排空方案
当前双主机 + FRP确认 FRPS 代理只绑定回环、12015 不属于 FRP、WSS/RTP/TURN/录音均按附录直连
多 cell / API 高可用固定并登记每个 cellId;校验每 cell ESL、回调、录音 URL 和并发;验证 Redis 租约接管但不宣称在途呼叫迁移

5.2 逐层连通测试

在每个实际调用方主机执行,而不是只在服务本机执行:

bash
getent hosts fs-a.internal asr.internal tts.internal postgres.internal redis.internal
nc -vz fs-a.internal 8021
nc -vz callflow-esl-a.internal 9911
curl -fsS http://asr.internal:10096/health
curl -fsS http://tts.internal:9080/health
curl -fsS http://emotion.internal:9090/health
curl -fsS http://llm.internal:9930/health
curl -fsS http://llm.internal:9930/health
curl -fsS http://callflow-esl-a.internal:9912/health

健康接口通过后继续验证协议:

  1. 用模块自带客户端连接 ASR /audio,发送 16kHz 单声道 PCM16,确认 partial / final。
  2. POST /tts,从 FreeSWITCH 主机 GET 返回的 wavUrl;再验证 /tts-stream 和 MP3 Content-Type。
  3. 使用有效内部 token 调用 NLU / Chat capabilities,并完成一次 NLU 与 SSE Chat 请求。
  4. 用短期 TURN 凭据分别完成 UDP 和 TCP authenticated allocation,确认 relay 端口落入规划范围。
  5. 从浏览器完成 WSS SIP REGISTER,检查证书、realm、401 challenge 和成功注册。
  6. 从 callflow-esl / callout-server 主机验证 inbound ESL 认证和命令,从 FreeSWITCH 建立 outbound socket。
  7. 分别访问每个 cell 的录音 URL和情绪音频 URL,确认只读、Content-Type 正确且无目录列表。

5.3 真实呼叫验收

每种部署场景至少完成一通内网呼叫和一通真实公网 / 5G WebRTC 呼叫:

  • SIP 注册成功,WSS 无反复重连;
  • ICE 选中预期 host 或 TURN relay candidate;
  • DTLS 完成,双方 RTP 收发计数增长,双向有声;
  • FreeSWITCH 建立正确 cell 的 outbound ESL;
  • ASR 持续产生 partial,停顿后产生 final;
  • NLU / Chat 返回,TTS WAV 与流式 MP3 均可播放;
  • 通话结束后录音可通过该 cell 的 URL 访问;
  • Emotion worker 能获取录音、提交分析、写回结果,处理音频 URL 可播放;
  • callout 进度、最终结果、事件兜底和数据库状态一致;
  • 结束后没有遗留 ASR WebSocket、TTS stream、ESL 会话或 TURN allocation。

6. 常见错误

现象常见原因修正
FreeSWITCH 无法连接 outbound ESLdialplan 使用了 FreeSWITCH 自己的 127.0.0.1,而 callflow-esl 跨机改为 FreeSWITCH 可达的 callflow-esl 地址并开放 9911/TCP
originate 接通后无业务接管callbackHost 从 FreeSWITCH 不可达从 FreeSWITCH 主机测试该 host/port
ASR 健康但通话无识别只验证了 callflow-esl → ASR,实际连接者 FreeSWITCH 不可达从 FreeSWITCH 验证 WS 路径、子协议和防火墙
TTS 合成成功但无声TTS publicBaseUrl 只对 TTS 自己或 callflow-esl 可达从 FreeSWITCH GET 返回 URL,修正广告地址 / 代理
WSS 握手失败或很快断开Nginx 丢失 Upgrade / Connection,证书域名错误,超时过短修正 WebSocket 代理与证书,延长读超时
WSS 注册成功但无媒体RTP / TURN relay 未放行,SDP 广告私网 IP,ICE candidate 错误对照 SDP、candidate、DTLS 和双方 RTP 计数逐层检查
TURN control 可连接但 allocation 失败只开了 3478,relay 范围未开放或 secret / realm 不一致放行完整 relay 范围并校准两端配置
媒体偶发断流代理缓冲、MTU / MSS、NAT 空闲回收或读超时关闭流式缓冲,检查路径 MTU、keepalive 与超时
录音在一个 cell 可播、另一个 404所有记录错误共用同一个录音 base URLcellId 配置 cells[].media.freeswitchRecordingBaseUrl
情绪结果有短文件名但页面 404processedDir 与 Nginx alias / emotionAudioBaseUrl 不一致对齐物理目录、URL 前缀和权限
内部管理端口出现在公网扫描结果服务绑定全部网卡且安全组 / 防火墙缺少源限制,或 FRP 绑定公网改为私网 / 回环绑定并增加源 ACL
FRP 上游能访问但媒体质量差错把 RTP、TURN relay 或 SIP WSS 放进 FRP将实时媒体与边缘信令迁到公网边缘主机直连

附录 A:当前双主机生产拓扑

本节记录仓库当前部署参考,不是新的通用默认值。公开 IP 和域名是现有网络标识;凭据仍全部 使用占位符。实际变更前以现场配置、Debian 12 部署FRP 说明WebRTC ICE/TURN 指南交叉核对。

A.1 主机职责

主机组件网络职责
aliyunFreeSWITCH(已有常驻实例)、callflow-esl、FRPS、coturn、Nginx、三个 Web 前端 SPA 与文档站公网 HTTPS/WSS、SIP/RTP、TURN;通过回环 FRP 使用应用与 AI 上游;本地发布录音与 Web 前端
WSL / 计算主机 (/data/ai-voice)callflow-servercallout-servercallai-server、ASR、TTS、Emotion、PostgreSQL、Redis、MySQL、MinIO、FRPC提供应用、AI 与数据存储服务,通过 FRPC 隧道反向映射至 aliyun 本地回环

A.2 FRP 与 ESL 映射

FRPS proxyBindAddr="127.0.0.1",因此下表端口只供 aliyun 本机 Nginx 和 callflow-esl 使用:

aliyun 回环端口后端
12000MySQL
12001Redis
12002MinIO API
12003MinIO Console
12004PostgreSQL
12008TTS(WSL 29080
12009Emotion(WSL 29090
12010ASR(WSL 20096
12011callflow API(WSL 29913
12012callout API(WSL 29920
12013callai-server(WSL 29930,NLU / Chat / Conversation / Script)

120051200612007 当前未分配:本地推理后端不再经 FRP 暴露,三个 Web 前端由 aliyun 本机 Nginx 静态托管而非隧道回源。隧道清单以仓库内 deploy/wsl-docker/frpc/frpc.toml 为准, 使用说明见 WSL 计算集群部署

12015/TCPaliyun 上 FreeSWITCH inbound ESL,不属于 FRP。它应用 callflow-esl-inbound ACL,只允许同机回环和明确的 Debian 出口地址;不得按 12000-12013 的回环代理方式理解。

A.3 公网信令与媒体

能力当前地址路径
callflow Web/APIhttps://callflow.wangijun.comNginx 本地静态托管 SPA;/api/ → 回环 12011/api/auth12012(统一登录),/esl-api/ → 本机 9912
callout Web/APIhttps://callout.wangijun.comNginx 本地静态托管 SPA;/api/ → 回环 12012
callai Web/APIhttps://callai.wangijun.comNginx 本地静态托管 SPA;/api/ → 回环 12013/api/auth12012(统一登录)
平台文档站https://ai-voice.wangijun.comNginx 本地静态托管 ai-voice-docs 构建产物
浏览器 SIP WSSwss://fs.wangijun.comNginx TLS → 本机 FreeSWITCH 私网 7443/WSS
ASR WSSwss://asr.wangijun.com/audioNginx → 回环 FRP 12010,仅需跨边界的调用者使用
TTS 媒体https://tts.wangijun.comNginx → 回环 FRP 12008
情绪音频https://emotion.wangijun.com/emotion-audioNginx / 文件映射与 Emotion 输出目录对齐
FreeSWITCH 录音https://recordings.wangijun.comaliyun Nginx 本地只读 alias
FreeSWITCH RTP123.57.205.60:30000-30199/UDP公网直达 aliyun,不经 FRP
TURN control123.57.205.60:3478/TCP+UDP公网直达 coturn
TURN relay123.57.205.60:31000-31040/UDP公网直达 coturn,与 RTP 范围分离

vars.xml 当前用 172.21.38.245 绑定 SIP/RTP,用 123.57.205.60 作为 external_sip_ip / external_rtp_ip。Nginx 到 FreeSWITCH WSS 仍保持加密上游。FRP 明确不 承载 SIP WSS、FreeSWITCH RTP、TURN control、TURN relay 或录音文件。

A.4 当前拓扑验收重点

  1. aliyun12000-12013 仅监听 127.0.0.1,公网无法直连。
  2. 12015/TCP 的 ACL 只包含明确的 callflow-esl / callout 来源。
  3. WSS 注册的 SIP realm 为 fs.wangijun.com,上游仍为 7443/WSS
  4. 5G 浏览器选择 123.57.205.60:310xx relay,DTLS 完成且 RTP 双向增长。
  5. FreeSWITCH 的 RTP 落在 30000-30199,TURN relay 落在 31000-31040
  6. ASR、TTS、API 经 FRP 断开时故障可见,但不影响 SIP/RTP/TURN 的边缘端口归属。
  7. 录音从 aliyun 本地 alias 读取,TTS 与情绪媒体分别按各自公开 URL 回源。

文档与代码在同一仓库维护,现有 Markdown 是唯一内容源。