Initial commit: WpywMail 桌面客户端:Node 零依赖本地服务 + React 19 / shadcn-ui 界面,支持收发信、注册、找回密码、会话与账号管理
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
'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');
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user