Files
ai-directory-platform/packages/core/scripts/verify-skeleton.ts
T
bigcode 05440ffb5b feat: 服务器迁移后的全量演进(依赖升级/安全审计/设计统一/截图/SEO 底座化)
在原仓库历史之上同步本服务器上的全部演进:

- 迁移至新服务器并重建 PG 数据库 + ddnc 开发域名绑定
- 全部依赖升级至最新(Next 16.3.5 / React 19.3 / better-auth 1.7.5 / Drizzle 0.45 / TS 7),
  overrides 消除已知 CVE,安全审计 19 项发现全部修复闭环
- 设计统一:各站宽度 token(--container + site-container)、classic hero 重设计、
  头部换行修复、页面过渡与 View Transitions
- 截图功能:api.adx9.com 截图服务接入(ow=800 本地化 webp、cf=1、去重/限流/占位图)
- SEO 底座化:canonical/hreflang/sitemap/JSON-LD/robots 全部收进 core,新站自动继承
- 加载体验:五站五形态路由骨架(data-theme 切换、shimmer 微光、延迟淡入)+
  过滤 useTransition 交叉淡入淡出(card-enter 错峰入场)
- 构建:experimental.cpus 限流防 PG 连接打满;清理死组件链
2026-09-15 18:19:53 +08:00

167 lines
6.5 KiB
TypeScript
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.
/**
* # 脚本 verify-skeleton
* 说明:验证 5 站的加载体验改造 ——
* 1) /tools 过滤:旧内容变暗 → shimmer 骨架覆盖层淡入 → 新卡片错峰入场(交叉淡入淡出);
* 2) 路由级骨架屏:各站按 data-theme 只显示自己的形态(.loading-variant)。
* 通过给导航请求加 1200ms 延迟让过渡可见;中断 prefetch 请求保证路由骨架能触发。
* 截图输出到 /tmp/skel/。
* 运行:bun packages/core/scripts/verify-skeleton.ts
*/
import { chromium } from "playwright";
import { mkdirSync } from "node:fs";
const OUT = "/tmp/skel";
mkdirSync(OUT, { recursive: true });
/** # 常量 SITES —— 站点 → 端口 → 期望的路由骨架形态 */
const SITES = [
{ name: "ai-stack", port: 3001, variant: "lv-ai-stack" },
{ name: "launchai", port: 3002, variant: "lv-launchai" },
{ name: "the-ai-ledger", port: 3003, variant: "lv-the-ai-ledger" },
{ name: "devai-tools", port: 3004, variant: "lv-devai-tools" },
{ name: "findai", port: 3005, variant: "lv-findai" },
];
const results: string[] = [];
const browser = await chromium.launch({
executablePath: "/opt/brave.com/brave/brave",
args: ["--no-sandbox", "--disable-dev-shm-usage"],
});
for (const site of SITES) {
const base = `http://localhost:${site.port}`;
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
const r: string[] = [];
r.push(`■ ${site.name} (:${site.port})`);
try {
// ── 1. 过滤交叉过渡 ─────────────────────────────
await page.goto(`${base}/tools`, { waitUntil: "domcontentloaded" });
await page.waitForSelector("a.card-enter", { timeout: 15000 });
// 过滤导航(带 query 的 /tools 请求)加延迟;prefetch 请求直接中断
await page.route("**/tools?*", async (route) => {
if (route.request().headers()["next-router-prefetch"]) {
return route.abort();
}
await new Promise((s) => setTimeout(s, 1200));
return route.continue();
});
const chip = page
.locator("button.rounded-lg")
.filter({ hasText: /\(\d+\)/ })
.first();
await chip.click();
await page.waitForTimeout(600); // 骨架覆盖层应已挂载(延迟 200ms + 余量)
const mid = await page.evaluate(() => ({
shimmer: document.querySelectorAll(".shimmer").length,
overlay: !!document.querySelector(".skeleton-in"),
dimmed: !!document.querySelector('[aria-busy="true"]'),
}));
r.push(` 过滤中: shimmer×${mid.shimmer} overlay=${mid.overlay} 变暗=${mid.dimmed}`);
await page.screenshot({ path: `${OUT}/${site.name}-filter-mid.png` });
// 等待骨架层卸载(数据到达 + 340ms 退出窗口)
await page.waitForFunction(
() => !document.querySelector(".skeleton-in"),
{ timeout: 15000 }
);
await page.waitForTimeout(500);
const after = await page.evaluate(() => ({
shimmer: document.querySelectorAll(".shimmer").length,
url: location.search,
cards: document.querySelectorAll("a.card-enter").length,
}));
r.push(` 完成后: shimmer×${after.shimmer} 卡片×${after.cards} URL=${after.url}`);
await page.screenshot({ path: `${OUT}/${site.name}-filter-after.png` });
// ── 2. 路由级骨架屏(首页 → /tools)─────────────
// 说明:VT 浏览器由 View Transition 覆盖整个等待期;Next 16 在流式响应
// 开始后才挂载 loading 兜底。prefetch 全部中断后,用 CDP 限速拖慢 RSC
// 流式响应,使兜底骨架窗口可持续数秒,便于校验与截图。
await page.goto(`${base}/`, { waitUntil: "domcontentloaded" });
await page.waitForTimeout(400);
await page.route("**/tools*", async (route) => {
if (route.request().headers()["next-router-prefetch"]) {
return route.abort();
}
return route.continue();
});
const cdp = await ctx.newCDPSession(page);
await cdp.send("Network.enable");
await cdp.send("Network.emulateNetworkConditions", {
offline: false,
latency: 0,
downloadThroughput: 30 * 1024,
uploadThroughput: 30 * 1024,
});
const nav = page.locator('header a[href*="/tools"]').first();
await nav.click();
const routeSkel = await page.evaluate(
(expected) =>
new Promise<{ shell: boolean; shown: string[]; expected: string }>((resolve) => {
let done = false;
// 每 60ms 轮询一次,捕捉 .loading-shell 挂载窗口
const timer = setInterval(() => {
const shell = document.querySelector(".loading-shell");
if (shell && !done) {
done = true;
const shown: string[] = [];
document.querySelectorAll(".loading-variant").forEach((el) => {
if (getComputedStyle(el).display !== "none") {
shown.push(el.className);
}
});
clearInterval(timer);
resolve({ shell: true, shown, expected });
}
}, 60);
// 8 秒仍未挂载则报告失败
setTimeout(() => {
if (!done) {
clearInterval(timer);
resolve({ shell: false, shown: [], expected });
}
}, 8000);
}),
site.variant
);
const shown = routeSkel.shown.join(",");
r.push(
` 路由骨架: shell=${routeSkel.shell} 可见类=${shown || "无"} (期望 ${site.variant})`
);
if (routeSkel.shell) {
// 等 .loading-shell 淡入完成(0.3s 动画 + 0.15s 延迟),保持限速让骨架窗口持续
await page.waitForTimeout(600);
await page.screenshot({ path: `${OUT}/${site.name}-route-skeleton.png` });
}
await cdp.send("Network.emulateNetworkConditions", {
offline: false,
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
});
await page.waitForSelector("a.card-enter", { timeout: 30000 });
r.push(" 路由导航完成: /tools 已渲染");
const ok =
routeSkel.shell &&
routeSkel.shown.length === 1 &&
routeSkel.shown[0].includes(site.variant);
results.push(r.join("\n") + (ok ? "\n ✓ 通过" : "\n ✗ 校验不符"));
} catch (e) {
r.push(` ✗ 失败: ${e instanceof Error ? e.message.split("\n")[0] : e}`);
results.push(r.join("\n"));
} finally {
await ctx.close();
}
}
await browser.close();
console.log(results.join("\n---\n"));