dsh-background-by-model 0.10.0 → 0.12.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.md CHANGED
@@ -1,6 +1,71 @@
1
1
  # Changelog
2
2
 
3
- > The full release history lives here; the **Recent optimizations** section of [README.md](./README.md#recent-optimizations) lists only the two most recent releases (v0.10.0 / v0.9.2).
3
+ > The full release history lives here; the **Recent optimizations** section of [README.md](./README.md#recent-optimizations) lists only the two most recent releases (v0.12.0 / v0.11.0).
4
+
5
+ ## v0.12.0
6
+
7
+ - **Fix: "installing the profile turned my AI access back off"** — the profile is a LOOK (rules, wallpapers, interface numbers), and authorisation is not part of a look: it answers "may an agent change this", was given by hand, and no button may take it back. `recommendedConfig()` carried the shipped default of `false`, so installing a look silently revoked it.
8
+ - **The other half of the same rule** — the profile still ships `agentAccess: false`, so that installing it cannot GRANT access; what is added here is the symmetric half, that installing it cannot REVOKE it either. It is the same sentence the import path already follows, which keeps the importing machine's own value rather than the file's.
9
+ - **The grant is read before the wipe** — the same reason as the revision: after `resetStore` that config is gone, and reading late would always find the shipped default.
10
+
11
+ - **Importing a theme obeys the same "bytes follow the config" rule** — the third path that rule had missed. An import deleted the pictures its old rules pointed at immediately, so a save that was then refused and rolled back brought the rules back on screen with their pictures really gone. Its deletes now queue behind the write, like every other removal.
12
+ - **A tool-added picture can no longer collide with yours** — the tool looked for the first free slot in the config ON DISK while the panel looked in its IN-MEMORY copy, with a download of several seconds in between: add a picture at that moment and both got the same `mN`, silently overwriting each other. It no longer asks "which number is free" but derives a name from the clock, which nothing else will choose.
13
+ - **Two callers cannot adopt at once** — the panel's button and the agent's `adopt_recommended` reach one function but cannot see each other. The wipe is irreversible, so the exclusion lives where both arrive: the second caller is told the profile is already being installed rather than wiping alongside.
14
+ - **`check:rev` covers all three** — an import must defer its deletes, a derived slot must fit `SLOT_RE`, and the exclusion must exist and be released in a `finally`. Each was verified to fail when its fix is reverted.
15
+
16
+ - **Fix: "delete quickly and the rule stays but its picture is gone"** — two saves fired back to back carry the SAME revision (a revision only advances when a save SUCCEEDS), so the first is accepted and the second is refused as stale and rolled back to the config from before the second edit. The second rule therefore leaves the screen — but the picture bytes it already deleted do not come back, which is a card reading "这张图片还没有取到内容" with the rule still there.
17
+ - **Config writes are chained** — each save waits for the previous one to finish and record its revision before starting, because two concurrent writes to one document can never both be right. A single chain for the whole config rather than per key: the config IS one document.
18
+ - **Picture bytes are released only after the config lands** — the bytes are what the config points AT, so the config that stops pointing at them has to be on disk first. When a write is refused, the queued deletes are dropped: those rules are still on screen and their pictures must not vanish. Genuinely unreferenced files are what the cleanup card is for.
19
+ - **Both removal paths changed** — deleting a rule and deleting one image. Leaving one on the immediate delete puts the bug back through the door next to the one that was closed.
20
+ - **The queue drains on page hide** — otherwise a delete can outlive the tab that asked for it.
21
+
22
+ - **Fix: "这张图片还没有取到内容" after deleting quickly** — slot names are RECYCLED: deleting a rule frees its `mN` immediately and the next upload takes the same number. Delete and write are independent requests on one connection, so which reaches the host first is whatever the transport decides that moment — the delete can land AFTER the write, leaving the config naming a slot whose bytes were just removed. The card is there, the slot name is there, the picture is not, and nothing reports an error because nothing failed.
23
+ - **Byte operations on one slot now run in the order they were asked for** — read, write, delete and fetch-by-URL share a queue **keyed by slot**. Per slot rather than global: adding seven pictures at once must not serialise into seven round trips.
24
+ - **A failure does not wedge the slot** — a rejected operation does not block the ones behind it.
25
+ - **`check:rev` pins the chain** — all four operations must go through the queue, the queue must be keyed by slot, and `nextSlot` must really recycle (or the check is describing something that cannot happen).
26
+
27
+ - **Fix: "could not write the recommended profile — your own is untouched" was a lie** — that line is only true when the DOWNLOAD fails. It was also shown for failures after the wipe (a picture that would not write, the config that would not write, a wipe that only half succeeded), and by then the user's rules and pictures were already gone. A message saying "nothing was touched" stops them from retrying and sends them looking for data that is not there.
28
+ - **Three outcomes now, each with its own message** — a download failure (the store is untouched, and it says so), a failure after the wipe (says the old rules are gone and that pressing again starts over), and success. The node half reports `wiped` itself rather than the browser guessing from an error string.
29
+ - **`applyRecommended` returns a result object instead of a boolean** — one boolean cannot hold the difference between "it failed and your data is still there" and "it failed and your data is gone".
30
+ - **The panel re-reads after a failed adopt** — when the wipe had already happened, this half's copy names rules that no longer exist, and painting from it is showing a ghost.
31
+
32
+ - **Fix: "installing the recommended profile also needs two presses"** — the same defect as the line above, but I had only fixed the ordinary-save path. The profile used to be two steps: `resetStore`, then the panel's own write. The wipe deletes the config ON DISK, which takes the revision back to 0 while the panel still holds the pre-wipe number — so its write was refused as stale and adopted **the config it had just replaced**. The button looked pressed and did nothing.
33
+ - **Installing the profile is one operation now** — the host's `adoptRecommended` (download, wipe, install, answer the revision it landed on) is the only implementation, and the panel makes one round trip instead of sequencing a wipe. The tools already called that function; both callers are finally the same path.
34
+ - **`applyThemeFile` loses its `wipeFirst` variant** — that was the doorway to the two-step path, and leaving it open leaves a way for the bug to come back. An import now only means "overlay this file on a live store".
35
+ - **`check:rev` pins it** — the panel must call that one operation and must not `resetStore` itself, `applyThemeFile` must not take `wipeFirst`, and the host must read the replaced revision BEFORE the wipe and floor on it. `check:node` adds a real round trip: one call installs the seven rules and answers a revision ahead of the one it replaced, matching what is on disk.
36
+
37
+ - **Fix: "click + once, the card flashes and disappears, click again and it stays"** — the panel did not record the revision the host answered a successful write with, so its NEXT save named a revision that was already gone, was refused as stale, and was handed the config from disk (which does not contain the edit it just made); adopting that rolled the edit back on screen. Every successful write now records the host's answer, so consecutive gestures stop contradicting each other.
38
+ - **Importing a theme is no longer refused by its own save** — the same defect by the other route: an import zeroed the revision, so the save that follows was necessarily stale, refused, and rolled back, making the file appear to import and then un-import itself. The revision now passes through the import (read before the wipe, since `resetConfig` replaces the config object).
39
+ - **`check:rev`** — pins the chain: a write must carry `expectedRev`, the success path must record the host's answer, `setRev` must mutate the live object rather than replacing the reference, an import must not zero the revision, and that value must be read before the wipe. Both assertions were verified to fail when the fix is removed; `check:node` also carries a two-consecutive-writes protocol round trip.
40
+
41
+ - **`background_edit` gains `adopt_recommended`** — the same operation as the Config tab's button: download the built-in profile's seven pictures, empty the store, install its seven rules. It is the only action in the tools that deletes data, so it requires an explicit `confirm: true` and otherwise refuses **before touching anything** (an explicit `false` is a refusal too; absence is not read as consent).
42
+ - **Download first, wipe second** — the panel's own order. One unreachable picture fails the whole attempt and names the slot, and the user's rules are untouched at that point; wiping first would turn "the CDN is down" into "the configuration is gone".
43
+ - **The adopted revision lands strictly above the one it replaced** — `resetStore` zeroes the history, so a panel still holding the pre-adoption config could sit at 1, match the profile's own first write, and overwrite it. Adopting now floors on the replaced revision, so that save is refused and the panel adopts the profile instead.
44
+ - **The festival art is not taken with it** — `resetStore` never touched `holiday-cache/`: that art is downloaded, belongs to no slot, and costs a round trip to get back.
45
+ - **`check:node`** — a new adopt section: refused with no `confirm` and with `confirm: false` before the host is reached, the rules becoming the built-in seven, all seven pictures on disk, the adopted revision ahead of the replaced one, a stale panel write refused with the profile still in place, and the festival art spared.
46
+
47
+
48
+ - **Two tools the agent can use to change the appearance** — `background_theme` reads the rules, the images (by slot, never as bytes) and the interface numbers; `background_edit` adds, edits and removes rules and images, and sets layout, opacity, brightness, blur, theme colour, the global interface opacities and the switch effect. Both call the same node-half operations the settings panel does, so there is one sanitizer and one write path, not a second opinion about them.
49
+ - **The switch is a privilege, not a preference** — a new `agentAccess` in the config, `false` by default. While it is off the two tools are absent from the model's tool table entirely (`tools.register`'s `when`), so a closed switch costs no context and cannot be called. The recommended profile does NOT turn it on either: pressing "use the recommended profile" asks for a look, not for an agent to be allowed to rewrite it.
50
+ - **One switch on the Config page**, above the holiday switch, because it is the heavier of the two. Turning it on confirms what was granted; turning it off needs no confirmation, since the capability simply stops existing.
51
+ - **An export never carries the grant** — `buildThemeFile` strips `agentAccess` (and `rev`), and `applyThemeFile` keeps the importing machine's own value regardless of what the file says. A theme file describes an appearance; access is not part of one.
52
+ - **The config carries a revision, and a stale write is refused** — two writers hold whole copies of `theme-config.json` (the panel, and now the tools), and both write them back wholesale. Every write names the revision it was computed from; a write that has fallen behind is refused with the config that won, which the loser then adopts and repaints. This is what stops the panel's 250 ms debounced save from landing on top of an agent's edit with both sides believing they succeeded.
53
+ - **The panel notices an agent's edit without a reload** — a 3 s `readRev` poll, registered on the existing watchdog schedule, a no-op while access is off.
54
+ - **`SCHEMA_VERSION` is 11** — two new fields, and both are load-bearing on their own: a schema-10 sanitizer would drop `agentAccess` (silently re-defaulting an authorisation) and `rev` (making every tool edit indistinguishable from a stale one).
55
+ - **`check:agent`** — the switch's defaults and its exact-true rule, the recommended profile not granting it, the revision arithmetic, and both fields being declared in the published shape. `check:node` gains the stale-write round trip through the real RPC handler, the access round trip and its fail-closed read, `readRev`, and the tool registrations with their gates.
56
+
57
+ ## v0.11.0
58
+
59
+ - **The runtime download source moves to Bilibili's image CDN** — the recommended profile's seven wallpapers and both holiday pictures now come from `i0`/`i1`/`i2.hdslb.com`, three edges falling back to each other (measured: same path, same bytes, identical sha1). The README's three screenshots play no part at runtime and stay where they were.
60
+ - **The recommended profile's rules ship inside the plugin** — seven model rules and their interface numbers moved into `src/profile.ts`, and the button downloads only the seven pictures. It no longer depends on GitHub being reachable, and the `preset.json` manifest, the schema-version gate and the "not a profile" error codes retire with it: what gets installed is always what this build itself writes, and nothing remote chooses what that is.
61
+ - **The request set is a compile-time constant** — slot → BFS path, written out in one table, and `fetchPresetImage` honours only that table: a slot it does not carry answers `bad slot` before any request, so a wrong press cannot become "fetch 64 files because somebody else's document said so". `PRESET_IMAGE_COUNT_MAX` and that whole attack surface go with it.
62
+ - **A BFS path is a content hash, so an address is immutable** — the "a tag can be re-pointed at different bytes" case cannot arise, which also retires "remember to bump `*_ASSETS_REF` along with the art".
63
+ - **The addresses must carry `@3840w.webp`** — the uploads are 2.4–12 MB PNGs (76 MB for the set); that suffix is what serves 250–365 KB 4K WebPs, byte-for-byte what an existing data directory already holds, so nothing is re-downloaded and nothing loses detail on a 4K screen.
64
+ - **The festival cache is keyed by content hash** — `holiday-cache/<tag>/` became `holiday-cache/<first eight of the hash>/`, computed by `holidayArtGeneration` from the URL. New art means a new address, therefore a new directory and a guaranteed re-download, with no version constant to remember; the old tag directories (and 0.7.9's flat layout) are taken by the same `pruneHolidayCache` once the new art lands, whose keep-set is the deduplicated set of "the current generation of each holiday".
65
+ - **`SCHEMA_VERSION` stays 10** — nothing entered or left the config shape: what changed is where these numbers come from, not what they look like, so an older host and an older exported file both still read it.
66
+ - **`check:preset` now pins the built-in profile itself** — rule order and paintability, the slot ↔ URL table agreeing in both directions, the webp transform and content hash in every address, `profileImagePath`'s null whitelist, generations that cannot collide, and the old remote wiring being gone.
67
+ - **`check:node` rewrites the holiday and profile sections** — the three edges asked in order and always for the SAME path, a holiday failing only when every edge does, a retry budget of nine requests (three edges × three rounds) for weather worth waiting out, the old `fetchPresetConfig` method provably gone, the slot whitelist (holiday slots, table-missing slots and malformed names all refused before anything is requested), a stale `holiday-cache/v0.1.0` directory being swept, and `resetStore` still leaving the festival art alone.
68
+ - **`check:scheme` section 3 no longer reaches the network** — it used to fetch the remote `preset.json` and compare schemas; that manifest is gone, so it now pins that the client asks for no head at all, that the slot list comes from the built-in table, and that the addresses carry the webp transform at Bilibili's hosts.
4
69
 
5
70
  ## v0.10.0
6
71
 
package/CHANGELOG.zh.md CHANGED
@@ -1,6 +1,71 @@
1
1
  # 更新日志
2
2
 
3
- > 这里保留完整的历史记录;[README.zh.md](./README.zh.md#近期优化) 的「近期优化」一节只列最近两个版本(v0.10.0 / v0.9.2)。
3
+ > 这里保留完整的历史记录;[README.zh.md](./README.zh.md#近期优化) 的「近期优化」一节只列最近两个版本(v0.12.0 / v0.11.0)。
4
+
5
+ ## v0.12.0
6
+
7
+ - **修掉"开着 AI 授权去用推荐配置,开关被关掉"** —— 推荐配置是**一份外观**(规则、壁纸、界面数值),而授权不是外观的一部分:它回答的是"AI 能不能改这套东西",是用户亲手给的,没有任何按钮可以收回。之前 `recommendedConfig()` 带着出厂默认 `false`,于是"装一份外观"顺手把授权撤销了。
8
+ - **同一条规则的另一面** —— 推荐配置出厂仍带 `agentAccess: false`,这样**安装它不能授予**权限;现在补上对称的一半:**安装它也不能撤销**。这和导入主题的处理是同一句话——导入时也保留**本机**的值,而不是文件里的。
9
+ - **授权值在清空之前读** —— 和版本号同一个道理:`resetStore` 之后那份配置就没了,晚一步读到的永远是出厂默认。
10
+
11
+ - **导入主题也遵守"字节等配置落地"** —— 这是同一条规则漏掉的第三条路径。导入时那条旧规则指向的图片会被直接删掉,如果紧随其后的保存被判陈旧、回滚,画面上规则回来了、图片却是真的没了。现在删除和配置写入一样排到保存成功之后。
12
+ - **AI 加的图片不再可能和你的撞号** —— 工具原来在**磁盘配置**上找第一个空号,面板却在**内存副本**上找同一个号;中间隔着几秒的下载,你正好加一张图就会拿到同一个 `mN`,于是互相覆盖。现在工具不再问"哪个号空着",而是取一个由时钟派生的名字,别的东西选不到它。
13
+ - **两条路径不能同时采用推荐配置** —— 面板的按钮和 AI 的 `adopt_recommended` 走同一个函数,但彼此看不见。清空不可逆,所以互斥放在两者都会到达的地方:后到的那个被告知"正在安装,请稍候",而不是跟着一起清空。
14
+ - **`check:rev` 补齐三组断言** —— 导入必须用延迟删除、槽位名必须由时钟派生且符合 `SLOT_RE`、互斥必须存在且用 `finally` 释放。每一条都验证过"撤掉修复就会红"。
15
+
16
+ - **修掉"快速删除后,规则还在但图片没了"** —— 两次保存接连发出时,它们**带着同一个版本号**(因为版本号正是"上一次保存成功后才前进"的),于是第一次被接受、第二次被判陈旧、并从**第二次编辑之前**的配置回滚。屏幕上的第二条规则因此消失,但它已经删掉的图片字节**不会回来** —— 这就是卡片上写着"这张图片还没有取到内容"而规则还在的原因。
17
+ - **配置写入现在串成一条链** —— 每次保存都等上一次结束(并记下新版本号)再开始,否则并发写入同一个文档永远不可能都对。这是全局一条链而不是按键分键:配置本身就是**一份文档**。
18
+ - **图片字节改成"配置落地之后再删"** —— 字节是配置**指向的东西**,所以必须先让不再指向它的那份配置落盘。保存被拒时,排队的删除一并丢弃:那些规则还在屏幕上,它们的图片不该消失。真要清理,清理卡本来就是为这种情况存在的。
19
+ - **两条移除路径都改了** —— 删规则与删单张图。留一处用即时删除,这个 bug 就会从那扇没关的门走回来。
20
+ - **页面关闭时把队列排空** —— 否则一次删除可能比要求它的那个标签页活得更久。
21
+
22
+ - **修掉删除点得快时"这张图片还没有取到内容"** —— 槽位号是**回收复用**的:删掉一条规则立刻释放它的 `mN`,下一次新增马上会拿到同一个号。而删除和写入是两个独立的请求,走同一条连接,谁先到宿主取决于当时传输层的安排 —— 删除可能**后于**写入落地,于是配置指着一个刚刚被删掉字节的槽位。卡片在、槽位名在、图片没了,而且没有任何地方报错,因为确实没有任何一步失败。
23
+ - **同一个槽位的字节操作现在严格按发起顺序执行** —— 读、写、删、按网址下载四个操作共用一个**按槽位分键**的队列。是每槽一个队列而不是全局一个:七张图一起加不应该被串成七次往返。
24
+ - **失败不会卡住槽位** —— 前一个操作抛错,后面的照常执行,队列不会因为一次失败而永久阻塞。
25
+ - **`check:rev` 钉住这条链** —— 四个操作都必须过队列、队列必须按槽位分键、以及 `nextSlot` 确实是回收式的(否则这条检查描述的就不是真事)。
26
+
27
+ - **修掉"未能写入推荐配置,你原来的配置没有被改动"这句假话** —— 那句提示只在下载阶段失败时才成立。可它同样出现在**清空之后**才失败的情况(某张图写不进去、配置写不进去、清空只成功了一半),而那时你的规则和图片其实已经没了。一条说"没动过"的提示会让人不敢再点、也不敢去别处找。
28
+ - **失败现在分三种,各有各的话** —— 下载失败(存储还没碰,明确说"没有被改动")、清空之后失败(明确说"你原来的规则已被清空,再点一次可以重来")、以及成功。分辨这一点的是 node 半边自己报告的 `wiped`,不是浏览器从错误字符串里猜。
29
+ - **`applyRecommended` 从返回 `boolean` 改为返回结果对象** —— 一个布尔值装不下"失败了,但你的数据还在"和"失败了,你的数据已经没了"这两种完全不同的处境。
30
+ - **失败后浏览器会重新读盘** —— 清空之后才失败时,面板手里的规则已经不存在了,继续照着它画就是在展示幻觉。
31
+
32
+ - **修掉"采用推荐配置同样得点两次"** —— 和上一条同源,但我第一次只修了普通保存那条路。推荐配置原来是两步:先`resetStore`清空、再由面板自己写一遍。而清空会把磁盘上的配置删掉、版本号随之回到 0,面板手里却还是清空前的那个数,于是它那次写入被判陈旧、被拒,并接盘了**刚刚被它替换掉的**配置 —— 按钮看上去"按下去了但没生效"。
33
+ - **推荐配置现在是一个操作** —— Host 侧的 `adoptRecommended`(下载→清空→装入→回传落在的版本号)成为唯一实现,面板走一次往返即可,不再自己编排清空。工具那条路本来就是这个函数,两条路终于合一。
34
+ - **删掉 `applyThemeFile` 的 `wipeFirst` 变体** —— 那正是两步路径的入口。留着它,这个 bug 就还有一条回来的路。导入现在只处理"把文件覆盖到已有存储上"这一件事。
35
+ - **`check:rev` 加钉** —— 面板必须调那一个操作、不得自己 `resetStore`、`applyThemeFile` 不得再有 `wipeFirst`、Host 必须在清空**之前**读被替换的版本号并以它为下限。`check:node` 另加一次真实往返:一次调用装上七条规则,返回的版本号**大于**被替换的那个,且与磁盘一致。
36
+
37
+ - **修掉"点一次 + 卡片闪一下后消失、再点一次才出现"** —— 面板写完配置后没有把宿主返回的新版本号记回自己的副本,于是**下一次**保存带着一个已经过期的版本号,被判为陈旧写入而拒绝;拒绝会把磁盘上那份(不含刚做的改动)交回来,面板接盘,画面上的改动就自己撤销了。现在每次写入成功都记下宿主的回答,连续操作不再自相矛盾。
38
+ - **导入主题也不再被自己的保存拒绝** —— 同一个毛病的另一条路:导入时把版本号写回 0,紧接着那次保存必然判陈旧、于是被拒并回滚,文件看起来"导入完又退回去"。版本号现在原样穿过导入(在清空之前读,因为 `resetConfig` 会把配置对象换掉)。
39
+ - **`check:rev`** —— 钉住这条链:写入必须带 `expectedRev`、成功路径必须记下宿主的回答、`setRev` 必须改写活对象而不是换引用、导入不得把版本号清零、且那个值要在清空之前读。两条断言都验证过"删掉修复就会失败";`check:node` 另有一次连续两次写入的协议往返。
40
+
41
+ - **`background_edit` 新增 `adopt_recommended`** —— 和「配置」页那个按钮做同一件事:下载内置推荐配置的七张图,清空存储,装上七条规则。这是工具里唯一一个会删数据的动作,所以它要求调用方显式带 `confirm: true`,否则**在动任何东西之前**就拒绝(`confirm: false` 也算拒绝,缺席不会被读成同意)。
42
+ - **先下载、后清空** —— 和面板同一个顺序。任何一张图取不到就整体失败并报出是哪个槽位,此时用户的规则一个字节都没动;先清空后下载会把"CDN 连不上"变成"配置没了"。
43
+ - **采用后的版本号严格大于被替换的那一个** —— `resetStore` 会把版本号打回 0,于是一个还拿着采用前配置的面板可能正好停在 1,与推荐配置自己那次写入撞上而**覆盖回去**。现在采用时以被替换的版本号为下限,所以那次保存会被拒绝、由面板接盘推荐配置。
44
+ - **节日画不跟着走** —— `resetStore` 本来就不动 `holiday-cache/`:那是下载来的、不属于任何槽位、重新取要花一个来回。
45
+ - **`check:node`** —— 新增 adopt 一节:无 confirm 与 confirm: false 都在触碰宿主之前被拒、确认后规则变成内置那七条、七张图都落盘、采用后的版本号大于采用前、以及"采用前面板"的陈旧写入确实被拒绝且推荐配置仍在、节日画未被清掉。
46
+
47
+
48
+ - **给 AI 两个改外观的工具** —— `background_theme` 读规则、图片(给槽位名,绝不给字节)与界面数值;`background_edit` 增删改规则与图片,设置布局、透明度、亮度、模糊、主题色、全局界面透明度与切换效果。两者调用的是面板用的同一批 node 半边操作,所以只有一套 sanitizer 和一条写入路径,而不是关于它们的第二种说法。
49
+ - **开关是授权,不是偏好** —— 配置里新增 `agentAccess`,默认 `false`。关着的时候那两个工具**完全不出现在模型的工具表里**(`tools.register` 的 `when`),所以关掉不占上下文、也不可能被调用。推荐配置**也不替你打开**:点「使用推荐配置」是要求一份外观,不是要求 AI 获得改它的权限。
50
+ - **「配置」页上一个开关**,放在节日开关上面,因为它更重。打开时会确认授权了什么;关闭不需要确认,因为能力直接不存在了。
51
+ - **导出永远不带这份授权** —— `buildThemeFile` 剥掉 `agentAccess`(以及 `rev`),`applyThemeFile` 则无论文件里写什么都不改**本机**的值。主题文件描述的是一份外观,授权不属于外观。
52
+ - **配置带上版本号,落后的写入会被拒绝** —— 两个写入者各自持有 `theme-config.json` 的整份副本(面板,如今还有工具),并且都整份写回。每次写入都带着"我基于哪个版本",落后的一方会被**拒绝**并拿到赢家的那份,由它接盘重绘。这正是拦住"面板 250 ms 防抖的保存落在 AI 的改动之上、双方都以为自己成功了"的那道闸。
53
+ - **面板不用刷新就能看到 AI 的改动** —— 3 秒一次的 `readRev` 轮询,挂在既有的调度器上,授权关闭时是个空操作。
54
+ - **`SCHEMA_VERSION` 升到 11** —— 两个新字段,且各自都是承重的:schema-10 的 sanitizer 会丢掉 `agentAccess`(把一份授权静默改回默认)和 `rev`(让每一次工具编辑都无法与陈旧写入区分)。
55
+ - **`check:agent`** —— 开关的默认值与"只有显式 true 才算",推荐配置不授权,版本号的运算,以及两个字段都在发布出来的形状里。`check:node` 新增真实 RPC 处理器的陈旧写入往返、授权的往返与其读失败即关闭、`readRev`,以及带闸门的工具注册。
56
+
57
+ ## v0.11.0
58
+
59
+ - **运行时下载源换成 B 站图床** —— 推荐配置的七张壁纸与中秋、国庆两张节日画全部改从 `i0/i1/i2.hdslb.com` 取,三个边缘互为回落(实测同样路径、同样字节,sha1 一致)。README 的三张截图不参与运行时,仍留在原来的地方。
60
+ - **推荐配置的规则改成随插件内置** —— 七条模型规则与那套界面数值搬进 `src/profile.ts`,按钮只下载七张图。于是它不再依赖 GitHub 能通,`preset.json` 清单、schema 版本闸与「不是配置」这类错误码一起退休:能被安装的规则永远就是这份构建自己写的那些,远端没有任何东西在选择装什么。
61
+ - **请求清单变成编译期常量** —— 槽位 → BFS 路径写死在一张表里,而 `fetchPresetImage` 只认这张表:表里没有的槽位直接回 `bad slot`,所以一次按错不可能变成「按别人给的清单去取 64 个文件」。`PRESET_IMAGE_COUNT_MAX` 与 65 张图那条攻击面随之消失。
62
+ - **BFS 路径是内容哈希,地址天然不可变** —— 不再有「tag 被重新指向别的字节」这种可能;顺带把「换素材要记得同时改 `*_ASSETS_REF`」这个坑一起消掉了。
63
+ - **地址必须带 `@3840w.webp`** —— 上传的原图是 2.4–12 MB 的 PNG(九张合计 76 MB),这样取才是 250–365 KB 的 4K WebP,并且与现有数据目录里那几份的 sha1 逐字节一致,所以 4K 屏上不掉画质、老用户也不需要重下。
64
+ - **节日画的缓存目录按内容哈希分代** —— `holiday-cache/<tag>/` 变成 `holiday-cache/<哈希前八位>/`,由 `holidayArtGeneration` 从 URL 现算。换图 = 换地址 = 换目录 = 必然重下,没有需要记得去改的版本常量;旧的 tag 目录(以及 0.7.9 的平铺布局)在新图落盘后由同一次 `pruneHolidayCache` 带走,清扫时的保留集是「每个节日当前那一代」的去重集合。
65
+ - **`SCHEMA_VERSION` 保持 10** —— 这次没有字段进出配置形状:变的是这些数值从哪来,不是它们长什么样,所以旧宿主、旧导出文件都照常读。
66
+ - **`check:preset` 改成钉内置配置本身** —— 规则顺序与可画性、槽位 ↔ 地址表的双向一致、每个地址的 webp 变换与内容哈希、`profileImagePath` 的 null 白名单、缓存分代不重复,以及旧的远端接线已经不在。
67
+ - **`check:node` 重写节日与推荐配置两节** —— 三个边缘的依次回落与「同一条路径」、全部边缘失败才失败、「一次坏天气值得再来一轮」的重试预算(3 边缘 × 3 轮 = 9 个请求封顶)、旧 `fetchPresetConfig` 方法确实已不存在、槽位白名单(节假日槽位、表外槽位、非法名都在下载前被拒)、`holiday-cache/v0.1.0` 这类旧目录会被清扫,以及 `resetStore` 仍然不动节日画。
68
+ - **`check:scheme` 第 3 节不再联网** —— 它原本要拉远端 `preset.json` 比 schema,现在清单不存在了,改为钉住「客户端不再请求清单」「槽位清单来自内置表」「地址带 webp 变换且指向 B 站」。
4
69
 
5
70
  ## v0.10.0
6
71
 
package/README.md CHANGED
@@ -107,7 +107,7 @@ One switch on the Config page and nothing else: on 中秋节 and 国庆节 the w
107
107
 
108
108
  - **中秋 is 农历八月十五**, resolved from the Chinese calendar in **Asia/Shanghai**, so it moves every year (2024-09-17, 2025-10-06, 2026-09-25 …); **国庆 is October 1–7**; when the two collide, 中秋 wins.
109
109
  - **A holiday with no image falls through** to the model rules rather than blanking the wallpaper.
110
- - **The art is downloaded, not shipped** — one compressed wallpaper per holiday, pinned to a release tag of the project's [assets repository](https://github.com/HarmlessFunny/assets) and cached under `holiday-cache/<tag>/` on the first read of the day. The download is gated on the calendar, so an ordinary day fetches nothing at all and every later boot is answered from disk.
110
+ - **The art is downloaded, not shipped** — one compressed wallpaper per holiday, served by **Bilibili's image CDN** (`i0`/`i1`/`i2.hdslb.com`, three edges falling back to each other) and cached under `holiday-cache/<hash>/` on the first read of the day. The directory is named after the **content hash** in the URL: new art means a new address, therefore a new directory and a guaranteed re-download — there is no version constant to remember to bump. The download is gated on the calendar, so an ordinary day fetches nothing at all and every later boot is answered from disk.
111
111
  - **Neither the art nor its palette is swappable** — a holiday slot is read-only, the colors and the full-bleed Fill mode come from the definition, and theme exports carry your rules only.
112
112
 
113
113
  ### Per-rule and per-image settings
@@ -126,12 +126,29 @@ One switch on the Config page and nothing else: on 中秋节 and 国庆节 the w
126
126
 
127
127
  ### Recommended profile
128
128
 
129
- One click on the Config tab downloads the author's own configuration — rules, wallpapers and interface settings — from the project's assets repository.
129
+ One click on the Config tab installs the author's own configuration — seven model rules and their interface numbers **ship inside the plugin**, and only the seven wallpapers are downloaded, from **Bilibili's image CDN**.
130
130
 
131
- - **Hosted in the store's own shape** (`preset/preset.json`, `preset/theme-config.json`, one `modelbg-<slot>` file per wallpaper), so it stays editable on GitHub and publishing it is a `cp`.
131
+ - **The rules are no longer a remote file** — they are compiled into the bundle, at the same version as the code that reads them. So this button does not need GitHub to be reachable, and there is no "the remote profile is newer than this build" case left to refuse: what gets installed is always what this build itself writes.
132
+ - **The wallpaper addresses are a constant table** — slot → BFS path, fixed at build time. The request set never comes off the network, so a wrong press cannot turn into "fetch 64 files because somebody else's document said so".
133
+ - **One address, one set of bytes** — a BFS path is a hash of the upload, so a wallpaper URL is immutable in a way a git tag is not.
134
+ - **`@3840w.webp` is load-bearing** — the uploads are 2.4–12 MB PNGs (76 MB for the set); with that suffix they are the 250–365 KB 4K WebPs an existing data directory already holds, byte for byte.
132
135
  - **Every interface number in it is what the plugin already ships with**, so on a new install the button adds rules and pictures; on a store you have already tuned it resets those too, because it is a whole configuration and not a patch.
133
136
  - **The download comes before the question**, so the confirmation can say what is actually in hand — and the wipe comes after the download, never before it. It is the only card in the panel that **asks before it deletes**.
134
- - **One missing wallpaper fails the whole attempt and names the slot**, and every file retries on its own, keeping what arrived for the next press.
137
+ - **A failure says which step it reached** — a download failure reports that nothing was touched; a failure AFTER the wipe says the old rules are already gone and that pressing again starts over. The wipe is irreversible, so those two situations must not share one comforting sentence.
138
+ - **One missing wallpaper fails the whole attempt and names the slot**, and every file retries on its own (three edges × three rounds), keeping what arrived for the next press.
139
+
140
+ ### Letting the AI change the background (Config tab, off by default)
141
+
142
+ The first card on the Config tab: one switch deciding whether **the AI may change this appearance through tools**.
143
+
144
+ - **An authorisation, not a preference** — while it is off the two tools **do not exist in the model's tool table at all**, so a closed switch costs no context and cannot be called by mistake. It is not "the tools are there but every call fails".
145
+ - **Off by default, and the recommended profile does not turn it on — and installing it does not turn one OFF either** — neither an upgrade, a fresh install nor "use the recommended profile" hands an agent the ability to rewrite your appearance. Only that switch does.
146
+ - **What the AI can do** — add, edit and remove rules; add an image to a rule from a URL; set per-image layout, opacity, brightness, blur and theme colour; set the global interface opacities and the switch effect; adjust rotation; and **install the recommended profile**.
147
+ - **Installing the recommended profile asks separately** — it does exactly what the panel's button does (download seven pictures, empty the store, install seven rules), so the tool requires an explicit `confirm: true` and otherwise refuses **before touching anything**. Download first, wipe second: if any picture cannot be fetched, your rules are not touched at all.
148
+ - **What it cannot do** — touch the holiday art (that slot is read-only), or read image bytes (the tools answer with slot names, never megabytes of base64).
149
+ - **Edits land immediately** — the panel checks the config's revision on disk every 3 seconds, so an agent's change repaints without a reload.
150
+ - **An export never carries the switch** — sending someone a theme file does not hand them "let the AI change my appearance", and a hand-edited file that says `true` is overruled by the **importing machine's** own value.
151
+ - **The AI and you cannot silently overwrite each other** — every write names the revision it was computed from, a write that has fallen behind is **refused** instead of overwriting, and the panel then adopts the copy that won.
135
152
 
136
153
  ### Image storage cleanup
137
154
 
@@ -153,7 +170,7 @@ Settings → **Theme** now has three tabs:
153
170
  | --- | --- | --- |
154
171
  | **Interface** | Global, shared by every model | Opacity for the main background, left panel, right panel, cards & panels, input & controls, settings panel, conversation text box, trajectory page and every plugin page |
155
172
  | **Model Background** | Per rule, per image | The ordered rule list, the live match readout, and each rule's images (filmstrip), **per-image layout mode, framing and theme color**, plus the rule's opacity, brightness, blur and [rotation](#image-rotation) |
156
- | **Config** | — | The global [switch effect](#switch-effect-global), the whole rule set as a file — [recommended profile](#recommended-profile), import and export (`dsh-background-by-model-theme.json`) — plus the single [holiday background](#holiday-backgrounds) switch and the [image storage cleanup](#image-storage-cleanup) |
173
+ | **Config** | — | The [let the AI change the background](#letting-the-ai-change-the-background-config-tab-off-by-default) switch, the global [switch effect](#switch-effect-global), the whole rule set as a file — [recommended profile](#recommended-profile), import and export (`dsh-background-by-model-theme.json`) — plus the single [holiday background](#holiday-backgrounds) switch and the [image storage cleanup](#image-storage-cleanup) |
157
174
 
158
175
  The old **Color** tab is gone — the theme color is now a property of each rule — and the old **Background** tab became **Model Background**.
159
176
 
@@ -166,7 +183,7 @@ Data directory: `~/.dsh/.dsh-background-by-model-data/` (Windows: `C:\Users\<you
166
183
  | `theme-config.json` | The rule list (each rule with its image list, and each image with its own look and rotation settings) plus the global Interface settings, the global switch effect and the holiday overrides |
167
184
  | `theme-config.json.bak` | The previous config, as of the last **successful** save. Every save writes a temp file and renames it over the live one, rotating the current good file into `.bak` first; a live file that will not parse is restored from here instead of falling back to the factory defaults. With neither readable, the cleanup card refuses to delete images |
168
185
  | `modelbg-<slot>` | One image, stored as raw bytes without a file extension — one file per image, so a rule with three images owns three slots |
169
- | `holiday-cache/<tag>/<asset>` | Festival art downloaded from the CDN, one file per holiday (`mid-autumn.webp`, `national-day.webp`) inside a directory named after the assets release this build pins (`v0.1.0`). The tag is part of the path so that a build pinning a new tag starts from an empty directory instead of being answered with the previous generation's bytes; the file inside is named after the **asset**, deliberately outside the slot namespace, which is what keeps a holiday slot read-only. Superseded generations are pruned once the new art is on disk. Delete a file here and it is simply re-downloaded the next time that holiday is in force |
186
+ | `holiday-cache/<hash>/<asset>` | Festival art downloaded from Bilibili's image CDN, one file per holiday (`mid-autumn.webp`, `national-day.webp`) inside a directory named after the **first eight characters of that picture's content hash** (`42e22801`, `30e97211` today). The hash is part of the path so that new art lands in a directory nobody has written to — it cannot be answered with the previous generation's bytes, and there is no version constant to keep in step. The file inside is named after the **asset**, deliberately outside the slot namespace, which is what keeps a holiday slot read-only. Superseded generations (including every `holiday-cache/<tag>/` from 0.10 and earlier) are pruned once the new art is on disk. Delete a file here and it is simply re-downloaded the next time that holiday is in force |
170
187
 
171
188
  ## Installation
172
189
 
@@ -197,7 +214,7 @@ npx @deepseek-ai/dsh web
197
214
  git clone https://github.com/HarmlessFunny/dsh-background-by-model.git
198
215
  cd dsh-background-by-model
199
216
  pnpm install
200
- pnpm test # builds lib/, typechecks, runs the thirteen checks
217
+ pnpm test # builds lib/, typechecks, runs the fifteen checks
201
218
  ```
202
219
 
203
220
  `pnpm run bundle` rebuilds `lib/` on its own. Either has to run before the plugin can load: a fresh clone has no `lib/`.
@@ -219,7 +236,7 @@ To mount a working copy into a profile instead, add it to the profile's `package
219
236
 
220
237
  After changing anything under `src/`, run `pnpm run bundle` again — the mounted profile loads `lib/`, so edits do not take effect until the bundle is rebuilt.
221
238
 
222
- `pnpm test` builds, typechecks and runs thirteen checks, none of which needs a dependency, a transformer or a browser. Each can also be run on its own with `pnpm check:holiday` / `pnpm check:rotation` / `pnpm check:transition` / `pnpm check:repaint` / `pnpm check:code-plate` / `pnpm check:image-mode` / `pnpm check:rules` / `pnpm check:preset` / `pnpm check:ui` / `pnpm check:watchdogs` / `pnpm check:scheme` / `pnpm check:store` / `pnpm check:node`:
239
+ `pnpm test` builds, typechecks and runs fifteen checks, none of which needs a dependency, a transformer or a browser. Each can also be run on its own with `pnpm check:holiday` / `pnpm check:rotation` / `pnpm check:transition` / `pnpm check:repaint` / `pnpm check:code-plate` / `pnpm check:image-mode` / `pnpm check:rules` / `pnpm check:preset` / `pnpm check:ui` / `pnpm check:watchdogs` / `pnpm check:scheme` / `pnpm check:store` / `pnpm check:agent` / `pnpm check:rev` / `pnpm check:node`:
223
240
 
224
241
  - **`check:holiday`** — the two holiday windows, the Beijing day boundary, leap eighth months across 1900–2100, and the four gates `pickHoliday` applies.
225
242
  - **`check:rotation`** — `nextIndex` / `isRotating`: no walking off the end, shuffle never repeating the image on screen, and nothing called "rotating" with nothing to rotate.
@@ -228,12 +245,14 @@ After changing anything under `src/`, run `pnpm run bundle` again — the mounte
228
245
  - **`check:code-plate`** — the code block's plate: its tokens declared on both `:root` and `body`, the light branch emitting the banner, and each surface reading its own plugin variable.
229
246
  - **`check:image-mode`** — the lift onto the image for the layout mode and the three paint effects, and which image a control edits against which the render layer reads.
230
247
  - **`check:rules`** — the two decisions behind the tab strip, which live in `src/client/rules-view.ts`, plus the page structure the layout rests on.
231
- - **`check:preset`** — the recommended profile's retry policy and what a failed attempt holds for the next one, with both halves shown using them.
248
+ - **`check:preset`** — the profile itself (its rules, its slot → URL table and its generation constant) alongside the retry policy and what a failed attempt holds for the next one, with both halves shown using them. The rules are compiled in now, so this is also what catches a rule pointing at a slot the table does not carry — a mistake that would otherwise only show up after a release.
232
249
  - **`check:ui`** — both dictionaries carrying the same keys, every `dab-…` class styled, no dead entry, and every `var(--dsw-…)` published by the installed theme.
233
250
  - **`check:watchdogs`** — one heartbeat, entries firing on their own due time, paused while the tab is hidden, no timer left by dispose, and no module outside the entry starting its own interval.
234
- - **`check:scheme`** — the `data-ds-dark-theme` hand-off: the plugin only rewrites the value it wrote, hands the flag back when it drops its tokens, and repaints when the scheme moves.
251
+ - **`check:scheme`** — the `data-ds-dark-theme` hand-off: the plugin only rewrites the value it wrote, hands the flag back when it drops its tokens, and repaints when the scheme moves. It no longer reaches the network: the recommended profile's manifest it used to compare against is compiled in now.
235
252
  - **`check:store`** — the two decisions behind the storage cleanup, `referencedSlots` and `orphanSlots`: a picture in use is never an orphan, a stray holiday-slot file always is, and the browser half sends RULE slots rather than the whole config.
236
- - **`check:node`** — the built node half through its real RPC handler with `DSH_HOME` redirected and `fetch` stubbed: the holiday block, the festival art's caching and pruning, mirror fallback, the per-image color AND effect lifts, the global switch effect, the atomic config write with its backup and its recovery, the store's usage report and cleanup, and the recommended profile end to end.
253
+ - **`check:agent`** — agent access and the config revision: off by default, only an explicit `true` granting it (`'yes'` / `1` / `null` do not), the recommended profile not turning it on, an export not carrying it, the revision only ever moving forward, and a config written before either field reading as revision 0.
254
+ - **`check:rev`** — the panel's revision chain: a write carries `expectedRev`, a successful one records the host's answer, `setRev` mutates the live object, an import neither zeroes the revision nor reads it after the wipe, installing the recommended profile goes through one `adoptRecommended` call rather than a `resetStore` plus a write, config writes are chained while picture bytes are released only after the config lands (imports included), a tool-added picture derives a slot name from the clock, and two callers cannot adopt at once. Miss any link and the next save refuses itself, so an edit visibly undoes itself ("click + once, the card flashes and disappears").
255
+ - **`check:node`** — the built node half through its real RPC handler with `DSH_HOME` redirected and `fetch` stubbed: the holiday block, the festival art's content-hash cache and its pruning, fallback across the three edges, the per-image color AND effect lifts, the global switch effect, the atomic config write with its backup and its recovery, the store's usage report and cleanup, and the recommended profile's seven pictures end to end (including the old manifest method being gone, the slot whitelist, and the retry budget).
237
256
 
238
257
  ## FAQ
239
258
 
@@ -289,6 +308,25 @@ The effect is **Instant**, or the duration is `0` — an effect and a duration a
289
308
 
290
309
  > Only the two most recent releases are listed here; older ones live in [CHANGELOG.md](./CHANGELOG.md).
291
310
 
311
+ ### v0.12.0
312
+
313
+ - **The agent can change the appearance through two tools** — `background_theme` reads; `background_edit` writes (add/edit/remove rules and images, layout, opacity, brightness, blur, theme colour, the global interface opacities, the switch effect, rotation, and installing the recommended profile behind a `confirm`). Both call the same node-half operations the settings panel does.
314
+ - **The switch ships off** — a new `agentAccess` in the config. While it is off the tools are **absent from the model's tool table**, costing no context and impossible to call; the recommended profile does not turn it on — and installing it does not turn one OFF either.
315
+ - **An export never carries the grant** — a theme file describes an appearance; `agentAccess` and `rev` are stripped on the way out and the importing machine's own value wins on the way in.
316
+ - **The config carries a revision, and a stale write is refused** — the panel and the tools cannot silently overwrite each other; the loser is handed the config that won and adopts it. A successful write must record the new revision, or the next save refuses itself and the edit visibly undoes itself ("click + once, the card flashes and disappears"). Installing the recommended profile is one round trip, not "wipe then write".
317
+ - **The panel checks the revision every 3 s**, so an agent's edit repaints without a reload — a no-op while access is off.
318
+ - **`SCHEMA_VERSION` is 11**, with a new `check:agent` and the stale-write and access round trips added to `check:node`.
319
+
320
+ ### v0.11.0
321
+
322
+ - **The download source moves to Bilibili's image CDN** — the recommended profile's seven wallpapers and both holiday pictures now come from `i0`/`i1`/`i2.hdslb.com`, three edges falling back to each other; the three screenshots in this README stay where they are, since they play no part at runtime.
323
+ - **The recommended profile's rules ship inside the plugin** — seven model rules and their interface numbers are compiled into the bundle, and the button downloads only the seven pictures. It no longer needs GitHub to be reachable, and there is no "the remote profile is newer than this build" gate left, nor a `preset.json` manifest — what gets installed is always what this build itself writes.
324
+ - **The request set is a constant table** — slot → BFS path, fixed at build time, and a BFS path is a hash of the upload. So a wallpaper URL is immutable in a way a git tag is not.
325
+ - **The addresses carry `@3840w.webp`** — the uploads are 2.4–12 MB PNGs (76 MB for the set); with that suffix they are the 250–365 KB 4K WebPs whose sha1 matches what is already in a data directory.
326
+ - **The festival cache is keyed by content hash** — `holiday-cache/<tag>/` became `holiday-cache/<first eight of the hash>/`. New art means a new address, therefore a new directory and a guaranteed re-download; the "remember to bump the version too" trap is gone, and the old tag directories are taken by the same sweep once the new art lands.
327
+ - **`SCHEMA_VERSION` stays 10** — nothing entered or left the config shape: what changed is where those numbers come from, not what they look like.
328
+ - **Checks** — `check:preset` now pins the built-in profile itself (rule order, the slot ↔ URL table, the webp transform and content hash in each address, the cache generation); `check:node` rewrites the holiday and profile sections (three-edge fallback and the retry budget, the old `fetchPresetConfig` method being gone, the slot whitelist, tag directories being swept); section 3 of `check:scheme` no longer reaches the network.
329
+
292
330
  ### v0.10.0
293
331
 
294
332
  - **The config is written atomically, with a `.bak` of the last good file** — a save writes a temp file and renames it over the live one, keeping the previous parseable file as the backup; a corrupt live file is restored from it instead of falling back to the factory defaults and then being overwritten by your next drag.
@@ -298,11 +336,6 @@ The effect is **Instant**, or the duration is `0` — an effect and a duration a
298
336
  - **A switch between two different opacity, brightness or blur values animates now** — each layer keeps its own picture's filter, and the wallpaper's opacity rides the transition instead of snapping.
299
337
  - **Dragging one of the three sliders no longer repaints everything per frame** — a drag frame writes only that field's property, and one full repaint lands when the gesture ends.
300
338
 
301
- ### v0.9.2
302
-
303
- - **The Config tab gains an "Image storage" card** — it reports how much is cleanable and how many files that is, then offers the button; only unreferenced files go, the config and the festival art stay, and any file that resists is named.
304
- - **`check:store`, so there are thirteen checks now** — both READMEs had been claiming ten, having missed `check:watchdogs` and `check:scheme` since v0.8.1; they are listed again alongside the new one.
305
-
306
339
  ## Star History
307
340
  [![Star History Chart](https://api.star-history.com/chart?repos=HarmlessFunny/dsh-background-by-model&type=timeline&legend=bottom-right)](https://www.star-history.com/?repos=HarmlessFunny%2Fdsh-background-by-model&type=timeline&legend=bottom-right)
308
341
 
package/README.zh.md CHANGED
@@ -107,7 +107,7 @@
107
107
 
108
108
  - **中秋是农历八月十五**,按**北京时间**从中国农历解析,所以每年都动(2024-09-17、2025-10-06、2026-09-25 …);**国庆是 10 月 1–7 日**;两者撞上时中秋优先。
109
109
  - **没有图的节日会顺延**给模型规则,而不是把背景刷白。
110
- - **画是下载的,不随包发布** —— 每个节日一张压缩后的壁纸,钉在项目 [assets 仓库](https://github.com/HarmlessFunny/assets)的一个发行 tag 上,当天第一次读取时缓存到 `holiday-cache/<tag>/`。下载由日历把关,所以平常的日子一个字节都不取,此后的每一次启动都直接读盘。
110
+ - **画是下载的,不随包发布** —— 每个节日一张压缩后的壁纸,从**B 站图床**(`i0/i1/i2.hdslb.com`,三个边缘互为回落)取,当天第一次读取时缓存到 `holiday-cache/<哈希>/`。缓存目录名来自 URL 里那段**内容哈希**:换图就必须换地址,于是换目录、必然重下,没有需要记得去改的版本号。下载由日历把关,所以平常的日子一个字节都不取,此后的每一次启动都直接读盘。
111
111
  - **画和它的配色都不可以换** —— 节日槽位只读,颜色与满幅的「填充」都取自定义,主题导出也只带你的规则。
112
112
 
113
113
  ### 每条规则与每张图各自的设置
@@ -126,12 +126,29 @@
126
126
 
127
127
  ### 推荐配置
128
128
 
129
- 「配置」页点一下就下载作者本人的整套配置 —— 规则、壁纸与界面设置 —— 来自项目的 assets 仓库。
129
+ 「配置」页点一下就换上作者本人的整套配置 —— 七条模型规则与那套界面数值**随插件内置**,只有七张壁纸从**B 站图床**下载。
130
130
 
131
- - **托管在数据目录自己的形状里**(`preset/preset.json`、`preset/theme-config.json`,每张壁纸一个 `modelbg-<slot>` 文件),所以它能在 GitHub 上直接编辑,发布它就是一次 `cp`。
131
+ - **规则不再是远端文件** —— 它编译在包里,和读它的代码同一个版本。所以这个按钮不需要 GitHub 能通,也不存在「远端配置比插件新」这种要拦的情况:能被安装的规则,永远是这份构建自己写的那些。
132
+ - **壁纸地址是一张常量表** —— 槽位 → BFS 路径,编译期就定死了。请求清单因此不是从网上下载来的,一次按错也不可能变成「按别人的清单去取 64 个文件」。
133
+ - **一个地址对应一份内容** —— BFS 的路径是上传内容的哈希,所以壁纸地址天然不可变,不像 git tag 那样能被重新指向别的字节。
134
+ - **`@3840w.webp` 是必须的** —— 它们上传的原图是 2.4–12 MB 的 PNG(九张合计 76 MB),加上这个后缀才是 250–365 KB 的 4K WebP,也正是现有数据目录里已经存着的那份字节(sha1 一致)。
132
135
  - **里面每一个界面数字都是插件本来就用着的值**,所以在新装上是「加上规则和图片」;而在你已经调过的数据目录上,它把这些也一起重置 —— 它是一整份配置,不是一个补丁。
133
136
  - **先下载、后提问**,所以确认框能如实说出手里有什么 —— 而清空在下载**之后**,绝不在之前。它是面板里唯一一张会**先问再删**的卡片。
134
- - **少一张壁纸就整体失败并报出是哪个槽位**,而且每个文件各自重试,已经下到的部分留给下一次。
137
+ - **失败会如实说清到了哪一步** —— 下载阶段失败说「没有被改动」;清空**之后**才失败则明说「你原来的规则已被清空,再点一次可以重来」。清空不可逆,所以这两种处境不能共用一句安慰话。
138
+ - **少一张壁纸就整体失败并报出是哪个槽位**,而且每个文件各自重试(三个边缘 × 三轮),已经下到的部分留给下一次。
139
+
140
+ ### 让 AI 改背景(「配置」页,默认关)
141
+
142
+ 「配置」页最上面一张卡片:一个开关,决定**AI 能不能通过工具改这套外观**。
143
+
144
+ - **这是授权,不是偏好** —— 关着的时候那两个工具**根本不出现在模型的工具表里**,所以它不占上下文、也不可能被误调用;不是"工具还在,但一调就报错"。
145
+ - **默认关,推荐配置也不替你打开** —— 升级、新装、点「使用推荐配置」都不会让 AI 突然获得改你外观的能力,那只能由你在这一行开关上给出。反过来也一样:**它也不会替你关掉**——已经打开的授权,装一份外观不会撤销。
146
+ - **AI 能做** —— 增删改规则、给规则加图(按网址下载)、逐图改布局 / 透明度 / 亮度 / 模糊 / 主题色、改全局界面透明度与切换效果、改轮换设置,以及**换上推荐配置**。
147
+ - **「换上推荐配置」要单独确认** —— 它和面板上那个按钮做同一件事(下载七张图、清空存储、装上七条规则),所以工具要求调用方显式带 `confirm: true`,否则**在动任何东西之前**就拒绝。先下载、后清空:任何一张图取不到,你的规则一个字节都没动。
148
+ - **AI 不能做** —— 改节日画(那个槽位只读)、读图片字节(工具只回槽位名,不回几 MB 的 base64)。
149
+ - **改动即时生效** —— 面板每 3 秒核对一次磁盘上的配置版本号,AI 改完不用刷新页面。
150
+ - **导出不带这个开关** —— 把主题文件发给别人,不会顺手把"允许 AI 改外观"也送给对方;对方手动改过的文件里写着 `true` 也没用,导入时一律以**本机**的值说了算。
151
+ - **AI 和你同时改不会互相覆盖** —— 每次写入都带着"我基于哪个版本",落后的一方会被**拒绝**而不是覆盖,然后面板自动接盘最新的那份。
135
152
 
136
153
  ### 图片存储清理
137
154
 
@@ -153,7 +170,7 @@
153
170
  | --- | --- | --- |
154
171
  | **界面** | 全局,所有模型共用 | 主背景、左侧栏、右侧栏、卡片面板、输入框与控件、设置面板、对话文本框、轨迹页与每一个插件页的透明度 |
155
172
  | **模型背景** | 每条规则、每张图片 | 有序规则列表、实时匹配状态,以及每条规则的图片(胶片条)、**逐图布局模式 / 取景 / 主题色**,加上规则自己的透明度、亮度、模糊度与[多图轮换](#多图轮换) |
156
- | **配置** | — | 全局[切换效果](#切换效果全局)、整套规则的进出 —— [推荐配置](#推荐配置)与导入 / 导出(`dsh-background-by-model-theme.json`)—— 外加[节日特殊背景](#节日特殊背景)那一个开关,以及[图片存储清理](#图片存储清理) |
173
+ | **配置** | — | [允许 AI 修改背景](#让-ai-改背景配置页默认关)那一个开关、全局[切换效果](#切换效果全局)、整套规则的进出 —— [推荐配置](#推荐配置)与导入 / 导出(`dsh-background-by-model-theme.json`)—— 外加[节日特殊背景](#节日特殊背景)那一个开关,以及[图片存储清理](#图片存储清理) |
157
174
 
158
175
  原先的**「色彩」**选项卡已删除 —— 主题色改为规则与图片各自的属性 —— 原**「背景」**选项卡改造成**「模型背景」**。
159
176
 
@@ -166,7 +183,7 @@
166
183
  | `theme-config.json` | 规则列表(每条规则含图片列表,每张图含自己的外观与轮换设置)+ 全局「界面」设置 + 全局切换效果 + 节日覆盖配置 |
167
184
  | `theme-config.json.bak` | 上一份**写成功时**的配置。每次保存都是「先写临时文件、再改名覆盖」,并在覆盖前把当前这份合格的文件留成 `.bak`;主文件读不出来时就从这里恢复,而不是退回出厂默认。读不出且没有备份时,清理卡会拒绝删图 |
168
185
  | `modelbg-<slot>` | 一张图片,保存为原始字节、无扩展名 —— 一张图一个文件,所以一条有三张图的规则占三个槽位 |
169
- | `holiday-cache/<tag>/<asset>` | 从 CDN 下载的节日画,一个节日一个文件(`mid-autumn.webp`、`national-day.webp`),装在一个以**插件所钉的 assets 发行版**命名的目录里(`v0.1.0`)。tag 进了路径,所以钉了新 tag 的构建从一个空目录开始,而不是被告知上一代的字节就是答案;目录里的文件名仍然是**素材名**,刻意放在槽位命名空间之外,这正是节日槽位只读的原因。被取代的旧代会在新图落盘之后被清掉。删掉这里的文件也没关系,下次那个节日生效时会重新下载 |
186
+ | `holiday-cache/<哈希>/<asset>` | 从 B 站图床下载的节日画,一个节日一个文件(`mid-autumn.webp`、`national-day.webp`),装在一个以**该图内容哈希的前八位**命名的目录里(今天是 `42e22801`、`30e97211`)。哈希进了路径,所以换了图就是换了一个空目录,不可能被告知上一代的字节就是答案 —— 也没有需要记得去改的版本常量;目录里的文件名仍然是**素材名**,刻意放在槽位命名空间之外,这正是节日槽位只读的原因。被取代的旧代(包括 0.10 及更早的 `holiday-cache/<tag>/`)会在新图落盘之后被清掉。删掉这里的文件也没关系,下次那个节日生效时会重新下载 |
170
187
 
171
188
  ## 安装
172
189
 
@@ -219,7 +236,7 @@ pnpm test # 构建 lib/、类型检查、跑十三道检查
219
236
 
220
237
  改完 `src/` 必须重新执行 `pnpm run bundle` 才会生效 —— 挂载的 profile 加载的是 `lib/`。
221
238
 
222
- `pnpm test` 会构建、类型检查并跑十三道校验,十三道都不需要依赖、不需要转译器、也不需要浏览器。也可以单独跑:`pnpm check:holiday` / `pnpm check:rotation` / `pnpm check:transition` / `pnpm check:repaint` / `pnpm check:code-plate` / `pnpm check:image-mode` / `pnpm check:rules` / `pnpm check:preset` / `pnpm check:ui` / `pnpm check:watchdogs` / `pnpm check:scheme` / `pnpm check:store` / `pnpm check:node`:
239
+ `pnpm test` 会构建、类型检查并跑十五道校验,十五道都不需要依赖、不需要转译器、也不需要浏览器。也可以单独跑:`pnpm check:holiday` / `pnpm check:rotation` / `pnpm check:transition` / `pnpm check:repaint` / `pnpm check:code-plate` / `pnpm check:image-mode` / `pnpm check:rules` / `pnpm check:preset` / `pnpm check:ui` / `pnpm check:watchdogs` / `pnpm check:scheme` / `pnpm check:store` / `pnpm check:agent` / `pnpm check:rev` / `pnpm check:node`:
223
240
 
224
241
  - **`check:holiday`** —— 两个节日窗口、北京时间的跨日边界、1900–2100 年间的闰八月,以及 `pickHoliday` 的四道闸门。
225
242
  - **`check:rotation`** —— `nextIndex` / `isRotating`:不走越界、随机不重复当前这张、没有可轮换的东西时绝不报「正在轮换」。
@@ -228,12 +245,14 @@ pnpm test # 构建 lib/、类型检查、跑十三道检查
228
245
  - **`check:code-plate`** —— 代码块底板:token 在 `:root` 与 `body` 上都有声明、浅色分支发出 banner、两块表面各读自己的插件变量。
229
246
  - **`check:image-mode`** —— 布局模式与三个壁纸效果放到图片上的那次抬升,以及某个控件编辑的是哪张图、渲染层读的又是哪张图。
230
247
  - **`check:rules`** —— 选项卡背后那两个住在 `src/client/rules-view.ts` 的判定,以及布局所依赖的页面结构。
231
- - **`check:preset`** —— 推荐配置的重试策略与失败那一次给下一次留下什么,并验证两侧真的在用它们。
248
+ - **`check:preset`** —— 推荐配置本身(规则、槽位 ↔ 地址表、那一代常量)与重试策略,以及失败那一次给下一次留下什么,并验证两侧真的在用它们。规则现在编译在包里,所以这里也负责拦住「某条规则指向一个表里没有的槽位」这种只有发布后才会发现的错。
232
249
  - **`check:ui`** —— 两本字典键集一致、每个 `dab-…` 类都有样式、没有死键,以及每个 `var(--dsw-…)` 都是已安装主题发布过的 token。
233
250
  - **`check:watchdogs`** —— 一条心跳、按各自到期时间触发、隐藏即暂停、dispose 不留定时器,且入口之外没有模块自己起 interval。
234
251
  - **`check:scheme`** —— `data-ds-dark-theme` 的交接:插件只改自己写的那个值、清掉 token 时一起交还标记、方案变了会重绘。
235
252
  - **`check:store`** —— 存储清理的两个判定:`referencedSlots` 与 `orphanSlots`。钉住「正在用的图不会被当成孤儿」与「节日槽位的文件必须可清」,外加浏览器半边传的是**规则槽位**而不是整份配置。
236
- - **`check:node`** —— 把构建好的 node 半边通过真实的 RPC 处理器跑一遍(`DSH_HOME` 指向临时目录、`fetch` 换成桩):节日配置块、节日画的缓存与清理、镜像回落、逐图颜色与逐图效果的抬升、全局切换效果、配置的原子写与备份/恢复、图片存储的用量报告与清理,以及推荐配置的整条链路。
253
+ - **`check:agent`** —— AI 授权与配置版本号:默认关闭、只有显式 `true` 才授予(`'yes'` / `1` / `null` 都不行)、推荐配置不替你打开、导出不带授权、版本号只增不减、以及旧配置读成 0。
254
+ - **`check:rev`** —— 面板的版本号链:写入必须带 `expectedRev`、成功后必须记下宿主的回答、`setRev` 改写活对象、导入不得把版本号清零且要在清空之前读,推荐配置必须走一次 `adoptRecommended` 而不是自己 `resetStore` 再写,且配置写入必须串成一条链、图片字节要等配置落盘之后再删(导入主题也一样)、AI 加的图片用时钟派生的槽位名、两条路径不能同时采用推荐配置。少了任何一环,下一次保存就会判自己陈旧,改动在画面上自己撤销("点一次 + 卡片闪一下后消失")。
255
+ - **`check:node`** —— 把构建好的 node 半边通过真实的 RPC 处理器跑一遍(`DSH_HOME` 指向临时目录、`fetch` 换成桩):节日配置块、节日画按内容哈希的缓存与清理、三个边缘的回落、逐图颜色与逐图效果的抬升、全局切换效果、配置的原子写与备份/恢复、图片存储的用量报告与清理,以及推荐配置的七张图(含旧清单方法的消失、槽位白名单、超时与重试预算)。
237
256
 
238
257
  ## 常见问题
239
258
 
@@ -289,6 +308,25 @@ pnpm test # 构建 lib/、类型检查、跑十三道检查
289
308
 
290
309
  > 这里只列最近两个版本;更早的版本见 [CHANGELOG.zh.md](./CHANGELOG.zh.md)。
291
310
 
311
+ ### v0.12.0
312
+
313
+ - **AI 可以通过两个工具改这套外观** —— `background_theme` 读,`background_edit` 写(增删改规则与图片、布局、透明度、亮度、模糊、主题色、全局界面透明度、切换效果、轮换,以及带 `confirm` 才执行的「换上推荐配置」)。它们调的是面板用的同一批 node 半边操作。
314
+ - **授权开关默认关** —— 配置里新增 `agentAccess`。关着时工具**根本不在模型的工具表里**,不占上下文也不会被误调;推荐配置也不替你打开。
315
+ - **导出不带授权** —— 主题文件只描述外观,`agentAccess` 与 `rev` 在导出时被剥掉,导入时以本机的值为准。
316
+ - **配置带版本号,落后写入被拒绝** —— 面板与工具不能互相静默覆盖;被拒的一方拿到赢家的配置并接盘重绘。写入成功后必须记下新版本号,否则下一次保存会判自己陈旧、改动在画面上自己撤销("点一次 + 卡片闪一下后消失")。推荐配置是一个操作的往返,不是"清空再写"两步。
317
+ - **面板 3 秒核对一次版本号**,AI 改完不用刷新;授权关闭时是空操作。
318
+ - **`SCHEMA_VERSION` 升到 11**,新增 `check:agent`,`check:node` 补上陈旧写入与授权往返。
319
+
320
+ ### v0.11.0
321
+
322
+ - **下载源换成 B 站图床** —— 推荐配置的七张壁纸与中秋、国庆两张节日画,全部改从 `i0/i1/i2.hdslb.com` 取,三个边缘互为回落;README 里那三张截图仍留在原处,它们不参与运行时。
323
+ - **推荐配置的规则改成随插件内置** —— 七条模型规则与那套界面数值编译进包里,按钮只下载七张图。于是它不再需要 GitHub 能通,也不再有「远端配置比插件新」这道闸和 `preset.json` 清单 —— 能被安装的规则永远就是这份构建自己写的那些。
324
+ - **请求清单变成一张常量表** —— 槽位 → BFS 路径编译期定死,而 BFS 路径是上传内容的哈希,所以壁纸地址天然不可变,不像 git tag 那样能被重新指向别的字节。
325
+ - **地址带 `@3840w.webp`** —— 上传的原图是 2.4–12 MB 的 PNG(九张合计 76 MB),加上这个后缀才是 250–365 KB 的 4K WebP,与现有数据目录里那几份字节的 sha1 完全一致。
326
+ - **节日画的缓存目录按内容哈希分代** —— 从 `holiday-cache/<tag>/` 改成 `holiday-cache/<哈希前八位>/`。换图就换地址、换目录、必然重下,那个「记得顺手把版本号也改了」的坑从此不存在;旧的 tag 目录会在新图落盘后由同一次清扫带走。
327
+ - **`SCHEMA_VERSION` 仍是 10** —— 这次没有字段进出配置形状:变的是这些数值从哪来,不是它们长什么样。
328
+ - **门禁** —— `check:preset` 改钉内置配置本身(规则顺序、槽位 ↔ 地址表、地址里的 webp 变换与内容哈希、缓存分代),`check:node` 重写节日与推荐配置两节(三个边缘的回落与重试预算、旧 `fetchPresetConfig` 方法已不存在、槽位白名单、tag 目录被清扫),`check:scheme` 第 3 节不再联网。
329
+
292
330
  ### v0.10.0
293
331
 
294
332
  - **配置改成原子写,并留一份 `.bak`** —— 保存先写临时文件再改名覆盖,覆盖前把当前能解析的那份留成备份;主文件坏了就从备份恢复,而不是退回出厂默认、再被你下一次拖动覆盖掉。
@@ -298,11 +336,6 @@ pnpm test # 构建 lib/、类型检查、跑十三道检查
298
336
  - **透明度、亮度或模糊不同的两张图之间切换现在有动画了** —— 每个图层记住自己那张图的滤镜,壁纸透明度也跟着过渡走,不再直接切。
299
337
  - **拖动这三个滑块不再每帧整块重绘** —— 拖动只写那个字段自己的属性,松手才走一次完整重绘。
300
338
 
301
- ### v0.9.2
302
-
303
- - **「配置」页多了一张「图片存储」卡片** —— 先报出可清理多少、涉及几个文件,再给你一个清理按钮;只删没有规则引用的文件,配置与节日画都不动,删不掉的文件会点名。
304
- - **`check:store`,于是现在十三道校验** —— 两份 README 此前一直写着「十道」,`check:watchdogs` 与 `check:scheme` 从 v0.8.1 起就没列进去,这次一并补上。
305
-
306
339
  ## Star History
307
340
 
308
341
  [![Star History Chart](https://api.star-history.com/chart?repos=HarmlessFunny/dsh-background-by-model&type=timeline&legend=bottom-right)](https://www.star-history.com/?repos=HarmlessFunny%2Fdsh-background-by-model&type=timeline&legend=bottom-right)
package/cordis.patch.yml CHANGED
@@ -1,6 +1,6 @@
1
- # Deep Midnight theme plugin: bundle patch layer.
1
+ # dsh-background-by-model: bundle patch layer.
2
2
  # When this package is installed into a profile via `dsh plugin add`,
3
- # this patch is applied as a layer, inserting the theme plugin row
3
+ # this patch is applied as a layer, inserting the plugin row
4
4
  # into the composed configuration.
5
5
 
6
6
  - insert: