mcp-probe-kit 3.0.11 → 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 +78 -19
- package/build/lib/gitnexus-bridge.d.ts +35 -2
- package/build/lib/gitnexus-bridge.js +311 -68
- 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 +3 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
2
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
3
|
-
import { execFileSync
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import spawn from "cross-spawn";
|
|
4
5
|
import * as fs from "node:fs";
|
|
5
6
|
import * as os from "node:os";
|
|
6
7
|
import * as path from "node:path";
|
|
@@ -60,9 +61,9 @@ function isBridgeEnabled() {
|
|
|
60
61
|
}
|
|
61
62
|
return !/^(0|false|no|off)$/i.test(raw.trim());
|
|
62
63
|
}
|
|
63
|
-
function splitArgs(raw) {
|
|
64
|
+
function splitArgs(raw, fallback = DEFAULT_GITNEXUS_ARGS) {
|
|
64
65
|
if (!raw) {
|
|
65
|
-
return [...
|
|
66
|
+
return [...fallback];
|
|
66
67
|
}
|
|
67
68
|
return raw
|
|
68
69
|
.trim()
|
|
@@ -118,14 +119,14 @@ function parseAvailableReposFromError(text) {
|
|
|
118
119
|
.map((item) => item.trim())
|
|
119
120
|
.filter(Boolean);
|
|
120
121
|
}
|
|
121
|
-
function
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
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";
|
|
125
128
|
}
|
|
126
|
-
function
|
|
127
|
-
const command = (process.env.MCP_GITNEXUS_COMMAND || "npx").trim() || "npx";
|
|
128
|
-
const bridgeArgs = splitArgs(process.env.MCP_GITNEXUS_ARGS);
|
|
129
|
+
function resolveNpxPackageArgs(bridgeArgs) {
|
|
129
130
|
const flags = [];
|
|
130
131
|
let packageSpec = "gitnexus@latest";
|
|
131
132
|
for (const arg of bridgeArgs) {
|
|
@@ -139,17 +140,58 @@ function resolveGitNexusCliCommand(subcommand) {
|
|
|
139
140
|
packageSpec = arg;
|
|
140
141
|
break;
|
|
141
142
|
}
|
|
142
|
-
return
|
|
143
|
+
return [...flags, packageSpec];
|
|
143
144
|
}
|
|
144
|
-
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) {
|
|
145
187
|
const normalized = (command || "").trim();
|
|
146
188
|
if (!normalized) {
|
|
147
|
-
return resolveExecutableCommand("npx", platform);
|
|
189
|
+
return resolveExecutableCommand("npx", platform, env);
|
|
148
190
|
}
|
|
149
191
|
if (path.isAbsolute(normalized) && fs.existsSync(normalized)) {
|
|
150
192
|
return normalized;
|
|
151
193
|
}
|
|
152
|
-
const found = findExecutablePath(normalized, platform);
|
|
194
|
+
const found = findExecutablePath(normalized, platform, env);
|
|
153
195
|
if (found) {
|
|
154
196
|
return found;
|
|
155
197
|
}
|
|
@@ -165,7 +207,7 @@ export function resolveExecutableCommand(command, platform = process.platform) {
|
|
|
165
207
|
}
|
|
166
208
|
return normalized;
|
|
167
209
|
}
|
|
168
|
-
function findExecutablePath(command, platform = process.platform) {
|
|
210
|
+
function findExecutablePath(command, platform = process.platform, env = process.env) {
|
|
169
211
|
const trimmed = (command || "").trim();
|
|
170
212
|
if (!trimmed || path.isAbsolute(trimmed)) {
|
|
171
213
|
return undefined;
|
|
@@ -196,6 +238,7 @@ function findExecutablePath(command, platform = process.platform) {
|
|
|
196
238
|
try {
|
|
197
239
|
const output = execFileSync("where.exe", [trimmed], {
|
|
198
240
|
encoding: "utf-8",
|
|
241
|
+
env,
|
|
199
242
|
stdio: ["ignore", "pipe", "ignore"],
|
|
200
243
|
windowsHide: true,
|
|
201
244
|
}).trim();
|
|
@@ -209,7 +252,7 @@ function findExecutablePath(command, platform = process.platform) {
|
|
|
209
252
|
// fall back to PATH scan
|
|
210
253
|
}
|
|
211
254
|
}
|
|
212
|
-
const pathEntries = (
|
|
255
|
+
const pathEntries = (env.PATH || "")
|
|
213
256
|
.split(path.delimiter)
|
|
214
257
|
.map((entry) => entry.trim())
|
|
215
258
|
.filter(Boolean);
|
|
@@ -231,54 +274,215 @@ function findExecutablePath(command, platform = process.platform) {
|
|
|
231
274
|
}
|
|
232
275
|
if (platform === "win32" && trimmed.toLowerCase() === "git") {
|
|
233
276
|
const commonCandidates = [
|
|
234
|
-
path.join(
|
|
235
|
-
path.join(
|
|
236
|
-
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"),
|
|
237
280
|
];
|
|
238
281
|
return preferredPath(commonCandidates);
|
|
239
282
|
}
|
|
240
283
|
return undefined;
|
|
241
284
|
}
|
|
242
|
-
function
|
|
243
|
-
|
|
244
|
-
|
|
285
|
+
export function resolveSpawnCommand(command, args, platform = process.platform, env = process.env) {
|
|
286
|
+
const executable = resolveExecutableCommand(command, platform, env);
|
|
287
|
+
return {
|
|
288
|
+
command: executable,
|
|
289
|
+
args,
|
|
290
|
+
};
|
|
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;
|
|
245
305
|
}
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
+
}
|
|
251
311
|
}
|
|
252
|
-
|
|
253
|
-
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
314
|
+
function extractStructuredCandidates(value) {
|
|
315
|
+
const candidates = toRecord(value)?.candidates;
|
|
316
|
+
if (!Array.isArray(candidates)) {
|
|
317
|
+
return [];
|
|
254
318
|
}
|
|
255
|
-
|
|
256
|
-
|
|
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
|
+
}
|
|
257
354
|
}
|
|
258
|
-
return
|
|
355
|
+
return [...tokens];
|
|
259
356
|
}
|
|
260
|
-
function
|
|
261
|
-
if (
|
|
262
|
-
return
|
|
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;
|
|
263
367
|
}
|
|
264
|
-
if (
|
|
265
|
-
|
|
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
|
+
}
|
|
266
378
|
}
|
|
267
|
-
return
|
|
379
|
+
return bag;
|
|
268
380
|
}
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
-
if (
|
|
272
|
-
return
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
+
}
|
|
276
470
|
}
|
|
277
471
|
return {
|
|
278
|
-
|
|
279
|
-
|
|
472
|
+
structuredContent: next,
|
|
473
|
+
changed,
|
|
474
|
+
note: changed ? describeQueryTopMatches(next) : undefined,
|
|
280
475
|
};
|
|
281
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
|
+
}
|
|
282
486
|
function extractText(result) {
|
|
283
487
|
if (!result || typeof result !== "object") {
|
|
284
488
|
return "";
|
|
@@ -539,10 +743,10 @@ function resolveMode(request) {
|
|
|
539
743
|
if (mode === "query" || mode === "context" || mode === "impact") {
|
|
540
744
|
return mode;
|
|
541
745
|
}
|
|
542
|
-
if (request.target && request.direction) {
|
|
746
|
+
if ((request.target || request.uid) && request.direction) {
|
|
543
747
|
return "impact";
|
|
544
748
|
}
|
|
545
|
-
if (request.target && !request.query) {
|
|
749
|
+
if ((request.target || request.uid) && !request.query) {
|
|
546
750
|
return "context";
|
|
547
751
|
}
|
|
548
752
|
return "query";
|
|
@@ -560,6 +764,10 @@ async function callBridgeTool(client, tool, args, signal, workspace) {
|
|
|
560
764
|
const durationMs = Date.now() - startedAt;
|
|
561
765
|
const text = extractText(result);
|
|
562
766
|
const isError = Boolean(result.isError);
|
|
767
|
+
const structuredContent = workspace
|
|
768
|
+
? mapValueToSourceRoot(result.structuredContent, workspace)
|
|
769
|
+
: result.structuredContent;
|
|
770
|
+
const status = extractStructuredStatus(structuredContent);
|
|
563
771
|
if (isError) {
|
|
564
772
|
return {
|
|
565
773
|
tool,
|
|
@@ -567,9 +775,8 @@ async function callBridgeTool(client, tool, args, signal, workspace) {
|
|
|
567
775
|
ok: false,
|
|
568
776
|
durationMs,
|
|
569
777
|
text: workspace ? mapValueToSourceRoot(text, workspace) : text,
|
|
570
|
-
structuredContent
|
|
571
|
-
|
|
572
|
-
: result.structuredContent,
|
|
778
|
+
structuredContent,
|
|
779
|
+
status,
|
|
573
780
|
error: workspace
|
|
574
781
|
? mapValueToSourceRoot(text || `GitNexus 工具 ${tool} 返回错误`, workspace)
|
|
575
782
|
: text || `GitNexus 工具 ${tool} 返回错误`,
|
|
@@ -581,9 +788,8 @@ async function callBridgeTool(client, tool, args, signal, workspace) {
|
|
|
581
788
|
ok: true,
|
|
582
789
|
durationMs,
|
|
583
790
|
text: workspace ? mapValueToSourceRoot(text, workspace) : text,
|
|
584
|
-
structuredContent
|
|
585
|
-
|
|
586
|
-
: result.structuredContent,
|
|
791
|
+
structuredContent,
|
|
792
|
+
status,
|
|
587
793
|
};
|
|
588
794
|
}
|
|
589
795
|
catch (error) {
|
|
@@ -603,6 +809,7 @@ export async function runCodeInsightBridge(request) {
|
|
|
603
809
|
const modeRequested = request.mode || "auto";
|
|
604
810
|
const modeResolved = resolveMode(request);
|
|
605
811
|
const requestedProjectRoot = resolveRequestedProjectRoot(request.projectRoot);
|
|
812
|
+
const launcher = resolveGitNexusBridgeCommand();
|
|
606
813
|
if (!isBridgeEnabled() || isEnvDisabled("MCP_ENABLE_GITNEXUS_BRIDGE")) {
|
|
607
814
|
const sourceRoot = findGitRoot(requestedProjectRoot) || requestedProjectRoot;
|
|
608
815
|
return {
|
|
@@ -616,6 +823,8 @@ export async function runCodeInsightBridge(request) {
|
|
|
616
823
|
executions: [],
|
|
617
824
|
warnings: ["bridge_disabled"],
|
|
618
825
|
repo: resolvePreferredRepoName(request.repo),
|
|
826
|
+
launcherStrategy: launcher.strategy,
|
|
827
|
+
ambiguities: [],
|
|
619
828
|
workspaceMode: "direct",
|
|
620
829
|
sourceRoot,
|
|
621
830
|
analysisRoot: sourceRoot,
|
|
@@ -635,6 +844,8 @@ export async function runCodeInsightBridge(request) {
|
|
|
635
844
|
executions: [],
|
|
636
845
|
warnings: ["bridge_failure_cached"],
|
|
637
846
|
repo: resolvePreferredRepoName(request.repo),
|
|
847
|
+
launcherStrategy: launcher.strategy,
|
|
848
|
+
ambiguities: [],
|
|
638
849
|
workspaceMode: "direct",
|
|
639
850
|
sourceRoot,
|
|
640
851
|
analysisRoot: sourceRoot,
|
|
@@ -654,6 +865,8 @@ export async function runCodeInsightBridge(request) {
|
|
|
654
865
|
executions: [],
|
|
655
866
|
warnings: ["project_root_required", "unsafe_home_directory"],
|
|
656
867
|
repo: resolvePreferredRepoName(request.repo),
|
|
868
|
+
launcherStrategy: launcher.strategy,
|
|
869
|
+
ambiguities: [],
|
|
657
870
|
workspaceMode: "direct",
|
|
658
871
|
sourceRoot: requestedProjectRoot,
|
|
659
872
|
analysisRoot: requestedProjectRoot,
|
|
@@ -665,7 +878,7 @@ export async function runCodeInsightBridge(request) {
|
|
|
665
878
|
const effectiveRepo = workspace.workspaceMode === "temp-repo"
|
|
666
879
|
? workspace.repoName
|
|
667
880
|
: resolvePreferredRepoName(request.repo) || workspace.repoName;
|
|
668
|
-
const { command, args } =
|
|
881
|
+
const { command, args } = launcher;
|
|
669
882
|
const warnings = [];
|
|
670
883
|
if (workspace.workspaceMode === "temp-repo") {
|
|
671
884
|
warnings.push("temp_repo_workspace");
|
|
@@ -702,30 +915,50 @@ export async function runCodeInsightBridge(request) {
|
|
|
702
915
|
warnings.push("缺少 query 参数,已跳过 query");
|
|
703
916
|
return;
|
|
704
917
|
}
|
|
705
|
-
|
|
918
|
+
const execution = await callBridgeTool(client, "query", {
|
|
706
919
|
query: queryText,
|
|
707
920
|
...(request.goal ? { goal: request.goal } : {}),
|
|
708
921
|
...(request.taskContext ? { task_context: request.taskContext } : {}),
|
|
922
|
+
...(request.includeContent ? { include_content: true } : {}),
|
|
709
923
|
...(effectiveRepo ? { repo: effectiveRepo } : {}),
|
|
710
|
-
}, 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);
|
|
711
942
|
};
|
|
712
943
|
const runContext = async () => {
|
|
713
|
-
if (!request.target) {
|
|
714
|
-
warnings.push("缺少 target 参数,已跳过 context");
|
|
944
|
+
if (!request.target && !request.uid) {
|
|
945
|
+
warnings.push("缺少 target/uid 参数,已跳过 context");
|
|
715
946
|
return;
|
|
716
947
|
}
|
|
717
948
|
executions.push(await callBridgeTool(client, "context", {
|
|
718
|
-
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 } : {}),
|
|
719
952
|
...(effectiveRepo ? { repo: effectiveRepo } : {}),
|
|
720
953
|
}, request.signal, workspace));
|
|
721
954
|
};
|
|
722
955
|
const runImpact = async () => {
|
|
723
|
-
if (!request.target) {
|
|
724
|
-
warnings.push("缺少 target 参数,已跳过 impact");
|
|
956
|
+
if (!request.target && !request.uid) {
|
|
957
|
+
warnings.push("缺少 target/uid 参数,已跳过 impact");
|
|
725
958
|
return;
|
|
726
959
|
}
|
|
727
960
|
executions.push(await callBridgeTool(client, "impact", {
|
|
728
|
-
target: request.target,
|
|
961
|
+
target: request.uid || request.target,
|
|
729
962
|
direction: request.direction || "upstream",
|
|
730
963
|
...(request.maxDepth ? { maxDepth: request.maxDepth } : {}),
|
|
731
964
|
...(typeof request.includeTests === "boolean"
|
|
@@ -736,10 +969,10 @@ export async function runCodeInsightBridge(request) {
|
|
|
736
969
|
};
|
|
737
970
|
if (modeRequested === "auto") {
|
|
738
971
|
await runQuery();
|
|
739
|
-
if (request.target) {
|
|
972
|
+
if (request.target || request.uid) {
|
|
740
973
|
await runContext();
|
|
741
974
|
}
|
|
742
|
-
if (request.target && request.direction) {
|
|
975
|
+
if ((request.target || request.uid) && request.direction) {
|
|
743
976
|
await runImpact();
|
|
744
977
|
}
|
|
745
978
|
}
|
|
@@ -772,6 +1005,8 @@ export async function runCodeInsightBridge(request) {
|
|
|
772
1005
|
executions: [],
|
|
773
1006
|
warnings: ["bridge_unavailable", ...warnings],
|
|
774
1007
|
repo: effectiveRepo,
|
|
1008
|
+
launcherStrategy: launcher.strategy,
|
|
1009
|
+
ambiguities: [],
|
|
775
1010
|
workspaceMode: workspace.workspaceMode,
|
|
776
1011
|
sourceRoot: workspace.sourceRoot,
|
|
777
1012
|
analysisRoot: workspace.analysisRoot,
|
|
@@ -800,6 +1035,7 @@ export async function runCodeInsightBridge(request) {
|
|
|
800
1035
|
}
|
|
801
1036
|
bridgeFailureUntil = 0;
|
|
802
1037
|
bridgeFailureReason = "";
|
|
1038
|
+
const ambiguities = buildAmbiguities(successful);
|
|
803
1039
|
if (successful.length === 0) {
|
|
804
1040
|
const fallbackError = failed.length > 0
|
|
805
1041
|
? shorten(failed.map((item) => `${item.tool}: ${item.error || "unknown error"}`).join(" | "), 220)
|
|
@@ -815,14 +1051,19 @@ export async function runCodeInsightBridge(request) {
|
|
|
815
1051
|
executions,
|
|
816
1052
|
warnings: ["bridge_call_failed", ...warnings],
|
|
817
1053
|
repo: effectiveRepo,
|
|
1054
|
+
launcherStrategy: launcher.strategy,
|
|
1055
|
+
ambiguities,
|
|
818
1056
|
workspaceMode: workspace.workspaceMode,
|
|
819
1057
|
sourceRoot: workspace.sourceRoot,
|
|
820
1058
|
analysisRoot: workspace.analysisRoot,
|
|
821
1059
|
pathMapped: workspace.pathMapped,
|
|
822
1060
|
};
|
|
823
1061
|
}
|
|
824
|
-
const
|
|
825
|
-
|
|
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(" | ")}`;
|
|
826
1067
|
return {
|
|
827
1068
|
provider: "gitnexus",
|
|
828
1069
|
enabled: true,
|
|
@@ -834,6 +1075,8 @@ export async function runCodeInsightBridge(request) {
|
|
|
834
1075
|
executions,
|
|
835
1076
|
warnings,
|
|
836
1077
|
repo: effectiveRepo,
|
|
1078
|
+
launcherStrategy: launcher.strategy,
|
|
1079
|
+
ambiguities,
|
|
837
1080
|
workspaceMode: workspace.workspaceMode,
|
|
838
1081
|
sourceRoot: workspace.sourceRoot,
|
|
839
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";
|