dsh-background-by-model 0.7.9 → 0.8.0-test.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.zh.md CHANGED
@@ -1,121 +1,119 @@
1
1
  # 更新日志
2
2
 
3
- > 这里保留完整的历史记录;[README.zh.md](./README.zh.md#近期优化) 的「近期优化」一节只列最近两个版本(v0.7.9 / v0.7.8)。
3
+ > 这里保留完整的历史记录;[README.zh.md](./README.zh.md#近期优化) 的「近期优化」一节只列最近两个版本(v0.8.0 / v0.7.10)。
4
4
 
5
- ## v0.7.9
6
-
7
- - **所有素材引用都钉到 assets 仓库的同一个发行 tag(`v0.1.0`),此前漂开的三处现在一致了** —— README 的三张截图读的是 `main` 分支,节日素材钉的是一个原始 commit 哈希(`HOLIDAY_ASSETS_COMMIT = '58a9e107…'`),而推荐配置是**刻意**跟着 `main` 的。三处引用,对「这个构建到底在看哪一份字节」给出了三种不同答案,而其中只有一处真的对仓库的变动免疫。现在 `HOLIDAY_ASSETS_COMMIT` 变成了 `HOLIDAY_ASSETS_REF = 'v0.1.0'`(值和名字一起改,因为它指的已经不是一个 commit),`PRESET_ASSETS_REF` 用同一个字符串,于是 `HarmlessFunny/assets` 的一个发行版就是插件所取的每一个字节的同一个版本 —— 节日素材、推荐配置和 README 的图片一起 —— 而且这个值人能读、能拿去和 GitHub 上的 tag 对照、也能在一处改完,而不是一个没人能对上日期的 40 位哈希。
5
+ ## v0.8.0
8
6
 
9
- - **节日素材的字节并没有跟着改名而变。** `58a9e107` 是 `v0.1.0` 的祖先,两张 `dsh-background-by-model/holiday/*.webp` 在这两个修订之间**逐字节相同**(blob 都是 `46ccfc89…` 和 `dc5080e4…`),所以下载还是同一次下载 —— 两个修订之间的差异是推荐配置到位(七个 `preset/modelbg-*`、`preset.json` 和 `theme-config.json`,这些在那个 commit 上还不存在)加上三张截图重拍。真正变了的是 raw 镜像也带上了 tag(`raw.githubusercontent.com/HarmlessFunny/assets/v0.1.0/…`),这才让「两个镜像服务同一个修订」是按**值**成立、而不是靠约定。
7
+ - **构建产物在打包时生成,而不是提交进仓库** —— `lib/` 是每次发版约 1 MB 的生成代码,也几乎就是这个仓库增长的全部;而让它和 `src/` 保持一致,是件得靠人记住的事。这里没有 `prepublishOnly` 钩子,所以过期的那份会静默发出去,两条流程里没有任何一步会发现。现在它被 `.gitignore` 挡住、由 `prepack` 生成 —— npm 在 `npm pack` 和 `npm publish` 时都会跑它,所以发出去的永远是从被打 tag 的那份源码构建出来的。装包的人这边没有任何变化:还是那三个文件,只是早一分钟才构建出来。
8
+ - **`github:` 安装这条路没了,这是刻意的** —— `dsh plugin --profile web add github:HarmlessFunny/dsh-background-by-model` 拉到的树里已经没有 `lib/`,也没有任何东西会去构建它,所以它只会装出一个加载不起来的插件。现在只剩 npm 一个来源,`npx` 那条也跟着它走;第三种方式(从源码跑一份工作副本)则把「必须先构建」摆在了最前面。
9
+ - **发版改由 GitHub Actions 走 OIDC 完成,于是版本带上了构建 provenance** —— npm token 彻底退出发版路径,npm 会给每个版本附上一份 provenance 证明,把它绑到产出它的那个提交和工作流上。到 v0.7.10 为止的每一个版本都是手动传上去的,什么都不能声称;从这一版起它们是可以核对的,而不只是被声明。
10
+ - **README 和这份文件都收回到 v0.3.x 那批条目的密度** —— 每个功能一句话带过,十道检查各一行,常见问题十五个一个不少、答案砍到承载事实的那一句。用户需要的东西一样没少;去掉的是「每个改动是怎么被想出来的」那部分叙述。
10
11
 
11
- - **推荐配置是刻意放弃「不发版就能修」的** —— 它此前跟着分支,因为它要求不可变的理由和节日素材正好相反:推荐配置的失败是**响亮的**(用户正盯着那个按钮,会拿到一个点名缺哪个槽位的错误),而节日是在没人盯着的那一天静默失效;所以钉版本在这里什么都买不到,而分支买到了「改一个 JSON 就能修好一个不好的推荐」。现在把这个交易换掉了。分支同时还买来了「插件的字节可能在用户装上之后、点下去之前发生变化」;现在一个 tag 同时覆盖截图、节日素材和这份配置,所取的每一个字节都不会再这样。代价是写明的而不是等着被发现的:改一个 JSON 现在要花一个 `assets` 里的 tag、`src/index.ts` 里的一个新值,以及一次 npm 发版 —— 而 tag 不是 commit。它**可以**被重新指向,而 jsDelivr 把版本引用按不可变处理,所以重新指向要发一次 purge 请求而不是 push,于是「修正」的形态是发一个新 tag,而不是强推旧的。这也正是 `presetKey` 在这次改动后仍然保留的原因:一个没清过缓存的边缘节点仍然会回同一个 URL 的上一次字节,所以两次取「这份配置」依然不保证是同一个修订,恢复下载的那一次不能把两个修订拼在一起。
12
+ ## v0.7.10
12
13
 
13
- - **门禁** —— `check:node` 里有两条断言钉的是旧设计,而它的 `revision()` 辅助函数根本看不见 tag:它匹配的是 40 位 SHA,而这也正是 raw 镜像的引用当年能不靠 `@` 被取出来的唯一原因。现在它从仓库名旁边匹配(jsDelivr 形状是 `HarmlessFunny/assets@<ref>`,raw 形状是 `HarmlessFunny/assets/<ref>/`),于是两个镜像仍然可以互相比较;两条断言都要求 `v0.1.0` —— 节日素材那条变成「pinned to the assets release tag, never to a branch」,推荐配置那条变成「read at the assets release tag, not at a branch」,取代了此前**故意**钉住分支的那一条。`pnpm test` 十道全过,`lib/` 就是那一次运行的产物。
14
-
15
- ## v0.7.8
14
+ - **节日画的缓存现在按插件所钉的发行版做键,于是更新过的插件拿到的是重新下载,而不是上一代的字节** —— 缓存住在 `holiday-cache/<asset>`,而 `readHolidayAsset` 只要那个文件存在且是一张合法图片就直接拿它作答:不比版本、不联网、不留日志。于是把一个构建指向 assets 仓库的新发行版,改变的是它本会去取的 URL,而实际返回的东西一点没变。现在 `holidayCachePath` 带上了 tag(`holiday-cache/v0.1.0/mid-autumn.webp`),发行版成了文件身份的一部分:钉了新 tag 的构建看的是一个没人写过的目录,更新之后遇到的第一个节日是一次缓存未命中,而 `ls holiday-cache/` 能回答这张盘上留着哪几代。
15
+ - **被取代的那一代会被清掉,而且是在新图落盘之后、绝不在之前。** 光有带版本的键会让每一代永远留在那里 —— 每代约 730 KB,还包括后续发行版已经不再发布的素材。`pruneHolidayCache` 在一次成功的缓存写入末尾运行,删掉名字不是本构建 tag 的代号目录,所以一个取不到的发行版不会让用户丢掉他已经有的画。它是尽力而为,`resetStore` 依然不动任何一代。
16
+ - **门禁** —— `check:node` 的两条 URL 断言与缓存路径现在都读同一个 `REF`,并钉住:所钉那一代里缺失的素材会被重新下载而不是读到旧的、被取代的那一代在新图落盘后消失、正在用的那一代不被碰到。`pnpm test` 十道全过。
16
17
 
17
- - **新装打开的就是一条空白规则,而不是「还没有规则」那一屏** —— 报告是「图一是刚安装上的情况,什么也没有,我希望刚安装上就如图二一样创建一条空白的规则」。成因是两种形状被读成了同一件事:完全没有配置文件时 `migrateLegacy` 返回的是 `rules: []`,而 `normalizeConfig` 把那个空数组当成「用户把规则删光了」。现在只有「`rules` 这个键根本不在」才意味着「从未配置过」,它换来一条空白规则 —— id 是 `r1`,也就是 `nextRuleId` 给空列表发的那个 id,所以它在面板上就是规则 1、就是兜底、就是默认选中的那张卡,下游没有任何分支需要认得它是被种出来的;而「键在、数组是空的」仍然是用户主动删光的结果,保持为空。按条数判断就没有这条区分,规则会在每次读盘时长回来。规则工厂本身挪进了 `src/schema.ts`(`blankRule`),出厂配置、`normalizeConfig` 和客户端的「新增规则」从此共用同一个形状,客户端的 `newRule` 随之删掉。种出来的那条规则是刻意「空」而不是「半成品」:它没有图、也没有自己的主题色,所以在被填上东西之前 `ruleCanPaint` 会让匹配跳过它,而不是让一个空背景顶掉规则 1 的兜底。
18
+ ## v0.7.9
18
19
 
19
- - **placeholder 不是「挑亮了」,是那条声明从来就没生效** —— 报告是「这个输入框的 placeholder 太亮了,让人以为真的有文字在里面」。`--dsw-alias-label-quaternary` 在整个 harness 主题里一个定义都没有:未定义的自定义属性会让这条声明在计算值阶段被丢掉,而 `color` 是继承属性,于是 placeholder 回落到 `inherit` —— 拿到输入框自己的 `label-primary`,也就是面板里最亮的墨色,写在一个用户一个字都没打过的框里。现在三个带 placeholder 的输入(规则匹配串、图片网址、轮换的自定义秒数)共读一条规则、统一用 `--dsw-alias-label-caption`:弱一档,也正是 `PLACEHOLDER_RULE`(`src/client/wallpaper.ts`)早就给输入框用的那个 token,所以插件对「空」这件事只有一个说法。同一个死名字还有两处:`.dab-brand-tag` 写的是「quaternary 加 fallback」,所以它一直画的就是 tertiary,去掉死名字不改变任何颜色;`.dab-pick-n` 的 `--dsw-mono` 则是少了个字母 —— 插件自己的变量是 `--dab-mono`。
20
+ - **所有素材引用都钉到 assets 仓库的同一个发行 tag(`v0.1.0`),此前漂开的三处现在一致了** —— README 的截图读 `main`,节日素材钉的是一个原始 commit 哈希(`HOLIDAY_ASSETS_COMMIT`),推荐配置是刻意跟着 `main` 的:三处引用,对「这个构建在看哪一份字节」给出三种答案。现在是 `HOLIDAY_ASSETS_REF = 'v0.1.0'`,`PRESET_ASSETS_REF` 用同一个字符串,于是 `HarmlessFunny/assets` 的一个发行版就是插件所取的每一个字节的同一个版本 —— 节日素材、推荐配置和 README 的图片一起 —— 而且这个值人能读、能拿去和 GitHub 上的 tag 对照、也能在一处改完。
21
+ - **节日素材的字节并没有跟着改名而变。** `58a9e107` 是 `v0.1.0` 的祖先,两张 `holiday/*.webp` 在这两个修订之间逐字节相同,所以下载还是同一次下载。真正变了的是 raw 镜像也带上了 tag,这才让「两个镜像服务同一个修订」是按值成立、而不是靠约定。
22
+ - **推荐配置是刻意放弃「不发版就能修」的** —— 它此前跟着分支,因为它要求不可变的理由和节日素材正好相反:推荐配置的失败是响亮的(用户正盯着那个按钮),而节日是在没人盯着的那一天静默失效。这就是换掉的东西:改一个 JSON 现在要花一个 `assets` 里的 tag、`src/index.ts` 里的一个新值,以及一次 npm 发版。而 jsDelivr 把版本引用按不可变处理,重新指向一个 tag 要发 purge 请求而不是 push —— 这也正是 `presetKey` 仍然保留的原因:一个没清过缓存的边缘节点仍然会回同一个 URL 的上一次字节。
23
+ - **门禁** —— `check:node` 的 `revision()` 辅助函数匹配的是 40 位 SHA,根本看不见 tag;现在它从仓库名旁边匹配(`assets@<ref>` 与 `assets/<ref>/`),于是两个镜像仍然可以互相比较,两条断言都要求 `v0.1.0` 而不再钉分支。`pnpm test` 十道全过。
20
24
 
21
- - **推荐配置的下载按文件重试,而且失败的那一次会把已经下到的部分留给下一次** —— 此前一个文件只有两次机会(两个镜像各一次),七张壁纸里第六张碰上一次 502、一次连接被重置或者一次 CDN 抽风,前面六张就白下了,而用户唯一的办法 —— 再按一次按钮 —— 是从零开始。现在每个文件最多三轮、轮与轮之间先等 400 ms 再等 800 ms(`src/preset.ts` 的 `PRESET_ATTEMPTS` / `presetRetryDelayMs`),而且失败的那一次会把已经拿到的字节留在那里(`presetHeld`):下一次按按钮先重新取 manifest 与 config(两个小文件),只有当这份 config 与那些字节来自的那一份**逐值相同**时才复用(`presetKey`,把对象键排序后的规范形式 —— 上游把键顺序重建过一遍不该白白丢掉这些字节),于是「第六张失败」从重下七张变成只重下一张。三种失败**不重试**,每一种都有自己的理由:404/403 是「这个文件不在那儿」(配置跟随的是分支,作者改个文件名就必须**快**而响地报到槽位上,而不是再等两轮)、超限是「字节本身就是问题,下一轮还是这些字节」、超时是「六十秒的沉默不是天气」(两个镜像在同一轮里都已经问过了,而这一条正是把单个文件的最坏情况钉在三分钟而不是六分钟上的东西:两个镜像都不吭声的那一轮当场就结束,也就是原来那两分钟;只有「一个镜像沉默、另一个是瞬态失败」混在一起的那一轮,才会把一分钟带进下一轮)。**严格性一个字没改**:第一个拿不到的文件仍然让整次尝试失败、仍然报出槽位名 —— 变便宜的只是重试,不是失败。按钮上那个计数现在描述的是**整份配置**而不是这一次的传输,所以续传的那一次从 6/7 开始、到 7/7 结束(这也是这次改动**没有**碰界面任何一处的原因)。节日素材刻意**没有**跟着改成多轮:它是在启动时、当天第一次绘制之前抓的,一轮的等待会直接加到首帧上 —— 而它有缓存,此后每一次读取都会再试一次,按钮却没有「此后再读一次」这回事。
25
+ ## v0.7.8
22
26
 
23
- - **门禁** —— `pnpm test` 现在是**十道**(新增 `check:preset`)。`check:node` 新增:空数据目录读出来正好一条规则、那条规则是空的(无匹配串、无颜色、无图)、它是规则 1(因此兼任兜底)、显式写空的规则列表读回仍然为空(这几条就是新装那条规则的形状,以及那条不该被种回来的区分);`resetStore` 之后那条断言改成同一语义 —— 它原来钉的正是旧行为(「读到一个空配置」)。重试本身按**请求数**钉住:两个镜像同时 503 一次时,同一个文件被再问一遍并最终拿到(日志里三行:jsDelivr、raw、又是 jsDelivr)、托管处没有的文件仍然只花两次请求(每镜像一次、不等待)、永久 500 的镜像总共只花六次请求(三轮 × 两个镜像 —— 重试是有预算的)。`check:ui` 新增第 6 节:UI 里每一个 `var(--dsw-…)` 都必须是已安装主题确实发布过的 token —— 读不到主题(peer 依赖不在)时这一节跳过并说明,因为那是关于这台宿主的事实,不是关于这个仓库的;这一类失败本来不会在任何地方报错,声明被丢掉、属性悄悄回落到继承值,只有人眼看出来不对。新的 `check:preset` 钉住策略本身与两侧的接线:六个失败原因各自「值不值得再问一次」(三个终态正是能在本地廉价判定的那三个,而超时之所以不重试是因为同一轮里两个镜像都已经问过)、等待指数增长且有上限、`presetKey` 对同一个值的不同键顺序给出同一个键(而图片列表的顺序**算数**,因为它就是轮换顺序)、续传拿到的是副本而不是那条记录本身、以及两侧真的在用这些判定 —— node 半边循环轮次并按策略决定要不要再来一轮、客户端按「还缺哪些」而不是整个槽位表来铺并发池、失败时把已下到的字节留成 `presetHeld`、成功时清掉、以及体积上限**在文件被收下之前**判定(收下之后再判的话,超限的那一次会把每个槽位都留在手里,下一次续传于是无文件可抓、直接把一份超限的配置放行)。
27
+ - **新装打开的就是一条空白规则,而不是「还没有规则」那一屏** —— 完全没有配置文件时 `migrateLegacy` 返回 `rules: []`,而 `normalizeConfig` 把那个空数组当成「用户把规则删光了」。现在只有「`rules` 这个键根本不在」才意味着「从未配置过」,它换来一条空白规则 —— id 是 `r1`,所以它在面板上就是规则 1、就是兜底、就是默认选中的那张卡。键在、数组是空的,仍然是用户主动删光的结果,保持为空。规则工厂挪进了 `src/schema.ts`(`blankRule`),出厂配置、sanitizer 与客户端的「新增规则」共用它。
28
+ - **placeholder 不是「挑亮了」,是那条声明从来就没生效** —— `--dsw-alias-label-quaternary` 在整个 harness 主题里一个定义都没有,声明在计算值阶段被丢掉,于是 placeholder 回落到 `inherit`,拿到输入框自己的 `label-primary`。现在三个带 placeholder 的输入统一读 `--dsw-alias-label-caption`,也正是 `PLACEHOLDER_RULE` 早就给输入框用的那个 token。同一个死名字还有两处:`.dab-brand-tag` 那个死别名,以及 `.dab-pick-n` 的 `--dsw-mono`(插件自己的变量是 `--dab-mono`)。
29
+ - **推荐配置的下载按文件重试,而失败的那一次会把已经下到的部分留给下一次** —— 此前一个文件只有两次机会(两个镜像各一次),七张壁纸里第六张碰上一次 502,前面六张就白下了。现在每个文件最多三轮、先等 400 ms 再等 800 ms,失败的那一次把已经拿到的字节留下,只有当 config 与那些字节来自的那一份逐值相同(`presetKey`)时才复用。404/403、超限、超时三种不重试,各有各的理由。严格性一个字没改 —— 第一个拿不到的文件仍然让整次尝试失败、仍然报出槽位名 —— 而按钮上那个计数现在描述的是整份配置,所以续传的那一次从 6/7 开始。
30
+ - **门禁** —— `pnpm test` 现在是十道(新增 `check:preset`)。`check:node` 钉住新装那条规则的形状、以及「显式写空的列表不会被种回来」的区分,并按请求数钉住重试本身:一次瞬态 503 三行日志、托管处没有的文件两次请求、永久损坏的镜像六次。`check:ui` 新增一节,要求 UI 里每一个 `var(--dsw-…)` 都是已安装主题确实发布过的 token,读不到主题时跳过并说明。
24
31
 
25
32
  ## v0.7.7
26
33
 
27
- - **新装出来就是作者本人那套外观,而且不用联网** —— 「界面」页的出厂默认值现在**就是**推荐配置里那套数字([assets 仓库](https://github.com/HarmlessFunny/assets)里的 `preset/theme-config.json`):主背景 `0%`、左侧栏 `10%`、右侧栏 `45%`、卡片 `100%`、代码块 `81%`、输入框与控件 `88%` 外加 `60 px` 模糊、设置面板 `100%`、对话文本框 `35%`、轨迹页 `73%`、其余模糊全 `0`。于是第一次装上、一个字节都还没下,界面就已经值得看;而推荐配置那一下点击加进来的是规则和图片,不是把底下的数字换掉。这些数字此前**每一个都是按「安全」而不是按「好看」逐项挑的** —— `bg` 取 0.85 是为了让宿主表面在壁纸之下仍然可见,`code` 全不透明是因为「代码块是拿来读的、不是拿来看穿的」,`input` 模糊取 0 是因为一层模糊要付一个合成层的代价 —— 这就是为什么以前新装打开的是一个「宿主的苍白近似」,而不是任何人真会用的配置。两个后果是写明的、不是等着以后被发现的:主背景从 `0%` 起步,所以还没有壁纸时窗口就是宿主自己的画布(配色与墨色不受影响,它们来自主题色而不是这个 alpha);而 `check:code-plate` 现在钉住的是配置里的 `81%`,不再是「默认不透明」。这些常量住在 `src/schema.ts`,旁边就写着它们是从真实数据目录抄出来的那一份,所以改推荐配置的数字就得把两边一起搬;README 的「界面」一节说的是同一件事。
28
-
29
- - **`rightbarOpacity` 保留了它「从未被拥有」的状态,而新装出来是已被拥有的** —— 预览栏的 `null` 意思是「跟随 `opacities.bg`」,而它不是一个理论状态:此前**每一个**版本都写过它 —— 用户从没拖过的那一栏,落盘就是显式的 `null`。所以新的净化器(`normalizeRightbarOpacity`)把 `null` 读回成它自己,只有「这个键根本不在」才拿到出厂的 `45%`。把 `null` 当成「缺失」,会把每一份「预览栏正跟着主背景」的既有配置悄悄重画 —— 而这正是这套配置形状做版本号要防的那种静默迁移。新装从 `45%` 起步、是已被拥有的;`null` 现在只剩下手写可达,因为界面从来就没有一个能回到它的控件。
30
-
31
- - **节日开关出厂是关的** —— 第一次装上只画模型规则说的东西,别的什么都不画;「配置」页那一个开关才是「要这个功能」的表达方式。它此前是开的,理由是「这个功能的意义就在于到日子背景自己换」;那个理由对于一份**已经被人选定的**配置是对的 —— 所以推荐配置仍然把它**打开** —— 但对于一个刚装上、自己还没有任何壁纸的插件不对:中秋当天,新用户看到的第一样东西会是一张没人要的节日图。净化规则也从「只有显式的 `false` 才关掉」改成「真正的布尔值算数,其它一律取出厂默认」:功能出现之前写下的配置里那个缺失的字段、以及手改出来的垃圾值,现在都意味着关。块内每个节日各自那个开关没动 —— 那是一个已经打开的功能内部的细节,不是这个开关本身。
32
-
34
+ - **新装出来就是作者本人那套外观,而且不用联网** —— 「界面」页的出厂默认值现在**就是** `preset/theme-config.json` 里那套数字:主背景 `0%`、左侧栏 `10%`、右侧栏 `45%`、卡片 `100%`、代码块 `81%`、输入框与控件 `88%` 外加 `60 px` 模糊、设置面板 `100%`、对话文本框 `35%`、轨迹页 `73%`。这些数字此前每一个都是按「安全」而不是按「好看」逐项挑的,这就是为什么以前新装打开的是一个宿主的苍白近似。这些常量住在 `src/schema.ts`,旁边写着它们是从真实数据目录抄出来的那一份。
35
+ - **`rightbarOpacity` 保留了它「从未被拥有」的状态,而新装出来是已被拥有的** —— 预览栏的 `null` 意思是「跟随 `opacities.bg`」,而此前每一个版本都写过它,因为用户从没拖过的那一栏落盘就是显式的 `null`。所以 `normalizeRightbarOpacity` 只有「这个键根本不在」才给出厂的 `45%`;把 `null` 当成缺失,会把每一份「预览栏正跟着主背景」的既有配置悄悄重画。
36
+ - **节日开关出厂是关的** —— 第一次装上只画模型规则说的东西,别的什么都不画,「配置」页那一个开关才是「要这个功能」的表达方式。推荐配置仍然把它**打开**。净化规则改成「真正的布尔值算数,其它一律取出厂默认」:功能出现之前写下的配置里那个缺失的字段、以及手改出来的垃圾值,现在都意味着关。
33
37
  - **「界面」页每张卡片上那个百分比胶囊删掉了** —— 那个数字本来就在它正下方的滑块行上,于是卡头又只剩图标和名字,`.dab-part-badge` 连同它的样式规则一起走。
34
-
35
- - **门禁** —— `pnpm test` 仍是九道;上面这些默认值动到的是 `check:code-plate`(代码块出厂走配置的 `81%`)与 `check:node`,后者的新断言是**按值**写死的,所以「改了常量没改配置、或改了配置没改常量」会直接让一道校验失败,而不是慢慢漂开:完全没有配置文件时读出来的那套外观数字、显式 `null` 的预览栏读回成它自己、它自己的数字被保留、非数字的手改值落回出厂的 `45%`,以及节日开关两个布尔方向加一个非布尔值。
38
+ - **门禁** —— `pnpm test` 仍是九道;上面这些默认值动到的是 `check:code-plate` 与 `check:node`,后者的新断言是**按值**写死的,所以「改了常量没改配置、或改了配置没改常量」会直接让一道校验失败,而不是慢慢漂开。
36
39
 
37
40
  ## v0.7.6
38
41
 
39
- - **规则页从一摞会自己长大的卡片变成一条选项卡 + 一个编辑器** —— 报告是「这个界面如果规则多且不收起的话会拉的很长」。此前每条规则是一张自展开的卡片,页面的高度等于所有卡片之和,只有用户手动一张张收起才会缩短。现在**每条规则是一个选项卡,页面上只挂载一个编辑器**:`ModelBgPage` 持有选中项(`selId`),`RuleCard` 不再拥有开合状态、永远渲染,它就是那个面板。于是页面高度只跟「一张卡片 + 一条选项卡」有关,与规则条数无关。选项卡把原本只在卡头上才看得到的东西搬了上来 —— 序号、首图缩略图、匹配串,以及两条不用展开就该知道的状态(它是不是当前生效的那条、它在轮换还是被关掉了);条子横向滚动而不是换行,理由和上面一样:一换行就把高度还给了规则条数,而且选项卡固定 34px、匹配串省略号截断,所以切换规则不会让整行抖动。**「+」和「切换哪条规则」这两个动作挪到了滚动区之外**,作为图标贴在条子右端:放在里面时,一个长列表里**唯一能新增规则**的控件会被滚走。条子的滚动条现在也用宿主自己那套 `--dsw-scrollbar-*`(5px、圆角、layer-2 染色、无步进箭头),此前它在面板那条细竖向滚动条旁边显示的是平台默认的粗浅色带箭头版本;选择器全部限定在 `dab-` 类上,因为这个样式表是注入宿主页面的,一个裸的 webkit-scrollbar 选择器会把整个 harness 一起改掉。**匹配测试器搬到了「资料」页** —— 它回答的是「这个模型落在哪条规则上」这个关于整份配置的问题,读一次就够,不是编辑某条规则时要反复看的东西,而在规则页上它把选项卡整整推下去一张卡片的高度;宿主过期的提示一起走了,因为那同样关乎整份配置的写入。选项卡背后那两个判定是纯的,现在住在 `src/client/rules-view.ts`(编辑器显示哪条规则、删除之后落在哪条 —— 落在邻居上,永远不是规则 1),由新的 `check:rules` 钉住,它同时还钉住布局所依赖的页面结构:只画一个编辑器、没有旧的堆叠容器、没有逐卡片的开合状态、条子既不换行也不长高、滚动条样式块仍然被限定住。另外页面描述去掉了 `max-width:56ch` —— 它让那段话提前换行、在旁边留出一块死空间,而下面的卡片是跑满宽度的。
40
-
41
- - **轮换卡片的控件按你的要求重新排了一遍** —— 四处改动,都是明确的请求(「'一小时'删掉,'切换模型时也换一张'放到'随机'旁边,'下一张、正在显示...'放到外面'轮换这组图'旁边,常驻」「把多图轮换板块放到主题色板块之下,把图二元素删掉」)。**常驻的手动换图**:此前「下一张」按钮和它旁边那句「正在显示第 2 / 3 张」被关在 `rotatable && enabled` 的判定里,于是它们会跟着轮换开关一起消失 —— 而那句读数正是这张卡片上**唯一**说明当前是哪张图的地方。现在按钮住在「轮换这组图」那一行的外侧,对所有规则状态都在,判据只有一条:当前生效的规则有两张以上图就可用(这正是 `rotateNow` 自己的判据,它只问生效规则和 `nextIndex`,从来不问 `rotate.enabled`);不足两张时按钮禁用、读数整句省略而不是印一个「0 / 0」。**「切换模型时也换一张」挪到顺序那一行**,和「顺序」「随机」并排:三者都在回答「什么时候换」,缺了它的那一行读起来像是把话说完了。**「1 小时」从停留时长预设里删掉**,剩五个(10 秒 / 30 秒 / 1 分 / 5 分 / 30 分)加自定义输入 —— 自定义输入照旧接受 3600 秒,5 秒到 24 小时的夹紧范围没变。**「图二元素」—— 那排八个固定预设色板 —— 删掉了**:滚轮、数值输入、从本图提取、吸管、跟随系统主题都还在,那一排只是通往同一个 setter 的快捷方式。它是 `state.ts` 里的 `PALETTE` 表、`ui.css.ts` 里的 `.dab-swatches` / `.dab-swatch` 规则(以及共享描边分组里的 `.dab-swatch:focus-visible`)唯一的读者,这些跟着它一起走了;`.dab-swatch-title` 和 `.dab-swatch-lg` 只是名字前缀相同,未受影响。**整个轮换块移到了第二列**,在主题色和壁纸滑块之下:它此前夹在胶片条和布局模式之间,在宽版两列卡片里是同一个位置,但在折叠成单列的窄版(`~760px` 以下,`.dab-rule-cols`)里,它是卡片里最高的一块 —— 一个带标题的开关、一个内部三行的带框面板 —— 正好落在图片和编辑图片的颜色控件中间。块内部一个字段都没动,就是同一段 JSX 挪了位置。两份 README 里描述那排色板的地方(「灵感色板」/ "an inspiration palette")、停留时长列表里的「1 小时」、以及手动换图的位置都相应改了。
42
-
43
- - **节日画不再随包发布:当天按需下载、按素材名缓存** —— 两张节日壁纸(228 KB + 500 KB)此前打进包里,占了整个 tarball 的三分之二 —— 为了一张一年只被看一天的画,而且是包里唯一属于「图片」而不是「代码」的东西,还要把它的二进制历史永久留在 `.git` 里。现在它们托管在项目自己的 [`HarmlessFunny/assets`](https://github.com/HarmlessFunny/assets) 仓库(和 README 截图放在一起,但目录分开标注),节点半边在节日当天第一次读取时下载、校验、写进 `~/.dsh/.dsh-background-by-model-data/holiday-cache/`。**引用钉死在 commit 上,不是分支**:运行时素材必须是不可变的 —— 分支引用意味着以后整理一次文档截图,就能让一个已经发布的功能在某个没人盯着的那天静默失效、日志里什么都不说;而 jsDelivr 对 commit 引用永久缓存,正是一个永不改变的文件想要的。**两个镜像**:`cdn.jsdelivr.net` 主用(在 `raw.githubusercontent.com` 不通的网络里也能到),raw 兜底,两个都指向同一个 revision,所以回退不可能混版本。**下载失败不是一个异常路径,而是一个正常状态**:两个镜像都不通,这个节日今天就不接管,模型规则继续画 —— `pickHoliday` 那道「槽位里到底有没有字节」的闸门本来就是为这个写的。**缓存条目会被校验而不是被信任**:不是图片的缓存文件按未命中处理并在下次下载时覆盖,否则一个写到一半被杀掉的进程会让这个节日永久报废,而用户根本不知道有那个文件存在;写入走临时名 + rename,读到一半的文件不可能被看见。**两条不变量一个字都没改**:缓存按**素材名**(`mid-autumn.webp`)命名,刻意放在槽位命名空间之外,所以 `writeImage` 照旧拒绝节日槽位、数据目录里照旧永远不会出现 `modelbg-h-*`。`globalThis.fetch` 在 `check:node` 里被整体替换成桩,测试因此不依赖任何 CDN 是活的,也不去断言一个不属于自己的文件今天有多少字节;新增的断言覆盖:加载插件和读配置不发起任何下载、冷读一次就够、第二次读盘不再传输、两次并发冷读共用一次传输、第一个镜像死了会回退、失败的抓取什么都不缓存、镜像返回 200 但内容是 HTML 会被拒绝、损坏的缓存会被重新下载、两个镜像指向同一个 commit。
44
-
45
- - **一键装上「推荐配置」** —— 「配置」页多了第三张卡,从同一个 [`HarmlessFunny/assets`](https://github.com/HarmlessFunny/assets) 仓库下载作者本人正在用的那套配置并就地覆盖你的。**它托管在数据目录自己的形状里**:`preset/preset.json`(形状闸门)、`preset/theme-config.json`(6.5 KB 可读 JSON)和 `preset/modelbg-<slot>`(每个槽位一个原始文件)。第一版是单个主题文件、所有壁纸内联成 base64,2.9 MB 里 99.8% 是图片 —— 在几十万字符的一行里找一个 `match` 字符串不是任何人能做的手改,格式就是为这个改的;换过来之后,改配置是在 GitHub 上编辑 6.5 KB 的 JSON,换壁纸是替换一个文件,发布是从 `~/.dsh/.dsh-background-by-model-data/` 里 `cp` 一下。node 半边新增 `fetchPresetConfig`(取 manifest 与 config、在**这一侧**跑共享 sanitizer、再从这个配置推导出要请求哪些文件)与 `fetchPresetImage`(按槽位取一个文件,魔数校验后返回 data URL),旧的单文档 `fetchPreset` 删掉;`fetchHolidayAsset` 上一版拆出的通用 `fetchAsset` 被两者复用。**请求清单从配置推导,而不是来自第二份清单**:加一张壁纸就是在 config 里加一条并放进同名文件;反过来说,一份不受信任的配置决定了这个进程会去网络几次,所以每个槽位名都要过 `SLOT_RE`(在 `normalizeImage` 里),数量另有上限。**config 里合法存在的两个节日槽位会被剔掉而不是去请求** —— `holidays.items` 里必然有 `h-midautumn` / `h-nationalday`,而数据目录里永远不可能有 `modelbg-h-*` 文件(上一版刚加的硬断言),照单去请求会让整个功能在一份插件自己保证不存在的文件上失败。客户端**一个文件一个请求**地取、同时最多四路 —— 实测同一份配置顺序取 8.3 秒、并发取 4.8 秒,因为付的是 HTTP/2 上的每请求延迟 —— 按钮上显示 `3/7`;每个文件因此有自己的镜像回退与自己的校验。**少一张就整体失败并报出是哪个槽位**:一份悄悄少了图的配置恰恰是「节日素材钉死 commit」那条契约要防的静默失效,把它报出来才让分支引用在这里站得住。落地仍然走本地文件那条 `applyThemeFile`(在客户端组装成 `exportTheme` 写出的形状),所以版本闸门、sanitizer、清空都还是同一条路;`preset.json` 让版本闸门在请求第一张壁纸之前就能生效。它仍然是这个面板里唯一会销毁东西的操作:下载在**提问之前**(确认框因此能如实说出手里有什么),清空在**下载成功之后**(CDN 不通不该让你在等一份永远没来的配置的路上先弄丢自己的配置),一个旧到写不了这套形状的宿主在删除任何东西之前就被拒绝,`holiday-cache/` 不碰,配置也从不落盘缓存。`check:node` 的桩新增了「按文件名供应一整个目录、缺名就是 404」这一档,覆盖:manifest 与 config 按序取且当场 sanitize、请求清单由规则推导且节日槽位被剔掉、一张壁纸一个请求且类型是嗅出来的、对单个文件回落到 raw、config 点名而托管处没有的文件是一个错误、节日槽位与畸形槽位名在任何请求之前就被拒绝、manifest 版本不是数字、写给更新形状的配置在请求第一张壁纸之前被拒绝、config 不是 JSON / 是数组 / 图片数超限、以及镜像谎报超长。
46
-
47
- - **发布包从 1.1 MB 瘦到约 220 kB** —— 除了上面那两张节日画,另外两样东西也在占地方,而且都不该由下载这个插件的人来付。**文档截图搬走了**:`example_img/`(三个 webp,220 KB)移到同一个 assets 仓库的 `dsh-background-by-model/` 目录下,两份 README 改用 jsDelivr 绝对地址引用 —— 这个插件此前专门把截图放进 `files` 是为了让 npm 页面上的图不裂,而绝对地址同样做得到,还不用把二进制塞进包里、更不用写进 `.git` 的历史。`screenshots.json` 删掉了:翻遍整个 DSH 安装目录和 `~/.dsh` 都没有任何消费方。**`lib/client.js.map` 不再发布**:它是给浏览器 bundle 用的源码映射,624 KB,对装包的人没有用处,仓库里仍然保留(要调试本地构建的人本来就该对着本地构建调)。10 个文件、约 220 kB packed / 690 kB unpacked,此前是 13 个文件、1.1 MB / 2.0 MB。(这两组数字刻意写成约数 —— 这条记录和两份 README 本身就是这个数字的一部分,每改一次就把它推高一点。这个数字想说的是:图片搬走之后,包里剩下的确实只有代码和文档。)
42
+ - **规则页从一摞会自己长大的卡片变成一条选项卡 + 一个编辑器** —— 每条规则是一个选项卡,页面上只挂载一个编辑器,于是页面高度只跟「一张卡片 + 一条选项卡」有关,与规则条数无关。选项卡把原本只在卡头上才看得到的东西搬了上来:序号、缩略图、匹配串,以及两条不用展开就该知道的状态(是不是当前生效、在轮换还是被关掉);条子横向滚动而不是换行,理由相同。「+」和「切换哪条规则」挪到了滚动区**之外**,因为放在里面时,一个长列表里唯一能新增规则的控件会被滚走。匹配测试器搬到了「资料」页 —— 它回答的正是关于整份配置的问题。选项卡背后那两个判定是纯的,住在 `src/client/rules-view.ts`,由新的 `check:rules` 钉住。
43
+ - **轮换卡片的控件按你的要求重新排了一遍** —— 「下一张」与「正在显示第 2 / 3 张」移出了轮换开关的判定(那句读数是这张卡片上唯一说明当前是哪张图的地方,却跟着开关一起消失);「切换模型时也换一张」挪到顺序那一行,三者都在回答「什么时候换」;「1 小时」从停留时长预设里删掉;那排八个固定色板删掉了,连同它唯一的读者(`state.ts` 里的 `PALETTE` 表与 `.dab-swatch*` 规则);整个轮换块移到第二列、主题色之下,离开折叠成单列时卡片里最糟的那个位置。
44
+ - **节日画不再随包发布:当天按需下载、按素材名缓存** —— 两张节日壁纸(228 KB + 500 KB)占了整个 tarball 的三分之二,为了一张一年只被看一天的画。现在它们托管在 [`HarmlessFunny/assets`](https://github.com/HarmlessFunny/assets),节点半边在节日当天第一次读取时下载、校验、写进 `~/.dsh/.dsh-background-by-model-data/holiday-cache/`。引用钉死在 commit 上而不是分支;`cdn.jsdelivr.net` 主用、raw 兜底,两者指向同一个 revision;下载失败是一个正常状态,所以模型规则继续画;不是图片的缓存条目按未命中处理而不是被信任。缓存仍然按**素材名**命名,刻意放在槽位命名空间之外,所以 `writeImage` 照旧拒绝节日槽位。
45
+ - **一键装上「推荐配置」** —— 「配置」页多了第三张卡,从同一个 assets 仓库下载作者本人正在用的那套配置:`preset/preset.json`(形状闸门)、`preset/theme-config.json`(6.5 KB 可读 JSON),以及每个槽位一个 `preset/modelbg-<slot>` 文件。第一版是单个主题文件、所有壁纸内联成 base64 —— 2.9 MB 里 99.8% 是图片,不是任何人能做手改的文档。请求清单从配置推导而不是来自第二份清单,所以每个槽位名都要过 `SLOT_RE`、数量另有上限;config 里合法存在的两个节日槽位会被剔掉而不是去请求;少一张壁纸就整体失败并报出是哪个槽位。它仍然是面板里唯一会销毁东西的操作:下载在提问**之前**,清空在下载成功**之后**。
46
+ - **发布包从 1.1 MB 瘦到约 220 kB** —— 文档截图搬到同一个 assets 仓库、两份 README 改用 jsDelivr 绝对地址引用(当年把它们放进 `files` 就是为了这个),`screenshots.json` 删掉(没有任何消费方),`lib/client.js.map`(624 KB)不再发布但仓库里仍然保留。10 个文件、约 220 kB packed / 690 kB unpacked,此前是 13 个文件、1.1 MB / 2.0 MB。
48
47
 
49
48
  ## v0.7.5
50
49
 
51
- - **布局模式属于图片,不属于规则** —— 适应 / 填充 / 拉伸 / 平铺 / 居中 此前是每条规则一个值、覆盖一轮轮换里的每一张图;这跟 0.7.1 把取景下放是同一条理由:一轮里既有横构图照片又有竖屏截图时,没法让一张留黑边、另一张铺满,总得有一张接受另一张的妥协。现在模式存在 `images[].bgMode` 上,从**正在画的那张图**读回(`rBgMode`),胶片条下面那排胶囊编辑的也是那里选中的那张图 —— 走的是取景编辑器同一条按槽位寻址、过 sanitizer、以 `repaintIfMoved` 收尾的写入路径。规则还没有图片时这排胶囊是**禁用**而不是隐藏:新规则天生是空的,把它抽掉就等于让卡片自己那句「点这个上传」去解释一个用户从没见过的控件。README 与 README.zh 里凡是说「模式属于规则」的地方都改了。
52
- - **配置格式升到 6,因为这次迁移两个方向都是无声的(修复)** —— schema 5 的宿主会按自己认识的键重建每一个图片条目,写进去的逐图模式会在第一次写入时消失(症状就是设成 拉伸 的图变回 适应);而 schema 6 读一份 schema 5 的配置时**必须**把规则级的值抬到它的每一张图上,否则所有现成壁纸都会掉到默认值。`normalizeImage` 多了一个承载它的参数,在 `normalizeRule` 里只解析一次;0.7 之前那个单 `slot`/`bgState` 老形态走同一条抬升路径,`migrateLegacy` 则直接写成新形状。`bgMode` 进了 `LEGACY_RULE_KEYS` —— 它是被**刻意消费**掉的迁移字段,不该在宿主日志里报成漂移。动手前先拿一份真实配置的副本验过:规则上的 填充 变成了它两张图各自的 填充,读入时与落盘后都是,drift 警告 0 条;手写的一对**不同**模式能原样读回。
53
- - **节日的图按定义就是 填充** —— 满幅的节日画不该有黑边。模式和它旁边的槽位、颜色一样取**定义**、不取磁盘,所以手改过的值换不掉节日的模式;旁边的**取景**仍然是你自己的。`check:node` 钉住了「定义胜出」这条。
54
- - **渐变只作用于当前调色板真正写入的东西(修复)** —— 报告是「有时候切换背景时(图上为浅色切换为浅色),白色色块会渐变为黑色色块…白色渐变完黑色后立马切到白色」,白色值量到的是 `237, 243, 254`。这个数字就是答案:`#edf3fe` 是 `--dsw-static-deepseek-50`,也就是宿主**浅色**方案画 `--dsw-specific-bubble` 用的值 —— 你自己那条消息的气泡 —— 它的深色方案值是 `#2c2c2e`。渐变样式表之所以注册了这个名字,是因为 `fadeTokenNames()` 要的是**深色**分支的 token 表(那份是超集),而 `fadeScopes()` 把整张表都交给了 `:root` 与 `body` 两个作用域:浅色分支只写其中 **35** 个(共 **80** 个),另外 45 个 —— 气泡、`--dsw-specific-selector`(设置里的下拉框底色)、提示与气泡框、代码块右上角那排小按钮、状态色、工具条与幽灵按钮的填充 —— 仍然归**宿主**。而这些名字宿主是按自己的时间表改的,触发者还是插件自己:它每次换配色都销毁并重新注册主题,而宿主的销毁器会把偏好重置为默认值 `system`(ui-theme 自己的 `register()`:「重置为默认偏好,这样界面不会留着已注销主题的 token」),在深色桌面上这就等于把整个界面交给宿主的深色调色板、直到那次调用结束;而 presenter 自己那句强制的样式刷新(`getComputedStyle(body).backgroundColor`)正是已经武装好的过渡抓住深色值的那一刻。报告里那次会话的切换时长是 `durationMs: 3000`,一帧的错色因此被拉成了三秒的渐变;值随后变回浅色且自身没有动画,这就是「立马切到白色」那一半。两处修改各治一半:作用域的 token 只在**当前调色板**确实写它时才发出(新增的 `written` 入参,传的就是本次要写的那一块的 `Object.keys(tokens)` —— 注册表仍然覆盖全部 80 个名字,而全部写入这 80 个的深色调色板完全没变),以及配色方案没变就**不再重建**皮肤(皮肤真正贡献的只是它的 `colorScheme`,token 本来就是我们自己用 `!important` 重发的,同一方案下两边名字集合完全一样)。在报告那套调色板上实测(色相 192):`body` 的过渡列表从 80 个名字降到 35 个,气泡与下拉框底色都不在里面了,而 `--dsw-alias-bg-base`、`--dsw-alias-bg-layer-2` 和代码块底板仍在 —— 插件真正绘制的表面一个都没丢掉渐变。
55
- - **代码块跟着调色板走,而且它自己的底板有了独立透明度(修复)** —— 报告是「代码块背景有点问题」:**浅色**界面里,深绿色文字压在近黑色的底板上。成因是 `var()` 在**声明它的地方**就被替换了:宿主的 shiki 表在 `:root` 上声明 `--shiki-background: var(--dsw-alias-markdown-code-block)`(还有 `--shiki-foreground`),而宿主自己 —— 也因此包括本插件 —— 只在 `body` 上声明这些 token,于是根元素上除了过渡注册的 `initial-value` 什么也读不到:那是宿主自己的颜色,且是在插件样式表被静音时采样的。结果底板在每一套浅色调色板下都停在宿主的深色值(`#1b1b1c`),而会随方案翻转的墨色(`body[data-ds-dark-theme]{--shiki-token-*}`)变成了深色 —— `#2f9e44` 的绿压在 `#1b1b1c` 上,对比度约 2:1。现在调色板规则对 `:root` 与 `body` 一并发出,`:root` 也加入渐变作用域(只有**声明**该 token 的元素才会做过渡,少了这个作用域,代码块会在周围每块表面淡入时自己硬跳到新色);底板与它的 banner 重新是调色板的染色 —— 写死一个常量只可能匹配两套墨色中的一套 —— 且浅色分支也发出 banner,不再偷偷借宿主的近白色。新增 **`code`** 透明度档位驱动这两块表面,各自读自己的插件自有变量(写入者是按 token **名字**设变量的,共用一个变量会让后写的 banner 把底板也一起画了);它默认不透明 —— 代码块是拿来读的,不是拿来看穿的 —— 而且只做透明度:把语法底下的壁纸冻上一层霜,只会让底板更糊。
56
- - **四个小修(修复)** —— 轮换开关不再「活过」它自己的图片:开着轮换把图片删到只剩一张时,开关仍显示「开」,而面板在少于两张时把这个开关禁用了,于是也关不掉 —— 设置说一套、图片数量说另一套,在下一张图到来之前无从调和。现在删图后不足两张就清掉 `enabled`,`normalizeRule` 对不经过面板的路径(导入的配置、别处删图后写回的配置)强制同一条不变量;重新加图时开关是关的,因为轮换是一个决定,不是数量的后果。「切换时长」被印了两遍(一个小节标题,隔一行又是滑块的标签)。资料页页脚的包名与品牌磁贴现在是指向项目仓库的链接 —— 这两处正是读者会去找「这东西哪来的」的地方 —— 两者都读 `repo.ts` 里那一个地址,`check:ui` 拿它和 `package.json` 的 `repository` 对比,于是改过名或搬过家的项目会让门禁失败,而不是发出去一个看起来完全正常的坏链接。
57
- - **门禁** —— `pnpm test` 现在是**八道**。`check:code-plate`(底板 token 在 `:root` 与 `body` 上都有声明、两个分支都发 banner、每块表面读自己的变量)与 `check:image-mode`(抬升本身,外加接线 —— 接错了不会报错,只会每张图长得一样)加入套件;`check:rotation` 增加了开关与数量的不变量(两侧都验),`check:ui` 增加仓库链接一致性,`check:transition` 增加归属规则(注册了但当前调色板不写的名字保留注册、但不给它过渡;写入集合与注册集合是**求交**、绝不并集),`check:repaint` 钉住武装渐变的那两行。
50
+ - **布局模式属于图片,不属于规则** —— 适应 / 填充 / 拉伸 / 平铺 / 居中 此前是每条规则一个值、覆盖一轮轮换里的每一张图,所以一张横构图照片和一张竖屏截图没法各自取景。现在模式存在 `images[].bgMode` 上、从正在画的那张图读回,胶片条下面那排胶囊编辑的也是那里选中的那张图;规则还没有图片时这排胶囊是**禁用**而不是隐藏。README 与 README.zh 里凡是说「模式属于规则」的地方都改了。
51
+ - **配置格式升到 6,因为这次迁移两个方向都是无声的(修复)** —— schema 5 的宿主会按自己认识的键重建每一个图片条目,写进去的逐图模式会在第一次写入时消失;而 schema 6 读一份 schema 5 的配置时**必须**把规则级的值抬到它的每一张图上,否则所有现成壁纸都会掉到默认值。`bgMode` 进了 `LEGACY_RULE_KEYS`,所以它是被刻意消费掉的迁移字段,不该在宿主日志里报成漂移。
52
+ - **节日的图按定义就是 填充** —— 满幅的节日画不该有黑边。模式和它旁边的槽位、颜色一样取定义、不取磁盘,所以手改过的值换不掉节日的模式;旁边的取景仍然是你自己的。
53
+ - **渐变只作用于当前调色板真正写入的东西(修复)** —— 报告是浅色切浅色时白色色块渐变成黑色再跳回白色,白色值量到的是 `237, 243, 254`。这个数字就是答案:`#edf3fe` 是 `--dsw-static-deepseek-50`,宿主**浅色**方案画用户消息气泡用的值,而渐变样式表之所以注册了它,是因为 `fadeTokenNames()` 要的是**深色**分支的 token 表(80 个名字),浅色分支只写其中 35 个。另外 45 个仍然归宿主,而宿主按自己的时间表改它们 —— 触发者还是插件自己,它每次换配色都销毁并重新注册主题。现在作用域的 token 只在**当前调色板**确实写它时才发出,配色深浅没变也不再重建皮肤。
54
+ - **代码块跟着调色板走,而且它自己的底板有了独立透明度(修复)** —— 报告是**浅色**界面里深绿色文字压在近黑色的底板上。宿主的 shiki 表在 `:root` 上声明 `var(--dsw-alias-markdown-code-block)`,而插件(和宿主自己)只在 `body` 上声明这些 token,于是根元素上除了宿主自己的颜色什么也读不到。现在调色板规则对 `:root` 与 `body` 一并发出、`:root` 也加入渐变作用域,底板与 banner 重新是调色板的染色。新增 **`code`** 透明度档位驱动这两块表面,各自读自己的插件自有变量,默认不透明 —— 代码块是拿来读的。
55
+ - **四个小修(修复)** —— 开着轮换把图片删到只剩一张时,开关仍显示「开」而面板在少于两张时把它禁用了,于是也关不掉;现在删图后不足两张就清掉 `enabled`,`normalizeRule` 对不经过面板的路径强制同一条不变量。「切换时长」被印了两遍(一个小节标题,隔一行又是滑块的标签)。资料页页脚的包名与品牌磁贴现在是指向项目仓库的链接,都读 `repo.ts` 里那一个地址,`check:ui` 拿它和 `package.json` 的 `repository` 对比。
56
+ - **门禁** —— `pnpm test` 现在是**八道**。`check:code-plate` 与 `check:image-mode` 加入套件;`check:rotation` 增加开关与数量的不变量,`check:ui` 增加仓库链接一致性,`check:transition` 增加归属规则,`check:repaint` 钉住武装渐变的那两行。
58
57
 
59
58
  ## v0.7.4
60
59
 
61
- - **弹窗里的控件现在和壁纸走同一个时钟,而不是等它切完再跳(这一条是修复)** —— 换主题色那一下,设置弹窗里的控件(「节日特殊背景」的开关填充、「平移推入」「标准」这些胶囊按钮)会**整整一套切换时长**停在上一套颜色上,直到背景切完才瞬间换成新的:一次 3 秒的换图看起来就是"界面先不管它、末尾来一下硬切"。这是从一个真实会话里量出来的(探针每 100ms 读一次 `getComputedStyle`):`body` 上的 `--dsw-alias-brand-primary` 确实在插值,930ms 起跑、3000ms 走完,中间点 `rgb(199,131,30)` 与 `ease` 曲线在 53% 时间点的值吻合;而同一个探针读到的**胶囊按钮实际底色**在整个 3 秒里每一次采样都是 `rgb(53,152,177)` —— 与旧值逐位相同,而不是混合值 —— 直到 4063ms(body 的动画刚结束)之后才用约 150ms 跳到 `rgb(230,126,0)`。
62
- - **卡住的是中间层,不是"继承拿不到动画值"** —— 这是先被排除掉的猜想。WPT 有一条 [custom-property-animation-inherited-used-by-standard-property](https://github.com/web-platform-tests/wpt/blob/master/css/css-properties-values-api/animation/custom-property-animation-inherited-used-by-standard-property.html) 明确要求"祖先上动画的自定义属性必须体现在子元素使用它的标准属性上",隔离实验也证实了:只继承、自己没有过渡的消费者**逐帧跟随**声明它的那个元素。真正的成因是插件自己把**整张 token 表**挂到了三个元素上,而其中两个(设置弹窗与轨迹页根节点)**只重新声明**三个 layer token,其余全是继承来的。给一个只继承的 token 挂过渡,该元素的过渡目标就是**武装那一刻它看到的值** —— 祖先正在插值,那个值还是旧的 —— 于是它自己变成一条被阻尼的过渡,整棵子树跟着这份滞后的副本走。同一组隔离实验里的对比:声明方跑 1200ms 过渡时,只继承且**没有**自己过渡的消费者逐帧跟随;只继承但**挂了**同样过渡的中间层到 t=1500ms 才走到 113/255。
63
- - **修法:过渡表按作用域给,每个元素只拿自己声明的 token** —— `paletteFadeCss` 的输入从 `selectors: string[]` 变成 `scopes: { selector, tokens }[]`:`body` 拿全表(其余一切都从它继承),弹窗与轨迹页根节点只拿它们重新声明的那三个 layer token。这不是"少挂几条"的优化,而是这个形状存在的全部理由 —— 一个作用域拿到自己没有声明的名字,就是把上面那条滞后复制到整棵子树上。产出的 CSS 从 `body,.dlg,[data-conversation-composer-overlay]{transition:…全表…}` 变成 `body{…全表…}` 加两条只列 layer token 的规则。
64
- - **门禁** —— `check:transition` 新增两条:**「a scope is never given a token it only inherits」**(弹窗那份列表里绝不允许出现 `--dsw-alias-label-primary` 这种它只继承的名字),以及「自己什么都没声明的 scope 不发规则」。原来那条「每个注册 token 都在过渡表里」照旧保留(`body` 仍然是全表),所以这次修复不可能悄悄退化成"所有作用域都拿全表"。
65
- - **顺带记录一个不构成原因的现象**:探针还显示那张 fade 表在武装后的一帧左右,就被后续一次非调色板 apply 重写成了不带 `transition` 的版本(`applyPaletteFade` 的 `animate=false` 分支),而**已经起跑的过渡不会因为声明被撤掉而中断**(同一次测量里,body 的过渡在表里已经没有 `transition` 的情况下照旧跑满 3000ms)。所以它不是这次卡顿的成因;但它意味着"过渡还没开始"的元素什么都拿不到 —— 留给下一次动这块代码的人。
66
- - **还有三块在硬切,而且正是那些用插件自己的颜色画的面(这一条是修复)** —— 后续反馈("还有几个元素的背景色是直接换的",界面透明度拉到最大)不是上一条那个 bug,随它一起来的截图直接指出了是哪几块:把中间那帧的每个像素分别与首帧、末帧比对(已到终色 / 仍在首色)之后,**设置弹窗自己那块底板** —— 表现为左侧栏、标题行和卡片四周的留白 —— 已经在终色上,而它里面的每一个 `.dab-card` 还在首色上。卡片在渐变、它背后的底板不渐变,说明这走的是另一条上色路径:底板不是用注册过的 alias token 画的,而是用 `--dsh-any-bg-settings-surface` —— 一个插件自有、内联写在 `<html>` 上、已经折进设置透明度的变量;而**未注册**的自定义属性根本不能插值,既没有 token 可注册,也没有 token 可过渡。同一形状还有两处:文件预览面板(`--dsh-any-bg-rightbar`,由 `RIGHTBAR_STYLE_RULE` 那三个选择器读取)和 Cordis 面板(`--dsh-any-op-menu-cordis`,在它上面重新声明 `--dsw-specific-menu`)。
67
- - **修法:作用域可以点一个标准属性,且上色规则与淡入共用同一个选择器** —— `PaletteFadeScope` 新增 `properties?: string[]`,与 token 一起排进同一条 `transition`,并且**故意不**按注册名过滤(属性名不是宿主需要发布的东西)。`fadeScopes()` 里设置弹窗底板与文件预览面板各带一条 `background-color`,Cordis 面板带 `--dsw-specific-menu` —— 它是同一类问题的第三种,但因为它重新声明的那个 token 本来就是我们自己的,所以点 token 就能到它。两条上色规则与各自的淡入作用域现在各读同一个共享常量(`RIGHTBAR_SEL`、`CORDIS_PANEL_SEL`),上色的那一半和淡入的那一半不可能再走岔。在 Chromium 里拿**真实产出的 CSS** 量过、按截图里那几个颜色跑一次 3000ms `ease`:旧 CSS 下底板、右侧面板、Cordis 面板在第一次采样时就已经是**终色**并且再没动过,而卡片从 `rgba(217,233,237)` 爬到 `rgba(255,255,255)`、胶囊从 `rgb(87,167,188)` 爬到 `rgb(236,151,67)`;新 CSS 下五者**每一次采样都处在同一进度** —— t=1500ms 时 80.2%,恰好是 `ease(0.5)`:底板 `rgba(239,234,221)`、右侧面板 `rgba(181,178,168)`,各自在自身的起止区间上与卡片逐位吻合。`0` ms 时长同样会把这几个属性一起解除武装,所以拖动滑杆时底板依旧贴着指针走。
68
- - **门禁** —— `check:transition` 补上了淡入形状的另一半:只带标准属性的作用域照样发规则、该属性与 token 用同一套时长与缓动、属性绝不会被当成颜色去注册、一个未注册的 token 名不再连累同一作用域里的属性、`0` ms 同样解除它。`check:repaint` 补上了配对本身:三块插件自绘的表面必须既由自有变量上色、又有一个点着同一个共享常量的淡入作用域 —— 以后再添第四块而忘了淡入,会是一条失败的门禁,而不是一张没人拍的截图。
60
+ - **弹窗里的控件现在和壁纸走同一个时钟,而不是等它切完再跳(这一条是修复)** —— 换主题色那一下,设置弹窗里的控件会**整整一套切换时长**停在上一套颜色上,直到背景切完才瞬间换成新的。这是从一个真实会话里量出来的:`body` 自己的 token 在那 3 秒里插值正常,而胶囊按钮的实际底色每一次采样都与旧值逐位相同,直到动画结束之后才用一段短过渡到达新色。
61
+ - **卡住的是中间层,不是「继承拿不到动画值」** —— 只继承、自己没有过渡的消费者**逐帧跟随**声明它的那个元素,隔离实验证实了这一点。真正的成因是插件把**整张 token 表**挂到了三个元素上,其中两个只**重新声明**三个 layer token:给一个只继承的 token 挂过渡,该元素的过渡目标就是**武装那一刻它看到的值**,于是它变成一条被阻尼的过渡,整棵子树跟着这份滞后的副本走。
62
+ - **修法:过渡表按作用域给,每个元素只拿自己声明的 token** —— `paletteFadeCss` 的输入从 `selectors: string[]` 变成 `scopes: { selector, tokens }[]`:`body` 拿全表,弹窗与轨迹页根节点只拿它们重新声明的那三个 layer token。这不是「少挂几条」的优化 —— 一个作用域拿到自己没有声明的名字,就是把上面那条滞后复制到整棵子树上。
63
+ - **还有三块在硬切,而且正是那些用插件自己的颜色画的面(这一条是修复)** —— 把中间那帧的每个像素分别与首帧、末帧比对之后,设置弹窗自己那块底板已经在终色上,而它里面的每一张卡片还在首色上。卡片在渐变、底板不渐变,说明走的是另一条上色路径:底板不是用注册过的 alias token 画的,而是用 `--dsh-any-bg-settings-surface`,而**未注册**的自定义属性根本不能插值。同一形状还有两处:文件预览面板与 Cordis 面板。
64
+ - **修法:作用域可以点一个标准属性,且上色规则与淡入共用同一个选择器** —— `PaletteFadeScope` 新增 `properties?: string[]`,与 token 一起排进同一条 `transition`,并且**故意不**按注册名过滤。`fadeScopes()` 里设置弹窗底板与文件预览面板各带一条 `background-color`,Cordis 面板带 `--dsw-specific-menu`;两条上色规则与各自的淡入作用域各读同一个共享常量,上色的那一半和淡入的那一半不可能再走岔。在 Chromium 里拿**真实产出的 CSS** 量过:五块表面每一次采样都处在同一进度 —— t=1500ms 时 80.2%,恰好是 `ease(0.5)`。
65
+ - **顺带记录一个不构成原因的现象** —— 探针还显示那张 fade 表在武装后的一帧左右,被后续一次非调色板 apply 重写成不带 `transition` 的版本,而**已经起跑的过渡不会因为声明被撤掉而中断**。它不是这次卡顿的成因,但它意味着「过渡还没开始」的元素什么都拿不到。
66
+ - **门禁** —— `check:transition` 补上了淡入形状的两半:一个作用域绝不会拿到它只继承的 token、自己什么都没声明的 scope 不发规则、只带标准属性的作用域照样发规则并与 token 用同一套时长与缓动、`0` ms 同样解除它。`check:repaint` 补上了配对本身:三块插件自绘的表面必须既由自有变量上色、又有一个点着同一个共享常量的淡入作用域 —— 以后再添第四块而忘了淡入,会是一条失败的门禁,而不是一张没人拍的截图。
69
67
 
70
68
  ## v0.7.3
71
69
 
72
- - **切换效果本身成了一个设置:四种效果、一种缓动、一个时长,都在「配置」页上** —— 这个插件做的每一次换图(切模型、轮换一步、手动「下一张」、节日接管、换成另一条规则生效)此前都走同一个写死的交叉淡入,而它的时长读的是一个没有任何控件能碰到的字段。效果和缓动是全局的,因为规则没有「过渡」这个概念:**淡入淡出**(opacity)、**直接切换**、**缩放淡入**、**平移推入**(transform,只作用在进来的那一层上 —— 壁纸层从来没用过这个属性,所以两种新效果一个字节的额外传输都不花)。「直接切换」和 `0` 毫秒是同一个硬切,而不是其中一个被读成「没设置」;`0` 是一个被保留的真实取值,不会被默认值顶掉;系统的「减少动态效果」是**否决**,而不是把动画缩短。判定是纯函数(`src/client/transition.ts`),正因如此 `check:transition` 才能钉住那些不会抛错的失败:起始状态与结束状态相同的效果(看上去什么都没做)、会把进来的那一层留在缩放状态上的效果(此后每一张壁纸都会被裁掉一块),以及 `fade` 不再逐字节等于早先版本写下的那个字符串。
73
- - **卡片上带一个预览色块,而它不是装饰** —— 壁纸淡入它自己是什么都看不见的,所以「在已经在屏的那张图上重放效果」会什么都不显示:这是这个功能唯一可能「明明在工作却看起来坏了」的方式。色块按此刻一次真实切换会用的时长重放选中的效果,点效果或点缓动都会立刻重放一次。
74
- - **时长只有一个,「跟随每条规则」删掉了** —— 那张卡的默认值是「跟随每条规则」,而它指向一个界面根本设不了的值:`rotate.fadeMs` 从 0.7.0 起就在格式里,却从来没有控件能到达它,所以那个选项是在一个真设置和一个幻影设置之间做选择。现在滑块本身就是那个设置,它让位的字段也一起删了。对唯一可能带上它的那种配置来说,这并不是一次无声的丢失:那个字段只可能被手工编辑,所以当一份早于这个设置的配置第一次被读取时,`legacyFadeMs` 会把找到的第一个「有意为之」的值折进全局时长 —— 等于出厂值 320 ms 的一律忽略(普通配置就保持普通默认值),而显式的 `durationMs` 永远胜过它旁边的旧字段。`rules[].rotate.fadeMs` 同样豁免漂移警告,理由相同:它是在被迁移,不是在漂移。
75
- - **界面配色现在和壁纸走同一个时钟** —— 换一个主题色本质是一次 `body{--dsw-alias-…}` 的重写,它在一帧之内改掉每一个表面,而背后的图还在淡入淡出:这就是动画前面接了一下硬切。这个渐变是**注册**问题而不是声明问题 —— 未注册的自定义属性无法插值 —— 所以 `src/client/palette-fade.ts` 会发出把别名令牌变成 `<color>` 的 `@property` 规则,外加一条 `transition`,作用于「自身的声明会随调色板变化」的那些元素:`body`,以及两个**重新声明**层令牌而不是继承它们的表面(设置弹窗与轨迹页根节点)。插件**内联**绘制的两处(三栏列,以及对话/轨迹卡片)根本不读这些令牌,所以它们把同一个时长直接挂在 `background-color` 上。时长、缓动与两项否决都来自壁纸用的**同一个** `transitionPlan`,所以「直接切换」、`0` 毫秒和「减少动态效果」不可能对图是一个意思、对界面又是另一个意思。它**只在调色板确实变了**的时候武装:其它每一次 apply 都在重写同一段令牌(拖滑块改的是 alpha),那些写入不带过渡、内联写 `none`,因为那里挂一条活着的过渡会让表面跟不上指针。宿主没有公布的令牌是**刻意不注册**的 —— 注册需要 `initial-value`,而一个没有别的东西去设置它的已注册属性,会解析到那个初始值、而不是插件自己样式表依赖的 `var(…, fallback)` 链,注册它就等于用一个人为的颜色替换掉回退值。
76
- - **浅↔深互切是立即切换,而壁纸照旧做动画(这一条是修复)** —— 在两套调色板之间插值会让整个界面走过中间调,而那里的墨色**两个方向都是错的**(深色文字走进一半的深色调色板,而且用的是**上一套**方案的墨色),所以浅↔深互切恰恰是渐变会让事情变糟的那一种情况。现在渐变只在**配色深浅不变**时武装(浅→浅、深→深);浅↔深立即切换,也就是渐变出现之前的行为。这条规则刻意不碰壁纸:一张图淡入另一张图没有「深浅」可以搞错,所以它照旧听切换效果的。`paletteFadeAllowed` 就是这条规则的全部,纯函数、由 `check:transition` 钉住;`check:repaint` 则要求调用点把问题的**两半**都问全(调色板变了**且**深浅没变),因为调用点正是后来某次编辑会悄悄丢掉其中一半的地方 —— 而插件内联绘制的那些表面拿到的是同一个答案,所以不可能出现「令牌不渐变、内联面板却在渐变」的半吊子状态。
77
- - **墨色现在是同步写入的,而那是一个真实存在、且只有一个写入者的 bug(这一条是修复)** —— 把一条规则换成深色主题色时,面板的文字可能留在**上一套**(浅色)调色板上,而所有表面都已经跟到新的那套:深色面板上的深色墨色,读不出来,只有重新加载才能清掉,日志里什么都不会说。这是从报告里的截图**量出来**的,不是猜的:面板是插件自己的深色 `bg-layer-2` —— `hsl(229,59%,27%)` = `rgb(28,43,109)`,在配置的 87% 面板透明度下合成为采样到的 `rgb(23,36,91)` —— 而标签墨色是画在它上面的**浅色**分支令牌:`rgba(0,0,0,0.85)` 压在那块藏青上恰好就是 `rgb(3,6,14)`,也就是从字形核心采样到的值;`rgba(0,0,0,0.7)` 恰好是提示行采样到的 `rgb(7,10,27)`。两个精确吻合,一块面板:表面和墨色来自两套不同的调色板。这种不对称是结构性的 —— 其它每个表面都有第二个同步的写入者(弹窗的层令牌来自 `applySettingsOverrides`,三栏的背景来自 view cards),所以只有令牌样式表可能变旧,而这唯一的写入者只能从一个包在 `catch {}` 里的 `requestAnimationFrame` 回调到达:不在渲染的窗口会吞掉那一帧,而 id 仍然武装着,把此后每一次更新都堵在它后面;写入内部出错则什么都不说。重新加载是唯一的解药,而报告里说的正是这件事。现在调色板一旦移动,就在**与它必须保持一致的表面同一个任务里**同步写入;帧路径留给它本来要做的事(合并一次滑块拖动,那时调色板本来就是对的),积压超过一秒的帧会被丢掉而不是永久阻塞,写入者也会把失败记进日志而不是吞掉。
78
- - **配置格式升到 5、导出格式升到 6,旧宿主依旧吃不掉配置** —— schema 4 的 sanitizer 会**按自己认识的键重建**配置,所以一个仍在运行的旧宿主进程会在刷新后的第一次写入时丢掉效果、缓动和时长,悄无声息,宿主日志里也什么都没有。因此公布的 `schema` 前进,客户端那句 `>=` 比较会把这样的宿主放进它本来就有的「扣住写入」分支,`warnUnknownConfigKeys` 也出于同样的理由往深一层比较固定形状的 `transition` 块。主题文件升到格式 6;5 及更早仍可导入。这个格式从未发布过,所以 `5` 仍然是它写下时的含义 —— `SCHEMA_VERSION` 防的是「新客户端配旧宿主进程」,而从来没有任何宿主用旧形状报出过 5。
79
- - **门禁** —— `check:transition`(`scripts/transition-check.ts`)加入 `pnpm test`,现在是**六道**:它逐字节钉住交叉淡入的字符串、「直接切换」与 `0` 时长的硬切语义、「减少动态效果」的否决、每种效果的落点状态、配色渐变的 CSS 形状(包括 `0` 毫秒只发注册、不发过渡规则),以及浅↔深那张真值表。`check:repaint` 新增了这个 bug 由之构成的三行:调色板移动时写入必须是同步的、不许吞掉自己的失败、并且必须在令牌与内联两个调用点都把渐变问题的两半问全。`check:node` 覆盖出厂默认值、往返、恶意取值、被保留的 `0` 时长、`transition` 漂移检查、公布 schema 为 5,以及那次旧值折算(手工调过的 1200、有意的 `0`、平平无奇的 320、显式的全局值胜出)。`check:ui` 覆盖新文案和两个新类(`.dab-tr-preview`、`.dab-tr-preview-in`),并把效果/缓动胶囊的标签登记进 `TABLE_KEYS` —— 它们是通过 `Record<TransitionEffect, …>` 这类表到达的,而这正是「编译器已经保证存在的标签」的既定登记机制。
70
+ - **切换效果本身成了一个设置:四种效果、一种缓动、一个时长,都在「配置」页上** —— 这个插件做的每一次换图此前都走同一个写死的交叉淡入,而它的时长读的是一个没有任何控件能碰到的字段。效果是**淡入淡出**、**直接切换**、**缩放淡入**、**平移推入**(后两种只对进来的那一层做 transform,所以一个字节的额外传输都不花);「直接切换」和 `0` 毫秒是同一个硬切,而不是其中一个被读成「没设置」;`0` 是一个被保留的真实取值;系统的「减少动态效果」是**否决**而不是把动画缩短。判定是纯函数(`src/client/transition.ts`),正因如此 `check:transition` 才能钉住那些不会抛错的失败。
71
+ - **卡片上带一个预览色块,而它不是装饰** —— 壁纸淡入它自己是什么都看不见的,所以「在已经在屏的那张图上重放效果」会什么都不显示:这是这个功能唯一可能「明明在工作却看起来坏了」的方式。色块按此刻一次真实切换会用的时长重放选中的效果。
72
+ - **时长只有一个,「跟随每条规则」删掉了** —— 那张卡的默认值是「跟随每条规则」,而它指向 `rotate.fadeMs`:一个从 0.7.0 起就在格式里、却从来没有控件能到达的字段。现在滑块本身就是那个设置,而 `legacyFadeMs` 会在读取一份早于这个设置的配置时,把找到的第一个「有意为之」的值折进全局时长;显式的 `durationMs` 永远胜过它旁边的旧字段。
73
+ - **界面配色现在和壁纸走同一个时钟** —— 这个渐变是**注册**问题而不是声明问题,因为未注册的自定义属性无法插值:`src/client/palette-fade.ts` 会发出把别名令牌变成 `<color>` 的 `@property` 规则,外加一条 `transition`,作用于「自身的声明会随调色板变化」的那些元素。它**只在调色板确实变了**的时候武装,因为拖滑块时挂着一条活着的过渡会让表面跟不上指针;宿主没有公布的令牌是**刻意不注册**的。
74
+ - **浅↔深互切是立即切换,而壁纸照旧做动画(这一条是修复)** —— 在两套调色板之间插值会让整个界面走过中间调,而那里的墨色**两个方向都是错的**,所以渐变只在**配色深浅不变**时武装。这条规则刻意不碰壁纸:一张图淡入另一张图没有「深浅」可以搞错,所以它照旧听切换效果的。
75
+ - **墨色现在是同步写入的,而那是一个真实存在、且只有一个写入者的 bug(这一条是修复)** —— 把一条规则换成深色主题色时,面板的文字可能留在**上一套**调色板上,而所有表面都已经跟到新的那套:深色面板上的深色墨色,读不出来,只有重新加载才能清掉,日志里什么都不会说。其它每个表面都有第二个同步的写入者,所以只有令牌样式表可能变旧,而这唯一的写入者只能从一个包在 `catch {}` 里的 `requestAnimationFrame` 回调到达 —— 不在渲染的窗口会吞掉那一帧,把此后每一次更新都堵在它后面。现在调色板一旦移动就在**与它必须保持一致的表面同一个任务里**同步写入,写入者也会把失败记进日志而不是吞掉。
76
+ - **配置格式升到 5、导出格式升到 6,旧宿主依旧吃不掉配置** —— schema 4 的 sanitizer 会**按自己认识的键重建**配置,所以一个仍在运行的旧宿主进程会在刷新后的第一次写入时丢掉效果、缓动和时长,悄无声息。主题文件升到格式 6;5 及更早仍可导入。
77
+ - **门禁** —— `check:transition` 加入 `pnpm test`,现在是**六道**。`check:repaint` 新增了这个 bug 由之构成的三行,`check:node` 覆盖公布 schema 与旧值折算,`check:ui` 覆盖新文案与两个新类。
80
78
 
81
79
  ## v0.7.2
82
80
 
83
- - **胶片条自己就是控件:拖拽排顺序,双击换画面** —— 顺序以前只能靠下面那排 ↑ / ↓ 和一个「设为第一张」按钮改,而五个缩略图本来就把顺序摆在那里,那对箭头是同一件事的第二种、更差的说法。现在抓住任意一张拖到新位置:落点画成一根 2px 竖条,画在目标格子**内部**,因为一块占位的竖条会把指针正在测量的那些格子推走。而「哪张在屏上」是胶片条的**另一个**手势 —— 双击它。它不改顺序,也**不写任何配置**:在屏哪张是运行时状态而不是设置,所以它不会被保存,下次启动照旧从规则的第一张开始。在非生效规则的格子上双击会说明原因(复用现成的那句「只有正在生效的规则能换画面上的图」),而不是留下一个看起来坏了的双击;两个手势都写在格子的悬停提示里。
84
- - **顺序和在屏的是哪张,从此互不干涉(这一条是修复)** —— 旧的两个动作(`moveRuleImage`、`setCurrentImage`)都以「把图片下标挪到刚被移动的那张图上」结尾,于是整理列表会把壁纸一起换掉:轮换关着的时候下标是粘住的,而这正是最容易撞见它的状态(「我调整顺序时,即便关了轮换,当前背景也可能会变,拖拽或点按钮调整顺序都有可能发生」)。新的 `moveRuleImageTo(id, slot, to)` 取代了它们两个:摘出之前先记下**正在画的那个槽位**,插回去之后把下标放回它身上。重排不是换主题 —— 三个按钮连同它们背后两个不再被调用的动作一起删掉了,`check:repaint` 的清单同步。
85
- - **在屏那张不再有边框,只留圆点** —— `.is-sel` 已经给「正在编辑的那张」画了圈,再给「正在显示的那张」画一个,就成了一排几乎一样的缩略图里同时有两个「选中」,而这是胶片条唯一不能留给人猜的问题。现在在屏 = 右上角那个圆点,仅此而已。
86
- - **一次上传的多张图,每一张都会各自取色** —— 主题色属于**图片**,所以一次带多张的上传有好几个调色板要填,而取色是**按下标**寻址的:`addRuleImages` 先把整批追加完,然后只对 `rule.images[rule.images.length - 1]` 取色。这对它长大时面对的单张上传是对的,对批量则静默地错 —— 最后一张之前的那些图没有自己的颜色、留在系统主题上,而界面上什么都不会说。现在按**这一批自己刚追加的那些槽位**寻址,所以「哪些图会被取色」不再取决于它们落在规则的哪个位置,也不取决于卡片当时选中了哪一张。
87
- - **卡片文案精简,「编辑位置」换了位置** —— 「规则列表」标题和它下面那句优先级说明、以及自动取色旁边那句解释都删了(面板标题已经介绍了规则,第一张卡上的 `兜底` 徽章说明了优先级;开关和它自己的标签留着)。标签去掉多余的名词:`当前第 1 / 5` → `第 1 / 5`,`添加图片` / `替换这张` / `移除图片` → `添加` / `替换` / `移除`,英文同步。`编辑位置` 从「图片动作」那一行移到**布局模式那一行的末尾**:它打开的东西编辑的是图片如何填满这块区域,正是那一行五个模式在说的事;它留在末尾而不是插在两个模式之间,因为那五个是互斥的,它不是第六个。它仍然只在**适应**下出现 —— 拖拽构图就是适应在做的事。
88
- - **门禁** —— `check:repaint` 新增一节钉住批量取色:批量形式必须遍历它拿到的每一个槽位、上传必须记录自己这批的槽位并给它们全部取色、按下标寻址的 `maybeAutoExtract(id, rule.images[…])` 不许回来;`moveRuleImageTo` 进入「每个写入路径都必须先采样 winner、再以 `repaintIfMoved` 收尾」的清单,两个被删掉的动作则从中移出。`check:ui` 覆盖新文案与新类(`.dab-strip-bar`,以及 `is-dragging` 状态),并且要求不再有调用者的那三个字典键在同一个提交里离开两份字典。
81
+ - **胶片条自己就是控件:拖拽排顺序,双击换画面** —— 顺序以前只能靠下面那排 ↑ / ↓ 和一个「设为第一张」按钮改,而五个缩略图本来就把顺序摆在那里。落点画成一根 2px 竖条,画在目标格子**内部**,因为一块占位的竖条会把指针正在测量的那些格子推走。而「哪张在屏上」是胶片条的**另一个**手势,且**不写任何配置**:在屏哪张是运行时状态而不是设置,所以下次启动照旧从规则的第一张开始。
82
+ - **顺序和在屏的是哪张,从此互不干涉(这一条是修复)** —— 旧的两个动作都以「把图片下标挪到刚被移动的那张图上」结尾,于是整理列表会把壁纸一起换掉。新的 `moveRuleImageTo(id, slot, to)` 取代了它们两个:摘出之前先记下**正在画的那个槽位**,插回去之后把下标放回它身上。
83
+ - **在屏那张不再有边框,只留圆点** —— `.is-sel` 已经给「正在编辑的那张」画了圈,再给「正在显示的那张」画一个,就成了一排几乎一样的缩略图里同时有两个「选中」。现在在屏 = 右上角那个圆点,仅此而已。
84
+ - **一次上传的多张图,每一张都会各自取色** —— 主题色属于**图片**,而取色是**按下标**寻址的:`addRuleImages` 先把整批追加完,然后只对最后一张取色,于是它之前的那些图没有自己的颜色、留在系统主题上。现在按**这一批自己刚追加的那些槽位**寻址。
85
+ - **卡片文案精简,「编辑位置」换了位置** —— 「规则列表」标题和两句解释都删了(面板标题与第一张卡上的 `兜底` 徽章已经说明了),标签去掉多余的名词(`第 1 / 5`、`添加` / `替换` / `移除`),`编辑位置` 移到布局模式那一行的末尾 —— 它打开的东西编辑的正是那一行五个模式在说的事。
86
+ - **门禁** —— `check:repaint` 新增一节钉住批量取色,`moveRuleImageTo` 进入「先采样 winner、再以 `repaintIfMoved` 收尾」的清单,两个被删掉的动作则从中移出。`check:ui` 覆盖新文案与新类。
89
87
 
90
88
  ## v0.7.1
91
89
 
92
- - **主题色下放到图片** —— 一条规则是一组图片的轮换,而这些图往往是为不同心情收的,不该共用一个强调色。每张图现在带自己的 `color: [h, s, l] | null`,整个调色区——色轮、数值输入、灵感色板、「从本图提取」、吸管、「跟随系统主题」——作用于**胶片条里选中的那张图**。控件上方多了一行写明目标(「作用于选中的图片:2 / 3」),胶片条标出哪些图已经各自有颜色(悬停显示色值),因为一个会悄悄换目标的区块,正是「我改了颜色却没反应」的开端。没有新控件、也没有布局改动:胶片条本来就是选图的入口,也本来就是逐图取景的入口。
93
- - **规则自己的 `color` 只剩一个含义:一张图都没有时它画什么** —— 它刻意**不是**该规则图片的默认值。没有自己颜色的图**跟随系统主题**,而不是去继承规则色;正是这一点才让「绿的、红的、跟随系统主题」这样一组轮换可以表达——若改成继承,清空某张图的颜色只会把它交还给规则色,「跟随系统主题」按钮看起来就成了失效的按钮(而且一条既有颜色又有图片的规则将无处抵达系统主题)。所以清空某张图的颜色只影响那一张;规则自己的颜色留着,等「只剩它能画」的那个状态。面板的两个分支遵循同一条规则:有图可指就编辑图,`images: []` 就编辑规则。
94
- - **升级不丢主题色,而区分只靠一个键** —— 这个版本之前写下的图片条目**根本没有** `color` 键,这就是 `normalizeImage` 的全部判据:缺键 = 「这张图从来没有自己的颜色」,于是规则色被抬升到它身上(只做一次,且在共享 sanitizer 里做,两半边行为一致,文件也会在第一次写入时自愈);显式 `null` = 「主动清空过」,任何时候都不会被重新继承覆盖。这也是为什么每次写入都会带上这个键、哪怕是 null——把「没有颜色」写成缺键,等于每次加载都把规则色继承回来。因此这份字段出现之前的配置升级后观感完全一致,0.7 之前那对单图字段(`slot` / `bgState`)也走同一次抬升(只是那一条是构造出来的,不是读出来的)。
95
- - **轮换每一步都重发界面调色板,否则这个功能一出生就带着那个 bug** —— 换图原本只重画壁纸,于是「逐图主题色」只存在于配置和面板里,界面配色要等**别的**事情顺手重跑一次 apply 才跟上:拖动调色盘,或者切换模型。这正是这个插件已经踩过一次的现象,一字不差(「更新的不太及时,只有我动调色盘或者切模型的时候才出现」)。所以调色板的施加被收成一个函数(`applyPalette`),模型切换和轮换的每一步都走它——定时器 tick 与手动「下一张」共用那个负责画一步的 `paintImage` 出口。只要 `paintImage` 或 `applyActive` 不再经过它、或者它不再读取「当前生效的颜色」(`activeColor`:正在显示的那张图的颜色,否则规则色),`pnpm check:repaint` 就会报错。
96
- - **节日配色同样逐图** —— 节日的固定配色同时写在节日条目**和它的图片**上,`normalizeHolidayRule` 每次读取都用定义强制覆盖两处(任一处手改都会被替换回来)。所以「节日多图轮换、每张带自己的配色」已经只差 `HOLIDAYS`、它的素材,以及 node 半边的「槽位 → 素材」映射,而不需要在渲染层再开一条代码路径。
97
- - **配置格式升到 4,旧宿主依旧吃不掉配置** —— `images[].color` 对 schema 3 的 `normalizeImage` 是未知字段,而它会**按自己认识的键重建**每个图片条目:颜色会一直在内存里活到下一次重新加载,然后悄无声息地消失,宿主日志里也什么都不会说。`SCHEMA_VERSION = 4` 在每次 `read` 时公布,客户端用 `>=` 比较,报出更低版本的宿主会进入扣住写入的分支:日志提示一次,面板里说明(「重启 DSH」)。主题文件升到格式 5;4 及更早仍可导入,它们从来没有的颜色由上面那次抬升补上。
98
- - **没有图片的规则只剩一个上传入口** —— 上方那块大预览**本身就是按钮**了(真正的 `<button>`,可聚焦、可键盘触发;点它上传,也可以把文件拖上去),而胶片条在一张图都没有时**完全不渲染**:它里面除了一块「添加图片」再没有别的东西,而胶片条是一组图片的列表,一张图都没有的状态下它没有存在的理由。拖放处理跟着一起上移而不是消失——把胶片条藏起来,不该把每条规则诞生时都要用的拖放也一起拿走。其它都没动:带文字的 **添加图片** / **从网址** 按钮还在原位,空状态那几句话一字未改;有图片时预览依旧刻意保持惰性(拖到胶片条是新增,「替换这张」仍是显式的带文字按钮——正是这一点让预览不会吞掉用户无意的点击)。
99
- - **门禁** —— `check:node` 通过真实 RPC 处理器把逐图主题色这套形状端到端跑了一遍:抬升的两个方向(缺键继承规则色、显式 `null` 不继承)、颜色经过文件往返后各自独立且保持顺序、节日两处颜色都被强制、公布的 schema 升到 4,以及 `null` 落盘时是**键**而不是被省略。`check:repaint` 多了一节「轮换步骤必须重发调色板」,而新写入路径 `setImageColor` 也进入它要求「先采样 winner、再以 `repaintIfMoved` 收尾」的清单。`check:ui` 覆盖那三条新文案与两个新类(`.dab-strip-dot`、`.dab-rule-thumb-add`),跟其它每一条一样。
90
+ - **主题色下放到图片** —— 一条规则是一组图片的轮换,而这些图往往是为不同心情收的、不该共用一个强调色,所以每张图现在带自己的 `color: [h, s, l] | null`,整个调色区作用于**胶片条里选中的那张图**。控件上方多了一行写明目标(「作用于选中的图片:2 / 3」),胶片条标出哪些图已经各自有颜色,因为一个会悄悄换目标的区块,正是「我改了颜色却没反应」的开端。没有新控件、也没有布局改动。
91
+ - **规则自己的 `color` 只剩一个含义:一张图都没有时它画什么** —— 它刻意**不是**该规则图片的默认值。没有自己颜色的图**跟随系统主题**,而不是去继承规则色,正是这一点才让「绿的、红的、跟随系统主题」这样一组轮换可以表达。所以清空某张图的颜色只影响那一张;规则自己的颜色留着,等「只剩它能画」的那个状态。
92
+ - **升级不丢主题色,而区分只靠一个键** —— 这个版本之前写下的图片条目**根本没有** `color` 键,这就是全部判据:缺键 = 「这张图从来没有自己的颜色」,于是规则色被抬升到它身上(只做一次,且在共享 sanitizer 里做)。显式 `null` = 「主动清空过」,任何时候都不会被重新继承覆盖 —— 这也是为什么每次写入都会带上这个键、哪怕是 null。
93
+ - **轮换每一步都重发界面调色板,否则这个功能一出生就带着那个 bug** —— 换图原本只重画壁纸,于是「逐图主题色」只存在于配置和面板里,界面配色要等**别的**事情顺手重跑一次 apply 才跟上。现在调色板的施加收成一个函数(`applyPalette`),模型切换和轮换的每一步都走它。只要 `paintImage` 或 `applyActive` 不再经过它,`pnpm check:repaint` 就会报错。
94
+ - **节日配色同样逐图** —— 节日的固定配色同时写在节日条目**和它的图片**上,`normalizeHolidayRule` 每次读取都用定义强制覆盖两处。所以「节日多图轮换、每张带自己的配色」已经只差 `HOLIDAYS` 与它的素材,而不需要在渲染层再开一条代码路径。
95
+ - **配置格式升到 4,旧宿主依旧吃不掉配置** —— `images[].color` 对 schema 3 的 `normalizeImage` 是未知字段,而它会**按自己认识的键重建**每个图片条目:颜色会一直在内存里活到下一次重新加载,然后悄无声息地消失。`SCHEMA_VERSION = 4` 在每次 `read` 时公布,报出更低版本的宿主会进入扣住写入的分支。主题文件升到格式 5。
96
+ - **没有图片的规则只剩一个上传入口** —— 上方那块大预览**本身就是按钮**了(真正的 `<button>`,可聚焦、可键盘触发;点它上传,也可以把文件拖上去),而胶片条在一张图都没有时**完全不渲染**,因为胶片条是一组图片的列表。拖放处理跟着一起上移而不是消失;有图片时预览依旧刻意保持惰性。
97
+ - **门禁** —— `check:node` 通过真实 RPC 处理器把逐图主题色这套形状端到端跑了一遍,`check:repaint` 多了一节「轮换步骤必须重发调色板」,`check:ui` 覆盖新文案与两个新类。
100
98
 
101
99
  ## v0.7.0
102
100
 
103
101
  - **每条规则多图轮换 —— 默认关闭** —— 规则现在持有一组有序图片(`images: [{ slot, bgState }]`),可以按自己的节奏循环:10 秒 / 30 秒 / 1 分钟 / 5 分钟 / 30 分钟 / 1 小时,或 5 秒 – 24 小时之间的任意秒数;顺序可选**按顺序**或**随机**。默认是关的:轮换真的要花流量、内存和电,一个偷偷让壁纸动起来的默认值,等于替用户做了决定。
104
- - **第二种触发完全不需要定时器** —— 「切换模型时也换一张」:每次切换模型落到这条规则上就前进一张。对一条「每次都想看到不一样」的规则来说,这就是全部功能:不排期、不唤醒,只在用户做事的时候换图。
105
- - **随机绝不会抽到当前正在显示的那张** —— 挑选在**除当前这张之外**的所有下标上等概率进行,因为一次「图没变」的 tick 与一次「压根没触发」的 tick 完全无法区分 —— 而这正是这个功能唯一可能悄无声息失败的方式。`pnpm check:rotation` 对 2、3、7 张图的列表把整个随机取值范围**穷举**了一遍(不是抽一次样本),同时钉住顺序巡回的环绕、越界与小数下标,以及「只有一张图或间隔非有限值时报出的绝不是『正在轮换』」。
106
- - **全插件只有一个定时器,始终对准生效规则,按签名重排** —— 每条规则一个定时器会让根本没在画的规则白白唤醒,而屏幕上同时只可能有一条规则的图。它只在**排期真正依赖的那些输入**组成的签名变化时重排:规则 id、开关、间隔、顺序、图片数量、标签页可见性。所以拖动滑块导致每秒重跑几十次 apply 也不会把停留计时反复清零(若按「每次 apply 就重排」的写法,把透明度滑块拖上 30 秒就意味着这 30 秒里轮换永远不会前进)。标签页隐藏时轮换跟着画面一起停下,回到前台时按全新间隔重新计时,而不是对着没人看的标签页画三次。
107
- - **换的只有画面,规则本身不动** —— 主题色、布局模式、背景透明度与模糊度都留在规则上,过渡用的就是切换模型时那套双层交叉淡入。规则还能要求比切换更慢的淡入,或者 `fadeMs: 0` 直接硬切 —— 而 0 是一个真实取值,不会被当成「没设置」。
102
+ - **第二种触发完全不需要定时器** —— 「切换模型时也换一张」:每次切换模型落到这条规则上就前进一张。不排期、不唤醒,只在用户做事的时候换图。
103
+ - **随机绝不会抽到当前正在显示的那张** —— 挑选在**除当前这张之外**的所有下标上等概率进行,因为一次「图没变」的 tick 与一次「压根没触发」的 tick 完全无法区分 —— 而这正是这个功能唯一可能悄无声息失败的方式。`pnpm check:rotation` 对 2、3、7 张图的列表把整个随机取值范围**穷举**了一遍,而不是抽一次样本。
104
+ - **全插件只有一个定时器,始终对准生效规则,按签名重排** —— 每条规则一个定时器会让根本没在画的规则白白唤醒,而屏幕上同时只可能有一条规则的图。它只在排期真正依赖的那些输入组成的签名变化时重排:规则 id、开关、间隔、顺序、图片数量、标签页可见性 —— 所以拖动滑块导致每秒重跑几十次 apply 也不会把停留计时反复清零。标签页隐藏时轮换跟着画面一起停下,回到前台时按全新间隔重新计时。
105
+ - **换的只有画面,规则本身不动** —— 主题色、布局模式、背景透明度与模糊度都留在规则上,过渡用的就是切换模型时那套双层交叉淡入。`fadeMs: 0` 是一个真实取值,不会被当成「没设置」。
108
106
  - **取景移到图片上,这才是它该在的地方** —— 构图属于某一张图:两张宽高比不同的壁纸共享规则的模式、颜色、透明度与模糊度,但为第一张算出来的那套适应取景,用在第二张上就是错的。每张图各自带一份 `bgState`,背景编辑器编辑的是胶片条里选中的那一张。
109
- - **单图缩略图变成胶片条** —— 可一次加多张(**多选文件**、**拖放**,或**一行一个网址**批量粘贴,逐个下载以免 N 个请求互相穿插、连进度都没法显示)、点选要编辑的那张、排序、设为**第一张**(不轮换时规则画的就是它)、**原地替换这张**(替换会保住它在轮换里的位置,所以它与「添加」分开命名)、或删除。
110
- - **规则可以一张图都没有,而这是一个必须成立的状态** —— 这个功能的第一版拒绝删除最后一张,转而「清空它的字节」,结果留下的正是条目本身:胶片条第 1 位一块永远删不掉的空白图,于是下一次上传只能排到第 2 位,规则继续什么都不画,预览也一直停在空帧。现在删除最后一张就是把列表清空。新建的规则也从空列表开始(第一张图到达时才分配槽位);手改导致「每一项都不可用」的列表同样落到这个状态,而不是被硬塞回一个坏槽位。只有「哪里都没提到可用图片」的规则(既没有列表、也没有旧式槽位)才会被 sanitizer 丢掉。
111
- - **一张图都没有的规则,靠主题色继续生效** —— 空列表不等于「这条规则失效」,而第一版把它当成了失效:「可用」只看有没有图,于是一条图片删光、主题色还在的规则**不再参与匹配**,它那份主题色连同色轮、灵感色板、「跟随系统主题」全成了摆设(报告里的现象就是「主题色无效」),而它原本匹配的模型还会被悄悄交给**下一条**规则 —— 把兜底规则的图删光,屏幕上出现的就是另一个模型的壁纸。现在判定标准是**有图,或者有自己的主题色**(`ruleCanPaint`):这样的规则照旧匹配、照旧当兜底,只是不铺壁纸、只给界面配色;两者都没有(比如刚新增的空规则)才会被跳过。面板上的提示也按三种真实状态分开说:没有图但有主题色、既没有图也没有主题色、有图但选中的那张还没取到内容 —— 之前这三种情况共用一句「这条规则还没有图片」,其中两种是假话;「从本图提取」旁边那句「优先从本图取色」也不再出现在无图可取的时候。
112
- - **凡是可能改变「谁赢」的编辑,现在都立刻生效;只有不可能改变的才允许跳过重画** —— `color`、`enabled`、`match` 都参与「哪条规则赢」的判定,但每条写入路径各自问的是一个很窄的问题(「这条规则是不是正在画的那条?」),于是只有对**已经**在生效的那条规则的编辑会重画。而「本来没在生效」的编辑恰恰就是**制造**赢家的那些:一条空规则的第一张图(正是它让这条规则变得可画 —— 见 `ruleCanPaint`),以及落在它身上的第一个颜色或自动提取出的颜色。这些改动确实进了配置、也进了面板自己的读数,但背后的界面还停在原来的规则上;报告里的说法是「更新的不太及时,只有我动调色盘或者切模型的时候才出现」—— 拖色轮或切模型才终于重跑了一次解析。现在每条路径都会在改动**之前**取一次赢家、改动之后再做比较(`repaintIfMoved`,判定本身是 `src/client/repaint.ts` 里的纯函数 `shouldRepaint`),这同时修好了一个会变旧的记录:当被编辑的规则**就是**赢家时,即使页面从没把它记成生效规则,也会重画。`pnpm check:repaint` 把这条判定的真值表走了一遍 —— 赢家移动的两个方向、以及那些必须保持廉价的情形 —— 并且只要入口文件里那九条写入路径(外加上传之后才到达的自动提取)中的任何一条不再询问它,就会失败。把它钉下来的实机验证让一条一次性规则走了完整序列(新建空规则 → 移到最上 → 上传一张图 → 之后什么都不碰):用旧判断时画面一直不动,直到有别的东西去动它;用现在这版,约 280 毫秒内画面就跟着换了,而且那张图自动提取出的颜色一起到位。
113
- - **清空之后再上传** —— 你加进去的那张就是你正在看的那张(卡片会把编辑焦点移到新加入的第一张,胶片条、大预览与位置读数一起跟上),而不是把面板留在之前选中的东西上。
114
- - **只加载用得到的图** —— 启动时只灌每条规则的**第一张**(与多图出现之前每条规则灌一张完全一致),外加今天生效的节日图与当前模型正在画的那张。其余的在展开卡片时、或轮换即将用到它时再取。若在启动时灌满每条规则的每张图,「五条规则各四张」就意味着首帧之前先把二十张全尺寸 data URL 从本插件自己的 RPC 通道推一遍;同理,store 报出的槽位列表也不再被灌进内存 —— 没有任何规则引用的孤儿槽位,本来就没有卡片可以显示它。
115
- - **旧宿主进程再也不可能吃掉一份配置 —— 判定改成版本比较,格式版本因此升到 3** —— 浏览器半边与 node 半边是分开加载的,所以「更新包之后只刷新一次页面」会把**新的**客户端 bundle 与仍在运行**上一个** node 半边的宿主进程配在一起。那个旧 sanitizer 完全不认识 `images`,于是会在下一次拖动滑块时悄无声息地丢掉每一条规则。因此 node 半边会公布自己写入的配置格式(每次 `read` 都带 `schema`),而发现答案是旧版的客户端会**扣住所有配置写入**(唯一入口:`src/client/rpc.ts` 的 `canWriteConfig`)、在控制台警告一次,并在面板里说明「重启 DSH」。悄悄不保存的修改,比明说要等重启的修改更糟;而悄悄删掉规则的修改,比两者都糟。0.7.0 开发期间这道闸门真的漏过一次:当时版本号是 2,正在运行的 0.7 node 半边虽然也报 `schema: 2`,却是**空规则修复之前**的构建,于是「把规则删空」的写入照旧放行,宿主把这几条规则从磁盘上抹掉了。所以这个版本号现在表示的是「**空图片列表合法**」(`SCHEMA_VERSION = 3`),而不只是「规则里有 `images` 字段」:任何报出更低版本的宿主一律进入扣住写入的状态。
116
- - **老配置照常读,老导出照常导入** —— `normalizeRule` 同时接受两种形状:规则里那对单图字段(`slot` / `bgState`)在读取时被提升成 `images: [{ slot, bgState }]`,同一条规则里重复的槽位被去掉,不可用的槽位被去掉。主题导出升到**格式版本 4**,导出与还原的是每条规则的**全部**图片而不只是第一张;版本 3 的文件仍可导入。
107
+ - **单图缩略图变成胶片条** —— 可一次加多张(**多选文件**、**拖放**,或**一行一个网址**批量粘贴,逐个下载以免 N 个请求互相穿插、连进度都没法显示)、点选要编辑的那张、排序、设为**第一张**、**原地替换这张**(替换会保住它在轮换里的位置,所以它与「添加」分开命名)、或删除。
108
+ - **规则可以一张图都没有,而这是一个必须成立的状态** —— 第一版拒绝删除最后一张,转而「清空它的字节」,结果留下的正是条目本身:胶片条第 1 位一块永远删不掉的空白图。现在删除最后一张就是把列表清空;新建的规则也从空列表开始;手改导致「每一项都不可用」的列表同样落到这个状态,而不是被硬塞回一个坏槽位。
109
+ - **一张图都没有的规则,靠主题色继续生效** —— 第一版把空列表当成了「这条规则失效」:「可用」只看有没有图,于是一条图片删光、主题色还在的规则**不再参与匹配**,而它原本匹配的模型还会被悄悄交给**下一条**规则。现在判定标准是**有图,或者有自己的主题色**(`ruleCanPaint`):这样的规则照旧匹配、照旧当兜底,只是不铺壁纸、只给界面配色。面板上的提示也按三种真实状态分开说。
110
+ - **凡是可能改变「谁赢」的编辑,现在都立刻生效;只有不可能改变的才允许跳过重画** —— `color`、`enabled`、`match` 都参与「哪条规则赢」的判定,但每条写入路径各自只问「这条规则是不是正在画的那条?」,于是只有对**已经**在生效的那条规则的编辑会重画 —— 而「本来没在生效」的编辑恰恰就是**制造**赢家的那些。现在每条路径都会在改动**之前**取一次赢家、改动之后再做比较(`repaintIfMoved`),`pnpm check:repaint` 在入口文件里那九条写入路径中的任何一条不再询问它时会失败。
111
+ - **清空之后再上传** —— 你加进去的那张就是你正在看的那张:卡片会把编辑焦点移到新加入的第一张,胶片条、大预览与位置读数一起跟上,而不是把面板留在之前选中的东西上。
112
+ - **只加载用得到的图** —— 启动时只灌每条规则的**第一张**(与多图出现之前每条规则灌一张完全一致),外加今天生效的节日图与当前模型正在画的那张;其余的在展开卡片时、或轮换即将用到它时再取。若在启动时灌满每条规则的每张图,「五条规则各四张」就意味着首帧之前先把二十张全尺寸 data URL 从本插件自己的 RPC 通道推一遍。
113
+ - **旧宿主进程再也不可能吃掉一份配置 —— 判定改成版本比较,格式版本因此升到 3** —— 浏览器半边与 node 半边是分开加载的,所以「更新包之后只刷新一次页面」会把**新的**客户端 bundle 与仍在运行**上一个** node 半边的宿主进程配在一起,而那个旧 sanitizer 完全不认识 `images`,于是会在下一次拖动滑块时悄无声息地丢掉每一条规则。因此 node 半边会公布自己写入的配置格式,而发现答案是旧版的客户端会**扣住所有配置写入**、在控制台警告一次,并在面板里说明「重启 DSH」。悄悄不保存的修改,比明说要等重启的修改更糟;而悄悄删掉规则的修改,比两者都糟。这个版本号现在表示的是「**空图片列表合法**」(`SCHEMA_VERSION = 3`),而不只是「规则里有 `images` 字段」,所以任何报出更低版本的宿主一律进入扣住写入的状态。
114
+ - **老配置照常读,老导出照常导入** —— `normalizeRule` 同时接受两种形状:规则里那对单图字段(`slot` / `bgState`)在读取时被提升成 `images: [{ slot, bgState }]`,同一条规则里重复的槽位被去掉,不可用的槽位被去掉。主题导出升到**格式版本 4**,导出与还原的是每条规则的**全部**图片;版本 3 的文件仍可导入。
117
115
  - **各图槽位互不干扰,删除规则只回收它自己那份** —— 一条三张图的规则占三个 `modelbg-<slot>` 文件;`nextSlot` 扫描配置里的**每一个**槽位(用户规则**与**节日条目都算),所以新加的一张图绝不会撞上已有的槽位;删除规则时,只有当某个槽位不再被任何地方引用时才回收。
118
- - **node 半边连嵌套字段的漂移也一并报告,迁移直接写新形状** —— `warnUnknownConfigKeys` 现在会往深一层比较 `rules[].rotate` 与 `rules[].images[]` —— 一个只有客户端 bundle 知道的字段正是在那里无声消失的;同时 `slot` / `bgState` 这两个旧字段名被明确豁免,因为 sanitizer 是**故意**要提升它们的。0.3 之前配置的迁移也直接写出图片列表,而不是把一个旧形状留在磁盘上等第一次写入。
116
+ - **node 半边连嵌套字段的漂移也一并报告,迁移直接写新形状** —— `warnUnknownConfigKeys` 现在会往深一层比较 `rules[].rotate` 与 `rules[].images[]` —— 一个只有客户端 bundle 知道的字段正是在那里无声消失的;同时 `slot` / `bgState` 这两个旧字段名被明确豁免,因为 sanitizer 是**故意**要提升它们的。0.3 之前配置的迁移也直接写出图片列表。
119
117
 
120
118
  ## v0.6.0
121
119