diff --git a/contracts/local/v0.1/config-read-v0.1.schema.json b/contracts/local/v0.1/config-read-v0.1.schema.json index e0f239c..bcbaa10 100644 --- a/contracts/local/v0.1/config-read-v0.1.schema.json +++ b/contracts/local/v0.1/config-read-v0.1.schema.json @@ -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"}, diff --git a/contracts/local/v0.4/task-discovery-v0.4-proposal.schema.json b/contracts/local/v0.4/task-discovery-v0.4-proposal.schema.json index 4f13a0a..0134da5 100644 --- a/contracts/local/v0.4/task-discovery-v0.4-proposal.schema.json +++ b/contracts/local/v0.4/task-discovery-v0.4-proposal.schema.json @@ -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"], diff --git a/contracts/local_v04_test.go b/contracts/local_v04_test.go index 8353b3f..9169a18 100644 --- a/contracts/local_v04_test.go +++ b/contracts/local_v04_test.go @@ -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) diff --git a/docs/contracts/config-read-fields-v0.1-proposal.md b/docs/contracts/config-read-fields-v0.1-proposal.md index ec5d9ea..700eebf 100644 --- a/docs/contracts/config-read-fields-v0.1-proposal.md +++ b/docs/contracts/config-read-fields-v0.1-proposal.md @@ -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=`)为用户给定路径;`/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 实际加载或任何生产外呼验收。 diff --git a/docs/contracts/config-read-v0.1.schema.json b/docs/contracts/config-read-v0.1.schema.json index e0f239c..bcbaa10 100644 --- a/docs/contracts/config-read-v0.1.schema.json +++ b/docs/contracts/config-read-v0.1.schema.json @@ -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"}, diff --git a/docs/contracts/local-contract-manifest-v0.1.json b/docs/contracts/local-contract-manifest-v0.1.json index f019006..977bf52 100644 --- a/docs/contracts/local-contract-manifest-v0.1.json +++ b/docs/contracts/local-contract-manifest-v0.1.json @@ -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", diff --git a/docs/contracts/local-contract-manifest-v0.2.json b/docs/contracts/local-contract-manifest-v0.2.json index 6c400f8..0396983 100644 --- a/docs/contracts/local-contract-manifest-v0.2.json +++ b/docs/contracts/local-contract-manifest-v0.2.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"}, diff --git a/docs/contracts/local-contract-manifest-v0.3.json b/docs/contracts/local-contract-manifest-v0.3.json index 610a147..07b7d71 100644 --- a/docs/contracts/local-contract-manifest-v0.3.json +++ b/docs/contracts/local-contract-manifest-v0.3.json @@ -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"} diff --git a/docs/contracts/local-contract-manifest-v0.4.json b/docs/contracts/local-contract-manifest-v0.4.json index 1900424..f34f12f 100644 --- a/docs/contracts/local-contract-manifest-v0.4.json +++ b/docs/contracts/local-contract-manifest-v0.4.json @@ -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" diff --git a/docs/contracts/task-discovery-v0.4-proposal.schema.json b/docs/contracts/task-discovery-v0.4-proposal.schema.json index 4f13a0a..0134da5 100644 --- a/docs/contracts/task-discovery-v0.4-proposal.schema.json +++ b/docs/contracts/task-discovery-v0.4-proposal.schema.json @@ -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"], diff --git a/docs/thirds/v0.4.md b/docs/thirds/v0.4.md index b1868e9..532e126 100644 --- a/docs/thirds/v0.4.md +++ b/docs/thirds/v0.4.md @@ -15,7 +15,7 @@ - 在线 `GET /internal/v1/dispatcher/tasks?after=` 返回 `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. 控制、通话与积压命令边界 diff --git a/internal/contract/schema_test.go b/internal/contract/schema_test.go index 4b42e9e..08aabfa 100644 --- a/internal/contract/schema_test.go +++ b/internal/contract/schema_test.go @@ -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") } diff --git a/scripts/validate-local-contracts.py b/scripts/validate-local-contracts.py index 81af9ba..23e53e3 100644 --- a/scripts/validate-local-contracts.py +++ b/scripts/validate-local-contracts.py @@ -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)]