Files
go-sip/docs/contracts/saas-dispatcher.md
T

22 KiB
Raw Blame History

SaaS ↔ Dispatcher 对接契约(MQ-only 设计与实现差异)

1. 适用范围与事实等级

用户已确认:RabbitMQ 是 SaaS 与 Dispatcher 的唯一交互通道,双方之间禁止任何 HTTP 请求或回调。每个 Dispatcher 都有独立、全局唯一的 ID,并通过各自独立的专用 Topic 接收事件。 本规则覆盖执行、控制、查询、补传、AI 配置与授权、录音上传会话及完成验证,不保留 HTTP 特例或回退。

OSS 补充确认:OSS 相关配置存于 Dispatcher 配置文件;Agent 经 Unary 向 Dispatcher 领取临时上传 TOKEN 后直传 OSS,不持有长期凭据。SaaS 不再下发 OSS 配置或上传 TOKEN。此次只调整配置/TOKEN 来源,上传完成后的 SaaS 独立校验和 MQ verified 职责保持不变。

本文区分三种事实:

层级 本次状态 使用边界
已确认设计 MQ-only、Dispatcher 唯一身份、独立 Topic 后续设计与实现必须遵守
待冻结的消息契约 身份分配/持久化、Topic 命名、消息类型、关联字段、错误与超时规则 见 §2、§5、§6;不能据中文语义自行拼 JSON 或给旧 Schema 加字段
现有实现/旧包 下列旧字段、路由及代码事实 仅用于识别差异,不代表新设计已实现或通过验收

当前固定包为 contracts/upstream/2026-09-19-p1-v1/(contracts.SourceCommit = 2026-09-19-p1-v1)。其中的 HTTP OpenAPI 与仅按租户路由的 MQ 拓扑不再是目标方案。旧包及其哈希保持不变;W01 须发布新版本、严格 Schema、拓扑及正反例,不能手改旧包、生成字段索引或通过放宽 additionalProperties 绕过冻结。

本轮只纠正文档,不修改代码、Proto 或 Schema,也不宣称新 MQ 闭环已通过。现有实现事实沿用此前核验记录,受影响部分必须按新基线重新验证。

2. 通信拓扑、Dispatcher 身份与交付语义

2.1 唯一通道与边界

交互 唯一允许的路径 语义
SaaS 下发执行、控制、查询、补传 SaaS → RabbitMQ → 指定 Dispatcher 专用 Topic/队列 持久受理不等于执行完成;响应仍经 MQ
Dispatcher 回传结果、查询响应和业务事件 Dispatcher → RabbitMQ → SaaS 专用订阅 能识别来源 Dispatcher、租户、原请求及业务对象
Dispatcher 获取 AI 配置/授权 Dispatcher → RabbitMQ → SaaS;SaaS → RabbitMQ → 原 Dispatcher 专用订阅 固定租户和不可变版本,响应不能被其它 Dispatcher 消费
业务上传会话、complete/verified Dispatcher ↔ RabbitMQ ↔ SaaS 只传业务会话/对象元信息与验证结果;不下发 OSS 配置或 TOKEN,不传录音字节
临时上传 TOKEN 领取/显式重新申请 Agent ↔ Unary ↔ Dispatcher D 依据自身配置文件提供受限 TOKEN/上传目标;不向 SaaS 申请 TOKEN
Dispatcher ↔ Agent 既有 Unary gRPC 不改为内部 MQ,也不让 Agent 直连 SaaS
Agent → OSS 受限目标上的直接 PUT 保留 HTTP(S) 对象上传;禁止的是 SaaS↔Dispatcher HTTP,不是 OSS/ARI/供应商协议或 gRPC 的 HTTP/2

2.2 全局唯一身份与独立 Topic

  • dispatcher_id 在本文中是逻辑身份名称,尚不是旧 MQ Schema 或 Proto 已有字段。每个 Dispatcher 的 ID 必须独立、全局不重复;不能拿租户 ID、Agent ID、Cell ID、地址或启动代次代替。
  • Dispatcher 身份与 dispatcher_epoch 分开:前者识别 Dispatcher,后者用于一次运行所有权/会话的 fencing。正常重启、恢复时如何保持身份及拒绝重复身份,须在 W01 冻结并由 W05 验证,不因 epoch 改变就丢弃原消息、执行或资产归属。
  • 每个 Dispatcher 有独立的接收 Topic 及对应队列/绑定;多个 Dispatcher 不能共用一条接收队列竞争消费指定目标的消息,也不能全部订阅同一广播 Topic 后仅靠正文过滤。
  • RabbitMQ 的 Topic 订阅由 exchange、routing key、queue 和 binding 表达;具体名称、类型、绑定格式及 ID 在信封/属性中的位置随新版本冻结。本轮不另造一套可直接部署的命名格式。
  • SaaS 发给 D1 的命令、配置、授权和上传结果,只能进入 D1 的专用接收路径;D2 的路径与之独立。D1 发出的响应/事件须能回溯 D1 与原请求。SaaS 订阅布局亦由同一版契约定义,不假定现有共享结果队列已满足新约束。
  • 独立 Dispatcher 路由不替代租户隔离:保留租户独立队列、有界窗口、原值 tenant_key 和复合幂等语义;新拓扑必须同时区分 Dispatcher 与租户,不能退化为 Dispatcher 内所有租户共享无界队列。
  • tenant_key 不清洗、编码或截断。旧布局的 224 UTF-8 字节预算不能在加上 Dispatcher 身份后直接照搬;W01 须校验完整 routing key/queue 名长度及分隔符、通配符边界,超限拒绝发布并保留源任务,不改变既有租户标识。

P1 仍只运行一个单活 Dispatcher。现在必须在合同及本地路由测试中区分两个 Dispatcher 身份;这不授权多节点上线、多 Dispatcher 共享配额、自动选主、HA 或自动迁移任务。未知执行不得因目标离线而改投另一个 Dispatcher。

2.3 请求、响应、持久化和恢复

  1. 所有请求和响应都走 MQ;异步响应必须关联原请求、目标/来源 Dispatcher、原租户及业务对象。精确键名、关联方式、消息枚举、错误与期限在 W01 冻结;trace_id 不能代替业务幂等身份。
  2. 发送意图/业务变更与 outbox 同事务;接收方持久 inbox 和处理状态后才 ACK。相同业务请求的重投返回原决定,同身份异内容冲突,不能生成第二次拨号或上传资产。
  3. publisher confirm、消费者 ACK、业务 accepted、控制 applied 和 SaaS verified 各自独立。confirm 只说明 broker 接收,不等于对端已应用;接收 ACK 不能代替业务响应。
  4. 响应重复、乱序、迟到、丢失和重启后恢复必须按原关联处理;响应等待有界,不跨网络持有 SQLite 写事务。超时表示未获确定结果,不等于业务失败,不允许 HTTP 查询兜底、换 ID 重拨或静默换 Dispatcher。
  5. 队列满、无绑定/不可路由、broker 断连必须可见并保留原消息。不能通过 confirm 单独认定路由成功;须覆盖 mandatory/return 和实际目标消费证据。
  6. MQ 往返响应不意味着新增一套任意 application receipt 协议。已有业务结果和 verified 语义保留;需要补齐的响应消息必须进入版本化契约,不能借现有八类业务事件自由透传。

3. RabbitMQ 执行命令:旧基线与待改项

3.1 旧拓扑(仅作实现差异记录,禁止用于新接入)

元素 旧值 新设计差异
command exchange agent-call.commands.v1,durable direct 须按 §2 冻结面向指定 Dispatcher 的 Topic 拓扑
tenant queue agent-call.executor.{tenant_key}.v1 没有 Dispatcher 身份,不能让多个 D 共用
routing key agent-call.tenant.{tenant_key}.call.execute 仅区分租户/操作,不能唯一指定 Dispatcher
event exchange agent-call.events.v1,durable topic 新发布路径须可识别来源 Dispatcher
dead-letter exchange agent-call.dead-letter.v1,durable topic 恢复必须保留原 Dispatcher、租户和消息身份
默认 prefetch 1 保持有界消费;不是多 Dispatcher 隔离证明

旧实现按消费租户声明 command queue 和 .dlq.v1,SaaS 结果队列基线为 agent-call.saas.events.v1、binding agent-call.#。这些名称记录旧包事实,不构成新拓扑批准。

3.2 旧 call.execute 外壳

以下是旧 Schema 的精确字段记录,additionalProperties: false;新 Dispatcher 路由与关联尚未进入该外壳,不能直接追加字段并宣称兼容。

字段 类型 必填/约束
schema_version string 旧版固定 1.0
command_type string 固定 call.execute
command_id string 1–128 字节;不可含空白、/、\\
tenant_id string 同上
tenant_key string 非空有效 UTF-8;旧实现额外限制 224 字节
trace_id string 同 id 约束
issued_at RFC3339 时间 必填
not_after RFC3339 时间 必填;不能因重投延期
payload object 符合 executePayload

3.3 旧 payload 数据结构

字段 类型 约束
execution_id string 必填 ID
task_id string 必填 ID
task_item_id string 必填 ID
task_revision integer >= 1
callee string 1–256 字符;保留原始被叫号码
route_policy_id string 必填 ID
caller_profile_id string 必填 ID
agent_version_id string 必填 ID
variables object 必填;旧 Schema 允许附加属性,业务白名单仍受源约束
ring_timeout_ms integer >= 1
max_call_duration_ms integer >= 1

对应 internal/contract.ExecutePayload;payload 以 json.RawMessage 保留原始 JSON。改传输不授权改变业务号码、版本、摘要或新增 MQ mode 字段。

3.4 旧接收事实与新验收要求

旧 ConsumeTenant 校验租户、声明队列,以 prefetch=1 消费;AcceptCommand/Store.IngestCommand 校验源 Schema、routing key、not_after 和原始 body SHA-256。当前以 command_id 查 inbox,同 ID 同 body 为重复、异 body 为冲突;新命令同事务写 inbox、task、command.result(accepted) outbox。成功后 ACK;永久错误 Reject(false),其余错误 Nack(requeue=true)。

旧初始结果 payload:

{
  "command_id": "<command_id>",
  "command_type": "call.execute",
  "execution_id": "<execution_id>",
  "status": "accepted",
  "reason_code": "accepted",
  "requested_task_revision": 1
}

新验收须补充 §2 的 Dispatcher 定向/来源校验、所有 MQ 交互的关联和持久恢复,以及租户复合幂等要求。不能把当前仅按 command_id 查重的事实写成这些要求已满足。

4. RabbitMQ 业务事件:旧字段语义与新路由要求

4.1 旧通用外壳

旧 routing key 为 agent-call.{event_type};新来源 Dispatcher 的表达待 W01 冻结。以下字段在旧版全部必填:

{
  "schema_version": "1.0",
  "event_id": "<id>",
  "event_type": "<event_type>",
  "tenant_id": "<id>",
  "tenant_key": "<原值>",
  "trace_id": "<id>",
  "occurred_at": "2026-09-19T00:00:00Z",
  "aggregate_type": "<aggregate>",
  "aggregate_id": "<id>",
  "aggregate_version": 1,
  "payload": {}
}

源枚举为 command.result、call.status、transcript.updated、call.finished、recording.ready、recording.failed、transcript.failed、contact.opt_out。它们不自动覆盖新增的配置/查询/上传响应消息。

事件同时通过 mq.schema.json 和 event-payloads.schema.json;EventBuilder 拒绝未知字段。旧实现由 SQLite 按 aggregate_type + aggregate_id 递增版本,Agent 不能指定版本;新基线仍须验证租户/Dispatcher 归属,不将旧实现等同完整隔离。

4.2 当前代码实际生成的事件

事件 当前代码行为
command.result 命令接收及 EXECUTION_ACCEPTED fact 生成
call.status / call.finished 对应 Agent fact 经 Dispatcher 校验后生成
transcript.updated 实时文字;不得改名 call.transcript
transcript.failed 当前映射为 aggregate_type=transcript,但 Schema 要求 transcript_segment,该路径阻塞
contact.opt_out 对应 Agent fact 经 Dispatcher 校验后生成
recording.ready 旧 CompleteUpload 本地对象验证后同事务写完成状态/outbox;不等于 SaaS MQ verified
recording.failed Schema 已定义,当前无对应 FactKind/生成路径

RECORDING_PROGRESS 只保存 fact,不发布 MQ 事件。上述已知实现差异不因本次文档改写而消失。

4.3 旧专属 payload 关键字段

完整约束仍在固定包 event-payloads.schema.json;下表不是第二套 Schema。

事件 必填字段
command.result command_id, command_type, status, reason_code
call.status call_id, execution_id, call_state, call_version, attempt_id, attempt_state
transcript.updated call_id, turn_id, segment_id, role, revision, text, is_final, start_ms, end_ms, playback_state
call.finished call_id, execution_id, call_version, outcome, started_at, ended_at, duration_ms, reason_code
recording.ready call_id, recording_id, oss_id, format, channels, sample_rate_hz, duration_ms, size_bytes, checksum_sha256
recording.failed call_id, recording_id, stage, reason_code, retryable
transcript.failed call_id, reason_code, retryable
contact.opt_out call_id, task_id, task_item_id, requested_at

新设计中 recording.ready 只能在 D 收到并持久校验 SaaS 的 MQ verified 结果及 oss_id 后发布;PUT 2xx、ETag、本地路径或 D 单独 HEAD 成功都不替代该结果。

4.4 Outbox 交付

旧 Dispatcher.FlushOutbox claim pending/retry 为 dispatching,发布 persistent JSON 并等 confirm;成功记 published,失败记 retry,重启把 dispatching 恢复为 retry。重复 fact 同 fact_id + content_sha256 不生成第二条事件,异摘要冲突。SaaS 按租户/事件身份幂等应用。

新设计将这一持久交付原则覆盖请求与响应,补齐不可路由、来源/目标、相关状态恢复验证。业务事件重投保留原身份、内容和域版本,broker confirm 不替代 SaaS 应用收讫。

5. SaaS → Dispatcher:控制、查询和补传全部经 MQ

5.1 待冻结内容

以下只定义已确认业务语义,消息类型名、信封字段、响应/错误枚举尚未发布。旧 HTTP header、URL 和状态码不能直接作为 MQ 合同,也不能塞进旧 call.execute 或宽松 metadata 中。

5.2 控制任务

SaaS 将 pause/resume/stop 控制发到目标 Dispatcher 专用 Topic。保留原 command_id、租户/任务归属、expected_task_revision CAS、active_call_policy=drain|hangup 与原因语义。D 持久受理后经 MQ 回报 accepted;经 Agent 屏障/挂断事实确认后才能回报 applied。重复控制不重复增加 revision,冲突不能伪装成功,stopped 不可恢复。

5.3 查询命令与通话

请求和响应都经 MQ;查询固定原 command_id 或 call_id,返回可证明的命令、执行、控制、通话及独立资产状态。响应关联原查询和目标 Dispatcher;不存在、保留过期与暂时不可用分开表达,精确错误码待冻结。超时不走 HTTP 补查,也不证明原执行未发生。

5.4 整体补传

仅允许以 call_id 或 source_command_id 请求整体业务结果补传,不增加 task/execution 范围或局部筛选。固定受理截止点,重发原事件 ID/内容/版本,实时优先、分批有界;补传自身结果不能递归进入集合。

补传不是把 call.execute 重新发布来重新执行。旧 HTTP source-command replay 当前重发原命令的行为不满足目标整体结果补传,须列入 W12 修正;不能因改为 MQ 就保留这一错误语义。

5.5 已废弃 HTTP 入口的处理

internal/control/http.go 当前存在控制、命令查询/补传 handler,通话查询/补传路由固定返回 404。这些只是旧实现事实,不再是可接入或待扩展的 SaaS 接口。W05/W12 应移除这些 SaaS HTTP 业务入口及相关部署说明,不保留兼容层、并行双通道或 HTTP 兜底。本轮未改代码,不能宣称入口已移除。

6. Dispatcher → SaaS:AI 配置与上传业务协调经 MQ

6.1 Dispatcher 配置、临时 TOKEN 与 complete/verified

  1. OSS 配置唯一来源是 D 的配置文件,包括所需服务地址、bucket、对象路径规则及签发授权所需的受控凭据配置/引用。配置文件格式及具体字段沿现有能力核验后冻结,本轮不新增猜测的配置键,也不在文档/样例/源码/日志中写实际密钥或完整 TOKEN。配置缺失或无效须明确失败,不改向 SaaS 取配置,不用 Agent 本地配置兜底。
  2. Agent 经 R12 向 D 领取临时上传 TOKEN。D 校验并持久关联原租户/执行/资产,依据自身配置复用官方 SDK 提供仅本对象可用、有有效期/方法/大小约束的 TOKEN 及必要上传目标信息,经 Unary 返回 Agent。Agent 不取得 D 的长期凭据或完整配置文件。精确 TOKEN 形态及与现有 UploadGrant 的映射待核验,不假定某个 SDK/Proto 已满足全部约束。
  3. 业务上传会话与 TOKEN 签发分开:既有 SaaS 业务会话/资产登记语义仍经 MQ,响应回原 D 专用 Topic;但该响应不再承担 OSS 配置或 TOKEN 的来源。upload_id、对象引用及会话/资产关联按新版合同冻结,不因 TOKEN 过期另造资产,也不新加一套未批准的登记协议。
  4. Agent 直接 PUT 文件到 OSS,经 R13 只提交原资产/会话、对象引用、大小和 SHA-256 等完成元信息。D 经 MQ 提交 complete,SaaS 仍独立验证对象后经 MQ 返回 verified、oss_id 或明确失败;D 持久校验 verified 后同事务写资产状态和 recording.ready outbox。
  5. TOKEN 过期/失效由 Agent 显式向 D 重新申请,D 仍按自身配置提供,不向 SaaS 申请 TOKEN,不自动续期/重试。MQ 响应丢失/重复沿原资产、请求和 upload_id 恢复;未获 SaaS verified 保留待完成状态和文件,不提前 ready。

复用的业务数据包括 recording_id、call_id、content_type、size_bytes、SHA-256、声道/采样率/时长。D→A 的临时授权含原会话、TOKEN/上传目标、方法、必要 headers、约束和有效期;D↔SaaS 的 MQ 只承担业务元信息/会话及最终验证,不传 D 的配置文件、长期凭据或临时 TOKEN。SaaS 为独立校验取得必要对象定位及读取能力的既有业务要求仍须满足,精确合同在 W01 冻结,不能假定“D 持有配置”即代表 SaaS 已能验证。

业务会话/complete 的 MQ 异步结果与 R12/R13 Unary 的衔接须由 W02/W11 冻结 pending、超时、原操作重取及最终结果;TOKEN 本身来自 D,不等待 SaaS 下发配置/TOKEN。不能无限阻塞 RPC,也不能收到 broker confirm 就返回已完成。旧 saas.openapi.yaml 仅作语义对照,不是新 MQ Schema;本轮不修改 Proto/配置格式或新增字段。

当前 D 用 internal/oss client 签发 grant 的职责与新确认方向一致,不应再把 D 签发能力列为待删除或“仅故障回退”;但配置文件读取、TOKEN 约束及完整接线仍须核验。当前 D 本地验证对象即发 ready 的旧行为仍不能替代 SaaS MQ verified。旧 SaaS HTTP upload-session/complete 方案继续废弃,不开发 HTTP client。

6.2 AI 不可变配置与授权

D 根据 MQ 任务中的原 tenant_id/tenant_key + agent_version_id,通过 MQ 向 SaaS 获取不可变配置及有效授权,SaaS 通过原 D 专用 Topic 返回;授权/撤销等交互同样不能走 HTTP。

保留既有版本、immutable、content_sha256、config 及租户授权语义;源 Schema、不可变摘要、有效期、撤销、能力和供应商受控引用均校验后持久绑定到原执行,再交付 Agent。缓存按租户/版本隔离,在途/原排队任务不漂移,同版本异内容拒绝,0/false 与未提供保真;无有效授权时拒绝新准入,不用 latest、CLI/env 或 SDK 默认值兜底。

旧 ai-config.openapi.yaml 的 AI GET 已废弃为 D↔SaaS 接入方式,不再开发该 HTTP client。当前 AISnapshotRaw/AIAuthorizationRaw 启动注入和 Agent 本地校验只证明旧路径;MQ 配置/授权、关联与持久恢复仍待 W01/W07 实现验证。消息细节不能由旧 OpenAPI 自动推定。

7. 待完成门禁与禁止误读

门禁 完成证据 当前状态
W01 身份/Topic/消息冻结 新版本/来源/哈希、完整消息 Schema、路由、关联/错误/期限及正反例 待冻结
W05/W12 MQ 控制面 全部交互持久接收/响应、移除旧 HTTP、补传语义正确 待实现/验证
W07 MQ AI 不可变配置/授权、迟到/撤销/重复与缓存隔离 待实现/验证
W02/W11 上传授权与业务协调 D 配置文件→临时 TOKEN→A 直传;R12/R13 与 MQ 业务结果有界衔接、SaaS verified 后才 ready 待核验/实现/验证
W13/W14 本地联合回归 D1/D2 Topic 隔离 fixture、身份冲突、broker 故障、全流程无 SaaS↔D HTTP 待验证

路由 fixture 只验证不同 Dispatcher 互不抢收,不把多 Dispatcher 调度或真实 SaaS 联调引入本轮。旧包、旧 HTTP handler、单租户 broker 测试和本地 OSS 成功,均不代表以上门禁通过。

8. 依据与相关文档

  • 计划与需求阅读索引:§1.2、§8.2 的 MQ-only 修订与状态。
  • 时间泳道图:目标流程,不冒充当前实现。
  • Dispatcher ↔ Agent 契约:现有 Proto/handler 事实与上传协调待改项。
  • 旧实现事实:internal/contract/contract.go、internal/mq/amqp.go、internal/tenant/routing.go、internal/dispatcher/consumer.go、internal/dispatcher/dispatcher.go、internal/store/store.go、internal/store/facts.go、internal/control/http.go。
  • 旧固定包:contracts/upstream/2026-09-19-p1-v1/ 下 mq.schema.json、event-payloads.schema.json、mq-topology.md、executor.openapi.yaml、saas.openapi.yaml、ai-config.openapi.yaml;保留原样,不代表 MQ-only 新契约已发布。