docs: refine task controls and tenant quota contracts

This commit is contained in:
2026-09-23 20:27:51 +08:00
parent 6e29ac87ac
commit ef84a0663d
8 changed files with 453 additions and 247 deletions
+6 -5
View File
@@ -155,7 +155,7 @@
- **现行合同(尚未切换)**:Dispatcher按MQ任务agent_version_id,经RabbitMQ专用Topic向SaaS取得不可变AI配置/授权,响应回原Dispatcher;校验租户/源Schema/不可变摘要/能力并持久绑定后向Agent交付执行快照。过去已废弃的独立AI版本GET不复活;新获批准的方向是**任务配置只读接口内含智能体**,须另冻合同而非旧GET兼容层。Agent不直连SaaS,不从CLI/env/源码常量或SDK默认覆盖AI业务值,不新增任务MQ模式字段或调参后台。
- 已有model/prompt/voice/speed/ASR输入与识别/temperature/max_tokens/timeout及对话控制必须实际传入SDK或控制器;热词/VAD/top_p/音量/阶段时限等所需扩展先在上游补GAP-09,再生成校验。严格additionalProperties不放宽,不借metadata/raw_request透传。
- **现行 AI 合同**:SaaS新版本供新任务引用,无需改代码/重启D/A;在途/原排队任务固定快照,同版本异内容拒绝。缓存按租户+版本隔离,断SaaS无有效授权缓存拒新准入;显式0/false与未提供保真,并发通话不得共享可变SDK参数。
- **用户批准的后续只读配置方向(替代前述MQ配置目标)**:SaaS 提供两条只读 HTTP 配置接口:该 D 所属资源分区的获 management 批准的 SIP 全量,以及任务配置(内含智能体当前获授权的不可变版本/内容);D 用各自 UUID+SECRETKEY 按需获取,不将凭据写入源码、样例或日志。SIP 在启动/重启先取全量、核验 Agent/Asterisk 已加载再读执行队列;任务按租户+任务缓存约60秒,活跃且有待接纳执行时到期主动刷新;已接纳/运行中执行固定原快照,未接纳允许最多约一分钟配置生效延迟。ETag/304 减少重复下载,缓存过期或 HTTP 不可用时拒绝新执行,**不使用过期缓存或MQ配置回退**;停/暂停、opt-out仍经MQ及时处理,不随缓存延迟。任务终结后仅清配置缓存,不删执行恢复/幂等/outbox。SIP 变更须关执行准入、排空/对账、确认加载后恢复;不能保证一分钟内完成发布。旧 `call.execute` 固定引用/排队 AI 固定规则须经新合同定义重绑定与修改-准入竞态;未发布新版前不可变现行语义。
- **用户批准的后续只读配置方向(替代前述MQ配置目标)**:拟定 SaaS 只读路径 `/internal/v1/dispatcher/sip`、`/internal/v1/dispatcher/task/:task_id`,以及下一轮归属任务发现 `/internal/v1/dispatcher/tasks`;新增第四条拟定 `/internal/v1/dispatcher/tenant/:tenant_id/quota`(项目路径草案)在取得任务后按 tenant_id 读取分给该D的租户总份额;前两条返回获 management 批准的 SIP 全量和含智能体获授权不可变版本的任务配置;D 用 `X-DISPATCHER-id`/`X-DISPATCHER-SECRET-KEY` 按需获取,不将凭据写入源码、样例或日志。SIP 在启动/重启先取全量、核验 Agent/Asterisk 已加载再读执行队列;任务按租户+任务缓存约60秒,活跃且有待接纳执行时到期主动刷新;已接纳/运行中执行固定原快照,未接纳允许最多约一分钟配置生效延迟。不采用 ETag/304,缓存到期重新读取完整 200 响应;过期或 HTTP 不可用时拒绝新执行,**不使用过期缓存或MQ配置回退**;停/暂停控制不随配置缓存延迟;**现行** opt-out 仍以 MQ 即时事件通知,下一版仅在最终 `call.result` 中通知 SaaS,业务签收前不可冒称两者等价。任务终结后仅清配置缓存,不删执行恢复/幂等/outbox。SIP 变更须关执行准入、排空/对账、确认加载后恢复;不能保证一分钟内完成发布。旧 `call.execute` 固定引用/排队 AI 固定规则须经新合同定义重绑定与修改-准入竞态;未发布新版前不可变现行语义。
- 凭据/供应商端点来自受控引用且有授权/出口校验,不能因可调参数绕过安全硬限额或启用不安全重试。OpenAI默认自动重试显式关闭;日志只留脱敏版本/摘要/有效参数,不打印prompt/变量/密钥。
- 静态发布只约束SIP/节点制品,不将AI配置硬编码;GAP-08/09及SDK参数PoC为P1门禁,验证入口见验收§5.1(现有E/L项子场景,不新增虚假通过数)。
@@ -164,20 +164,21 @@
- 本项目设计/运行/验收文档只在自身 `docs/` 维护。上游共享接口有唯一权威来源;导入带版本、来源和哈希的不可变契约包,再生成类型/校验,不维护重复手写 Schema。
- 新内部消息/许可/fencing 协议需先获批;不擅自改变 SaaS 路径、字段、状态、路由或控制语义。
- **现行已发布合同(新版本生效前必须遵守)**: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 新版本冻结并完成切换前不可擅改或声称已实现。
- **用户已批准的新目标,尚非现行合同或运行能力**:任务(含智能体)、SIP、任务发现和按 tenant_id 的租户额度走四条只读 HTTP;呼叫、控制及其必要回执/单份最终结果走 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添加字段。
- **用户确认的下一轮任务队列所有权硬边界(未实施,见 `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=<cursor>` 拉取该 D 新增/更新/撤销,`cursor` 为 SaaS 变更水位而非最大任务 ID,失效则重新取一致全量快照;轮询间隔不等于端到端发现时限,也不能替代 MQ 即时控制;停止任务旧消息逐条拒绝/ACK,不能直接删队列。同一 D 下各任务共享原值 `tenant_key` 的租户总额度,跨 D 仍须权威份额;任务数与队列资源有上限。现行代码启动时按 `--tenant-key` 自行声明租户队列,与新目标不符,F07 严格合同、F08 实现及 F09 验收前不得声称动态租户/任务队列已可用。
- **用户确认的下一轮任务队列所有权硬边界(未实施,见 `docs/plan-config-read-v0.1.md` §1/§3.4/§4):任务队列及 RabbitMQ 绑定只能由 SaaS 创建、维护、退役;Dispatcher 只消费,不能自行声明/建队、绑定或删除任务队列。** SaaS 需先确保持久任务/控制队列及绑定就绪,再向按归属 D+任务 ID 定位的任务队列发布 persistent 命令;D 离线时已存在的任务队列可积压,缺队列而未入队的原消息由 SaaS 保留并在就绪后按同一身份重发,不能称作 D 重启自动补收。新增按 D 身份授权的 `tasks` 全量清单用于 D 重启恢复,运行中拟每 30 秒 `GET /internal/v1/dispatcher/tasks?after=<cursor>` 拉取该 D 新增/更新/撤销,`cursor` 为 SaaS 变更水位而非最大任务 ID,失效则重新取一致全量快照;轮询间隔不等于端到端发现时限,也不能替代 MQ 即时控制;pause保留积压、resume重新核验后继续消费原积压,过期不延长;stop持久生效后未接纳旧消息**静默消费/ACK,不拨号、不回逐条结果**,不能直接删队列;控制本身和已有在途通话仍有处理/最终结果,本地错误与计数不静默。同一 D 下任务响应须带 tenant_id/原值 tenant_key,按 tenant_id 读取的租户额度在各任务间共用,0停新准入、缺失/过期失败拒新;降额不强挂或清未知占用,跨 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。
- 前一轮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。
- **待 review/待 SaaS 与业务签收的下一轮方向**见 `docs/plan-config-read-v0.1.md` §4.1:SaaS↔D 移除对外查询/补传命令及分散通话/转写/拒联/录音事件,保留呼叫/控制命令和必要命令回执;录音仍上传 OSS,D 在上传成功后只回传一份含最终转写、拒联事实与 OSS 路径的 `call.result` 草案。用户已确认不要求 SaaS 在通话结束前收到实时文字或拒联,但原验收与跨任务/跨 D 拦截能力将变化,必须单独修订并签收;录音失败/超时的有界收口仍待冻结。现行 Schema/代码未改、此草案不得冒称已上线,且本轮只改文档供用户 review。
- **待 review/待 SaaS 与业务签收的下一轮方向**见 `docs/plan-config-read-v0.1.md` §3.5/§4:SaaS↔D 移除对外查询/补传命令及分散通话/转写/拒联/录音事件,保留呼叫/控制命令和必要命令回执;录音仍上传 OSS,D 在上传成功后只回传一份含最终转写、拒联事实与 OSS 路径的 `call.result` 草案。用户已确认不要求 SaaS 在通话结束前收到实时文字或拒联,但原验收与跨任务/跨 D 拦截能力将变化,必须单独修订并签收;未产生录音的无应答/忙线等按not_created直接最终收口;录音失败/超时unavailable的有限期限仍待冻结。通话确认终结释放执行资源后释放占用,不等待OSS或MQ结果确认;未知仍占用。现行 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 应用收讫。重复投递、未知执行和恢复不能触发重复拨号。
- 配额覆盖所有 Cell/实例及未知占用;租约过期不自动释放不明通话。控制CAS为 expected_task_revision,pause与stop的drain/hangup区分;paused可按新授权恢复,stopped不可恢复。整体补传仅call_id/source_command_id,禁止新增task/execution补传。最后发起许可、权限和屏障须故障注入。
- 配额覆盖所有 Cell/实例及未知占用;租约过期不自动释放不明通话。**现行 v2** 控制 CAS 为 expected_task_revision,pause 与 stop 的 drain/hangup 区分;paused 可按新授权恢复,stopped 不可恢复。现行整体补传仅 call_id/source_command_id;下一版对外查询/补传目标取消但严格合同/代码未改。最后发起许可、权限和屏障须故障注入。
- **用户确认的下一版精简契约目标(待 SaaS/业务签收,不能混写现行 v2)**:`call.execute.payload` 仅 `task_id/callee`,外呼信封身份仍用于防止重拨;路由/主叫/智能体版本与 `ring_timeout_ms/max_call_duration_ms` 均从已批准的任务配置取得并持久绑定。`task.control` 的 pause/resume/stop 均不带 `command_id` 或 `expected_task_revision`,不设计控制去重,但 D 必须回 task/action/status 处理结果;乱序、控制重投/回执丢失与 stop 后 resume 的判定是 F07 签收阻塞项,不偷偷重引入 CAS/去重掩盖。任务配置新增明确 `caller_profile_id`,按allowed_trunk_ids顺序选首个时段/额度/加载/主叫均匹配的线路,选后固定、不自动换线重拨;有效通话上限取任务与已授权AI两者较小值。旧running配置/发现不得解除已持久的paused/stopped,resume须强制最新任务核验,stopped同ID不可逆。拟定配置响应不再包含 `agent.content_sha256`;SIP 快照/录音 checksum 为另有用途的字段,不误删。参见 `docs/plan-config-read-v0.1.md` 和 `docs/thirds/第三方对接事件与请求消费顺序_v0.1.md`。
- management是SIP配置唯一编辑/审批面。**现行 P1**通过批准的版本化静态制品和受控部署入口交付,D核验目标/准入屏障,Agent加载并报告;不要求在线发布控制面。静态交接合同须批准,旧直接写Agent面不能同时启用;成功必须证明精确快照已被Asterisk加载。
- **用户批准的后续 SIP 配置获取目标(取代此前MQ全量+增量目标)**:management 仍是 SIP 唯一编辑/审批方,SaaS 仅经只读 HTTP 提供该 D 资源分区的获批全量快照;新加入/重启 D 先查询、核验所属 Agent/Asterisk 实际加载的版本/摘要后才消费执行队列。后续约每60秒核对版本,有变化就关执行准入、排空旧活动通话并确认新版已加载;控制MQ照常处理。SaaS/management 分属不同配置系统时须证明SaaS分发的是同一份获批制品;不引入共享业务DB或MQ配置回退。此路径及多 D 不属现行 P1 已实现能力,需新只读HTTP/管理交接合同、资源配额份额和恢复/边界验收,详见 `docs/architecture/Dispatcher有界接纳与控制通道改造计划_v0.1.md` §3.2。
@@ -1,101 +1,51 @@
# Dispatcher 有界接纳与控制通道改造计划 v0.1
状态:**方案评估与后续实施计划;未修改契约、代码或 RabbitMQ 拓扑,未取得新方案的运行验收**。用户确认的原则是:Dispatcher 只是轻量消费者,RabbitMQ 保留尚未接纳的积压,SQLite 不是消息积压的存储兜底;本计划可提出独立控制通道,但其精确合同及 SaaS 接入须另行冻结。本文件是[本轮新计划](../plan-config-read-v0.1.md)的专项设计说明,不替代 `contracts/upstream/v1/`、新计划§8总台账或验收基线;[旧总计划](../archive/plan-0918.md)已归档。
**下一版设计草案,未实施、未获 SaaS 签收。** 唯一计划/台账是[plan-config-read-v0.1](../plan-config-read-v0.1.md);对外结构见[第三方对接](../thirds/第三方对接事件与请求消费顺序_v0.1.md)。本文同步替代此前的 ETag/304、控制 CAS、暂停丢弃旧积压和逐条停止回执方案,不修改现行 `contracts/upstream/v1/`。
## 1. 结论与适用范围
## 1. 目标与范围
“按租户并发上限取一批、执行完删本地记录,再取下一批”**方向正确,但不能直接照做**:应按**当前可用名额**接纳,而不是按名义上限固定取;名额须同时满足租户及共享的供应商、单 Cell、出口、媒体、AI 和执行许可约束。消息 ACK 后可以从 RabbitMQ 移除;SQLite 中的执行归属、未知占用、控制版本、幂等依据、未交付 outbox 和恢复事实不能随呼叫结束立即删除。
单 D、单租户、多任务的未接纳消息留 RabbitMQ,不把整个积压搬入 SQLite。SQLite 仅保存已经接纳的执行、共享额度、停止/暂停屏障、幂等和恢复事实/outbox。已有入库消费路径不能冒称已具备持续有界调度,须按新 F03 用端到端故障验证替换;不加第二层无界本地队列。
仅靠 `prefetch=1` 不解决磁盘积压:当前 `ConsumeTenant` 对 `call.execute` 调用 `IngestCommand`,后者先写 SQLite inbox/task/`command.result(accepted)` outbox,成功 ACK 后立即接收下一条;`Reserve` 是之后的独立步骤。当前启动入口的 `--consume` 分支只启动消费、控制处理与 outbox flush,**未发现持续将已入库执行任务自动 Reserve/下发 Agent 的调度循环**。因此本计划既不能把“已入库”说成“已占用执行名额”,也不能把本地单租户 MQ 测试说成有界接纳验证。当前队列声明及拓扑包未设积压上限。
SaaS 先建持久任务/控制队列和精确绑定,再发布 persistent 消息;mandatory return 与 confirm 都核对。D 无 create/bind/delete 权限,仅按授权任务清单消费,不抢其它 D 任务。任务队列按 D+task_id,租户额度仍按固定 tenant_id↔原值tenant_key 汇总。
**与现有方案的冲突须先消解:**[总体方案 §6.1 第 3 点](./Go重写方案_v0.3.md)写着“临时额度不足进入受限持久等待”;本次用户明确要求未接纳的积压留在 RabbitMQ,不能把该句当作允许无界 SQLite 等待的依据。新合同冻结时应同步修订该条及总计划中相应窗口/ACK 描述,保留已经 ACK 的在途/恢复事实;在一致化前暂停实施受影响的接纳路径,不能用本计划覆盖权威合同。
## 2. 配置、发现与额度
首期只针对**单活 D、一个启用租户、单 Cell**完成安全有界接纳与控制可达;多个租户的独立消费与公平调度为后续范围(历史来源见[旧 W16](../archive/plan-0918.md),本轮状态见[新计划 §8](../plan-config-read-v0.1.md)),不能靠每租户多开 goroutine 声称已实现。本文给出多租户设计约束,不提前开放第二真实租户、多 D 或跨 Cell 配额。
四个 GET 共用 `X-DISPATCHER-id` 与 `X-DISPATCHER-SECRET-KEY`:
## 2. 方案取舍
- `/internal/v1/dispatcher/sip`:本 D 获批 SIP 全量,变更关准入、排空/核验实际加载。
- `/internal/v1/dispatcher/task/:task_id`:任务及AI、tenant_id、tenant_key、明确主叫、候选线路优先级、两项任务超时。
- `/internal/v1/dispatcher/tasks`:启动一致全量/水位,运行每30秒 `?after=<cursor>` 获取变更;cursor非最大任务ID,旧ID变化也返回。
- `/internal/v1/dispatcher/tenant/:tenant_id/quota`:**新增路径/字段待签收**;拿到任务后取该租户分给本 D 的额度、版本、截止时间。额度0不接纳,缺失/过期失败不猜默认值。
| 方案 | 优点 | 不能接受的缺点 / 代价 | 判定 |
SIP/任务完整200响应最多缓存约60秒;租户额度也最多约60秒且不超过有效截止时间。不使用 ETag/304。同租户任务复用一份额度与占用,降额不强挂、不清未知;占用低于新限额后才接新。任务时限和AI授权对话时限取较小值;路由按任务获批候选顺序选首个可用且支持明确主叫的线路,拨号后不得自动换线重试。
## 3. 消费与状态机
所有任务接纳/控制在该任务内串行判定,最后发起仍校验屏障。新 `call.execute.payload={task_id,callee}`,外呼 `command_id` 保留用于重投不重拨;从有效任务/额度快照取得执行参数并持久绑定,事务预留租户+任务+线路/Cell/AI配额后才ACK。事务失败不能ACK或隐藏错误。
| 状态/动作 | 尚未接纳的积压 | 已接纳/在途 | 回传 |
| --- | --- | --- | --- |
| 继续先入 SQLite 再排队 | 接收/控制消息按原队列顺序到达 | ACK 后持续搬运积压,SQLite 可无界增长,违背 Dispatcher 定位 | 不采用 |
| 单一队列按额度停消费,或仅加 `prefetch` / 优先级 | 改动较少,未 ACK 数量有界 | `call.execute` 占住队头时,后面的 `task.control`、查询和 `ai.config.result` 无法及时到达;提高优先级不能抢回已投递/未 ACK 消息 | 不作为安全完成方案 |
| **呼叫接纳与控制/服务消息独立队列** | 呼叫满额留在 MQ,暂停/停止/查询不依赖呼叫名额;与当前 Topic 精确隔离方向一致 | 需新版拓扑/绑定、SaaS 发布端配合和跨队列乱序处理;两条队列及 outbox 仍要容量保护 | **推荐,仅在版本化合同及验证通过后实施** |
| running、有名额 | 有界接收并原子预留;满额不继续搬队列 | 依持久快照执行 | 接纳/拒绝回执,发生执行后最终结果 |
| pause | 关闭准入并暂停消费,已交付未接纳消息退回原队列,不ACK丢弃 | 按drain/hangup | 暂停控制回执;不把暂停积压当作逐条拒绝 |
| resume | 强制读取最新任务,running且授权/额度/时段有效才恢复原队列;过期消息拒绝,未过期正常接纳 | 不改旧快照 | 恢复控制回执;无需SaaS重发原积压 |
| stop | 先持久停止屏障,再小批量消费并ACK,**无拨号、无逐条回执、无最终结果** | 按drain/hangup处理,保留真实执行结果 | stop控制本身有回执;静默只针对尚未接纳消息 |
RabbitMQ 官方文档说明:`prefetch` 只约束未 ACK 交付;队列默认超长行为可能丢弃旧消息,需显式配置 `overflow=reject-publish` 并验证发送方的 publisher confirm/失败保留;官方讨论队头阻塞时建议先考虑多队列。参考:[Consumer Prefetch](https://www.rabbitmq.com/docs/consumer-prefetch)、[Queue Length Limit](https://www.rabbitmq.com/docs/maxlength)、[Priority Support](https://www.rabbitmq.com/docs/priority)。这些是设计参考,不是本项目已完成的 broker 验证。
本地 stopped 不可逆,同任务ID不得再resume;paused只有有效resume可解锁。tasks增量/全量和配置缓存不能用旧running解除屏障,低版本不能覆盖高版本;冲突/HTTP错误关新准入。SaaS先持久修改权威任务状态再发控制;无编号控制不设计去重/请求CAS,重投可能重复回执,不能声称exactly-once。乱序/权威状态不符时明确失败并保持关准入,不能根据旧resume自动重开;控制恢复需要新的有效resume核验。
## 3. 目标接收语义(待合同冻结)
D重启恢复屏障再应用SaaS全量,取更严格状态。stop排空不依赖通话配额/AI配置可用性:额度0、配置过期仍可ACK;ACK丢失重投再次静默处理。静默仍须保留脱敏计数/错误/恢复事实,不能删除其他任务或忽略已在途执行。SaaS在停止新发布、队列积压和未ACK清零、未知执行核清并完成签收的退役核验后才删队列;队列空不是通话结束证明。
```text
SaaS 原任务/可恢复发布记录 → RabbitMQ 某 D/租户的执行队列 → 仅有可用名额才接纳
→ SQLite:原执行身份、占用、恢复和必要事件 → ACK
SaaS 控制/查询、AI 配置响应 → 同 D/租户的独立控制/服务队列 → 持久处理 → ACK
```
## 4. 期限、占用与结果
1. **入口先查历史,后接纳新执行。** 精确验证 Dispatcher/租户/Schema/期限;重复命令恢复原决定,同键异内容拒绝;只有新执行才检查当前控制版本、AI 授权、资源资格及可用名额。未接纳消息仍在 RabbitMQ;不能因重复交付创建第二个任务、许可或拨号。
2. **名额与记录须同一 SQLite 短事务。** 把新执行的 inbox、必要任务/绑定、占用预留、`command.result` outbox 与配额条件更新一起提交后才 ACK;commit 前失败则消息保持可重投。不得以“先数内存 goroutine 再落库”替代事务。实际发起仍须最后许可、与当前版本一致的时段门禁和控制屏障,预留并非拨号授权。
3. **名额不足就停止执行消息的接纳。** 每个执行消费者仅允许有界未 ACK 在途(首期目标不超过 1 条);不反复 `Nack(requeue=true)` 形成热循环,也不无界拉取到内存。空位出现后按原消息继续处理;资源不足/控制关闭/AI 不可用不能偷偷换线、降级或先 ACK 存成本地待执行堆积。消息过期后按已冻结的期限与结果语义处理,不暗中续期或把过期命令重新拨号。
4. **空位不是单一租户计数。** 对特定执行按其需要的全部 scope 原子预留;未知/拨号/振铃/通话中的占用均计入。由于真正所需线路、AI 模式等须在读取消息后确认,队头执行若欠缺某一资源,后面的同队列执行可能等待;本期明确记录队头等待与过期,不以任意跳过/重排伪造公平。如果需求要求同租户内不同资源互不阻塞,须再行冻结路由语义,不能在本方案里偷偷扩充队列。
5. **持久化不等于无上限。** SQLite 保存已接纳/进行中/未知执行、尚需恢复的事实和投递 outbox,并为控制/事件/告警留磁盘余量;容量临界或 DB 不可写时停止新执行准入并告警,不能在失败路径先 ACK。能否继续接收控制取决于其持久化是否仍可用;整库满盘时必须报告不可用,不能保证控制已应用。
6. **执行结束只释放确定的名额。** 未知通话须对账,不能凭超时释放或重新发起;终态与 outbox 交付状态可复核后再按获批准的保留规则清理大字段。保留原执行/命令去重及 stop 墓碑,不把幂等证据按普通日志 TTL 删除。SQLite/WAL/备份占用也应纳入容量核算。
30秒轮询不是端到端发现保证,首条命令还需分页/配置/额度准备;有效期由SaaS覆盖其可接受排队时间,不使用30秒默认值。离线/暂停不延长not_after;过期/窗口外非stopped消息明确拒绝,不等待次日。stopped积压即使过期仍静默ACK。
### 3.1 外呼时段:任务与 SIP 线路双重限制(目标,尚未实施)
只有核实通话终结并释放执行资源才释放通话额度,未知不释放;**不等待录音上传和最终消息确认**。录音上传成功后单份call.result带OSS路径;正常未产生录音(忙线/无应答)以not_created即时终结,不申请上传;应有录音但失败用unavailable,上传失败/超时须签收有界终结期限。未接纳且stopped静默处理没有call.result,其他未接纳拒绝只有回执,不造通话。最终文字大小及无截断策略需F07签收,MQ重投同一事实身份不重拨/重PUT。
用户已明确:目标放行条件是**当前时刻同时落在任务配置的可外呼时段与所选 SIP trunk 的可外呼时段**,由这两项取代现有全局固定 Asia/Shanghai `[09:00, 20:00)`;任何一项缺失、不可确定、未批准或不匹配都不得发起。**任务配置可按周一至周日分别设置多个可外呼时段,并可选排除多个指定日期;任务和线路的时段及排除日期统一按 Asia/Shanghai 解释。**任务某日无允许时段,或当日位于排除日期列表,均不能外呼;排除日期优先于每周重复时段。例如任务周一有 `09:00–11:00`、`14:00–18:00`,线路周一允许 `10:00–16:00`,则该日只有 `10:00–11:00`、`14:00–16:00` 可以发起,若该日被排除则全天不允许。时段仅约束发起呼叫,不等于要求正在进行的通话到点强制挂断;不可把线路不可用视作默认允许或悄悄换线。精确配置字段、区间端点/精度、跨日写法、重复/重叠段校验、任务/线路配置生效版本与有效期**尚无冻结合同**,B1 前先向 SaaS/管理面确认,不按本计划猜填。
取消对外查询/补传与拆分实时文字/拒联/录音事件,不取消内部恢复、日志和外呼幂等。最终通知延迟对拒联业务的影响必须获业务签收。
- SaaS 负责在任务配置层只发布当下有权拨打的命令;Dispatcher 对实际匹配的任务快照、线路配置、授权和当前时刻做独立判断,Agent 在取得最后许可及实际 originate 前按相同权威快照再判断。任一侧门禁失败则拒绝并留可追溯结果,不允许 MQ 投递延迟或 RPC 超时绕过;时钟/配置版本不明就停止发起。改选另一条线路必须依既有路由授权及独立额度,不能为绕过关闭时段而静默换线。
- 任务时段与线路时段**交集为空或当前时刻不在交集**时,不占用执行名额等待下一窗口,不把已经接纳的任务放进 SQLite 排队,也不利用 MQ 留存把同一命令自动推迟到下一天。当前窗口内因名额不足尚留 MQ 的消息,在窗口结束时必须明确终结或过期:未到达消费者的消息也须通过获批准的有效期限、源侧结果/对账和 broker 可观察事实保证**下一个窗口不能直接拨出旧命令**。现有 `not_after` 只表示命令截止,不能独自证明任务/线路时段及跨队列状态;超时后的处置、回执和 SaaS 源任务保留须在 B1 冻结,不能只依赖无报告的 TTL 丢弃。
- 后续版本需要冻结任务按周一至周日逐日多段、可选多日期排除的权威来源/不可变绑定方式、已批准线路制品中的时段、两端可核对的版本、Asia/Shanghai 时间来源和表达规则;核验现有 `call.execute` 与静态线路 Schema(当前都没有时段字段)及 `not_after` 的关系,不在旧严格 Schema 中偷加字段。被更改/撤销的任务或线路版本须使旧准入失效;已接通与未知执行仍按原恢复规则处理,不自动重拨。
- **过渡禁令:**当前代码 `internal/callwindow`、Agent/Dispatcher 实际路径以及现行验收仍只实现 `[09:00, 20:00)`。新版任务/线路时段合同、SaaS 发布侧、配置交接、D/A 最后许可和本地故障/边界测试全部通过并获得相应授权之前,继续执行现有固定门禁;本文不授权在该时间段之外试拨、排队等待或真实放行。验收方案随后同步替换旧固定边界,分别测周一至周日多段/空日、多个排除日期优先、任务开/关、线路开/关、交集起止、跨日边界/时区、排队跨窗口、停用版本、D/A 时钟偏差及控制命令在关窗时仍可处理。
## 5. 开发先后与验收
### 3.2 多 Dispatcher 配置获取与运行中修改(目标,尚未实施)
不再维护另一套 B 工作包。使用总计划:F01(HTTP/额度)与F07(MQ/发现/状态/结果)并行冻结并联合C → F02配置读取 → F03仅此处的消费/屏障 → F04最后许可/时段 → F08结果集成与切换 → F09矩阵验证 → F06发布。F05多D另授权,避免F01/F07互等和F03/F08重复实现。
用户确认:每个 Dispatcher 有独立 ID/专属 MQ 队列,负责**互不重叠**的 Agent/Asterisk 执行资源;同一租户可有多个任务,但一个任务的呼叫/控制只由**一个固定归属的 Dispatcher**消费。任务进行中修改任务、智能体或 SIP 配置时,已接纳/已拨执行保持原快照;尚未接纳的呼叫允许使用**在配置修改后约一分钟内生效**的新版。现行“排队任务固定 AI 快照”和全 MQ 配置合同需重订,旧合同/当前 P1 不因此自动变更。
必须覆盖总计划§5:同租户多任务原子上限/降额/未知、pause积压100条并在resume继续、stop积压100条后originate/outbox均0而已有2通结果保留、重启/ACK丢失、旧running/迟到resume不解锁、任务ID小而版本新、分页/游标失效、D无建队权限、离线期限、无录音、确认终结即释放额度而OSS延迟、完整最终转写超限。
**目标改为两条只读 HTTP 配置接口,业务仍只用 MQ:**
| 接口(用途,正式路径/字段待冻) | 权威数据与读取时机 | 新 D / 配置变更 |
| --- | --- | --- |
| SIP 全量配置 | SaaS 只分发 management 已批准、适用于该 D 独占资源分区的版本化制品;management 仍是唯一编辑/审批面。D 用自己的 UUID+SECRETKEY 获取 | 启动/重启时取全量,获准且 Agent/Asterisk 实际加载相同版本/摘要后才消费新执行;其后约每 60 秒核对是否变更,变化时停执行准入、排空/对账旧使用者、应用并核验新版后恢复。若 management 与 SaaS 不共享同一权威配置,须先证明这条只读分发链不会产生第二份可编辑的权威配置 |
| 任务配置(内含智能体) | SaaS 保留任务、单 D 归属、状态/周时段/排除日及获授权的不可变智能体版本/正文;仅该任务的归属 D 按需读取 | 有待接纳执行时按租户+任务缓存,成功读取/核对后约 60 秒有效;仍有待接纳呼叫的活跃任务到期主动复核,不为每通呼叫下载完整配置;任务确实终结且无在途/未知执行引用后删除**配置缓存**,不删除恢复/幂等/outbox 事实 |
**缓存与后加入 D:**两个接口均须返回足以核对归属、批准状态、配置版本/摘要与生效依据的**完整一致响应**。正式路径、UUID+SECRETKEY 的凭据承载、字段、授权范围、ETag/`If-None-Match` 条件读取及 `304` 的有效期计算由新版合同冻结,不假定现有 SaaS 已提供;凭据由部署受控注入,不出现在源码、配置样例或日志。新 D 不需补听历史配置广播:启动时读已批准 SIP 全量、核验实际加载;收到归属任务的候选呼叫时读取任务及其智能体配置。缓存和**已接纳执行快照**分开保存,不能因“任务结束删缓存”删掉执行、未知占用或未交付 outbox。
**约一分钟延迟的边界:**成功核验的任务/SIP 配置缓存最多约 60 秒有效;SaaS 更新后在此期间允许尚未接纳呼叫使用原批准版本,新版不保证立刻生效。D 对仍有待接纳呼叫的活跃任务和 SIP 在到期时主动条件读取;`304` 可确认未变化并续期,变化须读完整新版本。到期刷新失败、答复不确定或归属/批准版本无法核对,停止接纳新执行、保持原 MQ 命令未 ACK,**不提供 MQ 配置回退或过期缓存兜底**。已经接纳/接通的通话沿用它自己的快照,即使普通配置缓存随后过期或被删除。暂停/停止控制与 opt-out 仍经 MQ 及时处理,不受这一分钟延迟约束;号码白名单、当次已知任务/线路时段和最后发起门禁仍检查。SIP 即使在一分钟内被发现有变化,加载可能需要更长时间;此时先停执行准入、排空/对账旧使用者,确认 Agent/Asterisk 已加载新版本后才恢复,不能宣称一分钟内完成 SIP 发布。超出消息期限或错过可外呼窗口的未接纳呼叫不得在下一时段自动拨出。
**候选执行使用配置:**SaaS 持久保存 `(tenant, task_id) → dispatcher_id`,`call.execute`/控制只送唯一归属 D。D 仅在有执行名额时取候选呼叫、保持有界未 ACK;任务缓存有效则校验归属/状态/允许时段及内含的智能体授权,否则调用任务只读接口取得新的获授权快照。再核对 SIP 批准版本、Agent/Asterisk 实际加载状态、额度和最后许可,在同一 SQLite 事务持久绑定原执行身份、任务+智能体+SIP 版本、占用及 outbox,成功后 ACK。HTTP 超时/失败不回退旧配置、不重拨;SIP 未就绪则停读执行队列,控制仍处理。现有 `call.execute` 的 `task_revision`、`route_policy_id`、`agent_version_id` 固定引用与排队呼叫可换新版的目标冲突,须由新版合同确定旧引用如何重新授权/作废及更新、HTTP 刷新、SQLite 接纳交错的冻结点,不能私改已发布的 MQ 正文。现有 `ai.config.request/result` 仅属旧 MQ 配置合同,在新版本中停止使用而非保留兼容回退。新接口、刷新与这些绑定语义均尚未实现。
**多 D 并不只靠不同 ID:**独立 D 各持 SQLite 配额,对各自 Agent/Asterisk 保持互斥所有权和会话 fencing。一个任务只由单 D 负责,可使**该任务本身**的并发在本地判断;但同一租户的任务可归不同 D,租户总上限或同一 SIP 供应商限额仍可能跨 D。为避免逐呼跨网络算额度,建议由权威预先给各 D 分配有界份额,保证各份额总和不超过共享上限;例如租户总额度 10,可预分 D1=4、D2=6,两边都不能再用“全局 10”独立放行。份额更改须先关受影响准入、核对未决/未知占用和所有权后生效,不能热借用已占额度;这仍是待合同确认的推荐方案,不是现有多 D 配额实现。已有任务指定 D 的 RabbitMQ 队列也不能由新 D 抢收;跨 D 转移未决任务须另行授权、对账与防重拨,不在本计划中假定自动 HA。
## 4. 独立控制通道的安全门槛
现有 `contracts/upstream/v1/mq-topology.json` **只有每 D/租户一条 `.in` 路由及其队列**,`call.execute`、`task.control`、查询、补传和现行 `ai.config.result` 均走该入口。**不得在现有冻结 v1 包外新增 binding、重命名路由或猜测新的消息字段。** 后续由 W01 发布版本化合同:同一 D/租户分别精确绑定执行队列与控制/查询队列;呼叫、控制、查询、上传事实与结果**仍只经 MQ**,任务/智能体/SIP 两类只读配置查询则仅走新 HTTP 接口,不保留 MQ 配置通道。保留原值 `tenant_key`、独立 D 身份、消息大小/路由长度预算和正反例;定义旧 MQ 配置消息如何停发、两端何时切换和旧执行队列如何对账,不让同一执行进入两条队列而双拨。控制通道仍须有独立容量预算、DLQ、发布失败保留和未 ACK 限制。
**仅把控制改道还不够。** 执行可能仍在 RabbitMQ,而 `task.control` 已先从另一队列到达。当前 `HandleTaskControl` 只查 SQLite 中已经存在的任务,找不到会回复 `not_found`:对尚未接纳的 `call.execute`,现实现无法建立阻止后续拨号的屏障。新版合同及实现必须冻结以下次序和责任,未解决前不得宣称控制安全:
- SaaS 停止为被暂停/停止的任务发布新执行,并明确如何处理已经发布但尚未接纳的消息;不能把仅停止新发布当作队列中旧消息已消失的证明。
- D 在控制受理时为对应租户/任务持久建立可用于**未接纳执行**的版本/状态屏障;`expected_task_revision` 如何对不存在于 SQLite 的任务做 CAS、如何返回 accepted/applied/not_found、pause 后如何恢复,须双方正式确定。执行入库前重新检查该屏障,禁止旧 revision 越过最终许可;已接纳执行继续沿原 Agent 屏障对账,不能提前声称 applied。
- 两个队列没有统一顺序:定义同一任务执行与 pause/stop 交错、重复/迟到/重启、过期、DLQ 恢复时的结果和原 ID;stop 墓碑不能因普通清理失效。若无法给出这些语义的新版契约、SaaS 签收及故障测试,则**保持现有合同,不启用新路由、不开放该容量模型的业务准入**。
## 5. Broker 满载与发布端责任
- 分别为执行、控制队列及 DLQ 制定有来源的**条数/字节数上限和磁盘预算**,隔离 broker 上核验队列类型及策略兼容性;超额以拒绝新发布、发送方持久保留原任务/ID为目标,不能使用会丢弃旧队头的默认溢出策略。配置漂移时不 ready,blocked、confirm 丢失、mandatory return 均不得记已交付;不把队列数上限当整个 broker 的磁盘上限。
- SaaS 发布端必须有可恢复源记录,发布失败或结果不确定时按原消息身份查询/重试;过了 `not_after` 不能偷偷延长原命令期限或换执行 ID 重拨,需按批准的业务规则终结/另行授权。若 SaaS 不具备上述能力,则总容量只是“把故障位置移给 MQ”,不满足交付条件。
- D 自身的结果 outbox 也需按未确认数量、最老年龄及磁盘余量监控;不得为保护入站容量而丢弃 `command.result`、控制或录音事实。只在可证明的健康及水位恢复后重新开启执行准入,避免恢复时突发抢占。
## 6. 工作包与验收门禁
| 顺序 / 映射 | 产出与前置 | 可验证的完成条件 |
| --- | --- | --- |
| B0 事实与预算(W00/W13) | 从真实单租户负载/profile 核定可用并发、活跃任务数/约60秒配置刷新请求量、SIP 全量大小、SQLite/WAL/outbox保留和 broker 容量;确认 SaaS 发布失败与停发责任;缺数写 blocked,不套用旧 DEV/P2 数值当生产预算 | 受批准的容量来源、磁盘余量与告警/恢复条件;现状对照记录,不宣称新方案已运行 |
| B1 接口冻结(W01/W05/W12/W08) | 发布**新版本**两条只读 HTTP 配置接口合同及 MQ 业务拓扑/必要 Schema、正反例/哈希;明确每 D 的 UUID+SECRETKEY 受控注入及归属、SIP 批准制品来源、任务内嵌智能体获授权快照、版本/摘要/ETag、最大约60秒生效滞后/到期失败行为、原 `call.execute` 修订引用如何重新授权及并发冻结点。同步修订旧“SQLite 有界等待”“排队 AI 固定版本”和全 MQ 配置规则,撤除旧 AI 配置 MQ 路径而不设兼容回退;保留执行/控制精确绑定与未接纳任务控制屏障、每 D 资源/共享额度份额及旧队列对账,冻结任务/线路时段、期限和旧全局门禁切换;与 SaaS/management 分别核对签收 | 新旧合同不混用、路由预算与 D1/D2/租户隔离、两接口冷启动/条件刷新/失败边界及未接纳新版绑定正反例通过;本阶段文档不是 B1 已完成证据 |
| B2 先写失败测试(W05/W08) | 用隔离 RabbitMQ/SQLite 与只读配置接口 Mock 构造满额/宕机、任务/线路交集和跨关窗滞留;覆盖任务/智能体变更后0/60秒、条件读取`304`/新`ETag`、HTTP超时/不可用、任务错归D1/D2、两D同租户额度份额、SIP冷启动/重启/加载失败、任务终结清缓存及修改与接纳竞态;随后将接收和原子额度预留合并到同一事务 | 超量消息不搬入 SQLite;一任务只在归属D接纳、份额总和不超全局;超期缓存或SIP未实际加载不接新呼叫,已接纳快照不变,停/暂停不等缓存;commit/ACK 丢失不重拨,关窗旧消息下一窗口不拨号 |
| B3 控制/服务通道(W05/W08/W07/W12) | SaaS 按新版 MQ 业务绑定发布;D 独立消费控制、查询/补传,持久前不 ACK;任务/智能体/SIP 配置由只读 HTTP 拉取,无 MQ 配置结果;对未接纳任务保存控制屏障并在执行接纳/最后许可校验 | 执行队列满时 stop/pause 可受理并正确 applied/reconciling,旧消息不会拨号;`not_found`/CAS/乱序/重启含未知执行得到合同定义结果;不改 OSS 上传事实路径、不保留配置 MQ 回退 |
| B4 发布背压与清理(W05/W12/W13) | 设置 broker 限额/拒绝策略,验证 SaaS 原身份保留/重试;加入本地高水位、控制预留空间、outbox 年龄及保留/压缩策略;缓存过期刷新失败不接新执行,任务终结后仅清可再获取的配置缓存 | 消息爆量时 RabbitMQ 不丢旧命令、发布方不丢源任务;DB 满盘或 HTTP 配置失效时停止执行接纳,持久事实仍可追溯;控制若无法持久化明确阻塞,不伪报 applied |
| B5 本地合并验证(W13/W14;W16 后续) | 同一基线跑 HTTP 两接口合同/缓存校验、MQ 业务合同、broker 故障/重启、DB 恢复、期限、任务×线路时段、旧配置 MQ 路径停用和持续负载;多租户公平另按 W16 开展 | 覆盖现有 C05/C11、S01–S03/S08–S15/S17/S19–S22/S24/S25/S27 的适用子场景;验证双时段边界、配置最多约60秒滞后/过期拒绝、未接纳任务跨窗口、最后发起和控制关窗并更新验收基线;本地证据独立成文,不把它写成生产/SaaS 签收或 W16 通过 |
代码阶段坚持 TDD,并执行项目要求的格式、`go vet ./...`、`go test -race ./...`、`go build ./...`、合同/Proto/本地 MQ 集成检查;按现有覆盖率门禁验收。B1 以前只做事实收集及方案澄清,不编辑现有冻结合同或抢先启用分流。每一批代码合并后重新验证同一套执行、控制和 outbox 故障路径。
## 7. 不做与待批准事项
本计划**不授权**真实 SaaS/生产 broker 修改、部署、真实外呼或费用;不实现多租户公平、跨 Cell/多 D 动态协调、另一个数据库、Redis 兜底、配置 HTTP↔MQ 故障回退或重拨。计划实施前仍需明确:SaaS 对两只读接口、任务内嵌智能体的授权/版本及最多约60秒延迟、原命令引用如何解除/冻结和新版控制语义的签收;management 对 SaaS SIP 已批准只读分发与实际加载回执的签收;每 D 独占资源及共享额度份额、真实容量预算、执行与控制队列上线/排空顺序、完成任务保留期限及源记录保障。任一关键前置缺失,相应工作包标记 blocked;不能以“暂停消费”代替已达成控制屏障。
依据:[现行 MQ 机器合同](../../contracts/upstream/v1/mq.schema.json)、[事件正文](../../contracts/upstream/v1/event-payloads.schema.json)、[第三方顺序说明](../thirds/第三方对接事件与请求消费顺序_v0.1.md)、[总体方案 §6.1–6.2](./Go重写方案_v0.3.md)、[验收 §4/§9](../acceptance/验证与切换验收_v0.3.md)、[唯一机读拓扑](../../contracts/upstream/v1/mq-topology.json)。
代码阶段遵守TDD,至少gofmt、`go vet ./...`、`go test -race ./...`、构建、本模块覆盖率≥65%;本轮只修订文档/项目草案,不记为这些验收已执行。不触发真实外呼、生产SaaS或云资源操作。
@@ -1,4 +1,4 @@
# 两条只读配置接口:任务、智能体、SIP 字段与返回结构 v0.1(项目提案)
# 只读配置与租户额度接口:任务、智能体、SIP 字段与返回结构 v0.1(项目提案)
**状态:项目自定义草案,非 SaaS 已有接口/实际 JSON、非已发布契约、非实现/验收。** 用户同意:截图可见的业务含义先映射为**项目定义的字段名**;截图没有但需求明确的结构由本项目设计。即使字段名与现有项目 Schema 或历史 OpenAPI 相同,也**不能**据此声称它是当前 SaaS 页面原有的后端键。SaaS 和 management 在 W01 签收前不得据此开始真实配置发布/拨号。
@@ -8,20 +8,21 @@
- **C = 本项目现有合同:**[AI 配置](../../contracts/upstream/v1/ai-config.schema.json)、[静态 Cell/SIP 制品](../../contracts/upstream/v1/static-cell-artifact.schema.json)及[当前 MQ 消息](../../contracts/upstream/v1/mq.schema.json)。字段语义可复用,但**不是 SaaS 当前 HTTP 响应证据**。旧 [OpenAPI/MQ 只读索引](../references/OpenAPI与MQ字段索引_v0.1.md) 也仅为历史参考。
- **N = 新项目字段:**任务每周多时段/排除日期、SIP 线路时段、任务与单 D 绑定、缓存/版本/错误返回等,由本提案定义;实际 SaaS 接口不存在已验证响应。
交付物:[机器可读响应草案](config-read-v0.1.schema.json)、[mock SIP 成功示例](examples/config-read-sip-v0.1.json)、[mock 任务成功示例](examples/config-read-task-v0.1.json)。两条接口均为 **SaaS→Dispatcher 的只读 HTTP 配置读取**;MQ 继续单独承担呼叫、控制、查询、上传事实与业务结果,不留旧 AI/SIP 配置 MQ 回退。**接口路径、UUID+SECRETKEY 承载位置、错误码的准确枚举、哈希计算规范及 SaaS/management 实际来源仍需 W01 冻结**,本文件不冒充外部权威发布物。
交付物:[机器可读响应草案](config-read-v0.1.schema.json)、[mock SIP 成功示例](examples/config-read-sip-v0.1.json)、[mock 任务成功示例](examples/config-read-task-v0.1.json)、[mock 租户额度示例](examples/config-read-tenant-quota-v0.1.json)。前两条接口为**拟定的只读 HTTP 配置读取**:`GET /internal/v1/dispatcher/sip` 与 `GET /internal/v1/dispatcher/task/:task_id`;下一轮另拟 `GET /internal/v1/dispatcher/tasks` 和 `?after=<cursor>` 发现归属任务;新增项目拟定 `GET /internal/v1/dispatcher/tenant/:tenant_id/quota` 按任务的租户 ID 读取本 D 的租户份额。四条请求携带 `X-DISPATCHER-id`(D UUID)及 `X-DISPATCHER-SECRET-KEY`(受控密钥),无请求体;实际 SaaS 实现仍待签收。业务呼叫/控制及回执/最终结果继续走 MQ,目标版本移除对外查询/补传;不留旧 AI/SIP 配置 MQ 回退。**HTTP 响应错误码、SIP 快照规范及 SaaS/management 实际来源仍需 W01/F07 冻结**,本文件不冒充外部权威发布物。
## 2. 请求与共同响应
Dispatcher 使用其**全局唯一 UUID**和部署受控的 **SECRETKEY**,仅查询自己管理的资源分区和归属任务;不记录完整密钥或返回配置中的提示词。接口只读、无启动/停止/修改副作用。准确 URL、请求参数/头、SaaS 对任务归属的核验、密钥轮换时序由双方签收;本文仅固化响应的**项目字段草案**,不推测 SaaS 已有 API 路径。
Dispatcher 仅用 `X-DISPATCHER-id`(全局唯一 UUID)和部署受控的 `X-DISPATCHER-SECRET-KEY` 请求归属资源;不记录密钥或在日志中打印配置提示词。接口只读、无控制副作用;SaaS 必须核验任务归属。Header 的准确校验、密钥轮换时序、HTTP 错误状态仍需 SaaS 签收。`GET /internal/v1/dispatcher/tasks`(含 `?after=<cursor>`)是**第三条待 F07 签收的任务发现接口**,其快照/变更示例见[第三方对接 §2.5](../thirds/第三方对接事件与请求消费顺序_v0.1.md),不混入本文件的配置响应 Schema;第四条租户额度响应属于本文件新增草案,路径/字段未获 SaaS 签收。
| 请求 | `200` 返回类型 | 何时读取 | `304` 与错误 |
| 请求 | `200` 返回类型 | 何时读取 | 错误处理 |
| --- | --- | --- | --- |
| SIP 全量配置 | `resource=sip_config`,一个 D 资源分区的已批准完整快照 | 新 D/重启先取齐并核对 Agent/Asterisk 精确加载;运行中约每 60 秒核对版本 | 未变且仍获批准可 `304`(空响应体);读取错误/到期停止新执行准入,旧活动通话依原快照排空 |
| 单任务配置(内含智能体) | `resource=task_config`,归属 D 的单任务有效配置和已授权智能体快照 | 有待接纳呼叫时获取,活跃任务缓存约 60 秒、到期主动复核,不逐呼下载整份配置 | 未变且授权仍有效可 `304`;失败/过期不以旧缓存放行新执行;已接纳执行仍用原快照 |
| `GET /internal/v1/dispatcher/sip` | `resource=sip_config`,本 D 的已批准完整 SIP 快照 | 新 D/重启先取齐并核对 Agent/Asterisk 精确加载;运行中约每 60 秒读取完整配置 | 读取失败/到期停止新执行准入,旧活动通话依原快照排空 |
| `GET /internal/v1/dispatcher/task/:task_id` | `resource=task_config`,归属 D 的单任务配置和已授权智能体快照 | 有待接纳呼叫时获取,活跃任务缓存约 60 秒;resume 必须重取最新配置,不能靠旧 running 恢复 | 失败/过期不放行;已接纳执行仍用原快照 |
| `GET /internal/v1/dispatcher/tenant/:tenant_id/quota`(新增路径草案) | `resource=tenant_quota`,分给该 D 的租户并发份额 | 拿到任务 tenant_id 后读取,同租户任务共享,缓存最多约60秒且不超过有效截止 | 缺失/过期/错身份停该租户新准入,额度0不影响stop静默排空 |
`200` 响应中的 `schema_version` 固定 `config-read.v0.1`,`resource` 区分两种结构,`dispatcher_id` 必须等于请求 D;HTTP `ETag` 标识**完整有效决策**,并非某个 UI 表单的草稿版本。`If-None-Match` 命中时 `304` **没有 JSON 响应体**;只有此前的批准状态、任务归属及 AI 授权仍足以覆盖下一次缓存有效期,才可延长本地缓存,否则返回 `200` 新快照或明确失败。SaaS 实际更新到 D 允许最多约 60 秒延迟;一旦缓存到期且刷新失败,**不可无限期沿用旧版本发起新呼叫**。MQ 停/暂停、opt-out 不等这 60 秒。已接纳/已接通呼叫固定自己的快照,不因本地配置缓存过期而变更。
`200` 响应中的 `schema_version` 固定 `config-read.v0.1`,`resource` 区分 SIP、任务及租户额度结构,`dispatcher_id` 必须等于 Header 中的 D。**不使用条件请求、ETag 或 `304`**:到期时重新 GET 完整响应;只有收到、验证并重新确认授权有效后才更新缓存。SaaS 变更到 D 的目标延迟约 60 秒;缓存到期且刷新失败,**不可无限期沿用旧版本发起新呼叫**。MQ 停/暂停不等待这 60 秒。已接纳/已接通呼叫固定自己的快照,不因缓存过期而漂移。
非 `200/304` 返回 `resource=error`、`error.code`、`error.message` 的脱敏 JSON(HTTP 状态及 code 逐项待 SaaS 确认);不得将错误吞成旧配置/空任务。原始 `call.execute` 的 `task_revision`、`agent_version_id`、`route_policy_id` 与后续新版本可能不同;如何让未接纳执行合法采用新版本、变更与接纳并发时何时冻结版本,**尚需版本化 MQ/HTTP 合同约定,不可静默改写原命令**。
非 `200` 返回 `resource=error`、`error.code`、`error.message` 的脱敏 JSON(HTTP 状态及 code 逐项待 SaaS 确认),不得吞成旧配置/空任务。下一版 `call.execute.payload` **只有 `task_id` 与 `callee`**;D 从已批准的任务快照读取路由/主叫/智能体版本及任务级 `ring_timeout_ms/max_call_duration_ms`,接纳前持久绑定完整快照,不能从精简命令中猜值或悄悄采用过期缓存。现行严格 MQ Schema 尚未修改。
## 3. 任务成功响应:字段与来源
@@ -29,18 +30,20 @@ Dispatcher 使用其**全局唯一 UUID**和部署受控的 **SECRETKEY**,仅
| 返回位置 | 类型 / 是否必有 | 含义及来源 |
| --- | --- | --- |
| `dispatcher_id`, `tenant_key`, `task_id`, `task_revision` | UUID v4、原值字符串、字符串、正整数;必有 | C:当前命令及每 D/租户身份;N:任务固定归属一个 D,SaaS 必须持久保存 `(tenant_key,task_id)→dispatcher_id`;不得跨 D 投递。`tenant_key` 需另验证 **≤196 UTF-8 字节**及路由段边界,Schema 的字符数不是字节数。 |
| `dispatcher_id`, `tenant_id`, `tenant_key`, `task_id`, `task_revision` | UUID v4、租户ID、原值租户键、任务ID、正整数;必有 | C:当前命令及每 D/租户身份;N:任务固定归属一个 D,SaaS 必须持久保存 `(tenant_key,task_id)→dispatcher_id`;不得跨 D 投递。tenant_id 与原值 tenant_key 一对一映射,取得任务后按 tenant_id 读取本 D 租户额度,不能从 task_id 猜。`tenant_key` 需另验证 **≤196 UTF-8 字节**及路由段边界,Schema 的字符数不是字节数。 |
| `status` | `running/paused/stopped/finished`;必有 | P:页面可见启停状态;N:面向 D 的状态枚举是本项目暂定,不承诺与页面/实际 API 状态值同名。停/暂停需配合 MQ 控制屏障,不能只靠缓存。 |
| `name`, `group_id` | 非空字符串、字符串或 `null`;必有 | P:任务名称/所属分组。显示信息不参与拨号许可;`null` 与空字符串不混同。 |
| `max_concurrent_calls` | 正整数;必有 | P:任务并发。仅限制**该任务**,另须服从 D 的租户份额、供应商、Cell 和 AI 配额。截图里的“5”是某次输入,不是默认值。 |
| `route_policy_id`, `allowed_trunk_ids[]` | 非空字符串、至少一条线路 ID;必有 | C:`route_policy_id` 已在命令;P:选择启用线路。N:`allowed_trunk_ids` 是本项目定义的白名单,须与已加载 SIP 制品的 `trunk_id` 对齐;不能把任务页面“每条线路数量”直接猜成运营商并发上限。 |
| `max_concurrent_calls`, `ring_timeout_ms`, `max_call_duration_ms` | 正整数、正整数毫秒、正整数毫秒;必有 | P:任务并发;N:振铃及最长通话时间是**任务配置**,所有新接纳呼叫由同一获准任务快照取得,不由逐呼命令任意覆盖。通话有效上限取任务 max_call_duration_ms 与已授权 AI conversation.max_duration_ms 较小值,执行侧/AI控制器一致且不改原快照;并发另受租户份额、供应商/Cell/AI约束;截图里的“5”不是默认值。 |
| `route_policy_id`, `caller_profile_id`, `allowed_trunk_ids[]` | 路由标识、明确主叫引用、有序候选线路数组;必有 | N:route_policy_id标识本任务规则,不另引入未定义查询;按候选顺序选首个已加载、时段/额度有效且支持此主叫引用的线路,无匹配不接纳。主叫不默认取首个,线路/主叫选择后持久绑定,拨号失败/未知不自动换线。 |
| `schedule.time_zone`, `starts_at`, `ends_at` | 固定 `Asia/Shanghai`、带偏移时间或 `null`;必有 | P:任务起止时间;N:三字段格式/无值约定。时间约束与星期段、排除日期、线路时段**同时成立**。 |
| `schedule.weekly_windows` | 七个星期键各为可空的时间段数组;必有 | P:周一至周日网格、同日多个时段;N:`{start,end}` 用 `HH:MM`,左闭右开,`start < end`,跨午夜拆到次日,不假定 UI 已有这个 JSON 结构。空数组=当天不可呼。 |
| `schedule.excluded_dates[]` | 不重复的 `YYYY-MM-DD` 数组;必有,可为空 | N:用户新增的可选多日期排除(截图**没有**此字段)。日期按 Asia/Shanghai 判断并优先于星期段;真实日期、时段排序/重叠与边界须在业务校验中处理。 |
| `agent.agent_version_id`, `content_sha256`, `config` | ID、64位小写 hex、严格 AI 对象;必有 | P:任务选择 AI 模型/智能体;C:现有不可变 `agent_version_id`、`content_sha256` 和[AI Schema](../../contracts/upstream/v1/ai-config.schema.json)。`config` 原样遵守该现有 Schema,不把截图未覆盖参数偷塞 `metadata`。 |
| `agent.agent_version_id`, `config` | ID、严格 AI 对象;必有 | P:任务选择 AI 模型/智能体;C:现有不可变 `agent_version_id` 和[AI Schema](../../contracts/upstream/v1/ai-config.schema.json)。不再返回 `content_sha256`;同版本不得变内容的检查应基于版本绑定及本地持久快照,不能悄悄接受漂移。`config` 原样遵守该现有 Schema,不把截图未覆盖参数偷塞 `metadata`。 |
| `agent.authorization_id`, `authorization_expires_at` | 非空 ID、带偏移时间;必有 | C:当前 MQ AI 授权含关联 ID 和有效期;N:嵌入任务 HTTP 响应的承载位置新设计,过期不可新接纳。 |
`agent.config` 当前可承载的**运行字段**:`mode`;ASR 的 `provider_ref/model/language/interim/input/timeout_ms`;LLM 的 `provider_ref/credential_ref/model/temperature/max_tokens/timeout_ms`;`prompt.text/allowed_variables/max_bytes`;TTS 的 `provider_ref/credential_ref/model/voice/speed/format/timeout_ms`;`conversation.opening/allow_interrupt/silence_timeout_ms/max_duration_ms/max_turns/sentence_max_chars/max_pending_audio_chunks` 等以**现有 AI Schema 本身为准**。`asr_only` 与 `full_ai` 两种模式均须按 Schema/SDK 能力分别校验;提示词不得出现在示例或日志中的真实用户文本。`agent.agent_version_id` 必须等于 `agent.config.agent_version_id`;`content_sha256` 的摘要生成规范/与现有 AI 授权摘要对齐前不可声称验签通过。
`agent.config` 当前可承载的**运行字段**:`mode`;ASR 的 `provider_ref/model/language/interim/input/timeout_ms`;LLM 的 `provider_ref/credential_ref/model/temperature/max_tokens/timeout_ms`;`prompt.text/allowed_variables/max_bytes`;TTS 的 `provider_ref/credential_ref/model/voice/speed/format/timeout_ms`;`conversation.opening/allow_interrupt/silence_timeout_ms/max_duration_ms/max_turns/sentence_max_chars/max_pending_audio_chunks` 等以**现有 AI Schema 本身为准**。`asr_only` 与 `full_ai` 两种模式均须按 Schema/SDK 能力分别校验;提示词不得出现在示例或日志中的真实用户文本。`agent.agent_version_id` 必须等于 `agent.config.agent_version_id`;授权身份及截止时间必须单独核验,不通过额外 `content_sha256` 字段证明授权。
新call.execute没有逐呼variables来源;需要未提供变量的提示词必须拒绝或在F01先补获批来源,不能填空继续执行。状态来源按总计划§3.3:stopped不可逆、paused只能经最新有效resume解除;任务缓存和tasks增量的旧running不能解锁。
### 3.1 页面观察但不作为 D 运行字段
@@ -73,12 +76,25 @@ Dispatcher 使用其**全局唯一 UUID**和部署受控的 **SECRETKEY**,仅
静态制品中的端点**引用**与此提案补充的对端**值**属于同一获批版本;若 SaaS 与 management 并非同一配置来源,必须证明它们同步一致,且 Agent/Asterisk 实际加载的版本/摘要匹配。`artifact.load_evidence` 为 `null` 只表示 HTTP 获得配置,**不代表 Asterisk 已加载**;D 必须另从实际执行侧核验。样例 `mode=mock`、`transport/auth_mode/registration_required/max_concurrent_calls=null` 是故意保留的供应商待确认项;**不满足 real 放行**。只提供SaaS读接口却不能取得已批准的端点及线路时段,不得声称已经提供了完整 SIP 配置。
## 4.1 租户额度响应(新增字段,N,未签收)
| 字段 | 类型/约束 | 业务语义 |
| --- | --- | --- |
| schema_version/resource | config-read.v0.1 / tenant_quota | 项目草案,不是现网版本。 |
| dispatcher_id/tenant_id/tenant_key | D UUID、租户ID、原值租户键,必有 | 与请求、任务、信封一致,SaaS一对一映射;错误不猜值。 |
| quota_revision | 正整数,必有 | 本D租户额度版本,旧版本不覆盖新分配。 |
| max_concurrent_calls | 非负整数,必有 | 本D同租户所有任务共用份额,0禁止新呼叫;不是每任务分别上限。 |
| valid_until | RFC3339时间,必有 | 截止后不得新准入,本地缓存最多约60秒且不得越过此时刻。 |
D 在同一事务预留租户/任务/线路等占用,未知继续计入;降额不强挂、占用低于新上限才再接新。额度缺失、过期/错身份、刷新失败关闭新准入,不能以任务额度代替。已确认通话终结/执行资源释放即可释放通话额度,不等录音上传或MQ确认;stop静默ACK不需申请通话名额。多D须由SaaS分份额,累计不超过租户总额;本轮只验证单D。
## 5. 需冻结的语义与验收前置
1. **来源:**确认 SaaS 的任务、智能体是该服务的权威配置;management 仍是唯一 SIP 编辑/审批方。确认 SaaS 分发的是已批准制品与线路补充字段同一版本,不能出现两份可写配置。
2. **身份和响应:**D 的 UUID+SECRETKEY 只能读取获分配的资源分区及任务;不在日志/示例保存真实密钥。精确 HTTP 路径、请求参数/头、鉴别方式、状态码、`ETag` 粒度与生效时间由 SaaS 正式签收。
3. **窗口与版本:**缓存成功核验起约 60 秒;SaaS 变更对未接纳呼叫最多约 60 秒延迟,过期刷新失败停止新准入,已接纳保留原快照。更新/SIP 加载期间停执行队列,不停控制 MQ;停/暂停与 opt-out 不等缓存。`304` 不得延长已撤销或即将过期的 AI 授权。跨日窗口、重叠段、当日排除、时间边界以及任务与线路交集需在业务校验并在新合同定稿时冻结。
4. **一致性:**`agent_version_id`、摘要、AI 授权一致且有效;任务归属/修订/route policy 与已发布命令如何重新授权;`artifact.trunks` 与 `trunk_details` 一一对应;线路 status、主叫、前缀、媒体、线路/租户/供应商额度来源和 Agent/Asterisk 实际加载不可依赖 JSON Schema 单独判断。供应商未知传输/鉴权/注册不得默认允许 real。
5. **退出与过渡:**新接口上线前现行 MQ-only/单 D/固定时段/静态 SIP 仍有效。切到新版本后配置只走 HTTP,旧 `ai.config.request/result` 停用,不做 HTTP→MQ 回退;业务 MQ 正常运行。多 D 任务归属/共享额度份额、旧命令/缓存/恢复记录和控制屏障须单独验证;任务结束只删除配置缓存,不删除未决执行与消息事实。
2. **身份和响应:**约定请求头 `X-DISPATCHER-id` 与 `X-DISPATCHER-SECRET-KEY`;只读取归属 D 的 `/internal/v1/dispatcher/sip`、`/internal/v1/dispatcher/tasks` 、`/internal/v1/dispatcher/task/:task_id` 和新增拟定 `/internal/v1/dispatcher/tenant/:tenant_id/quota`,不在日志/示例保存真实密钥。具体身份校验、状态码和生效时间仍须 SaaS 签收;不采用 `ETag`/`304`。
3. **窗口与版本:**缓存成功核验起约 60 秒;SaaS 变更对未接纳呼叫最多约 60 秒延迟,过期重新 GET 完整数据失败就停止新准入,已接纳保留原快照。更新/SIP 加载期间停执行队列,不停控制 MQ;停/暂停不等缓存。没有 `304` 延长授权的通道。跨日窗口、重叠段、当日排除、时间边界及任务与线路交集须在合同冻结。
4. **一致性:**`agent_version_id` 与 AI 授权一致且有效,同版内容漂移必须拒绝;任务归属/修订/route policy 以已绑定任务快照为准,MQ 命令不得覆盖;`artifact.trunks` 与 `trunk_details` 一一对应;线路 status、主叫、前缀、媒体、线路/租户/供应商额度来源和 Agent/Asterisk 实际加载不可依赖 JSON Schema 单独判断。供应商未知传输/鉴权/注册不得默认允许 real。
5. **消费状态:**pause保留原队列积压,resume最新配置/授权/额度有效才继续消费,无需SaaS重新投递;暂停不延长not_after。stop后未接纳积压静默消费ACK,不拨号、不发逐条回执/最终结果;控制本身与已在途通话结果仍回传,本地计数/错误不静默。状态优先级及例外按总计划§3.3,不加控制去重。
6. **退出与过渡:**新接口上线前现行 MQ-only/单 D/固定时段/静态 SIP 仍有效。切到新版本后配置只走 HTTP,旧 `ai.config.request/result` 停用,不做 HTTP→MQ 回退;业务 MQ 正常运行。多 D 任务归属/共享额度份额、旧命令/缓存/恢复记录和控制屏障须单独验证;任务结束只删除配置缓存,不删除未决执行与消息事实。
**验证状态:**本地 JSON Schema 草案可校验两个 mock `200` 示例及非法样例;它**不能**证明真实 SaaS 接口字段名、管理平台签收、hash 规范、Agent SDK 映射、SIP 实际加载或任何生产外呼验收。
**验证状态:**本地 JSON Schema 草案覆盖 SIP、任务和新增租户额度 `200` 示例及错误/非法样例;它**不能**证明真实 SaaS 接口字段名、管理平台签收、hash 规范、Agent SDK 映射、SIP 实际加载或任何生产外呼验收。
+22 -3
View File
@@ -5,6 +5,7 @@
"oneOf": [
{"$ref": "#/$defs/sip_response"},
{"$ref": "#/$defs/task_response"},
{"$ref": "#/$defs/tenant_quota_response"},
{"$ref": "#/$defs/error_response"}
],
"$defs": {
@@ -29,11 +30,12 @@
"task_response": {
"type": "object",
"additionalProperties": false,
"required": ["schema_version", "resource", "dispatcher_id", "tenant_key", "task_id", "task_revision", "status", "name", "group_id", "max_concurrent_calls", "route_policy_id", "allowed_trunk_ids", "schedule", "agent"],
"required": ["schema_version", "resource", "dispatcher_id", "tenant_id", "tenant_key", "task_id", "task_revision", "status", "name", "group_id", "max_concurrent_calls", "ring_timeout_ms", "max_call_duration_ms", "route_policy_id", "caller_profile_id", "allowed_trunk_ids", "schedule", "agent"],
"properties": {
"schema_version": {"const": "config-read.v0.1"},
"resource": {"const": "task_config"},
"dispatcher_id": {"$ref": "#/$defs/dispatcher_id"},
"tenant_id": {"type": "string", "minLength": 1, "maxLength": 128},
"tenant_key": {"type": "string", "minLength": 1, "maxLength": 196, "$comment": "Also validate <=196 UTF-8 bytes and routing word boundaries using the project-owned tenant contract."},
"task_id": {"type": "string", "minLength": 1, "maxLength": 128},
"task_revision": {"type": "integer", "minimum": 1},
@@ -41,12 +43,30 @@
"name": {"type": "string", "minLength": 1, "maxLength": 256},
"group_id": {"type": ["string", "null"], "maxLength": 128},
"max_concurrent_calls": {"type": "integer", "minimum": 1},
"ring_timeout_ms": {"type": "integer", "minimum": 1},
"max_call_duration_ms": {"type": "integer", "minimum": 1},
"route_policy_id": {"type": "string", "minLength": 1, "maxLength": 128},
"caller_profile_id": {"type": "string", "minLength": 1, "maxLength": 128},
"allowed_trunk_ids": {"type": "array", "minItems": 1, "maxItems": 32, "uniqueItems": true, "items": {"type": "string", "minLength": 1, "maxLength": 128}},
"schedule": {"$ref": "#/$defs/task_schedule"},
"agent": {"$ref": "#/$defs/agent"}
}
},
"tenant_quota_response": {
"type": "object", "additionalProperties": false,
"required": ["schema_version", "resource", "dispatcher_id", "tenant_id", "tenant_key", "quota_revision", "max_concurrent_calls", "valid_until"],
"properties": {
"schema_version": {"const": "config-read.v0.1"},
"resource": {"const": "tenant_quota"},
"dispatcher_id": {"$ref": "#/$defs/dispatcher_id"},
"tenant_id": {"type": "string", "minLength": 1, "maxLength": 128},
"tenant_key": {"type": "string", "minLength": 1, "maxLength": 196},
"quota_revision": {"type": "integer", "minimum": 1},
"max_concurrent_calls": {"type": "integer", "minimum": 0},
"valid_until": {"type": "string", "format": "date-time"}
},
"$comment": "PROPOSAL: SaaS-assigned share for this Dispatcher; aggregate all tasks of the tenant. Validate tenant_id/tenant_key mapping and expiry in business checks."
},
"error_response": {
"type": "object",
"additionalProperties": false,
@@ -96,10 +116,9 @@
},
"agent": {
"type": "object", "additionalProperties": false,
"required": ["agent_version_id", "content_sha256", "authorization_id", "authorization_expires_at", "config"],
"required": ["agent_version_id", "authorization_id", "authorization_expires_at", "config"],
"properties": {
"agent_version_id": {"type": "string", "minLength": 1, "maxLength": 128},
"content_sha256": {"$ref": "#/$defs/sha256"},
"authorization_id": {"type": "string", "minLength": 1, "maxLength": 128},
"authorization_expires_at": {"type": "string", "format": "date-time"},
"config": {"$ref": "https://go-sip.local/contracts/v1/ai-config.schema.json"}
@@ -2,6 +2,7 @@
"schema_version": "config-read.v0.1",
"resource": "task_config",
"dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6",
"tenant_id": "tenant-id-mock",
"tenant_key": "tenant-mock",
"task_id": "task-mock",
"task_revision": 2,
@@ -9,7 +10,10 @@
"name": "Mock task",
"group_id": null,
"max_concurrent_calls": 2,
"ring_timeout_ms": 30000,
"max_call_duration_ms": 120000,
"route_policy_id": "route-mock",
"caller_profile_id": "caller-profile-mock",
"allowed_trunk_ids": ["trunk-mock"],
"schedule": {
"time_zone": "Asia/Shanghai",
@@ -28,7 +32,6 @@
},
"agent": {
"agent_version_id": "agent-version-mock",
"content_sha256": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
"authorization_id": "auth-mock",
"authorization_expires_at": "2026-09-21T18:00:00+08:00",
"config": {
@@ -0,0 +1,10 @@
{
"schema_version": "config-read.v0.1",
"resource": "tenant_quota",
"dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6",
"tenant_id": "tenant-id-mock",
"tenant_key": "tenant-mock",
"quota_revision": 1,
"max_concurrent_calls": 3,
"valid_until": "2026-09-21T18:00:00+08:00"
}
+98 -61
View File
@@ -1,91 +1,128 @@
# Go SIP Agent:配置读取与有界外呼迭代计划 v0.1
**当前迭代唯一执行入口;状态:字段提案已建立,新 HTTP 合同/实现/验收均未完成。** 本文接替已恢复原始内容并归档的 [plan-0918.md](archive/plan-0918.md)。旧计划只作历史证据/背景,不能把旧 W01–W16 的本地通过数写成新接口、多 Dispatcher 或真实 SaaS 的验收。分项设计见 [有界消费与控制通道设计](architecture/Dispatcher有界接纳与控制通道改造计划_v0.1.md),字段草案见 [配置读取字段/返回结构提案](contracts/config-read-fields-v0.1-proposal.md);分项材料和草案均不替代已发布 Schema。
**当前迭代唯一执行入口。状态:项目提案;新 HTTP/MQ 合同、实现及验收均未完成。** 现行 `contracts/upstream/v1/` 和代码仍是旧基线,不将本文当作上线授权。分项设计:[有界消费与控制通道](architecture/Dispatcher有界接纳与控制通道改造计划_v0.1.md);对外结构:[第三方对接](thirds/第三方对接事件与请求消费顺序_v0.1.md);配置字段:[字段提案](contracts/config-read-fields-v0.1-proposal.md)、[草案 Schema](contracts/config-read-v0.1.schema.json)。分项材料须与本文一起更新,不能继续使用旧 ETag/304、控制 CAS 或即时反馈目标。
## 1. 范围和完成标准
本次要建立两个**只读 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、单租户**,允许同租户多任务验收;第二租户/多 D 只可另做明确标注的合同 fixture,不等于业务运行授权。真实 SaaS、OSS、云、供应商消费和外呼均另获授权;真实外呼仍遵守白名单、Asia/Shanghai `[09:00,20:00)` 及每线路/号码每日次数限制。未经新合同和验收,不替换现行运行规则。
**任务队列所有权(用户明确指定,下一轮合同硬边界):任务队列及其 RabbitMQ 绑定只由 SaaS 创建和维护;Dispatcher 只消费,不声明、重建、删除或绑定任务队列。队列按归属 D + 稳定任务 ID 定位,租户并发另按原值 `tenant_key` 汇总,不因队列改按任务划分而拆成每任务独立额度。SaaS 必须先确认持久队列与绑定就绪,再向其发布任务消息;D 离线期间只要该队列已存在,持久消息继续积压,重启后恢复消费。若发布时队列未就绪,消息不能靠 D 重启自动补收,SaaS 必须保留原消息身份并在队列就绪后重发;禁止把未路由消息误记为已交付。此目标与现行 D 启动以 `--tenant-key` 自行声明租户队列的代码/Schema 冲突,**尚未实施、不得绕过新版合同声称已支持动态任务**。下一轮新增归属 D 的 `tasks` 发现接口:启动/重启取带一致游标的完整快照,运行中每 30 秒用 `GET /tasks?after=<cursor>` 拉取增量,与现有两条单项配置接口分开签收,详情见 §4.1。`cursor` 是 SaaS 为本 D 任务变更分配的水位,**绝不是最大 `task_id`**;30 秒是轮询间隔提案,不等于从 SaaS 建队到 D 开始消费的端到端 30 秒保证,也不是现行服务承诺。
拟定四条只读 GET,共用 `X-DISPATCHER-id` 和 `X-DISPATCHER-SECRET-KEY`,无请求体:
当前 **P1 仍为单 D/单 Agent/单 Asterisk/单 Cell/单租户**,现行已发布 SaaS↔D 全 MQ 契约、静态 SIP 制品、Asia/Shanghai `[09:00,20:00)` 门禁和旧排队 AI 版本绑定,在新版本逐项发布、替换及验证前继续执行;本计划和字段草案**不授权**新线路、跨窗真实拨号、SaaS 生产部署或多 D 业务运行。外部真实 SaaS、云资源、供应商联调及拨号须分别获授权。
| 路径 | 用途与状态 |
| --- | --- |
| `/internal/v1/dispatcher/sip` | 用户指定路径;本 D 获管理面批准的 SIP 全量。 |
| `/internal/v1/dispatcher/task/:task_id` | 用户指定路径;任务归属、路由/主叫、智能体、时段及两项任务级超时。 |
| `/internal/v1/dispatcher/tasks`(可 `?after=<cursor>`) | 用户指定路径;启动一致全量快照,运行中每 30 秒按变更游标发现任务。 |
| `/internal/v1/dispatcher/tenant/:tenant_id/quota` | **新增路径/字段为项目草案**:取得任务后按其 `tenant_id` 读取该租户分配给本 D 的并发额度;实际路径须 SaaS 签收。 |
**完成条件分层:**
**任务队列及精确绑定只由 SaaS 创建、维护和退役;D 只消费,不建队、不绑定、不删除。** SaaS 先确认持久队列 ready 再发布 persistent 消息,mandatory 无 return 且 confirm 成功才记入队。D 离线时消息可积压;无队列时未成功入队的原消息由 SaaS 保留,就绪后按原身份重新发布,不能靠 D 重启倒灌。任务队列按 D+任务 ID 定位;租户身份保留原值用于核验和额度汇总。
- **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。
**用户明确的消费规则:pause 保留积压,resume 消费原队列积压;stop 持久生效后静默消费并 ACK 所有未接纳积压,不拨号,不向 SaaS 发布这些消息的回执或通话结果。** 静默只限定已停止任务的未接纳命令:停止控制本身仍有处理回执;已接纳/在途通话按 drain/hangup 处理并保留其最终结果。D 的错误日志、停止状态和计数不静默,ACK 丢失后重投仍不拨号、不补发被抑制结果。
完成分三层:**C**:F01/F07 正式来源、版本、严格 Schema、正反例与验收约定签收;**L**:单 D 本地隔离闭环和故障矩阵通过;**M**:多 D 独占资源/份额为另行授权的后续。项目字段草案、本地 Mock 和旧测试不能代签 C 或真实供应商结果。
## 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)区分现行 MQ 命令/回执、拟定 HTTP 两接口和**尚未发布的单份通话结果**,仅说明 SaaS↔Dispatcher 请求、返回和字段;是联调读物,**不是新 Schema 或外部签收**。已归档的[原 plan-0918](archive/plan-0918.md)保留旧 W、I/M/G、部署和证据的历史语境;不要继续写旧台账或以旧 `§9` 当本轮并行授权。每个工作包的新状态只在本文 §8 登记。
先读本文并按 §4 选择 F 包,再读[架构](architecture/Go重写方案_v0.3.md)、[现行验收](acceptance/验证与切换验收_v0.3.md)、[通信合同](contracts/通信与事件数据交互_v0.1.md)、[开源复用](dependencies/开源组件选型与复用清单_v0.2.md)及 `contracts/upstream/v1/`。新旧冲突须版本化替换,不向现行严格合同偷偷加字段。[页面分析](references/saas-page-snapshot-analysis.md)只作页面语义来源,不证明现网字段。原 [plan-0918](archive/plan-0918.md)不回写;本计划 §8 是唯一新台账。
## 3. 已确认方向与不能省略的边界
## 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+租户声明/消费队列、`--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 抢收或自动迁移。
### 3.1 配置、路由、时限
## 4. 分步任务(按依赖顺序)
- management 是 SIP 唯一编辑/审批面,SaaS 只分发获批完整版本。SIP 变更先关准入、排空/对账,再核验实际加载;发现变化约 60 秒不等于加载完成约 60 秒。供应商 transport/auth/registration 不明不得给 real 猜默认值。
- 新 `call.execute.payload` 仅 `task_id/callee`;外呼信封仍有 `command_id` 防重复拨号。任务必须返回 `tenant_id/tenant_key`,D 核对消息与任务归属后再按 `tenant_id` 取额度,身份错配/缺失即拒新准入。任务配置不返回 `agent.content_sha256`;版本不可变,同版本异内容拒绝,已有执行持久快照不漂移。
- **最小路由约定(项目草案,F01 签收):**`allowed_trunk_ids` 按列表顺序作为候选优先级,选首条满足时段、已加载状态、供应商/线路额度且支持任务 `caller_profile_id` 的线路;无满足项不接纳。`caller_profile_id` 是新增任务级明确引用,必须命中选中线路的获批主叫,不默认取第一个主叫。`route_policy_id` 只标识这份任务路由,不依赖另一个未定义的查询接口。选定线路/主叫后持久绑定;拨号失败或未知不自动换线重拨。
- `ring_timeout_ms` 是任务级振铃上限;通话有效上限是任务 `max_call_duration_ms` 与已授权 AI `conversation.max_duration_ms` 的**较小值**,控制器和执行侧均使用同一值。两值不相同必须记录来源/有效值,不改写智能体原快照或悄悄放宽任何上限。没有逐呼 `variables` 的新合同不提供联系人变量来源;需要未提供变量的提示词不得以空字符串代替继续执行,须在 F01 明确静态提示词范围或补齐获批来源。
- SIP/任务配置约 60 秒缓存到期重取完整 `200`,不使用 ETag/304;过期/失败拒绝新准入,不影响既有执行快照。配置内 `status=running` **不是恢复许可**,状态按 §3.3 独立处理。
| 工作包 | 输入 / 负责人边界 | 完成证据 / 未满足时状态 |
### 3.2 租户额度
取得有效任务的 `tenant_id` 后,D 请求拟定 `/internal/v1/dispatcher/tenant/:tenant_id/quota`。响应包括 D、tenant_id、原值 tenant_key、额度版本、`max_concurrent_calls`(允许 0)、有效截止时间;该值是 SaaS **分配给此 D 的租户份额**,不是每个任务各得一份。相同租户各任务复用同一额度快照/占用账本,SaaS 必须保证 tenant_id 与 tenant_key 一对一。
额度成功核验后最多缓存约 60 秒且不超过响应有效截止时间;缺失、身份错配、过期或刷新失败,关闭该租户所有任务的新准入,不使用无限额/默认额,不阻塞 stop 静默排空。额度调低时不强挂已接通通话、不抹掉已占用/未知执行,直到占用降至上限以下才再接新呼叫。D 以 SQLite 单事务检查并预留租户+任务+Cell/线路/AI 等额度,不能先分别判断后并发超卖。确认通话终结并释放执行资源后释放通话额度,**不等待录音上传或 MQ 最终结果确认**;未知通话继续占额。多 D 时各份额之和≤租户总额,未经 F05 授权不开放多 D 运行。
### 3.3 暂停、恢复、停止与状态来源
SaaS 先持久修改权威任务状态,再向独立 D 控制队列发布 `task.control`。请求不含 `command_id/expected_task_revision`,本阶段不设计控制去重;D 按任务串行处理控制与接纳,并持久记录生效状态后才 ACK/回执。
1. **pause:**立即关闭新接纳并保存暂停屏障,停止从该任务队列继续取新执行;已投递但未接纳的有界消息退回原队列,不能 ACK 丢弃或搬进无界 SQLite 待拨队列。已有执行按 drain/hangup 处理。重启仍暂停。
2. **resume:**重新 GET 单任务配置(不使用约 60 秒旧缓存),只有 SaaS 最新状态为 running、归属/授权/租户额度/时段有效、且本地未 stopped,才解除暂停并恢复消费原积压。无需 SaaS 重新发原消息;已过 `not_after` 的旧消息仍不可拨,暂停不能冻结或延长有效期。过期/窗口外非 stopped 命令给明确拒绝回执,不等待次日自动拨号。
3. **stop:**持久化不可逆停止屏障,同任务 ID 不能 resume/重新启用;SaaS 停止新发布。D 不申请通话额度,继续小批量消费未接纳积压,仅计本地处置计数并 ACK,不建外呼结果/outbox,不拨号。已停止任务重投/重启、额度为 0、配置缓存失效时仍可排空;不得借此清掉别的任务、伪造已接纳执行终态或漏掉控制回执/既有通话结果。
4. **优先级:**本地 stopped 永久高于任何 running;本地 paused 只能由有效 resume 解除。`tasks` 增量/全量及缓存更新可使状态更严格,不能清除已持久的暂停/停止屏障,且低于已知 `task_revision` 的状态不得覆盖新状态。冷启动恢复本地屏障与 SaaS 全量后取更严格者;冲突/缺失关闭新准入,不丢已有执行事实。
5. **无编号乱序:**控制重投可能重复返回回执,不保证按消息身份“只处理一次”。pause/stop 先关闭准入;与 SaaS 最新状态不符、不可核验或接收乱序时保持关闭并返回明确失败,不根据到达先后自动恢复。恢复必须重新发送有效 resume 并核对最新权威状态;不引入替代 command_id 或控制 CAS。SaaS 不可把发布成功视为控制已应用,丢失回执下状态不明不得主动扩量。
### 3.4 任务发现、发布与期限
D 启动/重启用 `/internal/v1/dispatcher/tasks` 拉一致全量快照和水位,运行中每 30 秒 `?after=<cursor>` 拉本 D 新增/更新/撤销。cursor 是变更水位,不是最大 task_id;完整分页连贯、持久应用成功才推进。游标失效、缺页、快照不一致时关闭受影响任务新准入并重拉全量。发现清单/任务响应必须提供 tenant_id/tenant_key 以便取额度。无变化不撤销消费关系。
**队列 ready 不等于 D 已开始消费。** 首条任务可能等待轮询、分页、配置/额度读取和可用名额;30 秒是轮询间隔,不是端到端发现 SLA。SaaS 设置的 `not_after` 应覆盖允许的排队和准备时间,不能用固定 30 秒示例承诺离线恢复必拨。D 重启仅恢复原消息:未过期且有效的才接纳;过期明确拒绝(已 stopped 则静默 ACK),绝不顺延期限/自动造新 command_id 重拨。若要求更短首呼时限,必须在 F07 另定就绪通知协议,不能假设已存在。
SaaS 清单须保留已 stopped 但未排空任务;停止新发布、积压/未 ACK 排空、无未知执行并完成双方约定的退役核验后,才撤销任务归属/删除队列。该核验不靠逐条停止回执,队列空不代表无在途通话;撤销握手待 F07 冻结。D 不创建/删除队列;任务 ID 唯一性、key 长度/字符、队列总数上限一并冻结。
### 3.5 最终结果与无录音
保留呼叫/控制回执及一种最终 `call.result`,不保留对外 query/replay、实时文字/拒联/录音拆分事件。用户已选择仅最终反馈;取消即时拒联的产品风险与现有验收冲突须正式业务签收。最终结果的身份在本地持久化,MQ 重投仍是同一份事实,不重新拨号或上传。
- 已产生录音:上传 OSS 成功后 `recording.status=uploaded`、携带路径;上传永久失败/到有限期限后 `unavailable`、明确错误与 null 路径。具体期限是 F07 阻塞项,不无限等待。
- 通话已尝试但忙线/无应答等**未产生录音**:终结已确认后即可发唯一结果,`recording.status=not_created`、身份/路径等资产值为 null,原因如 `no_answer`;不申请上传、不假称上传失败。录音本应生成却失败用 `unavailable` 和明确阶段原因,不伪装正常无录音。
- 未接纳且被停止屏障吞掉:无命令回执、无最终结果;其他未接纳拒绝只有命令回执,不能伪造通话。已接纳取消/失败/挂断仍按其事实形成最终结果。
- `outcome` 反映通话本身,录音错误写 recording 不改成呼叫失败。通话额度释放与上传/通知独立;日志只保留脱敏事实,不输出完整音频/文本/密钥。
- 最终文字合并后的消息大小与现行 MQ 上限可能冲突,F07 必须冻结可容纳的正文上限和明确失败策略,不截断完整转写、不拆成用户已移除的实时事件来掩盖。
## 4. 依赖顺序与工作包
**执行顺序:F00 → F01(HTTP/配置/额度)与 F07(MQ/发现/控制/结果)并行冻结 → 联合一致性签收 C → F02 → F03 → F04 → F08 → F09 → F06。F05 是另获授权的后续。** F01 不再要求先完成 F07 的 MQ 改写;二者共同签收后才能进入新路径实现。F03 单独负责新队列消费/状态屏障,F08 只集成结果交付和受控切换,不重复开发同一消费路径。
| 工作包 | 输入、责任与交付 | 门禁 |
| --- | --- | --- |
| 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 拓扑;下一轮 `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/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/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%;版本回退不得同时运行旧/新路径、触发第二次拨号。 |
| F00 草案整理 | 集成负责人同步本文、分项设计、对接文档、配置 Schema/示例;标明用户给定路径与项目新增额度/路由字段。 | 正反例可验证,不冒充 SaaS 现网。 |
| F01 HTTP 与配置/额度合同 | SaaS/management 签收四条 GET、两 Header、身份映射、任务主叫/候选优先级、两时限较小值、授权/缓存/配置不可变及租户额度份额/版本/有效期。 | 严格 Schema、来源/版本/哈希、正反例;静态提示词或变量来源明确。外部未签收则 blocked。 |
| F07 MQ、发现及控制/结果合同 | 与 F01 并行:SaaS 签收任务发现分页/水位、先建后发、队列生命周期、精简 call.execute、无编号控制、pause保留/resume续消费/stop静默排空、状态优先级、回执/最终结果及无录音;联合核验 HTTP 与 MQ 相同身份/版本。 | 新版本权威包及业务签收 C;撤销旧 query/replay/配置MQ/拆分事件;未决结果期限/消息大小/退役握手全部冻结。 |
| F02 配置与额度读取(C 后) | SaaS 提供四接口;D 读取并校验 SIP 实际加载、任务租户映射、按 tenant_id 的额度,完整 200 缓存;有期限拒绝与持久快照。 | 断 SaaS、降额、同租户多任务、过期/错身份、新旧配置竞态不超额/不漂移。 |
| F03 消费/发现/控制(F02 后) | D 移除新协议 `--tenant-key` 自建租户队列路径,按 SaaS 已建任务队列消费;发现游标/暂停恢复/停止静默ACK/独立控制与原子额度;未接纳仍留 MQ。 | §5 的队列/控制矩阵通过;D 无建队权限仍可工作,stop静默但可排查。 |
| F04 时段及最后拨号屏障(F03 后) | 任务×线路时段、排除日期、白名单、有效期和已绑定时限在 D/执行侧一致,最后许可与控制串行判定。 | 窗口边界/跨日/更新/暂停竞态无错误拨号,旧固定窗只在合同/验收批准后替换。 |
| F08 结果与受控切换(F04 后) | 组装 uploaded/unavailable/not_created 的最终结果与既有命令/控制回执;通话释放独立于上传;旧新版本互斥,恢复原身份 outbox。 | 本地全流程无重复拨号/PUT,停止积压不产生回传,已有执行结果不丢失。 |
| F09 本地验收(F08 后) | 集成负责人运行 §5 全矩阵;单 D 单租户多任务为 L,扩展 fixture 标为非 P1。 | gofmt、`go vet ./...`、`go test -race ./...`、`go build ./...`、本模块覆盖率≥65%,脱敏证据。 |
| F06 发布/切换(F09 后) | 单写 §8 状态,核对合同/制品/队列实际版本;真实 SaaS/OSS/云/线路另外授权。 | 旧本地证据不代签;回退不能并行旧新写入/额度或引起重拨。 |
| F05 多 D(后续另授权) | SaaS 固定任务归属、分配各 D 有界租户/供应商份额;D 独占 Agent/Asterisk/持久目录。 | 各份额总和≤总额,未知执行不跨 D 迁移,不用 fixture 冒称双 D 业务。 |
### 4.1 下一轮迭代:单份通话结果与对外消息收敛(新增,待 SaaS/业务签收)
## 5. 必测输入与预期结果
以下只修改目标文档,**不授权立即删现行 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=<cursor>` 拉取针对该 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 权威负责人冻结启动 `GET /tasks` 一致全量快照与水位、每 30 秒 `GET /tasks?after=<cursor>` 的按 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。 |
| 配置结构 | 缺身份 Header、task/消息/额度 tenant_id 或 tenant_key 不一致;agent.content_sha256/旧逐呼字段回流 | 拒绝新准入,不能猜默认/覆盖;call.execute.payload 仅 task_id/callee,task含两超时与明确主叫。 |
| 配置/路由 | 第一候选不可用、第二可用;无主叫映射;任务120秒/AI90秒;提示词缺变量 | 首个满足条件的候选并固定;主叫不可解/变量缺失不拨;有效时限90秒,两个控制点一致,拨后失败不自动换线。 |
| 租户共享额度 | 同 tenant 两任务额度各10、租户上限3,同时请求4次 | 合计最多3个占用;不同任务不各得3。未知占用计入。 |
| 降额/失效 | 已占3时从3降为1;额度0、有效期到、HTTP故障 | 现有不强挂、不清未知;新准入关闭,占用低于新上限且授权有效才接新;控制/stop排空不被额度阻塞。 |
| pause/resume | 暂停时队列100条、D已有有界未接纳交付;重启后恢复 | 暂停不丢消息/不新拨;未接纳退回原队列;重启仍暂停;有效resume继续消费原100条且无需SaaS重投,已过期的明确拒绝。 |
| stop 静默 | 未接纳积压100条、在途2通、stop;ACK丢失/重启/额度0 | 100条最终ACK,originate=0、新回执/最终结果outbox=0;本地计数可核查。stop控制有回执,2通按策略终结并各有最终结果;其他任务不受影响。 |
| 状态优先级 | pause/stop后收到旧running快照或低版本tasks;旧resume迟到/控制重复/HTTP断连 | 不自动恢复;resume未获最新running/未过屏障即明确失败;stopped不可逆。控制不按消息身份去重,不保证只回一次;回执不明不当成功。 |
| 新任务/离线期限 | 刚过轮询点新建队列;离线超过not_after;首条期限不足准备时间 | 消息先入已建队列;恢复只消费仍有效者,过期不拨不顺延,不自动新建外呼命令;30秒只是发请求周期。 |
| 发现/退役 | 较小ID任务变更、分页缺页/重复、游标失效、SaaS创建前误发、stopped仍有积压 | 不漏旧任务变更;重拉一致全量不越过未应用页;未入队原消息由SaaS保留;stopped保留清单直到静默排空,无未ACK/未知执行再退役。 |
| 队列权限/配额 | D禁止create/bind/delete、队列满、SQLite满盘、租户满额 | D只消费,不补建;SaaS保留未成功发布消息;事务失败不ACK已接纳;不会搬全部积压进SQLite,不阻塞控制/stop清理。 |
| 无录音/失败 | 无应答/忙线且无录音;应有录音却失败;OSS超时;停止未接纳命令 | 正常无录音not_created且不等待上传;故障unavailable有阶段原因并在签收期限内最终收口;停止未接纳无回传;不能伪造call_id/成功路径。 |
| 释放与结果 | 已确认结束但上传慢、MQ confirm丢失或重启、完整转写超过预算 | 通话额度已释放、未知仍占额;原结果身份重投不重拨/PUT;按签收的大小/失败合同处理,不静默截断或假成功。 |
| 时间及真实边界 | 任务/线路交集、空日、排除日、左闭右开、最后许可前控制到达 | 不跨窗或被暂停/停止后新拨,不等待次日自动执行;Mock时钟不等于real放行。 |
## 5. 必测边界与阻塞项
## 6. 写入与实施边界
1. **返回与缓存:**两接口 `200`/条件 `304`/错误、未知字段拒绝、任务归属错 D 拒绝、已有任务/线路版本不一致、摘要与实际加载不一致、授权过期、缓存第 59/60 秒、SaaS 断连、Dispatcher 重启、内存配置清理但恢复事实仍在;配置不完整绝不发新呼叫。
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 验收。
项目设计在自身 docs;现行 Schema 保持不动,新合同先签收再导入唯一版本化来源,不手工维护两套发布 Schema。本文/分项/guide/字段提案必须同步;源码、截图、真实凭据、无关父仓库不改动。TDD 先失败后实施,复用现有库,禁止隐藏错误、HTTP/MQ配置回退、重复外呼。大改另开分支,未获得子 Agent 授权由当前负责人执行。
## 6. 文档/写入边界
## 7. I/M/G 前置
新接口及单份最终结果版本必须在本项目 `docs/` 与导入的版本化 `contracts/` 保持唯一来源;UI 截图盘点不改造成第二套 SaaS Schema,字段索引只读生成。当前严格旧合同只描述运行中的 MQ-only 路径,新合同获得批准前不偷加 HTTP client、旧 MQ 配置回退或新任务路由键。原 [plan-0918.md](archive/plan-0918.md) 已恢复 HEAD 字节内容归档,归档与本轮状态均不回写旧台账。`docs/README.md` 和 `AGENTS.md` 要始终指向本文件;有并行授权时明确一 lane 一工作区/唯一写集合,公共合同、台账和集成状态由集成负责人单写。不得提交、暂存或清理无关修改;大规模重构前另开分支。
**I**:用户方向确认不等于 SaaS/management 权威签收。四接口、租户份额、路由/控制优先级、新消息 Schema、结果失败期限及退役握手未完成联合 C 前不实现新生产路径。**M**:旧 W 的通过不能代替新 F02–F09;仅本地隔离验证不代表真线路/OSS通过。**G**:部署版本、网络/ECS只读核验、SHA-256、systemd enabled+active、Asterisk/ARI/PJSIP、媒体监听和非生产诊断门禁全部保留;实际云/生产/拨号另获授权。
## 7. 当前责任与前置(I/M/G)
- **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 或真实供应商验收。
## 8. 本轮状态总台账(本文件唯一更新处)
## 8. 当前事实总台账
| 项目 | 当前事实 | 下一门禁 |
| --- | --- | --- |
| 历史计划 | `docs/archive/plan-0918.md` 从 HEAD 原样保存;原 W00–W16 状态只作历史 | 不再向归档文件写新状态。 |
| 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(下一轮) | 用户明确规定**SaaS 独占创建/绑定/退役任务队列,D 仅消费**,任务 ID 定位队列,`tasks` 一致全量快照供重启恢复,运行中每 30 秒按变更游标拉增量(非最大任务 ID);同一租户多任务共用租户额度。用户另确认对外去掉查询/补传、只保留最终通话结果及 OSS 路径的方向;[对接说明](thirds/第三方对接事件与请求消费顺序_v0.1.md)中的新事件仍仅是 review 草案,现行 MQ/代码未改。 | F07 先冻结队列所有权、SaaS 先建后发、离线/重启/在线任务发现、D 无建队权限、按租户额度及新版事件合同,再按 TDD 做 F08、隔离验收 F09;实时文字/拒联取消及录音失败/超时的唯一最终通知仍为阻塞项。 |
| 历史计划 | [plan-0918](archive/plan-0918.md)保留原文,只作历史。 | 不回写旧W台账。 |
| F00 | 用户确认按tenant_id另取租户额度、pause保留/resume续消费、stop静默排空;相关文档/项目 Schema 已修订,非现网合同。 | 正反例与文档一致性检查、外部签收。 |
| F01/F07 | 三条原路径/Header用户已指定;新增租户额度路径/字段、主叫/候选规则、控制优先级和无录音结构为本轮项目提案。 | 两合同包并行冻结后联合 C,不互设循环依赖。 |
| F02–F04/F08 | 新路径未实现;现行代码仍是旧 MQ-only/静态租户队列等基线。 | C 通过,逐包 TDD/故障注入。 |
| F09/F06 | 新验收及切换未执行,旧测试不代签。 | L证据与授权,真实联调另行安排。 |
| F05 | 多D/第二租户不在本轮运行范围。 | 另获阶段与额度/资源授权。 |
## 9. 协作和版本记录
## 9. 协作与版本记录
本轮只有当前负责人可更新本文件 §8 和共同合同/索引;子任务若获明确并行授权,必须独占文件/测试资源、不得从未跟踪文件假定 HEAD/worktree 已含它,合并每批后统一回归。没有明确真实线路/供应商/云/生产授权,不自动操作外部资源。旧计划的 §8/§9 仅为归档历史;新工作一律以本文件 §7–§9 为入口。版本 v0.1 是**规划与项目字段草案**,不是 W01/SaaS 的正式发布版本。
仅集成负责人更新本文 §8、公共合同和合并状态;有并行授权时一 lane 一工作区/独占写集合、每批回归,不假定未跟踪文件在 HEAD。v0.1 是项目规划及字段草案,不是 W01/SaaS 正式发布版本。真实环境的资源、拨号和生产权限不因文档改动增加。
@@ -1,16 +1,16 @@
# SaaS ↔ Dispatcher:请求与通话结果消费顺序 v0.1
本文只描述 **SaaS 与 Dispatcher(D)**。配置 GET 是**待签收草案**;`call.execute`、`task.control`、`command.result` 的示例取自**现行 MQ v2 合同**;下文“唯一通话结果 `call.result`”是**下一版本提案**,现行 Schema 和程序**尚不支持**。不能把本文的新旧示例拼接成已经可运行的单一版本。示例为非生产数据,JSON 代码块均为完整请求或返回体;字段说明写在块外。
本文只描述 **SaaS 与 Dispatcher(D)**。四个配置/任务发现/租户额度 GET 与简化 `call.execute`、无 `command_id` 的 `task.control`/控制回执均是**下一版待签收草案**;现行 MQ v2 仍要求旧字段;下文“唯一通话结果 `call.result`”是**下一版本提案**,现行 Schema 和程序**尚不支持**。不能把本文的新旧示例拼接成已经可运行的单一版本。示例为非生产数据,JSON 代码块均为完整请求或返回体;字段说明写在块外。
## 1. 触发顺序
| 顺序 | 请求与触发 | SaaS 处理/返回 |
| --- | --- | --- |
| 1 | D 启动或配置到期,按 D 身份读取 SIP 全量(拟定 HTTP GET)。 | SaaS 返回本 D 唯一获批版本;D 核验后才能接受新执行。 |
| 2(下一版拟定) | D 启动/重启 `GET /tasks` 取得本 D 的任务全量快照与变更游标,运行中每 30 秒 `GET /tasks?after=<cursor>`;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。当前共享队列的及时控制屏障仍待下一轮验收。 |
| 1 | D 启动或配置到期,带 `X-DISPATCHER-id`/`X-DISPATCHER-SECRET-KEY` 请求 `/internal/v1/dispatcher/sip`(拟定 HTTP GET)。 | SaaS 返回本 D 唯一获批版本;D 核验后才能接受新执行。 |
| 2(下一版拟定) | D 启动/重启 `GET /internal/v1/dispatcher/tasks` 取得本 D 的任务全量快照与变更游标,运行中每 30 秒 `GET /internal/v1/dispatcher/tasks?after=<cursor>`;SaaS 创建任务时先建好任务队列/绑定再发布。 | D 发现新任务后仅消费 SaaS 已创建的队列;D 离线期间消息可留在队列,较小任务 ID 的更新/停止也能由变更游标发现。路径、字段、30 秒时限尚待 SaaS 签收。 |
| 3 | SaaS 将任务固定分配给一个 D,向该 D 投递 `call.execute`(下一版精简 payload,**非现行 Schema**)。 | D 按消息中的任务 ID 请求 `GET /internal/v1/dispatcher/task/:task_id`,核验归属 D 的任务快照(拟定 HTTP);未接纳任务可受约 60 秒配置缓存延迟影响,已接纳执行固定原快照。 |
| 4 | D 从任务取得 `tenant_id`,请求拟定 `GET /internal/v1/dispatcher/tenant/:tenant_id/quota`,与同租户其他任务共享额度后判定接纳。 | 额度缺失/过期不接新呼叫;普通接纳/拒绝有 `command.result`,**已停止任务的未接纳积压仅消费并 ACK,无逐条回传**。 |
| 按需 | SaaS 投递 `task.control` 暂停、恢复或停止(下一版草案)。 | 控制本身有回执;暂停保留积压,恢复消费原队列;停止持久生效后静默消费并 ACK 未接纳积压,不拨号、不向 SaaS 回传这些消息的结果。已在途通话仍按策略处理并给最终结果。 |
| 5 | 通话终结且录音已上传 OSS,D 投递一条 `call.result`(**拟定 MQ 最终事件**)。 | SaaS 只处理这条最终的通话详情,按 `event_id` 去重;录音以 `bucket/object_key` 关联,不接收文件、不提供上传会话或验证结果。上传失败/超时的最终收口见 §4.2,时限尚未签收。 |
### 1.1 现行 MQ 地址与 JSON 字段不是一回事
@@ -44,14 +44,25 @@ SaaS 创建并绑定:
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。
`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 控制命令。
拟定的四个 GET 均**无请求 JSON 体**,统一使用 `X-DISPATCHER-id`(全局唯一 D UUID)和 `X-DISPATCHER-SECRET-KEY`(受控密钥,绝不写入文档/日志)。具体 SaaS 地址及 Header 校验/轮换仍待签收。SIP 返回本 D 全量;单任务按路径中的 `task_id` 查询,SaaS 必须核对归属 D 与原值 `tenant_key`,任务发现则按 D 返回归属清单。**不使用 ETag、If-None-Match 或 304**:任务与 SIP 配置约 60 秒缓存到期时 GET 完整 200 响应,失败只停新准入,已接纳执行保持绑定快照;MQ 控制不等待配置缓存。`tasks` 的每 30 秒增量轮询另见 §2.5。
### 2.1 SIP 配置:200,返回本 D 的完整获批快照
请求(地址/Header 仍待 SaaS 实现确认):
```http
GET /internal/v1/dispatcher/sip HTTP/1.1
Host: <SaaS 内网地址>
X-DISPATCHER-id: c046b893-8628-4589-ae50-619d049248a6
X-DISPATCHER-SECRET-KEY: <受控注入,不展示实际密钥>
```
完整 200 响应体:
```json
{
"schema_version": "config-read.v0.1",
@@ -166,11 +177,23 @@ D 仅按 SaaS /tasks 清单中的 queue 名称开始消费。
### 2.2 任务配置:200,ASR + LLM + TTS 模式
请求(`task_id` 示例为 `task-mock`):
```http
GET /internal/v1/dispatcher/task/task-mock HTTP/1.1
Host: <SaaS 内网地址>
X-DISPATCHER-id: c046b893-8628-4589-ae50-619d049248a6
X-DISPATCHER-SECRET-KEY: <受控注入,不展示实际密钥>
```
完整 200 响应体(仅一种智能体模式):
```json
{
"schema_version": "config-read.v0.1",
"resource": "task_config",
"dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6",
"tenant_id": "tenant-id-mock",
"tenant_key": "tenant-mock",
"task_id": "task-mock",
"task_revision": 2,
@@ -178,7 +201,10 @@ D 仅按 SaaS /tasks 清单中的 queue 名称开始消费。
"name": "Mock task",
"group_id": null,
"max_concurrent_calls": 2,
"ring_timeout_ms": 30000,
"max_call_duration_ms": 120000,
"route_policy_id": "route-mock",
"caller_profile_id": "caller-profile-mock",
"allowed_trunk_ids": [
"trunk-mock"
],
@@ -231,7 +257,6 @@ D 仅按 SaaS /tasks 清单中的 queue 名称开始消费。
},
"agent": {
"agent_version_id": "agent-version-mock",
"content_sha256": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
"authorization_id": "auth-mock",
"authorization_expires_at": "2026-09-21T18:00:00+08:00",
"config": {
@@ -290,12 +315,12 @@ D 仅按 SaaS /tasks 清单中的 queue 名称开始消费。
**字段说明/消费动作:**
- `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[]` 是路由引用及可用线路列表。
- `schema_version/resource/dispatcher_id/tenant_id/tenant_key/task_id`:版本、资源 `task_config`、归属 D、租户 ID、原值租户键和单任务 ID;D 必须验证请求归属并按 `tenant_id` 取得 §2.6 的租户额度。`task_revision` 是任务修订,`status` 为拟定 `running/paused/stopped/finished`;非 running 不接新呼叫。
- `name/group_id` 是名称及可空分组;`max_concurrent_calls` 是本任务额度,不等于跨任务/跨 D 总额度;`ring_timeout_ms/max_call_duration_ms` 是任务级振铃/最长通话毫秒上限;`route_policy_id` 标识这份任务路由;`allowed_trunk_ids[]` 依次列出候选优先级,选择首条已加载、时段/额度有效且支持任务 `caller_profile_id` 的线路;`caller_profile_id` 明确主叫引用,不默认取首个主叫。无匹配项不接纳,选定后固定、拨号失败不自动换线重拨。有效通话上限取任务 `max_call_duration_ms` 与 AI `conversation.max_duration_ms` 的较小值,执行与 AI 控制器一致,不改原授权配置。精简命令不带这些业务值。
- `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 复活。
- `agent.agent_version_id`:不可变智能体版本,必须与 `agent.config.agent_version_id` 对应;`authorization_id/authorization_expires_at` 为授权身份和截止时间,到期不得由过期缓存继续放行。不返回 `content_sha256`,同一版本内容变化必须拒绝并要求新版本。
- `agent.config.immutable/mode`:不可变标记及 `full_ai` 模式。`llm.provider_ref/model/temperature/max_tokens/timeout_ms` 为供应商引用、模型、采样、输出上限和超时;`prompt.text/allowed_variables/max_bytes` 为提示词、允许的变量和字节上限;`tts.provider_ref/model/voice/speed/format/timeout_ms` 为语音供应商引用、模型、声音、速度、音频格式与超时;`asr.provider_ref/model/language/input/interim/timeout_ms` 为识别供应商、可选模型、语种、输入格式、是否给出中间转写与超时;音频 `encoding/sample_rate_hz/channels/sample_width_bytes` 定义编码、采样率、声道和样本宽度;`conversation.opening/allow_interrupt/silence_timeout_ms/max_duration_ms/max_turns/sentence_max_chars/max_pending_audio_chunks` 控制开场、打断、静默时限、总时限、轮次及缓存上限。
- 未接纳呼叫在有效缓存窗口可能仍用旧批准版;已接纳呼叫固定原快照。现行 `call.execute` 已带 `task_revision/agent_version_id`,两者如何合法换版必须另行签收。
- 未接纳呼叫在有效缓存窗口可能仍用旧批准版;已接纳呼叫固定原快照。新版呼叫命令只给任务 ID 与被叫号码,D 须从有效任务配置取得固定版本和任务级超时,不从命令猜值;现行严格 Schema 仍是旧结构。
### 2.3 任务配置:200,仅 ASR 模式(独立情况)
@@ -304,6 +329,7 @@ D 仅按 SaaS /tasks 清单中的 queue 名称开始消费。
"schema_version": "config-read.v0.1",
"resource": "task_config",
"dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6",
"tenant_id": "tenant-id-mock",
"tenant_key": "tenant-mock",
"task_id": "task-mock",
"task_revision": 2,
@@ -311,7 +337,10 @@ D 仅按 SaaS /tasks 清单中的 queue 名称开始消费。
"name": "Mock task",
"group_id": null,
"max_concurrent_calls": 2,
"ring_timeout_ms": 30000,
"max_call_duration_ms": 120000,
"route_policy_id": "route-mock",
"caller_profile_id": "caller-profile-mock",
"allowed_trunk_ids": [
"trunk-mock"
],
@@ -364,7 +393,6 @@ D 仅按 SaaS /tasks 清单中的 queue 名称开始消费。
},
"agent": {
"agent_version_id": "agent_asr_v1",
"content_sha256": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
"authorization_id": "auth-mock",
"authorization_expires_at": "2026-09-21T18:00:00+08:00",
"config": {
@@ -399,15 +427,7 @@ D 仅按 SaaS /tasks 清单中的 queue 名称开始消费。
**字段说明/消费动作:**字段与 2.2 相同,但 `agent.config.mode=asr_only`,**没有** LLM、提示词或 TTS 对象;配置内外 `agent_version_id` 必须一致。只能按授权的识别配置执行,不应将未提供的字段填成默认值。
### 2.4 SIP 或任务:304,内容未变(拟定)
```http
HTTP/1.1 304 Not Modified
```
**响应体:无。**D 仅在已有完整且仍获批准的对应配置快照、授权仍有效时沿用;`304` 不代表智能体授权延期,也不证明 SIP 配置已经生效。具体 ETag、续期与错误状态码待签收。
### 2.5 SIP 或任务:错误返回(拟定;HTTP 状态码未定)
### 2.4 SIP 或任务:错误返回(拟定;HTTP 状态码未定)
```json
{
@@ -422,11 +442,20 @@ HTTP/1.1 304 Not Modified
**字段说明/消费动作:**`schema_version/resource` 标识草案错误对象;`error.code` 是机器可读错误代码(示例 `not_assigned` 表示该任务不归此 D),`error.message` 是可读说明,不含密钥。D 不得将失败当作空任务/无限制或使用过期配置接新呼叫;不能自动回退至 MQ 配置通道。
## 2.6 D ← SaaS:动态任务发现(**下一版草案,现行无此接口/Schema**)
### 2.5 D ← SaaS:动态任务发现(**下一版草案,现行无此接口/Schema**)
新增第三条只读 HTTP 接口,不属于 §2 现有的两种配置响应。D 以自身身份在启动/重启时 `GET /tasks` 取得**一致全量快照 + 游标**,运行中**每 30 秒**以 `GET /tasks?after=<cursor>` 请求针对本 D 的**任务变更**。`after` 不是最大 `task_id`:已存在的小 ID 任务被暂停、停止、改派也必须返回。以下路径、JSON 键/类型、认证承载和错误码仅是项目提案,待 SaaS/F07 冻结,不代表现网已提供;每 30 秒发起请求是轮询频率,不是端到端 30 秒发现保证,也不同于单任务配置约 60 秒缓存。无请求 JSON 体,具体鉴权/分页传递方式未签收,不伪造完整 HTTP 请求头。
第三条只读 HTTP 接口是 `GET /internal/v1/dispatcher/tasks`,与 §2.1/§2.2 两条配置响应分开。D 启动/重启时不带 `after` 读取**一致全量快照 + 游标**,运行中**每 30 秒** `GET /internal/v1/dispatcher/tasks?after=<cursor>` 读取针对本 D 的变更。`after` 是 SaaS 的变更水位,**不是最大 `task_id`**;旧任务的暂停、停止、改派也会返回。路径和两个 Header 作为本轮需求输入,响应 JSON、错误码/分页仍是项目草案,待 SaaS/F07 签收;30 秒是轮询频率,不是端到端 30 秒发现保证,也不同于单任务配置约 60 秒缓存。所有 GET 无请求体,密钥只由受控部署注入。
### 2.6.1 启动或重启:全量快照(200,拟定)
### 2.5.1 启动或重启:全量快照(200,拟定)
```http
GET /internal/v1/dispatcher/tasks HTTP/1.1
Host: <SaaS 内网地址>
X-DISPATCHER-id: c046b893-8628-4589-ae50-619d049248a6
X-DISPATCHER-SECRET-KEY: <受控注入,不展示实际密钥>
```
完整 200 响应体:
```json
{
@@ -438,6 +467,7 @@ HTTP/1.1 304 Not Modified
"tasks": [
{
"task_id": "task-a",
"tenant_id": "tenant-a",
"tenant_key": "tenant-a",
"status": "running",
"task_revision": 1,
@@ -450,6 +480,7 @@ HTTP/1.1 304 Not Modified
},
{
"task_id": "task-old",
"tenant_id": "tenant-a",
"tenant_key": "tenant-a",
"status": "stopped",
"task_revision": 3,
@@ -465,9 +496,18 @@ HTTP/1.1 304 Not Modified
}
```
**字段说明/消费动作:**`dispatcher_id` 是被授权的目标 D;`snapshot_id` 锁定同一次全量读取,跨页不得混杂新旧状态;`cursor` 是此快照覆盖的 SaaS 任务变更水位(示例数字只是**不透明字符串**,D 不按大小比较任务 ID);`tasks[]` 列出本 D 全部归属任务及**已停止但队列仍有积压的任务**;`tenant_key` 保留原值,用于同租户所有任务共享并发额度;`task_revision/status` 是任务版本和状态;`queue` 是**SaaS 已创建/绑定**的消费地址,D 只能读取,不能自行声明。`next_page_token` 非空时须在同一个 `snapshot_id` 下读完所有页再应用快照/水位,分页传递机制待签收。
**字段说明/消费动作:**`dispatcher_id` 是被授权的目标 D;`snapshot_id` 锁定同一次全量读取,跨页不得混杂新旧状态;`cursor` 是此快照覆盖的 SaaS 任务变更水位(示例数字只是**不透明字符串**,D 不按大小比较任务 ID);`tasks[]` 列出本 D 全部归属任务及**已停止但队列仍有积压的任务**;`tenant_id` 用于读取 §2.6 额度,`tenant_key` 保留原值并与 tenant_id 一对一核验,用于同租户所有任务共享并发额度;`task_revision/status` 是任务版本和状态;`queue` 是**SaaS 已创建/绑定**的消费地址,D 只能读取,不能自行声明。`next_page_token` 非空时须在同一个 `snapshot_id` 下读完所有页再应用快照/水位,分页传递机制待签收。
### 2.6.2 每 30 秒:增量变化(200,拟定)
### 2.5.2 每 30 秒:增量变化(200,拟定)
```http
GET /internal/v1/dispatcher/tasks?after=1042 HTTP/1.1
Host: <SaaS 内网地址>
X-DISPATCHER-id: c046b893-8628-4589-ae50-619d049248a6
X-DISPATCHER-SECRET-KEY: <受控注入,不展示实际密钥>
```
完整 200 响应体:
```json
{
@@ -481,6 +521,7 @@ HTTP/1.1 304 Not Modified
"cursor": "1043",
"operation": "assigned",
"task_id": "task-b",
"tenant_id": "tenant-a",
"tenant_key": "tenant-a",
"status": "running",
"task_revision": 1,
@@ -495,6 +536,7 @@ HTTP/1.1 304 Not Modified
"cursor": "1044",
"operation": "updated",
"task_id": "task-a",
"tenant_id": "tenant-a",
"tenant_key": "tenant-a",
"status": "stopped",
"task_revision": 2,
@@ -510,9 +552,9 @@ HTTP/1.1 304 Not Modified
}
```
**字段说明/消费动作:**`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 持久应用变更后才持久推进游标;分页时读完连续页,不得跳过未处理页。
**字段说明/消费动作:**`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,拟定;与停止不同)
### 2.5.3 任务改派/退役(200,拟定;与停止不同)
```json
{
@@ -526,6 +568,7 @@ HTTP/1.1 304 Not Modified
"cursor": "1045",
"operation": "removed",
"task_id": "task-old",
"tenant_id": "tenant-a",
"tenant_key": "tenant-a"
}
],
@@ -535,7 +578,7 @@ HTTP/1.1 304 Not Modified
**字段说明/消费动作:**`removed` 是 SaaS 确认此 D 不再消费该任务的撤销记录(tombstone),**不是** `stop` 一到就立刻删除队列。必须已停止新发布、旧队列积压和未 ACK 消息处理完毕,且改派时确认旧 D 没有未知执行后再终结旧所有权;具体握手/退役合同待签收。D 只停止消费,不负责删队列;队列生命周期仍归 SaaS。
### 2.6.4 没有变更(200,拟定)
### 2.5.4 没有变更(200,拟定)
```json
{
@@ -551,7 +594,7 @@ HTTP/1.1 304 Not Modified
**字段说明/消费动作:**SaaS 没有新变更时水位不动;D 等下一个 30 秒周期,不因空列表删除已有消费关系。
### 2.6.5 游标失效或缺页(错误,HTTP 状态待签收)
### 2.5.5 游标失效或缺页(错误,HTTP 状态待签收)
```json
{
@@ -566,15 +609,75 @@ HTTP/1.1 304 Not Modified
**字段说明/消费动作:**`cursor_expired` 表示 SaaS 已不能提供从旧游标起的连续变更;缺页、断续或快照分页不一致也应中止增量。D 不推进错误游标,停受影响任务的新接纳并重新拉一致全量快照;不能把错误当无变更或盲目根据 RabbitMQ 队列列表发现任务。真正的错误码、游标保留期/分页格式须 F07 签收。
## 3. SaaS → D:业务命令(现行 MQ 格式;执行语义以新合同为准)
### 2.6 D ← SaaS:按租户 ID 获取并发额度(新增项目草案)
下列 JSON 是**完整 MQ 请求**。`schema_version` 指现行消息版本 `2.0`;`command_id` 是同一命令的稳定幂等身份,`command_type` 是命令类别;`dispatcher_id/tenant_id/tenant_key` 确定目标和租户;`trace_id` 关联结果;`issued_at/not_after` 限定时效;`payload` 是对应业务参数。重投同一命令不能创建第二次执行。新版去除对外查询和补传**命令**,不等于允许吞掉 MQ 重投或丢失本地恢复事实。
D 从任务清单/单任务配置取得 `tenant_id`、原值 `tenant_key` 并核对外呼信封后,再请求额度;不能把任务额度当租户总额。以下路径和字段为**项目提案,尚未由 SaaS 发布**。
#### 2.6.1 有可用额度:请求与完整 200 响应
```http
GET /internal/v1/dispatcher/tenant/tenant-id-mock/quota HTTP/1.1
Host: <SaaS 内网地址>
X-DISPATCHER-id: c046b893-8628-4589-ae50-619d049248a6
X-DISPATCHER-SECRET-KEY: <受控注入,不展示实际密钥>
```
```json
{
"schema_version": "config-read.v0.1",
"resource": "tenant_quota",
"dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6",
"tenant_id": "tenant-id-mock",
"tenant_key": "tenant-mock",
"quota_revision": 1,
"max_concurrent_calls": 3,
"valid_until": "2026-09-21T18:00:00+08:00"
}
```
**字段说明/消费动作:**三个身份字段必须与任务及请求一致;`quota_revision` 为额度版本;`max_concurrent_calls` 是 SaaS **分给本 D 的租户份额**,同租户所有任务共同占用,不是每任务各得3路;`valid_until` 是有效截止。成功核验后最多缓存约60秒且不超过截止时间;同租户任务复用一份额度/占用,D 同一事务核查并预留租户+任务+线路等额度。未知通话继续计数;未来多D需份额之和≤总额,不各拿一份全额。
#### 2.6.2 降额或额度为零:200(独立情况)
```json
{
"schema_version": "config-read.v0.1",
"resource": "tenant_quota",
"dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6",
"tenant_id": "tenant-id-mock",
"tenant_key": "tenant-mock",
"quota_revision": 2,
"max_concurrent_calls": 0,
"valid_until": "2026-09-21T18:00:00+08:00"
}
```
**字段说明/消费动作:**0明确禁止新准入,不是无限额。降额时不强挂已有通话、不清未知占用,等占用低于新上限且授权有效才再接新。stop静默排空与控制不需要通话额度,不能因额度0卡住停止任务。
#### 2.6.3 缺失或失败:错误(HTTP 状态待签收)
```json
{
"schema_version": "config-read.v0.1",
"resource": "error",
"error": {
"code": "tenant_quota_unavailable",
"message": "No valid tenant allocation is available for this dispatcher."
}
}
```
**字段说明/消费动作:**缺失、身份不符、过期或刷新失败关闭该租户新准入,不用任务额度或无限额兜底;已有执行依原快照处理。核实通话终结并释放执行资源就释放通话额度,**不等待录音上传或最终结果 MQ 确认**;未知通话不能释放。
## 3. SaaS → D:下一版精简业务命令(**草案,现行严格 MQ Schema 不支持**)
以下 JSON 均是**完整的拟定下一版 MQ 请求**,`schema_version=command-next.v0.1-proposal` 是草案标记,不是现行 `2.0`。`dispatcher_id/tenant_id/tenant_key` 确定 D 和租户,MQ 发布参数另按 §1 任务 key 精确路由;`issued_at/not_after` 限定有效期;`command_type` 区分呼叫或控制。**仅** `call.execute` 仍带 `command_id`,用来识别不可重复的外呼执行;三个 `task.control` 均不带 `command_id`、`expected_task_revision`,本阶段不设计控制命令去重。没有这些字段后,控制的乱序、重投以及处理回执如何关联必须在 F07 由 SaaS 签收并如实验收,不能冒称当前协议已经支持。
### 3.1 发起外呼:call.execute
```json
{
"schema_version": "2.0",
"schema_version": "command-next.v0.1-proposal",
"dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6",
"tenant_id": "tenant-a",
"tenant_key": "tenant-a",
@@ -582,130 +685,144 @@ HTTP/1.1 304 Not Modified
"issued_at": "2026-09-18T10:00:00+08:00",
"command_id": "command-a",
"command_type": "call.execute",
"not_after": "2026-09-18T10:00:30+08:00",
"not_after": "2026-09-18T10:15:00+08:00",
"payload": {
"execution_id": "execution-a",
"task_id": "task-a",
"task_item_id": "item-a",
"task_revision": 1,
"callee": "15003164745",
"route_policy_id": "route-a",
"caller_profile_id": "caller-a",
"agent_version_id": "version-a",
"variables": {},
"ring_timeout_ms": 1000,
"max_call_duration_ms": 10000
"callee": "15003164745"
}
}
```
**字段说明/消费动作:**`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 尚未接纳时执行停止/暂停屏障;不拨号的命令拒绝也须给出回执。
**字段说明/消费动作:**`payload` **只有** `task_id`(SaaS 任务身份)及 `callee`(原始被叫号码,不带线路前缀);租户归属从信封及 `/internal/v1/dispatcher/task/:task_id` 的授权结果核对。路由/主叫/智能体版本和任务级 `ring_timeout_ms/max_call_duration_ms` 全由有效任务配置取得,D 接纳时绑定不可漂移的执行快照;生成 `execution_id` 是 D 内部事实,不由 SaaS 逐呼提供。信封 `command_id` 仅用于外呼命令身份:重投不能第二次拨号。本例15分钟有效期仅示意,不是默认值;SaaS 须覆盖其允许的轮询/配置/额度准备及排队时间。队列ready不代表D已消费;离线或暂停不延长not_after,恢复仅执行仍有效者,过期非stopped消息明确拒绝、不自动重建命令,stopped积压静默ACK。现行 MQ `2.0` 仍要求旧 payload,新版严格 Schema 和 SaaS 发布端未签收/修改前**不能直接用此消息上线**。
### 3.2 暂停任务:task.control / pause
```json
{
"schema_version": "2.0",
"schema_version": "command-next.v0.1-proposal",
"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": "task.control-a",
"command_type": "task.control",
"not_after": "2026-09-21T00:00:30Z",
"payload": {
"task_id": "task-a",
"action": "pause",
"expected_task_revision": 1,
"active_call_policy": "drain",
"reason": "local-test"
}
}
```
**字段说明/消费动作:**`task_id` 定位任务;`action=pause` 暂停**接纳**新呼叫;`expected_task_revision` 为比较条件;`active_call_policy=drain` 允许已在途通话自然结束;`reason` 为控制理由。暂停后是否恢复必须有新授权,不把未接纳旧命令留到 resume 时自动拨出。
**字段说明/消费动作:**`task_id` 定位任务;`action=pause` 停止新呼叫准入;`active_call_policy=drain` 允许在途通话自然结束;`reason` 是原因说明。此版**无 `command_id`、无 `expected_task_revision`、不定义控制去重**。D 保存暂停屏障并停止消费该任务新执行,已经交付但未接纳的有界消息退回原队列,不能ACK丢弃或搬入无界本地待拨队列;恢复会继续消费原积压,无需SaaS重发。已有执行按所选策略处理,暂停控制本身有回执。
### 3.3 恢复任务:task.control / resume
```json
{
"schema_version": "2.0",
"schema_version": "command-next.v0.1-proposal",
"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": "task.resume-a",
"command_type": "task.control",
"not_after": "2026-09-21T00:00:30Z",
"payload": {
"task_id": "task-a",
"action": "resume",
"expected_task_revision": 2,
"reason": "operator-resume"
}
}
```
**字段说明/消费动作:**只允许暂停任务按新授权恢复;`expected_task_revision` 须为当前实际版。已停止的任务不能用 resume 恢复。
**字段说明/消费动作:**resume 成功就是恢复消费**原任务队列的积压**,不是等待 SaaS 重发。D 必须绕过缓存读取最新任务,状态running且归属/授权/额度/时段有效、本地未stopped,才能解除paused;每条旧命令仍校验not_after,过期明确拒绝,不延长期限或等待次日。已停止任务不可恢复。请求无编号/修订、不设计控制去重,乱序时按下方状态优先级处理。
### 3.4 停止任务:task.control / stop
```json
{
"schema_version": "2.0",
"schema_version": "command-next.v0.1-proposal",
"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": "task.stop-a",
"command_type": "task.control",
"not_after": "2026-09-21T00:00:30Z",
"payload": {
"task_id": "task-a",
"action": "stop",
"expected_task_revision": 2,
"active_call_policy": "hangup",
"reason": "operator-stop"
}
}
```
**字段说明/消费动作:**`stop` 终止该任务的新呼叫准入;`active_call_policy=hangup` 结束已在途通话,若选择 `drain` 则等待自然结束。停止命令**不是清空 RabbitMQ 队列**:现行 D 仍消费租户共享队列并逐条拒绝/ACK 该任务积压消息;下一版改为继续消费**该任务的 SaaS 所建队列**并逐条拒绝/ACK,不影响其他任务。现行共享队列尚不保证控制能超越积压执行消息,下一版 D 专用控制队列及 stop-before-accept 屏障仍待签收。
**字段说明/消费动作:**`stop` 持久终止任务准入,SaaS 同步停止继续发布;D 小批量**静默消费并ACK所有尚未接纳积压**,不拨号、不发逐条 `command.result` 或 `call.result`,不申请通话额度。不是purge/delete队列,也不影响其他任务。ACK丢失、重启、额度0、配置失效后依旧排空且不补发结果;保留本地计数/错误。**停止控制本身仍有回执**;已接纳/在途通话按 `hangup` 或 `drain` 处理,并照常给真实最终结果,不能因“静默”丢弃它们。stopped 同任务ID不能resume。
### 3.5 D → SaaS:命令处理回执 `command.result`(保留,不是通话事件)
**配置、任务发现和控制的状态优先级(拟定):**SaaS 先持久变更权威任务状态,再发控制;D 对同任务串行处理控制/接纳。stopped不可逆,paused只能由上述有效resume解锁,旧running配置/清单不能解锁,低于已知task_revision的状态不可覆盖新状态。重启恢复本地屏障及全量时取更严格者;快照可关准入、不能擅自重开。pause/stop先关准入,最新权威状态不符/读取失败或迟到控制产生冲突时保守保持关闭并返回明确失败;需有效新resume才能恢复。不设计控制消息去重,可能多次回执;不能把MQ发布成功当控制已应用。该规则须F07双方签收,不是现行代码已保证。
### 3.5 D → SaaS:外呼命令处理回执(草案,不是通话结果)
```json
{
"schema_version": "2.0",
"event_id": "command.result-event-a",
"schema_version": "command-next.v0.1-proposal",
"event_id": "command-result-a",
"event_type": "command.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",
"trace_id": "trace-v2",
"occurred_at": "2026-09-18T10:00:01+08:00",
"aggregate_type": "command",
"aggregate_id": "command-1",
"aggregate_id": "command-a",
"aggregate_version": 1,
"payload": {
"command_id": "command-1",
"command_id": "command-a",
"command_type": "call.execute",
"status": "accepted",
"reason_code": "accepted",
"execution_id": "execution-1"
},
"dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6"
"execution_id": "execution-a"
}
}
```
**字段说明/消费动作:**信封 `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 的唯一通话结果。停止/暂停后的等待消息不得发起外呼;对重复命令按原身份去重。
**字段说明/消费动作:**`payload.command_id` 仅指向 §3.1 的外呼命令;`status` 区分接纳/拒绝,`execution_id` 是 D 接纳后生成的执行身份。已停止任务的未接纳积压**不发送此回执**;其他未接纳拒绝只有命令回执、不伪造通话。MQ 回执**不代表已拨号或已完成通话**,未知执行不得靠重投产生第二次呼叫;下一版字段/版本仍待 F07 签收。
### 3.6 D → SaaS:任务控制处理回执(草案;与外呼回执分开)
```json
{
"schema_version": "command-next.v0.1-proposal",
"event_id": "control-result-a",
"event_type": "command.result",
"dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6",
"tenant_id": "tenant-a",
"tenant_key": "tenant-a",
"trace_id": "trace-v2",
"occurred_at": "2026-09-18T10:00:01+08:00",
"aggregate_type": "task",
"aggregate_id": "task-a",
"aggregate_version": 2,
"payload": {
"command_type": "task.control",
"task_id": "task-a",
"action": "pause",
"status": "applied",
"reason_code": "applied",
"task_state": "paused"
}
}
```
**字段说明/消费动作:**控制请求不带 `command_id/expected_task_revision`,回执以 `task_id/action/status/task_state` 说明任务和实际处理结果,不提供按原控制编号一对一关联,也不把 `event_id` 用作控制去重身份。SaaS 仅能据已收到的事实更新展示;对控制并发/乱序、丢失回执和重投后的最终状态判定需要 F07 明确,不能把 MQ 发布成功当控制已生效。
## 4. D → SaaS:唯一通话结果(**拟定新 MQ 合同,尚无已发布 Schema**)
同一次通话只发布一种业务反馈 `call.result`:通话状态、最终转写、拒联结果、录音资产一次返回;不再将通话进度、实时文字、拒联、通话结束、录音成功/失败各自发布对外事件。**这会改变现有“实时文字/即时拒联”的产品要求,必须在新版合同与验收中明确批准;SaaS 在最终结果到达前不会获得这些反馈。**消息仍应可靠入队,断线后按同一事件身份重投;这不是对外“补传命令”。以下两个结构均为待签收提案,不能用现行 MQ/event Schema 校验,也不能作为已上线接口。
同一次通话只发布一种业务反馈 `call.result`:通话状态、最终转写、拒联结果、录音资产一次返回;不再将通话进度、实时文字、拒联、通话结束、录音成功/失败各自发布对外事件。**这会改变现有“实时文字/即时拒联”的产品要求,必须在新版合同与验收中明确批准;SaaS 在最终结果到达前不会获得这些反馈。**停止任务未接纳积压不产生通话事件;其它真实执行的消息仍应可靠入队,断线后按同一事件身份重投;这不是对外“补传命令”。以下三个结构均为待签收提案,不能用现行 MQ/event Schema 校验,也不能作为已上线接口。
### 4.1 录音已上传 OSS:最终成功结果
@@ -727,7 +844,6 @@ HTTP/1.1 304 Not Modified
"execution_id": "execution-a",
"call_id": "call-a",
"task_id": "task-a",
"task_item_id": "item-a",
"task_revision": 1,
"agent_version_id": "version-a",
"route_policy_id": "route-a",
@@ -768,7 +884,7 @@ HTTP/1.1 304 Not Modified
```
**字段说明/消费动作:**`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` 避免重复资产。
`payload.source_command_id/execution_id/call_id/task_id/task_revision/agent_version_id` 绑定原外呼命令、D 生成的执行/呼叫及从任务快照绑定的固定版本;`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` 避免重复资产。
### 4.2 录音上传未完成:最终异常结果(是否启用及截止时间待签收)
@@ -790,7 +906,6 @@ HTTP/1.1 304 Not Modified
"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",
@@ -824,10 +939,65 @@ HTTP/1.1 304 Not Modified
**字段说明/消费动作:**这是**防止录音永远未上传时通话结果永久消失的待定方案**:经合同规定的有限截止时间或确知不可恢复后,`recording.status=unavailable` 且 `bucket/object_key/size_bytes/checksum_sha256=null`,`recording.error_code` 表示未得到录音资产,呼叫自身的 `reason_code` 仍为 null;不能谎称上传成功,也不能默默丢弃最终结果。`outcome` 必须反映**通话本身**而非上传成败;若通话已接通/正常结束,不得仅因录音失败就把 `outcome` 改成 `failed`。具体结果字段、期限、未上传时是否仍发一次最终结果待 SaaS 签收;未签收前不能实施或用无限等待代替错误处理。
### 4.3 正常未产生录音:无应答结果(完整独立例,拟定)
```json
{
"schema_version": "call-result.v0.1-proposal",
"event_id": "call-result-003",
"event_type": "call.result",
"dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6",
"tenant_id": "tenant-a",
"tenant_key": "tenant-a",
"trace_id": "trace-c",
"occurred_at": "2026-09-18T10:00:30+08:00",
"aggregate_type": "call",
"aggregate_id": "call-c",
"aggregate_version": 1,
"payload": {
"source_command_id": "command-c",
"execution_id": "execution-c",
"call_id": "call-c",
"task_id": "task-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:00:30+08:00",
"duration_ms": 30000,
"outcome": "no_answer",
"reason_code": "ring_timeout",
"transcript": [],
"opt_out": false,
"recording": {
"status": "not_created",
"reason_code": "no_answer",
"recording_id": null,
"upload_id": null,
"bucket": null,
"object_key": null,
"format": null,
"channels": null,
"sample_rate_hz": null,
"duration_ms": null,
"size_bytes": null,
"checksum_sha256": null
}
}
}
```
**字段说明/消费动作:**这是已接纳、已尝试但无人接听且未产生录音的呼叫;`started_at/duration_ms` 此例表示呼叫尝试起点和尝试耗时,不冒称已接通时长。正常无录音用 `not_created`,资产字段为null,确认终结后即可发送,不申请/等待上传;忙线等正常无录音同类处理,原因须与事实一致。录音本应生成却失败应为 `unavailable` 加明确阶段原因,不伪装正常无录音。普通未接纳拒绝仅有命令回执;stopped未接纳积压无回执也无最终结果,不能虚构call_id。
**额度与文件交付分离:**确认通话终结、执行资源释放就释放通话额度,不等待OSS或最终通知确认,未知仍占额。已产生录音才按4.1/4.2收口,上传失败有限截止时间仍待签收;完整文字汇总可能超过原MQ大小上限,F07须明确预算和失败处理,不能偷偷截断或恢复被移除的实时事件。
## 5. 下一版本签收前不得误用
- 本文 §3 的旧 MQ 命令与 §4 的新通话结果**不能直接混合上线**;下一轮先由 SaaS 与本项目共同发布严格新版 Schema、正反例、哈希和幂等/队列拓扑,再实施生产者与消费者。
- 本文 §3/§4 都是**待签收的下一版 MQ 消息草案**,均不能直接混入现行 v2 合同;先由 SaaS 与本项目发布严格新版 Schema、正反例及新队列拓扑,再实施两端。外呼命令必须有可靠执行身份防重复拨号;控制不带编号/修订,不设计控制去重,其并发/乱序与回执关联后果必须在 F07 明确。
- 取消对外查询与补传命令不取消 D 的持久化恢复、同一身份重投、故障对账和**未知是否已拨号时绝不重拨**。没有核实状态的内部恢复能力不得发布新版本。
- 只在录音上传 OSS 后发布成功通话结果;若录音不能上传,有限等待、可观测故障及最终一次通知的合同必须先签收。通话已完成却无限等待不属于可验收方案。
- 已产生录音的通话在上传OSS后发布含资产的最终结果;正常未产生录音用not_created并在确认终结后直接回传,不等不存在的上传。应有录音却失败/上传超时的有限期限及unavailable结构必须先签收,不无限等待;通话占用释放独立于文件上传。
- 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。
- SIP、任务及新增租户额度 HTTP 响应以[项目草案 Schema](../contracts/config-read-v0.1.schema.json)校验;`tasks` 接口、精简 `call.execute`、无编号任务控制、两种回执及新 `call.result` 均**尚无已发布 Schema**。现行 v2 仍以[`mq.schema.json`](../../contracts/upstream/v1/mq.schema.json)、[`event-payloads.schema.json`](../../contracts/upstream/v1/event-payloads.schema.json)为准,不能拿新示例冒充当前可投消息。