@lorrylurui/code-intelligence-mcp 1.1.14 → 1.2.0
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 +91 -598
- package/dist/cli/ci-index-cli.js +66 -0
- package/dist/cli/ci-index.js +80 -0
- package/dist/cli/detect-duplicates.js +1 -6
- package/dist/cli/embedding-worker-cli.js +35 -0
- package/dist/cli/index-codebase.js +6 -7
- package/dist/config/env.js +3 -102
- package/dist/config/symbolStatus.js +8 -0
- package/dist/db/mysql.js +3 -6
- package/dist/db/schema.js +9 -2
- package/dist/indexer/astNormalizer.js +201 -0
- package/dist/indexer/babelParser.js +257 -28
- package/dist/indexer/categoryClassifier.js +129 -0
- package/dist/indexer/embedText.js +9 -7
- package/dist/indexer/extractMeta.js +7 -2
- package/dist/indexer/heuristics.js +42 -23
- package/dist/indexer/indexProject.js +145 -55
- package/dist/indexer/jsAstNormalizer.js +201 -0
- package/dist/indexer/persistSymbols.js +7 -3
- package/dist/indexer/tsAstNormalizer.js +363 -0
- package/dist/prompts/reusableCodeAdvisorPrompt.js +6 -3
- package/dist/repositories/symbolRepository.js +81 -7
- package/dist/services/embeddingQueue.js +56 -0
- package/dist/services/reindex.js +12 -9
- package/dist/tools/searchByStructure.js +3 -1
- package/dist/tools/searchSymbols.js +14 -3
- package/dist/workers/embeddingWorker.js +100 -0
- package/package.json +7 -4
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CI增量索引CLI:处理changed files和deleted files
|
|
4
|
+
*
|
|
5
|
+
* 用法:
|
|
6
|
+
* node src/cli/ci-index-cli.js --changed src/file1.ts,src/file2.ts --deleted src/old.ts --renamed src/old.ts:src/new.ts
|
|
7
|
+
*/
|
|
8
|
+
import { resolve } from 'node:path';
|
|
9
|
+
import { loadProjectDotenv } from '../config/env.js';
|
|
10
|
+
import { runIncrementalIndex } from './ci-index.js';
|
|
11
|
+
async function main() {
|
|
12
|
+
const args = process.argv.slice(2);
|
|
13
|
+
const projectRoot = resolve(process.env.INDEX_ROOT ?? process.cwd());
|
|
14
|
+
loadProjectDotenv(projectRoot);
|
|
15
|
+
let changedFiles = [];
|
|
16
|
+
let deletedFiles = [];
|
|
17
|
+
let renamedFiles = [];
|
|
18
|
+
for (let i = 0; i < args.length; i++) {
|
|
19
|
+
const arg = args[i];
|
|
20
|
+
if (arg === '--changed' && i + 1 < args.length) {
|
|
21
|
+
changedFiles = args[i + 1]
|
|
22
|
+
.split(',')
|
|
23
|
+
.map((s) => s.trim())
|
|
24
|
+
.filter(Boolean);
|
|
25
|
+
i++;
|
|
26
|
+
}
|
|
27
|
+
else if (arg === '--deleted' && i + 1 < args.length) {
|
|
28
|
+
deletedFiles = args[i + 1]
|
|
29
|
+
.split(',')
|
|
30
|
+
.map((s) => s.trim())
|
|
31
|
+
.filter(Boolean);
|
|
32
|
+
i++;
|
|
33
|
+
}
|
|
34
|
+
else if (arg === '--renamed' && i + 1 < args.length) {
|
|
35
|
+
renamedFiles = args[i + 1]
|
|
36
|
+
.split(',')
|
|
37
|
+
.map((s) => {
|
|
38
|
+
const [from, to] = s.split(':');
|
|
39
|
+
return { from: from.trim(), to: to.trim() };
|
|
40
|
+
})
|
|
41
|
+
.filter((r) => r.from && r.to);
|
|
42
|
+
i++;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (changedFiles.length === 0 &&
|
|
46
|
+
deletedFiles.length === 0 &&
|
|
47
|
+
renamedFiles.length === 0) {
|
|
48
|
+
console.error('Usage: node ci-index-cli.js --changed file1,file2 --deleted file3 --renamed old:new');
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
console.error(`[ci-index-cli] projectRoot=${projectRoot}`);
|
|
52
|
+
console.error(`[ci-index-cli] changed: ${changedFiles.join(', ')}`);
|
|
53
|
+
console.error(`[ci-index-cli] deleted: ${deletedFiles.join(', ')}`);
|
|
54
|
+
console.error(`[ci-index-cli] renamed: ${renamedFiles.map((r) => `${r.from}->${r.to}`).join(', ')}`);
|
|
55
|
+
await runIncrementalIndex({
|
|
56
|
+
projectRoot,
|
|
57
|
+
changedFiles,
|
|
58
|
+
deletedFiles,
|
|
59
|
+
renamedFiles,
|
|
60
|
+
});
|
|
61
|
+
console.error('[ci-index-cli] completed successfully');
|
|
62
|
+
}
|
|
63
|
+
main().catch((err) => {
|
|
64
|
+
console.error('[ci-index-cli] failed:', err);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// CI增量索引:处理changed files和deleted files
|
|
2
|
+
import { env, loadProjectDotenv } from '../config/env.js';
|
|
3
|
+
import { getMySqlPool } from '../db/mysql.js';
|
|
4
|
+
import { indexProject } from '../indexer/indexProject.js';
|
|
5
|
+
import { DEFAULT_STATUS_ON_UPSERT, SYMBOL_STATUS, } from '../config/symbolStatus.js';
|
|
6
|
+
import { enqueueEmbeddingBatch, closeEmbeddingQueue, } from '../services/embeddingQueue.js';
|
|
7
|
+
export async function runIncrementalIndex(opts) {
|
|
8
|
+
const { projectRoot, changedFiles, deletedFiles, renamedFiles = [] } = opts;
|
|
9
|
+
loadProjectDotenv(projectRoot);
|
|
10
|
+
const pool = getMySqlPool();
|
|
11
|
+
if (!pool) {
|
|
12
|
+
throw new Error('Failed to get MySQL pool');
|
|
13
|
+
}
|
|
14
|
+
const tableName = env.mysqlSymbolsTable;
|
|
15
|
+
// 1. 删除文件:标记 offline
|
|
16
|
+
for (const file of deletedFiles) {
|
|
17
|
+
await pool.query(`UPDATE ${tableName} SET status = ? WHERE path = ?`, [
|
|
18
|
+
SYMBOL_STATUS.OFFLINE,
|
|
19
|
+
file,
|
|
20
|
+
]);
|
|
21
|
+
console.error(`[ci-index] marked offline: ${file}`);
|
|
22
|
+
}
|
|
23
|
+
// 2. 重命名文件:更新path
|
|
24
|
+
for (const { from, to } of renamedFiles) {
|
|
25
|
+
await pool.query(`UPDATE ${tableName} SET path = ? WHERE path = ?`, [
|
|
26
|
+
to,
|
|
27
|
+
from,
|
|
28
|
+
]);
|
|
29
|
+
console.error(`[ci-index] renamed: ${from} -> ${to}`);
|
|
30
|
+
}
|
|
31
|
+
// 3. 变更/新增文件:重新索引并标记 pending
|
|
32
|
+
if (changedFiles.length > 0) {
|
|
33
|
+
const rows = await indexProject({
|
|
34
|
+
projectRoot,
|
|
35
|
+
globPatterns: changedFiles,
|
|
36
|
+
});
|
|
37
|
+
for (const row of rows) {
|
|
38
|
+
// 写入结构化数据,标记pending
|
|
39
|
+
await pool.query(`INSERT INTO ${tableName}
|
|
40
|
+
(name, type, category, path, description, content, meta,
|
|
41
|
+
file_hash, semantic_hash, status,
|
|
42
|
+
usage_count, created_at, updated_at)
|
|
43
|
+
VALUES (?, ?, ?, ?, ?, ?, CAST(? AS JSON), ?, ?, ?, 0, NOW(), NOW())
|
|
44
|
+
ON DUPLICATE KEY UPDATE
|
|
45
|
+
type = VALUES(type),
|
|
46
|
+
category = VALUES(category),
|
|
47
|
+
description = VALUES(description),
|
|
48
|
+
content = VALUES(content),
|
|
49
|
+
meta = VALUES(meta),
|
|
50
|
+
file_hash = VALUES(file_hash),
|
|
51
|
+
semantic_hash = VALUES(semantic_hash),
|
|
52
|
+
status = ?,
|
|
53
|
+
updated_at = NOW()`, [
|
|
54
|
+
row.name,
|
|
55
|
+
row.type,
|
|
56
|
+
row.category ?? null,
|
|
57
|
+
row.path,
|
|
58
|
+
row.description ?? null,
|
|
59
|
+
row.content ?? null,
|
|
60
|
+
JSON.stringify(row.meta),
|
|
61
|
+
row.file_hash,
|
|
62
|
+
row.semantic_hash,
|
|
63
|
+
DEFAULT_STATUS_ON_UPSERT,
|
|
64
|
+
DEFAULT_STATUS_ON_UPSERT,
|
|
65
|
+
]);
|
|
66
|
+
console.error(`[ci-index] indexed (pending): ${row.path}:${row.name}`);
|
|
67
|
+
}
|
|
68
|
+
// 批量入队:jobId = semanticHash,相同 hash 自动去重,1000 个符号可能只产生 N 个唯一 job
|
|
69
|
+
const hashes = [
|
|
70
|
+
...new Set(rows.map((r) => r.semantic_hash).filter(Boolean)),
|
|
71
|
+
];
|
|
72
|
+
if (hashes.length > 0) {
|
|
73
|
+
await enqueueEmbeddingBatch(hashes);
|
|
74
|
+
console.error(`[ci-index] enqueued ${hashes.length} unique semantic hashes for embedding`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
await closeEmbeddingQueue();
|
|
78
|
+
await pool.end();
|
|
79
|
+
console.error(`[ci-index] processed ${deletedFiles.length} deletions, ${renamedFiles.length} renames, ${changedFiles.length} changes`);
|
|
80
|
+
}
|
|
@@ -109,8 +109,7 @@ async function main() {
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
loadProjectDotenv(projectRoot);
|
|
112
|
-
console.error(`[duplicate-check] projectRoot=${projectRoot}, `
|
|
113
|
-
`MYSQL_ENABLED=${process.env.MYSQL_ENABLED}`);
|
|
112
|
+
console.error(`[duplicate-check] projectRoot=${projectRoot}, `);
|
|
114
113
|
// 3️ 解析命令行参数
|
|
115
114
|
const args = parseArgs(process.argv.slice(2));
|
|
116
115
|
const changedFilesPath = args.get('changed-files') ?? 'changed_files.txt';
|
|
@@ -131,10 +130,6 @@ async function main() {
|
|
|
131
130
|
}
|
|
132
131
|
else {
|
|
133
132
|
validateEnv();
|
|
134
|
-
const pool = getMySqlPool();
|
|
135
|
-
if (!pool || !env.mysqlEnabled) {
|
|
136
|
-
throw new Error('duplicate-check 需要 MYSQL_ENABLED=true 并可连接 MySQL。');
|
|
137
|
-
}
|
|
138
133
|
if (!env.embeddingServiceUrl) {
|
|
139
134
|
throw new Error('duplicate-check 需要 EMBEDDING_SERVICE_URL(embedding service)。');
|
|
140
135
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* embedding worker 进程入口。
|
|
3
|
+
*
|
|
4
|
+
* 本地启动:
|
|
5
|
+
* npm run worker:embedding
|
|
6
|
+
*
|
|
7
|
+
* 大仓分片(多进程并行):
|
|
8
|
+
* WORKER_CONCURRENCY=10 npm run worker:embedding &
|
|
9
|
+
* WORKER_CONCURRENCY=10 npm run worker:embedding &
|
|
10
|
+
* # 启动 N 个进程,BullMQ 自动分配任务,无需手动分片
|
|
11
|
+
*
|
|
12
|
+
* 环境变量:
|
|
13
|
+
* REDIS_URL Redis 连接 URL(默认 redis://127.0.0.1:6379)
|
|
14
|
+
* MYSQL_HOST / ... MySQL 连接配置
|
|
15
|
+
* EMBEDDING_SERVICE_URL Python embedding 服务地址
|
|
16
|
+
* WORKER_CONCURRENCY 单进程并发 job 数(默认 5)
|
|
17
|
+
* WORKER_RPM_LIMIT 全局 RPM 上限(默认 100,跨所有 worker 进程)
|
|
18
|
+
* PROJECT_ROOT 项目根目录,用于加载 .env(默认 cwd)
|
|
19
|
+
*/
|
|
20
|
+
import { loadProjectDotenv } from '../config/env.js';
|
|
21
|
+
import { startEmbeddingWorker } from '../workers/embeddingWorker.js';
|
|
22
|
+
const projectRoot = process.env.PROJECT_ROOT ?? process.cwd();
|
|
23
|
+
loadProjectDotenv(projectRoot);
|
|
24
|
+
const concurrency = Number(process.env.WORKER_CONCURRENCY ?? '5');
|
|
25
|
+
const rpmLimit = Number(process.env.WORKER_RPM_LIMIT ?? '100');
|
|
26
|
+
const worker = startEmbeddingWorker({ concurrency, rpmLimit });
|
|
27
|
+
console.error(`[embedding-worker] started concurrency=${concurrency} rpm_limit=${rpmLimit}`);
|
|
28
|
+
// 优雅关闭:等当前 job 执行完再退出
|
|
29
|
+
for (const sig of ['SIGINT', 'SIGTERM']) {
|
|
30
|
+
process.on(sig, async () => {
|
|
31
|
+
console.error('[embedding-worker] shutting down…');
|
|
32
|
+
await worker.close();
|
|
33
|
+
process.exit(0);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Phase 2 CLI:扫描代码库并写入 MySQL `symbols
|
|
3
|
+
* Phase 2 CLI:扫描代码库并写入 MySQL `symbols`。
|
|
4
4
|
*
|
|
5
5
|
* 环境变量加载顺序:
|
|
6
6
|
* 1. 命令行参数(最高优先级)
|
|
@@ -10,19 +10,18 @@
|
|
|
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
|
-
//
|
|
19
|
+
// const projectRoot = resolve(process.env.INDEX_ROOT ?? process.cwd());
|
|
20
|
+
loadProjectDotenv(resolve(process.env.INDEX_ROOT ?? process.cwd()));
|
|
21
21
|
const projectRoot = resolve(process.env.INDEX_ROOT ?? process.cwd());
|
|
22
|
-
|
|
23
|
-
console.error(`
|
|
24
|
-
|
|
25
|
-
`MYSQL_HOST=${process.env.MYSQL_HOST}`);
|
|
22
|
+
console.error(projectRoot, process.env.INDEX_ROOT);
|
|
23
|
+
console.error(`MYSQL_HOST=${process.env.MYSQL_HOST}` +
|
|
24
|
+
`[index] projectRoot=${projectRoot}`);
|
|
26
25
|
const globPatterns = process.env.INDEX_GLOB
|
|
27
26
|
? process.env.INDEX_GLOB.split(/\s+/)
|
|
28
27
|
.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,11 +10,6 @@ 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
14
|
const MCP_SERVER_ROOT = path.resolve(__dirname, '..', '..', './dist'); // MCP Server 根目录
|
|
21
15
|
const MCP_SERVER_ENV_PATH = path.resolve(MCP_SERVER_ROOT, '.env');
|
|
@@ -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_]*)=(.+)$/);
|
|
@@ -84,15 +72,13 @@ const requiredWhenEnabled = [
|
|
|
84
72
|
'MYSQL_USER',
|
|
85
73
|
'MYSQL_DATABASE',
|
|
86
74
|
];
|
|
87
|
-
console.error(`[Config]
|
|
88
|
-
MYSQL_HOST: ${process.env.MYSQL_HOST},
|
|
75
|
+
console.error(`[Config] MYSQL_HOST: ${process.env.MYSQL_HOST},
|
|
89
76
|
MYSQL_USER: ${process.env.MYSQL_USER},
|
|
90
77
|
MYSQL_DATABASE: ${process.env.MYSQL_DATABASE},
|
|
91
78
|
EMBEDDING_SERVICE_URL: ${process.env.EMBEDDING_SERVICE_URL},
|
|
92
79
|
MYSQL_SYMBOLS_TABLE: ${process.env.MYSQL_SYMBOLS_TABLE}
|
|
93
80
|
`);
|
|
94
81
|
export const env = {
|
|
95
|
-
mysqlEnabled: process.env.MYSQL_ENABLED === 'true',
|
|
96
82
|
mysqlHost: process.env.MYSQL_HOST ?? '127.0.0.1',
|
|
97
83
|
mysqlPort: Number(process.env.MYSQL_PORT ?? '3306'),
|
|
98
84
|
mysqlUser: process.env.MYSQL_USER ?? 'root',
|
|
@@ -102,98 +88,13 @@ export const env = {
|
|
|
102
88
|
mysqlSymbolsTable: process.env.MYSQL_SYMBOLS_TABLE ?? 'symbols',
|
|
103
89
|
/** Phase 5:指向 Python FastAPI 嵌入服务根 URL,如 http://127.0.0.1:8765 */
|
|
104
90
|
embeddingServiceUrl: (process.env.EMBEDDING_SERVICE_URL ?? '').trim(),
|
|
91
|
+
/** Redis 连接 URL,供 BullMQ embedding worker 使用 */
|
|
92
|
+
redisUrl: process.env.REDIS_URL ?? 'redis://127.0.0.1:6379',
|
|
105
93
|
};
|
|
106
94
|
export function validateEnv() {
|
|
107
|
-
if (!env.mysqlEnabled) {
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
95
|
for (const key of requiredWhenEnabled) {
|
|
111
96
|
if (!process.env[key]) {
|
|
112
97
|
throw new Error(`Missing environment variable: ${key}`);
|
|
113
98
|
}
|
|
114
99
|
}
|
|
115
100
|
}
|
|
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/db/mysql.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import mysql from
|
|
2
|
-
import { env } from
|
|
1
|
+
import mysql from 'mysql2/promise';
|
|
2
|
+
import { env } from '../config/env.js';
|
|
3
3
|
let pool = null;
|
|
4
4
|
export function getMySqlPool() {
|
|
5
|
-
if (!env.mysqlEnabled) {
|
|
6
|
-
return null;
|
|
7
|
-
}
|
|
8
5
|
if (!pool) {
|
|
9
6
|
pool = mysql.createPool({
|
|
10
7
|
host: env.mysqlHost,
|
|
@@ -13,7 +10,7 @@ export function getMySqlPool() {
|
|
|
13
10
|
password: env.mysqlPassword,
|
|
14
11
|
database: env.mysqlDatabase,
|
|
15
12
|
waitForConnections: true,
|
|
16
|
-
connectionLimit: 10
|
|
13
|
+
connectionLimit: 10,
|
|
17
14
|
});
|
|
18
15
|
}
|
|
19
16
|
return pool;
|
package/dist/db/schema.js
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
* 动态生成数据库表结构 SQL,表名可通过环境变量配置
|
|
3
3
|
*/
|
|
4
4
|
import { env } from '../config/env.js';
|
|
5
|
+
import { DEFAULT_STATUS_ON_UPSERT } from '../config/symbolStatus.js';
|
|
5
6
|
/** 获取 symbols 表的建表 SQL */
|
|
6
7
|
export function getSymbolsTableSQL() {
|
|
7
8
|
const tableName = env.mysqlSymbolsTable;
|
|
8
9
|
return `CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
9
10
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
10
11
|
name VARCHAR(255) NOT NULL,
|
|
11
|
-
type ENUM('component', '
|
|
12
|
+
type ENUM('component', 'function', 'type', 'class', 'interface', 'hook') NOT NULL,
|
|
12
13
|
category VARCHAR(255) NULL,
|
|
13
14
|
path TEXT NOT NULL,
|
|
14
15
|
description TEXT NULL,
|
|
@@ -20,7 +21,13 @@ export function getSymbolsTableSQL() {
|
|
|
20
21
|
updated_user VARCHAR(255) NOT NULL DEFAULT 'LorryIsLuRui',
|
|
21
22
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
22
23
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
23
|
-
|
|
24
|
+
file_hash VARCHAR(64) NULL COMMENT '文件内容 SHA256',
|
|
25
|
+
semantic_hash VARCHAR(64) NULL COMMENT 'normalized AST 语义模板 SHA256',
|
|
26
|
+
status TINYINT NOT NULL DEFAULT ${DEFAULT_STATUS_ON_UPSERT} COMMENT '状态: 0-offline(删除), 1-pending(待处理), 2-online(可用), 3-error(错误)',
|
|
27
|
+
UNIQUE KEY uk_symbols_path_name (path(512), name(255)),
|
|
28
|
+
INDEX idx_file_hash (file_hash),
|
|
29
|
+
INDEX idx_semantic_hash (semantic_hash),
|
|
30
|
+
INDEX idx_status (status)
|
|
24
31
|
)`;
|
|
25
32
|
}
|
|
26
33
|
/** 获取所有建表 SQL(可一次性执行) */
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* astNormalizer.ts
|
|
3
|
+
* 对 ts-morph Node 做语义级标准化,生成 semantic_hash。
|
|
4
|
+
*
|
|
5
|
+
* 去掉:参数名、泛型参数名、函数体实现、空白格式、字面量值
|
|
6
|
+
* 保留:参数类型结构、返回类型、sideEffects、hooks
|
|
7
|
+
*/
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
9
|
+
import { Node, SyntaxKind } from 'ts-morph';
|
|
10
|
+
// ─────────────────────────────────────────────
|
|
11
|
+
// 内置类型白名单:不替换为 $T
|
|
12
|
+
// ─────────────────────────────────────────────
|
|
13
|
+
const BUILTIN_TYPES = new Set([
|
|
14
|
+
'string',
|
|
15
|
+
'number',
|
|
16
|
+
'boolean',
|
|
17
|
+
'void',
|
|
18
|
+
'null',
|
|
19
|
+
'undefined',
|
|
20
|
+
'never',
|
|
21
|
+
'unknown',
|
|
22
|
+
'any',
|
|
23
|
+
'object',
|
|
24
|
+
'symbol',
|
|
25
|
+
'bigint',
|
|
26
|
+
'Promise',
|
|
27
|
+
'Array',
|
|
28
|
+
'Record',
|
|
29
|
+
'Map',
|
|
30
|
+
'Set',
|
|
31
|
+
'WeakMap',
|
|
32
|
+
'WeakSet',
|
|
33
|
+
'Partial',
|
|
34
|
+
'Required',
|
|
35
|
+
'Readonly',
|
|
36
|
+
'Pick',
|
|
37
|
+
'Omit',
|
|
38
|
+
'Exclude',
|
|
39
|
+
'Extract',
|
|
40
|
+
'NonNullable',
|
|
41
|
+
'ReturnType',
|
|
42
|
+
'InstanceType',
|
|
43
|
+
'React',
|
|
44
|
+
'ReactNode',
|
|
45
|
+
'ReactElement',
|
|
46
|
+
'FC',
|
|
47
|
+
'MouseEvent',
|
|
48
|
+
'KeyboardEvent',
|
|
49
|
+
'ChangeEvent',
|
|
50
|
+
'HTMLElement',
|
|
51
|
+
'HTMLDivElement',
|
|
52
|
+
'HTMLInputElement',
|
|
53
|
+
'CSSProperties',
|
|
54
|
+
'RefObject',
|
|
55
|
+
'MutableRefObject',
|
|
56
|
+
]);
|
|
57
|
+
function normalizeTypeName(name) {
|
|
58
|
+
if (BUILTIN_TYPES.has(name))
|
|
59
|
+
return name;
|
|
60
|
+
if (/^T[A-Z]/.test(name) || (name.length === 1 && /[A-Z]/.test(name)))
|
|
61
|
+
return '$T';
|
|
62
|
+
return name;
|
|
63
|
+
}
|
|
64
|
+
function normalizeTypeString(typeStr) {
|
|
65
|
+
return typeStr
|
|
66
|
+
.replace(/\b([A-Z][A-Za-z0-9]*)\b/g, (match) => normalizeTypeName(match))
|
|
67
|
+
.replace(/\s+/g, ' ')
|
|
68
|
+
.trim();
|
|
69
|
+
}
|
|
70
|
+
// ─────────────────────────────────────────────
|
|
71
|
+
// normalizeNode:递归遍历 AST,输出标准化字符串
|
|
72
|
+
// ─────────────────────────────────────────────
|
|
73
|
+
export function normalizeNode(node) {
|
|
74
|
+
const paramNames = new Map();
|
|
75
|
+
let paramIdx = 0;
|
|
76
|
+
function allocParam(name) {
|
|
77
|
+
if (!paramNames.has(name))
|
|
78
|
+
paramNames.set(name, `$p${paramIdx++}`);
|
|
79
|
+
return paramNames.get(name);
|
|
80
|
+
}
|
|
81
|
+
function visit(n) {
|
|
82
|
+
const kind = n.getKind();
|
|
83
|
+
// 函数体 → {}(不关心实现)
|
|
84
|
+
if (kind === SyntaxKind.Block)
|
|
85
|
+
return '{}';
|
|
86
|
+
// 参数:只保留类型,去参数名
|
|
87
|
+
if (Node.isParameterDeclaration(n)) {
|
|
88
|
+
const nameNode = n.getNameNode();
|
|
89
|
+
const typeNode = n.getTypeNode();
|
|
90
|
+
const typeStr = typeNode
|
|
91
|
+
? normalizeTypeString(typeNode.getText())
|
|
92
|
+
: '$unknown';
|
|
93
|
+
const prefix = n.isRestParameter() ? '...' : '';
|
|
94
|
+
const suffix = n.hasInitializer() ? '=$default' : '';
|
|
95
|
+
// 解构参数:{ userId, options }: Config → {}:Config
|
|
96
|
+
if (Node.isObjectBindingPattern(nameNode) ||
|
|
97
|
+
Node.isArrayBindingPattern(nameNode)) {
|
|
98
|
+
return `${prefix}{}:${typeStr}${suffix}`;
|
|
99
|
+
}
|
|
100
|
+
allocParam(n.getName());
|
|
101
|
+
return `${prefix}$p:${typeStr}${suffix}`;
|
|
102
|
+
}
|
|
103
|
+
// 泛型参数:T / TData → $T
|
|
104
|
+
if (Node.isTypeParameterDeclaration(n)) {
|
|
105
|
+
const constraint = n.getConstraint();
|
|
106
|
+
return `$T${constraint ? ` extends ${normalizeTypeString(constraint.getText())}` : ''}`;
|
|
107
|
+
}
|
|
108
|
+
// 类型引用:标准化名称
|
|
109
|
+
if (Node.isTypeReference(n))
|
|
110
|
+
return normalizeTypeString(n.getText());
|
|
111
|
+
// JSX:只记录存在
|
|
112
|
+
if (Node.isJsxElement(n) || Node.isJsxSelfClosingElement(n))
|
|
113
|
+
return '<JSX/>';
|
|
114
|
+
// 字面量 → 占位符
|
|
115
|
+
if (kind === SyntaxKind.StringLiteral)
|
|
116
|
+
return '"$s"';
|
|
117
|
+
if (kind === SyntaxKind.NumericLiteral ||
|
|
118
|
+
kind === SyntaxKind.BigIntLiteral)
|
|
119
|
+
return '$n';
|
|
120
|
+
if (kind === SyntaxKind.TrueKeyword || kind === SyntaxKind.FalseKeyword)
|
|
121
|
+
return '$b';
|
|
122
|
+
const children = n.getChildren();
|
|
123
|
+
if (children.length === 0)
|
|
124
|
+
return n.getText();
|
|
125
|
+
return children.map(visit).join('');
|
|
126
|
+
}
|
|
127
|
+
return visit(node).replace(/\s+/g, ' ').trim();
|
|
128
|
+
}
|
|
129
|
+
// ─────────────────────────────────────────────
|
|
130
|
+
// extractNormalizedSignature:从声明节点提取标准化签名
|
|
131
|
+
// ─────────────────────────────────────────────
|
|
132
|
+
export function extractNormalizedSignature(node) {
|
|
133
|
+
// 函数声明 / 箭头函数 / 函数表达式
|
|
134
|
+
if (Node.isFunctionDeclaration(node) ||
|
|
135
|
+
Node.isArrowFunction(node) ||
|
|
136
|
+
Node.isFunctionExpression(node)) {
|
|
137
|
+
const typeParams = Node.isFunctionDeclaration(node)
|
|
138
|
+
? node.getTypeParameters().map((tp) => normalizeNode(tp))
|
|
139
|
+
: [];
|
|
140
|
+
const params = node.getParameters().map((p) => normalizeNode(p));
|
|
141
|
+
const retNode = node.getReturnTypeNode?.();
|
|
142
|
+
const returnType = retNode
|
|
143
|
+
? normalizeTypeString(retNode.getText())
|
|
144
|
+
: '$inferred';
|
|
145
|
+
const tpStr = typeParams.length ? `<${typeParams.join(',')}>` : '';
|
|
146
|
+
return `fn${tpStr}(${params.join(',')})=>${returnType}`;
|
|
147
|
+
}
|
|
148
|
+
// 变量声明(const foo = () => {})
|
|
149
|
+
if (Node.isVariableDeclaration(node)) {
|
|
150
|
+
const init = node.getInitializer();
|
|
151
|
+
if (init &&
|
|
152
|
+
(Node.isArrowFunction(init) || Node.isFunctionExpression(init))) {
|
|
153
|
+
return extractNormalizedSignature(init);
|
|
154
|
+
}
|
|
155
|
+
return normalizeNode(node);
|
|
156
|
+
}
|
|
157
|
+
// interface / type alias
|
|
158
|
+
if (Node.isInterfaceDeclaration(node) ||
|
|
159
|
+
Node.isTypeAliasDeclaration(node)) {
|
|
160
|
+
return normalizeNode(node);
|
|
161
|
+
}
|
|
162
|
+
// class:只取方法签名列表
|
|
163
|
+
if (Node.isClassDeclaration(node)) {
|
|
164
|
+
const methods = node.getMethods().map((m) => {
|
|
165
|
+
const params = m.getParameters().map((p) => normalizeNode(p));
|
|
166
|
+
const retNode = m.getReturnTypeNode();
|
|
167
|
+
const ret = retNode
|
|
168
|
+
? normalizeTypeString(retNode.getText())
|
|
169
|
+
: '$inferred';
|
|
170
|
+
return `${m.getName()}(${params.join(',')})=>${ret}`;
|
|
171
|
+
});
|
|
172
|
+
return `class{${methods.join(';')}}`;
|
|
173
|
+
}
|
|
174
|
+
return normalizeNode(node);
|
|
175
|
+
}
|
|
176
|
+
// ─────────────────────────────────────────────
|
|
177
|
+
// computeSemanticHash
|
|
178
|
+
// 纳入:标准化签名 + name + type + description + sideEffects + hooks
|
|
179
|
+
// 排除:参数名、实现、格式、callers/callees
|
|
180
|
+
// ─────────────────────────────────────────────
|
|
181
|
+
export function computeSemanticHash(row) {
|
|
182
|
+
const node = row.node || null;
|
|
183
|
+
const meta = row.meta || {};
|
|
184
|
+
const stable = {
|
|
185
|
+
name: row.name,
|
|
186
|
+
type: row.type,
|
|
187
|
+
description: row.description ?? null,
|
|
188
|
+
signature: node ? extractNormalizedSignature(node) : '',
|
|
189
|
+
sideEffects: [
|
|
190
|
+
...(meta.sideEffects ?? []),
|
|
191
|
+
].sort(),
|
|
192
|
+
hooks: [...(meta.hooks ?? [])].sort(),
|
|
193
|
+
};
|
|
194
|
+
return createHash('sha256').update(JSON.stringify(stable)).digest('hex');
|
|
195
|
+
}
|
|
196
|
+
// ─────────────────────────────────────────────
|
|
197
|
+
// computeFileHash:对文件原始内容
|
|
198
|
+
// ─────────────────────────────────────────────
|
|
199
|
+
export function computeFileHash(fileContent) {
|
|
200
|
+
return createHash('sha256').update(fileContent).digest('hex');
|
|
201
|
+
}
|