@lorrylurui/code-intelligence-mcp 1.0.1 → 1.0.3
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 +25 -6
- package/dist/cli/detect-duplicates.js +1 -1
- package/dist/cli/index-codebase.js +20 -15
- package/dist/config/env.js +52 -10
- package/dist/db/schema.js +29 -0
- package/dist/indexer/babelParser.js +296 -0
- package/dist/indexer/indexProject.js +42 -14
- package/dist/indexer/persistSymbols.js +7 -4
- package/dist/repositories/symbolRepository.js +4 -4
- package/dist/services/reindex.js +16 -12
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -79,6 +79,24 @@ docker compose ps
|
|
|
79
79
|
mysql -u root -p code_intelligence < sql/schema.sql
|
|
80
80
|
```
|
|
81
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
|
+
|
|
82
100
|
## 4) 本地运行
|
|
83
101
|
|
|
84
102
|
### 普通开发(热更新)
|
|
@@ -138,11 +156,11 @@ npm run index
|
|
|
138
156
|
|
|
139
157
|
可选环境变量(见 `.env.example`):
|
|
140
158
|
|
|
141
|
-
| 变量 | 含义
|
|
142
|
-
| -------------- |
|
|
143
|
-
| `INDEX_ROOT` | 工程根目录,默认当前工作目录
|
|
144
|
-
| `INDEX_GLOB` |
|
|
145
|
-
| `INDEX_IGNORE` | 额外忽略的 glob
|
|
159
|
+
| 变量 | 含义 |
|
|
160
|
+
| -------------- | ------------------------------------------------ |
|
|
161
|
+
| `INDEX_ROOT` | 工程根目录,默认当前工作目录 |
|
|
162
|
+
| `INDEX_GLOB` | 空格分隔 glob,默认 `src/**/*.{ts,tsx}` |
|
|
163
|
+
| `INDEX_IGNORE` | 额外忽略的 glob 片段(空格分隔) |
|
|
146
164
|
|
|
147
165
|
**分类规则(首版启发式)**:`interface` / `type` → `type`;`.tsx` 且函数体含 JSX → `component`;路径或导出名含 `selector` → `selector`;其余导出函数 → `util`;`class` → `util`(可后续细化)。
|
|
148
166
|
|
|
@@ -239,7 +257,8 @@ npm run embedding:dev
|
|
|
239
257
|
Run with:
|
|
240
258
|
|
|
241
259
|
````bash
|
|
242
|
-
npx code-intelligence-mcp
|
|
260
|
+
- 脚本 cli 启动:npx code-intelligence-mcp(走mcp不执行)
|
|
261
|
+
- 给项目做索引,运行:npx code-intelligence-index, 项目根目录取配置或者cwd(重要,首次以及后续需要时执行:新项目必须执行一次建表)
|
|
243
262
|
---
|
|
244
263
|
|
|
245
264
|
### MCP 配置(核心)
|
|
@@ -172,7 +172,7 @@ async function main() {
|
|
|
172
172
|
return [];
|
|
173
173
|
const [dbRows] = await mysqlPool.query(`
|
|
174
174
|
SELECT id, name, type, path, CAST(meta AS CHAR) AS meta, embedding
|
|
175
|
-
FROM
|
|
175
|
+
FROM ${env.mysqlSymbolsTable}
|
|
176
176
|
WHERE type = ? AND embedding IS NOT NULL
|
|
177
177
|
ORDER BY usage_count DESC
|
|
178
178
|
LIMIT ?
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Phase 2 CLI:扫描代码库并写入 MySQL `symbols`(需 `MYSQL_ENABLED=true`)。
|
|
4
4
|
*/
|
|
5
|
-
import { resolve } from
|
|
6
|
-
import dotenv from
|
|
7
|
-
import { runReindex } from
|
|
5
|
+
import { resolve } from 'node:path';
|
|
6
|
+
import dotenv from 'dotenv';
|
|
7
|
+
import { runReindex } from '../services/reindex.js';
|
|
8
8
|
dotenv.config();
|
|
9
9
|
/**
|
|
10
10
|
* 入口:校验环境 → 连接池 → 按 `INDEX_*` 调用 `indexProject` → `upsertSymbols`。
|
|
@@ -14,30 +14,35 @@ dotenv.config();
|
|
|
14
14
|
async function main() {
|
|
15
15
|
const projectRoot = resolve(process.env.INDEX_ROOT ?? process.cwd());
|
|
16
16
|
const globPatterns = process.env.INDEX_GLOB
|
|
17
|
-
? process.env.INDEX_GLOB.split(
|
|
17
|
+
? process.env.INDEX_GLOB.split(/\s+/).map((s) => s.trim()).filter(Boolean)
|
|
18
18
|
: undefined;
|
|
19
19
|
const ignore = process.env.INDEX_IGNORE
|
|
20
|
-
? process.env.INDEX_IGNORE.split(
|
|
20
|
+
? process.env.INDEX_IGNORE.split(',').map((s) => s.trim())
|
|
21
21
|
: undefined;
|
|
22
22
|
console.error(`[index] projectRoot=${projectRoot}`);
|
|
23
|
-
const result = await runReindex({
|
|
23
|
+
const result = await runReindex({
|
|
24
|
+
projectRoot,
|
|
25
|
+
globPatterns,
|
|
26
|
+
ignore,
|
|
27
|
+
dryRun: false,
|
|
28
|
+
});
|
|
24
29
|
console.error(`[index] extracted ${result.extractedCount} symbol(s)`);
|
|
25
30
|
console.error(`[index] embeddings computed: ${result.embeddingsComputed}`);
|
|
26
|
-
console.error(
|
|
31
|
+
console.error('[index] upserted into MySQL, success:', result.upserted);
|
|
27
32
|
}
|
|
28
33
|
main().catch((err) => {
|
|
29
|
-
console.error(
|
|
34
|
+
console.error('[index] failed:', err);
|
|
30
35
|
const anyErr = err;
|
|
31
|
-
if (anyErr.code ===
|
|
32
|
-
const host = process.env.MYSQL_HOST ??
|
|
33
|
-
const port = process.env.MYSQL_PORT ??
|
|
36
|
+
if (anyErr.code === 'ECONNREFUSED') {
|
|
37
|
+
const host = process.env.MYSQL_HOST ?? '127.0.0.1';
|
|
38
|
+
const port = process.env.MYSQL_PORT ?? '3306';
|
|
34
39
|
console.error(`[index] 原因: 无法连接 ${host}:${port}(连接被拒绝)。请先在本机启动 MySQL/MariaDB,或把 .env 里的 MYSQL_HOST / MYSQL_PORT 改成实际地址。macOS 可用 brew services start mysql 等方式启动。`);
|
|
35
40
|
}
|
|
36
|
-
else if (anyErr.code ===
|
|
37
|
-
console.error(
|
|
41
|
+
else if (anyErr.code === 'ER_ACCESS_DENIED_ERROR') {
|
|
42
|
+
console.error('[index] 原因: 用户名或密码错误,请检查 MYSQL_USER / MYSQL_PASSWORD。');
|
|
38
43
|
}
|
|
39
|
-
else if (anyErr.code ===
|
|
40
|
-
console.error(
|
|
44
|
+
else if (anyErr.code === 'ENOTFOUND' || anyErr.code === 'ETIMEDOUT') {
|
|
45
|
+
console.error('[index] 原因: 网络不可达或超时,请检查 MYSQL_HOST 是否可解析、防火墙与安全组。');
|
|
41
46
|
}
|
|
42
47
|
process.exit(1);
|
|
43
48
|
});
|
package/dist/config/env.js
CHANGED
|
@@ -1,15 +1,57 @@
|
|
|
1
|
-
import dotenv from
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import dotenv from 'dotenv';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
const projectRoot = path.resolve(__dirname, '../../');
|
|
6
|
+
// 解析命令行参数 --key=value 格式,注入到 process.env
|
|
7
|
+
for (const arg of process.argv) {
|
|
8
|
+
const match = arg.match(/^--([A-Z_][A-Z0-9_]*)=(.+)$/);
|
|
9
|
+
if (match) {
|
|
10
|
+
process.env[match[1]] = match[2];
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
// 加载本地 .env(外部传入的 env 已经在 process.env 中,override: false 不会覆盖它们)
|
|
14
|
+
dotenv.config({
|
|
15
|
+
path: path.resolve(projectRoot, '.env'),
|
|
16
|
+
override: false,
|
|
17
|
+
});
|
|
18
|
+
// 尝试从第三方项目目录加载 .env(INDEX_ROOT 或 cwd)
|
|
19
|
+
const clientProjectRoot = process.env.INDEX_ROOT || process.cwd();
|
|
20
|
+
console.error(`[Config] Loading .env from client project root: ${clientProjectRoot}`);
|
|
21
|
+
dotenv.config({
|
|
22
|
+
path: path.resolve(clientProjectRoot, '.env'),
|
|
23
|
+
override: true,
|
|
24
|
+
});
|
|
25
|
+
// 外部传入的 env 已在上一步保留,这里确保环境变量已正确设置
|
|
26
|
+
for (const arg of process.argv) {
|
|
27
|
+
const match = arg.match(/^--([A-Z_][A-Z0-9_]*)=(.+)$/);
|
|
28
|
+
if (match) {
|
|
29
|
+
process.env[match[1]] = match[2];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const requiredWhenEnabled = [
|
|
33
|
+
'MYSQL_HOST',
|
|
34
|
+
'MYSQL_USER',
|
|
35
|
+
'MYSQL_DATABASE',
|
|
36
|
+
];
|
|
37
|
+
console.error(`[Config] MYSQL_ENABLED: ${process.env.MYSQL_ENABLED},
|
|
38
|
+
MYSQL_HOST: ${process.env.MYSQL_HOST},
|
|
39
|
+
MYSQL_USER: ${process.env.MYSQL_USER},
|
|
40
|
+
MYSQL_DATABASE: ${process.env.MYSQL_DATABASE},
|
|
41
|
+
EMBEDDING_SERVICE_URL: ${process.env.EMBEDDING_SERVICE_URL},
|
|
42
|
+
MYSQL_SYMBOLS_TABLE: ${process.env.MYSQL_SYMBOLS_TABLE}
|
|
43
|
+
`);
|
|
4
44
|
export const env = {
|
|
5
|
-
mysqlEnabled: process.env.MYSQL_ENABLED ===
|
|
6
|
-
mysqlHost: process.env.MYSQL_HOST ??
|
|
7
|
-
mysqlPort: Number(process.env.MYSQL_PORT ??
|
|
8
|
-
mysqlUser: process.env.MYSQL_USER ??
|
|
9
|
-
mysqlPassword: process.env.MYSQL_PASSWORD ??
|
|
10
|
-
mysqlDatabase: process.env.MYSQL_DATABASE ??
|
|
45
|
+
mysqlEnabled: process.env.MYSQL_ENABLED === 'true',
|
|
46
|
+
mysqlHost: process.env.MYSQL_HOST ?? '127.0.0.1',
|
|
47
|
+
mysqlPort: Number(process.env.MYSQL_PORT ?? '3306'),
|
|
48
|
+
mysqlUser: process.env.MYSQL_USER ?? 'root',
|
|
49
|
+
mysqlPassword: process.env.MYSQL_PASSWORD ?? '',
|
|
50
|
+
mysqlDatabase: process.env.MYSQL_DATABASE ?? 'code_intelligence',
|
|
51
|
+
/** symbols 表名,可通过 MYSQL_SYMBOLS_TABLE 环境变量配置 */
|
|
52
|
+
mysqlSymbolsTable: process.env.MYSQL_SYMBOLS_TABLE ?? 'symbols',
|
|
11
53
|
/** Phase 5:指向 Python FastAPI 嵌入服务根 URL,如 http://127.0.0.1:8765 */
|
|
12
|
-
embeddingServiceUrl: (process.env.EMBEDDING_SERVICE_URL ??
|
|
54
|
+
embeddingServiceUrl: (process.env.EMBEDDING_SERVICE_URL ?? '').trim(),
|
|
13
55
|
};
|
|
14
56
|
export function validateEnv() {
|
|
15
57
|
if (!env.mysqlEnabled) {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 动态生成数据库表结构 SQL,表名可通过环境变量配置
|
|
3
|
+
*/
|
|
4
|
+
import { env } from '../config/env.js';
|
|
5
|
+
/** 获取 symbols 表的建表 SQL */
|
|
6
|
+
export function getSymbolsTableSQL() {
|
|
7
|
+
const tableName = env.mysqlSymbolsTable;
|
|
8
|
+
return `CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
9
|
+
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
10
|
+
name VARCHAR(255) NOT NULL,
|
|
11
|
+
type ENUM('component', 'util', 'selector', 'type') NOT NULL,
|
|
12
|
+
category VARCHAR(255) NULL,
|
|
13
|
+
path TEXT NOT NULL,
|
|
14
|
+
description TEXT NULL,
|
|
15
|
+
content MEDIUMTEXT NULL,
|
|
16
|
+
meta JSON NULL,
|
|
17
|
+
usage_count INT NOT NULL DEFAULT 0,
|
|
18
|
+
embedding JSON NULL COMMENT 'Phase 5: L2-normalized vector from Python embedding service (e.g. 384-dim MiniLM)',
|
|
19
|
+
insert_user VARCHAR(255) NOT NULL DEFAULT 'LorryIsLuRui',
|
|
20
|
+
updated_user VARCHAR(255) NOT NULL DEFAULT 'LorryIsLuRui',
|
|
21
|
+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
22
|
+
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
23
|
+
UNIQUE KEY uk_symbols_path_name (path(512), name(255))
|
|
24
|
+
)`;
|
|
25
|
+
}
|
|
26
|
+
/** 获取所有建表 SQL(可一次性执行) */
|
|
27
|
+
export function getAllTableSQLs() {
|
|
28
|
+
return [getSymbolsTableSQL()];
|
|
29
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 使用 Babel 解析 JS/JSX 文件,提取导出声明以及无导出文件中的所有函数/类
|
|
3
|
+
*/
|
|
4
|
+
import * as babelParser from '@babel/parser';
|
|
5
|
+
import * as bt from '@babel/types';
|
|
6
|
+
import { getRelativePathForDisplay, inferCategoryFromPath } from './heuristics.js';
|
|
7
|
+
/** 从 JS 文件内容解析导出的代码块 */
|
|
8
|
+
export function parseJsFile(filePath, content, projectRoot) {
|
|
9
|
+
const out = [];
|
|
10
|
+
const isJsx = filePath.endsWith('.jsx') || filePath.endsWith('.tsx');
|
|
11
|
+
let ast;
|
|
12
|
+
try {
|
|
13
|
+
ast = babelParser.parse(content, {
|
|
14
|
+
sourceType: 'module',
|
|
15
|
+
plugins: [
|
|
16
|
+
'jsx',
|
|
17
|
+
'typescript',
|
|
18
|
+
'classPrivateMethods',
|
|
19
|
+
'classPrivateProperties',
|
|
20
|
+
'decorators-legacy',
|
|
21
|
+
'doExpressions',
|
|
22
|
+
'exportDefaultFrom',
|
|
23
|
+
'functionBind',
|
|
24
|
+
'logicalAssignment',
|
|
25
|
+
'nullishCoalescingOperator',
|
|
26
|
+
'objectRestSpread',
|
|
27
|
+
'optionalChaining',
|
|
28
|
+
'optionalCatchBinding',
|
|
29
|
+
],
|
|
30
|
+
strictMode: false,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
console.error(`[babelParser] Failed to parse ${filePath}:`, e);
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
// 第一轮:处理所有导出声明
|
|
38
|
+
for (const stmt of ast.program.body) {
|
|
39
|
+
const rows = processStatement(stmt, filePath, isJsx, projectRoot);
|
|
40
|
+
out.push(...rows);
|
|
41
|
+
}
|
|
42
|
+
// 第二轮:如果没有任何导出,则扫描所有函数/类
|
|
43
|
+
if (out.length === 0) {
|
|
44
|
+
for (const stmt of ast.program.body) {
|
|
45
|
+
const rows = scanAllDeclarations(stmt, filePath, isJsx, projectRoot);
|
|
46
|
+
out.push(...rows);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
/** 处理导出的声明 */
|
|
52
|
+
function processStatement(stmt, filePath, isJsx, projectRoot) {
|
|
53
|
+
const out = [];
|
|
54
|
+
// 处理命名导出: export function Foo() {}
|
|
55
|
+
if (bt.isExportNamedDeclaration(stmt)) {
|
|
56
|
+
const decl = stmt.declaration;
|
|
57
|
+
if (!decl)
|
|
58
|
+
return out;
|
|
59
|
+
if (bt.isFunctionDeclaration(decl)) {
|
|
60
|
+
const name = decl.id?.name;
|
|
61
|
+
if (name) {
|
|
62
|
+
out.push(createRowFromFunction(name, decl, filePath, projectRoot, isJsx));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else if (bt.isClassDeclaration(decl)) {
|
|
66
|
+
const name = decl.id?.name;
|
|
67
|
+
if (name) {
|
|
68
|
+
out.push(createRowFromClass(name, decl, filePath, projectRoot));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else if (bt.isVariableDeclaration(decl)) {
|
|
72
|
+
for (const declarator of decl.declarations) {
|
|
73
|
+
if (bt.isVariableDeclarator(declarator) && declarator.id && bt.isIdentifier(declarator.id)) {
|
|
74
|
+
const name = declarator.id.name;
|
|
75
|
+
const init = declarator.init;
|
|
76
|
+
if (name && (bt.isArrowFunctionExpression(init) || bt.isFunctionExpression(init))) {
|
|
77
|
+
const fnDecl = arrowToFunction(name, init);
|
|
78
|
+
out.push(createRowFromFunction(name, fnDecl, filePath, projectRoot, isJsx));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// 处理 module.exports = xxx
|
|
85
|
+
else if (bt.isExpressionStatement(stmt)) {
|
|
86
|
+
const expr = stmt.expression;
|
|
87
|
+
if (bt.isAssignmentExpression(expr) && bt.isMemberExpression(expr.left)) {
|
|
88
|
+
const left = expr.left;
|
|
89
|
+
// module.exports = xxx
|
|
90
|
+
if (bt.isIdentifier(left.object) && left.object.name === 'module' &&
|
|
91
|
+
bt.isIdentifier(left.property) && left.property.name === 'exports') {
|
|
92
|
+
const right = expr.right;
|
|
93
|
+
if (bt.isObjectExpression(right)) {
|
|
94
|
+
for (const prop of right.properties) {
|
|
95
|
+
if (bt.isObjectProperty(prop) && bt.isIdentifier(prop.key)) {
|
|
96
|
+
const name = prop.key.name;
|
|
97
|
+
const value = prop.value;
|
|
98
|
+
if (bt.isFunctionExpression(value) || bt.isArrowFunctionExpression(value)) {
|
|
99
|
+
const fnDecl = arrowToFunction(name, bt.isArrowFunctionExpression(value) ? value : value);
|
|
100
|
+
out.push(createRowFromFunction(name, fnDecl, filePath, projectRoot, isJsx));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
else if (bt.isFunctionExpression(right)) {
|
|
106
|
+
const fnDecl = arrowToFunction('default', right);
|
|
107
|
+
out.push(createRowFromFunction('default', fnDecl, filePath, projectRoot, isJsx));
|
|
108
|
+
}
|
|
109
|
+
else if (bt.isArrowFunctionExpression(right)) {
|
|
110
|
+
const fnDecl = arrowToFunction('default', right);
|
|
111
|
+
out.push(createRowFromFunction('default', fnDecl, filePath, projectRoot, isJsx));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// exports.xxx = xxx
|
|
115
|
+
else if (bt.isIdentifier(left.object) && left.object.name === 'exports') {
|
|
116
|
+
const name = bt.isIdentifier(left.property) ? left.property.name : null;
|
|
117
|
+
if (name) {
|
|
118
|
+
const right = expr.right;
|
|
119
|
+
if (bt.isFunctionExpression(right) || bt.isArrowFunctionExpression(right)) {
|
|
120
|
+
const fnDecl = arrowToFunction(name, bt.isArrowFunctionExpression(right) ? right : right);
|
|
121
|
+
out.push(createRowFromFunction(name, fnDecl, filePath, projectRoot, isJsx));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// 处理默认导出
|
|
128
|
+
else if (bt.isExportDefaultDeclaration(stmt)) {
|
|
129
|
+
const decl = stmt.declaration;
|
|
130
|
+
if (bt.isFunctionDeclaration(decl)) {
|
|
131
|
+
const name = decl.id?.name || 'default';
|
|
132
|
+
out.push(createRowFromFunction(name, decl, filePath, projectRoot, isJsx));
|
|
133
|
+
}
|
|
134
|
+
else if (bt.isClassDeclaration(decl)) {
|
|
135
|
+
const name = decl.id?.name || 'default';
|
|
136
|
+
out.push(createRowFromClass(name, decl, filePath, projectRoot));
|
|
137
|
+
}
|
|
138
|
+
else if (bt.isArrowFunctionExpression(decl)) {
|
|
139
|
+
const fnDecl = arrowToFunction('default', decl);
|
|
140
|
+
out.push(createRowFromFunction('default', fnDecl, filePath, projectRoot, isJsx));
|
|
141
|
+
}
|
|
142
|
+
else if (bt.isFunctionExpression(decl)) {
|
|
143
|
+
const fnDecl = arrowToFunction('default', decl);
|
|
144
|
+
out.push(createRowFromFunction('default', fnDecl, filePath, projectRoot, isJsx));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
/** 扫描无导出文件中的所有声明 */
|
|
150
|
+
function scanAllDeclarations(stmt, filePath, isJsx, projectRoot) {
|
|
151
|
+
const out = [];
|
|
152
|
+
// 函数声明: function foo() {}
|
|
153
|
+
if (bt.isFunctionDeclaration(stmt)) {
|
|
154
|
+
const name = stmt.id?.name;
|
|
155
|
+
if (name) {
|
|
156
|
+
out.push(createRowFromFunction(name, stmt, filePath, projectRoot, isJsx));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// 类声明: class Foo {}
|
|
160
|
+
else if (bt.isClassDeclaration(stmt)) {
|
|
161
|
+
const name = stmt.id?.name;
|
|
162
|
+
if (name) {
|
|
163
|
+
out.push(createRowFromClass(name, stmt, filePath, projectRoot));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// 变量声明: const foo = () => {}, const bar = function() {}
|
|
167
|
+
else if (bt.isVariableDeclaration(stmt)) {
|
|
168
|
+
for (const declarator of stmt.declarations) {
|
|
169
|
+
if (bt.isVariableDeclarator(declarator) && declarator.id && bt.isIdentifier(declarator.id)) {
|
|
170
|
+
const name = declarator.id.name;
|
|
171
|
+
const init = declarator.init;
|
|
172
|
+
if (name && (bt.isArrowFunctionExpression(init) || bt.isFunctionExpression(init))) {
|
|
173
|
+
const fnDecl = arrowToFunction(name, init);
|
|
174
|
+
out.push(createRowFromFunction(name, fnDecl, filePath, projectRoot, isJsx));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
/** 将 ArrowFunctionExpression 或 FunctionExpression 转换为 FunctionDeclaration */
|
|
182
|
+
function arrowToFunction(name, arrow) {
|
|
183
|
+
const body = arrow.body;
|
|
184
|
+
const blockBody = bt.isBlockStatement(body)
|
|
185
|
+
? body
|
|
186
|
+
: bt.blockStatement([bt.returnStatement(body)]);
|
|
187
|
+
return {
|
|
188
|
+
type: 'FunctionDeclaration',
|
|
189
|
+
id: { type: 'Identifier', name },
|
|
190
|
+
params: arrow.params,
|
|
191
|
+
body: blockBody,
|
|
192
|
+
generator: false,
|
|
193
|
+
async: arrow.async,
|
|
194
|
+
returnType: arrow.returnType,
|
|
195
|
+
typeParameters: arrow.typeParameters,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function createRowFromFunction(name, decl, filePath, projectRoot, isJsx) {
|
|
199
|
+
const relPath = getRelativePathForDisplay(projectRoot, filePath);
|
|
200
|
+
const category = inferCategoryFromPath(filePath);
|
|
201
|
+
// 检测是否有 JSX
|
|
202
|
+
const hasJsx = isJsx || containsJsx(decl);
|
|
203
|
+
// 判断类型:
|
|
204
|
+
// 1. 有 JSX = component
|
|
205
|
+
// 2. 名字包含 selector = selector
|
|
206
|
+
// 3. 大写开头(JSX 组件约定)= component
|
|
207
|
+
// 4. 其他 = util
|
|
208
|
+
const type = hasJsx
|
|
209
|
+
? 'component'
|
|
210
|
+
: name.toLowerCase().includes('selector')
|
|
211
|
+
? 'selector'
|
|
212
|
+
: isJsx && /^[A-Z]/.test(name)
|
|
213
|
+
? 'component'
|
|
214
|
+
: 'util';
|
|
215
|
+
const params = decl.params
|
|
216
|
+
.filter((p) => bt.isIdentifier(p))
|
|
217
|
+
.map((p) => p.name);
|
|
218
|
+
return {
|
|
219
|
+
name,
|
|
220
|
+
type,
|
|
221
|
+
category,
|
|
222
|
+
path: relPath,
|
|
223
|
+
description: null,
|
|
224
|
+
content: `function ${decl.id?.name || 'anonymous'}(${params.join(', ')}) { ... }`,
|
|
225
|
+
meta: {
|
|
226
|
+
params,
|
|
227
|
+
returnType: getReturnType(decl),
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function createRowFromClass(name, _decl, filePath, projectRoot) {
|
|
232
|
+
const relPath = getRelativePathForDisplay(projectRoot, filePath);
|
|
233
|
+
const category = inferCategoryFromPath(filePath);
|
|
234
|
+
// 大写开头的类视为组件
|
|
235
|
+
const type = /^[A-Z]/.test(name) ? 'component' : 'util';
|
|
236
|
+
return {
|
|
237
|
+
name,
|
|
238
|
+
type,
|
|
239
|
+
category,
|
|
240
|
+
path: relPath,
|
|
241
|
+
description: null,
|
|
242
|
+
content: null,
|
|
243
|
+
meta: { kind: 'class' },
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
/** 简单检测是否包含 JSX */
|
|
247
|
+
function containsJsx(node) {
|
|
248
|
+
const jsxTags = ['JSXElement', 'JSXFragment'];
|
|
249
|
+
let found = false;
|
|
250
|
+
const visit = (n) => {
|
|
251
|
+
if (jsxTags.includes(n.type)) {
|
|
252
|
+
found = true;
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
// 只遍历常见的包含子节点的属性
|
|
256
|
+
const keys = ['body', 'declarations', 'arguments', 'callee', 'init', 'left', 'right', 'consequent', 'alternate'];
|
|
257
|
+
for (const key of keys) {
|
|
258
|
+
const val = n[key];
|
|
259
|
+
if (Array.isArray(val)) {
|
|
260
|
+
for (const v of val) {
|
|
261
|
+
if (v && typeof v === 'object' && 'type' in v) {
|
|
262
|
+
visit(v);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
else if (val && typeof val === 'object' && 'type' in val) {
|
|
267
|
+
visit(val);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
visit(node);
|
|
272
|
+
return found;
|
|
273
|
+
}
|
|
274
|
+
function getReturnType(fn) {
|
|
275
|
+
if (!fn.returnType)
|
|
276
|
+
return undefined;
|
|
277
|
+
const rt = fn.returnType;
|
|
278
|
+
if (bt.isTSTypeAnnotation(rt)) {
|
|
279
|
+
const inner = rt.typeAnnotation;
|
|
280
|
+
if (bt.isTSStringKeyword(inner))
|
|
281
|
+
return 'string';
|
|
282
|
+
if (bt.isTSNumberKeyword(inner))
|
|
283
|
+
return 'number';
|
|
284
|
+
if (bt.isTSBooleanKeyword(inner))
|
|
285
|
+
return 'boolean';
|
|
286
|
+
if (bt.isTSVoidKeyword(inner))
|
|
287
|
+
return 'void';
|
|
288
|
+
if (bt.isTSAnyKeyword(inner))
|
|
289
|
+
return 'any';
|
|
290
|
+
if (bt.isTSUnknownKeyword(inner))
|
|
291
|
+
return 'unknown';
|
|
292
|
+
if (bt.isTSNullKeyword(inner))
|
|
293
|
+
return 'null';
|
|
294
|
+
}
|
|
295
|
+
return undefined;
|
|
296
|
+
}
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 扫描源码目录,用 ts-morph
|
|
2
|
+
* 扫描源码目录,用 ts-morph 解析 TS/TSX,Babel 解析 JS/JSX,生成待写入 MySQL 的代码块行。
|
|
3
3
|
*/
|
|
4
4
|
import fg from 'fast-glob';
|
|
5
5
|
import { join, resolve } from 'node:path';
|
|
6
6
|
import { Node, Project } from 'ts-morph';
|
|
7
|
+
import { readFileSync } from 'node:fs';
|
|
7
8
|
import { extractInterfaceOrTypeMeta, extractMetaFromCallable, } from './extractMeta.js';
|
|
8
9
|
import { getLeadingDocDescription, getRelativePathForDisplay, hasJsxInNode, inferCategoryFromPath, isSelectorLike, isTsxFile, snippetForNode, } from './heuristics.js';
|
|
10
|
+
import { parseJsFile } from './babelParser.js';
|
|
11
|
+
/** 判断文件类型 */
|
|
12
|
+
function isJsFile(filePath) {
|
|
13
|
+
return filePath.endsWith('.js') || filePath.endsWith('.jsx');
|
|
14
|
+
}
|
|
15
|
+
function isTsFile(filePath) {
|
|
16
|
+
return filePath.endsWith('.ts') || filePath.endsWith('.tsx');
|
|
17
|
+
}
|
|
9
18
|
/**
|
|
10
19
|
* 将 `extractFunctionMeta` 的 `params` 转为组件用的 `props` 名,或保留为 `params`。
|
|
11
20
|
* @returns 供 `IndexedSymbolRow.meta` 序列化入库。
|
|
@@ -147,32 +156,51 @@ export async function indexProject(opts) {
|
|
|
147
156
|
const projectRoot = resolve(opts.projectRoot);
|
|
148
157
|
const patterns = (opts.globPatterns ?? ['src/**/*.{ts,tsx}']).map((p) => p.startsWith('/') ? p : join(projectRoot, p).replace(/\\/g, '/'));
|
|
149
158
|
const ignore = [...DEFAULT_IGNORE, ...(opts.ignore ?? [])];
|
|
159
|
+
console.error(`[indexProject] patterns: ${patterns.join(', ')}`);
|
|
150
160
|
const files = await fg(patterns, {
|
|
151
161
|
absolute: true,
|
|
152
162
|
ignore,
|
|
153
163
|
onlyFiles: true,
|
|
154
164
|
dot: false,
|
|
155
165
|
});
|
|
166
|
+
console.error(`[indexProject] found ${files.length} file(s)`);
|
|
156
167
|
if (files.length === 0) {
|
|
157
168
|
return [];
|
|
158
169
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
skipFileDependencyResolution: true,
|
|
163
|
-
});
|
|
164
|
-
project.addSourceFilesAtPaths(files);
|
|
170
|
+
// 分离 TS/TSX 文件和 JS/JSX 文件
|
|
171
|
+
const tsFiles = files.filter(isTsFile);
|
|
172
|
+
const jsFiles = files.filter(isJsFile);
|
|
165
173
|
const out = [];
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
174
|
+
// 处理 TS/TSX 文件
|
|
175
|
+
if (tsFiles.length > 0) {
|
|
176
|
+
const project = new Project({
|
|
177
|
+
tsConfigFilePath: join(projectRoot, 'tsconfig.json'),
|
|
178
|
+
skipAddingFilesFromTsConfig: true,
|
|
179
|
+
skipFileDependencyResolution: true,
|
|
180
|
+
});
|
|
181
|
+
project.addSourceFilesAtPaths(tsFiles);
|
|
182
|
+
for (const sf of project.getSourceFiles()) {
|
|
183
|
+
const exported = sf.getExportedDeclarations();
|
|
184
|
+
for (const [exportName, decls] of exported) {
|
|
185
|
+
for (const decl of decls) {
|
|
186
|
+
const row = processDeclaration(exportName, decl, sf, projectRoot);
|
|
187
|
+
if (row) {
|
|
188
|
+
out.push(row);
|
|
189
|
+
}
|
|
173
190
|
}
|
|
174
191
|
}
|
|
175
192
|
}
|
|
176
193
|
}
|
|
194
|
+
// 处理 JS/JSX 文件
|
|
195
|
+
for (const file of jsFiles) {
|
|
196
|
+
try {
|
|
197
|
+
const content = readFileSync(file, 'utf-8');
|
|
198
|
+
const rows = parseJsFile(file, content, projectRoot);
|
|
199
|
+
out.push(...rows);
|
|
200
|
+
}
|
|
201
|
+
catch (e) {
|
|
202
|
+
console.error(`[indexProject] Failed to parse ${file}:`, e);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
177
205
|
return out;
|
|
178
206
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { env } from '../config/env.js';
|
|
2
|
+
import { getSymbolsTableSQL } from '../db/schema.js';
|
|
1
3
|
/**
|
|
2
4
|
* 依赖表上 `(path, name)` 唯一键:新行插入,已存在则更新类型/描述/内容与 meta;**不**修改 `usage_count`。
|
|
3
5
|
* @param rows 来自 `indexProject`;空数组时立即返回,不开启事务。
|
|
@@ -8,11 +10,12 @@ export async function upsertSymbols(pool, rows, embeddings) {
|
|
|
8
10
|
if (rows.length === 0)
|
|
9
11
|
return;
|
|
10
12
|
if (embeddings && embeddings.length !== rows.length) {
|
|
11
|
-
throw new Error(
|
|
13
|
+
throw new Error('upsertSymbols: embeddings length must match rows');
|
|
12
14
|
}
|
|
13
|
-
const actor = process.env.GITHUB_USERNAME?.trim() ||
|
|
15
|
+
const actor = process.env.GITHUB_USERNAME?.trim() || 'LorryIsLuRui';
|
|
16
|
+
await pool.query(getSymbolsTableSQL()); // 确保表存在
|
|
14
17
|
const sql = `
|
|
15
|
-
INSERT INTO
|
|
18
|
+
INSERT INTO ${env.mysqlSymbolsTable} (name, type, category, path, description, content, meta, insert_user, updated_user, embedding)
|
|
16
19
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
17
20
|
ON DUPLICATE KEY UPDATE
|
|
18
21
|
type = VALUES(type),
|
|
@@ -40,7 +43,7 @@ export async function upsertSymbols(pool, rows, embeddings) {
|
|
|
40
43
|
JSON.stringify(r.meta),
|
|
41
44
|
actor,
|
|
42
45
|
actor,
|
|
43
|
-
embJson
|
|
46
|
+
embJson,
|
|
44
47
|
]);
|
|
45
48
|
}
|
|
46
49
|
await conn.commit();
|
|
@@ -97,7 +97,7 @@ export class SymbolRepository {
|
|
|
97
97
|
const params = [`%${query}%`];
|
|
98
98
|
let sql = `
|
|
99
99
|
SELECT id, name, type, category, path, description, content, CAST(meta AS CHAR) AS meta, usage_count, created_at
|
|
100
|
-
FROM
|
|
100
|
+
FROM ${env.mysqlSymbolsTable}
|
|
101
101
|
WHERE (name LIKE ? OR description LIKE ?)
|
|
102
102
|
`;
|
|
103
103
|
params.push(`%${query}%`);
|
|
@@ -129,7 +129,7 @@ export class SymbolRepository {
|
|
|
129
129
|
}
|
|
130
130
|
let sql = `
|
|
131
131
|
SELECT id, name, type, category, path, description, content, CAST(meta AS CHAR) AS meta, usage_count, created_at, embedding
|
|
132
|
-
FROM
|
|
132
|
+
FROM ${env.mysqlSymbolsTable}
|
|
133
133
|
WHERE embedding IS NOT NULL
|
|
134
134
|
`;
|
|
135
135
|
const params = [];
|
|
@@ -158,7 +158,7 @@ export class SymbolRepository {
|
|
|
158
158
|
}
|
|
159
159
|
const [rows] = await this.pool.query(`
|
|
160
160
|
SELECT id, name, type, category, path, description, content, CAST(meta AS CHAR) AS meta, usage_count, created_at
|
|
161
|
-
FROM
|
|
161
|
+
FROM ${env.mysqlSymbolsTable}
|
|
162
162
|
WHERE name = ?
|
|
163
163
|
LIMIT 1
|
|
164
164
|
`, [name]);
|
|
@@ -197,7 +197,7 @@ export class SymbolRepository {
|
|
|
197
197
|
const params = [];
|
|
198
198
|
let sql = `
|
|
199
199
|
SELECT id, name, type, category, path, description, content, CAST(meta AS CHAR) AS meta, usage_count, created_at
|
|
200
|
-
FROM
|
|
200
|
+
FROM ${env.mysqlSymbolsTable}
|
|
201
201
|
WHERE 1 = 1
|
|
202
202
|
`;
|
|
203
203
|
if (type) {
|
package/dist/services/reindex.js
CHANGED
|
@@ -1,23 +1,27 @@
|
|
|
1
|
-
import { resolve } from
|
|
2
|
-
import { env, validateEnv } from
|
|
3
|
-
import { getMySqlPool } from
|
|
4
|
-
import { indexedRowToEmbedText } from
|
|
5
|
-
import { indexProject } from
|
|
6
|
-
import { upsertSymbols } from
|
|
7
|
-
import { createEmbeddingClient, embedAll } from
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { env, validateEnv } from '../config/env.js';
|
|
3
|
+
import { getMySqlPool } from '../db/mysql.js';
|
|
4
|
+
import { indexedRowToEmbedText } from '../indexer/embedText.js';
|
|
5
|
+
import { indexProject } from '../indexer/indexProject.js';
|
|
6
|
+
import { upsertSymbols } from '../indexer/persistSymbols.js';
|
|
7
|
+
import { createEmbeddingClient, embedAll, } from '../services/embeddingClient.js';
|
|
8
8
|
export async function runReindex(options = {}) {
|
|
9
9
|
validateEnv();
|
|
10
10
|
const pool = getMySqlPool();
|
|
11
|
+
console.error('[reindex] pool', 'options:', JSON.stringify(options));
|
|
11
12
|
if (!pool || !env.mysqlEnabled) {
|
|
12
|
-
|
|
13
|
+
console.error('[reindex] pool', pool, env.mysqlEnabled);
|
|
14
|
+
throw new Error('执行 reindex 前必须开启 MYSQL_ENABLED=true。');
|
|
13
15
|
}
|
|
14
|
-
await pool.query(
|
|
16
|
+
await pool.query('SELECT 1'); // 测试连接,提前捕获常见的连接错误(如拒绝、认证失败、超时等),并给出更友好的提示。
|
|
17
|
+
console.error('[reindex] MySQL connection successful');
|
|
15
18
|
const projectRoot = resolve(options.projectRoot ?? process.cwd());
|
|
16
19
|
const rows = await indexProject({
|
|
17
20
|
projectRoot,
|
|
18
21
|
globPatterns: options.globPatterns,
|
|
19
|
-
ignore: options.ignore
|
|
22
|
+
ignore: options.ignore,
|
|
20
23
|
});
|
|
24
|
+
console.error(`[reindex] extracted ${rows.length} symbol(s) from ${projectRoot}`);
|
|
21
25
|
let embeddingsComputed = false;
|
|
22
26
|
let embeddingPayload;
|
|
23
27
|
if (!options.dryRun && rows.length > 0 && env.embeddingServiceUrl) {
|
|
@@ -29,7 +33,7 @@ export async function runReindex(options = {}) {
|
|
|
29
33
|
embeddingsComputed = true;
|
|
30
34
|
}
|
|
31
35
|
catch (err) {
|
|
32
|
-
console.error(
|
|
36
|
+
console.error('[reindex] embedding skipped (service error):', err);
|
|
33
37
|
embeddingPayload = rows.map(() => null);
|
|
34
38
|
}
|
|
35
39
|
}
|
|
@@ -40,6 +44,6 @@ export async function runReindex(options = {}) {
|
|
|
40
44
|
projectRoot,
|
|
41
45
|
extractedCount: rows.length,
|
|
42
46
|
upserted: !options.dryRun,
|
|
43
|
-
embeddingsComputed
|
|
47
|
+
embeddingsComputed,
|
|
44
48
|
};
|
|
45
49
|
}
|
package/package.json
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lorrylurui/code-intelligence-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"private": false,
|
|
5
|
-
"description": "MCP server
|
|
5
|
+
"description": "MCP server 提供仓库内可复用代码块(ts/tsx/js/jsx/css/less)的索引和查询能力,支持基于代码上下文的智能推荐。",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|
|
8
8
|
"files": [
|
|
9
9
|
"dist"
|
|
10
10
|
],
|
|
11
11
|
"bin": {
|
|
12
|
-
"code-intelligence-mcp": "./dist/index.js"
|
|
12
|
+
"code-intelligence-mcp": "./dist/index.js",
|
|
13
|
+
"code-intelligence-index": "./dist/cli/index-codebase.js"
|
|
13
14
|
},
|
|
14
15
|
"scripts": {
|
|
15
16
|
"dev": "tsx watch --clear-screen=false --exclude node_modules --exclude dist src/index.ts",
|
|
@@ -24,6 +25,8 @@
|
|
|
24
25
|
"docker:logs": "docker compose logs -f mysql"
|
|
25
26
|
},
|
|
26
27
|
"dependencies": {
|
|
28
|
+
"@babel/parser": "^7.29.2",
|
|
29
|
+
"@babel/types": "^7.29.0",
|
|
27
30
|
"@modelcontextprotocol/sdk": "^1.12.3",
|
|
28
31
|
"@types/react": "^19.2.14",
|
|
29
32
|
"dotenv": "^16.4.5",
|