13 KiB
13 KiB
SaaS Dispatcher 对接文档 v0.5
相关占位说明:
/internal/v1/dispatcher/xxxxx 路由按系统当前架构路由进行定义即可
X-DISPATCHER-ID/X-DISPATCHER-SECRET-KEY HEADER 头KEY 获取忽略大小写
Dispatcher 角色下称 D
触发顺序
1.1 MQ 发布消费规则
RabbitMQ 有发布入口 exchange → 发布时指定的 routing key → 预先绑定的 queue → D 消费四步;
SaaS→D exchange: agent-call.dispatchers.v1
routing key: d.<dispatcher_id>.t.<tenant_id>.in
binding key: d.<dispatcher_id>.t.<tenant_id>.in
queue: agent-call.d.<dispatcher_id>.t.<tenant_id>.v1
consumer: 对应 Dispatcher
D→SaaS exchange: agent-call.saas.v1
routing key: d.<dispatcher_id>.t.<tenant_id>.out
queue: agent-call.saas.events.v1
consumer: SaaS
1.2 本轮任务队列与事件路由 硬边界:所有 exchange/queue/binding 均由 SaaS 创建、维护和退役;D 只消费 SaaS 创建的任务/控制队列,并向 SaaS 创建的结果 exchange 发布,不声明、绑定或删除队列。
SaaS 创建并绑定:
exchange: agent-call.dispatchers.v1 (topic, durable)
task routing: d.<dispatcher_id>.task.<task_id>.in
task queue: agent-call.d.<dispatcher_id>.task.<task_id>.v1
control route: d.<dispatcher_id>.control.in
control queue: agent-call.d.<dispatcher_id>.control.v1
dead-letter: agent-call.dead-letter.v1
D -> SaaS exchange: agent-call.saas.v1 (topic, durable)
result route: d.<dispatcher_id>.out
SaaS result queue: agent-call.saas.d.<dispatcher_id>.v1
HTTP请求 SIP列表配置
GET /internal/v1/dispatcher/sip HTTP/1.1
Host: <SaaS 服务地址>
X-DISPATCHER-id: c046b893-8628-4589-ae50-619d049248a6
X-DISPATCHER-SECRET-KEY: <密钥>
响应:
{
"resource": "sip_config", // 资源类型:SIP 配置
"dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6", // 这份配置所属的 Dispatcher
"trunks": [ // 该 Dispatcher 获批的线路列表
{
"trunk_id": "trunk-mock", // 线路的唯一标识
"provider_id": "provider-mock", // SIP 服务商标识
"codec": "PCMA", // 线路使用的语音编码
"dial_prefix": "", // 该线路拨号时添加的被叫前缀;空串表示不添加
"enabled": true, // 是否启用这条线路
"server_host": "sip.example.invalid", // SIP 服务端地址;此处是 Mock 地址
"server_port": 5060, // SIP 服务端端口
"transport": null, // 传输方式:udp、tcp、tls;null 表示尚未确认
"auth_mode": null, // 鉴权方式:ip、digest、none;null 表示尚未确认
"registration_required": null, // 是否需要 SIP 注册;null 表示尚未确认
"max_concurrent_calls": null, // 分配给该 Dispatcher 的线路并发上限;null 表示尚未确认
"caller_profiles": [ // 这条线路可用的主叫配置
{
"caller_profile_id": "caller-profile-mock", // 主叫配置标识,供任务引用
"caller_id": "BD00000000" // 实际主叫标识,保留原值及字母
}
],
"schedule": { // 这条线路允许外呼的每周时段
"time_zone": "Asia/Shanghai", // 判断时段使用的时区
"weekly_windows": { // 每天可配置多个时段;空数组表示当天不允许外呼
"monday": [{"start": "09:00", "end": "20:00"}], // 周一
"tuesday": [{"start": "09:00", "end": "20:00"}], // 周二
"wednesday": [{"start": "09:00", "end": "20:00"}], // 周三
"thursday": [{"start": "09:00", "end": "20:00"}], // 周四
"friday": [{"start": "09:00", "end": "20:00"}], // 周五
"saturday": [], // 周六不允许外呼
"sunday": [] // 周日不允许外呼
} }
} ]
}
AI 服务商全量读取
GET /internal/v1/dispatcher/ai-providers
{
"resource": "ai_providers",
"dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6",
"providers": [
{
"provider_ref": "asr-provider-a",
"role": "asr",
"enabled": true,
"adapter": "volcengine_asr",
"endpoint": "https://asr.example.invalid",
"credential": "managed-asr-a"
},
{
"provider_ref": "llm-provider-a",
"role": "llm",
"enabled": true,
"adapter": "openai_compatible",
"endpoint": "https://llm.example.invalid/v1",
"credential": "managed-llm-a"
},
{
"provider_ref": "tts-provider-a",
"role": "tts",
"enabled": true,
"adapter": "volcengine_tts",
"endpoint": "https://tts.example.invalid",
"credential": "managed-tts-a"
}
]
}
任务配置
GET /internal/v1/dispatcher/task/{task_id} HTTP/1.1
Host: <SaaS 服务地址>
X-DISPATCHER-id: c046b893-8628-4589-ae50-619d049248a6
X-DISPATCHER-SECRET-KEY: <密钥>
响应体: 仅 ASR 模式(Agent配置仅返回 ASR即可) ASR + LLM + TTS 模式
{
**"resource"**: **"task_config"**,
**"dispatcher_id"**: **"c046b893-8628-4589-ae50-619d049248a6"**,
**"tenant_id"**: **"tenant-id-mock"**,
**"task_id"**: **"task-mock"**,
**"task_revision"**: **2**,
**"status"**: **"running"**,
**"name"**: **"Mock task"**,
**"max_concurrent_calls"**: **2**,
**"ring_timeout_ms"**: **30000**,
**"max_call_duration_ms"**: **120000**,
**"route_policy_id"**: **"route-mock"**,
**"caller_profile_id"**: **"caller-profile-mock"**,
**"allowed_trunk_ids"**: [
**"trunk-mock"**
],
**"schedule"**: {
**"time_zone"**: **"Asia/Shanghai"**,
**"starts_at"**: **"2026-09-21T00:00:00+08:00"**,
**"ends_at"**: **null**,
**"weekly_windows"**: {
**"monday"**: [
{
**"start"**: **"09:00"**,
**"end"**: **"11:00"**
},
{
**"start"**: **"14:00"**,
**"end"**: **"18:00"**
}
],
**"tuesday"**: [
{
**"start"**: **"09:00"**,
**"end"**: **"18:00"**
}
],
**"wednesday"**: [
{
**"start"**: **"09:00"**,
**"end"**: **"18:00"**
}
],
**"thursday"**: [
{
**"start"**: **"09:00"**,
**"end"**: **"18:00"**
}
],
**"friday"**: [
{
**"start"**: **"09:00"**,
**"end"**: **"18:00"**
}
],
**"saturday"**: [
],
**"sunday"**: [
]
},
**"excluded_dates"**: [
**"2026-10-01"**,
**"2026-10-02"**
]
},
**"agent"**: {
**"immutable"**: **true**,
**"mode"**: **"full_ai"**,
**"llm"**: {
**"provider_ref"**: **"mock"**,
**"model"**: **"mock-chat-v1"**,
**"temperature"**: **0.2**,
**"max_tokens"**: **256**,
**"timeout_ms"**: **5000**
},
**"prompt"**: {
**"text"**: **"Mock prompt for an isolated test."**,
**"allowed_variables"**: [
],
**"max_bytes"**: **32768**
},
**"tts"**: {
**"provider_ref"**: **"mock"**,
**"model"**: **"mock-tts-v1"**,
**"voice"**: **"mock-neutral"**,
**"speed"**: **1**,
**"format"**: {
**"encoding"**: **"pcm_s16le"**,
**"sample_rate_hz"**: **16000**,
**"channels"**: **1**
},
**"timeout_ms"**: **5000**
},
**"asr"**: {
**"provider_ref"**: **"mock"**,
**"language"**: **"zh-CN"**,
**"input"**: {
**"encoding"**: **"pcm_s16le"**,
**"sample_rate_hz"**: **16000**,
**"channels"**: **1**,
**"sample_width_bytes"**: **2**
},
**"interim"**: **true**,
**"timeout_ms"**: **5000**
},
**"conversation"**: {
**"opening"**: **"开场白"**,
**"hangup_keywords"**: [
**"不用了"**,
**"请挂机"**
],
**"allow_interrupt"**: **true**,
**"silence_timeout_ms"**: **3000**,
**"max_duration_ms"**: **120000**,
**"max_turns"**: **20**,
**"sentence_max_chars"**: **80**,
**"max_pending_audio_chunks"**: **32**
}
}
}
任务列表
GET /internal/v1/dispatcher/tasks HTTP/1.1
Host: <SaaS 内网地址>
X-DISPATCHER-id: c046b893-8628-4589-ae50-619d049248a6
X-DISPATCHER-SECRET-KEY: <密钥>
增量请求
GET /internal/v1/dispatcher/tasks?after=1042 HTTP/1.1
Host: <SaaS 内网地址>
X-DISPATCHER-id: c046b893-8628-4589-ae50-619d049248a6
X-DISPATCHER-SECRET-KEY: <密钥>
响应:
{
"schema_version": "task-discovery.v0.2-proposal",
"dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6",
"cursor": "1042",
"tasks": [
{
"task_id": "task-a",
"tenant_id": 1001,
"status": "running",
"task_revision": 1
},
{
"task_id": "task-old",
"tenant_id": 1001,
"status": "stopped",
"task_revision": 3
}
]
}
按租户 ID 获取配额信息
GET /internal/v1/dispatcher/tenant/{tenant-id}/quota HTTP/1.1
Host: <SaaS 内网地址>
X-DISPATCHER-id: c046b893-8628-4589-ae50-619d049248a6
X-DISPATCHER-SECRET-KEY: <密钥>
{
"resource": "tenant_quota",
"dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6",
"tenant_id": "tenant-id-mock",
"quota_revision": 1,
"max_concurrent_calls": 3
}
控制事件
队列:agent-call.d.<dispatcher_id>.control.v1
SIP 线路变更
{
"event_id": "7cd23165-c88e-4f36-8cf2-b5e9ac67ecdb",
"event_type": "sip.config",
"dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6",
"payload": {
"trunk_id": "trunk-mock",
"change_type": "disabled", // enabled/removed/created
"revision": 8
}
}
任务控制
{
"dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6",
"tenant_id": 1001,
"issued_at": "2026-09-21T00:00:00Z",
"event_id": "7cd23165-c88e-4f36-8cf2-b5e9ac67ecdb",
"event_type":: "task.control",
"payload": {
"task_id": "task-a",
"action": "pause", // resume , stop
"reason": "local-test",
"options": { // 附加控制参数
"active_call_policy": "drain", // 活跃通话策略,仅stop,pause事件生效 [drain|hangup]
}
}
}
任务控制处理回执
队列 agent-call.saas.v1
{
"event_id": "7cd23165-c88e-4f36-8cf2-b5e9ac67ecdb",
"event_type":: "task.control",
"dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6",
"tenant_id": 1001,
"payload": {
"status": "applied"
}
}
外呼任务队列
队列:agent-call.d.<dispatcher_id>.t.<tenant_id>.v1
呼出
{
"event_id": "7cd23165-c88e-4f36-8cf2-b5e9ac67ecdb",
"event_type":: "call.execute",
"dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6",
"tenant_id": 1001,
"issued_at": "2026-09-18T10:00:00+08:00"
"payload": {
"task_id": "task-a",
"callee": "15003164745"
}
}
回执
队列 agent-call.saas.v1
开始调度执行
{
"event_id": "7cd23165-c88e-4f36-8cf2-b5e9ac67ecdb",
"event_type":: "call.execute",
"dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6",
"tenant_id": 1001,
"issued_at": "2026-09-18T10:00:00+08:00"
"payload": {
"status": "dispatched",
}
}
完成结果上报 成功
{
"event_id": "call-result-001",
"event_type": "call.execute.result",
"dispatcher_id": "c046b893-8628-4589-ae50-619d049248a6",
"tenant_id": 1001,
"issued_at": "2026-09-18T10:10:15+08:00",
"payload": {
"task_id": "task-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",
"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"
}
}
}
无应答
{
// ...
"payload": {
//...
"outcome": "no_answer",
"reason_code": 480,
"reason_message": "SIP ring timeout Reason",
"transcript": [],
"opt_out": false,
"recording": {}
}
}