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/README.md +22 -1
- package/lib/client.js +729 -44
- package/lib/client.js.map +1 -1
- package/lib/index.js +261 -33
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/diffDock.ts +12 -0
- package/src/client/events.ts +11 -0
- package/src/client/sidebar/panels/FileExplorer.ts +47 -0
- package/src/client/ui/DiffBox.ts +5 -3
- package/src/client/ui/EditorView.ts +317 -41
- package/src/client/ui/useFileWatch.ts +214 -0
- package/src/client/watchDecision.ts +274 -0
- package/src/compat.ts +1 -1
- package/src/dshVersion.ts +42 -10
- package/src/fileOpenSettings.ts +26 -2
- package/src/fileVersions.ts +167 -0
- package/src/index.ts +6 -1
- package/src/revert.ts +4 -2
- package/src/rpc.ts +62 -12
- package/src/shared/rpc.ts +25 -3
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([
|
|
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
|
|
756
|
-
for (const
|
|
757
|
-
|
|
758
|
-
const
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
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
|
-
}
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
7018
|
-
|
|
7019
|
-
|
|
7020
|
-
|
|
7021
|
-
|
|
7022
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
}
|