feat(ai): play configured closing remarks before keyword hangup

This commit is contained in:
2026-10-07 20:58:49 +08:00
parent 0bdf58b7ac
commit 2435fef5c0
21 changed files with 554 additions and 163 deletions
+33
View File
@@ -0,0 +1,33 @@
# 挂断关键词结束语:本地验证
## 范围与规则
使用者确认将 `agent.conversation.hangup_keywords` 改为对象数组。唯一字段规范和正例仍在 `contracts/local/`;业务说明在 `docs/thirds/saas-dispatcher.md`,不另维护平行字段定义。
- 仅用户最终 ASR 文本的字面包含匹配有效;同时命中多组时按配置顺序取第一组。
- 使用获批 TTS 合成对应结束语,完整发送音频后主动挂断,不调用 LLM 生成结束语。
- 命中后禁止新一轮对话;重复识别、合成失败及未知挂断均不触发重播或重试。
- 合成或播放失败保留关键词命中事实,明确返回错误并尝试结束通话;多个错误同时返回。未成功发送的音频不报告为完整播放。
- 旧字符串数组、缺字段、空字段及未知字段直接拒绝;ASR-only 不合成结束语。
## 可执行证据
| 检查 | 当前测试 |
| --- | --- |
| 新结构接纳、旧结构/空字段/错误大小写拒绝 | `TestBindHangupGroups`、`TestBindRejectsInvalidHangupGroups`、`TestCurrentContractExamples` 及新增旧结构反例 |
| 用户最终文本、字面匹配、组顺序、快照复制、并发单次预留 | `TestKeywordHangupOnlyFinalUserLiteralAndOrderedGroups`、`TestKeywordHangupConcurrentReservations` |
| 播完结束语后才挂断,且不再开始下一轮 | `TestApprovedKeywordPlaysClosingBeforeHangup`、`TestKeywordHangupWaitsForPlaybackReturn` |
| 合成/播放/挂断失败、多个错误并存、禁止重播 | `TestKeywordClosingFailuresRemainVisibleAndHangupOnce`、`TestKeywordHangupFailureVisibleWithoutRetry`、`TestKeywordHangupUsesFinalUserTextOnly` |
| 官方 TTS SDK、批准配置、结束语正文与发送/挂断顺序 | `TestApprovedFullAIUsesBoundProviderSDKsAndOnlyFinalUserKeyword`:真实 SDK 对接本机模拟 HTTP 服务;最终 ASR 文本为显式测试输入,不代表真实 ASR 识别成功 |
| 隔离 Mock 不捏造结束语音频 | `TestApprovedMockFullAIFreezesScriptAndReturnsKeywordClosing` |
P08 表保留当时的历史结果;其中旧 `TestKeywordHangupOnlyFinalUserLiteralAndOnlyOnce` 和 `TestKeywordHangupConcurrentDuplicateFinalTriggersOnce` 的现行替代分别为上表的顺序组匹配与并发预留测试。本变更涉及 A01/A04/A05/A08/A11 和 K09;其他业务边界不变,由完整回归及隔离 MQ/HTTPS/双向 TLS 测试核验。
## 本轮结果
- TDD:新配置和播放顺序测试先失败,完成实现后通过。
- `PATH=/tmp/sip-go-agent-tools/bin:$PATH make check`:通过;包括格式、现行合同和 Proto 来源/hash、`go vet ./...`、`go test -race ./...`、构建,以及实际启动临时 RabbitMQ 的隔离端到端测试。
- `bash scripts/coverage.sh`:手写业务语句覆盖率 **70.1%**,仅排除生成目录,超过 65%。
- `git diff --check`:通过。
仅修改并验证本地代码,没有部署、真实拨号、真实 AI/OSS 服务调用,也没有读取私有凭据或处置旧 SQLite、录音、恢复文件。音频发送顺序的本地验证不证明真实线路的远端播放或生产签收。
+1 -1
View File
@@ -36,7 +36,7 @@ RabbitMQ 是 Topic,**SaaS 独占创建、绑定、退役 exchange/queue,D
- 当前 TTS 唯一获批适配器是 `bailian_tts`:任务快照须明确提供 `qwen3-tts-flash`、`Cherry`、`Chinese`、速度 `1` 和单声道 16 kHz PCM16 目标格式;使用已核验的 provider 凭据及生成端点。每段仅发起一次生成请求,下载返回的短期音频引用后转换为电话可用的 PCM16;不可用、超时、缺少转换工具或参数不支持时显式失败,不回退旧火山 TTS、不隐式重试或记录签名音频 URL。历史测试凭据不是任务授权,本地转换 Mock 不构成真实百炼/通话验收。
- 每条 SaaS SIP 线路只配置一个原值 `caller_id`;任务只选获准线路,不再带 `caller_profile_id`,线路不再带 `caller_profiles`。Dispatcher 将线路主叫绑定到每次执行;Agent 在 Asterisk PJSIP 线路同时设置 `from_user` 与 `callerid`,核对实际加载的出站身份后才准入,不以 ARI 创建通道后的 `CALLERID(num)` 代替 SIP `From`。主叫不是 Digest 用户名,不从环境、CLI 或被叫前缀推断。被叫号码只来自归属当前 D、租户及已接纳任务的 `call.execute.payload.callee` 原值;不在 Dispatcher、SaaS Mock 或抓证脚本另设固定号码列表,也不在任务快照增加号码列表。Dispatcher 与非生产发布/抓证入口只接受 1–32 位 ASCII 数字的原始号码,不能把线路前缀当成该号码的本地替代值。已选 SIP trunk、任务与线路每周时段、任务排除日期、任务/租户/线路额度、任务与 AI 较小通话时限均在接纳及实际发呼叫指令前检查。线路字段未知则 fail-closed;选线后固定、不自动重拨/换线。隔离 Mock 中规则暂不满足时保留待执行指令、暂停该任务的调度,规则允许后重验;与人工 pause/stop 分离,不能自动解除人为停止。非生产真实路径不另设固定的 `09:00`–`20:00` 时间门禁或每线路每原始号码每日 3 次上限;任务/线路时段与额度以 SaaS 已校验快照为准。逐通抓证、Agent 活跃凭证及使用者逐次授权仍须满足;格式有效或本地 Mock 收件均不构成真实拨号授权。
- 接通事实为真时 `outcome=answered`(后续异常不抹掉接通);已发起但忙线、拒接、无人接听且确定结束为 `no_answer`;确认未接通并由 Agent/Asterisk 执行故障终结为 `failed`;未知状态保持未知占用,不能伪造结束、结果或自动重拨。真实 SIP 状态码原样数字写入 `reason_code`,无真实 SIP 码则 `null` 并以 `reason_message` 说明;禁止本地虚构数字错误码。`call.execute.result.payload` 的 `status_line`、`raw` 与 `sip_capture_error` 始终存在:仅将经同一 ARI 通道拨号前取得的 SIP Call-ID 与 HEP INVITE 事务严格关联的最终响应写入原样状态行、完整原样报文与状态码;`raw` 不拼装、不截断,不能从目标号码、时间、挂断原因或 ARI HTTP 状态猜测。无 SIP 响应时前两项为 `null`;若已发起 SIP 但镜像/关联/解码失败,第三项须写明确错误,已确认结束仍报告真实结果并释放额度,不以原文缺失伪装为通话未知。原始报文只进入受控结果通道,不写日志、仓库或长期测试证据。无应答且没有录音时 `transcript=[]`、`opt_out=false`、`recording={}`。
- 只有**用户侧 ASR 最终识别文本**包含任一 `hangup_keywords` 字面字符串才挂断;中间识别、助手回复、开场白、TTS 均不能触发;重复结果不可反复终结。同一任务 revision 不同内容拒绝准入;provider 禁用/角色不符不可调用。Mock 参数验证不等于真实供应商验收。
- `agent.conversation.hangup_keywords` 使用对象数组,字段及正例以现行 Schema 为准;旧字符串数组直接拒绝。只有**用户侧 ASR 最终识别文本**字面包含某组 `triggers` 才触发,同时命中多组按配置顺序取第一组;使用该组 `closingRemark` 合成语音,完整播放后主动挂断,不调用 LLM 生成结束语。匹配后不再开启新一轮对话,重复识别不得重播或重复挂断;中间识别、助手回复、开场白、TTS 均不能触发。合成或播放失败时明确报错并尝试结束通话,不重试、不把未播放音频报告为已播放;挂断结果不明仍保留未知事实。组名、触发词列表及结束语均必填且非空;ASR-only 不配置或合成结束语。同一任务 revision 不同内容拒绝准入;provider 禁用/角色不符不可调用。Mock 参数验证不等于真实供应商验收。
## 录音与最终结果(K10、K15、K16)