把项目从「手写 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 处引用自包含。
5.9 KiB
模拟器的契约:还原 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 条。