diff --git a/AGENTS.md b/AGENTS.md index f760096..ca07678 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -171,7 +171,8 @@ - **用户已修订目标:上传仅负责Agent直传及D可靠通知MQ,SaaS后续处理不属本项目职责。** D保留签发能力、不转发文件;R13持久保存事实与recording.uploaded outbox,消息为persistent、进入指定durable队列/绑定、mandatory无return且publisher confirm成功后才记交付完成。只写本地outbox不算入队;不申请SaaS会话、不等待verified/OSS ID、不新增VERIFYING、不伪造SaaS结果。 - 新版recording.uploaded取代本项目recording.ready,字段为call_id/recording_id/upload_id/bucket/object_key/format/channels/sample_rate_hz/duration_ms/size_bytes/checksum_sha256;不含TOKEN/密钥/签名URL。MQ失败/确认丢失/重启只恢复原消息身份的交付,不重新PUT或新建资产。现行AI/控制等必要请求响应不受此收缩影响;新目标AI改用任务只读HTTP获取,控制仍走MQ。 - 前一轮MQ-only文档纠正已结束;以下是**现行 MQ-only v1 合同的历史基线**,不得误当作新只读配置HTTP方案的限制或验收通过:现行机器依据见 `contracts/upstream/v1/mq-topology.json`、`mq.schema.json` 和 `event-payloads.schema.json`,第三方顺序说明见 `docs/thirds/第三方对接事件与请求消费顺序_v0.1.md`:纯Topic精确绑定、拒绝独立通配词段、tenant_key预算196 UTF-8字节、稳定UUID v4、旧版严格JSON配置及15分钟SDK预签名PUT;新目标保留业务MQ及OSS规则,仅替换配置获取,须另发版本化合同,不原地复用旧Schema。当前仅保留现有代码需要的契约包;未发生契约迁移前不新增契约,旧快照不作为当前工作树输入。新版须完成Schema/正反例/哈希验证后发布,不把方案确认当实现完成。历史 W01/W02/W04/W05/W07/W08/W11/W12/W13/W14 范围见归档 `docs/archive/plan-0918.md` §8.2;本轮新配置/控制路径须按 `docs/plan-config-read-v0.1.md` §4–§8 重新验证。全局唯一D身份/专用Topic及本地D1/D2隔离fixture为当前合同要求,不授权双D业务运行、HA或共享额度。 -- 文本实时事件准确名为transcript.updated,不新增call.transcript别名;OSS文本归档不能替代实时文字/opt-out,缺少专用资产授权接口时明确未启用,不能伪装recording.ready。 +- **现行已发布合同**中的实时文字事件名为transcript.updated,不新增call.transcript别名;OSS文本归档不能冒充当前实时文字/opt-out,缺少专用资产授权接口时明确未启用,不能伪装recording.ready。 +- **待 review/待 SaaS 与业务签收的下一轮方向**见 `docs/plan-config-read-v0.1.md` §4.1:SaaS↔D 移除对外查询/补传命令及分散通话/转写/拒联/录音事件,保留呼叫/控制命令和必要命令回执;录音仍上传 OSS,D 在上传成功后只回传一份含最终转写、拒联事实与 OSS 路径的 `call.result` 草案。用户已确认不要求 SaaS 在通话结束前收到实时文字或拒联,但原验收与跨任务/跨 D 拦截能力将变化,必须单独修订并签收;录音失败/超时的有界收口仍待冻结。现行 Schema/代码未改、此草案不得冒称已上线,且本轮只改文档供用户 review。 - 现有MQ信封command_type/command_id、event_type/aggregate_*与正文已对齐;事件payload专属约束尚需补齐,不把通用object校验当完整验收。字段索引只读生成,不手改成第二套Schema。 - `tenant_key` 原值一对一绑定,不清洗、编码或截断;旧布局224个UTF-8字节预算不能在加入D身份后直接照搬,W01须冻结并验证完整routing key/queue预算及分隔符/通配符边界;超限停止发布并保留源任务。 - 持久 inbox 后 ACK;状态与 outbox 同事务;confirm 不等于 SaaS 应用收讫。重复投递、未知执行和恢复不能触发重复拨号。 diff --git a/README.md b/README.md index 9199641..aa2e312 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ - 重写完整 Agent:调度控制面、Cell 外呼执行、ARI/RTP/录音、AI 流式适配、Cell 配置接收。 - 分阶段迁移,最终构建、测试和运行不依赖 Python、父仓库目录或其它业务项目内部代码。 - Asterisk 继续负责 SIP;独立 SIP 管理平台继续拥有配置管理权,均不纳入重写。 -- **现行已发布的 SaaS↔Dispatcher 合同**:全部请求、响应和事件只走 RabbitMQ,双方无 HTTP;每个 D 有全局唯一 ID 及独立接收 Topic/队列。新目标只让获批 SIP 全量配置及任务(内含智能体)经两条只读 HTTP 接口获取,执行/控制/查询/结果/上传事实仍走 MQ;合同、代码和验收未切换前继续遵守现行规则。P1 不扩为多 D 协调/HA。 +- **现行已发布的 SaaS↔Dispatcher 合同**:全部请求、响应和事件只走 RabbitMQ,双方无 HTTP;每个 D 有全局唯一 ID 及独立接收 Topic/队列。新目标只让获批 SIP 全量配置及任务(内含智能体)经两条只读 HTTP 接口获取,现行执行/控制/查询/结果/上传事实仍走 MQ;下一版另拟移除对外查询/补传及拆分通话事件,仅以一份含 OSS 路径的最终结果回传。两项目标均待合同签收、代码和验收切换,现行规则仍有效。P1 不扩为多 D 协调/HA。 - **OSS配置存于Dispatcher配置文件,Agent向Dispatcher领取临时上传TOKEN后直传OSS,不保存长期凭据。** SaaS不下发OSS配置/TOKEN;D保留SDK签发能力,不转发文件。上传不申请SaaS会话、不等待verified/OSS ID或业务处理回复;D仅在原上传事实可靠进入指定持久MQ队列后记录交付完成。实时文字仍为 `transcript.updated`,归档不能替代实时事件。 - 本次必须支持 **ASR-only** 与 **ASR + LLM + TTS** 两种模式;本阶段已完成本地/协议隔离双模式验收。百炼/火山ASR、OpenAI兼容LLM、火山TTS的真实供应商能力和生产参数联调仍标第二阶段/未启用,不能将 Mock 写成真实供应商通过;禁止复用旧LLM/TTS。 - **现行 AI 配置**按任务 agent_version_id 经 MQ 向 SaaS 取得并持久绑定;过去独立的 AI GET 已废弃。**新目标**将获授权的智能体配置放入任务只读 HTTP 响应,不复活旧 GET 或保留 MQ 配置回退。 模型、提示词、音色/语速、识别、超时/打断等参数不写死;新版本用于新任务,无需重启,在途通话固定快照。静态SIP发布不代表AI配置静态硬编码。 @@ -62,7 +62,7 @@ sip-go-agent agent upload-retry --spool /path/to/agent-spool \ | [旧 plan-0918 归档原文](docs/archive/plan-0918.md) | W00–W16 历史状态及旧 MQ-only 阶段证据;非本轮执行入口 | | [Go重写方案 v0.3](docs/architecture/Go重写方案_v0.3.md) | 本次单节点/单 Cell/单租户目标、至少3 SIP fixture、双AI模式、P0/P1/第二阶段分期 | | [通信与事件数据交互 v0.1](docs/contracts/通信与事件数据交互_v0.1.md) | 首发Unary、静态SIP与双模式、§6.1–6.3 SaaS配置来源/参数矩阵/安全边界 | -| [SaaS↔Dispatcher 第三方对接顺序](docs/thirds/第三方对接事件与请求消费顺序_v0.1.md) | 配置获取、MQ命令/查询、通话反馈及录音通知的请求、返回、字段和消费动作;区分现行与待签收目标,替代原六份已删除说明 | +| [SaaS↔Dispatcher 第三方对接顺序](docs/thirds/第三方对接事件与请求消费顺序_v0.1.md) | 配置获取、现行 MQ 命令/回执与拟定单份最终通话结果;取消查询/补传和分散事件仍待签收,替代原六份已删除说明 | | [现行 MQ 机器契约](contracts/upstream/v1/mq.schema.json)、[Agent Proto](proto/agent/v1/agent.proto) | 现行消息和Unary字段权威;HTTP配置仅有项目提案 | | [OpenAPI与MQ字段索引 v0.1](docs/references/OpenAPI与MQ字段索引_v0.1.md) | 旧源只读提取42个HTTP操作/115个组件及哈希;不代表新MQ-only入口,不手改生成物 | | [deploys:物理机 systemd 发布包](deploys/README.md) | 锁定 Debian 13/Go 1.27.1/发布版本,构建并上传不依赖 Docker 的 Agent/Dispatcher 安装包 | diff --git a/docs/README.md b/docs/README.md index 40aa03f..d1d7d3a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -23,7 +23,7 @@ ### 契约与决策 -- [`thirds/第三方对接事件与请求消费顺序_v0.1.md`](thirds/第三方对接事件与请求消费顺序_v0.1.md):只描述 SaaS↔Dispatcher 的配置读取、MQ 呼叫/控制/查询、通话反馈与录音通知的请求、返回、字段和消费顺序;现行与拟定目标分开,替代已删除的六份分散说明。 +- [`thirds/第三方对接事件与请求消费顺序_v0.1.md`](thirds/第三方对接事件与请求消费顺序_v0.1.md):只描述 SaaS↔Dispatcher 的配置读取、现行 MQ 呼叫/控制命令及回执、拟定单份最终通话结果;取消对外查询/补传和分散事件尚待签收,替代已删除的六份分散说明。 - [`../contracts/upstream/v1/mq.schema.json`](../contracts/upstream/v1/mq.schema.json)、[`event-payloads.schema.json`](../contracts/upstream/v1/event-payloads.schema.json)、[`mq-topology.json`](../contracts/upstream/v1/mq-topology.json):现行 MQ 信封、事件正文、路由的机器契约。 - [`contracts/config-read-fields-v0.1-proposal.md`](contracts/config-read-fields-v0.1-proposal.md):两只读接口字段来源、响应结构与项目自拟 Schema/Mock 示例;**不是现网 SaaS JSON 或正式合同**。 - [`decisions/`](decisions/):已记录的关键技术决策。 diff --git a/docs/plan-config-read-v0.1.md b/docs/plan-config-read-v0.1.md index 3c89eef..baf3ed8 100644 --- a/docs/plan-config-read-v0.1.md +++ b/docs/plan-config-read-v0.1.md @@ -4,28 +4,28 @@ ## 1. 范围和完成标准 -本次要建立两个**只读 SaaS→Dispatcher HTTP 配置接口**:取得 management 已批准、适用于该 Dispatcher 的完整 SIP 配置;取得归属该 Dispatcher 的单个任务配置(内含获授权的智能体不可变版本/参数)。任务、智能体及 SIP 配置修改后,**已接纳**的执行保持旧快照;尚未接纳的执行允许在不超过约 60 秒的已核验缓存期内使用旧批准版本。呼叫、控制、查询、补传、`recording.uploaded` 和业务结果仍唯一经 RabbitMQ;不保留旧 AI/SIP 配置 MQ 回退,也不复活历史业务 HTTP。 +本次要建立两个**只读 SaaS→Dispatcher HTTP 配置接口**:取得 management 已批准、适用于该 Dispatcher 的完整 SIP 配置;取得归属该 Dispatcher 的单个任务配置(内含获授权的智能体不可变版本/参数)。任务、智能体及 SIP 配置修改后,**已接纳**的执行保持旧快照;尚未接纳的执行允许在不超过约 60 秒的已核验缓存期内使用旧批准版本。下一轮按 §4.1 拟将 SaaS↔D 的 MQ 业务面收敛为呼叫/控制命令、命令回执和**录音上传 OSS 后的一份最终通话结果**:移除对外查询与补传命令、拆分的通话/转写/拒联/录音事件。文件仍上传 OSS,SaaS 从最终消息取得 `bucket/object_key`;不保留旧 AI/SIP 配置 MQ 回退,也不复活业务 HTTP。**现行严格 Schema 和代码尚未变更,不能将新目标写成已上线。** 当前 **P1 仍为单 D/单 Agent/单 Asterisk/单 Cell/单租户**,现行已发布 SaaS↔D 全 MQ 契约、静态 SIP 制品、Asia/Shanghai `[09:00,20:00)` 门禁和旧排队 AI 版本绑定,在新版本逐项发布、替换及验证前继续执行;本计划和字段草案**不授权**新线路、跨窗真实拨号、SaaS 生产部署或多 D 业务运行。外部真实 SaaS、云资源、供应商联调及拨号须分别获授权。 **完成条件分层:** -- **C(合同)**:两接口实际 SaaS/management 来源、路径/身份承载/返回及错误 Schema、版本/摘要/ETag/缓存/修改并发冻结点、现行 `call.execute` 旧引用如何合法换版,连同必须变更的 MQ 控制语义,以**新版本**获签收、正反例和哈希校验。当前 [项目自拟 Schema](contracts/config-read-v0.1.schema.json)只算草案,不是 C。 -- **L(单 D 本地闭环)**:单租户的任务+智能体读缓存、SIP 完整获批加载、有界消费/独立控制、任务×线路时段、ACK/恢复及 outbox 本地隔离故障测试通过;配置失效只停新准入,停止/暂停仍可达。完成本项目格式化、`go vet ./...`、`go test -race ./...`、构建和模块覆盖率 ≥65%,留脱敏事实证据。旧本地通过不代替 L。 +- **C(合同)**:两接口实际 SaaS/management 来源、路径/身份承载/返回及错误 Schema、版本/摘要/ETag/缓存/修改并发冻结点、现行 `call.execute` 旧引用如何合法换版,连同 MQ 控制屏障、移除对外查询/补传、单份最终通话结果的交付与失败收口,以**新版本**获签收、正反例和哈希校验。当前 [项目自拟 Schema](contracts/config-read-v0.1.schema.json)只算草案,不是 C。 +- **L(单 D 本地闭环)**:单租户的任务+智能体读缓存、SIP 完整获批加载、有界消费/独立控制、任务×线路时段、ACK/恢复及 outbox 本地隔离故障测试通过;配置失效只停新准入,停止/暂停仍可达;新合同下最终结果在 OSS 上传成功后只发一次,失败在有界时限内明确收口(细节须签收)。完成本项目格式化、`go vet ./...`、`go test -race ./...`、构建和模块覆盖率 ≥65%,留脱敏事实证据。旧本地通过不代替 L。 - **M(多 D 后续门禁)**:同租户多任务、**每任务只归一个 D**,不同 D 独占 Agent/Asterisk;D1/D2 任务/缓存/队列隔离、租户/供应商额度份额总和限制、未知占用和非自动迁移故障注入。仅当多 D 开发与授权另行确定时验收,不混入当前单 D 的 L。 ## 2. 权威资料和读法 1. **先读本文件**,按第 4 节找到 F 工作包,再读对应的项目现行合同:[Go 架构](architecture/Go重写方案_v0.3.md)、[验收](acceptance/验证与切换验收_v0.3.md)、[通信与事件](contracts/通信与事件数据交互_v0.1.md)、[开源复用](dependencies/开源组件选型与复用清单_v0.2.md)及 `contracts/upstream/v1/`。现行运行依据优先于尚未签收的新目标;差异写进 F01,不从旧严格 Schema 擅加字段。 2. [页面截图分析](references/saas-page-snapshot-analysis.md)只证明页面显示的业务含义,**不能**证明网络请求/响应的 JSON 键、类型或 SIP 实际管理字段。[字段提案](contracts/config-read-fields-v0.1-proposal.md)里的英文键是项目自定义;旧 [OpenAPI/MQ 索引](references/OpenAPI与MQ字段索引_v0.1.md)也只是历史对照,绝非现网 SaaS API 签收。 -3. [第三方对接事件与请求消费顺序](thirds/第三方对接事件与请求消费顺序_v0.1.md)只按 SaaS↔Dispatcher 现行 MQ 命令/反馈与拟定 HTTP 两接口的顺序说明请求、返回和字段;是联调读物,**不是新 Schema 或外部签收**。已归档的[原 plan-0918](archive/plan-0918.md)保留旧 W、I/M/G、部署和证据的历史语境;不要继续写旧台账或以旧 `§9` 当本轮并行授权。每个工作包的新状态只在本文 §8 登记。 +3. [第三方对接事件与请求消费顺序](thirds/第三方对接事件与请求消费顺序_v0.1.md)区分现行 MQ 命令/回执、拟定 HTTP 两接口和**尚未发布的单份通话结果**,仅说明 SaaS↔Dispatcher 请求、返回和字段;是联调读物,**不是新 Schema 或外部签收**。已归档的[原 plan-0918](archive/plan-0918.md)保留旧 W、I/M/G、部署和证据的历史语境;不要继续写旧台账或以旧 `§9` 当本轮并行授权。每个工作包的新状态只在本文 §8 登记。 ## 3. 已确认方向与不能省略的边界 - **配置源:**SaaS 负责任务、智能体及单任务→D 的持久归属;management 是 SIP 唯一编辑/审批面,SaaS 只读分发准确的获批准 SIP 制品。如 management/SaaS 不共享同一权威来源,先证明同步及全量范围,再宣称两接口足够。 - **两接口的响应:**SIP 全量须覆盖适用线路/连接、主叫、媒体、额度、时段及获批制品的版本/摘要;任务响应包含任务时段(周一至周日逐日多段、可选多排除日期)、线路选择、任务额度、所选智能体不可变配置及有效授权。统一 Asia/Shanghai;两种时段必须同时允许,缺失/未知禁止新拨号。供应商 transport/auth/registration 尚未确认时不得为 real 设默认值。UI 已显示但 Agent 无批准 SDK 参数的项记录为缺口,不通过 `metadata` 偷渡。 - **缓存与变更:**每 D 使用自己的 UUID+SECRETKEY 读取;成功校验的任务缓存按租户+任务约 60 秒,活跃且尚有待接纳执行时到期主动复核;SIP 启动/重启取全量,运行中约每 60 秒核对。条件请求的 `ETag/304` 可省内容,不能延长过期或撤销的 AI 授权。到期失败/答复不明就停**新执行准入**,不让 Agent 凭过期缓存发起新呼叫;已接纳的执行始终使用持久绑定的快照。SIP 变更先关准入、排空/对账旧使用者并核验 Agent/Asterisk 实际加载,**发现变化 ≤约60秒不代表加载 ≤约60秒**。任务终结仅删除可重取的配置缓存,不删幂等/执行恢复/outbox 事实。 -- **MQ 与任务所有权:**每任务只投递到归属 D/租户的队列,控制也投同一 D;不要求每任务单建队列。D 仅在有可用任务/租户/Cell/线路/AI 名额及配置资格时有界接收、同事务持久绑定执行/占用/outbox 后 ACK。其它呼叫仍留 MQ;控制/查询应有独立通道,满额仍能到达。当前共用队列与 `not_found` 处理无法保证未接纳任务先收到 stop 后不拨,必须先冻结屏障与乱序语义。缓存延迟不延迟 MQ stop/pause、opt-out、号码白名单或最后发起许可。 +- **MQ 与任务所有权:**每任务只投递到归属 D/租户的队列,控制也投同一 D;不要求每任务单建队列。D 仅在有可用任务/租户/Cell/线路/AI 名额及配置资格时有界接收、同事务持久绑定执行/占用/outbox 后 ACK。其它呼叫仍留 MQ;控制应有独立通道,满额仍能到达。当前共用队列与 `not_found` 处理无法保证未接纳任务先收到 stop 后不拨,必须先冻结屏障与乱序语义。缓存延迟不延迟 MQ stop/pause、号码白名单或最后发起许可。新目标不再向 SaaS 实时发送 opt-out/转写:在最终结果到达前,SaaS 无法按该事实拦截其他任务或跨 D 后续呼叫,原实时文字/即时拒联产品门禁必须由业务负责人明确批准修改;未签收前不切换。 - **时间/多 D:**目标时间由任务及所选 SIP 线路交集决定,排除日期优先;没有合同和最后拨号门禁前仍执行固定 `[09:00,20:00)`。多个 D 各有独占执行资源,但**单任务归一 D 仅解决该任务的并发**;若同一租户或供应商额度跨 D,共享总上限须权威分配有界份额,份额总和不超上限。D1 队列的未知/未决任务不能被 D2 抢收或自动迁移。 ## 4. 分步任务(按依赖顺序) @@ -33,27 +33,38 @@ | 工作包 | 输入 / 负责人边界 | 完成证据 / 未满足时状态 | | --- | --- | --- | | F00 页面与字段预盘点 | 项目文档负责人:引用截图、现有 AI/静态 SIP Schema 和旧只读索引,逐字段标 P/C/N;两种成功及错误响应草案、示例/正反例 | [字段提案](contracts/config-read-fields-v0.1-proposal.md)与[项目自拟 Schema](contracts/config-read-v0.1.schema.json)本地可校验;**草案完成≠ SaaS 实际字段已核验**。 | -| F01 新合同冻结(前置于任何 client/管理发布) | SaaS/management 权威负责人分别确认数据源、接口路径/UUID+SECRETKEY承载、字段类型/缺省/范围、批准状态、版本/哈希、响应错误、缓存约60秒与同时修改/接纳的冻结点;本项目集成负责人重版导入 W01 所需权威包并明确废弃旧 AI 配置 MQ 路径及新业务 MQ 拓扑 | 新版本来源/版本/哈希、严格 Schema、正反例、批准记录;任务字段与线下管理 SIP 完整可用,供应商不明参数、原 `call.execute` 引用矛盾及 HTTP 304 授权未解决均记 **blocked**,不能先写旧路客户端。 | +| F01 新合同冻结(前置于任何 client/管理发布) | SaaS/management 权威负责人分别确认数据源、接口路径/UUID+SECRETKEY承载、字段类型/缺省/范围、批准状态、版本/哈希、响应错误、缓存约60秒与同时修改/接纳的冻结点;本项目集成负责人重版导入 W01 所需权威包并明确废弃旧 AI 配置 MQ 路径、旧查询/补传命令及分散通话事件的新业务 MQ 拓扑 | 新版本来源/版本/哈希、严格 Schema、正反例、批准记录;任务字段与线下管理 SIP 完整可用,供应商不明参数、原 `call.execute` 引用矛盾及 HTTP 304 授权未解决均记 **blocked**,不能先写旧路客户端。 | | F02 最小单 D 配置读取(依赖 F01) | SaaS 提供两条只读接口;本项目 D 对 SIP 启动全量与 Agent/Asterisk applied 事实核验,对归属任务按需拉取内含 AI、60秒缓存/ETag/304、失效停新准入;所有密钥/端点受控注入 | 新 D 无历史广播亦可取全量;HTTP 过期/错误/更新竞态、新旧 Agent 参数及快照不漂移测试;SIP 制品未准确加载拒新执行,控制仍可达。 | | F03 有界执行队列与控制隔离(依赖 F01) | 本项目 D 消费、SQLite 额度/绑定和 MQ 适配:空位才接纳,未接纳留 MQ;执行/控制路由分离须新版合同及 SaaS 发布端配合;旧直写面禁并行 | 爆量消息 SQLite 未接纳积压不无界增长,MQ 队列有容量/发布拒绝与 SaaS 原消息保留;commit/ACK/confirm 丢失及 stop-before-accept 不拨、不忙重投;unknown 不自动释放。 | | F04 时段与最后屏障(依赖 F01/F02/F03) | SaaS 批准任务周多段/排除日与 SIP 线路时段,D/Agent 使用同一已加载版本及 Asia/Shanghai 注入时钟;替换固定窗须对应合同/验收同步更新 | 左闭右开、空日/多个排除日期/任务与线路交集、缓存更新后 ≤约60秒、队列跨窗不自动延迟、D/Agent 最后拨号门禁均通过;仅 Mock 时间不宣称 real 已放行。 | | F05 多 D 独占资源(后续,依赖 F01–F04 及另获本阶段授权) | SaaS 对 `(tenant_key,task_id)→dispatcher_id` 持久路由;D1/D2 各占独立 Agent/Asterisk/队列/持久目录,租户和供应商共享额度按 D 分份额 | 双 D 错投/重启/配置变更/未知通话及份额总和故障注入;不得用 D1/D2 本地合同 fixture 冒充双 D 真实业务或 HA。 | | F06 验收与切换(依赖相应 F 包) | 集成负责人单写现行 §8 台账,模块证据只存脱敏状态、计数、哈希;生产、真实 SIP/OSS/云另行授权 | 新 C→L→M 门禁分开记录。至少 gofmt、`go vet ./...`、`go test -race ./...`、`go build ./...`、本模块覆盖率≥65%;版本回退不得同时运行旧/新路径、触发第二次拨号。 | +### 4.1 下一轮迭代:单份通话结果与对外消息收敛(新增,待 SaaS/业务签收) + +以下只修改目标文档,**不授权立即删现行 MQ Schema、队列/代码,也不替代 F01 门禁**。一个任务仍归一个 D,SaaS 只向该 D 发送 `call.execute` 与 `task.control`;D 的 `command.result` 只表示命令回执,不能冒充通话结局。D 从已有内部事实整理一份拟定 `call.result`,汇总呼叫身份、结果、最终转写、拒联事实和录音 OSS 资产;文件仍上 OSS,成功上传并持久化后才投递最终结果。SaaS 不再使用对外 `call.query`、`command.query`、`call.replay`、`command.replay` 或各类拆分通话/录音事件。对外命令取消不删除 D 的内部恢复/去重事实,也不允许未知是否已拨号时重拨。 + +| 工作包 | 前置 / 下一步 | 可核验验收标准 | +| --- | --- | --- | +| F07 签收新 MQ 合同(依赖 F01) | SaaS、业务与本项目共同批准:撤销对外查询/补传及分散通话事件;冻结 `call.result` 的信封、字段、成功/失败结构、任务与命令关联、`event_id`/`upload_id` 幂等、路由、ACL、消息大小、超时及录音缺失的有界收口;确认业务愿意取消 SaaS 实时文字与即时拒联及其跨任务/跨 D 后果。任务配置统一给出振铃/最长通话时限,确定 `call.execute` 仍携带的执行快照如何取得同版值。 | 发布有来源、版本、哈希、严格 Schema 和正反例的新合同;覆盖已接通/未接通、录音成功、OSS 永久失败/过期、用户拒联、消息丢失/重复/乱序、命令重复、暂停/停止先到旧执行后到;真实 SaaS 与业务签收和新版原验收冲突处理均可追溯。无签收即 blocked,不用项目示意 JSON 冒充正式接口。 | +| F08 本地实现与受控切换(依赖 F07/F02–F04) | TDD 替换 SaaS↔D 旧查询/补传入口及分散事件/outbox,保留命令回执和本地恢复;录音已成功上传 OSS 才组装单一最终结果,失败/超时按 F07 有界规则只报一次;SaaS 消费端配合新版本,旧/新版本不得混写同一任务。 | 同一次通话至多一份稳定身份的最终结果;persistent、指定 durable 队列、mandatory 无 return、publisher confirm 成功后才记 MQ 交付;确认丢失或重启只重投原身份,不重新拨号/PUT。停止任务仍逐条消费并拒绝该任务积压命令、ACK,不清空租户共享队列。无端到端版本切换/恢复证据不得启用。 | +| F09 结果验收(依赖 F08) | 单 D 单租户隔离链路验收成功/失败全矩阵,按 F07 批准的新版本分开做真实 SaaS 联调(另获授权)。 | SaaS 仅收到命令回执和一份最终通话结果,成功结果含 OSS `bucket/object_key`、校验和、最终转写/拒联事实;录音缺失在约定时限内显式收口、不无限等候;积压/停止/重投、断 MQ/OSS 与重启后无重复拨号、无多份结果或误称 SaaS 已消费。校验格式化、`go vet ./...`、`go test -race ./...`、构建及本模块覆盖率≥65%,留脱敏事实;不把本地 Mock 当真实 SaaS/OSS 验收。 | + ## 5. 必测边界与阻塞项 1. **返回与缓存:**两接口 `200`/条件 `304`/错误、未知字段拒绝、任务归属错 D 拒绝、已有任务/线路版本不一致、摘要与实际加载不一致、授权过期、缓存第 59/60 秒、SaaS 断连、Dispatcher 重启、内存配置清理但恢复事实仍在;配置不完整绝不发新呼叫。 2. **消费与故障:**MQ 爆量/队列满发布端保留、D DB 满盘、ACK 丢失、旧命令重新投递、控制先到未入库执行、停/暂停与配置更新并发、执行中未知占用、不同任务落不同 D 的租户和运营商总额度;没有多 D 份额合同就不开放跨 D 测试。 3. **时间与安全边界:**任务/线路时段变化允许的约60秒延迟必须与“已接纳不变、未接纳可能旧版”区分;MQ stop/pause 和号码白名单不可延迟;配置过期则拒新准入,队列消息期限不可让旧任务次日自动拨。真实外呼只在获得明确安排、实际已通过相应门禁后才能尝试。 -4. **缺失权威:**页面截图没有现网 JSON 键,`config-read-v0.1.schema.json` 为项目自拟;SIP 线路完整管理信息、供应商鉴权/注册、AI UI 扩展和 `snapshot_sha256` 规范仍未获发布或外部签收;不能把 Mock 数据、旧 OpenAPI、计划或字段草案记为 SaaS/production 验收。 +4. **最终通话结果(下一轮):**通话结束但 OSS 尚未返回时不得发成功结果;成功后持久化原始资产事实再入 MQ,确认丢失重投同一事件身份;失败/过期按经签收的有限期限形成唯一显式结果。`outcome` 不因录音失败伪改通话结局;SaaS 收到唯一结果前不会获知文字/拒联,需有经批准的业务风险处置;控制与积压任务消息必须继续消费并拒绝/ACK,不能清空整个租户队列。 +5. **缺失权威:**页面截图没有现网 JSON 键,`config-read-v0.1.schema.json` 为项目自拟;SIP 线路完整管理信息、供应商鉴权/注册、AI UI 扩展和 `snapshot_sha256` 规范仍未获发布或外部签收;不能把 Mock 数据、旧 OpenAPI、计划或字段草案记为 SaaS/production 验收。 ## 6. 文档/写入边界 -新接口版本必须在本项目 `docs/` 与导入的版本化 `contracts/` 保持唯一来源;UI 截图盘点不改造成第二套 SaaS Schema,字段索引只读生成。当前严格旧合同只描述运行中的 MQ-only 路径,新合同获得批准前不偷加 HTTP client、旧 MQ 配置回退或新任务路由键。原 [plan-0918.md](archive/plan-0918.md) 已恢复 HEAD 字节内容归档,归档与本轮状态均不回写旧台账。`docs/README.md` 和 `AGENTS.md` 要始终指向本文件;有并行授权时明确一 lane 一工作区/唯一写集合,公共合同、台账和集成状态由集成负责人单写。不得提交、暂存或清理无关修改;大规模重构前另开分支。 +新接口及单份最终结果版本必须在本项目 `docs/` 与导入的版本化 `contracts/` 保持唯一来源;UI 截图盘点不改造成第二套 SaaS Schema,字段索引只读生成。当前严格旧合同只描述运行中的 MQ-only 路径,新合同获得批准前不偷加 HTTP client、旧 MQ 配置回退或新任务路由键。原 [plan-0918.md](archive/plan-0918.md) 已恢复 HEAD 字节内容归档,归档与本轮状态均不回写旧台账。`docs/README.md` 和 `AGENTS.md` 要始终指向本文件;有并行授权时明确一 lane 一工作区/唯一写集合,公共合同、台账和集成状态由集成负责人单写。不得提交、暂存或清理无关修改;大规模重构前另开分支。 ## 7. 当前责任与前置(I/M/G) -- **I = 契约来源/授权:**F00 仅盘点完成;F01 的 SaaS/management 源字段签收、HTTP 合同、MQ 控制/执行新协议及 SIP/AI 不确定项仍为 **blocked/pending**。没有 I,不开始 F02–F04 的真实配置接入,更不能借 Mock 名义发外呼。 +- **I = 契约来源/授权:**F00 仅盘点完成;F01 的 SaaS/management 源字段签收、HTTP 合同、MQ 控制/执行新协议及 SIP/AI 不确定项仍为 **blocked/pending**;F07 的单份结果合同、取消实时反馈的业务批准及录音失败有界收口也未签收。没有 I,不开始 F02–F04 的真实配置接入,更不能借 Mock 名义发外呼。 - **M = 本地 Mock/代码验证:**现有已归档计划中的 MQ/AI/静态 SIP 通过只说明旧版局部行为,不是本次新 HTTP/缓存/时段版本通过;按 F02–F04 重做。未获准真实 SaaS 时限于隔离 Mock 与本地合同测试。 - **G = 部署/切换前置:**目标 D/Agent 配置实际加载、broker 状态/队列、运行版本、SHA-256、systemd/Asterisk 状态/日志及非生产诊断必须留脱敏证据;生产安全屏障和资源变更另获批准。单 D 的 L 不授权 F05 多 D 或真实供应商验收。 @@ -62,11 +73,12 @@ | 项目 | 当前事实 | 下一门禁 | | --- | --- | --- | | 历史计划 | `docs/archive/plan-0918.md` 从 HEAD 原样保存;原 W00–W16 状态只作历史 | 不再向归档文件写新状态。 | -| F00 | 页面来源分级、两种成功/错误响应 Schema 及 Mock 示例已形成**项目自拟草案**;[第三方事件顺序及字段说明](thirds/第三方对接事件与请求消费顺序_v0.1.md)已整理现行 MQ 与目标 HTTP 的分界,均不能称作 SaaS 原 JSON 或签收 | 校验 Schema/正反例并取得 SaaS/management 字段来源与事件顺序签收。 | +| F00 | 页面来源分级、两种成功/错误响应 Schema 及 Mock 示例已形成**项目自拟草案**;[第三方事件顺序及字段说明](thirds/第三方对接事件与请求消费顺序_v0.1.md)已区分现行 MQ、拟定 HTTP 和下一版单份最终通话结果,均不能称作 SaaS 原 JSON 或签收 | 校验 Schema/正反例并取得 SaaS/management 字段来源与事件顺序签收。 | | F01 | 新 HTTP+业务 MQ 方向获用户同意,精确接口/返回/批准来源未签收 | 版本化合同、哈希和更新/接纳竞态语义冻结。 | | F02–F04 | 未实现;当前代码仍为旧 MQ-only/固定时段/单 D 静态交接 | I 先通过;TDD 测试先失败后实施,隔离链路验证。 | | F05 | 单任务单 D、资源独占方向确认,多 D 运行/配额份额未验收 | 后续另获阶段范围、资源及额度权威批准。 | | F06 | 新闭环未执行;旧本地测试不能代签 | 按 C/L/M 分层留证,不写假完成数。 | +| F07–F09(下一轮) | 用户确认对外去掉查询/补传、只保留最终通话结果及 OSS 路径的方向;[对接说明](thirds/第三方对接事件与请求消费顺序_v0.1.md)中的新事件仅为 review 草案,现行 MQ/代码未改。 | 先获 SaaS/业务签收 F07,再按 TDD 做 F08、隔离验收 F09;实时文字/拒联取消及录音失败/超时的唯一最终通知仍为阻塞项。 | ## 9. 协作和版本记录 diff --git a/docs/thirds/第三方对接事件与请求消费顺序_v0.1.md b/docs/thirds/第三方对接事件与请求消费顺序_v0.1.md index 55ca349..22a0ed5 100644 --- a/docs/thirds/第三方对接事件与请求消费顺序_v0.1.md +++ b/docs/thirds/第三方对接事件与请求消费顺序_v0.1.md @@ -1,26 +1,22 @@ -# SaaS ↔ Dispatcher:请求、返回与事件消费顺序 v0.1 +# SaaS ↔ Dispatcher:请求与通话结果消费顺序 v0.1 -本文只描述 **SaaS 与 Dispatcher(下称 D)之间的数据交互**。D 回传的通话、转写和录音结果均以 SaaS 收到的消息表示;内部执行及文件上传接口不在此范围。 +本文只描述 **SaaS 与 Dispatcher(D)**。配置 GET 是**待签收草案**;`call.execute`、`task.control`、`command.result` 的示例取自**现行 MQ v2 合同**;下文“唯一通话结果 `call.result`”是**下一版本提案**,现行 Schema 和程序**尚不支持**。不能把本文的新旧示例拼接成已经可运行的单一版本。示例为非生产数据,JSON 代码块均为完整请求或返回体;字段说明写在块外。 -**接口版本边界:**第 2 节两条只读 HTTP 配置接口是**待 SaaS 签收的项目草案,不是现网接口**,路径、身份信息的传递位置和 HTTP 错误状态码尚未确定;第 3–5 节的业务 MQ 格式是**现行 `2.0` 合同**。容量受限时不接纳、配置缓存约 60 秒及执行/控制分通道是目标行为,**现行部署不保证**。下方示例使用虚构身份,不包含生产凭据;JSON 块是完整消息/返回体,字段说明写在块外,以便复制校验。 +## 1. 触发顺序 -## 1. 通道与消费顺序 - -| 顺序 | 方向/触发 | 返回与消费动作 | +| 顺序 | 请求与触发 | SaaS 处理/返回 | | --- | --- | --- | -| 1 | D 启动或配置到期:读取本 D 的 SIP 全量配置(拟定 HTTP) | SaaS 返回获批快照;`200` 核验后使用,`304` 无体且授权仍有效才沿用;错误或过期停新准入。配置变更后由 D 自行确认生效。 | -| 2 | SaaS 将一项任务固定归属一个 D;SaaS 发布 `call.execute`(现行 MQ) | MQ publisher confirm 仅证明发布;D 根据租户/归属、期限、配置及配额决定是否接纳。 | -| 3 | D 查询归属任务及内嵌智能体(拟定 HTTP) | 有效 `200` 或满足授权的 `304` 形成候选配置;已接纳执行仍使用原快照。候选执行如何采用新版本须新合同冻结,不能自行修改原 MQ 命令。 | -| 4 | D 处理命令,回传 `command.result`(现行 MQ) | SaaS 按命令 ID 识别接纳、等待、应用、拒绝或未知;D 持久化后才 ACK 入站,ACK/响应丢失不能触发重拨。容量受限时不接纳、其余仍留 MQ 为拟定行为。 | -| 5 | D 回传 `call.status`、`transcript.updated/failed`、`contact.opt_out`(现行 MQ) | SaaS 按事件 ID 去重、按同一聚合版本归并;拒联不得等待配置缓存到期。不同聚合的事件没有全局到达顺序。 | -| 6 | D 回传 `call.finished`,之后可能回传 `recording.uploaded`(现行 MQ) | 结束事件不证明录音已上传;SaaS 按 `call_id/recording_id/upload_id` 关联事实,重复通知不生成第二份资产。 | -| 按需 | SaaS 发布 `task.control`、查询或补传命令(现行 MQ) | 与上述异步步骤并行,控制须及时处理;查询返回快照,补传不新建呼叫。执行/控制分队列尚待签收,不擅自改变现行路由。 | +| 1 | D 启动或配置到期,按 D 身份读取 SIP 全量(拟定 HTTP GET)。 | SaaS 返回本 D 唯一获批版本;D 核验后才能接受新执行。 | +| 2 | SaaS 将任务固定分配给一个 D,向该 D 投递 `call.execute`(现行 MQ 请求)。 | D 按消息中的租户原值和任务 ID 读取含智能体的任务配置(拟定 HTTP GET);未接纳任务可受约 60 秒缓存延迟影响,已接纳执行固定原快照。 | +| 3 | D 校验并持久处理这条呼叫命令。 | D 回传 `command.result` 作为接纳或拒绝的命令回执,不代表呼叫完成。 | +| 按需 | SaaS 投递 `task.control` 暂停、恢复或停止(现行 MQ 请求)。 | D 回传 `command.result`;停止不清空整个租户队列,属于已停止任务的积压命令逐条拒绝并 ACK。当前共享队列的及时控制屏障仍待下一轮验收。 | +| 4 | 通话终结且录音已上传 OSS,D 投递一条 `call.result`(**拟定 MQ 最终事件**)。 | SaaS 只处理这条最终的通话详情,按 `event_id` 去重;录音以 `bucket/object_key` 关联,不接收文件、不提供上传会话或验证结果。上传失败/超时的最终收口见 §4.2,时限尚未签收。 | -**MQ 路由(现行):**SaaS→D 发到 `agent-call.dispatchers.v2`,key `d..t..in`,D 从 `agent-call.d..t..v2` 消费;D→SaaS 发到 `agent-call.saas.v2`,key `d..t..out`,SaaS 从 `agent-call.saas.events.v2` 消费。`dispatcher_id` 为唯一 UUID v4;`tenant_key` 保留原值(≤196 UTF-8 字节,不能含独立 `*`/`#` 路由段)。消息 persistent、发布 mandatory 并等待 confirm;**confirm 不等于对端已消费或已执行**。同一任务只投归属 D,不能忙时自动换 D。 +**现行 MQ 路由(新版本变更前不动):**SaaS→D `agent-call.dispatchers.v2`,key `d..t..in`,归属 D 的 `agent-call.d..t..v2` 消费;D→SaaS `agent-call.saas.v2`,key `d..t..out`,SaaS 从 `agent-call.saas.events.v2` 消费。`dispatcher_id` 为唯一 UUID v4,`tenant_key` 保留原值;仅 publisher confirm **不等于** SaaS 已处理。下一版本是否复用拓扑由合同签收,不在本文中假设已上线。 ## 2. D ← SaaS:只读配置响应(拟定,非现网) -两接口均为 GET、**无请求 JSON 体**。D 使用自身 UUID 与 SECRETKEY,SIP 读本 D 全量,任务读原值 `tenant_key` + `task_id` 对应的单任务;实际 URL、请求头/参数、密钥承载方式待 SaaS 签收,本文**不虚构 HTTP 报文**。条件读取拟使用 `ETag/If-None-Match`,有效缓存约 60 秒;过期/请求失败只停新执行准入,既有执行保持已绑定快照,不妨碍 MQ 控制/查询。 +两接口均为 GET、**无请求 JSON 体**。D 使用自身 UUID 与 SECRETKEY,SIP 读本 D 全量,任务读原值 `tenant_key` + `task_id` 对应的单任务;实际 URL、请求头/参数、密钥承载方式待 SaaS 签收,本文**不虚构 HTTP 报文**。条件读取拟使用 `ETag/If-None-Match`,有效缓存约 60 秒;过期/请求失败只停新执行准入,既有执行保持已绑定快照,不妨碍 MQ 控制命令。 ### 2.1 SIP 配置:200,返回本 D 的完整获批快照 @@ -390,11 +386,11 @@ HTTP/1.1 304 Not Modified **字段说明/消费动作:**`schema_version/resource` 标识草案错误对象;`error.code` 是机器可读错误代码(示例 `not_assigned` 表示该任务不归此 D),`error.message` 是可读说明,不含密钥。D 不得将失败当作空任务/无限制或使用过期配置接新呼叫;不能自动回退至 MQ 配置通道。 -## 3. SaaS → D:MQ 命令和查询(现行合同) +## 3. SaaS → D:业务命令(现行 MQ 格式;执行语义以新合同为准) -所有 JSON 块都是**完整 MQ 消息**。命令公共字段:`schema_version=2.0`;`dispatcher_id` 是目标 D;`tenant_id/tenant_key` 是租户 ID 与保留原值的队列路由键;`trace_id` 贯穿请求与反馈;`issued_at/not_after` 为下发与截止时间;`command_id` 为稳定去重键,`command_type` 选定 payload 结构。到期不能等待次日再拨。D 持久化入站事实后才 ACK,重投不能新增执行。 +下列 JSON 是**完整 MQ 请求**。`schema_version` 指现行消息版本 `2.0`;`command_id` 是同一命令的稳定幂等身份,`command_type` 是命令类别;`dispatcher_id/tenant_id/tenant_key` 确定目标和租户;`trace_id` 关联结果;`issued_at/not_after` 限定时效;`payload` 是对应业务参数。重投同一命令不能创建第二次执行。新版去除对外查询和补传**命令**,不等于允许吞掉 MQ 重投或丢失本地恢复事实。 -### 3.1 发起呼叫:call.execute +### 3.1 发起外呼:call.execute ```json { @@ -403,10 +399,10 @@ HTTP/1.1 304 Not Modified "tenant_id": "tenant-a", "tenant_key": "tenant-a", "trace_id": "trace-v2", - "issued_at": "2026-09-21T00:00:00Z", + "issued_at": "2026-09-18T10:00:00+08:00", "command_id": "command-a", "command_type": "call.execute", - "not_after": "2026-09-21T00:00:30Z", + "not_after": "2026-09-18T10:00:30+08:00", "payload": { "execution_id": "execution-a", "task_id": "task-a", @@ -423,7 +419,7 @@ HTTP/1.1 304 Not Modified } ``` -**字段说明/消费动作:**`execution_id` 是本次执行身份,`task_id/task_item_id` 定位任务号码项,`task_revision` 是下发时版本;`callee` 是**原始号码**、不带线路前缀;`route_policy_id/caller_profile_id/agent_version_id` 是路由、主叫和智能体版本引用;`variables` 为获准提示词变量对象;`ring_timeout_ms/max_call_duration_ms` 是振铃与通话时限。D 校验版本、有效期及准入后异步回传 `command.result`,publisher confirm 不是接纳回执。真实外呼仍须满足独立授权和当前窗口。 +**字段说明/消费动作:**`execution_id` 唯一标识本次执行,区别于任务项 `task_item_id` 与消息身份 `command_id`;`task_revision/agent_version_id` 固定下发版本;`callee` 保留原始号码;`route_policy_id/caller_profile_id` 引用路由/主叫配置,不是线路 ID 或主叫号码。`variables` 只提供提示词允许的变量,不是任意扩展字段;`ring_timeout_ms/max_call_duration_ms` 为每次下发的振铃和通话上限(示例值只用于示例)。**下一轮须将超时的业务来源统一到任务配置并冻结其与命令快照的关系,不能让二者相互覆盖。**当 D 尚未接纳时执行停止/暂停屏障;不拨号的命令拒绝也须给出回执。 ### 3.2 暂停任务:task.control / pause @@ -448,7 +444,7 @@ HTTP/1.1 304 Not Modified } ``` -**字段说明/消费动作:**`task_id` 是目标任务;`action=pause` 暂停新执行;`expected_task_revision` 为任务修订的比较条件;`active_call_policy=drain` 保持在途通话自然结束;`reason` 是控制理由。结果须看 `command.result` 中实际应用版本,不以 MQ 发布成功为准。 +**字段说明/消费动作:**`task_id` 定位任务;`action=pause` 暂停**接纳**新呼叫;`expected_task_revision` 为比较条件;`active_call_policy=drain` 允许已在途通话自然结束;`reason` 为控制理由。暂停后是否恢复必须有新授权,不把未接纳旧命令留到 resume 时自动拨出。 ### 3.3 恢复任务:task.control / resume @@ -472,7 +468,7 @@ HTTP/1.1 304 Not Modified } ``` -**字段说明/消费动作:**`resume` 针对可恢复的 paused 任务;须提供当期 `expected_task_revision`,且新配置/授权仍有效。`stopped` 不可按普通 resume 恢复。 +**字段说明/消费动作:**只允许暂停任务按新授权恢复;`expected_task_revision` 须为当前实际版。已停止的任务不能用 resume 恢复。 ### 3.4 停止任务:task.control / stop @@ -497,389 +493,12 @@ HTTP/1.1 304 Not Modified } ``` -**字段说明/消费动作:**`stop` 禁止再接新执行;`active_call_policy=hangup` 表示需结束在途通话,另一策略 `drain` 表示只排空;这两种处理方式不能混用。对尚未接纳、仍在队列中的呼叫也须防止后续误拨;现行共享队列尚未完成该目标屏障。 +**字段说明/消费动作:**`stop` 终止该任务的新呼叫准入;`active_call_policy=hangup` 结束已在途通话,若选择 `drain` 则等待自然结束。停止命令**不是清空 RabbitMQ 队列**:D 仍消费该租户队列,并对属于已停止任务的积压呼叫逐条产生拒绝回执、ACK,不影响同队列其他任务。现行共享队列尚不保证控制能超越积压执行消息,这属于下一轮门禁。 -### 3.5 补传已有呼叫事件:call.replay +### 3.5 D → SaaS:命令处理回执 `command.result`(保留,不是通话事件) ```json -{ - "schema_version": "2.0", - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", - "tenant_id": "tenant-a", - "tenant_key": "tenant-a", - "trace_id": "trace-v2", - "issued_at": "2026-09-21T00:00:00Z", - "command_id": "call.replay-a", - "command_type": "call.replay", - "not_after": "2026-09-21T00:00:30Z", - "payload": { - "call_id": "call-a", - "reason": "local-test" - } -} -``` -**字段说明/消费动作:**`call_id` 指已有呼叫,`reason` 为补传原因;只补发已有事实,不新建任务/执行、不再次拨号。 - -### 3.6 补传已有命令结果:command.replay - -```json -{ - "schema_version": "2.0", - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", - "tenant_id": "tenant-a", - "tenant_key": "tenant-a", - "trace_id": "trace-v2", - "issued_at": "2026-09-21T00:00:00Z", - "command_id": "command.replay-a", - "command_type": "command.replay", - "not_after": "2026-09-21T00:00:30Z", - "payload": { - "source_command_id": "command-a", - "reason": "local-test" - } -} -``` - -**字段说明/消费动作:**`source_command_id` 指原命令,`reason` 为补传原因;按原命令身份补结果,不产生第二次外呼。 - -查询消息同样走 MQ,`message_type` 选 `command.query` 或 `call.query`;`message_id` 是本次查询标识,回复 `correlation_id` 指向它。查询请求还有同命令的 `schema_version/dispatcher_id/tenant_id/tenant_key/trace_id/issued_at/not_after`;不是 HTTP 业务查询。 - -### 3.7 查询命令:command.query 请求 - -```json -{ - "schema_version": "2.0", - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", - "tenant_id": "tenant-a", - "tenant_key": "tenant-a", - "trace_id": "trace-v2", - "issued_at": "2026-09-21T00:00:00Z", - "message_id": "command.query-a", - "message_type": "command.query", - "not_after": "2026-09-21T00:00:30Z", - "payload": { - "command_id": "command-a" - } -} -``` - -**字段说明/消费动作:**`payload.command_id` 是需要查证的源命令 ID。D 返回 4.1/4.3/4.4 中的一种结果,不发起新执行。 - -### 3.8 查询呼叫:call.query 请求 - -```json -{ - "schema_version": "2.0", - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", - "tenant_id": "tenant-a", - "tenant_key": "tenant-a", - "trace_id": "trace-v2", - "issued_at": "2026-09-21T00:00:00Z", - "message_id": "call.query-a", - "message_type": "call.query", - "not_after": "2026-09-21T00:00:30Z", - "payload": { - "call_id": "call-a" - } -} -``` - -**字段说明/消费动作:**`payload.call_id` 是已有呼叫 ID。D 返回 4.2/4.5/4.6 中的一种结果,查询不能触发重拨。 - -## 4. D → SaaS:MQ 查询结果(现行合同) - -响应公共字段:`schema_version/message_type/message_id` 是合同版本、回复类型及本次回复 ID;`dispatcher_id/tenant_id/tenant_key/trace_id` 维持身份范围与链路;`issued_at` 为回复时间;`correlation_id` **必须等于原查询的 `message_id`**;`status/reason_code` 决定结果。`ok` 的 `payload` 为该查询的完整视图,`pending` 的 payload 为空对象,`rejected` 的 payload 为拒绝详情。收到后按 `correlation_id` 匹配请求,不能把快照当成新的执行命令。 - -### 4.1 命令查询成功:command.query.result / ok - -```json -{ - "schema_version": "2.0", - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", - "tenant_id": "tenant-a", - "tenant_key": "tenant-a", - "trace_id": "trace-v2", - "issued_at": "2026-09-21T00:00:00Z", - "message_id": "query-reply-a", - "message_type": "command.query.result", - "correlation_id": "command.query-a", - "status": "ok", - "reason_code": "ok", - "payload": { - "command_id": "command-a", - "command_type": "call.execute", - "tenant_id": "tenant-a", - "tenant_key": "tenant-a", - "status": "accepted", - "aggregate_version": 1 - } -} -``` - -**字段说明/消费动作:**`payload.command_id/command_type` 是原命令;`tenant_id/tenant_key` 是租户范围;`status` 是命令状态;`aggregate_version` 是该命令的视图版本。可选 `task_id/execution_id/call_id` 表示关联对象;`reason_code/wait_reason_code` 是处理/等待原因;`requested_task_revision/applied_task_revision/task_state` 是控制期望版、实际应用版和任务状态;`accepted_at/waiting_since/admission_deadline/updated_at` 是各阶段时间。只展示本次已经存在的事实。 - -### 4.2 呼叫查询成功:call.query.result / ok(含反馈视图) - -```json -{ - "schema_version": "2.0", - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", - "tenant_id": "tenant-a", - "tenant_key": "tenant-a", - "trace_id": "trace-v2", - "issued_at": "2026-09-21T00:00:00Z", - "message_id": "call-query-reply-a", - "message_type": "call.query.result", - "correlation_id": "call.query-a", - "status": "ok", - "reason_code": "ok", - "payload": { - "call_id": "call-a", - "execution_id": "execution-1", - "call_state": "answered", - "call_version": 1, - "attempts": [ - { - "schema_version": "2.0", - "event_id": "call.status-event-a", - "event_type": "call.status", - "tenant_id": "tenant-a", - "tenant_key": "tenant-a", - "trace_id": "trace-1", - "occurred_at": "2026-09-18T00:00:01Z", - "aggregate_type": "call", - "aggregate_id": "call-a", - "aggregate_version": 1, - "payload": { - "call_id": "call-a", - "execution_id": "execution-1", - "task_id": "task-1", - "task_item_id": "item-1", - "call_state": "answered", - "call_version": 1, - "attempt_id": "attempt-1", - "attempt_state": "active", - "route_policy_id": "route-1", - "caller_profile_id": "caller-1", - "trunk_id": "trunk-1", - "cell_id": "cell-1", - "egress_pool_id": "egress-1", - "observed_at": "2026-09-18T00:00:01Z" - }, - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6" - } - ], - "transcript": { - "events": [ - { - "schema_version": "2.0", - "event_id": "transcript.updated-event-a", - "event_type": "transcript.updated", - "tenant_id": "tenant-a", - "tenant_key": "tenant-a", - "trace_id": "trace-1", - "occurred_at": "2026-09-18T00:00:01Z", - "aggregate_type": "transcript_segment", - "aggregate_id": "segment-1", - "aggregate_version": 1, - "payload": { - "call_id": "call-a", - "turn_id": "turn-1", - "segment_id": "segment-1", - "role": "customer", - "revision": 1, - "text": "您好", - "is_final": true, - "start_ms": 0, - "end_ms": 600, - "playback_state": "not_applicable" - }, - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6" - }, - { - "schema_version": "2.0", - "event_id": "transcript.failed-event-a", - "event_type": "transcript.failed", - "tenant_id": "tenant-a", - "tenant_key": "tenant-a", - "trace_id": "trace-1", - "occurred_at": "2026-09-18T00:00:05Z", - "aggregate_type": "transcript", - "aggregate_id": "call-1", - "aggregate_version": 1, - "payload": { - "call_id": "call-a", - "reason_code": "asr_timeout", - "retryable": false, - "segment_id": "segment-1", - "affected_segments": [ - "segment-1" - ] - }, - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6" - } - ] - }, - "recordings": [ - { - "schema_version": "2.0", - "event_id": "recording.uploaded-event-a", - "event_type": "recording.uploaded", - "tenant_id": "tenant-a", - "tenant_key": "tenant-a", - "trace_id": "trace-1", - "occurred_at": "2026-09-18T00:00:02Z", - "aggregate_type": "recording", - "aggregate_id": "recording-1", - "aggregate_version": 1, - "payload": { - "call_id": "call-a", - "recording_id": "recording-1", - "format": "wav", - "channels": 1, - "sample_rate_hz": 16000, - "duration_ms": 1000, - "size_bytes": 32000, - "checksum_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "upload_id": "upload-a", - "bucket": "example-bucket", - "object_key": "recordings/recording-a.wav" - }, - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6" - }, - { - "schema_version": "2.0", - "event_id": "recording.failed-event-a", - "event_type": "recording.failed", - "tenant_id": "tenant-a", - "tenant_key": "tenant-a", - "trace_id": "trace-1", - "occurred_at": "2026-09-18T00:00:04Z", - "aggregate_type": "recording", - "aggregate_id": "recording-1", - "aggregate_version": 1, - "payload": { - "call_id": "call-a", - "recording_id": "recording-1", - "stage": "upload", - "reason_code": "temporary_oss_unavailable", - "retryable": true, - "next_retry_at": "2026-09-18T00:01:00Z" - }, - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6" - } - ], - "delivery": { - "pending": 1, - "retry": 0, - "dispatching": 0, - "published": 0 - }, - "snapshot_at": "2026-09-21T00:00:00Z" - } -} -``` - -**字段说明/消费动作:**`payload.call_id/execution_id/call_state/call_version` 表示呼叫身份及状态版本;`attempts[]` 为完整 `call.status` 事件信封;`transcript.events[]` 为完整 `transcript.updated/failed` 事件信封;`recordings[]` 为完整 `recording.uploaded/failed` 事件信封;每个事件的字段解释见 §5。`delivery.pending/retry/dispatching/published` 为消息投递计数,**不是 SaaS 应用收讫**;`snapshot_at` 为视图生成时间。可选 `task_id/task_item_id/reason_code/outcome/started_at/ended_at/duration_ms` 为业务关联和结局。示例包含不同时间的反馈,列表只代表查询时已知事实,不表示事件在真实链路中依次抵达。 - -### 4.3 命令查询待定:command.query.result / pending - -```json -{ - "schema_version": "2.0", - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", - "tenant_id": "tenant-a", - "tenant_key": "tenant-a", - "trace_id": "trace-v2", - "issued_at": "2026-09-21T00:00:00Z", - "message_id": "query-reply-a", - "message_type": "command.query.result", - "correlation_id": "command.query-a", - "status": "pending", - "reason_code": "waiting", - "payload": {} -} -``` - -**字段说明/消费动作:**`status=pending`、`reason_code=waiting`、`payload={}` 表示当下尚无最终结果;SaaS 等待或再次查询,不能据此产生第二次发起命令。 - -### 4.4 命令查询被拒:command.query.result / rejected - -```json -{ - "schema_version": "2.0", - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", - "tenant_id": "tenant-a", - "tenant_key": "tenant-a", - "trace_id": "trace-v2", - "issued_at": "2026-09-21T00:00:00Z", - "message_id": "query-reply-a", - "message_type": "command.query.result", - "correlation_id": "command.query-a", - "status": "rejected", - "reason_code": "not_found", - "payload": { - "detail": "command not found", - "retryable": false - } -} -``` - -**字段说明/消费动作:**`reason_code=not_found` 给出拒绝原因;`payload.detail` 是可读说明,`retryable` 是是否可再次查询,不是重拨许可。其他原因必须使用合同允许的代码。 - -### 4.5 呼叫查询待定:call.query.result / pending - -```json -{ - "schema_version": "2.0", - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", - "tenant_id": "tenant-a", - "tenant_key": "tenant-a", - "trace_id": "trace-v2", - "issued_at": "2026-09-21T00:00:00Z", - "message_id": "call-query-reply-pending", - "message_type": "call.query.result", - "correlation_id": "call.query-a", - "status": "pending", - "reason_code": "waiting", - "payload": {} -} -``` - -**字段说明/消费动作:**与 4.3 同结构,但 `message_type` 和 `correlation_id` 属于呼叫查询;不把 `pending` 解释为呼叫不存在。 - -### 4.6 呼叫查询被拒:call.query.result / rejected - -```json -{ - "schema_version": "2.0", - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", - "tenant_id": "tenant-a", - "tenant_key": "tenant-a", - "trace_id": "trace-v2", - "issued_at": "2026-09-21T00:00:00Z", - "message_id": "call-query-reply-rejected", - "message_type": "call.query.result", - "correlation_id": "call.query-a", - "status": "rejected", - "reason_code": "not_found", - "payload": { - "detail": "call not found", - "retryable": false - } -} -``` - -**字段说明/消费动作:**与 4.4 同结构,`payload.detail/retryable` 是拒绝描述/再查询提示;不自动创建新的呼叫。 - -## 5. D → SaaS:业务事件(现行合同) - -下列每个 JSON 块都是**完整事件信封**,不只是 `payload`:`schema_version` 是 MQ 合同版本;`event_id` 是重复投递的去重键;`event_type` 选择业务正文;`dispatcher_id/tenant_id/tenant_key/trace_id` 给出来源和租户范围;`occurred_at` 是事实时间;`aggregate_type/aggregate_id/aggregate_version` 是同一对象的类型、ID、递增版本;`payload` 是具体业务数据。SaaS 持久消费并在**自身处理完成后** ACK;不同聚合没有全局顺序。同一身份重发要幂等处理。 - -### 5.1 命令已接纳:command.result / accepted - -```json { "schema_version": "2.0", "event_id": "command.result-event-a", @@ -902,292 +521,133 @@ HTTP/1.1 304 Not Modified } ``` -**字段说明/消费动作:**`payload.command_id/command_type` 对应源命令;`status` 可为 `accepted/waiting/applied/rejected/failed/unknown`,本例 `accepted`;`reason_code` 为处置原因;`execution_id` 为可选关联执行。控制事件还可能带 `task_id/task_item_id/call_id/requested_task_revision/applied_task_revision/admission_state/resource_reservation_id/permit_id`;判断实际控制结果要核对修订,不靠发布确认。 +**字段说明/消费动作:**信封 `event_id/event_type/dispatcher_id/tenant_id/tenant_key/trace_id/occurred_at/aggregate_*` 是消息身份、来源、时间和版本;`payload.command_id/command_type` 指源命令,`status` 可表示 `accepted/waiting/applied/rejected/failed/unknown`,`reason_code` 是处置原因;可选 `requested_task_revision/applied_task_revision/task_state` 用于确认控制实际生效版本。该回执不是本次通话详情,不替代 §4 的唯一通话结果。停止/暂停后的等待消息不得发起外呼;对重复命令按原身份去重。 -### 5.2 命令暂待:command.result / waiting +## 4. D → SaaS:唯一通话结果(**拟定新 MQ 合同,尚无已发布 Schema**) + +同一次通话只发布一种业务反馈 `call.result`:通话状态、最终转写、拒联结果、录音资产一次返回;不再将通话进度、实时文字、拒联、通话结束、录音成功/失败各自发布对外事件。**这会改变现有“实时文字/即时拒联”的产品要求,必须在新版合同与验收中明确批准;SaaS 在最终结果到达前不会获得这些反馈。**消息仍应可靠入队,断线后按同一事件身份重投;这不是对外“补传命令”。以下两个结构均为待签收提案,不能用现行 MQ/event Schema 校验,也不能作为已上线接口。 + +### 4.1 录音已上传 OSS:最终成功结果 ```json { - "schema_version": "2.0", - "event_id": "command.result-waiting-a", - "event_type": "command.result", + "schema_version": "call-result.v0.1-proposal", + "event_id": "call-result-001", + "event_type": "call.result", + "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", "tenant_id": "tenant-a", "tenant_key": "tenant-a", - "trace_id": "trace-1", - "occurred_at": "2026-09-18T00:00:00Z", - "aggregate_type": "command", - "aggregate_id": "command-1", - "aggregate_version": 1, - "payload": { - "command_id": "command-1", - "command_type": "call.execute", - "status": "waiting", - "reason_code": "waiting" - }, - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6" -} -``` - -**字段说明/消费动作:**候选命令尚未得到最终结果,SaaS 按原 `command_id` 等待或查询;本事件**不是**已接纳、更不是拨号证明。容量受限时不搬入本地待队列是目标消费语义,尚未实现。 - -### 5.3 命令被拒:command.result / rejected - -```json -{ - "schema_version": "2.0", - "event_id": "command.result-rejected-a", - "event_type": "command.result", - "tenant_id": "tenant-a", - "tenant_key": "tenant-a", - "trace_id": "trace-1", - "occurred_at": "2026-09-18T00:00:00Z", - "aggregate_type": "command", - "aggregate_id": "command-1", - "aggregate_version": 1, - "payload": { - "command_id": "command-1", - "command_type": "call.execute", - "status": "rejected", - "reason_code": "not_found" - }, - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6" -} -``` - -**字段说明/消费动作:**`rejected` 给出本次命令未被接受的原因;不要自动改投别的 D 或对未知状态盲目重发。`failed/unknown/applied` 的处理同样必须以实际 `status/reason_code` 为准。 - -### 5.4 通话进度:call.status - -```json -{ - "schema_version": "2.0", - "event_id": "call.status-event-a", - "event_type": "call.status", - "tenant_id": "tenant-a", - "tenant_key": "tenant-a", - "trace_id": "trace-1", - "occurred_at": "2026-09-18T00:00:01Z", + "trace_id": "trace-v2", + "occurred_at": "2026-09-18T10:10:15+08:00", "aggregate_type": "call", "aggregate_id": "call-a", "aggregate_version": 1, "payload": { + "source_command_id": "command-a", + "execution_id": "execution-a", "call_id": "call-a", - "execution_id": "execution-1", - "task_id": "task-1", - "task_item_id": "item-1", - "call_state": "answered", - "call_version": 1, - "attempt_id": "attempt-1", - "attempt_state": "active", - "route_policy_id": "route-1", - "caller_profile_id": "caller-1", - "trunk_id": "trunk-1", - "cell_id": "cell-1", - "egress_pool_id": "egress-1", - "observed_at": "2026-09-18T00:00:01Z" - }, - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6" -} -``` - -**字段说明/消费动作:**`call_id/execution_id` 是通话/执行;`call_state` 可为 `queued/dialing/ringing/answered/ended`;`call_version` 是通话版本;`attempt_id/attempt_state` 是本次尝试及其 `pending/active/ended/unknown` 状态。可选 `task_id/task_item_id/route_policy_id/caller_profile_id/trunk_id/cell_id/egress_pool_id/observed_at/reason_code` 为关联/线路/观察时间/原因。SaaS 以呼叫身份和版本更新进度,不因超时对接通或未知呼叫重拨。 - -### 5.5 实时转写:transcript.updated - -```json -{ - "schema_version": "2.0", - "event_id": "transcript.updated-event-a", - "event_type": "transcript.updated", - "tenant_id": "tenant-a", - "tenant_key": "tenant-a", - "trace_id": "trace-1", - "occurred_at": "2026-09-18T00:00:01Z", - "aggregate_type": "transcript_segment", - "aggregate_id": "segment-1", - "aggregate_version": 1, - "payload": { - "call_id": "call-a", - "turn_id": "turn-1", - "segment_id": "segment-1", - "role": "customer", - "revision": 1, - "text": "您好", - "is_final": true, - "start_ms": 0, - "end_ms": 600, - "playback_state": "not_applicable" - }, - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6" -} -``` - -**字段说明/消费动作:**`call_id/turn_id/segment_id` 定位呼叫/轮次/分段;`role` 标明说话方;`revision` 是该段修订,`text` 为文字,`is_final` 表示定稿;`start_ms/end_ms` 是段相对时间;`playback_state` 是播放状态。可选 `execution_id`。同段更高修订替换旧文本,不存在 `call.transcript` 别名。 - -### 5.6 转写失败:transcript.failed - -```json -{ - "schema_version": "2.0", - "event_id": "transcript.failed-event-a", - "event_type": "transcript.failed", - "tenant_id": "tenant-a", - "tenant_key": "tenant-a", - "trace_id": "trace-1", - "occurred_at": "2026-09-18T00:00:05Z", - "aggregate_type": "transcript", - "aggregate_id": "call-1", - "aggregate_version": 1, - "payload": { - "call_id": "call-a", - "reason_code": "asr_timeout", - "retryable": false, - "segment_id": "segment-1", - "affected_segments": [ - "segment-1" - ] - }, - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6" -} -``` - -**字段说明/消费动作:**`call_id/reason_code/retryable` 标明失败的呼叫、原因和可重试性;可选 `segment_id/affected_segments` 指定受影响片段。不能把失败当空文字成功。 - -### 5.7 拒绝联系:contact.opt_out - -```json -{ - "schema_version": "2.0", - "event_id": "contact.opt_out-event-a", - "event_type": "contact.opt_out", - "tenant_id": "tenant-a", - "tenant_key": "tenant-a", - "trace_id": "trace-1", - "occurred_at": "2026-09-18T00:00:06Z", - "aggregate_type": "contact", - "aggregate_id": "contact-1", - "aggregate_version": 1, - "payload": { - "call_id": "call-a", - "task_id": "task-1", - "task_item_id": "item-1", - "requested_at": "2026-09-18T00:00:06Z", - "turn_id": "turn-1", - "segment_id": "segment-1" - }, - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6" -} -``` - -**字段说明/消费动作:**`call_id/task_id/task_item_id/requested_at` 标明拒联的来源、任务项及时间;可选 `turn_id/segment_id` 定位对应对话。SaaS 应阻止不应再拨出的任务项,不能等待约 60 秒配置缓存到期。 - -### 5.8 通话结束:call.finished - -```json -{ - "schema_version": "2.0", - "event_id": "call.finished-event-a", - "event_type": "call.finished", - "tenant_id": "tenant-a", - "tenant_key": "tenant-a", - "trace_id": "trace-1", - "occurred_at": "2026-09-18T00:00:03Z", - "aggregate_type": "call", - "aggregate_id": "call-a", - "aggregate_version": 2, - "payload": { - "call_id": "call-a", - "execution_id": "execution-1", - "task_id": "task-1", - "task_item_id": "item-1", - "call_version": 1, + "task_id": "task-a", + "task_item_id": "item-a", + "task_revision": 1, + "agent_version_id": "version-a", + "route_policy_id": "route-a", + "caller_profile_id": "caller-a", + "callee": "15003164745", + "trunk_id": "trunk-a", + "started_at": "2026-09-18T10:00:00+08:00", + "ended_at": "2026-09-18T10:10:00+08:00", + "duration_ms": 600000, "outcome": "answered", - "started_at": "2026-09-18T00:00:00Z", - "ended_at": "2026-09-18T00:00:03Z", - "duration_ms": 3000, - "reason_code": "normal_clearing", - "asset_state": "complete", - "attempt_summary": [ + "reason_code": null, + "transcript": [ { - "attempt_id": "attempt-1", - "state": "ended", - "trunk_id": "trunk-1", - "cell_id": "cell-1" + "turn_id": "turn-1", + "segment_id": "segment-1", + "role": "user", + "text": "示例转写内容", + "start_ms": 1000, + "end_ms": 2500 } - ] - }, - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6" + ], + "opt_out": false, + "recording": { + "status": "uploaded", + "recording_id": "recording-a", + "upload_id": "upload-a", + "bucket": "example-bucket", + "object_key": "calls/tenant-a/call-a.wav", + "format": "wav", + "channels": 1, + "sample_rate_hz": 8000, + "duration_ms": 600000, + "size_bytes": 9600000, + "checksum_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + } } ``` -**字段说明/消费动作:**`call_id/execution_id/call_version` 标识结束的执行和呼叫版本;`outcome` 是接通/无应答/忙/失败/拒联/取消/未知之一;`started_at/ended_at/duration_ms/reason_code` 是开始、结束、时长和结束原因;可选 `task_id/task_item_id`、`attempt_summary[]`(各尝试身份、状态、线路和执行单元)、`asset_state`(`pending/complete/failed/unknown` 的当时资产状态)。仅此事件不保证录音通知已经送达。 +**字段说明/消费动作:**`schema_version/event_type` 是**待签收的新版本及单一通话结果类型**,现行 Schema 不认此值;`event_id` 是固定的事件身份,重复入队须相同;`dispatcher_id/tenant_id/tenant_key/trace_id` 限定来源和归属;`aggregate_type/aggregate_id/aggregate_version/occurred_at` 为呼叫聚合、版本和完成时间。 +`payload.source_command_id/execution_id/call_id/task_id/task_item_id/task_revision/agent_version_id` 绑定原命令、执行、呼叫及固定任务/智能体版本;`route_policy_id/caller_profile_id/trunk_id/callee` 为路由策略、主叫配置、实际线路及原始被叫;`started_at/ended_at/duration_ms/outcome/reason_code` 给出起止、时长、结果和可空原因。`transcript[]` 中 `turn_id/segment_id/role/text/start_ms/end_ms` 是最终转写片段及时间(完整用户文本是否允许外传须由本阶段业务合同确定);`opt_out` 表示通话中的拒联事实,只在最终消息里可见。`recording.status/recording_id/upload_id/bucket/object_key/format/channels/sample_rate_hz/duration_ms/size_bytes/checksum_sha256` 描述已成功上传的资产,不包含文件、TOKEN 或签名 URL。SaaS 使用 `call_id` 关联、`event_id` 去重并按固定 `upload_id` 避免重复资产。 -### 5.9 录音可用事实:recording.uploaded +### 4.2 录音上传未完成:最终异常结果(是否启用及截止时间待签收) ```json { - "schema_version": "2.0", - "event_id": "recording.uploaded-event-a", - "event_type": "recording.uploaded", + "schema_version": "call-result.v0.1-proposal", + "event_id": "call-result-002", + "event_type": "call.result", + "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", "tenant_id": "tenant-a", "tenant_key": "tenant-a", - "trace_id": "trace-1", - "occurred_at": "2026-09-18T00:00:02Z", - "aggregate_type": "recording", - "aggregate_id": "recording-1", + "trace_id": "trace-v2", + "occurred_at": "2026-09-18T10:10:15+08:00", + "aggregate_type": "call", + "aggregate_id": "call-b", "aggregate_version": 1, "payload": { - "call_id": "call-a", - "recording_id": "recording-1", - "format": "wav", - "channels": 1, - "sample_rate_hz": 16000, - "duration_ms": 1000, - "size_bytes": 32000, - "checksum_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "upload_id": "upload-a", - "bucket": "example-bucket", - "object_key": "recordings/recording-a.wav" - }, - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6" + "source_command_id": "execute-b", + "execution_id": "execution-b", + "call_id": "call-b", + "task_id": "task-a", + "task_item_id": "item-b", + "task_revision": 1, + "agent_version_id": "version-a", + "route_policy_id": "route-a", + "caller_profile_id": "caller-a", + "callee": "15003164745", + "trunk_id": "trunk-a", + "started_at": "2026-09-18T10:00:00+08:00", + "ended_at": "2026-09-18T10:10:00+08:00", + "duration_ms": 600000, + "outcome": "answered", + "reason_code": null, + "transcript": [], + "opt_out": false, + "recording": { + "status": "unavailable", + "error_code": "upload_timeout", + "recording_id": "recording-b", + "upload_id": "upload-b", + "bucket": null, + "object_key": null, + "format": "wav", + "channels": 1, + "sample_rate_hz": 8000, + "duration_ms": 600000, + "size_bytes": null, + "checksum_sha256": null + } + } } ``` -**字段说明/消费动作:**`call_id/recording_id/upload_id` 为关联呼叫、录音及上传事实身份;`bucket/object_key` 为资产位置;`format` 可为 `wav/raw_pcm/pcma`;`channels=1/sample_rate_hz/duration_ms/size_bytes` 为声道、采样率、时长及字节数;`checksum_sha256` 是文件内容摘要。SaaS 按事实身份去重并自行后处理;D 不提供完整音频、签名 URL、TOKEN 或已验证 OSS ID。D 的 MQ 入队确认不代表 SaaS 已处理。 +**字段说明/消费动作:**这是**防止录音永远未上传时通话结果永久消失的待定方案**:经合同规定的有限截止时间或确知不可恢复后,`recording.status=unavailable` 且 `bucket/object_key/size_bytes/checksum_sha256=null`,`recording.error_code` 表示未得到录音资产,呼叫自身的 `reason_code` 仍为 null;不能谎称上传成功,也不能默默丢弃最终结果。`outcome` 必须反映**通话本身**而非上传成败;若通话已接通/正常结束,不得仅因录音失败就把 `outcome` 改成 `failed`。具体结果字段、期限、未上传时是否仍发一次最终结果待 SaaS 签收;未签收前不能实施或用无限等待代替错误处理。 -### 5.10 录音失败:recording.failed(合同中存在,不能假定必发) +## 5. 下一版本签收前不得误用 -```json -{ - "schema_version": "2.0", - "event_id": "recording.failed-event-a", - "event_type": "recording.failed", - "tenant_id": "tenant-a", - "tenant_key": "tenant-a", - "trace_id": "trace-1", - "occurred_at": "2026-09-18T00:00:04Z", - "aggregate_type": "recording", - "aggregate_id": "recording-1", - "aggregate_version": 1, - "payload": { - "call_id": "call-a", - "recording_id": "recording-1", - "stage": "upload", - "reason_code": "temporary_oss_unavailable", - "retryable": true, - "next_retry_at": "2026-09-18T00:01:00Z" - }, - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6" -} -``` - -**字段说明/消费动作:**`call_id/recording_id` 定位资产;`stage/reason_code/retryable` 表示失败阶段/原因/是否可恢复;可选 `next_retry_at` 为后续处理时间。现行严格 Schema 仍列有旧 `complete/verify` 等阶段,不能据此要求 SaaS 提供上传会话或 verified 接口;是否实际发布此事件需新版合同确认。 - -## 6. 对接尚需确认的部分 - -1. **拟定 HTTP 配置:**两条真实路径、UUID+SECRETKEY 的实际承载、GET 条件读取和 ETag、`200/304` 与错误状态、完整字段、审批来源/摘要及同时修改时的冻结点。本文的英文配置键是项目提案,不是 SaaS 现网字段;确认前不能直接上线。 -2. **版本关联:**`call.execute` 带既有 `task_revision/agent_version_id/route_policy_id`;未接纳执行如何在约 60 秒缓存窗口使用新版配置,须明确合法的版本绑定,不可默改旧命令;已接纳始终用原快照。 -3. **容量和控制:**控制在执行队列拥塞时仍能到达、未接纳任务的 stop 屏障与新队列路由需要版本化 MQ 合同。现行 `.in` 路由不代表目标分队列已建成。 -4. **录音通知:**SaaS 接收 `recording.uploaded` 并自行确认处理;不申请上传会话,不向 D 下发 OSS TOKEN,也不把 publisher confirm 视为业务收讫。 - -现行 MQ 字段以 [`mq.schema.json`](../../contracts/upstream/v1/mq.schema.json)、[`event-payloads.schema.json`](../../contracts/upstream/v1/event-payloads.schema.json)、[`mq-topology.json`](../../contracts/upstream/v1/mq-topology.json) 为准;拟定 HTTP 字段以[配置读取草案](../contracts/config-read-v0.1.schema.json)为待确认结构。 +- 本文 §3 的旧 MQ 命令与 §4 的新通话结果**不能直接混合上线**;下一轮先由 SaaS 与本项目共同发布严格新版 Schema、正反例、哈希和幂等/队列拓扑,再实施生产者与消费者。 +- 取消对外查询与补传命令不取消 D 的持久化恢复、同一身份重投、故障对账和**未知是否已拨号时绝不重拨**。没有核实状态的内部恢复能力不得发布新版本。 +- 只在录音上传 OSS 后发布成功通话结果;若录音不能上传,有限等待、可观测故障及最终一次通知的合同必须先签收。通话已完成却无限等待不属于可验收方案。 +- SaaS 不再实时得知拒联及转写,会影响跨任务、跨 D 停呼与实时展示。新目标与既有产品要求冲突,须取得业务签收并修订原有验收,不能凭本文视为既有验收已通过。 +- 本文中的两条 HTTP 响应以[配置读取草案](../contracts/config-read-v0.1.schema.json)为项目提案;现行 MQ 命令/回执以[`mq.schema.json`](../../contracts/upstream/v1/mq.schema.json)、[`event-payloads.schema.json`](../../contracts/upstream/v1/event-payloads.schema.json)为准;新 `call.result` **尚无权威 Schema**,其字段和错误结构只供本轮 review。