Files
go-sip/docs/evidence/saas-dispatcher-implementation.md
T

75 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SaaS↔Dispatcher 项目内实施证据
## 改动前基线
- 基线提交:`f5c2d6a92036a579e0b070beb1d9381b0976f81e`;执行分支:`feat/saas-dispatcher-contract`;开始前已拉取并核对 `origin/main`,无待同步提交;Go 1.27.1。
- 原工作树已有用户修改:`docs/thirds/v0.4.md`、`docs/thirds/第三方对接事件与请求消费顺序_v0.1.md`;未跟踪:`docs/plan-saas-dispatcher-v05-v0.1.md`、`docs/thirds/v0.5-proposal.md`、`docs/.plan-saas-dispatcher-v05-v0.1.md.swp`。这些内容不得作为已提交基线,不能静默删除、暂存或覆盖;历史来源在改名时逐一核验。
- 全仓路径初筛发现 182 个代次式候选文件(包括历史证据/第三方合法版本,**不是** 182 个可删除目标);源码、契约与脚本约 119 个候选。去后缀会发生实存碰撞:`internal/mq/amqp_v3.go`→`internal/mq/amqp.go`;历史证据文件也有碰撞,历史证据不能按文件名自动改写。
- 尚未验收源码、契约、Proto 及数据兼容;此处只记录实施前状态,不构成 P01–P08 通过证明。
## 路径/符号映射与冲突处置(实施时逐项勾稽)
| 旧路径/符号 | 目标职责与处置 |
| --- | --- |
| `internal/mq/amqp.go` 的旧声明/租户队列 Broker 与 `internal/mq/amqp_v3.go` 的预建队列 Broker | 保留预建队列单一路径,合并必要发布/错误处理后归入 `internal/mq/amqp.go` 的 `Broker`;删除旧运行入口,不能直接覆盖。 |
| `internal/configread/discovery_v04.go` | `internal/configread/discovery.go`,重做新 cursor 语义,删除旧 snapshot/watermark 路径。 |
| `internal/dispatcher/task_queue_v3.go` / `task_control_v3.go` / `task_runtime_v3.go` | `task_queue.go` / `task_control.go` / `task_runtime.go`,类型按职责命名,不保留 V3/LocalV01 别名。 |
| `internal/dispatcher/call_result_v01.go` / `local_v01.go` | `call_result.go` / `local.go`;只留已确认的对外事件。 |
| `proto/agent/v1/agent.proto` / `gen/agent/v1/` | `proto/agent/agent.proto` / `gen/agent/`,同步生成配置、Go 导入与服务名,重新生成而非手改生成物。 |
| `contracts/local/v0.x/`、`docs/contracts/*-v0.x*` 与按代次加载/校验器 | 各领域只有一份无代次当前 Schema、示例及来源/hash;历史来源只能归档,不参与运行校验。 |
| `internal/store/migrations/*_local_v0x_*.sql` | 保留有意义的迁移序号并更换实现代次名;先验证现存数据库的迁移跟踪/未交付状态,不能以改名重跑破坏数据。 |
| `docs/plan-saas-dispatcher-v05-v0.1.md` / `docs/thirds/v0.5-proposal.md` | 分别成为 `docs/plan-saas-dispatcher.md` / `docs/thirds/saas-dispatcher.md`;既有未跟踪内容须先核对,链接/哈希同步更新。 |
## P01:项目内当前通信契约
- 新增 `contracts/local/config-read.schema.json`、`task-discovery.schema.json`、`mq.schema.json`、`mq-topology.json` 与 30 份正反例,manifest 覆盖 34 个当前 JSON 文件;`contracts.ReadCurrent` 只接受这五个当前文件,无旧目录回退。
- `docs/thirds/saas-dispatcher.md` 记录五类 HTTP、入站/出站事件、队列归属/共享结果队列及 K01–K16 的业务边界;数字 tenant_id、原值 credential、ASR-only、revision、空录音、真实 outcome/reason_code、48h 失败留待人工等均有字段/行为约定。机器校验拒绝字符串租户、旧 schema_version/credential_ref/call_id、未知 recording 状态、非法控制 policy。
- 测试:`bash scripts/check-current-contracts.sh`、`go test ./internal/contract ./contracts -count=1`、`git diff --check` 均通过;当前正例未出现旧配置或通话身份字段。来源/内容 SHA-256 在 manifest 中可重算。**这是当前合同文件的本地校验,不代表 P03–P08 运行行为、真实 SaaS 或旧合同清理通过。**
## P02:Proto/生成物分批改造(未整体签收)
- `proto/agent/v1/agent.proto`→`proto/agent/agent.proto`,包 `agent`;`gen/agent/v1/`→`gen/agent/`,源码 79 处导入/标识机械更新;旧生成路径已删除。已有 `RequestMeta.protocol_version=agent.v1` 是**有意义的线协议元数据**,没有为了去名改动字段或重编号。
- 原 Buf STANDARD 的 `PACKAGE_VERSION_SUFFIX` 与已批准的无代次内部包名冲突;`buf.yaml` 仅对这一条规则作例外,其余 STANDARD 规则保持。隔离安装 Buf v1.50.0 / protoc-gen-go v1.36.12 / protoc-gen-go-grpc v1.5.1 于 `/tmp/sip-go-agent-tools/bin`,未改应用依赖。
- `PATH=/tmp/sip-go-agent-tools/bin:$PATH sh scripts/check-proto.sh` 通过(lint/build/generate/新包测试/7 文件清单 hash);`git diff --check`、`go test ./internal/rpc ./internal/agent ./internal/dispatcher ./cmd/sip-go-agent -count=1` 通过。其他旧实现/契约入口、文件名碰撞和迁移数据安全仍待 P02/P07,**不能据此称全仓已无代次或整体完成**。
## P03:HTTP 读取分批改造(未整体签收)
- `contract.ValidateCurrent` 与 `configread` 按当前 Schema 读取 SIP、provider、task、quota 和 cursor 任务发现;严格检查数字 tenant_id、本 D 归属及不可变配置。provider 凭据原值只保留在内存快照,不写日志;Agent 参数中的显式 0/false 保真;无旧 Schema/旧配置回退。
- `store.OpenCurrent` 新建数字租户 SQLite 状态;旧表、旧版当前布局、残缺布局均在写入前拒绝并保留原记录;不执行旧数据迁移或自动清理。启动时完整发现同一快照一次提交,分页增量逐页持久提交后才推进**内存** cursor;失败关闭准入,重启重新取完整快照。HTTP 的旧 running 不能解除 MQ 暂停/终止,同 revision 异内容及跨任务 SIP/租户额度冲突拒绝。
- `CurrentBootstrap` 先关闭准入,核验 SIP 全量与 Agent/Asterisk 已加载 revision、读取任务和 provider/额度,再排空 MQ 控制积压,最后依据已验证 SIP revision 开准入;有更新的持久 SIP 通知时保持关闭但控制与结果处理仍可继续。`CurrentDiscoveryFollower` 逐页绑定任务快照;HTTP 错误、失效或授权不一致只失败,不回退旧读取。**目前只在隔离运行组件中调用,尚未接入 `cmd/sip-go-agent/main.go`;provider 向真实 Agent 交付及真实加载尚待 P05/P07。**
- TDD 与回归:`go test ./internal/configread ./internal/tenant ./internal/store ./internal/dispatcher -count=1`、`bash scripts/check-current-contracts.sh`、已提交 `a0118e3` 的干净归档测试通过;旧布局行/表原样保留由 `TestCurrentStoreRefusesPreviousCurrentLayoutBeforeModifyingDatabase` 覆盖。
## P04:隔离 MQ、控制与外呼接纳(仅项目内 Mock)
- 固定 `v1` 精确路由与 SaaS 共享结果队列;各 D 控制/任务队列均由 SaaS 预建,D 仅被动检查和消费。隔离 RabbitMQ 实测无 `configure` 权限、D1/D2 不串收、shared queue 实际收讫、断绑后 mandatory 失败不算交付;畸形控制积压拒绝并阻止启动准入。
- 执行消息先写持久 inbox 才 ACK;白名单号码格式错误单条拒绝,不暂停其它任务;任务/线路规则不满足时原执行身份留在 SQLite、该任务后续积压留在 SaaS 队列,窗口开放后重新核验才发出 Mock 指令。发指令前两次时窗/SIP/准入检查与持久额度占用;未知 Agent RPC 保持未知占用、不自动重拨。结果 outbox 只在 mandatory/return/confirm 成功后标记已入队,不宣称 SaaS 已处理。
- 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 隔离链路(项目内组件通过,主入口待 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,不能宣称真实通话媒体已验证。
- 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 结果代签。
## P06:录音/OSS/最终结果(进行中,未签收)
- 已新增 Agent 内存录音单次 PUT 组件 `UploadClient.UploadBytes`:复用受限授权、大小和 SHA-256 核对,成功路径不写临时录音或通话结果文件。OSS 明确拒绝保留状态码且不自动重试;传输结果不明时返回专用错误、停止自动重试并隐藏带签名的 URL。空授权头拒绝而非使服务崩溃。这里只验证组件,尚未连接实际录音或最终结果。
- 内部 OSS 授权增加 Dispatcher 原始 bucket,官方 SDK 签发时返回获批 bucket;Agent 收到缺少 bucket 的授权会在 PUT 前拒绝,失败恢复的显式重申请若返回了不同 bucket,也在再次 PUT 前拒绝。两条红灯测试证明先前会错误上传;修复后全包测试及 Agent/RPC/OSS race 测试通过。这里只核验本地授权载体,不代表新主入口已完成签发或真实 OSS 已验证。
- Agent 失败恢复隔离组件:仅在 OSS PUT 明确失败后,把原 bucket/object_key 对应的录音和通话信息两文件写入私有目录并同步落盘;双文件缺失或损坏明确报错、不伪造结果。完成保存后固定 48 小时窗口,按 1 分钟递增至最长 1 小时重试;到期保留原文件。PUT 前持久写入 in-flight,结果不明或进程重启不会二次 PUT;确认上传后先持久记录成功,再经注入的 Mock 回报通话结果,回报失败/重启仅重发原结果。启动扫描识别遗漏文件并提供不暴露原路径的稳定摘要;真实 Dispatcher 授权及回报尚未接线。
- Dispatcher 的无录音最终结果隔离组件:`CurrentStore.RecordCallResult` 仅在确认通话结束后,按持久任务快照校验任务、被叫、主叫和已选线路,并以源执行事件固定生成唯一最终结果身份;消息通过严格 MQ Schema 校验后与 outbox 在同一事务写入。同内容重投/重启只恢复原消息,冲突结果和 SQLite 写入失败均不会产生第二份结果。这里只验证隔离组件,Agent 实际回报尚未连通。
- Dispatcher 的原始 OSS 目标及已上传结果隔离组件:新 SQLite 布局把一次通话的 upload_id、bucket、object_key、录音格式/时长/大小和 SHA-256 唯一绑定到已保留的执行;不保存临时 URL 或 TOKEN。旧布局拒绝启动并原样保留待交付 outbox,不自动迁移或清理。已签发录音目标不能通过空录音结果绕过上传;已有空录音最终结果不能再签发录音目标。`RecordUploadedCallResult` 仅接受与持久绑定完全一致的录音事实及 Agent 所报告的成功 PUT 状态,录音确认与唯一最终结果 outbox 同事务提交;丢失回报或 MQ 投递时重用原消息,已确认后拒绝再次签发 PUT 授权。Mock 证明的是本地状态约束,不是独立 OSS 校验或真实 Agent 身份验证。
- Dispatcher 的终结与外呼回执顺序竞争隔离修复:原流程在 Agent 接受执行的 RPC 返回后才写入外呼回执,快速结束或 RPC 超时可能先到;现以一次 SQLite 事务在确认通话已结束后补齐原回执并释放占用,未知执行仅在确认结束后释放。迟到的执行响应、超时和重复结束不会产生第二份回执;注入 outbox 写入失败保留原占用。并发竞争及结束后立即生成唯一最终结果有单元测试;主入口真实 Agent 会话注入与通话执行仍未接线。
- Dispatcher 录音事实 Unary RPC 隔离服务:`RequestRecordingUpload`、`ReportCallEnded`、`ReportCallResult` 均要求已配置本 D、核验 mTLS 指纹及当前 Agent 会话、数字租户和已保留的执行;复用官方 SDK 仅对原始录音签发固定 15 分钟授权,显式重申请仍用相同 bucket/object_key。结束事实可先于外呼响应而持久化原回执;录音结果核对 D 已存目标和 Agent 报告的成功 PUT,再与唯一结果 outbox 同事务提交。Mock 覆盖会话/租户拒绝、同资产重申请、上传前结果拒绝、坏 JSON、已上传与无录音结果及重复回报。隔离测试还通过本地双向 TLS 的 gRPC 实际传输:Agent `RecordingClient` 每次读取并克隆当前会话元数据,经受控 D 客户端领取授权、上报结束和唯一结果;未配置客户端明确拒绝。不包含实际 OSS PUT 或主入口批准执行的媒体录音接线。
- 内存录音隔离组件:`RecordingSession` 仅复制共享通话流程实际读到和成功发送的 16-kHz PCM16,`EncodeMonoWAV` 直接在内存生成有界单声道 WAV;空音频、奇数字节、超过上限及未成功发送的音频都不能伪造成可上传录音。单元与 race 测试未产生业务文件。批准执行入口尚未接入该组件,且 Mock 中观测到的帧不等于真实 Asterisk 通话的全量媒体验收。
- Agent 录音交付隔离组件:`RecordingDelivery` 先确认结束,再依照录音是否实际生成分别上报唯一空录音结果或请求原授权并直传内存 WAV;录音生成失败保留通话真实结果、空录音对象及明确原因,不虚构上传事实。隔离测试通过本地 HTTP PUT 和假 Dispatcher RPC 覆盖成功无业务文件、OSS 明确失败后私有文件保存、恢复写入失败、未知 PUT 隔离、重启重领原目标、上传已确认后只重发原结果。再次调用不会隐式重新 PUT;正常已确认上传但尚未被 D 持久收讫的跨进程间隙仍受 K16 边界约束。此处未连接真实 D gRPC、主入口批准执行媒体或 MQ。
- 已验证:`go test ./... -count=1`、`go test -race ./internal/agent ./internal/rpc ./internal/store ./internal/callflow ./internal/media -count=1`、`go vet ./...`、`go build ./...`、`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`、`git diff --check`。尚未完成批准执行媒体录音到 Agent 交付组件的主入口接线、主入口 Agent↔Dispatcher 实际会话与录音执行接线(隔离 mTLS gRPC 已测)、真实执行时上传事实/最终结果交付及 MQ/端到端验收,不能宣称 P06 通过。
## 验收台账
A01–A12 的行为验证及 K01–K16 的运行时验证待 P03–P08 逐项填充;不得用本地 Mock 冒充外部签收。