chore: initial project snapshot

This commit is contained in:
2026-09-11 17:47:03 +08:00
commit 23d1b1274f
45 changed files with 4468 additions and 0 deletions
@@ -0,0 +1,510 @@
# SaaS 交互:OpenAPI 与 MQ 契约规划
**版本:** v0.3(沿用原文件路径)
**状态:** 租户独立命令队列+公平调度方案已确认;详细接口、配额数值及调度参数仍待评审,非已发布接口规范。仅更新规划,不实现服务、不部署、不拨号。
**依据:** [一期呼出应用开发计划](一期呼出应用开发计划_v1.0.md)、项目根目录 `AGENTS.md`
**范围:** 现有 SaaS 与呼出应用的双向 HTTP 接口、RabbitMQ 命令/事件、OSS 录音交接及联调验收。本文细化已有六类 HTTP 接口,不建设通用开放平台。
> 阅读约定:第 1 节为已确认边界;其余路径、字段、状态、数值和安全方案均为建议草案,须由用户评审后冻结。出现 MUST/必须等约束,是拟发布契约应具备的要求,不表示现有运行代码已经支持。
**本次修订:** 补齐拨号前等待/查询、准入期限、资源原因码、发布背压和控制竞争样例;与一期计划统一 execution_id 及单线路启动。六类 HTTP 路径不变,不新增配额管理或租户队列管理接口;本版仍为待冻结草案,不升级 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. **按租户 ID 映射到独立 RabbitMQ 命令队列,由呼出应用调度器负责租户间公平调度**。不再采用所有租户共用一个执行 FIFO;同时限制租户发布/积压、预取窗口及跨 Cell 并发/CPS。该架构已确认,命名和限值仍为草案。
## 2. 交付拆分与双方职责
| 交付物/能力 | 提供方 | 消费方 |
| --- | --- | --- |
| 呼出应用 HTTP:控制、查询、补传 | 呼出应用 | SaaS 后端 |
| SaaS HTTP:录音上传授权、完成确认 | SaaS/存储服务 | 呼出应用资产处理器 |
| `call.execute` 命令 | SaaS 业务调度器按可信租户归属发布、处理背压 | 呼出应用公平调度器按租户队列有界接收并准入 |
| 执行及资产事件 | 呼出应用 outbox 投递器 | SaaS 事件消费者 |
| MQ VHost、账号、ACL、持久队列和告警 | 运维按冻结契约配置 | 双方服务 |
| 对外发布规范、样例、错误码和验收用例 | 用户主导制定 | 双方评审实施 |
建议冻结后交付两个 OpenAPI 3.1 文件,分别描述两个 HTTP 服务;MQ 使用 AsyncAPI 文档及 JSON Schema,不把 AMQP 消费者伪装成 HTTP Webhook。首版先有契约及样例,不引入 SDK 生成器或开发者门户。
## 3. 公共约定
### 3.1 标识与时间
| 字段 | 定义 |
| --- | --- |
| `tenant_id` | SaaS 租户 ID;请求中的值必须与服务身份获授权范围匹配,不能仅相信报文自报租户 |
| `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` | 链路关联信息,不用于授权或幂等 |
- ID 均为不透明字符串,不按手机号、数字或 UUID 强制改写已有 SaaS ID;建议长度 1–128,拒绝控制字符、路径分隔符和空白首尾。最终字符集在 Schema 冻结。
- 时间使用 RFC 3339 UTC,例如 `2026-09-11T08:00:00.000Z`;持续时间字段以 `_ms` 结尾,大小为字节,采样率为 Hz。
- 未接通时 `answered_at``null`,不填写虚假接通时间;未完成的结果用明确状态,不用空字符串冒充成功。
- 所有唯一键和检索均包含租户作用域。不同租户碰巧使用相同 ID,不能互相查询或命中幂等记录。
### 3.2 HTTP 共性
- 使用 HTTPS,仅服务到服务调用,不向浏览器或公网客户直接暴露内部接口。
- 公共请求头:`Authorization: Bearer <service-token>``X-Tenant-ID``X-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`,含 `type``title``status``code``detail``request_id``retryable`,不暴露堆栈和凭证。
### 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;受控历史事件补传 |
| 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 认为当前应有的控制版本 |
| `task_revision` | 是 | 本次目标版本,必须等于 expected + 1 |
| `active_call_policy` | stop 时是 | `drain` / `hangup`;不设隐藏的强制挂断默认值 |
| `reason` | 是 | 有界审计原因,不放敏感客户资料 |
响应字段:`command_id``task_id``status=accepted``requested_task_revision``accepted_at``Location` 指向命令查询接口。
建议状态与竞争规则:
- `task_revision` 仅表示任务控制栅栏版本,不表示智能体配置版本。首版建议初始版本为 1,任务初始运行;尚未收到 execute 时的控制,也必须能为获授权任务建立持久屏障。
- 基于服务身份及双方可信任务绑定验证归属;不能因第一次见到任意 task_id 就将其认领给请求租户。任务绑定来源在 G0 确认,可来自获授权 SaaS 主数据或其受信发布边界,不额外开放拨号入口。
- 同任务控制串行化,提交时 CAS 校验 expected;版本冲突或另一个控制尚在生效中返回 409。重复 command_id 先走幂等,不重复推进版本。
- 暂停/停止受理后,先禁止发放新的拨号许可,再等待所有相关 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.2 命令查询
通用返回:`command_id``command_type``task_id/call_id`(适用时)、`status``reason_code``accepted_at``updated_at`
- execute`accepted` / `waiting` / `rejected` / `executing` / `completed` / `reconciling`accepted 仅表示可靠受理,waiting 表示已进入有界准入等待,executing 表示已提交拨号意图、不等于 SIP 已实际发出或接通;completed 指执行生命周期结束,不等于客户接通或业务成功。
- control`accepted` / `applying` / `applied` / `rejected` / `failed` / `reconciling`,另含 requested/applied revision 和当前任务状态。
- replay`accepted` / `running` / `completed` / `failed`,另含快照截止点、已选/已确认发布数量;completed 不代表 SaaS 已入库。
- HTTP 校验阶段就被拒绝、未持久化的命令允许查询不到;MQ 收到的合法业务拒绝应持久化并可查询。
execute 查询及对应 `command.result` 建议共用以下字段,MQ 的等待/状态变化仍按 command 聚合版本合并:
| 字段 | 规则 |
| --- | --- |
| `execution_id` | 原业务授权执行 ID,受理后始终可关联 |
| `call_id` | 首次资源准入成功并提交拨号意图时创建;此前为 `null`,不伪造通话记录 |
| `wait_reason_code` | waiting 时必填:`SCHEDULER_WAIT``TENANT_CONCURRENCY_EXHAUSTED``TENANT_CPS_EXHAUSTED``ROUTE_CAPACITY_EXHAUSTED``CAPACITY_EXHAUSTED`;非 waiting 为 `null` |
| `waiting_since` | 首次进入 waiting 的服务端时间;原因变化不重置,离开等待后保留;未等待为 `null` |
| `admission_deadline` | 受理时固定的首次发起准入截止时间;拒绝于受理前时为 `null`,重投/重启不延长 |
| `updated_at``aggregate_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_id``execution_id`、任务关联、`call_state``call_version`、起止时间、结果原因及以下独立信息:
- `attempts`:每次 attempt 的状态、原因、`trunk_id``egress_pool_id``cell_id`、时间;不暴露 SIP 密码或内部管理地址。
- `transcript`:处理状态、最终稿数量及是否仍有待完成段落;首版查询不返回整段文字,文字正文通过 MQ 到 SaaS。
- `recordings`:录音标识、处理状态、确认后的 `oss_id` 和失败原因;不返回上传凭证或播放链接。
- `delivery`:待投递/失败数量、最后确认发布时间。RabbitMQ confirm 仅表示 broker 接收,`saas_applied` 未有应用层证据时必须为 unknown,不能猜测为已入库。
-`snapshot_at`;查询是某一时点的执行快照,SaaS 根据版本合并,不用旧快照覆盖较新事件。
### 4.4 历史事件补传
请求:`command_id``event_types`(允许列表)、可选 `event_ids``reason`。范围始终限定到路径中的 call_id 和当前租户;event_ids 提供时与 event_types 取交集,不得跨通话。
- 在受理时记录固定事件截止点与选择条件;重复请求使用同一个补传任务和截止点,不把后续新事件悄悄纳入。
- 所有被选事件必须来自原持久化事件记录;保留 event_id、原时间和原 payload,可仅在传输头标记 replay。禁止重新合成历史业务事实。
- 只重发结果/文字/录音元数据;不执行拨号、不重放 `call.execute`、不重传录音文件、不重新调用 LLM/TTS。
- 对补传限速并独立计量,避免挤占实时结果队列;部分成功允许重试未确认部分,重复投递由 SaaS 去重。
- 补传命令自身结果也走 MQ `command.result`;不把本次补传生成的结果循环纳入本次选择集。
### 4.5 录音上传授权
请求:`recording_id``call_id``content_type``size_bytes``checksum_algorithm``checksum``channels``sample_rate_hz``duration_ms`。上传前须封口文件,确保大小和校验值稳定。
返回:`upload_id``recording_id``expires_at``upload_method``upload_url``required_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_id``size_bytes``checksum_algorithm``checksum`,可带 OSS 返回的 `etag`(仅作辅助,不当作文件内容摘要)。
- SaaS 根据 upload_id 找到受控目标,核验调用方、对象实际存在、大小、实际内容校验及录音绑定;不能只相信客户端提交的 checksum 或对象自报元数据。
- 校验算法需选择 OSS/存储服务能够独立验证的机制并冻结;无法独立验证时不能声称校验成功。ETag 尤其在分片/加密情况下不等于文件 MD5。
- 对象未就绪返回可重试错误;内容冲突进入隔离/人工处置,不创建就绪资产。
- 验证成功,幂等返回 `upload_id``recording_id``status=verified``oss_id``verified_at`。确认超时后用原幂等键重试,不能新建第二份录音。
- 呼出侧把 verified 事实与 `recording.ready` outbox 同事务持久化,再由 MQ 投递。SaaS 只有消费 ready 后才将资产作为业务录音关联展示。
- 任一步失败通过 `recording.failed` 回传可恢复性与原因;未取得有效 oss_id 不发布 ready。本地文件清理受恢复期限和已可靠持久化交接证据约束,不能仅收到 HTTP 200 就无条件删除。
## 5. RabbitMQ 拓扑与传输
### 5.1 最小拓扑草案
| 对象 | 名称建议 | 用途 |
| --- | --- | --- |
| 环境 VHost | `/ai-call-<env>` | 测试/生产隔离,具体路径由运维配置 |
| 命令 Topic Exchange | `ai-call.commands.v1` | Routing Key 为 `tenant.{tenant_key}.call.execute`;消息类型仍为 `call.execute` |
| 租户独立命令队列 | `ai-call.executor.{tenant_key}.v1` | 一租户一队列,精确绑定该租户 Routing Key;公平调度器有界读取,共享持久去重存储 |
| 事件 Topic Exchange | `ai-call.events.v1` | 发布第 7 节允许的事件 |
| SaaS 事件队列 | `ai-call.saas.events.v1` | SaaS 消费落库 |
| 死信 Exchange | `ai-call.dead.v1` | 隔离无效消息与耗尽重试 |
| 死信队列 | `ai-call.commands.dead.v1``ai-call.events.dead.v1` | 分方向审计与受控恢复 |
- `tenant_key` 是平台将 tenant_id 唯一映射为安全单段路由标识的结果,禁止点号、通配符、碰撞和调用方任意指定;原始 tenant_id 仍保留在正文。发布与消费均校验队列绑定、租户归属和正文一致,发现错配隔离告警,不向错误租户执行。
- 本轮仅改变执行命令的租户隔离拓扑,事件队列不自动扩展为一租户一队列;事件消费和补传仍需限速及租户校验,不能由此宣称结果链路已有同等等待时延保证。
- 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.2 传输属性
`content_type=application/json`、UTF-8、`delivery_mode=2``message_id=command_id/event_id``type=command_type/event_type``correlation_id` 可使用 command_id,正文 trace_id 负责跨环节跟踪。
建议消息上限 256 KiB、HTTP JSON 请求体上限 64 KiB,均待容量/样例验证;超限拒绝或按已定义的文字分段规则发送,禁止静默截断最终文字。MQ 不传文件或服务凭证。
### 5.3 租户公平调度(架构已确认,参数待冻结)
```text
SaaS 持久任务/发布记录
→ 命令 Exchange:按可信 tenant_id 映射路由
→ 租户 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_version``command_type=call.execute``command_id``tenant_id``trace_id``issued_at``not_after``payload`
payload 必需:
| 字段 | 说明 |
| --- | --- |
| `execution_id``task_id``task_item_id` | 业务授权执行及任务归属 |
| `task_revision` | 必须等于当前生效且允许运行的控制版本 |
| `callee` | 原始业务被叫字符串,不提前拼供应商前缀 |
| `route_policy_id` | 已配置且获租户授权的线路策略引用,可只包含一个供应商 |
| `caller_profile_id` | 已授权主叫引用,不允许 arbitrary From/PAI |
| `agent_version_id` | 服务端已可解析的不可变智能体配置版本 |
| `variables` | 经字段白名单、类型/长度校验的运行变量,不是任意代码或任意 URL |
| `ring_timeout_ms``max_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_version``event_id``event_type``tenant_id``trace_id``occurred_at``aggregate_type``aggregate_id``aggregate_version``payload`
- 聚合标识按 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 | 必需业务数据 | 合并规则 |
| --- | --- | --- |
| `command.result` | command_id、command_type、status、reason_code;适用的任务/执行/通话关联及版本;execute 增加第 4.2 节等待/准入字段 | 以 command 聚合版本更新;区分 accepted、waiting、executing、applied、completed;不把等待当成已拨号 |
| `call.status` | call_id、execution_id、任务关联、call_state、call_version、attempt_id、attempt 状态、实际线路/Cell/出口、时间/原因 | 保持 call/attempt 各自状态,不让迟到 ringing 回退 answered/ended |
| `transcript.updated` | call_id、turn_id、segment_id、role、revision、text、is_final、start_ms、end_ms、playback_state | 同 segment 较高 revision 替换;最终稿不能被迟到中间稿覆盖 |
| `call.finished` | call_id、execution_id、任务关联、call_version、outcome、起止时间、时长、原因、attempt 汇总及资产处理快照 | 固定通话终态;后处理未完成时标为 pending,不阻塞终态 |
| `recording.ready` | call_id、recording_id、oss_id、格式、声道、采样率、时长、大小及校验 | 只接收已验证资产;不包含上传凭证或公开 URL |
| `recording.failed` | call_id、recording_id、stage、reason_code、retryable、next_retry_at(若有) | 标记资产故障,不改变通话终态;后续合法 ready 可完成恢复 |
| `transcript.failed` | call_id、原因、retryable、受影响 segment(适用时) | 明确文字不完整,不能把部分文本假装最终完整记录 |
| `contact.opt_out` | call_id、task_id、task_item_id、请求时间、关联 turn/segment(若有) | SaaS 及时持久化拒绝再联系并阻止后续业务调度;不等待挂断 |
后两类事件补齐文字失败及拒绝再联系的可追踪闭环;opt_out 的触发判定需由业务及新 AI 规范确认,不能默认靠未经验证的关键词误判。
文字 role 建议为 `customer` / `agent` / `system`playback_state 使用 `not_applicable` / `generated` / `sent` / `playback_confirmed` / `cancelled` / `unknown`。只能报告实际具备的播放证据,不能将“已生成/已发送”写成“客户已听见”。超长 turn 拆成稳定 segment;最终稿是否允许修订及保留要求由 G0 冻结。
### 7.3 通话与资产状态
- call_state`queued → dialing → ringing → answered → ended`,允许省略未发生的阶段。queued 仅表示已创建 call_id 并持久化意图但尚无实际发起证据;资源准入前的 waiting 属于命令状态,不提前创建通话。dialing/振铃/接通须有对应执行证据。失败可从任一未接通阶段进入 ended;FALLBACK 保持逻辑通话未结束并新增 attempt,不能回退成新业务通话。
- outcome`completed` / `busy` / `no_answer` / `rejected` / `failed` / `cancelled` 等标准值;completed 仅表示正常结束,不是营销成交。最终值及 SIP/Asterisk 映射待供应商确认。
- 对账状态单独为 `reconciling`,不要凭本地连接断开生成虚假通话终态。
- recording`pending → uploading → verifying → ready`,失败另记 retryable/原因;transcript`pending/streaming/finalized/failed`delivery`pending/broker_confirmed/failed`。三类后处理不改写 ended。
## 8. 最小交互样例
以下 ID 为示例,不是现网资源、真实号码或可调用配置。
### 8.1 暂停任务
```http
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,"task_revision":2,"reason":"operator_pause"}
```
```json
{"command_id":"cmd-pause-demo","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 控制生效事件
```json
{
"schema_version": "1.0",
"event_id": "evt-pause-demo",
"event_type": "command.result",
"tenant_id": "tenant-demo",
"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 录音交接顺序
```text
呼出应用 → 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 不重复携带该版本字段。
```json
{
"command_id": "cmd-execute-demo",
"command_type": "call.execute",
"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_EXHAUSTED`wait_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 |
## 9. 错误码与责任
| HTTP / MQ | code 建议 | 调用方处理 |
| --- | --- | --- |
| 400 / 拒绝 | `INVALID_ARGUMENT` | 修正字段,不原样无限重试 |
| 401 / MQ 连接鉴权失败 | `UNAUTHENTICATED` | 受控刷新/修复服务凭证,告警 |
| 403 / 拒绝 | `FORBIDDEN``ROUTE_NOT_AUTHORIZED` | 修复授权,不切其他租户或私自换线路 |
| 404 | `RESOURCE_NOT_FOUND` | 检查关联;含不可见资源,防枚举 |
| 409 | `IDEMPOTENCY_CONFLICT``REVISION_CONFLICT``CONTROL_IN_PROGRESS` | 查询原操作/当前版本后处理 |
| 409 | `TASK_STOPPED``UPLOAD_CONTENT_MISMATCH` | 停止非法操作或隔离资产,不自动覆盖 |
| 409,可重试 | `CALL_NOT_REGISTERED``UPLOAD_NOT_READY` | 有限等待并用原键重试,不绕过关联校验 |
| 410 | `REPLAY_EXPIRED` | 超出已保留事件范围,走人工恢复流程 |
| 413 | `PAYLOAD_TOO_LARGE` | 按合法分段/限制调整,不截断核心数据 |
| 429(仅 HTTP | `RATE_LIMITED` | 接口限流,遵守 Retry-After 并用原幂等键有限重试;不是 AMQP 背压应答 |
| MQ 等待/准入拒绝 | `TENANT_CONCURRENCY_EXHAUSTED``TENANT_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_EXPIRED``TASK_STOPPED``TASK_PAUSED``STALE_REVISION``CONFIG_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 应用成功分开观测;本版不新增应用收讫回调接口,端到端验收结合 SaaS inbox/入库证据。若需要运行时逐事件应用确认,另评审 MQ receipt 契约。
## 11. G0 冻结清单
| 决策 | 本文建议/待补信息 | 确认方 |
| --- | --- | --- |
| 接口归属及域名 | 两份 OpenAPI;SaaS 是否已有可复用资产服务、测试 Base URL | 用户、SaaS |
| 服务身份 | 现有服务令牌体系优先;issuer/audience/scope、租户映射、轮换和 mTLS | 双方、运维 |
| 任务绑定与版本 | 初始版本 1、CAS 控制、停止不可恢复、可信任务归属来源 | 用户、SaaS |
| 执行幂等 | 新增 execution_id,与 command_id 分离;业务重新外呼许可及去重保留 | 用户、SaaS |
| 停止语义 | drain/hangup 显式选择;挂断权限与多 Cell 生效判据 | 用户、SaaS |
| 线路/AI 配置 | route/caller/agent 引用及同步来源;LLM/TTS 新规范、失败兜底 | 用户、供应方 |
| MQ 环境 | 租户独立命令队列已确认;冻结 tenant_key 映射、精确绑定、生命周期、队列数上限、quorum/HA、ACL、重试/DLQ 与死信可靠性 | 运维、双方 |
| 租户公平与背压 | 公平调度架构已确认;冻结轮转批量/周期、活跃队列发现、权重、prefetch、接收窗口、并发/CPS、发布速率/积压上限、拒绝发布策略、多实例协调及等待指标 | 用户、双方、运维 |
| 保底与借用 | 默认不承诺固定开始时限;如需 SLA,确认保底资源、借用/归还边界及可满足的租户总承诺 | 用户、业务/运维 |
| OSS ID 和校验 | 资产 ID 权威来源、校验算法、禁止覆盖/对象版本、文件大小和格式 | 用户、SaaS/存储 |
| 保留与恢复 | 幂等/屏障/事件/inbox/临时录音保留,最长中断和人工恢复范围 | 双方、业务/运维 |
| 限制与 SLO | 256 KiB MQ、64 KiB HTTP 为候选;首次准入窗口、到期处理延迟、not_after 边界、等待原因优先级、超时/重试/退避、CPS、事件时延和上传授权有效期需填实值;准入截止不是开始 SLA | 双方、运维 |
| 文字/拒绝再联系 | 最终稿修订、分段、播放证据、opt_out 触发和生效时限 | 用户、SaaS |
| 版本演进 | HTTP 主版本 `/v1`、MQ schema_version `1.x`;兼容矩阵和旧版退役窗口 | 双方 |
兼容建议:响应/事件允许增加可选字段;新增必填、字段语义变化或不兼容枚举按破坏性变更处理。对未知命令版本 fail closed;未知事件主版本隔离告警,不直接 ACK 丢弃。事件 payload 按 schema_version 校验,不能一边 strict 拒绝扩展一边声称任意可选字段都兼容。
## 12. 实施与验收顺序
1. **契约评审**:用户逐项确认第 11 节,发布冻结字段/状态/限值、双方负责人和变更记录。
2. **机器可读规范**:编写双方 OpenAPI 3.1、MQ AsyncAPI/JSON Schema、完整请求/响应及正反例;验证引用、样例、权限和错误响应。
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 取较早值,重投/重启不延长;租户/线路/系统瓶颈及纯调度超时原因可区分;拒绝后不再拨号,已发起通话不因首次准入截止被挂断 |
| 控制屏障 | 多 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 不早于持久化 |
| 补传边界 | 固定截止点、原 event_id/内容、无拨号、保留期过期明确报错;不得以 HTTP 结果替代 MQ |
| 完整交付 | SaaS inbox/业务落库、文字时间线和 oss_id 授权播放有证据;1000 路完整 AI 另有真实压测报告 |
**本版完成定义:** 完整规划草案可供评审;不意味着详细规范已冻结、服务已实现、云资源已部署或生产验收已通过。
+278
View File
@@ -0,0 +1,278 @@
# 一期呼出应用开发计划
**版本:** v1.3(沿用原文件路径)
**文档状态:** 3.1 交互方案已确认:所有业务结果经 MQ 回传,录音先上传 OSS 后回传 OSS ID;接口与消息规范由本人制定。生产网络架构已确认采用多机器+多 EIP 直连,不纳入单 EIP+NAT 方案;新增确认租户独立命令队列+公平调度。具体配额、等待指标、环境和新增工作量经 G0 评审后执行,运行代码待实现。
**适用范围:** 本人负责的一期外呼应用,以及与现有 SaaS、RabbitMQ、指定 SIP 系统的对接。
**编制依据:** 用户最新确认的工作范围。既有 Excel 仅作背景参考,本文件独立交付,不修改 Excel,也不沿用其三期工时汇总。
**本次修订:** 对齐交互规划 v0.3 的 execution_id、拨号前等待/查询、准入截止与 MQ 背压语义;明确单线路可启动、无备用不虚构。六类 HTTP 路径不变,详细字段、状态和数值仍待 G0 冻结,本次不改运行代码。
## 1. 目标与范围
### 1.1 本人负责的交付
1. 外呼任务与基础调度的**数据契约、指令接收、受理/执行应答及状态对账**。
2. **Asterisk 呼叫控制、指定 SIP 系统接入和 FALLBACK**
3. **实时语音 AI 链路的呼出应用**:衔接媒体、ASR、LLM、TTS,处理通话生命周期和打断。
4. 将**通话状态、文字及录音数据回传 SaaS**,供 SaaS 存储和展示;负责投递失败重试、补传与可追踪性。
“本人负责”指上述模块的设计、开发、联调和交付,不再把核心实现默认分配给 BgB。本人是否同时为原团队 ArchA,在 G0 登记;同一人不得被当成两个并行开发资源。
### 1.2 系统边界
| 系统 | 本期职责 | 不承担的职责 |
| --- | --- | --- |
| 现有 SaaS | 租户/权限、客户与任务管理、号码选择、基础业务调度、业务重试决策;按可信租户归属分队列发布,背压时持久保留待发布任务;接收回传数据、长期存储、页面展示 | 不直接操纵 Asterisk 通道,不与呼出应用各自重复发起同一通话 |
| 本人开发的呼出应用 | 按租户队列公平接收与调度执行指令;跨 Cell 租户配额及资源准入;维护执行事实;控制 Asterisk/AI;主备尝试;应答、回传和补偿 | 不复制 SaaS 的客户库、任务编排系统、账本和管理后台 |
| RabbitMQ | 沿用 SaaS 的消息基础设施,按租户隔离执行命令队列,承载所有业务结果事件及其重试/死信 | 队列隔离不等于调度公平或资源独占;不把消息 ACK 当成接通或业务成功;录音先上传 OSS,MQ 仅回传 OSS ID 和元数据,不运输录音二进制 |
| 指定 SIP 供应商线路 | 提供各 trunk 的接入参数、呼叫能力、原因码、并发/CPS 和线路规则 | 不假定线路天然跨故障域;供应商白名单与线路能力由线路方确认 |
呼出应用可以保存必要的执行、去重、控制屏障和投递记录,但不建立第二套 SaaS 主数据体系。
### 1.3 明确不做
- 计费、余额、支付、套餐、发票;保留基础用量指标,不实施扣费。
- 通用开放平台、第三方开发者门户、多种回调通道同时建设。
- RAG、流程编排、人工坐席及未经审核的开放式多线路编排;已审核的供应商 trunk 按约定策略动态选路属于本期外呼能力。
- 自研 ASR/LLM/TTS 模型、自研完整媒体引擎、集群自动扩容。
- SaaS 全部页面和业务模块重建。SaaS 配合改造由其对应负责人交付。
### 1.4 生产网络与容量基线(已确认)
- 生产唯一网络方案为**多机器+多 EIP 直连**。每个语音 Cell 包含 Asterisk、媒体适配和本地执行器,并绑定固定出口 IP;调度器按 `trunk_id + egress_pool_id + cell_id` 分配任务。
- 一通电话从建立到结束固定使用同一 Cell 和出口;供应商 trunk 预先配置,逐呼只选择已授权线路并应用其号码规则,不逐呼重写共享 SIP 配置、重载 Asterisk 或重建注册。
- 每个 Cell/出口独立维护供应商白名单、RTP 端口范围、并发上限、CPS、编解码和健康状态。供应商限制优先于本地理论容量。
- **单 EIP+NAT 不作为备选方案**,本计划不设计、不实现、不验收该架构;不得以通用 SNAT 代替多 EIP 直连。
- 容量口径为至少 **1000 路同时已接通的完整 ASR/LLM/TTS 通话**。拨号、振铃、CPS、AI 配额、RTP/UDP 端口、带宽、文件描述符、MQ、OSS 和故障冗余必须单独计入。
- 采用 N+1 或更高冗余。按真实压测得到的单 Cell 安全容量计算:`(Cell 数量 - 1) × 单 Cell 安全容量 >= 1000`,并额外预留发布、线路故障和突发拨号余量。该冗余保证故障后的承接能力,不保证故障 Cell 中的活动通话无损迁移。
- 当前 RTP `1000010800` 仅是配置基线,不是 1000 路容量保证;G4 前必须完成真实 SIP/RTP、ASR/LLM/TTS 和供应商配额压测,未压测不得承诺机器数量或规格。
### 1.5 多租户队列与公平调度(已确认,运行代码待实现)
- 采用**按租户 ID 映射到独立 RabbitMQ 命令队列+呼出应用调度器公平调度**。A 的大量积压不应使 B 必须等待 A 排空后才能进入执行调度;不再采用所有租户共用一个执行 FIFO。
- SaaS 依据可信租户归属发布;平台负责队列创建、精确路由绑定和停用/清理,不接受调用方任意指定其他租户队列。任务创建、业务重试仍由 SaaS 决定;执行公平与资源许可由呼出应用负责。
- 活跃且可调度租户间默认建议等权轮询;有明确差异化需求后按平台权重调度。每轮有限取数、按租户及全局限制预取和已持久化待发起窗口;额度耗尽或线路不可用时跳过该租户,不阻塞其他租户。不能消费到无界内存 FIFO 后再宣称公平。
- 租户并发/CPS 额度跨所有 Cell 和调度实例汇总;并发包括发起预留、拨号、振铃、接通和待对账占用,CPS 包括 FALLBACK。拨号前同时满足租户、供应商、Cell/出口和 AI 配额;多实例采用原子额度及调度所有权/租约协调,失效实例不能重复分配。
- 每租户限制发布速率、队列消息数/字节及待执行窗口,辅以全局 broker 水位保护。满队列明确拒绝发布,不丢弃队头旧命令;SaaS 保留发布记录,confirm 不确定或失败时使用原执行标识有限重试。重试/死信恢复回到原租户调度域,不能绕过配额。
- 有资源时保障公平分配新许可,不为公平强制挂断已接通电话;资源已满时需等释放。若要求固定开始时限,必须另外确认线路/AI/Cell 全链路保底或受限借用策略,不把轮询等同于 SLA 保证。
- 队列数量、轮转参数、配额与时延数值在 G0 冻结;详情见 [SaaS 交互规划第 5 节](SaaS交互_OpenAPI与MQ契约规划_v0.1.md)。本轮不改变事件回传队列为租户独立队列,不增加 HTTP 拨号入口。
## 2. 分工与双方交付物
| 负责人 | 交付内容 | 对接对象 |
| --- | --- | --- |
| 本人:呼出应用负责人 | 制定并发布 OpenAPI、MQ 拓扑、消息/事件字段、鉴权、错误码、幂等/重试及 OSS ID 回传规范;交付执行服务、ARI/SIP/FALLBACK、AI 呼出应用、MQ 结果回传和专项测试 | SaaS 后端、部署负责人、SIP/AI 供应方 |
| BgA:建议负责 SaaS 业务接口 | 任务/号码/配置快照,租户授权校验,控制接口调用,执行状态映射及查询接入 | 本人、FeA |
| BgB:建议负责 SaaS 消息与资产接入 | RabbitMQ 按租户路由发布、发布限流/背压持久重试,回传接收与去重,文字/录音元数据落库,上传授权与补传联调 | 本人、BgA、存储负责人 |
| FeA:SaaS 展示 | 任务/通话状态、文字时间线、录音鉴权播放、失败/处理中状态、联调验收页面 | BgA、BgB |
| 原团队部署负责人/ArchA | 环境、网络、证书、密钥、租户队列生命周期/绑定/ACL/容量策略、监控、发布和回滚支持 | 本人及各服务负责人 |
BgA/BgB 为建议配合分工,G0 由 SaaS 团队认领。不把部署负责人视为新增第五名成员;若与本人重合,需合并其工作日历。
## 3. 最小交互方案与执行顺序
### 3.1 已确认的最小方案
**已确认:RabbitMQ 下发呼叫执行指令,所有业务结果必须经 MQ 回传;录音先上传 OSS,上传成功并取得 OSS ID 后,通过 MQ 回传 OSS ID 及必要元数据。接口与消息规范由本人制定。**
- 不增加第二条能够独立拨号的 HTTP 入口。OpenAPI 仅承担任务控制、查询、OSS 上传授权及完成确认;同步接口应答不替代业务结果事件,不保留 HTTP 业务回调链路。
- 受理/拒绝、执行状态、文字、最终结果和录音就绪等事件统一走 MQ。录音文件不进入 MQ,上传失败不得发送录音就绪事件,失败状态仍经 MQ 回传。SaaS 根据 OSS ID 关联录音并提供租户授权播放。
- 由本人制定并发布 Exchange/Queue、Routing Key、消息/事件字段、API 路径、鉴权、错误码、版本、幂等/重试以及 OSS ID 命名和取值规范;SaaS 与运维按该规范对接、验证和落地。本文具体接口/字段仍为草案,本次确认不代表详细规范已经发布。
### 3.2 一次外呼的交互步骤
1. SaaS 创建任务及号码明细,固定智能体配置版本;执行租户授权、允许时段、基础频控及拒绝再联系检查。
2. SaaS 业务调度器为一次授权外呼生成 `execution_id`,为执行命令生成 `command_id`,按可信租户 ID 映射路由向该租户独立队列发布 `call.execute`,携带任务项、有效期及执行所需快照;受发布限流/队列背压时持久保留,两个 ID 均保持不变重试。
3. 呼出应用在活跃租户间公平轮转、有界取数,校验队列绑定与正文租户一致、授权归属、有效期和任务控制版本;持久化命令及去重结果后 ACK。此时仅表示“已接收”,不是“已拨号”。
4. 呼出应用通过 MQ 回传受理/拒绝应答。尚未取出的命令受租户队列容量和有效期约束;已受理但未准入时以 waiting 状态、等待原因和截止时间表达,通过 command_id 查询。准入截止取 not_after 与 accepted_at+服务端窗口的较早值,重投/重启不延长;超限区分过期、租户/线路/系统瓶颈或纯调度超时,经 MQ 终结,由 SaaS 对账后决定是否重新授权,不无限堆积。
5. 呼出应用公平分配执行机会,原子取得跨 Cell 租户并发/CPS 额度及供应商/Cell/出口/AI 完整资源许可;首次准入成功并提交拨号意图时分配 `call_id``attempt_id`,此前 call_id 为 null。拨号前再次检查暂停/停止屏障与有效期;提交意图不等于已发出 SIP 或接通,状态不明先对账。
6. 通过 Asterisk 向指定主 SIP 接入发起呼叫,通过 MQ 回传拨号、振铃、接通或失败状态。
7. 主尝试明确未接通且符合允许的线路故障条件时,确认旧通道结束后才选择已配置、获授权且兼容同一 Cell/出口的备用线路;无适合备用则结束失败,不虚构备用。状态不明时先对账,不盲目重拨。
8. 接通后建立媒体会话,衔接 ASR→LLM→TTS,处理客户插话、静音、超时及主动结束。
9. 文字按中间稿/最终稿通过 MQ 回传 SaaS。回传或 SaaS 页面故障不得阻塞实时音频链路。
10. 挂断后关闭媒体会话并清理通道,写入独立的通话终态并通过 MQ 回传;文字最终稿、录音上传等后处理继续异步执行。
11. 获取 OSS 上传授权,将录音上传 OSS,完成校验并取得 OSS ID 后,通过 MQ 发布含 OSS ID 的录音就绪事件;SaaS 消费后关联录音并展示。上传失败通过 MQ 回传失败状态,不发送无效的录音就绪事件。
12. 对未确认发布或未被 SaaS 应用的事件进行有限重试、MQ 补传和对账。SaaS 可查询通话及资产处理状态,查询不代替所有业务结果必须经 MQ 回传的要求。
## 4. G0:开发前确认清单
下列内容先在测试环境确认,不要求全部生产资源提前到位;未确认的关键外部依赖不能计为已经解决。
| 确认项 | 需要得到的结论 | 提供方 |
| --- | --- | --- |
| 工作边界 | 本人实际可投入时间;现有呼出应用、媒体组件和 SDK 可复用程度;SaaS 调度由谁实现 | 本人、SaaS 团队 |
| OpenAPI | 本人制定控制、查询、OSS 上传授权/完成确认的路径、字段、鉴权、错误码、超时、幂等和版本规则;业务结果走 MQ 已确认,不再作为待选项 | 本人制定;SaaS 后端对接 |
| RabbitMQ | 租户独立命令队列已确认;制定 tenant_id 路由映射、队列/绑定生命周期、ACL、消息/队列容量、拒绝发布背压、Confirm/ACK、按租户重试/DLQ 恢复;验证队列数量与 broker 全局资源上限 | 本人制定;SaaS/运维落地 |
| 公平调度 | 冻结轮转/权重、取数批量、活跃租户发现、未 ACK/待发起窗口、跨 Cell 租户并发/CPS、发布速率、多实例额度/所有权协调;明确等待指标及是否需要保底/借用 | 本人、SaaS、部署方 |
| 任务控制 | 暂停/恢复/停止语义;停止是否挂断已接通电话;业务重试与主备切换的边界 | SaaS、本人 |
| 指定 SIP 供应商线路 | 各 trunk 的测试账户、接入、注册或 IP 鉴权、主叫限制、拨号格式、并发/CPS、失败码、出口白名单和线路选择政策 | SIP/线路方 |
| Asterisk 与媒体 | Asterisk 版本、ARI 能力、多 Cell/EIP 网络、SIP/RTP 防火墙、编解码及采样率;测试电话双向可听 | 部署方、本人 |
| AI 接入 | 可用流式 ASR/LLM/TTS 接口、凭证、音频格式、取消能力、并发配额和超时限制 | AI 供应方、本人 |
| 文字与录音 | 中间稿展示要求;单/双声道;OSS 上传协议、校验、OSS ID 字段及取值、长期保留与播放授权;“先上传 OSS、再经 MQ 回传 OSS ID”已确认 | 本人制定回传规范;SaaS、存储/业务负责人配合 |
| 容量与验收 | 目标并发、CPS、最长通话、响应/打断延迟、回传时延、最长可恢复中断、临时磁盘上限 | 双方、部署方 |
**G0 输出:** 本人制定并发布的接口/消息契约 v1(含 OSS ID 回传规范)、对接验证记录、测试账号与样例、参数基线、风险清单及确认后的排期。不以“CRUD 开发速度”替代实时语音接入验证。
## 5. 接口、消息和应答契约
### 5.1 OpenAPI 清单草案
| 方向 | 接口草案 | 职责与应答 |
| --- | --- | --- |
| SaaS→呼出应用 | `POST /internal/v1/outbound/tasks/{task_id}/controls` | 提交暂停/恢复/停止;含 `command_id``task_revision`、动作。返回受理结果,执行生效另有确认 |
| SaaS→呼出应用 | `GET /internal/v1/outbound/commands/{command_id}` | 查询受理、等待/原因/准入截止、执行中、已生效、拒绝或失败;call_id 尚无时也可对账;重复请求不改变状态 |
| SaaS→呼出应用 | `GET /internal/v1/outbound/calls/{call_id}` | 查询通话终态、尝试记录、文字/录音处理状态和回传进度 |
| 呼出应用→SaaS | `POST /internal/v1/outbound/recording-uploads` | 获取绑定租户/通话、格式/大小约束和有效期的 OSS 上传授权;按本人制定的存储协议返回上传地址/会话 |
| 呼出应用→SaaS | `POST /internal/v1/outbound/recording-uploads/{upload_id}/complete` | 确认 OSS 上传完成及校验结果,返回或确认 OSS ID;随后通过 MQ 发布录音就绪事件,此接口不承担业务结果回调、不传 Base64 |
| SaaS→呼出应用 | `POST /internal/v1/outbound/calls/{call_id}/replays` | 授权触发结果/文字/录音元数据经 MQ 补传;只重传数据,绝不重新拨号 |
不提供 HTTP 业务事件接收接口;事件按类型定义 MQ Schema。接口路径及 OSS ID 字段的最终规范由本人制定并发布,SaaS 按规范对接。现有上传/资产能力可复用,但不改变“先上传 OSS,再通过 MQ 回传 OSS ID”的顺序。
### 5.2 RabbitMQ 执行指令
按租户 ID 映射独立队列,命令类型仍为 `call.execute`。具体 Exchange、带租户路由键及队列命名草案见 [SaaS 交互规划](SaaS交互_OpenAPI与MQ契约规划_v0.1.md);路由与正文 tenant_id 必须一致,重投不能改投其他租户以绕过限流。
`call.execute` 最小字段:
- 通用:`schema_version``command_id``tenant_id``trace_id``issued_at``not_after`
- 业务:`execution_id``task_id``task_item_id``task_revision`、原始被叫 `callee`、已授权的 `caller_profile_id` / `route_policy_id`;不预拼供应商前缀,不让调用方任意指定 SIP 地址、凭据或 Cell/出口。
- 配置:`agent_version_id`、变量及不可变配置快照,或双方确认的快照读取引用。
- 约束:最大通话时长、振铃超时、允许的 FALLBACK 策略引用。
执行指令不要求尚未生成的 `call_id/attempt_id`。同一命令的网络重试保持 command_id/execution_id 不变;同时以 `(tenant_id, command_id)``(tenant_id, execution_id)` 去重,换 command_id 也不能让同一次业务执行再次拨号。内部合法 FALLBACK 仅新建 attempt_id,保持 execution_id/call_id;业务重新外呼须先确认原执行事实并取得新授权,再生成新的 execution_id/command_id,受 SaaS 重试政策限制。不得把凭证直接塞进可广泛访问的消息或日志。
准入字段、状态/原因、时限和查询样例以 [SaaS 交互规划第 4.2、6.2、8.4 节](SaaS交互_OpenAPI与MQ契约规划_v0.1.md) 为详细草案:未消费持久化的命令允许查不到,404 不能当成未发布/未拨号证据;等待查询及 MQ 快照使用同一命令版本域。首次准入截止只限制首次发起;not_after 限制每次新尝试(含 FALLBACK),均不作为已发起通话的强制挂断时间。
### 5.3 MQ 回传事件
所有业务结果事件统一经 MQ 回传,规范由本人制定。录音回传使用 OSS ID,字段示例为 `oss_id`,最终命名及取值由本人规范确定;不默认将其等同于文件名、Object Key、ETag 或播放 URL。
通用外壳:`event_id``event_type``schema_version``tenant_id``trace_id``occurred_at``payload`;按事件类型要求 `command_id/task_id/task_item_id/call_id/attempt_id`,不统一强制全填。
| 事件类型 | 核心数据 | SaaS 的处理 |
| --- | --- | --- |
| `command.result` | 原命令、execution_id、受理/等待/执行/生效/拒绝/失败、原因、控制版本;execute 的 wait_reason_code、waiting_since、admission_deadline 和可空 call_id | 分清受理、资源等待、提交拨号意图和实际通话状态;按命令聚合版本更新 |
| `call.status` | 通话与尝试、状态版本、实际 trunk_id/egress_pool_id/cell_id、时间、标准原因和原始 SIP/Asterisk 原因 | 幂等更新,不被迟到事件回退终态;实际选路也进入通话查询 |
| `transcript.updated` | `turn_id`、角色、文本版本/序号、文本、中间/最终标记、起止时间、播放/取消标记 | 更新同一轮文字,不把中间稿累加成重复句子 |
| `recording.ready` | `call_id``oss_id`(OSS ID)、格式、声道、采样率、时长、大小、校验;`recording_id`可作业务关联标识 | 仅在 OSS 上传成功并确认 OSS ID 后发布;SaaS 根据 OSS ID 关联录音并提供租户授权播放 |
| `call.finished` | 通话终态、开始/接通/结束时间、时长、原因、FALLBACK 使用情况、各资产处理状态 | 电话结束即可落业务终态;后处理允许随后补齐 |
时间戳采用带时区的统一格式,时长单位由本人在 Schema 中明确。文本序号和状态版本分别定义作用域,不依赖 MQ 全局顺序;重试和补传保留原 `event_id`,避免重复应用。
### 5.4 可靠性与控制语义
- 命令 MQ ACK:仅在执行指令及幂等信息已持久化后确认。网络重投使用同一执行标识。
- 发布背压:confirm ACK 且无 mandatory return 只证明 broker 接收并路由;NACK、不可路由、confirm 不确定及 broker blocked 按交互规划第 5.4 节持久保留、原 ID 有限退避/修复。满队列明确拒绝、不丢旧消息,不能将 AMQP 背压映射成虚构的 HTTP 429 或已受理业务拒绝。
- 结果 MQ 发布:启用 Publisher Confirm 和不可路由检查;确认只代表消息进入有效队列,不代表 SaaS 已完成业务入库。
- SaaS 消费 ACK:按 `event_id` 去重,业务数据与接收记录持久化成功后 ACK;消费失败可重试/补传,不能静默丢失。
- 事件产生:执行事实与待投递记录同事务提交;独立投递任务负责重试。SaaS 接收侧同样去重,不依赖“恰好一次”假设。
- 拨号副作用:调用 ARI 前持久化发起意图及固定通道关联标识;超时/进程重启后先核实 Asterisk 的实际通道,不因本地缺少成功记录就再次拨号。无法核实的执行进入待对账。
- 重试分类:MQ 网络故障、发布未确认、不可路由或消费失败按本人制定的退避/DLQ规则处理;非法 Schema 隔离,鉴权异常告警。HTTP 的429/可恢复5xx重试仅用于控制、查询和 OSS 上传相关接口,不作为 HTTP 业务回调的备用链路。
- 过期/停止任务:死信重放仍检查有效期、控制版本和业务许可;数据补传不得触发重新外呼。
- 暂停:SaaS 停止发新指令;呼出应用建立覆盖 broker 积压、已持久化 waiting 和多 Cell 在途许可的控制屏障。未发起的等待执行按当前屏障/版本拒绝并经 MQ 回传;尚在队列的旧命令消费时拒绝,resume 不静默复活。待在途发起许可处理完或确认失效后才回传“已生效”;状态不明维持 applying/reconcilingapplied 不代表积压全部清理或已有电话全部结束。竞争样例见交互规划第 8.5 节。
- 停止:旧版本和迟到指令不能重新开启任务;是否终止已接通电话按 G0 确认的策略执行,强制挂断必须鉴权和审计。
- 终态:呼叫、文字/录音后处理、回传投递分别记录状态。录音失败不改写电话已经结束的事实。
## 6. 开发任务步骤与工时
下表保留**原本人负责范围的基准工时**,包含设计、编码、自测和正常联调,不包含 SaaS 团队实现工时。已将租户队列、公平调度和专项测试并入对应工作包,但其增量工时尚未重估;原合计不能视为已覆盖新增范围的交付承诺,G0 完成增量评估后更新总工时和排期。每项先有可验证产出,再进入下一依赖阶段。
| WBS | 任务包 | 开发步骤及交付物 | 本人工时 |
| --- | --- | --- | --- |
| D01 | 对接契约与技术验证 | 盘点已有组件→确认双方职责→冻结 OpenAPI/消息/状态→完成样例与 Mock→验证一通测试电话和 AI 流式能力 | 24–32h |
| D02 | 环境与应用骨架 | 打通 SaaS/MQ/ARI/AI/存储及多 Cell/EIP 网络→接入服务鉴权与密钥→建立最小执行/投递存储→健康检查与日志 | 16–24h |
| D03 | 指令、公平调度、应答与控制 | 租户队列有界接收/轮转→路由/租户/时效校验→持久去重→跨 Cell 租户额度与多调度实例协调→Cell/trunk/EIP/AI 资源准入→受理应答→暂停/停止屏障→重启恢复测试 | 原 32–48h,增量待评估 |
| D04 | Asterisk 主线路呼叫 | SIP参数与号码格式→ARI发起→通道/桥/媒体关联→振铃/接通/挂断→时间和原因归一→资源清理 | 32–48h |
| D05 | FALLBACK 与状态对账 | 确认可切换失败码→旧通道结束确认→备用尝试→迟到接通/ARI断线对账→次数/期限限制→防双拨演练 | 32–48h |
| D06 | 实时 AI 呼出应用 | 对接现有媒体能力→编解码/采样率适配→ASR/LLM/TTS流式联动→打断与取消旧生成→静音/超时/退出→异步隔离 | 48–72h |
| D07 | 文字生成与回传 | 定义轮次/版本→中间稿与最终稿→角色及播放/取消标记→持久事件→断网补传→SaaS时间线核对 | 16–24h |
| D08 | 录音上传与回传 | 录音生成→封装/元数据→OSS上传授权→上传/校验→确认OSS ID→经MQ回传OSS ID及元数据→重试/临时文件清理 | 24–32h |
| D09 | 查询、投递与补偿 | 命令/通话查询→受理与终态一致性→回传重试/隔离→授权重放→资产处理独立状态→恢复对账 | 32–40h |
| D10 | 可观测与运行保障 | 按租户等待/配额/积压指标→租户队列生命周期及 broker 上限→拒绝发布/背压→临时存储保护→密钥脱敏→部署、重启和优雅停机 | 原 24–32h,增量待评估 |
| D11 | 端到端专项验收 | SaaS真实联调→多 Cell/EIP 与供应商线路故障注入→重复/乱序/过期→多租户安全/公平/背压→多调度实例配额恢复→1000 路完整 AI 容量/延迟→回传与录音恢复 | 原 36–48h,增量待评估 |
| D12 | 灰度、交接与发布 | 固定版本→小流量试运行→问题收敛→回滚演练→交付配置/操作/排障说明与验收记录 | 12–16h |
| **原基准合计** | **本人范围** | **不含未认领的 SaaS 配合开发、供应商等待及本轮增量;新合计待 G0 评估** | **328464h(历史基准)** |
### 6.1 关键开发约束
**线路与 FALLBACK** 支持单线路启动,供应商按需预接入、逐呼从已配置且获授权的线路策略选路;不等待所有供应商接齐,也不要求凑齐主备。仅对确认的线路接入故障切换,且备用必须兼容本通话固定的 Cell/出口及白名单;当前仅一家线路,无适合备用则失败,不虚构备用或迁移活动通话。忙线、拒接、无效号码、黑名单默认不切,无人接听是否重试由 SaaS 业务策略决定。接通后不新建备用呼叫。本地媒体或 AI 故障不能直接当作 SIP 线路故障处理。状态不明进入待对账,禁止盲目重新拨号。
**实时 AI** 中间结果可合并,最终文字必须可恢复;TTS 文本区分已生成、已发送/播放确认和已取消,不将“生成完成”描述成“客户已听到”。音频线程不等待 SaaS 回调或录音上传。AI 失败使用有限重试及经确认的结束/兜底策略。
**录音:** 文件先上传 OSS,确认成功并取得 OSS ID 后,才通过 RabbitMQ 回传 OSS ID 及必要元数据;不发送二进制/Base64,也不以公开 URL 替代 OSS ID。SaaS 管理长期存储与播放权限,呼出侧仅按确认的恢复期限暂存。SaaS 不可用时有磁盘水位、积压告警和停止接单策略,不允许无限缓存。访问录音必须按租户和用户授权,签名地址短期有效且不写入公开日志。
**业务安全:** SaaS 在调度入口实施授权、允许时段、基础频控及拒绝再联系拦截;呼出应用实施租户/任务关联、有效期、资源上限和停机屏障。拒绝再联系事件及时回传,不能等二期才避免重复骚扰。
## 7. 里程碑与排期
| 门禁 | 本人产出 | SaaS/外部配合 | 通过条件 |
| --- | --- | --- | --- |
| G0:对接确认 | 本人制定并发布 D01 契约、样例、技术验证和参数基线 | SaaS/运维按规范认领接口及MQ、SIP、AI、OSS测试权限 | 已确认的双向MQ和OSS ID回传方案落实为可验证契约;关键测试依赖可用 |
| G1:指令闭环 | D02D03D09查询/应答基础 | SaaS能按租户发布、处理背压、接收应答和调用控制接口 | 重复指令不重复执行,过期/停止指令被拒绝,暂停生效可查询;大租户积压不阻塞有额度/资源的小租户,多实例不超租户配额 |
| G2:电话闭环 | D04–D05 | 当前主线路与授权测试号码;有备用时提供独立接入及白名单 | 单线路可双向通话;无备用明确失败不虚构容灾;有适合备用时验证合法切换,迟到接通不导致双拨。备用尚缺时记录其真实验证为待验,不把单线路通过称为完整 FALLBACK 验收 |
| G3AI与资产闭环 | D06D08D09补偿完善 | SaaS MQ结果消费、OSS上传授权/确认及FeA展示页面 | 多轮/打断可用;文字经MQ正确入库;OSS ID可关联录音并鉴权播放;断网可经MQ补回 |
| G4:上线验收 | D10D11 | 监控告警、多 Cell/EIP 故障演练环境、验收参与者 | 关键验收全部通过;至少1000路已接通完整ASR/LLM/TTS,且拨号/CPS/故障冗余达到G0基线;无阻断缺陷 |
| G5:一期交付 | D12 | 发布窗口、业务试用和运维交接 | 灰度通过;回滚可执行;遗留风险有明确负责人 |
执行顺序:D01→D02→D03/D04→D05/D06→D07/D08→D09完善→D10/D11→D12。D09/D10 的基础能力从早期开始,不到项目末尾才补幂等和日志。
SaaS 可在 G0 后按 Mock 并行开发;本人为单人时,上述模块不能被当成多个独立人力并行压缩工期。
### 7.1 工时与周期口径
以下为本轮调整前的历史口径;租户队列生命周期、公平调度、多实例额度协调及新增验收的增量尚未评估,不能沿用为更新后的固定交期。G0 同时复核 D01/D02 契约与环境配合影响,不擅自假定已有20%预留足以覆盖。
- 基准:**328464 小时**,即 **4158 人日**8小时/人日)。
- 加20%计划预留:**393.6556.8 小时**,即 **49.269.6 人日**
- 若本人每周有40小时可投入该范围,约 **1014个工作周**;若每周只有24小时,约 **1724个工作周**。均不含供应商或 SaaS 外部等待。
- 以上以可用 SIP/AI 服务及可复用媒体组件为前提,不包含自研媒体引擎。G0 验证不成立、新增供应商或接口发生破坏性变化时单独重估,不能靠20%预留覆盖全部新增范围。
- 这不是一期全团队总工时。BgA/BgB/FeA配合量及本人兼任部署/值守的占用,由对接负责人确认后再排入日历;不虚构额外人力,也不直接套用旧 Excel 的三期周期。
## 8. 验收用例与记录
每个用例记录:版本、环境、参数、操作、预期、实际、日志/事件证据和结论。使用授权测试号码,不以随机真实客户做故障实验。
| 编号 | 场景 | 验收条件 |
| --- | --- | --- |
| AT-01 | 命令重复、重启和ACK丢失 | 同一授权执行不因消息重复或更换 command_id 生成第二次拨号;已接收指令重启后可查询、可恢复;call_id 尚无时通过 command_id 观察 waiting,窗口不因重投/重启延长,截止后拒绝且不拨号,原因与 MQ 事件一致 |
| AT-02 | 暂停/停止与积压竞争 | 生效确认后不再新发起;迟到旧指令和死信重放不能重新开启已停止任务 |
| AT-03 | 主线路基本通话 | 发起、振铃、接通、双向语音、挂断及原因正确;通道/桥/媒体最终清理 |
| AT-04 | 单线路及合法FALLBACK | 无备用时可启动、遇故障明确失败;有适合备用时,仅主尝试明确结束、未接通且属于允许故障才切换,保持同一 Cell/出口,记录完整 attempt 和线路信息;无独立备用时真实切换验收标待验 |
| AT-05 | 不应FALLBACK | 已接通、状态不明、忙线/拒接/无效号码、本地AI故障不会引发未经授权的备用重拨 |
| AT-06 | 迟到接通/ARI断连 | 主备竞态通过对账收敛,不形成双通;不把“未收到事件”当成“未接通” |
| AT-07 | 多轮对话与打断 | 识别、生成、播音连续可用;打断停止旧播音/生成;延迟达到G0确认指标 |
| AT-08 | 文字重复、乱序和最终稿 | 同轮不重复堆叠;最终稿覆盖中间稿;取消内容标记正确;SaaS最终内容可核对 |
| AT-09 | 录音与OSS上传失败 | 先确认OSS上传成功,再经MQ回传OSS ID;SaaS可按OSS ID鉴权播放;失败不发ready并经MQ回传失败状态;临时文件受控清理 |
| AT-10 | SaaS/MQ中断与回传重放 | 所有业务结果经MQ回传,无HTTP回调旁路;音频不被回传阻塞;恢复后幂等补齐且无重复资产;积压达到上限有告警和保护 |
| AT-11 | 租户与凭证安全 | 非授权租户无法查询/补传/播放;凭证不出现在日志;失效上传授权被拒绝 |
| AT-12 | 多 Cell/EIP 容量、时段、退出与回滚 | 至少1000路已接通完整ASR/LLM/TTS;不超供应商并发/CPS、Cell媒体端口和最长通话;单 Cell/出口故障后仍满足N+1承接能力;发布回滚不丢待投递事件、不重新拨历史通话 |
| AT-13 | 大租户积压与公平调度 | A 大量积压后 B/C 新入队;B/C 有额度和可用资源时无需等 A 排空,按约定轮次/时延获得执行机会;记录到达、加入轮转和真实发起时间。预取、已 ACK 待发起及重启恢复仍公平;A 无可用线路时不阻塞 B |
| AT-14 | 跨 Cell/多调度实例租户配额 | 竞争发起、FALLBACK、所有权切换和进程重启均不重复占额/拨号、不超租户总并发/CPS;状态不明占用不因租约过期直接释放;资源满时不强制挂断,若有保底 SLA 则独立验收 |
| AT-15 | 租户队列背压、路由与生命周期 | A 发布洪峰/满队列被明确背压、SaaS 保留待发布记录;confirm 不确定用原 ID 重试不丢旧消息/不双拨;约定负载范围内 B 仍可发布调度;租户路由错配拒绝、创建/停用/恢复不误删积压,重试/DLQ 不绕过原租户配额 |
G4 前必须将“约定指标”填写为明确数值,包括目标并发/CPS、响应和打断延迟的测量起止点及分位值、回传完成时限、最长可恢复中断、磁盘水位和失败重试边界。多租户专项还须在 G0 冻结租户/队列数量、发布洪峰、轮转份额、等待时间起止点/分位值、资源可用前提、配额和背压阈值;区分资源耗尽等待与调度饥饿,不无条件承诺固定开始时限。未经测试,不宣称达到生产高可用或特定并发能力。
## 9. 发布、交付与运行责任
### 9.1 交付清单
1. 本人制定并发布的 OpenAPI v1、MQ 拓扑/消息/事件 Schema、OSS ID 回传规范、示例、错误码和对接验证记录。
2. 呼出应用代码/构建产物、配置模板及版本说明;不随交付包提供明文生产密钥。
3. Asterisk/SIP接入参数说明、主备切换策略及失败码映射。
4. 测试/回放工具、AT-01~AT-15验收记录、容量与延迟测试结果(含多租户公平、背压及配额恢复)。
5. 部署、数据库变更、升级、回滚、故障排查、死信重放和资产补传操作说明。
6. 监控清单:活动通话、发起速率、失败/FALLBACK、AI延迟、按租户命令积压/等待/额度/背压、broker 队列及水位、回传积压、录音失败、磁盘和资源残留。
### 9.2 发布步骤
- 部署测试环境,验证配置/权限/网络及数据备份;数据库改动与旧版本兼容。
- 先小流量、低并发灰度,观察主备尝试、对话体验、文字/录音和回传积压。
- 异常时先停止接新指令,对在途通话按确认策略排空或处置,保留执行/投递记录后回滚。
- 只有验收和恢复演练通过后才扩大流量,不把生产流量当作媒体链路的首次验证。
- 上线后明确SaaS、呼出应用、SIP、AI和运维各自的故障联系人;业务页面问题与媒体线路问题分别定位。
## 10. 范围变更规则
一期以“指令有应答、真实外呼可控、单线路可启动且合法切换不重复拨号、AI可对话、文字录音能回SaaS并恢复”为交付闭环。独立备用未提供时记录真实 FALLBACK 验证为待验,不宣称已具备线路容灾。
新增计费、RAG、复杂路由、第二种回传通道、多供应商通用适配或集群容灾时,另建变更项,写明工作量与对本期门禁的影响,不默认混入本计划。SaaS API、SIP参数、媒体组件或本人可投入时间变化时,更新本开发计划的版本并重新确认受影响的排期。
+134
View File
@@ -0,0 +1,134 @@
# 交互流程图(规划态)
**版本:** v0.1
**依据:** [一期计划 v1.3](一期呼出应用开发计划_v1.0.md)、[OpenAPI 与 MQ 契约规划 v0.3](SaaS交互_OpenAPI与MQ契约规划_v0.1.md)。
**状态:** 目标交互,尚未完整实现;具体字段、配额、等待窗口及 AI 协议待 G0 冻结。当前仅有 ASR 验证基础,LLM/TTS 未启用。本图不新增接口或变更契约。
```mermaid
flowchart TB
subgraph SaaS["SaaS:业务授权与发布"]
START["任务、号码、租户授权与业务检查"]
PUB["持久化发布记录<br/>command_id + execution_id"]
BACK["满队列拒绝/不可路由/confirm 不确定<br/>保留原 ID,有限退避与对账"]
START --> PUB
end
subgraph COMMANDS["RabbitMQ:唯一外呼执行入口"]
EX["call.execute<br/>按可信 tenant_id 映射路由"]
Q["各租户独立命令队列<br/>有界积压,不丢队头旧命令"]
EX --> Q
end
PUB --> EX
EX -. "发布受阻或结果不确定" .-> BACK
BACK -. "原租户、原 ID 重试" .-> PUB
subgraph OUTBOUND["呼出应用:接收、控制与公平准入"]
READ["按租户有界轮转<br/>校验 Schema、身份及正文/队列租户一致"]
VALID{"可信且合法?"}
DEAD["非法/不可信消息隔离告警<br/>不得按伪造租户执行或回传"]
IDEM{"command_id / execution_id<br/>是否已有记录?"}
ORIGINAL["同语义关联原结果;冲突拒绝<br/>不重新拨号"]
CHECK{"新执行授权、时效、版本<br/>及配置是否有效?"}
REJECT["持久化 rejected + outbox<br/>首次意图前拒绝:call_id 为空"]
ACCEPT["事务持久化命令、去重及 accepted outbox<br/>完成后 ACK;此时尚未拨号"]
ADMIT{"控制/期限仍有效<br/>且完整资源准入成功?"}
WAIT["waiting + 原因 + 准入截止<br/>call_id 为空;按 command_id 查询"]
WAKE{"到期或被控制阻止?<br/>到期有界唤醒,不等待资源释放"}
INTENT["固定 trunk + Cell + 出口,持久化拨号意图<br/>首次建 call_id;每次尝试建 attempt_id"]
FENCE{"执行器真实发起前<br/>再次检查控制、期限及租约"}
READ --> VALID
VALID -- "否" --> DEAD
VALID -- "是" --> IDEM
IDEM -- "是" --> ORIGINAL
IDEM -- "否" --> CHECK
CHECK -- "否" --> REJECT
CHECK -- "是" --> ACCEPT
ACCEPT --> ADMIT
ADMIT -- "是" --> INTENT
ADMIT -- "否" --> WAIT
WAIT --> WAKE
WAKE -- "是" --> REJECT
WAKE -- "否:继续公平轮转" --> ADMIT
INTENT --> FENCE
end
Q --> READ
subgraph VOICE["固定语音 Cell:电话与实时 AI"]
DIAL["本地执行器 → ARI / Asterisk<br/>经本 Cell 固定 EIP 向授权 SIP 线路发起"]
OBS{"通道事实/呼叫结果"}
RECON["reconciling:先对账<br/>不盲目重拨,不释放未知占用"]
FALL{"明确未接通且旧通道已结束<br/>允许故障、有同 Cell/出口的授权备用?"}
LEASE{"FALLBACK:重新检查控制、期限<br/>租户 CPS 与完整资源许可"}
AI["接通后的目标链路<br/>媒体 ↔ ASR → LLM → TTS → 媒体<br/>流式对话、打断与取消;LLM/TTS 待新规范"]
END["确认通道结束,清理资源<br/>持久化 call.finished;资产可仍为 pending"]
DIAL --> OBS
OBS -- "状态不明" --> RECON
RECON -- "取得可靠证据后收敛" --> OBS
OBS -- "已接通" --> AI
OBS -- "明确未接通" --> FALL
FALL -- "是" --> LEASE
LEASE -- "许可有效;同 call_id,新 attempt_id" --> INTENT
LEASE -- "否:不盲目重试" --> END
FALL -- "否:无备用不虚构" --> END
AI -- "挂断并确认" --> END
end
FENCE -- "通过" --> DIAL
FENCE -- "未通过且确认尚未发出:失败/取消收尾" --> END
FENCE -- "是否已发出不明" --> RECON
subgraph ASSET["异步录音交接:不阻塞实时音频或通话终态"]
FILE["封口录音文件,计算元数据与校验值"]
AUTH["呼出资产处理器 → SaaS HTTP<br/>申请绑定租户/通话的上传授权"]
OSS["呼出资产处理器 → OSS<br/>使用受控签名上传文件"]
COMPLETE["呼出资产处理器 → SaaS HTTP complete<br/>SaaS 实际校验 OSS 对象"]
VERIFIED{"校验成功并取得稳定 oss_id"}
READY["verified + recording.ready outbox<br/>同事务持久化,事件含 oss_id"]
FAILED["recording.failed + outbox<br/>按契约有限恢复;未验证不发 ready"]
FILE --> AUTH --> OSS --> COMPLETE --> VERIFIED
VERIFIED -- "是" --> READY
VERIFIED -- "否" --> FAILED
end
END -. "存在待交接录音时" .-> FILE
subgraph RESULTS["所有业务结果:持久化事件 → MQ → SaaS"]
OUTBOX["执行事实与 outbox 同事务<br/>投递器:confirm + mandatory,有限重试"]
EVENTS["RabbitMQ 事件 Exchange → SaaS 事件队列<br/>本轮不按租户拆分事件队列"]
INBOX["SaaStenant_id + event_id 去重<br/>业务更新与 inbox 同事务后 ACK<br/>按版本合并,关联 oss_id 并授权展示"]
OUTBOX --> EVENTS --> INBOX
end
REJECT --> OUTBOX
ACCEPT -. "command.result" .-> OUTBOX
WAIT -. "状态/原因变化" .-> OUTBOX
INTENT -. "executing 不等于实际拨号" .-> OUTBOX
OBS -. "call.status" .-> OUTBOX
RECON -. "待对账状态" .-> OUTBOX
AI -. "transcript.updated 等文字事件" .-> OUTBOX
END --> OUTBOX
READY --> OUTBOX
FAILED --> OUTBOX
CONTROL["SaaS → 呼出 HTTP:暂停/恢复/停止<br/>202 仅受理;跨 Cell 屏障确认后才 applied"]
QUERY["SaaS → 呼出 HTTP:命令/通话查询<br/>仅对账,不替代 MQ 业务结果"]
REPLAY["SaaS → 呼出 HTTP:历史事件补传<br/>固定范围、原 event_id;不拨号/不调用 AI"]
CONTROL -. "禁止新许可,处理在途许可" .-> ADMIT
CONTROL -. "真实发起前校验" .-> FENCE
CONTROL -. "生效结果经 MQ" .-> OUTBOX
QUERY -. "读取持久事实与版本快照" .-> OUTBOUND
REPLAY -. "重发原持久事件" .-> OUTBOX
classDef planned fill:#eef4ff,stroke:#4263a6,color:#172a46;
classDef pending fill:#fff4dc,stroke:#b7791f,color:#573b13;
classDef risk fill:#fff0ef,stroke:#b44c45,color:#632822;
classDef data fill:#eaf7f1,stroke:#2d8063,color:#164b39;
class START,PUB,READ,ACCEPT,INTENT,DIAL,CONTROL,QUERY,REPLAY planned;
class WAIT,AI pending;
class DEAD,REJECT,RECON,FAILED,BACK risk;
class OUTBOX,EVENTS,INBOX,OSS,READY data;
```
## 阅读边界
- 完整资源许可同时覆盖跨 Cell 的租户并发/CPS、供应商配额、Cell 媒体端口/节点容量、出口健康与 ASR/LLM/TTS 配额;每次 FALLBACK 也消耗 CPS。初次准入截止为 `min(not_after, accepted_at + 服务端窗口)`,重投/重启不延长;它不强制挂断已发起电话。
- `accepted`、broker confirm、消费者 ACK、`executing`、实际拨号、接通及 SaaS 入库是不同事实。可信业务拒绝可回传;非法消息先隔离。重复命令关联既有事实,不创建第二通电话。
- 暂停/停止覆盖队列积压、持久等待和多 Cell 在途许可;状态不明不宣称 applied。resume 不复活旧版本。公平不挂断已接通电话;stop 的 drain/hangup 按明确授权处理。
- 数据补传只重发原事件;录音恢复独立处理。文字/录音失败不改写通话终态。新业务外呼由 SaaS 完成对账及授权后决定,不由消费者自动换 ID。
@@ -0,0 +1,276 @@
# 第一部分:中间调度件与 MQ 回传设计
**版本:** v1.0;双向MQ交互方式已确认,详细契约及真实环境参数待 G0 对接评审。
**用户确认:** RabbitMQ 下发执行指令、RabbitMQ 回传业务事件;HTTP 仅用于控制、查询、录音上传授权及完成确认。
**范围:** 一期纯外呼、不计费、复用现有 SaaS。本文替代旧计划中的“HTTP 业务回调”草案,不修改原 Excel。
**交付性质:** 本次交付设计和操作/验收文档,不代表中间件代码已实现,也不代表已部署到任何服务器。
## 1. 组件边界与最小拓扑
```text
SaaS:任务/号码/配置/业务调度/权限/展示/长期存储
│ 发布 call.execute ▲ 接收 command.result / call.* / transcript.* / recording.ready
▼ │
RabbitMQcommand exchange event exchange → SaaS消费队列
│ ▲
▼ │
中间调度件:指令持久化 → 准入/控制 → 呼叫执行 → 数据落地/outbox → 事件投递
│ REST + 每节点一个长期ARI WebSocket │ HTTP上传授权/上传完成/接收结果查询
├── Asterisk A ── 主/备用 SIP接入 └── SaaS API及授权对象存储
└── Asterisk B ── 主/备用 SIP接入
│ externalMedia RTP(不经过RabbitMQ
└── 媒体/AI组件:VAD → ASR → LLM → TTS,支持取消与打断
```
- SaaS 决定何时、向谁发起,以及业务层“再次拨打”的政策;中间件不复制客户管理和任务编排。
- 中间件负责执行级准入、并发/CPS、暂停屏障、Asterisk 节点选择、SIP 尝试、执行事实、文字/录音回传及补偿。
- Asterisk 节点无共享通道状态;双节点不是存量通话无损接管。故障节点上的已接通通话不得自动在另一节点重拨。
- 一期采用一个活动中间件实例管理多个 Asterisk 节点,持久化后支持重启恢复。一个节点的 ARI 应用只允许一个控制者;不承诺活动中间件自动高可用。未来多实例必须按节点明确归属,不能竞争控制同一通道。
- 每个节点维持长期 ARI 事件连接;REST 请求和事件统一进入执行状态机。不可照搬 Mock 的“每通电话各建相同 app 的连接并丢弃事件”方式。
- 不新增 Redis、工作流引擎或独立服务拆分作为前置依赖。复用可用关系数据库存放执行与投递状态;媒体组件可以独立运行,但数据回传不能进入其音频实时线程。
## 2. 数据归属与最小持久化模型
| 数据 | 所有者与关键字段 | 约束 |
| --- | --- | --- |
| 任务、号码、智能体快照 | SaaStask_id、task_item_id、agent_version_id、variables、task_revision | SaaS 保持业务事实;快照不可在执行中悄悄替换 |
| command_inbox | 中间件;tenant_id、command_id、execution_id、request_hash、payload、accepted_at、status | 同租户 command_id 唯一;同键不同内容拒绝,不覆盖原命令 |
| task_control | 中间件;tenant_id、task_id、revision、desired_state、effective_state | 控制版本单调递增;停止屏障不能被迟到的旧指令解除 |
| call_execution | 中间件;tenant_id、call_id、execution_id、task_item_id、状态版本、时间、终态、原因 | 同租户execution_id唯一代表一次授权执行;同租户同一任务项不得同时存在两次活动业务执行 |
| call_attempt | 中间件;attempt_id、call_id、node_id、trunk_id、channel_id、bridge_id、external_channel_id、发起意图和清理状态 | 调用 ARI 前落地关联标识;同一 execution 只允许一个活动尝试 |
| call_artifact | 中间件;call_id、文字轮次/版本、录音位置/校验/状态、保留期限 | 电话结束与资产处理分离;本地暂存不替代 SaaS 长期存储 |
| event_outbox | 中间件;event_id、payload、聚合版本、发布次数、下次重试、published/applied状态 | 业务事实与事件同事务提交;broker confirm 不等于 SaaS 已落库 |
| event_inbox | SaaStenant_id、event_id、payload_hash、applied_at、处理结果 | 去重与业务落库同事务;业务提交后才 ACK;优先复用 SaaS 现有机制 |
一期单实例可以在同一进程内实现接收、执行、投递、对账和清理任务,但各自失败不能阻塞媒体。状态更新仍需事务及版本条件,不能仅靠内存字典防重复。
## 3. RabbitMQ 拓扑与可靠性
以下名称为规范草案,部署时按 SaaS 命名规则映射;业务 Schema 和语义保持一致。
| 对象 | 建议命名/绑定 | 用途 |
| --- | --- | --- |
| 命令交换机 | ai.outbound.command.v1topic、durable | SaaS 发布执行命令 |
| 命令队列 | ai.outbound.execute.v1,绑定 call.execute | 中间件消费;队列满时拒绝发布/暂停生产,不静默丢弃 |
| 事件交换机 | ai.outbound.event.v1topic、durable | 中间件回传业务事件 |
| SaaS事件队列 | saas.ai.outbound.event.v1,绑定本节规定的事件类型 | SaaS 幂等入库及触发展示 |
| 死信交换机/队列 | 复用 SaaS 标准 DLX,命令与事件分别隔离 | 超出重试预算、非法消息和不可恢复异常 |
必须落实:
1. 两侧生产者均启用 persistent 消息、Publisher Confirm 和 mandatory/不可路由检查;收到 confirm 但发生 basic.return 仍视为投递失败。
2. durable 队列不等于集群高可用。若现有 RabbitMQ 支持经验证的 quorum 队列可复用;不得在未知版本/策略下直接强制变更现有队列类型。
3. 中间件在命令及去重记录持久化后 ACK,不把整个电话挂在未 ACK 消息上;SaaS 在业务应用事件成功后 ACK。
4. 中间件已受理命令由持久化执行记录恢复。ACK 后进程崩溃不依赖重新收到同一条消息才能继续。
5. 基础 prefetch 从小值开始,配合数据库待执行数量上限。执行容量不足时降低/暂停拉取或给出明确拒绝结果,不能无界吸收任务。
6. 执行准入同时受租户、节点、线路的并发/CPS和任务控制状态限制。号码/DNC/时段等业务风控由 SaaS 统一决策,延迟执行和补投前重新校验有效性。
7. 基础设施暂时失败走有限退避;业务忙线、拒接不是 MQ 异常,不通过 nack/requeue 反复重拨。已有 SaaS 重试/DLQ 机制优先复用,不同时叠加两套无限重试。
8. 关键最终事件不能靠短 TTL 自动淘汰。published 事件保留至 SaaS 应用确认或经授权的保留期处理;删除策略和最长可恢复中断在 G0 明确。
9. 人工重放分为“命令恢复”和“数据补传”。命令恢复仍检查 execution_id、有效期、已接通事实和停止屏障;数据补传永不发起电话。
## 4. 命令、事件与应答契约
### 4.1 标识与公共规则
- command_id:一次指令;消息重投必须相同。execution_id:一次授权业务外呼;SaaS 决定再次外呼时才新建。
- call_id:一次业务外呼的聚合标识。attempt_id:一次 Asterisk/SIP 尝试;FALLBACK 新建 attempt,但不新建业务 execution。
- event_id:事件唯一标识;重发保持原值和内容。task_revision:任务控制版本。state_version:通话状态版本。
- 所有时间戳为 RFC3339 UTC;字段名含 `_ms` 的持续时间以毫秒计。号码、原始错误、录音引用均视为租户敏感数据。
- 每类消息单独定义必填字段。尚未产生通话时,不要求 command 带 call_id/attempt_id。
- tenant_id 必须与凭证、消息来源及任务归属校验,不仅相信 payload。只接受号码/允许的线路引用,不接受任意 ARI URL、原始 dial string、任意 externalMedia 地址或凭证。
- 下列 JSON 使用测试占位符;时间、号码和配置需由测试生成器替换,不得原样发送至生产。
### 4.2 call.execute
```json
{
"schema_version": "1.0",
"command_id": "cmd_demo_001",
"execution_id": "exec_demo_001",
"tenant_id": "tenant_test",
"trace_id": "trace_demo_001",
"issued_at": "2026-09-08T01:00:00Z",
"not_after": "2026-09-08T01:05:00Z",
"task_id": "task_demo",
"task_item_id": "item_demo",
"task_revision": 7,
"payload": {
"destination": "${AUTHORIZED_TEST_NUMBER}",
"agent_version_id": "agent_v1",
"agent_snapshot": {"prompt_version": "prompt_v1", "variables": {}},
"line_group_id": "line_group_test",
"ring_timeout_ms": 30000,
"max_call_duration_ms": 180000,
"fallback_policy_id": "primary_backup_v1"
}
}
```
快照须包含执行所需数据;若采用引用,SaaS 必须提供鉴权读取接口及一致的版本校验,不默认“只有ID就能运行”。同幂等键不同 request_hash 返回 IDEMPOTENCY_CONFLICT。过期/停止/越权/无可用线路返回明确拒绝结果,MQ ACK 不应被展示为“已呼叫”。
### 4.3 事件模型
公共外壳包括 event_id、event_type、schema_version、tenant_id、trace_id、occurred_at 和 payload;业务关联ID按事件类型携带。
| Routing key / event_type | 必需业务字段 | SaaS 更新规则 |
| --- | --- | --- |
| command.result | command_id、execution_id或控制对象、result、reason、effective_revision | result=ACCEPTED/REJECTED/APPLIED/FAILEDACCEPTED不等于执行完成 |
| call.status | call_id、attempt_id、state_version、state、node_id、trunk_id、时间与原因 | 版本不回退;attempt结果不直接覆盖已接通的业务终态 |
| transcript.partial | call_id、turn_id、role、revision、text、is_final=false | 可合并/限流;中间稿不是最终业务事实 |
| transcript.final | call_id、turn_id、role、revision、text、起止时间、播放/取消标记 | 同轮按版本幂等更新;必须持久和可补传 |
| recording.ready | call_id、recording_id、object_ref、格式/声道/采样率/时长/大小/checksum | 只传上传确认后的受控引用,不传二进制和长期公开URL |
| call.finished | call_id、state_version、final_state、cause、时长、attempt_count、资产处理状态 | 通话终态立即可见;录音/分析可随后完成 |
```json
{
"schema_version": "1.0",
"event_id": "evt_demo_status_2",
"event_type": "call.status",
"tenant_id": "tenant_test",
"trace_id": "trace_demo_001",
"occurred_at": "2026-09-08T01:00:06Z",
"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",
"payload": {
"state": "ANSWERED",
"state_version": 2,
"node_id": "ast-a",
"trunk_id": "sip-primary",
"answered_at": "2026-09-08T01:00:06Z"
}
}
```
```json
{
"schema_version": "1.0",
"event_id": "evt_demo_text_1",
"event_type": "transcript.final",
"tenant_id": "tenant_test",
"trace_id": "trace_demo_001",
"occurred_at": "2026-09-08T01:00:10Z",
"call_id": "call_demo",
"payload": {
"turn_id": "turn_1",
"role": "customer",
"revision": 2,
"text": "这是授权测试。",
"is_final": true,
"start_offset_ms": 500,
"end_offset_ms": 1800,
"playback_status": "not_applicable"
}
}
```
AI 文字另标记 generated/sent/playback_confirmed/cancelled。播放器确认不等于能证明客户实际听到了声音;被打断而未播出的内容不能全部作为“已说出”展示。
### 4.4 错误与恢复语义
统一原因至少包含:INVALID_COMMAND、IDEMPOTENCY_CONFLICT、TENANT_FORBIDDEN、TASK_STOPPED、COMMAND_EXPIRED、CAPACITY_EXCEEDED、NO_ROUTE、SIP_UNAVAILABLE、BUSY、REJECTED、NO_ANSWER、INVALID_NUMBER、AI_ERROR、MEDIA_ERROR、CONTROL_UNCERTAIN、UPLOAD_FAILED。保留原始 SIP 状态/Q.850/Asterisk 原因用于排障,映射由真实线路验证。
明确拒绝的执行不进入拨号;容量等待/拒绝政策由 SaaS 与中间件统一,不能两边各自重试。CONTROL_UNCERTAIN 表示需对账,不等价于可重拨失败。
## 5. HTTP 边界与接口清单
| 方向/方法 | 接口草案 | 请求/响应要点 |
| --- | --- | --- |
| SaaS→中间件 POST | /internal/v1/tasks/{task_id}/controls | command_id、expected_revision、action=PAUSE/RESUME/STOPforce_hangup默认false且另行授权;202返回已受理 |
| SaaS→中间件 GET | /internal/v1/commands/{command_id} | 查询ACCEPTED/APPLIED/REJECTED/FAILED与实际控制版本 |
| SaaS→中间件 GET | /internal/v1/calls/{call_id} | 通话/尝试、state_version、资产和投递状态;不改状态 |
| SaaS→中间件 POST | /internal/v1/calls/{call_id}/events/replay | 仅补传已有事件,保持event_id;鉴权、审计、限流 |
| 中间件→SaaS GET | /internal/v1/outbound/executions/{execution_id}/eligibility | 返回allowed、reason、current_task_revision、valid_until;每次发起前查询当前时段/退订/授权许可,不返回凭证 |
| 中间件→SaaS POST | /internal/v1/recording-uploads | call_id、格式、大小、checksum;返回upload_id、授权上传地址、过期时间及允许参数 |
| 中间件→SaaS POST | /internal/v1/recording-uploads/{upload_id}/complete | 幂等确认文件/校验;返回recording_id和object_ref |
| 中间件→SaaS POST | /internal/v1/outbound/receipts/query | 查询最多100个event_id的APPLIED/NOT_FOUND/FAILED结果;仅用于低频对账,不是业务回调 |
控制请求经过与命令相同的持久化/幂等机制。200/202不代表暂停已经生效;必须查询或消费 command.result(APPLIED)。非法参数400、无权限401/403、状态/版本冲突409、限流429;重复同内容请求返回原结果。
首次发起、延迟执行及FALLBACK之前查询当前业务许可,结合本地控制屏障再次校验;不得把命令发布时的授权永久缓存。许可查询不可用时不新拨号,进入有期限的等待/拒绝并告警;不会因此中断正在进行的媒体。授权变化与暂停生效的时间边界在G0确认。
鉴权沿用已批准的 SaaS 服务认证并限定租户权限;跨主机HTTPS,敏感操作审计。API路径及认证细节需对接签字后生成正式OpenAPI文件,不将示例路径当成双方已部署接口。
## 6. 状态机、控制与FALLBACK
### 6.1 执行状态
- ACCEPTED → QUEUED → DIALING → ANSWERED → TALKING → ENDED。
- DIALING允许直接ANSWEREDRINGING是可选观测状态,不要求每次都有振铃事件。
- 接通前可结束为FAILED、CANCELLED、EXPIRED;已接通后以ENDED及原因结束,不退回QUEUED。
- 控制连接断开且事实无法确定时进入RECONCILING。恢复时查询原node/channel,不能因本地没有成功回执就重拨。
- 文字/录音状态独立使用PENDING/PROCESSING/READY/FAILED;事件状态独立使用PENDING/PUBLISHED/APPLIED。资产失败不阻止电话终态落地。
### 6.2 控制屏障
PAUSE先阻止SaaS新调度,中间件再建立版本屏障,处理完在途发起许可后确认APPLIED。STOP为终止性控制,旧revision或死信不得恢复任务。已接通通话默认排空;强制挂断需要独立授权和审计。许可检查、发起意图与控制版本关联,避免“先检查暂停再被停止但仍拨出”的竞态。
### 6.3 两种故障不得混淆
1. **节点选择失败:** 尚未提交originate时,健康/容量不合格的Asterisk节点可以换选。
2. **已提交拨号:** 超时不代表失败。先用持久化的channel_id核实原尝试;事实不明不得换节点重拨。
3. **SIP主备切换:** 仅在明确未接通、旧通道已结束、失败码属于允许线路故障、尚在有效期且预算允许时执行。默认最多主/备两次尝试,节点与线路切换共用总预算,不能组合放大。
4. BUSY、REJECTED、INVALID_NUMBER、DNC拦截不切备用;NO_ANSWER由SaaS业务重试政策处理。已接通后禁止FALLBACK新拨号。
5. AI/本地媒体故障不通过换SIP解决。两条SIP接入同属一个故障域时,主备不构成独立容灾。
6. 故障节点已接通电话可能中断,记录原因并通知SaaS;一期不提供通话热迁移。新增呼叫转向健康节点需重新校验容量。
## 7. ARI、媒体、文字和录音生命周期
1. 建立节点长期ARI事件连接并确认应用注册;创建call/attempt、确定固定的PJSIP通道、桥和externalMedia通道标识并持久化发起意图。
2. 创建桥和externalMedia会话,为每通电话分配独立媒体端口/关联;发起PJSIP呼叫并消费StasisStart、ChannelStateChange、ChannelDestroyed等事件,按实际状态推进。
3. 接通后将目标通道加入桥;音频格式按SIP协商、RTP payload、采样率和AI接口要求转换,验证ASR不会把TTS回声当作客户输入。
4. RTP音频直接进入媒体组件。VAD/ASR/LLM/TTS及打断需要独立实现或可靠组件,Mock音调发送/包统计不是AI链路。
5. 关键文字最终稿异步持久化、写outbox并发布;中间稿可限流合并,最终稿和状态事件不可被慢消费者无限阻塞。
6. 接通后可通过ARI的桥录音能力启动混音录音,名称包含受控call/attempt标识;本期默认单轨混音,双声道需另行验证,不把桥录音直接称为双声道。
7. 挂断时停止/确认录音完成,等待录音文件封装完成后读取/上传。优先通过ARI stored recording接口获取,或由同节点受控进程读取持久卷,不把同一路径误当成跨主机共享文件。
8. SaaS上传授权→文件上传→完成确认/校验→写recording.ready到outbox→MQ发布→SaaS消费入库和页面播放。
9. 清理通道、桥、媒体会话;清理失败由补偿任务按记录核实处理。不得使用“清空所有通道”替代按call_id清理。
录音元数据:recording_id、call_id、attempt_id、format、channels、sample_rate_hz、duration_ms、size_bytes、checksum_sha256、object_ref、created_at。上传确认前不发ready。临时文件设置容量、水位和保留期限;未确认入库的文件不能在普通成功清理中删除,超期处置须告警和授权。
### 7.1 recording.ready 示例
```json
{
"schema_version": "1.0",
"event_id": "evt_demo_recording_1",
"event_type": "recording.ready",
"tenant_id": "tenant_test",
"trace_id": "trace_demo_001",
"occurred_at": "2026-09-08T01:03:10Z",
"call_id": "call_demo",
"attempt_id": "attempt_demo_1",
"payload": {
"recording_id": "recording_demo_1",
"object_ref": "tenant_test/call_demo/recording_demo_1.wav",
"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"
}
}
```
上传授权/完成确认是HTTPrecording.ready业务通知是MQ。例中校验值和尺寸为占位/演示,必须以实际文件计算结果为准。
## 8. 恢复、可观测与开发顺序
恢复顺序:暂停新执行→读取未完成attempt→核实节点/通道→修正执行事实→恢复未投递/未应用关键事件→清理孤儿资源→健康检查通过后恢复接单。锁或租约过期不能证明Asterisk通道已结束。
监控至少包括命令积压/最老年龄、拒绝原因、活动通话/线路CPS、ARI连接、未决attempt、FALLBACK、音频丢包与AI延迟、outbox积压、SaaS落库延迟、录音失败、临时磁盘和清理失败。call_id贯通日志;手机号、密钥、签名URL脱敏。
开发顺序与门禁:
1. G0:真实参数、Schema/控制语义、MQ拓扑和存储协议确认;本人主责,SaaS与运维认领。
2. G1inbox/outbox、命令接收/应答、控制和查询;重复消息与重启恢复通过。
3. G2:长期ARI连接、主线路通话、节点选择和安全FALLBACK;迟到接通不产生双拨。
4. G3:AI、最终文字、录音及MQ回传;SaaS入库展示和补传可验证。
5. G4:第三部分全部阻断用例通过;部署与回滚演练完成。
本人负责中间件/ARI/回传核心实现;BgA对接SaaS任务快照和控制;BgB对接SaaS MQ生产消费、事件入库与资产接口;FeA负责展示和播放;部署负责人支持环境与监控。若本人兼任ArchA,合并人力日历,不重复计算。
@@ -0,0 +1,257 @@
# 第二部分:Asterisk 部署与指定 SIP 对接步骤
## 1. 参考基线与使用边界
参考仓库:`git@git.ipao.vip:rogee/sip-research.git`
已审阅提交:`6a8064e53bb7eadd73f03373524953e68330976c`
主要依据:`asterisk/README.md``asterisk/deployment.md``asterisk/deploy/docker-compose.yml``conf/{ari,http,pjsip,rtp}.conf``scripts/{call,health}.sh``mock/{mock-agent,mock-provider}.py`
本次只静态检查参考实现,未连接仓库文档中的历史服务器,未停用其他语音系统,未执行任何远程部署。仓库历史测试记录不能作为本次环境的验收结果。
| 仓库已有内容 | 可以参考的部分 | 本期仍需补齐/纠正 |
| --- | --- | --- |
| 双Asterisk节点+Mock Provider | ARI与externalMedia的最小通话路径;节点由发起方选择 | 没有中间调度件、RabbitMQ、SaaS事件回传和执行持久化 |
| Asterisk镜像latest;历史记录22.10.1 | 容器部署结构 | latest不可复现;核实实际版本及安全状态后固定镜像digest |
| 8088/8089 HTTP映射 | 宿主机访问两节点ARI | 不能对公网无保护开放;Mock Compose没有真实SIP/RTP外网接入配置 |
| mock-agent | 创建桥、externalMedia、PJSIP通道、双向RTP和资源清理示例 | 不是生产事件状态机/AI组件;会话身份、并发、取消和恢复需重新实现 |
| mock-provider | SIP UAS应答及模拟音频 | 单RTP端口、按来源地址处理媒体等简化,不能证明真实多呼叫音频隔离 |
| health.sh | ARI、OPTIONS、通道查询思路 | 会跳过未启动节点;即使缺少必需节点也可能未增加fail,必须增加预期服务集合检查 |
| rtp.conf | 1000010800端口段 | 约200路只是历史估算,不是实测容量保证;strictrtp=no须安全复核 |
| 无录音持久卷 | 无 | 补齐录音落盘、读取、上传、失败保留和清理 |
不得复用示例ARI密码为生产凭据。不要复制仓库的历史root登录目标、停机命令或系统调优作为当前部署授权。特别是其大额UDP内存参数必须按实际主机重新评估,初次部署先保留操作系统默认值。
## 2. D0:环境与参数准备
### 2.1 环境要求
- 隔离的Mock测试主机;Linux、Docker Engine、Docker Compose插件、Git、Bash、curl、Python3可用。
- 生产建议两个独立Linux主机分别部署Asterisk A/B,同一主机只运行一个host-network Asterisk实例。
- 可用SaaS测试租户、RabbitMQ权限、主/备SIP接入、授权测试号码、AI接口、对象存储和中间件构建产物。
- 未完成中间件开发时,只能执行Mock部署及底座验证,不得标记完成MQ回传或一期上线。
### 2.2 上线前必须填写的参数
| 参数组 | 必填内容 |
| --- | --- |
| 版本 | Git提交、Asterisk镜像digest/实际版本、Python测试镜像digest、中间件版本、配置版本 |
| 节点 | node_id、管理网IP、SIP监听地址/端口、RTP地址/端口段、故障域、并发/CPS限额 |
| SIP | 主/备用endpoint名称、地址/端口、注册或IP鉴权、主叫/被叫格式、编解码、源IP清单、失败码 |
| 媒体 | externalMedia可达地址、每通电话端口分配方式、RTP回程、采样率、AI流式接口与配额 |
| MQ/API | VHost/队列、账号ACL/TLS、控制/查询地址、SaaS上传和接收状态查询协议 |
| 存储 | 本地录音路径/权限/配额、上传允许域名、保留期限、磁盘水位、播放授权 |
| 运行 | 允许呼叫时段、紧急停止方式、告警联系人、验收指标和发布窗口 |
全部凭据通过密钥管理或受控文件注入,不进入Git、MQ明文业务payload或工单日志。目录/文件权限需与容器实际UID核对,不能以chmod 777解决写入问题。
## 3. D1:复现仓库的隔离Mock环境
以下命令仅供操作人员在明确指定的测试主机执行;本次编写文档没有执行它们。所有步骤失败即停止,不自动清理现有容器。
### 3.1 固定源代码版本并复制部署目录
```bash
set -euo pipefail
export SRC_DIR="$HOME/sip-research-reference"
export MOCK_DIR="$HOME/asterisk-mock-validation"
test ! -e "$SRC_DIR"
test ! -e "$MOCK_DIR"
git clone --no-checkout git@git.ipao.vip:rogee/sip-research.git "$SRC_DIR"
git -C "$SRC_DIR" checkout --detach 6a8064e53bb7eadd73f03373524953e68330976c
mkdir -p "$MOCK_DIR"
cp -a "$SRC_DIR/asterisk/deploy/." "$MOCK_DIR/"
git -C "$SRC_DIR" rev-parse HEAD
```
在副本中完成以下修改并保存差异:
1. 将asterisk1/asterisk2的image固定到批准的同一digestCompose和scripts/call.sh中使用的Python测试镜像也固定到同一批准digest。镜像实际Asterisk版本必须记录,不把历史22.10.1等同于当前镜像版本。
2. 把ARI端口映射分别限制为`127.0.0.1:8088:8088``127.0.0.1:8089:8088`;SIP/媒体保持隔离Docker网络内。
3. 保留Compose项目名`asterisk-ari`和网络名,仓库call.sh引用了`asterisk-ari_astari`。若改名必须同步脚本,不能只改Compose。
4. `ast1``ast2``ast-mock-provider`名称冲突时改用另一测试环境;禁止删除不属于本次实验的同名容器。
5. 测试凭据仅能用于这一隔离Mock环境;进入共享或生产环境前替换,并同步健康检查/客户端读取方式。
### 3.2 启动并核查必须存在的服务
```bash
set -euo pipefail
cd "${MOCK_DIR:?先执行3.1并设置MOCK_DIR}"
docker version
docker compose version
docker compose --profile scale config --services
# 完成上述镜像/端口检查后才执行:
docker compose --profile scale up -d
for name in ast1 ast2 ast-mock-provider; do
test "$(docker inspect -f '{{.State.Running}}' "$name")" = "true"
done
docker exec ast1 asterisk -rx 'core show version'
docker exec ast2 asterisk -rx 'core show version'
docker exec ast1 asterisk -rx 'pjsip show contacts'
docker exec ast2 asterisk -rx 'pjsip show contacts'
bash scripts/health.sh
```
通过条件:三容器均运行、两节点实际版本已登记、ARI鉴权查询成功、Mock contact可用。health.sh的`fail=0`只能作为辅助证据,不能替代上述必需服务检查。若实际SIP不支持OPTIONS,生产探活需改为经验证的方式,不据此误摘除线路。
### 3.3 顺序验证两节点通话
```bash
set -euo pipefail
cd "${MOCK_DIR:?先设置MOCK_DIR}"
mkdir -p evidence
# 1001只用于仓库mock-trunk,不得将此脚本直接指向真实线路。
bash scripts/call.sh 1001 15 1 | tee evidence/mock-node1.log
bash scripts/call.sh 1001 15 2 | tee evidence/mock-node2.log
docker logs --since 10m ast-mock-provider > evidence/mock-provider.log 2>&1
docker exec ast1 asterisk -rx 'core show channels concise'
docker exec ast2 asterisk -rx 'core show channels concise'
```
通过条件:两次均出现通话应答及`bidirectional=YES`,有双向RTP计数,挂断后本次通话资源清理。脚本即使打印`bidirectional=NO`也不一定返回非零,验收必须检查结果内容,不能仅看退出码。
Mock多实例、固定端口和媒体关联方式存在简化,顺序成功不证明并发隔离;并发、迟到应答、真实编解码和FALLBACK在后续专项验证。
## 4. D2:真实SIP部署差异
### 4.1 生产网络拓扑
推荐初版:A/B分别运行在独立Linux主机,Asterisk使用host network;中间件通过管理网访问ARI,媒体组件与节点双向可达。这样避免将仓库同一Docker私网中的音频可达性误认为真实外网可达性。
同机运行两个host-network节点会争用5060、8088和RTP端口,不允许直接照搬。若必须同机双节点,需另行设计完整端口/地址及NAT映射,不能只改变ARI端口。
| 流向 | 放通与限制 |
| --- | --- |
| 中间件→ARI | 管理网HTTPS/受控代理或批准的私网链路;仅允许控制者,禁止公开8088 |
| Asterisk↔指定SIP | 供应商约定SIP协议/端口及源IP;不能只假设UDP5060 |
| Asterisk↔供应商媒体 | 协商的RTP/RTCP范围及来源;NAT需正确宣告外部地址 |
| Asterisk↔媒体组件 | 为并发会话分配独立端口/关联,双向路由和防火墙明确 |
| 中间件↔MQ/数据库/SaaS/存储 | 按最小权限开放;MQ管理端不向业务公网暴露 |
不要清空防火墙或覆盖所有sysctl。RTP端口预算需计入PJSIP、externalMedia和实际RTCP使用;不能仅根据端口数量承诺业务并发。
### 4.2 单节点Compose模板
这是**本期新增模板,不是参考仓库原有文件**。在当前节点的DEPLOY_DIR保存为docker-compose.yml,并准备conf目录中列出的配置文件、批准镜像及权限后再启动;不包含Mock Provider。
```yaml
name: ai-outbound-node
services:
asterisk:
image: ${ASTERISK_IMAGE:?必须提供批准的镜像digest}
network_mode: host
restart: unless-stopped
stop_grace_period: 60s
volumes:
- ./conf/http.conf:/etc/asterisk/http.conf:ro
- ./conf/ari.conf:/etc/asterisk/ari.conf:ro
- ./conf/pjsip.conf:/etc/asterisk/pjsip.conf:ro
- ./conf/rtp.conf:/etc/asterisk/rtp.conf:ro
- ./conf/extensions.conf:/etc/asterisk/extensions.conf:ro
- recordings:/var/spool/asterisk/recording
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
volumes:
recordings: {}
```
启动前通过`core show settings`和镜像说明核对实际录音目录/运行UID;若路径不同,同步修改挂载和读取方式。A/B各有本地录音卷,不是共享存储。宿主机永久损坏可能导致尚未上传的录音丢失;若业务要求该场景零丢失,必须另配可恢复存储并验收,不能由双Asterisk节点自动保证。
### 4.3 必改配置清单
| 文件/配置 | 必须处理的内容 |
| --- | --- |
| http.conf | 启用ARI所需HTTP;同机访问可绑定回环,异机绑定管理网/受控代理;不复制0.0.0.0公网暴露 |
| ari.conf | 新建独立服务账户、强密钥、限制CORS及访问来源;运行凭据不复用仓库样例 |
| pjsip.conf transport | 按供应商UDP/TCP/TLS要求、监听地址/端口、local_net和外部信令/媒体地址配置 |
| pjsip.conf endpoint/AOR | 主、备用endpoint分别配置;endpoint_id与中间件trunk_id一一对应;明确主叫/号码格式、编解码、媒体路由 |
| SIP鉴权 | IP鉴权与注册/Digest鉴权分别配置;需要注册时补auth/registration,不能只修改contact |
| 线路探活 | OPTIONS或供应商认可的探活;容量、CPS、失败码和冷却策略不能仅靠Avail决定 |
| rtp.conf | 显式端口段、来源限制;strictrtp是否开启需真实线路/媒体验证,不默认长期关闭 |
| extensions.conf | 本期新增只拒绝非预期呼入的上下文;出站endpoint引用该受限context,不开放公共拨号入口 |
| 录音卷 | 节点独立持久卷、实际UID权限、配额/清理/上传期限;不能只写容器可写层 |
本期只做外呼,无需为了该方案提前引入Kamailio、呼入路由或共享SIP注册体系。指定SIP的名称、注册信息和号码策略未提供前,不能生成已经可用于真实拨号的生产pjsip.conf。
## 5. D3:部署节点并核实ARI能力
以下在各自节点的部署目录执行,镜像变量应为`andrius/asterisk@sha256:...`或已批准的等价镜像,不接受latest。
```bash
set -euo pipefail
: "${DEPLOY_DIR:?设置当前节点的独立部署目录}"
: "${ASTERISK_IMAGE:?设置已批准的镜像digest}"
cd "$DEPLOY_DIR"
case "$ASTERISK_IMAGE" in *@sha256:*) ;; *) echo '必须固定镜像digest' >&2; exit 1;; esac
for f in http.conf ari.conf pjsip.conf rtp.conf extensions.conf; do
test -f "conf/$f"
done
docker compose config --services
docker compose pull asterisk
docker compose up -d asterisk
docker compose exec -T asterisk asterisk -rx 'core show version'
docker compose exec -T asterisk asterisk -rx 'core show settings'
docker compose exec -T asterisk asterisk -rx 'module show like res_ari'
docker compose exec -T asterisk asterisk -rx 'module show like chan_rtp'
docker compose exec -T asterisk asterisk -rx 'pjsip show endpoints'
docker compose exec -T asterisk asterisk -rx 'pjsip show contacts'
docker compose exec -T asterisk asterisk -rx 'pjsip show registrations'
```
`pjsip show registrations`仅对注册型接入要求成功;IP鉴权没有注册条目不算失败。核对bridges/channels/recordings等ARI资源、RTP通道及WAV格式支持,缺失时停止联调,不假定仓库挂载了modules.conf或自定义Dockerfile——它们在参考路径中不存在。
管理网接口检查使用权限为600的netrc/受控凭据文件,禁止把密码放在URL、命令历史或截图中:
```bash
set -euo pipefail
: "${ARI_BASE:?例如批准的管理网HTTPS地址,不含/ari}"
: "${ARI_NETRC:?设置受控netrc文件路径}"
test -f "$ARI_NETRC"
curl --fail --silent --show-error --netrc-file "$ARI_NETRC" \
"$ARI_BASE/ari/asterisk/info"
curl --fail --silent --show-error --netrc-file "$ARI_NETRC" \
"$ARI_BASE/ari/channels"
curl --fail --silent --show-error --netrc-file "$ARI_NETRC" \
"$ARI_BASE/ari/bridges"
```
中间件启动ARI事件连接后再核对应用注册。只有REST返回200而没有有效Stasis事件连接,不能认为应用可用。禁止用忽略证书验证的方式掩盖HTTPS配置错误。
## 6. D4:中间件与MQ部署顺序
参考仓库没有中间件镜像、启动命令、数据库迁移和RabbitMQ工具脚本,因此本节是**待开发服务的发布步骤**,不虚构仓库中不存在的可执行文件。
1. 发布第一部分约定的数据库结构/唯一约束和向后兼容迁移;备份执行/投递数据。
2. SaaS负责人创建/确认命令、事件、重试/DLQ及ACL;核对发布confirm、不可路由检测、持久化和消费ACK。
3. 配置节点表、ARI凭据引用、主备线路组、媒体地址池、租户授权、并发/CPS、有效期及磁盘阈值。
4. 配置SaaS当前业务许可查询、上传授权/完成确认、接收状态查询接口及允许的存储域名;不配置HTTP业务回调地址。
5. 先启动SaaS事件消费者,再启动中间件的查询/恢复/投递能力;消费执行命令暂不开启。
6. 中间件核对未决attempt、存量通道和未投递事件,建立每节点唯一ARI控制连接;健康通过后开放低并发执行。
7. 使用授权测试租户发布一条call.execute;核对command.result、call.status、transcript.final、call.finished、recording.ready到SaaS入库和页面的完整关联。
8. 执行第三部分故障/恢复用例,全部阻断项通过后才进入灰度。客户端SDK/框架选择不改变这条契约和门禁。
## 7. D5:录音专项对接
按已部署镜像的ARI接口核验以下能力,不直接将接口存在视为录音可用:
| 顺序 | ARI/存储动作 | 检查点 |
| --- | --- | --- |
| 1 | POST /ari/bridges/{bridgeId}/record | 受控name、format=wav;模块/目录有权限;返回成功并收到开始事件 |
| 2 | POST /ari/recordings/live/{recordingName}/stop | 通话结束时幂等处理;等待录音完成/文件封装,不读取仍在写的文件 |
| 3 | GET /ari/recordings/stored/{recordingName}/file | 获取可播放文件;名称URL编码;跨节点按原node_id读取 |
| 4 | SaaS上传授权→上传→complete | SHA-256、大小、通话归属通过;授权过期可重新申请 |
| 5 | MQ recording.ready→SaaS消费 | 可查询应用结果、租户鉴权播放;重复事件不产生重复资产 |
| 6 | 清理暂存 | SaaS已应用且达到批准保留策略后清理;未确认的失败文件告警,不静默删除 |
需要双声道、全程振铃录音、严格无录音损失或特殊合规策略时,单独确认;本模板仅以接通后单轨混音作为一期基线。
## 8. 灰度、回滚与运维交接
- 顺序:单节点单路→两节点顺序→真实多路隔离→已确认目标负载→小流量灰度。Mock成功不能跳过真实SIP/AI阶段。
- 回滚前停止新指令/建立屏障,排空或经授权处置在途电话,记录未决attempt、录音和outbox状态,再回滚中间件或节点镜像。
- 不使用`docker compose down -v`删除录音卷;不把删除数据库/队列当作恢复手段。配置和镜像回滚必须保留事件/执行事实,避免重复拨号。
- 若发现双拨、越权、录音静默丢失或终态回退,立即停止放量;按call_id对账后再恢复。
- 交付记录包括镜像digest、Git/配置版本、网络矩阵、密钥引用、部署命令记录、验收证据目录、故障联系人和批准的容量/恢复边界。
@@ -0,0 +1,140 @@
# 第三部分:联调步骤与验收标准
**当前状态:全部运行用例待执行。** 本次只完成参考代码审查和文档静态校验,没有实际部署、拨打电话或连接生产MQ。不得把参考仓库的历史成功记录填写为本次通过。
## 1. 验收分层与准入
| 层级 | 验证内容 | 不能替代的验证 |
| --- | --- | --- |
| L0 静态检查 | 文档、样例、配置语法、镜像/提交/参数登记、接口评审 | 不证明容器能启动、线路可用或业务可靠 |
| L1 Mock底座 | 两节点运行、ARI、Mock SIP、顺序双向RTP、清理 | 不证明真实AI、录音、MQ回传、并发隔离或生产主备 |
| L2 真实SIP/AI | 授权号码、指定主备接入、媒体、AI、多轮/打断及录音 | 不证明SaaS业务数据一致性和异常恢复 |
| L3 SaaS闭环 | MQ下发/回传、控制应答、最终文字、录音入库/播放、补偿 | 不替代容量、故障注入和安全检查 |
| L4 发布门禁 | 阻断用例、确认后的容量/时延、恢复、回滚、交接 | 全部有证据才能签字发布 |
开始L2前必须取得真实SIP/AI权限及测试号码授权;故障注入仅在隔离环境或批准窗口执行。不得为了验收中断无关服务、清空队列/数据库/录音卷或调整全局防火墙。
## 2. 建议指标与G0确认表
以下是**小规模一期试运行建议值,不是已测结论或合同承诺**。可根据真实供应商及需求在G0修改;正式验收单不得保留“待定”。
| 指标 | 建议初始目标 | 测量边界/样本 |
| --- | --- | --- |
| 真实并发/CPS | 目标10路、全局1 CPS;先1路再3路再10路,且不超过供应商限额 | 在确认后的目标负载下持续30分钟,覆盖两节点;SIP重传不算新增呼叫 |
| 控制/状态查询 | 查询P95≤500ms;控制APPLIED P95≤2s | 从SaaS请求到结果;控制时间包含发起屏障处理,不能只量HTTP202 |
| MQ命令受理 | P95≤2s | SaaS持久发布到中间件持久接收;等待拨号另计 |
| AI首音频响应 | P95≤1500ms | 客户语音结束被VAD确认到首个有效TTS音频包发往通话桥;不少于100个有效轮次 |
| 打断 | P95≤500ms | VAD确认插话到旧TTS停止向桥发送;另抽样核查客户侧体验 |
| 最终文字回传 | P95≤3s | 最终稿产生到SaaS业务库应用并可查询;UI刷新延迟另记 |
| 录音就绪 | 测试通话≤180s时,挂断后120s内SaaS可鉴权播放 | 包含录音封装、上传、校验、MQ消费;超大文件单独测 |
| 中断恢复 | SaaS消费/上传中断5分钟,恢复后10分钟内补齐本轮关键数据 | 固定目标负载、明确数据量和可用带宽;不允许通过删除积压通过 |
| 资源清理 | 正常结束后30s内释放本次通道/桥/媒体会话 | 录音可保留;异常节点须恢复后完成核对和清理 |
| 正确性底线 | 重复业务拨号0、跨租户访问0、终态非法回退0、静默丢失最终文字/录音0 | 对本轮全部execution/call/event核对;未决事实必须明确暴露 |
并发、CPS、阈值、事件/文件保留期限、磁盘水位、最长可恢复中断由本人、SaaS和运维共同签字。若本地录音尚未上传时宿主机永久丢失,必须如实记录无法恢复的边界,不虚称双节点能够保证录音零丢失。
## 3. 联调步骤与双方产出
| 步骤 | 操作与主责 | 本步产出 |
| --- | --- | --- |
| T0 契约冻结 | 本人提供消息/状态/错误码;BgA/BgB确认SaaS接口和MQ映射;运维确认网络/密钥 | 版本化契约、示例、环境参数和G0签字 |
| T1 SaaS-MQ空链路 | SaaS发送测试指令;中间件暂不拨真实电话,只校验/受理并MQ应答;SaaS消费 | command_id全链路记录,ACK与业务结果区分正确 |
| T2 Mock底座 | 运维按第二部分部署;本人分别在A/B执行顺序Mock通话 | 容器/ARI/PJSIP、双向RTP和清理证据;标记为L1 |
| T3 真实单路 | 本人驱动指定SIP和AI链路;SaaS下发授权号码/快照 | 正常通话、多轮/打断、状态和原因映射 |
| T4 数据回传 | BgB接收MQ事件及资产;FeA展示文字/录音/状态 | event_id→入库→页面可追溯,重复消费无重复内容 |
| T5 故障与对账 | 本人与运维注入MQ/ARI/SIP/上传故障;SaaS参与恢复 | 每项预期、实际、时间、原因、attempt/event/文件证据 |
| T6 容量与灰度 | 按G0目标压测,完成发布/回滚演练 | 指标报告、遗留问题及L4签字结论 |
“空链路”是专用测试模式,必须不能触发真实SIP;不能靠使用假号码来证明不拨号。正常SaaS调度与授权检查在L2/L3必须启用。
## 4. 阻断验收用例
下列所有用例初始状态均为“待执行”。本轮环境中任一项失败或缺证据,不得签署一期生产验收。
### 4.1 部署与接入
| 编号/场景 | 操作步骤 | 通过标准/证据 |
| --- | --- | --- |
| DEP-01 必需服务 | Mock层检查A/B及Mock服务,真实层检查批准的运行服务集合(不含Mock);停止一个必需服务后重做检查 | 缺少必需服务必须判失败,不能因health.sh输出SKIP而整体通过;保存服务清单 |
| DEP-02 ARI与Stasis | 检查鉴权REST和节点应用事件连接;断开WebSocket但保留REST | 控制者发现断连并停止不安全的新执行;REST200不能掩盖应用不可用 |
| DEP-03 网络/鉴权 | 用授权与未授权来源访问ARI/MQ;验证真实SIP/媒体双向路由 | 未授权访问被拒绝;无公网裸ARI;没有以关闭防火墙/证书校验取巧 |
| DEP-04 录音持久化 | 生成录音后重启/重建当前测试节点容器,保留卷 | 已完成本地录音仍可读取和校验;容器可写层不能成为唯一存储 |
### 4.2 命令与MQ可靠性
| 编号/场景 | 操作步骤 | 通过标准/证据 |
| --- | --- | --- |
| MQ-01 正常闭环 | 发布一条合法执行;完成电话及SaaS事件入库 | command、execution、call、attempt、event关联一致;ACCEPTED不被显示为已接通 |
| MQ-02 重复命令 | 同一command_id/execution_id重发10次;关闭FALLBACK以隔离变量 | 只产生一次业务拨号;SIP同一INVITE的协议重传不计为额外执行 |
| MQ-03 同键异内容 | 同租户同command_id改变号码/快照;另保持原凭证篡改tenant_id后发布 | 冲突/越权被拒绝,原命令不被覆盖;不产生第二次拨号;合法不同租户的命令空间不冲突 |
| MQ-04 持久化/ACK竞态 | 在命令提交前、提交后ACK前、ACK后分别终止中间件并恢复 | 未提交可重投,已提交能恢复;无丢失受理事实和重复业务拨号 |
| MQ-05 发布确认竞态 | 断开网络或让交换机无有效绑定;在事件提交与confirm之间停止进程 | 事件保留并重试;不可路由不能标记APPLIED;恢复后SaaS只有一次业务应用 |
| MQ-06 SaaS提交/ACK竞态 | 在event_inbox业务事务提交前后分别中断消费者 | 失败事务不ACK;重投事件幂等;最终文字/状态不重复、不缺失 |
| MQ-07 数据库失败 | 中间件接收时或SaaS应用事件时使数据库暂时不可用 | 不提前ACK/返回成功;不继续发起缺少持久执行意图的电话;恢复可对账 |
| MQ-08 容量与停止 | 积压多条指令,执行PAUSE/STOP;等APPLIED后重放旧指令及过期指令 | 生效后不再新发起,旧revision/过期执行被拒绝;无无限requeue热循环 |
| MQ-09 业务有效性 | 已排队号码被加入拒绝再联系/禁止时段,或授权被撤销后再执行 | SaaS业务许可重新校验不通过则不拨号;不得因早先发布成功绕过当前策略 |
### 4.3 Asterisk、SIP与FALLBACK
| 编号/场景 | 操作步骤 | 通过标准/证据 |
| --- | --- | --- |
| SIP-01 正常/无振铃事件 | 正常通话;让有效ANSWERED到达而无RINGING观测 | 可以直接从DIALING收敛到ANSWERED;最终时间和原因正确 |
| SIP-02 合法主备 | 主线路在明确未接通时发生已批准线路故障;确认旧尝试结束后切备用 | 同一call/execution下新attempt,最多达到配置总预算;不同时保留两条活动呼叫 |
| SIP-03 禁止误切 | 分别返回busy、reject、invalid;另在已接通后制造AI/媒体故障 | 这些场景不触发未经授权的SIP备用重拨;原因分类准确 |
| SIP-04 迟到接通/ARI超时 | 原尝试已提交,阻断控制事件,再让接通迟到;同时触发超时处理 | 进入对账,不因“未收到接通”立即备用拨号;没有双通;完整记录时序 |
| SIP-05 节点故障 | 分别在提交originate前、提交后事实不明、已接通后中断节点 | 前者可选健康节点;不明状态不盲重拨;已接通中断如实结束而非声称无损迁移 |
| SIP-06 并发隔离 | 多通话播放不同标识音/不同测试词并分布两节点;抓取允许范围内的媒体关联 | 每call的通道、端口、文字、录音不串线;不能只比较总RTP包数 |
| SIP-07 清理与重启 | 正常结束、客户挂断、振铃超时及中间件重启后检查资源 | 按call_id清理且不伤其他通话;残留有补偿和告警,达到清理时限 |
### 4.4 AI、文字、录音与安全
| 编号/场景 | 操作步骤 | 通过标准/证据 |
| --- | --- | --- |
| DATA-01 AI多轮/打断 | 至少100个有效轮次,覆盖播音时插话、静音和AI超时 | 达到确认的延迟基线;取消旧生成/播音;AI故障不无限阻塞电话 |
| DATA-02 文字版本 | 按乱序/重复方式回传中间稿和最终稿,再重放最终事件 | 同轮按版本收敛;最终文字完整;未播出的AI文本有正确标记 |
| DATA-03 录音闭环 | 完成电话→录音封装→上传校验→MQ ready→SaaS播放 | checksum/大小/时长可核对,事件在上传确认后产生;页面不先显示假成功 |
| DATA-04 上传故障 | 模拟上传中断、授权过期、checksum不符、完成确认丢失 | 授权/上传可重试且不产生重复资产;不提前ready;通话终态仍可展示 |
| DATA-05 SaaS中断 | 暂停SaaS消费及上传5分钟再恢复;模拟接近磁盘上限 | 音频不等待回传;有积压/磁盘保护;恢复后在目标时限补齐,不以删数据通过 |
| DATA-06 数据补传 | 按call_id触发事件重放;核对原command/attempt数量 | 仅补数据,不重新拨号;SaaS幂等消费,APPLIED状态能查询 |
| SEC-01 租户隔离 | 两租户相同业务ID或交叉引用call/recording,尝试查询、补传、播放 | 跨租户被拒绝;事件不能覆盖别的租户;播放权限独立验证 |
| SEC-02 输入/凭据 | 提交恶意dial string/媒体地址/超大消息,检查日志与签名URL | 不支持任意ARI调用/多目标注入;大小受限;密码、敏感号码和有效签名不泄露 |
| OPS-01 压测/回滚 | 按目标并发/CPS持续30分钟;停止接单并回滚版本,再恢复 | 不超限、无双拨/串线/终态回退;数据与录音卷保留;待投递事件可恢复 |
## 5. 证据、结果与签字模板
每个用例至少关联一组可定位证据;业务统计不能仅依赖截图。
- 版本:参考提交、实际镜像digest/Asterisk版本、中间件版本、SaaS版本、配置版本。
- 环境:node_id、网络路径、MQ策略、租户、测试号码授权、目标并发/CPS、时间同步情况。
- 执行:command_id、execution_id、call_id、attempt_id、event_id、状态版本、时间点与失败原因。
- 呼叫:ARI事件/查询、受控SIP信令证据、媒体会话关联、清理结果;SIP重传与真正新dialog区分。
- 数据:SaaS event_inbox应用记录、任务/通话结果、最终文字、录音checksum和受控播放结果。
- 恢复:注入故障、恢复动作、积压清空时间、残留/未决记录、异常告警及处置。
- 文件:按脱敏证据目录归档,凭据和客户数据不直接打包公开。
单项记录模板:
| 字段 | 填写内容 |
| --- | --- |
| 用例ID/环境/日期/执行人 | 待执行 |
| 前置条件与版本 | 待填写 |
| 输入与关联ID | 待填写 |
| 故障注入/执行动作 | 待填写 |
| 预期结果 | 引用本节对应标准 |
| 实际结果与指标 | 待填写,不能填参考仓库历史结果 |
| 证据路径与缺陷号 | 待填写 |
| 结论与复核人 | 待执行 / 通过 / 失败 / 阻塞;不得以空白代表通过 |
发布签字由本人(中间件/ARI)、SaaS后端(指令与事件)、FeA(展示/播放)、运维(部署/恢复)及业务验收人完成。角色重合时注明,不增加隐含人力。
## 6. 一期最终完成定义
只有以下条件全部满足,才能把“设计完成”升级为“实施验收完成”:
1. 文档契约、实际Schema/API和实现版本一致,G0参数无未确认阻断项。
2. 必需节点、真实SIP、AI、中间件、MQ及SaaS资产链路均通过相应层级,不以Mock替代。
3. 本文全部阻断用例有通过证据,确认后的指标达标;双拨、串线、越权、静默数据丢失为零。
4. 配置/凭据/卷/监控/告警/补偿/保留与回滚操作已交接;已知单实例及本地录音失效边界被接受或另行修复。
5. 一期没有计费入口或扣费行为,现有SaaS业务数据未被重复建设或破坏。
本次文档交付只完成L0中的参考审查与文档校验;L1–L4均待实际环境执行和签字。
@@ -0,0 +1,18 @@
# 一期中间调度件、Asterisk部署与验收文档
## 文档入口
- [合并版 Word 文档](一期中间调度件_Asterisk部署及验收_v1.0.docx)
- [01 中间调度件与MQ回传设计](01_中间调度件与MQ回传设计.md):职责、数据模型、消息和接口、幂等、控制屏障、FALLBACK、文字/录音回传。
- [02 Asterisk部署与SIP对接步骤](02_Asterisk部署与SIP对接步骤.md):固定参考版本、隔离Mock步骤、真实部署差异、节点模板、配置清单、录音及回滚。
- [03 联调步骤与验收标准](03_联调步骤与验收标准.md):L0–L4分层、量化指标建议、29项阻断用例、证据和签字模板。
## 已确认与待执行
用户确认采用**MQ下发指令、MQ回传事件**;HTTP只用于控制、查询和录音上传相关交互。本套文档优先于旧开发计划中的HTTP业务回调草案。
参考仓库:`git@git.ipao.vip:rogee/sip-research.git`;固定审阅提交:`6a8064e53bb7eadd73f03373524953e68330976c`
本次只审查参考代码并生成文档,没有远程部署、停用其他服务、拨打电话或修改现有Excel。仓库只有Mock通话底座,不能据此认定中间件、真实AI、MQ回传或录音链路已实现。
执行前补齐指定SIP主备参数、SaaS/MQ契约、镜像digest、部署地址、AI/存储凭据和验收基线。所有运行用例目前为**待执行**,文档语法校验不等于业务验收通过。
+126
View File
@@ -0,0 +1,126 @@
# 系统架构图(规划态)
**版本:** v0.1
**依据:** [一期计划 v1.3](一期呼出应用开发计划_v1.0.md)、[OpenAPI 与 MQ 契约规划 v0.3](SaaS交互_OpenAPI与MQ契约规划_v0.1.md)。
**状态:** 目标逻辑架构,尚未完整实现;框图不表示已部署,也不指定机器数、数据库产品或独立微服务数量。当前仅有 ASR 验证基础,完整 AI 与生产容量均未验收。
```mermaid
flowchart LR
subgraph SAAS["现有 SaaS:业务主数据与授权"]
UI["租户用户/管理页面"]
BUSINESS["任务、号码、配置版本、业务授权<br/>基础频控/拒绝再联系/重新外呼决策"]
SDB[("SaaS 持久存储<br/>业务主数据、发布记录、inbox、资产关联")]
PUBLISH["可信发布服务<br/>tenant_id 受控映射/限速/原 ID 重试"]
CONSUME["事件消费者<br/>租户校验/event_id 去重/版本合并<br/>业务与 inbox 同事务后 ACK"]
STORAGE["SaaS 存储接口<br/>上传授权、实际校验、确认 oss_id<br/>按租户/用户提供鉴权播放"]
UI --> BUSINESS
BUSINESS --> SDB
SDB --> PUBLISH
CONSUME --> SDB
STORAGE --> SDB
SDB --> UI
end
subgraph BROKER["RabbitMQ:可靠传输,不是活动通话唯一状态源"]
CMDX["命令 Exchange<br/>call.execute"]
QA["租户 A 命令队列"]
QB["租户 B 命令队列"]
QN["租户 N 命令队列"]
EVX["事件 Exchange"]
EVQ["SaaS 事件队列<br/>本轮不按租户拆分"]
DLQ["重试/死信与隔离<br/>受控恢复、不得绕过租户域或配额"]
CMDX --> QA & QB & QN
EVX --> EVQ
QA & QB & QN -. "异常隔离" .-> DLQ
end
PUBLISH -- "AMQP:持久消息、confirm + mandatory" --> CMDX
EVQ --> CONSUME
subgraph APP["呼出应用:逻辑组件,部署粒度待实施"]
API["HTTP 控制/查询/历史事件补传<br/>不提供创建通话或重新拨号接口"]
FAIR["租户公平调度器,可多实例<br/>有界轮转、预取和持久待发起窗口"]
ADMIT["原子资源准入与路由<br/>租户并发/CPS + 供应商并发/CPS<br/>Cell/端口/出口健康 + AI 配额"]
STATE[("可靠持久存储/协调<br/>命令与执行去重、控制屏障、发起意图<br/>调度所有权、额度与租约、事件与 outbox")]
OUTBOX["outbox 投递器<br/>所有业务结果经 MQ<br/>有限重试/原事件补传"]
ASSET["资产处理逻辑<br/>录音封口/有界暂存/上传与恢复<br/>可与 Cell 共置,不假设额外文件传输服务"]
FAIR --> ADMIT
FAIR <-->|"先持久化后 ACK"| STATE
ADMIT <-->|"所有权、原子额度与隔离令牌"| STATE
API <-->|"控制屏障/查询快照/原事件"| STATE
STATE --> OUTBOX
ASSET <-->|"资产状态 + outbox 同事务"| STATE
end
QA & QB & QN --> FAIR
DLQ -. "合法命令恢复回原租户调度域" .-> FAIR
BUSINESS -. "HTTPS:控制/查询/补传" .-> API
OUTBOX -- "AMQP:状态、文字、终态、录音元数据" --> EVX
subgraph CELLS["语音 Cell 池:多机器 + 多 EIP 直连"]
subgraph CELL1["Cell 1 · 固定出口"]
EXEC1["本地执行器<br/>真实发起前复核屏障/期限/租约"]
AST1["Asterisk<br/>预配置授权 trunk/通道/桥"]
MEDIA1["媒体与会话适配<br/>流式音频、打断取消、录音暂存"]
IP1["固定 EIP 1<br/>独立白名单/RTP 端口/健康"]
EXEC1 <-->|"ARI 控制/事件"| AST1
AST1 <-->|"实时媒体"| MEDIA1
AST1 <-->|"SIP / RTP"| IP1
end
subgraph CELLN["Cell N · 同构示意,非确定机器数"]
STACKN["本地执行器 + Asterisk + 媒体适配<br/>独立端口、容量、租约与健康"]
IPN["固定 EIP N<br/>独立供应商白名单"]
STACKN <-->|"SIP / RTP"| IPN
end
end
ADMIT -- "trunk_id + egress_pool_id + cell_id" --> EXEC1
ADMIT -- "同规则选择其他 Cell" --> STACKN
EXEC1 <-->|"执行事实/心跳/对账"| STATE
STACKN <-->|"执行事实/心跳/对账"| STATE
API -. "跨 Cell 控制屏障/在途许可收敛" .-> EXEC1
API -. "跨 Cell 控制屏障/在途许可收敛" .-> STACKN
MEDIA1 -. "本 Cell 已封口录音" .-> ASSET
STACKN -. "本 Cell 已封口录音" .-> ASSET
subgraph TELEPHONY["外部电话网络"]
SIP["已预接入并授权的 SIP 供应商<br/>逐呼应用本线路主叫/被叫规则<br/>支持单线路;无备用不虚构"]
CALLEE["被叫电话"]
SIP <-->|"电话接续/语音"| CALLEE
end
IP1 <-->|"SIP / RTP 直连"| SIP
IPN <-->|"SIP / RTP 直连"| SIP
subgraph AI["外部 AI 服务:目标链路,协议与配额待冻结"]
ASR["ASR<br/>仅有独立验证基础"]
LLM["LLM<br/>未启用,待新规范"]
TTS["TTS<br/>未启用,待新规范"]
ASR -->|"识别文本,经会话逻辑编排"| LLM
LLM -->|"回复文本,经会话逻辑编排"| TTS
end
MEDIA1 -- "客户流式音频" --> ASR
TTS -- "生成语音" --> MEDIA1
STACKN <-->|"同构实时 AI 会话,非 MQ 音频传输"| AI
MEDIA1 -. "文字/播放证据等执行事实" .-> STATE
OSS[("OSS<br/>持久录音对象,不经 MQ 传文件")]
ASSET -. "① HTTPS 申请授权;③ complete 并取得 oss_id" .-> STORAGE
ASSET -- "② 签名 HTTPS 上传文件" --> OSS
STORAGE <-->|"独立验证对象、大小与校验值"| OSS
classDef planned fill:#eef4ff,stroke:#4263a6,color:#172a46;
classDef pending fill:#fff4dc,stroke:#b7791f,color:#573b13;
classDef data fill:#eaf7f1,stroke:#2d8063,color:#164b39;
classDef endpoint fill:#f0edff,stroke:#7560a0,color:#3c2f5c;
class API,FAIR,ADMIT,EXEC1,AST1,MEDIA1,STACKN,OUTBOX,ASSET planned;
class ASR,LLM,TTS pending;
class SDB,STATE,OSS,EVX,EVQ,QA,QB,QN data;
class IP1,IPN,SIP,CALLEE endpoint;
```
## 架构约束
1. **业务与媒体分离:** RabbitMQ 是唯一拨号指令入口、全部业务结果回传通道;不承载实时音频。HTTP 仅控制、查询、补传及存储握手,不做业务结果回调。AI 箭头表达经 Cell 会话逻辑编排的数据顺序,不要求供应商服务互相直连。
2. **可靠性与公平:** 多调度实例共享持久去重、额度及所有权协调。租约失效停止新任务;未知活动通话须对账,不直接释放占用。队列满明确背压,SaaS 保留原 ID;恢复仍回原租户调度域。独立队列不等于独享 broker,也不保证固定开始时限。
3. **固定路由:** 一通电话及合法 FALLBACK 始终固定 Cell/出口;trunk 预接入,不逐呼改写共享 SIP 配置。不建设单 EIP + NAT,不自动迁移故障节点的活动通话。当前登记出口 `123.56.71.98` 不等于已确认可操作的 EIP,图中其他出口均为规划资源。
4. **容量是目标而非结论:** 至少 1000 路同时已接通的完整 ASR/LLM/TTS 通话;N+1 或更高冗余,`(Cell 数量 - 1) × 实测单 Cell 安全容量 >= 1000`。拨号、振铃、CPS、AI 配额、媒体端口、带宽与 MQ/OSS 另行计入;未压测不承诺数量或规格。
5. **资产闭环:** ①授权 → ②上传 OSS → ③SaaS 实际校验并确认 oss_id → ④verified 与 outbox 同事务 → ⑤MQ recording.ready → ⑥SaaS 去重关联。未验证不发 ready;失败经 MQ 回传。资产处理框是逻辑能力,不强制把录音跨节点搬到新服务。
服务鉴权、TLS、租户 ACL、密钥注入、监控与积压/磁盘水位保护横跨上述组件;为避免遮挡主链路,不额外画成一套管理平台。各外部依赖和真实测试状态仍以依据文档为准。
+213
View File
@@ -0,0 +1,213 @@
# 部署接入:本轮实现与运行说明
## 1. 实施边界
本轮依据用户选择,**仅复用 voice_test 的 ASR**,没有复制或启用其 LLM/TTS,也没有把原调研页面直接作为生产服务暴露。
| 内容 | 本轮状态 |
| --- | --- |
| ASR Web 服务 | 已实现:访问令牌、Origin校验、服务端模型/凭据选择、16k单声道PCM、识别结果展示 |
| ASR 连接与协议 | 已加固:断开清理、取消、写入/启动时限、最终结果背压、火山帧长度及gzip解压上限 |
| 部署底座 | 已实现:非root/read-only ASR容器、回环端口、Asterisk配置生成和显式启动检查 |
| 阿里云主机准备 | 已实现CLI驱动的只读计划、受控创建竞价实例、复用主机和绑定既有EIP;默认不修改云资源 |
| 云端实际操作 | 未执行;当前本机无aliyun CLI/云凭据,未核实固定IP归属,未创建实例或改绑IP |
| Asterisk真实接入 | 未执行;指定SIP地址/鉴权、VPC网络、镜像digest等仍需填写 |
| LLM/TTS | 未实现、未启用,等待用户新的供应商/协议/参数规范 |
| ARI业务调度、自动FALLBACK、MQ/OSS回传 | 尚未实现;仍按开发计划推进,不能把两个trunk配置当成自动切换代码 |
当前目标是一台北京竞价ECS上的部署底座。ASR测试台与SIP媒体尚未连通;浏览器识别不是电话外呼,也不是已完成SaaS业务验收。
参考:`sip-research@6a8064e53bb7eadd73f03373524953e68330976c`ASR协议源自`voice_test@6772bf4`,仅导入`asr.go/asr_bailian.go/asr_volc.go`后进行加固,不导入原main、配置页、LLM或TTS代码。
## 2. 目录与环境
```text
AGENTS.md 资源约束、安全规则、参考来源
compose.yaml ASR Web容器
compose.asterisk.yaml 独立Asterisk节点容器
.env.example 服务配置占位,不含真实凭据
services/asr-web/ ASR-only Go服务及Web页面
deploy/aliyun_host.py 阿里云CLI只读计划/显式apply
deploy/aliyun.example.json 实例/预算/网络参数占位
deploy/render_asterisk.py 受控配置生成
deploy/asterisk.example.json SIP接入参数占位
deploy/asterisk.sh 默认检查;显式up才启动
tests/ 离线部署逻辑及PCM测试
docs/ 需求、计划和运行文档
.local/ 本机临时状态/验证产物,不提交
```
开发检查使用Go 1.26、Python 3.11+、Node和Docker Engine/Compose。云操作另需阿里云官方CLI及已授权的本地Profile/RAM Role/STS。源码根目录是部署目录,旧Word/Excel/压缩包不随本次代码改动重新生成。
## 3. 固定出口IP与云资源
### 3.1 已知约束
- 区域固定为`cn-beijing`,我方SIP出口白名单公网IP为`123.56.71.98`
- 此IP不是SIP服务商服务器地址,不得用作trunk contact。
- 必须先核实是本账号EIP,还是既有实例的普通公网IP。没有可操作的该IP时停止,不能新分配随机IP替代。
- 创建只使用竞价`SpotWithPriceLimit`、单台实例和明确价格上限,新实例`InternetMaxBandwidthOut=0`,随后绑定已经确认的EIP。
- 已有主机必须有专用project标签,或由用户明确填写`adopt_instance_id`授权复用。未知绑定、非ECS资源、多个候选、库存不完整均停止。
- 既有主机的计费/竞价策略会在plan中展示。非竞价实例不自动转换;若不满足目标,由用户评审迁移,不能擅自停机或创建替代机。
### 3.2 先准备CLI与权限
从阿里云官方CLI发布渠道安装并验证版本,按团队密钥流程配置Profile/RAM Role/STS,不使用未经审计的`curl | bash`。不要把AK/Secret、SSH私钥或会话Token发到聊天中。
最少需要读取EIP和ECS的权限;实际apply还需要RunInstances和AssociateEipAddress权限。VSwitch、安全组、镜像和SSH KeyPair预先存在,脚本不会自动创建全开放安全组。
```bash
# 在项目根目录;已有配置文件不覆盖。
mkdir -p .local
if [ ! -e .local/aliyun.json ]; then
install -m 600 deploy/aliyun.example.json .local/aliyun.json
fi
aliyun --help
python3 deploy/aliyun_host.py --config .local/aliyun.json
```
默认只读查询。第一次必须核对返回的实例ID、IP类型、状态和计费策略。若提示实例不属于项目,先由用户核实实际资源归属,再决定是否填写adopt_instance_id,不能为了绕过校验随便填写。
### 3.3 缺主机时按需创建
创建前填写:`image_id``instance_type``vswitch_id``security_group_id``key_pair_name`、正数`spot_price_limit`、系统盘类型/大小。镜像架构、实例规格与VSwitch所在可用区必须兼容。
价格上限是每小时竞价计算资源上限,币种以账号计费为准;不包括系统盘、EIP、流量等费用。实际费用与创建授权必须由用户确认。
```bash
# 只有确认只读plan、规格、网络和预算后才执行。
python3 deploy/aliyun_host.py --config .local/aliyun.json --apply
```
- 没有可复用实例且固定EIP可用时才创建;RunInstances之前保存ClientToken。
- 创建请求超时后保留同一ClientToken;绑定失败保留已创建实例ID,重试时优先复用,不另造一台。
- 绑定前再次核对EIP;若已经被其他实例占用,停止,不解绑对方。
- 已有Stopped实例不自动启动或替换。实例/EIP未达到确认状态会报错,不宣称部署成功。
- `.local/aliyun-host.json`和锁文件是恢复依据,不能在出错后直接删除来强行重试。多个控制机必须共用明确的操作责任,不能各自用独立状态并行创建。
- 该脚本只准备ECS/EIP,不安装Docker、不上传SSH私钥、不配置DNS/HTTPS、不迁移活动通话。竞价回收后的监控/自动恢复尚未实现。
## 4. ASR Web启动与浏览器验证
### 4.1 配置
```bash
if [ ! -e .env ]; then
install -m 600 .env.example .env
fi
# 在本机生成访问令牌,填入.env;不要贴到聊天或工单。
python3 -c 'import secrets; print(secrets.token_urlsafe(32))'
```
- `ASR_WEB_TOKEN`至少32位URL-safe字符,是本测试台访问令牌,不是供应商API Key。
- `BAILIAN_API_KEY/BAILIAN_WS`用于非fun-*百炼ASR模型;`FUNASR_API_KEY/FUNASR_WS`用于fun-*模型,未配置Fun Key时可回退到百炼Key。
- `BAILIAN_ASR_MODELS`是服务端允许选择的ASR模型列表,默认`fun-asr-realtime`。模型是否支持当前协议需要真实供应商验证,不能把任意LLM模型放入列表。
- 火山使用`VOLC_APP_KEY``VOLC_APP_ID + VOLC_ACCESS_TOKEN`,并配置对应`VOLC_RESOURCE_ID/VOLC_WS`
- 供应商地址只接受WSS,不从浏览器接收任意上游地址或API Key。页面只展示凭据是否配置,不展示密钥。
- 无供应商凭据时服务可以启动,但模型禁用;`/healthz`只说明进程存活,不说明供应商、SIP或MQ可用。
### 4.2 启动
```bash
docker compose config --quiet
docker compose up -d --build
docker compose ps
curl --fail http://127.0.0.1:18088/healthz
```
默认仅在宿主机`127.0.0.1:18088`暴露。容器非root、只读文件系统、去除capabilities,并设置进程/内存上限。不要把测试台直接作为SaaS用户入口;目前是单一运维访问令牌,不是SaaS租户鉴权。
开发者也可从`services/asr-web`运行`go run .`,需提前通过环境设置ASR_WEB_TOKEN和必要上游配置;本地进程默认监听127.0.0.1:8080,不自动读取.env。
### 4.3 远程Web麦克风
优先在批准域名的TLS反向代理后访问,代理保留Host并支持WebSocket`PUBLIC_ORIGIN=https://批准域名`用于精确Origin校验。域名和证书尚未提供,因此本轮没有部署公共HTTPS入口。
临时验证可用SSH隧道:
```bash
: "${DEPLOY_SSH_USER:?设置经批准的部署账户}"
ssh -N -L 18088:127.0.0.1:18088 "${DEPLOY_SSH_USER}@123.56.71.98"
```
浏览器打开`http://localhost:18088`,使用本机安全上下文获取麦克风;如设置PUBLIC_ORIGIN,需要与实际访问的origin一致。不要要求用户禁用Chrome安全设置。
页面流程:填写服务令牌→读取模型→选择有凭据的模型→开始识别→查看中间/最终文字→结束或取消。音频为16k、单声道、PCM16LE、每包100ms;会话最长5分钟、同时最多4个连接。背压过大停止采集,不能无限缓存音频。
LLM/TTS固定显示“未启用/等待新规范”。用户未提供新规范前,不调用旧仓库实现,不提供隐式供应商回退。
## 5. Asterisk配置与启动
### 5.1 配置生成
```bash
mkdir -p .local
if [ ! -e .local/asterisk.json ]; then
install -m 600 deploy/asterisk.example.json .local/asterisk.json
fi
```
填写实际`local_net`、主/备SIP服务器、端口、IP或Digest鉴权方式、用户名及是否注册。当前生成器只提供经过明确限制的UDP/ulaw外呼基线,TCP/TLS及特殊号码/主叫策略需要另行确认。
通过受控进程环境注入:
- `ARI_PASSWORD`:至少32位,不能使用样例密码。
- Digest接入另需`SIP_PRIMARY_PASSWORD``SIP_BACKUP_PASSWORD`;仅在相应接入启用Digest时要求。
- 为防INI注入,换行、分号、方括号等字符会被拒绝;供应商固定密码不满足时需评审正确转义方案,不能删除校验绕过。
```bash
python3 deploy/render_asterisk.py --config .local/asterisk.json
```
生成到`deploy/asterisk/generated/`,文件权限600,目录750;已有目录不会被覆盖。更新时先生成新的版本目录,核对差异、备份旧版本,再由操作人员批准切换。
必须核对镜像中Asterisk的实际UID/GID及配置读取权限,只向必要进程授权,不用chmod 777。ARI默认回环8088;对外信令/媒体地址固定为123.56.71.98。RTP端口段1000010800、strictrtp=yes均需真实线路验证。
### 5.2 镜像与安全组
`.env`中的ASTERISK_IMAGE必须为批准镜像的`@sha256:`引用。参考仓库使用latest、历史记录22.10.1,不代表该镜像当前版本已经被本项目验证;本轮没有拉取或启动真实Asterisk镜像。
确认:SIP服务端IP/协议/端口、RTP回程、EIP/NAT、实际VPC网段、管理来源、录音卷目录和权限。安全组只按来源和用途开放;不公开裸ARI,不清空既有防火墙。
```bash
# 默认为检查,不启动容器。
bash deploy/asterisk.sh
# 参数、权限、源IP和发布窗口确认后,才显式启动。
bash deploy/asterisk.sh up
docker compose -f compose.asterisk.yaml exec -T asterisk \
asterisk -rx 'core show version'
docker compose -f compose.asterisk.yaml exec -T asterisk \
asterisk -rx 'pjsip show contacts'
```
只有一个节点采用host network;不要在同一宿主机直接启动第二套争用5060/8088/RTP的配置。主备trunk只是接入配置,自动FALLBACK、长期ARI事件连接、业务状态机和录音上传程序仍需开发。
开始真实外呼前,必须通过SIP供应商侧日志核对我方实际出口确为123.56.71.98;仅EIP绑定成功不能证明没有其他NAT/路由改变出口。
## 6. 测试与验收边界
```bash
python3 -m unittest discover -s tests -v
(cd services/asr-web && go test -race ./... && go vet ./...)
node --test tests/test_pcm.cjs
node --check services/asr-web/web/app.js
bash -n deploy/asterisk.sh
```
本轮验证分开记账:
- 自动测试:云只读/归属/固定IP、创建幂等、失败恢复、EIP绑定竞态、库存完整性;Asterisk配置验证;ASR鉴权、取消、模拟WebSocket结果、帧边界及PCM编码。
- 本地容器:ASR镜像构建、非root/read-only运行、回环HTTP与healthcheck;测试容器已清理。
- 浏览器:无效令牌拒绝、正常令牌读取未配置模型、模型禁用、LLM/TTS未启用。没有采集真实麦克风,也没有发送真实供应商请求。
- 未执行:真实阿里云查询/创建/绑定、SIP通话、真实ASR、LLM/TTS、MQ/OSS、完整外呼和竞价回收恢复。
本地模拟通过不等于生产可用。只有填齐资源和契约、执行既有验收文档相应场景并留存证据后,才可把阶段状态更新为真实环境验收通过。
## 7. 下一步需要用户提供/确认
1. 在本机通过安全方式配置阿里云CLI与Profile/RAM Role/STS,并确认123.56.71.98的实际归属及是否可迁移EIP。
2. 北京实例规格、镜像、VSwitch、安全组、SSH KeyPair、竞价上限及磁盘/EIP/流量预算;现有主机是否允许复用。
3. SIP主备真实地址、协议、鉴权、号码/主叫要求和接入限制。
4. ASR测试凭据、批准的模型/资源ID;Web HTTPS域名/证书或SSH访问方案。
5. 用户制定的MQ/OSS ID接口规范,以及新的LLM/TTS协议、参数与取消/打断规则。
以上未确认前,不创建计费资源、不改绑白名单IP、不自动拨真实号码,也不声称完成整个平台。