SyncTube 1.0.0 API 使用详解
SyncTube 1.0.0 无 REST API,程序化对接唯一通道为 WebSocket 消息协议:35 个事件逐域详解(认证/聊天/播放列表/播放控制/管理诊断)附实测回显,外加 5 条 stdin 终端命令实测与 /replay 崩溃缺陷预警。
SyncTube 1.0.0 API 使用详解
目标:讲清 SyncTube 1.0.0 的程序化对接方式——它没有 REST API,唯一程序接口是 WebSocket 消息协议(35 个事件)与服务进程的 5 条 stdin 终端命令。本文每个请求/响应均用 Python websocket-client 在 Ubuntu 22.04(Node.js v20.18.1)实测,回显逐行照抄(敏感值已脱敏)。
一、环境信息
本文所有实测结论来自以下环境,读者复现时保持相近版本即可:
- 操作系统:Ubuntu 22.04(x86_64)
- SyncTube:1.0.0(源码 master 分支,钉住提交
7b0ea58,2026-08-23) - Node.js:v20.18.1(
node -v实测) - 服务端点:
ws://192.168.1.10:4200/(与网页同端口) - 嵌入源:局域网内另一台服务器
192.168.1.10:8080,由 nginx 提供sample.mp4与stream.m3u8(HLS)测试媒体 - 客户端工具:Python 3.10 +
websocket-client(pip3 install websocket-client) - 示例账号:管理员
admin/q1w2e3r4(《Ubuntu 22.04 安装 SyncTube 1.0.0 指南》4.5 节初始化)
python3 -c "import websocket; print(websocket.__version__)"
curl -s -o /dev/null -w '%{http_code}\n' http://192.168.1.10:4200/
实测回显:
1.8.0
200
二、前置条件(端点、认证与客户端工具)
2.1 唯一端点与消息信封
一切交互走 一个 WebSocket 端点:ws://<host>:4200/——与网页同一端口,HTTP 升级协议后复用连接。没有 /api/ 前缀、没有版本化路径、没有认证专用端点,端点清单一句话说完了。
每条消息是一个 JSON 对象,采用信封模式:type 字段为事件名,同名(首字母小写)字段承载载荷:
{"type": "Message", "message": {"clientName": "alice", "text": "你好"}}
协议版本在连接握手的 Connected 推送里给出,本文实测 serverVersion=2。客户端与服务端共用同一套事件定义(源码 src/Types.hx 的 WsEventType 枚举),请求与推送只是方向不同。
💡 技术拓展:
{"type": "X", "x": {...}}这种"类型标签 + 同名词载荷"的信封是消息协议的经典设计(对比 REST 的"路径区分资源"):解析端先读type再派发到同名载荷处理器,加新事件只需扩展枚举不破坏旧消息;代价是每条消息多一层嵌套。同类协议中 MQTT 用主题区分、gRPC 用方法号区分,SyncTube 选择可读性最高的自描述信封——排查问题时抓包直接肉眼可读。
2.2 认证方式(匿名即游客,管理员走口令哈希)
- 不登录:连接后即以游客(Guest)身份在频道内,拥有
permissions.guest配置的权限; - 昵称登录:发送
Login {clientName}即以该昵称成为"注册用户"(group 含User=2); - 管理员登录:用已在
user/users.json注册的名字发送Login {clientName, passHash}。passHash不是明文密码,而是sha256(明文密码 + salt)的十六进制——salt由服务端在Connected推送中给出(每个部署实例固定一个随机盐)。
身份用 group 整数位标志表达,实测取值:
| group | 位组成 | 含义 |
|---|---|---|
| 0 | — | 未登录游客(Guest N) |
| 2 | User | 已登录普通用户 |
| 3 | Banned + User | 被封禁中的用户(实测见 5.5 节) |
| 6 | User + Leader | 取得放映权的用户 |
| 10 | User + Admin | 管理员 |
| 14 | User + Leader + Admin | 取得放映权的管理员(实测见 5.5 节) |
位权重:Banned=1、User=2、Leader=4、Admin=8,按位或叠加。
2.3 客户端工具选择
| 工具 | 适用 | 说明 |
|---|---|---|
Python websocket-client | 本文实测、写机器人 | 同步 API,create_connection 一行连接 |
wscat(npm 全局包) | 手工交互调试 | wscat -c ws://192.168.1.10:4200/ 后逐行粘贴 JSON |
| 浏览器 DevTools | 抓官方前端流量 | Network → WS 面板直接看网页自身的收发 |
安装本文实测工具:
pip3 install websocket-client
实测回显(已装则显示 satisfied):
Requirement already satisfied: websocket-client
三、API 总览(35 个事件清单)
服务端定义的事件共 35 个(src/Types.hx WsEventType 枚举逐一计数),按功能分六域。方向标注:发=客户端可发送,收=服务端推送(多数事件双向):
| 域 | 事件(方向) |
|---|---|
| 连接与在线状态(4) | Connected(收)、Disconnected(收)、UpdateClients(发/收)、Progress(收,缓存进度) |
| 认证(4) | Login(发/收)、PasswordRequest(收)、LoginError(收)、Logout(发/收) |
| 聊天(3) | Message(发/收)、ServerMessage(收)、ClearChat(发/收) |
| 播放列表(11) | AddVideo(发/收)、RemoveVideo(发/收)、SkipVideo(发/收)、SkipVideoStats(收)、PlayItem(发/收)、SetNextItem(发/收)、ToggleItemType(发/收)、UpdatePlaylist(发/收)、ClearPlaylist(发/收)、ShufflePlaylist(发)、TogglePlaylistLock(发/收) |
| 播放控制(9) | VideoLoaded(发/收)、GetTime(发/收)、SetTime(发/收)、Pause(发/收)、Play(发/收)、SetRate(发/收)、Rewind(发/收)、Flashback(发/收)、SetLeader(发/收) |
| 管理与诊断(4) | BanClient(发/收)、KickClient(发/收)、Dump(发/收,仅管理员)、CrashTest(发,仅管理员,触发进程崩溃自检) |
三个要点先行:
- 无分页、无过滤参数:列表类状态(播放列表、聊天历史、在线名单)一律全量快照语义——
UpdatePlaylist返回完整videoList,Connected一次性下发history(上限为配置的serverChatHistory条)与当前clients。数据量由配置参数封顶(totalVideoLimit、serverChatHistory),不存在游标或 offset; - 响应模式有三种:单播回显(如
GetTime只回请求者)、全员广播(如Message、Pause)、除发送者外广播(如SetRate——发送者收不到自己的回显,实测见 5.3 节); - 错误不走 HTTP 状态码:权限不足、参数错误一律以
ServerMessage(结构化textId)或协议级错误事件(LoginError)回给发送者。
四、认证与首次调用(实测)
4.1 连接即初始化:Connected 推送
建立 WebSocket 连接后,服务端无条件先推一条 Connected,这是整场会话的状态快照(实测回显节选,emotes 37 项与 filters 2 项折叠,youtubeApiKey 已脱敏):
{
"type": "Connected",
"connected": {
"uuid": "967ca4a5-a716-40c0-b75f-a86608d5c3d0",
"config": {
"channelName": "我的观影频道",
"isVerbose": false,
"maxLoginLength": 20,
"maxMessageLength": 200,
"port": 4200,
"requestLeaderOnPause": true,
"salt": "0dae25b521e9ded61b0a218fc67d4a3c56cc3d7674339f42b922d7bb650d57b7",
"serverVersion": 2,
"totalVideoLimit": 0,
"unpauseWithoutLeader": false,
"userVideoLimit": 0,
"voteSkipRatio": 0.51,
"youtubeApiKey": "${YOUTUBE_API_KEY}",
"youtubePlaylistLimit": 50,
"permissions": {"...": "四级展开后的权限表,见《SyncTube 1.0.0 配置参数详解》5.7 节"},
"emotes": "<37 项>",
"filters": "<2 项>"
},
"history": [
{"text": "hello from ws probe", "name": "leader1", "time": "2026-10-30 02:08:20"}
],
"isUnknownClient": true,
"clientName": "Guest 2",
"clients": [
{"name": "admin", "group": 14},
{"name": "Guest 2", "group": 0}
],
"videoList": [
{"author": "admin", "duration": 30, "isTemp": false, "playerType": "RawType",
"title": "HLS 测试流", "url": "http://192.168.1.10:8080/stream.m3u8"}
],
"isPlaylistOpen": true,
"itemPos": 0,
"globalIp": "182.54.113.29",
"playersCacheSupport": ["RawType"]
}
}
机器人开发的关键点:连接后的状态初始化全部依赖这条推送——昵称(clientName,未登录时是 Guest N)、在线名单、播放列表、播放进度都从这里拿,不要自己再逐项查询。
4.2 游客登录(Login)
import json, websocket
ws = websocket.create_connection("ws://192.168.1.10:4200/", timeout=6)
ws.settimeout(5)
json.loads(ws.recv()) # Connected
ws.send(json.dumps({"type": "Login", "login": {"clientName": "leader1"}}))
print(json.dumps(json.loads(ws.recv()), ensure_ascii=False, indent=1))
实测回显:
{
"type": "Login",
"login": {
"isUnknownClient": true,
"clientName": "leader1",
"clients": [
{
"name": "leader1",
"group": 2
}
]
}
}
响应的 clients 是全员在线名单(本例频道里只有自己),此后每次进出都有 UpdateClients 广播。
4.3 管理员口令登录(三步实测)
管理员登录的三条路径,完整实测(SALT 取自 4.1 的 Connected.config.salt):
import json, hashlib, websocket
URL, SALT = "ws://192.168.1.10:4200/", "0dae25b521e9ded61b0a218fc67d4a3c56cc3d7674339f42b922d7bb650d57b7"
def fresh_login(payload):
ws = websocket.create_connection(URL, timeout=6); ws.settimeout(5)
json.loads(ws.recv()) # Connected
ws.send(json.dumps({"type": "Login", "login": payload}))
return ws
# a) 管理员名不带密码 -> PasswordRequest
ws = fresh_login({"clientName": "admin"})
print(json.dumps(json.loads(ws.recv()), ensure_ascii=False))
# b) 错误口令 -> passwordMatchError + LoginError
ws = fresh_login({"clientName": "admin", "passHash": "deadbeef" * 8})
print(json.dumps(json.loads(ws.recv()), ensure_ascii=False))
print(json.dumps(json.loads(ws.recv()), ensure_ascii=False))
# c) 正确口令 sha256(密码+salt) -> Login 且 group=10
good = hashlib.sha256(("q1w2e3r4" + SALT).encode()).hexdigest()
ws = fresh_login({"clientName": "admin", "passHash": good})
print(json.dumps(json.loads(ws.recv()), ensure_ascii=False))
实测回显(a → b → c):
{"type": "PasswordRequest"}
{"type": "ServerMessage", "serverMessage": {"textId": "passwordMatchError"}}
{"type": "LoginError"}
{"type": "Login", "login": {"isUnknownClient": true, "clientName": "admin", "clients": [{"name": "Guest 1", "group": 0}, {"name": "Guest 2", "group": 0}, {"name": "admin", "group": 10}]}}
解读:a 路径是两段式登录——服务端先回 PasswordRequest,客户端补发 Login {clientName, passHash} 即可;b 路径口令错时先收到 ServerMessage(passwordMatchError) 再收到协议级 LoginError,客户端两条都要处理;c 路径成功后 group=10(User+Admin)。passHash 的构造公式实测有效:sha256("q1w2e3r4" + salt) 十六进制小写。
4.4 认证相关错误码速查
| 响应 | 触发条件 | 处理建议 |
|---|---|---|
PasswordRequest | 用管理员名登录且未带 passHash | 补发带 passHash 的 Login |
ServerMessage textId=passwordMatchError + LoginError | passHash 与 sha256(密码+salt) 不符 | 检查 salt 是否取自当前实例 |
| `ServerMessage textId=accessError | <action>` | 权限不足(如游客 ClearChat) |
实测游客越权(登录 guest1 后发 ClearChat,管理员缺位):
{"type": "ServerMessage", "serverMessage": {"textId": "accessError|clearChat"}}
五、各 API 域详解
5.1 聊天域(Message / ServerMessage / ClearChat)
发送聊天——Message {clientName, text},服务端全员广播(含发送者):
ws.send(json.dumps({"type": "Message",
"message": {"clientName": "leader1", "text": "hello from ws probe"}}))
实测回显(广播):
{
"message": {
"clientName": "leader1",
"text": "hello from ws probe"
},
"type": "Message"
}
超长消息截断不拒绝:配置 maxMessageLength=200(《SyncTube 1.0.0 配置参数详解》5.1 节)后发送 300 字消息,广播回来的 text 恰为前 200 字。clientName 服务端不校验与登录名一致——广播时原样携带,但历史记录里服务端按连接身份落名,伪造 clientName 不影响实际归属。
ServerMessage 是服务端的系统通知(错误、提示),载荷只有一个 textId:结构化错误(passwordMatchError、accessError|addVideo、totalVideoLimitError 等)或诊断文本(Dump 的附带输出,见 5.5 节)。前端按 textId 查语言包渲染,机器人按 textId 前缀判断错误类型。
ClearChat:清空聊天历史(含所有人的屏幕与 serverChatHistory 缓冲),需 clearChat 权限(默认仅管理员)。实测管理员发送后广播 {"type": "ClearChat"},游客发送则收到 accessError|clearChat(4.4 节回显)。
5.2 播放列表域(AddVideo 等 11 事件)
AddVideo——条目载荷要求最严格的事件,实测踩坑点有两个:
playerType必须是 5 个枚举值的字面量:RawType/YoutubeType/VkType/VimeoType/IframeType。实测传字符串"Raw"(想当然的写法)服务端直接拒收,终端日志与响应均为:
Wrong request for type "AddVideo":
line1: characters 220-225 : Variable 'playerType' should be of type 'PlayerType'
line1: characters 220-225 : Variable 'playerType' should be in the json
- 同 URL 不可重复添加,实测响应:
{
"type": "ServerMessage",
"serverMessage": {
"textId": "videoAlreadyExistsError"
}
}
成功添加的标准请求(直链 mp4 走 RawType,atEnd=true 追加到列表尾)与广播回显:
item = {"url": "http://192.168.1.10:8080/stream.m3u8", "title": "HLS 测试流",
"author": "admin", "duration": 30.0, "isTemp": False,
"doCache": False, "playerType": "RawType"}
ws.send(json.dumps({"type": "AddVideo", "addVideo": {"item": item, "atEnd": True}}))
{
"addVideo": {
"atEnd": true,
"item": {
"author": "admin",
"doCache": false,
"duration": 30,
"isTemp": false,
"playerType": "RawType",
"title": "HLS 测试流",
"url": "http://192.168.1.10:8080/stream.m3u8"
}
},
"type": "AddVideo"
}
字段说明:duration 秒(决定自动切条时机);isTemp=true 为临时条目(播完自动移除);doCache 表示是否走服务器缓存(需缓存依赖,见配置详解 5.6 节);HLS 的 .m3u8 直链按 RawType 添加实测可正常入列。
SkipVideo {url}——对当前正在播放的条目发起跳过投票(需 removeVideo 权限)。所需票数 = ceil(在线人数 × voteSkipRatio),默认 0.51 即过半。实测单人在线(需 1 票)发起后立即生效,服务端先广播 SkipVideo 确认切条、紧随一条 VideoLoaded 通知新条目加载:
{
"skipVideo": {
"url": "http://192.168.1.10:8080/sample.mp4"
},
"type": "SkipVideo"
}
{"type": "VideoLoaded"}
票数不足时广播 SkipVideoStats {voted, total, required} 计数进度。
PlayItem {pos}(需 changeOrder 权限):直接播放指定下标条目。实测回显:
{
"playItem": {
"pos": 0
},
"type": "PlayItem"
}
SetNextItem {pos}:把指定条目挪到"下一个播"的位置。注意实测发现其空转边界——pos 等于当前位置或当前位置 +1 时服务端静默忽略(源码 src/server/Main.hx:1044 的 if (pos == videoList.pos || pos == videoList.pos + 1) return;),机器人重试时不要误判为失败。
ToggleItemType {pos}:切换条目临时/永久态,广播回显 {type: "ToggleItemType", toggleItemType: {pos: 0}};载荷带 all: true 时整表统一切换并以 UpdatePlaylist 全量回。
ShufflePlaylist:随机打乱列表,服务端以 UpdatePlaylist 全量回推(实测回显中可见 isTemp 状态随条目保留):
{
"type": "UpdatePlaylist",
"updatePlaylist": {
"videoList": [
{"author": "leader1", "duration": 30, "isTemp": true, "playerType": "RawType",
"title": "本地测试视频", "url": "http://192.168.1.10:8080/sample.mp4"},
{"author": "admin", "duration": 30, "isTemp": false, "playerType": "RawType",
"title": "HLS 测试流", "url": "http://192.168.1.10:8080/stream.m3u8"}
]
}
}
TogglePlaylistLock(需 lockPlaylist 权限):锁定/解锁播放列表(锁定后游客与普通用户不能增删条目)。实测两次发送的回显:
{"type": "TogglePlaylistLock", "togglePlaylistLock": {"isOpen": false}}
{"type": "TogglePlaylistLock", "togglePlaylistLock": {"isOpen": true}}
RemoveVideo {url} / ClearPlaylist:删除单条 / 清空全表,均为权限内广播(清空会同时停掉播放计时器)。
5.3 播放控制域(SetLeader / GetTime / Pause / Play / SetRate / Rewind 等)
SetLeader {clientName}:放映权交接的核心事件——clientName 写自己即申请 Leader(需 requestLeader 权限),写他人即指派(需 setLeader 权限,默认仅管理员)。实测自任 Leader 的广播回显:
{
"type": "SetLeader",
"setLeader": {
"clientName": "leader1"
}
}
GetTime:进度校准心跳。官方前端周期性发送 {type: "GetTime", getTime: {time: <本地时间>}},服务端单播回权威时钟。实测回显:
{
"type": "GetTime",
"getTime": {
"time": 0,
"paused": true,
"rate": 1.5
}
}
time 为服务端权威进度(秒),paused 为暂停态,rate 仅在倍速 ≠1 时出现(且无 Leader 时服务端自动把倍速重置回 1,源码 src/server/Main.hx:948)。机器人对时即以此值为基准。
Pause {time} / Play {time}:仅 Leader 生效,全员广播(本 Leader 也会收到,便于多端一致):
{"type": "Pause", "pause": {"time": 5}}
{"type": "Play", "play": {"time": 5}}
SetRate {rate}:Leader 调整倍速(0.5~2 之间安全)。实测它是"除发送者外广播"——发送后等待 2 秒无任何回显,其他客户端才会收到 {type: "SetRate", setRate: {rate: 1.5}}。单连接脚本不要等待该事件的回执。
Rewind {time}:快进/回退到绝对秒数(需 rewind 权限),全员广播。实测发送 time: 10 时服务端返回的时间为 10.000814641502075——服务端在目标值上叠加了当前已播进度的零头(时钟平滑),不要假设回显值与请求值相等;负值会被钳制到 0(源码 src/server/Main.hx:978)。
Flashback:一键回退到最近一次大跨度操作(跳条、大 seek)前的状态——服务端在 Pause/SetTime/PlayItem 等操作跨度超过 FLASHBACK_DIST 时自动存档(源码 saveFlashbackTime),Flashback 无载荷,发送即回退。对应聊天命令 /fb 的协议层。
SetTime {time}:Leader 专用,语义与 Rewind 相同(绝对时间定位),区别仅在于 SetTime 不受 rewind 权限约束而走 Leader 身份校验(源码 src/server/Main.hx:953-955),是官方前端进度条拖动使用的事件。
VideoLoaded:客户端加载完当前条目后上报(无载荷 {type: "VideoLoaded"}),服务端借它感知全员就绪并广播。实测切条后服务端也会主动下发该事件(5.2 节 SkipVideo 回显的第二条)。
5.4 连接与状态域(UpdateClients / Disconnected / Progress)
UpdateClients:客户端主动发送 {type: "UpdateClients"} 拉取在线名单,服务端广播回全员(实测回显,admin group=14 为取得放映权的管理员):
{
"type": "UpdateClients",
"updateClients": {
"clients": [
{"name": "admin", "group": 14},
{"name": "guest1", "group": 2}
]
}
}
任何人进出、封禁、授权变化都会触发服务端主动推送该事件——名单维护无需轮询,监听即可。
Disconnected:连接断开后服务端在会话日志与广播中标记离场(机器人下线时其他观众可见)。
Progress:服务器缓存下载进度推送(type 为 Caching/Downloading/Uploading/Canceled/Completed 五态 + ratio 进度),仅在使用缓存功能时出现。
5.5 管理与诊断域(KickClient / BanClient / Dump / CrashTest)
KickClient {name}(需 banClient 权限):踢人。被踢者收到裸 KickClient 事件后断线,且前端标记 disabledReconnection(官方前端禁止其自动重连)。实测:管理员对 guest1 发送后,两个连接分别收到——
管理员侧(无回执,等 2 秒无响应)与被踢者侧:
{"type": "KickClient"}
BanClient {name, time}(需 banClient 权限):按被禁者 IP 封禁,time 单位为秒(源码 src/server/Main.hx:669:currentTime + time * 1000 毫秒换算),time=0 表示解封。实测三连:
- 封 3 秒:管理员收到广播名单更新,
guest1的 group 变为3(Banned=1 + User=2 的位标志实证):
{
"type": "UpdateClients",
"updateClients": {
"clients": [
{"name": "admin", "group": 14},
{"name": "guest1", "group": 3}
]
}
}
- 被封连接随即被服务端断开;封禁期间该 IP 新连接虽能建立但被标记
isBanned,封禁到期后自动恢复(实测 4 秒后重连group=2正常); - 解封(
time=0)同样广播UpdateClients,无独立回执。
边界行为(源码 src/server/Main.hx:657-660):封禁自己或任何管理员返回 ServerMessage textId=adminsCannotBeBannedError。
Dump(仅管理员):全量诊断转储,实测一次请求三条响应(顺序固定):
{"type": "ServerMessage", "serverMessage": {"textId": "Free space: 3145728KiB"}}
{"type": "ServerMessage", "serverMessage": {"textId": "Memory usage: {\n\trss : 56340480, \n\theapTotal : 7499776, \n\theapUsed : 6795000, \n\texternal : 2123246, \n\tarrayBuffers : 22009\n}"}}
{"type": "Dump", "dump": {"data": "{\n\t\"state\": { ... },\n\t\"clients\": [ ... ],\n\t\"logs\": [ ... ]\n}"}}
Dump.data 是格式化 JSON 字符串,json.loads 后包含三部分:state(与 user/state.json 同构的频道状态:播放列表/聊天历史/计时器/闪回栈)、clients(含 ip/isBanned/isAdmin/isLeader 的完整在线表)、logs(本会话全部事件流水)。机器人做巡检时读前两条 ServerMessage 即可拿到磁盘与内存水位。
CrashTest(仅管理员):官方预留的崩溃自检——触发 uncaughtException 使进程退出(退出前仍会 Saving state... 保存)。生产环境不要接入任何自动化流程;它的正规用途是在 systemd Restart=on-failure 托管下演练自动拉起(见《Ubuntu 22.04 安装 SyncTube 1.0.0 指南》4.6 节)。
六、服务进程终端命令(5 条)
6.1 命令入口
服务进程从 stdin 读取 5 条斜杠命令(src/server/ConsoleInput.hx)。前台 node build/server.js 启动时直接在终端输入即可。输入未知命令时服务端回显完整命令表(实测,控制序列已清理):
Unknown command "help me". List:
/addAdmin name password | Adds channel admin
/removeAdmin name | Removes channel admin
/replay name | Replay log file on server from user/logs/
/logList | Show log list from user/logs/
/exit | Exit process
⚠️ 注意:systemd 托管时进程没有交互终端,这 5 条命令全部不可用(等效输入
help me的回显可在调试时验证)。停止服务用sudo systemctl stop synctube,新增管理员走/setup网页流程(见《Ubuntu 22.04 安装 SyncTube 1.0.0 指南》常见问题 Q2)。
6.2 命令逐条实测
以下实测基于前台进程(启动于 /opt/synctube),输出行均照抄自服务 stdout(时间戳与控制序列清理,下同)。
/logList——列出 user/logs/ 下的会话日志(每次进程会话结束落盘一个 JSON,文件名即日志名,不含 .json 后缀):
2026-10-30_01_14_59
2026-10-30_01_20_30
2026-10-30_01_28_20
/addAdmin name password——内存中新增管理员(立即生效可登录;/exit 或 systemctl stop 时随 user/users.json 落盘):
Admin testadmin added.
参数缺失时的报错形态(实测 /addAdmin 不带参数):
Wrong count of arguments for command "addAdmin" (0 instead of 2)
/removeAdmin name——移除管理员:
Admin testadmin removed.
/replay name——把 user/logs/ 的历史日志在当前会话重放。实测两个关键行为:
- 参数不能带
.json后缀(带了会被二次拼接):
File "/opt/synctube/user/logs/2026-10-30_01_28_20.json.json" not found
- 正确参数重放包含连接事件的日志时,实测触发服务端未捕获异常并退出进程:
uncaughtException, {
message : Cannot read properties of null (reading 'headers'),
stack : TypeError: Cannot read properties of null (reading 'headers')
at server_Main.clientIp (/opt/synctube/build/server.js:4946:24)
at server_Main.checkBan (/opt/synctube/build/server.js:5898:17)
at server_Main.onMessage (/opt/synctube/build/server.js:5243:9)
at timer.run (/opt/synctube/build/server.js:4990:12)
at Timeout._onTimeout (/opt/synctube/build/server.js:2754:6)
at listOnTimeout (node:internal/timers:581:17)
at process.processTimers (node:internal/timers:519:7)
}
Saving state...
根因:重放历史连接事件时,重造的请求对象没有真实 socket,clientIp 读取 headers 抛 TypeError。这是提交 7b0ea58 的已知缺陷——/replay 在生产环境等同一次意外停机(systemd Restart=on-failure 会拉起,但观众全部掉线重连),仅建议在维护窗口排障时对纯聊天类日志谨慎使用,或改用 5.5 节 Dump 的 logs 字段做只读分析。
/exit——优雅退出:保存 user/users.json(含新增管理员)、user/state.json(播放列表/进度/聊天历史)与本次会话日志后进程退出:
Saving state...
6.3 终端命令与 WS 协议的职责边界
| 操作 | 走终端命令 | 走 WS 协议 |
|---|---|---|
| 新增/移除管理员 | /addAdmin /removeAdmin(可离线操作) | 无对应事件(管理口令只在 /setup 与登录校验) |
| 排障/诊断 | /logList /replay(会话日志) | Dump(只读、安全、可脚本化) |
| 停止服务 | /exit(前台进程) | 无(systemd 部署用 systemctl stop) |
| 封禁/踢人/清屏等频道操作 | — | BanClient / KickClient / ClearChat |
七、通用约定与常见问题
Q1:机器人断线后如何恢复?
websocket-client 侧没有自动重连——重连后必须把 Connected 推送当作全新初始化(昵称回到 Guest N,需要重新 Login;播放列表与进度从 Connected 重新拿),不要缓存旧会话的 clients/videoList 状态。
Q2:为什么我发的 SetRate/KickClient 收不到任何响应?
两类原因:SetRate 这类"除发送者外广播"事件本来就没有回执(5.3 节实测);管理类事件在权限不足时部分静默忽略(如 SetNextItem 的空转边界,5.2 节)。判断"成功"不要依赖每事件必有回执,而以随后的状态广播(UpdatePlaylist/UpdateClients)为准。
Q3:消息是按发送顺序返回的吗?
单连接内服务端按序处理、按序回推(TCP 保证);但广播类响应会穿插其他客户端触发的推送。实测脚本按"发一条收一条"推进时会收到错位的事件(本文取证时就遇到过 Dump 响应与 UpdateClients 穿插)——解析端务必按 type 分派而不是按序号配对。
Q4:能用 HTTP 直接调这些能力吗?
不能。除页面资源、/setup 初始化与静态文件外,服务端不提供任何 JSON over HTTP 的操作接口。集成一律走 WS;容器外的旁路(读 user/state.json)只能做只读监控,写入不生效(服务端启动加载 + 退出覆盖写)。
Q5:如何防止机器人昵称被冒充?
协议对 Login.clientName 无认证(同名后登录者会顶替名单显示)。真要防冒充,把机器人建成管理员(/addAdmin bot 密码)并走 4.3 节的 passHash 登录——管理员名有口令保护,且 adminsCannotBeBannedError 保证其不被封踢。
八、参考链接
- SyncTube 仓库:https://github.com/RblSb/SyncTube(本文事件枚举与源码行为基于提交
7b0ea58,查询日期 2026-10-30) - 协议事件定义源文件:
src/Types.hx(WsEventType枚举与各事件载荷 typedef) - 站内关联文档:《SyncTube 介绍与选型指南》、《Ubuntu 22.04 安装 SyncTube 1.0.0 指南》、《SyncTube 1.0.0 配置参数详解》
- 同类对照:《CyTube 介绍与选型指南》(CyTube 同样以 WebSocket 为核心交互协议,且额外提供 REST 化的 socket.io 接口,多频道模型下的 API 面更宽)