Skip to content

WebSocket API 文档 ​

项目:对面是AI吗
协议:WebSocket
数据格式:JSON(文本帧)
基础URL:wss://game.xfcode.top
心跳:客户端发送 {"type":"ping"},服务端回复 {"type":"pong"},10s 间隔,65s 超时断开
更新日期:2026-10-06


通用约定 ​

消息格式 ​

所有消息均为 JSON 文本帧,以 type 字段区分消息类型:

json
{"type": "消息类型", ...其他字段}

WebSocket 端点总览 ​

服务端在 WebSocketHandler 中按 请求路径(onOpen)与 消息 type 前缀(onMessage)双重路由。各端点前缀与 Handler 对照见文末附录。

通用消息类型 ​

所有游戏模式均支持以下通用消息:

type方向说明
pingC→S心跳请求
pongS→C心跳响应
get_stickersC→S获取表情包差异更新
stickers_listS→C表情列表(有变更时)
stickers_unchangedS→C表情无变化
errorS→C通用错误 {"type":"error","message":"..."}

时间字段约定 ​

  • 所有 time / created_at / createdAt 等时间字段均为 Unix 秒级时间戳(int),展示格式由客户端自行解析。
  • duration / timeout / seconds_remaining / ttl 等为 相对时长(秒),非时间戳。

认证机制 ​

所有 WebSocket 端点共用同一套身份认证体系,由 BaseGameHandler 统一实现。

连接阶段(onOpen) ​

WebSocket 连接建立时不进行身份验证,仅做以下检查:

  1. IP 提取 — 优先级:CF-Connecting-IP > X-Forwarded-For > X-Real-IP > remote_addr
  2. IP 去重(可选,默认开启)— 同一 IP 已有活跃连接时关闭旧连接
  3. IP 级封禁检查 — 调用 BanRepository::isBanned(),被封则推送 {"type":"error","message":"您已被管理员封禁"} 并关闭连接
  4. 全服公告推送 — 如有生效中的 Redis 广播,推送给新连接
  5. 在线人数推送 — 非聊天室端点推送 {"type":"online_count","count":N}(在线数按 IP 去重)
  6. 连接过载拒绝 — 服务过载时推送 {"type":"error","message":"服务器繁忙,请稍后再试"} 并关闭连接

真正的身份验证发生在客户端发送第一条业务消息时(如 join、lobby_join、board_gomoku_join、soup_lobby)。

业务消息认证(三级验证) ​

所有需要身份的消息均携带以下字段(三选一即可):

字段说明优先级
player_token已有的 JWT Token(老玩家)最高
password密码(新玩家注册/换设备恢复)中
仅 nickname昵称(兼容旧数据,需同设备 IP+FP 匹配)最低

Level 1:Token 验证(老玩家优先) ​

Client → Server: {type: "join", nickname: "小明", player_token: "eyJ..."}
  • Token 格式:base64(json_payload).hmac_sha256_sig
  • Payload 结构:{player_id, exp(365天), iat, jti}
  • HMAC 密钥:玩家在数据库中的 password_hash(bcrypt)
  • 验证内容:签名正确性 + 过期时间 + player_id 有效性

Token 可用跨模式复用 — 同一 player_id 可在 /ws、/ws/lobby、/ws/board/gomoku、/ws/board/go、/ws/soup 等各端点之间通用。

Level 2:密码验证(新玩家/换设备) ​

Client → Server: {type: "join", nickname: "小明", password: "mypassword"}
  • 按昵称查数据库,password_verify($password, $record['password_hash'])
  • 密码正确 → 生成新 Token 并返回
  • 密码错误 → 返回 {"type":"error","message":"密码不正确"},不中断连接,可重试
  • 昵称不存在 → 视为新玩家注册,自动创建身份

Level 3:仅昵称(兼容旧迁移数据) ​

Client → Server: {type: "join", nickname: "小明"}
  • 按昵称查数据库
  • 存在且同设备(IP + Fingerprint 匹配)→ 允许复用旧账号,生成新 Token
  • 存在但设备不同 → 返回 {"type":"error","message":"该昵称已被占用,请换一个或输入密码"}
  • 不存在 → 视为新玩家,自动创建

新玩家注册 ​

当昵称在数据库中不存在时,自动触发注册流程:

  1. 检查同 IP 账号数 ≤ 3(超出则拒绝,错误 不允许创建多个账号!)
  2. 调用 PlayerStatsRepository::createPlayer(nickname, ip, fp, password) 创建记录
  3. 密码使用传入的 password 或随机生成 bin2hex(random_bytes(8))
  4. BCrypt cost 10 加密存储
  5. 生成 JWT Token 返回给客户端

创建过于频繁时返回 账号创建过于频繁,请稍后再试。

在线唯一性 ​

同一 player_id 通过 Redis SETNX 锁定(120s TTL),防止多地同时登录:

  • 登录时 claimOnlineLock() 尝试获取锁
  • 连接关闭时自动释放
  • 获取锁失败 → 关闭已有连接,推送 {"type":"system","text":"该账号已在其他地方登录,请先退出后再重试"}

验证失败处理汇总 ​

端点消息类型失败响应 type失败示例连接
/wsjoinerror{"type":"error","message":"密码不正确"}不断开
/ws/lobbylobby_joinlobby_error{"type":"lobby_error","text":"密码不正确"}close
/ws/board/gomokuboard_gomoku_joinboard_gomoku_error{"type":"board_gomoku_error","data":"密码不正确"}不断开
/ws/board/goboard_go_joinboard_go_error{"type":"board_go_error","data":"密码不正确"}不断开
/ws/soupsoup_lobbysoup_error{"type":"soup_error","message":"密码不正确"}不断开
/ws/tempchattemp_jointemp_error{"type":"temp_error","text":"登录已失效,请重新登录"}close
全部onOpenerror{"type":"error","message":"您已被管理员封禁"}close
全部online locksystem{"type":"system","text":"该账号已在其他地方登录..."}close

Token 机制(内部) ​

生成: base64(json_encode({player_id, exp(365天), iat, jti(随机8字节)})) . "." . hmac_sha256(payload, password_hash)
验证: 解析 payload + sig → 查 DB 获取 password_hash → 重新计算 HMAC 对比
密钥: 每个玩家独立的 bcrypt password_hash
有效期: 365 天

1. "对面是AI吗" 主游戏 ​

WebSocket 路径:/ws
Handler:GameWebSocketHandler

1.1 通用消息 ​

error ​

方向:S→C

json
{"type":"error","message":"ERROR_MESSAGE"}

ping / pong ​

C→S:

json
{"type":"ping"}

S→C:

json
{"type":"pong"}

说明:心跳机制,每 10 秒一次。65 秒无心跳则断开连接。


1.2 匹配与对局流程 ​

join(加入匹配) ​

方向:C→S

json
{
  "type": "join",
  "nickname": "小明",
  "duration": 600,
  "fingerprint": "可选的设备指纹",
  "password": "密码(新玩家创建身份或恢复身份时)",
  "player_token": "已有的 token(已登录时)",
  "reconnect_session_id": "重连恢复时的 session_id"
}
字段类型必填说明
nicknamestring是1-16 字符
durationint是对局时长:300(5分钟)或 600(10分钟)
fingerprintstring否设备指纹
passwordstring条件新玩家创建身份或恢复时
player_tokenstring条件已有 token 时,与 password 二选一
reconnect_session_idstring否断线重连恢复

说明:加入匹配队列。身份验证成功后进入等待。


matched(匹配成功) ​

方向:S→C

json
{
  "type": "matched",
  "opponent_name": "小红",
  "duration": 600,
  "session_id": "sess_abc123",
  "player_id": "player_xxx",
  "token": "eyJ..."
}
字段说明
opponent_name对手昵称(可能为 AI 别名)
duration对局时长
session_id对局会话 ID
player_id你的玩家 ID
tokenJWT Token(新玩家首次匹配时下发)

说明:匹配成功后自动推送,开始聊天阶段。


message(发送聊天消息) ​

方向:C→S

json
{
  "type": "message",
  "text": "你好,请问你是 AI 吗?"
}
字段类型必填说明
textstring是消息内容,最多 300 字符

触发条件:对局状态为 chatting(聊天阶段)。

对手收到:

json
{"type":"message","text":"你好,请问你是 AI 吗?","sender":"对方"}

旁观者收到:

json
{"type":"spectate_message","text":"...","sender":"玩家1","side":"left"}

sticker(发送表情) ​

方向:C→S

json
{"type":"sticker","id":"表情ID"}

对手收到:

json
{"type":"sticker","id":"xxx","name":"大笑","url":"/stickers/xxx.png","sender":"对方"}

judge(判定) ​

方向:C→S

json
{
  "type": "judge",
  "guess": "human",
  "tag": "有逻辑"
}
字段类型必填说明
guessstring是"human"(认为是真人)或 "ai"(认为是 AI)
tagstring否标签,用于风格画像

触发条件:聊天阶段结束进入判定阶段。

对手判定进度通知(对手收到):

json
{
  "type": "judge_notify",
  "message": "对方已做出判断,你需要在 60 秒内完成判定,否则判负",
  "seconds_remaining": 60
}

判定结果(双方都判定完成后,双方收到):

json
{
  "type": "judged",
  "truth": "human",
  "opponent_guess": "ai",
  "opponent_tag": "机器人",
  "session_id": "sess_abc123",
  "opponent_name": "小红",
  "player_id": "player_xxx"
}
字段说明
truth你的真实身份:"human" 或 "ai"
opponent_guess对手对你的判断
opponent_tag对手给你的标签

leave(主动离开) ​

方向:C→S

json
{"type":"leave"}

对手收到:

json
{"type":"system","text":"对方已离开"}

然后:

json
{
  "type":"timeout",
  "reason":"opponent_left",
  "opponent_truth":"ai",
  "session_id":"sess_abc123",
  "opponent_name":"小红"
}

leave_result(确认离开结果页) ​

方向:C→S

json
{"type":"leave_result","session_id":"sess_abc123"}

说明:查看对战结果后确认离开,不主动断连,仅标记离开意向用于跨进程同步清理。


save_history(保存聊天记录) ​

方向:C→S

json
{"type":"save_history","session_id":"sess_abc123"}

响应:

json
{"type":"save_history_status","success":true,"message":"聊天记录已保存","id":123}

leave_message(留言) ​

方向:C→S

json
{"type":"leave_message","text":"打得很精彩!"}

响应:

json
{"type":"leave_message_status","success":true,"message":"留言已保存"}

report(举报) ​

方向:C→S

json
{"type":"report","reason":"对方使用了不文明用语"}
字段类型必填说明
reasonstring是举报原因,最多 100 字符

响应:

json
{"type":"report_result","success":true,"message":"举报已提交,管理员将尽快处理"}

update_nickname(更新昵称) ​

方向:C→S

json
{
  "type": "update_nickname",
  "nickname": "新昵称",
  "fp": "设备指纹"
}

响应:

json
{"type":"update_nickname_result","success":true}
// 或
{"type":"update_nickname_result","error":"昵称已被占用"}

1.3 服务器主动推送 ​

timeout(超时) ​

方向:S→C

json
{
  "type": "timeout",
  "reason": "chat_expired",
  "session_id": "sess_abc123",
  "opponent_name": "小红"
}
reason 值说明
chat_expired聊天时间到,进入判定阶段
you_timeout你判定超时,判负
opponent_timeout对手判定超时,你判胜
both_timeout双方判定超时,平局
opponent_left对手离开了对局
no_mutual_chat60 秒内双方无互发消息,对局终止

system(系统消息,对局内) ​

方向:S→C

json
{"type":"system","text":"已进入判定阶段,请判断对方是真人还是 AI"}

1.4 管理员消息 ​

admin_ban(管理员封禁) ​

方向:C→S

json
{"type":"admin_ban","token":"管理员token","reason":"违规行为"}

被禁者收到:

json
{"type":"banned","text":"你已被管理员封禁。原因:违规行为"}

admin_verify(验证管理员) ​

方向:C→S

json
{"type":"admin_verify","token":"管理员token"}

响应:

json
{"type":"admin_config","ws_url":"/admin/ws","super_admin":true}

ws_url 由 Config/App.php 的 Admin.Path(默认 admin)动态拼装为 /{Admin.Path}/ws。


2. 大厅聊天室 ​

WebSocket 路径:/ws/lobby
Handler:LobbyChatWebSocketHandler
前缀:lobby_

大厅聊天室为常驻公共聊天室,支持文本 / 表情 / 图片 / 卡片分享、@ 提醒、拍一拍、点歌、MD 投票与管理员治理。消息广播有 64KB 上限保护(超大消息自动降级)。

2.1 连接建立 ​

连接建立后不立即下发历史/在线列表,需先发送 lobby_join 完成身份验证。

lobby_join 通过后依次下发:

json
{"type":"lobby_joined","nickname":"小明","player_id":"player_xxx","token":"eyJ...","sender_titles":[],"sender_special_titles":[]}
{"type":"lobby_history","messages":[...]}
{"type":"lobby_online_count","players":[{"fd":1,"nickname":"小明","player_id":"player_xxx","is_bot":0,"muted":0,"isolated":0}]}

并向其他在线用户广播进入通知(不发给本人):

json
{"type":"lobby_system","text":"小明 进入了聊天室"}

跨 Handler 复用:棋类等待页可通过 board_{game}_open_lobby_chat 把棋局连接挂载进聊天室,同一 fd 参与广播与在线列表;此时聊天室侧的 lobby_* 消息由路由层按前缀转交。


2.2 用户消息 ​

lobby_join(加入聊天室) ​

方向:C→S

json
{
  "type": "lobby_join",
  "nickname": "小明",
  "password": "密码",
  "player_token": "已有 token"
}
字段类型必填说明
nicknamestring是1-12 字符
passwordstring条件新玩家/恢复身份时
player_tokenstring条件已有 token 时

错误(lobby_error):昵称 1~12 字符、密码不正确、您已被管理员封禁...(后三者会断开连接)。

响应:见 2.1 连接建立。


lobby_set_fp(上报设备指纹) ​

方向:C→S

json
{"type":"lobby_set_fp","fp":"设备指纹"}

说明:补充连接级指纹,用于封禁复查;无响应。


lobby_chat(发送聊天消息) ​

方向:C→S

json
{
  "type": "lobby_chat",
  "content": "大家好!",
  "reply_to_id": 123,
  "reply_to_name": "小红",
  "reply_to_text": "原始消息内容"
}
字段类型必填说明
contentstring是消息内容
reply_to_idint否回复的消息 ID
reply_to_namestring否回复对象的昵称
reply_to_textstring否回复消息的原文摘要(最多 100 字符)

触发条件:已加入聊天室 + 未被禁言 + 未被孤立 + 通过频率限制。

广播(普通文本):

json
{
  "type": "lobby_chat",
  "id": 456,
  "sender_name": "小明",
  "sender_id": "player_xxx",
  "content": "大家好!",
  "msg_type": "",
  "is_bot": false,
  "sender_titles": [],
  "sender_special_titles": [],
  "reply_to": {"id":123,"name":"小红","text":"原始消息内容"},
  "mentions": ["小红"],
  "time": 1730000000,
  "created_at": 1730000000
}

广播(含特殊 MD 语法):

json
{
  "type": "lobby_chat",
  "id": 457,
  "sender_name": "小明",
  "sender_id": "player_xxx",
  "content": {
    "v": 1,
    "blocks": [
      { "t": "text", "text": "投票:" },
      { "t": "vote", "question": "今天午饭吃什么", "options": ["米饭", "面条"], "max": 1, "mode": "bar", "sync": true }
    ]
  },
  "msg_type": "markdown",
  "is_bot": false,
  "sender_titles": [],
  "sender_special_titles": [],
  "reply_to": null,
  "mentions": [],
  "time": 1730000060,
  "created_at": 1730000060
}
广播字段类型说明
msg_typestringmarkdown(含特殊 MD 语法)/ 空字符串(普通文本)/ card.share.record / card.invite.board.gomoku / card.invite.board.go / card.share.fate
contentobject|stringmarkdown 时为 blocks 对象 {"v":1,"blocks":[...]};普通文本时为纯文本原文;卡片消息时为卡片 JSON 对象
is_botbool是否来自 BOT 网关
sender_titles / sender_special_titlesstring[]发送者佩戴标签 / 特殊标签
reply_toobject|null引用消息 {id,name,text}
mentionsstring[]本条消息实际 @ 到的在线昵称
time / created_atintUnix 秒级时间戳

@提醒(被 @ 的用户收到):

json
{
  "type": "lobby_mentioned",
  "message_id": 456,
  "sender_name": "小明",
  "content": "大家好!"
}

仅当消息含特殊 MD 语法(组件语法 [!...](类型:...) 或 %变量% 引用)时,后端才解析为 blocks 并将 msg_type 设为 markdown;普通文本消息保持纯文本。@全体成员 仅对管理员生效。

被禁言 / 频率限制回复(lobby_system):

json
{"type":"lobby_system","text":"你已被禁言,剩余 10 分钟"}
{"type":"lobby_system","text":"发言太频繁,请等待 3 秒"}

lobby_sticker(发送表情) ​

方向:C→S

json
{"type":"lobby_sticker","id":"表情ID"}

广播(type 为 sticker,非 lobby_sticker):

json
{
  "type": "sticker",
  "id": "xxx",
  "sticker_id": "xxx",
  "sticker_name": "大笑",
  "sticker_url": "/stickers/xxx.png",
  "sender_name": "小明",
  "sender_id": "player_xxx",
  "is_bot": false,
  "time": 1730000000,
  "created_at": 1730000000
}

lobby_image(发送图片) ​

方向:C→S

json
{"type":"lobby_image","key":"32位一次性key","client_id":"32位客户端关联id"}
字段类型必填说明
keystring是由 /api/image/upload 发放的一次性 key(32 位 hex,TTL 120 秒)
client_idstring否客户端生成的关联 ID,随广播原样回带(不入库),供前端把本地占位气泡对账成真实消息

广播(type 为 image):

json
{
  "type": "image",
  "content": "https://.../xxx.png",
  "sender_name": "小明",
  "sender_id": "player_xxx",
  "sender_titles": [],
  "sender_special_titles": [],
  "time": 1730000000,
  "created_at": 1730000000,
  "client_id": "客户端关联id"
}

错误(lobby_system):图片已过期,请重新上传、发送太频繁,请等待 N 秒、你已被禁言,剩余 N 分钟、你还未加入聊天室。


lobby_nudge(拍一拍) ​

方向:C→S

json
{"type":"lobby_nudge","target_fd":123,"target_nickname":"小红"}

目标收到:

json
{"type":"lobby_nudged","sender_name":"小明"}

广播:

json
{"type":"lobby_system","text":"小明 拍了拍 小红"}

限制:每个发送者 5 秒一次,超限返回 {"type":"lobby_error","text":"拍得太频繁了,请稍后再试"}。


lobby_revoke(撤回消息) ​

方向:C→S

json
{"type":"lobby_revoke","message_id":456}

限制:只能撤回自己 3 分钟内的消息。

广播:

json
{"type":"lobby_revoke","message_id":456,"sender_name":"小明"}

错误(lobby_error):无效的消息 ID、请先进入聊天室、撤回失败:消息不存在、不是你的消息或已超过3分钟。


lobby_report(举报消息) ​

方向:C→S

json
{"type":"lobby_report","message_id":456,"reason":"不文明用语"}
字段类型必填说明
message_idint是被举报消息 ID
reasonstring否举报原因,最多 255 字符(缺省为「违规消息」)

响应:

json
{"type":"lobby_report_ok","message":"举报已提交,管理员将尽快处理"}

错误(lobby_error):无效的消息 ID、请先设置昵称、消息不存在或已被删除、该消息已被举报,请等待管理处理。


lobby_card_share(分享战绩卡片) ​

方向:C→S

json
{"type":"lobby_card_share"}

说明:服务端从数据库读取真实战绩生成卡片(防伪造),受发言频率限制。

广播:一条 lobby_chat 消息,msg_type = card.share.record,content 为卡片对象:

json
{
  "type": "record",
  "version": 1,
  "title": "小明的战绩",
  "player": "小明",
  "fields": {"wins":10,"losses":5,"games":15,"rate":67},
  "footer": "XQFGameHub"
}

本人另外收到 {"type":"lobby_system","text":"战绩卡片已分享到聊天室"}。

错误(lobby_system):操作太频繁,请等待 N 秒。


lobby_btn_click(按钮点击次数限制) ​

方向:C→S

json
{
  "type": "lobby_btn_click",
  "key": "按钮key",
  "userName": "小明",
  "rule": {"mode":"global","globalLimit":10,"perUserLimit":3,"extra":{}}
}
字段类型必填说明
keystring是按钮唯一键
userNamestring否当前用户名(per-user / mixed 模式用)
rule.modestring否global / per-user / mixed(默认 global)
rule.globalLimitint否全局次数上限
rule.perUserLimitint否每用户次数上限
rule.extraobject否mixed 模式下特定用户覆盖上限 {用户名: 上限}

响应:

json
{"type":"lobby_btn_click_result","key":"按钮key","allowed":true,"remaining":9}
字段说明
allowed是否允许
remaining剩余次数(-1 表示无限制)

lobby_poll_vote(MD 投票) ​

方向:C→S

json
{"type":"lobby_poll_vote","message_id":456,"vote_id":"v1","options":[0,2]}
字段类型必填说明
message_idint是投票所在消息 ID
vote_idstring是投票组件 ID
optionsint[]是该用户当前全部已选选项下标(0-19,最多 10 项)

广播(服务端按用户差分更新票数后广播,仅实时同步不写历史):

json
{"type":"lobby_poll_update","vote_key":"456:v1","counts":{"0":3,"2":1}}

错误(lobby_error):请先进入聊天室。


2.3 点歌系统 ​

lobby_song_search(搜索歌曲) ​

方向:C→S

json
{"type":"lobby_song_search","keyword":"周杰伦"}

说明:若 keyword 为网易云分享链接 / 短链 / 纯歌曲 ID,则直接按该歌曲点歌,响应带 direct_requested。

响应:

json
{"type":"lobby_song_search_result","keyword":"周杰伦","songs":[{...}]}
// 或(直接点歌)
{"type":"lobby_song_search_result","direct_requested":true,"keyword":"...","song":{...}}

错误(lobby_song_search_result 内 error):请输入搜索关键词、请先设置昵称后再通过分享链接点歌、身份验证失败,请重新进入。


lobby_song_request(点歌) ​

方向:C→S

json
{"type":"lobby_song_request","song_id":"xxx","nickname":"小明"}

响应:

json
{"type":"lobby_song_requested","song":{...}}

错误(lobby_error):请先设置昵称、身份验证失败,请重新进入、无效的歌曲ID。


lobby_song_vote / lobby_song_remove_vote(投票/取消投票) ​

json
{"type":"lobby_song_vote","song_id":"xxx"}
json
{"type":"lobby_song_remove_vote","song_id":"xxx"}

广播:

json
{"type":"lobby_vote_update","song_id":"xxx","votes":5}
{"type":"lobby_remove_vote_update","song_id":"xxx","remove_votes":3}

错误(lobby_error):无效的歌曲ID、请先进入聊天室。


lobby_song_list / lobby_song_current(歌单查询) ​

方向:C→S

json
{"type":"lobby_song_list"}
{"type":"lobby_song_current"}

响应(歌单):

json
{"type":"lobby_song_list","playlist":[...],"pool":[...],"playing":{...}}

响应(当前播放):

json
{"type":"lobby_song_current","song":{...}}
// 或等待中
{"type":"lobby_song_current","waiting":true}

广播(列表更新):

json
{"type":"list_update","playlist":[...],"pool":[...],"playing":{...}}

lobby_song_rcmd(随机推荐) ​

方向:C→S

json
{"type":"lobby_song_rcmd"}

响应:

json
{"type":"lobby_song_rcmd_result","song":{...}}
// 或
{"type":"lobby_song_rcmd_result","error":"..."}

lobby_song_import(导入歌单) ​

方向:C→S

json
{"type":"lobby_song_import","keyword":"歌单链接或ID"}

进度:

json
{"type":"lobby_song_import_progress","done":5,"total":20,"name":"歌单名"}

结果:

json
{"type":"lobby_song_import_result","result":{...},"text":"..."}
// 或
{"type":"lobby_song_import_result","error":"..."}

lobby_song_admin_remove(管理员移除歌曲) ​

方向:C→S

json
{"type":"lobby_song_admin_remove","song_id":"xxx"}

说明:仅管理员可用,移除后广播 lobby_system 提示。


lobby_song_finished(客户端上报播放结束) ​

方向:C→S

json
{"type":"lobby_song_finished"}

说明:客户端播放结束时上报,服务端据此推进歌单/广播。


2.4 管理员消息 ​

以下消息仅对管理员 fd 生效(通过 lobby_admin_verify 本地注册、Tracker fd 或同 IP 判定),非管理员调用将被静默忽略。

lobby_delete(管理员删消息) ​

json
{"type":"lobby_delete","message_id":456}

广播:

json
{"type":"lobby_message_deleted","message_id":456}

lobby_mute(管理员禁言) ​

json
{"type":"lobby_mute","target_fd":123,"minutes":10}

目标收到:

json
{"type":"lobby_system","text":"你已被管理员禁言 10 分钟"}

广播:

json
{"type":"lobby_system","text":"小明 已被管理员禁言 10 分钟"}

lobby_unmute(管理员解除禁言) ​

json
{"type":"lobby_unmute","target_fd":123}

目标收到:{"type":"lobby_system","text":"你已被管理员解除禁言"}
广播:{"type":"lobby_system","text":"小红 的禁言已被解除"}


lobby_isolate(管理员孤立) ​

json
{"type":"lobby_isolate","target_fd":123,"minutes":10}

说明:孤立期间该玩家消息不广播(仅本人可见),静默执行,不提醒其他玩家。仅操作者本人收到 lobby_system 提示。


lobby_unisolate(管理员解除孤立) ​

json
{"type":"lobby_unisolate","target_fd":123}

说明:静默解除,仅操作者本人收到 lobby_system 提示。


lobby_ban(管理员封禁) ​

json
{"type":"lobby_ban","target_fd":123,"reason":"违规"}

执行封禁(IP + 指纹 + player_id)并断开目标 WebSocket 连接。


lobby_kick(管理员踢出) ​

json
{"type":"lobby_kick","target_fd":123}

被踢者收到:

json
{"type":"lobby_kicked","message":"您已被管理员踢出聊天室"}

说明:断开连接但不封禁,被踢者可重新进入。


lobby_admin_verify(管理员验证) ​

json
{"type":"lobby_admin_verify","token":"管理员token"}

响应:

json
{
  "type": "lobby_admin_verified",
  "is_admin": true,
  "username": "admin",
  "role": "super_admin",
  "super_admin": true
}

验证失败时返回 {"type":"lobby_admin_verified","is_admin":false}。


3. 五子棋 ​

WebSocket 路径:/ws/board/gomoku
Handler:GomokuHandler(继承 AbstractBoardGameHandler)
前缀:board_gomoku_

五子棋与围棋共用棋类对局外壳(AbstractBoardGameHandler):身份验证、房间、观战、对局内聊天、再弈、断线判负流程完全一致,仅棋盘规则不同。消息类型由前缀动态拼装:board_gomoku_ + 动作后缀。

3.1 连接建立 ​

服务端推送(连接成功后):

json
{"type":"board_gomoku_connected"}

3.2 棋类公共动作 ​

以下动作五子棋 / 围棋通用,示例以 board_gomoku_ 前缀;围棋只需将前缀替换为 board_go_(见 4 节)。

join(声明身份) ​

方向:C→S

json
{
  "type": "board_gomoku_join",
  "fp": "设备指纹",
  "nickname": "小明",
  "password": "密码",
  "player_token": "已有 token"
}
字段类型必填说明
fpstring否设备指纹
nicknamestring是昵称
passwordstring条件新玩家/恢复身份时
player_tokenstring条件已有 token 时,与 password 二选一

响应:

json
{"type":"board_gomoku_joined","data":{"token":"eyJ...","player_id":"player_xxx"}}

create_room(创建房间) ​

方向:C→S

json
{
  "type": "board_gomoku_create_room",
  "firstMove": "host",
  "boardSize": 15
}
字段类型必填说明
firstMovestring否"host"=房主执黑先手(默认);其它值=让对手先手
boardSizeint否棋盘大小,五子棋 5-30(默认 15);围棋仅支持 9/13/19(默认 19)

响应:

json
{"type":"board_gomoku_room_created","data":{"roomId":"ABCDE","color":1,"player_id":"player_xxx"}}
字段说明
roomId5 位房间码,用于分享和加入
color你的棋子颜色:1=黑棋(先手),2=白棋
player_id你的玩家 ID

错误:身份验证失败(未先完成 join)。


join_room(加入房间) ​

方向:C→S

json
{"type":"board_gomoku_join_room","roomId":"ABCDE"}
字段类型必填说明
roomIdstring是5 位房间码

场景一:作为对手加入(空位),发送给双方

房主收到:

json
{
  "type": "board_gomoku_game_start",
  "data": {
    "roomId": "ABCDE",
    "settings": {"boardSize":15,"firstMove":"host"},
    "currentTurn": 1,
    "myColor": 1,
    "player_id": "player_xxx",
    "opponent": {"nickname":"小红","player_id":"player_yyy"}
  }
}

加入者收到同结构消息,myColor 为 2,opponent 为房主身份。

场景二:作为旁观者加入(已有对手)

json
{
  "type": "board_gomoku_spectate_start",
  "data": {
    "roomId": "ABCDE",
    "settings": {"boardSize":15,"firstMove":"host"},
    "board": [[0,0,...],[0,0,...]],
    "currentTurn": 1
  }
}

若房间对局已结束,观战者会额外收到一次 board_gomoku_game_over。

错误(board_gomoku_error):

data 文案触发场景
身份验证失败未先完成 join
无效的房间号房间码为空
房间不存在房间不存在或已解散
你已是该房间房主房主加入自己房间
房主已离开,房间失效房主连接已断开

cancel_wait(取消等待) ​

方向:C→S

json
{"type":"board_gomoku_cancel_wait"}

说明:房主取消等待,销毁房间并退出等待页聊天大厅。 错误:没有进行中的房间。


surrender / timeout(认输 / 超时) ​

方向:C→S

json
{"type":"board_gomoku_surrender"}
json
{"type":"board_gomoku_timeout"}

广播终局:

json
{"type":"board_gomoku_game_over","data":{"winner":2,"reason":"认输","winPath":[]}}

request_rematch(请求再弈) ​

方向:C→S

json
{"type":"board_gomoku_request_rematch"}

说明:双方都发送后自动重新开局,黑白互换;双方收到 board_gomoku_game_start,观战者收到 board_gomoku_spectate_start。


chat_message(局内聊天) ​

方向:C→S

json
{"type":"board_gomoku_chat_message","msg":"好棋!"}
字段类型必填说明
msgstring是消息内容,最多 300 字符

广播(对手+观战者,不含发送者):

json
{"type":"board_gomoku_chat_message","data":{"msg":"好棋!","from":1,"time":1730000000}}
字段说明
from1=黑棋方,2=白棋方
timeUnix 秒级时间戳

share_invite(分享邀请到聊天室) ​

方向:C→S

json
{"type":"board_gomoku_share_invite"}

说明:向大厅聊天室广播一条 board_gomoku_invite 邀请卡片(受禁言与发言频率限制)。

响应:

json
{"type":"board_gomoku_invite_result","success":true,"message":"对局邀请已发送到聊天室"}
message 文案(success:false)触发场景
请先创建房间无房间
房间不存在或已结束房间已销毁
身份验证失败未完成 join
聊天室暂不可用lobbyHandler 未注入
你已被禁言,剩余 N 分钟 / 操作太频繁,请等待 N 秒发言限制

open_lobby_chat / close_lobby_chat(等待页聊天大厅) ​

方向:C→S

json
{"type":"board_gomoku_open_lobby_chat"}
json
{"type":"board_gomoku_close_lobby_chat"}

说明:等待对手期间复用同一条连接挂载聊天室(发送的 lobby_* 消息由路由层按前缀转给聊天室)。

响应:

json
{"type":"board_gomoku_lobby_chat_result","success":true,"message":""}

失败时 message 为 请先完成身份验证 / 聊天大厅暂不可用 / 进入聊天大厅失败。


error(错误) ​

方向:S→C

json
{"type":"board_gomoku_error","data":"错误描述"}

opponent_disconnected(对手断线) ​

方向:S→C

对局者断线时,对手收到:

json
{"type":"board_gomoku_opponent_disconnected","data":{"msg":"对手已断开连接,对局结束。"}}

观战者收到:

json
{"type":"board_gomoku_opponent_disconnected","data":{"msg":"对局者已离开,对局结束。"}}

3.3 五子棋专属动作 ​

place_piece(落子) ​

方向:C→S

json
{"type":"board_gomoku_place_piece","r":7,"c":7}
字段类型必填说明
rint是行坐标(0-based)
cint是列坐标(0-based)

广播(双方+观战者):

json
{
  "type": "board_gomoku_piece_placed",
  "data": {"r":7,"c":7,"color":1,"nextTurn":2}
}

game_over(游戏结束) ​

方向:S→C

json
{
  "type": "board_gomoku_game_over",
  "data": {
    "winner": 1,
    "reason": "win",
    "winPath": [[3,3],[4,4],[5,5],[6,6],[7,7]]
  }
}
字段说明
winner1=黑棋胜,2=白棋胜,0=平局
reason"win"(五连)、"draw"(棋盘下满平局)、"认输"、"超时"
winPath获胜的 5 个棋子坐标(非 win 时为空数组)

3.4 断线处理 ​

  • 对局中断线 → 断线方判负,对手与观战者收到 board_gomoku_opponent_disconnected,房间删除,双方战绩异步落库
  • 观战者断线 → 静默移除,不影响对局
  • 对局已结束断线 → 仅清理连接绑定

4. 围棋 ​

WebSocket 路径:/ws/board/go
Handler:GoHandler(继承 AbstractBoardGameHandler)
前缀:board_go_

对局外壳与五子棋完全一致(见 3.2 节棋类公共动作),将消息前缀替换为 board_go_ 即可:board_go_join / board_go_create_room / board_go_join_room / board_go_cancel_wait / board_go_surrender / board_go_timeout / board_go_request_rematch / board_go_chat_message / board_go_share_invite / board_go_open_lobby_chat / board_go_close_lobby_chat / board_go_error / board_go_opponent_disconnected(邀请卡片 card 为 board_go_invite,msg_type 为 card.invite.board.go)。

差异点:

  • 棋盘规格仅支持 9 / 13 / 19(GoService::normalizeSize,默认 19)
  • 采用中国规则数子法,贴目 7.5(komi),故不存在和棋
  • 落子一律以服务端为准(客户端不做乐观渲染,等待 board_go_stone_placed 回传完整盘面)
  • 终局后支持「续弈」

4.1 连接建立 ​

json
{"type":"board_go_connected"}

4.2 开局附加字段 ​

board_go_game_start 在公共字段基础上附加 komi:

json
{
  "type": "board_go_game_start",
  "data": {
    "roomId": "ABCDE",
    "settings": {"boardSize":19,"firstMove":"host"},
    "currentTurn": 1,
    "komi": 7.5,
    "myColor": 1,
    "player_id": "player_xxx",
    "opponent": {"nickname":"小红","player_id":"player_yyy"}
  }
}

4.3 围棋专属动作 ​

place_stone(落子) ​

方向:C→S

json
{"type":"board_go_place_stone","r":3,"c":3}
字段类型必填说明
rint是行坐标(0-based)
cint是列坐标(0-based)

广播(双方+观战者):

json
{
  "type": "board_go_stone_placed",
  "data": {
    "r": 3,
    "c": 3,
    "color": 1,
    "captured": 0,
    "nextTurn": 2,
    "board": [[0,0,...],[0,0,...]],
    "captures": {"1":0,"2":0},
    "koHash": "",
    "passes": 0,
    "moveNumber": 1
  }
}
字段说明
captured本手提子数
board完整盘面(0=空,1=黑,2=白)
captures双方累计提子数
koHash打劫盘面哈希
moveNumber手数

非法手仅回给本人(前端据此提示,盘面保持服务端权威):

json
{"type":"board_go_move_rejected","data":{"r":3,"c":3,"reason":"禁止自杀"}}
reason 文案触发场景
坐标超出棋盘坐标越界
该交叉点已有棋子目标点已有子
禁止自杀落子后自身无气
打劫:不能立即回提违反打劫规则

pass(虚手) ​

方向:C→S

json
{"type":"board_go_pass"}

广播:

json
{
  "type": "board_go_passed",
  "data": {"color":1,"nextTurn":2,"passes":1,"board":[...],"captures":{...},"moveNumber":2}
}

说明:连续两次虚手即终局数子。

request_resume(续弈) ​

方向:C→S

json
{"type":"board_go_request_resume"}

说明:终局(canResume=true)后,双方各确认一次可回到终局前盘面继续收束死子。

对方收到:

json
{"type":"board_go_resume_requested","data":{"msg":"对方请求续弈,点击「续弈」继续收束棋局。"}}

双方都确认后广播:

json
{
  "type": "board_go_resumed",
  "data": {
    "roomId": "ABCDE",
    "settings": {...},
    "board": [...],
    "currentTurn": 1,
    "captures": {...},
    "passes": 0,
    "lastMove": null,
    "moveNumber": 42,
    "komi": 7.5
  }
}

4.4 游戏结束 ​

方向:S→C

json
{
  "type": "board_go_game_over",
  "data": {
    "winner": 1,
    "reason": "数子",
    "score": {
      "black": 55,
      "white": 48.5,
      "black_stones": 40,
      "white_stones": 35,
      "black_territory": 15,
      "white_territory": 13.5,
      "neutral": 0,
      "komi": 7.5,
      "winner": 1,
      "owner": "black"
    },
    "canResume": true
  }
}
字段说明
winner1=黑胜,2=白胜
reason"数子"(两次虚手)、"认输"、"超时"
score数子结果;认输/超时时为 null
canResume是否可续弈(仅数子终局为 true)

5. 海龟汤 ​

WebSocket 路径:/ws/soup
Handler:SoupWebSocketHandler
前缀:soup_
开关:Soup.Enabled(默认开启)

海龟汤(真人房)为多人推理玩法:出题人(房主)持有汤底,猜题人提问,出题人判定。房间状态:lobby(大厅)→ picking(选题)→ playing(对局)→ revealed(结算)。

防作弊:汤底(truth/key_points/hints)仅下发给房主(出题人)与结算揭示时;猜题人端永远拿不到。

5.1 连接建立 ​

服务端推送(连接成功后):

json
{"type":"soup_connected"}
{"type":"soup_room_list","rooms":[...]}

online_count 也会按通用逻辑推送。

5.2 身份与大厅 ​

soup_lobby(进入大厅并拉取房间列表) ​

方向:C→S

json
{
  "type": "soup_lobby",
  "fp": "设备指纹",
  "nickname": "小明",
  "password": "密码",
  "player_token": "已有 token"
}

响应(身份验证通过后):

json
{"type":"soup_joined","data":{"token":"eyJ...","player_id":"player_xxx","nickname":"小明"}}
{"type":"soup_room_list","rooms":[...]}

5.3 房间消息 ​

soup_create(创建房间) ​

方向:C→S

json
{
  "type": "soup_create",
  "room_name": "我的汤面房",
  "source": "public",
  "puzzle_id": 0
}
字段类型必填说明
room_namestring否房间名(最多 24 字符),缺省为「昵称 的汤面房」
sourcestring否题目来源(如 public/random),配合 puzzle_id 可建房即选题
puzzle_idint否指定题目 ID

响应:

json
{"type":"soup_room_created","room_id":"ABCDEF"}

错误(soup_error):你已在一个房间中。

soup_join_room(加入房间) ​

方向:C→S

json
{"type":"soup_join_room","room_id":"ABCDEF"}

响应:

json
{"type":"soup_system","text":"已加入房间「我的汤面房」,等待出题人开始"}

同时广播 soup_room 房间快照。

错误(soup_error):

message 文案触发场景
你已在一个房间中重复加入
房间不存在或已解散房间码无效
对局已开始,无法加入房间处于 playing/revealed
房间已满人数达到 Soup.MaxPlayers(默认 6)

soup_pick(出题人选题) ​

方向:C→S

json
{"type":"soup_pick","source":"random","puzzle_id":0}

错误:只有出题人可以选题、对局进行中,无法更换题目。

soup_ready(出题人开始) ​

方向:C→S

json
{"type":"soup_ready"}

错误:只有出题人可以开始、请先选择一道汤面、至少需要 N 人才能开始(Soup.MinPlayers,默认 2)。

开始后按视角广播 soup_room 与 soup_round。

soup_leave(离开房间) ​

方向:C→S

json
{"type":"soup_leave"}

响应:

json
{"type":"soup_left"}

说明:房主离开 → 解散房间并通知所有成员(房主已解散房间);猜题人离开 → 广播 soup_system「X 离开了房间」。

5.4 对局消息 ​

soup_ask(猜题人提问) ​

方向:C→S

json
{"type":"soup_ask","text":"他是意外死亡的吗?"}
字段类型必填说明
textstring是问题内容,最多 200 字符

广播:

json
{
  "type": "soup_question",
  "question_id": 1,
  "text": "他是意外死亡的吗?",
  "nickname": "小明",
  "kind": "ask",
  "index": 1,
  "question": "他是意外死亡的吗?",
  "verdict": null
}

错误:当前不在对局中、出题人不能提问、本题提问已达上限,请提交汤底或放弃(Soup.MaxQuestions,默认 30)、提问太快了,请稍候(Soup.AskRateLimit,默认 3 秒)、提问内容不能为空。

soup_ai_suggest(出题人请求 AI 判定建议) ​

方向:C→S

json
{"type":"soup_ai_suggest","question_id":1}
字段类型必填说明
question_idint否指定问题 ID,缺省取最后一条未判定问题

响应:

json
{"type":"soup_ai_suggested","question_id":1,"verdict":"yes","reason":"符合汤面事实"}

错误:只有出题人可以请求 AI 建议、AI 建议已关闭(Soup.AiAssist=false)、当前不在对局中、没有待判定的提问、AI 暂时无法给出建议,请手动判定。

soup_answer(出题人判定) ​

方向:C→S

json
{"type":"soup_answer","question_id":1,"verdict":"yes","reason":"符合"}
字段类型必填说明
question_idint是目标问题 ID
verdictstring是yes / no / unrelated / close / correct(correct 仅用于猜底命中)
reasonstring否判定理由,最多 60 字符

广播:

json
{"type":"soup_verdict","question_id":1,"question":"他是意外死亡的吗?","verdict":"yes","reason":"符合"}

若为猜底(kind="guess"),额外广播:

json
{"type":"soup_guess_result","question_id":2,"correct":true,"nickname":"小明"}

命中(correct)即结算揭示。

错误:只有出题人可以判定、当前不在对局中、无效的判定、提问不存在、「命中」只能用于汤底猜测。

soup_guess(猜题人猜底) ​

方向:C→S

json
{"type":"soup_guess","text":"他其实没有死"}
字段类型必填说明
textstring是汤底猜测内容,最多 300 字符

错误:出题人不能猜底、当前不在对局中、汤底内容不能为空。

soup_giveup(出题人揭示汤底) ​

方向:C→S

json
{"type":"soup_giveup"}

错误:只有出题人可以揭示汤底、当前不在对局中。

soup_hint(出题人给提示) ​

方向:C→S

json
{"type":"soup_hint"}

广播:

json
{"type":"soup_hint","index":1,"text":"提示内容","total":3}

错误:只有出题人可以给出提示、当前不在对局中、没有更多提示了(Soup.MaxHints,默认 3)。

soup_next(出题人开始下一题) ​

方向:C→S

json
{"type":"soup_next"}

说明:仅结算状态可用;重置房间到 picking,广播 soup_room 与 soup_system「请出题人选择下一题」。 错误:只有出题人可以开始下一题、当前对局尚未结算。

5.5 服务器推送 ​

soup_room(房间快照,按视角裁剪) ​

json
{
  "type": "soup_room",
  "data": {
    "id": "ABCDEF",
    "name": "我的汤面房",
    "state": "playing",
    "host_player_id": "player_xxx",
    "host_nickname": "小明",
    "max_players": 6,
    "max_questions": 30,
    "max_hints": 3,
    "ai_assist": true,
    "your_role": "guesser",
    "is_host": false,
    "members": [
      {"player_id":"player_xxx","nickname":"小明","role":"host","is_host":true},
      {"player_id":"player_yyy","nickname":"小红","role":"guesser","is_host":false}
    ],
    "puzzle": {"id":1,"title":"...","surface":"...","difficulty":1,"tags":"..."},
    "hints_revealed": 0,
    "questions": [
      {"id":1,"text":"...","nickname":"小红","kind":"ask","verdict":"yes","reason":"","ai":null}
    ]
  }
}
说明
puzzle 对猜题人只含 id/title/surface/difficulty/tags;对房主额外含 truth/key_points/hints
questions[].ai 仅房主可见(soup_ai_suggest 结果)
your_role:host / guesser

soup_round(开局汤面) ​

json
{
  "type": "soup_round",
  "title": "...",
  "surface": "...",
  "difficulty": 1,
  "tags": "...",
  "host_nickname": "小明",
  "max_questions": 30,
  "max_hints": 3
}

房主额外收到 truth、key_points、hints。

soup_won(命中公告) ​

json
{"type":"soup_won","nickname":"小红","solution":"汤底全文"}

soup_over(结算) ​

json
{
  "type": "soup_over",
  "solution": "汤底全文",
  "key_points": ["..."],
  "hints": ["..."],
  "winner": "小红",
  "questions_used": 8,
  "hints_used": 1,
  "duration": 240
}
字段说明
winner命中者昵称;无人命中时为空字符串(房主胜)
duration对局时长(秒)

soup_system / soup_error ​

json
{"type":"soup_system","text":"..."}
{"type":"soup_error","message":"..."}

6. 缘分(默契测试) ​

WebSocket 路径:/ws/fate(声明用)
Handler:FateWebSocketHandler
前缀:fate.
开关:Fate.Enabled(默认开启)

默契测试依附于 1v1 对局:客户端仍在原 /ws 对局连接上发送 fate.* 消息,由路由层 fate. 前缀捕获转交本 Handler。因此一般不会单独连接 /ws/fate。

6.1 连接建立 ​

独立连接 /ws/fate 时:

json
{"type":"fate.connected"}

6.2 消息协议 ​

fate.accept(发起/接受默契邀请) ​

方向:C→S

json
{"type":"fate.accept"}

本方响应:

json
{"type":"fate.accepted","data":{"session_id":"sess_xxx"}}

对方收到:

json
{"type":"fate.invite","data":{"session_id":"sess_xxx","partner":"小明","timeout":30}}

双方都确认 → 开题:

json
{"type":"fate.start","data":{"quiz":[...],"partner":"小红","max_queues":5}}
字段说明
quiz题目列表(Fate.QuizCount,默认 5 题)
max_queues题目数量

错误:当前没有可进行默契测试的对局、身份异常,无法进入默契测试。

fate.decline(拒绝邀请) ​

方向:C→S

json
{"type":"fate.decline"}

本方响应:

json
{"type":"fate.declined","data":{"session_id":"sess_xxx"}}

对方收到:

json
{"type":"fate.opponent_done","data":{"status":"declined"}}

fate.submit(提交答案与猜测) ​

方向:C→S

json
{"type":"fate.submit","answers":[0,1,2,3,0],"guesses":[1,1,2,3,0]}
字段类型必填说明
answersint[]是自己的选项(0-3),长度必须等于题目数
guessesint[]否对对方选项的猜测(0-3),缺失按未中处理

本方响应:

json
{"type":"fate.submitted","data":{"session_id":"sess_xxx"}}

对方收到:

json
{"type":"fate.opponent_done","data":{"status":"submitted"}}

双方都提交 → 出报告:

json
{"type":"fate.report","data":{"record_id":123,"session_id":"sess_xxx","report":{...}}}

错误:当前不在答题状态、题目异常,请重试、请完成全部题目再提交。

fate.publish(官宣报告到聊天室) ​

方向:C→S

json
{"type":"fate.publish","record_id":123}

响应:

json
{"type":"fate.published","data":{"record_id":123,"already_published":false}}
字段说明
already_published已官宣过的报告只回执不重复广播

说明:需登录身份,record_id 必须属于调用者本人;成功后向大厅聊天室广播 fate_card 缘分卡片。 错误:报告不存在、请先登录后再官宣、报告不存在或无权官宣、身份信息异常,无法官宣。

fate.history(历史报告) ​

方向:C→S

json
{"type":"fate.history"}

响应:

json
{
  "type": "fate.history_list",
  "data": {
    "records": [
      {
        "id": 123,
        "nickname_a": "小明",
        "nickname_b": "小红",
        "score": 82,
        "verdict": "...",
        "lucken": "...",
        "stats": {...},
        "golds": [...],
        "published": true,
        "created_at": 1730000000
      }
    ],
    "total": 1
  }
}

6.3 断线处理 ​

一方断开时,另一方收到:

json
{"type":"fate.opponent_done","data":{"status":"left"}}

7. 方块世界(2D 沙盒) ​

WebSocket 路径:/ws/blockworld
Handler:BlockWorldWebSocketHandler
前缀:bw_
开关:BlockWorld.Enabled(默认关闭)

2D 多人沙盒世界。世界未加载就绪时拒绝连接。位置采用「客户端预测 + 服务端校验」:服务端不跑物理 tick,仅在收到上报时校验位移/碰撞;下行以 100ms(10Hz)批包聚合。

7.1 连接建立 ​

服务端推送(连接成功后,要求世界已就绪):

json
{"type":"bw_connected"}

若世界未就绪:{"type":"bw_error","data":"世界尚未就绪,请稍后再试"} 并关闭连接。

7.2 消息协议 ​

bw_join(进入世界) ​

方向:C→S

json
{
  "type": "bw_join",
  "fp": "设备指纹",
  "nickname": "小明",
  "password": "密码",
  "player_token": "已有 token",
  "x": 100.5,
  "y": 40.0
}
字段类型必填说明
fpstring否设备指纹
nicknamestring是昵称
passwordstring条件新玩家/恢复身份时
player_tokenstring条件已有 token 时
x / yfloat否期望出生点坐标(合法且无碰撞时采用,否则用世界默认出生点)

响应:

json
{"type":"bw_joined","player_id":"player_xxx","pid_hash":1234567890,"spawn":{"x":100.5,"y":40.0},"world":{"w":256,"h":128}}
{"type":"bw_pid_map","map":{"1234567890":"player_xxx"},"nick":{"player_xxx":"小明"}}
{"type":"bw_chunk","cx":6,"cy":2,"data":"..."}

世界广播(给其他玩家):

json
{"type":"bw_player_info","pidHash":1234567890,"pid":"player_xxx","nickname":"小明"}

错误:世界人数已满,请稍后再试、您已被管理员封禁...、身份验证错误。

bw_move(位置上报) ​

方向:C→S

json
{"type":"bw_move","x":101.2,"y":40.0,"dir":2}
字段类型必填说明
x / yfloat是坐标
dirint否朝向(0-3)

纠偏(位移超限/越界/穿墙时回拉到服务端权威位置):

json
{"type":"bw_correct","x":100.5,"y":40.0}

错误:尚未进入世界。

bw_dig(挖掘)/ bw_place(放置) ​

方向:C→S

json
{"type":"bw_dig","x":100,"y":40}
json
{"type":"bw_place","x":100,"y":40,"id":3}
字段类型必填说明
x / yint是方块坐标
idint是(place)方块 ID

广播:

json
{"type":"bw_block","x":100,"y":40,"id":3}

错误:距自身超出 ReachDistance 等由服务端判定,返回 bw_error。

bw_chat(世界内聊天) ​

方向:C→S

json
{"type":"bw_chat","text":"你好"}
字段类型必填说明
textstring是消息内容,最多 100 字符,最小间隔 800ms

广播:

json
{"type":"bw_chat","pidHash":1234567890,"nickname":"小明","text":"你好"}

bw_respawn(重生) ​

方向:C→S

json
{"type":"bw_respawn"}

响应:{"type":"bw_correct","x":...,"y":...}(回到世界默认出生点)

7.3 服务器推送 ​

bw_pack(移动批包) ​

100ms 周期性下发视野内玩家位置,二进制 base64 信封:

json
{"type":"bw_pack","enc":"b64","data":"..."}
  • 仅视野内(AOIViewChunks)且窗口内有位移的玩家进入批包;视野内仅 1 人时改发单条 bw_move JSON
  • 二进制格式:[count:1B] + N × [pidHash:4B][x:2B][y:2B][flags:2B],坐标量化为 1/8 方块(uint16)
  • flags:bit0-1 朝向,bit2-3 动作(V1 恒为 0),bit4 是否为观察者自己

bw_chunk / bw_block / bw_player_info / bw_leave / bw_error ​

json
{"type":"bw_chunk","cx":6,"cy":2,"data":"..."}
{"type":"bw_block","x":100,"y":40,"id":0}
{"type":"bw_player_info","pidHash":1234567890,"pid":"player_xxx","nickname":"小明"}
{"type":"bw_leave","pidHash":1234567890}
{"type":"bw_error","data":"错误描述"}

7.4 断线处理 ​

玩家离开时世界内广播 bw_leave(携带 pidHash)。


8. 管理后台 WebSocket ​

WebSocket 路径:/{Admin.Path}/ws(Admin.Path 默认 admin,即 /admin/ws)
Handler:AdminWebSocketHandler

8.1 连接与鉴权 ​

连接建立后服务端推送:

json
{"type":"need_admin_login"}

除 admin_connect 外,所有消息需先完成鉴权,否则返回 {"type":"error","message":"请先登录"}。

admin_connect(管理员鉴权) ​

方向:C→S

json
{"type":"admin_connect","token":"管理员token"}

成功响应:

json
{"type":"admin_connected","username":"admin","role":"admin"}
{"type":"sessions_list","sessions":[{"id":"sess_xxx","player1":"小明","player2":"小红","state":"chatting"}]}

失败:{"type":"error","message":"管理员验证失败"} / 管理员账号已被禁用或不存在,并关闭连接。

鉴权后服务端会推送在线管理列表:

json
{"type":"admin_online_list","online_list":[{"fd":10,"admin_id":1,"username":"admin","role":"admin","current_operation":"正在封禁玩家"}]}

当某管理员开始/结束操作时推送:

json
{"type":"admin_status","fd":10,"username":"admin","operation":"正在封禁玩家"}

8.2 消息类型总览 ​

请求 type响应 type说明
admin_connectadmin_connected、sessions_list鉴权
admin_sessionssessions_list拉取活跃对局列表
admin_ban_playersystem封禁对局玩家
admin_ban_by_infoadmin_banned_by_info按 IP/指纹封禁
admin_broadcastbroadcast {text,duration}全服公告
admin_room_broadcastroom_announce {text}房间公告
admin_spectatesession_detail {session_id,state,history,player1,player2}进入观战
admin_unspectateadmin_unspectated退出观战
admin_reportsadmin_reports举报列表
admin_report_detailadmin_report_detail {report}举报详情
admin_mark_reviewedadmin_mark_reviewed_result标记举报已处理
admin_sticker_upload / admin_sticker_addadmin_sticker_added {sticker}上传/添加表情
admin_sticker_batch_addadmin_sticker_batch_added批量添加表情
admin_sticker_deleteadmin_sticker_deleted {id}删除表情
admin_sticker_batch_deleteadmin_sticker_batch_deleted批量删除表情
admin_sticker_listadmin_stickers_list {stickers}表情列表
admin_sticker_review_listadmin_sticker_review_list待审表情列表
admin_sticker_approveadmin_sticker_approved通过表情审核
admin_sticker_batch_approveadmin_sticker_batch_approved批量通过审核
admin_sticker_rejectadmin_sticker_rejected拒绝表情审核
admin_sticker_batch_rejectadmin_sticker_batch_rejected批量拒绝审核
admin_listadmin_list {admins}管理员列表
admin_addadmin_added {admin}添加管理员
admin_deleteadmin_deleted {admin_id}删除管理员
admin_change_password / admin_own_passwordadmin_password_changed修改管理员密码
admin_my_logsadmin_my_logs我的操作日志
admin_all_logsadmin_all_logs全部操作日志
admin_lobby_playersadmin_lobby_players聊天室在线玩家
admin_lobby_messagesadmin_lobby_messages聊天室消息历史
admin_lobby_deletelobby_message_deleted删除聊天室消息
admin_lobby_bansystem / error封禁聊天室玩家
admin_lobby_announceroom_announce聊天室公告
admin_lobby_rate_limitlobby_rate_limit_info查询聊天室限流
admin_lobby_batch_deletelobby_message_deleted批量删除消息
admin_lobby_batch_ban—批量封禁聊天室玩家
admin_user_searchadmin_user_search_result搜索用户
admin_user_banbanned / opponent_banned / spectate_ended封禁用户
admin_user_unbanadmin_user_unban_result {ip,fp,player_id}解封用户
admin_user_list_bannedadmin_banned_list {records}封禁列表
community_postscommunity_posts社区帖子列表
community_post_detailcommunity_post_detail帖子详情
community_post_reviewcommunity_post_review_result审核帖子
community_post_batch_reviewcommunity_post_batch_review_result批量审核帖子
community_post_pincommunity_post_pin_result设置置顶
community_commentscommunity_comments评论列表
community_comment_reviewcommunity_comment_review_result审核评论
admin_user_get_tagsadmin_user_tags_result查询用户标签
admin_user_set_specialadmin_user_special_result设置特殊标签
admin_user_add_tagadmin_user_tag_added添加标签
admin_user_delete_tagadmin_user_tag_deleted删除标签
admin_bot_listadmin_bot_list_result {bots,total,page,page_size}BOT 列表
admin_bot_addadmin_bot_add_result {ok,bot|error}添加 BOT
admin_bot_set_statusadmin_bot_status_result {ok,id,status}切换 BOT 状态
admin_bot_deleteadmin_bot_delete_result {ok,id}删除 BOT
admin_announce_listadmin_announce_list_result公告列表
admin_bot_apply_listadmin_bot_apply_list_resultBOT 申请列表
admin_bot_apply_reviewadmin_bot_apply_review_result审核 BOT 申请
admin_soup_roomsadmin_soup_rooms海龟汤房间列表
admin_soup_dissolve—解散海龟汤房间
admin_soup_puzzlesadmin_soup_puzzles汤面(题库)列表
admin_soup_puzzle_status—更新汤面状态
admin_soup_puzzle_delete—删除汤面

未识别的类型返回 {"type":"error","message":"未知的管理消息类型: X"}。


9. Bot Gateway WebSocket ​

WebSocket 路径:/bot
Handler:BotGatewayWebSocketHandler

Bot Gateway 是开放给第三方 BOT 客户端的 WebSocket 网关,BOT 可以以独立身份加入聊天室。BOT 连接建立后需要先鉴权,鉴权成功后自动加入聊天室。

9.1 鉴权流程 ​

客户端 → 服务端: 连接 /bot
服务端 → 客户端: {"type":"bot_wait_auth"}
客户端 → 服务端: {"type":"bot_auth","player_id":"xx","key":"xx"}
服务端 → 客户端: {"type":"bot_auth_ok","nickname":"xx"} 或 {"type":"bot_error","message":"..."}

步骤说明:

  1. 客户端连接 ws://host/bot/
  2. 服务端返回 {"type":"bot_wait_auth"},等待鉴权
  3. 客户端发送鉴权消息 bot_auth
  4. 服务端校验 player_id + key 是否匹配 bot_list 表且状态启用
  5. 鉴权成功 → 自动以 BOT 身份加入聊天室 + 返回 bot_auth_ok
  6. 鉴权失败 → 返回 bot_error 并关闭连接

未鉴权时仅接受 bot_auth,其它消息一律返回 {"type":"bot_error","message":"请先发送鉴权消息 bot_auth"} 并关闭连接。

9.2 消息协议 ​

bot_auth(鉴权) ​

方向:C→S

json
{
  "type": "bot_auth",
  "player_id": "玩家ID",
  "key": "bot_key"
}
字段类型必填说明
player_idstring是绑定 BOT 的玩家 ID(从 BOT 面板获取)
keystring是BOT 鉴权密钥(从 BOT 面板获取)

bot_auth_ok(鉴权成功) ​

方向:S→C

json
{
  "type": "bot_auth_ok",
  "nickname": "我的机器人"
}
字段说明
nicknameBOT 在聊天室中的昵称

bot_wait_auth(等待鉴权) ​

方向:S→C

json
{"type": "bot_wait_auth"}

bot_error(鉴权失败) ​

方向:S→C

json
{"type": "bot_error", "message": "BOT 鉴权失败:玩家ID或KEY不正确"}

bot_ping / bot_pong(心跳) ​

方向:C→S → S→C

json
{"type": "bot_ping"}

响应:

json
{"type": "bot_pong"}

说明:BOT 客户端应每 30 秒发送一次 bot_ping 保持连接。

9.3 鉴权后的消息 ​

鉴权成功后,BOT 自动加入聊天室,所有消息直传聊天室(复用 LobbyChatWebSocketHandler 的全部能力)。BOT 发送的消息会自动携带 _bot 标记,前端据此识别为 BOT 消息。

BOT 可发送的消息类型:lobby_chat、lobby_sticker、lobby_nudge 等(与聊天室协议一致,见 2.2 节)。

注意:BOT 客户端无需发送 lobby_join,服务端在鉴权成功后已自动调用;客户端重复发送 lobby_join 会被忽略。


10. 临时聊天 TempChat WebSocket ​

WebSocket 路径:/ws/tempchat
Handler:TempChatWebSocketHandler
前缀:temp_

临时聊天(1v1 私密房)系统,支持邀请制、一次性房间、断线重连等功能。强制要求登录账号(无有效 token 直接拒绝),不支持游客。

10.1 连接建立 ​

连接建立后不发送额外消息,客户端需发送 temp_join 加入临时聊天。

10.2 消息协议 ​

temp_join(加入临时聊天) ​

方向:C→S

json
{
  "type": "temp_join",
  "nickname": "小明",
  "player_token": "eyJ..."
}
字段类型必填说明
nicknamestring否显示昵称(服务端以账号昵称为准),最多 12 字符
player_tokenstring是玩家 Token(必须登录,禁止游客)

说明:临时聊天强制要求登录账号,无有效 token 将被拒绝并关闭连接。

错误(temp_error 后关闭连接):临时聊天需要登录账号,请先登录、登录已失效,请重新登录、昵称无效,无法加入临时聊天。

响应:

json
{
  "type": "temp_joined",
  "nickname": "小明",
  "has_identity": true,
  "player_id": "player_xxx"
}

temp_search(搜索在线用户) ​

方向:C→S

json
{"type": "temp_search", "keyword": "小"}
字段类型必填说明
keywordstring是搜索关键词,最大 20 字符

响应:

json
{
  "type": "temp_search_result",
  "users": [
    {"player_id": "abc...", "nickname": "小红", "status": "online"}
  ]
}

temp_invite(发起邀请) ​

方向:C→S

json
{"type": "temp_invite", "target_player_id": "abc..."}
字段类型必填说明
target_player_idstring是目标玩家 ID

响应:

json
{"type": "temp_invite_sent", "ok": true, "invite_id": "iv_xxx"}

错误:

json
{"type": "temp_invite_sent", "ok": false, "error": "对方不在线"}
error 文案触发场景
缺少目标用户目标 ID 为空
不能邀请自己邀请自己
对方不在线目标不在线 / fd 失效
对方正在对局中,无法邀请目标状态 ingame
对方正在临时聊天中目标状态 busy

被邀请方收到:

json
{
  "type": "temp_invite",
  "invite_id": "iv_xxx",
  "from_name": "小明",
  "from_player_id": "abc...",
  "timeout": 60
}

说明:邀请有效期 60 秒,超时自动取消。超时后邀请方收到 {"type":"temp_invite_result","ok":false,"error":"邀请超时,对方未做出回应"},被邀请方收到 {"type":"temp_invite_expired","text":"邀请已过期"}。

temp_invite_resp(回应邀请) ​

方向:C→S

json
{"type": "temp_invite_resp", "invite_id": "iv_xxx", "accept": true}
字段类型必填说明
invite_idstring是邀请 ID
acceptbool是true=接受,false=拒绝

错误(temp_invite_result):邀请不存在或已过期、无权回应此邀请。

拒绝:

  • 邀请方收到:{"type":"temp_invite_result","ok":false,"error":"对方拒绝了您的邀请"}
  • 被邀请方收到:{"type":"temp_invite_dismissed"}

接受(双方进入房间):

邀请方收到(pending_join=true 表示邀请方不在临时聊天连接上,需跳转 /temp-chat 后发送 temp_rejoin 进房):

json
{
  "type": "temp_room_created",
  "room_id": "tr_xxx",
  "peer_name": "对方昵称",
  "peer_player_id": "对方ID",
  "pending_join": false
}

被邀请方收到:

json
{
  "type": "temp_room_created",
  "room_id": "tr_xxx",
  "peer_name": "对方昵称",
  "peer_player_id": "对方ID"
}

错误(temp_invite_result):对方状态已变化,无法进入。

temp_rejoin(断线重连) ​

方向:C→S

json
{
  "type": "temp_rejoin",
  "room_id": "tr_xxx",
  "nickname": "小明",
  "player_token": "eyJ..."
}
字段类型必填说明
room_idstring是房间 ID
nicknamestring否重连后显示的昵称
player_tokenstring是玩家 Token(必须匹配房间中的身份)

说明:断线 60 秒内可重连恢复。重连必须凭 token 匹配房间中的 a/b 一方身份,禁止纯昵称匹配(防冒认)。

响应:

json
{
  "type": "temp_room_created",
  "room_id": "tr_xxx",
  "peer_name": "对方昵称",
  "peer_player_id": "对方ID",
  "rejoined": true,
  "history": [{"sender_name":"小明","content":"你好","time":1730000000}]
}
字段说明
rejoined标记为重连恢复
history最近 50 条消息历史

错误(temp_closed):房间已关闭、房间校验失败。

对方收到:

json
{"type": "temp_system", "text": "小明 已重新连接"}

temp_history(加载更早消息) ​

方向:C→S

json
{"type": "temp_history", "offset": 50}
字段类型必填说明
offsetint是已加载条数(向上滚动加载更早消息)

响应:

json
{
  "type": "temp_history",
  "messages": [{"sender_name":"小明","content":"...","time":1730000000}],
  "offset": 75,
  "has_more": true
}
字段说明
has_more是否还有更多历史消息

temp_chat(发送消息) ​

方向:C→S

json
{
  "type": "temp_chat",
  "content": "你好!",
  "sticker_id": "表情ID",
  "sticker_url": "表情URL"
}
字段类型必填说明
contentstring条件文本内容,最大 1000 字符
sticker_idstring否表情 ID
sticker_urlstring否表情 URL(缺失时服务端按 sticker_id 解析)

说明:content、sticker_id、sticker_url 至少填一个。消息写入 Redis LIST(保留最近 500 条,TTL 7200 秒,房间关闭即删)。

广播(双方收到):

json
{
  "type": "temp_chat",
  "sender_name": "小明",
  "content": "你好!",
  "time": 1730000000,
  "player_id": "player_xxx",
  "avatar": "https://.../avatar.png",
  "sticker_id": "表情ID",
  "sticker_url": "表情URL"
}
字段说明
timeUnix 秒级时间戳
player_id / avatar仅账号玩家携带
sticker_id / sticker_url表情消息携带

若房间处于非 active 状态,返回 {"type":"temp_system","text":"房间不存在或已关闭"}。

get_stickers(获取表情) ​

方向:C→S

json
{"type":"get_stickers"}

响应:与主游戏/聊天室通用表情协议一致。

temp_exit(退出房间) ​

方向:C→S

json
{"type": "temp_exit"}

说明:主动退出房间,房间自动关闭。对方收到:

json
{"type": "temp_closed", "reason": "对方退出了房间,房间已自动关闭", "text": "对方退出了房间,房间已自动关闭"}

退出方自己收到:

json
{"type": "temp_closed", "reason": "已退出房间", "text": "已退出房间"}

temp_report(举报) ​

方向:C→S

json
{"type": "temp_report", "reason": "不当言论"}
字段类型必填说明
reasonstring是举报原因,最多 500 字符

响应:

json
{"type": "temp_report_result", "ok": true, "message": "举报已提交,将交由管理员审理"}

错误(temp_report_result):房间不存在、请填写举报原因。

temp_ping / temp_pong(心跳) ​

方向:C→S → S→C

json
{"type": "temp_ping"}

响应:

json
{"type": "temp_pong"}

10.3 服务器主动推送 ​

type说明关键字段
temp_joined加入成功nickname、has_identity、player_id
temp_search_result搜索结果users[]
temp_invite收到邀请invite_id、from_name、from_player_id、timeout
temp_invite_sent邀请发送结果ok、invite_id、error
temp_invite_result邀请处理结果ok、error
temp_invite_expired邀请已过期text
temp_invite_dismissed邀请已关闭—
temp_room_created进入房间/重连room_id、peer_name、peer_player_id、pending_join、rejoined、history[]
temp_chat聊天消息(广播)sender_name、content、sticker_id、sticker_url、time、player_id、avatar
temp_history历史消息加载messages[]、offset、has_more
temp_system系统提示text
temp_closed房间关闭reason、text
temp_report_result举报结果ok、message、error
temp_error错误提示text
temp_pong心跳响应—

10.4 断线处理 ​

  • 断线 60 秒内可重连恢复(通过 temp_rejoin);对方收到 {"type":"temp_system","text":"X 断开了连接,60秒内未重新连接将自动关闭房间"}
  • 重连时下发最近 50 条消息,更早的可向上滚动加载
  • 超时未重连自动关房(对方超时未重连,房间已自动关闭),消息不保存
  • 双方均离线时立即关房(双方均离线,房间已自动关闭)

附录:WebSocket 路径与 Handler 对照表 ​

各 Handler 源码均可在 GitHub 仓库 的 App/Core/WebSocket 目录查看。

WebSocket 路径Handler前缀说明源码
/wsGameWebSocketHandler无(兜底)"对面是AI吗" 主游戏GameWebSocketHandler.php
/ws/lobbyLobbyChatWebSocketHandlerlobby_大厅聊天室LobbyChatWebSocketHandler.php
/ws/board/gomokuBoardGame\GomokuHandlerboard_gomoku_五子棋(路径与前缀由 gameKey() 动态拼装)GomokuHandler.php
/ws/board/goBoardGame\GoHandlerboard_go_围棋(路径与前缀由 gameKey() 动态拼装)GoHandler.php
/ws/soupSoup\SoupWebSocketHandlersoup_海龟汤(Soup.Enabled)SoupWebSocketHandler.php
/ws/fateFate\FateWebSocketHandlerfate.缘分/默契测试(Fate.Enabled,实际复用 /ws 连接)FateWebSocketHandler.php
/ws/blockworldBlockWorld\BlockWorldWebSocketHandlerbw_2D 方块世界(BlockWorld.Enabled)BlockWorldWebSocketHandler.php
/ws/tempchatTempChatWebSocketHandlertemp_临时聊天(1v1 私密房)TempChatWebSocketHandler.php
/botBotGatewayWebSocketHandler—BOT 网关(开放给第三方 BOT,复用 lobby_*)BotGatewayWebSocketHandler.php
/{Admin.Path}/ws(默认 /admin/ws)Admin\AdminWebSocketHandleradmin_管理后台AdminWebSocketHandler.php

路由说明:onOpen 按请求路径匹配 Handler(未匹配时回退到 /ws);onMessage 按消息 type 前缀匹配,空前缀 Handler 作为兜底最后匹配。棋类前缀由 AbstractBoardGameHandler::gameKey() 动态拼装为 board_{gameKey}_,新增棋类只需实现 gameKey()。Admin.Path 由 Config/App.php 配置(默认 admin)。

所有端点共用的身份认证在 BaseGameHandler.php 中统一实现。

最近更新