Documentation Sync
This page translates plugins/adapters/douyin-live/README.md. Keep this English page aligned with the README; pnpm sync:adapter-docs updates the Chinese pages.
@zhin.js/adapter-douyin-live
Experimental · unofficial protocol — built on the unofficial, undocumented danmaku WebSocket (
webcast/im/push/v2) and page data of Douyin Live on the web. It may break at any time when the platform changes, upgrades its signature or tightens risk control. It is not part of the stable smoke tests, and its interface and config may change incompatibly. Make sure your use complies with Douyin's user agreement, and use it only for your own live room.
🚧 Listen-only for now:
sendModedefaults tooff.sendMode: directis not implemented yet (there is no stable, testable send API); sending fails withnot_implementedand no reply appears in the room.
Accounts are managed by DouyinLiveAdapter. The Endpoint extends the actual DouyinLiveClient, keeping its native methods and events; framework lifecycle, canonical inbound and send are wired through endpointSpec and the AdapterIndex without taking over native Client methods.
Session.event is the Client's public DouyinLiveChat event object (uid, nickname, text, timestamp, optional id / roomId), the same reference as the danmaku event. Network frames are parsed by the package's minimal protobuf decoder and are not a public event DTO.
A Douyin live danmaku adapter for Zhin.js (Plugin Runtime). The Bot watches danmaku in your own live room; danmaku enters the Agent's danmaku window just like Bilibili live danmaku. Design: ADR 0038: Continuous input merge §5. Discussion: #1049, #1135.
Read this first
- Unofficial protocol, will break: room resolution, WebSocket query parameters, the
signatureand protobuf fields all come from observing the web client. When the platform changes them, the connection keeps failing and retries with backoff; watch the logs. - No built-in signature: the platform expects a
signatureon the WebSocket URL (generated by obfuscated web scripts). This package does not ship or vendor any platform script or third-party scraper. PointsignerUrlat a signer service you run yourself. Without it the socket connects unsigned and the platform may refuse the handshake. - One reply per window: the adapter declares the
liveDanmakucapability. The Agent does not answer each danmaku; it collects danmaku in a window and the model picks one (or summarizes them) and replies once. See "Danmaku window" below. - Danmaku turns are read-only: they run with anonymous-viewer access and only see tools that declare
readOnly: true. They never show an approval card and never run shell, MCP or sub-agents. - Listen-only by default (sendMode α):
sendModedefaults tooff.directonly validates the room and content, then returnsnot_implemented; it never calls a platform write API. - Cookies are account credentials:
cookieis optional (a guestttwidis requested at startup when omitted). If you provide your browser cookie, inject it with an environment variable, never write it in plain text inzhin.config.yml, never commit it, and never paste it into logs or issues. The package never logs cookies. - Your own room only: the endpoint connects only to the configured
roomId. Marketing, traffic farming, spam and scraping other people's rooms are forbidden.
Current capabilities
| Capability | Status |
|---|---|
web_rid → internal room_id (live page data), guest ttwid | ✅ Done (re-resolved on every reconnect) |
Danmaku WebSocket: 10 s hb heartbeat, ack on needAck, watchdog, backoff reconnect | ✅ Done |
Decode gzip PushFrame → Response, parse only WebcastChatMessage | ✅ Done (gifts, entries, likes, follows ignored) |
| Dedupe by message id (last 500) | ✅ Done |
WebSocket signature | 🚧 Not built in; optional external signerUrl |
Sending danmaku (sendMode: direct) | 🚧 Not implemented, returns not_implemented |
| Filtering your own danmaku | 🚧 None yet (the package does not send and does not identify the logged-in account) |
| Gift / entry / like / follow events, QR login, multiple rooms, Console page | 🚧 Not done |
Prerequisites
- The web_rid of your own Douyin live room (the number in
https://live.douyin.com/<web_rid>) asroomId - Optional: your full browser cookie string in
.env(gitignored) asDOUYIN_LIVE_COOKIE. Without it a guestttwidis used - Optional: a self-hosted signer service URL as
signerUrl(see "Signature") - The Agent is enabled (
ai.agent): itsinboundQueue.livedrives the danmaku window
Minimal Configuration
configVersion: 2
plugins:
douyin-live: "@zhin.js/adapter-douyin-live"
douyin-live:
sendMode: off # default; direct is not implemented yet (returns not_implemented)
maxLength: 50 # reserved for future direct sending
maxChunks: 2
minSendIntervalSec: 5
endpoints:
- id: my-live
roomId: "123456789" # web_rid; your own room only
cookie: ${DOUYIN_LIVE_COOKIE}
# signerUrl: http://127.0.0.1:8080/signRequired fields (endpoints[i]): id, roomId.
Signature
Before connecting, the endpoint joins a fixed set of query parameters (live_id, aid, version_code, webcast_sdk_version, room_id, user_unique_id, identity, …) as k=v,k=v,... and takes its md5 as X-MS-STUB, following the web client. With signerUrl configured it requests:
GET <signerUrl>?stub=<X-MS-STUB>The response is either the plain-text signature or JSON { "signature": "..." }; it becomes the WebSocket signature parameter. You deploy and maintain the signer yourself; this package does not provide one.
Danmaku window
The window is controlled by the Agent config (not this adapter's config), the same as @zhin.js/adapter-bilibili-live:
ai:
agent:
inboundQueue:
live:
mode: pick-one # pick-one: answer one; summary: summarize the window and reply once
windowMs: 10000 # how long to wait after the first danmaku
minIntervalMs: 15000 # minimum gap between two danmaku repliesSessions and messages
| Field | Value |
|---|---|
conversation | { kind: 'channel', id: <web_rid> } (same as config roomId) |
message.id | Platform message id (falls back to timestamp + uid + sequence) |
sender.id | Viewer user id; anon:<nickname> when unavailable |
metadata | { roomId: <internal room_id>, liveDanmaku: true } |
Outbound (future direct) is plain text only: Markdown is flattened and images and other segments are unsupported. Douyin danmaku has no reply field, so a mention is folded into the body as @nickname.
Disk usage
The adapter writes nothing to disk: the dedupe table (last 500 message ids) and connection state live in memory and are cleared on restart.
Known limitations
- The protocol is undocumented; fields and signatures may change at any time, and unsigned connections may be refused.
- Viewers without a uid are told apart by nickname, so viewers with the same nickname are merged.
- This slice ships no Console page or RPC.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
Log douyin_live_unsigned | No signerUrl, connecting unsigned. If the handshake is refused, deploy a signer and set signerUrl |
Douyin live page returned no room_id | The room is not live, the web_rid is wrong, or the page changed. Check roomId |
Douyin live did not issue a ttwid cookie | Guest cookie request failed (risk control / network). Put your browser's ttwid in cookie |
| Connection keeps dropping and reconnecting | Signature or cookie expired, or the protocol changed; the watchdog also reconnects after 30 s without data |
Send returns send_disabled | sendMode: off (default), listen-only |
Send returns not_implemented | sendMode: direct is not implemented yet; switch back to off |