diff --git a/AGENTS.md b/AGENTS.md index ca07678..1a83b91 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -166,7 +166,8 @@ - **现行已发布合同(新版本生效前必须遵守)**:SaaS↔Dispatcher的全部交互唯一经RabbitMQ专用Topic订阅,双方无HTTP请求/回调/兼容通道或故障回退,包括执行、控制、查询、整体补传、AI配置/授权及recording.uploaded上传事实通知;上传不申请SaaS会话或等待verified/OSS ID回复。OSS配置/TOKEN不来自SaaS:Agent领取及显式重申请TOKEN只经D↔A Unary;本规则不禁止Agent→OSS、ARI、AI供应商HTTP(S)或gRPC的HTTP/2。 - **用户已批准的新目标,尚非现行合同或运行能力**:仅任务(内含智能体)与 SIP 获批配置由 SaaS 向 D 提供两条只读 HTTP 查询;呼叫、控制、查询、整体补传、recording.uploaded及其它业务事件/结果仍**唯一走MQ**,不提供配置HTTP→MQ回退,也不恢复旧业务HTTP通道。SaaS须分发 management 已批准的唯一 SIP 版本,management 仍为唯一编辑/审批面。当前严格 MQ Schema、HTTP 路径、身份/版本/缓存契约及验收在 W01 新版本冻结并完成切换前不可擅改或声称已实现。 - 新HTTP配置字段阶段草案见 `docs/contracts/config-read-fields-v0.1-proposal.md` 及同目录 `config-read-v0.1.schema.json`/mock示例;截图只证实UI含义,英文响应键为项目自定义,绝非SaaS现网接口已确认字段。用户新增任务排除日期、线路时段等未见截图项按项目需求设计;SIP传输/鉴权/注册及额度未知不能猜默认值。草案校验不代表SaaS/management签收或W01接口就绪;真实响应、审批来源、摘要和Agent/Asterisk实际加载仍须验证。当前唯一权威运行契约不因草案变化。 -- **每个Dispatcher必须有独立、全局唯一且不重复的ID及独立接收Topic/队列**;指定D的任务/现行MQ配置结果/上传结果不能由其它D抢收,也不能广播后仅靠正文过滤;新目标只读HTTP配置由该D UUID+SECRETKEY获取且须核对任务归属。身份与tenant/Agent/Cell ID、dispatcher_epoch分开;保留租户独立队列及原值tenant_key,完整新路由长度预算须重验。具体ID生成/持久化、Topic/绑定、消息字段/关联/错误/期限须随W01新版本冻结,不凭本文给旧严格Schema添加字段。 +- **每个Dispatcher必须有独立、全局唯一且不重复的ID及独立接收Topic/队列**;指定D的任务/现行MQ配置结果/上传结果不能由其它D抢收,也不能广播后仅靠正文过滤;新目标只读HTTP配置由该D UUID+SECRETKEY获取且须核对任务归属。身份与tenant/Agent/Cell ID、dispatcher_epoch分开;现行合同保留租户独立队列及原值tenant_key,完整新路由长度预算须重验。具体ID生成/持久化、Topic/绑定、消息字段/关联/错误/期限须随W01新版本冻结,不凭本文给旧严格Schema添加字段。 +- **用户确认的下一轮任务队列所有权硬边界(未实施,见 `docs/plan-config-read-v0.1.md` §1/§4.1):任务队列及 RabbitMQ 绑定只能由 SaaS 创建、维护、退役;Dispatcher 只消费,不能自行声明/建队、绑定或删除任务队列。** SaaS 需先确保持久任务/控制队列及绑定就绪,再向按归属 D+任务 ID 定位的任务队列发布 persistent 命令;D 离线时已存在的任务队列可积压,缺队列而未入队的原消息由 SaaS 保留并在就绪后按同一身份重发,不能称作 D 重启自动补收。新增按 D 身份授权的 `tasks` 全量清单用于 D 重启恢复,运行中拟每 30 秒 `GET /tasks?after=` 拉取该 D 新增/更新/撤销,`cursor` 为 SaaS 变更水位而非最大任务 ID,失效则重新取一致全量快照;轮询间隔不等于端到端发现时限,也不能替代 MQ 即时控制;停止任务旧消息逐条拒绝/ACK,不能直接删队列。同一 D 下各任务共享原值 `tenant_key` 的租户总额度,跨 D 仍须权威份额;任务数与队列资源有上限。现行代码启动时按 `--tenant-key` 自行声明租户队列,与新目标不符,F07 严格合同、F08 实现及 F09 验收前不得声称动态租户/任务队列已可用。 - **OSS相关配置存于Dispatcher配置文件,Agent向Dispatcher领取临时上传TOKEN后直传OSS,不保存长期凭据;SaaS不再下发OSS配置/TOKEN。** D复用官方SDK提供受限TOKEN/目标信息,配置缺失/无效明确失败;不在样例、源码、日志或证据中保存实际密钥/完整TOKEN。过期只允许A显式向D重新申请,不自动续期或向SaaS申请TOKEN;精确配置格式/TOKEN形态/UploadGrant映射另行核验,不猜字段。 - **用户已修订目标:上传仅负责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。 diff --git a/docs/plan-config-read-v0.1.md b/docs/plan-config-read-v0.1.md index baf3ed8..05e97e5 100644 --- a/docs/plan-config-read-v0.1.md +++ b/docs/plan-config-read-v0.1.md @@ -6,6 +6,8 @@ 本次要建立两个**只读 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 和代码尚未变更,不能将新目标写成已上线。** +**任务队列所有权(用户明确指定,下一轮合同硬边界):任务队列及其 RabbitMQ 绑定只由 SaaS 创建和维护;Dispatcher 只消费,不声明、重建、删除或绑定任务队列。队列按归属 D + 稳定任务 ID 定位,租户并发另按原值 `tenant_key` 汇总,不因队列改按任务划分而拆成每任务独立额度。SaaS 必须先确认持久队列与绑定就绪,再向其发布任务消息;D 离线期间只要该队列已存在,持久消息继续积压,重启后恢复消费。若发布时队列未就绪,消息不能靠 D 重启自动补收,SaaS 必须保留原消息身份并在队列就绪后重发;禁止把未路由消息误记为已交付。此目标与现行 D 启动以 `--tenant-key` 自行声明租户队列的代码/Schema 冲突,**尚未实施、不得绕过新版合同声称已支持动态任务**。下一轮新增归属 D 的 `tasks` 发现接口:启动/重启取带一致游标的完整快照,运行中每 30 秒用 `GET /tasks?after=` 拉取增量,与现有两条单项配置接口分开签收,详情见 §4.1。`cursor` 是 SaaS 为本 D 任务变更分配的水位,**绝不是最大 `task_id`**;30 秒是轮询间隔提案,不等于从 SaaS 建队到 D 开始消费的端到端 30 秒保证,也不是现行服务承诺。 + 当前 **P1 仍为单 D/单 Agent/单 Asterisk/单 Cell/单租户**,现行已发布 SaaS↔D 全 MQ 契约、静态 SIP 制品、Asia/Shanghai `[09:00,20:00)` 门禁和旧排队 AI 版本绑定,在新版本逐项发布、替换及验证前继续执行;本计划和字段草案**不授权**新线路、跨窗真实拨号、SaaS 生产部署或多 D 业务运行。外部真实 SaaS、云资源、供应商联调及拨号须分别获授权。 **完成条件分层:** @@ -25,7 +27,7 @@ - **配置源:**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、号码白名单或最后发起许可。新目标不再向 SaaS 实时发送 opt-out/转写:在最终结果到达前,SaaS 无法按该事实拦截其他任务或跨 D 后续呼叫,原实时文字/即时拒联产品门禁必须由业务负责人明确批准修改;未签收前不切换。 +- **MQ 与任务所有权(现行/目标分开):**现行代码按 D+租户声明/消费队列、`--tenant-key` 单租户启动;**目标由 SaaS 独占创建/绑定/退役 D+任务队列,D 只消费 SaaS 分配给自己的任务队列,不建立队列或绑定**。按 D 身份读取 `tasks` 一致全量快照以恢复任务消费,运行中每 30 秒按 SaaS 变更游标获取增量(新增、更新、撤销),游标失效须重新全量;任务停止也不能先删除仍有积压/未 ACK 的队列。SaaS 另负责建立 D 专用控制队列,控制不排在任务执行积压之后。D 有任务/租户/Cell/线路/AI 名额及配置资格才接纳执行,同事务持久绑定执行/额度/outbox 后 ACK;停止任务的旧命令仍逐条拒绝并 ACK,不清空其他任务。现行共用队列与 `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 +35,31 @@ | 工作包 | 输入 / 负责人边界 | 完成证据 / 未满足时状态 | | --- | --- | --- | | 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 拓扑;下一轮 `tasks` 全量发现、队列创建/绑定/退役及任务粒度路由由 F07 单独冻结,不默认塞进现有两条配置接口 | 新版本来源/版本/哈希、严格 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 不自动释放。 | +| F03 有界执行队列与控制隔离(目标实现依赖 F01/F07) | 本项目 D 消费、SQLite 租户总额度/执行绑定和 MQ 适配:空位才接纳,未接纳留 SaaS 创建的 MQ 任务队列;SaaS 声明任务/控制队列及绑定并确认就绪,D 只消费,旧直写面禁并行。F07 签收前旧租户队列实现仅作现行基线,不可冒充目标完成。 | 爆量消息 SQLite 未接纳积压不无界增长,MQ 队列有容量/发布拒绝与 SaaS 原消息保留;commit/ACK/confirm 丢失及 stop-before-accept 不拨、不忙重投;unknown 不自动释放。队列未就绪时发布必失败/原消息留存,D 不偷偷创建或清空队列。 | | 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。 | +| F05 多 D 独占资源(后续,依赖 F01–F04/F07 及另获本阶段授权) | SaaS 对 `(tenant_key,task_id)→dispatcher_id` 持久路由并独占建立目标 D 的任务队列;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 的内部恢复/去重事实,也不允许未知是否已拨号时重拨。 +**队列创建权不可倒置:SaaS 创建、绑定并持有任务队列和 D 专用控制队列的生命周期;D 只消费 SaaS 已声明的队列。** SaaS 新建/改派任务时先准备持久队列及精确绑定,再投递 persistent 消息,发布 mandatory 无 return 并有 confirm 才记入队;D 可离线,队列中的消息等待其重启。D 启动/重启从按 D 身份授权的 `GET /tasks` 取得**一致全量快照及快照游标**(任务 ID、原值 `tenant_key`、归属 D、状态、SaaS 创建的队列名/精确绑定及版本),恢复所有应消费任务;此后每 30 秒 `GET /tasks?after=` 拉取针对该 D 的**变更日志**,增量必须覆盖旧任务更新/停止、改派离开本 D 的撤销标记及新任务分配。`after` 是 SaaS 对该 D 任务变更的单调游标,**不是最大任务 ID**,因此不会漏掉小 ID 任务的后续变更。快照与水位须一致;分页按同一快照/连续游标读完,D 本地应用并持久记录后才推进游标;缺页、过期、重置或不一致则停受影响任务新准入并重新拉取全量,不用不完整清单继续消费。启动全量后 D 仍须在运行中每 30 秒轮询,每 30 秒请求一次是**待签收的轮询频率**,发现的端到端时限还受请求/分页/应用耗时影响并需另定,不复用任务配置约 60 秒缓存时限;请求失败不得假装新增任务已发现。已停止但仍有积压或未 ACK 的任务继续列为待排空状态,直到事实排空且 SaaS 确认退役;D 不因停止就丢消费/删队列。HTTP 路径、身份承载、游标/分页与失败码须 F07 冻结;现有单任务配置接口不能充当全量发现。任务队列按归属 D+任务 ID 定位,`task_id` 是否全局唯一及完整路由/长度预算必须冻结,不能因碰撞跨租户抢收。任务数/队列数有上限,不能无限制为历史任务保留 broker 资源。 + +**队列隔离任务,额度仍按租户统一计算。** 同一 D 中相同原值 `tenant_key` 的不同任务共同占用同一租户上限,D 在 SQLite 单事务中核查并预留租户+任务+线路/Cell 等已批准额度,未知通话继续占用;某租户满额时其任务消息留 MQ,不能让该租户积压阻塞其他租户及控制。若同一租户跨 D,仍按 F05 在 SaaS 权威分配有界份额,不能让各 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 验收。 | +| F07 签收发现接口与新 MQ 合同(依赖 F01) | SaaS 权威负责人冻结启动 `GET /tasks` 一致全量快照与水位、每 30 秒 `GET /tasks?after=` 的按 D 单调变更游标(不是最大 task_id)、新增/更新/撤销标记、连续分页、游标过期重拉全量及队列退役规则;冻结**仅 SaaS 可声明/绑定任务和控制队列、SaaS 先确认队列就绪后才能发布、D 只消费**的 ACL/拓扑/错误与握手,D+task 路由与任务 ID 唯一性/长度预算、持久化、消息容量及重复投递。同时与业务批准撤销对外查询/补传和分散通话事件,冻结 `call.result` 成功/失败结构、关联与幂等、录音缺失有界收口、取消即时文字/拒联的后果;统一任务时限来源与 `call.execute` 执行快照。 | 带来源/版本/哈希的严格 Schema、正反例及 SaaS/业务签收;证明离线新建/更新任务先有队列、D 重启全量恢复、运行中每 30 秒发起增量请求并在合同规定的时限内应用新任务/旧任务变更、失效游标全量重置、无队列时 SaaS 保留原消息、停用队列在排空后才退役;旧任务队列改派不得与旧 D 并发消费。覆盖配额跨任务、停/暂停、重复/乱序、OSS 成败及旧实时反馈验收冲突;任何边界未定即 blocked,不能把项目示意当正式接口。 | +| F08 本地实现与受控切换(依赖 F07/F02–F04) | TDD:SaaS 实现 `tasks` 一致全量快照及每 D 递增变更游标/连续分页、任务/控制队列创建绑定、ready 后发布及保留未路由原消息;D 移除新协议中的 `--tenant-key`/自行声明租户队列路径,启动全量恢复、运行中每 30 秒增量读取(游标失效重拉全量),按权威清单**只消费**归属任务队列并持久推进已应用游标,维持按 `tenant_key` 的原子总额度,控制走独立队列;替换对外旧查询/补传和分散事件/outbox,保留命令回执/内部恢复。OSS 成功才组装唯一最终结果,失败按 F07 有界收口;旧/新版本不得混写同一任务。 | D 对任务/控制队列无 create/bind/delete 权限仍能启动并处理;SaaS 先建后发与 offline 消息积压可验证;队列缺失 fail-closed,SaaS 原身份消息留待就绪后重发,D 不偷偷建队列。同租户多任务共享额度、满额任务不会阻塞其他租户/stop;停止任务逐条拒绝并 ACK 已积压命令,不清空队列。同次通话最终结果持稳定身份,MQ persistent/durable/mandatory/confirm 成功后才记交付;确认丢失/重启不重新拨号/PUT。无端到端切换/恢复证据不得启用。 | +| F09 结果验收(依赖 F08) | 当前 P1 单 D/单租户下本地隔离链路验收;为新增动态任务/租户发现另做**非 P1 扩展合同测试**,不能把它们写成本轮双租户/多 D 已验收;真实 SaaS 联调另获授权。 | SaaS 先建任务队列、D 离线发布、重启从一致全量清单/快照游标恢复、运行中每 30 秒轮询并按约定时限应用新任务及旧任务状态变更、分页/游标过期后全量对账、停用任务保留积压、缺队列未路由消息保留/补发、D 无建队权限、同租户多任务共享额度和控制队列不断路均有故障注入;不同 D 份额只验证合同不冒充真实多 D。SaaS 仅收命令回执和每通话一份含 OSS `bucket/object_key` 的最终结果,录音失败有界收口;MQ/OSS 断连、重启无重复拨号、无多份结果。格式化、`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 测试。 +2. **消费与故障:**由 SaaS 建/绑定任务与控制队列,先 ready 再发布;D 被撤销 create/bind/delete 权限仍只消费。D 离线期间 SaaS 新建任务并入队、D 重启 `tasks` 全量快照/游标一致、运行中每 30 秒增量发现新任务及**较小 ID 旧任务的更新/撤销**、分页缺页/重复与游标失效回全量、停止后待消费旧命令/队列退役、队列缺失时 mandatory return 和原消息保留均须覆盖。再测 MQ 爆量/队列满发布端保留、D DB 满盘、ACK 丢失、旧命令重投、控制先到未入库执行、停/暂停与配置更新并发、执行中未知占用、同一租户不同任务共享额度;不同 D 的租户和运营商总额无份额合同就不开放跨 D 测试。 3. **时间与安全边界:**任务/线路时段变化允许的约60秒延迟必须与“已接纳不变、未接纳可能旧版”区分;MQ stop/pause 和号码白名单不可延迟;配置过期则拒新准入,队列消息期限不可让旧任务次日自动拨。真实外呼只在获得明确安排、实际已通过相应门禁后才能尝试。 4. **最终通话结果(下一轮):**通话结束但 OSS 尚未返回时不得发成功结果;成功后持久化原始资产事实再入 MQ,确认丢失重投同一事件身份;失败/过期按经签收的有限期限形成唯一显式结果。`outcome` 不因录音失败伪改通话结局;SaaS 收到唯一结果前不会获知文字/拒联,需有经批准的业务风险处置;控制与积压任务消息必须继续消费并拒绝/ACK,不能清空整个租户队列。 5. **缺失权威:**页面截图没有现网 JSON 键,`config-read-v0.1.schema.json` 为项目自拟;SIP 线路完整管理信息、供应商鉴权/注册、AI UI 扩展和 `snapshot_sha256` 规范仍未获发布或外部签收;不能把 Mock 数据、旧 OpenAPI、计划或字段草案记为 SaaS/production 验收。 @@ -64,7 +70,7 @@ ## 7. 当前责任与前置(I/M/G) -- **I = 契约来源/授权:**F00 仅盘点完成;F01 的 SaaS/management 源字段签收、HTTP 合同、MQ 控制/执行新协议及 SIP/AI 不确定项仍为 **blocked/pending**;F07 的单份结果合同、取消实时反馈的业务批准及录音失败有界收口也未签收。没有 I,不开始 F02–F04 的真实配置接入,更不能借 Mock 名义发外呼。 +- **I = 契约来源/授权:**F00 仅盘点完成;F01 的 SaaS/management 源字段签收、HTTP 合同、MQ 控制/执行新协议及 SIP/AI 不确定项仍为 **blocked/pending**;F07 的 SaaS 独占任务/控制队列创建权、`tasks` 全量快照+每 30 秒 `after` 变更游标发现/任务路由合同、单份结果、取消实时反馈的业务批准及录音失败有界收口均未签收。没有 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 或真实供应商验收。 @@ -78,7 +84,7 @@ | 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;实时文字/拒联取消及录音失败/超时的唯一最终通知仍为阻塞项。 | +| F07–F09(下一轮) | 用户明确规定**SaaS 独占创建/绑定/退役任务队列,D 仅消费**,任务 ID 定位队列,`tasks` 一致全量快照供重启恢复,运行中每 30 秒按变更游标拉增量(非最大任务 ID);同一租户多任务共用租户额度。用户另确认对外去掉查询/补传、只保留最终通话结果及 OSS 路径的方向;[对接说明](thirds/第三方对接事件与请求消费顺序_v0.1.md)中的新事件仍仅是 review 草案,现行 MQ/代码未改。 | F07 先冻结队列所有权、SaaS 先建后发、离线/重启/在线任务发现、D 无建队权限、按租户额度及新版事件合同,再按 TDD 做 F08、隔离验收 F09;实时文字/拒联取消及录音失败/超时的唯一最终通知仍为阻塞项。 | ## 9. 协作和版本记录 diff --git a/docs/thirds/第三方对接事件与请求消费顺序_v0.1.md b/docs/thirds/第三方对接事件与请求消费顺序_v0.1.md index 22a0ed5..0bfedea 100644 --- a/docs/thirds/第三方对接事件与请求消费顺序_v0.1.md +++ b/docs/thirds/第三方对接事件与请求消费顺序_v0.1.md @@ -7,16 +7,48 @@ | 顺序 | 请求与触发 | SaaS 处理/返回 | | --- | --- | --- | | 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` 作为接纳或拒绝的命令回执,不代表呼叫完成。 | +| 2(下一版拟定) | D 启动/重启 `GET /tasks` 取得本 D 的任务全量快照与变更游标,运行中每 30 秒 `GET /tasks?after=`;SaaS 创建任务时先建好任务队列/绑定再发布。 | D 发现新任务后仅消费 SaaS 已创建的队列;D 离线期间消息可留在队列,较小任务 ID 的更新/停止也能由变更游标发现。路径、字段、30 秒时限尚待 SaaS 签收。 | +| 3 | SaaS 将任务固定分配给一个 D,向该 D 投递 `call.execute`(现行消息格式;新队列形态拟定)。 | D 按消息中的租户原值和任务 ID 读取含智能体的任务配置(拟定 HTTP GET);未接纳任务可受约 60 秒配置缓存延迟影响,已接纳执行固定原快照。 | +| 4 | 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,时限尚未签收。 | +| 5 | 通话终结且录音已上传 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` 保留原值;仅 publisher confirm **不等于** SaaS 已处理。下一版本是否复用拓扑由合同签收,不在本文中假设已上线。 +### 1.1 现行 MQ 地址与 JSON 字段不是一回事 -## 2. D ← SaaS:只读配置响应(拟定,非现网) +RabbitMQ 有**发布入口 exchange → 发布时指定的 routing key → 预先绑定的 queue → D 消费**四步;`.in` 只是路由键中约定的“给 D”后缀,**不是** RabbitMQ 自动寻找 D 的指令。现行已发布拓扑如下;这里只是解释旧合同,**不是**下一版按任务队列的设计: -两接口均为 GET、**无请求 JSON 体**。D 使用自身 UUID 与 SECRETKEY,SIP 读本 D 全量,任务读原值 `tenant_key` + `task_id` 对应的单任务;实际 URL、请求头/参数、密钥承载方式待 SaaS 签收,本文**不虚构 HTTP 报文**。条件读取拟使用 `ETag/If-None-Match`,有效缓存约 60 秒;过期/请求失败只停新执行准入,既有执行保持已绑定快照,不妨碍 MQ 控制命令。 +```text +SaaS→D exchange: agent-call.dispatchers.v2 + routing key: d..t..in + binding key: d..t..in + queue: agent-call.d..t..v2 + consumer: 对应 Dispatcher +D→SaaS exchange: agent-call.saas.v2 + routing key: d..t..out + queue: agent-call.saas.events.v2 + consumer: SaaS +``` + +例如 §3.1 的 JSON 带 `dispatcher_id=c046b893-8628-4589-ae50-619d049248a6`、`tenant_key=tenant-a`,SaaS 的**MQ 发布参数**就对应 `d.c046b893-8628-4589-ae50-619d049248a6.t.tenant-a.in`;D 消费队列 `agent-call.d.c046b893-8628-4589-ae50-619d049248a6.t.tenant-a.v2`。exchange、routing key、queue 和 binding **不在 JSON 的 `payload` 中**;JSON 的 `dispatcher_id/tenant_key` 是接收后核验身份。当前消费者启动需要 `--tenant-key` 且由 D 声明租户队列;这不是动态任务发现能力。即使 D 离线,只要队列/绑定已由有权一方预先创建并且消息持久入队,重启后仍可消费;若发布时队列不存在,事后建队**不能倒灌旧消息**,SaaS 要保留原消息并确认就绪后重投,mandatory 返回与 publisher confirm 应同时核对。`dispatcher_id` 是唯一 UUID v4;`tenant_key` 保留原值。仅 publisher confirm **不等于** SaaS 已处理。 + +### 1.2 下一版任务队列 KEY(项目示意,尚无发布的机器合同) + +**硬边界:任务队列与绑定只由 SaaS 创建/维护/退役;D 只消费,不声明、创建、绑定或删除。** SaaS 必须先确认持久队列/精确绑定就绪,再发布 persistent 命令。下面的 `v3-draft` 名称仅展示规则,**不是现网 exchange/queue,也不是可直接上线的名字**: + +```text +SaaS 创建并绑定: + exchange: agent-call.dispatchers.v3-draft + routing key: d..task..in + binding key: d..task..in + queue: agent-call.d..task..v3-draft +D 仅按 SaaS /tasks 清单中的 queue 名称开始消费。 +``` + +`dispatcher_id` 选唯一 D,`task_id` 选该 D 的**一项任务**,`.in` 区分入站;`tenant_key` **不再参与任务队列 KEY**,仍在消息 JSON 和 `/tasks` 结果中,用于租户归属核验及跨任务汇总并发。若 task_id 有重复、点号/通配符或超长,不能直接照拼:ID 唯一性、段格式、完整 key/queue 长度及最终版本名必须先由 F07 冻结。`task.control` 另走 SaaS 创建的 **D 专用控制队列**,不能排在某个任务的呼叫积压之后;命令回执和最终结果回 SaaS 的具体新路由同样待 F07 发布,不套用这些示意名称。暂停/停止靠任务状态,**不是因为 KEY 中有 task_id 就会自动清空队列**;已停止任务的旧命令仍逐条拒绝并 ACK。 + +## 2. D ← SaaS:只读配置与任务发现(拟定,非现网) + +前两条拟定配置接口均为 GET、**无请求 JSON 体**;下一版另拟增加 §2.6 的任务发现接口。D 使用自身 UUID 与 SECRETKEY,SIP 读本 D 全量,任务读原值 `tenant_key` + `task_id` 对应的单任务;实际 URL、请求头/参数、密钥承载方式待 SaaS 签收,本文**不虚构 HTTP 报文**。条件读取拟使用 `ETag/If-None-Match`,有效缓存约 60 秒;过期/请求失败只停新执行准入,既有执行保持已绑定快照,不妨碍 MQ 控制命令。 ### 2.1 SIP 配置:200,返回本 D 的完整获批快照 @@ -124,7 +156,9 @@ } ``` -**字段说明/消费动作:**- `schema_version/resource`:草案版本 `config-read.v0.1`、资源 `sip_config`;`dispatcher_id`:只能与发起请求的 D 相同。 +**字段说明/消费动作:** + +- `schema_version/resource`:草案版本 `config-read.v0.1`、资源 `sip_config`;`dispatcher_id`:只能与发起请求的 D 相同。 - `revision/snapshot_sha256/approved_at`:整份获批快照的修订、摘要、批准时间;摘要生成规则和版本来源仍待双方确定,示例摘要仅为占位值。 - `artifact`:静态 Cell 制品;`artifact_id/source_release/source_digest/approval_reference` 标识制品、来源版本/摘要和批准引用;`cell_id/revision/config_sha256` 标识执行单元及制品版本;`mode` 是 mock/real 范围;`allowed_targets` 是允许的原始号码;`load_evidence` 为可空加载证据。`trunks[]` 中 `trunk_id/provider_id/egress_pool_id` 定义线路、供应商、出口;`codec` 为 PCMA;`caller_profile_ids` 为主叫引用;`dial_prefix` 仅本线路前缀;`enabled` 是否启用;`sip_endpoint_ref/credential_ref/media_profile_id` 为连接、凭据和媒体配置引用,不传实际密码。`media_profiles` 下 `format/sample_rate_hz/channels/payload_type` 定义媒体格式。 - `trunk_details[]`:每项的 `trunk_id` 必须与 `artifact.trunks[]` 一一对应;`server_host/server_port` 为 SIP 服务端;`transport/auth_mode/registration_required` 是传输、认证和注册方式;`max_concurrent_calls` 是分配到该 D 的线路额度;`null` 代表未知,不可用于真实外呼。`caller_profiles[].caller_profile_id/caller_id` 给出主叫引用/原始标识(如含 `BD`),不可清洗成纯数字。 @@ -254,7 +288,9 @@ } ``` -**字段说明/消费动作:**- `schema_version/resource/dispatcher_id/tenant_key/task_id`:版本、资源 `task_config`、归属 D、原值租户键和单任务 ID;D 必须验证请求归属。`task_revision` 是任务修订,`status` 为拟定 `running/paused/stopped/finished`;非 running 不接新呼叫。 +**字段说明/消费动作:** + +- `schema_version/resource/dispatcher_id/tenant_key/task_id`:版本、资源 `task_config`、归属 D、原值租户键和单任务 ID;D 必须验证请求归属。`task_revision` 是任务修订,`status` 为拟定 `running/paused/stopped/finished`;非 running 不接新呼叫。 - `name/group_id` 是名称及可空分组;`max_concurrent_calls` 是本任务额度,不等于跨任务/跨 D 总额度;`route_policy_id/allowed_trunk_ids[]` 是路由引用及可用线路列表。 - `schedule.time_zone/starts_at/ends_at` 定义时区和可空的起止时间;`weekly_windows` 按星期列出每日多个左闭右开 `{start,end}`,空数组禁呼;`excluded_dates[]` 为按 Asia/Shanghai 日期优先排除的日子。任务时段还须与线路时段相交。 - `agent.agent_version_id/content_sha256`:不可变智能体版本及内容摘要,必须与 `agent.config.agent_version_id` 对应;`authorization_id/authorization_expires_at` 为授权身份和截止时间,到期不得由缓存/304 复活。 @@ -386,6 +422,150 @@ HTTP/1.1 304 Not Modified **字段说明/消费动作:**`schema_version/resource` 标识草案错误对象;`error.code` 是机器可读错误代码(示例 `not_assigned` 表示该任务不归此 D),`error.message` 是可读说明,不含密钥。D 不得将失败当作空任务/无限制或使用过期配置接新呼叫;不能自动回退至 MQ 配置通道。 +## 2.6 D ← SaaS:动态任务发现(**下一版草案,现行无此接口/Schema**) + +新增第三条只读 HTTP 接口,不属于 §2 现有的两种配置响应。D 以自身身份在启动/重启时 `GET /tasks` 取得**一致全量快照 + 游标**,运行中**每 30 秒**以 `GET /tasks?after=` 请求针对本 D 的**任务变更**。`after` 不是最大 `task_id`:已存在的小 ID 任务被暂停、停止、改派也必须返回。以下路径、JSON 键/类型、认证承载和错误码仅是项目提案,待 SaaS/F07 冻结,不代表现网已提供;每 30 秒发起请求是轮询频率,不是端到端 30 秒发现保证,也不同于单任务配置约 60 秒缓存。无请求 JSON 体,具体鉴权/分页传递方式未签收,不伪造完整 HTTP 请求头。 + +### 2.6.1 启动或重启:全量快照(200,拟定) + +```json +{ + "schema_version": "task-discovery.v0.1-proposal", + "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", + "mode": "snapshot", + "snapshot_id": "snapshot-1042", + "cursor": "1042", + "tasks": [ + { + "task_id": "task-a", + "tenant_key": "tenant-a", + "status": "running", + "task_revision": 1, + "queue": { + "exchange": "agent-call.dispatchers.v3-draft", + "routing_key": "d.c046b893-8628-4589-ae50-619d049248a6.task.task-a.in", + "binding_key": "d.c046b893-8628-4589-ae50-619d049248a6.task.task-a.in", + "queue_name": "agent-call.d.c046b893-8628-4589-ae50-619d049248a6.task.task-a.v3-draft" + } + }, + { + "task_id": "task-old", + "tenant_key": "tenant-a", + "status": "stopped", + "task_revision": 3, + "queue": { + "exchange": "agent-call.dispatchers.v3-draft", + "routing_key": "d.c046b893-8628-4589-ae50-619d049248a6.task.task-old.in", + "binding_key": "d.c046b893-8628-4589-ae50-619d049248a6.task.task-old.in", + "queue_name": "agent-call.d.c046b893-8628-4589-ae50-619d049248a6.task.task-old.v3-draft" + } + } + ], + "next_page_token": null +} +``` + +**字段说明/消费动作:**`dispatcher_id` 是被授权的目标 D;`snapshot_id` 锁定同一次全量读取,跨页不得混杂新旧状态;`cursor` 是此快照覆盖的 SaaS 任务变更水位(示例数字只是**不透明字符串**,D 不按大小比较任务 ID);`tasks[]` 列出本 D 全部归属任务及**已停止但队列仍有积压的任务**;`tenant_key` 保留原值,用于同租户所有任务共享并发额度;`task_revision/status` 是任务版本和状态;`queue` 是**SaaS 已创建/绑定**的消费地址,D 只能读取,不能自行声明。`next_page_token` 非空时须在同一个 `snapshot_id` 下读完所有页再应用快照/水位,分页传递机制待签收。 + +### 2.6.2 每 30 秒:增量变化(200,拟定) + +```json +{ + "schema_version": "task-discovery.v0.1-proposal", + "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", + "mode": "changes", + "from_cursor": "1042", + "next_cursor": "1044", + "changes": [ + { + "cursor": "1043", + "operation": "assigned", + "task_id": "task-b", + "tenant_key": "tenant-a", + "status": "running", + "task_revision": 1, + "queue": { + "exchange": "agent-call.dispatchers.v3-draft", + "routing_key": "d.c046b893-8628-4589-ae50-619d049248a6.task.task-b.in", + "binding_key": "d.c046b893-8628-4589-ae50-619d049248a6.task.task-b.in", + "queue_name": "agent-call.d.c046b893-8628-4589-ae50-619d049248a6.task.task-b.v3-draft" + } + }, + { + "cursor": "1044", + "operation": "updated", + "task_id": "task-a", + "tenant_key": "tenant-a", + "status": "stopped", + "task_revision": 2, + "queue": { + "exchange": "agent-call.dispatchers.v3-draft", + "routing_key": "d.c046b893-8628-4589-ae50-619d049248a6.task.task-a.in", + "binding_key": "d.c046b893-8628-4589-ae50-619d049248a6.task.task-a.in", + "queue_name": "agent-call.d.c046b893-8628-4589-ae50-619d049248a6.task.task-a.v3-draft" + } + } + ], + "next_page_token": null +} +``` + +**字段说明/消费动作:**`from_cursor` 对应请求的 `after`,`changes[].cursor` 是 SaaS 为本 D 变更生成的顺序水位,`next_cursor` 是成功处理整份回复后的下一次 `after`;`assigned` 为新归属、`updated` 为旧任务版本/状态变化。**任务 `task-a` 的 ID 比新任务旧,却仍被增量返回**,这正是不能用最大任务 ID 当游标的原因。先由 SaaS 创建/绑定任务队列并确认 ready,才能把 `assigned` 返回且开始发布;D 只消费。`stopped` 后 D 继续读该任务已积压消息、逐条拒绝并 ACK,绝不清空或删除队列。暂停/停止命令另走 SaaS 创建的 D 控制队列;30 秒任务清单轮询**不能代替即时控制**。同一租户 `task-a`、`task-b` 共同占用 `tenant-a` 额度。D 持久应用变更后才持久推进游标;分页时读完连续页,不得跳过未处理页。 + +### 2.6.3 任务改派/退役(200,拟定;与停止不同) + +```json +{ + "schema_version": "task-discovery.v0.1-proposal", + "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", + "mode": "changes", + "from_cursor": "1044", + "next_cursor": "1045", + "changes": [ + { + "cursor": "1045", + "operation": "removed", + "task_id": "task-old", + "tenant_key": "tenant-a" + } + ], + "next_page_token": null +} +``` + +**字段说明/消费动作:**`removed` 是 SaaS 确认此 D 不再消费该任务的撤销记录(tombstone),**不是** `stop` 一到就立刻删除队列。必须已停止新发布、旧队列积压和未 ACK 消息处理完毕,且改派时确认旧 D 没有未知执行后再终结旧所有权;具体握手/退役合同待签收。D 只停止消费,不负责删队列;队列生命周期仍归 SaaS。 + +### 2.6.4 没有变更(200,拟定) + +```json +{ + "schema_version": "task-discovery.v0.1-proposal", + "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", + "mode": "changes", + "from_cursor": "1045", + "next_cursor": "1045", + "changes": [], + "next_page_token": null +} +``` + +**字段说明/消费动作:**SaaS 没有新变更时水位不动;D 等下一个 30 秒周期,不因空列表删除已有消费关系。 + +### 2.6.5 游标失效或缺页(错误,HTTP 状态待签收) + +```json +{ + "schema_version": "task-discovery.v0.1-proposal", + "resource": "error", + "error": { + "code": "cursor_expired", + "message": "A full task snapshot is required." + } +} +``` + +**字段说明/消费动作:**`cursor_expired` 表示 SaaS 已不能提供从旧游标起的连续变更;缺页、断续或快照分页不一致也应中止增量。D 不推进错误游标,停受影响任务的新接纳并重新拉一致全量快照;不能把错误当无变更或盲目根据 RabbitMQ 队列列表发现任务。真正的错误码、游标保留期/分页格式须 F07 签收。 + ## 3. SaaS → D:业务命令(现行 MQ 格式;执行语义以新合同为准) 下列 JSON 是**完整 MQ 请求**。`schema_version` 指现行消息版本 `2.0`;`command_id` 是同一命令的稳定幂等身份,`command_type` 是命令类别;`dispatcher_id/tenant_id/tenant_key` 确定目标和租户;`trace_id` 关联结果;`issued_at/not_after` 限定时效;`payload` 是对应业务参数。重投同一命令不能创建第二次执行。新版去除对外查询和补传**命令**,不等于允许吞掉 MQ 重投或丢失本地恢复事实。 @@ -493,7 +673,7 @@ HTTP/1.1 304 Not Modified } ``` -**字段说明/消费动作:**`stop` 终止该任务的新呼叫准入;`active_call_policy=hangup` 结束已在途通话,若选择 `drain` 则等待自然结束。停止命令**不是清空 RabbitMQ 队列**:D 仍消费该租户队列,并对属于已停止任务的积压呼叫逐条产生拒绝回执、ACK,不影响同队列其他任务。现行共享队列尚不保证控制能超越积压执行消息,这属于下一轮门禁。 +**字段说明/消费动作:**`stop` 终止该任务的新呼叫准入;`active_call_policy=hangup` 结束已在途通话,若选择 `drain` 则等待自然结束。停止命令**不是清空 RabbitMQ 队列**:现行 D 仍消费租户共享队列并逐条拒绝/ACK 该任务积压消息;下一版改为继续消费**该任务的 SaaS 所建队列**并逐条拒绝/ACK,不影响其他任务。现行共享队列尚不保证控制能超越积压执行消息,下一版 D 专用控制队列及 stop-before-accept 屏障仍待签收。 ### 3.5 D → SaaS:命令处理回执 `command.result`(保留,不是通话事件)