feat(platform/P0): 无感用户系统 + PC 键盘双端一致(两 agent 并行 + 集成收口)
用户系统(B,游戏零改动): - POST /api/device 首访 0 交互静默建档(自动玩家一/二…,per-IP 频控);断网降级纯本地照玩 - 大厅档案切换器(切换=flush 上云→快照回灌→倒灌→服务器回拉,profileId 分桶修同设备串档;aria menu 编号可听) - claim 可选认领(命名+scrypt PIN);旧 users/sessions 单向迁移(PIN 明文即刻哈希,已真实数据验证);login 兼容+claim-merge 桶级 LWW;logout;/server/ 静态封禁 - PUT baseUpdatedAt 守卫(legacy 放行+警告);GET 无参全桶回拉(修 7/8 游戏跨设备只 PUT 不 GET) - web/profiles.js 分桶纯函数 + api-smoke.mjs 7/7(建档/LWW/claim-merge/迁移/频控/封禁/分桶 H1-H4) 键盘双端(A,游戏代码零改动): - gesture.js 键盘通道 +137 行:←↑↓→/WASD=滑动、空格=单击(走 300ms 双击窗与触摸共享虚拟指针)、回车=即时 doubletap、按住空格 ≥450ms=长按;repeat 抑制/表单守卫/方向键 preventDefault - settings.input(auto/touch/key)+ gesture.inputMode();遮罩键盘解锁与文案自适应(按空格开始/轻触开始) - 8 游戏遮罩改用自适应默认文案;大厅设置面板「操作方式」+页脚键位提示 - live control.mjs 键盘注入通道(CDP dispatchKeyEvent);engine-input.test 61 断言 回归:12 套件全绿(8 playtest + guide + engine-voice + engine-input + api-smoke 7/7);8001 服务器已重启加载新 API,/api/device 端到端验证通过 P1 待排:153 句×双性 TTS 键盘语音变体 / 6 位配对码跨设备 / 认领 UI 打磨(research/platform/00-synthesis.md §3)
This commit is contained in:
@@ -5,6 +5,12 @@
|
||||
* 外部 agent 通过 HTTP 逐步玩任意一款 SoundGame:
|
||||
* POST /session {game, settings?, viewport?} 开新页(预注入探针 + 设置种子)
|
||||
* POST /gesture {type, x?, y?} 真实触摸手势(tap/doubletap/swipe四向/longpress)
|
||||
* POST /gesture {keyboard:true, key, holdMs?} 键盘手势(CDP Input.dispatchKeyEvent;
|
||||
* key=ArrowLeft|…|KeyW…|Space|Enter,
|
||||
* holdMs 默认 60,长按空格传 ≥450)
|
||||
* POST /gesture {type, keyboard:true} 同款手势类型走键盘(tap→空格快按、
|
||||
* doubletap→回车、longpress→空格按 600ms、
|
||||
* swipeup→↑…)——会话剧本加 keyboard:true 即可复跑
|
||||
* GET /events?since=N 增量事件(voice/fetch/震动/console/手势…)
|
||||
* GET /listen 增量转写判定(OK/TRUNCATED/MISMATCH/SILENCE/SFX_BGM_ONLY)
|
||||
* GET /transcript 合并时间轴全量(复盘用)
|
||||
@@ -45,6 +51,7 @@ function mapSettings(raw = {}) {
|
||||
const out = {};
|
||||
if (raw.voice !== undefined) out.voice = raw.voice; // male|female
|
||||
if (raw.guide !== undefined) out.guide = raw.guide; // full|standard|off
|
||||
if (raw.input !== undefined) out.input = raw.input; // auto|touch|key(提示文案输入模式)
|
||||
if (raw.speed !== undefined) {
|
||||
out.speed = typeof raw.speed === 'number' ? raw.speed : SPEED_MAP[raw.speed] ?? 1.0;
|
||||
}
|
||||
@@ -295,7 +302,58 @@ async function buildListenRows() {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 手势编排
|
||||
async function gesture({ type, x, y }) {
|
||||
// 键盘通道(engine/gesture.js 键位):key 用 KeyboardEvent.code 值。
|
||||
const KEY_DESC = {
|
||||
ArrowUp: { code: 'ArrowUp', key: 'ArrowUp', vk: 38 },
|
||||
ArrowDown: { code: 'ArrowDown', key: 'ArrowDown', vk: 40 },
|
||||
ArrowLeft: { code: 'ArrowLeft', key: 'ArrowLeft', vk: 37 },
|
||||
ArrowRight: { code: 'ArrowRight', key: 'ArrowRight', vk: 39 },
|
||||
KeyW: { code: 'KeyW', key: 'w', vk: 87 },
|
||||
KeyA: { code: 'KeyA', key: 'a', vk: 65 },
|
||||
KeyS: { code: 'KeyS', key: 's', vk: 83 },
|
||||
KeyD: { code: 'KeyD', key: 'd', vk: 68 },
|
||||
Space: { code: 'Space', key: ' ', vk: 32, text: ' ' },
|
||||
Enter: { code: 'Enter', key: 'Enter', vk: 13, text: '\r' },
|
||||
};
|
||||
// 触摸手势类型 → 键盘等价(gesture.js 键位表):type 走键盘时按此换算
|
||||
const TYPE_TO_KEY = {
|
||||
tap: { key: 'Space', holdMs: 60 },
|
||||
doubletap: { key: 'Enter', holdMs: 60 },
|
||||
longpress: { key: 'Space', holdMs: 600 },
|
||||
swipeup: { key: 'ArrowUp', holdMs: 60 },
|
||||
swipedown: { key: 'ArrowDown', holdMs: 60 },
|
||||
swipeleft: { key: 'ArrowLeft', holdMs: 60 },
|
||||
swiperight: { key: 'ArrowRight', holdMs: 60 },
|
||||
};
|
||||
|
||||
/** 键盘手势:CDP Input.dispatchKeyEvent(rawKeyDown 会让页面收不到 code 派生
|
||||
* 的完整 KeyboardEvent 特性,用 keyDown/keyUp 对),比 Runtime.evaluate 派发
|
||||
* 合成事件更拟真(走真实输入管线,含默认行为与焦点语义)。 */
|
||||
async function keyGesture({ key, type, holdMs }) {
|
||||
let desc = key ? KEY_DESC[key] : null;
|
||||
let hold = holdMs;
|
||||
if (!desc && type) {
|
||||
const m = TYPE_TO_KEY[type];
|
||||
if (!m) throw new Error(`未知键盘手势类型: ${type}`);
|
||||
desc = KEY_DESC[m.key];
|
||||
hold = hold ?? m.holdMs;
|
||||
}
|
||||
if (!desc) throw new Error(`未知按键: ${key}(可用 ${Object.keys(KEY_DESC).join('|')})`);
|
||||
hold = hold ?? 60;
|
||||
const label = key || type;
|
||||
// 先把动作本身记进页面时间轴(t=按键开始),附 keyboard/key 供审计区分通道
|
||||
await S.page.eval(`window.__live && window.__live.events.push({ev:'gesture', type:${JSON.stringify(label)}, keyboard:true, key:${JSON.stringify(desc.code)}, t:performance.now()})`).catch(() => {});
|
||||
const base = { key: desc.key, code: desc.code, windowsVirtualKeyCode: desc.vk, nativeVirtualKeyCode: desc.vk };
|
||||
await S.page.send('Input.dispatchKeyEvent', { type: 'keyDown', ...base, ...(desc.text ? { text: desc.text, unmodifiedText: desc.text } : {}) });
|
||||
await sleep(hold);
|
||||
await S.page.send('Input.dispatchKeyEvent', { type: 'keyUp', ...base });
|
||||
await sleep(400); // 与触摸手势一致:给游戏响应时间(空格单击有 ~300ms 双击窗延迟)
|
||||
await drain();
|
||||
}
|
||||
|
||||
async function gesture(body = {}) {
|
||||
if (body.keyboard) return keyGesture(body); // 键盘通道:{"keyboard":true,"key":...} 或 {type,keyboard:true}
|
||||
const { type, x, y } = body;
|
||||
const cx = x ?? Math.round(S.viewport.width / 2);
|
||||
const cy = y ?? Math.round(S.viewport.height / 2);
|
||||
const pt = (px, py) => [{ x: px, y: py, id: 1 }];
|
||||
@@ -395,7 +453,7 @@ async function writeArtifacts() {
|
||||
for (const e of S.events) {
|
||||
const t = e.ta;
|
||||
if (t == null) continue;
|
||||
if (e.ev === 'gesture') items.push([t, `**GESTURE** ${e.type} @(${e.x},${e.y})`]);
|
||||
if (e.ev === 'gesture') items.push([t, e.keyboard ? `**KEY** ${e.key || e.type}` : `**GESTURE** ${e.type} @(${e.x},${e.y})`]);
|
||||
else if (e.ev === 'vstart') items.push([t, `VOICE ▶ ${voiceIdOf(e.url)} (dur ${e.dur}s)`]);
|
||||
else if (e.ev === 'vend') items.push([t, `VOICE ■ ${voiceIdOf(e.url)} (${e.reason})`]);
|
||||
else if (e.ev === 'start' && e.layer !== 'voice') items.push([t, `${(e.layer || 'sfx').toUpperCase()} ♪ ${e.url ? voiceIdOf(e.url) : '?'}${e.loop ? ' (loop)' : ''}`]);
|
||||
|
||||
Reference in New Issue
Block a user