209 lines
8.4 KiB
JavaScript
209 lines
8.4 KiB
JavaScript
// 用本地 Chromium 无头渲染评论页,精确测量各徽章的盒子尺寸(CDP,无需第三方依赖)
|
|||
|
|
import { spawn } from 'node:child_process';
|
||
|
|
import { mkdtempSync } from 'node:fs';
|
||
|
|
import { tmpdir } from 'node:os';
|
||
|
|
import { join } from 'node:path';
|
||
|
|
|
||
|
|
const CHROME = 'C:/Users/QunLIn/.agent-browser/browsers/chrome-154.0.8037.92/chrome.exe';
|
||
|
|
const PORT = 9333;
|
||
|
|
const URL_ = process.argv[2] || 'http://127.0.0.1:1313/comment.html';
|
||
|
|
|
||
|
|
const userDir = mkdtempSync(join(tmpdir(), 'cdp-badge-'));
|
||
|
|
const chrome = spawn(CHROME, [
|
||
|
|
'--headless=new',
|
||
|
|
`--remote-debugging-port=${PORT}`,
|
||
|
|
`--user-data-dir=${userDir}`,
|
||
|
|
'--no-first-run',
|
||
|
|
'--no-default-browser-check',
|
||
|
|
'--disable-gpu',
|
||
|
|
'--window-size=1280,900',
|
||
|
|
'about:blank',
|
||
|
|
], { stdio: 'ignore' });
|
||
|
|
|
||
|
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||
|
|
|
||
|
|
async function waitPort() {
|
||
|
|
for (let i = 0; i < 40; i++) {
|
||
|
|
try {
|
||
|
|
const r = await fetch(`http://127.0.0.1:${PORT}/json/version`);
|
||
|
|
if (r.ok) return await r.json();
|
||
|
|
} catch { /* not ready */ }
|
||
|
|
await sleep(300);
|
||
|
|
}
|
||
|
|
throw new Error('chrome 调试端口未就绪');
|
||
|
|
}
|
||
|
|
|
||
|
|
function cdp(wsUrl) {
|
||
|
|
return new Promise((resolve, reject) => {
|
||
|
|
const ws = new WebSocket(wsUrl);
|
||
|
|
let id = 0;
|
||
|
|
const pending = new Map();
|
||
|
|
ws.addEventListener('message', (ev) => {
|
||
|
|
const msg = JSON.parse(ev.data);
|
||
|
|
if (msg.id && pending.has(msg.id)) {
|
||
|
|
const { res, rej } = pending.get(msg.id);
|
||
|
|
pending.delete(msg.id);
|
||
|
|
msg.error ? rej(new Error(JSON.stringify(msg.error))) : res(msg.result);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
ws.addEventListener('error', reject);
|
||
|
|
ws.addEventListener('open', () => {
|
||
|
|
resolve({
|
||
|
|
send(method, params = {}) {
|
||
|
|
return new Promise((res, rej) => {
|
||
|
|
const mid = ++id;
|
||
|
|
pending.set(mid, { res, rej });
|
||
|
|
ws.send(JSON.stringify({ id: mid, method, params }));
|
||
|
|
});
|
||
|
|
},
|
||
|
|
close: () => ws.close(),
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
const MEASURE = `(() => {
|
||
|
|
const px = (n) => Math.round(n * 100) / 100;
|
||
|
|
const list = document.querySelector('.atk-list');
|
||
|
|
if (list) list.scrollIntoView({ block: 'start' });
|
||
|
|
const info = (el) => {
|
||
|
|
if (!el) return null;
|
||
|
|
const r = el.getBoundingClientRect();
|
||
|
|
const cs = getComputedStyle(el);
|
||
|
|
return {
|
||
|
|
text: (el.textContent || '').trim(),
|
||
|
|
cls: el.className.replace(' atk-cmt-badge', ''),
|
||
|
|
offW: el.offsetWidth, offH: el.offsetHeight,
|
||
|
|
w: px(r.width), h: px(r.height), top: px(r.top),
|
||
|
|
fontSize: cs.fontSize, lineHeight: cs.lineHeight, fontWeight: cs.fontWeight,
|
||
|
|
padding: cs.padding, border: cs.borderTopWidth, display: cs.display,
|
||
|
|
marginLeft: cs.marginLeft, marginRight: cs.marginRight,
|
||
|
|
parent: el.parentElement ? el.parentElement.className : null,
|
||
|
|
};
|
||
|
|
};
|
||
|
|
const heads = [...document.querySelectorAll('.atk-header')];
|
||
|
|
const out = { headerCount: heads.length, samples: [] };
|
||
|
|
heads.slice(0, 4).forEach((h, i) => {
|
||
|
|
const badges = [...h.querySelectorAll('.atk-badge, .atk-pinned-badge, .atk-title-badge, .atk-rank-badge, .atk-link-badge, .atk-feed-badge, .atk-region-badge')];
|
||
|
|
if (!badges.length) return;
|
||
|
|
out.samples.push({
|
||
|
|
i,
|
||
|
|
hidden: !!h.closest('[style*="display: none"]'),
|
||
|
|
innerWidth: window.innerWidth,
|
||
|
|
badges: badges.map(info),
|
||
|
|
});
|
||
|
|
});
|
||
|
|
return JSON.stringify(out, null, 1);
|
||
|
|
})()`;
|
||
|
|
|
||
|
|
try {
|
||
|
|
await waitPort();
|
||
|
|
// 新建标签页
|
||
|
|
const created = await fetch(`http://127.0.0.1:${PORT}/json/new?${encodeURIComponent(URL_)}`, { method: 'PUT' });
|
||
|
|
const tab = await created.json();
|
||
|
|
const c = await cdp(tab.webSocketDebuggerUrl);
|
||
|
|
await c.send('Runtime.enable');
|
||
|
|
await c.send('Page.enable');
|
||
|
|
await sleep(6000); // 等评论异步加载
|
||
|
|
// 强制日间模式并把评论区滚到视口
|
||
|
|
await c.send('Runtime.evaluate', {
|
||
|
|
expression: `(() => {
|
||
|
|
document.documentElement.setAttribute('data-theme', 'light');
|
||
|
|
const el = document.querySelector('.atk-comment .atk-header') || document.querySelector('.atk-list');
|
||
|
|
if (el) el.scrollIntoView({ block: 'center' });
|
||
|
|
return 'ok';
|
||
|
|
})()`,
|
||
|
|
returnByValue: true,
|
||
|
|
});
|
||
|
|
await sleep(1200);
|
||
|
|
const res = await c.send('Runtime.evaluate', { expression: MEASURE, returnByValue: true, awaitPromise: false });
|
||
|
|
console.log(res.result.value);
|
||
|
|
const shot = await c.send('Page.captureScreenshot', { format: 'png' });
|
||
|
|
const { writeFileSync } = await import('node:fs');
|
||
|
|
writeFileSync(process.argv[3] || 'badge-measure.png', Buffer.from(shot.data, 'base64'));
|
||
|
|
|
||
|
|
// 额外抓一张"徽章行"特写(放大 3 倍):先滚动,等布局稳定再取 rect
|
||
|
|
await c.send('Runtime.evaluate', {
|
||
|
|
expression: `(() => {
|
||
|
|
const hs = [...document.querySelectorAll('.atk-header')];
|
||
|
|
const h = hs.find((x) => x.querySelector('.atk-title-badge') || x.querySelector('.atk-link-badge')) || hs[0];
|
||
|
|
if (h) h.scrollIntoView({ block: 'center' });
|
||
|
|
return 'ok';
|
||
|
|
})()`,
|
||
|
|
returnByValue: true,
|
||
|
|
});
|
||
|
|
await sleep(600);
|
||
|
|
const rectRes = await c.send('Runtime.evaluate', {
|
||
|
|
expression: `(() => {
|
||
|
|
const hs = [...document.querySelectorAll('.atk-header')];
|
||
|
|
const h = hs.find((x) => x.querySelector('.atk-title-badge') || x.querySelector('.atk-link-badge')) || hs[0];
|
||
|
|
if (!h) return 'null';
|
||
|
|
const r = h.getBoundingClientRect();
|
||
|
|
return JSON.stringify({ x: Math.max(0, r.x - 8), y: Math.max(0, r.y - 8), width: Math.min(window.innerWidth - Math.max(0, r.x - 8), r.width + 16), height: r.height + 16 });
|
||
|
|
})()`,
|
||
|
|
returnByValue: true,
|
||
|
|
});
|
||
|
|
if (rectRes.result.value !== 'null') {
|
||
|
|
const clip = JSON.parse(rectRes.result.value);
|
||
|
|
const shot2 = await c.send('Page.captureScreenshot', { format: 'png', clip: { ...clip, scale: 3 } });
|
||
|
|
writeFileSync(String(process.argv[3] || 'badge-measure.png').replace(/\.png$/, '.row.png'), Buffer.from(shot2.data, 'base64'));
|
||
|
|
}
|
||
|
|
// 聚焦某个选择器截图(第 4 个参数,例如 .atk-fl-apply),并把它的几何也打印出来
|
||
|
|
const focusSel = process.argv[4];
|
||
|
|
if (focusSel) {
|
||
|
|
const fRes = await c.send('Runtime.evaluate', {
|
||
|
|
expression: `(() => {
|
||
|
|
const el = document.querySelector(${JSON.stringify(focusSel)});
|
||
|
|
if (!el) return 'null';
|
||
|
|
el.scrollIntoView({ block: 'center' });
|
||
|
|
return 'ok';
|
||
|
|
})()`,
|
||
|
|
returnByValue: true,
|
||
|
|
});
|
||
|
|
if (fRes.result.value === 'ok') {
|
||
|
|
await sleep(700);
|
||
|
|
const gRes = await c.send('Runtime.evaluate', {
|
||
|
|
expression: `(() => {
|
||
|
|
const el = document.querySelector(${JSON.stringify(focusSel)});
|
||
|
|
const r = el.getBoundingClientRect();
|
||
|
|
const cs = getComputedStyle(el);
|
||
|
|
const chain = (node) => {
|
||
|
|
const out = [];
|
||
|
|
let n = node;
|
||
|
|
while (n && n !== document.body) {
|
||
|
|
const s = getComputedStyle(n);
|
||
|
|
const b = n.getBoundingClientRect();
|
||
|
|
out.push({
|
||
|
|
tag: n.tagName.toLowerCase() + '.' + (n.className || '').toString().split(' ').slice(0, 2).join('.'),
|
||
|
|
x: Math.round(b.x), w: Math.round(b.width),
|
||
|
|
ml: s.marginLeft, mr: s.marginRight, pl: s.paddingLeft, pr: s.paddingRight,
|
||
|
|
bl: s.borderLeftWidth, disp: s.display, style: n.getAttribute('style') || '',
|
||
|
|
});
|
||
|
|
n = n.parentElement;
|
||
|
|
}
|
||
|
|
return out;
|
||
|
|
};
|
||
|
|
const other = [...document.querySelectorAll('.atk-content')].find((x) => !x.closest('.atk-fl-apply'));
|
||
|
|
const normal = [...document.querySelectorAll('.atk-comment-wrap, .atk-comment')].find((x) => !x.querySelector('.atk-fl-apply'));
|
||
|
|
return JSON.stringify({
|
||
|
|
card: { x: Math.round(r.x), w: Math.round(r.width), h: Math.round(r.height) },
|
||
|
|
cardStyle: { margin: cs.margin, padding: cs.padding, width: cs.width },
|
||
|
|
cardChain: chain(el),
|
||
|
|
otherContent: other ? { x: Math.round(other.getBoundingClientRect().x), w: Math.round(other.getBoundingClientRect().width) } : null,
|
||
|
|
otherChain: other ? chain(other) : null,
|
||
|
|
normalCommentChain: normal ? chain(normal) : null,
|
||
|
|
}, null, 1);
|
||
|
|
})()`,
|
||
|
|
returnByValue: true,
|
||
|
|
});
|
||
|
|
console.log('FOCUS', gRes.result.value);
|
||
|
|
const shotF = await c.send('Page.captureScreenshot', { format: 'png' });
|
||
|
|
writeFileSync(String(process.argv[3] || 'badge-measure.png').replace(/\.png$/, '.focus.png'), Buffer.from(shotF.data, 'base64'));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
c.close();
|
||
|
|
} catch (e) {
|
||
|
|
console.error('ERROR', e.message);
|
||
|
|
} finally {
|
||
|
|
chrome.kill();
|
||
|
|
}
|