- 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 个待拍板决策点
261 lines
27 KiB
Markdown
261 lines
27 KiB
Markdown
# 平台调研 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.html `PUBLIC_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. **跨设备恢复只做了 1/8**:只有 radio 在"本地无存档 + 已登录"时 GET 服务器存档(radio/game.js:325-342);rpg/dungeon/rhythm/escape/simon/space 只 PUT 不 GET——**已登录用户换设备,7 款游戏进度照样丢**(服务器上有副本却不拉回)。
|
||
2. **设置是"设备级"而非"玩家级"**:一台手机一个 `soundgame.settings`;家里视障玩家 2.2x 语速与普通玩家 1.0x 共用一部手机时互相覆盖(虽然登录后会拉 `_settings`,但游客态无档案可分)。
|
||
3. **大厅无读屏适配**:index.html **零 aria 属性**;登录表单是 JS 动态生成、仅 placeholder 无 `<label>`(读屏不友好);设置面板倒是用了正规 `<label for>+<select>`。卡片是真实 `<a>`,读屏可列出。→ 身份 UI 重做时必须补 ARIA 与可听化。
|
||
4. **"退出"不失效 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")
|
||
|
||
1. server.js 启动检测 users.json 为 v1(值是字符串)→ 一次性转 v2:uid 沿用旧用户名(`uuu.json` 进度文件零迁移),PIN 明文即刻转 scrypt,sessions token 全量搬入 tokens.json 并加 exp。
|
||
2. 客户端 `soundgame.auth` 旧结构 `{username, token}` 在 profile.js 初始化时识别 → 服务器换发(或直接沿用,token 仍有效)→ 包成单档案的档案表。老用户无感。
|
||
3. 游客的 localStorage 进度:首次进入新档案系统时,把现有 `soundgame.progress.*` 等本地 key **认领进首个自动档案**(数据不丢,升级为档案数据)——这是 Firebase `linkWithCredential`「UID 不变、数据原地升级」思想(§2.1)。
|
||
4. 大厅 `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"整句一个豁免裁定。
|