deepseek-harness-background 0.2.0 → 0.3.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/README.md +11 -3
- package/README.zh.md +11 -3
- package/lib/client.js +1264 -101
- package/lib/client.js.map +1 -1
- package/lib/index.js +78 -31
- package/package.json +86 -88
package/lib/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { settingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { randomBytes } from "node:crypto";
|
|
4
|
-
import { existsSync, mkdirSync,
|
|
5
|
-
import {
|
|
4
|
+
import { existsSync, mkdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { readFile } from "node:fs/promises";
|
|
6
|
+
import { extname, join, resolve } from "node:path";
|
|
6
7
|
import { homedir } from "node:os";
|
|
7
8
|
//#region src/settings.ts
|
|
8
9
|
/** Durable background settings — pure constants/types shared by both halves. */
|
|
@@ -23,14 +24,17 @@ const BACKGROUND_SETTINGS_FIELDS = [
|
|
|
23
24
|
"panelOpacity",
|
|
24
25
|
"blur",
|
|
25
26
|
"wallpaperBlur",
|
|
26
|
-
"fit"
|
|
27
|
+
"fit",
|
|
28
|
+
"timeline"
|
|
27
29
|
];
|
|
28
30
|
/** Browser-facing base path of the background API (shared with the host routes). */
|
|
29
31
|
const BACKGROUND_API_PREFIX = "/api/bg-wallpaper";
|
|
30
32
|
/** Minimum scrim opacity (0 = no veil at all). */
|
|
31
33
|
const SCRIM_MIN = 0;
|
|
32
|
-
/** Maximum scrim opacity (
|
|
33
|
-
|
|
34
|
+
/** Maximum scrim opacity (1 = fully opaque veil — hides the wallpaper entirely).
|
|
35
|
+
* Kept at 1 so every percent-unit slider shares one 0..1 domain: the same
|
|
36
|
+
* displayed percentage puts every thumb at the same fraction of its track. */
|
|
37
|
+
const SCRIM_MAX = 1;
|
|
34
38
|
/** Default scrim strength applied over user art (a light readability veil). */
|
|
35
39
|
const DEFAULT_SCRIM = .25;
|
|
36
40
|
/** Default background image opacity (1 = fully opaque). */
|
|
@@ -61,35 +65,46 @@ const BackgroundSettingsSchema = z.object({
|
|
|
61
65
|
/** Image URL or empty (mutually exclusive with uploadId at runtime). */
|
|
62
66
|
url: z.string().default(""),
|
|
63
67
|
opacity: z.number().min(0).max(1).default(1),
|
|
64
|
-
scrim: z.number().min(0).max(
|
|
68
|
+
scrim: z.number().min(0).max(1).default(DEFAULT_SCRIM),
|
|
65
69
|
panelOpacity: z.number().min(0).max(1).default(DEFAULT_PANEL_OPACITY),
|
|
66
70
|
blur: z.number().min(0).max(40).default(16),
|
|
67
71
|
wallpaperBlur: z.number().min(0).max(60).default(0),
|
|
68
|
-
fit: z.union([...FIT_MODES]).default(DEFAULT_FIT)
|
|
72
|
+
fit: z.union([...FIT_MODES]).default(DEFAULT_FIT),
|
|
73
|
+
timeline: z.boolean().default(true)
|
|
69
74
|
});
|
|
70
75
|
//#endregion
|
|
71
76
|
//#region src/harness-home.ts
|
|
72
77
|
/**
|
|
73
78
|
* Harness-home resolution — where the uploaded wallpaper images live.
|
|
74
79
|
*
|
|
75
|
-
* Mirrors the dsh launcher's precedence (see
|
|
76
|
-
*
|
|
77
|
-
* override, else `$DSH_HOME`, else `<homedir>/.dsh`. Kept as a tiny pure
|
|
80
|
+
* Mirrors the dsh launcher's precedence (see @deepseek-ai/dsh-home-paths'
|
|
81
|
+
* resolveDshHome): `$DSH_HOME`, else `<homedir>/.dsh`. Kept as a tiny pure
|
|
78
82
|
* function so the host half needs no out-of-tree dependency on the internal
|
|
79
|
-
* home-paths package.
|
|
83
|
+
* home-paths package. The semantics match the official resolver: an empty or
|
|
84
|
+
* whitespace-only `$DSH_HOME` counts as unset, a leading `~` / `~/` / `~\`
|
|
85
|
+
* expands against the OS home, and the result is normalized with
|
|
86
|
+
* path.resolve().
|
|
80
87
|
*/
|
|
81
88
|
/** First non-blank string in a candidate list, else undefined. */
|
|
82
89
|
function firstNonBlank(...values) {
|
|
83
90
|
for (const value of values) if (typeof value === "string" && value.trim() !== "") return value.trim();
|
|
84
91
|
}
|
|
92
|
+
/** Expand supported tilde prefixes against the OS home (mirrors expandHomePath). */
|
|
93
|
+
function expandHomePath(path) {
|
|
94
|
+
if (path === "~") return homedir();
|
|
95
|
+
if (path.startsWith("~/") || path.startsWith("~\\")) return join(homedir(), path.slice(2));
|
|
96
|
+
return path;
|
|
97
|
+
}
|
|
85
98
|
/**
|
|
86
|
-
* Resolve the harness home (the directory containing `profiles/`):
|
|
87
|
-
*
|
|
99
|
+
* Resolve the harness home (the directory containing `profiles/`): `$DSH_HOME`
|
|
100
|
+
* first, then `~/.dsh`. An empty/whitespace `$DSH_HOME` counts as unset; a
|
|
101
|
+
* leading tilde expands against the OS home; the result is absolute.
|
|
88
102
|
* @param env - environment (defaults to process.env).
|
|
89
|
-
* @returns the absolute harness home directory.
|
|
103
|
+
* @returns the normalized absolute harness home directory.
|
|
90
104
|
*/
|
|
91
105
|
function resolveHarnessHome(env = process.env) {
|
|
92
|
-
|
|
106
|
+
const selected = firstNonBlank(env.DSH_HOME) ?? join(homedir(), ".dsh");
|
|
107
|
+
return resolve(expandHomePath(selected));
|
|
93
108
|
}
|
|
94
109
|
/** Subdirectory under the harness home that this plugin owns. */
|
|
95
110
|
const PLUGIN_HOME_REL = join("deepseek-harness-background");
|
|
@@ -122,7 +137,8 @@ const DEFAULTS = {
|
|
|
122
137
|
panelOpacity: DEFAULT_PANEL_OPACITY,
|
|
123
138
|
blur: 16,
|
|
124
139
|
wallpaperBlur: 0,
|
|
125
|
-
fit: DEFAULT_FIT
|
|
140
|
+
fit: DEFAULT_FIT,
|
|
141
|
+
timeline: true
|
|
126
142
|
};
|
|
127
143
|
/** Keep only the schema's known fields (see BACKGROUND_SETTINGS_FIELDS). */
|
|
128
144
|
function pickKnown(section) {
|
|
@@ -149,7 +165,7 @@ const NUM_BOUNDS = {
|
|
|
149
165
|
},
|
|
150
166
|
scrim: {
|
|
151
167
|
min: 0,
|
|
152
|
-
max:
|
|
168
|
+
max: 1
|
|
153
169
|
},
|
|
154
170
|
panelOpacity: {
|
|
155
171
|
min: 0,
|
|
@@ -198,19 +214,42 @@ function requireMethod(req, res, method) {
|
|
|
198
214
|
* the request `Host` is rejected. Requests without either header (curl, node
|
|
199
215
|
* http) pass — this is a local single-user tool, and the fence only targets
|
|
200
216
|
* the cross-site browser vector.
|
|
217
|
+
*
|
|
218
|
+
* The `Host` itself must also be a loopback name: under a DNS-rebinding
|
|
219
|
+
* attack the attacker's page resolves their own domain to 127.0.0.1, so both
|
|
220
|
+
* `Origin` and `Host` carry the attacker's hostname and the self-consistency
|
|
221
|
+
* check above alone would admit them. The web server binds loopback (or is
|
|
222
|
+
* deliberately exposed on 0.0.0.0 by its operator), so accepting only
|
|
223
|
+
* 127.0.0.1 / localhost / [::1] keeps every legitimate same-origin client
|
|
224
|
+
* working while refusing rebounded ones.
|
|
201
225
|
*/
|
|
226
|
+
const LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
|
|
227
|
+
"127.0.0.1",
|
|
228
|
+
"localhost",
|
|
229
|
+
"[::1]",
|
|
230
|
+
"::1"
|
|
231
|
+
]);
|
|
232
|
+
function isLoopbackHost(host) {
|
|
233
|
+
let bare = host;
|
|
234
|
+
if (host.startsWith("[")) {
|
|
235
|
+
const end = host.indexOf("]");
|
|
236
|
+
if (end !== -1) bare = host.slice(0, end + 1);
|
|
237
|
+
} else {
|
|
238
|
+
const colon = host.indexOf(":");
|
|
239
|
+
if (colon !== -1) bare = host.slice(0, colon);
|
|
240
|
+
}
|
|
241
|
+
return LOOPBACK_HOSTS.has(bare) || LOOPBACK_HOSTS.has(host);
|
|
242
|
+
}
|
|
202
243
|
function isSameOriginRequest(req) {
|
|
203
244
|
const site = req.headers["sec-fetch-site"];
|
|
204
245
|
if (typeof site === "string" && site === "cross-site") return false;
|
|
246
|
+
const host = req.headers.host;
|
|
247
|
+
if (typeof host !== "string" || !isLoopbackHost(host)) return false;
|
|
205
248
|
const origin = req.headers.origin;
|
|
206
|
-
if (typeof origin === "string" && origin !== "" && origin !== "null") {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
return new URL(origin).host === host;
|
|
211
|
-
} catch {
|
|
212
|
-
return false;
|
|
213
|
-
}
|
|
249
|
+
if (typeof origin === "string" && origin !== "" && origin !== "null") try {
|
|
250
|
+
return new URL(origin).host === host;
|
|
251
|
+
} catch {
|
|
252
|
+
return false;
|
|
214
253
|
}
|
|
215
254
|
return true;
|
|
216
255
|
}
|
|
@@ -292,6 +331,12 @@ function validateSectionBody(body, home) {
|
|
|
292
331
|
if (raw === void 0) continue;
|
|
293
332
|
if (typeof raw !== "number" || !Number.isFinite(raw) || raw < min || raw > max) return "invalid-range";
|
|
294
333
|
}
|
|
334
|
+
const enabled = body.enabled;
|
|
335
|
+
if (enabled !== void 0 && typeof enabled !== "boolean") return "invalid-enabled";
|
|
336
|
+
const timeline = body.timeline;
|
|
337
|
+
if (timeline !== void 0 && typeof timeline !== "boolean") return "invalid-timeline";
|
|
338
|
+
const fit = body.fit;
|
|
339
|
+
if (fit !== void 0 && fit !== "" && !FIT_MODES.includes(fit)) return "invalid-fit";
|
|
295
340
|
return null;
|
|
296
341
|
}
|
|
297
342
|
/**
|
|
@@ -324,13 +369,14 @@ function storeUpload(bytes, mime, homeOverride) {
|
|
|
324
369
|
url: `${BACKGROUND_API_PREFIX}/image/${id}`
|
|
325
370
|
};
|
|
326
371
|
}
|
|
327
|
-
/** Resolve an upload id to its on-disk path (or "" if invalid / missing).
|
|
372
|
+
/** Resolve an upload id to its on-disk path (or "" if invalid / missing).
|
|
373
|
+
* Read-only: unlike the write path this never creates the plugin home, so a
|
|
374
|
+
* GET cannot pay for (or race) a mkdirSync on every request. */
|
|
328
375
|
function uploadPath(id, homeOverride) {
|
|
329
376
|
if (!isUploadId(id)) return "";
|
|
330
|
-
const dir =
|
|
377
|
+
const dir = pluginHome(homeOverride);
|
|
331
378
|
for (const ext of [
|
|
332
379
|
"jpg",
|
|
333
|
-
"jpeg",
|
|
334
380
|
"png",
|
|
335
381
|
"webp",
|
|
336
382
|
"gif"
|
|
@@ -478,7 +524,7 @@ function makeBackgroundRoutes(settings, opts = {}) {
|
|
|
478
524
|
{
|
|
479
525
|
kind: "prefix",
|
|
480
526
|
path: `${BACKGROUND_API_PREFIX}/image`,
|
|
481
|
-
handler: (req, res) => {
|
|
527
|
+
handler: async (req, res) => {
|
|
482
528
|
if (!isSameOriginRequest(req)) {
|
|
483
529
|
json(res, 403, {
|
|
484
530
|
ok: false,
|
|
@@ -497,11 +543,12 @@ function makeBackgroundRoutes(settings, opts = {}) {
|
|
|
497
543
|
return;
|
|
498
544
|
}
|
|
499
545
|
try {
|
|
500
|
-
const data =
|
|
546
|
+
const data = await readFile(abs);
|
|
501
547
|
const mime = mimeForPath(abs) ?? "application/octet-stream";
|
|
502
548
|
const stat = statSync(abs);
|
|
503
549
|
res.writeHead(200, {
|
|
504
550
|
"content-type": mime,
|
|
551
|
+
"x-content-type-options": "nosniff",
|
|
505
552
|
"content-length": String(data.length),
|
|
506
553
|
"last-modified": stat.mtime.toUTCString(),
|
|
507
554
|
"cache-control": "public, max-age=31536000, immutable"
|
|
@@ -528,7 +575,7 @@ const BACKGROUND_NAMESPACE = settingsNamespace(BACKGROUND_SETTINGS_NAMESPACE);
|
|
|
528
575
|
*/
|
|
529
576
|
function apply(ctx) {
|
|
530
577
|
ctx.inject(["settings", "webServer"], (hostCtx) => {
|
|
531
|
-
hostCtx.settings.register(BACKGROUND_NAMESPACE, BackgroundSettingsSchema, { applies: "live" });
|
|
578
|
+
if (!hostCtx.settings.describe().some((descriptor) => descriptor.ns === "ui-background")) hostCtx.settings.register(BACKGROUND_NAMESPACE, BackgroundSettingsSchema, { applies: "live" });
|
|
532
579
|
try {
|
|
533
580
|
for (const route of makeBackgroundRoutes(hostCtx.settings)) hostCtx.effect(() => hostCtx.webServer.register(route), "deepseek-harness-background: settings route");
|
|
534
581
|
} catch (error) {
|
package/package.json
CHANGED
|
@@ -1,88 +1,86 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "deepseek-harness-background",
|
|
3
|
-
"description": "Custom background image plugin for the DeepSeek Harness web GUI: upload a local picture or paste an image URL, and render it behind the whole app surface with adjustable opacity, scrim, panel transparency and frosted-glass blur.",
|
|
4
|
-
"version": "0.
|
|
5
|
-
"keywords": [
|
|
6
|
-
"deepseek-harness",
|
|
7
|
-
"dsh",
|
|
8
|
-
"plugin",
|
|
9
|
-
"dsh-plugin",
|
|
10
|
-
"theme",
|
|
11
|
-
"wallpaper",
|
|
12
|
-
"background",
|
|
13
|
-
"web-ui"
|
|
14
|
-
],
|
|
15
|
-
"author": "HaoyueQin",
|
|
16
|
-
"license": "MIT",
|
|
17
|
-
"repository": {
|
|
18
|
-
"type": "git",
|
|
19
|
-
"url": "git+https://github.com/HaoyueQin/deepseek-harness-background.git"
|
|
20
|
-
},
|
|
21
|
-
"homepage": "https://github.com/HaoyueQin/deepseek-harness-background",
|
|
22
|
-
"packageManager": "pnpm@11.9.0",
|
|
23
|
-
"type": "module",
|
|
24
|
-
"main": "lib/index.js",
|
|
25
|
-
"exports": {
|
|
26
|
-
".": "./lib/index.js",
|
|
27
|
-
"./client": "./lib/client.js",
|
|
28
|
-
"./
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"@deepseek-ai/dsh-client-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
"@deepseek-ai/
|
|
59
|
-
"
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
"@deepseek-ai/
|
|
69
|
-
"@deepseek-ai/dsh-
|
|
70
|
-
"@deepseek-ai/dsh-
|
|
71
|
-
"@deepseek-ai/
|
|
72
|
-
"@
|
|
73
|
-
"@
|
|
74
|
-
"@
|
|
75
|
-
"@
|
|
76
|
-
"@types/
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
"
|
|
82
|
-
"
|
|
83
|
-
"
|
|
84
|
-
"
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "deepseek-harness-background",
|
|
3
|
+
"description": "Custom background image plugin for the DeepSeek Harness web GUI: upload a local picture or paste an image URL, and render it behind the whole app surface with adjustable opacity, scrim, panel transparency and frosted-glass blur.",
|
|
4
|
+
"version": "0.3.1",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"deepseek-harness",
|
|
7
|
+
"dsh",
|
|
8
|
+
"plugin",
|
|
9
|
+
"dsh-plugin",
|
|
10
|
+
"theme",
|
|
11
|
+
"wallpaper",
|
|
12
|
+
"background",
|
|
13
|
+
"web-ui"
|
|
14
|
+
],
|
|
15
|
+
"author": "HaoyueQin",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/HaoyueQin/deepseek-harness-background.git"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/HaoyueQin/deepseek-harness-background",
|
|
22
|
+
"packageManager": "pnpm@11.9.0",
|
|
23
|
+
"type": "module",
|
|
24
|
+
"main": "lib/index.js",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": "./lib/index.js",
|
|
27
|
+
"./client": "./lib/client.js",
|
|
28
|
+
"./package.json": "./package.json"
|
|
29
|
+
},
|
|
30
|
+
"dsh": {
|
|
31
|
+
"bundle": {
|
|
32
|
+
"patch": "./cordis.patch.yml"
|
|
33
|
+
},
|
|
34
|
+
"client": {
|
|
35
|
+
"inject": [
|
|
36
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
37
|
+
"@deepseek-ai/dsh-client-locale"
|
|
38
|
+
],
|
|
39
|
+
"platform": "web"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsdown",
|
|
44
|
+
"prepare": "tsdown",
|
|
45
|
+
"watch": "tsdown --watch",
|
|
46
|
+
"test": "vitest run",
|
|
47
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
48
|
+
},
|
|
49
|
+
"files": [
|
|
50
|
+
"lib",
|
|
51
|
+
"cordis.patch.yml",
|
|
52
|
+
"README.md",
|
|
53
|
+
"README.zh.md"
|
|
54
|
+
],
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
57
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.1-rc.2",
|
|
58
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.1-rc.2",
|
|
59
|
+
"react": "^18.2.0"
|
|
60
|
+
},
|
|
61
|
+
"dependencies": {
|
|
62
|
+
"@deepseek-ai/dsh-settings": "^0.1.1-rc.2",
|
|
63
|
+
"@deepseek-ai/schemastery": "^3.18.1"
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
67
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.1-rc.2",
|
|
68
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.1-rc.2",
|
|
69
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.1-rc.2",
|
|
70
|
+
"@deepseek-ai/dsh-settings": "^0.1.1-rc.2",
|
|
71
|
+
"@deepseek-ai/schemastery": "^3.18.1",
|
|
72
|
+
"@testing-library/dom": "^10.4.1",
|
|
73
|
+
"@testing-library/react": "^16.3.0",
|
|
74
|
+
"@types/node": "^22.0.0",
|
|
75
|
+
"@types/react": "~18.3.1",
|
|
76
|
+
"@types/react-dom": "~18.3.0",
|
|
77
|
+
"clsx": "^2.1.1",
|
|
78
|
+
"jsdom": "^29.1.1",
|
|
79
|
+
"lightningcss": "^1.33.0",
|
|
80
|
+
"react": "^18.2.0",
|
|
81
|
+
"react-dom": "^18.2.0",
|
|
82
|
+
"tsdown": "^0.22.14",
|
|
83
|
+
"typescript": "^5.8.0",
|
|
84
|
+
"vitest": "^4.1.8"
|
|
85
|
+
}
|
|
86
|
+
}
|