From 9ecf778d6bd4e7df6de195e8ffe9683160a4a725 Mon Sep 17 00:00:00 2001 From: Rogee Date: Tue, 29 Sep 2026 22:29:14 +0800 Subject: [PATCH] Reject Agent media sample rate mismatches --- .../saas-dispatcher-implementation.md | 7 +++-- internal/ai/current.go | 6 ++++ internal/ai/current_media_profile_test.go | 30 +++++++++++++++++++ 3 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 internal/ai/current_media_profile_test.go diff --git a/docs/evidence/saas-dispatcher-implementation.md b/docs/evidence/saas-dispatcher-implementation.md index fa31922..4676f09 100644 --- a/docs/evidence/saas-dispatcher-implementation.md +++ b/docs/evidence/saas-dispatcher-implementation.md @@ -46,14 +46,15 @@ - pause/stop/resume 控制先持久挡住该任务新呼叫,Agent 确认收到指令后在同一事务提交应用状态和回执;省略策略默认 hangup,重复控制仍执行、重复回执复用事件身份,stopped 同 ID 不可恢复,未接纳旧外呼静默 ACK。SIP 通知先持久关闭准入,旧/未知通话未确认终结、SaaS 新版尚未分发或 Agent/Asterisk 未加载时不重开;等待期间控制和 outbox 仍可处理。 - TDD 与隔离验证:`go test ./internal/store ./internal/dispatcher ./internal/mq -count=1`、`bash scripts/check-current-mq-mock.sh`、`git diff --check` 通过;Mock 覆盖任务积压、恢复、控制、重复投递、发布失败、SIP revision 栅栏与格式错误。当前 Agent side effect 为注入的**假外呼**,主 CLI 仍旧;真实 SaaS/RabbitMQ、供应商、线路、录音与 `call.result` 均**未验收**,分别留 P05–P08。 -## P05:Agent 快照与 SDK 隔离链路(分批,未整体签收) +## P05:Agent 快照与 SDK 隔离链路(项目内组件通过,主入口待 P07) - Dispatcher 在启动、增量发现、恢复任务、SIP 更新及发指令前对完整 AI/provider 快照执行能力校验;不支持的任务关闭准入而不占额度或呼叫。已按用户确认保留现行 TTS Schema:火山 TTS V2 SDK 无法表达的 PCMA、0.5–2 倍以外或整数 speech_rate 无法精确表达的速度均明确拒绝,不静默修改配置;不支持的插话配置同样拒绝。 - `ExecuteApproved` 只在隔离 Mock 中签发:携带任务原始 JSON、引用的 provider 明文凭据、SIP revision、选定路由/主叫/原始被叫、独立的拨号期限及最大通话时限;任务+provider 原始字节+SIP revision 计算绑定摘要。Agent 对照已激活的 D 会话身份、绑定摘要、SDK 能力及**实际观察到的** SIP 加载结果;发出 Mock 指令前仅持久保存摘要和未知占用,失败/重复/重启不自动重拨,文件中不留明文凭据。当前 `LoadedSIP` 为可注入的隔离 Mock 观察器,**不等于真实 Asterisk 已加载核验**。 - ASR-only 不启动 LLM/TTS;full-AI 使用冻结的 model、显式 temperature=0/max_tokens、TTS voice/format/speech_rate 与 provider 原值凭据。真实 ASR SDK WebSocket 发出的音频输入/识别配置、LLM 与 TTS SDK 向隔离 HTTP Mock 发出的实际参数均已捕获核对;已配置的开场白由 TTS 单次合成,失败不自动重播且未完成时不进入对话轮次;空开场白不调用 TTS。关键词仅匹配用户侧最终 ASR,失败/未知挂断不自动重试。`ApprovedOriginator` 经生成的 Unary gRPC Stub 交付完整快照,SIP 全量 revision 不吻合即拒绝。 - 对话控制隔离验证:共享 `callflow` 不再根据转写或回复内的硬编码词推断拒联,仅处理明确的关键词/拒联事实;ASR-only 不播报开场或 TTS 回复。配置的首语音/静默期限、整通话期限与最大轮数交给媒体控制器;回复按 Unicode 字符数分片,火山 TTS 的缓存音频块数超限明确失败而不重试;不支持的插话配置在准入前拒绝。该控制器尚未接入新主 CLI,不能宣称真实通话媒体已验证。 -- 验证:`PATH=/tmp/sip-go-agent-tools/bin:$PATH bash scripts/check-proto.sh`、`go test -race ./internal/ai ./internal/callflow ./internal/configread ./internal/dispatcher ./internal/rpc -count=1`、`go vet ./...`、`go build ./...`、`git diff --check` 均通过。 -- **尚未完成:** 新主 CLI 接入、对话控制与真实媒体的完整联动、录音/OSS/最终结果及真实 Agent/Asterisk、SaaS、MQ、AI 供应商联调。这些不能被本地 Mock 结果代签,分别属于 P05 收尾、P06–P08 或第二阶段外部验收。 +- Agent 隔离媒体入口 `rpc.RunApprovedCall` 仅接收签发的执行快照、媒体会话和挂断动作:签发通话期限与 AI 总期限均约束整通电话;ASR-only 不合成开场,full-AI 开场完成才采集语音。父级期限不会被“未检测到语音”掩盖,空媒体 Mock 等到上下文真正结束。当前媒体只处理 16 kHz PCM16,SDK 虽接受、但媒体不能正确处理的 24 kHz ASR/TTS 配置在 Dispatcher 与 Agent 绑定时明确拒绝,不静默播放或识别错速音频。该入口尚未连接主 CLI,也未用于真实拨号。 +- 验证:`PATH=/tmp/sip-go-agent-tools/bin:$PATH bash scripts/check-current-contracts.sh`、`PATH=/tmp/sip-go-agent-tools/bin:$PATH bash scripts/check-proto.sh`、`go test ./... -count=1`、`go test -race ./internal/ai ./internal/callflow ./internal/configread ./internal/dispatcher ./internal/rpc -count=1`、`go vet ./...`、`go build ./...`、`git diff --check` 均通过。 +- **后续边界:** 录音直传/OSS 失败恢复及最终结果属 P06;新主 CLI 接线、真实媒体完整联动及旧路径清理属 P07;隔离端到端和 A01–A12 属 P08。真实 Agent/Asterisk、SaaS、MQ、AI 供应商联调未开展,不能由 Mock 结果代签。 ## 验收台账 diff --git a/internal/ai/current.go b/internal/ai/current.go index 27c6655..647b6d4 100644 --- a/internal/ai/current.go +++ b/internal/ai/current.go @@ -144,6 +144,9 @@ func BindCurrent(task configread.CurrentTask, providers map[string]configread.Cu if err != nil { return CurrentBound{}, fmt.Errorf("ASR input: %w", err) } + if sampleRate != doubaospeech.SampleRate(16000) { + return CurrentBound{}, errors.New("ASR media requires 16000 Hz PCM16") + } lang := doubaospeech.Language(settings.ASR.Language) switch lang { case doubaospeech.LanguageZhCN, doubaospeech.LanguageEnUS, doubaospeech.LanguageJaJP, doubaospeech.LanguageKoKR: @@ -201,6 +204,9 @@ func BindCurrent(task configread.CurrentTask, providers map[string]configread.Cu if err != nil { return CurrentBound{}, fmt.Errorf("TTS output: %w", err) } + if ttsSampleRate != doubaospeech.SampleRate(16000) { + return CurrentBound{}, errors.New("TTS media requires 16000 Hz PCM16") + } ttsRate := 0 if settings.TTS.Speed != nil { floatRate := (*settings.TTS.Speed - 1) * 100 diff --git a/internal/ai/current_media_profile_test.go b/internal/ai/current_media_profile_test.go new file mode 100644 index 0000000..ec218f7 --- /dev/null +++ b/internal/ai/current_media_profile_test.go @@ -0,0 +1,30 @@ +package ai + +import ( + "strings" + "testing" +) + +func TestCurrentBindRejectsSampleRatesUnsupportedByAgentMedia(t *testing.T) { + for _, tc := range []struct { + name, mode, endpoint string + }{ + {name: "ASR-only capture", mode: "asr_only", endpoint: "asr"}, + {name: "full-AI capture", mode: "full_ai", endpoint: "asr"}, + {name: "full-AI playback", mode: "full_ai", endpoint: "tts"}, + } { + t.Run(tc.name, func(t *testing.T) { + task, providers := currentFixture(t, tc.mode) + task = changeCurrentAgent(t, task, func(agent map[string]any) { + if tc.endpoint == "asr" { + agent["asr"].(map[string]any)["input"].(map[string]any)["sample_rate_hz"] = 24000 + } else { + agent["tts"].(map[string]any)["format"].(map[string]any)["sample_rate_hz"] = 24000 + } + }) + if _, err := BindCurrent(task, providers); err == nil || !strings.Contains(err.Error(), "media") { + t.Fatalf("SDK-supported sample rate cannot silently mismatch 16 kHz Agent media: %v", err) + } + }) + } +}