dsh-vscode-mode 0.1.52 → 0.1.53

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/lib/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { createRequire } from "node:module";
2
- import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
2
+ import { chmod, copyFile, cp, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
3
3
  import { basename, dirname, extname, join, normalize, resolve, sep } from "node:path";
4
4
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
5
5
  import { fileURLToPath, pathToFileURL } from "node:url";
6
6
  import { homedir, tmpdir } from "node:os";
7
- import { createHash } from "node:crypto";
7
+ import { createHash, randomUUID } from "node:crypto";
8
8
  import { Buffer as Buffer$1 } from "node:buffer";
9
9
  import { spawn } from "node:child_process";
10
10
  import { inflateRawSync } from "node:zlib";
@@ -127,6 +127,19 @@ const KEYBINDING_DEFAULTS = {
127
127
  "edrv.navigateForward": "Alt+ArrowRight|Ctrl+Shift+-"
128
128
  };
129
129
  //#endregion
130
+ //#region src/shared/integration.ts
131
+ /**
132
+ * dsh-vscode-mode shared — 系统集成载荷契约(Windows 右键菜单 + Unity 一键安装/更新)。
133
+ * Purity rule: no node/react imports(platform 用 string 表示)。
134
+ * 作者 ddj 2026-09-07
135
+ */
136
+ /** Unity 内嵌包名(UPM 包名,同时是项目 Packages/ 下的目录名)。 */
137
+ const UNITY_PACKAGE_NAME = "com.dsh.editor";
138
+ /** 注册表菜单键叶子名(HKCU\Software\Classes\*\shell\<leaf>)。 */
139
+ const SHELL_MENU_LEAF = "DSHEditor";
140
+ /** 深链基址默认值(DSH Web GUI 默认端口,dsh-web.cmd 实证)。 */
141
+ const INTEGRATION_BASE_DEFAULT = "http://127.0.0.1:3080";
142
+ //#endregion
130
143
  //#region src/fileOpenSettings.ts
131
144
  const FILE_OPEN_SETTINGS_NS = "dsh-vscode-mode";
132
145
  const FILE_OPEN_DEFAULT = "auto";
@@ -247,6 +260,11 @@ function normalizeValue(value) {
247
260
  function configValue(config) {
248
261
  return normalizeValue(config?.fileOpenTool);
249
262
  }
263
+ /** 配置/设置里的深链基址(缺省/非法回退默认值)。 */
264
+ function baseValueOf(config) {
265
+ const raw = config?.integrationBaseUrl;
266
+ return typeof raw === "string" && raw.trim() ? raw.trim() : INTEGRATION_BASE_DEFAULT;
267
+ }
250
268
  /**
251
269
  * 安装设置 section(版本自适应;任一策略不可用返回 false 不抛错)。
252
270
  * @author ddj 2026年08月24号 / 2026年09月02号
@@ -266,7 +284,8 @@ async function installOpenSettingsSection(ctx, ns, entry, hooks, loader = loadSe
266
284
  const strategy = await runSettingsInstall(ctx, ns, deps.z.object({
267
285
  fileOpenTool: deps.z.string().default(FILE_OPEN_DEFAULT),
268
286
  keybindings: deps.z.object(keybindingsShape(deps.z)).default({ ...KEYBINDING_DEFAULTS }),
269
- sidebarMinWidth: deps.z.number().default(300)
287
+ sidebarMinWidth: deps.z.number().default(300),
288
+ integrationBaseUrl: deps.z.string().default(INTEGRATION_BASE_DEFAULT)
270
289
  }), entry, {
271
290
  setSource: (source) => hooks.setSource(source),
272
291
  onChange: hooks.onChange
@@ -294,7 +313,10 @@ function setupOpenSettings(ctx, config, onChange) {
294
313
  };
295
314
  const setSource = (source) => notify(source().fileOpenTool);
296
315
  const settingsChange = () => syncRevision();
297
- installOpenSettingsSection(ctx, FILE_OPEN_SETTINGS_NS, { fileOpenTool: current }, {
316
+ installOpenSettingsSection(ctx, FILE_OPEN_SETTINGS_NS, {
317
+ fileOpenTool: current,
318
+ integrationBaseUrl: baseValueOf(config)
319
+ }, {
298
320
  setSource,
299
321
  onChange: settingsChange
300
322
  });
@@ -4309,6 +4331,926 @@ async function projectRefresh(ctx, workspacePath, serverName) {
4309
4331
  return projectOf(ctx, workspacePath, workspace.title ?? "");
4310
4332
  }
4311
4333
  //#endregion
4334
+ //#region src/unityBridge.ts
4335
+ /**
4336
+ * dsh-vscode-mode host — Unity 内嵌包一键安装/更新。
4337
+ * 机制:把包源目录 unity/com.dsh.editor 复制为 <Unity项目根>/Packages/com.dsh.editor
4338
+ * (Unity 内嵌包自动发现,无需改 manifest.json);安装/更新同入口(整目录替换);
4339
+ * 卸载 = 删除目标目录。登记清单存 ~/.dsh/dsh-vscode-mode/unity-projects.json。
4340
+ * 安全:只触碰固定目标目录;项目根须含 Assets + ProjectSettings;目标已存在且
4341
+ * package.json name 不是本包 → 拒绝覆盖。纯路径构造与项目特征判定可单测。
4342
+ * 作者 ddj 2026-09-07
4343
+ */
4344
+ /** Unity 项目特征目录。 */
4345
+ const UNITY_MARKERS = ["Assets", "ProjectSettings"];
4346
+ /** 登记清单文件名。 */
4347
+ const PROJECTS_FILE = "unity-projects.json";
4348
+ /** 包源目录相对插件包根的位置。 */
4349
+ const UNITY_DIR_NAME = "unity";
4350
+ /**
4351
+ * 包源目录(随插件包分发的 unity/com.dsh.editor 绝对路径)。
4352
+ * @author ddj 2026年09月07号
4353
+ * @param moduleUrl 模块 URL(缺省 import.meta.url;测试注入)
4354
+ * @returns 包源目录绝对路径
4355
+ */
4356
+ function unitySourceDir(moduleUrl = import.meta.url) {
4357
+ return join(dirname(fileURLToPath(moduleUrl)), "..", UNITY_DIR_NAME, UNITY_PACKAGE_NAME);
4358
+ }
4359
+ /**
4360
+ * 项目登记清单文件(~/.dsh/dsh-vscode-mode/unity-projects.json)。
4361
+ * @author ddj 2026年09月07号
4362
+ * @param home DSH home(测试可注入)
4363
+ * @returns 清单文件绝对路径
4364
+ */
4365
+ function unityProjectsFile(home = dshHome()) {
4366
+ return join(home, PLUGIN_ID, PROJECTS_FILE);
4367
+ }
4368
+ /**
4369
+ * 归一化用户输入的项目根:去首尾空白/引号/尾部分隔符。
4370
+ * @author ddj 2026年09月07号
4371
+ * @param raw 原始输入
4372
+ * @returns 归一化绝对路径
4373
+ */
4374
+ function normalizeUnityRoot(raw) {
4375
+ return String(raw ?? "").trim().replace(/^["']|["']$/g, "").replace(/[\\/]+$/, "");
4376
+ }
4377
+ /**
4378
+ * 目录名清单是否满足 Unity 项目特征(Assets + ProjectSettings 同时存在)。
4379
+ * @author ddj 2026年09月07号
4380
+ * @param entries 项目根下的目录名清单
4381
+ * @returns 是否 Unity 项目根
4382
+ */
4383
+ function isUnityProject(entries) {
4384
+ return UNITY_MARKERS.every((marker) => entries.includes(marker));
4385
+ }
4386
+ /**
4387
+ * 内嵌包目标目录(固定:<root>/Packages/com.dsh.editor)。
4388
+ * @author ddj 2026年09月07号
4389
+ * @param root Unity 项目根
4390
+ * @returns 目标目录绝对路径
4391
+ */
4392
+ function unityTargetOf(root) {
4393
+ return join(root, "Packages", UNITY_PACKAGE_NAME);
4394
+ }
4395
+ /**
4396
+ * 读 package.json 的 version(缺失/非法 → null)。
4397
+ * @author ddj 2026年09月07号
4398
+ * @param pkgFile package.json 绝对路径
4399
+ * @returns 版本号或 null
4400
+ */
4401
+ async function readPackageVersion(pkgFile) {
4402
+ try {
4403
+ const data = JSON.parse(await readFile(pkgFile, "utf8"));
4404
+ return typeof data?.version === "string" && data.version ? data.version : null;
4405
+ } catch {
4406
+ return null;
4407
+ }
4408
+ }
4409
+ /** 读登记清单(缺失/非法 → 空数组)。 */
4410
+ async function readProjects(home = dshHome()) {
4411
+ try {
4412
+ const data = JSON.parse(await readFile(unityProjectsFile(home), "utf8"));
4413
+ if (!Array.isArray(data)) return [];
4414
+ return data.filter((item) => !!item && typeof item.path === "string").map((item) => ({
4415
+ path: String(item.path),
4416
+ addedAt: String(item.addedAt ?? "")
4417
+ }));
4418
+ } catch {
4419
+ return [];
4420
+ }
4421
+ }
4422
+ /** 写登记清单(父目录按需创建)。 */
4423
+ async function writeProjects(records, home = dshHome()) {
4424
+ const file = unityProjectsFile(home);
4425
+ await mkdir(dirname(file), { recursive: true });
4426
+ await writeFile(file, JSON.stringify(records, null, 2) + "\n", "utf8");
4427
+ }
4428
+ /**
4429
+ * 单个项目的安装状态视图(版本对比 + 合法性)。
4430
+ * @author ddj 2026年09月07号
4431
+ * @param rawPath 项目路径(原始输入)
4432
+ * @param moduleUrl 模块 URL(测试注入)
4433
+ * @returns 项目状态
4434
+ */
4435
+ async function unityEntryOf(rawPath, moduleUrl = import.meta.url) {
4436
+ const root = normalizeUnityRoot(rawPath);
4437
+ const entry = {
4438
+ path: root,
4439
+ title: basename(root) || root,
4440
+ installedVersion: null,
4441
+ upToDate: false,
4442
+ missingDir: false
4443
+ };
4444
+ let names = [];
4445
+ try {
4446
+ names = await readdir(root);
4447
+ } catch {
4448
+ entry.missingDir = true;
4449
+ return entry;
4450
+ }
4451
+ if (!isUnityProject(names)) {
4452
+ entry.error = "不是 Unity 项目根(缺少 Assets 或 ProjectSettings 目录)";
4453
+ return entry;
4454
+ }
4455
+ const installed = await readPackageVersion(join(unityTargetOf(root), "package.json"));
4456
+ entry.installedVersion = installed;
4457
+ const sourceVersion = await readPackageVersion(join(unitySourceDir(moduleUrl), "package.json"));
4458
+ entry.upToDate = installed !== null && installed === sourceVersion;
4459
+ return entry;
4460
+ }
4461
+ /**
4462
+ * 列出登记项目与包源版本。
4463
+ * @author ddj 2026年09月07号
4464
+ * @param moduleUrl 模块 URL(测试注入)
4465
+ * @param home DSH home(测试注入)
4466
+ * @returns 列表载荷
4467
+ */
4468
+ async function unityList(moduleUrl = import.meta.url, home = dshHome()) {
4469
+ const source = unitySourceDir(moduleUrl);
4470
+ const records = await readProjects(home);
4471
+ const projects = [];
4472
+ for (const record of records) projects.push(await unityEntryOf(record.path, moduleUrl));
4473
+ return {
4474
+ sourceVersion: await readPackageVersion(join(source, "package.json")),
4475
+ sourcePath: source,
4476
+ projects
4477
+ };
4478
+ }
4479
+ /**
4480
+ * 登记一个 Unity 项目(校验通过才入清单,重复登记幂等)。
4481
+ * @author ddj 2026年09月07号
4482
+ * @param rawPath 项目路径(原始输入)
4483
+ * @param moduleUrl 模块 URL(测试注入)
4484
+ * @param home DSH home(测试注入)
4485
+ * @returns 项目状态
4486
+ */
4487
+ async function unityAdd(rawPath, moduleUrl = import.meta.url, home = dshHome()) {
4488
+ const root = normalizeUnityRoot(rawPath);
4489
+ const entry = await unityEntryOf(root, moduleUrl);
4490
+ if (entry.missingDir) throw new Error("目录不存在:" + root);
4491
+ if (entry.error) throw new Error(entry.error);
4492
+ const records = await readProjects(home);
4493
+ if (!records.some((record) => normalizeUnityRoot(record.path) === root)) {
4494
+ records.push({
4495
+ path: root,
4496
+ addedAt: (/* @__PURE__ */ new Date()).toISOString()
4497
+ });
4498
+ await writeProjects(records, home);
4499
+ }
4500
+ return entry;
4501
+ }
4502
+ /**
4503
+ * 移除一个项目的登记(不删除已安装的包目录)。
4504
+ * @author ddj 2026年09月07号
4505
+ * @param rawPath 项目路径(原始输入)
4506
+ * @param home DSH home(测试注入)
4507
+ */
4508
+ async function unityRemove(rawPath, home = dshHome()) {
4509
+ const root = normalizeUnityRoot(rawPath);
4510
+ await writeProjects((await readProjects(home)).filter((record) => normalizeUnityRoot(record.path) !== root), home);
4511
+ }
4512
+ /**
4513
+ * 一键安装/更新:整目录替换 <root>/Packages/com.dsh.editor 为包源内容。
4514
+ * @author ddj 2026年09月07号
4515
+ * @param rawPath 项目路径(原始输入)
4516
+ * @param moduleUrl 模块 URL(测试注入)
4517
+ * @returns 安装后项目状态
4518
+ */
4519
+ async function unityInstall(rawPath, moduleUrl = import.meta.url) {
4520
+ const root = normalizeUnityRoot(rawPath);
4521
+ const source = unitySourceDir(moduleUrl);
4522
+ const target = unityTargetOf(root);
4523
+ await validateInstallTarget(root, source, target);
4524
+ await rm(target, {
4525
+ recursive: true,
4526
+ force: true
4527
+ });
4528
+ await mkdir(join(target, ".."), { recursive: true });
4529
+ await cp(source, target, { recursive: true });
4530
+ return unityEntryOf(root, moduleUrl);
4531
+ }
4532
+ /**
4533
+ * 安装前校验:Unity 项目根 + 目标不与包源重叠 + 已存在目标必须是本包。
4534
+ * @author ddj 2026年09月07号
4535
+ * @param root 项目根
4536
+ * @param source 包源目录
4537
+ * @param target 目标目录
4538
+ */
4539
+ async function validateInstallTarget(root, source, target) {
4540
+ let names = [];
4541
+ try {
4542
+ names = await readdir(root);
4543
+ } catch {
4544
+ throw new Error("目录不存在:" + root);
4545
+ }
4546
+ if (!isUnityProject(names)) throw new Error("不是 Unity 项目根(缺少 Assets 或 ProjectSettings 目录)");
4547
+ if (target === source || target.startsWith(source + sep) || source.startsWith(target + sep)) throw new Error("目标目录与包源重叠,拒绝安装");
4548
+ const text = await readFile(join(target, "package.json"), "utf8").catch((error) => {
4549
+ if (error?.code === "ENOENT") return null;
4550
+ throw error;
4551
+ });
4552
+ if (text === null) return;
4553
+ let name = null;
4554
+ try {
4555
+ name = JSON.parse(text).name;
4556
+ } catch {}
4557
+ if (name !== "com.dsh.editor") throw new Error("目标目录已存在且不是 DSH 包,拒绝覆盖:" + target);
4558
+ }
4559
+ //#endregion
4560
+ //#region src/integrate.ts
4561
+ /**
4562
+ * dsh-vscode-mode host — 资源管理器右键菜单「在 DSH 文件编辑中打开」(跨平台)。
4563
+ * - Windows:launcher(assets/shell/dsh-open.cs 经 csc 编译 WinExe,csc 缺失降级 ps1)安装到
4564
+ * ~/.dsh/dsh-vscode-mode/shell/,HKCU\Software\Classes 三类菜单键(`*`/Directory/Directory\Background)。
4565
+ * - Linux:dsh-open.sh + GNOME Files(Nautilus)右键脚本 + KDE Dolphin 服务菜单(纯文件写入)。
4566
+ * - macOS:dsh-open.sh(Finder 快速操作由用户按配方手动创建,自动检测/移除)。
4567
+ * 生命周期:注册成功写 marker(shell/registered.json)→ 插件卸载/reload 清理注册痕迹,
4568
+ * 启动时 marker 存在则自动恢复(更新插件不丢注册);用户显式「移除注册」删 marker 永不自动恢复。
4569
+ * 所有 reg/csc 调用经 subprocess 服务 argv 数组(卸载清理用 child_process 直调,teardown 可靠)。
4570
+ * 纯函数(键路径/命令行/ini/脚本内容)可单测。
4571
+ * 作者 ddj 2026-09-07
4572
+ */
4573
+ /** 菜单显示名(Windows 键默认值 / Linux 菜单项名 / macOS workflow 名)。 */
4574
+ const MENU_LABEL = "在 DSH 文件编辑中打开";
4575
+ /** 编译 launcher 文件名(Windows)。 */
4576
+ const LAUNCHER_EXE = "dsh-open.exe";
4577
+ /** launcher C# 源文件名。 */
4578
+ const LAUNCHER_CS = "dsh-open.cs";
4579
+ /** 降级 launcher 脚本名(Windows)。 */
4580
+ const LAUNCHER_PS1 = "dsh-open.ps1";
4581
+ /** POSIX launcher 脚本名(Linux/macOS 通用)。 */
4582
+ const LAUNCHER_SH = "dsh-open.sh";
4583
+ /** 深链基址配置文件名。 */
4584
+ const INI_NAME = "dsh-open.ini";
4585
+ /** 注册 marker 文件名(存在 = 用户注册过且未显式移除)。 */
4586
+ const MARKER_NAME = "registered.json";
4587
+ /** Icon 值名。 */
4588
+ const REG_VALUE_NAME_ICON = "Icon";
4589
+ /** Nautilus 右键脚本文件名。 */
4590
+ const NAUTILUS_NAME = MENU_LABEL;
4591
+ /** Dolphin 服务菜单文件名。 */
4592
+ const DOLPHIN_NAME = "dsh-editor.desktop";
4593
+ /** Finder 快速操作(workflow 包)目录名。 */
4594
+ const FINDER_NAME = MENU_LABEL + ".workflow";
4595
+ /** DSH 小鲸鱼图标文件名(右键菜单 Icon,注册时复制到安装目录)。 */
4596
+ const WHALE_ICO = "dsh-whale.ico";
4597
+ /**
4598
+ * reg.exe 路径(SystemRoot 派生;env 可注入便于测试)。
4599
+ * @author ddj 2026年09月07号
4600
+ * @param env 环境映射(缺省 process.env)
4601
+ * @returns reg.exe 绝对路径
4602
+ */
4603
+ function regExe(env = process.env) {
4604
+ return join(systemRoot(env), "System32", "reg.exe");
4605
+ }
4606
+ /**
4607
+ * csc.exe 候选列表(.NET Framework 4.x 常驻路径,按存在性取首个)。
4608
+ * @author ddj 2026年09月07号
4609
+ * @param env 环境映射(缺省 process.env)
4610
+ * @returns 候选绝对路径列表
4611
+ */
4612
+ function cscCandidates(env = process.env) {
4613
+ const root = systemRoot(env);
4614
+ return [join(root, "Microsoft.NET", "Framework64", "v4.0.30319", "csc.exe"), join(root, "Microsoft.NET", "Framework", "v4.0.30319", "csc.exe")];
4615
+ }
4616
+ /**
4617
+ * Windows 三类菜单键完整路径。
4618
+ * @author ddj 2026年09月07号
4619
+ * @param leaf 菜单键叶子名(缺省 DSHEditor)
4620
+ * @returns files/dir/background 三个键路径
4621
+ */
4622
+ function menuKeyPaths(leaf = SHELL_MENU_LEAF) {
4623
+ const root = "HKCU\\Software\\Classes";
4624
+ return {
4625
+ files: root + "\\*\\shell\\" + leaf,
4626
+ dir: root + "\\Directory\\shell\\" + leaf,
4627
+ background: root + "\\Directory\\Background\\shell\\" + leaf
4628
+ };
4629
+ }
4630
+ /**
4631
+ * launcher 安装目录(~/.dsh/dsh-vscode-mode/shell)。
4632
+ * @author ddj 2026年09月07号
4633
+ * @param home DSH home(测试可注入)
4634
+ * @returns 安装目录绝对路径
4635
+ */
4636
+ function shellInstallDir(home = dshHome()) {
4637
+ return join(home, PLUGIN_ID, "shell");
4638
+ }
4639
+ /**
4640
+ * Nautilus 右键脚本路径(~/.local/share/nautilus/scripts/)。
4641
+ * @author ddj 2026年09月07号
4642
+ * @param home 用户 home(测试可注入)
4643
+ */
4644
+ function nautilusPath(home = homedir()) {
4645
+ return join(home, ".local", "share", "nautilus", "scripts", NAUTILUS_NAME);
4646
+ }
4647
+ /**
4648
+ * Dolphin 服务菜单路径(新 kio/servicemenus + 旧 kservices5,两个都写)。
4649
+ * @author ddj 2026年09月07号
4650
+ * @param home 用户 home(测试可注入)
4651
+ */
4652
+ function dolphinMenuPaths(home = homedir()) {
4653
+ return [join(home, ".local", "share", "kio", "servicemenus", DOLPHIN_NAME), join(home, ".local", "share", "kservices5", "ServiceMenus", DOLPHIN_NAME)];
4654
+ }
4655
+ /**
4656
+ * Finder 快速操作 workflow 包路径(~/Library/Services/)。
4657
+ * @author ddj 2026年09月07号
4658
+ * @param home 用户 home(测试可注入)
4659
+ */
4660
+ function finderWorkflowPath(home = homedir()) {
4661
+ return join(home, "Library", "Services", FINDER_NAME);
4662
+ }
4663
+ /**
4664
+ * 注册 marker 文件路径(shell/registered.json)。
4665
+ * @author ddj 2026年09月07号
4666
+ * @param home DSH home(测试可注入)
4667
+ */
4668
+ function markerFile(home = dshHome()) {
4669
+ return join(shellInstallDir(home), MARKER_NAME);
4670
+ }
4671
+ /**
4672
+ * command 默认值(Windows 编译 exe 版)。
4673
+ * @author ddj 2026年09月07号
4674
+ * @param exePath launcher 绝对路径
4675
+ * @param token 路径占位符
4676
+ * @returns 注册表 command 值
4677
+ */
4678
+ function commandValue(exePath, token = "%1") {
4679
+ return "\"" + exePath + "\" \"" + token + "\"";
4680
+ }
4681
+ /**
4682
+ * command 默认值(Windows 降级 ps1 版:隐藏窗口运行脚本)。
4683
+ * @author ddj 2026年09月07号
4684
+ * @param ps1Path launcher 脚本绝对路径
4685
+ * @param token 路径占位符
4686
+ * @param powerShell powershell.exe 路径(测试可注入)
4687
+ * @returns 注册表 command 值
4688
+ */
4689
+ function ps1CommandValue(ps1Path, token = "%1", powerShell = powerShellExe()) {
4690
+ return "\"" + powerShell + "\" -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File \"" + ps1Path + "\" \"" + token + "\"";
4691
+ }
4692
+ /**
4693
+ * dsh-open.ini 内容(base 行驱动 launcher 深链基址,三平台共用)。
4694
+ * @author ddj 2026年09月07号
4695
+ * @param baseUrl 深链基址
4696
+ * @returns ini 文件全文
4697
+ */
4698
+ function iniContent(baseUrl) {
4699
+ return "[dsh]\nbase=" + (baseUrl.trim() || "http://127.0.0.1:3080") + "\n";
4700
+ }
4701
+ /**
4702
+ * Nautilus 右键脚本内容(多选经 NAUTILUS_SCRIPT_SELECTED_FILE_PATHS 一次传给 launcher)。
4703
+ * @author ddj 2026年09月07号
4704
+ * @param launcherPath dsh-open.sh 绝对路径
4705
+ * @returns 脚本全文(#!/usr/bin/env bash)
4706
+ */
4707
+ function nautilusScript(launcherPath) {
4708
+ return [
4709
+ "#!/usr/bin/env bash",
4710
+ "# 由 dsh-vscode-mode 生成:GNOME Files(Nautilus)右键脚本 → DSH 文件编辑",
4711
+ "launcher=\"$HOME/.dsh/dsh-vscode-mode/shell/dsh-open.sh\"",
4712
+ "args=()",
4713
+ "while IFS= read -r line; do",
4714
+ " [ -n \"$line\" ] && args+=(\"$line\")",
4715
+ "done <<< \"${NAUTILUS_SCRIPT_SELECTED_FILE_PATHS:-}\"",
4716
+ "exec \"$launcher\" \"${args[@]}\"",
4717
+ ""
4718
+ ].join("\n");
4719
+ }
4720
+ /**
4721
+ * Dolphin 服务菜单内容(%F 多选传参)。
4722
+ * @author ddj 2026年09月07号
4723
+ * @param launcherPath dsh-open.sh 绝对路径
4724
+ * @returns .desktop 全文
4725
+ */
4726
+ function dolphinMenu(launcherPath) {
4727
+ return [
4728
+ "[Desktop Entry]",
4729
+ "Type=Service",
4730
+ "X-KDE-ServiceTypes=KonqPopupMenu/Plugin",
4731
+ "MimeType=all/allfiles;inode/directory;",
4732
+ "Actions=openInDshEditor",
4733
+ "[Desktop Action openInDshEditor]",
4734
+ "Name=" + MENU_LABEL,
4735
+ "Icon=utilities-terminal",
4736
+ "Exec=\"" + launcherPath + "\" %F",
4737
+ ""
4738
+ ].join("\n");
4739
+ }
4740
+ /** SystemRoot 解析(env 可注入)。 */
4741
+ function systemRoot(env) {
4742
+ return env.SystemRoot || "C:\\Windows";
4743
+ }
4744
+ /** powershell.exe 路径(Windows 降级注册用)。 */
4745
+ function powerShellExe(env = process.env) {
4746
+ return join(systemRoot(env), "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
4747
+ }
4748
+ /** POSIX launcher 脚本安装路径。 */
4749
+ function shLauncherPath(home = dshHome()) {
4750
+ return join(shellInstallDir(home), LAUNCHER_SH);
4751
+ }
4752
+ /**
4753
+ * 经 subprocess 服务运行一个命令(argv 数组,无 shell 插值)。
4754
+ * ⚠️ stdio 必须 'inherit':受管 host 环境禁止管道 stdio(node spawn pipe → EPERM),
4755
+ * inherit 直写宿主控制台且不建管道;本模块只需退出码(reg query 缺键 = 退出码 1)。
4756
+ * done 契约宽松归一化:{exitCode,signal} / {code} / undefined / 抛错(spawn 失败)。
4757
+ * @author ddj 2026年09月07号
4758
+ * @param ctx DSH 上下文
4759
+ * @param argv 命令与参数
4760
+ * @param cwd 工作目录(须已存在)
4761
+ * @returns 退出码(0=成功)
4762
+ */
4763
+ async function runArgv(ctx, argv, cwd = dshHome()) {
4764
+ const sub = ctx.get("subprocess");
4765
+ if (!sub || typeof sub.spawn !== "function") throw new Error("缺少 subprocess 服务");
4766
+ let handle;
4767
+ try {
4768
+ handle = sub.spawn({
4769
+ argv,
4770
+ cwd,
4771
+ stdio: {
4772
+ stdout: "inherit",
4773
+ stderr: "inherit",
4774
+ stdin: "ignore"
4775
+ },
4776
+ graceMs: 1e4
4777
+ });
4778
+ } catch (error) {
4779
+ return {
4780
+ code: 1,
4781
+ output: String(error)
4782
+ };
4783
+ }
4784
+ try {
4785
+ return normalizeOutcome(await handle.done);
4786
+ } catch (error) {
4787
+ return {
4788
+ code: 1,
4789
+ output: String(error)
4790
+ };
4791
+ }
4792
+ }
4793
+ /** 归一化 done 结果(契约宽松:{exitCode,signal} / {code} / 字符串 / undefined)。 */
4794
+ function normalizeOutcome(result) {
4795
+ if (result == null) return {
4796
+ code: 0,
4797
+ output: ""
4798
+ };
4799
+ if (typeof result === "string") return {
4800
+ code: 0,
4801
+ output: result
4802
+ };
4803
+ const record = result;
4804
+ if (typeof record.exitCode === "number") return {
4805
+ code: record.exitCode,
4806
+ output: ""
4807
+ };
4808
+ if (typeof record.code === "number") return {
4809
+ code: record.code,
4810
+ output: ""
4811
+ };
4812
+ return {
4813
+ code: 0,
4814
+ output: ""
4815
+ };
4816
+ }
4817
+ /** 文件存在性探测(best-effort)。 */
4818
+ async function fileExists(path) {
4819
+ try {
4820
+ return (await stat(path)).isFile();
4821
+ } catch {
4822
+ return false;
4823
+ }
4824
+ }
4825
+ /** Windows:探测一个注册表键是否存在(reg query 缺键 = 退出码 1,无需读输出)。 */
4826
+ async function keyExists(ctx, key) {
4827
+ return (await runArgv(ctx, [
4828
+ regExe(),
4829
+ "query",
4830
+ key,
4831
+ "/ve"
4832
+ ])).code === 0;
4833
+ }
4834
+ /** 读注册 marker(缺失/非法 → null)。 */
4835
+ async function readMarker(home = dshHome()) {
4836
+ try {
4837
+ const data = JSON.parse(await readFile(markerFile(home), "utf8"));
4838
+ if (typeof data?.baseUrl !== "string" || !data.baseUrl) return null;
4839
+ return {
4840
+ baseUrl: data.baseUrl,
4841
+ at: typeof data.at === "string" ? data.at : ""
4842
+ };
4843
+ } catch {
4844
+ return null;
4845
+ }
4846
+ }
4847
+ /** 写注册 marker(父目录已存在)。 */
4848
+ async function saveMarker(baseUrl, home = dshHome()) {
4849
+ const data = {
4850
+ baseUrl,
4851
+ at: (/* @__PURE__ */ new Date()).toISOString()
4852
+ };
4853
+ await writeFile(markerFile(home), JSON.stringify(data, null, 2) + "\n", "utf8");
4854
+ }
4855
+ /** 复制 POSIX launcher 并加执行位(三平台 ini 同写)。 */
4856
+ async function installPosix(dir, baseUrl) {
4857
+ await copyFile(join(assetsDirOf(import.meta.url), "shell", LAUNCHER_SH), join(dir, LAUNCHER_SH));
4858
+ await chmod(join(dir, LAUNCHER_SH), 493);
4859
+ await writeFile(join(dir, INI_NAME), iniContent(baseUrl), "utf8");
4860
+ }
4861
+ /** 写 Nautilus 右键脚本(父目录按需创建 + 执行位)。 */
4862
+ async function writeNautilus(home, launcherPath) {
4863
+ const script = nautilusPath(home);
4864
+ await mkdir(join(script, ".."), { recursive: true });
4865
+ await writeFile(script, nautilusScript(launcherPath), "utf8");
4866
+ await chmod(script, 493);
4867
+ }
4868
+ /** 写 Dolphin 服务菜单(新/旧两个路径,父目录按需创建)。 */
4869
+ async function writeDolphinMenus(home, launcherPath) {
4870
+ for (const menu of dolphinMenuPaths(home)) {
4871
+ await mkdir(join(menu, ".."), { recursive: true });
4872
+ await writeFile(menu, dolphinMenu(launcherPath), "utf8");
4873
+ }
4874
+ }
4875
+ /** Linux:移除 Nautilus 脚本与 Dolphin 服务菜单(仅本插件自有文件)。 */
4876
+ async function removePosixFiles(home) {
4877
+ await rm(nautilusPath(home), { force: true });
4878
+ for (const menu of dolphinMenuPaths(home)) await rm(menu, { force: true });
4879
+ }
4880
+ /** macOS:移除 Finder 快速操作(仅当其内容引用本插件 launcher,防误删用户手工产物)。 */
4881
+ async function dropFinderMenu(home) {
4882
+ const dir = finderWorkflowPath(home);
4883
+ let text = "";
4884
+ try {
4885
+ text = await readFile(join(dir, "Contents", "document.wflow"), "utf8");
4886
+ } catch {
4887
+ return;
4888
+ }
4889
+ if (!text.includes("dsh-open")) return;
4890
+ await rm(dir, {
4891
+ recursive: true,
4892
+ force: true
4893
+ });
4894
+ }
4895
+ /** 卸载清理专用:直调 reg delete(subprocess 服务在 teardown 可能已卸载)。⚠️ stdio 'ignore':受管环境禁管道。 */
4896
+ function regDeleteDirect(key) {
4897
+ return new Promise((resolve) => {
4898
+ try {
4899
+ const child = spawn(regExe(), [
4900
+ "delete",
4901
+ key,
4902
+ "/f"
4903
+ ], {
4904
+ windowsHide: true,
4905
+ timeout: 5e3,
4906
+ stdio: "ignore"
4907
+ });
4908
+ child.once("close", () => resolve());
4909
+ child.once("error", () => resolve());
4910
+ } catch {
4911
+ resolve();
4912
+ }
4913
+ });
4914
+ }
4915
+ /**
4916
+ * 查询系统集成状态(按平台探测条目注册状态 + launcher 安装态)。
4917
+ * @author ddj 2026年09月07号
4918
+ * @param ctx DSH 上下文
4919
+ * @param baseUrl 深链基址(设置 integrationBaseUrl)
4920
+ * @returns 集成状态
4921
+ */
4922
+ async function shellMenuStatus(ctx, baseUrl) {
4923
+ const dir = shellInstallDir();
4924
+ const platform = process.platform;
4925
+ let launcher;
4926
+ let entries = [];
4927
+ if (platform === "win32") {
4928
+ const exe = join(dir, LAUNCHER_EXE);
4929
+ const ps1 = join(dir, LAUNCHER_PS1);
4930
+ const compiled = await fileExists(exe);
4931
+ const ps1Present = await fileExists(ps1);
4932
+ launcher = {
4933
+ present: compiled || ps1Present,
4934
+ compiled,
4935
+ path: compiled ? exe : ps1
4936
+ };
4937
+ entries = [
4938
+ {
4939
+ id: "files",
4940
+ label: "文件",
4941
+ registered: await keyExists(ctx, menuKeyPaths().files)
4942
+ },
4943
+ {
4944
+ id: "dir",
4945
+ label: "文件夹",
4946
+ registered: await keyExists(ctx, menuKeyPaths().dir)
4947
+ },
4948
+ {
4949
+ id: "dirBackground",
4950
+ label: "文件夹空白处",
4951
+ registered: await keyExists(ctx, menuKeyPaths().background)
4952
+ }
4953
+ ];
4954
+ } else {
4955
+ const sh = shLauncherPath();
4956
+ launcher = {
4957
+ present: await fileExists(sh),
4958
+ compiled: false,
4959
+ path: sh
4960
+ };
4961
+ if (platform === "linux") {
4962
+ const menus = dolphinMenuPaths();
4963
+ entries = [{
4964
+ id: "nautilus",
4965
+ label: "GNOME Files(Nautilus)",
4966
+ registered: await fileExists(nautilusPath())
4967
+ }, {
4968
+ id: "dolphin",
4969
+ label: "KDE Dolphin",
4970
+ registered: await fileExists(menus[0]) || await fileExists(menus[1])
4971
+ }];
4972
+ } else if (platform === "darwin") entries = [{
4973
+ id: "finder",
4974
+ label: "Finder 快速操作",
4975
+ registered: await fileExists(finderWorkflowPath()),
4976
+ manual: true
4977
+ }];
4978
+ }
4979
+ return {
4980
+ platform,
4981
+ launcher,
4982
+ entries,
4983
+ baseUrl,
4984
+ unityPackagePath: unitySourceDir()
4985
+ };
4986
+ }
4987
+ /**
4988
+ * 注册右键菜单(平台分发):写 ini → 安装 launcher → 首次备份(Windows)→ 写菜单;
4989
+ * 成功后写 marker(驱动卸载清理与重启自动恢复)。
4990
+ * @author ddj 2026年09月07号
4991
+ * @param ctx DSH 上下文
4992
+ * @param baseUrl 深链基址
4993
+ * @returns 注册后状态
4994
+ */
4995
+ async function shellMenuRegister(ctx, baseUrl) {
4996
+ const dir = shellInstallDir();
4997
+ await mkdir(dir, { recursive: true });
4998
+ if (process.platform === "win32") {
4999
+ await writeFile(join(dir, INI_NAME), iniContent(baseUrl), "utf8");
5000
+ await installIcon(dir);
5001
+ const compiled = await installLauncher(ctx, dir, true);
5002
+ if (!await readMarker()) await backupKeys(ctx, dir);
5003
+ await writeMenuKeys(ctx, dir, compiled);
5004
+ } else if (process.platform === "linux") {
5005
+ await installPosix(dir, baseUrl);
5006
+ const launcherPath = join(dir, LAUNCHER_SH);
5007
+ await writeNautilus(homedir(), launcherPath);
5008
+ await writeDolphinMenus(homedir(), launcherPath);
5009
+ } else if (process.platform === "darwin") await installPosix(dir, baseUrl);
5010
+ await saveMarker(baseUrl);
5011
+ return shellMenuStatus(ctx, baseUrl);
5012
+ }
5013
+ /**
5014
+ * 移除右键菜单注册(平台分发)+ 删除 marker(显式移除后重启不再自动恢复)。
5015
+ * @author ddj 2026年09月07号
5016
+ * @param ctx DSH 上下文
5017
+ * @param baseUrl 深链基址
5018
+ * @returns 移除后状态
5019
+ */
5020
+ async function shellMenuRemove(ctx, baseUrl) {
5021
+ if (process.platform === "win32") {
5022
+ const paths = menuKeyPaths();
5023
+ for (const key of [
5024
+ paths.files,
5025
+ paths.dir,
5026
+ paths.background
5027
+ ]) await runArgv(ctx, [
5028
+ regExe(),
5029
+ "delete",
5030
+ key,
5031
+ "/f"
5032
+ ]);
5033
+ } else if (process.platform === "linux") await removePosixFiles(homedir());
5034
+ else if (process.platform === "darwin") await dropFinderMenu(homedir());
5035
+ await rm(markerFile(), { force: true });
5036
+ return shellMenuStatus(ctx, baseUrl);
5037
+ }
5038
+ /**
5039
+ * 系统集成生命周期(index.ts 经 ctx.effect 挂载):
5040
+ * 启动时 marker 存在 → 幂等自动恢复注册(插件更新/reload 不丢注册);
5041
+ * 返回的 disposer 在插件卸载/reload 时清理注册痕迹(marker 保留供下次恢复)。
5042
+ * @author ddj 2026年09月07号
5043
+ * @param ctx DSH 上下文
5044
+ * @returns 卸载清理 disposer
5045
+ */
5046
+ function shellMenuLifecycle(ctx) {
5047
+ restoreOnLoad(ctx).catch(() => {});
5048
+ return () => {
5049
+ cleanupOnUnload().catch(() => {});
5050
+ };
5051
+ }
5052
+ /** 启动自动恢复:marker 存在才执行(幂等;exe 已存在跳过编译)。 */
5053
+ async function restoreOnLoad(ctx) {
5054
+ const marker = await readMarker();
5055
+ if (!marker) return;
5056
+ await shellMenuRegister(ctx, marker.baseUrl);
5057
+ }
5058
+ /** 卸载清理:marker 存在 → 按平台移除注册痕迹(launcher 文件与 marker 保留)。 */
5059
+ async function cleanupOnUnload() {
5060
+ if (!await readMarker()) return;
5061
+ if (process.platform === "win32") {
5062
+ const paths = menuKeyPaths();
5063
+ for (const key of [
5064
+ paths.files,
5065
+ paths.dir,
5066
+ paths.background
5067
+ ]) await regDeleteDirect(key);
5068
+ } else if (process.platform === "linux") await removePosixFiles(homedir());
5069
+ else if (process.platform === "darwin") await dropFinderMenu(homedir());
5070
+ }
5071
+ /**
5072
+ * 安装 Windows launcher:优先 csc 编译 exe(无控制台闪窗),任一候选失败降级复制 ps1。
5073
+ * @author ddj 2026年09月07号
5074
+ * @param ctx DSH 上下文
5075
+ * @param dir 安装目录(已存在)
5076
+ * @param force 是否强制重编译(自动恢复时 false:exe 已存在直接复用)
5077
+ * @returns 是否编译成功(false=降级 ps1)
5078
+ */
5079
+ async function installLauncher(ctx, dir, force) {
5080
+ const exe = join(dir, LAUNCHER_EXE);
5081
+ if (!force && await fileExists(exe)) return true;
5082
+ const shellAssets = join(assetsDirOf(import.meta.url), "shell");
5083
+ for (const candidate of cscCandidates()) {
5084
+ if (!await fileExists(candidate)) continue;
5085
+ try {
5086
+ await copyFile(join(shellAssets, LAUNCHER_CS), join(dir, LAUNCHER_CS));
5087
+ if ((await runArgv(ctx, [
5088
+ candidate,
5089
+ "/nologo",
5090
+ "/target:winexe",
5091
+ "/r:System.Windows.Forms.dll",
5092
+ "/r:System.Web.Extensions.dll",
5093
+ "/out:" + exe,
5094
+ join(dir, "dsh-open.cs")
5095
+ ], dir)).code === 0 && await fileExists(exe)) return true;
5096
+ } catch {}
5097
+ }
5098
+ await copyFile(join(shellAssets, LAUNCHER_PS1), join(dir, LAUNCHER_PS1));
5099
+ return false;
5100
+ }
5101
+ /**
5102
+ * 首次注册前备份 Windows 三类既有键(best-effort,键不存在时导出失败忽略)。
5103
+ * @author ddj 2026年09月07号
5104
+ * @param ctx DSH 上下文
5105
+ * @param dir 备份文件落盘目录
5106
+ */
5107
+ async function backupKeys(ctx, dir) {
5108
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
5109
+ const paths = menuKeyPaths();
5110
+ const targets = [
5111
+ [paths.files, "files"],
5112
+ [paths.dir, "dir"],
5113
+ [paths.background, "background"]
5114
+ ];
5115
+ for (const [key, name] of targets) await runArgv(ctx, [
5116
+ regExe(),
5117
+ "export",
5118
+ key,
5119
+ join(dir, "backup-" + name + "-" + stamp + ".reg"),
5120
+ "/y"
5121
+ ], dir);
5122
+ }
5123
+ /** 复制 DSH 小鲸鱼 ico 到安装目录(右键菜单 Icon 用;缺失不阻塞注册)。 */
5124
+ async function installIcon(dir) {
5125
+ try {
5126
+ await copyFile(join(assetsDirOf(import.meta.url), "shell", WHALE_ICO), join(dir, WHALE_ICO));
5127
+ } catch {}
5128
+ }
5129
+ /**
5130
+ * 写 Windows 三类菜单键(显示名 + Icon + command 子键)。
5131
+ * @author ddj 2026年09月07号
5132
+ * @param ctx DSH 上下文
5133
+ * @param dir launcher 安装目录
5134
+ * @param compiled 是否为编译 exe(否则 ps1 command)
5135
+ */
5136
+ async function writeMenuKeys(ctx, dir, compiled) {
5137
+ const launcherPath = compiled ? join(dir, LAUNCHER_EXE) : join(dir, LAUNCHER_PS1);
5138
+ const iconPath = await fileExists(join(dir, "dsh-whale.ico")) ? join(dir, WHALE_ICO) : launcherPath;
5139
+ const keys = [
5140
+ [menuKeyPaths().files, "%1"],
5141
+ [menuKeyPaths().dir, "%1"],
5142
+ [menuKeyPaths().background, "%V"]
5143
+ ];
5144
+ for (const [key, token] of keys) await addMenuKey(ctx, key, iconPath, compiled ? commandValue(launcherPath, token) : ps1CommandValue(launcherPath, token));
5145
+ }
5146
+ /** 写一个 Windows 菜单键:默认值(显示名)+ Icon + command 子键。 */
5147
+ async function addMenuKey(ctx, key, iconPath, command) {
5148
+ const reg = regExe();
5149
+ await runArgv(ctx, [
5150
+ reg,
5151
+ "add",
5152
+ key,
5153
+ "/ve",
5154
+ "/t",
5155
+ "REG_SZ",
5156
+ "/d",
5157
+ MENU_LABEL,
5158
+ "/f"
5159
+ ]);
5160
+ await runArgv(ctx, [
5161
+ reg,
5162
+ "add",
5163
+ key,
5164
+ "/v",
5165
+ REG_VALUE_NAME_ICON,
5166
+ "/t",
5167
+ "REG_SZ",
5168
+ "/d",
5169
+ iconPath,
5170
+ "/f"
5171
+ ]);
5172
+ await runArgv(ctx, [
5173
+ reg,
5174
+ "add",
5175
+ key + "\\command",
5176
+ "/ve",
5177
+ "/t",
5178
+ "REG_SZ",
5179
+ "/d",
5180
+ command,
5181
+ "/f"
5182
+ ]);
5183
+ }
5184
+ //#endregion
5185
+ //#region src/externalHandoff.ts
5186
+ /**
5187
+ * dsh-vscode-mode host — 深链移交(避免重复打开 DSH 页面)。
5188
+ * launcher 投递待打开请求(edrv.external.handoff)→ 已打开页面 3s 轮询领取(edrv.external.pending)
5189
+ * 并就地执行打开规则;15s 内无页面轮询过 = 无活跃页面 → launcher 回退打开新页。
5190
+ * token 防多实例串扰;TTL 兜底防陈旧悬挂。模块级状态(host 单进程单例)。
5191
+ * 作者 ddj 2026-09-08
5192
+ */
5193
+ /** 待打开 TTL:超时未领取即丢弃(launcher 回退已自行处理)。 */
5194
+ const PENDING_TTL_MS = 6e4;
5195
+ /** 活跃页面判定窗口:窗口内有过 pending 轮询 = 有页面在。 */
5196
+ const PRESENCE_WINDOW_MS = 15e3;
5197
+ let pending = null;
5198
+ let lastPollAt = 0;
5199
+ /**
5200
+ * 投递一次外部打开请求;返回活跃页面数(0 = launcher 应回退开新页)与移交 token。
5201
+ * @author ddj 2026年09月08号
5202
+ * @param req 打开请求
5203
+ * @returns 活跃页面数与 token
5204
+ */
5205
+ function handoffOpen(req) {
5206
+ const token = randomUUID();
5207
+ pending = {
5208
+ ...req,
5209
+ token,
5210
+ at: Date.now()
5211
+ };
5212
+ return {
5213
+ token,
5214
+ clients: clientsAlive() ? 1 : 0
5215
+ };
5216
+ }
5217
+ /**
5218
+ * 页面轮询:刷新活跃态并领取待打开请求(取走即清,多页面仅一方执行)。
5219
+ * @author ddj 2026年09月08号
5220
+ * @returns 待打开请求或 null
5221
+ */
5222
+ function pollPending() {
5223
+ lastPollAt = Date.now();
5224
+ if (!pending) return null;
5225
+ if (Date.now() - pending.at > PENDING_TTL_MS) {
5226
+ pending = null;
5227
+ return null;
5228
+ }
5229
+ const taken = {
5230
+ paths: pending.paths,
5231
+ line: pending.line,
5232
+ column: pending.column
5233
+ };
5234
+ pending = null;
5235
+ return taken;
5236
+ }
5237
+ /**
5238
+ * 查询移交是否已被页面领取(take=true 时未领取则取走清除,供 launcher 回退开新页)。
5239
+ * @author ddj 2026年09月08号
5240
+ * @param token 移交 token
5241
+ * @param take 未领取时是否取走清除
5242
+ * @returns delivered=true = 已有页面领取(或已被更新的请求取代)
5243
+ */
5244
+ function pendingState(token, take) {
5245
+ const delivered = pending === null || pending.token !== token;
5246
+ if (take && !delivered) pending = null;
5247
+ return { delivered };
5248
+ }
5249
+ /** 活跃页面判定。 */
5250
+ function clientsAlive() {
5251
+ return Date.now() - lastPollAt <= PRESENCE_WINDOW_MS;
5252
+ }
5253
+ //#endregion
4312
5254
  //#region src/reveal.ts
4313
5255
  /**
4314
5256
  * dsh-vscode-mode host — 在 OS 文件浏览器中打开/定位路径(reveal 能力)。
@@ -5002,6 +5944,11 @@ async function latestPatchBackup(patchPath) {
5002
5944
  const base = basename(patchPath);
5003
5945
  return (await readdir(dir).catch(() => [])).filter((n) => n.startsWith(base + ".bak-")).sort().reverse()[0];
5004
5946
  }
5947
+ /** 读取设置中的深链基址(缺省/非法回退默认 3080)。 */
5948
+ async function integrationBaseUrlOf(ctx) {
5949
+ const value = (ctx.get("settings")?.describe?.({ redactSecrets: true })?.find((item) => item.ns === FILE_OPEN_SETTINGS_NS))?.value;
5950
+ return (typeof value?.integrationBaseUrl === "string" ? value.integrationBaseUrl.trim() : "") || "http://127.0.0.1:3080";
5951
+ }
5005
5952
  /** 各方法 handler 表(类型由 shared/rpc 的 RpcHandlerMap 约束)。 */
5006
5953
  function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearcher = newContentSearcher(ctx), lspHandlers) {
5007
5954
  return {
@@ -5568,6 +6515,7 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
5568
6515
  return {
5569
6516
  ok: true,
5570
6517
  fileOpenTool: normalizeFileOpenTool(value?.fileOpenTool ?? "auto"),
6518
+ integrationBaseUrl: await integrationBaseUrlOf(ctx),
5571
6519
  revision: descriptor?.revision
5572
6520
  };
5573
6521
  },
@@ -5578,12 +6526,15 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
5578
6526
  error: "设置服务不可用"
5579
6527
  };
5580
6528
  try {
5581
- await settings.update(FILE_OPEN_SETTINGS_NS, { fileOpenTool: normalizeFileOpenTool(args.fileOpenTool) }, args.expectedRevision);
6529
+ const patch = { fileOpenTool: normalizeFileOpenTool(args.fileOpenTool) };
6530
+ if (typeof args.integrationBaseUrl === "string" && args.integrationBaseUrl.trim()) patch.integrationBaseUrl = args.integrationBaseUrl.trim();
6531
+ await settings.update(FILE_OPEN_SETTINGS_NS, patch, args.expectedRevision);
5582
6532
  const descriptor = settings.describe?.({ redactSecrets: true })?.find((item) => item.ns === FILE_OPEN_SETTINGS_NS);
5583
6533
  const value = descriptor?.value;
5584
6534
  return {
5585
6535
  ok: true,
5586
6536
  fileOpenTool: normalizeFileOpenTool(value?.fileOpenTool),
6537
+ integrationBaseUrl: await integrationBaseUrlOf(ctx),
5587
6538
  revision: descriptor?.revision
5588
6539
  };
5589
6540
  } catch (error) {
@@ -5620,6 +6571,135 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
5620
6571
  };
5621
6572
  }
5622
6573
  },
6574
+ "edrv.integration.status": async () => {
6575
+ try {
6576
+ return {
6577
+ ok: true,
6578
+ ...await shellMenuStatus(ctx, await integrationBaseUrlOf(ctx))
6579
+ };
6580
+ } catch (error) {
6581
+ return {
6582
+ ok: false,
6583
+ error: String(error)
6584
+ };
6585
+ }
6586
+ },
6587
+ "edrv.integration.register": async () => {
6588
+ try {
6589
+ return {
6590
+ ok: true,
6591
+ ...await shellMenuRegister(ctx, await integrationBaseUrlOf(ctx))
6592
+ };
6593
+ } catch (error) {
6594
+ return {
6595
+ ok: false,
6596
+ error: String(error)
6597
+ };
6598
+ }
6599
+ },
6600
+ "edrv.integration.unregister": async () => {
6601
+ try {
6602
+ return {
6603
+ ok: true,
6604
+ ...await shellMenuRemove(ctx, await integrationBaseUrlOf(ctx))
6605
+ };
6606
+ } catch (error) {
6607
+ return {
6608
+ ok: false,
6609
+ error: String(error)
6610
+ };
6611
+ }
6612
+ },
6613
+ "edrv.unity.list": async () => {
6614
+ try {
6615
+ return {
6616
+ ok: true,
6617
+ ...await unityList()
6618
+ };
6619
+ } catch (error) {
6620
+ return {
6621
+ ok: false,
6622
+ error: String(error)
6623
+ };
6624
+ }
6625
+ },
6626
+ "edrv.unity.add": async (args) => {
6627
+ try {
6628
+ return {
6629
+ ok: true,
6630
+ project: await unityAdd(args.path)
6631
+ };
6632
+ } catch (error) {
6633
+ return {
6634
+ ok: false,
6635
+ error: String(error)
6636
+ };
6637
+ }
6638
+ },
6639
+ "edrv.unity.remove": async (args) => {
6640
+ try {
6641
+ await unityRemove(args.path);
6642
+ return { ok: true };
6643
+ } catch (error) {
6644
+ return {
6645
+ ok: false,
6646
+ error: String(error)
6647
+ };
6648
+ }
6649
+ },
6650
+ "edrv.unity.install": async (args) => {
6651
+ try {
6652
+ return {
6653
+ ok: true,
6654
+ project: await unityInstall(args.path)
6655
+ };
6656
+ } catch (error) {
6657
+ return {
6658
+ ok: false,
6659
+ error: String(error)
6660
+ };
6661
+ }
6662
+ },
6663
+ "edrv.externalStat": async (args) => {
6664
+ const fs = ctx.get("fs");
6665
+ if (!fs) return {
6666
+ ok: false,
6667
+ error: "缺少 fs"
6668
+ };
6669
+ try {
6670
+ const target = await fs.resolve(args.path);
6671
+ const info = await fs.stat(target);
6672
+ if (!info || info.type === "other") return {
6673
+ ok: true,
6674
+ kind: "missing"
6675
+ };
6676
+ return {
6677
+ ok: true,
6678
+ kind: info.type === "directory" ? "directory" : "file"
6679
+ };
6680
+ } catch (error) {
6681
+ return {
6682
+ ok: false,
6683
+ error: String(error)
6684
+ };
6685
+ }
6686
+ },
6687
+ "edrv.external.handoff": async (args) => ({
6688
+ ok: true,
6689
+ ...handoffOpen({
6690
+ paths: args.paths,
6691
+ line: args.line,
6692
+ column: args.column
6693
+ })
6694
+ }),
6695
+ "edrv.external.pending": async () => ({
6696
+ ok: true,
6697
+ open: pollPending()
6698
+ }),
6699
+ "edrv.external.pendingState": async (args) => ({
6700
+ ok: true,
6701
+ delivered: pendingState(args.token, args.take === true).delivered
6702
+ }),
5623
6703
  "edrv.perf.inventory": async () => {
5624
6704
  try {
5625
6705
  const home = dshHome();
@@ -8963,6 +10043,7 @@ function apply(ctx, config) {
8963
10043
  });
8964
10044
  registerRoutes(ctx, config, (method, args) => handleRpc(ctx, registry, method, args, searcher, contentSearcher, lspHandlers), (warning) => warnings.push(warning));
8965
10045
  installIsolation(ctx);
10046
+ ctx.effect(() => shellMenuLifecycle(ctx));
8966
10047
  sweepTreeCache();
8967
10048
  logCompatSummary(ctx, warnings);
8968
10049
  ctx.effect(() => () => {