beauticode-dsh 1.0.7 → 1.0.13

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/index.mjs CHANGED
@@ -199,9 +199,13 @@ export function apply(ctx, config = {}) {
199
199
  const ui = createBeauticodeUi({
200
200
  dataRoot,
201
201
  getBaseUrl: () => resolvePluginBaseUrl(ctx),
202
- sendJson,
203
- isSameOrigin,
204
- readJson,
202
+ sendJson,
203
+ isSameOrigin,
204
+ readJson,
205
+ pickMedia: config.pickMedia,
206
+ allowManagedUpload: config.allowManagedUpload,
207
+ now: config.now,
208
+ selectionTtlMs: config.selectionTtlMs,
205
209
  });
206
210
 
207
211
  const broadcast = (payload) => {
@@ -356,11 +360,21 @@ export function apply(ctx, config = {}) {
356
360
  path: "/__beauticode/ui/status",
357
361
  handler: (req, res) => ui.status(req, res),
358
362
  }),
359
- ctx.webServer.register({
360
- kind: "exact",
361
- path: "/__beauticode/ui/import",
362
- handler: (req, res) => ui.importFile(req, res),
363
- }),
363
+ ctx.webServer.register({
364
+ kind: "exact",
365
+ path: "/__beauticode/ui/import",
366
+ handler: (req, res) => ui.importFile(req, res),
367
+ }),
368
+ ctx.webServer.register({
369
+ kind: "exact",
370
+ path: "/__beauticode/ui/pick",
371
+ handler: (req, res) => ui.pickMedia(req, res),
372
+ }),
373
+ ctx.webServer.register({
374
+ kind: "exact",
375
+ path: "/__beauticode/ui/import-selected",
376
+ handler: (req, res) => ui.importSelected(req, res),
377
+ }),
364
378
  ctx.webServer.register({
365
379
  kind: "exact",
366
380
  path: "/__beauticode/ui/clear",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beauticode-dsh",
3
- "version": "1.0.7",
3
+ "version": "1.0.13",
4
4
  "description": "Cordis plugin: image/video backgrounds for DeepSeek Harness web.",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",
package/ui-host.mjs CHANGED
@@ -1,7 +1,8 @@
1
- import fs from "node:fs";
2
- import fsp from "node:fs/promises";
3
- import path from "node:path";
4
- import crypto from "node:crypto";
1
+ import fs from "node:fs";
2
+ import fsp from "node:fs/promises";
3
+ import path from "node:path";
4
+ import crypto from "node:crypto";
5
+ import { spawn } from "node:child_process";
5
6
  import { pipeline } from "node:stream/promises";
6
7
  import { Transform } from "node:stream";
7
8
  import { createBeauticodeActions } from "./agent.mjs";
@@ -10,8 +11,33 @@ import { createGalleryHandlers, resolveConfiguredSkinCenterUrl } from "./gallery
10
11
  import { hasLiveTray, resolveApplyBackend, stopInProcessSession } from "./host-apply.mjs";
11
12
 
12
13
  const IMAGE_EXTENSIONS = new Set([".jpg", ".jpeg", ".png", ".webp", ".avif"]);
13
- const MAX_IMAGE_BYTES = 18 * 1024 * 1024;
14
- const MAX_VIDEO_BYTES = 800 * 1024 * 1024;
14
+ const MAX_IMAGE_BYTES = 18 * 1024 * 1024;
15
+ const MAX_VIDEO_BYTES = 800 * 1024 * 1024;
16
+ const SELECTION_TTL_MS = 5 * 60 * 1000;
17
+ const MAX_PENDING_SELECTIONS = 16;
18
+ const PICKER_TIMEOUT_MS = 5 * 60 * 1000;
19
+ const NATIVE_PICKER_UNAVAILABLE = "native_picker_unavailable";
20
+ const NATIVE_PICKER_REQUIRED = "native_picker_required";
21
+ const PICKER_REQUEST_ABORTED = "picker_request_aborted";
22
+ const IMPORT_TIMING_LOG = "import-timing.jsonl";
23
+
24
+ function elapsedMs(startedAt) {
25
+ return Math.round((performance.now() - startedAt) * 10) / 10;
26
+ }
27
+
28
+ async function appendImportTiming(dataRoot, entry) {
29
+ try {
30
+ const logsDir = path.join(dataRoot, "logs");
31
+ await fsp.mkdir(logsDir, { recursive: true });
32
+ await fsp.appendFile(
33
+ path.join(logsDir, IMPORT_TIMING_LOG),
34
+ `${JSON.stringify({ at: new Date().toISOString(), ...entry })}\n`,
35
+ "utf8",
36
+ );
37
+ } catch {
38
+ /* Diagnostics must never alter import success/failure semantics. */
39
+ }
40
+ }
15
41
 
16
42
  export function parseImportFilename(raw) {
17
43
  if (typeof raw !== "string" || !raw.trim()) {
@@ -58,6 +84,174 @@ export function parseImportThemeName(raw) {
58
84
  }
59
85
  return { ok: true, name };
60
86
  }
87
+
88
+ function pickerUnavailable(message) {
89
+ const error = new Error(message);
90
+ error.code = NATIVE_PICKER_UNAVAILABLE;
91
+ return error;
92
+ }
93
+
94
+ function isPickerDependencyError(message) {
95
+ return /powershell|system\.windows\.forms|assembly|无法加载|找不到|not recognized/i.test(
96
+ String(message || ""),
97
+ );
98
+ }
99
+
100
+ function suggestedThemeName(fileName, fallback) {
101
+ const ext = path.extname(fileName);
102
+ const stem = (ext ? fileName.slice(0, -ext.length) : fileName)
103
+ .replace(/[<>:"/\\|?*]/g, " ")
104
+ .replace(/[\u0000-\u001f]/g, "")
105
+ .replace(/\s+/g, " ")
106
+ .trim()
107
+ .slice(0, 80);
108
+ return stem || fallback;
109
+ }
110
+
111
+ export function buildWindowsPickerScript(
112
+ kind,
113
+ { parentPid = process.pid, timeoutMs = PICKER_TIMEOUT_MS } = {},
114
+ ) {
115
+ const filter =
116
+ kind === "video"
117
+ ? "MP4 Video (*.mp4)|*.mp4"
118
+ : "Image Files (*.jpg;*.jpeg;*.png;*.webp;*.avif)|*.jpg;*.jpeg;*.png;*.webp;*.avif";
119
+ const safeParentPid = Number.isSafeInteger(parentPid) && parentPid > 0
120
+ ? parentPid
121
+ : process.pid;
122
+ const safeTimeoutMs = Number.isFinite(timeoutMs) && timeoutMs > 0
123
+ ? Math.ceil(timeoutMs)
124
+ : PICKER_TIMEOUT_MS;
125
+ return [
126
+ "$ErrorActionPreference = 'Stop'",
127
+ "$OutputEncoding = New-Object System.Text.UTF8Encoding($false)",
128
+ "[Console]::OutputEncoding = $OutputEncoding",
129
+ "Add-Type -AssemblyName System.Windows.Forms",
130
+ "Add-Type -AssemblyName System.Drawing",
131
+ "[System.Windows.Forms.Application]::EnableVisualStyles()",
132
+ "$owner = New-Object System.Windows.Forms.Form",
133
+ "$owner.Text = 'beautiCode 文件选择器'",
134
+ "$owner.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedToolWindow",
135
+ "$owner.ShowInTaskbar = $false",
136
+ "$owner.StartPosition = [System.Windows.Forms.FormStartPosition]::CenterScreen",
137
+ "$owner.Size = [System.Drawing.Size]::new(1, 1)",
138
+ "$owner.Opacity = 0.01",
139
+ "$owner.TopMost = $true",
140
+ "$dialog = New-Object System.Windows.Forms.OpenFileDialog",
141
+ `$dialog.Filter = '${filter}'`,
142
+ "$dialog.Multiselect = $false",
143
+ "$dialog.CheckFileExists = $true",
144
+ "$dialog.RestoreDirectory = $true",
145
+ "$dialog.Title = '选择 beautiCode 背景文件'",
146
+ `$dshPid = ${safeParentPid}`,
147
+ `$deadlineUtc = [DateTime]::UtcNow.AddMilliseconds(${safeTimeoutMs})`,
148
+ "$watchdog = New-Object System.Windows.Forms.Timer",
149
+ "$watchdog.Interval = 500",
150
+ "$watchdog.Add_Tick({ if ([DateTime]::UtcNow -ge $deadlineUtc -or -not (Get-Process -Id $dshPid -ErrorAction SilentlyContinue)) { $watchdog.Stop(); [Environment]::Exit(0) } })",
151
+ "try { [void]$owner.Show(); [void]$owner.Hide(); [void]$owner.Show(); [void]$owner.Activate(); [void]$owner.BringToFront(); [System.Windows.Forms.Application]::DoEvents(); if (-not $owner.IsHandleCreated) { throw 'Picker owner window was not created.' }; $watchdog.Start(); $result = $dialog.ShowDialog($owner); if ($result -eq [System.Windows.Forms.DialogResult]::OK) { [Console]::WriteLine($dialog.FileName) } } finally { $watchdog.Stop(); $watchdog.Dispose(); $dialog.Dispose(); $owner.Close(); $owner.Dispose() }",
152
+ ].join("; ");
153
+ }
154
+
155
+ export function createWindowsMediaPicker({
156
+ platform = process.platform,
157
+ spawnProcess = spawn,
158
+ parentProcess = process,
159
+ parentPid = process.pid,
160
+ timeoutMs = PICKER_TIMEOUT_MS,
161
+ } = {}) {
162
+ return function pickWindowsMedia(kind, { signal } = {}) {
163
+ if (platform !== "win32") {
164
+ throw pickerUnavailable("当前系统不支持 Windows 原生文件选择器。");
165
+ }
166
+ const script = buildWindowsPickerScript(kind, { parentPid, timeoutMs });
167
+ const encodedScript = Buffer.from(script, "utf16le").toString("base64");
168
+ return new Promise((resolve, reject) => {
169
+ const child = spawnProcess(
170
+ "powershell.exe",
171
+ [
172
+ "-NoLogo",
173
+ "-NoProfile",
174
+ "-ExecutionPolicy",
175
+ "Bypass",
176
+ "-STA",
177
+ "-EncodedCommand",
178
+ encodedScript,
179
+ ],
180
+ {
181
+ // Hide PowerShell's console while leaving the owned WinForms dialog visible.
182
+ windowsHide: true,
183
+ stdio: ["ignore", "pipe", "pipe"],
184
+ },
185
+ );
186
+ let stdout = "";
187
+ let stderr = "";
188
+ let settled = false;
189
+ const cleanup = () => {
190
+ clearTimeout(timer);
191
+ signal?.removeEventListener("abort", onAbort);
192
+ parentProcess.removeListener("exit", onParentExit);
193
+ };
194
+ const settle = (callback, value, { terminate = false } = {}) => {
195
+ if (settled) return;
196
+ settled = true;
197
+ cleanup();
198
+ if (terminate && !child.killed) child.kill("SIGKILL");
199
+ callback(value);
200
+ };
201
+ const onAbort = () => {
202
+ const error = new Error("文件选择请求已取消。");
203
+ error.code = PICKER_REQUEST_ABORTED;
204
+ settle(reject, error, { terminate: true });
205
+ };
206
+ const timer = setTimeout(() => {
207
+ settle(reject, new Error("文件选择器超时。"), { terminate: true });
208
+ }, timeoutMs);
209
+ const onParentExit = () => {
210
+ if (!child.killed) child.kill("SIGKILL");
211
+ };
212
+ parentProcess.once("exit", onParentExit);
213
+ if (signal?.aborted) onAbort();
214
+ else signal?.addEventListener("abort", onAbort, { once: true });
215
+ child.stdout.setEncoding("utf8");
216
+ child.stderr.setEncoding("utf8");
217
+ child.stdout.on("data", (chunk) => { stdout += chunk; });
218
+ child.stderr.on("data", (chunk) => { stderr += chunk; });
219
+ child.once("error", (error) => {
220
+ settle(
221
+ reject,
222
+ error?.code === "ENOENT"
223
+ ? pickerUnavailable("找不到 Windows PowerShell,无法打开原生文件选择器。")
224
+ : error,
225
+ );
226
+ });
227
+ child.once("close", (code) => {
228
+ if (settled) return;
229
+ if (code !== 0) {
230
+ const message = stderr.trim() || `文件选择器退出异常(${code})。`;
231
+ settle(
232
+ reject,
233
+ isPickerDependencyError(message) ? pickerUnavailable(message) : new Error(message),
234
+ );
235
+ return;
236
+ }
237
+ const selected = stdout.trim();
238
+ if (!selected) {
239
+ settle(resolve, { ok: true, cancelled: true });
240
+ return;
241
+ }
242
+ const name = path.basename(selected.replaceAll("\\", "/"));
243
+ const parsed = parseImportFilename(name);
244
+ if (!parsed.ok || parsed.kind !== kind) {
245
+ settle(reject, new Error(parsed.ok ? "选择的文件类型不匹配。" : parsed.error));
246
+ return;
247
+ }
248
+ settle(resolve, { ok: true, kind, path: path.resolve(selected), name });
249
+ });
250
+ });
251
+ };
252
+ }
253
+
254
+ const pickWindowsMedia = createWindowsMediaPicker();
61
255
 
62
256
  function limitBytes(maxBytes) {
63
257
  let size = 0;
@@ -75,13 +269,13 @@ function limitBytes(maxBytes) {
75
269
  });
76
270
  }
77
271
 
78
- function publicThemes(list) {
272
+ function publicThemes(list) {
79
273
  return (Array.isArray(list) ? list : []).map((theme) => ({
80
274
  id: theme.id,
81
275
  name: theme.name,
82
276
  type: theme.type ?? null,
83
277
  ...(theme.bundled ? { bundled: true } : {}),
84
- ...(theme.source ? { source: theme.source } : {}),
278
+ sourceMode: theme.sourceMode === "local" ? "local" : "managed",
85
279
  }));
86
280
  }
87
281
 
@@ -101,7 +295,18 @@ async function canReachBridge(baseUrl) {
101
295
  }
102
296
  }
103
297
 
104
- export function createBeauticodeUi({ dataRoot, baseUrl, getBaseUrl, sendJson, isSameOrigin, readJson }) {
298
+ export function createBeauticodeUi({
299
+ dataRoot,
300
+ baseUrl,
301
+ getBaseUrl,
302
+ sendJson,
303
+ isSameOrigin,
304
+ readJson,
305
+ pickMedia: injectedPicker,
306
+ allowManagedUpload: injectedAllowManagedUpload,
307
+ now: injectedNow,
308
+ selectionTtlMs = SELECTION_TTL_MS,
309
+ }) {
105
310
  const options = {
106
311
  dataRoot,
107
312
  get baseUrl() {
@@ -109,9 +314,54 @@ export function createBeauticodeUi({ dataRoot, baseUrl, getBaseUrl, sendJson, is
109
314
  return baseUrl;
110
315
  },
111
316
  };
112
- const actions = createBeauticodeActions(options);
113
- const gallery = createGalleryHandlers({ dataRoot, actions });
114
- let restoreStarted = false;
317
+ const actions = createBeauticodeActions(options);
318
+ const gallery = createGalleryHandlers({ dataRoot, actions });
319
+ const nativePicker = injectedPicker ?? pickWindowsMedia;
320
+ const allowManagedUpload =
321
+ typeof injectedAllowManagedUpload === "boolean"
322
+ ? injectedAllowManagedUpload
323
+ : process.platform !== "win32";
324
+ const now = typeof injectedNow === "function" ? injectedNow : Date.now;
325
+ const pendingSelections = new Map();
326
+ let restoreStarted = false;
327
+ let pickerBusy = false;
328
+ let activePickerAbort = null;
329
+ let activePickerResponse = null;
330
+ let disposed = false;
331
+
332
+ function pruneSelections() {
333
+ const timestamp = now();
334
+ for (const [id, selection] of pendingSelections) {
335
+ if (selection.expiresAt <= timestamp) pendingSelections.delete(id);
336
+ }
337
+ while (pendingSelections.size >= MAX_PENDING_SELECTIONS) {
338
+ const oldest = pendingSelections.keys().next().value;
339
+ if (!oldest) break;
340
+ pendingSelections.delete(oldest);
341
+ }
342
+ }
343
+
344
+ function rememberSelection(picked, pickerMs) {
345
+ pruneSelections();
346
+ const selectionId = crypto.randomUUID();
347
+ pendingSelections.set(selectionId, {
348
+ kind: picked.kind,
349
+ path: picked.path,
350
+ name: picked.name,
351
+ pickerMs,
352
+ expiresAt: now() + selectionTtlMs,
353
+ });
354
+ return {
355
+ ok: true,
356
+ cancelled: false,
357
+ selectionId,
358
+ name: picked.name,
359
+ suggestedThemeName: suggestedThemeName(
360
+ picked.name,
361
+ picked.kind === "video" ? "视频" : "图片",
362
+ ),
363
+ };
364
+ }
115
365
 
116
366
  async function restoreOnce() {
117
367
  if (await hasLiveTray(dataRoot)) {
@@ -136,8 +386,15 @@ export function createBeauticodeUi({ dataRoot, baseUrl, getBaseUrl, sendJson, is
136
386
  }
137
387
 
138
388
  return {
139
- dispose() {
140
- return stopInProcessSession(dataRoot);
389
+ dispose() {
390
+ disposed = true;
391
+ activePickerAbort?.abort();
392
+ activePickerResponse?.destroy();
393
+ activePickerAbort = null;
394
+ activePickerResponse = null;
395
+ pickerBusy = false;
396
+ pendingSelections.clear();
397
+ return stopInProcessSession(dataRoot);
141
398
  },
142
399
  scheduleRestore() {
143
400
  if (restoreStarted) return;
@@ -167,10 +424,15 @@ export function createBeauticodeUi({ dataRoot, baseUrl, getBaseUrl, sendJson, is
167
424
  ok: true,
168
425
  media: background?.type ?? null,
169
426
  muted: status.muted !== false,
170
- atmosphere: background?.effects?.preset ?? null,
171
- themeId: status.themeId || null,
172
- themes: publicThemes(listed.themes),
173
- message: status.message,
427
+ atmosphere: background?.effects?.preset ?? null,
428
+ themeId: status.themeId || null,
429
+ sourceMode: status.sourceMode ?? "clear",
430
+ themes: publicThemes(listed.themes),
431
+ message: status.message,
432
+ importPolicy: {
433
+ nativeLocalRequired: !allowManagedUpload,
434
+ managedUploadAllowed: allowManagedUpload,
435
+ },
174
436
  skinCenter: { url: center, enabled: Boolean(center) },
175
437
  });
176
438
  } catch (error) {
@@ -181,15 +443,25 @@ export function createBeauticodeUi({ dataRoot, baseUrl, getBaseUrl, sendJson, is
181
443
  }
182
444
  },
183
445
 
184
- async importFile(req, res) {
446
+ async importFile(req, res) {
185
447
  if (req.method !== "POST") {
186
448
  res.writeHead(405).end();
187
449
  return;
188
450
  }
189
- if (!isSameOrigin(req)) {
190
- res.writeHead(403).end();
191
- return;
192
- }
451
+ if (!isSameOrigin(req)) {
452
+ res.writeHead(403).end();
453
+ return;
454
+ }
455
+ if (!allowManagedUpload) {
456
+ req.resume();
457
+ sendJson(res, 409, {
458
+ ok: false,
459
+ code: "local_import_required",
460
+ error: "Windows 本地导入必须使用原生文件选择器;不会自动上传或复制媒体文件。",
461
+ });
462
+ return;
463
+ }
464
+ const importStartedAt = performance.now();
193
465
  const parsed = parseImportFilename(req.headers["x-beauticode-filename"]);
194
466
  if (!parsed.ok) {
195
467
  req.resume();
@@ -219,12 +491,29 @@ export function createBeauticodeUi({ dataRoot, baseUrl, getBaseUrl, sendJson, is
219
491
  try {
220
492
  await pipeline(req, limitBytes(parsed.maxBytes), fs.createWriteStream(tmpPath));
221
493
  const themeName = parsedTheme.name;
222
- const result =
223
- parsed.kind === "video"
224
- ? await actions.applyVideo({ path: tmpPath, themeName })
225
- : await actions.applyImage(tmpPath, undefined, { themeName });
226
- sendJson(res, 200, result);
227
- } catch (error) {
494
+ const result =
495
+ parsed.kind === "video"
496
+ ? await actions.applyVideo({ path: tmpPath, themeName, source: "managed" })
497
+ : await actions.applyImage(tmpPath, undefined, { themeName, source: "managed" });
498
+ await appendImportTiming(dataRoot, {
499
+ route: "managed-upload",
500
+ kind: parsed.kind,
501
+ ok: true,
502
+ sourceMode: result.sourceMode ?? "managed",
503
+ uploadAndApplyMs: elapsedMs(importStartedAt),
504
+ core: result.timings ?? null,
505
+ });
506
+ sendJson(res, 200, result);
507
+ } catch (error) {
508
+ await appendImportTiming(dataRoot, {
509
+ route: "managed-upload",
510
+ kind: parsed.kind,
511
+ ok: false,
512
+ sourceMode: error?.sourceMode ?? "managed",
513
+ uploadAndApplyMs: elapsedMs(importStartedAt),
514
+ core: error?.timings ?? null,
515
+ error: error instanceof Error ? error.message : String(error),
516
+ });
228
517
  const status = error?.statusCode === 413 ? 413 : 422;
229
518
  sendJson(res, status, {
230
519
  ok: false,
@@ -232,8 +521,147 @@ export function createBeauticodeUi({ dataRoot, baseUrl, getBaseUrl, sendJson, is
232
521
  });
233
522
  } finally {
234
523
  await fsp.rm(tmpPath, { force: true }).catch(() => {});
235
- }
236
- },
524
+ }
525
+ },
526
+
527
+ async pickMedia(req, res) {
528
+ if (req.method !== "POST") {
529
+ res.writeHead(405).end();
530
+ return;
531
+ }
532
+ if (!isSameOrigin(req)) {
533
+ res.writeHead(403).end();
534
+ return;
535
+ }
536
+ const body = await readJson(req);
537
+ if (body.kind !== "image" && body.kind !== "video") {
538
+ sendJson(res, 400, { ok: false, error: "kind 必须是 image 或 video。" });
539
+ return;
540
+ }
541
+ if (pickerBusy) {
542
+ sendJson(res, 409, { ok: false, error: "文件选择器已经打开。" });
543
+ return;
544
+ }
545
+ if (disposed) {
546
+ sendJson(res, 503, { ok: false, error: "beautiCode UI 已停止。" });
547
+ return;
548
+ }
549
+ pickerBusy = true;
550
+ const pickerStartedAt = performance.now();
551
+ const pickerAbort = new AbortController();
552
+ activePickerAbort = pickerAbort;
553
+ activePickerResponse = res;
554
+ const onResponseClose = () => {
555
+ if (!res.writableEnded) pickerAbort.abort();
556
+ };
557
+ res.once("close", onResponseClose);
558
+ try {
559
+ const picked = await nativePicker(body.kind, { signal: pickerAbort.signal });
560
+ if (pickerAbort.signal.aborted) return;
561
+ sendJson(
562
+ res,
563
+ 200,
564
+ picked?.cancelled
565
+ ? { ok: true, cancelled: true, pickerMs: elapsedMs(pickerStartedAt) }
566
+ : rememberSelection(picked, elapsedMs(pickerStartedAt)),
567
+ );
568
+ } catch (error) {
569
+ if (pickerAbort.signal.aborted || error?.code === PICKER_REQUEST_ABORTED) return;
570
+ const unavailable = error?.code === NATIVE_PICKER_UNAVAILABLE;
571
+ const code = unavailable
572
+ ? allowManagedUpload
573
+ ? NATIVE_PICKER_UNAVAILABLE
574
+ : NATIVE_PICKER_REQUIRED
575
+ : undefined;
576
+ sendJson(res, unavailable ? 501 : 422, {
577
+ ok: false,
578
+ ...(code ? { code } : {}),
579
+ error: error instanceof Error ? error.message : String(error),
580
+ });
581
+ } finally {
582
+ res.off("close", onResponseClose);
583
+ if (activePickerAbort === pickerAbort) activePickerAbort = null;
584
+ if (activePickerResponse === res) activePickerResponse = null;
585
+ pickerBusy = false;
586
+ }
587
+ },
588
+
589
+ async importSelected(req, res) {
590
+ if (req.method !== "POST") {
591
+ res.writeHead(405).end();
592
+ return;
593
+ }
594
+ if (!isSameOrigin(req)) {
595
+ res.writeHead(403).end();
596
+ return;
597
+ }
598
+ const body = await readJson(req);
599
+ const parsedTheme = parseImportThemeName(body.themeName);
600
+ const selectionId =
601
+ typeof body.selectionId === "string" ? body.selectionId.trim() : "";
602
+ if (!selectionId || !parsedTheme.ok) {
603
+ sendJson(res, 400, {
604
+ ok: false,
605
+ error: !parsedTheme.ok ? parsedTheme.error : "选择令牌无效。",
606
+ });
607
+ return;
608
+ }
609
+ pruneSelections();
610
+ const selected = pendingSelections.get(selectionId);
611
+ if (!selected) {
612
+ sendJson(res, 410, { ok: false, error: "文件选择已失效,请重新选择。" });
613
+ return;
614
+ }
615
+ pendingSelections.delete(selectionId);
616
+ const importStartedAt = performance.now();
617
+ try {
618
+ const result =
619
+ selected.kind === "video"
620
+ ? await actions.applyVideo({
621
+ path: selected.path,
622
+ themeName: parsedTheme.name,
623
+ source: "local",
624
+ })
625
+ : await actions.applyImage(selected.path, undefined, {
626
+ themeName: parsedTheme.name,
627
+ source: "local",
628
+ });
629
+ if (result.sourceMode !== "local") {
630
+ throw new Error("本地导入合同失败:后端没有保留 local 来源,操作已拒绝。");
631
+ }
632
+ const importTimings = {
633
+ pickerMs: selected.pickerMs ?? null,
634
+ applyAndSaveMs: elapsedMs(importStartedAt),
635
+ core: result.timings ?? null,
636
+ };
637
+ await appendImportTiming(dataRoot, {
638
+ route: "native-local",
639
+ kind: selected.kind,
640
+ ok: true,
641
+ sourceMode: "local",
642
+ ...importTimings,
643
+ });
644
+ result.importTimings = importTimings;
645
+ sendJson(res, 200, result);
646
+ } catch (error) {
647
+ await appendImportTiming(dataRoot, {
648
+ route: "native-local",
649
+ kind: selected.kind,
650
+ ok: false,
651
+ sourceMode: error?.sourceMode ?? "local",
652
+ pickerMs: selected.pickerMs ?? null,
653
+ applyAndSaveMs: elapsedMs(importStartedAt),
654
+ core: error?.timings ?? null,
655
+ error: error instanceof Error ? error.message : String(error),
656
+ });
657
+ sendJson(res, 422, {
658
+ ok: false,
659
+ sourceMode: error?.sourceMode ?? "local",
660
+ timings: error?.timings ?? null,
661
+ error: error instanceof Error ? error.message : String(error),
662
+ });
663
+ }
664
+ },
237
665
 
238
666
  async clear(req, res) {
239
667
  if (req.method !== "POST") {
@@ -120,12 +120,10 @@ export class DshHostApplier {
120
120
  };
121
121
  }
122
122
  if (currentMatches && last.failedClients > 0 && last.readyClients === 0) {
123
- const detail = typeof last.lastRenderError === "string" && last.lastRenderError
124
- ? ` (${last.lastRenderError})`
125
- : "";
126
123
  return {
127
124
  status: "fail",
128
- reason: `DeepSeek Harness client failed to render the background.${detail}`,
125
+ reason: last.lastRenderError ||
126
+ "DeepSeek Harness client failed to render the background.",
129
127
  details: { ...last },
130
128
  };
131
129
  }