# TikTok LIVE Studio 本地数据开放——完整上下文 > 本地 WebSocket 协议、公共事件、示例、排障和跨技术栈 Agent Skill 的完整中文接入上下文。Agent Skill 源文件保留英文原文。 Protocol version: `1.0.0`. This file is generated from the documentation and Agent Skill sources in the GitHub repository. Do not edit it manually. https://tiktok-live-studio-local-data-access.pages.dev/zh/guide/overview docs/zh/guide/overview.md # 概览 LIVE Studio 本地数据开放把主播机上选定的直播间事件,开放给受信任的本地第三方客户端。 LIVE Studio 在主播本机提供 WebSocket 端点。第三方客户端连接该端点,使用发放的凭证鉴权,随后接收实时事件。 ## 能做什么 - 本地 H5 工具可以根据点赞、礼物和评论做响应。 - Unity 或原生游戏客户端可以消费直播间事件,而不必嵌入 LIVE Studio。 - 开发者工具可以通过稳定的 JSON 协议做调试或自动化。 ## 不能做什么 - 不会把个性化状态推到观众设备。 - 不能让直播流按人分画面。 - 不提供远程云端 API。 - 不能绕过应用白名单或事件白名单策略。 ## 当前公开事件 | 事件 | 说明 | | --- | --- | | `live.like` | 直播间点赞消息 | | `live.gift` | 直播间礼物消息 | | `live.chat` | 直播间文本评论消息 | ## 接入模型 ```mermaid flowchart LR subgraph CreatorMachine[主播本机] Studio[LIVE Studio] Client[本地第三方客户端] end Studio -->|WebSocket /v1/third-party| Client ``` 端点只在本机开放。客户端必须连接 `127.0.0.1`,并完成鉴权后才能接收事件。 --- https://tiktok-live-studio-local-data-access.pages.dev/zh/demos/ docs/zh/demos/index.md INTERACTIONS IN ACTION 看看直播间能玩出什么。 用模拟点赞、礼物和评论试玩两款游戏,无需账号或凭证。再通过一段录制视频,看看塔防互动在 LIVE Studio 中的实际呈现。 [Tug of Trap](https://tiktok-live-studio-local-data-access.pages.dev/zh/demos/tug-of-trap) · [Candy Arena](https://tiktok-live-studio-local-data-access.pages.dev/zh/demos/candy-arena-duel). Public demo mode uses simulated events. [View resource](https://tiktok-live-studio-local-data-access.pages.dev/zh/demos/tower-defense) 视频案例 · 非在线试玩 Tower Defense × LIVE Studio 观看一份礼物如何变成防御塔,改变当前战局。这是一段观众互动转化为游戏动作的录制演示。 [观看案例 →](https://tiktok-live-studio-local-data-access.pages.dev/zh/demos/tower-defense) 准备接入真实事件?先[申请接入](https://tiktok-live-studio-local-data-access.pages.dev/zh/apply);已经获批的开发者可以直接阅读[快速开始](https://tiktok-live-studio-local-data-access.pages.dev/zh/guide/quick-start)。公开试玩模式不会连接你的直播间。 --- https://tiktok-live-studio-local-data-access.pages.dev/zh/demos/tug-of-trap docs/zh/demos/tug-of-trap.md # 拔河陷阱 **一款由整场直播共同参与的阵营对抗游戏,使用 Vite、TypeScript 与 PixiJS 构建。** 观众选择蓝队或红队,一起积累拉力,并通过礼物保护己方或干扰对手。 ![蓝红两队参与拔河陷阱](https://tiktok-live-studio-local-data-access.pages.dev/media/demos/tug-of-trap-cover.webp) [试玩模拟互动 ↗](https://liangpeiran-bit.github.io/playable_interaction_game/?demo=1) [开始接入](https://tiktok-live-studio-local-data-access.pages.dev/zh/guide/quick-start) 玩法形态多人阵营对抗 技术栈PixiJS + TypeScript 公开事件点赞 · 礼物 · 评论 数据链路本机 WebSocket ## 从直播事件到拔河 ```mermaid flowchart LR A[live.chat] --> B[选择蓝队或红队] C[live.like] --> D[个人力量与团队战意] E[live.gift] --> F[阵营技能] B --> G[拔河规则引擎] D --> G F --> G G --> H[角色、绳索、特效与结算] ``` | 事件 | 游戏映射 | 稳定性规则 | | --- | --- | --- | | `live.chat` | 选择阵营;识别号子并推进团队事件 | 单局锁队;评论指令带个人冷却 | | `live.like` | 强化观众小兵;每累计 100 赞形成团队点赞浪潮 | 使用累计里程碑,不按原始消息逐条触发 | | `live.gift` | 为已选阵营发动具名战术技能 | 按 `gift.id` 匹配,处理连送差值,并为未开局礼物排队 | ## 礼物如何影响比赛 | 礼物 ID | 礼物 | 效果 | | --- | --- | --- | | `5655` | 玫瑰 | 团队战意 +1 | | `5879` | 甜甜圈 | 8 秒护盾,可抵挡一次手柄反转 | | `7569` | 游戏手柄 | 反转主播操作 3.5 秒 | | `17358` | 仓鼠加油 | 6 名可见援军加入 8 秒 | | `11046` | 宇宙 | 对方拉力降至 70%,并反转操作 6 秒 | 观众未选队时送出的礼物会短暂等待认领,超时后进入真人较少的一队。同类效果通过上限、冷却或转化战意处理,不会无限叠加破坏对局。 ## 编辑与构建过程 1. **先验证游戏循环。** 在接入直播数据前,用确定性的拔河模拟建立队伍拉力、绳索位置、回合状态和胜负条件。 2. **隔离协议边界。** 端口发现、`SERVER_HELLO` 校验、鉴权、心跳、去重与重连都放在 PixiJS 场景之外。 3. **把事件变成公平规则。** 选队、点赞里程碑、号子窗口、礼物认领、连送处理和跨局队列均成为可测试的领域逻辑。 4. **提高演出完成度。** 从平面原型迭代为 3D 玩具沙盘,补齐蓝红状态、角色反应、礼物特效、MVP、连胜和结算动画。 5. **提供公开试玩。** `?demo=1` 明确使用模拟互动;正式模式仍要求 LIVE Studio 与仅保存在内存中的凭证。 ## 关键玩法状态
[Image](https://tiktok-live-studio-local-data-access.pages.dev/media/demos/tug-of-trap-rally.webp)
多人评论协作触发限时接力冲刺。
[Image](https://tiktok-live-studio-local-data-access.pages.dev/media/demos/tug-of-trap-universe.webp)
宇宙礼物同时改变拉力、操作、光照与状态提示。
## 工程经验 - 浏览器不假设固定端口,而是发现 `127.0.0.1:49152-65535`,校验 hello 后再鉴权。 - 凭证只保存在内存中;公开试玩不要求输入或持久化凭证。 - 事件适配器输出游戏领域命令,渲染层不直接解析协议载荷。 - 有界队列、冷却、去重和回合清理,避免高频事件破坏比赛状态。 - 线上只部署可试玩构建产物,开发源码可以继续保持私有。 下一步:[查看事件协议](https://tiktok-live-studio-local-data-access.pages.dev/zh/protocol/events)或[申请接入](https://tiktok-live-studio-local-data-access.pages.dev/zh/apply)。
--- https://tiktok-live-studio-local-data-access.pages.dev/zh/demos/candy-arena-duel docs/zh/demos/candy-arena-duel.md # 糖果擂台 **一款由主播操作的节奏对战游戏,使用 React、Phaser 和确定性的 TypeScript 战斗引擎构建。** 主播完成核心战斗,观众则通过能量、辅助和礼物技能直接加入对局。 ![糖果擂台节奏战斗](https://tiktok-live-studio-local-data-access.pages.dev/media/demos/candy-arena-cover.webp) [试玩模拟互动 ↗](https://liangpeiran-bit.github.io/candy-arena-duel/?demo=1) [查看 Web 示例](https://tiktok-live-studio-local-data-access.pages.dev/zh/samples/javascript) 玩法形态节奏战斗 技术栈React + Phaser 操作输入键盘 + 直播事件 质量保障单测 + E2E ## 两种输入,一个战斗引擎 ```mermaid flowchart LR A[主播节拍与闪避] --> D[确定性战斗引擎] B[live.like 与 live.chat] --> C[有上限的观众辅助] E[live.gift] --> F[具名攻击队列] C --> D F --> D D --> G[React HUD + Phaser 场景 + 音效] ``` | 输入 | 游戏映射 | 边界 | | --- | --- | --- | | 主播操作 | 方向序列、节拍确认与主动闪避 | 仍是赢得对局的主要方式 | | `live.like` | 每 100 赞增加 2 点英雄能量 | 每局最多 20 点 | | `live.chat` | 15 秒内 5 名不同观众助威,获得一张辅助券 | 身份窗口与单局存储上限 | | `live.gift` | 跳过节拍输入,直接排队释放英雄已有攻击 | 严格按 ID 匹配、处理连送差值、有界 TTL 队列 | ## 礼物复用已有战斗系统 | 礼物 ID | 礼物 | 触发的既有招式 | | --- | --- | --- | | `5655` | 玫瑰 | 星糖快拳 | | `6064` | GG | 彩虹回旋踢 | | `5487` | 手指爱心 | 彗星上勾拳 | | `5879` | 甜甜圈 | 星糖冲刺 | | `7569` | 游戏手柄 | 彗星上勾拳 | | `11046` | 宇宙 | 银河糖果风暴 | 接入层没有维护第二套“礼物专用战斗模型”。礼物事件和主播操作使用同一条攻击队列、动画状态机、命中时机、特效与声音链路。 ## 编辑与构建过程 1. **把视觉目标转成可玩循环。** React 负责 HUD 与操作,Phaser 负责竞技场、角色、镜头和特效。 2. **让战斗结果可复现。** 固定时间步引擎把伤害与时序从渲染层分离,便于验证和测试。 3. **建立主播操作深度。** 方向序列、节拍判定、Combo、大招能量和主动闪避,让没有直播事件时游戏仍然成立。 4. **接入本地数据开放。** 客户端扫描动态回环端口、完成鉴权、标准化公开事件,再交给独立的互动控制器。 5. **逐步提高演出与质量。** 持续补齐角色姿态、场景纵深、观众动画、礼物专属特效、音效、响应式布局、贴图检查、单元测试和多分辨率 E2E。 6. **分离公开试玩与真实接入。** `?demo=1` 跳过凭证并明确产生模拟事件;默认地址仍保留真实 LIVE Studio 连接门槛。 ## 战斗演出
[Image](https://tiktok-live-studio-local-data-access.pages.dev/media/demos/candy-arena-gift.webp)
宇宙事件进入已有大招链路。
[Image](https://tiktok-live-studio-local-data-access.pages.dev/media/demos/candy-arena-ultimate.webp)
攻击状态、镜头、角色姿态、粒子与声音同时结算。
## 工程经验 - React 与 Phaser 通过战斗引擎通信,而不是互相修改状态。 - 直播互动控制器负责助威窗口、点赞里程碑、礼物连送差值、队列与播报。 - 损坏或未知事件不会中断战斗,重连行为也不影响战斗时钟。 - 启动凭证提交后会离开 DOM,不写入 URL 或 Web Storage。 - 公开试玩明确标注并只使用模拟事件,不会与真实直播间连接成功混淆。 下一步:[实现 JavaScript 客户端](https://tiktok-live-studio-local-data-access.pages.dev/zh/samples/javascript)或[浏览礼物目录](https://tiktok-live-studio-local-data-access.pages.dev/zh/reference/gift-catalog)。
--- https://tiktok-live-studio-local-data-access.pages.dev/zh/demos/tower-defense docs/zh/demos/tower-defense.md # Tower Defense × LIVE Studio 一段录制的互动案例:礼物变成防御塔,实时改变 LIVE Studio 中的战局。这是视频,不是在线试玩,也不提供可下载的游戏工程。 [直接打开录制视频](https://tiktok-live-studio-local-data-access.pages.dev/media/interactive-tower-defense-demo.mp4)。 ## 在你的游戏中实现类似玩法 | 观众输入 | 游戏负责什么 | | --- | --- | | 收到礼物事件 | 按字符串 `payload.gift.id` 匹配自己配置的规则。 | | 满足选定的触发条件 | 生成防御塔,或执行游戏定义的其他动作。 | | 收到未知礼物 | 忽略或进入明确的默认处理,不断开连接。 | 视频展示互动效果,不代表其内部实现规范。要实现类似效果,可以从 [live.gift](https://tiktok-live-studio-local-data-access.pages.dev/zh/events/live-gift) 和[礼物目录](https://tiktok-live-studio-local-data-access.pages.dev/zh/reference/gift-catalog)开始。绑定开销较大的游戏动作前,先明确连击触发策略。 想动手体验模拟互动,可以试玩[拔河陷阱](https://tiktok-live-studio-local-data-access.pages.dev/zh/demos/tug-of-trap)或[糖果擂台](https://tiktok-live-studio-local-data-access.pages.dev/zh/demos/candy-arena-duel)。接收真实事件则从[快速开始](https://tiktok-live-studio-local-data-access.pages.dev/zh/guide/quick-start)开始。 --- https://tiktok-live-studio-local-data-access.pages.dev/zh/guide/quick-start docs/zh/guide/quick-start.md # 快速开始 目标很简单:完成一次鉴权,收到一条真实事件。不需要先做出完整游戏。 ## 1. 确认接入资格 **还没有获批?** 先[申请接入](https://tiktok-live-studio-local-data-access.pages.dev/zh/apply)。等待期间可以体验[模拟 Demo](https://tiktok-live-studio-local-data-access.pages.dev/zh/demos/),但试玩不代表已获得真实直播间数据。 **已经获批?** 请准备: - 与客户端运行在**同一台机器**上的 LIVE Studio。 - 按获批邮件说明,已开启本地数据能力的安装版本和账号。 - 为应用签发的 `app_id`、`key_id` 和 `secret`。 目前这里尚未列出已确认的最低 LIVE Studio 版本。请以获批邮件中的配置说明为准;仅升级客户端不代表能力已开通。缺少说明或凭证时,请联系 [TikTok_LIVE_Studio_Office@bytedance.com](mailto:TikTok_LIVE_Studio_Office@bytedance.com)。 ## 2. 运行本地示例 按 [H5:可运行示例](https://tiktok-live-studio-local-data-access.pages.dev/zh/samples/h5)下载三个小文件,并在 `127.0.0.1` 启动。预览服务需要 Node.js 22+,浏览器需要允许本地 WebSocket 访问。 运行时填写凭证,再点击 **Connect**。不要在公开试玩页输入密钥,也不要把密钥写进下载的源码。 预期状态: ```text SCANNING → AUTHENTICATING → AUTHENTICATED ``` 客户端会有界扫描 `49152–65535`,验证 `SERVER_HELLO` 后发送 `AUTH`。只有收到 `AUTH_RESULT.success=true` 才显示 **AUTHENTICATED**。 ## 3. 收到第一条事件 在已开播的直播间里,产生一次应用已开放类型的互动。若已开放点赞或评论,可以从它们开始,不必购买礼物来测试连接。 例如,本地日志将显示: ```text EVENT live.like ``` 这是预期输出,不是示例自动注入的模拟事件。直播间没有新互动时,可能不会产生事件;鉴权成功也不代表所有消息类型均已开放。 | 现象 | 先检查什么 | | --- | --- | | `ENDPOINT_NOT_FOUND` | 是否同机运行、LIVE Studio 安装版本和账号是否已开启能力,以及浏览器本地网络策略。 | | `INVALID_CREDENTIALS` 或 `ACCESS_DISABLED` | 签发的凭证和应用配置;不要连续快速重试。 | | 已显示 `AUTHENTICATED`,没有事件 | 是否已开播、是否产生新互动、对应 IM 消息类型是否已开放。 | | `POLICY_DISCONNECT_510` | 接入策略已变化,先确认资格再重连。 | 更多检查方法见[排障指南](https://tiktok-live-studio-local-data-access.pages.dev/zh/guide/troubleshooting)。 ## 4. 把事件接入游戏 选择一种[事件](https://tiktok-live-studio-local-data-access.pages.dev/zh/events/),把处理函数接到游戏动作上。礼物玩法可以从[礼物目录](https://tiktok-live-studio-local-data-access.pages.dev/zh/reference/gift-catalog#%E6%A3%80%E7%B4%A2%E7%A4%BC%E7%89%A9)复制字符串 ID,再决定连击何时触发效果。 保持连接逻辑与游戏规则独立。示例采用手动重连,不包含生产环境自动重试;每次新连接都需要重新发现服务和鉴权。上线前,按[连接生命周期](https://tiktok-live-studio-local-data-access.pages.dev/zh/protocol/connection)补齐可取消的退避重连。 想让 Agent 帮忙?[安装 Agent Skill](https://tiktok-live-studio-local-data-access.pages.dev/zh/guide/agent-skill),或提供 [llms.txt](https://tiktok-live-studio-local-data-access.pages.dev/zh/llms.txt)。Unity 开发者可以继续阅读[接入指南](https://tiktok-live-studio-local-data-access.pages.dev/zh/samples/unity)。 ::: warning 凭证管理 不要记录或上传密钥,不要放进公开静态资源或提交到仓库。此示例只在内存中使用密钥;正式客户端应通过受信任的运行时机制或系统密钥库管理凭证,不使用明文项目文件。 ::: --- https://tiktok-live-studio-local-data-access.pages.dev/zh/guide/architecture docs/zh/guide/architecture.md # 架构 LIVE Studio 本地数据开放是主播本机上的 WebSocket 服务。第三方客户端发现端点、用发放的凭证鉴权,随后接收直播间事件。 ## 部署模型 ```mermaid flowchart LR subgraph CreatorMachine[主播本机] Studio[LIVE Studio] Client[第三方客户端] end Studio -->|"ws://127.0.0.1:49152-65535/v1/third-party"| Client ``` 服务只接受回环连接。客户端必须连接 `127.0.0.1`。远程主机无法访问该端点。 ## 客户端流程 ```mermaid flowchart TD Scan[有界并发扫描
127.0.0.1:49152-65535] Hello[校验 SERVER_HELLO] Auth[发送 AUTH] Result{AUTH_RESULT} Events[接收 EVENT] Closed[连接关闭] Scan --> Hello Hello --> Auth Auth --> Result Result -->|success=true| Events Result -->|success=false| Closed Events --> Closed Closed --> Scan ``` 连接断开后,鉴权状态随之失效。新连接必须重新完成服务发现、`SERVER_HELLO` 校验和 `AUTH`。 ## 端点 | 字段 | 值 | | --- | --- | | Host | `127.0.0.1` | | 端口范围 | `49152` 到 `65535` | | Path | `/v1/third-party` | | 协议版本 | `1.0.0` | LIVE Studio 会从 `49152` 向上探测并绑定第一个空闲端口,重启后实际端口可能变化。请有界并发扫描候选 URL,直到收到合法的 `SERVER_HELLO`;不要写死端口,也不要一次打开整个范围。 ## 接入规则 客户端只有在以下条件全部满足时才能接收事件: - 当前 LIVE Studio 环境已开启本地数据开放。 - 发放的 `app_id`、`key_id` 和 `secret` 有效。 - 该应用已开通对应事件。 - 连接数未超过总上限和单应用上限。 如果之后接入被关闭、凭证被撤销或限额发生变化,服务端可能发送 `reason_code=510` 的 `DISCONNECT`,然后关闭 socket。 ## 协议消息 | 消息 | 方向 | 出现时机 | | --- | --- | --- | | `SERVER_HELLO` | 服务端到客户端 | WebSocket 打开后立即发送 | | `AUTH` | 客户端到服务端 | 客户端第一条 JSON 消息 | | `AUTH_RESULT` | 服务端到客户端 | 鉴权成功或失败后 | | `EVENT` | 服务端到客户端 | 鉴权成功之后 | | `DISCONNECT` | 服务端到客户端 | 主动关闭前(如适用) | 字段细节见 [连接生命周期](https://tiktok-live-studio-local-data-access.pages.dev/zh/protocol/connection) 和 [鉴权](https://tiktok-live-studio-local-data-access.pages.dev/zh/protocol/auth)。
--- https://tiktok-live-studio-local-data-access.pages.dev/zh/protocol/connection docs/zh/protocol/connection.md # 连接生命周期 第三方客户端通过本机 WebSocket 端点连接 LIVE Studio。 ## 端点 | 字段 | 值 | | --- | --- | | Host | `127.0.0.1` | | 端口范围 | `49152` 到 `65535` | | Path | `/v1/third-party` | | 协议版本 | `1.0.0` | 完整 URL: ```txt ws://127.0.0.1:{port}/v1/third-party ``` ## 服务发现流程 ```mermaid sequenceDiagram participant C as 客户端 participant B as 候选端点批次 loop 49152 到 65535,分批扫描 C->>B: 小批量并发打开 WebSocket B-->>C: 失败、超时或返回首条消息 C->>C: 校验每个 SERVER_HELLO alt 找到合法 hello C->>B: 关闭其他候选 Socket C->>C: 保留已验证连接并停止 else 本批没有合法 hello C->>C: 扫描下一批 end end ``` LIVE Studio 会从低到高探测该范围,并绑定第一个空闲端口。实际端口可能在重启后变化,客户端必须动态发现,不能写死或长期缓存。 该范围包含 16384 个候选端口。应小批量、有上限地并发扫描,选中合法端点后关闭其他候选连接;发送凭证前必须先校验 `SERVER_HELLO`。没有返回合法 hello 的端口应视为无关服务。不要一次打开整个端口范围。 ## 状态机 ```mermaid stateDiagram-v2 [*] --> Scanning Scanning --> WaitingHello: WebSocket 打开 WaitingHello --> Scanning: 失败 / 超时 / hello 不合法 WaitingHello --> Authenticating: hello 合法,发送 AUTH Authenticating --> Authenticated: AUTH_RESULT success=true Authenticating --> Closed: 鉴权失败或超时 Authenticated --> Authenticated: 接收 EVENT Authenticated --> Closed: 断开 / 关闭 / 传输错误 Closed --> [*] ``` ## 超时与心跳 | 配置 | 值 | | --- | --- | | 鉴权超时 | 20 秒 | | WebSocket ping 间隔 | 30 秒 | 协议使用原生 WebSocket ping/pong,不定义 JSON `HEARTBEAT` 消息。 ## 重连 连接关闭后,鉴权状态随之失效。新的 WebSocket 必须重新完成: 1. 端口发现 2. `SERVER_HELLO` 校验 3. `AUTH` 4. `AUTH_RESULT` 处理 请使用重连退避,避免在策略或网络故障期间打满连接上限。 --- https://tiktok-live-studio-local-data-access.pages.dev/zh/protocol/auth docs/zh/protocol/auth.md # 鉴权 第三方客户端必须先完成鉴权,才能接收事件。 ## 消息顺序 ```mermaid sequenceDiagram participant C as 客户端 participant S as LIVE Studio C->>S: WebSocket 连接 S-->>C: SERVER_HELLO C->>C: 校验 hello C->>S: AUTH alt 通过 S-->>C: AUTH_RESULT success=true S-->>C: EVENT... else 拒绝 S-->>C: AUTH_RESULT success=false S-->>C: 关闭 end ``` 校验 `SERVER_HELLO` 后,客户端发送的第一条 JSON 消息必须是 `AUTH`。 ## `SERVER_HELLO` 方向:服务端到客户端。 WebSocket 连接建立后立即发送。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `type` | string | 是 | 固定为 `SERVER_HELLO` | | `product` | string | 是 | 固定为 `tiktok_live_studio` | | `channel` | string | 是 | 固定为 `third-party-im` | | `version` | string | 是 | 服务端协议版本 | ```json { "type": "SERVER_HELLO", "product": "tiktok_live_studio", "channel": "third-party-im", "version": "1.0.0" } ``` ## `AUTH` 方向:客户端到服务端。 必须是客户端发出的第一条 JSON 消息。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `type` | string | 是 | 固定为 `AUTH` | | `app_id` | string | 是 | 发放的应用标识 | | `key_id` | string | 是 | 发放的密钥版本标识 | | `secret` | string | 是 | 原始应用 secret | | `version` | string | 否 | 客户端协议版本 | ```json { "type": "AUTH", "app_id": "your_app_id", "key_id": "your_key_id", "secret": "your_secret", "version": "1.0.0" } ``` ::: warning 发送原始 `secret`。不要在客户端侧做哈希或变换。 ::: ## `AUTH_RESULT` 方向:服务端到客户端。 鉴权成功或失败后发送。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `type` | string | 是 | 固定为 `AUTH_RESULT` | | `success` | boolean | 是 | 鉴权是否成功 | | `app_id` | string | 是 | 从请求解析出的应用标识 | | `app_name` | string | 否 | 应用展示名称(如有) | | `message` | string | 是 | 可读结果说明 | | `server_time` | number | 是 | Unix 时间戳,毫秒 | | `version` | string | 是 | 服务端协议版本 | | `error_code` | string | 否 | 鉴权失败时出现 | 成功: ```json { "type": "AUTH_RESULT", "success": true, "app_id": "your_app_id", "app_name": "Example App", "message": "Authentication successful", "server_time": 1786010400000, "version": "1.0.0" } ``` 失败: ```json { "type": "AUTH_RESULT", "success": false, "app_id": "your_app_id", "message": "Invalid credentials", "server_time": 1786010400000, "version": "1.0.0", "error_code": "INVALID_CREDENTIALS" } ``` ## 策略检查 鉴权只有在以下检查全部通过时才会成功: - 本地数据开放已开启。 - `app_id` 存在于白名单中。 - `key_id` 与配置的密钥匹配。 - `secret` 与发放的凭证匹配。 - 总连接数低于策略上限。 - 单应用连接数低于策略上限。 --- https://tiktok-live-studio-local-data-access.pages.dev/zh/protocol/events docs/zh/protocol/events.md # 事件信封 鉴权成功后,LIVE Studio 使用 `EVENT` 信封发送公开直播间事件。 ## 信封结构 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `type` | string | 是 | 固定为 `EVENT` | | `event` | string | 是 | 公开事件名 | | `timestamp` | number | 是 | Unix 时间戳,毫秒 | | `payload` | object | 是 | 事件专属 payload | ```json { "type": "EVENT", "event": "live.like", "timestamp": 1786010400000, "payload": { "message_id": "7520000000000000001" } } ``` ## 公开用户对象 部分事件 payload 会包含 `user` 对象。该对象本身以及其中所有字段都应视为可选。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `id` | string | 否 | 用户标识 | | `nickname` | string | 否 | 展示昵称 | | `display_id` | string | 否 | 公开展示 ID | | `avatar_url` | string | 否 | 头像 URL | 示例: ```json { "id": "7300000000000000000", "nickname": "Viewer", "display_id": "viewer_01", "avatar_url": "https://example.invalid/avatar.png" } ``` ## 去重 payload 包含 `message_id` 时,用它作为主去重键。 推荐键: ```txt {event}:{message_id} ``` ## 可选字段 客户端不要假设每个字段都存在。LIVE Studio 可能省略不可用字段,或给出空字符串、`0` 等安全回退值。 推荐处理方式: - 将 `user` 视为可选。 - 将未知事件名视为不支持并忽略。 - 容忍未知枚举值。 - 不要因为某个事件 payload 缺少可选字段就断开整条连接。 --- https://tiktok-live-studio-local-data-access.pages.dev/zh/protocol/errors docs/zh/protocol/errors.md # 错误码与断开 本页列出鉴权错误和应用层断开原因。 ## 鉴权错误 鉴权失败会写在 `AUTH_RESULT.error_code` 中。 | `error_code` | 含义 | 常见原因 | | --- | --- | --- | | `INVALID_FORMAT` | 鉴权消息格式无效 | JSON 非法、缺少 `type`、客户端第一条消息不是 `AUTH`,或缺少 `app_id`、`key_id`、`secret` | | `ACCESS_DISABLED` | 本地数据开放已关闭 | 本地数据开放策略未开启 | | `INVALID_CREDENTIALS` | 凭证不匹配 | 应用不在白名单、`key_id` 错误,或 `secret` 不匹配 | | `CONNECTION_LIMIT` | 达到连接上限 | 总连接数或单应用配额超限 | | `AUTH_TIMEOUT` | 鉴权超时 | 客户端未在 20 秒内发送合法 `AUTH` | | `SERVER_ERROR` | 服务端内部错误 | 本机网关出现非预期错误 | ## 断开消息 LIVE Studio 主动终止已鉴权连接时,可能在关闭 WebSocket 前发送 `DISCONNECT`。 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `type` | string | 是 | 固定为 `DISCONNECT` | | `app_id` | string | 是 | 已鉴权的应用标识 | | `reason_code` | number | 是 | 应用层原因码 | | `reason` | string | 是 | 可读原因说明 | | `timestamp` | number | 是 | Unix 时间戳,毫秒 | ```json { "type": "DISCONNECT", "app_id": "your_app_id", "reason_code": 510, "reason": "Application access policy changed", "timestamp": 1786010400000 } ``` ## 断开原因码 | `reason_code` | 名称 | 说明 | | --- | --- | --- | | `100` | `UserInitiated` | 服务端收到主动停止请求 | | `200` | `PeerClosed` | 对端已经关闭 socket | | `201` | `ClientError` | 客户端传输或消息错误 | | `300` | `HeartbeatTimeout` | 服务端未收到 WebSocket pong | | `400` | `AuthFailed` | 鉴权失败 | | `500` | `ServerShutdown` | LIVE Studio 本地事件服务正在停止 | | `510` | `AccessPolicyChanged` | 接入关闭、凭证撤销、白名单变更或配额策略变更 | ## 客户端行为 - 用 `error_code` 做鉴权失败分支。 - 用 `reason_code` 做断开处理。 - 不要把 `message` 或 `reason` 当作唯一的程序信号。 - 新连接必须重新做服务发现和鉴权。 - 连续失败后使用重连退避。 --- https://tiktok-live-studio-local-data-access.pages.dev/zh/events/live-like docs/zh/events/live-like.md # `live.like` `live.like` 表示直播间点赞消息。 没有合法 `message_id` 的事件不会投递。 ## Payload | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `message_id` | string | 是 | 用于去重的唯一消息标识 | | `room_id` | string | 是 | 直播间标识;不可用时为空字符串 | | `count` | string | 是 | 本条消息携带的点赞数;不可用时为空字符串 | | `total` | string | 是 | 累计点赞数;不可用时为空字符串 | | `user` | object | 否 | 公开用户对象 | ## 示例 ```json { "type": "EVENT", "event": "live.like", "timestamp": 1786010400000, "payload": { "message_id": "7520000000000000001", "room_id": "7520000000000000000", "count": "15", "total": "8432", "user": { "id": "7300000000000000000", "nickname": "Viewer", "display_id": "viewer_01", "avatar_url": "https://example.invalid/avatar.png" } } } ``` ## 客户端注意 - 将 `count` 和 `total` 当作字符串处理。 - 用 `message_id` 做去重。 - `user` 对象可能不存在。 --- https://tiktok-live-studio-local-data-access.pages.dev/zh/events/ docs/zh/events/index.md # 选择事件 先决定观众的行为要改变什么。客户端鉴权后,会收到当前应用开放的 IM 消息类型,不需要再发送订阅列表。 | 事件 | 适合的玩法 | 处理时注意 | | --- | --- | --- | | [live.like](https://tiktok-live-studio-local-data-access.pages.dev/zh/events/live-like) | 阵营能量、进度条、共同目标 | `count` 和 `total` 是字符串,运算前先校验。 | | [live.gift](https://tiktok-live-studio-local-data-access.pages.dev/zh/events/live-gift) | 指定礼物触发技能或生成物体 | 按 `gift.id` 匹配,不按名称;明确连击策略。 | | [live.chat](https://tiktok-live-studio-local-data-access.pages.dev/zh/events/live-chat) | 评论选队、指令、观众投票 | `content` 按纯文本处理。 | 三种事件使用同一套[事件信封](https://tiktok-live-studio-local-data-access.pages.dev/zh/protocol/events)。按事件类型与 `message_id` 去重;`user` 对象可能缺失。 ## 先选礼物,再配置效果 到[礼物目录](https://tiktok-live-studio-local-data-access.pages.dev/zh/reference/gift-catalog#%E6%A3%80%E7%B4%A2%E7%A4%BC%E7%89%A9)复制 ID,写入游戏配置。Gateway 负责投递事件,游戏负责决定效果。未知礼物 ID 不应导致断连。 ## 收不到某类事件? 鉴权成功不代表所有消息类型均已开放。确认当前应用配置,并在测试直播间产生一条新的互动。具体检查方法见[排障指南](https://tiktok-live-studio-local-data-access.pages.dev/zh/guide/troubleshooting)。 --- https://tiktok-live-studio-local-data-access.pages.dev/zh/events/live-gift docs/zh/events/live-gift.md # `live.gift` `live.gift` 表示直播间礼物消息。 ## Payload | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `message_id` | string | 是 | 用于去重的唯一消息标识 | | `room_id` | string | 是 | 直播间标识;不可用时为空字符串 | | `gift` | object | 是 | 礼物信息 | | `group_id` | string | 是 | 礼物分组标识;不可用时为空字符串 | | `group_count` | string | 是 | 礼物分组数量;不可用时为空字符串 | | `repeat_count` | string | 是 | 连击次数;不可用时为空字符串 | | `combo_count` | string | 是 | combo 次数;不可用时为空字符串 | | `repeat_end` | boolean | 是 | 连击或 combo 序列是否已结束 | | `user` | object | 否 | 公开用户对象 | ## Gift 对象 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `id` | string | 是 | 礼物标识 | | `name` | string | 是 | 礼物名称;不可用时为空字符串 | | `type` | number | 是 | 礼物类型枚举值;不可用时为 `0` | | `diamond_count` | number | 是 | 单个礼物的钻石价值;不可用时为 `0` | | `image_url` | string | 是 | 礼物图片 URL;不可用时为空字符串 | ## 示例 ```json { "type": "EVENT", "event": "live.gift", "timestamp": 1786010400000, "payload": { "message_id": "7520000000000000002", "room_id": "7520000000000000000", "gift": { "id": "5655", "name": "Rose", "type": 1, "diamond_count": 1, "image_url": "https://example.invalid/gift.png" }, "group_id": "group_001", "group_count": "1", "repeat_count": "3", "combo_count": "3", "repeat_end": true, "user": { "id": "7300000000000000000", "nickname": "Viewer", "display_id": "viewer_01", "avatar_url": "https://example.invalid/avatar.png" } } } ``` ## 客户端注意 - 将计数字段当作字符串处理。 - 容忍未知的礼物 `type` 值。 - 需要收尾 combo 展示时,使用 `repeat_end`。 - `user` 对象可能不存在。 ## 选择礼物并触发效果 可以通过可搜索的[礼物目录](https://tiktok-live-studio-local-data-access.pages.dev/zh/reference/gift-catalog)或[机器可读 JSON](https://tiktok-live-studio-local-data-access.pages.dev/data/gifts.json)查找礼物 ID。把“礼物 → 效果”规则放在业务配置中,并使用 `payload.gift.id` 的字符串值进行匹配。 ```ts const effectByGiftId = new Map([ ['5655', 'drop-rose'], ['6064', 'show-gg'], ['7569', 'controller-boost'] ]) function onGift(payload: LiveGiftPayload) { const effect = effectByGiftId.get(String(payload.gift.id)) if (!effect || !payload.repeat_end) return runEffect(effect, payload) } ``` 不要按 `gift.name` 匹配:名称可能被本地化,不同 ID 也可能重名。也不要按 `diamond_count` 匹配,因为价值和可用性可能变化。礼物目录只是编写配置时的快照,收到的实时事件才是运行时最终事实。 即时触发、连击结束触发和逐个礼物触发的差异,见[礼物目录:明确选择触发策略](https://tiktok-live-studio-local-data-access.pages.dev/zh/reference/gift-catalog#%E6%98%8E%E7%A1%AE%E9%80%89%E6%8B%A9%E8%A7%A6%E5%8F%91%E7%AD%96%E7%95%A5)。 --- https://tiktok-live-studio-local-data-access.pages.dev/zh/reference/gift-catalog docs/zh/reference/gift-catalog.md # 礼物目录 你可以先在这里找到礼物 ID,再把这个 ID 配置成应用或游戏里的具体效果。页面支持按名称或 ID 搜索、按钻石价值排序,并可直接复制 ID。 ::: warning 这是一个时间点快照 本目录来自 2026 年 9 月 4 日抓取的一次 CN 礼物面板响应,共提取 608 个唯一 ID,其中 348 个在该响应中标记为面板可见。原始响应明确标记 `is_full_gift_data=false`,因此它不是永久有效的全球礼物白名单。礼物是否可用、名称、价格和图片都可能随地区、直播间、账号、活动与时间变化。 ::: - [机器可读 JSON](https://tiktok-live-studio-local-data-access.pages.dev/data/gifts.json) - [下载 CSV](https://tiktok-live-studio-local-data-access.pages.dev/data/gifts.csv) 英文检索别名覆盖示例中的 **Rose**、**GG** 和 **Game Controller**,其他名称保留抓包原文。译名搜不到时请使用 ID。[下载检索别名](https://tiktok-live-studio-local-data-access.pages.dev/data/gift-aliases.json)。 选好礼物后,用字符串 `gift.id` 配置效果,不按名称或钻石数匹配。连击类动作默认等 `repeat_end=true` 再触发;展示信息以收到的真实事件为准。 ## 检索礼物 [Gift catalog JSON](https://tiktok-live-studio-local-data-access.pages.dev/data/gifts.json) · [Search aliases](https://tiktok-live-studio-local-data-access.pages.dev/data/gift-aliases.json) ## 按 ID 匹配,不要按名称匹配 使用 `payload.gift.id` 作为配置键。本次快照中所有 ID 都唯一,但有 21 组礼物重名;名称还可能随语言变化。不要用 `gift.name`、`diamond_count`、数组顺序或图片 URL 作为程序里的礼物身份。 ```ts const effectByGiftId: Record = { '5655': 'drop-rose', '6064': 'show-gg', '7569': 'activate-controller-boost' } function handleGift(payload: LiveGiftPayload) { const effect = effectByGiftId[String(payload.gift.id)] if (!effect) return // 推荐默认策略:等连击结束后只执行一次。 if (!payload.repeat_end) return runEffect(effect, { count: parsePositiveCount(payload.repeat_count) }) } ``` 建议把映射保存在业务配置中,让策划可以在不修改 WebSocket 客户端的情况下调整“礼物 → 效果”规则。遇到未知礼物 ID 时应忽略或进入明确的默认效果,不能因此断开连接。 ## 明确选择触发策略 `repeat_count`、`combo_count` 和 `group_count` 都是字符串。一次连击可能产生多条累计更新,如果每次都用完整累计值放大效果,就会重复执行。 请明确选择一种策略: - **连击结束触发:** 等待 `repeat_end=true`,校验最终数量后只执行一次。对于昂贵或会改变游戏状态的效果,这是最安全的默认方案。 - **即时更新:** 每个不同的 `message_id` 触发一次轻量视觉反馈,但不要把累计数量当作本次增量。 - **逐个礼物触发:** 按非空 `group_id` 保存上次计数,计算 `max(0, current - previous)`,并在 `repeat_end=true` 时清理状态。 ## 运行时数据才是最终事实 目录用于检索和配置。真实 `live.gift` 到达时,展示层应使用事件里的 `gift.name`、`diamond_count`、`type` 和 `image_url`。客户端还需要保留未知 ID 的通用兜底,因为新礼物或活动礼物可能在不更新客户端的情况下出现。 --- https://tiktok-live-studio-local-data-access.pages.dev/zh/events/live-chat docs/zh/events/live-chat.md # `live.chat` `live.chat` 表示直播间文本评论消息。 ## Payload | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `message_id` | string | 是 | 用于去重的唯一消息标识 | | `room_id` | string | 是 | 直播间标识;不可用时为空字符串 | | `content` | string | 是 | 评论文本内容 | | `user` | object | 否 | 公开用户对象 | ## 示例 ```json { "type": "EVENT", "event": "live.chat", "timestamp": 1786010400000, "payload": { "message_id": "7520000000000000002", "room_id": "7520000000000000000", "content": "hello live", "user": { "id": "7300000000000000000", "nickname": "Viewer", "display_id": "viewer_01", "avatar_url": "https://example.invalid/avatar.png" } } } ``` ## 客户端注意 - 用 `message_id` 做去重。 - `user` 对象可能不存在。 - `content` 是纯文本,不要假设其中包含 markup。 --- https://tiktok-live-studio-local-data-access.pages.dev/zh/guide/agent-skill docs/zh/guide/agent-skill.md # Agent Skill 本仓库提供了一套可复用的开发 Agent Skill,用于在任意应用技术栈中接入 LIVE Studio 本地数据开放能力。 ## 安装到其他项目 最快的分发方式是使用开放的 Skills CLI。它会从当前 GitHub 仓库发现 Skill,并安装到开发者机器上检测到的编码 Agent: ```bash npx skills add liangpeiran-bit/tiktok-live-studio-local-data-access \ --skill tiktok-live-studio-local-data-access \ -g ``` `-g` 表示在开发者的所有项目中可用;去掉 `-g` 则只安装到当前项目。需要无交互地指定 Agent 时: ```bash npx skills add liangpeiran-bit/tiktok-live-studio-local-data-access \ --skill tiktok-live-studio-local-data-access \ -g -a codex -a claude-code -a cursor -y ``` 本仓库更新后,可以更新已安装副本: ```bash npx skills update tiktok-live-studio-local-data-access -g -y ``` 没有 Node.js/npm 的开发者可以下载仓库或 Release 压缩包,再把**整个目录**复制到目标项目: ```text 源目录: .agents/skills/tiktok-live-studio-local-data-access/ Codex、Cursor、OpenCode、Copilot、Gemini CLI 推荐目标: /.agents/skills/tiktok-live-studio-local-data-access/ Claude Code 目标: /.claude/skills/tiktok-live-studio-local-data-access/ Trae 目标: /.trae/skills/tiktok-live-studio-local-data-access/ ``` 不要只复制 `SKILL.md`;协议、事件、技术栈适配和验收参考文件都是流程的一部分。 团队同时使用多种 Agent 产品时,建议把 `.agents/skills/` 作为唯一源。Claude Code 和 Trae 可以通过显式读取提示词,或由团队在本地维护链接/副本;不要在仓库中维护多份可编辑副本,以免内容漂移。 ## 给 Agent 的第一条任务 ```text 阅读 LIVE Studio Local Data Access Skill,把 live.like 接入当前项目。保持现有技术栈;先做模拟测试,不要请求或输出真实密钥。 ``` [LLM index](https://tiktok-live-studio-local-data-access.pages.dev/zh/llms.txt) · [Complete context](https://tiktok-live-studio-local-data-access.pages.dev/zh/llms-full.txt) ## Skill 包含什么 ```txt .agents/skills/tiktok-live-studio-local-data-access/ ├── SKILL.md ├── agents/openai.yaml ├── assets/gifts.json └── references/ ├── protocol.md ├── events.md ├── gift-triggers.md ├── stack-adaptation.md └── verification.md ``` 这套 Skill 以协议为核心,而不是让 Agent 生搬某一种语言的示例。Agent 会先检查开发者项目现有的运行时、依赖、生命周期和并发模型,再把同一套连接流程适配到 JavaScript、TypeScript、Electron、H5、Unity、.NET、Java、Kotlin、Python、Go、Rust、C++、Unreal Engine 或其他兼容的本地运行时。 ## Agent 会掌握什么 Skill 覆盖以下完整流程: - 判断客户端是否与 LIVE Studio 运行在同一台机器; - 扫描端口并严格校验 `SERVER_HELLO`; - 按正确顺序发送 `AUTH`,安全管理凭证; - 处理事件 DTO、可选字段、去重和纯文本安全; - 检索礼物目录、按 ID 配置效果并安全处理连击触发; - 管理长连接、取消、退避重连以及断开原因 `510`; - 适配不同技术栈的生命周期、线程和事件循环; - 使用模拟 WebSocket 服务测试、安全审查,并明确真实联调边界。 Skill 也明确禁止 Agent:把本地服务暴露到远端、在校验服务身份前发送凭证、自创 JSON 心跳、把密钥打进浏览器静态资源,或者为了接入 WebSocket 强制迁移项目技术栈。 ## 在本仓库中使用 支持的 Codex、Cursor 2.4+、OpenCode、GitHub Copilot 和 Gemini CLI 版本可以从 `.agents/skills/` 自动发现项目 Skill。需要时可显式调用: ```text 使用 $tiktok-live-studio-local-data-access,把 live.gift 接入当前项目。 ``` 不同 Agent 的显式调用语法可能不同。下面这条提示词不依赖具体 Agent 产品: ```text 完整阅读 .agents/skills/tiktok-live-studio-local-data-access/SKILL.md 以及它要求的参考文件,然后按照该流程把 LIVE Studio 本地事件接入当前项目,不要改变项目现有技术栈。 ``` 对于不会自动扫描 `.agents/skills/` 的 Agent,也可以使用这条显式读取提示词。 ## 提问示例 ```text 使用 LIVE Studio Local Data Access Skill,把点赞和礼物事件接入这个 Unity 游戏,复用项目已有的 WebSocket 包。 ``` ```text 按照 LIVE Studio Skill 审查这个 Python 客户端,重点检查 hello 校验、鉴权顺序、重连和密钥日志。 ``` ```text 排查这个 Electron 弹幕层为什么能鉴权,但 LIVE Studio 重启后收不到聊天事件。 ``` ## 后续维护 公开文档仍然是协议事实来源。当端点、协议版本、消息字段、事件结构或重连规则变化时: 1. 同步修改中英文文档; 2. 更新 Skill 的 `references/` 中对应文件; 3. 校验 Skill 并构建 VitePress 站点; 4. 将完整 Skill 目录重新分发到已安装副本的项目。 当前 Skill 对应协议版本为 `1.0.0`。 --- https://tiktok-live-studio-local-data-access.pages.dev/zh/guide/troubleshooting docs/zh/guide/troubleshooting.md # 排障 ## 联系支持 申请、配置说明缺失或尚未解决的接入问题,请联系 [TikTok_LIVE_Studio_Office@bytedance.com](mailto:TikTok_LIVE_Studio_Office@bytedance.com)。请提供 LIVE Studio 版本、客户端运行时、最后连接状态和脱敏错误码,不要发送 `secret`、完整 `AUTH` 内容或观众原始数据。申请结果通过邮箱通知,审核时间因申请情况而异。 本地客户端无法发现服务、鉴权失败或收不到事件时,使用本页。 ## 找不到本机服务 现象: - `49152` 到 `65535` 没有候选端点返回合法的 `SERVER_HELLO`。 - WebSocket 始终打不开。 检查: - LIVE Studio 正在运行。 - 客户端使用 `127.0.0.1`,而不是局域网 IP 或远程主机。 - 路径必须精确为 `/v1/third-party`。 - 当前环境已开启本地数据开放。 - 没有防火墙或安全软件拦截回环 WebSocket。 - 扫描器采用有界并发;对整个范围使用长超时串行扫描,看起来会像卡死。 - 客户端没有假定上一次启动使用的端口仍然有效。 ## 已连接但没有合法 `SERVER_HELLO` 现象: - Socket 打开了,但首条消息缺失或不符合协议。 - `product` 或 `channel` 不符合预期。 处理: - 把该端口当作非目标服务。 - 关闭连接。 - 继续扫描下一个端口。 - 不要向该连接发送凭证。 合法的 `SERVER_HELLO`: ```json { "type": "SERVER_HELLO", "product": "tiktok_live_studio", "channel": "third-party-im", "version": "1.0.0" } ``` ## `INVALID_FORMAT` `AUTH` 消息格式无效。 常见原因: - 客户端第一条 JSON 消息不是 `AUTH`。 - 缺少 `app_id`、`key_id` 或 `secret`。 - 必填字段是空字符串。 - payload 不是合法 JSON。 ## `ACCESS_DISABLED` 本地数据开放被策略关闭。 请联系 LIVE Studio 对接负责人,确认当前环境已开启本地数据开放。 ## `INVALID_CREDENTIALS` 提供的凭证与白名单不匹配。 检查: - `app_id` 没有多余空格。 - `key_id` 是当前密钥版本。 - `secret` 是原始明文,而不是哈希。 - 应用仍在白名单中。 ::: danger 不要记录 secret 排查凭证失败时,永远不要打印或上传原始 `secret`。 ::: ## `CONNECTION_LIMIT` 网关拒绝连接,因为达到了全局或单应用连接上限。 处理: - 每个本地客户端只复用一条连接。 - 关闭残留客户端进程。 - 避免无退避的重连循环。 ## `AUTH_TIMEOUT` Socket 打开后 20 秒内没有完成鉴权。 处理: - 校验 `SERVER_HELLO` 后立即发送 `AUTH`。 - 不要再等待其他应用层提示。 - 确保 `AUTH` payload 以 JSON 文本消息发送。 ## 已鉴权但没有事件 检查: - 该事件已在白名单中。 - 直播间正在产生该类型事件。 - 客户端按 `event` 名分发,而不是只按 payload 形状判断。 - 客户端能处理可选字段和空回退值。 ## 收到 `DISCONNECT`,`reason_code=510` `510` 表示接入策略发生了变化。典型原因: - 本地数据开放被关闭。 - 应用凭证被撤销或变更。 - 应用超过了策略限额。 - 事件或应用白名单被更新。 处理: - 停止使用当前连接。 - 刷新凭证,或联系对接负责人。 - 策略纠正后再重新做服务发现和鉴权。 ## 心跳超时 服务端使用原生 WebSocket ping/pong。标准 WebSocket 库通常会自动回复 pong。 除非你自己的应用层需要,否则不要实现自定义 JSON `HEARTBEAT` 消息。它不属于本协议。 --- https://tiktok-live-studio-local-data-access.pages.dev/zh/samples/javascript docs/zh/samples/javascript.md # JavaScript / TypeScript 示例 本示例展示核心客户端流程: 1. 扫描端口。 2. 校验 `SERVER_HELLO`。 3. 发送 `AUTH`。 4. 分发 `EVENT` 消息。 ## 最小客户端 ```ts type ServerHello = { type: 'SERVER_HELLO' product: 'tiktok_live_studio' channel: 'third-party-im' version: string } type AuthResult = { type: 'AUTH_RESULT' success: boolean app_id: string app_name?: string message: string server_time: number version: string error_code?: string } type EventEnvelope = { type: 'EVENT' event: string timestamp: number payload: Record } type Credentials = { appId: string keyId: string secret: string } const PORT_START = 49152 const PORT_END = 65535 const SCAN_BATCH_SIZE = 128 const HELLO_TIMEOUT_MS = 500 const PATH = '/v1/third-party' function isValidHello(value: unknown): value is ServerHello { const hello = value as Partial return ( hello.type === 'SERVER_HELLO' && hello.product === 'tiktok_live_studio' && hello.channel === 'third-party-im' && hello.version === '1.0.0' ) } function waitForFirstMessage(socket: WebSocket, timeoutMs = HELLO_TIMEOUT_MS): Promise { return new Promise((resolve, reject) => { let timer: ReturnType const cleanup = () => { clearTimeout(timer) socket.removeEventListener('message', onMessage) socket.removeEventListener('error', onError) } const onMessage = (event: MessageEvent) => { cleanup() try { resolve(JSON.parse(String(event.data))) } catch (error) { reject(error) } } const onError = () => { cleanup() reject(new Error('WebSocket error')) } timer = setTimeout(() => { cleanup() reject(new Error('SERVER_HELLO timeout')) }, timeoutMs) socket.addEventListener('message', onMessage, { once: true }) socket.addEventListener('error', onError, { once: true }) }) } async function connectCandidate(port: number): Promise { const socket = new WebSocket(`ws://127.0.0.1:${port}${PATH}`) try { const firstMessage = await waitForFirstMessage(socket) if (!isValidHello(firstMessage)) { socket.close() return null } return socket } catch { socket.close() return null } } async function scanBatch(startPort: number): Promise { const endPort = Math.min(startPort + SCAN_BATCH_SIZE - 1, PORT_END) const candidates = await Promise.all( Array.from({ length: endPort - startPort + 1 }, (_, index) => connectCandidate(startPort + index)) ) const winner = candidates.find((socket): socket is WebSocket => socket !== null) ?? null for (const socket of candidates) { if (socket && socket !== winner) socket.close() } return winner } export async function connectLiveStudio(credentials: Credentials): Promise { for (let batchStart = PORT_START; batchStart <= PORT_END; batchStart += SCAN_BATCH_SIZE) { const socket = await scanBatch(batchStart) if (!socket) continue socket.send( JSON.stringify({ type: 'AUTH', app_id: credentials.appId, key_id: credentials.keyId, secret: credentials.secret, version: '1.0.0' }) ) return socket } throw new Error('LIVE Studio Local Data Access endpoint not found') } ``` ## 分发消息 ```ts const socket = await connectLiveStudio({ appId: 'your_app_id', keyId: 'your_key_id', secret: 'your_secret' }) socket.addEventListener('message', (event) => { const message = JSON.parse(String(event.data)) as AuthResult | EventEnvelope if (message.type === 'AUTH_RESULT') { if (!message.success) { console.error('Authentication failed:', message.error_code, message.message) socket.close() } return } if (message.type === 'EVENT') { switch (message.event) { case 'live.like': console.log('like', message.payload) break case 'live.gift': console.log('gift', message.payload) break case 'live.chat': console.log('chat', message.payload) break default: console.warn('Unsupported event:', message.event) } } }) ``` ## 把礼物 ID 映射为效果 先在[礼物目录](https://tiktok-live-studio-local-data-access.pages.dev/zh/reference/gift-catalog)中选择 ID,再把映射放进业务配置,而不是写进 WebSocket 客户端。下面使用更安全的“连击结束触发”策略。 ```ts type LiveGiftPayload = { message_id: string gift: { id: string; name: string; diamond_count: number } repeat_count: string repeat_end: boolean } const giftEffects: Record void> = { '5655': (count) => dropRoses(count), '6064': () => showBanner('GG!'), '7569': () => activateControllerBoost() } function parsePositiveCount(value: string): number { const count = Number(value) return Number.isSafeInteger(count) && count > 0 ? count : 1 } function handleGift(payload: LiveGiftPayload) { const effect = giftEffects[String(payload.gift.id)] if (!effect || !payload.repeat_end) return effect(parsePositiveCount(payload.repeat_count)) } ``` 不要按名称或价格配置规则。为未知 ID 保留忽略或通用兜底,并在调用处理器前用 `message_id` 去重。 ## 生产注意 - 增加重连退避。 - 存在 `message_id` 时,用 `{event}:{payload.message_id}` 去重。 - 永远不要记录原始 `secret`。 - 将 `user` 及其内部字段全部视为可选。 --- https://tiktok-live-studio-local-data-access.pages.dev/zh/samples/unity docs/zh/samples/unity.md # Unity 接入指南 本页提供结构建议和伪代码,不是可直接下载运行的 Unity 项目。建议先用[本地 H5 示例](https://tiktok-live-studio-local-data-access.pages.dev/zh/samples/h5)验证第一条事件,再复用游戏现有的 WebSocket 库接入。 Unity 客户端应遵循与 JavaScript 客户端相同的协议流程: 1. 有界并发扫描 `127.0.0.1:49152-65535`。 2. 连接到 `/v1/third-party`。 3. 校验 `SERVER_HELLO`。 4. 发送 `AUTH`。 5. 等待 `AUTH_RESULT.success=true` 后,再按事件名分发 `EVENT` 消息。 ## 推荐结构 ```txt LiveStudioGatewayClient ├─ PortScanner ├─ WebSocketTransport ├─ Authenticator ├─ EventDispatcher └─ ReconnectPolicy ``` ## 伪代码 ```csharp const int PortStart = 49152; const int PortEnd = 65535; const int ScanBatchSize = 128; for (var batchStart = PortStart; batchStart <= PortEnd; batchStart += ScanBatchSize) { var batchEnd = Math.Min(batchStart + ScanBatchSize - 1, PortEnd); var attempts = Enumerable.Range(batchStart, batchEnd - batchStart + 1) .Select(port => ConnectAndValidateHello( $"ws://127.0.0.1:{port}/v1/third-party", timeoutMs: 500)); var candidates = await Task.WhenAll(attempts); var socket = candidates.FirstOrDefault(candidate => candidate != null); if (socket == null) continue; foreach (var candidate in candidates.Where(candidate => candidate != null && candidate != socket)) await candidate.Close(); await socket.SendJson(new { type = "AUTH", app_id = appId, key_id = keyId, secret = secret, version = "1.0.0" }); StartReceiveLoop(socket); break; } ``` `ConnectAndValidateHello` 只有在首条消息是当前客户端支持的 `SERVER_HELLO` 时才返回 Socket;连接失败、超时、消息格式错误或版本不兼容时都应主动关闭。并发数量必须有上限,不能一次打开全部 16384 个候选端口。 ## 事件分发 ```csharp void HandleEvent(EventEnvelope envelope) { switch (envelope.Event) { case "live.like": HandleLike(envelope.Payload); break; case "live.gift": HandleGift(envelope.Payload); break; case "live.chat": HandleChat(envelope.Payload); break; } } ``` ## 把礼物映射为玩法效果 先在[礼物目录](https://tiktok-live-studio-local-data-access.pages.dev/zh/reference/gift-catalog)中选择 ID,并在配置中以字符串保存。解析规则后,再把真正的玩法操作派发到 Unity 主线程。 ```csharp readonly Dictionary> giftEffects = new() { ["5655"] = count => SpawnRoses(count), ["6064"] = _ => ShowBanner("GG!"), ["7569"] = _ => ActivateControllerBoost(), }; void HandleGift(LiveGiftPayload payload) { if (!giftEffects.TryGetValue(payload.Gift.Id, out var effect)) return; if (!payload.RepeatEnd) return; var count = int.TryParse(payload.RepeatCount, out var parsed) && parsed > 0 ? parsed : 1; mainThreadQueue.Enqueue(() => effect(count)); } ``` 礼物目录只是编写配置时的快照。遇到未知 ID 时应忽略或进入通用兜底,不能中断接收循环。 ## Unity 注意 - 把玩法侧变更派发回 Unity 主线程。 - 每个应用只保持一条活跃的网关连接。 - 失败后不要每一帧都重连。 - 不要把原始 `secret` 存进场景、prefab 或日志。 - 驱动玩法前,用 `message_id` 做事件去重。 --- https://tiktok-live-studio-local-data-access.pages.dev/zh/samples/h5 docs/zh/samples/h5.md # H5:可运行示例 一个用来验证凭证、接收第一条事件的本地页面。使用浏览器原生 WebSocket,无需安装 npm 依赖。 ## 下载并运行 先确认[接入前提](https://tiktok-live-studio-local-data-access.pages.dev/zh/guide/quick-start#_1-%E7%A1%AE%E8%AE%A4%E6%8E%A5%E5%85%A5%E8%B5%84%E6%A0%BC)。准备 Node.js 22+,在尚不存在 `live-studio-starter` 子目录的位置,用 **PowerShell** 执行: ```powershell New-Item -ItemType Directory live-studio-starter Set-Location live-studio-starter $starterBase = 'https://tiktok-live-studio-local-data-access.pages.dev/samples' 'local-overlay.html', 'local-client.mjs', 'preview.mjs' | ForEach-Object { Invoke-WebRequest "$starterBase/$_" -OutFile $_ } node preview.mjs ``` 在 LIVE Studio 所在机器的浏览器中打开 **http://127.0.0.1:4178**,运行时输入 `app_id`、`key_id`、`secret`,点击 **Connect**。如果浏览器提示本地网络权限,仅为这个受信任的本地预览允许访问,不要关闭浏览器安全机制。 预期状态为 `SCANNING → AUTHENTICATING → AUTHENTICATED`。产生一条已开放类型的新互动后,日志显示 `EVENT live.like`、`EVENT live.gift` 或 `EVENT live.chat`。示例不会自动生成假事件。 点击 **Disconnect / cancel** 停止连接;在 PowerShell 中按 **Ctrl+C** 关闭预览服务。`4178` 仅用于访问开发页面,**不是** Gateway 端口。 ## 三个文件分别做什么 | 文件 | 职责 | | --- | --- | | [local-overlay.html](https://tiktok-live-studio-local-data-access.pages.dev/samples/local-overlay.html) | 运行时凭证输入、单连接管理、事件日志、示例礼物规则。 | | [local-client.mjs](https://tiktok-live-studio-local-data-access.pages.dev/samples/local-client.mjs) | 有界端口发现、hello 校验、等待鉴权结果、取消、有限缓存去重。 | | [preview.mjs](https://tiktok-live-studio-local-data-access.pages.dev/samples/preview.mjs) | 只在 loopback 提供示例静态文件,不接收凭证。 | 也可以手动下载这三个文件,放进同一目录。填写凭证前先检查源码。公开页面用于下载,**请在本地运行**,不要在公开文档域名中运行带凭证的客户端。 ## 修改礼物效果 页面把 `5655`、`6064`、`7569` 映射到三个示例动作名称。等 `repeat_end=true` 后打印一次动作,不会按每次累计计数重复放大效果。可以从[礼物目录](https://tiktok-live-studio-local-data-access.pages.dev/zh/reference/gift-catalog#%E6%A3%80%E7%B4%A2%E7%A4%BC%E7%89%A9)选择 ID,再替换成自己的游戏处理函数。 日志只显示事件类型和示例动作名称,不输出观众消息或凭证。若要展示评论,请按纯文本渲染。 ## 正式使用前 这是首条事件验证工具,不是生产 SDK。示例刻意采用**手动重连**,每次重新点击都会发现服务并鉴权,重复点击不会建立多条连接。正式客户端还需要可取消的退避重连、严格的 payload 校验,以及业务连击策略。去重缓存最多保留 500 个键、窗口为 60 秒,不保证“恰好一次”投递。 收到撤销接入通知(`DISCONNECT.reason_code=510`)时,先联系支持确认资格,再重连。详见[错误码与断开](https://tiktok-live-studio-local-data-access.pages.dev/zh/protocol/errors)和[连接生命周期](https://tiktok-live-studio-local-data-access.pages.dev/zh/protocol/connection)。 凭证只在内存中使用,不会在 hello 校验前发送,不写入浏览器存储,也不提交给预览服务。不要把密钥放进公开静态资源,也不要通过远程代理暴露本地连接。 --- https://tiktok-live-studio-local-data-access.pages.dev/samples/local-client.mjs docs/public/samples/local-client.mjs ```javascript // Local development starter, protocol 1.0.0. No automatic retry or secret storage. export async function connectLocal({ credentials, onStatus = () => {}, onEvent = () => {}, signal, WebSocketImpl = globalThis.WebSocket, portStart = 49152, portEnd = 65535, batchSize = 32, helloTimeout = 500, authTimeout = 10000 }) { if (!credentials?.app_id || !credentials?.key_id || !credentials?.secret) throw new Error('MISSING_CREDENTIALS') if (!Number.isInteger(portStart) || !Number.isInteger(portEnd) || portStart < 49152 || portEnd > 65535 || portStart > portEnd || !Number.isInteger(batchSize) || batchSize < 1 || batchSize > 128) throw new Error('INVALID_SCAN_RANGE') const cancelled = () => signal?.aborted const close = socket => { if (socket.readyState < 2) socket.close() } const parse = data => { try { return JSON.parse(data) } catch { return null } } function candidate(port) { if (cancelled()) return Promise.resolve(null) return new Promise(resolve => { let socket try { socket = new WebSocketImpl(`ws://127.0.0.1:${port}/v1/third-party`) } catch { resolve(null); return } let settled = false const finish = valid => { if (settled) return settled = true clearTimeout(timer) signal?.removeEventListener('abort', abort) socket.removeEventListener('message', hello) socket.removeEventListener('close', abort) if (!valid) close(socket) resolve(valid ? socket : null) } const abort = () => finish(false) const hello = event => { const message = parse(event.data) finish(message?.type === 'SERVER_HELLO' && message.product === 'tiktok_live_studio' && message.channel === 'third-party-im' && message.version === '1.0.0') } const timer = setTimeout(abort, helloTimeout) socket.addEventListener('message', hello) socket.addEventListener('error', abort, { once: true }) socket.addEventListener('close', abort, { once: true }) signal?.addEventListener('abort', abort, { once: true }) if (cancelled()) abort() }) } onStatus('SCANNING') let socket = null for (let start = portStart; start <= portEnd && !socket && !cancelled(); start += batchSize) { const candidates = await Promise.all(Array.from({ length: Math.min(batchSize, portEnd - start + 1) }, (_, i) => candidate(start + i))) socket = candidates.find(item => item?.readyState === 1) ?? null for (const item of candidates) if (item && (item !== socket || cancelled())) close(item) } if (cancelled()) throw new Error('CANCELLED') if (!socket) throw new Error('ENDPOINT_NOT_FOUND') onStatus('AUTHENTICATING') return new Promise((resolve, reject) => { let authenticated = false let stopped = false const seen = new Map() const finish = status => { if (stopped) return stopped = true clearTimeout(timer) signal?.removeEventListener('abort', abort) socket.removeEventListener('message', receive) socket.removeEventListener('close', disconnected) socket.removeEventListener('error', failed) seen.clear() close(socket) onStatus(status) if (!authenticated) reject(new Error(status)) } const abort = () => finish('CANCELLED') const disconnected = () => finish('DISCONNECTED') const failed = () => finish('TRANSPORT_ERROR') const receive = event => { const message = parse(event.data) if (!message || typeof message !== 'object') return if (message.type === 'DISCONNECT') { finish(message.reason_code === 510 ? 'POLICY_DISCONNECT_510' : 'DISCONNECTED'); return } if (!authenticated) { if (message.type !== 'AUTH_RESULT') return if (message.success !== true) { const known = ['INVALID_FORMAT', 'ACCESS_DISABLED', 'INVALID_CREDENTIALS', 'CONNECTION_LIMIT', 'AUTH_TIMEOUT', 'SERVER_ERROR'] finish(known.includes(message.error_code) ? message.error_code : 'AUTH_FAILED') return } authenticated = true clearTimeout(timer) onStatus('AUTHENTICATED') resolve({ close: () => finish('DISCONNECTED') }) return } if (message.type !== 'EVENT' || !['live.like', 'live.gift', 'live.chat'].includes(message.event)) return const payload = message.payload if (!payload || typeof payload !== 'object' || typeof payload.message_id !== 'string' || !payload.message_id) return const key = `${message.event}:${payload.message_id}` const now = Date.now() if (seen.has(key) && now - seen.get(key) < 60000) return seen.delete(key) seen.set(key, now) if (seen.size > 500) seen.delete(seen.keys().next().value) try { onEvent(message) } catch { onStatus('EVENT_HANDLER_ERROR') } } const timer = setTimeout(() => finish('AUTH_TIMEOUT'), authTimeout) signal?.addEventListener('abort', abort, { once: true }) socket.addEventListener('message', receive) socket.addEventListener('close', disconnected) socket.addEventListener('error', failed) if (cancelled()) { abort(); return } try { socket.send(JSON.stringify({ type: 'AUTH', app_id: credentials.app_id, key_id: credentials.key_id, secret: credentials.secret, version: '1.0.0' })) } catch { failed() } // This function does not persist credentials or log server-supplied error text. credentials = null }) } ``` --- https://tiktok-live-studio-local-data-access.pages.dev/samples/local-overlay.html docs/public/samples/local-overlay.html ```html LIVE Studio — first local event

Your first local event

Run on the same machine as an enabled LIVE Studio installation. Credentials stay in memory and are sent only after a matching SERVER_HELLO. This starter never retries automatically.

Not connected


Gift mappings in this sample: 5655 → drop-rose, 6064 → show-gg, 7569 → controller-boost. Replace these with your own rules. Viewer text and credentials are never printed here.

```
--- https://tiktok-live-studio-local-data-access.pages.dev/samples/preview.mjs docs/public/samples/preview.mjs ```javascript // Serve only this starter's two assets, only on loopback. No credential endpoint. import { createServer } from 'node:http' import { readFile } from 'node:fs/promises' const files = { '/': ['local-overlay.html', 'text/html'], '/local-client.mjs': ['local-client.mjs', 'text/javascript'] } const server = createServer(async (request, response) => { const asset = files[request.url] if (request.method !== 'GET' || !Array.isArray(asset)) { response.writeHead(404).end(); return } try { const content = await readFile(new URL(asset[0], import.meta.url)) response.writeHead(200, { 'Content-Type': asset[1] + '; charset=utf-8', 'Cache-Control': 'no-store', 'Content-Security-Policy': "default-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'unsafe-inline'; connect-src ws://127.0.0.1:*; form-action 'none'; frame-ancestors 'none'", 'X-Content-Type-Options': 'nosniff' }) response.end(content) } catch { response.writeHead(500).end('Download local-overlay.html and local-client.mjs into the same folder as preview.mjs.'); } }) server.on('error', () => { console.error('Could not start preview. Check whether 127.0.0.1:4178 is already in use.'); process.exitCode = 1 }) server.listen(4178, '127.0.0.1', () => console.log('Open http://127.0.0.1:4178 — press Ctrl+C to stop.')) ``` --- https://raw.githubusercontent.com/liangpeiran-bit/tiktok-live-studio-local-data-access/main/.agents/skills/tiktok-live-studio-local-data-access/SKILL.md .agents/skills/tiktok-live-studio-local-data-access/SKILL.md # TikTok LIVE Studio Local Data Access Build a safe, protocol-correct local client without forcing the project into a particular language, framework, engine, or WebSocket library. ## Scope Use this skill for software that runs on the same creator machine as TikTok LIVE Studio and needs to consume approved local room events. Do not treat this protocol as a remote API. The service binds to `127.0.0.1`; a cloud server, another computer, or a mobile device cannot connect to it directly. ## Read the references Before implementing or diagnosing a connection, read [references/protocol.md](https://raw.githubusercontent.com/liangpeiran-bit/tiktok-live-studio-local-data-access/main/.agents/skills/tiktok-live-studio-local-data-access/references/protocol.md). Also read the files that match the task: - Event models, dispatch, or deduplication: [references/events.md](https://raw.githubusercontent.com/liangpeiran-bit/tiktok-live-studio-local-data-access/main/.agents/skills/tiktok-live-studio-local-data-access/references/events.md) - Gift catalog lookup or gift-to-effect mapping: [references/gift-triggers.md](https://raw.githubusercontent.com/liangpeiran-bit/tiktok-live-studio-local-data-access/main/.agents/skills/tiktok-live-studio-local-data-access/references/gift-triggers.md) - Language, framework, runtime, or game-engine adaptation: [references/stack-adaptation.md](https://raw.githubusercontent.com/liangpeiran-bit/tiktok-live-studio-local-data-access/main/.agents/skills/tiktok-live-studio-local-data-access/references/stack-adaptation.md) - Tests, review, or completion checks: [references/verification.md](https://raw.githubusercontent.com/liangpeiran-bit/tiktok-live-studio-local-data-access/main/.agents/skills/tiktok-live-studio-local-data-access/references/verification.md) The repository documentation is the public source of truth. If it disagrees with this packaged skill, follow the repository docs and update the skill in the same change. ## Workflow ### 1. Establish feasibility Determine where the client actually runs. - Continue when the WebSocket client runs on the same Windows machine as TikTok LIVE Studio. - For Electron, desktop apps, game engines, local services, and command-line tools, put the connection in a long-lived local runtime. - For H5, continue only when the page runs locally on the creator machine and the browser permits loopback WebSockets. Never embed production credentials in public web assets. - If the proposed client runs remotely, explain the loopback limitation. Do not invent a remote proxy or ask the developer to expose the local endpoint. Confirm which events the application needs and how credentials will be supplied at runtime. Never request that a developer paste a raw secret into source control, chat logs, screenshots, or test fixtures. ### 2. Inspect the existing project Before writing code: 1. Identify the language, runtime, lifecycle model, concurrency model, dependency manager, and test framework. 2. Find an existing WebSocket client or platform API and reuse it when suitable. 3. Locate the project's configuration or secret-storage pattern. 4. Locate existing logging, cancellation, retry, and event-dispatch patterns. 5. Preserve the project's architecture and naming conventions. Do not migrate frameworks or add a large dependency merely to open one WebSocket. Add a library only when the runtime lacks a suitable client and the project owner permits the dependency. ### 3. Design explicit components Keep these responsibilities separable even when the project uses a small single-file implementation: - **Discovery** scans `127.0.0.1:49152-65535` in small, bounded parallel batches and accepts only a verified server hello. - **Transport** owns exactly one active socket, text-message decoding, close handling, and cancellation. - **Authentication** sends `AUTH` only after hello validation and waits for `AUTH_RESULT`. - **Dispatcher** validates the envelope, routes known event names, and ignores unknown events safely. - **Reconnect policy** rediscoveries and reauthenticates with bounded exponential backoff and jitter. - **Application adapter** translates protocol events into domain actions, UI updates, overlay actions, or game-engine main-thread work. Do not couple credentials or socket lifecycle to a UI component that may mount repeatedly. ### 4. Implement the state machine Use explicit states or equivalent guards: ```text idle -> scanning -> waiting_hello -> authenticating -> authenticated | invalid/timeout | rejected v v scanning closed authenticated -> closed -> backoff -> scanning ``` Required behavior: 1. Scan candidate ports in ascending, bounded parallel batches and apply short, cancellable connect/hello timeouts. Never open the entire range at once. 2. Inspect the first server JSON message. 3. Accept the endpoint only when `type`, `product`, `channel`, and supported `version` match the protocol. 4. Send credentials only to that verified endpoint. 5. Send `AUTH` as the first client JSON message. 6. Do not dispatch events until `AUTH_RESULT.success` is `true`. 7. On every new socket, repeat discovery, hello validation, and authentication. 8. Prevent concurrent scan attempts, duplicate sockets, and reconnect timers that survive shutdown. ### 5. Handle events defensively - Dispatch by the envelope's `event` value, not by guessing from payload fields. - Deduplicate payloads with a `message_id` using `{event}:{message_id}`. - Preserve documented string count fields as strings unless the application performs an explicit checked conversion. - Treat `user` and all user fields as optional. - Treat chat `content` as untrusted plain text; escape it before rendering into HTML or rich UI. - Ignore unknown event names and tolerate unknown enum values. - Isolate malformed or unsupported events; one bad payload must not terminate an otherwise healthy connection. - For gift-driven behavior, key rules by the string `gift.id`, not by localized name or diamond value. Read [references/gift-triggers.md](https://raw.githubusercontent.com/liangpeiran-bit/tiktok-live-studio-local-data-access/main/.agents/skills/tiktok-live-studio-local-data-access/references/gift-triggers.md) before implementing combo-sensitive effects. ### 6. Apply security and retry rules - Send the raw issued `secret`; do not hash or transform it. - Keep credentials out of source files, browser bundles, version control, telemetry, exception messages, and normal logs. - Redact `secret` and preferably credential identifiers in diagnostic output. - Never send credentials before validating `SERVER_HELLO`. - Use the library's native WebSocket ping/pong behavior. Do not create a JSON `HEARTBEAT` message. - Reuse one connection per local app/client. - Back off after failures. Do not retry invalid credentials or policy failures in a tight loop. - Treat disconnect reason `510` as requiring credential or policy intervention; stop automatic retry until configuration changes or the user explicitly retries. ### 7. Verify before handing off Read [references/verification.md](https://raw.githubusercontent.com/liangpeiran-bit/tiktok-live-studio-local-data-access/main/.agents/skills/tiktok-live-studio-local-data-access/references/verification.md), then run the checks supported by the project. Prefer deterministic tests with a mock WebSocket server; a live smoke test is optional and must use credentials supplied through a secure runtime mechanism. ## Completion report When implementation is complete, report: - where the long-lived connection is owned; - how credentials are supplied without being committed; - which events are supported; - how reconnect and reason `510` behave; - which build, lint, and tests were run; - any runtime limitation or manual live-test step that remains. Do not claim live connectivity when only mock tests were run. --- https://raw.githubusercontent.com/liangpeiran-bit/tiktok-live-studio-local-data-access/main/.agents/skills/tiktok-live-studio-local-data-access/references/protocol.md .agents/skills/tiktok-live-studio-local-data-access/references/protocol.md # Protocol contract This is the implementation contract for protocol version `1.0.0`. ## Deployment boundary The service is local to the creator machine. | Setting | Value | | --- | --- | | Host | `127.0.0.1` | | Port range | `49152` through `65535`, inclusive | | Path | `/v1/third-party` | | URL | `ws://127.0.0.1:{port}/v1/third-party` | | Protocol version | `1.0.0` | | Authentication timeout | 20 seconds | | Native WebSocket ping interval | 30 seconds | Do not replace `127.0.0.1` with a LAN address, public hostname, or cloud endpoint. Do not expose or proxy this endpoint to a network. ## Discovery and message order LIVE Studio probes this range from low to high and binds the first available port. The selected port is not stable across launches and must not be hard-coded or treated as permanent discovery state. Because the range contains 16,384 candidates, scan it in small, bounded parallel batches. Do not scan one port at a time with a long timeout, and do not open the entire range at once. For each candidate in the current batch: 1. Open the candidate WebSocket with a bounded timeout. 2. Wait for the first server text message with a bounded timeout. 3. Parse it as JSON and validate all `SERVER_HELLO` identity fields. 4. If connection, timeout, parsing, or validation fails, close that socket and continue scanning. 5. When one candidate returns a valid hello, keep that socket, close every losing candidate socket, and stop scanning. 6. Send `AUTH` as the first client JSON message. 7. Wait for `AUTH_RESULT` before accepting events. Never send credentials to a socket that has not produced a valid hello. ## `SERVER_HELLO` Direction: server to client. It is sent immediately after the WebSocket opens. ```json { "type": "SERVER_HELLO", "product": "tiktok_live_studio", "channel": "third-party-im", "version": "1.0.0" } ``` Validate exact values for `type`, `product`, and `channel`. For `version`, accept only versions the client explicitly supports. Do not silently assume an unknown version is compatible. ## `AUTH` Direction: client to server. It must be the first client JSON message. ```json { "type": "AUTH", "app_id": "your_app_id", "key_id": "your_key_id", "secret": "your_raw_secret", "version": "1.0.0" } ``` | Field | Type | Required | Notes | | --- | --- | --- | --- | | `type` | string | yes | Exactly `AUTH` | | `app_id` | string | yes | Issued application ID | | `key_id` | string | yes | Issued key version ID | | `secret` | string | yes | Raw issued secret; do not hash or transform | | `version` | string | no | Client protocol version | Do not log the serialized `AUTH` message. ## `AUTH_RESULT` Direction: server to client. ```json { "type": "AUTH_RESULT", "success": true, "app_id": "your_app_id", "app_name": "Example App", "message": "Authentication successful", "server_time": 1786010400000, "version": "1.0.0" } ``` | Field | Type | Required | | --- | --- | --- | | `type` | string | yes | | `success` | boolean | yes | | `app_id` | string | yes | | `app_name` | string | no | | `message` | string | yes | | `server_time` | number | yes, Unix milliseconds | | `version` | string | yes | | `error_code` | string | only on relevant failures | Branch program logic on `success` and `error_code`, not on human-readable `message`. Authentication error codes: | Code | Meaning | Retry guidance | | --- | --- | --- | | `INVALID_FORMAT` | Malformed JSON or AUTH contract | Fix the client; do not loop | | `ACCESS_DISABLED` | Local access disabled | Wait for policy/user intervention | | `INVALID_CREDENTIALS` | App, key, or secret mismatch | Refresh credentials; do not loop | | `CONNECTION_LIMIT` | Total or per-app limit reached | Close duplicates; back off | | `AUTH_TIMEOUT` | AUTH not accepted within 20 seconds | Fix ordering/timing; then retry | | `SERVER_ERROR` | Local gateway failure | Back off and retry with a cap | ## `EVENT` Direction: server to client, only after successful authentication. ```json { "type": "EVENT", "event": "live.chat", "timestamp": 1786010400000, "payload": {} } ``` The envelope requires `type`, string `event`, numeric millisecond `timestamp`, and object `payload`. See [events.md](https://raw.githubusercontent.com/liangpeiran-bit/tiktok-live-studio-local-data-access/main/.agents/skills/tiktok-live-studio-local-data-access/references/events.md) for event payloads. ## `DISCONNECT` The server may send this before intentionally closing an authenticated socket. ```json { "type": "DISCONNECT", "app_id": "your_app_id", "reason_code": 510, "reason": "Application access policy changed", "timestamp": 1786010400000 } ``` Required fields are `type`, `app_id`, numeric `reason_code`, human-readable `reason`, and numeric millisecond `timestamp`. Branch program logic on `reason_code`, not `reason`. | Code | Name | Client behavior | | --- | --- | --- | | `100` | `UserInitiated` | Close cleanly | | `200` | `PeerClosed` | Reconnect only if the app still needs data | | `201` | `ClientError` | Inspect the client failure before retrying | | `300` | `HeartbeatTimeout` | Check event-loop stalls; back off and reconnect | | `400` | `AuthFailed` | Use auth failure handling | | `500` | `ServerShutdown` | Back off and rediscover later | | `510` | `AccessPolicyChanged` | Stop automatic retry until credentials or policy change | ## Close, heartbeat, and reconnect - The server uses native WebSocket ping/pong. Standard clients normally answer ping automatically. - There is no protocol-level JSON `HEARTBEAT` message. - A closed socket loses authentication state. - Every reconnect must scan again, validate a new hello, send AUTH, and await AUTH_RESULT. - Use cancellable exponential backoff with jitter and a reasonable cap. - Ensure application shutdown cancels active scans, sockets, and pending reconnect timers. --- https://raw.githubusercontent.com/liangpeiran-bit/tiktok-live-studio-local-data-access/main/.agents/skills/tiktok-live-studio-local-data-access/references/events.md .agents/skills/tiktok-live-studio-local-data-access/references/events.md # Event contracts Events arrive inside the authenticated `EVENT` envelope. ## Common envelope ```json { "type": "EVENT", "event": "live.like", "timestamp": 1786010400000, "payload": {} } ``` Dispatch by `event`. Ignore unknown event names without closing the connection. When `payload.message_id` exists, the recommended deduplication key is: ```text {event}:{message_id} ``` Choose a bounded cache with expiration so a long-running client does not leak memory. ## Public user The `user` object is optional. Every nested field is also optional. | Field | Type | | --- | --- | | `id` | string | | `nickname` | string | | `display_id` | string | | `avatar_url` | string | Do not reject an otherwise valid event because `user` or one of these fields is absent. ## `live.like` | Payload field | Type | Required | Notes | | --- | --- | --- | --- | | `message_id` | string | yes | Deduplication ID; invalid IDs are not delivered | | `room_id` | string | yes | May be empty | | `count` | string | yes | Per-message count; may be empty | | `total` | string | yes | Accumulated count; may be empty | | `user` | object | no | Public user | Keep `count` and `total` as strings at the protocol boundary. If the domain needs numbers, parse them with checked conversion and retain a safe fallback. ## `live.gift` | Payload field | Type | Required | Notes | | --- | --- | --- | --- | | `message_id` | string | yes | Deduplication ID | | `room_id` | string | yes | May be empty | | `gift` | object | yes | Gift data | | `group_id` | string | yes | May be empty | | `group_count` | string | yes | May be empty | | `repeat_count` | string | yes | May be empty | | `combo_count` | string | yes | May be empty | | `repeat_end` | boolean | yes | Combo/repeat completion | | `user` | object | no | Public user | Gift object: | Field | Type | Required | Notes | | --- | --- | --- | --- | | `id` | string | yes | Gift ID | | `name` | string | yes | May be empty | | `type` | number | yes | May be `0`; tolerate unknown enum values | | `diamond_count` | number | yes | Per-gift value; may be `0` | | `image_url` | string | yes | May be empty | All gift count fields documented as strings must remain strings at the protocol boundary. Use `repeat_end` when the application needs to finalize combo presentation. For gift selection and effect dispatch, read [gift-triggers.md](https://raw.githubusercontent.com/liangpeiran-bit/tiktok-live-studio-local-data-access/main/.agents/skills/tiktok-live-studio-local-data-access/references/gift-triggers.md). Match configuration by the string `gift.id`; do not use name, price, image URL, or catalog position as identity. The bundled catalog at `../assets/gifts.json` is an authoring snapshot, while each live event is the runtime source of truth. ## `live.chat` | Payload field | Type | Required | Notes | | --- | --- | --- | --- | | `message_id` | string | yes | Deduplication ID | | `room_id` | string | yes | May be empty | | `content` | string | yes | Untrusted plain text | | `user` | object | no | Public user | Render `content` as text. Escape or sanitize it before inserting it into HTML, rich text, command interpreters, logs, or overlays. ## Compatibility rules - Validate fields needed for the application's action, but do not crash the connection on one malformed event. - Tolerate extra object fields for forward compatibility. - Tolerate unknown enum values. - Keep transport DTOs separate from stricter domain models when the application needs stronger invariants. - Do not invent undocumented event names or payload fields. --- https://raw.githubusercontent.com/liangpeiran-bit/tiktok-live-studio-local-data-access/main/.agents/skills/tiktok-live-studio-local-data-access/references/gift-triggers.md .agents/skills/tiktok-live-studio-local-data-access/references/gift-triggers.md # Gift selection and effect mapping Read this file when a task maps `live.gift` events to application or game effects. The skill ships a curated snapshot at `../assets/gifts.json`. The public source is: ```text https://tiktok-live-studio-local-data-access.pages.dev/data/gifts.json ``` The catalog is a discovery aid, not a permanent global allowlist. Its metadata records region, capture time, `is_full_gift_data`, captured count, displayed count, and limitations. Gift availability and presentation can vary by region, room, account, campaign, and time. ## Identity rules - Key application configuration by the string value of `payload.gift.id`. - Do not key by `gift.name`; names can be duplicated and localized. - Do not key by `diamond_count`, image URL, or catalog order; these are mutable presentation data. - Use the live event values for display and keep a fallback for unknown IDs. Example domain configuration: ```json { "5655": { "effect": "drop-rose", "trigger": "combo-end" }, "6064": { "effect": "show-gg", "trigger": "immediate" }, "7569": { "effect": "controller-boost", "trigger": "combo-end" } } ``` Keep this mapping outside discovery, transport, authentication, and protocol DTOs. Validate configured IDs as strings, but do not reject a running connection when an event contains an unconfigured gift. ## Combo policy `repeat_count`, `combo_count`, and `group_count` are strings and may represent cumulative progress. Choose a policy explicitly: 1. **Combo end**: if `repeat_end` is false, wait; when true, execute once with the final checked count. Prefer this for state-changing effects. 2. **Immediate update**: execute a lightweight update once per deduplicated `message_id`; never interpret a cumulative count as an event delta. 3. **Per unit**: maintain the last checked count per non-empty `group_id`, calculate `max(0, current - previous)`, and remove state at `repeat_end=true`. Checked parsing must reject negative, fractional, non-finite, or unsafe values and use an application-defined fallback. Bound and expire per-group state so malformed or abandoned combos cannot leak memory. ## Verification Test at least: - two same-name gifts with different IDs select different rules; - string and numeric-looking IDs are normalized to the documented string key; - an unknown ID takes the intended ignore or fallback path; - duplicate `message_id` values do not repeat an effect; - intermediate cumulative combo updates do not multiply the final effect; - `repeat_end=true` clears per-group state; - gameplay or UI effects run on the runtime's required main thread; - the static catalog is never treated as runtime authorization or availability. --- https://raw.githubusercontent.com/liangpeiran-bit/tiktok-live-studio-local-data-access/main/.agents/skills/tiktok-live-studio-local-data-access/references/stack-adaptation.md .agents/skills/tiktok-live-studio-local-data-access/references/stack-adaptation.md # Technology-stack adaptation The protocol is stack-independent. Adapt its state machine to the project's existing runtime instead of translating one sample line by line. ## Selection rules 1. Reuse the project's current WebSocket client or platform API when it supports text messages, close/error handling, cancellation, and native ping/pong. 2. Match the existing synchronous, callback, promise, coroutine, task, or async-stream model. 3. Keep socket I/O off UI or game render loops. 4. Marshal domain/UI work onto the framework's required main thread. 5. Make shutdown explicit and idempotent. 6. Use the project's dependency manager and quality commands. Do not introduce a second package manager. 7. Keep gift-to-effect rules in domain configuration keyed by `gift.id`; do not put gift names, prices, or effect execution into the transport layer. 8. Discover the dynamic `49152-65535` endpoint with cancellable, bounded-concurrency batches. Never open the full range at once. ## Browser and H5 overlays - A browser page can connect only when it runs on the creator machine and browser policy permits `ws://127.0.0.1`. - Use the native `WebSocket` API unless the application already has a wrapper. - Browser JavaScript cannot control native ping/pong; the browser handles it. - Never compile real `app_id`, `key_id`, or `secret` into a public/static bundle. - Render chat with `textContent` or the framework's escaped text binding, not raw HTML. - Close the socket and cancel retries when the page or component is disposed. If secure runtime credential delivery is impossible, recommend a trusted local desktop/service host instead of weakening credential handling. ## Node.js and Electron - Prefer the runtime's supported WebSocket implementation or an existing project library. - In Electron, keep the connection in a long-lived main/service process rather than a remounting renderer component. - Expose a narrow, validated IPC event surface to renderers. Never forward the secret. - Tie retry cancellation to application shutdown and prevent multiple windows from creating duplicate connections. ## .NET desktop and Unity - Prefer `ClientWebSocket` when compatible with the target runtime; otherwise reuse the project's established Unity WebSocket package. - Run connect/receive loops in cancellable tasks or coroutines without blocking the game loop. - Marshal Unity object, scene, and UI updates onto Unity's main thread. - Own the connection in a persistent service or object and avoid reconnecting from `Update()`. - Cancel and dispose on application quit, domain reload, or owner teardown. ## Java and Kotlin - Prefer the platform HTTP/WebSocket client or an existing OkHttp-style dependency. - Use the project's executor, coroutine scope, or lifecycle owner. - Serialize state transitions so callbacks cannot create two authenticated sockets. - Cancel callbacks and retry jobs when the owner stops. ## Python - Reuse the project's sync or async WebSocket library; do not mix event-loop models. - In async applications, make scan, receive, and backoff cancellation-aware. - In GUI applications, forward events to the toolkit's main/UI thread. - Avoid logging exception objects that embed request payloads or credentials. ## Go - Reuse the project's WebSocket library and `context.Context` cancellation pattern. - Give one goroutine ownership of connection state or protect transitions explicitly. - Stop read loops and retry timers when the context is canceled. - Bound deduplication storage and avoid goroutine leaks during repeated discovery. ## Rust - Match the project's existing async runtime; do not add a second runtime solely for WebSockets. - Model connection state with enums and use cancellation/drop semantics consistently. - Avoid holding locks while awaiting socket operations or backoff timers. - Keep secret values out of `Debug` output and error contexts. ## C++ and Unreal Engine - Prefer the engine/framework WebSocket module already enabled by the project. - Bind callbacks with lifetime-safe weak references and unbind on shutdown. - Dispatch gameplay and UI work to the game thread. - Keep discovery/reconnect logic out of tick functions and prevent stale callbacks from previous sockets. ## Other stacks For any other runtime, map the same responsibilities: | Protocol concern | Runtime capability needed | | --- | --- | | Port discovery | Cancellable bounded-concurrency batches with short connect/hello deadlines | | Hello validation | Text frame reception, JSON parsing, deadline | | Authentication | Ordered text send after validation | | Event loop | Long-lived receive callback/stream/task | | Native heartbeat | Standards-compliant WebSocket ping/pong | | Reconnect | Cancellation-aware timer with backoff and jitter | | App integration | Safe thread/executor/lifecycle handoff | | Secret handling | Runtime configuration or OS/project secret store | If the runtime cannot meet one of these requirements, surface the limitation before implementation rather than weakening the protocol. --- https://raw.githubusercontent.com/liangpeiran-bit/tiktok-live-studio-local-data-access/main/.agents/skills/tiktok-live-studio-local-data-access/references/verification.md .agents/skills/tiktok-live-studio-local-data-access/references/verification.md # Verification checklist Use the project's existing test framework and commands. A mock WebSocket server should emulate the protocol without using real credentials. ## Automated cases At minimum, verify: - Discovery skips connection failures, timeouts, non-JSON first messages, and invalid hello identity fields. - Credentials are never sent to an endpoint with an invalid hello. - `AUTH` is the first client JSON message after a valid hello. - Events are not delivered to application handlers before `AUTH_RESULT.success=true`. - Each documented auth error reaches the intended retry or intervention branch. - `live.like`, `live.gift`, and `live.chat` preserve documented field types. - Gift rules match by the string `gift.id`; same-name IDs remain distinguishable and unknown IDs take the intended fallback path. - Combo-sensitive effects do not replay a cumulative count on every update, and per-group state is cleared at `repeat_end=true`. - Missing `user` and missing optional user fields are accepted. - Unknown events and unknown gift type values do not close the connection. - Duplicate `{event}:{message_id}` values are suppressed within the cache window. - Chat content is rendered or forwarded as plain text, not executable markup. - A malformed event is isolated without killing the receive loop. - Socket close causes a fresh discovery and authentication cycle. - Repeated failures use backoff rather than a tight loop. - Reason `510` stops automatic retry pending intervention. - Shutdown cancels discovery, receive work, active sockets, and retry timers. - Starting twice does not create two active connections. ## Security review Search the changed files and test snapshots for: - real or placeholder-like production credentials; - logging of AUTH payloads, secrets, or configuration objects containing secrets; - secrets embedded in browser bundles, checked-in config, URLs, query strings, telemetry, or exception text; - credentials sent before exact hello validation; - a remote proxy, non-loopback binding, or advice to expose the service to a network. ## Quality commands Run the narrowest relevant commands first, followed by the project's normal build/type-check/lint/test commands in proportion to the change. Use the repository's required package manager and task runner. Do not claim that a build or test passed unless its command completed successfully. ## Optional live smoke test Run only when TikTok LIVE Studio and approved credentials are available on the same machine. 1. Supply credentials through the project's secure runtime mechanism. 2. Start the client and confirm it finds a valid hello. 3. Confirm successful authentication without printing credentials. 4. Trigger one enabled event and confirm exactly one application action. 5. Stop/restart the local service and confirm rediscovery plus reauthentication. 6. Remove credentials and diagnostic artifacts after the test when appropriate. Record whether testing was mock-only or included a live smoke test.