171 lines
5.7 KiB
JavaScript
171 lines
5.7 KiB
JavaScript
import './wb-legacy-disabled.mjs'
|
|
|
|
/**
|
|
* 采集首页的垂直结构(各区块 y 坐标与尺寸)。
|
|
*
|
|
* 用法:node scripts/wb-home-layout.mjs <port> [outJson]
|
|
*/
|
|
|
|
import { writeFile } from 'node:fs/promises'
|
|
|
|
const port = Number(process.argv[2] ?? 9223)
|
|
const outJson = process.argv[3] ?? null
|
|
|
|
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 SEL = [
|
|
'.workbuddy-topbar',
|
|
'.conversation-sidebar',
|
|
'[class*="welcome"]',
|
|
'[class*="greeting"]',
|
|
'.wb-segmented',
|
|
'.quick-actions',
|
|
'.quick-actions__list',
|
|
'[class*="input-box"]',
|
|
'[class*="cr-input"]',
|
|
'.cr-input-container',
|
|
'.cr-input-editor-host',
|
|
'[class*="permission-setting"]',
|
|
'.cr-input-footer',
|
|
'[class*="suggestion"]',
|
|
'[class*="quick-prompt"]',
|
|
'img[class*="mascot"]',
|
|
'[class*="mascot"]',
|
|
'[class*="empty"]',
|
|
'[class*="scene"]'
|
|
];
|
|
const clean = (v) => (v == null ? '' : String(v).replace(/\\s+/g, ' ').trim());
|
|
const out = [];
|
|
for (const sel of SEL) {
|
|
let nodes = [];
|
|
try { nodes = [...document.querySelectorAll(sel)]; } catch { continue; }
|
|
if (!nodes.length) continue;
|
|
out.push({
|
|
sel,
|
|
count: nodes.length,
|
|
items: nodes.slice(0, 6).map((el) => {
|
|
const r = el.getBoundingClientRect();
|
|
const cs = getComputedStyle(el);
|
|
return {
|
|
cls: clean(el.className).slice(0, 90),
|
|
rect: [Math.round(r.x), Math.round(r.y), Math.round(r.width), Math.round(r.height)],
|
|
display: cs.display,
|
|
flexDirection: cs.flexDirection,
|
|
alignItems: cs.alignItems,
|
|
justifyContent: cs.justifyContent,
|
|
gap: cs.gap,
|
|
padding: cs.padding,
|
|
margin: cs.margin,
|
|
radius: cs.borderRadius,
|
|
bg: cs.backgroundColor,
|
|
shadow: cs.boxShadow === 'none' ? '' : cs.boxShadow,
|
|
fontSize: cs.fontSize,
|
|
fontWeight: cs.fontWeight
|
|
};
|
|
})
|
|
});
|
|
}
|
|
|
|
// 主内容容器的直接子元素序列(看垂直堆叠顺序)
|
|
const side = document.querySelector('.conversation-sidebar');
|
|
const sideRight = side ? side.getBoundingClientRect().right : 240;
|
|
let best = null, bestArea = 0;
|
|
document.querySelectorAll('div').forEach((el) => {
|
|
const r = el.getBoundingClientRect();
|
|
if (r.x >= sideRight - 4 && r.width > 500 && r.height > 300) {
|
|
const a = r.width * r.height;
|
|
if (a > bestArea) { bestArea = a; best = el; }
|
|
}
|
|
});
|
|
let stack = [];
|
|
if (best) {
|
|
const walk = (el, depth, path) => {
|
|
if (depth > 4 || stack.length > 60) return;
|
|
[...el.children].forEach((c, i) => {
|
|
const r = c.getBoundingClientRect();
|
|
if (r.width < 60 || r.height < 8) return;
|
|
stack.push({
|
|
depth,
|
|
path: path + '/' + i,
|
|
tag: c.tagName.toLowerCase(),
|
|
cls: clean(c.className).slice(0, 70),
|
|
rect: [Math.round(r.x), Math.round(r.y), Math.round(r.width), Math.round(r.height)],
|
|
text: clean(c.textContent).slice(0, 40)
|
|
});
|
|
walk(c, depth + 1, path + '/' + i);
|
|
});
|
|
};
|
|
walk(best, 0, '');
|
|
}
|
|
|
|
return JSON.stringify({ viewport: { w: innerWidth, h: innerHeight }, out, stack });
|
|
})()`
|
|
|
|
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}\n`)
|
|
for (const entry of data.out) {
|
|
console.log(`### ${entry.sel} (${entry.count})`)
|
|
for (const it of entry.items) {
|
|
console.log(` .${it.cls}`)
|
|
console.log(` rect ${JSON.stringify(it.rect)} display=${it.display}/${it.flexDirection} align=${it.alignItems} justify=${it.justifyContent} gap=${it.gap}`)
|
|
console.log(` pad=${it.padding} margin=${it.margin}`)
|
|
console.log(` radius=${it.radius} bg=${it.bg} fs=${it.fontSize}/${it.fontWeight}`)
|
|
if (it.shadow) console.log(` shadow=${it.shadow}`)
|
|
}
|
|
console.log('')
|
|
}
|
|
|
|
console.log('### 垂直堆叠(y 升序)')
|
|
const sorted = [...data.stack].sort((a, b) => a.rect[1] - b.rect[1])
|
|
for (const s of sorted) {
|
|
console.log(` y=${String(s.rect[1]).padStart(4)} h=${String(s.rect[3]).padStart(4)} x=${String(s.rect[0]).padStart(4)} w=${String(s.rect[2]).padStart(4)} ${' '.repeat(s.depth)}${s.tag}.${s.cls.split(/\s+/)[0]} "${s.text}"`)
|
|
}
|
|
|
|
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)
|
|
})
|