import './wb-legacy-disabled.mjs' /** * 按文案定位元素并导出其与父级的样式 —— 用于采集那些类名不稳定(CSS Module 哈希)的界面。 * * 用法:node scripts/wb-probe.mjs <文案1,文案2,...> [outJson] */ import { writeFile } from 'node:fs/promises' const port = Number(process.argv[2] ?? 9223) const texts = (process.argv[3] ?? '').split(',').map((s) => s.trim()).filter(Boolean) const outJson = process.argv[4] ?? null const PROPS = [ 'display', 'flex-direction', 'flex-wrap', 'align-items', 'justify-content', 'width', 'height', 'min-height', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left', 'margin-top', 'margin-bottom', 'gap', 'background-color', 'color', 'border-top-width', 'border-top-color', 'border-radius', 'font-size', 'font-weight', 'line-height', 'box-shadow', 'position' ] async function main() { const list = await (await fetch(`http://127.0.0.1:${port}/json/list`)).json() const page = list.filter((t) => t.type === 'page')[0] if (!page) throw new Error('未找到页面') const ws = new WebSocket(page.webSocketDebuggerUrl) await new Promise((r, j) => { const t = setTimeout(() => j(new Error('连接超时')), 8000) ws.addEventListener('open', () => { clearTimeout(t) r() }) ws.addEventListener('error', () => { clearTimeout(t) j(new Error('连接失败')) }) }) const send = (method, params = {}) => new Promise((res, rej) => { const id = Math.floor(Math.random() * 1e9) const timer = setTimeout(() => rej(new Error(`超时 ${method}`)), 25000) const h = (ev) => { const m = JSON.parse(ev.data) if (m.id === id) { clearTimeout(timer) ws.removeEventListener('message', h) res(m.result) } } ws.addEventListener('message', h) ws.send(JSON.stringify({ id, method, params })) }) const expr = `(() => { const TEXTS = ${JSON.stringify(texts)}; const PROPS = ${JSON.stringify(PROPS)}; const clean = (v) => (v == null ? '' : String(v).replace(/\\s+/g, ' ').trim()); const pick = (el) => { if (!el) return null; const cs = getComputedStyle(el); const o = {}; for (const p of PROPS) { const v = clean(cs.getPropertyValue(p)); if (v && v !== 'normal' && v !== 'auto' && v !== 'none' && v !== '0px' && v !== 'rgba(0, 0, 0, 0)') o[p] = v; } return o; }; const R = (el) => { if (!el) return null; const r = el.getBoundingClientRect(); return [Math.round(r.x), Math.round(r.y), Math.round(r.width), Math.round(r.height)]; }; const out = {}; for (const t of TEXTS) { let best = null; document.querySelectorAll('*').forEach((el) => { if (el.children.length !== 0) return; if (clean(el.textContent) !== t) return; const r = el.getBoundingClientRect(); if (r.width <= 0 || r.height <= 0) return; if (!best || r.width * r.height < best.getBoundingClientRect().width * best.getBoundingClientRect().height) best = el; }); if (!best) { out[t] = null; continue; } const p1 = best.parentElement; const p2 = p1?.parentElement ?? null; out[t] = { self: { tag: best.tagName.toLowerCase(), cls: clean(best.className).slice(0, 90), rect: R(best), st: pick(best) }, parent: p1 ? { tag: p1.tagName.toLowerCase(), cls: clean(p1.className).slice(0, 90), rect: R(p1), st: pick(p1) } : null, grand: p2 ? { tag: p2.tagName.toLowerCase(), cls: clean(p2.className).slice(0, 90), rect: R(p2), st: pick(p2) } : null }; } // 输入区 const inp = document.querySelector('textarea, [contenteditable="true"], input[type="text"]'); out.__input = inp ? { tag: inp.tagName.toLowerCase(), cls: clean(inp.className).slice(0, 90), rect: R(inp), placeholder: inp.getAttribute('placeholder') || inp.getAttribute('data-placeholder') || '', parent: inp.parentElement ? { cls: clean(inp.parentElement.className).slice(0, 90), rect: R(inp.parentElement), st: pick(inp.parentElement) } : null, grand: inp.parentElement?.parentElement ? { cls: clean(inp.parentElement.parentElement.className).slice(0, 90), rect: R(inp.parentElement.parentElement), st: pick(inp.parentElement.parentElement) } : null } : null; return JSON.stringify({ viewport: { w: innerWidth, h: innerHeight }, out }); })()` const r = await send('Runtime.evaluate', { expression: expr, returnByValue: true }) const data = JSON.parse(r.result.value) console.log(`viewport ${data.viewport.w}x${data.viewport.h}`) for (const [key, v] of Object.entries(data.out)) { console.log(`\n### ${key}`) if (!v) { console.log(' (未找到)') continue } if (key === '__input') { console.log(` ${v.tag}.${v.cls} ${JSON.stringify(v.rect)} placeholder="${v.placeholder}"`) if (v.parent) console.log(` ↑ .${v.parent.cls} ${JSON.stringify(v.parent.rect)} ${JSON.stringify(v.parent.st)}`) if (v.grand) console.log(` ↑↑ .${v.grand.cls} ${JSON.stringify(v.grand.rect)} ${JSON.stringify(v.grand.st)}`) continue } console.log(` 自身 ${v.self.tag}.${v.self.cls} ${JSON.stringify(v.self.rect)}`) console.log(` ${JSON.stringify(v.self.st)}`) if (v.parent) { console.log(` 父级 ${v.parent.tag}.${v.parent.cls} ${JSON.stringify(v.parent.rect)}`) console.log(` ${JSON.stringify(v.parent.st)}`) } if (v.grand) { console.log(` 祖父 ${v.grand.tag}.${v.grand.cls} ${JSON.stringify(v.grand.rect)}`) console.log(` ${JSON.stringify(v.grand.st)}`) } } if (outJson) { await writeFile(outJson, JSON.stringify(data, null, 2), 'utf8') console.log(`\n已写出 ${outJson}`) } ws.close() } main().catch((e) => { console.error('失败:', e.message) process.exit(1) })