一句话定位

callout-server + callout-webpage 是平台的智能外呼子系统:管理”外呼活动 →
联系人名单 → 拨号节奏 → 拨打结果”的完整生命周期,把名单批量、可控、可重试地交给
FreeSWITCH 发起呼叫,接通后复用 callflow-esl 的业务编排能力。

分工记一句话:callout 负责”什么时候打、打给谁、结果如何”,callflow-esl 负责”接通之后说什么”。

除这条核心主线外,callout-server 已经是一个运营级外呼平台:角色鉴权与坐席工作台、
外呼策略中心与 DNC 免打扰、外呼线路与两层并发、异步名单导入、媒体资产与分角色转写、
业务结果模板、运行事件告警,以及通话后情绪分析。下文先把主线讲清,再逐块展开这些平台能力。

它和其它组件的关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
┌────────────────┐   HTTP    ┌────────────────────┐
│ callout-webpage│ ───────► │ callout-server │
│ (Quasar 19920) │ │ (Bun :9920) │
└────────────────┘ └─────────┬──────────┘
│ inbound ESL (8021)
│ originate {business_code=...}

┌────────────────────┐
│ FreeSWITCH │
└─────────┬──────────┘
接通后 dialplan socket │ (9911)

┌────────────────────┐
│ callflow-esl │ ← 业务编排(说什么)
└─────────┬──────────┘
│ POST /api/call-results

┌────────────────────┐
│ callout-server │ ← 回写拨打/业务结果
└─────────┬──────────┘
通话结束后排队 │ POST /analyze(可选)

┌──────────────────────────┐
│ emotion-analysis-server │ ← 通话后情绪分析
└──────────────────────────┘
  • callout-server 不通过 HTTP 调用 callflow-esl,而是直接连 FreeSWITCH inbound ESL
    下发 originate {business_code=...,...} <dialString> &socket(<callbackHost:callbackPort> async full)
  • originate 时把 business_code 与联系人 / 活动级 variables 写进 channel variable,FreeSWITCH
    接通后把这条腿通过 dialplan 的 outbound socket 送回 callflow-esl,由 business_code 选中业务;
  • 通话结束后,由 FreeSWITCH 事件监听 / callflow-esl / 业务侧把结果 POST /api/call-results
    回写给 callout-server(生产建议带 X-Callout-Internal-Token)。

核心概念

概念 说明
外呼活动 Campaign campaigns 一次外呼任务:绑定业务、主叫号码、并发、重试、工作时段、计划窗口等策略
联系人 Contact contacts 活动下的一个被叫号码及其当前状态、重试计数、最近结果快照
拨打尝试 Attempt call_attempts 一次 originate 的提交结果 + 整通通话生命周期 + 业务结果(含情绪字段)
派发批次 Batch callout_batches 一次手动或调度派发的统计(请求/认领/提交/失败数)
外呼线路 Line lines 拨号串模板 + 主叫号码 + 共享并发上限,多活动可共用一条线路
外呼策略 Policy callout_policies 策略中心:禁呼日期 / 节假日 / 时段 / 号码规则 / 频控 / 重试 / 并发,可全局或任务级
DNC 免打扰 dnc_numbers 全局免打扰名单,派发前独立拦截

数据表都在独立的 callout schema(与 callflow 的业务路由表分开),定义见
apps/callout-server/src/db/schema.ts,部署或本地初始化统一执行 bun run db:push。完整数据表见
下文「数据表总览」

活动与联系人生命周期

活动状态机

1
2
3
4
5
draft ──启动──► running ──暂停──► paused ──恢复──► running
│ │
└──────── 无可拨联系人 ────────────┘

completed ──归档──► archived
  • running 是唯一会被后台调度器自动派发的状态;
  • 当活动下没有 pending 也没有”到期可重试”的联系人时,自动标记为 completed

联系人状态机

1
2
3
4
pending ──派发──► dialing ──提交成功──► submitted ──结果回写──┬──► completed
▲ │ │
│ └──提交被拒/无人接听/检测为空号 ──► failed ┘
└────────── 手动 retry / 自动重试(到期) ──────────────────┘
  • failed 联系人在 retryCount < maxRetriesnextRetryAt 到期后变为可重试;
  • 手动重试通过 POST /api/campaigns/:id/retry 把匹配联系人重置为 pending
  • 运行中可用 POST /api/campaigns/:campaignId/contacts/pause 暂停指定联系人。

调度与节奏

外呼最怕”一拥而上打爆线路”或”在不该打的时间骚扰用户”。callout-server 用以下几个
活动级参数控制节奏:

参数 字段 作用
最大并发 maxConcurrency 同时处于 dialing + submitted 的联系人上限;派发时严格不超
拨号间隔 callIntervalMs 自动调度中两次 originate 提交之间的最小间隔,平滑外呼速率
工作时段 workdayStart / workdayEnd HH:mm;都设置时仅在窗口内自动派发,留空表示不限
计划窗口 scheduledStartAt / scheduledEndAt 活动整体的计划起止时间
最大重试 maxRetries 每个联系人的自动重试次数上限(默认 0,不自动重试)
重试间隔 retryIntervalSeconds 失败后多久变为可重试(写入 nextRetryAt

两种派发方式

  1. 手动派发 POST /api/campaigns/:id/dispatch:一次最多认领 100 个 pending
    联系人(limit 夹紧在 1..100),立即提交。适合小批量测试或人工触发。
  2. 自动调度:后台定时器(scheduler.intervalMs,默认 5s)扫描所有 running
    活动,逐个检查”是否在工作时段、是否到拨号间隔、是否有并发余量”,每轮最多认领
    min(scheduler.batchSize, campaign.maxConcurrency) 个联系人派发,并更新 lastScheduledAt 控制节奏。

启动前可调用 GET /api/campaigns/:id/preflight 预检任务状态、计划窗口、待呼名单、线路与策略;
运行中可用 POST /api/campaigns/:id/batches/:batchId/cancel 取消仍在 running 的调度批次。

并发模型与多实例

callout-server 派发联系人时使用两层并发限制

  • 任务并发 campaigns.maxConcurrency:限制同一活动内同时活跃的联系人数;
  • 线路并发 lines.maxConcurrency:限制共享同一外呼线路的所有活动的同时活跃联系人数。

活跃 = 状态为 dialingsubmitted。绑定线路的活动,一次批次实际派发数为:

1
min(请求批次大小, 活动剩余槽位, 线路剩余槽位)

未绑定线路时只用前两项;线路被禁用 / 删除时,派发会在提交 originate 前失败。

多实例部署用两层协调

  • Redis 锁(跨实例):一个全局调度器锁保证每轮只有一个实例扫描运行中活动;一个活动锁保证
    手动派发与调度器派发不会同时提交同一活动;
  • PostgreSQL 行锁(最终一致):派发事务先锁活动与绑定线路,再算可用槽位,然后用
    FOR UPDATE SKIP LOCKED 占用联系人——即使 Redis 配置错误或锁过期,同一联系人也不会被重复占用。

数据库锁刻意保持短生命周期(只覆盖批次创建、并发计算、联系人占用、lastScheduledAt 更新);
FreeSWITCH originate 提交发生在事务提交之后,网络调用期间不持有数据库锁。

角色与鉴权

启用鉴权后(auth.enabled,默认开启;本地联调可 CALLOUT_AUTH_ENABLED=false 关闭),
管理 API 按角色授权。POST /api/auth/login 返回 Bearer Token,GET /api/auth/me 下发当前用户的
permissions 列表,前端据此做菜单 / 路由 / 按钮门控(单一事实来源)。启用后会自动创建引导管理员账号。

角色 说明 关键权限
admin 系统管理员 全部权限(含用户、审计、线路)
supervisor 运营主管 活动 / 策略 / 联系人 / 导入 / 合规 / 坐席管理 / 坐席操作;不含 adminline:writeaudit:read
agent 话务坐席 contact:write + agent:operate(更新自身状态、软电话记录、领取 / 呼出 / 回拨 / 转人工);不能创建坐席

agent:write(创建坐席 / 坐席组)与 agent:operate(坐席自助操作)刻意分离:坐席本人可操作
软电话工作流,但不能管理坐席花名册。敏感操作(用户/角色管理、创建/更新/删除、派发、批量暂停、
导入错误 CSV 导出、媒体下载等)都会写入 audit_logs

坐席工作台与转人工

callout 不只做机器外呼,也支持人工坐席接管:

  • 坐席与坐席组 agents / agent_groups:坐席在线 / 忙碌 / 示闲 / 离线状态由 PATCH /api/agents/:id/status 维护;
  • 领取线索 POST /api/agents/:id/claim-contact:坐席领取联系人并生成跟进记录(follow_up_records);
  • 人工回拨 POST /api/agents/:id/callbacks:通过 FreeSWITCH originate 先呼坐席分机、再桥接客户
  • 人工外呼 POST /api/agents/:id/manual-calls:坐席直接呼出客户号码;
  • AI 转人工 POST /api/agents/:id/transfers:把 AI 通话线索转人工,生成软电话桥接呼叫。

坐席呼出 / 回拨 / 转人工都会在 originate 变量里写入 callout_softphone_call_id,记录在
softphone_calls。启用 freeswitchEvents.enabled 后,FreeSWITCH 的 CHANNEL_ANSWER /
CHANNEL_HANGUP / CHANNEL_HANGUP_COMPLETE 会回写软电话的接听 / 结束时间、时长与挂机原因,
避免记录长期停留在 submitted。坐席分机拨号串模板由 freeswitch.agentDialStringTemplate
(默认 user/{{agentExtension}})控制。

外呼策略中心与 DNC

活动 / 联系人级的”能不能打、什么时候打”由策略中心 callout_policies 统一管理,可在
全局任务级生效,执行语义为更严格优先:全局策略和任务策略都必须通过,任务级不能绕过全局禁呼。
POST /api/policies/evaluate 可试算某次派发是否被允许。策略类型:

类型 作用
blocked_dates 命中 YYYY-MM-DD 日期时阻断派发
holiday_blocklist 按节假日阻断,支持指定年份日期、每年重复日期与补班放行,如 { "dates": ["2026-02-17"], "recurringDates": ["01-01"], "workdayOverrides": ["2026-02-14"], "region": "CN" }
daily_window / weekly_window 仅允许指定每日 / 星期时段派发
phone_blacklist / phone_prefix_blocklist 按号码或前缀阻断单个联系人
contact_frequency_limit 限制同一号码最近 N 天最大触达次数,如 { "days": 7, "maxAttempts": 2 }
rate_limit / retry_rule / concurrency_limit 收紧呼叫间隔、重试与并发配置

旧版独立的 blocked_dates 表 / /api/blocked-dates 接口已并入策略中心:禁呼日期现在是
blocked_dates 这一策略类型,通过 /api/policies 维护。

DNC 免打扰名单 dnc_numbers 是全局的,派发前独立于策略表先行拦截。当 POST /api/call-results
回写 businessStatusopt_out / unsubscribe / do_not_call / dnc / rejected,或
businessResultoptOut / unsubscribe / doNotCall / dnc / rejectCall:true 时,平台会
自动把该号码沉淀到 DNC,后续派发直接拦截。

回铃音检测(可选)

普通外呼只能区分”接通 / 没接通”。开启回铃音检测后,callout-server 在彩铃/回铃阶段做两类检测:

  1. 运营商提示音:把早媒体音频 fork 到一个 ASR WebSocket(可直接复用 sherpa-asr-online-server 的
    /audio),用识别文本匹配运营商提示音关键词,从而在接人之前就判断号码状态:
提示音类别 典型话术
空号 empty_number “您拨打的号码是空号”
关机 powered_off “您拨打的电话已关机”
停机 out_of_service “您拨打的电话已停机”
无法接通 unreachable “您拨打的用户暂时无法接通”
忙 busy “正在通话中”
无人接听 no_answer 久振未接
  1. 回铃嘟声:在 B-leg 早媒体上启动 FreeSWITCH tone_detect(依赖 mod_spandsp),默认检测 450Hz,
    累计 5 次后判定为无人接听。

该功能由 config.jsonringbackDetection 配置块控制,也可用 CALLOUT_RINGBACK_DETECTION_*
环境变量覆盖:enabled代码默认关闭,但仓库内 config.json 默认开启)、wsUrlbugName
mixTypesampleRatedetectionTimeoutMs(默认 20000),以及嘟声检测的 toneDetectionEnabled
(默认随回铃检测启用)、toneFrequency(默认 450)、toneHits(默认 5)、toneTimeoutMs。开启后派发走
submitOutboundCallWithRingbackDetection(),检测结果写回对应 attempt,把”无人接听”和
“空号/停机”区分开,提升名单清洗与重试策略的准确性。实现见 src/freeswitch-ringback-client.ts

通话后情绪分析(可选)

启用 emotionAnalysis.enabled=true 并配置 endpoint(默认 http://127.0.0.1:9090/analyze)后,
callout-server 的后台 worker 会把已完成的呼叫尝试排队,把 attemptId、尽力提取的转写文本、
recordingUrltranscriptUrl 发到 情绪分析服务POST /analyze
再把融合后的情绪极性 / 分数保存到 call_attempts.emotion_* 字段,供按客户情绪筛选线索。

worker 的轮询间隔、并发与重试由 emotionAnalysis.intervalMs / maxConcurrency /
retryIntervalMs / maxRetries 控制。文本 BERT + 音频 wav2vec2 的推理细节见
情绪分析服务

异步名单导入

大名单导入走服务端异步任务,避免一次性请求超时:

  • POST /api/campaigns/:campaignId/import-files:上传 CSV/TSV,创建服务端暂存行异步导入任务;
  • POST /api/campaigns/:campaignId/import-jobs:以 JSON 联系人行创建异步导入任务;
  • 导入支持号码归一化、CN 地区格式校验、运营商限制、固话开关;失败行进入错误明细 / CSV;
  • 进度与结果:GET /api/import-jobs/api/import-jobs/:id/errors/api/import-jobs/:id/errors.csv

任务相关数据在 import_jobs / import_job_rows / import_job_errors,由 importJobs worker 处理。

坐席间流转与转人工会议

AI 外呼通话「转人工」不是简单挂断重拨,而是把客户腿放进一个 FreeSWITCH 会议室,再把坐席
拉进同一个会议室,从而支持盲转、协商转(三方)和主管旁听。callout-server 用 conference_sessions
表登记每个会议会话(会议室 ID、关联 attempt/contact、客户腿 UUID、当前主坐席及其软电话腿、状态)。

1
2
3
4
5
6
AI 通话中 ──转人工──► callflow-esl 把客户腿移入会议室
│ 登记 conference_sessions

callout-server ──originate 坐席分机入会──► FreeSWITCH 会议室 ◄── 客户腿
(会议成员 API) ▲
主管 monitor/barge ────────┘

会议成员 API(成员动态进出的原语):

操作 端点 说明
加成员 POST /api/conference-sessions/:id/participants role=agent(默认)拉坐席:带 agentId 指定、否则空闲池预占;replacePrimary=true盲转(加新坐席后踢旧主坐席并切 primary),否则为协商转 / 三方(primary 不变)。role=supervisor主管介入:必须带 agentId,不占坐席池,mode=monitor 静默监听(mute)、mode=barge 可发言
减成员 DELETE /api/conference-sessions/:id/participants/:softphoneCallId 对成员腿 uuid_kill:协商转完成(移除旧主坐席)/ 取消(移除新坐席)/ 主管退出;移除主坐席时自动把 primary 提升为剩余坐席腿
监控 GET /api/conference-sessionsGET /api/conference-sessions/:id 列表 / 详情,附 FreeSWITCH 实时成员数与 live / ended / unknown 状态(只读不写库)

坐席入会通过 inbound ESL originate 坐席分机、业务编码固定 agent-conference-guest。AI 业务侧发起
转人工时,先由 callflow-esl 调用公开的 POST /api/transfers/route 预占空闲坐席并拿到坐席分机。

僵尸会话兜底conference_sessions.status 只在坐席腿挂机事件命中收尾路径时才翻 ended
客户先挂断、事件丢失或进程重启会留下「FS 里会议已没、库里仍 active」的僵尸行。后台
conferenceReconcile worker(默认每 30s、60s 宽限期)用实时成员数校准:成员数为 0 且过宽限期即
回收卡住的坐席腿、挂断残留客户腿、关闭会话并记 conference_session_reconciled 事件;FreeSWITCH
不可达时整轮跳过,绝不误关。

前端 callout-webpage实时作战大屏转人工会议监控面板即基于这组 API。

结果回写

callout-server 只把 call_attempts.status 当作 originate 提交结果
(accepted / rejected / error)。整通通话的生命周期与业务结果由外部回写:

1
2
3
POST /api/call-results
Content-Type: application/json
X-Callout-Internal-Token: <internalApi.callResultToken>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"attemptId": 123,
"callStatus": "completed",
"hangupCause": "NORMAL_CLEARING",
"answeredAt": "2026-06-03T09:00:00.000Z",
"endedAt": "2026-06-03T09:01:20.000Z",
"durationSeconds": 80,
"businessStatus": "success",
"intentLevel": "A",
"intentCategory": "interested",
"resultSchemaCode": "default_intent_v1",
"summary": "用户有兴趣,要求下午回电",
"recordingUrl": "http://example.local/recordings/123.wav",
"transcriptUrl": "http://example.local/transcripts/123.json",
"businessResult": { "callbackTime": "15:00" }
}
  • 可用 attemptIdrequestIdcampaignId + contactId 定位 attempt;
  • intentLevel / intentCategory / resultSchemaCode 作为标准筛选字段保存(模板由
    /api/result-schemas 维护,可定义意向等级、意向分类与字段元数据);businessResult
    不透明 JSON 继续保存具体业务的扩展数据;
  • callout 据 callStatus + businessStatus 推导联系人最终状态(completed / failed),并决定是否
    安排重试、是否把活动标记 completed,以及是否把号码沉淀到 DNC。

录音 / 转写等媒体由 media_assets / transcript_segments 管理:GET/POST /api/media-assets
GET /api/media-assets/:id/download(过期返回 410)、POST /api/media-assets/cleanup-expired
以及分角色(机器人 / 客户 / 坐席)转写片段 GET/POST /api/media-assets/:id/transcript-segments
录音文件本身仍由 FreeSWITCH / callflow 侧生成并提供访问,callout 只保存并返回 URL。

运行事件与告警

runtime_events 记录调度、生命周期补偿、导入、会议对账等后台运行事件。管理端通过
GET /api/runtime/overviewGET /api/runtime/events(SSE 实时流)展示,PATCH /api/runtime/events/:id/acknowledge 确认告警。配置 notifications.webhookUrl 后,达到
notifications.minLevel 的事件会同步发送 webhook。常见事件:调度器未捕获异常 →
scheduler_failed;生命周期超时补偿 → lifecycle_timeout_reconciled;导入完成 / 失败 →
import_job_completed / import_job_failed;情绪分析 worker 异常 →
emotion_analysis_worker_failed;会议对账收尾僵尸会话 → conference_session_reconciled

服务启动时按配置拉起 5 个后台 worker(均 setInterval 驱动并带重入保护):调度器
scheduler、生命周期对账 lifecycleReconcile、会议对账 conferenceReconcile、异步导入
importJobs、通话后情绪分析 emotionAnalysis,外加一个 FreeSWITCH inbound ESL 事件监听器
freeswitchEvents,回写接听 / 挂机兜底)。

HTTP API 一览

所有响应统一用 { ok, data } / { ok, error } 信封。下表按功能分组列出主要端点,完整清单
apps/callout-server/src/router.ts 与该项目 README.md 为准。

分组 方法 路径
健康 / 鉴权 GET /health · POST /api/auth/login · POST /api/auth/logout · GET /api/auth/me · GET /api/roles/permissions
看板 GET /api/overview · GET /api/overview/trend
业务来源 GET /api/businesses(来自 callflow.call_businesses
活动 GET/POST /api/campaigns · GET/PATCH/DELETE /api/campaigns/:id · GET /api/campaigns/:id/preflight
联系人 GET/POST /api/campaigns/:id/contacts · PATCH/DELETE …/:contactId · POST …/contacts/pause
派发 POST /api/campaigns/:id/dispatch · POST /api/campaigns/:id/retry · POST /api/campaigns/:id/batches/:batchId/cancel
呼叫明细 GET /api/call-attempts(支持按 intentLevel/intentCategory/resultSchemaCode 筛选) · POST /api/call-results
线路 GET/POST /api/lines · PATCH/DELETE /api/lines/:id
策略 / DNC GET/POST /api/policies · PATCH/DELETE /api/policies/:id · POST /api/policies/evaluate · GET/POST /api/dnc · DELETE /api/dnc/:id
坐席 GET /api/agents · POST /api/agents · POST /api/agent-groups · PATCH /api/agents/:id/status · POST /api/agents/:id/claim-contact · …/callbacks · …/manual-calls · …/transfers · GET /api/softphone-calls · GET/POST /api/follow-ups
导入 POST …/import-files · POST …/import-jobs · GET /api/import-jobs · /api/import-jobs/:id/errors[.csv]
媒体 / 结果模板 GET/POST /api/media-assets · …/:id/download · …/cleanup-expired · …/:id/transcript-segments · GET/POST /api/result-schemas · PATCH /api/result-schemas/:id
转人工会议 POST /api/transfers/route(公开回调) · GET /api/conference-sessions[/:id] · POST /api/conference-sessions/:id/participants · DELETE /api/conference-sessions/:id/participants/:softphoneCallId
运行 / 审计 GET /api/runtime/overview · GET /api/runtime/events(SSE) · PATCH /api/runtime/events/:id/acknowledge · GET /api/audit-logs
用户 GET/POST /api/users · PATCH/DELETE /api/users/:id

数据表总览

表都在 callout schema,定义见 src/db/schema.ts

作用
campaigns 外呼任务:业务、主叫号码、并发 / 重试 / 间隔 / 工作时段 / 计划窗口 / 状态 / 变量
lines / line_numbers 外呼线路与号码池:拨号串模板、共享 maxConcurrency、号码分配模式(一号多并发 / 号码池轮换 / 一号一并发);一条线路可挂多个主叫号码并带号码级并发与轮换权重
contacts 活动中的单个被叫:号码、状态、重试计数、最近呼叫 / 业务结果快照
call_attempts 一次 originate 尝试:提交状态、通话状态、挂机原因、时间、业务结果与情绪字段
callout_batches 一次派发批次(manual / scheduler)的请求 / 占用 / 提交 / 失败计数
callout_policies 策略中心:禁呼 / 节假日 / 时段 / 号码规则 / 频控 / 重试 / 并发
dnc_numbers DNC 免打扰名单
users / auth_sessions 管理台用户、角色与会话
audit_logs 敏感操作审计
agents / agent_groups 坐席与坐席分组
follow_up_records 人工跟进记录
softphone_calls 坐席软电话呼出 / 回拨 / 转人工的提交与 FS 接听 / 挂机状态
import_jobs / import_job_rows / import_job_errors 异步导入任务、暂存行、号码校验与错误明细
media_assets / transcript_segments 录音 / 转写资产与分角色转写片段
conference_sessions 转人工会议会话:会议室 ID、关联呼叫 / 联系人、客户腿 UUID、当前主坐席及软电话腿、状态
result_schemas 业务结果模板:意向等级、意向分类、可结构化字段定义
runtime_events 调度、补偿、会议对账、告警等运行事件

配置

运行时配置来自 apps/callout-server/config.json,可被 CALLOUT_ 前缀环境变量覆盖。常用分组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"server": { "host": "0.0.0.0", "port": 9920, "allowedOrigins": ["http://localhost:19920"] },
"auth": { "enabled": true, "sessionTtlSeconds": 86400 },
"scheduler": { "enabled": true, "intervalMs": 5000, "batchSize": 20 },
"redis": { "host": "127.0.0.1", "port": 6379, "keyPrefix": "callout", "lockTtlMs": 60000 },
"emotionAnalysis": { "enabled": true, "endpoint": "http://127.0.0.1:9090/analyze" },
"internalApi": { "callResultToken": "" },
"freeswitch": {
"host": "192.168.2.184", "port": 8021, "password": "ClueCon",
"callbackHost": "192.168.2.246", "callbackPort": 9911,
"originateDialStringTemplate": "user/{{destinationNumber}}"
},
"db": { "url": "postgres://postgres:postgres@localhost:5432/freeswitch" }
}
字段 说明
server.host / port / allowedOrigins HTTP 监听与管理台跨域白名单
auth.enabled / sessionTtlSeconds 角色鉴权开关与会话有效期
scheduler.* 后台调度开关、轮询间隔、每轮每活动批量
redis.* 多实例分布式锁(缺省单实例运行)
emotionAnalysis.* 通话后情绪分析开关、端点、超时 / 并发 / 重试
internalApi.callResultToken /api/call-results 内部回写令牌(请求头 X-Callout-Internal-Token
freeswitch.callbackHost / callbackPort callflow-esl outbound socket 地址,必须 FS 可达
freeswitch.originateDialStringTemplate 默认拨号串模板,活动可用 dialStringTemplate 覆盖
db.url PostgreSQL;callout 从中派生 calloutcallflow 两个 schema

lifecycleReconcile / conferenceReconcile / importJobs / notifications / ringbackDetection /
freeswitchEvents 等带内置默认值的分组、以及完整字段表与环境变量覆盖,见 配置参考
callbackHost / callbackPort 与 callflow-esl 同样的”回拨地址”陷阱在这里同样成立,参见
部署拓扑 的网络陷阱章节。

前端 callout-webpage

Quasar 前端,开发端口 19920,通过 VITE_CALLOUT_API_BASE_URL(默认
http://127.0.0.1:9920)连接 callout-server,并按 GET /api/auth/me 返回的 permissions
做菜单 / 路由 / 按钮门控。主要界面:

  • 看板:活动 / 联系人 / 尝试计数与接通、完成、失败率、平均时长,以及趋势图;
  • 活动列表与详情:新建 / 编辑活动,启动 / 暂停调度;联系人、拨打尝试、派发批次三联面板,
    支持导入名单、手动派发、重试、暂停、增删改联系人;
  • 策略 / DNC / 线路:策略中心、DNC 名单、外呼线路与号码池维护(含号码分配模式);
  • 运营监控:实时并发、调度批次、卡住联系人、近 1 分钟提交 / 失败与运行事件告警(SSE);
  • 实时作战大屏:聚合运行态势、通话流时间线、坐席状态墙、进行中转人工会议看板、线路 / 任务容量与实时事件,面向运营与演示;
  • 转人工会议监控:查看进行中会议与成员腿,盲转 / 协商转目标坐席、主管监听 / 插话、移除成员,并显示 FreeSWITCH 实时成员数;
  • 坐席工作台:坐席状态、领取线索、人工外呼 / 回拨 / 转人工、软电话记录;
  • 呼叫明细:按意向等级 / 分类 / 结果模板筛选线索,命中 recordingUrl 可直接播放,并展示通话后情绪分析(极性 + 分数 + 文本 / 音频原始结论)。

下一步