447 lines
13 KiB
Markdown
447 lines
13 KiB
Markdown
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 消费四步;
|
||
```Plain Text
|
||
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 发布,不声明、绑定或删除队列。
|
||
```Plain Text
|
||
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列表配置
|
||
```HTTP
|
||
GET /internal/v1/dispatcher/sip HTTP/1.1
|
||
Host: <SaaS 服务地址>
|
||
X-DISPATCHER-id: c046b893-8628-4589-ae50-619d049248a6
|
||
X-DISPATCHER-SECRET-KEY: <密钥>
|
||
```
|
||
响应:
|
||
```JSON
|
||
{
|
||
"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`
|
||
```JSON
|
||
{
|
||
"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"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
任务配置
|
||
```HTTP
|
||
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 模式
|
||
```JSON
|
||
{
|
||
**"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**
|
||
}
|
||
}
|
||
}
|
||
```
|
||
任务列表
|
||
```HTTP
|
||
GET /internal/v1/dispatcher/tasks HTTP/1.1
|
||
Host: <SaaS 内网地址>
|
||
X-DISPATCHER-id: c046b893-8628-4589-ae50-619d049248a6
|
||
X-DISPATCHER-SECRET-KEY: <密钥>
|
||
```
|
||
增量请求
|
||
```HTTP
|
||
GET /internal/v1/dispatcher/tasks?after=1042 HTTP/1.1
|
||
Host: <SaaS 内网地址>
|
||
X-DISPATCHER-id: c046b893-8628-4589-ae50-619d049248a6
|
||
X-DISPATCHER-SECRET-KEY: <密钥>
|
||
```
|
||
响应:
|
||
```JSON
|
||
{
|
||
"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 获取配额信息
|
||
```HTTP
|
||
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: <密钥>
|
||
```
|
||
```JSON
|
||
{
|
||
"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 线路变更
|
||
```JSON
|
||
{
|
||
"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
|
||
}
|
||
}
|
||
```
|
||
任务控制
|
||
```JSON
|
||
{
|
||
"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
|
||
>
|
||
>
|
||
```JSON
|
||
{
|
||
"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
|
||
>
|
||
>
|
||
呼出
|
||
```JSON
|
||
{
|
||
"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
|
||
>
|
||
>
|
||
开始调度执行
|
||
```JSON
|
||
{
|
||
"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",
|
||
}
|
||
}
|
||
```
|
||
完成结果上报
|
||
成功
|
||
```JSON
|
||
{
|
||
"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"
|
||
}
|
||
}
|
||
}
|
||
```
|
||
无应答
|
||
```JSON
|
||
{
|
||
// ...
|
||
"payload": {
|
||
//...
|
||
"outcome": "no_answer",
|
||
"reason_code": 480,
|
||
"reason_message": "SIP ring timeout Reason",
|
||
"transcript": [],
|
||
"opt_out": false,
|
||
"recording": {}
|
||
}
|
||
}
|
||
```
|
||
|