Files
SpineWallpaper/docs/adr/0006-simulator-contract.md
Shuery 3f11426964 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 处引用自包含。
2026-10-02 01:27:02 +08:00

73 lines
5.9 KiB
Markdown
Raw Permalink 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.
# 模拟器的契约:还原 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 条。