@iasiv5/dsh-skins 0.0.1 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js ADDED
@@ -0,0 +1,2025 @@
1
+ // src/index.js
2
+ import { readFileSync as readFileSync3 } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { dirname as dirname4, join as join3 } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ // src/host/restart.js
8
+ import { spawn } from "node:child_process";
9
+
10
+ // src/host/errors.js
11
+ function codedError(code, message, params) {
12
+ const error = message instanceof Error ? message : new Error(message);
13
+ error.code = code;
14
+ if (params !== void 0) error.params = params;
15
+ return error;
16
+ }
17
+ function publicError(error) {
18
+ const value = {
19
+ error: error instanceof Error ? error.message : String(error)
20
+ };
21
+ if (error !== null && error !== void 0 && error.code !== void 0) value.code = error.code;
22
+ if (error !== null && error !== void 0 && error.params !== void 0) value.params = error.params;
23
+ return value;
24
+ }
25
+
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
+ // src/host/atomic-write.js
301
+ import { mkdirSync, renameSync, writeFileSync } from "node:fs";
302
+ import { dirname } from "node:path";
303
+ function atomicWriteText(file, content, fs = { mkdirSync, writeFileSync, renameSync }) {
304
+ fs.mkdirSync(dirname(file), { recursive: true });
305
+ const temporary = `${file}.${process.pid}.tmp`;
306
+ fs.writeFileSync(temporary, content);
307
+ fs.renameSync(temporary, file);
308
+ }
309
+
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
+ // src/host/personalization/image-meta.js
840
+ function ascii(buffer, offset, length) {
841
+ return String.fromCharCode(...buffer.subarray(offset, offset + length));
842
+ }
843
+ function parsePng(buffer) {
844
+ if (buffer.length < 24) return null;
845
+ if (ascii(buffer, 0, 8) !== "‰PNG\r\n\n") return null;
846
+ if (ascii(buffer, 12, 4) !== "IHDR") return null;
847
+ if (buffer.readUInt32BE(8) !== 13) return null;
848
+ const width = buffer.readUInt32BE(16);
849
+ const height = buffer.readUInt32BE(20);
850
+ if (width === 0 || height === 0) return null;
851
+ return { mime: "image/png", width, height, animated: false };
852
+ }
853
+ function parseGif(buffer) {
854
+ if (buffer.length < 10) return null;
855
+ const signature = ascii(buffer, 0, 6);
856
+ if (signature !== "GIF87a" && signature !== "GIF89a") return null;
857
+ const width = buffer.readUInt16LE(6);
858
+ const height = buffer.readUInt16LE(8);
859
+ if (width === 0 || height === 0) return null;
860
+ return { mime: "image/gif", width, height, animated: null };
861
+ }
862
+ var JPEG_SOF_MARKERS = /* @__PURE__ */ new Set([
863
+ 192,
864
+ 193,
865
+ 194,
866
+ 195,
867
+ 197,
868
+ 198,
869
+ 199,
870
+ 201,
871
+ 202,
872
+ 203,
873
+ 205,
874
+ 206,
875
+ 207
876
+ ]);
877
+ function parseJpeg(buffer) {
878
+ if (buffer.length < 4 || buffer[0] !== 255 || buffer[1] !== 216) return null;
879
+ let offset = 2;
880
+ while (offset + 9 < buffer.length) {
881
+ if (buffer[offset] !== 255) return null;
882
+ const marker = buffer[offset + 1];
883
+ if (marker === 216 || marker === 1 || marker >= 208 && marker <= 215) {
884
+ offset += 2;
885
+ continue;
886
+ }
887
+ if (marker === 218 || marker === 217) return null;
888
+ const length = buffer.readUInt16BE(offset + 2);
889
+ if (length < 2) return null;
890
+ if (JPEG_SOF_MARKERS.has(marker)) {
891
+ const width = buffer.readUInt16BE(offset + 7);
892
+ const height = buffer.readUInt16BE(offset + 5);
893
+ if (width === 0 || height === 0) return null;
894
+ return { mime: "image/jpeg", width, height, animated: false };
895
+ }
896
+ offset += 2 + length;
897
+ }
898
+ return null;
899
+ }
900
+ function parseWebp(buffer) {
901
+ if (buffer.length < 30) return null;
902
+ if (ascii(buffer, 0, 4) !== "RIFF" || ascii(buffer, 8, 4) !== "WEBP") return null;
903
+ if (buffer.readUInt32LE(4) + 8 > buffer.length) return null;
904
+ const chunk = ascii(buffer, 12, 4);
905
+ const chunkSize = buffer.readUInt32LE(16);
906
+ if (20 + chunkSize > buffer.length) return null;
907
+ if (chunk === "VP8X") {
908
+ const flags = buffer[20];
909
+ const width = buffer.readUIntLE(24, 3) + 1;
910
+ const height = buffer.readUIntLE(27, 3) + 1;
911
+ return { mime: "image/webp", width, height, animated: (flags & 2) !== 0 };
912
+ }
913
+ if (chunk === "VP8 ") {
914
+ if (ascii(buffer, 23, 3) !== "*") return null;
915
+ const width = buffer.readUInt16LE(26) & 16383;
916
+ const height = buffer.readUInt16LE(28) & 16383;
917
+ if (width === 0 || height === 0) return null;
918
+ return { mime: "image/webp", width, height, animated: false };
919
+ }
920
+ if (chunk === "VP8L") {
921
+ if (buffer[20] !== 47) return null;
922
+ const bits = buffer.readUInt32LE(21);
923
+ const width = (bits & 16383) + 1;
924
+ const height = (bits >> 14 & 16383) + 1;
925
+ return { mime: "image/webp", width, height, animated: false };
926
+ }
927
+ return null;
928
+ }
929
+ function detectImageMeta(buffer) {
930
+ if (!Buffer.isBuffer(buffer) || buffer.length < 10) return null;
931
+ return parsePng(buffer) ?? parseJpeg(buffer) ?? parseGif(buffer) ?? parseWebp(buffer);
932
+ }
933
+ function extensionForMime(mime) {
934
+ if (mime === "image/png") return "png";
935
+ if (mime === "image/jpeg") return "jpg";
936
+ if (mime === "image/webp") return "webp";
937
+ if (mime === "image/gif") return "gif";
938
+ return null;
939
+ }
940
+
941
+ // src/shared/personalization/catalog.js
942
+ var CONFIG_VERSION = 1;
943
+ var GLOBAL_MAX_BYTES = 20 * 1024 * 1024;
944
+ var GLOBAL_MAX_PIXELS = 4e7;
945
+ var GIF_MAX_PIXELS = 12e6;
946
+ var ASSET_ID_PATTERN = /^u_[0-9a-f]{32}$/;
947
+ var ASSET_ID_PREFIX = "u_";
948
+ var BUILTIN_REF_PATTERN = /^builtin:([a-z0-9][a-z0-9-]*):([a-z0-9][a-z0-9-]*)$/;
949
+ var USER_IMAGE_MIMES = ["image/png", "image/jpeg", "image/webp", "image/gif"];
950
+ var WALLPAPER_FIELD = {
951
+ key: "wallpaper",
952
+ type: "image",
953
+ scope: "single",
954
+ labelKey: "personalization.wallpaper",
955
+ default: null,
956
+ // filled per skin below
957
+ allowedUserMime: USER_IMAGE_MIMES,
958
+ maxBytes: GLOBAL_MAX_BYTES,
959
+ maxWidth: 16384,
960
+ maxHeight: 16384,
961
+ maxPixels: GLOBAL_MAX_PIXELS
962
+ };
963
+ 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
+ // ADR-0004 (reversing design §9a): every catalog skin declares the same
1008
+ // standard field set. The legacy skins' catalog slogan defaults are
1009
+ // same-source with their factories' static `slogans`, and panelOpacity
1010
+ // default 55 anchors the derived projection to the baked alpha strings —
1011
+ // the byte-equivalence invariant is pinned by the projector tests.
1012
+ // Legacy wallpaper semantics (scrim never applies to user images,
1013
+ // placeholder branch) live inside each skin's project() branches.
1014
+ openbmc: {
1015
+ builtinAssets: { art: { mime: "image/webp", labelKey: "personalization.openbmc.art" } },
1016
+ fields: [
1017
+ { ...WALLPAPER_FIELD, default: "builtin:openbmc:art", builtinChoices: ["art"] },
1018
+ {
1019
+ key: "slogan",
1020
+ type: "text",
1021
+ scope: "locale",
1022
+ labelKey: "personalization.slogan",
1023
+ maxLength: 40,
1024
+ default: { zh: "察于未萌 · 治于未乱", en: "Govern before the storm" }
1025
+ },
1026
+ {
1027
+ key: "panelOpacity",
1028
+ type: "range",
1029
+ scope: "single",
1030
+ labelKey: "personalization.panelTranslucency",
1031
+ min: 0,
1032
+ max: 100,
1033
+ step: 1,
1034
+ unit: "%",
1035
+ default: 55
1036
+ }
1037
+ ]
1038
+ },
1039
+ "uefi-harness": {
1040
+ builtinAssets: { art: { mime: "image/webp", labelKey: "personalization.uefi.art" } },
1041
+ fields: [
1042
+ { ...WALLPAPER_FIELD, default: "builtin:uefi-harness:art", builtinChoices: ["art"] },
1043
+ {
1044
+ key: "slogan",
1045
+ type: "text",
1046
+ scope: "locale",
1047
+ labelKey: "personalization.slogan",
1048
+ maxLength: 40,
1049
+ default: { zh: "启于固件 · 行于万象", en: "Boot before everything" }
1050
+ },
1051
+ {
1052
+ key: "panelOpacity",
1053
+ type: "range",
1054
+ scope: "single",
1055
+ labelKey: "personalization.panelTranslucency",
1056
+ min: 0,
1057
+ max: 100,
1058
+ step: 1,
1059
+ unit: "%",
1060
+ default: 55
1061
+ }
1062
+ ]
1063
+ },
1064
+ // 美人志(出厂皮肤,注册顺序第一位):6 张内置精选,出厂默认 yuntai
1065
+ // (001合照)领头并对齐 tgcf 的「出厂默认领先」惯例;panelOpacity 锚定
1066
+ // tgcf 二次曲线(projector 数学见 skins/meirenzhi/index.js)。
1067
+ meirenzhi: {
1068
+ builtinAssets: {
1069
+ yuntai: { mime: "image/webp", labelKey: "personalization.meirenzhi.yuntai" },
1070
+ mupeiling: { mime: "image/webp", labelKey: "personalization.meirenzhi.mupeiling" },
1071
+ ziling: { mime: "image/webp", labelKey: "personalization.meirenzhi.ziling" },
1072
+ nangongwan: { mime: "image/webp", labelKey: "personalization.meirenzhi.nangongwan" },
1073
+ nangongque: { mime: "image/webp", labelKey: "personalization.meirenzhi.nangongque" },
1074
+ yinyue: { mime: "image/webp", labelKey: "personalization.meirenzhi.yinyue" }
1075
+ },
1076
+ fields: [
1077
+ {
1078
+ ...WALLPAPER_FIELD,
1079
+ default: "builtin:meirenzhi:yuntai",
1080
+ builtinChoices: [
1081
+ "yuntai",
1082
+ "mupeiling",
1083
+ "ziling",
1084
+ "nangongwan",
1085
+ "nangongque",
1086
+ "yinyue"
1087
+ ]
1088
+ },
1089
+ {
1090
+ key: "slogan",
1091
+ type: "text",
1092
+ scope: "locale",
1093
+ labelKey: "personalization.slogan",
1094
+ maxLength: 40,
1095
+ default: { zh: "风起凡尘 · 红颜问道", en: "From mortal dust, immortals bloom" }
1096
+ },
1097
+ {
1098
+ key: "panelOpacity",
1099
+ type: "range",
1100
+ scope: "single",
1101
+ labelKey: "personalization.panelTranslucency",
1102
+ min: 0,
1103
+ max: 100,
1104
+ step: 1,
1105
+ unit: "%",
1106
+ default: 35
1107
+ }
1108
+ ]
1109
+ }
1110
+ };
1111
+ function getSkinSchema(skinId) {
1112
+ const entry = SKINS[skinId];
1113
+ if (entry === void 0) return null;
1114
+ return { skinId, fields: entry.fields, builtinAssets: entry.builtinAssets };
1115
+ }
1116
+ function getField(skinId, key) {
1117
+ return SKINS[skinId]?.fields.find((field) => field.key === key) ?? null;
1118
+ }
1119
+ function listAssetFields(skinId) {
1120
+ return SKINS[skinId]?.fields.filter((field) => field.type === "image") ?? [];
1121
+ }
1122
+ function defaultsFor(skinId) {
1123
+ const values = {};
1124
+ for (const field of SKINS[skinId]?.fields ?? []) values[field.key] = field.default;
1125
+ return values;
1126
+ }
1127
+ function isSameOverrideValue(a, b) {
1128
+ if (a === b) return true;
1129
+ if (a === null || b === null || typeof a !== "object" || typeof b !== "object" || Array.isArray(a) || Array.isArray(b)) {
1130
+ return false;
1131
+ }
1132
+ const keysA = Object.keys(a);
1133
+ return keysA.length === Object.keys(b).length && keysA.every((key) => Object.hasOwn(b, key) && isSameOverrideValue(a[key], b[key]));
1134
+ }
1135
+ function resolveImageRef(value) {
1136
+ if (typeof value !== "string" || value.length === 0) return null;
1137
+ if (value.startsWith("builtin:")) {
1138
+ const match = BUILTIN_REF_PATTERN.exec(value);
1139
+ return match === null ? null : { kind: "builtin", skinId: match[1], assetKey: match[2] };
1140
+ }
1141
+ if (ASSET_ID_PATTERN.test(value)) return { kind: "user", id: value };
1142
+ return null;
1143
+ }
1144
+ var HEX_COLOR = /^#[0-9a-f]{6}$/i;
1145
+ var CONTROL_CHARS = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/;
1146
+ function scopeKeys(scope) {
1147
+ if (scope === "locale") return ["zh", "en"];
1148
+ if (scope === "colorScheme") return ["light", "dark"];
1149
+ return null;
1150
+ }
1151
+ function validScopeObject(value, scope, checkMember) {
1152
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
1153
+ const keys = scopeKeys(scope);
1154
+ if (Object.keys(value).length !== keys.length) return false;
1155
+ for (const key of keys) {
1156
+ if (!Object.prototype.hasOwnProperty.call(value, key)) return false;
1157
+ if (!checkMember(value[key])) return false;
1158
+ }
1159
+ return true;
1160
+ }
1161
+ function validateTextMember(value, field) {
1162
+ if (typeof value !== "string") return false;
1163
+ if (CONTROL_CHARS.test(value)) return false;
1164
+ return value.length <= field.maxLength;
1165
+ }
1166
+ function validateRangeMember(value, field) {
1167
+ if (typeof value !== "number" || !Number.isFinite(value)) return false;
1168
+ if (value < field.min || value > field.max) return false;
1169
+ if (field.step !== void 0 && field.step > 0) {
1170
+ const steps = (value - field.min) / field.step;
1171
+ if (Math.abs(steps - Math.round(steps)) > 1e-9) return false;
1172
+ }
1173
+ return true;
1174
+ }
1175
+ function validateScalar(value, field) {
1176
+ switch (field.type) {
1177
+ case "text":
1178
+ return validateTextMember(value, field);
1179
+ case "range":
1180
+ return validateRangeMember(value, field);
1181
+ case "select":
1182
+ return typeof value === "string" && field.options.some((option) => option.value === value);
1183
+ default:
1184
+ return false;
1185
+ }
1186
+ }
1187
+ function validateImageRef(value, field, skinId, meta) {
1188
+ const ref = resolveImageRef(value);
1189
+ if (ref === null) return false;
1190
+ if (ref.kind === "builtin") {
1191
+ if (ref.skinId !== skinId) return false;
1192
+ return SKINS[skinId]?.builtinAssets[ref.assetKey] !== void 0;
1193
+ }
1194
+ if (meta === void 0 || meta === null) return true;
1195
+ return metaSatisfiesField(field, meta);
1196
+ }
1197
+ function metaSatisfiesField(field, meta) {
1198
+ if (typeof meta.mime !== "string" || !field.allowedUserMime.includes(meta.mime)) return false;
1199
+ if (meta.byteLength > field.maxBytes) return false;
1200
+ if (meta.width > field.maxWidth || meta.height > field.maxHeight) return false;
1201
+ const maxPixels = meta.mime === "image/gif" ? Math.min(field.maxPixels, GIF_MAX_PIXELS) : field.maxPixels;
1202
+ return meta.width * meta.height <= maxPixels;
1203
+ }
1204
+ function validateOverride(skinId, key, value, metaProvider) {
1205
+ const field = getField(skinId, key);
1206
+ if (field === null) return { ok: false, code: "UNKNOWN_FIELD" };
1207
+ const provider = typeof metaProvider === "function" ? metaProvider : void 0;
1208
+ if (field.scope === "single") {
1209
+ if (field.type === "image") {
1210
+ const ref = resolveImageRef(value);
1211
+ if (ref === null) return { ok: false, code: "BAD_SHAPE" };
1212
+ if (ref.kind === "builtin") {
1213
+ if (!validateImageRef(value, field, skinId, void 0)) return { ok: false, code: "BAD_ASSET" };
1214
+ return { ok: true };
1215
+ }
1216
+ if (provider !== void 0) {
1217
+ const meta = provider(ref.id);
1218
+ if (meta === null) return { ok: false, code: "MISSING_ASSET" };
1219
+ if (!validateImageRef(value, field, skinId, meta)) return { ok: false, code: "BAD_ASSET" };
1220
+ }
1221
+ return { ok: true };
1222
+ }
1223
+ if (!validateScalar(value, field)) return { ok: false, code: "BAD_VALUE" };
1224
+ return { ok: true };
1225
+ }
1226
+ if (!validScopeObject(value, field.scope, (member) => {
1227
+ if (field.type === "text") return validateTextMember(member, field);
1228
+ if (field.type === "range") return validateRangeMember(member, field);
1229
+ if (field.type === "color") return typeof member === "string" && HEX_COLOR.test(member);
1230
+ return false;
1231
+ })) return { ok: false, code: "BAD_SHAPE" };
1232
+ return { ok: true };
1233
+ }
1234
+
1235
+ // src/host/personalization/store.js
1236
+ var STATE_FILE = "state.json";
1237
+ var ASSETS_DIR = "assets";
1238
+ var QUARANTINE_DIR = "quarantine";
1239
+ var DISK_SAFETY_RESERVE = 64 * 1024 * 1024;
1240
+ var CORRUPT_BACKUP_LIMIT = 3;
1241
+ var ID_ATTEMPTS = 5;
1242
+ function defaultFs() {
1243
+ return {
1244
+ existsSync: existsSync2,
1245
+ mkdirSync: mkdirSync3,
1246
+ readFileSync: readFileSync2,
1247
+ writeFileSync: writeFileSync3,
1248
+ renameSync: renameSync3,
1249
+ unlinkSync: unlinkSync2,
1250
+ readdirSync,
1251
+ rmdirSync,
1252
+ statSync,
1253
+ statfsSync,
1254
+ copyFileSync,
1255
+ openSync,
1256
+ closeSync
1257
+ };
1258
+ }
1259
+ function sha256Hex(buffer) {
1260
+ return createHash("sha256").update(buffer).digest("hex");
1261
+ }
1262
+ function sanitizeDisplayName(name2) {
1263
+ if (typeof name2 !== "string") return null;
1264
+ const cleaned = name2.replace(/[\u0000-\u001f\u007f]/g, "");
1265
+ if (cleaned.trim().length === 0) return null;
1266
+ if (Buffer.byteLength(cleaned, "utf8") > 200) return null;
1267
+ return cleaned;
1268
+ }
1269
+ function isValidStateShape(state) {
1270
+ if (state === null || typeof state !== "object" || Array.isArray(state)) return false;
1271
+ if (!Number.isInteger(state.configVersion) || state.configVersion < 1) return false;
1272
+ if (!Number.isInteger(state.revision) || state.revision < 0) return false;
1273
+ if (typeof state.skins !== "object" || state.skins === null || Array.isArray(state.skins)) return false;
1274
+ if (typeof state.library !== "object" || state.library === null || Array.isArray(state.library)) return false;
1275
+ if (state.recoveryCleanup !== void 0) {
1276
+ const pending = state.recoveryCleanup;
1277
+ if (pending === null || typeof pending !== "object" || Array.isArray(pending) || !Array.isArray(pending.quarantine) || !pending.quarantine.every((name2) => typeof name2 === "string")) return false;
1278
+ }
1279
+ return true;
1280
+ }
1281
+ var ASSET_EXTENSIONS = /* @__PURE__ */ new Set(["png", "jpg", "webp", "gif"]);
1282
+ function isValidAssetMeta(meta, id) {
1283
+ if (meta === null || typeof meta !== "object" || Array.isArray(meta)) return false;
1284
+ if (typeof meta.id !== "string" || meta.id !== id || !ASSET_ID_PATTERN.test(id)) return false;
1285
+ if (typeof meta.displayName !== "string") return false;
1286
+ if (typeof meta.mime !== "string" || !/^(image\/(png|jpeg|webp|gif))$/.test(meta.mime)) return false;
1287
+ if (typeof meta.extension !== "string" || !ASSET_EXTENSIONS.has(meta.extension)) return false;
1288
+ if (meta.extension !== extensionForMime(meta.mime)) return false;
1289
+ if (!Number.isInteger(meta.byteLength) || meta.byteLength <= 0 || meta.byteLength > GLOBAL_MAX_BYTES) return false;
1290
+ if (!Number.isInteger(meta.width) || meta.width <= 0) return false;
1291
+ if (!Number.isInteger(meta.height) || meta.height <= 0) return false;
1292
+ const pixelCap = meta.mime === "image/gif" ? GIF_MAX_PIXELS : GLOBAL_MAX_PIXELS;
1293
+ if (meta.width * meta.height > pixelCap) return false;
1294
+ if (typeof meta.sha256 !== "string" || !/^[0-9a-f]{64}$/.test(meta.sha256)) return false;
1295
+ return typeof meta.createdAt === "string";
1296
+ }
1297
+ function isValidStateDeep(state) {
1298
+ if (!isValidStateShape(state)) return false;
1299
+ for (const [id, meta] of Object.entries(state.library)) {
1300
+ if (!isValidAssetMeta(meta, id)) return false;
1301
+ }
1302
+ for (const section of Object.values(state.skins)) {
1303
+ if (section === null || typeof section !== "object" || Array.isArray(section)) return false;
1304
+ }
1305
+ return true;
1306
+ }
1307
+ function emptyState() {
1308
+ return { configVersion: CONFIG_VERSION, revision: 0, skins: {}, library: {} };
1309
+ }
1310
+ function createPersonalizationStore(options = {}) {
1311
+ const dataDir = options.dataDir;
1312
+ if (typeof dataDir !== "string" || dataDir.length === 0) {
1313
+ throw new Error("personalization store requires a dataDir");
1314
+ }
1315
+ const fs = options.fs ?? defaultFs();
1316
+ const now = options.now ?? Date.now;
1317
+ const stateFile = join2(dataDir, STATE_FILE);
1318
+ const assetsDir = join2(dataDir, ASSETS_DIR);
1319
+ const quarantineDir = join2(dataDir, QUARANTINE_DIR);
1320
+ let state = null;
1321
+ let mode = "normal";
1322
+ let recovery = null;
1323
+ let chain = Promise.resolve();
1324
+ let initialized = false;
1325
+ function ensureDirs() {
1326
+ fs.mkdirSync(assetsDir, { recursive: true });
1327
+ }
1328
+ function readStateFile() {
1329
+ if (!fs.existsSync(stateFile)) return { kind: "missing" };
1330
+ let parsed;
1331
+ try {
1332
+ parsed = JSON.parse(fs.readFileSync(stateFile, "utf8"));
1333
+ } catch {
1334
+ return { kind: "corrupt" };
1335
+ }
1336
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) && Number.isInteger(parsed.configVersion) && parsed.configVersion > CONFIG_VERSION) {
1337
+ return { kind: "future", state: parsed };
1338
+ }
1339
+ if (!isValidStateShape(parsed)) return { kind: "corrupt" };
1340
+ return { kind: "ok", state: parsed };
1341
+ }
1342
+ function listAssetFiles() {
1343
+ if (!fs.existsSync(assetsDir)) return [];
1344
+ return fs.readdirSync(assetsDir).filter((name2) => !name2.startsWith("."));
1345
+ }
1346
+ function blobFileFor(id, extension) {
1347
+ return join2(assetsDir, `${id}.${extension}`);
1348
+ }
1349
+ function commitState(next) {
1350
+ atomicWriteText(stateFile, `${JSON.stringify(next, null, 2)}
1351
+ `, fs);
1352
+ state = next;
1353
+ }
1354
+ function enqueue(work) {
1355
+ const run = chain.then(work);
1356
+ chain = run.then(() => void 0, () => void 0);
1357
+ return run;
1358
+ }
1359
+ function sniffBlobFile(name2) {
1360
+ const file = join2(assetsDir, name2);
1361
+ let buffer;
1362
+ try {
1363
+ buffer = fs.readFileSync(file);
1364
+ } catch {
1365
+ return { name: name2, kind: "unreadable" };
1366
+ }
1367
+ const meta = detectImageMeta(buffer);
1368
+ if (meta === null) return { name: name2, kind: "unrecognized" };
1369
+ const id = name2.split(".")[0];
1370
+ if (!ASSET_ID_PATTERN.test(id)) return { name: name2, kind: "unrecognized" };
1371
+ const extension = extensionForMime(meta.mime);
1372
+ if (name2 !== `${id}.${extension}`) return { name: name2, kind: "unrecognized" };
1373
+ return {
1374
+ name: name2,
1375
+ kind: "asset",
1376
+ meta: {
1377
+ id,
1378
+ displayName: name2,
1379
+ mime: meta.mime,
1380
+ extension,
1381
+ byteLength: buffer.length,
1382
+ width: meta.width,
1383
+ height: meta.height,
1384
+ sha256: sha256Hex(buffer),
1385
+ createdAt: new Date(now()).toISOString()
1386
+ }
1387
+ };
1388
+ }
1389
+ function init() {
1390
+ if (initialized) return;
1391
+ try {
1392
+ initOnce();
1393
+ } catch (error) {
1394
+ initialized = false;
1395
+ state = null;
1396
+ throw error;
1397
+ }
1398
+ }
1399
+ function initOnce() {
1400
+ initialized = true;
1401
+ const read = readStateFile();
1402
+ if (read.kind === "future" || read.kind === "ok" && read.state.configVersion > CONFIG_VERSION) {
1403
+ mode = "unsupported";
1404
+ state = read.state;
1405
+ return;
1406
+ }
1407
+ ensureDirs();
1408
+ const assetFiles = listAssetFiles();
1409
+ if (read.kind === "missing" && assetFiles.length === 0) {
1410
+ commitState(emptyState());
1411
+ mode = "normal";
1412
+ gcNow();
1413
+ return;
1414
+ }
1415
+ if (read.kind === "missing" || read.kind === "corrupt" || !isValidStateDeep(read.state)) {
1416
+ const candidateLibrary = {};
1417
+ const quarantine = [];
1418
+ for (const scanned of assetFiles.map(sniffBlobFile)) {
1419
+ if (scanned.kind === "asset") candidateLibrary[scanned.meta.id] = scanned.meta;
1420
+ else quarantine.push(scanned.name);
1421
+ }
1422
+ mode = "recovery";
1423
+ recovery = { candidateLibrary, quarantine, configLost: true };
1424
+ state = emptyState();
1425
+ return;
1426
+ }
1427
+ mode = "normal";
1428
+ state = read.state;
1429
+ if (state.recoveryCleanup !== void 0) {
1430
+ finishRecoveryCleanup();
1431
+ }
1432
+ normalizeState();
1433
+ gcNow();
1434
+ }
1435
+ function normalizeState() {
1436
+ const provider = metaProviderFactory(state.library);
1437
+ let removed = false;
1438
+ for (const skinId of Object.keys(state.skins)) {
1439
+ if (getSkinSchema(skinId) === null) {
1440
+ delete state.skins[skinId];
1441
+ removed = true;
1442
+ continue;
1443
+ }
1444
+ const section = state.skins[skinId];
1445
+ const defaults = defaultsFor(skinId);
1446
+ for (const key of Object.keys(section)) {
1447
+ if (!validateOverride(skinId, key, section[key], provider).ok) {
1448
+ delete section[key];
1449
+ removed = true;
1450
+ continue;
1451
+ }
1452
+ if (isSameOverrideValue(section[key], defaults[key])) {
1453
+ delete section[key];
1454
+ removed = true;
1455
+ }
1456
+ }
1457
+ if (Object.keys(section).length === 0) {
1458
+ delete state.skins[skinId];
1459
+ removed = true;
1460
+ }
1461
+ }
1462
+ if (removed) {
1463
+ state.revision += 1;
1464
+ commitState(state);
1465
+ }
1466
+ }
1467
+ function gcNow() {
1468
+ if (mode !== "normal" || state?.recoveryCleanup !== void 0) return;
1469
+ const live = new Set(Object.keys(state.library));
1470
+ for (const name2 of listAssetFiles()) {
1471
+ const id = name2.split(".")[0];
1472
+ if (!live.has(id)) {
1473
+ try {
1474
+ fs.unlinkSync(join2(assetsDir, name2));
1475
+ } catch {
1476
+ }
1477
+ }
1478
+ }
1479
+ }
1480
+ function requireNormal(operation) {
1481
+ if (mode === "unsupported") {
1482
+ throw codedError("STORE_READONLY", `配置状态为更高版本(configVersion>${CONFIG_VERSION}),${operation}被拒绝`);
1483
+ }
1484
+ if (mode === "recovery") {
1485
+ throw codedError("STORE_RECOVERY_REQUIRED", `配置状态待恢复,${operation}被拒绝`);
1486
+ }
1487
+ }
1488
+ function metaProviderFactory(library) {
1489
+ return (id) => library[id] ?? null;
1490
+ }
1491
+ function referencesFor(skins, library) {
1492
+ const references = {};
1493
+ for (const id of Object.keys(library)) references[id] = [];
1494
+ for (const [skinId, section] of Object.entries(skins)) {
1495
+ for (const field of listAssetFields(skinId)) {
1496
+ const value = section?.[field.key];
1497
+ if (typeof value === "string" && references[value] !== void 0) {
1498
+ references[value].push({ skinId, key: field.key });
1499
+ }
1500
+ }
1501
+ }
1502
+ return references;
1503
+ }
1504
+ function snapshot() {
1505
+ init();
1506
+ const library = state.library ?? {};
1507
+ const skins = state.skins ?? {};
1508
+ const base = {
1509
+ configVersion: state.configVersion,
1510
+ revision: state.revision,
1511
+ skins,
1512
+ library: Object.values(library),
1513
+ mode,
1514
+ quota: {
1515
+ count: Object.keys(library).length,
1516
+ totalBytes: Object.values(library).reduce((sum, meta) => sum + (meta?.byteLength ?? 0), 0)
1517
+ }
1518
+ };
1519
+ if (mode === "normal") base.references = referencesFor(skins, library);
1520
+ if (mode === "recovery") base.recovery = recovery;
1521
+ return base;
1522
+ }
1523
+ async function applyOperations({ baseRevision, operations }) {
1524
+ init();
1525
+ requireNormal("配置写入");
1526
+ return enqueue(() => {
1527
+ if (!Number.isInteger(baseRevision) || baseRevision < 0 || baseRevision !== state.revision) {
1528
+ throw codedError("REVISION_CONFLICT", "配置已被其他会话修改(修订号过期),请刷新后重试");
1529
+ }
1530
+ if (!Array.isArray(operations) || operations.length === 0 || operations.length > 64) {
1531
+ throw codedError("INVALID_CONFIG", "operations 必须是 1–64 个元素的数组");
1532
+ }
1533
+ for (const operation of operations) {
1534
+ if (operation === null || typeof operation !== "object") {
1535
+ throw codedError("INVALID_CONFIG", "operation 必须是对象");
1536
+ }
1537
+ const { op, skinId, key } = operation;
1538
+ if (op !== "set" && op !== "delete") throw codedError("INVALID_CONFIG", "op 必须是 set 或 delete");
1539
+ if (getField(skinId, key) === null) {
1540
+ throw codedError("INVALID_CONFIG", `未知字段 ${skinId}.${key}`);
1541
+ }
1542
+ }
1543
+ const draft = structuredClone(state);
1544
+ const provider = metaProviderFactory(draft.library);
1545
+ for (const operation of operations) {
1546
+ const { op, skinId, key } = operation;
1547
+ if (op === "set") {
1548
+ const verdict = validateOverride(skinId, key, operation.value, provider);
1549
+ if (!verdict.ok) {
1550
+ throw codedError("INVALID_CONFIG", `${skinId}.${key} 校验失败(${verdict.code})`, { code: verdict.code });
1551
+ }
1552
+ }
1553
+ }
1554
+ for (const { op, skinId, key, value } of operations) {
1555
+ draft.skins[skinId] = draft.skins[skinId] ?? {};
1556
+ if (op === "set") draft.skins[skinId][key] = value;
1557
+ else delete draft.skins[skinId][key];
1558
+ }
1559
+ draft.revision += 1;
1560
+ commitState(draft);
1561
+ gcNow();
1562
+ return { revision: draft.revision };
1563
+ });
1564
+ }
1565
+ function ingestAssetMeta(buffer, { declaredMime } = {}) {
1566
+ const meta = detectImageMeta(buffer);
1567
+ if (meta === null) throw codedError("UNSUPPORTED_IMAGE", "无法识别的图片格式");
1568
+ if (meta.animated === true) throw codedError("ANIMATION_UNSUPPORTED", "动画 WebP 暂不支持");
1569
+ if (typeof declaredMime === "string" && declaredMime !== "" && declaredMime !== meta.mime) {
1570
+ throw codedError("UNSUPPORTED_IMAGE", `声明的 ${declaredMime} 与实际内容 ${meta.mime} 不符`);
1571
+ }
1572
+ if (buffer.length > GLOBAL_MAX_BYTES) {
1573
+ throw codedError("UPLOAD_TOO_LARGE", "图片超过 20MB 上限");
1574
+ }
1575
+ const pixelCap = meta.mime === "image/gif" ? GIF_MAX_PIXELS : GLOBAL_MAX_PIXELS;
1576
+ if (meta.width * meta.height > pixelCap) {
1577
+ throw codedError("UPLOAD_TOO_LARGE", `图片像素超过上限(${meta.mime === "image/gif" ? "12" : "40"}MP)`);
1578
+ }
1579
+ return meta;
1580
+ }
1581
+ function checkDiskSpace(incomingBytes) {
1582
+ if (typeof fs.statfsSync !== "function") return;
1583
+ try {
1584
+ const stats = fs.statfsSync(dataDir);
1585
+ const required = BigInt(incomingBytes) * 3n + BigInt(DISK_SAFETY_RESERVE);
1586
+ if (BigInt(stats.bsize) * BigInt(stats.bavail) < required) {
1587
+ throw codedError("DISK_FULL", "磁盘剩余空间不足,拒绝写入");
1588
+ }
1589
+ } catch (error) {
1590
+ if (error?.code === "DISK_FULL") throw error;
1591
+ }
1592
+ }
1593
+ function writeBlobExclusive(buffer, extension) {
1594
+ for (let attempt = 0; attempt < ID_ATTEMPTS; attempt += 1) {
1595
+ const id = ASSET_ID_PREFIX + randomUUID2().replaceAll("-", "");
1596
+ const target = blobFileFor(id, extension);
1597
+ let descriptor;
1598
+ try {
1599
+ descriptor = fs.openSync(target, "wx");
1600
+ } catch (error) {
1601
+ if (error?.code === "EEXIST") continue;
1602
+ throw error;
1603
+ }
1604
+ try {
1605
+ fs.writeFileSync(descriptor, buffer);
1606
+ return { id, target };
1607
+ } finally {
1608
+ try {
1609
+ fs.closeSync(descriptor);
1610
+ } catch {
1611
+ }
1612
+ }
1613
+ }
1614
+ throw codedError("STORE_WRITE_FAILED", "无法分配新的资产 id(连续碰撞)");
1615
+ }
1616
+ async function uploadAsset(buffer, { displayName, declaredMime }) {
1617
+ init();
1618
+ requireNormal("图片上传");
1619
+ const meta = ingestAssetMeta(buffer, { declaredMime });
1620
+ const name2 = sanitizeDisplayName(displayName);
1621
+ if (name2 === null) throw codedError("FILENAME_INVALID", "文件展示名无效");
1622
+ return enqueue(() => {
1623
+ checkDiskSpace(buffer.length);
1624
+ const extension = extensionForMime(meta.mime);
1625
+ const { id, target } = writeBlobExclusive(buffer, extension);
1626
+ const asset = {
1627
+ id,
1628
+ displayName: name2,
1629
+ mime: meta.mime,
1630
+ extension,
1631
+ byteLength: buffer.length,
1632
+ width: meta.width,
1633
+ height: meta.height,
1634
+ sha256: sha256Hex(buffer),
1635
+ createdAt: new Date(now()).toISOString()
1636
+ };
1637
+ const draft = structuredClone(state);
1638
+ draft.library[id] = asset;
1639
+ draft.revision += 1;
1640
+ try {
1641
+ commitState(draft);
1642
+ } catch (error) {
1643
+ try {
1644
+ fs.unlinkSync(target);
1645
+ } catch {
1646
+ }
1647
+ throw error;
1648
+ }
1649
+ return { asset, revision: draft.revision };
1650
+ });
1651
+ }
1652
+ async function deleteAsset(id) {
1653
+ init();
1654
+ requireNormal("图片删除");
1655
+ if (typeof id !== "string" || !ASSET_ID_PATTERN.test(id)) {
1656
+ throw codedError("INVALID_ASSET_ID", "非法的图片 id");
1657
+ }
1658
+ return enqueue(() => {
1659
+ const meta = state.library[id];
1660
+ if (meta === void 0) throw codedError("ASSET_NOT_FOUND", "图片不存在");
1661
+ const draft = structuredClone(state);
1662
+ const affectedSkins = [];
1663
+ for (const [skinId, section] of Object.entries(draft.skins)) {
1664
+ for (const field of listAssetFields(skinId)) {
1665
+ if (section?.[field.key] === id) {
1666
+ delete section[field.key];
1667
+ affectedSkins.push({ skinId, key: field.key });
1668
+ }
1669
+ }
1670
+ }
1671
+ delete draft.library[id];
1672
+ draft.revision += 1;
1673
+ commitState(draft);
1674
+ try {
1675
+ fs.unlinkSync(blobFileFor(id, meta.extension));
1676
+ } catch {
1677
+ }
1678
+ gcNow();
1679
+ return { revision: draft.revision, affectedSkins };
1680
+ });
1681
+ }
1682
+ function serveAsset(url) {
1683
+ init();
1684
+ const path = String(url ?? "").split("?")[0];
1685
+ const match = /\/dsh-skins\/assets\/([^/]+)$/.exec(path);
1686
+ if (match === null) return null;
1687
+ const name2 = match[1];
1688
+ if (!/^u_[0-9a-f]{32}\.(png|jpe?g|webp|gif)$/.test(name2)) return null;
1689
+ const id = name2.split(".")[0];
1690
+ const meta = (state.library ?? {})[id];
1691
+ if (meta === void 0) return null;
1692
+ if (name2 !== `${id}.${meta.extension}`) return null;
1693
+ try {
1694
+ return { buffer: fs.readFileSync(blobFileFor(id, meta.extension)), meta };
1695
+ } catch {
1696
+ return null;
1697
+ }
1698
+ }
1699
+ async function confirmRecovery() {
1700
+ init();
1701
+ if (mode !== "recovery") throw codedError("STORE_NOT_RECOVERING", "当前不在恢复模式");
1702
+ return enqueue(() => {
1703
+ try {
1704
+ if (fs.existsSync(stateFile) && typeof fs.copyFileSync === "function") {
1705
+ try {
1706
+ fs.copyFileSync(stateFile, `${stateFile}.corrupt.${now()}.json`);
1707
+ pruneCorruptBackups();
1708
+ } catch {
1709
+ }
1710
+ }
1711
+ } catch {
1712
+ }
1713
+ const draft = emptyState();
1714
+ draft.library = recovery.candidateLibrary;
1715
+ draft.revision = 1;
1716
+ draft.recoveryCleanup = { quarantine: [...recovery.quarantine] };
1717
+ commitState(draft);
1718
+ finishRecoveryCleanup();
1719
+ mode = "normal";
1720
+ recovery = null;
1721
+ gcNow();
1722
+ return { revision: draft.revision };
1723
+ });
1724
+ }
1725
+ function finishRecoveryCleanup() {
1726
+ const pending = state.recoveryCleanup;
1727
+ if (pending === void 0) return;
1728
+ fs.mkdirSync(quarantineDir, { recursive: true });
1729
+ for (const name2 of pending.quarantine) {
1730
+ try {
1731
+ fs.renameSync(join2(assetsDir, name2), join2(quarantineDir, name2));
1732
+ } catch {
1733
+ }
1734
+ }
1735
+ const cleaned = structuredClone(state);
1736
+ delete cleaned.recoveryCleanup;
1737
+ commitState(cleaned);
1738
+ }
1739
+ function pruneCorruptBackups() {
1740
+ const dir = join2(dataDir);
1741
+ if (!fs.existsSync(dir)) return;
1742
+ const backups = fs.readdirSync(dir).filter((name2) => name2.startsWith(`${STATE_FILE}.corrupt.`)).sort();
1743
+ while (backups.length > CORRUPT_BACKUP_LIMIT) {
1744
+ try {
1745
+ fs.unlinkSync(join2(dir, backups.shift()));
1746
+ } catch {
1747
+ }
1748
+ }
1749
+ }
1750
+ return {
1751
+ init,
1752
+ snapshot,
1753
+ applyOperations,
1754
+ uploadAsset,
1755
+ deleteAsset,
1756
+ serveAsset,
1757
+ confirmRecovery,
1758
+ getMode: () => {
1759
+ init();
1760
+ return mode;
1761
+ }
1762
+ };
1763
+ }
1764
+
1765
+ // src/host/personalization-routes.js
1766
+ var CODE_STATUS = {
1767
+ INVALID_CONFIG: 400,
1768
+ INVALID_ASSET_ID: 400,
1769
+ FILENAME_INVALID: 400,
1770
+ ASSET_NOT_FOUND: 404,
1771
+ UNKNOWN_SKIN: 404,
1772
+ UNSUPPORTED_IMAGE: 415,
1773
+ ANIMATION_UNSUPPORTED: 415,
1774
+ UPLOAD_TOO_LARGE: 413,
1775
+ DISK_FULL: 507,
1776
+ STORE_READONLY: 409,
1777
+ REVISION_CONFLICT: 409,
1778
+ STORE_RECOVERY_REQUIRED: 409,
1779
+ STORE_NOT_RECOVERING: 409
1780
+ };
1781
+ function sendJson2(response, status, value, extraHeaders = {}) {
1782
+ response.writeHead(status, {
1783
+ "content-type": "application/json; charset=utf-8",
1784
+ "cache-control": "no-store",
1785
+ ...extraHeaders
1786
+ });
1787
+ response.end(JSON.stringify(value));
1788
+ }
1789
+ function sendError(response, error) {
1790
+ const status = CODE_STATUS[error?.code] ?? 500;
1791
+ sendJson2(response, status, publicError(error));
1792
+ }
1793
+ function header2(headers, name2) {
1794
+ const value = headers?.[name2];
1795
+ return typeof value === "string" ? value : Array.isArray(value) ? value[0] : void 0;
1796
+ }
1797
+ function method2(request, response, expected) {
1798
+ if (request.method === expected) return true;
1799
+ response.writeHead(405, { allow: expected });
1800
+ response.end();
1801
+ return false;
1802
+ }
1803
+ async function readJsonBody2(request, limit = 128 * 1024) {
1804
+ const type = String(request.headers["content-type"] ?? "").toLowerCase();
1805
+ if (!type.startsWith("application/json")) throw codedError("INVALID_CONFIG", "content-type must be application/json");
1806
+ const chunks = [];
1807
+ let size = 0;
1808
+ for await (const chunk of request) {
1809
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
1810
+ size += buffer.length;
1811
+ if (size > limit) throw codedError("INVALID_CONFIG", "request body too large");
1812
+ chunks.push(buffer);
1813
+ }
1814
+ if (size === 0) return {};
1815
+ let value;
1816
+ try {
1817
+ value = JSON.parse(Buffer.concat(chunks).toString("utf8"));
1818
+ } catch {
1819
+ throw codedError("INVALID_CONFIG", "请求体不是合法 JSON");
1820
+ }
1821
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
1822
+ throw codedError("INVALID_CONFIG", "invalid request body");
1823
+ }
1824
+ return value;
1825
+ }
1826
+ async function readRawBody(request, limit, overflowCode = "UPLOAD_TOO_LARGE") {
1827
+ const chunks = [];
1828
+ let size = 0;
1829
+ for await (const chunk of request) {
1830
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
1831
+ size += buffer.length;
1832
+ if (size > limit) throw codedError(overflowCode, "request body too large");
1833
+ chunks.push(buffer);
1834
+ }
1835
+ return Buffer.concat(chunks);
1836
+ }
1837
+ function decodeDisplayName(request) {
1838
+ const raw = header2(request.headers, "x-filename");
1839
+ if (raw === void 0 || raw === "") return "wallpaper";
1840
+ try {
1841
+ return decodeURIComponent(raw);
1842
+ } catch {
1843
+ throw codedError("FILENAME_INVALID", "x-filename 不是合法的 encodeURIComponent 输出");
1844
+ }
1845
+ }
1846
+ function mountPersonalizationRoutes(host, options) {
1847
+ const { store, assetsBasePath = "/dsh-skins/assets", trustedHosts = [] } = options;
1848
+ const disposers = [];
1849
+ const fence = (request, response) => {
1850
+ if (isTrustedRequest(request, trustedHosts)) return true;
1851
+ sendJson2(response, 403, { error: "trusted DSH Web request required" });
1852
+ return false;
1853
+ };
1854
+ try {
1855
+ const configRoute = host.webServer.register({
1856
+ kind: "exact",
1857
+ path: "/dsh-skins/config",
1858
+ handler: async (request, response) => {
1859
+ if (!fence(request, response)) return;
1860
+ if (request.method === "GET") return sendJson2(response, 200, store.snapshot());
1861
+ if (!method2(request, response, "PATCH")) return;
1862
+ try {
1863
+ const body = await readJsonBody2(request);
1864
+ const result = await store.applyOperations(body);
1865
+ return sendJson2(response, 200, result);
1866
+ } catch (error) {
1867
+ return sendError(response, error);
1868
+ }
1869
+ }
1870
+ });
1871
+ disposers.push(configRoute);
1872
+ const recoveryRoute = host.webServer.register({
1873
+ kind: "exact",
1874
+ path: "/dsh-skins/recovery",
1875
+ handler: async (request, response) => {
1876
+ if (!fence(request, response)) return;
1877
+ if (!method2(request, response, "POST")) return;
1878
+ try {
1879
+ return sendJson2(response, 200, await store.confirmRecovery());
1880
+ } catch (error) {
1881
+ return sendError(response, error);
1882
+ }
1883
+ }
1884
+ });
1885
+ disposers.push(recoveryRoute);
1886
+ const libraryRoute = host.webServer.register({
1887
+ kind: "exact",
1888
+ path: "/dsh-skins/library",
1889
+ handler: async (request, response) => {
1890
+ if (!fence(request, response)) return;
1891
+ if (request.method === "GET") return sendJson2(response, 200, store.snapshot());
1892
+ if (!method2(request, response, "POST")) return;
1893
+ try {
1894
+ const declaredLength = Number(header2(request.headers, "content-length") ?? "0");
1895
+ if (declaredLength > GLOBAL_MAX_BYTES) throw codedError("UPLOAD_TOO_LARGE", "图片超过 20MB 上限");
1896
+ const buffer = await readRawBody(request, GLOBAL_MAX_BYTES + 1024);
1897
+ const displayName = decodeDisplayName(request);
1898
+ const declaredMime = header2(request.headers, "content-type");
1899
+ const result = await store.uploadAsset(buffer, { displayName, declaredMime });
1900
+ return sendJson2(response, 201, result);
1901
+ } catch (error) {
1902
+ return sendError(response, error);
1903
+ }
1904
+ }
1905
+ });
1906
+ disposers.push(libraryRoute);
1907
+ const libraryDeleteRoute = host.webServer.register({
1908
+ kind: "prefix",
1909
+ path: "/dsh-skins/library",
1910
+ handler: async (request, response) => {
1911
+ if (!fence(request, response)) return;
1912
+ if (!method2(request, response, "DELETE")) return;
1913
+ const path = String(request.url ?? "").split("?")[0];
1914
+ const match = /\/dsh-skins\/library\/([^/]+)$/.exec(path);
1915
+ const suffix = match?.[1] ?? "";
1916
+ if (!ASSET_ID_PATTERN.test(suffix)) {
1917
+ return sendJson2(response, 400, { error: "invalid asset id", code: "INVALID_ASSET_ID" });
1918
+ }
1919
+ try {
1920
+ return sendJson2(response, 200, await store.deleteAsset(suffix));
1921
+ } catch (error) {
1922
+ return sendError(response, error);
1923
+ }
1924
+ }
1925
+ });
1926
+ disposers.push(libraryDeleteRoute);
1927
+ const assetsRoute = host.webServer.register({
1928
+ kind: "prefix",
1929
+ path: assetsBasePath,
1930
+ handler: async (request, response) => {
1931
+ if (!fence(request, response)) return;
1932
+ if (!method2(request, response, "GET")) return;
1933
+ try {
1934
+ const blob = store.serveAsset(String(request.url ?? ""));
1935
+ if (blob === null) {
1936
+ return sendJson2(response, 404, { error: "asset not found", code: "ASSET_NOT_FOUND" });
1937
+ }
1938
+ response.writeHead(200, {
1939
+ "content-type": blob.meta.mime,
1940
+ "content-length": blob.meta.byteLength,
1941
+ "cache-control": "private, max-age=31536000, immutable",
1942
+ etag: `"${blob.meta.sha256}"`,
1943
+ "x-content-type-options": "nosniff",
1944
+ "cross-origin-resource-policy": "same-origin"
1945
+ });
1946
+ response.end(blob.buffer);
1947
+ } catch (error) {
1948
+ return sendError(response, error);
1949
+ }
1950
+ }
1951
+ });
1952
+ disposers.push(assetsRoute);
1953
+ } catch (error) {
1954
+ for (let index = disposers.length - 1; index >= 0; index -= 1) {
1955
+ try {
1956
+ disposers[index]();
1957
+ } catch {
1958
+ }
1959
+ }
1960
+ throw error;
1961
+ }
1962
+ return () => {
1963
+ for (const dispose of disposers) dispose();
1964
+ };
1965
+ }
1966
+
1967
+ // src/index.js
1968
+ var name = "@iasiv5/dsh-skins";
1969
+ function packageRoot() {
1970
+ return join3(dirname4(fileURLToPath(import.meta.url)), "..");
1971
+ }
1972
+ function packageVersion() {
1973
+ const manifest = JSON.parse(readFileSync3(join3(packageRoot(), "package.json"), "utf8"));
1974
+ if (typeof manifest.version !== "string") throw new Error("dsh-skins package.json 缺少版本号");
1975
+ return manifest.version;
1976
+ }
1977
+ function dshHome() {
1978
+ return process.env.DSH_HOME?.trim() || join3(homedir(), ".dsh");
1979
+ }
1980
+ function apply(ctx) {
1981
+ ctx.inject(["webServer", "agents", "webRuntime"], (hostContext) => {
1982
+ const root = dshHome();
1983
+ const profileDir = join3(root, "profiles", "web");
1984
+ const updater = createSelfUpdater({
1985
+ profileDir,
1986
+ cacheFile: join3(root, "dsh-skins", "update-cache.json"),
1987
+ currentVersion: packageVersion()
1988
+ }, {
1989
+ runner: runDshPlugin
1990
+ });
1991
+ const restart = createRestartScheduler(ctx.get("appExit"));
1992
+ const personalization = createPersonalizationStore({
1993
+ dataDir: join3(root, "dsh-skins")
1994
+ });
1995
+ hostContext.effect(() => {
1996
+ let disposeUpdate = null;
1997
+ let disposePersonalization = null;
1998
+ try {
1999
+ disposeUpdate = mountUpdateRoutes(hostContext, {
2000
+ updater,
2001
+ restart,
2002
+ agents: hostContext.agents,
2003
+ trustedHosts: hostContext.webRuntime.trustedHosts
2004
+ });
2005
+ disposePersonalization = mountPersonalizationRoutes(hostContext, {
2006
+ store: personalization,
2007
+ trustedHosts: hostContext.webRuntime.trustedHosts
2008
+ });
2009
+ } catch (error) {
2010
+ disposePersonalization?.();
2011
+ disposeUpdate?.();
2012
+ throw error;
2013
+ }
2014
+ return () => {
2015
+ disposePersonalization();
2016
+ disposeUpdate();
2017
+ };
2018
+ }, "dsh-skins: self-update routes");
2019
+ });
2020
+ }
2021
+ export {
2022
+ apply,
2023
+ createSelfUpdater,
2024
+ name
2025
+ };