dsh-background-by-model 0.9.1 → 0.9.2
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 +11 -1
- package/CHANGELOG.zh.md +11 -1
- package/README.md +24 -5
- package/README.zh.md +24 -5
- package/lib/client.js +229 -2
- package/lib/index.js +201 -1
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
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.9.
|
|
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.9.2 / v0.9.1).
|
|
4
|
+
|
|
5
|
+
## v0.9.2
|
|
6
|
+
|
|
7
|
+
- **The Config tab reports how many unreferenced images there are, and clears them** — images are stored at full fidelity, so an interrupted upload, a failed file deletion or a wholesale replacement can leave a `modelbg-<slot>` file that no rule references. That residue is invisible by construction: boot reads the slots the config names, so an orphaned file is never loaded, never painted and never reported anywhere.
|
|
8
|
+
- **Read first, delete second** — the card prints how much is cleanable and how many files that is, and only then offers the button; the figures describe what the press would free, not the store's total (a total file count reads as "delete all eight"). The sweep re-reads afterwards rather than subtracting, so a file that failed to delete still counts, and the toast names any that resisted.
|
|
9
|
+
- **The sweep asks nothing** — one press deletes. What goes is by definition referenced by no rule, and a "confirm?" here only teaches people to click through; the safety moved into the **gate** (an unreadable config disables the button). The numbers are still read first — announcing a size after deleting is the cleanup nobody would agree to.
|
|
10
|
+
- **A `modelbg-h-*` file is prunable** — that slot belongs to no config (the festival art lives in `holiday-cache/`, and the slot is read-only), so this is the only path that can ever remove one. `referencedSlots` deliberately takes RULES only, and `check:store` pins the call site so a future edit cannot add the holiday entries back.
|
|
11
|
+
- **An older host reads as "unknown", not "0 B"** — the node half answers two new methods (`storeUsage`, `pruneOrphans`), and a bundle talking to a node half from before them shows "restart DSH" instead of claiming a measurement nobody made.
|
|
12
|
+
- **`check:store`** — the two decisions (`referencedSlots` / `orphanSlots`) on plain node: a picture in use is never an orphan, `keep` only ever spares a slot (the gap between an upload's bytes and the rule entry that names them), and the client sends rule slots only. `check:node` covers the sweep through the real RPC handler.
|
|
13
|
+
- **The READMEs catch up with the checks** — they had been claiming ten since v0.8.1's `check:watchdogs` and `check:scheme` landed; both are now listed, as is `check:store`.
|
|
4
14
|
|
|
5
15
|
## v0.9.1
|
|
6
16
|
|
package/CHANGELOG.zh.md
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
# 更新日志
|
|
2
2
|
|
|
3
|
-
> 这里保留完整的历史记录;[README.zh.md](./README.zh.md#近期优化) 的「近期优化」一节只列最近两个版本(v0.9.
|
|
3
|
+
> 这里保留完整的历史记录;[README.zh.md](./README.zh.md#近期优化) 的「近期优化」一节只列最近两个版本(v0.9.2 / v0.9.1)。
|
|
4
|
+
|
|
5
|
+
## v0.9.2
|
|
6
|
+
|
|
7
|
+
- **「配置」页会报出有多少没人引用的图片,并把它们清掉** —— 图片按原图保存,所以一次被中断的上传、一次失败的文件删除或一次整批替换,都可能在磁盘上留下一个没有任何规则引用的 `modelbg-<slot>` 文件。这种残留天生不可见:启动只读配置里点名的槽位,所以孤儿文件从来不会被加载、被画出、也不会出现在任何地方。
|
|
8
|
+
- **先读、后删** —— 卡片先显示「可清理多少 MB / 几个文件」,然后才给出按钮;数字只描述这一按会释放什么(不是「正在使用多少」或总共几个文件 —— 后者会被读成「八个都要删」)。清理完重新读一次而不是用减法,所以没删掉的文件仍然算在里面,提示里也会点名是哪几个。
|
|
9
|
+
- **清理不问确认** —— 点一下就删。要删的文件按定义不被任何规则引用,而「确认」在这里只教人不看就点;安全性改放在**前置判断**上:配置读不出来时按钮是灰的。数字仍然是先读的 —— 先删后报数才是没人会同意的那个操作。
|
|
10
|
+
- **`modelbg-h-*` 可以被清掉** —— 那个槽位不属于任何配置(节日画在 `holiday-cache/`,槽位只读),所以这是唯一能删掉它的路径。`referencedSlots` 刻意只接**规则**,`check:store` 钉住调用点,免得以后的改动把节日条目加回去。
|
|
11
|
+
- **旧版宿主编译出来是「读不到」,不是「0 B」** —— node 半边新增两个方法(`storeUsage`、`pruneOrphans`),而对着旧 node 半边的浏览器半边会显示「重启 DSH」,而不是报一个没人量过的数字。
|
|
12
|
+
- **`check:store`** —— 两个判定(`referencedSlots` / `orphanSlots`)在纯 node 上跑:正在用的图永远不会是孤儿、`keep` 只可能**放过**槽位(上传的字节落地与命名它的规则条目之间的那段空隙)、以及客户端只送规则槽位。`check:node` 通过真实 RPC 处理器覆盖清理本身。
|
|
13
|
+
- **两份 README 追上校验的条数** —— 从 v0.8.1 的 `check:watchdogs` 与 `check:scheme` 起它们就一直写着「十道」;现在都列出来了,`check:store` 也在内。
|
|
4
14
|
|
|
5
15
|
## v0.9.1
|
|
6
16
|
|
package/README.md
CHANGED
|
@@ -130,9 +130,20 @@ One click on the Config tab downloads the author's own configuration — rules,
|
|
|
130
130
|
|
|
131
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`.
|
|
132
132
|
- **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
|
-
- **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
|
|
133
|
+
- **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
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.
|
|
135
135
|
|
|
136
|
+
### Image storage cleanup
|
|
137
|
+
|
|
138
|
+
The last card on the Config tab. Images are stored at **full fidelity**, so an interrupted upload, a failed file deletion or a wholesale config replacement can leave a file on disk that no rule references any more — and on a tidy machine that number is normally **0**. The card reads the numbers, and the button deletes **without asking**.
|
|
139
|
+
|
|
140
|
+
- **Read first, delete second** — it shows how much is cleanable and how many files that is, before it offers the button. A cleanup whose size is announced afterwards is a cleanup nobody agrees to. The numbers describe only **what the press would free**: "2.1 MB in use" answers a question nobody asked on this card, and a total file count reads as "eight to delete" when the honest answer is one.
|
|
141
|
+
- **Only unreferenced files** — every byte a current rule uses is untouched, and the config itself is never written. The card re-reads after the sweep instead of subtracting, so a file that failed to delete still counts.
|
|
142
|
+
- **Failures are named** — one undeletable file does not protect the other six, and it is not reported as success either: the toast lists which ones.
|
|
143
|
+
- **No confirmation step** — what it deletes is by definition referenced by no rule, and is not a choice made from a working set, so a "confirm?" here only teaches people to click through. The safety is in the **gate**: when the config cannot be read the button is disabled (next bullet), rather than seven files being listed and then asked about.
|
|
144
|
+
- **It also clears a stray `modelbg-h-*`** — that slot belongs to no config (festival art lives in `holiday-cache/`, and the slot is read-only), so this is the only path that can ever remove such a file.
|
|
145
|
+
- **An older host reads as "unknown", not "0 B"** — the two nothings must not look the same.
|
|
146
|
+
|
|
136
147
|
## Settings
|
|
137
148
|
|
|
138
149
|
Settings → **Theme** now has three tabs:
|
|
@@ -141,7 +152,7 @@ Settings → **Theme** now has three tabs:
|
|
|
141
152
|
| --- | --- | --- |
|
|
142
153
|
| **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 |
|
|
143
154
|
| **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) |
|
|
144
|
-
| **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 |
|
|
155
|
+
| **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) |
|
|
145
156
|
|
|
146
157
|
The old **Color** tab is gone — the theme color is now a property of each rule — and the old **Background** tab became **Model Background**.
|
|
147
158
|
|
|
@@ -184,7 +195,7 @@ npx @deepseek-ai/dsh web
|
|
|
184
195
|
git clone https://github.com/HarmlessFunny/dsh-background-by-model.git
|
|
185
196
|
cd dsh-background-by-model
|
|
186
197
|
pnpm install
|
|
187
|
-
pnpm test # builds lib/, typechecks, runs the
|
|
198
|
+
pnpm test # builds lib/, typechecks, runs the thirteen checks
|
|
188
199
|
```
|
|
189
200
|
|
|
190
201
|
`pnpm run bundle` rebuilds `lib/` on its own. Either has to run before the plugin can load: a fresh clone has no `lib/`.
|
|
@@ -206,7 +217,7 @@ To mount a working copy into a profile instead, add it to the profile's `package
|
|
|
206
217
|
|
|
207
218
|
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.
|
|
208
219
|
|
|
209
|
-
`pnpm test` builds, typechecks and runs
|
|
220
|
+
`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`:
|
|
210
221
|
|
|
211
222
|
- **`check:holiday`** — the two holiday windows, the Beijing day boundary, leap eighth months across 1900–2100, and the four gates `pickHoliday` applies.
|
|
212
223
|
- **`check:rotation`** — `nextIndex` / `isRotating`: no walking off the end, shuffle never repeating the image on screen, and nothing called "rotating" with nothing to rotate.
|
|
@@ -217,7 +228,10 @@ After changing anything under `src/`, run `pnpm run bundle` again — the mounte
|
|
|
217
228
|
- **`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.
|
|
218
229
|
- **`check:preset`** — the recommended profile's retry policy and what a failed attempt holds for the next one, with both halves shown using them.
|
|
219
230
|
- **`check:ui`** — both dictionaries carrying the same keys, every `dab-…` class styled, no dead entry, and every `var(--dsw-…)` published by the installed theme.
|
|
220
|
-
- **`check:
|
|
231
|
+
- **`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.
|
|
232
|
+
- **`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.
|
|
233
|
+
- **`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.
|
|
234
|
+
- **`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 lift, the global switch effect, the store's usage report and cleanup, and the recommended profile end to end.
|
|
221
235
|
|
|
222
236
|
## FAQ
|
|
223
237
|
|
|
@@ -270,6 +284,11 @@ The effect is **Instant**, or the duration is `0` — an effect and a duration a
|
|
|
270
284
|
|
|
271
285
|
> Only the two most recent releases are listed here; older ones live in [CHANGELOG.md](./CHANGELOG.md).
|
|
272
286
|
|
|
287
|
+
### v0.9.2
|
|
288
|
+
|
|
289
|
+
- **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.
|
|
290
|
+
- **`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.
|
|
291
|
+
|
|
273
292
|
### v0.9.1
|
|
274
293
|
|
|
275
294
|
- **The three wallpaper sliders are now one slider plus a picker** — the options read Image opacity / Image brightness / Image blur, the picker matches the buttons, and the hint below is gone.
|
package/README.zh.md
CHANGED
|
@@ -130,9 +130,20 @@
|
|
|
130
130
|
|
|
131
131
|
- **托管在数据目录自己的形状里**(`preset/preset.json`、`preset/theme-config.json`,每张壁纸一个 `modelbg-<slot>` 文件),所以它能在 GitHub 上直接编辑,发布它就是一次 `cp`。
|
|
132
132
|
- **里面每一个界面数字都是插件本来就用着的值**,所以在新装上是「加上规则和图片」;而在你已经调过的数据目录上,它把这些也一起重置 —— 它是一整份配置,不是一个补丁。
|
|
133
|
-
- **先下载、后提问**,所以确认框能如实说出手里有什么 ——
|
|
133
|
+
- **先下载、后提问**,所以确认框能如实说出手里有什么 —— 而清空在下载**之后**,绝不在之前。它是面板里唯一一张会**先问再删**的卡片。
|
|
134
134
|
- **少一张壁纸就整体失败并报出是哪个槽位**,而且每个文件各自重试,已经下到的部分留给下一次。
|
|
135
135
|
|
|
136
|
+
### 图片存储清理
|
|
137
|
+
|
|
138
|
+
「配置」页最后一张卡片:图片是按**原图**保存的,所以一次被中断的上传、一次失败的文件删除,或一次整套配置的替换,都可能在磁盘上留下不再被任何规则引用的文件 —— 而这台机器上「孤儿」正常是 **0**。卡片把用量读出来,**点清理就直接删**。
|
|
139
|
+
|
|
140
|
+
- **先读、后删** —— 卡片先显示「可清理多少 MB / 几个文件」,再给出清理按钮;先删再报数的操作没人会同意。数字只描述**这一按会释放什么**:显示「正在使用 2.1 MB」是在回答一个没人问的问题,而显示「8 个文件」会让人以为八张全要删掉。
|
|
141
|
+
- **只删没人引用的** —— 当前规则用到的图一个字节都不动,配置本身也不写。清理完会重新读一次,而不是用减法算,所以删除失败的文件仍然算在里面。
|
|
142
|
+
- **失败会点名** —— 一个文件删不掉不会让其余六个留下来,但也不会被说成「已清理」:提示里会列出是哪几个。
|
|
143
|
+
- **不问确认** —— 要删的东西按定义没有任何规则引用,而且不是从一个能用的集合里挑出来的,所以「确认」在这里只教人不看就点。安全性放在**前置判断**上:配置读不出来时按钮是灰的(见下一条),而不是先把七个文件列出来再问一遍。
|
|
144
|
+
- **顺手清掉误入槽位的节日文件** —— `modelbg-h-*` 不属于任何配置(节日画在 `holiday-cache/` 里,那个槽位只读),所以它是唯一一个只有这条路径能清掉的残留。
|
|
145
|
+
- **宿主是旧版时显示「读不到」而不是「0 B」** —— 两种「没有」必须长得不一样。
|
|
146
|
+
|
|
136
147
|
## 设置界面
|
|
137
148
|
|
|
138
149
|
设置的**「主题」**分类下现在有三个选项卡:
|
|
@@ -141,7 +152,7 @@
|
|
|
141
152
|
| --- | --- | --- |
|
|
142
153
|
| **界面** | 全局,所有模型共用 | 主背景、左侧栏、右侧栏、卡片面板、输入框与控件、设置面板、对话文本框、轨迹页与每一个插件页的透明度 |
|
|
143
154
|
| **模型背景** | 每条规则、每张图片 | 有序规则列表、实时匹配状态,以及每条规则的图片(胶片条)、**逐图布局模式 / 取景 / 主题色**,加上规则自己的透明度、亮度、模糊度与[多图轮换](#多图轮换) |
|
|
144
|
-
| **配置** | — | 全局[切换效果](#切换效果全局)、整套规则的进出 —— [推荐配置](#推荐配置)与导入 / 导出(`dsh-background-by-model-theme.json`)—— 外加[节日特殊背景](#节日特殊背景)
|
|
155
|
+
| **配置** | — | 全局[切换效果](#切换效果全局)、整套规则的进出 —— [推荐配置](#推荐配置)与导入 / 导出(`dsh-background-by-model-theme.json`)—— 外加[节日特殊背景](#节日特殊背景)那一个开关,以及[图片存储清理](#图片存储清理) |
|
|
145
156
|
|
|
146
157
|
原先的**「色彩」**选项卡已删除 —— 主题色改为规则与图片各自的属性 —— 原**「背景」**选项卡改造成**「模型背景」**。
|
|
147
158
|
|
|
@@ -184,7 +195,7 @@ npx @deepseek-ai/dsh web
|
|
|
184
195
|
git clone https://github.com/HarmlessFunny/dsh-background-by-model.git
|
|
185
196
|
cd dsh-background-by-model
|
|
186
197
|
pnpm install
|
|
187
|
-
pnpm test # 构建 lib
|
|
198
|
+
pnpm test # 构建 lib/、类型检查、跑十三道检查
|
|
188
199
|
```
|
|
189
200
|
|
|
190
201
|
`pnpm run bundle` 只重建 `lib/`。两者都必须在插件能加载之前先跑一次:新 clone 出来没有 `lib/`。
|
|
@@ -206,7 +217,7 @@ pnpm test # 构建 lib/、类型检查、跑十道检查
|
|
|
206
217
|
|
|
207
218
|
改完 `src/` 必须重新执行 `pnpm run bundle` 才会生效 —— 挂载的 profile 加载的是 `lib/`。
|
|
208
219
|
|
|
209
|
-
`pnpm test`
|
|
220
|
+
`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`:
|
|
210
221
|
|
|
211
222
|
- **`check:holiday`** —— 两个节日窗口、北京时间的跨日边界、1900–2100 年间的闰八月,以及 `pickHoliday` 的四道闸门。
|
|
212
223
|
- **`check:rotation`** —— `nextIndex` / `isRotating`:不走越界、随机不重复当前这张、没有可轮换的东西时绝不报「正在轮换」。
|
|
@@ -217,7 +228,10 @@ pnpm test # 构建 lib/、类型检查、跑十道检查
|
|
|
217
228
|
- **`check:rules`** —— 选项卡背后那两个住在 `src/client/rules-view.ts` 的判定,以及布局所依赖的页面结构。
|
|
218
229
|
- **`check:preset`** —— 推荐配置的重试策略与失败那一次给下一次留下什么,并验证两侧真的在用它们。
|
|
219
230
|
- **`check:ui`** —— 两本字典键集一致、每个 `dab-…` 类都有样式、没有死键,以及每个 `var(--dsw-…)` 都是已安装主题发布过的 token。
|
|
220
|
-
- **`check:
|
|
231
|
+
- **`check:watchdogs`** —— 一条心跳、按各自到期时间触发、隐藏即暂停、dispose 不留定时器,且入口之外没有模块自己起 interval。
|
|
232
|
+
- **`check:scheme`** —— `data-ds-dark-theme` 的交接:插件只改自己写的那个值、清掉 token 时一起交还标记、方案变了会重绘。
|
|
233
|
+
- **`check:store`** —— 存储清理的两个判定:`referencedSlots` 与 `orphanSlots`。钉住「正在用的图不会被当成孤儿」与「节日槽位的文件必须可清」,外加浏览器半边传的是**规则槽位**而不是整份配置。
|
|
234
|
+
- **`check:node`** —— 把构建好的 node 半边通过真实的 RPC 处理器跑一遍(`DSH_HOME` 指向临时目录、`fetch` 换成桩):节日配置块、节日画的缓存与清理、镜像回落、逐图颜色抬升、全局切换效果、图片存储的用量报告与清理,以及推荐配置的整条链路。
|
|
221
235
|
|
|
222
236
|
## 常见问题
|
|
223
237
|
|
|
@@ -270,6 +284,11 @@ pnpm test # 构建 lib/、类型检查、跑十道检查
|
|
|
270
284
|
|
|
271
285
|
> 这里只列最近两个版本;更早的版本见 [CHANGELOG.zh.md](./CHANGELOG.zh.md)。
|
|
272
286
|
|
|
287
|
+
### v0.9.2
|
|
288
|
+
|
|
289
|
+
- **「配置」页多了一张「图片存储」卡片** —— 先报出可清理多少、涉及几个文件,再给你一个清理按钮;只删没有规则引用的文件,配置与节日画都不动,删不掉的文件会点名。
|
|
290
|
+
- **`check:store`,于是现在十三道校验** —— 两份 README 此前一直写着「十道」,`check:watchdogs` 与 `check:scheme` 从 v0.8.1 起就没列进去,这次一并补上。
|
|
291
|
+
|
|
273
292
|
### v0.9.1
|
|
274
293
|
|
|
275
294
|
- **三条壁纸滑块合并成一条,旁边用下拉框切换** —— 选项改叫图片透明度 / 图片亮度 / 图片模糊,下拉框与按钮同色,滑块下的说明文字删掉。
|
package/lib/client.js
CHANGED
|
@@ -1167,6 +1167,50 @@ window.__ModuleLoader__.load({
|
|
|
1167
1167
|
const parts = [d.provider, d.model].filter((s) => typeof s === "string" && s !== "");
|
|
1168
1168
|
return parts.length > 0 ? parts.join(" ") : null;
|
|
1169
1169
|
}
|
|
1170
|
+
/**
|
|
1171
|
+
* What the store occupies and what nothing points at — read-only.
|
|
1172
|
+
*
|
|
1173
|
+
* `keep` is this bundle's live slot list, which the node half unions with the config
|
|
1174
|
+
* on disk (see `storeUsage`): the two are written at different moments, and the
|
|
1175
|
+
* report must never call a file an orphan in the gap between an upload's bytes
|
|
1176
|
+
* landing and its rule entry arriving. Sending the list can therefore only SPARE a
|
|
1177
|
+
* slot, never condemn one.
|
|
1178
|
+
*/
|
|
1179
|
+
async function readStoreUsage(keep) {
|
|
1180
|
+
const data = await rpcCall("storeUsage", { keep });
|
|
1181
|
+
if (data === null || typeof data !== "object") return null;
|
|
1182
|
+
const d = data;
|
|
1183
|
+
return {
|
|
1184
|
+
totalBytes: numberOr(d.totalBytes, 0),
|
|
1185
|
+
usedBytes: numberOr(d.usedBytes, 0),
|
|
1186
|
+
orphanBytes: numberOr(d.orphanBytes, 0),
|
|
1187
|
+
files: numberOr(d.files, 0),
|
|
1188
|
+
slots: Array.isArray(d.slots) ? d.slots.filter((s) => typeof s === "string") : [],
|
|
1189
|
+
orphans: Array.isArray(d.orphans) ? d.orphans.filter((o) => o !== null && typeof o === "object" && typeof o.slot === "string").map((o) => ({
|
|
1190
|
+
slot: o.slot,
|
|
1191
|
+
bytes: numberOr(o.bytes, 0)
|
|
1192
|
+
})) : [],
|
|
1193
|
+
trusted: d.trusted === true
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Delete every stored image nothing points at. The one destructive half of the
|
|
1198
|
+
* pair, and it takes the same `keep` for the same reason.
|
|
1199
|
+
*/
|
|
1200
|
+
async function pruneOrphanImages(keep) {
|
|
1201
|
+
const data = await rpcCall("pruneOrphans", { keep });
|
|
1202
|
+
if (data === null || typeof data !== "object") return null;
|
|
1203
|
+
const d = data;
|
|
1204
|
+
return {
|
|
1205
|
+
removed: Array.isArray(d.removed) ? d.removed.filter((s) => typeof s === "string") : [],
|
|
1206
|
+
failed: Array.isArray(d.failed) ? d.failed.filter((s) => typeof s === "string") : [],
|
|
1207
|
+
freedBytes: numberOr(d.freedBytes, 0),
|
|
1208
|
+
refused: d.refused === true
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
function numberOr(value, fallback) {
|
|
1212
|
+
return typeof value === "number" && isFinite(value) && value >= 0 ? value : fallback;
|
|
1213
|
+
}
|
|
1170
1214
|
//#endregion
|
|
1171
1215
|
//#region src/client/utils/color.ts
|
|
1172
1216
|
function hsvToHsl(h, s, v) {
|
|
@@ -1927,6 +1971,13 @@ window.__ModuleLoader__.load({
|
|
|
1927
1971
|
.dab-profile-ico{width:38px;height:38px;border-radius:11px;display:grid;place-items:center;margin-bottom:13px;color:var(--dsw-alias-brand-primary);background:var(--dsw-alias-bg-layer-2);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 12%,transparent)}
|
|
1928
1972
|
.dab-profile-title{font-size:14px;font-weight:650}
|
|
1929
1973
|
.dab-profile-desc{font-size:12px;color:var(--dsw-alias-label-tertiary);line-height:1.55;margin:5px 0 15px}
|
|
1974
|
+
/* The image store's three numbers. A row of VALUES with a caption under each,
|
|
1975
|
+
not a sentence: they are read as a measurement ("how much is orphaned"), and a
|
|
1976
|
+
sentence makes the reader hunt for the one number that matters. */
|
|
1977
|
+
.dab-store-stats{display:flex;gap:16px;margin:2px 0 14px}
|
|
1978
|
+
.dab-store-stat{display:flex;flex-direction:column;gap:2px;min-width:0}
|
|
1979
|
+
.dab-store-stat-v{font-size:14px;font-weight:650;font-family:var(--dab-mono);letter-spacing:.01em;color:var(--dsw-alias-label-primary)}
|
|
1980
|
+
.dab-store-stat-k{font-size:11px;color:var(--dsw-alias-label-tertiary);white-space:nowrap}
|
|
1930
1981
|
.dab-footer{margin-top:6px;padding:14px 4px 0;border-top:1px solid var(--dsw-alias-border-l2);display:flex;align-items:center;justify-content:space-between;font-size:11px;color:var(--dsw-alias-label-tertiary)}
|
|
1931
1982
|
.dab-footer-mono{font-family:var(--dab-mono);letter-spacing:.02em}
|
|
1932
1983
|
/* Same reset as the brand: the package name links to the repository, and the
|
|
@@ -4248,6 +4299,11 @@ window.__ModuleLoader__.load({
|
|
|
4248
4299
|
className,
|
|
4249
4300
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M7.2 1.8l1.2 3.1 3.1 1.2-3.1 1.2-1.2 3.1-1.2-3.1L2.9 6.1 6 4.9l1.2-3.1z" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M12 10.5l.5 1.2 1.2.5-1.2.5-.5 1.2-.5-1.2-1.2-.5 1.2-.5.5-1.2z" })]
|
|
4250
4301
|
});
|
|
4302
|
+
const RefreshIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Glyph, {
|
|
4303
|
+
size,
|
|
4304
|
+
className,
|
|
4305
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M13.2 8A5.2 5.2 0 1 1 11.5 4.2" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M13.4 1.8v2.9h-2.9" })]
|
|
4306
|
+
});
|
|
4251
4307
|
const DownloadIcon = ({ size, className }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Glyph, {
|
|
4252
4308
|
size,
|
|
4253
4309
|
className,
|
|
@@ -4767,6 +4823,18 @@ window.__ModuleLoader__.load({
|
|
|
4767
4823
|
recFailTooLarge: "这份推荐配置体积过大,未做任何改动",
|
|
4768
4824
|
recFailImage: "这张壁纸没拿到(素材库里缺这个文件):",
|
|
4769
4825
|
recFailApply: "未能写入推荐配置,你原来的配置没有被改动",
|
|
4826
|
+
storeCardTitle: "图片存储",
|
|
4827
|
+
storeCardDesc: "因意外导致的孤儿图片这里可以清掉",
|
|
4828
|
+
storeOrphans: "可清理",
|
|
4829
|
+
storeFiles: "个可删文件",
|
|
4830
|
+
storeReading: "正在读取…",
|
|
4831
|
+
storeUnknown: "读不到存储信息(宿主进程可能是旧版插件,重启 DSH 后可用)",
|
|
4832
|
+
storeCleanNone: "没有多余的文件",
|
|
4833
|
+
storeClean: "清理",
|
|
4834
|
+
storeDoneFreed: "已清理,释放",
|
|
4835
|
+
storePartial: "有些文件没能删除:",
|
|
4836
|
+
storeRefused: "配置文件读不出来(theme-config.json 解析失败),现在分不清哪些图片还在用,所以没有删除任何东西。修好或删掉那个文件后重启 DSH 再试",
|
|
4837
|
+
storeRefresh: "重新读取",
|
|
4770
4838
|
footerTag: "按模型换背景插件",
|
|
4771
4839
|
repoLink: "在 GitHub 上打开仓库",
|
|
4772
4840
|
holidayTitle: "节日特殊背景",
|
|
@@ -4938,6 +5006,18 @@ window.__ModuleLoader__.load({
|
|
|
4938
5006
|
recFailTooLarge: "The recommended profile is too large. Nothing was changed.",
|
|
4939
5007
|
recFailImage: "Could not fetch this wallpaper — the asset host has no such file:",
|
|
4940
5008
|
recFailApply: "Could not write the recommended profile — your own is untouched",
|
|
5009
|
+
storeCardTitle: "Image storage",
|
|
5010
|
+
storeCardDesc: "Orphaned image files left behind by an accident can be cleared here",
|
|
5011
|
+
storeOrphans: "Cleanable",
|
|
5012
|
+
storeFiles: "deletable files",
|
|
5013
|
+
storeReading: "Reading…",
|
|
5014
|
+
storeUnknown: "Could not read the store — the host process may be an older build; restart DSH",
|
|
5015
|
+
storeCleanNone: "Nothing to clean up",
|
|
5016
|
+
storeClean: "Clean up",
|
|
5017
|
+
storeDoneFreed: "Cleaned up, freed",
|
|
5018
|
+
storePartial: "Some files could not be deleted: ",
|
|
5019
|
+
storeRefused: "The config file could not be read (theme-config.json does not parse), so nothing on disk can be told apart from an orphan. Nothing was deleted — fix or remove that file and restart DSH",
|
|
5020
|
+
storeRefresh: "Refresh",
|
|
4941
5021
|
footerTag: "Per-model wallpaper plugin",
|
|
4942
5022
|
repoLink: "Open the repository on GitHub",
|
|
4943
5023
|
holidayTitle: "Holiday backgrounds",
|
|
@@ -7316,6 +7396,41 @@ window.__ModuleLoader__.load({
|
|
|
7316
7396
|
});
|
|
7317
7397
|
}
|
|
7318
7398
|
//#endregion
|
|
7399
|
+
//#region src/format.ts
|
|
7400
|
+
/**
|
|
7401
|
+
* Byte counts as a person reads them — the only two sentences this plugin says
|
|
7402
|
+
* about size, in one place so their difference cannot be an accident.
|
|
7403
|
+
*
|
|
7404
|
+
* They are NOT the same question, which is why there are two:
|
|
7405
|
+
*
|
|
7406
|
+
* - a download's size describes ONE file that just arrived;
|
|
7407
|
+
* - a storage figure describes a total that is routinely zero.
|
|
7408
|
+
*
|
|
7409
|
+
* The floor below is right for the first and wrong for the second, and the wrong
|
|
7410
|
+
* answer is not a crash: it is a card that offers to clean up 1 KB and a button
|
|
7411
|
+
* that refuses to, on a store where there is nothing to clean. Neither number is
|
|
7412
|
+
* ever deleted or written — these are display strings — but a maintenance panel
|
|
7413
|
+
* whose numbers disagree with its own button is worse than no panel.
|
|
7414
|
+
*/
|
|
7415
|
+
/**
|
|
7416
|
+
* Size of one download, at the precision a person reads it at.
|
|
7417
|
+
*
|
|
7418
|
+
* Floors at 1 KB on purpose: it is shown beside a file that just arrived, and
|
|
7419
|
+
* "0 KB" beside a picture that downloaded fine reads as a failure.
|
|
7420
|
+
*/
|
|
7421
|
+
const fmtBytes = (n) => n >= 1048576 ? `${(n / 1024 / 1024).toFixed(1)} MB` : `${Math.max(1, Math.round(n / 1024))} KB`;
|
|
7422
|
+
/**
|
|
7423
|
+
* A figure on the storage card, where zero is a real and — on a tidy store — the
|
|
7424
|
+
* usual answer.
|
|
7425
|
+
*
|
|
7426
|
+
* `fmtBytes` would print "1 KB" for it, so the card claimed 1 KB of waste while
|
|
7427
|
+
* the clean button sat disabled (its condition is `orphanBytes === 0`, which was
|
|
7428
|
+
* true all along). Rounding remains conservative in the other direction: a real
|
|
7429
|
+
* orphan is never allowed to read as zero, because a number that cannot go down
|
|
7430
|
+
* is exactly the complaint this feature exists to answer.
|
|
7431
|
+
*/
|
|
7432
|
+
const fmtStoreBytes = (n) => n > 0 ? fmtBytes(n) : "0 KB";
|
|
7433
|
+
//#endregion
|
|
7319
7434
|
//#region src/client/components/pages/ProfilePage.tsx
|
|
7320
7435
|
/** Chip labels, keyed by the value they set — so no chip can be built from a
|
|
7321
7436
|
* value the schema does not accept, and `check:ui` still sees every key used. */
|
|
@@ -7359,8 +7474,6 @@ window.__ModuleLoader__.load({
|
|
|
7359
7474
|
"too many images": "recFailTooMany",
|
|
7360
7475
|
"too large": "recFailTooLarge"
|
|
7361
7476
|
};
|
|
7362
|
-
/** Size of a download, at the precision a person reads it at. */
|
|
7363
|
-
const fmtBytes = (n) => n >= 1048576 ? `${(n / 1024 / 1024).toFixed(1)} MB` : `${Math.max(1, Math.round(n / 1024))} KB`;
|
|
7364
7477
|
function ProfilePage({ p, notify }) {
|
|
7365
7478
|
const { t, exportTheme, importTheme, setTransition } = p;
|
|
7366
7479
|
const importRef = (0, react.useRef)(null);
|
|
@@ -7463,6 +7576,46 @@ window.__ModuleLoader__.load({
|
|
|
7463
7576
|
setPending(null);
|
|
7464
7577
|
notify(ok ? t("recDone") : t("recFailApply"), ok);
|
|
7465
7578
|
};
|
|
7579
|
+
const [usage, setUsage] = (0, react.useState)(null);
|
|
7580
|
+
/** Distinguishes "not asked yet" from "asked, and the host cannot answer". */
|
|
7581
|
+
const [usageRead, setUsageRead] = (0, react.useState)(false);
|
|
7582
|
+
const [storeBusy, setStoreBusy] = (0, react.useState)(false);
|
|
7583
|
+
const refreshStore = async () => {
|
|
7584
|
+
const next = await p.readStoreUsage();
|
|
7585
|
+
setUsage(next);
|
|
7586
|
+
setUsageRead(true);
|
|
7587
|
+
};
|
|
7588
|
+
(0, react.useEffect)(() => {
|
|
7589
|
+
let live = true;
|
|
7590
|
+
(async () => {
|
|
7591
|
+
const next = await p.readStoreUsage();
|
|
7592
|
+
if (!live) return;
|
|
7593
|
+
setUsage(next);
|
|
7594
|
+
setUsageRead(true);
|
|
7595
|
+
})();
|
|
7596
|
+
return () => {
|
|
7597
|
+
live = false;
|
|
7598
|
+
};
|
|
7599
|
+
}, [store.rulesRev]);
|
|
7600
|
+
const onClean = async () => {
|
|
7601
|
+
setStoreBusy(true);
|
|
7602
|
+
const res = await p.pruneOrphanImages();
|
|
7603
|
+
await refreshStore();
|
|
7604
|
+
setStoreBusy(false);
|
|
7605
|
+
if (res === null) {
|
|
7606
|
+
notify(t("storeUnknown"), false);
|
|
7607
|
+
return;
|
|
7608
|
+
}
|
|
7609
|
+
if (res.refused) {
|
|
7610
|
+
notify(t("storeRefused"), false);
|
|
7611
|
+
return;
|
|
7612
|
+
}
|
|
7613
|
+
if (res.failed.length > 0) {
|
|
7614
|
+
notify(`${t("storePartial")}${res.failed.join(", ")}`, false);
|
|
7615
|
+
return;
|
|
7616
|
+
}
|
|
7617
|
+
notify(res.removed.length === 0 ? t("storeCleanNone") : `${t("storeDoneFreed")} ${fmtStoreBytes(res.freedBytes)}`, true);
|
|
7618
|
+
};
|
|
7466
7619
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
7467
7620
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
7468
7621
|
className: "dab-head dab-rise",
|
|
@@ -7745,6 +7898,74 @@ window.__ModuleLoader__.load({
|
|
|
7745
7898
|
}
|
|
7746
7899
|
})
|
|
7747
7900
|
]
|
|
7901
|
+
}),
|
|
7902
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
7903
|
+
className: "dab-card dab-card-hover",
|
|
7904
|
+
children: [
|
|
7905
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
7906
|
+
className: "dab-profile-ico",
|
|
7907
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrashIcon, { size: 17 })
|
|
7908
|
+
}),
|
|
7909
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
7910
|
+
className: "dab-profile-title",
|
|
7911
|
+
children: t("storeCardTitle")
|
|
7912
|
+
}),
|
|
7913
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
7914
|
+
className: "dab-profile-desc",
|
|
7915
|
+
children: t("storeCardDesc")
|
|
7916
|
+
}),
|
|
7917
|
+
usage === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7918
|
+
className: "dab-hint",
|
|
7919
|
+
style: { marginBottom: 12 },
|
|
7920
|
+
children: usageRead ? t("storeUnknown") : t("storeReading")
|
|
7921
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7922
|
+
className: "dab-store-stats",
|
|
7923
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7924
|
+
className: "dab-store-stat",
|
|
7925
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7926
|
+
className: "dab-store-stat-v",
|
|
7927
|
+
children: fmtStoreBytes(usage.orphanBytes)
|
|
7928
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7929
|
+
className: "dab-store-stat-k",
|
|
7930
|
+
children: t("storeOrphans")
|
|
7931
|
+
})]
|
|
7932
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7933
|
+
className: "dab-store-stat",
|
|
7934
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7935
|
+
className: "dab-store-stat-v",
|
|
7936
|
+
children: usage.orphans.length
|
|
7937
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7938
|
+
className: "dab-store-stat-k",
|
|
7939
|
+
children: t("storeFiles")
|
|
7940
|
+
})]
|
|
7941
|
+
})]
|
|
7942
|
+
}), usage.trusted ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7943
|
+
className: "dab-hint dab-stale",
|
|
7944
|
+
style: { marginBottom: 12 },
|
|
7945
|
+
children: t("storeRefused")
|
|
7946
|
+
})] }),
|
|
7947
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7948
|
+
style: {
|
|
7949
|
+
display: "flex",
|
|
7950
|
+
gap: 8,
|
|
7951
|
+
flexWrap: "wrap"
|
|
7952
|
+
},
|
|
7953
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
7954
|
+
type: "button",
|
|
7955
|
+
className: "dab-btn",
|
|
7956
|
+
disabled: storeBusy || usage === null || usage.orphanBytes === 0 || !usage.trusted,
|
|
7957
|
+
title: usage !== null && !usage.trusted ? t("storeRefused") : usage !== null && usage.orphanBytes === 0 ? t("storeCleanNone") : t("storeClean"),
|
|
7958
|
+
onClick: () => void onClean(),
|
|
7959
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrashIcon, { size: 14 }), t("storeClean")]
|
|
7960
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
7961
|
+
type: "button",
|
|
7962
|
+
className: "dab-btn dab-btn-ghost",
|
|
7963
|
+
disabled: storeBusy,
|
|
7964
|
+
onClick: () => void refreshStore(),
|
|
7965
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RefreshIcon, { size: 14 }), t("storeRefresh")]
|
|
7966
|
+
})]
|
|
7967
|
+
})
|
|
7968
|
+
]
|
|
7748
7969
|
})
|
|
7749
7970
|
]
|
|
7750
7971
|
}),
|
|
@@ -8648,6 +8869,12 @@ window.__ModuleLoader__.load({
|
|
|
8648
8869
|
applyRecommended: async (preset) => {
|
|
8649
8870
|
if (!canWriteConfig()) return false;
|
|
8650
8871
|
return await applyThemeFile({ applyActive }, preset, true);
|
|
8872
|
+
},
|
|
8873
|
+
readStoreUsage: () => readStoreUsage(cfg.rules.flatMap(ruleSlots)),
|
|
8874
|
+
pruneOrphanImages: async () => {
|
|
8875
|
+
const res = await pruneOrphanImages(cfg.rules.flatMap(ruleSlots));
|
|
8876
|
+
for (const slot of res?.removed ?? []) setImage(slot, null);
|
|
8877
|
+
return res;
|
|
8651
8878
|
}
|
|
8652
8879
|
};
|
|
8653
8880
|
};
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { access, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
1
|
+
import { access, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dshHomePath } from "@deepseek-ai/dsh-home-paths";
|
|
3
3
|
//#region src/holiday.ts
|
|
4
4
|
const HOLIDAYS = [{
|
|
@@ -464,6 +464,41 @@ function presetRetryDelayMs(failedAttempt) {
|
|
|
464
464
|
return 400 * 2 ** Math.max(0, failedAttempt - 1);
|
|
465
465
|
}
|
|
466
466
|
//#endregion
|
|
467
|
+
//#region src/store.ts
|
|
468
|
+
/**
|
|
469
|
+
* Every slot the config still points at, in first-seen order and without repeats.
|
|
470
|
+
*
|
|
471
|
+
* Rules only. A holiday's slot is deliberately NOT included: its art lives in
|
|
472
|
+
* `holiday-cache/`, it is never written to the slot namespace, and a
|
|
473
|
+
* `modelbg-h-*` file therefore cannot belong to anything (see the read-only
|
|
474
|
+
* invariant in MAINTAINING §7.1). Counting the slot as referenced would make such
|
|
475
|
+
* a file permanently unprunable — the one residue this cleanup could never clear.
|
|
476
|
+
*/
|
|
477
|
+
function referencedSlots(rules) {
|
|
478
|
+
const seen = /* @__PURE__ */ new Set();
|
|
479
|
+
const out = [];
|
|
480
|
+
for (const rule of rules) for (const image of rule.images) {
|
|
481
|
+
if (typeof image.slot !== "string" || seen.has(image.slot)) continue;
|
|
482
|
+
seen.add(image.slot);
|
|
483
|
+
out.push(image.slot);
|
|
484
|
+
}
|
|
485
|
+
return out;
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* The files whose slot appears in neither `referenced` nor `kept`.
|
|
489
|
+
*
|
|
490
|
+
* `stored` order is preserved because the caller deletes in it and reports what it
|
|
491
|
+
* removed in that order, which is the only order the user has ever seen these files
|
|
492
|
+
* in. `kept` exists for one reason: the browser half's live config can be a few
|
|
493
|
+
* hundred milliseconds ahead of its own write to disk (a slot is written, then the
|
|
494
|
+
* config that names it), and an image whose bytes have landed but whose rule entry
|
|
495
|
+
* has not must not be deleted in that window.
|
|
496
|
+
*/
|
|
497
|
+
function orphanSlots(stored, referenced, kept = []) {
|
|
498
|
+
const live = /* @__PURE__ */ new Set([...referenced, ...kept]);
|
|
499
|
+
return stored.filter((slot) => !live.has(slot));
|
|
500
|
+
}
|
|
501
|
+
//#endregion
|
|
467
502
|
//#region src/index.ts
|
|
468
503
|
/**
|
|
469
504
|
* Node half of dsh-background-by-model: file-backed rule persistence.
|
|
@@ -674,6 +709,61 @@ async function readConfig() {
|
|
|
674
709
|
}
|
|
675
710
|
return normalizeConfig(await migrateLegacy(raw));
|
|
676
711
|
}
|
|
712
|
+
/**
|
|
713
|
+
* The config, plus whether it is safe to decide what the store still owns from it.
|
|
714
|
+
*
|
|
715
|
+
* `readConfig` deliberately swallows a corrupt file and answers with defaults —
|
|
716
|
+
* right for painting (a broken file must not leave a blank panel) and WRONG for
|
|
717
|
+
* the cleanup, which would then see zero referenced slots and treat every image on
|
|
718
|
+
* disk as an orphan. That is the one way this feature could delete a user's whole
|
|
719
|
+
* wallpaper collection, so the two cases are told apart here and the sweep refuses
|
|
720
|
+
* rather than guesses:
|
|
721
|
+
*
|
|
722
|
+
* - no file at all → trusted. A fresh store has images only if something wrote
|
|
723
|
+
* them, and nothing did;
|
|
724
|
+
* - a file that parses to an object → trusted (its rules are what they say);
|
|
725
|
+
* - a file that is there but does not parse, or is not an object → NOT trusted.
|
|
726
|
+
*
|
|
727
|
+
* A config whose `rules` are missing entirely is trusted: `normalizeConfig` seeds
|
|
728
|
+
* one blank rule for that case, which owns no slot, and the sanitizer's answer is
|
|
729
|
+
* exactly what a reload would paint.
|
|
730
|
+
*
|
|
731
|
+
* The untrusted branch deliberately does NOT go through `migrateLegacy`: that one
|
|
732
|
+
* WRITES a repaired config back to disk for the shapes earlier releases left
|
|
733
|
+
* behind, and running it here would turn "refuse to delete" into "quietly rewrite
|
|
734
|
+
* whatever the user's broken file said, then delete against the rewrite". The
|
|
735
|
+
* display answer for an unreadable file is the sanitizer's defaults, computed
|
|
736
|
+
* without touching the disk.
|
|
737
|
+
*/
|
|
738
|
+
async function readConfigForSweep() {
|
|
739
|
+
await ensureDir();
|
|
740
|
+
let text;
|
|
741
|
+
try {
|
|
742
|
+
text = await readFile(configPath(), "utf8");
|
|
743
|
+
} catch {
|
|
744
|
+
return {
|
|
745
|
+
config: await readConfig(),
|
|
746
|
+
trusted: true
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
let raw;
|
|
750
|
+
try {
|
|
751
|
+
raw = JSON.parse(text);
|
|
752
|
+
} catch {
|
|
753
|
+
return {
|
|
754
|
+
config: normalizeConfig({}),
|
|
755
|
+
trusted: false
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return {
|
|
759
|
+
config: normalizeConfig({}),
|
|
760
|
+
trusted: false
|
|
761
|
+
};
|
|
762
|
+
return {
|
|
763
|
+
config: normalizeConfig(await migrateLegacy(raw)),
|
|
764
|
+
trusted: true
|
|
765
|
+
};
|
|
766
|
+
}
|
|
677
767
|
const LEGACY_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
678
768
|
"color",
|
|
679
769
|
"bgMode",
|
|
@@ -1159,6 +1249,106 @@ async function resetStore() {
|
|
|
1159
1249
|
return ok;
|
|
1160
1250
|
}
|
|
1161
1251
|
/**
|
|
1252
|
+
* What the image store occupies, and which of it nothing points at any more.
|
|
1253
|
+
*
|
|
1254
|
+
* Read-only: it reports, it never deletes. Splitting the two is what lets the panel
|
|
1255
|
+
* show a number before asking, and it is also the only honest way to answer "how
|
|
1256
|
+
* much would this free" — a cleanup whose size is announced AFTER the fact is a
|
|
1257
|
+
* cleanup nobody agrees to.
|
|
1258
|
+
*
|
|
1259
|
+
* `keep` is the browser half's own slot list. It is unioned with the config ON DISK
|
|
1260
|
+
* rather than replacing it because the two are not written at the same moment: an
|
|
1261
|
+
* upload writes the bytes first and the config that names them a moment later, so a
|
|
1262
|
+
* read landing in between would see a file the store is about to legitimately own.
|
|
1263
|
+
* The union can only ever make the report SMALLER (a slot is spared), which is the
|
|
1264
|
+
* safe direction for something whose answer is "delete these files".
|
|
1265
|
+
*
|
|
1266
|
+
* A file that cannot be stat'd is still listed, at 0 bytes: it exists as far as
|
|
1267
|
+
* `readdir` is concerned, and hiding it would leave a residue the panel claims is
|
|
1268
|
+
* not there.
|
|
1269
|
+
*
|
|
1270
|
+
* `trusted` is false when the config file on disk is there but unreadable (see
|
|
1271
|
+
* `readConfigForSweep`). The numbers are then still reported — the user asked what
|
|
1272
|
+
* the store occupies, and that question has an answer — but the caller must not
|
|
1273
|
+
* delete: with no trustworthy rule list, EVERY file would look like an orphan.
|
|
1274
|
+
*/
|
|
1275
|
+
async function storeUsage(keep) {
|
|
1276
|
+
const stored = await listSlots();
|
|
1277
|
+
const { config, trusted } = await readConfigForSweep();
|
|
1278
|
+
const orphans = orphanSlots(stored, referencedSlots(config.rules), keep);
|
|
1279
|
+
const sizes = /* @__PURE__ */ new Map();
|
|
1280
|
+
let totalBytes = 0;
|
|
1281
|
+
for (const slot of stored) {
|
|
1282
|
+
let bytes = 0;
|
|
1283
|
+
try {
|
|
1284
|
+
bytes = (await stat(imagePath(slot))).size;
|
|
1285
|
+
} catch {}
|
|
1286
|
+
sizes.set(slot, bytes);
|
|
1287
|
+
totalBytes += bytes;
|
|
1288
|
+
}
|
|
1289
|
+
let orphanBytes = 0;
|
|
1290
|
+
for (const slot of orphans) orphanBytes += sizes.get(slot) ?? 0;
|
|
1291
|
+
return {
|
|
1292
|
+
totalBytes,
|
|
1293
|
+
usedBytes: totalBytes - orphanBytes,
|
|
1294
|
+
orphanBytes,
|
|
1295
|
+
files: stored.length,
|
|
1296
|
+
slots: stored,
|
|
1297
|
+
orphans: orphans.map((slot) => ({
|
|
1298
|
+
slot,
|
|
1299
|
+
bytes: sizes.get(slot) ?? 0
|
|
1300
|
+
})),
|
|
1301
|
+
trusted
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
/**
|
|
1305
|
+
* Delete every stored image nothing points at, and report what was removed.
|
|
1306
|
+
*
|
|
1307
|
+
* `keep` is the caller's live slot list, with the same meaning and the same
|
|
1308
|
+
* union-with-disk rule as `storeUsage`. The answer carries the freed byte count so
|
|
1309
|
+
* the panel can say what happened, and `failed` names the slots that resisted —
|
|
1310
|
+
* a cleanup that removed six of seven files and said "done" would leave the user
|
|
1311
|
+
* with a number that never goes down and no way to learn why.
|
|
1312
|
+
*
|
|
1313
|
+
* Failures are per file and never abort the sweep: one locked file must not protect
|
|
1314
|
+
* the other six. Nothing else in the store is touched — the config, the festival art
|
|
1315
|
+
* and the released generations under `holiday-cache/` are all outside this walk.
|
|
1316
|
+
*
|
|
1317
|
+
* REFUSES on an unreadable config (`trusted: false`): every file would look
|
|
1318
|
+
* unreferenced, and this is the one operation in the plugin that could delete a
|
|
1319
|
+
* whole wallpaper collection in a single press. A refusal costs a number on a card;
|
|
1320
|
+
* guessing costs the user their pictures.
|
|
1321
|
+
*/
|
|
1322
|
+
async function pruneOrphans(keep) {
|
|
1323
|
+
const usage = await storeUsage(keep);
|
|
1324
|
+
if (!usage.trusted) {
|
|
1325
|
+
console.warn("dsh-background-by-model: refusing to prune — \"theme-config.json\" is unreadable, so nothing on disk can be told apart from an orphan");
|
|
1326
|
+
return {
|
|
1327
|
+
removed: [],
|
|
1328
|
+
failed: [],
|
|
1329
|
+
freedBytes: 0,
|
|
1330
|
+
refused: true
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
const removed = [];
|
|
1334
|
+
const failed = [];
|
|
1335
|
+
let freedBytes = 0;
|
|
1336
|
+
for (const entry of usage.orphans) try {
|
|
1337
|
+
await rm(imagePath(entry.slot), { force: true });
|
|
1338
|
+
removed.push(entry.slot);
|
|
1339
|
+
freedBytes += entry.bytes;
|
|
1340
|
+
} catch (e) {
|
|
1341
|
+
failed.push(entry.slot);
|
|
1342
|
+
console.warn(`dsh-background-by-model: failed to remove the orphaned image for slot "${entry.slot}"`, e);
|
|
1343
|
+
}
|
|
1344
|
+
return {
|
|
1345
|
+
removed,
|
|
1346
|
+
failed,
|
|
1347
|
+
freedBytes,
|
|
1348
|
+
refused: false
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
/**
|
|
1162
1352
|
* Download an image from a network URL into one slot (replacing its bytes).
|
|
1163
1353
|
* Returns { ok, dataUrl?, error? }; never throws.
|
|
1164
1354
|
*/
|
|
@@ -1277,6 +1467,8 @@ async function handleRpcMethod(ctx, endpoint, payload) {
|
|
|
1277
1467
|
const method = endpoint.startsWith(`${RPC_NS}/`) ? endpoint.slice(21) : "";
|
|
1278
1468
|
const rawSlot = payload?.slot;
|
|
1279
1469
|
const slot = typeof rawSlot === "string" && SLOT_RE.test(rawSlot) ? rawSlot : null;
|
|
1470
|
+
const rawKeep = payload?.keep;
|
|
1471
|
+
const keep = Array.isArray(rawKeep) ? rawKeep.filter((s) => typeof s === "string" && SLOT_RE.test(s)) : [];
|
|
1280
1472
|
try {
|
|
1281
1473
|
switch (method) {
|
|
1282
1474
|
case "read": return {
|
|
@@ -1350,6 +1542,14 @@ async function handleRpcMethod(ctx, endpoint, payload) {
|
|
|
1350
1542
|
ok: true,
|
|
1351
1543
|
value: await resetStore()
|
|
1352
1544
|
};
|
|
1545
|
+
case "storeUsage": return {
|
|
1546
|
+
ok: true,
|
|
1547
|
+
value: await storeUsage(keep)
|
|
1548
|
+
};
|
|
1549
|
+
case "pruneOrphans": return {
|
|
1550
|
+
ok: true,
|
|
1551
|
+
value: await pruneOrphans(keep)
|
|
1552
|
+
};
|
|
1353
1553
|
case "defaultModel": return {
|
|
1354
1554
|
ok: true,
|
|
1355
1555
|
value: defaultModel(ctx)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-background-by-model",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
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",
|
|
@@ -65,8 +65,9 @@
|
|
|
65
65
|
"check:ui": "node scripts/ui-strings-check.ts",
|
|
66
66
|
"check:watchdogs": "node scripts/watchdogs-check.ts",
|
|
67
67
|
"check:scheme": "node scripts/scheme-check.ts",
|
|
68
|
+
"check:store": "node scripts/store-check.ts",
|
|
68
69
|
"check:node": "node scripts/node-half-check.mjs",
|
|
69
|
-
"test": "tsdown && tsc -p tsconfig.json && node scripts/holiday-check.ts && node scripts/rotation-check.ts && node scripts/transition-check.ts && node scripts/repaint-check.ts && node scripts/code-plate-check.ts && node scripts/image-mode-check.ts && node scripts/rules-view-check.ts && node scripts/preset-check.ts && node scripts/ui-strings-check.ts && node scripts/watchdogs-check.ts && node scripts/scheme-check.ts && node scripts/node-half-check.mjs"
|
|
70
|
+
"test": "tsdown && tsc -p tsconfig.json && node scripts/holiday-check.ts && node scripts/rotation-check.ts && node scripts/transition-check.ts && node scripts/repaint-check.ts && node scripts/code-plate-check.ts && node scripts/image-mode-check.ts && node scripts/rules-view-check.ts && node scripts/preset-check.ts && node scripts/ui-strings-check.ts && node scripts/watchdogs-check.ts && node scripts/scheme-check.ts && node scripts/store-check.ts && node scripts/node-half-check.mjs"
|
|
70
71
|
},
|
|
71
72
|
"devDependencies": {
|
|
72
73
|
"@deepseek-ai/dsh-home-paths": ">=0.2.1-alpha.2",
|