beauticode-dsh 1.0.0

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 (40) hide show
  1. package/README.zh-CN.md +48 -0
  2. package/agent.mjs +595 -0
  3. package/atmosphere.js +306 -0
  4. package/bin/beauticode-dsh +219 -0
  5. package/cli.js +219 -0
  6. package/client.js +370 -0
  7. package/console.js +303 -0
  8. package/control-client.mjs +445 -0
  9. package/cordis.patch.yml +4 -0
  10. package/host-apply.mjs +163 -0
  11. package/index.mjs +570 -0
  12. package/package.json +56 -0
  13. package/presets.mjs +73 -0
  14. package/themes/internal-beyond/NOTICE.md +9 -0
  15. package/themes/internal-beyond/bg-canvas-4k.png +0 -0
  16. package/themes/internal-beyond/bg-canvas.png +0 -0
  17. package/themes/internal-beyond/bg-infernal.jpg +0 -0
  18. package/themes/internal-beyond/bg-internal.jpg +0 -0
  19. package/ui-host.mjs +292 -0
  20. package/vendor/adapter-dsh/bridge.js +215 -0
  21. package/vendor/adapter-dsh/host-descriptor.js +15 -0
  22. package/vendor/adapter-dsh/index.js +6 -0
  23. package/vendor/adapter-dsh/injector-lock.js +27 -0
  24. package/vendor/adapter-dsh/package.json +5 -0
  25. package/vendor/adapter-dsh/session.js +527 -0
  26. package/vendor/adapter-dsh/token.js +46 -0
  27. package/vendor/adapter-dsh/tray-handoff.js +54 -0
  28. package/vendor/core/apply-transaction.js +287 -0
  29. package/vendor/core/background-store.js +1116 -0
  30. package/vendor/core/bundled-gallery.js +53 -0
  31. package/vendor/core/constants.js +47 -0
  32. package/vendor/core/error-message.js +208 -0
  33. package/vendor/core/file-lock.js +151 -0
  34. package/vendor/core/host-session.js +2 -0
  35. package/vendor/core/index.js +12 -0
  36. package/vendor/core/media-server.js +544 -0
  37. package/vendor/core/media-validation.js +244 -0
  38. package/vendor/core/package.json +5 -0
  39. package/vendor/core/paths.js +168 -0
  40. package/vendor/core/types.js +15 -0
@@ -0,0 +1,48 @@
1
+ # DeepSeek Harness 桥接插件
2
+
3
+ 该 Cordis 插件向 DSH Web 注入 beautiCode 浏览器客户端,并提供本机鉴权接口。支持图片、MP4、播放位置、静音、摸鱼模式以及清除背景。
4
+
5
+ 装好插件并运行 `dsh web` 后,侧栏「设置」上方会出现「背景」:可从文件夹选图片或 MP4、清除、开关声音、切换已保存主题。网页控制台没有摸鱼;浅色/深色跟 DSH 自己的外观走。下次打开会恢复上次背景。
6
+
7
+ 插件也会注册对话工具(`beauticode_*`)和斜杠命令(`/bg`、`/bg-theme`、`/bg-clear`)。不需要托盘;托盘若已在跑则复用它。
8
+
9
+ beautiCode **不启动** DeepSeek Harness。请先自己运行 `dsh web`。
10
+
11
+ 不需要 fork 仓库。发布到 npm 之后,一行安装:
12
+
13
+ ```sh
14
+ npx @beauticode/dsh-plugin
15
+ npx @deepseek-ai/dsh web
16
+ ```
17
+
18
+ 已有 pnpm 时也可以:
19
+
20
+ ```sh
21
+ npx @deepseek-ai/dsh plugin --profile web add @beauticode/dsh-plugin
22
+ npx @deepseek-ai/dsh web
23
+ ```
24
+
25
+ 在仓库根目录一键写入插件(不需要 pnpm):
26
+
27
+ ```powershell
28
+ powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\install-dsh-plugin.ps1
29
+ npx @deepseek-ai/dsh web
30
+ ```
31
+
32
+ 也可以手动(`dsh plugin add` 需要 pnpm):
33
+
34
+ ```sh
35
+ dsh plugin --profile web add file:%CD%/integrations/deepseek-harness
36
+ # 或
37
+ npx @deepseek-ai/dsh plugin --profile web add file:%CD%/integrations/deepseek-harness
38
+ npx @deepseek-ai/dsh web
39
+ ```
40
+
41
+ 手动接入时:
42
+
43
+ 1. 修改 `cordis.patch.example.yml` 中的 `file:///.../index.mjs`。
44
+ 2. 确保 DSH 和 beautiCode 使用相同的 `BEAUTICODE_DATA_ROOT`。
45
+ 3. 自己运行 `dsh web`(或带 `--patch`)。
46
+ 4. 打开页面后用侧栏「背景」,或输入 `/bg <本机绝对路径>`,或直接跟 AI 说把某个视频/图片设为背景。
47
+
48
+ 安全边界:DSH Web 必须绑定本机回环地址;控制端点需要随机令牌;媒体 URL 仅允许带令牌的回环 HTTP 地址;浏览器回执只接受同源请求。
package/agent.mjs ADDED
@@ -0,0 +1,595 @@
1
+ import {
2
+ callDshControl,
3
+ formatStatusText,
4
+ inspectLocalMedia,
5
+ matchSavedTheme,
6
+ stripPathQuotes,
7
+ } from "./control-client.mjs";
8
+ import { resolveApplyBackend, stopInProcessSession } from "./host-apply.mjs";
9
+ import { ATMOSPHERE_PRESETS, effectsForPreset, presetImagePath } from "./presets.mjs";
10
+
11
+ const APPLY_TIMEOUT_MS = 180_000;
12
+ const QUICK_TIMEOUT_MS = 15_000;
13
+
14
+ const RESULT_SCHEMA = {
15
+ type: "object",
16
+ additionalProperties: true,
17
+ properties: {
18
+ ok: { type: "boolean" },
19
+ message: { type: "string" },
20
+ },
21
+ required: ["ok", "message"],
22
+ };
23
+
24
+ const BG_USAGE =
25
+ "用法:/bg <本机图片或 MP4 绝对路径>。切换主题用 /bg-theme <名称>,清除用 /bg-clear。";
26
+
27
+ function asText(message) {
28
+ return [{ type: "text", text: String(message) }];
29
+ }
30
+
31
+ function fail(error) {
32
+ throw error instanceof Error ? error : new Error(String(error));
33
+ }
34
+
35
+ function commandResultFromError(error) {
36
+ return {
37
+ kind: "error",
38
+ text: error instanceof Error ? error.message : String(error),
39
+ };
40
+ }
41
+
42
+ function normalizeActionOptions(dataRootOrOptions) {
43
+ if (typeof dataRootOrOptions === "string") {
44
+ return { dataRoot: dataRootOrOptions };
45
+ }
46
+ return dataRootOrOptions && typeof dataRootOrOptions === "object"
47
+ ? dataRootOrOptions
48
+ : {};
49
+ }
50
+
51
+ function unwrapApply(result, fallbackMode, message) {
52
+ if (!result || result.ok === false) {
53
+ fail(result?.error || "操作失败。");
54
+ }
55
+ return {
56
+ ok: true,
57
+ generation: result.generation ?? null,
58
+ mode: result.mode ?? fallbackMode,
59
+ message,
60
+ };
61
+ }
62
+
63
+ function presentStatus(status) {
64
+ return {
65
+ ok: true,
66
+ hostReady: status.hostReady === true || status.sessions > 0,
67
+ sessions: status.sessions ?? 0,
68
+ fish: status.fish === true,
69
+ muted: status.muted !== false,
70
+ tone: status.tone ?? "dark",
71
+ background: status.manifest?.background ?? status.background ?? null,
72
+ message: formatStatusText(status),
73
+ };
74
+ }
75
+
76
+ function presentThemes(themes) {
77
+ const list = Array.isArray(themes) ? themes : [];
78
+ return {
79
+ ok: true,
80
+ themes: list,
81
+ message:
82
+ list.length === 0
83
+ ? "还没有已保存的主题。"
84
+ : `已保存主题:${list.map((theme) => theme.name).join("、")}。`,
85
+ };
86
+ }
87
+
88
+ export function createBeauticodeActions(dataRootOrOptions) {
89
+ const options = normalizeActionOptions(dataRootOrOptions);
90
+ const dataRoot = options.dataRoot;
91
+ const request = (spec) =>
92
+ callDshControl(dataRoot, {
93
+ timeoutMs: spec.timeoutMs ?? APPLY_TIMEOUT_MS,
94
+ ...spec,
95
+ });
96
+
97
+ async function backend() {
98
+ return resolveApplyBackend(options);
99
+ }
100
+
101
+ return {
102
+ async applyImage(imagePath, signal, options) {
103
+ const inspected = await inspectLocalMedia(imagePath);
104
+ if (!inspected.ok) fail(inspected.error);
105
+ if (inspected.kind !== "image") {
106
+ fail("beauticode_apply_image 只接受图片文件。");
107
+ }
108
+ const effects = effectsForPreset(options?.effects?.preset) || options?.effects || null;
109
+ const input = { type: "image", imagePath: inspected.path };
110
+ if (effects) input.effects = effects;
111
+ const resolved = await backend();
112
+ if (resolved.kind === "tray") {
113
+ const body = { imagePath: inspected.path };
114
+ if (effects) body.effects = effects;
115
+ return unwrapApply(
116
+ await request({
117
+ method: "POST",
118
+ path: "/apply/image",
119
+ body,
120
+ signal,
121
+ }),
122
+ "image",
123
+ "已将图片设为背景。",
124
+ );
125
+ }
126
+ return unwrapApply(await resolved.session.apply(input), "image", "已将图片设为背景。");
127
+ },
128
+
129
+ async applyPreset(id, signal) {
130
+ const preset = ATMOSPHERE_PRESETS[id];
131
+ const imagePath = presetImagePath(id);
132
+ if (!preset || !imagePath) fail("未找到内置主题文件。");
133
+ const result = await this.applyImage(imagePath, signal, {
134
+ effects: effectsForPreset(id),
135
+ });
136
+ try {
137
+ await this.setTone(preset.tone, signal);
138
+ } catch {
139
+ /* tone is best-effort; the wallpaper still applied */
140
+ }
141
+ return {
142
+ ...result,
143
+ atmosphere: id,
144
+ message: `已应用 ${preset.name} 活壁纸。`,
145
+ };
146
+ },
147
+
148
+ async setTone(tone, signal) {
149
+ const resolved = await backend();
150
+ const result =
151
+ resolved.kind === "tray"
152
+ ? await request({
153
+ method: "POST",
154
+ path: "/mode/tone",
155
+ body: { tone },
156
+ signal,
157
+ timeoutMs: QUICK_TIMEOUT_MS,
158
+ })
159
+ : await resolved.session.setBackgroundTone(tone);
160
+ if (result.ok === false) fail(result.error || "无法切换背景色调。");
161
+ return { ok: true, tone: result.tone ?? tone };
162
+ },
163
+
164
+ async applyVideo(input, signal) {
165
+ const inspected = await inspectLocalMedia(input.path);
166
+ if (!inspected.ok) fail(inspected.error);
167
+ if (inspected.kind !== "video") {
168
+ fail("beauticode_apply_video 只接受 .mp4 文件。");
169
+ }
170
+ const body = { videoPath: inspected.path };
171
+ const localInput = { type: "video", videoPath: inspected.path };
172
+ if (typeof input.poster === "string" && input.poster.trim()) {
173
+ const poster = await inspectLocalMedia(input.poster);
174
+ if (!poster.ok) fail(poster.error);
175
+ if (poster.kind !== "image") fail("poster 必须是图片文件。");
176
+ body.imagePath = poster.path;
177
+ localInput.imagePath = poster.path;
178
+ }
179
+ if (input.startAt != null && input.startAt !== "") {
180
+ const startAt = Number(input.startAt);
181
+ if (!Number.isFinite(startAt) || startAt < 0) {
182
+ fail("startAt 必须是非负数字(秒)。");
183
+ }
184
+ body.startAt = startAt;
185
+ localInput.startAt = startAt;
186
+ }
187
+ const resolved = await backend();
188
+ if (resolved.kind === "tray") {
189
+ return unwrapApply(
190
+ await request({
191
+ method: "POST",
192
+ path: "/apply/video",
193
+ body,
194
+ signal,
195
+ }),
196
+ "video",
197
+ "已将视频设为背景。",
198
+ );
199
+ }
200
+ return unwrapApply(
201
+ await resolved.session.apply(localInput),
202
+ "video",
203
+ "已将视频设为背景。",
204
+ );
205
+ },
206
+
207
+ async clear(signal) {
208
+ const resolved = await backend();
209
+ if (resolved.kind === "tray") {
210
+ return unwrapApply(
211
+ await request({
212
+ method: "POST",
213
+ path: "/apply/clear",
214
+ body: {},
215
+ signal,
216
+ }),
217
+ "clear",
218
+ "已清除背景。",
219
+ );
220
+ }
221
+ return unwrapApply(await resolved.session.apply({ type: "clear" }), "clear", "已清除背景。");
222
+ },
223
+
224
+ async status(signal) {
225
+ const resolved = await backend();
226
+ if (resolved.kind === "tray") {
227
+ return presentStatus(
228
+ await request({
229
+ method: "GET",
230
+ path: "/status",
231
+ signal,
232
+ timeoutMs: QUICK_TIMEOUT_MS,
233
+ }),
234
+ );
235
+ }
236
+ const status = await resolved.session.status();
237
+ return presentStatus({
238
+ ...status,
239
+ hostReady: resolved.session.isHostReady,
240
+ });
241
+ },
242
+
243
+ async listThemes(signal) {
244
+ const resolved = await backend();
245
+ if (resolved.kind === "tray") {
246
+ const result = await request({
247
+ method: "GET",
248
+ path: "/theme/list",
249
+ signal,
250
+ timeoutMs: QUICK_TIMEOUT_MS,
251
+ });
252
+ return presentThemes(result.themes);
253
+ }
254
+ return presentThemes(await resolved.session.listSavedThemes());
255
+ },
256
+
257
+ async useTheme(query, signal) {
258
+ const listed = await this.listThemes(signal);
259
+ const matched = matchSavedTheme(listed.themes, query);
260
+ if (!matched.ok) fail(matched.error);
261
+ const resolved = await backend();
262
+ const result =
263
+ resolved.kind === "tray"
264
+ ? await request({
265
+ method: "POST",
266
+ path: "/theme/use",
267
+ body: { id: matched.theme.id },
268
+ signal,
269
+ })
270
+ : await resolved.session.useSavedTheme(matched.theme.id);
271
+ return {
272
+ ...unwrapApply(
273
+ result,
274
+ matched.theme.type ?? null,
275
+ `已切换到主题「${matched.theme.name}」。`,
276
+ ),
277
+ theme: matched.theme,
278
+ };
279
+ },
280
+
281
+ async setFish(enabled, signal) {
282
+ const resolved = await backend();
283
+ const want = Boolean(enabled);
284
+ const result =
285
+ resolved.kind === "tray"
286
+ ? await request({
287
+ method: "POST",
288
+ path: "/mode/fish",
289
+ body: { enabled: want },
290
+ signal,
291
+ timeoutMs: QUICK_TIMEOUT_MS,
292
+ })
293
+ : await resolved.session.setFishMode(want);
294
+ if (result.ok === false) fail(result.error || "无法切换摸鱼模式。");
295
+ return {
296
+ ok: true,
297
+ fish: result.fish === true,
298
+ message: result.fish ? "已进入摸鱼模式。" : "已退出摸鱼模式。",
299
+ };
300
+ },
301
+
302
+ async setMuted(muted, signal) {
303
+ const resolved = await backend();
304
+ const want = Boolean(muted);
305
+ const result =
306
+ resolved.kind === "tray"
307
+ ? await request({
308
+ method: "POST",
309
+ path: "/mode/muted",
310
+ body: { muted: want },
311
+ signal,
312
+ timeoutMs: QUICK_TIMEOUT_MS,
313
+ })
314
+ : await resolved.session.setMuted(want);
315
+ if (result.ok === false) fail(result.error || "无法切换背景声音。");
316
+ const blocked = result.blocked === true;
317
+ return {
318
+ ok: true,
319
+ muted: result.muted !== false,
320
+ blocked,
321
+ message: blocked
322
+ ? "浏览器阻止了开启声音,视频将继续静音播放。"
323
+ : result.muted
324
+ ? "背景视频已静音。"
325
+ : "背景视频声音已打开。",
326
+ };
327
+ },
328
+ };
329
+ }
330
+
331
+ export async function runBgCommand(dataRootOrOptions, rawInput, signal) {
332
+ const actions = createBeauticodeActions(dataRootOrOptions);
333
+ const value = stripPathQuotes(rawInput);
334
+ if (!value) {
335
+ try {
336
+ const status = await actions.status(signal);
337
+ return `${status.message}\n${BG_USAGE}`;
338
+ } catch (error) {
339
+ const message = error instanceof Error ? error.message : String(error);
340
+ return `${message}\n${BG_USAGE}`;
341
+ }
342
+ }
343
+ const inspected = await inspectLocalMedia(value);
344
+ if (!inspected.ok) fail(inspected.error);
345
+ if (inspected.kind === "video") {
346
+ const result = await actions.applyVideo({ path: inspected.path }, signal);
347
+ return result.message;
348
+ }
349
+ const result = await actions.applyImage(inspected.path, signal);
350
+ return result.message;
351
+ }
352
+
353
+ function registerOne(register, definition) {
354
+ try {
355
+ register(definition);
356
+ } catch {
357
+ /* One bad schema or duplicate name must not drop the rest. */
358
+ }
359
+ }
360
+
361
+ function registerBeauticodeTools(ctx, options) {
362
+ const tools = ctx.tools;
363
+ if (!tools || typeof tools.register !== "function") return;
364
+ const actions = createBeauticodeActions(options);
365
+
366
+ registerOne(tools.register.bind(tools), {
367
+ name: "beauticode_apply_video",
368
+ description:
369
+ "把本机 MP4 设为 DeepSeek Harness 网页背景。path 必须是绝对路径。不需要 beautiCode 托盘。",
370
+ parameters: {
371
+ type: "object",
372
+ additionalProperties: false,
373
+ properties: {
374
+ path: { type: "string", description: "本机 MP4 的绝对路径。" },
375
+ poster: { type: "string", description: "可选海报图片的绝对路径。" },
376
+ startAt: { type: "number", description: "可选起始播放位置(秒)。" },
377
+ },
378
+ required: ["path"],
379
+ },
380
+ output: { schema: RESULT_SCHEMA, render: (_args, value) => asText(value.message) },
381
+ timeoutMs: APPLY_TIMEOUT_MS,
382
+ async execute(args, exec) {
383
+ return actions.applyVideo(
384
+ {
385
+ path: args?.path,
386
+ poster: args?.poster,
387
+ startAt: args?.startAt,
388
+ },
389
+ exec?.signal,
390
+ );
391
+ },
392
+ });
393
+
394
+ registerOne(tools.register.bind(tools), {
395
+ name: "beauticode_apply_image",
396
+ description:
397
+ "把本机图片设为 DeepSeek Harness 网页背景。path 必须是绝对路径。不需要 beautiCode 托盘。",
398
+ parameters: {
399
+ type: "object",
400
+ additionalProperties: false,
401
+ properties: {
402
+ path: { type: "string", description: "本机图片的绝对路径(jpg / jpeg / png / webp / avif)。" },
403
+ },
404
+ required: ["path"],
405
+ },
406
+ output: { schema: RESULT_SCHEMA, render: (_args, value) => asText(value.message) },
407
+ timeoutMs: APPLY_TIMEOUT_MS,
408
+ async execute(args, exec) {
409
+ return actions.applyImage(args?.path, exec?.signal);
410
+ },
411
+ });
412
+
413
+ registerOne(tools.register.bind(tools), {
414
+ name: "beauticode_theme_list",
415
+ description: "列出 beautiCode 已保存的背景主题。",
416
+ parameters: { type: "object", additionalProperties: false, properties: {} },
417
+ output: { schema: RESULT_SCHEMA, render: (_args, value) => asText(value.message) },
418
+ timeoutMs: QUICK_TIMEOUT_MS,
419
+ async execute(_args, exec) {
420
+ return actions.listThemes(exec?.signal);
421
+ },
422
+ });
423
+
424
+ registerOne(tools.register.bind(tools), {
425
+ name: "beauticode_theme_use",
426
+ description: "按名称或 ID 切换已保存的 beautiCode 背景主题。",
427
+ parameters: {
428
+ type: "object",
429
+ additionalProperties: false,
430
+ properties: {
431
+ name: { type: "string", description: "主题名称或 ID。" },
432
+ },
433
+ required: ["name"],
434
+ },
435
+ output: { schema: RESULT_SCHEMA, render: (_args, value) => asText(value.message) },
436
+ timeoutMs: APPLY_TIMEOUT_MS,
437
+ async execute(args, exec) {
438
+ return actions.useTheme(args?.name, exec?.signal);
439
+ },
440
+ });
441
+
442
+ registerOne(tools.register.bind(tools), {
443
+ name: "beauticode_clear",
444
+ description: "清除 DeepSeek Harness 上的 beautiCode 背景。",
445
+ parameters: { type: "object", additionalProperties: false, properties: {} },
446
+ output: { schema: RESULT_SCHEMA, render: (_args, value) => asText(value.message) },
447
+ timeoutMs: APPLY_TIMEOUT_MS,
448
+ async execute(_args, exec) {
449
+ return actions.clear(exec?.signal);
450
+ },
451
+ });
452
+
453
+ registerOne(tools.register.bind(tools), {
454
+ name: "beauticode_status",
455
+ description: "查看当前 beautiCode 背景、摸鱼和声音状态。",
456
+ parameters: { type: "object", additionalProperties: false, properties: {} },
457
+ output: { schema: RESULT_SCHEMA, render: (_args, value) => asText(value.message) },
458
+ timeoutMs: QUICK_TIMEOUT_MS,
459
+ async execute(_args, exec) {
460
+ return actions.status(exec?.signal);
461
+ },
462
+ });
463
+
464
+ registerOne(tools.register.bind(tools), {
465
+ name: "beauticode_set_fish",
466
+ description: "打开或关闭摸鱼模式(隐藏 DSH 界面,只留背景)。需要已经有背景。",
467
+ parameters: {
468
+ type: "object",
469
+ additionalProperties: false,
470
+ properties: {
471
+ enabled: { type: "boolean", description: "true 进入摸鱼,false 退出。" },
472
+ },
473
+ required: ["enabled"],
474
+ },
475
+ output: { schema: RESULT_SCHEMA, render: (_args, value) => asText(value.message) },
476
+ timeoutMs: QUICK_TIMEOUT_MS,
477
+ async execute(args, exec) {
478
+ if (typeof args?.enabled !== "boolean") fail("enabled 必须是布尔值。");
479
+ return actions.setFish(args.enabled, exec?.signal);
480
+ },
481
+ });
482
+
483
+ registerOne(tools.register.bind(tools), {
484
+ name: "beauticode_set_muted",
485
+ description: "打开或关闭背景视频声音。默认静音。",
486
+ parameters: {
487
+ type: "object",
488
+ additionalProperties: false,
489
+ properties: {
490
+ muted: { type: "boolean", description: "true 静音,false 尝试开声音。" },
491
+ },
492
+ required: ["muted"],
493
+ },
494
+ output: { schema: RESULT_SCHEMA, render: (_args, value) => asText(value.message) },
495
+ timeoutMs: QUICK_TIMEOUT_MS,
496
+ async execute(args, exec) {
497
+ if (typeof args?.muted !== "boolean") fail("muted 必须是布尔值。");
498
+ return actions.setMuted(args.muted, exec?.signal);
499
+ },
500
+ });
501
+
502
+ try {
503
+ const prompt = typeof ctx.get === "function" ? ctx.get("systemPrompt") : ctx.systemPrompt;
504
+ if (prompt && typeof prompt.section === "function") {
505
+ prompt.section({
506
+ name: "tool:beauticode",
507
+ order: 160,
508
+ text:
509
+ "beautiCode 可以把本机图片或 MP4 设为当前 DeepSeek Harness 页面背景。用户要换背景、导入视频或壁纸、切换已保存主题、摸鱼或开关背景声音时,调用 beauticode_* 工具,不要用 shell 改文件或 curl。路径必须是本机绝对路径。用户也可以直接输入斜杠命令 /bg、/bg-theme、/bg-clear。不需要 beautiCode 托盘。",
510
+ });
511
+ }
512
+ } catch {
513
+ /* Prompt guidance is optional. */
514
+ }
515
+ }
516
+
517
+ function registerBeauticodeCommands(ctx, options) {
518
+ const commands = ctx.commands;
519
+ if (!commands || typeof commands.register !== "function") return;
520
+ const actions = createBeauticodeActions(options);
521
+
522
+ registerOne(commands.register.bind(commands), {
523
+ name: "bg",
524
+ description: "把本机图片或 MP4 设为 beautiCode 背景",
525
+ input: { hint: "本机图片或 MP4 的绝对路径" },
526
+ async handler({ rawInput, signal }) {
527
+ try {
528
+ return { kind: "success", text: await runBgCommand(options, rawInput, signal) };
529
+ } catch (error) {
530
+ return commandResultFromError(error);
531
+ }
532
+ },
533
+ });
534
+
535
+ registerOne(commands.register.bind(commands), {
536
+ name: "bg-theme",
537
+ description: "切换已保存的 beautiCode 背景主题",
538
+ input: { hint: "主题名称" },
539
+ async handler({ rawInput, signal }) {
540
+ try {
541
+ const name = stripPathQuotes(rawInput);
542
+ if (!name) {
543
+ const listed = await actions.listThemes(signal);
544
+ return { kind: "success", text: `${listed.message} 用法:/bg-theme <名称>` };
545
+ }
546
+ const result = await actions.useTheme(name, signal);
547
+ return { kind: "success", text: result.message };
548
+ } catch (error) {
549
+ return commandResultFromError(error);
550
+ }
551
+ },
552
+ });
553
+
554
+ registerOne(commands.register.bind(commands), {
555
+ name: "bg-clear",
556
+ description: "清除 beautiCode 背景",
557
+ async handler({ signal }) {
558
+ try {
559
+ const result = await actions.clear(signal);
560
+ return { kind: "success", text: result.message };
561
+ } catch (error) {
562
+ return commandResultFromError(error);
563
+ }
564
+ },
565
+ });
566
+ }
567
+
568
+ export function registerAgentSurfaces(ctx, options = {}) {
569
+ if (!ctx || typeof ctx.inject !== "function") return;
570
+ const dataRoot = options.dataRoot;
571
+ if (typeof dataRoot !== "string" || !dataRoot) return;
572
+ const actionOptions = {
573
+ dataRoot,
574
+ baseUrl: options.baseUrl,
575
+ };
576
+ if (typeof ctx.effect === "function") {
577
+ ctx.effect(() => () => {
578
+ void stopInProcessSession(dataRoot);
579
+ });
580
+ }
581
+ try {
582
+ ctx.inject(["tools"], (inner) => {
583
+ registerBeauticodeTools(inner, actionOptions);
584
+ });
585
+ } catch {
586
+ /* Keep the page bridge alive if this DSH build has no tool registry. */
587
+ }
588
+ try {
589
+ ctx.inject(["commands"], (inner) => {
590
+ registerBeauticodeCommands(inner, actionOptions);
591
+ });
592
+ } catch {
593
+ /* Same: slash commands are optional on a webServer-only composition. */
594
+ }
595
+ }