第三方 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 + secret | POST /v1/anchors/{id}/ingest-sources | 弹幕平台直推 |
两种 Key,先搞清楚你拿的是哪种:
| 每主播专用 Key(推荐) | 租户级 Key | |
|---|---|---|
| 能操作 | 只有它绑定的那一个主播 | 租户下所有主播 |
| 建主播 / 改配置 | ❌ 不行 | 需要对应 scope |
| 泄露影响 | 只影响那一个主播(吊销重建即可) | 整个租户 |
| 怎么拿 | 后台 → 选主播 → API 接入 → 生成新 Key | python 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"}
| HTTP | code | 含义 |
|---|---|---|
| 401 | unauthorized | Key 缺失/无效/已吊销 |
| 403 | forbidden_scope | Key 缺对应 scope;或每主播 Key 想建主播 |
| 403 | api_disabled | 该主播的对外 API 被运营关掉了(后台 API 接入页签可开) |
| 404 | not_found | 主播不存在 或不属于你的租户 / 不是你 Key 绑定的那个(刻意不区分) |
| 409 | anchor_not_live / slot_full | 未开播 / 进程容量已满 |
| 422 | validation_failed | 参数问题(文案会说明哪一项) |
| 429 | rate_limited | 预留(当前不限额) |
| 500 | internal | 平台内部错误,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}]}