dsh-vscode-mode 0.1.30 → 0.1.35

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 (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +30 -0
  3. package/lib/client.js +1439 -46
  4. package/lib/client.js.map +1 -1
  5. package/lib/index.js +3519 -222
  6. package/lib/index.js.map +1 -1
  7. package/package.json +2 -2
  8. package/src/capture.ts +3 -0
  9. package/src/client/events.ts +11 -0
  10. package/src/client/index.ts +20 -2
  11. package/src/client/monaco/lsp/index.ts +30 -0
  12. package/src/client/monaco/lsp/lspClient.ts +192 -0
  13. package/src/client/monaco/lsp/providers.ts +229 -0
  14. package/src/client/outline/OutlinePanel.tsx +1 -1
  15. package/src/client/outline/lspSource.ts +49 -0
  16. package/src/client/outline/parse.ts +31 -1
  17. package/src/client/outline/types.ts +2 -0
  18. package/src/client/paths.ts +36 -0
  19. package/src/client/sidebar/panels/FileExplorer.ts +95 -30
  20. package/src/client/sidebar/panels/SearchPanel.ts +11 -2
  21. package/src/client/state/explorerCache.ts +3 -1
  22. package/src/client/state/explorerEntriesCache.ts +172 -0
  23. package/src/client/state/viewStateCache.ts +3 -1
  24. package/src/client/styles/editor.css +16 -0
  25. package/src/client/styles/mcp.css +63 -0
  26. package/src/client/ui/EditorView.ts +61 -6
  27. package/src/client/ui/LspSettings.ts +268 -0
  28. package/src/client/ui/McpSettings.ts +5 -1
  29. package/src/client/ui/PerfSettings.ts +242 -0
  30. package/src/client/ui/PerfSizeBadge.ts +54 -0
  31. package/src/compat.ts +3 -2
  32. package/src/fileOpenSettings.ts +1 -0
  33. package/src/index.ts +25 -2
  34. package/src/lsp/client.ts +205 -0
  35. package/src/lsp/config.ts +107 -0
  36. package/src/lsp/extmgr.ts +208 -0
  37. package/src/lsp/jsonrpc.ts +86 -0
  38. package/src/lsp/manager.ts +133 -0
  39. package/src/lsp/providers.ts +235 -0
  40. package/src/lsp/rpc.ts +334 -0
  41. package/src/lsp/server.ts +346 -0
  42. package/src/lsp/settings.ts +44 -0
  43. package/src/lsp/transport.ts +143 -0
  44. package/src/lsp/uri.ts +62 -0
  45. package/src/lsp/zip.ts +82 -0
  46. package/src/paths.ts +284 -0
  47. package/src/perf.ts +340 -0
  48. package/src/perfPatch.ts +76 -0
  49. package/src/routes.ts +7 -12
  50. package/src/rpc.ts +184 -36
  51. package/src/search/content.ts +14 -7
  52. package/src/search/ripgrep.ts +54 -1
  53. package/src/search/types.ts +4 -1
  54. package/src/shared/lsp.ts +138 -0
  55. package/src/shared/rpc.ts +103 -2
  56. package/src/store.ts +2 -2
  57. package/src/treeIndex.ts +397 -0
  58. package/src/treeRead.ts +87 -0
package/lib/index.js CHANGED
@@ -1,10 +1,12 @@
1
1
  import { createRequire } from "node:module";
2
- import { readFile, stat } from "node:fs/promises";
3
- import { dirname, extname, join, normalize, resolve, sep } from "node:path";
4
- import { fileURLToPath } from "node:url";
5
- import { existsSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
2
+ import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
3
+ import { basename, dirname, extname, join, normalize, resolve, sep } from "node:path";
4
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
6
  import { homedir } from "node:os";
7
7
  import { createHash } from "node:crypto";
8
+ import { spawn } from "node:child_process";
9
+ import { inflateRawSync } from "node:zlib";
8
10
  //#region src/mcp.ts
9
11
  /**
10
12
  * dsh-vscode-mode host — MCP 运行时管理适配层。
@@ -368,6 +370,258 @@ async function runPnpmInstall(ctx, profileDir) {
368
370
  }
369
371
  }
370
372
  //#endregion
373
+ //#region src/shared/rpc.ts
374
+ /** webServer 精确路由。 */
375
+ const RPC_PATH = "/edrv/rpc";
376
+ //#endregion
377
+ //#region src/paths.ts
378
+ /**
379
+ * dsh-vscode-mode host — 路径常量与动态路径统一出口(PathConst)。
380
+ * 所有 host 侧文件路径/缓存键/路由前缀从这里取,避免散落内联:
381
+ * - 静态:插件缓存根(~/.dsh/dsh-vscode-mode/cache,下分 workspace/ 工作区级与
382
+ * user/ 用户级)、日志根、工作区侧车名、路由前缀、静态资源目录(import.meta.url 派生)
383
+ * - 动态:每工作区树缓存文件(workspace/<id>/tree.v<schema>.json)、home 解析
384
+ * (DSH_HOME → ~/.dsh)
385
+ * - 清理:缓存 sweep(分级:工作区级按版本/TTL/空目录、用户级按版本/保留期、根级残留)
386
+ * 纯函数可单测;raw fs 写入与引擎自身插件(settings-file 用 node:fs + DSH_HOME)一致。
387
+ * 作者 ddj 2026-09-01
388
+ */
389
+ /** 插件 id(也是 .dsh 下插件工作区目录名)。 */
390
+ const PLUGIN_ID = "dsh-vscode-mode";
391
+ /** DSH 工作区目录名(默认 home 下)。 */
392
+ const DSH_DIR_NAME = ".dsh";
393
+ /** home 覆盖环境变量(与引擎 resolveDshHome 一致)。 */
394
+ const DSH_HOME_ENV = "DSH_HOME";
395
+ /** 插件缓存目录总预算:超限按 mtime 从旧到新删文件,防任意增长撑爆磁盘(最后兜底)。 */
396
+ const CACHE_TOTAL_CAP = 209715200;
397
+ /** 工作区根侧车:审查记录(用户数据,随项目走,不迁移)。 */
398
+ const SIDECAR = ".dsh-edit-review.json";
399
+ /** 工作区根侧车:归档(用户数据)。 */
400
+ const SIDECAR_ARCHIVE = ".dsh-edit-review-archive.json";
401
+ /** 旧 debug 日志文件名(工作区,迁移后由插件清理)。 */
402
+ const DEBUG_LOG = ".dsh-edit-review-debug.log";
403
+ /** 旧树缓存 sidecar 名(工作区,迁移后由插件清理;缓存可重建,删除无损)。 */
404
+ const LEGACY_TREE_SIDECARS = [".dsh-edit-review-tree.json", ".dsh-vscode-mode-tree.json"];
405
+ /** 路由前缀(兼容性检查用)。 */
406
+ const ROUTE_PREFIX = "/edrv";
407
+ /** Monaco 静态资源前缀。 */
408
+ const VENDOR_PREFIX = "/edrv/vendor";
409
+ /** 稳定短哈希(cwd → 缓存文件名段;sha1 截断,跨运行稳定)。 */
410
+ function hashOf(text) {
411
+ return createHash("sha1").update(String(text)).digest("hex").slice(0, 16);
412
+ }
413
+ /**
414
+ * 可读安全路径段编码:非安全码位 → ~XXXX 大写十六进制(`~` 本身也转义)。
415
+ * 与 DSH 引擎 dsh-session-persistence-jsonl 的 encodeSegment 逐字节一致,
416
+ * 用于从 cwd/id 反推 `~/.dsh/sessions` 下的会话目录,纯函数可单测。
417
+ * @author ddj 2026年09月02号
418
+ * @param raw 原始路径段(非空)
419
+ * @returns 路径安全段(`.`/`..` 特判为 ~002E / ~002E~002E)
420
+ */
421
+ function encodeSegment(raw) {
422
+ if (raw.length === 0) throw new Error("cannot encode an empty path segment");
423
+ if (raw === ".") return "~002E";
424
+ if (raw === "..") return "~002E~002E";
425
+ let out = "";
426
+ for (let i = 0; i < raw.length; i++) {
427
+ const code = raw.charCodeAt(i);
428
+ const ch = String.fromCharCode(code);
429
+ if (ch !== "~" && /^[A-Za-z0-9._-]$/.test(ch)) out += ch;
430
+ else out += "~" + code.toString(16).toUpperCase().padStart(4, "0");
431
+ }
432
+ return out;
433
+ }
434
+ /**
435
+ * DSH 工作区目录键:cwd → `--<可读键>--`。分隔符(/ \ :)折叠为单个 `-`,
436
+ * 其余非安全码位转 ~XXXX,`--` 包裹、251 截断。与引擎 projectKey 一致。
437
+ * @author ddj 2026年09月02号
438
+ * @param cwd 会话工作区绝对路径(非空)
439
+ * @returns `~/.dsh/sessions` 下的工作区目录名
440
+ */
441
+ function sessionWorkspaceKey(cwd) {
442
+ if (cwd.length === 0) throw new Error("cannot encode an empty project path");
443
+ let readable = "";
444
+ let separatorRun = false;
445
+ for (let i = 0; i < cwd.length; i++) {
446
+ const code = cwd.charCodeAt(i);
447
+ const ch = String.fromCharCode(code);
448
+ if (ch === "/" || ch === "\\" || ch === ":") {
449
+ if (!separatorRun) readable += "-";
450
+ separatorRun = true;
451
+ } else if (ch !== "~" && /^[A-Za-z0-9._-]$/.test(ch)) {
452
+ readable += ch;
453
+ separatorRun = false;
454
+ } else {
455
+ readable += "~" + code.toString(16).toUpperCase().padStart(4, "0");
456
+ separatorRun = false;
457
+ }
458
+ }
459
+ return `--${(readable.replace(/^-+/, "") || "root").slice(0, 251)}--`;
460
+ }
461
+ /** DSH 会话目录段:id → 路径安全段(与引擎 encodeSegment 一致)。 */
462
+ function sessionIdSegment(id) {
463
+ return encodeSegment(id);
464
+ }
465
+ /** DSH 会话根目录(~/.dsh/sessions;引擎 dsh-base 默认 root)。 */
466
+ function sessionsRoot(home = dshHome()) {
467
+ return join(home, "sessions");
468
+ }
469
+ /**
470
+ * 解析 DSH home:DSH_HOME(去空白)→ ~/.dsh。
471
+ * @author ddj 2026年09月01号
472
+ * @param env 环境映射(缺省 process.env;测试可注入)
473
+ * @returns 归一化的 DSH home 绝对路径
474
+ */
475
+ function dshHome(env = process.env) {
476
+ const value = env[DSH_HOME_ENV];
477
+ if (value && value.trim()) return value;
478
+ return join(homedir(), DSH_DIR_NAME);
479
+ }
480
+ /** 插件缓存根(~/.dsh/dsh-vscode-mode/cache,下分 workspace/ 与 user/ 两级)。 */
481
+ function pluginCacheRoot(home = dshHome()) {
482
+ return join(home, PLUGIN_ID, "cache");
483
+ }
484
+ /** 工作区级缓存目录:按工作区 id(cwd hash)隔离,换工作区即失效。 */
485
+ function workspaceCacheDir(workspaceId, home = dshHome()) {
486
+ return join(pluginCacheRoot(home), "workspace", workspaceId);
487
+ }
488
+ /** 插件日志根(~/.dsh/dsh-vscode-mode/logs,用户级)。 */
489
+ function pluginLogRoot(home = dshHome()) {
490
+ return join(home, PLUGIN_ID, "logs");
491
+ }
492
+ /** 每工作区树缓存文件(工作区级:workspace/<id>/tree.v<schema>.json,文件名含版本便于 sweep)。 */
493
+ function treeCacheFile(cwd, home = dshHome(), schema = 1) {
494
+ return join(workspaceCacheDir(hashOf(cwd), home), "tree.v" + schema + ".json");
495
+ }
496
+ /** 每工作区 debug 日志文件(用户级日志根下,按 cwd hash 隔离)。 */
497
+ function debugLogFile(cwd, home = dshHome()) {
498
+ return join(pluginLogRoot(home), "debug." + hashOf(cwd) + ".log");
499
+ }
500
+ /** 插件包 assets 目录(import.meta.url 派生;config.imageDir 可覆盖,见 imageDirOf)。 */
501
+ function assetsDirOf(moduleUrl) {
502
+ return join(dirname(fileURLToPath(moduleUrl)), "..", "assets");
503
+ }
504
+ /** Monaco AMD 静态目录(随包发布)。 */
505
+ function vendorDirOf(moduleUrl) {
506
+ return join(assetsDirOf(moduleUrl), "vendor");
507
+ }
508
+ /** 图标目录:config.imageDir 覆盖优先,否则插件包 assets/。 */
509
+ function imageDirOf(config, moduleUrl) {
510
+ const cfg = config;
511
+ if (cfg && typeof cfg.imageDir === "string" && cfg.imageDir) return cfg.imageDir.replace(/\\/g, "/");
512
+ return assetsDirOf(moduleUrl);
513
+ }
514
+ /** 树缓存文件名 → schema 版本(不匹配返回 null)。 */
515
+ function treeSchemaOf(filename) {
516
+ const m = /^tree\.v(\d+)\./.exec(filename);
517
+ return m ? Number(m[1]) : null;
518
+ }
519
+ /** 按「schema 版本 + 保留期」清扫某目录内文件(目录级,返回相对路径清单)。 */
520
+ async function sweepDirFiles(dir, removed, now, dropAll) {
521
+ const names = await readdir(dir).catch(() => []);
522
+ for (const name of names) {
523
+ const full = join(dir, name);
524
+ let drop = dropAll;
525
+ if (!drop) {
526
+ const schema = treeSchemaOf(name);
527
+ if (schema === null) drop = true;
528
+ else if (schema !== 1) drop = true;
529
+ else try {
530
+ if (now - (await stat(full)).mtimeMs > 2592e6) drop = true;
531
+ } catch (error) {
532
+ drop = true;
533
+ }
534
+ }
535
+ if (!drop) continue;
536
+ try {
537
+ await rm(full, { force: true });
538
+ removed.push(name);
539
+ } catch (error) {}
540
+ }
541
+ }
542
+ /** 清扫工作区级缓存:逐工作区目录按版本/TTL 清文件,空目录删除。 */
543
+ async function sweepWorkspaceTier(dir, removed, now) {
544
+ const ids = await readdir(dir).catch(() => []);
545
+ for (const id of ids) {
546
+ const wsDir = join(dir, id);
547
+ await sweepDirFiles(wsDir, removed, now, false);
548
+ if (!(await readdir(wsDir).catch(() => [])).length) await rm(wsDir, {
549
+ recursive: true,
550
+ force: true
551
+ }).catch(() => {});
552
+ }
553
+ }
554
+ /** 总预算兜底:递归收集全部缓存文件,按 mtime 从旧到新删,直到总大小 ≤ 预算。 */
555
+ async function sweepByTotalCap(root, removed, cap) {
556
+ const files = [];
557
+ const walk = async (dir) => {
558
+ const names = await readdir(dir).catch(() => []);
559
+ for (const name of names) {
560
+ const full = join(dir, name);
561
+ try {
562
+ const info = await stat(full);
563
+ if (info.isDirectory()) await walk(full);
564
+ else files.push({
565
+ path: full,
566
+ mtime: info.mtimeMs,
567
+ size: info.size
568
+ });
569
+ } catch (error) {}
570
+ }
571
+ };
572
+ await walk(root);
573
+ let total = files.reduce((sum, f) => sum + f.size, 0);
574
+ files.sort((a, b) => a.mtime - b.mtime);
575
+ for (const f of files) {
576
+ if (total <= cap) break;
577
+ try {
578
+ await rm(f.path, { force: true });
579
+ total -= f.size;
580
+ removed.push(f.path.slice(root.length + 1));
581
+ } catch (error) {}
582
+ }
583
+ }
584
+ /**
585
+ * 清理插件缓存根(分级 + 总预算兜底):
586
+ * - 根级残留文件(旧扁平格式/未知)→ 删
587
+ * - workspace/ 工作区级 → 按「schema 版本 + 保留期」清文件,空目录删除
588
+ * - user/ 用户级 → 按「schema 版本 + 保留期」清文件
589
+ * - 总大小超预算 → 按最旧先删(最后防线,防任意增长撑爆磁盘)
590
+ * best-effort 静默。
591
+ * @author ddj 2026年09月01号
592
+ * @param home DSH home(测试可注入)
593
+ * @param cap 总预算字节数(缺省 CACHE_TOTAL_CAP;测试可注入小预算)
594
+ * @returns 删除的文件相对路径清单
595
+ */
596
+ async function sweepTreeCache(home = dshHome(), cap = CACHE_TOTAL_CAP) {
597
+ const dir = pluginCacheRoot(home);
598
+ const names = await readdir(dir).catch(() => []);
599
+ if (!names.length) return [];
600
+ const removed = [];
601
+ const now = Date.now();
602
+ await Promise.all(names.map(async (name) => {
603
+ const full = join(dir, name);
604
+ try {
605
+ if ((await stat(full)).isDirectory()) {
606
+ if (name === "workspace") await sweepWorkspaceTier(full, removed, now);
607
+ else if (name === "user") await sweepDirFiles(full, removed, now, false);
608
+ else {
609
+ await rm(full, {
610
+ recursive: true,
611
+ force: true
612
+ });
613
+ removed.push(name);
614
+ }
615
+ } else {
616
+ await rm(full, { force: true });
617
+ removed.push(name);
618
+ }
619
+ } catch (error) {}
620
+ }));
621
+ await sweepByTotalCap(dir, removed, cap);
622
+ return removed;
623
+ }
624
+ //#endregion
371
625
  //#region src/compat.ts
372
626
  /**
373
627
  * dsh-vscode-mode host — 兼容层:与其他插件 / DSH 版本的统一探测、适配、护栏与自诊断。
@@ -381,8 +635,6 @@ const PLUGIN_NAME = "dsh-vscode-mode";
381
635
  const MCP_PACKAGE = "@deepseek-ai/dsh-mcp-client";
382
636
  /** 项目级 MCP 的 loader entry id 固定前缀(区分 profile 全局 MCP;loader id 不能用 ':',故用 '.')。 */
383
637
  const PROJECT_ENTRY_PREFIX = "vsm-mcp.";
384
- /** webServer 路由前缀(RPC 精确路由 + 静态资源前缀共用)。 */
385
- const ROUTE_PREFIX = "/edrv";
386
638
  /** 判断新旧格式项目 MCP 条目 id(旧残留不被视为全局 MCP)。 */
387
639
  function isProjectEntryId(id) {
388
640
  return id.startsWith("vsm-mcp.") || id.startsWith("vsm-mcp:");
@@ -508,10 +760,6 @@ async function buildReport(ctx, options) {
508
760
  };
509
761
  }
510
762
  //#endregion
511
- //#region src/shared/rpc.ts
512
- /** webServer 精确路由。 */
513
- const RPC_PATH = "/edrv/rpc";
514
- //#endregion
515
763
  //#region src/routes.ts
516
764
  /**
517
765
  * dsh-vscode-mode host — webServer 路由注册(/edrv/rpc、/edrv/assets/*、/edrv/vendor/*)。
@@ -526,7 +774,6 @@ const IMG_ROUTES = [{
526
774
  file: "compare_select.png"
527
775
  }];
528
776
  /** Monaco AMD 构建静态资源(随包发布,离线可用;前缀路由 /edrv/vendor/*)。 */
529
- const VENDOR_PREFIX = "/edrv/vendor";
530
777
  const VENDOR_MIME = {
531
778
  ".js": "text/javascript; charset=utf-8",
532
779
  ".css": "text/css; charset=utf-8",
@@ -543,14 +790,10 @@ const VENDOR_MIME = {
543
790
  ".html": "text/html; charset=utf-8",
544
791
  ".md": "text/markdown"
545
792
  };
546
- /** 图标目录:默认插件包内 assets/(随包发布);config.imageDir 可覆盖(用于自定义换图)。 */
547
- function imageDirOf(config) {
548
- const cfg = config;
549
- if (cfg && typeof cfg.imageDir === "string" && cfg.imageDir) return cfg.imageDir.replace(/\\/g, "/");
550
- return join(dirname(fileURLToPath(import.meta.url)), "..", "assets");
551
- }
552
- /** Monaco AMD 静态目录(随包发布)。 */
553
- const vendorDir = join(dirname(fileURLToPath(import.meta.url)), "..", "assets", "vendor");
793
+ /** 图标目录(paths.ts 统一解析:config.imageDir 覆盖优先,否则插件包 assets/)。 */
794
+ const imgDirOf = (config) => imageDirOf(config, import.meta.url);
795
+ /** Monaco AMD 静态目录(随包发布,paths.ts 统一解析)。 */
796
+ const vendorDir = vendorDirOf(import.meta.url);
554
797
  /**
555
798
  * 注册全部 webServer 路由(带兼容护栏:前缀冲突预警 + 注册失败降级跳过)。
556
799
  * @author ddj 2026年08月20号
@@ -560,7 +803,7 @@ const vendorDir = join(dirname(fileURLToPath(import.meta.url)), "..", "assets",
560
803
  * @param onWarning 兼容性警告收集(可选)
561
804
  */
562
805
  function registerRoutes(ctx, config, handleRpc, onWarning) {
563
- const imgDir = imageDirOf(config);
806
+ const imgDir = imgDirOf(config);
564
807
  const web = ctx.get("webServer");
565
808
  if (!web) return;
566
809
  resetOwnRoutes();
@@ -1068,10 +1311,6 @@ function archiveEntryFor(recs, cwd, reason) {
1068
1311
  }))
1069
1312
  };
1070
1313
  }
1071
- //#endregion
1072
- //#region src/store.ts
1073
- const SIDECAR = ".dsh-edit-review.json";
1074
- const SIDECAR_ARCHIVE = ".dsh-edit-review-archive.json";
1075
1314
  /** 会话沙箱策略(policyOf):可能不存在,调用方需容忍 undefined。 */
1076
1315
  function policyOf(ctx, session) {
1077
1316
  const svc = ctx.get("sandboxPolicy");
@@ -1333,6 +1572,459 @@ function bucketOf(registry, ctx, cwd) {
1333
1572
  });
1334
1573
  }
1335
1574
  //#endregion
1575
+ //#region src/tree.ts
1576
+ /** 目录在文件前、名称按小写字母序的比较器(对齐 VSCode 资源管理器习惯)。 */
1577
+ function entryOrder(a, b) {
1578
+ const aDir = a.type === "directory" ? 0 : 1;
1579
+ const bDir = b.type === "directory" ? 0 : 1;
1580
+ if (aDir !== bDir) return aDir - bDir;
1581
+ const la = a.name.toLocaleLowerCase("en-US");
1582
+ const lb = b.name.toLocaleLowerCase("en-US");
1583
+ return la < lb ? -1 : la > lb ? 1 : 0;
1584
+ }
1585
+ /**
1586
+ * 归一化目录相对路径:去空白、反斜杠→斜杠、去首尾斜杠、`.`→''。
1587
+ * 任一路径段为 `..` 返回 null(拒绝越界)。
1588
+ * @author ddj 2026年08月26号
1589
+ * @param path 原始相对路径('' 表示根目录)
1590
+ * @returns 归一化相对路径('' = 根),非法返回 null
1591
+ */
1592
+ function normalizeRel(path) {
1593
+ const rel = String(path ?? "").trim().replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
1594
+ if (!rel) return "";
1595
+ const segs = rel.split("/").filter((seg) => seg !== ".");
1596
+ if (!segs.length) return "";
1597
+ for (const seg of segs) if (seg === "..") return null;
1598
+ return segs.join("/");
1599
+ }
1600
+ /**
1601
+ * fs.listDir 结果 → TreeEntry 数组:相对路径拼接 + 目录优先排序。
1602
+ * @author ddj 2026年08月26号
1603
+ * @param rel 父目录相对路径('' = 根)
1604
+ * @param children 目录子项
1605
+ * @returns 排序后的树条目
1606
+ */
1607
+ function toTreeEntries(rel, children) {
1608
+ const out = [];
1609
+ for (const child of children) {
1610
+ if (!child || typeof child.name !== "string" || !child.name) continue;
1611
+ out.push({
1612
+ name: child.name,
1613
+ path: rel ? rel + "/" + child.name : child.name,
1614
+ type: child.type === "directory" ? "directory" : child.type === "file" ? "file" : "other",
1615
+ size: typeof child.size === "number" ? child.size : void 0
1616
+ });
1617
+ }
1618
+ return out.sort(entryOrder);
1619
+ }
1620
+ /** 列取失败(带稳定语义 code,调用方转文案)。 */
1621
+ var DirListError = class extends Error {
1622
+ code;
1623
+ constructor(code, message) {
1624
+ super(message);
1625
+ this.code = code;
1626
+ }
1627
+ };
1628
+ /** node fs 错误码 → 稳定语义错误。 */
1629
+ function codeOf(err) {
1630
+ const code = err?.code;
1631
+ if (code === "ENOENT") return new DirListError("not-found", "目录不存在");
1632
+ if (code === "ENOTDIR") return new DirListError("not-dir", "不是目录");
1633
+ if (code === "EACCES" || code === "EPERM" || code === "EBUSY") return new DirListError("permission", "权限不足");
1634
+ return new DirListError("io", String(err?.message ?? err));
1635
+ }
1636
+ /** 并行 stat 一批绝对路径 → 目录/文件/other(follow symlink,失败按 other)。 */
1637
+ async function statTypes(paths, out) {
1638
+ const cap = Math.min(paths.length, 256);
1639
+ for (let i = 0; i < cap; i += 32) {
1640
+ const chunk = paths.slice(i, i + 32);
1641
+ await Promise.all(chunk.map(async (p) => {
1642
+ try {
1643
+ const info = await stat(p);
1644
+ out.set(p, info.isDirectory() ? "directory" : info.isFile() ? "file" : "other");
1645
+ } catch (error) {
1646
+ out.set(p, "other");
1647
+ }
1648
+ }));
1649
+ }
1650
+ }
1651
+ /**
1652
+ * 快速列取目录:一次 readdir(withFileTypes) 判型;symlink 并行 follow-stat。
1653
+ * @author ddj 2026年08月31号
1654
+ * @param absDir 绝对目录路径(fs.resolve 后的 targetKey)
1655
+ * @returns 子项列表(name/type;size 缺省,树界面不展示大小)
1656
+ * @throws DirListError 目录不存在/不是目录/权限不足/IO 失败
1657
+ */
1658
+ async function listDirCheap(absDir) {
1659
+ let dirents;
1660
+ try {
1661
+ dirents = await readdir(absDir, {
1662
+ withFileTypes: true,
1663
+ encoding: "utf8"
1664
+ });
1665
+ } catch (error) {
1666
+ throw codeOf(error);
1667
+ }
1668
+ const out = [];
1669
+ const syms = [];
1670
+ for (const d of dirents) if (d.isDirectory()) out.push({
1671
+ name: d.name,
1672
+ type: "directory"
1673
+ });
1674
+ else if (d.isFile()) out.push({
1675
+ name: d.name,
1676
+ type: "file"
1677
+ });
1678
+ else if (d.isSymbolicLink()) syms.push(d.name);
1679
+ else out.push({
1680
+ name: d.name,
1681
+ type: "other"
1682
+ });
1683
+ if (syms.length) {
1684
+ const symAbs = syms.map((name) => join(absDir, name));
1685
+ const types = /* @__PURE__ */ new Map();
1686
+ await statTypes(symAbs, types);
1687
+ for (let i = 0; i < syms.length; i++) out.push({
1688
+ name: syms[i],
1689
+ type: types.get(symAbs[i]) ?? "other"
1690
+ });
1691
+ }
1692
+ return out;
1693
+ }
1694
+ /** 落盘单目录条目上限:超出截断并标 full=false(截断仍可即时渲染,客户端后台刷新补全)。 */
1695
+ const PERSIST_DIR_ENTRIES = 1500;
1696
+ /** 落盘防抖:变更后静默 10s 再写一次。 */
1697
+ const PERSIST_DEBOUNCE_MS = 1e4;
1698
+ /** re-heal 防抖:写入密集期合并为 3s 后一轮。 */
1699
+ const HEAL_DEBOUNCE_MS = 3e3;
1700
+ /** cwd → 索引状态(随会话销毁清理)。 */
1701
+ const states = /* @__PURE__ */ new Map();
1702
+ /** 单调时钟:连续 put 的 ts 严格递增(同毫秒不并列,逐出/裁剪顺序可预期)。 */
1703
+ let clock = 0;
1704
+ /** 取下一时间戳(不小于真实时间且严格递增)。 */
1705
+ function nextTs() {
1706
+ clock = Math.max(Date.now(), clock + 1);
1707
+ return clock;
1708
+ }
1709
+ /** 条目原始值 → 合法条目(非法返回 null)。 */
1710
+ function entryOf(raw) {
1711
+ if (!raw || typeof raw !== "object") return null;
1712
+ const e = raw;
1713
+ if (typeof e.name !== "string" || !e.name) return null;
1714
+ const type = e.type === "directory" ? "directory" : e.type === "file" ? "file" : "other";
1715
+ return {
1716
+ name: e.name,
1717
+ type
1718
+ };
1719
+ }
1720
+ /** 写入路径 → 相对工作区根的 rel(绝对路径按 cwd 前缀裁剪;越界/非法 → null)。 */
1721
+ function relOf(cwd, path) {
1722
+ let rel = String(path ?? "").trim().replace(/\\/g, "/");
1723
+ const cl = cwd.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
1724
+ if (rel.toLowerCase().startsWith(cl + "/")) rel = rel.slice(cl.length + 1);
1725
+ else if (rel.startsWith("/") || /^[a-z]:\//i.test(rel)) return null;
1726
+ rel = rel.replace(/^\/+|\/+$/g, "");
1727
+ if (!rel) return null;
1728
+ const segs = rel.split("/").filter((s) => s !== "" && s !== ".");
1729
+ if (!segs.length) return null;
1730
+ for (const s of segs) if (s === "..") return null;
1731
+ return segs.join("/");
1732
+ }
1733
+ /** rel 的父目录 + 全部祖先('a/b/c' → ['a/b','a',''];'a' → [''])。 */
1734
+ function ancestorsOf(rel) {
1735
+ const segs = rel.split("/");
1736
+ const out = [];
1737
+ for (let i = segs.length - 1; i >= 1; i--) out.push(segs.slice(0, i).join("/"));
1738
+ out.push("");
1739
+ return out;
1740
+ }
1741
+ /** 序列化索引数据。 */
1742
+ function indexSerialize(data) {
1743
+ const lean = {
1744
+ v: 1,
1745
+ root: data.root,
1746
+ dirs: {}
1747
+ };
1748
+ for (const [rel, dir] of Object.entries(data.dirs)) lean.dirs[rel] = {
1749
+ ts: dir.ts,
1750
+ full: dir.full,
1751
+ entries: dir.entries.map((e) => ({
1752
+ name: e.name,
1753
+ type: e.type
1754
+ }))
1755
+ };
1756
+ return JSON.stringify(lean);
1757
+ }
1758
+ /** 解析索引文本;损坏/格式不符 → null。 */
1759
+ function indexParse(text) {
1760
+ if (!text) return null;
1761
+ try {
1762
+ const raw = JSON.parse(text);
1763
+ if (!raw || raw.v !== 1 || !raw.dirs || typeof raw.dirs !== "object") return null;
1764
+ const dirs = {};
1765
+ for (const [rel, d] of Object.entries(raw.dirs)) {
1766
+ if (!d || typeof d !== "object") continue;
1767
+ const dir = d;
1768
+ if (!Array.isArray(dir.entries)) continue;
1769
+ const entries = [];
1770
+ for (const rawEntry of dir.entries) {
1771
+ const e = entryOf(rawEntry);
1772
+ if (!e) continue;
1773
+ entries.push({
1774
+ name: e.name,
1775
+ path: rel ? rel + "/" + e.name : e.name,
1776
+ type: e.type
1777
+ });
1778
+ }
1779
+ if (!entries.length) continue;
1780
+ dirs[rel] = {
1781
+ ts: typeof dir.ts === "number" ? dir.ts : 0,
1782
+ full: dir.full === true,
1783
+ entries
1784
+ };
1785
+ }
1786
+ return {
1787
+ v: 1,
1788
+ root: typeof raw.root === "string" ? raw.root : null,
1789
+ dirs
1790
+ };
1791
+ } catch (error) {
1792
+ return null;
1793
+ }
1794
+ }
1795
+ /** 落盘裁剪:目录按 ts 取新裁旧、单目录超限截断标 full=false(内存不受影响)。 */
1796
+ function trimPersist(data, maxDirs = 400, maxEntries = PERSIST_DIR_ENTRIES) {
1797
+ const dirs = {};
1798
+ const sorted = Object.entries(data.dirs).sort((a, b) => b[1].ts - a[1].ts);
1799
+ for (const [rel, dir] of sorted) {
1800
+ if (Object.keys(dirs).length >= maxDirs) break;
1801
+ const full = dir.entries.length <= maxEntries;
1802
+ dirs[rel] = {
1803
+ ts: dir.ts,
1804
+ full,
1805
+ entries: full ? dir.entries : dir.entries.slice(0, maxEntries)
1806
+ };
1807
+ }
1808
+ return {
1809
+ v: 1,
1810
+ root: data.root,
1811
+ dirs
1812
+ };
1813
+ }
1814
+ /** 取(或建)某 cwd 的索引状态,并惰性加载落盘快照(合并:文件 ts 更新才采纳)。 */
1815
+ function ensureState(ctx, cwd) {
1816
+ const existing = states.get(cwd);
1817
+ if (existing) return existing;
1818
+ const state = {
1819
+ ctx,
1820
+ cwd,
1821
+ data: {
1822
+ v: 1,
1823
+ root: null,
1824
+ dirs: {}
1825
+ },
1826
+ stale: /* @__PURE__ */ new Set(),
1827
+ dirty: false,
1828
+ persistTimer: null,
1829
+ healTimer: null,
1830
+ pending: /* @__PURE__ */ new Map(),
1831
+ loading: false,
1832
+ loadPromise: null
1833
+ };
1834
+ states.set(cwd, state);
1835
+ state.loadPromise = loadIndex(state);
1836
+ removeLegacyTreeSidecars(state.cwd);
1837
+ return state;
1838
+ }
1839
+ /** 清理旧工作区树 sidecar(.dsh-edit-review-tree.json/.dsh-vscode-mode-tree.json)。 */
1840
+ async function removeLegacyTreeSidecars(cwd) {
1841
+ await Promise.all(LEGACY_TREE_SIDECARS.map(async (name) => {
1842
+ try {
1843
+ await rm(join(cwd, name), { force: true });
1844
+ } catch (error) {}
1845
+ }));
1846
+ }
1847
+ /** 惰性加载落盘快照一次(缺失/损坏 → 空索引;失败静默)。 */
1848
+ async function loadIndex(state) {
1849
+ if (state.loading) return;
1850
+ state.loading = true;
1851
+ try {
1852
+ const parsed = indexParse(await readFile(treeCacheFile(state.cwd), "utf8"));
1853
+ if (!parsed) return;
1854
+ for (const [rel, dir] of Object.entries(parsed.dirs)) {
1855
+ const cur = state.data.dirs[rel];
1856
+ if (!cur || dir.ts > cur.ts) state.data.dirs[rel] = dir;
1857
+ }
1858
+ if (parsed.root && !state.data.root) state.data.root = parsed.root;
1859
+ } catch (error) {} finally {
1860
+ state.loading = false;
1861
+ }
1862
+ }
1863
+ /** 命中:新鲜且未失效 → 条目(并刷新 ts 保活);否则 null。 */
1864
+ function hitIndex(cwd, rel) {
1865
+ const state = states.get(cwd);
1866
+ if (!state) return null;
1867
+ const dir = state.data.dirs[rel];
1868
+ if (!dir || state.stale.has(rel)) return null;
1869
+ const now = Date.now();
1870
+ if (now - dir.ts > 6e4) return null;
1871
+ dir.ts = now;
1872
+ return dir.entries;
1873
+ }
1874
+ /** 更新某目录条目:写内存 + 打脏 + 防抖落盘;stale 撤销。 */
1875
+ function putIndex(ctx, cwd, rel, entries) {
1876
+ const state = ensureState(ctx, cwd);
1877
+ const dirs = state.data.dirs;
1878
+ if (!dirs[rel] && Object.keys(dirs).length >= 2e3) {
1879
+ let oldest = rel;
1880
+ let oldestTs = Infinity;
1881
+ for (const [k, v] of Object.entries(dirs)) if (v.ts < oldestTs) {
1882
+ oldestTs = v.ts;
1883
+ oldest = k;
1884
+ }
1885
+ delete dirs[oldest];
1886
+ }
1887
+ dirs[rel] = {
1888
+ ts: nextTs(),
1889
+ full: entries.length <= PERSIST_DIR_ENTRIES,
1890
+ entries
1891
+ };
1892
+ state.stale.delete(rel);
1893
+ state.dirty = true;
1894
+ schedulePersist(state);
1895
+ }
1896
+ /** 写入路径失效:父目录+祖先进 stale,并调度后台 re-heal。 */
1897
+ function invalidateIndex(ctx, cwd, path) {
1898
+ const rel = relOf(cwd, path);
1899
+ if (rel === null) return;
1900
+ const state = ensureState(ctx, cwd);
1901
+ for (const anc of ancestorsOf(rel)) state.stale.add(anc);
1902
+ scheduleHeal(state);
1903
+ }
1904
+ /** 调度 re-heal(防抖合并;已有定时器则复用)。 */
1905
+ function scheduleHeal(state) {
1906
+ if (state.healTimer) return;
1907
+ state.healTimer = setTimeout(() => {
1908
+ state.healTimer = null;
1909
+ healRound(state);
1910
+ }, HEAL_DEBOUNCE_MS);
1911
+ }
1912
+ /** 后台自愈一轮:并行重列 ≤HEAL_ROUND_MAX 个 stale 目录;失败移出(交给 TTL/展开路径自愈)。 */
1913
+ async function healRound(state) {
1914
+ if (!state.stale.size) return;
1915
+ const targets = [...state.stale].slice(0, 20);
1916
+ await Promise.all(targets.map(async (rel) => {
1917
+ if ("error" in await listDirCached(state.ctx, state.cwd, rel, true)) state.stale.delete(rel);
1918
+ }));
1919
+ if (state.stale.size) scheduleHeal(state);
1920
+ }
1921
+ /** 调度落盘(防抖;已有定时器则重置)。 */
1922
+ function schedulePersist(state) {
1923
+ if (state.persistTimer) clearTimeout(state.persistTimer);
1924
+ state.persistTimer = setTimeout(() => {
1925
+ state.persistTimer = null;
1926
+ persistNow(state);
1927
+ }, PERSIST_DEBOUNCE_MS);
1928
+ }
1929
+ /** 落盘快照(裁剪 + 序列化 + 超限跳过;raw fs 写 ~/.dsh 缓存根,失败静默)。 */
1930
+ async function persistNow(state) {
1931
+ if (!state.dirty) return;
1932
+ try {
1933
+ const json = indexSerialize(trimPersist(state.data));
1934
+ if (json.length > 8388608) {
1935
+ state.dirty = false;
1936
+ return;
1937
+ }
1938
+ await mkdir(dirname(treeCacheFile(state.cwd)), { recursive: true });
1939
+ await writeFile(treeCacheFile(state.cwd), json, "utf8");
1940
+ state.dirty = false;
1941
+ } catch (error) {}
1942
+ }
1943
+ /** 清理某 cwd 索引(会话销毁时):清定时器,dirty 则尽力补写一次。 */
1944
+ function disposeIndex(cwd) {
1945
+ const state = states.get(cwd);
1946
+ if (!state) return;
1947
+ if (state.persistTimer) {
1948
+ clearTimeout(state.persistTimer);
1949
+ state.persistTimer = null;
1950
+ }
1951
+ if (state.healTimer) {
1952
+ clearTimeout(state.healTimer);
1953
+ state.healTimer = null;
1954
+ }
1955
+ if (state.dirty) persistNow(state);
1956
+ states.delete(cwd);
1957
+ }
1958
+ /** 错误 → 用户文案(DirListError 用自身 message,其余带上下文)。 */
1959
+ function errorText(error) {
1960
+ if (error instanceof DirListError) return error.message;
1961
+ return "读取目录失败:" + String(error);
1962
+ }
1963
+ /**
1964
+ * 目录列取统一入口(RPC handler 用):命中索引直出;失效/force 走快路径
1965
+ * (resolve + listDirCheap + putIndex);同 (cwd,rel) 在途请求复用同一 Promise。
1966
+ * @author ddj 2026年08月31号 / 2026年09月01号
1967
+ * @param ctx DSH 上下文(仅用于工作区 IO)
1968
+ * @param cwd 工作区根
1969
+ * @param rel 归一化相对路径('' = 根)
1970
+ * @param force 跳过索引命中,强制实列
1971
+ * @returns 目录条目与工作区根,或错误文案
1972
+ */
1973
+ async function listDirCached(ctx, cwd, rel, force) {
1974
+ const state = ensureState(ctx, cwd);
1975
+ if (state.loadPromise) await state.loadPromise;
1976
+ const fs = ctx.get("fs");
1977
+ if (!fs) return { error: "缺少 fs" };
1978
+ if (!state.data.root) try {
1979
+ state.data.root = String(fs.processPath(await fs.resolve(".", { cwd })));
1980
+ } catch (error) {
1981
+ return { error: errorText(error) };
1982
+ }
1983
+ const root = state.data.root;
1984
+ if (!force) {
1985
+ const hit = hitIndex(cwd, rel);
1986
+ if (hit) return {
1987
+ entries: hit,
1988
+ root
1989
+ };
1990
+ }
1991
+ const inflight = state.pending.get(rel);
1992
+ if (inflight) {
1993
+ const res = await inflight;
1994
+ return "entries" in res ? {
1995
+ entries: res.entries,
1996
+ root
1997
+ } : res;
1998
+ }
1999
+ const task = (async () => {
2000
+ try {
2001
+ const entries = toTreeEntries(rel, await listDirCheap((await fs.resolve(rel || ".", { cwd })).targetKey));
2002
+ putIndex(ctx, cwd, rel, entries);
2003
+ return { entries };
2004
+ } catch (error) {
2005
+ if (error instanceof DirListError && error.code === "not-found") {
2006
+ if (state.data.dirs[rel]) {
2007
+ delete state.data.dirs[rel];
2008
+ state.dirty = true;
2009
+ schedulePersist(state);
2010
+ }
2011
+ state.stale.delete(rel);
2012
+ }
2013
+ return { error: errorText(error) };
2014
+ }
2015
+ })();
2016
+ state.pending.set(rel, task);
2017
+ try {
2018
+ const res = await task;
2019
+ return "entries" in res ? {
2020
+ entries: res.entries,
2021
+ root
2022
+ } : res;
2023
+ } finally {
2024
+ if (state.pending.get(rel) === task) state.pending.delete(rel);
2025
+ }
2026
+ }
2027
+ //#endregion
1336
2028
  //#region src/capture.ts
1337
2029
  /** 校验并提取工具 result meta 中的文件 hunk。 */
1338
2030
  function validHunks(raw) {
@@ -1432,6 +2124,7 @@ async function captureToolResult(ctx, registry, exec, result) {
1432
2124
  bucket.set(exec.callId, record);
1433
2125
  prune(bucket);
1434
2126
  await saveBucket(ctx, cwd, bucket, session);
2127
+ invalidateIndex(ctx, cwd, path);
1435
2128
  } catch (error) {
1436
2129
  console.error("edrv capture failed", error);
1437
2130
  }
@@ -1487,7 +2180,7 @@ function pathMatch(value, query) {
1487
2180
  * 作者 ddj 2026年08月24号
1488
2181
  */
1489
2182
  const STDOUT_CAP$1 = 4 << 20;
1490
- const STDERR_CAP$1 = 65536;
2183
+ const STDERR_CAP$2 = 65536;
1491
2184
  const GRACE_MS$1 = 2e4;
1492
2185
  /** 默认排除目录(文件/内容搜索共用,导出供 provider 复用)。 */
1493
2186
  const EXCLUDES = [
@@ -1579,6 +2272,65 @@ function parseOutput(handle, maxResults) {
1579
2272
  };
1580
2273
  }
1581
2274
  /**
2275
+ * 读取已收集的 stderr(无收集器 → 空文本)。
2276
+ * @author ddj 2026年08月27号
2277
+ * @param handle subprocess handle
2278
+ * @returns stderr 文本
2279
+ */
2280
+ function readStderr(handle) {
2281
+ const reader = handle.collected?.stderr;
2282
+ return reader ? reader.readFrom(0) : { text: "" };
2283
+ }
2284
+ /**
2285
+ * 取 stderr 首条非空行(去 \r,截断 300 字符防撑爆 UI)。
2286
+ * @author ddj 2026年08月27号
2287
+ * @param text stderr 全文
2288
+ * @returns 首条诊断行(无则空串)
2289
+ */
2290
+ function firstStderrLine(text) {
2291
+ for (const raw of String(text ?? "").split(/\r?\n/)) {
2292
+ const line = raw.trim();
2293
+ if (!line) continue;
2294
+ return line.length > 300 ? line.slice(0, 300) + "…" : line;
2295
+ }
2296
+ return "";
2297
+ }
2298
+ /**
2299
+ * 分类 rg 非 0/1 退出:pattern=模式错误(正则/glob 无效,无结果),
2300
+ * partial=遍历错误(rg 已输出的命中仍有效,结果不完整),hard=硬失败(其他退出码/超时被杀)。
2301
+ * @author ddj 2026年08月27号
2302
+ * @param code 退出码(null/undefined = 被终止,通常为 20s 超时)
2303
+ * @param stderrText stderr 全文
2304
+ * @returns 分类与中文提示文案(partial 的 message 即 warning 内容)
2305
+ */
2306
+ function rgExitFailure(code, stderrText) {
2307
+ const detail = firstStderrLine(stderrText);
2308
+ if (/regex parse error/i.test(detail)) return {
2309
+ kind: "pattern",
2310
+ message: "正则表达式无效:" + detail
2311
+ };
2312
+ if (/error parsing glob/i.test(detail)) return {
2313
+ kind: "pattern",
2314
+ message: "文件过滤模式无效:" + detail
2315
+ };
2316
+ if (code === 2) {
2317
+ const count = String(stderrText ?? "").split(/\r?\n/).filter((line) => line.trim()).length;
2318
+ const suffix = count > 1 ? "(共 " + count + " 处)" : "";
2319
+ return {
2320
+ kind: "partial",
2321
+ message: detail ? "部分路径无法访问,结果可能不完整:" + detail + suffix : "部分路径无法访问,结果可能不完整"
2322
+ };
2323
+ }
2324
+ if (code === null || code === void 0) return {
2325
+ kind: "hard",
2326
+ message: "搜索超时(20 秒内未完成,已终止)"
2327
+ };
2328
+ return {
2329
+ kind: "hard",
2330
+ message: "ripgrep 退出码:" + String(code) + (detail ? "(" + detail + ")" : "")
2331
+ };
2332
+ }
2333
+ /**
1582
2334
  * 使用打包 ripgrep 搜索工作区文件。
1583
2335
  * @author ddj 2026年08月24号
1584
2336
  * @param input provider 输入
@@ -1609,7 +2361,7 @@ async function searchRipgrep(input) {
1609
2361
  stdio: {
1610
2362
  stdin: "ignore",
1611
2363
  stdout: { maxBytes: STDOUT_CAP$1 },
1612
- stderr: { maxBytes: STDERR_CAP$1 }
2364
+ stderr: { maxBytes: STDERR_CAP$2 }
1613
2365
  },
1614
2366
  graceMs: GRACE_MS$1,
1615
2367
  signal: input.signal
@@ -1621,7 +2373,16 @@ async function searchRipgrep(input) {
1621
2373
  throw new Error("ripgrep 启动失败:" + String(error));
1622
2374
  }
1623
2375
  const code = outcome.exitCode ?? outcome.code;
1624
- if (code !== 0 && code !== 1) throw new Error("ripgrep 退出码:" + String(code));
2376
+ if (code !== 0 && code !== 1) {
2377
+ const failure = rgExitFailure(code, readStderr(handle).text);
2378
+ if (failure.kind !== "partial") throw new Error(failure.message);
2379
+ return {
2380
+ ...parseOutput(handle, input.maxResults),
2381
+ warning: failure.message,
2382
+ complete: false,
2383
+ source: "ripgrep"
2384
+ };
2385
+ }
1625
2386
  const parsed = parseOutput(handle, input.maxResults);
1626
2387
  if (!handle.collected?.stdout) throw new Error("ripgrep stdout 不可用");
1627
2388
  return {
@@ -2065,7 +2826,7 @@ function newSearcher(ctx) {
2065
2826
  //#endregion
2066
2827
  //#region src/search/content.ts
2067
2828
  const STDOUT_CAP = 16 << 20;
2068
- const STDERR_CAP = 65536;
2829
+ const STDERR_CAP$1 = 65536;
2069
2830
  const GRACE_MS = 2e4;
2070
2831
  const DEFAULT_MAX_MATCHES = 500;
2071
2832
  const DEFAULT_MAX_FILES = 100;
@@ -2227,7 +2988,7 @@ async function searchRipgrepContent(input) {
2227
2988
  stdio: {
2228
2989
  stdin: "ignore",
2229
2990
  stdout: { maxBytes: STDOUT_CAP },
2230
- stderr: { maxBytes: STDERR_CAP }
2991
+ stderr: { maxBytes: STDERR_CAP$1 }
2231
2992
  },
2232
2993
  graceMs: GRACE_MS,
2233
2994
  signal: input.signal
@@ -2239,17 +3000,23 @@ async function searchRipgrepContent(input) {
2239
3000
  throw new Error("ripgrep 启动失败:" + String(error));
2240
3001
  }
2241
3002
  const code = outcome.exitCode ?? outcome.code;
2242
- if (code !== 0 && code !== 1) throw new Error("ripgrep 退出码:" + String(code));
3003
+ const failure = code !== 0 && code !== 1 ? rgExitFailure(code, readStderr(handle).text) : null;
3004
+ if (failure && failure.kind !== "partial") throw new Error(failure.message);
2243
3005
  const reader = handle.collected?.stdout;
2244
3006
  if (!reader) throw new Error("ripgrep stdout 不可用");
2245
3007
  const output = reader.readFrom(0);
2246
3008
  const capped = applyCaps(parseRgJsonLines(output.text, root), Math.max(1, Math.min(input.maxResults ?? DEFAULT_MAX_MATCHES, DEFAULT_MAX_MATCHES)), DEFAULT_MAX_FILES);
2247
3009
  const truncated = Boolean(output.lossy) || capped.truncated;
2248
- return {
3010
+ const result = {
2249
3011
  ...capped,
2250
3012
  complete: !truncated,
2251
3013
  source: "ripgrep"
2252
3014
  };
3015
+ if (failure) {
3016
+ result.complete = false;
3017
+ result.warning = failure.message;
3018
+ }
3019
+ return result;
2253
3020
  }
2254
3021
  /**
2255
3022
  * 内容搜索编排器:短期缓存 + 同根在途取消 + 会话清理。
@@ -2314,7 +3081,8 @@ var ContentSearcher = class {
2314
3081
  const cached = this.cache.get(key);
2315
3082
  if (cached) return {
2316
3083
  matches: cached.matches,
2317
- truncated: cached.truncated
3084
+ truncated: cached.truncated,
3085
+ warning: cached.warning
2318
3086
  };
2319
3087
  this.inflight.get(root)?.abort();
2320
3088
  const controller = new AbortController();
@@ -2341,7 +3109,8 @@ var ContentSearcher = class {
2341
3109
  this.cache.set(key, result);
2342
3110
  return {
2343
3111
  matches: result.matches,
2344
- truncated: result.truncated
3112
+ truncated: result.truncated,
3113
+ warning: result.warning
2345
3114
  };
2346
3115
  } catch (error) {
2347
3116
  if (controller.signal.aborted) return {
@@ -2813,53 +3582,7 @@ async function projectRefresh(ctx, workspacePath, serverName) {
2813
3582
  return projectOf(ctx, workspacePath, workspace.title ?? "");
2814
3583
  }
2815
3584
  //#endregion
2816
- //#region src/tree.ts
2817
- /** 目录在文件前、名称按小写字母序的比较器(对齐 VSCode 资源管理器习惯)。 */
2818
- function entryOrder(a, b) {
2819
- const aDir = a.type === "directory" ? 0 : 1;
2820
- const bDir = b.type === "directory" ? 0 : 1;
2821
- if (aDir !== bDir) return aDir - bDir;
2822
- const la = a.name.toLocaleLowerCase("en-US");
2823
- const lb = b.name.toLocaleLowerCase("en-US");
2824
- return la < lb ? -1 : la > lb ? 1 : 0;
2825
- }
2826
- /**
2827
- * 归一化目录相对路径:去空白、反斜杠→斜杠、去首尾斜杠、`.`→''。
2828
- * 任一路径段为 `..` 返回 null(拒绝越界)。
2829
- * @author ddj 2026年08月26号
2830
- * @param path 原始相对路径('' 表示根目录)
2831
- * @returns 归一化相对路径('' = 根),非法返回 null
2832
- */
2833
- function normalizeRel(path) {
2834
- const rel = String(path ?? "").trim().replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
2835
- if (!rel) return "";
2836
- const segs = rel.split("/").filter((seg) => seg !== ".");
2837
- if (!segs.length) return "";
2838
- for (const seg of segs) if (seg === "..") return null;
2839
- return segs.join("/");
2840
- }
2841
- /**
2842
- * fs.listDir 结果 → TreeEntry 数组:相对路径拼接 + 目录优先排序。
2843
- * @author ddj 2026年08月26号
2844
- * @param rel 父目录相对路径('' = 根)
2845
- * @param children 目录子项
2846
- * @returns 排序后的树条目
2847
- */
2848
- function toTreeEntries(rel, children) {
2849
- const out = [];
2850
- for (const child of children) {
2851
- if (!child || typeof child.name !== "string" || !child.name) continue;
2852
- out.push({
2853
- name: child.name,
2854
- path: rel ? rel + "/" + child.name : child.name,
2855
- type: child.type === "directory" ? "directory" : child.type === "file" ? "file" : "other",
2856
- size: typeof child.size === "number" ? child.size : void 0
2857
- });
2858
- }
2859
- return out.sort(entryOrder);
2860
- }
2861
- //#endregion
2862
- //#region src/reveal.ts
3585
+ //#region src/reveal.ts
2863
3586
  /**
2864
3587
  * dsh-vscode-mode host — 在 OS 文件浏览器中打开/定位路径(reveal 能力)。
2865
3588
  * 纯函数 revealCommand 平台分发 + revealInExplorer 经 ctx.subprocess.spawn 发射
@@ -2926,6 +3649,462 @@ async function revealInExplorer(ctx, absPath, isDir) {
2926
3649
  }
2927
3650
  }
2928
3651
  //#endregion
3652
+ //#region src/perf.ts
3653
+ /**
3654
+ * dsh-vscode-mode host — DSH 会话性能管理(会话卫生 + 体积 stat + 侧车摘要)。
3655
+ * 职责:
3656
+ * - 盘点 ~/.dsh/sessions 全部工作区会话(每会话仅 stat 日志文件,不解压,轻量);
3657
+ * - 移出/恢复/清除:会话目录在 ~/.dsh/sessions 与 ~/.dsh/sessions-archive 间可逆搬迁;
3658
+ * - 当前会话体积 stat(对话头部指示器用,纯 stat);
3659
+ * - 侧车摘要:agent/脚本取关键字段,避免整份读 .dsh-edit-review.json 进对话。
3660
+ * 纯函数可单测(路径编码/移出规划);fs 操作 best-effort 返回逐项结果不抛。
3661
+ * 作者 ddj 2026-09-02
3662
+ */
3663
+ /** 会话日志文件名(zstd 优先,降级纯 jsonl)。 */
3664
+ const LOG_NAMES = ["session.jsonl.zstd", "session.jsonl"];
3665
+ /** 归档根(~/.dsh/sessions-archive,与 sessions 同级,可逆移出的落点)。 */
3666
+ function sessionsArchiveRoot(home = dshHome()) {
3667
+ return join(home, "sessions-archive");
3668
+ }
3669
+ /** 路径段校验:非空、非 . / ..、不含分隔符与 Windows 非法字符(防 join 逃逸)。 */
3670
+ function validDirName(name) {
3671
+ return typeof name === "string" && name.length > 0 && name !== "." && name !== ".." && !/[/\\]/.test(name) && !/[:*?"<>|]/.test(name);
3672
+ }
3673
+ /** target 必须落在 root 内(自身或其子路径),防路径穿越。 */
3674
+ function withinDir(target, root) {
3675
+ const rel = resolve(target);
3676
+ const base = resolve(root);
3677
+ return rel === base || rel.startsWith(base + sep);
3678
+ }
3679
+ /** stat 一个会话目录里的日志文件(首个存在的),缺失 → null。 */
3680
+ async function statSessionLog(sessionDir) {
3681
+ for (const name of LOG_NAMES) try {
3682
+ const info = await stat(join(sessionDir, name));
3683
+ if (info.isFile()) return {
3684
+ size: info.size,
3685
+ mtimeMs: info.mtimeMs
3686
+ };
3687
+ } catch (error) {}
3688
+ return null;
3689
+ }
3690
+ /**
3691
+ * 盘点 ~/.dsh/sessions:按工作区分组,每会话仅 stat 日志文件体积与 mtime。
3692
+ * 不解压日志(V8 展开成本高),活跃标记由调用方传入后置。
3693
+ * @author ddj 2026年09月02号
3694
+ * @param home DSH home(测试可注入)
3695
+ * @param archive 归档根(默认 sessions-archive;测试可注入)
3696
+ * @returns 工作区聚合(按体积降序)+ 会话明细(按体积降序)+ 汇总
3697
+ */
3698
+ async function scanSessionInventory(home = dshHome(), archive = sessionsArchiveRoot(home)) {
3699
+ const root = sessionsRoot(home);
3700
+ const workspaces = [];
3701
+ const sessions = [];
3702
+ let totalBytes = 0;
3703
+ let totalCount = 0;
3704
+ const wsDirs = await readdir(root, { withFileTypes: true }).catch(() => []);
3705
+ for (const ws of wsDirs) {
3706
+ if (!ws.isDirectory() || !validDirName(ws.name)) continue;
3707
+ const wsDir = join(root, ws.name);
3708
+ const wsSessions = [];
3709
+ let wsBytes = 0;
3710
+ let minMtime = Number.POSITIVE_INFINITY;
3711
+ let maxMtime = 0;
3712
+ const sessionDirs = await readdir(wsDir, { withFileTypes: true }).catch(() => []);
3713
+ for (const sd of sessionDirs) {
3714
+ if (!sd.isDirectory()) continue;
3715
+ const info = await statSessionLog(join(wsDir, sd.name));
3716
+ if (!info) continue;
3717
+ const row = {
3718
+ sessionId: sd.name,
3719
+ workspaceKey: ws.name,
3720
+ bytes: info.size,
3721
+ mtime: info.mtimeMs,
3722
+ active: false
3723
+ };
3724
+ wsSessions.push(row);
3725
+ wsBytes += info.size;
3726
+ totalBytes += info.size;
3727
+ totalCount += 1;
3728
+ if (info.mtimeMs < minMtime) minMtime = info.mtimeMs;
3729
+ if (info.mtimeMs > maxMtime) maxMtime = info.mtimeMs;
3730
+ }
3731
+ wsSessions.sort((a, b) => b.bytes - a.bytes);
3732
+ sessions.push(...wsSessions);
3733
+ workspaces.push({
3734
+ workspaceKey: ws.name,
3735
+ sessionCount: wsSessions.length,
3736
+ totalBytes: wsBytes,
3737
+ minMtime: Number.isFinite(minMtime) ? minMtime : 0,
3738
+ maxMtime
3739
+ });
3740
+ }
3741
+ workspaces.sort((a, b) => b.totalBytes - a.totalBytes);
3742
+ return {
3743
+ workspaces,
3744
+ sessions,
3745
+ totals: {
3746
+ workspaces: workspaces.length,
3747
+ sessions: totalCount,
3748
+ totalBytes
3749
+ }
3750
+ };
3751
+ }
3752
+ /** 标记活跃会话(live id 与目录段名双向匹配,兼容编码差异)。 */
3753
+ function markActiveSessions(sessions, activeIds) {
3754
+ const raw = new Set(activeIds);
3755
+ const encoded = /* @__PURE__ */ new Set();
3756
+ for (const id of raw) encoded.add(sessionIdSegment(id));
3757
+ for (const s of sessions) if (raw.has(s.sessionId) || encoded.has(s.sessionId)) s.active = true;
3758
+ }
3759
+ /**
3760
+ * 移出规划(纯函数):从盘点中按「显式集合」或「规则(minBytes / olderThanDays)」
3761
+ * 圈选可移出会话。活跃会话一律排除;无显式集合且无规则时不选(防误移)。
3762
+ * @author ddj 2026年09月02号
3763
+ * @param inventory 盘点结果
3764
+ * @param criteria 圈选条件
3765
+ * @returns 待移出清单与释放字节
3766
+ */
3767
+ function planMoveOut(inventory, criteria) {
3768
+ const items = [];
3769
+ const explicit = Array.isArray(criteria.sessionIds) && criteria.sessionIds.length > 0;
3770
+ const cutoff = criteria.olderThanDays ? Date.now() - criteria.olderThanDays * 24 * 60 * 60 * 1e3 : 0;
3771
+ for (const s of inventory.sessions) {
3772
+ if (s.active) continue;
3773
+ if (criteria.workspaceKey && s.workspaceKey !== criteria.workspaceKey) continue;
3774
+ if (explicit) {
3775
+ if (!criteria.sessionIds.includes(s.sessionId)) continue;
3776
+ } else {
3777
+ if (criteria.minBytes && s.bytes < criteria.minBytes) continue;
3778
+ if (criteria.olderThanDays && s.mtime > cutoff) continue;
3779
+ if (!criteria.minBytes && !criteria.olderThanDays) continue;
3780
+ }
3781
+ items.push({
3782
+ workspaceKey: s.workspaceKey,
3783
+ sessionId: s.sessionId,
3784
+ bytes: s.bytes
3785
+ });
3786
+ }
3787
+ return {
3788
+ items,
3789
+ reclaimedBytes: items.reduce((sum, i) => sum + i.bytes, 0)
3790
+ };
3791
+ }
3792
+ /**
3793
+ * 移出会话目录到归档区(同卷 rename,可逆)。活跃会话拒绝;路径越界拒绝;
3794
+ * 逐项 best-effort,成功项写归档 manifest。默认不 dry-run(dryRun 时只校验不搬迁)。
3795
+ * @author ddj 2026年09月02号
3796
+ * @param home DSH home
3797
+ * @param archive 归档根
3798
+ * @param items 待移出清单(来自 planMoveOut 或用户确认)
3799
+ * @param activeIds 活跃会话 id 集合(执行期再校验一次)
3800
+ * @param dryRun 仅校验不搬迁
3801
+ */
3802
+ async function moveOutSessions(home, archive, items, activeIds, dryRun = false) {
3803
+ const root = sessionsRoot(home);
3804
+ const rawActive = new Set(activeIds);
3805
+ const encodedActive = /* @__PURE__ */ new Set();
3806
+ for (const id of rawActive) encodedActive.add(sessionIdSegment(id));
3807
+ const isActive = (id) => rawActive.has(id) || encodedActive.has(id);
3808
+ const moved = [];
3809
+ const failures = [];
3810
+ for (const item of items) {
3811
+ if (!validDirName(item.workspaceKey)) {
3812
+ failures.push({
3813
+ workspaceKey: item.workspaceKey,
3814
+ sessionId: item.sessionId,
3815
+ error: "工作区键不合法"
3816
+ });
3817
+ continue;
3818
+ }
3819
+ if (isActive(item.sessionId)) {
3820
+ failures.push({
3821
+ workspaceKey: item.workspaceKey,
3822
+ sessionId: item.sessionId,
3823
+ error: "活跃会话,拒绝移出"
3824
+ });
3825
+ continue;
3826
+ }
3827
+ const seg = sessionIdSegment(item.sessionId);
3828
+ const src = join(root, item.workspaceKey, seg);
3829
+ const dst = join(archive, item.workspaceKey, seg);
3830
+ if (!withinDir(src, root) || !withinDir(dst, archive)) {
3831
+ failures.push({
3832
+ workspaceKey: item.workspaceKey,
3833
+ sessionId: item.sessionId,
3834
+ error: "路径越界,拒绝执行"
3835
+ });
3836
+ continue;
3837
+ }
3838
+ if (dryRun) {
3839
+ try {
3840
+ if ((await stat(src)).isDirectory()) moved.push({
3841
+ ...item,
3842
+ bytes: (await statSessionLog(src))?.size ?? item.bytes
3843
+ });
3844
+ else failures.push({
3845
+ workspaceKey: item.workspaceKey,
3846
+ sessionId: item.sessionId,
3847
+ error: "源目录不存在"
3848
+ });
3849
+ } catch (error) {
3850
+ failures.push({
3851
+ workspaceKey: item.workspaceKey,
3852
+ sessionId: item.sessionId,
3853
+ error: "源目录不存在"
3854
+ });
3855
+ }
3856
+ continue;
3857
+ }
3858
+ try {
3859
+ await mkdir(dirname(dst), { recursive: true });
3860
+ await rename(src, dst);
3861
+ const bytes = (await statSessionLog(dst))?.size ?? item.bytes;
3862
+ moved.push({
3863
+ ...item,
3864
+ bytes
3865
+ });
3866
+ } catch (error) {
3867
+ failures.push({
3868
+ workspaceKey: item.workspaceKey,
3869
+ sessionId: item.sessionId,
3870
+ error: String(error)
3871
+ });
3872
+ }
3873
+ }
3874
+ if (moved.length && !dryRun) await appendMoveManifest(archive, moved);
3875
+ return {
3876
+ moved,
3877
+ failures,
3878
+ reclaimedBytes: moved.reduce((sum, i) => sum + i.bytes, 0)
3879
+ };
3880
+ }
3881
+ /** 恢复单个会话:归档 → sessions。目标已存在则拒绝(不覆盖)。 */
3882
+ async function restoreSession(home, archive, workspaceKey, sessionId) {
3883
+ if (!validDirName(workspaceKey)) return {
3884
+ ok: false,
3885
+ error: "工作区键不合法"
3886
+ };
3887
+ const seg = sessionIdSegment(sessionId);
3888
+ const root = sessionsRoot(home);
3889
+ const src = join(archive, workspaceKey, seg);
3890
+ const dst = join(root, workspaceKey, seg);
3891
+ if (!withinDir(src, archive) || !withinDir(dst, root)) return {
3892
+ ok: false,
3893
+ error: "路径越界,拒绝执行"
3894
+ };
3895
+ try {
3896
+ await stat(src);
3897
+ } catch (error) {
3898
+ return {
3899
+ ok: false,
3900
+ error: "归档中不存在该会话"
3901
+ };
3902
+ }
3903
+ try {
3904
+ await stat(dst);
3905
+ return {
3906
+ ok: false,
3907
+ error: "目标位置已存在同名会话"
3908
+ };
3909
+ } catch (error) {}
3910
+ try {
3911
+ await mkdir(dirname(dst), { recursive: true });
3912
+ await rename(src, dst);
3913
+ return { ok: true };
3914
+ } catch (error) {
3915
+ return {
3916
+ ok: false,
3917
+ error: String(error)
3918
+ };
3919
+ }
3920
+ }
3921
+ /** 清除归档区早于 N 天的会话(破坏性,仅限归档区)。 */
3922
+ async function purgeArchive(archive, olderThanDays) {
3923
+ const cutoff = Date.now() - olderThanDays * 24 * 60 * 60 * 1e3;
3924
+ let removed = 0;
3925
+ let reclaimedBytes = 0;
3926
+ const wsDirs = await readdir(archive, { withFileTypes: true }).catch(() => []);
3927
+ for (const ws of wsDirs) {
3928
+ if (!ws.isDirectory()) continue;
3929
+ const wsDir = join(archive, ws.name);
3930
+ const sessionDirs = await readdir(wsDir, { withFileTypes: true }).catch(() => []);
3931
+ for (const sd of sessionDirs) {
3932
+ if (!sd.isDirectory()) continue;
3933
+ const info = await statSessionLog(join(wsDir, sd.name));
3934
+ if (!info || info.mtimeMs > cutoff) continue;
3935
+ try {
3936
+ await rm(join(wsDir, sd.name), {
3937
+ recursive: true,
3938
+ force: true
3939
+ });
3940
+ removed += 1;
3941
+ reclaimedBytes += info.size;
3942
+ } catch (error) {}
3943
+ }
3944
+ }
3945
+ return {
3946
+ removed,
3947
+ reclaimedBytes
3948
+ };
3949
+ }
3950
+ /** 当前会话持久化体积(对话头部指示器用):仅 stat,近零成本。 */
3951
+ async function sessionSizeOf(home, cwd, sessionId) {
3952
+ const dir = join(sessionsRoot(home), sessionWorkspaceKey(cwd), sessionIdSegment(sessionId));
3953
+ for (const name of LOG_NAMES) try {
3954
+ const info = await stat(join(dir, name));
3955
+ if (info.isFile()) return {
3956
+ bytes: info.size,
3957
+ exists: true
3958
+ };
3959
+ } catch (error) {}
3960
+ return {
3961
+ bytes: 0,
3962
+ exists: false
3963
+ };
3964
+ }
3965
+ /**
3966
+ * 侧车摘要:返回活跃记录数、每文件 pending 计数、归档体积。
3967
+ * agent/脚本取关键字段即可,不必整份读 .dsh-edit-review.json(避免大对象进对话)。
3968
+ * @author ddj 2026年09月02号
3969
+ * @param ctx DSH host 上下文(fs 服务)
3970
+ * @param cwd 工作区路径(null 时返回空摘要)
3971
+ */
3972
+ async function sidecarSummaryOf(ctx, cwd) {
3973
+ const fs = ctx.get("fs");
3974
+ const summary = {
3975
+ active: 0,
3976
+ pendingByFile: [],
3977
+ archiveBytes: 0
3978
+ };
3979
+ if (!fs || !cwd) return summary;
3980
+ try {
3981
+ const recs = parseSidecar(await fs.readText(await fs.resolve(SIDECAR, { cwd })))?.workspaces?.[cwd]?.records;
3982
+ if (recs) {
3983
+ const byFile = /* @__PURE__ */ new Map();
3984
+ let active = 0;
3985
+ for (const rec of Object.values(recs)) {
3986
+ if (rec.archived) continue;
3987
+ active += 1;
3988
+ const sm = recSummary(rec);
3989
+ if (sm.pending > 0 && rec.path) byFile.set(rec.path, (byFile.get(rec.path) ?? 0) + sm.pending);
3990
+ }
3991
+ summary.active = active;
3992
+ summary.pendingByFile = [...byFile.entries()].map(([path, pending]) => ({
3993
+ path,
3994
+ pending
3995
+ })).sort((a, b) => b.pending - a.pending);
3996
+ }
3997
+ try {
3998
+ summary.archiveBytes = (await fs.stat(await fs.resolve(".dsh-edit-review-archive.json", { cwd })))?.size ?? 0;
3999
+ } catch (error) {}
4000
+ } catch (error) {}
4001
+ return summary;
4002
+ }
4003
+ /** 解析归档 manifest(损坏/缺失 → 空)。 */
4004
+ function parseMoveManifest(text) {
4005
+ if (!text) return {
4006
+ version: 1,
4007
+ items: []
4008
+ };
4009
+ try {
4010
+ const data = JSON.parse(text);
4011
+ return {
4012
+ version: 1,
4013
+ items: Array.isArray(data.items) ? data.items : []
4014
+ };
4015
+ } catch (error) {
4016
+ return {
4017
+ version: 1,
4018
+ items: []
4019
+ };
4020
+ }
4021
+ }
4022
+ /** 追加移出记录到归档 manifest(best-effort,失败不影响主流程)。 */
4023
+ async function appendMoveManifest(archive, items) {
4024
+ try {
4025
+ const file = join(archive, ".manifest.json");
4026
+ const prev = parseMoveManifest(await readFile(file, "utf8").catch(() => null));
4027
+ const at = (/* @__PURE__ */ new Date()).toISOString();
4028
+ for (const item of items) prev.items.push({
4029
+ at,
4030
+ workspaceKey: item.workspaceKey,
4031
+ sessionId: item.sessionId
4032
+ });
4033
+ await mkdir(archive, { recursive: true });
4034
+ await writeFile(file, JSON.stringify(prev, null, 2), "utf8");
4035
+ } catch (error) {}
4036
+ }
4037
+ //#endregion
4038
+ //#region src/perfPatch.ts
4039
+ /**
4040
+ * dsh-vscode-mode host — profile patch 压缩调优(文本行级,零 YAML 依赖)。
4041
+ * 在 ~/.dsh/profiles/<profile>/cordis.patch.yml 末尾追加/移除一段带标记注释的
4042
+ * id 定向覆盖块(compaction-basic / tool-result-pruner 更低阈值)。
4043
+ * 纯文本函数可单测:幂等追加、整段移除、无块原样返回。
4044
+ * 作者 ddj 2026-09-02
4045
+ */
4046
+ /** 性能块起始标记注释(文件内唯一锚点)。 */
4047
+ const PERF_MARK_START = "# dsh-vscode-mode perf tuning (start)";
4048
+ /** 性能块结束标记注释。 */
4049
+ const PERF_MARK_END = "# dsh-vscode-mode perf tuning (end)";
4050
+ /**
4051
+ * 建议的压缩调优 YAML 块(顶层 loader patch 条目,id 定向覆盖)。
4052
+ * - compaction-basic:thresholdRatio 0.8→0.6、retainRatio 0.16→0.12(更早触发压缩);
4053
+ * - tool-result-pruner:thresholdChars 8192→4096、head/tail 同步收窄(大 tool result 提前截断)。
4054
+ * 注意:压缩只降低模型可见上下文压力,不重写已持久化会话日志(append-only)。
4055
+ * @author ddj 2026年09月02号
4056
+ * @returns 带前后标记注释与尾换行的 YAML 块
4057
+ */
4058
+ function perfConfigBlock() {
4059
+ return [
4060
+ PERF_MARK_START,
4061
+ "# 更早触发会话内压缩:降低模型可见上下文压力(仅影响后续轮次,不缩小已持久化日志)。",
4062
+ "- id: compaction-basic",
4063
+ " config:",
4064
+ " thresholdRatio: 0.6",
4065
+ " retainRatio: 0.12",
4066
+ "- id: tool-result-pruner",
4067
+ " config:",
4068
+ " thresholdChars: 4096",
4069
+ " headChars: 2048",
4070
+ " tailChars: 512",
4071
+ PERF_MARK_END,
4072
+ ""
4073
+ ].join("\n");
4074
+ }
4075
+ /** 当前文本是否已包含性能块。 */
4076
+ function patchHasPerfConfig(text) {
4077
+ return text.includes(PERF_MARK_START);
4078
+ }
4079
+ /**
4080
+ * 移除性能块(起止标记注释之间的整段,含所在行),无块时原样返回。
4081
+ * 移除后压缩多余空行、确保以单个换行结尾。
4082
+ * @author ddj 2026年09月02号
4083
+ * @param text 原 patch 文本
4084
+ * @returns 移除后的文本
4085
+ */
4086
+ function patchRemovePerfConfig(text) {
4087
+ const start = text.indexOf(PERF_MARK_START);
4088
+ if (start === -1) return text;
4089
+ const end = text.indexOf(PERF_MARK_END, start);
4090
+ const lineStart = text.lastIndexOf("\n", start - 1) + 1;
4091
+ if (end === -1) return text.slice(0, lineStart).replace(/\n+$/, "\n");
4092
+ const after = (() => {
4093
+ const nl = text.indexOf("\n", end);
4094
+ return nl === -1 ? text.length : nl + 1;
4095
+ })();
4096
+ return (text.slice(0, lineStart) + text.slice(after)).replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");
4097
+ }
4098
+ /**
4099
+ * 幂等追加性能块:已有块先整段移除再追加到文件末尾(保证全文件只有一份、位置固定)。
4100
+ * @author ddj 2026年09月02号
4101
+ * @param text 原 patch 文本
4102
+ * @returns 追加后的文本
4103
+ */
4104
+ function patchInsertPerfConfig(text) {
4105
+ return patchRemovePerfConfig(text).replace(/\n+$/, "") + "\n" + perfConfigBlock();
4106
+ }
4107
+ //#endregion
2929
4108
  //#region src/rpc.ts
2930
4109
  /** cwd → 内存缓冲:行数组 + 累计字节数 + 待触发 flush 定时器(攒批落盘,避免每条日志全文件读改写)。 */
2931
4110
  const debugBuffers = /* @__PURE__ */ new Map();
@@ -2944,13 +4123,12 @@ const STALE_CHECK_MIN_MS = 1e4;
2944
4123
  /**
2945
4124
  * 调试日志入队:先攒内存缓冲(行 + 字节数),满 DEBUG_BUF_CAP 立即落盘,
2946
4125
  * 否则空闲 DEBUG_FLUSH_IDLE_MS 后落盘;落盘 = 读旧文件 + 追加整批 + 超限截断 + 一次写入。
2947
- * @author ddj 2026年08月26号
4126
+ * @author ddj 2026年08月26号 / 2026年09月01号
2948
4127
  * @param ctx DSH 上下文
2949
- * @param cwd 工作区(日志文件按工作区旁车存放)
2950
- * @param policy 会话沙箱策略(入队时刻捕获)
4128
+ * @param cwd 工作区(日志按 cwd hash 存 ~/.dsh/dsh-vscode-mode/logs/)
2951
4129
  * @param line 单条日志文本(不含换行)
2952
4130
  */
2953
- function enqueueDebug(ctx, cwd, policy, line) {
4131
+ function enqueueDebug(ctx, cwd, line) {
2954
4132
  const st = debugBuffers.get(cwd) ?? {
2955
4133
  lines: [],
2956
4134
  len: 0,
@@ -2963,7 +4141,7 @@ function enqueueDebug(ctx, cwd, policy, line) {
2963
4141
  const batch = st.lines;
2964
4142
  st.lines = [];
2965
4143
  st.len = 0;
2966
- flushDebug(ctx, cwd, policy, batch);
4144
+ flushDebug(cwd, batch);
2967
4145
  };
2968
4146
  if (st.len >= DEBUG_BUF_CAP) {
2969
4147
  if (st.timer) clearTimeout(st.timer);
@@ -2971,24 +4149,27 @@ function enqueueDebug(ctx, cwd, policy, line) {
2971
4149
  } else if (!st.timer) st.timer = setTimeout(flush, DEBUG_FLUSH_IDLE_MS);
2972
4150
  debugBuffers.set(cwd, st);
2973
4151
  }
4152
+ /** cwd → 已清理旧工作区 debug 日志标记(一次性)。 */
4153
+ const debugLegacyCleaned = /* @__PURE__ */ new Set();
2974
4154
  /**
2975
4155
  * 批量落盘一条 debug 日志缓冲:读旧文件 → 追加 → 超上限截断保留尾部 → 一次写回。
2976
4156
  * 串行链保证同一 cwd 的读改写不交错丢行;写失败静默忽略(调试日志不阻塞业务)。
2977
- * @author ddj 2026年08月26号
2978
- * @param ctx DSH 上下文
2979
- * @param cwd 工作区
2980
- * @param policy 会话沙箱策略(enqueue 时刻捕获)
4157
+ * @author ddj 2026年08月26号 / 2026年09月01号
4158
+ * @param cwd 工作区(日志落 ~/.dsh/dsh-vscode-mode/logs/debug.<cwdHash>.log)
2981
4159
  * @param batch 本批日志行
2982
4160
  */
2983
- function flushDebug(ctx, cwd, policy, batch) {
4161
+ function flushDebug(cwd, batch) {
2984
4162
  const task = (debugWriteQueues.get(cwd) ?? Promise.resolve()).then(async () => {
2985
4163
  try {
2986
- const fs = ctx.get("fs");
2987
- if (!fs) return;
2988
- const target = await fs.resolve(".dsh-edit-review-debug.log", { cwd });
2989
- const appended = await fs.readText(target).catch(() => "") + batch.map((l) => (/* @__PURE__ */ new Date()).toISOString() + " " + l + "\n").join("");
4164
+ await mkdir(pluginLogRoot(), { recursive: true });
4165
+ const target = debugLogFile(cwd);
4166
+ const appended = await readFile(target, "utf8").catch(() => "") + batch.map((l) => (/* @__PURE__ */ new Date()).toISOString() + " " + l + "\n").join("");
2990
4167
  const next = appended.length > DEBUG_LOG_CAP ? appended.slice(-Math.floor(DEBUG_LOG_CAP / 2)) : appended;
2991
- await fs.writeText(target, next, void 0, void 0, policy);
4168
+ await writeFile(target, next, "utf8");
4169
+ if (!debugLegacyCleaned.has(cwd)) {
4170
+ debugLegacyCleaned.add(cwd);
4171
+ await rm(join(cwd, DEBUG_LOG), { force: true }).catch(() => {});
4172
+ }
2992
4173
  } catch (e) {}
2993
4174
  });
2994
4175
  debugWriteQueues.set(cwd, task);
@@ -3073,9 +4254,31 @@ async function applyDecisions(ctx, session, cwd, bucket, items) {
3073
4254
  if (changed) await saveBucket(ctx, cwd, bucket, session);
3074
4255
  return results;
3075
4256
  }
4257
+ /** 当前活跃会话 id 集合(live sessions,用于移出/恢复护栏)。 */
4258
+ function activeSessionIds(ctx) {
4259
+ const sessions = ctx.get("sessions");
4260
+ if (!sessions || typeof sessions.list !== "function") return /* @__PURE__ */ new Set();
4261
+ return new Set(sessions.list().map((s) => s.id).filter(Boolean));
4262
+ }
4263
+ /** profile patch 文件定位(依赖本插件的 profile,同 devForm)。 */
4264
+ async function patchFileInfo() {
4265
+ const profileDir = findProfileDir();
4266
+ if (!profileDir) return {};
4267
+ return {
4268
+ profileDir,
4269
+ patchPath: join(profileDir, "cordis.patch.yml")
4270
+ };
4271
+ }
4272
+ /** 最新一次压缩配置备份文件名(cordis.patch.yml.bak-<ts>,按名倒序取新)。 */
4273
+ async function latestPatchBackup(patchPath) {
4274
+ const dir = join(patchPath, "..");
4275
+ const base = basename(patchPath);
4276
+ return (await readdir(dir).catch(() => [])).filter((n) => n.startsWith(base + ".bak-")).sort().reverse()[0];
4277
+ }
3076
4278
  /** 各方法 handler 表(类型由 shared/rpc 的 RpcHandlerMap 约束)。 */
3077
- function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearcher = newContentSearcher(ctx)) {
4279
+ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearcher = newContentSearcher(ctx), lspHandlers) {
3078
4280
  return {
4281
+ ...lspHandlers ?? {},
3079
4282
  "edrv.list": async (args) => {
3080
4283
  const sc = await requireSession(ctx, args.sessionId);
3081
4284
  if ("err" in sc) return {
@@ -3261,6 +4464,7 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
3261
4464
  try {
3262
4465
  const target = await resolveTarget(ctx, sc.session, args.path);
3263
4466
  await fs.writeText(target, args.content, void 0, void 0, policyOf(ctx, sc.session));
4467
+ invalidateIndex(ctx, sc.cwd, args.path);
3264
4468
  const bucket = await bucketOf(registry, ctx, sc.cwd);
3265
4469
  let changed = false;
3266
4470
  for (const rec of bucket.values()) if (rec.path === args.path && rec.superseded !== true) {
@@ -3397,7 +4601,7 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
3397
4601
  error: sc.err
3398
4602
  };
3399
4603
  const text = String(args.text ?? "");
3400
- enqueueDebug(ctx, sc.cwd, policyOf(ctx, sc.session), text);
4604
+ enqueueDebug(ctx, sc.cwd, text);
3401
4605
  console.error("[edrv-debug] " + text);
3402
4606
  return { ok: true };
3403
4607
  },
@@ -3454,40 +4658,22 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
3454
4658
  ok: false,
3455
4659
  error: sc.err
3456
4660
  };
3457
- const fs = ctx.get("fs");
3458
- if (!fs) return {
4661
+ const rel = normalizeRel(args.path);
4662
+ if (rel === null) return {
3459
4663
  ok: false,
3460
- error: "缺少 fs"
4664
+ error: "路径不合法"
4665
+ };
4666
+ const res = await listDirCached(ctx, sc.cwd, rel, args.force === true);
4667
+ if ("error" in res) return {
4668
+ ok: false,
4669
+ error: res.error
4670
+ };
4671
+ return {
4672
+ ok: true,
4673
+ root: res.root,
4674
+ path: rel,
4675
+ entries: res.entries
3461
4676
  };
3462
- try {
3463
- const rel = normalizeRel(args.path);
3464
- if (rel === null) return {
3465
- ok: false,
3466
- error: "路径不合法"
3467
- };
3468
- const target = await resolveTarget(ctx, sc.session, rel || ".");
3469
- const info = await fs.stat(target);
3470
- if (!info) return {
3471
- ok: false,
3472
- error: "目录不存在"
3473
- };
3474
- if (info.type !== "directory") return {
3475
- ok: false,
3476
- error: "不是目录"
3477
- };
3478
- const children = await fs.listDir(target);
3479
- return {
3480
- ok: true,
3481
- root: fs.processPath(await fs.resolve(".", { cwd: sc.cwd })),
3482
- path: rel,
3483
- entries: toTreeEntries(rel, children)
3484
- };
3485
- } catch (error) {
3486
- return {
3487
- ok: false,
3488
- error: "读取目录失败:" + String(error)
3489
- };
3490
- }
3491
4677
  },
3492
4678
  "edrv.revealInExplorer": async (args) => {
3493
4679
  const sc = await requireSession(ctx, args.sessionId);
@@ -3705,58 +4891,263 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
3705
4891
  error: String(error)
3706
4892
  };
3707
4893
  }
3708
- }
3709
- };
3710
- }
3711
- /**
3712
- * 统一入口:按方法分发到 handler 表。
3713
- * @author ddj 2026年08月20号 / 2026年08月26号
3714
- */
3715
- async function handleRpc(ctx, registry, method, args, searcher = newSearcher(ctx), contentSearcher = newContentSearcher(ctx)) {
3716
- const handler = buildHandlers(ctx, registry, searcher, contentSearcher)[method];
3717
- if (!handler) return {
3718
- ok: false,
3719
- error: "未知方法: " + String(method)
3720
- };
3721
- return handler(args);
3722
- }
3723
- //#endregion
3724
- //#region src/mcpIsolation.ts
3725
- /**
3726
- * dsh-vscode-mode host — 项目 MCP 的 agent 级隔离。
3727
- * 项目 MCP 连接仍由 Host 维护,但每个 agent 只继承当前 workspace 的项目工具;
3728
- * tools.restrict() 负责模型可见性,tools.guard() 负责执行层兜底拒绝。
3729
- * 作者 ddj 2026年08月22号
3730
- */
3731
- const UNKNOWN_PROJECT = "__unknown_project__";
3732
- /** 统一 workspace 路径,供 cwd 父路径匹配。 */
3733
- function normalizePath(path) {
3734
- return String(path ?? "").replace(/\\/g, "/").replace(/\/+/g, "/").replace(/\/$/, "").toLowerCase();
3735
- }
3736
- /** 在已注册 workspace 中匹配 cwd,优先最长父路径。 */
3737
- function matchWorkspace(cwd, paths) {
3738
- if (!cwd) return void 0;
3739
- const target = normalizePath(cwd);
3740
- const matches = paths.filter((path) => {
3741
- const root = normalizePath(path);
3742
- return target === root || target.startsWith(root + "/");
3743
- });
3744
- matches.sort((a, b) => normalizePath(b).length - normalizePath(a).length);
3745
- return matches[0];
3746
- }
3747
- /** 根据 entry id 判断是否为新旧格式项目 MCP(兼容层统一判定)。 */
3748
- const isProjectEntry = isProjectEntryId;
3749
- /** 返回一个 agent 不应继承的项目工具名。 */
3750
- function denyTools(toolProjects, currentPath) {
3751
- return [...toolProjects.entries()].filter(([, owner]) => owner !== currentPath).map(([name]) => name).sort();
3752
- }
3753
- /** 从 loader entry 和工具注册表建立工具到项目路径的映射。 */
3754
- function projectTools(ctx) {
3755
- const result = /* @__PURE__ */ new Map();
3756
- const workspaces = ctx.get("workspaceRegistry")?.list?.() ?? [];
3757
- const byHash = new Map(workspaces.map((ws) => [hashWorkspace(ws.path), ws.path]));
3758
- const visible = ctx.get("tools")?.view?.(void 0)?.visible;
3759
- if (!(visible instanceof Map)) return result;
4894
+ },
4895
+ "edrv.perf.inventory": async () => {
4896
+ try {
4897
+ const home = dshHome();
4898
+ const inventory = await scanSessionInventory(home, sessionsArchiveRoot(home));
4899
+ const active = activeSessionIds(ctx);
4900
+ markActiveSessions(inventory.sessions, active);
4901
+ return {
4902
+ ok: true,
4903
+ ...inventory,
4904
+ activeIds: [...active]
4905
+ };
4906
+ } catch (error) {
4907
+ return {
4908
+ ok: false,
4909
+ error: "盘点失败:" + String(error)
4910
+ };
4911
+ }
4912
+ },
4913
+ "edrv.perf.sessionSize": async (args) => {
4914
+ try {
4915
+ return {
4916
+ ok: true,
4917
+ ...await sessionSizeOf(dshHome(), args.cwd, args.sessionId ?? "")
4918
+ };
4919
+ } catch (error) {
4920
+ return {
4921
+ ok: false,
4922
+ error: "读取会话体积失败:" + String(error)
4923
+ };
4924
+ }
4925
+ },
4926
+ "edrv.perf.movePlan": async (args) => {
4927
+ try {
4928
+ const home = dshHome();
4929
+ const inventory = await scanSessionInventory(home, sessionsArchiveRoot(home));
4930
+ markActiveSessions(inventory.sessions, activeSessionIds(ctx));
4931
+ return {
4932
+ ok: true,
4933
+ ...planMoveOut(inventory, {
4934
+ workspaceKey: args.workspaceKey,
4935
+ sessionIds: args.sessionIds,
4936
+ minBytes: args.minBytes,
4937
+ olderThanDays: args.olderThanDays
4938
+ })
4939
+ };
4940
+ } catch (error) {
4941
+ return {
4942
+ ok: false,
4943
+ error: "移出规划失败:" + String(error)
4944
+ };
4945
+ }
4946
+ },
4947
+ "edrv.perf.moveOut": async (args) => {
4948
+ try {
4949
+ const home = dshHome();
4950
+ const items = (args.sessionIds ?? []).map((sessionId) => ({
4951
+ workspaceKey: args.workspaceKey,
4952
+ sessionId,
4953
+ bytes: 0
4954
+ }));
4955
+ return {
4956
+ ok: true,
4957
+ ...await moveOutSessions(home, sessionsArchiveRoot(home), items, activeSessionIds(ctx), args.dryRun === true)
4958
+ };
4959
+ } catch (error) {
4960
+ return {
4961
+ ok: false,
4962
+ error: "移出失败:" + String(error)
4963
+ };
4964
+ }
4965
+ },
4966
+ "edrv.perf.restore": async (args) => {
4967
+ try {
4968
+ const result = await restoreSession(dshHome(), sessionsArchiveRoot(dshHome()), args.workspaceKey, args.sessionId);
4969
+ return result.ok ? {
4970
+ ok: true,
4971
+ restored: true
4972
+ } : {
4973
+ ok: false,
4974
+ error: result.error ?? "恢复失败"
4975
+ };
4976
+ } catch (error) {
4977
+ return {
4978
+ ok: false,
4979
+ error: "恢复失败:" + String(error)
4980
+ };
4981
+ }
4982
+ },
4983
+ "edrv.perf.purgeArchive": async (args) => {
4984
+ try {
4985
+ return {
4986
+ ok: true,
4987
+ ...await purgeArchive(sessionsArchiveRoot(dshHome()), args.olderThanDays)
4988
+ };
4989
+ } catch (error) {
4990
+ return {
4991
+ ok: false,
4992
+ error: "清除失败:" + String(error)
4993
+ };
4994
+ }
4995
+ },
4996
+ "edrv.perf.sidecarSummary": async (args) => {
4997
+ const sc = await requireSession(ctx, args.sessionId);
4998
+ const cwd = "err" in sc ? null : sc.cwd;
4999
+ try {
5000
+ return {
5001
+ ok: true,
5002
+ ...await sidecarSummaryOf(ctx, cwd)
5003
+ };
5004
+ } catch (error) {
5005
+ return {
5006
+ ok: false,
5007
+ error: "读取侧车摘要失败:" + String(error)
5008
+ };
5009
+ }
5010
+ },
5011
+ "edrv.perf.configGet": async () => {
5012
+ try {
5013
+ const { profileDir, patchPath } = await patchFileInfo();
5014
+ const block = perfConfigBlock();
5015
+ if (!patchPath) return {
5016
+ ok: true,
5017
+ applied: false,
5018
+ block
5019
+ };
5020
+ const text = await readFile(patchPath, "utf8").catch(() => "");
5021
+ const backup = await latestPatchBackup(patchPath);
5022
+ return {
5023
+ ok: true,
5024
+ profileDir,
5025
+ patchPath,
5026
+ applied: patchHasPerfConfig(text),
5027
+ block,
5028
+ backup
5029
+ };
5030
+ } catch (error) {
5031
+ return {
5032
+ ok: false,
5033
+ error: "读取压缩配置失败:" + String(error)
5034
+ };
5035
+ }
5036
+ },
5037
+ "edrv.perf.configApply": async () => {
5038
+ try {
5039
+ const { patchPath } = await patchFileInfo();
5040
+ if (!patchPath) return {
5041
+ ok: false,
5042
+ error: "未找到依赖本插件的 profile(检查 DSH_HOME/profiles)"
5043
+ };
5044
+ const text = await readFile(patchPath, "utf8").catch(() => "");
5045
+ const backup = patchPath + ".bak-" + Date.now();
5046
+ if (text) await writeFile(backup, text, "utf8");
5047
+ await mkdir(join(patchPath, ".."), { recursive: true });
5048
+ await writeFile(patchPath, patchInsertPerfConfig(text), "utf8");
5049
+ return {
5050
+ ok: true,
5051
+ applied: true,
5052
+ backup,
5053
+ restart: true
5054
+ };
5055
+ } catch (error) {
5056
+ return {
5057
+ ok: false,
5058
+ error: "写入压缩配置失败:" + String(error)
5059
+ };
5060
+ }
5061
+ },
5062
+ "edrv.perf.configUndo": async () => {
5063
+ try {
5064
+ const { patchPath } = await patchFileInfo();
5065
+ if (!patchPath) return {
5066
+ ok: false,
5067
+ error: "未找到依赖本插件的 profile(检查 DSH_HOME/profiles)"
5068
+ };
5069
+ const backup = await latestPatchBackup(patchPath);
5070
+ if (backup) {
5071
+ const backupPath = join(join(patchPath, ".."), backup);
5072
+ const saved = await readFile(backupPath, "utf8").catch(() => null);
5073
+ if (saved !== null) {
5074
+ await writeFile(patchPath, saved, "utf8");
5075
+ await rm(backupPath, { force: true }).catch(() => {});
5076
+ return {
5077
+ ok: true,
5078
+ restored: true,
5079
+ backup
5080
+ };
5081
+ }
5082
+ }
5083
+ const text = await readFile(patchPath, "utf8").catch(() => "");
5084
+ if (!patchHasPerfConfig(text)) return {
5085
+ ok: false,
5086
+ error: "没有可撤销的压缩配置"
5087
+ };
5088
+ await writeFile(patchPath, patchRemovePerfConfig(text), "utf8");
5089
+ return {
5090
+ ok: true,
5091
+ restored: true
5092
+ };
5093
+ } catch (error) {
5094
+ return {
5095
+ ok: false,
5096
+ error: "撤销压缩配置失败:" + String(error)
5097
+ };
5098
+ }
5099
+ }
5100
+ };
5101
+ }
5102
+ /**
5103
+ * 统一入口:按方法分发到 handler 表。
5104
+ * @author ddj 2026年08月20号 / 2026年08月26号
5105
+ */
5106
+ async function handleRpc(ctx, registry, method, args, searcher = newSearcher(ctx), contentSearcher = newContentSearcher(ctx), lspHandlers) {
5107
+ const handler = buildHandlers(ctx, registry, searcher, contentSearcher, lspHandlers)[method];
5108
+ if (!handler) return {
5109
+ ok: false,
5110
+ error: "未知方法: " + String(method)
5111
+ };
5112
+ return handler(args);
5113
+ }
5114
+ //#endregion
5115
+ //#region src/mcpIsolation.ts
5116
+ /**
5117
+ * dsh-vscode-mode host — 项目 MCP 的 agent 级隔离。
5118
+ * 项目 MCP 连接仍由 Host 维护,但每个 agent 只继承当前 workspace 的项目工具;
5119
+ * tools.restrict() 负责模型可见性,tools.guard() 负责执行层兜底拒绝。
5120
+ * 作者 ddj 2026年08月22号
5121
+ */
5122
+ const UNKNOWN_PROJECT = "__unknown_project__";
5123
+ /** 统一 workspace 路径,供 cwd 父路径匹配。 */
5124
+ function normalizePath(path) {
5125
+ return String(path ?? "").replace(/\\/g, "/").replace(/\/+/g, "/").replace(/\/$/, "").toLowerCase();
5126
+ }
5127
+ /** 在已注册 workspace 中匹配 cwd,优先最长父路径。 */
5128
+ function matchWorkspace(cwd, paths) {
5129
+ if (!cwd) return void 0;
5130
+ const target = normalizePath(cwd);
5131
+ const matches = paths.filter((path) => {
5132
+ const root = normalizePath(path);
5133
+ return target === root || target.startsWith(root + "/");
5134
+ });
5135
+ matches.sort((a, b) => normalizePath(b).length - normalizePath(a).length);
5136
+ return matches[0];
5137
+ }
5138
+ /** 根据 entry id 判断是否为新旧格式项目 MCP(兼容层统一判定)。 */
5139
+ const isProjectEntry = isProjectEntryId;
5140
+ /** 返回一个 agent 不应继承的项目工具名。 */
5141
+ function denyTools(toolProjects, currentPath) {
5142
+ return [...toolProjects.entries()].filter(([, owner]) => owner !== currentPath).map(([name]) => name).sort();
5143
+ }
5144
+ /** 从 loader entry 和工具注册表建立工具到项目路径的映射。 */
5145
+ function projectTools(ctx) {
5146
+ const result = /* @__PURE__ */ new Map();
5147
+ const workspaces = ctx.get("workspaceRegistry")?.list?.() ?? [];
5148
+ const byHash = new Map(workspaces.map((ws) => [hashWorkspace(ws.path), ws.path]));
5149
+ const visible = ctx.get("tools")?.view?.(void 0)?.visible;
5150
+ if (!(visible instanceof Map)) return result;
3760
5151
  for (const entry of entriesOf(ctx)) {
3761
5152
  const id = String(entry.id ?? entry.options?.id ?? "");
3762
5153
  if (!isProjectEntry(id)) continue;
@@ -3856,54 +5247,1960 @@ function dropFileIndex(cwd) {
3856
5247
  fileIndex.delete(cwd);
3857
5248
  }
3858
5249
  //#endregion
3859
- //#region src/index.ts
5250
+ //#region src/lsp/jsonrpc.ts
3860
5251
  /**
3861
- * @dsh-external 生态 → dsh-vscode-mode:DSH 上的类 VSCode 编码体验(Host 半入口)。
3862
- * 职责:装配 capture(tools/result 捕获 edit/write 差异)、RPC 分发(edrv.* / mcp.* / vscode.* / compat)、
3863
- * webServer 路由(/edrv/rpc、/edrv/assets/*、/edrv/vendor/*)、工作区旁车持久化、
3864
- * 兼容层装配(依赖守卫 + 重复装配自诊断 + 启动日志兼容性报告)。
3865
- * 结构:shared/(双面契约) + model/store/capture/workspace/revert/registry/rpc/routes/compat(host 模块)。
3866
- * 作者 ddj 2026-08-20
5252
+ * dsh-vscode-mode host — LSP JSON-RPC 帧编解码(纯函数,node 可测)。
5253
+ * LSP over stdio 用 Content-Length 帧:
5254
+ * Content-Length: <n>\r\n\r\n<恰好 n 字节 JSON>
5255
+ * 导出:encodeMessage(对象→帧 Buffer)、createFrameParser(流式粘包/拆包)、parseFrame(单帧)。
5256
+ * 作者 ddj 2026-08-27
3867
5257
  */
3868
- const name = "dsh-vscode-mode";
3869
- const inject = [
3870
- "sessions",
3871
- "fs",
3872
- "webServer",
3873
- "loader",
3874
- "tools",
3875
- "workspaceRegistry",
3876
- "agents"
3877
- ];
5258
+ const HEADER_END = "\r\n\r\n";
5259
+ /** 消息头正则(仅取 Content-Length;其它头忽略)。 */
5260
+ const HEADER_RE = /^Content-Length:\s*(\d+)\s*$/im;
5261
+ /** 将对象编码为带 Content-Length 头的帧。 */
5262
+ function encodeMessage(message) {
5263
+ const body = JSON.stringify(message);
5264
+ const head = "Content-Length: " + Buffer.byteLength(body, "utf8") + "\r\n\r\n";
5265
+ return Buffer.from(head + body, "utf8");
5266
+ }
3878
5267
  /**
3879
- * 装配插件:挂事件监听、注册路由、安装兼容层。
3880
- * @author ddj 2026年08月20号
3881
- * @param ctx DSH 上下文(sessions/fs/webServer 由 inject 提供;sandboxPolicy/subprocess 惰性获取)
3882
- * @param config 插件配置(可选 imageDir 覆盖图标目录)
5268
+ * 尝试从 buffer 解析单个帧;成功返回 { body, rest },数据不足返回 null。
5269
+ * @author ddj 2026年08月27号
5270
+ * @param buffer 已累积的字节
5271
+ * @returns 解析结果(rest 为剩余未消费字节)
3883
5272
  */
3884
- function apply(ctx, config) {
3885
- const registry = /* @__PURE__ */ new Map();
3886
- const searcher = newSearcher(ctx);
3887
- const contentSearcher = newContentSearcher(ctx);
3888
- /** 兼容性警告收集(route 护栏等写入,启动日志一并输出)。 */
3889
- const warnings = [];
3890
- setupOpenSettings(ctx, config, () => {});
3891
- ctx.on("tools/result", (exec, result) => {
3892
- captureToolResult(ctx, registry, exec, result);
5273
+ function parseFrame(buffer) {
5274
+ const headEnd = buffer.indexOf(HEADER_END);
5275
+ if (headEnd < 0) return null;
5276
+ const header = buffer.subarray(0, headEnd).toString("utf8");
5277
+ const m = HEADER_RE.exec(header);
5278
+ if (!m) {
5279
+ const next = buffer.indexOf("Content-Length:");
5280
+ return next >= 0 ? parseFrame(buffer.subarray(next)) : null;
5281
+ }
5282
+ const length = Number(m[1]);
5283
+ const bodyStart = headEnd + 4;
5284
+ if (buffer.length < bodyStart + length) return null;
5285
+ return {
5286
+ body: buffer.subarray(bodyStart, bodyStart + length).toString("utf8"),
5287
+ rest: buffer.subarray(bodyStart + length)
5288
+ };
5289
+ }
5290
+ /** 帧解析器:push 数据块,吐出完整消息(可能拆包后重组、也可能一包多帧)。 */
5291
+ function createFrameParser() {
5292
+ let buffer = Buffer.alloc(0);
5293
+ return {
5294
+ push(chunk) {
5295
+ if (chunk.length) buffer = Buffer.concat([buffer, chunk]);
5296
+ const out = [];
5297
+ for (;;) {
5298
+ const parsed = parseFrame(buffer);
5299
+ if (!parsed) break;
5300
+ buffer = parsed.rest;
5301
+ try {
5302
+ out.push(JSON.parse(parsed.body));
5303
+ } catch (error) {
5304
+ out.push({
5305
+ jsonrpc: "2.0",
5306
+ error: {
5307
+ code: -32700,
5308
+ message: "Parse error: " + String(error)
5309
+ },
5310
+ id: null
5311
+ });
5312
+ }
5313
+ }
5314
+ return out;
5315
+ },
5316
+ clear() {
5317
+ buffer = Buffer.alloc(0);
5318
+ }
5319
+ };
5320
+ }
5321
+ /** 构造标准 JSON-RPC 请求消息。 */
5322
+ function requestMessage(id, method, params) {
5323
+ return {
5324
+ jsonrpc: "2.0",
5325
+ id,
5326
+ method,
5327
+ params
5328
+ };
5329
+ }
5330
+ /** 构造标准 JSON-RPC 通知消息(无 id,fire-and-forget)。 */
5331
+ function notifyMessage(method, params) {
5332
+ return {
5333
+ jsonrpc: "2.0",
5334
+ method,
5335
+ params
5336
+ };
5337
+ }
5338
+ //#endregion
5339
+ //#region src/lsp/transport.ts
5340
+ /**
5341
+ * dsh-vscode-mode host — LSP 语言服务器进程传输层。
5342
+ * 基于 node:child_process.spawn(stdio pipe 双向流)。刻意不用 DSH ctx.subprocess
5343
+ * 抽象:LSP 需要持续写 stdin + 流式读 stdout,而该抽象 stdin 不可写、输出为一次性收集。
5344
+ * 作者 ddj 2026-08-27
5345
+ */
5346
+ const STDERR_CAP = 262144;
5347
+ const running = /* @__PURE__ */ new Set();
5348
+ /**
5349
+ * 启动语言服务器进程。
5350
+ * @author ddj 2026年08月27号
5351
+ * @param spec 启动参数(argv 数组,不经 shell,Windows 引号安全)
5352
+ * @param onMessage stdout 消息回调
5353
+ * @param logger 诊断日志(可选)
5354
+ * @returns 传输句柄
5355
+ */
5356
+ function spawnServer(spec, onMessage, logger) {
5357
+ const { argv, cwd, env } = spec;
5358
+ const child = spawn(argv[0], argv.slice(1), {
5359
+ cwd,
5360
+ env: {
5361
+ ...process.env,
5362
+ ...env
5363
+ },
5364
+ stdio: [
5365
+ "pipe",
5366
+ "pipe",
5367
+ "pipe"
5368
+ ],
5369
+ windowsHide: true
3893
5370
  });
3894
- ctx.on("session/disposed", (session) => {
3895
- const cwd = cwdOf(session);
3896
- if (cwd) {
3897
- registry.delete(cwd);
3898
- dropFileIndex(cwd);
3899
- searcher.dispose(cwd);
3900
- contentSearcher.dispose(cwd);
5371
+ running.add(child);
5372
+ let stderrBuf = Buffer.alloc(0);
5373
+ let alive = true;
5374
+ const parser = createFrameParser();
5375
+ child.stderr.on("data", (chunk) => {
5376
+ if (stderrBuf.length < STDERR_CAP) stderrBuf = Buffer.concat([stderrBuf, chunk], Math.min(stderrBuf.length + chunk.length, STDERR_CAP));
5377
+ logger?.(chunk.toString("utf8"));
5378
+ });
5379
+ let exited = false;
5380
+ const emitExit = (code, signal) => {
5381
+ if (exited) return;
5382
+ exited = true;
5383
+ alive = false;
5384
+ running.delete(child);
5385
+ transport.onExit?.(code, signal);
5386
+ };
5387
+ child.on("error", (error) => {
5388
+ logger?.("spawn error: " + String(error));
5389
+ emitExit(null, null);
5390
+ });
5391
+ child.on("exit", (code, signal) => emitExit(code, signal));
5392
+ child.on("close", (code, signal) => emitExit(code, signal));
5393
+ const transport = {
5394
+ write(chunk) {
5395
+ if (!alive || !child.stdin.writable) return false;
5396
+ child.stdin.write(chunk);
5397
+ return true;
5398
+ },
5399
+ onMessage,
5400
+ stderr() {
5401
+ return stderrBuf.toString("utf8");
5402
+ },
5403
+ onExit: null,
5404
+ dispose() {
5405
+ if (!alive) return;
5406
+ child.kill();
5407
+ setTimeout(() => {
5408
+ if (!alive) return;
5409
+ alive = false;
5410
+ running.delete(child);
5411
+ try {
5412
+ if (process.platform === "win32") spawn("taskkill", [
5413
+ "/PID",
5414
+ String(child.pid),
5415
+ "/T",
5416
+ "/F"
5417
+ ], {
5418
+ windowsHide: true,
5419
+ stdio: "ignore"
5420
+ });
5421
+ else child.kill("SIGKILL");
5422
+ } catch (error) {
5423
+ logger?.("force kill error: " + String(error));
5424
+ }
5425
+ }, 1500).unref?.();
5426
+ },
5427
+ get alive() {
5428
+ return alive;
5429
+ },
5430
+ get pid() {
5431
+ return child.pid;
3901
5432
  }
5433
+ };
5434
+ child.stdout.on("data", (chunk) => {
5435
+ for (const message of parser.push(chunk)) transport.onMessage?.(message);
3902
5436
  });
3903
- registerRoutes(ctx, config, (method, args) => handleRpc(ctx, registry, method, args, searcher, contentSearcher), (warning) => warnings.push(warning));
3904
- installIsolation(ctx);
3905
- logCompatSummary(ctx, warnings);
3906
- ctx.logger?.info?.("[dsh-vscode-mode] 编辑差异审查已装配(/edrv/rpc 路由就绪,项目 MCP 隔离已启用)");
5437
+ return transport;
5438
+ }
5439
+ /** 强杀全部运行中的 LSP 子进程(插件卸载/退出用)。 */
5440
+ function disposeAllServers() {
5441
+ for (const child of running) try {
5442
+ if (process.platform === "win32" && child.pid) spawn("taskkill", [
5443
+ "/PID",
5444
+ String(child.pid),
5445
+ "/T",
5446
+ "/F"
5447
+ ], {
5448
+ windowsHide: true,
5449
+ stdio: "ignore"
5450
+ });
5451
+ else child.kill("SIGKILL");
5452
+ } catch (error) {}
5453
+ running.clear();
5454
+ }
5455
+ //#endregion
5456
+ //#region src/lsp/client.ts
5457
+ /**
5458
+ * dsh-vscode-mode host — LSP 客户端(JSON-RPC over stdio)。
5459
+ * 职责:请求/通知/握手/超时/服务器端请求处理/生命周期。transport 无关性:
5460
+ * 只依赖 write/onMessage 契约,便于单测时注入假传输。
5461
+ * 作者 ddj 2026-08-27
5462
+ */
5463
+ const DEFAULT_TIMEOUT_MS = 2e4;
5464
+ var LspError = class extends Error {
5465
+ code;
5466
+ constructor(code, message) {
5467
+ super(message);
5468
+ this.code = code;
5469
+ }
5470
+ };
5471
+ /**
5472
+ * 创建 LSP 客户端。
5473
+ * @author ddj 2026年08月27号
5474
+ * @param transport 传输句柄(write/onMessage/alive/dispose)
5475
+ * @param events 事件回调
5476
+ * @returns LSP 客户端
5477
+ */
5478
+ function createLspClient(transport, events = {}) {
5479
+ let nextId = 1;
5480
+ let alive = transport.alive;
5481
+ const pending = /* @__PURE__ */ new Map();
5482
+ let serverCapabilities = {
5483
+ definition: false,
5484
+ references: false,
5485
+ documentSymbol: false,
5486
+ workspaceSymbol: false,
5487
+ hover: false
5488
+ };
5489
+ const log = (line) => events.onLog?.(line);
5490
+ createFrameParser();
5491
+ transport.onMessage = (message) => {
5492
+ const msg = message;
5493
+ if (typeof msg.method === "string") {
5494
+ if (msg.method === "$/progress") events.onProgress?.(msg.params);
5495
+ handleServerRequest(msg);
5496
+ return;
5497
+ }
5498
+ if (typeof msg.id === "number" || typeof msg.id === "string") {
5499
+ const entry = pending.get(Number(msg.id));
5500
+ if (!entry) return;
5501
+ pending.delete(Number(msg.id));
5502
+ clearTimeout(entry.timer);
5503
+ if (msg.error) {
5504
+ const err = msg.error;
5505
+ entry.reject(new LspError(err.code ?? -32e3, err.message ?? "LSP error"));
5506
+ } else entry.resolve(msg.result);
5507
+ }
5508
+ };
5509
+ /** 服务器端请求:消息/日志类打日志,配置/能力类回 result:null,其余回 method not found。 */
5510
+ const handleServerRequest = async (msg) => {
5511
+ const { method, params, id } = msg;
5512
+ const respond = (payload) => {
5513
+ transport.write(encodeMessage(payload));
5514
+ };
5515
+ try {
5516
+ if (method === "window/logMessage" || method === "window/showMessage" || method === "window/showMessageRequest") {
5517
+ const p = params;
5518
+ log("[server " + (p?.type ?? "") + "] " + String(p?.message ?? ""));
5519
+ if (id !== void 0) respond({
5520
+ jsonrpc: "2.0",
5521
+ id,
5522
+ result: null
5523
+ });
5524
+ return;
5525
+ }
5526
+ if (method === "workspace/configuration") {
5527
+ const p = params;
5528
+ const items = Array.isArray(p?.items) ? p.items.map(() => null) : [];
5529
+ if (id !== void 0) respond({
5530
+ jsonrpc: "2.0",
5531
+ id,
5532
+ result: items
5533
+ });
5534
+ return;
5535
+ }
5536
+ if (method === "window/workDoneProgress/create" || method === "client/registerCapability" || method === "workspace/workspaceFolders") {
5537
+ if (id !== void 0) respond({
5538
+ jsonrpc: "2.0",
5539
+ id,
5540
+ result: null
5541
+ });
5542
+ return;
5543
+ }
5544
+ if (id !== void 0) respond({
5545
+ jsonrpc: "2.0",
5546
+ id,
5547
+ error: {
5548
+ code: -32601,
5549
+ message: "Method not found: " + method
5550
+ }
5551
+ });
5552
+ } catch (error) {
5553
+ log("server request handler error: " + String(error));
5554
+ if (id !== void 0) respond({
5555
+ jsonrpc: "2.0",
5556
+ id,
5557
+ error: {
5558
+ code: -32603,
5559
+ message: String(error)
5560
+ }
5561
+ });
5562
+ }
5563
+ };
5564
+ const transportExit = transport.onExit;
5565
+ transport.onExit = (code, signal) => {
5566
+ alive = false;
5567
+ for (const entry of pending.values()) {
5568
+ clearTimeout(entry.timer);
5569
+ entry.reject(/* @__PURE__ */ new Error("LSP 服务器已退出(" + String(code) + "/" + String(signal) + ")"));
5570
+ }
5571
+ pending.clear();
5572
+ events.onExit?.(code, signal);
5573
+ transportExit?.(code, signal);
5574
+ };
5575
+ const client = {
5576
+ async request(method, params, timeoutMs = DEFAULT_TIMEOUT_MS) {
5577
+ if (!alive) throw new Error("LSP 服务器未运行");
5578
+ const id = nextId++;
5579
+ return new Promise((resolve, reject) => {
5580
+ const timer = setTimeout(() => {
5581
+ pending.delete(id);
5582
+ reject(/* @__PURE__ */ new Error("LSP 请求超时:" + method + "(" + timeoutMs + "ms)"));
5583
+ }, timeoutMs);
5584
+ pending.set(id, {
5585
+ resolve,
5586
+ reject,
5587
+ timer
5588
+ });
5589
+ transport.write(encodeMessage(requestMessage(id, method, params)));
5590
+ });
5591
+ },
5592
+ notify(method, params) {
5593
+ if (!alive) return;
5594
+ transport.write(encodeMessage(notifyMessage(method, params)));
5595
+ },
5596
+ async initialize(rootUri, workspaceFolders) {
5597
+ const caps = (await client.request("initialize", {
5598
+ processId: process.pid,
5599
+ rootUri: rootUri || null,
5600
+ rootPath: null,
5601
+ workspaceFolders: workspaceFolders.length ? workspaceFolders.map((uri) => ({
5602
+ uri,
5603
+ name: uri
5604
+ })) : null,
5605
+ capabilities: {
5606
+ window: { workDoneProgress: true },
5607
+ workspace: { configuration: true },
5608
+ textDocument: {
5609
+ synchronization: {
5610
+ dynamicRegistration: false,
5611
+ willSave: false,
5612
+ didSave: true
5613
+ },
5614
+ definition: { dynamicRegistration: false },
5615
+ references: { dynamicRegistration: false },
5616
+ documentSymbol: { dynamicRegistration: false },
5617
+ hover: { dynamicRegistration: false }
5618
+ }
5619
+ }
5620
+ }))?.capabilities ?? {};
5621
+ serverCapabilities = {
5622
+ definition: Boolean(caps.definitionProvider),
5623
+ references: Boolean(caps.referencesProvider),
5624
+ documentSymbol: Boolean(caps.documentSymbolProvider),
5625
+ workspaceSymbol: Boolean(caps.workspaceSymbolProvider),
5626
+ hover: Boolean(caps.hoverProvider)
5627
+ };
5628
+ client.notify("initialized", {});
5629
+ events.onReady?.(serverCapabilities);
5630
+ return serverCapabilities;
5631
+ },
5632
+ async shutdown() {
5633
+ try {
5634
+ await client.request("shutdown", null, 3e3);
5635
+ } catch (error) {
5636
+ log("shutdown: " + String(error));
5637
+ }
5638
+ try {
5639
+ client.notify("exit", null);
5640
+ } catch (error) {
5641
+ log("exit: " + String(error));
5642
+ }
5643
+ },
5644
+ get alive() {
5645
+ return alive;
5646
+ }
5647
+ };
5648
+ return client;
5649
+ }
5650
+ //#endregion
5651
+ //#region src/lsp/uri.ts
5652
+ /**
5653
+ * dsh-vscode-mode host — 路径 ↔ file:// URI 转换(纯函数,node 可测)。
5654
+ * LSP 的 uri 字段必须是 file:// 绝对 URI;客户端 Monaco 用 edrv:///<encodeURI(path)>。
5655
+ * 作者 ddj 2026-08-27
5656
+ */
5657
+ /**
5658
+ * 绝对路径 → file:// URI(Windows 盘符保留:D:/x → file:///D:/x)。
5659
+ * @author ddj 2026年08月27号
5660
+ * @param absPath 绝对路径(正/反斜杠均可)
5661
+ * @returns file:// URI
5662
+ */
5663
+ function pathToFileUri(absPath) {
5664
+ const normalized = absPath.replace(/\\/g, "/");
5665
+ const drive = /^([A-Za-z]:)(?:\/(.*))?$/.exec(normalized);
5666
+ if (drive) {
5667
+ const tail = drive[2] ? "/" + drive[2].split("/").map(encodeURIComponent).join("/") : "";
5668
+ return "file:///" + drive[1] + tail;
5669
+ }
5670
+ return pathToFileURL(normalized).toString();
5671
+ }
5672
+ //#endregion
5673
+ //#region src/lsp/server.ts
5674
+ /**
5675
+ * dsh-vscode-mode host — 单语言服务器会话(绑定 工作区根 + 语言)。
5676
+ * 负责:启动传输 + LSP 客户端握手、文档同步(didOpen/didChange/didSave/didClose)、
5677
+ * 请求封装(definition/references/documentSymbol/workspaceSymbol/hover)、状态上报。
5678
+ * 作者 ddj 2026-08-27
5679
+ */
5680
+ /**
5681
+ * 创建语言服务器会话。
5682
+ * @author ddj 2026年08月27号
5683
+ * @param spec provider 规格(argv/cwd)
5684
+ * @param root 工作区根(绝对路径)
5685
+ * @param languageId 语言 id
5686
+ * @param logger 诊断日志
5687
+ * @returns 会话句柄
5688
+ */
5689
+ function createLspServer(spec, root, languageId, logger) {
5690
+ const rootUri = pathToFileUri(root);
5691
+ const docs = /* @__PURE__ */ new Map();
5692
+ let phase = "idle";
5693
+ let transport = null;
5694
+ let client = null;
5695
+ let capabilities = {
5696
+ definition: false,
5697
+ references: false,
5698
+ documentSymbol: false,
5699
+ workspaceSymbol: false,
5700
+ hover: false
5701
+ };
5702
+ let disposed = false;
5703
+ let readyWait = Promise.resolve(false);
5704
+ let resolveReady = null;
5705
+ let lastProgress = {};
5706
+ let lastStatus = {
5707
+ languageId,
5708
+ source: spec.kind,
5709
+ phase: "idle",
5710
+ reason: spec.ready ? void 0 : spec.reason,
5711
+ root
5712
+ };
5713
+ const log = (line) => logger?.("[" + languageId + "@" + root + "] " + line);
5714
+ const setPhase = (next, reason) => {
5715
+ phase = next;
5716
+ lastStatus = {
5717
+ languageId,
5718
+ source: spec.kind,
5719
+ phase: next,
5720
+ reason,
5721
+ root,
5722
+ ...progressFields()
5723
+ };
5724
+ server.onStateChange?.(lastStatus);
5725
+ };
5726
+ /** 返回当前进度字段,避免空进度污染状态载荷。 */
5727
+ const progressFields = () => ({
5728
+ progress: lastProgress.value,
5729
+ progressMessage: lastProgress.message
5730
+ });
5731
+ /** 处理 LSP $/progress 通知并映射为编辑器状态。 */
5732
+ const applyProgress = (params) => {
5733
+ const report = params?.value;
5734
+ if (report?.kind === "begin") {
5735
+ lastProgress = {
5736
+ value: 0,
5737
+ message: typeof report.message === "string" ? report.message : "正在解析工作区"
5738
+ };
5739
+ if (phase === "ready") phase = "indexing";
5740
+ } else if (report?.kind === "end") {
5741
+ lastProgress = {
5742
+ value: 100,
5743
+ message: typeof report.message === "string" ? report.message : "解析完成"
5744
+ };
5745
+ if (phase === "indexing") phase = "ready";
5746
+ } else if (typeof report?.percentage === "number") {
5747
+ lastProgress = {
5748
+ value: Math.max(0, Math.min(100, report.percentage)),
5749
+ message: typeof report.message === "string" ? report.message : lastProgress.message
5750
+ };
5751
+ if (phase === "ready") phase = "indexing";
5752
+ } else if (typeof report?.message === "string") lastProgress = {
5753
+ ...lastProgress,
5754
+ message: report.message
5755
+ };
5756
+ lastStatus = {
5757
+ ...lastStatus,
5758
+ phase,
5759
+ ...progressFields()
5760
+ };
5761
+ server.onStateChange?.(lastStatus);
5762
+ };
5763
+ const ensureClient = () => {
5764
+ if (!client) throw new Error("LSP 服务器未启动");
5765
+ return client;
5766
+ };
5767
+ const server = {
5768
+ languageId,
5769
+ root,
5770
+ get phase() {
5771
+ return phase;
5772
+ },
5773
+ get capabilities() {
5774
+ return capabilities;
5775
+ },
5776
+ status() {
5777
+ return lastStatus;
5778
+ },
5779
+ onStateChange: null,
5780
+ async start() {
5781
+ if (disposed) return false;
5782
+ if (phase === "starting") return readyWait;
5783
+ if (phase === "ready" || phase === "indexing") return true;
5784
+ readyWait = new Promise((resolve) => {
5785
+ resolveReady = resolve;
5786
+ });
5787
+ lastProgress = {};
5788
+ setPhase("starting");
5789
+ try {
5790
+ transport = spawnServer({
5791
+ argv: spec.argv,
5792
+ cwd: spec.cwd ?? root
5793
+ }, (message) => {}, (line) => log("stderr: " + line));
5794
+ client = createLspClient(transport, {
5795
+ onReady: (caps) => {
5796
+ capabilities = caps;
5797
+ for (const doc of docs.values()) client?.notify("textDocument/didOpen", { textDocument: {
5798
+ uri: doc.uri,
5799
+ languageId,
5800
+ version: doc.version,
5801
+ text: doc.text
5802
+ } });
5803
+ setPhase("ready");
5804
+ resolveReady?.(true);
5805
+ resolveReady = null;
5806
+ log("ready: definition=" + caps.definition + " references=" + caps.references + " symbols=" + caps.documentSymbol);
5807
+ },
5808
+ onProgress: (params) => applyProgress(params),
5809
+ onExit: (code, signal) => {
5810
+ if (disposed) return;
5811
+ resolveReady?.(false);
5812
+ resolveReady = null;
5813
+ log("server exited (" + String(code) + "/" + String(signal) + ")");
5814
+ client = null;
5815
+ transport = null;
5816
+ setPhase("idle", "服务器已退出(" + String(code) + "/" + String(signal) + "),等待重启");
5817
+ },
5818
+ onLog: (line) => log("lsp: " + line)
5819
+ });
5820
+ capabilities = await client.initialize(rootUri, [rootUri]);
5821
+ return true;
5822
+ } catch (error) {
5823
+ log("start failed: " + String(error));
5824
+ setPhase("unavailable", "启动失败:" + String(error));
5825
+ resolveReady?.(false);
5826
+ resolveReady = null;
5827
+ try {
5828
+ await server.dispose();
5829
+ } catch {}
5830
+ return false;
5831
+ }
5832
+ },
5833
+ sync(path, text, version) {
5834
+ const existing = docs.get(path);
5835
+ const uri = existing?.uri ?? pathToFileUri(root + "/" + path.split("/").join("/"));
5836
+ if (existing) {
5837
+ existing.version = version;
5838
+ existing.text = text;
5839
+ if (phase === "ready" || phase === "indexing") client?.notify("textDocument/didChange", {
5840
+ textDocument: {
5841
+ uri,
5842
+ version
5843
+ },
5844
+ contentChanges: [{ text }]
5845
+ });
5846
+ return;
5847
+ }
5848
+ docs.set(path, {
5849
+ path,
5850
+ uri,
5851
+ version,
5852
+ text
5853
+ });
5854
+ if (phase === "ready" || phase === "indexing") client?.notify("textDocument/didOpen", { textDocument: {
5855
+ uri,
5856
+ languageId,
5857
+ version,
5858
+ text
5859
+ } });
5860
+ },
5861
+ close(path) {
5862
+ const doc = docs.get(path);
5863
+ if (!doc) return;
5864
+ docs.delete(path);
5865
+ if (phase === "ready" || phase === "indexing") client?.notify("textDocument/didClose", { textDocument: { uri: doc.uri } });
5866
+ },
5867
+ async definition(path, line, character) {
5868
+ const doc = docs.get(path);
5869
+ if (!doc || !await readyWait) return [];
5870
+ return normalizeLocations(await ensureClient().request("textDocument/definition", {
5871
+ textDocument: { uri: doc.uri },
5872
+ position: {
5873
+ line,
5874
+ character
5875
+ }
5876
+ }));
5877
+ },
5878
+ async references(path, line, character, includeDeclaration) {
5879
+ const doc = docs.get(path);
5880
+ if (!doc || !await readyWait) return [];
5881
+ return normalizeLocations(await ensureClient().request("textDocument/references", {
5882
+ textDocument: { uri: doc.uri },
5883
+ position: {
5884
+ line,
5885
+ character
5886
+ },
5887
+ context: { includeDeclaration }
5888
+ }));
5889
+ },
5890
+ async documentSymbol(path) {
5891
+ const doc = docs.get(path);
5892
+ if (!doc || !await readyWait) return [];
5893
+ const result = await ensureClient().request("textDocument/documentSymbol", { textDocument: { uri: doc.uri } });
5894
+ if (!Array.isArray(result)) return [];
5895
+ return result.map(normalizeSymbol).filter((s) => s !== null);
5896
+ },
5897
+ async workspaceSymbol(query) {
5898
+ if (!await readyWait) return [];
5899
+ const result = await ensureClient().request("workspace/symbol", { query });
5900
+ if (!Array.isArray(result)) return [];
5901
+ return result.map(normalizeSymbol).filter((s) => s !== null);
5902
+ },
5903
+ async hover(path, line, character) {
5904
+ const doc = docs.get(path);
5905
+ if (!doc || !await readyWait) return null;
5906
+ const result = await ensureClient().request("textDocument/hover", {
5907
+ textDocument: { uri: doc.uri },
5908
+ position: {
5909
+ line,
5910
+ character
5911
+ }
5912
+ });
5913
+ if (!result || !result.contents) return null;
5914
+ return { contents: stringifyHoverContents(result.contents) };
5915
+ },
5916
+ async dispose() {
5917
+ disposed = true;
5918
+ resolveReady?.(false);
5919
+ resolveReady = null;
5920
+ if (client) {
5921
+ await client.shutdown().catch(() => {});
5922
+ client = null;
5923
+ }
5924
+ if (transport) {
5925
+ transport.dispose();
5926
+ transport = null;
5927
+ }
5928
+ setPhase("stopped");
5929
+ }
5930
+ };
5931
+ return server;
5932
+ }
5933
+ /** 归一化 definition/references 结果(Location | Location[] | LocationLink[])。 */
5934
+ function normalizeLocations(result) {
5935
+ if (!Array.isArray(result)) return [];
5936
+ const out = [];
5937
+ for (const item of result) {
5938
+ if (!item || typeof item !== "object") continue;
5939
+ const obj = item;
5940
+ const uri = typeof obj.targetUri === "string" ? obj.targetUri : typeof obj.uri === "string" ? obj.uri : "";
5941
+ const range = obj.targetRange ?? obj.range;
5942
+ if (!uri || !range || !range.start || !range.end) continue;
5943
+ out.push({
5944
+ uri,
5945
+ range: {
5946
+ start: {
5947
+ line: toInt(range.start.line),
5948
+ character: toInt(range.start.character)
5949
+ },
5950
+ end: {
5951
+ line: toInt(range.end.line),
5952
+ character: toInt(range.end.character)
5953
+ }
5954
+ }
5955
+ });
5956
+ }
5957
+ return out;
5958
+ }
5959
+ /** 归一化 documentSymbol/workspaceSymbol 结果(DocumentSymbol | SymbolInformation)。 */
5960
+ function normalizeSymbol(item) {
5961
+ if (!item || typeof item !== "object") return null;
5962
+ const obj = item;
5963
+ if (typeof obj.name !== "string") return null;
5964
+ const selection = obj.selectionRange ?? (obj.location && obj.location.range);
5965
+ const range = obj.range ?? selection;
5966
+ if (!range || typeof range !== "object") return null;
5967
+ const r = range;
5968
+ if (!r.start || !r.end) return null;
5969
+ const symbol = {
5970
+ name: obj.name,
5971
+ kind: toInt(obj.kind),
5972
+ range: {
5973
+ start: {
5974
+ line: toInt(r.start.line),
5975
+ character: toInt(r.start.character)
5976
+ },
5977
+ end: {
5978
+ line: toInt(r.end.line),
5979
+ character: toInt(r.end.character)
5980
+ }
5981
+ },
5982
+ selectionRange: selection ? {
5983
+ start: {
5984
+ line: toInt(selection.start?.line ?? toInt(r.start.line)),
5985
+ character: toInt(selection.start?.character ?? toInt(r.start.character))
5986
+ },
5987
+ end: {
5988
+ line: toInt(selection.end?.line ?? toInt(r.end.line)),
5989
+ character: toInt(selection.end?.character ?? toInt(r.end.character))
5990
+ }
5991
+ } : {
5992
+ start: {
5993
+ line: toInt(r.start.line),
5994
+ character: toInt(r.start.character)
5995
+ },
5996
+ end: {
5997
+ line: toInt(r.end.line),
5998
+ character: toInt(r.end.character)
5999
+ }
6000
+ }
6001
+ };
6002
+ if (typeof obj.detail === "string" && obj.detail) symbol.detail = obj.detail;
6003
+ if (typeof obj.containerName === "string" && obj.containerName) symbol.containerName = obj.containerName;
6004
+ if (Array.isArray(obj.children)) {
6005
+ const kids = obj.children.map(normalizeSymbol).filter((s) => s !== null);
6006
+ if (kids.length) symbol.children = kids;
6007
+ }
6008
+ return symbol;
6009
+ }
6010
+ /** hover contents → 文本行(MarkupContent | MarkedString | MarkedString[])。 */
6011
+ function stringifyHoverContents(contents) {
6012
+ if (typeof contents === "string") return [contents];
6013
+ if (Array.isArray(contents)) return contents.map(stringifyHoverContents).flat().filter(Boolean);
6014
+ if (contents && typeof contents === "object") {
6015
+ const value = contents.value;
6016
+ if (typeof value === "string") return [value];
6017
+ }
6018
+ return [];
6019
+ }
6020
+ function toInt(value) {
6021
+ const n = Number(value);
6022
+ return Number.isFinite(n) ? Math.max(0, Math.floor(n)) : 0;
6023
+ }
6024
+ //#endregion
6025
+ //#region src/lsp/manager.ts
6026
+ /**
6027
+ * dsh-vscode-mode host — LSP 服务器管理器。
6028
+ * 实例注册表(key=root|languageId,refcount 共享),崩溃退避重启,session/disposed 清理。
6029
+ * 作者 ddj 2026-08-27
6030
+ */
6031
+ /**
6032
+ * 创建 LSP 管理器。
6033
+ * @author ddj 2026年08月27号
6034
+ * @param logger 诊断日志
6035
+ * @returns 管理器
6036
+ */
6037
+ function createLspManager(logger) {
6038
+ const entries = /* @__PURE__ */ new Map();
6039
+ const keyOf = (root, languageId) => root + "|" + languageId;
6040
+ const log = (line) => logger?.("[lsp-manager] " + line);
6041
+ /** 同步摘除注册表项并后台回收(refs 归零/工作区清理用;statusAll 立即可见归零)。 */
6042
+ const dropEntry = (entry) => {
6043
+ if (entry.restartTimer) {
6044
+ clearTimeout(entry.restartTimer);
6045
+ entry.restartTimer = null;
6046
+ }
6047
+ entries.delete(entry.key);
6048
+ entry.server.dispose().catch(() => {});
6049
+ };
6050
+ const manager = {
6051
+ acquire(root, languageId, resolveSpec) {
6052
+ const key = keyOf(root, languageId);
6053
+ let entry = entries.get(key);
6054
+ if (!entry) {
6055
+ const spec = resolveSpec(languageId);
6056
+ const server = createLspServer(spec, root, languageId, log);
6057
+ server.onStateChange = (status) => manager.onStatusChange?.(status);
6058
+ entry = {
6059
+ key,
6060
+ server,
6061
+ refs: 0,
6062
+ failures: 0,
6063
+ restartTimer: null
6064
+ };
6065
+ entries.set(key, entry);
6066
+ if (spec.ready) server.start().then((ok) => {
6067
+ if (!ok && entries.has(key)) entry.failures++;
6068
+ });
6069
+ }
6070
+ entry.refs++;
6071
+ return entry.server;
6072
+ },
6073
+ release(root, languageId) {
6074
+ const entry = entries.get(keyOf(root, languageId));
6075
+ if (!entry) return;
6076
+ entry.refs = Math.max(0, entry.refs - 1);
6077
+ if (entry.refs === 0) dropEntry(entry);
6078
+ },
6079
+ releaseRoot(root) {
6080
+ for (const entry of [...entries.values()]) if (entry.server.root === root) dropEntry(entry);
6081
+ },
6082
+ statusAll() {
6083
+ return [...entries.values()].map((entry) => entry.server.status());
6084
+ },
6085
+ peek(root, languageId) {
6086
+ return entries.get(keyOf(root, languageId))?.server;
6087
+ },
6088
+ async disposeAll() {
6089
+ const list = [...entries.values()];
6090
+ entries.clear();
6091
+ for (const entry of list) {
6092
+ if (entry.restartTimer) clearTimeout(entry.restartTimer);
6093
+ await entry.server.dispose();
6094
+ }
6095
+ },
6096
+ onStatusChange: null
6097
+ };
6098
+ return manager;
6099
+ }
6100
+ //#endregion
6101
+ //#region src/lsp/zip.ts
6102
+ /**
6103
+ * dsh-vscode-mode host — 纯 Node ZIP 解压(零依赖,跨平台)。
6104
+ * 供 extmgr 解包 VSIX 使用;只实现 VSIX 需要的子集:
6105
+ * 读取 EOCD + 中央目录,支持 stored(0)/deflate(8),忽略 zip64(VSIX < 4GB)。
6106
+ * 作者 ddj 2026-08-27
6107
+ */
6108
+ const EOCD_SIG = 101010256;
6109
+ const CDIR_SIG = 33639248;
6110
+ const LHDR_SIG = 67324752;
6111
+ /**
6112
+ * 解析 ZIP 缓冲区 → 条目列表(不解压内容,仅元数据)。
6113
+ * @author ddj 2026年08月27号
6114
+ * @param buf zip 字节
6115
+ * @returns 条目元数据
6116
+ */
6117
+ function zipEntries(buf) {
6118
+ const eocd = findEocd(buf);
6119
+ if (eocd < 0) throw new Error("不是有效的 ZIP 文件(找不到 EOCD)");
6120
+ const total = buf.readUInt16LE(eocd + 10);
6121
+ let offset = buf.readUInt32LE(eocd + 16);
6122
+ const out = [];
6123
+ for (let i = 0; i < total; i++) {
6124
+ if (buf.readUInt32LE(offset) !== CDIR_SIG) throw new Error("ZIP 中央目录损坏");
6125
+ const method = buf.readUInt16LE(offset + 10);
6126
+ const compressed = buf.readUInt32LE(offset + 20);
6127
+ const uncompressed = buf.readUInt32LE(offset + 24);
6128
+ const nameLen = buf.readUInt16LE(offset + 28);
6129
+ const extraLen = buf.readUInt16LE(offset + 30);
6130
+ const commentLen = buf.readUInt16LE(offset + 32);
6131
+ const localOffset = buf.readUInt32LE(offset + 42);
6132
+ const name = buf.subarray(offset + 46, offset + 46 + nameLen).toString("utf8");
6133
+ out.push({
6134
+ name,
6135
+ method,
6136
+ compressed,
6137
+ uncompressed,
6138
+ localOffset
6139
+ });
6140
+ offset += 46 + nameLen + extraLen + commentLen;
6141
+ }
6142
+ return out;
6143
+ }
6144
+ /**
6145
+ * 解压 ZIP → 条目列表(目录条目含尾部 /)。
6146
+ * @author ddj 2026年08月27号
6147
+ * @param buf zip 字节
6148
+ * @returns 条目(含内容)
6149
+ */
6150
+ function unzip(buf) {
6151
+ const metas = zipEntries(buf);
6152
+ const out = [];
6153
+ for (const meta of metas) {
6154
+ const lh = meta.localOffset;
6155
+ if (buf.readUInt32LE(lh) !== LHDR_SIG) throw new Error("ZIP 本地头损坏:" + meta.name);
6156
+ const nameLen = buf.readUInt16LE(lh + 26);
6157
+ const extraLen = buf.readUInt16LE(lh + 28);
6158
+ const dataStart = lh + 30 + nameLen + extraLen;
6159
+ const isDir = /\/$/.test(meta.name);
6160
+ let data = Buffer.alloc(0);
6161
+ if (!isDir && meta.compressed > 0) {
6162
+ const raw = buf.subarray(dataStart, dataStart + meta.compressed);
6163
+ data = meta.method === 8 ? Buffer.from(inflateRawSync(raw)) : meta.method === 0 ? Buffer.from(raw) : Buffer.alloc(0);
6164
+ }
6165
+ out.push({
6166
+ path: meta.name,
6167
+ data,
6168
+ isDirectory: isDir
6169
+ });
6170
+ }
6171
+ return out;
6172
+ }
6173
+ /** 定位 EOCD(从尾部向前扫,容忍尾部注释)。 */
6174
+ function findEocd(buf) {
6175
+ const maxScan = Math.min(buf.length, 65557);
6176
+ for (let i = buf.length - 22; i >= buf.length - maxScan; i--) {
6177
+ if (i < 0) break;
6178
+ if (buf.readUInt32LE(i) === EOCD_SIG) return i;
6179
+ }
6180
+ return -1;
6181
+ }
6182
+ //#endregion
6183
+ //#region src/lsp/extmgr.ts
6184
+ /**
6185
+ * dsh-vscode-mode host — VSIX 扩展管理器(extmgr)。
6186
+ * 管理 DSH 专属扩展目录(~/.dsh/dsh-vscode-mode/extensions/),支持:
6187
+ * 本地 .vsix 安装、Open VSX 市场搜索/下载安装、卸载、更新检测与安装。
6188
+ * 已装扩展作为 LSP provider 的"扩展源"(kind=extension),见 providers.ts。
6189
+ * 作者 ddj 2026-08-27
6190
+ */
6191
+ const OPEN_VSX = "https://open-vsx.org";
6192
+ const META_FILE = "package.json";
6193
+ /** 当前平台(与 VSCode platform 命名对齐:win32-x64/darwin-arm64/linux-x64)。 */
6194
+ const PLATFORM = (() => {
6195
+ if (process.platform === "win32") return process.arch === "arm64" ? "win32-arm64" : "win32-x64";
6196
+ if (process.platform === "darwin") return process.arch === "arm64" ? "darwin-arm64" : "darwin-x64";
6197
+ return process.arch === "arm64" ? "linux-arm64" : "linux-x64";
6198
+ })();
6199
+ /** DSH 专属扩展目录。 */
6200
+ function extensionsRoot(home = dshHome()) {
6201
+ return join(home, PLUGIN_ID, "extensions");
6202
+ }
6203
+ /** 从 VSIX 字节里读 extension/package.json 清单。 */
6204
+ function vsixManifest(vsix) {
6205
+ for (const entry of unzip(vsix)) if (entry.path === "extension/package.json" || entry.path === "extension\\package.json") return JSON.parse(entry.data.toString("utf8"));
6206
+ throw new Error("VSIX 缺少 extension/package.json");
6207
+ }
6208
+ /**
6209
+ * 解包 VSIX 到目录(仅取 extension/ 子树,剥前缀;返回清单)。
6210
+ * @author ddj 2026年08月27号
6211
+ * @param vsix vsix 字节
6212
+ * @param dest 目标目录(已存在则先清空)
6213
+ * @returns 清单对象
6214
+ */
6215
+ function unpackVsix(vsix, dest) {
6216
+ rmSync(dest, {
6217
+ recursive: true,
6218
+ force: true
6219
+ });
6220
+ mkdirSync(dest, { recursive: true });
6221
+ let manifest = null;
6222
+ for (const entry of unzip(vsix)) {
6223
+ const rel = entry.path.replace(/^extension[\\/]/, "");
6224
+ if (!rel || entry.path.startsWith("..")) continue;
6225
+ const safe = rel.split(/[\\/]/).filter((part) => part && part !== ".." && part !== ".").join("/");
6226
+ if (!safe) continue;
6227
+ const target = join(dest, ...safe.split("/"));
6228
+ if (entry.isDirectory) {
6229
+ mkdirSync(target, { recursive: true });
6230
+ continue;
6231
+ }
6232
+ mkdirSync(dirname(target), { recursive: true });
6233
+ writeFileSync(target, entry.data);
6234
+ if (rel === "package.json") manifest = JSON.parse(entry.data.toString("utf8"));
6235
+ }
6236
+ if (!manifest) throw new Error("VSIX 解包后缺少 package.json");
6237
+ return manifest;
6238
+ }
6239
+ /** 扩展信息(清单 + 目录)。 */
6240
+ function extInfoOf(manifest, dir) {
6241
+ const namespace = String(manifest.publisher ?? "unknown");
6242
+ const name = String(manifest.name ?? "unknown");
6243
+ const version = String(manifest.version ?? "0.0.0");
6244
+ return {
6245
+ id: namespace + "." + name,
6246
+ namespace,
6247
+ name,
6248
+ version,
6249
+ displayName: typeof manifest.displayName === "string" ? manifest.displayName : void 0,
6250
+ description: typeof manifest.description === "string" ? manifest.description : void 0,
6251
+ dirName: basename(dir),
6252
+ dir
6253
+ };
6254
+ }
6255
+ /** 安装本地 .vsix 文件(已存在同 id 则升级替换)。 */
6256
+ async function installVsixFile(vsixPath, home = dshHome()) {
6257
+ return installVsixBuffer(readFileSync(vsixPath), home);
6258
+ }
6259
+ /** 安装 VSIX 字节(解包进 extensionsRoot;同名旧版本先清)。 */
6260
+ function installVsixBuffer(vsix, home = dshHome()) {
6261
+ const info = extInfoOf(vsixManifest(vsix), "");
6262
+ const target = join(extensionsRoot(home), info.id + "-" + info.version);
6263
+ return extInfoOf(unpackVsix(vsix, target), target);
6264
+ }
6265
+ /** 扫描已装扩展目录。 */
6266
+ function listInstalled(home = dshHome()) {
6267
+ const root = extensionsRoot(home);
6268
+ if (!existsSync(root)) return [];
6269
+ const out = [];
6270
+ for (const dir of readdirSync(root)) {
6271
+ const full = join(root, dir);
6272
+ const meta = join(full, META_FILE);
6273
+ if (!existsSync(meta)) continue;
6274
+ try {
6275
+ const manifest = JSON.parse(readFileSync(meta, "utf8"));
6276
+ out.push(extInfoOf(manifest, full));
6277
+ } catch (error) {}
6278
+ }
6279
+ return out;
6280
+ }
6281
+ /** 卸载扩展(按 id)。 */
6282
+ function uninstall(id, home = dshHome()) {
6283
+ const target = listInstalled(home).find((e) => e.id === id)?.dir;
6284
+ if (!target) return false;
6285
+ rmSync(target, {
6286
+ recursive: true,
6287
+ force: true
6288
+ });
6289
+ return true;
6290
+ }
6291
+ /** Open VSX 单扩展元数据(latest + 下载 URL)。 */
6292
+ async function marketGet(namespace, name, version) {
6293
+ const path = version ? namespace + "/" + name + "/" + version : namespace + "/" + name;
6294
+ const res = await fetch(OPEN_VSX + "/api/" + path, { headers: { accept: "application/json" } });
6295
+ if (!res.ok) return null;
6296
+ return await res.json();
6297
+ }
6298
+ /** Open VSX 搜索。 */
6299
+ async function marketSearch(query, size = 12) {
6300
+ const url = OPEN_VSX + "/api/-/search?query=" + encodeURIComponent(query) + "&size=" + size;
6301
+ const res = await fetch(url, { headers: { accept: "application/json" } });
6302
+ if (!res.ok) throw new Error("市场搜索失败:HTTP " + res.status);
6303
+ return ((await res.json()).extensions ?? []).map((item) => ({
6304
+ namespace: String(item.namespace ?? ""),
6305
+ name: String(item.name ?? ""),
6306
+ id: String(item.namespace ?? "") + "." + String(item.name ?? ""),
6307
+ version: String(item.version ?? ""),
6308
+ displayName: typeof item.displayName === "string" ? item.displayName : void 0,
6309
+ description: typeof item.description === "string" ? item.description : void 0,
6310
+ download: typeof item.files?.download === "string" ? item.files.download : void 0
6311
+ }));
6312
+ }
6313
+ /** 平台匹配的 VSIX 下载 URL(探测平台变体,失败回退 API 默认)。 */
6314
+ async function resolveDownload(namespace, name, version) {
6315
+ const candidate = OPEN_VSX + "/api/" + namespace + "/" + name + "/" + PLATFORM + "/" + version + "/file/" + namespace + "." + name + "-" + version + "@" + PLATFORM + ".vsix";
6316
+ try {
6317
+ if ((await fetch(candidate, { method: "HEAD" })).ok) return candidate;
6318
+ } catch (error) {}
6319
+ const download = ((await marketGet(namespace, name, version))?.files)?.download;
6320
+ if (typeof download === "string") return download;
6321
+ throw new Error("市场未提供 " + namespace + "." + name + "@" + version + " 的下载地址");
6322
+ }
6323
+ /** 从 Open VSX 安装/更新扩展。 */
6324
+ async function installFromMarket(namespace, name, version) {
6325
+ const meta = await marketGet(namespace, name, version);
6326
+ if (!meta) throw new Error("市场找不到 " + namespace + "." + name + (version ? "@" + version : ""));
6327
+ const url = await resolveDownload(namespace, name, version ?? String(meta.version ?? ""));
6328
+ const res = await fetch(url);
6329
+ if (!res.ok) throw new Error("下载失败:HTTP " + res.status);
6330
+ return installVsixBuffer(Buffer.from(await res.arrayBuffer()));
6331
+ }
6332
+ /** 更新已装扩展(对比市场最新版;返回 null 表示已最新)。 */
6333
+ async function updateExtension(id) {
6334
+ const installed = listInstalled().find((e) => e.id === id);
6335
+ if (!installed) throw new Error("扩展未安装:" + id);
6336
+ const meta = await marketGet(installed.namespace, installed.name);
6337
+ const latest = String(meta?.version ?? "");
6338
+ if (!latest || latest === installed.version) return null;
6339
+ return installFromMarket(installed.namespace, installed.name, latest);
6340
+ }
6341
+ //#endregion
6342
+ //#region src/lsp/providers.ts
6343
+ /**
6344
+ * dsh-vscode-mode host — 语言服务器 provider 解析。
6345
+ * 优先级:手动配置 > extmgr 扩展源(Phase 2 挂载) > 自动发现(~/.vscode/extensions、
6346
+ * ~/.dsh/dsh-vscode-mode/extensions、PATH)。产物 LspProviderSpec:{ ready, argv, cwd, reason },
6347
+ * 供 manager 启动服务器。
6348
+ * 作者 ddj 2026-08-27
6349
+ */
6350
+ /** 平台目录名(LuaLS/OmniSharp 的 server/bin/<平台>)。 */
6351
+ function platformServerDir(platform = process.platform) {
6352
+ if (platform === "win32") return "Windows";
6353
+ if (platform === "darwin") return "macOS";
6354
+ return "Linux";
6355
+ }
6356
+ /** exe 后缀(Windows 为 .exe)。 */
6357
+ function exeSuffix(platform = process.platform) {
6358
+ return platform === "win32" ? ".exe" : "";
6359
+ }
6360
+ /** 列出目录下匹配前缀的子目录(如 ~/.vscode/extensions/sumneko.lua-*)。 */
6361
+ function listMatchingDirs(dir, prefix) {
6362
+ if (!existsSync(dir)) return [];
6363
+ let entries;
6364
+ try {
6365
+ entries = readdirSync(dir, { withFileTypes: true });
6366
+ } catch (error) {
6367
+ return [];
6368
+ }
6369
+ return entries.filter((e) => e.isDirectory() && e.name.startsWith(prefix)).map((e) => join(dir, e.name));
6370
+ }
6371
+ /** VSCode 扩展目录(~/.vscode/extensions、~/.vscode-server/extensions + extmgr 目录;无则空数组)。 */
6372
+ function vscodeExtensionsDirs(home = dshHome()) {
6373
+ const userHome = homedir();
6374
+ return [
6375
+ join(userHome, ".vscode", "extensions"),
6376
+ join(userHome, ".vscode-server", "extensions"),
6377
+ extensionsRoot(home)
6378
+ ].filter((dir) => existsSync(dir));
6379
+ }
6380
+ /** 扩展源 provider 注册表:languageId → 启动参数。 */
6381
+ const extProviders = /* @__PURE__ */ new Map();
6382
+ /** 取扩展源 provider spec(未注册返回 null)。 */
6383
+ function extensionProviderFor(languageId) {
6384
+ const p = extProviders.get(languageId);
6385
+ if (!p || !p.argv.length) return null;
6386
+ return {
6387
+ languageId,
6388
+ kind: "extension",
6389
+ argv: p.argv,
6390
+ cwd: p.cwd,
6391
+ ready: true
6392
+ };
6393
+ }
6394
+ /** PATH 中查找可执行文件(不含扩展名校验,交由 spawn 报错)。 */
6395
+ function findInPath(name) {
6396
+ const pathEnv = process.env.PATH ?? "";
6397
+ const exts = process.platform === "win32" ? [
6398
+ "",
6399
+ ".exe",
6400
+ ".cmd",
6401
+ ".bat"
6402
+ ] : [""];
6403
+ for (const dir of pathEnv.split(sep === "\\" ? ";" : ":")) {
6404
+ if (!dir) continue;
6405
+ for (const ext of exts) {
6406
+ const candidate = join(dir, name + ext);
6407
+ if (existsSync(candidate)) return candidate;
6408
+ }
6409
+ }
6410
+ return null;
6411
+ }
6412
+ /** 查找 dotnet(PATH + 常见安装路径)。 */
6413
+ function findDotnet() {
6414
+ const inPath = findInPath("dotnet");
6415
+ if (inPath) return inPath;
6416
+ for (const c of ["C:/Program Files/dotnet/dotnet.exe", "C:/Program Files (x86)/dotnet/dotnet.exe"]) if (existsSync(c)) return c;
6417
+ return null;
6418
+ }
6419
+ /** LuaLS 自动发现候选(VSCode 扩展目录 + extmgr 目录下 sumneko.lua-* 的 server/bin 可执行)。 */
6420
+ function candidateLuaServers(home = dshHome()) {
6421
+ const plat = platformServerDir();
6422
+ const suffix = exeSuffix();
6423
+ const out = [];
6424
+ for (const dir of vscodeExtensionsDirs(home)) for (const extDir of listMatchingDirs(dir, "sumneko.lua-")) {
6425
+ const binDir = join(extDir, "server", "bin");
6426
+ const binName = "lua-language-server" + suffix;
6427
+ const bin = existsSync(join(binDir, binName)) ? join(binDir, binName) : join(binDir, plat, binName);
6428
+ if (existsSync(bin) && !out.includes(bin)) out.push(bin);
6429
+ }
6430
+ return out;
6431
+ }
6432
+ /** C# 自动发现候选:Roslyn dll(需 dotnet)或 OmniSharp 可执行。 */
6433
+ function candidateCSharpServers(home = dshHome()) {
6434
+ const out = {};
6435
+ for (const dir of vscodeExtensionsDirs(home)) for (const extDir of listMatchingDirs(dir, "ms-dotnettools.csharp-")) {
6436
+ const roslyn = join(extDir, ".roslyn", "Microsoft.CodeAnalysis.LanguageServer.dll");
6437
+ if (!out.roslynDll && existsSync(roslyn)) out.roslynDll = roslyn;
6438
+ const omni = join(extDir, ".omnisharp", "OmniSharp" + exeSuffix());
6439
+ if (!out.omnisharpExe && existsSync(omni)) out.omnisharpExe = omni;
6440
+ }
6441
+ const dotnet = findDotnet();
6442
+ if (dotnet) out.dotnet = dotnet;
6443
+ return out;
6444
+ }
6445
+ /**
6446
+ * 手动配置归一化:path 指向可执行文件/目录,command 为可执行名(PATH 解析)。
6447
+ * 未配置时返回空 reason(由各 resolve 兜底输出语言相关安装指引文案)。
6448
+ * @author ddj 2026年08月27号
6449
+ */
6450
+ function manualSpec(manual) {
6451
+ if (!manual) return {
6452
+ argv: [],
6453
+ ready: false
6454
+ };
6455
+ const pathValue = typeof manual.path === "string" && manual.path.trim() ? manual.path.trim() : "";
6456
+ const commandValue = typeof manual.command === "string" && manual.command.trim() ? manual.command.trim() : "";
6457
+ if (!pathValue && !commandValue) return {
6458
+ argv: [],
6459
+ ready: false
6460
+ };
6461
+ if (pathValue) {
6462
+ if (!existsSync(pathValue)) return {
6463
+ argv: [],
6464
+ ready: false,
6465
+ reason: "路径不存在:" + pathValue
6466
+ };
6467
+ return {
6468
+ argv: [pathValue],
6469
+ ready: true
6470
+ };
6471
+ }
6472
+ const found = findInPath(commandValue);
6473
+ if (!found) return {
6474
+ argv: [],
6475
+ ready: false,
6476
+ reason: "PATH 中找不到命令:" + commandValue
6477
+ };
6478
+ return {
6479
+ argv: [found],
6480
+ ready: true
6481
+ };
6482
+ }
6483
+ /**
6484
+ * 解析 Lua provider(优先级:手动 > 扩展源 > 自动发现)。
6485
+ * @author ddj 2026年08月27号
6486
+ * @param manual 手动配置
6487
+ * @param home DSH home(缺省真实;测试可注入临时目录)
6488
+ * @returns provider 规格
6489
+ */
6490
+ function resolveLuaProvider(manual, home = dshHome()) {
6491
+ const base = { languageId: "lua" };
6492
+ const manualResolved = manualSpec(manual);
6493
+ if (manualResolved.ready) return {
6494
+ ...base,
6495
+ kind: "manual",
6496
+ argv: manualResolved.argv,
6497
+ cwd: manualResolved.cwd,
6498
+ ready: true
6499
+ };
6500
+ const ext = extensionProviderFor("lua");
6501
+ if (ext) return ext;
6502
+ const candidates = candidateLuaServers(home);
6503
+ if (candidates.length) return {
6504
+ ...base,
6505
+ kind: "discover",
6506
+ argv: [candidates[0]],
6507
+ cwd: void 0,
6508
+ ready: true
6509
+ };
6510
+ const inPath = findInPath("lua-language-server" + exeSuffix());
6511
+ if (inPath) return {
6512
+ ...base,
6513
+ kind: "discover",
6514
+ argv: [inPath],
6515
+ ready: true
6516
+ };
6517
+ return {
6518
+ ...base,
6519
+ kind: "none",
6520
+ argv: [],
6521
+ ready: false,
6522
+ reason: manualResolved.reason ?? "未发现 lua-language-server(可安装 LuaLS 或手动指定路径)"
6523
+ };
6524
+ }
6525
+ /**
6526
+ * 解析 C# provider(优先级:手动 > 扩展源 > 自动发现;需 dotnet + Roslyn dll;或 OmniSharp)。
6527
+ * @author ddj 2026年08月27号
6528
+ * @param manual 手动配置
6529
+ * @param home DSH home(缺省真实;测试可注入临时目录)
6530
+ * @returns provider 规格
6531
+ */
6532
+ function resolveCSharpProvider(manual, home = dshHome()) {
6533
+ const base = { languageId: "csharp" };
6534
+ const manualResolved = manualSpec(manual);
6535
+ if (manualResolved.ready) return {
6536
+ ...base,
6537
+ kind: "manual",
6538
+ argv: manualResolved.argv,
6539
+ cwd: manualResolved.cwd,
6540
+ ready: true
6541
+ };
6542
+ const ext = extensionProviderFor("csharp");
6543
+ if (ext) return ext;
6544
+ const discovered = candidateCSharpServers(home);
6545
+ if (discovered.roslynDll && discovered.dotnet) return {
6546
+ ...base,
6547
+ kind: "discover",
6548
+ argv: [
6549
+ discovered.dotnet,
6550
+ discovered.roslynDll,
6551
+ "--stdio"
6552
+ ],
6553
+ ready: true
6554
+ };
6555
+ if (discovered.omnisharpExe) return {
6556
+ ...base,
6557
+ kind: "discover",
6558
+ argv: [discovered.omnisharpExe, "--stdio"],
6559
+ ready: true
6560
+ };
6561
+ const reasons = [];
6562
+ if (!discovered.dotnet) reasons.push("未找到 dotnet");
6563
+ if (!discovered.roslynDll && !discovered.omnisharpExe) reasons.push("未发现 ms-dotnettools.csharp 扩展");
6564
+ return {
6565
+ ...base,
6566
+ kind: "none",
6567
+ argv: [],
6568
+ ready: false,
6569
+ reason: reasons.join(";") + "(可手动指定 Roslyn/OmniSharp 入口)"
6570
+ };
6571
+ }
6572
+ const PROVIDER_RESOLVERS = {
6573
+ lua: resolveLuaProvider,
6574
+ csharp: resolveCSharpProvider
6575
+ };
6576
+ //#endregion
6577
+ //#region src/lsp/config.ts
6578
+ const LSP_SETTINGS_NS = "dsh-vscode-mode.languageServers";
6579
+ const LSP_LANGUAGES = ["lua", "csharp"];
6580
+ function sanitizeLang(raw) {
6581
+ if (!raw || typeof raw !== "object") return void 0;
6582
+ const obj = raw;
6583
+ const out = {};
6584
+ if (typeof obj.enabled === "boolean") out.enabled = obj.enabled;
6585
+ if (typeof obj.command === "string") out.command = obj.command;
6586
+ if (typeof obj.path === "string") out.path = obj.path;
6587
+ return out;
6588
+ }
6589
+ /** 从插件组合配置读取。 */
6590
+ function configFromPlugin(config) {
6591
+ const raw = config?.languageServers;
6592
+ if (!raw || typeof raw !== "object") return {};
6593
+ const out = {};
6594
+ for (const lang of LSP_LANGUAGES) {
6595
+ const v = sanitizeLang(raw[lang]);
6596
+ if (v) out[lang] = v;
6597
+ }
6598
+ return out;
6599
+ }
6600
+ /** 从 settings 描述符读取(无 settings 服务/无该 section → {})。 */
6601
+ function configFromSettings(ctx) {
6602
+ try {
6603
+ const value = (ctx.get("settings")?.describe?.({ redactSecrets: true })?.find((item) => item.ns === LSP_SETTINGS_NS))?.value;
6604
+ if (!value || typeof value !== "object") return {};
6605
+ const out = {};
6606
+ for (const lang of LSP_LANGUAGES) {
6607
+ const v = sanitizeLang(value[lang]);
6608
+ if (v) out[lang] = v;
6609
+ }
6610
+ return out;
6611
+ } catch (error) {
6612
+ return {};
6613
+ }
6614
+ }
6615
+ /** 合并配置:settings 覆盖插件配置。 */
6616
+ function mergeConfig(a, b) {
6617
+ const out = { ...a };
6618
+ for (const lang of LSP_LANGUAGES) {
6619
+ const av = a[lang];
6620
+ const bv = b[lang];
6621
+ if (bv) out[lang] = {
6622
+ ...av,
6623
+ ...bv
6624
+ };
6625
+ else if (av) out[lang] = av;
6626
+ }
6627
+ return out;
6628
+ }
6629
+ /**
6630
+ * 解析某语言的 provider spec(合并配置后)。
6631
+ * @author ddj 2026年08月27号
6632
+ * @param ctx DSH 上下文
6633
+ * @param pluginConfig 插件组合配置
6634
+ * @param languageId 语言 id
6635
+ * @returns provider 规格
6636
+ */
6637
+ function resolveProviderSpec(ctx, pluginConfig, languageId) {
6638
+ const resolver = PROVIDER_RESOLVERS[languageId];
6639
+ if (!resolver) return {
6640
+ languageId,
6641
+ kind: "none",
6642
+ argv: [],
6643
+ ready: false,
6644
+ reason: "不支持的语言:" + languageId
6645
+ };
6646
+ const lang = mergeConfig(configFromPlugin(pluginConfig), configFromSettings(ctx))[languageId];
6647
+ if (lang && lang.enabled === false) return {
6648
+ languageId,
6649
+ kind: "none",
6650
+ argv: [],
6651
+ ready: false,
6652
+ reason: "已在设置中禁用"
6653
+ };
6654
+ return resolver({
6655
+ command: lang?.command,
6656
+ path: lang?.path
6657
+ });
6658
+ }
6659
+ /** 语言 → 扩展名(供客户端判断是否触发 LSP)。 */
6660
+ function langOfPath(path) {
6661
+ const base = String(path || "").split(/[\\/]/).pop() || "";
6662
+ const dot = base.lastIndexOf(".");
6663
+ if (dot <= 0) return null;
6664
+ const ext = base.slice(dot + 1).toLowerCase();
6665
+ for (const [lang, exts] of Object.entries({
6666
+ lua: ["lua"],
6667
+ csharp: ["cs", "csx"]
6668
+ })) if (exts.includes(ext)) return lang;
6669
+ return null;
6670
+ }
6671
+ //#endregion
6672
+ //#region src/lsp/rpc.ts
6673
+ /**
6674
+ * 打开文档跟踪:openDocs(docKey→sessionId) + rootLangRefs 计数 + sessionAcquired 归属。
6675
+ * 保证 manager 的 acquire/release 一一配对(多会话共享同一 root|lang 也平衡)。
6676
+ */
6677
+ function createDocTracker() {
6678
+ const openDocs = /* @__PURE__ */ new Map();
6679
+ const rootLangRefs = /* @__PURE__ */ new Map();
6680
+ const sessionDocs = /* @__PURE__ */ new Map();
6681
+ const keyOf = (root, lang, path) => root + "|" + lang + "|" + path;
6682
+ const rootLangOf = (docKey) => docKey.slice(0, docKey.lastIndexOf("|"));
6683
+ const rememberDoc = (sessionId, docKey) => {
6684
+ const docs = sessionDocs.get(sessionId) ?? /* @__PURE__ */ new Set();
6685
+ docs.add(docKey);
6686
+ sessionDocs.set(sessionId, docs);
6687
+ };
6688
+ const forgetDoc = (sessionId, docKey) => {
6689
+ const docs = sessionDocs.get(sessionId);
6690
+ docs?.delete(docKey);
6691
+ if (docs?.size === 0) sessionDocs.delete(sessionId);
6692
+ };
6693
+ const releaseRef = (rl) => {
6694
+ const next = (rootLangRefs.get(rl) ?? 1) - 1;
6695
+ if (next <= 0) {
6696
+ rootLangRefs.delete(rl);
6697
+ return true;
6698
+ }
6699
+ rootLangRefs.set(rl, next);
6700
+ return false;
6701
+ };
6702
+ return {
6703
+ /** 记录文档打开;同一文档可由多个会话打开,但每个会话只计一次。 */
6704
+ open(root, lang, path, sessionId) {
6705
+ const docKey = keyOf(root, lang, path);
6706
+ const owners = openDocs.get(docKey) ?? /* @__PURE__ */ new Set();
6707
+ if (owners.has(sessionId)) return false;
6708
+ owners.add(sessionId);
6709
+ openDocs.set(docKey, owners);
6710
+ rememberDoc(sessionId, docKey);
6711
+ const rl = root + "|" + lang;
6712
+ const count = rootLangRefs.get(rl) ?? 0;
6713
+ rootLangRefs.set(rl, count + 1);
6714
+ return count === 0;
6715
+ },
6716
+ /** 记录文档关闭;返回 true 表示 root|lang 已无文档(应 release)。 */
6717
+ close(root, lang, path, sessionId) {
6718
+ const docKey = keyOf(root, lang, path);
6719
+ const owners = openDocs.get(docKey);
6720
+ if (!owners?.has(sessionId)) return false;
6721
+ owners.delete(sessionId);
6722
+ if (owners.size === 0) openDocs.delete(docKey);
6723
+ forgetDoc(sessionId, docKey);
6724
+ return releaseRef(root + "|" + lang);
6725
+ },
6726
+ /** 关闭某会话全部文档;返回全局引用归零、需要 release 的 root|lang。 */
6727
+ closeSession(sessionId) {
6728
+ const docs = sessionDocs.get(sessionId);
6729
+ if (!docs) return [];
6730
+ const toRelease = /* @__PURE__ */ new Set();
6731
+ for (const docKey of docs) {
6732
+ const owners = openDocs.get(docKey);
6733
+ owners?.delete(sessionId);
6734
+ if (owners?.size === 0) openDocs.delete(docKey);
6735
+ if (releaseRef(rootLangOf(docKey))) toRelease.add(rootLangOf(docKey));
6736
+ }
6737
+ sessionDocs.delete(sessionId);
6738
+ return [...toRelease];
6739
+ }
6740
+ };
6741
+ }
6742
+ /**
6743
+ * 构造 edrv.lsp.* handlers(一次性创建,tracker 状态跨请求保留)。
6744
+ * @author ddj 2026年08月27号
6745
+ * @param deps 依赖(ctx/pluginConfig/manager)
6746
+ * @returns { handlers, disposeSession } handlers 为局部 map,disposeSession 供 session/disposed 清理
6747
+ */
6748
+ function createLspRpc(deps) {
6749
+ const { ctx, pluginConfig, manager } = deps;
6750
+ const tracker = createDocTracker();
6751
+ /** 解析会话与工作区根(失败 → null + error)。 */
6752
+ const rootOf = async (sessionId) => {
6753
+ const session = sessionOf(ctx, sessionId);
6754
+ if (!session) return { err: "会话不存在" };
6755
+ if (!cwdOf(session)) return { err: "会话无工作区" };
6756
+ try {
6757
+ return {
6758
+ root: await searchRoot(ctx, session),
6759
+ sessionId: typeof session.id === "string" && session.id ? session.id : sessionId ?? ""
6760
+ };
6761
+ } catch (error) {
6762
+ return { err: String(error) };
6763
+ }
6764
+ };
6765
+ const acquireServer = (root, lang) => manager.acquire(root, lang, (languageId) => resolveProviderSpec(ctx, pluginConfig, languageId));
6766
+ const serverOf = (root, lang) => manager.peek(root, lang);
6767
+ const rootLocations = (locations, root) => locations.map((location) => ({
6768
+ ...location,
6769
+ root
6770
+ }));
6771
+ const disposeSession = (sessionId) => {
6772
+ for (const rl of tracker.closeSession(sessionId)) {
6773
+ const sep = rl.lastIndexOf("|");
6774
+ manager.release(rl.slice(0, sep), rl.slice(sep + 1));
6775
+ }
6776
+ };
6777
+ return {
6778
+ handlers: {
6779
+ "edrv.lsp.status": async (args) => {
6780
+ if (!args.sessionId) return {
6781
+ ok: true,
6782
+ servers: manager.statusAll()
6783
+ };
6784
+ const sc = await rootOf(args.sessionId);
6785
+ if ("err" in sc) return {
6786
+ ok: false,
6787
+ error: sc.err
6788
+ };
6789
+ return {
6790
+ ok: true,
6791
+ servers: manager.statusAll().filter((status) => status.root === sc.root)
6792
+ };
6793
+ },
6794
+ "edrv.lsp.configGet": async () => {
6795
+ const settings = configFromSettings(ctx);
6796
+ const plugin = configFromPlugin(pluginConfig);
6797
+ const merged = { ...plugin };
6798
+ for (const lang of Object.keys(settings)) merged[lang] = {
6799
+ ...plugin[lang],
6800
+ ...settings[lang]
6801
+ };
6802
+ return {
6803
+ ok: true,
6804
+ config: merged
6805
+ };
6806
+ },
6807
+ "edrv.lsp.configUpdate": async (args) => {
6808
+ const lang = args.languageId;
6809
+ if (!lang || !resolveProviderSpec(ctx, pluginConfig, lang).languageId) return {
6810
+ ok: false,
6811
+ error: "不支持的语言:" + lang
6812
+ };
6813
+ const current = configFromSettings(ctx);
6814
+ const target = {
6815
+ ...current,
6816
+ [lang]: { ...current[lang] ?? {} }
6817
+ }[lang];
6818
+ if (args.enabled !== void 0) target.enabled = args.enabled;
6819
+ if (args.command !== void 0) target.command = args.command || void 0;
6820
+ if (args.path !== void 0) target.path = args.path || void 0;
6821
+ try {
6822
+ const settings = ctx.get("settings");
6823
+ if (!settings?.update) return {
6824
+ ok: false,
6825
+ error: "设置服务不可用"
6826
+ };
6827
+ await settings.update(LSP_SETTINGS_NS, { [lang]: target });
6828
+ const stored = (settings.describe?.({ redactSecrets: true })?.find((item) => item.ns === LSP_SETTINGS_NS))?.value;
6829
+ return {
6830
+ ok: true,
6831
+ config: {
6832
+ ...configFromPlugin(pluginConfig),
6833
+ ...stored
6834
+ }
6835
+ };
6836
+ } catch (error) {
6837
+ return {
6838
+ ok: false,
6839
+ error: "配置保存失败:" + String(error)
6840
+ };
6841
+ }
6842
+ },
6843
+ "edrv.lsp.sync": async (args) => {
6844
+ const lang = langOfPath(args.path);
6845
+ if (!lang) return { ok: true };
6846
+ const sc = await rootOf(args.sessionId);
6847
+ if ("err" in sc) return {
6848
+ ok: false,
6849
+ error: sc.err
6850
+ };
6851
+ const server = tracker.open(sc.root, lang, args.path, sc.sessionId) ? acquireServer(sc.root, lang) : serverOf(sc.root, lang);
6852
+ if (!server) return {
6853
+ ok: false,
6854
+ error: "语言服务器未注册"
6855
+ };
6856
+ server.sync(args.path, args.text, args.version);
6857
+ return { ok: true };
6858
+ },
6859
+ "edrv.lsp.close": async (args) => {
6860
+ const lang = langOfPath(args.path);
6861
+ if (!lang) return { ok: true };
6862
+ const sc = await rootOf(args.sessionId);
6863
+ if ("err" in sc) return { ok: true };
6864
+ if (tracker.close(sc.root, lang, args.path, sc.sessionId)) manager.release(sc.root, lang);
6865
+ return { ok: true };
6866
+ },
6867
+ "edrv.lsp.definition": async (args) => {
6868
+ const lang = langOfPath(args.path);
6869
+ if (!lang) return {
6870
+ ok: true,
6871
+ locations: []
6872
+ };
6873
+ const sc = await rootOf(args.sessionId);
6874
+ if ("err" in sc) return {
6875
+ ok: false,
6876
+ error: sc.err
6877
+ };
6878
+ try {
6879
+ const server = serverOf(sc.root, lang);
6880
+ if (!server) return {
6881
+ ok: true,
6882
+ locations: []
6883
+ };
6884
+ const locations = await server.definition(args.path, args.position.line, args.position.character);
6885
+ const rooted = rootLocations(locations, sc.root);
6886
+ const truncated = rooted.length > 500;
6887
+ return {
6888
+ ok: true,
6889
+ locations: truncated ? rooted.slice(0, 500) : rooted,
6890
+ truncated: truncated || void 0
6891
+ };
6892
+ } catch (error) {
6893
+ return {
6894
+ ok: false,
6895
+ error: "LSP 定义查询失败:" + String(error)
6896
+ };
6897
+ }
6898
+ },
6899
+ "edrv.lsp.references": async (args) => {
6900
+ const lang = langOfPath(args.path);
6901
+ if (!lang) return {
6902
+ ok: true,
6903
+ locations: []
6904
+ };
6905
+ const sc = await rootOf(args.sessionId);
6906
+ if ("err" in sc) return {
6907
+ ok: false,
6908
+ error: sc.err
6909
+ };
6910
+ try {
6911
+ const server = serverOf(sc.root, lang);
6912
+ if (!server) return {
6913
+ ok: true,
6914
+ locations: []
6915
+ };
6916
+ const locations = await server.references(args.path, args.position.line, args.position.character, args.includeDeclaration !== false);
6917
+ const rooted = rootLocations(locations, sc.root);
6918
+ const truncated = rooted.length > 500;
6919
+ return {
6920
+ ok: true,
6921
+ locations: truncated ? rooted.slice(0, 500) : rooted,
6922
+ truncated: truncated || void 0
6923
+ };
6924
+ } catch (error) {
6925
+ return {
6926
+ ok: false,
6927
+ error: "LSP 引用查询失败:" + String(error)
6928
+ };
6929
+ }
6930
+ },
6931
+ "edrv.lsp.documentSymbol": async (args) => {
6932
+ const lang = langOfPath(args.path);
6933
+ if (!lang) return {
6934
+ ok: true,
6935
+ symbols: []
6936
+ };
6937
+ const sc = await rootOf(args.sessionId);
6938
+ if ("err" in sc) return {
6939
+ ok: false,
6940
+ error: sc.err
6941
+ };
6942
+ try {
6943
+ const server = serverOf(sc.root, lang);
6944
+ if (!server) return {
6945
+ ok: true,
6946
+ symbols: []
6947
+ };
6948
+ return {
6949
+ ok: true,
6950
+ symbols: await server.documentSymbol(args.path)
6951
+ };
6952
+ } catch (error) {
6953
+ return {
6954
+ ok: false,
6955
+ error: "LSP 大纲查询失败:" + String(error)
6956
+ };
6957
+ }
6958
+ },
6959
+ "edrv.lsp.workspaceSymbol": async (args) => {
6960
+ const sc = await rootOf(args.sessionId);
6961
+ if ("err" in sc) return {
6962
+ ok: false,
6963
+ error: sc.err
6964
+ };
6965
+ const out = { symbols: [] };
6966
+ for (const lang of ["lua", "csharp"]) {
6967
+ const existing = serverOf(sc.root, lang);
6968
+ if (!existing) continue;
6969
+ try {
6970
+ const symbols = await existing.workspaceSymbol(args.query);
6971
+ if (symbols.length) out.symbols.push(...symbols);
6972
+ } catch (error) {}
6973
+ }
6974
+ const truncated = out.symbols.length > 500;
6975
+ return {
6976
+ ok: true,
6977
+ symbols: out.symbols.slice(0, 500),
6978
+ truncated: truncated || void 0
6979
+ };
6980
+ },
6981
+ "edrv.lsp.hover": async (args) => {
6982
+ const lang = langOfPath(args.path);
6983
+ if (!lang) return {
6984
+ ok: true,
6985
+ hover: void 0
6986
+ };
6987
+ const sc = await rootOf(args.sessionId);
6988
+ if ("err" in sc) return {
6989
+ ok: false,
6990
+ error: sc.err
6991
+ };
6992
+ try {
6993
+ const server = serverOf(sc.root, lang);
6994
+ if (!server) return {
6995
+ ok: true,
6996
+ hover: void 0
6997
+ };
6998
+ return {
6999
+ ok: true,
7000
+ hover: await server.hover(args.path, args.position.line, args.position.character) ?? void 0
7001
+ };
7002
+ } catch (error) {
7003
+ return {
7004
+ ok: false,
7005
+ error: "LSP hover 查询失败:" + String(error)
7006
+ };
7007
+ }
7008
+ },
7009
+ "edrv.lsp.ext.list": async () => ({
7010
+ ok: true,
7011
+ extensions: listInstalled()
7012
+ }),
7013
+ "edrv.lsp.ext.market": async (args) => {
7014
+ try {
7015
+ return {
7016
+ ok: true,
7017
+ extensions: await marketSearch(args.query, args.size ?? 12)
7018
+ };
7019
+ } catch (error) {
7020
+ return {
7021
+ ok: false,
7022
+ error: "市场搜索失败:" + String(error)
7023
+ };
7024
+ }
7025
+ },
7026
+ "edrv.lsp.ext.install": async (args) => {
7027
+ try {
7028
+ let info;
7029
+ if (args.vsixPath) info = await installVsixFile(args.vsixPath);
7030
+ else if (args.namespace && args.name) info = await installFromMarket(args.namespace, args.name, args.version);
7031
+ else return {
7032
+ ok: false,
7033
+ error: "缺少安装源(vsixPath 或 namespace+name)"
7034
+ };
7035
+ return {
7036
+ ok: true,
7037
+ extension: info
7038
+ };
7039
+ } catch (error) {
7040
+ return {
7041
+ ok: false,
7042
+ error: "安装失败:" + String(error)
7043
+ };
7044
+ }
7045
+ },
7046
+ "edrv.lsp.ext.uninstall": async (args) => {
7047
+ return uninstall(args.id) ? { ok: true } : {
7048
+ ok: false,
7049
+ error: "扩展未安装:" + args.id
7050
+ };
7051
+ },
7052
+ "edrv.lsp.ext.update": async (args) => {
7053
+ try {
7054
+ const updated = await updateExtension(args.id);
7055
+ if (!updated) {
7056
+ const current = listInstalled().find((e) => e.id === args.id);
7057
+ return current ? {
7058
+ ok: true,
7059
+ extension: current,
7060
+ updated: false
7061
+ } : {
7062
+ ok: false,
7063
+ error: "扩展未安装:" + args.id
7064
+ };
7065
+ }
7066
+ return {
7067
+ ok: true,
7068
+ extension: updated,
7069
+ updated: true
7070
+ };
7071
+ } catch (error) {
7072
+ return {
7073
+ ok: false,
7074
+ error: "更新失败:" + String(error)
7075
+ };
7076
+ }
7077
+ },
7078
+ "edrv.lsp.ext.updates": async () => {
7079
+ try {
7080
+ const updates = [];
7081
+ for (const ext of listInstalled()) {
7082
+ const meta = await marketGet(ext.namespace, ext.name);
7083
+ const latest = String(meta?.version ?? "");
7084
+ if (latest && latest !== ext.version) updates.push({
7085
+ id: ext.id,
7086
+ current: ext.version,
7087
+ latest,
7088
+ displayName: ext.displayName
7089
+ });
7090
+ }
7091
+ return {
7092
+ ok: true,
7093
+ updates
7094
+ };
7095
+ } catch (error) {
7096
+ return {
7097
+ ok: false,
7098
+ error: "检查更新失败:" + String(error)
7099
+ };
7100
+ }
7101
+ }
7102
+ },
7103
+ disposeSession
7104
+ };
7105
+ }
7106
+ //#endregion
7107
+ //#region src/lsp/settings.ts
7108
+ /**
7109
+ * 安装 LSP 设置 section。
7110
+ * @author ddj 2026年08月27号
7111
+ * @param ctx DSH 上下文
7112
+ * @param entry 初始配置
7113
+ * @returns 是否成功安装
7114
+ */
7115
+ async function installLspSettingsSection(ctx, entry) {
7116
+ const deps = await loadSettingsDeps();
7117
+ if (!deps) return false;
7118
+ try {
7119
+ const z = deps.z;
7120
+ const langShape = {};
7121
+ for (const lang of LSP_LANGUAGES) langShape[lang] = z.object({
7122
+ enabled: z.boolean(),
7123
+ command: z.string(),
7124
+ path: z.string()
7125
+ }).default({});
7126
+ const schema = z.object(langShape).default({});
7127
+ deps.installSettingsSection(ctx, LSP_SETTINGS_NS, schema, entry, {
7128
+ setSource: () => {},
7129
+ onChange: () => {}
7130
+ });
7131
+ return true;
7132
+ } catch (error) {
7133
+ console.error("[dsh-vscode-mode] LSP 设置 section 安装失败:" + String(error));
7134
+ return false;
7135
+ }
7136
+ }
7137
+ //#endregion
7138
+ //#region src/index.ts
7139
+ /**
7140
+ * @dsh-external 生态 → dsh-vscode-mode:DSH 上的类 VSCode 编码体验(Host 半入口)。
7141
+ * 职责:装配 capture(tools/result 捕获 edit/write 差异)、RPC 分发(edrv.* / mcp.* / vscode.* / compat)、
7142
+ * webServer 路由(/edrv/rpc、/edrv/assets/*、/edrv/vendor/*)、工作区旁车持久化、
7143
+ * 兼容层装配(依赖守卫 + 重复装配自诊断 + 启动日志兼容性报告)。
7144
+ * 结构:shared/(双面契约) + model/store/capture/workspace/revert/registry/rpc/routes/compat(host 模块)。
7145
+ * 作者 ddj 2026-08-20
7146
+ */
7147
+ const name = "dsh-vscode-mode";
7148
+ const inject = [
7149
+ "sessions",
7150
+ "fs",
7151
+ "webServer",
7152
+ "loader",
7153
+ "tools",
7154
+ "workspaceRegistry",
7155
+ "agents"
7156
+ ];
7157
+ /**
7158
+ * 装配插件:挂事件监听、注册路由、安装兼容层。
7159
+ * @author ddj 2026年08月20号
7160
+ * @param ctx DSH 上下文(sessions/fs/webServer 由 inject 提供;sandboxPolicy/subprocess 惰性获取)
7161
+ * @param config 插件配置(可选 imageDir 覆盖图标目录)
7162
+ */
7163
+ function apply(ctx, config) {
7164
+ const registry = /* @__PURE__ */ new Map();
7165
+ const searcher = newSearcher(ctx);
7166
+ const contentSearcher = newContentSearcher(ctx);
7167
+ /** LSP 服务器管理器(语言智能:跳转/引用/大纲)。 */
7168
+ const lspManager = createLspManager((line) => ctx.logger?.debug?.("[" + name + "] " + line));
7169
+ /** 兼容性警告收集(route 护栏等写入,启动日志一并输出)。 */
7170
+ const warnings = [];
7171
+ setupOpenSettings(ctx, config, () => {});
7172
+ installLspSettingsSection(ctx, {});
7173
+ /** LSP RPC 与会话清理(一次性创建,tracker 状态跨请求保留)。 */
7174
+ const lspRpc = createLspRpc({
7175
+ ctx,
7176
+ pluginConfig: config,
7177
+ manager: lspManager
7178
+ });
7179
+ const lspHandlers = lspRpc.handlers;
7180
+ ctx.on("tools/result", (exec, result) => {
7181
+ captureToolResult(ctx, registry, exec, result);
7182
+ });
7183
+ ctx.on("session/disposed", (session) => {
7184
+ const cwd = cwdOf(session);
7185
+ if (cwd) {
7186
+ registry.delete(cwd);
7187
+ dropFileIndex(cwd);
7188
+ searcher.dispose(cwd);
7189
+ contentSearcher.dispose(cwd);
7190
+ disposeIndex(cwd);
7191
+ }
7192
+ const sid = session?.id;
7193
+ if (typeof sid === "string") lspRpc.disposeSession(sid);
7194
+ });
7195
+ registerRoutes(ctx, config, (method, args) => handleRpc(ctx, registry, method, args, searcher, contentSearcher, lspHandlers), (warning) => warnings.push(warning));
7196
+ installIsolation(ctx);
7197
+ sweepTreeCache();
7198
+ logCompatSummary(ctx, warnings);
7199
+ ctx.effect(() => () => {
7200
+ lspManager.disposeAll().catch(() => {});
7201
+ disposeAllServers();
7202
+ });
7203
+ ctx.logger?.info?.("[" + name + "] 编辑差异审查已装配(/edrv/rpc 路由就绪,项目 MCP 隔离已启用,语言服务器 LSP 已接入)");
3907
7204
  }
3908
7205
  /**
3909
7206
  * 异步输出兼容性报告摘要(含重复装配/路由冲突自诊断)。