simplify current local contract definitions

This commit is contained in:
2026-09-28 14:26:33 +08:00
parent 4473f2bebd
commit bd2e5ad2b1
13 changed files with 81 additions and 25 deletions
@@ -30,7 +30,7 @@
"task_response": {
"type": "object",
"additionalProperties": false,
"required": ["schema_version", "resource", "dispatcher_id", "tenant_id", "tenant_key", "task_id", "task_revision", "status", "name", "group_id", "max_concurrent_calls", "ring_timeout_ms", "max_call_duration_ms", "route_policy_id", "caller_profile_id", "allowed_trunk_ids", "schedule", "agent"],
"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"},
@@ -8,9 +8,27 @@
{"$ref": "#/$defs/error_response"}
],
"$defs": {
"dispatcher_id": {"$ref": "https://go-sip.local/contracts/proposals/task-discovery-v0.3-proposal.schema.json#/$defs/dispatcher_id"},
"cursor": {"$ref": "https://go-sip.local/contracts/proposals/task-discovery-v0.3-proposal.schema.json#/$defs/cursor"},
"task": {"$ref": "https://go-sip.local/contracts/proposals/task-discovery-v0.3-proposal.schema.json#/$defs/task"},
"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"],
+3
View File
@@ -16,6 +16,9 @@ func TestReadLocalV04SchemasAndRejectInvalidPaths(t *testing.T) {
if err != nil || !json.Valid(data) {
t.Fatalf("embedded v0.4 schema %s: valid=%v err=%v", name, json.Valid(data), err)
}
if name == "task-discovery-v0.4-proposal.schema.json" && bytes.Contains(data, []byte("task-discovery-v0.3-proposal.schema.json")) {
t.Fatal("v0.4 task discovery must not reference the historical v0.3 schema")
}
original, err := Files.ReadFile("local/v0.4/" + name)
if err != nil || !bytes.Equal(data, original) {
t.Fatalf("runtime schema differs from embedded bundle: %s err=%v", name, err)
@@ -5,7 +5,7 @@
## 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 响应证据**。本地 HTTP 请求/错误/分页语义以[第三方对接契约](../thirds/第三方对接事件与请求消费顺序_v0.1.md)为准。旧 [OpenAPI/MQ 只读索引](../references/OpenAPI与MQ字段索引_v0.1.md) 也仅为历史参考。
- **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=<cursor>`)为用户给定路径;`/internal/v1/dispatcher/tenant/:tenant_id/quota` 与任务路由字段 `route_policy_id`、`caller_profile_id`、`allowed_trunk_ids` 为本地项目定义。它们不是截图/现网接口已验证的响应键;本地 Mock 按本契约验证。
@@ -14,7 +14,7 @@
## 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` 的游标和分页错误按第三方契约 §2.5:400 `invalid_cursor`/`invalid_page_token`、410 `cursor_expired`/`snapshot_expired`。任务发现严格结构见[任务发现 Schema](task-discovery-v0.1-proposal.schema.json),正例见[第三方对接 §2.5](../thirds/第三方对接事件与请求消费顺序_v0.1.md),不混入本文件的配置响应 Schema。以上仅为本地 Mock/Go 契约,不代表外部 SaaS 状态码。
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` 返回类型 | 何时读取 | 错误处理 |
| --- | --- | --- | --- |
@@ -34,7 +34,7 @@ Dispatcher 仅用 `X-DISPATCHER-id`(全局唯一 UUID)和部署受控的 `X-
| --- | --- | --- |
| `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` 与空字符串不混同。 |
| `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:三字段格式/无值约定。时间约束与星期段、排除日期、线路时段**同时成立**。 |
@@ -96,7 +96,7 @@ D 在同一事务预留租户/任务/线路等占用,未知继续计入;降
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,不加控制去重。
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 实际加载或任何生产外呼验收。
+1 -1
View File
@@ -30,7 +30,7 @@
"task_response": {
"type": "object",
"additionalProperties": false,
"required": ["schema_version", "resource", "dispatcher_id", "tenant_id", "tenant_key", "task_id", "task_revision", "status", "name", "group_id", "max_concurrent_calls", "ring_timeout_ms", "max_call_duration_ms", "route_policy_id", "caller_profile_id", "allowed_trunk_ids", "schedule", "agent"],
"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"},
@@ -17,11 +17,11 @@
},
{
"path": "docs/contracts/config-read-fields-v0.1-proposal.md",
"sha256": "14f89655b3565d3e2607e1509b5cfd272f090e7266ba352e16a8cccdd43aef32"
"sha256": "28dcf3ce7be73b2b8f6b9e2dddbe242b2d8ea02d61b2671a5bf0d62eb40c7ab3"
},
{
"path": "docs/contracts/config-read-v0.1.schema.json",
"sha256": "d3fbf066295916b5322fff44c98a9e847de592135885ddff057f7f089fa4dfea"
"sha256": "043dd26a9033b7fd6401c2fc918a1fec2ecbaac0bd59cbba6da72d02125b8184"
},
{
"path": "docs/contracts/examples/call-result-invalid-missing-checksum-v0.1.json",
@@ -3,7 +3,7 @@
"hash_algorithm": "SHA-256",
"source": {"path": "docs/thirds/v0.2.md", "sha256": "5358eaaecf944704975feab9150dcae8224ea89247a1086c68965ea46c253e31"},
"artifacts": [
{"path": "docs/contracts/config-read-v0.1.schema.json", "sha256": "d3fbf066295916b5322fff44c98a9e847de592135885ddff057f7f089fa4dfea"},
{"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"},
@@ -11,7 +11,7 @@
{"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": "d3fbf066295916b5322fff44c98a9e847de592135885ddff057f7f089fa4dfea"},
{"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"}
@@ -4,21 +4,17 @@
"status": "project-local proposal; external SaaS compatibility unverified",
"source": {
"path": "docs/thirds/v0.4.md",
"sha256": "85a72ab93526a49aed78b9f762b005c46eaccfecaaaff50013f3af6be18a3470"
"sha256": "08dcf39aabf77cd4cbf9dd2c3d7beaa691134c91354004e4ff59f1b0dbb86556"
},
"artifacts": [
{
"path": "docs/contracts/task-discovery-v0.4-proposal.schema.json",
"sha256": "95c02504d432c01ee57d8074c63ea60dc1118dd92057ae138b2d1e4340b7d73c"
"sha256": "e72eb5da004185d975c05cd4e2ca63314793127bbe07d8738a99f5ecad1d1f13"
},
{
"path": "docs/contracts/task-control-v0.4-proposal.schema.json",
"sha256": "5aa94f799da190a3e97c2fa4e911980ceac52662322e85bc2f57eecb5b21de3c"
},
{
"path": "docs/contracts/task-discovery-v0.3-proposal.schema.json",
"sha256": "da8eda2e8f5416b2fb35f68e09a98f37d1417a878e9271b8e6d0d6824b94814c"
},
{
"path": "contracts/upstream/v1/mq.schema.json",
"sha256": "d566899abbc8f0f8404cfe79ea58b0d1e7bf7e9fc663b09b282c15d5c74f505d"
@@ -8,9 +8,27 @@
{"$ref": "#/$defs/error_response"}
],
"$defs": {
"dispatcher_id": {"$ref": "https://go-sip.local/contracts/proposals/task-discovery-v0.3-proposal.schema.json#/$defs/dispatcher_id"},
"cursor": {"$ref": "https://go-sip.local/contracts/proposals/task-discovery-v0.3-proposal.schema.json#/$defs/cursor"},
"task": {"$ref": "https://go-sip.local/contracts/proposals/task-discovery-v0.3-proposal.schema.json#/$defs/task"},
"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"],
+1 -1
View File
@@ -15,7 +15,7 @@
- 在线 `GET /internal/v1/dispatcher/tasks?after=<watermark-or-current_cursor>` 返回 `schema_version=task-discovery.v0.4-proposal`、`mode=changes`、`dispatcher_id`、`tasks[]` 和 `next_cursor`。非空页严格前进,空页等于本次请求游标;SaaS 保证每 D 事件连续、分页完整、有序、不得提前丢失未消费增量,无法满足即显式报错并关闭新执行、重新全量。HTTP 400 不合法请求、403 D 无归属、503 服务不可用;错误响应严格见 Schema,**不复用** v0.2 `changes[]`/410。具体身份验证仍按现有只读 HTTP 配置合同。
- **仅在本次进程运行期间**按发现水位定时查询增量,用于新增任务归属、撤销/退役和必要身份校验。水位不是最大 task_id;异常页/缺页不得推进游标或视为空变更。重启重新全量,不要求从上一次进程的永久事件游标续读;旧 v0.3 的 `after=0` 事件回放和 SQLite 持久游标不得被当作新全量响应。
- 任务列表的任务状态可以作为启动快照的初始状态及增量身份/一致性校验,**运行中 pause/resume/stop 由独立 MQ 控制队列生效**;增量页 `running` 不能自动解除暂停或不可逆停止。若列表状态与已应用的 MQ 控制矛盾,关闭该任务新接纳并报错、等待受控恢复,不按 HTTP 到达顺序偷偷切换状态。`GET /internal/v1/dispatcher/task/:task_id` 的授权配置和 resume 的新鲜状态核验仍保留,不等同于恢复“发现页控制状态”机制。
- 项目内发现字段已在 [`task-discovery-v0.4-proposal.schema.json`](../contracts/task-discovery-v0.4-proposal.schema.json) 独立严格声明,任务条目引用现有 v0.3 `$defs/task`,不重造业务字段;控制入站严格格式见 [`task-control-v0.4-proposal.schema.json`](../contracts/task-control-v0.4-proposal.schema.json)。机器 Schema 只约束消息结构;多页相同水位、完整性和快照与 MQ 积压的交接顺序须另由 Mock/代码验证。真实 SaaS 的这些字段、应用收讫、增量保留期限**仍未签收**,不能以项目内 Schema/Mock 自证兼容;不直接引用 v0.2 的 `changes`/410 或原地复用 v0.3 的严格 Schema。
- 项目内发现字段已在 [`task-discovery-v0.4-proposal.schema.json`](../contracts/task-discovery-v0.4-proposal.schema.json) 独立严格声明,任务条目与游标等通用定义在 v0.4 Schema 内完整声明,不再依赖历史 v0.3 Schema;控制入站严格格式见 [`task-control-v0.4-proposal.schema.json`](../contracts/task-control-v0.4-proposal.schema.json)。机器 Schema 只约束消息结构;多页相同水位、完整性和快照与 MQ 积压的交接顺序须另由 Mock/代码验证。真实 SaaS 的这些字段、应用收讫、增量保留期限**仍未签收**,不能以项目内 Schema/Mock 自证兼容;不直接引用 v0.2 的 `changes`/410 或原地复用 v0.3 的严格 Schema。
## 3. 控制、通话与积压命令边界
+22
View File
@@ -2,6 +2,7 @@ package contract
import (
"bytes"
"encoding/json"
"os"
"path/filepath"
"testing"
@@ -61,6 +62,27 @@ func TestProjectLocalConfigurationSchemasValidatePositivesAndRejectNegatives(t *
if err := ValidateLocalConfigRead(read("config-read-task-v0.1.json")); err != nil {
t.Fatalf("valid task config: %v", err)
}
var task map[string]any
if err := json.Unmarshal(read("config-read-task-v0.1.json"), &task); err != nil {
t.Fatal(err)
}
delete(task, "name")
delete(task, "group_id")
withoutDisplayFields, err := json.Marshal(task)
if err != nil {
t.Fatal(err)
}
if err := ValidateLocalConfigRead(withoutDisplayFields); err != nil {
t.Fatalf("task without optional display fields: %v", err)
}
task["name"] = 123
withInvalidDisplayField, err := json.Marshal(task)
if err != nil {
t.Fatal(err)
}
if err := ValidateLocalConfigRead(withInvalidDisplayField); err == nil {
t.Fatal("invalid optional task name accepted")
}
if err := ValidateLocalConfigRead(read("config-read-invalid-extra-property-v0.1.json")); err == nil {
t.Fatal("config-read schema accepted an additional property")
}
+1 -2
View File
@@ -390,7 +390,7 @@ v4_schema_paths = {
name: ROOT / f"docs/contracts/{name}-v0.4-proposal.schema.json"
for name in ("task-discovery", "task-control", "call-execute")
}
v4_registry = registry.with_resource(event_schema["$id"], Resource.from_contents(event_schema))
v4_registry = registry
v4_examples = {name: sorted(EXAMPLES.glob(f"{name}-*-v0.4.json")) for name in v4_schema_paths}
v4_positive = v4_negative = 0
for name, schema_path in v4_schema_paths.items():
@@ -427,7 +427,6 @@ v4_artifacts = v4_manifest.get("artifacts", [])
required_v4 = {
*(p.relative_to(ROOT).as_posix() for p in v4_schema_paths.values()),
*(p.relative_to(ROOT).as_posix() for examples in v4_examples.values() for p in examples),
event_schema_path.relative_to(ROOT).as_posix(),
"contracts/upstream/v1/mq.schema.json",
}
v4_paths = [entry.get("path") for entry in v4_artifacts if isinstance(entry, dict)]