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

+72
View File
@@ -0,0 +1,72 @@
# 模拟器的契约:还原 WE 的 API 面,不自检,替身显式化
## 背景
本地调试壁纸需要在没有 Wallpaper Engine 的情况下跑起来,因此有一个"WE 模拟器"。旧实现(`tools/wallpaper-engine-simulator.js`,1589 行)在这个位置上出过**事故级**的错:它靠 `typeof window.wallpaperRegisterListener === "function"` 来判断"我是不是已经在真实 WE 里",而这个 API **在真实 WE 里不存在**——判据恒为 false,于是模拟器在真实 WE 里也会自我激活并覆盖官方 API。
WE 实际注入的网页壁纸 API 只有 `window.wallpaperPropertyListener`(外加属性/通用属性/暂停/用户目录文件变更这几个回调)。`window.wallpaperRegisterListener` 与 `window.wallpaperEngine` 都不存在。
## 决策
### 1. 不做环境自检;由注入决定它是否存在
模拟器**没有**任何"我在哪"的判断。它只在有人把一个 `<script>` 注入页面时才存在——注入是事实,不是猜测,不存在误判空间。旧实现反过来做(靠 `typeof window.wallpaperRegisterListener === "function"` 自检),而那个 API 在真实 WE 里不存在,判据恒为 false,于是它在真实 WE 里也自我激活并覆盖官方 API。事故级。
**注入点有两个,都合法,但用途不同:**
| 场景 | 谁注入 | 模拟器本体从哪来 | 产物 |
| --- | --- | --- | --- |
| `pnpm dev` | 调试服按路由注入 | 调试服自己的 `/simulator/wallpaper-engine.js` | 分发目录**保持干净** |
| 静态托管(GitHub Pages) | 构建期写进 `index.html` | 分发自带的 `./scripts/wallpaper-engine.js` | `pnpm build --with-sim` |
| 上传 Wallpaper Engine | 没有人注入 | 不存在 | `pnpm build`(默认) |
默认的发布产物里**没有**模拟器文件,也**没有**任何注入痕迹——它必须与"用户从创意工坊下载到的东西"完全一致。
#### 两条供给路径各自踩过的坑
**调试服路径**:驱动原先把动态 `import()` 指向 `/release/<dir>/scripts/wallpaper-engine.js`,而那个文件只有 `--with-sim` 才会被复制进分发——于是默认构建下 `pnpm dev` 必然 404、**面板静默消失**。404 只留在控制台里,看起来像"面板没渲染",很难联想到是构建产物缺了文件。现在调试服从自己的路由提供,与分发目录里有没有它无关。
**静态托管路径**:地址必须是**相对**的 `./scripts/wallpaper-engine.js`。GitHub Pages 把站点放在 `/<repo>/` 子路径下,根绝对路径 `/scripts/…` 会 404。这一点由 `tools/checks/verify-static-preview.mts` 把关——它刻意把分发挂在 `/repo/` 前缀后面服务,挂在根上测不出这个错。
两条路径会在 `--with-sim` 构建 + 调试服同时出现时重叠,所以驱动带幂等闸(`window.__weSimDriver`)。没有它模拟器会被 mount 两遍,页面上出现两个面板。
#### 为什么不让 `pnpm dev` 自动带上 `--with-sim`
那样能一行修好调试路径,但代价是调试时看到的分发目录多了一个文件,不再是你要上传的那份。"调试对象 == 发布对象"这条不变量比少写一行更重要。
### 2. 公开面严格等于 WE 的公开面
模拟器只提供 WE 真正有的东西,一个不多:
- `window.wallpaperPropertyListener`(**等壁纸自己注册**,绝不代注册);
- 不提供 `window.wallpaperEngine`、`window.wallpaperRegisterListener`、`wallpaperGetUserDataPath` 等 WE 没有的全局。
理由:一个凭空承诺的 API 比没有 API 更危险——壁纸会依赖它,然后在真实环境里崩。冒烟测试里有两项断言就是专门盯这个:`typeof window.wallpaperEngine === "undefined"`。
### 3. 属性下发语义
- 初始属性**一次整批**交给 `applyUserProperties`,不逐条发(逐条会让壁纸的 `render()` 被调用 N 次,掩盖真实的性能与竞态)。
- 顺序:先 `applyGeneralProperties`(fps),再 `applyUserProperties`。
- **下发前必须等到 listener 就位**。`?__propsAt=dom` 复现的正是"属性早于壁纸模块到达"这条竞态;此时 listener 还是 undefined,直接下发会被静默吞掉、页面停在默认预设上。真实 WE 不会丢(属性在消息队列里),所以模拟器等,且**只等不代注册**——代注册会掩盖"壁纸忘了在模块顶层注册 listener"这个真错误。
- 壁纸必须在模块顶层注册 listener(见 `src/runtime/index.ts`),否则属性在 `load` 之前到达时收不到。
### 4. 替身必须显式化
浏览器不是 CEF,有几件事物理上做不到。它们不被伪装成"和 WE 一样",而是列入 `window.__weSim.substituted`,并在面板顶部有一条**常驻**的「⚠ 模拟环境」横幅:
| 能力 | WE 的行为 | 模拟器的替身 |
| --- | --- | --- |
| `file` / `texture` / `directory` 选择器 | 弹原生对话框,回传 `file:///` 绝对路径 | `<input type=file>` + `URL.createObjectURL`(回传 `blob:`) |
| 用户数据目录绝对路径 | `wallpaperGetUserDataPath` 返回真实路径 | 虚拟路径 |
| `document.cookie` | CEF 里被隔离 | 浏览器里可用(未隔离) |
### 5. 装载时序
模拟器源码是 ES module(要能正常用 `import`/`export` 写类型),但**不能用 `<script type="module">` 装载**:module 脚本会被延迟到解析完成后执行,那样它就不可能早于壁纸模块就位。驱动脚本因此是一个**经典** `<script>`,内部用动态 `import()` 装载模拟器。两者都由 `tools/lib/drivers.ts` 生成,调试服与对拍测试台共用同一份。
## 未采纳的方案
- **靠某个全局变量的存在自检**:这正是旧实现的事故成因,见第 1 条。
- **提供 `wallpaperEngine` 之类的便利全局**:见第 2 条。
- **替模拟器实现真实文件路径**:浏览器做不到,伪装只会让壁纸在真机上表现不同。
- **用 `<script type="module">` 直接加载模拟器**:时序不成立,见第 5 条。