feat: TS 构建管线、Spine 抓取器与 wallpapers/ 唯一真相来源

把项目从「手写 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 处引用自包含。
This commit is contained in:
Shuery committed 2026-10-02 01:27:02 +08:00
1 parent b8eee05d78
commit 3f11426964
297 files changed
+216627 -1926

No files matched your search

+148
View File
@@ -0,0 +1,148 @@
// 浏览器端冒烟:在真实无头 Edge 里验证
// ① 分发能正常渲染(spine canvas 出现、无异常)
// ② WE 模拟器确实生效(window.__weSim、面板、替身横幅)
// ③ 属性下发链路真的打通(经模拟器改 preset → 背景与骨架跟着换)
// ④ ?propsAt=dom 这条"load 之前下发"的竞态路径不会把配置丢掉
// ⑤ 单档分发(属性表里没有 preset/bgm)不白屏
//
// 判定只用页面**真实可观察**的量:canvas 是否出现、body 背景、播放器 config。
// 不要用 window.__player —— 那是 tools/serve.mjs 的对拍驱动注入的探针,不是产品行为。
//
// 用法:node tools/checks/smoke-sim.mts [base]
// 前提:调试服在跑(node tools/dev.ts --no-build)。
// 不需要 `--with-sim`:模拟器由调试服自己从 build/scripts/wallpaper-engine.js 提供
// (它默认不进发布产物,见 ADR 0006 §1 与 dev.ts 的路由说明)。
import { withPage, sleep } from "../cdp.mjs";
const BASE = process.argv[2] ?? "http://127.0.0.1:5173";
let failures = 0;
const check = (ok: unknown, label: string, detail?: unknown) => {
if (!ok) failures++;
console.log(` ${ok ? "✓" : "✗"} ${label}${detail !== undefined ? ` → ${String(detail)}` : ""}`);
};
const RELEASE = "collection-all";
/** 就绪判定:spine-container 里出现 canvas(= SpinePlayer 真的创建成功)。 */
async function waitCanvas(evaluate, tag) {
const deadline = Date.now() + 40000;
while (Date.now() < deadline) {
const state = await evaluate(`(function(){
var c = document.getElementById("spine-container");
return { canvas: c ? c.querySelectorAll("canvas").length : 0, children: c ? c.children.length : -1 };
})()`);
if (state && state.canvas > 0) return { ok: true, ...state };
await sleep(200);
}
return { ok: false, canvas: 0, error: `${tag} 超时` };
}
function assertClean(tag, exceptions, consoleLines) {
check(exceptions.length === 0, `${tag} 0 未捕获异常`, exceptions.slice(0, 2).join(" | ") || "无");
const noisy = consoleLines.filter((l) => /error|uncaught/i.test(l));
check(noisy.length === 0, `${tag} 0 控制台报错`, noisy.slice(0, 3).join(" | ") || "干净");
}
// ── ① 正常路径 ──
console.log("=== ① 正常路径:模拟器生效 + 属性下发 ===");
const normal = await withPage({ width: 1920, height: 1080 }, async ({ send, evaluate, consoleLines, exceptions }) => {
await send("Page.navigate", { url: `${BASE}/release/${RELEASE}/` });
const ready = await waitCanvas(evaluate, "normal");
await sleep(600);
const sim = await evaluate(`(function(){
var w = window.__weSim;
if (!w) return null;
return { isSimulated: w.__isSimulated, substituted: w.substituted.length,
release: w.release, deliveries: w.deliveries.map(function(d){ return d.type; }),
panel: !!document.getElementById("wesim"),
panels: document.querySelectorAll("#wesim").length,
edges: document.querySelectorAll("#wesim-edge").length,
banner: document.getElementById("wesim") ? document.querySelector("#wesim .banner").textContent : null,
hasEngineGlobal: typeof window.wallpaperEngine,
hasRegisterListener: typeof window.wallpaperRegisterListener };
})()`);
const view = await evaluate(`(function(){
return { bg: document.body.style.backgroundImage,
canvas: document.querySelectorAll("canvas").length,
canvasSize: (function(){ var c = document.querySelector("canvas"); return c ? c.clientWidth + "x" + c.clientHeight : null; })() };
})()`);
return { ready, sim, view, consoleLines, exceptions };
});
check(normal.ready.ok, "Spine 画布已创建(渲染就绪)", normal.ready.error ?? `canvas=${normal.ready.canvas}`);
check(normal.sim?.isSimulated === true, "window.__weSim.__isSimulated = true");
check((normal.sim?.substituted ?? 0) > 0, "替身清单非空(做不到的 API 被显式标记)", `${normal.sim?.substituted} 项`);
check(normal.sim?.panel === true, "模拟器面板已挂载(#wesim)");
// 回归断言:`--with-sim` 构建的分发自带驱动,调试服又会注入一次。
// 没有幂等闸的话模拟器会被 mount 两遍,页面上出现两个面板。
check(normal.sim?.panels === 1, "面板恰好一个(驱动幂等,没有被注入两次)", `${normal.sim?.panels} 个`);
check(normal.sim?.edges === 1, "右边缘热区恰好一个", `${normal.sim?.edges} 个`);
check(/模拟环境/.test(normal.sim?.banner ?? ""), "面板有常驻的「模拟环境」横幅", normal.sim?.banner?.trim());
check(normal.sim?.deliveries?.includes("applyUserProperties"), "已下发 applyUserProperties", normal.sim?.deliveries?.join(", "));
check(normal.sim?.deliveries?.includes("applyGeneralProperties"), "已下发 applyGeneralProperties", normal.sim?.deliveries?.join(", "));
check(/ava\.jpg/.test(normal.view.bg ?? ""), "默认预设(xilian)的背景已应用", normal.view.bg?.slice(-32));
// 严格还原 WE:这两个 API 在真实 WE 里都不存在,模拟器绝不能凭空提供
check(normal.sim?.hasEngineGlobal === "undefined", "未凭空暴露 window.wallpaperEngine(WE 没有这个全局)", normal.sim?.hasEngineGlobal);
check(normal.sim?.hasRegisterListener === "undefined", "未暴露 window.wallpaperRegisterListener(WE 没有这个 API)", normal.sim?.hasRegisterListener);
assertClean("正常路径", normal.exceptions, normal.consoleLines);
// ── ② 属性下发真的改变了画面 ──
console.log("\n=== ② 经模拟器改 preset → 背景与骨架跟着换 ===");
const switched = await withPage({ width: 1920, height: 1080 }, async ({ send, evaluate, exceptions }) => {
await send("Page.navigate", { url: `${BASE}/release/${RELEASE}/` });
await waitCanvas(evaluate, "switch");
await sleep(600);
const bgBefore = await evaluate("document.body.style.backgroundImage");
// setProperties 收的是**值**,不是 WE 的 { value } 信封(信封由模拟器自己套)
await evaluate('window.__weSim.setProperties({ preset: "kv37" })');
await sleep(2000);
const after = await evaluate(`(function(){
return { bg: document.body.style.backgroundImage,
canvas: document.querySelectorAll("canvas").length,
// 骨架换了就意味着播放器被重建过:容器里应当只有一张新 canvas
children: document.getElementById("spine-container").children.length };
})()`);
return { bgBefore, ...after, exceptions };
});
check(switched.bgBefore !== switched.bg, "背景图已更换", `${switched.bgBefore?.slice(-22)} → ${switched.bg?.slice(-22)}`);
check(/kv37_xilian/.test(switched.bg ?? ""), "换成了 kv37 的背景", switched.bg?.slice(-30));
check(switched.canvas === 1, "换骨架后只剩一张 canvas(旧播放器已 dispose)", `canvas=${switched.canvas}`);
check(switched.children === 1, "容器里没有残留节点(dispose 生效)", `children=${switched.children}`);
check(switched.exceptions.length === 0, "切换过程 0 异常", switched.exceptions.slice(0, 2).join(" | ") || "无");
// ── ③ load 之前的属性下发 ──
console.log("\n=== ③ ?__propsAt=dom:load 之前下发不丢配置 ===");
const race = await withPage({ width: 1920, height: 1080 }, async ({ send, evaluate, exceptions }) => {
const props = encodeURIComponent(JSON.stringify({ preset: "kv37" }));
await send("Page.navigate", { url: `${BASE}/release/${RELEASE}/?__propsAt=dom&__props=${props}` });
const ready = await waitCanvas(evaluate, "race");
await sleep(800);
const state = await evaluate(`(function(){
return { bg: document.body.style.backgroundImage, canvas: document.querySelectorAll("canvas").length,
propsAt: window.__weSim ? window.__weSim.deliveries[0] && window.__weSim.deliveries[0].at : null };
})()`);
return { ready, ...state, exceptions };
});
check(race.ready.ok, "播放器仍被创建(配置没被丢弃)", race.ready.error ?? "ok");
check(/kv37_xilian/.test(race.bg ?? ""), "load 前下发的 preset 已生效", race.bg?.slice(-30));
check(race.canvas === 1, "只有一张 canvas", `canvas=${race.canvas}`);
check(race.exceptions.length === 0, "0 异常", race.exceptions.slice(0, 2).join(" | ") || "无");
// ── ④ 单档分发 ──
console.log("\n=== ④ 单档分发(属性表里没有 preset/bgm) ===");
const single = await withPage({ width: 1920, height: 1080 }, async ({ send, evaluate, consoleLines, exceptions }) => {
await send("Page.navigate", { url: `${BASE}/release/single-hsr-kv37/` });
const ready = await waitCanvas(evaluate, "single");
await sleep(800);
const bg = await evaluate("document.body.style.backgroundImage");
const props = await evaluate("JSON.stringify(Object.keys(window.__weProperties))");
return { ready, bg, props, consoleLines, exceptions };
});
check(single.ready.ok, "单档分发渲染就绪(未白屏)", single.ready.error ?? "ok");
check(/kv37_xilian/.test(single.bg ?? ""), "单档分发背景已应用", single.bg?.slice(-30));
check(!/"preset"/.test(single.props ?? ""), "单档分发确实没有 preset 属性", single.props);
check(!/"bgm"/.test(single.props ?? ""), "单档分发确实没有 bgm 属性", single.props);
assertClean("单档分发", single.exceptions, single.consoleLines);
console.log(failures === 0 ? "\n模拟器浏览器冒烟通过" : `\n模拟器浏览器冒烟失败:${failures} 处`);
process.exit(failures === 0 ? 0 : 1);