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

5.9 KiB
Raw Permalink Blame History

模拟器的契约:还原 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 条。