Files
agent-call/docs/SaaS交互_OpenAPI与MQ契约规划_v0.1.md
T

66 KiB
Raw Blame History

SaaS 交互:OpenAPI 与 MQ 契约规划

版本: v1.0(沿用原文件路径)
状态: 用户已接受方案,作为契约驱动 Mock 开发依据;尚未生成机器可读规范或实现全部接口,非生产验收报告。外部资料由协调取得,真实参数须验证。
依据: 最终开发、部署、监控与验收计划、项目根目录 AGENTS.md
范围: 双方七条业务 HTTP 路径、RabbitMQ 命令/事件、OSS 录音交接;包含按源命令补传,不建设通用开放平台。

阅读约定:授权撤销、租户绑定/传输超限、按命令补传、应用确认等方案已经接受。正文中的首期“建议”按最终计划的已接受方案实施,不再重新拍板;G0 完成 Schema/Mock 配置与可验证用例,未提供的真实供应商、预算、保留/恢复等参数仍需落实。Mock 成功不代表真实接口、供应商或生产容量通过。

本次修订: 方案从建议转为已接受的开发基线,七条路径保持不变;外部依赖按 OpenAPI/MQ Schema 及各自流式/SIP协议 Mock。部署、监控、测试数值、交付门禁统一引用最终计划;不新增拨号入口,不升级 HTTP /v1

1. 已确认的系统边界

  1. RabbitMQ 是唯一外呼执行指令入口,不提供 HTTP 创建通话或重新拨号接口。
  2. 全部业务结果通过 MQ 回传:受理/拒绝、控制生效、呼叫状态、文字、最终结果、录音就绪/失败等。HTTP 查询用于对账,不替代 MQ 事件。
  3. HTTP 仅承担任务控制、命令/通话查询、OSS 上传授权与完成确认、触发历史事件 MQ 补传。HTTP 上传完成确认是存储握手,不是业务结果回调。
  4. 录音先上传 OSS,校验成功并取得 OSS ID 后发布 recording.ready;MQ 不传录音二进制、Base64 或公开播放 URL。
  5. SaaS 负责客户/任务主数据、业务调度、业务重试决策、授权和长期存储;呼出应用只保存必要执行事实、控制屏障、幂等、资源租约和投递记录。
  6. 生产采用多机器、多 EIP 直连;每通电话固定 Cell/出口。SaaS 不逐呼改写共享 SIP 配置,也不能任意指定 SIP 地址、凭证或越权主叫。
  7. 目前只实现 ASR 验证基础,LLM/TTS 协议尚待新规范;本文对完整 AI 链路的描述是目标契约,不代表已启用或验收。
  8. 按 SaaS 提供的 tenant_key 绑定到独立 RabbitMQ 命令队列,由呼出应用调度器负责租户间公平调度。不再采用所有租户共用一个执行 FIFO;同时限制租户发布/积压、预取窗口及跨 Cell 并发/CPS。tenant_key 是 SaaS 业务数据,命令、队列路由和后续 MQ 回调均原样使用;命名规则已统一,运行参数和限值仍待 G0 冻结。

2. 交付拆分与双方职责

交付物/能力 提供方 消费方
呼出应用 HTTP:控制、查询、补传 呼出应用 SaaS 后端
SaaS HTTP:录音上传授权、完成确认 SaaS/存储服务 呼出应用资产处理器
call.execute 命令 SaaS 业务调度器按可信租户归属发布、处理背压 呼出应用公平调度器按租户队列有界接收并准入
执行及资产事件 呼出应用 outbox 投递器 SaaS 事件消费者
MQ VHost、账号、ACL、持久队列和告警 运维按冻结契约配置 双方服务
对外发布规范、样例、错误码和验收用例 用户主导制定 双方评审实施

本文是标识、字段、HTTP 路径、MQ 拓扑/事件和状态语义的唯一维护来源;计划与交付文档只引用,完整样例集中在第 8 节。冻结后交付两个 OpenAPI 3.1 文件及共用 MQ JSON SchemaMQ 拓扑沿用本文表格;AsyncAPI 等有明确工具消费需求再补,不作为首期门禁。D01 以共用 Schema 校验全部示例,区分命令、事件和 HTTP 响应;静态必填核对不冒充完整 Schema 验收,不引入 SDK 生成器或开发者门户。

3. 公共约定

3.1 标识与时间

字段 定义
tenant_id SaaS 租户 ID;请求中的值必须与服务身份获授权范围匹配,不能仅相信报文自报租户
tenant_key SaaS 提供的租户业务数据;命令、队列路由和后续 MQ 回调原样保留,不由呼出应用清洗、编码、截断或重命名
task_id / task_item_id SaaS 任务/号码明细 ID;不因重试改变原明细归属
execution_id 建议新增:SaaS 为一次获授权的业务外呼分配的 ID;网络重试、消息重投和内部线路切换不变,业务重新外呼才新建
command_id 单条执行、控制或补传命令 ID;同一命令重试保持不变
call_id 呼出应用为一次业务执行分配的逻辑通话 ID;一个 execution_id 最多关联一个 call_id
attempt_id 一次线路拨号尝试 ID;合法 FALLBACK 创建新的 attempt,但不创建第二个业务执行
event_id 事件身份;重试/历史补传保留原值及原内容
recording_id / upload_id 逻辑录音 ID / 上传会话 ID;授权续期不产生第二份逻辑录音
oss_id SaaS 存储服务确认的、不透明且稳定的资产 ID;不默认等同于 Object Key、ETag、文件名或 URL
trace_id 链路关联信息,不用于授权或幂等
  • tenant_key 外,ID 均为不透明字符串,不按手机号、数字或 UUID 强制改写已有 SaaS ID;建议长度 1–128,拒绝控制字符、路径分隔符和空白首尾。最终字符集在 Schema 冻结。
  • tenant_key 不适用上述本地 ID 格式限制;呼出应用不清洗、编码、截断或重命名,正文、队列及路由绑定值须精确一致。该原值一致性规则不免除服务身份/可信归属检查;传输字段可承载性另见第 5.1.1 节建议。
  • 时间使用 RFC 3339 UTC,例如 2026-09-11T08:00:00.000Z;持续时间字段以 _ms 结尾,大小为字节,采样率为 Hz。
  • 未接通时 answered_atnull,不填写虚假接通时间;未完成的结果用明确状态,不用空字符串冒充成功。
  • 所有唯一键和检索均包含租户作用域。不同租户碰巧使用相同 ID,不能互相查询或命中幂等记录。

3.1.1 租户绑定与原值生命周期(已确认)

  • 建议由 SaaS 维护可信的 tenant_id ↔ tenant_key 一对一注册关系,平台据此创建/发现队列;业务身份、配额、公平份额及去重始终按 tenant_id 聚合,不按队列数量另发额度。正文/队列/routing key 精确一致仍须同时满足可信注册归属,不能让自报值认领租户。
  • tenant_key 是原始字符串;JSON 转义、UTF-8 传输、管理 API 路径转义及 ACL 正则字面量转义仅是协议表示,解码后的业务值必须完全相同。不能把原值直接拼成 ACL 正则、Shell、文件路径或 SQL。这里不增加字符清洗、大小写转换或业务格式限制。
  • 建议首期不支持原地变更 key;保留期内不向别的租户复用旧绑定。迁移须另行评审停发、屏障、队列排空、去重/事件保留与恢复规则,不自动创建第二队列来绕过额度。
  • 命令、任务、执行和 outbox 持久保存当时的 tenant_key;历史回传使用原快照,不能按最新配置重新写值。幂等键仍为 (tenant_id, command_id)(tenant_id, execution_id)(tenant_id, event_id)不加入 tenant_key;同 event_id 异内容应隔离告警而非重复应用。
  • HTTP 控制可早于首条 executetenant_key 从服务身份获授权的可信注册关系解析并保存,而不是从首次执行猜测。绑定不可用或不一致时明确拒绝/报依赖不可用,不确认 accepted,更不能伪造 key 发送事件;任务归属另按第 4.1 节验证。

3.2 HTTP 共性

  • 使用 HTTPS,仅服务到服务调用,不向浏览器或公网客户直接暴露内部接口。
  • 公共请求头:Authorization: Bearer <service-token>X-Tenant-IDX-Request-ID;有 JSON 请求体时使用 Content-Type: application/json。可传播 traceparent
  • Bearer 凭证优先接入双方已有服务身份体系,不另建账号平台;建议短期且限定 audience/scope/租户的令牌。跨管理网场景可增加 mTLS,具体签发/轮换方式列入 G0。
  • POST 必须提供 Idempotency-Key。控制/补传接口要求它等于 command_id;存储接口为一次逻辑操作生成稳定键。
  • 同租户、服务调用方、操作及幂等键下,相同语义请求返回原操作标识;请求内容不同返回 409 IDEMPOTENCY_CONFLICT。服务端对校验后的语义字段比较,不因 JSON 字段顺序产生冲突。
  • 202 Accepted 仅代表命令与 outbox 已可靠持久化,返回查询位置;不是控制已生效或补传已完成。普通查询返回 200,首次创建上传会话返回 201,完成存储确认返回 200
  • 同步校验/鉴权失败返回 HTTP 错误,不代表接受了命令;未通过鉴权的请求不产生租户业务事件。持久化受理后的业务结果必须走 MQ。
  • 成功响应直接返回资源对象;错误使用 application/problem+json,含 typetitlestatuscodedetailrequest_idretryable,不暴露堆栈和凭证。

3.3 重试及数据期限

  • HTTP 超时或连接断开:先按原命令/幂等键查询或重试,不换 ID。只对网络错误、429 和约定可恢复 5xx 做有上限退避;遵守 Retry-After,不得无限重试业务拒绝。
  • 命令去重、控制屏障和事件保留期必须覆盖最大合法重投/恢复窗口;数据过期后不能把旧执行当作新执行。
  • 执行 ID 去重记录的清理需依赖“该授权已永久失效”的可靠依据;控制停止墓碑同理。未建立安全清理机制前,不能仅凭普通 TTL 删除防重拨屏障。
  • 已知但超出事件补传保留期返回 410 REPLAY_EXPIRED;不存在或无权访问资源统一返回 404,避免跨租户枚举。

4. HTTP OpenAPI 规划

两个服务分别使用自己的 Base URL,不共享服务实现;下表路径均为相对各自服务根路径。

提供方 方法与路径 最小权限 结果
呼出应用 POST /internal/v1/outbound/tasks/{task_id}/controls outbound.control 202;持久化控制命令
呼出应用 GET /internal/v1/outbound/commands/{command_id} outbound.read 200;命令与生效状态
呼出应用 GET /internal/v1/outbound/calls/{call_id} outbound.read 200;执行事实与资产/投递快照
呼出应用 POST /internal/v1/outbound/calls/{call_id}/replays outbound.replay 202;受控历史事件补传
呼出应用 POST /internal/v1/outbound/commands/{source_command_id}/replays outbound.replay 202;无 call_id 的原命令结果补传
SaaS POST /internal/v1/outbound/recording-uploads recording.upload 201/200;创建或取得原上传会话
SaaS POST /internal/v1/outbound/recording-uploads/{upload_id}/complete recording.complete 200;存储验证成功,确认 OSS ID

4.1 任务控制

请求字段:

字段 必填 规则
command_id Idempotency-Key 一致
action pause / resume / stop
expected_task_revision SaaS 认为当前应有的控制版本;CAS 成功后服务端生成目标版本 expected + 1
active_call_policy stop 时是 drain / hangup;不设隐藏的强制挂断默认值
reason 有界审计原因,不放敏感客户资料

响应字段:command_idtask_idstatus=acceptedrequested_task_revisionaccepted_atLocation 指向命令查询接口。

建议状态与竞争规则:

  • task_revision 仅表示任务控制栅栏版本,不表示智能体配置版本。首版建议初始版本为 1,任务初始运行;尚未收到 execute 时的控制,也必须能为获授权任务建立持久屏障。
  • 基于服务身份及双方可信任务绑定验证归属;不能因第一次见到任意 task_id 就将其认领给请求租户。任务绑定来源在 G0 确认,可来自获授权 SaaS 主数据或其受信发布边界,不额外开放拨号入口。
  • 同任务控制串行化,提交时 CAS 校验 expected,服务端在同一事务生成并保存目标版本 expected + 1;控制请求不接受 task_revision,响应/查询/MQ 仍区分 requested_task_revision 与 applied_task_revision。版本冲突或另一个控制尚在生效中返回 409;重复 command_id 先走幂等并返回原目标版本,不重复递增。执行命令的 task_revision 不变,仍必须使用已生效版本。
  • 暂停/停止受理后,先禁止发放新的拨号许可,再等待所有相关 Cell 上的在途许可完成或失效;确认后才能回传 applied。存在状态不明节点时保持 applying/reconciling,不能虚报全局生效。
  • 每次真实发起和 FALLBACK 都检查当前控制版本、状态、授权期限及有效资源租约;节点失联或租约过期停止新发起。仅把数据库改为 paused 不足以形成屏障。
  • pause:已拨出和已接通电话继续原生命周期;未发起执行不再启动,报告阻止原因。resume:允许新版本指令,不自动重放旧积压。
  • 旧版本命令拒绝;未来版本命令也不自动缓存为可执行。SaaS 如需重新调度被阻止且确认未拨号的执行,先完成对账及业务授权,再生成新的执行命令,不能由消费者静默换 ID 重试。
  • stop + drain:停止新发起,已有电话自然结束;stop + hangup:额外需要 outbound.hangup 权限,审计后终止该任务已有通道。挂断未确认前不能宣称 hangup 控制已完全生效。
  • stopped 不允许 resume;重新开展业务使用新任务,避免迟到指令复活。控制失败后不能自行放开已经建立的暂停/停止屏障。
  • HTTP 查询和 MQ command.result 均区分 requested revision 与 applied revision;这项多 Cell 屏障能力为待实现项。

4.1.1 已排队执行的授权撤销(已确认)

首期建议复用任务级 pause/stop 屏障,允许为安全暂停整个受影响任务,暂不增加号码级控制接口;不能仅发布另一条 call.execute 或仅停止新增发布来撤销旧命令。

  1. SaaS 持久化拒绝再联系/撤销标记,并与任务生成及发布出队的授权检查协调,立即阻止该业务对象产生新的授权执行。持久追踪所有受影响任务及待发布、发布不确定、已发布和已受理的执行;不能只查 broker 中可见消息。
  2. 对所有受影响且未停止的任务提交现有 pause 控制,保存各任务的 command_id/revision。控制必须覆盖 broker 积压、waiting、已提交但尚未真实发起的意图以及 FALLBACK;不能通过改写/删除共享队列中的号码消息实现撤销。
  3. 全部相关任务屏障 applied、在途许可已确认不能再发起后,SaaS 才标记执行侧撤销生效。HTTP 超时、CAS 冲突、已有控制处理中或 Cell 失联时保持 pending/reconciling,原 ID 重试或对账,不虚报成功、不放开发布。已发出的电话按既定生命周期继续,强制挂断须另有授权。
  4. 后续若恢复受影响任务,必须再次过滤撤销对象、完成旧执行事实对账,并使用当前已生效版本的新授权执行;旧 command_id/execution_id 原样重投仍命中原事实,不静默复活。stopped 任务不恢复。
  5. SaaS 只在允许拨号时段内发布,not_after 不晚于本次允许时段结束;时段/授权在发布后被提前收紧也触发上述屏障。单靠原 not_after 不能感知新撤销。

contact.opt_out 经 MQ 到达 SaaS 后触发上述流程。须分别测量客户提出→事件落库→SaaS 禁止新发布→各任务屏障 applied 的时间;MQ 延迟期间不能宣称已全局撤销。G0 冻结生效时限、关联任务枚举依据及整任务暂停的业务代价;若要求立即生效或必须逐号码隔离,需另评审撤销机制,不擅自降低 MQ-09 验收标准。

4.2 命令查询

通用返回:command_idcommand_typetask_id/call_id(适用时)、statusreason_codeaccepted_atupdated_at

  • executeaccepted / waiting / rejected / executing / completed / reconcilingaccepted 仅表示可靠受理,waiting 表示已进入有界准入等待,executing 表示已提交拨号意图、不等于 SIP 已实际发出或接通;completed 指执行生命周期结束,不等于客户接通或业务成功。
  • controlaccepted / applying / applied / rejected / failed / reconciling,另含 requested/applied revision 和当前任务状态。
  • replayaccepted / running / completed / failed,另含快照截止点、已选/已确认发布数量;completed 不代表 SaaS 已入库。
  • HTTP 校验阶段就被拒绝、未持久化的命令允许查询不到;MQ 收到的合法业务拒绝应持久化并可查询。

execute 查询及对应 command.result 建议共用以下字段,MQ 的等待/状态变化仍按 command 聚合版本合并:

字段 规则
execution_id 原业务授权执行 ID,受理后始终可关联
call_id 首次资源准入成功并提交拨号意图时创建;此前为 null,不伪造通话记录
wait_reason_code waiting 时必填:SCHEDULER_WAITTENANT_CONCURRENCY_EXHAUSTEDTENANT_CPS_EXHAUSTEDROUTE_CAPACITY_EXHAUSTEDCAPACITY_EXHAUSTED;非 waiting 为 null
waiting_since 首次进入 waiting 的服务端时间;原因变化不重置,离开等待后保留;未等待为 null
admission_deadline 受理时固定的首次发起准入截止时间;拒绝于受理前时为 null,重投/重启不延长
updated_ataggregate_version 当前命令快照的更新时间和版本;HTTP 与 MQ 使用同一版本域。HTTP 为响应字段,MQ 的 aggregate_version 只在事件外壳中,不重复放入 payload
  • waiting 的原因表示当前主要阻塞项,不是完整资源诊断或预计开始时间;多个条件同时不足时按 G0 冻结的优先级选择。原因改变可发布新版本快照,不按轮询周期重复发事件。
  • 首次发起前以 (tenant_id, command_id) 查询;尚在 broker 队列、未被呼出侧持久化的命令允许返回 404。404 不证明消息未发布或电话一定未发生,不能据此换 execution_id 重拨;SaaS 保留原发布记录并用原 ID 对账/有限重试。
  • 状态路径为 accepted → waiting → executing → completed,有资源时可跳过 waiting。尚未提交拨号意图的 accepted/waiting 可因过期、控制或准入超时进入 rejected;提交意图后出现不确定性进入 reconciling,不回退 waiting 或以“未拨号拒绝”释放未知占用。

4.3 通话查询

返回 call_idexecution_id、任务关联、call_statecall_version、起止时间、结果原因及以下独立信息:

  • attempts:每次 attempt 的状态、原因、trunk_idegress_pool_idcell_id、时间;不暴露 SIP 密码或内部管理地址。
  • transcript:处理状态、最终稿数量及是否仍有待完成段落;首版查询不返回整段文字,文字正文通过 MQ 到 SaaS。
  • recordings:录音标识、处理状态、确认后的 oss_id 和失败原因;不返回上传凭证或播放链接。
  • delivery:待投递/失败数量、最后确认发布时间。RabbitMQ confirm 仅表示 broker 接收,saas_applied 未有应用层证据时必须为 unknown,不能猜测为已入库。
  • snapshot_at;查询是某一时点的执行快照,SaaS 根据版本合并,不用旧快照覆盖较新事件。

4.4 历史事件补传

请求体仅包含 command_id(本次补传操作 ID)和 reasonIdempotency-Key 等于本次 command_id。首期不支持事件类型/ID 筛选,额外筛选字段按 INVALID_ARGUMENT 拒绝,不能静默忽略后扩大重放范围。

  • POST /internal/v1/outbound/calls/{call_id}/replays:整体补传当前租户、该 call_id 关联的已保留历史业务事件,包含通话/文字/录音及关联执行命令结果;不跨通话、不包含补传操作自身结果。
  • POST /internal/v1/outbound/commands/{source_command_id}/replays:整体补传当前租户、aggregate_type=commandaggregate_id=source_command_id 的原 command.result;无 call_id 也可恢复。source_command_id 不得等于本次 command_id。两条路径均校验 outbound.replay 及资源归属;不存在/无权访问返回404,已知保留期过期返回410。
  • 两种补传均返回 202 及命令查询 Location;本次状态为 accepted/running/completed/failedcompleted 仅表示范围内消息完成 broker 确认,不表示 SaaS 应用。
  • 受理时持久化租户、资源范围和固定事件截止点;重复请求复用原任务/截止点,不纳入后来新事件。执行时分批读取,不因“整体补传”一次加载全部历史到内存。
  • 所有被选事件必须来自原持久化事件记录;保留 event_id、原时间和原 payload,可仅在传输头标记 replay。禁止重新合成历史业务事实。
  • 只重发结果/文字/录音元数据;不执行拨号、不重放 call.execute、不重传录音文件、不重新调用 LLM/TTS。
  • 对补传限速并独立计量,避免挤占实时结果队列;部分成功允许重试未确认部分,重复投递由 SaaS 去重。
  • 补传命令自身结果也走 MQ command.result;不把本次补传生成的结果循环纳入本次选择集。

4.5 录音上传授权

请求:recording_idcall_idcontent_typesize_byteschecksum_algorithmchecksumchannelssample_rate_hzduration_ms。上传前须封口文件,确保大小和校验值稳定。

返回:upload_idrecording_idexpires_atupload_methodupload_urlrequired_headers、约束及已完成会话的 oss_id(如有)。

  • 首版建议单文件签名 PUT;最长录音推导的文件上限不满足单 PUT 或恢复窗口时,再增加分片协议,不在本版虚构分片接口。
  • SaaS 按 tenant/call/recording 绑定会话并分配存储位置,不接受调用方任意 bucket/object key。因 MQ 延迟尚无通话记录时,不绕过归属检查;返回可重试 CALL_NOT_REGISTERED,呼出侧保留本地文件。
  • 同一逻辑录音禁止因重复申请产生第二份资产;授权失效后重新请求原会话,可返回新的临时签名,但会话、对象和内容约束保持不变。更换内容必须使用明确的新录音版本。
  • URL 为受控 OSS HTTPS 目标,服务端限制可访问域名/存储端点、不跟随任意重定向,防止上传器成为 SSRF 或外传通道。
  • 凭证只允许指定对象写入,不授予列桶/删桶/任意对象权限;完成对象应不可被原授权继续覆盖,使用 OSS 禁止覆盖约束或可靠对象版本固定机制。
  • 授权响应 Cache-Control: no-store;签名 URL、token、required_headers 中的秘密不得进入日志或 MQ。

4.6 上传完成确认

请求:recording_idsize_byteschecksum_algorithmchecksum,可带 OSS 返回的 etag(仅作辅助,不当作文件内容摘要)。

  • SaaS 根据 upload_id 找到受控目标,核验调用方、对象实际存在、大小、实际内容校验及录音绑定;不能只相信客户端提交的 checksum 或对象自报元数据。
  • 校验算法需选择 OSS/存储服务能够独立验证的机制并冻结;无法独立验证时不能声称校验成功。ETag 尤其在分片/加密情况下不等于文件 MD5。
  • 对象未就绪返回可重试错误;内容冲突进入隔离/人工处置,不创建就绪资产。
  • 验证成功,幂等返回 upload_idrecording_idstatus=verifiedoss_idverified_at。确认超时后用原幂等键重试,不能新建第二份录音。
  • 呼出侧把 verified 事实与 recording.ready outbox 同事务持久化,再由 MQ 投递。SaaS 只有消费 ready 后才将资产作为业务录音关联展示。
  • 任一步失败通过 recording.failed 回传可恢复性与原因;未取得有效 oss_id 不发布 ready。本地文件清理受恢复期限和已可靠持久化交接证据约束,不能仅收到 HTTP 200 就无条件删除。

5. RabbitMQ 拓扑与传输

5.1 最小拓扑与命名规则

对象 名称/绑定 用途
环境 VHost /agent-call-<env> 测试/生产隔离,具体路径由运维配置
命令 Direct Exchange agent-call.commands.v1 direct、durableRouting Key 为 agent-call.tenant.{tenant_key}.call.execute,消息类型仍为 call.execute
租户独立命令队列 agent-call.executor.{tenant_key}.v1 一租户一队列,精确绑定 agent-call.tenant.{tenant_key}.call.execute;公平调度器有界读取,共享持久去重存储
事件 Topic Exchange agent-call.events.v1 发布第 7 节允许的事件;Routing Key 为 agent-call.{event_type}
SaaS 事件队列 agent-call.saas.events.v1 SaaS 消费落库
死信 Exchange agent-call.dead.v1 隔离无效消息与耗尽重试
死信队列 agent-call.commands.dead.v1agent-call.events.dead.v1 分方向审计与受控恢复
  • agent-call 是 VHost、Exchange、Queue 和 Routing Key 的固定命名空间。命令键固定为 agent-call.tenant.{tenant_key}.call.execute;事件键固定为 agent-call.{event_type}。AMQP type 字段仍使用无前缀的 call.execute 或事件类型。
  • tenant_key 是 SaaS 提供的原始业务数据,不是呼出应用重新生成的路由标识;不做清洗、编码、大小写转换、截断或其它业务格式限制。命令、队列路由、消息正文和所有后续 MQ 回调原样使用;正文值与队列/路由绑定值须精确一致,同时验证可信归属;传输上限处理见第 5.1.1 节,不擅自改写业务值。
  • 命令 exchange 使用 direct,使 tenant_key 中的点号、*# 等字符不会被当作 topic 通配语义;不得将命令 exchange 改回 topic 并依赖格式限制规避冲突。
  • 本轮仅改变执行命令的租户隔离拓扑,事件队列不自动扩展为一租户一队列;事件消费和补传仍需限速及租户校验,不能由此宣称结果链路已有同等等待时延保证。
  • Exchange/Queue durable,业务消息 persistent;生产建议 quorum queue,最终以现有 RabbitMQ 版本及 HA 部署确认,不把 durable 单节点当高可用。
  • 命令/事件分别使用最小权限账号,TLS 连接;SaaS 不能消费执行器队列,呼出应用不能消费 SaaS 事件队列,也不能发布 execute 指令。
  • 所有生产者使用 publisher confirm + mandatory/不可路由检查;消费者手动 ACK,只在本地必要数据持久化成功后 ACK。
  • 队列不替代业务状态库;多消费者不承诺全局顺序。业务版本负责乱序合并,数据库约束和执行租约负责去重/准入。
  • 可信 SaaS 生产者可被授权发布多个租户;发布者身份来自 broker 账号及受控发布边界,不信任可伪造的 message header。若开放非受信租户直连 MQ,必须另做租户隔离设计,本期不开放。
  • 命令业务过期以 not_after 校验为准,队列 TTL 仅用于积压治理。过期命令应有可追踪的拒绝/隔离记录,不静默消失;DLQ 恢复不能绕过有效期和停止屏障。命令重试/恢复必须回到原租户调度域并受相同配额约束,不经全局重试 FIFO 直接抢占执行。共享死信队列只用于隔离审计,不是执行入口。
  • 消费瞬时故障不得无限 nack(requeue=true) 热循环;采用有上限的延迟重试/持久重试记录。若发布到重试队列,必须等新发布确认后再 ACK 原消息,并依靠幂等承受重复。
  • 死信路径也要验证不丢失;quorum 至少一次 dead-letter 或应用确认后转存二选一落实,不能默认普通 DLX 搬运具有端到端保证。
  • 非法 Schema/未知主版本进入隔离队列,不凭不可信字段向任意租户发送错误事件;可安全识别的合法业务拒绝发布 command.result 后完成处理。

5.1.1 tenant_key 的传输可承载性(已确认)

原样透传不等于 RabbitMQ 可承载任意长度。AMQP 0-9-1 routing key 使用 shortstr,最多 255 字节;当前 agent-call.tenant. + .call.execute 固定占 31 字节,tenant_key 可用预算为 224 个 UTF-8 字节。队列名也受 broker 字节上限约束,应分别检查完整队列名与路由键;direct 只消除 topic 通配语义,不消除长度限制。

建议注册/发布前做完整传输字段的可承载性检查,不清洗、不截断、不替换原值。超限时停止为该绑定创建队列和发送执行命令,SaaS 持久保留原始任务并明确标记传输不支持、告警;未发布/未受理的状态不是呼出侧 MQ 业务拒绝,不凭空发 command.result,也不无限重试相同不适配值。其它租户不受该注册失败阻断。

超限停发且保留原任务的方案已接受;接入时用实际 SaaS 数据和 broker/client 版本验证承载能力。若业务后来要求必须接收更长 key,再走传输标识分离的变更评审。本版不擅自引入哈希/编码映射,也不宣称已支持任意长度。契约测试覆盖 224/225 字节、中文多字节、点号、星号、井号及 ACL 正则特殊字符;允许值在命令和所有回传中逐值相等。

5.2 传输属性

content_type=application/json、UTF-8、delivery_mode=2message_id=command_id/event_idtype=command_type/event_typecorrelation_id 可使用 command_id,正文 trace_id 负责跨环节跟踪。

建议消息上限 256 KiB、HTTP JSON 请求体上限 64 KiB,均待容量/样例验证;超限拒绝或按已定义的文字分段规则发送,禁止静默截断最终文字。MQ 不传文件或服务凭证。

5.3 租户公平调度(架构已确认,参数待冻结)

SaaS 持久任务/发布记录
  → 命令 Exchange:按 SaaS tenant_key 原样路由
    → 租户 A 命令队列 ┐
    → 租户 B 命令队列 ├→ 公平调度器 → 租户额度+完整资源租约 → 固定 Cell/出口
    → 租户 C 命令队列 ┘
  1. 责任分离: SaaS 决定业务任务、号码和重新外呼授权,并按租户发布;呼出应用决定租户间执行机会和物理资源分配。不要求 SaaS 等待上一租户批次执行完才发布下一租户,也不复制其任务编排系统。
  2. 轮转而非排空: 默认建议活跃且可调度租户等权轮询,差异化服务获确认后采用加权轮询。每轮每租户最多取有限条命令;租户没额度、线路不可用时跳过,不能阻塞全局循环。新活跃租户应及时加入轮转,不能等待 A 队列排空。权重由平台配置,不接受每条消息自报高优先级。
  3. 有界预取与持久待执行窗口: 按租户及全局限制 prefetch/未 ACK 和已 ACK 未发起数量;队列不等于进程,一租户一队列不要求一套服务。禁止先把全部租户消费到同一个无界内存 FIFO;已持久化的待执行命令及重启恢复也按租户公平选取。prefetch 本身不是公平算法。
  4. 配额双重检查: 读取前检查租户可接收窗口/资源可用性,持久化命令及去重/outbox 后 ACK;这不是提前保证拨号容量。实际发起必须原子取得租户额度并同时满足供应商并发/CPS、Cell 端口、出口健康、AI 配额和控制屏障,失败释放未使用预留。已受理命令的有限等待/拒绝按第 6.2 节处理;尚未取出的命令受队列积压上限及有效期约束。
  5. 多实例与恢复: 多个调度器需通过租户调度所有权/租约及隔离令牌协调同一租户的轮次、窗口和额度,不能每个实例独立再分一份。公平性覆盖整个调度域,不只覆盖每个进程自己的队列。失去所有权的旧实例停止分配;已拨通但状态不明的占用不能仅因租约到期就释放,需对账,防止超额与重复拨号。具体协调实现待设计评审。
  6. 公平的边界: 保证有可用资源和租户额度时不被另一租户积压饿死,不保证相同接通数或相同通话时长。资源已被活动通话占满时等待释放,不为公平挂断电话。若要明确开始时限,另确认不可借用保底容量或受限借用策略,覆盖线路/AI/Cell 全链路;不能把空闲份额借出后又承诺立即收回。
租户限制 统计范围/约束
并发占用上限 跨所有 Cell 汇总,包含发起预留、拨号、振铃、接通及待对账占用;从发起前占用到确认资源结束后释放
CPS 上限 跨所有调度实例和 Cell 统计真实拨号尝试,包括 FALLBACK;切线路不重置租户额度
接收/待发起窗口 每租户和全局的未 ACK、已持久化未发起数量均有界;拒绝/过期处理不得绕成新的无界工作队列
发布速率、队列容量 每租户限制消息数/字节、发布速率并辅以全局 broker 水位保护;独立队列仍共享磁盘、内存及网络
调度权重 默认建议等权;只改变有竞争时新增许可份额,不绕过硬上限,不保证通话时长比例

5.4 队列生命周期与背压

  • 平台依据可信租户注册关系幂等创建队列、精确绑定和权限,再允许 SaaS 发布;启动/故障恢复能重新发现活跃队列,不依赖仅存在于内存的清单。租户停用先拒绝新发布并建立控制屏障,完成积压/活动通话/重试及去重保留处置后才能删除队列,不以自动过期直接删积压。
  • 本期仅可信 SaaS 发布服务连接 broker,最终用户不直连。发布服务强制执行每租户路由、速率和容量策略;AMQP 普通资源 ACL 不能替代可信发布服务的逐租户授权校验。
  • 队列满采用明确拒绝发布的溢出策略(建议验证 reject-publish 与 quorum 的组合),不能丢弃队头旧命令来给新命令让路。SaaS 任务/发布记录持久保留;nack、不可路由或 confirm 不确定时,有限退避后使用原 command_id/execution_id 重试,不换 ID、不转投其他租户队列。broker blocked/全局水位达到阈值时也要背压。
  • 消费端的公平轮转不能隔离发布洪峰的全部成本;大任务可长期保留在 SaaS 数据库,向本租户 MQ 分批有界推送。允许积压但不允许无限灌入;不能以独立队列为由承诺任意租户数、无限 quorum 副本或无限 broker 吞吐。
  • HTTP 暂停/停止控制保持原路径,不排在 A 的大批 execute 后;实际发起前的屏障校验仍强制执行。补传/失败重试不能绕过租户配额占满执行资源。

发布结果必须与呼出应用的业务受理区分:

AMQP 结果 SaaS 处理
confirm ACK 且未收到 mandatory return broker 已接收并路由,不代表呼出应用已受理、已准入或已拨号
mandatory return(不可路由) 持久保留原发布记录,修复租户队列/绑定后在有效期内原 ID 有限重试;不得转投其他租户
confirm NACK(含队列满时拒绝发布) 记录发布未成功并退避;原因依 broker 诊断,不能把所有 NACK 都当成队列满
confirm 超时、连接中断或 broker blocked 状态不确定/发布受阻,实施背压;原 ID 有限重试并接受可能重复,不能假定消息未入队

以上不是 HTTP 429,也不是 MQ command.result 业务拒绝;队列创建/绑定/配额通过受控平台配置落实,首版不新增管理 API。正文租户、路由键、队列绑定的正反例和 broker 版本相关的满队列行为必须纳入契约测试。

6. 执行命令 call.execute

6.1 字段

通用外壳:schema_versioncommand_type=call.executecommand_idtenant_idtenant_keytrace_idissued_atnot_afterpayload

payload 必需:

字段 说明
execution_idtask_idtask_item_id 业务授权执行及任务归属
task_revision 必须等于当前生效且允许运行的控制版本
callee 原始业务被叫字符串,不提前拼供应商前缀
route_policy_id 已配置且获租户授权的线路策略引用,可只包含一个供应商
caller_profile_id 已授权主叫引用,不允许 arbitrary From/PAI
agent_version_id 服务端已可解析的不可变智能体配置版本
variables 经字段白名单、类型/长度校验的运行变量,不是任意代码或任意 URL
ring_timeout_msmax_call_duration_ms 不超过服务端和供应商上限

agent_version_id 必须对应呼出侧可用且可信的快照;配置如何同步、LLM/TTS 参数和取消/音频契约留待新规范。缺失配置明确拒绝,不私自读取旧 voice_test LLM/TTS 或任意远程 URL。

线路策略由呼出侧解析为授权 trunk_id + egress_pool_id + cell_id,实际选择写入事件。呼叫固定 Cell/出口;允许的 FALLBACK 也必须兼容同一 Cell/出口及供应商白名单,无适合备用就失败,不迁移活动通话。当前只有一家线路,不虚构 backup。

6.2 幂等与发起

  1. 验证身份边界、Schema 和 tenant/task 关联;对已记录的 command_id/execution_id 先检查语义冲突并返回/关联原结果,不因重投时已过期或控制已变而改写原事实。新执行再检查有效期、控制版本和配置/线路授权;防重查重不绕过当前调用方的租户访问校验。
  2. 事务记录命令,使用 (tenant_id, command_id) 唯一键;另以 (tenant_id, execution_id) 保护业务执行,换 command_id 也不能绕过同一次执行去重。
  3. 同 execution_id 的语义冲突拒绝;相同执行重复只关联原结果,不能再次拨号。接受/拒绝事实及其 outbox 同事务提交,随后 ACK。
  4. 在租户公平调度下,实际拨号前原子取得跨 Cell 的租户并发/CPS 额度及供应商并发/CPS、Cell 端口/出口健康和 AI 配额等完整租约,重新检查有效期与控制屏障。已受理不等于已获得资源,FALLBACK 不绕过租户额度。
  5. 受理时固定 admission_deadline = min(not_after, accepted_at + 服务端准入窗口),窗口值在 G0 冻结,不允许消息自报延长。broker 中未消费的等待不计入该窗口,但始终消耗 issued_at→not_after 的授权期限;取出时已过期直接拒绝。等待进入/原因变化及退出通过第 4.2 节查询和 MQ 事件表达。
    • 首次发起在持久化意图前检查:now >= not_after 优先拒绝为 COMMAND_EXPIRED;否则 now >= admission_deadline 按当前资源瓶颈拒绝,返回对应租户/线路/系统资源原因码;纯调度等待或截止时资源刚释放但尚未发起,均返回 ADMISSION_TIMEOUT。拒绝原因写入 reason_code,非 waiting 的 wait_reason_code 清空,保留 waiting_since 和 admission_deadline。到期处理不依赖资源释放或该租户再次获调度,必须有界扫描/唤醒并终结待发起命令;允许的处理延迟在 G0 冻结。
    • 已受理后拒绝需持久化并经 MQ 发布,不因原消息已 ACK 而丢失终结结果;准入失败释放已确认未使用的预留,不释放状态不明占用。是否新建获授权业务执行由 SaaS 对账后决定,不无限排队或私自重拨。
    • admission_deadline 只约束首次发起,不挂断已拨出/已接通电话;not_after 是每次新拨号尝试(包括 FALLBACK)的发起截止时间,不是活动通话的强制结束时间。FALLBACK 仍受总尝试/执行限制、控制屏障、租户 CPS 和完整资源准入约束,不能借其延长首次等待。
  6. ARI 发起前持久化意图、call_id/attempt_id 和固定通道关联;执行器真实发起前再次检查有效期、控制/租约及首次准入截止,不能仅凭早先提交意图越过截止。已提交意图但确认尚未发出且授权失效时,以 completed 和对应失败/取消的 call.finished 收尾,不回退为无 call_id 的 rejected;是否已发出不明时进入 reconciling。网络超时/重启先对账,已接通或旧通道未确认结束时禁止创建备用尝试。

7. MQ 事件契约

7.1 通用外壳与合并

必需:schema_versionevent_idevent_typetenant_idtenant_keytrace_idoccurred_ataggregate_typeaggregate_idaggregate_versionpayload

  • 聚合标识按 command/call/transcript_segment/recording 分域;版本由对应事实持久化事务单调递增,不依赖消息到达时间。
  • command/task/call/attempt 关联按事件类型必填;尚未生成 call_id 的拒绝不伪造 call_id。
  • SaaS 以 (tenant_id, event_id) 建 inbox 唯一键,并将业务更新和 inbox 同事务提交,成功后 ACK。
  • 版本比较限定同一实体及同一状态域。较旧事件不回退状态,但仍可补充尚未落库的独立 attempt/录音记录;不能用一个全局“最大版本”丢弃其他资产或文字。
  • 事件 payload 是该事件声明的状态域快照;call.finished 的终态与资产处理状态分别合并,不能覆盖后到或先到但更新的 recording.ready。
  • 不承诺端到端 exactly-once;采用至少一次传输、持久去重及外部拨号副作用对账。

7.2 事件列表

Routing Key / event_type 必需业务数据 合并规则
agent-call.command.result / command.result command_id、command_type、status、reason_code;适用的任务/执行/通话关联及版本;execute 增加第 4.2 节等待/准入字段 以 command 聚合版本更新;区分 accepted、waiting、executing、applied、completed;不把等待当成已拨号
agent-call.call.status / call.status call_id、execution_id、任务关联、call_state、call_version、attempt_id、attempt 状态、实际线路/Cell/出口、时间/原因 保持 call/attempt 各自状态,不让迟到 ringing 回退 answered/ended
agent-call.transcript.updated / transcript.updated call_id、turn_id、segment_id、role、revision、text、is_final、start_ms、end_ms、playback_state 同 segment 较高 revision 替换;最终稿不能被迟到中间稿覆盖
agent-call.call.finished / call.finished call_id、execution_id、任务关联、call_version、outcome、起止时间、时长、原因、attempt 汇总及资产处理快照 固定通话终态;后处理未完成时标为 pending,不阻塞终态
agent-call.recording.ready / recording.ready call_id、recording_id、oss_id、格式、声道、采样率、时长、大小及校验 只接收已验证资产;不包含上传凭证或公开 URL
agent-call.recording.failed / recording.failed call_id、recording_id、stage、reason_code、retryable、next_retry_at(若有) 标记资产故障,不改变通话终态;后续合法 ready 可完成恢复
agent-call.transcript.failed / transcript.failed call_id、原因、retryable、受影响 segment(适用时) 明确文字不完整,不能把部分文本假装最终完整记录
agent-call.contact.opt_out / contact.opt_out call_id、task_id、task_item_id、请求时间、关联 turn/segment(若有) SaaS 及时持久化拒绝再联系并阻止后续业务调度;不等待挂断

后两类事件补齐文字失败及拒绝再联系的可追踪闭环;opt_out 的触发判定需由业务及新 AI 规范确认,不能默认靠未经验证的关键词误判。

文字 role 建议为 customer / agent / systemplayback_state 使用 not_applicable / generated / sent / playback_confirmed / cancelled / unknown。只能报告实际具备的播放证据,不能将“已生成/已发送”写成“客户已听见”。超长 turn 拆成稳定 segment;最终稿是否允许修订及保留要求由 G0 冻结。

7.3 通话与资产状态

  • call_statequeued → dialing → ringing → answered → ended,允许省略未发生的阶段。queued 仅表示已创建 call_id 并持久化意图但尚无实际发起证据;资源准入前的 waiting 属于命令状态,不提前创建通话。dialing/振铃/接通须有对应执行证据。失败可从任一未接通阶段进入 ended;FALLBACK 保持逻辑通话未结束并新增 attempt,不能回退成新业务通话。
  • outcomecompleted / busy / no_answer / rejected / failed / cancelled 等标准值;completed 仅表示正常结束,不是营销成交。最终值及 SIP/Asterisk 映射待供应商确认。
  • 对账状态单独为 reconciling,不要凭本地连接断开生成虚假通话终态。
  • recordingpending → uploading → verifying → ready,失败另记 retryable/原因;transcriptpending/streaming/finalized/faileddeliverypending/broker_confirmed/failed。三类后处理不改写 ended。

8. 最小交互样例

以下 ID 为示例,不是现网资源、真实号码或可调用配置。

8.1 暂停任务

POST /internal/v1/outbound/tasks/task-demo/controls
Authorization: Bearer <service-token>
X-Tenant-ID: tenant-demo
X-Request-ID: req-demo-01
Idempotency-Key: cmd-pause-demo
Content-Type: application/json

{"command_id":"cmd-pause-demo","action":"pause","expected_task_revision":1,"reason":"operator_pause"}
{"command_id":"cmd-pause-demo","tenant_id":"tenant-demo","tenant_key":"tenant-demo-key","task_id":"task-demo","status":"accepted","requested_task_revision":2,"accepted_at":"2026-09-11T08:00:00.000Z"}

HTTP 返回 202 后,直到收到 MQ applied 或查询到 applied,SaaS 才能显示“暂停已生效”。即使收到 applied,已有通话也可能继续。

8.2 控制生效事件

{
  "schema_version": "1.0",
  "event_id": "evt-pause-demo",
  "event_type": "command.result",
  "tenant_id": "tenant-demo",
  "tenant_key": "tenant-demo-key",
  "trace_id": "trace-demo",
  "occurred_at": "2026-09-11T08:00:01.000Z",
  "aggregate_type": "command",
  "aggregate_id": "cmd-pause-demo",
  "aggregate_version": 2,
  "payload": {
    "command_id": "cmd-pause-demo",
    "command_type": "task.control",
    "task_id": "task-demo",
    "status": "applied",
    "reason_code": "CONTROL_APPLIED",
    "requested_task_revision": 2,
    "applied_task_revision": 2,
    "task_state": "paused"
  }
}

8.3 录音交接顺序

呼出应用 → SaaS HTTP:申请 recording_id 绑定的上传授权
呼出应用 → OSS:按签名授权上传封口文件
呼出应用 → SaaS HTTPcompleteSaaS 实际校验,返回稳定 oss_id
呼出应用 → 本地数据库:verified 事实 + recording.ready outbox 同事务
呼出应用 → RabbitMQ → SaaSrecording.ready,去重落库后 ACK
SaaS → 已授权用户:按 oss_id 提供短期鉴权播放

8.4 拨号前等待及超时

以下是 GET /internal/v1/outbound/commands/cmd-execute-demo 的 execute 等待快照示例;同一事实的 command.result 使用第 7.1 节外壳,外壳 aggregate_version=2payload 不重复携带该版本字段。

{
  "command_id": "cmd-execute-demo",
  "command_type": "call.execute",
  "tenant_id": "tenant-demo",
  "tenant_key": "tenant-demo-key",
  "task_id": "task-demo",
  "execution_id": "exec-demo",
  "call_id": null,
  "status": "waiting",
  "reason_code": null,
  "wait_reason_code": "TENANT_CONCURRENCY_EXHAUSTED",
  "accepted_at": "2026-09-11T08:00:00.000Z",
  "waiting_since": "2026-09-11T08:00:00.000Z",
  "admission_deadline": "2026-09-11T08:00:30.000Z",
  "updated_at": "2026-09-11T08:00:01.000Z",
  "aggregate_version": 2
}

示例假设服务端窗口为 30 秒、not_after 为 08:01:00Z30 秒仅为演示,不是已冻结限值。08:00:30Z 仍无租户并发额度时,命令变为 rejectedreason_code=TENANT_CONCURRENCY_EXHAUSTEDwait_reason_code=null,版本递增并经 MQ 回传;不创建 call_id。若 not_after 更早,则以它作为 admission_deadline,到期报 COMMAND_EXPIRED。拒绝后原 ID 重投返回原拒绝,不重新排队。

8.5 暂停/停止与积压竞争

场景 契约预期
execute 尚在租户队列,随后暂停/停止 控制不排在 execute 后;消费旧命令时检查持久屏障并拒绝。applied 表示不再允许新发起,不表示全部积压已处理完或拒绝事件已投递完
execute 已 waiting,随后暂停/停止 未提交拨号意图的等待命令终结为 rejected,经 MQ 报 TASK_PAUSED/TASK_STOPPED;任务已恢复运行但命令仍为旧版本时用 STALE_REVISION。拒绝优先级为过期、停止、暂停、版本,再检查资源;不得被 resume 静默复活
Cell 已持有在途许可,控制受理 先禁止新许可,等待旧许可完成或失效并确认无法再发起;状态不明维持 applying/reconciling,不提前发 applied
pause 已 applied,随后 resume resume 只允许当前生效版本的新授权指令;旧版本仍拒绝,原 command_id/execution_id 重投仍命中原结果
stop + drain / hangup drain 不挂断活动电话;hangup 需额外权限并等通道结束确认,未确认不能称完全生效;停止任务不可 resume

8.6 执行命令与事件样例

以下四个样例从交付设计移入此处统一维护,字段按第 6、7 节校验;测试时间、号码、配置和文件校验占位符须替换,不得原样发至生产。执行命令保留 task_revision,表示当前已生效版本,不是控制请求的目标版本。

call.execute

{
  "schema_version": "1.0",
  "command_type": "call.execute",
  "command_id": "cmd_demo_001",
  "tenant_id": "tenant_test",
  "tenant_key": "tenant_test",
  "trace_id": "trace_demo_001",
  "issued_at": "2026-09-08T01:00:00Z",
  "not_after": "2026-09-08T01:05:00Z",
  "payload": {
    "execution_id": "exec_demo_001",
    "task_id": "task_demo",
    "task_item_id": "item_demo",
    "task_revision": 7,
    "callee": "${AUTHORIZED_TEST_NUMBER}",
    "route_policy_id": "route_policy_test",
    "caller_profile_id": "caller_profile_test",
    "agent_version_id": "agent_v1",
    "variables": {},
    "ring_timeout_ms": 30000,
    "max_call_duration_ms": 180000
  }
}

call.status

{
  "schema_version": "1.0",
  "event_id": "evt_demo_status_2",
  "event_type": "call.status",
  "tenant_id": "tenant_test",
  "tenant_key": "tenant_test",
  "trace_id": "trace_demo_001",
  "occurred_at": "2026-09-08T01:00:06Z",
  "aggregate_type": "call",
  "aggregate_id": "call_demo",
  "aggregate_version": 2,
  "payload": {
    "command_id": "cmd_demo_001",
    "execution_id": "exec_demo_001",
    "task_id": "task_demo",
    "task_item_id": "item_demo",
    "call_id": "call_demo",
    "attempt_id": "attempt_demo_1",
    "call_state": "answered",
    "call_version": 2,
    "cell_id": "cell-demo-a",
    "egress_pool_id": "egress-demo-a",
    "attempt_state": "answered",
    "trunk_id": "sip-primary",
    "answered_at": "2026-09-08T01:00:06Z"
  }
}

transcript.updated

{
  "schema_version": "1.0",
  "event_id": "evt_demo_text_1",
  "event_type": "transcript.updated",
  "tenant_id": "tenant_test",
  "tenant_key": "tenant_test",
  "trace_id": "trace_demo_001",
  "occurred_at": "2026-09-08T01:00:10Z",
  "aggregate_type": "transcript_segment",
  "aggregate_id": "segment_demo_1",
  "aggregate_version": 2,
  "payload": {
    "call_id": "call_demo",
    "turn_id": "turn_1",
    "segment_id": "segment_demo_1",
    "role": "customer",
    "revision": 2,
    "text": "这是授权测试。",
    "is_final": true,
    "start_ms": 500,
    "end_ms": 1800,
    "playback_state": "not_applicable"
  }
}

recording.ready

{
  "schema_version": "1.0",
  "event_id": "evt_demo_recording_1",
  "event_type": "recording.ready",
  "tenant_id": "tenant_test",
  "tenant_key": "tenant_test",
  "trace_id": "trace_demo_001",
  "occurred_at": "2026-09-08T01:03:10Z",
  "aggregate_type": "recording",
  "aggregate_id": "recording_demo_1",
  "aggregate_version": 1,
  "payload": {
    "call_id": "call_demo",
    "recording_id": "recording_demo_1",
    "oss_id": "oss_demo_1",
    "format": "wav",
    "channels": 1,
    "sample_rate_hz": 8000,
    "duration_ms": 15000,
    "size_bytes": 240044,
    "checksum_sha256": "${SHA256_OF_UPLOADED_FILE}",
    "created_at": "2026-09-08T01:03:01Z"
  }
}

录音校验值和尺寸仅为演示,实际算法与独立校验能力须按第 4.6 节冻结;AI 文字的播放标记只表示可验证的播放器事实,不证明客户实际听到。

9. 错误码与责任

HTTP / MQ code 建议 调用方处理
400 / 拒绝 INVALID_ARGUMENT 修正字段,不原样无限重试
401 / MQ 连接鉴权失败 UNAUTHENTICATED 受控刷新/修复服务凭证,告警
403 / 拒绝 FORBIDDENROUTE_NOT_AUTHORIZED 修复授权,不切其他租户或私自换线路
404 RESOURCE_NOT_FOUND 检查关联;含不可见资源,防枚举
409 IDEMPOTENCY_CONFLICTREVISION_CONFLICTCONTROL_IN_PROGRESS 查询原操作/当前版本后处理
409 TASK_STOPPEDUPLOAD_CONTENT_MISMATCH 停止非法操作或隔离资产,不自动覆盖
409,可重试 CALL_NOT_REGISTEREDUPLOAD_NOT_READY 有限等待并用原键重试,不绕过关联校验
410 REPLAY_EXPIRED 超出已保留事件范围,走人工恢复流程
413 PAYLOAD_TOO_LARGE 按合法分段/限制调整,不截断核心数据
429(仅 HTTP RATE_LIMITED 接口限流,遵守 Retry-After 并用原幂等键有限重试;不是 AMQP 背压应答
MQ 等待/准入拒绝 TENANT_CONCURRENCY_EXHAUSTEDTENANT_CPS_EXHAUSTED 当前租户跨 Cell 并发/CPS 不足;waiting 是暂时等待,rejected 才终结本次命令,不自动重拨
MQ 等待/准入拒绝 ROUTE_CAPACITY_EXHAUSTED 授权线路无可用并发/CPS 或兼容健康出口;线路未授权仍报 ROUTE_NOT_AUTHORIZED,不混为容量不足
MQ 等待/准入拒绝 CAPACITY_EXHAUSTED 系统 Cell/媒体或 AI 资源不足;已终结后由 SaaS 决定是否重新授权
MQ 等待 / 准入拒绝 SCHEDULER_WAIT / ADMISSION_TIMEOUT 前者仅表示等待调度;纯调度等待超过准入期限用后者终结,不承诺固定开始时限
503 DEPENDENCY_UNAVAILABLE 有限退避、告警及停止接新任务保护
MQ 业务拒绝 COMMAND_EXPIREDTASK_STOPPEDTASK_PAUSEDSTALE_REVISIONCONFIG_NOT_READY SaaS 决定是否重新授权调度
MQ 执行状态 EXECUTION_UNCERTAIN 进入对账,不盲目重新拨号

HTTP 错误码与 MQ reason_code 共用词汇但不是一一映射;MQ 没有 HTTP 状态码。完整枚举、SIP 原因映射和错误描述脱敏在冻结阶段补齐。

10. 安全、容量与可观测性

  • 服务权限细分为读、控制、强制挂断、补传、上传和完成确认;租户/任务/通话/资产逐层校验,不凭 URL 中的 ID 直接放行。
  • 上传完成确认与事件消费均校验同租户关联,防止将其他租户 oss_id 挂到本通话。播放授权由 SaaS 按最终用户权限执行。
  • 被叫、转写和录音按敏感业务数据处理,日志脱敏;密钥、签名地址、ARI 密码、SSH 私钥不进入代码/文档/事件。DLQ 和审计也受访问与保留控制。
  • 记录 command_id/execution_id/call_id/attempt_id/event_id 关联及控制、重放、挂断操作审计;真实身份从认证上下文取得,不仅依赖 reason 文本。
  • RabbitMQ 不放在实时音频循环上;最终文字与重要状态持久化,临时中间稿可在形成事件前合并,但不能丢最终稿。积压/磁盘水位达到上限时停止新接单。
  • 容量目标仍是至少 1000 路同时已接通的完整 AI 通话,不是 HTTP QPS。MQ/OSS 负载应按每秒文字事件、每通话状态事件和录音产生速率单独估算、压测。
  • 监控 HTTP 延迟/429/5xx、命令准入时延、控制屏障耗时、outbox 最老年龄、MQ redelivery/DLQ、SaaS 消费落库延迟、OSS 校验/上传失败、暂存磁盘和资源租约耗尽。
  • 按租户统计队列深度/字节、最老命令年龄、可调度等待时间、实际新增拨号份额、并发/CPS 占用、未 ACK/待发起窗口及背压次数;分别标注配额不足、无线路资源和纯调度等待。压测 broker 队列/副本数量和全局水位,不能只看全局平均延迟掩盖 B 饥饿。
  • broker confirm 与 SaaS 应用成功分开观测;应用确认与清理建议见第 10.1 节,不推断 unknown 为成功或失败。

10.1 投递责任、应用证据与清理(已确认方案)

  • 首期不增加 MQ receipt 或 HTTP 收讫接口。呼出侧自动负责 outbox→broker 的确认、不可路由及有限重试;SaaS 负责消费事务、inbox 去重、失败重试/DLQ 告警和应用对账。呼出侧不能自动识别具体哪些已确认发布事件仍未被应用,saas_applied 默认 unknown。
  • SaaS 根据自身 inbox/业务状态与命令/通话查询发现缺口,受控触发第 4.4 节 MQ 补传;查询只是诊断,不替代业务事件。published 不变成 appliedreplay completed 也不等于应用成功。unknown 本身不触发无限重复发布。
  • 事件和录音清理按 G0 确认的保留/交接策略执行:事件有可靠 broker 确认、仍可覆盖约定恢复窗口;录音需 OSS verified、ready outbox 持久化并确认发布、对象保留期覆盖恢复。普通清理还须无已知隔离/恢复任务并达到批准期限,不能仅按 HTTP 200 或“估计 SaaS 已应用”删除。未确认/失败数据到水位先背压告警;超期删除需明确授权及审计,不以 unknown 永久无限保存。
  • 端到端验收必须取得 SaaS inbox 和资产关联证据。若后续业务变更要求自动逐事件应用确认,再单独评审 MQ receipt 的幂等/重试/保留契约及工时;首期不暗加 HTTP 业务回调或收讫机制。

11. 契约落地与外部参数核验清单

方案已接受;下表不再阻塞 Mock 开发。D01按最终计划生成可验证规范,运行数值使用其第5节测试基线;涉及真实 SaaS/供应商/预算/保留与恢复的内容仍需外部落实,并在真实替换前验证。

核验项 已接受规则/待核验参数 责任方
接口归属及域名 两份 OpenAPI;SaaS 是否已有可复用资产服务、测试 Base URL 用户、SaaS
服务身份 现有服务令牌体系优先;issuer/audience/scope、租户映射、轮换和 mTLS 双方、运维
任务绑定与版本 初始版本 1、CAS 控制、停止不可恢复、可信任务归属来源 用户、SaaS
租户绑定与传输(已确认) 一对一/不原地变更/保留期不复用,224字节预算及超限停发;Mock注册可先开发,真实租户资料和传输能力接入时核验 SaaS、运维
授权撤销(已确认方案) 第 4.1.1 节:复用全部受影响任务屏障、SaaS 发布侧串行授权检查、枚举依据、在途收敛、生效时限及整任务暂停代价 用户、SaaS
无 call_id 补传(已确认) 第 4.4 节第七条 HTTP 路径、原命令范围、权限与固定截止点;不增加拨号入口 用户、双方
应用证据与清理(已确认方案) 第 10.1 节:首期只自动确认 broker,SaaS 对账补传;保留/交接依据,若必须自动应用确认则另选 MQ receipt 用户、双方、运维
执行幂等 新增 execution_id,与 command_id 分离;业务重新外呼许可及去重保留 用户、SaaS
停止语义 drain/hangup 显式选择;挂断权限与多 Cell 生效判据 用户、SaaS
线路/AI 配置 route/caller/agent 引用及同步来源;LLM/TTS 新规范、失败兜底 用户、供应方
MQ 环境 租户独立命令队列已确认;采用 agent-call 命名空间、direct 命令 exchange、agent-call.tenant.{tenant_key}.call.execute 路由及 tenant_key 原样透传;冻结精确绑定、生命周期、队列数上限、quorum/HA、ACL、重试/DLQ 与死信可靠性 运维、双方
租户公平与背压 公平调度架构已确认;冻结轮转批量/周期、活跃队列发现、权重、prefetch、接收窗口、并发/CPS、发布速率/积压上限、拒绝发布策略、多实例协调及等待指标 用户、双方、运维
保底与借用 默认不承诺固定开始时限;如需 SLA,确认保底资源、借用/归还边界及可满足的租户总承诺 用户、业务/运维
OSS ID 和校验 资产 ID 权威来源、校验算法、禁止覆盖/对象版本、文件大小和格式 用户、SaaS/存储
保留与恢复 幂等/屏障/事件/inbox/临时录音保留,最长中断和人工恢复范围 双方、业务/运维
限制与 SLO 使用最终计划第5节DEV/SCALE-MOCK初始基线;生产CPS、租户额度、保留与恢复等实际数值在真实替换前登记,准入截止不是开始 SLA 双方、运维
文字/拒绝再联系 最终稿首期不修订;分段与播放证据、明确opt_out来源及生效时延按最终计划验证,真实判定规则外部核验 SaaS、供应方
版本演进 HTTP 主版本 /v1、MQ schema_version 1.x;兼容矩阵和旧版退役窗口 双方

兼容建议:响应/事件允许增加可选字段;新增必填、字段语义变化或不兼容枚举按破坏性变更处理。对未知命令版本 fail closed;未知事件主版本隔离告警,不直接 ACK 丢弃。事件 payload 按 schema_version 校验,不能一边 strict 拒绝扩展一边声称任意可选字段都兼容。

12. 实施与验收顺序

  1. 契约落地:方案已接受,按最终计划D01生成字段/状态/权限规范及Mock profile;第11节真实资料并行协调,不重复等待已接受方案拍板。
  2. 机器可读规范:编写双方 OpenAPI 3.1、共用 MQ JSON Schema,复用本文拓扑表和集中样例;验证引用、权限及正反例,不要求首期交付 AsyncAPI。
  3. 契约测试/Mock:先验证 HTTP 控制与查询、MQ 去重和 OSS 存储握手;Mock 通过不表示真实 SIP/AI 可用。
  4. 真实单通话联调:授权测试号码,打通 execute → 状态/终态 → OSS → ready → SaaS 页面;分别记录 ASR、LLM/TTS、SIP、MQ、OSS 证据。
  5. 恢复和容量:重复、乱序、跨租户、重启、多 Cell 屏障、上传失败、断网与补传;新增大租户洪峰下小租户公平、背压及多调度实例配额验收,完成真实完整 AI 的容量/延迟测试后再发布生产结论。
验收项 必须得到的证据
唯一拨号入口 HTTP 规范无创建/重拨接口;补传不会调用 ARI originate
双层幂等 重复 command_id、换 command_id 但相同 execution_id、ACK 丢失/进程重启均不重复拨号
不确定发起 ARI 超时后先对账,未确认原通道结束不再发起
等待与准入截止 call_id 尚无时按 command_id 查询 waiting;等待事件/查询版本一致;窗口与 not_after 取较早值,重投/重启不延长;租户/线路/系统瓶颈及纯调度超时原因可区分;拒绝后不再拨号,已发起通话不因首次准入截止被挂断
控制屏障 仅提交 expected 版本,服务端 CAS 递增;重复命令不再递增,冲突不推进版本,执行仍用已生效 task_revision;多 Cell 竞争/失联/旧版本不能越过屏障,202 不等于 applied
租户隔离 跨租户查询、控制、录音授权/确认/关联和补传均失败;命令路由、队列绑定与正文 tenant_key 不一致时隔离,不能越权执行或回调错误租户
公平调度 A 大量积压后 B/C 新入队,在 B/C 有额度且资源可用时,无需等 A 排空即可获调度;测量到达→入轮转→发起的分段延迟和实际份额,达到 G0 冻结指标;有界预取、已 ACK 积压及重启恢复不破坏公平
多实例配额 多调度器/多 Cell 同时争抢、FALLBACK、所有权切换/失联重启均不重复分配租户额度;待对账活动通话不因租约过期被误释放,不超并发/CPS
背压与队列生命周期 A 队列满/发布洪峰时明确拒绝且 SaaS 持久保留,confirm 丢失原 ID 重试不双拨、不丢旧消息;B 在约定 broker 负载范围仍可发布调度;租户创建/停用/恢复不丢积压、不误删队列
非抢占边界 A 已占满可用资源时不为 B 强制挂断;按释放资源继续公平分配。若签订开始时限 SLA,另验证不可借用保底或明确借用约束,不用普通轮询冒充保证
乱序终态 迟到 ringing 不回退 endedcall.finished 不覆盖更新的 ready;旧文字不覆盖最终稿
OSS 完整性 缺对象、错误大小/摘要、授权过期、完成后覆盖、complete 超时重试均不产生无效或重复 ready
MQ 可靠性 不可路由、confirm 丢失、SaaS 事务失败、重试队列/DLQ 故障均可追踪恢复,ACK 不早于持久化
补传边界 单通话/命令整体补传,固定截止点、原事件、分批限速;无 call_id 的结果可恢复;非法筛选字段及跨租户资源拒绝,保留期过期报错;本次 replay 结果不入范围,不拨号,completed 不代表 SaaS 应用
撤销闭环 broker 积压、waiting、发起前、FALLBACK 的号码被禁用/授权撤销;覆盖关联多任务、发布竞争、CAS 冲突、失联及恢复;全部屏障 applied 后不再新发起,之前不得显示全局生效
租户绑定与传输 无 execute 的合法控制仍能原样回传 key;正文/绑定/身份错配拒绝;旧 key 不被别的租户复用;224/225 UTF-8 字节及特殊字符正反例;不通过多 key 绕过额度或幂等
应用证据 broker confirm 后暂停 SaaS 消费,saas_applied 仍 unknown;无无限重投;SaaS 恢复后凭 inbox 核对并按需补传;清理不伪称已入库,未确认失败数据不可静默删除
完整交付 SaaS inbox/业务落库、文字时间线和 oss_id 授权播放有证据;1000 路完整 AI 另有真实压测报告

本版完成定义: 已接受的文本契约可用于D01机器可读规范与Mock实现;不代表OpenAPI/Schema已生成、服务已实现或生产通过。最终开发/部署/监控/验收及数值范围以最终计划为准。