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/README.zh-CN.md +2 -2
- package/agent.mjs +69 -52
- package/client.js +102 -22
- package/console.js +166 -46
- package/control-client.mjs +6 -4
- package/index.mjs +22 -8
- package/package.json +1 -1
- package/themes/internal-beyond/bg-canvas.png +0 -0
- package/ui-host.mjs +459 -31
- package/vendor/adapter-dsh/bridge.js +2 -4
- package/vendor/adapter-dsh/session.js +61 -27
- package/vendor/adapter-dsh/tray-handoff.js +13 -11
- package/vendor/core/apply-transaction.js +87 -45
- package/vendor/core/background-store.js +211 -351
- package/vendor/core/error-message.js +0 -8
- package/vendor/core/file-lock.js +13 -34
- package/vendor/core/index.js +1 -2
- package/vendor/core/media-server.js +22 -40
- package/vendor/core/media-source.js +36 -0
- package/vendor/core/media-validation.js +26 -19
- package/vendor/core/paths.js +0 -1
- package/vendor/core/process-liveness.js +0 -205
- package/vendor/core/skin-pack.js +0 -64
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
|
Binary file
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
173
|
-
|
|
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
|
-
|
|
227
|
-
|
|
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:
|
|
125
|
+
reason: last.lastRenderError ||
|
|
126
|
+
"DeepSeek Harness client failed to render the background.",
|
|
129
127
|
details: { ...last },
|
|
130
128
|
};
|
|
131
129
|
}
|