API 集成文档
面向外部系统开发者:如何调用本服务的 HTTP API,把「AI 外呼任务」(Task)或
「AI 语音秘书」(VoiceAgent)能力接入你自己的业务系统。本文档只讲 API 调用,
不涉及部署/运维——部署相关内容见仓库根目录 README.md。
本文档面向要把本服务当作外部依赖来调用的开发者,可以整份发给合作方/客户, 不需要访问仓库源码。仓库内部的
README.md面向的是维护这个服务本身的人, 两者内容有重叠但侧重不同。
两种业务能力
| Task(外呼任务) | VoiceAgent(语音秘书) | |
|---|---|---|
| 适用场景 | 一次性、有明确交付标准的外呼,比如"确认对方明天能否参会" | 常驻的 AI 客服/秘书入口,挂在官网/IM 签名上,任何人随时可点开咨询 |
| 链接生命周期 | 一次性,有效期到期或用过一次后失效(见状态机) | 长期有效,可反复使用、多人同时使用 |
| 每次调用产出 | 1 个 task_id + 1 条通话链接 + 1 份任务报告 |
1 个 agent_id(画像)+ 1 条长期链接;每次通话各自产生 1 条 session_id 会话记录 |
| 谁发起通话 | 你指定的通话对象(participant_label),你把链接发给对方 |
任何点开链接的人,身份不预先知道 |
| 通话目标 | 单次指定(task_description + success_criteria) |
绑定在画像上、长期复用(call_instructions + success_criteria) |
两者共用同一套通话引擎、人设/音色目录、计费与报告生成逻辑,账号和 API Key 也 是同一套(一个账号名下的 Key 能同时调 Task 和 VoiceAgent 两套接口),选哪个 纯粹取决于你的业务场景是"一次性任务"还是"常驻入口"。
典型集成流程(两种业务共通):
- 你的系统调用创建接口(
POST /api/tasks或POST /api/agents),拿到一条call_url。 - 把这条链接发给通话对象(短信/IM/官网按钮等,由你的系统决定渠道),对方在 浏览器打开链接、点击接听,AI 通过实时语音完成对话。
- 通话结束后,服务端自动生成结构化报告(
report,含verdict判定结果)。 - 你的系统轮询查询接口(
GET /api/tasks/{task_id}或GET /api/agents/{agent_id}/sessions/{session_id})获取状态和报告,或者按 自己的节奏定期拉取列表接口。
轮询和回调二选一:默认走轮询(建议间隔不低于 10-15 秒)。如果不想轮询, 创建时填
result_callback_url,通话结束后本服务会把结果 POST 给你,见 「Advanced 模式」。
快速开始
export API_BASE="https://your-deployment.example.com" # 替换成实际部署地址
# 0. 注册账号(邮箱唯一,密码至少 8 位)——只建号 + 发验证邮件,不返回登录态
curl -X POST "$API_BASE/api/auth/register" -H "Content-Type: application/json" \
-d '{"email": "you@example.com", "password": "at-least-8-characters"}'
# 0.1 打开验证邮件里的链接(?verify_token=...),或直接拿这个 token 调接口完成验证——
# 验证成功即完成注册并自动签发第一个登录态,响应里的 session_token 只显示这一次
curl -X POST "$API_BASE/api/auth/verify-email" -H "Content-Type: application/json" \
-d '{"token": "<邮件链接里的 verify_token>"}'
export SESSION_TOKEN="<上一步返回的 session_token>"
# 已验证过的账号之后直接用 /api/auth/login 换新的 session_token
# 0.5 用登录态创建一把业务用的 API Key,响应里的 api_key 只显示这一次,立即保存
curl -X POST "$API_BASE/api/auth/keys" -H "Authorization: Bearer $SESSION_TOKEN" \
-H "Content-Type: application/json" -d '{"label": "生产环境"}'
export API_KEY="<上一步返回的 api_key>"
# 1. 创建一次外呼任务
curl -X POST "$API_BASE/api/tasks" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"requester": "crm-system",
"task_description": "确认对方明天下午2点是否有空参加会议",
"success_criteria": "明确得到对方是否能参加的答复",
"participant_label": "张先生",
"persona_id": "steady_professional"
}'
# 响应里的 call_url 就是要发给通话对象的链接,把它发出去即可
浏览器打开 call_url 后会展示一个通话页面(用 getUserMedia 采集麦克风,浏览
器要求安全上下文,即 HTTPS 或 localhost),对方点击接听即可开始通话,你的系统
不需要参与通话本身,只需要创建任务/画像和之后查询结果。
也可以跳过上面的 curl,直接在浏览器打开 $API_BASE/(首页/账号中心)完成
注册/登录/创建 Key,界面上复制粘贴即可,不需要自己拼请求。
完整的接口文档(带侧边栏导航,可以整份发给外部合作方):$API_BASE/docs。
账号与鉴权
先注册账号,登录后创建业务用的 API Key,调 Task/VoiceAgent 接口时带这把 Key——每把 Key 只能访问它归属账号自己创建的任务/画像/会话,看不到、也改不了 别的账号的数据,不需要你自己在请求里传任何"我是谁"的参数。
两套凭证完全独立,别混用:
登录态(sess_...) |
API Key(sk-live-...) |
|
|---|---|---|
| 怎么拿到 | POST /api/auth/register / POST /api/auth/login |
登录态调用 POST /api/auth/keys 创建 |
| 用来调什么 | 只能调 /api/auth/*(账号管理:创建/查看/吊销 Key) |
只能调业务接口:/api/tasks、/api/agents(含子资源) |
| 有效期 | 有 TTL,到期需要重新登录(默认 30 天,具体以部署方配置为准) | 不过期,只能手动吊销 |
| 能有几个 | 每次登录一个,互不影响 | 任意多个,建议按环境/用途分开创建,出问题时单独吊销 |
用错类型的 token 会被拒绝,不会"降级生效":
| 情况 | HTTP 状态码 |
|---|---|
缺少 Authorization header |
401,{"detail": "缺少 Authorization: Bearer <api_key>"}(或 <session_token>) |
| API Key 无效/已吊销/已过期账号 | 401,{"detail": "API key 无效"} |
| 登录态无效/已吊销 | 401,{"detail": "登录状态无效,请重新登录"} |
| 登录态已过期 | 401,{"detail": "登录状态已过期,请重新登录"} |
| 拿登录态去调业务接口,或拿 API Key 去调账号管理接口 | 401(两套 token 分别校验,类型不对直接查不到) |
不需要鉴权的接口:
GET /api/personas、GET /api/agent-voices——这两个是纯静态 目录数据,方便你在拿到 Key 之前就能先渲染选择器。
账号 API:注册、登录、API Key 管理
Base path /api/auth。register/login/verify-email/resend-verification/
forgot-password/reset-password 是公开接口,不需要鉴权(拿不到登录态的场景本来
就要靠它们自救);其余接口都要求登录态(Authorization: Bearer <session_token>),
不接受业务 API Key。
注册是两步:POST /register 只建号并发出验证邮件,不返回登录态;邮箱验证
通过才算完成注册,第一个 session_token 由 POST /verify-email 签发。未验证的账号
调 POST /login 会被 403 拒绝。
POSTPOST /api/auth/register
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
email |
string | ✅ | 邮箱,账号唯一标识,不区分大小写 |
password |
string(≥8) | ✅ | 密码,至少 8 位 |
curl -X POST "$API_BASE/api/auth/register" -H "Content-Type: application/json" \
-d '{"email": "you@example.com", "password": "at-least-8-characters"}'
响应 200——注意这里不返回登录态,只是建号并往这个邮箱发了一封验证邮件:
{"message": "注册成功,请查收邮箱完成验证后登录"}
邮箱已被注册过时返回 409({"detail": "该邮箱已注册,请直接登录"})。
每个 IP 每小时最多注册 5 次,超出返回 429。
POSTPOST /api/auth/verify-email
验证邮件里的链接形如 {部署域名}/?verify_token=<token>(有效期 24 小时,打开即由
首页自动完成验证)。要在自己的系统里代办这一步,就把 verify_token 取出来调这个
接口:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
token |
string | ✅ | 验证邮件链接里的 verify_token |
curl -X POST "$API_BASE/api/auth/verify-email" -H "Content-Type: application/json" \
-d '{"token": "<邮件链接里的 verify_token>"}'
验证成功 = 完成注册 + 自动登录,响应直接签发这个账号的第一个登录态(结构同
login),不需要再单独调一次 /login:
{
"session_token": "sess_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"expires_at": "2026-08-16T02:00:00Z",
"user": {
"user_id": "user_1a2b3c4d5e6f7890",
"email": "you@example.com",
"status": "active",
"email_verified": true,
"quota_limit_amount": null,
"quota_used_amount": 0.0,
"created_at": "2026-07-17T02:00:00Z"
}
}
session_token 只在这一次响应里出现,请求方需要自己保存;忘记了不能找回,
只能重新 /login 换一个新的。token 无效/已用过/已过期返回 400
({"detail": "链接无效或已过期,请重新申请"}),这时用下面的重发接口再要一封。
POSTPOST /api/auth/resend-verification
验证邮件丢了/过期了用这个重新要一封。公开接口——账号在验证之前根本拿不到登录态, 只能按邮箱重发。
curl -X POST "$API_BASE/api/auth/resend-verification" -H "Content-Type: application/json" \
-d '{"email": "you@example.com"}'
无论邮箱是否存在、是否已验证,都返回同样的 200
({"message": "如果该邮箱已注册且尚未验证,我们已重新发送验证邮件"})——不通过
响应差异暴露账号是否存在。每个 IP 每小时最多 5 次,超出返回 429。
POSTPOST /api/auth/login
字段同 register。响应结构同 verify-email(新签发一个 session_token,旧的
登录态不受影响,可以多端同时登录)。
curl -X POST "$API_BASE/api/auth/login" -H "Content-Type: application/json" \
-d '{"email": "you@example.com", "password": "at-least-8-characters"}'
| 情况 | 响应 |
|---|---|
| 邮箱不存在 / 密码错误 | 401,{"detail": "邮箱或密码错误"}(两种情况文案和耗时都一致,避免被用来探测邮箱是否已注册) |
| 邮箱尚未验证 | 403,{"detail": "邮箱尚未验证,请查收验证邮件完成验证后再登录"} |
| 账号已被禁用 | 403,{"detail": "账号已被禁用,请联系管理员"} |
| 超出限流 | 429(每 IP 每 5 分钟 10 次、每邮箱每 5 分钟 5 次) |
POSTPOST /api/auth/forgot-password / POST /api/auth/reset-password
忘记密码走这两步,不需要联系部署方。
# 1. 申请重置邮件(公开接口)
curl -X POST "$API_BASE/api/auth/forgot-password" -H "Content-Type: application/json" \
-d '{"email": "you@example.com"}'
# 2. 拿邮件链接({部署域名}/?reset_token=<token>,有效期 30 分钟)里的 token 设新密码
curl -X POST "$API_BASE/api/auth/reset-password" -H "Content-Type: application/json" \
-d '{"token": "<邮件链接里的 reset_token>", "password": "new-at-least-8-chars"}'
第一步无论邮箱是否存在都返回同样的 200
({"message": "如果该邮箱已注册,我们已发送密码重置邮件"}),每个 IP 每小时最多
5 次。第二步成功返回 204,并吊销该账号此前所有登录态(密码可能已泄露,旧登录
态一律作废,需要重新登录)——已创建的 API Key 不受影响,照常可用。token 无效/已用过/
已过期返回 400。
POSTPOST /api/auth/change-password
已登录状态下主动改密码,靠当前密码证明身份,不需要邮件 token。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
current_password |
string | ✅ | 当前密码 |
new_password |
string(≥8) | ✅ | 新密码,至少 8 位 |
curl -X POST "$API_BASE/api/auth/change-password" -H "Authorization: Bearer $SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{"current_password": "old-password", "new_password": "new-at-least-8-chars"}'
成功返回 204,并吊销除当前这条之外的所有登录态(发起改密的这一端不会被自己
登出)。当前密码不正确返回 400({"detail": "当前密码不正确"})。
POSTPOST /api/auth/logout
curl -X POST "$API_BASE/api/auth/logout" -H "Authorization: Bearer $SESSION_TOKEN"
吊销当前这一个登录态(204),不影响用同一账号在别处登录产生的其它登录态、
也不影响任何 API Key。
GETGET /api/auth/me
curl "$API_BASE/api/auth/me" -H "Authorization: Bearer $SESSION_TOKEN"
返回当前登录账号信息(结构同 verify-email/login 响应里的 user 字段),
常用来确认 session_token 是否还有效。其中 quota_limit_amount 是部署方给这个账号
配的总额度上限(元,null 表示不限制),quota_used_amount 是累计已产生的费用——
额度用尽后新通话会被拒绝,见「已知限制」。
POSTPOST /api/auth/keys:创建 API Key
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
label |
string(≤100) | 否 | 给这把 Key 起个名字,方便在列表里区分用途,比如 "生产环境" |
curl -X POST "$API_BASE/api/auth/keys" -H "Authorization: Bearer $SESSION_TOKEN" \
-H "Content-Type: application/json" -d '{"label": "生产环境"}'
{
"key_id": "key_1a2b3c4d5e6f7890",
"label": "生产环境",
"api_key": "sk-live-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"display_prefix": "sk-live-xxxx**********xxxx",
"created_at": "2026-07-17T02:05:00Z"
}
api_key 只在这一次响应里出现,之后无法再找回,只能看到 display_prefix
这种打了码的展示形式——弄丢了不能补发,只能重新创建一把新的、把旧的吊销。
GETGET /api/auth/keys:查看自己名下的 Key
curl "$API_BASE/api/auth/keys" -H "Authorization: Bearer $SESSION_TOKEN"
{
"items": [
{
"key_id": "key_1a2b3c4d5e6f7890",
"label": "生产环境",
"display_prefix": "sk-live-xxxx**********xxxx",
"created_at": "2026-07-17T02:05:00Z",
"last_used_at": "2026-07-17T03:00:00Z",
"revoked_at": null
}
]
}
last_used_at 每次这把 Key 成功调用业务接口都会更新,方便判断哪些 Key 长期
没用、可以放心吊销。
DELETEDELETE /api/auth/keys/{key_id}:吊销 Key
curl -X DELETE "$API_BASE/api/auth/keys/key_1a2b3c4d5e6f7890" \
-H "Authorization: Bearer $SESSION_TOKEN"
204,立即生效——吊销后这把 Key 再调任何业务接口都返回 401,且不可逆,
没有"恢复"操作,只能创建一把新的。key_id 不存在或不属于当前账号时返回 404。
公共资源:人设与音色目录
GETGET /api/personas(Task 场景用,无需鉴权)
创建 Task 时必须从这里选一个 persona_id(决定通话时的语气风格和音色)。
curl "$API_BASE/api/personas"
[
{
"id": "warm_friendly",
"display_name": "温暖亲和",
"description": "热情随和,像朋友一样自然聊天,适合日常提醒、温情联络类任务",
"bot_name": "小暖",
"gender": "女", "age": 24, "city": "成都", "occupation": "外呼助手客服专员",
"personality": "……",
"voice_name": "zh_female_vv_jupiter_bigtts"
},
{ "id": "steady_professional", "display_name": "沉稳专业", "...": "适合正式确认、商务沟通类任务" },
{ "id": "crisp_efficient", "display_name": "干练利落", "...": "适合信息核实、时间敏感类任务" },
{ "id": "sunny_lively", "display_name": "阳光活泼", "...": "年轻男声,适合活动邀约、青年用户回访类任务" }
]
| persona_id | 名称 | 适用场景 |
|---|---|---|
warm_friendly |
温暖亲和 | 日常提醒、温情联络类任务 |
steady_professional |
沉稳专业 | 正式确认、商务沟通类任务 |
crisp_efficient |
干练利落 | 信息核实、时间敏感类任务 |
sunny_lively |
阳光活泼 | 活动邀约、青年用户回访类任务(年轻男声) |
人设列表由部署方配置,可能会新增/调整,不要在你的系统里硬编码这份列表, 建议启动时或定期拉取一次并缓存。
GETGET /api/agent-voices(VoiceAgent 场景用,无需鉴权)
创建/编辑 VoiceAgent 画像时选择 voice_name 用:
curl "$API_BASE/api/agent-voices"
[
{"voice_name": "zh_female_vv_jupiter_bigtts", "label": "vivi · 温暖知性", "gender": "女"},
{"voice_name": "zh_female_xiaohe_jupiter_bigtts", "label": "小何 · 干练利落", "gender": "女"},
{"voice_name": "zh_male_yunzhou_jupiter_bigtts", "label": "云舟 · 沉稳专业", "gender": "男"},
{"voice_name": "zh_male_xiaotian_jupiter_bigtts", "label": "小天 · 清朗年轻", "gender": "男"}
]
Task API(外呼任务)
Base path:/api/tasks,全部需要鉴权。
POST创建任务 POST /api/tasks
请求体
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
requester |
string(1-200) | ✅ | 你账号内部的标识(自由文本,比如你自己系统里的子系统名/员工名),仅用于之后按 requester 筛选任务列表,不参与鉴权——真正决定"这个任务归谁"的是创建时用的 API Key 所属账号 |
task_description |
string | ✅ | 外呼任务描述,会拼进通话人设的 system prompt,即"这通电话要做什么" |
success_criteria |
string | 否 | 交付标准,自然语言描述,通话结束后 AI 据此判定任务是否达成(verdict)。不传则跳过报告生成,report 恒为 null——适合没有"达成标准"概念的场景(见 Advanced 模式) |
participant_label |
string(1-100) | ✅ | 通话对象的称呼(比如"张先生"),不需要是注册用户,仅用于让 AI 在通话中礼貌称呼对方 |
persona_id |
string | ✅ | 通话人设 + 音色,取值见 GET /api/personas |
call_link_ttl_seconds |
int | 否 | 覆盖默认的链接有效期(秒),不传则用部署方配置的默认值 |
reusable |
bool | 否 | 是否允许链接在产生一次有效对话后继续复用、在有效期内发起下一轮通话,默认 false(一次有效对话后链接即失效,行为与不传该字段完全一致)。复用次数上限由部署方内部设置,不通过这个字段开放,达到上限后链接按到期处理,见下方状态机 |
record_audio |
bool | 否 | 把本通通话音频混成单轨 WAV 落盘(默认 false),随结果回调/查询接口交付,见「通话录音与单通时长覆盖」 |
max_call_duration_seconds |
int(1-3600) | 否 | 本任务单通通话的时长上限(秒),不传沿用部署全局默认(600)。到点前端 5 秒倒计时优雅退出、不配合的客户端由服务端强制挂断 |
explicit_dialect |
string | 否 | TTS 输出的方言口音(改变合成语音的口音,不是识别矫正),取值 dongbei/sichuan/shaanxi/yue/beijing/henan/tianjin/shanghai;不传 = 默认普通话。音色与方言的组合合法性由调用方负责,非法组合语音引擎会报错。传入白名单外的 code 会返回 422 |
mid_call_webhook_url |
string | 否 | 通话中监督回调:按节拍 POST 通话概况,响应里的 instruction 文本静默注入 AI 的 instructions,见「通话中监督回调」 |
mid_call_webhook_interval_seconds |
int(30-1800) | 否 | 监督回调节拍(秒),默认 300 |
mid_call_webhook_timeout_seconds |
int(1-30) | 否 | 单次监督回调的超时(秒),默认 5。导演类用法(webhook 里跑 LLM 现算指令)按实测往返放宽;回调在独立 worker 执行不阻塞音频链路,放宽只影响注入时机 |
soft_landing_hint_override |
string | 否 | 临近时长上限的内置收尾提示措辞覆盖,不传用默认文案 |
result_callback_url metadata callback_auth_token *_override |
— | 否 | 自定义人设、结果回调等可选能力,见「Advanced 模式」 |
curl -X POST "$API_BASE/api/tasks" \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
-d '{
"requester": "crm-system",
"task_description": "确认对方明天下午2点是否有空参加会议",
"success_criteria": "明确得到对方是否能参加的答复",
"participant_label": "张先生",
"persona_id": "steady_professional",
"call_link_ttl_seconds": 3600
}'
响应 200
{
"task_id": "task_1a2b3c4d5e6f7890",
"status": "pending",
"call_url": "https://your-deployment.example.com/call/xxxxxxxxxxxxxxxx",
"expires_at": "2026-07-18T02:00:00Z",
"reusable": false
}
把 call_url 发给通话对象即可,无需再对这条链接做任何处理。
错误:persona_id 不在合法取值内时返回 400:
{"detail": "未知的 persona_id: xxx,可选值:warm_friendly, steady_professional, crisp_efficient, sunny_lively"}
GET查询单个任务 GET /api/tasks/{task_id}
curl "$API_BASE/api/tasks/task_1a2b3c4d5e6f7890" -H "Authorization: Bearer $API_KEY"
响应 200
{
"task_id": "task_1a2b3c4d5e6f7890",
"requester": "crm-system",
"task_description": "确认对方明天下午2点是否有空参加会议",
"success_criteria": "明确得到对方是否能参加的答复",
"participant_label": "张先生",
"persona_id": "steady_professional",
"status": "completed",
"reusable": false,
"session_count": 0,
"call_url": "https://your-deployment.example.com/call/xxxxxxxxxxxxxxxx",
"transcript": [
{"role": "agent", "text": "您好,请问是张先生吗?"},
{"role": "user", "text": "是的,你好。"}
],
"report": {
"verdict": "met",
"summary": "张先生确认明天下午2点有空,可以参加会议。",
"extracted_fields": {"available": true},
"full_markdown": "## 通话结果\n..."
},
"token_usage": { "voice": { "...": "见「用量与计费字段」" }, "report": { "...": "..." } },
"cost": { "voice": {"amount": 16.6735, "currency": "CNY"}, "report": {"amount": 0.0021, "currency": "USD"} },
"error_message": null,
"created_at": "2026-07-17T01:00:00Z",
"updated_at": "2026-07-17T01:12:00Z",
"call_started_at": "2026-07-17T01:10:00Z",
"call_ended_at": "2026-07-17T01:12:00Z"
}
在通话尚未开始/尚未结束时,transcript、report、cost 等字段为 null,靠
status 判断当前所处阶段(见下方状态机)。
transcript 每一轮的 role 只有两种取值:user(通话对象)和 agent(AI 一侧)。
不是 assistant——assistant 只出现在 Advanced 模式下传给语音供应商的
dialog_context 里,不会出现在任何查询接口的响应中。
report.verdict 取值:
| verdict | 含义 |
|---|---|
met |
完全达成交付标准 |
partial |
部分达成 |
not_met |
未达成 |
错误:任务不存在返回 404,{"detail": "task not found"}。
GET查询任务列表 GET /api/tasks
| Query 参数 | 说明 |
|---|---|
requester |
按创建方过滤(精确匹配) |
status |
按状态过滤:pending / in_call / completed / expired |
limit |
分页大小,默认 50,最大 200 |
offset |
分页偏移,默认 0 |
curl "$API_BASE/api/tasks?requester=crm-system&status=completed&limit=20" \
-H "Authorization: Bearer $API_KEY"
{ "items": [ { "...": "同上单条任务结构" } ], "total": 137 }
Task 状态机
reusable=false(默认,一次性任务):
pending ──(通话产生有效对话)──▶ in_call ──(报告已生成)──▶ completed
▲ │
└──────(未产生有效对话,可重试)──┘
│
└──(链接过期未使用,惰性判定)──▶ expired
reusable=true(可复用任务):产生有效对话后不进入 completed 终态,而是退回 pending,
链接在有效期内可以再次打开发起新一轮通话,直到链接过期,或者复用次数达到部署方内部设置
的上限(未开放给调用方配置)——达到上限后同样按到期处理,直接置为 expired:
pending ──(通话产生有效对话,未达复用上限)──▶ in_call ──▶ pending(可再次拨打)
▲ │
└────────────(未产生有效对话,可重试)──────────┘
│
├──(链接过期未使用,惰性判定)───────────────▶ expired
└──(通话产生有效对话,已达复用上限)─────────▶ expired
若 TTL 到期时刚好有一通电话正在进行中,这一通不受影响,会正常聊完、正常生成报告/触发
result_callback_url;只是聊完之后不能再打开链接发起下一通。
| 状态 | 含义 |
|---|---|
pending |
已创建,链接尚未被打开;也是通话失败/未产生有效对话后的退回态,reusable=true 时也是一次有效对话结束后(未达复用上限时)的退回态,链接在有效期内可重新打开 |
in_call |
通话进行中 |
completed |
通话结束,报告已生成,终态;reusable=true 的任务不会进入这个状态 |
expired |
链接不可再用于发起新通话,终态:可能是链接过期未使用,也可能是 reusable=true 的任务已达到复用次数上限 |
pending 状态下重试没有次数上限,只受链接有效期约束——次数上限只针对
reusable=true 时"已产生有效对话"的次数(session_count),未产生有效对话的失败尝试
不计入。
reusable=true 的任务发起新一通时,会自动把该 task 之前已产生有效对话的历次通话内容
接进本次的对话上下文,AI 能接着上一通继续聊(比如上一通聊到一半被打断,下一通不需要
从头介绍一遍),调用方不需要自己拼历史、也不需要用其它字段传递上一通的内容。
reusable=true 的任务每完成一次有效对话,都会:
- 让
GET /api/tasks/{task_id}里的transcript/report更新为最近一次通话的内容(不是历史拼接); - 让
token_usage/cost反映该 task 到目前为止的累计用量/费用(含本次及此前所有通话之和); - 单独触发一次
result_callback_url(如果设置了),payload 只含本次通话内容,见docs/prd/advanced_mode_for_custom_integrators.md3.2 节的session_id字段说明; - 完整的逐通历史可以通过
GET /api/tasks/{task_id}/sessions查询(下)。
GET查询任务的通话历史 GET /api/tasks/{task_id}/sessions(仅 reusable 任务有意义)
curl "$API_BASE/api/tasks/task_1a2b3c4d5e6f7890/sessions" -H "Authorization: Bearer $API_KEY"
{
"items": [
{
"session_id": "tsess_9f8e7d6c5b4a3210",
"task_id": "task_1a2b3c4d5e6f7890",
"status": "completed",
"transcript": [{"role": "user", "text": "..."}],
"report": { "...": "同上单条任务里的 report 结构" },
"token_usage": { "...": "只含这一通" },
"cost": { "...": "只含这一通" },
"error_message": null,
"created_at": "2026-07-17T01:00:00Z",
"call_started_at": "2026-07-17T01:00:05Z",
"call_ended_at": "2026-07-17T01:02:30Z"
}
],
"total": 1
}
reusable=false 的任务没有这类记录(items 恒为空数组)——完整的(唯一一次)通话内容
直接看 GET /api/tasks/{task_id} 本身即可。也可以用 GET
/api/tasks/{task_id}/sessions/{session_id} 查询单条记录,结构同上方列表里的单条。
VoiceAgent API(语音秘书)
Base path:/api/agents,全部需要鉴权。VoiceAgent 是"先创建一个常驻画像,
拿到一条长期链接,之后任何人点开都能发起独立通话"的模式——跟 Task 的
"一次性任务对应一次性链接"不同。
POST创建秘书画像 POST /api/agents
请求体
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
owner |
string(1-200) | ✅ | 你账号内部的标识(自由文本),仅用于筛选列表,不参与鉴权,语义同 Task 的 requester |
name |
string(1-50) | ✅ | 秘书昵称,通话开场白/页面标题展示用 |
avatar_url |
string | 否 | 头像图片 URL:可以是任意外部图片链接,也可以是 POST /api/agents/uploads/avatar 上传后返回的 URL |
ringtone_url |
string | 否 | 来电铃声音频 URL,来源同 avatar_url(外部链接或 POST /api/agents/uploads/ringtone 上传后返回的 URL);不传则通话页使用全站默认铃声 |
gender |
string(1-10) | ✅ | 性别,用于人物设定 |
age |
int(1-119) | ✅ | 年龄,用于人物设定 |
city |
string(1-50) | ✅ | 城市,用于人物设定 |
occupation |
string(1-100) | ✅ | 职业,用于人物设定 |
personality |
string | ✅ | 性格特点,拼进 system prompt 的人物设定 |
tone |
string | ✅ | 语气/说话风格说明 |
voice_name |
string | ✅ | 音色,取值见 GET /api/agent-voices |
knowledge_base |
string | 否 | 秘书应了解的信息:营业时间/价格/服务范围/常见问题等,默认空 |
knowledge_pack |
object | 否 | 按条目组织的知识包,见下方「知识包」;不传表示不挂。跟 knowledge_base 的区别:后者是整段进 prompt 的自由文本,知识包的详情要等聊到那一条才注入 |
caller_history_sessions |
int(0-10) | 否 | 跨通话记忆:同一个 caller_ref 最近几通已完成会话接进下一通,见下方「跨通话记忆」;不传/0 表示关闭 |
turn_end_silence_ms |
int(500-50000) | 否 | 已废弃的 no-op:2.0 协议的判停等待参数,现在的引擎只有 3.0 全双工协议(判停由模型语义判断),不再读取该字段。保留接受只是为了不破坏既有调用方,见 docs/prd/legacy_protocol_sunset.md |
explicit_dialect |
string | 否 | TTS 输出的方言口音(改变合成语音的口音,不是识别矫正),取值 dongbei/sichuan/shaanxi/yue/beijing/henan/tianjin/shanghai;不传 = 默认普通话。音色与方言的组合合法性由调用方负责;可在 PATCH /api/agents/{agent_id} 修改(传 null 恢复普通话),白名单外的 code 返回 422 |
opening_line |
string(≤200) | 否 | 接通后秘书先说的开场台词,一字不差地念出来;不传用通用模板「你好,我是{name}。今天想聊点什么?」。人设强的秘书建议配一句合人设的(例:「在下东方幻,一个普普通通的读书人。小朋友,今天想背哪一首诗呀?」)。开了跨通话记忆时会在后面追加一句上次聊到的内容 |
call_instructions |
string | ✅ | 通话指导/workflow:秘书在通话中要做什么 |
success_criteria |
string | 否 | 通话目标是否达成的判断标准,供报告生成使用。不传则跳过报告生成,会话的 report 恒为 null |
allowed_origins |
string[] | 否 | 允许嵌入 widget.js 的网站域名白名单(如 https://example.com),不填表示禁止任何网站嵌入,见下方「嵌入到自己网站」一节 |
record_audio |
bool | 否 | 把本画像下的通话音频混成单轨 WAV 落盘(默认 false),每一通的录音随结果回调/会话查询接口交付,见「通话录音与单通时长覆盖」 |
max_call_duration_seconds |
int(1-3600) | 否 | 本画像下单通通话的时长上限(秒),不传沿用部署全局默认(600)。到点前端 5 秒倒计时优雅退出、不配合的客户端由服务端强制挂断;可在 PATCH /api/agents/{agent_id} 修改 |
context_callback_url result_callback_url metadata callback_auth_token |
string / object | 否 | 每次会话现算人设、结果回调等可选能力,见「Advanced 模式」 |
curl -X POST "$API_BASE/api/agents" \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
-d '{
"owner": "my-business",
"name": "小雅",
"gender": "女",
"age": 26,
"city": "杭州",
"occupation": "客服专员",
"personality": "耐心细致,善于倾听。",
"tone": "语气温和亲切。",
"voice_name": "zh_female_vv_jupiter_bigtts",
"knowledge_base": "营业时间:周一到周日 9:00-18:00。地址:……",
"call_instructions": "了解来电人的诉求,记录留言或收集预约意向。",
"success_criteria": "拿到来电人的称呼和一种联系方式即视为达成。"
}'
响应 200:AgentResponse,包含创建时传入的全部字段,加上:
{
"agent_id": "agent_a1b2c3d4e5f67890",
"status": "active",
"call_url": "https://your-deployment.example.com/call/yyyyyyyyyyyyyyyy",
"widget_key": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
"quota_limit_amount": null,
"quota_used_amount": 0.0,
"created_at": "2026-07-17T01:00:00Z",
"updated_at": "2026-07-17T01:00:00Z"
}
call_url 和 widget_key 是两个不同用途、不能互换的凭据:
call_url:仅供你自己直接打开测试用的私有直拨链接,不要贴到网站、IM 签名或分享给任何人——/call/{token}这条路径只按 IP 限流,没有域名校验,谁拿到 这个链接谁就能直接发起通话。widget_key:设计上就是可以公开出现在客户网站 HTML 源码里的标识,配合allowed_origins白名单用在 widget.js 嵌入代码的data-agent-key属性里(见 下方「嵌入到自己网站」一节或/guide页面);泄露也 不会被拿去绕过域名校验直接 拨打,因为它本身只能换一个短时一次性 session token。
知识包:按条目组织、详情按需自取
knowledge_base 是整段进 prompt 的自由文本,适合几百字的营业信息;但有一类资料
通话开始前不知道对方会聊到哪一条(陪小朋友背古诗不知道要背哪首、产品客服不知道
客户会问哪个型号),全量塞进去又太大。knowledge_pack 是这类资料在本服务里的做法:
- 每个条目分
summary(摘要)和detail(详情)。所有条目的 summary 在每次会话 建连时全量拼进 system prompt(古诗题目索引、型号一句话介绍),不需要知道会聊到哪条。 - 详情由模型在通话中调内置工具
lookup_knowledge_entry按题目自取:聊到哪一条、 什么时候需要详情,由模型根据对话自己判断,服务端把那一条的detail(连同inject_reply_hint的使用说明)作为工具结果回传,模型同轮续说。服务端不在转写里 做关键词匹配、也不择机注入。 inject_mode/inject_trigger_phrases是 2.0 注入策略的遗留字段,继续接受但 不再影响运行时,新包直接不写即可。
{
"knowledge_pack": {
"title": "小学必背古诗词",
"system_role_intro": "下面是诗集里每首诗的摘要索引……需要某首的原文时调 lookup_knowledge_entry 按题目查询,用查到的原文核对,不要凭记忆背。",
"inject_reply_hint": "上面的诗句是书上印的,不是小朋友念的。他还没开始背就只说一句让他开始;他已经背过就拿原文核对你刚才的点评。",
"entries": [
{
"id": "jingyesi",
"title": "静夜思",
"keywords": ["静夜思", "床前明月光"],
"summary": "《静夜思》唐·李白\n床前明月光,疑是地上霜。\n举头望明月,低头思故乡。",
"detail": "逐句白话:……\n背景:……\n引导问题(挑一两个问):……"
}
]
}
}
| 字段 | 说明 |
|---|---|
title |
拼进 system prompt 的资料库小节标题 |
system_role_intro |
摘要列表前的说明:这个列表是什么、怎么用、查到详情后该怎么处理——跟资料类型强相关,由你自己写 |
inject_mode / inject_trigger_phrases |
2.0 遗留字段,接受但不再影响运行时 |
inject_reply_hint |
可选。模型查到详情后紧接着那一轮该做什么,附在工具返回值末尾。不交代清楚模型可能把资料当成对方说的话去回应(实测:把查到的诗句当成小朋友背出来的,夸"一字不差")。写清楚"对方此刻什么都没说"以及该回什么,例如"对方还没开始背,只说一句让他开始" |
entries[].id |
包内唯一 |
entries[].keywords |
工具按题目模糊匹配用:模型报题目时可能的说法(名称、首句、别名、常见误听)都放进来,近名条目靠更长的关键词区分 |
entries[].summary / detail |
摘要建连时全量进 prompt;详情经 lookup_knowledge_entry 按需返回。详情写给模型看,不是念给对方听的 |
体积约束:语音模型的 prompt 预算是 12288 token(中文约 0.86 token/字),超出时
建连不报错、第一次生成回复才失败(通话页表现为"连接错误 … prompt too long")。画像
人设本身要占几千字,所以所有条目 summary 合计(加上 system_role_intro)限制在
6000 字以内,超出创建时直接返回 400——只有聊到时才需要的内容(原文、规格)放进
detail 按需返回,summary 只留让对方能点名的索引信息(一两百个条目时通常就是
"名称 + 一行说明")。整包序列化后不超过 30 万字符。
跨通话记忆(caller_history_sessions)
默认每通会话彼此独立,AI 不知道"这个人上次来过"。设置 caller_history_sessions = N
后,同一个 caller_ref(入口链接 {call_url}/{caller_ref} 那段,见「caller_ref」)
最近 N 通已完成的会话会接进下一通:
- 转写原文作为历史对话轮次(火山引擎
dialog_context)随本次建连传给模型,按从旧到新 排列。协议层只保留最近 20 轮,较长的通话只剩末尾——所以还有下面这层。只接最近 24h 内的 通话原文(部署级配置):更早的通话只保留摘要行,因为没有时间戳的原话会被模型当成刚说的 (真实通话验证过:20 天前交代的任务被说成"刚在整理")。 - system prompt 里多一节「与这位来电人的历史通话」:每通的时间、报告
summary、以及 查询过的知识包条目(knowledge_entries,服务端按模型工具调用记下来的确定性事实, 不依赖 LLM 总结——注意它反映"模型查过什么",模型凭摘要就能答、没调工具的条目不在 其中)。设了success_criteria的画像是质检报告的 summary;没设的陪伴型画像挂断后 本服务也会自动生成一句通话摘要(防止转写过期后那通电话在历史小节里什么都不剩)。只保留 最近 14 天内的通话(部署级配置),更早的不注入。"上次背过哪几首、哪首没背对"靠 这一节,不靠转写尾巴。
几条边界:
- 只对带
caller_ref的直拨链接生效。widget 嵌入的匿名访客、不带caller_ref的直拨 没有身份,无从记忆。caller_ref由你定义,同一个人每次用同一个值即可(如孩子的 编号);它出现在地址栏里,敏感场景自行签名。 - 走 advanced 模式(
context_callback_url)的画像同样生效,且与你回调返回的内容叠加: 你的system_role负责人设和你那边的互动历史(比如 IM 聊天记录,建议渲染成 system_role 里的一节),本服务负责"这个人之前打过的电话"。回调若返回了dialog_context,转写以你的 为准、不再附本服务记录的电话转写(「历史通话」小节仍会拼进 system_role)。 - 只接已完成(产生了有效对话)的会话;不同
caller_ref、不同画像之间不串。 - 上限 10 通。历史越多 system prompt 越长、每通成本越高,一般 2-3 通足够。
会话记录(GET /api/agents/{agent_id}/sessions/{session_id} 和结果回调)多一个字段
knowledge_entries:本次通话里模型查询过的知识包条目 [{"id", "title"}],按首次
查询的顺序;没挂知识包或没查询过为 null。
上传头像 / 来电铃声
如果不想直接传外部图片/音频链接,可以先把文件上传到本服务换一个 URL,再填进
avatar_url/ringtone_url。两个接口都不挂在具体某个画像下(创建画像前
agent_id 还不存在),上传和创建/编辑画像是两步独立请求。
| 接口 | 允许的文件类型 | 大小上限 |
|---|---|---|
POST /api/agents/uploads/avatar |
png / jpeg / webp / gif | 5MB |
POST /api/agents/uploads/ringtone |
mp3 / wav / ogg / m4a | 10MB |
curl -X POST "$API_BASE/api/agents/uploads/avatar" \
-H "Authorization: Bearer $API_KEY" \
-F "file=@./avatar.png"
响应 200:{"url": "/uploads/avatars/xxxxxxxx.png"}——把这个 url 原样填进
创建/编辑画像请求体的 avatar_url 字段即可。文件类型不支持或超出大小上限返回 400。
GET查询单个画像 GET /api/agents/{agent_id}
curl "$API_BASE/api/agents/agent_a1b2c3d4e5f67890" -H "Authorization: Bearer $API_KEY"
响应结构同创建接口的 AgentResponse。不存在时返回 404。
GET查询画像列表 GET /api/agents
| Query 参数 | 说明 |
|---|---|
owner |
按创建方过滤(精确匹配) |
status |
按状态过滤:active / disabled |
limit / offset |
分页,默认 50/最大 200、0 |
curl "$API_BASE/api/agents?owner=my-business&status=active" -H "Authorization: Bearer $API_KEY"
{ "items": [ { "...": "同上单条画像结构" } ], "total": 4 }
PATCH编辑画像 PATCH /api/agents/{agent_id}
请求体所有字段可选(AgentUpdateRequest,字段同创建接口,额外支持
status),只传要改的字段;改动立即对后续新会话生效,不影响已完成的
历史会话。
# 更新知识库
curl -X PATCH "$API_BASE/api/agents/agent_a1b2c3d4e5f67890" \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
-d '{"knowledge_base": "……更新后的知识库内容……"}'
# 临时下线:入口链接展示"当前不可用",历史会话保留,可随时改回 active 重新启用
curl -X PATCH "$API_BASE/api/agents/agent_a1b2c3d4e5f67890" \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
-d '{"status": "disabled"}'
knowledge_pack 是整包替换(不做条目级合并),传 null 卸载。
错误:voice_name / status 传了非法取值、knowledge_pack 格式不对时返回 400;画像不存在返回 404。
GET查询会话列表 GET /api/agents/{agent_id}/sessions
一个画像下每次通话都会产生一条独立的 AgentSession。
| Query 参数 | 说明 |
|---|---|
status |
按状态过滤:in_call / completed / no_dialogue |
limit / offset |
分页,默认 50/最大 200、0 |
curl "$API_BASE/api/agents/agent_a1b2c3d4e5f67890/sessions?status=completed" \
-H "Authorization: Bearer $API_KEY"
{
"items": [
{
"session_id": "asess_9f8e7d6c5b4a3210",
"agent_id": "agent_a1b2c3d4e5f67890",
"status": "completed",
"transcript": [ {"role": "agent", "text": "您好,我是小雅……"} ],
"report": { "verdict": "met", "summary": "……", "extracted_fields": {}, "full_markdown": "……" },
"token_usage": { "...": "见「用量与计费字段」" },
"cost": { "...": "见「用量与计费字段」" },
"error_message": null,
"created_at": "2026-07-17T01:05:00Z",
"call_started_at": "2026-07-17T01:05:02Z",
"call_ended_at": "2026-07-17T01:07:30Z"
}
],
"total": 12
}
画像不存在时返回 404({"detail": "agent not found"})。
GET查询单条会话 GET /api/agents/{agent_id}/sessions/{session_id}
curl "$API_BASE/api/agents/agent_a1b2c3d4e5f67890/sessions/asess_9f8e7d6c5b4a3210" \
-H "Authorization: Bearer $API_KEY"
响应结构同上方列表里的单条会话。session_id 不存在、或者不属于该 agent_id
时返回 404({"detail": "session not found"})。
AgentSession 状态机
in_call ──(产生了有效对话,报告已生成)──▶ completed
│
└──(未产生有效对话,比如秒挂/连接失败)──▶ no_dialogue
比 Task 状态机简单:入口链接本身不过期、不被占用,no_dialogue 是终态,不
重试——来电人重新打开常驻入口链接会开启全新一条会话,不是"重试这一条"。
嵌入到自己网站(widget.js)
前面反复提到 VoiceAgent 的 call_url 不能对外分享。要让访客能发起通话,正确做法是
在自己的网站里嵌入 widget.js——它渲染一个悬浮的圆形头像按钮,访客点击后弹出接听
面板,走的是「公开 widget_key + 域名白名单 + 短时一次性 session token」这套模型,
agent_token 全程不出现在页面源码里。
第一步:给画像配置允许嵌入的域名白名单(不配则任何网站都嵌不了)。
curl -X PATCH "$API_BASE/api/agents/agent_a1b2c3d4e5f67890" \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
-d '{"allowed_origins": ["https://example.com", "https://www.example.com"]}'
allowed_origins 必须是「协议 + 域名(+ 端口)」形式、不带路径,跟浏览器发出的
Origin 头逐字符精确匹配——https://example.com 和 https://www.example.com 是两个
不同的来源,都要用就都写上。传 [] 清空白名单(等于禁止任何网站嵌入),不传该字段
表示不改动。
第二步:把这段代码贴进自己网站页面的 <body>(data-agent-key 填画像的
widget_key,不是 agent_token)。
<script src="https://your-deployment.example.com/call/static/widget.js"
data-agent-key="<widget_key>" async></script>
可选属性:data-position 取 bottom-right(默认)/ bottom-left / top-right /
top-left,data-offset-x / data-offset-y 是相对该角的像素偏移(默认各 20px)。
访客必须点击按钮、再点一次「接听」才会真正请求麦克风——浏览器的自动播放/录音策略
要求这样一次明确的用户手势,做不成打开页面就自动接通。
widget.js 内部用到下面两个公开接口(不需要 API Key,调用方是访客浏览器而不是 你的服务端)。自己实现前端入口、不用 widget.js 时才需要直接对接:
| 接口 | 说明 |
|---|---|
POST /api/widget/session |
请求体 {"agent_key": "<widget_key>"}。校验浏览器自动带上的 Origin 是否在白名单里,通过则签发短时一次性 session_token,响应 {"session_token": "...", "expires_in_seconds": N, "ws_path": "/call/ws/<session_token>"}——把 ws_path 拼上部署域名的 wss:// 前缀即为通话建连地址 |
GET /api/widget/agent-info?agent_key=<widget_key> |
悬浮按钮渲染头像用,只返回 {"name": ..., "avatar_url": ...} 这两个本来就公开展示的字段,换不到任何通话能力,因此不做 Origin 校验 |
失败情况:缺少 Origin 返回 400;widget_key 无效或画像已 disabled 返回 403
({"detail": "该 Agent 当前不可用"},刻意不区分两者,避免被当成探测接口);域名不在
白名单返回 403({"detail": "当前域名未被授权使用此 Agent,请在控制台配置 allowed_origins"});
请求过频返回 429。换个没在白名单里的域名打开同一段代码,点按钮不会有反应
(浏览器控制台能看到 403),这是白名单在生效,不是接错线。
session_token 是一次性的:建立 WS 连接那一刻即被消费,且连接会被锁定在签发时校验
通过的那一个 Origin 上,拿去别处复用无效。
allowed_origins同时也是/call/{agent_token}页面的frame-ancestors白名单—— 想把入口链接直接<iframe>进自己的页面,域名也要先加进这个列表,否则浏览器会 拒绝渲染。
面向非开发者的图文版操作指引(可以直接发给客户):部署域名下的 /guide 页面。
Advanced 模式:自定义人设与回调
前面几节讲的是标准用法:人设从目录里选(persona_id / voice_name),结果靠轮询拿。
如果你的系统自己就有一套人设/上下文管理,或者不想轮询,可以用下面这组可选字段接管
这两件事。这些字段全部可选,不传时行为跟前面完全一致,标准用法的读者可以跳过本节。
Task:覆盖人设
| 字段 | 类型 | 说明 |
|---|---|---|
system_role_override |
string | 直接指定通话的 system prompt,完全跳过 persona_id 对应的默认模板拼接(含 task_description/success_criteria 的拼接和内置合规话术) |
opening_trigger_override |
string | 直接指定开场白引导语(写给 AI 的指令,不是台词本身,见下方说明) |
speaking_style_override |
string | 直接指定说话风格描述 |
silence_check_trigger_override |
string | 双方沉默一段时间后,AI 主动打破僵局的话术;不传用默认通用文案 |
silence_goodbye_trigger_override |
string | 打破僵局后仍然沉默,AI 主动告别挂断的话术;不传用默认通用文案 |
前三个是一组,要么都传、要么都不传——只传其中一两个会返回 422
(system_role_override / opening_trigger_override / speaking_style_override 三个字段
要么都设置,要么都不设置)。后两个各自独立可选,不参与这条校验:定制人设主体的
调用方不必被迫连冷场话术一起想清楚。
注意 persona_id 仍然必填——即使三个 override 都传了,音色依然取自 persona_id,
被跳过的只是文本模板部分。
VoiceAgent:每次会话现算人设(context_callback_url)
Task 的 override 是创建时就固定的静态文本。VoiceAgent 面对的是"一个画像服务许多不同
终端用户"的场景,人设往往要到每次通话真正开始那一刻才能算出来(比如要带上这个
用户的历史对话)。设置 context_callback_url 后,每次新会话开始时本服务会 POST 到
这个地址,用你返回的内容构造本次会话的 system prompt:
POST {context_callback_url}
{
"agent_id": "agent_a1b2c3d4e5f67890",
"call_id": "asess_9f8e7d6c5b4a3210",
"caller_ref": "u_12345"
}
期望你返回 200 + 这样一个 JSON:
{
"system_role": "……本次会话的 system prompt……",
"opening_trigger": "……开场白引导语……",
"speaking_style": "……说话风格描述……",
"dialog_context": [{"role": "user", "text": "上次聊到旅行"},
{"role": "assistant", "text": "对,你说想去云南"}],
"silence_check_trigger": "……可选……",
"silence_goodbye_trigger": "……可选……"
}
- 前三个字段必需且不能为空串,缺任何一个都算失败。
dialog_context可选,用来把历史对话接进本次通话,AI 能接着上次继续聊。注意这里 用的是语音供应商的协议格式:角色只认user/assistant,且必须严格交替。 (与之相对,查询接口返回的transcript里 AI 侧的角色是agent。)- 后两个冷场话术可选,不传走默认通用文案。
三个 *_trigger 是写给 AI 的指令,不是台词原文。 描述这句话要在什么场景下、达成什么
目的、有什么约束(比如"电话刚接通,一两句话打个招呼,可以从最近聊的话题里挑一个接上"),
真正说出口的话由 AI 结合 system_role 和这通电话到目前为止的实时对话现场组织:
开场白按人设和近况开口;冷场关心、冷场告别会接着刚才在聊的内容说,而不是千篇一律的
"喂,还在吗"。所以 trigger 里写意图和风格就好,不需要也不应该塞具体的对话上下文。
同一套字段对 Task 的 *_trigger_override 同样适用。
- tools / tool_callback_url 可选,让 AI 在通话中调用你的工具,见下一节。
失败即通话失败:回调超时、非 200、响应体不符合上面的格式时,本次会话直接建连 失败,浏览器侧收到错误提示并断开——不会静默退回默认人设模板。这是刻意的:与其 让 AI 用错误的人设跟你的用户聊完一整通,不如让失败立刻可见。
通话中调用你的工具(tools + tool_callback_url)
AI 在电话里只有一张嘴:对方说"帮我把链接发到微信""记住我女儿的生日""挂了以后给我发三张
照片",它办不了,只能拒绝或者转移话题。context_callback_url 的响应里可以声明一组工具,
AI 判断需要时调用,本服务把调用转给你执行:
{
"system_role": "……",
"opening_trigger": "……",
"speaking_style": "……",
"tools": [
{
"name": "send_message",
"description": "给对方发一条文字消息到微信。对方明确让你发链接、发地址、发刚说的内容时调用。",
"parameters": {
"type": "object",
"properties": {"content": {"type": "string", "description": "消息正文"}},
"required": ["content"]
},
"timeout_seconds": 5
}
],
"tool_callback_url": "https://your-service.example.com/live-call/tool"
}
tools[]每项name(同一次响应内唯一)、description、parameters(标准 JSON Schema,type: object)原样交给语音模型,写法跟给任何 LLM 声明 function calling 一样:description要说清什么时候该调、什么时候不该调。timeout_seconds可选,默认 5 秒、最长 15 秒。- 声明了
tools就必须给tool_callback_url;两者都不传,行为跟以前完全一样。 - AI 决定调用时,本服务 POST 到
tool_callback_url(鉴权同其它回调,见「回调鉴权」):
POST {tool_callback_url}
{
"agent_id": "agent_a1b2c3d4e5f67890",
"call_id": "asess_9f8e7d6c5b4a3210",
"caller_ref": "u_12345",
"metadata": {"agent_type": "companion"},
"name": "send_message",
"arguments": {"content": "https://example.com/article"}
}
返回 200 + {"output": "已发送"},output 是给 AI 看的执行结果文本,AI 据此在同一句话里
接着说("发你微信了,看一下")。响应不是这个形状时,整个响应体会序列化成文本交给 AI。
- 失败不阻塞通话:超时、非 2xx、连不上,AI 收到的是"暂时办不了"之类的结果文本,会照常
往下说,不会卡住。所以你的端点要快:AI 拿到结果前不会开口,几秒的空白对方就会觉得卡了。
耗时的事(生图、搜索、跑任务)不要在回调里做完,记下来立即返回"已安排",做完再通过你
自己的渠道送达。
- arguments 里不会有终端用户的身份——用 caller_ref 自己解出来。AI 拿不到也不该拿到
你系统里的用户 ID。
- 一次可能有多个调用并发到达(AI 一句话里调了两个工具),按 call_id + name 各自处理即可;
通话日志里每次调用有 tool_call / tool_result 事件(含耗时和返回文本),排障时看
logs/calls/{call_id}.jsonl。
- 引擎现只有 3.0 全双工协议(2.0 已下线,见 docs/prd/legacy_protocol_sunset.md)。
通话中监督回调(mid_call_webhook_url)
Task 支持。创建任务时带 mid_call_webhook_url 后,引擎在通话进行中按节拍回调你,
你返回一段指令文本,引擎把它静默注入 AI 的 instructions(不产生语音、不打断
进行中的播报)。典型用法:通话节奏提示("已通话 10 分钟,还有 5 分钟")、通话中
纠偏(用你自己的 LLM 看转写、现算调整指令)。三个字段独立可选,不要求走
advanced 三件套;鉴权复用 callback_auth_token。
POST {mid_call_webhook_url}
Authorization: Bearer {callback_auth_token}
{
"task_id": "task_1a2b...",
"metadata": {...}, # 创建任务时传的 metadata 原样带回
"elapsed_seconds": 623,
"remaining_seconds": 1177, # 配了时长上限才有这个键
"turn_count": 41,
"transcript_tail": [ # 最近 30 轮定稿转写
{"role": "user", "text": "..."}, {"role": "agent", "text": "..."}
]
}
← 200 {"instruction": "不超过 500 字的任意文本"} # 注入;超长截断后照常注入
← 204 # 本轮没有指令(instruction 为空串或缺失同此)
← 超时(5s) / 非 200 / 响应非法 # 本轮跳过,通话不受影响,不重试
规则与边界:
- 节拍从会话建立起算,
mid_call_webhook_interval_seconds控制(默认 300,允许 30–1800;下限防止把自己配成高频轮询);单次回调超时由mid_call_webhook_timeout_seconds控制(默认 5,允许 1–30——导演类用法在 webhook 里跑 LLM,按实测往返放宽)。配置了时长上限的通话在临近上限的收尾 窗口会额外回调一次(remaining_seconds体现紧急度),这次回调不重置节拍。 instruction的内容引擎不解释:只做长度约束(单条 ≤500 字,超长截断记日志)。 指令是"建议"语义——注入推送失败时保留旧指令、通话照常,不熔断。- 同一时刻至多保留最新 1 条指令(新指令覆盖旧指令),一条指令保持 2 个节拍后 自动消失;指令整体在 instructions 里占一个独立小节,硬上限 600 字。
- 临近通话时长上限时(提前
min(300, 上限/3)秒),引擎无论你是否配置监督回调 都会注入一条内置收尾提示;soft_landing_hint_override可以覆盖它的措辞。 - 不配置
mid_call_webhook_url的任务,通话行为与之前完全一致。 - 每次回调与注入在通话日志里有
mid_call_webhook(injected / skipped / error / truncated)和instruction_injection(source = webhook / builtin_landing)事件, 排查看logs/calls/{task_id}.jsonl。
和 tool 通道的分工:监督回调是"推"(平台节拍决定时机,适合行为/节奏类指令),
tool 是"拉"(模型按对话需要取内容,轮内等待)。不要用 tool 传行为指令(模型不会
自觉定时拉取,且轮内等待会让对方听到死寂),也不要用监督回调传内容查询(模型
不知道自己需要什么)。详见 docs/prd/mid_call_webhook.md 的对比表。
caller_ref:让一个画像区分不同终端用户
在入口链接后面再接一段路径,这段内容会原样出现在 context_callback_url 和
result_callback_url 的请求体里:
{call_url}/{caller_ref}
本服务对 caller_ref 只做透传,不解析、不校验、不签名——防篡改是你自己的责任
(终端用户能看到并修改地址栏里的这段内容,敏感场景请自行加签名)。不带这段路径时
行为不变,回调里的 caller_ref 为 null。
不走 advanced 模式的画像也能用这段路径:它是「跨通话记忆」 识别"同一个来电人"的依据。
结果回调(result_callback_url)
Task 和 VoiceAgent 都支持。设置后,每通电话 finalize 时 POST 一次结果给你,不用轮询:
POST {result_callback_url}
{
"call_id": "task_1a2b3c4d5e6f7890",
"session_id": "tsess_9f8e...",
"resource_type": "task",
"metadata": {"order_id": "A123"},
"transcript": [{"role": "user", "text": "..."}, {"role": "agent", "text": "..."}],
"token_usage": {"...": "见「用量与计费字段」"},
"cost": {"...": "同上"},
"report": {"...": "同查询接口的 report"},
"error_message": null,
"started_at": "2026-07-17T01:10:00+00:00",
"ended_at": "2026-07-17T01:12:00+00:00",
"duration_seconds": 120.0
}
字段差异按资源类型区分:
| 字段 | Task | VoiceAgent |
|---|---|---|
call_id |
task_id |
session_id |
resource_type |
"task" |
"agent_session" |
session_id |
reusable=true 时标识"这是第几通";非 reusable 恒为 null |
不出现 |
caller_ref |
不出现 | 入口链接路径里透传的那段,没有则 null |
knowledge_entries |
不出现 | 本次通话里模型查询过的知识包条目 [{"id", "title"}],没挂知识包或没查询过为 null |
audio |
开了 record_audio 才出现(三态语义见下) |
同左 |
其余字段两者一致。几个容易踩的点:
metadata是你在创建 Task/画像时传入的任意 JSON 对象,本服务不解析、原样带回, 用来把回调关联回你自己的业务单据。report只有配置了success_criteria才有值,否则为null。transcript只含本次通话,不做跨通话拼接;而查询接口里reusableTask 的token_usage/cost是累计值,两者刻意不同,不要把每次回调的用量再自行加总。- 未产生有效对话的通话尝试(未接通/秒挂等)不触发这个回调。
audio是三态:没开record_audio时键不出现(payload 与不录音时代一字不差); 开了且录音成功是一个完整对象"audio": {"url": "https://.../call_audio/<uuid>.wav", "format": "wav", "duration_seconds": 1830.4, "size_bytes": 58572800, "sample_rate": 16000, "channels": 1, "dropped_chunks": 0};开了但录音中途失败是null(比如磁盘 写挂了)——调用方能据此区分"没开录音"和"开了但文件丢了"。url的生命周期就是 部署配置的录音保留期(默认 30 天),过期后 404,请及时转存;dropped_chunks非 0 表示有音频块因拥塞被丢弃(0 = 完整录音)。详见下文「通话录音与单通时长覆盖」。
回调鉴权
创建 Task/画像时可以带 callback_auth_token。本服务发起上面几种回调时会带上
Authorization: Bearer {callback_auth_token},由你自己校验,确认请求确实来自本服务。
不做 HMAC 签名——代价是 token 明文过一次网络,请务必让回调地址走 HTTPS。
投递保证
结果回调是尽力而为,失败不重试:超时或非 2xx 只在服务端记一条日志,不会重发。
通话结果本身已经落库,回调丢失时用查询接口兜底
(GET /api/tasks/{task_id} / GET /api/agents/{agent_id}/sessions/{session_id})。
对可靠性敏感的集成,建议以回调为主、低频轮询为辅,不要把回调当成唯一的数据来源。
上述字段都可以在 PATCH /api/agents/{agent_id} 里修改(传空字符串清空 URL 类字段),
也都会在查询接口的响应里原样返回,方便你确认当前配置。
通话录音与单通时长覆盖
Task(创建时)和 VoiceAgent 画像(创建/编辑时)各有两个可选字段,互不依赖:
record_audio(默认 false):把通话音频混成单轨 WAV 落盘后交付给你。
- 格式 16kHz / 单声道 / 16bit PCM WAV,双方内容混在同一条音轨(分轨暂不支持), 约 1.92MB/分钟。文件时长与实际通话时长一致(没人说话的时段是静音,不是把两路 音频首尾拼接)。
- 落盘不占用通话的实时路径:录音中途失败不影响通话本身,回调里
audio为null。 - 交付:
result_callback_url的audio字段(三态语义见上一节)+ 查询接口 (TaskResponse.audio/TaskSessionResponse.audio/AgentSessionResponse.audio)。url是部署域名下的绝对地址(/call_audio/<uuid>.wav),直接 GET 即可下载; 文件名纯 UUID,不可枚举,URL 只会出现在 callback/查询接口里。 url的生命周期 = 部署配置的录音保留期(CALL_AUDIO_RETENTION_DAYS,默认 30 天):过期后文件被自动清理、URL 404。查询接口里的元数据(时长/大小等)永久 保留,但要把音频内容留存到自己系统里的话,收到 callback 后请尽快转存。
max_call_duration_seconds(默认不设 = 沿用部署的全局上限,默认 600 秒):
本任务/画像下单通通话的时长上限,取值 1–3600 秒。
- 到点按服务端现有看门狗语义收尾:对端收到
force_exit控制消息、界面展示退出 原因 + 5 秒倒计时优雅挂断;倒计时后仍在线的客户端由服务端强制挂断兜底。计费与 callback 口径不受影响(费用照常按实际时长结算并随结果回调)。 - Task 只能在创建时设置;画像可以在
PATCH /api/agents/{agent_id}里改 (传null恢复全局默认,不传该字段表示不改动)。
两个能力都默认关闭:未开启录音、未覆盖时长的通话,行为与不使用这些字段时完全一致。
用量与计费字段
token_usage / cost 出现在 TaskResponse 和 AgentSessionResponse 里,
结构完全一致:
{
"token_usage": {
"voice": {
"input_text_tokens": 1000, "input_audio_tokens": 20000,
"cached_text_tokens": 100, "cached_audio_tokens": 0,
"output_text_tokens": 800, "output_audio_tokens": 50000
},
"report": { "input_tokens": 1200, "output_tokens": 300 },
"line_llm": { "calls": 3, "input_tokens": 15000, "cached_tokens": 9000, "output_tokens": 60 }
},
"cost": {
"voice": { "amount": 16.6735, "currency": "CNY" },
"report": { "amount": 0.0021, "currency": "USD" },
"line_llm": { "amount": 0.0128, "currency": "CNY" }
}
}
voice:实时语音通话的用量/费用,只要通话真正连上、产生过 token 消耗就会 记录,即使最终任务/会话没有产生有效对话,这笔钱也是真花出去了的。report:报告生成 LLM 调用的用量/费用。cost.report的单价由部署方配置, 未配置时该字段可能缺失(token_usage.report仍会记录)。line_llm:通话中 AI 主动开口的几句话(开场白、冷场关心、冷场告别)由一个文本模型 按实时对话现写,这里是它的用量/费用;calls是本通请求次数(每通最多 3 次)。 没有用到时该字段缺失。- 通话结束前,这两个字段都是
null。 cost.voice和cost.report不强行合计成一个数字:两者币种通常不同 (语音服务多为 CNY,报告 LLM 多为 USD),需要合并请自行按当前汇率换算。
这一层只做用量记录 + 成本计算,不是完整的计费/账本系统:不会给
requester / owner 维护余额、不做扣费/欠费拦截、不出账单。如果你的业务
需要按调用方扣费或出账单,需要在自己的系统里基于这些字段再加一层。
错误码
| HTTP 状态码 | 场景 |
|---|---|
400 |
请求体字段不合法(比如 persona_id / voice_name / status 取值不在允许范围内),或字段格式校验失败(长度/类型等,FastAPI 自动校验) |
401 |
缺少/无效/已吊销/已过期的 Authorization: Bearer <api_key> 或 <session_token>,见「账号与鉴权」 |
403 |
账号已被禁用,或邮箱尚未验证(都发生在 POST /api/auth/login);widget 接口里还表示域名不在 allowed_origins 白名单、或画像已 disabled |
404 |
资源不存在:task_id / agent_id / session_id / key_id 不存在,或不属于当前账号(跨账号访问也会得到这个状态码,不会区分"不存在"和"没权限") |
409 |
注册邮箱已被使用(POST /api/auth/register) |
422 |
请求体缺少必填字段,或跨字段校验不通过(比如 Advanced 模式下三个 *_override 只传了一部分)——FastAPI/Pydantic 标准错误格式 |
429 |
触发限流:注册/找回密码/重发验证邮件每 IP 每小时 5 次,登录每 IP 每 5 分钟 10 次、每邮箱 5 次;widget 接口另有独立频率限制 |
错误响应统一是 {"detail": "..."}(422 时 detail 是 FastAPI 标准的字段级
错误数组)。
集成最佳实践
- 优先用回调,轮询兜底:填了
result_callback_url就不必轮询(见 「Advanced 模式」);但回调失败不重试,对可靠性 敏感的集成建议再配一个低频轮询兜底。纯轮询时,建议单个任务/会话 的状态轮询间隔不低于 10-15 秒;批量场景优先用列表接口 (GET /api/tasks?status=pending等)按需过滤,避免对同一批任务反复发大量 单条查询请求。 - 不要硬编码人设/音色列表:
persona_id/voice_name的合法取值由部署方 维护,可能增删,启动时或定期拉取/api/personas、/api/agent-voices并缓存, 不要写死在你的代码里。 requester/owner只是过滤用的标签,隔离靠账号:如果你的系统本身要 服务多个下游客户/业务线,且要求"业务线 A 看不到业务线 B 的任务",直接给每 条业务线分配各自的账号 + API Key 即可,不需要在requester上自己发明一套 权限约定——账号级别的隔离服务端已经保证了。- API Key 只在创建时出现一次,
session_token同理:都要在拿到响应的当下 存进你自己的密钥管理设施(环境变量/secret manager),刷新页面、换终端都无法 再次查看明文,只能重新创建。 - Task 的
call_url直接发给最终用户即可:这是一个自包含的网页链接,你的系统 不需要代理或包装它,也不需要额外鉴权——链接本身(call_token)就是访问凭证, 而且一次性/有过期时间,泄露的影响范围有限。VoiceAgent 的call_url则相反, 不要对外分享(见上方"创建秘书画像"一节)——它长期有效又没有过期时间,公开 出去泄露的窗口是无限期的;VoiceAgent 场景下要让外部访客能发起通话,应该用widget_key+allowed_origins配合 widget.js 嵌入,而不是直接分发call_url。 - 任务重试是"重新打开同一条链接",不是重新调用创建接口:Task 在
pending状态下、链接有效期内可以被重新打开重试,不需要(也不应该)为同一次 外呼多次调用POST /api/tasks生成多条链接。 - Task 报告字段
extracted_fields内容取决于任务本身的success_criteria描述,是自然语言 LLM 判定后提取的结构化信息,字段名和数量不固定,不要假设 固定 schema,按需读取、缺失时做好兜底。没传success_criteria的任务不生成报告,report整体为null。
已知限制
- Task 线不支持通话中工具调用:任务描述/知识库在通话前一次性拼进 system
prompt,AI 全程按人设自然对话,不能在通话过程中动态查询你的业务系统数据。
画像线有两条例外:advanced 模式(
context_callback_url响应声明tools, 见上文「通话中调用你的工具」)可以让 AI 查你的业务系统;知识包 (knowledge_pack)提供内置的lookup_knowledge_entry工具,但内容仍然是 创建画像时就准备好的静态资料,不是实时查询。 - 通话链接不支持真实电话号码接入:只能通过网页链接(浏览器麦克风)发起 通话,不打真实电话号码。
- 无并发上限:VoiceAgent 常驻链接同时来多少人就新建多少条会话,没有限流; 接入到高流量场景前请与部署方确认容量规划。
- 单通通话有时长上限:超过部署方配置的时长(默认 10 分钟)会被强制挂断, 已产生的对话照常生成报告。长时间通话的场景请先与部署方确认这个值。
- 额度用尽会拒绝新通话:账号(以及可选的单个画像)有累计费用上限,用尽后新通话
在打开链接/建立连接阶段就被拒绝,接口层面表现为通话页提示额度耗尽、WS 以
4402关闭——不是创建接口报错。当前额度可以在GET /api/auth/me和画像详情里查到。 - 账号不支持多成员协作:一个账号就是一个邮箱 + 一份密码,不支持拉多个团队成员 进来共享,也没有角色/权限分级(想让团队多人都能管理同一批 Key/任务,目前只能共用 同一个账号)。忘记密码可以自助找回,见 「账号 API」。
如需了解产品定位、设计取舍等背景,见仓库内 docs/prd/voice_agent.md(面向
维护者,非本 API 文档的一部分)。
本页由 docs/api/integration_guide.md 生成,源文档才是唯一维护
入口——发现内容有误请去改那份 Markdown,然后重新跑
scripts/generate_api_docs.py。