docs: establish MQ-only and Dispatcher OSS token baseline
This commit is contained in:
@@ -14,6 +14,8 @@
|
||||
|
||||
精确字段号和枚举以 `proto/agent/v1/agent.proto` 为唯一源;本文不另造 protobuf。
|
||||
|
||||
**SaaS↔Dispatcher 边界已修订为 MQ-only**,详见 [SaaS↔Dispatcher 契约](./saas-dispatcher.md)。D 有全局唯一身份和独立接收 Topic;这不改变内部 Unary 或 Agent→OSS 直传。**OSS 配置由 D 配置文件维护,Agent 向 D 领取临时上传 TOKEN,SaaS 不再提供 OSS 配置/TOKEN。** D 的签发职责保留;本文“当前实现”仍不能证明配置文件/TOKEN 全部约束已接通,尤其 D 本地对象验证不能代替 SaaS MQ verified。本轮不改变 SaaS 最终校验归属,未改 Proto/代码。
|
||||
|
||||
## 2. Service 方法与方向
|
||||
|
||||
| RPC | 方向 | 当前代码状态 | 接收端 |
|
||||
@@ -39,7 +41,7 @@
|
||||
1. Dispatcher 从受控 Agent endpoint 文件读取 `agent_id`、`cell_id`、地址和 `server_name`。
|
||||
2. `rpc.DialFromFiles` 使用 CA、客户端证书、私钥和 server name 建立 TLS gRPC 连接。
|
||||
3. Dispatcher 对每个 endpoint 先 `GetAgentStatus`,再以返回的 `boot_id` 调 `ActivateAgent`。
|
||||
4. Dispatcher 生成本次 `dispatcher_epoch`;Agent 用 `session_generation` 持久化 fencing 水位。
|
||||
4. Dispatcher 生成本次 `dispatcher_epoch`;Agent 用 `session_generation` 持久化 fencing 水位。`dispatcher_epoch` 是运行代次,不是全局唯一的 Dispatcher 逻辑 ID;后者的 MQ 关联及与内部会话的绑定待新契约冻结,当前 Proto 未因此自动增加字段。
|
||||
5. Agent 新会话会 fence 旧的 `agent_id + cell_id + boot_id + epoch + generation` 组合;旧请求返回 `ABORTED`,不会自动释放未知执行。
|
||||
|
||||
### 3.2 mTLS 与身份
|
||||
@@ -172,7 +174,7 @@ Dispatcher 从已校验的 `call.execute` 构造 binding;Agent 回报不能改
|
||||
|
||||
请求字段:`meta`、`target`、`state`(`OPEN/CLOSED/DRAINING/QUARANTINED`)、`barrier_id`、`expected_admission_generation`、`reason`。
|
||||
|
||||
响应:`OperationReceipt` + `applied_admission_generation`。Agent 侧按目标 Agent 做 generation CAS;代次不匹配返回 `CONFLICT`。当前代码没有 `AgentCoordinator` 的调用封装,也没有把此状态操作接到 SaaS HTTP 控制入口。
|
||||
响应:`OperationReceipt` + `applied_admission_generation`。Agent 侧按目标 Agent 做 generation CAS;代次不匹配返回 `CONFLICT`。当前代码没有 `AgentCoordinator` 的调用封装。该方法属于 D→A 内部准入职责,不把它直接等同 SaaS 任务控制;SaaS 业务控制只能经 MQ 进入 D,旧 HTTP 控制入口应移除。
|
||||
|
||||
### 5.5 `GetExecutionPermit`
|
||||
|
||||
@@ -274,16 +276,20 @@ Dispatcher 侧额外要求:
|
||||
|
||||
当前 Dispatcher 验证:Agent/Cell/operation/idempotency 元数据、完整 execution/tenant binding、合法 `tenant_key`、asset ID、upload ID、正数文件大小和 SHA-256;超过 OSS 最大文件大小返回 `RESOURCE_EXHAUSTED`。
|
||||
|
||||
成功响应:`OperationReceipt`、`UploadGrant`、`state`。当前生产 Dispatcher handler:
|
||||
成功响应:`OperationReceipt`、`UploadGrant`、`state`。目标是 D 读取自身 OSS 配置文件并向 A 提供临时 TOKEN/受限上传信息;以下记录当前 handler,不代表配置入口、TOKEN 形态和新版业务会话约束已全部验收:
|
||||
|
||||
- 以 `tenant_key + "\\0" + execution_id + "\\0" + asset_id` 的 SHA-256 hex 生成 object key,并加配置的 key prefix;
|
||||
- 将 binding、asset、grant、object key、state 持久到 Dispatcher SQLite;
|
||||
- grant 的过期时间由 OSS client 配置提供;
|
||||
- 同 upload ID 同 binding/asset 返回原 grant;绑定不同返回冲突;
|
||||
- 过期 grant 只有在显式再次 `RequestUpload` 时才替换,不自动续期。
|
||||
- 过期 grant 只有在显式再次 `RequestUpload` 时才替换,不自动续期。目标流程同样由 Agent 显式向 D 重新领取 TOKEN;D 使用自身配置,不向 SaaS 申请 TOKEN,不改变原资产/会话。
|
||||
|
||||
新请求当前返回 `UPLOAD_STATE_REQUESTED`;持久层状态为 `granted`。`UPLOADING` 枚举存在,但当前 Dispatcher handler 不把 Agent 的 PUT 过程映射为该状态。
|
||||
|
||||
目标流程为 **D 依据自身配置文件向 A 提供临时上传 TOKEN,不向 SaaS 申请 OSS 配置/TOKEN**。D 侧配置缺失/无效时明确失败,不切换配置源;长期凭据不交给 A,不写入示例、日志或证据。TOKEN 的精确形态、SDK 能力及与 `UploadGrant` 的映射须核验冻结,不能只把现有字段改称 TOKEN 就宣称完成。
|
||||
|
||||
既有 SaaS 业务会话/资产登记仍经 MQ,D 关联原租户/执行/资产后才交付相应上传信息;这不是由 SaaS 签 TOKEN。业务响应可能晚于 RPC deadline,W02/W11 仍须冻结 pending、有界等待、原操作重取及最终结果,不无限阻塞 Unary、不因超时另造 upload ID。当前 D 的本地签发能力保留复用,禁止删除后改为等待 SaaS 下发配置。本段不新增 RPC/Proto 字段。
|
||||
|
||||
### 6.3 Agent → OSS 直接上传
|
||||
|
||||
Agent 获得 grant 后使用 `internal/agent.UploadClient.UploadFile`:
|
||||
@@ -312,6 +318,8 @@ Dispatcher 当前执行:
|
||||
|
||||
响应为 `OperationReceipt`、`state=COMPLETED`、`oss_id`。同 upload ID 同 OSS ID 的重复 complete 返回 `ACCEPTED`;未知 upload 返回 `NOT_FOUND`;对象校验失败返回 `FAILED_PRECONDITION` 且标记可重试。
|
||||
|
||||
以上是旧本地验证事实。**目标流程必须由 D 经 MQ 提交 complete,SaaS 独立验证对象后经本 D 专用 Topic 返回 verified/oss_id;D 校验原请求、租户、资产和会话并持久化后,才可记完成及写 recording.ready outbox。** D 本地 HEAD、PUT 2xx 或 broker confirm 不能替代 SaaS verified。等待中/超时/重复响应及 A 获取最终结果的 Unary 衔接由 W02/W11 冻结,尚未完成;不凭此说明宣称当前 handler 已符合目标。
|
||||
|
||||
## 7. 错误、幂等与未知结果
|
||||
|
||||
### 7.1 gRPC FailureCode
|
||||
@@ -339,8 +347,8 @@ Agent 侧写操作的内存 operation key 为:
|
||||
|
||||
1. `GetBootstrap`、`SetAdmissionState` 虽有 handler,但当前 Dispatcher 启动流程没有调用完整 bootstrap/admission 编排。
|
||||
2. `Execute` 的当前 RPC 实现只证明准备/幂等/配置校验,不证明 ARI/RTP/SIP 已由该 RPC 直接完成。
|
||||
3. Dispatcher → SaaS 的 `recording-uploads` HTTP handshake 未在当前 Go client 中接入;当前完成路径是 Dispatcher 直接验证 OSS。
|
||||
4. Dispatcher → SaaS 的 AI version GET 未接入;当前 AI snapshot/authorization 由启动输入提供并在 Agent 侧校验。
|
||||
3. D 配置文件→临时 TOKEN→A 直传的完整接线/约束,以及 SaaS 业务会话/complete/verified 的 MQ 协调与 R12/R13 有界衔接仍待核验;D 已有签发能力保留复用,但本地对象验证不能替代 SaaS verified。旧 `recording-uploads` HTTP 方案继续废弃,不开发 client。
|
||||
4. SaaS AI 配置/授权的 MQ 请求响应与持久绑定尚未接通;当前快照/授权由启动输入提供。旧 AI version GET 已废弃,不能作为后续实现方向。
|
||||
5. 不能把 generated service 中的全量方法数当作每个 listener 都可调用;实际 listener 能力以第 2 节和 `DispatcherServer` 代码为准。
|
||||
|
||||
## 9. 依据文件
|
||||
|
||||
+131
-222
@@ -1,64 +1,90 @@
|
||||
# SaaS ↔ Dispatcher 对接契约(实现事实)
|
||||
# SaaS ↔ Dispatcher 对接契约(MQ-only 设计与实现差异)
|
||||
|
||||
## 1. 适用范围与事实等级
|
||||
|
||||
本文只描述当前 Go 实现能够证明的字段、方向、接口和状态;不把上游 OpenAPI 中尚未接入的 HTTP 客户端写成“已实现”。
|
||||
**用户已确认:RabbitMQ 是 SaaS 与 Dispatcher 的唯一交互通道,双方之间禁止任何 HTTP 请求或回调。每个 Dispatcher 都有独立、全局唯一的 ID,并通过各自独立的专用 Topic 接收事件。** 本规则覆盖执行、控制、查询、补传、AI 配置与授权、录音上传会话及完成验证,不保留 HTTP 特例或回退。
|
||||
|
||||
| 内容 | 当前状态 | 事实来源 |
|
||||
**OSS 补充确认:OSS 相关配置存于 Dispatcher 配置文件;Agent 经 Unary 向 Dispatcher 领取临时上传 TOKEN 后直传 OSS,不持有长期凭据。SaaS 不再下发 OSS 配置或上传 TOKEN。此次只调整配置/TOKEN 来源,上传完成后的 SaaS 独立校验和 MQ verified 职责保持不变。**
|
||||
|
||||
本文区分三种事实:
|
||||
|
||||
| 层级 | 本次状态 | 使用边界 |
|
||||
| --- | --- | --- |
|
||||
| SaaS → Dispatcher:`call.execute` RabbitMQ 命令 | 已实现 | `internal/dispatcher/consumer.go`、`internal/store/store.go` |
|
||||
| Dispatcher → SaaS:RabbitMQ 业务事件及 outbox | 已实现 | `internal/contract/contract.go`、`internal/store/facts.go`、`internal/dispatcher/dispatcher.go` |
|
||||
| SaaS → Dispatcher:控制、命令查询、命令补传 HTTP | 已实现,但只有当前代码列出的行为 | `internal/control/http.go` |
|
||||
| SaaS → Dispatcher:通话查询、通话补传 HTTP | 路由存在,当前固定返回 `404` | `internal/control/http.go` |
|
||||
| Dispatcher → SaaS:录音 upload-session/complete HTTP | 上游契约已定义,当前代码没有 SaaS HTTP client | `contracts/upstream/2026-09-19-p1-v1/saas.openapi.yaml` |
|
||||
| Dispatcher → SaaS:AI version GET | 上游契约已定义,当前代码没有 SaaS HTTP client;当前 Agent 启动时读取本地快照 | `contracts/upstream/2026-09-19-p1-v1/ai-config.openapi.yaml`、`internal/ai/*.go` |
|
||||
| 已确认设计 | MQ-only、Dispatcher 唯一身份、独立 Topic | 后续设计与实现必须遵守 |
|
||||
| 待冻结的消息契约 | 身份分配/持久化、Topic 命名、消息类型、关联字段、错误与超时规则 | 见 §2、§5、§6;不能据中文语义自行拼 JSON 或给旧 Schema 加字段 |
|
||||
| 现有实现/旧包 | 下列旧字段、路由及代码事实 | 仅用于识别差异,不代表新设计已实现或通过验收 |
|
||||
|
||||
契约包固定来源为 `contracts.SourceCommit = 2026-09-19-p1-v1`。MQ 与事件正文必须以该目录中的 JSON Schema 为准,不维护第二套手写 Schema。
|
||||
当前固定包为 `contracts/upstream/2026-09-19-p1-v1/`(`contracts.SourceCommit = 2026-09-19-p1-v1`)。其中的 HTTP OpenAPI 与仅按租户路由的 MQ 拓扑**不再是目标方案**。旧包及其哈希保持不变;W01 须发布新版本、严格 Schema、拓扑及正反例,不能手改旧包、生成字段索引或通过放宽 `additionalProperties` 绕过冻结。
|
||||
|
||||
## 2. 通信拓扑与方向
|
||||
本轮只纠正文档,不修改代码、Proto 或 Schema,也不宣称新 MQ 闭环已通过。现有实现事实沿用此前核验记录,受影响部分必须按新基线重新验证。
|
||||
|
||||
| 方向 | 接口 | 传输 | 交付语义 |
|
||||
| --- | --- | --- | --- |
|
||||
| SaaS → Dispatcher | `call.execute` | RabbitMQ command exchange | Dispatcher 完成 Schema、租户路由、截止时间和 SQLite 持久化后才 ACK |
|
||||
| Dispatcher → SaaS | `command.result` 等事件 | RabbitMQ event exchange + SQLite outbox | publisher confirm 只代表 broker 收到;不代表 SaaS 业务事务已应用 |
|
||||
| SaaS → Dispatcher | 控制/查询/补传 | 内部 HTTP | 控制和补传的 `202` 只代表已接受/持久化,不代表 Agent 已执行 |
|
||||
| Dispatcher → SaaS | 录音会话申请/完成 | 上游定义的内部 HTTP | 当前项目尚未接入;不能用本地 OSS 结果代替 SaaS `verified` |
|
||||
| Dispatcher → SaaS | `GET /internal/v1/ai/agent-versions/{agent_version_id}` | 上游定义的内部 HTTP | 当前项目尚未接入;不能读取 `latest` 或用默认配置替代 |
|
||||
## 2. 通信拓扑、Dispatcher 身份与交付语义
|
||||
|
||||
`tenant_key` 原值复制到消息体、队列、binding 和 routing key;必须是有效 UTF-8,最大 `224` 字节,不清洗、编码或截断。
|
||||
### 2.1 唯一通道与边界
|
||||
|
||||
## 3. RabbitMQ 命令:SaaS → Dispatcher
|
||||
| 交互 | 唯一允许的路径 | 语义 |
|
||||
| --- | --- | --- |
|
||||
| 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 |
|
||||
|
||||
### 3.1 拓扑
|
||||
### 2.2 全局唯一身份与独立 Topic
|
||||
|
||||
| 元素 | 值 |
|
||||
| --- | --- |
|
||||
| command exchange | `agent-call.commands.v1`,durable `direct` |
|
||||
| tenant queue | `agent-call.executor.{tenant_key}.v1`,durable、单一精确 binding |
|
||||
| routing key | `agent-call.tenant.{tenant_key}.call.execute` |
|
||||
| event exchange | `agent-call.events.v1`,durable `topic` |
|
||||
| Dispatcher dead-letter exchange | `agent-call.dead-letter.v1`,durable `topic` |
|
||||
| 默认 prefetch | `1` |
|
||||
- `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 名长度及分隔符、通配符边界,超限拒绝发布并保留源任务,不改变既有租户标识。
|
||||
|
||||
Dispatcher 为每个消费租户声明 command queue 和 `.dlq.v1` 队列。SaaS 结果队列由 SaaS 管理,拓扑基线为 `agent-call.saas.events.v1`,binding `agent-call.#`。
|
||||
P1 仍只运行一个单活 Dispatcher。现在必须在合同及本地路由测试中区分两个 Dispatcher 身份;这不授权多节点上线、多 Dispatcher 共享配额、自动选主、HA 或自动迁移任务。未知执行不得因目标离线而改投另一个 Dispatcher。
|
||||
|
||||
### 3.2 `call.execute` 外壳
|
||||
### 2.3 请求、响应、持久化和恢复
|
||||
|
||||
消息为 JSON,`additionalProperties: false`:
|
||||
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` |
|
||||
| `schema_version` | string | 旧版固定 `1.0` |
|
||||
| `command_type` | string | 固定 `call.execute` |
|
||||
| `command_id` | string | 1–128 字节;不可含空白、`/`、`\\` |
|
||||
| `tenant_id` | string | 同上 |
|
||||
| `tenant_key` | string | 非空;有效 UTF-8;实现额外限制 224 字节 |
|
||||
| `tenant_key` | string | 非空有效 UTF-8;旧实现额外限制 224 字节 |
|
||||
| `trace_id` | string | 同 `id` 约束 |
|
||||
| `issued_at` | RFC3339 时间 | 必填 |
|
||||
| `not_after` | RFC3339 时间 | 必填;Dispatcher 接收时已到期则拒绝 |
|
||||
| `payload` | object | 必须符合 `executePayload` |
|
||||
| `not_after` | RFC3339 时间 | 必填;不能因重投延期 |
|
||||
| `payload` | object | 符合 `executePayload` |
|
||||
|
||||
### 3.3 `payload` 数据结构
|
||||
### 3.3 旧 `payload` 数据结构
|
||||
|
||||
| 字段 | 类型 | 约束 |
|
||||
| --- | --- | --- |
|
||||
@@ -66,26 +92,21 @@ Dispatcher 为每个消费租户声明 command queue 和 `.dlq.v1` 队列。SaaS
|
||||
| `task_id` | string | 必填 ID |
|
||||
| `task_item_id` | string | 必填 ID |
|
||||
| `task_revision` | integer | `>= 1` |
|
||||
| `callee` | string | 1–256 字符;保留业务原始被叫号码 |
|
||||
| `callee` | string | 1–256 字符;保留原始被叫号码 |
|
||||
| `route_policy_id` | string | 必填 ID |
|
||||
| `caller_profile_id` | string | 必填 ID |
|
||||
| `agent_version_id` | string | 必填 ID |
|
||||
| `variables` | object | 必填;当前 Schema 允许任意附加属性,业务白名单仍由上游约束 |
|
||||
| `variables` | object | 必填;旧 Schema 允许附加属性,业务白名单仍受源约束 |
|
||||
| `ring_timeout_ms` | integer | `>= 1` |
|
||||
| `max_call_duration_ms` | integer | `>= 1` |
|
||||
|
||||
Go 实现对应 `internal/contract.ExecutePayload`;`payload` 以 `json.RawMessage` 保留原始 JSON,Dispatcher 不把变量转成另一套协议。
|
||||
对应 `internal/contract.ExecutePayload`;`payload` 以 `json.RawMessage` 保留原始 JSON。改传输不授权改变业务号码、版本、摘要或新增 MQ mode 字段。
|
||||
|
||||
### 3.4 接收、幂等与 ACK
|
||||
### 3.4 旧接收事实与新验收要求
|
||||
|
||||
1. `ConsumeTenant` 先校验 `tenant_key`,声明租户队列,再以 `prefetch=1` 消费。
|
||||
2. `AcceptCommand` 调用 `Store.IngestCommand`:校验源 Schema、routing key、`not_after`,计算原始 body 的 SHA-256。
|
||||
3. 以 `command_id` 查 inbox:同 ID 同 body 为重复;同 ID 不同 body 为冲突并拒绝。
|
||||
4. 新命令在一个 SQLite 事务内写入 inbox、task 和 `command.result(accepted)` outbox。
|
||||
5. handler 成功后才 ACK。Schema、JSON、租户路由等永久错误 `Reject(false)`,经死信队列处理;其它错误 `Nack(requeue=true)`。
|
||||
6. `command_id`、`execution_id`、`task_id` 等业务标识不因重投而更换;`not_after` 不因重投延期。
|
||||
旧 `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)`。
|
||||
|
||||
当前 `command.result` 初始 payload 为:
|
||||
旧初始结果 payload:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -98,11 +119,13 @@ Go 实现对应 `internal/contract.ExecutePayload`;`payload` 以 `json.RawMess
|
||||
}
|
||||
```
|
||||
|
||||
## 4. RabbitMQ 事件:Dispatcher → SaaS
|
||||
新验收须补充 §2 的 Dispatcher 定向/来源校验、所有 MQ 交互的关联和持久恢复,以及租户复合幂等要求。不能把当前仅按 `command_id` 查重的事实写成这些要求已满足。
|
||||
|
||||
### 4.1 通用外壳
|
||||
## 4. RabbitMQ 业务事件:旧字段语义与新路由要求
|
||||
|
||||
事件 routing key 固定为 `agent-call.{event_type}`。外壳字段全部必填:
|
||||
### 4.1 旧通用外壳
|
||||
|
||||
旧 routing key 为 `agent-call.{event_type}`;新来源 Dispatcher 的表达待 W01 冻结。以下字段在旧版全部必填:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -120,30 +143,27 @@ Go 实现对应 `internal/contract.ExecutePayload`;`payload` 以 `json.RawMess
|
||||
}
|
||||
```
|
||||
|
||||
`event_type` 的源 Schema 枚举为:
|
||||
源枚举为 `command.result`、`call.status`、`transcript.updated`、`call.finished`、`recording.ready`、`recording.failed`、`transcript.failed`、`contact.opt_out`。它们不自动覆盖新增的配置/查询/上传响应消息。
|
||||
|
||||
`command.result`、`call.status`、`transcript.updated`、`call.finished`、`recording.ready`、`recording.failed`、`transcript.failed`、`contact.opt_out`。
|
||||
|
||||
事件 payload 必须同时通过 `mq.schema.json` 和 `event-payloads.schema.json`。`EventBuilder` 不接受未知字段;`aggregate_version` 由 Dispatcher SQLite 按 `aggregate_type + aggregate_id` 递增,Agent 不能指定它。
|
||||
事件同时通过 `mq.schema.json` 和 `event-payloads.schema.json`;`EventBuilder` 拒绝未知字段。旧实现由 SQLite 按 `aggregate_type + aggregate_id` 递增版本,Agent 不能指定版本;新基线仍须验证租户/Dispatcher 归属,不将旧实现等同完整隔离。
|
||||
|
||||
### 4.2 当前代码实际生成的事件
|
||||
|
||||
| 事件 | 事实来源 | 当前代码行为 |
|
||||
| --- | --- | --- |
|
||||
| `command.result` | 命令持久受理、执行接受事实 | 命令接收路径直接生成;Agent `EXECUTION_ACCEPTED` fact 也映射到该类型 |
|
||||
| `call.status` | Agent `CALL_STATUS` fact | Dispatcher 校验 fact 后生成 |
|
||||
| `call.finished` | Agent `CALL_FINISHED` fact | Dispatcher 校验 fact 后生成 |
|
||||
| `transcript.updated` | Agent `TRANSCRIPT_UPDATED` fact | 保留实时文字事件名,不使用 `call.transcript` |
|
||||
| `transcript.failed` | Agent `TRANSCRIPT_FAILED` fact | 当前代码尝试映射,但使用 `aggregate_type=transcript`;`mq.schema.json` 只允许 `transcript_segment`,因此该路径当前无法通过双 Schema 校验 |
|
||||
| `contact.opt_out` | Agent `CONTACT_OPT_OUT` fact | Dispatcher 校验 fact 后生成 |
|
||||
| `recording.ready` | 已验证 OSS 上传完成 | `CompleteUpload` 将完成状态和 outbox 放在同一 SQLite 事务 |
|
||||
| `recording.failed` | 上游 Schema | 当前 Go 代码没有对应 `FactKind` 或事件生成路径 |
|
||||
| 事件 | 当前代码行为 |
|
||||
| --- | --- |
|
||||
| `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 只写入 Dispatcher 事实表,不生成 SaaS MQ 事件。`TRANSCRIPT_FAILED` 虽在两个事件 payload Schema 中声明,但当前 `internal/rpc/dispatcher_events.go` 将其聚合类型写成 `transcript`,随后被 `mq.schema.json` 拒绝;不能把该路径记为已交付。
|
||||
`RECORDING_PROGRESS` 只保存 fact,不发布 MQ 事件。上述已知实现差异不因本次文档改写而消失。
|
||||
|
||||
### 4.3 已冻结的专属 payload 关键字段
|
||||
### 4.3 旧专属 payload 关键字段
|
||||
|
||||
完整约束在 `event-payloads.schema.json`;下面列出对接方必须使用的字段,不是新的 Schema:
|
||||
完整约束仍在固定包 `event-payloads.schema.json`;下表不是第二套 Schema。
|
||||
|
||||
| 事件 | 必填字段 |
|
||||
| --- | --- |
|
||||
@@ -156,189 +176,78 @@ Go 实现对应 `internal/contract.ExecutePayload`;`payload` 以 `json.RawMess
|
||||
| `transcript.failed` | `call_id`, `reason_code`, `retryable` |
|
||||
| `contact.opt_out` | `call_id`, `task_id`, `task_item_id`, `requested_at` |
|
||||
|
||||
`recording.ready` 只能表示对象已验证;不能用 PUT 成功、ETag 或本地路径替代 `oss_id` 和 SHA-256。
|
||||
新设计中 `recording.ready` 只能在 D 收到并持久校验 SaaS 的 MQ verified 结果及 `oss_id` 后发布;PUT 2xx、ETag、本地路径或 D 单独 HEAD 成功都不替代该结果。
|
||||
|
||||
### 4.4 Outbox 交付
|
||||
|
||||
`Store.RecordExecutionFact`、命令接收和上传完成均可在状态事务中写 outbox。`Dispatcher.FlushOutbox`:
|
||||
旧 `Dispatcher.FlushOutbox` claim `pending/retry` 为 `dispatching`,发布 persistent JSON 并等 confirm;成功记 `published`,失败记 `retry`,重启把 `dispatching` 恢复为 `retry`。重复 fact 同 `fact_id + content_sha256` 不生成第二条事件,异摘要冲突。SaaS 按租户/事件身份幂等应用。
|
||||
|
||||
- claim `pending/retry` 记录并标记 `dispatching`;
|
||||
- 通过 RabbitMQ persistent JSON message 发布并等待 publisher confirm;
|
||||
- 成功标记 `published`;失败标记 `retry`;
|
||||
- 重启时将 `dispatching` 恢复为 `retry`。
|
||||
新设计将这一持久交付原则覆盖请求与响应,补齐不可路由、来源/目标、相关状态恢复验证。业务事件重投保留原身份、内容和域版本,broker confirm 不替代 SaaS 应用收讫。
|
||||
|
||||
重复 fact 使用相同 `fact_id + content_sha256` 时不创建第二条事件;同 fact ID 不同摘要返回冲突。SaaS 必须以 `event_id` 做 inbox 幂等,broker confirm 不能当作 SaaS 应用收讫。
|
||||
## 5. SaaS → Dispatcher:控制、查询和补传全部经 MQ
|
||||
|
||||
## 5. Dispatcher 提供的 HTTP 接口:SaaS → Dispatcher
|
||||
### 5.1 待冻结内容
|
||||
|
||||
### 5.1 公共请求头
|
||||
|
||||
当前 `internal/control.Handler` 要求:
|
||||
|
||||
- `Authorization: Bearer <token>`;配置了 token 时必须精确匹配;
|
||||
- `X-Tenant-ID`;
|
||||
- `X-Request-ID`。
|
||||
|
||||
当前 Handler 未强制检查权限 scope。源 `executor.openapi.yaml` 另外要求 `Idempotency-Key`;控制接口当前代码只把该值传入存储层,没有把缺失值直接拒绝,这属于实现与源 OpenAPI 的已知差异。
|
||||
|
||||
错误响应为 JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "about:blank",
|
||||
"title": "<HTTP status>",
|
||||
"status": 400,
|
||||
"code": "<code>",
|
||||
"detail": "<detail>",
|
||||
"request_id": "<X-Request-ID>",
|
||||
"retryable": false
|
||||
}
|
||||
```
|
||||
以下只定义已确认业务语义,**消息类型名、信封字段、响应/错误枚举尚未发布**。旧 HTTP header、URL 和状态码不能直接作为 MQ 合同,也不能塞进旧 `call.execute` 或宽松 metadata 中。
|
||||
|
||||
### 5.2 控制任务
|
||||
|
||||
**方向:SaaS → Dispatcher**
|
||||
SaaS 将 pause/resume/stop 控制发到目标 Dispatcher 专用 Topic。保留原 `command_id`、租户/任务归属、`expected_task_revision` CAS、`active_call_policy=drain|hangup` 与原因语义。D 持久受理后经 MQ 回报 accepted;经 Agent 屏障/挂断事实确认后才能回报 applied。重复控制不重复增加 revision,冲突不能伪装成功,stopped 不可恢复。
|
||||
|
||||
`POST /internal/v1/outbound/tasks/{task_id}/controls`
|
||||
### 5.3 查询命令与通话
|
||||
|
||||
请求体:
|
||||
请求和响应都经 MQ;查询固定原 `command_id` 或 `call_id`,返回可证明的命令、执行、控制、通话及独立资产状态。响应关联原查询和目标 Dispatcher;不存在、保留过期与暂时不可用分开表达,精确错误码待冻结。超时不走 HTTP 补查,也不证明原执行未发生。
|
||||
|
||||
| 字段 | 类型/约束 |
|
||||
| --- | --- |
|
||||
| `command_id` | 必填 ID;当前代码用于响应,不单独生成控制 outbox |
|
||||
| `action` | `pause`、`resume`、`stop` |
|
||||
| `expected_task_revision` | integer,`>=1`;CAS 版本 |
|
||||
| `active_call_policy` | 可选:`drain` 或 `hangup` |
|
||||
| `reason` | 非空,最大 512 字符 |
|
||||
### 5.4 整体补传
|
||||
|
||||
当前成功响应为 `202`:
|
||||
仅允许以 `call_id` 或 `source_command_id` 请求整体业务结果补传,不增加 task/execution 范围或局部筛选。固定受理截止点,重发原事件 ID/内容/版本,实时优先、分批有界;补传自身结果不能递归进入集合。
|
||||
|
||||
```json
|
||||
{
|
||||
"command_id": "<id>",
|
||||
"tenant_id": "<tenant>",
|
||||
"tenant_key": "<tenant_key>",
|
||||
"task_id": "<task_id>",
|
||||
"status": "accepted",
|
||||
"requested_task_revision": 1,
|
||||
"accepted_at": "2026-09-19T00:00:00Z"
|
||||
}
|
||||
```
|
||||
补传不是把 `call.execute` 重新发布来重新执行。旧 HTTP source-command replay 当前重发原命令的行为不满足目标整体结果补传,须列入 W12 修正;不能因改为 MQ 就保留这一错误语义。
|
||||
|
||||
实现先按 `X-Tenant-ID + task_id` 查询任务,再以任务的 `execution_id` 调用 `ApplyControlDetailed`。版本冲突为 `409`;任务不存在为 `404`;输入错误为 `400`。`202` 不表示 Agent 已应用控制。
|
||||
### 5.5 已废弃 HTTP 入口的处理
|
||||
|
||||
### 5.3 查询命令
|
||||
`internal/control/http.go` 当前存在控制、命令查询/补传 handler,通话查询/补传路由固定返回 `404`。这些只是旧实现事实,**不再是可接入或待扩展的 SaaS 接口**。W05/W12 应移除这些 SaaS HTTP 业务入口及相关部署说明,不保留兼容层、并行双通道或 HTTP 兜底。本轮未改代码,不能宣称入口已移除。
|
||||
|
||||
**方向:SaaS → Dispatcher**
|
||||
## 6. Dispatcher → SaaS:AI 配置与上传业务协调经 MQ
|
||||
|
||||
`GET /internal/v1/outbound/commands/{command_id}`
|
||||
### 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。
|
||||
|
||||
```json
|
||||
{
|
||||
"command_id": "<id>",
|
||||
"command_type": "call.execute",
|
||||
"tenant_id": "<tenant>",
|
||||
"tenant_key": "<tenant_key>",
|
||||
"task_id": "<task_id>",
|
||||
"execution_id": "<execution_id>",
|
||||
"call_id": null,
|
||||
"status": "persisted",
|
||||
"reason_code": null,
|
||||
"wait_reason_code": null,
|
||||
"accepted_at": "<inbox.persisted_at>",
|
||||
"waiting_since": null,
|
||||
"admission_deadline": "<envelope.not_after>",
|
||||
"requested_task_revision": 1,
|
||||
"applied_task_revision": null,
|
||||
"task_state": null,
|
||||
"aggregate_version": 1,
|
||||
"updated_at": "<inbox.received_at>"
|
||||
}
|
||||
```
|
||||
复用的业务数据包括 `recording_id`、`call_id`、`content_type`、`size_bytes`、SHA-256、声道/采样率/时长。D→A 的临时授权含原会话、TOKEN/上传目标、方法、必要 headers、约束和有效期;D↔SaaS 的 MQ 只承担业务元信息/会话及最终验证,不传 D 的配置文件、长期凭据或临时 TOKEN。SaaS 为独立校验取得必要对象定位及读取能力的既有业务要求仍须满足,精确合同在 W01 冻结,不能假定“D 持有配置”即代表 SaaS 已能验证。
|
||||
|
||||
查询按租户隔离;不存在返回 `404`。当前实现不返回 call snapshot、控制应用版本或完整状态机。
|
||||
业务会话/complete 的 MQ 异步结果与 R12/R13 Unary 的衔接须由 W02/W11 冻结 pending、超时、原操作重取及最终结果;TOKEN 本身来自 D,不等待 SaaS 下发配置/TOKEN。不能无限阻塞 RPC,也不能收到 broker confirm 就返回已完成。旧 `saas.openapi.yaml` 仅作语义对照,不是新 MQ Schema;本轮不修改 Proto/配置格式或新增字段。
|
||||
|
||||
### 5.4 按 source command 补传
|
||||
当前 D 用 `internal/oss` client 签发 grant 的职责与新确认方向一致,**不应再把 D 签发能力列为待删除或“仅故障回退”**;但配置文件读取、TOKEN 约束及完整接线仍须核验。当前 D 本地验证对象即发 ready 的旧行为仍不能替代 SaaS MQ verified。旧 SaaS HTTP upload-session/complete 方案继续废弃,不开发 HTTP client。
|
||||
|
||||
**方向:SaaS → Dispatcher**
|
||||
### 6.2 AI 不可变配置与授权
|
||||
|
||||
`POST /internal/v1/outbound/commands/{source_command_id}/replays`
|
||||
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 默认值兜底。
|
||||
|
||||
```json
|
||||
{"command_id":"<new-request-id>","reason":"<1..512 chars>"}
|
||||
```
|
||||
旧 `ai-config.openapi.yaml` 的 AI GET 已废弃为 D↔SaaS 接入方式,不再开发该 HTTP client。当前 `AISnapshotRaw`/`AIAuthorizationRaw` 启动注入和 Agent 本地校验只证明旧路径;MQ 配置/授权、关联与持久恢复仍待 W01/W07 实现验证。消息细节不能由旧 OpenAPI 自动推定。
|
||||
|
||||
必须提供 `Idempotency-Key`。成功响应为 `202`:
|
||||
## 7. 待完成门禁与禁止误读
|
||||
|
||||
```json
|
||||
{"command_id":"<new-request-id>","status":"accepted","snapshot_cutoff":"<now>"}
|
||||
```
|
||||
|
||||
当前实现读取原 inbox body,保留原 `command_id` 和原消息内容,按原租户 routing key 写入 outbox;`replay-<Idempotency-Key>` 只是 outbox event ID,不是新的业务 command ID。不存在 source command 返回 `404`。原消息的 `not_after` 不会被改写,重发后仍可能因过期被拒绝。
|
||||
|
||||
### 5.5 当前不可用的通话接口
|
||||
|
||||
以下路由已匹配,但当前固定返回 `404`:
|
||||
|
||||
- `GET /internal/v1/outbound/calls/{call_id}`;
|
||||
- `POST /internal/v1/outbound/calls/{call_id}/replays`。
|
||||
|
||||
不得依据上游 OpenAPI 的 `Call` 结构宣称当前代码已经提供通话查询或通话补传。
|
||||
|
||||
## 6. Dispatcher → SaaS 的已声明、未接入 HTTP
|
||||
|
||||
### 6.1 录音 upload session / complete
|
||||
|
||||
权威源为 `contracts/upstream/2026-09-19-p1-v1/saas.openapi.yaml`。
|
||||
|
||||
| 方向 | 方法 | 路径 |
|
||||
| 门禁 | 完成证据 | 当前状态 |
|
||||
| --- | --- | --- |
|
||||
| Dispatcher → SaaS | `POST` | `/internal/v1/outbound/recording-uploads` |
|
||||
| Dispatcher → SaaS | `POST` | `/internal/v1/outbound/recording-uploads/{upload_id}/complete` |
|
||||
| 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 | 待验证 |
|
||||
|
||||
公共要求:Bearer、`X-Tenant-ID`、`X-Request-ID`、`Idempotency-Key`。
|
||||
路由 fixture 只验证不同 Dispatcher 互不抢收,不把多 Dispatcher 调度或真实 SaaS 联调引入本轮。旧包、旧 HTTP handler、单租户 broker 测试和本地 OSS 成功,均不代表以上门禁通过。
|
||||
|
||||
申请请求的实际源字段为 `recording_id`、`call_id`、`content_type=audio/wav`、`size_bytes`、`checksum_algorithm=SHA-256`、`checksum`、`channels=1`、`sample_rate_hz`、`duration_ms`。返回 `upload_id`、`recording_id`、`expires_at`、`upload_method=PUT`、`upload_url`、`required_headers`、`constraints` 和可空 `oss_id`。
|
||||
## 8. 依据与相关文档
|
||||
|
||||
complete 请求为 `recording_id`、`size_bytes`、`checksum_algorithm=SHA-256`、`checksum`、可空 `etag`;成功返回 `status=verified`、`oss_id`、`verified_at`。
|
||||
|
||||
当前 Go 代码没有调用这两个 SaaS HTTP 路径。当前 Dispatcher upload handler 直接使用本地 `internal/oss` client 签发和验证 OSS grant;这条路径属于 Dispatcher ↔ Agent 的当前实现,不能写成 SaaS 已联调。
|
||||
|
||||
### 6.2 AI immutable version GET
|
||||
|
||||
权威源为 `contracts/upstream/2026-09-19-p1-v1/ai-config.openapi.yaml`:
|
||||
|
||||
`GET /internal/v1/ai/agent-versions/{agent_version_id}`
|
||||
|
||||
请求要求租户和 request header,返回不可变版本的 `tenant_id`、`agent_version_id`、`status`、`immutable=true`、`content_sha256` 和 `config`。`config` 顶层由 `agent_version_id`、`immutable=true`、`asr`、`conversation` 以及 full-AI 模式所需的 `llm`、`prompt`、`tts` 组成;ASR-only 不得带后三者。
|
||||
|
||||
当前实现只在 `internal/rpc.ServerOptions` 接收本地 `AISnapshotRaw`/`AIAuthorizationRaw`,由 `internal/ai/snapshot.go` 和 `internal/ai/authorization.go` 校验版本、摘要、租户、模式、有效期和 egress;没有 SaaS GET client,也没有“断 SaaS 后使用任意默认配置”的回退。
|
||||
|
||||
## 7. 禁止误读
|
||||
|
||||
1. RabbitMQ publisher confirm ≠ SaaS 已应用。
|
||||
2. HTTP `202 accepted` ≠ Agent 已执行或控制已生效。
|
||||
3. 当前本地 OSS grant/verify ≠ SaaS recording session/verified 已完成。
|
||||
4. Schema 支持 `recording.failed`、通话查询或 AI GET ≠ 当前 Go 代码已经生成或提供这些接口。
|
||||
5. 任何重试都必须保留原 `command_id`、`execution_id`、`event_id` 或 `upload_id` 的业务语义;未知执行不能换 ID 重拨。
|
||||
|
||||
## 8. 依据文件
|
||||
|
||||
- `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`
|
||||
- `contracts/upstream/2026-09-19-p1-v1/event-payloads.schema.json`
|
||||
- `contracts/upstream/2026-09-19-p1-v1/mq-topology.md`
|
||||
- `contracts/upstream/2026-09-19-p1-v1/executor.openapi.yaml`
|
||||
- `contracts/upstream/2026-09-19-p1-v1/saas.openapi.yaml`
|
||||
- `contracts/upstream/2026-09-19-p1-v1/ai-config.openapi.yaml`
|
||||
- [计划与需求阅读索引](../plan-0918.md):§1.2、§8.2 的 MQ-only 修订与状态。
|
||||
- [时间泳道图](./saas-rabbitmq-oss-dispatcher-agent-timeline.md):目标流程,不冒充当前实现。
|
||||
- [Dispatcher ↔ Agent 契约](./dispatcher-agent.md):现有 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 新契约已发布。
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
# SaaS ↔ RabbitMQ ↔ Dispatcher ↔ Agent ↔ OSS 时间泳道图
|
||||
|
||||
## 1. 用途与事实等级
|
||||
|
||||
**本图描述用户已确认的目标设计:SaaS 与 Dispatcher 的所有交互只经 RabbitMQ,不存在双方直连 HTTP。每个 Dispatcher 具有独立、全局唯一的 ID 和独立接收 Topic。** 不再把 AI GET 或录音 HTTP 握手列为待实现目标。
|
||||
|
||||
P1 运行范围仍为单节点、单 Cell、单租户、单活 Dispatcher;用 D1/D2 说明消息隔离,并不扩大为多 Dispatcher 调度或 HA。Dispatcher↔Agent 保持 Unary gRPC,Agent→OSS 保持直接上传,音频字节不经过 Dispatcher 或 MQ。**OSS 配置存于 D 的配置文件,Agent 向 D 领取临时上传 TOKEN;SaaS 不下发 OSS 配置/TOKEN。上传完成仍由 SaaS 独立校验并经 MQ 返回 verified,此职责不变。**
|
||||
|
||||
- **已确认**:MQ-only、全局唯一 Dispatcher 身份、专用 Topic、原业务与幂等边界。
|
||||
- **待冻结**:精确 Topic/队列/绑定、身份生命周期、MQ 消息类型/字段、请求响应关联/错误,以及异步结果与现有 Unary 的衔接。
|
||||
- **现有实现不等于目标完成**:旧租户 MQ、HTTP 控制、启动注入 AI 和上传待改项见 §5;D 签发 TOKEN 的职责保留,但 D 本地对象校验不能替代 SaaS verified。
|
||||
|
||||
图中“配置请求”“控制请求”“verified 响应”等为中文语义标签,**不是已发布的 command_type/event_type**。精确结构须在新版契约包冻结;旧 `contracts/upstream/2026-09-19-p1-v1/` 和 Proto 不因文档修改而自动支持这些交互。
|
||||
|
||||
## 2. 独立 Dispatcher 的订阅关系
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
S[SaaS] --> Q[RabbitMQ]
|
||||
Q --> T1[D1 专用接收 Topic / 队列]
|
||||
Q --> T2[D2 专用接收 Topic / 队列]
|
||||
T1 --> D1[Dispatcher D1 / 全局唯一 ID]
|
||||
T2 --> D2[Dispatcher D2 / 另一全局唯一 ID]
|
||||
D1 --> Q
|
||||
D2 --> Q
|
||||
Q --> ST[SaaS 专用订阅]
|
||||
ST --> S
|
||||
```
|
||||
|
||||
D1/D2 不竞争同一条接收队列,不靠全量广播后过滤模拟隔离。发往 D1 的任务、控制、AI 配置/授权、业务上传会话/验证结果只能由 D1 接收;D 发出的消息须能识别来源及原请求。Dispatcher 身份不替代租户身份,也不等于 `dispatcher_epoch`。具体命名/关联及完整路由长度约束见 [SaaS↔Dispatcher §2](./saas-dispatcher.md#2-通信拓扑dispatcher-身份与交付语义)。
|
||||
|
||||
## 3. 目标时间泳道图
|
||||
|
||||
时间自上而下;所有 S↔D 路径均经过 MQ。MQ 中的 D 接收订阅和 SaaS 接收订阅是独立方向,不是两套业务流程。为避免伪造字段,图只索引现有业务数据与待冻结语义。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant S as SaaS
|
||||
participant SQ as RabbitMQ / SaaS 专用订阅
|
||||
participant DQ as RabbitMQ / D1 专用接收 Topic 与队列
|
||||
participant D as Dispatcher D1 / 全局唯一 ID
|
||||
participant A as Agent
|
||||
participant O as OSS
|
||||
|
||||
Note over S,O: MQ-only 目标流程;新消息 Schema 待冻结,不代表已经实现。
|
||||
Note over DQ,D: D1 与 D2 的接收路径互相独立;本图仅展开 D1。
|
||||
|
||||
S->>DQ: call.execute(明确目标 D1;旧 payload 语义见 saas-dispatcher §3)
|
||||
DQ->>D: 按 D1 专用绑定投递
|
||||
Note over D: 校验目标、租户、版本、幂等及期限;持久 inbox/task/outbox。
|
||||
D-->>DQ: 持久成功后 ACK
|
||||
D->>SQ: command.result accepted(来源 D1、原命令关联)
|
||||
SQ->>S: 业务受理结果
|
||||
Note over S,SQ: SaaS 持久接收后 ACK;publisher confirm 不等于业务已应用。
|
||||
|
||||
opt 当前执行尚无合法绑定的配置与有效授权
|
||||
D->>SQ: AI 配置/授权请求(原租户、agent_version_id、请求关联;§6.2)
|
||||
SQ->>S: 配置/授权请求
|
||||
S->>DQ: 不可变配置、摘要、授权或明确拒绝(目标 D1、原请求关联)
|
||||
DQ->>D: 配置/授权响应
|
||||
Note over D: 校验并持久绑定到原执行;无有效授权不得继续发起。
|
||||
D-->>DQ: 持久成功后 ACK
|
||||
end
|
||||
|
||||
D->>A: GetAgentStatus(dispatcher-agent §4.1、§5.1)
|
||||
A-->>D: AgentStatus、boot、能力与资源
|
||||
D->>A: ActivateAgent(§5.2)
|
||||
A-->>D: ACTIVE + Session
|
||||
Note over D,A: 原执行配置交付、bootstrap/admission 完整编排仍须按契约核验,不由本图宣称完成。
|
||||
D->>A: GetExecutionPermit(原 binding、配置摘要、预留;§5.5)
|
||||
A-->>D: OperationReceipt + ExecutionPermit
|
||||
D->>A: Execute(原 call.execute、binding、配置摘要、permit;§5.6)
|
||||
A-->>D: OperationReceipt
|
||||
Note over D,A: ACCEPTED 不等于 SIP 已发起;拨号仍受许可、控制和时间窗口约束。
|
||||
|
||||
opt 执行期间控制任务
|
||||
S->>DQ: 控制请求(原任务、CAS、pause/resume/stop;saas-dispatcher §5.2)
|
||||
DQ->>D: 投递给 D1
|
||||
Note over D: 持久控制与 outbox;关闭相关新发起权限。
|
||||
D-->>DQ: 持久成功后 ACK
|
||||
D->>SQ: 控制 accepted
|
||||
SQ->>S: 已受理,不是 applied
|
||||
D->>A: ApplyTaskControl(dispatcher-agent §5.7)
|
||||
A-->>D: receipt / 控制事实
|
||||
Note over D,A: 所有必需屏障和挂断事实核验完成后才可 applied。
|
||||
D->>SQ: 控制进度或 applied 结果
|
||||
SQ->>S: 按原控制关联更新状态
|
||||
end
|
||||
|
||||
Note over A: 通话、媒体、AI 和录音产生真实 ExecutionFact。
|
||||
A->>D: ReportExecutionEvent(dispatcher-agent §6.1)
|
||||
D-->>A: 事实持久接收结果
|
||||
D->>SQ: call.status / transcript.updated / call.finished / contact.opt_out
|
||||
SQ->>S: 持久应用业务事件(saas-dispatcher §4)
|
||||
|
||||
A->>D: RequestUpload(原 binding、AssetDescriptor、upload_id;§6.2)
|
||||
Note over D: OSS 配置来自 D 配置文件;缺失/无效明确失败,不向 SaaS 获取配置或 TOKEN。
|
||||
Note over D,A: 业务会话 MQ 结果可能晚于 Unary deadline;有界等待/重取待冻结,不是等 SaaS 签 TOKEN。
|
||||
D->>SQ: 原资产业务上传会话请求(仅元信息,不申请 TOKEN;saas-dispatcher §6.1)
|
||||
SQ->>S: 既有业务会话/资产登记语义
|
||||
S->>DQ: 业务会话结果或拒绝(目标 D1、原请求/资产关联;无 OSS 配置/TOKEN)
|
||||
DQ->>D: 原业务会话响应
|
||||
Note over D: 校验/持久会话关联,依据自身配置通过 SDK 提供临时 TOKEN。
|
||||
D-->>DQ: 持久成功后 ACK
|
||||
D-->>A: Unary 返回 D 提供的临时 TOKEN 及受限 UploadGrant(精确映射待核验)
|
||||
|
||||
A->>O: PUT recording bytes(dispatcher-agent §6.3)
|
||||
O-->>A: PUT 结果 / ETag
|
||||
Note over A,O: A 本地记录实际大小与 SHA-256;PUT 或 ETag 不等于 verified。
|
||||
A->>D: CompleteUpload(原资产/会话、大小、SHA-256;§6.4)
|
||||
D->>SQ: complete 请求(原会话及元信息)
|
||||
SQ->>S: 完成请求
|
||||
S->>O: 独立验证原对象(具体校验方式由 SaaS 合同定义)
|
||||
O-->>S: 对象验证依据
|
||||
S->>DQ: verified + oss_id 或明确失败(原关联)
|
||||
DQ->>D: 完成验证结果
|
||||
Note over D: 只有合法 verified 才同事务记录完成状态及 recording.ready outbox。
|
||||
D-->>DQ: 持久成功后 ACK
|
||||
D-->>A: 通过获批 Unary 衔接返回最终上传结果
|
||||
D->>SQ: recording.ready(saas-dispatcher §4.3)
|
||||
SQ->>S: OSS ID 与原录音元信息
|
||||
|
||||
opt 查询或整体补传
|
||||
S->>DQ: 原 command/call 查询,或 call_id/source_command_id 整体补传请求
|
||||
DQ->>D: 按目标 D1 接收(saas-dispatcher §5.3–§5.4)
|
||||
D->>SQ: 关联查询结果,或原事件身份/内容/版本的补传
|
||||
SQ->>S: 查询响应或补传结果
|
||||
Note over S,D: 不重新投 call.execute 执行;响应超时不走 HTTP 兜底。
|
||||
end
|
||||
```
|
||||
|
||||
图中后续步骤均以所需校验和前置条件成功为前提;拒绝、超时和失败保留原关联及待恢复状态,不继续执行成功分支。各类 MQ 请求/响应都适用持久后 ACK,图未重复画出所有 broker confirm/消费者 ACK;它们不能代替业务状态。
|
||||
|
||||
## 4. 交互与结构索引
|
||||
|
||||
| 交互 | 数据/语义来源 | 新设计状态 |
|
||||
| --- | --- | --- |
|
||||
| 身份、目标与专用 Topic | `saas-dispatcher.md §2` | 原则已确认;精确命名/消息关联待冻结 |
|
||||
| 执行请求/受理结果 | `saas-dispatcher.md §3–§4` | 旧业务字段可对照;Dispatcher 路由待改 |
|
||||
| AI 配置/授权请求响应 | `saas-dispatcher.md §6.2` | 全部 MQ;旧 HTTP GET 不再是目标 |
|
||||
| 状态/激活/许可/执行 | `dispatcher-agent.md §4–§5` | 既有 Unary;完整配置/执行接线以代码证据为准 |
|
||||
| 控制/查询/补传 | `saas-dispatcher.md §5`;`dispatcher-agent.md §5.7` | MQ 请求与响应待冻结/实现,旧 HTTP 废弃 |
|
||||
| Agent 事实与业务事件 | `dispatcher-agent.md §6.1`;`saas-dispatcher.md §4` | 旧 fact/outbox 已有;来源路由与已知映射差异待验证 |
|
||||
| 临时上传 TOKEN | `saas-dispatcher.md §6.1`;`dispatcher-agent.md §6.2` | OSS 配置在 D 文件;D 提供 TOKEN,A 经 Unary 领取,SaaS 不签发或下发配置 |
|
||||
| 业务上传会话 | `saas-dispatcher.md §6.1` | 保留既有 SaaS 业务语义,经 MQ 关联原资产/会话;不作为 TOKEN 来源,异步衔接待冻结 |
|
||||
| 文件上传 | `dispatcher-agent.md §6.3` | Agent→OSS,不经 D/MQ,不改变对象上传协议 |
|
||||
| complete/verified/ready | `saas-dispatcher.md §6.1`;`dispatcher-agent.md §6.4` | SaaS MQ verified 才触发 D 的 ready;待实现/验证 |
|
||||
|
||||
## 5. 当前实现与目标设计差异
|
||||
|
||||
| 能力 | 既有实现事实 | 必须完成的纠正 |
|
||||
| --- | --- | --- |
|
||||
| Dispatcher 身份/订阅 | MQ 命令仅按 tenant key 路由 | 全局唯一 ID、独立 Topic/队列、原请求定向响应及来源校验 |
|
||||
| 控制/查询/补传 | 旧 HTTP handler;部分通话路由固定 404,source-command replay 重发原命令 | 移除 SaaS HTTP 业务入口;全部 MQ,补传只恢复原业务结果,不重拨 |
|
||||
| AI 配置来源 | 启动注入 `AISnapshotRaw` / `AIAuthorizationRaw` | SaaS MQ 配置/授权及持久绑定,不接旧 AI GET |
|
||||
| 上传 TOKEN 与验证 | D 已有 OSS client 签发 grant、直接验证对象并发 ready | 保留 D 签发职责,核验配置文件/TOKEN 约束;业务会话与 verified 仍经 SaaS MQ,本地对象验证不能替代 verified |
|
||||
| R12/R13 | 当前同步本地 grant/verify | 核验 R12 依据 D 配置提供 TOKEN,以及业务会话/complete 的 MQ 持久关联、有界等待、原操作恢复与最终 Unary 交付 |
|
||||
| Agent Execute | handler 证明准备/校验/幂等接收 | 不能据此宣称实际 ARI/RTP/SIP/AI 生命周期已完成 |
|
||||
| TRANSCRIPT_FAILED | 当前 aggregate 类型映射不通过 Schema | 已知差异保留,另行实现修正,不因文档变更标通过 |
|
||||
|
||||
## 6. 幂等、失败与验收边界
|
||||
|
||||
1. 重投沿原请求、Dispatcher、租户和业务对象关联;同 ID 异内容冲突,不能换 execution/attempt/asset 绕过。
|
||||
2. D1 离线不把未决请求/响应改投 D2;MQ 中断不回退 HTTP,执行未知不自动重拨。
|
||||
3. 配置迟到、重复或已撤销时不能覆盖在途绑定;无有效授权拒新准入。
|
||||
4. 上传回包丢失复用原 `upload_id` 和资产摘要;TOKEN 过期只接受 Agent 显式向 D 重新申请,配置不来自 SaaS,也不把 D 配置/TOKEN 经 MQ 传给 SaaS。未取得 SaaS verified 时不得发 ready 或提前删文件。
|
||||
5. 新 boot/epoch 不清旧未知执行和占用;RPC 超时按原 binding 查询/对账。
|
||||
6. 交付检查覆盖无 SaaS↔D HTTP、D1/D2 路由隔离、错目标/重复身份、请求响应乱序/超时/重启恢复、不可路由与 confirm 丢失;不以文档图、单租户 Mock 或本地 OSS 验证冒充通过。
|
||||
|
||||
## 7. 相关文档
|
||||
|
||||
- [SaaS ↔ Dispatcher 对接契约](./saas-dispatcher.md)
|
||||
- [Dispatcher ↔ Agent 对接契约](./dispatcher-agent.md)
|
||||
- [计划与需求阅读索引](../plan-0918.md)
|
||||
Reference in New Issue
Block a user