dsh-vscode-mode 0.4.2 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -259,6 +259,15 @@ function bindHostLog(ctx) {
259
259
  }
260
260
  //#endregion
261
261
  //#region src/fileOpenSettings.ts
262
+ /**
263
+ * dsh-vscode-mode host — 文件链接打开工具 + 快捷键的持久化设置。
264
+ * 依赖守卫:schemastery 动态加载;@deepseek-ai/dsh-settings 仅作 legacy 探测——
265
+ * rc 线导出 installSettingsSection free function,0.1.2-alpha 起移除(改由 settings
266
+ * 服务的 installSection 方法承载,见 runSettingsInstall 三策略)。
267
+ * 缺失/任一策略失败时插件仍可加载(fileOpenTool 降级为配置值,compat 报告可见),
268
+ * 全程 try/catch,不产生未捕获 rejection。
269
+ * 作者 ddj 2026年08月24号 / 2026年08月26号 / 2026年09月02号
270
+ */
262
271
  const FILE_OPEN_SETTINGS_NS = "dsh-vscode-mode";
263
272
  const FILE_OPEN_DEFAULT = "auto";
264
273
  /** AI 内联补全配置默认值(默认关闭;路由空 = 自动;档位空 = 跟随模型默认)。 */
@@ -270,14 +279,31 @@ const AI_CONFIG_DEFAULT = {
270
279
  };
271
280
  let depsPromise;
272
281
  /**
282
+ * 宿主锚点动态导入:先经 process.argv[1](DSH 启动脚本所在树)解析并加载目标包,
283
+ * 避开本插件自身 node_modules 的 dev 依赖副本(dev-link 下 import() 相对插件位置
284
+ * 解析,会命中 rc 线旧包 dsh-settings@0.1.0-rc.8);argv[1] 缺失或解析失败回退
285
+ * 普通 specifier import。
286
+ * @author ddj 2026年09月15号
287
+ * @param specifier 包名
288
+ * @returns 加载的模块命名空间
289
+ */
290
+ async function hostImport(specifier) {
291
+ const entry = process.argv[1];
292
+ if (entry) try {
293
+ const resolved = createRequire(entry).resolve(specifier);
294
+ return await import(pathToFileURL(resolved).href);
295
+ } catch {}
296
+ return import(specifier);
297
+ }
298
+ /**
273
299
  * 动态加载设置依赖(模块级缓存;任一缺失/失败返回 null 而非抛错)。
274
300
  * installSettingsSection 仅在 rc 线 dsh-settings 中存在时提供(alpha 起移除,
275
301
  * 属性探测得到 undefined,不抛错)。
276
- * @author ddj 2026年08月24号
302
+ * @author ddj 2026年08月24号 / 2026年09月15号
277
303
  * @returns 设置依赖或 null
278
304
  */
279
305
  function loadSettingsDeps() {
280
- if (!depsPromise) depsPromise = Promise.all([import("@deepseek-ai/dsh-settings"), import("schemastery")]).then(([dshSettings, schemastery]) => ({
306
+ if (!depsPromise) depsPromise = Promise.all([hostImport("@deepseek-ai/dsh-settings"), hostImport("schemastery")]).then(([dshSettings, schemastery]) => ({
281
307
  installSettingsSection: dshSettings.installSettingsSection,
282
308
  z: schemastery.default
283
309
  })).catch(() => null);
@@ -736,6 +762,8 @@ function inDshRange(version, range) {
736
762
  function familyLabel(input) {
737
763
  const version = parseDshVersion(input);
738
764
  if (!version) return "未知";
765
+ if (inDshRange(version, { from: "0.1.6-alpha.1" })) return "0.1.6-alpha 及更新(MCP SDK v2 + Web 侧边栏终端 + 文件链接默认侧栏预览)";
766
+ if (inDshRange(version, { from: "0.1.5-alpha.1" })) return "0.1.5-alpha 及更新(官方右侧 Sidebar 编辑区 + sidebar.panellist)";
739
767
  if (inDshRange(version, { from: "0.1.3-alpha.1" })) return "0.1.3-alpha 及更新(对话文件链接=remote.session.openWorkspacePath)";
740
768
  if (inDshRange(version, { from: "0.1.2-alpha.1" })) return "0.1.2-alpha 及更新(设置 API=settings.installSection)";
741
769
  return "0.1.0/0.1.1 rc 线(设置 API=installSettingsSection)";
@@ -748,19 +776,48 @@ const VERSION_CANDIDATES = [
748
776
  "@deepseek-ai/dsh/package.json"
749
777
  ];
750
778
  let detectedVersion;
779
+ /**
780
+ * 宿主入口 require 锚点:process.argv[1] 指向运行中 DSH 的启动脚本
781
+ * (安装树 node_modules/@deepseek-ai/dsh/... 或 profile 树),从其所在目录
782
+ * 向上解析能命中宿主的 @deepseek-ai 包——避开本插件自身 node_modules 里的
783
+ * dev 依赖副本(dev-link 安装下 import.meta.url 锚点会先命中 rc 线副本,
784
+ * 曾致版本探测与 deps 加载错位到 0.1.0-rc.8,见 fileOpenSettings 的 hostImport)。
785
+ * @author ddj 2026年09月15号
786
+ * @returns 宿主锚点 require;argv[1] 缺失或不可用时 undefined
787
+ */
788
+ function hostRequire() {
789
+ const entry = process.argv[1];
790
+ if (!entry) return void 0;
791
+ try {
792
+ return createRequire(entry);
793
+ } catch {
794
+ return;
795
+ }
796
+ }
797
+ /** 用指定 resolver 解析一个候选包版本(解析/读取失败返回 null)。 */
798
+ function resolveVersionOf(specifier, resolver) {
799
+ try {
800
+ const file = resolver.resolve(specifier);
801
+ const pkg = JSON.parse(readFileSync(file, "utf8"));
802
+ if (typeof pkg.version === "string" && parseDshVersion(pkg.version) !== null) return pkg.version;
803
+ } catch {}
804
+ return null;
805
+ }
751
806
  /** 探测运行中 DSH 核心版本(模块级缓存;失败空串,不抛错)。 */
752
807
  function detectDshVersion() {
753
808
  if (detectedVersion !== void 0) return detectedVersion;
754
809
  detectedVersion = "";
755
- const require = createRequire(import.meta.url);
756
- for (const specifier of VERSION_CANDIDATES) try {
757
- const file = require.resolve(specifier);
758
- const pkg = JSON.parse(readFileSync(file, "utf8"));
759
- if (typeof pkg.version === "string" && parseDshVersion(pkg.version) !== null) {
760
- detectedVersion = pkg.version;
761
- break;
810
+ const resolvers = [hostRequire(), createRequire(import.meta.url)];
811
+ for (const resolver of resolvers) {
812
+ if (!resolver) continue;
813
+ for (const specifier of VERSION_CANDIDATES) {
814
+ const version = resolveVersionOf(specifier, resolver);
815
+ if (version !== null) {
816
+ detectedVersion = version;
817
+ return detectedVersion;
818
+ }
762
819
  }
763
- } catch {}
820
+ }
764
821
  return detectedVersion;
765
822
  }
766
823
  //#endregion
@@ -1642,7 +1699,7 @@ function detectGuards(ctx) {
1642
1699
  }];
1643
1700
  }
1644
1701
  /** 已实测覆盖的最高 DSH 版本(适配矩阵上界,超过则提示,见 buildReport)。 */
1645
- const TESTED_DSH_MAX = "0.1.3-alpha.2";
1702
+ const TESTED_DSH_MAX = "0.1.6-alpha.1";
1646
1703
  /** 版本适配机制状态行:DSH 版本探测 + 设置 section 安装策略。 */
1647
1704
  function versionAdapters(dshVersion) {
1648
1705
  const adapters = [];
@@ -1688,7 +1745,7 @@ async function buildReport(ctx, options) {
1688
1745
  if (settingsInstallStrategy() === "none") warnings.push("设置 section 安装不可用:" + settingsInstallNote());
1689
1746
  const parsed = parseDshVersion(dshVersion);
1690
1747
  const testedMax = parseDshVersion(TESTED_DSH_MAX);
1691
- if (parsed && testedMax && compareDshVersions(parsed, testedMax) > 0) warnings.push("DSH " + dshVersion + " 高于已实测版本(0.1.3-alpha.2):设置 API 按能力探测运行,异常时请回报适配矩阵");
1748
+ if (parsed && testedMax && compareDshVersions(parsed, testedMax) > 0) warnings.push("DSH " + dshVersion + " 高于已实测版本(0.1.6-alpha.1):设置 API 按能力探测运行,异常时请回报适配矩阵");
1692
1749
  for (const guard of guards) if (!guard.active && guard.note) warnings.push(guard.note);
1693
1750
  return {
1694
1751
  pluginVersion: options?.version ?? pluginVersionOf(),
@@ -4240,7 +4297,8 @@ async function revertHunk(ctx, session, record, idx) {
4240
4297
  const location = locateHunks(content, [precise])[0];
4241
4298
  if (!location?.matched) return {
4242
4299
  ok: false,
4243
- error: "回滚失败:该区域可能已被后续修改影响"
4300
+ stale: true,
4301
+ error: "回滚失败:该区域在文件中已不存在(可能已被后续修改覆盖)"
4244
4302
  };
4245
4303
  const result = applyLocations(content, [location], true);
4246
4304
  await fs.writeText(target, result.content, void 0, void 0, policyOf(ctx, session));
@@ -6965,6 +7023,29 @@ function snippetTargetOf(path) {
6965
7023
  return isSnippetFilePath(path) ? path.replace(/\\/g, "/") : null;
6966
7024
  }
6967
7025
  /**
7026
+ * 本地文件系统版本令牌(mtime+size):与 ctx.fs 的不透明版本串用途相同,
7027
+ * 仅供全局片段文件(走 node:fs 直读、不经 ctx.fs)在 edrv.read 时回带。
7028
+ * @author ddj 2026年09月15号
7029
+ * @param info node fs.Stats(stat 失败传 null)
7030
+ * @returns 版本令牌;取不到 → 空串(不启用护栏)
7031
+ */
7032
+ function fileVersionOf(info) {
7033
+ if (!info || typeof info.mtimeMs !== "number" || typeof info.size !== "number") return "";
7034
+ return info.mtimeMs + ":" + info.size;
7035
+ }
7036
+ /**
7037
+ * 判定错误是否为「读取后文件已被外部修改」(版本守卫拒绝写入)。
7038
+ * 不依赖 host fs 的 FsError 类(外置模块,instanceof 不可靠),按错误码与文案双重判定。
7039
+ * @author ddj 2026年09月15号
7040
+ * @param error 捕获到的异常
7041
+ * @returns 是否为版本冲突
7042
+ */
7043
+ function isStaleVersion(error) {
7044
+ const err = error;
7045
+ if (err && err.code === "FS_STALE_VERSION") return true;
7046
+ return (err && typeof err.message === "string" ? err.message : String(error)).includes("changed since it was read");
7047
+ }
7048
+ /**
6968
7049
  * 手动保存收尾(edrv.save / edrv.saveBinary 共用):目录树失效 + 该路径 pending
6969
7050
  * 文本差异记录标记 superseded 并归档(旧文本 diff 不得再应用到新内容上)。
6970
7051
  * @author ddj 2026年09月22号
@@ -6991,8 +7072,9 @@ async function afterManualSave(ctx, registry, sc, path) {
6991
7072
  }
6992
7073
  /**
6993
7074
  * 批量决策核心:一次会话/桶解析 + 逐项处理 + 统一落盘(accept/reject 单条与 decideBatch 共用)。
6994
- * rejected 项先做回滚,失败记入该项 error 且不改决策;本批次新增"已解决"记录一次性归档,
6995
- * 有任何成功项则整桶只写盘一次,避免逐条读写整个 sidecar。
7075
+ * rejected 项先做回滚,失败记入该项 error 且不改决策;hunk 的新文本已不在文件中
7076
+ * (被后续修改覆盖/撤销)时不算失败:不写文件,按已回滚记录决策并归档(结果项带 stale)。
7077
+ * 本批次新增"已解决"记录一次性归档,有任何成功项则整桶只写盘一次,避免逐条读写整个 sidecar。
6996
7078
  * @author ddj 2026年08月25号
6997
7079
  * @param items 决策项(按数组顺序处理,rejected 的先后即回滚顺序)
6998
7080
  * @returns 逐项结果(ok 项含更新后的记录视图)
@@ -7011,26 +7093,33 @@ async function applyDecisions(ctx, session, cwd, bucket, items) {
7011
7093
  });
7012
7094
  continue;
7013
7095
  }
7096
+ let revertedStale = false;
7014
7097
  if (item.decision === "rejected") {
7015
- const outcome = (item.scope ?? "call") === "hunk" ? await revertHunk(ctx, session, record, item.hunkIndex ?? -1) : await revertCall(ctx, session, record);
7098
+ const scope = item.scope ?? "call";
7099
+ const outcome = scope === "hunk" ? await revertHunk(ctx, session, record, item.hunkIndex ?? -1) : await revertCall(ctx, session, record);
7016
7100
  if (!outcome.ok) {
7017
- results.push({
7018
- callId: item.callId,
7019
- ok: false,
7020
- error: outcome.error
7021
- });
7022
- continue;
7101
+ if (scope !== "hunk" || outcome.stale !== true) {
7102
+ results.push({
7103
+ callId: item.callId,
7104
+ ok: false,
7105
+ error: outcome.error
7106
+ });
7107
+ continue;
7108
+ }
7109
+ revertedStale = true;
7023
7110
  }
7024
7111
  }
7025
7112
  const wasResolved = recordResolved(record);
7026
7113
  markDecision(record, item.scope ?? "call", item.hunkIndex, item.decision);
7027
7114
  changed = true;
7028
7115
  if (!wasResolved && recordResolved(record)) resolved.push(record);
7029
- results.push({
7116
+ const result = {
7030
7117
  callId: item.callId,
7031
7118
  ok: true,
7032
7119
  record: recView(record)
7033
- });
7120
+ };
7121
+ if (revertedStale) result.stale = true;
7122
+ results.push(result);
7034
7123
  }
7035
7124
  if (resolved.length) await archiveRecords(ctx, session, cwd, bucket, resolved, items.some((item) => item.decision === "rejected") ? "已处理(回滚)" : "已处理", { deferSave: true });
7036
7125
  if (changed) await saveBucket(ctx, cwd, bucket, session);
@@ -7063,7 +7152,7 @@ async function integrationBaseUrlOf(ctx) {
7063
7152
  return (typeof value?.integrationBaseUrl === "string" ? value.integrationBaseUrl.trim() : "") || "http://127.0.0.1:3080";
7064
7153
  }
7065
7154
  /** 各方法 handler 表(类型由 shared/rpc 的 RpcHandlerMap 约束)。 */
7066
- function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearcher = newContentSearcher(ctx), lspHandlers, aiHandlers) {
7155
+ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearcher = newContentSearcher(ctx), lspHandlers, aiHandlers, fileVersions) {
7067
7156
  return {
7068
7157
  ...lspHandlers ?? {},
7069
7158
  ...aiHandlers ?? {},
@@ -7154,10 +7243,12 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
7154
7243
  const snippetTarget = snippetTargetOf(args.path);
7155
7244
  if (snippetTarget && args.encoding !== "base64") try {
7156
7245
  const content = await readFile(snippetTarget, "utf8");
7246
+ const info = await stat(snippetTarget).catch(() => null);
7157
7247
  return {
7158
7248
  ok: true,
7159
7249
  content,
7160
- size: content.length
7250
+ size: content.length,
7251
+ version: fileVersionOf(info)
7161
7252
  };
7162
7253
  } catch (error) {
7163
7254
  return {
@@ -7195,7 +7286,8 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
7195
7286
  content: Buffer.from(bytes).toString("base64"),
7196
7287
  size: bytes.byteLength,
7197
7288
  encoding: "base64",
7198
- mime: binaryMimeOf(args.path)
7289
+ mime: binaryMimeOf(args.path),
7290
+ version: String(info.version ?? "")
7199
7291
  };
7200
7292
  }
7201
7293
  if ((info.size ?? 0) > 8388608) return {
@@ -7206,7 +7298,8 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
7206
7298
  return {
7207
7299
  ok: true,
7208
7300
  content,
7209
- size: content.length
7301
+ size: content.length,
7302
+ version: String(info.version ?? "")
7210
7303
  };
7211
7304
  } catch (error) {
7212
7305
  return {
@@ -7215,6 +7308,13 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
7215
7308
  };
7216
7309
  }
7217
7310
  },
7311
+ "edrv.versions": async (args) => {
7312
+ if (!fileVersions) return {
7313
+ ok: false,
7314
+ error: "文件版本观察器未装配"
7315
+ };
7316
+ return fileVersions.versions(args);
7317
+ },
7218
7318
  "edrv.original": async (args) => {
7219
7319
  const sc = await requireSession(ctx, args.sessionId);
7220
7320
  if ("err" in sc) return {
@@ -7291,10 +7391,23 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
7291
7391
  };
7292
7392
  try {
7293
7393
  const target = await resolveTarget(ctx, sc.session, args.path);
7294
- await fs.writeText(target, args.content, void 0, void 0, policyOf(ctx, sc.session));
7394
+ const rev = typeof args.rev === "string" && args.rev ? args.rev : null;
7395
+ const intent = rev ? {
7396
+ kind: "replaceIfVersion",
7397
+ version: rev
7398
+ } : void 0;
7399
+ const outcome = await fs.writeText(target, args.content, intent, void 0, policyOf(ctx, sc.session));
7295
7400
  await afterManualSave(ctx, registry, sc, args.path);
7296
- return { ok: true };
7401
+ return {
7402
+ ok: true,
7403
+ rev: outcome?.version ? String(outcome.version) : rev ?? void 0
7404
+ };
7297
7405
  } catch (error) {
7406
+ if (isStaleVersion(error)) return {
7407
+ ok: false,
7408
+ conflict: true,
7409
+ error: "文件已被外部修改,请先重新加载(或用编辑器内容覆盖)"
7410
+ };
7298
7411
  return {
7299
7412
  ok: false,
7300
7413
  error: "保存失败:" + String(error)
@@ -8222,8 +8335,8 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
8222
8335
  * 统一入口:按方法分发到 handler 表。
8223
8336
  * @author ddj 2026年08月20号 / 2026年08月26号
8224
8337
  */
8225
- async function handleRpc(ctx, registry, method, args, searcher = newSearcher(ctx), contentSearcher = newContentSearcher(ctx), lspHandlers, aiHandlers) {
8226
- const handler = buildHandlers(ctx, registry, searcher, contentSearcher, lspHandlers, aiHandlers)[method];
8338
+ async function handleRpc(ctx, registry, method, args, searcher = newSearcher(ctx), contentSearcher = newContentSearcher(ctx), lspHandlers, aiHandlers, fileVersions) {
8339
+ const handler = buildHandlers(ctx, registry, searcher, contentSearcher, lspHandlers, aiHandlers, fileVersions)[method];
8227
8340
  if (!handler) return {
8228
8341
  ok: false,
8229
8342
  error: "未知方法: " + String(method)
@@ -11656,6 +11769,118 @@ function createAiRpc(deps) {
11656
11769
  })
11657
11770
  } };
11658
11771
  }
11772
+ /** 基准表键:工作区根 + 归一化路径。 */
11773
+ function memoKey(cwd, path) {
11774
+ return String(cwd).replace(/\\/g, "/") + "\0" + String(path).replace(/\\/g, "/");
11775
+ }
11776
+ /** 清理超期/超量记忆(每次写入前调用,摊还 O(1))。 */
11777
+ function sweep(memos, now) {
11778
+ if (memos.size === 0) return;
11779
+ for (const [key, memo] of memos) if (now - memo.at > 3e5) memos.delete(key);
11780
+ while (memos.size > 500) {
11781
+ const oldest = memos.keys().next().value;
11782
+ if (oldest === void 0) break;
11783
+ memos.delete(oldest);
11784
+ }
11785
+ }
11786
+ /**
11787
+ * 创建文件版本观察器(host 单例,随插件装配创建、卸载 dispose)。
11788
+ * @author ddj 2026年09月15号
11789
+ * @param ctx DSH host 上下文
11790
+ * @returns 观察器实例
11791
+ */
11792
+ function createFileVersions(ctx) {
11793
+ /** 基准表:memoKey → 上次观察到的版本(仅用于变化判定与树失效)。 */
11794
+ const memos = /* @__PURE__ */ new Map();
11795
+ /**
11796
+ * 记录一次观察并按需失效目录树;返回本次是否为「变化」。
11797
+ * @author ddj 2026年09月15号
11798
+ * @param cwd 工作区根
11799
+ * @param path 请求路径
11800
+ * @param version 当前版本令牌(空串 = 后端不提供)
11801
+ * @param present 磁盘上是否存在
11802
+ * @returns 是否检测到变化
11803
+ */
11804
+ const syncTree = (cwd, path, version, present) => {
11805
+ const now = Date.now();
11806
+ const key = memoKey(cwd, path);
11807
+ const prev = memos.get(key);
11808
+ sweep(memos, now);
11809
+ const changed = prev !== void 0 && (prev.version !== version || !present);
11810
+ memos.set(key, {
11811
+ version,
11812
+ at: now
11813
+ });
11814
+ if (changed) log.debug("检测到磁盘变化(已失效目录树):" + path);
11815
+ if (changed) invalidateIndex(ctx, cwd, path);
11816
+ return changed;
11817
+ };
11818
+ /**
11819
+ * 单条路径观察:解析 → stat → 组装条目(异常一律降级为 missing,不整批失败)。
11820
+ * @author ddj 2026年09月15号
11821
+ * @param fs host fs 服务
11822
+ * @param cwd 工作区根(为空表示会话无工作区,跳过变化判定)
11823
+ * @param path 请求路径
11824
+ * @returns 版本条目
11825
+ */
11826
+ const observe = async (fs, cwd, path) => {
11827
+ try {
11828
+ const target = await fs.resolve(path, cwd ? { cwd } : {});
11829
+ const info = await fs.stat(target);
11830
+ if (!info || info.type === "other") {
11831
+ if (cwd) syncTree(cwd, path, "", false);
11832
+ return {
11833
+ path,
11834
+ version: "",
11835
+ type: "missing"
11836
+ };
11837
+ }
11838
+ const version = info.version ? String(info.version) : "";
11839
+ if (cwd) syncTree(cwd, path, version, true);
11840
+ return {
11841
+ path,
11842
+ version,
11843
+ size: typeof info.size === "number" ? info.size : void 0,
11844
+ type: info.type
11845
+ };
11846
+ } catch (error) {
11847
+ if (cwd) syncTree(cwd, path, "", false);
11848
+ return {
11849
+ path,
11850
+ version: "",
11851
+ type: "missing",
11852
+ error: String(error)
11853
+ };
11854
+ }
11855
+ };
11856
+ /**
11857
+ * 批查入口(RPC handler 直接委托):同一会话的路径共享一次会话解析。
11858
+ * @author ddj 2026年09月15号
11859
+ * @param args 会话 id 与路径列表
11860
+ * @returns 始终 ok 的逐条结果
11861
+ */
11862
+ const versions = async (args) => {
11863
+ const fs = ctx.get("fs");
11864
+ const paths = (Array.isArray(args.paths) ? args.paths : []).filter((p) => typeof p === "string" && p.length > 0).slice(0, 200);
11865
+ if (!fs || !paths.length) return {
11866
+ ok: true,
11867
+ items: []
11868
+ };
11869
+ const cwd = cwdOf(sessionOf(ctx, args.sessionId));
11870
+ const items = [];
11871
+ for (const path of paths) items.push(await observe(fs, cwd, path));
11872
+ return {
11873
+ ok: true,
11874
+ items
11875
+ };
11876
+ };
11877
+ return {
11878
+ versions,
11879
+ dispose() {
11880
+ memos.clear();
11881
+ }
11882
+ };
11883
+ }
11659
11884
  //#endregion
11660
11885
  //#region src/index.ts
11661
11886
  /**
@@ -11712,6 +11937,8 @@ function apply(ctx, config) {
11712
11937
  ctx,
11713
11938
  settings: openSettings
11714
11939
  }).handlers;
11940
+ /** 文件磁盘新鲜度观察器(客户端轮询 edrv.versions;变化时顺带失效目录树缓存)。 */
11941
+ const fileVersions = createFileVersions(ctx);
11715
11942
  ctx.on("tools/result", (exec, result) => {
11716
11943
  captureToolResult(ctx, registry, exec, result);
11717
11944
  });
@@ -11727,7 +11954,7 @@ function apply(ctx, config) {
11727
11954
  const sid = session?.id;
11728
11955
  if (typeof sid === "string") lspRpc.disposeSession(sid);
11729
11956
  });
11730
- registerRoutes(ctx, config, (method, args) => handleRpc(ctx, registry, method, args, searcher, contentSearcher, lspHandlers, aiHandlers), (warning) => warnings.push(warning));
11957
+ registerRoutes(ctx, config, (method, args) => handleRpc(ctx, registry, method, args, searcher, contentSearcher, lspHandlers, aiHandlers, fileVersions), (warning) => warnings.push(warning));
11731
11958
  installIsolation(ctx);
11732
11959
  ctx.effect(() => shellMenuLifecycle(ctx));
11733
11960
  sweepTreeCache();
@@ -11736,6 +11963,7 @@ function apply(ctx, config) {
11736
11963
  lspManager.disposeAll().catch(() => {});
11737
11964
  disposeAllServers();
11738
11965
  });
11966
+ ctx.effect(() => () => fileVersions.dispose());
11739
11967
  hookExitReclaim();
11740
11968
  log.info("编辑差异审查已装配(/edrv/rpc 路由就绪,项目 MCP 隔离已启用,语言服务器 LSP 已接入,规则注入" + (rulesInstalled ? "已接入" : "未接入") + ",技能组" + (skillsDispatched ? "装配中" : "未装配") + ")");
11741
11969
  }