wallpaper-engine-dsh 0.1.5 → 0.1.7
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/LICENSE +21 -0
- package/README.md +6 -5
- package/README.zh.md +5 -4
- package/lib/client.js +290 -53
- package/lib/core.js +68 -52
- package/lib/index.js +184 -110
- package/package.json +7 -3
package/lib/index.js
CHANGED
|
@@ -6,17 +6,21 @@
|
|
|
6
6
|
* All parsing/enumeration logic lives in the pure layer (lib/core.js); this
|
|
7
7
|
* file only wires it into same-origin HTTP routes on `ctx.webServer`:
|
|
8
8
|
*
|
|
9
|
-
* GET /we-background/inventory[?refresh=1] → { installDir,
|
|
9
|
+
* GET /we-background/inventory[?refresh=1] → { installDir, total,
|
|
10
|
+
* portableCount, wallpapers, playlists }
|
|
10
11
|
* GET /we-background/media/<token>[/<sub-asset>…] → video / web HTML + assets
|
|
11
12
|
* GET /we-background/preview/<token> → preview image
|
|
12
13
|
*
|
|
13
14
|
* Security model
|
|
14
|
-
* - Tokens are 72-bit random values
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* - Tokens are 72-bit random values (not reversible encodings of paths).
|
|
16
|
+
* Knowing a filesystem path grants nothing; only files enumerated into
|
|
17
|
+
* the inventory are reachable. Tokens are stable per wallpaper across
|
|
18
|
+
* TTL-triggered rebuilds (so an in-flight video stream is never killed
|
|
19
|
+
* by a background refresh) and re-minted on an explicit ?refresh=1, so
|
|
20
|
+
* deliberately stale URLs die.
|
|
18
21
|
* - Web-wallpaper sub-assets resolve strictly INSIDE their project
|
|
19
|
-
* directory (core.isInsideDir) — `..`
|
|
22
|
+
* directory (core.isInsideDir) — `..` and backslash traversal are
|
|
23
|
+
* answered 403.
|
|
20
24
|
* - project.json-declared files must also be inside the project directory
|
|
21
25
|
* (core.readProject), so a hostile workshop item cannot point the server
|
|
22
26
|
* at arbitrary files.
|
|
@@ -26,6 +30,9 @@
|
|
|
26
30
|
* - Everything is loopback same-origin; `X-Content-Type-Options: nosniff`
|
|
27
31
|
* on every media response.
|
|
28
32
|
*
|
|
33
|
+
* Async: discovery (reg.exe, vdf scan) and inventory builds run through
|
|
34
|
+
* fs/promises, so a rebuild never blocks the event loop mid-stream.
|
|
35
|
+
*
|
|
29
36
|
* Lifecycle: every route registers through the plugin fiber and unwinds on
|
|
30
37
|
* unload; the inventory cache and token map are dropped with it. The plugin
|
|
31
38
|
* contributes no model-visible tool and no prompt text.
|
|
@@ -33,11 +40,9 @@
|
|
|
33
40
|
|
|
34
41
|
import {
|
|
35
42
|
createReadStream,
|
|
36
|
-
|
|
37
|
-
readFileSync,
|
|
38
|
-
statSync,
|
|
43
|
+
promises as fsp,
|
|
39
44
|
} from 'node:fs';
|
|
40
|
-
import {
|
|
45
|
+
import { execFile } from 'node:child_process';
|
|
41
46
|
import { randomBytes } from 'node:crypto';
|
|
42
47
|
import { join } from 'node:path';
|
|
43
48
|
|
|
@@ -47,133 +52,197 @@ import * as core from './core.js';
|
|
|
47
52
|
const BASE = '/we-background';
|
|
48
53
|
/** Inventory is cached and rebuilt at most this often unless ?refresh=1. */
|
|
49
54
|
const INVENTORY_TTL_MS = 30_000;
|
|
55
|
+
/** Steam layout barely moves; re-discover at most this often on TTL builds. */
|
|
56
|
+
const DISCOVERY_TTL_MS = 10 * 60_000;
|
|
57
|
+
|
|
58
|
+
/** fs/promises-backed io for the pure layer (awaited there). */
|
|
59
|
+
const asyncIo = {
|
|
60
|
+
existsSync: async (p) => { try { await fsp.access(p); return true; } catch { return false; } },
|
|
61
|
+
readFileSync: (p, encoding) => fsp.readFile(p, encoding),
|
|
62
|
+
readdirSync: (p) => fsp.readdir(p),
|
|
63
|
+
statSync: (p) => fsp.stat(p),
|
|
64
|
+
};
|
|
50
65
|
|
|
51
66
|
/** Steam root recorded by the Windows installer; null elsewhere/on failure. */
|
|
52
67
|
function steamRegistryRoot() {
|
|
53
|
-
if (process.platform !== 'win32') return null;
|
|
54
|
-
|
|
68
|
+
if (process.platform !== 'win32') return Promise.resolve(null);
|
|
69
|
+
return new Promise((resolvePromise) => {
|
|
55
70
|
const reg = join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'reg.exe');
|
|
56
|
-
|
|
71
|
+
execFile(
|
|
57
72
|
reg,
|
|
58
73
|
['query', 'HKCU\\Software\\Valve\\Steam', '/v', 'SteamPath'],
|
|
59
|
-
{ encoding: 'utf8', windowsHide: true, timeout: 5000
|
|
74
|
+
{ encoding: 'utf8', windowsHide: true, timeout: 5000 },
|
|
75
|
+
(err, stdout) => {
|
|
76
|
+
if (err) { resolvePromise(null); return; }
|
|
77
|
+
resolvePromise(core.steamPathFromRegQuery(stdout));
|
|
78
|
+
},
|
|
60
79
|
);
|
|
61
|
-
|
|
62
|
-
} catch {
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
80
|
+
});
|
|
65
81
|
}
|
|
66
82
|
|
|
67
83
|
/** Full discovery pass: install dir + every library owning WE content. */
|
|
68
|
-
function discover() {
|
|
84
|
+
async function discover() {
|
|
69
85
|
const probes = core.steamProbeDirs();
|
|
70
|
-
const registryRoot = steamRegistryRoot();
|
|
86
|
+
const registryRoot = await steamRegistryRoot();
|
|
71
87
|
const searchRoots = registryRoot ? [registryRoot, ...probes] : probes;
|
|
72
88
|
const vdfLibraries = [];
|
|
73
89
|
for (const root of searchRoots) {
|
|
74
90
|
const vdf = join(root, 'steamapps', 'libraryfolders.vdf');
|
|
75
|
-
if (!existsSync(vdf)) continue;
|
|
91
|
+
if (!await asyncIo.existsSync(vdf)) continue;
|
|
76
92
|
try {
|
|
77
|
-
vdfLibraries.push(...core.librariesFromVdfText(readFileSync(vdf, 'utf8')));
|
|
93
|
+
vdfLibraries.push(...core.librariesFromVdfText(await asyncIo.readFileSync(vdf, 'utf8')));
|
|
78
94
|
} catch { /* unreadable vdf → skip this root */ }
|
|
79
95
|
}
|
|
80
|
-
const installDir = core.findInstallDir(
|
|
96
|
+
const installDir = await core.findInstallDir(
|
|
81
97
|
core.installDirCandidates({ registryRoot, libraryRoots: vdfLibraries }),
|
|
98
|
+
asyncIo,
|
|
82
99
|
);
|
|
83
|
-
const libraryRoots = core.workshopLibraryRoots(searchRoots, vdfLibraries);
|
|
100
|
+
const libraryRoots = await core.workshopLibraryRoots(searchRoots, vdfLibraries, asyncIo);
|
|
84
101
|
return { installDir, libraryRoots };
|
|
85
102
|
}
|
|
86
103
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Register all routes on a webServer-like registry and return a disposer.
|
|
106
|
+
* Extracted from apply() so integration tests can drive the HTTP surface
|
|
107
|
+
* against a fixture discovery without a live Steam install.
|
|
108
|
+
*
|
|
109
|
+
* @param {{ register(route: { kind: 'exact'|'prefix', path: string, handler: Function }): () => void }} webServer
|
|
110
|
+
* @param {{ discover?: () => Promise<{ installDir: string|null, libraryRoots: string[] }>, inventoryTtlMs?: number, discoveryTtlMs?: number }} [opts]
|
|
111
|
+
*/
|
|
112
|
+
export function createRouteRegistrar(webServer, opts = {}) {
|
|
113
|
+
const discoverFn = opts.discover || discover;
|
|
114
|
+
const inventoryTtlMs = opts.inventoryTtlMs ?? INVENTORY_TTL_MS;
|
|
115
|
+
const discoveryTtlMs = opts.discoveryTtlMs ?? DISCOVERY_TTL_MS;
|
|
94
116
|
|
|
95
117
|
// ── Token registry ──────────────────────────────────────────────────────
|
|
96
|
-
// token → { abs, rootDir|null }. rootDir is set for renderable projects
|
|
97
|
-
//
|
|
118
|
+
// token → { abs, rootDir|null }. rootDir is set for renderable web projects
|
|
119
|
+
// so their bundled sub-assets can be fetched (contained, see below).
|
|
120
|
+
// Tokens are keyed per wallpaper asset (`<id>:media` / `<id>:preview`):
|
|
121
|
+
// TTL rebuilds REUSE the token for an unchanged asset (in-flight streams
|
|
122
|
+
// survive), prune assets that vanished, and only an explicit ?refresh=1
|
|
123
|
+
// re-mints everything.
|
|
98
124
|
const tokens = new Map();
|
|
125
|
+
const tokenByKey = new Map();
|
|
99
126
|
const mint = (entry) => {
|
|
127
|
+
const key = entry && entry.key;
|
|
128
|
+
if (key && tokenByKey.has(key)) return tokenByKey.get(key);
|
|
100
129
|
const token = randomBytes(9).toString('base64url'); // 72 bits, url-safe
|
|
101
130
|
tokens.set(token, entry);
|
|
131
|
+
if (key) tokenByKey.set(key, token);
|
|
102
132
|
return token;
|
|
103
133
|
};
|
|
134
|
+
const remintAll = () => { tokens.clear(); tokenByKey.clear(); };
|
|
135
|
+
const pruneTokens = (payload) => {
|
|
136
|
+
const live = new Set();
|
|
137
|
+
for (const w of payload.wallpapers) {
|
|
138
|
+
for (const url of [w.media, w.preview]) {
|
|
139
|
+
if (!url) continue;
|
|
140
|
+
const token = url.slice(url.lastIndexOf('/') + 1);
|
|
141
|
+
live.add(token);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
for (const token of [...tokens.keys()]) {
|
|
145
|
+
if (!live.has(token)) {
|
|
146
|
+
const entry = tokens.get(token);
|
|
147
|
+
tokens.delete(token);
|
|
148
|
+
if (entry && entry.key && tokenByKey.get(entry.key) === token) {
|
|
149
|
+
tokenByKey.delete(entry.key);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
};
|
|
104
154
|
|
|
105
155
|
// ── Inventory (cached) ──────────────────────────────────────────────────
|
|
106
156
|
let cache = null; // { at: number, payload: object }
|
|
157
|
+
let discoveryCache = null; // { at: number, value: object }
|
|
158
|
+
let building = null; // in-flight build promise (request coalescing)
|
|
107
159
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
discoveryCache = discover();
|
|
115
|
-
return discoveryCache;
|
|
160
|
+
async function getDiscovery(force) {
|
|
161
|
+
const fresh = discoveryCache && Date.now() - discoveryCache.at < discoveryTtlMs;
|
|
162
|
+
if (!force && fresh) return discoveryCache.value;
|
|
163
|
+
const value = await discoverFn();
|
|
164
|
+
discoveryCache = { at: Date.now(), value };
|
|
165
|
+
return value;
|
|
116
166
|
}
|
|
117
167
|
|
|
118
|
-
function buildInventory(force) {
|
|
119
|
-
|
|
120
|
-
|
|
168
|
+
async function buildInventory(force) {
|
|
169
|
+
if (force) remintAll(); // explicit refresh: stale URLs die on purpose
|
|
170
|
+
const payload = await core.buildInventoryFrom(await getDiscovery(force), {
|
|
171
|
+
...asyncIo,
|
|
172
|
+
mint,
|
|
173
|
+
base: BASE,
|
|
174
|
+
});
|
|
175
|
+
pruneTokens(payload);
|
|
176
|
+
return payload;
|
|
121
177
|
}
|
|
122
178
|
|
|
123
179
|
function getInventory(force) {
|
|
124
|
-
if (!force && cache && Date.now() - cache.at <
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
180
|
+
if (!force && cache && Date.now() - cache.at < inventoryTtlMs) {
|
|
181
|
+
return Promise.resolve(cache.payload);
|
|
182
|
+
}
|
|
183
|
+
// Coalesce concurrent builds (double-clicked 刷新, parallel clients).
|
|
184
|
+
if (building) return building;
|
|
185
|
+
building = buildInventory(force)
|
|
186
|
+
.then((payload) => { cache = { at: Date.now(), payload }; return payload; })
|
|
187
|
+
.finally(() => { building = null; });
|
|
188
|
+
return building;
|
|
128
189
|
}
|
|
129
190
|
|
|
130
191
|
// ── Response helpers ────────────────────────────────────────────────────
|
|
131
|
-
function sendJson(res, status, body) {
|
|
192
|
+
function sendJson(req, res, status, body) {
|
|
193
|
+
const text = JSON.stringify(body);
|
|
132
194
|
res.statusCode = status;
|
|
133
195
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
134
196
|
res.setHeader('Cache-Control', 'no-store');
|
|
135
|
-
res.
|
|
197
|
+
res.setHeader('Content-Length', String(Buffer.byteLength(text)));
|
|
198
|
+
res.end(req.method === 'HEAD' ? undefined : text);
|
|
136
199
|
}
|
|
137
200
|
|
|
138
201
|
/** Stream one file with Range support (video seeking) and HEAD handling. */
|
|
139
202
|
function serveFile(absPath, req, res) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
203
|
+
fsp.stat(absPath).then((st) => {
|
|
204
|
+
if (!st.isFile()) {
|
|
205
|
+
res.statusCode = 404;
|
|
206
|
+
res.setHeader('Content-Length', '0');
|
|
207
|
+
res.end();
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
res.setHeader('Content-Type', core.mimeFor(absPath));
|
|
211
|
+
res.setHeader('Accept-Ranges', 'bytes');
|
|
212
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
213
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
151
214
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
215
|
+
const range = core.parseRangeHeader(req.headers.range, st.size);
|
|
216
|
+
if (range && range.error) {
|
|
217
|
+
res.statusCode = 416;
|
|
218
|
+
res.setHeader('Content-Range', `bytes */${st.size}`);
|
|
219
|
+
res.setHeader('Content-Length', '0');
|
|
220
|
+
res.end();
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const head = req.method === 'HEAD';
|
|
224
|
+
if (range) {
|
|
225
|
+
res.statusCode = 206;
|
|
226
|
+
res.setHeader('Content-Range', `bytes ${range.start}-${range.end}/${st.size}`);
|
|
227
|
+
res.setHeader('Content-Length', String(range.end - range.start + 1));
|
|
228
|
+
} else {
|
|
229
|
+
res.setHeader('Content-Length', String(st.size));
|
|
230
|
+
}
|
|
231
|
+
if (head) { res.end(); return; }
|
|
168
232
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
233
|
+
const stream = range
|
|
234
|
+
? createReadStream(absPath, { start: range.start, end: range.end })
|
|
235
|
+
: createReadStream(absPath);
|
|
236
|
+
// The file can vanish between stat and open; ending normally would
|
|
237
|
+
// leave the already-announced Content-Length unsatisfied and the
|
|
238
|
+
// client hanging — destroy the response instead.
|
|
239
|
+
stream.on('error', () => { res.destroy(); });
|
|
240
|
+
stream.pipe(res);
|
|
241
|
+
}).catch(() => {
|
|
242
|
+
res.statusCode = 404;
|
|
243
|
+
res.setHeader('Content-Length', '0');
|
|
174
244
|
res.end();
|
|
175
245
|
});
|
|
176
|
-
stream.pipe(res);
|
|
177
246
|
}
|
|
178
247
|
|
|
179
248
|
// ── Routes ──────────────────────────────────────────────────────────────
|
|
@@ -184,20 +253,15 @@ export function apply(ctx) {
|
|
|
184
253
|
path: `${BASE}/inventory`,
|
|
185
254
|
handler: (req, res) => {
|
|
186
255
|
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
187
|
-
sendJson(res, 405, { error: 'method not allowed' });
|
|
256
|
+
sendJson(req, res, 405, { error: 'method not allowed' });
|
|
188
257
|
return;
|
|
189
258
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
res.setHeader('Content-Length', String(Buffer.byteLength(payload)));
|
|
197
|
-
res.end(req.method === 'HEAD' ? undefined : payload);
|
|
198
|
-
} catch (err) {
|
|
199
|
-
sendJson(res, 500, { error: String(err && err.message ? err.message : err) });
|
|
200
|
-
}
|
|
259
|
+
const url = new URL(req.url || '/', 'http://x');
|
|
260
|
+
getInventory(url.searchParams.get('refresh') === '1')
|
|
261
|
+
.then((payload) => sendJson(req, res, 200, payload))
|
|
262
|
+
.catch((err) => {
|
|
263
|
+
sendJson(req, res, 500, { error: String(err && err.message ? err.message : err) });
|
|
264
|
+
});
|
|
201
265
|
},
|
|
202
266
|
}));
|
|
203
267
|
|
|
@@ -208,14 +272,14 @@ export function apply(ctx) {
|
|
|
208
272
|
path: `${BASE}/${seg}`,
|
|
209
273
|
handler: (req, res) => {
|
|
210
274
|
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
211
|
-
sendJson(res, 405, { error: 'method not allowed' });
|
|
275
|
+
sendJson(req, res, 405, { error: 'method not allowed' });
|
|
212
276
|
return;
|
|
213
277
|
}
|
|
214
278
|
let pathname;
|
|
215
279
|
try {
|
|
216
280
|
pathname = new URL(req.url || '/', 'http://x').pathname;
|
|
217
281
|
} catch {
|
|
218
|
-
sendJson(res, 400, { error: 'bad request' });
|
|
282
|
+
sendJson(req, res, 400, { error: 'bad request' });
|
|
219
283
|
return;
|
|
220
284
|
}
|
|
221
285
|
const rest = pathname.slice(prefix.length); // "<token>" | "<token>/<rel…>"
|
|
@@ -223,14 +287,14 @@ export function apply(ctx) {
|
|
|
223
287
|
const rawToken = slash < 0 ? rest : rest.slice(0, slash);
|
|
224
288
|
let token;
|
|
225
289
|
try { token = decodeURIComponent(rawToken); } catch {
|
|
226
|
-
sendJson(res, 400, { error: 'bad token' });
|
|
290
|
+
sendJson(req, res, 400, { error: 'bad token' });
|
|
227
291
|
return;
|
|
228
292
|
}
|
|
229
293
|
const entry = tokens.get(token);
|
|
230
294
|
if (!entry) {
|
|
231
295
|
res.statusCode = 404;
|
|
232
|
-
|
|
233
|
-
|
|
296
|
+
res.setHeader('Content-Length', '0');
|
|
297
|
+
res.end();
|
|
234
298
|
return;
|
|
235
299
|
}
|
|
236
300
|
let abs = entry.abs;
|
|
@@ -238,22 +302,25 @@ export function apply(ctx) {
|
|
|
238
302
|
// Sub-asset of a bundled web wallpaper: must stay inside its dir.
|
|
239
303
|
if (seg !== 'media' || !entry.rootDir) {
|
|
240
304
|
res.statusCode = 404;
|
|
241
|
-
|
|
242
|
-
|
|
305
|
+
res.setHeader('Content-Length', '0');
|
|
306
|
+
res.end();
|
|
243
307
|
return;
|
|
244
308
|
}
|
|
245
309
|
let rel;
|
|
246
310
|
try { rel = decodeURIComponent(rest.slice(slash + 1)); } catch {
|
|
247
|
-
sendJson(res, 400, { error: 'bad path' });
|
|
311
|
+
sendJson(req, res, 400, { error: 'bad path' });
|
|
248
312
|
return;
|
|
249
313
|
}
|
|
250
|
-
|
|
251
|
-
|
|
314
|
+
// Backslash parts are never legitimate in a URL path here; on win32
|
|
315
|
+
// `..\evil` would slip past a `/`-only split (the isInsideDir
|
|
316
|
+
// backstop still catches it — this is defense in depth).
|
|
317
|
+
if (!rel || rel.split('/').some((part) => part === '..' || part === '' || part.includes('\\'))) {
|
|
318
|
+
sendJson(req, res, 403, { error: 'forbidden' });
|
|
252
319
|
return;
|
|
253
320
|
}
|
|
254
321
|
abs = join(entry.rootDir, ...rel.split('/'));
|
|
255
322
|
if (!core.isInsideDir(entry.rootDir, abs)) {
|
|
256
|
-
sendJson(res, 403, { error: 'forbidden' });
|
|
323
|
+
sendJson(req, res, 403, { error: 'forbidden' });
|
|
257
324
|
return;
|
|
258
325
|
}
|
|
259
326
|
}
|
|
@@ -262,17 +329,24 @@ export function apply(ctx) {
|
|
|
262
329
|
}));
|
|
263
330
|
}
|
|
264
331
|
|
|
265
|
-
// Warm the discovery cache now (
|
|
266
|
-
// request answers
|
|
267
|
-
|
|
332
|
+
// Warm the discovery cache now (async, off the request path) so the first
|
|
333
|
+
// inventory request answers quickly.
|
|
334
|
+
getDiscovery(false).catch(() => { /* non-fatal: inventory will 500 with reason */ });
|
|
268
335
|
|
|
269
336
|
return () => {
|
|
270
337
|
for (const d of disposers) { try { d(); } catch { /* ignore */ } }
|
|
271
338
|
disposers.length = 0;
|
|
272
|
-
|
|
339
|
+
remintAll();
|
|
273
340
|
cache = null;
|
|
274
341
|
discoveryCache = null;
|
|
275
342
|
};
|
|
276
343
|
}
|
|
277
344
|
|
|
345
|
+
export const inject = ['webServer'];
|
|
346
|
+
|
|
347
|
+
export function apply(ctx) {
|
|
348
|
+
// inject guarantees the service is present before apply runs.
|
|
349
|
+
return createRouteRegistrar(ctx.webServer);
|
|
350
|
+
}
|
|
351
|
+
|
|
278
352
|
export default { inject, apply };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wallpaper-engine-dsh",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Wallpaper Engine
|
|
3
|
+
"version": "0.1.7",
|
|
4
|
+
"description": "Wallpaper Engine library as the DSH web GUI background — video/web/still wallpapers, rotation lists, search, resource monitor, bilingual UI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|
|
@@ -9,10 +9,14 @@
|
|
|
9
9
|
"type": "git",
|
|
10
10
|
"url": "git+https://github.com/Weilv-D/wallpaper-engine-dsh.git"
|
|
11
11
|
},
|
|
12
|
+
"bugs": "https://github.com/Weilv-D/wallpaper-engine-dsh/issues",
|
|
13
|
+
"homepage": "https://github.com/Weilv-D/wallpaper-engine-dsh#readme",
|
|
12
14
|
"keywords": [
|
|
13
15
|
"dsh",
|
|
14
16
|
"dsh-plugin",
|
|
17
|
+
"deepseek",
|
|
15
18
|
"deepseek-harness",
|
|
19
|
+
"cordis",
|
|
16
20
|
"wallpaper",
|
|
17
21
|
"wallpaper-engine",
|
|
18
22
|
"background",
|
|
@@ -59,7 +63,7 @@
|
|
|
59
63
|
"scripts": {
|
|
60
64
|
"build": "node scripts/build-client.mjs",
|
|
61
65
|
"verify": "node scripts/verify-client.mjs",
|
|
62
|
-
"test": "node --test test/core.test.mjs",
|
|
66
|
+
"test": "node --test test/core.test.mjs test/host.test.mjs",
|
|
63
67
|
"prepare": "node scripts/prepare.mjs",
|
|
64
68
|
"prepublishOnly": "npm run test && npm run build && npm run verify",
|
|
65
69
|
"release": "npm version patch -m \"chore: release v%s\" && git push --follow-tags",
|