dsh-background-by-model 0.10.0 → 0.11.0
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 +14 -1
- package/CHANGELOG.zh.md +14 -1
- package/README.md +21 -13
- package/README.zh.md +20 -12
- package/lib/client.js +181 -95
- package/lib/index.js +203 -196
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
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.
|
|
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.11.0 / v0.10.0).
|
|
4
|
+
|
|
5
|
+
## v0.11.0
|
|
6
|
+
|
|
7
|
+
- **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.
|
|
8
|
+
- **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.
|
|
9
|
+
- **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.
|
|
10
|
+
- **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".
|
|
11
|
+
- **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.
|
|
12
|
+
- **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".
|
|
13
|
+
- **`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.
|
|
14
|
+
- **`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.
|
|
15
|
+
- **`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.
|
|
16
|
+
- **`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
17
|
|
|
5
18
|
## v0.10.0
|
|
6
19
|
|
package/CHANGELOG.zh.md
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
# 更新日志
|
|
2
2
|
|
|
3
|
-
> 这里保留完整的历史记录;[README.zh.md](./README.zh.md#近期优化) 的「近期优化」一节只列最近两个版本(v0.
|
|
3
|
+
> 这里保留完整的历史记录;[README.zh.md](./README.zh.md#近期优化) 的「近期优化」一节只列最近两个版本(v0.11.0 / v0.10.0)。
|
|
4
|
+
|
|
5
|
+
## v0.11.0
|
|
6
|
+
|
|
7
|
+
- **运行时下载源换成 B 站图床** —— 推荐配置的七张壁纸与中秋、国庆两张节日画全部改从 `i0/i1/i2.hdslb.com` 取,三个边缘互为回落(实测同样路径、同样字节,sha1 一致)。README 的三张截图不参与运行时,仍留在原来的地方。
|
|
8
|
+
- **推荐配置的规则改成随插件内置** —— 七条模型规则与那套界面数值搬进 `src/profile.ts`,按钮只下载七张图。于是它不再依赖 GitHub 能通,`preset.json` 清单、schema 版本闸与「不是配置」这类错误码一起退休:能被安装的规则永远就是这份构建自己写的那些,远端没有任何东西在选择装什么。
|
|
9
|
+
- **请求清单变成编译期常量** —— 槽位 → BFS 路径写死在一张表里,而 `fetchPresetImage` 只认这张表:表里没有的槽位直接回 `bad slot`,所以一次按错不可能变成「按别人给的清单去取 64 个文件」。`PRESET_IMAGE_COUNT_MAX` 与 65 张图那条攻击面随之消失。
|
|
10
|
+
- **BFS 路径是内容哈希,地址天然不可变** —— 不再有「tag 被重新指向别的字节」这种可能;顺带把「换素材要记得同时改 `*_ASSETS_REF`」这个坑一起消掉了。
|
|
11
|
+
- **地址必须带 `@3840w.webp`** —— 上传的原图是 2.4–12 MB 的 PNG(九张合计 76 MB),这样取才是 250–365 KB 的 4K WebP,并且与现有数据目录里那几份的 sha1 逐字节一致,所以 4K 屏上不掉画质、老用户也不需要重下。
|
|
12
|
+
- **节日画的缓存目录按内容哈希分代** —— `holiday-cache/<tag>/` 变成 `holiday-cache/<哈希前八位>/`,由 `holidayArtGeneration` 从 URL 现算。换图 = 换地址 = 换目录 = 必然重下,没有需要记得去改的版本常量;旧的 tag 目录(以及 0.7.9 的平铺布局)在新图落盘后由同一次 `pruneHolidayCache` 带走,清扫时的保留集是「每个节日当前那一代」的去重集合。
|
|
13
|
+
- **`SCHEMA_VERSION` 保持 10** —— 这次没有字段进出配置形状:变的是这些数值从哪来,不是它们长什么样,所以旧宿主、旧导出文件都照常读。
|
|
14
|
+
- **`check:preset` 改成钉内置配置本身** —— 规则顺序与可画性、槽位 ↔ 地址表的双向一致、每个地址的 webp 变换与内容哈希、`profileImagePath` 的 null 白名单、缓存分代不重复,以及旧的远端接线已经不在。
|
|
15
|
+
- **`check:node` 重写节日与推荐配置两节** —— 三个边缘的依次回落与「同一条路径」、全部边缘失败才失败、「一次坏天气值得再来一轮」的重试预算(3 边缘 × 3 轮 = 9 个请求封顶)、旧 `fetchPresetConfig` 方法确实已不存在、槽位白名单(节假日槽位、表外槽位、非法名都在下载前被拒)、`holiday-cache/v0.1.0` 这类旧目录会被清扫,以及 `resetStore` 仍然不动节日画。
|
|
16
|
+
- **`check:scheme` 第 3 节不再联网** —— 它原本要拉远端 `preset.json` 比 schema,现在清单不存在了,改为钉住「客户端不再请求清单」「槽位清单来自内置表」「地址带 webp 变换且指向 B 站」。
|
|
4
17
|
|
|
5
18
|
## v0.10.0
|
|
6
19
|
|
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,
|
|
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,15 @@ 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
|
|
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
|
-
- **
|
|
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
|
+
- **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.
|
|
135
138
|
|
|
136
139
|
### Image storage cleanup
|
|
137
140
|
|
|
@@ -166,7 +169,7 @@ Data directory: `~/.dsh/.dsh-background-by-model-data/` (Windows: `C:\Users\<you
|
|
|
166
169
|
| `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
170
|
| `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
171
|
| `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/<
|
|
172
|
+
| `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
173
|
|
|
171
174
|
## Installation
|
|
172
175
|
|
|
@@ -228,12 +231,12 @@ After changing anything under `src/`, run `pnpm run bundle` again — the mounte
|
|
|
228
231
|
- **`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
232
|
- **`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
233
|
- **`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
|
|
234
|
+
- **`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
235
|
- **`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
236
|
- **`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.
|
|
237
|
+
- **`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
238
|
- **`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
|
|
239
|
+
- **`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
240
|
|
|
238
241
|
## FAQ
|
|
239
242
|
|
|
@@ -289,6 +292,16 @@ The effect is **Instant**, or the duration is `0` — an effect and a duration a
|
|
|
289
292
|
|
|
290
293
|
> Only the two most recent releases are listed here; older ones live in [CHANGELOG.md](./CHANGELOG.md).
|
|
291
294
|
|
|
295
|
+
### v0.11.0
|
|
296
|
+
|
|
297
|
+
- **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.
|
|
298
|
+
- **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.
|
|
299
|
+
- **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.
|
|
300
|
+
- **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.
|
|
301
|
+
- **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.
|
|
302
|
+
- **`SCHEMA_VERSION` stays 10** — nothing entered or left the config shape: what changed is where those numbers come from, not what they look like.
|
|
303
|
+
- **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.
|
|
304
|
+
|
|
292
305
|
### v0.10.0
|
|
293
306
|
|
|
294
307
|
- **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 +311,6 @@ The effect is **Instant**, or the duration is `0` — an effect and a duration a
|
|
|
298
311
|
- **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
312
|
- **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
313
|
|
|
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
314
|
## Star History
|
|
307
315
|
[](https://www.star-history.com/?repos=HarmlessFunny%2Fdsh-background-by-model&type=timeline&legend=bottom-right)
|
|
308
316
|
|
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
|
-
- **画是下载的,不随包发布** ——
|
|
110
|
+
- **画是下载的,不随包发布** —— 每个节日一张压缩后的壁纸,从**B 站图床**(`i0/i1/i2.hdslb.com`,三个边缘互为回落)取,当天第一次读取时缓存到 `holiday-cache/<哈希>/`。缓存目录名来自 URL 里那段**内容哈希**:换图就必须换地址,于是换目录、必然重下,没有需要记得去改的版本号。下载由日历把关,所以平常的日子一个字节都不取,此后的每一次启动都直接读盘。
|
|
111
111
|
- **画和它的配色都不可以换** —— 节日槽位只读,颜色与满幅的「填充」都取自定义,主题导出也只带你的规则。
|
|
112
112
|
|
|
113
113
|
### 每条规则与每张图各自的设置
|
|
@@ -126,12 +126,15 @@
|
|
|
126
126
|
|
|
127
127
|
### 推荐配置
|
|
128
128
|
|
|
129
|
-
|
|
129
|
+
「配置」页点一下就换上作者本人的整套配置 —— 七条模型规则与那套界面数值**随插件内置**,只有七张壁纸从**B 站图床**下载。
|
|
130
130
|
|
|
131
|
-
-
|
|
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
|
+
- **少一张壁纸就整体失败并报出是哪个槽位**,而且每个文件各自重试(三个边缘 × 三轮),已经下到的部分留给下一次。
|
|
135
138
|
|
|
136
139
|
### 图片存储清理
|
|
137
140
|
|
|
@@ -166,7 +169,7 @@
|
|
|
166
169
|
| `theme-config.json` | 规则列表(每条规则含图片列表,每张图含自己的外观与轮换设置)+ 全局「界面」设置 + 全局切换效果 + 节日覆盖配置 |
|
|
167
170
|
| `theme-config.json.bak` | 上一份**写成功时**的配置。每次保存都是「先写临时文件、再改名覆盖」,并在覆盖前把当前这份合格的文件留成 `.bak`;主文件读不出来时就从这里恢复,而不是退回出厂默认。读不出且没有备份时,清理卡会拒绝删图 |
|
|
168
171
|
| `modelbg-<slot>` | 一张图片,保存为原始字节、无扩展名 —— 一张图一个文件,所以一条有三张图的规则占三个槽位 |
|
|
169
|
-
| `holiday-cache
|
|
172
|
+
| `holiday-cache/<哈希>/<asset>` | 从 B 站图床下载的节日画,一个节日一个文件(`mid-autumn.webp`、`national-day.webp`),装在一个以**该图内容哈希的前八位**命名的目录里(今天是 `42e22801`、`30e97211`)。哈希进了路径,所以换了图就是换了一个空目录,不可能被告知上一代的字节就是答案 —— 也没有需要记得去改的版本常量;目录里的文件名仍然是**素材名**,刻意放在槽位命名空间之外,这正是节日槽位只读的原因。被取代的旧代(包括 0.10 及更早的 `holiday-cache/<tag>/`)会在新图落盘之后被清掉。删掉这里的文件也没关系,下次那个节日生效时会重新下载 |
|
|
170
173
|
|
|
171
174
|
## 安装
|
|
172
175
|
|
|
@@ -228,12 +231,12 @@ pnpm test # 构建 lib/、类型检查、跑十三道检查
|
|
|
228
231
|
- **`check:code-plate`** —— 代码块底板:token 在 `:root` 与 `body` 上都有声明、浅色分支发出 banner、两块表面各读自己的插件变量。
|
|
229
232
|
- **`check:image-mode`** —— 布局模式与三个壁纸效果放到图片上的那次抬升,以及某个控件编辑的是哪张图、渲染层读的又是哪张图。
|
|
230
233
|
- **`check:rules`** —— 选项卡背后那两个住在 `src/client/rules-view.ts` 的判定,以及布局所依赖的页面结构。
|
|
231
|
-
- **`check:preset`** ——
|
|
234
|
+
- **`check:preset`** —— 推荐配置本身(规则、槽位 ↔ 地址表、那一代常量)与重试策略,以及失败那一次给下一次留下什么,并验证两侧真的在用它们。规则现在编译在包里,所以这里也负责拦住「某条规则指向一个表里没有的槽位」这种只有发布后才会发现的错。
|
|
232
235
|
- **`check:ui`** —— 两本字典键集一致、每个 `dab-…` 类都有样式、没有死键,以及每个 `var(--dsw-…)` 都是已安装主题发布过的 token。
|
|
233
236
|
- **`check:watchdogs`** —— 一条心跳、按各自到期时间触发、隐藏即暂停、dispose 不留定时器,且入口之外没有模块自己起 interval。
|
|
234
237
|
- **`check:scheme`** —— `data-ds-dark-theme` 的交接:插件只改自己写的那个值、清掉 token 时一起交还标记、方案变了会重绘。
|
|
235
238
|
- **`check:store`** —— 存储清理的两个判定:`referencedSlots` 与 `orphanSlots`。钉住「正在用的图不会被当成孤儿」与「节日槽位的文件必须可清」,外加浏览器半边传的是**规则槽位**而不是整份配置。
|
|
236
|
-
- **`check:node`** —— 把构建好的 node 半边通过真实的 RPC 处理器跑一遍(`DSH_HOME` 指向临时目录、`fetch`
|
|
239
|
+
- **`check:node`** —— 把构建好的 node 半边通过真实的 RPC 处理器跑一遍(`DSH_HOME` 指向临时目录、`fetch` 换成桩):节日配置块、节日画按内容哈希的缓存与清理、三个边缘的回落、逐图颜色与逐图效果的抬升、全局切换效果、配置的原子写与备份/恢复、图片存储的用量报告与清理,以及推荐配置的七张图(含旧清单方法的消失、槽位白名单、超时与重试预算)。
|
|
237
240
|
|
|
238
241
|
## 常见问题
|
|
239
242
|
|
|
@@ -289,6 +292,16 @@ pnpm test # 构建 lib/、类型检查、跑十三道检查
|
|
|
289
292
|
|
|
290
293
|
> 这里只列最近两个版本;更早的版本见 [CHANGELOG.zh.md](./CHANGELOG.zh.md)。
|
|
291
294
|
|
|
295
|
+
### v0.11.0
|
|
296
|
+
|
|
297
|
+
- **下载源换成 B 站图床** —— 推荐配置的七张壁纸与中秋、国庆两张节日画,全部改从 `i0/i1/i2.hdslb.com` 取,三个边缘互为回落;README 里那三张截图仍留在原处,它们不参与运行时。
|
|
298
|
+
- **推荐配置的规则改成随插件内置** —— 七条模型规则与那套界面数值编译进包里,按钮只下载七张图。于是它不再需要 GitHub 能通,也不再有「远端配置比插件新」这道闸和 `preset.json` 清单 —— 能被安装的规则永远就是这份构建自己写的那些。
|
|
299
|
+
- **请求清单变成一张常量表** —— 槽位 → BFS 路径编译期定死,而 BFS 路径是上传内容的哈希,所以壁纸地址天然不可变,不像 git tag 那样能被重新指向别的字节。
|
|
300
|
+
- **地址带 `@3840w.webp`** —— 上传的原图是 2.4–12 MB 的 PNG(九张合计 76 MB),加上这个后缀才是 250–365 KB 的 4K WebP,与现有数据目录里那几份字节的 sha1 完全一致。
|
|
301
|
+
- **节日画的缓存目录按内容哈希分代** —— 从 `holiday-cache/<tag>/` 改成 `holiday-cache/<哈希前八位>/`。换图就换地址、换目录、必然重下,那个「记得顺手把版本号也改了」的坑从此不存在;旧的 tag 目录会在新图落盘后由同一次清扫带走。
|
|
302
|
+
- **`SCHEMA_VERSION` 仍是 10** —— 这次没有字段进出配置形状:变的是这些数值从哪来,不是它们长什么样。
|
|
303
|
+
- **门禁** —— `check:preset` 改钉内置配置本身(规则顺序、槽位 ↔ 地址表、地址里的 webp 变换与内容哈希、缓存分代),`check:node` 重写节日与推荐配置两节(三个边缘的回落与重试预算、旧 `fetchPresetConfig` 方法已不存在、槽位白名单、tag 目录被清扫),`check:scheme` 第 3 节不再联网。
|
|
304
|
+
|
|
292
305
|
### v0.10.0
|
|
293
306
|
|
|
294
307
|
- **配置改成原子写,并留一份 `.bak`** —— 保存先写临时文件再改名覆盖,覆盖前把当前能解析的那份留成备份;主文件坏了就从备份恢复,而不是退回出厂默认、再被你下一次拖动覆盖掉。
|
|
@@ -298,11 +311,6 @@ pnpm test # 构建 lib/、类型检查、跑十三道检查
|
|
|
298
311
|
- **透明度、亮度或模糊不同的两张图之间切换现在有动画了** —— 每个图层记住自己那张图的滤镜,壁纸透明度也跟着过渡走,不再直接切。
|
|
299
312
|
- **拖动这三个滑块不再每帧整块重绘** —— 拖动只写那个字段自己的属性,松手才走一次完整重绘。
|
|
300
313
|
|
|
301
|
-
### v0.9.2
|
|
302
|
-
|
|
303
|
-
- **「配置」页多了一张「图片存储」卡片** —— 先报出可清理多少、涉及几个文件,再给你一个清理按钮;只删没有规则引用的文件,配置与节日画都不动,删不掉的文件会点名。
|
|
304
|
-
- **`check:store`,于是现在十三道校验** —— 两份 README 此前一直写着「十道」,`check:watchdogs` 与 `check:scheme` 从 v0.8.1 起就没列进去,这次一并补上。
|
|
305
|
-
|
|
306
314
|
## Star History
|
|
307
315
|
|
|
308
316
|
[](https://www.star-history.com/?repos=HarmlessFunny%2Fdsh-background-by-model&type=timeline&legend=bottom-right)
|
package/lib/client.js
CHANGED
|
@@ -103,12 +103,14 @@ window.__ModuleLoader__.load({
|
|
|
103
103
|
id: "mid-autumn",
|
|
104
104
|
slot: "h-midautumn",
|
|
105
105
|
asset: "mid-autumn.webp",
|
|
106
|
+
source: "/bfs/new_dyn/42e22801827aff71267b03c95fcfd7834168597.png@1672w.webp",
|
|
106
107
|
color: "#384A77",
|
|
107
108
|
kind: "lunar"
|
|
108
109
|
}, {
|
|
109
110
|
id: "national-day",
|
|
110
111
|
slot: "h-nationalday",
|
|
111
112
|
asset: "national-day.webp",
|
|
113
|
+
source: "/bfs/new_dyn/30e97211189f7869dae83b909d2a3cc84168597.png@3456w.webp",
|
|
112
114
|
color: "#FFF6EB",
|
|
113
115
|
kind: "gregorian"
|
|
114
116
|
}];
|
|
@@ -1167,17 +1169,13 @@ window.__ModuleLoader__.load({
|
|
|
1167
1169
|
};
|
|
1168
1170
|
}
|
|
1169
1171
|
/**
|
|
1170
|
-
*
|
|
1172
|
+
* One of the profile's wallpapers, addressed by the slot the built-in config names.
|
|
1171
1173
|
*
|
|
1172
1174
|
* Raw rather than `rpcCall` for the same reason `fetchImageUrl` is: the node half
|
|
1173
|
-
* answers this one with a value that CARRIES its own failure, and the `error`
|
|
1174
|
-
*
|
|
1175
|
-
*
|
|
1175
|
+
* answers this one with a value that CARRIES its own failure, and the `error` inside
|
|
1176
|
+
* it is the only thing worth showing the user — the generic "rpc failed" log would
|
|
1177
|
+
* throw that distinction away.
|
|
1176
1178
|
*/
|
|
1177
|
-
async function fetchPresetConfig() {
|
|
1178
|
-
return unwrapPreset(await rpcRaw("fetchPresetConfig", {}));
|
|
1179
|
-
}
|
|
1180
|
-
/** One of the profile's wallpapers, addressed by the slot the config names. */
|
|
1181
1179
|
async function fetchPresetImage(slot) {
|
|
1182
1180
|
return unwrapPreset(await rpcRaw("fetchPresetImage", { slot }));
|
|
1183
1181
|
}
|
|
@@ -4970,7 +4968,7 @@ window.__ModuleLoader__.load({
|
|
|
4970
4968
|
importDone: "已导入配置",
|
|
4971
4969
|
importFail: "导入失败,文件格式不正确",
|
|
4972
4970
|
recCardTitle: "推荐配置",
|
|
4973
|
-
recCardDesc: "
|
|
4971
|
+
recCardDesc: "换上作者维护的一整套配置(规则与界面数值随插件内置,壁纸从图床下载),一键替换当前的全部内容",
|
|
4974
4972
|
recAdopt: "采用推荐配置",
|
|
4975
4973
|
recBusy: "正在下载",
|
|
4976
4974
|
recConfirmTitle: "替换当前的全部配置?",
|
|
@@ -4980,12 +4978,9 @@ window.__ModuleLoader__.load({
|
|
|
4980
4978
|
recConfirmOk: "删除并替换",
|
|
4981
4979
|
recConfirmCancel: "取消",
|
|
4982
4980
|
recDone: "已采用推荐配置",
|
|
4983
|
-
recFailNetwork: "
|
|
4984
|
-
recFailNotProfile: "素材库返回的内容不是配置,请稍后重试",
|
|
4985
|
-
recFailNewer: "这份推荐配置需要更新的插件版本,未做任何改动",
|
|
4986
|
-
recFailTooMany: "这份推荐配置列出的图片数量超出限制,未做任何改动",
|
|
4981
|
+
recFailNetwork: "壁纸下载失败,请检查网络后重试(已下到的会留到下一次)",
|
|
4987
4982
|
recFailTooLarge: "这份推荐配置体积过大,未做任何改动",
|
|
4988
|
-
recFailImage: "
|
|
4983
|
+
recFailImage: "这张壁纸没拿到(图床上这个地址没有回应):",
|
|
4989
4984
|
recFailApply: "未能写入推荐配置,你原来的配置没有被改动",
|
|
4990
4985
|
storeCardTitle: "图片存储",
|
|
4991
4986
|
storeCardDesc: "因意外导致的孤儿图片这里可以清掉",
|
|
@@ -5154,7 +5149,7 @@ window.__ModuleLoader__.load({
|
|
|
5154
5149
|
importDone: "Profile imported",
|
|
5155
5150
|
importFail: "Import failed — invalid file",
|
|
5156
5151
|
recCardTitle: "Recommended profile",
|
|
5157
|
-
recCardDesc: "
|
|
5152
|
+
recCardDesc: "Install the author's own full setup — rules and interface numbers ship with the plugin, wallpapers are downloaded from the image host — replacing everything you have",
|
|
5158
5153
|
recAdopt: "Use recommended profile",
|
|
5159
5154
|
recBusy: "Downloading",
|
|
5160
5155
|
recConfirmTitle: "Replace your entire profile?",
|
|
@@ -5164,12 +5159,9 @@ window.__ModuleLoader__.load({
|
|
|
5164
5159
|
recConfirmOk: "Delete and replace",
|
|
5165
5160
|
recConfirmCancel: "Cancel",
|
|
5166
5161
|
recDone: "Recommended profile applied",
|
|
5167
|
-
recFailNetwork: "Could not download the
|
|
5168
|
-
recFailNotProfile: "The asset host answered with something that is not a profile — try again later",
|
|
5169
|
-
recFailNewer: "This profile needs a newer plugin version. Nothing was changed.",
|
|
5170
|
-
recFailTooMany: "The recommended profile names more images than the plugin will fetch. Nothing was changed.",
|
|
5162
|
+
recFailNetwork: "Could not download the wallpapers — check your connection and retry (what arrived is kept for the next try)",
|
|
5171
5163
|
recFailTooLarge: "The recommended profile is too large. Nothing was changed.",
|
|
5172
|
-
recFailImage: "Could not fetch this wallpaper — the
|
|
5164
|
+
recFailImage: "Could not fetch this wallpaper — the image host does not answer for that address:",
|
|
5173
5165
|
recFailApply: "Could not write the recommended profile — your own is untouched",
|
|
5174
5166
|
storeCardTitle: "Image storage",
|
|
5175
5167
|
storeCardDesc: "Orphaned image files left behind by an accident can be cleared here",
|
|
@@ -5206,41 +5198,6 @@ window.__ModuleLoader__.load({
|
|
|
5206
5198
|
//#endregion
|
|
5207
5199
|
//#region src/preset.ts
|
|
5208
5200
|
/**
|
|
5209
|
-
* JSON with object keys in a fixed (sorted) order — the same string for the same
|
|
5210
|
-
* value, whatever order the object that carried it happened to build its keys in.
|
|
5211
|
-
*
|
|
5212
|
-
* `JSON.stringify` alone is not an identity for a value that came off the wire and
|
|
5213
|
-
* through a sanitizer: key order is an accident of how the object was assembled,
|
|
5214
|
-
* and a resume that missed on it would silently re-download everything (the safe
|
|
5215
|
-
* direction) while a resume that MATCHED on a value it should not have would be
|
|
5216
|
-
* the unsafe one — so this is deliberately total as well: anything JSON cannot
|
|
5217
|
-
* carry (a function, a `Symbol`, `undefined`) serializes as `null` and an
|
|
5218
|
-
* `undefined` property is skipped, exactly as `JSON.stringify` treats it.
|
|
5219
|
-
*/
|
|
5220
|
-
function canonical(value) {
|
|
5221
|
-
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
|
5222
|
-
if (value !== null && typeof value === "object") return `{${Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => `${JSON.stringify(k)}:${canonical(v)}`).join(",")}}`;
|
|
5223
|
-
return JSON.stringify(value) ?? "null";
|
|
5224
|
-
}
|
|
5225
|
-
/**
|
|
5226
|
-
* The identity of one revision of the profile: the manifest's shape version plus
|
|
5227
|
-
* the config, in canonical form.
|
|
5228
|
-
*
|
|
5229
|
-
* The profile is served at ONE version of the assets repository, but a version
|
|
5230
|
-
* reference is only as fixed as the tag behind it: re-point the tag and every
|
|
5231
|
-
* edge that has not been purged keeps answering with the previous bytes of the
|
|
5232
|
-
* same URL. So "the profile" is still not guaranteed to be one fixed thing, and
|
|
5233
|
-
* the bytes a previous attempt is holding are only known to belong to what is
|
|
5234
|
-
* being installed when that config comes back the same. Order-insensitivity is
|
|
5235
|
-
* what lets this be a comparison of VALUES rather than of spellings —
|
|
5236
|
-
* `normalizeConfig` (./schema) rebuilds the config it reads, so pinning the key
|
|
5237
|
-
* to the exact key order the wire happened to use would make the hold useless
|
|
5238
|
-
* the moment anything upstream normalized differently.
|
|
5239
|
-
*/
|
|
5240
|
-
function presetKey(version, config) {
|
|
5241
|
-
return `${version}\n${canonical(config)}`;
|
|
5242
|
-
}
|
|
5243
|
-
/**
|
|
5244
5201
|
* The bytes of a failed attempt that may be reused for the revision `key` names.
|
|
5245
5202
|
*
|
|
5246
5203
|
* A fresh object every time, never the held record itself: the caller fills it in
|
|
@@ -5264,6 +5221,145 @@ window.__ModuleLoader__.load({
|
|
|
5264
5221
|
function presetMissing(slots, images) {
|
|
5265
5222
|
return slots.filter((slot) => images[slot] === void 0);
|
|
5266
5223
|
}
|
|
5224
|
+
/** Every slot the profile installs, in list order — the download set. */
|
|
5225
|
+
const PROFILE_SLOTS = [
|
|
5226
|
+
{
|
|
5227
|
+
slot: "m1",
|
|
5228
|
+
path: "/bfs/new_dyn/e4565384e7e5b2d75756e577f505534b4168597.png"
|
|
5229
|
+
},
|
|
5230
|
+
{
|
|
5231
|
+
slot: "m2",
|
|
5232
|
+
path: "/bfs/new_dyn/aa6e8649701c91e5ac8f43ff28b97a924168597.png"
|
|
5233
|
+
},
|
|
5234
|
+
{
|
|
5235
|
+
slot: "m3",
|
|
5236
|
+
path: "/bfs/new_dyn/5da92b31df5ee2a20b17ec6be353cf5c4168597.png"
|
|
5237
|
+
},
|
|
5238
|
+
{
|
|
5239
|
+
slot: "m4",
|
|
5240
|
+
path: "/bfs/new_dyn/14bb1e53cb2c13ca80bd23e3c1d2a04e4168597.png"
|
|
5241
|
+
},
|
|
5242
|
+
{
|
|
5243
|
+
slot: "m5",
|
|
5244
|
+
path: "/bfs/new_dyn/f51fff40de2d6eaa5a73d177f7bbfa794168597.png"
|
|
5245
|
+
},
|
|
5246
|
+
{
|
|
5247
|
+
slot: "m6",
|
|
5248
|
+
path: "/bfs/new_dyn/cfa71d4e60a2f12aaeec3efe1aa680b34168597.png"
|
|
5249
|
+
},
|
|
5250
|
+
{
|
|
5251
|
+
slot: "m7",
|
|
5252
|
+
path: "/bfs/new_dyn/c09d34b79d9b447d7db98da68b97afc54168597.png"
|
|
5253
|
+
}
|
|
5254
|
+
].map((i) => i.slot);
|
|
5255
|
+
/**
|
|
5256
|
+
* Identity of this generation of the built-in profile.
|
|
5257
|
+
*
|
|
5258
|
+
* Only one thing reads it: what a failed download KEEPS for the next press
|
|
5259
|
+
* (`PresetHeld.key` in ./preset). Editing the rules or the pictures below must bump
|
|
5260
|
+
* it, or a retry could staple a wallpaper from the old set to a config from the new
|
|
5261
|
+
* one — the same reason the remote profile was compared by value rather than
|
|
5262
|
+
* assumed. Nothing else in the plugin persists it.
|
|
5263
|
+
*/
|
|
5264
|
+
const RECOMMENDED_REVISION = "builtin-1";
|
|
5265
|
+
/**
|
|
5266
|
+
* One rule of the profile: the match string, its picture and that picture's color.
|
|
5267
|
+
*
|
|
5268
|
+
* The color is a per-image value (schema 10), and the triple here is the one the
|
|
5269
|
+
* profile has always carried — an HSL accent per model, used to tint the interface
|
|
5270
|
+
* while its wallpaper is on screen.
|
|
5271
|
+
*/
|
|
5272
|
+
function profileRule(id, match, slot, color) {
|
|
5273
|
+
return {
|
|
5274
|
+
id,
|
|
5275
|
+
images: [defaultImage(slot, color, "fill")],
|
|
5276
|
+
match,
|
|
5277
|
+
enabled: true,
|
|
5278
|
+
color: null,
|
|
5279
|
+
rotate: defaultRotation()
|
|
5280
|
+
};
|
|
5281
|
+
}
|
|
5282
|
+
/**
|
|
5283
|
+
* The profile, as a fresh config every call.
|
|
5284
|
+
*
|
|
5285
|
+
* ORDER IS THE FALLBACK: matching runs top→bottom and the first usable rule is what
|
|
5286
|
+
* an unmatched model gets, so `deepseek` leads — the model this plugin was written
|
|
5287
|
+
* for. Everything below the rules is the interface numbers a fresh install already
|
|
5288
|
+
* starts on (`DEFAULT_PART_OPACITIES` and friends), which is what makes installing
|
|
5289
|
+
* the profile "add the rules and the pictures" on a clean store.
|
|
5290
|
+
*
|
|
5291
|
+
* Deliberately NOT taken from `freshThemeConfig()`: that one seeds a single BLANK
|
|
5292
|
+
* rule, and this one must not inherit it.
|
|
5293
|
+
*/
|
|
5294
|
+
function recommendedConfig() {
|
|
5295
|
+
return {
|
|
5296
|
+
rules: [
|
|
5297
|
+
profileRule("r1", "deepseek", "m3", [
|
|
5298
|
+
191.88931210132625,
|
|
5299
|
+
.5046198735613555,
|
|
5300
|
+
.82
|
|
5301
|
+
]),
|
|
5302
|
+
profileRule("r2", "gpt", "m1", [
|
|
5303
|
+
263.5122459614382,
|
|
5304
|
+
.31076923076923096,
|
|
5305
|
+
.82
|
|
5306
|
+
]),
|
|
5307
|
+
profileRule("r3", "claude", "m2", [
|
|
5308
|
+
18.923814597762384,
|
|
5309
|
+
.5486699795381468,
|
|
5310
|
+
.6
|
|
5311
|
+
]),
|
|
5312
|
+
profileRule("r4", "qwen", "m4", [
|
|
5313
|
+
26.25977410947,
|
|
5314
|
+
.6228354978354975,
|
|
5315
|
+
.82
|
|
5316
|
+
]),
|
|
5317
|
+
profileRule("r5", "glm", "m5", [
|
|
5318
|
+
30.15559737458117,
|
|
5319
|
+
.6552284141830319,
|
|
5320
|
+
.7646280172718907
|
|
5321
|
+
]),
|
|
5322
|
+
profileRule("r6", "kimi", "m6", [
|
|
5323
|
+
229.39645097591443,
|
|
5324
|
+
.5752269829617896,
|
|
5325
|
+
.2
|
|
5326
|
+
]),
|
|
5327
|
+
profileRule("r7", "grok", "m7", [
|
|
5328
|
+
217.3791348600509,
|
|
5329
|
+
.7702645947081058,
|
|
5330
|
+
.2
|
|
5331
|
+
])
|
|
5332
|
+
],
|
|
5333
|
+
opacities: { ...DEFAULT_PART_OPACITIES },
|
|
5334
|
+
transition: defaultTransition(),
|
|
5335
|
+
settingsOpacity: 1,
|
|
5336
|
+
chatTextOpacity: DEFAULT_CHAT_TEXT_OPACITY,
|
|
5337
|
+
trajectoryOpacity: DEFAULT_TRAJECTORY_OPACITY,
|
|
5338
|
+
rightbarOpacity: DEFAULT_RIGHTBAR_OPACITY,
|
|
5339
|
+
pageOpacity: 0,
|
|
5340
|
+
autoExtract: true,
|
|
5341
|
+
holidays: {
|
|
5342
|
+
enabled: true,
|
|
5343
|
+
items: defaultHolidayRules()
|
|
5344
|
+
}
|
|
5345
|
+
};
|
|
5346
|
+
}
|
|
5347
|
+
/**
|
|
5348
|
+
* The profile as the document both imports consume — the same shape `exportTheme`
|
|
5349
|
+
* writes, so everything downstream (the wipe, the slot loop) is the hand-picked
|
|
5350
|
+
* file's path.
|
|
5351
|
+
*
|
|
5352
|
+
* `images` is filled by the caller as the downloads land, which is what makes a
|
|
5353
|
+
* partially downloaded profile a resumable one rather than an all-or-nothing press.
|
|
5354
|
+
*/
|
|
5355
|
+
function recommendedThemeFile(images = {}) {
|
|
5356
|
+
return {
|
|
5357
|
+
version: 10,
|
|
5358
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5359
|
+
config: recommendedConfig(),
|
|
5360
|
+
images
|
|
5361
|
+
};
|
|
5362
|
+
}
|
|
5267
5363
|
//#endregion
|
|
5268
5364
|
//#region src/client/repaint.ts
|
|
5269
5365
|
/**
|
|
@@ -7722,19 +7818,15 @@ window.__ModuleLoader__.load({
|
|
|
7722
7818
|
/**
|
|
7723
7819
|
* The ways the recommended-profile download can fail, mapped to their copy.
|
|
7724
7820
|
*
|
|
7725
|
-
*
|
|
7726
|
-
*
|
|
7727
|
-
*
|
|
7728
|
-
*
|
|
7729
|
-
*
|
|
7730
|
-
*
|
|
7731
|
-
* one is the author's file naming, and only the slot tells them which.
|
|
7821
|
+
* Two cases, because the profile is now only PICTURES: the rules and their numbers
|
|
7822
|
+
* are compiled in, so there is no manifest to be malformed, no shape to be too new
|
|
7823
|
+
* and no config to name too many slots. What is left is the network ("retry") and
|
|
7824
|
+
* the cap on the total ("nobody should install this"). A failure that names a SLOT
|
|
7825
|
+
* is rendered separately — that one is about one BFS address, and the slot is what
|
|
7826
|
+
* says which.
|
|
7732
7827
|
*/
|
|
7733
7828
|
const REC_ERROR_KEYS = {
|
|
7734
7829
|
"download failed": "recFailNetwork",
|
|
7735
|
-
"not a profile": "recFailNotProfile",
|
|
7736
|
-
"newer version": "recFailNewer",
|
|
7737
|
-
"too many images": "recFailTooMany",
|
|
7738
7830
|
"too large": "recFailTooLarge"
|
|
7739
7831
|
};
|
|
7740
7832
|
function ProfilePage({ p, notify }) {
|
|
@@ -8418,12 +8510,12 @@ window.__ModuleLoader__.load({
|
|
|
8418
8510
|
/**
|
|
8419
8511
|
* Ceiling on what one recommended profile may add up to, across every wallpaper.
|
|
8420
8512
|
*
|
|
8421
|
-
* The node half caps
|
|
8422
|
-
*
|
|
8423
|
-
*
|
|
8424
|
-
*
|
|
8425
|
-
*
|
|
8426
|
-
* button press into half a gigabyte
|
|
8513
|
+
* The node half caps ONE file, but it answers one independent call at a time and
|
|
8514
|
+
* never sees a running total — the client is the only side that does, so the total
|
|
8515
|
+
* is bounded here. The real profile is 2.2 MB of images across seven of them, so
|
|
8516
|
+
* this is generous by design: it exists so that a build which grew a wrong URL
|
|
8517
|
+
* (or an edge that served an upload instead of the transform) cannot turn one
|
|
8518
|
+
* button press into half a gigabyte.
|
|
8427
8519
|
*/
|
|
8428
8520
|
const PRESET_TOTAL_MAX = 33554432;
|
|
8429
8521
|
/**
|
|
@@ -8434,7 +8526,8 @@ window.__ModuleLoader__.load({
|
|
|
8434
8526
|
* CDN edge was still cold). What is being paid is per-request latency, and the
|
|
8435
8527
|
* host speaks HTTP/2 — the bytes are identical either way and the connection is
|
|
8436
8528
|
* reused — so overlapping them buys wall-clock time and risks nothing but a
|
|
8437
|
-
* little contention.
|
|
8529
|
+
* little contention. The same holds for the Bilibili edges the pictures come from
|
|
8530
|
+
* now (see `PROFILE_HOSTS` in ../profile).
|
|
8438
8531
|
*
|
|
8439
8532
|
* Four rather than all seven: a burst that wide competes for the same bandwidth
|
|
8440
8533
|
* and makes the per-file timeout harder to reason about, and four already hides
|
|
@@ -9060,30 +9153,28 @@ window.__ModuleLoader__.load({
|
|
|
9060
9153
|
}
|
|
9061
9154
|
},
|
|
9062
9155
|
/**
|
|
9063
|
-
* Fetch the
|
|
9064
|
-
* separate, because the page has to ask before anything is destroyed and
|
|
9065
|
-
* question should describe what is actually in hand.
|
|
9156
|
+
* Fetch the recommended profile's wallpapers. Downloads only — applying them
|
|
9157
|
+
* is separate, because the page has to ask before anything is destroyed and
|
|
9158
|
+
* the question should describe what is actually in hand.
|
|
9159
|
+
*
|
|
9160
|
+
* The RULES are not fetched at all: they are compiled into the bundle
|
|
9161
|
+
* (./profile), so this is a set of picture transfers and nothing else. That
|
|
9162
|
+
* is why there is no manifest round, no shape gate and no "not a profile"
|
|
9163
|
+
* failure left — nothing remote can decide what this installs.
|
|
9066
9164
|
*
|
|
9067
|
-
*
|
|
9068
|
-
*
|
|
9069
|
-
*
|
|
9070
|
-
* retry rounds (the node half's business, ./preset), validation and slot name
|
|
9071
|
-
* in a failure.
|
|
9165
|
+
* Each slot is one request, up to `PRESET_CONCURRENCY` at a time, and each
|
|
9166
|
+
* file keeps its own host fallback, retry rounds (the node half's business,
|
|
9167
|
+
* ./preset), validation and slot name in a failure.
|
|
9072
9168
|
*
|
|
9073
9169
|
* STRICT: the first wallpaper that cannot be had stops the queue. What had
|
|
9074
9170
|
* already arrived is kept for the next press (`presetHeld` below) — the
|
|
9075
9171
|
* difference between retrying one file and retrying the lot. Nothing is
|
|
9076
|
-
* touched here, so
|
|
9077
|
-
*
|
|
9172
|
+
* touched here, so a wrong URL arrives as an error naming the slot rather
|
|
9173
|
+
* than as a profile quietly missing a picture.
|
|
9078
9174
|
*/
|
|
9079
9175
|
fetchRecommended: async (onProgress) => {
|
|
9080
|
-
const
|
|
9081
|
-
|
|
9082
|
-
ok: false,
|
|
9083
|
-
error: head.error
|
|
9084
|
-
};
|
|
9085
|
-
const slots = head.slots;
|
|
9086
|
-
const key = presetKey(head.version, head.config);
|
|
9176
|
+
const slots = PROFILE_SLOTS;
|
|
9177
|
+
const key = RECOMMENDED_REVISION;
|
|
9087
9178
|
const images = presetResume(presetHeld, key);
|
|
9088
9179
|
let bytes = 0;
|
|
9089
9180
|
for (const url of Object.values(images)) bytes += url.length;
|
|
@@ -9131,12 +9222,7 @@ window.__ModuleLoader__.load({
|
|
|
9131
9222
|
presetHeld = null;
|
|
9132
9223
|
return {
|
|
9133
9224
|
ok: true,
|
|
9134
|
-
preset:
|
|
9135
|
-
version: head.version,
|
|
9136
|
-
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9137
|
-
config: head.config,
|
|
9138
|
-
images
|
|
9139
|
-
}
|
|
9225
|
+
preset: recommendedThemeFile(images)
|
|
9140
9226
|
};
|
|
9141
9227
|
},
|
|
9142
9228
|
/** Replace EVERYTHING with a downloaded profile: empty the store, then write it. */
|
package/lib/index.js
CHANGED
|
@@ -5,15 +5,31 @@ const HOLIDAYS = [{
|
|
|
5
5
|
id: "mid-autumn",
|
|
6
6
|
slot: "h-midautumn",
|
|
7
7
|
asset: "mid-autumn.webp",
|
|
8
|
+
source: "/bfs/new_dyn/42e22801827aff71267b03c95fcfd7834168597.png@1672w.webp",
|
|
8
9
|
color: "#384A77",
|
|
9
10
|
kind: "lunar"
|
|
10
11
|
}, {
|
|
11
12
|
id: "national-day",
|
|
12
13
|
slot: "h-nationalday",
|
|
13
14
|
asset: "national-day.webp",
|
|
15
|
+
source: "/bfs/new_dyn/30e97211189f7869dae83b909d2a3cc84168597.png@3456w.webp",
|
|
14
16
|
color: "#FFF6EB",
|
|
15
17
|
kind: "gregorian"
|
|
16
18
|
}];
|
|
19
|
+
/**
|
|
20
|
+
* The generation a holiday's ART belongs to, for the on-disk cache.
|
|
21
|
+
*
|
|
22
|
+
* The BFS path is content-addressed (`bfs/new_dyn/<sha1 of the upload>`), so the
|
|
23
|
+
* first eight characters of that hash are a content identity: change the art, change
|
|
24
|
+
* the directory, and a cache keyed by this cannot answer a new upload with the
|
|
25
|
+
* previous bytes. That is the property the pinned assets release tag used to provide
|
|
26
|
+
* and the reason it is now derivable instead of remembered — there is no version
|
|
27
|
+
* constant to bump, and a forgotten bump is not possible.
|
|
28
|
+
*/
|
|
29
|
+
function holidayArtGeneration(def) {
|
|
30
|
+
const hash = /new_dyn\/([0-9a-f]+)/.exec(def.source);
|
|
31
|
+
return hash === null ? "v1" : hash[1].slice(0, 8);
|
|
32
|
+
}
|
|
17
33
|
//#endregion
|
|
18
34
|
//#region src/schema.ts
|
|
19
35
|
/**
|
|
@@ -475,20 +491,21 @@ function normalizeConfig(raw) {
|
|
|
475
491
|
*
|
|
476
492
|
* Three reasons are terminal, and each for its own:
|
|
477
493
|
*
|
|
478
|
-
* - `missing` — the host says the file is not there.
|
|
479
|
-
*
|
|
480
|
-
*
|
|
481
|
-
*
|
|
482
|
-
*
|
|
494
|
+
* - `missing` — the host says the file is not there. A BFS path is a hash of an
|
|
495
|
+
* upload (see `PROFILE_HOSTS` in ./profile), so a 404 is not a flake that might
|
|
496
|
+
* clear: the picture it names either exists or never will. It has to reach the
|
|
497
|
+
* user as a loud failure NAMING the slot — and it has to reach them FAST:
|
|
498
|
+
* paying two more rounds for a file that is provably absent only makes the
|
|
499
|
+
* sentence later, on a button the user is watching.
|
|
483
500
|
* - `oversized` — the bytes are the problem, and the next round would serve the
|
|
484
501
|
* same bytes.
|
|
485
|
-
* - `timeout` — sixty seconds of silence is not weather.
|
|
486
|
-
*
|
|
487
|
-
*
|
|
488
|
-
*
|
|
489
|
-
*
|
|
490
|
-
*
|
|
491
|
-
*
|
|
502
|
+
* - `timeout` — sixty seconds of silence is not weather. Every host was already
|
|
503
|
+
* asked inside the SAME round, so a retry round would re-ask hosts that each
|
|
504
|
+
* just spent a minute not answering. This is also what keeps the worst case for
|
|
505
|
+
* one file at three minutes instead of nine: a round in which every edge was
|
|
506
|
+
* silent is over immediately (three minutes, exactly what it cost before there
|
|
507
|
+
* were rounds at all), and only a round that mixed a silent edge with a
|
|
508
|
+
* transient one can carry a round's worth of latency into the next one.
|
|
492
509
|
*
|
|
493
510
|
* Everything else is a hiccup: a thrown connection, a 5xx, a 429, a 408, or a 200
|
|
494
511
|
* with no body.
|
|
@@ -500,7 +517,7 @@ function presetRetryable(reason) {
|
|
|
500
517
|
* The delay before the round that follows `failedAttempt` (1-based), in ms.
|
|
501
518
|
*
|
|
502
519
|
* Doubling rather than fixed: the second retry is the one that has to survive a
|
|
503
|
-
*
|
|
520
|
+
* edge having a bad few hundred milliseconds, and it is still short enough that
|
|
504
521
|
* a user staring at "downloading" does not read it as a hang.
|
|
505
522
|
*/
|
|
506
523
|
function presetRetryDelayMs(failedAttempt) {
|
|
@@ -542,6 +559,88 @@ function orphanSlots(stored, referenced, kept = []) {
|
|
|
542
559
|
return stored.filter((slot) => !live.has(slot));
|
|
543
560
|
}
|
|
544
561
|
//#endregion
|
|
562
|
+
//#region src/profile.ts
|
|
563
|
+
/**
|
|
564
|
+
* Where the pictures are hosted.
|
|
565
|
+
*
|
|
566
|
+
* Three hosts, tried in order, and they are not mirrors of each other by
|
|
567
|
+
* configuration — `i0`/`i1`/`i2.hdslb.com` are separate BFS edges that answer the
|
|
568
|
+
* same path with the same bytes (verified by sha1), which is the same arrangement
|
|
569
|
+
* the festival art already relies on.
|
|
570
|
+
*/
|
|
571
|
+
const PROFILE_HOSTS = [
|
|
572
|
+
"https://i0.hdslb.com",
|
|
573
|
+
"https://i1.hdslb.com",
|
|
574
|
+
"https://i2.hdslb.com"
|
|
575
|
+
];
|
|
576
|
+
/**
|
|
577
|
+
* The BFS transform that turns the uploaded PNG into the file this plugin stores.
|
|
578
|
+
*
|
|
579
|
+
* Load-bearing, and the reason it is spelled out once here:
|
|
580
|
+
*
|
|
581
|
+
* - the UPLOADED files are 2.4–12 MB PNGs (76 MB for the set). `PRESET_IMAGE_MAX`
|
|
582
|
+
* is 8 MB and the store keeps full fidelity, so serving them as uploaded would
|
|
583
|
+
* both refuse the largest ones and put a 76 MB wallpaper collection on disk;
|
|
584
|
+
* - `@3840w.webp` answers 250–365 KB at 3840×2160, which is what the previous
|
|
585
|
+
* asset host served and what an existing store already holds — the sha1 of the
|
|
586
|
+
* bytes is identical, so this is the same picture, not a re-encode;
|
|
587
|
+
* - the suffix is plain arithmetic rather than a CDN feature that may move: the
|
|
588
|
+
* path before it is `bfs/new_dyn/<sha1 of the upload>`, i.e. CONTENT-ADDRESSED,
|
|
589
|
+
* so a URL here can never be re-pointed at different bytes the way a git tag
|
|
590
|
+
* can.
|
|
591
|
+
*/
|
|
592
|
+
const BFS_SUFFIX = "@3840w.webp";
|
|
593
|
+
/**
|
|
594
|
+
* Slot → BFS path, for the seven wallpapers the profile is made of.
|
|
595
|
+
*
|
|
596
|
+
* The slots are `m1`..`m7` rather than names, because a slot is a FILENAME
|
|
597
|
+
* (`modelbg-<slot>`) and `nextSlot` hands out exactly this namespace. The mapping
|
|
598
|
+
* to a model is the rule list below, never the slot name.
|
|
599
|
+
*/
|
|
600
|
+
const IMAGE_PATHS = [
|
|
601
|
+
{
|
|
602
|
+
slot: "m1",
|
|
603
|
+
path: "/bfs/new_dyn/e4565384e7e5b2d75756e577f505534b4168597.png"
|
|
604
|
+
},
|
|
605
|
+
{
|
|
606
|
+
slot: "m2",
|
|
607
|
+
path: "/bfs/new_dyn/aa6e8649701c91e5ac8f43ff28b97a924168597.png"
|
|
608
|
+
},
|
|
609
|
+
{
|
|
610
|
+
slot: "m3",
|
|
611
|
+
path: "/bfs/new_dyn/5da92b31df5ee2a20b17ec6be353cf5c4168597.png"
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
slot: "m4",
|
|
615
|
+
path: "/bfs/new_dyn/14bb1e53cb2c13ca80bd23e3c1d2a04e4168597.png"
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
slot: "m5",
|
|
619
|
+
path: "/bfs/new_dyn/f51fff40de2d6eaa5a73d177f7bbfa794168597.png"
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
slot: "m6",
|
|
623
|
+
path: "/bfs/new_dyn/cfa71d4e60a2f12aaeec3efe1aa680b34168597.png"
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
slot: "m7",
|
|
627
|
+
path: "/bfs/new_dyn/c09d34b79d9b447d7db98da68b97afc54168597.png"
|
|
628
|
+
}
|
|
629
|
+
];
|
|
630
|
+
IMAGE_PATHS.map((i) => i.slot);
|
|
631
|
+
/**
|
|
632
|
+
* The URL one slot's wallpaper is fetched from, on the first host — `${host}${url}`
|
|
633
|
+
* is how a caller builds the other two.
|
|
634
|
+
*
|
|
635
|
+
* Returns null for a slot this profile does not carry, and that null is the whole
|
|
636
|
+
* whitelist: `fetchPresetImage` (./index) answers `bad slot` for it, so a caller on
|
|
637
|
+
* the public RPC cannot name a file of its own.
|
|
638
|
+
*/
|
|
639
|
+
function profileImagePath(slot) {
|
|
640
|
+
const entry = IMAGE_PATHS.find((i) => i.slot === slot);
|
|
641
|
+
return entry === void 0 ? null : `${entry.path}${BFS_SUFFIX}`;
|
|
642
|
+
}
|
|
643
|
+
//#endregion
|
|
545
644
|
//#region src/index.ts
|
|
546
645
|
/**
|
|
547
646
|
* Node half of dsh-background-by-model: file-backed rule persistence.
|
|
@@ -593,90 +692,56 @@ const LEGACY_FILES = [
|
|
|
593
692
|
const WALLPAPER_FETCH_MAX = 26214400;
|
|
594
693
|
const WALLPAPER_FETCH_TIMEOUT = 2e4;
|
|
595
694
|
/**
|
|
596
|
-
* Where the festival art is hosted
|
|
695
|
+
* Where the festival art is hosted.
|
|
597
696
|
*
|
|
598
|
-
*
|
|
599
|
-
*
|
|
697
|
+
* Three edges of Bilibili's image CDN, tried in order. They are not mirrors by
|
|
698
|
+
* configuration — `i0`/`i1`/`i2.hdslb.com` answer the same path with the same bytes
|
|
699
|
+
* (checked by sha1) — which is the same arrangement the recommended profile uses
|
|
700
|
+
* (see `PROFILE_HOSTS` in ./profile).
|
|
600
701
|
*
|
|
601
|
-
*
|
|
602
|
-
*
|
|
603
|
-
*
|
|
604
|
-
*
|
|
605
|
-
* does not reach jsDelivr's edge on its own, so new art means a new tag, never a
|
|
606
|
-
* force-push of the old one.
|
|
702
|
+
* A path under `bfs/new_dyn/` is CONTENT-ADDRESSED (the tail is a hash of the
|
|
703
|
+
* upload), so nothing here needs a version constant: the art a build names cannot be
|
|
704
|
+
* swapped under it the way a re-pointed git tag could, and `holidayArtGeneration`
|
|
705
|
+
* (./holiday) derives the cache's directory from that same hash.
|
|
607
706
|
*/
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
* is not, and raw is the fallback for a CDN that is blocked or down. Both serve the
|
|
614
|
-
* same version, so a fallback cannot mix revisions.
|
|
615
|
-
*/
|
|
616
|
-
const HOLIDAY_ASSET_HOSTS = [`https://cdn.jsdelivr.net/gh/${HOLIDAY_ASSETS_REPO}@${HOLIDAY_ASSETS_REF}/${HOLIDAY_ASSETS_DIR}`, `https://raw.githubusercontent.com/${HOLIDAY_ASSETS_REPO}/${HOLIDAY_ASSETS_REF}/${HOLIDAY_ASSETS_DIR}`];
|
|
707
|
+
const HOLIDAY_HOSTS = [
|
|
708
|
+
"https://i0.hdslb.com",
|
|
709
|
+
"https://i1.hdslb.com",
|
|
710
|
+
"https://i2.hdslb.com"
|
|
711
|
+
];
|
|
617
712
|
/**
|
|
618
713
|
* Sub-directory of the store holding downloaded festival art: one directory per
|
|
619
|
-
*
|
|
714
|
+
* GENERATION of the art (see `holidayArtGeneration`), one file per asset inside it.
|
|
620
715
|
*
|
|
621
716
|
* NOT part of the slot namespace, and must never become part of it: files are named
|
|
622
|
-
* after the ASSET and their parent after the
|
|
623
|
-
* nothing here can be mistaken for a user's `modelbg-<slot>`. That is what lets a
|
|
717
|
+
* after the ASSET and their parent after the art's content hash, never after a slot,
|
|
718
|
+
* so nothing here can be mistaken for a user's `modelbg-<slot>`. That is what lets a
|
|
624
719
|
* holiday slot stay read-only while the art itself is downloaded.
|
|
625
720
|
*
|
|
626
|
-
* The
|
|
627
|
-
* answer a NEW
|
|
628
|
-
*
|
|
629
|
-
*
|
|
630
|
-
*
|
|
721
|
+
* The generation segment is load-bearing: a cache keyed by asset name alone would
|
|
722
|
+
* answer a NEW upload with the OLD bytes and say nothing about it. With the hash in
|
|
723
|
+
* the path, a build that names new art looks in a directory nobody has written to
|
|
724
|
+
* yet, so the first holiday after an update is a miss. Superseded generations are
|
|
725
|
+
* swept by `pruneHolidayCache`.
|
|
631
726
|
*/
|
|
632
727
|
const HOLIDAY_CACHE_DIR = "holiday-cache";
|
|
633
|
-
/** Cap and timeout for one asset download; these files are
|
|
728
|
+
/** Cap and timeout for one asset download; these files are 207 KB and 446 KB. */
|
|
634
729
|
const HOLIDAY_FETCH_MAX = 8388608;
|
|
635
730
|
const HOLIDAY_FETCH_TIMEOUT = 1e4;
|
|
636
731
|
/**
|
|
637
|
-
*
|
|
638
|
-
*
|
|
639
|
-
*
|
|
640
|
-
*
|
|
641
|
-
*
|
|
642
|
-
*
|
|
643
|
-
*
|
|
644
|
-
*
|
|
645
|
-
*
|
|
646
|
-
*
|
|
647
|
-
*
|
|
648
|
-
* bytes of the same URL, and a resumed download must not glue two revisions
|
|
649
|
-
* together (see `fetchPresetConfig`). A profile written for a config shape this
|
|
650
|
-
* build cannot read is refused by the `preset.json` gate.
|
|
651
|
-
*/
|
|
652
|
-
const PRESET_ASSETS_REPO = "HarmlessFunny/assets";
|
|
653
|
-
const PRESET_ASSETS_REF = "v0.1.0";
|
|
654
|
-
const PRESET_ASSET_DIR = "dsh-background-by-model/preset";
|
|
655
|
-
/** The shape gate: `{"version": 9}` — see SCHEMA_VERSION in ./schema. */
|
|
656
|
-
const PRESET_MANIFEST = "preset.json";
|
|
657
|
-
const PRESET_CONFIG = "theme-config.json";
|
|
658
|
-
/** Images are named after the slot they fill, exactly as they are on disk. */
|
|
659
|
-
const PRESET_IMAGE_PREFIX = "modelbg-";
|
|
660
|
-
/** Mirrors, tried in order — same reason as the festival art's. */
|
|
661
|
-
const PRESET_ASSET_HOSTS = [`https://cdn.jsdelivr.net/gh/${PRESET_ASSETS_REPO}@${PRESET_ASSETS_REF}/${PRESET_ASSET_DIR}`, `https://raw.githubusercontent.com/${PRESET_ASSETS_REPO}/${PRESET_ASSETS_REF}/${PRESET_ASSET_DIR}`];
|
|
662
|
-
/**
|
|
663
|
-
* Caps for the profile, now per file rather than for one document: a profile is a
|
|
664
|
-
* SET of transfers, and each has a different way of going wrong.
|
|
665
|
-
*
|
|
666
|
-
* The count is the one that matters — the request set is derived from the config,
|
|
667
|
-
* so a hostile config could name a thousand slots and turn one button press into a
|
|
668
|
-
* thousand downloads. `SLOT_RE` bounds each name, `PRESET_IMAGE_COUNT_MAX` bounds
|
|
669
|
-
* how many there are, and the running TOTAL is bounded on the client, which is the
|
|
670
|
-
* only side that sees more than one file.
|
|
671
|
-
*
|
|
672
|
-
* The timeout is set by measurement: a 350 KB wallpaper takes a second or two over
|
|
673
|
-
* jsDelivr, and the fallback mirror is the slower one, so a tight budget would fail
|
|
674
|
-
* exactly in the situation it exists for.
|
|
732
|
+
* Caps for the recommended profile's pictures.
|
|
733
|
+
*
|
|
734
|
+
* A profile is a SET of transfers, and what can go wrong is different per file: the
|
|
735
|
+
* node half bounds ONE download (`PRESET_IMAGE_MAX`) and the client bounds the
|
|
736
|
+
* running TOTAL, because it is the only side that sees more than one file.
|
|
737
|
+
*
|
|
738
|
+
* The count is a constant now rather than something a remote config declares
|
|
739
|
+
* (`PROFILE_SLOTS` in ./profile), so there is nothing left to bound: seven names,
|
|
740
|
+
* each already inside `SLOT_RE`. The timeout is set by measurement — a 250–365 KB
|
|
741
|
+
* wallpaper takes a second or two over the CDN, and the second edge is the slower
|
|
742
|
+
* one, so a tight budget would fail exactly in the situation it exists for.
|
|
675
743
|
*/
|
|
676
|
-
const PRESET_MANIFEST_MAX = 65536;
|
|
677
|
-
const PRESET_CONFIG_MAX = 1048576;
|
|
678
744
|
const PRESET_IMAGE_MAX = 8388608;
|
|
679
|
-
const PRESET_IMAGE_COUNT_MAX = 64;
|
|
680
745
|
const PRESET_FETCH_TIMEOUT = 6e4;
|
|
681
746
|
const dataDir = () => dshHomePath(DATA_DIR);
|
|
682
747
|
const configPath = () => dshHomePath(DATA_DIR, CONFIG_FILE);
|
|
@@ -684,7 +749,13 @@ const configBackupPath = () => dshHomePath(DATA_DIR, CONFIG_BACKUP);
|
|
|
684
749
|
const configTempPath = () => dshHomePath(DATA_DIR, `${CONFIG_FILE}${CONFIG_TEMP}`);
|
|
685
750
|
const legacyWallpaperPath = () => dshHomePath(DATA_DIR, LEGACY_WALLPAPER);
|
|
686
751
|
const imagePath = (slot) => dshHomePath(DATA_DIR, `${IMAGE_PREFIX}${slot}`);
|
|
687
|
-
|
|
752
|
+
/** The cache directory of one GENERATION of the festival art (see `holidayCachePath`). */
|
|
753
|
+
const holidayGenerationPath = (generation) => dshHomePath(DATA_DIR, HOLIDAY_CACHE_DIR, generation);
|
|
754
|
+
/** The cached file of one asset: its own generation directory, named by the asset. */
|
|
755
|
+
const holidayCachePath = (asset) => {
|
|
756
|
+
const def = HOLIDAYS.find((h) => h.asset === asset);
|
|
757
|
+
return dshHomePath(DATA_DIR, HOLIDAY_CACHE_DIR, def === void 0 ? "v1" : holidayArtGeneration(def), asset);
|
|
758
|
+
};
|
|
688
759
|
const exists = async (p) => {
|
|
689
760
|
try {
|
|
690
761
|
await access(p);
|
|
@@ -1021,7 +1092,7 @@ async function writeConfig(config) {
|
|
|
1021
1092
|
*
|
|
1022
1093
|
* The null answer is what a DOWNLOAD is checked against (see `fetchHolidayAsset`):
|
|
1023
1094
|
* neither a status code nor a `content-type` header survives contact with a
|
|
1024
|
-
* misconfigured
|
|
1095
|
+
* misconfigured origin, so the payload's own magic bytes are the only claim that
|
|
1025
1096
|
* cannot lie.
|
|
1026
1097
|
*/
|
|
1027
1098
|
function imageMimeOrNull(buf) {
|
|
@@ -1094,10 +1165,12 @@ async function readHolidayAsset(asset) {
|
|
|
1094
1165
|
holidayInFlight.set(asset, task);
|
|
1095
1166
|
return await task;
|
|
1096
1167
|
}
|
|
1097
|
-
/** Try every
|
|
1168
|
+
/** Try every host in turn; the first usable answer is cached and returned. */
|
|
1098
1169
|
async function downloadHolidayAsset(asset) {
|
|
1099
|
-
|
|
1100
|
-
|
|
1170
|
+
const def = HOLIDAYS.find((h) => h.asset === asset);
|
|
1171
|
+
if (def === void 0) return null;
|
|
1172
|
+
for (const host of HOLIDAY_HOSTS) {
|
|
1173
|
+
const buf = await fetchHolidayAsset(`${host}${def.source}`);
|
|
1101
1174
|
if (buf === null) continue;
|
|
1102
1175
|
await writeHolidayCache(asset, buf);
|
|
1103
1176
|
return buf;
|
|
@@ -1158,18 +1231,18 @@ async function fetchAsset(url, max, timeout) {
|
|
|
1158
1231
|
}
|
|
1159
1232
|
}
|
|
1160
1233
|
/**
|
|
1161
|
-
* The status codes worth another round: 5xx is a
|
|
1234
|
+
* The status codes worth another round: 5xx is a host having a bad moment, 429 one
|
|
1162
1235
|
* asking to be left alone, 408 one giving up on us. Every other non-2xx means the
|
|
1163
|
-
* file is not there (
|
|
1164
|
-
*
|
|
1236
|
+
* file is not there (404 for a missing path, 403 for a refused one), and that answer
|
|
1237
|
+
* must arrive fast and unchanged.
|
|
1165
1238
|
*/
|
|
1166
1239
|
function fetchFailureForStatus(status) {
|
|
1167
1240
|
return status >= 500 || status === 429 || status === 408 ? "server" : "missing";
|
|
1168
1241
|
}
|
|
1169
1242
|
/**
|
|
1170
|
-
* One festival picture, or null when no
|
|
1243
|
+
* One festival picture, or null when no host served an image.
|
|
1171
1244
|
*
|
|
1172
|
-
* One round, unlike the profile's (`
|
|
1245
|
+
* One round, unlike the profile's (`fetchProfileFile`): this is fetched during boot,
|
|
1173
1246
|
* before the first wallpaper is painted, and a retry round would delay that paint.
|
|
1174
1247
|
* The art is cached once it arrives and every later read tries again.
|
|
1175
1248
|
*/
|
|
@@ -1178,19 +1251,20 @@ async function fetchHolidayAsset(url) {
|
|
|
1178
1251
|
return !got.ok || imageMimeOrNull(got.buf) === null ? null : got.buf;
|
|
1179
1252
|
}
|
|
1180
1253
|
/**
|
|
1181
|
-
* One
|
|
1254
|
+
* One of the profile's pictures, or null when no host served it.
|
|
1182
1255
|
*
|
|
1183
|
-
* Every
|
|
1184
|
-
* the rounds are the retry (a
|
|
1256
|
+
* Every host is asked in every round, so a round is "all the hosts there are" and
|
|
1257
|
+
* the rounds are the retry (a host that answers 502 once and correctly the next
|
|
1185
1258
|
* time, on the sixth of seven files). A round whose failures were all terminal — the
|
|
1186
|
-
*
|
|
1187
|
-
* requests and a sentence naming the slot (`presetRetryable`,
|
|
1259
|
+
* path is gone, or too big — ends the attempts outright, so a wrong URL costs three
|
|
1260
|
+
* requests rather than nine and a sentence naming the slot (`presetRetryable`,
|
|
1261
|
+
* ./preset).
|
|
1188
1262
|
*/
|
|
1189
|
-
async function
|
|
1263
|
+
async function fetchProfileFile(path, max) {
|
|
1190
1264
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
1191
1265
|
let retryable = false;
|
|
1192
|
-
for (const host of
|
|
1193
|
-
const got = await fetchAsset(`${host}
|
|
1266
|
+
for (const host of PROFILE_HOSTS) {
|
|
1267
|
+
const got = await fetchAsset(`${host}${path}`, max, PRESET_FETCH_TIMEOUT);
|
|
1194
1268
|
if (got.ok) return got.buf;
|
|
1195
1269
|
if (presetRetryable(got.reason)) retryable = true;
|
|
1196
1270
|
}
|
|
@@ -1200,102 +1274,30 @@ async function fetchPresetFile(name, max) {
|
|
|
1200
1274
|
return null;
|
|
1201
1275
|
}
|
|
1202
1276
|
/**
|
|
1203
|
-
*
|
|
1204
|
-
*
|
|
1205
|
-
* Two files in order: the manifest says what SHAPE the config is written for, and a
|
|
1206
|
-
* config this build cannot read is refused here — before anything is downloaded in
|
|
1207
|
-
* earnest, long before anything is deleted. The config then says what the profile IS.
|
|
1277
|
+
* One of the profile's wallpapers, as a data URL ready to be written into a slot.
|
|
1208
1278
|
*
|
|
1209
|
-
* The
|
|
1210
|
-
*
|
|
1211
|
-
*
|
|
1212
|
-
*
|
|
1213
|
-
*
|
|
1214
|
-
*
|
|
1279
|
+
* The same two checks the festival art gets, for the same reason: the bytes must
|
|
1280
|
+
* actually be an image (`imageMimeOrNull`, never the `content-type` header — a CDN
|
|
1281
|
+
* may label a transform it does not serve), and the slot must be one this build is
|
|
1282
|
+
* willing to write. The slot name is re-checked here because this is a public RPC:
|
|
1283
|
+
* `profileImagePath` is the whitelist, and a name it does not carry has no URL at
|
|
1284
|
+
* all, so a caller cannot ask this process for a file of its own choosing.
|
|
1215
1285
|
*
|
|
1216
1286
|
* Nothing is cached to disk, unlike the festival art: that cache exists because the
|
|
1217
1287
|
* art is fetched on a schedule and must work offline afterwards, while this is a
|
|
1218
1288
|
* button whose whole point is to fetch what is there NOW.
|
|
1219
1289
|
*/
|
|
1220
|
-
async function fetchPresetConfig() {
|
|
1221
|
-
const manifestRaw = await fetchPresetFile(PRESET_MANIFEST, PRESET_MANIFEST_MAX);
|
|
1222
|
-
if (manifestRaw === null) return {
|
|
1223
|
-
ok: false,
|
|
1224
|
-
error: "download failed"
|
|
1225
|
-
};
|
|
1226
|
-
let manifest;
|
|
1227
|
-
try {
|
|
1228
|
-
manifest = JSON.parse(manifestRaw.toString("utf8"));
|
|
1229
|
-
} catch {
|
|
1230
|
-
return {
|
|
1231
|
-
ok: false,
|
|
1232
|
-
error: "not a profile"
|
|
1233
|
-
};
|
|
1234
|
-
}
|
|
1235
|
-
const declared = manifest?.version;
|
|
1236
|
-
if (typeof declared !== "number" || !isFinite(declared)) return {
|
|
1237
|
-
ok: false,
|
|
1238
|
-
error: "not a profile"
|
|
1239
|
-
};
|
|
1240
|
-
if (declared > 10) {
|
|
1241
|
-
console.warn(`dsh-background-by-model: the recommended profile declares schema ${declared}, this build writes 10`);
|
|
1242
|
-
return {
|
|
1243
|
-
ok: false,
|
|
1244
|
-
error: "newer version"
|
|
1245
|
-
};
|
|
1246
|
-
}
|
|
1247
|
-
if (declared < 10) console.warn(`dsh-background-by-model: the recommended profile is written for schema ${declared}; importing it with defaults for the fields it predates`);
|
|
1248
|
-
const configRaw = await fetchPresetFile(PRESET_CONFIG, PRESET_CONFIG_MAX);
|
|
1249
|
-
if (configRaw === null) return {
|
|
1250
|
-
ok: false,
|
|
1251
|
-
error: "download failed"
|
|
1252
|
-
};
|
|
1253
|
-
let raw;
|
|
1254
|
-
try {
|
|
1255
|
-
raw = JSON.parse(configRaw.toString("utf8"));
|
|
1256
|
-
} catch {
|
|
1257
|
-
return {
|
|
1258
|
-
ok: false,
|
|
1259
|
-
error: "not a profile"
|
|
1260
|
-
};
|
|
1261
|
-
}
|
|
1262
|
-
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return {
|
|
1263
|
-
ok: false,
|
|
1264
|
-
error: "not a profile"
|
|
1265
|
-
};
|
|
1266
|
-
const config = normalizeConfig(raw);
|
|
1267
|
-
const slots = /* @__PURE__ */ new Set();
|
|
1268
|
-
for (const rule of config.rules) for (const image of rule.images) {
|
|
1269
|
-
if (holidayAssetFor(image.slot) !== null) continue;
|
|
1270
|
-
slots.add(image.slot);
|
|
1271
|
-
}
|
|
1272
|
-
if (slots.size > PRESET_IMAGE_COUNT_MAX) return {
|
|
1273
|
-
ok: false,
|
|
1274
|
-
error: "too many images"
|
|
1275
|
-
};
|
|
1276
|
-
return {
|
|
1277
|
-
ok: true,
|
|
1278
|
-
version: declared,
|
|
1279
|
-
config,
|
|
1280
|
-
slots: [...slots]
|
|
1281
|
-
};
|
|
1282
|
-
}
|
|
1283
|
-
/**
|
|
1284
|
-
* One of the profile's wallpapers, as a data URL ready to be written into a slot.
|
|
1285
|
-
*
|
|
1286
|
-
* The same two checks the festival art gets, for the same reason: the bytes must
|
|
1287
|
-
* actually be an image (`imageMimeOrNull`, never the `content-type` header — the
|
|
1288
|
-
* mirror serves extensionless files as `application/octet-stream`), and the slot
|
|
1289
|
-
* must be one this process is willing to write. The slot name is re-checked here
|
|
1290
|
-
* because this is a public RPC: the list this side hands out is not the only
|
|
1291
|
-
* thing a caller could ask for.
|
|
1292
|
-
*/
|
|
1293
1290
|
async function fetchPresetImage(slot) {
|
|
1294
1291
|
if (!SLOT_RE.test(slot) || holidayAssetFor(slot) !== null) return {
|
|
1295
1292
|
ok: false,
|
|
1296
1293
|
error: "bad slot"
|
|
1297
1294
|
};
|
|
1298
|
-
const
|
|
1295
|
+
const path = profileImagePath(slot);
|
|
1296
|
+
if (path === null) return {
|
|
1297
|
+
ok: false,
|
|
1298
|
+
error: "bad slot"
|
|
1299
|
+
};
|
|
1300
|
+
const buf = await fetchProfileFile(path, PRESET_IMAGE_MAX);
|
|
1299
1301
|
if (buf === null) return {
|
|
1300
1302
|
ok: false,
|
|
1301
1303
|
error: "download failed"
|
|
@@ -1320,7 +1322,9 @@ async function fetchPresetImage(slot) {
|
|
|
1320
1322
|
async function writeHolidayCache(asset, buf) {
|
|
1321
1323
|
try {
|
|
1322
1324
|
await ensureDir();
|
|
1323
|
-
|
|
1325
|
+
const def = HOLIDAYS.find((h) => h.asset === asset);
|
|
1326
|
+
if (def === void 0) return;
|
|
1327
|
+
await mkdir(holidayGenerationPath(holidayArtGeneration(def)), { recursive: true });
|
|
1324
1328
|
const target = holidayCachePath(asset);
|
|
1325
1329
|
const temp = `${target}.tmp`;
|
|
1326
1330
|
await writeFile(temp, buf);
|
|
@@ -1330,21 +1334,28 @@ async function writeHolidayCache(asset, buf) {
|
|
|
1330
1334
|
console.warn(`dsh-background-by-model: could not cache the "${asset}" holiday art`, e);
|
|
1331
1335
|
}
|
|
1332
1336
|
}
|
|
1337
|
+
/** The generation directories this build still reads — one per holiday, deduped. */
|
|
1338
|
+
function holidayGenerations() {
|
|
1339
|
+
return [...new Set(HOLIDAYS.map(holidayArtGeneration))];
|
|
1340
|
+
}
|
|
1333
1341
|
/**
|
|
1334
|
-
* Drop every generation of the cache except the
|
|
1342
|
+
* Drop every generation of the cache except the ones this build reads.
|
|
1335
1343
|
*
|
|
1336
|
-
* Called AFTER a successful download, never before it: a
|
|
1337
|
-
*
|
|
1344
|
+
* Called AFTER a successful download, never before it: a build whose art turns out
|
|
1345
|
+
* to be unreachable must not cost the user the art they already had.
|
|
1338
1346
|
*
|
|
1339
|
-
* It walks the cache ROOT and compares GENERATION names, never asset names, so
|
|
1340
|
-
* generation in use cannot be touched. That also makes an upgrade clean up
|
|
1341
|
-
* itself
|
|
1347
|
+
* It walks the cache ROOT and compares GENERATION names, never asset names, so a
|
|
1348
|
+
* generation in use cannot be touched. That is also what makes an upgrade clean up
|
|
1349
|
+
* after itself twice over: the release-tag directories of every earlier version
|
|
1350
|
+
* (`holiday-cache/<tag>/`) and 0.7.9's flat `holiday-cache/<asset>` files are both
|
|
1351
|
+
* gone from here, because neither name is a content hash.
|
|
1342
1352
|
*/
|
|
1343
1353
|
async function pruneHolidayCache() {
|
|
1344
1354
|
try {
|
|
1345
1355
|
const root = dshHomePath(DATA_DIR, HOLIDAY_CACHE_DIR);
|
|
1356
|
+
const keep = new Set(holidayGenerations());
|
|
1346
1357
|
for (const entry of await readdir(root)) {
|
|
1347
|
-
if (entry
|
|
1358
|
+
if (keep.has(entry)) continue;
|
|
1348
1359
|
await rm(dshHomePath(DATA_DIR, HOLIDAY_CACHE_DIR, entry), {
|
|
1349
1360
|
recursive: true,
|
|
1350
1361
|
force: true
|
|
@@ -1701,10 +1712,6 @@ async function handleRpcMethod(ctx, endpoint, payload) {
|
|
|
1701
1712
|
ok: true,
|
|
1702
1713
|
value: await fetchImageUrl(slot, payload?.url ?? null)
|
|
1703
1714
|
};
|
|
1704
|
-
case "fetchPresetConfig": return {
|
|
1705
|
-
ok: true,
|
|
1706
|
-
value: await fetchPresetConfig()
|
|
1707
|
-
};
|
|
1708
1715
|
case "fetchPresetImage":
|
|
1709
1716
|
if (slot === null) return {
|
|
1710
1717
|
ok: true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-background-by-model",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Appearance plugin for DeepSeek Harness: an ordered list of model rules, each carrying its own wallpapers — every image with its own framing and theme color — plus layout mode, opacity, brightness and blur; the background follows the model you switch to, and a rule can rotate through several images on its own rhythm.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|