feat(platform/P1): 配对码跨设备 + 认领打磨 + 键盘语音双套资产(两 agent 并行 + 集成收口)

配对码与认领(B):
- POST /api/pair/start|join:6 位码 10 分钟单次有效、仅存内存零泄露面、5 连错 per-IP 封锁 10 分钟;join=新设备档案按桶 LWW 并入老档案+token 改绑(复用 claim-merge);日志脱敏
- 大厅:生成配对码(speechSynthesis 逐位朗读+aria-live+重听)/从其他设备转入(one-time-code 输入框)/从本机移除/401 过期自愈引导/aria 补全/玩家名可听化
- api-smoke 12/12(配对全流程/频控/封锁/过期自愈/移除/digitsToCn)+ CDP 真浏览器 E2E 22/22

键盘语音双套资产(A,路线 a):
- 164 变体句(8 游戏 grep 实测,超出调研预估 153:含轻触/点两下/按住变体)×2 性别=328 生成 656 文件 36.9 分钟;触屏主资产零字节改动(PCM 逐样本比对后还原)
- 引擎 _candidates 三跳路由 key→gender→平铺(inputMode 跟随);缓存键 mode|gender|id;engine-input 新增 K9 路由断言(72 条)
- check_all_assets 增 key 列(两性别一致且 ⊆ 主集)8 游戏全绿;voice_text.json 增 __key__ 表;live control.mjs key 会话对照键盘措辞验收(集成)
- voice-ids 文档:8 游戏键盘变体小节由 KEY_LINES 程序化生成(与台词表逐字断言一致);补齐 storyline/escape/simon 三个历史缺失文档;design README 增 new8 索引
- mock 适配:playtest persona 加 maxTouchPoints=1 保持触屏链断言语义零改动

回归:12 套件全绿(8 playtest + guide 122 + engine-voice 19 + engine-input 72 + api-smoke 12/12)
同批入库:批次 2/3 已完稿设计文档(17-abyss/18-detective/19-cipher 全套,20-22 在途)
This commit is contained in:
xsl
2026-09-10 00:43:23 +08:00
parent ede522dda1
commit 566241e05d
694 changed files with 4242 additions and 168 deletions
+32 -2
View File
@@ -1,10 +1,11 @@
/**
* profiles.js — 无感档案系统的大厅侧纯函数(P0-B)。
* profiles.js — 无感档案系统的大厅侧纯函数(P0-B + P1 配对码)。
*
* 被 web/index.html(UI / 网络编排)与 web/server/test/api-smoke.mjs(node 断言)
* 共用:只做 localStorage 形状计算,不碰 DOM / fetch——测试里传一个 Map 版
* storage({getItem,setItem,removeItem})即可。自动名汉字数字与 server.js 的
* cnNum 保持同一词表(玩家一~九可听,见 research/platform/01-identity.md §4.2)。
* cnNum 保持同一词表(玩家一~九可听,见 research/platform/01-identity.md §4.2);
* digitsToCn / removeProfile 为 P1 配对码与「从本机移除」的纯函数地基。
*
* 档案分桶模型(research/platform/03-data-sync.md §4.4):
* soundgame.profiles 本机档案表 [{id, name, token|null, claimed?}]
@@ -120,6 +121,35 @@ export function nextLocalProfileName(registry) {
return '玩家' + cnNum(registry.length + 1);
}
/**
* 配对码/数字串的逐位中文读法(P1 裁定:配置类数字不走 TTS 资产,交给
* 浏览器 speechSynthesis;0 必须读「零」而非字母 O,故自行映射)。
* '048920' → '零 四 八 九 二 零'(空格分隔,朗读端再替换为停顿)。
*/
export function digitsToCn(str) {
const d = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九'];
return String(str).split('').map((ch) => (ch >= '0' && ch <= '9' ? d[Number(ch)] : ch)).join(' ');
}
/**
* 「从本机移除(不删云端)」:清本机档案快照与同步表并出注册表;
* 若是当前档案则同时清 CURRENT(调用方负责善后:新建匿名或切换)。
* 服务器侧数据不动;token 是否吊销由调用方决定(UI 会 best-effort logout)。
*/
export function removeProfile(storage, id) {
const list = readRegistry(storage).filter((p) => p.id !== id);
writeRegistry(storage, list);
try {
storage.removeItem(snapshotKeyOf(id));
storage.removeItem(syncKeyOf(id));
if (storage.getItem(CURRENT_KEY) === id) {
storage.removeItem(CURRENT_KEY);
storage.removeItem(AUTH_KEY); // 是当前档案:游戏侧虚拟视图一并清掉(调用方重建)
}
} catch (_) { /* 隐私模式 */ }
return list;
}
// ---------------- 快照:回灌(切走)/ 倒灌(切入) ----------------
/** 回灌:把当前活动 key 原样存进档案快照。 */