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
@@ -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或云资源操作。