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(一次有效对话后链接即失效,行为与不传该字段完全一致)。复用次数上限由部署方内部设置,不通过这个字段开放,达到上限后链接按到期处理,见下方状态机 |
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) | 否 | 对方停顿多少毫秒算"说完了"(语音判停等待),不传走语音引擎默认 1500。对方会一句一顿说很久的场景(背诵、讲述、说话慢的老人)调到 2500–4000,否则 AI 会在句间停顿处接话;代价是每次回复都多等这么久 |
call_instructions |
string | ✅ | 通话指导/workflow:秘书在通话中要做什么 |
success_criteria |
string | 否 | 通话目标是否达成的判断标准,供报告生成使用。不传则跳过报告生成,会话的 report 恒为 null |
allowed_origins |
string[] | 否 | 允许嵌入 widget.js 的网站域名白名单(如 https://example.com),不填表示禁止任何网站嵌入,见下方「嵌入到自己网站」一节 |
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(古诗正文、型号一句话介绍),不需要知道会聊到哪条。 - 服务端在每条定稿的用户转写里按
keywords做子串匹配(忽略标点/空白,长关键词 优先)记住"当前聊到哪一条",按inject_mode把那一条的detail注入给模型, 同一条一次通话只注入一次,换条目重新计算。 inject_mode:on_match(默认)匹配到就注入,适合问到什么答什么的资料;on_trigger匹配到只记住,等对方说出inject_trigger_phrases之一(如"背完了") 再注入,适合先完成一件事再进入需要详情的阶段的流程。两种模式都排到模型当前这一轮 说完之后注入,对方一直没触发时冷场兜底注入。
{
"knowledge_pack": {
"title": "小学必背古诗词",
"system_role_intro": "下面是诗集里每首诗的原文,这是唯一可信的版本……收到系统提示开头的资料消息时不要念出来,用自己的话讲。",
"inject_mode": "on_trigger",
"inject_trigger_phrases": ["背完了", "背好了"],
"inject_reply_hint": "上面的诗句是书上印的,不是小朋友念的。他还没开始背就只说一句让他开始;他已经背过就拿原文核对你刚才的点评。",
"entries": [
{
"id": "jingyesi",
"title": "静夜思",
"keywords": ["静夜思", "床前明月光"],
"summary": "《静夜思》唐·李白\n床前明月光,疑是地上霜。\n举头望明月,低头思故乡。",
"detail": "逐句白话:……\n背景:……\n引导问题(挑一两个问):……"
}
]
}
}
| 字段 | 说明 |
|---|---|
title |
拼进 system prompt 的资料库小节标题 |
system_role_intro |
摘要列表前的说明:这个列表是什么、怎么用、收到注入的资料消息该怎么处理——跟资料类型强相关,由你自己写 |
inject_mode |
on_match(默认)/ on_trigger |
inject_trigger_phrases |
on_trigger 模式必填;对方说出其中任意一个(子串匹配)就注入当前条目的详情 |
inject_reply_hint |
可选。详情注入后模型紧接着那一轮该做什么。注入在协议上等于"对方又说了一句",模型一定会回一轮,不交代清楚它会把资料当成对方说的话去回应(实测:把注入的诗句当成小朋友背出来的,夸"一字不差")。写清楚"对方此刻什么都没说"以及该回什么,例如"对方还没开始背,只说一句让他开始" |
entries[].id |
包内唯一 |
entries[].keywords |
转写里出现即视为在聊这一条;名称、首句、别名、常见误听都放进来,近名条目靠更长的关键词区分 |
entries[].summary / detail |
摘要建连时全量进 prompt;详情按需注入。详情写给模型看,不是念给对方听的 |
体积约束:语音模型的 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 轮,较长的通话只剩末尾——所以还有下面这层。 - system prompt 里多一节「与这位来电人的历史通话」:每通的时间、报告
summary(设了success_criteria才有)、以及聊到的知识包条目(knowledge_entries,服务端按 关键词匹配记下来的确定性事实,不依赖 LLM 总结)。"上次背过哪几首、哪首没背对"靠 这一节,不靠转写尾巴。
几条边界:
- 只对带
caller_ref的直拨链接生效。widget 嵌入的匿名访客、不带caller_ref的直拨 没有身份,无从记忆。caller_ref由你定义,同一个人每次用同一个值即可(如孩子的 编号);它出现在地址栏里,敏感场景自行签名。 - 走 advanced 模式(
context_callback_url)的画像不生效——那种情况下上下文由你的 系统负责,想带历史就自己放进回调返回的dialog_context。 - 只接已完成(产生了有效对话)的会话;不同
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 | 直接指定开场白引导语 |
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。)- 后两个冷场话术可选,不传走默认通用文案。
失败即通话失败:回调超时、非 200、响应体不符合上面的格式时,本次会话直接建连 失败,浏览器侧收到错误提示并断开——不会静默退回默认人设模板。这是刻意的:与其 让 AI 用错误的人设跟你的用户聊完一整通,不如让失败立刻可见。
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 |
其余字段两者一致。几个容易踩的点:
metadata是你在创建 Task/画像时传入的任意 JSON 对象,本服务不解析、原样带回, 用来把回调关联回你自己的业务单据。report只有配置了success_criteria才有值,否则为null。transcript只含本次通话,不做跨通话拼接;而查询接口里reusableTask 的token_usage/cost是累计值,两者刻意不同,不要把每次回调的用量再自行加总。- 未产生有效对话的通话尝试(未接通/秒挂等)不触发这个回调。
回调鉴权
创建 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 类字段),
也都会在查询接口的响应里原样返回,方便你确认当前配置。
用量与计费字段
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 }
},
"cost": {
"voice": { "amount": 16.6735, "currency": "CNY" },
"report": { "amount": 0.0021, "currency": "USD" }
}
}
voice:实时语音通话的用量/费用,只要通话真正连上、产生过 token 消耗就会 记录,即使最终任务/会话没有产生有效对话,这笔钱也是真花出去了的。report:报告生成 LLM 调用的用量/费用。cost.report的单价由部署方配置, 未配置时该字段可能缺失(token_usage.report仍会记录)。- 通话结束前,这两个字段都是
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。
已知限制
- 通话中不支持工具调用:任务描述/知识库在通话前一次性拼进 system
prompt,AI 全程按人设自然对话,不能在通话过程中动态查询你的业务系统数据。
唯一的例外是画像上的知识包(
knowledge_pack):资料按条目在通话中由服务端择机 注入,但内容仍然是创建画像时就准备好的静态资料,不是实时查询。 - 通话链接不支持真实电话号码接入:只能通过网页链接(浏览器麦克风)发起 通话,不打真实电话号码。
- 无并发上限: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。