feat: implement local P1 contracts and Mock call flow
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
{
|
||||
"$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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
{
|
||||
"$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."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,20 @@
|
||||
# 只读配置与租户额度接口:任务、智能体、SIP 字段与返回结构 v0.1(项目提案)
|
||||
# 只读配置与租户额度接口:任务、智能体、SIP 字段与返回结构 v0.1(项目内 F01 规范)
|
||||
|
||||
**状态:项目自定义草案,非 SaaS 已有接口/实际 JSON、非已发布契约、非实现/验收。** 用户同意:截图可见的业务含义先映射为**项目定义的字段名**;截图没有但需求明确的结构由本项目设计。即使字段名与现有项目 Schema 或历史 OpenAPI 相同,也**不能**据此声称它是当前 SaaS 页面原有的后端键。SaaS 和 management 在 W01 签收前不得据此开始真实配置发布/拨号。
|
||||
**状态:项目内 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 响应证据**。旧 [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 响应证据**。本地 HTTP 请求/错误/分页语义以[第三方对接契约](../thirds/第三方对接事件与请求消费顺序_v0.1.md)为准。旧 [OpenAPI/MQ 只读索引](../references/OpenAPI与MQ字段索引_v0.1.md) 也仅为历史参考。
|
||||
- **N = 新项目字段:**任务每周多时段/排除日期、SIP 线路时段、任务与单 D 绑定、缓存/版本/错误返回等,由本提案定义;实际 SaaS 接口不存在已验证响应。
|
||||
|
||||
交付物:[机器可读响应草案](config-read-v0.1.schema.json)、[mock SIP 成功示例](examples/config-read-sip-v0.1.json)、[mock 任务成功示例](examples/config-read-task-v0.1.json)、[mock 租户额度示例](examples/config-read-tenant-quota-v0.1.json)。前两条接口为**拟定的只读 HTTP 配置读取**:`GET /internal/v1/dispatcher/sip` 与 `GET /internal/v1/dispatcher/task/:task_id`;下一轮另拟 `GET /internal/v1/dispatcher/tasks` 和 `?after=<cursor>` 发现归属任务;新增项目拟定 `GET /internal/v1/dispatcher/tenant/:tenant_id/quota` 按任务的租户 ID 读取本 D 的租户份额。四条请求携带 `X-DISPATCHER-id`(D UUID)及 `X-DISPATCHER-SECRET-KEY`(受控密钥),无请求体;实际 SaaS 实现仍待签收。业务呼叫/控制及回执/最终结果继续走 MQ,目标版本移除对外查询/补传;不留旧 AI/SIP 配置 MQ 回退。**HTTP 响应错误码、SIP 快照规范及 SaaS/management 实际来源仍需 W01/F07 冻结**,本文件不冒充外部权威发布物。
|
||||
路径来源:`/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 按本契约验证。
|
||||
|
||||
交付物:[机器可读响应 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=<cursor>` 与 `/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` 请求归属资源;不记录密钥或在日志中打印配置提示词。接口只读、无控制副作用;SaaS 必须核验任务归属。Header 的准确校验、密钥轮换时序、HTTP 错误状态仍需 SaaS 签收。`GET /internal/v1/dispatcher/tasks`(含 `?after=<cursor>`)是**第三条待 F07 签收的任务发现接口**,其快照/变更示例见[第三方对接 §2.5](../thirds/第三方对接事件与请求消费顺序_v0.1.md),不混入本文件的配置响应 Schema;第四条租户额度响应属于本文件新增草案,路径/字段未获 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` 的游标和分页错误按第三方契约 §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 状态码。
|
||||
|
||||
| 请求 | `200` 返回类型 | 何时读取 | 错误处理 |
|
||||
| --- | --- | --- | --- |
|
||||
@@ -22,7 +24,7 @@ Dispatcher 仅用 `X-DISPATCHER-id`(全局唯一 UUID)和部署受控的 `X-
|
||||
|
||||
`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 尚未修改。
|
||||
非 `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. 任务成功响应:字段与来源
|
||||
|
||||
@@ -76,7 +78,7 @@ Dispatcher 仅用 `X-DISPATCHER-id`(全局唯一 UUID)和部署受控的 `X-
|
||||
|
||||
静态制品中的端点**引用**与此提案补充的对端**值**属于同一获批版本;若 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,未签收)
|
||||
## 4.1 租户额度响应(项目内新增字段 N,外部未签收)
|
||||
|
||||
| 字段 | 类型/约束 | 业务语义 |
|
||||
| --- | --- | --- |
|
||||
@@ -88,13 +90,13 @@ Dispatcher 仅用 `X-DISPATCHER-id`(全局唯一 UUID)和部署受控的 `X-
|
||||
|
||||
D 在同一事务预留租户/任务/线路等占用,未知继续计入;降额不强挂、占用低于新上限才再接新。额度缺失、过期/错身份、刷新失败关闭新准入,不能以任务额度代替。已确认通话终结/执行资源释放即可释放通话额度,不等录音上传或MQ确认;stop静默ACK不需申请通话名额。多D须由SaaS分份额,累计不超过租户总额;本轮只验证单D。
|
||||
|
||||
## 5. 需冻结的语义与验收前置
|
||||
## 5. 外部待核事项(不阻塞本地 F01/C)
|
||||
|
||||
1. **来源:**确认 SaaS 的任务、智能体是该服务的权威配置;management 仍是唯一 SIP 编辑/审批方。确认 SaaS 分发的是已批准制品与线路补充字段同一版本,不能出现两份可写配置。
|
||||
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` 延长授权的通道。跨日窗口、重叠段、当日排除、时间边界及任务与线路交集须在合同冻结。
|
||||
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` 示例及错误/非法样例;它**不能**证明真实 SaaS 接口字段名、管理平台签收、hash 规范、Agent SDK 映射、SIP 实际加载或任何生产外呼验收。
|
||||
**验证状态:**本地 JSON Schema 草案包含 SIP、任务、租户额度 `200` 成功响应、有效错误响应及一个额外字段非法样例;只有 Schema 校验通过的有效示例可作为正例,非法样例必须被拒绝。此离线校验**不能**证明真实 SaaS 接口字段名、management 签收、hash 规范、Agent SDK 映射、SIP 实际加载或任何生产外呼验收。
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://go-sip.local/contracts/proposals/config-read-v0.1.schema.json",
|
||||
"title": "PROPOSAL: project-defined SaaS read-only configuration responses; NOT a published SaaS contract",
|
||||
"title": "Project-local F01 contract: read-only configuration responses; external SaaS compatibility unverified",
|
||||
"oneOf": [
|
||||
{"$ref": "#/$defs/sip_response"},
|
||||
{"$ref": "#/$defs/task_response"},
|
||||
@@ -36,8 +36,8 @@
|
||||
"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": "Also validate <=196 UTF-8 bytes and routing word boundaries using the project-owned tenant contract."},
|
||||
"task_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},
|
||||
@@ -65,7 +65,7 @@
|
||||
"max_concurrent_calls": {"type": "integer", "minimum": 0},
|
||||
"valid_until": {"type": "string", "format": "date-time"}
|
||||
},
|
||||
"$comment": "PROPOSAL: SaaS-assigned share for this Dispatcher; aggregate all tasks of the tenant. Validate tenant_id/tenant_key mapping and expiry in business checks."
|
||||
"$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",
|
||||
@@ -78,7 +78,7 @@
|
||||
"type": "object", "additionalProperties": false,
|
||||
"required": ["code", "message"],
|
||||
"properties": {
|
||||
"code": {"type": "string", "pattern": "^[a-z][a-z0-9_]{0,63}$"},
|
||||
"code": {"enum": ["invalid_request", "unauthorized", "dispatcher_not_authorized", "resource_not_found", "tenant_quota_unavailable", "service_unavailable"]},
|
||||
"message": {"type": "string", "minLength": 1, "maxLength": 256}
|
||||
}
|
||||
}
|
||||
@@ -159,7 +159,7 @@
|
||||
"sunday": {"$ref": "#/$defs/windows"}
|
||||
}
|
||||
},
|
||||
"windows": {"type": "array", "items": {"$ref": "#/$defs/window"}},
|
||||
"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"],
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
"schema_version": "config-read.v0.1",
|
||||
"resource": "error",
|
||||
"error": {
|
||||
"code": "not_assigned",
|
||||
"code": "resource_not_found",
|
||||
"message": "Task is not assigned to this Dispatcher."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"schema_version": "config-read.v0.1",
|
||||
"resource": "error",
|
||||
"error": {
|
||||
"code": "resource_not_found",
|
||||
"message": "Task is not assigned to this Dispatcher."
|
||||
},
|
||||
"unexpected": true
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"schema_version": "local-mock-recording-failure.v0.1",
|
||||
"upload_id": "upload-a",
|
||||
"recording_id": "recording-a",
|
||||
"error_code": "upload_authorization_expired"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"schema_version": "local-mock-recording-failure.v0.1",
|
||||
"upload_id": "upload-a",
|
||||
"recording_id": "recording-a",
|
||||
"error_code": "upload_timeout"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"schema_version": "local-mock-recording-failure.v0.1",
|
||||
"upload_id": "upload-a",
|
||||
"recording_id": "recording-a",
|
||||
"error_code": "upload_failed"
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"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."}}}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"schema_version": "task-discovery.v0.2-proposal",
|
||||
"dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6",
|
||||
"next_cursor": "opaque-watermark-004",
|
||||
"changes": [],
|
||||
"next_page_token": "obsolete"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"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"}
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"schema_version": "task-discovery.v0.2-proposal",
|
||||
"dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6",
|
||||
"next_cursor": "opaque-watermark-004",
|
||||
"changes": []
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"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": "14f89655b3565d3e2607e1509b5cfd272f090e7266ba352e16a8cccdd43aef32"
|
||||
},
|
||||
{
|
||||
"path": "docs/contracts/config-read-v0.1.schema.json",
|
||||
"sha256": "d3fbf066295916b5322fff44c98a9e847de592135885ddff057f7f089fa4dfea"
|
||||
},
|
||||
{
|
||||
"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."
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"manifest_version": "local-contract-manifest.v0.2",
|
||||
"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/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"}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
# 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 验证通过。
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"$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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"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.<dispatcher_id>.task.<task_id>.in",
|
||||
"binding_key": "same as routing_key",
|
||||
"queue_name": "agent-call.d.<dispatcher_id>.task.<task_id>.v3",
|
||||
"durable": true,
|
||||
"exclusive": false,
|
||||
"auto_delete": false,
|
||||
"dead_letter_exchange": "agent-call.dead-letter.v3",
|
||||
"dead_letter_routing_key": "d.<dispatcher_id>.dead-letter"
|
||||
},
|
||||
"control": {
|
||||
"owner": "saas",
|
||||
"exchange": "agent-call.dispatchers.v3",
|
||||
"routing_key": "d.<dispatcher_id>.control.in",
|
||||
"binding_key": "same as routing_key",
|
||||
"queue_name": "agent-call.d.<dispatcher_id>.control.v3",
|
||||
"durable": true,
|
||||
"exclusive": false,
|
||||
"auto_delete": false,
|
||||
"dead_letter_exchange": "agent-call.dead-letter.v3",
|
||||
"dead_letter_routing_key": "d.<dispatcher_id>.dead-letter"
|
||||
},
|
||||
"result": {
|
||||
"owner": "saas",
|
||||
"exchange": "agent-call.saas.v3",
|
||||
"routing_key": "d.<dispatcher_id>.out",
|
||||
"binding_key": "same as routing_key",
|
||||
"queue_name": "agent-call.saas.d.<dispatcher_id>.v3",
|
||||
"durable": true,
|
||||
"exclusive": false,
|
||||
"auto_delete": false
|
||||
},
|
||||
"dead_letter": {
|
||||
"owner": "saas",
|
||||
"exchange": "agent-call.dead-letter.v3",
|
||||
"routing_key": "d.<dispatcher_id>.dead-letter",
|
||||
"binding_key": "same as routing_key",
|
||||
"queue_name": "agent-call.d.<dispatcher_id>.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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
{
|
||||
"$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}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
{
|
||||
"$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}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
## 1. 范围、权威与状态
|
||||
|
||||
本文件保留现有外部命令/事件与内部职责目录,并明确本轮适用范围:**P1为1 Agent/1 Asterisk/单 Cell、至少3家SIP trunk 的契约与协议 fixture、单租户、静态配置、ASR-only与完整AI双模式;真实 SaaS/MQ 联调、双节点、第二 Cell、第二租户和生产切换延期第二阶段。** 全量目录不等于本轮全部实现;当前只交付设计,运行通过记录另见验收证据。
|
||||
本文件保留现有外部命令/事件与内部职责目录,并明确本轮适用范围:**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)及[第三方时序](../thirds/第三方对接事件与请求消费顺序_v0.1.md)与[归档计划§1.2/§8.2](../archive/plan-0918.md)。用户后续批准的**配置只读 HTTP**目标仅见[新计划](../plan-config-read-v0.1.md),尚未替换本现行契约。P1仍单活D;D1/D2仅用于本地路由隔离fixture,不开发多D协调。
|
||||
- **现行外部已发布 SaaS↔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;**“缺口”** 明确阻塞相应实现/验收。
|
||||
@@ -29,10 +29,10 @@
|
||||
|
||||
### 1.2 分期与本轮传输修订
|
||||
|
||||
- P1保留call.execute、控制/查询/整体补传/录音协调的既有业务语义及8种业务事件;旧7条HTTP路径全部废弃为接入方式,对应请求响应改经MQ。整体补传恢复原结果,不重新投call.execute执行。
|
||||
- **现行外部基线**保留旧 call.execute、控制/查询/整体补传/录音协调语义及8种业务事件;旧7条HTTP路径不再作为现行接入方式,对应请求响应经MQ。此项记录现行 v1,不是本轮本地目标。
|
||||
- 42操作/115组件仅为上游目录;不把管理平台30操作移入Dispatcher。按实际入口及引用闭包生成校验,来源包/只读索引仍完整留存,不通过删Schema缩小范围。
|
||||
- 单租户仅指启用策略:保留tenant_key精确路由、租户独立队列/复合幂等键、有界窗口和单 Cell 全局配额;双租户公平、第二 Cell 汇总和多Dispatcher协调另立第二阶段。
|
||||
- P1不新增任务MQ模式字段或临时接口。MQ-only所需的控制/查询/配置/上传请求响应必须经GAP-10正式发布,不能伪装为现有8类事件或宽松透传。ASR-only表达沿GAP-08;R04/R06在线改配延后,但最后许可、控制、静态维护屏障和持久恢复不能延后。
|
||||
- 本轮本地目标按第三方对接契约使用四条只读配置/发现 GET;命令/控制/必要回执与单份最终 `call.result` 仍走 MQ,不恢复 query/replay、拆分实时文字/拒联/录音事件,也不新增任务 mode 字段。旧 MQ-only GAP-10 只描述外部 v1 基线;本地 C 不等待外部发布,真实兼容性另行记录。ASR-only表达沿GAP-08;R04/R06在线改配延后,但最后许可、控制、静态维护屏障和持久恢复不能延后。
|
||||
|
||||
## 2. 角色、传输和可靠性边界
|
||||
|
||||
|
||||
Reference in New Issue
Block a user