@iasiv5/dsh-skins 1.2.3 → 1.3.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));
@@ -1242,12 +453,12 @@ var CORRUPT_BACKUP_LIMIT = 3;
1242
453
  var ID_ATTEMPTS = 5;
1243
454
  function defaultFs() {
1244
455
  return {
1245
- existsSync: existsSync2,
1246
- mkdirSync: mkdirSync3,
1247
- readFileSync: readFileSync2,
1248
- writeFileSync: writeFileSync3,
1249
- renameSync: renameSync3,
1250
- unlinkSync: unlinkSync2,
456
+ existsSync,
457
+ mkdirSync: mkdirSync2,
458
+ readFileSync,
459
+ writeFileSync: writeFileSync2,
460
+ renameSync: renameSync2,
461
+ unlinkSync,
1251
462
  readdirSync,
1252
463
  rmdirSync,
1253
464
  statSync,
@@ -1315,9 +526,9 @@ function createPersonalizationStore(options = {}) {
1315
526
  }
1316
527
  const fs = options.fs ?? defaultFs();
1317
528
  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);
529
+ const stateFile = join(dataDir, STATE_FILE);
530
+ const assetsDir = join(dataDir, ASSETS_DIR);
531
+ const quarantineDir = join(dataDir, QUARANTINE_DIR);
1321
532
  let state = null;
1322
533
  let mode = "normal";
1323
534
  let recovery = null;
@@ -1345,7 +556,7 @@ function createPersonalizationStore(options = {}) {
1345
556
  return fs.readdirSync(assetsDir).filter((name2) => !name2.startsWith("."));
1346
557
  }
1347
558
  function blobFileFor(id, extension) {
1348
- return join2(assetsDir, `${id}.${extension}`);
559
+ return join(assetsDir, `${id}.${extension}`);
1349
560
  }
1350
561
  function commitState(next) {
1351
562
  atomicWriteText(stateFile, `${JSON.stringify(next, null, 2)}
@@ -1358,7 +569,7 @@ function createPersonalizationStore(options = {}) {
1358
569
  return run;
1359
570
  }
1360
571
  function sniffBlobFile(name2) {
1361
- const file = join2(assetsDir, name2);
572
+ const file = join(assetsDir, name2);
1362
573
  let buffer;
1363
574
  try {
1364
575
  buffer = fs.readFileSync(file);
@@ -1472,7 +683,7 @@ function createPersonalizationStore(options = {}) {
1472
683
  const id = name2.split(".")[0];
1473
684
  if (!live.has(id)) {
1474
685
  try {
1475
- fs.unlinkSync(join2(assetsDir, name2));
686
+ fs.unlinkSync(join(assetsDir, name2));
1476
687
  } catch {
1477
688
  }
1478
689
  }
@@ -1593,7 +804,7 @@ function createPersonalizationStore(options = {}) {
1593
804
  }
1594
805
  function writeBlobExclusive(buffer, extension) {
1595
806
  for (let attempt = 0; attempt < ID_ATTEMPTS; attempt += 1) {
1596
- const id = ASSET_ID_PREFIX + randomUUID2().replaceAll("-", "");
807
+ const id = ASSET_ID_PREFIX + randomUUID().replaceAll("-", "");
1597
808
  const target = blobFileFor(id, extension);
1598
809
  let descriptor;
1599
810
  try {
@@ -1729,7 +940,7 @@ function createPersonalizationStore(options = {}) {
1729
940
  fs.mkdirSync(quarantineDir, { recursive: true });
1730
941
  for (const name2 of pending.quarantine) {
1731
942
  try {
1732
- fs.renameSync(join2(assetsDir, name2), join2(quarantineDir, name2));
943
+ fs.renameSync(join(assetsDir, name2), join(quarantineDir, name2));
1733
944
  } catch {
1734
945
  }
1735
946
  }
@@ -1738,12 +949,12 @@ function createPersonalizationStore(options = {}) {
1738
949
  commitState(cleaned);
1739
950
  }
1740
951
  function pruneCorruptBackups() {
1741
- const dir = join2(dataDir);
952
+ const dir = join(dataDir);
1742
953
  if (!fs.existsSync(dir)) return;
1743
954
  const backups = fs.readdirSync(dir).filter((name2) => name2.startsWith(`${STATE_FILE}.corrupt.`)).sort();
1744
955
  while (backups.length > CORRUPT_BACKUP_LIMIT) {
1745
956
  try {
1746
- fs.unlinkSync(join2(dir, backups.shift()));
957
+ fs.unlinkSync(join(dir, backups.shift()));
1747
958
  } catch {
1748
959
  }
1749
960
  }
@@ -1764,6 +975,52 @@ function createPersonalizationStore(options = {}) {
1764
975
  }
1765
976
 
1766
977
  // src/host/personalization-routes.js
978
+ function parseAuthority(authority) {
979
+ try {
980
+ return new URL(`http://${authority}`);
981
+ } catch {
982
+ return void 0;
983
+ }
984
+ }
985
+ function canonicalAuthority(entry, parsed) {
986
+ const port = parsed.port !== "" ? parsed.port : new URL(`https://${entry}`).port;
987
+ return port === "" ? parsed.hostname : `${parsed.hostname}:${port}`;
988
+ }
989
+ function isLoopbackHostname(hostname) {
990
+ if (hostname === "localhost" || hostname === "[::1]") return true;
991
+ const parts = hostname.split(".");
992
+ return parts.length === 4 && parts[0] === "127" && parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255);
993
+ }
994
+ function isTrustedAuthority(hostUrl, trustedHosts) {
995
+ return trustedHosts.some((entry) => {
996
+ const parsed = parseAuthority(entry);
997
+ if (parsed === void 0) return false;
998
+ return canonicalAuthority(entry, parsed) === parsed.hostname ? parsed.hostname === hostUrl.hostname : parsed.host === hostUrl.host;
999
+ });
1000
+ }
1001
+ function isTrustedRequest(request, trustedHosts = []) {
1002
+ const host = header(request.headers, "host");
1003
+ if (host === void 0) return false;
1004
+ const hostUrl = parseAuthority(host);
1005
+ if (hostUrl === void 0) return false;
1006
+ const socketAddress = request.socket?.remoteAddress ?? request.info?.remoteAddress;
1007
+ if (isLoopbackHostname(hostUrl.hostname)) {
1008
+ if (typeof socketAddress === "string") {
1009
+ const loopbackPeer = socketAddress === "127.0.0.1" || socketAddress === "::1" || socketAddress === "::ffff:127.0.0.1";
1010
+ if (!loopbackPeer) return false;
1011
+ }
1012
+ } else if (!isTrustedAuthority(hostUrl, trustedHosts)) {
1013
+ return false;
1014
+ }
1015
+ if (header(request.headers, "sec-fetch-site") === "cross-site") return false;
1016
+ const origin = header(request.headers, "origin");
1017
+ if (origin === void 0) return true;
1018
+ try {
1019
+ return new URL(origin).host === hostUrl.host;
1020
+ } catch {
1021
+ return false;
1022
+ }
1023
+ }
1767
1024
  var CODE_STATUS = {
1768
1025
  INVALID_CONFIG: 400,
1769
1026
  INVALID_ASSET_ID: 400,
@@ -1779,7 +1036,7 @@ var CODE_STATUS = {
1779
1036
  STORE_RECOVERY_REQUIRED: 409,
1780
1037
  STORE_NOT_RECOVERING: 409
1781
1038
  };
1782
- function sendJson2(response, status, value, extraHeaders = {}) {
1039
+ function sendJson(response, status, value, extraHeaders = {}) {
1783
1040
  response.writeHead(status, {
1784
1041
  "content-type": "application/json; charset=utf-8",
1785
1042
  "cache-control": "no-store",
@@ -1789,19 +1046,19 @@ function sendJson2(response, status, value, extraHeaders = {}) {
1789
1046
  }
1790
1047
  function sendError(response, error) {
1791
1048
  const status = CODE_STATUS[error?.code] ?? 500;
1792
- sendJson2(response, status, publicError(error));
1049
+ sendJson(response, status, publicError(error));
1793
1050
  }
1794
- function header2(headers, name2) {
1051
+ function header(headers, name2) {
1795
1052
  const value = headers?.[name2];
1796
1053
  return typeof value === "string" ? value : Array.isArray(value) ? value[0] : void 0;
1797
1054
  }
1798
- function method2(request, response, expected) {
1055
+ function method(request, response, expected) {
1799
1056
  if (request.method === expected) return true;
1800
1057
  response.writeHead(405, { allow: expected });
1801
1058
  response.end();
1802
1059
  return false;
1803
1060
  }
1804
- async function readJsonBody2(request, limit = 128 * 1024) {
1061
+ async function readJsonBody(request, limit = 128 * 1024) {
1805
1062
  const type = String(request.headers["content-type"] ?? "").toLowerCase();
1806
1063
  if (!type.startsWith("application/json")) throw codedError("INVALID_CONFIG", "content-type must be application/json");
1807
1064
  const chunks = [];
@@ -1836,7 +1093,7 @@ async function readRawBody(request, limit, overflowCode = "UPLOAD_TOO_LARGE") {
1836
1093
  return Buffer.concat(chunks);
1837
1094
  }
1838
1095
  function decodeDisplayName(request) {
1839
- const raw = header2(request.headers, "x-filename");
1096
+ const raw = header(request.headers, "x-filename");
1840
1097
  if (raw === void 0 || raw === "") return "wallpaper";
1841
1098
  try {
1842
1099
  return decodeURIComponent(raw);
@@ -1849,7 +1106,7 @@ function mountPersonalizationRoutes(host, options) {
1849
1106
  const disposers = [];
1850
1107
  const fence = (request, response) => {
1851
1108
  if (isTrustedRequest(request, trustedHosts)) return true;
1852
- sendJson2(response, 403, { error: "trusted DSH Web request required" });
1109
+ sendJson(response, 403, { error: "trusted DSH Web request required" });
1853
1110
  return false;
1854
1111
  };
1855
1112
  try {
@@ -1858,12 +1115,12 @@ function mountPersonalizationRoutes(host, options) {
1858
1115
  path: "/dsh-skins/config",
1859
1116
  handler: async (request, response) => {
1860
1117
  if (!fence(request, response)) return;
1861
- if (request.method === "GET") return sendJson2(response, 200, store.snapshot());
1862
- if (!method2(request, response, "PATCH")) return;
1118
+ if (request.method === "GET") return sendJson(response, 200, store.snapshot());
1119
+ if (!method(request, response, "PATCH")) return;
1863
1120
  try {
1864
- const body = await readJsonBody2(request);
1121
+ const body = await readJsonBody(request);
1865
1122
  const result = await store.applyOperations(body);
1866
- return sendJson2(response, 200, result);
1123
+ return sendJson(response, 200, result);
1867
1124
  } catch (error) {
1868
1125
  return sendError(response, error);
1869
1126
  }
@@ -1875,9 +1132,9 @@ function mountPersonalizationRoutes(host, options) {
1875
1132
  path: "/dsh-skins/recovery",
1876
1133
  handler: async (request, response) => {
1877
1134
  if (!fence(request, response)) return;
1878
- if (!method2(request, response, "POST")) return;
1135
+ if (!method(request, response, "POST")) return;
1879
1136
  try {
1880
- return sendJson2(response, 200, await store.confirmRecovery());
1137
+ return sendJson(response, 200, await store.confirmRecovery());
1881
1138
  } catch (error) {
1882
1139
  return sendError(response, error);
1883
1140
  }
@@ -1889,16 +1146,16 @@ function mountPersonalizationRoutes(host, options) {
1889
1146
  path: "/dsh-skins/library",
1890
1147
  handler: async (request, response) => {
1891
1148
  if (!fence(request, response)) return;
1892
- if (request.method === "GET") return sendJson2(response, 200, store.snapshot());
1893
- if (!method2(request, response, "POST")) return;
1149
+ if (request.method === "GET") return sendJson(response, 200, store.snapshot());
1150
+ if (!method(request, response, "POST")) return;
1894
1151
  try {
1895
- const declaredLength = Number(header2(request.headers, "content-length") ?? "0");
1152
+ const declaredLength = Number(header(request.headers, "content-length") ?? "0");
1896
1153
  if (declaredLength > GLOBAL_MAX_BYTES) throw codedError("UPLOAD_TOO_LARGE", "图片超过 20MB 上限");
1897
1154
  const buffer = await readRawBody(request, GLOBAL_MAX_BYTES + 1024);
1898
1155
  const displayName = decodeDisplayName(request);
1899
- const declaredMime = header2(request.headers, "content-type");
1156
+ const declaredMime = header(request.headers, "content-type");
1900
1157
  const result = await store.uploadAsset(buffer, { displayName, declaredMime });
1901
- return sendJson2(response, 201, result);
1158
+ return sendJson(response, 201, result);
1902
1159
  } catch (error) {
1903
1160
  return sendError(response, error);
1904
1161
  }
@@ -1910,15 +1167,15 @@ function mountPersonalizationRoutes(host, options) {
1910
1167
  path: "/dsh-skins/library",
1911
1168
  handler: async (request, response) => {
1912
1169
  if (!fence(request, response)) return;
1913
- if (!method2(request, response, "DELETE")) return;
1170
+ if (!method(request, response, "DELETE")) return;
1914
1171
  const path = String(request.url ?? "").split("?")[0];
1915
1172
  const match = /\/dsh-skins\/library\/([^/]+)$/.exec(path);
1916
1173
  const suffix = match?.[1] ?? "";
1917
1174
  if (!ASSET_ID_PATTERN.test(suffix)) {
1918
- return sendJson2(response, 400, { error: "invalid asset id", code: "INVALID_ASSET_ID" });
1175
+ return sendJson(response, 400, { error: "invalid asset id", code: "INVALID_ASSET_ID" });
1919
1176
  }
1920
1177
  try {
1921
- return sendJson2(response, 200, await store.deleteAsset(suffix));
1178
+ return sendJson(response, 200, await store.deleteAsset(suffix));
1922
1179
  } catch (error) {
1923
1180
  return sendError(response, error);
1924
1181
  }
@@ -1930,11 +1187,11 @@ function mountPersonalizationRoutes(host, options) {
1930
1187
  path: assetsBasePath,
1931
1188
  handler: async (request, response) => {
1932
1189
  if (!fence(request, response)) return;
1933
- if (!method2(request, response, "GET")) return;
1190
+ if (!method(request, response, "GET")) return;
1934
1191
  try {
1935
1192
  const blob = store.serveAsset(String(request.url ?? ""));
1936
1193
  if (blob === null) {
1937
- return sendJson2(response, 404, { error: "asset not found", code: "ASSET_NOT_FOUND" });
1194
+ return sendJson(response, 404, { error: "asset not found", code: "ASSET_NOT_FOUND" });
1938
1195
  }
1939
1196
  response.writeHead(200, {
1940
1197
  "content-type": blob.meta.mime,
@@ -1967,60 +1224,24 @@ function mountPersonalizationRoutes(host, options) {
1967
1224
 
1968
1225
  // src/index.js
1969
1226
  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
1227
  function dshHome() {
1979
- return process.env.DSH_HOME?.trim() || join3(homedir(), ".dsh");
1228
+ return process.env.DSH_HOME?.trim() || join2(homedir(), ".dsh");
1980
1229
  }
1981
1230
  function apply(ctx) {
1982
- ctx.inject(["webServer", "agents", "webRuntime"], (hostContext) => {
1231
+ ctx.inject(["webServer", "webRuntime"], (hostContext) => {
1983
1232
  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
1233
  const personalization = createPersonalizationStore({
1994
- dataDir: join3(root, "dsh-skins")
1234
+ dataDir: join2(root, "dsh-skins")
1995
1235
  });
1996
1236
  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");
1237
+ return mountPersonalizationRoutes(hostContext, {
1238
+ store: personalization,
1239
+ trustedHosts: hostContext.webRuntime.trustedHosts
1240
+ });
1241
+ }, "dsh-skins: personalization routes");
2020
1242
  });
2021
1243
  }
2022
1244
  export {
2023
1245
  apply,
2024
- createSelfUpdater,
2025
1246
  name
2026
1247
  };