- 01-identity:推荐自动匿名档案(首访 0 交互)+ Netflix 式切换器 + 配对码跨设备;passkey 因 LAN HTTP 不可用否决;审计出 7/8 游戏跨设备只 PUT 不 GET、大厅零 aria、玩家名不可朗读三个存量问题 - 02-input-parity:方向键/WASD+空格+回车(双击即时)键位(audiogames 社区惯例同构);语音双套资产路线(153 句含操作词,306 TTS/37min);MVP ~200 行 1-2 人日 - 03-data-sync:设备 token 即档案、localStorage 按 profileId 分桶(修同设备串档)、桶级 LWW + baseUpdatedAt 守卫、claim 可选认领 + PIN scrypt;公网前 8 条清单 - 00-synthesis:需求映射 + 架构收敛 + P0/P1 分期 + 3 个待拍板决策点
27 KiB
平台调研 01 · 无感多用户身份系统
课题:多个玩家(家庭 WiFi 手机/PC + 未来公网 game.xiangsilian.com)可区分、可保留各自进度与设置,且绝不能强迫注册。 调研日期:2026-09-09。方法:本仓库代码审计 + 业界模式检索(来源链接见 §3.6)。
0. 结论速览
主推:方案 B「自动匿名档案 + 设备档案切换器 + 配对码跨设备」——首次访问 0 输入 0 点击即自动建档(服务器静默发 token,玩家无感知),同设备多玩家用 Netflix 式档案切换,跨设备/找回用「旧设备出示 6 位配对码 → 新设备输入」;纯静态公网形态自动降级为方案 A(纯本地档案)。PIN 降级为可选的"认领"升级项(防兄弟恶作剧),passkey 留给公网化之后再评估(LAN HTTP 下技术上不可用,见 §3.3)。
三个待产品拍板的决策点见文末 §6。
1. 现状审计
1.1 服务器(web/server/server.js,310 行)
| 项 | 现状 | 评估 |
|---|---|---|
| 注册/登录 | POST /api/register {username, pin} / POST /api/login,PIN 4~6 位数字 |
PIN 明文存 users.json;用户名可中文(USERNAME_RE) |
| Token | 登录返回 24 字节随机 hex,内存 + sessions.json | 永不过期、无登出端点——客户端"退出"只是删 localStorage,token 在服务器侧永久有效 |
| 进度 API | GET/PUT /api/progress,Bearer token,data 自由格式但须带 summary(大厅卡片显示) |
接口形状健康,可复用 |
| 并发 | 按文件串行 Promise 队列,tmp+rename 原子写 | 健康,可复用 |
| 防护 | 无速率限制、无 IP 封禁、HTTP 明文 | HANDOVER §2.2 已自我标注"玩具级" |
数据模型(web/server/data/ 实测):
users.json { "uuu": "0858" } // username -> pin 明文
sessions.json { "<48位hex>": "uuu" } // token -> username,无过期时间
uuu.json { "_settings": {voice, speed, bgm, guide} } // 一用户一文件 {<game>: data}
1.2 客户端(web/index.html + 各游戏)
- 登录态:localStorage
soundgame.auth={username, token},大厅与 7 个游戏各自直读此 key。 - 游客态今天就能玩:不登录 → 全部 8 款游戏可玩,进度写各游戏自己的 localStorage key,设置写
soundgame.settings。服务器同步仅在登录时进行(PUT 静默跳过)。即"本地优先 + 可选账号"架构今天已经成立,只是"账号"这一半摩擦大(要起名+想 PIN+输两遍)且公网不可用。 - 公网形态(docs/deploy.md):Nginx 纯静态、不跑任何 API;大厅检测
https://*.xiangsilian.com即隐藏整个用户面板(index.htmlPUBLIC_STATIC)→ 公网访客全部是孤立的 localStorage 游客。deploy.md 已写明公网开放 API 的前置(127.0.0.1 绑定、只反代 /api/、limit_req、PIN 哈希、token 过期)。
localStorage key 全量清单(多档案方案的现实约束,见 §4):
| key | 归属 | 说明 |
|---|---|---|
soundgame.auth |
大厅+7游戏 | 登录 token |
soundgame.settings |
engine/settings.js | 四项全局设置 |
soundgame.progress.rpg / .dungeon / .radio |
各游戏 | 存档(命名不统一) |
soundgame.rhythm.progress |
rhythm | 存档 |
soundgame.simon.best / soundgame.space.best / soundgame.escape.stats |
各游戏 | 最高分/统计 |
soundgame.rpg.tutIntroSeen / soundgame.escape.guideDone / soundgame.space.drillDone |
各游戏 | 一次性教学标记 |
| (storyline 无本地 key) | — | 无本地持久化 |
1.3 审计发现的关键缺口(新方案必须解决)
- 跨设备恢复只做了 1/8:只有 radio 在"本地无存档 + 已登录"时 GET 服务器存档(radio/game.js:325-342);rpg/dungeon/rhythm/escape/simon/space 只 PUT 不 GET——已登录用户换设备,7 款游戏进度照样丢(服务器上有副本却不拉回)。
- 设置是"设备级"而非"玩家级":一台手机一个
soundgame.settings;家里视障玩家 2.2x 语速与普通玩家 1.0x 共用一部手机时互相覆盖(虽然登录后会拉_settings,但游客态无档案可分)。 - 大厅无读屏适配:index.html 零 aria 属性;登录表单是 JS 动态生成、仅 placeholder 无
<label>(读屏不友好);设置面板倒是用了正规<label for>+<select>。卡片是真实<a>,读屏可列出。→ 身份 UI 重做时必须补 ARIA 与可听化。 - "退出"不失效 token、sessions.json 无清理,长期只增不减。
1.4 语音技术栈对身份 UI 的约束(重要)
全仓库无 speechSynthesis 使用——所有语音都是离线 TTS 预生成音频资产(Qwen3-TTS 流水线),且设计规范禁止数字碎片拼接(数字按档位整句预生成)。推论:任意自定义玩家名无法被"说出来",档案名要么从预生成词表选、要么用预生成的"玩家一/二/三",要么为大厅引入浏览器 SpeechSynthesis(与整体录音质感不一致,需产品拍板,见 §6-D3)。
2. 业界模式调研发现
2.1 模式总表
| 模式 | 代表 | 首访摩擦 | 进度保留 | 换设备 | 启示 |
|---|---|---|---|---|---|
| 匿名设备账号(首访自动建档,后可升级) | Firebase Anonymous Auth、Unity Authentication、腾讯 CloudBase 匿名登录 | 0 | 云端 | 需升级/绑定 | 业界标准做法:UID 服务端生成、linkWithCredential 升级同 UID 数据不迁移;官方警告匿名账号丢失凭证即孤儿数据,建议 30 天不活跃清理;升级提示放在"账号开始有价值的那一刻" |
| 本地优先 + 可选账号链接 | Wordle(原版→NYT)、Poki | 0 | localStorage/cookie | 登录后云同步 | Wordle 原版全 localStorage;迁移 NYT 时靠"旧域名最后访问一次"携带本地数据;后加免费 NYT 账号才跨设备。Poki FAQ 明说:无账号进度存浏览器 cookie,建账号可跨设备。SoundGame 现状就是这个模式的前半段 |
| 档案切换器(一设备多档案) | Netflix、Nintendo Switch、Jellyfin/Emby 的诉求贴 | 选头像 1 击 | 每档案独立 | 档案级 | 家庭共设备场景的标准解;Switch 儿童档可选 PIN;自托管社区(Jellyfin #7059)反复索要同款 |
| Passkey 无用户名一键建档 | web.dev/Auth0/FIDO | 1 击+生物识别 | 云端 | iCloud/Google 密钥链自动同步 | 理论上最优雅;但见 §2.3 硬约束 |
| QR/配对码跨设备 | Jackbox(房间码+QR)、Google TV、WhatsApp Web | — | — | 扫码/输码 | "已有身份的设备给新设备发短期票据"是无邮箱场景的换设备通行做法 |
| 纯游客不留进度 | chess.com Guest Play | 0 | 不留(无 ELO 无历史) | 不支持 | 反面教材:我们的核心诉求就是"游客也要留进度",不采用 |
2.2 无邮箱无手机号的"找回"业界处理
- passkey 多设备同步:丢单台设备无忧(iCloud Keychain / Google Password Manager 同步);但全家桶全丢时,苹果恢复 iCloud 钥匙串本身可能要短信验证——没有邮箱/手机号时基本无解(sixcolors、Authsignal、r/Passkeys 讨论)。
- 恢复码:通行替代(一次性长码,抄在纸上)。对家庭游戏场景等价于"配对码的长期版"。
- 业界现实:小休闲产品(Wordle 时代、Poki 游客)干脆不支持找回,丢就丢;有价值的账号才给恢复通道。
- 对本项目的翻译:找回 = 「另一台仍登录的设备出示配对码」(Jackbox/WhatsApp 模式),加一层可选 PIN 认领兜底"旧设备已刷机"的情况;不做邮箱/手机号。
2.3 Passkey 的硬约束(决定性发现)
WebAuthn 要求 Secure Context(HTTPS 或 localhost)(W3C WebAuthn L3、MDN)。http://192.168.x.x:8000 这种局域网 IP + HTTP 不满足,navigator.credentials 直接为 null(Corbado、StackOverflow #55971593、mkcert #210)。家庭 WiFi 主路径(serve.sh 打印的局域网地址)天然用不了 passkey;变通(ngrok/mkcert 全家设备装 CA)对家庭用户不现实。另:本项目服务器零依赖原则下,WebAuthn 服务端验证(COSE/ES256)要么引入 @simplewebauthn/server(破例)要么手搓密码学(危险)。→ passkey 只能作为公网 HTTPS 域名下的可选增强,且建议推迟。
2.4 双形态(公网+内网)的安全底线
不需要银行级,但要"防邻居蹭网互删进度":
| 层 | 局域网形态(现状 HTTP) | 公网形态(未来 HTTPS) |
|---|---|---|
| 传输 | HTTP 明文 = 同 WiFi 者可嗅 token。威胁模型只能假设家庭 WiFi 可信(WPA2/3 密码不外传即可);要更强就得走公网域名(家庭内网也可访问 https://game.xiangsilian.com) | TLS,嗅探关闭 |
| 认证 | 匿名档案 token + 可选 PIN 认领(防"家人恶作剧互删"级别) | 同左 + 登录/配对端点限速 |
| Token | 加 TTL(滑动续期,如 180 天)+ 登出可吊销 | 同左 |
| PIN(若设置) | scrypt 哈希存储(node:crypto 自带,不破零依赖) | 同左 + limit_req(deploy.md 已规划) |
| 服务边界 | 服务器只绑 LAN | 绑 127.0.0.1,Nginx 只反代 /api/(deploy.md「以后若要开放用户系统」原文照做) |
deploy.md 已写的前置清单与本方案完全兼容——本方案把其中"PIN 改哈希、token 过期"消化进档案系统本身。
2.5 中文社区实践补充
前端圈通行做法与本报告方案一致:首次访问生成 UUID 存 localStorage(可 Cookie 双保险)作设备身份 → 服务器据此签发 token → 同一设备 ID 对应唯一匿名账号(腾讯 CloudBase 匿名登录文档明确"同一设备只能生成一个匿名用户、可后续绑定升级")。浏览器指纹(FingerprintJS)可作 localStorage 被清后的兜底,但隐私争议大且家庭场景收益低,不建议引入。
2.6 来源链接
- Firebase 匿名认证最佳实践:https://firebase.blog/posts/2023/07/best-practices-for-anonymous-authentication/
- Unity Authentication 匿名登录:https://docs.unity.com/en-us/authentication/approaches-to-authentication
- 腾讯 CloudBase 匿名登录(同设备唯一匿名账号):https://docs.cloudbase.net/http-api/auth/auth-sign-in-anonymously
- 前端设备唯一标识实践(掘金):https://juejin.cn/post/7640043551352635402;CSDN:https://blog.csdn.net/weixin_43242942/article/details/139881190
- Wordle 本地存储与 NYT 迁移:https://www.reddit.com/r/wordle/comments/1cszd9v/(含 localStorage 实证); NYT 关联统计报道见 CNET
- Poki FAQ(无账号=cookie 存进度):https://poki.com/en/c/faq;隐私声明:https://poki.com/en/c/privacy-statement
- chess.com Guest Play:https://www.chess.com/forum/view/site-feedback/playing-as-a-guest
- Netflix 档案切换:https://help.netflix.com/en/node/322532753336036;Jellyfin 多档案切换诉求:https://github.com/jellyfin/jellyfin-web/discussions/7059;UX 分析:https://medium.com/@mariajennings/reflection-point-patterns-and-flows-78acd5ab234c
- WebAuthn 去用户名化(discoverable credentials):https://stackoverflow.com/questions/73562080/;web.dev 注册指南:https://web.dev/articles/passkey-registration
- WebAuthn Secure Context 约束:W3C https://www.w3.org/TR/webauthn-3/;MDN https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer/create;LAN IP 不可用实证 https://www.corbado.com/blog/test-passkeys-localhost-ngrok、https://stackoverflow.com/questions/55971593/;mkcert 局域网讨论 https://github.com/FiloSottile/mkcert/issues/210
- Passkey 找回/全丢设备:https://www.authsignal.com/blog/articles/what-happens-when-your-passkey-device-is-lost-understanding-recovery-and-device-sync、https://sixcolors.com/link/2022/06/how-do-you-recover-passkeys-if-you-lose-all-your-devices/、https://www.reddit.com/r/Passkeys/comments/1faz0gt/、Apple https://support.apple.com/en-us/102195
- Jackbox 房间码/QR 第二屏:https://www.jackboxgames.com/how-to-play、https://www.jackboxgames.com/blog/how-to-play-party-pack-nine-remotely;TV QR 登录模式 https://www.cosmiclearn.com/googletv/tv-authentication.php;配对码设计 https://softwarewitchcraft.com/posts/designing-pairing-codes-tradeoffs-mistakes-simple-approach-that-works/
- QR 多端登录(streaming/passkey 场景):https://mojoauth.com/blog/multi-device-login-for-streaming-implementing-passkeys-across-ios-android-web-and-tv
3. 候选方案矩阵
维度说明:★ 越多越好(工作量列反着看:★ 越少越好=工作量越小)。"语音适配"= 视障用户可听、可手势操作的成本。
| 方案 | 首访摩擦 | 同设备多玩家 | 跨设备同玩家 | 隐私与安全 | 实现工作量(文件级) | 语音适配 |
|---|---|---|---|---|---|---|
| A. 纯本地档案(localStorage 多命名空间档案,无服务器身份) | ★★★★★ 0 击(自动"玩家一") | ★★★★★ | ✗(只能整档导出/导入码) | ★★★★★ 无攻击面 | ★(新 engine/profile.js ~150 行 + index.html 改面板;游戏零改动若走"换档=换 key 快照") | ★★★★(无配对旅程要念) |
| B. 自动匿名档案 + 切换器 + 配对码(首访静默 POST 建档拿 token;Netflix 式切换;跨设备=6 位配对码/QR;可选 PIN 认领) | ★★★★★ 0 输入 0 击 | ★★★★★ | ★★★★(配对码:需旧设备在手;PIN 认领兜底) | ★★★(token TTL+吊销、scrypt PIN;LAN HTTP 仍明文=家庭可信假设) | ★★(server.js +~180 行含配对/迁移;engine/profile.js ~250 行;index.html 面板重做;8 个游戏零改动,见 §4.3 换档走服务器倒灌) | ★★★(切换/配对语音线要新增台词资产) |
| C. 改良 PIN(自动建档但随机发 PIN 一次展示;保留现登录 UI 为跨设备主路径) | ★★★(首访弹出"抄下你的 PIN"本身是打断+认知负担) | ★★★(PIN 是档案口令,家人共用即互看) | ★★★★(名+PIN 哪都能登) | ★★★(须配 scrypt+限速,否则弱于 B) | ★★(server 改动小于 B,无配对子系统) | ★★★(输 PIN 数字键盘可听化) |
| D. Passkey 一键建档(WebAuthn discoverable credential,无用户名无密码) | ★★★★(1 击+生物识别,但要系统弹窗理解成本) | ★★★★(每档案一个 passkey) | ★★★★★(iCloud/Google 同步白送) | ★★★★★ | ★★★★★(服务端 COSE/ES256 验证:引依赖或手搓密码学;LAN HTTP 不可用,见 §2.3) | ★★★(系统弹窗有系统级旁白) |
| E. QR/配对码(独立小方案,只做"身份搬运") | — | — | ★★★★ | ★★★★(短期单次码) | ★(+~60 行) | ★★(QR 不可听,须配数字码朗读) |
关键否决/保留理由:
- D 否决为主方案:局域网 HTTP 私有 IP 下 WebAuthn API 直接不存在(§2.3),而家庭 WiFi 恰是主场景;零依赖约束下服务端实现成本最高。留作公网化之后的可选"认领"升级项。
- C 不推荐但保留 API 兼容:PIN 从"人人必经"降级为"想要跨设备找回的人可选设置",现有 username+PIN 端点在迁移窗口保留。
- E 不是独立方案,是 B 的跨设备旅程构件(A 若只做本地,也可后补导出码)。
4. 推荐方案(主推 B,A 为其离线/公网静态降级形态)
4.1 总体形态
┌─ 首访:0 输入 → 静默 POST /api/profile → token + "玩家一"
局域网(serve.sh)──┤─ 切换:大厅档案面板(本地保存该设备所有 {id,name,token})
└─ 跨设备:旧设备"出示配对码"(6 位数字,10 分钟单次)→ 新设备输入 → 拉全部进度
公网(Nginx)────── API 未开放期:自动降级为方案 A(纯本地档案,localStorage 快照式换档)
API 开放期(deploy.md 前置完成后):与局域网同一套 B
可选升级 ────────── "认领":给档案设 PIN(scrypt 哈希),用于名+PIN 跨设备登录(旧设备不在手时兜底)
离线/无 API 时 B→A 的降级是同一份 profile.js 代码的两种存储后端(服务器或 localStorage 快照),不是两套系统——现状 index.html 的 api() 已有"服务器不可达静默降级"基因,延续即可。
4.2 三个旅程(语音 + 手势描述)
本仓库手势语言:上/下/左/右滑动、单击(约 300ms 延迟)、双击;全项目 8 个统一 earcon(engine/guide.js 运行时合成)。大厅新增语音资产需走 TTS 流水线(台词表加进 tools/gen_voice_dual.py,男女双声)。
旅程 1 · 首次进入(0 感知建档)
- 打开大厅,无任何弹窗/表单。页面加载时后台静默建档(服务器形态拿 token;无服务器则纯本地)。
- 首次完成任一游戏存档后,大厅顶部档案区轻声播一句(可被任何触摸打断,遵守引擎语音链规则):「已创建玩家档案:玩家一。你可以在大厅顶部切换或重命名。」同时卡片刷新出进度摘要。不打断优先级高于这句闲聊的任何游戏语音。
- 视障路径:大厅是真实 DOM 按钮,TalkBack/VoiceOver 逐个可读;补
aria-live播报上述句子;卡片本身已可读(真实<a>)。
旅程 2 · 切换玩家(同设备,2 击)
- 大厅顶部常驻「玩家:〔玩家一〕」按钮(真实
<button>+aria-haspopup)。单击 → 展开档案列表(每行一个<button role="menuitemradio">:玩家一✓ / 玩家二 / +新玩家 / ⚙ 重命名·配对·设PIN)。 - 单击某档案 → 播确认 earcon(guide 词表的 success 音)→ 语音「已切换到玩家二」→ 下方游戏卡进度摘要刷新。
- 切换的数据搬运(详见 4.3):旧档案本地进度全部 PUT 上服务器 → 新档案全部进度 GET 倒灌回原有 localStorage key → 游戏代码零改动。离线(纯本地后端)则等价地在 localStorage 快照间搬运。
- 听觉化档案名:预生成「玩家一~玩家九」音 + 每档案一个签名 earcon(8 音词表外允许第 9 种"档案身份音",或复用音高变体),任意汉字名不朗读只显示(决策点 D3)。
旅程 3 · 换设备(配对码)
- 旧设备:档案面板 →「换设备 / 配对」→ 显示 6 位数字配对码 + QR(QR 给明眼人扫码用,同 LAN 下直开新设备页)。语音路径:逐位朗读数字(需预生成 0-9 单数字音——设计规范禁"数字碎片拼接"是对整句台词的;配对码逐位播报是系统级新场景,需设计侧裁一条豁免或用「第 x 位是 y」整句预生成,见决策点 D3 附注)。
- 新设备:大厅档案面板 →「我是老玩家」→ 数字小键盘(大按键、无 IME)逐位输入,单击选键、输入完自动校验;错误播 error earcon +「配对码不正确,请重试」。
- 成功 → 成功 earcon +「欢迎回来,玩家二」→ 新设备拿到 token,全部进度从服务器倒灌本地(顺带修复审计缺口 1:跨设备拉回从此对所有 8 款游戏生效,因为倒灌发生在大厅层而非各游戏内)。
- 兜底:旧设备已丢失且档案设过 PIN →「用玩家名+PIN 登录」(保留现有 login UI 为这条低频路径);没设 PIN 也没旧设备 → 接受丢失、重新开始(与 Wordle/Poki 游客同水位,写入产品预期管理文案)。
4.3 服务器数据模型变更(users.json v2 + tokens + pairings)
data/users.json(v2,启动时自动从 v1 迁移)
{
"u_a1b2c3": {
"id": "u_a1b2c3",
"name": "玩家一",
"kind": "device", // device=匿名档案 | claimed=已设PIN
"pinHash": null, // claimed: "scrypt$N$r$p$salt$hash"(node:crypto 自带)
"createdAt": "…", "lastSeenAt": "…"
},
"uuu": { "id": "uuu", "name": "uuu", "kind": "claimed", "pinHash": "scrypt$…(由明文 0858 迁移时哈希)", … }
}
data/tokens.json { "<hex>": { "uid": "u_a1b2c3", "exp": 1893456000, "lastUsedAt": … } } // 滑动 180 天
data/pairings.json { "483920": { "uid": "u_a1b2c3", "exp": …, "used": false } } // 10 分钟单次
data/<uid>.json { "<game>": data } // 结构不变;老用户 uid=旧用户名,文件零迁移
API 增删(server.js 内保持零依赖):
| 端点 | 说明 |
|---|---|
POST /api/profile(新) |
无参或 {name?} → 建匿名档案 + 返回 {token, profile}。配简单 per-IP 频控(如 10 个/天)防刷 |
GET /api/profile(新) |
当前 token 的档案信息 |
PUT /api/profile(新) |
改名 |
POST /api/profile/pin(新) |
设置/修改 PIN(kind→claimed,scrypt) |
POST /api/pair/start(新) |
已登录 → 生成 6 位配对码(10 分钟单次) |
POST /api/pair/redeem(新) |
{code} → 新 token(配对码换登录) |
POST /api/logout(新) |
吊销当前 token |
GET /api/progress / PUT /api/progress |
不变 |
POST /api/register / POST /api/login |
保留一个迁移窗口(等价于 claimed 档案),之后降级为内部专用 |
| 顺带修复 | sessions.json → tokens.json 迁移时全部给 180 天滑动过期;写入时惰性清理过期项 |
4.4 向后兼容与迁移(现有用户 = data/ 里的 1 个真实账号 "uuu")
- server.js 启动检测 users.json 为 v1(值是字符串)→ 一次性转 v2:uid 沿用旧用户名(
uuu.json进度文件零迁移),PIN 明文即刻转 scrypt,sessions token 全量搬入 tokens.json 并加 exp。 - 客户端
soundgame.auth旧结构{username, token}在 profile.js 初始化时识别 → 服务器换发(或直接沿用,token 仍有效)→ 包成单档案的档案表。老用户无感。 - 游客的 localStorage 进度:首次进入新档案系统时,把现有
soundgame.progress.*等本地 key 认领进首个自动档案(数据不丢,升级为档案数据)——这是 FirebaselinkWithCredential「UID 不变、数据原地升级」思想(§2.1)。 - 大厅
PUBLIC_STATIC分支保留:公网无 API 期 → 档案面板仍在但走纯本地后端(比今天"整个面板藏掉"更好)。
4.5 文件级改动清单
| 文件 | 改动 | 规模估计 |
|---|---|---|
web/server/server.js |
新端点(§4.3 表)、scrypt、token TTL/吊销、配对码、v1→v2 启动迁移、per-IP 简单频控 | +~180 行 |
web/engine/profile.js(新) |
档案表(本地 {id,name,token} 列表)、激活档案、换档搬运(flush 本地全部 key→PUT;GET→倒灌)、双后端(服务器/纯本地快照)、配对 UI 逻辑 | ~250 行 |
web/index.html |
登录面板 → 档案面板(列表+新玩家+重命名+配对+设PIN+名PIN登录折叠项);补 aria(动态表单加 label、aria-live 播报区);PUBLIC_STATIC 分支改为本地档案模式 |
~150 行重写 |
8 个 web/*/game.js |
零改动(换档搬运在大厅层完成;radio 自带的 GET 逻辑与新机制共存不冲突) | 0 |
web/engine/settings.js |
零改动(_settings 本来就走 progress API,档案化后自动变成玩家级设置) |
0 |
web/serve.sh / web/server/README.md / HANDOVER.md §2.2、§6 |
文档更新(玩具级警告改写为实际水位) | 小 |
tools/gen_voice_dual.py 台词表 |
新增大厅语音(sys_profile_created / switched / pair_code 逐位数字 / welcome_back 等,男女双声)+ 走一轮 TTS 流水线 | 若干条 |
docs/deploy.md |
「以后若要开放用户系统」小节更新为本方案公网路径 | 小 |
4.6 风险与缓解
| 风险 | 等级 | 缓解 |
|---|---|---|
| localStorage 被清/换浏览器 → 匿名档案孤儿(业界公认,Firebase 官方警告同款) | 中 | 引导时机学 Firebase:在"第一次想跨设备/第二次设 PIN 机会点"提示设 PIN 或配对,而非首访打断;服务器对 180 天不活跃 device 档案做归档清理(防 users.json 无限膨胀) |
| LAN HTTP 明文下 token 可被同 WiFi 者嗅探 → "邻居删进度" | 中 | 威胁模型写明:家庭 WiFi 密码可信即安全;要更强走 https://game.xiangsilian.com(内网也能访问公网域名)。认领 PIN 防的是"家人恶作剧"级别 |
| 换档搬运期间断网/杀页面 → 两档案数据交错 | 低 | 搬运顺序固定:先全部 flush 完成再切换;失败即中止切换并播 error;纯本地后端快照为原子 JSON |
| 一次性教学标记(tutIntroSeen 等 3 个 key)跨档案泄漏(换了档案教学不再重播) | 低 | 可接受(教学只教一次本就是产品取向);彻底修需游戏改动或 key 前缀化,列为后续可选项 |
| 配对码被念出来的体验(视障用户逐位听 6 位数字) | 中 | 数字音预生成 + "重复播报"手势(双击重听);或改 4 位词表码(每词一个预生成音)——设计侧裁决 |
| 任意汉字名无法朗读,视障家人无法"听出"谁在玩 | 中 | 每档案绑定签名 earcon + 固定编号音(玩家一~九可听),自定义名只作视觉显示(决策点 D3) |
| 匿名档案被脚本批量刷(公网开放后) | 低 | per-IP 频控 + limit_req(Nginx 层,deploy.md 已规划);家庭场景几乎无此风险 |
5. 备选方案说明
- 若决定完全不上服务器身份(公网永远纯静态):直接做方案 A。损失:跨设备、玩家级设置云同步、家庭共用 PC 时的云端备份;得到:零攻击面、实现最省。A 是 B 的真子集(profile.js 双后端中的本地后端),先做 A 再补 B 不返工——这也是把 A 设计为 B 的降级形态的原因。
- 若公网化后想上 passkey(D):在 claimed 档案上加第二凭据即可(passkey 与 PIN 并存),客户端改动集中在大厅,服务端需引入 WebAuthn 验证依赖或手搓(建议届时引入
@simplewebauthn/server并为服务器破一次零依赖例,或用 Nginx+auth 子服务隔离依赖)。届时 B 的配对码仍是兜底通道,两者不冲突。
6. 三个关键设计决策点(需产品拍板)
- D1 · 档案名怎么起:首访完全 0 感知(自动"玩家一",事后改名)还是首访点一次"起个名字/用默认"?(推荐前者:改名入口在档案面板,随时可改;起名不是注册,不该挡在玩游戏前面。)
- D2 · 跨设备找回的底线:只配对码(旧设备必须在手)够不够?是否同时保留"名+PIN 登录"作为旧设备丢失时的兜底(推荐保留,PIN 变为可选认领项);还是彻底极简、丢了就丢(Wordle 同水位)?
- D3 · 视障玩家的"名字可听化":自定义汉字名不朗读(只显示+签名 earcon+编号音)够不够?还是引入浏览器 SpeechSynthesis 念任意名(与全预生成语音的质感不一致、移动端质量参差)?附带:配对码逐位播报需要设计侧给"数字单音"或"第 x 位是 y"整句一个豁免裁定。