Files
SpineWallpaper/tools/cdp.mjs
T
Shuery 399f99edd7 test: 零依赖对拍测试台(可在不改 dist/ 的前提下复现 WE 行为)
tools/serve.mjs    本地静态服务 + 测试驱动注入(模拟 WE 下发用户属性、冻结动画相位、藏 canvas 取参考帧)
tools/cdp.mjs      最小 CDP 客户端(真实时间等待;--screenshot 的虚拟时间会在骨骼加载完成前就耗尽)
tools/shot.mjs     截图/取状态,就绪判据可传入
tools/diff.mjs     ffmpeg 差分:PSNR / SSIM / 平均亮度差
tools/capture.mjs  对拍矩阵:壁纸 × 分辨率 × (正常, 藏 canvas)

实测:xilian 1920x1080 spine 层贡献 mean|diff|=26.1/255,kv37 为 10.2/255,
同配置两次截图噪声底 0.68/255。
2026-09-19 19:13:57 +08:00

127 lines
4.3 KiB
JavaScript

// 最小 CDP 客户端:用真实时间等待页面就绪,再截图 / 取状态。
// 为什么不用 msedge --screenshot:那个走的是"虚拟时间",而贴图解码与 WebGL 初始化吃的是真实 CPU 时间,
// 虚拟时间预算会在骨骼真正加载完之前就耗尽,截出来的是还没画内容的帧(这个坑已经踩过一次)。
import { spawn } from "node:child_process";
import { existsSync } from "node:fs";
import { mkdir } from "node:fs/promises";
import { resolve } from "node:path";
const EDGE = [
"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe",
"C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe",
].find((p) => existsSync(p));
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
export async function withPage(opts, fn) {
const { width = 1920, height = 1080, profile = "tools/.cache/edge-cdp", extraArgs = [] } = opts;
if (!EDGE) throw new Error("找不到 msedge.exe");
await mkdir(resolve(profile), { recursive: true });
const child = spawn(
EDGE,
[
"--headless=new",
"--remote-debugging-port=0",
`--user-data-dir=${resolve(profile)}`,
"--no-first-run",
"--no-default-browser-check",
"--disable-extensions",
"--hide-scrollbars",
"--enable-unsafe-swiftshader",
"--autoplay-policy=no-user-gesture-required",
"--mute-audio",
`--window-size=${width},${height}`,
...extraArgs,
"about:blank",
],
{ stdio: ["ignore", "pipe", "pipe"] },
);
let stderr = "";
const wsUrl = await new Promise((res, rej) => {
const timer = setTimeout(() => rej(new Error("等待 DevTools 端点超时")), 20000);
child.stderr.on("data", (d) => {
stderr += d;
const m = stderr.match(/DevTools listening on (ws:\/\/\S+)/);
if (m) {
clearTimeout(timer);
res(m[1]);
}
});
child.on("close", () => {
clearTimeout(timer);
rej(new Error("Edge 提前退出:\n" + stderr.slice(-800)));
});
});
const port = new URL(wsUrl).port;
const list = await (await fetch(`http://127.0.0.1:${port}/json/list`)).json();
const page = list.find((t) => t.type === "page");
if (!page) throw new Error("没有 page target");
const ws = new WebSocket(page.webSocketDebuggerUrl);
await new Promise((res, rej) => {
ws.onopen = res;
ws.onerror = (e) => rej(new Error("CDP WebSocket 连接失败: " + (e?.message ?? "")));
});
let nextId = 1;
const pending = new Map();
const consoleLines = [];
const exceptions = [];
ws.onmessage = (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(`${msg.error.message} (${JSON.stringify(msg.error.data ?? "")})`)) : res(msg.result);
return;
}
if (msg.method === "Runtime.consoleAPICalled") {
const text = (msg.params.args ?? []).map((a) => a.value ?? a.description ?? a.type).join(" ");
consoleLines.push(`${msg.params.type}: ${text}`);
}
if (msg.method === "Runtime.exceptionThrown") {
const d = msg.params.exceptionDetails;
exceptions.push(d.exception?.description ?? d.text);
}
if (msg.method === "Log.entryAdded" && msg.params.entry.level === "error") {
consoleLines.push(`log-error: ${msg.params.entry.text}`);
}
};
const send = (method, params = {}) =>
new Promise((res, rej) => {
const id = nextId++;
pending.set(id, { res, rej });
ws.send(JSON.stringify({ id, method, params }));
});
const evaluate = async (expression) => {
const r = await send("Runtime.evaluate", { expression, returnByValue: true, awaitPromise: true });
if (r.exceptionDetails) throw new Error(r.exceptionDetails.exception?.description ?? "evaluate 失败");
return r.result?.value;
};
try {
await send("Page.enable");
await send("Runtime.enable");
await send("Log.enable");
await send("Emulation.setDeviceMetricsOverride", {
width,
height,
deviceScaleFactor: 1,
mobile: false,
});
return await fn({ send, evaluate, consoleLines, exceptions, stderr: () => stderr });
} finally {
try { ws.close(); } catch {}
child.kill();
await sleep(120);
}
}
export { sleep };