mcp-probe-kit 3.0.12 → 3.0.13
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 +76 -20
- package/build/lib/__tests__/gitnexus-bridge.unit.test.js +37 -1
- package/build/lib/gitnexus-bridge.d.ts +35 -2
- package/build/lib/gitnexus-bridge.js +316 -41
- package/build/tools/__tests__/code_insight.unit.test.js +51 -1
- package/build/tools/code_insight.d.ts +31 -1
- package/build/tools/code_insight.js +204 -71
- package/docs/i18n/en.json +20 -0
- package/docs/i18n/ja.json +20 -0
- package/docs/i18n/ko.json +20 -0
- package/docs/i18n/zh-CN.json +20 -0
- package/docs/pages/getting-started.html +71 -10
- package/package.json +1 -1
|
@@ -61,9 +61,9 @@ function isBridgeEnabled() {
|
|
|
61
61
|
}
|
|
62
62
|
return !/^(0|false|no|off)$/i.test(raw.trim());
|
|
63
63
|
}
|
|
64
|
-
function splitArgs(raw) {
|
|
64
|
+
function splitArgs(raw, fallback = DEFAULT_GITNEXUS_ARGS) {
|
|
65
65
|
if (!raw) {
|
|
66
|
-
return [...
|
|
66
|
+
return [...fallback];
|
|
67
67
|
}
|
|
68
68
|
return raw
|
|
69
69
|
.trim()
|
|
@@ -119,14 +119,14 @@ function parseAvailableReposFromError(text) {
|
|
|
119
119
|
.map((item) => item.trim())
|
|
120
120
|
.filter(Boolean);
|
|
121
121
|
}
|
|
122
|
-
function
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
122
|
+
function isGitNexusCliCommand(command) {
|
|
123
|
+
const normalized = path.basename((command || "").trim()).toLowerCase();
|
|
124
|
+
return normalized === "gitnexus"
|
|
125
|
+
|| normalized === "gitnexus.cmd"
|
|
126
|
+
|| normalized === "gitnexus.exe"
|
|
127
|
+
|| normalized === "gitnexus.bat";
|
|
126
128
|
}
|
|
127
|
-
function
|
|
128
|
-
const command = (process.env.MCP_GITNEXUS_COMMAND || "npx").trim() || "npx";
|
|
129
|
-
const bridgeArgs = splitArgs(process.env.MCP_GITNEXUS_ARGS);
|
|
129
|
+
function resolveNpxPackageArgs(bridgeArgs) {
|
|
130
130
|
const flags = [];
|
|
131
131
|
let packageSpec = "gitnexus@latest";
|
|
132
132
|
for (const arg of bridgeArgs) {
|
|
@@ -140,17 +140,58 @@ function resolveGitNexusCliCommand(subcommand) {
|
|
|
140
140
|
packageSpec = arg;
|
|
141
141
|
break;
|
|
142
142
|
}
|
|
143
|
-
return
|
|
143
|
+
return [...flags, packageSpec];
|
|
144
144
|
}
|
|
145
|
-
export function
|
|
145
|
+
export function resolveGitNexusBridgeCommand(env = process.env, platform = process.platform) {
|
|
146
|
+
const explicitCommand = env.MCP_GITNEXUS_COMMAND?.trim();
|
|
147
|
+
if (explicitCommand) {
|
|
148
|
+
const args = splitArgs(env.MCP_GITNEXUS_ARGS, isGitNexusCliCommand(explicitCommand) ? ["mcp"] : DEFAULT_GITNEXUS_ARGS);
|
|
149
|
+
return {
|
|
150
|
+
...resolveSpawnCommand(explicitCommand, args, platform, env),
|
|
151
|
+
strategy: "env",
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const localCli = findExecutablePath("gitnexus", platform, env);
|
|
155
|
+
if (localCli) {
|
|
156
|
+
return {
|
|
157
|
+
command: localCli,
|
|
158
|
+
args: ["mcp"],
|
|
159
|
+
strategy: "local",
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
...resolveSpawnCommand("npx", splitArgs(env.MCP_GITNEXUS_ARGS), platform, env),
|
|
164
|
+
strategy: "npx",
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function resolveGitNexusCliCommand(subcommand, env = process.env, platform = process.platform) {
|
|
168
|
+
const explicitCommand = env.MCP_GITNEXUS_COMMAND?.trim();
|
|
169
|
+
if (explicitCommand) {
|
|
170
|
+
if (isGitNexusCliCommand(explicitCommand)) {
|
|
171
|
+
return resolveSpawnCommand(explicitCommand, [subcommand], platform, env);
|
|
172
|
+
}
|
|
173
|
+
const bridgeArgs = splitArgs(env.MCP_GITNEXUS_ARGS);
|
|
174
|
+
return resolveSpawnCommand(explicitCommand, [...resolveNpxPackageArgs(bridgeArgs), subcommand], platform, env);
|
|
175
|
+
}
|
|
176
|
+
const localCli = findExecutablePath("gitnexus", platform, env);
|
|
177
|
+
if (localCli) {
|
|
178
|
+
return {
|
|
179
|
+
command: localCli,
|
|
180
|
+
args: [subcommand],
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
const bridgeArgs = splitArgs(env.MCP_GITNEXUS_ARGS);
|
|
184
|
+
return resolveSpawnCommand("npx", [...resolveNpxPackageArgs(bridgeArgs), subcommand], platform, env);
|
|
185
|
+
}
|
|
186
|
+
export function resolveExecutableCommand(command, platform = process.platform, env = process.env) {
|
|
146
187
|
const normalized = (command || "").trim();
|
|
147
188
|
if (!normalized) {
|
|
148
|
-
return resolveExecutableCommand("npx", platform);
|
|
189
|
+
return resolveExecutableCommand("npx", platform, env);
|
|
149
190
|
}
|
|
150
191
|
if (path.isAbsolute(normalized) && fs.existsSync(normalized)) {
|
|
151
192
|
return normalized;
|
|
152
193
|
}
|
|
153
|
-
const found = findExecutablePath(normalized, platform);
|
|
194
|
+
const found = findExecutablePath(normalized, platform, env);
|
|
154
195
|
if (found) {
|
|
155
196
|
return found;
|
|
156
197
|
}
|
|
@@ -166,7 +207,7 @@ export function resolveExecutableCommand(command, platform = process.platform) {
|
|
|
166
207
|
}
|
|
167
208
|
return normalized;
|
|
168
209
|
}
|
|
169
|
-
function findExecutablePath(command, platform = process.platform) {
|
|
210
|
+
function findExecutablePath(command, platform = process.platform, env = process.env) {
|
|
170
211
|
const trimmed = (command || "").trim();
|
|
171
212
|
if (!trimmed || path.isAbsolute(trimmed)) {
|
|
172
213
|
return undefined;
|
|
@@ -197,6 +238,7 @@ function findExecutablePath(command, platform = process.platform) {
|
|
|
197
238
|
try {
|
|
198
239
|
const output = execFileSync("where.exe", [trimmed], {
|
|
199
240
|
encoding: "utf-8",
|
|
241
|
+
env,
|
|
200
242
|
stdio: ["ignore", "pipe", "ignore"],
|
|
201
243
|
windowsHide: true,
|
|
202
244
|
}).trim();
|
|
@@ -210,7 +252,7 @@ function findExecutablePath(command, platform = process.platform) {
|
|
|
210
252
|
// fall back to PATH scan
|
|
211
253
|
}
|
|
212
254
|
}
|
|
213
|
-
const pathEntries = (
|
|
255
|
+
const pathEntries = (env.PATH || "")
|
|
214
256
|
.split(path.delimiter)
|
|
215
257
|
.map((entry) => entry.trim())
|
|
216
258
|
.filter(Boolean);
|
|
@@ -232,21 +274,215 @@ function findExecutablePath(command, platform = process.platform) {
|
|
|
232
274
|
}
|
|
233
275
|
if (platform === "win32" && trimmed.toLowerCase() === "git") {
|
|
234
276
|
const commonCandidates = [
|
|
235
|
-
path.join(
|
|
236
|
-
path.join(
|
|
237
|
-
path.join(
|
|
277
|
+
path.join(env["ProgramFiles"] || "C:\\Program Files", "Git", "cmd", "git.exe"),
|
|
278
|
+
path.join(env["ProgramFiles"] || "C:\\Program Files", "Git", "bin", "git.exe"),
|
|
279
|
+
path.join(env["ProgramFiles(x86)"] || "C:\\Program Files (x86)", "Git", "cmd", "git.exe"),
|
|
238
280
|
];
|
|
239
281
|
return preferredPath(commonCandidates);
|
|
240
282
|
}
|
|
241
283
|
return undefined;
|
|
242
284
|
}
|
|
243
|
-
export function resolveSpawnCommand(command, args, platform = process.platform) {
|
|
244
|
-
const executable = resolveExecutableCommand(command, platform);
|
|
285
|
+
export function resolveSpawnCommand(command, args, platform = process.platform, env = process.env) {
|
|
286
|
+
const executable = resolveExecutableCommand(command, platform, env);
|
|
245
287
|
return {
|
|
246
288
|
command: executable,
|
|
247
289
|
args,
|
|
248
290
|
};
|
|
249
291
|
}
|
|
292
|
+
function toRecord(value) {
|
|
293
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
294
|
+
? value
|
|
295
|
+
: undefined;
|
|
296
|
+
}
|
|
297
|
+
function extractStructuredStatus(value) {
|
|
298
|
+
const status = toRecord(value)?.status;
|
|
299
|
+
return typeof status === "string" ? status : undefined;
|
|
300
|
+
}
|
|
301
|
+
function extractStructuredMessage(value) {
|
|
302
|
+
const record = toRecord(value);
|
|
303
|
+
if (!record) {
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
for (const key of ["message", "summary", "explanation", "detail"]) {
|
|
307
|
+
const candidate = record[key];
|
|
308
|
+
if (typeof candidate === "string" && candidate.trim()) {
|
|
309
|
+
return candidate.trim();
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
314
|
+
function extractStructuredCandidates(value) {
|
|
315
|
+
const candidates = toRecord(value)?.candidates;
|
|
316
|
+
if (!Array.isArray(candidates)) {
|
|
317
|
+
return [];
|
|
318
|
+
}
|
|
319
|
+
return candidates
|
|
320
|
+
.filter((item) => item && typeof item === "object" && !Array.isArray(item))
|
|
321
|
+
.map((item) => ({ ...item }));
|
|
322
|
+
}
|
|
323
|
+
function buildAmbiguities(executions) {
|
|
324
|
+
return executions
|
|
325
|
+
.filter((item) => item.ok && item.status === "ambiguous")
|
|
326
|
+
.map((item) => {
|
|
327
|
+
const candidates = extractStructuredCandidates(item.structuredContent);
|
|
328
|
+
const message = extractStructuredMessage(item.structuredContent) || item.text;
|
|
329
|
+
return {
|
|
330
|
+
tool: item.tool,
|
|
331
|
+
message: message ? shorten(message, 220) : undefined,
|
|
332
|
+
candidates,
|
|
333
|
+
};
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
const QUERY_STOP_WORDS = new Set([
|
|
337
|
+
"the", "and", "for", "with", "from", "into", "that", "this", "user", "flow",
|
|
338
|
+
"sign", "code", "project", "module",
|
|
339
|
+
"理解", "分析", "流程", "模块", "项目", "代码", "查询", "相关", "功能",
|
|
340
|
+
]);
|
|
341
|
+
function tokenizeQueryHints(...values) {
|
|
342
|
+
const tokens = new Set();
|
|
343
|
+
for (const value of values) {
|
|
344
|
+
for (const token of (value || "").toLowerCase().split(/[^a-z0-9_\u4e00-\u9fa5]+/)) {
|
|
345
|
+
const normalized = token.trim();
|
|
346
|
+
if (!normalized || QUERY_STOP_WORDS.has(normalized)) {
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (normalized.length < 3 && !/[\u4e00-\u9fa5]/.test(normalized)) {
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
tokens.add(normalized);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return [...tokens];
|
|
356
|
+
}
|
|
357
|
+
function collectStringFields(value, depth = 0, bag = [], currentKey = "") {
|
|
358
|
+
if (depth > 3 || value == null) {
|
|
359
|
+
return bag;
|
|
360
|
+
}
|
|
361
|
+
if (typeof value === "string") {
|
|
362
|
+
const normalized = value.trim().toLowerCase();
|
|
363
|
+
if (normalized) {
|
|
364
|
+
bag.push({ key: currentKey.toLowerCase(), value: normalized });
|
|
365
|
+
}
|
|
366
|
+
return bag;
|
|
367
|
+
}
|
|
368
|
+
if (Array.isArray(value)) {
|
|
369
|
+
for (const item of value.slice(0, 12)) {
|
|
370
|
+
collectStringFields(item, depth + 1, bag, currentKey);
|
|
371
|
+
}
|
|
372
|
+
return bag;
|
|
373
|
+
}
|
|
374
|
+
if (typeof value === "object") {
|
|
375
|
+
for (const [key, item] of Object.entries(value).slice(0, 20)) {
|
|
376
|
+
collectStringFields(item, depth + 1, bag, key);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return bag;
|
|
380
|
+
}
|
|
381
|
+
function scoreQueryCandidate(candidate, terms) {
|
|
382
|
+
const fields = collectStringFields(candidate);
|
|
383
|
+
if (fields.length === 0 || terms.length === 0) {
|
|
384
|
+
return 0;
|
|
385
|
+
}
|
|
386
|
+
let score = 0;
|
|
387
|
+
let matchedTerms = 0;
|
|
388
|
+
for (const term of terms) {
|
|
389
|
+
let matched = false;
|
|
390
|
+
for (const field of fields) {
|
|
391
|
+
if (!field.value.includes(term)) {
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
matched = true;
|
|
395
|
+
if (field.key.includes("name") || field.key.includes("title") || field.key.includes("label")) {
|
|
396
|
+
score += field.value === term ? 24 : 16;
|
|
397
|
+
}
|
|
398
|
+
else if (field.key.includes("path") || field.key.includes("file") || field.key.includes("module")) {
|
|
399
|
+
score += 10;
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
score += 4;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (matched) {
|
|
406
|
+
matchedTerms += 1;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
if (matchedTerms > 0) {
|
|
410
|
+
score += matchedTerms * 5;
|
|
411
|
+
}
|
|
412
|
+
if (matchedTerms === terms.length) {
|
|
413
|
+
score += 12;
|
|
414
|
+
}
|
|
415
|
+
const candidateRecord = toRecord(candidate);
|
|
416
|
+
const priority = candidateRecord?.priority;
|
|
417
|
+
if (typeof priority === "number" && Number.isFinite(priority)) {
|
|
418
|
+
score += priority;
|
|
419
|
+
}
|
|
420
|
+
return score;
|
|
421
|
+
}
|
|
422
|
+
function describeQueryTopMatches(structuredContent) {
|
|
423
|
+
const processes = toRecord(structuredContent)?.processes;
|
|
424
|
+
if (!Array.isArray(processes) || processes.length === 0) {
|
|
425
|
+
return undefined;
|
|
426
|
+
}
|
|
427
|
+
const labels = processes
|
|
428
|
+
.slice(0, 3)
|
|
429
|
+
.map((item) => {
|
|
430
|
+
const record = toRecord(item);
|
|
431
|
+
return [
|
|
432
|
+
record?.heuristicLabel,
|
|
433
|
+
record?.title,
|
|
434
|
+
record?.name,
|
|
435
|
+
record?.processName,
|
|
436
|
+
].find((value) => typeof value === "string" && value.trim());
|
|
437
|
+
})
|
|
438
|
+
.filter((value) => typeof value === "string" && value.trim().length > 0);
|
|
439
|
+
return labels.length > 0 ? `Top matches: ${labels.join(" | ")}` : undefined;
|
|
440
|
+
}
|
|
441
|
+
export function rerankQueryStructuredContent(structuredContent, hints) {
|
|
442
|
+
const record = toRecord(structuredContent);
|
|
443
|
+
if (!record) {
|
|
444
|
+
return { structuredContent, changed: false };
|
|
445
|
+
}
|
|
446
|
+
const terms = tokenizeQueryHints(hints.query, hints.goal, hints.taskContext);
|
|
447
|
+
if (terms.length === 0) {
|
|
448
|
+
return { structuredContent, changed: false };
|
|
449
|
+
}
|
|
450
|
+
let changed = false;
|
|
451
|
+
const next = { ...record };
|
|
452
|
+
for (const key of ["processes", "definitions"]) {
|
|
453
|
+
const value = next[key];
|
|
454
|
+
if (!Array.isArray(value) || value.length < 2) {
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
const reranked = value
|
|
458
|
+
.map((item, index) => ({
|
|
459
|
+
item,
|
|
460
|
+
index,
|
|
461
|
+
score: scoreQueryCandidate(item, terms),
|
|
462
|
+
}))
|
|
463
|
+
.sort((a, b) => (b.score - a.score) || (a.index - b.index))
|
|
464
|
+
.map((entry) => entry.item);
|
|
465
|
+
const orderChanged = reranked.some((item, index) => item !== value[index]);
|
|
466
|
+
if (orderChanged) {
|
|
467
|
+
next[key] = reranked;
|
|
468
|
+
changed = true;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return {
|
|
472
|
+
structuredContent: next,
|
|
473
|
+
changed,
|
|
474
|
+
note: changed ? describeQueryTopMatches(next) : undefined,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
function describeExecutionSummary(item) {
|
|
478
|
+
if (item.tool === "query") {
|
|
479
|
+
const rerankedTopMatches = describeQueryTopMatches(item.structuredContent);
|
|
480
|
+
if (rerankedTopMatches) {
|
|
481
|
+
return shorten(rerankedTopMatches, 110);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return shorten(item.text || "已返回结构化结果", 110);
|
|
485
|
+
}
|
|
250
486
|
function extractText(result) {
|
|
251
487
|
if (!result || typeof result !== "object") {
|
|
252
488
|
return "";
|
|
@@ -507,10 +743,10 @@ function resolveMode(request) {
|
|
|
507
743
|
if (mode === "query" || mode === "context" || mode === "impact") {
|
|
508
744
|
return mode;
|
|
509
745
|
}
|
|
510
|
-
if (request.target && request.direction) {
|
|
746
|
+
if ((request.target || request.uid) && request.direction) {
|
|
511
747
|
return "impact";
|
|
512
748
|
}
|
|
513
|
-
if (request.target && !request.query) {
|
|
749
|
+
if ((request.target || request.uid) && !request.query) {
|
|
514
750
|
return "context";
|
|
515
751
|
}
|
|
516
752
|
return "query";
|
|
@@ -528,6 +764,10 @@ async function callBridgeTool(client, tool, args, signal, workspace) {
|
|
|
528
764
|
const durationMs = Date.now() - startedAt;
|
|
529
765
|
const text = extractText(result);
|
|
530
766
|
const isError = Boolean(result.isError);
|
|
767
|
+
const structuredContent = workspace
|
|
768
|
+
? mapValueToSourceRoot(result.structuredContent, workspace)
|
|
769
|
+
: result.structuredContent;
|
|
770
|
+
const status = extractStructuredStatus(structuredContent);
|
|
531
771
|
if (isError) {
|
|
532
772
|
return {
|
|
533
773
|
tool,
|
|
@@ -535,9 +775,8 @@ async function callBridgeTool(client, tool, args, signal, workspace) {
|
|
|
535
775
|
ok: false,
|
|
536
776
|
durationMs,
|
|
537
777
|
text: workspace ? mapValueToSourceRoot(text, workspace) : text,
|
|
538
|
-
structuredContent
|
|
539
|
-
|
|
540
|
-
: result.structuredContent,
|
|
778
|
+
structuredContent,
|
|
779
|
+
status,
|
|
541
780
|
error: workspace
|
|
542
781
|
? mapValueToSourceRoot(text || `GitNexus 工具 ${tool} 返回错误`, workspace)
|
|
543
782
|
: text || `GitNexus 工具 ${tool} 返回错误`,
|
|
@@ -549,9 +788,8 @@ async function callBridgeTool(client, tool, args, signal, workspace) {
|
|
|
549
788
|
ok: true,
|
|
550
789
|
durationMs,
|
|
551
790
|
text: workspace ? mapValueToSourceRoot(text, workspace) : text,
|
|
552
|
-
structuredContent
|
|
553
|
-
|
|
554
|
-
: result.structuredContent,
|
|
791
|
+
structuredContent,
|
|
792
|
+
status,
|
|
555
793
|
};
|
|
556
794
|
}
|
|
557
795
|
catch (error) {
|
|
@@ -571,6 +809,7 @@ export async function runCodeInsightBridge(request) {
|
|
|
571
809
|
const modeRequested = request.mode || "auto";
|
|
572
810
|
const modeResolved = resolveMode(request);
|
|
573
811
|
const requestedProjectRoot = resolveRequestedProjectRoot(request.projectRoot);
|
|
812
|
+
const launcher = resolveGitNexusBridgeCommand();
|
|
574
813
|
if (!isBridgeEnabled() || isEnvDisabled("MCP_ENABLE_GITNEXUS_BRIDGE")) {
|
|
575
814
|
const sourceRoot = findGitRoot(requestedProjectRoot) || requestedProjectRoot;
|
|
576
815
|
return {
|
|
@@ -584,6 +823,8 @@ export async function runCodeInsightBridge(request) {
|
|
|
584
823
|
executions: [],
|
|
585
824
|
warnings: ["bridge_disabled"],
|
|
586
825
|
repo: resolvePreferredRepoName(request.repo),
|
|
826
|
+
launcherStrategy: launcher.strategy,
|
|
827
|
+
ambiguities: [],
|
|
587
828
|
workspaceMode: "direct",
|
|
588
829
|
sourceRoot,
|
|
589
830
|
analysisRoot: sourceRoot,
|
|
@@ -603,6 +844,8 @@ export async function runCodeInsightBridge(request) {
|
|
|
603
844
|
executions: [],
|
|
604
845
|
warnings: ["bridge_failure_cached"],
|
|
605
846
|
repo: resolvePreferredRepoName(request.repo),
|
|
847
|
+
launcherStrategy: launcher.strategy,
|
|
848
|
+
ambiguities: [],
|
|
606
849
|
workspaceMode: "direct",
|
|
607
850
|
sourceRoot,
|
|
608
851
|
analysisRoot: sourceRoot,
|
|
@@ -622,6 +865,8 @@ export async function runCodeInsightBridge(request) {
|
|
|
622
865
|
executions: [],
|
|
623
866
|
warnings: ["project_root_required", "unsafe_home_directory"],
|
|
624
867
|
repo: resolvePreferredRepoName(request.repo),
|
|
868
|
+
launcherStrategy: launcher.strategy,
|
|
869
|
+
ambiguities: [],
|
|
625
870
|
workspaceMode: "direct",
|
|
626
871
|
sourceRoot: requestedProjectRoot,
|
|
627
872
|
analysisRoot: requestedProjectRoot,
|
|
@@ -633,7 +878,7 @@ export async function runCodeInsightBridge(request) {
|
|
|
633
878
|
const effectiveRepo = workspace.workspaceMode === "temp-repo"
|
|
634
879
|
? workspace.repoName
|
|
635
880
|
: resolvePreferredRepoName(request.repo) || workspace.repoName;
|
|
636
|
-
const { command, args } =
|
|
881
|
+
const { command, args } = launcher;
|
|
637
882
|
const warnings = [];
|
|
638
883
|
if (workspace.workspaceMode === "temp-repo") {
|
|
639
884
|
warnings.push("temp_repo_workspace");
|
|
@@ -670,30 +915,50 @@ export async function runCodeInsightBridge(request) {
|
|
|
670
915
|
warnings.push("缺少 query 参数,已跳过 query");
|
|
671
916
|
return;
|
|
672
917
|
}
|
|
673
|
-
|
|
918
|
+
const execution = await callBridgeTool(client, "query", {
|
|
674
919
|
query: queryText,
|
|
675
920
|
...(request.goal ? { goal: request.goal } : {}),
|
|
676
921
|
...(request.taskContext ? { task_context: request.taskContext } : {}),
|
|
922
|
+
...(request.includeContent ? { include_content: true } : {}),
|
|
677
923
|
...(effectiveRepo ? { repo: effectiveRepo } : {}),
|
|
678
|
-
}, request.signal, workspace)
|
|
924
|
+
}, request.signal, workspace);
|
|
925
|
+
if (execution.ok) {
|
|
926
|
+
const reranked = rerankQueryStructuredContent(execution.structuredContent, {
|
|
927
|
+
query: queryText,
|
|
928
|
+
goal: request.goal,
|
|
929
|
+
taskContext: request.taskContext,
|
|
930
|
+
});
|
|
931
|
+
execution.structuredContent = reranked.structuredContent;
|
|
932
|
+
if (reranked.changed) {
|
|
933
|
+
warnings.push("query_results_reranked");
|
|
934
|
+
if (reranked.note) {
|
|
935
|
+
execution.text = execution.text
|
|
936
|
+
? `${reranked.note}\n\n${execution.text}`
|
|
937
|
+
: reranked.note;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
executions.push(execution);
|
|
679
942
|
};
|
|
680
943
|
const runContext = async () => {
|
|
681
|
-
if (!request.target) {
|
|
682
|
-
warnings.push("缺少 target 参数,已跳过 context");
|
|
944
|
+
if (!request.target && !request.uid) {
|
|
945
|
+
warnings.push("缺少 target/uid 参数,已跳过 context");
|
|
683
946
|
return;
|
|
684
947
|
}
|
|
685
948
|
executions.push(await callBridgeTool(client, "context", {
|
|
686
|
-
name: request.target,
|
|
949
|
+
...(request.uid ? { uid: request.uid } : { name: request.target }),
|
|
950
|
+
...(request.filePath ? { file_path: request.filePath } : {}),
|
|
951
|
+
...(request.includeContent ? { include_content: true } : {}),
|
|
687
952
|
...(effectiveRepo ? { repo: effectiveRepo } : {}),
|
|
688
953
|
}, request.signal, workspace));
|
|
689
954
|
};
|
|
690
955
|
const runImpact = async () => {
|
|
691
|
-
if (!request.target) {
|
|
692
|
-
warnings.push("缺少 target 参数,已跳过 impact");
|
|
956
|
+
if (!request.target && !request.uid) {
|
|
957
|
+
warnings.push("缺少 target/uid 参数,已跳过 impact");
|
|
693
958
|
return;
|
|
694
959
|
}
|
|
695
960
|
executions.push(await callBridgeTool(client, "impact", {
|
|
696
|
-
target: request.target,
|
|
961
|
+
target: request.uid || request.target,
|
|
697
962
|
direction: request.direction || "upstream",
|
|
698
963
|
...(request.maxDepth ? { maxDepth: request.maxDepth } : {}),
|
|
699
964
|
...(typeof request.includeTests === "boolean"
|
|
@@ -704,10 +969,10 @@ export async function runCodeInsightBridge(request) {
|
|
|
704
969
|
};
|
|
705
970
|
if (modeRequested === "auto") {
|
|
706
971
|
await runQuery();
|
|
707
|
-
if (request.target) {
|
|
972
|
+
if (request.target || request.uid) {
|
|
708
973
|
await runContext();
|
|
709
974
|
}
|
|
710
|
-
if (request.target && request.direction) {
|
|
975
|
+
if ((request.target || request.uid) && request.direction) {
|
|
711
976
|
await runImpact();
|
|
712
977
|
}
|
|
713
978
|
}
|
|
@@ -740,6 +1005,8 @@ export async function runCodeInsightBridge(request) {
|
|
|
740
1005
|
executions: [],
|
|
741
1006
|
warnings: ["bridge_unavailable", ...warnings],
|
|
742
1007
|
repo: effectiveRepo,
|
|
1008
|
+
launcherStrategy: launcher.strategy,
|
|
1009
|
+
ambiguities: [],
|
|
743
1010
|
workspaceMode: workspace.workspaceMode,
|
|
744
1011
|
sourceRoot: workspace.sourceRoot,
|
|
745
1012
|
analysisRoot: workspace.analysisRoot,
|
|
@@ -768,6 +1035,7 @@ export async function runCodeInsightBridge(request) {
|
|
|
768
1035
|
}
|
|
769
1036
|
bridgeFailureUntil = 0;
|
|
770
1037
|
bridgeFailureReason = "";
|
|
1038
|
+
const ambiguities = buildAmbiguities(successful);
|
|
771
1039
|
if (successful.length === 0) {
|
|
772
1040
|
const fallbackError = failed.length > 0
|
|
773
1041
|
? shorten(failed.map((item) => `${item.tool}: ${item.error || "unknown error"}`).join(" | "), 220)
|
|
@@ -783,14 +1051,19 @@ export async function runCodeInsightBridge(request) {
|
|
|
783
1051
|
executions,
|
|
784
1052
|
warnings: ["bridge_call_failed", ...warnings],
|
|
785
1053
|
repo: effectiveRepo,
|
|
1054
|
+
launcherStrategy: launcher.strategy,
|
|
1055
|
+
ambiguities,
|
|
786
1056
|
workspaceMode: workspace.workspaceMode,
|
|
787
1057
|
sourceRoot: workspace.sourceRoot,
|
|
788
1058
|
analysisRoot: workspace.analysisRoot,
|
|
789
1059
|
pathMapped: workspace.pathMapped,
|
|
790
1060
|
};
|
|
791
1061
|
}
|
|
792
|
-
const
|
|
793
|
-
|
|
1062
|
+
const summary = ambiguities.length > 0
|
|
1063
|
+
? `GitNexus 返回了 ${ambiguities.length} 个歧义结果,请指定 uid 或 file_path 后重试。`
|
|
1064
|
+
: `GitNexus 图谱增强已启用(${successful.length}/${executions.length} 成功): ${successful
|
|
1065
|
+
.map((item) => `${item.tool}: ${describeExecutionSummary(item)}`)
|
|
1066
|
+
.join(" | ")}`;
|
|
794
1067
|
return {
|
|
795
1068
|
provider: "gitnexus",
|
|
796
1069
|
enabled: true,
|
|
@@ -802,6 +1075,8 @@ export async function runCodeInsightBridge(request) {
|
|
|
802
1075
|
executions,
|
|
803
1076
|
warnings,
|
|
804
1077
|
repo: effectiveRepo,
|
|
1078
|
+
launcherStrategy: launcher.strategy,
|
|
1079
|
+
ambiguities,
|
|
805
1080
|
workspaceMode: workspace.workspaceMode,
|
|
806
1081
|
sourceRoot: workspace.sourceRoot,
|
|
807
1082
|
analysisRoot: workspace.analysisRoot,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from "vitest";
|
|
2
|
-
import { codeInsight, resolveCodeInsightQuery } from "../code_insight.js";
|
|
2
|
+
import { buildCodeInsightDelegatedPlan, codeInsight, deriveCodeInsightStatus, resolveCodeInsightQuery, } from "../code_insight.js";
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as fs from "node:fs";
|
|
5
5
|
import * as path from "node:path";
|
|
@@ -76,6 +76,56 @@ describe("code_insight 单元测试", () => {
|
|
|
76
76
|
expect(resolved.finalQuery).toMatch(/核心流程/);
|
|
77
77
|
expect(resolved.finalQuery).toMatch(/依赖关系/);
|
|
78
78
|
});
|
|
79
|
+
test("歧义结果会生成候选选择 delegated plan", () => {
|
|
80
|
+
const status = deriveCodeInsightStatus({
|
|
81
|
+
available: true,
|
|
82
|
+
ambiguities: [
|
|
83
|
+
{
|
|
84
|
+
tool: "context",
|
|
85
|
+
message: "找到多个 login 符号",
|
|
86
|
+
candidates: [{ uid: "Method:auth.ts:login:12", file_path: "/repo/src/auth.ts" }],
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
executions: [{ tool: "context", ok: true, durationMs: 10, args: {}, status: "ambiguous" }],
|
|
90
|
+
});
|
|
91
|
+
const plan = buildCodeInsightDelegatedPlan({
|
|
92
|
+
status,
|
|
93
|
+
ambiguities: [
|
|
94
|
+
{
|
|
95
|
+
tool: "context",
|
|
96
|
+
message: "找到多个 login 符号",
|
|
97
|
+
candidates: [{ uid: "Method:auth.ts:login:12", file_path: "/repo/src/auth.ts" }],
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
showPlan: true,
|
|
101
|
+
});
|
|
102
|
+
expect(status).toBe("ambiguous");
|
|
103
|
+
expect(plan?.kind).toBe("ambiguity");
|
|
104
|
+
expect(plan?.steps[1].action).toMatch(/uid 或 file_path/);
|
|
105
|
+
});
|
|
106
|
+
test("未显式要求保存时不生成 docs delegated plan", async () => {
|
|
107
|
+
const prev = process.env.MCP_ENABLE_GITNEXUS_BRIDGE;
|
|
108
|
+
process.env.MCP_ENABLE_GITNEXUS_BRIDGE = "0";
|
|
109
|
+
try {
|
|
110
|
+
const result = await codeInsight({
|
|
111
|
+
mode: "auto",
|
|
112
|
+
});
|
|
113
|
+
expect(result.isError).toBe(false);
|
|
114
|
+
const text = String(result.content?.[0]?.text || "");
|
|
115
|
+
const structured = result.structuredContent;
|
|
116
|
+
expect(text).not.toMatch(/delegated plan/);
|
|
117
|
+
expect(structured.plan).toBeUndefined();
|
|
118
|
+
expect(structured.projectDocs).toBeUndefined();
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
if (prev === undefined) {
|
|
122
|
+
delete process.env.MCP_ENABLE_GITNEXUS_BRIDGE;
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
process.env.MCP_ENABLE_GITNEXUS_BRIDGE = prev;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
});
|
|
79
129
|
test("返回 docs 保存指引而不直接代写文件", async () => {
|
|
80
130
|
const prev = process.env.MCP_ENABLE_GITNEXUS_BRIDGE;
|
|
81
131
|
process.env.MCP_ENABLE_GITNEXUS_BRIDGE = "0";
|
|
@@ -1,5 +1,34 @@
|
|
|
1
|
-
import { type CodeInsightMode } from "../lib/gitnexus-bridge.js";
|
|
1
|
+
import { type CodeInsightAmbiguity, type CodeInsightBridgeResult, type CodeInsightMode } from "../lib/gitnexus-bridge.js";
|
|
2
2
|
import { type ToolExecutionContext } from "../lib/tool-execution-context.js";
|
|
3
|
+
type CodeInsightStatus = "ok" | "degraded" | "ambiguous" | "not_found";
|
|
4
|
+
interface ProjectDocsPlan {
|
|
5
|
+
docsDir: string;
|
|
6
|
+
projectContextFilePath: string;
|
|
7
|
+
latestMarkdownFilePath: string;
|
|
8
|
+
latestJsonFilePath: string;
|
|
9
|
+
archiveMarkdownFilePath: string;
|
|
10
|
+
archiveJsonFilePath: string;
|
|
11
|
+
navigationSnippet: string;
|
|
12
|
+
devGuideSnippet: string;
|
|
13
|
+
}
|
|
14
|
+
interface DelegatedPlanStep {
|
|
15
|
+
id: string;
|
|
16
|
+
action: string;
|
|
17
|
+
outputs?: string[];
|
|
18
|
+
note?: string;
|
|
19
|
+
}
|
|
20
|
+
interface DelegatedPlan {
|
|
21
|
+
mode: "delegated";
|
|
22
|
+
kind: "docs" | "ambiguity";
|
|
23
|
+
steps: DelegatedPlanStep[];
|
|
24
|
+
}
|
|
25
|
+
export declare function deriveCodeInsightStatus(result: Pick<CodeInsightBridgeResult, "available" | "ambiguities" | "executions">): CodeInsightStatus;
|
|
26
|
+
export declare function buildCodeInsightDelegatedPlan(input: {
|
|
27
|
+
status: CodeInsightStatus;
|
|
28
|
+
ambiguities: CodeInsightAmbiguity[];
|
|
29
|
+
projectDocs?: ProjectDocsPlan;
|
|
30
|
+
showPlan: boolean;
|
|
31
|
+
}): DelegatedPlan | undefined;
|
|
3
32
|
export declare function resolveCodeInsightQuery(input: {
|
|
4
33
|
mode: CodeInsightMode;
|
|
5
34
|
query: string;
|
|
@@ -16,3 +45,4 @@ export declare function codeInsight(args: any, context?: ToolExecutionContext):
|
|
|
16
45
|
}[];
|
|
17
46
|
isError: boolean;
|
|
18
47
|
}>;
|
|
48
|
+
export {};
|