@iasiv5/dsh-skins 1.2.3 → 1.4.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/lib/index.js CHANGED
@@ -1,11 +1,25 @@
1
1
  // src/index.js
2
- import { readFileSync as readFileSync3 } from "node:fs";
3
2
  import { homedir } from "node:os";
4
- import { dirname as dirname4, join as join3 } from "node:path";
5
- import { fileURLToPath } from "node:url";
3
+ import { join as join2 } from "node:path";
6
4
 
7
- // src/host/restart.js
8
- import { spawn } from "node:child_process";
5
+ // src/host/personalization/store.js
6
+ import { createHash, randomUUID } from "node:crypto";
7
+ import {
8
+ closeSync,
9
+ copyFileSync,
10
+ existsSync,
11
+ mkdirSync as mkdirSync2,
12
+ openSync,
13
+ readFileSync,
14
+ readdirSync,
15
+ renameSync as renameSync2,
16
+ rmdirSync,
17
+ statfsSync,
18
+ statSync,
19
+ unlinkSync,
20
+ writeFileSync as writeFileSync2
21
+ } from "node:fs";
22
+ import { join } from "node:path";
9
23
 
10
24
  // src/host/errors.js
11
25
  function codedError(code, message, params) {
@@ -23,280 +37,6 @@ function publicError(error) {
23
37
  return value;
24
38
  }
25
39
 
26
- // src/host/restart.js
27
- var RELAUNCH_HELPER = String.raw`
28
- const { spawn } = require('node:child_process')
29
- const [parentPid, executable, cwd, argsJson] = process.argv.slice(1)
30
- const args = JSON.parse(argsJson)
31
- const waitForParent = () => {
32
- try {
33
- process.kill(Number(parentPid), 0)
34
- setTimeout(waitForParent, 100)
35
- } catch {
36
- setTimeout(() => {
37
- const child = spawn(executable, args, {
38
- cwd,
39
- env: process.env,
40
- detached: false,
41
- stdio: 'ignore',
42
- })
43
- child.once('error', () => process.exit(1))
44
- child.once('exit', code => process.exit(code == null ? 1 : code))
45
- }, 750)
46
- }
47
- }
48
- waitForParent()
49
- `;
50
- var MANAGED_RESTART_EXIT_CODE = 75;
51
- function serviceManagedEnv(env) {
52
- return typeof env === "object" && env !== null && (env.INVOCATION_ID !== void 0 || env.NOTIFY_SOCKET !== void 0);
53
- }
54
- function createRestartScheduler(exit, overrides = {}) {
55
- if (typeof exit !== "function") return { available: false, schedule() {
56
- } };
57
- const runtime = {
58
- execPath: process.execPath,
59
- argv: [...process.argv],
60
- cwd: process.cwd(),
61
- env: process.env,
62
- pid: process.pid,
63
- spawn,
64
- setTimeout,
65
- ...overrides
66
- };
67
- let scheduled = false;
68
- return {
69
- available: true,
70
- schedule() {
71
- if (scheduled) return;
72
- scheduled = true;
73
- if (serviceManagedEnv(runtime.env)) {
74
- const managed = runtime.setTimeout(() => exit(MANAGED_RESTART_EXIT_CODE), 150);
75
- managed.unref?.();
76
- return;
77
- }
78
- const helper = runtime.spawn(runtime.execPath, [
79
- "-e",
80
- RELAUNCH_HELPER,
81
- String(runtime.pid),
82
- runtime.execPath,
83
- runtime.cwd,
84
- JSON.stringify(runtime.argv.slice(1))
85
- ], {
86
- cwd: runtime.cwd,
87
- env: runtime.env,
88
- detached: true,
89
- stdio: "ignore"
90
- });
91
- helper.unref();
92
- const timer = runtime.setTimeout(() => exit(0), 150);
93
- timer.unref?.();
94
- }
95
- };
96
- }
97
- function agentList(agents) {
98
- if (agents === null || agents === void 0 || typeof agents.list !== "function") return null;
99
- try {
100
- const list = agents.list();
101
- return Array.isArray(list) ? list : null;
102
- } catch {
103
- return null;
104
- }
105
- }
106
- function restartSafety(agents) {
107
- const list = agentList(agents);
108
- if (list === null) return { state: "unknown", running: null };
109
- const running = list.filter((agent) => agent?.status === "running").length;
110
- return { state: running > 0 ? "blocked" : "safe", running };
111
- }
112
- async function waitForRestartSafety(agents, confirmUnknown = false) {
113
- const list = agentList(agents);
114
- if (list === null) {
115
- if (!confirmUnknown) {
116
- throw codedError("RESTART_SAFETY_UNKNOWN", "无法确认 Agent 运行状态,请再次确认后重启");
117
- }
118
- return { state: "unknown", running: null };
119
- }
120
- const running = list.filter((agent) => agent?.status === "running").length;
121
- if (running > 0) {
122
- throw codedError("AGENTS_RUNNING", `检测到 ${running} 个 Agent 正在运行,请等待任务结束后再重启`, { count: running });
123
- }
124
- await Promise.all(list.map((agent) => typeof agent?.whenIdle === "function" ? agent.whenIdle() : void 0));
125
- const after = agentList(agents);
126
- if (after === null) {
127
- if (!confirmUnknown) {
128
- throw codedError("RESTART_SAFETY_UNKNOWN", "复核 Agent 状态失败,请再次确认后重启");
129
- }
130
- return { state: "unknown", running: null };
131
- }
132
- const started = after.filter((agent) => agent?.status === "running").length;
133
- if (started > 0) {
134
- throw codedError("AGENTS_RUNNING", `检测到 ${started} 个 Agent 刚刚开始运行,请稍后再重启`, { count: started });
135
- }
136
- return { state: "safe", running: 0 };
137
- }
138
-
139
- // src/host/routes.js
140
- function sendJson(response, status, value) {
141
- response.writeHead(status, {
142
- "content-type": "application/json; charset=utf-8",
143
- "cache-control": "no-store"
144
- });
145
- response.end(JSON.stringify(value));
146
- }
147
- function header(headers, name2) {
148
- const value = headers?.[name2];
149
- return typeof value === "string" ? value : Array.isArray(value) ? value[0] : void 0;
150
- }
151
- function parseAuthority(authority) {
152
- try {
153
- return new URL(`http://${authority}`);
154
- } catch {
155
- return void 0;
156
- }
157
- }
158
- function canonicalAuthority(entry, parsed) {
159
- const port = parsed.port !== "" ? parsed.port : new URL(`https://${entry}`).port;
160
- return port === "" ? parsed.hostname : `${parsed.hostname}:${port}`;
161
- }
162
- function isLoopbackHostname(hostname) {
163
- if (hostname === "localhost" || hostname === "[::1]") return true;
164
- const parts = hostname.split(".");
165
- return parts.length === 4 && parts[0] === "127" && parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255);
166
- }
167
- function isTrustedAuthority(hostUrl, trustedHosts) {
168
- return trustedHosts.some((entry) => {
169
- const parsed = parseAuthority(entry);
170
- if (parsed === void 0) return false;
171
- return canonicalAuthority(entry, parsed) === parsed.hostname ? parsed.hostname === hostUrl.hostname : parsed.host === hostUrl.host;
172
- });
173
- }
174
- function isTrustedRequest(request, trustedHosts = []) {
175
- const host = header(request.headers, "host");
176
- if (host === void 0) return false;
177
- const hostUrl = parseAuthority(host);
178
- if (hostUrl === void 0) return false;
179
- const socketAddress = request.socket?.remoteAddress ?? request.info?.remoteAddress;
180
- if (isLoopbackHostname(hostUrl.hostname)) {
181
- if (typeof socketAddress === "string") {
182
- const loopbackPeer = socketAddress === "127.0.0.1" || socketAddress === "::1" || socketAddress === "::ffff:127.0.0.1";
183
- if (!loopbackPeer) return false;
184
- }
185
- } else if (!isTrustedAuthority(hostUrl, trustedHosts)) {
186
- return false;
187
- }
188
- if (header(request.headers, "sec-fetch-site") === "cross-site") return false;
189
- const origin = header(request.headers, "origin");
190
- if (origin === void 0) return true;
191
- try {
192
- return new URL(origin).host === hostUrl.host;
193
- } catch {
194
- return false;
195
- }
196
- }
197
- async function readJsonBody(request, limit = 4096) {
198
- const type = String(request.headers["content-type"] ?? "").toLowerCase();
199
- if (!type.startsWith("application/json")) throw new Error("content-type must be application/json");
200
- const chunks = [];
201
- let size = 0;
202
- for await (const chunk of request) {
203
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
204
- size += buffer.length;
205
- if (size > limit) throw new Error("request body too large");
206
- chunks.push(buffer);
207
- }
208
- if (size === 0) return {};
209
- const value = JSON.parse(Buffer.concat(chunks).toString("utf8"));
210
- if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error("invalid request body");
211
- return value;
212
- }
213
- function method(request, response, expected) {
214
- if (request.method === expected) return true;
215
- response.writeHead(405, { allow: expected });
216
- response.end();
217
- return false;
218
- }
219
- function mountUpdateRoutes(host, options) {
220
- const { updater, restart, agents, trustedHosts = [] } = options;
221
- const updateRoute = host.webServer.register({
222
- kind: "exact",
223
- path: "/dsh-skins/update",
224
- handler: async (request, response) => {
225
- if (!isTrustedRequest(request, trustedHosts)) return sendJson(response, 403, { error: "trusted DSH Web request required" });
226
- if (request.method === "GET") {
227
- const url = new URL(request.url ?? "/", "http://localhost");
228
- const force = url.searchParams.get("force") === "1" && request.headers["x-dsh-skins-force"] === "1";
229
- try {
230
- const status = await updater.status(force);
231
- return sendJson(response, 200, {
232
- ...status,
233
- operation: updater.currentOperation(),
234
- restartRequired: updater.restartRequired,
235
- restartAvailable: restart.available === true,
236
- restartSafety: restartSafety(agents)
237
- });
238
- } catch (error) {
239
- return sendJson(response, 502, {
240
- ...publicError(error),
241
- operation: updater.currentOperation(),
242
- restartRequired: updater.restartRequired,
243
- restartAvailable: restart.available === true,
244
- restartSafety: restartSafety(agents)
245
- });
246
- }
247
- }
248
- if (!method(request, response, "POST")) return;
249
- try {
250
- return sendJson(response, 202, { operation: updater.startUpdate() });
251
- } catch (error) {
252
- return sendJson(response, 409, publicError(error));
253
- }
254
- }
255
- });
256
- const restartRoute = host.webServer.register({
257
- kind: "exact",
258
- path: "/dsh-skins/restart",
259
- handler: async (request, response) => {
260
- if (!isTrustedRequest(request, trustedHosts)) return sendJson(response, 403, { error: "trusted DSH Web request required" });
261
- if (!method(request, response, "POST")) return;
262
- if (restart.available !== true) {
263
- return sendJson(response, 501, publicError(codedError("RESTART_UNAVAILABLE", "当前 DSH Host 不支持自重启")));
264
- }
265
- if (!updater.restartRequired) {
266
- return sendJson(response, 409, publicError(codedError("NO_PENDING_UPDATE", "当前没有等待重启应用的更新")));
267
- }
268
- try {
269
- const body = await readJsonBody(request);
270
- const confirmUnknown = body.confirmUnknown === true;
271
- await waitForRestartSafety(agents, confirmUnknown);
272
- sendJson(response, 202, { restarting: true });
273
- restart.schedule();
274
- } catch (error) {
275
- sendJson(response, 409, {
276
- ...publicError(error),
277
- restartSafety: restartSafety(agents)
278
- });
279
- }
280
- }
281
- });
282
- return () => {
283
- restartRoute();
284
- updateRoute();
285
- };
286
- }
287
-
288
- // src/host/self-update.js
289
- import { randomUUID } from "node:crypto";
290
- import {
291
- existsSync,
292
- mkdirSync as mkdirSync2,
293
- readFileSync,
294
- renameSync as renameSync2,
295
- unlinkSync,
296
- writeFileSync as writeFileSync2
297
- } from "node:fs";
298
- import { dirname as dirname2, join } from "node:path";
299
-
300
40
  // src/host/atomic-write.js
301
41
  import { mkdirSync, renameSync, writeFileSync } from "node:fs";
302
42
  import { dirname } from "node:path";
@@ -307,535 +47,6 @@ function atomicWriteText(file, content, fs = { mkdirSync, writeFileSync, renameS
307
47
  fs.renameSync(temporary, file);
308
48
  }
309
49
 
310
- // src/host/self-update.js
311
- var PACKAGE_NAME = "@iasiv5/dsh-skins";
312
- var REPOSITORY = "iasiv5/dsh-skins";
313
- var REGISTRY_LATEST_URL = "https://registry.npmjs.org/@iasiv5%2Fdsh-skins/latest";
314
- var NPM_PACKAGE_URL = "https://www.npmjs.com/package/@iasiv5/dsh-skins";
315
- var CACHE_TTL_MS = 60 * 60 * 1e3;
316
- var CACHE_SCHEMA_VERSION = 2;
317
- var PROFILE_FILES = ["package.json", "pnpm-lock.yaml", "pnpm-workspace.yaml"];
318
- var STABLE_VERSION_RE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
319
- var NPM_SELF_SPEC_RE = /^npm:@iasiv5\/dsh-skins@/i;
320
- function isRecord(value) {
321
- return value !== null && typeof value === "object" && !Array.isArray(value);
322
- }
323
- function parseStableVersion(value) {
324
- const raw = String(value ?? "").trim();
325
- const version = raw.startsWith("v") ? raw.slice(1) : raw;
326
- const match = STABLE_VERSION_RE.exec(version);
327
- if (match === null) return null;
328
- return {
329
- version,
330
- tag: `v${version}`,
331
- parts: match.slice(1).map(Number)
332
- };
333
- }
334
- function compareStableVersions(left, right) {
335
- const a = parseStableVersion(left);
336
- const b = parseStableVersion(right);
337
- if (a === null || b === null) throw new Error("invalid stable semantic version");
338
- for (let index = 0; index < 3; index += 1) {
339
- if (a.parts[index] === b.parts[index]) continue;
340
- return a.parts[index] > b.parts[index] ? 1 : -1;
341
- }
342
- return 0;
343
- }
344
- function repositoryIdentity(value) {
345
- const raw = typeof value === "string" ? value : isRecord(value) && typeof value.url === "string" ? value.url : "";
346
- return raw.trim().replace(/^git\+/, "").replace(/^git@github\.com:/i, "").replace(/^ssh:\/\/git@github\.com\//i, "").replace(/^https?:\/\/github\.com\//i, "").replace(/\.git$/i, "").replace(/\/$/, "").toLowerCase();
347
- }
348
- function detectInstallSource(spec) {
349
- const value = String(spec ?? "").trim();
350
- if (/^link:/i.test(value)) return { kind: "link", spec: value };
351
- if (/^file:/i.test(value) || /\.tgz(?:$|[?#])/i.test(value)) return { kind: "file", spec: value };
352
- if (/^(?:git\+|github:|git@|ssh:\/\/|https?:\/\/)/i.test(value)) return { kind: "github", spec: value };
353
- const range = NPM_SELF_SPEC_RE.test(value) ? value.replace(NPM_SELF_SPEC_RE, "") : value;
354
- if (range === "*" || range === "latest" || /^[\s~^><=]*\d/.test(range)) {
355
- return { kind: "npm", spec: value };
356
- }
357
- return { kind: "unknown", spec: value };
358
- }
359
- function readJson(file, fallback = null) {
360
- try {
361
- return JSON.parse(readFileSync(file, "utf8"));
362
- } catch {
363
- return fallback;
364
- }
365
- }
366
- function readProfileManifest(profileDir) {
367
- return readJson(join(profileDir, "package.json"), {});
368
- }
369
- function readInstalledManifest(profileDir) {
370
- return readJson(join(profileDir, "node_modules", PACKAGE_NAME, "package.json"), null);
371
- }
372
- function captureProfileSnapshot(profileDir) {
373
- return PROFILE_FILES.map((name2) => {
374
- const file = join(profileDir, name2);
375
- return {
376
- name: name2,
377
- exists: existsSync(file),
378
- content: existsSync(file) ? readFileSync(file) : null
379
- };
380
- });
381
- }
382
- function restoreProfileSnapshot(profileDir, snapshot) {
383
- for (const item of snapshot) {
384
- const file = join(profileDir, item.name);
385
- if (item.exists) {
386
- mkdirSync2(dirname2(file), { recursive: true });
387
- const temporary = `${file}.${process.pid}.rollback`;
388
- writeFileSync2(temporary, item.content);
389
- renameSync2(temporary, file);
390
- } else if (existsSync(file)) {
391
- unlinkSync(file);
392
- }
393
- }
394
- }
395
- function preserveProfileBundles(profileDir, snapshot) {
396
- const packageSnapshot = snapshot.find((item) => item.name === "package.json" && item.exists);
397
- if (packageSnapshot?.content === null || packageSnapshot?.content === void 0) return;
398
- let before;
399
- try {
400
- before = JSON.parse(packageSnapshot.content.toString("utf8"));
401
- } catch {
402
- return;
403
- }
404
- const bundles = before.dsh?.profile?.bundles;
405
- if (!Array.isArray(bundles) || !bundles.includes(PACKAGE_NAME)) return;
406
- const current = readProfileManifest(profileDir);
407
- current.dsh = {
408
- ...current.dsh,
409
- profile: {
410
- ...current.dsh?.profile,
411
- bundles: [...bundles]
412
- }
413
- };
414
- atomicWriteText(join(profileDir, "package.json"), `${JSON.stringify(current, null, 2)}
415
- `);
416
- }
417
- function validatePluginManifest(manifest, expectedVersion) {
418
- if (!isRecord(manifest)) throw codedError("RELEASE_MANIFEST_MISSING", "发布的包缺少 package.json");
419
- if (manifest.name !== PACKAGE_NAME) {
420
- throw codedError("RELEASE_NAME_MISMATCH", `发布的包名必须是 ${PACKAGE_NAME}`, { expected: PACKAGE_NAME });
421
- }
422
- if (manifest.version !== expectedVersion) {
423
- throw codedError(
424
- "RELEASE_VERSION_MISMATCH",
425
- `发布版本 v${expectedVersion} 与包版本 ${String(manifest.version ?? "missing")} 不一致`,
426
- { tag: expectedVersion, version: String(manifest.version ?? "missing") }
427
- );
428
- }
429
- if (repositoryIdentity(manifest.repository) !== REPOSITORY.toLowerCase()) {
430
- throw codedError("RELEASE_REPOSITORY_MISMATCH", `发布的包仓库必须是 ${REPOSITORY}`, { repository: REPOSITORY });
431
- }
432
- if (!isRecord(manifest.dsh) || !isRecord(manifest.dsh.client) || manifest.dsh.client.platform !== "web") {
433
- throw codedError("RELEASE_NOT_WEB_PLUGIN", "发布的包不是 DSH Web 客户端插件");
434
- }
435
- if (!isRecord(manifest.dsh.bundle) || typeof manifest.dsh.bundle.patch !== "string") {
436
- throw codedError("RELEASE_NO_BUNDLE_PATCH", "发布的包未声明 DSH bundle patch");
437
- }
438
- return manifest;
439
- }
440
- async function fetchRegistryJson(url, options = {}) {
441
- const response = await fetch(url, {
442
- headers: {
443
- accept: "application/json",
444
- "user-agent": `${PACKAGE_NAME}/${options.currentVersion ?? "unknown"}`
445
- },
446
- signal: options.signal
447
- });
448
- if (!response.ok) {
449
- throw codedError(
450
- "REGISTRY_CHECK_FAILED",
451
- `npm registry 更新检查失败(HTTP ${response.status})`,
452
- { status: response.status }
453
- );
454
- }
455
- return response.json();
456
- }
457
- async function fetchLatestStableRelease(options = {}) {
458
- const manifest = await (options.fetchJson ?? fetchRegistryJson)(REGISTRY_LATEST_URL, options);
459
- if (!isRecord(manifest) || manifest.name !== PACKAGE_NAME) {
460
- throw codedError("REGISTRY_NAME_MISMATCH", "npm registry latest 返回的包名异常", { expected: PACKAGE_NAME });
461
- }
462
- const parsed = parseStableVersion(manifest.version);
463
- if (parsed === null || manifest.version !== parsed.version) {
464
- throw codedError(
465
- "REGISTRY_VERSION_INVALID",
466
- `npm registry latest 不是严格 X.Y.Z 版本:${String(manifest.version ?? "missing")}`,
467
- { version: String(manifest.version ?? "missing") }
468
- );
469
- }
470
- return {
471
- version: parsed.version,
472
- tag: parsed.tag,
473
- htmlUrl: `${NPM_PACKAGE_URL}/v/${parsed.version}`,
474
- name: parsed.tag
475
- };
476
- }
477
- async function resolveReleaseArtifact(release, options = {}) {
478
- const fetchJson = options.fetchJson ?? fetchRegistryJson;
479
- const manifest = await fetchJson(
480
- `https://registry.npmjs.org/@iasiv5%2Fdsh-skins/${release.version}`,
481
- options
482
- );
483
- if (!isRecord(manifest) || !isRecord(manifest.dist) || typeof manifest.dist.integrity !== "string") {
484
- throw codedError("REGISTRY_INTEGRITY_MISSING", "npm registry 版本元数据缺少 dist.integrity");
485
- }
486
- validatePluginManifest(manifest, release.version);
487
- return { ...release, manifest, integrity: manifest.dist.integrity };
488
- }
489
- function cachePayload(currentVersion, checkedAt, release) {
490
- return {
491
- schemaVersion: CACHE_SCHEMA_VERSION,
492
- currentVersion,
493
- checkedAt,
494
- release
495
- };
496
- }
497
- function validCachedRelease(value, currentVersion, now, ttlMs) {
498
- if (!isRecord(value) || value.schemaVersion !== CACHE_SCHEMA_VERSION) return null;
499
- if (value.currentVersion !== currentVersion) return null;
500
- if (!Number.isFinite(value.checkedAt) || now - value.checkedAt < 0 || now - value.checkedAt >= ttlMs) return null;
501
- const release = value.release;
502
- if (!isRecord(release) || parseStableVersion(release.version) === null) return null;
503
- if (release.tag !== `v${release.version}` || typeof release.htmlUrl !== "string") return null;
504
- return { checkedAt: value.checkedAt, release };
505
- }
506
- function publicOperation(operation) {
507
- if (operation === null) return null;
508
- return {
509
- id: operation.id,
510
- phase: operation.phase,
511
- message: operation.message,
512
- startedAt: operation.startedAt,
513
- ...operation.finishedAt === void 0 ? {} : { finishedAt: operation.finishedAt },
514
- ...operation.error === void 0 ? {} : { error: operation.error },
515
- ...operation.rolledBack === void 0 ? {} : { rolledBack: operation.rolledBack },
516
- ...operation.release === void 0 ? {} : { release: operation.release },
517
- ...operation.code === void 0 ? {} : { code: operation.code },
518
- ...operation.params === void 0 ? {} : { params: operation.params },
519
- ...operation.rollbackError === void 0 ? {} : { rollbackError: operation.rollbackError }
520
- };
521
- }
522
- function sourceReason(source) {
523
- if (source.kind === "link") return "development-link";
524
- if (source.kind === "file") return "local-file";
525
- if (source.kind === "github") return "legacy-github";
526
- if (source.kind !== "npm") return "unsupported-source";
527
- return null;
528
- }
529
- function validateInstalledState(profileDir, artifact) {
530
- const profile = readProfileManifest(profileDir);
531
- const dependency = profile.dependencies?.[PACKAGE_NAME];
532
- if (typeof dependency !== "string" || dependency !== artifact.version) {
533
- throw codedError("PROFILE_NOT_PINNED", "profile 未固定到已验证的精确版本");
534
- }
535
- if (!Array.isArray(profile.dsh?.profile?.bundles) || !profile.dsh.profile.bundles.includes(PACKAGE_NAME)) {
536
- throw codedError("PROFILE_BUNDLE_MISSING", "profile 未注册 dsh-skins bundle");
537
- }
538
- const installed = readInstalledManifest(profileDir);
539
- validatePluginManifest(installed, artifact.version);
540
- return installed;
541
- }
542
- function createSelfUpdater(options, dependencies = {}) {
543
- const profileDir = options.profileDir;
544
- const cacheFile = options.cacheFile;
545
- const ttlMs = options.ttlMs ?? CACHE_TTL_MS;
546
- const now = dependencies.now ?? Date.now;
547
- const latestRelease = dependencies.fetchLatestRelease ?? fetchLatestStableRelease;
548
- const resolveArtifact = dependencies.resolveReleaseArtifact ?? resolveReleaseArtifact;
549
- const runner = dependencies.runner;
550
- if (typeof runner !== "function") throw new Error("self updater requires a profile runner");
551
- let currentVersion = options.currentVersion;
552
- if (parseStableVersion(currentVersion) === null) throw new Error(`invalid current version ${currentVersion}`);
553
- let memoryCache = null;
554
- let operation = null;
555
- let updatePromise = null;
556
- let restartRequired = false;
557
- const readSource = () => {
558
- const profile = readProfileManifest(profileDir);
559
- return detectInstallSource(profile.dependencies?.[PACKAGE_NAME]);
560
- };
561
- const readCache = () => {
562
- if (memoryCache !== null) {
563
- const valid2 = validCachedRelease(memoryCache, currentVersion, now(), ttlMs);
564
- if (valid2 !== null) return valid2;
565
- }
566
- const disk = readJson(cacheFile, null);
567
- const valid = validCachedRelease(disk, currentVersion, now(), ttlMs);
568
- if (valid !== null) memoryCache = disk;
569
- return valid;
570
- };
571
- const writeCache = (checkedAt, release) => {
572
- memoryCache = cachePayload(currentVersion, checkedAt, release);
573
- atomicWriteText(cacheFile, `${JSON.stringify(memoryCache, null, 2)}
574
- `);
575
- };
576
- const buildStatus = (release, checkedAt, cached) => {
577
- const source = readSource();
578
- const releaseNewer = compareStableVersions(release.version, currentVersion) > 0;
579
- const disabledReason = sourceReason(source);
580
- return {
581
- currentVersion,
582
- source,
583
- latest: release,
584
- checkedAt,
585
- cached,
586
- updateAvailable: releaseNewer,
587
- canUpdate: releaseNewer && disabledReason === null,
588
- disabledReason,
589
- restartRequired
590
- };
591
- };
592
- const status = async (force = false) => {
593
- if (!force) {
594
- const cached = readCache();
595
- if (cached !== null) return buildStatus(cached.release, cached.checkedAt, true);
596
- }
597
- const signal = AbortSignal.timeout(1e4);
598
- const release = await latestRelease({ currentVersion, signal });
599
- const checkedAt = now();
600
- writeCache(checkedAt, release);
601
- return buildStatus(release, checkedAt, false);
602
- };
603
- const setOperation = (patch) => {
604
- Object.assign(operation, patch);
605
- if (operation.phase === "done" || operation.phase === "failed") {
606
- operation.finishedAt = new Date(now()).toISOString();
607
- }
608
- };
609
- const executeUpdate = async () => {
610
- let snapshot = null;
611
- let previousSpec = null;
612
- let previousVersion = null;
613
- try {
614
- setOperation({ phase: "checking", message: "正在重新检查最新正式版本" });
615
- const before = await status(true);
616
- if (!before.canUpdate) {
617
- if (before.disabledReason === "development-link") {
618
- throw codedError("UPDATE_LINK_PROTECTED", "本地 link 开发模式不会被在线更新覆盖");
619
- }
620
- if (before.updateAvailable) {
621
- throw codedError("UPDATE_SOURCE_UNSUPPORTED", "当前安装来源不支持一键更新");
622
- }
623
- throw codedError("UPDATE_ALREADY_LATEST", "已经是最新正式版本");
624
- }
625
- setOperation({ phase: "preparing", message: "正在验证 npm 版本元数据" });
626
- const artifact = await resolveArtifact(before.latest, {
627
- currentVersion,
628
- signal: AbortSignal.timeout(15e3)
629
- });
630
- operation.release = {
631
- version: artifact.version,
632
- tag: artifact.tag,
633
- htmlUrl: artifact.htmlUrl
634
- };
635
- const profile = readProfileManifest(profileDir);
636
- previousSpec = profile.dependencies?.[PACKAGE_NAME];
637
- const previousManifest = readInstalledManifest(profileDir);
638
- previousVersion = previousManifest?.version ?? currentVersion;
639
- if (detectInstallSource(previousSpec).kind !== "npm") {
640
- throw codedError("UPDATE_SOURCE_CHANGED", "更新开始前安装来源已变化,请重新打开主题切换器");
641
- }
642
- validatePluginManifest(previousManifest, previousVersion);
643
- snapshot = captureProfileSnapshot(profileDir);
644
- setOperation({ phase: "installing", message: `正在安装 ${artifact.tag}` });
645
- await runner("web", ["add", `${PACKAGE_NAME}@${artifact.version}`], {
646
- onChunk: (chunk) => {
647
- if (String(chunk).trim() !== "") operation.message = `正在安装 ${artifact.tag}`;
648
- }
649
- });
650
- preserveProfileBundles(profileDir, snapshot);
651
- setOperation({ phase: "validating", message: "正在校验安装结果" });
652
- validateInstalledState(profileDir, artifact);
653
- currentVersion = artifact.version;
654
- restartRequired = true;
655
- writeCache(now(), {
656
- version: artifact.version,
657
- tag: artifact.tag,
658
- htmlUrl: artifact.htmlUrl,
659
- name: artifact.name
660
- });
661
- setOperation({ phase: "done", message: `已安装 ${artifact.tag},重启后生效`, rolledBack: false });
662
- } catch (error) {
663
- const original = error instanceof Error ? error : new Error(String(error));
664
- let rollbackError = null;
665
- let rolledBack = false;
666
- if (snapshot !== null && typeof previousSpec === "string" && parseStableVersion(previousVersion) !== null) {
667
- setOperation({ phase: "rollback", message: "更新失败,正在恢复原版本" });
668
- try {
669
- await runner("web", ["add", `${PACKAGE_NAME}@${previousVersion}`], {});
670
- restoreProfileSnapshot(profileDir, snapshot);
671
- const restored = readInstalledManifest(profileDir);
672
- validatePluginManifest(restored, previousVersion);
673
- const restoredProfile = readProfileManifest(profileDir);
674
- if (restoredProfile.dependencies?.[PACKAGE_NAME] !== previousSpec) {
675
- throw codedError("ROLLBACK_LOCKFILE_MISMATCH", "恢复后的版本锁定校验失败");
676
- }
677
- if (!Array.isArray(restoredProfile.dsh?.profile?.bundles) || !restoredProfile.dsh.profile.bundles.includes(PACKAGE_NAME)) {
678
- throw codedError("ROLLBACK_BUNDLE_MISSING", "恢复后的 bundle 注册校验失败");
679
- }
680
- rolledBack = true;
681
- } catch (rollbackFailure) {
682
- rollbackError = rollbackFailure instanceof Error ? rollbackFailure : new Error(String(rollbackFailure));
683
- try {
684
- restoreProfileSnapshot(profileDir, snapshot);
685
- } catch {
686
- }
687
- }
688
- }
689
- const message = rollbackError === null ? original.message : `${original.message};自动回滚失败:${rollbackError.message}`;
690
- setOperation({
691
- phase: "failed",
692
- message,
693
- error: message,
694
- rolledBack,
695
- ...original.code === void 0 ? {} : { code: original.code },
696
- ...original.params === void 0 ? {} : { params: original.params },
697
- ...rollbackError === null ? {} : {
698
- rollbackError: {
699
- message: rollbackError.message,
700
- ...rollbackError.code === void 0 ? {} : { code: rollbackError.code },
701
- ...rollbackError.params === void 0 ? {} : { params: rollbackError.params }
702
- }
703
- }
704
- });
705
- } finally {
706
- updatePromise = null;
707
- }
708
- };
709
- return {
710
- async status(force = false) {
711
- return status(force);
712
- },
713
- startUpdate() {
714
- if (updatePromise !== null) return publicOperation(operation);
715
- operation = {
716
- id: randomUUID(),
717
- phase: "queued",
718
- message: "更新已排队",
719
- startedAt: new Date(now()).toISOString()
720
- };
721
- updatePromise = executeUpdate();
722
- return publicOperation(operation);
723
- },
724
- currentOperation() {
725
- return publicOperation(operation);
726
- },
727
- get restartRequired() {
728
- return restartRequired;
729
- }
730
- };
731
- }
732
-
733
- // src/host/runner.js
734
- import { spawn as spawn2 } from "node:child_process";
735
- import { dirname as dirname3, resolve } from "node:path";
736
- var COMMAND_TIMEOUT_MS = 10 * 60 * 1e3;
737
- var WINDOWS_METACHARS = /[\s"&|<>^()%!]/;
738
- function quoteWindowsArg(value) {
739
- if (!WINDOWS_METACHARS.test(value)) return value;
740
- return `"${value.replace(/"/g, '""')}"`;
741
- }
742
- function spawnPortable(file, args, options) {
743
- const { viaShell = false, ...spawnOptions } = options;
744
- if (process.platform !== "win32" || viaShell !== true) {
745
- return spawn2(file, args, { ...spawnOptions, shell: false });
746
- }
747
- const commandLine = [file, ...args].map(quoteWindowsArg).join(" ");
748
- return spawn2(process.env.ComSpec ?? "cmd.exe", ["/d", "/s", "/c", `"${commandLine}"`], {
749
- ...spawnOptions,
750
- shell: false,
751
- windowsVerbatimArguments: true
752
- });
753
- }
754
- function currentDshInvocation() {
755
- const entry = process.argv[1];
756
- if (entry !== void 0 && /[\\/](?:bin\.(?:js|ts)|dsh)$/.test(entry)) {
757
- const absolute = resolve(entry);
758
- return {
759
- file: process.execPath,
760
- prefix: [...process.execArgv, absolute],
761
- cwd: dirname3(absolute),
762
- viaShell: false
763
- };
764
- }
765
- return { file: "dsh", prefix: [], cwd: void 0, viaShell: process.platform === "win32" };
766
- }
767
- function runDshPlugin(profile, args, options = {}) {
768
- const invocation = options.invocation ?? currentDshInvocation();
769
- const argv = [...invocation.prefix, "plugin", "--profile", profile, ...args, "--reporter=ndjson"];
770
- return new Promise((resolvePromise, reject) => {
771
- const child = spawnPortable(invocation.file, argv, {
772
- cwd: invocation.cwd,
773
- env: { ...process.env, CI: "true" },
774
- stdio: ["ignore", "pipe", "pipe"],
775
- detached: process.platform !== "win32",
776
- viaShell: invocation.viaShell
777
- });
778
- let output = "";
779
- let settled = false;
780
- const finish = (error) => {
781
- if (settled) return;
782
- settled = true;
783
- clearTimeout(timer);
784
- if (error) reject(error);
785
- else resolvePromise(output);
786
- };
787
- const append = (chunk) => {
788
- const text = String(chunk);
789
- output = (output + text).slice(-256 * 1024);
790
- options.onChunk?.(text);
791
- };
792
- child.stdout?.on("data", append);
793
- child.stderr?.on("data", append);
794
- child.on("error", finish);
795
- child.on("close", (code) => {
796
- if (code === 0) finish();
797
- else {
798
- const tail = output.trim().slice(-1200) || "no output";
799
- finish(codedError(
800
- "UPDATE_COMMAND_FAILED",
801
- `DSH 插件更新失败(exit ${String(code)}):${tail}`,
802
- { exitCode: String(code), output: tail }
803
- ));
804
- }
805
- });
806
- const timer = setTimeout(() => {
807
- if (child.pid !== void 0) {
808
- try {
809
- if (process.platform === "win32") child.kill("SIGTERM");
810
- else process.kill(-child.pid, "SIGTERM");
811
- } catch {
812
- }
813
- }
814
- finish(codedError("UPDATE_COMMAND_TIMEOUT", "DSH 插件更新超时"));
815
- }, options.timeoutMs ?? COMMAND_TIMEOUT_MS);
816
- timer.unref?.();
817
- });
818
- }
819
-
820
- // src/host/personalization/store.js
821
- import { createHash, randomUUID as randomUUID2 } from "node:crypto";
822
- import {
823
- closeSync,
824
- copyFileSync,
825
- existsSync as existsSync2,
826
- mkdirSync as mkdirSync3,
827
- openSync,
828
- readFileSync as readFileSync2,
829
- readdirSync,
830
- renameSync as renameSync3,
831
- rmdirSync,
832
- statfsSync,
833
- statSync,
834
- unlinkSync as unlinkSync2,
835
- writeFileSync as writeFileSync3
836
- } from "node:fs";
837
- import { join as join2 } from "node:path";
838
-
839
50
  // src/host/personalization/image-meta.js
840
51
  function ascii(buffer, offset, length) {
841
52
  return String.fromCharCode(...buffer.subarray(offset, offset + length));
@@ -961,49 +172,6 @@ var WALLPAPER_FIELD = {
961
172
  maxPixels: GLOBAL_MAX_PIXELS
962
173
  };
963
174
  var SKINS = {
964
- tgcf: {
965
- builtinAssets: {
966
- // Registry mirrors the grid: the factory default (moonlit) leads.
967
- moonlit: { mime: "image/webp", labelKey: "personalization.tgcf.moonlit" },
968
- crimson: { mime: "image/webp", labelKey: "personalization.tgcf.crimson" },
969
- pale: { mime: "image/webp", labelKey: "personalization.tgcf.pale" },
970
- "seal-favicon": { mime: "image/webp" }
971
- },
972
- fields: [
973
- // Factory default rides moonlit (花怜 · 月下同伞) since the 1.0.0
974
- // wallpaper addition; it leads the grid (user ruling) and its label
975
- // carries the (默认壁纸) suffix. crimson/pale remain selectable, and
976
- // stored overrides are untouched by the default flip.
977
- { ...WALLPAPER_FIELD, default: "builtin:tgcf:moonlit", builtinChoices: ["moonlit", "crimson", "pale"] },
978
- {
979
- key: "slogan",
980
- type: "text",
981
- scope: "locale",
982
- labelKey: "personalization.slogan",
983
- maxLength: 40,
984
- default: { zh: "百无禁忌", en: "No Taboos" }
985
- },
986
- {
987
- // Ruling #14: ONE translucency knob. It drives the panel tint, the
988
- // wallpaper scrim and the blur as one combined visual (the curve
989
- // lives in the tgcf projector, calibrated through the historical
990
- // defaults P=82 → scrim 30 / blur 12); the blur/scrim fields are
991
- // retired — pre-1.0.0 there are no external users to migrate.
992
- // Ruling #15: factory default 10; ruling #17 re-tunes it to 30 —
993
- // a faint veil so menus read while the wallpaper still leads;
994
- // 1.0.0 re-tunes it to 35 (user) for a touch more panel presence.
995
- key: "panelOpacity",
996
- type: "range",
997
- scope: "single",
998
- labelKey: "personalization.panelTranslucency",
999
- min: 0,
1000
- max: 100,
1001
- step: 1,
1002
- unit: "%",
1003
- default: 35
1004
- }
1005
- ]
1006
- },
1007
175
  // ADR-0004 (reversing design §9a): every catalog skin declares the same
1008
176
  // standard field set. The legacy skins' catalog slogan defaults are
1009
177
  // same-source with their factories' static `slogans`, and panelOpacity
@@ -1062,9 +230,9 @@ var SKINS = {
1062
230
  }
1063
231
  ]
1064
232
  },
1065
- // 美人志(v1.0.6 前的出厂皮肤,现注册顺序第二位):6 张内置精选,出厂默认 yuntai
1066
- // (001合照)领头并对齐 tgcf 的「出厂默认领先」惯例;panelOpacity 锚定
1067
- // tgcf 二次曲线(projector 数学见 skins/meirenzhi/index.js)。
233
+ // 美人志(v1.0.6 前的出厂皮肤,现注册顺序第三位):6 张内置精选,出厂默认 yuntai
234
+ // (001合照)领头并对齐已移除 tgcf 皮肤的「出厂默认领先」惯例;panelOpacity 锚定
235
+ // 其二次曲线(projector 数学见 skins/meirenzhi/index.js)。
1068
236
  meirenzhi: {
1069
237
  builtinAssets: {
1070
238
  yuntai: { mime: "image/webp", labelKey: "personalization.meirenzhi.yuntai" },
@@ -1242,12 +410,12 @@ var CORRUPT_BACKUP_LIMIT = 3;
1242
410
  var ID_ATTEMPTS = 5;
1243
411
  function defaultFs() {
1244
412
  return {
1245
- existsSync: existsSync2,
1246
- mkdirSync: mkdirSync3,
1247
- readFileSync: readFileSync2,
1248
- writeFileSync: writeFileSync3,
1249
- renameSync: renameSync3,
1250
- unlinkSync: unlinkSync2,
413
+ existsSync,
414
+ mkdirSync: mkdirSync2,
415
+ readFileSync,
416
+ writeFileSync: writeFileSync2,
417
+ renameSync: renameSync2,
418
+ unlinkSync,
1251
419
  readdirSync,
1252
420
  rmdirSync,
1253
421
  statSync,
@@ -1315,9 +483,9 @@ function createPersonalizationStore(options = {}) {
1315
483
  }
1316
484
  const fs = options.fs ?? defaultFs();
1317
485
  const now = options.now ?? Date.now;
1318
- const stateFile = join2(dataDir, STATE_FILE);
1319
- const assetsDir = join2(dataDir, ASSETS_DIR);
1320
- const quarantineDir = join2(dataDir, QUARANTINE_DIR);
486
+ const stateFile = join(dataDir, STATE_FILE);
487
+ const assetsDir = join(dataDir, ASSETS_DIR);
488
+ const quarantineDir = join(dataDir, QUARANTINE_DIR);
1321
489
  let state = null;
1322
490
  let mode = "normal";
1323
491
  let recovery = null;
@@ -1345,7 +513,7 @@ function createPersonalizationStore(options = {}) {
1345
513
  return fs.readdirSync(assetsDir).filter((name2) => !name2.startsWith("."));
1346
514
  }
1347
515
  function blobFileFor(id, extension) {
1348
- return join2(assetsDir, `${id}.${extension}`);
516
+ return join(assetsDir, `${id}.${extension}`);
1349
517
  }
1350
518
  function commitState(next) {
1351
519
  atomicWriteText(stateFile, `${JSON.stringify(next, null, 2)}
@@ -1358,7 +526,7 @@ function createPersonalizationStore(options = {}) {
1358
526
  return run;
1359
527
  }
1360
528
  function sniffBlobFile(name2) {
1361
- const file = join2(assetsDir, name2);
529
+ const file = join(assetsDir, name2);
1362
530
  let buffer;
1363
531
  try {
1364
532
  buffer = fs.readFileSync(file);
@@ -1472,7 +640,7 @@ function createPersonalizationStore(options = {}) {
1472
640
  const id = name2.split(".")[0];
1473
641
  if (!live.has(id)) {
1474
642
  try {
1475
- fs.unlinkSync(join2(assetsDir, name2));
643
+ fs.unlinkSync(join(assetsDir, name2));
1476
644
  } catch {
1477
645
  }
1478
646
  }
@@ -1593,7 +761,7 @@ function createPersonalizationStore(options = {}) {
1593
761
  }
1594
762
  function writeBlobExclusive(buffer, extension) {
1595
763
  for (let attempt = 0; attempt < ID_ATTEMPTS; attempt += 1) {
1596
- const id = ASSET_ID_PREFIX + randomUUID2().replaceAll("-", "");
764
+ const id = ASSET_ID_PREFIX + randomUUID().replaceAll("-", "");
1597
765
  const target = blobFileFor(id, extension);
1598
766
  let descriptor;
1599
767
  try {
@@ -1729,7 +897,7 @@ function createPersonalizationStore(options = {}) {
1729
897
  fs.mkdirSync(quarantineDir, { recursive: true });
1730
898
  for (const name2 of pending.quarantine) {
1731
899
  try {
1732
- fs.renameSync(join2(assetsDir, name2), join2(quarantineDir, name2));
900
+ fs.renameSync(join(assetsDir, name2), join(quarantineDir, name2));
1733
901
  } catch {
1734
902
  }
1735
903
  }
@@ -1738,12 +906,12 @@ function createPersonalizationStore(options = {}) {
1738
906
  commitState(cleaned);
1739
907
  }
1740
908
  function pruneCorruptBackups() {
1741
- const dir = join2(dataDir);
909
+ const dir = join(dataDir);
1742
910
  if (!fs.existsSync(dir)) return;
1743
911
  const backups = fs.readdirSync(dir).filter((name2) => name2.startsWith(`${STATE_FILE}.corrupt.`)).sort();
1744
912
  while (backups.length > CORRUPT_BACKUP_LIMIT) {
1745
913
  try {
1746
- fs.unlinkSync(join2(dir, backups.shift()));
914
+ fs.unlinkSync(join(dir, backups.shift()));
1747
915
  } catch {
1748
916
  }
1749
917
  }
@@ -1764,6 +932,52 @@ function createPersonalizationStore(options = {}) {
1764
932
  }
1765
933
 
1766
934
  // src/host/personalization-routes.js
935
+ function parseAuthority(authority) {
936
+ try {
937
+ return new URL(`http://${authority}`);
938
+ } catch {
939
+ return void 0;
940
+ }
941
+ }
942
+ function canonicalAuthority(entry, parsed) {
943
+ const port = parsed.port !== "" ? parsed.port : new URL(`https://${entry}`).port;
944
+ return port === "" ? parsed.hostname : `${parsed.hostname}:${port}`;
945
+ }
946
+ function isLoopbackHostname(hostname) {
947
+ if (hostname === "localhost" || hostname === "[::1]") return true;
948
+ const parts = hostname.split(".");
949
+ return parts.length === 4 && parts[0] === "127" && parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255);
950
+ }
951
+ function isTrustedAuthority(hostUrl, trustedHosts) {
952
+ return trustedHosts.some((entry) => {
953
+ const parsed = parseAuthority(entry);
954
+ if (parsed === void 0) return false;
955
+ return canonicalAuthority(entry, parsed) === parsed.hostname ? parsed.hostname === hostUrl.hostname : parsed.host === hostUrl.host;
956
+ });
957
+ }
958
+ function isTrustedRequest(request, trustedHosts = []) {
959
+ const host = header(request.headers, "host");
960
+ if (host === void 0) return false;
961
+ const hostUrl = parseAuthority(host);
962
+ if (hostUrl === void 0) return false;
963
+ const socketAddress = request.socket?.remoteAddress ?? request.info?.remoteAddress;
964
+ if (isLoopbackHostname(hostUrl.hostname)) {
965
+ if (typeof socketAddress === "string") {
966
+ const loopbackPeer = socketAddress === "127.0.0.1" || socketAddress === "::1" || socketAddress === "::ffff:127.0.0.1";
967
+ if (!loopbackPeer) return false;
968
+ }
969
+ } else if (!isTrustedAuthority(hostUrl, trustedHosts)) {
970
+ return false;
971
+ }
972
+ if (header(request.headers, "sec-fetch-site") === "cross-site") return false;
973
+ const origin = header(request.headers, "origin");
974
+ if (origin === void 0) return true;
975
+ try {
976
+ return new URL(origin).host === hostUrl.host;
977
+ } catch {
978
+ return false;
979
+ }
980
+ }
1767
981
  var CODE_STATUS = {
1768
982
  INVALID_CONFIG: 400,
1769
983
  INVALID_ASSET_ID: 400,
@@ -1779,7 +993,7 @@ var CODE_STATUS = {
1779
993
  STORE_RECOVERY_REQUIRED: 409,
1780
994
  STORE_NOT_RECOVERING: 409
1781
995
  };
1782
- function sendJson2(response, status, value, extraHeaders = {}) {
996
+ function sendJson(response, status, value, extraHeaders = {}) {
1783
997
  response.writeHead(status, {
1784
998
  "content-type": "application/json; charset=utf-8",
1785
999
  "cache-control": "no-store",
@@ -1789,19 +1003,19 @@ function sendJson2(response, status, value, extraHeaders = {}) {
1789
1003
  }
1790
1004
  function sendError(response, error) {
1791
1005
  const status = CODE_STATUS[error?.code] ?? 500;
1792
- sendJson2(response, status, publicError(error));
1006
+ sendJson(response, status, publicError(error));
1793
1007
  }
1794
- function header2(headers, name2) {
1008
+ function header(headers, name2) {
1795
1009
  const value = headers?.[name2];
1796
1010
  return typeof value === "string" ? value : Array.isArray(value) ? value[0] : void 0;
1797
1011
  }
1798
- function method2(request, response, expected) {
1012
+ function method(request, response, expected) {
1799
1013
  if (request.method === expected) return true;
1800
1014
  response.writeHead(405, { allow: expected });
1801
1015
  response.end();
1802
1016
  return false;
1803
1017
  }
1804
- async function readJsonBody2(request, limit = 128 * 1024) {
1018
+ async function readJsonBody(request, limit = 128 * 1024) {
1805
1019
  const type = String(request.headers["content-type"] ?? "").toLowerCase();
1806
1020
  if (!type.startsWith("application/json")) throw codedError("INVALID_CONFIG", "content-type must be application/json");
1807
1021
  const chunks = [];
@@ -1836,7 +1050,7 @@ async function readRawBody(request, limit, overflowCode = "UPLOAD_TOO_LARGE") {
1836
1050
  return Buffer.concat(chunks);
1837
1051
  }
1838
1052
  function decodeDisplayName(request) {
1839
- const raw = header2(request.headers, "x-filename");
1053
+ const raw = header(request.headers, "x-filename");
1840
1054
  if (raw === void 0 || raw === "") return "wallpaper";
1841
1055
  try {
1842
1056
  return decodeURIComponent(raw);
@@ -1849,7 +1063,7 @@ function mountPersonalizationRoutes(host, options) {
1849
1063
  const disposers = [];
1850
1064
  const fence = (request, response) => {
1851
1065
  if (isTrustedRequest(request, trustedHosts)) return true;
1852
- sendJson2(response, 403, { error: "trusted DSH Web request required" });
1066
+ sendJson(response, 403, { error: "trusted DSH Web request required" });
1853
1067
  return false;
1854
1068
  };
1855
1069
  try {
@@ -1858,12 +1072,12 @@ function mountPersonalizationRoutes(host, options) {
1858
1072
  path: "/dsh-skins/config",
1859
1073
  handler: async (request, response) => {
1860
1074
  if (!fence(request, response)) return;
1861
- if (request.method === "GET") return sendJson2(response, 200, store.snapshot());
1862
- if (!method2(request, response, "PATCH")) return;
1075
+ if (request.method === "GET") return sendJson(response, 200, store.snapshot());
1076
+ if (!method(request, response, "PATCH")) return;
1863
1077
  try {
1864
- const body = await readJsonBody2(request);
1078
+ const body = await readJsonBody(request);
1865
1079
  const result = await store.applyOperations(body);
1866
- return sendJson2(response, 200, result);
1080
+ return sendJson(response, 200, result);
1867
1081
  } catch (error) {
1868
1082
  return sendError(response, error);
1869
1083
  }
@@ -1875,9 +1089,9 @@ function mountPersonalizationRoutes(host, options) {
1875
1089
  path: "/dsh-skins/recovery",
1876
1090
  handler: async (request, response) => {
1877
1091
  if (!fence(request, response)) return;
1878
- if (!method2(request, response, "POST")) return;
1092
+ if (!method(request, response, "POST")) return;
1879
1093
  try {
1880
- return sendJson2(response, 200, await store.confirmRecovery());
1094
+ return sendJson(response, 200, await store.confirmRecovery());
1881
1095
  } catch (error) {
1882
1096
  return sendError(response, error);
1883
1097
  }
@@ -1889,16 +1103,16 @@ function mountPersonalizationRoutes(host, options) {
1889
1103
  path: "/dsh-skins/library",
1890
1104
  handler: async (request, response) => {
1891
1105
  if (!fence(request, response)) return;
1892
- if (request.method === "GET") return sendJson2(response, 200, store.snapshot());
1893
- if (!method2(request, response, "POST")) return;
1106
+ if (request.method === "GET") return sendJson(response, 200, store.snapshot());
1107
+ if (!method(request, response, "POST")) return;
1894
1108
  try {
1895
- const declaredLength = Number(header2(request.headers, "content-length") ?? "0");
1109
+ const declaredLength = Number(header(request.headers, "content-length") ?? "0");
1896
1110
  if (declaredLength > GLOBAL_MAX_BYTES) throw codedError("UPLOAD_TOO_LARGE", "图片超过 20MB 上限");
1897
1111
  const buffer = await readRawBody(request, GLOBAL_MAX_BYTES + 1024);
1898
1112
  const displayName = decodeDisplayName(request);
1899
- const declaredMime = header2(request.headers, "content-type");
1113
+ const declaredMime = header(request.headers, "content-type");
1900
1114
  const result = await store.uploadAsset(buffer, { displayName, declaredMime });
1901
- return sendJson2(response, 201, result);
1115
+ return sendJson(response, 201, result);
1902
1116
  } catch (error) {
1903
1117
  return sendError(response, error);
1904
1118
  }
@@ -1910,15 +1124,15 @@ function mountPersonalizationRoutes(host, options) {
1910
1124
  path: "/dsh-skins/library",
1911
1125
  handler: async (request, response) => {
1912
1126
  if (!fence(request, response)) return;
1913
- if (!method2(request, response, "DELETE")) return;
1127
+ if (!method(request, response, "DELETE")) return;
1914
1128
  const path = String(request.url ?? "").split("?")[0];
1915
1129
  const match = /\/dsh-skins\/library\/([^/]+)$/.exec(path);
1916
1130
  const suffix = match?.[1] ?? "";
1917
1131
  if (!ASSET_ID_PATTERN.test(suffix)) {
1918
- return sendJson2(response, 400, { error: "invalid asset id", code: "INVALID_ASSET_ID" });
1132
+ return sendJson(response, 400, { error: "invalid asset id", code: "INVALID_ASSET_ID" });
1919
1133
  }
1920
1134
  try {
1921
- return sendJson2(response, 200, await store.deleteAsset(suffix));
1135
+ return sendJson(response, 200, await store.deleteAsset(suffix));
1922
1136
  } catch (error) {
1923
1137
  return sendError(response, error);
1924
1138
  }
@@ -1930,11 +1144,11 @@ function mountPersonalizationRoutes(host, options) {
1930
1144
  path: assetsBasePath,
1931
1145
  handler: async (request, response) => {
1932
1146
  if (!fence(request, response)) return;
1933
- if (!method2(request, response, "GET")) return;
1147
+ if (!method(request, response, "GET")) return;
1934
1148
  try {
1935
1149
  const blob = store.serveAsset(String(request.url ?? ""));
1936
1150
  if (blob === null) {
1937
- return sendJson2(response, 404, { error: "asset not found", code: "ASSET_NOT_FOUND" });
1151
+ return sendJson(response, 404, { error: "asset not found", code: "ASSET_NOT_FOUND" });
1938
1152
  }
1939
1153
  response.writeHead(200, {
1940
1154
  "content-type": blob.meta.mime,
@@ -1967,60 +1181,24 @@ function mountPersonalizationRoutes(host, options) {
1967
1181
 
1968
1182
  // src/index.js
1969
1183
  var name = "@iasiv5/dsh-skins";
1970
- function packageRoot() {
1971
- return join3(dirname4(fileURLToPath(import.meta.url)), "..");
1972
- }
1973
- function packageVersion() {
1974
- const manifest = JSON.parse(readFileSync3(join3(packageRoot(), "package.json"), "utf8"));
1975
- if (typeof manifest.version !== "string") throw new Error("dsh-skins package.json 缺少版本号");
1976
- return manifest.version;
1977
- }
1978
1184
  function dshHome() {
1979
- return process.env.DSH_HOME?.trim() || join3(homedir(), ".dsh");
1185
+ return process.env.DSH_HOME?.trim() || join2(homedir(), ".dsh");
1980
1186
  }
1981
1187
  function apply(ctx) {
1982
- ctx.inject(["webServer", "agents", "webRuntime"], (hostContext) => {
1188
+ ctx.inject(["webServer", "webRuntime"], (hostContext) => {
1983
1189
  const root = dshHome();
1984
- const profileDir = join3(root, "profiles", "web");
1985
- const updater = createSelfUpdater({
1986
- profileDir,
1987
- cacheFile: join3(root, "dsh-skins", "update-cache.json"),
1988
- currentVersion: packageVersion()
1989
- }, {
1990
- runner: runDshPlugin
1991
- });
1992
- const restart = createRestartScheduler(ctx.get("appExit"));
1993
1190
  const personalization = createPersonalizationStore({
1994
- dataDir: join3(root, "dsh-skins")
1191
+ dataDir: join2(root, "dsh-skins")
1995
1192
  });
1996
1193
  hostContext.effect(() => {
1997
- let disposeUpdate = null;
1998
- let disposePersonalization = null;
1999
- try {
2000
- disposeUpdate = mountUpdateRoutes(hostContext, {
2001
- updater,
2002
- restart,
2003
- agents: hostContext.agents,
2004
- trustedHosts: hostContext.webRuntime.trustedHosts
2005
- });
2006
- disposePersonalization = mountPersonalizationRoutes(hostContext, {
2007
- store: personalization,
2008
- trustedHosts: hostContext.webRuntime.trustedHosts
2009
- });
2010
- } catch (error) {
2011
- disposePersonalization?.();
2012
- disposeUpdate?.();
2013
- throw error;
2014
- }
2015
- return () => {
2016
- disposePersonalization();
2017
- disposeUpdate();
2018
- };
2019
- }, "dsh-skins: self-update routes");
1194
+ return mountPersonalizationRoutes(hostContext, {
1195
+ store: personalization,
1196
+ trustedHosts: hostContext.webRuntime.trustedHosts
1197
+ });
1198
+ }, "dsh-skins: personalization routes");
2020
1199
  });
2021
1200
  }
2022
1201
  export {
2023
1202
  apply,
2024
- createSelfUpdater,
2025
1203
  name
2026
1204
  };