docs: clarify SaaS Dispatcher integration and config proposals
This commit is contained in:
@@ -77,10 +77,10 @@
|
||||
|
||||
## 当前范围
|
||||
|
||||
- 后续Agent必须先读 `docs/plan-0918.md`:映射W/子任务,按索引读取需求/契约正文,确认I/M/G前置、授权及写入边界后实施;并行时按§9认领,子Agent只写独占模块/证据,§8总台账、公共文件及合并状态由集成负责人单写。计划不替代权威Schema或验收,不重复审批已确认方向,不自动授权真实云/付费/拨号。
|
||||
- 后续Agent必须先读 **`docs/plan-config-read-v0.1.md`**:按 F 工作包/文档索引读取需求与契约,确认 C/L/M 和 I/M/G 前置、授权及写入边界后实施;并行时按该计划§9认领,子Agent只写独占模块/证据,§8本轮总台账、公共文件及合并状态由集成负责人单写。已恢复原内容的 `docs/archive/plan-0918.md` 仅保留旧 W00–W16 历史,不是现行计划入口。计划不替代权威Schema或验收,不重复审批已确认方向,不自动授权真实云/付费/拨号。
|
||||
- 当前MQ-only/OSS调整目标已获用户明确批准由当前Agent独立执行,**不启动子Agent**,不得再以子Agent模型/fast环境阻塞本目标;此前按任务类型自动分工规则在本目标不适用。若未来另获启动授权,仍遵守下条模型要求。
|
||||
- 用户指定:若启动开发及配套审查子Agent,固定 **gpt-5.6-luna、max思考、fast模式**。启动前查询精确provider/model和runner支持并显式配置(当前工具用模型`:max`后缀及`fast: true`,不继承默认);不可用/不支持/无法核验则报告阻塞,不静默换模型、降思考档、关fast或换CLI。此为后续执行约束,本轮仅修复计划,未启动开发子Agent。
|
||||
- 并行开发须先有获授权的可追溯Git/契约基线、一lane一工作区/测试资源、无交叠写集合及每批合并后回归;当前子项目尚未跟踪的文件不能假定存在于HEAD/worktree。不得自动提交、暂存或清理父项目无关改动;详情见计划§9。
|
||||
- 并行开发须先有获授权的可追溯Git/契约基线、一lane一工作区/测试资源、无交叠写集合及每批合并后回归;当前子项目尚未跟踪的文件不能假定存在于HEAD/worktree。不得自动提交、暂存或清理父项目无关改动;详情见 `docs/plan-config-read-v0.1.md` §9。
|
||||
- 当前已获授权进行本项目开发:W01 项目内契约基线和 W02 Proto/stubs 已建立;仍不能把设计、Mock、Proto或88项测试清单写成真实供应商/生产验收已通过。入口和权威依据仍为 `docs/architecture/Go重写方案_v0.3.md`、`docs/acceptance/验证与切换验收_v0.3.md`、`docs/contracts/通信与事件数据交互_v0.1.md`、`docs/references/OpenAPI与MQ字段索引_v0.1.md`、`docs/dependencies/开源组件选型与复用清单_v0.2.md`。
|
||||
- 开发准备见 `docs/architecture/G0开发准备与契约冻结提案_v0.1.md`:D01–D10的方案方向、双模式/许可/恢复机制及内部PoC初始profile已获用户确认;本项目已自行交付 W01/W02 开发基线,但外部权威发布、真实预算、供应商签收和 G0/PoC 仍需分别验证,不能混写为生产合同。缺失字段细节、实际预算及方案变更另行确认。本轮验收基线收敛为单节点/单 Agent/单 Cell/单租户;双节点、第二 Cell、第二租户及其公平/故障矩阵不在本轮开发或验收范围,跨 Cell/多租户能力保留为后续阶段。
|
||||
- 用户已确认完整 Go Agent、分阶段替换:调度、Cell 执行、ARI/RTP/录音、AI 流及 Cell 配置接收。最终没有 Python 运行依赖;不重写 Asterisk、不实现第二套管理后台。
|
||||
@@ -101,7 +101,8 @@
|
||||
|
||||
- 外呼号码白名单:`15003164745`、`15830461047`。
|
||||
- 所有 SIP 线路仅允许在此列表范围内发起外呼;不在列表内的号码必须拒绝。该列表仅用于已授权的 Mock/明确安排的测试;不得因写入此处而自动发起真实呼叫,原始号码保持不变。
|
||||
- SIP 外呼时间窗口固定为 Asia/Shanghai 每日 `09:00`(含)至 `20:00`(不含);窗口外 Dispatcher/Agent 必须 fail-closed,禁止等待、自动延迟、重试或换线。mock 测试可注入时间验证边界,不能用 mock 结果宣称 real 放行。
|
||||
- **当前已实现且有本地边界测试的门禁**仍为 Asia/Shanghai 每日 `09:00`(含)至 `20:00`(不含);窗口外 Dispatcher/Agent 必须 fail-closed,禁止等待、自动延迟、重试或换线。mock 测试可注入时间验证边界,不能用 mock 结果宣称 real 放行。下述非生产和真实验证条款中的固定时间同样是**当前运行限制**,不得因目标变更擅自放开。
|
||||
- **用户确认的后续目标**:由任务配置的可外呼时段与所选 SIP 线路的可外呼时段共同决定是否可发起,二者须同时允许,取代固定全局 `09:00`–`20:00`。任务按周一至周日分别配置多个允许时段,可选排除多个指定日期;任务和线路时段/排除日期统一按 Asia/Shanghai 判断,排除日期全天不允许外呼。精确字段、跨日/边界、版本及排队消息跨窗口的期限和回执尚无冻结合同;先按 `docs/architecture/Dispatcher有界接纳与控制通道改造计划_v0.1.md` 完成契约、D/A最后门禁与验收更新。完成之前仍按现行固定门禁拒绝窗口外外呼,任务或线路时段也不能授权真实试拨;缺失/不确定时段拒绝,不等下一窗口、自动重试或静默换线。
|
||||
- 主叫标识保留原值(包括 `BD`),不能按纯数字手机号清洗,也不能直接当成 Digest 认证用户名;具体 From/PAI 等字段映射仍需确认。
|
||||
- 业务原始被叫号码保持不变;使用该线路时按其规则构造 `7089<被叫号码>`,避免重复添加或把该前缀带到其他供应商线路。
|
||||
- 传输协议、IP/Digest 鉴权、是否注册及并发限制仍需供应商确认;当前供应商已反馈需使用 PCMA,Asterisk 配置以 `allow=alaw` 表示,仍需真实线路验证。
|
||||
@@ -122,7 +123,7 @@
|
||||
|
||||
## 本次上线目标与分期(用户已确认)
|
||||
|
||||
- P1以稳定快速内测上线为目标:1个节点、1个Agent、1套Asterisk、1个单活Dispatcher/SQLite、1个启用租户;本轮不开发、不验收双节点、第二 Cell/第二 Asterisk或第二租户。
|
||||
- P1以稳定快速内测上线为目标:1个节点、1个Agent、1套Asterisk、1个单活Dispatcher/SQLite、1个启用租户;本轮不开发、不验收双节点、第二 Cell/第二 Asterisk或第二租户。用户已确认后续多 Dispatcher 目标为每 D 独立 ID/接收队列、独占 Agent/Asterisk 执行资源;同一租户可有多任务,但每个任务只能有一个 D 归属,SaaS 持久绑定租户+任务→D,呼叫/控制不得分发到其它 D,不能暗中迁移。单任务并发可由归属 D 本地判断;跨 D 租户/供应商总上限仍须先冻结有界额度份额,不能各 D 各按全局上限放行。本轮不把 D1/D2 隔离 fixture 冒充多 D 业务运行。
|
||||
- 至少3家独立SIP trunk 的静态配置、路由/主叫/前缀/codec/额度约束和协议 Mock/mixed 覆盖仍需保持;真实供应商外呼和 ECS 仅作为第二阶段联调,不是本轮前置。
|
||||
- ASR-only和ASR+LLM+TTS均按批准的不可变AI配置在本地/隔离链路验收;不擅自加MQ模式字段,不复用旧LLM/TTS。真实供应商未联调时必须明确标记为第二阶段,不能把 Mock 写成真实供应商通过。
|
||||
- P1使用管理平台批准的静态单 Cell 快照和受控维护窗口,不做在线发布/回滚编排;静态配置必须关准入、排空、核验实际加载,旧直写通道不得并行。
|
||||
@@ -151,9 +152,10 @@
|
||||
## AI配置与参数(用户已确认)
|
||||
|
||||
- P1采用百炼/火山ASR、OpenAI兼容LLM、火山TTS;SDK首选及未通过门禁见组件清单§1.3/§4.3。基础栈方向确定不等于精确版本、许可证或参数能力已验收;不为补字段改为自写协议。
|
||||
- Dispatcher按MQ任务agent_version_id,经RabbitMQ专用Topic向SaaS取得不可变AI配置/授权,响应回原Dispatcher;校验租户/源Schema/不可变摘要/能力并持久绑定后向Agent交付执行快照。旧SaaS AI版本GET方案已废弃,不再开发HTTP client。Agent不直连SaaS,不从CLI/env/源码常量或SDK默认覆盖AI业务值,不新增task-config猜测路径、任务MQ模式字段或调参后台。
|
||||
- **现行合同(尚未切换)**:Dispatcher按MQ任务agent_version_id,经RabbitMQ专用Topic向SaaS取得不可变AI配置/授权,响应回原Dispatcher;校验租户/源Schema/不可变摘要/能力并持久绑定后向Agent交付执行快照。过去已废弃的独立AI版本GET不复活;新获批准的方向是**任务配置只读接口内含智能体**,须另冻合同而非旧GET兼容层。Agent不直连SaaS,不从CLI/env/源码常量或SDK默认覆盖AI业务值,不新增任务MQ模式字段或调参后台。
|
||||
- 已有model/prompt/voice/speed/ASR输入与识别/temperature/max_tokens/timeout及对话控制必须实际传入SDK或控制器;热词/VAD/top_p/音量/阶段时限等所需扩展先在上游补GAP-09,再生成校验。严格additionalProperties不放宽,不借metadata/raw_request透传。
|
||||
- SaaS新版本供新任务引用,无需改代码/重启D/A;在途/原排队任务固定快照,同版本异内容拒绝。缓存按租户+版本隔离,断SaaS无有效授权缓存拒新准入;显式0/false与未提供保真,并发通话不得共享可变SDK参数。
|
||||
- **现行 AI 合同**:SaaS新版本供新任务引用,无需改代码/重启D/A;在途/原排队任务固定快照,同版本异内容拒绝。缓存按租户+版本隔离,断SaaS无有效授权缓存拒新准入;显式0/false与未提供保真,并发通话不得共享可变SDK参数。
|
||||
- **用户批准的后续只读配置方向(替代前述MQ配置目标)**:SaaS 提供两条只读 HTTP 配置接口:该 D 所属资源分区的获 management 批准的 SIP 全量,以及任务配置(内含智能体当前获授权的不可变版本/内容);D 用各自 UUID+SECRETKEY 按需获取,不将凭据写入源码、样例或日志。SIP 在启动/重启先取全量、核验 Agent/Asterisk 已加载再读执行队列;任务按租户+任务缓存约60秒,活跃且有待接纳执行时到期主动刷新;已接纳/运行中执行固定原快照,未接纳允许最多约一分钟配置生效延迟。ETag/304 减少重复下载,缓存过期或 HTTP 不可用时拒绝新执行,**不使用过期缓存或MQ配置回退**;停/暂停、opt-out仍经MQ及时处理,不随缓存延迟。任务终结后仅清配置缓存,不删执行恢复/幂等/outbox。SIP 变更须关执行准入、排空/对账、确认加载后恢复;不能保证一分钟内完成发布。旧 `call.execute` 固定引用/排队 AI 固定规则须经新合同定义重绑定与修改-准入竞态;未发布新版前不可变现行语义。
|
||||
- 凭据/供应商端点来自受控引用且有授权/出口校验,不能因可调参数绕过安全硬限额或启用不安全重试。OpenAI默认自动重试显式关闭;日志只留脱敏版本/摘要/有效参数,不打印prompt/变量/密钥。
|
||||
- 静态发布只约束SIP/节点制品,不将AI配置硬编码;GAP-08/09及SDK参数PoC为P1门禁,验证入口见验收§5.1(现有E/L项子场景,不新增虚假通过数)。
|
||||
|
||||
@@ -161,18 +163,21 @@
|
||||
|
||||
- 本项目设计/运行/验收文档只在自身 `docs/` 维护。上游共享接口有唯一权威来源;导入带版本、来源和哈希的不可变契约包,再生成类型/校验,不维护重复手写 Schema。
|
||||
- 新内部消息/许可/fencing 协议需先获批;不擅自改变 SaaS 路径、字段、状态、路由或控制语义。
|
||||
- **SaaS↔Dispatcher的全部交互唯一经RabbitMQ专用Topic订阅完成,禁止双方任何HTTP请求/回调/兼容通道或故障回退**,包括执行、控制、查询、整体补传、AI配置/授权及recording.uploaded上传事实通知;上传不申请SaaS会话或等待verified/OSS ID回复。OSS配置/TOKEN不来自SaaS:Agent领取及显式重申请TOKEN只经D↔A Unary;本规则不禁止Agent→OSS、ARI、AI供应商HTTP(S)或gRPC的HTTP/2。
|
||||
- **每个Dispatcher必须有独立、全局唯一且不重复的ID及独立接收Topic/队列**;指定D的任务/配置/上传结果不能由其它D抢收,也不能广播后仅靠正文过滤。身份与tenant/Agent/Cell ID、dispatcher_epoch分开;保留租户独立队列及原值tenant_key,完整新路由长度预算须重验。具体ID生成/持久化、Topic/绑定、消息字段/关联/错误/期限须随W01新版本冻结,不凭本文给旧严格Schema添加字段。
|
||||
- **现行已发布合同(新版本生效前必须遵守)**:SaaS↔Dispatcher的全部交互唯一经RabbitMQ专用Topic订阅,双方无HTTP请求/回调/兼容通道或故障回退,包括执行、控制、查询、整体补传、AI配置/授权及recording.uploaded上传事实通知;上传不申请SaaS会话或等待verified/OSS ID回复。OSS配置/TOKEN不来自SaaS:Agent领取及显式重申请TOKEN只经D↔A Unary;本规则不禁止Agent→OSS、ARI、AI供应商HTTP(S)或gRPC的HTTP/2。
|
||||
- **用户已批准的新目标,尚非现行合同或运行能力**:仅任务(内含智能体)与 SIP 获批配置由 SaaS 向 D 提供两条只读 HTTP 查询;呼叫、控制、查询、整体补传、recording.uploaded及其它业务事件/结果仍**唯一走MQ**,不提供配置HTTP→MQ回退,也不恢复旧业务HTTP通道。SaaS须分发 management 已批准的唯一 SIP 版本,management 仍为唯一编辑/审批面。当前严格 MQ Schema、HTTP 路径、身份/版本/缓存契约及验收在 W01 新版本冻结并完成切换前不可擅改或声称已实现。
|
||||
- 新HTTP配置字段阶段草案见 `docs/contracts/config-read-fields-v0.1-proposal.md` 及同目录 `config-read-v0.1.schema.json`/mock示例;截图只证实UI含义,英文响应键为项目自定义,绝非SaaS现网接口已确认字段。用户新增任务排除日期、线路时段等未见截图项按项目需求设计;SIP传输/鉴权/注册及额度未知不能猜默认值。草案校验不代表SaaS/management签收或W01接口就绪;真实响应、审批来源、摘要和Agent/Asterisk实际加载仍须验证。当前唯一权威运行契约不因草案变化。
|
||||
- **每个Dispatcher必须有独立、全局唯一且不重复的ID及独立接收Topic/队列**;指定D的任务/现行MQ配置结果/上传结果不能由其它D抢收,也不能广播后仅靠正文过滤;新目标只读HTTP配置由该D UUID+SECRETKEY获取且须核对任务归属。身份与tenant/Agent/Cell ID、dispatcher_epoch分开;保留租户独立队列及原值tenant_key,完整新路由长度预算须重验。具体ID生成/持久化、Topic/绑定、消息字段/关联/错误/期限须随W01新版本冻结,不凭本文给旧严格Schema添加字段。
|
||||
- **OSS相关配置存于Dispatcher配置文件,Agent向Dispatcher领取临时上传TOKEN后直传OSS,不保存长期凭据;SaaS不再下发OSS配置/TOKEN。** D复用官方SDK提供受限TOKEN/目标信息,配置缺失/无效明确失败;不在样例、源码、日志或证据中保存实际密钥/完整TOKEN。过期只允许A显式向D重新申请,不自动续期或向SaaS申请TOKEN;精确配置格式/TOKEN形态/UploadGrant映射另行核验,不猜字段。
|
||||
- **用户已修订目标:上传仅负责Agent直传及D可靠通知MQ,SaaS后续处理不属本项目职责。** D保留签发能力、不转发文件;R13持久保存事实与recording.uploaded outbox,消息为persistent、进入指定durable队列/绑定、mandatory无return且publisher confirm成功后才记交付完成。只写本地outbox不算入队;不申请SaaS会话、不等待verified/OSS ID、不新增VERIFYING、不伪造SaaS结果。
|
||||
- 新版recording.uploaded取代本项目recording.ready,字段为call_id/recording_id/upload_id/bucket/object_key/format/channels/sample_rate_hz/duration_ms/size_bytes/checksum_sha256;不含TOKEN/密钥/签名URL。MQ失败/确认丢失/重启只恢复原消息身份的交付,不重新PUT或新建资产。AI/控制等必要请求响应不受此收缩影响。
|
||||
- 前一轮MQ-only文档纠正已结束;当前目标已获批准修改本项目契约/代码/配置/测试。精确方案见已确认的 `docs/contracts/mq-only-v1-freeze-proposal.md`:纯Topic精确绑定、拒绝独立通配词段、tenant_key预算196 UTF-8字节、稳定UUID v4、严格JSON配置及15分钟SDK预签名PUT;不得重开已批准方向。当前仅保留现有代码需要的契约包;未发生契约迁移前不新增契约,旧快照不作为当前工作树输入。新版须完成Schema/正反例/哈希验证后发布,不把方案确认当实现完成。受影响W01/W02/W04/W05/W07/W08/W11/W12/W13/W14按plan§8.2重新验证。全局唯一D身份/专用Topic及本地D1/D2隔离fixture为当前合同要求,不授权双D业务运行、HA或共享额度。
|
||||
- 新版recording.uploaded取代本项目recording.ready,字段为call_id/recording_id/upload_id/bucket/object_key/format/channels/sample_rate_hz/duration_ms/size_bytes/checksum_sha256;不含TOKEN/密钥/签名URL。MQ失败/确认丢失/重启只恢复原消息身份的交付,不重新PUT或新建资产。现行AI/控制等必要请求响应不受此收缩影响;新目标AI改用任务只读HTTP获取,控制仍走MQ。
|
||||
- 前一轮MQ-only文档纠正已结束;以下是**现行 MQ-only v1 合同的历史基线**,不得误当作新只读配置HTTP方案的限制或验收通过:现行机器依据见 `contracts/upstream/v1/mq-topology.json`、`mq.schema.json` 和 `event-payloads.schema.json`,第三方顺序说明见 `docs/thirds/第三方对接事件与请求消费顺序_v0.1.md`:纯Topic精确绑定、拒绝独立通配词段、tenant_key预算196 UTF-8字节、稳定UUID v4、旧版严格JSON配置及15分钟SDK预签名PUT;新目标保留业务MQ及OSS规则,仅替换配置获取,须另发版本化合同,不原地复用旧Schema。当前仅保留现有代码需要的契约包;未发生契约迁移前不新增契约,旧快照不作为当前工作树输入。新版须完成Schema/正反例/哈希验证后发布,不把方案确认当实现完成。历史 W01/W02/W04/W05/W07/W08/W11/W12/W13/W14 范围见归档 `docs/archive/plan-0918.md` §8.2;本轮新配置/控制路径须按 `docs/plan-config-read-v0.1.md` §4–§8 重新验证。全局唯一D身份/专用Topic及本地D1/D2隔离fixture为当前合同要求,不授权双D业务运行、HA或共享额度。
|
||||
- 文本实时事件准确名为transcript.updated,不新增call.transcript别名;OSS文本归档不能替代实时文字/opt-out,缺少专用资产授权接口时明确未启用,不能伪装recording.ready。
|
||||
- 现有MQ信封command_type/command_id、event_type/aggregate_*与正文已对齐;事件payload专属约束尚需补齐,不把通用object校验当完整验收。字段索引只读生成,不手改成第二套Schema。
|
||||
- `tenant_key` 原值一对一绑定,不清洗、编码或截断;旧布局224个UTF-8字节预算不能在加入D身份后直接照搬,W01须冻结并验证完整routing key/queue预算及分隔符/通配符边界;超限停止发布并保留源任务。
|
||||
- 持久 inbox 后 ACK;状态与 outbox 同事务;confirm 不等于 SaaS 应用收讫。重复投递、未知执行和恢复不能触发重复拨号。
|
||||
- 配额覆盖所有 Cell/实例及未知占用;租约过期不自动释放不明通话。控制CAS为 expected_task_revision,pause与stop的drain/hangup区分;paused可按新授权恢复,stopped不可恢复。整体补传仅call_id/source_command_id,禁止新增task/execution补传。最后发起许可、权限和屏障须故障注入。
|
||||
- management是SIP配置唯一编辑/审批面。P1通过批准的版本化静态制品和受控部署入口交付,D核验目标/准入屏障,Agent加载并报告;不要求在线发布控制面。静态交接合同须批准,旧直接写Agent面不能同时启用;成功必须证明精确快照已被Asterisk加载。
|
||||
- management是SIP配置唯一编辑/审批面。**现行 P1**通过批准的版本化静态制品和受控部署入口交付,D核验目标/准入屏障,Agent加载并报告;不要求在线发布控制面。静态交接合同须批准,旧直接写Agent面不能同时启用;成功必须证明精确快照已被Asterisk加载。
|
||||
- **用户批准的后续 SIP 配置获取目标(取代此前MQ全量+增量目标)**:management 仍是 SIP 唯一编辑/审批方,SaaS 仅经只读 HTTP 提供该 D 资源分区的获批全量快照;新加入/重启 D 先查询、核验所属 Agent/Asterisk 实际加载的版本/摘要后才消费执行队列。后续约每60秒核对版本,有变化就关执行准入、排空旧活动通话并确认新版已加载;控制MQ照常处理。SaaS/management 分属不同配置系统时须证明SaaS分发的是同一份获批制品;不引入共享业务DB或MQ配置回退。此路径及多 D 不属现行 P1 已实现能力,需新只读HTTP/管理交接合同、资源配额份额和恢复/边界验收,详见 `docs/architecture/Dispatcher有界接纳与控制通道改造计划_v0.1.md` §3.2。
|
||||
|
||||
## 安全和真实验证
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# go-sip
|
||||
|
||||
面向生产的Go SIP调度与执行项目:同一module/二进制通过Cobra提供 `dispatcher`、`agent` 两个业务子命令,分阶段替换Python,保留既有业务语义,SaaS交互统一为MQ-only。
|
||||
面向生产的Go SIP调度与执行项目:同一module/二进制通过Cobra提供 `dispatcher`、`agent` 两个业务子命令,分阶段替换Python,保留既有业务语义。现行 SaaS↔Dispatcher 合同为 MQ-only;新增两只读 HTTP 配置接口仅是本轮待冻结的目标。
|
||||
|
||||
> **当前状态:本轮单节点/单Dispatcher/单Agent/单Cell/单租户的 MQ-only 本地范围已完成。** 控制、AI配置/授权、查询/补传、上传通知及恢复均有 loopback RabbitMQ 证据;业务源码覆盖率为65.8%。真实SaaS/MQ、供应商/ECS、生产切换及第二Cell/第二租户仍属第二阶段;当前开发主机缺少 Asterisk/tcpdump,部署 preflight 已按要求 fail-closed,未冒充 mixed/real 通过。
|
||||
> 本项目已独立拆仓运营;源码、配置、依赖、迁移、测试、部署和文档均在此目录内维护。远程仓库为 `git.ipao.vip/rogee/go-sip`,本地 Git 默认分支为 `main`;真实外呼仍受逐次授权、capture-first、白名单和时间门禁约束。
|
||||
@@ -17,10 +17,10 @@
|
||||
- 重写完整 Agent:调度控制面、Cell 外呼执行、ARI/RTP/录音、AI 流式适配、Cell 配置接收。
|
||||
- 分阶段迁移,最终构建、测试和运行不依赖 Python、父仓库目录或其它业务项目内部代码。
|
||||
- Asterisk 继续负责 SIP;独立 SIP 管理平台继续拥有配置管理权,均不纳入重写。
|
||||
- **SaaS↔Dispatcher全部请求、响应和事件只走RabbitMQ,双方禁止任何HTTP。每个D都有全局唯一ID及独立接收Topic/队列,不能共享队列抢收或广播后过滤。** 执行/控制/查询/补传、AI配置/授权及 `recording.uploaded` 均在内;本地契约已冻结,P1不扩为多D协调/HA。
|
||||
- **现行已发布的 SaaS↔Dispatcher 合同**:全部请求、响应和事件只走 RabbitMQ,双方无 HTTP;每个 D 有全局唯一 ID 及独立接收 Topic/队列。新目标只让获批 SIP 全量配置及任务(内含智能体)经两条只读 HTTP 接口获取,执行/控制/查询/结果/上传事实仍走 MQ;合同、代码和验收未切换前继续遵守现行规则。P1 不扩为多 D 协调/HA。
|
||||
- **OSS配置存于Dispatcher配置文件,Agent向Dispatcher领取临时上传TOKEN后直传OSS,不保存长期凭据。** SaaS不下发OSS配置/TOKEN;D保留SDK签发能力,不转发文件。上传不申请SaaS会话、不等待verified/OSS ID或业务处理回复;D仅在原上传事实可靠进入指定持久MQ队列后记录交付完成。实时文字仍为 `transcript.updated`,归档不能替代实时事件。
|
||||
- 本次必须支持 **ASR-only** 与 **ASR + LLM + TTS** 两种模式;本阶段已完成本地/协议隔离双模式验收。百炼/火山ASR、OpenAI兼容LLM、火山TTS的真实供应商能力和生产参数联调仍标第二阶段/未启用,不能将 Mock 写成真实供应商通过;禁止复用旧LLM/TTS。
|
||||
- **AI业务配置/授权由D按任务agent_version_id经MQ向SaaS取得,经本D专用Topic收响应、校验并持久绑定后交付Agent;旧AI GET已废弃。** 模型、提示词、音色/语速、识别、超时/打断等参数不写死;新版本用于新任务,无需重启,在途通话固定快照。静态SIP发布不代表AI配置静态硬编码。
|
||||
- **现行 AI 配置**按任务 agent_version_id 经 MQ 向 SaaS 取得并持久绑定;过去独立的 AI GET 已废弃。**新目标**将获授权的智能体配置放入任务只读 HTTP 响应,不复活旧 GET 或保留 MQ 配置回退。 模型、提示词、音色/语速、识别、超时/打断等参数不写死;新版本用于新任务,无需重启,在途通话固定快照。静态SIP发布不代表AI配置静态硬编码。
|
||||
- 本轮验收范围收敛为单节点、单 Agent、单 Cell、单租户;保留 tenant_key 原值、独立队列、复合幂等和有界窗口。双节点、第二 Cell、双租户公平/背压/恢复不在本轮开发或验收范围,作为后续阶段。
|
||||
- 上传链路仅包含R12临时授权、Agent直传、R13报告及 `recording.uploaded` 可靠入队;SaaS后续资产处理不属于本项目。真实SaaS/MQ联调仍第二阶段,不新增生产授权。
|
||||
|
||||
@@ -54,16 +54,16 @@ sip-go-agent agent upload-retry --spool /path/to/agent-spool \
|
||||
|
||||
## 文档
|
||||
|
||||
文档目录总览见 [`docs/README.md`](docs/README.md)。后续Agent先读 [项目开发计划与需求阅读索引](docs/plan-0918.md),按W/子任务确认I/M/G前置并阅读详细设计/权威契约。并行开发按§9登记单写范围、隔离工作区/测试资源和合并回归,由集成负责人统一维护总台账;开发子Agent固定使用 **gpt-5.6-luna+max+fast**,不可用时报告阻塞,不静默降级。本轮未启动开发子Agent。
|
||||
文档目录总览见 [`docs/README.md`](docs/README.md)。后续 Agent 先读[本轮配置读取与有界外呼计划](docs/plan-config-read-v0.1.md),按 F 工作包确认 C/L/M 和 I/M/G 前置及权威合同;原 [plan-0918](docs/archive/plan-0918.md) 恢复原文归档,仅供追溯。并行开发按新计划§9登记单写范围、隔离工作区/测试资源和合并回归,由集成负责人统一维护§8总台账;开发子Agent固定使用 **gpt-5.6-luna+max+fast**,不可用时报告阻塞,不静默降级。本轮未启动开发子Agent。
|
||||
|
||||
| 文档 | 内容 |
|
||||
| --- | --- |
|
||||
| [plan-0918:开发计划与需求阅读索引](docs/plan-0918.md) | 首读入口:W00–W16及并行子任务、I/M/G关口、R0–R10索引、认领/单写/隔离/合并规则;部署候选前置到W13-a |
|
||||
| [本轮配置读取与有界外呼计划](docs/plan-config-read-v0.1.md) | 首读入口:F00–F06、C/L/M 与 I/M/G 门禁、本轮单写/验证状态;两只读接口仍是草案 |
|
||||
| [旧 plan-0918 归档原文](docs/archive/plan-0918.md) | W00–W16 历史状态及旧 MQ-only 阶段证据;非本轮执行入口 |
|
||||
| [Go重写方案 v0.3](docs/architecture/Go重写方案_v0.3.md) | 本次单节点/单 Cell/单租户目标、至少3 SIP fixture、双AI模式、P0/P1/第二阶段分期 |
|
||||
| [通信与事件数据交互 v0.1](docs/contracts/通信与事件数据交互_v0.1.md) | 首发Unary、静态SIP与双模式、§6.1–6.3 SaaS配置来源/参数矩阵/安全边界 |
|
||||
| [SaaS↔Dispatcher契约](docs/contracts/saas-dispatcher.md) | MQ-only目标、D唯一身份/独立Topic、待冻结消息及旧实现差异 |
|
||||
| [Dispatcher↔Agent契约](docs/contracts/dispatcher-agent.md) | 当前Unary/Proto事实及MQ上传结果的衔接待办 |
|
||||
| [SaaS/MQ/D/A/OSS泳道图](docs/contracts/saas-rabbitmq-oss-dispatcher-agent-timeline.md) | 全MQ目标时序,Agent直传OSS,无SaaS↔D HTTP |
|
||||
| [SaaS↔Dispatcher 第三方对接顺序](docs/thirds/第三方对接事件与请求消费顺序_v0.1.md) | 配置获取、MQ命令/查询、通话反馈及录音通知的请求、返回、字段和消费动作;区分现行与待签收目标,替代原六份已删除说明 |
|
||||
| [现行 MQ 机器契约](contracts/upstream/v1/mq.schema.json)、[Agent Proto](proto/agent/v1/agent.proto) | 现行消息和Unary字段权威;HTTP配置仅有项目提案 |
|
||||
| [OpenAPI与MQ字段索引 v0.1](docs/references/OpenAPI与MQ字段索引_v0.1.md) | 旧源只读提取42个HTTP操作/115个组件及哈希;不代表新MQ-only入口,不手改生成物 |
|
||||
| [deploys:物理机 systemd 发布包](deploys/README.md) | 锁定 Debian 13/Go 1.27.1/发布版本,构建并上传不依赖 Docker 的 Agent/Dispatcher 安装包 |
|
||||
| [验证与切换验收 v0.3](docs/acceptance/验证与切换验收_v0.3.md) | 10个首发汇总门禁、88项基线按阶段适用、P2公平性及独立容量验收 |
|
||||
@@ -79,11 +79,13 @@ sip-go-agent agent upload-retry --spool /path/to/agent-spool \
|
||||
- 业务代码范围仅包括 SIP Agent/Dispatcher 与 Asterisk;RabbitMQ、OSS、AI 供应商及 SaaS API 是 SaaS 提供的基础设施,不在本项目生产包中部署。独立集成测试使用自有隔离数据库、RabbitMQ 和契约 fixture;OSS数据面复用官方SDK/标准HTTP,D按自身配置文件提供临时TOKEN,A直传且不持有长期AK/SK;不申请SaaS上传会话,不等待SaaS校验或业务处理;外部 SIP Mock 只能以固定镜像及版本化协议接入,不导入其源码。
|
||||
- 真实外呼、云创建、供应商调用和消费授权均是独立门禁,不能由测试成功或本文档自动授权。
|
||||
|
||||
## 阶段与下一步
|
||||
## 旧阶段基线与本轮入口
|
||||
|
||||
以下为旧阶段的原有分期记录;两只读 HTTP 配置接口、有界消费和时段改造的新依赖、状态与门禁以[本轮计划](docs/plan-config-read-v0.1.md)为准。
|
||||
|
||||
1. **P0:先完成MQ-only新合同(GAP-10)。** 冻结D唯一ID/生命周期、独立Topic/队列/绑定、全部请求响应/关联/错误/期限及Unary异步衔接;旧包原样保留。其余已有基线按受影响范围重新验证,包括8种事件payload、双AI模式(GAP-08)、SaaS任务AI配置读取/调参(GAP-09)、首发Unary职责、单 Cell 静态快照来源/加载回执、D配置文件/临时上传TOKEN及上传通知可靠入队和至少3家SIP trunk的配置/协议 fixture。只核验实际采用的SDK;火山TTS参数覆盖、精确版本/许可证未核验前不宣布锁库,不等待未来动态发布/文本OSS归档合同。
|
||||
2. **P1:本次单节点内测上线。** 完成单 Cell、单租户、双模式、幂等/控制/配额/恢复/录音安全的本地/隔离闭环;真实 ECS、真实外呼、生产 SaaS/MQ 联调不作为本阶段前置。
|
||||
3. **第二阶段:** 真实 SaaS/MQ 对接、RabbitMQ ACL/TLS及适用的业务回执、真实供应商/ECS 联调,以及双节点、第二 Cell、第二租户公平调度。
|
||||
4. **后续另立项:** 在线动态发布、自动跨供应商FALLBACK、多Dispatcher HA/分布式配额、权重借用、文本OSS归档、1000路完整AI/N+1。既有call/command整体补传不是通用回放平台,当前单节点首发仍保留。
|
||||
|
||||
阶段目标详见主方案§1/§10,首发验收见验收方案§1.1–§1.2;文件名保持不变。本次MQ-only本地门禁以计划§8.2和`docs/evidence/20260922-mq-only-local-final.md`为准;真实依赖、供应商、云/拨号、生产receipt、容量和切换仍属第二阶段,归档旧证据见 `docs/archive/evidence/20260920-local-p1-acceptance.md`。
|
||||
阶段目标详见主方案§1/§10,首发验收见验收方案§1.1–§1.2;文件名保持不变。旧 MQ-only 本地门禁见[归档计划 §8.2](docs/archive/plan-0918.md)和`docs/evidence/20260922-mq-only-local-final.md`;本轮新目标仅按[新计划 §8](docs/plan-config-read-v0.1.md)记状态;真实依赖、供应商、云/拨号、生产receipt、容量和切换仍属第二阶段,归档旧证据见 `docs/archive/evidence/20260920-local-p1-acceptance.md`。
|
||||
|
||||
+8
-8
@@ -4,7 +4,7 @@
|
||||
|
||||
## 首读顺序
|
||||
|
||||
1. [`plan-0918.md`](plan-0918.md):W00–W16 任务台账、阅读索引、I/M/G 门禁和当前范围。
|
||||
1. [`plan-config-read-v0.1.md`](plan-config-read-v0.1.md):本轮两只读配置接口、有界消费及外呼时段的 F00–F06 执行入口、C/L/M 和 I/M/G 门禁。原 [`plan-0918.md`](archive/plan-0918.md) 已按 HEAD 原文归档,仅作旧 W00–W16 历史。
|
||||
2. [`architecture/Go重写方案_v0.3.md`](architecture/Go重写方案_v0.3.md):单节点、单 Cell、单租户的总体方案与分期。
|
||||
3. [`contracts/`](contracts/):SaaS、Dispatcher、Agent、RabbitMQ 和 OSS 的项目内契约。
|
||||
4. [`acceptance/验证与切换验收_v0.3.md`](acceptance/验证与切换验收_v0.3.md):验收门禁、证据要求和第二阶段边界。
|
||||
@@ -19,29 +19,29 @@
|
||||
- [`architecture/G0开发准备与契约冻结提案_v0.1.md`](architecture/G0开发准备与契约冻结提案_v0.1.md):D01–D10 方向及 G0 前置。
|
||||
- [`dependencies/开源组件选型与复用清单_v0.2.md`](dependencies/开源组件选型与复用清单_v0.2.md):组件、SDK、版本和 PoC 门禁。
|
||||
- [`references/OpenAPI与MQ字段索引_v0.1.md`](references/OpenAPI与MQ字段索引_v0.1.md):旧源只读索引,不是当前 MQ-only Schema。
|
||||
- [`references/saas-page-snapshot-analysis.md`](references/saas-page-snapshot-analysis.md):SaaS 页面可见字段盘点;其中旧 MQ 配置建议已被新目标取代,非契约/验收证据。
|
||||
|
||||
### 契约与决策
|
||||
|
||||
- [`contracts/saas-dispatcher.md`](contracts/saas-dispatcher.md):SaaS↔Dispatcher 的 MQ-only 边界、D 身份和专用 Topic。
|
||||
- [`contracts/dispatcher-agent.md`](contracts/dispatcher-agent.md):Dispatcher↔Agent 的 Unary 与异步结果边界。
|
||||
- [`contracts/saas-rabbitmq-oss-dispatcher-agent-timeline.md`](contracts/saas-rabbitmq-oss-dispatcher-agent-timeline.md):全链路时序和 OSS 直传路径。
|
||||
- [`contracts/mq-only-v1-freeze-proposal.md`](contracts/mq-only-v1-freeze-proposal.md):已确认的 MQ-only v1 冻结方向。
|
||||
- [`thirds/第三方对接事件与请求消费顺序_v0.1.md`](thirds/第三方对接事件与请求消费顺序_v0.1.md):只描述 SaaS↔Dispatcher 的配置读取、MQ 呼叫/控制/查询、通话反馈与录音通知的请求、返回、字段和消费顺序;现行与拟定目标分开,替代已删除的六份分散说明。
|
||||
- [`../contracts/upstream/v1/mq.schema.json`](../contracts/upstream/v1/mq.schema.json)、[`event-payloads.schema.json`](../contracts/upstream/v1/event-payloads.schema.json)、[`mq-topology.json`](../contracts/upstream/v1/mq-topology.json):现行 MQ 信封、事件正文、路由的机器契约。
|
||||
- [`contracts/config-read-fields-v0.1-proposal.md`](contracts/config-read-fields-v0.1-proposal.md):两只读接口字段来源、响应结构与项目自拟 Schema/Mock 示例;**不是现网 SaaS JSON 或正式合同**。
|
||||
- [`decisions/`](decisions/):已记录的关键技术决策。
|
||||
|
||||
### 证据
|
||||
|
||||
- [`evidence/README.md`](evidence/README.md):证据与开发文档的职责边界、保留和归档规则。
|
||||
- `evidence/` 保存可复核的运行事实、测试结果和边界说明;历史证据不因当前契约变化而改写。
|
||||
- 当前 MQ-only 本地结论以 [`plan-0918.md` §8.2](plan-0918.md) 和 [`20260922-mq-only-local-final.md`](evidence/20260922-mq-only-local-final.md) 为准。
|
||||
- 旧 MQ-only 本地结论见[归档计划 §8.2](archive/plan-0918.md)和 [`20260922-mq-only-local-final.md`](evidence/20260922-mq-only-local-final.md);**本轮新方向的状态只在[新计划 §8](plan-config-read-v0.1.md)登记**。
|
||||
- 证据中的 `mock`、`mixed`、`real` 必须按原记录理解;本地或隔离通过不等于真实 SaaS、供应商、生产或容量验收通过。
|
||||
|
||||
### 归档
|
||||
|
||||
- [`archive/`](archive/) 保存已被当前基线替代、但仍需保留追溯价值的文档,以及无项目归属的旧参考资料。
|
||||
- [`archive/`](archive/) 保存已被当前基线替代、但仍需保留追溯价值的文档,包括恢复原文的[旧总计划](archive/plan-0918.md),以及无项目归属的旧参考资料。
|
||||
- 归档文件不作为当前实现、契约或验收依据;需要引用历史事实时必须使用归档路径。
|
||||
|
||||
## 维护规则
|
||||
|
||||
- 不在 `docs/` 复制上游 Schema;上游契约包、生成物和字段索引保持单一来源。
|
||||
- 不把证据摘要改写成新的通过结论;范围、环境和未执行项必须保持可追溯。
|
||||
- 新增文档先归入方案、契约、决策、证据或归档中的一个明确类别,不在根目录新增无类别文件。
|
||||
- 新增文档先归入方案、契约、决策、证据或归档中的一个明确类别;本轮根目录的 `plan-config-read-v0.1.md` 是单一执行入口,不新增其它无类别根文件。
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
本文件是 [Go 重写方案](../architecture/Go重写方案_v0.3.md) 的执行清单,**不是验收通过报告**。项目内已具备 Go 可执行程序、W01/W02 契约与本地测试入口;下文仍将本地通过、协议 Mock、真实集成和生产签收严格分开。
|
||||
|
||||
**本轮MQ-only修订重新打开受影响验收**:SaaS↔D所有请求/响应/事件只能走RabbitMQ,每个D全局唯一ID、独立接收Topic/队列。旧HTTP/仅租户路由/本地OSS验证证据不能覆盖新基线,见[计划§8.2](../plan-0918.md)和[SaaS↔D契约](../contracts/saas-dispatcher.md)。新增子场景纳入既有88项,不新增虚假通过数;本轮仅改文档,未执行这些运行测试。
|
||||
**本轮MQ-only修订重新打开受影响验收**:SaaS↔D所有请求/响应/事件只能走RabbitMQ,每个D全局唯一ID、独立接收Topic/队列。旧HTTP/仅租户路由/本地OSS验证证据不能覆盖新基线,现行 MQ-only 旧版验证事实见[归档计划§8.2](../archive/plan-0918.md)和[现行 MQ 机器契约](../../contracts/upstream/v1/mq.schema.json);第三方时序说明见[事件顺序](../thirds/第三方对接事件与请求消费顺序_v0.1.md),用户新批准的两只读 HTTP 配置接口仍须按[新计划](../plan-config-read-v0.1.md)另验。新增子场景纳入既有88项,不新增虚假通过数;本轮仅改文档,未执行这些运行测试。
|
||||
|
||||
每个阶段分别报告:通过、失败、未执行、被外部条件阻塞,以及“本阶段不适用/延后”。延后不得计入通过率或当成实现。旧Python证据不计Go通过数;管理平台、SIP Mock、供应商和Go Agent分别签收。
|
||||
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
# Dispatcher 有界接纳与控制通道改造计划 v0.1
|
||||
|
||||
状态:**方案评估与后续实施计划;未修改契约、代码或 RabbitMQ 拓扑,未取得新方案的运行验收**。用户确认的原则是:Dispatcher 只是轻量消费者,RabbitMQ 保留尚未接纳的积压,SQLite 不是消息积压的存储兜底;本计划可提出独立控制通道,但其精确合同及 SaaS 接入须另行冻结。本文件是[本轮新计划](../plan-config-read-v0.1.md)的专项设计说明,不替代 `contracts/upstream/v1/`、新计划§8总台账或验收基线;[旧总计划](../archive/plan-0918.md)已归档。
|
||||
|
||||
## 1. 结论与适用范围
|
||||
|
||||
“按租户并发上限取一批、执行完删本地记录,再取下一批”**方向正确,但不能直接照做**:应按**当前可用名额**接纳,而不是按名义上限固定取;名额须同时满足租户及共享的供应商、单 Cell、出口、媒体、AI 和执行许可约束。消息 ACK 后可以从 RabbitMQ 移除;SQLite 中的执行归属、未知占用、控制版本、幂等依据、未交付 outbox 和恢复事实不能随呼叫结束立即删除。
|
||||
|
||||
仅靠 `prefetch=1` 不解决磁盘积压:当前 `ConsumeTenant` 对 `call.execute` 调用 `IngestCommand`,后者先写 SQLite inbox/task/`command.result(accepted)` outbox,成功 ACK 后立即接收下一条;`Reserve` 是之后的独立步骤。当前启动入口的 `--consume` 分支只启动消费、控制处理与 outbox flush,**未发现持续将已入库执行任务自动 Reserve/下发 Agent 的调度循环**。因此本计划既不能把“已入库”说成“已占用执行名额”,也不能把本地单租户 MQ 测试说成有界接纳验证。当前队列声明及拓扑包未设积压上限。
|
||||
|
||||
**与现有方案的冲突须先消解:**[总体方案 §6.1 第 3 点](./Go重写方案_v0.3.md)写着“临时额度不足进入受限持久等待”;本次用户明确要求未接纳的积压留在 RabbitMQ,不能把该句当作允许无界 SQLite 等待的依据。新合同冻结时应同步修订该条及总计划中相应窗口/ACK 描述,保留已经 ACK 的在途/恢复事实;在一致化前暂停实施受影响的接纳路径,不能用本计划覆盖权威合同。
|
||||
|
||||
首期只针对**单活 D、一个启用租户、单 Cell**完成安全有界接纳与控制可达;多个租户的独立消费与公平调度为后续范围(历史来源见[旧 W16](../archive/plan-0918.md),本轮状态见[新计划 §8](../plan-config-read-v0.1.md)),不能靠每租户多开 goroutine 声称已实现。本文给出多租户设计约束,不提前开放第二真实租户、多 D 或跨 Cell 配额。
|
||||
|
||||
## 2. 方案取舍
|
||||
|
||||
| 方案 | 优点 | 不能接受的缺点 / 代价 | 判定 |
|
||||
| --- | --- | --- | --- |
|
||||
| 继续先入 SQLite 再排队 | 接收/控制消息按原队列顺序到达 | ACK 后持续搬运积压,SQLite 可无界增长,违背 Dispatcher 定位 | 不采用 |
|
||||
| 单一队列按额度停消费,或仅加 `prefetch` / 优先级 | 改动较少,未 ACK 数量有界 | `call.execute` 占住队头时,后面的 `task.control`、查询和 `ai.config.result` 无法及时到达;提高优先级不能抢回已投递/未 ACK 消息 | 不作为安全完成方案 |
|
||||
| **呼叫接纳与控制/服务消息独立队列** | 呼叫满额留在 MQ,暂停/停止/查询不依赖呼叫名额;与当前 Topic 精确隔离方向一致 | 需新版拓扑/绑定、SaaS 发布端配合和跨队列乱序处理;两条队列及 outbox 仍要容量保护 | **推荐,仅在版本化合同及验证通过后实施** |
|
||||
|
||||
RabbitMQ 官方文档说明:`prefetch` 只约束未 ACK 交付;队列默认超长行为可能丢弃旧消息,需显式配置 `overflow=reject-publish` 并验证发送方的 publisher confirm/失败保留;官方讨论队头阻塞时建议先考虑多队列。参考:[Consumer Prefetch](https://www.rabbitmq.com/docs/consumer-prefetch)、[Queue Length Limit](https://www.rabbitmq.com/docs/maxlength)、[Priority Support](https://www.rabbitmq.com/docs/priority)。这些是设计参考,不是本项目已完成的 broker 验证。
|
||||
|
||||
## 3. 目标接收语义(待合同冻结)
|
||||
|
||||
```text
|
||||
SaaS 原任务/可恢复发布记录 → RabbitMQ 某 D/租户的执行队列 → 仅有可用名额才接纳
|
||||
→ SQLite:原执行身份、占用、恢复和必要事件 → ACK
|
||||
SaaS 控制/查询、AI 配置响应 → 同 D/租户的独立控制/服务队列 → 持久处理 → ACK
|
||||
```
|
||||
|
||||
1. **入口先查历史,后接纳新执行。** 精确验证 Dispatcher/租户/Schema/期限;重复命令恢复原决定,同键异内容拒绝;只有新执行才检查当前控制版本、AI 授权、资源资格及可用名额。未接纳消息仍在 RabbitMQ;不能因重复交付创建第二个任务、许可或拨号。
|
||||
2. **名额与记录须同一 SQLite 短事务。** 把新执行的 inbox、必要任务/绑定、占用预留、`command.result` outbox 与配额条件更新一起提交后才 ACK;commit 前失败则消息保持可重投。不得以“先数内存 goroutine 再落库”替代事务。实际发起仍须最后许可、与当前版本一致的时段门禁和控制屏障,预留并非拨号授权。
|
||||
3. **名额不足就停止执行消息的接纳。** 每个执行消费者仅允许有界未 ACK 在途(首期目标不超过 1 条);不反复 `Nack(requeue=true)` 形成热循环,也不无界拉取到内存。空位出现后按原消息继续处理;资源不足/控制关闭/AI 不可用不能偷偷换线、降级或先 ACK 存成本地待执行堆积。消息过期后按已冻结的期限与结果语义处理,不暗中续期或把过期命令重新拨号。
|
||||
4. **空位不是单一租户计数。** 对特定执行按其需要的全部 scope 原子预留;未知/拨号/振铃/通话中的占用均计入。由于真正所需线路、AI 模式等须在读取消息后确认,队头执行若欠缺某一资源,后面的同队列执行可能等待;本期明确记录队头等待与过期,不以任意跳过/重排伪造公平。如果需求要求同租户内不同资源互不阻塞,须再行冻结路由语义,不能在本方案里偷偷扩充队列。
|
||||
5. **持久化不等于无上限。** SQLite 保存已接纳/进行中/未知执行、尚需恢复的事实和投递 outbox,并为控制/事件/告警留磁盘余量;容量临界或 DB 不可写时停止新执行准入并告警,不能在失败路径先 ACK。能否继续接收控制取决于其持久化是否仍可用;整库满盘时必须报告不可用,不能保证控制已应用。
|
||||
6. **执行结束只释放确定的名额。** 未知通话须对账,不能凭超时释放或重新发起;终态与 outbox 交付状态可复核后再按获批准的保留规则清理大字段。保留原执行/命令去重及 stop 墓碑,不把幂等证据按普通日志 TTL 删除。SQLite/WAL/备份占用也应纳入容量核算。
|
||||
|
||||
### 3.1 外呼时段:任务与 SIP 线路双重限制(目标,尚未实施)
|
||||
|
||||
用户已明确:目标放行条件是**当前时刻同时落在任务配置的可外呼时段与所选 SIP trunk 的可外呼时段**,由这两项取代现有全局固定 Asia/Shanghai `[09:00, 20:00)`;任何一项缺失、不可确定、未批准或不匹配都不得发起。**任务配置可按周一至周日分别设置多个可外呼时段,并可选排除多个指定日期;任务和线路的时段及排除日期统一按 Asia/Shanghai 解释。**任务某日无允许时段,或当日位于排除日期列表,均不能外呼;排除日期优先于每周重复时段。例如任务周一有 `09:00–11:00`、`14:00–18:00`,线路周一允许 `10:00–16:00`,则该日只有 `10:00–11:00`、`14:00–16:00` 可以发起,若该日被排除则全天不允许。时段仅约束发起呼叫,不等于要求正在进行的通话到点强制挂断;不可把线路不可用视作默认允许或悄悄换线。精确配置字段、区间端点/精度、跨日写法、重复/重叠段校验、任务/线路配置生效版本与有效期**尚无冻结合同**,B1 前先向 SaaS/管理面确认,不按本计划猜填。
|
||||
|
||||
- SaaS 负责在任务配置层只发布当下有权拨打的命令;Dispatcher 对实际匹配的任务快照、线路配置、授权和当前时刻做独立判断,Agent 在取得最后许可及实际 originate 前按相同权威快照再判断。任一侧门禁失败则拒绝并留可追溯结果,不允许 MQ 投递延迟或 RPC 超时绕过;时钟/配置版本不明就停止发起。改选另一条线路必须依既有路由授权及独立额度,不能为绕过关闭时段而静默换线。
|
||||
- 任务时段与线路时段**交集为空或当前时刻不在交集**时,不占用执行名额等待下一窗口,不把已经接纳的任务放进 SQLite 排队,也不利用 MQ 留存把同一命令自动推迟到下一天。当前窗口内因名额不足尚留 MQ 的消息,在窗口结束时必须明确终结或过期:未到达消费者的消息也须通过获批准的有效期限、源侧结果/对账和 broker 可观察事实保证**下一个窗口不能直接拨出旧命令**。现有 `not_after` 只表示命令截止,不能独自证明任务/线路时段及跨队列状态;超时后的处置、回执和 SaaS 源任务保留须在 B1 冻结,不能只依赖无报告的 TTL 丢弃。
|
||||
- 后续版本需要冻结任务按周一至周日逐日多段、可选多日期排除的权威来源/不可变绑定方式、已批准线路制品中的时段、两端可核对的版本、Asia/Shanghai 时间来源和表达规则;核验现有 `call.execute` 与静态线路 Schema(当前都没有时段字段)及 `not_after` 的关系,不在旧严格 Schema 中偷加字段。被更改/撤销的任务或线路版本须使旧准入失效;已接通与未知执行仍按原恢复规则处理,不自动重拨。
|
||||
- **过渡禁令:**当前代码 `internal/callwindow`、Agent/Dispatcher 实际路径以及现行验收仍只实现 `[09:00, 20:00)`。新版任务/线路时段合同、SaaS 发布侧、配置交接、D/A 最后许可和本地故障/边界测试全部通过并获得相应授权之前,继续执行现有固定门禁;本文不授权在该时间段之外试拨、排队等待或真实放行。验收方案随后同步替换旧固定边界,分别测周一至周日多段/空日、多个排除日期优先、任务开/关、线路开/关、交集起止、跨日边界/时区、排队跨窗口、停用版本、D/A 时钟偏差及控制命令在关窗时仍可处理。
|
||||
|
||||
### 3.2 多 Dispatcher 配置获取与运行中修改(目标,尚未实施)
|
||||
|
||||
用户确认:每个 Dispatcher 有独立 ID/专属 MQ 队列,负责**互不重叠**的 Agent/Asterisk 执行资源;同一租户可有多个任务,但一个任务的呼叫/控制只由**一个固定归属的 Dispatcher**消费。任务进行中修改任务、智能体或 SIP 配置时,已接纳/已拨执行保持原快照;尚未接纳的呼叫允许使用**在配置修改后约一分钟内生效**的新版。现行“排队任务固定 AI 快照”和全 MQ 配置合同需重订,旧合同/当前 P1 不因此自动变更。
|
||||
|
||||
**目标改为两条只读 HTTP 配置接口,业务仍只用 MQ:**
|
||||
|
||||
| 接口(用途,正式路径/字段待冻) | 权威数据与读取时机 | 新 D / 配置变更 |
|
||||
| --- | --- | --- |
|
||||
| SIP 全量配置 | SaaS 只分发 management 已批准、适用于该 D 独占资源分区的版本化制品;management 仍是唯一编辑/审批面。D 用自己的 UUID+SECRETKEY 获取 | 启动/重启时取全量,获准且 Agent/Asterisk 实际加载相同版本/摘要后才消费新执行;其后约每 60 秒核对是否变更,变化时停执行准入、排空/对账旧使用者、应用并核验新版后恢复。若 management 与 SaaS 不共享同一权威配置,须先证明这条只读分发链不会产生第二份可编辑的权威配置 |
|
||||
| 任务配置(内含智能体) | SaaS 保留任务、单 D 归属、状态/周时段/排除日及获授权的不可变智能体版本/正文;仅该任务的归属 D 按需读取 | 有待接纳执行时按租户+任务缓存,成功读取/核对后约 60 秒有效;仍有待接纳呼叫的活跃任务到期主动复核,不为每通呼叫下载完整配置;任务确实终结且无在途/未知执行引用后删除**配置缓存**,不删除恢复/幂等/outbox 事实 |
|
||||
|
||||
**缓存与后加入 D:**两个接口均须返回足以核对归属、批准状态、配置版本/摘要与生效依据的**完整一致响应**。正式路径、UUID+SECRETKEY 的凭据承载、字段、授权范围、ETag/`If-None-Match` 条件读取及 `304` 的有效期计算由新版合同冻结,不假定现有 SaaS 已提供;凭据由部署受控注入,不出现在源码、配置样例或日志。新 D 不需补听历史配置广播:启动时读已批准 SIP 全量、核验实际加载;收到归属任务的候选呼叫时读取任务及其智能体配置。缓存和**已接纳执行快照**分开保存,不能因“任务结束删缓存”删掉执行、未知占用或未交付 outbox。
|
||||
|
||||
**约一分钟延迟的边界:**成功核验的任务/SIP 配置缓存最多约 60 秒有效;SaaS 更新后在此期间允许尚未接纳呼叫使用原批准版本,新版不保证立刻生效。D 对仍有待接纳呼叫的活跃任务和 SIP 在到期时主动条件读取;`304` 可确认未变化并续期,变化须读完整新版本。到期刷新失败、答复不确定或归属/批准版本无法核对,停止接纳新执行、保持原 MQ 命令未 ACK,**不提供 MQ 配置回退或过期缓存兜底**。已经接纳/接通的通话沿用它自己的快照,即使普通配置缓存随后过期或被删除。暂停/停止控制与 opt-out 仍经 MQ 及时处理,不受这一分钟延迟约束;号码白名单、当次已知任务/线路时段和最后发起门禁仍检查。SIP 即使在一分钟内被发现有变化,加载可能需要更长时间;此时先停执行准入、排空/对账旧使用者,确认 Agent/Asterisk 已加载新版本后才恢复,不能宣称一分钟内完成 SIP 发布。超出消息期限或错过可外呼窗口的未接纳呼叫不得在下一时段自动拨出。
|
||||
|
||||
**候选执行使用配置:**SaaS 持久保存 `(tenant, task_id) → dispatcher_id`,`call.execute`/控制只送唯一归属 D。D 仅在有执行名额时取候选呼叫、保持有界未 ACK;任务缓存有效则校验归属/状态/允许时段及内含的智能体授权,否则调用任务只读接口取得新的获授权快照。再核对 SIP 批准版本、Agent/Asterisk 实际加载状态、额度和最后许可,在同一 SQLite 事务持久绑定原执行身份、任务+智能体+SIP 版本、占用及 outbox,成功后 ACK。HTTP 超时/失败不回退旧配置、不重拨;SIP 未就绪则停读执行队列,控制仍处理。现有 `call.execute` 的 `task_revision`、`route_policy_id`、`agent_version_id` 固定引用与排队呼叫可换新版的目标冲突,须由新版合同确定旧引用如何重新授权/作废及更新、HTTP 刷新、SQLite 接纳交错的冻结点,不能私改已发布的 MQ 正文。现有 `ai.config.request/result` 仅属旧 MQ 配置合同,在新版本中停止使用而非保留兼容回退。新接口、刷新与这些绑定语义均尚未实现。
|
||||
|
||||
**多 D 并不只靠不同 ID:**独立 D 各持 SQLite 配额,对各自 Agent/Asterisk 保持互斥所有权和会话 fencing。一个任务只由单 D 负责,可使**该任务本身**的并发在本地判断;但同一租户的任务可归不同 D,租户总上限或同一 SIP 供应商限额仍可能跨 D。为避免逐呼跨网络算额度,建议由权威预先给各 D 分配有界份额,保证各份额总和不超过共享上限;例如租户总额度 10,可预分 D1=4、D2=6,两边都不能再用“全局 10”独立放行。份额更改须先关受影响准入、核对未决/未知占用和所有权后生效,不能热借用已占额度;这仍是待合同确认的推荐方案,不是现有多 D 配额实现。已有任务指定 D 的 RabbitMQ 队列也不能由新 D 抢收;跨 D 转移未决任务须另行授权、对账与防重拨,不在本计划中假定自动 HA。
|
||||
|
||||
## 4. 独立控制通道的安全门槛
|
||||
|
||||
现有 `contracts/upstream/v1/mq-topology.json` **只有每 D/租户一条 `.in` 路由及其队列**,`call.execute`、`task.control`、查询、补传和现行 `ai.config.result` 均走该入口。**不得在现有冻结 v1 包外新增 binding、重命名路由或猜测新的消息字段。** 后续由 W01 发布版本化合同:同一 D/租户分别精确绑定执行队列与控制/查询队列;呼叫、控制、查询、上传事实与结果**仍只经 MQ**,任务/智能体/SIP 两类只读配置查询则仅走新 HTTP 接口,不保留 MQ 配置通道。保留原值 `tenant_key`、独立 D 身份、消息大小/路由长度预算和正反例;定义旧 MQ 配置消息如何停发、两端何时切换和旧执行队列如何对账,不让同一执行进入两条队列而双拨。控制通道仍须有独立容量预算、DLQ、发布失败保留和未 ACK 限制。
|
||||
|
||||
**仅把控制改道还不够。** 执行可能仍在 RabbitMQ,而 `task.control` 已先从另一队列到达。当前 `HandleTaskControl` 只查 SQLite 中已经存在的任务,找不到会回复 `not_found`:对尚未接纳的 `call.execute`,现实现无法建立阻止后续拨号的屏障。新版合同及实现必须冻结以下次序和责任,未解决前不得宣称控制安全:
|
||||
|
||||
- SaaS 停止为被暂停/停止的任务发布新执行,并明确如何处理已经发布但尚未接纳的消息;不能把仅停止新发布当作队列中旧消息已消失的证明。
|
||||
- D 在控制受理时为对应租户/任务持久建立可用于**未接纳执行**的版本/状态屏障;`expected_task_revision` 如何对不存在于 SQLite 的任务做 CAS、如何返回 accepted/applied/not_found、pause 后如何恢复,须双方正式确定。执行入库前重新检查该屏障,禁止旧 revision 越过最终许可;已接纳执行继续沿原 Agent 屏障对账,不能提前声称 applied。
|
||||
- 两个队列没有统一顺序:定义同一任务执行与 pause/stop 交错、重复/迟到/重启、过期、DLQ 恢复时的结果和原 ID;stop 墓碑不能因普通清理失效。若无法给出这些语义的新版契约、SaaS 签收及故障测试,则**保持现有合同,不启用新路由、不开放该容量模型的业务准入**。
|
||||
|
||||
## 5. Broker 满载与发布端责任
|
||||
|
||||
- 分别为执行、控制队列及 DLQ 制定有来源的**条数/字节数上限和磁盘预算**,隔离 broker 上核验队列类型及策略兼容性;超额以拒绝新发布、发送方持久保留原任务/ID为目标,不能使用会丢弃旧队头的默认溢出策略。配置漂移时不 ready,blocked、confirm 丢失、mandatory return 均不得记已交付;不把队列数上限当整个 broker 的磁盘上限。
|
||||
- SaaS 发布端必须有可恢复源记录,发布失败或结果不确定时按原消息身份查询/重试;过了 `not_after` 不能偷偷延长原命令期限或换执行 ID 重拨,需按批准的业务规则终结/另行授权。若 SaaS 不具备上述能力,则总容量只是“把故障位置移给 MQ”,不满足交付条件。
|
||||
- D 自身的结果 outbox 也需按未确认数量、最老年龄及磁盘余量监控;不得为保护入站容量而丢弃 `command.result`、控制或录音事实。只在可证明的健康及水位恢复后重新开启执行准入,避免恢复时突发抢占。
|
||||
|
||||
## 6. 工作包与验收门禁
|
||||
|
||||
| 顺序 / 映射 | 产出与前置 | 可验证的完成条件 |
|
||||
| --- | --- | --- |
|
||||
| B0 事实与预算(W00/W13) | 从真实单租户负载/profile 核定可用并发、活跃任务数/约60秒配置刷新请求量、SIP 全量大小、SQLite/WAL/outbox保留和 broker 容量;确认 SaaS 发布失败与停发责任;缺数写 blocked,不套用旧 DEV/P2 数值当生产预算 | 受批准的容量来源、磁盘余量与告警/恢复条件;现状对照记录,不宣称新方案已运行 |
|
||||
| B1 接口冻结(W01/W05/W12/W08) | 发布**新版本**两条只读 HTTP 配置接口合同及 MQ 业务拓扑/必要 Schema、正反例/哈希;明确每 D 的 UUID+SECRETKEY 受控注入及归属、SIP 批准制品来源、任务内嵌智能体获授权快照、版本/摘要/ETag、最大约60秒生效滞后/到期失败行为、原 `call.execute` 修订引用如何重新授权及并发冻结点。同步修订旧“SQLite 有界等待”“排队 AI 固定版本”和全 MQ 配置规则,撤除旧 AI 配置 MQ 路径而不设兼容回退;保留执行/控制精确绑定与未接纳任务控制屏障、每 D 资源/共享额度份额及旧队列对账,冻结任务/线路时段、期限和旧全局门禁切换;与 SaaS/management 分别核对签收 | 新旧合同不混用、路由预算与 D1/D2/租户隔离、两接口冷启动/条件刷新/失败边界及未接纳新版绑定正反例通过;本阶段文档不是 B1 已完成证据 |
|
||||
| B2 先写失败测试(W05/W08) | 用隔离 RabbitMQ/SQLite 与只读配置接口 Mock 构造满额/宕机、任务/线路交集和跨关窗滞留;覆盖任务/智能体变更后0/60秒、条件读取`304`/新`ETag`、HTTP超时/不可用、任务错归D1/D2、两D同租户额度份额、SIP冷启动/重启/加载失败、任务终结清缓存及修改与接纳竞态;随后将接收和原子额度预留合并到同一事务 | 超量消息不搬入 SQLite;一任务只在归属D接纳、份额总和不超全局;超期缓存或SIP未实际加载不接新呼叫,已接纳快照不变,停/暂停不等缓存;commit/ACK 丢失不重拨,关窗旧消息下一窗口不拨号 |
|
||||
| B3 控制/服务通道(W05/W08/W07/W12) | SaaS 按新版 MQ 业务绑定发布;D 独立消费控制、查询/补传,持久前不 ACK;任务/智能体/SIP 配置由只读 HTTP 拉取,无 MQ 配置结果;对未接纳任务保存控制屏障并在执行接纳/最后许可校验 | 执行队列满时 stop/pause 可受理并正确 applied/reconciling,旧消息不会拨号;`not_found`/CAS/乱序/重启含未知执行得到合同定义结果;不改 OSS 上传事实路径、不保留配置 MQ 回退 |
|
||||
| B4 发布背压与清理(W05/W12/W13) | 设置 broker 限额/拒绝策略,验证 SaaS 原身份保留/重试;加入本地高水位、控制预留空间、outbox 年龄及保留/压缩策略;缓存过期刷新失败不接新执行,任务终结后仅清可再获取的配置缓存 | 消息爆量时 RabbitMQ 不丢旧命令、发布方不丢源任务;DB 满盘或 HTTP 配置失效时停止执行接纳,持久事实仍可追溯;控制若无法持久化明确阻塞,不伪报 applied |
|
||||
| B5 本地合并验证(W13/W14;W16 后续) | 同一基线跑 HTTP 两接口合同/缓存校验、MQ 业务合同、broker 故障/重启、DB 恢复、期限、任务×线路时段、旧配置 MQ 路径停用和持续负载;多租户公平另按 W16 开展 | 覆盖现有 C05/C11、S01–S03/S08–S15/S17/S19–S22/S24/S25/S27 的适用子场景;验证双时段边界、配置最多约60秒滞后/过期拒绝、未接纳任务跨窗口、最后发起和控制关窗并更新验收基线;本地证据独立成文,不把它写成生产/SaaS 签收或 W16 通过 |
|
||||
|
||||
代码阶段坚持 TDD,并执行项目要求的格式、`go vet ./...`、`go test -race ./...`、`go build ./...`、合同/Proto/本地 MQ 集成检查;按现有覆盖率门禁验收。B1 以前只做事实收集及方案澄清,不编辑现有冻结合同或抢先启用分流。每一批代码合并后重新验证同一套执行、控制和 outbox 故障路径。
|
||||
|
||||
## 7. 不做与待批准事项
|
||||
|
||||
本计划**不授权**真实 SaaS/生产 broker 修改、部署、真实外呼或费用;不实现多租户公平、跨 Cell/多 D 动态协调、另一个数据库、Redis 兜底、配置 HTTP↔MQ 故障回退或重拨。计划实施前仍需明确:SaaS 对两只读接口、任务内嵌智能体的授权/版本及最多约60秒延迟、原命令引用如何解除/冻结和新版控制语义的签收;management 对 SaaS SIP 已批准只读分发与实际加载回执的签收;每 D 独占资源及共享额度份额、真实容量预算、执行与控制队列上线/排空顺序、完成任务保留期限及源记录保障。任一关键前置缺失,相应工作包标记 blocked;不能以“暂停消费”代替已达成控制屏障。
|
||||
|
||||
依据:[现行 MQ 机器合同](../../contracts/upstream/v1/mq.schema.json)、[事件正文](../../contracts/upstream/v1/event-payloads.schema.json)、[第三方顺序说明](../thirds/第三方对接事件与请求消费顺序_v0.1.md)、[总体方案 §6.1–6.2](./Go重写方案_v0.3.md)、[验收 §4/§9](../acceptance/验证与切换验收_v0.3.md)、[唯一机读拓扑](../../contracts/upstream/v1/mq-topology.json)。
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## 1. 状态、权限与使用方式
|
||||
|
||||
**状态:D01–D10原方向已确认,旧W01契约和W02 Proto已有项目内证据;本轮用户确认SaaS↔Dispatcher全MQ,受影响G0的项目内部分已按[计划§1.2/§8.2](../plan-0918.md)重新验证。** SaaS与D之间所有请求、响应和事件禁止HTTP,每个D具备全局唯一ID和独立接收Topic/队列;V1 Schema、拓扑和关联已有本地包及RabbitMQ证据,外部发布/签收另计。OSS补充确认:配置存于D配置文件,A向D领取固定15分钟临时上传TOKEN后直传;SaaS不再提供OSS配置/TOKEN,上传完成以recording.uploaded可靠入队为界,不等待SaaS verified或OSS ID,D不转发文件。原HTTP方向被本修订替代,不把旧证据覆盖到新设计。
|
||||
**状态:D01–D10原方向已确认,旧W01契约和W02 Proto已有项目内证据;本轮用户确认SaaS↔Dispatcher全MQ,受影响G0的项目内部分已按[归档旧计划§1.2/§8.2](../archive/plan-0918.md)重新验证;本轮两只读 HTTP 配置目标见[新计划](../plan-config-read-v0.1.md),旧验证不覆盖新接口。** SaaS与D之间所有请求、响应和事件禁止HTTP,每个D具备全局唯一ID和独立接收Topic/队列;V1 Schema、拓扑和关联已有本地包及RabbitMQ证据,外部发布/签收另计。OSS补充确认:配置存于D配置文件,A向D领取固定15分钟临时上传TOKEN后直传;SaaS不再提供OSS配置/TOKEN,上传完成以recording.uploaded可靠入队为界,不等待SaaS verified或OSS ID,D不转发文件。原HTTP方向被本修订替代,不把旧证据覆盖到新设计。
|
||||
|
||||
本项目已创建并验证自己的 Go module、W01 bundle、W02 Proto/stubs、RPC/mTLS 和本地 Mock 测试;未修改父项目权威来源、字段索引或生成产物,未访问真实供应商或创建云资源。文件名保留“提案”以保持链接稳定,不代表还需重复审批已确认方向。
|
||||
|
||||
@@ -47,7 +47,7 @@ P2 增加多个同时活跃租户的等权轮询、额度不足跳过、公平
|
||||
|
||||
### 2.3 本轮MQ-only补充(已确认方向,消息细节待冻结)
|
||||
|
||||
本轮补充纳入D03/D04/D07/D09/D10及通信设计GAP-10,不新增运行验收编号。完整约束见[SaaS↔D契约](../contracts/saas-dispatcher.md):每个D全局唯一ID、独立Topic/接收队列、请求/响应固定原D与租户、持久inbox/outbox、错目标/重复身份/重投/乱序/超时/重启恢复;新路由长度须重算,不能照搬旧224字节租户预算。ID生命周期、Topic/绑定、消息枚举/字段/关联、错误/期限未冻结前不实现猜测协议。
|
||||
本轮补充纳入D03/D04/D07/D09/D10及通信设计GAP-10,不新增运行验收编号。现行完整约束以[MQ Schema](../../contracts/upstream/v1/mq.schema.json)和[拓扑](../../contracts/upstream/v1/mq-topology.json)为准,第三方步骤见[事件顺序说明](../thirds/第三方对接事件与请求消费顺序_v0.1.md):每个D全局唯一ID、独立Topic/接收队列、请求/响应固定原D与租户、持久inbox/outbox、错目标/重复身份/重投/乱序/超时/重启恢复;新路由长度须重算,不能照搬旧224字节租户预算。ID生命周期、Topic/绑定、消息枚举/字段/关联、错误/期限未冻结前不实现猜测协议。
|
||||
|
||||
P1仍为单节点/单Agent/单Cell/单租户/单活D,下文沿用的早期两Cell/两Agent全量矩阵仅为后续目录,不是本轮门禁。新增D1/D2本地消息fixture只验证定向隔离,不授权多D业务调度、HA或共享配额。旧OpenAPI/只读索引和历史证据原样保存;新MQ生产链不得保留SaaS↔D HTTP;D提供上传TOKEN的职责保留,项目完成边界为recording.uploaded可靠入队,不等待SaaS对象处理。
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ Dispatcher配置包含MQ/SaaS受控引用、SQLite路径、两个Agent Endpoint
|
||||
|
||||
### 3.2 权威来源与独立拆仓
|
||||
|
||||
**本轮用户已确认SaaS↔Dispatcher全MQ:双方不再有任何HTTP请求/回调;每个D有全局唯一ID和独立接收Topic/队列。** 执行、控制、查询、补传、AI配置/授权及上传完成事实均经MQ;上传不申请SaaS会话、不等待verified/OSS ID,详见[SaaS↔D契约](../contracts/saas-dispatcher.md)与[计划§1.2/§8.2](../plan-0918.md)。旧HTTP和旧租户路由已删除;新Schema/拓扑/身份生命周期/关联已在项目内唯一 V1 包和本地证据中冻结,旧源包及哈希不改。
|
||||
**本轮用户已确认SaaS↔Dispatcher全MQ:双方不再有任何HTTP请求/回调;每个D有全局唯一ID和独立接收Topic/队列。** 执行、控制、查询、补传、AI配置/授权及上传完成事实均经MQ;上传不申请SaaS会话、不等待verified/OSS ID,详见[现行 MQ Schema](../../contracts/upstream/v1/mq.schema.json)、[第三方事件顺序](../thirds/第三方对接事件与请求消费顺序_v0.1.md)与[归档旧计划§1.2/§8.2](../archive/plan-0918.md);这是现行已发布 MQ-only 合同的历史依据,后续仅配置改为两只读 HTTP 接口的目标见[新计划](../plan-config-read-v0.1.md),尚未成为运行事实。旧HTTP和旧租户路由已删除;新Schema/拓扑/身份生命周期/关联已在项目内唯一 V1 包和本地证据中冻结,旧源包及哈希不改。
|
||||
|
||||
现有上游权威是《SaaS交互_OpenAPI与MQ契约规划_v0.1.md》(正文 v1.0)及经核验的发布产物。现有产物包括 `mq.schema.json`、`executor.openapi.yaml`、`cell-agent.openapi.yaml`、AI 配置及 SIP 管理相关契约。文件存在不代表完整覆盖:P0 必须逐条核对正文、Schema、状态语义和实现差异。
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
## 本次归档
|
||||
|
||||
- [`plan-0918.md`](plan-0918.md):原 W00–W16 项目总计划,按仓库 HEAD **逐字恢复**后归档。归档原因:用户要求改用[本轮新计划](../plan-config-read-v0.1.md)单写 F00–F06 状态;旧计划的批准和本地证据仍可追溯,不能代签新接口通过。为保留原文,旧计划内相对链接仍按归档前位置书写,不作为现行导航。
|
||||
- `evidence/20260920-acceptance-status.md`:MQ-only 修订前的验收状态摘要。
|
||||
- `evidence/20260920-local-p1-acceptance.md`:修订前的单节点本地 P1 汇总,不能覆盖当前 MQ-only 修订。
|
||||
- `evidence/20260920-local-oss-mq-integration.md`:旧 OSS/MQ 验证,仍保留 `recording.ready` 等历史事实。
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
# 两条只读配置接口:任务、智能体、SIP 字段与返回结构 v0.1(项目提案)
|
||||
|
||||
**状态:项目自定义草案,非 SaaS 已有接口/实际 JSON、非已发布契约、非实现/验收。** 用户同意:截图可见的业务含义先映射为**项目定义的字段名**;截图没有但需求明确的结构由本项目设计。即使字段名与现有项目 Schema 或历史 OpenAPI 相同,也**不能**据此声称它是当前 SaaS 页面原有的后端键。SaaS 和 management 在 W01 签收前不得据此开始真实配置发布/拨号。
|
||||
|
||||
## 1. 来源、交付边界
|
||||
|
||||
- **P = 页面观察:**[SaaS 截图分析](../references/saas-page-snapshot-analysis.md) §2–4;只证明表单/列表可见,尤其§7的 **MQ 配置建议是已被新 HTTP 方向取代的历史方案**,不作为新合同。
|
||||
- **C = 本项目现有合同:**[AI 配置](../../contracts/upstream/v1/ai-config.schema.json)、[静态 Cell/SIP 制品](../../contracts/upstream/v1/static-cell-artifact.schema.json)及[当前 MQ 消息](../../contracts/upstream/v1/mq.schema.json)。字段语义可复用,但**不是 SaaS 当前 HTTP 响应证据**。旧 [OpenAPI/MQ 只读索引](../references/OpenAPI与MQ字段索引_v0.1.md) 也仅为历史参考。
|
||||
- **N = 新项目字段:**任务每周多时段/排除日期、SIP 线路时段、任务与单 D 绑定、缓存/版本/错误返回等,由本提案定义;实际 SaaS 接口不存在已验证响应。
|
||||
|
||||
交付物:[机器可读响应草案](config-read-v0.1.schema.json)、[mock SIP 成功示例](examples/config-read-sip-v0.1.json)、[mock 任务成功示例](examples/config-read-task-v0.1.json)。两条接口均为 **SaaS→Dispatcher 的只读 HTTP 配置读取**;MQ 继续单独承担呼叫、控制、查询、上传事实与业务结果,不留旧 AI/SIP 配置 MQ 回退。**接口路径、UUID+SECRETKEY 承载位置、错误码的准确枚举、哈希计算规范及 SaaS/management 实际来源仍需 W01 冻结**,本文件不冒充外部权威发布物。
|
||||
|
||||
## 2. 请求与共同响应
|
||||
|
||||
Dispatcher 使用其**全局唯一 UUID**和部署受控的 **SECRETKEY**,仅查询自己管理的资源分区和归属任务;不记录完整密钥或返回配置中的提示词。接口只读、无启动/停止/修改副作用。准确 URL、请求参数/头、SaaS 对任务归属的核验、密钥轮换时序由双方签收;本文仅固化响应的**项目字段草案**,不推测 SaaS 已有 API 路径。
|
||||
|
||||
| 请求 | `200` 返回类型 | 何时读取 | `304` 与错误 |
|
||||
| --- | --- | --- | --- |
|
||||
| SIP 全量配置 | `resource=sip_config`,一个 D 资源分区的已批准完整快照 | 新 D/重启先取齐并核对 Agent/Asterisk 精确加载;运行中约每 60 秒核对版本 | 未变且仍获批准可 `304`(空响应体);读取错误/到期停止新执行准入,旧活动通话依原快照排空 |
|
||||
| 单任务配置(内含智能体) | `resource=task_config`,归属 D 的单任务有效配置和已授权智能体快照 | 有待接纳呼叫时获取,活跃任务缓存约 60 秒、到期主动复核,不逐呼下载整份配置 | 未变且授权仍有效可 `304`;失败/过期不以旧缓存放行新执行;已接纳执行仍用原快照 |
|
||||
|
||||
`200` 响应中的 `schema_version` 固定 `config-read.v0.1`,`resource` 区分两种结构,`dispatcher_id` 必须等于请求 D;HTTP `ETag` 标识**完整有效决策**,并非某个 UI 表单的草稿版本。`If-None-Match` 命中时 `304` **没有 JSON 响应体**;只有此前的批准状态、任务归属及 AI 授权仍足以覆盖下一次缓存有效期,才可延长本地缓存,否则返回 `200` 新快照或明确失败。SaaS 实际更新到 D 允许最多约 60 秒延迟;一旦缓存到期且刷新失败,**不可无限期沿用旧版本发起新呼叫**。MQ 停/暂停、opt-out 不等这 60 秒。已接纳/已接通呼叫固定自己的快照,不因本地配置缓存过期而变更。
|
||||
|
||||
非 `200/304` 返回 `resource=error`、`error.code`、`error.message` 的脱敏 JSON(HTTP 状态及 code 逐项待 SaaS 确认);不得将错误吞成旧配置/空任务。原始 `call.execute` 的 `task_revision`、`agent_version_id`、`route_policy_id` 与后续新版本可能不同;如何让未接纳执行合法采用新版本、变更与接纳并发时何时冻结版本,**尚需版本化 MQ/HTTP 合同约定,不可静默改写原命令**。
|
||||
|
||||
## 3. 任务成功响应:字段与来源
|
||||
|
||||
响应中的英文键**全部是本项目提议的返回键**,不是从截图抓到的 SaaS JSON。P/C/N 只说明其业务含义的依据:
|
||||
|
||||
| 返回位置 | 类型 / 是否必有 | 含义及来源 |
|
||||
| --- | --- | --- |
|
||||
| `dispatcher_id`, `tenant_key`, `task_id`, `task_revision` | UUID v4、原值字符串、字符串、正整数;必有 | C:当前命令及每 D/租户身份;N:任务固定归属一个 D,SaaS 必须持久保存 `(tenant_key,task_id)→dispatcher_id`;不得跨 D 投递。`tenant_key` 需另验证 **≤196 UTF-8 字节**及路由段边界,Schema 的字符数不是字节数。 |
|
||||
| `status` | `running/paused/stopped/finished`;必有 | P:页面可见启停状态;N:面向 D 的状态枚举是本项目暂定,不承诺与页面/实际 API 状态值同名。停/暂停需配合 MQ 控制屏障,不能只靠缓存。 |
|
||||
| `name`, `group_id` | 非空字符串、字符串或 `null`;必有 | P:任务名称/所属分组。显示信息不参与拨号许可;`null` 与空字符串不混同。 |
|
||||
| `max_concurrent_calls` | 正整数;必有 | P:任务并发。仅限制**该任务**,另须服从 D 的租户份额、供应商、Cell 和 AI 配额。截图里的“5”是某次输入,不是默认值。 |
|
||||
| `route_policy_id`, `allowed_trunk_ids[]` | 非空字符串、至少一条线路 ID;必有 | C:`route_policy_id` 已在命令;P:选择启用线路。N:`allowed_trunk_ids` 是本项目定义的白名单,须与已加载 SIP 制品的 `trunk_id` 对齐;不能把任务页面“每条线路数量”直接猜成运营商并发上限。 |
|
||||
| `schedule.time_zone`, `starts_at`, `ends_at` | 固定 `Asia/Shanghai`、带偏移时间或 `null`;必有 | P:任务起止时间;N:三字段格式/无值约定。时间约束与星期段、排除日期、线路时段**同时成立**。 |
|
||||
| `schedule.weekly_windows` | 七个星期键各为可空的时间段数组;必有 | P:周一至周日网格、同日多个时段;N:`{start,end}` 用 `HH:MM`,左闭右开,`start < end`,跨午夜拆到次日,不假定 UI 已有这个 JSON 结构。空数组=当天不可呼。 |
|
||||
| `schedule.excluded_dates[]` | 不重复的 `YYYY-MM-DD` 数组;必有,可为空 | N:用户新增的可选多日期排除(截图**没有**此字段)。日期按 Asia/Shanghai 判断并优先于星期段;真实日期、时段排序/重叠与边界须在业务校验中处理。 |
|
||||
| `agent.agent_version_id`, `content_sha256`, `config` | ID、64位小写 hex、严格 AI 对象;必有 | P:任务选择 AI 模型/智能体;C:现有不可变 `agent_version_id`、`content_sha256` 和[AI Schema](../../contracts/upstream/v1/ai-config.schema.json)。`config` 原样遵守该现有 Schema,不把截图未覆盖参数偷塞 `metadata`。 |
|
||||
| `agent.authorization_id`, `authorization_expires_at` | 非空 ID、带偏移时间;必有 | C:当前 MQ AI 授权含关联 ID 和有效期;N:嵌入任务 HTTP 响应的承载位置新设计,过期不可新接纳。 |
|
||||
|
||||
`agent.config` 当前可承载的**运行字段**:`mode`;ASR 的 `provider_ref/model/language/interim/input/timeout_ms`;LLM 的 `provider_ref/credential_ref/model/temperature/max_tokens/timeout_ms`;`prompt.text/allowed_variables/max_bytes`;TTS 的 `provider_ref/credential_ref/model/voice/speed/format/timeout_ms`;`conversation.opening/allow_interrupt/silence_timeout_ms/max_duration_ms/max_turns/sentence_max_chars/max_pending_audio_chunks` 等以**现有 AI Schema 本身为准**。`asr_only` 与 `full_ai` 两种模式均须按 Schema/SDK 能力分别校验;提示词不得出现在示例或日志中的真实用户文本。`agent.agent_version_id` 必须等于 `agent.config.agent_version_id`;`content_sha256` 的摘要生成规范/与现有 AI 授权摘要对齐前不可声称验签通过。
|
||||
|
||||
### 3.1 页面观察但不作为 D 运行字段
|
||||
|
||||
| 原页面可见项 | 归属判断 / 暂不返回原因 |
|
||||
| --- | --- |
|
||||
| 智能体名称/描述、草稿/提交、文字/语音/线路测试 | SaaS 管理页面元数据/测试入口,不能代替 `agent_version_id` 的已发布运行快照。 |
|
||||
| 提示词编辑器工具、独立开场白、挂断触发/结束语 | `prompt.text` 与 `conversation.opening` 可映射当前合同;挂断条件与结束语尚无当前严格 AI 字段,不能猜到通话控制里。 |
|
||||
| ASR 页面 PCM/Opus/AAC/OGG/WAV、标点、去语气词、单句时长 | 当前合同支持的输入为 `pcm_s16le` 等已定义值;其它编码及三个开关/时长需先验证媒体和 SDK,并修订 GAP-09/新合同。 |
|
||||
| LLM 对话模式、Top-P、重复惩罚、Top-K、随机种子、思考/流式开关 | 模型/温度等已有字段可用;其余没有获批准的运行字段及参数能力 PoC,**不进入 HTTP 的 `agent.config`**,不静默忽略后宣称已生效。 |
|
||||
| TTS 公共/个人音色、情绪、音调、MP3/WAV 选项、试听 | 已有 `voice/speed/format` 可按实际能力承载;其它参数和试听不直接映射现有 Agent 可执行配置。 |
|
||||
| 话后分析提示词及 A–F 意向规则 | 页面可见但当前 AI 快照没有对应执行和结果契约;仍由 SaaS 负责或另行定义,不能伪装成外呼 Agent 参数。 |
|
||||
| 任务拨打顺序/时间间隔、自动重呼及次数/条件、结束动作、黑名单组、备注 | P:页面有这些项;本接口只返回 D **当前已获授权且有实现责任**的准入信息。排序、间隔、名单/运营策略应由 SaaS 明确负责;自动重呼不得伪装成 MQ 重投或 D 的自动再拨。 |
|
||||
| 导入号码、号码列表、统计、通话记录、意向图表 | 属单次 `call.execute`/SaaS 展示与运营事实,不能一次塞进“任务配置”返回;不能从截图冻结数据页的 API 列名和返回结构。 |
|
||||
|
||||
这些字段**已在字段盘点中固化存在性和缺口**,不是声称 SaaS 已有相应返回键。若用户明确要求其中某项由 D/Agent 执行,先核实上游模型/SDK、增加获批准的严格字段及正反例,不在新接口中以 `metadata` 或 raw JSON 穿透。
|
||||
|
||||
## 4. SIP 成功响应:字段与来源
|
||||
|
||||
| 返回位置 | 类型 / 是否必有 | 含义及来源 |
|
||||
| --- | --- | --- |
|
||||
| `dispatcher_id`, `revision`, `approved_at`, `snapshot_sha256` | UUID v4、正整数、带偏移时间、64位小写 hex;必有 | N:SaaS 只读分发给指定 D 的完整获批版本;`snapshot_sha256` 意图覆盖 HTTP SIP 全量,包括线路补充信息。**精确哈希/规范化规则待签收;示例摘要是占位,不是实算/验收证据。** |
|
||||
| `artifact` | 对象;必有 | C:复用[现有静态 Cell/SIP Schema](../../contracts/upstream/v1/static-cell-artifact.schema.json),含 `artifact_id/source_release/source_digest/approval_reference/cell_id/revision/config_sha256/mode/allowed_targets/trunks`;可有 `media_profiles/ari/media/recording/load_evidence`。这些是**本项目交接字段**,非截图证明的 SaaS 原字段。 |
|
||||
| `artifact.trunks[]` | 严格数组;必有 | C:`trunk_id/provider_id/egress_pool_id/codec/caller_profile_ids/dial_prefix/enabled/media_profile_id`,及可选 `sip_endpoint_ref/credential_ref`。前缀只用于该线路;主叫保留原值包括字母;现有 PCMA 方向仍须实际线路验证。 |
|
||||
| `trunk_details[].trunk_id` | 字符串;必有 | N:与 `artifact.trunks[].trunk_id` 精确一一匹配,保证全量无漏行、无虚构备用线路;仅靠 JSON Schema 不足以判断跨数组一致性。 |
|
||||
| `server_host`, `server_port`, `transport` | 主机/端口、`udp/tcp/tls/null`;必有 | N:补足“全量 SIP”需要的实际对端;页面仅显示任务选线路,**没有管理线路完整配置截图**。供应商传输未知时为 `null`,绝不擅自按 UDP 默认发起 real。mock 示例地址非真实供应商。 |
|
||||
| `auth_mode`, `registration_required` | `ip/digest/none/null`、`true/false/null`;必有 | N:供应商鉴权/注册未知时 `null`,不得把主叫号当 Digest 账号;real 放行前须供应商/management 批准并验证。不返回密码、私钥或真实 TOKEN。 |
|
||||
| `max_concurrent_calls` | 正整数或 `null`;必有 | N:线路/供应商获批份额,`null` 表示未知(real 必须拒绝新准入),不能拿截图任务“线路数量”猜限额;跨 D 份额总和须受源配额约束。 |
|
||||
| `caller_profiles[]` | `{caller_profile_id, caller_id}` 数组;必有 | C:旧制品仅有 profile ID;N:全量响应映射 profile→原样主叫标识。From/PAI 具体映射仍待供应商确认;示例主叫是 mock,不是生产号。 |
|
||||
| `schedule.time_zone`, `weekly_windows` | `Asia/Shanghai`,七天逐日零或多个时段;必有 | N:SIP 线路允许拨打时段(截图未提供),与任务时段相交;无允许段则不能呼叫。时间跨午夜拆到次日,结果还受 Agent 最后拨号门禁约束。 |
|
||||
|
||||
静态制品中的端点**引用**与此提案补充的对端**值**属于同一获批版本;若 SaaS 与 management 并非同一配置来源,必须证明它们同步一致,且 Agent/Asterisk 实际加载的版本/摘要匹配。`artifact.load_evidence` 为 `null` 只表示 HTTP 获得配置,**不代表 Asterisk 已加载**;D 必须另从实际执行侧核验。样例 `mode=mock`、`transport/auth_mode/registration_required/max_concurrent_calls=null` 是故意保留的供应商待确认项;**不满足 real 放行**。只提供SaaS读接口却不能取得已批准的端点及线路时段,不得声称已经提供了完整 SIP 配置。
|
||||
|
||||
## 5. 需冻结的语义与验收前置
|
||||
|
||||
1. **来源:**确认 SaaS 的任务、智能体是该服务的权威配置;management 仍是唯一 SIP 编辑/审批方。确认 SaaS 分发的是已批准制品与线路补充字段同一版本,不能出现两份可写配置。
|
||||
2. **身份和响应:**D 的 UUID+SECRETKEY 只能读取获分配的资源分区及任务;不在日志/示例保存真实密钥。精确 HTTP 路径、请求参数/头、鉴别方式、状态码、`ETag` 粒度与生效时间由 SaaS 正式签收。
|
||||
3. **窗口与版本:**缓存成功核验起约 60 秒;SaaS 变更对未接纳呼叫最多约 60 秒延迟,过期刷新失败停止新准入,已接纳保留原快照。更新/SIP 加载期间停执行队列,不停控制 MQ;停/暂停与 opt-out 不等缓存。`304` 不得延长已撤销或即将过期的 AI 授权。跨日窗口、重叠段、当日排除、时间边界以及任务与线路交集需在业务校验并在新合同定稿时冻结。
|
||||
4. **一致性:**`agent_version_id`、摘要、AI 授权一致且有效;任务归属/修订/route policy 与已发布命令如何重新授权;`artifact.trunks` 与 `trunk_details` 一一对应;线路 status、主叫、前缀、媒体、线路/租户/供应商额度来源和 Agent/Asterisk 实际加载不可依赖 JSON Schema 单独判断。供应商未知传输/鉴权/注册不得默认允许 real。
|
||||
5. **退出与过渡:**新接口上线前现行 MQ-only/单 D/固定时段/静态 SIP 仍有效。切到新版本后配置只走 HTTP,旧 `ai.config.request/result` 停用,不做 HTTP→MQ 回退;业务 MQ 正常运行。多 D 任务归属/共享额度份额、旧命令/缓存/恢复记录和控制屏障须单独验证;任务结束只删除配置缓存,不删除未决执行与消息事实。
|
||||
|
||||
**验证状态:**本地 JSON Schema 草案可校验两个 mock `200` 示例及非法样例;它**不能**证明真实 SaaS 接口字段名、管理平台签收、hash 规范、Agent SDK 映射、SIP 实际加载或任何生产外呼验收。
|
||||
@@ -0,0 +1,153 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://go-sip.local/contracts/proposals/config-read-v0.1.schema.json",
|
||||
"title": "PROPOSAL: project-defined SaaS read-only configuration responses; NOT a published SaaS contract",
|
||||
"oneOf": [
|
||||
{"$ref": "#/$defs/sip_response"},
|
||||
{"$ref": "#/$defs/task_response"},
|
||||
{"$ref": "#/$defs/error_response"}
|
||||
],
|
||||
"$defs": {
|
||||
"sip_response": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["schema_version", "resource", "dispatcher_id", "revision", "snapshot_sha256", "approved_at", "artifact", "trunk_details"],
|
||||
"properties": {
|
||||
"schema_version": {"const": "config-read.v0.1"},
|
||||
"resource": {"const": "sip_config"},
|
||||
"dispatcher_id": {"$ref": "#/$defs/dispatcher_id"},
|
||||
"revision": {"type": "integer", "minimum": 1},
|
||||
"snapshot_sha256": {"$ref": "#/$defs/sha256"},
|
||||
"approved_at": {"type": "string", "format": "date-time"},
|
||||
"artifact": {"$ref": "https://go-sip.local/contracts/v1/static-cell-artifact.schema.json"},
|
||||
"trunk_details": {
|
||||
"type": "array", "minItems": 1, "maxItems": 32,
|
||||
"items": {"$ref": "#/$defs/trunk_details"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"task_response": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["schema_version", "resource", "dispatcher_id", "tenant_key", "task_id", "task_revision", "status", "name", "group_id", "max_concurrent_calls", "route_policy_id", "allowed_trunk_ids", "schedule", "agent"],
|
||||
"properties": {
|
||||
"schema_version": {"const": "config-read.v0.1"},
|
||||
"resource": {"const": "task_config"},
|
||||
"dispatcher_id": {"$ref": "#/$defs/dispatcher_id"},
|
||||
"tenant_key": {"type": "string", "minLength": 1, "maxLength": 196, "$comment": "Also validate <=196 UTF-8 bytes and routing word boundaries using the project-owned tenant contract."},
|
||||
"task_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"task_revision": {"type": "integer", "minimum": 1},
|
||||
"status": {"enum": ["running", "paused", "stopped", "finished"]},
|
||||
"name": {"type": "string", "minLength": 1, "maxLength": 256},
|
||||
"group_id": {"type": ["string", "null"], "maxLength": 128},
|
||||
"max_concurrent_calls": {"type": "integer", "minimum": 1},
|
||||
"route_policy_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"allowed_trunk_ids": {"type": "array", "minItems": 1, "maxItems": 32, "uniqueItems": true, "items": {"type": "string", "minLength": 1, "maxLength": 128}},
|
||||
"schedule": {"$ref": "#/$defs/task_schedule"},
|
||||
"agent": {"$ref": "#/$defs/agent"}
|
||||
}
|
||||
},
|
||||
"error_response": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["schema_version", "resource", "error"],
|
||||
"properties": {
|
||||
"schema_version": {"const": "config-read.v0.1"},
|
||||
"resource": {"const": "error"},
|
||||
"error": {
|
||||
"type": "object", "additionalProperties": false,
|
||||
"required": ["code", "message"],
|
||||
"properties": {
|
||||
"code": {"type": "string", "pattern": "^[a-z][a-z0-9_]{0,63}$"},
|
||||
"message": {"type": "string", "minLength": 1, "maxLength": 256}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dispatcher_id": {
|
||||
"type": "string", "format": "uuid",
|
||||
"pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
|
||||
},
|
||||
"sha256": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
|
||||
"trunk_details": {
|
||||
"type": "object", "additionalProperties": false,
|
||||
"required": ["trunk_id", "server_host", "server_port", "transport", "auth_mode", "registration_required", "max_concurrent_calls", "caller_profiles", "schedule"],
|
||||
"properties": {
|
||||
"trunk_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"server_host": {"type": "string", "minLength": 1, "maxLength": 255},
|
||||
"server_port": {"type": "integer", "minimum": 1, "maximum": 65535},
|
||||
"transport": {"enum": ["udp", "tcp", "tls", null]},
|
||||
"auth_mode": {"enum": ["ip", "digest", "none", null]},
|
||||
"registration_required": {"type": ["boolean", "null"]},
|
||||
"max_concurrent_calls": {"type": ["integer", "null"], "minimum": 1},
|
||||
"caller_profiles": {
|
||||
"type": "array", "minItems": 1, "maxItems": 32,
|
||||
"items": {
|
||||
"type": "object", "additionalProperties": false,
|
||||
"required": ["caller_profile_id", "caller_id"],
|
||||
"properties": {
|
||||
"caller_profile_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"caller_id": {"type": "string", "minLength": 1, "maxLength": 64}
|
||||
}
|
||||
}
|
||||
},
|
||||
"schedule": {"$ref": "#/$defs/weekly_schedule"}
|
||||
}
|
||||
},
|
||||
"agent": {
|
||||
"type": "object", "additionalProperties": false,
|
||||
"required": ["agent_version_id", "content_sha256", "authorization_id", "authorization_expires_at", "config"],
|
||||
"properties": {
|
||||
"agent_version_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"content_sha256": {"$ref": "#/$defs/sha256"},
|
||||
"authorization_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"authorization_expires_at": {"type": "string", "format": "date-time"},
|
||||
"config": {"$ref": "https://go-sip.local/contracts/v1/ai-config.schema.json"}
|
||||
}
|
||||
},
|
||||
"task_schedule": {
|
||||
"type": "object", "additionalProperties": false,
|
||||
"required": ["time_zone", "starts_at", "ends_at", "weekly_windows", "excluded_dates"],
|
||||
"properties": {
|
||||
"time_zone": {"const": "Asia/Shanghai"},
|
||||
"starts_at": {"type": ["string", "null"], "format": "date-time"},
|
||||
"ends_at": {"type": ["string", "null"], "format": "date-time"},
|
||||
"weekly_windows": {"$ref": "#/$defs/weekly_windows"},
|
||||
"excluded_dates": {
|
||||
"type": "array", "uniqueItems": true,
|
||||
"items": {"type": "string", "format": "date"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"weekly_schedule": {
|
||||
"type": "object", "additionalProperties": false,
|
||||
"required": ["time_zone", "weekly_windows"],
|
||||
"properties": {
|
||||
"time_zone": {"const": "Asia/Shanghai"},
|
||||
"weekly_windows": {"$ref": "#/$defs/weekly_windows"}
|
||||
}
|
||||
},
|
||||
"weekly_windows": {
|
||||
"type": "object", "additionalProperties": false,
|
||||
"required": ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"],
|
||||
"properties": {
|
||||
"monday": {"$ref": "#/$defs/windows"},
|
||||
"tuesday": {"$ref": "#/$defs/windows"},
|
||||
"wednesday": {"$ref": "#/$defs/windows"},
|
||||
"thursday": {"$ref": "#/$defs/windows"},
|
||||
"friday": {"$ref": "#/$defs/windows"},
|
||||
"saturday": {"$ref": "#/$defs/windows"},
|
||||
"sunday": {"$ref": "#/$defs/windows"}
|
||||
}
|
||||
},
|
||||
"windows": {"type": "array", "items": {"$ref": "#/$defs/window"}},
|
||||
"window": {
|
||||
"type": "object", "additionalProperties": false,
|
||||
"required": ["start", "end"],
|
||||
"properties": {
|
||||
"start": {"type": "string", "pattern": "^(?:[01][0-9]|2[0-3]):[0-5][0-9]$"},
|
||||
"end": {"type": "string", "pattern": "^(?:(?:[01][0-9]|2[0-3]):[0-5][0-9]|24:00)$"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema_version": "config-read.v0.1",
|
||||
"resource": "error",
|
||||
"error": {
|
||||
"code": "not_assigned",
|
||||
"message": "Task is not assigned to this Dispatcher."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"schema_version": "config-read.v0.1",
|
||||
"resource": "sip_config",
|
||||
"dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6",
|
||||
"revision": 1,
|
||||
"snapshot_sha256": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
|
||||
"approved_at": "2026-09-21T08:00:00+08:00",
|
||||
"artifact": {
|
||||
"artifact_id": "artifact-cell-mock-1",
|
||||
"source_release": "mock-release-1",
|
||||
"source_digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"approval_reference": "mock-approval-1",
|
||||
"cell_id": "cell-mock",
|
||||
"revision": 1,
|
||||
"config_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
"mode": "mock",
|
||||
"allowed_targets": ["15003164745", "15830461047"],
|
||||
"trunks": [{
|
||||
"trunk_id": "trunk-mock",
|
||||
"provider_id": "provider-mock",
|
||||
"egress_pool_id": "egress-mock",
|
||||
"codec": "PCMA",
|
||||
"caller_profile_ids": ["caller-profile-mock"],
|
||||
"dial_prefix": "",
|
||||
"enabled": true,
|
||||
"sip_endpoint_ref": "sip-endpoint-mock",
|
||||
"credential_ref": null,
|
||||
"media_profile_id": "pcma-8k"
|
||||
}],
|
||||
"media_profiles": {
|
||||
"pcma-8k": {
|
||||
"format": "alaw",
|
||||
"sample_rate_hz": 8000,
|
||||
"channels": 1,
|
||||
"payload_type": 8
|
||||
}
|
||||
},
|
||||
"load_evidence": null
|
||||
},
|
||||
"trunk_details": [{
|
||||
"trunk_id": "trunk-mock",
|
||||
"server_host": "sip.example.invalid",
|
||||
"server_port": 5060,
|
||||
"transport": null,
|
||||
"auth_mode": null,
|
||||
"registration_required": null,
|
||||
"max_concurrent_calls": null,
|
||||
"caller_profiles": [{
|
||||
"caller_profile_id": "caller-profile-mock",
|
||||
"caller_id": "BD00000000"
|
||||
}],
|
||||
"schedule": {
|
||||
"time_zone": "Asia/Shanghai",
|
||||
"weekly_windows": {
|
||||
"monday": [{"start": "09:00", "end": "20:00"}],
|
||||
"tuesday": [{"start": "09:00", "end": "20:00"}],
|
||||
"wednesday": [{"start": "09:00", "end": "20:00"}],
|
||||
"thursday": [{"start": "09:00", "end": "20:00"}],
|
||||
"friday": [{"start": "09:00", "end": "20:00"}],
|
||||
"saturday": [],
|
||||
"sunday": []
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"schema_version": "config-read.v0.1",
|
||||
"resource": "task_config",
|
||||
"dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6",
|
||||
"tenant_key": "tenant-mock",
|
||||
"task_id": "task-mock",
|
||||
"task_revision": 2,
|
||||
"status": "running",
|
||||
"name": "Mock task",
|
||||
"group_id": null,
|
||||
"max_concurrent_calls": 2,
|
||||
"route_policy_id": "route-mock",
|
||||
"allowed_trunk_ids": ["trunk-mock"],
|
||||
"schedule": {
|
||||
"time_zone": "Asia/Shanghai",
|
||||
"starts_at": "2026-09-21T00:00:00+08:00",
|
||||
"ends_at": null,
|
||||
"weekly_windows": {
|
||||
"monday": [{"start": "09:00", "end": "11:00"}, {"start": "14:00", "end": "18:00"}],
|
||||
"tuesday": [{"start": "09:00", "end": "18:00"}],
|
||||
"wednesday": [{"start": "09:00", "end": "18:00"}],
|
||||
"thursday": [{"start": "09:00", "end": "18:00"}],
|
||||
"friday": [{"start": "09:00", "end": "18:00"}],
|
||||
"saturday": [],
|
||||
"sunday": []
|
||||
},
|
||||
"excluded_dates": ["2026-10-01", "2026-10-02"]
|
||||
},
|
||||
"agent": {
|
||||
"agent_version_id": "agent-version-mock",
|
||||
"content_sha256": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
|
||||
"authorization_id": "auth-mock",
|
||||
"authorization_expires_at": "2026-09-21T18:00:00+08:00",
|
||||
"config": {
|
||||
"agent_version_id": "agent-version-mock",
|
||||
"immutable": true,
|
||||
"mode": "full_ai",
|
||||
"llm": {
|
||||
"provider_ref": "mock",
|
||||
"model": "mock-chat-v1",
|
||||
"temperature": 0.2,
|
||||
"max_tokens": 256,
|
||||
"timeout_ms": 5000
|
||||
},
|
||||
"prompt": {
|
||||
"text": "Mock prompt for an isolated test.",
|
||||
"allowed_variables": [],
|
||||
"max_bytes": 32768
|
||||
},
|
||||
"tts": {
|
||||
"provider_ref": "mock",
|
||||
"model": "mock-tts-v1",
|
||||
"voice": "mock-neutral",
|
||||
"speed": 1.0,
|
||||
"format": {
|
||||
"encoding": "pcm_s16le",
|
||||
"sample_rate_hz": 16000,
|
||||
"channels": 1
|
||||
},
|
||||
"timeout_ms": 5000
|
||||
},
|
||||
"asr": {
|
||||
"provider_ref": "mock",
|
||||
"language": "zh-CN",
|
||||
"input": {
|
||||
"encoding": "pcm_s16le",
|
||||
"sample_rate_hz": 16000,
|
||||
"channels": 1,
|
||||
"sample_width_bytes": 2
|
||||
},
|
||||
"interim": true,
|
||||
"timeout_ms": 5000
|
||||
},
|
||||
"conversation": {
|
||||
"opening": "",
|
||||
"allow_interrupt": true,
|
||||
"silence_timeout_ms": 3000,
|
||||
"max_duration_ms": 120000,
|
||||
"max_turns": 20,
|
||||
"sentence_max_chars": 80,
|
||||
"max_pending_audio_chunks": 32
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
本文件保留现有外部命令/事件与内部职责目录,并明确本轮适用范围:**P1为1 Agent/1 Asterisk/单 Cell、至少3家SIP trunk 的契约与协议 fixture、单租户、静态配置、ASR-only与完整AI双模式;真实 SaaS/MQ 联调、双节点、第二 Cell、第二租户和生产切换延期第二阶段。** 全量目录不等于本轮全部实现;当前只交付设计,运行通过记录另见验收证据。
|
||||
|
||||
- **本轮用户已确认SaaS↔Dispatcher全部交互只经RabbitMQ专用Topic,禁止双方HTTP。每个D有全局唯一ID及独立接收Topic/队列,不能共享队列抢收指定D的消息。** 精确身份/拓扑/消息/关联待新版合同冻结,见[MQ-only契约](saas-dispatcher.md)与[计划§1.2/§8.2](../plan-0918.md)。P1仍单活D;D1/D2仅用于本地路由隔离fixture,不开发多D协调。
|
||||
- **本轮用户已确认SaaS↔Dispatcher全部交互只经RabbitMQ专用Topic,禁止双方HTTP。每个D有全局唯一ID及独立接收Topic/队列,不能共享队列抢收指定D的消息。** 精确身份/拓扑/消息/关联待新版合同冻结,见[现行 MQ 机器契约](../../contracts/upstream/v1/mq.schema.json)及[第三方时序](../thirds/第三方对接事件与请求消费顺序_v0.1.md)与[归档计划§1.2/§8.2](../archive/plan-0918.md)。用户后续批准的**配置只读 HTTP**目标仅见[新计划](../plan-config-read-v0.1.md),尚未替换本现行契约。P1仍单活D;D1/D2仅用于本地路由隔离fixture,不开发多D协调。
|
||||
- 旧外部业务字段以《SaaS交互_OpenAPI与MQ契约规划_v0.1.md》正文v1.0及固定包记录为语义来源;其中HTTP传输和旧租户路由已被MQ-only修订替代。旧OpenAPI/哈希只作对照,不手改源包或只读索引,不把中文MQ语义当已发布字段。
|
||||
- [OpenAPI与MQ字段索引](../references/OpenAPI与MQ字段索引_v0.1.md) 是5份OpenAPI、42个HTTP操作、115个命名组件及2份JSON Schema的只读机器提取快照,记录源哈希,不是第二套手写Schema。
|
||||
- 下文 **“现有契约”** 不允许自行改字段/语义;**“内部草案”** 是待批准的gRPC方法/数据模型,不冒充已有OpenAPI;**“缺口”** 明确阻塞相应实现/验收。
|
||||
@@ -129,7 +129,7 @@ P1从管理批准的静态route_policy/caller_profile选择供应商trunk与获
|
||||
|
||||
## 6. SaaS↔Dispatcher 全MQ交互目录
|
||||
|
||||
旧7条业务HTTP路径及AI GET均不再作为目标接入。其业务语义由新版MQ合同承接;旧[字段索引](../references/OpenAPI与MQ字段索引_v0.1.md)只作只读对照,不手改生成物。下表中文名称不是已获批消息枚举,详见[SaaS↔D契约§5–§6](saas-dispatcher.md)。
|
||||
旧7条业务HTTP路径及AI GET均不再作为目标接入。其业务语义由新版MQ合同承接;旧[字段索引](../references/OpenAPI与MQ字段索引_v0.1.md)只作只读对照,不手改生成物。下表中文名称不是已获批消息枚举,现行消息以[MQ Schema](../../contracts/upstream/v1/mq.schema.json)为准,消费顺序见[第三方说明 §4–§5](../thirds/第三方对接事件与请求消费顺序_v0.1.md)。
|
||||
|
||||
| 业务语义 | MQ请求/响应方向 | 保留的约束 |
|
||||
| --- | --- | --- |
|
||||
@@ -188,7 +188,7 @@ OSS配置/TOKEN来源不属于上述SaaS MQ目录:OSS配置存于D配置文件
|
||||
|
||||
## 7. 内部gRPC公共规则(草案)
|
||||
|
||||
以下为早期内部方法职责草案,不是当前Proto字段权威;W02已交付的Proto/handler事实见[Dispatcher↔Agent契约](dispatcher-agent.md)。本轮MQ异步协调仍需核验,不因已有Unary就宣称端到端完成,也不据本文新增SaaS接口。
|
||||
以下为早期内部方法职责草案,不是当前Proto字段权威;W02已交付的Proto/handler事实仅见[Agent Proto](../../proto/agent/v1/agent.proto);第三方对接文档只说明 SaaS↔D,不定义内部方法。本轮MQ异步协调仍需核验,不因已有Unary就宣称端到端完成,也不据本文新增SaaS接口。
|
||||
|
||||
- 采用官方grpc-go与protobuf,全部Unary;两个角色都可作为受控gRPC客户端/服务端,共用HTTP/2连接池。
|
||||
- 方向认证:Agent只接受受信Dispatcher角色的管理调用;Dispatcher只接受Agent群组证书和有效节点会话。共享证书只证明群组,不证明agent_id。
|
||||
|
||||
@@ -4,15 +4,15 @@
|
||||
|
||||
## 文档职责
|
||||
|
||||
- 方案、契约、决策和 `plan-0918.md`:记录当前应遵循的规则、范围和结论。
|
||||
- 方案、契约、决策和 [`plan-config-read-v0.1.md`](../plan-config-read-v0.1.md):记录本轮应遵循的方向、范围与结论;旧 `plan-0918.md` 已归档。
|
||||
- 本目录:记录某个 commit、制品、环境和命令实际得到的结果、失败边界和阻塞原因。
|
||||
- `docs/archive/evidence/`:保存已被当前基线替代、但仍有追溯价值的历史证据。
|
||||
|
||||
证据不能单独把“方向已确认”升级为“实现完成”,也不能把本地、Mock 或隔离结果升级为真实供应商、生产或容量验收。当前状态以 [`../plan-0918.md`](../plan-0918.md) 的台账和适用契约为准。
|
||||
证据不能单独把“方向已确认”升级为“实现完成”,也不能把本地、Mock 或隔离结果升级为真实供应商、生产或容量验收。本轮状态以[新计划 §8](../plan-config-read-v0.1.md)及适用契约为准;旧 MQ-only 通过范围见[归档原计划](../archive/plan-0918.md)。
|
||||
|
||||
## 当前入口
|
||||
|
||||
- [`../plan-0918.md`](../plan-0918.md):当前状态、范围和下一步的唯一开发台账。
|
||||
- [`../plan-config-read-v0.1.md`](../plan-config-read-v0.1.md):本轮 F00–F06 状态、范围和下一步的唯一开发台账;旧[归档计划](../archive/plan-0918.md)只作历史。
|
||||
- [`20260922-mq-only-local-final.md`](20260922-mq-only-local-final.md):当前 MQ-only 单节点/单 Cell/单租户本地汇总。
|
||||
- [`20260920-scope-amendment.md`](20260920-scope-amendment.md):本轮范围修订事实。
|
||||
- [`20260921-mq-v1-contracts.md`](20260921-mq-v1-contracts.md):MQ-only v1 本地契约验证事实。
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
# Go SIP Agent:配置读取与有界外呼迭代计划 v0.1
|
||||
|
||||
**当前迭代唯一执行入口;状态:字段提案已建立,新 HTTP 合同/实现/验收均未完成。** 本文接替已恢复原始内容并归档的 [plan-0918.md](archive/plan-0918.md)。旧计划只作历史证据/背景,不能把旧 W01–W16 的本地通过数写成新接口、多 Dispatcher 或真实 SaaS 的验收。分项设计见 [有界消费与控制通道设计](architecture/Dispatcher有界接纳与控制通道改造计划_v0.1.md),字段草案见 [配置读取字段/返回结构提案](contracts/config-read-fields-v0.1-proposal.md);分项材料和草案均不替代已发布 Schema。
|
||||
|
||||
## 1. 范围和完成标准
|
||||
|
||||
本次要建立两个**只读 SaaS→Dispatcher HTTP 配置接口**:取得 management 已批准、适用于该 Dispatcher 的完整 SIP 配置;取得归属该 Dispatcher 的单个任务配置(内含获授权的智能体不可变版本/参数)。任务、智能体及 SIP 配置修改后,**已接纳**的执行保持旧快照;尚未接纳的执行允许在不超过约 60 秒的已核验缓存期内使用旧批准版本。呼叫、控制、查询、补传、`recording.uploaded` 和业务结果仍唯一经 RabbitMQ;不保留旧 AI/SIP 配置 MQ 回退,也不复活历史业务 HTTP。
|
||||
|
||||
当前 **P1 仍为单 D/单 Agent/单 Asterisk/单 Cell/单租户**,现行已发布 SaaS↔D 全 MQ 契约、静态 SIP 制品、Asia/Shanghai `[09:00,20:00)` 门禁和旧排队 AI 版本绑定,在新版本逐项发布、替换及验证前继续执行;本计划和字段草案**不授权**新线路、跨窗真实拨号、SaaS 生产部署或多 D 业务运行。外部真实 SaaS、云资源、供应商联调及拨号须分别获授权。
|
||||
|
||||
**完成条件分层:**
|
||||
|
||||
- **C(合同)**:两接口实际 SaaS/management 来源、路径/身份承载/返回及错误 Schema、版本/摘要/ETag/缓存/修改并发冻结点、现行 `call.execute` 旧引用如何合法换版,连同必须变更的 MQ 控制语义,以**新版本**获签收、正反例和哈希校验。当前 [项目自拟 Schema](contracts/config-read-v0.1.schema.json)只算草案,不是 C。
|
||||
- **L(单 D 本地闭环)**:单租户的任务+智能体读缓存、SIP 完整获批加载、有界消费/独立控制、任务×线路时段、ACK/恢复及 outbox 本地隔离故障测试通过;配置失效只停新准入,停止/暂停仍可达。完成本项目格式化、`go vet ./...`、`go test -race ./...`、构建和模块覆盖率 ≥65%,留脱敏事实证据。旧本地通过不代替 L。
|
||||
- **M(多 D 后续门禁)**:同租户多任务、**每任务只归一个 D**,不同 D 独占 Agent/Asterisk;D1/D2 任务/缓存/队列隔离、租户/供应商额度份额总和限制、未知占用和非自动迁移故障注入。仅当多 D 开发与授权另行确定时验收,不混入当前单 D 的 L。
|
||||
|
||||
## 2. 权威资料和读法
|
||||
|
||||
1. **先读本文件**,按第 4 节找到 F 工作包,再读对应的项目现行合同:[Go 架构](architecture/Go重写方案_v0.3.md)、[验收](acceptance/验证与切换验收_v0.3.md)、[通信与事件](contracts/通信与事件数据交互_v0.1.md)、[开源复用](dependencies/开源组件选型与复用清单_v0.2.md)及 `contracts/upstream/v1/`。现行运行依据优先于尚未签收的新目标;差异写进 F01,不从旧严格 Schema 擅加字段。
|
||||
2. [页面截图分析](references/saas-page-snapshot-analysis.md)只证明页面显示的业务含义,**不能**证明网络请求/响应的 JSON 键、类型或 SIP 实际管理字段。[字段提案](contracts/config-read-fields-v0.1-proposal.md)里的英文键是项目自定义;旧 [OpenAPI/MQ 索引](references/OpenAPI与MQ字段索引_v0.1.md)也只是历史对照,绝非现网 SaaS API 签收。
|
||||
3. [第三方对接事件与请求消费顺序](thirds/第三方对接事件与请求消费顺序_v0.1.md)只按 SaaS↔Dispatcher 现行 MQ 命令/反馈与拟定 HTTP 两接口的顺序说明请求、返回和字段;是联调读物,**不是新 Schema 或外部签收**。已归档的[原 plan-0918](archive/plan-0918.md)保留旧 W、I/M/G、部署和证据的历史语境;不要继续写旧台账或以旧 `§9` 当本轮并行授权。每个工作包的新状态只在本文 §8 登记。
|
||||
|
||||
## 3. 已确认方向与不能省略的边界
|
||||
|
||||
- **配置源:**SaaS 负责任务、智能体及单任务→D 的持久归属;management 是 SIP 唯一编辑/审批面,SaaS 只读分发准确的获批准 SIP 制品。如 management/SaaS 不共享同一权威来源,先证明同步及全量范围,再宣称两接口足够。
|
||||
- **两接口的响应:**SIP 全量须覆盖适用线路/连接、主叫、媒体、额度、时段及获批制品的版本/摘要;任务响应包含任务时段(周一至周日逐日多段、可选多排除日期)、线路选择、任务额度、所选智能体不可变配置及有效授权。统一 Asia/Shanghai;两种时段必须同时允许,缺失/未知禁止新拨号。供应商 transport/auth/registration 尚未确认时不得为 real 设默认值。UI 已显示但 Agent 无批准 SDK 参数的项记录为缺口,不通过 `metadata` 偷渡。
|
||||
- **缓存与变更:**每 D 使用自己的 UUID+SECRETKEY 读取;成功校验的任务缓存按租户+任务约 60 秒,活跃且尚有待接纳执行时到期主动复核;SIP 启动/重启取全量,运行中约每 60 秒核对。条件请求的 `ETag/304` 可省内容,不能延长过期或撤销的 AI 授权。到期失败/答复不明就停**新执行准入**,不让 Agent 凭过期缓存发起新呼叫;已接纳的执行始终使用持久绑定的快照。SIP 变更先关准入、排空/对账旧使用者并核验 Agent/Asterisk 实际加载,**发现变化 ≤约60秒不代表加载 ≤约60秒**。任务终结仅删除可重取的配置缓存,不删幂等/执行恢复/outbox 事实。
|
||||
- **MQ 与任务所有权:**每任务只投递到归属 D/租户的队列,控制也投同一 D;不要求每任务单建队列。D 仅在有可用任务/租户/Cell/线路/AI 名额及配置资格时有界接收、同事务持久绑定执行/占用/outbox 后 ACK。其它呼叫仍留 MQ;控制/查询应有独立通道,满额仍能到达。当前共用队列与 `not_found` 处理无法保证未接纳任务先收到 stop 后不拨,必须先冻结屏障与乱序语义。缓存延迟不延迟 MQ stop/pause、opt-out、号码白名单或最后发起许可。
|
||||
- **时间/多 D:**目标时间由任务及所选 SIP 线路交集决定,排除日期优先;没有合同和最后拨号门禁前仍执行固定 `[09:00,20:00)`。多个 D 各有独占执行资源,但**单任务归一 D 仅解决该任务的并发**;若同一租户或供应商额度跨 D,共享总上限须权威分配有界份额,份额总和不超上限。D1 队列的未知/未决任务不能被 D2 抢收或自动迁移。
|
||||
|
||||
## 4. 分步任务(按依赖顺序)
|
||||
|
||||
| 工作包 | 输入 / 负责人边界 | 完成证据 / 未满足时状态 |
|
||||
| --- | --- | --- |
|
||||
| F00 页面与字段预盘点 | 项目文档负责人:引用截图、现有 AI/静态 SIP Schema 和旧只读索引,逐字段标 P/C/N;两种成功及错误响应草案、示例/正反例 | [字段提案](contracts/config-read-fields-v0.1-proposal.md)与[项目自拟 Schema](contracts/config-read-v0.1.schema.json)本地可校验;**草案完成≠ SaaS 实际字段已核验**。 |
|
||||
| F01 新合同冻结(前置于任何 client/管理发布) | SaaS/management 权威负责人分别确认数据源、接口路径/UUID+SECRETKEY承载、字段类型/缺省/范围、批准状态、版本/哈希、响应错误、缓存约60秒与同时修改/接纳的冻结点;本项目集成负责人重版导入 W01 所需权威包并明确废弃旧 AI 配置 MQ 路径及新业务 MQ 拓扑 | 新版本来源/版本/哈希、严格 Schema、正反例、批准记录;任务字段与线下管理 SIP 完整可用,供应商不明参数、原 `call.execute` 引用矛盾及 HTTP 304 授权未解决均记 **blocked**,不能先写旧路客户端。 |
|
||||
| F02 最小单 D 配置读取(依赖 F01) | SaaS 提供两条只读接口;本项目 D 对 SIP 启动全量与 Agent/Asterisk applied 事实核验,对归属任务按需拉取内含 AI、60秒缓存/ETag/304、失效停新准入;所有密钥/端点受控注入 | 新 D 无历史广播亦可取全量;HTTP 过期/错误/更新竞态、新旧 Agent 参数及快照不漂移测试;SIP 制品未准确加载拒新执行,控制仍可达。 |
|
||||
| F03 有界执行队列与控制隔离(依赖 F01) | 本项目 D 消费、SQLite 额度/绑定和 MQ 适配:空位才接纳,未接纳留 MQ;执行/控制路由分离须新版合同及 SaaS 发布端配合;旧直写面禁并行 | 爆量消息 SQLite 未接纳积压不无界增长,MQ 队列有容量/发布拒绝与 SaaS 原消息保留;commit/ACK/confirm 丢失及 stop-before-accept 不拨、不忙重投;unknown 不自动释放。 |
|
||||
| F04 时段与最后屏障(依赖 F01/F02/F03) | SaaS 批准任务周多段/排除日与 SIP 线路时段,D/Agent 使用同一已加载版本及 Asia/Shanghai 注入时钟;替换固定窗须对应合同/验收同步更新 | 左闭右开、空日/多个排除日期/任务与线路交集、缓存更新后 ≤约60秒、队列跨窗不自动延迟、D/Agent 最后拨号门禁均通过;仅 Mock 时间不宣称 real 已放行。 |
|
||||
| F05 多 D 独占资源(后续,依赖 F01–F04 及另获本阶段授权) | SaaS 对 `(tenant_key,task_id)→dispatcher_id` 持久路由;D1/D2 各占独立 Agent/Asterisk/队列/持久目录,租户和供应商共享额度按 D 分份额 | 双 D 错投/重启/配置变更/未知通话及份额总和故障注入;不得用 D1/D2 本地合同 fixture 冒充双 D 真实业务或 HA。 |
|
||||
| F06 验收与切换(依赖相应 F 包) | 集成负责人单写现行 §8 台账,模块证据只存脱敏状态、计数、哈希;生产、真实 SIP/OSS/云另行授权 | 新 C→L→M 门禁分开记录。至少 gofmt、`go vet ./...`、`go test -race ./...`、`go build ./...`、本模块覆盖率≥65%;版本回退不得同时运行旧/新路径、触发第二次拨号。 |
|
||||
|
||||
## 5. 必测边界与阻塞项
|
||||
|
||||
1. **返回与缓存:**两接口 `200`/条件 `304`/错误、未知字段拒绝、任务归属错 D 拒绝、已有任务/线路版本不一致、摘要与实际加载不一致、授权过期、缓存第 59/60 秒、SaaS 断连、Dispatcher 重启、内存配置清理但恢复事实仍在;配置不完整绝不发新呼叫。
|
||||
2. **消费与故障:**MQ 爆量/队列满发布端保留、D DB 满盘、ACK 丢失、旧命令重新投递、控制先到未入库执行、停/暂停与配置更新并发、执行中未知占用、不同任务落不同 D 的租户和运营商总额度;没有多 D 份额合同就不开放跨 D 测试。
|
||||
3. **时间与安全边界:**任务/线路时段变化允许的约60秒延迟必须与“已接纳不变、未接纳可能旧版”区分;MQ stop/pause 和号码白名单不可延迟;配置过期则拒新准入,队列消息期限不可让旧任务次日自动拨。真实外呼只在获得明确安排、实际已通过相应门禁后才能尝试。
|
||||
4. **缺失权威:**页面截图没有现网 JSON 键,`config-read-v0.1.schema.json` 为项目自拟;SIP 线路完整管理信息、供应商鉴权/注册、AI UI 扩展和 `snapshot_sha256` 规范仍未获发布或外部签收;不能把 Mock 数据、旧 OpenAPI、计划或字段草案记为 SaaS/production 验收。
|
||||
|
||||
## 6. 文档/写入边界
|
||||
|
||||
新接口版本必须在本项目 `docs/` 与导入的版本化 `contracts/` 保持唯一来源;UI 截图盘点不改造成第二套 SaaS Schema,字段索引只读生成。当前严格旧合同只描述运行中的 MQ-only 路径,新合同获得批准前不偷加 HTTP client、旧 MQ 配置回退或新任务路由键。原 [plan-0918.md](archive/plan-0918.md) 已恢复 HEAD 字节内容归档,归档与本轮状态均不回写旧台账。`docs/README.md` 和 `AGENTS.md` 要始终指向本文件;有并行授权时明确一 lane 一工作区/唯一写集合,公共合同、台账和集成状态由集成负责人单写。不得提交、暂存或清理无关修改;大规模重构前另开分支。
|
||||
|
||||
## 7. 当前责任与前置(I/M/G)
|
||||
|
||||
- **I = 契约来源/授权:**F00 仅盘点完成;F01 的 SaaS/management 源字段签收、HTTP 合同、MQ 控制/执行新协议及 SIP/AI 不确定项仍为 **blocked/pending**。没有 I,不开始 F02–F04 的真实配置接入,更不能借 Mock 名义发外呼。
|
||||
- **M = 本地 Mock/代码验证:**现有已归档计划中的 MQ/AI/静态 SIP 通过只说明旧版局部行为,不是本次新 HTTP/缓存/时段版本通过;按 F02–F04 重做。未获准真实 SaaS 时限于隔离 Mock 与本地合同测试。
|
||||
- **G = 部署/切换前置:**目标 D/Agent 配置实际加载、broker 状态/队列、运行版本、SHA-256、systemd/Asterisk 状态/日志及非生产诊断必须留脱敏证据;生产安全屏障和资源变更另获批准。单 D 的 L 不授权 F05 多 D 或真实供应商验收。
|
||||
|
||||
## 8. 本轮状态总台账(本文件唯一更新处)
|
||||
|
||||
| 项目 | 当前事实 | 下一门禁 |
|
||||
| --- | --- | --- |
|
||||
| 历史计划 | `docs/archive/plan-0918.md` 从 HEAD 原样保存;原 W00–W16 状态只作历史 | 不再向归档文件写新状态。 |
|
||||
| F00 | 页面来源分级、两种成功/错误响应 Schema 及 Mock 示例已形成**项目自拟草案**;[第三方事件顺序及字段说明](thirds/第三方对接事件与请求消费顺序_v0.1.md)已整理现行 MQ 与目标 HTTP 的分界,均不能称作 SaaS 原 JSON 或签收 | 校验 Schema/正反例并取得 SaaS/management 字段来源与事件顺序签收。 |
|
||||
| F01 | 新 HTTP+业务 MQ 方向获用户同意,精确接口/返回/批准来源未签收 | 版本化合同、哈希和更新/接纳竞态语义冻结。 |
|
||||
| F02–F04 | 未实现;当前代码仍为旧 MQ-only/固定时段/单 D 静态交接 | I 先通过;TDD 测试先失败后实施,隔离链路验证。 |
|
||||
| F05 | 单任务单 D、资源独占方向确认,多 D 运行/配额份额未验收 | 后续另获阶段范围、资源及额度权威批准。 |
|
||||
| F06 | 新闭环未执行;旧本地测试不能代签 | 按 C/L/M 分层留证,不写假完成数。 |
|
||||
|
||||
## 9. 协作和版本记录
|
||||
|
||||
本轮只有当前负责人可更新本文件 §8 和共同合同/索引;子任务若获明确并行授权,必须独占文件/测试资源、不得从未跟踪文件假定 HEAD/worktree 已含它,合并每批后统一回归。没有明确真实线路/供应商/云/生产授权,不自动操作外部资源。旧计划的 §8/§9 仅为归档历史;新工作一律以本文件 §7–§9 为入口。版本 v0.1 是**规划与项目字段草案**,不是 W01/SaaS 的正式发布版本。
|
||||
@@ -0,0 +1,140 @@
|
||||
# SaaS 页面截图字段与能力盘点
|
||||
|
||||
> **资料性质:页面调研,非契约、非验收证据。** 本文只整理用户提供的页面截图中可见的字段和操作;原始截图仅供本地参考,不随仓库提交。截图不能证明后端行为、字段传输方式、权限、持久化或线上功能已启用。未看清或无法判断的内容标为待核验。本文不修改或替代现有项目契约。
|
||||
>
|
||||
> 后续项目自拟的两只读 HTTP 接口字段与返回结构见[字段提案](../contracts/config-read-fields-v0.1-proposal.md)。本文 §7 早期 MQ 配置获取建议已被用户批准的新方向取代;页面观察仍有效,但不是 SaaS 原有 JSON 字段名的证明。
|
||||
|
||||
## 1. 范围与结论
|
||||
|
||||
截图覆盖智能体配置与外呼任务的创建、运行数据查看:
|
||||
|
||||
- 智能体:基础信息、提示词、ASR、LLM、TTS、话后分析。
|
||||
- 外呼任务:任务配置、计划时间、线路与重拨设置、任务统计、号码列表、通话记录。
|
||||
- 页面同时展示 AI 参数配置和任务运营配置;两者不能据此视为同一配置对象。
|
||||
|
||||
## 2. 智能体配置
|
||||
|
||||
| 页面 | 截图可见字段或能力 | 备注 |
|
||||
| --- | --- | --- |
|
||||
| 基础信息 | 必填智能体名称、描述(界面显示字数计数);保存草稿、提交;文字/语音/线路测试入口 | 截图显示“已上线、有草稿变更”状态。按钮可见不代表操作已成功;描述字数上限及发布后的版本行为仍应以实际页面核验。 |
|
||||
| 提示词 | 提示词编辑器(字数计数、复制/剪切、格式及预览等工具);独立开场白;挂断触发条件和结束语配置 | 截图中的具体业务提示词不在此复述。准确字段拆分、变量约束和保存后的版本行为待核验。 |
|
||||
| ASR | ASR 模型;音频格式选项 PCM/Opus/AAC/OGG/WAV、采样率 8000/16000 Hz、语言选项;中间结果、标点、去语气词等开关;单句时长 | 截图示例为单句最长 20 秒;不能据此推断每种格式都可用于项目链路,也不能说明 SaaS 到 Agent 的传输与预处理方式。 |
|
||||
| LLM | 模型和对话模式;温度、Top-P、重复惩罚、Top-K、随机种子等采样参数;思考与流式输出开关 | 参数名称和取值范围需以可访问的实际页面或上游定义复核。 |
|
||||
| TTS | 模型、公共/个人音色选择及试听;情绪、语速、音调;MP3/PCM/WAV 输出格式和采样率选项;测试文本与试听入口 | 音色授权、参数范围及运行时是否接受这些设置待核验。 |
|
||||
| 话后分析 | 可编辑分析提示词;按 A–F 等级对通话意向分类,并配置各等级判定规则 | 截图体现分析配置,不证明分析结果的消息、存储或下游消费方式。 |
|
||||
|
||||
## 3. 外呼任务配置
|
||||
|
||||
创建页将配置分为基础信息、时间、其他设置和高级设置等区域。截图中可见:
|
||||
|
||||
- **基础信息**:必填任务名称、所属分组、必选 AI 对话模型;拨打顺序为随机/顺序/倒序;并发数;已启用线路选择、每条线路的数量设置及添加/移除线路入口。
|
||||
- **时间**:任务开始/结束时间;按周一至周日的小时网格设置一个或多个时段,并有快捷时段按钮;拨打时间间隔开关及 1/3/5/10/30/60/120 秒选项。
|
||||
- **其他设置**:自动重呼开关及条件设置入口;结束动作(截图状态分别显示“暂停任务”和“关闭任务”);黑名单开关及分组选择。
|
||||
- **高级设置**:备注输入框;页面提示更多高级功能待开放。
|
||||
- **授权提示**:其中一个创建状态提示当前分组没有可用授权线路,并指向线路管理/租户授权相关入口。只能证明该页面会提示线路不可用,不能证明具体授权校验规则。
|
||||
|
||||
两张创建页截图展示了默认表单和已填写状态。已填写示例中并发数为 5、选择一条线路、拨打间隔为 30 秒;自动重呼显示间隔 1 分钟、最多 3 次、条件 5 项。这些只是截图当时的表单值,不是通用默认值或项目约束。
|
||||
|
||||
## 4. 任务运行与数据页面
|
||||
|
||||
### 4.1 任务列表及统计
|
||||
|
||||
页面呈现任务分组、任务状态、并发/进度类信息,并提供启动、暂停、关闭、编辑或导入等操作入口。任务详情可切换统计、号码、通话记录、运行概览、任务详情和日志等视图。
|
||||
|
||||
统计页可见话术、线路、开始/结束日期筛选,以及并发、排队待呼叫数和呼叫状态等信息;汇总项包含呼叫成功、拒接、无应答、关机、占线、呼叫失败,另有意向等级/标签、获客成本及运营商等图表。图表中的样例数值属于截图数据,不作为项目容量、成功率或验收结论。
|
||||
|
||||
### 4.2 号码列表
|
||||
|
||||
号码页提供按号码、通话状态、归属地/运营商等条件筛选,并展示号码、通话状态与时间、通话时长、客户属性、拨打次数、归属地/运营商和创建时间等信息;截图还显示导入、导出、批量删除等入口。本文不复制截图中的号码或客户样例。
|
||||
|
||||
### 4.3 通话记录
|
||||
|
||||
通话记录页提供号码和意向标签等筛选,表格可见号码、公司/联系人、意向标签、模型标签、通话起止时间、时长、备注和操作等列。具体数据权限、录音/文本的查看方式和留存策略无法由截图确认。
|
||||
|
||||
## 5. 与项目现有 AI 配置契约的关系
|
||||
|
||||
当前权威 AI 配置 Schema 为 [`contracts/upstream/v1/ai-config.schema.json`](../../contracts/upstream/v1/ai-config.schema.json),其中 AI 配置对象采用严格字段校验。已定义的核心内容包括:
|
||||
|
||||
- ASR:供应商引用、模型、语言、中间结果、超时及输入音频编码/采样率/声道/采样宽度。
|
||||
- LLM:供应商/凭据引用、模型、温度、最大 token 数和超时。
|
||||
- Prompt:提示词文本、允许变量及最大字节数;对话配置另含开场白和对话控制参数。
|
||||
- TTS:供应商/凭据引用、模型、音色、语速、超时及编码/采样率/声道。
|
||||
|
||||
截图可见但不能直接映射为当前 AI Schema 字段的内容包括:LLM 的 Top-P、重复惩罚、Top-K、随机种子、思考/流式开关;TTS 情绪、音调及额外格式选择;话后分析提示词;以及任务的线路、计划时间、重拨、黑名单和运营统计字段。它们可能属于独立任务配置、SaaS 页面专属设置或其他上游契约,需逐项核对权威来源和版本。
|
||||
|
||||
**不要仅凭截图向严格 Schema 添加字段,也不要把未映射参数塞入 `metadata` 或原始请求字段。** 若确认这些参数需要进入本项目运行配置,应先按现行契约流程确认来源、语义、默认/显式空值、约束和版本,再更新契约与生成物。
|
||||
|
||||
## 6. 待核验项
|
||||
|
||||
1. 智能体各页面字段的准确名称、枚举、单位、范围、默认值与必填条件。
|
||||
2. AI 配置是否按不可变版本发布;任务引用智能体时绑定哪个版本,以及在途任务如何固定配置。
|
||||
3. ASR/TTS 页面格式与项目媒体管线、当前 Schema 编码之间的转换关系。
|
||||
4. LLM 高级参数、话后分析及分析结果的上游契约、执行位置和消息/存储路径。
|
||||
5. 任务线路分配、并发、拨打窗口、重拨条件及黑名单的实际服务端校验语义。
|
||||
6. 截图所见启动、暂停、关闭、导入/导出和测试入口的权限、效果及失败处理。
|
||||
7. 号码、通话记录、录音与识别文本的查看权限和留存周期。
|
||||
|
||||
## 7. Go 外呼能力的 MQ 接入映射建议
|
||||
|
||||
> **本节是映射建议,不是已批准的新契约。** 按现有契约,不应把整个页面表单序列化成一条 MQ 消息,也不应让浏览器直接连 RabbitMQ。表单由 SaaS 服务端保存;消息只携带执行所需的严格字段和版本引用。
|
||||
|
||||
### 7.1 推荐消息流
|
||||
|
||||
1. **发布智能体配置**:SaaS 保存草稿;用户发布时生成不可变 `agent_version_id`。每次编辑不直接推送整份配置,也不覆盖已发布版本。
|
||||
2. **投递外呼命令**:每个可执行呼叫使用 `call.execute` 发给目标 Dispatcher/租户的专属 `.in` 路由。命令携带 `agent_version_id`,不内嵌整份 ASR/LLM/TTS 配置。
|
||||
3. **按需获取 AI 快照**:Dispatcher 在准入/起拨前如无对应的有效快照,向 SaaS 发布 `ai.config.request`,其 payload 只有 `agent_version_id`;消息头包含请求 ID、目标 Dispatcher、租户、关联 trace 和有效期限。
|
||||
4. **返回不可变配置**:SaaS 以 `ai.config.result` 回到同一 Dispatcher/租户的 `.in` 路由;`correlation_id` 对应原请求 `message_id`。成功响应包含 `snapshot` 和 `authorization`,而不是可变的表单草稿。
|
||||
5. **校验后交付 Agent**:Dispatcher 校验 Schema、租户/版本、摘要、授权有效期及撤销状态,持久绑定快照后,经现有 Unary gRPC 将执行快照交给 Agent。Agent 不直连 SaaS,也不消费这条 MQ。
|
||||
6. **回传执行事实**:Dispatcher 将呼叫状态、结束结果、实时转写、拒绝联系等事件通过 MQ 发回 SaaS;SaaS 用这些事实更新任务详情和统计。`recording.uploaded` 只表示上传事实已通知入队,不代表 SaaS 已完成后续处理。
|
||||
|
||||
消息方向和路由应沿用现有契约:Dispatcher→SaaS 经 `agent-call.saas.v2` 和 `d.<dispatcher_id>.t.<tenant_key>.out`;SaaS→Dispatcher 经 `agent-call.dispatchers.v2` 和 `d.<dispatcher_id>.t.<tenant_key>.in`。必须精确路由到目标 Dispatcher 与租户,不广播后再靠正文筛选。D→SaaS 发布需使用持久消息、mandatory routing 和 publisher confirm;输入先持久化再 ACK。
|
||||
|
||||
`ai.config.request` 的示意 payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": "2.0",
|
||||
"message_type": "ai.config.request",
|
||||
"message_id": "<request-id>",
|
||||
"dispatcher_id": "<dispatcher-uuid-v4>",
|
||||
"tenant_id": "<tenant-id>",
|
||||
"tenant_key": "<original-tenant-key>",
|
||||
"trace_id": "<trace-id>",
|
||||
"issued_at": "<RFC3339>",
|
||||
"not_after": "<RFC3339>",
|
||||
"payload": { "agent_version_id": "<published-version-id>" }
|
||||
}
|
||||
```
|
||||
|
||||
这条请求/响应路径已有契约和存储入口,但当前代码文档明确指出**正常运行时尚无调用方发出 `ai.config.request`**;因此它还不是已打通的生产往返链路。现行字段详见[MQ 机器 Schema](../../contracts/upstream/v1/mq.schema.json);第三方方向/步骤见[事件顺序说明 §4](../thirds/第三方对接事件与请求消费顺序_v0.1.md)。
|
||||
|
||||
### 7.2 表单字段与现有承载位置
|
||||
|
||||
| 表单信息 | 现有 MQ/配置承载位置 | 接入说明 |
|
||||
| --- | --- | --- |
|
||||
| 智能体名称、描述 | SaaS 管理元数据;执行时引用 `agent_version_id` | 不放进每次呼叫消息,除非业务确实要求 Agent 使用这些显示字段。 |
|
||||
| 提示词、允许变量、开场白和对话控制 | `ai.config.result` 的 `config.prompt`、`config.conversation` | 单次呼叫的变量值放 `call.execute.payload.variables`,并按快照的 `allowed_variables` 校验。 |
|
||||
| ASR 模型、语言、中间结果、输入音频参数、超时 | `config.asr` | 现有格式约束是 `pcm_s16le`、单声道;截图里的 Opus/AAC/OGG/WAV 不能未经适配直接写入。 |
|
||||
| LLM 模型、温度、最大 token、超时 | `config.llm` | 截图中的 Top-P、重复惩罚、Top-K、随机种子、思考/流式开关不在当前 Schema 中;先登记契约 GAP,禁止塞进 `metadata` 伪装支持。 |
|
||||
| TTS 模型、音色、语速、编码/采样率/声道、超时 | `config.tts` | 现有输出编码为 `pcm_s16le` 或 `pcma`;截图中的情绪、音调及额外格式须先确认 SDK/媒体链路能力和契约字段。 |
|
||||
| 话后分析提示词和 A–F 意向规则 | 当前 AI 快照及事件 Schema 未定义对应字段/结果事件 | 暂不发送;先确认分析执行方、输入事实、结果归属和 SaaS 消费契约。 |
|
||||
| 任务名、分组、目标号码、智能体版本 | `call.execute` 中的 `task_id`、`task_item_id`、`execution_id`、`callee`、`agent_version_id` 等 | 当前命令是单次呼叫执行数据,不是完整任务表单或批量号码文件;`callee` 保留业务原始号码。 |
|
||||
| 线路选择、主叫配置 | `call.execute.payload.route_policy_id`、`caller_profile_id` | 传已批准的策略/配置 ID,不传 SIP 地址、密码、长期凭据或临时 TOKEN。 |
|
||||
| 振铃超时、最大通话时长、单次个性化变量 | `ring_timeout_ms`、`max_call_duration_ms`、`variables` | 这三项属于当前 `call.execute` payload;仍须通过对应范围、权限和硬限制校验。 |
|
||||
| 任务暂停/恢复/停止 | `task.control` | 使用 `expected_task_revision` 做 CAS;活动呼叫的 drain/hangup 语义不等同于创建表单里的自动重拨或结束选项。 |
|
||||
| 拨打时段/顺序/间隔、任务并发、自动重拨、黑名单 | 当前没有一组可直接承载这些表单值的 `call.execute` 字段 | 不能把整组值塞入 `variables` 或未知字段。需先明确由 SaaS 侧筛选/排程,还是另有经批准的 Dispatcher 任务策略契约;任务并发最终不得突破 Dispatcher 配额。固定的 Asia/Shanghai 09:00–20:00 门禁仍须由 Dispatcher/Agent fail-closed 执行。重拨不能用 MQ 重投或 RPC 超时重试代替。 |
|
||||
|
||||
现行权威结构见[MQ 消息 Schema](../../contracts/upstream/v1/mq.schema.json)、[事件正文](../../contracts/upstream/v1/event-payloads.schema.json)与[AI 配置 Schema](../../contracts/upstream/v1/ai-config.schema.json);待签收的新读取路径见[第三方顺序说明](../thirds/第三方对接事件与请求消费顺序_v0.1.md)。当前 Schema 拒绝未定义字段;配置引用只传 `provider_ref`/`credential_ref`,不得传真实凭据。
|
||||
|
||||
### 7.3 本轮建议范围
|
||||
|
||||
先按现有消息接通两条最小路径:
|
||||
|
||||
- `call.execute.agent_version_id` → `ai.config.request` → `ai.config.result` → Dispatcher 持久化并绑定不可变快照 → Unary gRPC 交付 Agent。
|
||||
- Agent/Dispatcher 执行事实 → 现有 `call.status`、`call.finished`、`transcript.updated`、`contact.opt_out`、`recording.uploaded` 等事件 → SaaS 更新列表、统计和记录页。
|
||||
|
||||
Top-P 等新增 AI 参数、话后分析结果、任务排程/重拨/黑名单策略均先列为契约缺口;未确认字段归属和责任边界前,不新增 MQ 消息字段、不声称表单已接入外呼能力。
|
||||
|
||||
## 8. 截图来源
|
||||
|
||||
页面字段分析以用户提供的智能体和外呼任务截图为参考;原始截图仅保留在本地,不作为仓库附件或第三方接口证据。
|
||||
@@ -1,368 +0,0 @@
|
||||
# Dispatcher ↔ Agent 对接契约(实现事实)
|
||||
|
||||
## 1. 适用范围与实现边界
|
||||
|
||||
当前边界是项目自有的 Unary gRPC `agent.v1.AgentControlService`。消息只携带控制数据、执行绑定、事实和上传元信息,不经过 Dispatcher 传输音频字节。
|
||||
|
||||
| 方向/角色 | 当前实现 |
|
||||
| --- | --- |
|
||||
| Dispatcher → Agent | `internal/rpc.Server` 提供 Agent 侧服务;`internal/dispatcher.AgentCoordinator` 通过配置的 endpoint 调用 |
|
||||
| Agent → Dispatcher | `internal/rpc.DispatcherServer` 提供 Dispatcher 侧同名服务,仅接收事件和上传 RPC |
|
||||
| 传输 | gRPC Unary + mTLS;`internal/rpc.Client` 不做业务自动重试 |
|
||||
| 业务权威 | Dispatcher SQLite 管理任务、配额、事实、outbox;Agent 只维护本地会话、执行/文件恢复事实 |
|
||||
| 数据面 | Agent 按 Dispatcher 下发的 grant 直接 PUT 到 OSS;Dispatcher 不接收或转发录音内容 |
|
||||
|
||||
精确字段号和枚举以 `proto/agent/v1/agent.proto` 为唯一源;本文不另造 protobuf。
|
||||
|
||||
**SaaS↔Dispatcher 边界已修订为 MQ-only**,详见 [SaaS↔Dispatcher 契约](./saas-dispatcher.md)。D 有全局唯一身份和独立接收 Topic;这不改变内部 Unary 或 Agent→OSS 直传。**OSS 配置由 D 配置文件维护,Agent 向 D 领取临时上传 TOKEN,SaaS 不再提供 OSS 配置/TOKEN。** D的签发职责保留;用户已将上传边界收缩为recording.uploaded可靠进入指定持久队列,不等待SaaS会话、verified或OSS ID,不新增VERIFYING。本文旧handler行为仅作差异记录,R13须改为以可靠入队完成,不以文档或Schema通过宣称已接通。
|
||||
|
||||
## 2. Service 方法与方向
|
||||
|
||||
| RPC | 方向 | 当前代码状态 | 接收端 |
|
||||
| --- | --- | --- | --- |
|
||||
| `GetAgentStatus` | Dispatcher → Agent | 已由 `AgentCoordinator.Probe` 调用 | Agent `rpc.Server` |
|
||||
| `ActivateAgent` | Dispatcher → Agent | 已由 `AgentCoordinator.Activate` 调用 | Agent `rpc.Server` |
|
||||
| `GetBootstrap` | Dispatcher → Agent | Agent 侧已实现;当前启动绑定流程未调用 | Agent `rpc.Server` |
|
||||
| `SetAdmissionState` | Dispatcher → Agent | Agent 侧已实现;当前 `AgentCoordinator` 没有调用封装 | Agent `rpc.Server` |
|
||||
| `Execute` | Dispatcher → Agent | 已调用;当前 RPC handler 只准备并记录执行状态 | Agent `rpc.Server` |
|
||||
| `GetExecutionPermit` | Dispatcher → Agent | 已由 `ExecuteRaw` 调用 | Agent `rpc.Server` |
|
||||
| `ApplyTaskControl` | Dispatcher → Agent | 已由 `AgentCoordinator.Control` 调用 | Agent `rpc.Server` |
|
||||
| `QueryExecution` | Dispatcher → Agent | 用于超时/响应丢失后的对账 | Agent `rpc.Server` |
|
||||
| `ReportExecutionEvent` | Agent → Dispatcher | 已接收、去重并生成 MQ outbox | Dispatcher `rpc.DispatcherEventServer` |
|
||||
| `RequestUpload` | Agent → Dispatcher | 已接收并签发 OSS grant | Dispatcher `rpc.DispatcherUploadServer` |
|
||||
| `CompleteUpload` | Agent → Dispatcher | 已持久保存上传事实并将 `recording.uploaded` 通知可靠入队 | Dispatcher `rpc.DispatcherUploadServer` |
|
||||
|
||||
`DispatcherServer` 对外只实现 `ReportExecutionEvent`、`RequestUpload`、`CompleteUpload`;其它 RPC 在 Dispatcher listener 上返回 `UNIMPLEMENTED`。Agent `rpc.Server` 虽实现完整 generated service,但其 upload handler 在非 `mock` 模式明确返回 `UNIMPLEMENTED`。
|
||||
|
||||
## 3. 连接、认证与会话
|
||||
|
||||
### 3.1 连接
|
||||
|
||||
1. Dispatcher 从受控 Agent endpoint 文件读取 `agent_id`、`cell_id`、地址和 `server_name`。
|
||||
2. `rpc.DialFromFiles` 使用 CA、客户端证书、私钥和 server name 建立 TLS gRPC 连接。
|
||||
3. Dispatcher 对每个 endpoint 先 `GetAgentStatus`,再以返回的 `boot_id` 调 `ActivateAgent`。
|
||||
4. Dispatcher 生成本次 `dispatcher_epoch`;Agent 用 `session_generation` 持久化 fencing 水位。`dispatcher_epoch` 是运行代次,不是全局唯一的 Dispatcher 逻辑 ID;后者的 MQ 关联及与内部会话的绑定待新契约冻结,当前 Proto 未因此自动增加字段。
|
||||
5. Agent 新会话会 fence 旧的 `agent_id + cell_id + boot_id + epoch + generation` 组合;旧请求返回 `ABORTED`,不会自动释放未知执行。
|
||||
|
||||
### 3.2 mTLS 与身份
|
||||
|
||||
- Agent listener 要求 verified peer certificate;可按证书 fingerprint 和允许的 Agent ID 限制。
|
||||
- Dispatcher listener 同样要求 verified mTLS peer、fingerprint allowlist 和可选的 `AllowedAgentIDs`。
|
||||
- 共用证书只证明证书组;`agent_id`、`cell_id`、`boot_id` 必须经过 Dispatcher 激活绑定,不能信任 Agent 自报 endpoint。
|
||||
- gRPC RPC 失败不等于业务失败。尤其 `Execute`、permit 和上传完成超时后,调用方必须查询原执行/上传状态,不能换 execution ID、attempt ID 或 upload ID。
|
||||
|
||||
## 4. 公共消息结构
|
||||
|
||||
### 4.1 `RequestMeta`
|
||||
|
||||
| 字段 | 类型 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| `protocol_version` | string | 当前实现发送 `agent.v1` |
|
||||
| `request_id` | string | 单次 RPC 请求关联 |
|
||||
| `trace_id` | string | 跨模块追踪 |
|
||||
| `operation_id` | string | 业务操作标识;写操作必填 |
|
||||
| `deadline_unix_ms` | int64 | 协议字段;当前 handler 未单独执行该值的截止检查 |
|
||||
| `dispatcher_epoch` | string | 当前 Dispatcher 会话代次 |
|
||||
| `agent_id` / `cell_id` | string | endpoint 绑定身份 |
|
||||
| `boot_id` | string | Agent 进程启动身份 |
|
||||
| `session_generation` | uint64 | 会话 fencing 代次 |
|
||||
| `idempotency_key` | string | 同操作重报必须复用;写操作必填 |
|
||||
|
||||
### 4.2 `ResponseMeta`、`Failure`、`OperationReceipt`
|
||||
|
||||
`ResponseMeta` 回显协议、请求、trace、operation、Dispatcher epoch、Agent/Cell/boot/generation,并增加 `observed_at_unix_ms`。
|
||||
|
||||
`Failure`:
|
||||
|
||||
| 字段 | 类型 |
|
||||
| --- | --- |
|
||||
| `code` | `FailureCode` |
|
||||
| `retryable` | bool |
|
||||
| `detail` | string |
|
||||
| `field` | string |
|
||||
|
||||
`OperationReceipt`:`meta`、`result`、可选 `failure`、`fact_id`、`content_sha256`、`accepted_at_unix_ms`。
|
||||
|
||||
`ResultCode` 为 `ACCEPTED`、`APPLIED`、`REJECTED`、`UNKNOWN`、`CONFLICT`;`ACCEPTED` 只表示接收/持久记录,不能直接解释为已拨号或已挂断。
|
||||
|
||||
### 4.3 `ExecutionBinding`
|
||||
|
||||
| 字段 |
|
||||
| --- |
|
||||
| `tenant_id`, `tenant_key` |
|
||||
| `execution_id`, `task_id`, `task_item_id` |
|
||||
| `task_revision` |
|
||||
| `call_id`, `attempt_id` |
|
||||
| `agent_version_id` |
|
||||
| `route_policy_id`, `caller_profile_id` |
|
||||
|
||||
Dispatcher 从已校验的 `call.execute` 构造 binding;Agent 回报不能改写租户、任务或资产归属。
|
||||
|
||||
### 4.4 `AssetDescriptor`、`ExecutionFact`、`UploadGrant`
|
||||
|
||||
`AssetDescriptor`:
|
||||
|
||||
| 字段 | 类型/约束 |
|
||||
| --- | --- |
|
||||
| `kind` | `RECORDING` 或 `TRANSCRIPT` |
|
||||
| `asset_id`, `call_id`, `execution_id` | string |
|
||||
| `format` | string |
|
||||
| `size_bytes`, `duration_ms` | int64 |
|
||||
| `checksum_sha256` | string |
|
||||
| `channels`, `sample_rate_hz` | int32 |
|
||||
|
||||
`ExecutionFact`:
|
||||
|
||||
| 字段 | 类型/约束 |
|
||||
| --- | --- |
|
||||
| `fact_id` | 稳定事实 ID,必填 |
|
||||
| `content_sha256` | 事实内容摘要,必填 |
|
||||
| `binding` | `ExecutionBinding` |
|
||||
| `kind` | `FactKind` |
|
||||
| `observed_at_unix_ms` | Agent 事实发生时间,必填 |
|
||||
| `source_boot_id` | 事实来源 boot,必填 |
|
||||
| `source_sequence` | uint64,诊断/排序关联 |
|
||||
| `payload_json` | JSON object 字节,必填 |
|
||||
|
||||
`UploadGrant`:`upload_id`、`target_url`、`headers[]`、`expires_at_unix_ms`、`object_key`、`required_checksum_sha256`、`max_bytes`。grant 不包含长期 OSS 密钥。
|
||||
|
||||
## 5. Dispatcher → Agent RPC
|
||||
|
||||
### 5.1 `GetAgentStatus`
|
||||
|
||||
请求:`RequestMeta` + 可选 `AgentBinding target`。激活前仅发送 protocol/request/trace/operation/agent/cell,不能带 session binding。
|
||||
|
||||
响应:`ResponseMeta` + `AgentStatus`:
|
||||
|
||||
- `agent_id`、`cell_id`、`boot_id`;
|
||||
- `software_version`、`protocol_version`、`asterisk_version`;
|
||||
- `admission_state`;
|
||||
- `capabilities[]`;
|
||||
- `resources`(CPU、内存、FD、spool、媒体端口及 `sample_fresh`);
|
||||
- `applied_configs[]`;
|
||||
- `mtls_authenticated`、`session_active`、`status_reason`。
|
||||
|
||||
当前 `Probe` 至少校验返回的 Agent/Cell 与目标一致且 `boot_id` 非空。
|
||||
|
||||
### 5.2 `ActivateAgent`
|
||||
|
||||
请求:`RequestMeta`、`AgentBinding`、`activation_operation_id`、可选 `session_nonce`、`session_expires_at_unix_ms`。
|
||||
|
||||
`AgentBinding` 由 Dispatcher 提供:`agent_id`、`cell_id`、`expected_boot_id`、`dispatcher_epoch`、`session_generation`、`endpoint_id`。
|
||||
|
||||
响应:`ResponseMeta`、`state=ACTIVE`、`Session`:
|
||||
|
||||
- `dispatcher_epoch`;
|
||||
- `session_generation`;
|
||||
- `expires_at_unix_ms`;
|
||||
- `session_credential`(bytes,敏感数据)。
|
||||
|
||||
当前 Agent 实现实际将 session 有效期固定为 `now + 10 分钟`;`session_nonce` 和调用方提供的 `session_expires_at_unix_ms` 当前没有参与校验。后续请求由 mTLS + 会话绑定字段授权,当前 handler 未在每个请求中再次传输或校验 `session_credential`。
|
||||
|
||||
### 5.3 `GetBootstrap`
|
||||
|
||||
请求:`RequestMeta`、`agent_id`、`cell_id`、`boot_id`、`session_generation`。当前 Agent handler 以 `RequestMeta` 会话授权为准。
|
||||
|
||||
响应:`ResponseMeta`、`state`、`runtime_configs[]`、`UploadPolicy`:
|
||||
|
||||
- `ConfigReference`:`kind`、`version`、`sha256`、`source`;
|
||||
- `UploadPolicy`:`enabled`、`max_asset_bytes`、`allowed_hosts[]`。
|
||||
|
||||
当前 Dispatcher 启动绑定流程只执行 status + activate,没有调用 bootstrap。
|
||||
|
||||
### 5.4 `SetAdmissionState`
|
||||
|
||||
请求字段:`meta`、`target`、`state`(`OPEN/CLOSED/DRAINING/QUARANTINED`)、`barrier_id`、`expected_admission_generation`、`reason`。
|
||||
|
||||
响应:`OperationReceipt` + `applied_admission_generation`。Agent 侧按目标 Agent 做 generation CAS;代次不匹配返回 `CONFLICT`。当前代码没有 `AgentCoordinator` 的调用封装。该方法属于 D→A 内部准入职责,不把它直接等同 SaaS 任务控制;SaaS 业务控制只能经 MQ 进入 D,旧 HTTP 控制入口应移除。
|
||||
|
||||
### 5.5 `GetExecutionPermit`
|
||||
|
||||
请求:`meta`、`ExecutionBinding`、`resource_reservation_id`、`expected_task_revision`、`admission_generation`、`config_sha256`。
|
||||
|
||||
响应:`OperationReceipt` + `ExecutionPermit`:
|
||||
|
||||
| 字段 | 含义 |
|
||||
| --- | --- |
|
||||
| `permit_id` | 当前实现为 `permit-<execution_id>` |
|
||||
| `resource_reservation_id` | 对应 Dispatcher reservation |
|
||||
| `issued_at_unix_ms` / `expires_at_unix_ms` | 许可时间窗 |
|
||||
| `dispatcher_epoch` / `session_generation` | fencing 绑定 |
|
||||
| `fencing_token` | 随机 fencing token |
|
||||
| `config_sha256` | 执行配置摘要 |
|
||||
|
||||
当前 `rpc.Server` 的 permit 默认有效期为 1 秒;Dispatcher 在收到传输错误时不重新申请另一个 reservation,而是先 `QueryExecution` 对账。
|
||||
|
||||
### 5.6 `Execute`
|
||||
|
||||
请求字段:`meta`、`ExecutionBinding`、`call_execute_json`、`config_sha256`、`admission_generation`、`resource_reservation_id`、`permit_id`。
|
||||
|
||||
`call_execute_json` 必须是原始、已通过 `mq.schema.json` 的 `call.execute` body。Agent 当前会再次解码并校验:租户、execution、task、task item、AI version 必须与 binding 一致,并在已提供本地 AI 快照/授权时校验摘要、版本、租户、模式、有效期和 egress pool。
|
||||
|
||||
成功响应:`OperationReceipt` + `state`。当前 `rpc.Server.Execute` 的可证明副作用是:
|
||||
|
||||
- 写入执行准备日志;
|
||||
- 写入内存 execution record;
|
||||
- 同 idempotency key 同内容返回原 receipt;不同内容返回 `CONFLICT`;
|
||||
- 在非 mock 模式检查 Asia/Shanghai 外呼时间窗口。
|
||||
|
||||
当前该 RPC handler 本身不证明已调用 ARI/RTP 或已经产生 SIP 外呼。`AgentCoordinator.ExecuteRaw` 当前发送 binding、原始 command JSON、config digest 和 permit ID,不填充 `admission_generation` 与 `resource_reservation_id`。
|
||||
|
||||
### 5.7 `ApplyTaskControl`
|
||||
|
||||
请求:`meta`、`ExecutionBinding`、`action`(`PAUSE/RESUME/STOP`)、`active_call_policy`(`DRAIN/HANGUP`)、`expected_task_revision`、`reason`。
|
||||
|
||||
响应:`OperationReceipt`、`applied_task_revision`、`state`。
|
||||
|
||||
当前 Agent handler:
|
||||
|
||||
- 找不到 execution 返回 `NOT_FOUND`;
|
||||
- task revision 不一致返回 `CONFLICT`;
|
||||
- terminal execution 不允许 resume;
|
||||
- `STOP` 将 call state 置为 `stopped`/terminal;
|
||||
- `PAUSE`/`RESUME` 更新内存 call state。
|
||||
|
||||
当前 handler 没有依据 `active_call_policy` 实施实际 drain/hangup,也没有把 `reason` 写入业务事实;真正通话屏障仍需 ARI/执行器事实补齐。
|
||||
|
||||
### 5.8 `QueryExecution`
|
||||
|
||||
请求:`meta` + `ExecutionBinding`。
|
||||
|
||||
成功响应包含 `ExecutionSnapshot`:binding、execution state、`call_state`、`attempt_id`、`reason_code`、`observed_at_unix_ms`、`unknown`、`assets[]`。
|
||||
|
||||
当前 handler 查询 Agent 内存 execution record;未找到返回 `NOT_FOUND`,当前实现不会填充资产列表。Dispatcher 只在原 RPC 结果未知或回包不完整时用原 binding 对账,不能据此自动重拨。
|
||||
|
||||
## 6. Agent → Dispatcher RPC
|
||||
|
||||
### 6.1 `ReportExecutionEvent`
|
||||
|
||||
请求:`RequestMeta` + `ExecutionFact`。
|
||||
|
||||
Dispatcher 侧额外要求:
|
||||
|
||||
- verified mTLS peer;可选允许的 Agent ID;
|
||||
- `meta.agent_id/cell_id/boot_id` 非空;
|
||||
- operation/idempotency key 非空;
|
||||
- fact ID、内容摘要、source boot、观测时间和 payload 非空;
|
||||
- binding 至少包含 `tenant_id`、`tenant_key`、`execution_id`;
|
||||
- `payload_json` 必须解码为 JSON object。
|
||||
|
||||
`FactKind` 到 SaaS MQ 事件的当前映射:
|
||||
|
||||
| FactKind | Dispatcher 输出 | aggregate |
|
||||
| --- | --- | --- |
|
||||
| `EXECUTION_ACCEPTED` | `command.result` | `command` |
|
||||
| `CALL_STATUS` | `call.status` | `call` |
|
||||
| `CALL_FINISHED` | `call.finished` | `call` |
|
||||
| `TRANSCRIPT_UPDATED` | `transcript.updated` | `transcript_segment` |
|
||||
| `TRANSCRIPT_FAILED` | 尝试生成 `transcript.failed`,但当前 `aggregate_type=transcript` 不通过 `mq.schema.json` | 当前路径阻塞 |
|
||||
| `CONTACT_OPT_OUT` | `contact.opt_out` | `call` |
|
||||
| `RECORDING_PROGRESS` | 不发布 MQ 事件,只保存 fact | `execution_fact` |
|
||||
|
||||
当前 Dispatcher 为有事件的 fact 生成 `event_id = execution-fact-<fact_id>`,并在同一 SQLite 事务内。注意:`TRANSCRIPT_FAILED` 的当前代码映射使用 `aggregate_type=transcript`,而 `mq.schema.json` 只允许 `transcript_segment`;因此该 fact 当前会在事件 Schema 校验阶段失败,不能按成功回报处理。
|
||||
|
||||
具体流程为:
|
||||
|
||||
1. 以 `fact_id + content_sha256` 去重;
|
||||
2. 保存 binding、payload、source boot、source sequence、观测时间;
|
||||
3. 分配 aggregate version;
|
||||
4. 写入权威事件 outbox。
|
||||
|
||||
重复 fact 同摘要返回 `ACCEPTED`;同 fact ID 不同摘要返回 `CONFLICT`。Agent 不能通过 payload 或请求字段指定 `aggregate_version`。
|
||||
|
||||
### 6.2 `RequestUpload`
|
||||
|
||||
请求:`meta`、`ExecutionBinding`、`AssetDescriptor`、`upload_id`。
|
||||
|
||||
当前 Dispatcher 验证:Agent/Cell/operation/idempotency 元数据、完整 execution/tenant binding、合法 `tenant_key`、asset ID、upload ID、正数文件大小和 SHA-256;超过 OSS 最大文件大小返回 `RESOURCE_EXHAUSTED`。
|
||||
|
||||
成功响应:`OperationReceipt`、`UploadGrant`、`state`。D读取严格JSON配置文件,复用官方SDK提供15分钟预签名PUT;本地签发、单次上传及通知恢复证据见[上传验证](../evidence/20260921-mq-upload-progress.md)。当前handler:
|
||||
|
||||
- 以 `tenant_key + "\\0" + execution_id + "\\0" + asset_id` 的 SHA-256 hex 生成 object key,并加配置的 key prefix;
|
||||
- 将 binding、asset、grant、object key、state 持久到 Dispatcher SQLite;
|
||||
- grant有效期固定15分钟,不允许通过配置改变;
|
||||
- 同upload ID、同operation ID及原请求正文返回原grant,包括原到期时间;绑定或正文不同返回冲突;
|
||||
- 重新签发必须使用显式新请求身份,保持原资产及对象绑定;不因原请求重放自动续期,不向SaaS申请TOKEN。
|
||||
|
||||
新请求当前返回 `UPLOAD_STATE_REQUESTED`;持久层状态为 `granted`。上传通知可靠入队后返回 `UPLOAD_STATE_COMPLETED`,失败返回 `UPLOAD_STATE_FAILED`。
|
||||
|
||||
目标流程为 **D 依据自身配置文件向 A 提供临时上传 TOKEN,不向 SaaS 申请 OSS 配置/TOKEN**。D 侧配置缺失/无效时明确失败,不切换配置源;长期凭据不交给 A,不写入示例、日志或证据。当前TOKEN形态为官方SDK生成的受限预签名PUT信息,映射到UploadGrant;它不是OSS原生强制一次性凭据,Agent通过持久尝试状态保证每次授权尝试最多一次PUT。
|
||||
|
||||
用户已收缩上传职责:**R12不申请SaaS会话,不等待SaaS回复**。D校验原租户/执行/资产后按自身配置提供15分钟SDK预签名PUT,保留原upload_id;过期仅显式向D重新申请。签发能力保留复用,不引入第二配置源或新的上传控制协议。
|
||||
|
||||
### 6.3 Agent → OSS 直接上传
|
||||
|
||||
Agent 获得 grant 后使用 `internal/agent.UploadClient.UploadFile`:
|
||||
|
||||
- 只允许 HTTPS;除非显式配置,否则不允许 HTTP;
|
||||
- 可限制目标 host;禁止 grant 注入 `Host` 和 `Content-Length`;
|
||||
- 使用同一文件描述符预读校验,再按`Content-Length` PUT并对实际发送字节计数和计算SHA-256;文件变化明确失败;
|
||||
- 禁止重定向;2xx 才算 PUT 成功;
|
||||
- 返回 HTTP status、文件大小、SHA-256、ETag;
|
||||
- 文件内容不经过 Dispatcher,Agent 不把源文件删除或移动。
|
||||
|
||||
PUT成功仅是文件上传事实,还须由D将通知可靠送入MQ;既不等待也不声称SaaS已应用。
|
||||
|
||||
### 6.4 `CompleteUpload`
|
||||
|
||||
请求:`meta`、原 `ExecutionBinding`、原 `AssetDescriptor`、`upload_id`、`uploaded_size_bytes`、`uploaded_checksum_sha256`。
|
||||
|
||||
Dispatcher当前执行:
|
||||
|
||||
1. 校验原upload ID、完整binding/asset、实际上传大小、SHA-256和grant的max_bytes;只接受录音事实。
|
||||
2. 使用签发时持久保存的bucket/object key,不因当前配置改变对象位置。
|
||||
3. 在同一SQLite事务内保存原上传事实及固定event_id的recording.uploaded outbox。
|
||||
4. 未确认入队时返回Unavailable并保留uploaded状态;publisher确认原通知可靠入队后,重复R13返回ACCEPTED及COMPLETED。
|
||||
|
||||
完成依据是persistent消息进入指定durable队列/绑定、mandatory无return且publisher confirm成功。仅写本地outbox不算交付完成。通知恢复不要求重取TOKEN或重新PUT,也不因原grant此时过期而重传已上传的文件。
|
||||
|
||||
旧OSS HEAD/verified及oss_id响应已删除,Proto保留原字段编号和名称为reserved。不等待SaaS会话或消费回复,不新增VERIFYING、不发recording.ready;完成不表示SaaS已处理。当前MQ wire为2.0,运行使用唯一 V1 契约包(包含 AI JCS 摘要规则),上传字段沿用已批准的[V1冻结方案](mq-only-v1-freeze-proposal.md)。本地MQ无绑定、确认丢失、重启及原通知恢复已有[证据](../evidence/20260921-mq-upload-progress.md),不等于真实OSS/SaaS联调通过。
|
||||
|
||||
## 7. 错误、幂等与未知结果
|
||||
|
||||
### 7.1 gRPC FailureCode
|
||||
|
||||
`INVALID_ARGUMENT`、`UNAUTHENTICATED`、`PERMISSION_DENIED`、`FAILED_PRECONDITION`、`ABORTED`、`RESOURCE_EXHAUSTED`、`UNAVAILABLE`、`DEADLINE_EXCEEDED`、`NOT_FOUND`、`ALREADY_EXISTS`。
|
||||
|
||||
调用方处理原则:
|
||||
|
||||
- 参数、Schema、绑定错误:拒绝,不改写成另一任务;
|
||||
- CAS、session、fact、upload 绑定冲突:查询原对象,不换 ID 绕过;
|
||||
- `UNAVAILABLE`/`DEADLINE_EXCEEDED`:结果可能已发生,先查询/对账;
|
||||
- `Execute` 结果未知:Dispatcher 标记 reservation/task 为 unknown,禁止自动重新 originate;
|
||||
- 上传回包丢失:复用原 `upload_id` 和原资产摘要;
|
||||
- Agent 新 boot:保留旧未知占用,先恢复和对账,不因新 boot 的空状态释放配额。
|
||||
|
||||
### 7.2 RPC 幂等键
|
||||
|
||||
Agent 侧写操作的内存 operation key 为:
|
||||
|
||||
`agent_id + "\\0" + operation_id + "\\0" + idempotency_key`
|
||||
|
||||
同 key 同 protobuf 内容返回原 receipt;同 key 不同内容返回 `CONFLICT`。Dispatcher 事件侧使用 `fact_id + content_sha256`,上传侧使用 `upload_id + binding + asset` 绑定。
|
||||
|
||||
## 8. 当前未实现或未接通的部分
|
||||
|
||||
1. `GetBootstrap`、`SetAdmissionState` 虽有 handler,但当前 Dispatcher 启动流程没有调用完整 bootstrap/admission 编排。
|
||||
2. `Execute` 的当前 RPC 实现只证明准备/幂等/配置校验,不证明 ARI/RTP/SIP 已由该 RPC 直接完成。
|
||||
3. D配置文件→15分钟TOKEN→A直传及recording.uploaded可靠入队/R13完成已接通并有本地MQ证据。保留D签发,删除旧ready/oss_id完成路径;不开发SaaS上传会话或verified往返,不新增VERIFYING,旧上传HTTP继续废弃。
|
||||
4. SaaS AI 配置/授权的 MQ 请求响应与持久绑定已有本地RabbitMQ/SQLite恢复证据;Agent动态交付仍受现有Unary快照载体边界约束。旧 AI version GET 已废弃,不能作为后续实现方向。
|
||||
5. 不能把 generated service 中的全量方法数当作每个 listener 都可调用;实际 listener 能力以第 2 节和 `DispatcherServer` 代码为准。
|
||||
|
||||
## 9. 依据文件
|
||||
|
||||
- `proto/agent/v1/agent.proto`
|
||||
- `internal/dispatcher/agent.go`
|
||||
- `internal/rpc/client.go`
|
||||
- `internal/rpc/server.go`
|
||||
- `internal/rpc/dispatcher_server.go`
|
||||
- `internal/rpc/dispatcher_events.go`
|
||||
- `internal/rpc/dispatcher_upload.go`
|
||||
- `internal/agent/upload.go`
|
||||
- `internal/store/facts.go`
|
||||
- `internal/store/uploads.go`
|
||||
- `internal/ai/snapshot.go`
|
||||
- `internal/ai/authorization.go`
|
||||
- `cmd/sip-go-agent/main.go`
|
||||
- `contracts/upstream/v1/event-payloads.schema.json`
|
||||
- `contracts/upstream/v1/ai-authorization.schema.json`
|
||||
@@ -1,171 +0,0 @@
|
||||
# Dispatcher → MQ → SaaS 消息定义与 Topic 规则
|
||||
|
||||
本文定义 Dispatcher 发往 RabbitMQ、供 SaaS 消费的数据结构,以及当前 Dispatcher broker 使用的交换机、队列和 routing key。消息为 UTF-8 JSON,`schema_version` 固定 `2.0`,单条消息体上限 256 KiB。未知字段和不符合对应 payload 结构的消息拒绝。
|
||||
|
||||
SaaS 发往 Dispatcher 的命令、查询及 AI 配置响应见 [`saas-mq.md`](saas-mq.md)。
|
||||
|
||||
## Dispatcher 向 SaaS 发送的数据
|
||||
|
||||
当前有三类输出:业务事件、Service request、Service response。
|
||||
|
||||
| 类别 | `event_type` / `message_type` | 当前处理情况 |
|
||||
| --- | --- | --- |
|
||||
| 业务事件 | `command.result` | 任务接收、任务控制和重放结果 |
|
||||
| 业务事件 | `call.status`、`call.finished` | 通话状态及结束事实 |
|
||||
| 业务事件 | `transcript.updated`、`transcript.failed` | 实时文字更新及失败事实 |
|
||||
| 业务事件 | `contact.opt_out` | 用户拒绝联系事实 |
|
||||
| 业务事件 | `recording.uploaded` | OSS 上传事实的可靠 MQ 通知 |
|
||||
| 业务事件 | `recording.failed` | 结构可校验、通话查询快照可保留;当前业务路径未生成该事件 |
|
||||
| Service request | `ai.config.request` | 结构及持久化入口存在,但当前仅测试调用,正常运行链路未接通 |
|
||||
| Service response | `command.query.result`、`call.query.result` | 分别响应 SaaS 的 `command.query`、`call.query` |
|
||||
|
||||
### 业务事件信封
|
||||
|
||||
所有事件字段必填:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": "2.0",
|
||||
"event_id": "<event-id>",
|
||||
"event_type": "recording.uploaded",
|
||||
"dispatcher_id": "<canonical-lowercase-uuid-v4>",
|
||||
"tenant_id": "<id>",
|
||||
"tenant_key": "<original-tenant-key>",
|
||||
"trace_id": "<id>",
|
||||
"occurred_at": "<RFC3339 timestamp>",
|
||||
"aggregate_type": "call",
|
||||
"aggregate_id": "<id>",
|
||||
"aggregate_version": 1,
|
||||
"payload": {}
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 / 规则 |
|
||||
| --- | --- |
|
||||
| `schema_version` | 固定 `2.0` |
|
||||
| `event_id`、`tenant_id`、`trace_id`、`aggregate_id` | 非空 ID,最多 128 个字符;事件 ID 同时作为 AMQP `message_id` |
|
||||
| `event_type` | 下列八种之一 |
|
||||
| `dispatcher_id` | 规范小写 UUID v4,由 Dispatcher 填入 |
|
||||
| `tenant_key` | 原值保留;非空有效 UTF-8,最多 196 字节;不得含独立的 `*` 或 `#` topic 段 |
|
||||
| `occurred_at` | RFC3339 时间,由 Dispatcher 生成 |
|
||||
| `aggregate_type` | 非空字符串,最多 64 字符 |
|
||||
| `aggregate_version` | 正整数,由 Dispatcher 持久状态递增;不能由 Agent 指定 |
|
||||
| `payload` | 按 `event_type` 使用下述结构;不接受额外字段 |
|
||||
|
||||
### Event payload
|
||||
|
||||
未列为必填的字段为可选;每种 payload 都拒绝未定义字段。ID 为 JSON string,最多 128 个字符;时间字段为 RFC3339 JSON string;计数、版本、时长及偏移量为 JSON integer,标志位为 JSON boolean;枚举字段为 JSON string。
|
||||
|
||||
| `event_type` | 必填字段 | 可选字段 / 规则 |
|
||||
| --- | --- | --- |
|
||||
| `command.result` | `command_id`、`command_type`、`status`、`reason_code`(非空,最多 128 字符) | `task_id`、`task_item_id`、`execution_id`、`call_id`、`requested_task_revision`、`applied_task_revision`、`admission_state`、`resource_reservation_id`、`permit_id`。`command_type` 为 `call.execute`、`task.control`、`call.replay`、`command.replay`;`status` 为 `accepted`、`waiting`、`applied`、`rejected`、`failed`、`unknown`;revision 非负;`admission_state` 为 `open`、`closed`、`draining`、`quarantined`、`unknown` |
|
||||
| `call.status` | `call_id`、`execution_id`、`call_state`、`call_version`、`attempt_id`、`attempt_state` | `task_id`、`task_item_id`、`route_policy_id`、`caller_profile_id`、`trunk_id`、`cell_id`、`egress_pool_id`、`observed_at`、`reason_code`。`call_state` 为 `queued`、`dialing`、`ringing`、`answered`、`ended`;`attempt_state` 为 `pending`、`active`、`ended`、`unknown`;`call_version >= 1`;`reason_code` 最多 128 字符 |
|
||||
| `call.finished` | `call_id`、`execution_id`、`call_version`、`outcome`、`started_at`、`ended_at`、`duration_ms`、`reason_code` | `task_id`、`task_item_id`、`attempt_summary`(最多 32 项)、`asset_state`。`outcome` 为 `answered`、`no_answer`、`busy`、`failed`、`opt_out`、`cancelled`、`unknown`;`duration_ms >= 0`;`asset_state` 为 `pending`、`complete`、`failed`、`unknown`。每个 `attempt_summary` 必填 `attempt_id`、`state`(`pending`、`active`、`ended`、`unknown`),可带 `trunk_id`、`cell_id`、`reason_code` |
|
||||
| `transcript.updated` | `call_id`、`turn_id`、`segment_id`、`role`、`revision`、`text`、`is_final`、`start_ms`、`end_ms`、`playback_state` | `execution_id`。`role` 为 `customer`、`agent`、`system`;`revision >= 1`;`text` 最多 32768 字符;时间偏移非负;`playback_state` 为 `not_applicable`、`generated`、`sent`、`playback_confirmed`、`cancelled`、`unknown` |
|
||||
| `transcript.failed` | `call_id`、`reason_code`(非空,最多 128 字符)、`retryable` | `segment_id` 或 `affected_segments`(最多 256 个 ID) |
|
||||
| `recording.uploaded` | `call_id`、`recording_id`、`format`、`channels`、`sample_rate_hz`、`duration_ms`、`size_bytes`、`checksum_sha256`、`upload_id`、`bucket`、`object_key` | 无。`format` 为 `wav`、`raw_pcm`、`pcma`;`channels=1`;采样率 8000–48000 Hz;`duration_ms >= 0`、`size_bytes >= 1`;SHA-256 为小写 64 位十六进制;`bucket` 长度 1–63,`object_key` 长度 1–1024 |
|
||||
| `recording.failed` | `call_id`、`recording_id`、`stage`、`reason_code`、`retryable` | `next_retry_at`。`stage` 为 `seal`、`request`、`upload`、`complete`、`verify`、`cleanup`;`reason_code` 非空且最多 128 字符;`next_retry_at` 为 RFC3339 时间 |
|
||||
| `contact.opt_out` | `call_id`、`task_id`、`task_item_id`、`requested_at` | `turn_id`、`segment_id` |
|
||||
|
||||
`recording.uploaded` 只报告 Agent 已直传 OSS 的既成事实,包含 bucket/object key、格式、大小及校验和;不含 TOKEN、密钥或签名 URL。入队成功不代表 SaaS 已处理,也不等待 `verified`、OSS ID 或 SaaS 回执。
|
||||
|
||||
### Service request:`ai.config.request`
|
||||
|
||||
当前代码定义并能持久化的请求信封如下;正常运行链路尚无生产调用方:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": "2.0",
|
||||
"message_type": "ai.config.request",
|
||||
"message_id": "<request-id>",
|
||||
"dispatcher_id": "<canonical-lowercase-uuid-v4>",
|
||||
"tenant_id": "<id>",
|
||||
"tenant_key": "<original-tenant-key>",
|
||||
"trace_id": "<id>",
|
||||
"issued_at": "<RFC3339 timestamp>",
|
||||
"not_after": "<RFC3339 timestamp>",
|
||||
"payload": { "agent_version_id": "<id>" }
|
||||
}
|
||||
```
|
||||
|
||||
所有字段必填;payload 仅含 `agent_version_id`。对应 SaaS 响应 `ai.config.result` 的结构见 [`saas-mq.md`](saas-mq.md)。
|
||||
|
||||
### Service response:查询结果
|
||||
|
||||
查询响应共用信封;`not_after` 不出现在响应中:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": "2.0",
|
||||
"message_type": "command.query.result",
|
||||
"message_id": "<response-id>",
|
||||
"dispatcher_id": "<canonical-lowercase-uuid-v4>",
|
||||
"tenant_id": "<id>",
|
||||
"tenant_key": "<original-tenant-key>",
|
||||
"trace_id": "<id>",
|
||||
"issued_at": "<RFC3339 timestamp>",
|
||||
"correlation_id": "<original-request-message-id>",
|
||||
"status": "ok",
|
||||
"reason_code": "ok",
|
||||
"payload": {}
|
||||
}
|
||||
```
|
||||
|
||||
`message_type` 为 `command.query.result` 或 `call.query.result`;`correlation_id` 等于原请求的 `message_id`。`status` 可为 `ok`、`pending`、`rejected`:
|
||||
|
||||
- `ok`:`reason_code=ok`,payload 为查询结果。
|
||||
- `pending`:`reason_code=waiting`,payload 是空 object;当前查询处理路径不生成该状态。
|
||||
- `rejected`:原因是 `invalid_request`、`not_found`、`conflict`、`expired`、`not_authorized`、`unavailable`、`unsupported` 之一;payload 为必填 `{ "detail": "...", "retryable": true|false }`。
|
||||
|
||||
#### `command.query.result` payload
|
||||
|
||||
必填:`command_id`、`command_type`、`tenant_id`、`tenant_key`、`status`、`aggregate_version`、`updated_at`。
|
||||
|
||||
可选:`task_id`、`execution_id`、`call_id`、`reason_code`、`wait_reason_code`、`accepted_at`、`waiting_since`、`admission_deadline`、`requested_task_revision`、`applied_task_revision`、`task_state`。`aggregate_version >= 1`。当前查询实现返回命令类型、租户绑定、状态、版本及更新时间;有对应事实时附带 `reason_code`、`accepted_at`。
|
||||
|
||||
#### `call.query.result` payload
|
||||
|
||||
必填:
|
||||
|
||||
- `call_id`、`execution_id`、`call_state`、`call_version`。
|
||||
- `attempts`:`call.status` 事件信封数组。
|
||||
- `transcript.events`:`transcript.updated` / `transcript.failed` 事件信封数组。
|
||||
- `recordings`:`recording.uploaded` / `recording.failed` 事件信封数组。
|
||||
- `delivery`:`pending`、`retry`、`dispatching`、`published` 四种 outbox 状态的非负计数。
|
||||
- `snapshot_at`:快照生成时间。
|
||||
|
||||
可选:`task_id`、`task_item_id`、`reason_code`、`outcome`、`started_at`、`ended_at`、`duration_ms`。查询快照按原事件信封返回明细;超过单条 MQ 大小限制时返回 `unavailable`,不返回部分快照。
|
||||
|
||||
## MQ Topic 与队列规则
|
||||
|
||||
### 交换机
|
||||
|
||||
| 名称 | 类型 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| `agent-call.dispatchers.v2` | durable topic | SaaS → Dispatcher 命令、查询和 AI 配置响应 |
|
||||
| `agent-call.saas.v2` | durable topic | Dispatcher → SaaS 事件、查询响应及 AI 配置请求 |
|
||||
| `agent-call.dead-letter.v2` | durable topic | Dispatcher 入站队列的死信交换机 |
|
||||
|
||||
三个交换机均不自动删除。
|
||||
|
||||
### 队列与精确 routing key
|
||||
|
||||
| 名称 / 模板 | 用途 | 绑定 |
|
||||
| --- | --- | --- |
|
||||
| `agent-call.d.<dispatcher_id>.owner.v2` | Dispatcher 身份占用标记;非 durable、exclusive,连接断开后释放 | 无 |
|
||||
| `agent-call.d.<dispatcher_id>.t.<tenant_key>.v2` | 该 Dispatcher、该租户的 SaaS→D durable inbox | `agent-call.dispatchers.v2`:`d.<dispatcher_id>.t.<tenant_key>.in` |
|
||||
| `agent-call.d.<dispatcher_id>.t.<tenant_key>.dlq.v2` | 对应 inbox 的 durable dead-letter queue | `agent-call.dead-letter.v2`:同一条 `.in` routing key |
|
||||
| `agent-call.saas.events.v2` | D→SaaS durable outbound queue | `agent-call.saas.v2`:每个 D/租户各自的 `d.<dispatcher_id>.t.<tenant_key>.out` |
|
||||
|
||||
- `<dispatcher_id>` 由部署提供,同一 Dispatcher 重启时复用,格式必须是规范小写 UUID v4;部署需保证全局唯一。独占 owner 队列可阻止同一 RabbitMQ broker 上并发重复占用该 ID,但不能证明跨 broker 全局唯一。SaaS 必须按目标 Dispatcher 的 ID 投递,不能广播后再靠消息正文筛选。
|
||||
- `<tenant_key>` 保留原值,不 trim、不编码、不截断。必须是非空有效 UTF-8,最多 196 字节;topic key 中独立的 `*`、`#` 段禁止。完整 AMQP routing key 和 queue 名称不得超过 255 字节。
|
||||
- 输入 routing key 必须精确匹配 `.in`;输出 routing key 必须精确匹配 `.out`。每个租户独立绑定,不使用通配绑定替代身份隔离。
|
||||
- 当前 broker 只允许向 `agent-call.saas.v2` 发布;发布前要求该租户的 `.out` route 已声明。SaaS 队列不存在或消息未路由时,发布不能算成功。
|
||||
|
||||
### 投递与确认
|
||||
|
||||
- 发布使用 `application/json`、persistent delivery、mandatory routing,并等待 publisher confirm;mandatory return、负确认、超时或连接异常均视为未确认交付。
|
||||
- Dispatcher 先将事件/响应写入 SQLite outbox;只有收到正向 publisher confirm 且未发生 return 后才标记为已发布。重试沿用原消息身份和内容。
|
||||
- publisher confirm 只表示 RabbitMQ 接受了消息,不表示 SaaS 业务已消费或处理。
|
||||
- SaaS→D 使用 durable inbox 和手动 ACK。处理成功并完成持久化后 ACK;暂时错误 requeue;永久错误 reject,由 inbox 的 dead-letter 配置送入该租户 DLQ。
|
||||
- 默认 prefetch 为 1。单条消息必须是 UTF-8 JSON,大小为 1–262144 字节。
|
||||
@@ -1,112 +0,0 @@
|
||||
# MQ-only V1 与 Dispatcher OSS 配置:已确认合同
|
||||
|
||||
状态:用户已确认本文件的身份、纯Topic、配置、消息及查询结构;随后通过目标修订确认**上传只负责可靠入队**,并单独确认 `recording.uploaded` 名称/字段及R13完成边界。旧上传会话/verified等待方案被替代,不新增VERIFYING。单一 V1 机读包已包含 MQ-only 与 AI JCS 规则并通过离线测试;本地运行切换、RabbitMQ往返和联合恢复已有证据,真实SaaS/生产验收仍不在本轮。
|
||||
|
||||
机读来源:`contracts/upstream/v1/`。`manifest.json`记录当前项目内 V1 文件哈希;不是外部SaaS签收。不另设运行时第二套契约或 HTTP 回退。当前目标由本会话Agent独立执行,不启动子Agent。
|
||||
|
||||
## 1. Topic 与租户边界
|
||||
|
||||
用户已选择**只用Topic,不增加headers exchange**:保留tenant_key原值;每D/租户独立接收队列;拒绝按点号分词后为`*`或`#`的独立词段。不能广播后过滤、编码/截断租户标识或换别名绕过。
|
||||
|
||||
| 资源 | 已确认名称/规则 |
|
||||
| --- | --- |
|
||||
| D接收exchange | `agent-call.dispatchers.v2`,durable topic |
|
||||
| SaaS接收exchange | `agent-call.saas.v2`,durable topic |
|
||||
| 死信exchange | `agent-call.dead-letter.v2`,durable topic |
|
||||
| D/租户接收队列 | `agent-call.d.<dispatcher_id>.t.<tenant_key>.v2`,durable |
|
||||
| 对应死信队列 | `agent-call.d.<dispatcher_id>.t.<tenant_key>.dlq.v2`,durable |
|
||||
| SaaS→D routing key | `d.<dispatcher_id>.t.<tenant_key>.in`,精确绑定 |
|
||||
| D→SaaS routing key | `d.<dispatcher_id>.t.<tenant_key>.out`,精确绑定 |
|
||||
| SaaS接收队列(本地合同) | `agent-call.saas.events.v2`,持久绑定获配置的D/租户出站键 |
|
||||
| D身份占用队列 | `agent-call.d.<dispatcher_id>.owner.v2`,exclusive、非持久,只锁身份 |
|
||||
|
||||
36字节UUID下最长死信队列固定部分59字节,tenant_key上限为**196个UTF-8字节**;仍逐一检查完整资源名255字节限制。Schema的字符数校验不替代运行时字节校验。非法/超长输入明确拒绝并保留源任务,不静默修正。
|
||||
|
||||
## 2. Dispatcher身份与恢复
|
||||
|
||||
- 配置文件提供稳定、规范小写UUID v4。部署时独立生成,不复制示例ID;启动时不自动生成/替换。
|
||||
- 初次使用与SQLite绑定,后续必须一致;更换ID沿用旧DB时拒绝,不清库或抹掉未知执行。
|
||||
- `dispatcher_epoch`是运行代次,不是稳定ID。请求、消息、资产与执行保持原归属。
|
||||
- 同一broker上的exclusive身份队列拒绝重复ID;业务队列必须持久且不能exclusive。连接断开拒新准入,不清未知占用。
|
||||
- 不宣称跨不同broker存在全局注册服务,不实现HA或自动换D。
|
||||
|
||||
## 3. 消息集合与关联
|
||||
|
||||
消息为persistent JSON、严格Schema、最大256KiB;未知字段拒绝,不新增任务mode字段。
|
||||
|
||||
### 3.1 命令与事件
|
||||
|
||||
- 命令保留`command_type`、`command_id`、租户、trace、issued/not_after及payload,新增`dispatcher_id`,`schema_version=2.0`。
|
||||
- 命令类型:`call.execute`、`task.control`、`call.replay`、`command.replay`。旧HTTP body的重复command_id归并到既有MQ信封,不维护两个独立命令身份。task_id/call_id/source_command_id来自原业务目标。
|
||||
- 事件保留`event_type`、`event_id`、租户、trace、occurred_at、aggregate和payload,新增来源`dispatcher_id`,版本2.0。
|
||||
- v2事件:`command.result`、`call.status`、`transcript.updated`、`call.finished`、`recording.uploaded`、`recording.failed`、`transcript.failed`、`contact.opt_out`。
|
||||
- **recording.uploaded取代本项目的recording.ready**,不冒用旧verified语义。其payload固定为`call_id`、`recording_id`、`upload_id`、`bucket`、`object_key`、`format`、`channels`、`sample_rate_hz`、`duration_ms`、`size_bytes`、`checksum_sha256`,不含TOKEN、密钥、签名URL或SaaS OSS ID。
|
||||
- 控制accepted不等于applied;整体补传保持原事件身份、版本和内容,不能重新投执行命令。
|
||||
|
||||
### 3.2 必要请求/响应
|
||||
|
||||
| 请求 | 方向 | 响应 |
|
||||
| --- | --- | --- |
|
||||
| `command.query` | SaaS→D | `command.query.result` |
|
||||
| `call.query` | SaaS→D | `call.query.result` |
|
||||
| `ai.config.request` | D→SaaS | `ai.config.result` |
|
||||
|
||||
上传不在此表:**不存在upload-session、complete/verified请求响应或等待SaaS回复的上传控制流程。**
|
||||
|
||||
公共请求字段:schema_version、message_type、message_id、dispatcher_id、tenant_id、tenant_key、trace_id、issued_at、not_after、payload。
|
||||
|
||||
响应使用对应公共身份字段,增加correlation_id、status、reason_code,**不带not_after**;correlation_id固定指原请求,dispatcher_id始终是业务所属D。status为ok/pending/rejected;reason_code严格按Schema分支校验。不可路由、超时、已受理但结果未知与业务拒绝分开,不把confirm当业务已应用。
|
||||
|
||||
本地服务请求30秒接收期限,不覆盖上游命令期限,也不让已受理结果失效。已知重复返回原决定;未知且过期拒绝;重启/迟到/乱序保留原关联,不另造ID重做业务。
|
||||
|
||||
### 3.3 查询结构
|
||||
|
||||
用户已批准补齐旧Call中的空白object定义:
|
||||
|
||||
- attempts:已有call.status事件集合。
|
||||
- transcript.events:已有transcript.updated/transcript.failed事件集合。
|
||||
- recordings:recording.uploaded/recording.failed事件集合。
|
||||
- delivery:pending、retry、dispatching、published四种已有投递状态的非负计数。
|
||||
|
||||
保留原顶层通话字段,只读取持久事实,不伪造通话/资产结果。快照受256KiB限制,超限明确失败,不截断后伪称完整。AI快照/授权沿用原严格配置Schema;原OpenAPI receipt/config的allOf组合展平为同一闭合对象,不放宽字段。
|
||||
|
||||
## 4. D配置文件与临时TOKEN
|
||||
|
||||
D通过`--config <path>`读取严格JSON,未知字段、重复键、缺失或无效值报错;移除旧D OSS flags/隐式环境覆盖,不保留并行配置源。
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6",
|
||||
"oss": {
|
||||
"endpoint": "https://oss.example.invalid",
|
||||
"region": "example-region",
|
||||
"bucket": "example-bucket",
|
||||
"object_prefix": "recordings",
|
||||
"access_key_id_env": "DISPATCHER_OSS_ACCESS_KEY_ID",
|
||||
"access_key_secret_env": "DISPATCHER_OSS_ACCESS_KEY_SECRET"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这是字段示例,不是可部署的凭据/ID。文件明确引用受控凭据变量;环境仅提供这些凭据,不覆盖endpoint/bucket等配置。监听、TLS、持久目录等既有部署选项不全部搬迁。
|
||||
|
||||
D复用官方SDK的预签名PUT及必要headers,沿用UploadGrant作为临时TOKEN交付形式,不另造JWT/STS服务或签名算法。有效期固定900秒;A每次获准尝试只PUT一次,失败/过期保留文件,显式重新RequestUpload才换授权;不启用自动重传,不宣称OSS原生强制一次性。
|
||||
|
||||
## 5. R12/R13与上传通知交付边界
|
||||
|
||||
1. R12由D读取配置、校验原租户/执行/资产并提供临时TOKEN,**不向SaaS申请会话或授权**。
|
||||
2. Agent直接PUT对象,保存真实结果与本地恢复记录,再经R13报告原资产、upload_id、大小和摘要;文件字节不经过D。
|
||||
3. D校验与原grant/资产一致,把上传事实和固定event_id的recording.uploaded outbox同事务保存。重复R13不得生成第二资产/通知身份。
|
||||
4. 通知以persistent消息发送至正确的durable队列/绑定,启用mandatory并确认没有return,再取得publisher confirm;只有满足这些条件才记交付完成。
|
||||
5. **R13完成仅表示本项目已把事实交付MQ**,不是SaaS已处理。不返回虚构OSS ID,不发recording.ready,不新增VERIFYING或最终校验查询协议;原实现中的OSS ID返回/等待路径随实现删除。
|
||||
6. broker断连、不可路由、confirm丢失或崩溃时保留持久记录和原event_id,恢复通知交付。仅写本地outbox、发到无队列exchange或未确认时均不能算完成。
|
||||
7. 元信息重报/消息重投不触发第二次PUT;通知确认前保留所需恢复信息及源文件,不靠SaaS消费回复决定完成。AI/控制等必要请求响应不受此收缩影响。
|
||||
|
||||
## 6. 验证与事实边界
|
||||
|
||||
- TDD;v2 Schema正反例、来源/文件哈希、拓扑与Go路由规则一致性检查。旧v1不改。
|
||||
- 运行时须验证指定本地持久队列实际接收、不可路由/confirm丢失/重启恢复及无SaaS消费者也能完成交付;离线Schema通过不替代这些检查。
|
||||
- 基线总覆盖率32.1%,排除生成代码诊断值44.2%,均未达65%;不能只挑新增文件宣称整体达标。
|
||||
- 不接真实云/供应商/SaaS,不真实拨号;部署诊断缺失不能伪造通过。
|
||||
- RabbitMQ官方Topic/队列规则参考:https://www.rabbitmq.com/docs/exchanges 、https://www.rabbitmq.com/docs/queues 。公开检索确认点号分词、通配词段与exclusive单连接;直接抓取受工具fake-IP检查阻止,未宣称完整在线文档或PoC已验收。
|
||||
@@ -1,250 +0,0 @@
|
||||
# SaaS ↔ Dispatcher 对接契约(MQ-only 设计与实现差异)
|
||||
|
||||
## 1. 适用范围与事实等级
|
||||
|
||||
**用户已确认:RabbitMQ 是 SaaS 与 Dispatcher 的唯一交互通道,双方之间禁止任何 HTTP 请求或回调。每个 Dispatcher 都有独立、全局唯一的 ID,并通过各自独立的专用 Topic 接收事件。** 本规则覆盖执行、控制、查询、补传、AI 配置与授权、recording.uploaded上传事实通知,不保留HTTP特例或回退;上传不等待SaaS会话或校验回复。
|
||||
|
||||
**OSS 补充确认:OSS 相关配置存于 Dispatcher 配置文件;Agent 经 Unary 向 Dispatcher 领取临时上传 TOKEN 后直传 OSS,不持有长期凭据。SaaS 不再下发 OSS 配置或上传 TOKEN。用户随后修订目标:本项目只保证上传事实可靠进入指定持久MQ队列,不关心SaaS后续处理;不等待上传会话、verified或OSS ID,不新增VERIFYING。**
|
||||
|
||||
本文区分三种事实:
|
||||
|
||||
| 层级 | 本次状态 | 使用边界 |
|
||||
| --- | --- | --- |
|
||||
| 已确认设计 | MQ-only、Dispatcher 唯一身份、独立 Topic | 后续设计与实现必须遵守 |
|
||||
| 当前项目内契约 | 唯一 V1 包中的身份/Topic/消息/配置/正反例、哈希和 AI JCS 规则 | 以[mq-only V1冻结方案](mq-only-v1-freeze-proposal.md)及机读包为准;本地运行往返已有证据,外部SaaS签收仍不在本轮 |
|
||||
| 现有实现/旧包 | 下列旧字段、路由及代码事实 | 仅用于识别差异,不代表新设计已实现或通过验收 |
|
||||
|
||||
当前固定包为 `contracts/upstream/v1/`(`contracts.SourceCommit = v1`)。其中不包含已归档的 HTTP OpenAPI,也不使用仅按租户路由的旧拓扑;当前 V1 包的 Schema、拓扑、正反例和哈希是唯一运行输入。不能在包外另造版本或通过放宽 `additionalProperties` 绕过冻结。
|
||||
|
||||
当前已完成本地实现阶段;唯一 V1 机读包、AI摘要、离线正反例、路由规则及RabbitMQ运行往返均有证据。下文旧字段/代码描述仅用于解释差异;运行证据不等于外部SaaS签收。
|
||||
|
||||
## 2. 通信拓扑、Dispatcher 身份与交付语义
|
||||
|
||||
### 2.1 唯一通道与边界
|
||||
|
||||
| 交互 | 唯一允许的路径 | 语义 |
|
||||
| --- | --- | --- |
|
||||
| SaaS 下发执行、控制、查询、补传 | SaaS → RabbitMQ → 指定 Dispatcher 专用 Topic/队列 | 持久受理不等于执行完成;响应仍经 MQ |
|
||||
| Dispatcher 回传结果、查询响应和业务事件 | Dispatcher → RabbitMQ → SaaS 专用订阅 | 能识别来源 Dispatcher、租户、原请求及业务对象 |
|
||||
| Dispatcher 获取 AI 配置/授权 | Dispatcher → RabbitMQ → SaaS;SaaS → RabbitMQ → 原 Dispatcher 专用订阅 | 固定租户和不可变版本,响应不能被其它 Dispatcher 消费 |
|
||||
| 上传完成通知 | Dispatcher → RabbitMQ指定持久队列 | recording.uploaded仅含事实元信息;入队即完成本项目交付,不等待SaaS消费/会话/verified/OSS ID |
|
||||
| 临时上传 TOKEN 领取/显式重新申请 | Agent ↔ Unary ↔ Dispatcher | D 依据自身配置文件提供受限 TOKEN/上传目标;不向 SaaS 申请 TOKEN |
|
||||
| Dispatcher ↔ Agent | 既有 Unary gRPC | 不改为内部 MQ,也不让 Agent 直连 SaaS |
|
||||
| Agent → OSS | 受限目标上的直接 PUT | 保留 HTTP(S) 对象上传;禁止的是 SaaS↔Dispatcher HTTP,不是 OSS/ARI/供应商协议或 gRPC 的 HTTP/2 |
|
||||
|
||||
### 2.2 全局唯一身份与独立 Topic
|
||||
|
||||
- `dispatcher_id` 是当前 V1 运行信封和路由中的独立全局身份;每个 Dispatcher 的 ID 必须独立、全局不重复,不能拿租户 ID、Agent ID、Cell ID、地址或启动代次代替。
|
||||
- Dispatcher 身份与 `dispatcher_epoch` 分开:前者识别 Dispatcher,后者用于一次运行所有权/会话的 fencing。身份持久绑定、重复身份占用、重启恢复及失效会话拒绝已有本地测试;不因 epoch 改变就丢弃原消息、执行或资产归属。
|
||||
- 每个 Dispatcher 有独立的接收 Topic 及对应队列/绑定;多个 Dispatcher **不能共用一条接收队列竞争消费指定目标的消息,也不能全部订阅同一广播 Topic 后仅靠正文过滤**。
|
||||
- RabbitMQ 的 Topic 订阅由 exchange、routing key、queue 和 binding 表达;当前固定为 `agent-call.dispatchers.v2`、`agent-call.saas.v2`、`agent-call.dead-letter.v2` 及每个 Dispatcher/租户的独立队列和精确路由键,字段以唯一 V1 机读契约包为准。
|
||||
- SaaS 发给 D1 的命令、配置、授权和上传结果,只能进入 D1 的专用接收路径;D2 的路径与之独立。D1 发出的响应/事件须能回溯 D1 与原请求。SaaS 订阅布局亦由同一版契约定义,不假定现有共享结果队列已满足新约束。
|
||||
- 独立 Dispatcher 路由不替代租户隔离:保留租户独立队列、有界窗口、原值 `tenant_key` 和复合幂等语义;新拓扑必须同时区分 Dispatcher 与租户,不能退化为 Dispatcher 内所有租户共享无界队列。
|
||||
- `tenant_key` 不清洗、编码或截断。旧布局的 224 UTF-8 字节预算不能在加上 Dispatcher 身份后直接照搬;W01 须校验完整 routing key/queue 名长度及分隔符、通配符边界,超限拒绝发布并保留源任务,不改变既有租户标识。
|
||||
|
||||
P1 仍只运行一个单活 Dispatcher。现在必须在合同及本地路由测试中区分两个 Dispatcher 身份;这不授权多节点上线、多 Dispatcher 共享配额、自动选主、HA 或自动迁移任务。未知执行不得因目标离线而改投另一个 Dispatcher。
|
||||
|
||||
### 2.3 请求、响应、持久化和恢复
|
||||
|
||||
1. 所有请求和响应都走 MQ;异步响应必须关联原请求、目标/来源 Dispatcher、原租户及业务对象。精确键名、关联方式、消息枚举、错误与期限在 W01 冻结;`trace_id` 不能代替业务幂等身份。
|
||||
2. 发送意图/业务变更与 outbox 同事务;接收方持久 inbox 和处理状态后才 ACK。相同业务请求的重投返回原决定,同身份异内容冲突,不能生成第二次拨号或上传资产。
|
||||
3. publisher confirm、消费者ACK、业务accepted和控制applied各自独立;上传只验证可靠入队,不增加SaaS verified条件。confirm 只说明 broker 接收,不等于对端已应用;接收 ACK 不能代替业务响应。
|
||||
4. 响应重复、乱序、迟到、丢失和重启后恢复必须按原关联处理;响应等待有界,不跨网络持有 SQLite 写事务。超时表示未获确定结果,不等于业务失败,不允许 HTTP 查询兜底、换 ID 重拨或静默换 Dispatcher。
|
||||
5. 队列满、无绑定/不可路由、broker 断连必须可见并保留原消息。不能通过 confirm 单独认定路由成功;须覆盖 mandatory/return 和指定持久队列接收证据;上传无需SaaS消费者回复。
|
||||
6. MQ 往返响应不意味着新增一套任意 application receipt 协议。已有控制等业务结果保留;上传会话/verified往返已移出本项目;需要补齐的响应消息必须进入版本化契约,不能借现有八类业务事件自由透传。
|
||||
|
||||
## 3. RabbitMQ 执行命令:旧基线与待改项
|
||||
|
||||
### 3.1 旧拓扑(仅作实现差异记录,禁止用于新接入)
|
||||
|
||||
| 元素 | 旧值 | 新设计差异 |
|
||||
| --- | --- | --- |
|
||||
| command exchange | `agent-call.commands.v1`,durable `direct` | 须按 §2 冻结面向指定 Dispatcher 的 Topic 拓扑 |
|
||||
| tenant queue | `agent-call.executor.{tenant_key}.v1` | 没有 Dispatcher 身份,不能让多个 D 共用 |
|
||||
| routing key | `agent-call.tenant.{tenant_key}.call.execute` | 仅区分租户/操作,不能唯一指定 Dispatcher |
|
||||
| event exchange | `agent-call.events.v1`,durable `topic` | 新发布路径须可识别来源 Dispatcher |
|
||||
| dead-letter exchange | `agent-call.dead-letter.v1`,durable `topic` | 恢复必须保留原 Dispatcher、租户和消息身份 |
|
||||
| 默认 prefetch | `1` | 保持有界消费;不是多 Dispatcher 隔离证明 |
|
||||
|
||||
旧实现按消费租户声明 command queue 和 `.dlq.v1`,SaaS 结果队列基线为 `agent-call.saas.events.v1`、binding `agent-call.#`。这些名称仅记录旧实现事实,不构成当前拓扑;当前 V1 运行使用精确 Dispatcher/租户路由。
|
||||
|
||||
### 3.2 旧 `call.execute` 外壳
|
||||
|
||||
以下是旧 Schema 的精确字段记录,`additionalProperties: false`;新 Dispatcher 路由与关联尚未进入该外壳,不能直接追加字段并宣称兼容。
|
||||
|
||||
| 字段 | 类型 | 必填/约束 |
|
||||
| --- | --- | --- |
|
||||
| `schema_version` | string | 旧版固定 `1.0` |
|
||||
| `command_type` | string | 固定 `call.execute` |
|
||||
| `command_id` | string | 1–128 字节;不可含空白、`/`、`\\` |
|
||||
| `tenant_id` | string | 同上 |
|
||||
| `tenant_key` | string | 非空有效 UTF-8;旧实现额外限制 224 字节 |
|
||||
| `trace_id` | string | 同 `id` 约束 |
|
||||
| `issued_at` | RFC3339 时间 | 必填 |
|
||||
| `not_after` | RFC3339 时间 | 必填;不能因重投延期 |
|
||||
| `payload` | object | 符合 `executePayload` |
|
||||
|
||||
### 3.3 旧 `payload` 数据结构
|
||||
|
||||
| 字段 | 类型 | 约束 |
|
||||
| --- | --- | --- |
|
||||
| `execution_id` | string | 必填 ID |
|
||||
| `task_id` | string | 必填 ID |
|
||||
| `task_item_id` | string | 必填 ID |
|
||||
| `task_revision` | integer | `>= 1` |
|
||||
| `callee` | string | 1–256 字符;保留原始被叫号码 |
|
||||
| `route_policy_id` | string | 必填 ID |
|
||||
| `caller_profile_id` | string | 必填 ID |
|
||||
| `agent_version_id` | string | 必填 ID |
|
||||
| `variables` | object | 必填;旧 Schema 允许附加属性,业务白名单仍受源约束 |
|
||||
| `ring_timeout_ms` | integer | `>= 1` |
|
||||
| `max_call_duration_ms` | integer | `>= 1` |
|
||||
|
||||
对应 `internal/contract.ExecutePayload`;`payload` 以 `json.RawMessage` 保留原始 JSON。改传输不授权改变业务号码、版本、摘要或新增 MQ mode 字段。
|
||||
|
||||
### 3.4 旧接收事实与新验收要求
|
||||
|
||||
旧 `ConsumeTenant` 校验租户、声明队列,以 `prefetch=1` 消费;`AcceptCommand`/`Store.IngestCommand` 校验源 Schema、routing key、`not_after` 和原始 body SHA-256。当前以 `command_id` 查 inbox,同 ID 同 body 为重复、异 body 为冲突;新命令同事务写 inbox、task、`command.result(accepted)` outbox。成功后 ACK;永久错误 `Reject(false)`,其余错误 `Nack(requeue=true)`。
|
||||
|
||||
旧初始结果 payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"command_id": "<command_id>",
|
||||
"command_type": "call.execute",
|
||||
"execution_id": "<execution_id>",
|
||||
"status": "accepted",
|
||||
"reason_code": "accepted",
|
||||
"requested_task_revision": 1
|
||||
}
|
||||
```
|
||||
|
||||
新验收须补充 §2 的 Dispatcher 定向/来源校验、所有 MQ 交互的关联和持久恢复,以及租户复合幂等要求。不能把当前仅按 `command_id` 查重的事实写成这些要求已满足。
|
||||
|
||||
## 4. RabbitMQ 业务事件:旧字段语义与新路由要求
|
||||
|
||||
### 4.1 旧通用外壳
|
||||
|
||||
旧 routing key 为 `agent-call.{event_type}`;新来源 Dispatcher 的表达待 W01 冻结。以下字段在旧版全部必填:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"event_id": "<id>",
|
||||
"event_type": "<event_type>",
|
||||
"tenant_id": "<id>",
|
||||
"tenant_key": "<原值>",
|
||||
"trace_id": "<id>",
|
||||
"occurred_at": "2026-09-19T00:00:00Z",
|
||||
"aggregate_type": "<aggregate>",
|
||||
"aggregate_id": "<id>",
|
||||
"aggregate_version": 1,
|
||||
"payload": {}
|
||||
}
|
||||
```
|
||||
|
||||
源枚举为 `command.result`、`call.status`、`transcript.updated`、`call.finished`、`recording.uploaded`、`recording.failed`、`transcript.failed`、`contact.opt_out`。它们不自动覆盖新增的配置/查询/上传响应消息。
|
||||
|
||||
事件同时通过 `mq.schema.json` 和 `event-payloads.schema.json`;`EventBuilder` 拒绝未知字段。旧实现由 SQLite 按 `aggregate_type + aggregate_id` 递增版本,Agent 不能指定版本;新基线仍须验证租户/Dispatcher 归属,不将旧实现等同完整隔离。
|
||||
|
||||
### 4.2 当前代码实际生成的事件
|
||||
|
||||
| 事件 | 当前代码行为 |
|
||||
| --- | --- |
|
||||
| `command.result` | 命令接收及 `EXECUTION_ACCEPTED` fact 生成 |
|
||||
| `call.status` / `call.finished` | 对应 Agent fact 经 Dispatcher 校验后生成 |
|
||||
| `transcript.updated` | 实时文字;不得改名 `call.transcript` |
|
||||
| `transcript.failed` | 当前映射为 `aggregate_type=transcript`,但 Schema 要求 `transcript_segment`,该路径阻塞 |
|
||||
| `contact.opt_out` | 对应 Agent fact 经 Dispatcher 校验后生成 |
|
||||
| `recording.uploaded` | 当前上传事实;D同事务保存事实及outbox,可靠进入指定持久队列后完成本项目交付,不代表SaaS已处理 |
|
||||
| `recording.failed` | Schema 已定义,当前无对应 FactKind/生成路径 |
|
||||
|
||||
`RECORDING_PROGRESS` 只保存 fact,不发布 MQ 事件。上述已知实现差异不因本次文档改写而消失。
|
||||
|
||||
### 4.3 旧专属 payload 关键字段
|
||||
|
||||
完整约束仍在固定包 `event-payloads.schema.json`;下表不是第二套 Schema。
|
||||
|
||||
| 事件 | 必填字段 |
|
||||
| --- | --- |
|
||||
| `command.result` | `command_id`, `command_type`, `status`, `reason_code` |
|
||||
| `call.status` | `call_id`, `execution_id`, `call_state`, `call_version`, `attempt_id`, `attempt_state` |
|
||||
| `transcript.updated` | `call_id`, `turn_id`, `segment_id`, `role`, `revision`, `text`, `is_final`, `start_ms`, `end_ms`, `playback_state` |
|
||||
| `call.finished` | `call_id`, `execution_id`, `call_version`, `outcome`, `started_at`, `ended_at`, `duration_ms`, `reason_code` |
|
||||
| `recording.uploaded` | `call_id`, `recording_id`, `upload_id`, `bucket`, `object_key`, `format`, `channels`, `sample_rate_hz`, `duration_ms`, `size_bytes`, `checksum_sha256` |
|
||||
| `recording.failed` | `call_id`, `recording_id`, `stage`, `reason_code`, `retryable` |
|
||||
| `transcript.failed` | `call_id`, `reason_code`, `retryable` |
|
||||
| `contact.opt_out` | `call_id`, `task_id`, `task_item_id`, `requested_at` |
|
||||
|
||||
`recording.uploaded`只表示上传事实已可靠进入指定持久队列,不表示SaaS已消费或处理;字段与可靠入队边界见§6.1。
|
||||
|
||||
### 4.4 Outbox 交付
|
||||
|
||||
旧 `Dispatcher.FlushOutbox` claim `pending/retry` 为 `dispatching`,发布 persistent JSON 并等 confirm;成功记 `published`,失败记 `retry`,重启把 `dispatching` 恢复为 `retry`。重复 fact 同 `fact_id + content_sha256` 不生成第二条事件,异摘要冲突。SaaS 按租户/事件身份幂等应用。
|
||||
|
||||
新设计将这一持久交付原则覆盖请求与响应,补齐不可路由、来源/目标、相关状态恢复验证。业务事件重投保留原身份、内容和域版本,broker confirm 不替代 SaaS 应用收讫。
|
||||
|
||||
## 5. SaaS → Dispatcher:控制、查询和补传全部经 MQ
|
||||
|
||||
### 5.1 待冻结内容
|
||||
|
||||
消息类型、信封和响应枚举以唯一 V1 机读包及[mq-only V1合同](mq-only-v1-freeze-proposal.md)为准。旧HTTP header、URL和状态码不是MQ合同,不能塞进旧call.execute或宽松metadata中。
|
||||
|
||||
### 5.2 控制任务
|
||||
|
||||
SaaS 将 pause/resume/stop 控制发到目标 Dispatcher 专用 Topic。保留原 `command_id`、租户/任务归属、`expected_task_revision` CAS、`active_call_policy=drain|hangup` 与原因语义。D 持久受理后经 MQ 回报 accepted;经 Agent 屏障/挂断事实确认后才能回报 applied。重复控制不重复增加 revision,冲突不能伪装成功,stopped 不可恢复。
|
||||
|
||||
### 5.3 查询命令与通话
|
||||
|
||||
请求和响应都经 MQ;查询固定原 `command_id` 或 `call_id`,返回可证明的命令、执行、控制、通话及独立资产状态。响应关联原查询和目标 Dispatcher;不存在、保留过期与暂时不可用分开表达,精确错误码待冻结。超时不走 HTTP 补查,也不证明原执行未发生。
|
||||
|
||||
### 5.4 整体补传
|
||||
|
||||
仅允许以 `call_id` 或 `source_command_id` 请求整体业务结果补传,不增加 task/execution 范围或局部筛选。固定受理截止点,重发原事件 ID/内容/版本,实时优先、分批有界;补传自身结果不能递归进入集合。
|
||||
|
||||
补传不是把 `call.execute` 重新发布来重新执行。当前MQ replay只重送已持久的原业务事实或原命令回执,重复、迟到和重启沿原消息身份恢复,不创建任务、不拨号、不新建资产。
|
||||
|
||||
### 5.5 已废弃 HTTP 入口的处理
|
||||
|
||||
旧`internal/control/` HTTP业务实现及测试、Dispatcher HTTP启动路径、CLI参数和环境配置均已删除。执行、控制、查询、整体补传和AI配置/授权已有MQ本地往返、重复、错目标、迟到/重启恢复证据;不以HTTP删除代替外部SaaS验收。
|
||||
|
||||
## 6. Dispatcher → SaaS:AI 配置与上传业务协调经 MQ
|
||||
|
||||
### 6.1 Dispatcher配置、临时TOKEN与recording.uploaded
|
||||
|
||||
1. OSS配置唯一来自D严格JSON配置文件;A经R12取得D用官方SDK提供的15分钟预签名PUT及headers,不持长期凭据,不向SaaS申请会话或授权。字段已在v2配置Schema冻结。
|
||||
2. A仅执行一次PUT并持久记录结果,R13只报告原upload_id、binding、资产、大小与摘要;D校验后将事实和固定event_id的outbox同事务保存。
|
||||
3. D发布persistent的`recording.uploaded`到正确durable队列/绑定,启用mandatory并处理return。收到publisher confirm且确认未被退回,才能将本次交付记为完成;只写本地outbox或无队列的exchange不算成功。
|
||||
4. 通知payload固定为call_id、recording_id、upload_id、bucket、object_key、format、channels、sample_rate_hz、duration_ms、size_bytes、checksum_sha256,不含TOKEN、密钥、签名URL或SaaS OSS ID。
|
||||
5. broker故障、无绑定、confirm丢失和重启保留原消息身份并恢复通知,不重新PUT、新建资产或重新拨号。源文件与恢复记录在通知未确认前保留;TOKEN过期仅显式向D重申请。
|
||||
6. **R13完成只表示本项目已可靠交付MQ,不表示SaaS已消费/处理。** 不新增VERIFYING,不等待SaaS上传会话、verified或OSS ID,不发recording.ready。AI/控制等必要响应仍按各自合同处理。
|
||||
|
||||
D现有SDK签发能力保留;旧D直接HEAD校验并生成ready/OSS ID的完成路径已删除,不能保留为兼容层。当前handler已接入上述新入队完成边界;本地RabbitMQ无绑定、确认丢失、重启和原消息恢复已有证据,不等于真实OSS/SaaS联调。
|
||||
|
||||
### 6.2 AI 不可变配置与授权
|
||||
|
||||
D 根据 MQ 任务中的原 `tenant_id/tenant_key + agent_version_id`,通过 MQ 向 SaaS 获取不可变配置及有效授权,SaaS 通过原 D 专用 Topic 返回;授权/撤销等交互同样不能走 HTTP。
|
||||
|
||||
保留既有版本、`immutable`、`content_sha256`、`config` 及租户授权语义;源 Schema、不可变摘要、有效期、撤销、能力和供应商受控引用均校验后持久绑定到原执行,再交付 Agent。缓存按租户/版本隔离,在途/原排队任务不漂移,同版本异内容拒绝,0/false 与未提供保真;无有效授权时拒绝新准入,不用 latest、CLI/env 或 SDK 默认值兜底。
|
||||
|
||||
旧 `ai-config.openapi.yaml` 的 AI GET 已废弃为 D↔SaaS 接入方式,不再开发该 HTTP client。Dispatcher当前通过MQ请求/接收内嵌不可变配置和授权,按租户、版本、摘要、有效期、撤销和出口持久校验;实际本地RabbitMQ往返及SQLite重启证据见`docs/evidence/mq-ai-local-roundtrip.md`。Agent动态交付仍受现有Unary快照载体边界约束,不凭启动fixture宣称外部动态交付。
|
||||
|
||||
## 7. 当前门禁状态与禁止误读
|
||||
|
||||
| 门禁 | 完成证据 | 当前状态 |
|
||||
| --- | --- | --- |
|
||||
| W01 身份/Topic/消息冻结 | v2项目内Schema、路由、正反例及哈希 | 已生成并离线验证;不是运行时或外部签收 |
|
||||
| W05/W12 MQ 控制面 | 全部交互持久接收/响应、移除旧 HTTP、补传语义正确 | 本地完成;见`mq-control-recovery.md`、查询/补传证据;外部SaaS不在本轮 |
|
||||
| W07 MQ AI | 不可变配置/授权、迟到/撤销/重复与缓存隔离 | 本地MQ请求/响应、重复、范围和SQLite恢复完成;见`mq-ai-local-roundtrip.md` |
|
||||
| W02/W11 上传授权与通知入队 | D配置→临时TOKEN→A直传;recording.uploaded可靠入队后R13完成,无SaaS等待 | 本地完成;见`20260921-mq-upload-progress.md`;不宣称SaaS消费 |
|
||||
| W13/W14 本地联合回归 | D1/D2 Topic 隔离 fixture、身份冲突、broker 故障、全流程无 SaaS↔D HTTP | 本地完成;全仓质量和部署诊断状态见最终证据,真实供应商/生产仍延期 |
|
||||
|
||||
路由 fixture 只验证不同 Dispatcher 互不抢收,不把多 Dispatcher 调度或真实 SaaS 联调引入本轮。旧包、旧 HTTP handler、单租户 broker 测试和本地 OSS 成功,均不代表以上门禁通过。
|
||||
|
||||
## 8. 依据与相关文档
|
||||
|
||||
- [计划与需求阅读索引](../plan-0918.md):§1.2、§8.2 的 MQ-only 修订与状态。
|
||||
- [时间泳道图](./saas-rabbitmq-oss-dispatcher-agent-timeline.md):已按当前recording.uploaded边界更新;外部SaaS/真实OSS处理仍不在本地证据范围。
|
||||
- [Dispatcher ↔ Agent 契约](./dispatcher-agent.md):现有 Proto/handler 事实、15分钟授权和上传通知恢复边界。
|
||||
- 旧实现事实:`internal/contract/contract.go`、`internal/mq/amqp.go`、`internal/tenant/routing.go`、`internal/dispatcher/consumer.go`、`internal/dispatcher/dispatcher.go`、`internal/store/store.go`、`internal/store/facts.go`。旧HTTP源码仅可在历史基线中查阅,不是当前运行路径。
|
||||
- 当前固定包:`contracts/upstream/v1/` 下的 `mq.schema.json`、`event-payloads.schema.json`、`mq-topology.json`、AI/Dispatcher/OSS/静态 Cell Schema 及业务正反例;归档目录中的旧 OpenAPI 和历史包不属于运行输入。
|
||||
@@ -1,176 +0,0 @@
|
||||
# SaaS → MQ → Dispatcher 消息定义
|
||||
|
||||
本文定义 SaaS 发往 RabbitMQ、由 Dispatcher 消费的当前消息结构。消息使用 UTF-8 JSON,`schema_version` 固定为 `2.0`;封闭的信封和 payload 拒绝未知字段,未定义的消息类型拒绝处理。明确开放的区域只有 `call.execute.variables`、AI 配置 `metadata` 及 AI 快照外层的扩展字段。单条消息体上限为 256 KiB。命令和 Service 信封中的普通 ID 为非空 JSON string,最多 128 个字符,且不得含空白、`/` 或 `\\`;时间为 RFC3339 JSON string。`dispatcher_id`、`tenant_key` 使用各自规则。
|
||||
|
||||
## 消息类型
|
||||
|
||||
| 顶层类型 | `command_type` / `message_type` | Dispatcher 处理 |
|
||||
| --- | --- | --- |
|
||||
| 命令 | `call.execute` | 创建并接收外呼任务 |
|
||||
| 命令 | `task.control` | 暂停、恢复或停止任务 |
|
||||
| 命令 | `call.replay` | 重发指定通话的已持久化业务事件 |
|
||||
| 命令 | `command.replay` | 根据源命令重发已持久化业务事件 |
|
||||
| Service request | `command.query` | 查询已接收命令状态,回复 `command.query.result` |
|
||||
| Service request | `call.query` | 查询通话快照,回复 `call.query.result` |
|
||||
| Service response | `ai.config.result` | 接收与原请求绑定的不可变 AI 配置快照及授权 |
|
||||
|
||||
所有 SaaS→Dispatcher 消息都必须发往目标 Dispatcher 和租户的精确 `.in` routing key;交换机、队列和 routing key 见 [`dispatcher-mq.md`](dispatcher-mq.md)。
|
||||
|
||||
## 命令信封
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": "2.0",
|
||||
"command_type": "call.execute",
|
||||
"command_id": "<id>",
|
||||
"dispatcher_id": "<canonical-lowercase-uuid-v4>",
|
||||
"tenant_id": "<id>",
|
||||
"tenant_key": "<original-tenant-key>",
|
||||
"trace_id": "<id>",
|
||||
"issued_at": "<RFC3339 timestamp>",
|
||||
"not_after": "<RFC3339 timestamp>",
|
||||
"payload": {}
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 / 规则 |
|
||||
| --- | --- |
|
||||
| `schema_version` | 固定 `2.0` |
|
||||
| `command_type` | `call.execute`、`task.control`、`call.replay`、`command.replay` 之一 |
|
||||
| `command_id`、`tenant_id`、`trace_id` | 非空 ID,最多 128 个字符;不得含空白、`/` 或 `\\` |
|
||||
| `dispatcher_id` | 规范小写 UUID v4;必须与目标 Dispatcher 一致 |
|
||||
| `tenant_key` | 原值透传;非空有效 UTF-8,最多 196 字节;不得含独立的 `*` 或 `#` topic 段 |
|
||||
| `issued_at`、`not_after` | RFC3339 时间。`now >= not_after` 时消息过期,不执行 |
|
||||
| `payload` | 随 `command_type` 使用下列唯一结构;不接受额外字段 |
|
||||
|
||||
### `call.execute`
|
||||
|
||||
以下字段全部必填:
|
||||
|
||||
| 字段 | 类型 / 规则 |
|
||||
| --- | --- |
|
||||
| `execution_id`、`task_id`、`task_item_id` | ID |
|
||||
| `task_revision` | 整数,`>= 1` |
|
||||
| `callee` | 非空字符串,最多 256 字符;Dispatcher 保留原值 |
|
||||
| `route_policy_id`、`caller_profile_id`、`agent_version_id` | ID |
|
||||
| `variables` | JSON object;业务变量字段由任务内容决定 |
|
||||
| `ring_timeout_ms`、`max_call_duration_ms` | 整数,`>= 1` |
|
||||
|
||||
### `task.control`
|
||||
|
||||
| 字段 | 必填 | 类型 / 规则 |
|
||||
| --- | --- | --- |
|
||||
| `task_id` | 是 | ID |
|
||||
| `action` | 是 | `pause`、`resume`、`stop` |
|
||||
| `expected_task_revision` | 是 | 整数,`>= 1`;按该版本做 CAS 校验 |
|
||||
| `reason` | 是 | 非空字符串,最多 512 字符 |
|
||||
| `active_call_policy` | 否 | `drain` 或 `hangup` |
|
||||
|
||||
### `call.replay`
|
||||
|
||||
- `call_id`:必填 ID。
|
||||
- `reason`:必填非空字符串,最多 512 字符。
|
||||
|
||||
### `command.replay`
|
||||
|
||||
- `source_command_id`:必填 ID。
|
||||
- `reason`:必填非空字符串,最多 512 字符。
|
||||
|
||||
重放只恢复原业务事件的交付,不创建新的执行、通话或录音资产,也不改变原事件身份和消息体。
|
||||
|
||||
## Service request 信封
|
||||
|
||||
`command.query` 和 `call.query` 共用以下信封,所有字段必填:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": "2.0",
|
||||
"message_type": "command.query",
|
||||
"message_id": "<id>",
|
||||
"dispatcher_id": "<canonical-lowercase-uuid-v4>",
|
||||
"tenant_id": "<id>",
|
||||
"tenant_key": "<original-tenant-key>",
|
||||
"trace_id": "<id>",
|
||||
"issued_at": "<RFC3339 timestamp>",
|
||||
"not_after": "<RFC3339 timestamp>",
|
||||
"payload": {}
|
||||
}
|
||||
```
|
||||
|
||||
| `message_type` | `payload` 必填字段 |
|
||||
| --- | --- |
|
||||
| `command.query` | `command_id`:ID |
|
||||
| `call.query` | `call_id`:ID |
|
||||
|
||||
Dispatcher 回复的 `*.result` 信封和 payload 定义见 [`dispatcher-mq.md`](dispatcher-mq.md)。相同 `message_id` 的重复请求必须保持消息体一致;相同 ID 不同内容会被拒绝。查询响应先持久化后确认输入消息。
|
||||
|
||||
## `ai.config.result`
|
||||
|
||||
这是对 Dispatcher 原始 `ai.config.request` 的响应。所有字段必填;该响应信封不含 `not_after`:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": "2.0",
|
||||
"message_type": "ai.config.result",
|
||||
"message_id": "<response-id>",
|
||||
"dispatcher_id": "<canonical-lowercase-uuid-v4>",
|
||||
"tenant_id": "<id>",
|
||||
"tenant_key": "<original-tenant-key>",
|
||||
"trace_id": "<id>",
|
||||
"issued_at": "<RFC3339 timestamp>",
|
||||
"correlation_id": "<original-request-message-id>",
|
||||
"status": "ok",
|
||||
"reason_code": "ok",
|
||||
"payload": {}
|
||||
}
|
||||
```
|
||||
|
||||
- `status`:`ok`、`pending`、`rejected`。
|
||||
- `correlation_id`:必须等于原 `ai.config.request.message_id`;`dispatcher_id`、`tenant_id`、`tenant_key` 也必须与原请求一致。
|
||||
- `pending`:`reason_code` 固定 `waiting`,`payload` 必须为空 object。
|
||||
- `rejected`:`reason_code` 为 `invalid_request`、`not_found`、`conflict`、`expired`、`not_authorized`、`unavailable`、`unsupported` 之一;`payload` 必须为 `{ "detail": "...", "retryable": true|false }`。
|
||||
- `ok`:`reason_code` 固定 `ok`,payload 必须同时包含 `snapshot` 和 `authorization`。
|
||||
|
||||
### `ok` 的 `snapshot`
|
||||
|
||||
| 字段 | 类型 / 规则 |
|
||||
| --- | --- |
|
||||
| `tenant_id` | 与信封租户一致 |
|
||||
| `agent_version_id` | Agent 配置版本 ID,须与原请求一致 |
|
||||
| `status` | `published` 或 `reused` |
|
||||
| `immutable` | 固定 `true` |
|
||||
| `content_sha256` | 小写 64 位 SHA-256;须与配置规范化摘要一致 |
|
||||
| `config` | 下述 AI 配置对象 |
|
||||
|
||||
`config` 必填 `agent_version_id`、`immutable`、`asr`、`conversation`;`immutable` 固定 `true`。支持两种配置形态:
|
||||
|
||||
- Full AI:必填 `llm`、`prompt`、`tts`,且 `conversation.opening` 必填;若提供 `mode`,值为 `full_ai`。
|
||||
- ASR-only:`mode` 必填且为 `asr_only`;不得提供 `llm`、`prompt`、`tts`。
|
||||
|
||||
| 对象 | 必填字段 | 可选字段与约束 |
|
||||
| --- | --- | --- |
|
||||
| `asr` | `provider_ref`、`language`、`input` | `credential_ref`、`model`、`interim`、`timeout_ms`;`input` 必须含 `encoding=pcm_s16le`、`sample_rate_hz`(8000–48000)、`channels=1`、`sample_width_bytes=2` |
|
||||
| `llm` | `provider_ref`、`model` | `credential_ref`、`temperature`(0–2)、`max_tokens`(正整数)、`timeout_ms`(正整数) |
|
||||
| `prompt` | `text`、`allowed_variables` | `text` 非空且最多 32768 字符;变量最多 32 个,名称匹配 `[A-Za-z_][A-Za-z0-9_]*`;`max_bytes` 为 1–32768 |
|
||||
| `tts` | `provider_ref`、`model`、`voice`、`format` | `credential_ref`、`speed`(0.25–3)、`timeout_ms`;`format.encoding` 为 `pcm_s16le` 或 `pcma`,`sample_rate_hz` 为 8000–48000,`channels=1` |
|
||||
| `conversation` | `allow_interrupt`、`silence_timeout_ms`、`max_duration_ms`、`max_turns`、`sentence_max_chars`、`max_pending_audio_chunks` | `opening`;`max_duration_ms` 为 1–3600000,`max_turns` 为 1–1000,其余整数下限为 1 |
|
||||
| `metadata` | — | 可选 JSON object;不是业务参数透传通道 |
|
||||
|
||||
除明确开放的 `metadata` object 外,各 AI 配置对象均拒绝未定义字段;AI 快照外层允许扩展字段,但这些字段不构成业务参数。`provider_ref`、`credential_ref` 是受控引用,不是凭据本身;MQ 消息不得携带密钥或访问令牌。
|
||||
|
||||
### `ok` 的 `authorization`
|
||||
|
||||
必填:`authorization_id`、`tenant_id`、`tenant_key`、`agent_version_id`、`config_sha256`、`mode`、`issued_at`、`expires_at`、`source`、`revoked`。
|
||||
|
||||
- `mode`:`full_ai` 或 `asr_only`;`source`:`saas` 或 `mock-saas`。
|
||||
- `config_sha256`:小写 64 位 SHA-256;须与快照配置摘要一致。
|
||||
- `credential_refs` 可选,包含 `asr`、`llm`、`tts` 的受控引用。
|
||||
- `allowed_egress_pool_ids` 可选;提供时至少一个且不得重复。
|
||||
- `revoked=true` 时必须提供 `revocation_reason`(最多 256 字符)。
|
||||
- 授权中的租户、版本、配置摘要、有效期与快照及原请求必须匹配;拒绝已撤销或超出请求时间窗的响应。
|
||||
|
||||
目前 Dispatcher 已接入 `ai.config.result` 的消费和校验;正常运行链路尚无调用方发出 `ai.config.request`,因此两者当前不构成已接通的生产往返流程。
|
||||
|
||||
## 消费与重复投递
|
||||
|
||||
输入消息由 Dispatcher 按消息身份及租户范围校验。业务状态和所需响应/事件先持久化,处理成功后才 ACK;暂时性错误重入队,永久错误进入对应死信队列。重复消息只能恢复原身份的处理结果,不能重复创建拨号或业务资产。
|
||||
@@ -1,116 +0,0 @@
|
||||
# SaaS ↔ RabbitMQ ↔ Dispatcher ↔ Agent ↔ OSS 时间泳道图
|
||||
|
||||
## 1. 已确认边界
|
||||
|
||||
- SaaS↔D全部交互经RabbitMQ,无双方HTTP。每个D有全局唯一ID及独立Topic/租户队列,不抢收其他D的消息。
|
||||
- D↔A保持Unary;OSS配置在D配置文件,A向D领取15分钟临时TOKEN并直传OSS,D不转发文件。
|
||||
- **上传只保证recording.uploaded可靠进入指定持久队列**。不申请SaaS上传会话,不等待verified/OSS ID,不新增VERIFYING,不把入队当SaaS已处理。
|
||||
- P1单D/单A/单Cell/单租户;D1/D2只用于本地消息隔离验证,不扩展调度HA。
|
||||
|
||||
本图为目标流程;V1 Schema/fixture通过不等于运行时接线或真实供应商验收。精确消息见[已确认V1合同](mq-only-v1-freeze-proposal.md)与`contracts/upstream/v1/`。
|
||||
|
||||
## 2. 独立订阅关系
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
S[SaaS] --> MQ[RabbitMQ topic exchanges]
|
||||
MQ --> T1[D1 / 原租户独立持久队列]
|
||||
MQ --> T2[D2 / 原租户独立持久队列]
|
||||
T1 --> D1[Dispatcher D1 / UUID v4]
|
||||
T2 --> D2[Dispatcher D2 / 另一UUID v4]
|
||||
D1 --> MQ
|
||||
D2 --> MQ
|
||||
MQ --> SQ[SaaS指定持久接收队列]
|
||||
SQ --> S
|
||||
```
|
||||
|
||||
采用精确Topic绑定,拒绝独立`*`/`#`词段,保留其他合法tenant_key原值;最长资源名决定196个UTF-8字节预算。D身份不同于dispatcher_epoch,重启不清除原消息/执行归属。
|
||||
|
||||
## 3. 时间泳道
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant S as SaaS
|
||||
participant SQ as RabbitMQ / SaaS指定持久队列
|
||||
participant DQ as RabbitMQ / D1专用持久队列
|
||||
participant D as Dispatcher D1
|
||||
participant A as Agent
|
||||
participant O as OSS
|
||||
|
||||
S->>DQ: call.execute(目标D1、原租户、版本与期限)
|
||||
DQ->>D: 精确投递
|
||||
Note over D: 校验并同事务持久inbox/task/outbox,重复返回原决定。
|
||||
D-->>DQ: 持久后ACK
|
||||
D->>SQ: command.result accepted
|
||||
SQ->>S: 受理结果,不是已拨号
|
||||
|
||||
opt 缺少当前执行可用的不可变AI配置/授权
|
||||
D->>SQ: ai.config.request(原租户/版本/请求身份)
|
||||
SQ->>S: 配置请求
|
||||
S->>DQ: ai.config.result(原请求关联)
|
||||
DQ->>D: 定向响应
|
||||
Note over D: 校验摘要/授权并持久绑定;不使用latest或本地默认覆盖。
|
||||
D-->>DQ: 持久后ACK
|
||||
end
|
||||
|
||||
D->>A: 状态/激活/准入、执行快照与最后许可
|
||||
A-->>D: 既有Unary回执/实际状态
|
||||
D->>A: Execute(原binding与许可)
|
||||
A-->>D: 接收回执
|
||||
Note over D,A: 仍须满足时间窗口、白名单、配额及控制屏障;回执不等于已发起。
|
||||
|
||||
opt 控制、查询或整体补传
|
||||
S->>DQ: task.control / query / replay
|
||||
DQ->>D: 原D、租户和目标校验
|
||||
D->>A: 必要的控制/状态核验
|
||||
A-->>D: 实际回执/事实
|
||||
D->>SQ: 控制结果、查询响应或原业务事件补传
|
||||
SQ->>S: 按原请求关联处理,不重新执行呼叫
|
||||
end
|
||||
|
||||
A->>D: ReportExecutionEvent
|
||||
D-->>A: 事实持久接收结果
|
||||
D->>SQ: call.status / transcript.updated / call.finished / contact.opt_out
|
||||
SQ->>S: 业务事件
|
||||
|
||||
A->>D: R12 RequestUpload(原binding/asset/upload_id)
|
||||
Note over D: OSS配置来自D文件,复用SDK签发15分钟TOKEN;不向SaaS申请会话。
|
||||
D-->>A: 临时TOKEN/受限UploadGrant
|
||||
A->>O: 一次PUT(文件直接上传OSS)
|
||||
O-->>A: PUT结果
|
||||
Note over A: 保存真实上传结果/大小/摘要与恢复记录;不自动再次PUT。
|
||||
A->>D: R13 CompleteUpload(仅原资产元信息)
|
||||
Note over D: 校验原绑定;上传事实与固定event_id的outbox同事务持久化。
|
||||
D->>SQ: persistent recording.uploaded,mandatory
|
||||
alt 正确durable队列接收、无return且publisher confirm成功
|
||||
SQ-->>D: broker确认
|
||||
Note over D: 持久标记通知已交付;没有SaaS处理结果或OSS ID。
|
||||
D-->>A: 本项目交付完成
|
||||
opt SaaS自行消费,非本项目完成条件
|
||||
SQ->>S: recording.uploaded
|
||||
end
|
||||
else 不可路由、断连或确认丢失
|
||||
SQ-->>D: return / nack / error / timeout
|
||||
Note over D,A: 保留原通知身份与恢复信息,不宣称完成;恢复通知而非重新PUT。
|
||||
end
|
||||
```
|
||||
|
||||
图中所有MQ请求/响应都遵守持久后ACK;publisher confirm只证明broker接收,不能单独证明正确路由,须同时排除return并核验指定持久队列/绑定。对上传而言这就是交付终点;AI/控制等流程仍需要各自的业务响应。
|
||||
|
||||
## 4. 数据与验证索引
|
||||
|
||||
| 交互 | 依据 | 验证重点 |
|
||||
| --- | --- | --- |
|
||||
| 身份/精确Topic/租户队列 | v2合同§1–§2及mq-topology.json | 不串收/抢收,稳定ID、字节预算、危险词段拒绝 |
|
||||
| 命令/查询/AI请求响应 | v2合同§3及mq.schema.json | 严格字段、原请求关联、持久恢复、accepted/applied分开 |
|
||||
| D↔A | dispatcher-agent.md及Proto | 保持Unary,版本和执行事实以代码证据为准 |
|
||||
| D配置/TOKEN/A直传 | v2合同§4–§5 | 无长期凭据下发、15分钟、一次PUT、显式重申请 |
|
||||
| recording.uploaded | event-payloads.schema.json | 11个已确认事实字段,无TOKEN、密钥或SaaS OSS ID |
|
||||
| 通知完成 | v2合同§5 | 指定durable队列、persistent、mandatory无return、confirm成功;无需SaaS回复 |
|
||||
|
||||
## 5. 当前实现差异
|
||||
|
||||
旧SaaS业务HTTP实现和启动配置已删除,执行、查询、控制、AI请求及上传通知已接入单一 V1 MQ。本地证据覆盖D身份/租户隔离、不可路由、confirm/DLQ、断连、重复、迟到revision、SQLite重启和recording.uploaded恢复;不得把这些本地证据当作外部SaaS或生产通过。
|
||||
|
||||
旧会话/verified控制流程已移出目标,不再为它增加RPC、状态或测试服务器业务系统。通知重复/确认丢失/重启沿原消息身份恢复,不重新PUT、不新建资产、不重拨;SaaS后续处理不属于本项目。
|
||||
@@ -0,0 +1,151 @@
|
||||
# SaaS ↔ Dispatcher 第三方对接:请求、事件与消费顺序 v0.1
|
||||
|
||||
**用途:SaaS 与 Dispatcher 的第三方联调说明,不是新增权威 Schema 或生产验收。** 本文按一项任务的一通外呼说明:**谁请求 → 发往何处 → 收到什么 → 消费后做什么**。Dispatcher 接收的内部通话反馈仅体现为它回传 SaaS 的业务事件;内部执行接口、媒体及资产上传操作不在本对接范围。字段结构以[现行 MQ Schema](../../contracts/upstream/v1/mq.schema.json)、[事件 Schema](../../contracts/upstream/v1/event-payloads.schema.json)、[拓扑](../../contracts/upstream/v1/mq-topology.json)为准。两条配置 HTTP 只读接口目前只有[项目自定义字段提案及样例](../contracts/config-read-fields-v0.1-proposal.md),路径、凭据传递和真实 SaaS 响应**未经 SaaS/management 签收**。本轮步骤/验收见[新计划](../plan-config-read-v0.1.md)。
|
||||
|
||||
> **版本门禁:**现行可核对的 MQ 消息为 `schema_version=2.0`、单 D/单租户本地路径;**现行运行合同仍为全 MQ、静态 SIP、固定 Asia/Shanghai `[09:00,20:00)`、排队任务固定 AI 版本**。用户批准的新目标是配置读 HTTP、业务走 MQ、缓存约 60 秒、新任务×线路时间窗口、有界消费及控制分道;这些**尚未发布/实现**。下文以 `现行`、`拟定` 标识,不能把拟定步骤当作现网可调用接口。原 `docs/thirds/` 六份分散说明已由用户有意删除,本文不引用它们。
|
||||
|
||||
## 1. 参与者和通道
|
||||
|
||||
| 对接方 | 职责与接收范围 |
|
||||
| --- | --- |
|
||||
| SaaS | 保存租户、任务、智能体授权和固定 `(tenant_key,task_id)→dispatcher_id` 归属;向该 D 发送 MQ 业务命令/查询,消费 D 回传的业务事件;拟定的 SIP HTTP 响应只能分发已经批准的配置,不能自行修改原始 SIP 配置。 |
|
||||
| Dispatcher(D) | 每实例有独立 UUID 和专属 MQ 接收队列;拟定通过只读 HTTP 查询本 D 的 SIP 全量及归属任务(含智能体)配置;接收 MQ 命令、回传命令结果/通话反馈/录音事实。D 与其内部执行系统的交互不属于 SaaS 第三方接口。 |
|
||||
|
||||
**现行 MQ 路由(不可按任务随意创建队列):**SaaS→指定 D 发到 durable Topic Exchange `agent-call.dispatchers.v2`,routing key `d.<dispatcher_id>.t.<tenant_key>.in`,D 消费 durable 队列 `agent-call.d.<dispatcher_id>.t.<tenant_key>.v2`;D→SaaS 发到 `agent-call.saas.v2`,key `d.<dispatcher_id>.t.<tenant_key>.out`,SaaS 消费 `agent-call.saas.events.v2`。永久拒绝消息走 `agent-call.dead-letter.v2`/该 D/租户 `.dlq.v2`;`agent-call.d.<dispatcher_id>.owner.v2` 仅防止多个进程占同一 D 身份,不是业务队列。业务消息 persistent、mandatory 发布、publisher confirm,单消息 ≤262144 字节;`tenant_key` **保留原值**,≤196 UTF-8 字节且不能出现独立 `*`/`#` 路由段。一个任务只投其归属 D,**单任务单 D 不自动解决多个任务跨 D 的租户/供应商总额度**。
|
||||
|
||||
**拟定变化:**配置 GET(SIP 全量、任务内含智能体)不再走 MQ;呼叫/控制/查询/补传/结果/上传事实依旧只走 MQ,**没有配置 HTTP→MQ 故障回退**。执行与控制分队列及原 AI 配置 MQ 消息停用仍待版本化合同;不能对现行 `.in` 键擅加新绑定。SaaS 与 D 需分别配置其对端可达性;UUID+SECRETKEY 的具体 HTTP 传递位置、URL 路径、错误状态码和生效规则尚待 SaaS 确认,不提供可直接照抄上线的 curl。
|
||||
|
||||
## 2. 正常流程:严格按因果顺序,异步事件不保证跨队列到达顺序
|
||||
|
||||
| 步骤/状态 | 发起 → 接收;请求或事件结构 | 返回结构 | 消费后必须触发的动作 |
|
||||
| --- | --- | --- | --- |
|
||||
| 0. SaaS 准备 SIP 配置(拟定) | SaaS 确保本 D 可读取的是唯一获批准的 SIP 全量配置;配置从何处进入 SaaS 属上游前置,不在本接口规定交互步骤。 | 配置须有获批版本/摘要及适用 D 范围;这不代表 D 已加载。 | 来源不明或版本不一致时,SaaS 不得将草稿作为可供 D 外呼的正式快照。 |
|
||||
| 1. D 启动/恢复,读 SIP(拟定) | D 带自身 UUID+SECRETKEY 调“取**本 D SIP 全量**”只读 HTTP;无请求体,实际 URL/凭据承载未定。新 D 不依赖已错过的 MQ 广播。运行期间约每 60 秒条件校验版本。 | `200` SIP 全量;版本未变且仍批准可 `304`(**无 JSON 体**);错误返回脱敏 `resource=error`(具体 HTTP 状态待定)。完整结构见 §3.1 及[Schema](../contracts/config-read-v0.1.schema.json)。 | 校验 D、批准版本/摘要、线路与时间/媒体数据,关执行准入、排空/对账旧执行,D 内部核验对应版本**确已生效**后方能消费**新执行**;不停止控制/查询。到期 HTTP 失败不沿用过期配置继续新拨。发现 SIP 变化约 60 秒≠新版必在 60 秒内生效。 |
|
||||
| 2. SaaS 创建任务/指派 D(拟定) | SaaS 持久保存 `(tenant_key,task_id)→dispatcher_id`,同任务 `call.execute`/`task.control` 只发给这个 D。此内部记录不是新的 MQ 广播。 | D 尚未收呼叫,无同步任务列表返回;已接纳执行继续用原不可变快照。 | 同租户多任务可以归不同 D;跨 D 共享总额度必须预先分配各 D 份额,不能每 D 各按全局上限放行。不因 D 忙碌悄悄改投别的 D。 |
|
||||
| 3. SaaS 发候选呼叫(现行 MQ 消息/新消费语义拟定) | 发布 `command_type=call.execute` 命令信封,`payload` 见 §4;发到该 D/租户 `.in` key;`command_id` 及源 execution/task item 身份保持稳定。 | **MQ publisher confirm 不是 D 接受执行结果**;随后 D 发 `command.result` 异步事件(§5)。 | D 校验身份、路由、期限/Schema及重复身份。有名额才接纳;新目标无名额时其余消息留 MQ,不批量搬进 SQLite。**当前代码仍先持久化后 ACK,尚未实现按空位停消费**。不能 Nack 热循环。 |
|
||||
| 4. D 读任务+智能体(拟定) | D 在候选执行可接纳时读“取归属任务配置”只读 HTTP;可用经核验的租户+任务缓存,约 60 秒到期主动校验,支持条件 `ETag/If-None-Match`;不逐呼下载完整内容。 | `200` 返回**一项任务+内嵌获授权智能体**,`304` 无体表示仍可复用原配置,错误返回 `resource=error`;结构见 §3.2。不是旧独立 AI GET,也不是现行 `ai.config.request/result` MQ。 | 检查任务归属、状态、有效授权、时间窗、线路选择及现行命令的版本引用;调用失败/过期**不接新呼叫**,但先前已接纳/通话中保留原快照。允许 SaaS 更新后最多约 60 秒使用原有效配置;停/暂停 MQ 命令不能等缓存失效。`call.execute.task_revision/agent_version_id` 如何合法换版需新合同签收。 |
|
||||
| 5. D 准入与 ACK(业务语义拟定) | 无新对外请求;D 原子记录 inbox、任务/AI/SIP **本次执行快照**、任务/租户/线路等已批准份额、许可与 `command.result` outbox;再 ACK 原 MQ 消息。 | 成功后 D→SaaS 异步 `command.result`;失败/未知不得谎称已接纳。现行 `command.result.status` 可为 `accepted/waiting/applied/rejected/failed/unknown`;新容量规则尚须冻结。 | 任务/线路时间交集、排除日期、号码白名单、Cell/AI/供应商配额均需满足;缓存 TTL 不是拨号授权。即使 ACK 丢失重投,按原命令/执行身份恢复,不得发起第二次拨号。 |
|
||||
| 6. D 回传呼叫反馈(现行 MQ 事件) | D 按已经取得的呼叫事实向 `agent-call.saas.v2` 依次或异步发布 `call.status`、`transcript.updated`、`transcript.failed`、`contact.opt_out`;正文见 §5。内部事实如何产生不属于 SaaS 对接接口。 | SaaS 从该 D/租户 `.out` 绑定队列收到 `event_id/event_type/aggregate_*` 及对应 payload;`transcript.updated` **没有 `call.transcript` 别名**。 | SaaS 按事件 ID 幂等、按同一聚合版本归并;转写可修订,拒联及时阻止后续不该拨打的任务项。不能把确认入 MQ 当作 SaaS 已处理,也不能假设跨事件类型全局顺序。 |
|
||||
| 7. D 回传最终通话结果(现行 MQ 事件) | D 发布 `call.finished`;完整 payload 见 §5。 | SaaS 收通话最终结果及其可选 `asset_state`;**通话结束不等于录音上传完成**。 | SaaS 更新本任务/通话结果;按同一 `call_id` 关联之后可能抵达的录音事实,不能因反馈迟到重发呼叫。 |
|
||||
| 8. D 回传录音上传事实(现行 MQ 事件) | D 在确认已有完整资产事实后向 SaaS 发布 `recording.uploaded`;SaaS **不需要提供上传 TOKEN、上传会话或 complete/verified 接口**。 | SaaS 只收 `call_id/recording_id/upload_id/bucket/object_key/format/channels/sample_rate_hz/duration_ms/size_bytes/checksum_sha256`(§5);**不含 TOKEN/密钥/签名 URL**。 | persistent、进入 durable 队列、mandatory 无 return 且 publisher confirm 成功只证明 MQ 交付,**不证明 SaaS 已消费/验证**;确认丢失按同一 `upload_id` 去重,不因重复消息产生第二份资产。 |
|
||||
| 9. 任务配置更新/终结(拟定 HTTP) | 活跃且有未接纳执行的任务约每 60 秒由 D 校验 HTTP;SIP 也按期校验;任务终结无新呼叫时不再请求该任务配置。 | SaaS 新版本在成功读取后影响尚未接纳的执行,最多约 60 秒延迟;已接纳执行不漂移。 | D 到期读不到配置就暂停新执行准入;仅删除可重新读取的配置缓存,不删未决执行/幂等与未交付 MQ 结果。 |
|
||||
|
||||
**重要:**步骤 0/1/2/4/5/9 中的配置读取及新消费语义属于**计划中的新功能**;现行 SaaS↔D MQ 合同没有两条 HTTP 配置接口,也没有“配置版本改变后任意重写旧 `call.execute`”的许可。第三方可据此核对拟议协议和阻塞项,**不能据此擅自上线新接口**。
|
||||
|
||||
## 3. 两个配置读取请求/返回及字段含义(项目自拟,非现网)
|
||||
|
||||
### 3.1 SIP 全量配置
|
||||
|
||||
**请求:**D 用自己的 UUID+SECRETKEY 请求本 D 的 SIP 全量快照;只读 GET、无 JSON 请求体。路径、参数/头及 HTTP 错误状态未冻结,禁止凭本文创造真实 SaaS 路径。启动时必须成功,后续成功核对起约 60 秒有效;D 只能读取独占执行资源分区。
|
||||
|
||||
**`200` 结构:**
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": "config-read.v0.1",
|
||||
"resource": "sip_config",
|
||||
"dispatcher_id": "<该D的UUIDv4>",
|
||||
"revision": 1,
|
||||
"snapshot_sha256": "<整份已批准快照的64位十六进制摘要>",
|
||||
"approved_at": "<批准时间,含时区偏移>",
|
||||
"artifact": { "...": "现有静态Cell制品;详见机器Schema和Mock示例" },
|
||||
"trunk_details": [ { "...": "各trunk连接、主叫及时段;详见下表" } ]
|
||||
}
|
||||
```
|
||||
|
||||
此代码块是**结构示意,不是可校验样例**;完整可校验 Mock 见[示例](../contracts/examples/config-read-sip-v0.1.json)。
|
||||
|
||||
| 返回字段 | 含义 / 第三方必须保证 |
|
||||
| --- | --- |
|
||||
| `schema_version`, `resource` | 草案版本与固定资源标记 `sip_config`;新版本不能和现行 MQ 的 `schema_version=2.0` 混写。 |
|
||||
| `dispatcher_id` | 收配置的唯一 D 身份,不是 `agent_id`/租户 ID;错 D 的响应必须拒绝。 |
|
||||
| `revision`, `snapshot_sha256`, `approved_at` | **整份**管理面获批配置的递增版号、摘要与批准时间;摘要算法、版本来源尚待管理面/SaaS 签收;Mock 摘要是占位,不是验证通过。 |
|
||||
| `artifact` | [项目现有静态 Cell 制品](../../contracts/upstream/v1/static-cell-artifact.schema.json):`artifact_id/source_release/source_digest/approval_reference/cell_id/revision/config_sha256/mode/allowed_targets/trunks`,可能含 `media_profiles/ari/media/recording/load_evidence`;字段属于**项目既有契约**,不是页面截图的 SaaS 原字段。 |
|
||||
| `artifact.trunks[]` | 一项独立线路:`trunk_id` 唯一标识、`provider_id` 供应商、`egress_pool_id` 出口、`codec=PCMA` 编码、`caller_profile_ids` 可用主叫引用、`dial_prefix` 仅该线路的前缀、`enabled` 是否启用、`sip_endpoint_ref/credential_ref` 受控引用及 `media_profile_id` 媒体参数 ID;不能把主叫当 Digest 用户名、把同一服务器当备用。 |
|
||||
| `trunk_details[].trunk_id` | 必须与制品中的 `trunk_id` 一一对应,确保确为**全量**、无漏删;JSON Schema 不能单独验证两数组一一对应,接入方需另校验。 |
|
||||
| `server_host/server_port` | SIP 服务端地址/端口;mock 示例域名不可用于 real。服务商实际传输、注册和鉴权仍待核验。 |
|
||||
| `transport/auth_mode/registration_required` | SIP 传输方式、IP/Digest/无认证、是否注册。未确认用 `null`,**real 不得把 null 当默认 UDP/免认证**;不含密码。 |
|
||||
| `max_concurrent_calls` | 供应商/该 D 获分线路额度;`null` 表示未知,不得让各 D 各按共享上限放行。跨 D 份额总和须由权威源保证。 |
|
||||
| `caller_profiles[].caller_profile_id/caller_id` | 主叫配置的引用与原样 SIP 主叫标识;不能按纯数字手机号清洗(可含 `BD`),From/PAI 映射待供应商确认。 |
|
||||
| `schedule.time_zone/weekly_windows` | Asia/Shanghai;周一至周日逐日零或多个左闭右开时段,某日空数组即该日不允许呼叫。与任务时段取交集;未加载/未知窗口不可放行。 |
|
||||
|
||||
`304` 空体可复用原完整快照前提是原批准状态仍有效;到期或接口失败则关闭新执行准入。SIP 变更须由 D 停止新准入、对账旧通话并自行确认新制品实际生效;仅 `200` 或 MQ publisher confirm 不是生效证据。
|
||||
|
||||
### 3.2 任务+智能体配置
|
||||
|
||||
**请求:**D 使用自身 UUID+SECRETKEY,以归属租户原值 `tenant_key` 和 `task_id` 查**该单任务**;GET 无 JSON 请求体,具体 URL/头待冻。只对尚未接纳的候选执行使用配置缓存;请求不携带号码列表或原始录音。
|
||||
|
||||
**`200` 层级:** `schema_version/resource=task_config/dispatcher_id/tenant_key/task_id/task_revision/status/name/group_id/max_concurrent_calls/route_policy_id/allowed_trunk_ids/schedule/agent`。完整可校验的 [Mock 示例](../contracts/examples/config-read-task-v0.1.json)及[Schema](../contracts/config-read-v0.1.schema.json)为准。
|
||||
|
||||
| 返回字段 | 含义 / 消费者动作 |
|
||||
| --- | --- |
|
||||
| `dispatcher_id/tenant_key/task_id` | SaaS 保存的固定 `(tenant_key,task_id)→D`;D 验证租户原值、任务及自己身份,不能把一任务同时发给多个 D。 |
|
||||
| `task_revision` | 任务配置修订号,与原 `call.execute.task_revision` 可能不同;如何让未接纳旧命令换用新版,需新版合同明定并发冻结点,D 不能私改旧消息。 |
|
||||
| `status` | 项目自定义 `running/paused/stopped/finished` 枚举,不声称是 SaaS 现网枚举。`paused/stopped/finished` 不允许新发;MQ `task.control` 必须及时生效,不能等缓存。 |
|
||||
| `name/group_id` | 任务名称与可空分组引用(页面可见语义);不作为权限或拨号依据。 |
|
||||
| `max_concurrent_calls` | **该任务**的并发上限;与租户、该 D 获分供应商额度和 Cell/AI 许可同时限制,不代表同租户多个任务的全局上限。 |
|
||||
| `route_policy_id/allowed_trunk_ids[]` | 路由版本引用/获准线路集合;D 只能从已加载且当前可用的线路中按获批准策略选择,不能为绕时段静默换线。 |
|
||||
| `schedule.time_zone/starts_at/ends_at` | 时区固定 Asia/Shanghai,起/止日期时间可为 `null` 表示未设界限,不用 SDK 默认值填业务值。 |
|
||||
| `schedule.weekly_windows` | 七个星期键对应多个 `{start,end}`(`HH:MM`);同一天可多段,左闭右开,跨午夜按合同拆段;空数组=该日禁呼。 |
|
||||
| `schedule.excluded_dates[]` | 用户新增、截图未见的多日期排除;`YYYY-MM-DD`,按 Asia/Shanghai 日历判断,**优先于**所有星期时段。 |
|
||||
| `agent.agent_version_id/content_sha256` | 任务所引用的不可变智能体版本和内容摘要;与 `agent.config.agent_version_id` 必须一致,同版本不同内容拒绝。具体哈希生成规则待签收。 |
|
||||
| `agent.authorization_id/authorization_expires_at` | 对该 D/租户/智能体配置的有效授权标识及期限;过期不准新执行,即使 `ETag`/`304` 命中也不能复活授权。 |
|
||||
| `agent.config` | [现有 AI 严格 Schema](../../contracts/upstream/v1/ai-config.schema.json),包括 `mode`、ASR 的 provider/model/input/识别参数、LLM 的 provider/model/temperature/max_tokens/timeout、`prompt.text`及变量、TTS 的 voice/speed/format、conversation 控制参数。`full_ai/asr_only` 仅按对应模式的已验证 SDK 参数执行,不把 UI 的 Top-P/情绪/话后分析等未经签收字段偷放 `metadata`。 |
|
||||
|
||||
**错误与缓存:**`200` 可含新批准内容;`304` **无体**且此前授权仍能覆盖新的缓存有效期,方可沿用;其它返回按拟定 `{schema_version:"config-read.v0.1",resource:"error",error:{code,message}}`([Mock](../contracts/examples/config-read-error-v0.1.json))失败处理,具体状态和错误码待 SaaS 签收。成功核验起任务缓存约 60 秒,配置变更最多约 60 秒后影响未接纳呼叫;到期读不到即停新准入,已接纳原快照仍可完成。任务结束且没有未知/在途引用时,仅删除可再读的**配置缓存**,不清除幂等与 outbox。
|
||||
|
||||
## 4. MQ 请求与控制:按入站发生时处理
|
||||
|
||||
**现行命令信封(SaaS→D)必有**:
|
||||
|
||||
| 字段 | 定义 |
|
||||
| --- | --- |
|
||||
| `schema_version` | 固定 `2.0`,与 HTTP 配置草案版本独立。 |
|
||||
| `command_type/command_id` | `call.execute/task.control/call.replay/command.replay`;`command_id` 是本次命令的稳定去重身份,重复投递不能产生新的执行。 |
|
||||
| `dispatcher_id` | 目标 D 全局唯一 UUID v4,不等于运行时 `dispatcher_epoch`。SaaS 必须发到该 D 的精确 routing key。 |
|
||||
| `tenant_id/tenant_key` | 租户内部 ID 与**原值**路由键;不得清洗、截断或广播后靠消息体过滤。 |
|
||||
| `trace_id/issued_at/not_after` | 全链路关联标识、生成时间、消息截止时间;超期不得等下个外呼窗口自动拨。 |
|
||||
| `payload` | 按 `command_type` 严格选择以下**唯一**正文;不允许添加未批准字段。 |
|
||||
|
||||
| `command_type` | payload **必有字段**与含义 | 触发后果/异步返回 |
|
||||
| --- | --- | --- |
|
||||
| `call.execute` | `execution_id` 本次执行稳定身份;`task_id` 所属任务;`task_item_id` 任务内号码项;`task_revision` 下发时任务修订;`callee` 原始被叫(不加线路前缀);`route_policy_id` 路由引用;`caller_profile_id` 主叫引用;`agent_version_id` 智能体版本;`variables` prompt变量对象;`ring_timeout_ms` 振铃时限;`max_call_duration_ms` 最大通话时长。 | D 校验/接纳后发 `command.result`。同命令或执行重复不重新 originate;`variables` 虽在当前 Schema 允许 object,不能因此注入未批准的 AI 字段。 |
|
||||
| `task.control` | `task_id` 目标任务;`action=pause/resume/stop`;`expected_task_revision` CAS 期望版;`reason` 原因;可选 `active_call_policy=drain/hangup` 控制在途通话(具体 action/策略组合以合同为准)。 | 结果由 `command.result` 的 requested/applied revision 区分;`stop` 后不可按普通 resume 重启。控制先于尚在 MQ 的 `call.execute` 到达时,新版本必须有拦截屏障;**现行共享队列/任务不存在返回 `not_found` 的路径尚未满足新目标**。 |
|
||||
| `call.replay` | `call_id` + `reason`。 | 只补传**已有呼叫**事件,不新建执行、不重拨;结果按原身份去重。 |
|
||||
| `command.replay` | `source_command_id` + `reason`。 | 只补传已有命令响应,不能当第二次 originate;不是 SaaS 任意补任务/执行。 |
|
||||
|
||||
**现行 MQ 查询(两向异步,不是 HTTP 业务接口):**SaaS→D `message_type=command.query` 的 `payload={command_id}` 或 `call.query` 的 `payload={call_id}`;请求必有 `schema_version/message_type/message_id/dispatcher_id/tenant_id/tenant_key/trace_id/issued_at/not_after/payload`,`message_id` 是回包关联身份。D→SaaS 回同身份范围的 `command.query.result`/`call.query.result`:必有 `schema_version/message_type/message_id/dispatcher_id/tenant_id/tenant_key/trace_id/issued_at/correlation_id/status/reason_code/payload`,其中 `correlation_id` 指向原请求 `message_id`,`status=ok/pending/rejected`;`pending` 的 `reason_code=waiting` 且 payload 空对象;`rejected` 的 payload 为 `{detail,retryable}`,reason_code 必须使用[现行 Schema](../../contracts/upstream/v1/mq.schema.json)规定的值。
|
||||
|
||||
- `command.query.result` 的 `ok.payload` 是 [`executor_Command`](../../contracts/upstream/v1/mq.schema.json):`command_id/command_type/tenant_id/tenant_key/status/aggregate_version` 必有,`task_id/execution_id/call_id` 是关联对象,`reason_code/wait_reason_code` 为具体处置或等待原因,`requested_task_revision/applied_task_revision/task_state` 表示控制 CAS 与任务状态,`accepted_at/waiting_since/admission_deadline/updated_at` 为相应时间。**某字段可空或缺省以机器 Schema 为准**;不是另建 SaaS 状态库。
|
||||
- `call.query.result` 的 `ok.payload` 是 [`executor_Call`](../../contracts/upstream/v1/mq.schema.json):`call_id/execution_id/call_state/call_version/attempts/transcript/recordings/delivery/snapshot_at` 必有;可含 `task_id/task_item_id/reason_code/outcome/started_at/ended_at/duration_ms`。`attempts[]` 是 `call.status` 事件信封数组;`transcript.events[]` 是 `transcript.updated/failed` 事件信封数组;`recordings[]` 是 `recording.uploaded/failed` 事件信封数组;`delivery={pending,retry,dispatching,published}` 是非负消息计数,**不是 SaaS 已处理数**;`snapshot_at` 是本次视图时间。不把查询快照或 confirm 冒充 SaaS 已处理。
|
||||
- **现行旧配置 MQ 消息** `ai.config.request/result` 仍在[现行 Schema](../../contracts/upstream/v1/mq.schema.json),但新接口签收/切换后**停止使用,不作为 HTTP 失败回退**。第三方新实现不需再为新配置另外做 MQ AI 请求或 SIP 快照 MQ 通道。
|
||||
|
||||
## 5. D→SaaS 业务事件:每次都是异步通知
|
||||
|
||||
**现行事件信封必有**:`schema_version=2.0`、`event_id`(重投去重)、`event_type`、`dispatcher_id`、`tenant_id/tenant_key`、`trace_id`、`occurred_at`、`aggregate_type/aggregate_id/aggregate_version`(同一对象的类型/身份/版本)及按类型严格校验的 `payload`。`aggregate_version` 用于**同一聚合**归并,不能假定不同呼叫或不同队列之间全局有序;唯一权威为[事件 Schema](../../contracts/upstream/v1/event-payloads.schema.json)。
|
||||
|
||||
| `event_type` | payload **必有字段**:含义 | 可选字段/收到后处理 |
|
||||
| --- | --- | --- |
|
||||
| `command.result` | `command_id/command_type` 源命令身份/类别;`status` 为 `accepted/waiting/applied/rejected/failed/unknown`;`reason_code` 原因。 | 可含 `task_id/task_item_id/execution_id/call_id`、`requested_task_revision/applied_task_revision`、`admission_state`、`resource_reservation_id/permit_id`。SaaS 根据关联身份和 revision 判定命令是否已应用;**MQ confirm 不等于这条事件已被 SaaS 消费**。 |
|
||||
| `call.status` | `call_id/execution_id` 通话/执行;`call_state=queued/dialing/ringing/answered/ended`、`call_version` 状态版本、`attempt_id` 当次尝试、`attempt_state=pending/active/ended/unknown`。 | 可含 `task_id/task_item_id/route_policy_id/caller_profile_id/trunk_id/cell_id/egress_pool_id/observed_at/reason_code`;SaaS 按版本/身份归并,不因超时重试已接通/未知呼叫。 |
|
||||
| `transcript.updated` | `call_id/turn_id/segment_id` 通话、轮次和分段;`role=customer/agent/system`;`revision` 片段修订、`text` 实时文字、`is_final` 是否最终段;`start_ms/end_ms` 段时刻、`playback_state` AI 声音生成/发送/播放/取消状态。 | 可含 `execution_id`;同段更高 revision 更新,不能把 OSS 文字资产替代实时文字;正文/音频不进入长期诊断和外部聊天。 |
|
||||
| `transcript.failed` | `call_id`、`reason_code`、`retryable`:识别/转写失败原因及可重试性。 | 可含 `segment_id/affected_segments`;失败不是空字幕“成功”。 |
|
||||
| `contact.opt_out` | `call_id/task_id/task_item_id/requested_at`:联系人拒联事实及发生时间。 | 可含 `turn_id/segment_id`;SaaS 须停止后续不该拨出的任务项,不能等一分钟配置缓存后才处理。 |
|
||||
| `call.finished` | `call_id/execution_id/call_version/outcome/started_at/ended_at/duration_ms/reason_code`:通话最终结局、时长和原因。 | 可含 `task_id/task_item_id/attempt_summary/asset_state`,其中 `asset_state=pending/complete/failed/unknown` 是当时资产状态;录音上传仍可能稍后完成。 |
|
||||
| `recording.uploaded` | `call_id/recording_id` 通话/录音身份;`upload_id` 本次上传事实 ID;`bucket/object_key` OSS 目标;`format=wav/raw_pcm/pcma`、`channels=1`、`sample_rate_hz`、`duration_ms`、`size_bytes` 描述实际资产;`checksum_sha256` 文件内容的 SHA-256。 | SaaS 按原身份/哈希去重、再按自身流程处理;D 只保证已持久 MQ 入队,不返回 SaaS verified/OSS ID。**不含 TOKEN、授权 Header、签名 URL 或完整音频**。 |
|
||||
| `recording.failed` | Schema 存在:`call_id/recording_id/stage/reason_code/retryable`,可含 `next_retry_at`。 | **不要当作当前上传流程必定发出的事件**;其 `verify/complete` 等旧 stage 尚在严格 Schema 中,目标流程只负责直传及可靠 `recording.uploaded`,失败须保留可追溯事实和恢复证据,需新版合同明确是否发布此事件。 |
|
||||
|
||||
SaaS 从 `.out` 队列持久消费并在自己处理成功后 ACK;D 本地 outbox 写入不是 MQ 交付,MQ publisher confirm 也不是 SaaS 应用收讫。D 重启/confirm 丢失可能重发**同一** `event_id` 或 `upload_id`,SaaS 去重,不为重复 `recording.uploaded` 生成第二个资产。事件时间线由 `occurred_at`、聚合版本与实际事实确定,**不能把上表顺序当成逐消息的全局顺序**。
|
||||
|
||||
## 6. 联调前双方要补齐的合同与验收清单
|
||||
|
||||
| 待决项 | 第三方需确认/提供什么;否则不得声称对接完成 |
|
||||
| --- | --- |
|
||||
| 两条 HTTP 配置只读接口 | SaaS 签收真实路径、UUID+SECRETKEY 传递方式、归属/权限范围、`200/304` 及错误状态/字段、ETag 粒度、授权续期、是否真能把管理面获批 SIP **完整**分发;当前英文键是项目自定义草案,**非现网原字段**。 |
|
||||
| SIP 配置来源与生效 | SaaS 提供的 SIP 只读响应必须对应管理面唯一获批版本/摘要及完整线路对端/传输/鉴权/注册/主叫/前缀/额度/时段;D 自行验证配置生效后才接新任务。现有 mock 示例的 `null` 供应商参数不可用于 real。 |
|
||||
| 任务版本与配置变化 | 现行 `call.execute` 已带 `task_revision/agent_version_id/route_policy_id`;用户新目标容许尚未接纳呼叫在最多约 60 秒内沿用旧批准配置,合同须明确旧命令如何获授权新绑定及并发冻结点,已接纳不变。排除日期、任务×SIP时段的类型/界限也须签收。 |
|
||||
| 控制可达与容量保护 | 现行每 D/租户只有一个 `.in` 队列;目标拟分执行/控制通道,需版控 routing/binding、未入库任务的 stop 屏障、MQ 满队列发布失败保留与配额边界。没有新协议不得自行拆现有队列、广播抢收或各 D 重复放行共享额度。 |
|
||||
| 录音事实与收讫 | SaaS 不申请录音会话、不下发 OSS TOKEN;D 可靠发布 `recording.uploaded` 仅代表通知进队列。SaaS 需要的应用收讫/后处理在自身流程定义,不能让 D 伪造 verified 或 OSS ID。 |
|
||||
| 当前与新版本验收 | 新 HTTP 配置、缓存、单任务单 D、多 D 份额、D/A 时段门禁及经纪代理拓扑均未通过真实 SaaS/生产验收。按[新计划 §4–§8](../plan-config-read-v0.1.md)分别记录 C/L/M;Mock、现行消息字段或本文不代签新协议。 |
|
||||
|
||||
**不做:**本文不授权真实拨号、云资源/网络变更、生产发布、二套管理后台、HTTP 业务回退或新的 MQ 配置订阅。运行版本不同不得混用新 HTTP 草案与现行 `ai.config.request/result` 再说“已对接”;外部拿到本文后首先确认 §6,再安排隔离合同/Mock 测试。
|
||||
Reference in New Issue
Block a user