VibeHub 游戏后端 SDK
🤖 给 AI 用:开发前先读
https://vibe.lumigrav.space/llms.txt(索引)与/sdk/v3/llms-full.txt(高性能联机规范、红线)。给用户的可复制引导词见 docs/prompts.md。
给你的网页游戏接上 账号登录、三层数据、多人联机,无需自己搭后端。我们只提供通用基础设施——排行榜、背包、房间锁、段位匹配等具体功能,全由你用 SDK 在自己的代码里组合实现。
游戏托管在
vibeapps.lumigrav.space,账号与数据在 VibeHub 主站。SDK 帮你跨源打通,玩家用 VibeHub 账号一键登录。
引入 SDK(一行,必须用绝对地址——游戏页面在 vibeapps 域,相对路径 /sdk/... 会 404):
<script src="https://vibe.lumigrav.space/sdk/v3/vibehub.js"></script>
运行时可通过 VibeHub.version 读取完整内部版本;VibeHub.channel 可识别当前加载的是
Stable、Beta 还是未知来源。TypeScript 声明文件位于
https://vibe.lumigrav.space/sdk/v3/vibehub.d.ts。
SDK 版本与兼容策略
推荐作品使用稳定大版本地址 /sdk/v3/vibehub.js:
- v3 的 Bug 修复和向后兼容的新能力会自动生效,作品无需重新上传。
- 已打开的页面不会热更新,需要重新加载后才会取得新版 SDK。
/sdk/beta/vibehub.js是可能随时变化的测试通道,只有明确接受风险的诊断或测试作品 才应使用;普通作品不要加载 Beta。- 无版本路径
/sdk/vibehub.js、/sdk/vibehub.d.ts、/sdk/llms-full.txt不再提供; 每个作品必须在 URL 中明确选择 Major,查询参数不能代替版本路径。 - 只有不兼容升级才使用新的 Major 路径,例如
/sdk/v4/vibehub.js;v3 作品不会被 静默切换到 v4。
30 秒上手
const vibe = await VibeHub.init({ work: '你的作品slug' });
// 1. 登录
const user = await vibe.login(); // {id, name, image},弹窗授权,token 仅驻留当前页面内存
// 2. 三层数据
await vibe.save.set('progress', { level: 5 }); // 玩家自己
await vibe.save.get(["skin","settings"]); // 批量读取
await vibe.global.get('config'); // 全游戏(读)
const room = await vibe.room.join('room-1'); // 进房
await room.data.set('score', { a: 3, b: 1 }); // 单局共享(仅房主可写)
// 3. 联机(P2P)
room.onMessage((msg, fromId) => { /* 收消息 */ });
room.onPeer(({ type, id }) => { /* 'join' | 'leave' */ });
room.send({ x: 10 }); // 广播;room.send(obj, id) 发给指定人
账号登录与退出
SDK 只提供认证能力,不强行注入游戏 UI。游戏可以按自己的视觉风格放置登录、退出和账号状态;使用下面这些状态与 API 即可:
| API | 说明 |
|---|---|
await vibe.login() | 打开 VibeHub 登录/授权弹窗;成功返回 { id, name, image },名称和头像与主站公开个人资料一致 |
vibe.logout() | 同步退出当前作品的游戏账号,清除内存 token、当前用户、player 缓存和旧 SDK 遗留 token |
vibe.isLoggedIn() | 当前 SDK 实例是否已经完成登录 |
vibe.onAuthChange(callback) | 登录或退出时收到 user / null;返回取消监听函数 |
vibe.user | 当前内存中的用户;尚未登录时为 null |
vibe.logout() 不会退出 VibeHub 主站账号,也不会删除云存档。因此玩家再次登录时,如果主站会话仍有效,授权弹窗会继续使用该主站账号。它也不会自动退出当前联机房间;游戏应在收到 null 时停止需要登录的操作,并按玩法决定是否 room.leave()。
作品 token 只保存在当前 SDK 实例内,不写入 localStorage 或 sessionStorage。刷新作品页面后
需要再次授权;如果主站仍已登录,这通常只是一次很快的授权跳转。弹窗不可用时,授权页可把
token 通过 URL fragment 带回原作品,VibeHub.init() 会自动接收并立即清除 fragment。
SDK 不内置固定样式的账号组件,只提供上面的状态和方法。浏览器会直接下载
vibehub.js,因此开发者能查看带注释的源文件;这些注释是实现补充,不替代本文档和
vibehub.d.ts 所定义的公开 API。
无样式登录/退出 UI
下面是可以直接复用的最小框架。它只负责状态和事件,不包含 CSS:
<div data-vibe-auth>
<span data-vibe-auth-status aria-live="polite">未登录</span>
<button type="button" data-vibe-auth-login>登录</button>
<button type="button" data-vibe-auth-logout hidden>退出游戏账号</button>
</div>
<script type="module">
function mountVibeAuth(root, vibe) {
if (!root) throw new Error("找不到登录 UI 根元素");
const status = root.querySelector("[data-vibe-auth-status]");
const loginButton = root.querySelector("[data-vibe-auth-login]");
const logoutButton = root.querySelector("[data-vibe-auth-logout]");
if (!status || !loginButton || !logoutButton) {
throw new Error("登录 UI 缺少必要的 data-vibe-auth-* 元素");
}
function render(user) {
status.textContent = user ? `已登录:${user.name || "VibeHub 用户"}` : "未登录";
loginButton.hidden = Boolean(user);
logoutButton.hidden = !user;
loginButton.disabled = false;
logoutButton.disabled = false;
}
async function login() {
loginButton.disabled = true;
status.textContent = "登录中…";
try {
await vibe.login();
} catch (error) {
status.textContent = error instanceof Error ? error.message : "登录失败";
loginButton.disabled = false;
}
}
function logout() {
logoutButton.disabled = true;
vibe.logout();
}
loginButton.addEventListener("click", login);
logoutButton.addEventListener("click", logout);
const stopWatching = vibe.onAuthChange(render);
render(vibe.user);
// 页面或组件销毁时调用,避免重复绑定事件。
return function unmountVibeAuth() {
stopWatching();
loginButton.removeEventListener("click", login);
logoutButton.removeEventListener("click", logout);
};
}
const vibe = await window.VibeHub.init({ work: "你的作品slug" });
const unmountAuth = mountVibeAuth(document.querySelector("[data-vibe-auth]"), vibe);
// 不再使用这段 UI 时:unmountAuth();
</script>
原生 <button> 已支持键盘操作,aria-live="polite" 会播报登录状态变化。开发者只需用 CSS 选择 [data-vibe-auth]、[data-vibe-auth-login] 等属性添加样式。
核心概念:三层数据作用域
一张通用 KV 表,三个作用域。这是整套架构的基础。
| 层 | SDK | 存什么 | 写权限 | 读权限 | 生命周期 |
|---|---|---|---|---|---|
| 玩家 | vibe.save.* | 存档、皮肤、英雄、武器、设置、个人战绩 | 玩家本人 | 玩家本人 | 长期 |
| 单局 | room.data.* | 这一局的共享状态、房间元数据 | 房间 owner(房主/创建者) | 同作品且知道 roomId 的登录玩家 | 短期(默认 24h TTL,可自定义) |
| 全局 | vibe.global.* | 排行榜、配置表、全服统计、公告 | 作品创作者 | 所有玩家 | 长期 |
每个作用域的方法一致:get(key) / set(key, value) / all() / remove(key),可加 namespace 分组。
批量读取:vibe.save.get(["k1", "k2"]) 一次 HTTP 取多 key,返回 {k1: v1, k2: v2}。
客户端缓存:get() 走内置 LRU 缓存(256 条),重复读取同 key 不产生网络请求。set()/remove() 即时同步缓存。
⚠️ 版本更新注意:数据绑定"作品(slug)",跟代码版本无关——同一 slug 下更新游戏,数据保留。但你改了数据结构要自己做兼容(如
if (p && !p.v2) p = {...升级}),SDK 不自动迁移。换 slug 就是另一份数据。
对局实时状态不要写库
联机中高频变化的状态(比分、位置、出牌、画图)走 P2P 在玩家间同步,不写数据库。 数据库只存"需要跨玩家持久"的东西(排行、背包、房间元数据)。这样延迟低、服务器无压力。
玩家层能干什么
除了存档,vibe.save 适合做设置/偏好(音量、键位)、进度解锁(关卡、成就)、个人最高分/纪录、每日签到、背包养成、教程状态、联机战绩等——本质是"按玩家隔离、跨设备的 KV",比 localStorage 强在换设备不丢。
联机:两种拓扑 + VibeNet 中继
vibe.room.join(roomId, { topology: 'host' | 'mesh' }),默认 host。
拓扑对比
| host(房主权威,默认) | mesh(全互联) | |
|---|---|---|
| 连接 | 每人只连房主 | 两两全连 |
| 实时状态 | 房主演算并广播权威状态 | 各自广播、各自演算 |
| 权威 | 房主(状态一致、防作弊) | 无(人人平等) |
| 适合 | 棋牌、你画我猜、答题、有裁判 | 双人对打、协作绘图 |
| 规模 | 连接线性,易扩 | 连接平方增长 |
VibeNet 公共浏览器中继(自动降级,默认开启)
SDK 内置能力,游戏无需写任何中继代码。 以下行为全部由 SDK 自动完成。
游戏得到的效果:
- P2P 直连成功 → 最佳延迟,始终优先走直连
- 某条直连失败 → 自动切换到全局节点池中的主 relay
- 主 relay 失败 → 已连接暖备立即提升;P2P 恢复后短暂双发再切回直连
- 断线 → 自动重连(指数退避 1s→60s,最长等待 120s)
内部实现(SDK 自动处理,仅供参考):
- 主站和官方 SDK 页面在可运行时匿名贡献同一个全局节点池;登录、退出和 Room 生命周期不启停贡献
- 节点发现按剩余连接、总上行、总缓冲和软网络多样性做无放回随机轮换
- 稳态只有一个主路径发送业务,最多一个暖备仅保活与探测;切换窗口最多双路短暂发送
- Wire v2 携带
pathEpoch + streamId + uint32 sequence;接收端仅在 AES-GCM 验证成功后 提交有界去重窗口,因此损坏副本不会误杀后续有效副本 host拓扑由房主转发非房主之间的广播或定向消息
安全说明:
- Room 业务 payload 使用房间级 AES-GCM 加密与完整性校验;公共 relay 只能转发密文, 修改来源、目标、序号或 payload 都会被接收端拒绝。房内参与者共享该房间密钥,因此这不是 针对恶意房间成员的端到端隔离
- 匿名 relay 的短期凭证只用于防止伪造节点信令,不代表用户身份;它不能调用游戏数据 API
- 每条连接还必须持有绑定
relay + work + room + player的短期 grant;公共节点只能在该 连接所属房间内转发 SDK wire,不能访问 URL、TCP 服务或成为 VPN/任意网络代理 - 公共 relay 仍能看到连接双方的 IP/ICE 候选、流量大小与时序;“匿名节点”不等于网络匿名
新 API
// 玩家列表(relay 包含 primary / warm / candidate 角色)
const players = room.peers();
// [{id, open, latency, relay, role?, score?, reconnecting}]
// 当前自动传输状态,用于诊断 UI
const network = room.networkStats();
// {state, pathEpoch, primaryRelayId, warmRelayId, switching,
// lastSwitchReason, duplicateSuppressed}
// 手动重连
room.reconnect(peerId);
// onPeer 新增事件类型
room.onPeer(({type, id, active, reason, detail}) => {
// type: 'join' | 'leave' | 'connecting' | 'reconnecting' | 'relay' | 'error'
});
三人场景中,如果 A、B 都在 NAT 后,而 C 公网可达且网络良好,只要 A↔C、B↔C 都能建立 WebRTC,C 就能桥接两人,通常可以联机。但这不是 TURN 等价物:对称 NAT、 企业防火墙或 UDP/ICE 被禁时,A 或 B 仍可能连不上 C。需要覆盖这类极端双人网络时, 后续仍应部署 TURN 或平台自有 relay 服务。
批量读取 & 缓存
// 批量取多个 key(一次 HTTP)
const { skin, settings } = await vibe.save.get(["skin", "settings"]);
// 单次读取走客户端缓存(LRU 256 条),重复读无网络开销
const progress = await vibe.save.get("progress");
Room 数据 TTL
// room scope 默认 24h 过期,可自定义
await room.data.set("matchResult", { winner: 1, ts: Date.now() });
// TTL 2 小时
await room.data.set("tempConfig", { v: 2 }, { ttl: 7200 });
// 永不过期
await room.data.set("persistent", { v: 3 }, { ttl: 0 });
二进制消息
room.send(arrayBuffer); // 直接发 ArrayBuffer
// onMessage 收到的 payload 保持原始类型
Token 生命周期
- Token 2 小时有效,且只驻留当前页面内存。
- 遇到 401 时 SDK 会清除当前登录态、触发
onAuthChange(null)并抛出error.code === "AUTH_EXPIRED";SDK 不会在后台请求中突然弹窗。游戏应回到登录 UI, 由玩家点击后再次调用login(),需要时重新加入房间。
轻量状态同步与快照插值
// 小体量、最后写入胜出的共享状态;更新会自动广播。
room.state.on("phase", (value, previous) => renderPhase(value));
room.state.set("phase", "playing");
// 高频权威快照仍由游戏自己 send;sync 只负责本地缓冲与插值。
room.sync.push("world", { _t: Date.now(), x: 10, y: 20 });
const interpolated = room.sync.get("world");
room.sync.clear("world");
room.state 适合准备状态、回合阶段、低频开关,不适合每帧物理状态。room.sync
不产生网络请求,也不会自动发送快照;游戏收到 room.send() 的快照后再调用 push()。
匹配:房间发现
房间发现只读 room 层的元数据 + presence(这是 scope=room,不是全局层)。
// join 会原子认领一个尚无 owner 的隐藏房间;无需先 announce。
const room = await vibe.room.join("room-1");
// owner(房主/创建者)需要大厅可发现时再登记
await room.announce({ open: true, listed: true, max: 6, mode: '经典', /* 任意自定义字段 */ });
// 任何玩家列出开放的局
const rooms = await vibe.rooms.list(); // [{roomId, players, open, max, mode, ...}]
// 快速匹配:找第一个开放且未满的房间,返回 roomId;没有则 null(你再自建)
const roomId = await vibe.rooms.quickJoin();
const room = await vibe.room.join(roomId ?? 'room-' + Date.now());
presence 心跳由 SDK 自动维护(进房即开始),人数实时反映在 players 字段。
用积木搭高级功能(示例)
我们只提供上面的基础接口,下面这些由你在自己的代码里实现:
房间锁 / 密码房
// 房主:announce 时存一个密码哈希(或明文,MVP)
await room.announce({ listed: true, pass: '1234' });
// 加入者:先读元数据校验,再 join
const meta = await vibe.rooms.get(roomId);
if (meta.pass !== prompt('密码?')) return alert('密码错误');
await vibe.room.join(roomId);
私密房(不进大厅):announce({ listed: false }),只把 roomId 发给朋友。
段位匹配:quickJoin({ filter: r => Math.abs(r.mmr - myMmr) < 200 })。
全服排行榜:房主结算后写 room.data → 创作者定期汇总写 vibe.global.set('leaderboard', top),大家 vibe.global.get('leaderboard') 读。
踢人:host 拓扑里房主关闭与该玩家的 DataChannel 并广播。
一键复制给 AI 的集成指令
我的网页游戏托管在 VibeHub。请用 VibeHub SDK 实现登录/退出、三层数据和多人联机。
SDK 通过 <script src="https://vibe.lumigrav.space/sdk/v3/vibehub.js"></script> 引入,
全局对象 window.VibeHub。网络操作使用 async;logout/isLoggedIn/onAuthChange 是同步 API。
1) 初始化并登录:
const vibe = await VibeHub.init({ work: '我的作品slug' });
const user = await vibe.login(); // {id,name,image},弹窗授权;token 仅驻留当前页面内存
// 用户主动退出时调用 vibe.logout();它不会退出 VibeHub 主站
用 vibe.onAuthChange(user => {}) 更新账号 UI,至少提供登录和退出按钮。
2) 三层数据(方法一致:get/set/all/remove,可加 namespace):
vibe.save.* 玩家自己的数据(存档/皮肤/设置),本人读写
room.data.* 单局共享数据,仅房主可写、本局可读
vibe.global.* 全游戏数据(排行/配置),创作者写、所有人读
读取后要做版本兼容(如 if(p&&!p.v2) p={...}),SDK 不自动迁移。
批量读取:vibe.save.get(["skin","settings","progress"]) 一次取多个 key。
客户端缓存:get() 自动 LRU 缓存(256 条),重复读不产生网络请求。
Room 数据 TTL:room scope 写入默认 24h 过期,房间关闭后自动失效。
3) 联机(WebRTC P2P + VibeNet 全局浏览器中继,自动开启):
const room = await vibe.room.join(roomId, { topology: 'host' });
room.onMessage((msg, fromId) => {});
room.onPeer(({type,id,reason,detail}) => {}); // join|leave|connecting|reconnecting|relay|error
room.send(obj); room.send(obj, id); room.isHost; room.peers(); room.networkStats(); room.reconnect(id);
room.leave();
对局实时状态走 P2P 不写库;host 拓扑下玩家 send 输入、房主广播权威状态。
4) 匹配(房间发现,读 room 层元数据):
await room.announce({ open:true, listed:true, max:6, ...自定义 }); // 房主登记
const rooms = await vibe.rooms.list(); // 列出开放的局
const id = await vibe.rooms.quickJoin(); // 找未满的局,没有返回 null
房间号、密码锁、私密房、段位筛选都由我用这些自行实现。
请把登录、数据、联机接入我的游戏,UI 用简洁的深色风格。
限制与说明
- 单条 value ≤ 16KB(
GAME_KV_MAX_BYTES)。 - 单条 P2P 消息编码后 ≤ 65535 bytes;大文件应使用对象存储,不要塞入 DataChannel 消息。
- 联机默认 P2P(STUN 打洞)+ 全局匿名浏览器中继兜底。它能明显增加可用路径,但不保证穿透对称 NAT/严格防火墙,也不替代 TURN。
- Wire v2 最多转发两跳,按来源 + 路径代际 + 流 + 32 位序号在认证后去重;网络恢复时会优先回到直连。
- relay 评分在用户浏览器本地维护——同一节点对不同玩家可能有不同分数(取决网络状况)。
- 主站与官方 SDK 的匿名节点默认开启;注册只获得绑定临时 peerId 的短期能力凭证。
- presence 心跳约 20s 一次,45s 无心跳视为离线。
- host 拓扑中房主掉线影响全房;SDK 目前不会迁移权威状态,游戏应结束本局或自行重建新房间。
- Host 认领:第一个原子
claim成功的玩家成为稳定 owner/host;房间元数据过期后才能由新玩家接管。它不再依赖随机 peerId 比较。 - 直连与 relay 都走统一二进制 wire 协议;业务 payload 使用房间级 AES-GCM, 保证完整性并阻止公共 relay 读取;JSON、string、ArrayBuffer 和 Uint8Array 保持原类型。
- Room 数据 TTL:room scope 写入默认 24h 过期;可传
{ ttl: 秒数 }自定义,传 0 永久。过期数据自动失效(读取返回 null)。 - 批量读取:
vibe.save.get(["k1", "k2"])一次取多 key,减少 HTTP 往返。 - 客户端缓存:
get()走按作品/用户/房间隔离的 LRU 缓存(256 条);set()/remove()只在服务端成功后更新,登出清理当前用户的 player 缓存。 - 中继延迟约 20-100ms(比直连多一跳),对 FPS 等延迟敏感游戏建议客户端做插值缓冲。
认证与鉴权
- 换 token:
/api/game-auth要求主站登录会话,且作品 slug 必须真实存在——只有部署在 VibeHub 上的游戏才能签发 token。 - 调接口:
/api/sdk/*校验 HMAC 签名的 game token(绑定用户 + 作品,2 小时过期)。 - 写权限:player 层仅本人;room 层仅 owner;global 层仅作品创作者。读权限见上表。
- Room 读取边界:MVP 把随机/私密 roomId 当作 capability;服务端校验作品和 roomId,
但没有持久化成员名单。敏感数据仍不应写入
room.data。 - 跨源:仅
PROJECTS_ORIGIN可跨域调用。
MVP 边界:服务端校验 slug 存在,但未强制校验请求确实来自该 slug 页面。涉及排行榜/竞技奖励时会再加 origin↔slug 绑定校验。
API 参考(底层,一般用不到)
| 端点 | 方法 | 说明 |
|---|---|---|
/connect | 页面 | 授权页 |
/api/game-auth | POST | 主站会话换 game token |
/api/sdk/me | GET | 当前用户 |
/api/sdk/data?scope=&room=&ns=&key= | GET/PUT/DELETE | 三层 KV(scope=player/room/global) |
/api/sdk/rooms | GET/POST | 房间列表 / get·claim·announce·presence·close |
/api/sdk/signal | POST/GET | P2P 信令 |
/api/agent/deploy-status?project= | GET | Agent 查部署(Bearer API Token) |
/api/tags | GET | 所有标签与分类(公开,无需认证) |
/api/projects?q=&page= | GET | Agent 查找当前账号项目;更新时必须精确匹配 slug |
/api/upload/presign | POST | 获取新增/变化文件的 OSS POST 表单或本地 PUT 地址 |
/api/projects/:id | POST | 部署(deploy),body 可带 work/version 见下 |
deploy 时自动填表(POST /api/projects/:id)
除 files 外可携带,让 Agent 一次请求填好资料、无需再进后台:
work(可选):{ title, description, categoryId, tagIds, coverImage, screenshots }。创建时全量写入;更新时只改出现的字段。coverImage/screenshots填作品文件相对路径(须在本次 files 里),服务端拼完整 URL。玩法说明写进description(如## 玩法分段)。标签只能选/api/tags返回的现有标签。version(可选):{ note }。传了记一条更新日志,版本号平台自动递增(1,2,3...),只需写note。files中应携带预签名阶段使用的 SHA-256hash;部署成功后平台持久化 hash,下一次 更新只返回新增或变化文件。OSS multipart 文件段的 MIME 必须与uploadFields["Content-Type"]完全一致,文件字段最后添加。
Agent 复制指令见 skills/vibehub-publish/SKILL.md(或 GET /api/skill)。
官方 CLI:首次发布与更新
# 首次发布会创建项目
node cli/vibehub.js deploy --token <API_TOKEN> --name "作品名" --dir <部署根目录>
# 更新按当前账号下的 slug 精确查找,不会创建重复项目
node cli/vibehub.js update --token <API_TOKEN> --slug <项目slug> --dir <部署根目录> \
--note "本次更新说明"
两条命令都要求部署根目录顶层存在 index.html,并自动计算 SHA-256。更新会复用 presign
返回的 skipped、上传变化文件,并在 deploy 阶段提交完整文件清单;--name、
--description、--note 均为更新时的可选参数。
所有 /api/sdk/* 需 Authorization: Bearer <gameToken>,仅放行 PROJECTS_ORIGIN 跨源。
Agent 闭环调试:查询部署状态
开发游戏的 Agent 可用 API Token 轮询部署进度,自动判断"部署成功拿到 URL"或"失败拿到报错",从而闭环改代码重新部署。
curl -H "Authorization: Bearer <API_Token>" \
"https://vibe.lumigrav.space/api/agent/deploy-status?project=<slug>"
返回 { slug, projectStatus, deployed, url, progress, lastError, latestDeployment }。
progress:0 未部署 → 20 pending → 50 uploading/deploying → 100 成功或失败。
复制给 AI 的轮询指令:
部署我的游戏后,请轮询以下接口直到 progress=100:
GET https://vibe.lumigrav.space/api/agent/deploy-status?project=<slug>
请求头 Authorization: Bearer <API_Token>
若 deployed=true,把 url 给我;若 lastError 非空,读取 latestDeployment.logs
定位问题、修改代码后重新部署,再轮询,直到成功。