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 !== 8) 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); }