beauticode-dsh 1.0.5 → 1.0.7

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/cli.js CHANGED
@@ -46,9 +46,17 @@ function defaultDataRoot() {
46
46
  return path.join(os.homedir(), ".beauticode");
47
47
  }
48
48
 
49
- function defaultDshHome() {
50
- return process.env.DSH_HOME || path.join(os.homedir(), ".dsh");
51
- }
49
+ function defaultDshHome() {
50
+ return process.env.DSH_HOME || path.join(os.homedir(), ".dsh");
51
+ }
52
+
53
+ function defaultPluginHome(dshHome) {
54
+ return path.join(path.resolve(dshHome), "plugins", pluginName);
55
+ }
56
+
57
+ function legacyDefaultPluginHome() {
58
+ return path.join(defaultDataRoot(), "plugin");
59
+ }
52
60
 
53
61
  function toFileUri(filePath) {
54
62
  const full = path.resolve(filePath).replaceAll("\\", "/");
@@ -222,15 +230,34 @@ async function sameInstalledVersion(dest) {
222
230
  }
223
231
  }
224
232
 
225
- async function copyPackage(dest) {
233
+ async function copyPackage(dest) {
226
234
  if (await sameInstalledVersion(dest)) return false;
227
235
  await fsp.rm(dest, { recursive: true, force: true });
228
236
  await fsp.cp(here, dest, {
229
237
  recursive: true,
230
238
  filter: (source) => shouldCopy(source),
231
239
  });
232
- return true;
233
- }
240
+ return true;
241
+ }
242
+
243
+ async function removeLegacyManagedPlugin(legacyHome, currentHome) {
244
+ if (!legacyHome) return false;
245
+ const legacy = path.resolve(legacyHome);
246
+ if (legacy === path.resolve(currentHome) || !fs.existsSync(legacy)) return false;
247
+ try {
248
+ const pkg = JSON.parse(await fsp.readFile(path.join(legacy, "package.json"), "utf8"));
249
+ if (
250
+ ![pluginName, "@beauticode/dsh-plugin"].includes(pkg.name) ||
251
+ !fs.existsSync(path.join(legacy, "index.mjs"))
252
+ ) {
253
+ return false;
254
+ }
255
+ } catch {
256
+ return false;
257
+ }
258
+ await fsp.rm(legacy, { recursive: true, force: true });
259
+ return true;
260
+ }
234
261
 
235
262
  async function ensureEngine(dest) {
236
263
  const vendor = path.join(dest, "vendor", "adapter-dsh", "index.js");
@@ -320,7 +347,7 @@ async function removeWebPackageDep(webPackage) {
320
347
  return true;
321
348
  }
322
349
 
323
- async function install(opts) {
350
+ async function install(opts) {
324
351
  const dest = path.resolve(opts.pluginHome);
325
352
  const dshHome = path.resolve(opts.dshHome);
326
353
  const webProfile = path.join(dshHome, "profiles", "web");
@@ -340,21 +367,25 @@ async function install(opts) {
340
367
  await linkPluginIntoProfile(webProfile, dest);
341
368
  await ensureWebPackageDep(webPackage, dest);
342
369
  await writePatch(webPatch, packageInsert());
343
- if (fs.existsSync(homePatch)) {
370
+ if (fs.existsSync(homePatch)) {
344
371
  const homeRaw = await fsp.readFile(homePatch, "utf8");
345
- if (hasBridge(homeRaw)) await removePatch(homePatch);
346
- }
347
- console.log(`已写入 ${webPatch}`);
348
- console.log(copied ? `插件已安装到 ${dest}` : `已复用已安装的插件 ${dest}`);
349
- console.log("请自己运行:npx @deepseek-ai/dsh web");
372
+ if (hasBridge(homeRaw)) await removePatch(homePatch);
373
+ }
374
+ const migrated = await removeLegacyManagedPlugin(opts.legacyPluginHome, dest);
375
+ console.log(`已写入 ${webPatch}`);
376
+ console.log(copied ? `插件已安装到 ${dest}` : `已复用已安装的插件 ${dest}`);
377
+ if (migrated) console.log("已迁移 1.0.5 的旧插件目录,已保留主题数据。");
378
+ console.log("请自己运行:npx @deepseek-ai/dsh web");
350
379
  return { dest, patch: webPatch };
351
380
  }
352
381
 
353
- await fsp.mkdir(dshHome, { recursive: true });
354
- await writePatch(homePatch, fileUriInsert(toFileUri(indexFile)));
355
- console.log(`DSH web profile 还不存在,已写入 ${homePatch}`);
356
- console.log(copied ? `插件已安装到 ${dest}` : `已复用已安装的插件 ${dest}`);
357
- console.log("请自己运行:npx @deepseek-ai/dsh web");
382
+ await fsp.mkdir(dshHome, { recursive: true });
383
+ await writePatch(homePatch, fileUriInsert(toFileUri(indexFile)));
384
+ const migrated = await removeLegacyManagedPlugin(opts.legacyPluginHome, dest);
385
+ console.log(`DSH web profile 还不存在,已写入 ${homePatch}`);
386
+ console.log(copied ? `插件已安装到 ${dest}` : `已复用已安装的插件 ${dest}`);
387
+ if (migrated) console.log("已迁移 1.0.5 的旧插件目录,已保留主题数据。");
388
+ console.log("请自己运行:npx @deepseek-ai/dsh web");
358
389
  return { dest, patch: homePatch };
359
390
  }
360
391
 
@@ -386,11 +417,15 @@ async function uninstall(opts) {
386
417
  return { removed };
387
418
  }
388
419
 
389
- export async function runCli(argv = process.argv.slice(2)) {
390
- const dshHome = argValue(argv, "--dsh-home") || defaultDshHome();
391
- const pluginHome =
392
- argValue(argv, "--plugin-home") || path.join(defaultDataRoot(), "plugin");
393
- const opts = { dshHome, pluginHome };
420
+ export async function runCli(argv = process.argv.slice(2)) {
421
+ const dshHome = argValue(argv, "--dsh-home") || defaultDshHome();
422
+ const requestedPluginHome = argValue(argv, "--plugin-home");
423
+ const pluginHome = requestedPluginHome || defaultPluginHome(dshHome);
424
+ const opts = {
425
+ dshHome,
426
+ pluginHome,
427
+ legacyPluginHome: requestedPluginHome ? null : legacyDefaultPluginHome(),
428
+ };
394
429
  if (argv.includes("--remove")) return uninstall(opts);
395
430
  return install(opts);
396
431
  }
package/console.js CHANGED
@@ -221,7 +221,7 @@ body:not([data-ds-dark-theme]) #beauticode-console-pop .bc-theme-del:hover{backg
221
221
  }
222
222
  }
223
223
 
224
- async function run(task) {
224
+ async function run(task) {
225
225
  if (busy) return;
226
226
  busy = true;
227
227
  for (const button of pop.querySelectorAll(".bc-btn, .bc-theme-toggle, .bc-theme-item, .bc-theme-del")) button.disabled = true;
@@ -236,8 +236,31 @@ body:not([data-ds-dark-theme]) #beauticode-console-pop .bc-theme-del:hover{backg
236
236
  } finally {
237
237
  busy = false;
238
238
  for (const button of pop.querySelectorAll(".bc-btn, .bc-theme-toggle, .bc-theme-item, .bc-theme-del")) button.disabled = false;
239
- }
240
- }
239
+ }
240
+ }
241
+
242
+ function askThemeName(fileName) {
243
+ const suggested = String(fileName || "")
244
+ .replace(/\.[^.]+$/, "")
245
+ .trim()
246
+ .slice(0, 80);
247
+ const entered = window.prompt("请给该主题取个名字(1–80 个字符):", suggested);
248
+ if (entered == null) return null;
249
+ const name = entered.trim();
250
+ if (!name) {
251
+ showMessage("主题名不能为空。");
252
+ return null;
253
+ }
254
+ if (name.length > 80) {
255
+ showMessage("主题名不能超过 80 个字符。");
256
+ return null;
257
+ }
258
+ if (/[<>:"/\\|?*]/.test(name) || /[\u0000-\u001f]/.test(name)) {
259
+ showMessage('主题名不能包含 < > : " / \\ | ? * 或控制字符。');
260
+ return null;
261
+ }
262
+ return name;
263
+ }
241
264
 
242
265
  trigger.addEventListener("click", (event) => {
243
266
  event.stopPropagation();
@@ -313,16 +336,21 @@ body:not([data-ds-dark-theme]) #beauticode-console-pop .bc-theme-del:hover{backg
313
336
  }),
314
337
  );
315
338
  });
316
- fileInput.addEventListener("change", () => {
317
- const file = fileInput.files?.[0];
318
- fileInput.value = "";
319
- if (!file) return;
320
- void run(() =>
321
- request("/__beauticode/ui/import", {
322
- method: "POST",
323
- headers: { "x-beauticode-filename": encodeURIComponent(file.name) },
324
- body: file,
325
- }),
339
+ fileInput.addEventListener("change", () => {
340
+ const file = fileInput.files?.[0];
341
+ fileInput.value = "";
342
+ if (!file) return;
343
+ const themeName = askThemeName(file.name);
344
+ if (!themeName) return;
345
+ void run(() =>
346
+ request("/__beauticode/ui/import", {
347
+ method: "POST",
348
+ headers: {
349
+ "x-beauticode-filename": encodeURIComponent(file.name),
350
+ "x-beauticode-theme-name": encodeURIComponent(themeName),
351
+ },
352
+ body: file,
353
+ }),
326
354
  );
327
355
  });
328
356
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beauticode-dsh",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
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
@@ -4,7 +4,7 @@ import path from "node:path";
4
4
  import crypto from "node:crypto";
5
5
  import { pipeline } from "node:stream/promises";
6
6
  import { Transform } from "node:stream";
7
- import { createBeauticodeActions, themeNameFromFilePath } from "./agent.mjs";
7
+ import { createBeauticodeActions } from "./agent.mjs";
8
8
  import { callDshControl } from "./control-client.mjs";
9
9
  import { createGalleryHandlers, resolveConfiguredSkinCenterUrl } from "./gallery-host.mjs";
10
10
  import { hasLiveTray, resolveApplyBackend, stopInProcessSession } from "./host-apply.mjs";
@@ -13,7 +13,7 @@ const IMAGE_EXTENSIONS = new Set([".jpg", ".jpeg", ".png", ".webp", ".avif"]);
13
13
  const MAX_IMAGE_BYTES = 18 * 1024 * 1024;
14
14
  const MAX_VIDEO_BYTES = 800 * 1024 * 1024;
15
15
 
16
- export function parseImportFilename(raw) {
16
+ export function parseImportFilename(raw) {
17
17
  if (typeof raw !== "string" || !raw.trim()) {
18
18
  return { ok: false, error: "缺少文件名。" };
19
19
  }
@@ -35,7 +35,29 @@ export function parseImportFilename(raw) {
35
35
  return { ok: true, name, ext, kind: "image", maxBytes: MAX_IMAGE_BYTES };
36
36
  }
37
37
  return { ok: false, error: "只支持图片(jpg / jpeg / png / webp / avif)或 MP4 视频。" };
38
- }
38
+ }
39
+
40
+ export function parseImportThemeName(raw) {
41
+ if (Array.isArray(raw)) raw = raw[0];
42
+ if (typeof raw !== "string" || !raw.trim()) {
43
+ return { ok: false, error: "请先给该主题取名。" };
44
+ }
45
+ let decoded = raw.trim();
46
+ try {
47
+ decoded = decodeURIComponent(decoded);
48
+ } catch {
49
+ return { ok: false, error: "主题名编码无效。" };
50
+ }
51
+ const name = decoded.trim();
52
+ if (!name) return { ok: false, error: "主题名不能为空。" };
53
+ if (name.length > 80) {
54
+ return { ok: false, error: "主题名不能超过 80 个字符。" };
55
+ }
56
+ if (/[<>:"/\\|?*]/.test(name) || /[\u0000-\u001f]/.test(name)) {
57
+ return { ok: false, error: "主题名包含非法字符。" };
58
+ }
59
+ return { ok: true, name };
60
+ }
39
61
 
40
62
  function limitBytes(maxBytes) {
41
63
  let size = 0;
@@ -168,13 +190,21 @@ export function createBeauticodeUi({ dataRoot, baseUrl, getBaseUrl, sendJson, is
168
190
  res.writeHead(403).end();
169
191
  return;
170
192
  }
171
- const parsed = parseImportFilename(req.headers["x-beauticode-filename"]);
193
+ const parsed = parseImportFilename(req.headers["x-beauticode-filename"]);
172
194
  if (!parsed.ok) {
173
195
  req.resume();
174
196
  sendJson(res, 400, { ok: false, error: parsed.error });
175
- return;
176
- }
177
- const length = Number(req.headers["content-length"]);
197
+ return;
198
+ }
199
+ const parsedTheme = parseImportThemeName(
200
+ req.headers["x-beauticode-theme-name"],
201
+ );
202
+ if (!parsedTheme.ok) {
203
+ req.resume();
204
+ sendJson(res, 400, { ok: false, error: parsedTheme.error });
205
+ return;
206
+ }
207
+ const length = Number(req.headers["content-length"]);
178
208
  if (Number.isFinite(length) && length > parsed.maxBytes) {
179
209
  req.resume();
180
210
  sendJson(res, 413, { ok: false, error: "文件过大。" });
@@ -188,10 +218,7 @@ export function createBeauticodeUi({ dataRoot, baseUrl, getBaseUrl, sendJson, is
188
218
  );
189
219
  try {
190
220
  await pipeline(req, limitBytes(parsed.maxBytes), fs.createWriteStream(tmpPath));
191
- const themeName = themeNameFromFilePath(
192
- parsed.name,
193
- parsed.kind === "video" ? "视频" : "图片",
194
- );
221
+ const themeName = parsedTheme.name;
195
222
  const result =
196
223
  parsed.kind === "video"
197
224
  ? await actions.applyVideo({ path: tmpPath, themeName })
@@ -93,7 +93,10 @@ export class DshSession {
93
93
  this.startHandoffLoop();
94
94
  void this.yieldToTrayIfRequested();
95
95
  }
96
- void this.watchOnce();
96
+ // Finish the initial restore check before accepting the first user apply.
97
+ // Otherwise a click immediately after lazy session startup can race the
98
+ // background reapply and be rejected as a concurrent operation.
99
+ await this.watchOnce();
97
100
  return { port: this.bridgePort() };
98
101
  }
99
102
  async apply(input) {
@@ -68,6 +68,7 @@ export async function ensureDataLayout(paths) {
68
68
  entry === SNAPSHOTS_DIR_NAME ||
69
69
  entry === SAVED_DIR_NAME ||
70
70
  entry === RUNTIME_MEDIA_DIR_NAME ||
71
+ entry === "tmp" ||
71
72
  entry === "logs" ||
72
73
  entry === "engine-launcher.log" ||
73
74
  entry === "injector.lock" ||