API 集成文档 / Docs
API REFERENCE

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 两套接口),选哪个 纯粹取决于你的业务场景是"一次性任务"还是"常驻入口"。

典型集成流程(两种业务共通):

  1. 你的系统调用创建接口(POST /api/tasksPOST /api/agents),拿到一条 call_url
  2. 把这条链接发给通话对象(短信/IM/官网按钮等,由你的系统决定渠道),对方在 浏览器打开链接、点击接听,AI 通过实时语音完成对话。
  3. 通话结束后,服务端自动生成结构化报告(report,含 verdict 判定结果)。
  4. 你的系统轮询查询接口(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/personasGET /api/agent-voices——这两个是纯静态 目录数据,方便你在拿到 Key 之前就能先渲染选择器。


账号 API:注册、登录、API Key 管理

Base path /api/authregister/login/verify-email/resend-verification/ forgot-password/reset-password 是公开接口,不需要鉴权(拿不到登录态的场景本来 就要靠它们自救);其余接口都要求登录态(Authorization: Bearer <session_token>), 不接受业务 API Key。

注册是两步POST /register 只建号并发出验证邮件,不返回登录态;邮箱验证 通过才算完成注册,第一个 session_tokenPOST /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"
}

在通话尚未开始/尚未结束时,transcriptreportcost 等字段为 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查询任务的通话历史 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": "拿到来电人的称呼和一种联系方式即视为达成。"
  }'

响应 200AgentResponse,包含创建时传入的全部字段,加上:

{
  "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_urlwidget_key 是两个不同用途、不能互换的凭据:

知识包:按条目组织、详情按需注入

knowledge_base 是整段进 prompt 的自由文本,适合几百字的营业信息;但有一类资料 通话开始前不知道对方会聊到哪一条(陪小朋友背古诗不知道要背哪首、产品客服不知道 客户会问哪个型号),全量塞进去又太大。通话中没有工具调用(见「已知限制」),模型自己 也查不了。knowledge_pack 是这类资料在本服务里的做法:

{
  "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 通已完成的会话会接进下一通:

几条边界:

会话记录(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.comhttps://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-positionbottom-right(默认)/ bottom-left / top-right / top-leftdata-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 返回 400widget_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 主动告别挂断的话术;不传用默认通用文案

前三个是一组,要么都传、要么都不传——只传其中一两个会返回 422system_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": "……可选……"
}

失败即通话失败:回调超时、非 200、响应体不符合上面的格式时,本次会话直接建连 失败,浏览器侧收到错误提示并断开——不会静默退回默认人设模板。这是刻意的:与其 让 AI 用错误的人设跟你的用户聊完一整通,不如让失败立刻可见。

caller_ref:让一个画像区分不同终端用户

在入口链接后面再接一段路径,这段内容会原样出现在 context_callback_urlresult_callback_url 的请求体里:

{call_url}/{caller_ref}

本服务对 caller_ref 只做透传,不解析、不校验、不签名——防篡改是你自己的责任 (终端用户能看到并修改地址栏里的这段内容,敏感场景请自行加签名)。不带这段路径时 行为不变,回调里的 caller_refnull

不走 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

其余字段两者一致。几个容易踩的点:

回调鉴权

创建 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 出现在 TaskResponseAgentSessionResponse 里, 结构完全一致:

{
  "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" }
  }
}

这一层只做用量记录 + 成本计算,不是完整的计费/账本系统:不会给 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": "..."}422detail 是 FastAPI 标准的字段级 错误数组)。


集成最佳实践


已知限制

如需了解产品定位、设计取舍等背景,见仓库内 docs/prd/voice_agent.md(面向 维护者,非本 API 文档的一部分)。

本页由 docs/api/integration_guide.md 生成,源文档才是唯一维护 入口——发现内容有误请去改那份 Markdown,然后重新跑 scripts/generate_api_docs.py