dsh-vscode-mode 0.1.12 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -1,6 +1,8 @@
1
+ import { createRequire } from "node:module";
1
2
  import { readFile } from "node:fs/promises";
2
3
  import { dirname, extname, join, normalize, resolve, sep } from "node:path";
3
4
  import { fileURLToPath } from "node:url";
5
+ import { existsSync } from "node:fs";
4
6
  import { createHash } from "node:crypto";
5
7
  //#region src/shared/rpc.ts
6
8
  /** webServer 精确路由。 */
@@ -898,82 +900,600 @@ async function captureToolResult(ctx, registry, exec, result) {
898
900
  }
899
901
  }
900
902
  //#endregion
901
- //#region src/workspace.ts
902
- /** 文件清单缓存 TTL 与扫描上限(避免大工作区反复全量扫描)。 */
903
- const FILE_INDEX_TTL = 6e4;
904
- const SCAN_CAP = 6e3;
905
- /** cwd → { at, files }(随会话销毁清理)。 */
906
- const fileIndex = /* @__PURE__ */ new Map();
903
+ //#region src/search/query.ts
907
904
  /**
908
- * 扫描工作区文件清单(快速打开/搜索用):subprocess(Windows powershell / POSIX find),
909
- * 排除常见重目录,容量上限 SCAN_CAP;结果缓存(TTL FILE_INDEX_TTL)。失败返回 null。
910
- * @author ddj 2026年08月20号
905
+ * 规范化用户搜索文本。
906
+ * @author ddj 2026年08月24号
907
+ * @param raw 原始搜索文本
908
+ * @returns 用于比较和缓存的 query
909
+ */
910
+ function prepareQuery(raw) {
911
+ const value = String(raw ?? "").trim();
912
+ return {
913
+ raw: value,
914
+ text: value.replaceAll("\\", "/").toLocaleLowerCase("en-US")
915
+ };
916
+ }
917
+ /**
918
+ * 归一化路径分隔符并移除无意义前缀。
919
+ * @author ddj 2026年08月24号
920
+ * @param value 原始路径
921
+ * @returns 比较用路径
922
+ */
923
+ function pathText(value) {
924
+ return value.replaceAll("\\", "/").replace(/^\.\//, "");
925
+ }
926
+ /**
927
+ * 生成不区分大小写的路径 key。
928
+ * @author ddj 2026年08月24号
929
+ * @param value 原始路径
930
+ * @returns 去重 key
931
+ */
932
+ function pathKey(value) {
933
+ return pathText(value).toLocaleLowerCase("en-US");
934
+ }
935
+ /**
936
+ * 判断路径是否包含连续 query。
937
+ * @author ddj 2026年08月24号
938
+ * @param value 文件路径
939
+ * @param query 已规范化 query
940
+ * @returns 是否命中
941
+ */
942
+ function pathMatch(value, query) {
943
+ return pathText(value).toLocaleLowerCase("en-US").includes(query.text);
944
+ }
945
+ //#endregion
946
+ //#region src/search/ripgrep.ts
947
+ /**
948
+ * dsh-vscode-mode host — ripgrep 文件发现 provider。
949
+ * 通过独立 argv 调用打包 rg,避免先构建巨型全量文件数组。
950
+ * 作者 ddj 2026年08月24号
951
+ */
952
+ const STDOUT_CAP = 4 << 20;
953
+ const STDERR_CAP = 65536;
954
+ const GRACE_MS = 2e4;
955
+ const EXCLUDES = [
956
+ "node_modules",
957
+ ".git",
958
+ ".tmp",
959
+ ".cache",
960
+ "dist",
961
+ "build",
962
+ "vendor",
963
+ "coverage",
964
+ "__pycache__"
965
+ ];
966
+ /**
967
+ * 将 query 转成 rg 的字面 glob。
968
+ * @author ddj 2026年08月24号
969
+ * @param query 已规范化 query
970
+ * @returns 安全 glob
971
+ */
972
+ function queryGlob(query) {
973
+ return "**/*" + pathText(query).replace(/[\*?\[\]{}!]/g, (char) => "\\" + char) + "*";
974
+ }
975
+ /**
976
+ * 返回当前平台的 rg 可执行文件路径,不让可选依赖影响模块加载。
977
+ * @author ddj 2026年08月24号
978
+ * @returns rg 路径或 null
979
+ */
980
+ function ripgrepPath() {
981
+ try {
982
+ const ripgrep = createRequire(import.meta.url)("@vscode/ripgrep");
983
+ if (typeof ripgrep.rgPath !== "string" || !existsSync(ripgrep.rgPath)) return null;
984
+ return join(ripgrep.rgPath);
985
+ } catch (error) {
986
+ return null;
987
+ }
988
+ }
989
+ /**
990
+ * 解析会话搜索根目录,并转换到 subprocess 执行世界。
991
+ * @author ddj 2026年08月24号
911
992
  * @param ctx DSH 上下文
912
- * @param session 会话(取工作区根)
913
- * @param cwd 工作区目录
914
- * @returns 绝对路径清单;不可用(无 subprocess/fs)或扫描失败返回 null
993
+ * @param session 当前会话
994
+ * @returns subprocess 可访问的根目录
915
995
  */
916
- async function listWorkspaceFiles(ctx, session, cwd) {
917
- const cached = fileIndex.get(cwd);
918
- if (cached && Date.now() - cached.at < FILE_INDEX_TTL) return cached.files;
919
- const sub = ctx.get("subprocess");
996
+ async function searchRoot(ctx, session) {
920
997
  const fs = ctx.get("fs");
921
- if (!sub || !fs) return null;
998
+ if (!fs) throw new Error("缺少 fs");
922
999
  const policy = policyOf(ctx, session);
923
- let root;
1000
+ const cwd = session?.header?.cwd;
1001
+ const rootTarget = await fs.resolve(policy?.workspaceRoot ?? ".", cwd ? { cwd } : {});
1002
+ return fs.processPath(rootTarget);
1003
+ }
1004
+ /**
1005
+ * 读取收集输出并解析为路径候选。
1006
+ * @author ddj 2026年08月24号
1007
+ * @param handle subprocess handle
1008
+ * @param maxResults 候选上限
1009
+ * @returns 路径与完整性状态
1010
+ */
1011
+ function parseOutput(handle, maxResults) {
1012
+ const reader = handle.collected?.stdout;
1013
+ if (!reader) return {
1014
+ files: [],
1015
+ truncated: false,
1016
+ complete: false
1017
+ };
1018
+ const output = reader.readFrom(0);
1019
+ const values = output.text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
1020
+ const unique = [...new Set(values)];
1021
+ const truncated = Boolean(output.lossy) || unique.length > maxResults;
1022
+ return {
1023
+ files: unique.slice(0, maxResults).map(pathText),
1024
+ truncated,
1025
+ complete: !truncated
1026
+ };
1027
+ }
1028
+ /**
1029
+ * 使用打包 ripgrep 搜索工作区文件。
1030
+ * @author ddj 2026年08月24号
1031
+ * @param input provider 输入
1032
+ * @returns 有界搜索结果
1033
+ */
1034
+ async function searchRipgrep(input) {
1035
+ const sub = input.ctx.get("subprocess");
1036
+ if (!sub) throw new Error("缺少 subprocess");
1037
+ const binary = ripgrepPath();
1038
+ if (!binary) throw new Error("ripgrep 不可用");
1039
+ const root = input.root ?? await searchRoot(input.ctx, input.session);
1040
+ const argv = [
1041
+ binary,
1042
+ "--no-config",
1043
+ "--files",
1044
+ "--hidden",
1045
+ "--no-ignore",
1046
+ "--glob-case-insensitive",
1047
+ "--glob",
1048
+ queryGlob(prepareQuery(input.query).text)
1049
+ ];
1050
+ for (const excluded of EXCLUDES) argv.push("--glob", "!**/" + excluded + "/**");
1051
+ argv.push("--", root);
1052
+ const handle = sub.spawn({
1053
+ argv,
1054
+ cwd: root,
1055
+ stdio: {
1056
+ stdin: "ignore",
1057
+ stdout: { maxBytes: STDOUT_CAP },
1058
+ stderr: { maxBytes: STDERR_CAP }
1059
+ },
1060
+ graceMs: GRACE_MS,
1061
+ signal: input.signal
1062
+ });
1063
+ let outcome;
924
1064
  try {
925
- const rootTarget = await fs.resolve(policy?.workspaceRoot ?? ".", {});
926
- root = fs.processPath(rootTarget);
1065
+ outcome = await handle.done;
927
1066
  } catch (error) {
928
- root = cwd;
1067
+ throw new Error("ripgrep 启动失败:" + String(error));
929
1068
  }
930
- const lines = [];
931
- const run = async (argv) => {
932
- const handle = sub.spawn({
933
- argv,
934
- stdio: {
935
- stdout: { maxBytes: 8 << 20 },
936
- stderr: { maxBytes: 65536 },
937
- stdin: "ignore"
938
- },
939
- graceMs: 2e4
940
- });
941
- const outcome = await handle.done;
942
- const code = outcome?.exitCode ?? outcome?.code;
943
- if (code !== 0) throw new Error("scan exit " + code);
944
- const text = handle.collected?.stdout?.readFrom(0).text ?? "";
945
- for (const ln of text.split(/\r?\n/)) {
946
- const s = ln.trim();
947
- if (s) lines.push(s);
1069
+ const code = outcome.exitCode ?? outcome.code;
1070
+ if (code !== 0 && code !== 1) throw new Error("ripgrep 退出码:" + String(code));
1071
+ const parsed = parseOutput(handle, input.maxResults);
1072
+ if (!handle.collected?.stdout) throw new Error("ripgrep stdout 不可用");
1073
+ return {
1074
+ ...parsed,
1075
+ source: "ripgrep"
1076
+ };
1077
+ }
1078
+ /**
1079
+ * 构造 ripgrep provider。
1080
+ * @author ddj 2026年08月24号
1081
+ * @returns provider 实例
1082
+ */
1083
+ function newRgProvider() {
1084
+ return { search: searchRipgrep };
1085
+ }
1086
+ //#endregion
1087
+ //#region src/search/fallback.ts
1088
+ const RESULT_CAP = 500;
1089
+ const EXCLUDED = /* @__PURE__ */ new Set([
1090
+ "node_modules",
1091
+ ".git",
1092
+ ".tmp",
1093
+ ".cache",
1094
+ "dist",
1095
+ "build",
1096
+ "vendor",
1097
+ "coverage",
1098
+ "__pycache__"
1099
+ ]);
1100
+ /**
1101
+ * 在一个目录树中递归查找匹配文件。
1102
+ * @author ddj 2026年08月24号
1103
+ * @param fs 文件系统能力
1104
+ * @param target 当前目录目标
1105
+ * @param input 搜索输入
1106
+ * @param files 已保留候选
1107
+ * @param signal 取消信号
1108
+ * @returns 是否因达到上限而截断
1109
+ */
1110
+ async function walk(fs, target, input, files, signal) {
1111
+ if (signal.aborted) throw new DOMException("搜索已取消", "AbortError");
1112
+ const entries = await fs.listDir(target, signal);
1113
+ for (const entry of entries) {
1114
+ if (signal.aborted) throw new DOMException("搜索已取消", "AbortError");
1115
+ if (entry.type === "directory") {
1116
+ if (EXCLUDED.has(entry.name)) continue;
1117
+ if (await walk(fs, entry.target, input, files, signal)) return true;
1118
+ continue;
948
1119
  }
1120
+ if (entry.type !== "file") continue;
1121
+ const path = pathText(entry.target.displayPath ?? entry.name);
1122
+ if (!pathMatch(path, {
1123
+ raw: input.query,
1124
+ text: pathText(input.query).toLocaleLowerCase("en-US")
1125
+ })) continue;
1126
+ files.push(path);
1127
+ if (files.length >= Math.min(input.maxResults, RESULT_CAP)) return true;
1128
+ }
1129
+ return false;
1130
+ }
1131
+ /**
1132
+ * 使用 ctx.fs.listDir 搜索文件。
1133
+ * @author ddj 2026年08月24号
1134
+ * @param input provider 输入
1135
+ * @returns 有界 fallback 结果
1136
+ */
1137
+ async function searchFallback(input) {
1138
+ const fs = input.ctx.get("fs");
1139
+ if (!fs) throw new Error("缺少 fs");
1140
+ const policy = policyOf(input.ctx, input.session);
1141
+ const cwd = input.session?.header?.cwd;
1142
+ const rootTarget = await fs.resolve(policy?.workspaceRoot ?? ".", cwd ? {
1143
+ cwd,
1144
+ signal: input.signal
1145
+ } : { signal: input.signal });
1146
+ const signal = input.signal ?? new AbortController().signal;
1147
+ const files = [];
1148
+ const truncated = await walk(fs, rootTarget, input, files, signal);
1149
+ return {
1150
+ files,
1151
+ truncated,
1152
+ complete: !truncated,
1153
+ source: "fallback"
949
1154
  };
950
- try {
951
- if (process.platform === "win32") await run([
952
- "powershell",
953
- "-NoProfile",
954
- "-NonInteractive",
955
- "-Command",
956
- "Get-ChildItem -LiteralPath \"" + String(root).replace(/"/g, "\"\"") + "\" -Recurse -File -ErrorAction SilentlyContinue | Where-Object { $_.FullName -notmatch '(node_modules|\\.git|\\.tmp|\\.cache|dist|build|vendor|coverage|__pycache__)' } | Select-Object -First 6000 | ForEach-Object { $_.FullName }"
957
- ]);
958
- else await run([
959
- "/bin/sh",
960
- "-c",
961
- "find " + JSON.stringify(root) + " -type f -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/.tmp/*' -not -path '*/.cache/*' -not -path '*/dist/*' -not -path '*/build/*' -not -path '*/vendor/*' -not -path '*/coverage/*' 2>/dev/null | head -n 6000"
962
- ]);
963
- } catch (error) {
964
- console.error("edrv scan failed", error);
965
- return null;
1155
+ }
1156
+ /**
1157
+ * 构造 fallback provider。
1158
+ * @author ddj 2026年08月24号
1159
+ * @returns provider 实例
1160
+ */
1161
+ function newFallback() {
1162
+ return { search: searchFallback };
1163
+ }
1164
+ //#endregion
1165
+ //#region src/search/ranker.ts
1166
+ /**
1167
+ * 生成搜索候选。
1168
+ * @author ddj 2026年08月24号
1169
+ * @param path 文件路径
1170
+ * @param source 候选来源
1171
+ * @returns 内部候选
1172
+ */
1173
+ function candidateOf(path, source) {
1174
+ const normalizedPath = pathText(path);
1175
+ return {
1176
+ path,
1177
+ basename: normalizedPath.split("/").pop() ?? normalizedPath,
1178
+ normalizedPath: normalizedPath.toLocaleLowerCase("en-US"),
1179
+ source
1180
+ };
1181
+ }
1182
+ /**
1183
+ * 对连续匹配候选进行排序。
1184
+ * @author ddj 2026年08月24号
1185
+ * @param candidates 候选列表
1186
+ * @param query 已规范化 query
1187
+ * @returns 排序后的候选
1188
+ */
1189
+ function rankCandidates(candidates, query) {
1190
+ const text = query.text;
1191
+ const score = (candidate) => {
1192
+ const base = candidate.basename.toLocaleLowerCase("en-US");
1193
+ if (base.startsWith(text)) return 0;
1194
+ if (base.includes(text)) return 1;
1195
+ if (candidate.normalizedPath.includes(text)) return 2;
1196
+ return 3;
1197
+ };
1198
+ return [...candidates].sort((left, right) => score(left) - score(right) || left.normalizedPath.localeCompare(right.normalizedPath, "en-US"));
1199
+ }
1200
+ //#endregion
1201
+ //#region src/search/orchestrator.ts
1202
+ const CACHE_TTL = 6e4;
1203
+ const CACHE_LIMIT = 100;
1204
+ const RESULT_LIMIT = 50;
1205
+ const PROVIDER_VERSION = "ripgrep-v1";
1206
+ const POLICY_VERSION = "search-policy-v1";
1207
+ /**
1208
+ * 短期有界搜索缓存。
1209
+ * @author ddj 2026年08月24号
1210
+ * @returns 缓存对象
1211
+ */
1212
+ var SearchCache = class {
1213
+ entries = /* @__PURE__ */ new Map();
1214
+ /**
1215
+ * 读取未过期条目。
1216
+ * @author ddj 2026年08月24号
1217
+ * @param key 缓存 key
1218
+ * @param now 当前时间
1219
+ * @returns 缓存结果或 undefined
1220
+ */
1221
+ get(key, now = Date.now()) {
1222
+ const entry = this.entries.get(key);
1223
+ if (!entry) return void 0;
1224
+ if (now - entry.at >= CACHE_TTL) {
1225
+ this.entries.delete(key);
1226
+ return;
1227
+ }
1228
+ this.entries.delete(key);
1229
+ this.entries.set(key, entry);
1230
+ return {
1231
+ ...entry.result,
1232
+ files: [...entry.result.files]
1233
+ };
966
1234
  }
967
- const files = lines.slice(0, SCAN_CAP);
968
- if (files.length) fileIndex.set(cwd, {
969
- at: Date.now(),
970
- files
971
- });
972
- return files;
1235
+ /**
1236
+ * 写入成功 provider 结果。
1237
+ * @author ddj 2026年08月24号
1238
+ * @param key 缓存 key
1239
+ * @param result provider 结果
1240
+ * @param now 当前时间
1241
+ */
1242
+ set(key, result, now = Date.now()) {
1243
+ this.entries.delete(key);
1244
+ this.entries.set(key, {
1245
+ at: now,
1246
+ result: {
1247
+ ...result,
1248
+ files: [...result.files]
1249
+ }
1250
+ });
1251
+ while (this.entries.size > CACHE_LIMIT) this.entries.delete(this.entries.keys().next().value);
1252
+ }
1253
+ /**
1254
+ * 清理指定根目录的缓存。
1255
+ * @author ddj 2026年08月24号
1256
+ * @param root 工作区根目录
1257
+ */
1258
+ clearRoot(root) {
1259
+ for (const key of this.entries.keys()) if (key.startsWith(root + "|")) this.entries.delete(key);
1260
+ }
1261
+ /**
1262
+ * 清空全部缓存。
1263
+ * @author ddj 2026年08月24号
1264
+ */
1265
+ clear() {
1266
+ this.entries.clear();
1267
+ }
1268
+ };
1269
+ /**
1270
+ * 获取 policy 版本,未知策略使用固定插件版本。
1271
+ * @author ddj 2026年08月24号
1272
+ * @param ctx DSH 上下文
1273
+ * @param session 当前会话
1274
+ * @returns 稳定版本文本
1275
+ */
1276
+ function policyVersion(ctx, session) {
1277
+ const policy = policyOf(ctx, session);
1278
+ const version = policy?.version ?? policy?.policyVersion;
1279
+ return typeof version === "string" || typeof version === "number" ? String(version) : POLICY_VERSION;
973
1280
  }
974
- /** 会话销毁时清理文件索引缓存。 */
975
- function dropFileIndex(cwd) {
976
- fileIndex.delete(cwd);
1281
+ /**
1282
+ * 将 root 内绝对路径统一成相对显示路径。
1283
+ * @author ddj 2026年08月24号
1284
+ * @param value 原始路径
1285
+ * @param root 搜索根
1286
+ * @returns 兼容 edrv.read 的路径
1287
+ */
1288
+ function displayPath(value, root) {
1289
+ const path = pathText(value);
1290
+ const base = pathText(root).replace(/\/$/, "");
1291
+ const lowerPath = path.toLocaleLowerCase("en-US");
1292
+ const lowerBase = base.toLocaleLowerCase("en-US");
1293
+ if (lowerPath === lowerBase) return ".";
1294
+ if (lowerPath.startsWith(lowerBase + "/")) return path.slice(base.length + 1);
1295
+ return path;
1296
+ }
1297
+ /**
1298
+ * 为显示路径生成跨绝对/相对形式的去重 key。
1299
+ * @author ddj 2026年08月24号
1300
+ * @param value 原始路径
1301
+ * @param root 搜索根
1302
+ * @returns 去重 key
1303
+ */
1304
+ function displayKey(value, root) {
1305
+ return pathKey(displayPath(value, root));
1306
+ }
1307
+ /**
1308
+ * 合并 provider 与 active diff 候选并去重。
1309
+ * @author ddj 2026年08月24号
1310
+ * @param result provider 结果
1311
+ * @param activePaths 活跃差异路径
1312
+ * @param root 搜索根
1313
+ * @param query 已规范化 query
1314
+ * @returns 候选列表
1315
+ */
1316
+ function mergeCandidates(result, activePaths, root, query) {
1317
+ const candidates = /* @__PURE__ */ new Map();
1318
+ for (const path of result.files) {
1319
+ const shown = displayPath(path, root);
1320
+ if (pathMatch(shown, query)) candidates.set(displayKey(path, root), candidateOf(shown, "workspace"));
1321
+ }
1322
+ for (const path of activePaths) {
1323
+ const shown = displayPath(path, root);
1324
+ if (!pathMatch(shown, query)) continue;
1325
+ const key = displayKey(path, root);
1326
+ if (!candidates.has(key)) candidates.set(key, candidateOf(shown, "active-diff"));
1327
+ }
1328
+ return [...candidates.values()];
1329
+ }
1330
+ /**
1331
+ * 搜索编排器。
1332
+ * @author ddj 2026年08月24号
1333
+ */
1334
+ var SearchOrchestrator = class {
1335
+ ctx;
1336
+ cache = new SearchCache();
1337
+ inflight = /* @__PURE__ */ new Map();
1338
+ rootsByCwd = /* @__PURE__ */ new Map();
1339
+ provider;
1340
+ fallback;
1341
+ ranker;
1342
+ /**
1343
+ * 创建搜索编排器。
1344
+ * @author ddj 2026年08月24号
1345
+ * @param ctx DSH 上下文
1346
+ * @param provider 主 provider,可替换测试
1347
+ * @param fallback 降级 provider,可替换测试
1348
+ * @param ranker 排序器,可替换测试
1349
+ */
1350
+ constructor(ctx, provider = newRgProvider(), fallback = newFallback(), ranker = { rank: rankCandidates }) {
1351
+ this.ctx = ctx;
1352
+ this.provider = provider;
1353
+ this.fallback = fallback;
1354
+ this.ranker = ranker;
1355
+ }
1356
+ /**
1357
+ * 执行一次工作区搜索。
1358
+ * @author ddj 2026年08月24号
1359
+ * @param request 搜索请求
1360
+ * @returns 旧 RPC 响应字段
1361
+ */
1362
+ async search(request) {
1363
+ const query = prepareQuery(request.query);
1364
+ if (query.text.length < 2) return {
1365
+ files: [],
1366
+ truncated: false
1367
+ };
1368
+ let root;
1369
+ try {
1370
+ root = await searchRoot(this.ctx, request.session);
1371
+ } catch (error) {
1372
+ return this.rankActive(request.activePaths, request.cwd, query);
1373
+ }
1374
+ const rootKey = pathText(root);
1375
+ const roots = this.rootsByCwd.get(request.cwd) ?? /* @__PURE__ */ new Set();
1376
+ roots.add(rootKey);
1377
+ this.rootsByCwd.set(request.cwd, roots);
1378
+ const key = [
1379
+ rootKey,
1380
+ query.text,
1381
+ policyVersion(this.ctx, request.session),
1382
+ PROVIDER_VERSION
1383
+ ].join("|");
1384
+ const cached = this.cache.get(key);
1385
+ if (cached) return this.finish(cached, request.activePaths, root, query);
1386
+ this.inflight.get(root)?.abort();
1387
+ const controller = new AbortController();
1388
+ this.inflight.set(root, controller);
1389
+ try {
1390
+ let result;
1391
+ let providerOk = true;
1392
+ try {
1393
+ result = await this.provider.search({
1394
+ ctx: this.ctx,
1395
+ session: request.session,
1396
+ cwd: request.cwd,
1397
+ query: query.raw,
1398
+ maxResults: 500,
1399
+ signal: controller.signal,
1400
+ root
1401
+ });
1402
+ } catch (error) {
1403
+ providerOk = false;
1404
+ if (controller.signal.aborted) return {
1405
+ files: [],
1406
+ truncated: false
1407
+ };
1408
+ try {
1409
+ result = await this.fallback.search({
1410
+ ctx: this.ctx,
1411
+ session: request.session,
1412
+ cwd: request.cwd,
1413
+ query: query.raw,
1414
+ maxResults: 500,
1415
+ signal: controller.signal,
1416
+ root
1417
+ });
1418
+ } catch (fallbackError) {
1419
+ return this.rankActive(request.activePaths, root, query);
1420
+ }
1421
+ }
1422
+ if (controller.signal.aborted) return {
1423
+ files: [],
1424
+ truncated: false
1425
+ };
1426
+ if (providerOk) this.cache.set(key, result);
1427
+ return this.finish(result, request.activePaths, root, query);
1428
+ } finally {
1429
+ if (this.inflight.get(root) === controller) this.inflight.delete(root);
1430
+ }
1431
+ }
1432
+ /**
1433
+ * 清理会话对应根目录的缓存和在途搜索。
1434
+ * @author ddj 2026年08月24号
1435
+ * @param cwd 会话工作区
1436
+ */
1437
+ dispose(cwd) {
1438
+ const roots = this.rootsByCwd.get(cwd) ?? /* @__PURE__ */ new Set([cwd]);
1439
+ for (const root of roots) {
1440
+ this.inflight.get(root)?.abort();
1441
+ this.inflight.delete(root);
1442
+ this.cache.clearRoot(root);
1443
+ }
1444
+ this.rootsByCwd.delete(cwd);
1445
+ }
1446
+ /**
1447
+ * 清理全部状态。
1448
+ * @author ddj 2026年08月24号
1449
+ */
1450
+ disposeAll() {
1451
+ for (const controller of this.inflight.values()) controller.abort();
1452
+ this.inflight.clear();
1453
+ this.rootsByCwd.clear();
1454
+ this.cache.clear();
1455
+ }
1456
+ /**
1457
+ * 将 provider 结果与 active diff 合并、排序和截断。
1458
+ * @author ddj 2026年08月24号
1459
+ * @param result provider 结果
1460
+ * @param activePaths active diff 路径
1461
+ * @param root 搜索根
1462
+ * @param query 已规范化 query
1463
+ * @returns 旧 RPC 响应字段
1464
+ */
1465
+ finish(result, activePaths, root, query) {
1466
+ const candidates = this.ranker.rank(mergeCandidates(result, activePaths, root, query), query);
1467
+ return {
1468
+ files: candidates.slice(0, RESULT_LIMIT).map((candidate) => candidate.path),
1469
+ truncated: result.truncated || candidates.length > RESULT_LIMIT
1470
+ };
1471
+ }
1472
+ /**
1473
+ * provider 失败时仅保留 active diff 命中。
1474
+ * @author ddj 2026年08月24号
1475
+ * @param paths active diff 路径
1476
+ * @param root 搜索根
1477
+ * @param query 已规范化 query
1478
+ * @returns 旧 RPC 响应字段
1479
+ */
1480
+ rankActive(paths, root, query) {
1481
+ return this.finish({
1482
+ files: [],
1483
+ truncated: false,
1484
+ complete: true,
1485
+ source: "active-diff"
1486
+ }, paths, root, query);
1487
+ }
1488
+ };
1489
+ /**
1490
+ * 创建默认编排器。
1491
+ * @author ddj 2026年08月24号
1492
+ * @param ctx DSH 上下文
1493
+ * @returns 搜索编排器
1494
+ */
1495
+ function newSearcher(ctx) {
1496
+ return new SearchOrchestrator(ctx);
977
1497
  }
978
1498
  //#endregion
979
1499
  //#region src/revert.ts
@@ -1540,7 +2060,7 @@ async function requireSession(ctx, sessionId) {
1540
2060
  };
1541
2061
  }
1542
2062
  /** 各方法 handler 表(类型由 shared/rpc 的 RpcHandlerMap 约束)。 */
1543
- function buildHandlers(ctx, registry) {
2063
+ function buildHandlers(ctx, registry, searcher = newSearcher(ctx)) {
1544
2064
  return {
1545
2065
  "edrv.list": async (args) => {
1546
2066
  const sc = await requireSession(ctx, args.sessionId);
@@ -1868,28 +2388,17 @@ function buildHandlers(ctx, registry) {
1868
2388
  ok: false,
1869
2389
  error: sc.err
1870
2390
  };
1871
- const query = String(args.query ?? "").trim().toLowerCase();
1872
- const files = [];
1873
- if (query.length >= 2) {
1874
- const bucket = await bucketOf(registry, ctx, sc.cwd);
1875
- const pool = /* @__PURE__ */ new Set();
1876
- for (const rec of bucket.values()) if (rec.path) pool.add(rec.path);
1877
- const listed = await listWorkspaceFiles(ctx, sc.session, sc.cwd);
1878
- if (listed) for (const f of listed) pool.add(f);
1879
- const score = (p) => {
1880
- const pl = p.toLowerCase();
1881
- if ((pl.split(/[\\/]/).pop() || "").includes(query)) return 0;
1882
- if (pl.includes(query)) return 1;
1883
- return -1;
1884
- };
1885
- const hit = [...pool].filter((p) => score(p) >= 0);
1886
- hit.sort((a, b) => score(a) - score(b) || (a < b ? -1 : 1));
1887
- files.push(...hit.slice(0, 50));
1888
- }
2391
+ const bucket = await bucketOf(registry, ctx, sc.cwd);
2392
+ const activePaths = [];
2393
+ for (const record of bucket.values()) if (record.path) activePaths.push(record.path);
1889
2394
  return {
1890
2395
  ok: true,
1891
- files,
1892
- truncated: files.length >= 50
2396
+ ...await searcher.search({
2397
+ session: sc.session,
2398
+ cwd: sc.cwd,
2399
+ query: args.query,
2400
+ activePaths
2401
+ })
1893
2402
  };
1894
2403
  },
1895
2404
  "mcp.list": async () => ({
@@ -2017,8 +2526,8 @@ function buildHandlers(ctx, registry) {
2017
2526
  * 统一入口:按方法分发到 handler 表。
2018
2527
  * @author ddj 2026年08月20号
2019
2528
  */
2020
- async function handleRpc(ctx, registry, method, args) {
2021
- const handler = buildHandlers(ctx, registry)[method];
2529
+ async function handleRpc(ctx, registry, method, args, searcher = newSearcher(ctx)) {
2530
+ const handler = buildHandlers(ctx, registry, searcher)[method];
2022
2531
  if (!handler) return {
2023
2532
  ok: false,
2024
2533
  error: "未知方法: " + String(method)
@@ -2161,6 +2670,14 @@ function installIsolation(ctx) {
2161
2670
  }, "vscode-mode:mcp-isolation");
2162
2671
  }
2163
2672
  //#endregion
2673
+ //#region src/workspace.ts
2674
+ /** cwd → { at, files }(随会话销毁清理)。 */
2675
+ const fileIndex = /* @__PURE__ */ new Map();
2676
+ /** 会话销毁时清理文件索引缓存。 */
2677
+ function dropFileIndex(cwd) {
2678
+ fileIndex.delete(cwd);
2679
+ }
2680
+ //#endregion
2164
2681
  //#region src/index.ts
2165
2682
  /**
2166
2683
  * @dsh-external 生态 → dsh-vscode-mode:DSH 上的类 VSCode 编码体验(Host 半入口)。
@@ -2187,6 +2704,7 @@ const inject = [
2187
2704
  */
2188
2705
  function apply(ctx, config) {
2189
2706
  const registry = /* @__PURE__ */ new Map();
2707
+ const searcher = newSearcher(ctx);
2190
2708
  ctx.on("tools/result", (exec, result) => {
2191
2709
  captureToolResult(ctx, registry, exec, result);
2192
2710
  });
@@ -2195,9 +2713,10 @@ function apply(ctx, config) {
2195
2713
  if (cwd) {
2196
2714
  registry.delete(cwd);
2197
2715
  dropFileIndex(cwd);
2716
+ searcher.dispose(cwd);
2198
2717
  }
2199
2718
  });
2200
- registerRoutes(ctx, config, (method, args) => handleRpc(ctx, registry, method, args));
2719
+ registerRoutes(ctx, config, (method, args) => handleRpc(ctx, registry, method, args, searcher));
2201
2720
  installIsolation(ctx);
2202
2721
  ctx.logger?.info?.("[dsh-vscode-mode] 编辑差异审查已装配(/edrv/rpc 路由就绪,项目 MCP 隔离已启用)");
2203
2722
  }