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 +607 -88
- package/lib/index.js.map +1 -1
- package/package.json +4 -1
- package/src/index.ts +4 -1
- package/src/rpc.ts +10 -24
- package/src/search/fallback.ts +76 -0
- package/src/search/orchestrator.ts +276 -0
- package/src/search/query.ts +62 -0
- package/src/search/ranker.ts +48 -0
- package/src/search/ripgrep.ts +144 -0
- package/src/search/types.ts +47 -0
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/
|
|
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
|
-
*
|
|
909
|
-
*
|
|
910
|
-
* @
|
|
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
|
-
* @
|
|
914
|
-
* @returns 绝对路径清单;不可用(无 subprocess/fs)或扫描失败返回 null
|
|
993
|
+
* @param session 当前会话
|
|
994
|
+
* @returns subprocess 可访问的根目录
|
|
915
995
|
*/
|
|
916
|
-
async function
|
|
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 (!
|
|
998
|
+
if (!fs) throw new Error("缺少 fs");
|
|
922
999
|
const policy = policyOf(ctx, session);
|
|
923
|
-
|
|
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
|
-
|
|
926
|
-
root = fs.processPath(rootTarget);
|
|
1065
|
+
outcome = await handle.done;
|
|
927
1066
|
} catch (error) {
|
|
928
|
-
|
|
1067
|
+
throw new Error("ripgrep 启动失败:" + String(error));
|
|
929
1068
|
}
|
|
930
|
-
const
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
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
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
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
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
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
|
-
|
|
976
|
-
|
|
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
|
|
1872
|
-
const
|
|
1873
|
-
|
|
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
|
-
|
|
1892
|
-
|
|
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
|
}
|