把项目从「手写 dist/」改成「wallpapers/ 是唯一真相来源,dist/ 由 pnpm build 生成」,
并补上配套的类型、门禁与抓取器。一次提交落地整条管线,因为拆开会留下不能构建的中间态。
- src/:运行时与模拟器源码(TS,strict),编译到 build/ 再拷进各分发
- tools/:build / dev / check-{syntax,paths,dist},以及抓取器与回归门禁 tools/checks/
(.scratch/ 下那批一次性脚本移入 tools/checks/ 并入库为长期门禁)
- wallpapers/:七档壁纸的源数据 + README.md(id/音频/预设的完整规范)
- docs/adr/0005-0008:构建管线与分发拓扑、模拟器契约、自包含 sim、每骨架资源布局
- .gitignore:排除 .scratch/ 的参考资料副本(上游 spine 整仓克隆 ~1.2 GB、
抓取侦查数据 ~680 MB)与调试转储;这些是本地调查材料,补偿会让仓库无法克隆
- 归一化 .gitignore/CONTEXT.md 行尾(工作区 CRLF、索引 LF 造成的整文件假 diff)
同时修掉三档卡住构建的未完工壁纸:
- kv45 的 meta.json 里 id 还是抓取期场景名 scene_main,经 downloader promote 正名为 kv45
- shajin / zhigengniao_juheye 的 meta.json 误用了骨架描述文件(name/spine/animations/pages)、
且都缺 preset.template.json;现按规范重建:骨架沉到 spines/<名>/(spine-ts 按 atlas 所在
目录解析贴图页)、补上元数据与单骨架预设,并清掉 zhigengniao 骨架里指向作者机的绝对路径
- 顺带 promote 已在 sources.yml 里的 kv46(月升之前,与兽共舞)
pnpm check 五道门全绿:10 个分发 / 7 档壁纸 / 141 处引用自包含。
158 lines
7.1 KiB
TypeScript
158 lines
7.1 KiB
TypeScript
// 静态预览验收:证明 WE 模拟面板**不依赖调试服**,且是**右边缘滑出**的。
|
||
//
|
||
// 为什么要有这个测试:
|
||
// ① 面板曾经只在调试服注入时才存在。用户要求 GitHub Pages 预览也能弹出它——
|
||
// 静态托管没有调试服,所以必须由 `--with-sim` 把驱动**注入 index.html**。
|
||
// ② Pages 把站点放在 `/<repo>/` **子路径**下。驱动若用根绝对路径 `/scripts/…`,
|
||
// 在子路径下必然 404。所以这个测试刻意把分发挂在 `/repo/` 前缀后面服务,
|
||
// 而不是挂在根上——挂根上测不出这个错。
|
||
//
|
||
// 用法:node tools/checks/verify-static-preview.mts [分发目录名]
|
||
// 前提:已经 `pnpm build --with-sim`。
|
||
|
||
import { createServer } from "node:http";
|
||
import { createReadStream } from "node:fs";
|
||
import { stat } from "node:fs/promises";
|
||
import { extname, join, normalize, resolve } from "node:path";
|
||
import { withPage, sleep } from "../cdp.mjs";
|
||
|
||
const DIR = process.argv[2] ?? "collection-all";
|
||
const PORT = Number(process.env.STATIC_PORT ?? 8195);
|
||
const ROOT = resolve("dist/releases");
|
||
/** 模拟 GitHub Pages 的 /<repo>/ 子路径。**不能**是空串,否则测不出根绝对路径的错。 */
|
||
const PREFIX = "/repo";
|
||
|
||
let failures = 0;
|
||
const check = (ok: unknown, label: string, detail?: unknown) => {
|
||
if (!ok) failures++;
|
||
console.log(` ${ok ? "✓" : "✗"} ${label}${detail !== undefined ? ` → ${String(detail)}` : ""}`);
|
||
};
|
||
|
||
const MIME = {
|
||
".html": "text/html; charset=utf-8",
|
||
".js": "text/javascript; charset=utf-8",
|
||
".css": "text/css; charset=utf-8",
|
||
".json": "application/json; charset=utf-8",
|
||
".atlas": "text/plain; charset=utf-8",
|
||
".png": "image/png",
|
||
".jpg": "image/jpeg",
|
||
".webp": "image/webp",
|
||
".gif": "image/gif",
|
||
".flac": "audio/flac",
|
||
".mp3": "audio/mpeg",
|
||
".ogg": "audio/ogg",
|
||
};
|
||
|
||
const notFound: string[] = [];
|
||
const server = createServer(async (req, res) => {
|
||
const pathname = decodeURIComponent(new URL(req.url ?? "/", "http://x").pathname);
|
||
if (!pathname.startsWith(`${PREFIX}/`)) {
|
||
notFound.push(pathname);
|
||
res.writeHead(404).end("outside prefix");
|
||
return;
|
||
}
|
||
let rel = pathname.slice(PREFIX.length + 1);
|
||
if (rel === "" || rel.endsWith("/")) rel += "index.html";
|
||
const file = join(ROOT, normalize(rel));
|
||
if (!file.startsWith(ROOT)) {
|
||
res.writeHead(403).end("escape");
|
||
return;
|
||
}
|
||
const info = await stat(file).catch(() => null);
|
||
if (!info?.isFile()) {
|
||
notFound.push(pathname);
|
||
res.writeHead(404).end("not found");
|
||
return;
|
||
}
|
||
res.writeHead(200, { "content-type": MIME[extname(file).toLowerCase()] ?? "application/octet-stream", "content-length": info.size });
|
||
createReadStream(file).pipe(res);
|
||
});
|
||
|
||
await new Promise<void>((r) => server.listen(PORT, "127.0.0.1", () => r()));
|
||
const BASE = `http://127.0.0.1:${PORT}${PREFIX}/${DIR}/`;
|
||
console.log(`静态托管(模拟 Pages 子路径):${BASE}\n`);
|
||
|
||
const W = 1920;
|
||
const H = 1080;
|
||
const EDGE_X = W - 8; // 16px 热区之内
|
||
const AWAY_X = 200; // 远离右边缘
|
||
|
||
const result = await withPage({ width: W, height: H, profile: "tools/.cache/edge-static" }, async ({ send, evaluate, consoleLines, exceptions }) => {
|
||
const move = async (x, y) => {
|
||
await send("Input.dispatchMouseEvent", { type: "mouseMoved", x, y, button: "none", buttons: 0 });
|
||
await sleep(320); // > 面板 .18s 过渡
|
||
};
|
||
const open = () => evaluate(`document.getElementById("wesim").dataset.open`);
|
||
|
||
await send("Page.navigate", { url: BASE });
|
||
await sleep(3500);
|
||
|
||
const initial = await evaluate(`(function(){
|
||
var p = document.getElementById("wesim");
|
||
var e = document.getElementById("wesim-edge");
|
||
return {
|
||
sim: !!window.__weSim,
|
||
isSimulated: window.__weSim ? window.__weSim.__isSimulated : null,
|
||
driverOnce: window.__weSimDriver === true,
|
||
panel: !!p, edge: !!e,
|
||
open: p ? p.dataset.open : null,
|
||
banner: p && p.querySelector(".banner") ? p.querySelector(".banner").textContent : null,
|
||
canvas: document.querySelectorAll("canvas").length,
|
||
hasToggle: !!document.querySelector("#wesim .toggle")
|
||
};
|
||
})()`);
|
||
|
||
// ── 交互:右边缘滑出 ──
|
||
await move(AWAY_X, H / 2);
|
||
const closedInitially = await open();
|
||
await move(EDGE_X, H / 2);
|
||
const openedByEdge = await open();
|
||
await move(AWAY_X, H / 2);
|
||
const closedByLeave = await open();
|
||
await move(EDGE_X, H / 2);
|
||
const reopened = await open();
|
||
|
||
// 取消收起:此刻真实指针就停在右边缘热区里。没有 armed 规则的话,
|
||
// 面板滑走会立刻让边缘命中再次触发,表现为"取消关不掉"。
|
||
// (官方面板没有 ×,底部是确认/取消,所以这里点「取消」。)
|
||
await evaluate(`document.querySelector("#wesim .foot button:not(.primary)").click()`);
|
||
await sleep(400);
|
||
const afterClose = await open();
|
||
|
||
// 离开边缘再回来 → 重新武装
|
||
await move(AWAY_X, H / 2);
|
||
await move(EDGE_X, H / 2);
|
||
const afterRearm = await open();
|
||
|
||
return { initial, closedInitially, openedByEdge, closedByLeave, reopened, afterClose, afterRearm, consoleLines, exceptions };
|
||
});
|
||
|
||
const r = result;
|
||
console.log("=== ① 静态托管下模拟器可用(无调试服)===");
|
||
check(r.initial.sim === true, "window.__weSim 存在(驱动已随产物注入)");
|
||
check(r.initial.isSimulated === true, "window.__weSim.__isSimulated = true");
|
||
check(r.initial.driverOnce === true, "驱动幂等标记已置位");
|
||
check(r.initial.panel === true, "#wesim 面板已挂载");
|
||
check(r.initial.edge === true, "#wesim-edge 右边缘热区已挂载");
|
||
check(r.initial.hasToggle === false, "没有常驻开关按钮(打开方式只有右边缘)");
|
||
check(/模拟环境/.test(r.initial.banner ?? ""), "面板有「模拟环境」横幅", r.initial.banner?.trim());
|
||
check(r.initial.canvas > 0, "壁纸本身也渲染了", `canvas=${r.initial.canvas}`);
|
||
|
||
console.log("\n=== ② 右边缘滑出交互 ===");
|
||
check(r.closedInitially === "0", "初始收起", `data-open=${r.closedInitially}`);
|
||
check(r.openedByEdge === "1", "指针移到右边缘 → 滑出", `data-open=${r.openedByEdge}`);
|
||
check(r.closedByLeave === "0", "指针离开 → 自动收回", `data-open=${r.closedByLeave}`);
|
||
check(r.reopened === "1", "再次移到右边缘 → 再次滑出", `data-open=${r.reopened}`);
|
||
check(r.afterClose === "0", "「取消」收起后不会被边缘热区立刻弹回", `data-open=${r.afterClose}`);
|
||
check(r.afterRearm === "1", "离开边缘后再回来 → 重新可触发", `data-open=${r.afterRearm}`);
|
||
|
||
console.log("\n=== ③ 子路径下没有 404(相对路径正确)===");
|
||
check(notFound.length === 0, "静态服务器 0 个 404", notFound.slice(0, 3).join(" | ") || "无");
|
||
const noisy = r.consoleLines.filter((l) => /error|uncaught/i.test(l));
|
||
check(r.exceptions.length === 0, "0 未捕获异常", r.exceptions.slice(0, 2).join(" | ") || "无");
|
||
check(noisy.length === 0, "0 控制台报错", noisy.slice(0, 3).join(" | ") || "干净");
|
||
|
||
server.close();
|
||
console.log(failures === 0 ? "\n静态预览:通过(面板不依赖调试服,且随右边缘滑出)" : `\n静态预览:失败 ${failures} 处`);
|
||
process.exit(failures === 0 ? 0 : 1);
|