dsh-background-by-model 0.5.5 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -1
- package/CHANGELOG.zh.md +47 -1
- package/README.md +91 -23
- package/README.zh.md +92 -24
- package/holiday/mid-autumn.webp +0 -0
- package/holiday/national-day.webp +0 -0
- package/lib/client.js +1697 -199
- package/lib/client.js.map +1 -1
- package/lib/index.js +320 -15
- package/package.json +10 -4
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",
|
|
@@ -9,6 +53,26 @@ const BG_MODES = [
|
|
|
9
53
|
"tile",
|
|
10
54
|
"center"
|
|
11
55
|
];
|
|
56
|
+
/** Every accepted `rotate.order`, in UI order. */
|
|
57
|
+
const ROTATE_ORDERS = ["order", "shuffle"];
|
|
58
|
+
/** Fastest allowed dwell time — below this the cross-fade never settles. */
|
|
59
|
+
const ROTATE_MIN_MS = 5e3;
|
|
60
|
+
/** Slowest allowed dwell time (24 h): one image per day is still "rotation". */
|
|
61
|
+
const ROTATE_MAX_MS = 864e5;
|
|
62
|
+
/** Longest allowed cross-fade; 0 is a hard cut. */
|
|
63
|
+
const ROTATE_FADE_MAX_MS = 3e3;
|
|
64
|
+
/** Everything off: the shipped default, and what a config predating this feature gets. */
|
|
65
|
+
const DEFAULT_ROTATION = {
|
|
66
|
+
enabled: false,
|
|
67
|
+
intervalMs: 6e4,
|
|
68
|
+
order: "order",
|
|
69
|
+
advanceOnSwitch: false,
|
|
70
|
+
fadeMs: 320
|
|
71
|
+
};
|
|
72
|
+
/** A fresh rotation block (never hand out the shared default object). */
|
|
73
|
+
function defaultRotation() {
|
|
74
|
+
return { ...DEFAULT_ROTATION };
|
|
75
|
+
}
|
|
12
76
|
/**
|
|
13
77
|
* Main interface opacities (0..1), global (Interface page):
|
|
14
78
|
* `bg` main background (`--dsw-alias-bg-base`)
|
|
@@ -60,6 +124,68 @@ const DEFAULT_PART_BLURS = {
|
|
|
60
124
|
rightbar: 0,
|
|
61
125
|
input: 0
|
|
62
126
|
};
|
|
127
|
+
/**
|
|
128
|
+
* `#RRGGBB` → the `[h, s, l]` triple every rule stores (`s` and `l` in 0..1), or
|
|
129
|
+
* null when the string is not a hex color.
|
|
130
|
+
*
|
|
131
|
+
* A holiday names its color as hex because that is how a color is quoted, and a
|
|
132
|
+
* hand-converted triple is a magic number nothing downstream can check. The
|
|
133
|
+
* conversion lives here rather than in ./holiday because `[h, s, l]` is this
|
|
134
|
+
* module's own rule shape.
|
|
135
|
+
*/
|
|
136
|
+
function hexToHsl(hex) {
|
|
137
|
+
const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
|
|
138
|
+
if (m === null) return null;
|
|
139
|
+
const n = parseInt(m[1], 16);
|
|
140
|
+
const r = (n >> 16 & 255) / 255;
|
|
141
|
+
const g = (n >> 8 & 255) / 255;
|
|
142
|
+
const b = (n & 255) / 255;
|
|
143
|
+
const max = Math.max(r, g, b);
|
|
144
|
+
const min = Math.min(r, g, b);
|
|
145
|
+
const l = (max + min) / 2;
|
|
146
|
+
const d = max - min;
|
|
147
|
+
if (d === 0) return [
|
|
148
|
+
0,
|
|
149
|
+
0,
|
|
150
|
+
l
|
|
151
|
+
];
|
|
152
|
+
const s = d / (1 - Math.abs(2 * l - 1));
|
|
153
|
+
let h;
|
|
154
|
+
if (max === r) h = 60 * ((g - b) / d % 6);
|
|
155
|
+
else if (max === g) h = 60 * ((b - r) / d + 2);
|
|
156
|
+
else h = 60 * ((r - g) / d + 4);
|
|
157
|
+
if (h < 0) h += 360;
|
|
158
|
+
return [
|
|
159
|
+
clamp(h, 0, 360, 0),
|
|
160
|
+
clamp01(s, 0),
|
|
161
|
+
clamp01(l, .5)
|
|
162
|
+
];
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* One holiday's appearance, untouched.
|
|
166
|
+
*
|
|
167
|
+
* Holidays default to `fill` rather than a rule's `fit`: these are full-bleed
|
|
168
|
+
* festival wallpapers, and `fit` would letterbox them on every aspect ratio the
|
|
169
|
+
* art was not cut for.
|
|
170
|
+
*/
|
|
171
|
+
function defaultHolidayRule(def) {
|
|
172
|
+
const color = hexToHsl(def.color);
|
|
173
|
+
return {
|
|
174
|
+
id: def.id,
|
|
175
|
+
images: [{
|
|
176
|
+
slot: def.slot,
|
|
177
|
+
bgState: { ...DEFAULT_BG_STATE },
|
|
178
|
+
color
|
|
179
|
+
}],
|
|
180
|
+
match: "",
|
|
181
|
+
enabled: true,
|
|
182
|
+
color,
|
|
183
|
+
bgMode: "fill",
|
|
184
|
+
wallpaperOpacity: 1,
|
|
185
|
+
blur: 0,
|
|
186
|
+
rotate: defaultRotation()
|
|
187
|
+
};
|
|
188
|
+
}
|
|
63
189
|
/** Slot names reach the filesystem, so they are strictly whitelisted. */
|
|
64
190
|
const SLOT_RE = /^[A-Za-z0-9_-]{1,32}$/;
|
|
65
191
|
function clamp(n, lo, hi, def) {
|
|
@@ -86,23 +212,143 @@ function normalizeHsl(raw) {
|
|
|
86
212
|
clamp(raw[2], 0, 1, .25)
|
|
87
213
|
];
|
|
88
214
|
}
|
|
89
|
-
/** Coerce one persisted
|
|
215
|
+
/** Coerce one persisted rotation block; a config without one gets the shipped default. */
|
|
216
|
+
function normalizeRotation(raw) {
|
|
217
|
+
const r = raw ?? {};
|
|
218
|
+
const d = DEFAULT_ROTATION;
|
|
219
|
+
return {
|
|
220
|
+
enabled: r.enabled === true,
|
|
221
|
+
intervalMs: clamp(r.intervalMs, ROTATE_MIN_MS, ROTATE_MAX_MS, d.intervalMs),
|
|
222
|
+
order: ROTATE_ORDERS.includes(r.order) ? r.order : d.order,
|
|
223
|
+
advanceOnSwitch: r.advanceOnSwitch === true,
|
|
224
|
+
fadeMs: clamp(r.fadeMs, 0, ROTATE_FADE_MAX_MS, d.fadeMs)
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
/** Own-property test for values that may be anything at all (a raw JSON entry). */
|
|
228
|
+
function hasOwn(o, key) {
|
|
229
|
+
return Object.prototype.hasOwnProperty.call(o, key);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Coerce one persisted image entry, or null when it lacks a usable slot.
|
|
233
|
+
*
|
|
234
|
+
* `inherit` is the RULE's own color, and it is applied only to an entry that has
|
|
235
|
+
* no `color` key at all — the shape every release before 0.7.1 wrote, where a
|
|
236
|
+
* rule's color was the only color there was. Lifting it here, once, is what makes
|
|
237
|
+
* an upgrade a no-op on screen: a themed wallpaper keeps its theme instead of
|
|
238
|
+
* silently dropping to the system palette, and it happens in the shared sanitizer
|
|
239
|
+
* so both halves lift identically.
|
|
240
|
+
*
|
|
241
|
+
* The KEY's presence is the whole distinction, which is why `null` is not the
|
|
242
|
+
* same as absent: an entry with `color: null` is an image the user explicitly set
|
|
243
|
+
* to "follow the system theme", and re-inheriting the rule's color over that
|
|
244
|
+
* would undo a deliberate choice on every load.
|
|
245
|
+
*/
|
|
246
|
+
function normalizeImage(raw, inherit = null) {
|
|
247
|
+
const i = raw ?? {};
|
|
248
|
+
if (typeof i.slot !== "string" || !SLOT_RE.test(i.slot)) return null;
|
|
249
|
+
return {
|
|
250
|
+
slot: i.slot,
|
|
251
|
+
bgState: normalizeBgState(i.bgState),
|
|
252
|
+
color: hasOwn(i, "color") ? normalizeHsl(i.color) : inherit
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Coerce one persisted rule, or null when it names no image AT ALL.
|
|
257
|
+
*
|
|
258
|
+
* Accepts BOTH image shapes: the 0.7 list (`images`), and the single
|
|
259
|
+
* `slot`/`bgState` pair every earlier version wrote — the latter is lifted to a
|
|
260
|
+
* one-entry list here rather than at a dozen call sites, which is what keeps the
|
|
261
|
+
* rest of the codebase free of "old config" branches. Duplicate slots inside one
|
|
262
|
+
* rule are dropped: two entries pointing at the same bytes would only make the
|
|
263
|
+
* rotation look stuck.
|
|
264
|
+
*
|
|
265
|
+
* The rule's color is resolved FIRST and handed to every image entry, because it
|
|
266
|
+
* is what an entry written before 0.7.1 inherits (`normalizeImage`). The rule keeps
|
|
267
|
+
* it as well: it is not a default for its images at paint time any more, but it
|
|
268
|
+
* is still what the rule paints once its last picture is gone.
|
|
269
|
+
*
|
|
270
|
+
* An EMPTY list is a legitimate rule, not a broken one: removing a rule's last
|
|
271
|
+
* picture has to be expressible, and forcing an entry back in is what produced a
|
|
272
|
+
* phantom blank image that took slot 1 and could never be deleted again (the next
|
|
273
|
+
* upload landed at position 2 and the rule kept painting nothing). What such a
|
|
274
|
+
* rule then does is `ruleCanPaint`'s business: with a color of its own it paints
|
|
275
|
+
* the interface, with neither it is skipped by matching — which the card's own
|
|
276
|
+
* hint says in so many words. Only a rule that names no image anywhere — no list,
|
|
277
|
+
* no legacy slot, or a slot that could never be a filename — is dropped.
|
|
278
|
+
*/
|
|
90
279
|
function normalizeRule(raw) {
|
|
91
280
|
const r = raw ?? {};
|
|
92
281
|
const id = typeof r.id === "string" && r.id !== "" ? r.id : null;
|
|
93
|
-
|
|
94
|
-
|
|
282
|
+
if (id === null) return null;
|
|
283
|
+
const color = normalizeHsl(r.color);
|
|
284
|
+
const images = [];
|
|
285
|
+
const seen = /* @__PURE__ */ new Set();
|
|
286
|
+
const hadList = Array.isArray(r.images);
|
|
287
|
+
if (hadList) for (const entry of r.images) {
|
|
288
|
+
const image = normalizeImage(entry, color);
|
|
289
|
+
if (image === null || seen.has(image.slot)) continue;
|
|
290
|
+
seen.add(image.slot);
|
|
291
|
+
images.push(image);
|
|
292
|
+
}
|
|
293
|
+
const legacySlot = typeof r.slot === "string" && SLOT_RE.test(r.slot) ? r.slot : null;
|
|
294
|
+
if (images.length === 0 && legacySlot !== null) images.push({
|
|
295
|
+
slot: legacySlot,
|
|
296
|
+
bgState: normalizeBgState(r.bgState),
|
|
297
|
+
color
|
|
298
|
+
});
|
|
299
|
+
if (images.length === 0 && !hadList) return null;
|
|
95
300
|
const mode = BG_MODES.includes(r.bgMode) ? r.bgMode : "fit";
|
|
96
301
|
return {
|
|
97
302
|
id,
|
|
98
|
-
|
|
303
|
+
images,
|
|
99
304
|
match: typeof r.match === "string" ? r.match : "",
|
|
100
305
|
enabled: r.enabled !== false,
|
|
101
|
-
color
|
|
306
|
+
color,
|
|
102
307
|
bgMode: mode,
|
|
103
308
|
wallpaperOpacity: clamp01(r.wallpaperOpacity, 1),
|
|
104
309
|
blur: clamp(r.blur, 0, 60, 0),
|
|
105
|
-
|
|
310
|
+
rotate: normalizeRotation(r.rotate)
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Coerce one persisted holiday entry.
|
|
315
|
+
*
|
|
316
|
+
* `id`, `images` and every theme color (the rule's and each image's) come from
|
|
317
|
+
* the DEFINITION, never from disk: a holiday's slot is the only thing tying it to
|
|
318
|
+
* its bytes, and its theme color is a fixed part of what that holiday looks like —
|
|
319
|
+
* so a stale or hand-edited value would either orphan the wallpaper, point the
|
|
320
|
+
* holiday at a rule's image, or paint a festival in a color it does not have.
|
|
321
|
+
* Everything else is sanitized exactly like a rule's field — except the image
|
|
322
|
+
* list and the rotation, which stay the definition's own single image and stay
|
|
323
|
+
* off, so a festival can never be swapped or cycled.
|
|
324
|
+
*/
|
|
325
|
+
function normalizeHolidayRule(def, raw) {
|
|
326
|
+
const r = raw ?? {};
|
|
327
|
+
const base = defaultHolidayRule(def);
|
|
328
|
+
return {
|
|
329
|
+
...base,
|
|
330
|
+
enabled: r.enabled !== false,
|
|
331
|
+
bgMode: BG_MODES.includes(r.bgMode) ? r.bgMode : base.bgMode,
|
|
332
|
+
wallpaperOpacity: clamp01(r.wallpaperOpacity, base.wallpaperOpacity),
|
|
333
|
+
blur: clamp(r.blur, 0, 60, base.blur),
|
|
334
|
+
images: base.images.map((image) => ({
|
|
335
|
+
slot: image.slot,
|
|
336
|
+
bgState: normalizeBgState(r.images?.[0]?.bgState ?? r.bgState),
|
|
337
|
+
color: image.color
|
|
338
|
+
}))
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
/** Coerce the whole holiday block; unknown ids are dropped, missing ones defaulted. */
|
|
342
|
+
function normalizeHolidays(raw) {
|
|
343
|
+
const r = raw ?? {};
|
|
344
|
+
const stored = /* @__PURE__ */ new Map();
|
|
345
|
+
if (Array.isArray(r.items)) for (const item of r.items) {
|
|
346
|
+
const id = item?.id;
|
|
347
|
+
if (typeof id === "string") stored.set(id, item);
|
|
348
|
+
}
|
|
349
|
+
return {
|
|
350
|
+
enabled: r.enabled !== false,
|
|
351
|
+
items: HOLIDAYS.map((def) => normalizeHolidayRule(def, stored.get(def.id)))
|
|
106
352
|
};
|
|
107
353
|
}
|
|
108
354
|
/** Coerce an unknown persisted value into a valid ThemeConfig, falling back per-field. */
|
|
@@ -123,7 +369,8 @@ function normalizeConfig(raw) {
|
|
|
123
369
|
chatTextOpacity: clamp01(r.chatTextOpacity, 0),
|
|
124
370
|
trajectoryOpacity: clamp01(r.trajectoryOpacity, 1),
|
|
125
371
|
rightbarOpacity: typeof r.rightbarOpacity === "number" && isFinite(r.rightbarOpacity) ? clamp01(r.rightbarOpacity, 1) : null,
|
|
126
|
-
autoExtract: r.autoExtract !== false
|
|
372
|
+
autoExtract: r.autoExtract !== false,
|
|
373
|
+
holidays: normalizeHolidays(r.holidays)
|
|
127
374
|
};
|
|
128
375
|
}
|
|
129
376
|
//#endregion
|
|
@@ -201,14 +448,16 @@ async function migrateLegacy(raw) {
|
|
|
201
448
|
const next = {
|
|
202
449
|
rules: [{
|
|
203
450
|
id: "m1",
|
|
204
|
-
|
|
451
|
+
images: [{
|
|
452
|
+
slot: "m1",
|
|
453
|
+
bgState: r.bgState ?? {}
|
|
454
|
+
}],
|
|
205
455
|
match: "",
|
|
206
456
|
enabled: true,
|
|
207
457
|
color: Array.isArray(r.color) ? r.color : null,
|
|
208
458
|
bgMode: typeof r.bgMode === "string" ? r.bgMode : "fit",
|
|
209
459
|
wallpaperOpacity: typeof r.wallpaperOpacity === "number" ? r.wallpaperOpacity : 1,
|
|
210
|
-
blur: typeof r.blur === "number" ? r.blur : 0
|
|
211
|
-
bgState: r.bgState ?? {}
|
|
460
|
+
blur: typeof r.blur === "number" ? r.blur : 0
|
|
212
461
|
}],
|
|
213
462
|
opacities: r.opacities,
|
|
214
463
|
blurs: r.blurs,
|
|
@@ -248,6 +497,7 @@ const LEGACY_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
|
248
497
|
"blur",
|
|
249
498
|
"bgState"
|
|
250
499
|
]);
|
|
500
|
+
const LEGACY_RULE_KEYS = /* @__PURE__ */ new Set(["slot", "bgState"]);
|
|
251
501
|
const warnedConfigKeys = /* @__PURE__ */ new Set();
|
|
252
502
|
function warnUnknownConfigKeys(raw, normalized) {
|
|
253
503
|
if (raw === null || typeof raw !== "object") return;
|
|
@@ -269,8 +519,23 @@ function warnUnknownConfigKeys(raw, normalized) {
|
|
|
269
519
|
const sent = Array.isArray(r.rules) ? r.rules[0] : void 0;
|
|
270
520
|
const have = normalized.rules[0];
|
|
271
521
|
if (sent !== null && typeof sent === "object" && have !== void 0) {
|
|
522
|
+
const rule = sent;
|
|
272
523
|
const keys = new Set(Object.keys(have));
|
|
273
|
-
for (const key of Object.keys(
|
|
524
|
+
for (const key of Object.keys(rule)) if (!keys.has(key) && !LEGACY_RULE_KEYS.has(key)) warned(`rules[].${key}`);
|
|
525
|
+
const nested = [[
|
|
526
|
+
"rules[].rotate",
|
|
527
|
+
rule.rotate,
|
|
528
|
+
have.rotate
|
|
529
|
+
], [
|
|
530
|
+
"rules[].images[]",
|
|
531
|
+
Array.isArray(rule.images) ? rule.images[0] : void 0,
|
|
532
|
+
have.images[0]
|
|
533
|
+
]];
|
|
534
|
+
for (const [label, got, known] of nested) {
|
|
535
|
+
if (got === null || typeof got !== "object" || known === null || known === void 0) continue;
|
|
536
|
+
const knownKeys = new Set(Object.keys(known));
|
|
537
|
+
for (const key of Object.keys(got)) if (!knownKeys.has(key)) warned(`${label}.${key}`);
|
|
538
|
+
}
|
|
274
539
|
}
|
|
275
540
|
}
|
|
276
541
|
async function writeConfig(config) {
|
|
@@ -294,17 +559,52 @@ function sniffImageMime(buf) {
|
|
|
294
559
|
if (buf.length >= 2 && buf[0] === 66 && buf[1] === 77) return "image/bmp";
|
|
295
560
|
return "image/jpeg";
|
|
296
561
|
}
|
|
297
|
-
/**
|
|
562
|
+
/**
|
|
563
|
+
* Read one slot as a data URL, or null when it has nothing to serve.
|
|
564
|
+
*
|
|
565
|
+
* A holiday slot is answered from the package and from nowhere else — the data
|
|
566
|
+
* directory is never consulted for it, which is what makes the festival art
|
|
567
|
+
* impossible to swap. Everything else is the user's own bytes. Both are resolved
|
|
568
|
+
* lazily, so nothing about the bundled art is loaded — or even looked for — until
|
|
569
|
+
* a holiday actually reaches for it.
|
|
570
|
+
*/
|
|
298
571
|
async function readImage(slot) {
|
|
572
|
+
const asset = bundledAssetFor(slot);
|
|
573
|
+
if (asset !== null) {
|
|
574
|
+
const bundled = await readFileOrNull(bundledAssetPath(asset));
|
|
575
|
+
return bundled === null ? null : toDataUrl(bundled);
|
|
576
|
+
}
|
|
577
|
+
const own = await readFileOrNull(imagePath(slot));
|
|
578
|
+
return own === null ? null : toDataUrl(own);
|
|
579
|
+
}
|
|
580
|
+
async function readFileOrNull(path) {
|
|
299
581
|
try {
|
|
300
|
-
|
|
301
|
-
return `data:${sniffImageMime(buf)};base64,${buf.toString("base64")}`;
|
|
582
|
+
return await readFile(path);
|
|
302
583
|
} catch {
|
|
303
584
|
return null;
|
|
304
585
|
}
|
|
305
586
|
}
|
|
587
|
+
function toDataUrl(buf) {
|
|
588
|
+
return `data:${sniffImageMime(buf)};base64,${buf.toString("base64")}`;
|
|
589
|
+
}
|
|
590
|
+
/** Absolute path of one wallpaper bundled inside the package. */
|
|
591
|
+
function bundledAssetPath(asset) {
|
|
592
|
+
return fileURLToPath(new URL(`../holiday/${asset}`, import.meta.url));
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* The wallpaper bundled for one slot, or null when the slot is not a holiday's.
|
|
596
|
+
*
|
|
597
|
+
* This mapping is the ONLY thing that makes a holiday slot read-only and
|
|
598
|
+
* package-served, so both `readImage` and `writeImage` go through it rather than
|
|
599
|
+
* each testing the holiday list on their own. It reads no config: the bundled art
|
|
600
|
+
* has stopped being a fallback that a flag could disable.
|
|
601
|
+
*/
|
|
602
|
+
function bundledAssetFor(slot) {
|
|
603
|
+
return HOLIDAYS.find((h) => h.slot === slot)?.asset ?? null;
|
|
604
|
+
}
|
|
306
605
|
/** Persist one rule image (null removes it); false keeps the previous file. */
|
|
307
606
|
async function writeImage(slot, dataUrl) {
|
|
607
|
+
if (bundledAssetFor(slot) !== null) return false;
|
|
308
608
|
await ensureDir();
|
|
309
609
|
try {
|
|
310
610
|
if (dataUrl === null) {
|
|
@@ -333,6 +633,10 @@ async function listSlots() {
|
|
|
333
633
|
* Returns { ok, dataUrl?, error? }; never throws.
|
|
334
634
|
*/
|
|
335
635
|
async function fetchImageUrl(slot, url) {
|
|
636
|
+
if (bundledAssetFor(slot) !== null) return {
|
|
637
|
+
ok: false,
|
|
638
|
+
error: "read-only slot"
|
|
639
|
+
};
|
|
336
640
|
if (url === null) {
|
|
337
641
|
const ok = await writeImage(slot, null);
|
|
338
642
|
return {
|
|
@@ -435,7 +739,8 @@ async function handleRpcMethod(ctx, endpoint, payload) {
|
|
|
435
739
|
ok: true,
|
|
436
740
|
value: {
|
|
437
741
|
config: await readConfig(),
|
|
438
|
-
slots: await listSlots()
|
|
742
|
+
slots: await listSlots(),
|
|
743
|
+
schema: 4
|
|
439
744
|
}
|
|
440
745
|
};
|
|
441
746
|
case "writeConfig": return {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-background-by-model",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Appearance plugin for DeepSeek Harness: an ordered list of model rules, each carrying its own
|
|
3
|
+
"version": "0.7.1",
|
|
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 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",
|
|
7
7
|
"dsh",
|
|
@@ -49,13 +49,19 @@
|
|
|
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:rotation": "node scripts/rotation-check.ts",
|
|
61
|
+
"check:repaint": "node scripts/repaint-check.ts",
|
|
62
|
+
"check:ui": "node scripts/ui-strings-check.ts",
|
|
63
|
+
"check:node": "node scripts/node-half-check.mjs",
|
|
64
|
+
"test": "tsdown && tsc -p tsconfig.json && node scripts/holiday-check.ts && node scripts/rotation-check.ts && node scripts/repaint-check.ts && node scripts/ui-strings-check.ts && node scripts/node-half-check.mjs"
|
|
59
65
|
},
|
|
60
66
|
"devDependencies": {
|
|
61
67
|
"@deepseek-ai/dsh-home-paths": "^0.1.0-rc.6",
|