dsh-background-by-model 0.5.5 → 0.6.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 +17 -1
- package/CHANGELOG.zh.md +17 -1
- package/README.md +29 -10
- package/README.zh.md +29 -10
- package/holiday/mid-autumn.webp +0 -0
- package/holiday/national-day.webp +0 -0
- package/lib/client.js +392 -46
- package/lib/client.js.map +1 -1
- package/lib/index.js +180 -4
- package/package.json +7 -3
package/lib/index.js
CHANGED
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
import { access, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
2
3
|
import { dshHomePath } from "@deepseek-ai/dsh-home-paths";
|
|
4
|
+
//#region src/holiday.ts
|
|
5
|
+
const HOLIDAYS = [{
|
|
6
|
+
id: "mid-autumn",
|
|
7
|
+
slot: "h-midautumn",
|
|
8
|
+
asset: "mid-autumn.webp",
|
|
9
|
+
color: "#384A77",
|
|
10
|
+
kind: "lunar"
|
|
11
|
+
}, {
|
|
12
|
+
id: "national-day",
|
|
13
|
+
slot: "h-nationalday",
|
|
14
|
+
asset: "national-day.webp",
|
|
15
|
+
color: "#FFF6EB",
|
|
16
|
+
kind: "gregorian"
|
|
17
|
+
}];
|
|
18
|
+
//#endregion
|
|
3
19
|
//#region src/schema.ts
|
|
20
|
+
/**
|
|
21
|
+
* The persisted configuration shape — declared exactly ONCE, for both halves.
|
|
22
|
+
*
|
|
23
|
+
* This shape used to be written out twice: the node half owns persistence and
|
|
24
|
+
* sanitizes every write, the browser half owns the UI and its own reading of the
|
|
25
|
+
* same file. A field added to only one side was silently dropped by the other
|
|
26
|
+
* side's sanitizer — the matching slider stayed live in memory, `writeConfig`
|
|
27
|
+
* wrote a config without it, and the next load fell back to the default.
|
|
28
|
+
* `blurs.rightbar` / `rightbarOpacity` in 0.3.2 was exactly that bug, and the key
|
|
29
|
+
* lists were duplicated 6 times across the two halves, so it was due to happen.
|
|
30
|
+
*
|
|
31
|
+
* The types, the key lists, the defaults and the pure normalizers therefore live
|
|
32
|
+
* here and both halves import them: adding a field means editing this file, and
|
|
33
|
+
* the two halves cannot disagree about the shape. The host additionally logs one
|
|
34
|
+
* warning per unknown key while sanitizing a write (see `warnUnknownConfigKeys`
|
|
35
|
+
* in `src/index.ts`), so a field that reaches the node half without being
|
|
36
|
+
* declared here shows up in the host log instead of quietly disappearing.
|
|
37
|
+
*
|
|
38
|
+
* Nothing in this module may touch the DOM, Node or the Cordis context: it is
|
|
39
|
+
* imported by the browser bundle and by the node half alike.
|
|
40
|
+
*/
|
|
41
|
+
const DEFAULT_BG_STATE = {
|
|
42
|
+
zoom: 1,
|
|
43
|
+
x: 0,
|
|
44
|
+
y: 0,
|
|
45
|
+
iw: 0,
|
|
46
|
+
ih: 0
|
|
47
|
+
};
|
|
4
48
|
/** Every accepted `bgMode`, in UI order. */
|
|
5
49
|
const BG_MODES = [
|
|
6
50
|
"fit",
|
|
@@ -60,6 +104,63 @@ const DEFAULT_PART_BLURS = {
|
|
|
60
104
|
rightbar: 0,
|
|
61
105
|
input: 0
|
|
62
106
|
};
|
|
107
|
+
/**
|
|
108
|
+
* `#RRGGBB` → the `[h, s, l]` triple every rule stores (`s` and `l` in 0..1), or
|
|
109
|
+
* null when the string is not a hex color.
|
|
110
|
+
*
|
|
111
|
+
* A holiday names its color as hex because that is how a color is quoted, and a
|
|
112
|
+
* hand-converted triple is a magic number nothing downstream can check. The
|
|
113
|
+
* conversion lives here rather than in ./holiday because `[h, s, l]` is this
|
|
114
|
+
* module's own rule shape.
|
|
115
|
+
*/
|
|
116
|
+
function hexToHsl(hex) {
|
|
117
|
+
const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
|
|
118
|
+
if (m === null) return null;
|
|
119
|
+
const n = parseInt(m[1], 16);
|
|
120
|
+
const r = (n >> 16 & 255) / 255;
|
|
121
|
+
const g = (n >> 8 & 255) / 255;
|
|
122
|
+
const b = (n & 255) / 255;
|
|
123
|
+
const max = Math.max(r, g, b);
|
|
124
|
+
const min = Math.min(r, g, b);
|
|
125
|
+
const l = (max + min) / 2;
|
|
126
|
+
const d = max - min;
|
|
127
|
+
if (d === 0) return [
|
|
128
|
+
0,
|
|
129
|
+
0,
|
|
130
|
+
l
|
|
131
|
+
];
|
|
132
|
+
const s = d / (1 - Math.abs(2 * l - 1));
|
|
133
|
+
let h;
|
|
134
|
+
if (max === r) h = 60 * ((g - b) / d % 6);
|
|
135
|
+
else if (max === g) h = 60 * ((b - r) / d + 2);
|
|
136
|
+
else h = 60 * ((r - g) / d + 4);
|
|
137
|
+
if (h < 0) h += 360;
|
|
138
|
+
return [
|
|
139
|
+
clamp(h, 0, 360, 0),
|
|
140
|
+
clamp01(s, 0),
|
|
141
|
+
clamp01(l, .5)
|
|
142
|
+
];
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* One holiday's appearance, untouched.
|
|
146
|
+
*
|
|
147
|
+
* Holidays default to `fill` rather than a rule's `fit`: these are full-bleed
|
|
148
|
+
* festival wallpapers, and `fit` would letterbox them on every aspect ratio the
|
|
149
|
+
* art was not cut for.
|
|
150
|
+
*/
|
|
151
|
+
function defaultHolidayRule(def) {
|
|
152
|
+
return {
|
|
153
|
+
id: def.id,
|
|
154
|
+
slot: def.slot,
|
|
155
|
+
match: "",
|
|
156
|
+
enabled: true,
|
|
157
|
+
color: hexToHsl(def.color),
|
|
158
|
+
bgMode: "fill",
|
|
159
|
+
wallpaperOpacity: 1,
|
|
160
|
+
blur: 0,
|
|
161
|
+
bgState: { ...DEFAULT_BG_STATE }
|
|
162
|
+
};
|
|
163
|
+
}
|
|
63
164
|
/** Slot names reach the filesystem, so they are strictly whitelisted. */
|
|
64
165
|
const SLOT_RE = /^[A-Za-z0-9_-]{1,32}$/;
|
|
65
166
|
function clamp(n, lo, hi, def) {
|
|
@@ -105,6 +206,41 @@ function normalizeRule(raw) {
|
|
|
105
206
|
bgState: normalizeBgState(r.bgState)
|
|
106
207
|
};
|
|
107
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* Coerce one persisted holiday entry.
|
|
211
|
+
*
|
|
212
|
+
* `id`, `slot` and `color` come from the DEFINITION, never from disk: a holiday's
|
|
213
|
+
* slot is the only thing tying it to its bytes, and its theme color is a fixed
|
|
214
|
+
* part of what that holiday looks like — so a stale or hand-edited value would
|
|
215
|
+
* either orphan the wallpaper, point the holiday at a rule's image, or paint a
|
|
216
|
+
* festival in a color it does not have. Everything else is sanitized exactly like
|
|
217
|
+
* a rule's field.
|
|
218
|
+
*/
|
|
219
|
+
function normalizeHolidayRule(def, raw) {
|
|
220
|
+
const r = raw ?? {};
|
|
221
|
+
const base = defaultHolidayRule(def);
|
|
222
|
+
return {
|
|
223
|
+
...base,
|
|
224
|
+
enabled: r.enabled !== false,
|
|
225
|
+
bgMode: BG_MODES.includes(r.bgMode) ? r.bgMode : base.bgMode,
|
|
226
|
+
wallpaperOpacity: clamp01(r.wallpaperOpacity, base.wallpaperOpacity),
|
|
227
|
+
blur: clamp(r.blur, 0, 60, base.blur),
|
|
228
|
+
bgState: normalizeBgState(r.bgState)
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
/** Coerce the whole holiday block; unknown ids are dropped, missing ones defaulted. */
|
|
232
|
+
function normalizeHolidays(raw) {
|
|
233
|
+
const r = raw ?? {};
|
|
234
|
+
const stored = /* @__PURE__ */ new Map();
|
|
235
|
+
if (Array.isArray(r.items)) for (const item of r.items) {
|
|
236
|
+
const id = item?.id;
|
|
237
|
+
if (typeof id === "string") stored.set(id, item);
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
enabled: r.enabled !== false,
|
|
241
|
+
items: HOLIDAYS.map((def) => normalizeHolidayRule(def, stored.get(def.id)))
|
|
242
|
+
};
|
|
243
|
+
}
|
|
108
244
|
/** Coerce an unknown persisted value into a valid ThemeConfig, falling back per-field. */
|
|
109
245
|
function normalizeConfig(raw) {
|
|
110
246
|
const r = raw ?? {};
|
|
@@ -123,7 +259,8 @@ function normalizeConfig(raw) {
|
|
|
123
259
|
chatTextOpacity: clamp01(r.chatTextOpacity, 0),
|
|
124
260
|
trajectoryOpacity: clamp01(r.trajectoryOpacity, 1),
|
|
125
261
|
rightbarOpacity: typeof r.rightbarOpacity === "number" && isFinite(r.rightbarOpacity) ? clamp01(r.rightbarOpacity, 1) : null,
|
|
126
|
-
autoExtract: r.autoExtract !== false
|
|
262
|
+
autoExtract: r.autoExtract !== false,
|
|
263
|
+
holidays: normalizeHolidays(r.holidays)
|
|
127
264
|
};
|
|
128
265
|
}
|
|
129
266
|
//#endregion
|
|
@@ -294,17 +431,52 @@ function sniffImageMime(buf) {
|
|
|
294
431
|
if (buf.length >= 2 && buf[0] === 66 && buf[1] === 77) return "image/bmp";
|
|
295
432
|
return "image/jpeg";
|
|
296
433
|
}
|
|
297
|
-
/**
|
|
434
|
+
/**
|
|
435
|
+
* Read one slot as a data URL, or null when it has nothing to serve.
|
|
436
|
+
*
|
|
437
|
+
* A holiday slot is answered from the package and from nowhere else — the data
|
|
438
|
+
* directory is never consulted for it, which is what makes the festival art
|
|
439
|
+
* impossible to swap. Everything else is the user's own bytes. Both are resolved
|
|
440
|
+
* lazily, so nothing about the bundled art is loaded — or even looked for — until
|
|
441
|
+
* a holiday actually reaches for it.
|
|
442
|
+
*/
|
|
298
443
|
async function readImage(slot) {
|
|
444
|
+
const asset = bundledAssetFor(slot);
|
|
445
|
+
if (asset !== null) {
|
|
446
|
+
const bundled = await readFileOrNull(bundledAssetPath(asset));
|
|
447
|
+
return bundled === null ? null : toDataUrl(bundled);
|
|
448
|
+
}
|
|
449
|
+
const own = await readFileOrNull(imagePath(slot));
|
|
450
|
+
return own === null ? null : toDataUrl(own);
|
|
451
|
+
}
|
|
452
|
+
async function readFileOrNull(path) {
|
|
299
453
|
try {
|
|
300
|
-
|
|
301
|
-
return `data:${sniffImageMime(buf)};base64,${buf.toString("base64")}`;
|
|
454
|
+
return await readFile(path);
|
|
302
455
|
} catch {
|
|
303
456
|
return null;
|
|
304
457
|
}
|
|
305
458
|
}
|
|
459
|
+
function toDataUrl(buf) {
|
|
460
|
+
return `data:${sniffImageMime(buf)};base64,${buf.toString("base64")}`;
|
|
461
|
+
}
|
|
462
|
+
/** Absolute path of one wallpaper bundled inside the package. */
|
|
463
|
+
function bundledAssetPath(asset) {
|
|
464
|
+
return fileURLToPath(new URL(`../holiday/${asset}`, import.meta.url));
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* The wallpaper bundled for one slot, or null when the slot is not a holiday's.
|
|
468
|
+
*
|
|
469
|
+
* This mapping is the ONLY thing that makes a holiday slot read-only and
|
|
470
|
+
* package-served, so both `readImage` and `writeImage` go through it rather than
|
|
471
|
+
* each testing the holiday list on their own. It reads no config: the bundled art
|
|
472
|
+
* has stopped being a fallback that a flag could disable.
|
|
473
|
+
*/
|
|
474
|
+
function bundledAssetFor(slot) {
|
|
475
|
+
return HOLIDAYS.find((h) => h.slot === slot)?.asset ?? null;
|
|
476
|
+
}
|
|
306
477
|
/** Persist one rule image (null removes it); false keeps the previous file. */
|
|
307
478
|
async function writeImage(slot, dataUrl) {
|
|
479
|
+
if (bundledAssetFor(slot) !== null) return false;
|
|
308
480
|
await ensureDir();
|
|
309
481
|
try {
|
|
310
482
|
if (dataUrl === null) {
|
|
@@ -333,6 +505,10 @@ async function listSlots() {
|
|
|
333
505
|
* Returns { ok, dataUrl?, error? }; never throws.
|
|
334
506
|
*/
|
|
335
507
|
async function fetchImageUrl(slot, url) {
|
|
508
|
+
if (bundledAssetFor(slot) !== null) return {
|
|
509
|
+
ok: false,
|
|
510
|
+
error: "read-only slot"
|
|
511
|
+
};
|
|
336
512
|
if (url === null) {
|
|
337
513
|
const ok = await writeImage(slot, null);
|
|
338
514
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-background-by-model",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Appearance plugin for DeepSeek Harness: an ordered list of model rules, each carrying its own wallpaper, theme color, layout mode, opacity and blur — the background follows the model you switch to.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
|
@@ -49,13 +49,17 @@
|
|
|
49
49
|
"CHANGELOG.md",
|
|
50
50
|
"CHANGELOG.zh.md",
|
|
51
51
|
"cordis.patch.yml",
|
|
52
|
-
"example_img"
|
|
52
|
+
"example_img",
|
|
53
|
+
"holiday"
|
|
53
54
|
],
|
|
54
55
|
"scripts": {
|
|
55
56
|
"bundle": "tsdown",
|
|
56
57
|
"watch": "tsdown --watch",
|
|
57
58
|
"typecheck": "tsc -p tsconfig.json",
|
|
58
|
-
"
|
|
59
|
+
"check:holiday": "node scripts/holiday-check.ts",
|
|
60
|
+
"check:ui": "node scripts/ui-strings-check.ts",
|
|
61
|
+
"check:node": "node scripts/node-half-check.mjs",
|
|
62
|
+
"test": "tsdown && tsc -p tsconfig.json && node scripts/holiday-check.ts && node scripts/ui-strings-check.ts && node scripts/node-half-check.mjs"
|
|
59
63
|
},
|
|
60
64
|
"devDependencies": {
|
|
61
65
|
"@deepseek-ai/dsh-home-paths": "^0.1.0-rc.6",
|