beauticode-dsh 1.0.12 → 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/ui-host.mjs +144 -79
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beauticode-dsh",
3
- "version": "1.0.12",
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
@@ -15,6 +15,7 @@ const MAX_IMAGE_BYTES = 18 * 1024 * 1024;
15
15
  const MAX_VIDEO_BYTES = 800 * 1024 * 1024;
16
16
  const SELECTION_TTL_MS = 5 * 60 * 1000;
17
17
  const MAX_PENDING_SELECTIONS = 16;
18
+ const PICKER_TIMEOUT_MS = 5 * 60 * 1000;
18
19
  const NATIVE_PICKER_UNAVAILABLE = "native_picker_unavailable";
19
20
  const NATIVE_PICKER_REQUIRED = "native_picker_required";
20
21
  const PICKER_REQUEST_ABORTED = "picker_request_aborted";
@@ -107,103 +108,150 @@ function suggestedThemeName(fileName, fallback) {
107
108
  return stem || fallback;
108
109
  }
109
110
 
110
- function pickWindowsMedia(kind, { signal } = {}) {
111
- if (process.platform !== "win32") {
112
- throw pickerUnavailable("当前系统不支持 Windows 原生文件选择器。");
113
- }
111
+ export function buildWindowsPickerScript(
112
+ kind,
113
+ { parentPid = process.pid, timeoutMs = PICKER_TIMEOUT_MS } = {},
114
+ ) {
114
115
  const filter =
115
116
  kind === "video"
116
117
  ? "MP4 Video (*.mp4)|*.mp4"
117
118
  : "Image Files (*.jpg;*.jpeg;*.png;*.webp;*.avif)|*.jpg;*.jpeg;*.png;*.webp;*.avif";
118
- const script = [
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 [
119
126
  "$ErrorActionPreference = 'Stop'",
120
127
  "$OutputEncoding = New-Object System.Text.UTF8Encoding($false)",
121
128
  "[Console]::OutputEncoding = $OutputEncoding",
122
129
  "Add-Type -AssemblyName System.Windows.Forms",
130
+ "Add-Type -AssemblyName System.Drawing",
123
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",
124
140
  "$dialog = New-Object System.Windows.Forms.OpenFileDialog",
125
141
  `$dialog.Filter = '${filter}'`,
126
142
  "$dialog.Multiselect = $false",
127
143
  "$dialog.CheckFileExists = $true",
144
+ "$dialog.RestoreDirectory = $true",
128
145
  "$dialog.Title = '选择 beautiCode 背景文件'",
129
- "$result = $dialog.ShowDialog()",
130
- "if ($result -eq [System.Windows.Forms.DialogResult]::OK) { [Console]::WriteLine($dialog.FileName) }",
131
- "$dialog.Dispose()",
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() }",
132
152
  ].join("; ");
133
- return new Promise((resolve, reject) => {
134
- const child = spawn(
135
- "powershell.exe",
136
- [
137
- "-NoLogo",
138
- "-NoProfile",
139
- "-ExecutionPolicy",
140
- "Bypass",
141
- "-STA",
142
- "-Command",
143
- script,
144
- ],
145
- {
146
- // Hide PowerShell's console while leaving the WinForms dialog visible.
147
- windowsHide: true,
148
- stdio: ["ignore", "pipe", "pipe"],
149
- },
150
- );
151
- let stdout = "";
152
- let stderr = "";
153
- let aborted = false;
154
- const onAbort = () => {
155
- aborted = true;
156
- clearTimeout(timer);
157
- child.kill();
158
- const error = new Error("文件选择请求已取消。");
159
- error.code = PICKER_REQUEST_ABORTED;
160
- reject(error);
161
- };
162
- const timer = setTimeout(() => {
163
- signal?.removeEventListener("abort", onAbort);
164
- child.kill();
165
- reject(new Error("文件选择器超时。"));
166
- }, 5 * 60 * 1000);
167
- if (signal?.aborted) onAbort();
168
- else signal?.addEventListener("abort", onAbort, { once: true });
169
- child.stdout.setEncoding("utf8");
170
- child.stderr.setEncoding("utf8");
171
- child.stdout.on("data", (chunk) => { stdout += chunk; });
172
- child.stderr.on("data", (chunk) => { stderr += chunk; });
173
- child.once("error", (error) => {
174
- clearTimeout(timer);
175
- signal?.removeEventListener("abort", onAbort);
176
- if (aborted) return;
177
- reject(
178
- error?.code === "ENOENT"
179
- ? pickerUnavailable("找不到 Windows PowerShell,无法打开原生文件选择器。")
180
- : error,
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
+ },
181
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
+ });
182
250
  });
183
- child.once("close", (code) => {
184
- clearTimeout(timer);
185
- signal?.removeEventListener("abort", onAbort);
186
- if (aborted) return;
187
- if (code !== 0) {
188
- const message = stderr.trim() || `文件选择器退出异常(${code})。`;
189
- reject(isPickerDependencyError(message) ? pickerUnavailable(message) : new Error(message));
190
- return;
191
- }
192
- const selected = stdout.trim();
193
- if (!selected) {
194
- resolve({ ok: true, cancelled: true });
195
- return;
196
- }
197
- const name = path.basename(selected.replaceAll("\\", "/"));
198
- const parsed = parseImportFilename(name);
199
- if (!parsed.ok || parsed.kind !== kind) {
200
- reject(new Error(parsed.ok ? "选择的文件类型不匹配。" : parsed.error));
201
- return;
202
- }
203
- resolve({ ok: true, kind, path: path.resolve(selected), name });
204
- });
205
- });
251
+ };
206
252
  }
253
+
254
+ const pickWindowsMedia = createWindowsMediaPicker();
207
255
 
208
256
  function limitBytes(maxBytes) {
209
257
  let size = 0;
@@ -277,6 +325,9 @@ export function createBeauticodeUi({
277
325
  const pendingSelections = new Map();
278
326
  let restoreStarted = false;
279
327
  let pickerBusy = false;
328
+ let activePickerAbort = null;
329
+ let activePickerResponse = null;
330
+ let disposed = false;
280
331
 
281
332
  function pruneSelections() {
282
333
  const timestamp = now();
@@ -336,6 +387,12 @@ export function createBeauticodeUi({
336
387
 
337
388
  return {
338
389
  dispose() {
390
+ disposed = true;
391
+ activePickerAbort?.abort();
392
+ activePickerResponse?.destroy();
393
+ activePickerAbort = null;
394
+ activePickerResponse = null;
395
+ pickerBusy = false;
339
396
  pendingSelections.clear();
340
397
  return stopInProcessSession(dataRoot);
341
398
  },
@@ -485,9 +542,15 @@ export function createBeauticodeUi({
485
542
  sendJson(res, 409, { ok: false, error: "文件选择器已经打开。" });
486
543
  return;
487
544
  }
545
+ if (disposed) {
546
+ sendJson(res, 503, { ok: false, error: "beautiCode UI 已停止。" });
547
+ return;
548
+ }
488
549
  pickerBusy = true;
489
550
  const pickerStartedAt = performance.now();
490
551
  const pickerAbort = new AbortController();
552
+ activePickerAbort = pickerAbort;
553
+ activePickerResponse = res;
491
554
  const onResponseClose = () => {
492
555
  if (!res.writableEnded) pickerAbort.abort();
493
556
  };
@@ -517,6 +580,8 @@ export function createBeauticodeUi({
517
580
  });
518
581
  } finally {
519
582
  res.off("close", onResponseClose);
583
+ if (activePickerAbort === pickerAbort) activePickerAbort = null;
584
+ if (activePickerResponse === res) activePickerResponse = null;
520
585
  pickerBusy = false;
521
586
  }
522
587
  },