Files
go-sip/docs/thirds/saas-dispatcher.md
T

11 KiB
Raw Blame History

SaaS ↔ Dispatcher:项目内唯一现行通信约定

本文根据用户提供的 v0.5-proposal.md 及已确认的 K01–K16 整理为可校验的项目内合同;原提案含注释、排版错误及被后续确认取代的旧队列/租户字段。项目内 Schema 与合法/非法 JSON 的唯一机器来源为 contracts/local/;不得从本 Markdown 复制第二套手写 Schema。本地隔离 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 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 不再启用。HTTP 旧 running 不得覆盖已持久的 pause/stop;resume 经最新任务配置核验后只能解除人工暂停。

MQ:固定 v1 传输,唯一事件清单

RabbitMQ 是 Topic,SaaS 独占创建、绑定、退役 exchange/queue,D 只消费/发布,无 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;pause/resume/stop,无控制去重/CAS 字段;省略 active_call_policy 默认 hangup,显式仅 drain/hangup。成功将操作发给 Agent 后才回应用回执,回执不是已完成排空/挂断的证据;stop 同 ID 不可恢复 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/Shanghai 09: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)

  1. 有效录音先直接 PUT 至 OSS,只上传录音;成功即持久入 D 的最终结果/outbox,正常路径不生成录音或结果业务文件(不得先写临时文件再删)。无应答无录音直接发真实结果,recording={};录音生成失败仍报告真实通话 outcome、空录音与明确 reason_message,不走 OSS/重试,不伪造 SIP 码。
  2. OSS 上传首次失败时,录音和结果恢复信息两者均保存成功后才建立固定重试起点/48 小时截止,保持原 bucket、object_key 和消息身份。本地结果文件仅用于恢复 MQ 回报,不上传 OSS。任一恢复文件不能完整保存:显式报错,保留已有内容,暂不发送最终结果、也不宣称有可恢复副本,等待人工修复。
  3. 从首次两文件完整保存起重试:间隔为 1、2、4、8、16、32、60 分钟,其后每 60 分钟一次;重启/失败不重置起点,SDK 默认自动重试不得改变节奏。每次经 D↔A RPC 显式领取有效上传授权,不换对象/不向 SaaS 申请 TOKEN。48 小时仍不成功:停止自动重试、保留两文件和进度、标记待人工,不发伪造 uploaded/unavailable 或最终结果,也不自动重开窗口。
  4. 上传成功后仅恢复原最终结果消息的 MQ 可靠交付,MQ 失败不重新 PUT、不新建资产、不重拨;48 小时是 OSS 重试窗口,不是 MQ outbox 的清除期限。已确认结束的通话资源及时释放,不等待 OSS/MQ;未知执行不释放。进程在正常上传尚未成功、失败恢复两文件尚未完整保存前退出,内存录音可能丢失,不能声称零丢失,也不能为了隐藏限制悄悄预写盘。

校验、来源与界限

  • 字段及结构的唯一机器契约:config-read.schema.json、task-discovery.schema.json、mq.schema.json;正反例在 contracts/local/examples/,来源/hash 在 contracts/local/manifest.json,检查入口 go test ./contracts -run TestCurrentContractExamples 及 scripts/check-current-contracts.sh。外部供应商仍未签收。
  • JSON 样例是隔离 Mock 虚构数据;example-only-not-a-real-secret 不是凭据。严禁将真实凭据、完整用户音频或完整对话放入源码/日志/证据。运行时须按接入方权限与实际加载事实再核验,不以机器 Schema 通过取代拨号授权。