'use strict'; /** * 针对性诊断:键盘动作(S / R / #)为什么没生效。 * 做法:无头打开页面,把 app.js 里的内部状态(state.selected / state.open)与 * 当前焦点元素、toast 文案都直接读出来 —— 比猜快得多。 */ const { spawn } = require('node:child_process'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const URL_ = 'http://127.0.0.1:8788/'; const PORT = 9223; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); class Cdp { constructor(ws) { this.ws = ws; this.id = 0; this.pending = new Map(); this.errors = []; } static async connect(url) { const ws = new WebSocket(url); await new Promise((res, rej) => { ws.addEventListener('open', res, { once: true }); ws.addEventListener('error', () => rej(new Error('CDP connect failed')), { once: true }); }); const c = new Cdp(ws); ws.addEventListener('message', (ev) => { let m; try { m = JSON.parse(ev.data); } catch { return; } if (m.id && c.pending.has(m.id)) { const { resolve, reject } = c.pending.get(m.id); c.pending.delete(m.id); if (m.error) reject(new Error(m.error.message)); else resolve(m.result); } else if (m.method === 'Runtime.exceptionThrown') { const d = m.params.exceptionDetails; c.errors.push((d.exception && d.exception.description ? d.exception.description.split('\n')[0] : d.text)); } else if (m.method === 'Runtime.consoleAPICalled' && m.params.type === 'error') { c.errors.push('console.error: ' + m.params.args.map((a) => a.value || a.description || '').join(' ')); } }); return c; } send(method, params = {}) { const id = ++this.id; return new Promise((resolve, reject) => { this.pending.set(id, { resolve, reject }); this.ws.send(JSON.stringify({ id, method, params })); setTimeout(() => { if (this.pending.has(id)) { this.pending.delete(id); reject(new Error(method + ' timeout')); } }, 20000); }); } async eval(expression) { const r = await this.send('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true }); if (r.exceptionDetails) return { __err: r.exceptionDetails.text || 'eval error' }; return r.result ? r.result.value : undefined; } } (async () => { const edge = ['C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe', 'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe'].find((p) => fs.existsSync(p)); const profile = fs.mkdtempSync(path.join(os.tmpdir(), 'wpyw-diag-')); const child = spawn(edge, ['--headless=new', `--remote-debugging-port=${PORT}`, `--user-data-dir=${profile}`, '--no-first-run', '--disable-extensions', '--window-size=1600,1000', URL_], { stdio: 'ignore' }); let cdp = null; const out = []; const log = (s) => { out.push(s); process.stdout.write(s + '\n'); }; try { let target = null; for (let i = 0; i < 40 && !target; i++) { await sleep(400); try { const list = await (await fetch(`http://127.0.0.1:${PORT}/json/list`)).json(); target = list.find((t) => t.type === 'page' && t.webSocketDebuggerUrl); } catch { /* 等 */ } } cdp = await Cdp.connect(target.webSocketDebuggerUrl); await cdp.send('Runtime.enable'); await cdp.send('Page.navigate', { url: URL_ }); // 等界面渲染 let rows = 0; for (let i = 0; i < 40 && rows === 0; i++) { await sleep(500); rows = await cdp.eval('document.querySelectorAll(".row").length'); } log(`邮件行 = ${rows}`); // state 能不能从外部读到? const probe = await cdp.eval('typeof state'); log(`typeof state = ${probe}`); log(''); log('── 步骤 1:点击第一行 ──────────────────────────'); await cdp.eval('document.querySelector(".row").click()'); await sleep(1500); log(JSON.stringify(await cdp.eval(`({ selected: (typeof state !== 'undefined' ? state.selected : 'N/A'), openUid: (typeof state !== 'undefined' && state.open ? state.open.uid : null), activeTag: document.activeElement ? document.activeElement.tagName : null, activeId: document.activeElement ? document.activeElement.id : null, readerSubject: (document.querySelector('.reader__subject')||{}).textContent || '', toast: (document.getElementById('toastText')||{}).textContent || '', rowsNow: document.querySelectorAll('.row').length, })`))); log(''); log('── 步骤 2:派发 keydown "s" ────────────────────'); const uid = await cdp.eval('document.querySelector(".row")?.dataset.uid'); log(`目标 UID = ${uid}`); await cdp.eval(`document.dispatchEvent(new KeyboardEvent('keydown', { key: 's', bubbles: true }))`); await sleep(1800); log(JSON.stringify(await cdp.eval(`({ flaggedInState: (() => { const m = state.messages.find(x => String(x.uid) === '${uid}'); return m ? m.flagged : 'not-found'; })(), rowHasFlagClass: !!(document.querySelector('.row[data-uid="${uid}"]')?.classList.contains('is-flagged')), openFlagged: (state.open ? state.open.flagged : null), toast: (document.getElementById('toastText')||{}).textContent || '', })`))); log(''); log('── 步骤 3:派发 keydown "r" ────────────────────'); await cdp.eval(`document.dispatchEvent(new KeyboardEvent('keydown', { key: 'r', bubbles: true }))`); await sleep(800); log(JSON.stringify(await cdp.eval(`({ composeHidden: document.getElementById('compose').hidden, composeTitle: document.getElementById('composeTitle').textContent, openUid: (state.open ? state.open.uid : null), })`))); log(''); log('── 步骤 4:直接在 document 上监听,验证事件到底有没有到达 ──'); await cdp.eval(`(() => { window.__probe = []; document.addEventListener('keydown', (e) => window.__probe.push(e.key + '|' + (document.activeElement ? document.activeElement.tagName : '?')), true); return true; })()`); await cdp.eval(`document.dispatchEvent(new KeyboardEvent('keydown', { key: 's', bubbles: true }))`); await sleep(600); log('捕获到的 keydown:' + JSON.stringify(await cdp.eval('window.__probe'))); // 对比:真实用户按键(CDP Input.dispatchKeyEvent 走浏览器输入管线) log(''); log('── 步骤 5:用 CDP 真实键盘事件(Input.dispatchKeyEvent)──'); await cdp.eval('document.querySelector(".row").click()'); await sleep(1200); await cdp.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 's', code: 'KeyS', windowsVirtualKeyCode: 83, nativeVirtualKeyCode: 83, text: 's', unmodifiedText: 's' }); await cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 's', code: 'KeyS', windowsVirtualKeyCode: 83, nativeVirtualKeyCode: 83 }); await sleep(1800); log(JSON.stringify(await cdp.eval(`({ rowHasFlagClass: !!(document.querySelector('.row[data-uid="${uid}"]')?.classList.contains('is-flagged')), toast: (document.getElementById('toastText')||{}).textContent || '', })`))); log(''); log('运行时报错:' + JSON.stringify(cdp.errors.slice(0, 10))); } catch (err) { log('FATAL ' + (err && err.stack ? err.stack : err)); } finally { if (cdp) try { cdp.ws.close(); } catch { /* 忽略 */ } try { child.kill(); } catch { /* 忽略 */ } await sleep(400); try { fs.rmSync(profile, { recursive: true, force: true }); } catch { /* 忽略 */ } fs.writeFileSync('E:\\deepseek\\artifacts\\client-ui-diag.txt', out.join('\n'), 'utf8'); } })();