📚 games · 模组知识

HOOP 系统设计图纸 · 正文唯一真相是 docs/games.md;改 md 再跑 ./tools/deploy_docs.sh,这页是生成的

版本 04c293a4
2026-09-08 15:41
档案
module games
summary 游戏平台:一个游戏 = 后端 games 表一行 + 一个文件夹的 H5;上线 = 改 live_version 一个整数,文件按 v{N} 不可变、只由后端发号;发布成功就有测试版(固定 /test/),按 Build 才有正式版;工人交活不拿钥匙,靠「在这间房」+ 三道机器闸 + 基线冲突 409,平台机器人代推 git 再直写托管存储;window.hoop 由 App 注入(不在任何仓里下载),hoop-kit 是手感库、烘焙进 AI 游戏;影子 id g_ = HMAC 按游戏隔离(昵称 / 头像另有规则,不是全都不可关联);榜四道闸;对战房状态只在 Redis,平台只在「回合归属」「最终胜负」两处权威,2 人房双确认 4 秒兜底,断线不立即判负(普通模式 30 秒宽限后仍可能判负,live 模式不因断线判负),Redis 结算与库里落 matches 不是一笔事务;约战卡 = game_invite 消息 + 60 秒呼叫窗;没做 verified 签分、观战、多游戏房;promote 本身没有回滚闸,只有探活那条有条件的尝试。
updated 2026-09-08
verified_ref af743ff8bd9eee599a3168df5370c0284574f886
games_ref 2f022816a9726f21e16c31e403b89a04adcfdb6f
source_ref [backend/internal/domain/games, backend/internal/domain/room, backend/internal/domain/developer(发布 / 托管 / hoop-kit 那半), backend/internal/mcp/tools_game.go, backend/internal/mcp/tools_work.go(只引), backend/internal/domain/message/service.go(约战卡那段), app/lib/screens/game_webview_screen.dart, app/lib/screens/games_hub_screen.dart, app/lib/screens/game_.dart, app/lib/widgets/game_.dart, app/lib/widgets/active_match_bar.dart, hoop-games/lib/, hoop-games/deploy.sh]
status current
review_after 2026-12-07
owner nova
supersedes [GAME_PLATFORM_SPEC.md, GAME_KIT.md(§10 工坊除外 → rooms / studio), GAME_WORKERS.md, game_workers.html, PVP_GAME_INTEGRATION.md]
partially_supersedes [GAME_STAGING.md(吸收 §五 as-built 与 §六;§一〜四 留作历史), PVP_ROOMS.md(吸收 §0b 一人一房与 §1 中继式权威;§3〜§7 规格作废、§5 那 9 个不存在的 SDK 名要删), PVP_ROOM_INTERNALS.md(只引;改房间代码仍看它), GAME_DEV_KIT.md(它是 SDK 面的唯一出处,本文只指路), GAME_INVITE_STANDARD.md(吸收 §9 as-built), game_workroom.html(吸收 ③ ⑦ ⑬② 三段判据;⑪ ⑫ 归 AI_BOTS), PVP_TEST_PLAN.md(只摘 5 条必过)]

这份怎么读:每一节先讲「它解决什么、用户看到什么」,再讲「为什么这样做、数据在哪、哪一步会断、断了留下什么、有什么保障和缺口」;代码位置、参数、行号收在每段末尾的「出处」里。 出处标记:[code@af743ff8 文件:行] = HOOP 仓那棵树上的实现;[games@2f022816 文件:行] = hoop-games 仓那棵树(本文的检查器不核这个仓);[prod@日期,谁核] = 生产机当天核过;[decision@日期,谁] = 拍板过的决定。 边界:游戏工作房的生命周期、工作台页签归 rooms;绿能量 / 内购 / 分成的规则归 wallet(本文只写游戏摸到钱的那几个口);素材管线、工坊(Skills / Assets / Services)、开发者身份归 studio;小程序是姐妹平台,归 studio / apps。「房」这个字在 HOOP 里有三个意思:对战房(本文 §3.6,Redis 里的)、游戏工作房(rooms)、四场景社群房(community);Jeff 08-12 说过「我们可不是为游戏而做的」,别把三个混成一个。

0. 给接手的人:三分钟读懂游戏平台

先认六个词:游戏(game) = 后端 games 表一行(id、entry_url、两个版本指针、挂哪间工作房)+ 一个文件夹的 H5(index.html 自包含);两个指针 = staging_version(测试版)和 live_version(正式版),都是整数,文件按 v{N} 目录不可变存放,上线 = 改一个整数,不搬文件;影子身份 = 游戏拿到的玩家 id g_…,是 HMAC 算出来的,按游戏隔离,游戏拿不到真实账号 id(昵称 / 头像按第 4 条另算);桥(window.hoop) = App 的 WebView 在页面一开始就注进去的对象,H5 只认它,任何仓里都下载不到;对战房(room) = Redis 里的一个房间状态,平台只裁「轮到谁」和「谁赢了」,不跑游戏规则;交活(submit) = 工人把整个文件夹交给平台,平台过三道闸后替它 push git、再直写托管存储。

它保证到什么程度: 1. 版本号只由后端发,发号先于写文件:脚本那条路先 POST …/deploys 拿号,交活直写那条路在 PublishFolder 里直接调 allocDeployVersion —— 两条路各自发号,但都进同一个 Go 函数、同一把每游戏顾问锁;脚本没令牌当场停、绝不回落到数本地目录;内容指纹相同(source_hash)不铸新号。 2. 「对象留存」「入口可访问」「响应缓存」是三件事:留存 —— 清理只留最近 5 版,live、staging、任何 Build 不删;可访问 —— 托管端点只放行 live 或 staging 命中的 v{N},别的 v{N} 就算对象还在也回 404;缓存 —— .html 一律 60 秒优先,命中的 v{N} 非 html 一年 immutable,其余(含整个 test/)60 秒。探活 —— 每 5 分钟探入口,连续两次探不到就尝试退到别的可达 Build(候选是「不等于当前」的 Build 倒序取 5,可能选到更高的一版);候选全探不通 = 尝试发房内提示「没有别的 build 可退」+ ops 红报;查候选 / 开事务 / 改指针 / 提交任一步报错 = 直接返回,不回退、也不保证发房通知(进入尝试前那一次 ops 红报已发过);只探入口,不证明玩法正常。 3. 工人零钥匙:交活的票就是「我在这间房」;三道闸(范围 → 账本 CHANGELOG 必须动过 → 基线没被别人推走否则 409)全在服务端;目标文件夹只信库里 pages_folder,永远不信 owner 能改的 entry_url;push 用 force:falserefs/heads/master,撞车 409 不硬推。git 推成、存储写坏 = 这版发布未完成、可能有残留对象(见 §1.1 断点),代码没有重试。 4. 影子 id 按游戏隔离,昵称和头像不是:g_ + HMAC(每游戏派生密钥, gameID|realUID)[:24],游戏 token 15 分钟。launch 正常就把真实昵称 nameavatar_url 一起给;只有外部游戏(isExternal)且未同意、或同意状态查询报错时把 name 置空,avatar_url 照给(指向公开头像端点,走 games 作用域的 persona 头像流,和真身头像是否同一张未核);自研 / 平台创作者的游戏不弹同意。所以能借昵称跨游戏对人的口不止对战房的 opponent.name(§4)。 5. 榜四道闸:metric 格式、分数 ≤ 2^53、必须真启动过这个游戏、30 次 / 分;live 对战另有每秒涨分上限,超了静默丢弃。没有 verified 签分(games.secret 列 Go 里零引用)。 6. 对战只在两处权威:回合归属(回合门)、最终胜负(2 人房双方各报一次,4 秒等不到另一方按单方兜底;不一致 = 散局无胜负;多人房房主一报即结,否则取唯一最高票 —— 不要求过半,并列 = 散局)。房间状态只在 Redis,库里只落最终 matches,但 Redis 结算和落库不是一笔:抢到结算标记 → FinishSaveMatch 失败只记日志、不释放标记、照样发 room.ended 和改卡 —— 可能房已结束、卡已是记分板、库里没这局(补偿未核,§4)。断线不等于输:30 秒宽限且要「离线标记还在 + presence 没上线」两条都成立才判负;live 模式(g2048 / bubble / tetris / blockblast)永不因断线判负,3 分钟打完看分。 7. 一人一房(Jeff 07-08):进另一间房 = 旧房挂起,不是拒绝;leave 是挂起,forfeit 才是认输。

当前明确没做的(§4 全表,先说四条):verified 签分与 casual / verified 两档榜;观战(membership 是二元的,没有旁观者概念);多游戏协作房(一房一游戏唯一索引);批准 / 上线之后的「自动回滚」只有探活那一种,promote 本身没有回滚闸。

出了问题先查哪:§5 —— 「push 了手机上还是旧的」先分是 Pages 线还是托管线、再看是 test/ 60 秒缓存还是 v{N} 的一年缓存;「交活 422 / 409」按错误码对 §3.3 的表;「对战被判负 / 没结算」看 §3.6 的四个时钟。


1. 两段流程的一生

1.1 工人交一版 → 房里有测试版 → 按 Build 上线

你看到的:工作房里 AI 工人接卡、改游戏、交活;几秒后房里多一句「测试版已发 0.00N」,房里的人在工作台点「测试版」就能玩;满意了游戏主人(或房里的管理员 / 绑房的分身)按「Build」,正式版变成这一版,所有玩家重开游戏即新版。

背后发生的: 1. 取活:工人调 game_fetchGET /v1/workrooms/{cid}/game,先过「是不是这间房的有效成员」;读文件走 game_file_read…/game/raw/{path}(git master 那棵树的 blob 代理,≤ 8 MiB;没有 pages_folder 的游戏回 404 not_git_line)。工人没有 git 钥匙、没有部署令牌。 2. 交活 = 整个文件夹一次快照:game_submitPOST …/game/submit(30 MiB 上限)。三道闸:① 范围闸 —— 路径干净、扩展名白名单(.glb / .gltf 09-07 才加)、总量 20 MB、二进制按魔数验身(因为 Cloudflare Pages 对不存在的文件回「200 + 主页」,曾把三张「PNG」收成 index.html)、内容扫描(跨域 / 劫持)→ 各自 422;② 账本闸 —— CHANGELOG.md 必须在且 blob sha 必须和 HEAD 不同 → 422 no_changelog / stale_changelog;③ 基线闸 —— 第一次交随便,之后带的基线和 HEAD 不符 → 409 base_moved,提示里不许编造上一个提交人和时间。 3. 平台机器人代推:blobs → tree(仓里有、这次没交的文件会被删,因为交活是全量快照)→ commit 署「via HOOP platform submit, by 昵称」→ PATCH refs/heads/masterforce:false,撞车 409 race;目标文件夹从库里 pages_folder 取并再验一次 ^[a-z0-9][a-z0-9-]{0,63}$。 4. 直写托管存储(09-03 起):PublishFolder 把这个文件夹写进 hosted/{game}/v{N}/,写完读回 index.html 核一遍才动指针;内容指纹和当前测试版一样就不铸号;失败时 abort 尽力删已写对象(删错忽略),回 "cloud":"ci" + publish_error —— 这只是个标记,代码没有安排重试;CI 只在 hoop-games 仓 push master 时触发,触发凭据和是否真跑未核。「读回 index.html」只是 GetObject 成功即算,不比内容。第一次从 Pages 搬进托管线的游戏,先搬清单再切 entry_url,反了会 404。 5. 测试版就绪:staging_version = N,房里发系统消息;手机端「测试版」入口打 POST …/launch/staging,没权限的人后端静默回落正式版(不是 403)。 6. Build 上线:POST /v1/dev/games/{id}/promotelive_version = staging_version,写 game_promotions(kind 是算出来的:to ≥ from 是 promote,否则 rollback);谁能按 —— Jeff 09-08 定的规矩:游戏主人、这个工作室的管理员、房里的 AI,三种都能按测试版和正式版;代码现状(这棵树):owner / admin / 该游戏绑定工作房的现任成员分身(Vega 08-18 收紧,分身没绑房一律不能按)。Jeff 说「现在游戏里 admin 按不了」,李敏正让另一个窗口改;哪一层挡住了 admin 本文未核(§7)。 7. 玩家为什么重开即新版:正式版地址是 …/v{N}/,换版 = 换路径,老路径的一年缓存伤不到新版;换下来的 v{N}两个指针都不再指它托管源站才回 404(旧 live 仍是 staging 时照样能开),已缓存的副本客户端 / CDN 还可能回;Pages 线没有这道端点规则;App 的 WebView 又是 RELOAD_IGNORING_LOCAL_CACHE_DATA

哪一步会断、留下什么:第 1 步不在房 = 403;第 2 步任一闸 = 422 / 409,git 一个字节没动;第 3 步 GitHub 撞车 = 409 race,重取基线再交;第 4 步写存储失败 = git 已推、这版发布未完成、可能有残留对象(首发时 importPagesBuild 已搬进来的旧 live 对象也在),回 cloud: ci 但没人保证 CI 会补跑;第 5 步之前 test/ 都是上一版;第 6 步 admin 现在按不了(Jeff 09-08 说的,待另一个窗口修);promote 之后没有回滚闸,只有探活那条有条件的尝试(§3.2);Pages 线的游戏(按 CHANGELOG 09-03 的记录还剩 ghostsurvivors / hexgl / texas,此刻分布未核)走的是 deploy.sh 整仓快照,不走第 4 步。

出处:取活 / 读文件 [code@af743ff8 backend/internal/mcp/tools_game.go:14-35]、房成员门 [code@af743ff8 backend/internal/domain/games/service.go:212-219],raw 代理 [code@af743ff8 backend/internal/domain/games/gitfiles.go:208];三道闸 [code@af743ff8 backend/internal/domain/games/submit.go:42-43,84,100,109-154,179-193,206-209],基线判决 :305-316、提示不编造 :337-359、全量快照删文件 :466-471force:false :491-496、发布失败降级 :522-529;直写托管 [code@af743ff8 backend/internal/domain/developer/publish.go:30-66,73,80-139](abort 删残留 :88-90,读回只 GetObject :105-110);发号先于写 [code@af743ff8 backend/internal/domain/developer/developer.go:1574];staging 静默回落 [code@af743ff8 backend/internal/domain/games/service.go:337-339,382-386];promote / rollback developer.go:1841-1882,kind 派生 [code@af743ff8 backend/internal/domain/games/repository.go:619];发布权 [code@af743ff8 backend/internal/domain/developer/release_verdict_test.go:1];手机 staging 分流 [code@af743ff8 app/lib/api/api_client.dart:3790-3800],忽略本地缓存 [code@af743ff8 app/lib/screens/game_webview_screen.dart:1136-1145];守卫 baseline_test.gosubmit_folder_test.gosubmit_hint_test.gosubmit_glb_test.godeploy_race_test.go [decision@2026-08-11,Jeff:「满意了按一个按钮就变成 production,修改的时候不影响线上版本」] [decision@2026-08-15,Jeff:工人零钥匙、三道机器闸、平台代推;「每个游戏这样加我会死!」→ 主令牌] [decision@2026-09-02,Lisa:有代码就有测试版 0.001,按 Build 才有正式版,live 0 = 不上平台] [decision@2026-09-03,记录署名不一;2026-09-08 Jeff 语音重申:AI 自己交、必走托管线]

1.2 约一局对战:从聊天里的卡到结算

你看到的:聊天「+」选一个 PVP 游戏 → 群里出现一张约战卡(268×150),对方手机响 60 秒邀请铃 → 对方点「接受」直接进游戏 → 两人各自出招,回合制有 30 秒射钟,live 模式 3 分钟看分 → 打完卡变成记分板;中途谁断线,卡不会立刻判他输。

背后发生的: 1. 建房:room.create {game, max:2, conv_id} 走 WS。壳无条件把 game 覆盖成当前页面的 gameId(身份归壳不归页面);游戏名过 gamegate(60 秒缓存的可玩名单,库挂了用旧名单,冷启动 + 库挂拒绝);live 还是回合制由游戏 id 推断,不是客户端参数。房状态写 Redis room:{rid},TTL 2 小时。 2. :开放房(群里)= SendGameInviteGroup,定向(1 对 1)= SendGameInvite;都是 messages.type='game_invite',body 带 game / room_id / mode / max / joined / status / expires_at(2h,= 房 TTL) / ring_until(60s,= 占座窗,同一个常数引用不抄)。拉黑查两次(room 域发横幅前一次、message 域发卡前一次),因为「卡失败不阻止邀请」这条契约让单一处检查漏一条路。对方 WS 不在线才发 APNs,铃声 game_invite。 3. 进房:room.join 门禁在 Lua joinScript 里原子判(私房要在 rinv 名单里,09-02 评审后收紧);2 人房加入即自动开局(waiting → ready 只做一次,没有「点准备」)。手机端接受 = push 游戏页带 joinRoomId,壳调 __hoopAutoJoin(rid)。所有卡在房状态变化时整批重写(不是只改最新一张),广播 message.edited;有人进了就删 ring_until,卡不能再说「没人接」。 4. 对局:room.action(回合门,不是你的回合 → not_turn)/ room.endturn(snapshot) / room.say(不过回合门,10 次 / 秒);快照 shadow-keyed,只收自己那段 ≤ 2 KB;射钟 30 秒既挂进程内 AfterFunc 又写 durable ZSET,room.ping 每 5 秒带权威回合信息,H5 侧对不上就本地合成一条 room.turn{reconciled:true} 自愈。 5. 断线:房级心跳 12 秒没到 → markPeerGone → 30 秒宽限 → judgeGrace;判负前两条都要成立:离线标记还在 presence:online:{uid} 不存在。live 模式跳过这一整段,3 分钟到点看分,平局 = 散局。 6. 结算:2 人房各报 room.result,Lua 返回 pending / agree / dispute / already;4 秒等不到对方按单方;dispute = abandoned 无胜负;多人房房主一报即结、否则取唯一最高票(不要求过半)。收口 finalizeResult:FinalizeOnce 抢标记 → 取房失败 / Finish 失败会释放标记 → SaveMatchmatches 失败只记日志、不释放,接着照样 pushEnded + finishCard

哪一步会断、留下什么:第 1 步游戏名不在名单 = room.err;第 2 步被拉黑 = 横幅和卡都没有;第 3 步 not_invited / room_full / room_closed / not_found = 手机 toast 一次并把这个 rid 记进 rejected 不再重发;第 4 步超射钟 = 自动过回合;第 5 步宽限内回来 = 继续,leave 只是挂起;第 6 步双方都不报 = 房 2 小时 TTL 到期,卡跟着房走;第 6 步落库失败 = 房已结束、卡已是记分板、matches 没这局,补偿未核;手机端「回到对战」悬浮条数据全来自服务器 room.resume,没有本地猜

出处:身份归壳 [code@af743ff8 app/lib/screens/game_webview_screen.dart:930-943];gamegate [code@af743ff8 backend/internal/domain/room/gamegate.go:28,51-70];live 由 id 推断 [code@af743ff8 backend/internal/domain/room/manager.go:78-89],状态机 :32-45,TTL :48,占座窗 :459;卡 [code@af743ff8 backend/internal/domain/message/service.go:1807,1811-1836,1876,1925-1998],拉黑两处 [code@af743ff8 backend/internal/domain/room/ws.go:76-82,209-212];WS 全表 ws.go:137-715,join 门禁 Lua [code@af743ff8 backend/internal/domain/room/manager.go:27,174-181];时钟 ws.go:13-33,688-697,1076-1167;断线两条判据 ws.go:1268-1284;结算 ws.go:414-509,910-925,结算 Lua resultScript manager.go:232-241,finalizeResult 与落库失败不释放 ws.go:856-877,FinalizeOnce manager.go:561-570;手机卡 [code@af743ff8 app/lib/widgets/game_invite_card.dart:109-158,188-189,302-376],全局弹窗 [code@af743ff8 app/lib/widgets/game_invite_listener.dart:73-144,180-240],悬浮条只信服务器 [code@af743ff8 app/lib/widgets/active_match_bar_host.dart:4-8,59-98],拒绝码不重发 game_webview_screen.dart:830-862;守卫 room/invite_gate_test.goblock_gate_test.golive_test.gows_flow_test.gomessage/gameinvite_db_test.goapp/test/room_game_identity_test.dartgame_bridge_join_reject_0903_test.dart [decision@2026-07-08,Jeff:一人一房] [decision@2026-07-14,Jeff:邀请卡六条] [decision@2026-08-21,Jeff:「我也不可能邀请你两个小时」→ 60 秒呼叫窗] [decision@2026-09-02,评审 #6:私房要在 rinv 里才能进]


2. 用户能看到的能力(怎么用 / 限制 / 入口)

能力 怎么用 / 限制 后端入口 App 落点
游戏货架 只列 status='active' AND live_version>0;收藏架 + 最近在玩;快照先画、空列表不存 GET /v1/games/v1/games/mine games_hub_screen
玩一局 正式版永远 live;测试版入口没权限静默回落;外部游戏首次先问「分享资料给这个游戏?」,拒绝 = 昵称置空、头像照给;自研游戏不问 POST …/launch…/launch/staging game_webview_screengame_profile_consent_sheet
好友榜 / 全球榜(top 默认 50 上限 200);四道闸;日榜 48h 周榜 9d POST …/scoresGET …/leaderboard/* 底栏 🏆、game_stats_screen
我的战绩 单游戏三 tab(好友 / 世界 / 挑战);跨游戏形态在固定源码里没有入口接到(线上未核) GET …/records/mine/v1/games/stats/mine game_stats_screen
约战 聊天「+」只列 pvp=true 的游戏(不写死);定向 / 开放两种卡;群里开放卡故意没有「拒绝」 WS room.create / invite / join / decline / close game_invite_pickergame_invite_cardgame_invite_listener
回到对战 全局悬浮条,数据只来自 room.resume WS room.resume active_match_bar_host
云存档 每人每游戏一份 JSON ≤ 16 KB;data:null 不是错;读库失败 503 不给假空档 GET|PUT …/save hoop.save.*
商城 / 广告 / 库存 H5 只拿 {ok},买没买要再问 inventory;余额不够 → 充值页(wallet) …/shop*…/inventory…/ad game_shop_sheet
画作库(Pictionary) 投稿 / 看 / 猜 / 同桌 …/drawings* hoop.gallery.*
社交(赞 / 收藏 / 分享 / 评价) 原生底栏;H5 里的社交条只是本地计数(桥没有 social API) …/social…/like|favorite|share|review game_bottom_bar
出品方 看 / 关注 / 进主页 creator.* 桥 → 已有接口 game_webview_screen
工人取活交活 房成员才行;交活整包 ≤ 20 MB / 300 文件;三道闸 + 基线 GET …/game…/game/raw/*POST …/game/submit MCP game_fetch / game_file_read / game_submit
测试版 / Build / 退回 房里人玩测试版;Build 只 owner / admin / 绑房分身;退回只能到 is_build 版且先探路径 …/promote…/rollback…/deploys 工作台第 5 页(rooms)、hosting_workspace_screen(studio)
直发(CI / 脚本) 令牌口:发号 → 就绪 → 回执;409 = 别人抢先,不是失败 POST /v1/games/{id}/deploys…/{ver}/ready…/{ver}/ci…/publish-from-git hoop-games/deploy.sh
手感库 AI 游戏烘焙进去、手写游戏自己 <script src>;清单从源码解析,不手维护 GET /v1/games/kit hoop-games/lib/hoop-kit.js
H5 报错遥测 免鉴权,10 / 分 / IP,1 KB POST /v1/errlog hoop-games/lib/pvp-live.js

出处:路由 [code@af743ff8 backend/internal/domain/games/handler.go:48-76][code@af743ff8 backend/internal/domain/games/save.go:46-47,70-75][code@af743ff8 backend/internal/domain/games/social.go:278-283][code@af743ff8 backend/internal/domain/games/gallery.go:384-388][code@af743ff8 backend/internal/domain/games/stats.go:11-13,135];developer 路由 [code@af743ff8 backend/internal/domain/developer/developer.go:113-178];货架 [code@af743ff8 app/lib/screens/games_hub_screen.dart:154-158,383,557-561],选人面板不写死 [code@af743ff8 app/lib/widgets/game_invite_picker.dart:53-80],战绩跨游戏形态不可达 [code@af743ff8 app/lib/screens/game_stats_screen.dart:33-36]


3. 内部怎么运作

3.1 注册表、两条托管线、一个文件夹

用户看到的:货架上一个游戏 = 一张封面 + 一个名字(中英分列,不混);点进去是一个网页。

怎么做的:games 表一行(000055 起):id(短码,和文件夹名不一定相同:2048 → g2048block-blast → blockblast 是 5 个特例)、entry_urlstatus(active / draft / review / disabled)、live_version / staging_versionworkroom_id(一房一游戏唯一索引)、pages_folder(只有管理员能写,交活和发号都只信它)、deploy_tokencover_keypvp(000274 一次性回填 19 个,Go 里没有写路径,新 PVP 游戏不改 SQL 就不会出现在约战面板)、name_zh / name_en。文件两条线:托管线 = R2 hosted/{game}/v{N}/{path},域名 games.hoopcomm.com(CHANGELOG 09-03 记录当天 49 个 active 搬到这条线);Pages 线 = hoop-games.pages.dev/{folder}/v{N}//test/,由 deploy.sh 整仓快照上传(同一记录说剩 3 个)。这两个数是那天的盘点,此刻线上分布未核。H5 仓一个游戏 = 一个顶层文件夹 + index.html(部署脚本只认 depth-2 的 index.html,08-29 tools/ 折成幽灵游戏红了 13 次之后定的);仓里没有清单文件,注册是后端的事,deploy 时后端回 404 = 「这个文件夹不是游戏」。

出处:表 [code@af743ff8 backend/migrations/000055_games.up.sql:5-26],指针 000140 / 000205,pages_folder 000260 与 admin 写 [code@af743ff8 backend/internal/domain/developer/developer.go:2907-2932],pvp [code@af743ff8 backend/migrations/000274_games_pvp.up.sql:6-18],名字 000287 / 000338;两条线 developer.go:228-232[code@af743ff8 backend/internal/domain/developer/publish.go:30];H5 仓布局 [games@2f022816 deploy.sh:76-93],folder→id 只留一行 [games@2f022816 deploy.sh:147-155],404 = 不是游戏 [games@2f022816 deploy.sh:364-366];列表门 [code@af743ff8 backend/internal/domain/games/repository.go:234,273] [decision@2026-09-04,Jeff:「不可以有中英文混在一起」→ 000338]

3.2 发布与版本:两个整数、四种缓存、一条探活

用户看到的:发布成功后可以打开测试版(0.00N);按 Build 可把测试版设为正式版。正式版入口连续两次探测失败后,系统尝试切到其他可达 Build;没有可达候选时会尝试发一句房内提示。查询或事务失败可能直接返回,不能依赖一定收到房内通知;实际通知还取决于通知回调和工作房配置(say 在回调为空或房 / 开发者 id 为空时直接返回)。

怎么做的:版本号只由后端发(§0 第 1 条);test/固定别名不是 /v{N}/(08-30 定的,链接本身不变;能不能打开仍取决于 staging 指针和对象在不在);端点先判可访问:test/ 映射到 staging(staging 为 0 回 404),v{N} 只放行等于 live 或 staging 的那两版,其余 404;再定缓存:.html 一律 60 秒优先、命中的 v{N} 非 html 一年 immutable、其余(含整个 test/)60 秒、封面 3600;test/ 曾被打成 immutable 在边缘 HIT 了 8 小时,所以显式排除。Pages 线 _headersno-cache,靠边缘 304;但 09-07 发现 Cloudflare 边缘对 lib/*.js 存了一年 immutable 副本,几天不更新,当时用 ?v=8 绕过 —— 「推完重开即新版」在托管线的旧对象上不成立。清理:留最近 5 版,永不删 live / staging / is_build;deploy.sh 那边同样三道守卫(KEEP_LIVE / KEEP_STAGING / ALL 哨兵)且上传前先把云上的 live / staging / 每个 Build 按文件逐个核齐,缺就从 CDN 拉回来(「有 index.html ≠ 这一版完整」,sheep v26 少一张 PNG 差点半版上线)。探活:reconcile.go 每 5 分钟探入口,连续两次探不到 → 查「is_build 且版本不等于当前」倒序取 5 个候选、逐个探,退到第一个探得通的(可能比当前更高);候选全探不通 = 尝试发房内提示「连续 10 分钟探不到且没有别的 build 可退」+ ops 红报;查候选 / Begin / Exec / Commit 报错 = 直接返回,不回退、不保证房通知;staging 只喊不回滚。日出口流量 10 GB / 游戏 → 429。

出处:可访问与缓存 [code@af743ff8 backend/internal/domain/developer/developer.go:961-1009],egress :209,944-951,清理 :1019-1053;test/ 别名 [code@af743ff8 backend/internal/domain/games/service.go:263-316];探活与回退 [code@af743ff8 backend/internal/domain/developer/reconcile.go:9-14,114,178-221];H5 仓 _headers [games@2f022816 _headers:2-5],边缘死缓存 [games@2f022816 CHANGELOG.md:542-569],三道清理守卫 [games@2f022816 deploy.sh:376-436],逐文件核齐 [games@2f022816 deploy.sh:232-289],发号不回落 [games@2f022816 deploy.sh:142-143,352-372],守卫 [games@2f022816 guard_keep_builds.sh:21-37](没接进任何流水线);守卫 staging_url_test.goversion_label_test.godesk_versions_test.go [decision@2026-08-30,Jeff:Build 永不清理;「小改动只是 changelog 里的一条记录,而 build 是真正发出去的那一次」]

3.3 工人取活交活

见 §1.1。要点补三条:任务卡的取 / 报 / 收(work_task_*,queued → working → done|failed|cancelled,租约 5 分钟、3 分钟没心跳算陈)归 rooms,本文只引;两条流水线(交活直写 vs deploy.sh 整仓)HTTP 入口不同(直写在 PublishFolder 内调函数,脚本打 POST …/deploys),但都进同一个 allocDeployVersion 和同一把每游戏顾问锁;托管线以外的「hosted submit」是 501 not_yet 的桩,注释承诺的下一刀没来。

出处:[code@af743ff8 backend/internal/mcp/tools_work.go:69-195][code@af743ff8 backend/internal/domain/workroom/workroom.go:60,68,833-841,878-883];501 桩 [code@af743ff8 backend/internal/domain/games/submit.go:18,206-209]

3.4 桥、手感库、模拟器:三样东西别混

用户看到的:H5 游戏里能看到自己的头像和名字、能交分、能开商城、能约战;开发者在浏览器里也能跑起来。

怎么做的:(a) window.hoop 是桥,由 App 的 WebView 在 AT_DOCUMENT_START 注入(_hoopJs()),H5 调 callHandler('hoopCall', method, args),Dart 侧一个 switch 接住;room.* 一律直发 WS,_ws.reconnect / _conn.down 两条只在壳内;注入完成发 hoopready;分组新名(hoop.user / score / friends / share / ui / shop / ads / save / creator / room)和旧名完全等价,守卫扫源码比对 JS 名与 Dart switch,另一守卫比对 GAME_DEV_KIT.md。(b) hoop-kit.js 是手感库(v1.0.1,九个模块),零网络零导航,因为它是在安检之后烘焙进 AI 生成的 HTML 的,而安检是纯文本匹配(连注释都扫);源头在 hoop-games 仓 lib/,后端那份由 tools/sync_hoopkit.sh 生成,/v1/games/kit 的清单是从源码解析的;未知属性返回可链式的 no-op 并只警告一次(AI 会编 API)。(c) hoop-sim.js 是模拟器,window.hoop 已在就自动退位;hoop-compat.js 是名字垫片,只补缺、不覆盖、搬同一个函数对象(桥跟 TestFlight 走、游戏跟 Cloudflare 走,节奏对不上);pvp-live.js 是共用对战流程库(接新 live 游戏还要在后端 liveGames 加一行,漏了就按回合制跑永不结算);social-bar.js 是本地计数的社交条,App 画了原生的它就自隐。模拟器不是 1:1:room.declinecreator.*gallery.*shop.buy 都没有,注释说的「1:1 对齐」不成立。

出处:注入与 switch [code@af743ff8 app/lib/screens/game_webview_screen.dart:720-909,913-1108,1146-1167],room.* 直发 :930-943;守卫 app/test/game_bridge_method_names_test.dartsdk_doc_matches_bridge_test.dart;hoop-kit [code@af743ff8 backend/internal/domain/developer/hoop-kit.js:9-27,533-568],清单解析 [code@af743ff8 backend/internal/domain/developer/kit_manifest.go:11-31];H5 仓 [games@2f022816 lib/hoop-sim.js:23-47,132-136][games@2f022816 lib/hoop-compat.js:8-33,52-78][games@2f022816 lib/pvp-live.js:8-63][games@2f022816 lib/social-bar.js:24-32,373-387] [decision@2026-08-05,Jeff:kit A 案烘焙,「可以,建最牛的」] [decision@2026-08-21,Jeff:「游戏那边为什么还用旧名字 赶紧做」→ hoop-compat]

3.5 身份、榜、资料同意

怎么做的:影子 id 见 §0 第 4 条;游戏 token HS256 15 分钟(parseGameToken 没有调用方,留给未来网关);LaunchResult 正常带真实昵称 + avatar_url;ProfileSharingGate 回 (isExternal, consented):外部且未同意 → name=""NeedsProfileConsent=true,查询报错 → name="" 但不问(保守侧);同意记 consent_recordsgame:<id>重新 launch;拒绝 = 昵称置空、头像照给、下次再问;自研游戏不弹。榜在 Redis ZSET(lb:{game}:{metric}:{period}),好友榜读时现算 O(好友数)不预生成;四道闸见 §0 第 5 条;/v1/games/{id}/avatars/{guid} 公开,guid 本身就是能力令牌;/v1/games/stats/mine 故意不收 user_id 参数,否则成了去匿名化的神谕。头像流走 AvatarStreamer 接口注入。

出处:[code@af743ff8 backend/internal/domain/games/identity.go:16-18,39-42,66-67];昵称 / 头像与同意 [code@af743ff8 backend/internal/domain/games/service.go:408-425][code@af743ff8 backend/internal/domain/games/model.go:69][code@af743ff8 backend/internal/domain/games/repository.go:568]、手机 [code@af743ff8 app/lib/screens/game_webview_screen.dart:544-572][code@af743ff8 app/lib/widgets/game_profile_consent_sheet.dart:12-17];榜与闸 [code@af743ff8 backend/internal/domain/games/service.go:134-138,447-496,569-571];公开头像 [code@af743ff8 backend/internal/domain/games/handler.go:5-6,415-433];守卫 leaderboard_guard_test.gostats_test.go

3.6 对战房:状态在 Redis,权威只两处

见 §1.2。补四条:跨节点广播复用单一 hoop:events 频道全量扇出(按房分频道是次级欠账);控制流有进程内存(AfterFunc)+ durable ZSET 兜底,每 2 秒扫 500 条 / 类;room.dbg 是自称临时的调试通道,还挂着;ErrNotTurn 声明了没人返回,WS 层直接发字符串 not_turn。限流:say 10 / 秒、action 20 / 秒、create 15 / 分、invite 10 / 分。改房间代码看 PVP_ROOM_INTERNALS.md,本文不吞它。

出处:[code@af743ff8 backend/internal/domain/room/manager.go:1-8,25,719-755][code@af743ff8 backend/internal/domain/room/ws.go:13-33,710,1076-1127]

3.7 云存档与足球阵容

怎么做的:game_saves(user_id, game_id, data JSONB),16 KB 上限,只校验大小,不校验形状;save.go 的注释写 flicksoccer 是唯一在用的游戏(17 人)—— 那是写注释那天的盘点,不是本文核的生产事实。足球阵容「PvP 总点恒定 25 / 人」「成长只进 PvE」「钱只买个性」这些规则只在图纸里,后端没有任何校验(SOCCER_SQUAD.md 标 P3 待做);阵容同步靠 room.say({t:'sq'}),隐含依赖 say 10 / 秒且不过回合门。

出处:[code@af743ff8 backend/internal/domain/games/save.go:22,46-75],表 000139;[code@af743ff8 backend/migrations/000274_games_pvp.up.sql:16] [decision@2026-07-11,Jeff:「成长=个性,不是数值碾压」]

3.8 手机端壳:围栏、退避、缓存

怎么做的:单例壳(上一个实例的路由被 removeRoute);导航围栏纯函数 gameNavVerdict:同 host 且同第一段路径才放行,主框架的 data: / blob: 拒,外域主框架开系统浏览器;加载失败四条捕获 → 退避 1 / 3 / 6 秒后亮失败页,启动失败用 friendlyError 不给原始异常;返回键先判「页内能不能后退」(去 hash 后不同且同 host 同第一段)再退出;退出前 hoop.room.leave()(挂起,不是认输)。缓存:last_game_id、首帧底色、自绘返回键名单、货架快照 gameshub:all|mine(空不存)、战绩 / 分析按 key 分快照、game_names_v2(键带版本);兜底货架三款;没有指数退避,只有 WebView 那一处。心跳 5 秒 / 看门狗 12 秒 / 强制重连节流 8 秒全在注入的 JS 里。

出处:[code@af743ff8 app/lib/screens/game_webview_screen.dart:142-166,226,403-418,518-542,894-905,1171-1197,1447-1483,1819-1862];[code@af743ff8 app/lib/widgets/game_back_button.dart:34-66];[code@af743ff8 app/lib/widgets/game_load_error_view.dart:11-15];快照 [code@af743ff8 app/lib/screens/games_hub_screen.dart:154-158,383,417-437],[code@af743ff8 app/lib/util/game_names.dart:42-44];守卫 game_nav_fence_test.dartgame_load_error_golden_test.dartgame_back_in_page_0908_test.dart [decision@2026-09-04,Jeff:「我不要有原生的那个 back button」→ chrome({back:'own'})] [decision@2026-09-07,賈斯特:返回应回到原本的游戏页面]


4. 还没做的(不是现状)

状态 出处
verified 签分 / casual-verified 两档榜 未做;games.secret 列 Go 零引用 000055:10;GAME_PLATFORM_SPEC §3
观战 未做,membership 二元 room/ 全包无 spectator
多游戏协作房 一房一游戏唯一索引 000206;GAME_STAGING §六⑦
promote 之后的回滚闸 只有探活自动回滚;人工 rollback 只能到 Build reconcile.godeveloper.go:1841-1882
交活 git 已推、发布未完成 abort 尽力删残留(删错忽略),回 cloud:ci 标记,代码没有重试;CI 是否补跑未核 submit.go:522-529publish.go:80-139
结算落库失败 Redis 已结束、卡已改、matches 没这局;finalizeResult 这条分支不释放标记、没有补偿,别处有没有补偿待核 room/ws.go:856-877
探活回退 只在有可达候选时尝试,候选不限于更低版本;候选全探不通只尝试发房内提示;查询 / 事务报错直接返回 reconcile.go:178-221
新 PVP 游戏进约战面板 games.pvp 没有 Go 写路径,要手写 SQL 000274:6-8
hosted 线 submit 501 not_yet submit.go:206-209
影子身份被昵称削弱(M3)、token 走 URL(M2) 待做 PVP_ROOM_INTERNALS §8
房间状态改动非全原子(坎 2)、单一 hoop:events 全量扇出、sweeper 全实例重复扫 待做 PVP_ROOM_INTERNALS §5 / §7 / §9
云存档形状 / 足球点数校验 未做,只校 16 KB save.go:22;SOCCER_SQUAD P3
invite 桥接自家选人界面 仍回落系统分享,GameInvitePicker 存在但没接到桥上 game_webview_screen.dart:1020-1029
nav 只有 home / records;nextGame = 退出 其余 not_built game_webview_screen.dart:1030-1045
跨游戏战绩页、/v1/games/stats/mine 固定源码里唯一构造点永远传 gameId,入口没接到;线上未核 game_stats_screen.dart:33-36
guard_keep_builds.sh 接进流水线 没接,没人跑 hoop-games 根
45 个托管游戏的 live_version 指针陈旧 hoop-games CHANGELOG 09-07 记录:只切了 2 个,其余「知道那批怎么传的人来收尾」;此刻状态未核 hoop-games CHANGELOG:591-592
托管线 CSP img-src 挡出品方头像 两个选项写了没拍 hoop-games CHANGELOG:564-569
真机双端 PVP 验收(PVP_TEST_PLAN) 跑没跑没人回填 PVP_TEST_PLAN 抬头
待拍板(7 件,rooms / studio 侧):游戏房是否第七种房型、事件条算不算未读、房里的人能不能按上线、第二期换房、封面比例、谁能换封面、files 比上一版 未拍 game_workroom.html ⑨ ⑬
待拍板(games 侧):hosted_bundles 底座把 games 三个指针列迁走 未拍,一拍 GAME_STAGING 分工表作废 MINIAPP_PLATFORM §七.1

5. 出问题先查哪里 · 动它之前

排障一条路: 1. push 了手机还是旧的:先看后端日志有没有 publish_error(git 推成、发布没完成);再分线 —— 托管线看 entry_url 是不是 games.hoopcomm.com;再看指针(GET …/deploys/live)有没有动;没动 = 发号 / 就绪那步没走完(deploy.sh 只在 200 全绿后才 …/ready,409 = 别人抢先);动了还旧 = 缓存:.html 一律 60 秒优先、命中 live / staging 的 v{N} 非 html 一年(改版就是换路径,不该旧)、其余含 test/ 60 秒、lib/*.js 的边缘 immutable 副本要换地址。 2. 交活被拒:422 bad_path / bad_ext / too_large / bad_content / forbidden_content = 范围闸;422 no_changelog / stale_changelog = CHANGELOG 没动;422 no_base / 409 base_moved = 基线;409 race = GitHub 撞车;cloud:ci = git 已推、这版发布未完成、可能有残留对象。 3. 测试版看不到:不在那间房 → 后端静默回落正式版,不是报错;游戏没绑房 → 分身按不了 Build。 4. 对战被判负 / 没结算:看四个钟 —— 射钟 30 秒、心跳 12 秒、宽限 30 秒、结算等 4 秒;判负要「离线标记 + presence 不在线」两条;live 模式不判负。没结算 = 双方都没报 → 等 2 小时 TTL。卡是记分板但战绩里没有 = SaveMatch 失败(日志「落库战绩失败」),这条分支没有补偿,别处待核。 5. 约战卡没响:拉黑(两处)、ring_until 60 秒已过、对方 WS 在线就不发推送、被邀者正开着同一个游戏且在栈顶时不弹全局弹窗(交给游戏内)。 6. H5 说桥没有某方法:先分是桥(App 版本,跟 TestFlight)还是垫片 / 模拟器(hoop-games 仓);模拟器不是 1:1。

改它之前: - 动版本 / 发布:发号必须先于写文件(deploy_race_test);别在任何脚本里抄「文件夹 → id」表;test/ 永远不许 immutable;清理永不删 Build。 - 动交活闸:目标文件夹只信 pages_folder;提示不许编造提交人;.glb 那类新格式要同时进白名单和魔数表。 - 动桥:JS 名和 Dart switch 一起改(守卫扫源码);GAME_DEV_KIT.md 是 SDK 唯一出处,PVP_ROOMS §5 那 9 个名字是假的。 - 动 hoop-kit:改 hoop-games 仓 lib/hoop-kit.js,跑 tools/sync_hoopkit.sh,别手改后端那份;不许加网络 / 导航(安检扫注释)。 - 动对战房:先读 PVP_ROOM_INTERNALS.md;seq / snapshot / turn_deadline 只由各自原子操作维护;新 live 游戏要在 liveGames 加一行;games.pvp 要手写 SQL。 - 别信的旧图纸:GAME_PLATFORM_SPEC 的状态与 SDK 清单;GAME_STAGING §一 那张「两条路」表;PVP_ROOMS §5 SDK 合约;PVP_GAME_INTEGRATION 少 7 个方法;GAME_WORKERS §二 第三闸叫错(html 那份的「验身闸(魔数)」才对);GAME_KIT 抬头「待部署」与 §7 自相矛盾、「九模块」实为十一;发布权的规矩以 Jeff 09-08 语音为准(主人 / 工作室管理员 / AI 三种),代码现状是 08-18 Vega 那版,两边差在 admin 那格。


6. 不能碰的数(当前值 / 实现出处 / 守卫或未覆盖)

🔒 = Jeff / 评审明确锁定的;其余是普通配置值,改之前把守卫一起改。

当前值 实现出处(af743ff8) 守卫 / 未覆盖
一房一游戏 🔒 唯一索引 000206 DB 约束
一人一房 🔒 进新房 = 旧房挂起 room/ws.go:1044-1061 未覆盖
版本号只由后端发 🔒 发号先于写 developer.go:1574;hoop-games deploy.sh:352-356 deploy_race_test.go
Build 永不删 🔒 is_build 跳过清理 developer.go:1019-1053;deploy.sh:424-431 guard_keep_builds.sh(没接流水线)
可访问 + 缓存 只放行 live / staging 的 v{N};html 60s 优先 / 命中 v{N} 非 html 一年 / 其余 60s / 封面 3600 developer.go:961-1009cover.go:49 未覆盖
留几版 5 developer.go:1019 部分
探活 5 分钟、连续 2 次 reconcile.go:114 未覆盖
日出口流量 10 GB / 游戏 developer.go:209 未覆盖
交活上限 300 文件 / 20 MB;body 30 MiB;读文件 8 MiB submit.go:42-43,100;tools_game.go:14 未覆盖
榜四道闸 🔒 metric ^[a-z0-9_]{1,16}$、≤ 2^53、必须玩过、30 / 分 games/service.go:447-496 leaderboard_guard_test.go(含结构检查)
影子 id g_ + HMAC 24 位;token 15 分钟 identity.go:16-18,39-42 未覆盖
云存档 16 KB save.go:22 未覆盖(hoop-games _sim_save_test.py 只守模拟器)
「玩过」门槛 3 秒 games/repository.go:259 mygames_test.go
对战四个钟 🔒 射钟 30s / 心跳 12s / 宽限 30s / 结算等 4s room/ws.go:13-15,33 间接(ws_flow_test.goinvite_gate_test.go)
live 时长 / 涨分上限 180000 ms;400 / 350 / 1200 / 800 每秒 room/manager.go:81-102 live_test.go
占座 = 呼叫窗 🔒 60 秒,同一常数引用 room/manager.go:459message/service.go:1807 invite_gate_test.go:63
房 TTL 2 小时 room/manager.go:48 未覆盖
限流 say 10/s、action 20/s、create 15/min、invite 10/min、snapshot 2048 B room/ws.go:18-26 未覆盖
gamegate 缓存 60 秒 room/gamegate.go:28 gamegate_test.go
壳退避 1 / 3 / 6 秒 game_load_error_view.dart:11-15 game_load_error_golden_test.dart
壳心跳 5s / 12s / 8s / 900ms game_webview_screen.dart:894-905 未覆盖
约战卡 268×150,最多 4 行 game_invite_card.dart:188-189,472 golden gamecards_*
_forceLegacySocial 🔒 必须为空 game_webview_screen.dart:326 native_social_all_games_test.dart
errlog 10 / 分 / IP,1 KB errlog.go:25-27 未覆盖
hoop-kit 版本 1.0.1 developer/hoop-kit.js:27;hoop-games lib/hoop-kit.js:27 kit-test.html:63(hoop-games)

7. 出处与旧图纸去向

  • 代码(HOOP [code@af743ff8]):backend/internal/domain/games/{model,handler,service,repository,identity,submit,gitfiles,contentgate,gallery,social,save,stats,records,analytics,ad,cover,errlog}.go(19 个 _test.go)、backend/internal/domain/room/{manager,ws,gamegate,repository}.gobackend/internal/domain/developer/{developer,publish,reconcile,kit_manifest}.go + hoop-kit.js / hoop-compat.jsbackend/internal/mcp/{tools_game,tools_work}.gobackend/internal/domain/message/service.go(约战卡)、App lib/screens/{game_webview_screen,games_hub_screen,game_stats_screen,game_analytics_screen,game_shop_sheet,ai_game_builder_screen,game_revenue_screen,game_file_reader_screen}.dartlib/widgets/{game_invite_card,game_invite_listener,game_invite_picker,game_bottom_bar,game_switch_deck,game_back_button,game_load_error_view,game_profile_consent_sheet,game_sheet,active_match_bar_host,active_match_bar,game_social_pill}.dartlib/util/{game_names,game_title,invite_ringtone}.darthoop-games [games@2f022816]:deploy.shguard_keep_builds.sh_headers_redirects404.htmllib/{hoop-kit,hoop-sim,hoop-compat,pvp-live,social-bar}.js.github/workflows/deploy.yml
  • 迁移:000055 games / game_players、000056 / 000057 样板、000059 icon、000121 matches、000130 shop、000133 game_invite 类型、000134 画作、000136 社交、000139 云存档、000140 live_version / game_deploys、000166 广告、000174 会话、000184 / 000201 / 000202 / 000212 / 000257 / 000336 任务卡(rooms)、000205 / 000206 staging 与绑房、000209 部署令牌、000234 note、000250 cover_key(代码注释写 000247,不存在)、000260 pages_folder、000274 pvp、000285 草稿文件、000287 / 000338 中英名、000306 分享流、000315 is_build、000316 一次性清版本、000333 邀请卡索引、000337 source_hash。本模组最新 000338;仓库最新 000360。
  • Redis:榜 ZSET、房间全部状态(room: uroom: rinv: rres: rsnap: rsc: rdisc: rseen: rfinal: rheal: rsched:*)、gamegate 缓存、egress 计数、战绩册(stats.go 零迁移);任务卡 / 交活不用 Redis。
  • 生产:本文没有在生产机核任何项(49 个 active 是否真全在托管线、45 个陈旧指针现状、liveGames 名单、Caddy /hosted 转发)→ [prod@ 待核]
  • 决定(只列本文引到的):07-08 Jeff 一人一房;07-11 Jeff 足球「成长=个性」;07-14 Jeff 邀请卡六条;07-26 Jeff WebView 忽略本地缓存;08-05 Jeff kit A 案烘焙、源头在 hoop-games;08-11 Jeff 按一个按钮上线、房里人都能看测试版;08-12 Atlas 评审六条;08-13 Jeff 测试版按钮与正式版排在一起;08-15 Jeff 工人终局(零钥匙、三闸、代推、主令牌);08-18 Vega 发布权收紧、榜四道闸、身份归壳;08-21 Jeff 60 秒呼叫窗、hoop-compat;08-30 Jeff Build 永不清理;09-02 Lisa 全部版本化、live 0 = 不上平台;09-02 评审 #6 私房门禁;09-03 「交活只动这一个游戏」(署名冲突见下);09-03 Jeff「部署你以后不用问我」;09-04 Jeff chrome({back:'own'})、中英不混;09-07 Vega Master .glb;09-07 賈斯特 返回回页内;09-08 Jeff(语音):交游戏 —— 任何 AI 都可以在游戏工作室里自己交到测试版和正式版,但一定要走我们的托管线;上线 —— 游戏主人、这个工作室的管理员、AI 三种都能按(测试版和正式版都算)。
  • 未核清单:① 生产未核(见上);② 09-03 那句「改!」的署名不再追:Jeff 09-08 语音把规矩定成自己的(见决定);「现在游戏里 admin 按不了」是挡在后端 release_verdict 还是 App 那层,未核;③ PVP_TEST_PLAN 的真机双端到底跑没跑;④ liveGames 名单和 games.pvp=true 的 19 个是否一致;⑤ hoop-games 仓五个不同的「游戏总数」(50 / 53 / 55 / 60 / 70)对应此刻 58 个 index.html 目录中哪些算游戏;⑥ [games@…] 标记本文检查器不核,行号靠分身;⑦ 交活写存储失败后 CI 是否会补跑:平台代推确实是 PATCH refs/heads/master,deploy.yml 也是 push master 触发,但触发凭据 / Secrets / 实际跑成未核;⑧ 拒绝同意后 avatar_url 指向的 persona 头像与真身头像是否同一张;⑨ 结算落库失败有没有别处补偿。
  • 旧图纸去向:GAME_PLATFORM_SPEC.md 整份替代(四条铁律与影子身份公式并入 §0 / §3.5);GAME_KIT.md §1〜§9 替代(E2E 基准表留在原文供查),§10 工坊归 rooms / studio;GAME_WORKERS.md + game_workers.html 合成本文 §1.1 / §3.3(html 独有的四条真虫与主令牌已并入),两份只留一份;PVP_GAME_INTEGRATION.md 替代,骨架搬进 GAME_DEV_KIT.md §3;GAME_STAGING.md §五 / §六 并入 §3.2,§一〜四留作历史并标废「两条路」表;PVP_ROOMS.md 只留 §0b / §1 取向,§5 那 9 个名字要删;PVP_ROOM_INTERNALS.md 保持独立(改代码看它),顺手修 §2 rinv / rfinal 各出现两次;GAME_DEV_KIT.md 是 SDK 唯一出处,要改 §0 / §4 / §5 的「CF Pages + 找 Jeff 注册」为 publish-from-git、补 .glb;GAME_INVITE_STANDARD.md §9 并入 §1.2;game_workroom.html ③ ⑦ ⑬② 判据并入边界说明,⑪ ⑫ 归 AI_BOTS,⑬ 分刀表已全完成要标;PVP_TEST_PLAN.md 留在原处、抬头要回填;SOCCER_SQUAD.md 原样留,§2 改引本文;DEPLOY_PVP_20260708.md 已归档不吸收;00-目录.md 游戏段按 :265 约定该挂 ⚠️ 的至少五份。