dsh-vscode-mode 0.4.3 → 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(),
@@ -6966,6 +7023,29 @@ function snippetTargetOf(path) {
6966
7023
  return isSnippetFilePath(path) ? path.replace(/\\/g, "/") : null;
6967
7024
  }
6968
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
+ /**
6969
7049
  * 手动保存收尾(edrv.save / edrv.saveBinary 共用):目录树失效 + 该路径 pending
6970
7050
  * 文本差异记录标记 superseded 并归档(旧文本 diff 不得再应用到新内容上)。
6971
7051
  * @author ddj 2026年09月22号
@@ -7072,7 +7152,7 @@ async function integrationBaseUrlOf(ctx) {
7072
7152
  return (typeof value?.integrationBaseUrl === "string" ? value.integrationBaseUrl.trim() : "") || "http://127.0.0.1:3080";
7073
7153
  }
7074
7154
  /** 各方法 handler 表(类型由 shared/rpc 的 RpcHandlerMap 约束)。 */
7075
- 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) {
7076
7156
  return {
7077
7157
  ...lspHandlers ?? {},
7078
7158
  ...aiHandlers ?? {},
@@ -7163,10 +7243,12 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
7163
7243
  const snippetTarget = snippetTargetOf(args.path);
7164
7244
  if (snippetTarget && args.encoding !== "base64") try {
7165
7245
  const content = await readFile(snippetTarget, "utf8");
7246
+ const info = await stat(snippetTarget).catch(() => null);
7166
7247
  return {
7167
7248
  ok: true,
7168
7249
  content,
7169
- size: content.length
7250
+ size: content.length,
7251
+ version: fileVersionOf(info)
7170
7252
  };
7171
7253
  } catch (error) {
7172
7254
  return {
@@ -7204,7 +7286,8 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
7204
7286
  content: Buffer.from(bytes).toString("base64"),
7205
7287
  size: bytes.byteLength,
7206
7288
  encoding: "base64",
7207
- mime: binaryMimeOf(args.path)
7289
+ mime: binaryMimeOf(args.path),
7290
+ version: String(info.version ?? "")
7208
7291
  };
7209
7292
  }
7210
7293
  if ((info.size ?? 0) > 8388608) return {
@@ -7215,7 +7298,8 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
7215
7298
  return {
7216
7299
  ok: true,
7217
7300
  content,
7218
- size: content.length
7301
+ size: content.length,
7302
+ version: String(info.version ?? "")
7219
7303
  };
7220
7304
  } catch (error) {
7221
7305
  return {
@@ -7224,6 +7308,13 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
7224
7308
  };
7225
7309
  }
7226
7310
  },
7311
+ "edrv.versions": async (args) => {
7312
+ if (!fileVersions) return {
7313
+ ok: false,
7314
+ error: "文件版本观察器未装配"
7315
+ };
7316
+ return fileVersions.versions(args);
7317
+ },
7227
7318
  "edrv.original": async (args) => {
7228
7319
  const sc = await requireSession(ctx, args.sessionId);
7229
7320
  if ("err" in sc) return {
@@ -7300,10 +7391,23 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
7300
7391
  };
7301
7392
  try {
7302
7393
  const target = await resolveTarget(ctx, sc.session, args.path);
7303
- 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));
7304
7400
  await afterManualSave(ctx, registry, sc, args.path);
7305
- return { ok: true };
7401
+ return {
7402
+ ok: true,
7403
+ rev: outcome?.version ? String(outcome.version) : rev ?? void 0
7404
+ };
7306
7405
  } catch (error) {
7406
+ if (isStaleVersion(error)) return {
7407
+ ok: false,
7408
+ conflict: true,
7409
+ error: "文件已被外部修改,请先重新加载(或用编辑器内容覆盖)"
7410
+ };
7307
7411
  return {
7308
7412
  ok: false,
7309
7413
  error: "保存失败:" + String(error)
@@ -8231,8 +8335,8 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
8231
8335
  * 统一入口:按方法分发到 handler 表。
8232
8336
  * @author ddj 2026年08月20号 / 2026年08月26号
8233
8337
  */
8234
- async function handleRpc(ctx, registry, method, args, searcher = newSearcher(ctx), contentSearcher = newContentSearcher(ctx), lspHandlers, aiHandlers) {
8235
- 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];
8236
8340
  if (!handler) return {
8237
8341
  ok: false,
8238
8342
  error: "未知方法: " + String(method)
@@ -11665,6 +11769,118 @@ function createAiRpc(deps) {
11665
11769
  })
11666
11770
  } };
11667
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
+ }
11668
11884
  //#endregion
11669
11885
  //#region src/index.ts
11670
11886
  /**
@@ -11721,6 +11937,8 @@ function apply(ctx, config) {
11721
11937
  ctx,
11722
11938
  settings: openSettings
11723
11939
  }).handlers;
11940
+ /** 文件磁盘新鲜度观察器(客户端轮询 edrv.versions;变化时顺带失效目录树缓存)。 */
11941
+ const fileVersions = createFileVersions(ctx);
11724
11942
  ctx.on("tools/result", (exec, result) => {
11725
11943
  captureToolResult(ctx, registry, exec, result);
11726
11944
  });
@@ -11736,7 +11954,7 @@ function apply(ctx, config) {
11736
11954
  const sid = session?.id;
11737
11955
  if (typeof sid === "string") lspRpc.disposeSession(sid);
11738
11956
  });
11739
- 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));
11740
11958
  installIsolation(ctx);
11741
11959
  ctx.effect(() => shellMenuLifecycle(ctx));
11742
11960
  sweepTreeCache();
@@ -11745,6 +11963,7 @@ function apply(ctx, config) {
11745
11963
  lspManager.disposeAll().catch(() => {});
11746
11964
  disposeAllServers();
11747
11965
  });
11966
+ ctx.effect(() => () => fileVersions.dispose());
11748
11967
  hookExitReclaim();
11749
11968
  log.info("编辑差异审查已装配(/edrv/rpc 路由就绪,项目 MCP 隔离已启用,语言服务器 LSP 已接入,规则注入" + (rulesInstalled ? "已接入" : "未接入") + ",技能组" + (skillsDispatched ? "装配中" : "未装配") + ")");
11750
11969
  }