@lorrylurui/code-intelligence-mcp 2.0.4 → 2.0.6
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 +41 -0
- package/dist/config/env.js +9 -0
- package/dist/config/tuning.js +114 -0
- package/dist/db/schema.js +37 -0
- package/dist/index.js +1 -0
- package/dist/indexer/babelParser.js +2 -1
- package/dist/indexer/chunkText.js +164 -0
- package/dist/indexer/embedText.js +2 -2
- package/dist/indexer/indexProject.js +193 -22
- package/dist/indexer/jsAstNormalizer.js +36 -6
- package/dist/prompts/reusableCodeAdvisorPrompt.js +63 -34
- package/dist/repositories/chunkRepository.js +181 -0
- package/dist/repositories/symbolRepository.js +108 -15
- package/dist/server/createServer.js +16 -0
- package/dist/services/contextAssembler.js +150 -0
- package/dist/services/ranking.js +109 -58
- package/dist/services/recommendationService.js +515 -46
- package/dist/services/reindex.js +25 -0
- package/dist/tools/getSymbolDetail.js +2 -1
- package/dist/tools/queryDocs.js +113 -0
- package/dist/tools/recommendComponent.js +86 -10
- package/dist/tools/searchByStructure.js +2 -1
- package/dist/tools/searchSymbols.js +57 -21
- package/dist/types/chunk.js +1 -0
- package/dist/workers/embeddingWorker.js +0 -1
- package/package.json +1 -1
|
@@ -1,10 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ──────────────────────────────────────────────────────────────────────────────
|
|
3
|
+
* [Agent 闭环流程总览]
|
|
4
|
+
*
|
|
5
|
+
* recommendComponent (核心 agent 主循环)
|
|
6
|
+
*
|
|
7
|
+
* 1. 解析输入,生成 query 变体(query rewrite,多轮尝试)
|
|
8
|
+
* 2. 对每个 query 变体依次尝试:
|
|
9
|
+
* 2.1. 搜索候选(优先语义,异常时回退关键词)
|
|
10
|
+
* 2.2. 结构字段补充搜索(props/hooks)
|
|
11
|
+
* 2.3. 合并去重、按 category 过滤、过滤不可复用项
|
|
12
|
+
* 2.4. 排序、Top-K 详情补查(enrich)
|
|
13
|
+
* 2.5. 质量门控(quality gate,必须命中 requiredProps/hooks 或高分)
|
|
14
|
+
* 2.6. 优先级调整(如名称/路径命中加分、demo 路径降权)
|
|
15
|
+
* 2.7. 命中则立即返回推荐结果,记录 debug trace
|
|
16
|
+
* 2.8. 未命中则进入下一 query 变体(自动重试)
|
|
17
|
+
* 3. 所有变体均未命中则返回无结果,debug trace 记录所有尝试
|
|
18
|
+
*
|
|
19
|
+
* 关键特性:
|
|
20
|
+
* - query rewrite + retry(自动多轮尝试)
|
|
21
|
+
* - 结构/语义/关键词多路融合
|
|
22
|
+
* - Top-K 详情补查
|
|
23
|
+
* - 质量门控与优先级调整
|
|
24
|
+
* - 全流程 debug trace(可用于 agent 反思/可观测性)
|
|
25
|
+
*
|
|
26
|
+
* 总结:
|
|
27
|
+
* “实现了一个单 agent 闭环推荐系统,支持 query 自动重写与多轮重试,融合语义/结构/关键词多路检索,Top-K 详情补查,质量门控与优先级调整,并输出全流程 debug trace,便于 agent 反思和可观测性。”
|
|
28
|
+
* ──────────────────────────────────────────────────────────────────────────────
|
|
29
|
+
*/
|
|
1
30
|
import { rankSemanticHits, rankSymbols } from './ranking.js';
|
|
31
|
+
import { DEMO_PATH_PRIORITY_PENALTY, LITERAL_MATCH_PRIORITY_BOOST, MIN_LITERAL_MATCH_SCORE, MIN_RECOMMENDATION_SCORE, MIN_SEMANTIC_TEXT_MATCH_SCORE, REQUIRED_FIELD_FALLBACK_MIN_SCORE, } from '../config/tuning.js';
|
|
32
|
+
/** 跳过原因标识 */
|
|
33
|
+
const SKIPPED_REASON = {
|
|
34
|
+
NO_COMBINED: 'no_combined',
|
|
35
|
+
NO_QUALIFIED: 'no_qualified',
|
|
36
|
+
};
|
|
37
|
+
/** 查询方式标识 */
|
|
38
|
+
const QUERIED_BY = {
|
|
39
|
+
SEMANTIC: 'semantic',
|
|
40
|
+
KEYWORD: 'keyword',
|
|
41
|
+
};
|
|
42
|
+
/** 回退原因标识 */
|
|
43
|
+
const FALLBACK_REASON = {
|
|
44
|
+
SEMANTIC_ERROR: 'semantic_error_fallback_keyword',
|
|
45
|
+
};
|
|
46
|
+
/** 推荐结果文案 */
|
|
47
|
+
const RECOMMENDATION_MESSAGE = {
|
|
48
|
+
FOUND: '已找到可复用组件候选,首选已按综合匹配度排序。',
|
|
49
|
+
NOT_FOUND: '未找到符合条件的可复用组件。',
|
|
50
|
+
};
|
|
51
|
+
/** 详情补查的 top-k 条数 */
|
|
52
|
+
const ENRICH_TOP_K = 3;
|
|
53
|
+
/** 最多取查询变体数量 */
|
|
54
|
+
const MAX_QUERY_VARIANTS = 2;
|
|
55
|
+
/** 结构/语义搜索 limit 倍数 */
|
|
56
|
+
const STRUCTURE_LIMIT_MULTIPLIER = 4;
|
|
57
|
+
/** 结构/语义搜索 limit 最小值 */
|
|
58
|
+
const STRUCTURE_LIMIT_MIN = 12;
|
|
59
|
+
/** 关键词搜索命中时的默认相似度补值 */
|
|
60
|
+
const DEFAULT_KEYWORD_SIMILARITY = 0.55;
|
|
2
61
|
function uniqueStrings(values = []) {
|
|
3
62
|
return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
|
|
4
63
|
}
|
|
64
|
+
const QUERY_REWRITE_PATTERNS = [
|
|
65
|
+
/^帮我找(找)?(一个|一下)?/g,
|
|
66
|
+
/^有没有(现成的)?/g,
|
|
67
|
+
/^请推荐(一个|一下)?/g,
|
|
68
|
+
/可复用/g,
|
|
69
|
+
/现成的/g,
|
|
70
|
+
/封装好的/g,
|
|
71
|
+
/(组件|函数|hook|工具|util)(实现)?/gi,
|
|
72
|
+
];
|
|
73
|
+
/**
|
|
74
|
+
* 对原始查询进行清洗和变体生成,去掉无意义的词,提炼更核心的查询内容
|
|
75
|
+
*/
|
|
76
|
+
function buildQueryVariants(rawQuery) {
|
|
77
|
+
const base = rawQuery.trim();
|
|
78
|
+
if (!base)
|
|
79
|
+
return [];
|
|
80
|
+
let rewritten = base;
|
|
81
|
+
for (const pattern of QUERY_REWRITE_PATTERNS) {
|
|
82
|
+
rewritten = rewritten.replace(pattern, ' ');
|
|
83
|
+
}
|
|
84
|
+
rewritten = rewritten.replace(/\s+/g, ' ').trim();
|
|
85
|
+
if (!rewritten || rewritten === base) {
|
|
86
|
+
return [base];
|
|
87
|
+
}
|
|
88
|
+
return uniqueStrings([base, rewritten]);
|
|
89
|
+
}
|
|
5
90
|
function normalizeToken(value) {
|
|
6
91
|
return value.trim().toLowerCase();
|
|
7
92
|
}
|
|
93
|
+
const CATEGORY_TYPE_HINTS = [
|
|
94
|
+
{ keywords: ['util'], types: ['function'] },
|
|
95
|
+
{ keywords: ['hook'], types: ['hook'] },
|
|
96
|
+
{ keywords: ['type'], types: ['type', 'interface'] },
|
|
97
|
+
{ keywords: ['class'], types: ['class'] },
|
|
98
|
+
{ keywords: ['component'], types: ['component'] },
|
|
99
|
+
];
|
|
100
|
+
const QUERY_TYPE_HINTS = [
|
|
101
|
+
{
|
|
102
|
+
keywords: [' util', 'util ', 'helper', '函数', '方法', '计算', '获取'],
|
|
103
|
+
types: ['function'],
|
|
104
|
+
},
|
|
105
|
+
{ keywords: ['hook', ' use'], types: ['hook'] },
|
|
106
|
+
];
|
|
107
|
+
function resolveTypesByHints(source, hints) {
|
|
108
|
+
for (const hint of hints) {
|
|
109
|
+
if (hint.keywords.some((keyword) => source.includes(keyword))) {
|
|
110
|
+
return [...new Set(hint.types)];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
function inferSearchTypes(input) {
|
|
116
|
+
const query = input.query.toLowerCase();
|
|
117
|
+
const category = (input.category ?? '').toLowerCase();
|
|
118
|
+
const categoryTypes = resolveTypesByHints(category, CATEGORY_TYPE_HINTS);
|
|
119
|
+
if (categoryTypes.length > 0)
|
|
120
|
+
return categoryTypes;
|
|
121
|
+
const queryTypes = resolveTypesByHints(query, QUERY_TYPE_HINTS);
|
|
122
|
+
if (queryTypes.length > 0)
|
|
123
|
+
return queryTypes;
|
|
124
|
+
return ['component'];
|
|
125
|
+
}
|
|
8
126
|
function getMetaStrings(symbol, key) {
|
|
9
127
|
const value = symbol.meta?.[key];
|
|
10
128
|
if (!Array.isArray(value))
|
|
@@ -51,84 +169,435 @@ function mergeCandidates(symbols) {
|
|
|
51
169
|
}
|
|
52
170
|
return merged;
|
|
53
171
|
}
|
|
172
|
+
const NON_REUSABLE_PATH_SEGMENTS = [
|
|
173
|
+
'__tests__',
|
|
174
|
+
'__mocks__',
|
|
175
|
+
'/test/',
|
|
176
|
+
'/tests/',
|
|
177
|
+
'/fixtures/',
|
|
178
|
+
'/stories/',
|
|
179
|
+
'/story/',
|
|
180
|
+
];
|
|
181
|
+
const DEMO_LIKE_PATH_SEGMENTS_STRICT = [
|
|
182
|
+
'/one-ui-demo/',
|
|
183
|
+
'/example/',
|
|
184
|
+
'/examples/',
|
|
185
|
+
'/demo/',
|
|
186
|
+
'/demos/',
|
|
187
|
+
];
|
|
188
|
+
const DEMO_LIKE_PATH_SEGMENTS_SOFT = [
|
|
189
|
+
'/one-ui-demo/',
|
|
190
|
+
'/example/',
|
|
191
|
+
'/examples/',
|
|
192
|
+
];
|
|
193
|
+
const NON_REUSABLE_PATH_PATTERNS = [
|
|
194
|
+
'.test.',
|
|
195
|
+
'.spec.',
|
|
196
|
+
'.stories.',
|
|
197
|
+
'.story.',
|
|
198
|
+
'.mock.',
|
|
199
|
+
];
|
|
200
|
+
const NON_REUSABLE_NAME_TOKENS = ['mock', 'fixture', 'example', 'demo'];
|
|
201
|
+
function isDemoLikePath(path, strict = false) {
|
|
202
|
+
const normalizedPath = path.toLowerCase();
|
|
203
|
+
const segments = strict
|
|
204
|
+
? DEMO_LIKE_PATH_SEGMENTS_STRICT
|
|
205
|
+
: DEMO_LIKE_PATH_SEGMENTS_SOFT;
|
|
206
|
+
return segments.some((segment) => normalizedPath.includes(segment));
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* 判断是否为可复用候选,过滤掉明显的测试/示例代码。虽然有可能误伤一些真实组件,但优先保证推荐结果的实用性和专业度。
|
|
210
|
+
* @param symbol 要判断的代码符号
|
|
211
|
+
* @returns boolean 是否为可复用候选
|
|
212
|
+
*/
|
|
213
|
+
export function isReusableCandidate(symbol) {
|
|
214
|
+
const path = symbol.path.toLowerCase();
|
|
215
|
+
const name = symbol.name.toLowerCase();
|
|
216
|
+
if (isDemoLikePath(path, true) ||
|
|
217
|
+
NON_REUSABLE_PATH_SEGMENTS.some((segment) => path.includes(segment)) ||
|
|
218
|
+
NON_REUSABLE_PATH_PATTERNS.some((pattern) => path.includes(pattern))) {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
return !NON_REUSABLE_NAME_TOKENS.some((token) => name === token || name.startsWith(token));
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* 判断推荐结果的props/hooks等结构字段是否满足查询要求,作为强相关推荐的加分项之一。虽然有可能遗漏一些未正确标注字段的结果,但优先保证推荐结果的相关性和准确性。
|
|
225
|
+
* @param symbol 要判断的代码符号
|
|
226
|
+
* @param requiredProps 必需的属性列表
|
|
227
|
+
* @param requiredHooks 必需的钩子列表
|
|
228
|
+
* @returns boolean 是否为强相关推荐结果
|
|
229
|
+
*/
|
|
230
|
+
function hasAllRequiredFields(symbol, requiredProps, requiredHooks) {
|
|
231
|
+
if (requiredProps.length === 0 && requiredHooks.length === 0) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
const props = getMetaStrings(symbol, 'props').map(normalizeToken);
|
|
235
|
+
const hooks = getMetaStrings(symbol, 'hooks').map(normalizeToken);
|
|
236
|
+
return (requiredProps.every((field) => props.includes(normalizeToken(field))) &&
|
|
237
|
+
requiredHooks.every((field) => hooks.includes(normalizeToken(field))));
|
|
238
|
+
}
|
|
239
|
+
function extractLiteralTokens(query) {
|
|
240
|
+
const tokens = new Set();
|
|
241
|
+
const genericTokens = new Set([
|
|
242
|
+
'component',
|
|
243
|
+
'components',
|
|
244
|
+
'hook',
|
|
245
|
+
'hooks',
|
|
246
|
+
'util',
|
|
247
|
+
'utils',
|
|
248
|
+
'function',
|
|
249
|
+
'functions',
|
|
250
|
+
'class',
|
|
251
|
+
'classes',
|
|
252
|
+
'type',
|
|
253
|
+
'types',
|
|
254
|
+
]);
|
|
255
|
+
const normalized = query.trim().toLowerCase();
|
|
256
|
+
for (const match of normalized.matchAll(/[a-z0-9_]+/g)) {
|
|
257
|
+
const token = match[0];
|
|
258
|
+
if (token.length >= 3 && !genericTokens.has(token)) {
|
|
259
|
+
tokens.add(token);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return [...tokens];
|
|
263
|
+
}
|
|
264
|
+
// eg: query='useDebounceInput组件', symbol.name='useDebounceInput' => match; query='防抖组件', symbol.name='useDebounceInput' => match; query='input组件', symbol.name='useDebounceInput' => weak match
|
|
265
|
+
function hasStrongLiteralMatch(query, symbol) {
|
|
266
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
267
|
+
const name = symbol.name.toLowerCase();
|
|
268
|
+
const path = symbol.path.toLowerCase();
|
|
269
|
+
const basename = path.split('/').pop() ?? '';
|
|
270
|
+
if (normalizedQuery &&
|
|
271
|
+
(name === normalizedQuery || path.includes(normalizedQuery))) {
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
const tokens = extractLiteralTokens(query);
|
|
275
|
+
return tokens.some((token) => name === token || name.includes(token) || basename.includes(token));
|
|
276
|
+
}
|
|
277
|
+
function isStrongEnoughRecommendation(item, query, queriedBy, requiredProps, requiredHooks) {
|
|
278
|
+
const hasRequiredFieldMatch = hasAllRequiredFields(item.symbol, requiredProps, requiredHooks);
|
|
279
|
+
const hasLiteralMatch = hasStrongLiteralMatch(query, item.symbol);
|
|
280
|
+
if (queriedBy === QUERIED_BY.SEMANTIC) {
|
|
281
|
+
return ((item.score >= MIN_RECOMMENDATION_SCORE.semantic &&
|
|
282
|
+
(item.reason.textMatch.score >= MIN_SEMANTIC_TEXT_MATCH_SCORE ||
|
|
283
|
+
hasRequiredFieldMatch)) ||
|
|
284
|
+
(hasLiteralMatch && item.score >= MIN_LITERAL_MATCH_SCORE));
|
|
285
|
+
}
|
|
286
|
+
return (item.score >= MIN_RECOMMENDATION_SCORE.keyword ||
|
|
287
|
+
(hasRequiredFieldMatch &&
|
|
288
|
+
item.score >= REQUIRED_FIELD_FALLBACK_MIN_SCORE));
|
|
289
|
+
}
|
|
290
|
+
function computeRecommendationPriority(item, query) {
|
|
291
|
+
let score = item.score;
|
|
292
|
+
const notes = [];
|
|
293
|
+
const path = item.symbol.path.toLowerCase();
|
|
294
|
+
if (hasStrongLiteralMatch(query, item.symbol)) {
|
|
295
|
+
score += LITERAL_MATCH_PRIORITY_BOOST;
|
|
296
|
+
notes.push('名称或文件名命中查询');
|
|
297
|
+
}
|
|
298
|
+
if (isDemoLikePath(path)) {
|
|
299
|
+
score -= DEMO_PATH_PRIORITY_PENALTY;
|
|
300
|
+
notes.push('示例工程路径降权');
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
score: Number(Math.max(0, score).toFixed(3)),
|
|
304
|
+
reason: notes.length > 0
|
|
305
|
+
? `${item.reason.summary} + ${notes.join(' + ')}`
|
|
306
|
+
: item.reason.summary,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
54
309
|
export class RecommendationService {
|
|
55
310
|
repository;
|
|
56
311
|
constructor(repository) {
|
|
57
312
|
this.repository = repository;
|
|
58
313
|
}
|
|
314
|
+
/**
|
|
315
|
+
* 根据查询和提示信息从仓库中获取候选结果,优先语义搜索并在出错时回退关键词搜索,返回搜索结果和相关的调试信息供后续处理使用。
|
|
316
|
+
* @param query 查询字符串
|
|
317
|
+
* @param searchTypes 搜索的符号类型
|
|
318
|
+
* @param preferSemantic 是否优先使用语义搜索
|
|
319
|
+
* @param limit 返回结果的数量限制
|
|
320
|
+
* @returns 包含搜索结果和调试信息的对象
|
|
321
|
+
*/
|
|
322
|
+
async gatherSearchResults(query, searchTypes, preferSemantic, limit) {
|
|
323
|
+
let queriedBy = preferSemantic
|
|
324
|
+
? QUERIED_BY.SEMANTIC
|
|
325
|
+
: QUERIED_BY.KEYWORD;
|
|
326
|
+
let fallbackReason = null;
|
|
327
|
+
if (preferSemantic) {
|
|
328
|
+
try {
|
|
329
|
+
const semanticGroups = await Promise.all(searchTypes.map((type) => this.repository.searchSemanticHits(query, {
|
|
330
|
+
type,
|
|
331
|
+
limit: Math.max(limit * STRUCTURE_LIMIT_MULTIPLIER, STRUCTURE_LIMIT_MIN),
|
|
332
|
+
})));
|
|
333
|
+
const searchResults = semanticGroups.flat();
|
|
334
|
+
return {
|
|
335
|
+
queriedBy,
|
|
336
|
+
searchResults,
|
|
337
|
+
fallbackReason,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
queriedBy = QUERIED_BY.KEYWORD;
|
|
342
|
+
fallbackReason = FALLBACK_REASON.SEMANTIC_ERROR;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const keywordGroups = await Promise.all(searchTypes.map((type) => this.repository.search(query, type)));
|
|
346
|
+
return {
|
|
347
|
+
queriedBy,
|
|
348
|
+
searchResults: keywordGroups
|
|
349
|
+
.flat()
|
|
350
|
+
.map((symbol) => ({ symbol, similarity: 0 })),
|
|
351
|
+
fallbackReason,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* 对排名靠前的候选项进行详情补查
|
|
356
|
+
*/
|
|
357
|
+
async enrichTopCandidatesWithDetail(ranked) {
|
|
358
|
+
const topSymbols = ranked
|
|
359
|
+
.slice(0, ENRICH_TOP_K)
|
|
360
|
+
.map((item) => item.symbol);
|
|
361
|
+
if (topSymbols.length === 0) {
|
|
362
|
+
return { ranked, enrichedCount: 0 };
|
|
363
|
+
}
|
|
364
|
+
const detailMap = new Map();
|
|
365
|
+
await Promise.all(topSymbols.map(async (symbol) => {
|
|
366
|
+
try {
|
|
367
|
+
const detail = await this.repository.getByName(symbol.name);
|
|
368
|
+
if (detail && detail.id === symbol.id) {
|
|
369
|
+
detailMap.set(symbol.id, detail);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
// 详情补查失败时继续主流程,避免影响推荐输出。
|
|
374
|
+
}
|
|
375
|
+
}));
|
|
376
|
+
if (detailMap.size === 0) {
|
|
377
|
+
return { ranked, enrichedCount: 0 };
|
|
378
|
+
}
|
|
379
|
+
const enriched = ranked.map((item) => {
|
|
380
|
+
const detail = detailMap.get(item.symbol.id);
|
|
381
|
+
return detail ? { ...item, symbol: detail } : item;
|
|
382
|
+
});
|
|
383
|
+
return {
|
|
384
|
+
ranked: enriched,
|
|
385
|
+
enrichedCount: detailMap.size,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Agent 主循环:根据输入生成 query 变体,依次尝试多轮检索,融合语义/结构/关键词,Top-K 详情补查,质量门控,优先级调整。
|
|
390
|
+
* 命中即返回推荐,否则遍历所有变体,最终输出 debug trace。
|
|
391
|
+
*/
|
|
59
392
|
async recommendComponent(input) {
|
|
393
|
+
this.logStart(input);
|
|
394
|
+
const { requiredProps, requiredHooks, structureFields, searchTypes, preferSemantic, limit, queryVariants, } = this.preprocessInput(input);
|
|
395
|
+
let queriedBy = preferSemantic
|
|
396
|
+
? QUERIED_BY.SEMANTIC
|
|
397
|
+
: QUERIED_BY.KEYWORD;
|
|
398
|
+
let lastRankedCandidates = [];
|
|
399
|
+
let lastCombinedCount = 0;
|
|
400
|
+
let selectedQuery = null;
|
|
401
|
+
let fallbackReason = null;
|
|
402
|
+
const attempts = [];
|
|
403
|
+
this.logSearchTypes(searchTypes);
|
|
404
|
+
for (const queryVariant of queryVariants) {
|
|
405
|
+
const { attempt, combined, searchResults, gathered } = await this.tryQueryVariant({
|
|
406
|
+
queryVariant,
|
|
407
|
+
input,
|
|
408
|
+
searchTypes,
|
|
409
|
+
preferSemantic,
|
|
410
|
+
limit,
|
|
411
|
+
structureFields,
|
|
412
|
+
requiredProps,
|
|
413
|
+
requiredHooks,
|
|
414
|
+
});
|
|
415
|
+
queriedBy = gathered.queriedBy;
|
|
416
|
+
if (!fallbackReason && gathered.fallbackReason) {
|
|
417
|
+
fallbackReason = gathered.fallbackReason;
|
|
418
|
+
}
|
|
419
|
+
lastCombinedCount = combined.length;
|
|
420
|
+
this.logAttemptCheckpoint('attempt.summary', attempt);
|
|
421
|
+
if (combined.length === 0) {
|
|
422
|
+
attempt.skippedReason = SKIPPED_REASON.NO_COMBINED;
|
|
423
|
+
this.logAttemptCheckpoint('attempt.skipped.no_combined', attempt);
|
|
424
|
+
attempts.push(attempt);
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
const candidates = await this.rankAndEnrichCandidates({
|
|
428
|
+
combined,
|
|
429
|
+
searchResults,
|
|
430
|
+
queryVariant,
|
|
431
|
+
queriedBy,
|
|
432
|
+
requiredProps,
|
|
433
|
+
requiredHooks,
|
|
434
|
+
attempt,
|
|
435
|
+
limit,
|
|
436
|
+
});
|
|
437
|
+
lastRankedCandidates = candidates;
|
|
438
|
+
if (candidates.length > 0) {
|
|
439
|
+
selectedQuery = queryVariant;
|
|
440
|
+
attempts.push(attempt);
|
|
441
|
+
this.logAttemptCheckpoint('attempt.success', attempt);
|
|
442
|
+
this.logAttemptsTrace('recommendComponent.result.found', {
|
|
443
|
+
selectedQuery,
|
|
444
|
+
queriedBy,
|
|
445
|
+
attempts,
|
|
446
|
+
fallbackReason,
|
|
447
|
+
});
|
|
448
|
+
return this.buildResult({
|
|
449
|
+
recommended: candidates[0] ?? null,
|
|
450
|
+
alternatives: candidates.slice(1, limit),
|
|
451
|
+
queriedBy,
|
|
452
|
+
requiredProps,
|
|
453
|
+
requiredHooks,
|
|
454
|
+
attempts,
|
|
455
|
+
selectedQuery,
|
|
456
|
+
fallbackReason,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
this.logAttemptCheckpoint('attempt.no_candidate_after_rank', attempt);
|
|
460
|
+
attempts.push(attempt);
|
|
461
|
+
}
|
|
462
|
+
this.logAttemptsTrace('recommendComponent.result.not_found', {
|
|
463
|
+
selectedQuery,
|
|
464
|
+
queriedBy,
|
|
465
|
+
attempts,
|
|
466
|
+
fallbackReason,
|
|
467
|
+
});
|
|
468
|
+
return this.buildResult({
|
|
469
|
+
recommended: null,
|
|
470
|
+
alternatives: [],
|
|
471
|
+
queriedBy,
|
|
472
|
+
requiredProps,
|
|
473
|
+
requiredHooks,
|
|
474
|
+
attempts,
|
|
475
|
+
selectedQuery,
|
|
476
|
+
fallbackReason,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
logStart(input) {
|
|
480
|
+
console.error('[code-intelligence-mcp] recommendComponent.start query=%s category=%s semantic=%s limit=%s requiredProps=%s requiredHooks=%s', input.query, input.category ?? '', String(input.semantic ?? true), String(input.limit ?? 5), JSON.stringify(input.requiredProps ?? []), JSON.stringify(input.requiredHooks ?? []));
|
|
481
|
+
}
|
|
482
|
+
logSearchTypes(searchTypes) {
|
|
483
|
+
console.error('[code-intelligence-mcp] recommendComponent.searchTypes types=%s', JSON.stringify(searchTypes));
|
|
484
|
+
}
|
|
485
|
+
preprocessInput(input) {
|
|
60
486
|
const requiredProps = uniqueStrings(input.requiredProps);
|
|
61
487
|
const requiredHooks = uniqueStrings(input.requiredHooks);
|
|
62
488
|
const structureFields = uniqueStrings([
|
|
63
489
|
...requiredProps,
|
|
64
490
|
...requiredHooks,
|
|
65
491
|
]);
|
|
492
|
+
const searchTypes = inferSearchTypes(input);
|
|
66
493
|
const preferSemantic = input.semantic ?? true;
|
|
67
494
|
const limit = input.limit ?? 5;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
495
|
+
const queryVariants = buildQueryVariants(input.query).slice(0, MAX_QUERY_VARIANTS);
|
|
496
|
+
const res = {
|
|
497
|
+
requiredProps,
|
|
498
|
+
requiredHooks,
|
|
499
|
+
structureFields,
|
|
500
|
+
searchTypes,
|
|
501
|
+
preferSemantic,
|
|
502
|
+
limit,
|
|
503
|
+
queryVariants,
|
|
504
|
+
};
|
|
505
|
+
console.error('[code-intelligence-mcp] recommendComponent.preprocess queryVariants=%s requiredProps=%s requiredHooks=%s structureFields=%s searchTypes=%s preferSemantic=%s limit=%s', JSON.stringify(queryVariants), JSON.stringify(requiredProps), JSON.stringify(requiredHooks), JSON.stringify(structureFields), JSON.stringify(searchTypes), String(preferSemantic), String(limit));
|
|
506
|
+
return res;
|
|
507
|
+
}
|
|
508
|
+
async tryQueryVariant({ queryVariant, input, searchTypes, preferSemantic, limit, structureFields, requiredProps, requiredHooks, }) {
|
|
509
|
+
const gathered = await this.gatherSearchResults(queryVariant, searchTypes, preferSemantic, limit);
|
|
510
|
+
const searchResults = gathered.searchResults;
|
|
511
|
+
const attempt = {
|
|
512
|
+
query: queryVariant,
|
|
513
|
+
queriedBy: gathered.queriedBy,
|
|
514
|
+
searchCount: searchResults.length,
|
|
515
|
+
structureCount: 0,
|
|
516
|
+
combinedCount: 0,
|
|
517
|
+
qualifiedCount: 0,
|
|
518
|
+
detailEnrichedCount: 0,
|
|
519
|
+
};
|
|
87
520
|
const structureResults = structureFields.length
|
|
88
|
-
? await this.repository.searchByStructure(structureFields, {
|
|
89
|
-
type
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
})
|
|
521
|
+
? (await Promise.all(searchTypes.map((type) => this.repository.searchByStructure(structureFields, {
|
|
522
|
+
type,
|
|
523
|
+
limit: Math.max(limit * STRUCTURE_LIMIT_MULTIPLIER, STRUCTURE_LIMIT_MIN),
|
|
524
|
+
})))).flat()
|
|
93
525
|
: [];
|
|
94
|
-
|
|
526
|
+
attempt.structureCount = structureResults.length;
|
|
527
|
+
const mergedBeforeCategory = mergeCandidates([
|
|
95
528
|
...structureResults,
|
|
96
529
|
...searchResults.map((item) => item.symbol),
|
|
97
|
-
])
|
|
530
|
+
]);
|
|
531
|
+
let combined = mergedBeforeCategory.filter((symbol) => input.category
|
|
98
532
|
? (symbol.category ?? '')
|
|
99
533
|
.toLowerCase()
|
|
100
534
|
.includes(input.category.toLowerCase())
|
|
101
535
|
: true);
|
|
102
|
-
if (combined.length === 0
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
},
|
|
111
|
-
message: '未找到符合条件的可复用组件。',
|
|
112
|
-
};
|
|
536
|
+
if (combined.length === 0 &&
|
|
537
|
+
input.category &&
|
|
538
|
+
mergedBeforeCategory.length) {
|
|
539
|
+
combined = mergedBeforeCategory;
|
|
540
|
+
}
|
|
541
|
+
const reusableCandidates = combined.filter(isReusableCandidate);
|
|
542
|
+
if (reusableCandidates.length > 0) {
|
|
543
|
+
combined = reusableCandidates;
|
|
113
544
|
}
|
|
114
|
-
|
|
545
|
+
attempt.combinedCount = combined.length;
|
|
546
|
+
return { attempt, combined, searchResults, gathered };
|
|
547
|
+
}
|
|
548
|
+
async rankAndEnrichCandidates({ combined, searchResults, queryVariant, queriedBy, requiredProps, requiredHooks, attempt, limit, }) {
|
|
549
|
+
const ranked = queriedBy === QUERIED_BY.SEMANTIC
|
|
115
550
|
? rankSemanticHits(combined.map((symbol) => ({
|
|
116
551
|
symbol,
|
|
117
552
|
similarity: searchResults.find((item) => item.symbol.id === symbol.id)?.similarity ?? 0.55,
|
|
118
|
-
})))
|
|
119
|
-
: rankSymbols(
|
|
120
|
-
const
|
|
553
|
+
})), queryVariant)
|
|
554
|
+
: rankSymbols(queryVariant, combined);
|
|
555
|
+
const enriched = await this.enrichTopCandidatesWithDetail(ranked);
|
|
556
|
+
const enrichedRanked = enriched.ranked;
|
|
557
|
+
attempt.detailEnrichedCount = enriched.enrichedCount;
|
|
558
|
+
const qualifiedRanked = enrichedRanked.filter((item) => isStrongEnoughRecommendation(item, queryVariant, queriedBy, requiredProps, requiredHooks));
|
|
559
|
+
attempt.qualifiedCount = qualifiedRanked.length;
|
|
560
|
+
if (qualifiedRanked.length === 0) {
|
|
561
|
+
attempt.skippedReason = SKIPPED_REASON.NO_QUALIFIED;
|
|
562
|
+
}
|
|
563
|
+
const prioritizedRanked = qualifiedRanked
|
|
564
|
+
.map((item) => {
|
|
565
|
+
const adjusted = computeRecommendationPriority(item, queryVariant);
|
|
566
|
+
return {
|
|
567
|
+
item,
|
|
568
|
+
adjustedScore: adjusted.score,
|
|
569
|
+
adjustedReason: adjusted.reason,
|
|
570
|
+
};
|
|
571
|
+
})
|
|
572
|
+
.sort((a, b) => b.adjustedScore - a.adjustedScore);
|
|
573
|
+
const candidates = prioritizedRanked.map((entry) => toCandidate(entry.item.symbol, entry.adjustedScore, entry.adjustedReason, requiredProps, requiredHooks));
|
|
574
|
+
console.error('[code-intelligence-mcp] recommendComponent.rank query=%s queriedBy=%s enriched=%s qualified=%s candidates=%s', queryVariant, queriedBy, String(enrichedRanked.length), String(qualifiedRanked.length), String(candidates.length));
|
|
575
|
+
return candidates;
|
|
576
|
+
}
|
|
577
|
+
logAttemptCheckpoint(stage, attempt) {
|
|
578
|
+
console.error('[code-intelligence-mcp] recommendComponent.%s query=%s queriedBy=%s search=%s structure=%s combined=%s qualified=%s enriched=%s skipped=%s', stage, attempt.query, attempt.queriedBy, String(attempt.searchCount), String(attempt.structureCount), String(attempt.combinedCount), String(attempt.qualifiedCount), String(attempt.detailEnrichedCount), attempt.skippedReason ?? 'none');
|
|
579
|
+
}
|
|
580
|
+
logAttemptsTrace(stage, payload) {
|
|
581
|
+
console.error('[code-intelligence-mcp] %s selectedQuery=%s queriedBy=%s attempts=%s fallbackReason=%s', stage, payload.selectedQuery ?? 'none', payload.queriedBy, JSON.stringify(payload.attempts), payload.fallbackReason ?? 'none');
|
|
582
|
+
}
|
|
583
|
+
buildResult({ recommended, alternatives, queriedBy, requiredProps, requiredHooks, attempts, selectedQuery, fallbackReason, }) {
|
|
121
584
|
return {
|
|
122
|
-
recommended
|
|
123
|
-
alternatives
|
|
585
|
+
recommended,
|
|
586
|
+
alternatives,
|
|
124
587
|
queriedBy,
|
|
125
588
|
structureFilter: {
|
|
126
589
|
requiredProps,
|
|
127
590
|
requiredHooks,
|
|
128
591
|
},
|
|
129
|
-
message:
|
|
130
|
-
?
|
|
131
|
-
:
|
|
592
|
+
message: recommended !== null
|
|
593
|
+
? RECOMMENDATION_MESSAGE.FOUND
|
|
594
|
+
: RECOMMENDATION_MESSAGE.NOT_FOUND,
|
|
595
|
+
debug: {
|
|
596
|
+
attempts,
|
|
597
|
+
selectedQuery,
|
|
598
|
+
retryUsed: attempts.length > 1,
|
|
599
|
+
fallbackReason,
|
|
600
|
+
},
|
|
132
601
|
};
|
|
133
602
|
}
|
|
134
603
|
}
|
package/dist/services/reindex.js
CHANGED
|
@@ -10,6 +10,26 @@ import { computeFileHash } from '../indexer/tsAstNormalizer.js';
|
|
|
10
10
|
import { getRelativePathForDisplay } from '../indexer/heuristics.js';
|
|
11
11
|
import { enqueueEmbeddingBatch, closeEmbeddingQueue, } from '../services/embeddingQueue.js';
|
|
12
12
|
import { SYMBOL_STATUS } from '../config/symbolStatus.js';
|
|
13
|
+
function isCallerDebugEnabled() {
|
|
14
|
+
return /^(1|true|yes|on)$/i.test(process.env.DEBUG_CALLERS ?? '');
|
|
15
|
+
}
|
|
16
|
+
function getCallerDebugMatch() {
|
|
17
|
+
return (process.env.DEBUG_CALLERS_MATCH ?? '').trim().toLowerCase();
|
|
18
|
+
}
|
|
19
|
+
function debugMatchedFiles(stage, files, projectRoot) {
|
|
20
|
+
if (!isCallerDebugEnabled())
|
|
21
|
+
return;
|
|
22
|
+
const match = getCallerDebugMatch();
|
|
23
|
+
const normalized = files.map((file) => getRelativePathForDisplay(projectRoot, file));
|
|
24
|
+
const matched = match
|
|
25
|
+
? normalized.filter((file) => file.toLowerCase().includes(match))
|
|
26
|
+
: normalized;
|
|
27
|
+
console.error(`[callers.debug] ${stage} ${JSON.stringify({
|
|
28
|
+
match: match || null,
|
|
29
|
+
count: matched.length,
|
|
30
|
+
files: matched,
|
|
31
|
+
})}`);
|
|
32
|
+
}
|
|
13
33
|
export async function runReindex(options = {}) {
|
|
14
34
|
const projectRoot = resolve(options.projectRoot ?? process.cwd());
|
|
15
35
|
const { dryRun = false, forceRebuild = false } = options;
|
|
@@ -35,6 +55,7 @@ export async function runReindex(options = {}) {
|
|
|
35
55
|
dot: false,
|
|
36
56
|
});
|
|
37
57
|
console.error(`[reindex] glob found ${allFiles.length} file(s)`);
|
|
58
|
+
debugMatchedFiles('reindex-all-files', allFiles, projectRoot);
|
|
38
59
|
// ─── 2. file_hash 过滤:跳过 AST 未变的文件(CPU 优化)────────────────
|
|
39
60
|
// forceRebuild 时跳过此过滤,file_hash 不可复用(模板/模型变更时相同文件产出不同 content)
|
|
40
61
|
let filesToIndex = allFiles;
|
|
@@ -58,9 +79,13 @@ export async function runReindex(options = {}) {
|
|
|
58
79
|
});
|
|
59
80
|
skippedFiles = allFiles.length - filesToIndex.length;
|
|
60
81
|
console.error(`[reindex] file_hash: ${skippedFiles} unchanged (skipped), ${filesToIndex.length} changed (to parse)`);
|
|
82
|
+
const skippedAbsFiles = allFiles.filter((absPath) => !filesToIndex.includes(absPath));
|
|
83
|
+
debugMatchedFiles('reindex-files-to-parse', filesToIndex, projectRoot);
|
|
84
|
+
debugMatchedFiles('reindex-files-skipped', skippedAbsFiles, projectRoot);
|
|
61
85
|
}
|
|
62
86
|
else if (forceRebuild) {
|
|
63
87
|
console.error(`[reindex] forceRebuild=true, skipping file_hash filter — parsing all ${allFiles.length} file(s)`);
|
|
88
|
+
debugMatchedFiles('reindex-files-to-parse', filesToIndex, projectRoot);
|
|
64
89
|
}
|
|
65
90
|
if (filesToIndex.length === 0) {
|
|
66
91
|
console.error('[reindex] all files unchanged, nothing to do');
|
|
@@ -7,7 +7,8 @@ export function createGetSymbolDetailTool(repository) {
|
|
|
7
7
|
name: 'get_symbol_detail',
|
|
8
8
|
description: '获取单个代码块的完整详情(含源码、参数类型、调用关系、副作用)。\n' +
|
|
9
9
|
'仅在以下情况调用:search_symbols 返回的摘要信息不足以判断是否适用(如签名模糊、副作用不明确)。\n' +
|
|
10
|
-
'通常对 top 1-3
|
|
10
|
+
'通常对 top 1-3 候选调用,不要对所有结果批量调用。\n' +
|
|
11
|
+
'约束:若 recommend_component 已返回结果,默认不要调用本工具;仅在用户明确要求“看源码细节/二次验证”时调用。',
|
|
11
12
|
inputSchema: getSymbolDetailInput.shape,
|
|
12
13
|
handler: async (input) => {
|
|
13
14
|
const symbol = await repository.getByName(input.name);
|