@lorrylurui/code-intelligence-mcp 1.0.7 → 1.0.9

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
@@ -156,11 +156,11 @@ npm run index
156
156
 
157
157
  可选环境变量(见 `.env.example`):
158
158
 
159
- | 变量 | 含义 |
160
- | -------------- | ------------------------------------------------ |
161
- | `INDEX_ROOT` | 工程根目录,默认当前工作目录 |
162
- | `INDEX_GLOB` | 空格分隔 glob,默认 `src/**/*.{ts,tsx}` |
163
- | `INDEX_IGNORE` | 额外忽略的 glob 片段(空格分隔) |
159
+ | 变量 | 含义 |
160
+ | -------------- | --------------------------------------- |
161
+ | `INDEX_ROOT` | 工程根目录,默认当前工作目录 |
162
+ | `INDEX_GLOB` | 空格分隔 glob,默认 `src/**/*.{ts,tsx}` |
163
+ | `INDEX_IGNORE` | 额外忽略的 glob 片段(空格分隔) |
164
164
 
165
165
  **分类规则(首版启发式)**:`interface` / `type` → `type`;`.tsx` 且函数体含 JSX → `component`;路径或导出名含 `selector` → `selector`;其余导出函数 → `util`;`class` → `util`(可后续细化)。
166
166
 
@@ -293,3 +293,322 @@ Run with:
293
293
  - recommend-component
294
294
  - reusable-code-advisor
295
295
  ````
296
+
297
+ ---
298
+
299
+ Code Intelligence 功能完整总结
300
+
301
+ 项目定位
302
+
303
+ 智能代码推荐系统:解决日常团队开发中可复用逻辑(组件、样式、selectors、类型声明等)重复开发问题,提高代码复
304
+ 用率。
305
+
306
+ 一、系统架构
307
+
308
+ ┌─────────────────────────────────────────────────────────────────┐
309
+ │ MCP Client (Claude/Cursor) │
310
+ │ 用户提问 → 模型分析 → 返回结果 │
311
+ └─────────────────────────────────────────────────────────────────┘
312
+
313
+
314
+ ┌─────────────────────────────────────────────────────────────────┐
315
+ │ MCP Server (Node.js) │
316
+ ├─────────────────────────────────────────────────────────────────┤
317
+ │ Tools (4个) │ Prompts (1个) │ DB Layer │
318
+ │ - search_symbols │ - reusable-code- │ - MySQL │
319
+ │ - get_symbol_detail │ advisor │ - embedding │
320
+ │ - search_by_struct │ │ │
321
+ │ - reindex │ │ │
322
+ └─────────────────────────────────────────────────────────────────┘
323
+
324
+
325
+ ┌─────────────────────────────────────────────────────────────────┐
326
+ │ Indexer (源码解析) │
327
+ │ ts-ormorph (TS/TSX) + Babel (JS/JSX) │
328
+ └─────────────────────────────────────────────────────────────────┘
329
+
330
+
331
+ ┌─────────────────────────────────────────────────────────────────┐
332
+ │ Embedding Service (Python FastAPI) │
333
+ │ 向量化 + 语义检索 │
334
+ └─────────────────────────────────────────────────────────────────┘
335
+
336
+ 二、MCP Server Tools(4个)
337
+
338
+ 1. search_symbols 通用检索
339
+
340
+ 功能:根据 query 和 type 进行语义搜索,对结果进行权重排序
341
+
342
+ 入参:
343
+ {
344
+ query: string, // 搜索关键词
345
+ type?: 'component' | 'util' | 'selector' | 'type', // 可选
346
+ semantic?: boolean, // 是否启用语义搜索(需 embedding 服务)
347
+ ranked?: boolean, // 是否排序,默认 true
348
+ limit?: number, // 返回数量,默认 20
349
+ }
350
+
351
+ 权重排序算法(RANK_WEIGHTS):
352
+ const RANK_WEIGHTS = {
353
+ textMatch: 0.4, // 文本匹配度
354
+ usage: 0.3, // 使用频率
355
+ recency: 0.15, // 最近更新时间
356
+ commonPath: 0.15, // common 路径偏好
357
+ }
358
+
359
+ score = textScore _ 0.4 + usage _ 0.3 + recency _ 0.15 + commonPath _ 0.15
360
+
361
+ 难点:
362
+
363
+ - 多维度权重调优
364
+ - 文本匹配算法(模糊匹配 + 语义匹配)
365
+ - 冷启动时无 embedding 向量fallback 到文本匹配
366
+
367
+ ---
368
+
369
+ 2. get_symbol_detail 获取详情
370
+
371
+ 功能:根据 name 获取代码块的完整信息
372
+
373
+ 入参:
374
+ {
375
+ name: string, // 代码块名称
376
+ }
377
+
378
+ 返回:完整代码块信息包括 meta(props/params/properties/hooks)
379
+
380
+ 难点:需要从 MySQL 解析 JSON 格式的 meta 字段
381
+
382
+ ---
383
+
384
+ 3. search_by_structure 结构化搜索
385
+
386
+ 功能:通过结构化字段搜索代码块,适用于 API 形态查询
387
+
388
+ 入参:
389
+ {
390
+ fields: string[], // 结构字段,如 ['onChange', 'value']
391
+ type?: 'component' | 'util' | 'selector' | 'type',
392
+ category?: string, // 业务分类
393
+ limit?: number,
394
+ }
395
+
396
+ 匹配逻辑:匹配 symbol.meta 中的:
397
+
398
+ - props - 组件 props
399
+ - params - 函数参数
400
+ - properties - 对象属性
401
+ - hooks - React hooks
402
+
403
+ 难点:
404
+
405
+ - 不支持 LLM 向量检索,需全表扫描 + 内存过滤
406
+ - 需要在 MySQL 中存储 JSON 格式的 meta
407
+
408
+ ---
409
+
410
+ 4. reindex 重建索引
411
+
412
+ 功能:扫描源码目录,解析并写入 MySQL + 向量
413
+
414
+ 入参:
415
+ {
416
+ projectRoot?: string, // 项目根目录,默认 cwd
417
+ globPatterns?: string[], // glob 模式,默认 ['**/*.{ts,tsx}']
418
+ ignore?: string[], // 忽略目录
419
+ dryRun?: boolean, // 仅预览不写入
420
+ }
421
+
422
+ 处理流程:
423
+
424
+ 1. 收集文件(fast-glob)
425
+
426
+ 2. 分离 TS/TSX 和 JS/JSX
427
+
428
+ 3. TS/TSX:ts-ormorph 解析
429
+ JS/JSX:Babel 解析
430
+
431
+ 4. 提取 meta(props/params/properties/hooks)
432
+
433
+ 5. 写入 MySQL
434
+
435
+ 6. 写入 embedding 向量(可选)
436
+
437
+ 难点:
438
+
439
+ - ts-ormorph:需要 tsconfig.json,不存在时用默认配置
440
+ - Babel 解析:支持 JSX、TypeScript、装饰器等语法
441
+ - meta 提取:
442
+ - extractFunctionMeta:提取函数参数、返回值类型
443
+ - extractHooksFromBody:提取 useState/useEffect 等
444
+ - extractInterfaceOrTypeMeta:提取接口属性
445
+ - 路径推断 category:从路径智能推断业务分类(如 src/components/form/\* → form)
446
+ - 忽略规则:node_modules、dist、build、.git、coverage、.next、.nuxt、.venv 等
447
+
448
+ ---
449
+
450
+ 三、MCP Prompt(1个)
451
+
452
+ reusable-code-advisor 多工具编排
453
+
454
+ 功能:在实现需求时检索并推荐最合适的可复用代码
455
+
456
+ 工作流:
457
+
458
+ 1. 调用 search_symbols 检索候选,type 根据用户需求传(component/util/selector/type)
459
+ 2. 如果用户指定了结构过滤条件(props/params/properties/hooks),额外调用 search_by_structure 做结构匹配
460
+ 3. 先 search_symbols(limit=20) 拉候选,再对 Top 3 调用 get_symbol_detail 做深度判断
461
+ 4. 若仅凭签名/摘要无法判断,调用 get_symbol_detail 获取详情
462
+ 5. 从以下维度对比候选:
463
+ - 功能匹配度
464
+ - API 是否简单、入参是否合适
465
+ - 依赖与副作用风险
466
+ - 复用安全性(稳定性、耦合度、是否便于扩展)
467
+ 6. 给出唯一首选推荐,并说明理由
468
+
469
+ 返回格式:
470
+
471
+ - 首选:<代码块名>
472
+ - 理由:1~3 条要点
473
+ - 其他候选:简要列出及取舍
474
+ - 用法提示:结合用户场景的最小集成说明
475
+
476
+ 难点:多工具组合调用逻辑、意图判断
477
+
478
+ ---
479
+
480
+ 四、GitHub Actions(CI/CD 检测评论)
481
+
482
+ 1. duplicate-check 工作流
483
+
484
+ 功能:检测代码重复实现,在 PR/Commit 上自动评论
485
+
486
+ 触发条件:
487
+
488
+ - push 到 main 分支
489
+ - PR opened/synchronize/reopened
490
+
491
+ 工作流:
492
+
493
+ 1. 计算变更文件列表(git diff)
494
+ 2. 运行 detect-duplicates 脚本
495
+ 3. 生成报告(JSON + Markdown)
496
+ 4. 上传 artifact
497
+ 5. 评论到 PR 或 Commit
498
+
499
+ 评论逻辑:
500
+
501
+ - PR 事件:直接评论到 PR
502
+ - Push 事件:查找关联 PR 并评论,无关联则评论到 Commit
503
+
504
+ 发布为 GitHub Action:
505
+
506
+ - 仓库:lorrylurui/code-intelligence-check
507
+ - 第三方使用:
508
+ - uses: lorrylurui/code-intelligence-check@v1
509
+ with:
510
+ is-mock-mode: 'true' # 无需 MySQL
511
+
512
+ ---
513
+
514
+ 五、Embedding Service(Python FastAPI)
515
+
516
+ 功能
517
+
518
+ - 文本向量化:将查询和代码块转为向量(384维 MiniLM)
519
+ - 语义检索:余弦相似度计算
520
+
521
+ API
522
+
523
+ POST /embed
524
+ Body: { "texts": ["查询文本"] }
525
+ Response: { "embeddings": [[0.1, 0.2, ...]] }
526
+
527
+ GET /health
528
+
529
+ ---
530
+
531
+ 六、数据库设计
532
+
533
+ symbols 表
534
+
535
+ CREATE TABLE symbols (
536
+ id INT AUTO_INCREMENT PRIMARY KEY,
537
+ name VARCHAR(255) NOT NULL COMMENT '代码块名称',
538
+ type ENUM('component', 'util', 'selector', 'type') NOT NULL COMMENT '类型',
539
+ category VARCHAR(100) COMMENT '业务分类',
540
+ path VARCHAR(500) NOT NULL COMMENT '文件路径',
541
+ description TEXT COMMENT '描述/文档',
542
+ content LONGTEXT COMMENT '完整代码内容',
543
+ meta JSON COMMENT '结构化元信息:props/params/properties/hooks',
544
+ usage_count INT DEFAULT 0 COMMENT '使用频率',
545
+ embedding JSON NULL COMMENT '向量',
546
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
547
+ INDEX idx_type (type),
548
+ INDEX idx_category (category),
549
+ INDEX idx_usage (usage_count DESC)
550
+ );
551
+
552
+ ---
553
+
554
+ 七、环境配置
555
+
556
+ 环境变量
557
+
558
+ ┌───────────────────────┬──────────────┬───────────────────┐
559
+ │ 变量 │ 说明 │ 默认值 │
560
+ ├───────────────────────┼──────────────┼───────────────────┤
561
+ │ MYSQL_ENABLED │ 启用 MySQL │ false │
562
+ ├───────────────────────┼──────────────┼───────────────────┤
563
+ │ MYSQL_HOST │ MySQL 主机 │ - │
564
+ ├───────────────────────┼──────────────┼───────────────────┤
565
+ │ MYSQL_PORT │ MySQL 端口 │ 3306 │
566
+ ├───────────────────────┼──────────────┼───────────────────┤
567
+ │ MYSQL_USER │ MySQL 用户 │ - │
568
+ ├───────────────────────┼──────────────┼───────────────────┤
569
+ │ MYSQL_PASSWORD │ MySQL 密码 │ - │
570
+ ├───────────────────────┼──────────────┼───────────────────┤
571
+ │ MYSQL_DATABASE │ 数据库名 │ code_intelligence │
572
+ ├───────────────────────┼──────────────┼───────────────────┤
573
+ │ MYSQL_SYMBOLS_TABLE │ 表名 │ symbols │
574
+ ├───────────────────────┼──────────────┼───────────────────┤
575
+ │ EMBEDDING_SERVICE_URL │ 向量服务 URL │ - │
576
+ └───────────────────────┴──────────────┴───────────────────┘
577
+
578
+ 环境变量加载逻辑
579
+
580
+ 1. 加载本地 .env
581
+ 2. 加载第三方 .env(按变量维度覆盖,只覆盖第三方明确配置的变量)
582
+ 3. 命令行参数 --KEY=VALUE 优先级最高
583
+
584
+ ---
585
+
586
+ 八、技术难点总结
587
+
588
+ ┌────────────┬───────────────────────────────────────────────────┐
589
+ │ 模块 │ 难点 │
590
+ ├────────────┼───────────────────────────────────────────────────┤
591
+ │ 权重排序 │ textMatch/usage/recency/commonPath 四维权重调优 │
592
+ ├────────────┼───────────────────────────────────────────────────┤
593
+ │ TS 解析 │ ts-ormorph 需要 tsconfig.json,不存在时用默认配置 │
594
+ ├────────────┼───────────────────────────────────────────────────┤
595
+ │ JS 解析 │ Babel 支持 JSX/TS/装饰器等复杂语法 │
596
+ ├────────────┼───────────────────────────────────────────────────┤
597
+ │ Meta 提取 │ 函数/接口/类/hooks 的参数、返回值、类型解析 │
598
+ ├────────────┼───────────────────────────────────────────────────┤
599
+ │ 路径推断 │ 从文件路径智能推断业务分类(category) │
600
+ ├────────────┼───────────────────────────────────────────────────┤
601
+ │ 结构搜索 │ meta 存储为 JSON,全表扫描 + 内存过滤 │
602
+ ├────────────┼───────────────────────────────────────────────────┤
603
+ │ 向量检索 │ 向量生成、存储、余弦相似度计算 │
604
+ ├────────────┼───────────────────────────────────────────────────┤
605
+ │ 多工具编排 │ Prompt 中多 Tool 组合调用逻辑 │
606
+ ├────────────┼───────────────────────────────────────────────────┤
607
+ │ 环境加载 │ 本地/第三方 .env 按变量维度合并 │
608
+ ├────────────┼───────────────────────────────────────────────────┤
609
+ │ CI 评论 │ PR/Commit 评论逻辑、关联 PR 查找 │
610
+ └────────────┴───────────────────────────────────────────────────┘
611
+
612
+ ---
613
+
614
+ 这就是完整详细的功能总结。如需补充其他细节请告诉我。
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * 从 AST 抽取写入 `symbols.meta` 的结构化字段(props/params、hooks、返回值、类型成员等)。
3
3
  */
4
- import { Node, } from 'ts-morph';
4
+ import { Node, SyntaxKind, } from 'ts-morph';
5
5
  /**
6
6
  * 将节点收窄为可调用的函数形态,便于统一抽取参数与函数体。
7
7
  * @returns 若不是函数声明/表达式/箭头函数则为 `undefined`。
@@ -77,7 +77,7 @@ export function extractReturnTypeText(fn) {
77
77
  return rt ? rt.getText() : null;
78
78
  }
79
79
  /**
80
- * 汇总单个可调用代码块的元数据:`params`、可选 `hooks`、可选 `returnType`。
80
+ * 汇总单个可调用代码块的元数据:`params`、可选 `hooks`、可选 `returnType`、可选 `sideEffects`。
81
81
  * @returns 扁平对象,由上层按 `component`/`util` 再映射为 `props` 或保留 `params`。
82
82
  */
83
83
  export function extractFunctionMeta(fn) {
@@ -85,11 +85,13 @@ export function extractFunctionMeta(fn) {
85
85
  const paramTypeFields = extractParamTypeFieldNames(fn);
86
86
  const hooks = extractHooksFromBody(fn);
87
87
  const returnType = extractReturnTypeText(fn);
88
+ const sideEffects = extractSideEffects(fn);
88
89
  return {
89
90
  params,
90
91
  ...(paramTypeFields.length ? { paramTypeFields } : {}),
91
92
  ...(hooks.length ? { hooks } : {}),
92
93
  ...(returnType ? { returnType } : {}),
94
+ ...(sideEffects.length ? { sideEffects } : {}),
93
95
  };
94
96
  }
95
97
  /**
@@ -116,3 +118,94 @@ export function extractInterfaceOrTypeMeta(node) {
116
118
  }
117
119
  return { kind: 'unknown' };
118
120
  }
121
+ /**
122
+ * 静态分析函数体的副作用。
123
+ * @returns 副作用类型数组,去重排序后写入 `meta.sideEffects`。
124
+ */
125
+ export function extractSideEffects(node) {
126
+ const effects = new Set();
127
+ // 1. 检测网络请求
128
+ node.forEachDescendant((n) => {
129
+ if (Node.isCallExpression(n)) {
130
+ const expr = n.getExpression();
131
+ const text = expr.getText().toLowerCase();
132
+ // fetch、axios、xhr、ajax、superagent 等
133
+ if (text === 'fetch' ||
134
+ text === 'axios' ||
135
+ text === 'xhr' ||
136
+ text === 'ajax' ||
137
+ text.startsWith('axios.') ||
138
+ text.includes('request')) {
139
+ effects.add('network');
140
+ }
141
+ // XMLHttpRequest
142
+ if (text.includes('xmlhttprequest')) {
143
+ effects.add('network');
144
+ }
145
+ }
146
+ });
147
+ // 2. 检测计时器
148
+ node.forEachDescendant((n) => {
149
+ if (Node.isCallExpression(n)) {
150
+ const text = n.getExpression().getText();
151
+ if (text === 'setTimeout' ||
152
+ text === 'setInterval' ||
153
+ text === 'requestAnimationFrame' ||
154
+ text === 'setImmediate') {
155
+ effects.add('timer');
156
+ }
157
+ }
158
+ });
159
+ // 3. 检测 DOM / 全局对象操作
160
+ node.forEachDescendant((n) => {
161
+ if (Node.isExpressionStatement(n)) {
162
+ const text = n.getText();
163
+ // document. / window. 开头的赋值或调用
164
+ if (/\bdocument\.\w+/.test(text) ||
165
+ /\bwindow\.\w+/.test(text) ||
166
+ /\bnavigator\.\w+/.test(text) ||
167
+ /\blocation\.\w+/.test(text)) {
168
+ // 区分读取和写入
169
+ if (/=/.test(text) && !text.includes('===') && !text.includes('==')) {
170
+ effects.add('dom');
171
+ }
172
+ }
173
+ }
174
+ });
175
+ // 4. 检测存储操作
176
+ node.forEachDescendant((n) => {
177
+ if (Node.isCallExpression(n)) {
178
+ const text = n.getExpression().getText();
179
+ if (text.includes('localStorage') ||
180
+ text.includes('sessionStorage') ||
181
+ text.includes('cookie')) {
182
+ effects.add('storage');
183
+ }
184
+ }
185
+ });
186
+ // 5. 检测入参修改(检测 mutation pattern)
187
+ // 如 props.items.push(...), obj.x = ...
188
+ const params = node.getKind() === SyntaxKind.FunctionDeclaration
189
+ ? node.getParameters()
190
+ : node.getKind() === SyntaxKind.ArrowFunction
191
+ ? node.getParameters()
192
+ : node.getKind() === SyntaxKind.FunctionExpression
193
+ ? node.getParameters()
194
+ : [];
195
+ if (params.length > 0) {
196
+ const paramNames = new Set(params.map((p) => p.getName()));
197
+ node.forEachDescendant((n) => {
198
+ if (Node.isExpressionStatement(n)) {
199
+ const text = n.getText();
200
+ // 检测 param.x = ... 或 param.push/pop/splice 等
201
+ for (const param of paramNames) {
202
+ if (text.includes(`${param}.`) || text.startsWith(`${param} =`)) {
203
+ effects.add('mutation');
204
+ break;
205
+ }
206
+ }
207
+ }
208
+ });
209
+ }
210
+ return [...effects].sort();
211
+ }
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * 扫描源码目录,用 ts-morph 解析 TS/TSX,Babel 解析 JS/JSX,生成待写入 MySQL 的代码块行。
3
+ * 同时分析调用关系,填充 meta.callers / meta.callees。
3
4
  */
4
5
  import fg from 'fast-glob';
5
6
  import { join, resolve } from 'node:path';
@@ -159,6 +160,7 @@ const DEFAULT_IGNORE = [
159
160
  ];
160
161
  /**
161
162
  * 按 glob 收集文件,用 ts-morph 加载并遍历每个文件的导出,生成全部代码块行。
163
+ * 同时分析调用关系,填充 meta.callers / meta.callees。
162
164
  * @returns 数组可交给 `upsertSymbols`;无匹配文件时返回空数组(不写库)。
163
165
  */
164
166
  export async function indexProject(opts) {
@@ -167,6 +169,7 @@ export async function indexProject(opts) {
167
169
  const ignore = [...DEFAULT_IGNORE, ...(opts.ignore ?? [])];
168
170
  console.error(`[indexProject] patterns: ${patterns.join(', ')}`);
169
171
  const files = await fg(patterns, {
172
+ // 根据pattern收集文件列表,支持绝对路径和相对路径(相对于projectRoot),并自动排除node_modules、dist等常见目录。
170
173
  absolute: true,
171
174
  ignore,
172
175
  onlyFiles: true,
@@ -180,6 +183,7 @@ export async function indexProject(opts) {
180
183
  const tsFiles = files.filter(isTsFile);
181
184
  const jsFiles = files.filter(isJsFile);
182
185
  const out = [];
186
+ const symbolMap = new Map(); // name -> { name, path, exports }
183
187
  // 处理 TS/TSX 文件(只有 tsconfig.json 存在时才处理)
184
188
  const tsConfigPath = join(projectRoot, 'tsconfig.json');
185
189
  const hasTsConfig = existsSync(tsConfigPath);
@@ -191,12 +195,19 @@ export async function indexProject(opts) {
191
195
  });
192
196
  project.addSourceFilesAtPaths(tsFiles);
193
197
  for (const sf of project.getSourceFiles()) {
198
+ const relPath = getRelativePathForDisplay(projectRoot, sf.getFilePath());
194
199
  const exported = sf.getExportedDeclarations();
195
200
  for (const [exportName, decls] of exported) {
196
201
  for (const decl of decls) {
197
202
  const row = processDeclaration(exportName, decl, sf, projectRoot);
198
203
  if (row) {
199
204
  out.push(row);
205
+ // 建立符号映射
206
+ const key = `${row.name}|${row.path}`;
207
+ if (!symbolMap.has(key)) {
208
+ symbolMap.set(key, { name: row.name, path: row.path, exports: new Set() });
209
+ }
210
+ symbolMap.get(key).exports.add(exportName);
200
211
  }
201
212
  }
202
213
  }
@@ -208,10 +219,107 @@ export async function indexProject(opts) {
208
219
  const content = readFileSync(file, 'utf-8');
209
220
  const rows = parseJsFile(file, content, projectRoot);
210
221
  out.push(...rows);
222
+ // 建立符号映射
223
+ for (const row of rows) {
224
+ const key = `${row.name}|${row.path}`;
225
+ if (!symbolMap.has(key)) {
226
+ symbolMap.set(key, { name: row.name, path: row.path, exports: new Set() });
227
+ }
228
+ symbolMap.get(key).exports.add(row.name);
229
+ }
211
230
  }
212
231
  catch (e) {
213
232
  console.error(`[indexProject] Failed to parse ${file}:`, e);
214
233
  }
215
234
  }
235
+ // 分析调用关系,填充 meta.callers / meta.callees
236
+ if (tsFiles.length > 0 && hasTsConfig) {
237
+ const project = new Project({
238
+ tsConfigFilePath: tsConfigPath,
239
+ skipAddingFilesFromTsConfig: true,
240
+ skipFileDependencyResolution: true,
241
+ });
242
+ project.addSourceFilesAtPaths(tsFiles);
243
+ analyzeRelations(project, symbolMap, projectRoot, out);
244
+ }
216
245
  return out;
217
246
  }
247
+ /**
248
+ * 分析调用关系,填充每个符号的 meta.callers 和 meta.callees
249
+ */
250
+ function analyzeRelations(project, symbolMap, projectRoot, rows) {
251
+ // 构造快速查找:exportName -> [ {name, path} ]
252
+ const exportToSymbol = new Map();
253
+ for (const [key, value] of symbolMap) {
254
+ for (const exp of value.exports) {
255
+ const list = exportToSymbol.get(exp) || [];
256
+ list.push({ name: value.name, path: value.path });
257
+ exportToSymbol.set(exp, list);
258
+ }
259
+ }
260
+ // 收集所有 callers 和 callees
261
+ const callersMap = new Map(); // key = "name|path" -> set of callers
262
+ const calleesMap = new Map(); // key = "name|path" -> set of callees
263
+ for (const sf of project.getSourceFiles()) {
264
+ const filePath = sf.getFilePath();
265
+ const relPath = getRelativePathForDisplay(projectRoot, filePath);
266
+ // 获取当前文件导出的符号
267
+ const exported = sf.getExportedDeclarations();
268
+ const fileExportNames = new Set();
269
+ for (const [name, decls] of exported) {
270
+ fileExportNames.add(name);
271
+ }
272
+ // 遍历 AST 查找调用
273
+ sf.forEachDescendant((node) => {
274
+ // 1. 函数调用
275
+ if (Node.isCallExpression(node)) {
276
+ const expr = node.getExpression();
277
+ const name = Node.isIdentifier(expr) ? expr.getText() : null;
278
+ if (name && exportToSymbol.has(name)) {
279
+ const targets = exportToSymbol.get(name);
280
+ // 当前文件是谁在调用
281
+ for (const [expName, decls] of exported) {
282
+ for (const decl of decls) {
283
+ const callerKey = `${expName}|${relPath}`;
284
+ for (const target of targets) {
285
+ // callees: 我调用了谁
286
+ const calleeSet = calleesMap.get(callerKey) || new Set();
287
+ calleeSet.add({ name: target.name, path: target.path });
288
+ calleesMap.set(callerKey, calleeSet);
289
+ // callers: 谁调用了我
290
+ const callerSet = callersMap.get(`${target.name}|${target.path}`) || new Set();
291
+ callerSet.add({ name: expName, path: relPath });
292
+ callersMap.set(`${target.name}|${target.path}`, callerSet);
293
+ }
294
+ }
295
+ }
296
+ }
297
+ }
298
+ // 2. Import 导入
299
+ if (Node.isImportDeclaration(node)) {
300
+ const moduleSpec = node.getModuleSpecifier().getText();
301
+ // 简单处理:只处理从 ./ 或 ../ 开始的相对导入
302
+ if (moduleSpec.startsWith("'.") || moduleSpec.startsWith('".')) {
303
+ const importPath = moduleSpec.slice(1, -1); // 去掉引号
304
+ // 尝试匹配已索引的符号
305
+ // 这里简化处理,暂不展开
306
+ }
307
+ }
308
+ });
309
+ }
310
+ // 写入 rows 的 meta
311
+ for (const row of rows) {
312
+ const key = `${row.name}|${row.path}`;
313
+ const callers = callersMap.get(key);
314
+ const callees = calleesMap.get(key);
315
+ if (callers && callers.size > 0) {
316
+ row.meta = row.meta || {};
317
+ row.meta.callers = [...callers].slice(0, 20); // 限制数量
318
+ }
319
+ if (callees && callees.size > 0) {
320
+ row.meta = row.meta || {};
321
+ row.meta.callees = [...callees].slice(0, 20);
322
+ }
323
+ }
324
+ console.error(`[analyzeRelations] processed ${rows.length} symbols`);
325
+ }
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- const REUSABLE_CODE_ADVISOR_DESCRIPTION = '在实现需求时检索并推荐最合适的可复用代码代码块(函数/类/模块等)。在用户要求复用现有代码、询问是否已有组件/函数/服务、或要在多个代码块中选最优时使用。';
2
+ const REUSABLE_CODE_ADVISOR_DESCRIPTION = '在实现需求时检索并推荐最合适的可复用代码代码块(组件/函数/类/模块等)。在用户要求复用现有代码、询问是否已有组件/函数/服务、或要在多个代码块中选最优时使用。';
3
3
  /** 与 SKILL.md 中 `# 可复用代码推荐` 起至约束、示例说明为止的正文一致(无 YAML frontmatter)。 */
4
4
  const REUSABLE_CODE_ADVISOR_MARKDOWN = `# 可复用代码推荐
5
5
 
@@ -7,24 +7,26 @@ const REUSABLE_CODE_ADVISOR_MARKDOWN = `# 可复用代码推荐
7
7
 
8
8
  当用户需要可复用代码或实现类需求时,按顺序执行:
9
9
 
10
- 1. 先判断意图:若意图是“组件推荐”(如推荐表单组件、弹窗组件、列表组件等),优先调用 \`recommend_component\`;否则走通用多工具流程。
11
- 2. 通用多工具流程中,先调用 \`search_symbols\` 检索可能满足需求的候选代码块;query 用短英文/关键词(如 formInput),需要限定类型时再加 type(如 component),不要用整句中文。
12
- 3. 执行细则:先 \`search_symbols(limit=20)\` 拉候选,再对 Top 3 调用 \`get_symbol_detail\` 做深度判断。
13
- 4. 若仅凭签名/摘要无法判断,对最相关的若干候选调用 \`get_symbol_detail\` 获取详情。
10
+ 1. 调用 search_symbols 检索候选,type 根据用户需求传(component/util/selector/type)
11
+ 2. 如果用户指定了结构过滤条件(props/params/properties/hooks),额外调用 search_by_structure 做结构匹配
12
+ 3. search_symbols(limit=20) 拉候选,再对 Top 3 调用 get_symbol_detail 做深度判断
13
+ 4. 若仅凭签名/摘要无法判断,对最相关的若干候选调用 get_symbol_detail 获取详情
14
14
  5. 从以下维度对比候选:
15
15
  - 与用户需求的**功能匹配度**
16
16
  - **API 是否简单**、入参是否合适
17
17
  - **依赖与副作用**风险
18
18
  - **复用安全性**(稳定性、耦合度、是否便于扩展)
19
- 6. 给出**唯一首选**推荐,并说明理由。
19
+ 6. 给出**唯一首选**推荐,并说明理由
20
20
 
21
21
  ## 回复结构
22
22
 
23
23
  按此结构输出(字段名可保留英文或改为中文小标题,二选一全文统一):
24
24
 
25
- - **首选:** \`<代码块名>\`
25
+ - **首选:** <代码块名>
26
+ - **使用范围:** 展示 meta.callers(被哪些模块调用),用于评估稳定性和适用范围;若无调用记录则显示"新增"
27
+ - **副作用:** 展示 meta.sideEffects(network/timer/dom/storage/mutation),优先推荐无副作用的方案,标注有副作用的方案风险
26
28
  - **理由:** 1~3 条要点
27
- - **其他候选:** 简要列出及取舍
29
+ - **其他候选:** 简要列出及取舍(同步标注副作用)
28
30
  - **用法提示:** 结合用户场景的最小集成说明
29
31
 
30
32
  ## 约束
@@ -33,6 +35,12 @@ const REUSABLE_CODE_ADVISOR_MARKDOWN = `# 可复用代码推荐
33
35
  - 若无合适代码块,明确说明,并给出最接近的选项及差距。
34
36
  - 推理简洁,面向落地实现。
35
37
 
38
+ ## 使用反馈
39
+
40
+ 当最终采纳了某个推荐代码块后,必须调用 inc_usage 工具记录采纳行为:
41
+ “请调用 inc_usage({ symbolId: <选中的代码块 id> })”
42
+ 其中 symbolId 从 search_symbols 或 search_by_structure 返回结果的 id 字段获取。这条记录会用于后续排序优化。
43
+
36
44
  ## 更多示例
37
45
 
38
46
  与仓库内 \`.cursor/skills/reusable-code-advisor/examples.md\` 中的示例一致(在 Cursor 或本地打开该文件查看)。
@@ -167,6 +167,22 @@ export class SymbolRepository {
167
167
  }
168
168
  return mapRow(rows[0]);
169
169
  }
170
+ /**
171
+ * 将指定代码块的 usage_count +1,用于用户采纳推荐后记录。
172
+ */
173
+ async incUsage(symbolId) {
174
+ if (!this.pool) {
175
+ // 内存模式:找到并 +1
176
+ const idx = inMemorySymbols.findIndex((s) => s.id === symbolId);
177
+ if (idx >= 0) {
178
+ inMemorySymbols[idx].usageCount++;
179
+ return true;
180
+ }
181
+ return false;
182
+ }
183
+ const [result] = await this.pool.query(`UPDATE ${env.mysqlSymbolsTable} SET usage_count = usage_count + 1 WHERE id = ?`, [symbolId]);
184
+ return result.affectedRows > 0;
185
+ }
170
186
  async searchByStructure(fields, opts) {
171
187
  const normalized = fields.map((f) => f.trim()).filter(Boolean);
172
188
  if (normalized.length === 0)
@@ -1,12 +1,11 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- import { registerRecommendComponentPrompt } from "../prompts/recommendComponentPrompt.js";
3
2
  import { registerReusableCodeAdvisorPrompt } from "../prompts/reusableCodeAdvisorPrompt.js";
4
3
  import { SymbolRepository } from "../repositories/symbolRepository.js";
5
4
  import { createSearchSymbolsTool } from "../tools/searchSymbols.js";
6
5
  import { createGetSymbolDetailTool } from "../tools/getSymbolDetail.js";
7
- import { createRecommendComponentTool } from "../tools/recommendComponent.js";
8
6
  import { createReindexTool } from "../tools/reindex.js";
9
7
  import { createSearchByStructureTool } from "../tools/searchByStructure.js";
8
+ import { createIncUsageTool } from "../tools/incUsage.js";
10
9
  export function createServer() {
11
10
  const server = new McpServer({
12
11
  name: "code-intelligence-mcp",
@@ -19,11 +18,10 @@ export function createServer() {
19
18
  server.tool(detailTool.name, detailTool.description, detailTool.inputSchema, detailTool.handler);
20
19
  const structureTool = createSearchByStructureTool(repository);
21
20
  server.tool(structureTool.name, structureTool.description, structureTool.inputSchema, structureTool.handler);
22
- const recommendTool = createRecommendComponentTool(repository);
23
- server.tool(recommendTool.name, recommendTool.description, recommendTool.inputSchema, recommendTool.handler);
24
21
  const reindexTool = createReindexTool();
25
22
  server.tool(reindexTool.name, reindexTool.description, reindexTool.inputSchema, reindexTool.handler);
26
- registerRecommendComponentPrompt(server);
23
+ const incUsageTool = createIncUsageTool(repository);
24
+ server.tool(incUsageTool.name, incUsageTool.description, incUsageTool.inputSchema, incUsageTool.handler);
27
25
  registerReusableCodeAdvisorPrompt(server);
28
26
  return server;
29
27
  }
@@ -1,28 +1,27 @@
1
1
  export function createEmbeddingClient(baseUrl) {
2
- const root = baseUrl.replace(/\/$/, "");
2
+ const root = baseUrl.replace(/\/$/, '');
3
3
  return {
4
4
  async embed(texts) {
5
5
  if (texts.length === 0)
6
6
  return [];
7
7
  const res = await fetch(`${root}/embed`, {
8
- method: "POST",
9
- headers: { "Content-Type": "application/json" },
8
+ method: 'POST',
9
+ headers: { 'Content-Type': 'application/json' },
10
10
  body: JSON.stringify({ texts }),
11
- signal: AbortSignal.timeout(180_000)
11
+ signal: AbortSignal.timeout(180_000),
12
12
  });
13
13
  if (!res.ok) {
14
- const t = await res.text().catch(() => "");
15
- throw new Error(`embedding service HTTP ${res.status}${t ? `: ${t.slice(0, 200)}` : ""}`);
14
+ const t = await res.text().catch(() => '');
15
+ throw new Error(`embedding service HTTP ${res.status}${t ? `: ${t.slice(0, 200)}` : ''}`);
16
16
  }
17
17
  const data = (await res.json());
18
18
  if (!data.embeddings || !Array.isArray(data.embeddings)) {
19
- throw new Error("embedding service returned invalid JSON (missing embeddings)");
19
+ throw new Error('embedding service returned invalid JSON (missing embeddings)');
20
20
  }
21
21
  return data.embeddings;
22
- }
22
+ },
23
23
  };
24
24
  }
25
- /** Chunked embed to avoid huge request bodies. */
26
25
  export async function embedAll(client, texts, chunkSize = 48) {
27
26
  const out = [];
28
27
  for (let i = 0; i < texts.length; i += chunkSize) {
@@ -1,4 +1,4 @@
1
- /** Cosine similarity for equal-length numeric vectors. */
1
+ // 计算两个向量的余弦相似度,返回值在 -1 1 之间,1 表示完全相似,0 表示不相关,-1 表示完全相反。
2
2
  export function cosineSimilarity(a, b) {
3
3
  if (a.length === 0 || b.length !== a.length)
4
4
  return 0;
@@ -0,0 +1,33 @@
1
+ import { z } from 'zod';
2
+ export const incUsageInput = z.object({
3
+ /** 要增加使用计数的代码块 ID(从搜索结果中获取) */
4
+ symbolId: z.number().int().positive(),
5
+ });
6
+ export function createIncUsageTool(repository) {
7
+ return {
8
+ name: 'inc_usage',
9
+ description: '当开发者采纳了某个推荐代码块时,调用此工具记录。usage_count 会 +1,用于后续排序优化。',
10
+ inputSchema: incUsageInput.shape,
11
+ handler: async (input) => {
12
+ const success = await repository.incUsage(input.symbolId);
13
+ if (!success) {
14
+ return {
15
+ content: [
16
+ {
17
+ type: 'text',
18
+ text: JSON.stringify({ error: '未找到该代码块', symbolId: input.symbolId }, null, 2),
19
+ },
20
+ ],
21
+ };
22
+ }
23
+ return {
24
+ content: [
25
+ {
26
+ type: 'text',
27
+ text: JSON.stringify({ ok: true, symbolId: input.symbolId, message: 'usage_count 已 +1' }, null, 2),
28
+ },
29
+ ],
30
+ };
31
+ },
32
+ };
33
+ }
@@ -8,7 +8,23 @@ export const recommendComponentInput = z.object({
8
8
  export function createRecommendComponentTool(repository) {
9
9
  return {
10
10
  name: "recommend_component",
11
- description: "基于关键词检索 + 可选结构过滤 + 排序 + 详情补全,推荐最合适的可复用组件。",
11
+ description: `推荐最合适的可复用组件。
12
+
13
+ 工作流(自动完成,无需关心内部细节):
14
+ 1. 关键词检索:根据 query 在 component 类型中搜索候选
15
+ 2. 结构过滤:如果传了 props 参数,按结构匹配进一步过滤
16
+ 3. 语义排序:对候选进行语义相似度排序
17
+ 4. 详情补全:获取 Top 结果的完整代码详情
18
+ 5. 返回推荐:输出 Top N 结果及评分、理由
19
+
20
+ 输入参数:
21
+ - query:搜索关键词,如 "form"、"modal"、"table"
22
+ - props(可选):结构过滤所需的 props,如 ["onChange", "value"]
23
+ - limit(可选):返回数量,默认 3,最大 20
24
+
25
+ 输出:
26
+ - results 数组,每项包含 name、path、score、reason、detail
27
+ - 如果没有合适结果,会返回最接近的候选并说明差距`,
12
28
  inputSchema: recommendComponentInput.shape,
13
29
  handler: async (input) => {
14
30
  const result = await recommendComponent(input.query, repository, {
@@ -1,27 +1,28 @@
1
1
  // 按结构字段检索(匹配 meta.props / meta.params / meta.properties / meta.hooks)
2
- import { z } from "zod";
3
- import { rankSymbols } from "../services/ranking.js";
2
+ import { z } from 'zod';
3
+ import { rankSymbols } from '../services/ranking.js';
4
4
  export const searchByStructureInput = z.object({
5
5
  fields: z.array(z.string().min(1)).min(1),
6
- type: z.enum(["component", "util", "selector", "type"]).optional(),
6
+ type: z.enum(['component', 'util', 'selector', 'type']).optional(),
7
7
  category: z.string().optional(),
8
8
  limit: z.number().int().min(1).max(100).optional().default(20),
9
- ranked: z.boolean().optional().default(true)
9
+ ranked: z.boolean().optional().default(true),
10
10
  });
11
11
  export function createSearchByStructureTool(repository) {
12
12
  return {
13
- name: "search_by_structure",
14
- description: "Search symbols by structured fields like props/params/properties/hooks. Use for API-shape queries.",
13
+ name: 'search_by_structure',
14
+ description: '通过结构化字段(如 props/params/properties/hooks)搜索代码块,适用于 API 形态的查询。',
15
15
  inputSchema: searchByStructureInput.shape,
16
16
  handler: async (input) => {
17
17
  const rows = await repository.searchByStructure(input.fields, {
18
18
  type: input.type,
19
19
  category: input.category,
20
- limit: input.limit
20
+ limit: input.limit,
21
21
  });
22
- const query = input.fields.join(" ");
22
+ const query = input.fields.join(' ');
23
23
  const resultRows = input.ranked
24
24
  ? rankSymbols(query, rows).map((item) => ({
25
+ id: item.symbol.id,
25
26
  name: item.symbol.name,
26
27
  type: item.symbol.type,
27
28
  category: item.symbol.category,
@@ -31,25 +32,26 @@ export function createSearchByStructureTool(repository) {
31
32
  score: item.score,
32
33
  reason: item.reason.summary,
33
34
  reasonDetail: item.reason,
34
- meta: item.symbol.meta
35
+ meta: item.symbol.meta,
35
36
  }))
36
37
  : rows.map((r) => ({
38
+ id: r.id,
37
39
  name: r.name,
38
40
  type: r.type,
39
41
  category: r.category,
40
42
  path: r.path,
41
43
  description: r.description,
42
44
  usageCount: r.usageCount,
43
- meta: r.meta
45
+ meta: r.meta,
44
46
  }));
45
47
  return {
46
48
  content: [
47
49
  {
48
- type: "text",
49
- text: JSON.stringify(resultRows, null, 2)
50
- }
51
- ]
50
+ type: 'text',
51
+ text: JSON.stringify(resultRows, null, 2),
52
+ },
53
+ ],
52
54
  };
53
- }
55
+ },
54
56
  };
55
57
  }
@@ -22,6 +22,7 @@ export function createSearchSymbolsTool(repository) {
22
22
  const simById = new Map(hits.map((h) => [h.symbol.id, h.similarity]));
23
23
  const resultRows = input.ranked
24
24
  ? rankSemanticHits(hits).map((item) => ({
25
+ id: item.symbol.id,
25
26
  name: item.symbol.name,
26
27
  type: item.symbol.type,
27
28
  path: item.symbol.path,
@@ -33,6 +34,7 @@ export function createSearchSymbolsTool(repository) {
33
34
  semanticSimilarity: Number((simById.get(item.symbol.id) ?? 0).toFixed(4)),
34
35
  }))
35
36
  : hits.map((h) => ({
37
+ id: h.symbol.id,
36
38
  name: h.symbol.name,
37
39
  type: h.symbol.type,
38
40
  path: h.symbol.path,
@@ -52,6 +54,7 @@ export function createSearchSymbolsTool(repository) {
52
54
  const rows = await repository.search(input.query, input.type);
53
55
  const resultRows = input.ranked
54
56
  ? rankSymbols(input.query, rows).map((item) => ({
57
+ id: item.symbol.id,
55
58
  name: item.symbol.name,
56
59
  type: item.symbol.type,
57
60
  path: item.symbol.path,
@@ -62,6 +65,7 @@ export function createSearchSymbolsTool(repository) {
62
65
  reasonDetail: item.reason,
63
66
  }))
64
67
  : rows.map((r) => ({
68
+ id: r.id,
65
69
  name: r.name,
66
70
  type: r.type,
67
71
  path: r.path,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lorrylurui/code-intelligence-mcp",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "private": false,
5
5
  "description": "MCP server 提供仓库内可复用代码块(ts/tsx/js/jsx/css/less)的索引和查询能力,支持基于代码上下文的智能推荐。",
6
6
  "type": "module",