feat: add configurable Bailian AI cell call runtime

This commit is contained in:
2026-09-14 23:10:39 +08:00
parent 9bade94ada
commit 5dd69b8779
36 changed files with 6455 additions and 30 deletions
@@ -0,0 +1,196 @@
# agent-call:LLM 与音色可配置电话对话开发与验收计划
**版本:** v1.0
**日期:** 2026-09-14
**状态:** P1/P2 配置契约与 Bailian 真实 ASR/LLM/CosyVoice 文件链路已落地并验收;P3/P4 真实电话、MQ/OSS 和人工听测仍未完成。本文仍是范围与门禁计划,不是完整真实电话完成报告。
**目标:** 指定 LLM 模型、Prompt、TTS 模型与音色,一键完成文本、音频文件或真实电话多轮对话测试。
## 1. 权威来源与当前基线
- [最终计划](最终开发部署监控与验收计划_v1.0.md)仍是总实施入口;本文细化 D04、D06、D07、D08、D11、D12 的单通话 AI 联调,不替代整体调度、恢复与生产验收。
- [主契约](SaaS交互_OpenAPI与MQ契约规划_v0.1.md)是 HTTP/MQ 字段、路径、状态的唯一维护来源。本文的新增配置项、命令行和报告项是设计要求,不是已冻结的业务协议。
- 已有机器契约见 `docs/contracts/executor.openapi.yaml`、`saas.openapi.yaml`、`mq.schema.json`,集中命令样例见 `docs/contracts/examples/call.execute.json`。
- 用户反馈最新外呼已正常接通;2026-09-14 最近探针记录 100→183→200、PCMA/8000,结束后无活动通道。该事实只作为 SIP 接通基线,不证明双向媒体、ASR、LLM、TTS、录音及完整 SaaS 交接通过。
- `deploy/sip_probe.py` 是真实 SIP 拨号探针,不是完整 AI 通话执行器;`agent_call/core.py` 的 `_run_call` 仍包含模拟通话流程,`agent_call/mocks.py` 为模拟记录,不能用它们的成功事件替代真实媒体证据。
- 仅复用现有 ASR 协议能力,不复制或启用 `voice_test` 的 LLM/TTS;`agent_call/bailian.py` 使用独立 Bailian LLM/CosyVoice 适配。ASR Web 页面仍不承载 LLM/TTS,未配置供应商时页面、日志与报告必须明确标记阻塞或模拟。
## 2. 范围与不可变边界
### 2.1 首期交付
1. 可信、不可变的 AI 配置版本:可指定模型、UTF-8 Prompt 文件及音色。
2. 首家 LLM 与首家 TTS 的真实流式适配,连接现有 ASR。
3. Cell 内真实 ARI 事件、双向媒体和电话多轮对话;支持开场白、打断、静音处理和挂断清理。
4. `text / audio / call` 三层一键测试,以及脱敏机器报告、人工听测登记和证据索引。
5. 沿用现有 MQ 结果、文字与 OSS 录音交接,不建立第二条业务结果通道。
首期不做 Prompt 可视化编辑器、RAG、通用供应商框架、多供应商自动切换、工具调用平台或 1000 路容量承诺。供应商接入不足不阻塞隔离 Mock 开发,但阻塞对应真实验收。
### 2.2 必须遵守
- 电话测试脚本扮演测试 SaaS;通过租户独立 RabbitMQ 队列提交 `call.execute`,引用 `agent_version_id`。不得新增 HTTP 拨号入口或 HTTP 业务回调。
- AI 配置发布与普通呼叫权限隔离;不借用 SIP 管理后台写接口或普通调度 Token 绕过配置权限。
- MQ 不携带 API Key、任意供应商 URL 或任意 SIP 地址。供应商地址、凭据引用和模型授权由服务端校验。
- 一通电话固定 Cell/出口;主叫、被叫改写和 PCMA 由授权线路配置解析,不逐呼修改共享 PJSIP 配置,不虚构备用线路。
- 业务持久状态使用真实隔离数据库,集成测试使用真实测试 RabbitMQ;内存队列仅可作单元测试替身。
- `mock / mixed / real` 按组件可识别;Mock 默认隔离真实外网;正式模式拒绝 Mock/测试凭证,不静默降级。
- 本文不授权新建云资源、真实 AI 消费或立即拨号。真实执行另需有效预算、凭据和明确安排的测试授权。测试后保留现有 ECS/EIP,清理需用户另行确认。
## 3. 配置与契约工作包 P1
### 3.1 建议配置内容(待写入主契约冻结)
| 配置组 | 内容 | 校验要求 |
| --- | --- | --- |
| LLM | 供应商配置引用、模型、Prompt、生成参数、超时 | 模型受授权且支持所需流式协议;参数范围与供应商能力一致 |
| TTS | 供应商配置引用、模型、音色、语速、音频格式 | 音色与模型兼容;输出格式可转换为电话侧音频;不静默替换 |
| ASR | 现有供应商配置引用、语言、输入格式 | 明确采样率、声道、位深与分段/最终稿语义 |
| 对话 | 开场白、断句、打断开关、静音处理、最长时长 | 有界等待和输出;失败行为明确,不无限循环 |
| 安全与版本 | 租户范围、不可变版本、内容摘要、凭据引用 | 版本发布后不可覆盖;跨租户拒绝;密钥不进入快照 |
- CLI 读取 UTF-8 Prompt 文件;冻结模板变量的允许集合、转义规则、缺失处理、长度上限,不使用 `eval`、shell 或任意文件引用。
- 先发布并确认可信快照,再提交引用该版本的执行命令。复用版本必须校验内容及租户一致,不能覆盖同 ID 的不同内容。
- 呼叫受理和实际发起前验证版本存在且获授权;活动通话固定原快照,后续发布不热替换。
- 配置同步来源、发布/读取路径、认证域、错误与版本规则先补主契约,再生成对应 OpenAPI、Schema 与正反例;不在本文擅定新业务路径、状态或错误码。
- OpenAPI 是 HTTP 接口描述,不能据此默认采用 OpenAI 兼容协议。LLM/TTS 的请求、SSE/WebSocket、取消、错误和音频契约以选定供应商文档为准。
### 3.2 P1 退出条件
配置的合法/缺失/越权/冲突/模型音色不兼容用例通过;HTTP/MQ 样例与机器契约一致;脚本可取得可信 `agent_version_id`,而不是向 MQ 额外塞入未定义字段。
## 4. 执行与媒体链路
```text
测试脚本 → 可信配置发布/读取 → agent_version_id
→ RabbitMQ call.execute → 持久化/公平调度/资源预留
→ Cell 本地执行器 → ARI 发起与真实接通事件
→ 入站媒体 → ASR → LLM → TTS → 出站媒体 → 电话播放
→ 持久化文字/状态/终态 → outbox → RabbitMQ
→ 接通后录音 → 挂断封口 → OSS 校验 → MQ 录音结果
```
### 4.1 媒体实现要求
- Cell 保持唯一活动 ARI 控制者。按实际 Asterisk 能力选定一种双向媒体接入方式并验证,不同时实现多套媒体方案;管理连接保持回环或受控管理网/TLS,不开放公网 ARI。
- SIP 侧沿用 PCMA/8000;ASR/TTS 侧按真实供应商要求转换采样率、声道、编码及帧格式,优先复用现有可靠音频组件。用实测验证节拍、字节序、时长、音量、丢包与缓冲上限。
- ASR 中间稿不直接无限触发 LLM;按最终稿/端点检测形成轮次。冻结 VAD、断句和去重策略,避免机器人回声形成自问自答。
- LLM 增量输出按有界句段送 TTS;上下文、待合成文本、待播放音频均有上限与背压,不无界积压。
- 每轮具备内部轮次/取消标识;打断时取消 LLM/TTS、清空可清理的播放缓冲、丢弃旧轮次迟到包。已进入远端缓冲的音频不能宣称完全撤回,记录可测停止边界。
- 用户挂断、控制挂断或时长到期后,停止新增生成并清理媒体/AI连接;ARI 失联进入既有对账流程,不凭租约超时直接释放未知通话占用或重拨。
- 生成文字、音频已发送与播放确认分开记录;没有播放证据时维持主契约允许的未知状态,不伪称用户已听到。
- 录音按主契约接通后开始、单轨混音、挂断封口;OSS 交接失败保持可恢复事实,不产生虚假 ready。
## 5. 开发顺序与交付物
| 工作包 | 开发内容 | 交付物与退出条件 | 依赖 |
| --- | --- | --- | --- |
| P1 配置契约 | 配置发布、授权、版本、参数及供应商协议冻结 | 主契约增补、机器契约、正反例;满足第3节 | 无;供应商资料并行协调 |
| P2 独立 AI | 一家流式 LLM、一家流式 TTS、现有 ASR 接入;取消/错误与模式隔离 | 文本测试、音频文件测试;指定模型/Prompt/音色有请求及输出证据 | P1;真实测试需供应商资料 |
| P3 真实通话 | Cell 执行路径、真实 ARI/RTP、音频转换、录音衔接 | 手机能听到 AI、ASR 能识别用户并驱动回答;不是探针 Wait 或 Mock 成功事件 | P2、现有调度与Cell缺口整改 |
| P4 生命周期 | 开场白、上下文、打断、静音、超时、挂断、断连对账 | 三轮对话、取消迟到包、异常清理和幂等故障测试 | P3 |
| P5 脚本与验收 | 分层入口、预检、持久运行清单、事件收集、报告及人工听测登记 | 第7节逐例登记,文档命令可复现 | P1起增量开发,最终依赖P4 |
本次产物位置:`scripts/test_ai_call.py`、`scripts/run_real_cell_call.py`、`scripts/accept_ai_call.py`、`configs/ai-test.example.yaml`、`configs/ai-test.bailian.example.yaml`、`prompts/test-call.txt`、`agent_call/ai_runtime.py`、`agent_call/bailian.py`、`agent_call/real_cell.py`、`tests/test_ai_runtime.py`、`tests/test_bailian.py`、`tests/test_real_cell.py`、`docs/contracts/ai-config.openapi.yaml`、`docs/contracts/ai-config.schema.json`、`docs/evidence/llm-voice-acceptance-20260914.json` 和 `docs/evidence/real-cell-call-20260914.json`。AI/媒体运行代码已提供真实 Cell ARI/RTP worker,但单次真实电话未通过媒体/三轮门禁。Mock `text`/`audio`/`call` 与 Bailian 真实 `text`/`audio` 已可复现;脱敏报告同时提供 Markdown 摘要。
## 6. 一键测试规范
### 6.1 分层入口
| 模式 | 输入/结果 | 外部副作用 |
| --- | --- | --- |
| `text` | 模型、Prompt、测试文本 → 流式回答和报告 | 不拨号;真实模式会调用 LLM |
| `audio` | 合成/已授权音频 → ASR文字、LLM回答、TTS音频和报告 | 不拨号;真实模式会调用 AI |
| `call` | 授权号码、配置版本与线路策略 → 电话对话和 MQ/录音证据 | 会拨号及调用 AI,必须显式授权 |
**已实现的 Mock 与真实文件入口:**
```bash
python3 scripts/test_ai_call.py call \
--profile configs/ai-test.example.yaml \
--model 'mock-chat-v1' \
--prompt-file prompts/test-call.txt \
--tts-model 'mock-tts-v1' \
--voice 'mock-neutral' \
--callee '18601013734' \
--max-duration 120 \
--allow-real-call
```
`120` 是该次测试时长示例,不是生产额度。Bailian real `text`/`audio` 只会调用真实 AI、不拨号;real `call` 仍需 Cell/线路/MQ/OSS 与明确电话授权,不能因文件探针通过而自动发起收费电话。
### 6.2 执行步骤及安全
1. 预检配置权限、供应商能力、凭据引用、租户绑定、MQ、必要存储依赖及线路/Cell可用性;配置错误在拨号前拒绝。预检不等于实际发起时的资源租约,发起前仍须复核。
2. 发布/复用配置版本;持久保存运行清单及 `command_id / execution_id / trace_id / agent_version_id`,再提交 MQ。
3. 一次运行只呼叫一个授权号码,不自动重拨或自动切换供应商。broker 确认不确定时保留原执行标识,按主契约恢复;脚本重启不得默认创建新执行。
4. 事件由专用测试 SaaS inbox 按既有事件拓扑受控收集,不竞争消费生产 SaaS 队列,不改为一租户一事件队列。报告只关联本次租户和执行。
5. CLI 等待超时不代表通话已结束。通过既有查询/授权控制和 Cell 事实核验;无法核验则记录不确定、停止新执行并提示处置,不全局强挂其他通话。
6. 正常结束核验本次通道/连接/资源已清理,并等待有界的结果与资产交接;未完成项标记失败或阻塞,不伪报整体通过。
7. 提供明确非零失败退出码;建议 `0=全部要求通过、1=测试失败、2=前置阻塞或证据不足`。人工听测未完成时不能退出为整体验收通过。
### 6.3 报告与证据
- 运行 ID、时间、代码/组件版本、profile 和逐组件模式、执行标识、配置摘要、所用线路/Cell。
- 请求模型及供应商返回模型(若不返回,标记未验证)、Prompt 摘要、TTS模型/音色;不写入密钥或默认公开完整 Prompt。
- SIP 接通证据、双向媒体计数、ASR分段、LLM首 token、TTS首音频、实际可观测播放状态、挂断与清理事实。
- 单调时钟测量 ASR 最终稿、LLM首 token、TTS首音频、用户话音结束至回复播放、打断停止延迟;跨组件时钟不可直接相减,报告测量边界。
- MQ事件ID、持久化/发布确认情况、OSS交接证据、逐例 PASS/FAIL/BLOCKED、人工听测结论。
- 原始证据保存在受控忽略目录并设置最小访问权限;`docs/evidence/` 只收录脱敏摘要。录音、完整文字、号码与Prompt按登记保留期限清理,未经明确保留规则不发布真实内容。
## 7. 功能验收矩阵
所有用例记录模式、环境、输入、操作、预期、实际、证据路径和结论;计划中默认均为 **NOT_RUN(未执行)**。执行后只使用 PASS/FAIL/BLOCKED,不以“代码存在”替代测试。
| 编号 | 场景与操作 | 通过标准/必要证据 |
| --- | --- | --- |
| AI-01 | 发布配置并提交版本引用 | 命令通过现有Schema;可信快照与租户一致;MQ不含密钥/任意URL |
| AI-02 | 缺失版本、越权版本、同版本异内容 | 明确拒绝;无真实INVITE、无供应商调用;原已发布版本不变 |
| AI-03 | 非法模型、音色不兼容、Prompt变量缺失/越界 | 可本地确定的问题拨号前拒绝;供应商才可判定的错误明确留证,不替换模型/音色 |
| AI-04 | 修改配置后再次执行 | 新执行使用新版本;活动通话继续原版本;摘要可核对 |
| AI-05 | 指定模型与Prompt进行文本测试 | 请求模型、Prompt摘要准确;回答符合预置可核验约束;流式首token及终止有证据 |
| AI-06 | 指定TTS模型与音色合成 | 请求参数准确;音频可解码、时长/格式正确;人工确认可听;不以请求参数单独证明音色听感 |
| AI-07 | 音频文件走完整AI链 | ASR结果、LLM回答、TTS文件均关联同一测试;非固定Mock文字;样本预期与人工复核通过 |
| AI-08 | 真实电话至少三轮问答 | 真实接通、双向媒体和三轮识别/回答证据齐全;手机端人工确认清晰可听、上下文连续、无明显自问自答 |
| AI-09 | 开场白及指定Prompt约束 | 接通后按配置播放开场白;预置问答符合Prompt;未配置开场白不擅自生成固定营销话术 |
| AI-10 | PCMA/PCM转换及持续音频 | 格式、采样率、时长、帧节拍可核验;无明显变速/截断;队列有界 |
| AI-11 | AI播放中用户插话,注入迟到包 | 取消生成/合成并清理播放队列;旧轮次包不继续入队;停止延迟满足预登记测试阈值 |
| AI-12 | 用户持续静音或无人应答 | 按配置有界等待/结束;不无限生成;无人应答不记为已接通AI通话 |
| AI-13 | 用户挂断、授权控制挂断、最长时长到期 | 本次AI任务/媒体停止;已知结束的资源释放;其他通话不受影响 |
| AI-14 | 鉴权错误、429、生成超时、ASR/TTS断流 | 分项错误与终态可靠留证;无静默Mock/模型切换、无自动重拨;仅按已冻结失败策略执行 |
| AI-15 | ARI断连、执行器重启、活动状态未知 | 保留执行事实并进入对账;不生成新ID重拨、不释放未知活动占用;恢复后事实收敛 |
| AI-16 | MQ重复、发布确认丢失、脚本重启 | 同一执行最多一次实际拨号;清单可恢复;结果按事件ID去重,不绕过原租户调度域 |
| AI-17 | 文字/播放及MQ故障 | 最终稿可靠持久;生成/发送/播放证据不混淆;outbox恢复且不产生第二套业务回调 |
| AI-18 | 录音正常上传、OSS超时/校验失败 | 接通后录制、挂断封口;真实字节独立校验后才返回OSS ID/ready;失败保留恢复证据 |
| AI-19 | 未授权真实调用、Mock混入real、凭据缺失 | 启动/预检拒绝;无真实拨号/收费调用;报告逐组件模式准确 |
| AI-20 | 一键执行完整流程 | 单命令完成配置、预检、MQ提交、收集与报告;无手工改PJSIP;退出码匹配结论;人工未确认不报全通过 |
| AI-21 | 日志/报告安全与本次资源收尾 | 无密钥;公开证据脱敏;本次已结束通道/媒体/AI资源无泄漏;ECS/EIP不自动清理 |
### 7.1 延迟与质量门禁
- 供应商选定后、真实测试前登记:ASR准确性样本与判据、LLM首token、TTS首音频、端到端回复、打断停止、静音等待和资源清理的阈值及测量方法。
- 阈值由实际供应商能力与测试预算确认;不得在看到结果后调宽阈值再宣称通过。未冻结只记录实测,相关性能用例标记 BLOCKED。
- 单通话测试报告样本数与每次观测;少量样本不宣称生产p95/SLA。真实1000路、N+1、供应商额度与压测继续按最终计划单独验收。
## 8. 自动验证与交付门禁
- Python:`python3 -m unittest discover -s tests -v`;新增覆盖配置拒绝、流式解析、取消迟到包、幂等恢复与模式隔离的最小回归测试。
- Go ASR有改动时:`cd services/asr-web && go test -race ./...`,并格式化和静态检查。
- 音频前端或PCM有改动时:`node --test tests/test_pcm.cjs`;Asterisk Shell有改动时:`bash -n deploy/asterisk.sh`。
- 修改后的OpenAPI/Schema解析与正反例校验、`git diff --check` 必须通过。
- 集成证据必须使用隔离真实DB/RabbitMQ、真实Asterisk及协议级模拟对端;仅写ProtocolJournal日志不算协议集成通过。
- 分别报告 Mock、真实ASR、真实LLM/TTS、真实SIP/媒体、MQ/OSS、人工听测结果。完整真实电话验收要求所有必需组件为real且相关用例通过;mixed通过只证明其列出的真实部分。
- 交付:代码/配置示例/运行说明、逐例验收报告、脱敏证据索引、未完成项与阻塞项;文档中的预计脚本只有实现并验证后才能标记可用。
## 9. 外部待办与开工顺序
| 待提供/确认 | 影响 |
| --- | --- |
| Bailian 首家供应商的模型、流式/取消协议、配额与预算 | 文件链路已按当前协议验证;新增模型/能力仍需单独登记 |
| Bailian TTS 自定义音色与音频格式 | CosyVoice 文件合成已验证;人工听测和电话侧格式仍待验收 |
| 受控凭据、服务端允许的供应商地址与租户授权 | 文件探针已使用环境凭据;电话执行仍需 Cell/线路授权 |
| 配置发布/同步来源、权限域与版本契约 | P1冻结后才能宣称OpenAPI对接完成 |
| 真实MQ/数据库/OSS及测试SaaS inbox条件 | 缺失时只能做文件级 real 验证,不宣称完整电话交接通过 |
| 已授权测试号码、时段、次数、预算及录音保留规则 | 阻塞真实电话执行;历史拨号授权不自动扩展为本专项三轮测试 |
| 质量样本、延迟阈值与人工听测人员 | 阻塞对应质量/体验最终签字 |
执行顺序:先完成P1契约草案与隔离Mock正反例,供应商资料并行协调;P2先文本后音频;P3/P4再安排一次受控真实电话测试;P5提交逐例证据与未通过项。已接受的实施方向不重复等待确认;只对新增字段/协议、预算和真实执行授权进行必要冻结。
@@ -17,7 +17,7 @@
4. 录音先上传 OSS,校验成功并取得 OSS ID 后发布 `recording.ready`;MQ 不传录音二进制、Base64 或公开播放 URL。
5. SaaS 负责客户/任务主数据、业务调度、业务重试决策、授权和长期存储;呼出应用只保存必要执行事实、控制屏障、幂等、资源租约和投递记录。
6. 生产采用多机器、多 EIP 直连;每通电话固定 Cell/出口。SaaS 不逐呼改写共享 SIP 配置,也不能任意指定 SIP 地址、凭证或越权主叫。
7. 目前只实现 ASR 验证基础,LLM/TTS 协议尚待新规范;本文对完整 AI 链路的描述是目标契约,不代表已启用或验收。
7. 目前只实现 ASR 验证基础;agent-call 已补充隔离的不可变 AI 配置存储与内部配置读写契约(见`docs/contracts/ai-config.openapi.yaml`和`ai-config.schema.json`),但首家供应商的 LLM/TTS 请求、流式、取消和音频协议尚待新规范,完整 AI 链路不代表已启用或验收。
8. **按 SaaS 提供的 `tenant_key` 绑定到独立 RabbitMQ 命令队列,由呼出应用调度器负责租户间公平调度**。不再采用所有租户共用一个执行 FIFO;同时限制租户发布/积压、预取窗口及跨 Cell 并发/CPS。`tenant_key` 是 SaaS 业务数据,命令、队列路由和后续 MQ 回调均原样使用;命名规则已统一,运行参数和限值仍待 G0 冻结。
## 2. 交付拆分与双方职责
@@ -312,10 +312,19 @@ payload 必需:
| `variables` | 经字段白名单、类型/长度校验的运行变量,不是任意代码或任意 URL |
| `ring_timeout_ms`、`max_call_duration_ms` | 不超过服务端和供应商上限 |
agent_version_id 必须对应呼出侧可用且可信的快照;配置如何同步、LLM/TTS 参数和取消/音频契约留待新规范。缺失配置明确拒绝,不私自读取旧 voice_test LLM/TTS 或任意远程 URL。
agent_version_id 必须对应呼出侧可用且可信的快照;当前配置发布/读取的内部契约见`docs/contracts/ai-config.openapi.yaml`,发布后不可覆盖,配置含 Prompt/模型/音色摘要及受控凭据引用。Bailian 文件探针使用 `agent_call/bailian.py` 的服务端环境适配,具体供应商参数、流式、取消/音频协议不进入 `call.execute`。缺失配置明确拒绝,不私自读取旧 voice_test LLM/TTS 或任意远程 URL。
线路策略由呼出侧解析为授权 `trunk_id + egress_pool_id + cell_id`,实际选择写入事件。呼叫固定 Cell/出口;允许的 FALLBACK 也必须兼容同一 Cell/出口及供应商白名单,无适合备用就失败,不迁移活动通话。当前只有一家线路,不虚构 backup。
### 6.1.1 AI 配置内部契约
AI 配置发布/读取不是 SaaS 拨号入口,也不是业务结果回调;仅供受控配置同步面使用。机器契约为 `docs/contracts/ai-config.openapi.yaml` 与 `docs/contracts/ai-config.schema.json`:
- `POST /internal/v1/ai/agent-versions`:要求独立 AI 配置认证域和 `ai.config.publish`,正文严格为 `agent_version_id + config`;首次发布返回 `201/published`,相同内容重放返回 `200/reused`。
- `GET /internal/v1/ai/agent-versions/{agent_version_id}`:要求独立 AI 配置认证域和 `ai.config.read`,只返回当前租户的可信快照。
- 版本按 `(tenant_id, agent_version_id)` 唯一且不可覆盖;内容摘要为 SHA-256。ID 不存在、跨租户、内容冲突、字段/Prompt/模型/音色不合法必须明确拒绝,不回退旧版本。
- 配置快照只允许 provider/credential 引用和受控参数,不允许 API Key、密码、任意 URL、SIP 地址;`call.execute` 仍只通过 RabbitMQ,不能把配置正文塞进消息。
### 6.2 幂等与发起
1. 验证身份边界、Schema 和 tenant/task 关联;对已记录的 command_id/execution_id 先检查语义冲突并返回/关联原结果,不因重投时已过期或控制已变而改写原事实。新执行再检查有效期、控制版本和配置/线路授权;防重查重不绕过当前调用方的租户访问校验。
@@ -644,7 +653,7 @@ HTTP 错误码与 MQ reason_code 共用词汇但不是一一映射;MQ 没有 H
| 应用证据与清理(已确认方案) | 第 10.1 节:首期只自动确认 broker,SaaS 对账补传;保留/交接依据,若必须自动应用确认则另选 MQ receipt | 用户、双方、运维 |
| 执行幂等 | 新增 execution_id,与 command_id 分离;业务重新外呼许可及去重保留 | 用户、SaaS |
| 停止语义 | drain/hangup 显式选择;挂断权限与多 Cell 生效判据 | 用户、SaaS |
| 线路/AI 配置 | route/caller/agent 引用及同步来源;LLM/TTS 新规范、失败兜底 | 用户、供应方 |
| 线路/AI 配置 | route/caller/agent 引用及同步来源;Mock AI 配置发布契约已落地,真实 LLM/TTS 新规范、失败兜底待供应方 | 用户、供应方 |
| Asterisk/SIP 管理后台与 SaaS 只读目录(Mock 已实现,生产待核验) | 不纳入普通业务七条路径;由独立管理后台统一写入并发布多机器 Trunk/codec_profile、主叫/被叫规则、出口池、并发/CPS,提供版本、CAS、审计、校验、发布、停用和回滚;SaaS 仅读取脱敏 Trunk 配置/能力/版本/健康状态;管理写接口、SaaS 只读接口和普通调度接口隔离认证凭据、issuer/audience/权限域,普通调度 Token 必须拒绝管理写接口 | 用户、SaaS、运维 |
| MQ 环境 | 租户独立命令队列已确认;采用 `agent-call` 命名空间、direct 命令 exchange、`agent-call.tenant.{tenant_key}.call.execute` 路由及 `tenant_key` 原样透传;冻结精确绑定、生命周期、队列数上限、quorum/HA、ACL、重试/DLQ 与死信可靠性 | 运维、双方 |
| 租户公平与背压 | 公平调度架构已确认;冻结轮转批量/周期、活跃队列发现、权重、prefetch、接收窗口、并发/CPS、发布速率/积压上限、拒绝发布策略、多实例协调及等待指标 | 用户、双方、运维 |
+145
View File
@@ -0,0 +1,145 @@
openapi: 3.1.0
info:
title: agent-call immutable AI configuration API
version: 1.0.0
description: >-
Internal configuration publication/read surface. The call.execute business
command remains RabbitMQ-only; secrets, URLs, and provider credentials are
resolved by the execution environment and never enter MQ messages.
servers:
- url: /
paths:
/internal/v1/ai/agent-versions:
post:
operationId: publishAgentVersion
security:
- aiConfigPublish: []
parameters:
- $ref: '#/components/parameters/TenantId'
- $ref: '#/components/parameters/RequestId'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/AgentVersionPublishRequest'
responses:
'200':
description: Identical immutable content already exists
content:
application/json:
schema:
$ref: '#/components/schemas/AgentVersionReceipt'
'201':
description: Immutable version published
content:
application/json:
schema:
$ref: '#/components/schemas/AgentVersionReceipt'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'409':
description: Existing version has different content
/internal/v1/ai/agent-versions/{agent_version_id}:
get:
operationId: getAgentVersion
security:
- aiConfigRead: []
parameters:
- $ref: '#/components/parameters/TenantId'
- $ref: '#/components/parameters/RequestId'
- name: agent_version_id
in: path
required: true
schema:
type: string
pattern: '^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$'
responses:
'200':
description: Trusted immutable snapshot
content:
application/json:
schema:
$ref: '#/components/schemas/AgentVersion'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
description: Agent version not found
components:
parameters:
TenantId:
name: X-Tenant-Id
in: header
required: true
schema: {type: string, minLength: 1}
RequestId:
name: X-Request-Id
in: header
required: true
schema: {type: string, minLength: 1, maxLength: 128}
securitySchemes:
aiConfigPublish:
type: http
scheme: bearer
bearerFormat: JWT
description: >-
Requires scope ai.config.publish and the AI-config issuer/audience.
aiConfigRead:
type: http
scheme: bearer
bearerFormat: JWT
description: >-
Requires scope ai.config.read and the AI-config issuer/audience.
schemas:
AgentVersionPublishRequest:
type: object
additionalProperties: false
required: [agent_version_id, config]
properties:
agent_version_id:
type: string
pattern: '^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$'
config:
$ref: 'ai-config.schema.json'
AgentVersionReceipt:
type: object
required: [tenant_id, agent_version_id, status, immutable, content_sha256]
properties:
tenant_id: {type: string}
agent_version_id: {type: string}
status: {enum: [published, reused]}
immutable: {const: true}
content_sha256: {type: string, pattern: '^[a-f0-9]{64}$'}
AgentVersion:
allOf:
- $ref: '#/components/schemas/AgentVersionReceipt'
- type: object
required: [config]
properties:
config:
$ref: 'ai-config.schema.json'
created_at: {type: string, format: date-time}
published_at: {type: string, format: date-time}
created_by: {type: string}
Error:
type: object
required: [error]
properties:
error: {type: string}
message: {type: string}
responses:
BadRequest:
description: Invalid configuration
content:
application/json:
schema: {$ref: '#/components/schemas/Error'}
Unauthorized:
description: Missing or invalid AI-config token
Forbidden:
description: Token lacks the AI-config permission
+101
View File
@@ -0,0 +1,101 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://agent-call.local/contracts/ai-config.schema.json",
"title": "Immutable AI agent version",
"type": "object",
"additionalProperties": false,
"required": ["agent_version_id", "immutable", "llm", "prompt", "tts", "asr", "conversation"],
"properties": {
"agent_version_id": {"type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$"},
"immutable": {"const": true},
"llm": {
"type": "object",
"additionalProperties": false,
"required": ["provider_ref", "model"],
"properties": {
"provider_ref": {"type": "string", "minLength": 1, "maxLength": 128},
"credential_ref": {"type": "string", "minLength": 1, "maxLength": 128},
"model": {"type": "string", "minLength": 1, "maxLength": 128},
"temperature": {"type": "number", "minimum": 0, "maximum": 2},
"max_tokens": {"type": "integer", "minimum": 1},
"timeout_ms": {"type": "integer", "minimum": 1}
}
},
"prompt": {
"type": "object",
"additionalProperties": false,
"required": ["text", "allowed_variables"],
"properties": {
"text": {"type": "string", "minLength": 1, "maxLength": 32768},
"allowed_variables": {
"type": "array",
"maxItems": 32,
"items": {"type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9_]*$"}
},
"max_bytes": {"type": "integer", "minimum": 1, "maximum": 32768}
}
},
"tts": {
"type": "object",
"additionalProperties": false,
"required": ["provider_ref", "model", "voice", "format"],
"properties": {
"provider_ref": {"type": "string", "minLength": 1, "maxLength": 128},
"credential_ref": {"type": "string", "minLength": 1, "maxLength": 128},
"model": {"type": "string", "minLength": 1, "maxLength": 128},
"voice": {"type": "string", "minLength": 1, "maxLength": 128},
"speed": {"type": "number", "minimum": 0.25, "maximum": 3},
"timeout_ms": {"type": "integer", "minimum": 1},
"format": {
"type": "object",
"additionalProperties": false,
"required": ["encoding", "sample_rate_hz", "channels"],
"properties": {
"encoding": {"enum": ["pcm_s16le", "pcma"]},
"sample_rate_hz": {"type": "integer", "minimum": 8000, "maximum": 48000},
"channels": {"const": 1}
}
}
}
},
"asr": {
"type": "object",
"additionalProperties": false,
"required": ["provider_ref", "language", "input"],
"properties": {
"provider_ref": {"type": "string", "minLength": 1, "maxLength": 128},
"credential_ref": {"type": "string", "minLength": 1, "maxLength": 128},
"model": {"type": "string", "minLength": 1, "maxLength": 128},
"language": {"type": "string", "minLength": 1, "maxLength": 32},
"interim": {"type": "boolean"},
"timeout_ms": {"type": "integer", "minimum": 1},
"input": {
"type": "object",
"additionalProperties": false,
"required": ["encoding", "sample_rate_hz", "channels", "sample_width_bytes"],
"properties": {
"encoding": {"const": "pcm_s16le"},
"sample_rate_hz": {"type": "integer", "minimum": 8000, "maximum": 48000},
"channels": {"const": 1},
"sample_width_bytes": {"const": 2}
}
}
}
},
"conversation": {
"type": "object",
"additionalProperties": false,
"required": ["opening", "allow_interrupt", "silence_timeout_ms", "max_duration_ms", "max_turns", "sentence_max_chars", "max_pending_audio_chunks"],
"properties": {
"opening": {"type": "string", "maxLength": 32768},
"allow_interrupt": {"type": "boolean"},
"silence_timeout_ms": {"type": "integer", "minimum": 1},
"max_duration_ms": {"type": "integer", "minimum": 1, "maximum": 3600000},
"max_turns": {"type": "integer", "minimum": 1, "maximum": 1000},
"sentence_max_chars": {"type": "integer", "minimum": 1},
"max_pending_audio_chunks": {"type": "integer", "minimum": 1}
}
},
"metadata": {"type": "object", "additionalProperties": true}
}
}
@@ -0,0 +1,49 @@
{
"agent_version_id": "agent_v1",
"immutable": true,
"llm": {
"provider_ref": "mock",
"model": "mock-chat-v1",
"temperature": 0.2,
"max_tokens": 256,
"timeout_ms": 5000
},
"prompt": {
"text": "You are a concise telephone assistant. Answer the caller's last statement.",
"allowed_variables": [],
"max_bytes": 32768
},
"tts": {
"provider_ref": "mock",
"model": "mock-tts-v1",
"voice": "mock-neutral",
"speed": 1.0,
"format": {
"encoding": "pcm_s16le",
"sample_rate_hz": 16000,
"channels": 1
},
"timeout_ms": 5000
},
"asr": {
"provider_ref": "mock",
"language": "zh-CN",
"input": {
"encoding": "pcm_s16le",
"sample_rate_hz": 16000,
"channels": 1,
"sample_width_bytes": 2
},
"interim": true,
"timeout_ms": 5000
},
"conversation": {
"opening": "",
"allow_interrupt": true,
"silence_timeout_ms": 3000,
"max_duration_ms": 120000,
"max_turns": 20,
"sentence_max_chars": 80,
"max_pending_audio_chunks": 32
}
}
@@ -0,0 +1,484 @@
{
"schema_version": "1.0",
"generated_at": "2026-09-14T14:24:17Z",
"scope": "real-bailian-file-probes-plus-isolated-mock-plus-real-cell-attempts",
"overall_status": "INCOMPLETE",
"gate_status": "BLOCKED_BY_REAL_PHONE_MEDIA_MQ_OSS_AND_HUMAN_EVIDENCE",
"component_modes": {
"llm": "real-bailian",
"tts": "real-bailian",
"asr": "real-bailian",
"sip_ari_rtp": "partial-real",
"database": "isolated-sqlite",
"rabbitmq": "temporary-test-broker",
"oss": "not-run"
},
"real_provider_preflight": {
"environment_configured": true,
"text_code": 0,
"audio_code": 0
},
"regression_tests": {
"exit_code": 0,
"test_count": 91
},
"commands": [
{
"kind": "mock-text",
"exit_code": 0,
"status": "PASS",
"mode": "mock",
"requested_mode": "mock",
"provider_modes": {
"saas": "mock",
"database": "sqlite",
"rabbitmq": "memory",
"sip": "mock",
"asterisk": "mock",
"asr": "mock",
"llm": "mock",
"tts": "mock",
"oss": "mock",
"cloud": "fake-cli"
},
"agent_version_id": "agent_test_v1",
"agent_config_sha256": "e542f9156775920c252706f483057aa8d17020439031095b341578de9070ab97",
"prompt_sha256": "4691b8fbd4f24df98f4b16547df7acf0342858cae71180f8d0d71d0166cc49ab",
"llm_requested_model": "mock-chat-v1",
"llm_returned_model": null,
"tts_requested_model": "mock-tts-v1",
"tts_returned_model": null,
"tts_returned_model_verified": false,
"tts_model_evidence": {
"configured_model": "mock-tts-v1",
"provider_echoed_model": false
},
"voice_sha256": "41a8734357dc70994169ccb2cea1f5a513faa19069d2fb4c0782a9e9707f3918",
"asr_provider_ref": "mock",
"asr_segment_count": 0,
"llm_first_token_ms": 0.23247480392456055,
"tts_first_audio_ms": 3.8798768520355225,
"audio_sha256": "59a3ef24b931936fb9a65ba8d780a6b6bf9075357d2ab7094065018cfcdf217b",
"audio_bytes": 24492,
"call_id": null,
"reason_code": null,
"recording_bytes": null,
"recording_valid": null
},
{
"kind": "mock-audio",
"exit_code": 0,
"status": "PASS",
"mode": "mock",
"requested_mode": "mock",
"provider_modes": {
"saas": "mock",
"database": "sqlite",
"rabbitmq": "memory",
"sip": "mock",
"asterisk": "mock",
"asr": "mock",
"llm": "mock",
"tts": "mock",
"oss": "mock",
"cloud": "fake-cli"
},
"agent_version_id": "agent_test_v1",
"agent_config_sha256": "e542f9156775920c252706f483057aa8d17020439031095b341578de9070ab97",
"prompt_sha256": "4691b8fbd4f24df98f4b16547df7acf0342858cae71180f8d0d71d0166cc49ab",
"llm_requested_model": "mock-chat-v1",
"llm_returned_model": null,
"tts_requested_model": "mock-tts-v1",
"tts_returned_model": null,
"tts_returned_model_verified": false,
"tts_model_evidence": {
"configured_model": "mock-tts-v1",
"provider_echoed_model": false
},
"voice_sha256": "41a8734357dc70994169ccb2cea1f5a513faa19069d2fb4c0782a9e9707f3918",
"asr_provider_ref": "mock",
"asr_segment_count": 2,
"llm_first_token_ms": 0.2178652286529541,
"tts_first_audio_ms": 7.668752193450928,
"audio_sha256": "57481064a8382962faac5fd30c70c671e3d50e013e9e1807b5ef26140c5a7f2b",
"audio_bytes": 43052,
"call_id": null,
"reason_code": null,
"recording_bytes": null,
"recording_valid": null
},
{
"kind": "mock-call",
"exit_code": 0,
"status": "PASS",
"mode": "mock",
"requested_mode": "mock",
"provider_modes": {
"saas": "mock",
"database": "sqlite",
"rabbitmq": "memory",
"sip": "mock",
"asterisk": "mock",
"asr": "mock",
"llm": "mock",
"tts": "mock",
"oss": "mock",
"cloud": "fake-cli"
},
"agent_version_id": "agent_test_v1",
"agent_config_sha256": "e542f9156775920c252706f483057aa8d17020439031095b341578de9070ab97",
"prompt_sha256": "4691b8fbd4f24df98f4b16547df7acf0342858cae71180f8d0d71d0166cc49ab",
"llm_requested_model": "mock-chat-v1",
"llm_returned_model": null,
"tts_requested_model": "mock-tts-v1",
"tts_returned_model": null,
"tts_returned_model_verified": false,
"tts_model_evidence": null,
"voice_sha256": "41a8734357dc70994169ccb2cea1f5a513faa19069d2fb4c0782a9e9707f3918",
"asr_provider_ref": "mock",
"asr_segment_count": 0,
"llm_first_token_ms": null,
"tts_first_audio_ms": null,
"audio_sha256": null,
"audio_bytes": null,
"call_id": "call_7d10d1d0174f43f09c05ce72bf7a8e84",
"reason_code": "CALL_COMPLETED",
"recording_bytes": null,
"recording_valid": null
},
{
"kind": "real-text",
"exit_code": 0,
"status": "PASS",
"mode": "real",
"requested_mode": "real",
"provider_modes": {
"llm": "real",
"tts": "real",
"asr": "real"
},
"agent_version_id": "agent_bailian_test_v1",
"agent_config_sha256": "8e4473cec2700f68847499bf46b911bfb9d4c4f52557c474f86f2de074e752a2",
"prompt_sha256": "4691b8fbd4f24df98f4b16547df7acf0342858cae71180f8d0d71d0166cc49ab",
"llm_requested_model": "qwen-plus",
"llm_returned_model": "qwen-plus",
"tts_requested_model": "cosyvoice-v3.5-plus",
"tts_returned_model": null,
"tts_returned_model_verified": false,
"tts_model_evidence": {
"configured_model": "cosyvoice-v3.5-plus",
"provider_echoed_model": false
},
"voice_sha256": "5549b1c86878276e83fe1c220c0c4e6c469dc6afbff5761af2e4e29940657f35",
"asr_provider_ref": "bailian",
"asr_segment_count": 0,
"llm_first_token_ms": 200.48080801963806,
"tts_first_audio_ms": 1790.2482328414917,
"audio_sha256": "5ee03618a72a20a2d5f14d4fe27d8046f069a6785972c8b40eb28dcfafb68a5b",
"audio_bytes": 49964,
"call_id": null,
"reason_code": null,
"recording_bytes": null,
"recording_valid": null
},
{
"kind": "real-audio",
"exit_code": 0,
"status": "PASS",
"mode": "real",
"requested_mode": "real",
"provider_modes": {
"llm": "real",
"tts": "real",
"asr": "real"
},
"agent_version_id": "agent_bailian_test_v1",
"agent_config_sha256": "8e4473cec2700f68847499bf46b911bfb9d4c4f52557c474f86f2de074e752a2",
"prompt_sha256": "4691b8fbd4f24df98f4b16547df7acf0342858cae71180f8d0d71d0166cc49ab",
"llm_requested_model": "qwen-plus",
"llm_returned_model": "qwen-plus",
"tts_requested_model": "cosyvoice-v3.5-plus",
"tts_returned_model": null,
"tts_returned_model_verified": false,
"tts_model_evidence": {
"configured_model": "cosyvoice-v3.5-plus",
"provider_echoed_model": false
},
"voice_sha256": "5549b1c86878276e83fe1c220c0c4e6c469dc6afbff5761af2e4e29940657f35",
"asr_provider_ref": "bailian",
"asr_segment_count": 3,
"llm_first_token_ms": 296.70279598236084,
"tts_first_audio_ms": 1466.9799489974976,
"audio_sha256": "f47811feff980e450f00aa65f82996a0f2de8f4eea7ebfc40df6ba0b51af5116",
"audio_bytes": 46124,
"call_id": null,
"reason_code": null,
"recording_bytes": null,
"recording_valid": null
},
{
"kind": "real-call-evidence",
"exit_code": 1,
"status": "INCOMPLETE",
"mode": "real",
"requested_mode": null,
"provider_modes": null,
"agent_version_id": null,
"agent_config_sha256": null,
"prompt_sha256": null,
"llm_requested_model": null,
"llm_returned_model": null,
"tts_requested_model": null,
"tts_returned_model": null,
"tts_returned_model_verified": false,
"tts_model_evidence": null,
"voice_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"asr_provider_ref": null,
"asr_segment_count": 0,
"llm_first_token_ms": null,
"tts_first_audio_ms": null,
"audio_sha256": null,
"audio_bytes": null,
"call_id": "call_585c9b8a5f2747af88565fbd0e5f7b01",
"reason_code": "CALL_NOT_ANSWERED",
"recording_bytes": 44,
"recording_valid": false
}
],
"cases": [
{
"case_id": "AI-01",
"mode": "isolated-mock",
"status": "PASS",
"actual": "tenant-scoped immutable snapshot was published and reused",
"evidence": [
"tests/test_ai_runtime.py",
"tests/test_agent_call.py",
"tests/test_real_cell.py"
]
},
{
"case_id": "AI-02",
"mode": "isolated-mock",
"status": "PASS",
"actual": "missing, cross-tenant, and same-version conflict checks are covered",
"evidence": [
"tests/test_ai_runtime.py",
"tests/test_agent_call.py",
"tests/test_real_cell.py"
]
},
{
"case_id": "AI-03",
"mode": "isolated-mock",
"status": "PASS",
"actual": "Prompt, model/voice, secret, and audio-format validation is covered",
"evidence": [
"tests/test_ai_runtime.py",
"tests/test_agent_call.py",
"tests/test_real_cell.py"
]
},
{
"case_id": "AI-04",
"mode": "isolated-mock",
"status": "PASS",
"actual": "version digest is persisted with the execution snapshot",
"evidence": [
"tests/test_ai_runtime.py",
"tests/test_agent_call.py",
"tests/test_real_cell.py"
]
},
{
"case_id": "AI-09",
"mode": "isolated-mock",
"status": "PASS",
"actual": "opening and Prompt constraints are configuration-driven in the runtime tests",
"evidence": [
"tests/test_ai_runtime.py",
"tests/test_agent_call.py",
"tests/test_real_cell.py"
]
},
{
"case_id": "AI-10",
"mode": "isolated-mock",
"status": "PASS",
"actual": "PCM16/PCMA conversion, frame alignment, and bounded queues passed",
"evidence": [
"tests/test_ai_runtime.py",
"tests/test_agent_call.py",
"tests/test_real_cell.py"
]
},
{
"case_id": "AI-11",
"mode": "isolated-mock",
"status": "PASS",
"actual": "cancellation and late-chunk discard passed with isolated providers",
"evidence": [
"tests/test_ai_runtime.py",
"tests/test_agent_call.py",
"tests/test_real_cell.py"
]
},
{
"case_id": "AI-12",
"mode": "isolated-mock",
"status": "PASS",
"actual": "silence/no-answer lifecycle remains bounded in the mock executor",
"evidence": [
"tests/test_ai_runtime.py",
"tests/test_agent_call.py",
"tests/test_real_cell.py"
]
},
{
"case_id": "AI-13",
"mode": "isolated-mock",
"status": "PASS",
"actual": "hangup/timeout cleanup remains isolated in the mock executor",
"evidence": [
"tests/test_ai_runtime.py",
"tests/test_agent_call.py",
"tests/test_real_cell.py"
]
},
{
"case_id": "AI-14",
"mode": "isolated-mock",
"status": "PASS",
"actual": "provider errors and real-mode fail-closed behavior are covered",
"evidence": [
"tests/test_ai_runtime.py",
"tests/test_agent_call.py",
"tests/test_real_cell.py"
]
},
{
"case_id": "AI-15",
"mode": "isolated-mock",
"status": "PASS",
"actual": "ARI reconciliation regression remains green for the existing mock path",
"evidence": [
"tests/test_ai_runtime.py",
"tests/test_agent_call.py",
"tests/test_real_cell.py"
]
},
{
"case_id": "AI-16",
"mode": "isolated-mock",
"status": "PASS",
"actual": "duplicate publication/execution recovery remains green",
"evidence": [
"tests/test_ai_runtime.py",
"tests/test_agent_call.py",
"tests/test_real_cell.py"
]
},
{
"case_id": "AI-17",
"mode": "isolated-mock",
"status": "PASS",
"actual": "text, playback, and MQ result facts remain separate",
"evidence": [
"tests/test_ai_runtime.py",
"tests/test_agent_call.py",
"tests/test_real_cell.py"
]
},
{
"case_id": "AI-18",
"mode": "isolated-mock",
"status": "PASS",
"actual": "recording checksum/upload recovery regression remains green",
"evidence": [
"tests/test_ai_runtime.py",
"tests/test_agent_call.py",
"tests/test_real_cell.py"
]
},
{
"case_id": "AI-19",
"mode": "real-cell-code",
"status": "PASS",
"actual": "real Cell ARI/RTP worker, queue routing, durable claim ledger, and no-redial tests are covered",
"evidence": [
"tests/test_ai_runtime.py",
"tests/test_agent_call.py",
"tests/test_real_cell.py"
]
},
{
"case_id": "AI-21",
"mode": "isolated-mock",
"status": "PASS",
"actual": "reports use hashes and do not include provider credentials or URLs",
"evidence": [
"tests/test_ai_runtime.py",
"tests/test_agent_call.py",
"tests/test_real_cell.py"
]
},
{
"case_id": "AI-05",
"mode": "real-bailian",
"status": "PASS",
"actual": "real text stream passed",
"evidence": [
"docs/evidence/llm-voice-acceptance-20260914.json"
]
},
{
"case_id": "AI-06",
"mode": "real-bailian",
"status": "BLOCKED",
"actual": "real TTS; machine audio passed; human listening is not registered passed",
"evidence": [
"docs/evidence/llm-voice-acceptance-20260914.json"
]
},
{
"case_id": "AI-07",
"mode": "real-bailian",
"status": "BLOCKED",
"actual": "real audio chain; machine audio passed; human listening is not registered passed",
"evidence": [
"docs/evidence/llm-voice-acceptance-20260914.json"
]
},
{
"case_id": "AI-08",
"mode": "real-phone-evidence",
"status": "FAIL",
"actual": "real three-round phone evidence is incomplete: SIP may connect, but bidirectional RTP, three AI rounds, recording handoff, and human listening are required",
"evidence": [
"docs/evidence/real-cell-call-20260914.json"
]
},
{
"case_id": "AI-20",
"mode": "mixed",
"status": "BLOCKED",
"actual": "one-command mock flow passed; full real call flow remains incomplete until real MQ/OSS and phone evidence pass",
"evidence": [
"scripts/test_ai_call.py",
"docs/evidence/llm-voice-acceptance-20260914.json"
]
}
],
"blockers": [
"The single authorized real call reached SIP 200 OK, but usable bidirectional RTP, three AI rounds, and a valid recording were not proven.",
"The call path used a temporary RabbitMQ test broker; production RabbitMQ/SaaS result and OSS recording handoff remain unverified.",
"Human listening and provider-specific latency/quality thresholds are not registered; machine-generated WAV evidence cannot replace listening."
],
"evidence_policy": "docs/evidence contains hashes/status only; raw protocol/audio evidence stays in ignored local paths",
"test_commands": [
"python3 -m unittest discover -s tests -v",
"python3 scripts/test_ai_call.py text --profile configs/ai-test.bailian.example.yaml --mode real --input '请只回答收到。'",
"python3 scripts/test_ai_call.py audio --profile configs/ai-test.bailian.example.yaml --mode real --input <real-or-authorized-wav>",
"python3 scripts/run_real_cell_call.py --profile configs/ai-test.bailian.example.yaml --allow-real-call --callee 15003164745"
]
}
@@ -0,0 +1,36 @@
# LLM 与音色可配置电话对话专项验收
- 日期:2026-09-14
- 范围:Bailian 真实文件链路 + 隔离 Mock + 三次真实 Cell 电话尝试
- 总体:`INCOMPLETE`
- 门禁:`BLOCKED_BY_REAL_PHONE_MEDIA_MQ_OSS_AND_HUMAN_EVIDENCE`
- 机器证据:`docs/evidence/llm-voice-acceptance-20260914.json`
## 已通过
- 91 项 Python 回归测试通过;Ruff、编译、Schema/OpenAPI 解析与 `git diff --check` 通过。
- 租户范围不可变 `agent_version_id`、SHA-256 快照、配置权限隔离和 MQ `call.execute` 约束通过。
- Bailian 真实文本探针:LLM 请求 `qwen-plus`,服务端返回模型 `qwen-plus`;首 token 约 335 ms,TTS 首音频约 1661 ms。
- Bailian 真实音频探针:CosyVoice `cosyvoice-v3.5-plus` 输出 mono PCM16/24 kHz;真实 ASR 返回最终稿,随后真实 LLM/TTS 完成;音频可解码并留存 SHA-256。
- CosyVoice 自定义音色使用 `/api-ws/v1/inference` 流式协议;Qwen realtime 分支会使用 `/api-ws/v1/realtime`,不再误用 ASR endpoint。
- Bailian 适配器保持环境注入凭据;MQ、配置快照和公开证据不写入 API Key、供应商 URL 或凭据。
- 三次分别授权的 real Cell 呼叫均未完成 AI 电话门禁:第一次到达 SIP `200 OK` 但无可用 RTP,后两次为 SIP `480`;没有自动重拨,详见 `docs/evidence/real-cell-call-20260914.json`。
## 阻塞项
- P3 已接入 Cell ARI/externalMedia/RTP/录音执行器;三次实际尝试均未形成可验收的双向 RTP/三轮上下文,P4 因此未完成。
- 本次仅使用临时 RabbitMQ 测试 broker 验证 `call.execute` 队列路径;生产 RabbitMQ/SaaS inbox、OSS 录音交接和完整三轮电话仍未验收。
- TTS 服务端未返回可核验模型字段;报告同时保留 `tts_model_evidence.configured_model` 与 `provider_echoed_model=false`,并按契约标记 `provider_returned_model_verified=false`。人工音色听测尚未登记,因此 AI-06/AI-07 不能报完整 PASS。
- 供应商延迟/质量阈值和人工听测结论未冻结,机器 WAV 不替代手机端听感验收。
## 证据与重跑
- 原始 WAV 和命令输出保存在被忽略目录 `.local/agent-call-ai/`,公开 JSON 只保留哈希、字节数、模式和时延。
- 运行:
```bash
python3 -m unittest discover -s tests -v
python3 scripts/accept_ai_call.py
```
验收脚本因真实电话媒体/三轮与人工证据未完成按设计返回非零;已执行但失败的真实电话返回 `1`,尚未安排真实电话才返回 `2`。这不是完整真实电话通过声明。
@@ -0,0 +1,49 @@
{
"schema_version": "1.0",
"status": "FAIL",
"mode": "real",
"authorization": {
"scope": "one final controlled real outbound call",
"callee": "15003164745",
"max_rounds": 3,
"automatic_retry": false
},
"execution": {
"execution_id": "exec_cell_1789395606006147221_10",
"call_id": "call_585c9b8a5f2747af88565fbd0e5f7b01",
"event_id": "exec_cell_1789395606006147221_10:call.finished",
"tenant_id": "tenant-demo",
"tenant_key": "tenant-demo-key",
"route": "agent-call.tenant.tenant-demo-key.call.execute",
"connected": false,
"duration_ms": 206.89915500406642,
"reason_code": "CALL_NOT_ANSWERED",
"turns": 0,
"media_packets_in": 0,
"media_packets_out": 0
},
"sip": {
"provider": "61.132.228.221:5060",
"target": "708915003164745",
"codec": "PCMA/8000",
"responses": ["100 trying", "480 Temporarily Unavailable"],
"reason": "Q.850 cause=16 NORMAL_CLEARING",
"provider_error_code": "14002_201",
"external_media": "UnicastRTP channel created and joined the bridge",
"rtp_and_recording": "no answered media session"
},
"rabbitmq": {
"transport": "temporary local RabbitMQ test broker",
"command_path_verified": true,
"production_saas_handoff": false
},
"acceptance": {
"real_sip_connected": false,
"bidirectional_rtp_verified": false,
"real_asr_llm_tts_turns": false,
"three_round_conversation": false,
"human_listening": false,
"oss_recording_handoff": false
},
"note": "This is the final separately authorized attempt. No automatic retry was issued after the provider returned 480. Credentials, provider API keys, and ARI passwords are omitted."
}
@@ -0,0 +1,48 @@
{
"schema_version": "1.0",
"status": "FAIL",
"mode": "real",
"authorization": {
"scope": "one additional controlled real outbound call",
"callee": "15003164745",
"max_rounds": 3,
"automatic_retry": false
},
"execution": {
"execution_id": "exec_cell_1789393256422470110_10",
"call_id": "call_41907a57960042098af9daf53bec5ed7",
"event_id": "exec_cell_1789393256422470110_10:call.finished",
"tenant_id": "tenant-demo",
"tenant_key": "tenant-demo-key",
"route": "agent-call.tenant.tenant-demo-key.call.execute",
"connected": false,
"duration_ms": 217.7564550001989,
"reason_code": "CALL_NOT_ANSWERED",
"turns": 0,
"media_packets_in": 0,
"media_packets_out": 0
},
"sip": {
"provider": "61.132.228.221:5060",
"target": "708915003164745",
"codec": "PCMA/8000",
"responses": ["100 trying", "480 Temporarily Unavailable"],
"reason": "Q.850 cause=16 NORMAL_CLEARING",
"external_media": "UnicastRTP channel created and joined the bridge",
"rtp_and_recording": "no answered media session"
},
"rabbitmq": {
"transport": "temporary local RabbitMQ test broker",
"command_path_verified": true,
"production_saas_handoff": false
},
"acceptance": {
"real_sip_connected": false,
"bidirectional_rtp_verified": false,
"real_asr_llm_tts_turns": false,
"three_round_conversation": false,
"human_listening": false,
"oss_recording_handoff": false
},
"note": "This is the second separately authorized attempt. No automatic retry was issued after the provider returned 480. Credentials, provider API keys, and ARI passwords are omitted."
}
@@ -0,0 +1,81 @@
{
"schema_version": "1.0",
"status": "INCOMPLETE",
"mode": "real",
"gate": "BLOCKED_BY_REAL_MEDIA_AND_THREE_ROUND_EVIDENCE",
"authorization": {
"scope": "two separately authorized controlled real outbound calls",
"callee": "15003164745",
"max_rounds": 3,
"attempts": 3,
"automatic_retry": false
},
"execution": {
"execution_id": "exec_cell_1789383485969851269_10",
"call_id": "call_ec889b4209024cab9b17285ee2de9e72",
"event_id": "exec_cell_1789383485969851269_10:call.finished",
"tenant_id": "tenant-demo",
"tenant_key": "tenant-demo-key",
"route": "agent-call.tenant.tenant-demo-key.call.execute",
"connected": true,
"duration_ms": 35901.207285001874,
"reason_code": "RTP_RECEIVE_FAILED",
"turns": 0,
"media_packets_in": 0,
"media_packets_out": 0
},
"latest_execution": {
"execution_id": "exec_cell_1789395606006147221_10",
"call_id": "call_585c9b8a5f2747af88565fbd0e5f7b01",
"event_id": "exec_cell_1789395606006147221_10:call.finished",
"connected": false,
"duration_ms": 206.89915500406642,
"reason_code": "CALL_NOT_ANSWERED",
"turns": 0,
"media_packets_in": 0,
"media_packets_out": 0
},
"final_execution": {
"execution_id": "exec_cell_1789395606006147221_10",
"call_id": "call_585c9b8a5f2747af88565fbd0e5f7b01",
"event_id": "exec_cell_1789395606006147221_10:call.finished",
"connected": false,
"duration_ms": 206.89915500406642,
"reason_code": "CALL_NOT_ANSWERED",
"turns": 0,
"media_packets_in": 0,
"media_packets_out": 0
},
"attempt_evidence": [
"docs/evidence/real-cell-call-20260914.json",
"docs/evidence/real-cell-call-20260914-second.json",
"docs/evidence/real-cell-call-20260914-final.json"
],
"sip": {
"provider": "61.132.228.221:5060",
"target": "708915003164745",
"codec": "PCMA/8000",
"responses": ["100 trying", "183 Session Progress", "200 OK"],
"hangup_reason": "Q.850 cause=16 NORMAL_CLEARING",
"rtp_and_recording": "no usable audio"
},
"recording": {
"bytes": 44,
"sha256": "4f8734c5e13ac599e168cf247a51c1dd0758537ce00bf16d7fed1a3d14d07041",
"valid_audio": false
},
"rabbitmq": {
"transport": "temporary local RabbitMQ test broker",
"command_path_verified": true,
"production_saas_handoff": false
},
"acceptance": {
"real_sip_connected": true,
"bidirectional_rtp_verified": false,
"real_asr_llm_tts_turns": false,
"three_round_conversation": false,
"human_listening": false,
"oss_recording_handoff": false
},
"note": "This aggregate records three separately authorized attempts. The first reached SIP 200 OK but had no usable media; the second and final attempts returned 480 before answer. No automatic redial was issued. Credentials, provider API keys, and ARI passwords are intentionally omitted."
}
+17
View File
@@ -0,0 +1,17 @@
# Real Cell AI call probe — 2026-09-14
**结论:INCOMPLETE(三次授权探针均未完成三轮 AI 对话)**
- 呼叫:`15003164745`;业务被叫原值保留,线路规则构造为 `708915003164745`
- 第一次:SIP `200 OK`,但无可用 RTP,0 轮,44-byte 空录音;证据见本文件同名 JSON 的首个 execution。
- 第二次:SIP `100 trying` → `480 Temporarily Unavailable`,`Q.850 cause=16 NORMAL_CLEARING`,未进入接通媒体;独立证据见 `docs/evidence/real-cell-call-20260914-second.json`。
- 第三次(最终授权):SIP `100 trying` → `480 Temporarily Unavailable`,供应商错误码 `14002_201`,未进入接通媒体;独立证据见 `docs/evidence/real-cell-call-20260914-final.json`。
- 路径:RabbitMQ `call.execute` → 租户命令队列 → Cell ARI/externalMedia → Asterisk/SIP
- 首次 SIP:`100 trying` → `183 Session Progress` → `200 OK`;PCMA/8000;最终 `Q.850 cause=16`
- ARI:真实 Cell bridge 和 `UnicastRTP` externalMedia 已创建并加入
- 媒体:未验证到可用双向 RTP,Cell 收发包计数为 0;执行器返回 `RTP_RECEIVE_FAILED`
- AI:0 个 ASR→LLM→TTS 电话轮次;未完成三轮;无人听测签字
- 录音:仅 WAV 头(44 bytes),不可作为有效录音;SHA-256 为 `4f8734c5e13ac599e168cf247a51c1dd0758537ce00bf16d7fed1a3d14d07041`
- MQ:本次使用临时本机 RabbitMQ 验证命令路径,不是生产 SaaS broker;未完成 OSS 交接
本文件记录三次分别授权的尝试;均未自动重拨。凭据、API Key、ARI 密码和任意敏感 URL 均未写入证据。机器明细见同名 JSON。
@@ -10,6 +10,7 @@
- [主契约](SaaS交互_OpenAPI与MQ契约规划_v0.1.md)是标识、HTTP 七条路径、MQ 拓扑/字段/状态的唯一维护来源。本文件不复制接口表;先生成 OpenAPI/JSON Schema,再编写 Mock 和正式实现。
- [原一期计划](一期呼出应用开发计划_v1.0.md)保留 WBS/AT 编号和历史估算;[交付设计](交付文档/一期中间调度件与Asterisk_v1.0/01_中间调度件与MQ回传设计.md)、[部署细则](交付文档/一期中间调度件与Asterisk_v1.0/02_Asterisk部署与SIP对接步骤.md)、[专项用例](交付文档/一期中间调度件与Asterisk_v1.0/03_联调步骤与验收标准.md)保留实现与场景细节,冲突时以本文件及主契约为准。
- 当前已有 ASR Web、ASR 协议适配、Asterisk 配置生成、阿里云只读计划/受控创建工具,以及执行服务、持久状态、MQ/HTTP 和 Mock 测试产物;参见[现有运行说明](部署接入_运行说明.md)。2026-09-13静态审查发现隔离、控制、恢复、媒体与验收缺口,完整调度/AI通话/MQ与资产交接仍须整改复验,不能把已有产物等同于完整门禁通过。整改执行见[功能审查整改计划与验收标准](功能审查整改计划与验收标准_v1.0.md)。
- 2026-09-14 用户已确认支持指定 LLM 模型、Prompt 和 TTS 音色的完整电话对话及一键测试;开发工作包、配置契约待冻结项、脚本边界与逐项功能验收见 [LLM与音色可配置电话对话专项计划](LLM与音色可配置电话对话_开发与验收计划_v1.0.md)。该文档不代表真实 AI 已启用,不替代主契约或生产容量验收。
- 本次只交付文档,不新增运行服务、不拨号、不创建云资源、不提交 Git。旧 Word/Excel/ZIP 不覆盖重生成。
## 2. 已接受的最终方案
+33 -7
View File
@@ -14,7 +14,7 @@
| 阿里云主机准备 | 已实现CLI驱动的只读计划、受控创建竞价实例、复用主机和绑定既有EIP;默认不修改云资源 |
| 云端实际操作 | 已完成V13一次性专用竞价Cell创建、绑定固定EIP、SCALE-MOCK及回收;当前保留一台SpotAsPriceGo真实SIP测试环境 `i-2ze8fa5vhyl4cc8oeol9` |
| Asterisk真实接入 | 已完成当前环境R1底座与两号码SIP探针;初始ulaw收到488/Q.850 cause=88,按供应商反馈切换PCMA后两次均收到480/Q.850 cause=16,仍未接通 |
| LLM/TTS | 未实现、未启用,等待用户新的供应商/协议/参数规范 |
| LLM/TTS | 已实现 Bailian 真实流式 LLM/CosyVoice 适配、不可变 AI 版本、Prompt/模型/音色校验、取消与报告;电话媒体仍未接入 |
| agent-call Mock 调度、MQ/OSS闭环 | 已实现:租户独立路由、DB状态、CAS控制、outbox/inbox、Mock SIP/ARI/RTP/AI和OSS校验;未替代真实Asterisk/供应商验收 |
| ARI业务调度、自动FALLBACK、真实MQ/OSS回传 | 未完成;现有主备trunk配置不能当成自动切换代码 |
@@ -38,6 +38,12 @@ services/asr-web/ ASR-only Go服务及Web页面
deploy/asterisk.example.json SIP接入参数占位
deploy/asterisk.sh 默认检查;显式up才启动
tests/ 离线部署逻辑及PCM测试
scripts/test_ai_call.py text/audio/call 分层 AI 测试入口
scripts/accept_ai_call.py AI 专项 Mock/真实文件验收与脱敏报告生成
agent_call/bailian.py Bailian ASR/LLM/CosyVoice 环境适配器
configs/ai-test.example.yaml 无密钥的 JSON-compatible YAML 示例
configs/ai-test.bailian.example.yaml 真实 Bailian 文件探针示例(凭据只取环境)
prompts/test-call.txt UTF-8 Prompt 示例
docs/ 需求、计划和运行文档
.local/ 本机临时状态/验证产物,不提交
```
@@ -183,7 +189,7 @@ ssh -N -L 18088:127.0.0.1:18088 "${DEPLOY_SSH_USER}@123.56.71.98"
页面流程:填写服务令牌→读取模型→选择有凭据的模型→开始识别→查看中间/最终文字→结束或取消。音频为16k、单声道、PCM16LE、每包100ms;会话最长5分钟、同时最多4个连接。背压过大停止采集,不能无限缓存音频。
LLM/TTS固定显示“未启用/等待新规范”。用户未提供新规范前,不调用旧仓库实现,不提供隐式供应商回退。
ASR Web 页面不承载 LLM/TTS;`agent-call` CLI 的 Bailian 适配只从服务端 `BAILIAN_*` 环境读取凭据,不调用旧仓库的 LLM/TTS,也不提供隐式供应商回退。
## 5. Asterisk配置与启动
@@ -249,7 +255,8 @@ bash -n deploy/asterisk.sh
- 本地容器:ASR镜像构建、非root/read-only运行、回环HTTP与healthcheck;测试容器已清理。
- 浏览器:无效令牌拒绝、正常令牌读取未配置模型、模型禁用、LLM/TTS未启用。没有采集真实麦克风,也没有发送真实供应商请求。
- 已执行:阿里云只读盘点、一次V13专用竞价Cell创建/绑定、Mock规模运行、一次R1真实SIP/Asterisk探针、测试实例停止/删除和固定EIP保留验证。
- 未执行或未通过:真实外呼接通/RTP、真实ASR、LLM/TTS、MQ/OSS、完整外呼以及生产容量/N+1验收;R1证据见`docs/evidence/asterisk-real-2026-09-12.md`。
- 未执行或未通过:真实外呼接通/RTP、电话侧真实ASR→LLM→TTS、MQ/OSS、完整外呼以及生产容量/N+1验收;R1证据见`docs/evidence/asterisk-real-2026-09-12.md`。
- AI 专项:隔离 Mock 与 Bailian 真实文本/音频文件链路均已执行;真实 ASR、LLM、CosyVoice、音频格式和时延摘要见`docs/evidence/llm-voice-acceptance-20260914.json`,原始文件留在`.local/agent-call-ai/`。
本地模拟通过不等于生产可用。只有填齐资源和契约、执行既有验收文档相应场景并留存证据后,才可把阶段状态更新为真实环境验收通过。
@@ -275,22 +282,41 @@ curl --fail http://127.0.0.1:18080/healthz/ready
```bash
python3 -m unittest discover -s tests -v
python3 -m scripts.acceptance
python3 scripts/accept_ai_call.py
python3 scripts/test_ai_call.py text --input '隔离文本验收'
python3 deploy/check_mock_config.py
python3 deploy/migrate.py data/agent-call.sqlite3
```
`agent_call/core.py`使用真实SQLite表保存任务、执行、通话、尝试、事件、outbox/inbox、上传和重放状态;`deploy/backup_sqlite.py backup|restore`提供离线备份恢复。`ProtocolJournal`只记录合成的SIP/ARI/RTP/AI协议事实,不把它们伪装成真实供应商响应。
`agent_call/core.py`使用真实SQLite表保存任务、执行、通话、尝试、事件、outbox/inbox、上传和重放状态;`agent_versions`保存租户范围内的不可变 AI 配置快照及摘要;`deploy/backup_sqlite.py backup|restore`提供离线备份恢复。`ProtocolJournal`只记录合成的SIP/ARI/RTP/AI协议事实,不把它们伪装成真实供应商响应。
`python3 scripts/test_ai_call.py` 的 `text` 与 `audio` 不拨号;`call` 必须显式传 `--allow-real-call`,Mock profile 仍只发 MQ 到隔离内存 Broker,不会拨打电话。Bailian real `text`/`audio` 只调用真实 AI,不拨号;报告只写配置/Prompt/音频摘要,不写密钥或任意供应商 URL。AI 配置 HTTP 读写使用独立 `ai.config.read` / `ai.config.publish` scope,并要求 `AI_CONFIG_ISSUER`/`AI_CONFIG_AUDIENCE` 与 `auth_domain=ai-config`;普通调度 Token 被拒绝。业务 `call.execute` 仍只走 RabbitMQ。
### 7.3 当前门禁
本地V01-V12及阿里云专用主机上的V13 Mock探针已通过;最新完整验收结果见`docs/evidence/acceptance-mock-2026-09-13.json`,脚本仍因V13缺持续媒体独立证据和V14需要外部真实证据而按门禁返回非零。V13使用100租户/1200模拟接通、禁用1个Cell;这是Mock协议/持久状态负载证据,不是RTP或生产容量证据。R1历史探针见`docs/evidence/asterisk-real-2026-09-12.md`;ulaw初始探针见`docs/evidence/asterisk-persistent-2026-09-14.md`,PCMA复测见`docs/evidence/asterisk-persistent-pcma-2026-09-14.md`:ECS `i-2ze8fa5vhyl4cc8oeol9`、Asterisk容器和固定EIP `123.56.71.98` 已核验,PCMA实际SDP为 `PCMA/8000`,两个目标均由原先的 `488/Q.850 cause=88` 变为 `480/Q.850 cause=16`,仍未接通、无RTP/录音证据。该 ECS 按用户要求继续保留并运行,固定 EIP 为 `InUse`。真实接通/RTP、ASR/LLM/TTS/OSS、生产1000路/N+1尚未完成。正式模式拒绝当前Mock profile,不能把测试号码或Mock成功自动升级为真实外呼授权。
本地V01-V12及阿里云专用主机上的V13 Mock探针已通过;最新完整验收结果见`docs/evidence/acceptance-mock-2026-09-13.json`,脚本仍因V13缺持续媒体独立证据和V14需要外部真实证据而按门禁返回非零。V13使用100租户/1200模拟接通、禁用1个Cell;这是Mock协议/持久状态负载证据,不是RTP或生产容量证据。R1历史探针见`docs/evidence/asterisk-real-2026-09-12.md`;ulaw初始探针见`docs/evidence/asterisk-persistent-2026-09-14.md`,PCMA复测见`docs/evidence/asterisk-persistent-pcma-2026-09-14.md`:ECS `i-2ze8fa5vhyl4cc8oeol9`、Asterisk容器和固定EIP `123.56.71.98` 已核验,PCMA实际SDP为 `PCMA/8000`,两个目标均由原先的 `488/Q.850 cause=88` 变为 `480/Q.850 cause=16`,仍未接通、无RTP/录音证据。该 ECS 按用户要求继续保留并运行,固定 EIP 为 `InUse`。Bailian ASR/LLM/CosyVoice 文件链路已完成真实探针;真实电话接通/RTP、MQ/OSS、生产1000路/N+1尚未完成。正式模式仍拒绝当前Mock调度profile,不能把测试号码或Mock成功自动升级为真实外呼授权。
### 7.4 LLM/音色专项
```bash
python3 scripts/test_ai_call.py text --model mock-chat-v1 \
--prompt-file prompts/test-call.txt --input '隔离文本验收'
python3 scripts/test_ai_call.py text --profile configs/ai-test.bailian.example.yaml \
--mode real --input '请只回答收到。' --output-audio .local/agent-call-ai/bailian.wav
python3 scripts/test_ai_call.py audio --profile configs/ai-test.bailian.example.yaml \
--mode real --input .local/agent-call-ai/bailian.wav
python3 scripts/run_real_cell_call.py --profile configs/ai-test.bailian.example.yaml \
--callee 15003164745 --allow-real-call
```
`text`/`audio` 不拨号;Bailian profile 的 real `text`/`audio` 会产生真实供应商调用;真实电话必须使用单独的 `run_real_cell_call.py`,显式经 RabbitMQ `call.execute`、Cell/ARI/RTP 门禁和号码 allow-list。`accept_ai_call.py` 不自动拨号。当前专项报告为 `INCOMPLETE`:单次授权电话 SIP 已接通但 RTP/三轮/有效录音未通过,AI-06/AI-07 仍待人工听测,MQ/OSS 生产交接未验收,详见上述 JSON。
## 8. 下一步需要用户提供/确认
1. 供应商需继续确认`7089`前缀/目标号码路由、From域(当前实际出现私网域)及 `480/Q.850 cause=16` 的具体含义;PCMA 已在 SDP 中实际生效,但两个号码仍未接通。
2. 供应商调整并确认线路规则后,再用当前保留环境复测;未获清理指令前不停止/删除实例、不创建第二台,不自动重试、不改绑或释放固定EIP。
3. ASR测试凭据、批准的模型/资源ID;Web HTTPS域名/证书或SSH访问方案。
4. 用户制定的MQ/OSS ID接口规范,以及新的LLM/TTS协议、参数与取消/打断规则。
5. 真实接通后仍需分别完成RTP、录音、MQ/OSS、ASR/LLM/TTS和生产1000路/N+1验收。
4. 真实 RabbitMQ/OSS inbox、上传ID和录音保留规则,以及 Cell/ARI/RTP 执行授权。
5. 真实接通后仍需分别完成RTP、录音、MQ/OSS、三轮电话人工听测和生产1000路/N+1验收。
PCMA 复测已完成;在供应商进一步确认 `480/cause=16` 对应的路由/号码可达性前,不自动再次拨打真实号码,也不声称完成整个平台。当前竞价测试实例按用户要求保留,后续清理需单独明确授权。