14 KiB
SaaS ↔ Dispatcher:项目内唯一现行通信约定
本文是 SaaS↔Dispatcher 唯一当前人类可读规范,合并了已确认的业务规则、第三方交互及本地验收边界。字段、路由、正反例的唯一可执行依据仍为
contracts/local/;内部 RPC 以proto/agent/agent.proto为准,不在 Markdown 复制第二套 Schema。历史提案与批准记录保留在archive/sources/,不构成并行版本。P01–P08 仅完成项目内隔离 Mock 验证;真实 SaaS/management、供应商与生产均未签收,本文不授权真实外呼。
HTTP:五类只读配置
只由归属 D 使用 X-DISPATCHER-ID 和 X-DISPATCHER-SECRET-KEY 读取;HTTP header 名大小写无关。返回中的 dispatcher_id 须等于本 D,任务/配额的 tenant_id 是正整数;不接受旧字符串租户 ID。不在日志、样例或证据中打印真实凭据。业务正文只使用当前 Schema,不接受 schema_version、旧版 tenant_key、agent_version_id、授权期限或内容摘要等删除字段;也不根据字段做版本分支或从 HTTP 失败回退旧 MQ 配置。
| 资源 | 方法和路径 | 应用规则 | 正例 |
|---|---|---|---|
| SIP 全量 | GET /internal/v1/dispatcher/sip |
revision 是实际加载版本核对依据;未知 transport/auth_mode/registration_required/max_concurrent_calls 为 null,不能作为可执行线路默认值;变更时关准入、排空并核验 Agent/Asterisk 实际加载 |
sip |
| AI provider 全量 | GET /internal/v1/dispatcher/ai-providers |
启动时一次读取全局列表,本进程全部任务共用到下一次重启;运行中 start/resume 不重复拉取,服务商变更须重启后才生效。provider_ref 只标识供应商;将明文 credential 原值交给选定角色的 SDK,不引入 ref 查找或交换;缺失、禁用、角色不匹配、无效凭据拒绝执行 |
providers |
| 任务 | GET /internal/v1/dispatcher/task/{task_id} |
归属、状态和 task_revision 校验后持久绑定同一 agent/provider 快照;不可变摘要由本项目计算。同一 revision 内容不同拒绝;ASR-only 只需 ASR,不强制 LLM/TTS;0 和 false 原样保留 |
ASR-only · full AI |
| 任务列表 | GET /internal/v1/dispatcher/tasks,后续 ?after=<cursor> |
首次/重启完整读到 带 cursor 且 tasks=[] 的终止页;非空短页不能提前结束。非空页持久成功后才使用下一 cursor;控制队列积压处理前不开新任务准入。不使用旧 snapshot_id/watermark/mode=snapshot | page · end |
| 租户额度 | GET /internal/v1/dispatcher/tenant/{tenant_id}/quota |
quota_revision 为业务修订号;未知占用不得算成已释放 |
quota |
HTTP 非 200、正文不合法、缺字段、归属冲突、缓存失效或读取失败时拒绝新的相关准入并记录脱敏错误;已持久接纳的执行继续使用原快照。错误体示例 error,不能当成功配置解析。任务仅有运行/暂停/终止;终止是不可逆 stop,同一 task_id 不再启用。任务配置只能在暂停或停止后修改;没有 edit 事件,也不定时刷新任务列表。启动/重启时完整读取任务列表及独立的全量 SIP;运行中新建任务由 task.control 的 start 触发读取,人工暂停后修改配置由 resume 触发重读任务和租户配额,两者沿用启动时的 AI 服务商列表和已生效的 SIP 快照,不另行读取或向 Agent 核验全量 SIP。HTTP 旧 running 不得覆盖已持久的 pause/stop;resume 经最新任务配置核验后只能解除人工暂停。SIP 无常规定时拉取;sip.config 持久关准入并触发 SIP 全量读取,等待已接纳呼叫结束及 Agent 加载核验后,将新 SIP 绑定到本地任务快照(不重读任务列表),然后才可恢复准入。通知待处理时可重试,读取或校验失败不回退旧 SIP,也不开准入。
MQ:固定 v1 传输,唯一事件清单
RabbitMQ 是 Topic,SaaS 独占创建、绑定、退役 exchange/queue,D 只消费/发布,并仅在 stop 时对自己负责的任务队列执行 purge;不创建、删除或绑定队列,无 configure 权限。机器拓扑见 mq-topology.json。外呼维持每 D、每任务独立的 d.<dispatcher_id>.task.<task_id>.in 路由及 agent-call.d.<dispatcher_id>.task.<task_id>.v1 队列;每 D 单独控制路由 d.<dispatcher_id>.control.in 及队列 agent-call.d.<dispatcher_id>.control.v1。SaaS 将所有 D 的输出 d.<dispatcher_id>.out 精确绑定至同一个结果队列(本地 Mock 名 agent-call.saas.events.v1,不是强制 SaaS 实际队列名)。入站 exchange agent-call.dispatchers.v1;出站 exchange agent-call.saas.v1;死信 exchange agent-call.dead-letter.v1。不广播后靠正文过滤,不使用独立通配段,不把被动声明/发布 confirm 冒充指定队列已收到:须以实际路由、mandatory return 与 confirm 联合验证。
event_id 是入站/回执关联和内部防重复处理的消息身份;dispatcher_id 是 D 身份,tenant_id 是正整数。不是所有事件共用一个统一必填信封:sip.config 没有 tenant_id/issued_at;回执按其示例字段,最终结果使用 issued_at 而非 occurred_at。正文的 schema_version 已移除。只接受以下外发事件及必要入站事件;所有字段直接按 mq.schema.json 和正例校验:
| 事件 | 方向和处理规则 | 正例 |
|---|---|---|
sip.config |
SaaS→D;revision 触发全量重新读取和实际加载核验,不用通知正文代替全量 |
notification |
task.control |
SaaS→D;start/pause/resume/stop,无控制去重/CAS 字段。start 读取新任务配置及租户配额并持久绑定,无 Agent 控制动作;resume 重读最新任务配置和额度后解除人工暂停;均沿用启动时 AI 服务商列表及已生效 SIP 快照,不重复拉取全局服务商或 SIP,也不额外核验 SIP;pause/stop 发给 Agent,省略 active_call_policy 默认 hangup,显式仅 drain/hangup。配置读取、归属或 AI 核验失败时不接纳,SIP 待处理时新呼叫仍关闭;stop 先停止该任务消费者(使在途未确认消息回队列),持久关闭任务准入并抑制本地待执行指令,再对该任务队列执行 purge 清除当时待投递消息;purge 失败不发送成功回执、保持准入关闭并重试原控制消息。SaaS 须停止继续投递已 stop 的任务;purge 不阻止之后新投递,这些消息不执行。成功操作后才回应用回执,回执不是已完成活跃通话排空/挂断、SaaS 已停止投递或未来消息不存在的证据;stop 同 ID 不可恢复 |
start · control · ack |
call.execute |
SaaS→D 仅 {task_id,callee};一次指令保留独立消息/执行身份,路由/主叫/AI/时限从绑定任务读取。dispatched 表示已发出呼叫指令,不表示接通;白名单/单号码格式不合规则回 rejected,reason_code:null,reason_message、不拨号不发最终结果、不暂停整任务 |
execute · dispatched · rejected |
call.execute.result |
D→SaaS;按 task_id + 原号码关联,每次呼叫仅一份最终结果;不新增外部 call_id/source_command_id;真正终结且录音成功上传、无录音或预期录音生成失败后才发送 |
uploaded · empty |
旧 command.result、call.result、分散通话/转写/拒联事件、recording.uploaded 不再作为对外并行通知或兼容别名。D 在 inbox 持久后 ACK;状态/outbox 同事务;结果发布使用原消息身份可靠交付;confirm 不是 SaaS 应用收讫。消息年龄不让旧命令绕过准入;未来 issued_at 不提前接纳。重复投递/未知执行不触发再次拨号。
调度、AI 与真实结果(K01–K09、K11–K14)
- 白名单仅含
15003164745、15830461047原值;已选 SIP trunk、任务与线路每周时段、任务排除日期、任务/租户/线路额度、任务与 AI 较小通话时限均在接纳及实际发呼叫指令前检查。线路字段未知则 fail-closed;选线后固定、不自动重拨/换线。隔离 Mock 中规则暂不满足时保留待执行指令、暂停该任务的调度,规则允许后重验;与人工 pause/stop 分离,不能自动解除人为停止。本规则不放宽真实路径 Asia/Shanghai09:00–20:00固定门禁或授权真实拨号。 - 接通事实为真时
outcome=answered(后续异常不抹掉接通);已发起但忙线、拒接、无人接听且确定结束为no_answer;确认未接通并由 Agent/Asterisk 执行故障终结为failed;未知状态保持未知占用,不能伪造结束、结果或自动重拨。真实 SIP 状态码原样数字写入reason_code,无真实 SIP 码则null并以reason_message说明;禁止本地虚构数字错误码。无应答且没有录音时transcript=[]、opt_out=false、recording={}。 - 只有用户侧 ASR 最终识别文本包含任一
hangup_keywords字面字符串才挂断;中间识别、助手回复、开场白、TTS 均不能触发;重复结果不可反复终结。同一任务 revision 不同内容拒绝准入;provider 禁用/角色不符不可调用。Mock 参数验证不等于真实供应商验收。
录音与最终结果(K10、K15、K16)
- 有效录音先直接 PUT 至 OSS,只上传录音;成功即持久入 D 的最终结果/outbox,正常路径不生成录音或结果业务文件(不得先写临时文件再删)。无应答无录音直接发真实结果,
recording={};录音生成失败仍报告真实通话 outcome、空录音与明确reason_message,不走 OSS/重试,不伪造 SIP 码。 - OSS 上传首次失败时,录音和结果恢复信息两者均保存成功后才建立固定重试起点/48 小时截止,保持原 bucket、object_key 和消息身份。本地结果文件仅用于恢复 MQ 回报,不上传 OSS。任一恢复文件不能完整保存:显式报错,保留已有内容,暂不发送最终结果、也不宣称有可恢复副本,等待人工修复。
- 从首次两文件完整保存起重试:间隔为 1、2、4、8、16、32、60 分钟,其后每 60 分钟一次;重启/失败不重置起点,SDK 默认自动重试不得改变节奏。每次经 D↔A RPC 显式领取有效上传授权,不换对象/不向 SaaS 申请 TOKEN。48 小时仍不成功:停止自动重试、保留两文件和进度、标记待人工,不发伪造 uploaded/unavailable 或最终结果,也不自动重开窗口。
- 上传成功后仅恢复原最终结果消息的 MQ 可靠交付,MQ 失败不重新 PUT、不新建资产、不重拨;48 小时是 OSS 重试窗口,不是 MQ outbox 的清除期限。已确认结束的通话资源及时释放,不等待 OSS/MQ;未知执行不释放。进程在正常上传尚未成功、失败恢复两文件尚未完整保存前退出,内存录音可能丢失,不能声称零丢失,也不能为了隐藏限制悄悄预写盘。
Agent RPC、会话与恢复边界
- D↔A 只采用预绑定身份和双向 TLS Unary gRPC;当前八个方法见
agent.proto。Agent 不直连 SaaS、没有业务数据库;D 的 SQLite 持久保存任务/额度/inbox/outbox,Agent 执行及上传恢复文件私有。旧 SQLite/Agent 状态发现后只读失败关闭,不自动删除或迁移;未知通话占用不得因超时、新 boot 或重启自行释放。 - Agent 会话提前续期;仅对已验证 mTLS、同 Agent/Cell/boot/Dispatcher epoch、相邻且仍有效的代际拒绝,
RequestRecordingUpload、ReportCallEnded、ReportCallResult可在六秒或调用方更早期限内,沿原事件/operation/幂等键重报原事实。其它拒绝、超时及结果不明停止重报,不自动重拨、重传不确定 OSS PUT 或放宽最新代际栅栏;详细错误边界见proto/ERRORS.md。
校验、来源与验收界限
- 当前字段及结构:
config-read.schema.json、task-discovery.schema.json、mq.schema.json;正反例在contracts/local/examples/,来源路径和 SHA-256 在manifest.json。历史原件按原字节存入archive/sources/,旧上游 v1 存入archive/upstream/;它们不嵌入当前运行合同,不提供回退入口。make check校验当前合同、历史来源、Proto、格式、race、vet、构建及隔离 MQ 实际收件。 - 已完成的 P01–P08、A01–A12、K01–K16 项目内证据见
P08 对照;本地手写代码覆盖率为 72.0%,发布清单production_approval=false。本地 Mock、假工具诊断和 SHA-256 不证明真实 SaaS/management/MQ 应用收讫、OSS/AI/SIP/Asterisk/ECS、现场抓包或生产已通过。外部真实接入、部署和拨号均需另行授权、签收和验证。 - JSON 样例是隔离 Mock 虚构数据;
example-only-not-a-real-secret不是凭据。严禁将真实凭据、完整用户音频或完整对话放入源码/日志/证据。运行时须按接入方权限与实际加载事实再核验,不以机器 Schema 通过取代拨号授权。