188 lines
7.3 KiB
JavaScript
188 lines
7.3 KiB
JavaScript
import { writeFileSync } from "node:fs";
|
||
|
||
const cdpPort = process.env.CDP_PORT ?? "9223";
|
||
const baseUrl = process.env.SITE_URL ?? "http://127.0.0.1:4322";
|
||
const pages = await fetch(`http://127.0.0.1:${cdpPort}/json/list`).then((response) => response.json());
|
||
const page = pages.find((entry) => entry.type === "page");
|
||
if (!page) throw new Error(`CDP ${cdpPort} 没有可用页面`);
|
||
|
||
const socket = new WebSocket(page.webSocketDebuggerUrl);
|
||
await new Promise((resolve, reject) => {
|
||
socket.addEventListener("open", resolve, { once: true });
|
||
socket.addEventListener("error", reject, { once: true });
|
||
});
|
||
|
||
let nextId = 0;
|
||
const pending = new Map();
|
||
const exceptions = [];
|
||
socket.addEventListener("message", (event) => {
|
||
const message = JSON.parse(event.data);
|
||
if (message.id && pending.has(message.id)) {
|
||
const { resolve, reject } = pending.get(message.id);
|
||
pending.delete(message.id);
|
||
if (message.error) reject(new Error(message.error.message));
|
||
else resolve(message.result);
|
||
}
|
||
if (message.method === "Runtime.exceptionThrown") {
|
||
exceptions.push(message.params.exceptionDetails.text);
|
||
}
|
||
});
|
||
|
||
const command = (method, params = {}) => new Promise((resolve, reject) => {
|
||
const id = ++nextId;
|
||
pending.set(id, { resolve, reject });
|
||
socket.send(JSON.stringify({ id, method, params }));
|
||
});
|
||
const pause = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
|
||
const evaluate = async (expression) => {
|
||
const result = await command("Runtime.evaluate", {
|
||
expression,
|
||
returnByValue: true,
|
||
awaitPromise: true,
|
||
});
|
||
if (result.exceptionDetails) throw new Error(result.exceptionDetails.text);
|
||
return result.result.value;
|
||
};
|
||
const navigate = async (path) => {
|
||
await command("Page.navigate", { url: `${baseUrl}${path}` });
|
||
for (let attempt = 0; attempt < 30; attempt += 1) {
|
||
await pause(100);
|
||
if (await evaluate("document.readyState === 'complete'")) return;
|
||
}
|
||
throw new Error(`${path} 加载超时`);
|
||
};
|
||
const screenshot = async (path) => {
|
||
const result = await command("Page.captureScreenshot", { format: "png", captureBeyondViewport: false });
|
||
writeFileSync(path, Buffer.from(result.data, "base64"));
|
||
};
|
||
|
||
await command("Page.enable");
|
||
await command("Runtime.enable");
|
||
await command("Emulation.setDeviceMetricsOverride", {
|
||
width: 1440,
|
||
height: 1100,
|
||
deviceScaleFactor: 1,
|
||
mobile: false,
|
||
});
|
||
await navigate("/moe/");
|
||
|
||
const k3 = await evaluate(`(() => {
|
||
const root = document.querySelector("[data-moe-lab]");
|
||
const started = performance.now();
|
||
root.querySelector('[data-preset-button="k3"]').click();
|
||
const duration = performance.now() - started;
|
||
const skew = root.querySelector("[data-skew]");
|
||
const capacity = root.querySelector("[data-capacity]");
|
||
skew.value = "100";
|
||
capacity.value = "0.75";
|
||
skew.dispatchEvent(new Event("input", { bubbles: true }));
|
||
capacity.dispatchEvent(new Event("input", { bubbles: true }));
|
||
root.querySelector('[data-balance="none"]').click();
|
||
const none = {
|
||
ratio: root.querySelector("[data-load-ratio]").textContent,
|
||
overflow: root.querySelector("[data-drop-rate]").textContent,
|
||
};
|
||
root.querySelector('[data-balance="quantile"]').click();
|
||
return {
|
||
duration: Number(duration.toFixed(2)),
|
||
title: root.querySelector("[data-route-title]").textContent,
|
||
fraction: root.querySelector("[data-active-fraction]").textContent,
|
||
communication: root.querySelector("[data-comm-value]").textContent,
|
||
communicationNote: root.querySelector("[data-comm-note]").textContent,
|
||
combination: root.querySelector("[data-combination-value]").textContent,
|
||
none,
|
||
quantile: {
|
||
ratio: root.querySelector("[data-load-ratio]").textContent,
|
||
overflow: root.querySelector("[data-drop-rate]").textContent,
|
||
},
|
||
metrics: root.querySelectorAll(".metric-grid > article").length,
|
||
};
|
||
})()`);
|
||
|
||
const latent = await evaluate(`(() => {
|
||
const root = document.querySelector("[data-moe-lab]");
|
||
const started = performance.now();
|
||
root.querySelector('[data-preset-button="latent"]').click();
|
||
return {
|
||
duration: Number((performance.now() - started).toFixed(2)),
|
||
fraction: root.querySelector("[data-active-fraction]").textContent,
|
||
communication: root.querySelector("[data-comm-value]").textContent,
|
||
note: root.querySelector("[data-comm-note]").textContent,
|
||
};
|
||
})()`);
|
||
|
||
const layout = await evaluate(`(() => {
|
||
const header = document.querySelector(".site-header");
|
||
const nav = document.querySelector(".top-nav");
|
||
const meta = document.querySelector(".header-meta");
|
||
return {
|
||
documentOverflow: document.documentElement.scrollWidth - document.documentElement.clientWidth,
|
||
navGap: Number((meta.getBoundingClientRect().left - nav.getBoundingClientRect().right).toFixed(1)),
|
||
articleSections: document.querySelectorAll(".article-section").length,
|
||
};
|
||
})()`);
|
||
|
||
await evaluate(`(() => {
|
||
document.documentElement.style.scrollBehavior = "auto";
|
||
document.querySelector("[data-moe-lab]").scrollIntoView({ block: "start", behavior: "instant" });
|
||
return { scrollY, top: document.querySelector("[data-moe-lab]").getBoundingClientRect().top };
|
||
})()`);
|
||
await pause(200);
|
||
await screenshot("/tmp/llm-atlas-moe-lab-desktop.png");
|
||
|
||
await command("Emulation.setDeviceMetricsOverride", {
|
||
width: 390,
|
||
height: 844,
|
||
deviceScaleFactor: 1,
|
||
mobile: true,
|
||
});
|
||
await navigate("/moe/");
|
||
const mobile = await evaluate(`({
|
||
documentOverflow: document.documentElement.scrollWidth - document.documentElement.clientWidth,
|
||
menuVisible: getComputedStyle(document.querySelector("#menu-toggle")).display !== "none",
|
||
title: document.querySelector("h1").innerText,
|
||
})`);
|
||
await screenshot("/tmp/llm-atlas-moe-mobile.png");
|
||
|
||
await command("Emulation.setDeviceMetricsOverride", {
|
||
width: 1440,
|
||
height: 1100,
|
||
deviceScaleFactor: 1,
|
||
mobile: false,
|
||
});
|
||
await navigate("/");
|
||
const home = await evaluate(`({
|
||
documentOverflow: document.documentElement.scrollWidth - document.documentElement.clientWidth,
|
||
releaseCards: document.querySelectorAll(".release-card").length,
|
||
navLinks: document.querySelectorAll(".top-nav a").length,
|
||
})`);
|
||
await screenshot("/tmp/llm-atlas-home-desktop.png");
|
||
await evaluate(`(() => {
|
||
document.documentElement.style.scrollBehavior = "auto";
|
||
document.querySelector("#new-chapters").scrollIntoView({ block: "start", behavior: "instant" });
|
||
})()`);
|
||
await pause(100);
|
||
await screenshot("/tmp/llm-atlas-home-releases.png");
|
||
|
||
const report = { k3, latent, layout, mobile, home, exceptions };
|
||
console.log(JSON.stringify(report, null, 2));
|
||
|
||
const failures = [];
|
||
if (!k3.title.includes("896 选 16")) failures.push("K3 预设未生效");
|
||
if (k3.metrics !== 6) failures.push(`指标卡数量异常:${k3.metrics}`);
|
||
if (!k3.communicationNote.includes("2× latent")) failures.push("K3 latent 通信说明缺失");
|
||
if (!latent.note.includes("4× latent")) failures.push("LatentMoE 4× 通信说明缺失");
|
||
if (layout.documentOverflow > 0 || mobile.documentOverflow > 0 || home.documentOverflow > 0) {
|
||
failures.push("页面存在横向溢出");
|
||
}
|
||
if (layout.navGap < 0) failures.push(`桌面导航碰撞:${layout.navGap}px`);
|
||
if (!mobile.menuVisible) failures.push("移动端菜单按钮未显示");
|
||
if (home.releaseCards !== 2) failures.push(`首页新章卡数量异常:${home.releaseCards}`);
|
||
if (exceptions.length) failures.push(`浏览器脚本异常:${exceptions.join("; ")}`);
|
||
|
||
socket.close();
|
||
if (failures.length) {
|
||
failures.forEach((failure) => console.error(`- ${failure}`));
|
||
process.exit(1);
|
||
}
|