@lorrylurui/code-intelligence-mcp 1.1.13 → 1.1.15
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 +15 -593
- package/dist/cli/index-codebase.js +2 -4
- package/dist/config/env.js +1 -97
- package/dist/index.js +4 -2
- package/dist/indexer/babelParser.js +219 -15
- package/dist/indexer/extractMeta.js +7 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,614 +1,36 @@
|
|
|
1
1
|
# Code Intelligence MCP (Minimal)
|
|
2
2
|
|
|
3
|
-
最小可用的 Node MCP Server 框架,包含:
|
|
4
|
-
|
|
5
3
|
- MCP Server(stdio)
|
|
6
|
-
- Tool: `search_symbols
|
|
4
|
+
- Tool: `search_symbols`
|
|
7
5
|
- Tool: `get_symbol_detail`
|
|
8
6
|
- Tool: `search_by_structure`
|
|
9
7
|
- Tool: `reindex`
|
|
10
8
|
- Tool: `recommend_component`
|
|
11
|
-
-
|
|
9
|
+
- Tool: `incUsage`
|
|
10
|
+
- Prompt: `reusable-code-advisor`
|
|
12
11
|
- MySQL Repository(可选启用)
|
|
13
|
-
- Cursor Skill:`reusable-code-advisor`(`.cursor/skills/reusable-code-advisor
|
|
14
|
-
|
|
15
|
-
## 1) 安装
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
npm install
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
## 2) 环境变量
|
|
22
|
-
|
|
23
|
-
复制 `.env.example` 为 `.env`。
|
|
24
|
-
|
|
25
|
-
默认不强制连接 MySQL(未配置时走内存示例数据)。
|
|
26
|
-
|
|
27
|
-
如果你要连接 MySQL,请设置:
|
|
28
|
-
|
|
29
|
-
```env
|
|
30
|
-
MYSQL_ENABLED=true
|
|
31
|
-
MYSQL_HOST=127.0.0.1
|
|
32
|
-
MYSQL_PORT=3306
|
|
33
|
-
MYSQL_USER=root
|
|
34
|
-
MYSQL_PASSWORD=devpassword
|
|
35
|
-
MYSQL_DATABASE=code_intelligence
|
|
36
|
-
|
|
37
|
-
# Phase 5(可选):句向量服务根 URL,与 `npm run embedding:dev` 默认端口一致
|
|
38
|
-
# EMBEDDING_SERVICE_URL=http://127.0.0.1:8765
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
密码需与下方 Docker / 本机 MySQL 配置一致(文档示例里 `devpassword` 对应 Compose)。
|
|
42
|
-
|
|
43
|
-
### 用 Docker 启动 MySQL(推荐本地开发)
|
|
44
|
-
|
|
45
|
-
1. 安装 [Docker Desktop](https://www.docker.com/products/docker-desktop/)(或 Docker Engine + Compose 插件)。
|
|
46
|
-
2. 在项目根目录执行:
|
|
47
|
-
|
|
48
|
-
```bash
|
|
49
|
-
npm run docker:up
|
|
50
|
-
# 或:docker compose up -d
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
3. 首次启动会自动挂载 `sql/schema.sql` 到 `docker-entrypoint-initdb.d`,**创建库表**(仅**空数据卷**时执行一次)。
|
|
54
|
-
4. 复制 `.env.example` 为 `.env`,设置 `MYSQL_ENABLED=true`,`MYSQL_PASSWORD` 与 `docker-compose.yml` 里 `MYSQL_ROOT_PASSWORD`(默认 `devpassword`)一致。
|
|
55
|
-
5. 等待容器健康(约数十秒):
|
|
56
|
-
|
|
57
|
-
```bash
|
|
58
|
-
docker compose ps
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
6. 再执行 `npm run index` 或启动 MCP。
|
|
62
|
-
|
|
63
|
-
常用命令:
|
|
64
|
-
|
|
65
|
-
| 命令 | 说明 |
|
|
66
|
-
| ------------------------ | ------------------------------ |
|
|
67
|
-
| `npm run docker:logs` | 查看 MySQL 日志 |
|
|
68
|
-
| `npm run docker:down` | 停止容器(数据卷保留,库仍在) |
|
|
69
|
-
| `docker compose down -v` | **删除卷**(清空库,慎用) |
|
|
70
|
-
|
|
71
|
-
**端口冲突**:若本机已有服务占用 `3306`,把 `docker-compose.yml` 里 `ports` 改为 `"3307:3306"`,并在 `.env` 设 `MYSQL_PORT=3307`。
|
|
72
|
-
|
|
73
|
-
## 3) 初始化数据库(可选)
|
|
74
|
-
|
|
75
|
-
- **已用上述 Docker 首次启动**:若卷为空,建表已由 `sql/schema.sql` 自动执行,一般无需再跑下面命令。
|
|
76
|
-
- **本机 mysql 客户端 / 手动执行**:
|
|
77
|
-
|
|
78
|
-
```bash
|
|
79
|
-
mysql -u root -p code_intelligence < sql/schema.sql
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
### 自定义表名(第三方项目集成)
|
|
83
|
-
|
|
84
|
-
若需使用不同的表名,可通过环境变量配置:
|
|
85
|
-
|
|
86
|
-
```bash
|
|
87
|
-
# 设置自定义表名
|
|
88
|
-
export MYSQL_SYMBOLS_TABLE=my_project_symbols
|
|
89
|
-
|
|
90
|
-
# 然后server代码内部执行建表(表名会在代码中动态替换)
|
|
91
|
-
mysql -u root -p code_intelligence -e "$(node -e \"import('./dist/db/schema.js').then(m => console.log(m.getSymbolsTableSQL()))\")"
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
或在 `.env` 中配置:
|
|
95
|
-
|
|
96
|
-
```env
|
|
97
|
-
MYSQL_SYMBOLS_TABLE=my_project_symbols
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
## 4) 本地运行
|
|
101
|
-
|
|
102
|
-
### 普通开发(热更新)
|
|
103
|
-
|
|
104
|
-
```bash
|
|
105
|
-
npm run dev
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
使用 `tsx watch`,改 `src/` 会自动重启;已关闭清屏(`--clear-screen=false`),并排除 `node_modules`、`dist`。
|
|
109
|
-
|
|
110
|
-
### 接 Cursor MCP(不污染 stdout)
|
|
111
|
-
|
|
112
|
-
MCP 走 **stdio**,协议数据必须在子进程的 **stdout** 上;若用 `npm run dev` 接 MCP,`npm` 或部分工具可能往 stdout 打杂讯,导致握手异常。
|
|
113
|
-
|
|
114
|
-
推荐用 **专用脚本**:子进程只跑 `tsx src/index.ts`,**监听/重启日志只打到 stderr**。
|
|
115
|
-
|
|
116
|
-
```bash
|
|
117
|
-
npm run dev:mcp
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
**Cursor `mcp.json` 示例(推荐直接调 node,避免 npm):**
|
|
121
|
-
|
|
122
|
-
```json
|
|
123
|
-
{
|
|
124
|
-
"mcpServers": {
|
|
125
|
-
"code-intelligence-mcp": {
|
|
126
|
-
"command": "node",
|
|
127
|
-
"args": ["/绝对路径/Intelligence-code/scripts/mcp-dev-watch.mjs"],
|
|
128
|
-
"cwd": "/绝对路径/Intelligence-code"
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
也可继续用 `"command": "npm"`, `"args": ["run", "dev:mcp"]`,但部分环境下 npm 仍可能产生额外输出;若 tools 不稳定,请改用上面的 `node .../mcp-dev-watch.mjs`。
|
|
135
|
-
|
|
136
|
-
### MCP Prompt(非 Cursor 客户端)
|
|
137
|
-
|
|
138
|
-
服务器注册 Prompt **`reusable-code-advisor`**:客户端执行 `prompts/list` 可见;`prompts/get` 时可传可选参数 **`userRequest`**(用户当前需求或关键词),返回的消息正文与 Cursor Skill 工作流一致。
|
|
139
|
-
文案与 `.cursor/skills/reusable-code-advisor/SKILL.md` 正文需**手动同步**(见 `src/prompts/reusableCodeAdvisorPrompt.ts` 顶部注释)。
|
|
140
|
-
|
|
141
|
-
在 **MCP Inspector** 中切换到 **Prompts** 面板即可选择并调试。
|
|
142
|
-
|
|
143
|
-
## 5) Phase 2:代码索引(ts-morph + fast-glob → MySQL)
|
|
144
|
-
|
|
145
|
-
1. **建表 / 迁移**
|
|
146
|
-
- 新库:执行 `sql/schema.sql`(已含 `(path, name)` 唯一索引,便于重复执行 `npm run index` 时 upsert)。
|
|
147
|
-
- 旧库若只有早期表结构:执行 `sql/migrations/002_symbols_unique_path_name.sql`(若已有重复 `path+name` 需先清理)。
|
|
148
|
-
|
|
149
|
-
2. **配置 MySQL**(`.env` 中 `MYSQL_ENABLED=true` 等)。
|
|
150
|
-
|
|
151
|
-
3. **跑索引**(日志在 stderr,不污染 MCP stdout):
|
|
152
|
-
|
|
153
|
-
```bash
|
|
154
|
-
npm run index
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
可选环境变量(见 `.env.example`):
|
|
158
|
-
|
|
159
|
-
| 变量 | 含义 |
|
|
160
|
-
| -------------- | --------------------------------------- |
|
|
161
|
-
| `INDEX_ROOT` | 工程根目录,默认当前工作目录 |
|
|
162
|
-
| `INDEX_GLOB` | 空格分隔 glob,默认 `src/**/*.{ts,tsx}` |
|
|
163
|
-
| `INDEX_IGNORE` | 额外忽略的 glob 片段(空格分隔) |
|
|
164
|
-
|
|
165
|
-
**分类规则(首版启发式)**:`interface` / `type` → `type`;`.tsx` 且函数体含 JSX → `component`;路径或导出名含 `selector` → `selector`;其余导出函数 → `util`;`class` → `util`(可后续细化)。
|
|
166
|
-
|
|
167
|
-
**常见错误 `ECONNREFUSED 127.0.0.1:3306`**:本机没有在该端口监听 MySQL。请先启动数据库服务(例如 macOS Homebrew:`brew services start mysql` / `mariadb`),或把 `.env` 里的 `MYSQL_HOST`、`MYSQL_PORT` 改成你实际使用的实例(含 Docker 映射端口)。索引脚本会先执行 `SELECT 1` 再扫描代码,避免库不可用时仍跑完解析。
|
|
168
|
-
|
|
169
|
-
## 6) 后续演进建议
|
|
170
|
-
|
|
171
|
-
- 新增 Tool:`list_dependencies`、`get_usage_stats`
|
|
172
|
-
- Indexer:更细的 selector 识别、`export default` 命名、类组件等
|
|
173
|
-
- Phase 5 语义检索已落地(见下文);后续可换 pgvector / FAISS、更大模型
|
|
174
|
-
|
|
175
|
-
## 8) Phase 3(增强)
|
|
176
|
-
|
|
177
|
-
- `search_symbols` 已支持 `ranked` 参数(默认 `true`),返回 `score` 和 `reason`。
|
|
178
|
-
- 新增 `search_by_structure`,可按 `fields`(匹配 `meta.props/params/properties/hooks`)检索。
|
|
179
|
-
- 两个搜索 tool 的 ranking 已升级:除可读 `reason` 外,还返回结构化 `reasonDetail`(含各维度得分、权重和匹配方式),方便前端/Agent解释。
|
|
180
|
-
|
|
181
|
-
示例:
|
|
182
|
-
|
|
183
|
-
```json
|
|
184
|
-
{
|
|
185
|
-
"fields": ["onChange", "value"],
|
|
186
|
-
"type": "component",
|
|
187
|
-
"limit": 10
|
|
188
|
-
}
|
|
189
|
-
```
|
|
12
|
+
- Cursor Skill:`reusable-code-advisor`(`.cursor/skills/reusable-code-advisor/`,
|
|
190
13
|
|
|
191
|
-
|
|
14
|
+
## 1) 配置mcp servers
|
|
192
15
|
|
|
193
|
-
```json
|
|
194
|
-
{
|
|
195
|
-
"dryRun": false
|
|
196
|
-
}
|
|
197
16
|
```
|
|
198
|
-
|
|
199
|
-
可选参数:
|
|
200
|
-
|
|
201
|
-
- `projectRoot`: 指定索引根目录(默认 MCP 进程当前目录)
|
|
202
|
-
- `globPatterns`: 自定义扫描 glob 列表
|
|
203
|
-
- `ignore`: 额外忽略规则
|
|
204
|
-
- `dryRun`: `true` 时只扫描,不写 MySQL
|
|
205
|
-
|
|
206
|
-
## 9) Phase 4(Skill)
|
|
207
|
-
|
|
208
|
-
- 新增 Skill Tool:`recommend_component`
|
|
209
|
-
- 流程已落地:关键词搜索 -> 结构过滤(可选 `props`)-> ranking -> detail 补全 -> 返回 reason
|
|
210
|
-
- 新增 Prompt:`recommend-component`(用于在支持 MCP Prompt 的客户端快速触发该流程)
|
|
211
|
-
|
|
212
|
-
示例:
|
|
213
|
-
|
|
214
|
-
```json
|
|
215
|
-
{
|
|
216
|
-
"query": "带校验的表单组件",
|
|
217
|
-
"props": ["value", "onChange"],
|
|
218
|
-
"limit": 3
|
|
219
|
-
}
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
## 10) Phase 5(语义检索,可选)
|
|
223
|
-
|
|
224
|
-
1. **迁移**:若库是在增加 `embedding` 列之前创建的,执行:
|
|
225
|
-
|
|
226
|
-
```bash
|
|
227
|
-
mysql -u root -p code_intelligence < sql/migrations/003_add_embedding.sql
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
2. **Python 依赖**(建议虚拟环境;首次运行会下载模型权重,体积约数百 MB):
|
|
231
|
-
|
|
232
|
-
```bash
|
|
233
|
-
cd embedding-service
|
|
234
|
-
python3 -m venv .venv
|
|
235
|
-
source .venv/bin/activate
|
|
236
|
-
pip install -r requirements.txt
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
3. **启动嵌入服务**(默认 `127.0.0.1:8765`):
|
|
240
|
-
|
|
241
|
-
```bash
|
|
242
|
-
npm run embedding:dev
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
4. **`.env`** 增加 `EMBEDDING_SERVICE_URL=http://127.0.0.1:8765`,再执行 **`npm run index`** 或 MCP **`reindex`**(`dryRun=false`)写入向量。未配置 URL 时与 Phase 2 行为一致,不写入 `embedding`。
|
|
246
|
-
|
|
247
|
-
5. **`search_symbols`**:传入 `semantic: true` 可做自然语言检索;可选 `limit`(默认 20)。返回中会含 `semanticSimilarity`(余弦相似度)。当前实现按 `usage_count` 取最多 3000 条有向量的候选再精排;超大规模仓库请改为 ANN。
|
|
248
|
-
|
|
249
|
-
环境变量 **`EMBEDDING_MODEL`**(仅 Python):覆盖默认的 `all-MiniLM-L6-v2`。
|
|
250
|
-
|
|
251
|
-
## 7) VS Code 迁移
|
|
252
|
-
|
|
253
|
-
迁移步骤见 `docs/vscode-mcp-migration.md`。
|
|
254
|
-
|
|
255
|
-
# 使用说明
|
|
256
|
-
|
|
257
|
-
Run with:
|
|
258
|
-
|
|
259
|
-
````bash
|
|
260
|
-
- 脚本 cli 启动:npx code-intelligence-mcp(走mcp不执行)
|
|
261
|
-
- 给项目做索引,运行:npx code-intelligence-index, 项目根目录取配置或者cwd(重要,首次以及后续需要时执行:新项目必须执行一次建表)
|
|
262
|
-
---
|
|
263
|
-
|
|
264
|
-
### MCP 配置(核心)
|
|
265
|
-
|
|
266
|
-
```md
|
|
267
|
-
## MCP Config
|
|
268
|
-
|
|
269
|
-
```json
|
|
270
17
|
{
|
|
271
18
|
"mcpServers": {
|
|
272
|
-
"code-intelligence": {
|
|
19
|
+
"code-intelligence-mcp": {
|
|
273
20
|
"command": "npx",
|
|
274
|
-
"args": ["code-intelligence-mcp"]
|
|
21
|
+
"args": ["-y", "@lorrylurui/code-intelligence-mcp"]
|
|
275
22
|
}
|
|
276
23
|
}
|
|
277
24
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
### 支持的 Tools Prompts
|
|
281
|
-
|
|
282
|
-
```md
|
|
283
|
-
## Tools
|
|
284
|
-
|
|
285
|
-
- search_symbols
|
|
286
|
-
- get_symbol_detail
|
|
287
|
-
- search_by_structure
|
|
288
|
-
- recommend_component
|
|
289
|
-
- reindex
|
|
290
|
-
|
|
291
|
-
## Prompts
|
|
292
|
-
|
|
293
|
-
- recommend-component
|
|
294
|
-
- reusable-code-advisor
|
|
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
|
-
---
|
|
25
|
+
```
|
|
585
26
|
|
|
586
|
-
|
|
27
|
+
## 2)配置流水线
|
|
587
28
|
|
|
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
|
-
└────────────┴───────────────────────────────────────────────────┘
|
|
29
|
+
```
|
|
30
|
+
- uses: lorrylurui/code-intelligence-check@v1
|
|
31
|
+
```
|
|
611
32
|
|
|
612
|
-
|
|
33
|
+
## 3) 项目根目录环境变量
|
|
613
34
|
|
|
614
|
-
|
|
35
|
+
MYSQL\*SYMBOLS_TABLE=frontend_collections_symbols
|
|
36
|
+
INDEX_GLOB=interview-code-collection/\*\*/\_.{js,jsx,ts,tsx}
|
|
@@ -10,19 +10,17 @@
|
|
|
10
10
|
import { resolve } from 'node:path';
|
|
11
11
|
import { loadProjectDotenv } from '../config/env.js';
|
|
12
12
|
import { runReindex } from '../services/reindex.js';
|
|
13
|
-
// dotenv.config();
|
|
14
13
|
/**
|
|
15
14
|
* 入口:加载第三方 .env → 校验环境 → 调用 runReindex。
|
|
16
15
|
* 进度与统计输出到 **stderr**,避免占用 stdout。
|
|
17
16
|
* 进程退出码:成功 `0`,无 MySQL 或异常 `1`。
|
|
18
17
|
*/
|
|
19
18
|
async function main() {
|
|
20
|
-
// 2️ 确定项目根目录并加载第三方 .env(仅覆盖未定义的变量)
|
|
21
19
|
const projectRoot = resolve(process.env.INDEX_ROOT ?? process.cwd());
|
|
22
20
|
loadProjectDotenv(projectRoot);
|
|
23
|
-
console.error(`[index] projectRoot=${projectRoot}`);
|
|
24
21
|
console.error(`[index] MYSQL_ENABLED=${process.env.MYSQL_ENABLED}, ` +
|
|
25
|
-
`MYSQL_HOST=${process.env.MYSQL_HOST}`
|
|
22
|
+
`MYSQL_HOST=${process.env.MYSQL_HOST}` +
|
|
23
|
+
`[index] projectRoot=${projectRoot}`);
|
|
26
24
|
const globPatterns = process.env.INDEX_GLOB
|
|
27
25
|
? process.env.INDEX_GLOB.split(/\s+/)
|
|
28
26
|
.map((s) => s.trim())
|
package/dist/config/env.js
CHANGED
|
@@ -3,7 +3,6 @@ import path from 'node:path';
|
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { existsSync, readFileSync } from 'node:fs';
|
|
5
5
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
-
const projectRoot = __dirname;
|
|
7
6
|
// 解析命令行参数 --key=value 格式,注入到 process.env
|
|
8
7
|
for (const arg of process.argv) {
|
|
9
8
|
const match = arg.match(/^--([A-Z_][A-Z0-9_]*)=(.+)$/);
|
|
@@ -11,13 +10,8 @@ for (const arg of process.argv) {
|
|
|
11
10
|
process.env[match[1]] = match[2];
|
|
12
11
|
}
|
|
13
12
|
}
|
|
14
|
-
// 加载本地 .env(外部传入的 env 已经在 process.env 中,override: false 不会覆盖它们)
|
|
15
|
-
// dotenv.config({
|
|
16
|
-
// path: path.resolve(projectRoot, '.env'),
|
|
17
|
-
// override: false,
|
|
18
|
-
// });
|
|
19
13
|
// MCP Server 本地 .env 路径(固定指向项目根目录)
|
|
20
|
-
const MCP_SERVER_ROOT = path.resolve(__dirname, '..', '..'); // MCP Server 根目录
|
|
14
|
+
const MCP_SERVER_ROOT = path.resolve(__dirname, '..', '..', './dist'); // MCP Server 根目录
|
|
21
15
|
const MCP_SERVER_ENV_PATH = path.resolve(MCP_SERVER_ROOT, '.env');
|
|
22
16
|
dotenv.config({
|
|
23
17
|
path: MCP_SERVER_ENV_PATH,
|
|
@@ -28,9 +22,6 @@ dotenv.config({
|
|
|
28
22
|
* 行为:优先使用第三方显式设置的值,否则保留 MCP Server 本地配置
|
|
29
23
|
*/
|
|
30
24
|
export function loadProjectDotenv(projectRoot) {
|
|
31
|
-
// 始终确保 MCP Server 本地的 .env 被加载(补充加载,确保有默认值)
|
|
32
|
-
// if (existsSync(MCP_SERVER_ENV_PATH)) {
|
|
33
|
-
// }
|
|
34
25
|
const envPath = path.resolve(projectRoot, '.env');
|
|
35
26
|
if (!existsSync(envPath)) {
|
|
36
27
|
return;
|
|
@@ -69,9 +60,6 @@ export function loadProjectDotenv(projectRoot) {
|
|
|
69
60
|
}
|
|
70
61
|
}
|
|
71
62
|
}
|
|
72
|
-
// 尝试从第三方项目目录加载 .env,按变量维度覆盖(只覆盖第三方明确配置的变量)
|
|
73
|
-
// const clientProjectRoot = process.env.INDEX_ROOT || process.cwd();
|
|
74
|
-
// loadProjectDotenv(clientProjectRoot);
|
|
75
63
|
// 外部传入的 env 已在上一步保留,这里确保环境变量已正确设置
|
|
76
64
|
for (const arg of process.argv) {
|
|
77
65
|
const match = arg.match(/^--([A-Z_][A-Z0-9_]*)=(.+)$/);
|
|
@@ -113,87 +101,3 @@ export function validateEnv() {
|
|
|
113
101
|
}
|
|
114
102
|
}
|
|
115
103
|
}
|
|
116
|
-
// import dotenv from 'dotenv';
|
|
117
|
-
// import path from 'node:path';
|
|
118
|
-
// import { fileURLToPath } from 'node:url';
|
|
119
|
-
// import { existsSync, readFileSync } from 'node:fs';
|
|
120
|
-
// const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
121
|
-
// const projectRoot = path.resolve(__dirname, '../../');
|
|
122
|
-
// // 解析命令行参数 --key=value 格式,注入到 process.env
|
|
123
|
-
// for (const arg of process.argv) {
|
|
124
|
-
// const match = arg.match(/^--([A-Z_][A-Z0-9_]*)=(.+)$/);
|
|
125
|
-
// if (match) {
|
|
126
|
-
// process.env[match[1]] = match[2];
|
|
127
|
-
// }
|
|
128
|
-
// }
|
|
129
|
-
// // 加载本地 .env(外部传入的 env 已经在 process.env 中,override: false 不会覆盖它们)
|
|
130
|
-
// dotenv.config({
|
|
131
|
-
// path: path.resolve(projectRoot, '.env'),
|
|
132
|
-
// override: false,
|
|
133
|
-
// });
|
|
134
|
-
// // 尝试从第三方项目目录加载 .env,按变量维度覆盖(只覆盖第三方明确配置的变量)
|
|
135
|
-
// const clientProjectRoot = process.env.INDEX_ROOT || process.cwd();
|
|
136
|
-
// const clientEnvPath = path.resolve(clientProjectRoot, '.env');
|
|
137
|
-
// if (existsSync(clientEnvPath)) {
|
|
138
|
-
// console.error(
|
|
139
|
-
// `[Config] Merging .env from client project root: ${clientProjectRoot}`
|
|
140
|
-
// );
|
|
141
|
-
// // 手动解析第三方 .env,只覆盖其明确配置的变量
|
|
142
|
-
// const clientEnvContent = readFileSync(clientEnvPath, 'utf-8');
|
|
143
|
-
// for (const line of clientEnvContent.split('\n')) {
|
|
144
|
-
// const trimmed = line.trim();
|
|
145
|
-
// if (!trimmed || trimmed.startsWith('#')) continue;
|
|
146
|
-
// const eqIdx = trimmed.indexOf('=');
|
|
147
|
-
// if (eqIdx === -1) continue;
|
|
148
|
-
// const key = trimmed.slice(0, eqIdx).trim();
|
|
149
|
-
// const value = trimmed.slice(eqIdx + 1).trim();
|
|
150
|
-
// // 移除引号
|
|
151
|
-
// const cleanValue = value.replace(/^["']|["']$/g, '');
|
|
152
|
-
// if (key) {
|
|
153
|
-
// process.env[key] = cleanValue;
|
|
154
|
-
// }
|
|
155
|
-
// }
|
|
156
|
-
// }
|
|
157
|
-
// // 外部传入的 env 已在上一步保留,这里确保环境变量已正确设置
|
|
158
|
-
// for (const arg of process.argv) {
|
|
159
|
-
// const match = arg.match(/^--([A-Z_][A-Z0-9_]*)=(.+)$/);
|
|
160
|
-
// if (match) {
|
|
161
|
-
// process.env[match[1]] = match[2];
|
|
162
|
-
// }
|
|
163
|
-
// }
|
|
164
|
-
// const requiredWhenEnabled = [
|
|
165
|
-
// 'MYSQL_HOST',
|
|
166
|
-
// 'MYSQL_USER',
|
|
167
|
-
// 'MYSQL_DATABASE',
|
|
168
|
-
// ] as const;
|
|
169
|
-
// console.error(
|
|
170
|
-
// `[Config] MYSQL_ENABLED: ${process.env.MYSQL_ENABLED},
|
|
171
|
-
// MYSQL_HOST: ${process.env.MYSQL_HOST},
|
|
172
|
-
// MYSQL_USER: ${process.env.MYSQL_USER},
|
|
173
|
-
// MYSQL_DATABASE: ${process.env.MYSQL_DATABASE},
|
|
174
|
-
// EMBEDDING_SERVICE_URL: ${process.env.EMBEDDING_SERVICE_URL},
|
|
175
|
-
// MYSQL_SYMBOLS_TABLE: ${process.env.MYSQL_SYMBOLS_TABLE}
|
|
176
|
-
// `
|
|
177
|
-
// );
|
|
178
|
-
// export const env = {
|
|
179
|
-
// mysqlEnabled: process.env.MYSQL_ENABLED === 'true',
|
|
180
|
-
// mysqlHost: process.env.MYSQL_HOST ?? '127.0.0.1',
|
|
181
|
-
// mysqlPort: Number(process.env.MYSQL_PORT ?? '3306'),
|
|
182
|
-
// mysqlUser: process.env.MYSQL_USER ?? 'root',
|
|
183
|
-
// mysqlPassword: process.env.MYSQL_PASSWORD ?? '',
|
|
184
|
-
// mysqlDatabase: process.env.MYSQL_DATABASE ?? 'code_intelligence',
|
|
185
|
-
// /** symbols 表名,可通过 MYSQL_SYMBOLS_TABLE 环境变量配置 */
|
|
186
|
-
// mysqlSymbolsTable: process.env.MYSQL_SYMBOLS_TABLE ?? 'symbols',
|
|
187
|
-
// /** Phase 5:指向 Python FastAPI 嵌入服务根 URL,如 http://127.0.0.1:8765 */
|
|
188
|
-
// embeddingServiceUrl: (process.env.EMBEDDING_SERVICE_URL ?? '').trim(),
|
|
189
|
-
// };
|
|
190
|
-
// export function validateEnv(): void {
|
|
191
|
-
// if (!env.mysqlEnabled) {
|
|
192
|
-
// return;
|
|
193
|
-
// }
|
|
194
|
-
// for (const key of requiredWhenEnabled) {
|
|
195
|
-
// if (!process.env[key]) {
|
|
196
|
-
// throw new Error(`Missing environment variable: ${key}`);
|
|
197
|
-
// }
|
|
198
|
-
// }
|
|
199
|
-
// }
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
-
import {
|
|
3
|
+
import { loadProjectDotenv } from './config/env.js';
|
|
4
4
|
import { createServer } from './server/createServer.js';
|
|
5
5
|
async function main() {
|
|
6
|
-
|
|
6
|
+
// 加载第三方项目的 .env(通过 INDEX_ROOT 指定,或默认当前工作目录)
|
|
7
|
+
const projectRoot = process.env.INDEX_ROOT || process.cwd();
|
|
8
|
+
loadProjectDotenv(projectRoot);
|
|
7
9
|
const server = createServer();
|
|
8
10
|
const transport = new StdioServerTransport();
|
|
9
11
|
await server.connect(transport);
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import * as babelParser from '@babel/parser';
|
|
5
5
|
import * as bt from '@babel/types';
|
|
6
|
-
import { getRelativePathForDisplay, inferCategoryFromPath } from './heuristics.js';
|
|
6
|
+
import { getRelativePathForDisplay, inferCategoryFromPath, } from './heuristics.js';
|
|
7
7
|
/** 从 JS 文件内容解析导出的代码块 */
|
|
8
8
|
export function parseJsFile(filePath, content, projectRoot) {
|
|
9
9
|
const out = [];
|
|
@@ -70,10 +70,14 @@ function processStatement(stmt, filePath, isJsx, projectRoot) {
|
|
|
70
70
|
}
|
|
71
71
|
else if (bt.isVariableDeclaration(decl)) {
|
|
72
72
|
for (const declarator of decl.declarations) {
|
|
73
|
-
if (bt.isVariableDeclarator(declarator) &&
|
|
73
|
+
if (bt.isVariableDeclarator(declarator) &&
|
|
74
|
+
declarator.id &&
|
|
75
|
+
bt.isIdentifier(declarator.id)) {
|
|
74
76
|
const name = declarator.id.name;
|
|
75
77
|
const init = declarator.init;
|
|
76
|
-
if (name &&
|
|
78
|
+
if (name &&
|
|
79
|
+
(bt.isArrowFunctionExpression(init) ||
|
|
80
|
+
bt.isFunctionExpression(init))) {
|
|
77
81
|
const fnDecl = arrowToFunction(name, init);
|
|
78
82
|
out.push(createRowFromFunction(name, fnDecl, filePath, projectRoot, isJsx));
|
|
79
83
|
}
|
|
@@ -84,19 +88,26 @@ function processStatement(stmt, filePath, isJsx, projectRoot) {
|
|
|
84
88
|
// 处理 module.exports = xxx
|
|
85
89
|
else if (bt.isExpressionStatement(stmt)) {
|
|
86
90
|
const expr = stmt.expression;
|
|
87
|
-
if (bt.isAssignmentExpression(expr) &&
|
|
91
|
+
if (bt.isAssignmentExpression(expr) &&
|
|
92
|
+
bt.isMemberExpression(expr.left)) {
|
|
88
93
|
const left = expr.left;
|
|
89
94
|
// module.exports = xxx
|
|
90
|
-
if (bt.isIdentifier(left.object) &&
|
|
91
|
-
|
|
95
|
+
if (bt.isIdentifier(left.object) &&
|
|
96
|
+
left.object.name === 'module' &&
|
|
97
|
+
bt.isIdentifier(left.property) &&
|
|
98
|
+
left.property.name === 'exports') {
|
|
92
99
|
const right = expr.right;
|
|
93
100
|
if (bt.isObjectExpression(right)) {
|
|
94
101
|
for (const prop of right.properties) {
|
|
95
|
-
if (bt.isObjectProperty(prop) &&
|
|
102
|
+
if (bt.isObjectProperty(prop) &&
|
|
103
|
+
bt.isIdentifier(prop.key)) {
|
|
96
104
|
const name = prop.key.name;
|
|
97
105
|
const value = prop.value;
|
|
98
|
-
if (bt.isFunctionExpression(value) ||
|
|
99
|
-
|
|
106
|
+
if (bt.isFunctionExpression(value) ||
|
|
107
|
+
bt.isArrowFunctionExpression(value)) {
|
|
108
|
+
const fnDecl = arrowToFunction(name, bt.isArrowFunctionExpression(value)
|
|
109
|
+
? value
|
|
110
|
+
: value);
|
|
100
111
|
out.push(createRowFromFunction(name, fnDecl, filePath, projectRoot, isJsx));
|
|
101
112
|
}
|
|
102
113
|
}
|
|
@@ -112,11 +123,15 @@ function processStatement(stmt, filePath, isJsx, projectRoot) {
|
|
|
112
123
|
}
|
|
113
124
|
}
|
|
114
125
|
// exports.xxx = xxx
|
|
115
|
-
else if (bt.isIdentifier(left.object) &&
|
|
116
|
-
|
|
126
|
+
else if (bt.isIdentifier(left.object) &&
|
|
127
|
+
left.object.name === 'exports') {
|
|
128
|
+
const name = bt.isIdentifier(left.property)
|
|
129
|
+
? left.property.name
|
|
130
|
+
: null;
|
|
117
131
|
if (name) {
|
|
118
132
|
const right = expr.right;
|
|
119
|
-
if (bt.isFunctionExpression(right) ||
|
|
133
|
+
if (bt.isFunctionExpression(right) ||
|
|
134
|
+
bt.isArrowFunctionExpression(right)) {
|
|
120
135
|
const fnDecl = arrowToFunction(name, bt.isArrowFunctionExpression(right) ? right : right);
|
|
121
136
|
out.push(createRowFromFunction(name, fnDecl, filePath, projectRoot, isJsx));
|
|
122
137
|
}
|
|
@@ -166,10 +181,14 @@ function scanAllDeclarations(stmt, filePath, isJsx, projectRoot) {
|
|
|
166
181
|
// 变量声明: const foo = () => {}, const bar = function() {}
|
|
167
182
|
else if (bt.isVariableDeclaration(stmt)) {
|
|
168
183
|
for (const declarator of stmt.declarations) {
|
|
169
|
-
if (bt.isVariableDeclarator(declarator) &&
|
|
184
|
+
if (bt.isVariableDeclarator(declarator) &&
|
|
185
|
+
declarator.id &&
|
|
186
|
+
bt.isIdentifier(declarator.id)) {
|
|
170
187
|
const name = declarator.id.name;
|
|
171
188
|
const init = declarator.init;
|
|
172
|
-
if (name &&
|
|
189
|
+
if (name &&
|
|
190
|
+
(bt.isArrowFunctionExpression(init) ||
|
|
191
|
+
bt.isFunctionExpression(init))) {
|
|
173
192
|
const fnDecl = arrowToFunction(name, init);
|
|
174
193
|
out.push(createRowFromFunction(name, fnDecl, filePath, projectRoot, isJsx));
|
|
175
194
|
}
|
|
@@ -215,6 +234,8 @@ function createRowFromFunction(name, decl, filePath, projectRoot, isJsx) {
|
|
|
215
234
|
const params = decl.params
|
|
216
235
|
.filter((p) => bt.isIdentifier(p))
|
|
217
236
|
.map((p) => p.name);
|
|
237
|
+
const hooks = extractHooksFromBody(decl);
|
|
238
|
+
const sideEffects = extractSideEffects(decl);
|
|
218
239
|
return {
|
|
219
240
|
name,
|
|
220
241
|
type,
|
|
@@ -223,8 +244,11 @@ function createRowFromFunction(name, decl, filePath, projectRoot, isJsx) {
|
|
|
223
244
|
description: null,
|
|
224
245
|
content: `function ${decl.id?.name || 'anonymous'}(${params.join(', ')}) { ... }`,
|
|
225
246
|
meta: {
|
|
247
|
+
kind: 'function',
|
|
226
248
|
params,
|
|
227
249
|
returnType: getReturnType(decl),
|
|
250
|
+
...(hooks.length ? { hooks } : {}),
|
|
251
|
+
...(sideEffects.length ? { sideEffects } : {}),
|
|
228
252
|
},
|
|
229
253
|
};
|
|
230
254
|
}
|
|
@@ -253,7 +277,17 @@ function containsJsx(node) {
|
|
|
253
277
|
return;
|
|
254
278
|
}
|
|
255
279
|
// 只遍历常见的包含子节点的属性
|
|
256
|
-
const keys = [
|
|
280
|
+
const keys = [
|
|
281
|
+
'body',
|
|
282
|
+
'declarations',
|
|
283
|
+
'arguments',
|
|
284
|
+
'callee',
|
|
285
|
+
'init',
|
|
286
|
+
'left',
|
|
287
|
+
'right',
|
|
288
|
+
'consequent',
|
|
289
|
+
'alternate',
|
|
290
|
+
];
|
|
257
291
|
for (const key of keys) {
|
|
258
292
|
const val = n[key];
|
|
259
293
|
if (Array.isArray(val)) {
|
|
@@ -294,3 +328,173 @@ function getReturnType(fn) {
|
|
|
294
328
|
}
|
|
295
329
|
return undefined;
|
|
296
330
|
}
|
|
331
|
+
/**
|
|
332
|
+
* 遍历 Babel AST 节点,收集所有满足条件的回调
|
|
333
|
+
*/
|
|
334
|
+
function visitNodes(node, callback) {
|
|
335
|
+
callback(node);
|
|
336
|
+
for (const key of Object.keys(node)) {
|
|
337
|
+
const val = node[key];
|
|
338
|
+
if (Array.isArray(val)) {
|
|
339
|
+
for (const v of val) {
|
|
340
|
+
if (v && typeof v === 'object' && 'type' in v) {
|
|
341
|
+
visitNodes(v, callback);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
else if (val && typeof val === 'object' && 'type' in val) {
|
|
346
|
+
visitNodes(val, callback);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* 从函数体中提取 React Hooks(use 开头的函数调用)
|
|
352
|
+
*/
|
|
353
|
+
function extractHooksFromBody(fn) {
|
|
354
|
+
const seen = new Set();
|
|
355
|
+
const body = fn.body;
|
|
356
|
+
if (!body || !bt.isBlockStatement(body))
|
|
357
|
+
return [];
|
|
358
|
+
visitNodes(body, (n) => {
|
|
359
|
+
if (bt.isCallExpression(n)) {
|
|
360
|
+
const callee = n.callee;
|
|
361
|
+
if (bt.isIdentifier(callee) && callee.name.startsWith('use')) {
|
|
362
|
+
seen.add(callee.name);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
return [...seen].sort();
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* 获取节点的文本表示(通过 AST 节点属性构建)
|
|
370
|
+
*/
|
|
371
|
+
function getNodeText(n) {
|
|
372
|
+
const node = n;
|
|
373
|
+
if (!node || typeof node !== 'object')
|
|
374
|
+
return '';
|
|
375
|
+
const type = node.type;
|
|
376
|
+
if (type === 'MemberExpression' || type === 'OptionalMemberExpression') {
|
|
377
|
+
const obj = getNodeText(node.object);
|
|
378
|
+
const propNode = node.property;
|
|
379
|
+
let prop = '';
|
|
380
|
+
if (propNode && typeof propNode === 'object') {
|
|
381
|
+
const propType = propNode.type;
|
|
382
|
+
if (propType === 'Identifier') {
|
|
383
|
+
prop = propNode.name || '';
|
|
384
|
+
}
|
|
385
|
+
else if (propType === 'Literal') {
|
|
386
|
+
prop = String(propNode.value ?? '');
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
const computed = node.computed;
|
|
390
|
+
return obj + (computed ? `[${prop}]` : `.${prop}`);
|
|
391
|
+
}
|
|
392
|
+
if (type === 'Identifier') {
|
|
393
|
+
return node.name || '';
|
|
394
|
+
}
|
|
395
|
+
if (type === 'Literal' || type === 'NullLiteral') {
|
|
396
|
+
const val = node.value;
|
|
397
|
+
return val === null ? 'null' : String(val);
|
|
398
|
+
}
|
|
399
|
+
if (type === 'CallExpression' || type === 'OptionalCallExpression') {
|
|
400
|
+
const callee = getNodeText(node.callee);
|
|
401
|
+
return callee + '(...)';
|
|
402
|
+
}
|
|
403
|
+
if (type === 'AssignmentExpression') {
|
|
404
|
+
const left = getNodeText(node.left);
|
|
405
|
+
return left + ' = ...';
|
|
406
|
+
}
|
|
407
|
+
return '';
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* 静态分析函数体的副作用
|
|
411
|
+
*/
|
|
412
|
+
function extractSideEffects(fn) {
|
|
413
|
+
const effects = new Set();
|
|
414
|
+
const body = fn.body;
|
|
415
|
+
if (!body || !bt.isBlockStatement(body))
|
|
416
|
+
return [];
|
|
417
|
+
const paramNames = new Set(fn.params
|
|
418
|
+
.filter((p) => bt.isIdentifier(p))
|
|
419
|
+
.map((p) => p.name));
|
|
420
|
+
visitNodes(body, (n) => {
|
|
421
|
+
// 1. 网络请求
|
|
422
|
+
if (bt.isCallExpression(n)) {
|
|
423
|
+
const calleeText = n.callee && 'name' in n.callee
|
|
424
|
+
? n.callee.name
|
|
425
|
+
: '';
|
|
426
|
+
const calleeTextLower = calleeText.toLowerCase();
|
|
427
|
+
if (calleeTextLower === 'fetch' ||
|
|
428
|
+
calleeTextLower === 'axios' ||
|
|
429
|
+
calleeTextLower === 'xhr' ||
|
|
430
|
+
calleeTextLower === 'ajax' ||
|
|
431
|
+
calleeText.startsWith('axios.') ||
|
|
432
|
+
calleeTextLower.includes('request')) {
|
|
433
|
+
effects.add('network');
|
|
434
|
+
}
|
|
435
|
+
if (calleeTextLower.includes('xmlhttprequest')) {
|
|
436
|
+
effects.add('network');
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
// 2. 计时器
|
|
440
|
+
if (bt.isCallExpression(n)) {
|
|
441
|
+
const calleeName = n.callee && 'name' in n.callee
|
|
442
|
+
? n.callee.name
|
|
443
|
+
: '';
|
|
444
|
+
if (calleeName === 'setTimeout' ||
|
|
445
|
+
calleeName === 'setInterval' ||
|
|
446
|
+
calleeName === 'requestAnimationFrame' ||
|
|
447
|
+
calleeName === 'setImmediate') {
|
|
448
|
+
effects.add('timer');
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
// 3. DOM/全局对象操作
|
|
452
|
+
if (bt.isExpressionStatement(n)) {
|
|
453
|
+
const text = getNodeText(n.expression);
|
|
454
|
+
if (/\bdocument\.\w+/.test(text) ||
|
|
455
|
+
/\bwindow\.\w+/.test(text) ||
|
|
456
|
+
/\bnavigator\.\w+/.test(text) ||
|
|
457
|
+
/\blocation\.\w+/.test(text)) {
|
|
458
|
+
if (/=/.test(text) &&
|
|
459
|
+
!text.includes('===') &&
|
|
460
|
+
!text.includes('==')) {
|
|
461
|
+
effects.add('dom');
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
// 4. 存储操作
|
|
466
|
+
if (bt.isCallExpression(n)) {
|
|
467
|
+
const text = getNodeText(n);
|
|
468
|
+
if (text.includes('localStorage') ||
|
|
469
|
+
text.includes('sessionStorage') ||
|
|
470
|
+
text.includes('cookie')) {
|
|
471
|
+
effects.add('storage');
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
// 5. 入参修改
|
|
475
|
+
if (bt.isAssignmentExpression(n)) {
|
|
476
|
+
const leftText = getNodeText(n.left);
|
|
477
|
+
for (const param of paramNames) {
|
|
478
|
+
if (leftText.startsWith(`${param}.`) ||
|
|
479
|
+
leftText.startsWith(`${param}[`)) {
|
|
480
|
+
effects.add('mutation');
|
|
481
|
+
break;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
if (bt.isCallExpression(n) && n.callee) {
|
|
486
|
+
const calleeText = getNodeText(n.callee);
|
|
487
|
+
for (const param of paramNames) {
|
|
488
|
+
if (calleeText.startsWith(`${param}.`) ||
|
|
489
|
+
calleeText.startsWith(`${param}[`)) {
|
|
490
|
+
// 检测 push/pop/splice 等 mutations
|
|
491
|
+
if (/\.(push|pop|shift|unshift|splice|sort|reverse|fill)\(/.test(calleeText)) {
|
|
492
|
+
effects.add('mutation');
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
return [...effects].sort();
|
|
500
|
+
}
|
|
@@ -87,6 +87,7 @@ export function extractFunctionMeta(fn) {
|
|
|
87
87
|
const returnType = extractReturnTypeText(fn);
|
|
88
88
|
const sideEffects = extractSideEffects(fn);
|
|
89
89
|
return {
|
|
90
|
+
kind: 'function',
|
|
90
91
|
params,
|
|
91
92
|
...(paramTypeFields.length ? { paramTypeFields } : {}),
|
|
92
93
|
...(hooks.length ? { hooks } : {}),
|
|
@@ -116,6 +117,7 @@ export function extractInterfaceOrTypeMeta(node) {
|
|
|
116
117
|
if (Node.isTypeAliasDeclaration(node)) {
|
|
117
118
|
return { kind: 'typeAlias' };
|
|
118
119
|
}
|
|
120
|
+
// 其他类型(如 enum)暂不处理,标记为 unknown 以供后续扩展。
|
|
119
121
|
return { kind: 'unknown' };
|
|
120
122
|
}
|
|
121
123
|
/**
|
|
@@ -166,7 +168,9 @@ export function extractSideEffects(node) {
|
|
|
166
168
|
/\bnavigator\.\w+/.test(text) ||
|
|
167
169
|
/\blocation\.\w+/.test(text)) {
|
|
168
170
|
// 区分读取和写入
|
|
169
|
-
if (/=/.test(text) &&
|
|
171
|
+
if (/=/.test(text) &&
|
|
172
|
+
!text.includes('===') &&
|
|
173
|
+
!text.includes('==')) {
|
|
170
174
|
effects.add('dom');
|
|
171
175
|
}
|
|
172
176
|
}
|
|
@@ -199,7 +203,8 @@ export function extractSideEffects(node) {
|
|
|
199
203
|
const text = n.getText();
|
|
200
204
|
// 检测 param.x = ... 或 param.push/pop/splice 等
|
|
201
205
|
for (const param of paramNames) {
|
|
202
|
-
if (text.includes(`${param}.`) ||
|
|
206
|
+
if (text.includes(`${param}.`) ||
|
|
207
|
+
text.startsWith(`${param} =`)) {
|
|
203
208
|
effects.add('mutation');
|
|
204
209
|
break;
|
|
205
210
|
}
|