第三方 API 接入(10 分钟版)

基础地址:https://szr.dzhan.cc(内网调试用 http://192.168.199.150:8010) 实时语音地址:wss://szr.dzhan.cc(内网 ws://192.168.199.150:7880)

0. 你要先拿到什么

东西谁给用途
API Key(vt_live_…)平台运营在每个主播的「API 接入」页签里生成(推荐),或发一把租户级 Key所有服务端调用
anchor_id就是那把 Key 绑定的主播;租户级 Key 需要你自己指定指向某个主播
ingest_url + secretPOST /v1/anchors/{id}/ingest-sources弹幕平台直推

两种 Key,先搞清楚你拿的是哪种:

每主播专用 Key(推荐)租户级 Key
能操作只有它绑定的那一个主播租户下所有主播
建主播 / 改配置❌ 不行需要对应 scope
泄露影响只影响那一个主播(吊销重建即可)整个租户
怎么拿后台 → 选主播 → API 接入 → 生成新 Keypython scripts/tenant.py issue-key

每主播 Key 默认带这些 scope(不能改大):anchor:read、anchor:control、session:create、danmaku:ingest、event:read。 拿到后用 GET /v1/me 能看到 key.anchor_id——非空就说明是被限定的。

每个主播还有一个总开关(后台 → API 接入)。关掉后该主播所有 /v1/* 返回 403 api_disabled,但观众页和直播不受影响。

Key 的 scope 决定能干什么:

anchor:read       查主播/状态
anchor:write      建/改/删主播(每主播 Key 不会有)
anchor:control    开播/停播/点播/弹幕/音乐
show:write        节目单生成与审核(每主播 Key 不会有)
session:create    换观众端 LiveKit token(嵌入用)
danmaku:ingest    弹幕注入与直推源管理
event:read        SSE 事件流
webhook:write     Webhook 订阅
platform:admin    给别的租户发 Key(只给自营用)

1. 自检

curl -H "Authorization: Bearer vt_live_xxx" https://szr.dzhan.cc/v1/me
{"ok":true,"tenant":{"id":"t_acme","name":"某公司","slug":"acme"},
 "key":{"id":"k_123","name":"prod","scopes":["anchor:read","session:create"]},
 "limits":{"max_dynamic_anchors":4,"max_danmaku_batch":50},
 "endpoints":{"api_base":"https://szr.dzhan.cc","livekit_url":"wss://live.example.com"}}

2. 建一个主播

curl -X POST https://szr.dzhan.cc/v1/anchors \
  -H "Authorization: Bearer vt_live_xxx" -H 'Content-Type: application/json' \
  -d '{
    "slug": "acme-host",
    "display_name": "小A",
    "persona": {"system": "你是「小A」,某公司的主播。说话简短、口语化,不夸张。"},
    "voice": {"speed_ratio": 1.0, "pitch_ratio": 1.0},
    "knowledge": {"provider": "none"},
    "music": {"enabled": true, "track": "builtin:synthwave"},
    "show": {"theme": "新人常见问题", "duration_min": 30}
  }'

返回 {"ok":true,"anchor":{"id":"a_1f2c3d4e","status":"stopped",...}}。

knowledge 想接知识库:{"provider":"fastgpt_app","app_id":"<FastGPT 应用 id>"}(平台侧已配好 Key), 或 {"provider":"fastgpt_bridge","datasets":["<datasetId>"]}。

3. 开播 / 查状态 / 停播

curl -X POST https://szr.dzhan.cc/v1/anchors/a_1f2c3d4e/start -H "Authorization: Bearer …"
curl        https://szr.dzhan.cc/v1/anchors/a_1f2c3d4e/state -H "Authorization: Bearer …"
curl -X POST https://szr.dzhan.cc/v1/anchors/a_1f2c3d4e/stop  -H "Authorization: Bearer …"

开播后 30~60 秒内会听到人声(进程拉起 + 派单 + 问候语)。

{"ok":true,"anchor_id":"a_1f2c3d4e","live":true,"room":"vtb-acme-acme-host",
 "stage":{"phase":"script","speaking":true,"segment_title":"开场","sentence_index":3,"total_sentences":34},
 "judge":{"window_min":60,"seen":12,"replied":9}}

4. 让主播说一句(运营插播)

curl -X POST https://szr.dzhan.cc/v1/anchors/a_1f2c3d4e/say \
  -H "Authorization: Bearer …" -H 'Content-Type: application/json' \
  -H "Idempotency-Key: say-20261102-001" \
  -d '{"text":"我们的新品今天上线,前 100 名有优惠。"}'

→ {"ok":true,"speech_id":"s_7d5cbe1ad6","text":"…"}

Idempotency-Key 24 小时内重复提交会命中缓存(返回上次结果,带 idempotent_replay:true)。

5. 把观众放进直播间(嵌入)

不要把 API Key 放前端。正确做法:你的后端换 token,前端拿 token。

# 你的后端
curl -X POST https://szr.dzhan.cc/v1/sessions -H "Authorization: Bearer …" \
  -H 'Content-Type: application/json' \
  -d '{"anchor_id":"a_1f2c3d4e","identity":"u_9001","name":"张三","ttl":600,"allow_danmaku":true}'
{"ok":true,"token":"eyJhbGciOi…","ws_url":"wss://live.example.com","room":"vtb-acme-acme-host",
 "identity":"u_9001","expires_at":1793620000,"allow_danmaku":true}

前端贴一段:

<script src="https://szr.dzhan.cc/embed.js"
        data-anchor="a_1f2c3d4e"
        data-token="eyJhbGciOi…"
        data-theme="dark" data-ratio="16/9"></script>

或手动控制:

const vtb = VTB.mount('#box', { anchor: 'a_1f2c3d4e', token: 'eyJ…' });
vtb.on('caption', (d) => showSubtitle(d.text));
vtb.on('state', (d) => console.log(d.phase, d.speaking));
vtb.danmaku('主播你好呀');
vtb.mute(true);

观众端 token 默认 不能发音频(can_publish=false),只能订阅 + 发数据(弹幕)。

6. 点播的另两种形态

# 纯文本问答(不发声、不进房间):适合客服机器人复用同一套知识
curl -X POST https://szr.dzhan.cc/v1/anchors/a_1f2c3d4e/ask \
  -H "Authorization: Bearer …" -H 'Content-Type: application/json' \
  -d '{"text":"退款怎么处理?","user":"张三"}'
# → {"ok":true,"answer":"…","sources":[{"text":"…","score":0.71}],"latency_ms":1600}

7. 事件(SSE / Webhook)

# 服务端订阅(推荐 Webhook);前端订阅用 SSE
curl -N "https://szr.dzhan.cc/v1/events?anchor=a_1f2c3d4e&access_token=<key>"
id: 361
event: speech.finished
data: {"id":361,"type":"speech.finished","payload":{"kind":"script","text":"…","duration_ms":3592}}

Webhook:

curl -X POST https://szr.dzhan.cc/v1/webhooks -H "Authorization: Bearer …" \
  -H 'Content-Type: application/json' \
  -d '{"url":"https://your.app/vtb-hook","events":["danmaku.replied","speech.finished"]}'
# → {"webhook":{"id":"w_ab12","secret":"…(只显示一次)"}}

接收与验签(Node 示例):

const crypto = require('crypto');
app.post('/vtb-hook', (req, res) => {
  const ts = req.get('X-VTB-Timestamp');
  const sig = req.get('X-VTB-Signature');             // sha256=…
  const body = req.rawBody;                            // 原始字节,别用 JSON.stringify
  const expect = 'sha256=' + crypto.createHmac('sha256', SECRET)
                                 .update(ts + '.').update(body).digest('hex');
  if (expect !== sig) return res.status(401).end();
  res.json({ ok: true });                              // 先 200,再异步处理
  handle(req.body);
});

投递策略:超时 10s,失败按 1s/5s/30s/5min/30min 重试 5 次,连续 20 次失败自动停用。

8. 弹幕接入

见 ingest.md:三条路径(单条 / 批量 / 平台直推)+ 洪峰聚合 + 回复回传。

9. 错误码

{"ok":false,"error":{"code":"anchor_not_live","message":"主播当前未开播:a_1f2c3d4e","retryable":true},
 "request_id":"r_1f3c9a"}
HTTPcode含义
401unauthorizedKey 缺失/无效/已吊销
403forbidden_scopeKey 缺对应 scope;或每主播 Key 想建主播
403api_disabled该主播的对外 API 被运营关掉了(后台 API 接入页签可开)
404not_found主播不存在 或不属于你的租户 / 不是你 Key 绑定的那个(刻意不区分)
409anchor_not_live / slot_full未开播 / 进程容量已满
422validation_failed参数问题(文案会说明哪一项)
429rate_limited预留(当前不限额)
500internal平台内部错误,retryable:true 可退避重试

所有响应带 X-Request-Id,排查时报这个 id。

10. 机器可读的接口定义

GET /v1/openapi.json(公开,无需 Key)—— 从路由表实时生成,26 条路径, 可以直接喂给 Postman / Insomnia / openapi-generator,不会跟代码漂移。

curl https://szr.dzhan.cc/v1/openapi.json -o openapi.json

11. 用量

curl "https://szr.dzhan.cc/v1/usage?days=7" -H "Authorization: Bearer …"
# → {"ok":true,"days":7,"daily":[{"day":"2026-11-02","total":128,"errors":3}]}