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,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)为准,不能拿新示例冒充当前可投消息。