@lorrylurui/code-intelligence-mcp 2.0.4 → 2.0.5

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 CHANGED
@@ -10,6 +10,14 @@
10
10
  - Prompt: `reusable-code-advisor`
11
11
  - Cursor Skill:`reusable-code-advisor`(`.cursor/skills/reusable-code-advisor/`,
12
12
 
13
+ ## 开发
14
+
15
+ ```
16
+ 1. npm run dev:mcp 启动mcp server
17
+ 2. npm run embedding:dev 启动本地python环境
18
+ 3. npm run worker:embedding 启动worker队列
19
+ ```
20
+
13
21
  ## 1) 配置mcp servers
14
22
 
15
23
  ```
@@ -37,73 +45,3 @@
37
45
 
38
46
  MYSQL\*SYMBOLS_TABLE=frontend_collections_symbols
39
47
  INDEX_GLOB=xxx/\*\*/\_.{js,jsx,ts,tsx}
40
-
41
- # 待优化项
42
-
43
- 修复优先级:
44
- ✅8
45
- ✅5
46
- ✅134 done,但是第二层embedding需要优化,llm fallback太慢+漂移,也需要调整模型
47
- ✅6
48
- ✅27
49
-
50
- 1. meta里面有多个信息,哪些做结构化过滤,哪些做向量检索?
51
- 结论:ast normalizers后拼一个语义模板,用这个模板内容生成向量
52
- ❓做法见qa-doc/semantic-phrase.md
53
- 2. 对于 class类型,content字段保留关键方法或摘要,而不是完全为空
54
- 最新修改:content赋值为语义模板
55
- 3. category过于模糊,
56
- 这三层怎么做:category 优先使用规则和 embedding 分类,
57
- LLM 只作为 fallback,避免不稳定和成本问题
58
- ❓做法见qa-doc/category.md
59
- 4. type category meta.kind 字段是否多余了?type只有5个值,
60
- type表达代码结构、category表达语义结构,kind?
61
- type: function / component / hook / class / type / interface
62
- category:最新的三层结构(还没实现,只有文档)
63
- kind: 现在跟type重叠较多,建议弱化meta.kind → 只保留特殊情况:
64
- ❓改造方法qa-doc/type-category-kind.md
65
- 5. 在ci做增量索引时,把changed files,如果是1000+文件,性能爆炸,考虑用file hash 判断?embedding也没有优化缓存?
66
- ❓见qa-doc/ci-hash-solution 方案:🥈 file hash + ast normalizer hash,新增semantic_hash
67
- - CI 增量(git changed files 触发)
68
- 只需要 semantic_hash
69
- file_hash 可省,因为文件必然变了
70
- - 每日全量扫描
71
- file_hash 用来跳过 AST 解析(CPU 优化)
72
- semantic_hash 用来跳过 embedding(费用优化)
73
- content_hash 删掉,职责完全被 semantic_hash 覆盖
74
- 6. 大仓问题:
75
- ❓big-repo.md
76
- - ci embedding解耦,新增embedding_status, ci时,全量写入status='pending'-> ci finish
77
- - ci如果检测到文件删除,则对被删除的代码块标记delete(这里需要新增字段)
78
- - node+redis 消费写embedding job
79
- - 对语义模板semantic_hash做向量缓存,semantic_hash相同即功能未变
80
- - 大仓分片并行
81
- 7. content暂时用不到,但也不用删除,目前暴利截取4000字符需要优化:
82
- content(降级为辅助字段):✔ 不参与 embedding✔ 不参与排序✔ 不参与过滤✔ 用于:1. LLM改造建议 2.debug 3.future rerank
83
- 最简单:只存 signature
84
- 最优:content = {
85
- signature: "function fetchData(url, options)",
86
- snippet: "核心逻辑代码(<=300行)",
87
- keyCalls: ["fetch", "cache"]
88
- }
89
- 8. TopK???,首先去掉usage过滤,再做两次topk,1.根据余弦相似度选topk 2.对1的结果用现有的usage,updated_at等加权排序
90
- ❓topK.md
91
- 现在:SQL过滤(type) → ORDER BY usage_count DESC LIMIT 3000→ embedding 相似度排序→ 取 top20
92
- 这个逻辑不对,导致query: "debounce function",debounce 使用少 ❌ fetch 很热门 ✅,结果Top3000里全是 fetch / request, debounce 被过滤掉 ❌
93
-
94
- 👉 优点:
95
- • 不阻塞 CI
96
- • 可扩展
97
-
98
- 6. 大仓问题呢?
99
-
100
- # 简历里还没做的优化
101
-
102
- 1. embedding基石 - 语义模板模板,使用ast数据拼装语义模板
103
- 2. class的content为null
104
- 3. category分层 1.规则 2.预设所有种类,使用embedding召回 3.llm兜底
105
- 4. type meta.kind逻辑优化,现在太重叠了
106
- 5. ci-hash-solution
107
- 6. 大仓问题
108
- 7. content优化
109
- 8. ✅topk优化
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ async function main() {
6
6
  // 加载第三方项目的 .env(通过 INDEX_ROOT 指定,或默认当前工作目录)
7
7
  const projectRoot = process.env.INDEX_ROOT || process.cwd();
8
8
  loadProjectDotenv(projectRoot);
9
+ console.error('[code-intelligence-mcp] env.loaded env.projectRoot=%s', projectRoot);
9
10
  const server = createServer();
10
11
  const transport = new StdioServerTransport();
11
12
  await server.connect(transport);
@@ -357,6 +357,7 @@ function createRowFromFunction(name, decl, filePath, projectRoot, isJsx) {
357
357
  function createRowFromClass(name, _decl, filePath, projectRoot) {
358
358
  const relPath = getRelativePathForDisplay(projectRoot, filePath);
359
359
  const category = inferCategoryFromPath(filePath);
360
+ const jsdoc = parseJsDocInfo(_decl);
360
361
  // 大写开头的类视为组件
361
362
  const type = /^[A-Z]/.test(name) ? 'component' : 'function';
362
363
  return {
@@ -364,7 +365,7 @@ function createRowFromClass(name, _decl, filePath, projectRoot) {
364
365
  type,
365
366
  category,
366
367
  path: relPath,
367
- description: null,
368
+ description: jsdoc.description,
368
369
  // content meta.kind 暂时废弃不用,
369
370
  content: null,
370
371
  meta: {},
@@ -3,7 +3,9 @@
3
3
  * 同时分析调用关系,填充 meta.callers / meta.callees。
4
4
  */
5
5
  import fg from 'fast-glob';
6
- import { join, resolve } from 'node:path';
6
+ import * as babelParser from '@babel/parser';
7
+ import * as bt from '@babel/types';
8
+ import { dirname, join, resolve } from 'node:path';
7
9
  import { Node, Project } from 'ts-morph';
8
10
  import { readFileSync, existsSync } from 'node:fs';
9
11
  import { extractInterfaceOrTypeMeta, extractMetaFromCallable, } from './extractMeta.js';
@@ -12,6 +14,40 @@ import { parseJsFile } from './babelParser.js';
12
14
  import { isParamPlaceholder } from './paramPlaceholder.js';
13
15
  import { computeFileHash, computeSemanticHash } from './tsAstNormalizer.js';
14
16
  const CALLERS_LIMIT = 20;
17
+ const BABEL_PLUGINS = [
18
+ 'jsx',
19
+ 'typescript',
20
+ 'classPrivateMethods',
21
+ 'classPrivateProperties',
22
+ 'decorators-legacy',
23
+ 'doExpressions',
24
+ 'exportDefaultFrom',
25
+ 'functionBind',
26
+ 'logicalAssignment',
27
+ 'nullishCoalescingOperator',
28
+ 'objectRestSpread',
29
+ 'optionalChaining',
30
+ 'optionalCatchBinding',
31
+ ];
32
+ function isCallerDebugEnabled() {
33
+ return /^(1|true|yes|on)$/i.test(process.env.DEBUG_CALLERS ?? '');
34
+ }
35
+ function getCallerDebugMatch() {
36
+ return (process.env.DEBUG_CALLERS_MATCH ?? '').trim().toLowerCase();
37
+ }
38
+ function shouldLogCallerDebug(parts) {
39
+ if (!isCallerDebugEnabled())
40
+ return false;
41
+ const match = getCallerDebugMatch();
42
+ if (!match)
43
+ return true;
44
+ return parts.some((part) => part?.toLowerCase().includes(match));
45
+ }
46
+ function logCallerDebug(stage, payload, parts) {
47
+ if (!shouldLogCallerDebug(parts))
48
+ return;
49
+ console.error(`[callers.debug] ${stage} ${JSON.stringify(payload)}`);
50
+ }
15
51
  /** 判断文件类型 */
16
52
  function isJsFile(filePath) {
17
53
  return filePath.endsWith('.js') || filePath.endsWith('.jsx');
@@ -30,9 +66,9 @@ function mergeCallableMeta(symbolType, raw) {
30
66
  const paramTypeFields = raw.paramTypeFields;
31
67
  const { params: _p, paramTypeFields: _f, ...rest } = raw;
32
68
  if (symbolType === 'component' && params?.length) {
33
- const props = [
34
- ...new Set([...(paramTypeFields ?? []), ...params]),
35
- ].filter((name) => name.toLowerCase() !== 'props' && !isParamPlaceholder(name)).sort();
69
+ const props = [...new Set([...(paramTypeFields ?? []), ...params])]
70
+ .filter((name) => name.toLowerCase() !== 'props' && !isParamPlaceholder(name))
71
+ .sort();
36
72
  return { ...rest, ...(props.length ? { props } : {}) };
37
73
  }
38
74
  return { ...rest, ...(params?.length ? { params } : {}) };
@@ -198,6 +234,142 @@ export const DEFAULT_IGNORE = [
198
234
  '**/dist-srv/**',
199
235
  '**/.turbo/**',
200
236
  ];
237
+ function resolveImportedSourceFile(currentFilePath, moduleSpecifier) {
238
+ if (!moduleSpecifier.startsWith('.'))
239
+ return null;
240
+ const base = resolve(dirname(currentFilePath), moduleSpecifier);
241
+ const candidates = [
242
+ base,
243
+ `${base}.ts`,
244
+ `${base}.tsx`,
245
+ `${base}.js`,
246
+ `${base}.jsx`,
247
+ join(base, 'index.ts'),
248
+ join(base, 'index.tsx'),
249
+ join(base, 'index.js'),
250
+ join(base, 'index.jsx'),
251
+ ];
252
+ for (const candidate of candidates) {
253
+ if (existsSync(candidate))
254
+ return candidate;
255
+ }
256
+ return null;
257
+ }
258
+ function applyRelationMaps(rows, callersMap, calleesMap) {
259
+ for (const row of rows) {
260
+ const key = `${row.name}|${row.path}`;
261
+ const callers = callersMap.get(key);
262
+ const callees = calleesMap.get(key);
263
+ if (callers?.size) {
264
+ row.meta.callers = [...callers].slice(0, CALLERS_LIMIT).map((s) => {
265
+ const [name, ...pathParts] = s.split('|');
266
+ return { name, path: pathParts.join('|') };
267
+ });
268
+ }
269
+ if (callees?.size) {
270
+ row.meta.callees = [...callees].slice(0, CALLERS_LIMIT).map((s) => {
271
+ const [name, ...pathParts] = s.split('|');
272
+ return { name, path: pathParts.join('|') };
273
+ });
274
+ }
275
+ }
276
+ }
277
+ function analyzeJsRelations(jsFiles, symbolMap, projectRoot, rows) {
278
+ const callersMap = new Map();
279
+ const calleesMap = new Map();
280
+ for (const filePath of jsFiles) {
281
+ const relPath = getRelativePathForDisplay(projectRoot, filePath);
282
+ const callerRows = rows.filter((row) => row.path === relPath);
283
+ if (callerRows.length === 0)
284
+ continue;
285
+ logCallerDebug('js-file-start', {
286
+ filePath,
287
+ relPath,
288
+ callerRows: callerRows.map((row) => row.name),
289
+ }, [filePath, relPath, ...callerRows.map((row) => row.name)]);
290
+ let ast;
291
+ try {
292
+ ast = babelParser.parse(readFileSync(filePath, 'utf-8'), {
293
+ sourceType: 'module',
294
+ plugins: [...BABEL_PLUGINS],
295
+ strictMode: false,
296
+ });
297
+ }
298
+ catch (error) {
299
+ console.error(`[analyzeJsRelations] Failed to parse ${filePath}:`, error);
300
+ continue;
301
+ }
302
+ for (const stmt of ast.program.body) {
303
+ if (!bt.isImportDeclaration(stmt))
304
+ continue;
305
+ const moduleSpecifier = stmt.source.value;
306
+ if (typeof moduleSpecifier !== 'string')
307
+ continue;
308
+ const importedFile = resolveImportedSourceFile(filePath, moduleSpecifier);
309
+ logCallerDebug('js-import-resolve', {
310
+ from: relPath,
311
+ moduleSpecifier,
312
+ importedFile,
313
+ }, [relPath, moduleSpecifier, importedFile]);
314
+ if (!importedFile)
315
+ continue;
316
+ const importedRelPath = getRelativePathForDisplay(projectRoot, importedFile);
317
+ const importedNames = stmt.specifiers
318
+ .map((spec) => {
319
+ if (bt.isImportSpecifier(spec)) {
320
+ return bt.isIdentifier(spec.imported)
321
+ ? spec.imported.name
322
+ : spec.imported.value;
323
+ }
324
+ if (bt.isImportDefaultSpecifier(spec)) {
325
+ return spec.local.name;
326
+ }
327
+ return null;
328
+ })
329
+ .filter((name) => Boolean(name));
330
+ logCallerDebug('js-import-specifiers', {
331
+ from: relPath,
332
+ moduleSpecifier,
333
+ importedRelPath,
334
+ importedNames,
335
+ }, [relPath, moduleSpecifier, importedRelPath, ...importedNames]);
336
+ for (const callerRow of callerRows) {
337
+ const callerKey = `${callerRow.name}|${callerRow.path}`;
338
+ for (const importedName of importedNames) {
339
+ const targetKey = `${importedName}|${importedRelPath}`;
340
+ const target = symbolMap.get(targetKey);
341
+ logCallerDebug('js-import-target', {
342
+ callerKey,
343
+ targetKey,
344
+ hit: Boolean(target),
345
+ availableKeysInFile: [...symbolMap.keys()].filter((key) => key.endsWith(`|${importedRelPath}`)),
346
+ }, [callerKey, targetKey, importedRelPath, importedName]);
347
+ if (!target)
348
+ continue;
349
+ const calleeSet = calleesMap.get(callerKey) || new Set();
350
+ calleeSet.add(`${target.name}|${target.path}`);
351
+ calleesMap.set(callerKey, calleeSet);
352
+ const callerSet = callersMap.get(`${target.name}|${target.path}`) ||
353
+ new Set();
354
+ callerSet.add(callerKey);
355
+ callersMap.set(`${target.name}|${target.path}`, callerSet);
356
+ logCallerDebug('js-link-added', {
357
+ callerKey,
358
+ calleeKey: `${target.name}|${target.path}`,
359
+ }, [callerKey, target.name, target.path]);
360
+ }
361
+ }
362
+ }
363
+ }
364
+ applyRelationMaps(rows, callersMap, calleesMap);
365
+ for (const row of rows) {
366
+ logCallerDebug('js-row-final', {
367
+ row: `${row.name}|${row.path}`,
368
+ callers: row.meta.callers ?? [],
369
+ callees: row.meta.callees ?? [],
370
+ }, [row.name, row.path]);
371
+ }
372
+ }
201
373
  /**
202
374
  * 按 glob 收集文件,用 ts-morph 加载并遍历每个文件的导出,生成全部代码块行。
203
375
  * 同时分析调用关系,填充 meta.callers / meta.callees。
@@ -269,6 +441,10 @@ export async function indexProject(opts) {
269
441
  });
270
442
  }
271
443
  symbolMap.get(key).exports.add(exportName);
444
+ logCallerDebug('symbol-map-add-ts', {
445
+ key,
446
+ exportName,
447
+ }, [row.name, row.path, exportName]);
272
448
  }
273
449
  }
274
450
  }
@@ -291,6 +467,10 @@ export async function indexProject(opts) {
291
467
  });
292
468
  }
293
469
  symbolMap.get(key).exports.add(row.name);
470
+ logCallerDebug('symbol-map-add-js', {
471
+ key,
472
+ exportName: row.name,
473
+ }, [row.name, row.path]);
294
474
  }
295
475
  }
296
476
  catch (e) {
@@ -307,6 +487,9 @@ export async function indexProject(opts) {
307
487
  project.addSourceFilesAtPaths(tsFiles);
308
488
  analyzeRelations(project, symbolMap, projectRoot, out);
309
489
  }
490
+ if (jsFiles.length > 0) {
491
+ analyzeJsRelations(jsFiles, symbolMap, projectRoot, out);
492
+ }
310
493
  return out;
311
494
  }
312
495
  /**
@@ -392,25 +575,13 @@ function analyzeRelations(project, symbolMap, projectRoot, rows) {
392
575
  }
393
576
  });
394
577
  }
395
- // 写入 rows meta
578
+ applyRelationMaps(rows, callersMap, calleesMap);
396
579
  for (const row of rows) {
397
- const key = `${row.name}|${row.path}`;
398
- const callers = callersMap.get(key);
399
- const callees = calleesMap.get(key);
400
- if (callers?.size) {
401
- row.meta.callers = [...callers].slice(0, CALLERS_LIMIT).map((s) => {
402
- // ✅ 反序列化回对象
403
- const [name, ...pathParts] = s.split('|');
404
- return { name, path: pathParts.join('|') };
405
- });
406
- }
407
- if (callees?.size) {
408
- row.meta.callees = [...callees].slice(0, CALLERS_LIMIT).map((s) => {
409
- const [name, ...pathParts] = s.split('|');
410
- // path:为了防止路径里万一含有 | 字符时截断错误。
411
- return { name, path: pathParts.join('|') };
412
- });
413
- }
580
+ logCallerDebug('ts-row-final', {
581
+ row: `${row.name}|${row.path}`,
582
+ callers: row.meta.callers ?? [],
583
+ callees: row.meta.callees ?? [],
584
+ }, [row.name, row.path]);
414
585
  }
415
586
  console.error(`[analyzeRelations] processed ${rows.length} symbols`);
416
587
  }
@@ -171,17 +171,32 @@ function traverseNode(node, cb) {
171
171
  }
172
172
  }
173
173
  export function inferBehaviorFromJS(node) {
174
- const behavior = [];
174
+ const behavior = new Set();
175
+ const add = (label) => {
176
+ behavior.add(label);
177
+ };
175
178
  // 遍历 AST
176
179
  traverseNode(node, (n) => {
177
180
  // fetch / axios
178
181
  if (t.isCallExpression(n) && t.isIdentifier(n.callee)) {
179
182
  const name = n.callee.name.toLowerCase();
180
183
  if (name.includes('fetch') || name.includes('axios')) {
181
- behavior.push('performs network request');
184
+ add('performs network request');
182
185
  }
183
186
  if (name.includes('settimeout')) {
184
- behavior.push('uses timer');
187
+ add('uses timer');
188
+ }
189
+ if (name.includes('getscroll')) {
190
+ add('tracks scroll position');
191
+ }
192
+ if (name.includes('getoffset')) {
193
+ add('computes element offset');
194
+ }
195
+ if (name.includes('throttle')) {
196
+ add('throttles position updates');
197
+ }
198
+ if (name.includes('addeventlistener')) {
199
+ add('listens to viewport events');
185
200
  }
186
201
  }
187
202
  if (t.isCallExpression(n) &&
@@ -190,17 +205,32 @@ export function inferBehaviorFromJS(node) {
190
205
  const prop = n.callee.property.name;
191
206
  if (prop === 'getBoundingClientRect' ||
192
207
  prop === 'getComputedStyle') {
193
- behavior.push('reads dom layout');
208
+ add('reads dom layout');
194
209
  }
210
+ if (prop === 'addEventListener') {
211
+ add('listens to viewport events');
212
+ }
213
+ }
214
+ if (t.isIdentifier(n)) {
215
+ const name = n.name.toLowerCase();
216
+ if (name === 'offsettop' || name === 'offsetbottom') {
217
+ add('supports offset positioning');
218
+ }
219
+ if (name === 'scrolltop') {
220
+ add('tracks scroll position');
221
+ }
222
+ }
223
+ if (t.isStringLiteral(n) && n.value.toLowerCase() === 'fixed') {
224
+ add('uses fixed positioning');
195
225
  }
196
226
  // localStorage
197
227
  if (t.isMemberExpression(n) &&
198
228
  t.isIdentifier(n.object) &&
199
229
  n.object.name === 'localStorage') {
200
- behavior.push('uses local storage');
230
+ add('uses local storage');
201
231
  }
202
232
  });
203
- return Array.from(new Set(behavior));
233
+ return [...behavior].sort();
204
234
  }
205
235
  export function extractNormalizedSignatureJS(node) {
206
236
  if (t.isFunctionDeclaration(node) || t.isFunctionExpression(node)) {
@@ -3,22 +3,30 @@ const REUSABLE_CODE_ADVISOR_DESCRIPTION = '在实现需求时检索并推荐最
3
3
  /** 与 SKILL.md 中 `# 可复用代码推荐` 起至约束、示例说明为止的正文一致(无 YAML frontmatter)。 */
4
4
  const REUSABLE_CODE_ADVISOR_MARKDOWN = `# 可复用代码推荐
5
5
 
6
- ## 工作流
7
-
8
- 当用户需要可复用代码或实现类需求时,按顺序执行:
9
-
10
- 1. 如果用户是在找可复用组件,优先调用 recommend_component,由它完成候选搜索、结构过滤和排序。
11
- 2. 只有当用户明确要求手动检索、或需要更细粒度控制时,才组合调用 search_symbols / search_by_structure / get_symbol_detail。
12
- 3. 若仅凭摘要无法判断,对最相关的若干候选调用 get_symbol_detail 获取详情。
13
- 4. 从以下维度对比候选:
14
- - 与用户需求的**功能匹配度**
15
- - **API 是否简单**、入参是否合适
16
- - **依赖与副作用**风险
17
- - **复用安全性**(稳定性、耦合度、是否便于扩展)
18
- 5. 给出**唯一首选**推荐,并说明理由,同时使用 **AskUserQuestion** 工具,提供两个选项:
19
- - 采纳推荐
20
- - 取消
21
- 6. 用户选择"采纳推荐"后,立即调用 inc_usage 工具记录该行为(symbolId 从搜索结果的 id 字段获取),不要遗漏此步骤。
6
+ ## 工作流(三级降级链,严格按序,每级有结果即终止)
7
+
8
+ ### 第一级:recommend_component(唯一首选)
9
+ 1. 用户询问可复用组件/函数/util/工具 → 必须先调用 \`recommend_component\`
10
+ 2. 返回 recommended != null → 将工具返回文本**原样输出**,**完全停止**,不得继续调用任何工具
11
+ 3. 返回 recommended = null 进入第二级
12
+
13
+ ### 第二级:search_symbols(仅在第一级无结果时)
14
+ 4. 调用 \`search_symbols\`(semantic=true,传入原始 query)
15
+ 5. 返回非空结果 → 取第一条,按固定回复结构输出,**完全停止**
16
+ 6. 返回空结果 → 进入第三级
17
+
18
+ ### 第三级:输出无结果模板
19
+ 7. 直接输出无结果固定模板,**完全停止**
20
+ 8. 禁止进行 grep、read file、file search 等文件系统操作
21
+
22
+ ## 硬性约束
23
+
24
+ - **不得跳过第一级直接调用 search_symbols**:对任何"帮我找 X""有没有 X"类问题,第一步永远是 \`recommend_component\`
25
+ - **工具返回后不得自由发挥**:输出必须以工具返回结果为唯一事实来源,按模板格式输出,禁止改写为散文或追加额外检索过程
26
+ - **禁止文件系统兜底**:MCP 三级链全部无结果后,只输出无结果模板,不读文件、不 grep
27
+ - **禁止工作区外路径**:禁止引用或读取工作区外文件(如 \`/Users/.../not_git_private/...\`)
28
+ - **禁止过程叙述**:不得输出"我先检索""Ran search_symbols""Read file"等过程描述
29
+ - 用户选择"采纳推荐"后,立即调用 \`inc_usage\`(symbolId 从结果 id 字段获取)
22
30
 
23
31
  ## 不适用场景
24
32
 
@@ -30,32 +38,53 @@ const REUSABLE_CODE_ADVISOR_MARKDOWN = `# 可复用代码推荐
30
38
  ## 搜索结果判断
31
39
 
32
40
  根据 semanticSimilarity 决定推荐置信度:
33
- - **> 0.85**:高置信度,可直接推荐
34
- - **0.6 – 0.85**:中等置信度,需结合 description 和 get_symbol_detail 综合判断
35
- - **< 0.6**:低置信度,说明可能无合适实现,明确告知用户
41
+ - **> 0.85**:高置信度,直接推荐
42
+ - **0.6 – 0.85**:中等置信度,需结合 description 综合判断
43
+ - **< 0.6**:低置信度,明确告知用户可能无合适实现
36
44
  - **空结果**:明确说"未找到已有实现",不要凭空推荐
37
45
 
38
- ## 回复结构
46
+ ## 回复结构(固定模板,不得改写)
47
+
48
+ > ⚠️ 以下模板中的所有字段值(symbolId、使用范围、副作用、理由等)**均由 \`recommend_component\` 工具返回文本中已填好**,禁止自行推断或从代码文件中读取。LLM 只需将工具返回的文本**原样复制输出**,不得改写、补全或省略任何字段。
39
49
 
40
- 按此结构输出(字段名可保留英文或改为中文小标题,二选一全文统一):
50
+ 有结果时:
41
51
 
42
- - **首选:** <代码块名>
43
- - **使用范围:** 展示 meta.callers(被哪些模块调用),用于评估稳定性和适用范围;若无调用记录则显示"新增"
44
- - **副作用:** 展示 meta.sideEffects(network/timer/dom/storage/mutation),优先推荐无副作用的方案,标注有副作用的方案风险
45
- - **理由:** 1~3 条要点
46
- - **其他候选:** 简要列出及取舍(同步标注副作用)
47
- - **用法提示:** 结合用户场景的最小集成说明
48
- - **是否采纳:** 展示两个选项:选项1. 采纳推荐 选项2. 取消。等待用户确认
52
+ 首选:<符号名> <文件路径>
53
+ 使用范围:<callers "新增">
54
+ 副作用:<sideEffects "无">
55
+ 理由:
56
+ 1. <要点1>
57
+ 2. <要点2>
58
+ 其他候选:<候选A(副作用)>; <候选B(副作用)>
59
+ 用法提示:
60
+ <最小集成示例>
61
+ 是否采纳:
62
+ 1. 采纳推荐
63
+ 2. 取消
49
64
 
50
- ## 约束
65
+ > 输出上述模板后**等待用户在聊天框输入回复**,识别规则:
66
+ > - 用户输入 **"1"、"采纳"、"采纳推荐"、"ok"、"好的"** 或类似确认词 → 从上方输出文本中读取 \`symbolId:<id>\` 那一行的值,立即调用 \`inc_usage\` 工具传入该 id,调用成功后回复"✓ 已记录使用,可直接集成"
67
+ > - 用户输入 **"2"、"取消"、"不用了"** 或类似否定词 → 回复"好的,已取消",停止
68
+ > - 用户输入其他内容(如追问细节)→ 正常回答,回答结束后再次展示"是否采纳"选项
51
69
 
52
- - 优先推荐已有可复用代码块,避免轻易建议新写一套。
53
- - 若无合适代码块,明确说明,并给出最接近的选项及差距。
54
- - 推理简洁,面向落地实现。
70
+ 无结果时:
55
71
 
56
- ## 更多示例
72
+ 首选:未找到已有实现
73
+ 使用范围:无
74
+ 副作用:无
75
+ 理由:
76
+ 1. 当前索引中没有满足条件的符号
77
+ 2. 已尝试可用检索方式,仍无可用候选
78
+ 其他候选:无
79
+ 用法提示:
80
+ // 可先创建一个最小可复用实现
81
+ 是否采纳:
82
+ 1. 让我新建一个最小可复用实现
83
+ 2. 取消
57
84
 
58
- 与仓库内 \`.cursor/skills/reusable-code-advisor/examples.md\` 中的示例一致(在 Cursor 或本地打开该文件查看)。
85
+ > 输出上述模板后**等待用户在聊天框输入回复**,识别规则:
86
+ > - 用户输入 **"1"、"新建"、"帮我创建"** 或类似确认词 → 进入新建流程,引导用户确认最小接口设计
87
+ > - 用户输入 **"2"、"取消"、"不用了"** → 回复"好的,已取消",停止
59
88
  `;
60
89
  export function registerReusableCodeAdvisorPrompt(server) {
61
90
  server.prompt('reusable-code-advisor', REUSABLE_CODE_ADVISOR_DESCRIPTION, {