From 444937d6fe186c047dfddf119fc54ec3a3e35e1f Mon Sep 17 00:00:00 2001 From: Rogee Date: Thu, 1 Oct 2026 11:12:07 +0800 Subject: [PATCH] Consolidate current SaaS contract and retire redundant documents --- AGENTS.md | 6 +- README.md | 6 +- contracts/current_layout_test.go | 27 + contracts/local/manifest.json | 6 +- docs/README.md | 30 +- docs/archive/README.md | 8 +- .../plan-saas-dispatcher-completed.md} | 0 docs/archive/sources/README.md | 12 + .../sources}/plan-saas-dispatcher-v05-v0.1.md | 0 docs/{thirds => archive/sources}/v0.4.md | 2 + .../sources}/v0.5-proposal.md | 0 .../第三方对接事件与请求消费顺序_v0.1.md | 2 + .../ai-authorization-v0.2.schema.json | 109 --- .../call-execute-v0.4-proposal.schema.json | 27 - .../call-result-v0.1-proposal.schema.json | 159 ---- .../command-next-v0.1-proposal.schema.json | 209 ---- .../config-read-fields-v0.1-proposal.md | 102 -- .../config-read-fields-v0.2-proposal.md | 100 -- .../config-read-fields-v0.3-proposal.md | 15 - docs/contracts/config-read-v0.1.schema.json | 172 ---- docs/contracts/config-read-v0.2.schema.json | 174 ---- docs/contracts/config-read-v0.3.schema.json | 173 ---- .../ai-authorization-invalid-egress-v0.2.json | 18 - .../examples/ai-authorization-v0.2.json | 15 - .../examples/call-execute-altcallee-v0.4.json | 11 - .../call-execute-invalid-expiry-v0.4.json | 12 - .../examples/call-execute-old-v0.4.json | 11 - .../examples/call-execute-recent-v0.4.json | 11 - ...-result-invalid-missing-checksum-v0.1.json | 44 - .../examples/call-result-uploaded-v0.1.json | 54 -- .../command-next-invalid-control-id-v0.1.json | 18 - .../examples/config-read-error-v0.1.json | 8 - .../config-read-http-statuses-v0.1.json | 71 -- ...nfig-read-invalid-extra-property-v0.1.json | 9 - ...config-read-sip-invalid-artifact-v0.2.json | 38 - .../config-read-sip-invalid-egress-v0.3.json | 37 - .../examples/config-read-sip-v0.1.json | 65 -- .../examples/config-read-sip-v0.2.json | 37 - .../examples/config-read-sip-v0.3.json | 36 - .../examples/config-read-task-v0.1.json | 88 -- .../config-read-tenant-quota-v0.1.json | 10 - ...l-mock-recording-failure-expired-v0.1.json | 6 - ...-recording-failure-invalid-extra-v0.1.json | 7 - ...ecording-failure-invalid-timeout-v0.1.json | 6 - ...-recording-failure-upload-failed-v0.1.json | 6 - ...tic-cell-artifact-invalid-egress-v0.2.json | 45 - .../examples/static-cell-artifact-v0.2.json | 44 - .../task-control-invalid-drain-v0.4.json | 10 - .../task-control-invalid-expiry-v0.4.json | 11 - .../examples/task-control-pause-v0.4.json | 10 - .../examples/task-control-resume-v0.4.json | 10 - .../examples/task-control-stop-v0.4.json | 10 - .../examples/task-discovery-changes-v0.2.json | 32 - .../examples/task-discovery-changes-v0.4.json | 9 - .../examples/task-discovery-empty-v0.3.json | 6 - .../examples/task-discovery-error-v0.4.json | 8 - .../task-discovery-http-statuses-v0.1.json | 82 -- .../task-discovery-http-statuses-v0.2.json | 10 - .../task-discovery-http-statuses-v0.3.json | 9 - .../task-discovery-invalid-changes-v0.3.json | 7 - .../task-discovery-invalid-cursor-v0.3.json | 6 - ...ask-discovery-invalid-page-token-v0.2.json | 7 - ...discovery-invalid-queue-property-v0.1.json | 24 - .../task-discovery-invalid-queue-v0.2.json | 13 - .../examples/task-discovery-invalid-v0.4.json | 10 - .../task-discovery-no-change-v0.2.json | 6 - .../examples/task-discovery-page-v0.3.json | 9 - .../examples/task-discovery-removed-v0.3.json | 8 - .../task-discovery-snapshot-page1-v0.4.json | 11 - .../task-discovery-snapshot-page2-v0.4.json | 11 - .../task-discovery-snapshot-v0.1.json | 23 - .../task-discovery-snapshot-v0.2.json | 14 - .../examples/task-discovery-updated-v0.3.json | 8 - .../local-contract-manifest-v0.1.json | 84 -- .../local-contract-manifest-v0.2.json | 18 - .../local-contract-manifest-v0.3.json | 19 - .../local-contract-manifest-v0.4.json | 83 -- .../local-contract-manifest-v0.5.json | 11 - .../local-contract-manifest-v0.6.json | 16 - ...-mock-recording-failure-manifest-v0.1.json | 30 - .../local-mock-recording-failure-v0.1.md | 17 - ...al-mock-recording-failure-v0.1.schema.json | 18 - docs/contracts/mq-topology-v0.1-proposal.json | 84 -- .../static-cell-artifact-v0.2.schema.json | 363 ------- .../task-control-v0.4-proposal.schema.json | 27 - .../task-discovery-v0.1-proposal.schema.json | 152 --- .../task-discovery-v0.2-proposal.schema.json | 98 -- .../task-discovery-v0.3-proposal.schema.json | 59 -- .../task-discovery-v0.4-proposal.schema.json | 75 -- docs/contracts/通信与事件数据交互_v0.1.md | 343 ------- docs/evidence/saas-dispatcher-p07-audit.md | 4 +- .../saas-dispatcher-p08-acceptance.md | 6 +- docs/thirds/saas-dispatcher.md | 12 +- docs/thirds/v0.2.md | 899 ------------------ docs/thirds/v0.3.md | 28 - 95 files changed, 82 insertions(+), 4774 deletions(-) rename docs/{plan-saas-dispatcher.md => archive/plan-saas-dispatcher-completed.md} (100%) create mode 100644 docs/archive/sources/README.md rename docs/{ => archive/sources}/plan-saas-dispatcher-v05-v0.1.md (100%) rename docs/{thirds => archive/sources}/v0.4.md (96%) rename docs/{thirds => archive/sources}/v0.5-proposal.md (100%) rename docs/{thirds => archive/sources}/第三方对接事件与请求消费顺序_v0.1.md (99%) delete mode 100644 docs/contracts/ai-authorization-v0.2.schema.json delete mode 100644 docs/contracts/call-execute-v0.4-proposal.schema.json delete mode 100644 docs/contracts/call-result-v0.1-proposal.schema.json delete mode 100644 docs/contracts/command-next-v0.1-proposal.schema.json delete mode 100644 docs/contracts/config-read-fields-v0.1-proposal.md delete mode 100644 docs/contracts/config-read-fields-v0.2-proposal.md delete mode 100644 docs/contracts/config-read-fields-v0.3-proposal.md delete mode 100644 docs/contracts/config-read-v0.1.schema.json delete mode 100644 docs/contracts/config-read-v0.2.schema.json delete mode 100644 docs/contracts/config-read-v0.3.schema.json delete mode 100644 docs/contracts/examples/ai-authorization-invalid-egress-v0.2.json delete mode 100644 docs/contracts/examples/ai-authorization-v0.2.json delete mode 100644 docs/contracts/examples/call-execute-altcallee-v0.4.json delete mode 100644 docs/contracts/examples/call-execute-invalid-expiry-v0.4.json delete mode 100644 docs/contracts/examples/call-execute-old-v0.4.json delete mode 100644 docs/contracts/examples/call-execute-recent-v0.4.json delete mode 100644 docs/contracts/examples/call-result-invalid-missing-checksum-v0.1.json delete mode 100644 docs/contracts/examples/call-result-uploaded-v0.1.json delete mode 100644 docs/contracts/examples/command-next-invalid-control-id-v0.1.json delete mode 100644 docs/contracts/examples/config-read-error-v0.1.json delete mode 100644 docs/contracts/examples/config-read-http-statuses-v0.1.json delete mode 100644 docs/contracts/examples/config-read-invalid-extra-property-v0.1.json delete mode 100644 docs/contracts/examples/config-read-sip-invalid-artifact-v0.2.json delete mode 100644 docs/contracts/examples/config-read-sip-invalid-egress-v0.3.json delete mode 100644 docs/contracts/examples/config-read-sip-v0.1.json delete mode 100644 docs/contracts/examples/config-read-sip-v0.2.json delete mode 100644 docs/contracts/examples/config-read-sip-v0.3.json delete mode 100644 docs/contracts/examples/config-read-task-v0.1.json delete mode 100644 docs/contracts/examples/config-read-tenant-quota-v0.1.json delete mode 100644 docs/contracts/examples/local-mock-recording-failure-expired-v0.1.json delete mode 100644 docs/contracts/examples/local-mock-recording-failure-invalid-extra-v0.1.json delete mode 100644 docs/contracts/examples/local-mock-recording-failure-invalid-timeout-v0.1.json delete mode 100644 docs/contracts/examples/local-mock-recording-failure-upload-failed-v0.1.json delete mode 100644 docs/contracts/examples/static-cell-artifact-invalid-egress-v0.2.json delete mode 100644 docs/contracts/examples/static-cell-artifact-v0.2.json delete mode 100644 docs/contracts/examples/task-control-invalid-drain-v0.4.json delete mode 100644 docs/contracts/examples/task-control-invalid-expiry-v0.4.json delete mode 100644 docs/contracts/examples/task-control-pause-v0.4.json delete mode 100644 docs/contracts/examples/task-control-resume-v0.4.json delete mode 100644 docs/contracts/examples/task-control-stop-v0.4.json delete mode 100644 docs/contracts/examples/task-discovery-changes-v0.2.json delete mode 100644 docs/contracts/examples/task-discovery-changes-v0.4.json delete mode 100644 docs/contracts/examples/task-discovery-empty-v0.3.json delete mode 100644 docs/contracts/examples/task-discovery-error-v0.4.json delete mode 100644 docs/contracts/examples/task-discovery-http-statuses-v0.1.json delete mode 100644 docs/contracts/examples/task-discovery-http-statuses-v0.2.json delete mode 100644 docs/contracts/examples/task-discovery-http-statuses-v0.3.json delete mode 100644 docs/contracts/examples/task-discovery-invalid-changes-v0.3.json delete mode 100644 docs/contracts/examples/task-discovery-invalid-cursor-v0.3.json delete mode 100644 docs/contracts/examples/task-discovery-invalid-page-token-v0.2.json delete mode 100644 docs/contracts/examples/task-discovery-invalid-queue-property-v0.1.json delete mode 100644 docs/contracts/examples/task-discovery-invalid-queue-v0.2.json delete mode 100644 docs/contracts/examples/task-discovery-invalid-v0.4.json delete mode 100644 docs/contracts/examples/task-discovery-no-change-v0.2.json delete mode 100644 docs/contracts/examples/task-discovery-page-v0.3.json delete mode 100644 docs/contracts/examples/task-discovery-removed-v0.3.json delete mode 100644 docs/contracts/examples/task-discovery-snapshot-page1-v0.4.json delete mode 100644 docs/contracts/examples/task-discovery-snapshot-page2-v0.4.json delete mode 100644 docs/contracts/examples/task-discovery-snapshot-v0.1.json delete mode 100644 docs/contracts/examples/task-discovery-snapshot-v0.2.json delete mode 100644 docs/contracts/examples/task-discovery-updated-v0.3.json delete mode 100644 docs/contracts/local-contract-manifest-v0.1.json delete mode 100644 docs/contracts/local-contract-manifest-v0.2.json delete mode 100644 docs/contracts/local-contract-manifest-v0.3.json delete mode 100644 docs/contracts/local-contract-manifest-v0.4.json delete mode 100644 docs/contracts/local-contract-manifest-v0.5.json delete mode 100644 docs/contracts/local-contract-manifest-v0.6.json delete mode 100644 docs/contracts/local-mock-recording-failure-manifest-v0.1.json delete mode 100644 docs/contracts/local-mock-recording-failure-v0.1.md delete mode 100644 docs/contracts/local-mock-recording-failure-v0.1.schema.json delete mode 100644 docs/contracts/mq-topology-v0.1-proposal.json delete mode 100644 docs/contracts/static-cell-artifact-v0.2.schema.json delete mode 100644 docs/contracts/task-control-v0.4-proposal.schema.json delete mode 100644 docs/contracts/task-discovery-v0.1-proposal.schema.json delete mode 100644 docs/contracts/task-discovery-v0.2-proposal.schema.json delete mode 100644 docs/contracts/task-discovery-v0.3-proposal.schema.json delete mode 100644 docs/contracts/task-discovery-v0.4-proposal.schema.json delete mode 100644 docs/contracts/通信与事件数据交互_v0.1.md delete mode 100644 docs/thirds/v0.2.md delete mode 100644 docs/thirds/v0.3.md diff --git a/AGENTS.md b/AGENTS.md index 41ef1e1..d25ef36 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -77,9 +77,9 @@ ## 当前范围与权威入口 -- 当前工作由使用者批准按 P01–P08 实施,K01–K16 已确认,不重复审批;本目标由当前 Agent 独立执行,**不启动子 Agent**。只有未来重新获得授权,才按使用者指定的 `gpt-5.6-luna`、`max` 思考和 `fast: true` 逐项核验并显式配置,不静默换模型、降档或关闭 fast。 -- 唯一现行 SaaS↔Dispatcher 业务说明是 [`docs/thirds/saas-dispatcher.md`](docs/thirds/saas-dispatcher.md);当前项目内 Schema、拓扑、正反例及来源/hash 在 [`contracts/local/`](contracts/local/);内部 Agent RPC 在 [`proto/agent/agent.proto`](proto/agent/agent.proto)。进度与缺口见 [`docs/plan-saas-dispatcher.md`](docs/plan-saas-dispatcher.md) 和 [`docs/evidence/saas-dispatcher-implementation.md`](docs/evidence/saas-dispatcher-implementation.md)。文档不能代替机器合同或外部签收。 -- [`docs/plan-saas-dispatcher-v05-v0.1.md`](docs/plan-saas-dispatcher-v05-v0.1.md) 的原路径和原 SHA-256 已由 `contracts/local/manifest.json` 固定,只作**不可变历史来源**;旧 F/W 工作包、v0.1–v0.4 本地提案、旧 MQ-only 合同及 `docs/archive/` 不作为当前运行入口。固定 MQ `v1` 名称、HTTP `/internal/v1/dispatcher/...` 路径和有业务意义的 revision 是通信规则,不是应删除的自有实现代次。不得为历史路径新建兼容层或配置回退。 +- P01–P08 及 K01–K16 已完成**项目内隔离 Mock** 核验;本轮把分散的人类可读契约、文档与第三方对接合为唯一当前规范,不重审已确认规则。若未来另获启动开发/审查子 Agent 授权,必须按使用者指定的 `gpt-5.6-luna`、`max` 思考和 `fast: true` 逐项核验并显式配置,不静默换模型、降档或关闭 fast。 +- 唯一现行 SaaS↔Dispatcher 业务规范是 [`docs/thirds/saas-dispatcher.md`](docs/thirds/saas-dispatcher.md);当前项目内 Schema、拓扑、正反例及来源/hash 在 [`contracts/local/`](contracts/local/);内部 Agent RPC 在 [`proto/agent/agent.proto`](proto/agent/agent.proto)。本地验收与外部缺口见 [`docs/evidence/saas-dispatcher-p08-acceptance.md`](docs/evidence/saas-dispatcher-p08-acceptance.md)。Markdown 不代替机器合同或外部签收,也不另外维护一份平行字段定义。 +- 已由当前合同来源清单固定哈希的历史提案与计划保留**原字节**于 [`docs/archive/sources/`](docs/archive/sources/README.md),使用者原有未提交的两份旧对接文档也按原字节归档;旧上游 v1 在 [`docs/archive/upstream/`](docs/archive/upstream/README.md) 可离线校验,但不嵌入运行合同。旧 F/W 工作包、旧 MQ-only 合同及归档不作为当前运行入口。固定 MQ `v1`、HTTP `/internal/v1/dispatcher/...` 和业务 revision 是现行通信规则,不是自有实现代次;不得为历史路径新建兼容或回退。 - 当前本地范围仅**单节点、单 Dispatcher、单 Agent、单 Cell、单租户和隔离 Mock**。根命令只接受显式 `agent`/`dispatcher` 业务子命令,mixed/real 启动即拒绝;没有真实 SaaS、management、RabbitMQ、OSS、AI 供应商、Asterisk、SIP 线路、ECS 或生产签收。生产发布包仍为 `production_approval=false`。本机 Mock HTTPS、双向 TLS、RabbitMQ 和哈希通过均不授权真实呼叫。 - 开发按 TDD 分批,小步提交;不得覆盖使用者现存修改/未跟踪文件,不自动清理、迁移或覆盖任何现存 SQLite、spool、outbox 和 Agent 恢复文件。旧 `.executions` 及恢复根目录中旧 `.uploads`、`.upload-locks`、逐执行 `state.json` 的发现须只读失败关闭,现存未交付事实由使用者确认处置。真实云账号、EIP、线路、拨号、生产部署和共享数据操作分别需要明确授权。 diff --git a/README.md b/README.md index ccc209a..cf12e47 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,14 @@ 独立 Go SIP 调度与执行项目。单一 Go 1.27.1 module/制品通过 Cobra 显式提供 `dispatcher`、`agent` 两个业务子命令;当前业务启动只允许**隔离 Mock**,mixed/real 直接拒绝。目标是分阶段以 Go 替换 Agent,不重写 Asterisk,也不建立第二套 SaaS 管理后台。项目的源码、文档、依赖、构建、测试、配置和发布入口均在本仓库内。 -> **范围与进度:** [当前实施与验收入口](docs/plan-saas-dispatcher.md)记录 P01–P07 项目内已验证,以及 P08 最终 A01–A12/K01–K16 与 ≥65% 业务覆盖率对照待完成。隔离 RabbitMQ、双向 TLS/HTTPS、OSS PUT 与 AI Mock 的通过,不代表真实 SaaS、管理平台、MQ 应用收讫、OSS/AI 供应商、Asterisk/SIP/ECS、真实拨号或生产切换已验证;当前发布清单 `production_approval=false`。真实试拨另需逐次授权,并遵守白名单、Asia/Shanghai `09:00`–`20:00` 及拨号前诊断抓包门禁。 +> **范围与进度:** [唯一现行规范](docs/thirds/saas-dispatcher.md)和 [P01–P08 本地验收对照](docs/evidence/saas-dispatcher-p08-acceptance.md)分别说明当前合同及已通过的隔离测试(手写业务覆盖率 72.0%)。隔离 RabbitMQ、双向 TLS/HTTPS、OSS PUT 与 AI Mock 的通过,不代表真实 SaaS、管理平台、MQ 应用收讫、OSS/AI 供应商、Asterisk/SIP/ECS、真实拨号或生产切换已验证;当前发布清单 `production_approval=false`。真实试拨另需逐次授权,并遵守白名单、Asia/Shanghai `09:00`–`20:00` 及拨号前诊断抓包门禁。 ## 唯一当前接口 - [SaaS↔Dispatcher 通信说明](docs/thirds/saas-dispatcher.md);机器合同为 [`contracts/local/`](contracts/local/) 的 Schema、拓扑、正反例和来源/hash。SaaS 的 SIP、任务、归属任务清单、租户额度和 AI 供应商配置只经五类只读 HTTP;呼叫、控制、必要回执与每通话唯一结果只经固定 `v1` RabbitMQ 通信名。固定 HTTP `/internal/v1/dispatcher/...`、MQ `v1` 及业务 revision 是有效合同,不是旧实现代次。 - Dispatcher 独立 UUID/资源归属、SaaS 预建的控制/任务队列、共享持久结果队列及本地 SQLite 权威任务/额度/inbox/outbox;未知通话不释放占用、不重拨、重启继续原消息身份。Agent 与 Dispatcher 通过双向 TLS Unary gRPC 和预绑定身份/会话代际交互,业务恢复落私有文件。内部联系见 [`proto/agent/agent.proto`](proto/agent/agent.proto)。 - 任务内不可变获批 AI 参数支持 ASR-only 与 ASR+LLM+TTS;录音经短期授权由 Agent **一次** HTTPS PUT 至 OSS,明确失败才按受控文件恢复,结果未知不自动再传,最终事实进入 MQ。批准 Mock 的 SIP revision 回报不证明 Asterisk 已实际加载。 -- [`docs/plan-saas-dispatcher-v05-v0.1.md`](docs/plan-saas-dispatcher-v05-v0.1.md) 保持原字节和来源 hash,**只作为当前机器合同的历史来源**,不是另一个运行计划;更早的 F/W/v0.1–v0.4、旧 MQ-only 与部署示例在历史文档或 [`docs/archive/`](docs/archive/) 中追溯,不作为运行回退。 +- 历史来源和使用者修改的旧文档按原字节保存在 [`docs/archive/sources/`](docs/archive/sources/README.md),上游 v1 保存在 [`docs/archive/upstream/`](docs/archive/upstream/README.md) 并可离线校验;旧阶段计划已归档。它们不是另一个当前版本或运行回退。 ## 本地验证(不触碰真实线路) @@ -22,7 +22,7 @@ make release-check-local # 本地制品、hash、当前拓扑、Mock-only/不 ## 导航 -- [当前实施与 A01–A12 验收](docs/plan-saas-dispatcher.md) · [当前通信合同](docs/thirds/saas-dispatcher.md) · [当前机器合同](contracts/local/manifest.json) · [可复核本地证据](docs/evidence/saas-dispatcher-implementation.md) +- [唯一现行规范](docs/thirds/saas-dispatcher.md) · [当前机器合同](contracts/local/manifest.json) · [P01–P08 本地验收](docs/evidence/saas-dispatcher-p08-acceptance.md) - [文档索引](docs/README.md) · [物理部署及独立 Asterisk 约束](deploys/README.md) · [项目开发与安全约束](AGENTS.md) 现存 SQLite、spool、outbox 与 Agent 恢复文件不自动清空或迁移;发现旧状态拒绝启动并保留原文件,必须先厘清未交付事实。仓库不保存真实密钥、完整用户音频或完整对话。 diff --git a/contracts/current_layout_test.go b/contracts/current_layout_test.go index 3d06cec..7aebaea 100644 --- a/contracts/current_layout_test.go +++ b/contracts/current_layout_test.go @@ -1,11 +1,38 @@ package contracts_test import ( + "errors" "os" + "path/filepath" "strings" "testing" + + "git.ipao.vip/rogee/go-sip/contracts" ) +func TestOneCurrentHumanContractAndArchivedSources(t *testing.T) { + entries, err := os.ReadDir(filepath.Join("..", "docs", "thirds")) + if err != nil { + t.Fatal(err) + } + if len(entries) != 1 || entries[0].Name() != "saas-dispatcher.md" { + t.Fatalf("expected one current human-readable contract, got %v", entries) + } + for _, path := range []string{filepath.Join("..", "docs", "contracts"), filepath.Join("..", "docs", "plan-saas-dispatcher.md")} { + if _, err := os.Stat(path); !errors.Is(err, os.ErrNotExist) { + t.Fatalf("redundant current document %s remains or cannot be checked: %v", path, err) + } + } + for _, name := range []string{"v0.4.md", "第三方对接事件与请求消费顺序_v0.1.md", "v0.5-proposal.md", "plan-saas-dispatcher-v05-v0.1.md"} { + if _, err := os.Stat(filepath.Join("..", "docs", "archive", "sources", name)); err != nil { + t.Fatalf("historical source %s missing from archive: %v", name, err) + } + } + if _, err := contracts.Files.ReadFile("upstream/v1/mq.schema.json"); err == nil { + t.Fatal("retired upstream v1 contract still embedded with current runtime contracts") + } +} + func TestCurrentContractDirectoryExcludesHistoricalGenerations(t *testing.T) { entries, err := os.ReadDir("local") if err != nil { diff --git a/contracts/local/manifest.json b/contracts/local/manifest.json index 141bb39..487be3e 100644 --- a/contracts/local/manifest.json +++ b/contracts/local/manifest.json @@ -2,9 +2,9 @@ "scope": "project-local-current", "status": "isolated-mock-only-external-unverified", "sources": { - "docs/thirds/v0.5-proposal.md": "612fdaee50aff6aa7fbef16c2d469d99857646c6d2235617d0e67f6098cd7ada", - "docs/plan-saas-dispatcher-v05-v0.1.md": "666f39e56ea9f4b55661efcac82edd6f9729848e2d60e5f24cdf5aa3ac97ee87", - "docs/thirds/saas-dispatcher.md": "0ee54323ac815ced2327dc8f37140fca53605466e8f78cf8183223326075ca03" + "docs/archive/sources/v0.5-proposal.md": "612fdaee50aff6aa7fbef16c2d469d99857646c6d2235617d0e67f6098cd7ada", + "docs/archive/sources/plan-saas-dispatcher-v05-v0.1.md": "666f39e56ea9f4b55661efcac82edd6f9729848e2d60e5f24cdf5aa3ac97ee87", + "docs/thirds/saas-dispatcher.md": "19102a481aae33320aa3f6df619d3ea39af6d41609a34d56b21ca078e735e393" }, "bundle_sha256": "4ff0afffa2c865050091c042d8f98bbe344ba9a4f4c3652e721a5077217722ed", "bundle_algorithm": "sha256 of sorted relative-path + space + sha256(file) + newline; only root-level JSON and examples/**/*.json, excluding manifest.json" diff --git a/docs/README.md b/docs/README.md index 1d4be1a..e82180e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,25 +1,9 @@ -# 文档导航 +# 文档入口 -`docs/` 是本项目设计、合同、决策和验收事实的维护目录。以下入口以**当前项目内隔离 Mock 合同**为准;真实 SaaS/management、供应商、线路、ECS 和生产切换仍未签收。 +- **唯一现行人类可读规范:** [`thirds/saas-dispatcher.md`](thirds/saas-dispatcher.md),包含当前 SaaS↔Dispatcher 契约、业务规则、第三方交互和本地验收边界。 +- **可执行的机器合同:** [`../contracts/local/manifest.json`](../contracts/local/manifest.json) 及同目录 Schema、MQ 拓扑和正反例;内部 Agent RPC 以 [`../proto/agent/agent.proto`](../proto/agent/agent.proto) 为准。Markdown 不替代机器校验。 +- **可核查的验收事实:** [`evidence/saas-dispatcher-p08-acceptance.md`](evidence/saas-dispatcher-p08-acceptance.md)。P01–P08 只在单节点/单 Agent/单 Cell/单租户的隔离 Mock 中验证;真实 SaaS/management、MQ 应用收讫、OSS/AI/SIP/Asterisk/ECS 和生产均未签收。 +- **归档来源与历史:** [`archive/sources/README.md`](archive/sources/README.md) 保存合同来源及使用者修改的旧文档原件;[`archive/upstream/README.md`](archive/upstream/README.md) 保存可离线核验的上游 v1;[`archive/plan-saas-dispatcher-completed.md`](archive/plan-saas-dispatcher-completed.md) 是已完成的阶段计划,不是第二份现行规范。其它旧证据和工作包均只供追溯,不作为兼容回退。 +- **部署与开发约束:** [`../deploys/README.md`](../deploys/README.md)、[`../AGENTS.md`](../AGENTS.md)。实际新主机/版本/Cell 验证及任何真实试拨必须另获授权并执行部署、状态、拨号前受限抓包与诊断门禁。 -## 首读顺序 - -1. [`plan-saas-dispatcher.md`](plan-saas-dispatcher.md):P01–P08 当前实施进度、A01–A12/K01–K16 验收与未验证边界。 -2. [`thirds/saas-dispatcher.md`](thirds/saas-dispatcher.md):唯一现行 SaaS↔Dispatcher 项目内通信说明,五类只读 HTTP、固定 `v1` MQ、归属、控制、唯一结果与上传恢复。 -3. [`../contracts/local/manifest.json`](../contracts/local/manifest.json) 与 [`../contracts/local/`](../contracts/local/):唯一当前机器 Schema、拓扑、正反例和不可变来源/hash;内部 Agent RPC 见 [`../proto/agent/agent.proto`](../proto/agent/agent.proto)。Markdown 不代替机器校验。 -4. [`evidence/saas-dispatcher-implementation.md`](evidence/saas-dispatcher-implementation.md):本地测试、故障/重启与未验证项的可核查证据;[`evidence/saas-dispatcher-p07-audit.md`](evidence/saas-dispatcher-p07-audit.md) 逐项核查旧入口、历史例外与来源/hash。 -5. [`../AGENTS.md`](../AGENTS.md):本独立项目开发、SIP、部署、真实呼叫与诊断约束。 - -## 其它文档 - -- [`architecture/`](architecture/) 保存产品方案、边界与阶段背景;[`decisions/`](decisions/) 保存设计决定;[`contracts/`](contracts/) 是原有合同和提案记录,当前机器约束以 `../contracts/local/` 为准;[`acceptance/`](acceptance/) 保存跨阶段验收基线,须按本轮单节点/单 Cell/单租户范围核对。 -- [`plan-saas-dispatcher-v05-v0.1.md`](plan-saas-dispatcher-v05-v0.1.md) 是**不可变历史来源**:原文件路径和 SHA-256 被当前合同 `manifest.json` 引用,必须保持原字节,不是第二个当前运行计划。旧 [`plan-config-read-v0.1.md`](plan-config-read-v0.1.md)、[`plan-dispatcher-state-v0.1.md`](plan-dispatcher-state-v0.1.md)、[`plan-0926.md`](plan-0926.md)、`thirds/v0.1–v0.4` 和上游 MQ-only v1 只供追溯旧事实,不能覆盖当前项目内合同,也不能作为兼容回退。 -- [`archive/`](archive/) 保存旧 W/F 工作包、旧部署样例、上一版 AGENTS 原文,以及 [`contracts-local-history/`](archive/contracts-local-history/README.md) 中 17 个逐字节保留的旧本地 Schema/示例;它们均不参与当前合同或运行校验。[`archive/plan-0918.md`](archive/plan-0918.md) 是旧 W00–W16 原文,不是现在的执行入口。归档说明不能代签真实验收。 -- [`evidence/README.md`](evidence/README.md) 说明历史证据保留规则;旧本地结果仍按当时环境和版本解读,不能据此推断当前合同或真实供应商已通过。 -- [`../deploys/README.md`](../deploys/README.md) 为 Mock-only 本地发布及物理环境要求,实际新主机/版本/Cell 诊断与拨号前受限抓包见 [`../deploys/test/nonprod-call-evidence.sh`](../deploys/test/nonprod-call-evidence.sh)。本地包检查不替代真实 Asterisk 加载或生产审批。 - -## 维护规则 - -- 不在文档中维护第二套 Schema,不改写已被来源/hash 固定的原文;新增当前字段先更新机器合同及正反例,再更新当前说明与来源记录。 -- 当前状态只在 `plan-saas-dispatcher.md` 与实施证据中维护;历史文件保留真实来源和阶段,不为消除旧名称而重写归档或受保护的使用者修改。 -- 报告须区分本地隔离 Mock、真实非生产和生产,注明哪些测试未执行。源码、样例、日志、证据均不得保存真实凭据、完整用户音频或完整用户对话。 +新增字段应先更新机器合同与正反例,再同步唯一现行规范、来源哈希和本地验收;不另写平行 Schema 或版本文档。不得在源码、日志、文档和证据中保存真实密钥、完整音频或完整对话。 diff --git a/docs/archive/README.md b/docs/archive/README.md index b88c3ef..a10ecab 100644 --- a/docs/archive/README.md +++ b/docs/archive/README.md @@ -2,7 +2,13 @@ 这里保留已被当前基线替代、但仍有追溯价值的历史记录;归档不等于删除。 -## 本次归档 +## 当前接口整理的归档 + +- [`sources/README.md`](sources/README.md):当前机器合同所需的历史提案/计划原件,以及使用者此前修改的两份旧对接文档,原字节和 SHA-256 均有记录。 +- [`upstream/README.md`](upstream/README.md):已退役上游 v1 全量原件,保留原始清单、以归档路径离线校验,但不再嵌入当前运行代码。 +- [`plan-saas-dispatcher-completed.md`](plan-saas-dispatcher-completed.md):已完成的 P01–P08 阶段计划,不是现行接口说明;现行规范只有 [`../thirds/saas-dispatcher.md`](../thirds/saas-dispatcher.md)。 + +## 先前归档 - [`plan-0918.md`](plan-0918.md):原 W00–W16 项目总计划,按仓库 HEAD **逐字恢复**后归档。归档原因:用户要求改用[本轮新计划](../plan-config-read-v0.1.md)单写 F00–F06 状态;旧计划的批准和本地证据仍可追溯,不能代签新接口通过。为保留原文,旧计划内相对链接仍按归档前位置书写,不作为现行导航。 - `evidence/20260920-acceptance-status.md`:MQ-only 修订前的验收状态摘要。 diff --git a/docs/plan-saas-dispatcher.md b/docs/archive/plan-saas-dispatcher-completed.md similarity index 100% rename from docs/plan-saas-dispatcher.md rename to docs/archive/plan-saas-dispatcher-completed.md diff --git a/docs/archive/sources/README.md b/docs/archive/sources/README.md new file mode 100644 index 0000000..f6d8710 --- /dev/null +++ b/docs/archive/sources/README.md @@ -0,0 +1,12 @@ +# 不可变历史来源 + +这里只有追溯当前本地合同及保存使用者原有修改所需的原件,**不是**当前接口版本。唯一现行说明见 [`../../thirds/saas-dispatcher.md`](../../thirds/saas-dispatcher.md);机器合同见 `contracts/local/manifest.json`。移动时保持原文件字节不变,下面的 SHA-256 对应归档后文件。 + +| 原路径 | 归档文件 | SHA-256 | 用途 | +| --- | --- | --- | --- | +| `docs/thirds/v0.5-proposal.md` | [`v0.5-proposal.md`](v0.5-proposal.md) | `612fdaee50aff6aa7fbef16c2d469d99857646c6d2235617d0e67f6098cd7ada` | 当前合同的不可变提案来源;清单仍可离线校验。 | +| `docs/plan-saas-dispatcher-v05-v0.1.md` | [`plan-saas-dispatcher-v05-v0.1.md`](plan-saas-dispatcher-v05-v0.1.md) | `666f39e56ea9f4b55661efcac82edd6f9729848e2d60e5f24cdf5aa3ac97ee87` | P01–P08 历史批准及来源。 | +| `docs/thirds/v0.4.md` | [`v0.4.md`](v0.4.md) | `27c7070b0cfec90dded2db5e2083bb23e6dd1c7ddf2e1346f54159835b8c928d` | 使用者未提交的版本导航已随完整文件原样保存。 | +| `docs/thirds/第三方对接事件与请求消费顺序_v0.1.md` | [`第三方对接事件与请求消费顺序_v0.1.md`](第三方对接事件与请求消费顺序_v0.1.md) | `e774c39c91673302095c666a57609cbaae623e55fe73a3d280eff02d381affa4` | 使用者未提交的版本导航已随完整文件原样保存。 | + +历史文档内部的相对链接保持原文,不能当作现行导航;未迁移其旧语义或据此宣称真实 SaaS、供应商或生产已通过。 diff --git a/docs/plan-saas-dispatcher-v05-v0.1.md b/docs/archive/sources/plan-saas-dispatcher-v05-v0.1.md similarity index 100% rename from docs/plan-saas-dispatcher-v05-v0.1.md rename to docs/archive/sources/plan-saas-dispatcher-v05-v0.1.md diff --git a/docs/thirds/v0.4.md b/docs/archive/sources/v0.4.md similarity index 96% rename from docs/thirds/v0.4.md rename to docs/archive/sources/v0.4.md index 532e126..9237460 100644 --- a/docs/thirds/v0.4.md +++ b/docs/archive/sources/v0.4.md @@ -2,6 +2,8 @@ **项目内 v0.4 Schema、隔离 Mock、运行代码及本地验收已完成(见 [`dispatcher-v04-local-acceptance.md`](../evidence/dispatcher-v04-local-acceptance.md));真实 SaaS 尚未签收或联调。** 已发布外部合同仍以 `contracts/upstream/v1/` 为基线;v0.3 历史发现见 [`v0.3`](v0.3.md),其余历史规则见[第三方对接顺序 v0.1](第三方对接事件与请求消费顺序_v0.1.md)。本文件不授权真实呼叫或部署。实施步骤与待冻字段见 [`plan-dispatcher-state-v0.1.md`](../plan-dispatcher-state-v0.1.md)。 +**后续需求(仅文档评估,尚无 Schema/代码)**:AI 服务商全量接口、任务挂机关键词及 SIP 控制队列变更通知见 [下一版提案](v0.5-proposal.md)。其中移除 `agent_version_id` 与现行严格任务 Schema 冲突,不属于 v0.4 已通过的本地验收。 + ## 1. 不变的两类 SaaS → D 队列 - 每个 Dispatcher 一条 SaaS 预建、独占的**控制队列**,接收 `task.control` 的 pause/resume/stop;每个归属任务另有一条 SaaS 预建**任务队列**,接收 `call.execute`。D 只消费,不能创建、绑定或删除。D→SaaS 的控制/命令回执和 `call.result` 仍走既有 per-D 结果路由,不能把结果队列误认为入站任务队列。 diff --git a/docs/thirds/v0.5-proposal.md b/docs/archive/sources/v0.5-proposal.md similarity index 100% rename from docs/thirds/v0.5-proposal.md rename to docs/archive/sources/v0.5-proposal.md diff --git a/docs/thirds/第三方对接事件与请求消费顺序_v0.1.md b/docs/archive/sources/第三方对接事件与请求消费顺序_v0.1.md similarity index 99% rename from docs/thirds/第三方对接事件与请求消费顺序_v0.1.md rename to docs/archive/sources/第三方对接事件与请求消费顺序_v0.1.md index 44869e4..0a58d0f 100644 --- a/docs/thirds/第三方对接事件与请求消费顺序_v0.1.md +++ b/docs/archive/sources/第三方对接事件与请求消费顺序_v0.1.md @@ -2,6 +2,8 @@ 本文是本轮 P1 Go/Mock 实现使用的**项目内 SaaS 对接契约**。F01/F07 按本文和链接的机器 Schema 冻结后,直接用于本地实现和 Mock SaaS 验证;不等待外部 SaaS/management 签收或连通。下文“草案/拟定/待签收”仅表示真实 SaaS/management 兼容性未验证,不阻塞本地实现。真实 SaaS 当前仍可能运行 `contracts/upstream/v1`,其兼容性未验证;Mock 通过只证明本地契约,不代表真实 SaaS、management 或生产验收。不要把现行 v1 与本轮契约拼成一个线上协议。示例为非生产数据,JSON 代码块均为完整请求或返回体;字段说明写在块外。 +**版本导航:**本页保留当时 F01/F07 v0.1 的请求示例与历史决策;任务发现、控制与外呼现行项目内基线见 [v0.4](v0.4.md)。新增 AI 服务商全量读取、挂机关键词和 SIP 控制队列通知的**未实施评估**见 [v0.5 提案](v0.5-proposal.md);其 `agent_version_id` 移除、事件格式不能直接写入本页旧严格 Schema 或视为真实 SaaS 已支持。 + 路径来源:`/internal/v1/dispatcher/sip`、`/internal/v1/dispatcher/task/:task_id`、`/internal/v1/dispatcher/tasks`(含 `?after=`)为用户给定路径;`/internal/v1/dispatcher/tenant/:tenant_id/quota` 与 `route_policy_id`、`caller_profile_id`、`allowed_trunk_ids` 是项目定义的新增路径/字段,按本文实现并在真实对接时记录兼容状态。 ## 0. 本轮项目内实施规则 diff --git a/docs/contracts/ai-authorization-v0.2.schema.json b/docs/contracts/ai-authorization-v0.2.schema.json deleted file mode 100644 index 566040d..0000000 --- a/docs/contracts/ai-authorization-v0.2.schema.json +++ /dev/null @@ -1,109 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://go-sip.local/contracts/proposals/ai-authorization-v0.2.schema.json", - "title": "Dispatcher to Agent immutable AI authorization", - "type": "object", - "additionalProperties": false, - "required": [ - "authorization_id", - "tenant_id", - "tenant_key", - "agent_version_id", - "config_sha256", - "mode", - "issued_at", - "expires_at", - "source", - "revoked" - ], - "properties": { - "authorization_id": { - "type": "string", - "minLength": 1, - "maxLength": 128 - }, - "tenant_id": { - "type": "string", - "minLength": 1, - "maxLength": 128 - }, - "tenant_key": { - "type": "string", - "minLength": 1, - "maxLength": 224 - }, - "agent_version_id": { - "type": "string", - "minLength": 1, - "maxLength": 128 - }, - "config_sha256": { - "type": "string", - "pattern": "^[a-f0-9]{64}$" - }, - "mode": { - "enum": [ - "full_ai", - "asr_only" - ] - }, - "issued_at": { - "type": "string", - "format": "date-time" - }, - "expires_at": { - "type": "string", - "format": "date-time" - }, - "source": { - "enum": [ - "saas", - "mock-saas" - ] - }, - "credential_refs": { - "type": "object", - "additionalProperties": false, - "properties": { - "asr": { - "type": "string", - "minLength": 1, - "maxLength": 128 - }, - "llm": { - "type": "string", - "minLength": 1, - "maxLength": 128 - }, - "tts": { - "type": "string", - "minLength": 1, - "maxLength": 128 - } - } - }, - "revoked": { - "type": "boolean" - }, - "revocation_reason": { - "type": "string", - "maxLength": 256 - } - }, - "allOf": [ - { - "if": { - "properties": { - "revoked": { - "const": true - } - } - }, - "then": { - "required": [ - "revocation_reason" - ] - } - } - ] -} diff --git a/docs/contracts/call-execute-v0.4-proposal.schema.json b/docs/contracts/call-execute-v0.4-proposal.schema.json deleted file mode 100644 index bf88383..0000000 --- a/docs/contracts/call-execute-v0.4-proposal.schema.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://go-sip.local/contracts/proposals/call-execute-v0.4-proposal.schema.json", - "title": "Project-local call.execute ingress; external SaaS compatibility unverified", - "$comment": "Only replaces call.execute ingress in command-next.v0.1-proposal. This MQ command has no expiry; dispatcher must obtain and check fresh independent authorization before admission and originate. The command.result egress remains v0.1.", - "type": "object", - "additionalProperties": false, - "required": ["schema_version", "dispatcher_id", "tenant_id", "tenant_key", "trace_id", "issued_at", "command_id", "command_type", "payload"], - "properties": { - "schema_version": {"const": "call-execute.v0.4-proposal"}, - "dispatcher_id": {"$ref": "https://go-sip.local/contracts/v1/mq.schema.json#/$defs/dispatcherId"}, - "tenant_id": {"$ref": "https://go-sip.local/contracts/v1/mq.schema.json#/$defs/id"}, - "tenant_key": {"$ref": "https://go-sip.local/contracts/v1/mq.schema.json#/$defs/tenantKey"}, - "trace_id": {"$ref": "https://go-sip.local/contracts/v1/mq.schema.json#/$defs/id"}, - "issued_at": {"$ref": "https://go-sip.local/contracts/v1/mq.schema.json#/$defs/time"}, - "command_id": {"$ref": "https://go-sip.local/contracts/v1/mq.schema.json#/$defs/id"}, - "command_type": {"const": "call.execute"}, - "payload": { - "type": "object", "additionalProperties": false, - "required": ["task_id", "callee"], - "properties": { - "task_id": {"type": "string", "pattern": "^[A-Za-z0-9_-]{1,128}$"}, - "callee": {"type": "string", "minLength": 1} - } - } - } -} diff --git a/docs/contracts/call-result-v0.1-proposal.schema.json b/docs/contracts/call-result-v0.1-proposal.schema.json deleted file mode 100644 index 52cfba0..0000000 --- a/docs/contracts/call-result-v0.1-proposal.schema.json +++ /dev/null @@ -1,159 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://go-sip.local/contracts/proposals/call-result-v0.1-proposal.schema.json", - "title": "Project-local contract: single final call.result event; external compatibility unverified", - "$comment": "Project-local contract derived from docs/thirds/第三方对接事件与请求消费顺序_v0.1.md; not the published MQ v2 contract. Runtime caps each serialized UTF-8 JSON body at 8,388,608 bytes, without compression, truncation, or event splitting. Expected recording finalizes no later than call.ended_at + 15m; at most one PUT per upload_id and one immutable call.result event.", - "type": "object", - "additionalProperties": false, - "required": ["schema_version", "event_id", "event_type", "dispatcher_id", "tenant_id", "tenant_key", "trace_id", "occurred_at", "aggregate_type", "aggregate_id", "aggregate_version", "payload"], - "properties": { - "schema_version": {"const": "call-result.v0.1-proposal"}, - "event_id": {"$ref": "https://go-sip.local/contracts/v1/mq.schema.json#/$defs/id"}, - "event_type": {"const": "call.result"}, - "dispatcher_id": {"$ref": "https://go-sip.local/contracts/v1/mq.schema.json#/$defs/dispatcherId"}, - "tenant_id": {"$ref": "https://go-sip.local/contracts/v1/mq.schema.json#/$defs/id"}, - "tenant_key": {"$ref": "https://go-sip.local/contracts/v1/mq.schema.json#/$defs/tenantKey"}, - "trace_id": {"$ref": "https://go-sip.local/contracts/v1/mq.schema.json#/$defs/id"}, - "occurred_at": {"$ref": "https://go-sip.local/contracts/v1/mq.schema.json#/$defs/time"}, - "aggregate_type": {"const": "call"}, - "aggregate_id": {"$ref": "https://go-sip.local/contracts/v1/mq.schema.json#/$defs/id"}, - "aggregate_version": {"type": "integer", "minimum": 1}, - "payload": { - "type": "object", - "additionalProperties": false, - "required": ["source_command_id", "execution_id", "call_id", "task_id", "task_revision", "agent_version_id", "route_policy_id", "caller_profile_id", "callee", "trunk_id", "started_at", "ended_at", "duration_ms", "outcome", "reason_code", "transcript", "opt_out", "recording"], - "properties": { - "source_command_id": {"$ref": "https://go-sip.local/contracts/v1/mq.schema.json#/$defs/id"}, - "execution_id": {"$ref": "https://go-sip.local/contracts/v1/mq.schema.json#/$defs/id"}, - "call_id": {"$ref": "https://go-sip.local/contracts/v1/mq.schema.json#/$defs/id"}, - "task_id": {"$ref": "#/$defs/task_id"}, - "task_revision": {"type": "integer", "minimum": 1}, - "agent_version_id": {"$ref": "https://go-sip.local/contracts/v1/mq.schema.json#/$defs/id"}, - "route_policy_id": {"$ref": "https://go-sip.local/contracts/v1/mq.schema.json#/$defs/id"}, - "caller_profile_id": {"$ref": "https://go-sip.local/contracts/v1/mq.schema.json#/$defs/id"}, - "callee": {"type": "string", "minLength": 1}, - "trunk_id": {"$ref": "https://go-sip.local/contracts/v1/mq.schema.json#/$defs/id"}, - "started_at": {"$ref": "https://go-sip.local/contracts/v1/mq.schema.json#/$defs/time"}, - "ended_at": {"$ref": "https://go-sip.local/contracts/v1/mq.schema.json#/$defs/time"}, - "duration_ms": {"type": "integer", "minimum": 0}, - "outcome": {"type": "string", "minLength": 1}, - "reason_code": {"type": ["string", "null"], "minLength": 1}, - "transcript": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": false, - "required": ["turn_id", "segment_id", "role", "text", "start_ms", "end_ms"], - "properties": { - "turn_id": {"$ref": "https://go-sip.local/contracts/v1/mq.schema.json#/$defs/id"}, - "segment_id": {"$ref": "https://go-sip.local/contracts/v1/mq.schema.json#/$defs/id"}, - "role": {"type": "string", "minLength": 1}, - "text": {"type": "string"}, - "start_ms": {"type": "integer", "minimum": 0}, - "end_ms": {"type": "integer", "minimum": 0} - } - } - }, - "opt_out": {"type": "boolean"}, - "recording": { - "oneOf": [ - {"$ref": "#/$defs/recording_uploaded"}, - {"$ref": "#/$defs/recording_unavailable"}, - {"$ref": "#/$defs/recording_not_created"} - ] - } - }, - "$comment": "aggregate_id must equal payload.call_id; timestamps and duration must agree. Runtime enforces the 8 MiB serialized-body budget. If too large, retain the exact result in durable outbox and expose blocked_payload_too_large; never truncate, split, or discard it." - } - }, - "$defs": { - "task_id": {"type": "string", "pattern": "^[A-Za-z0-9_-]{1,128}$"}, - "recording_asset_fields": { - "type": "object", - "required": ["recording_id", "upload_id", "bucket", "object_key", "format", "channels", "sample_rate_hz", "duration_ms", "size_bytes", "checksum_sha256"], - "properties": { - "recording_id": {"type": ["string", "null"], "minLength": 1}, - "upload_id": {"type": ["string", "null"], "minLength": 1}, - "bucket": {"type": ["string", "null"], "minLength": 1}, - "object_key": {"type": ["string", "null"], "minLength": 1}, - "format": {"type": ["string", "null"], "minLength": 1}, - "channels": {"type": ["integer", "null"], "minimum": 1}, - "sample_rate_hz": {"type": ["integer", "null"], "minimum": 1}, - "duration_ms": {"type": ["integer", "null"], "minimum": 0}, - "size_bytes": {"type": ["integer", "null"], "minimum": 0}, - "checksum_sha256": {"type": ["string", "null"], "pattern": "^[a-f0-9]{64}$"} - } - }, - "recording_uploaded": { - "allOf": [ - {"$ref": "#/$defs/recording_asset_fields"}, - { - "type": "object", - "required": ["status"], - "properties": { - "status": {"const": "uploaded"}, - "recording_id": {"type": "string", "minLength": 1}, - "upload_id": {"type": "string", "minLength": 1}, - "bucket": {"type": "string", "minLength": 1}, - "object_key": {"type": "string", "minLength": 1}, - "format": {"type": "string", "minLength": 1}, - "channels": {"type": "integer", "minimum": 1}, - "sample_rate_hz": {"type": "integer", "minimum": 1}, - "duration_ms": {"type": "integer", "minimum": 0}, - "size_bytes": {"type": "integer", "minimum": 0}, - "checksum_sha256": {"type": "string", "pattern": "^[a-f0-9]{64}$"} - } - } - ], - "unevaluatedProperties": false - }, - "recording_unavailable": { - "allOf": [ - {"$ref": "#/$defs/recording_asset_fields"}, - { - "type": "object", - "required": ["status", "error_code"], - "properties": { - "status": {"const": "unavailable"}, - "error_code": {"enum": ["upload_authorization_failed", "upload_authorization_expired", "upload_failed", "upload_timeout", "deadline_exceeded", "checksum_mismatch"]}, - "recording_id": {"type": "string", "minLength": 1}, - "upload_id": {"type": "string", "minLength": 1}, - "bucket": {"type": "null"}, - "object_key": {"type": "null"}, - "format": {"type": "string", "minLength": 1}, - "channels": {"type": "integer", "minimum": 1}, - "sample_rate_hz": {"type": "integer", "minimum": 1}, - "duration_ms": {"type": "integer", "minimum": 0}, - "size_bytes": {"type": "null"}, - "checksum_sha256": {"type": "null"} - } - } - ], - "unevaluatedProperties": false - }, - "recording_not_created": { - "allOf": [ - {"$ref": "#/$defs/recording_asset_fields"}, - { - "type": "object", - "required": ["status", "reason_code"], - "properties": { - "status": {"const": "not_created"}, - "reason_code": {"type": "string", "pattern": "^[a-z][a-z0-9_]{0,63}$"}, - "recording_id": {"type": "null"}, - "upload_id": {"type": "null"}, - "bucket": {"type": "null"}, - "object_key": {"type": "null"}, - "format": {"type": "null"}, - "channels": {"type": "null"}, - "sample_rate_hz": {"type": "null"}, - "duration_ms": {"type": "null"}, - "size_bytes": {"type": "null"}, - "checksum_sha256": {"type": "null"} - } - } - ], - "unevaluatedProperties": false - } - } -} diff --git a/docs/contracts/command-next-v0.1-proposal.schema.json b/docs/contracts/command-next-v0.1-proposal.schema.json deleted file mode 100644 index f3549a4..0000000 --- a/docs/contracts/command-next-v0.1-proposal.schema.json +++ /dev/null @@ -1,209 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://go-sip.local/contracts/proposals/command-next-v0.1-proposal.schema.json", - "title": "Project-local contract: SaaS command and command-result messages; external compatibility unverified", - "$comment": "Project-local contract derived from docs/thirds/第三方对接事件与请求消费顺序_v0.1.md; not the published MQ v2 contract. Serialized UTF-8 JSON bodies are capped at 8,388,608 bytes by runtime validation.", - "oneOf": [ - {"$ref": "#/$defs/call_execute"}, - {"$ref": "#/$defs/control_pause"}, - {"$ref": "#/$defs/control_resume"}, - {"$ref": "#/$defs/control_stop"}, - {"$ref": "#/$defs/command_result_call"}, - {"$ref": "#/$defs/command_result_control"} - ], - "$defs": { - "id": {"$ref": "https://go-sip.local/contracts/v1/mq.schema.json#/$defs/id"}, - "dispatcher_id": {"$ref": "https://go-sip.local/contracts/v1/mq.schema.json#/$defs/dispatcherId"}, - "task_id": {"type": "string", "pattern": "^[A-Za-z0-9_-]{1,128}$"}, - "tenant_key": {"$ref": "https://go-sip.local/contracts/v1/mq.schema.json#/$defs/tenantKey"}, - "time": {"$ref": "https://go-sip.local/contracts/v1/mq.schema.json#/$defs/time"}, - "command_base": { - "type": "object", - "required": ["schema_version", "dispatcher_id", "tenant_id", "tenant_key", "trace_id", "issued_at", "not_after"], - "properties": { - "schema_version": {"const": "command-next.v0.1-proposal"}, - "dispatcher_id": {"$ref": "#/$defs/dispatcher_id"}, - "tenant_id": {"$ref": "#/$defs/id"}, - "tenant_key": {"$ref": "#/$defs/tenant_key"}, - "trace_id": {"$ref": "#/$defs/id"}, - "issued_at": {"$ref": "#/$defs/time"}, - "not_after": {"$ref": "#/$defs/time"} - } - }, - "call_execute": { - "allOf": [ - {"$ref": "#/$defs/command_base"}, - { - "type": "object", - "required": ["command_id", "command_type", "payload"], - "properties": { - "command_id": {"$ref": "#/$defs/id"}, - "command_type": {"const": "call.execute"}, - "payload": { - "type": "object", - "additionalProperties": false, - "required": ["task_id", "callee"], - "properties": { - "task_id": {"$ref": "#/$defs/task_id"}, - "callee": {"type": "string", "minLength": 1} - } - } - } - } - ], - "unevaluatedProperties": false, - "$comment": "Check not_after against current time and bind the task/tenant snapshot before originate; not_after must be later than issued_at." - }, - "control_pause": { - "allOf": [ - {"$ref": "#/$defs/command_base"}, - { - "type": "object", - "required": ["command_type", "payload"], - "properties": { - "command_type": {"const": "task.control"}, - "payload": { - "type": "object", - "additionalProperties": false, - "required": ["task_id", "action", "active_call_policy", "reason"], - "properties": { - "task_id": {"$ref": "#/$defs/task_id"}, - "action": {"const": "pause"}, - "active_call_policy": {"enum": ["drain", "hangup"]}, - "reason": {"type": "string", "minLength": 1, "maxLength": 512} - } - } - } - } - ], - "unevaluatedProperties": false - }, - "control_resume": { - "allOf": [ - {"$ref": "#/$defs/command_base"}, - { - "type": "object", - "required": ["command_type", "payload"], - "properties": { - "command_type": {"const": "task.control"}, - "payload": { - "type": "object", - "additionalProperties": false, - "required": ["task_id", "action", "reason"], - "properties": { - "task_id": {"$ref": "#/$defs/task_id"}, - "action": {"const": "resume"}, - "reason": {"type": "string", "minLength": 1, "maxLength": 512} - } - } - } - } - ], - "unevaluatedProperties": false - }, - "control_stop": { - "allOf": [ - {"$ref": "#/$defs/command_base"}, - { - "type": "object", - "required": ["command_type", "payload"], - "properties": { - "command_type": {"const": "task.control"}, - "payload": { - "type": "object", - "additionalProperties": false, - "required": ["task_id", "action", "active_call_policy", "reason"], - "properties": { - "task_id": {"$ref": "#/$defs/task_id"}, - "action": {"const": "stop"}, - "active_call_policy": {"enum": ["drain", "hangup"]}, - "reason": {"type": "string", "minLength": 1, "maxLength": 512} - } - } - } - } - ], - "unevaluatedProperties": false - }, - "event_base": { - "type": "object", - "required": ["schema_version", "event_id", "event_type", "dispatcher_id", "tenant_id", "tenant_key", "trace_id", "occurred_at", "aggregate_type", "aggregate_id", "aggregate_version"], - "properties": { - "schema_version": {"const": "command-next.v0.1-proposal"}, - "event_id": {"$ref": "#/$defs/id"}, - "event_type": {"const": "command.result"}, - "dispatcher_id": {"$ref": "#/$defs/dispatcher_id"}, - "tenant_id": {"$ref": "#/$defs/id"}, - "tenant_key": {"$ref": "#/$defs/tenant_key"}, - "trace_id": {"$ref": "#/$defs/id"}, - "occurred_at": {"$ref": "#/$defs/time"}, - "aggregate_type": {"enum": ["command", "task"]}, - "aggregate_id": {"$ref": "#/$defs/id"}, - "aggregate_version": {"type": "integer", "minimum": 1} - } - }, - "command_result_call": { - "allOf": [ - {"$ref": "#/$defs/event_base"}, - { - "type": "object", - "required": ["aggregate_type", "payload"], - "properties": { - "aggregate_type": {"const": "command"}, - "payload": { - "type": "object", - "additionalProperties": false, - "required": ["command_id", "command_type", "status", "reason_code"], - "properties": { - "command_id": {"$ref": "#/$defs/id"}, - "command_type": {"const": "call.execute"}, - "status": {"enum": ["accepted", "rejected"]}, - "reason_code": {"type": "string", "pattern": "^[a-z][a-z0-9_]{0,63}$"}, - "execution_id": {"$ref": "#/$defs/id"} - }, - "allOf": [ - { - "if": {"properties": {"status": {"const": "accepted"}}}, - "then": {"required": ["execution_id"]} - } - ] - } - } - } - ], - "unevaluatedProperties": false - }, - "command_result_control": { - "allOf": [ - {"$ref": "#/$defs/event_base"}, - { - "type": "object", - "required": ["aggregate_type", "payload"], - "properties": { - "aggregate_type": {"const": "task"}, - "payload": { - "type": "object", - "additionalProperties": false, - "required": ["command_type", "task_id", "action", "status", "reason_code", "task_state"], - "properties": { - "command_type": {"const": "task.control"}, - "task_id": {"$ref": "#/$defs/task_id"}, - "action": {"enum": ["pause", "resume", "stop"]}, - "status": {"enum": ["applied", "rejected"]}, - "reason_code": {"type": "string", "pattern": "^[a-z][a-z0-9_]{0,63}$"}, - "task_state": {"enum": ["running", "paused", "stopped", "finished"]} - }, - "allOf": [ - {"if": {"properties": {"action": {"const": "pause"}, "status": {"const": "applied"}}}, "then": {"properties": {"task_state": {"const": "paused"}}}}, - {"if": {"properties": {"action": {"const": "resume"}, "status": {"const": "applied"}}}, "then": {"properties": {"task_state": {"const": "running"}}}}, - {"if": {"properties": {"action": {"const": "stop"}, "status": {"const": "applied"}}}, "then": {"properties": {"task_state": {"const": "stopped"}}}} - ] - } - } - } - ], - "unevaluatedProperties": false, - "$comment": "No control command_id or request dedupe identity exists. event_id identifies a receipt event, not a control command." - } - } -} diff --git a/docs/contracts/config-read-fields-v0.1-proposal.md b/docs/contracts/config-read-fields-v0.1-proposal.md deleted file mode 100644 index 700eebf..0000000 --- a/docs/contracts/config-read-fields-v0.1-proposal.md +++ /dev/null @@ -1,102 +0,0 @@ -# 只读配置与租户额度接口:任务、智能体、SIP 字段与返回结构 v0.1(项目内 F01 规范) - -**状态:项目内 F01 字段规范;不是 SaaS 已有接口/实际 JSON,也不是外部发布契约。** 本地字段由本文件与[第三方对接契约](../thirds/第三方对接事件与请求消费顺序_v0.1.md)定义,使用严格 Schema、正反例、来源/hash 和 Mock 验证;不等待 SaaS/management 外部签收即可完成本地 C。截图可见的业务含义映射为**项目定义的字段名**;截图没有但需求明确的结构由本项目设计。即使字段名与现有项目 Schema 或历史 OpenAPI 相同,也**不能**据此声称它是当前 SaaS 页面原有后端键。真实配置发布、拨号或外部切换仍需另行授权和验证。 - -## 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 响应证据**。任务发现与分页语义以[当前 v0.4 契约](../thirds/v0.4.md)为准;其它历史背景见[第三方对接契约 v0.1](../thirds/第三方对接事件与请求消费顺序_v0.1.md)。旧 [OpenAPI/MQ 只读索引](../references/OpenAPI与MQ字段索引_v0.1.md) 也仅为历史参考。 -- **N = 新项目字段:**任务每周多时段/排除日期、SIP 线路时段、任务与单 D 绑定、缓存/版本/错误返回等,由本提案定义;实际 SaaS 接口不存在已验证响应。 - -路径来源:`/internal/v1/dispatcher/sip`、`/internal/v1/dispatcher/task/:task_id`、`/internal/v1/dispatcher/tasks`(含 `?after=`)为用户给定路径;`/internal/v1/dispatcher/tenant/:tenant_id/quota` 与任务路由字段 `route_policy_id`、`caller_profile_id`、`allowed_trunk_ids` 为本地项目定义。它们不是截图/现网接口已验证的响应键;本地 Mock 按本契约验证。 - -交付物:[机器可读响应 Schema](config-read-v0.1.schema.json)、[mock SIP 成功示例](examples/config-read-sip-v0.1.json)、[mock 任务成功示例](examples/config-read-task-v0.1.json)、[mock 租户额度示例](examples/config-read-tenant-quota-v0.1.json)、[mock 错误响应示例](examples/config-read-error-v0.1.json)及[预期被 Schema 拒绝的非法示例](examples/config-read-invalid-extra-property-v0.1.json)。四条只读 GET 为本地目标:`/internal/v1/dispatcher/sip`、`/internal/v1/dispatcher/task/:task_id`、`/internal/v1/dispatcher/tasks?after=` 与 `/internal/v1/dispatcher/tenant/:tenant_id/quota`。均携带 `X-DISPATCHER-id`(D UUID)及 `X-DISPATCHER-SECRET-KEY`(受控密钥),无请求体;真实 SaaS 实现和字段兼容性未验证。呼叫/控制/回执/最终结果走 MQ,目标移除对外 query/replay,不留旧 AI/SIP 配置 MQ 回退。错误状态和 code 按本文件 §2 与第三方对接契约定义并由 Mock 验证;本地 Schema 不冒充外部权威发布物。 - -## 2. 请求与共同响应 - -Dispatcher 仅用 `X-DISPATCHER-id`(全局唯一 UUID)和部署受控的 `X-DISPATCHER-SECRET-KEY` 请求归属资源;不记录密钥或在日志中打印配置提示词。接口只读、无控制副作用;服务端必须核验任务归属。Header 的本地错误约定为 HTTP 401 `unauthorized`、403 `dispatcher_not_authorized`;请求格式错误为 400 `invalid_request`,资源缺失/未归属为 404 `resource_not_found`,租户额度不可用为 503 `tenant_quota_unavailable`,临时服务故障为 503 `service_unavailable`。`GET /internal/v1/dispatcher/tasks` 的启动快照与运行期增量按[当前 v0.4 契约](../thirds/v0.4.md)执行:分页快照不可继续时返回 409 `snapshot_unavailable`,请求无效时返回 400 `invalid_request`/`invalid_page_token`;不沿用历史版 410 `cursor_expired`/`snapshot_expired`。任务发现严格结构见[任务发现 v0.4 Schema](task-discovery-v0.4-proposal.schema.json),不混入本文件的配置响应 Schema。以上仅为本地 Mock/Go 契约,不代表外部 SaaS 状态码。 - -| 请求 | `200` 返回类型 | 何时读取 | 错误处理 | -| --- | --- | --- | --- | -| `GET /internal/v1/dispatcher/sip` | `resource=sip_config`,本 D 的已批准完整 SIP 快照 | 新 D/重启先取齐并核对 Agent/Asterisk 精确加载;运行中约每 60 秒读取完整配置 | 读取失败/到期停止新执行准入,旧活动通话依原快照排空 | -| `GET /internal/v1/dispatcher/task/:task_id` | `resource=task_config`,归属 D 的单任务配置和已授权智能体快照 | 有待接纳呼叫时获取,活跃任务缓存约 60 秒;resume 必须重取最新配置,不能靠旧 running 恢复 | 失败/过期不放行;已接纳执行仍用原快照 | -| `GET /internal/v1/dispatcher/tenant/:tenant_id/quota`(新增路径草案) | `resource=tenant_quota`,分给该 D 的租户并发份额 | 拿到任务 tenant_id 后读取,同租户任务共享,缓存最多约60秒且不超过有效截止 | 缺失/过期/错身份停该租户新准入,额度0不影响stop静默排空 | - -`200` 响应中的 `schema_version` 固定 `config-read.v0.1`,`resource` 区分 SIP、任务及租户额度结构,`dispatcher_id` 必须等于 Header 中的 D。**不使用条件请求、ETag 或 `304`**:到期时重新 GET 完整响应;只有收到、验证并重新确认授权有效后才更新缓存。SaaS 变更到 D 的目标延迟约 60 秒;缓存到期且刷新失败,**不可无限期沿用旧版本发起新呼叫**。MQ 停/暂停不等待这 60 秒。已接纳/已接通呼叫固定自己的快照,不因缓存过期而漂移。 - -非 `200` 返回 `resource=error`、`error.code`、`error.message` 的脱敏 JSON(HTTP 状态与 code 按本节约定;真实 SaaS 是否一致尚未验证),不得吞成旧配置/空任务。下一版 `call.execute.payload` **只有 `task_id` 与 `callee`**;D 从已批准的任务快照读取路由/主叫/智能体版本及任务级 `ring_timeout_ms/max_call_duration_ms`,接纳前持久绑定完整快照,不能从精简命令中猜值或悄悄采用过期缓存。现行严格 MQ Schema 尚未修改。 - -## 3. 任务成功响应:字段与来源 - -响应中的英文键**全部是本项目提议的返回键**,不是从截图抓到的 SaaS JSON。P/C/N 只说明其业务含义的依据: - -| 返回位置 | 类型 / 是否必有 | 含义及来源 | -| --- | --- | --- | -| `dispatcher_id`, `tenant_id`, `tenant_key`, `task_id`, `task_revision` | UUID v4、租户ID、原值租户键、任务ID、正整数;必有 | C:当前命令及每 D/租户身份;N:任务固定归属一个 D,SaaS 必须持久保存 `(tenant_key,task_id)→dispatcher_id`;不得跨 D 投递。tenant_id 与原值 tenant_key 一对一映射,取得任务后按 tenant_id 读取本 D 租户额度,不能从 task_id 猜。`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`, `ring_timeout_ms`, `max_call_duration_ms` | 正整数、正整数毫秒、正整数毫秒;必有 | P:任务并发;N:振铃及最长通话时间是**任务配置**,所有新接纳呼叫由同一获准任务快照取得,不由逐呼命令任意覆盖。通话有效上限取任务 max_call_duration_ms 与已授权 AI conversation.max_duration_ms 较小值,执行侧/AI控制器一致且不改原快照;并发另受租户份额、供应商/Cell/AI约束;截图里的“5”不是默认值。 | -| `route_policy_id`, `caller_profile_id`, `allowed_trunk_ids[]` | 路由标识、明确主叫引用、有序候选线路数组;必有 | N:route_policy_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`, `config` | ID、严格 AI 对象;必有 | P:任务选择 AI 模型/智能体;C:现有不可变 `agent_version_id` 和[AI Schema](../../contracts/upstream/v1/ai-config.schema.json)。不再返回 `content_sha256`;同版本不得变内容的检查应基于版本绑定及本地持久快照,不能悄悄接受漂移。`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` 字段证明授权。 - -新call.execute没有逐呼variables来源;需要未提供变量的提示词必须拒绝或在F01先补获批来源,不能填空继续执行。状态来源按总计划§3.3:stopped不可逆、paused只能经最新有效resume解除;任务缓存和tasks增量的旧running不能解锁。 - -### 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 配置。 - -## 4.1 租户额度响应(项目内新增字段 N,外部未签收) - -| 字段 | 类型/约束 | 业务语义 | -| --- | --- | --- | -| schema_version/resource | config-read.v0.1 / tenant_quota | 项目草案,不是现网版本。 | -| dispatcher_id/tenant_id/tenant_key | D UUID、租户ID、原值租户键,必有 | 与请求、任务、信封一致,SaaS一对一映射;错误不猜值。 | -| quota_revision | 正整数,必有 | 本D租户额度版本,旧版本不覆盖新分配。 | -| max_concurrent_calls | 非负整数,必有 | 本D同租户所有任务共用份额,0禁止新呼叫;不是每任务分别上限。 | -| valid_until | RFC3339时间,必有 | 截止后不得新准入,本地缓存最多约60秒且不得越过此时刻。 | - -D 在同一事务预留租户/任务/线路等占用,未知继续计入;降额不强挂、占用低于新上限才再接新。额度缺失、过期/错身份、刷新失败关闭新准入,不能以任务额度代替。已确认通话终结/执行资源释放即可释放通话额度,不等录音上传或MQ确认;stop静默ACK不需申请通话名额。多D须由SaaS分份额,累计不超过租户总额;本轮只验证单D。 - -## 5. 外部待核事项(不阻塞本地 F01/C) - -1. **外部来源与审批:**真实 SaaS/management 联调前,确认 SaaS 的任务、智能体是该服务的权威配置;management 仍是唯一 SIP 编辑/审批方。证明 SaaS 分发的是已批准制品与线路补充字段同一版本,不能出现两份可写配置。此项不阻塞本地 Mock。 -2. **身份和响应:**本地请求头为 `X-DISPATCHER-id` 与 `X-DISPATCHER-SECRET-KEY`;只读取归属 D 的 `/internal/v1/dispatcher/sip`、`/internal/v1/dispatcher/tasks`、`/internal/v1/dispatcher/task/:task_id` 和 `/internal/v1/dispatcher/tenant/:tenant_id/quota`,不在日志/示例保存真实密钥。身份校验、状态码与生效时间按本文件和第三方对接契约作为本地规则;真实 SaaS 兼容性未验证。不采用 `ETag`/`304`。 -3. **窗口与版本:**本地缓存成功核验起约 60 秒;SaaS 变更对未接纳呼叫最多约 60 秒延迟,过期重新 GET 完整数据失败就停止新准入,已接纳保留原快照。更新/SIP 加载期间停执行队列,不停控制 MQ;停/暂停不等缓存。没有 `304` 延长授权的通道。跨日窗口、重叠段、当日排除、时间边界及任务与线路交集按本地 Schema/业务测试执行;真实 SaaS 行为未验证。 -4. **一致性:**`agent_version_id` 与 AI 授权一致且有效,同版内容漂移必须拒绝;任务归属/修订/route policy 以已绑定任务快照为准,MQ 命令不得覆盖;`artifact.trunks` 与 `trunk_details` 一一对应;线路 status、主叫、前缀、媒体、线路/租户/供应商额度来源和 Agent/Asterisk 实际加载不可依赖 JSON Schema 单独判断。供应商未知传输/鉴权/注册不得默认允许 real。 -5. **消费状态:**pause保留原队列积压,resume最新配置/授权/额度有效才继续消费,无需SaaS重新投递;新版命令不带 `not_after`,积压外呼仍须在接纳和拨号前重新核验时段及授权。stop后未接纳积压静默消费ACK,不拨号、不发逐条回执/最终结果;控制本身与已在途通话结果仍回传,本地计数/错误不静默。状态优先级及例外按总计划§3.3,不加控制去重。 -6. **退出与过渡:**新接口上线前现行 MQ-only/单 D/固定时段/静态 SIP 仍有效。切到新版本后配置只走 HTTP,旧 `ai.config.request/result` 停用,不做 HTTP→MQ 回退;业务 MQ 正常运行。多 D 任务归属/共享额度份额、旧命令/缓存/恢复记录和控制屏障须单独验证;任务结束只删除配置缓存,不删除未决执行与消息事实。 - -**验证状态:**本地 JSON Schema 草案包含 SIP、任务、租户额度 `200` 成功响应、有效错误响应及一个额外字段非法样例;只有 Schema 校验通过的有效示例可作为正例,非法样例必须被拒绝。此离线校验**不能**证明真实 SaaS 接口字段名、management 签收、hash 规范、Agent SDK 映射、SIP 实际加载或任何生产外呼验收。 diff --git a/docs/contracts/config-read-fields-v0.2-proposal.md b/docs/contracts/config-read-fields-v0.2-proposal.md deleted file mode 100644 index 649007f..0000000 --- a/docs/contracts/config-read-fields-v0.2-proposal.md +++ /dev/null @@ -1,100 +0,0 @@ -# 只读配置与租户额度接口:任务、智能体、SIP 字段与返回结构 v0.1(项目内 F01 规范) - -**状态:项目内 F01 字段规范;不是 SaaS 已有接口/实际 JSON,也不是外部发布契约。** 本地字段由本文件与[第三方对接契约](../thirds/第三方对接事件与请求消费顺序_v0.1.md)定义,使用严格 Schema、正反例、来源/hash 和 Mock 验证;不等待 SaaS/management 外部签收即可完成本地 C。截图可见的业务含义映射为**项目定义的字段名**;截图没有但需求明确的结构由本项目设计。即使字段名与现有项目 Schema 或历史 OpenAPI 相同,也**不能**据此声称它是当前 SaaS 页面原有后端键。真实配置发布、拨号或外部切换仍需另行授权和验证。 - -## 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 响应证据**。任务发现与分页语义以[当前 v0.4 契约](../thirds/v0.4.md)为准;其它历史背景见[第三方对接契约 v0.1](../thirds/第三方对接事件与请求消费顺序_v0.1.md)。旧 [OpenAPI/MQ 只读索引](../references/OpenAPI与MQ字段索引_v0.1.md) 也仅为历史参考。 -- **N = 新项目字段:**任务每周多时段/排除日期、SIP 线路时段、任务与单 D 绑定、缓存/版本/错误返回等,由本提案定义;实际 SaaS 接口不存在已验证响应。 - -路径来源:`/internal/v1/dispatcher/sip`、`/internal/v1/dispatcher/task/:task_id`、`/internal/v1/dispatcher/tasks`(含 `?after=`)为用户给定路径;`/internal/v1/dispatcher/tenant/:tenant_id/quota` 与任务路由字段 `route_policy_id`、`caller_profile_id`、`allowed_trunk_ids` 为本地项目定义。它们不是截图/现网接口已验证的响应键;本地 Mock 按本契约验证。 - -交付物:[新版机器可读响应 Schema](config-read-v0.2.schema.json)、[新版 mock SIP 成功示例](examples/config-read-sip-v0.2.json)([旧 Schema](config-read-v0.1.schema.json)/[旧示例](examples/config-read-sip-v0.1.json)保持历史不变)、[mock 任务成功示例](examples/config-read-task-v0.1.json)、[mock 租户额度示例](examples/config-read-tenant-quota-v0.1.json)、[mock 错误响应示例](examples/config-read-error-v0.1.json)及[预期被 Schema 拒绝的非法示例](examples/config-read-invalid-extra-property-v0.1.json)。四条只读 GET 为本地目标:`/internal/v1/dispatcher/sip`、`/internal/v1/dispatcher/task/:task_id`、`/internal/v1/dispatcher/tasks?after=` 与 `/internal/v1/dispatcher/tenant/:tenant_id/quota`。均携带 `X-DISPATCHER-id`(D UUID)及 `X-DISPATCHER-SECRET-KEY`(受控密钥),无请求体;真实 SaaS 实现和字段兼容性未验证。呼叫/控制/回执/最终结果走 MQ,目标移除对外 query/replay,不留旧 AI/SIP 配置 MQ 回退。错误状态和 code 按本文件 §2 与第三方对接契约定义并由 Mock 验证;本地 Schema 不冒充外部权威发布物。 - -## 2. 请求与共同响应 - -Dispatcher 仅用 `X-DISPATCHER-id`(全局唯一 UUID)和部署受控的 `X-DISPATCHER-SECRET-KEY` 请求归属资源;不记录密钥或在日志中打印配置提示词。接口只读、无控制副作用;服务端必须核验任务归属。Header 的本地错误约定为 HTTP 401 `unauthorized`、403 `dispatcher_not_authorized`;请求格式错误为 400 `invalid_request`,资源缺失/未归属为 404 `resource_not_found`,租户额度不可用为 503 `tenant_quota_unavailable`,临时服务故障为 503 `service_unavailable`。`GET /internal/v1/dispatcher/tasks` 的启动快照与运行期增量按[当前 v0.4 契约](../thirds/v0.4.md)执行:分页快照不可继续时返回 409 `snapshot_unavailable`,请求无效时返回 400 `invalid_request`/`invalid_page_token`;不沿用历史版 410 `cursor_expired`/`snapshot_expired`。任务发现严格结构见[任务发现 v0.4 Schema](task-discovery-v0.4-proposal.schema.json),不混入本文件的配置响应 Schema。以上仅为本地 Mock/Go 契约,不代表外部 SaaS 状态码。 - -| 请求 | `200` 返回类型 | 何时读取 | 错误处理 | -| --- | --- | --- | --- | -| `GET /internal/v1/dispatcher/sip` | `resource=sip_config`,本 D 的已批准完整 SIP 快照 | 新 D/重启先取齐并核对 Agent/Asterisk 精确加载;运行中约每 60 秒读取完整配置 | 读取失败/到期停止新执行准入,旧活动通话依原快照排空 | -| `GET /internal/v1/dispatcher/task/:task_id` | `resource=task_config`,归属 D 的单任务配置和已授权智能体快照 | 有待接纳呼叫时获取,活跃任务缓存约 60 秒;resume 必须重取最新配置,不能靠旧 running 恢复 | 失败/过期不放行;已接纳执行仍用原快照 | -| `GET /internal/v1/dispatcher/tenant/:tenant_id/quota`(新增路径草案) | `resource=tenant_quota`,分给该 D 的租户并发份额 | 拿到任务 tenant_id 后读取,同租户任务共享,缓存最多约60秒且不超过有效截止 | 缺失/过期/错身份停该租户新准入,额度0不影响stop静默排空 | - -SIP `200` 响应的 `schema_version` 固定 `config-read.v0.2`,其余配置响应继续使用 `config-read.v0.1`,`resource` 区分 SIP、任务及租户额度结构,`dispatcher_id` 必须等于 Header 中的 D。**不使用条件请求、ETag 或 `304`**:到期时重新 GET 完整响应;只有收到、验证并重新确认授权有效后才更新缓存。SaaS 变更到 D 的目标延迟约 60 秒;缓存到期且刷新失败,**不可无限期沿用旧版本发起新呼叫**。MQ 停/暂停不等待这 60 秒。已接纳/已接通呼叫固定自己的快照,不因缓存过期而漂移。 - -非 `200` 返回 `resource=error`、`error.code`、`error.message` 的脱敏 JSON(HTTP 状态与 code 按本节约定;真实 SaaS 是否一致尚未验证),不得吞成旧配置/空任务。下一版 `call.execute.payload` **只有 `task_id` 与 `callee`**;D 从已批准的任务快照读取路由/主叫/智能体版本及任务级 `ring_timeout_ms/max_call_duration_ms`,接纳前持久绑定完整快照,不能从精简命令中猜值或悄悄采用过期缓存。现行严格 MQ Schema 尚未修改。 - -## 3. 任务成功响应:字段与来源 - -响应中的英文键**全部是本项目提议的返回键**,不是从截图抓到的 SaaS JSON。P/C/N 只说明其业务含义的依据: - -| 返回位置 | 类型 / 是否必有 | 含义及来源 | -| --- | --- | --- | -| `dispatcher_id`, `tenant_id`, `tenant_key`, `task_id`, `task_revision` | UUID v4、租户ID、原值租户键、任务ID、正整数;必有 | C:当前命令及每 D/租户身份;N:任务固定归属一个 D,SaaS 必须持久保存 `(tenant_key,task_id)→dispatcher_id`;不得跨 D 投递。tenant_id 与原值 tenant_key 一对一映射,取得任务后按 tenant_id 读取本 D 租户额度,不能从 task_id 猜。`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`, `ring_timeout_ms`, `max_call_duration_ms` | 正整数、正整数毫秒、正整数毫秒;必有 | P:任务并发;N:振铃及最长通话时间是**任务配置**,所有新接纳呼叫由同一获准任务快照取得,不由逐呼命令任意覆盖。通话有效上限取任务 max_call_duration_ms 与已授权 AI conversation.max_duration_ms 较小值,执行侧/AI控制器一致且不改原快照;并发另受租户份额、供应商/Cell/AI约束;截图里的“5”不是默认值。 | -| `route_policy_id`, `caller_profile_id`, `allowed_trunk_ids[]` | 路由标识、明确主叫引用、有序候选线路数组;必有 | N:route_policy_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`, `config` | ID、严格 AI 对象;必有 | P:任务选择 AI 模型/智能体;C:现有不可变 `agent_version_id` 和[AI Schema](../../contracts/upstream/v1/ai-config.schema.json)。不再返回 `content_sha256`;同版本不得变内容的检查应基于版本绑定及本地持久快照,不能悄悄接受漂移。`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` 字段证明授权。 - -新call.execute没有逐呼variables来源;需要未提供变量的提示词必须拒绝或在F01先补获批来源,不能填空继续执行。状态来源按总计划§3.3:stopped不可逆、paused只能经最新有效resume解除;任务缓存和tasks增量的旧running不能解锁。 - -### 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` | UUID v4、正整数、带偏移时间;必有 | N:指定 D 的完整获批 SIP 线路版本。每次内容变化递增 revision;相同版本内容漂移及版本倒退必须拒绝。 | -| `trunks[]` | 严格数组;必有 | N:单份获批线路清单;每项含 `trunk_id/provider_id/egress_pool_id/codec/dial_prefix/enabled`、服务端、鉴权、主叫、额度及每周时段。线路 ID 和同线路主叫引用不得重复。前缀只用于本线路,主叫保留原值(可含字母);PCMA 仍须实际线路验证。 | -| `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 只提供 SIP 连接和线路拨号约束,不下发 Agent/Asterisk 的部署参数、静态 Cell 制品、运行模式或全局号码白名单。部署参数及 Cell 身份由本地受控配置核对。management 仍是 SIP 唯一编辑/审批面,SaaS 必须分发同一获批版本。D 从 Agent 实际运行状态核对已加载的 SIP revision;HTTP `200` 及仅收到配置不代表已生效。示例 `transport/auth_mode/registration_required/max_concurrent_calls=null` 是供应商待确认项,**不满足 real 放行**。 - -## 4.1 租户额度响应(项目内新增字段 N,外部未签收) - -| 字段 | 类型/约束 | 业务语义 | -| --- | --- | --- | -| schema_version/resource | config-read.v0.1 / tenant_quota | 项目草案,不是现网版本。 | -| dispatcher_id/tenant_id/tenant_key | D UUID、租户ID、原值租户键,必有 | 与请求、任务、信封一致,SaaS一对一映射;错误不猜值。 | -| quota_revision | 正整数,必有 | 本D租户额度版本,旧版本不覆盖新分配。 | -| max_concurrent_calls | 非负整数,必有 | 本D同租户所有任务共用份额,0禁止新呼叫;不是每任务分别上限。 | -| valid_until | RFC3339时间,必有 | 截止后不得新准入,本地缓存最多约60秒且不得越过此时刻。 | - -D 在同一事务预留租户/任务/线路等占用,未知继续计入;降额不强挂、占用低于新上限才再接新。额度缺失、过期/错身份、刷新失败关闭新准入,不能以任务额度代替。已确认通话终结/执行资源释放即可释放通话额度,不等录音上传或MQ确认;stop静默ACK不需申请通话名额。多D须由SaaS分份额,累计不超过租户总额;本轮只验证单D。 - -## 5. 外部待核事项(不阻塞本地 F01/C) - -1. **外部来源与审批:**真实 SaaS/management 联调前,确认 SaaS 的任务、智能体是该服务的权威配置;management 仍是唯一 SIP 编辑/审批方。证明 SaaS 分发的是 management 已批准的完整 SIP 线路版本,不能出现两份可写配置。此项不阻塞本地 Mock。 -2. **身份和响应:**本地请求头为 `X-DISPATCHER-id` 与 `X-DISPATCHER-SECRET-KEY`;只读取归属 D 的 `/internal/v1/dispatcher/sip`、`/internal/v1/dispatcher/tasks`、`/internal/v1/dispatcher/task/:task_id` 和 `/internal/v1/dispatcher/tenant/:tenant_id/quota`,不在日志/示例保存真实密钥。身份校验、状态码与生效时间按本文件和第三方对接契约作为本地规则;真实 SaaS 兼容性未验证。不采用 `ETag`/`304`。 -3. **窗口与版本:**本地缓存成功核验起约 60 秒;SaaS 变更对未接纳呼叫最多约 60 秒延迟,过期重新 GET 完整数据失败就停止新准入,已接纳保留原快照。更新/SIP 加载期间停执行队列,不停控制 MQ;停/暂停不等缓存。没有 `304` 延长授权的通道。跨日窗口、重叠段、当日排除、时间边界及任务与线路交集按本地 Schema/业务测试执行;真实 SaaS 行为未验证。 -4. **一致性:**`agent_version_id` 与 AI 授权一致且有效,同版内容漂移必须拒绝;任务归属/修订/route policy 以已绑定任务快照为准,MQ 命令不得覆盖;`trunks` 中线路 ID/主叫引用唯一;线路 status、主叫、前缀、线路/租户/供应商额度来源和 Agent/Asterisk 实际加载不可依赖 JSON Schema 单独判断。供应商未知传输/鉴权/注册不得默认允许 real。 -5. **消费状态:**pause保留原队列积压,resume最新配置/授权/额度有效才继续消费,无需SaaS重新投递;新版命令不带 `not_after`,积压外呼仍须在接纳和拨号前重新核验时段及授权。stop后未接纳积压静默消费ACK,不拨号、不发逐条回执/最终结果;控制本身与已在途通话结果仍回传,本地计数/错误不静默。状态优先级及例外按总计划§3.3,不加控制去重。 -6. **退出与过渡:**新接口上线前现行 MQ-only/单 D/固定时段/静态 SIP 仍有效。切到新版本后配置只走 HTTP,旧 `ai.config.request/result` 停用,不做 HTTP→MQ 回退;业务 MQ 正常运行。多 D 任务归属/共享额度份额、旧命令/缓存/恢复记录和控制屏障须单独验证;任务结束只删除配置缓存,不删除未决执行与消息事实。 - -**验证状态:**本地 JSON Schema 草案包含 SIP、任务、租户额度 `200` 成功响应、有效错误响应及一个额外字段非法样例;只有 Schema 校验通过的有效示例可作为正例,非法样例必须被拒绝。此离线校验**不能**证明真实 SaaS 接口字段名、management 签收、hash 规范、Agent SDK 映射、SIP 实际加载或任何生产外呼验收。 diff --git a/docs/contracts/config-read-fields-v0.3-proposal.md b/docs/contracts/config-read-fields-v0.3-proposal.md deleted file mode 100644 index 96aa384..0000000 --- a/docs/contracts/config-read-fields-v0.3-proposal.md +++ /dev/null @@ -1,15 +0,0 @@ -# 项目内出口池字段收敛(v0.3 提案) - -此提案是项目内 Mock 契约,不代表 SaaS/management/AI 供应商已发布或签收。旧导入的 `contracts/upstream/v1/` 和 SIP v0.2 包保持原样,仅作历史输入;真实外部切换须另行核验新版权威来源。 - -## SIP 只读配置 - -`GET /internal/v1/dispatcher/sip` 的 200 使用 `config-read.v0.3`,依据 [Schema](config-read-v0.3.schema.json) 和 [正例](examples/config-read-sip-v0.3.json)。根级仍为 `schema_version/resource/dispatcher_id/revision/approved_at/trunks`;线路保留 `trunk_id/provider_id/codec/dial_prefix/enabled`、连接参数、主叫、额度和时段。**不定义 `egress_pool_id`**;[旧字段反例](examples/config-read-sip-invalid-egress-v0.3.json) 须被拒绝。任务与租户额度接口保持 v0.1。完整 SIP 版本单调递增,同版本内容变更必须拒绝;Agent 实际加载版本核对不变。 - -## AI 授权与静态 Cell 制品 - -用户已确认没有独立出口池授权。项目内 [AI 授权 v0.2 Schema](ai-authorization-v0.2.schema.json) 不再定义 `allowed_egress_pool_ids`,只保留租户、版本、不可变配置摘要、期限、撤销、供应商/凭据引用等检查;[正例](examples/ai-authorization-v0.2.json)和[旧字段反例](examples/ai-authorization-invalid-egress-v0.2.json)须分别通过/拒绝。 - -项目内 [静态 Cell 制品 v0.2 Schema](static-cell-artifact-v0.2.schema.json) 不再定义 `trunks[].egress_pool_id`,保留 Cell 身份、部署版本、线路、codec、端点引用与实际加载核验;[正例](examples/static-cell-artifact-v0.2.json)和[旧字段反例](examples/static-cell-artifact-invalid-egress-v0.2.json)须分别通过/拒绝。部署配置不来自 SaaS。 - -Dispatcher 仍按**任务允许线路**、全局号码白名单、任务与线路时段、额度、授权期限执行准入;选线固定后不自动换线或重拨。删除独立出口池条件不等于放宽上述限制。外部 v1 的 AI 授权/静态制品/分散事件 Schema 不原地修改;这些旧字段不得作为新版运行入口的依据。没有真实 SaaS/management/Agent-Asterisk 联调证据,不能宣称已对外切换。 diff --git a/docs/contracts/config-read-v0.1.schema.json b/docs/contracts/config-read-v0.1.schema.json deleted file mode 100644 index bcbaa10..0000000 --- a/docs/contracts/config-read-v0.1.schema.json +++ /dev/null @@ -1,172 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://go-sip.local/contracts/proposals/config-read-v0.1.schema.json", - "title": "Project-local F01 contract: read-only configuration responses; external SaaS compatibility unverified", - "oneOf": [ - {"$ref": "#/$defs/sip_response"}, - {"$ref": "#/$defs/task_response"}, - {"$ref": "#/$defs/tenant_quota_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_id", "tenant_key", "task_id", "task_revision", "status", "max_concurrent_calls", "ring_timeout_ms", "max_call_duration_ms", "route_policy_id", "caller_profile_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_id": {"type": "string", "minLength": 1, "maxLength": 128}, - "tenant_key": {"type": "string", "minLength": 1, "maxLength": 196, "$comment": "Validate <=196 UTF-8 bytes in business logic; preserve the original value and do not place tenant_key in queue/routing names."}, - "task_id": {"type": "string", "pattern": "^[A-Za-z0-9_-]{1,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}, - "ring_timeout_ms": {"type": "integer", "minimum": 1}, - "max_call_duration_ms": {"type": "integer", "minimum": 1}, - "route_policy_id": {"type": "string", "minLength": 1, "maxLength": 128}, - "caller_profile_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"} - } - }, - "tenant_quota_response": { - "type": "object", "additionalProperties": false, - "required": ["schema_version", "resource", "dispatcher_id", "tenant_id", "tenant_key", "quota_revision", "max_concurrent_calls", "valid_until"], - "properties": { - "schema_version": {"const": "config-read.v0.1"}, - "resource": {"const": "tenant_quota"}, - "dispatcher_id": {"$ref": "#/$defs/dispatcher_id"}, - "tenant_id": {"type": "string", "minLength": 1, "maxLength": 128}, - "tenant_key": {"type": "string", "minLength": 1, "maxLength": 196}, - "quota_revision": {"type": "integer", "minimum": 1}, - "max_concurrent_calls": {"type": "integer", "minimum": 0}, - "valid_until": {"type": "string", "format": "date-time"} - }, - "$comment": "Project-local response: assigned share for this Dispatcher, aggregated across all tasks of the tenant. Validate tenant_id/tenant_key mapping and valid_until in business logic." - }, - "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": {"enum": ["invalid_request", "unauthorized", "dispatcher_not_authorized", "resource_not_found", "tenant_quota_unavailable", "service_unavailable"]}, - "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", "authorization_id", "authorization_expires_at", "config"], - "properties": { - "agent_version_id": {"type": "string", "minLength": 1, "maxLength": 128}, - "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"}, "$comment": "Each window is left-closed/right-open, start < end, and windows within a day must not overlap. Cross-midnight windows are split across two weekdays; 24:00 is allowed only as end."}, - "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)$"} - } - } - } -} diff --git a/docs/contracts/config-read-v0.2.schema.json b/docs/contracts/config-read-v0.2.schema.json deleted file mode 100644 index bd34a59..0000000 --- a/docs/contracts/config-read-v0.2.schema.json +++ /dev/null @@ -1,174 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://go-sip.local/contracts/proposals/config-read-v0.2.schema.json", - "title": "Project-local F01 contract: read-only configuration responses; external SaaS compatibility unverified", - "oneOf": [ - {"$ref": "#/$defs/sip_response"}, - {"$ref": "#/$defs/task_response"}, - {"$ref": "#/$defs/tenant_quota_response"}, - {"$ref": "#/$defs/error_response"} - ], - "$defs": { - "sip_response": { - "type": "object", - "additionalProperties": false, - "required": ["schema_version", "resource", "dispatcher_id", "revision", "approved_at", "trunks"], - "properties": { - "schema_version": {"const": "config-read.v0.2"}, - "resource": {"const": "sip_config"}, - "dispatcher_id": {"$ref": "#/$defs/dispatcher_id"}, - "revision": {"type": "integer", "minimum": 1}, - "approved_at": {"type": "string", "format": "date-time"}, - "trunks": { - "type": "array", "minItems": 1, "maxItems": 32, - "items": {"$ref": "#/$defs/trunk"} - } - } - }, - "task_response": { - "type": "object", - "additionalProperties": false, - "required": ["schema_version", "resource", "dispatcher_id", "tenant_id", "tenant_key", "task_id", "task_revision", "status", "max_concurrent_calls", "ring_timeout_ms", "max_call_duration_ms", "route_policy_id", "caller_profile_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_id": {"type": "string", "minLength": 1, "maxLength": 128}, - "tenant_key": {"type": "string", "minLength": 1, "maxLength": 196, "$comment": "Validate <=196 UTF-8 bytes in business logic; preserve the original value and do not place tenant_key in queue/routing names."}, - "task_id": {"type": "string", "pattern": "^[A-Za-z0-9_-]{1,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}, - "ring_timeout_ms": {"type": "integer", "minimum": 1}, - "max_call_duration_ms": {"type": "integer", "minimum": 1}, - "route_policy_id": {"type": "string", "minLength": 1, "maxLength": 128}, - "caller_profile_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"} - } - }, - "tenant_quota_response": { - "type": "object", "additionalProperties": false, - "required": ["schema_version", "resource", "dispatcher_id", "tenant_id", "tenant_key", "quota_revision", "max_concurrent_calls", "valid_until"], - "properties": { - "schema_version": {"const": "config-read.v0.1"}, - "resource": {"const": "tenant_quota"}, - "dispatcher_id": {"$ref": "#/$defs/dispatcher_id"}, - "tenant_id": {"type": "string", "minLength": 1, "maxLength": 128}, - "tenant_key": {"type": "string", "minLength": 1, "maxLength": 196}, - "quota_revision": {"type": "integer", "minimum": 1}, - "max_concurrent_calls": {"type": "integer", "minimum": 0}, - "valid_until": {"type": "string", "format": "date-time"} - }, - "$comment": "Project-local response: assigned share for this Dispatcher, aggregated across all tasks of the tenant. Validate tenant_id/tenant_key mapping and valid_until in business logic." - }, - "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": {"enum": ["invalid_request", "unauthorized", "dispatcher_not_authorized", "resource_not_found", "tenant_quota_unavailable", "service_unavailable"]}, - "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}$" - }, - "trunk": { - "type": "object", "additionalProperties": false, - "required": ["trunk_id", "provider_id", "egress_pool_id", "codec", "dial_prefix", "enabled", "server_host", "server_port", "transport", "auth_mode", "registration_required", "max_concurrent_calls", "caller_profiles", "schedule"], - "properties": { - "trunk_id": {"type": "string", "minLength": 1, "maxLength": 128}, - "provider_id": {"type": "string", "minLength": 1, "maxLength": 128}, - "egress_pool_id": {"type": "string", "minLength": 1, "maxLength": 128}, - "codec": {"const": "PCMA"}, - "dial_prefix": {"type": "string", "maxLength": 32}, - "enabled": {"type": "boolean"}, - "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", "authorization_id", "authorization_expires_at", "config"], - "properties": { - "agent_version_id": {"type": "string", "minLength": 1, "maxLength": 128}, - "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"}, "$comment": "Each window is left-closed/right-open, start < end, and windows within a day must not overlap. Cross-midnight windows are split across two weekdays; 24:00 is allowed only as end."}, - "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)$"} - } - } - } -} diff --git a/docs/contracts/config-read-v0.3.schema.json b/docs/contracts/config-read-v0.3.schema.json deleted file mode 100644 index b1a8559..0000000 --- a/docs/contracts/config-read-v0.3.schema.json +++ /dev/null @@ -1,173 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://go-sip.local/contracts/proposals/config-read-v0.3.schema.json", - "title": "Project-local F01 contract: read-only configuration responses; external SaaS compatibility unverified", - "oneOf": [ - {"$ref": "#/$defs/sip_response"}, - {"$ref": "#/$defs/task_response"}, - {"$ref": "#/$defs/tenant_quota_response"}, - {"$ref": "#/$defs/error_response"} - ], - "$defs": { - "sip_response": { - "type": "object", - "additionalProperties": false, - "required": ["schema_version", "resource", "dispatcher_id", "revision", "approved_at", "trunks"], - "properties": { - "schema_version": {"const": "config-read.v0.3"}, - "resource": {"const": "sip_config"}, - "dispatcher_id": {"$ref": "#/$defs/dispatcher_id"}, - "revision": {"type": "integer", "minimum": 1}, - "approved_at": {"type": "string", "format": "date-time"}, - "trunks": { - "type": "array", "minItems": 1, "maxItems": 32, - "items": {"$ref": "#/$defs/trunk"} - } - } - }, - "task_response": { - "type": "object", - "additionalProperties": false, - "required": ["schema_version", "resource", "dispatcher_id", "tenant_id", "tenant_key", "task_id", "task_revision", "status", "max_concurrent_calls", "ring_timeout_ms", "max_call_duration_ms", "route_policy_id", "caller_profile_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_id": {"type": "string", "minLength": 1, "maxLength": 128}, - "tenant_key": {"type": "string", "minLength": 1, "maxLength": 196, "$comment": "Validate <=196 UTF-8 bytes in business logic; preserve the original value and do not place tenant_key in queue/routing names."}, - "task_id": {"type": "string", "pattern": "^[A-Za-z0-9_-]{1,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}, - "ring_timeout_ms": {"type": "integer", "minimum": 1}, - "max_call_duration_ms": {"type": "integer", "minimum": 1}, - "route_policy_id": {"type": "string", "minLength": 1, "maxLength": 128}, - "caller_profile_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"} - } - }, - "tenant_quota_response": { - "type": "object", "additionalProperties": false, - "required": ["schema_version", "resource", "dispatcher_id", "tenant_id", "tenant_key", "quota_revision", "max_concurrent_calls", "valid_until"], - "properties": { - "schema_version": {"const": "config-read.v0.1"}, - "resource": {"const": "tenant_quota"}, - "dispatcher_id": {"$ref": "#/$defs/dispatcher_id"}, - "tenant_id": {"type": "string", "minLength": 1, "maxLength": 128}, - "tenant_key": {"type": "string", "minLength": 1, "maxLength": 196}, - "quota_revision": {"type": "integer", "minimum": 1}, - "max_concurrent_calls": {"type": "integer", "minimum": 0}, - "valid_until": {"type": "string", "format": "date-time"} - }, - "$comment": "Project-local response: assigned share for this Dispatcher, aggregated across all tasks of the tenant. Validate tenant_id/tenant_key mapping and valid_until in business logic." - }, - "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": {"enum": ["invalid_request", "unauthorized", "dispatcher_not_authorized", "resource_not_found", "tenant_quota_unavailable", "service_unavailable"]}, - "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}$" - }, - "trunk": { - "type": "object", "additionalProperties": false, - "required": ["trunk_id", "provider_id", "codec", "dial_prefix", "enabled", "server_host", "server_port", "transport", "auth_mode", "registration_required", "max_concurrent_calls", "caller_profiles", "schedule"], - "properties": { - "trunk_id": {"type": "string", "minLength": 1, "maxLength": 128}, - "provider_id": {"type": "string", "minLength": 1, "maxLength": 128}, - "codec": {"const": "PCMA"}, - "dial_prefix": {"type": "string", "maxLength": 32}, - "enabled": {"type": "boolean"}, - "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", "authorization_id", "authorization_expires_at", "config"], - "properties": { - "agent_version_id": {"type": "string", "minLength": 1, "maxLength": 128}, - "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"}, "$comment": "Each window is left-closed/right-open, start < end, and windows within a day must not overlap. Cross-midnight windows are split across two weekdays; 24:00 is allowed only as end."}, - "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)$"} - } - } - } -} diff --git a/docs/contracts/examples/ai-authorization-invalid-egress-v0.2.json b/docs/contracts/examples/ai-authorization-invalid-egress-v0.2.json deleted file mode 100644 index 4728fd6..0000000 --- a/docs/contracts/examples/ai-authorization-invalid-egress-v0.2.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "authorization_id": "auth-1", - "tenant_id": "tenant-1", - "tenant_key": "tenant-demo-key", - "agent_version_id": "agent_asr_v1", - "config_sha256": "51a1f367066aaaaa7c5f5ce50b229eb8644d27ada57f8b5575c254dd4c9930d7", - "mode": "asr_only", - "issued_at": "2026-09-18T00:00:00Z", - "expires_at": "2026-09-18T00:01:00Z", - "source": "mock-saas", - "credential_refs": { - "asr": "mock-asr-credential" - }, - "allowed_egress_pool_ids": [ - "egress-mock" - ], - "revoked": false -} diff --git a/docs/contracts/examples/ai-authorization-v0.2.json b/docs/contracts/examples/ai-authorization-v0.2.json deleted file mode 100644 index 7298b2a..0000000 --- a/docs/contracts/examples/ai-authorization-v0.2.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "authorization_id": "auth-1", - "tenant_id": "tenant-1", - "tenant_key": "tenant-demo-key", - "agent_version_id": "agent_asr_v1", - "config_sha256": "51a1f367066aaaaa7c5f5ce50b229eb8644d27ada57f8b5575c254dd4c9930d7", - "mode": "asr_only", - "issued_at": "2026-09-18T00:00:00Z", - "expires_at": "2026-09-18T00:01:00Z", - "source": "mock-saas", - "credential_refs": { - "asr": "mock-asr-credential" - }, - "revoked": false -} diff --git a/docs/contracts/examples/call-execute-altcallee-v0.4.json b/docs/contracts/examples/call-execute-altcallee-v0.4.json deleted file mode 100644 index da4cfaa..0000000 --- a/docs/contracts/examples/call-execute-altcallee-v0.4.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "schema_version": "call-execute.v0.4-proposal", - "dispatcher_id": "11111111-1111-4111-8111-111111111111", - "tenant_id": "33333333-3333-4333-8333-333333333333", - "tenant_key": "tenant-A", - "trace_id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", - "command_id": "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", - "issued_at": "2026-09-27T02:00:00Z", - "command_type": "call.execute", - "payload": {"task_id": "a02", "callee": "15830461047"} -} diff --git a/docs/contracts/examples/call-execute-invalid-expiry-v0.4.json b/docs/contracts/examples/call-execute-invalid-expiry-v0.4.json deleted file mode 100644 index 45cedee..0000000 --- a/docs/contracts/examples/call-execute-invalid-expiry-v0.4.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "schema_version": "call-execute.v0.4-proposal", - "dispatcher_id": "11111111-1111-4111-8111-111111111111", - "tenant_id": "33333333-3333-4333-8333-333333333333", - "tenant_key": "tenant-A", - "trace_id": "44444444-4444-4444-8444-444444444444", - "command_id": "77777777-7777-4777-8777-777777777777", - "issued_at": "2026-09-20T01:00:00Z", - "not_after": "2026-09-20T01:05:00Z", - "command_type": "call.execute", - "payload": {"task_id": "a01", "callee": "15003164745"} -} diff --git a/docs/contracts/examples/call-execute-old-v0.4.json b/docs/contracts/examples/call-execute-old-v0.4.json deleted file mode 100644 index 20a5575..0000000 --- a/docs/contracts/examples/call-execute-old-v0.4.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "schema_version": "call-execute.v0.4-proposal", - "dispatcher_id": "11111111-1111-4111-8111-111111111111", - "tenant_id": "33333333-3333-4333-8333-333333333333", - "tenant_key": "tenant-A", - "trace_id": "44444444-4444-4444-8444-444444444444", - "command_id": "77777777-7777-4777-8777-777777777777", - "issued_at": "2026-09-20T01:00:00Z", - "command_type": "call.execute", - "payload": {"task_id": "a01", "callee": "15003164745"} -} diff --git a/docs/contracts/examples/call-execute-recent-v0.4.json b/docs/contracts/examples/call-execute-recent-v0.4.json deleted file mode 100644 index b4c1048..0000000 --- a/docs/contracts/examples/call-execute-recent-v0.4.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "schema_version": "call-execute.v0.4-proposal", - "dispatcher_id": "11111111-1111-4111-8111-111111111111", - "tenant_id": "33333333-3333-4333-8333-333333333333", - "tenant_key": "tenant-A", - "trace_id": "88888888-8888-4888-8888-888888888888", - "command_id": "99999999-9999-4999-8999-999999999999", - "issued_at": "2026-09-27T01:00:00Z", - "command_type": "call.execute", - "payload": {"task_id": "a01", "callee": "15003164745"} -} diff --git a/docs/contracts/examples/call-result-invalid-missing-checksum-v0.1.json b/docs/contracts/examples/call-result-invalid-missing-checksum-v0.1.json deleted file mode 100644 index e422022..0000000 --- a/docs/contracts/examples/call-result-invalid-missing-checksum-v0.1.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "schema_version": "call-result.v0.1-proposal", - "event_id": "call-result-invalid-001", - "event_type": "call.result", - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", - "tenant_id": "tenant-a", - "tenant_key": "tenant-a", - "trace_id": "trace-v2", - "occurred_at": "2026-09-18T10:10:15+08:00", - "aggregate_type": "call", - "aggregate_id": "call-a", - "aggregate_version": 1, - "payload": { - "source_command_id": "command-a", - "execution_id": "execution-a", - "call_id": "call-a", - "task_id": "task-a", - "task_revision": 1, - "agent_version_id": "version-a", - "route_policy_id": "route-a", - "caller_profile_id": "caller-a", - "callee": "15003164745", - "trunk_id": "trunk-a", - "started_at": "2026-09-18T10:00:00+08:00", - "ended_at": "2026-09-18T10:10:00+08:00", - "duration_ms": 600000, - "outcome": "answered", - "reason_code": null, - "transcript": [], - "opt_out": false, - "recording": { - "status": "uploaded", - "recording_id": "recording-a", - "upload_id": "upload-a", - "bucket": "example-bucket", - "object_key": "calls/tenant-a/call-a.wav", - "format": "wav", - "channels": 1, - "sample_rate_hz": 8000, - "duration_ms": 600000, - "size_bytes": 9600000 - } - } -} diff --git a/docs/contracts/examples/call-result-uploaded-v0.1.json b/docs/contracts/examples/call-result-uploaded-v0.1.json deleted file mode 100644 index 651ffd6..0000000 --- a/docs/contracts/examples/call-result-uploaded-v0.1.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "schema_version": "call-result.v0.1-proposal", - "event_id": "call-result-001", - "event_type": "call.result", - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", - "tenant_id": "tenant-a", - "tenant_key": "tenant-a", - "trace_id": "trace-v2", - "occurred_at": "2026-09-18T10:10:15+08:00", - "aggregate_type": "call", - "aggregate_id": "call-a", - "aggregate_version": 1, - "payload": { - "source_command_id": "command-a", - "execution_id": "execution-a", - "call_id": "call-a", - "task_id": "task-a", - "task_revision": 1, - "agent_version_id": "version-a", - "route_policy_id": "route-a", - "caller_profile_id": "caller-a", - "callee": "15003164745", - "trunk_id": "trunk-a", - "started_at": "2026-09-18T10:00:00+08:00", - "ended_at": "2026-09-18T10:10:00+08:00", - "duration_ms": 600000, - "outcome": "answered", - "reason_code": null, - "transcript": [ - { - "turn_id": "turn-1", - "segment_id": "segment-1", - "role": "user", - "text": "示例转写内容", - "start_ms": 1000, - "end_ms": 2500 - } - ], - "opt_out": false, - "recording": { - "status": "uploaded", - "recording_id": "recording-a", - "upload_id": "upload-a", - "bucket": "example-bucket", - "object_key": "calls/tenant-a/call-a.wav", - "format": "wav", - "channels": 1, - "sample_rate_hz": 8000, - "duration_ms": 600000, - "size_bytes": 9600000, - "checksum_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - } - } -} diff --git a/docs/contracts/examples/command-next-invalid-control-id-v0.1.json b/docs/contracts/examples/command-next-invalid-control-id-v0.1.json deleted file mode 100644 index 2742835..0000000 --- a/docs/contracts/examples/command-next-invalid-control-id-v0.1.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "schema_version": "command-next.v0.1-proposal", - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", - "tenant_id": "tenant-a", - "tenant_key": "tenant-a", - "trace_id": "trace-v2", - "issued_at": "2026-09-21T00:00:00Z", - "not_after": "2026-09-21T00:00:30Z", - "command_id": "control-must-not-have-id", - "expected_task_revision": 2, - "command_type": "task.control", - "payload": { - "task_id": "task-a", - "action": "pause", - "active_call_policy": "drain", - "reason": "local-test" - } -} diff --git a/docs/contracts/examples/config-read-error-v0.1.json b/docs/contracts/examples/config-read-error-v0.1.json deleted file mode 100644 index 4f4f0a8..0000000 --- a/docs/contracts/examples/config-read-error-v0.1.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "schema_version": "config-read.v0.1", - "resource": "error", - "error": { - "code": "resource_not_found", - "message": "Task is not assigned to this Dispatcher." - } -} diff --git a/docs/contracts/examples/config-read-http-statuses-v0.1.json b/docs/contracts/examples/config-read-http-statuses-v0.1.json deleted file mode 100644 index b768af7..0000000 --- a/docs/contracts/examples/config-read-http-statuses-v0.1.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "fixture_version": "config-read-http-statuses.v0.1", - "responses": [ - { - "status": 400, - "body": { - "schema_version": "config-read.v0.1", - "resource": "error", - "error": { - "code": "invalid_request", - "message": "Request parameters are invalid." - } - } - }, - { - "status": 401, - "body": { - "schema_version": "config-read.v0.1", - "resource": "error", - "error": { - "code": "unauthorized", - "message": "Dispatcher credentials are invalid." - } - } - }, - { - "status": 403, - "body": { - "schema_version": "config-read.v0.1", - "resource": "error", - "error": { - "code": "dispatcher_not_authorized", - "message": "Dispatcher is not authorized for this resource." - } - } - }, - { - "status": 404, - "body": { - "schema_version": "config-read.v0.1", - "resource": "error", - "error": { - "code": "resource_not_found", - "message": "The requested task or configuration resource was not found." - } - } - }, - { - "status": 503, - "body": { - "schema_version": "config-read.v0.1", - "resource": "error", - "error": { - "code": "tenant_quota_unavailable", - "message": "A current tenant quota is unavailable." - } - } - }, - { - "status": 503, - "body": { - "schema_version": "config-read.v0.1", - "resource": "error", - "error": { - "code": "service_unavailable", - "message": "Configuration service is temporarily unavailable." - } - } - } - ] -} diff --git a/docs/contracts/examples/config-read-invalid-extra-property-v0.1.json b/docs/contracts/examples/config-read-invalid-extra-property-v0.1.json deleted file mode 100644 index f9b4158..0000000 --- a/docs/contracts/examples/config-read-invalid-extra-property-v0.1.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "schema_version": "config-read.v0.1", - "resource": "error", - "error": { - "code": "resource_not_found", - "message": "Task is not assigned to this Dispatcher." - }, - "unexpected": true -} diff --git a/docs/contracts/examples/config-read-sip-invalid-artifact-v0.2.json b/docs/contracts/examples/config-read-sip-invalid-artifact-v0.2.json deleted file mode 100644 index 45f4ead..0000000 --- a/docs/contracts/examples/config-read-sip-invalid-artifact-v0.2.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "schema_version": "config-read.v0.2", - "resource": "sip_config", - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", - "revision": 1, - "approved_at": "2026-09-21T08:00:00+08:00", - "artifact": {}, - "trunks": [{ - "trunk_id": "trunk-mock", - "provider_id": "provider-mock", - "egress_pool_id": "egress-mock", - "codec": "PCMA", - "dial_prefix": "", - "enabled": true, - "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": [] - } - } - }] -} diff --git a/docs/contracts/examples/config-read-sip-invalid-egress-v0.3.json b/docs/contracts/examples/config-read-sip-invalid-egress-v0.3.json deleted file mode 100644 index dceccfe..0000000 --- a/docs/contracts/examples/config-read-sip-invalid-egress-v0.3.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "schema_version": "config-read.v0.3", - "resource": "sip_config", - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", - "revision": 1, - "approved_at": "2026-09-21T08:00:00+08:00", - "trunks": [{ - "trunk_id": "trunk-mock", - "provider_id": "provider-mock", - "egress_pool_id": "egress-mock", - "codec": "PCMA", - "dial_prefix": "", - "enabled": true, - "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": [] - } - } - }] -} diff --git a/docs/contracts/examples/config-read-sip-v0.1.json b/docs/contracts/examples/config-read-sip-v0.1.json deleted file mode 100644 index fe905ba..0000000 --- a/docs/contracts/examples/config-read-sip-v0.1.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "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": [] - } - } - }] -} diff --git a/docs/contracts/examples/config-read-sip-v0.2.json b/docs/contracts/examples/config-read-sip-v0.2.json deleted file mode 100644 index 816e702..0000000 --- a/docs/contracts/examples/config-read-sip-v0.2.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "schema_version": "config-read.v0.2", - "resource": "sip_config", - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", - "revision": 1, - "approved_at": "2026-09-21T08:00:00+08:00", - "trunks": [{ - "trunk_id": "trunk-mock", - "provider_id": "provider-mock", - "egress_pool_id": "egress-mock", - "codec": "PCMA", - "dial_prefix": "", - "enabled": true, - "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": [] - } - } - }] -} diff --git a/docs/contracts/examples/config-read-sip-v0.3.json b/docs/contracts/examples/config-read-sip-v0.3.json deleted file mode 100644 index 93060b9..0000000 --- a/docs/contracts/examples/config-read-sip-v0.3.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "schema_version": "config-read.v0.3", - "resource": "sip_config", - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", - "revision": 1, - "approved_at": "2026-09-21T08:00:00+08:00", - "trunks": [{ - "trunk_id": "trunk-mock", - "provider_id": "provider-mock", - "codec": "PCMA", - "dial_prefix": "", - "enabled": true, - "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": [] - } - } - }] -} diff --git a/docs/contracts/examples/config-read-task-v0.1.json b/docs/contracts/examples/config-read-task-v0.1.json deleted file mode 100644 index 9b4fce3..0000000 --- a/docs/contracts/examples/config-read-task-v0.1.json +++ /dev/null @@ -1,88 +0,0 @@ -{ - "schema_version": "config-read.v0.1", - "resource": "task_config", - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", - "tenant_id": "tenant-id-mock", - "tenant_key": "tenant-mock", - "task_id": "task-mock", - "task_revision": 2, - "status": "running", - "name": "Mock task", - "group_id": null, - "max_concurrent_calls": 2, - "ring_timeout_ms": 30000, - "max_call_duration_ms": 120000, - "route_policy_id": "route-mock", - "caller_profile_id": "caller-profile-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", - "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 - } - } - } -} diff --git a/docs/contracts/examples/config-read-tenant-quota-v0.1.json b/docs/contracts/examples/config-read-tenant-quota-v0.1.json deleted file mode 100644 index 29129eb..0000000 --- a/docs/contracts/examples/config-read-tenant-quota-v0.1.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "schema_version": "config-read.v0.1", - "resource": "tenant_quota", - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", - "tenant_id": "tenant-id-mock", - "tenant_key": "tenant-mock", - "quota_revision": 1, - "max_concurrent_calls": 3, - "valid_until": "2026-09-21T18:00:00+08:00" -} diff --git a/docs/contracts/examples/local-mock-recording-failure-expired-v0.1.json b/docs/contracts/examples/local-mock-recording-failure-expired-v0.1.json deleted file mode 100644 index fbe7b98..0000000 --- a/docs/contracts/examples/local-mock-recording-failure-expired-v0.1.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "schema_version": "local-mock-recording-failure.v0.1", - "upload_id": "upload-a", - "recording_id": "recording-a", - "error_code": "upload_authorization_expired" -} diff --git a/docs/contracts/examples/local-mock-recording-failure-invalid-extra-v0.1.json b/docs/contracts/examples/local-mock-recording-failure-invalid-extra-v0.1.json deleted file mode 100644 index a04bd23..0000000 --- a/docs/contracts/examples/local-mock-recording-failure-invalid-extra-v0.1.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "schema_version": "local-mock-recording-failure.v0.1", - "upload_id": "upload-a", - "recording_id": "recording-a", - "error_code": "upload_failed", - "put_url": "https://example.invalid/never-a-real-token" -} diff --git a/docs/contracts/examples/local-mock-recording-failure-invalid-timeout-v0.1.json b/docs/contracts/examples/local-mock-recording-failure-invalid-timeout-v0.1.json deleted file mode 100644 index ec60c25..0000000 --- a/docs/contracts/examples/local-mock-recording-failure-invalid-timeout-v0.1.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "schema_version": "local-mock-recording-failure.v0.1", - "upload_id": "upload-a", - "recording_id": "recording-a", - "error_code": "upload_timeout" -} diff --git a/docs/contracts/examples/local-mock-recording-failure-upload-failed-v0.1.json b/docs/contracts/examples/local-mock-recording-failure-upload-failed-v0.1.json deleted file mode 100644 index 2e9388c..0000000 --- a/docs/contracts/examples/local-mock-recording-failure-upload-failed-v0.1.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "schema_version": "local-mock-recording-failure.v0.1", - "upload_id": "upload-a", - "recording_id": "recording-a", - "error_code": "upload_failed" -} diff --git a/docs/contracts/examples/static-cell-artifact-invalid-egress-v0.2.json b/docs/contracts/examples/static-cell-artifact-invalid-egress-v0.2.json deleted file mode 100644 index bc4c0b2..0000000 --- a/docs/contracts/examples/static-cell-artifact-invalid-egress-v0.2.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "artifact_id": "artifact-cell-a-1", - "source_release": "management-snapshot-1", - "source_digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "approval_reference": "mock-approval-1", - "cell_id": "cell-a", - "revision": 1, - "config_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "mode": "mock", - "trunks": [ - { - "trunk_id": "trunk-mock", - "provider_id": "provider-mock", - "egress_pool_id": "egress-mock", - "codec": "PCMA", - "caller_profile_ids": [ - "caller_profile_test" - ], - "dial_prefix": "7089", - "enabled": true, - "sip_endpoint_ref": "mock-sip-endpoint", - "credential_ref": null, - "media_profile_id": "slin16-16k-pt118" - } - ], - "load_evidence": null, - "allowed_targets": [ - "15003164745", - "15830461047" - ], - "media_profiles": { - "pcma-8k-pt8": { - "format": "alaw", - "sample_rate_hz": 8000, - "channels": 1, - "payload_type": 8 - }, - "slin16-16k-pt118": { - "format": "slin16", - "sample_rate_hz": 16000, - "channels": 1, - "payload_type": 118 - } - } -} diff --git a/docs/contracts/examples/static-cell-artifact-v0.2.json b/docs/contracts/examples/static-cell-artifact-v0.2.json deleted file mode 100644 index df62cb6..0000000 --- a/docs/contracts/examples/static-cell-artifact-v0.2.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "artifact_id": "artifact-cell-a-1", - "source_release": "management-snapshot-1", - "source_digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "approval_reference": "mock-approval-1", - "cell_id": "cell-a", - "revision": 1, - "config_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "mode": "mock", - "trunks": [ - { - "trunk_id": "trunk-mock", - "provider_id": "provider-mock", - "codec": "PCMA", - "caller_profile_ids": [ - "caller_profile_test" - ], - "dial_prefix": "7089", - "enabled": true, - "sip_endpoint_ref": "mock-sip-endpoint", - "credential_ref": null, - "media_profile_id": "slin16-16k-pt118" - } - ], - "load_evidence": null, - "allowed_targets": [ - "15003164745", - "15830461047" - ], - "media_profiles": { - "pcma-8k-pt8": { - "format": "alaw", - "sample_rate_hz": 8000, - "channels": 1, - "payload_type": 8 - }, - "slin16-16k-pt118": { - "format": "slin16", - "sample_rate_hz": 16000, - "channels": 1, - "payload_type": 118 - } - } -} diff --git a/docs/contracts/examples/task-control-invalid-drain-v0.4.json b/docs/contracts/examples/task-control-invalid-drain-v0.4.json deleted file mode 100644 index 6a4d38c..0000000 --- a/docs/contracts/examples/task-control-invalid-drain-v0.4.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "schema_version": "task-control.v0.4-proposal", - "dispatcher_id": "11111111-1111-4111-8111-111111111111", - "tenant_id": "33333333-3333-4333-8333-333333333333", - "tenant_key": "tenant-A", - "trace_id": "44444444-4444-4444-8444-444444444444", - "issued_at": "2026-09-27T01:00:00Z", - "command_type": "task.control", - "payload": {"task_id": "a01", "action": "pause", "active_call_policy": "drain", "reason": "paused by SaaS"} -} diff --git a/docs/contracts/examples/task-control-invalid-expiry-v0.4.json b/docs/contracts/examples/task-control-invalid-expiry-v0.4.json deleted file mode 100644 index 43a6122..0000000 --- a/docs/contracts/examples/task-control-invalid-expiry-v0.4.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "schema_version": "task-control.v0.4-proposal", - "dispatcher_id": "11111111-1111-4111-8111-111111111111", - "tenant_id": "33333333-3333-4333-8333-333333333333", - "tenant_key": "tenant-A", - "trace_id": "44444444-4444-4444-8444-444444444444", - "issued_at": "2026-09-20T01:00:00Z", - "not_after": "2026-09-20T01:05:00Z", - "command_type": "task.control", - "payload": {"task_id": "a01", "action": "stop", "reason": "stopped by SaaS"} -} diff --git a/docs/contracts/examples/task-control-pause-v0.4.json b/docs/contracts/examples/task-control-pause-v0.4.json deleted file mode 100644 index e5ebddc..0000000 --- a/docs/contracts/examples/task-control-pause-v0.4.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "schema_version": "task-control.v0.4-proposal", - "dispatcher_id": "11111111-1111-4111-8111-111111111111", - "tenant_id": "33333333-3333-4333-8333-333333333333", - "tenant_key": "tenant-A", - "trace_id": "44444444-4444-4444-8444-444444444444", - "issued_at": "2026-09-27T01:00:00Z", - "command_type": "task.control", - "payload": {"task_id": "a01", "action": "pause", "reason": "paused by SaaS"} -} diff --git a/docs/contracts/examples/task-control-resume-v0.4.json b/docs/contracts/examples/task-control-resume-v0.4.json deleted file mode 100644 index ab3d8f0..0000000 --- a/docs/contracts/examples/task-control-resume-v0.4.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "schema_version": "task-control.v0.4-proposal", - "dispatcher_id": "11111111-1111-4111-8111-111111111111", - "tenant_id": "33333333-3333-4333-8333-333333333333", - "tenant_key": "tenant-A", - "trace_id": "66666666-6666-4666-8666-666666666666", - "issued_at": "2026-09-27T01:02:00Z", - "command_type": "task.control", - "payload": {"task_id": "a01", "action": "resume", "reason": "resumed by SaaS"} -} diff --git a/docs/contracts/examples/task-control-stop-v0.4.json b/docs/contracts/examples/task-control-stop-v0.4.json deleted file mode 100644 index 6ab6ff6..0000000 --- a/docs/contracts/examples/task-control-stop-v0.4.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "schema_version": "task-control.v0.4-proposal", - "dispatcher_id": "11111111-1111-4111-8111-111111111111", - "tenant_id": "33333333-3333-4333-8333-333333333333", - "tenant_key": "tenant-A", - "trace_id": "55555555-5555-4555-8555-555555555555", - "issued_at": "2026-09-27T01:01:00Z", - "command_type": "task.control", - "payload": {"task_id": "a01", "action": "stop", "reason": "stopped by SaaS"} -} diff --git a/docs/contracts/examples/task-discovery-changes-v0.2.json b/docs/contracts/examples/task-discovery-changes-v0.2.json deleted file mode 100644 index 36b9097..0000000 --- a/docs/contracts/examples/task-discovery-changes-v0.2.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "schema_version": "task-discovery.v0.2-proposal", - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", - "next_cursor": "opaque-watermark-004", - "changes": [ - { - "cursor": "opaque-watermark-002", - "operation": "assigned", - "task_id": "task-new", - "tenant_id": "tenant-id-mock", - "tenant_key": "tenant-mock", - "status": "running", - "task_revision": 1 - }, - { - "cursor": "opaque-watermark-003", - "operation": "updated", - "task_id": "task-mock", - "tenant_id": "tenant-id-mock", - "tenant_key": "tenant-mock", - "status": "stopped", - "task_revision": 3 - }, - { - "cursor": "opaque-watermark-004", - "operation": "removed", - "task_id": "task-old", - "tenant_id": "tenant-id-mock", - "tenant_key": "tenant-mock" - } - ] -} diff --git a/docs/contracts/examples/task-discovery-changes-v0.4.json b/docs/contracts/examples/task-discovery-changes-v0.4.json deleted file mode 100644 index 828072c..0000000 --- a/docs/contracts/examples/task-discovery-changes-v0.4.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "schema_version": "task-discovery.v0.4-proposal", - "mode": "changes", - "dispatcher_id": "11111111-1111-4111-8111-111111111111", - "tasks": [ - {"task_id": "a01", "tenant_id": "tenant-1", "tenant_key": "tenant-A", "status": "removed", "task_revision": 6} - ], - "next_cursor": "43" -} diff --git a/docs/contracts/examples/task-discovery-empty-v0.3.json b/docs/contracts/examples/task-discovery-empty-v0.3.json deleted file mode 100644 index 4807d0f..0000000 --- a/docs/contracts/examples/task-discovery-empty-v0.3.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "schema_version": "task-discovery.v0.3-proposal", - "dispatcher_id": "11111111-1111-4111-8111-111111111111", - "tasks": [], - "next_cursor": "4" -} diff --git a/docs/contracts/examples/task-discovery-error-v0.4.json b/docs/contracts/examples/task-discovery-error-v0.4.json deleted file mode 100644 index 612622e..0000000 --- a/docs/contracts/examples/task-discovery-error-v0.4.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "schema_version": "task-discovery.v0.4-proposal", - "resource": "error", - "error": { - "code": "snapshot_unavailable", - "message": "Snapshot is no longer available; restart full discovery with admission closed" - } -} diff --git a/docs/contracts/examples/task-discovery-http-statuses-v0.1.json b/docs/contracts/examples/task-discovery-http-statuses-v0.1.json deleted file mode 100644 index a2a4168..0000000 --- a/docs/contracts/examples/task-discovery-http-statuses-v0.1.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "fixture_version": "task-discovery-http-statuses.v0.1", - "responses": [ - { - "status": 400, - "body": { - "schema_version": "task-discovery.v0.1-proposal", - "resource": "error", - "error": { - "code": "invalid_cursor", - "message": "The cursor is invalid." - } - } - }, - { - "status": 400, - "body": { - "schema_version": "task-discovery.v0.1-proposal", - "resource": "error", - "error": { - "code": "invalid_page_token", - "message": "The page token is invalid." - } - } - }, - { - "status": 401, - "body": { - "schema_version": "task-discovery.v0.1-proposal", - "resource": "error", - "error": { - "code": "unauthorized", - "message": "Dispatcher credentials are invalid." - } - } - }, - { - "status": 403, - "body": { - "schema_version": "task-discovery.v0.1-proposal", - "resource": "error", - "error": { - "code": "dispatcher_not_authorized", - "message": "Dispatcher is not authorized for this resource." - } - } - }, - { - "status": 410, - "body": { - "schema_version": "task-discovery.v0.1-proposal", - "resource": "error", - "error": { - "code": "cursor_expired", - "message": "The cursor expired; a full snapshot is required." - } - } - }, - { - "status": 410, - "body": { - "schema_version": "task-discovery.v0.1-proposal", - "resource": "error", - "error": { - "code": "snapshot_expired", - "message": "The snapshot expired; a full snapshot is required." - } - } - }, - { - "status": 503, - "body": { - "schema_version": "task-discovery.v0.1-proposal", - "resource": "error", - "error": { - "code": "service_unavailable", - "message": "Task discovery is temporarily unavailable." - } - } - } - ] -} diff --git a/docs/contracts/examples/task-discovery-http-statuses-v0.2.json b/docs/contracts/examples/task-discovery-http-statuses-v0.2.json deleted file mode 100644 index e444aea..0000000 --- a/docs/contracts/examples/task-discovery-http-statuses-v0.2.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "fixture_version": "task-discovery-http-statuses.v0.2", - "responses": [ - {"status": 400, "body": {"schema_version": "task-discovery.v0.2-proposal", "resource": "error", "error": {"code": "invalid_cursor", "message": "The cursor is invalid."}}}, - {"status": 401, "body": {"schema_version": "task-discovery.v0.2-proposal", "resource": "error", "error": {"code": "unauthorized", "message": "Dispatcher credentials are invalid."}}}, - {"status": 403, "body": {"schema_version": "task-discovery.v0.2-proposal", "resource": "error", "error": {"code": "dispatcher_not_authorized", "message": "Dispatcher is not authorized for this resource."}}}, - {"status": 410, "body": {"schema_version": "task-discovery.v0.2-proposal", "resource": "error", "error": {"code": "cursor_expired", "message": "A full task snapshot is required."}}}, - {"status": 503, "body": {"schema_version": "task-discovery.v0.2-proposal", "resource": "error", "error": {"code": "service_unavailable", "message": "Task discovery is temporarily unavailable."}}} - ] -} diff --git a/docs/contracts/examples/task-discovery-http-statuses-v0.3.json b/docs/contracts/examples/task-discovery-http-statuses-v0.3.json deleted file mode 100644 index 9a1eee6..0000000 --- a/docs/contracts/examples/task-discovery-http-statuses-v0.3.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "fixture_version": "task-discovery-http-statuses.v0.3", - "responses": [ - {"status": 400, "body": {"schema_version": "task-discovery.v0.3-proposal", "resource": "error", "error": {"code": "invalid_cursor", "message": "Unknown or malformed event cursor"}}}, - {"status": 401, "body": {"schema_version": "task-discovery.v0.3-proposal", "resource": "error", "error": {"code": "unauthorized", "message": "Invalid Dispatcher credentials"}}}, - {"status": 403, "body": {"schema_version": "task-discovery.v0.3-proposal", "resource": "error", "error": {"code": "dispatcher_not_authorized", "message": "Dispatcher is not assigned these tasks"}}}, - {"status": 503, "body": {"schema_version": "task-discovery.v0.3-proposal", "resource": "error", "error": {"code": "service_unavailable", "message": "Event state is temporarily unavailable"}}} - ] -} diff --git a/docs/contracts/examples/task-discovery-invalid-changes-v0.3.json b/docs/contracts/examples/task-discovery-invalid-changes-v0.3.json deleted file mode 100644 index 96f50be..0000000 --- a/docs/contracts/examples/task-discovery-invalid-changes-v0.3.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "schema_version": "task-discovery.v0.3-proposal", - "dispatcher_id": "11111111-1111-4111-8111-111111111111", - "tasks": [], - "next_cursor": "2", - "changes": [] -} diff --git a/docs/contracts/examples/task-discovery-invalid-cursor-v0.3.json b/docs/contracts/examples/task-discovery-invalid-cursor-v0.3.json deleted file mode 100644 index 6b19f0d..0000000 --- a/docs/contracts/examples/task-discovery-invalid-cursor-v0.3.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "schema_version": "task-discovery.v0.3-proposal", - "dispatcher_id": "11111111-1111-4111-8111-111111111111", - "tasks": [], - "next_cursor": "04" -} diff --git a/docs/contracts/examples/task-discovery-invalid-page-token-v0.2.json b/docs/contracts/examples/task-discovery-invalid-page-token-v0.2.json deleted file mode 100644 index 19ec138..0000000 --- a/docs/contracts/examples/task-discovery-invalid-page-token-v0.2.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "schema_version": "task-discovery.v0.2-proposal", - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", - "next_cursor": "opaque-watermark-004", - "changes": [], - "next_page_token": "obsolete" -} diff --git a/docs/contracts/examples/task-discovery-invalid-queue-property-v0.1.json b/docs/contracts/examples/task-discovery-invalid-queue-property-v0.1.json deleted file mode 100644 index c15aaf6..0000000 --- a/docs/contracts/examples/task-discovery-invalid-queue-property-v0.1.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "schema_version": "task-discovery.v0.1-proposal", - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", - "mode": "snapshot", - "snapshot_id": "snapshot-1042", - "cursor": "1042", - "tasks": [ - { - "task_id": "task-a", - "tenant_id": "tenant-a", - "tenant_key": "tenant-a", - "status": "running", - "task_revision": 1, - "queue": { - "exchange": "agent-call.dispatchers.v3", - "routing_key": "d.c046b893-8628-4589-ae50-619d049248a6.task.task-a.in", - "binding_key": "d.c046b893-8628-4589-ae50-619d049248a6.task.task-a.in", - "queue_name": "agent-call.d.c046b893-8628-4589-ae50-619d049248a6.task.task-a.v3", - "unexpected": true - } - } - ], - "next_page_token": null -} diff --git a/docs/contracts/examples/task-discovery-invalid-queue-v0.2.json b/docs/contracts/examples/task-discovery-invalid-queue-v0.2.json deleted file mode 100644 index ea31bdf..0000000 --- a/docs/contracts/examples/task-discovery-invalid-queue-v0.2.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "schema_version": "task-discovery.v0.2-proposal", - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", - "cursor": "opaque-watermark-001", - "tasks": [{ - "task_id": "task-mock", - "tenant_id": "tenant-id-mock", - "tenant_key": "tenant-mock", - "status": "running", - "task_revision": 2, - "queue": {"queue_name": "not-allowed-in-response"} - }] -} diff --git a/docs/contracts/examples/task-discovery-invalid-v0.4.json b/docs/contracts/examples/task-discovery-invalid-v0.4.json deleted file mode 100644 index 7ba7168..0000000 --- a/docs/contracts/examples/task-discovery-invalid-v0.4.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "schema_version": "task-discovery.v0.4-proposal", - "mode": "snapshot", - "dispatcher_id": "11111111-1111-4111-8111-111111111111", - "snapshot_id": "22222222-2222-4222-8222-222222222222", - "watermark": "42", - "tasks": [], - "next_page_token": null, - "unexpected_unapproved_field": true -} diff --git a/docs/contracts/examples/task-discovery-no-change-v0.2.json b/docs/contracts/examples/task-discovery-no-change-v0.2.json deleted file mode 100644 index 2c5d74d..0000000 --- a/docs/contracts/examples/task-discovery-no-change-v0.2.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "schema_version": "task-discovery.v0.2-proposal", - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", - "next_cursor": "opaque-watermark-004", - "changes": [] -} diff --git a/docs/contracts/examples/task-discovery-page-v0.3.json b/docs/contracts/examples/task-discovery-page-v0.3.json deleted file mode 100644 index e3f297d..0000000 --- a/docs/contracts/examples/task-discovery-page-v0.3.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "schema_version": "task-discovery.v0.3-proposal", - "dispatcher_id": "11111111-1111-4111-8111-111111111111", - "tasks": [ - {"task_id": "a01", "tenant_id": "tenant-1", "tenant_key": "tenant-A", "status": "running", "task_revision": 1}, - {"task_id": "a02", "tenant_id": "tenant-1", "tenant_key": "tenant-A", "status": "paused", "task_revision": 1} - ], - "next_cursor": "2" -} diff --git a/docs/contracts/examples/task-discovery-removed-v0.3.json b/docs/contracts/examples/task-discovery-removed-v0.3.json deleted file mode 100644 index 92fdfbb..0000000 --- a/docs/contracts/examples/task-discovery-removed-v0.3.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "schema_version": "task-discovery.v0.3-proposal", - "dispatcher_id": "11111111-1111-4111-8111-111111111111", - "tasks": [ - {"task_id": "a02", "tenant_id": "tenant-1", "tenant_key": "tenant-A", "status": "removed", "task_revision": 2} - ], - "next_cursor": "4" -} diff --git a/docs/contracts/examples/task-discovery-snapshot-page1-v0.4.json b/docs/contracts/examples/task-discovery-snapshot-page1-v0.4.json deleted file mode 100644 index 9238cdb..0000000 --- a/docs/contracts/examples/task-discovery-snapshot-page1-v0.4.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "schema_version": "task-discovery.v0.4-proposal", - "mode": "snapshot", - "dispatcher_id": "11111111-1111-4111-8111-111111111111", - "snapshot_id": "22222222-2222-4222-8222-222222222222", - "watermark": "42", - "tasks": [ - {"task_id": "a01", "tenant_id": "tenant-1", "tenant_key": "tenant-A", "status": "running", "task_revision": 5} - ], - "next_page_token": "page-2" -} diff --git a/docs/contracts/examples/task-discovery-snapshot-page2-v0.4.json b/docs/contracts/examples/task-discovery-snapshot-page2-v0.4.json deleted file mode 100644 index 4cda94f..0000000 --- a/docs/contracts/examples/task-discovery-snapshot-page2-v0.4.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "schema_version": "task-discovery.v0.4-proposal", - "mode": "snapshot", - "dispatcher_id": "11111111-1111-4111-8111-111111111111", - "snapshot_id": "22222222-2222-4222-8222-222222222222", - "watermark": "42", - "tasks": [ - {"task_id": "a02", "tenant_id": "tenant-1", "tenant_key": "tenant-A", "status": "paused", "task_revision": 3} - ], - "next_page_token": null -} diff --git a/docs/contracts/examples/task-discovery-snapshot-v0.1.json b/docs/contracts/examples/task-discovery-snapshot-v0.1.json deleted file mode 100644 index d56d2c8..0000000 --- a/docs/contracts/examples/task-discovery-snapshot-v0.1.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "schema_version": "task-discovery.v0.1-proposal", - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", - "mode": "snapshot", - "snapshot_id": "snapshot-001", - "cursor": "change-watermark-001", - "tasks": [ - { - "task_id": "task-mock", - "tenant_id": "tenant-id-mock", - "tenant_key": "tenant-mock", - "status": "running", - "task_revision": 2, - "queue": { - "exchange": "agent-call.dispatchers.v3", - "routing_key": "d.c046b893-8628-4589-ae50-619d049248a6.task.task-mock.in", - "binding_key": "d.c046b893-8628-4589-ae50-619d049248a6.task.task-mock.in", - "queue_name": "agent-call.d.c046b893-8628-4589-ae50-619d049248a6.task.task-mock.v3" - } - } - ], - "next_page_token": null -} diff --git a/docs/contracts/examples/task-discovery-snapshot-v0.2.json b/docs/contracts/examples/task-discovery-snapshot-v0.2.json deleted file mode 100644 index 4b16812..0000000 --- a/docs/contracts/examples/task-discovery-snapshot-v0.2.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "schema_version": "task-discovery.v0.2-proposal", - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", - "cursor": "opaque-watermark-001", - "tasks": [ - { - "task_id": "task-mock", - "tenant_id": "tenant-id-mock", - "tenant_key": "tenant-mock", - "status": "running", - "task_revision": 2 - } - ] -} diff --git a/docs/contracts/examples/task-discovery-updated-v0.3.json b/docs/contracts/examples/task-discovery-updated-v0.3.json deleted file mode 100644 index c0e2158..0000000 --- a/docs/contracts/examples/task-discovery-updated-v0.3.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "schema_version": "task-discovery.v0.3-proposal", - "dispatcher_id": "11111111-1111-4111-8111-111111111111", - "tasks": [ - {"task_id": "a01", "tenant_id": "tenant-1", "tenant_key": "tenant-A", "status": "paused", "task_revision": 1} - ], - "next_cursor": "3" -} diff --git a/docs/contracts/local-contract-manifest-v0.1.json b/docs/contracts/local-contract-manifest-v0.1.json deleted file mode 100644 index 977bf52..0000000 --- a/docs/contracts/local-contract-manifest-v0.1.json +++ /dev/null @@ -1,84 +0,0 @@ -{ - "manifest_version": "local-contract-manifest.v0.1", - "hash_algorithm": "SHA-256", - "scope": "Project-local F01/F07 contracts; not external SaaS or management acceptance.", - "source": { - "path": "docs/thirds/第三方对接事件与请求消费顺序_v0.1.md", - "sha256": "788c36a86f3d5bc34639db5ac42b7c7f565169c696c0901737c240ee0da89411" - }, - "artifacts": [ - { - "path": "docs/contracts/call-result-v0.1-proposal.schema.json", - "sha256": "8068508cfd05e35d06b4c1c06bee826104be7d176fd07b6822714704d321bf35" - }, - { - "path": "docs/contracts/command-next-v0.1-proposal.schema.json", - "sha256": "fcd3ec1d56baa69a22fac76363a533e252658fb3b7a4fe7020f4322d965716de" - }, - { - "path": "docs/contracts/config-read-fields-v0.1-proposal.md", - "sha256": "28dcf3ce7be73b2b8f6b9e2dddbe242b2d8ea02d61b2671a5bf0d62eb40c7ab3" - }, - { - "path": "docs/contracts/config-read-v0.1.schema.json", - "sha256": "043dd26a9033b7fd6401c2fc918a1fec2ecbaac0bd59cbba6da72d02125b8184" - }, - { - "path": "docs/contracts/examples/call-result-invalid-missing-checksum-v0.1.json", - "sha256": "9117dc78b53513e82f059816aea07b97d8980f3f8fc5b8ae02939450dedf09ac" - }, - { - "path": "docs/contracts/examples/call-result-uploaded-v0.1.json", - "sha256": "616c3f9e6b1ce77f98f365bc398e53177ce3dd50148db61327b4277bed6edf17" - }, - { - "path": "docs/contracts/examples/command-next-invalid-control-id-v0.1.json", - "sha256": "6c4fa5ff9e2992ac4c0a18357e16c2bfb4ae8b580b2afefa5cc219725059f5ce" - }, - { - "path": "docs/contracts/examples/config-read-error-v0.1.json", - "sha256": "90e95ab65820baa15ee44fafb7b8bdef3a09b62fba27ef178e4519467dafcfc3" - }, - { - "path": "docs/contracts/examples/config-read-http-statuses-v0.1.json", - "sha256": "2918407fbe722bffee4cbb638c4581f2f3530a8a2639a8c74085ed8ad95d2294" - }, - { - "path": "docs/contracts/examples/config-read-invalid-extra-property-v0.1.json", - "sha256": "6ad2d17767b22e28a47a88149a590e1a22b5f0f8ed036acdc6ab42f779def319" - }, - { - "path": "docs/contracts/examples/config-read-sip-v0.1.json", - "sha256": "7ff720634d3e190d91df44e9eaf3548be8c302be3b81d3d33f16b32ad0b034d4" - }, - { - "path": "docs/contracts/examples/config-read-task-v0.1.json", - "sha256": "571c1fe3c9e18108bf23b6053929ee23b12ff2f611b7194f38fb63e84003bd1c" - }, - { - "path": "docs/contracts/examples/config-read-tenant-quota-v0.1.json", - "sha256": "b95e06550a920238e9160b5bda8e305abcf0b5feef3db43f8a25b5e1b568e87d" - }, - { - "path": "docs/contracts/examples/task-discovery-http-statuses-v0.1.json", - "sha256": "67b80bd351373ad95b56e0200951b5aaebf24bbf7d58f2bbc826fc4e68fd2472" - }, - { - "path": "docs/contracts/examples/task-discovery-invalid-queue-property-v0.1.json", - "sha256": "bee48e2edddab074652bcaf8a81ca55770a725282add0ae13e458a9d31df94ef" - }, - { - "path": "docs/contracts/examples/task-discovery-snapshot-v0.1.json", - "sha256": "1b2389e58ea025097c38aaed72cf0f322c4e427d51f04b380d3e4c8173e9b2b5" - }, - { - "path": "docs/contracts/mq-topology-v0.1-proposal.json", - "sha256": "20c0f69057e283df81823f7ff333e2c1cc7d756a68c10a22fc7d50ad793d53c4" - }, - { - "path": "docs/contracts/task-discovery-v0.1-proposal.schema.json", - "sha256": "ff0d5e292272bd66654af91a5a3927c736a550e5766029f4c00c552125743b8e" - } - ], - "note": "Hashes cover the exact raw bytes of source and artifacts. This manifest does not hash itself." -} diff --git a/docs/contracts/local-contract-manifest-v0.2.json b/docs/contracts/local-contract-manifest-v0.2.json deleted file mode 100644 index af8d841..0000000 --- a/docs/contracts/local-contract-manifest-v0.2.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "manifest_version": "local-contract-manifest.v0.2", - "hash_algorithm": "SHA-256", - "source": {"path": "docs/thirds/v0.2.md", "sha256": "446a8726a8ce86f3a5910e9bc2f004a5142934ed7e5bc8146c075f682337d16e"}, - "artifacts": [ - {"path": "docs/contracts/config-read-v0.1.schema.json", "sha256": "043dd26a9033b7fd6401c2fc918a1fec2ecbaac0bd59cbba6da72d02125b8184"}, - {"path": "docs/contracts/command-next-v0.1-proposal.schema.json", "sha256": "fcd3ec1d56baa69a22fac76363a533e252658fb3b7a4fe7020f4322d965716de"}, - {"path": "docs/contracts/call-result-v0.1-proposal.schema.json", "sha256": "8068508cfd05e35d06b4c1c06bee826104be7d176fd07b6822714704d321bf35"}, - {"path": "docs/contracts/mq-topology-v0.1-proposal.json", "sha256": "20c0f69057e283df81823f7ff333e2c1cc7d756a68c10a22fc7d50ad793d53c4"}, - {"path": "docs/contracts/task-discovery-v0.2-proposal.schema.json", "sha256": "aef9fa5c4d7e37edda5d6f0bd8fdbc4aac37c09b6fc8f38a3bc52ca544c52c34"}, - {"path": "docs/contracts/examples/task-discovery-snapshot-v0.2.json", "sha256": "1640a04a75dfe53f5f221a7be22ee4f55e3501f029948fb27e50ef5efb72592d"}, - {"path": "docs/contracts/examples/task-discovery-changes-v0.2.json", "sha256": "58bb5743fdd35b502a643422d302284a2f6060b07dc9a4d4700df70b089a62e3"}, - {"path": "docs/contracts/examples/task-discovery-no-change-v0.2.json", "sha256": "45abb87f7e8c0ea1efa58a94f15e0da7ceef2a7f10ed7cc74ddb88abcee24177"}, - {"path": "docs/contracts/examples/task-discovery-http-statuses-v0.2.json", "sha256": "e72979c2de0951fcd58ee8914b70a7133625365497c94503161a52751ba19710"}, - {"path": "docs/contracts/examples/task-discovery-invalid-queue-v0.2.json", "sha256": "44bc481f9a8022b2d5172b5052cf937dc44038363e6303feb32fa76ab2b60b92"}, - {"path": "docs/contracts/examples/task-discovery-invalid-page-token-v0.2.json", "sha256": "e370ba50597efa4c129a4ba6d87288189343b901e1233699616f657a82a809d6"} - ] -} diff --git a/docs/contracts/local-contract-manifest-v0.3.json b/docs/contracts/local-contract-manifest-v0.3.json deleted file mode 100644 index 07b7d71..0000000 --- a/docs/contracts/local-contract-manifest-v0.3.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "manifest_version": "local-contract-manifest.v0.3", - "hash_algorithm": "SHA-256", - "source": {"path": "docs/thirds/v0.3.md", "sha256": "9a22e9ab5da7d859b5fde4b77cb50ddc148e39567704609d633b91a529af564d"}, - "artifacts": [ - {"path": "docs/contracts/task-discovery-v0.3-proposal.schema.json", "sha256": "da8eda2e8f5416b2fb35f68e09a98f37d1417a878e9271b8e6d0d6824b94814c"}, - {"path": "docs/contracts/examples/task-discovery-empty-v0.3.json", "sha256": "4b1b03b75662d52b013cb080ceb13160f7811ab4396f07e908dcf684cf06f032"}, - {"path": "docs/contracts/examples/task-discovery-http-statuses-v0.3.json", "sha256": "520caf658630055e1354a466767cf4ca6cbc887e6bc00e7182a79b0fa2a925ef"}, - {"path": "docs/contracts/examples/task-discovery-invalid-changes-v0.3.json", "sha256": "fdddeae103e0107156ea49bef825226d1be0782a1d4a13cfb6d839522923ab0d"}, - {"path": "docs/contracts/examples/task-discovery-invalid-cursor-v0.3.json", "sha256": "a5645ce382388d289cfda043975793a6b0e0be69befd9adb2182ab8db5a7ae5d"}, - {"path": "docs/contracts/examples/task-discovery-page-v0.3.json", "sha256": "4ede6ebe75b93c8687f625be05bbedb61471c43303d57aef6ed4b8cbb0fabb44"}, - {"path": "docs/contracts/examples/task-discovery-removed-v0.3.json", "sha256": "be1d9d853ad8a7c4ba271821932f16856daf1c63723682bc656ec18721da33d1"}, - {"path": "docs/contracts/examples/task-discovery-updated-v0.3.json", "sha256": "76aa5528f4c9362301492282151e7ddd362590ebea096ffef7b82131e9c99435"}, - {"path": "docs/contracts/config-read-v0.1.schema.json", "sha256": "043dd26a9033b7fd6401c2fc918a1fec2ecbaac0bd59cbba6da72d02125b8184"}, - {"path": "docs/contracts/command-next-v0.1-proposal.schema.json", "sha256": "fcd3ec1d56baa69a22fac76363a533e252658fb3b7a4fe7020f4322d965716de"}, - {"path": "docs/contracts/call-result-v0.1-proposal.schema.json", "sha256": "8068508cfd05e35d06b4c1c06bee826104be7d176fd07b6822714704d321bf35"}, - {"path": "docs/contracts/mq-topology-v0.1-proposal.json", "sha256": "20c0f69057e283df81823f7ff333e2c1cc7d756a68c10a22fc7d50ad793d53c4"} - ] -} diff --git a/docs/contracts/local-contract-manifest-v0.4.json b/docs/contracts/local-contract-manifest-v0.4.json deleted file mode 100644 index f34f12f..0000000 --- a/docs/contracts/local-contract-manifest-v0.4.json +++ /dev/null @@ -1,83 +0,0 @@ -{ - "manifest_version": "local-contract-manifest.v0.4", - "hash_algorithm": "SHA-256", - "status": "project-local proposal; external SaaS compatibility unverified", - "source": { - "path": "docs/thirds/v0.4.md", - "sha256": "08dcf39aabf77cd4cbf9dd2c3d7beaa691134c91354004e4ff59f1b0dbb86556" - }, - "artifacts": [ - { - "path": "docs/contracts/task-discovery-v0.4-proposal.schema.json", - "sha256": "e72eb5da004185d975c05cd4e2ca63314793127bbe07d8738a99f5ecad1d1f13" - }, - { - "path": "docs/contracts/task-control-v0.4-proposal.schema.json", - "sha256": "5aa94f799da190a3e97c2fa4e911980ceac52662322e85bc2f57eecb5b21de3c" - }, - { - "path": "contracts/upstream/v1/mq.schema.json", - "sha256": "d566899abbc8f0f8404cfe79ea58b0d1e7bf7e9fc663b09b282c15d5c74f505d" - }, - { - "path": "docs/contracts/examples/task-discovery-snapshot-page1-v0.4.json", - "sha256": "5085883ed0fd591644de091b944acd9d725714b6d5c48247b0428221ebc33867" - }, - { - "path": "docs/contracts/examples/task-discovery-snapshot-page2-v0.4.json", - "sha256": "064243f15bc33530d459f71c08f6b9d00fc776151ecaf4be4dc9a8582f5f993a" - }, - { - "path": "docs/contracts/examples/task-discovery-changes-v0.4.json", - "sha256": "f3afc04d6cae564d31c9165e889e99c109f92c7e1bdbb3853421321963c30b6e" - }, - { - "path": "docs/contracts/examples/task-discovery-error-v0.4.json", - "sha256": "71dd9986b49f56db792348443cf8595d7743447783129daf302d4550e73d5e96" - }, - { - "path": "docs/contracts/examples/task-discovery-invalid-v0.4.json", - "sha256": "ff840c254f7abb1f6a2d9d472f10bda9c646f2ee6cfd485da1d4b4c04018314c" - }, - { - "path": "docs/contracts/examples/task-control-pause-v0.4.json", - "sha256": "0adbeaed2517c33bd5b5b2e4e72c0c663263c4f32858b807f5f70190ad98a543" - }, - { - "path": "docs/contracts/examples/task-control-resume-v0.4.json", - "sha256": "95c1cb22147c79751f09310fcade441835d02893bb6dc337fc49a1ab5e870358" - }, - { - "path": "docs/contracts/examples/task-control-stop-v0.4.json", - "sha256": "d84b2be049bf9348fd67446f3e9a075df2f248777b81ed76153bbef4f4894d4d" - }, - { - "path": "docs/contracts/examples/task-control-invalid-drain-v0.4.json", - "sha256": "36e6d1bee9f2255158696fb61d9754e968dafe1927145df56412ea8d23e46b39" - }, - { - "path": "docs/contracts/call-execute-v0.4-proposal.schema.json", - "sha256": "0e019e1775810bfc0616683f5dd6bd49d9d97de92b9c063c56966a894d2461ed" - }, - { - "path": "docs/contracts/examples/call-execute-altcallee-v0.4.json", - "sha256": "1c58585f9e30745b4ca21c029f22f416931ad40a00e24e6c8389f113e964435c" - }, - { - "path": "docs/contracts/examples/call-execute-invalid-expiry-v0.4.json", - "sha256": "b332703b0113f64e1f8bb77bac2ee312d9a22d6d4a69214784f557381137edc7" - }, - { - "path": "docs/contracts/examples/call-execute-old-v0.4.json", - "sha256": "8e2285c5dada3ee3e5511d5a687577142e6bdcf034da789a4a8b4001e3127327" - }, - { - "path": "docs/contracts/examples/call-execute-recent-v0.4.json", - "sha256": "d03f5f4b04fde957e21ac300246098286ff6260c45b68a192bacb658af4e8cde" - }, - { - "path": "docs/contracts/examples/task-control-invalid-expiry-v0.4.json", - "sha256": "614fda38465be236224858d3706e304a76dd651b905f8b84347492b68e96755e" - } - ] -} diff --git a/docs/contracts/local-contract-manifest-v0.5.json b/docs/contracts/local-contract-manifest-v0.5.json deleted file mode 100644 index 0a09fba..0000000 --- a/docs/contracts/local-contract-manifest-v0.5.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "manifest_version": "local-contract-manifest.v0.5", - "hash_algorithm": "SHA-256", - "source": {"path": "docs/contracts/config-read-fields-v0.2-proposal.md", "sha256": "037dc0540d7f89f2f13624239989e2e2ffb2b5eab2770e7c46f37a51eae41afa"}, - "artifacts": [ - {"path": "docs/thirds/v0.2.md", "sha256": "446a8726a8ce86f3a5910e9bc2f004a5142934ed7e5bc8146c075f682337d16e"}, - {"path": "docs/contracts/config-read-v0.2.schema.json", "sha256": "e91809bc90c1913ed094d5a275df03c6dfb0bdaf54f624b8633780c269935809"}, - {"path": "docs/contracts/examples/config-read-sip-v0.2.json", "sha256": "b7a159e70b882eaab7a83f36d969627176e0be1a7f0b0d58e2f4ce8448860790"}, - {"path": "docs/contracts/examples/config-read-sip-invalid-artifact-v0.2.json", "sha256": "403370acfceeda428873600adabc759f90faed2bfc766380ece3bd998bc90980"} - ] -} diff --git a/docs/contracts/local-contract-manifest-v0.6.json b/docs/contracts/local-contract-manifest-v0.6.json deleted file mode 100644 index e6e35ce..0000000 --- a/docs/contracts/local-contract-manifest-v0.6.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "manifest_version": "local-contract-manifest.v0.6", - "hash_algorithm": "SHA-256", - "source": {"path": "docs/contracts/config-read-fields-v0.3-proposal.md", "sha256": "12539253327218dfccf585d4bbf473165542c581385b901488bfe7160354e2ed"}, - "artifacts": [ - {"path": "docs/contracts/config-read-v0.3.schema.json", "sha256": "df7c0c4e77d102a35793b8aa5a17326d35a2bd7ced04be6ef50bb9d52df64036"}, - {"path": "docs/contracts/examples/config-read-sip-v0.3.json", "sha256": "b45d56a4d550f05fba7d222b51e85528b7d8ad465e1dca5f86375676fa0cc8b9"}, - {"path": "docs/contracts/examples/config-read-sip-invalid-egress-v0.3.json", "sha256": "418802ad781454b1f72ae032bfb0310e0b67927d1a407627564e8fa4fa19f94d"}, - {"path": "docs/contracts/ai-authorization-v0.2.schema.json", "sha256": "8e5da3c374857518f9589e62e5bfd08a58328820d29204acf441ccd0eaecbac6"}, - {"path": "docs/contracts/examples/ai-authorization-v0.2.json", "sha256": "85528d6279c0057269b34a9b7dfa748c27f0ef85379a2dd934df68d62cc8fc28"}, - {"path": "docs/contracts/examples/ai-authorization-invalid-egress-v0.2.json", "sha256": "2aacf5b3a8afbc5c457ad008323559f1d51ce31c85741631481b46c46118168d"}, - {"path": "docs/contracts/static-cell-artifact-v0.2.schema.json", "sha256": "72029d19309b719fa88271ea0f3fa0ac86240cad8288852d2ef9a1a3071769eb"}, - {"path": "docs/contracts/examples/static-cell-artifact-v0.2.json", "sha256": "abe9585a45039c4af55c695e542a38e659be6f7247274ef51fb8e8f255bfb87e"}, - {"path": "docs/contracts/examples/static-cell-artifact-invalid-egress-v0.2.json", "sha256": "129821e4de654ef12d3e7f155e9301ecddf798f9ca3399316a20a55273fbc61f"} - ] -} diff --git a/docs/contracts/local-mock-recording-failure-manifest-v0.1.json b/docs/contracts/local-mock-recording-failure-manifest-v0.1.json deleted file mode 100644 index 9046ab6..0000000 --- a/docs/contracts/local-mock-recording-failure-manifest-v0.1.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "manifest_version": "local-mock-recording-failure-manifest.v0.1", - "hash_algorithm": "SHA-256", - "source": { - "path": "docs/contracts/local-mock-recording-failure-v0.1.md", - "sha256": "741e58d1eda31a0bc88d14bf0aae34daa824e129cc252b4cda99200f71e1ecf4" - }, - "artifacts": [ - { - "path": "docs/contracts/local-mock-recording-failure-v0.1.schema.json", - "sha256": "3f0b58aa8b0047282d9bd9215cfc9220aaa4cc5635636e13041ab58d16fa7c87" - }, - { - "path": "docs/contracts/examples/local-mock-recording-failure-expired-v0.1.json", - "sha256": "18f48df76ef633e33dfcf1564d69a85b4a2608b00df81ad8fcf46fd4b843ee54" - }, - { - "path": "docs/contracts/examples/local-mock-recording-failure-invalid-extra-v0.1.json", - "sha256": "9321796140b42b51677fd6d24903fd9510ff37e05f6e36551b5551b7c9dc7eff" - }, - { - "path": "docs/contracts/examples/local-mock-recording-failure-invalid-timeout-v0.1.json", - "sha256": "6eb0ebf2e4042e3f059f83d430d68fcd89cf34cf36568e47050133730a80a094" - }, - { - "path": "docs/contracts/examples/local-mock-recording-failure-upload-failed-v0.1.json", - "sha256": "cd1028f02ac66fa9518c026a260b0cceec9a1016c4be5afab4d174d910f3746c" - } - ] -} diff --git a/docs/contracts/local-mock-recording-failure-v0.1.md b/docs/contracts/local-mock-recording-failure-v0.1.md deleted file mode 100644 index f97211c..0000000 --- a/docs/contracts/local-mock-recording-failure-v0.1.md +++ /dev/null @@ -1,17 +0,0 @@ -# Local Mock 录音失败事实 v0.1(项目内合同) - -仅供单节点 P1 本地 Mock 验证;用户已批准此内部事实,不代表 SaaS/management、mixed/real 或生产协议已签收。SaaS 的 v0.1 合同及 `contracts/upstream/v1/` 不变。 - -## 消息与身份 - -- 复用同一 mTLS AgentControl listener 的 `ReportExecutionEvent`,`ExecutionFact.kind=FACT_KIND_RECORDING_PROGRESS`;Mock V3 拒绝其它旧分散执行事件。`payload_json` 必须满足 `local-mock-recording-failure-v0.1.schema.json`,编码后最多 4096 字节;不得包含上传 TOKEN、签名 URL、凭据、音频、完整对话或供应商响应正文。 -- `fact_id` 为一次生成、写入 Agent 文件日志的 UUID v4;`content_sha256` 是原始 `payload_json` 字节的 SHA-256。`source_boot_id`、`source_sequence`(正整数)及观察时间和事实一起持久化。重启后不重建事实身份或时间;每次请求的 `RequestMeta` 使用**当前已激活**的 Agent/Cell/boot/Dispatcher epoch/session generation。旧 `source_boot_id` 可与新请求的 boot 不同,不能因此丢弃原事实;无当前会话则保留事实、拒绝上报。 -- Dispatcher 先核验 mTLS、允许的 Agent、当前未过期节点会话、已发唯一外呼决定及已确认结束的 Mock 通话,再核对任务绑定、`recording_id` 与 `upload_id`。签收前将事实身份与 `unavailable` 结果在同一 SQLite 事务持久化,并使唯一 `call.result` 可靠入 outbox;途中失败由原事实/结果恢复,不重拨、不重 PUT。相同事实幂等,不同事实或矛盾状态拒绝。事实签收不等于 MQ publisher confirm,更不等于 SaaS 应用签收。 - -## 失败与期限 - -- Agent 仅上报明确失败:授权无效/过期、已确认的 PUT 拒绝、录音文件明确缺失或已证实的 checksum 不匹配。HTTP 401/403 → `upload_authorization_failed`,明确的其它 4xx(不含 408/429)→ `upload_failed`;本地授权过期 → `upload_authorization_expired`,checksum 不匹配 → `checksum_mismatch`。发送前先持久记录;报告失败或回执不匹配只重送同一事实,不重 PUT。 -- 传输未知、HTTP 408/429/5xx 不冒称明确失败;保留未知占用/录音等待,超过通话结束后 15 分钟由 Dispatcher 以 `upload_timeout` 收口。首次到达截止点后的新失败事实拒绝;截止点前已持久化的同一事实在重启后仍可幂等重送。`upload_timeout` 与 `deadline_exceeded` 只由 Dispatcher 生成,不允许 Agent 上报。 -- 通话确认结束即释放执行额度,不等待上传或 MQ;未知通话仍占用。外发只有一份最终 `call.result`,不得并行生成 `recording.uploaded` 或旧分散通话事件。8,388,608 字节以上的最终消息完整保留、持久阻塞并记录 event_id、字节数和 SHA-256,不截断或拆分。 - -当前默认 Mock originator 只模拟无应答,不生成真实录音;已上传及明确失败由隔离 Mock 测试注入,不据此宣称真实 OSS、供应商或 SaaS 验证通过。 diff --git a/docs/contracts/local-mock-recording-failure-v0.1.schema.json b/docs/contracts/local-mock-recording-failure-v0.1.schema.json deleted file mode 100644 index 1ef49a1..0000000 --- a/docs/contracts/local-mock-recording-failure-v0.1.schema.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://go-sip.local/contracts/proposals/local-mock-recording-failure-v0.1.schema.json", - "title": "Local Mock recording failure fact v0.1 (project proposal only)", - "description": "Payload of AgentControl.ReportExecutionEvent FACT_KIND_RECORDING_PROGRESS for an explicitly failed Mock recording upload. Maximum encoded payload: 4096 bytes. Not a SaaS event or mixed/real contract.", - "type": "object", - "additionalProperties": false, - "required": ["schema_version", "upload_id", "recording_id", "error_code"], - "properties": { - "schema_version": {"const": "local-mock-recording-failure.v0.1"}, - "upload_id": {"type": "string", "minLength": 1, "maxLength": 128, "pattern": "^[A-Za-z0-9._:-]+$"}, - "recording_id": {"type": "string", "minLength": 1, "maxLength": 128, "pattern": "^[A-Za-z0-9._:-]+$"}, - "error_code": { - "type": "string", - "enum": ["upload_authorization_failed", "upload_authorization_expired", "upload_failed", "checksum_mismatch"] - } - } -} diff --git a/docs/contracts/mq-topology-v0.1-proposal.json b/docs/contracts/mq-topology-v0.1-proposal.json deleted file mode 100644 index 51abce2..0000000 --- a/docs/contracts/mq-topology-v0.1-proposal.json +++ /dev/null @@ -1,84 +0,0 @@ -{ - "contract_version": "project-saas-dispatcher.v0.1", - "amqp_protocol": "0-9-1", - "exchanges": { - "commands": {"name": "agent-call.dispatchers.v3", "type": "topic", "durable": true}, - "results": {"name": "agent-call.saas.v3", "type": "topic", "durable": true}, - "dead_letter": {"name": "agent-call.dead-letter.v3", "type": "topic", "durable": true} - }, - "queues": { - "task": { - "owner": "saas", - "exchange": "agent-call.dispatchers.v3", - "routing_key": "d..task..in", - "binding_key": "same as routing_key", - "queue_name": "agent-call.d..task..v3", - "durable": true, - "exclusive": false, - "auto_delete": false, - "dead_letter_exchange": "agent-call.dead-letter.v3", - "dead_letter_routing_key": "d..dead-letter" - }, - "control": { - "owner": "saas", - "exchange": "agent-call.dispatchers.v3", - "routing_key": "d..control.in", - "binding_key": "same as routing_key", - "queue_name": "agent-call.d..control.v3", - "durable": true, - "exclusive": false, - "auto_delete": false, - "dead_letter_exchange": "agent-call.dead-letter.v3", - "dead_letter_routing_key": "d..dead-letter" - }, - "result": { - "owner": "saas", - "exchange": "agent-call.saas.v3", - "routing_key": "d..out", - "binding_key": "same as routing_key", - "queue_name": "agent-call.saas.d..v3", - "durable": true, - "exclusive": false, - "auto_delete": false - }, - "dead_letter": { - "owner": "saas", - "exchange": "agent-call.dead-letter.v3", - "routing_key": "d..dead-letter", - "binding_key": "same as routing_key", - "queue_name": "agent-call.d..dead-letter.v3", - "durable": true, - "exclusive": false, - "auto_delete": false - } - }, - "limits": { - "task_id_pattern": "^[A-Za-z0-9_-]{1,128}$", - "dispatcher_id_format": "lowercase canonical UUID v4", - "max_task_queues_per_dispatcher_including_draining": 256, - "max_routing_key_bytes": 255, - "max_queue_name_bytes": 255, - "task_routing_key_max_bytes": 175, - "task_queue_name_max_bytes": 186, - "json_message_body_max_bytes": 8388608 - }, - "publishing": { - "messages_persistent": true, - "mandatory": true, - "publisher_confirms": true, - "mark_outbox_delivered_only_after_no_return_and_positive_confirm": true, - "positive_confirm_is_saas_application_receipt": false, - "retry_reuses_same_identity_and_exact_body": true - }, - "ownership": { - "saas_creates_binds_and_retires_all_queues": true, - "dispatcher_may_consume_task_and_control_queues": true, - "dispatcher_may_declare_bind_or_delete_queues": false - }, - "inbound_processing": { - "ack_after_durable_inbox_and_state_commit": true, - "expired_not_after_is_application_rejection_not_broker_ttl": true, - "invalid_json_or_schema_nack_requeue_false_to_dead_letter_queue": true, - "redelivered_call_execute_must_not_originate_again": true - } -} diff --git a/docs/contracts/static-cell-artifact-v0.2.schema.json b/docs/contracts/static-cell-artifact-v0.2.schema.json deleted file mode 100644 index a2098e0..0000000 --- a/docs/contracts/static-cell-artifact-v0.2.schema.json +++ /dev/null @@ -1,363 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://go-sip.local/contracts/proposals/static-cell-artifact-v0.2.schema.json", - "title": "Project-owned v1 static Cell/SIP hand-off artifact", - "type": "object", - "additionalProperties": false, - "required": [ - "artifact_id", - "source_release", - "source_digest", - "approval_reference", - "cell_id", - "revision", - "config_sha256", - "mode", - "allowed_targets", - "trunks" - ], - "properties": { - "artifact_id": { - "type": "string", - "minLength": 1, - "maxLength": 128 - }, - "source_release": { - "type": "string", - "minLength": 1, - "maxLength": 128 - }, - "source_digest": { - "type": "string", - "pattern": "^[a-f0-9]{64}$" - }, - "approval_reference": { - "type": "string", - "minLength": 1, - "maxLength": 256 - }, - "cell_id": { - "type": "string", - "minLength": 1, - "maxLength": 128 - }, - "revision": { - "type": "integer", - "minimum": 1 - }, - "config_sha256": { - "type": "string", - "pattern": "^[a-f0-9]{64}$" - }, - "mode": { - "enum": [ - "mock", - "mixed", - "real" - ] - }, - "allowed_targets": { - "type": "array", - "minItems": 1, - "maxItems": 1000, - "uniqueItems": true, - "items": { - "type": "string", - "pattern": "^[0-9]{11,15}$" - } - }, - "trunks": { - "type": "array", - "minItems": 1, - "maxItems": 32, - "items": { - "type": "object", - "additionalProperties": false, - "required": [ - "trunk_id", - "provider_id", - "codec", - "caller_profile_ids", - "dial_prefix", - "enabled", - "media_profile_id" - ], - "properties": { - "trunk_id": { - "type": "string", - "minLength": 1, - "maxLength": 128 - }, - "provider_id": { - "type": "string", - "minLength": 1, - "maxLength": 128 - }, - "codec": { - "const": "PCMA" - }, - "caller_profile_ids": { - "type": "array", - "minItems": 1, - "uniqueItems": true, - "items": { - "type": "string", - "minLength": 1, - "maxLength": 128 - } - }, - "dial_prefix": { - "type": "string", - "maxLength": 32 - }, - "enabled": { - "type": "boolean" - }, - "sip_endpoint_ref": { - "type": "string", - "maxLength": 128 - }, - "credential_ref": { - "type": [ - "string", - "null" - ], - "maxLength": 128 - }, - "media_profile_id": { - "type": "string", - "minLength": 1, - "maxLength": 128 - } - } - } - }, - "ari": { - "type": "object", - "additionalProperties": false, - "required": [ - "base_url", - "websocket_url", - "application", - "credential_ref" - ], - "properties": { - "base_url": { - "type": "string", - "format": "uri", - "pattern": "^https?://" - }, - "websocket_url": { - "type": "string", - "format": "uri", - "pattern": "^wss?://" - }, - "application": { - "type": "string", - "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$" - }, - "credential_ref": { - "type": "string", - "minLength": 1, - "maxLength": 128 - } - } - }, - "media_profiles": { - "type": "object", - "minProperties": 1, - "maxProperties": 16, - "additionalProperties": { - "type": "object", - "additionalProperties": false, - "required": [ - "format", - "sample_rate_hz", - "channels", - "payload_type" - ], - "properties": { - "format": { - "enum": [ - "slin16", - "alaw" - ] - }, - "sample_rate_hz": { - "enum": [ - 8000, - 16000 - ] - }, - "channels": { - "const": 1 - }, - "payload_type": { - "type": "integer", - "minimum": 0, - "maximum": 127 - } - }, - "allOf": [ - { - "if": { - "properties": { - "format": { - "const": "alaw" - } - } - }, - "then": { - "properties": { - "sample_rate_hz": { - "const": 8000 - }, - "payload_type": { - "const": 8 - } - } - } - }, - { - "if": { - "properties": { - "format": { - "const": "slin16" - } - } - }, - "then": { - "properties": { - "sample_rate_hz": { - "const": 16000 - }, - "payload_type": { - "type": "integer", - "minimum": 96, - "maximum": 127 - } - } - } - } - ] - } - }, - "media": { - "type": "object", - "additionalProperties": false, - "required": [ - "bind_address", - "port", - "format", - "sample_rate_hz", - "channels", - "payload_type" - ], - "properties": { - "bind_address": { - "type": "string", - "minLength": 1, - "maxLength": 255 - }, - "port": { - "type": "integer", - "minimum": 1024, - "maximum": 65535 - }, - "format": { - "enum": [ - "slin16", - "alaw" - ] - }, - "sample_rate_hz": { - "enum": [ - 8000, - 16000 - ] - }, - "channels": { - "const": 1 - }, - "payload_type": { - "type": "integer", - "minimum": 0, - "maximum": 127 - } - } - }, - "recording": { - "type": "object", - "additionalProperties": false, - "required": [ - "enabled", - "format", - "directory", - "max_bytes" - ], - "properties": { - "enabled": { - "const": true - }, - "format": { - "const": "wav" - }, - "directory": { - "type": "string", - "minLength": 1, - "maxLength": 512 - }, - "max_bytes": { - "type": "integer", - "minimum": 16000, - "maximum": 1073741824 - } - } - }, - "load_evidence": { - "type": [ - "object", - "null" - ], - "additionalProperties": false, - "properties": { - "asterisk_config_sha256": { - "type": "string", - "pattern": "^[a-f0-9]{64}$" - }, - "loaded_at": { - "type": "string", - "format": "date-time" - }, - "status": { - "enum": [ - "not-yet-loaded", - "loaded" - ] - } - } - } - }, - "allOf": [ - { - "if": { - "properties": { - "mode": { - "enum": [ - "mixed", - "real" - ] - } - } - }, - "then": { - "required": [ - "ari", - "media", - "media_profiles", - "recording" - ] - } - } - ] -} diff --git a/docs/contracts/task-control-v0.4-proposal.schema.json b/docs/contracts/task-control-v0.4-proposal.schema.json deleted file mode 100644 index c519298..0000000 --- a/docs/contracts/task-control-v0.4-proposal.schema.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://go-sip.local/contracts/proposals/task-control-v0.4-proposal.schema.json", - "title": "Project-local inbound pause/resume/stop commands; external compatibility unverified", - "$comment": "Replaces only task.control ingress in command-next.v0.1-proposal. Existing command.result egress remains under the v0.1 contract. Pause and stop always require hangup; no active_call_policy or MQ command expiry field.", - "type": "object", - "additionalProperties": false, - "required": ["schema_version", "dispatcher_id", "tenant_id", "tenant_key", "trace_id", "issued_at", "command_type", "payload"], - "properties": { - "schema_version": {"const": "task-control.v0.4-proposal"}, - "dispatcher_id": {"$ref": "https://go-sip.local/contracts/v1/mq.schema.json#/$defs/dispatcherId"}, - "tenant_id": {"$ref": "https://go-sip.local/contracts/v1/mq.schema.json#/$defs/id"}, - "tenant_key": {"$ref": "https://go-sip.local/contracts/v1/mq.schema.json#/$defs/tenantKey"}, - "trace_id": {"$ref": "https://go-sip.local/contracts/v1/mq.schema.json#/$defs/id"}, - "issued_at": {"$ref": "https://go-sip.local/contracts/v1/mq.schema.json#/$defs/time"}, - "command_type": {"const": "task.control"}, - "payload": { - "type": "object", "additionalProperties": false, - "required": ["task_id", "action", "reason"], - "properties": { - "task_id": {"type": "string", "pattern": "^[A-Za-z0-9_-]{1,128}$"}, - "action": {"enum": ["pause", "resume", "stop"]}, - "reason": {"type": "string", "minLength": 1, "maxLength": 512} - } - } - } -} diff --git a/docs/contracts/task-discovery-v0.1-proposal.schema.json b/docs/contracts/task-discovery-v0.1-proposal.schema.json deleted file mode 100644 index f023e15..0000000 --- a/docs/contracts/task-discovery-v0.1-proposal.schema.json +++ /dev/null @@ -1,152 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://go-sip.local/contracts/proposals/task-discovery-v0.1-proposal.schema.json", - "title": "Project-local contract: Dispatcher task discovery responses; external compatibility unverified", - "oneOf": [ - {"$ref": "#/$defs/snapshot_response"}, - {"$ref": "#/$defs/changes_response"}, - {"$ref": "#/$defs/error_response"} - ], - "$defs": { - "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}$" - }, - "cursor": { - "type": "string", - "minLength": 1, - "maxLength": 512, - "$comment": "Opaque SaaS change watermark; never compare to task_id or infer numeric order." - }, - "task_fields": { - "type": "object", - "required": ["task_id", "tenant_id", "tenant_key", "status", "task_revision"], - "properties": { - "task_id": {"type": "string", "pattern": "^[A-Za-z0-9_-]{1,128}$"}, - "tenant_id": {"type": "string", "minLength": 1, "maxLength": 128}, - "tenant_key": { - "type": "string", - "minLength": 1, - "maxLength": 196, - "$comment": "Also validate the existing UTF-8 byte limit and tenant_id mapping in business logic; tenant_key is not part of any queue or routing key." - }, - "status": {"enum": ["running", "paused", "stopped", "finished"]}, - "task_revision": {"type": "integer", "minimum": 1} - } - }, - "queue": { - "type": "object", - "additionalProperties": false, - "required": ["exchange", "routing_key", "binding_key", "queue_name"], - "properties": { - "exchange": {"const": "agent-call.dispatchers.v3"}, - "routing_key": {"type": "string", "maxLength": 175, "pattern": "^d\\.[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\\.task\\.[A-Za-z0-9_-]{1,128}\\.in$"}, - "binding_key": {"type": "string", "maxLength": 175, "pattern": "^d\\.[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\\.task\\.[A-Za-z0-9_-]{1,128}\\.in$"}, - "queue_name": {"type": "string", "maxLength": 186, "pattern": "^agent-call\\.d\\.[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\\.task\\.[A-Za-z0-9_-]{1,128}\\.v3$"} - }, - "$comment": "SaaS-created and managed queue address; D only consumes. Runtime derives names from dispatcher_id/task_id, requires binding_key = routing_key, and rejects mismatches. Max 256 assigned or draining task queues per D." - }, - "snapshot_task": { - "allOf": [ - {"$ref": "#/$defs/task_fields"}, - { - "type": "object", - "required": ["queue"], - "properties": {"queue": {"$ref": "#/$defs/queue"}} - } - ], - "unevaluatedProperties": false - }, - "changed_task": { - "allOf": [ - {"$ref": "#/$defs/task_fields"}, - { - "type": "object", - "required": ["cursor", "operation", "queue"], - "properties": { - "cursor": {"$ref": "#/$defs/cursor"}, - "operation": {"enum": ["assigned", "updated"]}, - "queue": {"$ref": "#/$defs/queue"} - } - } - ], - "unevaluatedProperties": false - }, - "removed_task": { - "type": "object", - "additionalProperties": false, - "required": ["cursor", "operation", "task_id", "tenant_id", "tenant_key"], - "properties": { - "cursor": {"$ref": "#/$defs/cursor"}, - "operation": {"const": "removed"}, - "task_id": {"type": "string", "pattern": "^[A-Za-z0-9_-]{1,128}$"}, - "tenant_id": {"type": "string", "minLength": 1, "maxLength": 128}, - "tenant_key": { - "type": "string", - "minLength": 1, - "maxLength": 196, - "$comment": "Also validate the existing UTF-8 byte limit and tenant_id mapping in business logic." - } - } - }, - "snapshot_response": { - "type": "object", - "additionalProperties": false, - "required": ["schema_version", "dispatcher_id", "mode", "snapshot_id", "cursor", "tasks", "next_page_token"], - "properties": { - "schema_version": {"const": "task-discovery.v0.1-proposal"}, - "dispatcher_id": {"$ref": "#/$defs/dispatcher_id"}, - "mode": {"const": "snapshot"}, - "snapshot_id": {"type": "string", "minLength": 1, "maxLength": 128}, - "cursor": {"$ref": "#/$defs/cursor"}, - "tasks": {"type": "array", "maxItems": 100, "items": {"$ref": "#/$defs/snapshot_task"}}, - "next_page_token": {"type": ["string", "null"], "minLength": 1, "maxLength": 512} - }, - "$comment": "Each page has at most 100 tasks. All pages retain the first page's snapshot_id/cursor; stage and persist all pages before atomically replacing the active snapshot and advancing the cursor." - }, - "changes_response": { - "type": "object", - "additionalProperties": false, - "required": ["schema_version", "dispatcher_id", "mode", "from_cursor", "next_cursor", "changes", "next_page_token"], - "properties": { - "schema_version": {"const": "task-discovery.v0.1-proposal"}, - "dispatcher_id": {"$ref": "#/$defs/dispatcher_id"}, - "mode": {"const": "changes"}, - "from_cursor": {"$ref": "#/$defs/cursor"}, - "next_cursor": {"$ref": "#/$defs/cursor"}, - "changes": { - "type": "array", - "maxItems": 100, - "items": { - "oneOf": [ - {"$ref": "#/$defs/changed_task"}, - {"$ref": "#/$defs/removed_task"} - ] - } - }, - "next_page_token": {"type": ["string", "null"], "minLength": 1, "maxLength": 512} - }, - "$comment": "Each page has at most 100 changes. All pages retain from_cursor/next_cursor for the same window; apply changes in response order and persist all pages before atomically advancing next_cursor. Cursor and page-token continuity are cross-response semantics." - }, - "error_response": { - "type": "object", - "additionalProperties": false, - "required": ["schema_version", "resource", "error"], - "properties": { - "schema_version": {"const": "task-discovery.v0.1-proposal"}, - "resource": {"const": "error"}, - "error": { - "type": "object", - "$comment": "Local HTTP mapping: invalid_cursor/invalid_page_token=400, unauthorized=401, dispatcher_not_authorized=403, cursor_expired/snapshot_expired=410, service_unavailable=503. Status is transport metadata and is not part of the JSON body; external SaaS compatibility is unverified.", - "additionalProperties": false, - "required": ["code", "message"], - "properties": { - "code": {"enum": ["invalid_cursor", "cursor_expired", "snapshot_expired", "invalid_page_token", "unauthorized", "dispatcher_not_authorized", "service_unavailable"]}, - "message": {"type": "string", "minLength": 1, "maxLength": 256} - } - } - } - } - } -} diff --git a/docs/contracts/task-discovery-v0.2-proposal.schema.json b/docs/contracts/task-discovery-v0.2-proposal.schema.json deleted file mode 100644 index 88b9dff..0000000 --- a/docs/contracts/task-discovery-v0.2-proposal.schema.json +++ /dev/null @@ -1,98 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://go-sip.local/contracts/proposals/task-discovery-v0.2-proposal.schema.json", - "title": "Project-local single-response Dispatcher task discovery; external compatibility unverified", - "oneOf": [ - {"$ref": "#/$defs/snapshot_response"}, - {"$ref": "#/$defs/changes_response"}, - {"$ref": "#/$defs/error_response"} - ], - "$defs": { - "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}$" - }, - "cursor": { - "type": "string", "minLength": 1, "maxLength": 512, - "$comment": "Opaque SaaS change watermark; never compare numerically or derive from task_id." - }, - "task": { - "type": "object", "additionalProperties": false, - "required": ["task_id", "tenant_id", "tenant_key", "status", "task_revision"], - "properties": { - "task_id": {"type": "string", "pattern": "^[A-Za-z0-9_-]{1,128}$"}, - "tenant_id": {"type": "string", "minLength": 1, "maxLength": 128}, - "tenant_key": {"type": "string", "minLength": 1, "maxLength": 196, - "$comment": "Also enforce the UTF-8 byte limit and tenant_id mapping in business logic."}, - "status": {"enum": ["running", "paused", "stopped", "finished"]}, - "task_revision": {"type": "integer", "minimum": 1} - } - }, - "change": { - "oneOf": [ - { - "type": "object", "additionalProperties": false, - "required": ["cursor", "operation", "task_id", "tenant_id", "tenant_key", "status", "task_revision"], - "properties": { - "cursor": {"$ref": "#/$defs/cursor"}, - "operation": {"enum": ["assigned", "updated"]}, - "task_id": {"type": "string", "pattern": "^[A-Za-z0-9_-]{1,128}$"}, - "tenant_id": {"type": "string", "minLength": 1, "maxLength": 128}, - "tenant_key": {"type": "string", "minLength": 1, "maxLength": 196}, - "status": {"enum": ["running", "paused", "stopped", "finished"]}, - "task_revision": {"type": "integer", "minimum": 1} - } - }, - { - "type": "object", "additionalProperties": false, - "required": ["cursor", "operation", "task_id", "tenant_id", "tenant_key"], - "properties": { - "cursor": {"$ref": "#/$defs/cursor"}, - "operation": {"const": "removed"}, - "task_id": {"type": "string", "pattern": "^[A-Za-z0-9_-]{1,128}$"}, - "tenant_id": {"type": "string", "minLength": 1, "maxLength": 128}, - "tenant_key": {"type": "string", "minLength": 1, "maxLength": 196} - } - } - ] - }, - "snapshot_response": { - "type": "object", "additionalProperties": false, - "required": ["schema_version", "dispatcher_id", "cursor", "tasks"], - "properties": { - "schema_version": {"const": "task-discovery.v0.2-proposal"}, - "dispatcher_id": {"$ref": "#/$defs/dispatcher_id"}, - "cursor": {"$ref": "#/$defs/cursor"}, - "tasks": {"type": "array", "maxItems": 256, "items": {"$ref": "#/$defs/task"}} - }, - "$comment": "No pagination or queue address in the body. Persist the entire consistent response before advancing the cursor." - }, - "changes_response": { - "type": "object", "additionalProperties": false, - "required": ["schema_version", "dispatcher_id", "next_cursor", "changes"], - "properties": { - "schema_version": {"const": "task-discovery.v0.2-proposal"}, - "dispatcher_id": {"$ref": "#/$defs/dispatcher_id"}, - "next_cursor": {"$ref": "#/$defs/cursor"}, - "changes": {"type": "array", "maxItems": 256, "items": {"$ref": "#/$defs/change"}} - }, - "$comment": "No change: changes=[] and next_cursor=request after. An unrepresentable complete change set requires HTTP 410 cursor_expired, never a partial 200." - }, - "error_response": { - "type": "object", "additionalProperties": false, - "required": ["schema_version", "resource", "error"], - "properties": { - "schema_version": {"const": "task-discovery.v0.2-proposal"}, - "resource": {"const": "error"}, - "error": { - "type": "object", "additionalProperties": false, - "required": ["code", "message"], - "properties": { - "code": {"enum": ["invalid_cursor", "cursor_expired", "unauthorized", "dispatcher_not_authorized", "service_unavailable"]}, - "message": {"type": "string", "minLength": 1, "maxLength": 256} - } - } - } - } - } -} diff --git a/docs/contracts/task-discovery-v0.3-proposal.schema.json b/docs/contracts/task-discovery-v0.3-proposal.schema.json deleted file mode 100644 index d753688..0000000 --- a/docs/contracts/task-discovery-v0.3-proposal.schema.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://go-sip.local/contracts/proposals/task-discovery-v0.3-proposal.schema.json", - "title": "Project-local event-cursor Dispatcher task pages; external compatibility unverified", - "oneOf": [ - {"$ref": "#/$defs/page_response"}, - {"$ref": "#/$defs/error_response"} - ], - "$defs": { - "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}$" - }, - "cursor": { - "type": "string", "pattern": "^(0|[1-9][0-9]{0,19})$", - "$comment": "Canonical decimal uint64 event ID per Dispatcher; semantic parsing enforces the uint64 maximum. Never derived from task_id or expires." - }, - "task": { - "type": "object", "additionalProperties": false, - "required": ["task_id", "tenant_id", "tenant_key", "status", "task_revision"], - "properties": { - "task_id": {"type": "string", "pattern": "^[A-Za-z0-9_-]{1,128}$"}, - "tenant_id": {"type": "string", "minLength": 1, "maxLength": 128}, - "tenant_key": {"type": "string", "minLength": 1, "maxLength": 196, - "$comment": "Also enforce UTF-8 byte limit and tenant_id mapping in business logic."}, - "status": {"enum": ["running", "paused", "stopped", "finished", "removed"]}, - "task_revision": {"type": "integer", "minimum": 1} - }, - "$comment": "Removed tombstones retain original task and tenant identity indefinitely. No per-item event_id or queue location." - }, - "page_response": { - "type": "object", "additionalProperties": false, - "required": ["schema_version", "dispatcher_id", "next_cursor", "tasks"], - "properties": { - "schema_version": {"const": "task-discovery.v0.3-proposal"}, - "dispatcher_id": {"$ref": "#/$defs/dispatcher_id"}, - "next_cursor": {"$ref": "#/$defs/cursor"}, - "tasks": {"type": "array", "maxItems": 256, "items": {"$ref": "#/$defs/task"}} - }, - "$comment": "Initial and later pages share one shape. A nonempty page advances after to its last returned event; an empty page repeats after and signals caught up. Server guarantees ascending complete event positions; client cannot verify ordering inside a page without per-item IDs." - }, - "error_response": { - "type": "object", "additionalProperties": false, - "required": ["schema_version", "resource", "error"], - "properties": { - "schema_version": {"const": "task-discovery.v0.3-proposal"}, - "resource": {"const": "error"}, - "error": { - "type": "object", "additionalProperties": false, - "required": ["code", "message"], - "properties": { - "code": {"enum": ["invalid_cursor", "unauthorized", "dispatcher_not_authorized", "service_unavailable"]}, - "message": {"type": "string", "minLength": 1, "maxLength": 256} - } - } - } - } - } -} diff --git a/docs/contracts/task-discovery-v0.4-proposal.schema.json b/docs/contracts/task-discovery-v0.4-proposal.schema.json deleted file mode 100644 index 0134da5..0000000 --- a/docs/contracts/task-discovery-v0.4-proposal.schema.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://go-sip.local/contracts/proposals/task-discovery-v0.4-proposal.schema.json", - "title": "Project-local restart snapshot and live assignment changes; SaaS compatibility unverified", - "oneOf": [ - {"$ref": "#/$defs/snapshot_page"}, - {"$ref": "#/$defs/change_page"}, - {"$ref": "#/$defs/error_response"} - ], - "$defs": { - "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}$" - }, - "cursor": { - "type": "string", "pattern": "^(0|[1-9][0-9]{0,19})$", - "$comment": "Canonical decimal uint64 event ID per Dispatcher; semantic parsing enforces the uint64 maximum. Never derived from task_id or expires." - }, - "task": { - "type": "object", "additionalProperties": false, - "required": ["task_id", "tenant_id", "tenant_key", "status", "task_revision"], - "properties": { - "task_id": {"type": "string", "pattern": "^[A-Za-z0-9_-]{1,128}$"}, - "tenant_id": {"type": "string", "minLength": 1, "maxLength": 128}, - "tenant_key": {"type": "string", "minLength": 1, "maxLength": 196, - "$comment": "Also enforce UTF-8 byte limit and tenant_id mapping in business logic."}, - "status": {"enum": ["running", "paused", "stopped", "finished", "removed"]}, - "task_revision": {"type": "integer", "minimum": 1} - }, - "$comment": "Removed tombstones retain original task and tenant identity indefinitely. No per-item event_id or queue location." - }, - "snapshot_page": { - "type": "object", "additionalProperties": false, - "required": ["schema_version", "mode", "dispatcher_id", "snapshot_id", "watermark", "next_page_token", "tasks"], - "properties": { - "schema_version": {"const": "task-discovery.v0.4-proposal"}, - "mode": {"const": "snapshot"}, - "dispatcher_id": {"$ref": "#/$defs/dispatcher_id"}, - "snapshot_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}$"}, - "watermark": {"$ref": "#/$defs/cursor"}, - "next_page_token": {"type": ["string", "null"], "minLength": 1, "maxLength": 256}, - "tasks": {"type": "array", "maxItems": 256, "items": {"$ref": "#/$defs/task"}} - }, - "$comment": "Every page has identical snapshot_id and watermark; null next_page_token ends this consistent complete inventory. No task appears twice, and removed tasks are omitted only from the complete inventory. Server retains stopped tasks with not-yet-retired queues. Semantic client checks enforce consistency." - }, - "change_page": { - "type": "object", "additionalProperties": false, - "required": ["schema_version", "mode", "dispatcher_id", "next_cursor", "tasks"], - "properties": { - "schema_version": {"const": "task-discovery.v0.4-proposal"}, - "mode": {"const": "changes"}, - "dispatcher_id": {"$ref": "#/$defs/dispatcher_id"}, - "next_cursor": {"$ref": "#/$defs/cursor"}, - "tasks": {"type": "array", "maxItems": 256, "items": {"$ref": "#/$defs/task"}} - }, - "$comment": "Live-only event cursor; ordered complete event positions per dispatcher, no server expiry while the process is running. Removed tombstones are explicit; status values are not online control commands. A nonempty page advances the cursor, and an empty page repeats it." - }, - "error_response": { - "type": "object", "additionalProperties": false, - "required": ["schema_version", "resource", "error"], - "properties": { - "schema_version": {"const": "task-discovery.v0.4-proposal"}, - "resource": {"const": "error"}, - "error": { - "type": "object", "additionalProperties": false, - "required": ["code", "message"], - "properties": { - "code": {"enum": ["invalid_request", "invalid_page_token", "snapshot_unavailable", "unauthorized", "dispatcher_not_authorized", "service_unavailable"]}, - "message": {"type": "string", "minLength": 1, "maxLength": 256} - } - } - } - } - } -} diff --git a/docs/contracts/通信与事件数据交互_v0.1.md b/docs/contracts/通信与事件数据交互_v0.1.md deleted file mode 100644 index f3ecac6..0000000 --- a/docs/contracts/通信与事件数据交互_v0.1.md +++ /dev/null @@ -1,343 +0,0 @@ -# Dispatcher / Agent 通信与事件数据交互 v0.1 - -## 1. 范围、权威与状态 - -本文件保留现有外部命令/事件与内部职责目录,并明确本轮适用范围:**P1为1 Agent/1 Asterisk/单 Cell、至少3家SIP trunk 的契约与协议 fixture、单租户、静态配置、ASR-only与完整AI双模式;真实 SaaS/MQ 联调、双节点、第二 Cell、第二租户和生产切换延期第二阶段。** 本地 P1 实施按配置读取计划推进;全量目录不等于本轮全部实现,运行通过记录另见验收证据。 - -- **现行外部已发布 SaaS↔Dispatcher 契约仍只经 RabbitMQ 专用 Topic,禁止双方 HTTP。每个 D 有全局唯一 ID 及独立接收 Topic/队列,不能共享队列抢收指定 D 的消息。** 精确身份/拓扑/消息/关联见[现行 MQ 机器契约](../../contracts/upstream/v1/mq.schema.json)和[归档计划§1.2/§8.2] (../archive/plan-0918.md)。本轮 P1 项目内目标由[新计划](../plan-config-read-v0.1.md)与[第三方对接契约](../thirds/第三方对接事件与请求消费顺序_v0.1.md)定义:四条只读 GET、简化命令/控制及单份最终 `call.result`;F01/F07 Schema/正反例/hash 与 Mock C 通过后即可本地实施,不等待外部签收/连通。真实 SaaS 兼容性仍未验证;本地 C 不改变现行外部契约,也不代表生产验收。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;**“缺口”** 明确阻塞相应实现/验收。 -- 用户已确认保留Unary RPC、Dispatcher维护Agent Endpoint列表、Agent共用一套mTLS证书。OSS配置存于Dispatcher配置文件,Agent向D领取临时上传TOKEN后直传OSS;SaaS不下发OSS配置/TOKEN;本项目只保证recording.uploaded可靠入队,不等待SaaS会话、verified或OSS ID。文本继续实时MQ回传,OSS只作归档。 -- 准确的文字事件名是 **`transcript.updated`**;`call.transcript` 是之前讨论中的泛称,不是合法event_type,不新增该别名。 -- 首发AI范围已确认:**百炼/火山ASR、OpenAI兼容LLM、火山TTS**。业务控制参数由Dispatcher按任务版本向SaaS获取,Agent按执行快照使用;不得从源码常量、本地业务配置或SDK默认值形成第二配置源。具体模型/协议/额度仍须批准和PoC。 - -### 1.1 已对齐部分与仍待补齐的约束 - -此前逐字段及SHA-256复核确认,**旧MQ信封与对应版本正文对齐**;这不代表新增D身份/专用Topic及全MQ请求响应已纳入该版Schema。以下为旧外部源的对齐/缺口记录,项目内旧补齐成果以固定包为准;本轮新差异另见GAP-10。 - -| 项 | 当前正文/Schema一致内容 | 仍须验证 | -| --- | --- | --- | -| 命令类别/ID | command_type / command_id | 拒绝旧type/message_id作为替代,保护作用域幂等 | -| 事件类别 | event_type,枚举覆盖8种事件 | 不接受call.transcript等不存在的别名 | -| 聚合版本 | aggregate_type / aggregate_id / aggregate_version | 按实体域合并,不能降为全局event_version | -| 时间 | issued_at / not_after、occurred_at | 区分授权、发生和接收时间,验证截止 | -| 命令payload | required已含task_revision等正文12字段 | 类型、长度、白名单及跨字段业务规则 | -| 事件payload | 目前仍主要是通用object | 8种专属payload、条件必填/状态/失败分支需在上游唯一源补齐 | - -**GAP-01只指事件专属payload和未机读化业务约束的覆盖不足,不指信封字段漂移。** P0补齐并跑正反例。本轮不覆盖父项目已有文件;不能把仅通过通用object校验当完整事件验收。 - -### 1.2 分期与本轮传输修订 - -- **现行外部基线**保留旧 call.execute、控制/查询/整体补传/录音协调语义及8种业务事件;旧7条HTTP路径不再作为现行接入方式,对应请求响应经MQ。此项记录现行 v1,不是本轮本地目标。 -- 42操作/115组件仅为上游目录;不把管理平台30操作移入Dispatcher。按实际入口及引用闭包生成校验,来源包/只读索引仍完整留存,不通过删Schema缩小范围。 -- 单租户仅指启用策略:保留tenant_key精确路由、租户独立队列/复合幂等键、有界窗口和单 Cell 全局配额;双租户公平、第二 Cell 汇总和多Dispatcher协调另立第二阶段。 -- 本轮本地目标按第三方对接契约使用四条只读配置/发现 GET;命令/控制/必要回执与单份最终 `call.result` 仍走 MQ,不恢复 query/replay、拆分实时文字/拒联/录音事件,也不新增任务 mode 字段。旧 MQ-only GAP-10 只描述外部 v1 基线;本地 C 不等待外部发布,真实兼容性另行记录。ASR-only表达沿GAP-08;R04/R06在线改配延后,但最后许可、控制、静态维护屏障和持久恢复不能延后。 - -## 2. 角色、传输和可靠性边界 - -| 通道 | 发送方 → 接收方 | 内容 | 接受/交付的含义 | -| --- | --- | --- | --- | -| RabbitMQ执行/控制/查询/补传 | SaaS → MQ → 指定D专用Topic;响应经MQ回SaaS | call.execute及既有业务语义 | 校验目标/租户/原请求,持久受理和outbox后才ACK;accepted不等于applied | -| RabbitMQ录音通知 | Dispatcher → MQ → 指定持久队列 | 原上传事实及recording.uploaded通知 | persistent、正确绑定、mandatory无return、publisher confirm后完成本项目交付;不传OSS配置/TOKEN,不等待SaaS处理 | -| 临时上传TOKEN | Agent ↔ Unary ↔ Dispatcher | D依配置文件提供TOKEN/受限上传信息;过期显式重新申请 | 长期凭据不交给A,配置无效明确失败,不向SaaS取配置/TOKEN | -| RabbitMQ AI配置/授权 | Dispatcher ↔ MQ ↔ SaaS | 任务引用的不可变AI版本及有效授权 | D专用Topic收原请求响应并持久绑定;旧GET已废弃,Agent不直连SaaS | -| 内部Unary gRPC | Dispatcher ↔ Agent | 执行授权、控制、配置、状态、最终文字、上传元信息 | 每个RPC有独立deadline、权限、请求关联及幂等;不是一条双向数据流 | -| ARI/RTP | Agent ↔ 本Cell Asterisk | 通道/桥/媒体/录音 | 实际拨号副作用不与任何数据库事务原子提交 | -| OSS数据面 | Agent → OSS | P1已封口录音;文本OSS归档后续 | PUT成功后由D报告事实;实际发送大小/SHA-256一致,ETag不等于SHA-256 | -| RabbitMQ结果 | Dispatcher → MQ → SaaS专用订阅 | 本文8类业务event_type及新版冻结的响应 | 来源D/租户/请求可关联;confirm只表示broker收妥,持久inbox后ACK,不擅自新增application receipt协议 | -| SIP配置管理 | 管理平台 → 批准静态制品/受控部署 → Agent;D核验准入 | 版本/哈希/目标及实际加载事实,P1维护窗口生效 | 管理平台唯一编辑面;静态交接见GAP-03;在线D推送暂缓 | - -Agent不持MQ/SaaS管理凭据、不直接消费SaaS队列,不新增公开HTTP拨号/结果回调。普通Unary同样复用HTTP/2连接,不能按每通电话新建连接。 - -## 3. 标识和版本不可混用 - -| 标识/版本 | 范围与用途 | -| --- | --- | -| Dispatcher逻辑ID(精确字段待冻结) | 全局唯一、独立接收Topic;与tenant/Agent/Cell ID及dispatcher_epoch分开。身份持久化/重复拒绝及请求响应关联须新合同冻结 | -| tenant_id / tenant_key | 前者可信归属,后者原值绑定;D隔离不替代租户隔离。旧224字节预算不能直接套新拓扑,完整长度/通配符边界重新验证,超限保留任务停发,不清洗/编码/截断 | -| command_id | 租户作用域业务幂等身份;旧HTTP Idempotency-Key语义须映射到获批MQ合同,不照搬header或猜字段 | -| execution_id | 租户作用域授权执行,换command_id不得重复拨号 | -| task_id / task_item_id / task_revision | 任务、成员和控制版本;与软件/配置版本无关 | -| call_id / attempt_id | 一次逻辑通话与具体拨号尝试;只有持久化意图后才产生call。P1不启用自动FALLBACK;未来启用仍属原执行并计CPS | -| event_id / aggregate_* | SaaS MQ inbox和对应实体/状态域版本;由Dispatcher持久事务分配/递增 | -| turn_id / segment_id / revision | 文字片段与最终稿替换语义,不以消息到达时间判断新旧 | -| recording_id / upload_id / bucket / object_key | 原录音和上传事实、对象位置;不含OSS ID、TOKEN或签名URL | -| agent_id / cell_id(内部草案) | Dispatcher预配置的执行端身份与Cell绑定,不能由Agent自报覆盖;共享证书不等于单节点身份 | -| boot_id / session_epoch(内部草案) | 一次进程启动及Dispatcher绑定代次;旧回报不能覆盖新会话,旧执行事实仍需对账,不直接丢弃 | -| agent_version / protocol_version | 二进制发布版本、gRPC协议版本;不是AI的agent_version_id | -| desired/applied revision、config_sha256 | 发布意图与实际加载事实;相同版本异哈希冲突,不能以文件已写代替applied | - -内部关联字段最终名称/格式在Proto冻结时确定。现有外部信封/配置优先以**原版本JSON字节+schema引用/摘要**嵌入内部消息并按源Schema校验,避免在Proto再手写一份业务Schema;不得经Struct/float转换破坏大整数、空值或哈希语义。 - -## 4. 现有call.execute完整业务入口 - -旧routing key为`agent-call.tenant.{tenant_key}.call.execute`、direct exchange,**仅作旧实现对照,不用于新接入**。新版必须定向指定D专用Topic/队列并保留租户隔离,精确命名在GAP-10冻结。下表保留旧业务字段语义,不代表已包含D身份/关联。 - -| 外壳字段 | 语义 | -| --- | --- | -| schema_version | 已支持契约版本;不支持明确拒绝 | -| command_type | 固定call.execute | -| command_id、tenant_id、tenant_key、trace_id | 幂等、归属、路由和追踪 | -| issued_at、not_after | 签发和每次新发起截止;不能以重投延期 | -| payload | 下表;不接受任意SIP/AI URL、凭据或主叫注入 | - -| payload字段 | 约束 | -| --- | --- | -| execution_id、task_id、task_item_id | 原业务归属及执行身份 | -| task_revision | 必须等于当前已生效、允许运行的控制版本 | -| callee | 原始号码,不提前拼线路前缀;服务端白名单另验 | -| route_policy_id、caller_profile_id | 服务端已配置并授权的策略/主叫引用 | -| agent_version_id | 当前租户可信、不可变AI快照 | -| variables | 白名单、类型、长度约束;非代码或任意URL | -| ring_timeout_ms、max_call_duration_ms | 不超过服务端/供应商上限 | - -Dispatcher先验身份/Schema/关联,再识别历史幂等事实;新执行才检查当前时效/控制/配置/资源。固定admission_deadline,准入失败有界终结,不依赖资源释放才扫描。实际发起前再次检查所有租约/控制/截止;意图已落地但是否发出未知时reconciling,不回退成无call_id的拒绝,也不重拨。 - -P1从管理批准的静态route_policy/caller_profile选择供应商trunk与获授权Cell,不在通话过程中改绑或自动跨供应商重拨。每家使用原始callee应用自身规则。AI模式从该租户不可变agent_version_id读取:ASR-only只占ASR资源且不调用LLM/TTS;完整模式同时校验三类AI额度,不能静默降级。当前Schema缺少明确模式表达,批准GAP-08前不伪造空LLM/TTS配置。 - -## 5. 8种SaaS业务事件全集 - -### 5.1 通用外壳 - -每种事件必有:`schema_version`、`event_id`、`event_type`、`tenant_id`、`tenant_key`、`trace_id`、`occurred_at`、`aggregate_type`、`aggregate_id`、`aggregate_version`、`payload`。 - -旧routing key为`agent-call.{event_type}`;新版须冻结来源D及业务归属的表达。SaaS按`(tenant_id,event_id)`去重,inbox与业务更新同事务,成功后ACK。Dispatcher保存版本化快照及outbox,重发保留event_id/内容,不因RPC重报创建第二个业务事件。本文不会把示例里的可选字段擅自升级成机器required;尚缺的payload Schema见GAP-01。 - -| event_type | 事实来源 / 发布者 | 必需或条件业务字段 | 时点与合并 | -| --- | --- | --- | --- | -| command.result | Dispatcher自身受理/控制/执行汇总 → Dispatcher | command_id、command_type、status、reason_code;适用的task/execution/call关联;execute含等待/准入字段;control含requested/applied_task_revision | command聚合;accepted/waiting不代表已拨;MQ受理响应不代表applied;重试不重复递增revision | -| call.status | Agent经ARI观测+Dispatcher授权账本 → Dispatcher | call_id、execution_id、任务关联、call_state、call_version、attempt_id、attempt状态、实际线路/Cell/出口、时间/原因;尚未定名的键在GAP-01冻结 | 同call/attempt域更新;只有实际证据才dialing/ringing/answered,迟到状态不回退 | -| transcript.updated | Agent的ASR/对话/播放证据 → Dispatcher | call_id、turn_id、segment_id、role、revision、text、is_final、start_ms、end_ms、playback_state | transcript_segment域;同段高revision替换,final不被中间稿覆盖;不等整通话OSS上传 | -| call.finished | Agent终态事实+Dispatcher对账/汇总 → Dispatcher | call_id、execution_id、任务关联、call_version、outcome、起止/时长/原因、attempt汇总、资产处理快照 | 固定通话终态,后处理可pending,不覆盖独立资产的新状态 | -| recording.uploaded | Dispatcher经MQ可靠发布上传事实 | call_id、recording_id、upload_id、bucket、object_key、format、channels、sample_rate_hz、duration_ms、size_bytes、checksum_sha256 | recording域;只报告已知事实,不携带OSS ID、上传凭据或公开URL | -| recording.failed | Agent本地/上传失败、Dispatcher授权/校验失败 → Dispatcher | call_id、recording_id、stage、reason_code、retryable、next_retry_at(若有) | 标记资产失败,不改变通话终态;合法ready可完成恢复 | -| transcript.failed | Agent/Dispatcher发现文字缺段或不可恢复错误 → Dispatcher | call_id、原因、retryable、受影响segment(适用时) | 明确不完整,不能把现有部分文件包装成完整最终稿 | -| contact.opt_out | 获批业务判定 → Agent及时报告 → Dispatcher | call_id、task_id、task_item_id、请求时间、关联turn/segment(若有) | SaaS及时持久禁发并处理关联任务屏障,不等挂断;不自造关键词判定 | - -中文描述但尚无精确JSON键/类型的字段(例如部分终止原因、attempt汇总、失败细分)必须在GAP-01中按上游定义补全后生成;不得由Go实现自行发明。表中源于既有样例的具体键须通过正文/机读联合验收。 - -### 5.2 文字与资产语义 - -- role建议customer/agent/system;playback_state为not_applicable/generated/sent/playback_confirmed/cancelled/unknown,具体冻结按主契约。生成/发送不等于已听见。 -- 当前同段final同内容幂等、异内容冲突;未来允许修订须改契约。超长turn拆稳定segment,不截断文本。 -- call_state允许queued→dialing→ringing→answered→ended,省略未发生阶段;waiting是命令状态。reconciling不是虚构终态。 -- recording的pending/uploading/uploaded/failed、transcript的pending/streaming/finalized/failed、delivery的pending/broker_confirmed/failed分别维护;不新增VERIFYING。 -- `call.finished`先到、较低版本的独立`recording.uploaded`后到仍应合并;不能用全局最大版本滤掉资产/片段。 -- 文本OSS归档不是第9种既有事件,也不能冒充recording.uploaded;查看实时文字继续用transcript.updated。归档授权/引用扩展见GAP-02,P1不启用且不阻塞实时文字。 -- ASR-only仍上报真实customer文字及获批opt-out事实,不伪造agent回答/播放或接通证据。两模式下角色/播放状态/失败分支的合法组合须在GAP-01/GAP-08补齐;不能为省事关闭实时文字或opt-out。 - -## 6. SaaS↔Dispatcher 全MQ交互目录 - -旧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请求/响应方向 | 保留的约束 | -| --- | --- | --- | -| 任务控制 | SaaS→指定D;D→SaaS | expected_task_revision CAS,pause/resume/stop与drain/hangup;accepted不等于applied | -| 命令查询 | SaaS→指定D;D→SaaS | 原command及等待/执行事实,结果关联原查询 | -| 通话查询 | SaaS→指定D;D→SaaS | 原call/attempt及独立资产状态,不按当前配置补历史 | -| call整体补传 | SaaS→指定D;D→SaaS | 固定截止点/原事件ID和版本,不支持局部筛选,不重拨 | -| source-command整体补传 | SaaS→指定D;D→SaaS | 尚无call也可补传结果,不重发执行命令、不递归自身结果 | -| 上传完成事实 | D→MQ指定持久队列 | 原upload/recording事实和对象位置;可靠入队后完成本项目交付,不获取OSS配置/TOKEN | -| SaaS后续处理 | 不在本项目职责 | 不等待消费、verified或OSS ID,不新增VERIFYING | -| AI配置/授权 | D→SaaS;SaaS→原D专用Topic | 原租户/不可变版本/摘要/有效授权,见§6.1 | - -所有请求响应均持久关联目标/来源D、原租户及业务对象;持久后ACK、状态/outbox同事务、重复/迟到/超时/重启沿原关联恢复。超时不表示未执行,不换D重拨,不回退HTTP。错误分类保留“不存在/冲突/保留过期”等语义,精确MQ错误码及期限待GAP-10冻结,不直接搬HTTP状态码。 - -OSS配置/TOKEN来源不属于上述SaaS MQ目录:OSS配置存于D配置文件,A经Unary向D领取/显式重新申请临时TOKEN,配置缺失/无效明确失败。AI配置管理仍归SaaS,管理平台30操作不移入D;Cell静态制品唯一写入和屏障不变,内部Unary及Agent→OSS直传不受SaaS↔D禁HTTP规则影响。 - -### 6.1 SaaS任务配置 → Dispatcher → Agent(P1必需) - -“任务配置”沿用MQ任务的 `agent_version_id` 及获批 `variables` 引用:**D经MQ向SaaS请求不可变配置及有效授权,SaaS经原D专用Topic响应**。旧AI版本GET已废弃为D接入方式,不开发HTTP client或猜测task-config路径。AI配置编辑仍归SaaS;GAP-09冻结版本/授权与受控引用语义,GAP-10冻结MQ消息/关联/期限。 - -1. D先验MQ可信租户/版本/幂等;历史执行走原事实恢复,不因调参重新执行。新执行在发起前取得对应租户版本,不把模型/音色/timeout等直接加入call.execute。 -2. D验证源Schema、不可变内容摘要、租户授权、两种模式和SDK能力,解析该版本批准的provider_ref/credential_ref。SaaS返回的受控供应商配置提供API种类/协议版本、端点、region/资源标识等;缺合同标blocked,不在源码中按供应商名称拼端点或填示例resource_id。 -3. 缓存键至少绑定租户和agent_version_id;同版本异内容拒绝并告警。按批准的撤销/新鲜度策略使用已验证缓存;SaaS不可达且无仍有效的已授权快照则暂停/拒绝新准入,遵守原admission_deadline,不用默认模型、其它租户缓存或无限期旧配置顶替。MQ请求重投/响应恢复沿原请求关联且有界,不持SQLite事务等待网络;不回退HTTP。 -4. D将该执行最终有效配置、源版本/摘要及SDK能力匹配绑定到原execution。沿获批R07传送原Schema JSON/摘要或已确认缓存引用;引用缺失可经R03受控获取,**不依赖延后的R04热更新**。A二次校验并报告实际使用版本/摘要,错版本不进入最后许可。 -5. Agent从会话局部只读快照生成SDK请求和本地控制器参数;可复用连接/Transport,不修改所有通话共用的model/voice/temperature等全局对象。首发有明确的百炼/火山ASR薄适配,选择一次固定到执行,不建插件、自动AI fallback或同通话动态换供应商。 -6. 在已支持并获授权的模型/参数范围内,调参在SaaS发布新AI版本,由新任务显式引用后生效,无需改Go代码、重建镜像或重启D/A。排队旧任务/在途通话固定原版本,不能读取“latest”热改;授权撤销/stop仍按控制协议收敛,不以快照固定为由忽略撤销。 - -**静态发布仅指SIP/节点部署配置,不意味着AI业务参数写死。** 配置/授权同样必须MQ-only;实际音频仍A↔供应商,SaaS/D不代理音频流。 - -### 6.2 参数覆盖与缺口(需求索引,不是新Schema) - -现有严格对象 `additionalProperties: false` 必须保留。下表“已有”是当前源字段;“待补”是P0向上游提交的语义需求,**不是可直接发送的JSON键/已获批枚举**。每个启用参数须在批准的SDK/模型能力矩阵中有单位、范围、缺省、对应请求字段或本地控制点及测试证据。 - -| 控制范围 | 当前源契约已有 | 首发需补齐/确认的可调能力 | -| --- | --- | --- | -| 通用供应商/模式 | 各AI的provider_ref、credential_ref、model;agent_version_id | GAP-08两模式;GAP-09受控端点/API版本、模型能力、火山app/resource/cluster及认证类型引用的来源/授权,不把密钥作为普通配置值 | -| ASR输入与结果 | asr.language/interim/timeout_ms/input(encoding/sample_rate_hz/channels/sample_width_bytes) | 模型必填或明确缺省、实时中间稿/最终稿行为、发送帧时长/块大小及结束规则;不能沿用示例16kHz覆盖源配置 | -| ASR识别调试 | 当前未定义这些专属字段 | 所选协议支持的热词/词表引用、标点、ITN/文本规范化、语气词/顺滑控制、语种提示、VAD/端点检测/尾部静音阈值;百炼与火山分别映射,不假设同名同义 | -| LLM模型/采样 | llm.model/temperature/max_tokens/timeout_ms | top_p、stop序列、上下文轮数/Token预算;供应商确有需求和支持时补penalty/seed/推理控制,不能把任意extra JSON透传。max_tokens与实际API输出/推理Token语义须匹配 | -| 提示词与变量 | prompt.text/allowed_variables/max_bytes;MQ variables | 渲染失败/缺变量/超限明确拒绝,不执行模板代码;上下文截取策略可审计,不把正文或变量值写入普通日志 | -| TTS声音与音频 | tts.model/voice/speed/timeout_ms/format(encoding/sample_rate_hz/channels) | 所选火山协议支持的音量/增益、音调,以及确有需求的情感/风格;语速/音量单位和范围显式映射,不靠示例speaker/resource_id默认值 | -| 对话/打断 | conversation.opening/allow_interrupt/silence_timeout_ms/max_duration_ms/max_turns | 本地与服务端VAD职责、触发打断的最短语音/防抖等适用阈值;只由可信配置控制,关闭打断也必须保留stop/hangup权限 | -| 分句/缓存/时序 | conversation.sentence_max_chars/max_pending_audio_chunks;各AI.timeout_ms | 分句等待、音频缓存按时长/字节的上限、连接/首结果或Token/首音频/流空闲/总时限的作用域;SDK超时与本地看门狗不能互相覆盖 | - -首发必须把已有字段和实际选定模型支持、商务调试需要的上述扩展接通;不为不存在的模型能力造兼容层。未支持参数要在发布/准入时报明确错误,不能“接受但忽略”;必要参数缺SDK支持则对应能力blocked(见组件清单§4.3)。 - -### 6.3 默认值、适配与安全边界 - -- SaaS发布不可变版本时按获批Schema/供应商能力物化默认值并留来源;JSON Schema的default只是注解,不能假设校验器会自动填。过渡中缺必需有效值则拒绝,不从Go常量、env、SDK默认或示例请求补业务值。协议固定常量及部署安全上限不属于可任意调参范围。 -- 区分未提供、null、显式0/false/空列表;例如temperature=0、interim=false、allow_interrupt=false不能被Go零值/omitempty或SDK设置器吞掉。条件必填与缺省由上游定义,不能因ASR-only跳过整个配置校验。 -- SDK请求字段/单位按批准映射转换,保留requested/effective的脱敏差异;越界、冲突、不支持报可定位字段原因,不静默截断/钳制。总通话时限不得超过MQ命令、授权及平台上限,组合规则G0冻结;任务不能通过调大timeout突破stop/许可/资源硬屏障。 -- provider端点只能来自可信SaaS配置并命中受控host/协议/端口及出口策略,禁任意重定向/内网探测;localhost或IP直连例外须显式管理批准。credential_ref按租户/供应商/执行授权解析,由受控Secret/短期授权交付;Agent不持SaaS管理凭据,不把密钥、prompt、完整请求/对话记入日志。 -- 供应商差异只通过上游批准的有类型、有限范围配置扩展表达;不得借metadata、variables、自由headers或raw_request字典绕过严格Schema。部署只提供身份/网络/硬限额/Secret,不覆写业务模型/音色/语速等值。 -- 默认禁用可能重复计费/播放的AI SDK自动重试(OpenAI显式WithMaxRetries(0));未来开启须有批准的副作用/幂等语义,不因SaaS配置了retry就无限重发。已出流/已取消/结果未知不可重放旧生成。 -- 调试证据仅含获授权的租户/执行关联、配置版本/摘要、SDK及协议版本、参数名/脱敏有效值和拒绝原因;不新增公共调参API。普通日志不打印prompt/变量/密钥,敏感缓存按独立持久化权限和保留策略管理。 - -## 7. 内部gRPC公共规则(草案) - -以下为早期内部方法职责草案,不是当前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。 -- 元数据至少表达协议版本、request/trace关联、已绑定Agent/Cell/boot/会话代次、操作幂等标识、截止时间;具体字段冻结,不把敏感token放业务payload或日志。 -- 外部租户身份从Dispatcher已受理执行绑定,Agent报告不能切换tenant/call/asset归属。动态地址只能从预配置Endpoint列表获得,不执行Agent自报URL,防SSRF/错误绑定。 -- 请求幂等键必须包含操作和目标;同键同内容返回原结果,同键异内容冲突。RPC超时只表示结果未知,不能自动重拨;查询/恢复保持原标识。 -- Execute/Control/Apply返回accepted只表本地接收/持久文件记录;真正applied/终态通过回报或查询确认。业务“不能执行”与gRPC传输错误分开。 -- 重要执行/资产事实先写Agent文件,Dispatcher将事实去重、业务更新与MQ outbox同事务持久后才返回成功。回包丢失重报原fact,不新增MQ事件;该内部确认不是SaaS应用收讫。 -- 状态采样可以覆盖旧快照,最终文字、opt-out、终态、资产事件不可静默丢弃。有界重试/背压,接近文件容量阈值停新准入而非丢事实。 -- 事实的source_sequence/boot用于关联和诊断,不能用Agent全局最大序列丢掉其它通话或迟到资产。认证的当前会话与事实发生时的boot分开;合法历史文件经当前会话上报仍需按原执行对账,不能仅因旧boot丢弃。 -- Proto留存已发布字段号,不复用删除字段;兼容范围、未知枚举/能力降级和gRPC最大消息/超时/并发在G0 profile冻结,不以4MiB库默认值代替契约。 - -### 7.1 错误分类与重试(内部草案) - -| gRPC状态/业务情况 | 调用方动作 | -| --- | --- | -| UNAUTHENTICATED / PERMISSION_DENIED | 不重试成其它身份,更新批准凭据/会话或隔离并告警;不能据此释放未知通话 | -| INVALID_ARGUMENT | 拒绝坏字段/Schema/尺寸,不自动改写为另一任务 | -| FAILED_PRECONDITION | 未引导、版本/配置/资产契约未就绪等先修条件;文本归档缺接口不能fallback录音 | -| ABORTED / ALREADY_EXISTS | CAS/归属冲突或同幂等键异内容,查询原决定,不换ID绕过 | -| RESOURCE_EXHAUSTED | 按受控退避/原截止反压;先确认是否已accepted,不重复占额度 | -| UNAVAILABLE / DEADLINE_EXCEEDED | 结果可能已发生;查询/对账,仅对获批幂等操作重试 | -| CANCELLED | 取消本次RPC等待不等于停止已获授权的通话;业务停止走明确控制命令 | -| NOT_FOUND | 查询目标未找到,不足以证明从未拨过/未上传;结合中央事实和持久文件判断 | - -传输错误码不直接映射SaaS业务reason_code,后者仍由正文契约及事实决定;已接受长任务通过后续报告完成,不持有一个长时间阻塞RPC。 - -## 8. Unary职责目录与首发子集(内部草案) - -字段组均为数据需求,不是已批准的字段编号。对已有HTTP/MQ结构用源契约引用,不重写第二套结构。 - -**P1保留R01–R03、R05、R07–R13的实际职责;R04/R06在线改配延后。** 方法数不作为交付指标,G0可批准合并简单职责,但不能省略许可/控制/查询/事实持久确认/录音交接,也不先生成未用服务空壳。R03用于初次绑定后的策略/版本引用;静态制品由受控部署入口提供,不依赖R04/R06才能启动。 - -| ID / 方法职责 | 方向 | 请求数据 | 响应与副作用 | -| --- | --- | --- | --- | -| R01 GetAgentStatus | D→A | 已配置Endpoint目标、请求关联;激活前只允许Dispatcher身份做受限探测 | 返回boot/版本/能力/采样/加载快照;不会发起电话、不回凭据 | -| R02 ActivateAgent | D→A | D确定的agent/cell绑定、boot、会话代次、受限会话凭据/期限、协议选择 | A校验目标/本地既有归属,保存会话;D持久绑定后才可取敏感配置;冲突或旧boot隔离 | -| R03 GetBootstrap | A→D | 群组mTLS+激活后节点会话、已知运行配置版本 | 未激活只返回pending/非敏感兼容信息;已激活返回本Agent运行策略、SIP/AI期望版本索引、OSS策略引用,不返回全局凭据库 | -| R04 ApplyRuntimeConfig(后续) | D→A | 不可变运行配置版本/摘要/前置版本、适用Agent/boot/会话;受限凭据引用或密封交付 | 校验、原子应用、报告结果;配置不合法/依赖不可用则not-ready,不覆盖最后已确认可用版本 | -| R05 SetAdmissionState | D→A | 受影响trunk/资源范围、发布/维护屏障标识、关闭/开放条件、目标代次 | 关闭新准入并报告许可/预留/拨号/振铃/已接通/未知占用;开放需D确认,不是SaaS任务控制事件 | -| R06 ApplyTrunkConfig(后续) | D→A | Publication原结构:mode/cell_id/trunk_id/revision/expected_local_revision/config/config_sha256,加已冻结内部屏障关联 | SDK/生成器校验和实际加载;沿用Acknowledgement/State语义,未知不假applied;缺屏障拒绝破坏性reload | -| R07 Execute | D→A | 正文call.execute原版本JSON、已持久call/attempt/通道关联、唯一归属/资源许可、路由及配置版本引用 | 接受/拒绝;不等待整通话;同execution不重拨,ARI响应丢失转对账 | -| R08 GetExecutionPermit | A→D | 原执行/attempt、配置/控制/会话版本及许可关联 | D核验控制/时效/完整配额后给有界最后许可或拒绝;所有发出许可纳入pause/发布屏障,不能重复退款/发额度 | -| R09 ApplyTaskControl | D→A | 原ControlRequest语义、task/租户目标、requested revision、持久控制命令及授权策略 | accepted/applying;真正屏障/挂断确认后回报applied;pause/drain保留已拨出/振铃及已接通的原生命周期,stop hangup另验权限 | -| R10 QueryExecution | D→A | 原执行/通道关联或受限分页对账请求 | 返回Asterisk观测、执行文件/未交付资产状态及证据时间;通道不在当前列表不证明从未拨过 | -| R11 ReportExecutionEvent | A→D | 稳定fact标识/内容摘要、执行/通道归属、观测时间、来源序列、事实类别及源业务数据 | D事务去重并生成/关联权威MQ事件,成功回持久接收结果;调用方不指定aggregate_version跳过D裁决 | -| R12 RequestUpload | A→D | 绑定执行的资产类别/稳定ID、size/checksum及源录音元信息;同一资产的显式重试申请(新15分钟 token) | D依据自身OSS配置文件,经SDK提供临时TOKEN及受限目标/headers/期限,A不持长期凭据;不申请SaaS业务会话;原请求重放返回原授权及原期限,新显式请求才可重新签发。text_archive分支在GAP-02冻结前拒绝,不伪装录音 | -| R13 CompleteUpload | A→D | 原绑定录音/上传、实际文件元信息与完成事实 | D同事务保存事实和recording.uploaded outbox;原通知可靠进入指定durable队列后返回完成,MQ未确认时保留恢复状态;不等待SaaS回复、不返回OSS ID;text_archive仍受GAP-02门禁 | - -P1的R05/R09及静态维护必须校验目标/版本并收敛R08许可,不长期锁SQLite等网络。未来R06同样纳入屏障;“全部Unary”或“静态配置”都不等于无需业务屏障。 - -### 8.1 内部事实与外部事件映射(草案) - -下表列全R11/R13需要承载的事实种类;名称只是草案标签,不新增RabbitMQ event_type。Agent报告事实,Dispatcher裁决全局状态/版本。 - -| 类别 | 来源/入口 | 必须关联的数据 | Dispatcher输出 | -| --- | --- | --- | --- | -| 执行接收/拒绝 | R07结果及R11 | 原命令/执行/意图/Agent/boot、是否已持久接收、拒绝原因 | 更新内部投递状态;按命令状态机发command.result,意图已建立的失败不伪造无call拒绝 | -| 通话阶段观测 | ARI→R11 | call/attempt/通道、线路/Cell/出口快照、观测阶段/时间及证据 | call.status;不能仅凭本地计时报告dialing/answered | -| 通话终态 | ARI/执行器→R11 | 原执行/通话/attempt、终止来源/原因/时长、未决资产 | 对账后call.finished及对应command.result,后处理不阻塞终态 | -| 最终/中间文字与播放 | ASR/AI→R11 | turn/segment/revision/text/final/播放证据及时间范围 | transcript.updated;不把生成当已播放 | -| 文字失败 | R11 | 通话、受影响段/原因/可恢复性 | transcript.failed | -| 拒绝再联系 | 获批判定→R11 | 通话/任务/成员、请求时间/段关联 | contact.opt_out,及时驱动SaaS禁发/任务屏障 | -| 控制屏障/挂断进度 | R09/R11/查询 | command/task/revision、目标范围、旧许可与各阶段占用、挂断事实 | D汇合所有必要目标后才command.result applied | -| 配置加载/失败/恢复 | P1静态部署后R01/R11;后续R04/R06 | 静态制品/目标关联、Agent/boot、desired/applied/revision/hash、实际加载证据 | P1留存加载证据/AgentStatus;在线管理发布回执后续,不新增SaaS业务事件 | -| 资产上传/通知/失败 | R12/R13及失败R11 | 绑定录音/上传、文件封口/size/checksum、对象位置和通知事实 | recording.uploaded/failed;成功以可靠MQ入队为准,文本归档扩展受GAP-02限制 | - -健康采样走R01,不把每次心跳作为持久业务MQ事件。节点移除要先保留受控只收尾状态直到原执行/资产对账完成;强制移除需显式人工恢复路径,不能一删Endpoint就丢弃待交付事实。 - -## 9. Agent状态数据字典(内部草案) - -通过R01周期查询;样本时刻、接收时刻、采样窗口及缺失原因都记录。指标缺失是unknown,不填0。gRPC health SERVING只表示服务能响应,不能替代可拨号判断。下表为能力目录:P1只冻结身份/boot、协议、准入所需CPU/内存/FD/媒体/spool资源、静态applied版本、线路与本模式AI健康;完整IO/负载历史、staged发布态及大清单分页按需后续建设,不能让非准入遥测缺失阻塞整个首发。 - -| 数据组 | 字段需求/语义 | 调度用途 | -| --- | --- | --- | -| 身份与会话 | 预配置agent_id/cell_id、boot_id、会话代次、启动时间、最后采样序列 | 拒绝错节点/旧boot/乱序覆盖;发现重复实例时隔离新准入 | -| 软件 | 二进制版本/构建提交、gRPC协议/功能能力、配置Schema/生成器版本、Asterisk版本及镜像标识(可核验时) | 不兼容禁止调度;同项目更新不要求两角色同时瞬间升级 | -| 整机 | OS/架构、CPU核数/使用率、load1/5/15、内存可用/进程RSS、FD使用/上限、磁盘与spool容量/IO | 拒绝过载而非越配;区分宿主机、容器/cgroup和进程口径 | -| 媒体资源 | 媒体端口总量/已分配/可用、已知通话阶段数、RTP丢包/抖动/包率、ARI连接与事件滞后 | 资源不足/证据过期则跳过,不能把CPU空闲当可无限拨号 | -| 配置 | 每provider/trunk的desired、staged、applied版本/摘要、加载确认时间、状态/错误/发布屏障 | 匹配本次路由的精确已加载版本;pending/failed/漂移不接新任务 | -| SIP线路 | 受配的provider_id/trunk_id、transport/codec能力、注册是否适用及状态、受控健康观测、出口/白名单核验状态 | “支持协议”与“已配置且获授权供应商”分开;不把不需注册线路当注册失败 | -| 凭据 | 仅引用/版本/可用性/到期信息,不返回密钥正文 | 过期/不可解析阻止新准入并告警 | -| AI与资产 | 可信agent_version缓存可用性、ASR/LLM/TTS依赖健康;待上传条数/字节/最老年龄、最近失败/重试 | 按模式必需资源与spool门禁;未启用能力明确标注,不把Mock变real | -| 就绪 | registering/bootstrapping/ready/draining/degraded/offline/quarantined等内部状态及原因 | 最终是否调度由D以权威配额、最后许可及新鲜状态综合决定 | - -使用gopsutil/标准库/ARI SDK,不自写/proc解析器;不读取不必要的用户环境、设备序列号或完整配置秘密。P1仅回传有界静态供应商清单/必要状态,不先建分页服务。新鲜度使用D接收时间,Agent壁钟仅作观测,偏差按既有profile保护;ASR-only不会因未用的LLM/TTS离线变not-ready,完整模式不可缺任何必要依赖。 - -## 10. 文件、事件和OSS交付 - -### 10.1 Agent文件最小集合 - -每个执行/资产有受控目录与元信息文件、文字追加文件、音频临时/封口文件、待回报事实及上传进度。字段记录原tenant/execution/call/attempt关联、内容摘要、是否封口/验证/已被D持久接收、下一步恢复动作;目录名不得直接拼任意tenant_key/外部路径。 - -关键元信息先同步到盘后原子替换,文件单写者、追加记录尾部可识别,不把Flush当Sync;不创建Agent SQLite、通用数据库或自造消息中间件。重启扫描恢复原事实/通知,不重跑originate,也不自动重新PUT;失败或过期上传必须显式重新申请。录音用现成Asterisk/音频能力,不手写WAV头。 - -### 10.2 录音时序 - -1. 接通开始流式记录实际双向音频;实时文字同时走R11,不等资产封口。 -2. 完成/取消时正确封口;故障时保留完整段并明确不完整状态。 -3. A调用R12向D领取临时上传TOKEN;D按自身OSS配置文件提供固定15分钟的受限授权/目标。配置缺失/无效明确失败,保留原文件;不向SaaS取配置/TOKEN,也不申请上传会话或资产登记。 -4. A按指定目标/headers直传OSS,不持长期凭据;TOKEN失效仅显式向D重新申请,对象ID/内容绑定不变,不自动续期/重传。 -5. A成功PUT后调用R13;D事务保存原上传事实和recording.uploaded outbox。持久消息进入指定durable队列/绑定、mandatory无return且publisher confirm成功后,才完成本项目交付;不新增VERIFYING,不等待SaaS处理或OSS ID。 -6. A只有取得“D持久接收”还不够立即删文件,仍须满足原通知可靠入队、无未决恢复和至少24h测试保留条件。MQ故障或确认丢失只恢复原消息身份的通知,不重新PUT、新建资产或重拨。 - -### 10.3 文本归档 - -P1保留本地文字恢复文件和实时transcript.updated;文本OSS归档延后,是额外资产,不替代实时文字和opt-out。 - -需要SaaS另定义文本归档授权/complete/引用:资产类型、ID、JSONL或其它格式、编码、内容清单/哈希、segment版本、完整性/失败、保留和查询权限。**现有recording接口没有这些定义,不能用wav/recording_id伪装。** 未冻结前明确“文本OSS归档未启用”,不影响已批准的文字MQ链路;也不宣称该需求已实现。 - -## 11. 断连、重复与乱序处理 - -| 故障 | 必须执行 | 禁止行为 | -| --- | --- | --- | -| Execute超时/响应丢失 | 原ID查D事实及Asterisk/文件,必要时reconciling;SDK不能无条件重试副作用 | 换execution/attempt/Cell重新拨号 | -| ReportEvent回包丢失 | A重报同fact;D关联原event_id及版本 | 新建一个语义重复MQ事件 | -| D不可达 | A停新执行,已有获授权通话按原策略继续、文本/录音/结果落盘;恢复补报 | 因断RPC就伪造call.finished或直接释放未知占用 | -| A新boot/失联 | D保留未知占用,重新绑定前对账、核验配置/许可;不自动迁移活动通话 | 看到新boot的0通话就清旧配额 | -| 配置部分成功 | 阻塞受影响资源、保留真实installed快照、对账/回滚也要确认 | 只改desired/active指针就重新ready | -| OSS成功但complete/回报丢失 | 原资产/会话幂等恢复;防旧签名覆盖verified对象 | 新建另一份资产或重拨 | -| D恢复较旧SQLite备份 | 停新准入、恢复唯一所有权、比对A/MQ/资产事实再开放 | 恢复过期许可/遗漏幂等水位后直接运行 | -| 永久丢盘 | 报明确资产/文字失败与告警,仍对账SIP副作用 | 假称文件可恢复或用合成内容替代 | - -## 12. P1静态发布与后续在线发布 - -P1数据流:管理平台审批不可变制品 → 核验来源/版本/哈希及单 Cell 授权 fixture → D/R05关闭受影响资源新准入、收敛许可/占用 → 维护窗口由受控部署入口原子交付/加载或重启 → R01/R10及Asterisk实际加载证据核验 → D恢复满足条件的资源准入。初装也先核验再ready;有未知占用不得跳过屏障,失败/人工恢复均重新核验,不靠旧active指针自动开放。 - -`Publication`和`Acknowledgement/State`见字段索引。GAP-03只先批准P1静态制品/加载事实所需适配:精确cell/trunk、revision、config_sha256、来源和唯一写入路径;不私改现有Schema、不假称在线API已支持。凭据仍用credential_ref受控解析,不能塞入MQ。 - -未来在线发布才增加D持久发布编排、R04/R06推送、全目标回执/动态新增移除/自动回滚。P1静态节点清单的人工变更和凭据轮换仍须维护/排空/重激活;稳定SIP配置不逐呼改写。配置变更不自动授权真实测试呼叫或额外注册探测,管理平台30个业务API不搬入Dispatcher。 - -## 13. 缺口与冻结责任 - -[G0开发准备与契约冻结方案](../architecture/G0开发准备与契约冻结提案_v0.1.md) D01–D10方向及模式/许可/恢复机制已获用户确认;下表仍跟踪尚未交付的源字段/合同和验证,不再表示已确认方向待用户审批。该文档不是第二套Schema,权威源发布并验证后才关闭相应GAP。 - -D07补充确认:**OSS配置存于D配置文件,Agent经R12向D领取固定15分钟临时上传TOKEN后直传OSS**;SaaS不下发OSS配置/TOKEN,D保留SDK签发能力,不转发文件。Agent成功PUT后经R13报告原上传事实,D持久保存recording.uploaded outbox并以可靠入队完成本项目交付;不申请SaaS会话、不等待complete/verified/OSS ID,不自动续期,Agent不持长期凭据。 - -| ID | 缺口 | 文档处理/退出条件 | -| --- | --- | --- | -| GAP-01 | 信封已对齐,但8种事件payload专属Schema及部分条件规则未完整机读化 | 在上游唯一生成源补齐并验正反例;未覆盖部分阻塞冻结/业务上线,不能以object校验冒充完整验收 | -| GAP-02 | D的OSS配置文件/TOKEN约束及上传事实通知;文本归档仍缺合同 | 配置/TOKEN由D提供而非SaaS;核验配置格式、SDK及UploadGrant映射、显式重申请、对象定位、实际size/checksum和recording.uploaded可靠入队;不等待SaaS处理;文本归档延后 | -| GAP-03 | 静态制品交接与后续在线管理发布适配 | P1先批准静态版本/哈希/目标/来源/加载事实及唯一写入合同,旧直写停用;完整在线发布/回滚和R04/R06延后 | -| GAP-04 | 首发Unary及身份/许可/状态结构尚无批准Proto | P1冻结R01–R03/R05/R07–R13实际职责、字段/错误/幂等/大小/超时;可获批合并,R04/R06不先造空框架 | -| GAP-05 | 共用证书的单节点授权与全组泄露风险 | 保留用户共用证书决定,但必须有受控Endpoint、独立D身份、自动节点会话、重放隔离及全组轮换/撤销演练;不能宣称节点级证书隔离 | -| GAP-06 | 首发资源保护、模式能力、单 Cell 授权/限额与维护窗口 | P1登记受限profile及基础恢复条件;缺必需能力拒绝,低负载不越额;SIP 外呼增加 Asia/Shanghai `09:00`–`20:00` 时间门禁。复杂评分/滚动升级后续 | -| GAP-07 | 单活D故障/人工恢复目标与永久资产损失 | P1核验唯一所有权、SQLite备份/恢复/对账、文件损失和RPO/RTO;跨机自动HA后续,不新增PG/NFS共享 | -| GAP-08 | 当前AI Schema强制llm/prompt/tts/asr/conversation且无明确ASR-only表达 | P0由上游批准两模式选择/缺省、条件必填、资源/超时及文字播放/失败语义并生成校验;不增临时MQ字段、不伪造LLM/TTS配置,两种真实模式都通过才可P1签收 | -| GAP-09 | SaaS任务AI配置的读取归属/授权、provider_ref解析、调试参数和有效快照尚未完全机读化 | AI配置/授权由SaaS经MQ响应原D,保留租户/不可变版本绑定,旧AI GET不再作为接入;在唯一源补§6.2实际参数、单位/默认/范围/能力、缓存撤销及摘要/Unary交接规则;锁定百炼/火山ASR、OpenAI兼容LLM、火山TTS参数映射PoC。不加临时路径或任意透传,SDK缺字段先补库/替代 | - -**GAP-10(本轮新增,已确认方向、未冻结机读合同)**:全MQ请求响应、D全局唯一身份及生命周期、独立Topic/队列/绑定、租户/目标/来源/原请求关联、完整路由预算/通配符边界、错误/期限/重复/迟到/重启恢复,以及R12/R13有界异步衔接。由W01/W02发布新版Schema/拓扑/正反例并核验后解除;不得修改旧不可变包或把图中的中文名称当新枚举。GAP-10是当前P1门禁,不新增88项编号,细则纳入既有C/S/E/L子场景。 - -具体验收见 [验证与切换验收](../acceptance/验证与切换验收_v0.3.md) §1.1–§1.2。GAP-01/05/07/08/09及GAP-02/03/04/06的P1部分均为首发门禁;延后部分只在相应功能启用前冻结,不能记为通过。“文档齐全”不等于契约已获批。 diff --git a/docs/evidence/saas-dispatcher-p07-audit.md b/docs/evidence/saas-dispatcher-p07-audit.md index 5f5042e..3714f10 100644 --- a/docs/evidence/saas-dispatcher-p07-audit.md +++ b/docs/evidence/saas-dispatcher-p07-audit.md @@ -1,5 +1,7 @@ # P07:唯一现行入口残留与合法例外核查 +> 本表记录 P07 当时状态,不是当前接口导航。当时的计划现存于 [`../archive/plan-saas-dispatcher-completed.md`](../archive/plan-saas-dispatcher-completed.md);两份当时受保护的旧文档已按原字节归档。唯一现行规范见 [`../thirds/saas-dispatcher.md`](../thirds/saas-dispatcher.md)。 + 范围:分支 `feat/saas-dispatcher-contract`,实施基线 `f5c2d6a92036a579e0b070beb1d9381b0976f81e`;逐批清理和测试事实见 [`saas-dispatcher-implementation.md`](saas-dispatcher-implementation.md)。本表只确认**项目内隔离 Mock 的现行入口与历史隔离**,不证明 SaaS、management、真实 MQ、OSS/AI 供应商、Asterisk、SIP、ECS 或生产已验收。两份使用者修改的 `docs/thirds/v0.4.md`、`docs/thirds/第三方对接事件与请求消费顺序_v0.1.md` 未暂存、覆盖或清理。 | 范围 | 残留核查及可复核结果 | @@ -9,7 +11,7 @@ | SQLite/恢复 | `internal/store/` 没有旧 migration SQL;`Store.Open` 在写表前检查既有表和 `user_version`,旧/残缺布局保留原数据并拒绝,测试包含未知占用、会话/结果/上传 outbox 故障与重启。Agent 旧 `.executions`、`.uploads`、`.upload-locks` 及执行 `state.json` 的检查只读失败关闭;未自动清空、迁移、重拨或将未知 PUT 当失败重传。本次只删除无人调用的源码与样例,没有处置任何用户存量文件。 | | Proto 与 Agent | `proto/agent/agent.proto` 的 `AgentControlService` 恰好八个获批 RPC:状态、激活、获批执行/控制、已加载 SIP、录音授权、结束、结果;`scripts/check-proto.sh` 校验 `gen/` 与 `proto/manifest.json` 来源/生成 hash。`agent.v1` 是仍在使用的**内部通信协议标识**,没有旧业务 RPC 兼容服务。 | | MQ、HTTP 与脚本 | 五类只读 HTTP 来自当前合同;控制/任务队列由 SaaS 预建、Dispatcher 无 configure 权限;结果进入同一 durable 队列,mandatory/return/confirm 与原身份恢复按当前 schema/拓扑测试。`scripts/check-current-mq-mock.sh` 明确要求 `TestBrokerSharedResultQueueAndNoConfigure`、`TestRuntimeIsolatedControlBacklogExecuteAndSharedResult`、`TestDispatcherCommandStartsWithIsolatedMQHTTPAndAgent` **三项均实际 PASS**,匹配零项不得算通过。当前 release/build 检查绑定本地合同、MQ 拓扑、上游历史来源和 Proto manifest 的 SHA-256;`production_approval=false`,不拨号、不部署。 | -| 文档、部署及链接 | `AGENTS.md`、根 `README.md`、`docs/README.md`、[`../plan-saas-dispatcher.md`](../plan-saas-dispatcher.md) 只把 `docs/thirds/saas-dispatcher.md`、当前机器合同和本证据作为当前入口。12 份当前/归档索引中的 Markdown 相对链接全部解析成功;旧部署 JSON 原字节归档并以来源路径/SHA-256 映射,物理部署说明明确当前制品 Mock-only。原计划 `docs/plan-saas-dispatcher-v05-v0.1.md` 仍在原路径且 SHA-256 为 `666f39e56ea9f4b55661efcac82edd6f9729848e2d60e5f24cdf5aa3ac97ee87`,由当前 `contracts/local/manifest.json` 固定为历史来源,不改写为新计划,也不保留旧运行入口。 | +| 文档、部署及链接 | `AGENTS.md`、根 `README.md`、`docs/README.md`、[`../archive/plan-saas-dispatcher-completed.md`](../archive/plan-saas-dispatcher-completed.md) 只把 `docs/thirds/saas-dispatcher.md`、当前机器合同和本证据作为当前入口。12 份当前/归档索引中的 Markdown 相对链接全部解析成功;旧部署 JSON 原字节归档并以来源路径/SHA-256 映射,物理部署说明明确当前制品 Mock-only。原计划 `docs/plan-saas-dispatcher-v05-v0.1.md` 仍在原路径且 SHA-256 为 `666f39e56ea9f4b55661efcac82edd6f9729848e2d60e5f24cdf5aa3ac97ee87`,由当前 `contracts/local/manifest.json` 固定为历史来源,不改写为新计划,也不保留旧运行入口。 | ## 已核实的名称例外(均不是旧业务分支) diff --git a/docs/evidence/saas-dispatcher-p08-acceptance.md b/docs/evidence/saas-dispatcher-p08-acceptance.md index 07e4875..27cf77d 100644 --- a/docs/evidence/saas-dispatcher-p08-acceptance.md +++ b/docs/evidence/saas-dispatcher-p08-acceptance.md @@ -1,6 +1,8 @@ # P08 项目内验收对照 -本记录按 `docs/plan-saas-dispatcher-v05-v0.1.md` §3.1、§6 核对 P01–P08、K01–K16 和 A01–A12。**范围仅限单节点、单 Dispatcher、单 Agent、单 Cell、单租户的隔离 Mock。** 本机证书、RabbitMQ 容器和 OSS/AI 模拟服务不代表 SaaS 应用收讫、供应商验收或真实拨号。原计划是来源事实,不是第二份当前运行合同;当前入口是 `docs/plan-saas-dispatcher.md`。 +> 本表保留 P08 当时的本地测试结果、来源路径与哈希;后续整理未改写验收事实。原计划现位于 `docs/archive/sources/plan-saas-dispatcher-v05-v0.1.md`,完成阶段计划位于 `docs/archive/plan-saas-dispatcher-completed.md`;**当前唯一规范**见 [`../thirds/saas-dispatcher.md`](../thirds/saas-dispatcher.md)。 + +本记录按 `docs/plan-saas-dispatcher-v05-v0.1.md` §3.1、§6 核对 P01–P08、K01–K16 和 A01–A12。**范围仅限单节点、单 Dispatcher、单 Agent、单 Cell、单租户的隔离 Mock。** 本机证书、RabbitMQ 容器和 OSS/AI 模拟服务不代表 SaaS 应用收讫、供应商验收或真实拨号。原计划是当时的来源事实,不是第二份运行合同;当时的状态入口 `docs/plan-saas-dispatcher.md` 已归档。 ## 可复现的本地门禁与来源 @@ -44,7 +46,7 @@ | A09 结果与录音 | 下方五路径和额外的恢复/窗口测试;`TestFinalResultNeedsConfirmedEndAndRemainsExactlyOne`、`TestFinalResultPayloadNoAnswerHasNoFabricatedTranscriptOrAsset`。48 小时到期保留待人工、不发假结果。 | 模拟 HTTPS 对象接收不等于供应商 OSS 成功。 | | A10 数据保护与恢复 | `TestRecordingRecoveryDiscoversAndResumesAfterProcessRestart`、`TestRecordingRecoveryUnknownPUTNeverRetriesAfterRestart`、`TestFinalResultOutboxFailureRollsBackAndCanResume`、`TestFinalResultRestartResumesSameOutboxBody`;会话代际竞态测试见下表。旧 SQLite 与旧 Agent 状态只读失败关闭,未自行清理。 | 未保存恢复双文件时进程崩溃不保证零丢失;真实主机重启未验。 | | A11 测试与构建 | TDD:新增会话/录音测试先红后绿;上述 `make check`、72.0% 覆盖率及本地发布检查通过。`TestNonprodCallEvidenceFailsBeforeDialWithoutRequiredGates`、`TestNonprodPreflightRejectsIncompleteCapturedEvidence` 以临时假工具验证缺资源、缺诊断/抓包事实即失败关闭。 | 假工具不能替代实际 ECS、Asterisk/systemd、ARI/PJSIP、拨号前 SIP/RTP 抓包及收尾哈希。未经另行授权不执行这些现场动作。 | -| A12 交付边界 | 唯一当前入口 `docs/plan-saas-dispatcher.md`、`docs/thirds/saas-dispatcher.md`;本表分开本地证据和外部缺口。`production_approval=false`;已有业务数据未自动处置。 | 不宣称第二 Dispatcher、第二 Cell/租户、多机、容量、真实线路或生产通过。 | +| A12 交付边界 | 验收时的状态入口为 `docs/plan-saas-dispatcher.md`,业务规范为 `docs/thirds/saas-dispatcher.md`;前者现已归档,本表仍分开本地证据和外部缺口。`production_approval=false`;已有业务数据未自动处置。 | 不宣称第二 Dispatcher、第二 Cell/租户、多机、容量、真实线路或生产通过。 | ### A09 五条录音路径及错误边界 diff --git a/docs/thirds/saas-dispatcher.md b/docs/thirds/saas-dispatcher.md index 5879c88..565c370 100644 --- a/docs/thirds/saas-dispatcher.md +++ b/docs/thirds/saas-dispatcher.md @@ -1,6 +1,6 @@ # SaaS ↔ Dispatcher:项目内唯一现行通信约定 -> 本文根据用户提供的 `v0.5-proposal.md` 及已确认的 K01–K16 整理为可校验的项目内合同;原提案含注释、排版错误及被后续确认取代的旧队列/租户字段。项目内 Schema 与合法/非法 JSON 的唯一机器来源为 [`contracts/local/`](../../contracts/local/);不得从本 Markdown 复制第二套手写 Schema。**本地隔离 Mock 可验收,不等于 SaaS/management 已签收或真实外呼获授权。** +> 本文是 SaaS↔Dispatcher **唯一当前人类可读规范**,合并了已确认的业务规则、第三方交互及本地验收边界。字段、路由、正反例的唯一可执行依据仍为 [`contracts/local/`](../../contracts/local/);内部 RPC 以 [`proto/agent/agent.proto`](../../proto/agent/agent.proto) 为准,不在 Markdown 复制第二套 Schema。历史提案与批准记录保留在 [`archive/sources/`](../archive/sources/README.md),不构成并行版本。**P01–P08 仅完成项目内隔离 Mock 验证;真实 SaaS/management、供应商与生产均未签收,本文不授权真实外呼。** ## HTTP:五类只读配置 @@ -44,7 +44,13 @@ RabbitMQ 是 Topic,**SaaS 独占创建、绑定、退役 exchange/queue,D 3. 从首次两文件完整保存起重试:间隔为 1、2、4、8、16、32、60 分钟,其后每 60 分钟一次;重启/失败不重置起点,SDK 默认自动重试不得改变节奏。每次经 D↔A RPC 显式领取有效上传授权,不换对象/不向 SaaS 申请 TOKEN。48 小时仍不成功:停止自动重试、保留两文件和进度、标记待人工,**不发**伪造 uploaded/unavailable 或最终结果,也不自动重开窗口。 4. 上传成功后仅恢复原最终结果消息的 MQ 可靠交付,MQ 失败不重新 PUT、不新建资产、不重拨;48 小时是 OSS 重试窗口,**不是** MQ outbox 的清除期限。已确认结束的通话资源及时释放,不等待 OSS/MQ;未知执行不释放。进程在正常上传尚未成功、失败恢复两文件尚未完整保存前退出,内存录音可能丢失,不能声称零丢失,也不能为了隐藏限制悄悄预写盘。 -## 校验、来源与界限 +## Agent RPC、会话与恢复边界 -- 字段及结构的唯一机器契约:[`config-read.schema.json`](../../contracts/local/config-read.schema.json)、[`task-discovery.schema.json`](../../contracts/local/task-discovery.schema.json)、[`mq.schema.json`](../../contracts/local/mq.schema.json);正反例在 `contracts/local/examples/`,来源/hash 在 `contracts/local/manifest.json`,检查入口 `go test ./contracts -run TestCurrentContractExamples` 及 `scripts/check-current-contracts.sh`。外部供应商仍未签收。 +- D↔A 只采用预绑定身份和双向 TLS Unary gRPC;当前八个方法见 [`agent.proto`](../../proto/agent/agent.proto)。Agent 不直连 SaaS、没有业务数据库;D 的 SQLite 持久保存任务/额度/inbox/outbox,Agent 执行及上传恢复文件私有。旧 SQLite/Agent 状态发现后只读失败关闭,不自动删除或迁移;未知通话占用不得因超时、新 boot 或重启自行释放。 +- Agent 会话提前续期;仅对**已验证 mTLS、同 Agent/Cell/boot/Dispatcher epoch、相邻且仍有效的代际拒绝**,`RequestRecordingUpload`、`ReportCallEnded`、`ReportCallResult` 可在六秒或调用方更早期限内,沿原事件/operation/幂等键重报原事实。其它拒绝、超时及结果不明停止重报,不自动重拨、重传不确定 OSS PUT 或放宽最新代际栅栏;详细错误边界见 [`proto/ERRORS.md`](../../proto/ERRORS.md)。 + +## 校验、来源与验收界限 + +- 当前字段及结构:[`config-read.schema.json`](../../contracts/local/config-read.schema.json)、[`task-discovery.schema.json`](../../contracts/local/task-discovery.schema.json)、[`mq.schema.json`](../../contracts/local/mq.schema.json);正反例在 `contracts/local/examples/`,来源路径和 SHA-256 在 [`manifest.json`](../../contracts/local/manifest.json)。历史原件按原字节存入 [`archive/sources/`](../archive/sources/README.md),旧上游 v1 存入 [`archive/upstream/`](../archive/upstream/README.md);它们不嵌入当前运行合同,不提供回退入口。`make check` 校验当前合同、历史来源、Proto、格式、race、vet、构建及隔离 MQ 实际收件。 +- 已完成的 P01–P08、A01–A12、K01–K16 项目内证据见 [`P08 对照`](../evidence/saas-dispatcher-p08-acceptance.md);本地手写代码覆盖率为 72.0%,发布清单 `production_approval=false`。本地 Mock、假工具诊断和 SHA-256 不证明真实 SaaS/management/MQ 应用收讫、OSS/AI/SIP/Asterisk/ECS、现场抓包或生产已通过。外部真实接入、部署和拨号均需另行授权、签收和验证。 - JSON 样例是隔离 Mock 虚构数据;`example-only-not-a-real-secret` **不是凭据**。严禁将真实凭据、完整用户音频或完整对话放入源码/日志/证据。运行时须按接入方权限与实际加载事实再核验,不以机器 Schema 通过取代拨号授权。 diff --git a/docs/thirds/v0.2.md b/docs/thirds/v0.2.md deleted file mode 100644 index 60cfd24..0000000 --- a/docs/thirds/v0.2.md +++ /dev/null @@ -1,899 +0,0 @@ -- 本文是新版项目内字段与状态语义的说明;v0.1 文档及证据仅留历史,不作为新版运行契约。四条拟定 GET 的响应字段、路径和外部兼容性尚待真实 SaaS 核对。SIP 新版机器校验为[配置读取 v0.3 Schema](../contracts/config-read-v0.3.schema.json)(旧 SIP v0.1 Schema 保留历史);其他配置字段仍沿用原合同。机器校验文件另有[原配置读取 Schema](../contracts/config-read-v0.1.schema.json)、[任务发现 Schema](../contracts/task-discovery-v0.2-proposal.schema.json)、[命令/控制 Schema](../contracts/command-next-v0.1-proposal.schema.json)、[最终结果 Schema](../contracts/call-result-v0.1-proposal.schema.json),队列拓扑为[MQ 拓扑文件](../contracts/mq-topology-v0.1-proposal.json)。它们均为项目内版本,不修改现行上游 v1 契约。 -- 每个 SaaS↔D JSON 消息体按 UTF-8 序列化后最多 **8,388,608 bytes**。超限结果保留在持久 outbox,标记 `blocked_payload_too_large` 并记录 event_id/字节数/SHA-256;不发布、不截断、不拆分、不丢弃,需由显式版本变更处理。 -- 对已接纳且预期有录音的通话,上传阶段最迟在 `call.ended_at + 15m` 收口;OSS 成功发送 `uploaded`,明确 PUT 失败立即发送 `unavailable`,仍无确定结果则到期发送 `unavailable`。授权固定 15 分钟;每个录音/upload_id/object_key 组合最多一次 PUT。授权在 PUT 前过期时,Agent 可在上述截止时间内显式向 D 为同一 upload_id/object_key 重新申请授权;不自动续期或创建第二份资产,任何已发起 PUT 都不得重试。确认未产生录音的 `not_created` 立即收口。`call.result` 只生成一次,MQ 重投复用原 event_id。 -- 控制按 task 串行处理,并核对最新任务状态:pause 只接受权威状态 `paused`,resume 只接受 `running` 且本地未 stopped,stop 只接受 `stopped`;乱序/不一致时保持准入关闭并拒绝,stopped 不可逆。控制本身无 command_id/expected revision,不按消息身份去重,重复动作只保持状态幂等。 -- SaaS 先持久 stopped 并停止向任务队列发布,再发 stop。D 持久屏障后静默 ACK 全部未接纳积压,已接纳通话继续按策略收口;仅在任务队列排空后发 `task.control` 的 stopped/applied 回执。SaaS 收到该回执后才可删除队列/绑定;离线或无回执时保留队列。队列只由 SaaS 创建/删除,D 不声明、不绑定、不删除;任务发现 `removed` 只在该退役顺序之后发出,D 清配置但保留执行恢复和结果 outbox。 -- 每个 D 最多允许 256 个仍归属或正在退役的任务队列;task_id 仅允许 ASCII `[A-Za-z0-9_-]{1,128}`,精确命名与最大字节数见 MQ 拓扑文件。`tasks` 每次完整返回,不分页,单 D 最多 256 个归属或正在退役的任务;增量变更一次完整返回且有相同的 256 条上限,超限返回 HTTP 410 `cursor_expired`、不得截断。`cursor` 是不透明变更水位;只有完整响应和任务归属已原子持久化后才推进。游标过期返回 HTTP 410 和 `cursor_expired`,D 关闭新准入并重新取完整快照。轮询周期 30 秒不是端到端发现 SLA。 - -## 1. 触发顺序 - -| 顺序 | 请求与触发 | SaaS 处理/返回 | -| --- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | -| 1 | D 启动或配置到期,带 `X-DISPATCHER-id`/`X-DISPATCHER-SECRET-KEY` 请求 `/internal/v1/dispatcher/sip`(拟定 HTTP GET)。 | SaaS 返回本 D 唯一获批版本;D 核验后才能接受新执行。 | -| 2 | D 启动/重启 `GET /internal/v1/dispatcher/tasks` 取得本 D 的任务全量快照与变更游标,运行中每 30 秒(暂定) `GET /internal/v1/dispatcher/tasks?after=`;SaaS 创建任务时先建好任务队列/绑定再发布。 | D 发现新任务后仅消费 SaaS 已创建的队列;D 离线期间消息可留在队列,任务 ID 的更新/停止也能由变更游标发现 | -| 3 | SaaS 将任务固定分配给一个 D,向该 D 投递 `call.execute`(下一版精简 payload,**非现行 Schema**)。 | D 按消息中的任务 ID 请求 `GET /internal/v1/dispatcher/task/:task_id`,核验归属 D 的任务快照(拟定 HTTP);未接纳任务可受约 60 秒配置缓存延迟影响,已接纳执行固定原快照。 | -| 4 | D 从任务取得 `tenant_id`,请求拟定 `GET /internal/v1/dispatcher/tenant/:tenant_id/quota`,与同租户其他任务共享额度后判定接纳。 | 额度缺失/过期不接新呼叫;普通接纳/拒绝有 `command.result`,**已停止任务的未接纳积压仅消费并 ACK,无逐条回传**。 | -| 按需 | SaaS 投递 `task.control` 暂停、恢复或停止(下一版草案)。 | 控制本身有回执;暂停保留积压,恢复消费原队列;停止持久生效后静默消费并 ACK 未接纳积压,不拨号、不向 SaaS 回传这些消息的结果。已在途通话仍按策略处理并给最终结果。 | -| 5 | 通话终结且录音已上传 OSS,D 投递一条本地目标事件 `call.result`。 | SaaS 只处理这条最终的通话详情,按 `event_id` 去重;录音以 `bucket/object_key` 关联,不接收文件、不提供上传会话或验证结果。上传失败/超时按 §0 和 §4.2 的 15 分钟规则收口。 | - -### 1.1 现行 MQ 地址与 JSON 字段不是一回事 - -RabbitMQ 有**发布入口 exchange → 发布时指定的 routing key → 预先绑定的 queue → D 消费**四步; - -```text -SaaS→D exchange: agent-call.dispatchers.v2 - routing key: d..t..in - binding key: d..t..in - queue: agent-call.d..t..v2 - consumer: 对应 Dispatcher -D→SaaS exchange: agent-call.saas.v2 - routing key: d..t..out - queue: agent-call.saas.events.v2 - consumer: SaaS -``` - -例如 §3.1 的 JSON 带 `dispatcher_id=c046b893-8628-4589-ae50-619d049248a6`、`tenant_key=tenant-a`,SaaS 的**MQ 发布参数**就对应 `d.c046b893-8628-4589-ae50-619d049248a6.t.tenant-a.in`;D 消费队列 `agent-call.d.c046b893-8628-4589-ae50-619d049248a6.t.tenant-a.v2`。exchange、routing key、queue 和 binding **不在 JSON 的 `payload` 中**; - -### 1.2 本轮任务队列与事件路由 - -**硬边界:所有 exchange/queue/binding 均由 SaaS 创建、维护和退役;D 只消费 SaaS 创建的任务/控制队列,并向 SaaS 创建的结果 exchange 发布,不声明、绑定或删除队列。** 现行外部 MQ v2 拓扑保持原样;本轮本地目标使用 v3 名称,完整机器拓扑见 [MQ topology](../contracts/mq-topology-v0.1-proposal.json)。 - -```text -SaaS 创建并绑定: - exchange: agent-call.dispatchers.v3 (topic, durable) - task routing: d..task..in - task queue: agent-call.d..task..v3 - control route: d..control.in - control queue: agent-call.d..control.v3 - dead-letter: agent-call.dead-letter.v3 - D -> SaaS exchange: agent-call.saas.v3 (topic, durable) - result route: d..out - SaaS result queue: agent-call.saas.d..v3 -``` - -所有业务队列 durable、非 exclusive、非 auto-delete;发布消息设 persistent、mandatory,并启用 publisher confirm。SaaS 必须先确认目标队列及精确 binding 已就绪再发布;未路由或 confirm 不成功时保留原消息,恢复后以相同身份/正文重发。D 持久 inbox 与状态提交成功后才 ACK;`call.execute.command_id` 去重并禁止二次 originate。D 的结果 outbox 只有在无 mandatory return 且收到 positive confirm 后才标记已交付;confirm 仅证明 broker 接收,不代表 SaaS 应用处理。Schema/JSON 错误在记录脱敏事实后 `nack(requeue=false)`,由 SaaS 配置的 dead-letter binding 接收;不得静默 ACK 丢弃或无限 requeue。 - -`dispatcher_id` 是小写 canonical UUID v4;`task_id` 全局唯一且仅允许 ASCII `[A-Za-z0-9_-]{1,128}`,不含点号、通配符或分隔符。每个 D 最多 256 个尚未退役的任务队列(含 stopped/draining);`tenant_key` 不进入 queue/routing key,仍按原值保留在消息中并用于额度归属。AMQP routing key 和 queue name 上限均为 255 bytes;上述 task routing key 最长 175 bytes、task queue 最长 186 bytes。`task.control` 走独立 D 控制队列;`command.result`/`call.result` 统一走 per-D result route。消息不设置 broker TTL,`not_after` 由 D 校验并明确拒绝过期命令;stopped 任务积压仍由 D 静默 ACK。 - -## 2. D ← SaaS:只读配置与任务发现 - -四个 GET 均**无请求 JSON 体**,统一使用 `X-DISPATCHER-ID`(全局唯一 D UUID)和 `X-DISPATCHER-SECRET-KEY`(HEADER 头统一转小写判定匹配)。本地 Mock 使用隔离测试凭据;真实 SaaS 地址、认证实现及轮换未验证,不阻塞本地开发。SIP 返回本 D 全量;单任务按路径中的 `task_id` 查询,SaaS 必须核对归属 D 与原值 `tenant_key`,任务发现则按 D 返回归属清单。**不使用 ETag、If-None-Match 或 304**:任务与 SIP 配置约 60 秒缓存到期时 GET 完整 200 响应,失败只停新准入,已接纳执行保持绑定快照;MQ 控制不等待配置缓存。`tasks` 的每 30 秒增量轮询另见 §2.5。 - -### 2.1 SIP 配置:200,返回本 D 的完整获批线路(项目内新版;其他 v0.2 历史内容不变) - -请求(地址/Header 仍待 SaaS 实现确认): - -```http -GET /internal/v1/dispatcher/sip HTTP/1.1 -Host: -X-DISPATCHER-id: c046b893-8628-4589-ae50-619d049248a6 -X-DISPATCHER-SECRET-KEY: <受控注入,不展示实际密钥> -``` - -完整 200 响应体: - -```json -{ - "schema_version": "config-read.v0.3", - "resource": "sip_config", - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", - "revision": 1, - "approved_at": "2026-09-21T08:00:00+08:00", - "trunks": [ - { - "trunk_id": "trunk-mock", - "provider_id": "provider-mock", - "codec": "PCMA", - "dial_prefix": "", - "enabled": true, - "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": [] - } - } - } - ] -} -``` - -**字段说明/消费动作:** - > Cell:一个外呼应用Asterisk实例(当前阶段不扩展复杂分布式,写死单实例数据,仅填充Trunk 数据列表,后期根据需求调整分布式架构); - > Trunk: 一条外呼线路; -- `schema_version/resource`:草案版本 `config-read.v0.3`、资源 `sip_config`;`dispatcher_id`:只能与发起请求的 D 相同。 -- `revision/approved_at`:本 D 获批的完整 SIP 线路版本和批准时间。每次更新均须递增 revision;同版本内容不得变化。D 持久核验同版内容不漂移,不接纳倒退版本。 -- `trunks[]`:唯一的线路列表;`trunk_id/provider_id` 定义线路及供应商,`codec` 为 PCMA,`dial_prefix` 只用于该线路,`enabled` 控制线路是否可用。`server_host/server_port/transport/auth_mode/registration_required` 为连接方式;未知传输、鉴权、注册或额度不得放行真实外呼。`max_concurrent_calls` 为分配给本 D 的线路额度;`caller_profiles[].caller_profile_id/caller_id` 为主叫引用及原值(可含 `BD`)。`schedule` 是 Asia/Shanghai 每周逐日多时段、左闭右开,空日不可呼。线路 ID 和主叫引用不能重复。 -- SaaS 只提供 SIP 连接及线路拨号约束,不下发 Agent/Asterisk 的 Cell、ARI、媒体、录音、部署制品、全局号码白名单或运行模式。白名单和部署设置由本地受控配置承担。D 只用一个 SIP `revision` 与 Agent 回报的**实际已加载 SIP 版本**核对;加载/核验失败关闭新准入,不以 HTTP `200` 或仅收到配置冒充 Asterisk 已加载。部署时确定的 Agent/Cell 身份由本地核对,不由 SaaS 控制。 - -### 2.2 任务配置:200,ASR + LLM + TTS 模式 - -请求(`task_id` 示例为 `task-mock`): - -```http -GET /internal/v1/dispatcher/task/task-mock HTTP/1.1 -Host: -X-DISPATCHER-id: c046b893-8628-4589-ae50-619d049248a6 -X-DISPATCHER-SECRET-KEY: <受控注入,不展示实际密钥> -``` - -完整 200 响应体(仅一种智能体模式): - -```json -{ - "schema_version": "config-read.v0.1", - "resource": "task_config", - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", - "tenant_id": "tenant-id-mock", - "tenant_key": "tenant-mock", - "task_id": "task-mock", - "task_revision": 2, - "status": "running", - "name": "Mock task", - "group_id": null, - "max_concurrent_calls": 2, - "ring_timeout_ms": 30000, - "max_call_duration_ms": 120000, - "route_policy_id": "route-mock", - "caller_profile_id": "caller-profile-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", - "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 - } - } - } -} -``` - -**字段说明/消费动作:** - -- `schema_version/resource/dispatcher_id/tenant_id/tenant_key/task_id`:版本、资源 `task_config`、归属 D、租户 ID、原值租户键和单任务 ID;D 必须验证请求归属并按 `tenant_id` 取得 §2.6 的租户额度。`task_revision` 是任务修订,`status` 为拟定 `running/paused/stopped/finished`;非 running 不接新呼叫。 -- `name/group_id` 是名称及可空分组;`max_concurrent_calls` 是本任务额度,不等于跨任务/跨 D 总额度;`ring_timeout_ms/max_call_duration_ms` 是任务级振铃/最长通话毫秒上限;`route_policy_id` 标识这份任务路由;`allowed_trunk_ids[]` 依次列出候选优先级,选择首条已加载、时段/额度有效且支持任务 `caller_profile_id` 的线路;`caller_profile_id` 明确主叫引用,不默认取首个主叫。无匹配项不接纳,选定后固定、拨号失败不自动换线重拨。有效通话上限取任务 `max_call_duration_ms` 与 AI `conversation.max_duration_ms` 的较小值,执行与 AI 控制器一致,不改原授权配置。精简命令不带这些业务值。 -- `schedule.time_zone/starts_at/ends_at` 定义时区和可空的起止时间;`weekly_windows` 按星期列出每日多个左闭右开 `{start,end}`,空数组禁呼;`excluded_dates[]` 为按 Asia/Shanghai 日期优先排除的日子。任务时段还须与线路时段相交。 -- `agent.agent_version_id`:不可变智能体版本,必须与 `agent.config.agent_version_id` 对应;`authorization_id/authorization_expires_at` 为授权身份和截止时间,到期不得由过期缓存继续放行。不返回 `content_sha256`,同一版本内容变化必须拒绝并要求新版本。 -- `agent.config.immutable/mode`:不可变标记及 `full_ai` 模式。`llm.provider_ref/model/temperature/max_tokens/timeout_ms` 为供应商引用、模型、采样、输出上限和超时;`prompt.text/allowed_variables/max_bytes` 为提示词、允许的变量和字节上限;`tts.provider_ref/model/voice/speed/format/timeout_ms` 为语音供应商引用、模型、声音、速度、音频格式与超时;`asr.provider_ref/model/language/input/interim/timeout_ms` 为识别供应商、可选模型、语种、输入格式、是否给出中间转写与超时;音频 `encoding/sample_rate_hz/channels/sample_width_bytes` 定义编码、采样率、声道和样本宽度;`conversation.opening/allow_interrupt/silence_timeout_ms/max_duration_ms/max_turns/sentence_max_chars/max_pending_audio_chunks` 控制开场、打断、静默时限、总时限、轮次及缓存上限。 -- 未接纳呼叫在有效缓存窗口可能仍用旧批准版;已接纳呼叫固定原快照。新版呼叫命令只给任务 ID 与被叫号码,D 须从有效任务配置取得固定版本和任务级超时,不从命令猜值;现行严格 Schema 仍是旧结构。 - -### 2.3 任务配置:200,仅 ASR 模式(独立情况) - -```json -{ - "schema_version": "config-read.v0.1", - "resource": "task_config", - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", - "tenant_id": "tenant-id-mock", - "tenant_key": "tenant-mock", - "task_id": "task-mock", - "task_revision": 2, - "status": "running", - "name": "Mock task", - "group_id": null, - "max_concurrent_calls": 2, - "ring_timeout_ms": 30000, - "max_call_duration_ms": 120000, - "route_policy_id": "route-mock", - "caller_profile_id": "caller-profile-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_asr_v1", - "authorization_id": "auth-mock", - "authorization_expires_at": "2026-09-21T18:00:00+08:00", - "config": { - "agent_version_id": "agent_asr_v1", - "immutable": true, - "mode": "asr_only", - "asr": { - "provider_ref": "mock", - "model": "mock-asr-v1", - "language": "zh-CN", - "input": { - "encoding": "pcm_s16le", - "sample_rate_hz": 16000, - "channels": 1, - "sample_width_bytes": 2 - }, - "interim": true, - "timeout_ms": 5000 - }, - "conversation": { - "allow_interrupt": false, - "silence_timeout_ms": 3000, - "max_duration_ms": 120000, - "max_turns": 20, - "sentence_max_chars": 80, - "max_pending_audio_chunks": 32 - } - } - } -} -``` - -**字段说明/消费动作:** 字段与 2.2 相同,但 `agent.config.mode=asr_only`,**没有** LLM、提示词或 TTS 对象;配置内外 `agent_version_id` 必须一致。只能按授权的识别配置执行,不应将未提供的字段填成默认值。 - -### 2.4 SIP 或任务:错误返回(`resource_not_found`,HTTP 404,项目内规则) - -```json -{ - "schema_version": "config-read.v0.1", - "resource": "error", - "error": { - "code": "resource_not_found", - "message": "Task is not assigned to this Dispatcher." - } -} -``` - -**字段说明/消费动作:**`schema_version/resource` 标识项目内错误对象;`error.code` 是机器可读错误代码(`resource_not_found` 同时表示任务不存在或不归此 D),`error.message` 是可读说明,不含密钥。本地将此错误映射为 HTTP 404;真实 SaaS 是否采用相同状态码尚未验证。D 不得将失败当作空任务/无限制或使用过期配置接新呼叫;不能自动回退至 MQ 配置通道。 - -### 2.5 D ← SaaS:动态任务发现 - -第三条只读 HTTP 接口是 `GET /internal/v1/dispatcher/tasks`。D 启动/重启时不带 `after` 读取**一致全量快照 + 游标**,运行中**每 30 秒** `GET /internal/v1/dispatcher/tasks?after=` 读取针对本 D 的变更。`after` 是 SaaS 的变更水位,**不是最大 `task_id`**;旧任务的暂停、停止、改派也会返回。单次返回完整快照或完整变更集,不分页;全量超出 256 个归属任务时明确失败;增量变更超出单次返回上限时返回 HTTP 410 `cursor_expired`,重新取全量,不以部分成功跳过变更。 - -### 2.5.1 启动或重启:全量快照(HTTP 200,本地目标) - -```http -GET /internal/v1/dispatcher/tasks HTTP/1.1 -Host: -X-DISPATCHER-id: c046b893-8628-4589-ae50-619d049248a6 -X-DISPATCHER-SECRET-KEY: <受控注入,不展示实际密钥> -``` - -完整 200 响应体: - -```json -{ - "schema_version": "task-discovery.v0.2-proposal", - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", - "cursor": "1042", - "tasks": [ - { - "task_id": "task-a", - "tenant_id": "tenant-a", - "tenant_key": "tenant-a", - "status": "running", - "task_revision": 1 - }, - { - "task_id": "task-old", - "tenant_id": "tenant-a", - "tenant_key": "tenant-a", - "status": "stopped", - "task_revision": 3 - } - ] -} -``` - -**字段说明/消费动作:**`dispatcher_id` 是被授权的目标 D;`cursor` 是此快照覆盖的 SaaS 任务变更水位(示例数字只是**不透明字符串**,D 不按大小比较任务 ID);`tasks[]` 列出本 D 全部归属任务及**已停止但队列仍有积压的任务**;`tenant_id` 用于读取 §2.6 额度,`tenant_key` 保留原值并与 tenant_id 一对一核验,用于同租户所有任务共享并发额度;`task_revision/status` 是任务版本和状态;队列地址按 §1.2 双方已确定的 D/task 命名规则推导,不在响应正文重复。D 只能消费 SaaS 已创建/绑定的队列,不能声明或绑定。 - -### 2.5.2 每 30 秒:增量变化(HTTP 200,本地目标) - -```http -GET /internal/v1/dispatcher/tasks?after=1042 HTTP/1.1 -Host: -X-DISPATCHER-id: c046b893-8628-4589-ae50-619d049248a6 -X-DISPATCHER-SECRET-KEY: <受控注入,不展示实际密钥> -``` - -完整 200 响应体(包含任务退役): - -```json -{ - "schema_version": "task-discovery.v0.2-proposal", - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", - "next_cursor": "1045", - "changes": [ - { - "cursor": "1043", - "operation": "assigned", - "task_id": "task-b", - "tenant_id": "tenant-a", - "tenant_key": "tenant-a", - "status": "running", - "task_revision": 1 - }, - { - "cursor": "1044", - "operation": "updated", - "task_id": "task-a", - "tenant_id": "tenant-a", - "tenant_key": "tenant-a", - "status": "stopped", - "task_revision": 2 - }, - { - "cursor": "1045", - "operation": "removed", - "task_id": "task-old", - "tenant_id": "tenant-a", - "tenant_key": "tenant-a" - } - ] -} -``` - -**增量与退役:**`next_cursor` 是本次完整变更集持久化后的下次 `after`,不按数值或 task_id 比较;`changes[]` 按 SaaS 顺序应用。没有变更时 `changes: []` 且 `next_cursor` 等于请求的 `after`;`removed` 项只带 `cursor/operation/task_id/tenant_id/tenant_key`,须在 §0 停止发布、排空、回执及 SaaS 退役队列/绑定之后发送,不等同于 stopped。D 收到后停止消费、清任务配置,但保留执行恢复和 outbox。错误正文为 `schema_version/resource:error/error:{code,message}`;400 `invalid_cursor`、401 `unauthorized`、403 `dispatcher_not_authorized`、410 `cursor_expired`、503 `service_unavailable`,均不推进游标且关闭新准入;410 重取并原子持久化全量快照后才恢复。任务队列由 SaaS 创建/维护/退役,响应不提供地址;30 秒轮询不能代替 MQ 即时控制。以上均为项目内规则,尚未获真实 SaaS 确认。 - -### 2.6 D ← SaaS:按租户 ID 获取并发额度(新增项目草案) - -D 从任务清单/单任务配置取得 `tenant_id`、原值 `tenant_key` 并核对外呼信封后,再请求额度;不能把任务额度当租户总额。以下路径和字段为**项目提案,尚未由 SaaS 发布**。 - -#### 2.6.1 有可用额度:请求与完整 200 响应 - -```http -GET /internal/v1/dispatcher/tenant/tenant-id-mock/quota HTTP/1.1 -Host: -X-DISPATCHER-id: c046b893-8628-4589-ae50-619d049248a6 -X-DISPATCHER-SECRET-KEY: <受控注入,不展示实际密钥> -``` - -```json -{ - "schema_version": "config-read.v0.1", - "resource": "tenant_quota", - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", - "tenant_id": "tenant-id-mock", - "tenant_key": "tenant-mock", - "quota_revision": 1, - "max_concurrent_calls": 3, - "valid_until": "2026-09-21T18:00:00+08:00" -} -``` - -**字段说明/消费动作:**三个身份字段必须与任务及请求一致;`quota_revision` 为额度版本;`max_concurrent_calls` 是 SaaS **分给本 D 的租户份额**,同租户所有任务共同占用,不是每任务各得3路;`valid_until` 是有效截止。成功核验后最多缓存约60秒且不超过截止时间;同租户任务复用一份额度/占用,D 同一事务核查并预留租户+任务+线路等额度。未知通话继续计数;未来多D需份额之和≤总额,不各拿一份全额。 - -#### 2.6.2 降额或额度为零:200(独立情况) - -```json -{ - "schema_version": "config-read.v0.1", - "resource": "tenant_quota", - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", - "tenant_id": "tenant-id-mock", - "tenant_key": "tenant-mock", - "quota_revision": 2, - "max_concurrent_calls": 0, - "valid_until": "2026-09-21T18:00:00+08:00" -} -``` - -**字段说明/消费动作:** 0明确禁止新准入,不是无限额。降额时不强挂已有通话、不清未知占用,等占用低于新上限且授权有效才再接新。stop静默排空与控制不需要通话额度,不能因额度0卡住停止任务。 - -#### 2.6.3 无可用租户额度:错误(HTTP 503,项目内规则) - -```json -{ - "schema_version": "config-read.v0.1", - "resource": "error", - "error": { - "code": "tenant_quota_unavailable", - "message": "No valid tenant allocation is available for this dispatcher." - } -} -``` - -**字段说明/消费动作:** 服务端无法提供有效租户份额(缺失、过期或暂不可用)时,本地返回 HTTP 503 与 `tenant_quota_unavailable`;收到 `200` 但身份与请求/任务不符时,D 拒绝并关闭该租户新准入。不得用任务额度或无限额兜底;已有执行依原快照处理。核实通话终结并释放执行资源就释放通话额度,**不等待录音上传或最终结果 MQ 确认**;未知通话不能释放。 - -## 3. SaaS → D:下一版精简业务命令 - -以下 JSON 是本项目 F07 冻结的完整下一版 MQ 请求;`schema_version=command-next.v0.1-proposal` 标识项目内版本,不是现行外部 `2.0`。`dispatcher_id/tenant_id/tenant_key` 确定 D 和租户,MQ 发布参数另按 §1 任务 key 精确路由;`issued_at/not_after` 限定有效期;`command_type` 区分呼叫或控制。**仅** `call.execute` 仍带 `command_id`,用来识别不可重复的外呼执行;三个 `task.control` 均不带 `command_id`、`expected_task_revision`,本地不设计控制命令去重。控制的乱序、重投及处理回执按本节规则和本地 Schema/Mock 测试处理;真实 SaaS 兼容性及从现行 v2 切换仍未验证,不属于本地 C 的外部验收证据。 - -### 3.1 发起外呼:call.execute - -```json -{ - "schema_version": "command-next.v0.1-proposal", - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", - "tenant_id": "tenant-a", - "tenant_key": "tenant-a", - "trace_id": "trace-v2", - "issued_at": "2026-09-18T10:00:00+08:00", - "command_id": "command-a", - "command_type": "call.execute", - "not_after": "2026-09-18T10:15:00+08:00", - "payload": { - "task_id": "task-a", - "callee": "15003164745" - } -} -``` - -**字段说明/消费动作:**`payload` **只有** `task_id`(SaaS 任务身份)及 `callee`(原始被叫号码,不带线路前缀);租户归属从信封及 `/internal/v1/dispatcher/task/:task_id` 的授权结果核对。路由/主叫/智能体版本和任务级 `ring_timeout_ms/max_call_duration_ms` 全由有效任务配置取得,D 接纳时绑定不可漂移的执行快照;生成 `execution_id` 是 D 内部事实,不由 SaaS 逐呼提供。信封 `command_id` 仅用于外呼命令身份:重投不能第二次拨号。本例15分钟有效期仅示意,不是默认值;SaaS 须覆盖其允许的轮询/配置/额度准备及排队时间。队列ready不代表D已消费;离线或暂停不延长not_after,恢复仅执行仍有效者,过期非stopped消息明确拒绝、不自动重建命令,stopped积压静默ACK。此为项目内 v0.1 payload,由[命令/控制 Schema](../contracts/command-next-v0.1-proposal.schema.json)严格校验并由本地 C 验证;真实 SaaS 兼容性和从现行 v2 切换未验证,不属于本地通过证据。 - -### 3.2 暂停任务:task.control / pause - -```json -{ - "schema_version": "command-next.v0.1-proposal", - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", - "tenant_id": "tenant-a", - "tenant_key": "tenant-a", - "trace_id": "trace-v2", - "issued_at": "2026-09-21T00:00:00Z", - "command_type": "task.control", - "not_after": "2026-09-21T00:00:30Z", - "payload": { - "task_id": "task-a", - "action": "pause", - "active_call_policy": "drain", - "reason": "local-test" - } -} -``` - -**字段说明/消费动作:**`task_id` 定位任务;`action=pause` 停止新呼叫准入;`active_call_policy=drain` 允许在途通话自然结束;`reason` 是原因说明。控制**无 `command_id`、无 `expected_task_revision`,不做按消息去重**。D 持久暂停屏障、停止该队列消费,并将已预取但未接纳的消息 `nack(requeue=true)` 回原队列;不 ACK 丢弃、不搬入本地待拨队列。已接纳通话按 `drain/hangup` 执行;控制回执在屏障持久且未接纳投递已退回后发送,不等待通话结束。SaaS 按每任务状态变更顺序发布控制,D 每任务串行处理。 - -### 3.3 恢复任务:task.control / resume - -```json -{ - "schema_version": "command-next.v0.1-proposal", - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", - "tenant_id": "tenant-a", - "tenant_key": "tenant-a", - "trace_id": "trace-v2", - "issued_at": "2026-09-21T00:00:00Z", - "command_type": "task.control", - "not_after": "2026-09-21T00:00:30Z", - "payload": { - "task_id": "task-a", - "action": "resume", - "reason": "operator-resume" - } -} -``` - -**字段说明/消费动作:**resume 成功就是恢复消费**原任务队列的积压**,不是等待 SaaS 重发。D 必须绕过缓存读取最新任务;仅当权威状态为 `running`、D/租户归属有效且本地从未 stopped 时解除 paused。每条旧命令仍校验 `not_after`,过期明确拒绝,不延长期限或等待次日。已停止任务不可恢复。控制无编号/修订,不去重;重复 resume 对状态幂等,但每次实际投递都可有独立回执。 - -### 3.4 停止任务:task.control / stop - -```json -{ - "schema_version": "command-next.v0.1-proposal", - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", - "tenant_id": "tenant-a", - "tenant_key": "tenant-a", - "trace_id": "trace-v2", - "issued_at": "2026-09-21T00:00:00Z", - "command_type": "task.control", - "not_after": "2026-09-21T00:00:30Z", - "payload": { - "task_id": "task-a", - "action": "stop", - "active_call_policy": "hangup", - "reason": "operator-stop" - } -} -``` - -**字段说明/消费动作:**`stop` 持久终止任务准入,SaaS 先停止该队列发布并确认已有发布处理完,再投递 stop。D 持久 stopped 屏障后静默 ACK 所有未接纳积压,不拨号、不发逐条 `command.result`/`call.result`、不申请额度;不是 purge/delete,也不影响其他任务。D 取消普通 consumer,结清已预取消息后用 `basic.get` 排空队列至空;仅在无未 ACK 投递且确认空队列后发送 stopped/applied 回执。SaaS 收到回执后才可删除队列/绑定并在任务发现中发 `removed`。ACK 丢失、重启、额度 0 或配置失效不改变排空规则;保留本地计数/错误。已接纳/在途通话按 `hangup` 或 `drain` 处理并照常发最终结果;stopped 同任务 ID 不可 resume。 - -**任务发现与控制状态规则(项目内 v0.1):** SaaS 先持久变更权威任务状态,再按每任务顺序发布控制;D 对同任务串行处理。stopped 不可逆;paused 只能由新鲜任务 GET 确认 `running` 的 resume 解锁。D 不允许旧 running 配置/快照覆盖更高 `task_revision` 或清除本地 stopped 屏障;重启恢复持久屏障,全量快照只能收紧准入,不能自行重开。pause/stop 先持久关闭准入;action 与最新任务状态不一致、读取失败或出现乱序冲突时保持关闭并返回 `state_mismatch`/`task_unavailable`。重复 pause/resume/stop 只对状态幂等,不做控制消息去重;每次处理都可产生独立 `event_id` 回执,回执自身重投复用原 event_id。MQ 发布成功不等于控制已应用。 - -### 3.5 D → SaaS:外呼命令处理回执 - -```json -{ - "schema_version": "command-next.v0.1-proposal", - "event_id": "command-result-a", - "event_type": "command.result", - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", - "tenant_id": "tenant-a", - "tenant_key": "tenant-a", - "trace_id": "trace-v2", - "occurred_at": "2026-09-18T10:00:01+08:00", - "aggregate_type": "command", - "aggregate_id": "command-a", - "aggregate_version": 1, - "payload": { - "command_id": "command-a", - "command_type": "call.execute", - "status": "accepted", - "reason_code": "accepted", - "execution_id": "execution-a" - } -} -``` - -**字段说明/消费动作:**`payload.command_id` 仅指向 §3.1 的外呼命令;`status` 区分接纳/拒绝,`execution_id` 是 D 接纳后生成的执行身份。已停止任务的未接纳积压**不发送此回执**;其他未接纳拒绝只有命令回执、不伪造通话。MQ 回执**不代表已拨号或已完成通话**;按 `command_id` 持久去重,未知执行不得靠重投产生第二次呼叫。字段由项目内 Schema 校验。 - -### 3.6 D → SaaS:任务控制处理回执 - -```json -{ - "schema_version": "command-next.v0.1-proposal", - "event_id": "control-result-a", - "event_type": "command.result", - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", - "tenant_id": "tenant-a", - "tenant_key": "tenant-a", - "trace_id": "trace-v2", - "occurred_at": "2026-09-18T10:00:01+08:00", - "aggregate_type": "task", - "aggregate_id": "task-a", - "aggregate_version": 2, - "payload": { - "command_type": "task.control", - "task_id": "task-a", - "action": "pause", - "status": "applied", - "reason_code": "applied", - "task_state": "paused" - } -} -``` - -**字段说明/消费动作:** 控制请求不带 `command_id/expected_task_revision`,回执以 `task_id/action/status/reason_code/task_state` 说明处理事实,不提供按控制编号一对一关联,也不把 `event_id` 用作控制去重身份。D 按最新任务状态和本地终态屏障处理乱序;对同一状态的重复动作可重复回执。回执丢失时 SaaS 以最新任务 GET 和后续状态发现收敛,不能把 MQ 发布成功当控制已生效。 - -## 4. D → SaaS:唯一通话结果(项目内 v0.1 `call.result` 契约) - -同一次通话只发布一种业务反馈 `call.result`:通话状态、最终转写、拒联结果、录音资产一次返回;不再将通话进度、实时文字、拒联、通话结束、录音成功/失败各自发布对外事件。**本轮按该简化实现和验收**,不要求 SaaS 在最终结果前收到实时文字或拒联;真实 SaaS 消费兼容性未验证。停止任务未接纳积压不产生通话事件;其它已接纳执行的消息可靠入队,断线后按原事件身份重投;这不是对外“补传命令”。结果结构由[最终结果 Schema](../contracts/call-result-v0.1-proposal.schema.json)严格校验,不属于现行外部 MQ v2。 - -### 4.1 录音已上传 OSS:最终成功结果 - -```json -{ - "schema_version": "call-result.v0.1-proposal", - "event_id": "call-result-001", - "event_type": "call.result", - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", - "tenant_id": "tenant-a", - "tenant_key": "tenant-a", - "trace_id": "trace-v2", - "occurred_at": "2026-09-18T10:10:15+08:00", - "aggregate_type": "call", - "aggregate_id": "call-a", - "aggregate_version": 1, - "payload": { - "source_command_id": "command-a", - "execution_id": "execution-a", - "call_id": "call-a", - "task_id": "task-a", - "task_revision": 1, - "agent_version_id": "version-a", - "route_policy_id": "route-a", - "caller_profile_id": "caller-a", - "callee": "15003164745", - "trunk_id": "trunk-a", - "started_at": "2026-09-18T10:00:00+08:00", - "ended_at": "2026-09-18T10:10:00+08:00", - "duration_ms": 600000, - "outcome": "answered", - "reason_code": null, - "transcript": [ - { - "turn_id": "turn-1", - "segment_id": "segment-1", - "role": "user", - "text": "示例转写内容", - "start_ms": 1000, - "end_ms": 2500 - } - ], - "opt_out": false, - "recording": { - "status": "uploaded", - "recording_id": "recording-a", - "upload_id": "upload-a", - "bucket": "example-bucket", - "object_key": "calls/tenant-a/call-a.wav", - "format": "wav", - "channels": 1, - "sample_rate_hz": 8000, - "duration_ms": 600000, - "size_bytes": 9600000, - "checksum_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - } - } -} -``` - -**字段说明/消费动作:**`schema_version/event_type` 是项目内 v0.1 的单一通话结果类型,严格由本地 Schema 校验;现行外部 v2 Schema 保持不变,不能混用。`event_id` 是固定的事件身份,重复入队须相同;`dispatcher_id/tenant_id/tenant_key/trace_id` 限定来源和归属;`aggregate_type/aggregate_id/aggregate_version/occurred_at` 为呼叫聚合、版本和完成时间。 -`payload.source_command_id/execution_id/call_id/task_id/task_revision/agent_version_id` 绑定原外呼命令、D 生成的执行/呼叫及从任务快照绑定的固定版本;`route_policy_id/caller_profile_id/trunk_id/callee` 为路由策略、主叫配置、实际线路及原始被叫;`started_at/ended_at/duration_ms/outcome/reason_code` 给出起止、时长、结果和可空原因。`transcript[]` 中 `turn_id/segment_id/role/text/start_ms/end_ms` 是仅随最终结果发送的转写片段及时间;`opt_out` 表示通话中的拒联事实,只在最终消息里可见。`recording.status/recording_id/upload_id/bucket/object_key/format/channels/sample_rate_hz/duration_ms/size_bytes/checksum_sha256` 描述已成功上传的资产,不包含文件、TOKEN 或签名 URL。SaaS 使用 `call_id` 关联、`event_id` 去重并按固定 `upload_id` 避免重复资产。 - -### 4.2 录音上传未完成:15 分钟内收口为最终异常结果 - -```json -{ - "schema_version": "call-result.v0.1-proposal", - "event_id": "call-result-002", - "event_type": "call.result", - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", - "tenant_id": "tenant-a", - "tenant_key": "tenant-a", - "trace_id": "trace-v2", - "occurred_at": "2026-09-18T10:10:15+08:00", - "aggregate_type": "call", - "aggregate_id": "call-b", - "aggregate_version": 1, - "payload": { - "source_command_id": "execute-b", - "execution_id": "execution-b", - "call_id": "call-b", - "task_id": "task-a", - "task_revision": 1, - "agent_version_id": "version-a", - "route_policy_id": "route-a", - "caller_profile_id": "caller-a", - "callee": "15003164745", - "trunk_id": "trunk-a", - "started_at": "2026-09-18T10:00:00+08:00", - "ended_at": "2026-09-18T10:10:00+08:00", - "duration_ms": 600000, - "outcome": "answered", - "reason_code": null, - "transcript": [], - "opt_out": false, - "recording": { - "status": "unavailable", - "error_code": "upload_timeout", - "recording_id": "recording-b", - "upload_id": "upload-b", - "bucket": null, - "object_key": null, - "format": "wav", - "channels": 1, - "sample_rate_hz": 8000, - "duration_ms": 600000, - "size_bytes": null, - "checksum_sha256": null - } - } -} -``` - -**字段说明/消费动作:** 若录音预期存在但授权/PUT 明确失败,立即以 `recording.status=unavailable` 收口;若仍无确定结果,最迟于 `call.ended_at + 15m` 收口,`bucket/object_key/size_bytes/checksum_sha256=null`,`recording.error_code` 仅可为 `upload_authorization_failed`、`upload_authorization_expired`、`upload_failed`、`upload_timeout`、`deadline_exceeded` 或 `checksum_mismatch`,分别记录授权、PUT、总期限或校验阶段;呼叫自身 `reason_code` 保持通话事实。不能谎称上传成功或默默丢弃最终结果。`outcome` 必须反映**通话本身**而非上传成败;已接通/正常结束不得因录音失败改成 `failed`。同一录音最多一次 PUT;超时/结果未知不重试 PUT。 - -### 4.3 正常未产生录音:无应答结果 - -```json -{ - "schema_version": "call-result.v0.1-proposal", - "event_id": "call-result-003", - "event_type": "call.result", - "dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", - "tenant_id": "tenant-a", - "tenant_key": "tenant-a", - "trace_id": "trace-c", - "occurred_at": "2026-09-18T10:00:30+08:00", - "aggregate_type": "call", - "aggregate_id": "call-c", - "aggregate_version": 1, - "payload": { - "source_command_id": "command-c", - "execution_id": "execution-c", - "call_id": "call-c", - "task_id": "task-a", - "task_revision": 1, - "agent_version_id": "version-a", - "route_policy_id": "route-a", - "caller_profile_id": "caller-a", - "callee": "15003164745", - "trunk_id": "trunk-a", - "started_at": "2026-09-18T10:00:00+08:00", - "ended_at": "2026-09-18T10:00:30+08:00", - "duration_ms": 30000, - "outcome": "no_answer", - "reason_code": "ring_timeout", - "transcript": [], - "opt_out": false, - "recording": { - "status": "not_created", - "reason_code": "no_answer", - "recording_id": null, - "upload_id": null, - "bucket": null, - "object_key": null, - "format": null, - "channels": null, - "sample_rate_hz": null, - "duration_ms": null, - "size_bytes": null, - "checksum_sha256": null - } - } -} -``` - -**字段说明/消费动作:** 这是已接纳、已尝试但无人接听且未产生录音的呼叫;`started_at/duration_ms` 此例表示呼叫尝试起点和尝试耗时,不冒称已接通时长。正常无录音用 `not_created`,资产字段为null,确认终结后即可发送,不申请/等待上传;忙线等正常无录音同类处理,原因须与事实一致。录音本应生成却失败应为 `unavailable` 加明确阶段原因,不伪装正常无录音。普通未接纳拒绝仅有命令回执;stopped未接纳积压无回执也无最终结果,不能虚构call_id。 - -**额度与文件交付分离:** 确认通话终结、执行资源释放就释放通话额度,不等待 OSS 或最终通知确认,未知仍占额。已产生录音才按 4.1/4.2 收口;每条 JSON 消息体上限 8,388,608 bytes,超限持久阻塞 outbox,不截断、不拆分、不恢复实时事件。 - -## 5. 本地实现与外部验收边界 - -- 本文及链接的 `docs/contracts` Schema/正反例/MQ 拓扑是本轮 P1 Go/Mock 的项目内契约。完成 F01/F07 版本、来源/hash、严格校验和 Mock SaaS 端到端 C 后,可直接进入本地实现;不要求真实 SaaS、management 或供应商签收/连通。 -- `contracts/upstream/v1/` 与现行外部 MQ v2 继续作为真实 SaaS 的既有基线。本地 v3 路由和消息不得混入 v2,也不得把 Mock 通过写成 SaaS、management 或生产验收。 -- 本地 MQ 采用 v3 durable topic/queue,任务和 D 结果队列均由 SaaS 创建维护;D 只消费任务/控制并发布结果。命令用 `command_id` 持久去重防止二次 originate;任务控制无 `command_id/expected_task_revision`、不按消息去重,乱序/过期失败关闭。 -- 对外只保留必要命令/控制回执和每个已接纳通话唯一的最终 `call.result`。不保留 query/replay、实时转写/拒联/通话进度/录音拆分事件;stopped 任务未接纳积压只静默 ACK,不产生逐条结果。正常无录音立即以 `not_created` 收口;预期录音失败最晚在 `call.ended_at + 15m` 以 `unavailable` 收口;每个 upload_id 最多一次 PUT,重投复用原 event_id,不重新上传。 -- 所有 MQ JSON 正文上限为 8,388,608 bytes。超限消息留在持久 outbox 并显式阻塞,不截断、不拆分、不丢弃。该上限仅为本地 v0.1 规则;真实 SaaS 与 broker 的兼容性需另行验证。 -- 四条 GET、严格 Schema、任务发现单次完整响应/游标过期恢复、队列退役握手和 `call.result` 正反例均按本文及对应 schema 验证。旧 v0.1 分页契约及本地证据仅为历史;采用本版须重测 F03/F09,不能把旧通过记录当作本版通过。外部正式版本、部署与切换仍是独立事实和授权门禁。 diff --git a/docs/thirds/v0.3.md b/docs/thirds/v0.3.md deleted file mode 100644 index b65314b..0000000 --- a/docs/thirds/v0.3.md +++ /dev/null @@ -1,28 +0,0 @@ -# 第三方任务发现事件游标分页 v0.3(项目内提案) - -> 仅替换 [`v0.2`](v0.2.md) §2.5 的任务发现目标;其他 SaaS 项目内业务接口仍按 v0.1。设计依据为 [`plan-0926.md`](../plan-0926.md)。本文件、Schema 和 Mock **未经 SaaS/业务签收,不是现网接口或生产合同**;当前本地仅运行 v0.3;v0.2 仅作历史,真实 SaaS 尚未切换。 - -## 2.5 唯一任务发现路径 - -`GET /internal/v1/dispatcher/tasks?after=` 由指定 Dispatcher 使用既有 `X-DISPATCHER-id` / `X-DISPATCHER-SECRET-KEY` 读取自己的任务。首次无本协议游标时发送 `after=0`;之后只发送已和任务状态一起持久化的 `next_cursor`。`after` 是**该 D 范围内单调、不可复用的事件 ID**,不是任务 ID、页号或任务配置版本。规范形式是无前导零的十进制 uint64 字符串;只允许起点为 `0`。D 身份不可更换来绕过游标。 - -所有 HTTP 200 均采用同一形状:`schema_version=task-discovery.v0.3-proposal`、`dispatcher_id`、`tasks[]` 和 `next_cursor`。`tasks[]` 中每项为 `task_id`、`tenant_id`、原值 `tenant_key`、`status`、`task_revision`;**没有** `changes`、`operation`、`snapshot`、`mode`、单项 `event_id`、SaaS 队列名或任务页 token。同一任务有更晚的事件时可再次出现。SaaS 对每个任务只需返回**最新状态**,不要求回放所有中间状态;已有任务的 `status` 变化更新 SaaS 状态;新 `paused`、`stopped`、`finished`、`removed` 可关闭或保持准入。**后续 `running`(即使更新且已持久化)只更新 SaaS 状态与游标,不直接解除已持久的 `paused`;必须收到 MQ `resume` 并由任务只读接口新鲜确认 `running` 才能重开原队列。**其他字段仍须校验身份、版本及归属。任务具体执行配置仍由独立只读接口取得,不由发现列表取代。 - -SaaS 在响应中按其事件序号升序选取尚未返回的**最新任务记录**。每页至多 256 项,活动任务总量仍受单 D 256 上限约束;撤销墓碑的累计数量不在这个活动上限之内。非空页的 `next_cursor` 必须是**最后一个实际返回任务**的事件 ID,并严格大于请求的 `after`,绝不能前进到尚未返回的更新之后。空页表示本轮追平,`tasks=[]` 且 `next_cursor=after`;D 可在首次追平后开放经核验的新执行,其后约 30 秒再次查询。不得依靠“不足 256 项”断定追平:无论每页实际数量,D 都必须继续读取直到空页。若页内同一任务重复出现、任务身份发生冲突或游标不前进,D 拒绝整页而不是挑一条使用。 - -**信任边界**:用户确认仅返回响应级 `next_cursor`,不返回每项 `event_id`。D 可以验证游标形式、单调前进、响应身份和任务数据,但**不能独立证明 SaaS 的页内顺序及没有漏项**;SaaS 必须保证选择完整、有序、无跳跃,真实 SaaS 签收及并发分页故障测试属于外部门禁。Mock 正例不代签这一保证。 - -### 状态和撤销 - -`status` 只允许 `running`、`paused`、`stopped`、`finished`、`removed`。`removed` 是该 D 归属撤销的**任务墓碑**,必须保留原任务/租户身份;某任务没有出现在本页不表示撤销。SaaS 先持久化 stop/撤销及其必要回执、协调自己拥有的任务队列退役,再发出墓碑;D 收到后停止新接纳,不清理执行恢复、已入队结果、幂等或未知占用,也不自动强挂在途通话。paused 保留原积压;MQ 控制即时执行,不等轮询。发现页无论新旧 `running` 均不解除已持久的 paused/stopped 屏障;stopped 同任务不可逆,removed 不撤销旧执行事实。 - -### 一致性、错误和恢复 - -- 任务页与游标同一 SQLite 事务提交;页提交失败不能推进游标。首次启动/恢复在读到**空页**并完成授权、归属和 SaaS 预建队列核验前不消费执行消息。中途失败、响应无效或网络不可用时保留本地任务、控制、执行事实与错误记录,拒绝新执行;MQ 即时控制仍继续。不能拿空列表推断未返回任务已经 removed。 -- 游标**永不过期**:不提供 410 或 `cursor_expired`,也不因时间推移要求 D 自动从零重建。SaaS 必须长期维护每个任务可追溯的最新事件位置和撤销墓碑;对任意曾提交的 D 游标,不能返回遗漏后续状态的伪造空页。`after=0` 的首次分页同样有界而不一次返回全部。序号回退、持久记录丢失或无法证明连续时返回明确错误,D fail-closed 并等待处理,不暗中重置。 -- HTTP 错误:400 `invalid_cursor`、401 `unauthorized`、403 `dispatcher_not_authorized`、503 `service_unavailable`;成功只允许 200。返回体按本版本严格 Schema,错误正文不含任务或凭据。具体身份、响应大小与实际 SaaS 系统行为仍待签收。 -- 当前 v0.2 游标**不能**重解释为事件 ID;切换前必须核验旧任务、积压与在途执行安全收口。新版只运行一个任务发现消费者,不保留旧 `changes`/410/分页 token 兼容路径或 HTTP→MQ 回退。SaaS 独占建队、绑定和退役,D 只消费预建队列。 - -## 验收及容量阻断 - -本地 Mock 要覆盖初始多页、空页、同一任务再出现、撤销、跨页并发更新、重启原游标续读、重复页、事务回滚、发现页 `paused→running` 后准入仍暂停直到 MQ `resume`+新鲜任务核验、暂停/停止竞态和 queue-only 消费;所有关键边界 fail-closed,不重拨。实时控制与最终结果的其他 v0.1 业务合同不变。活动任务 256 上限**不约束永久墓碑**;历史撤销增长、离线追赶时间、SaaS 数据保留及灾备后序号连续性未签收,不能声称容量有界或真实联调/生产可用。