Files
llm-atlas/scripts/check-alignment-browser.mjs
T
2026-07-29 08:39:32 +08:00

242 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { writeFileSync } from "node:fs";
const cdpPort = process.env.CDP_PORT ?? "9224";
const baseUrl = process.env.SITE_URL ?? "http://127.0.0.1:4323";
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.exception?.description ?? 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.exception?.description ?? result.exceptionDetails.text);
return result.result.value;
};
const navigate = async (path) => {
await command("Page.navigate", { url: `${baseUrl}${path}` });
for (let attempt = 0; attempt < 70; attempt += 1) {
await pause(100);
if (await evaluate("document.readyState === 'complete'")) return;
}
throw new Error(`${path} 加载超时`);
};
const screenshot = async (path, full = false) => {
const params = { format: "png", captureBeyondViewport: full };
if (full) {
const metrics = await command("Page.getLayoutMetrics");
params.clip = {
x: 0,
y: 0,
width: metrics.cssContentSize.width,
height: metrics.cssContentSize.height,
scale: 1,
};
}
const result = await command("Page.captureScreenshot", params);
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("/post-training/alignment/");
await screenshot("/tmp/llm-atlas-alignment-desktop.png");
const overview = await evaluate(`(() => ({
title: document.querySelector("h1")?.textContent.trim(),
sections: document.querySelectorAll(".article-section").length,
tocLinks: document.querySelectorAll(".side-rail a").length,
paperLinks: document.querySelectorAll(".paper-chain a").length,
labTabs: document.querySelectorAll("[data-alignment-tab]").length,
labPanels: document.querySelectorAll("[data-alignment-panel]").length,
navLinks: document.querySelectorAll(".top-nav a").length,
documentOverflow: document.documentElement.scrollWidth - document.documentElement.clientWidth,
}))()`);
const sft = await evaluate(`(() => {
const root = document.querySelector("[data-alignment-lab]");
const read = () => ({
active: root.querySelector("[data-sft-active]").textContent.trim(),
nll: root.querySelector("[data-sft-nll]").textContent.trim(),
reading: root.querySelector("[data-sft-reading]").textContent.trim(),
lossStates: [...root.querySelectorAll("[data-sft-loss]")].map((node) => node.textContent.trim()),
});
const initial = read();
root.querySelector('[data-sft-mask="all"]').click();
const quality = root.querySelector("[data-sft-quality]");
quality.value = "unsafe";
quality.dispatchEvent(new Event("change", { bubbles: true }));
return { initial, unsafeAll: read() };
})()`);
const preference = await evaluate(`(() => {
const root = document.querySelector("[data-alignment-lab]");
root.querySelector('[data-alignment-tab="preference"]').click();
root.querySelector('[data-pref-preset="bothbad"]').click();
return {
rewardA: root.querySelector('[data-reward="a"]').textContent.trim(),
rewardB: root.querySelector('[data-reward="b"]').textContent.trim(),
probability: root.querySelector("[data-pref-probability]").textContent.trim(),
reading: root.querySelector("[data-pref-reading]").textContent.trim(),
visiblePanel: root.querySelector("[data-alignment-panel]:not([hidden])").dataset.alignmentPanel,
};
})()`);
const update = await evaluate(`(() => {
const root = document.querySelector("[data-alignment-lab]");
root.querySelector('[data-alignment-tab="update"]').click();
root.querySelector('[data-update-method="dpo"]').click();
const stale = root.querySelector("[data-update-stale]");
stale.value = "85";
stale.dispatchEvent(new Event("input", { bubbles: true }));
return {
models: root.querySelector("[data-update-models]").textContent.trim(),
regime: root.querySelector("[data-update-regime]").textContent.trim(),
equation: root.querySelector("[data-update-equation]").textContent.trim(),
reading: root.querySelector("[data-update-reading]").textContent.trim(),
steps: [...root.querySelectorAll("[data-update-step]")].map((node) => node.textContent.trim()),
};
})()`);
const recipe = await evaluate(`(() => {
const root = document.querySelector("[data-alignment-lab]");
root.querySelector('[data-alignment-tab="recipe"]').click();
root.querySelector('[data-recipe-id="k3"]').click();
const selected = {
family: root.querySelector("[data-recipe-family]").textContent.trim(),
regime: root.querySelector("[data-recipe-regime]").textContent.trim(),
constraints: root.querySelector("[data-recipe-constraints]").textContent.trim(),
path: [...root.querySelectorAll("[data-recipe-step]")].map((node) => node.textContent.trim()),
risk: root.querySelector("[data-recipe-risk]").textContent.trim(),
};
const firstTab = root.querySelector('[data-alignment-tab="sft"]');
firstTab.focus();
firstTab.dispatchEvent(new KeyboardEvent("keydown", { key: "End", bubbles: true }));
return {
...selected,
keyboardSelected: root.querySelector('[data-alignment-tab][aria-selected="true"]').dataset.alignmentTab,
keyboardVisible: root.querySelector("[data-alignment-panel]:not([hidden])").dataset.alignmentPanel,
};
})()`);
await evaluate(`document.querySelector("[data-alignment-lab]").scrollIntoView({ block: "start", behavior: "instant" })`);
await pause(180);
await screenshot("/tmp/llm-atlas-alignment-lab-desktop.png");
await navigate("/");
const home = await evaluate(`(() => ({
releaseCards: document.querySelectorAll(".release-card").length,
firstRelease: document.querySelector(".release-card h2").textContent.trim(),
firstHref: document.querySelector(".release-card").getAttribute("href"),
paperCount: document.querySelector(".hero-stats div:nth-child(3) b").textContent.trim(),
navLinks: document.querySelectorAll(".top-nav a").length,
}))()`);
await navigate("/papers/");
const papers = await evaluate(`(() => {
const button = [...document.querySelectorAll("[data-filter]")].find((node) => node.textContent.includes("后训练"));
button?.click();
return {
total: document.querySelectorAll("[data-paper]").length,
alignmentVisible: document.querySelectorAll("[data-paper]:not([hidden])").length,
hasAlignmentFilter: Boolean(button),
};
})()`);
await command("Emulation.setDeviceMetricsOverride", {
width: 390,
height: 844,
deviceScaleFactor: 1,
mobile: true,
});
await navigate("/post-training/alignment/");
const mobile = await evaluate(`(() => {
const root = document.querySelector("[data-alignment-lab]");
root.scrollIntoView({ block: "start", behavior: "instant" });
const toggle = document.querySelector("#menu-toggle");
toggle?.click();
return {
documentOverflow: document.documentElement.scrollWidth - document.documentElement.clientWidth,
menuVisible: getComputedStyle(toggle).display !== "none",
menuOpen: toggle.getAttribute("aria-expanded"),
mobileLinks: document.querySelectorAll("#mobile-nav a").length,
tabs: root.querySelectorAll("[data-alignment-tab]").length,
offenders: [...document.querySelectorAll("body *")]
.filter((node) => node.getBoundingClientRect().right > document.documentElement.clientWidth + 1)
.slice(0, 12)
.map((node) => ({
tag: node.tagName,
className: typeof node.className === "string" ? node.className : "",
right: Math.round(node.getBoundingClientRect().right),
width: Math.round(node.getBoundingClientRect().width),
})),
};
})()`);
await pause(180);
await screenshot("/tmp/llm-atlas-alignment-mobile.png");
const report = { overview, sft, preference, update, recipe, home, papers, mobile, exceptions };
console.log(JSON.stringify(report, null, 2));
const numeric = (value) => Number.parseFloat(value);
const failures = [];
if (!overview.title.includes("真正与人协作")) failures.push("章节标题异常");
if (overview.sections !== 22 || overview.tocLinks !== 22) failures.push("章节/目录数量异常");
if (overview.paperLinks !== 44) failures.push("正式论文链不是 44 个节点");
if (overview.labTabs !== 4 || overview.labPanels !== 4) failures.push("四联实验结构异常");
if (overview.navLinks !== 18 || home.navLinks !== 18 || mobile.mobileLinks !== 18) failures.push("全站导航未同步推理服务专题");
if (overview.documentOverflow > 1 || mobile.documentOverflow > 1) failures.push("桌面或移动端存在横向溢出");
if (sft.initial.active !== "6 / 10" || !sft.initial.lossStates.slice(0, 4).every((value) => value === "MASKED")) failures.push("SFT response-only mask 异常");
if (sft.unsafeAll.active !== "10 / 10" || numeric(sft.unsafeAll.nll) <= numeric(sft.initial.nll) || !sft.unsafeAll.reading.includes("错误")) failures.push("SFT 全序列/坏示范交互异常");
if (numeric(preference.rewardA) >= 0 || numeric(preference.rewardB) >= 0 || !preference.reading.includes("两坏") || preference.visiblePanel !== "preference") failures.push("both-bad 偏好实验异常");
if (update.models !== "2" || update.regime !== "OFFLINE" || !update.equation.includes("Δlog") || !update.reading.includes("旧候选")) failures.push("DPO / 陈旧分布实验异常");
if (!update.steps[0].includes("Fixed preference")) failures.push("DPO 更新流程没有切换");
if (!recipe.family.includes("Multi-effort") || !recipe.regime.includes("9 RL experts") || !recipe.constraints.includes("verbosity")) failures.push("K3 配方合同异常");
if (!recipe.path.some((step) => step.includes("3 domains × 3 efforts")) || !recipe.path.some((step) => step.includes("MOPD"))) failures.push("K3 配方路径异常");
if (recipe.keyboardSelected !== "recipe" || recipe.keyboardVisible !== "recipe") failures.push("实验 tab 键盘导航异常");
if (home.releaseCards !== 13 || !home.firstRelease.includes("推理优化") || home.firstHref !== "/systems/inference/") failures.push("首页推理服务首发入口异常");
if (home.paperCount !== "400" || papers.total !== 400 || !papers.hasAlignmentFilter || papers.alignmentVisible < 35) failures.push("论文库后训练标签或论文总数异常");
if (!mobile.menuVisible || mobile.menuOpen !== "true" || mobile.tabs !== 4) failures.push("移动端导航或实验异常");
if (exceptions.length) failures.push(`浏览器异常:${exceptions.join(" | ")}`);
if (failures.length) {
console.error(`\nFAIL\n- ${failures.join("\n- ")}`);
process.exitCode = 1;
} else {
console.log("\nPASS alignment browser regression");
}
socket.close();