zen-gitsync 2.11.23 → 2.11.25
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/package.json +2 -1
- package/src/gitCommit.js +1 -1
- package/src/ui/public/assets/{index-PbJjwkxi.css → index-ECLMif0J.css} +1 -1
- package/src/ui/public/assets/{index-D9qDSA8y.js → index-wOJowcbr.js} +6 -5
- package/src/ui/public/index.html +2 -2
- package/src/ui/server/routes/codeAnalysis.js +425 -29
package/src/ui/public/index.html
CHANGED
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
11
11
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
12
12
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;1,400&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
|
13
|
-
<script type="module" crossorigin src="/assets/index-
|
|
13
|
+
<script type="module" crossorigin src="/assets/index-wOJowcbr.js"></script>
|
|
14
14
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-BM3Ffeng.js">
|
|
15
15
|
<link rel="modulepreload" crossorigin href="/assets/vendor-CsS_wI9o.js">
|
|
16
16
|
<link rel="stylesheet" crossorigin href="/assets/vendor-CjiV6IT4.css">
|
|
17
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
17
|
+
<link rel="stylesheet" crossorigin href="/assets/index-ECLMif0J.css">
|
|
18
18
|
</head>
|
|
19
19
|
<body>
|
|
20
20
|
<div id="app"></div>
|
|
@@ -69,6 +69,294 @@ async function safeReadFile(filePath, maxBytes = 200000) {
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
73
|
+
// 静态 import 解析 & 模块依赖图
|
|
74
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 根据文件路径推断模块的语义角色(用于 AI 分析失败时的兜底描述)
|
|
78
|
+
*/
|
|
79
|
+
function inferModuleRole(filePath, inDegree = 0) {
|
|
80
|
+
const base = path.basename(filePath, path.extname(filePath));
|
|
81
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
82
|
+
const fwd = filePath.replace(/\\/g, '/');
|
|
83
|
+
const low = fwd.toLowerCase();
|
|
84
|
+
|
|
85
|
+
// ── 特殊文件名 ──
|
|
86
|
+
if (/^(main|app)$/i.test(base)) return 'Vue 应用入口';
|
|
87
|
+
if (/^server$/i.test(base)) return 'HTTP 服务主入口';
|
|
88
|
+
if (/^index$/i.test(base)) {
|
|
89
|
+
const parentDir = fwd.split('/').slice(-2, -1)[0] || '';
|
|
90
|
+
if (low.includes('/server') || low.includes('/backend')) return '服务端应用入口';
|
|
91
|
+
if (low.includes('/stores')) return `${parentDir} 状态模块`;
|
|
92
|
+
if (low.includes('/routes')) return `${parentDir} 路由入口`;
|
|
93
|
+
if (low.includes('/utils')) return `${parentDir} 工具集`;
|
|
94
|
+
return `${parentDir} 模块入口`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── Pinia / Vuex Store ──
|
|
98
|
+
if (low.includes('/stores/') || /store$/i.test(base)) {
|
|
99
|
+
const name = base.replace(/store$/i, '').replace(/([A-Z])/g, ' $1').trim();
|
|
100
|
+
return `${name} 状态管理`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── Vue 组件 ──
|
|
104
|
+
if (ext === '.vue' || low.includes('/components/')) return `${base} 组件`;
|
|
105
|
+
|
|
106
|
+
// ── Views / Pages ──
|
|
107
|
+
if (low.includes('/views/') || /view$/i.test(base) || /page$/i.test(base)) return `${base} 页面`;
|
|
108
|
+
|
|
109
|
+
// ── Routes ──
|
|
110
|
+
if (low.includes('/routes/') || /route[s]?$/i.test(base)) {
|
|
111
|
+
const name = base.replace(/route[s]?$/i, '').trim();
|
|
112
|
+
return name ? `${name} 路由模块` : '路由处理模块';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── Lang / i18n ──
|
|
116
|
+
if (low.includes('/lang/') || low.includes('/i18n/') || low.includes('/locale')) return '国际化文案资源';
|
|
117
|
+
|
|
118
|
+
// ── Utils / Helpers ──
|
|
119
|
+
if (low.includes('/utils/') || /util[s]?$/i.test(base) || /helper[s]?$/i.test(base)) return `${base} 工具集`;
|
|
120
|
+
|
|
121
|
+
// ── Composables / Hooks ──
|
|
122
|
+
if (low.includes('/composables/') || low.includes('/hooks/') || /^use[A-Z]/.test(base)) return `${base} 组合式函数`;
|
|
123
|
+
|
|
124
|
+
// ── API / Services ──
|
|
125
|
+
if (low.includes('/api/') || low.includes('/services/') || /service[s]?$/i.test(base) || /api$/i.test(base)) return `${base} API 服务`;
|
|
126
|
+
|
|
127
|
+
// ── Middleware ──
|
|
128
|
+
if (low.includes('/middleware/') || /middleware$/i.test(base)) return `${base} 中间件`;
|
|
129
|
+
|
|
130
|
+
// ── Config ──
|
|
131
|
+
if (/config$/i.test(base) || low.includes('/config/')) return `${base} 配置`;
|
|
132
|
+
|
|
133
|
+
// ── Types ──
|
|
134
|
+
if (/types?$/i.test(base) || low.includes('/types/')) return `${base} 类型定义`;
|
|
135
|
+
|
|
136
|
+
// ── 按引用次数降级 ──
|
|
137
|
+
if (inDegree >= 20) return `高频共享模块(被引用 ${inDegree} 次)`;
|
|
138
|
+
if (inDegree >= 5) return `核心共享模块(被引用 ${inDegree} 次)`;
|
|
139
|
+
return `${base} 功能模块`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 正则提取 import / require / dynamic-import 的模块路径(TypeScript/Vue 通用)
|
|
144
|
+
*/
|
|
145
|
+
function parseImportsRegex(src) {
|
|
146
|
+
const results = new Set();
|
|
147
|
+
// import ... from '...' | export ... from '...' | import type ... from '...'
|
|
148
|
+
const importRe = /(?:^|;|\n)\s*(?:import|export)(?:\s+type)?\s+(?:[^'"`;\n]*?\bfrom\s+)?['"]([^'"` \n]+)['"]/gm;
|
|
149
|
+
let m;
|
|
150
|
+
while ((m = importRe.exec(src)) !== null) { if (m[1]) results.add(m[1]); }
|
|
151
|
+
// require('...')
|
|
152
|
+
const requireRe = /\brequire\s*\(\s*['"]([^'"` \n]+)['"]\s*\)/g;
|
|
153
|
+
while ((m = requireRe.exec(src)) !== null) { if (m[1]) results.add(m[1]); }
|
|
154
|
+
// dynamic import('...')
|
|
155
|
+
const dynRe = /\bimport\s*\(\s*['"]([^'"` \n]+)['"]\s*\)/g;
|
|
156
|
+
while ((m = dynRe.exec(src)) !== null) { if (m[1]) results.add(m[1]); }
|
|
157
|
+
return [...results];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 从源码中静态提取 import 路径
|
|
162
|
+
* - JS/MJS/CJS/JSX:优先用 acorn AST 解析,失败则正则兜底
|
|
163
|
+
* - TS/TSX/Vue:正则(import 语法与 JS 相同,acorn 不支持 TypeScript 类型语法)
|
|
164
|
+
*/
|
|
165
|
+
async function parseStaticImports(filePath, content) {
|
|
166
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
167
|
+
let src = content || '';
|
|
168
|
+
|
|
169
|
+
// Vue SFC:提取 <script> 块
|
|
170
|
+
if (ext === '.vue') {
|
|
171
|
+
const m = src.match(/<script(?:[^>]*)>([\s\S]*?)<\/script>/i);
|
|
172
|
+
src = m ? m[1] : '';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// JS / MJS / CJS / JSX → 尝试 acorn AST(最准确)
|
|
176
|
+
if (['.js', '.mjs', '.cjs', '.jsx'].includes(ext)) {
|
|
177
|
+
try {
|
|
178
|
+
const acorn = await import('acorn');
|
|
179
|
+
const ast = acorn.parse(src, {
|
|
180
|
+
ecmaVersion: 2022,
|
|
181
|
+
sourceType: 'module',
|
|
182
|
+
allowImportExportEverywhere: true,
|
|
183
|
+
allowHashBang: true,
|
|
184
|
+
});
|
|
185
|
+
const imports = [];
|
|
186
|
+
for (const node of ast.body) {
|
|
187
|
+
if (node.type === 'ImportDeclaration') imports.push(node.source.value);
|
|
188
|
+
if (node.type === 'ExportNamedDeclaration' && node.source) imports.push(node.source.value);
|
|
189
|
+
if (node.type === 'ExportAllDeclaration' && node.source) imports.push(node.source.value);
|
|
190
|
+
// CommonJS require('...')
|
|
191
|
+
if (node.type === 'ExpressionStatement') {
|
|
192
|
+
const expr = node.expression;
|
|
193
|
+
if (expr?.type === 'CallExpression' && expr.callee?.name === 'require' &&
|
|
194
|
+
expr.arguments?.[0]?.type === 'Literal') {
|
|
195
|
+
imports.push(expr.arguments[0].value);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return imports;
|
|
200
|
+
} catch { /* fallthrough to regex */ }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// TS / TSX / Vue / 其他 → 正则解析
|
|
204
|
+
return parseImportsRegex(src);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** 可解析的扩展名(按优先级) */
|
|
208
|
+
const RESOLVE_EXTS = ['.ts', '.tsx', '.js', '.mjs', '.jsx', '.vue'];
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* 尝试在文件集合中找到匹配的实际文件(带扩展名推断 + index 文件)
|
|
212
|
+
*/
|
|
213
|
+
function tryResolveWithExts(base, allFilesSet) {
|
|
214
|
+
if (allFilesSet.has(base)) return base;
|
|
215
|
+
for (const ext of RESOLVE_EXTS) {
|
|
216
|
+
if (allFilesSet.has(base + ext)) return base + ext;
|
|
217
|
+
if (allFilesSet.has(base + '/index' + ext)) return base + '/index' + ext;
|
|
218
|
+
}
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* 将 import 路径解析为项目内相对路径
|
|
224
|
+
* 支持相对路径(./xxx)和路径别名(@/xxx、@components/xxx 等)
|
|
225
|
+
* 别名按长度降序匹配,避免 @/ 抢占 @components/ 等更长别名
|
|
226
|
+
*/
|
|
227
|
+
function resolveImportPath(fromFile, importPath, allFilesSet, aliasMap) {
|
|
228
|
+
// 按别名长度降序排列,优先匹配最长别名(@components/ 优先于 @/)
|
|
229
|
+
const sortedAliases = Object.entries(aliasMap).sort((a, b) => b[0].length - a[0].length);
|
|
230
|
+
for (const [alias, target] of sortedAliases) {
|
|
231
|
+
if (importPath.startsWith(alias)) {
|
|
232
|
+
const resolved = (target + importPath.slice(alias.length)).replace(/\\/g, '/');
|
|
233
|
+
return tryResolveWithExts(resolved, allFilesSet);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// 非相对路径(node_modules 包)—— 不解析
|
|
237
|
+
if (!importPath.startsWith('.')) return null;
|
|
238
|
+
|
|
239
|
+
const fromDir = path.dirname(fromFile).replace(/\\/g, '/');
|
|
240
|
+
const joined = path.posix.normalize(fromDir + '/' + importPath);
|
|
241
|
+
return tryResolveWithExts(joined, allFilesSet);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* 从 vite.config 解析路径别名(支持多别名:@、@components、@views 等)
|
|
246
|
+
* @param {string} resolvedDir 项目根目录(绝对路径)
|
|
247
|
+
* @param {string[]} subFiles 子系统文件列表(相对路径)
|
|
248
|
+
* @param {string} rootPath 子系统 rootPath(相对路径,如 src/ui/client/src)
|
|
249
|
+
* @returns {Record<string, string>} { 'alias/' → 'target/dir/' }
|
|
250
|
+
*/
|
|
251
|
+
async function detectAliasMap(resolvedDir, subFiles, rootPath = '') {
|
|
252
|
+
const aliasMap = {};
|
|
253
|
+
|
|
254
|
+
// 计算要搜索 vite.config 的候选目录
|
|
255
|
+
// 通常 vite.config.ts 在 rootPath 的父目录(如 src/ui/client)
|
|
256
|
+
const searchDirs = [resolvedDir];
|
|
257
|
+
if (rootPath) {
|
|
258
|
+
const parent = path.dirname(rootPath);
|
|
259
|
+
if (parent && parent !== '.' && parent !== rootPath) {
|
|
260
|
+
searchDirs.push(path.resolve(resolvedDir, parent));
|
|
261
|
+
}
|
|
262
|
+
// rootPath 本身也搜一下
|
|
263
|
+
searchDirs.push(path.resolve(resolvedDir, rootPath));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
for (const searchDir of searchDirs) {
|
|
267
|
+
for (const cfgName of ['vite.config.ts', 'vite.config.js', 'vite.config.mjs']) {
|
|
268
|
+
try {
|
|
269
|
+
const cfgPath = path.join(searchDir, cfgName);
|
|
270
|
+
const content = await fs.readFile(cfgPath, 'utf8');
|
|
271
|
+
|
|
272
|
+
// 提取 alias 对象块(支持跨行)
|
|
273
|
+
const aliasBlockMatch = content.match(/alias\s*:\s*\{([^}]+)\}/s);
|
|
274
|
+
if (!aliasBlockMatch) continue;
|
|
275
|
+
|
|
276
|
+
const aliasBody = aliasBlockMatch[1];
|
|
277
|
+
// 匹配 "@xxx": path.resolve(__dirname, "./relpath") 或 path.resolve(xxx, "relpath")
|
|
278
|
+
const entryRe = /["'](@[^"']*)["']\s*:\s*path\.resolve\s*\([^,)]+,\s*["']([^"']+)["']\s*\)/g;
|
|
279
|
+
let m;
|
|
280
|
+
const cfgDir = path.dirname(cfgPath);
|
|
281
|
+
while ((m = entryRe.exec(aliasBody)) !== null) {
|
|
282
|
+
const aliasKey = m[1]; // e.g. '@', '@components'
|
|
283
|
+
const relTarget = m[2]; // e.g. './src', './src/components'
|
|
284
|
+
const absTarget = path.resolve(cfgDir, relTarget);
|
|
285
|
+
const relToProject = path.relative(resolvedDir, absTarget).replace(/\\/g, '/');
|
|
286
|
+
aliasMap[aliasKey + '/'] = relToProject + '/';
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (Object.keys(aliasMap).length > 0) return aliasMap;
|
|
290
|
+
} catch { /* ignore */ }
|
|
291
|
+
}
|
|
292
|
+
if (Object.keys(aliasMap).length > 0) break;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// 兜底:启发式推断 @/ 指向文件数最多的 src 目录
|
|
296
|
+
if (!aliasMap['@/']) {
|
|
297
|
+
for (const c of ['src/ui/client/src', 'src/client/src', 'client/src', 'src']) {
|
|
298
|
+
if (subFiles.some(f => f.startsWith(c + '/'))) {
|
|
299
|
+
aliasMap['@/'] = c + '/';
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return aliasMap;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* 构建完整模块依赖图
|
|
310
|
+
* @param {string[]} subFiles 子系统文件列表(相对路径)
|
|
311
|
+
* @param {string} resolvedDir 项目根目录(绝对路径)
|
|
312
|
+
* @param {string} rootPath 子系统 rootPath(用于定位 vite.config)
|
|
313
|
+
* @returns {{ graph, inDegree, hubFiles, entryCandidates, totalEdges, fileSizes }}
|
|
314
|
+
*/
|
|
315
|
+
async function buildDependencyGraph(subFiles, resolvedDir, rootPath = '') {
|
|
316
|
+
const allFilesSet = new Set(subFiles);
|
|
317
|
+
const aliasMap = await detectAliasMap(resolvedDir, subFiles, rootPath);
|
|
318
|
+
|
|
319
|
+
const graph = {}; // file → string[](它 import 的项目内文件)
|
|
320
|
+
const fileSizes = {}; // file → 行数
|
|
321
|
+
|
|
322
|
+
for (const file of subFiles) {
|
|
323
|
+
const content = await safeReadFile(path.resolve(resolvedDir, file), 150000);
|
|
324
|
+
fileSizes[file] = content ? content.split('\n').length : 0;
|
|
325
|
+
const rawImports = await parseStaticImports(file, content);
|
|
326
|
+
const resolved = [];
|
|
327
|
+
for (const imp of rawImports) {
|
|
328
|
+
const r = resolveImportPath(file, imp, allFilesSet, aliasMap);
|
|
329
|
+
if (r) resolved.push(r);
|
|
330
|
+
}
|
|
331
|
+
graph[file] = [...new Set(resolved)];
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// 计算入度(被多少文件 import)
|
|
335
|
+
const inDegree = {};
|
|
336
|
+
for (const file of subFiles) inDegree[file] = 0;
|
|
337
|
+
for (const deps of Object.values(graph)) {
|
|
338
|
+
for (const dep of deps) { if (dep in inDegree) inDegree[dep]++; }
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Hub files = 被最多模块依赖的核心文件
|
|
342
|
+
const hubFiles = Object.entries(inDegree)
|
|
343
|
+
.sort((a, b) => b[1] - a[1])
|
|
344
|
+
.filter(([, d]) => d > 0)
|
|
345
|
+
.slice(0, 8)
|
|
346
|
+
.map(([file, inDeg]) => ({ file, inDegree: inDeg, lines: fileSizes[file] || 0 }));
|
|
347
|
+
|
|
348
|
+
// 入口候选 = 出度 > 0 且入度 = 0(项目根节点,没有被其他文件 import)
|
|
349
|
+
const entryCandidates = subFiles.filter(
|
|
350
|
+
f => (graph[f]?.length || 0) > 0 && (inDegree[f] || 0) === 0
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
const totalEdges = Object.values(graph).reduce((s, d) => s + d.length, 0);
|
|
354
|
+
|
|
355
|
+
return { graph, inDegree, hubFiles, entryCandidates, totalEdges, fileSizes };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
359
|
+
|
|
72
360
|
/**
|
|
73
361
|
* 调用 OpenAI 兼容 API,返回 JSON
|
|
74
362
|
*/
|
|
@@ -298,7 +586,8 @@ export function registerCodeAnalysisRoutes({ app, configManager }) {
|
|
|
298
586
|
} else {
|
|
299
587
|
// 无多个 package.json,尝试已知路径模式
|
|
300
588
|
for (const pattern of KNOWN_PATTERNS) {
|
|
301
|
-
|
|
589
|
+
// 必须是目录前缀匹配(加 / 后缀),避免 'server' 误匹配 'server.js'
|
|
590
|
+
const hasMatch = codeFiles.some(f => f.startsWith(pattern.pathPattern + '/'));
|
|
302
591
|
if (hasMatch) {
|
|
303
592
|
// 避免重复添加同名模式
|
|
304
593
|
if (!programmaticSubsystems.find(s => s.rootPath === pattern.pathPattern)) {
|
|
@@ -382,14 +671,46 @@ ${JSON.stringify(codeFiles.slice(0, 300))}
|
|
|
382
671
|
continue;
|
|
383
672
|
}
|
|
384
673
|
|
|
674
|
+
// ── Step 2.5: 静态 import 解析 → 构建真实模块依赖图 ───────────────
|
|
675
|
+
log(`[${subsystemName}] 正在静态解析 import 依赖(${subFiles.length} 个文件)...`, 'info');
|
|
676
|
+
const { graph: depGraph, inDegree, hubFiles, entryCandidates, totalEdges, fileSizes } =
|
|
677
|
+
await buildDependencyGraph(subFiles, resolved, sub.rootPath || '');
|
|
678
|
+
log(`[${subsystemName}] 依赖图完成:${totalEdges} 条 import 关系,${hubFiles.length} 个核心模块`, 'success');
|
|
679
|
+
if (stopped) break;
|
|
680
|
+
|
|
681
|
+
// 向前端推送静态依赖图数据
|
|
682
|
+
sendSse(res, 'depgraph', {
|
|
683
|
+
subsystem: sub.name,
|
|
684
|
+
graph: depGraph,
|
|
685
|
+
hubFiles,
|
|
686
|
+
entryCandidates,
|
|
687
|
+
inDegree,
|
|
688
|
+
fileSizes,
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
// ── Step 3: 识别入口文件(优先静态分析结论,AI 作为兜底) ──────────
|
|
385
692
|
log(`[${subsystemName}] 正在识别入口文件...`, 'thinking');
|
|
386
693
|
|
|
387
|
-
//
|
|
388
|
-
const
|
|
694
|
+
// 静态入口候选(入度=0 且有出度的根节点)按常见命名排序
|
|
695
|
+
const ENTRY_NAME_PRIORITY = ['main', 'index', 'app', 'server', 'start', 'entry'];
|
|
696
|
+
const staticEntryCandidates = [...entryCandidates].sort((a, b) => {
|
|
697
|
+
const aScore = ENTRY_NAME_PRIORITY.findIndex(n => path.basename(a, path.extname(a)).toLowerCase().includes(n));
|
|
698
|
+
const bScore = ENTRY_NAME_PRIORITY.findIndex(n => path.basename(b, path.extname(b)).toLowerCase().includes(n));
|
|
699
|
+
return (aScore === -1 ? 99 : aScore) - (bScore === -1 ? 99 : bScore);
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
let subLang = sub.language || 'Unknown';
|
|
703
|
+
let subSummary = '';
|
|
704
|
+
let subEntryFiles = staticEntryCandidates.slice(0, 3);
|
|
705
|
+
let subEntryFunctions = ['main'];
|
|
706
|
+
|
|
707
|
+
// 仅当静态分析无法确定入口时,才调用 AI
|
|
708
|
+
if (subEntryFiles.length === 0) {
|
|
709
|
+
const subEntryPrompt = `你是软件架构师。以下是子系统的代码文件列表,请识别语言和入口文件。
|
|
389
710
|
|
|
390
711
|
子系统: ${subsystemName}
|
|
391
712
|
文件列表:
|
|
392
|
-
${JSON.stringify(subFiles.slice(0,
|
|
713
|
+
${JSON.stringify(subFiles.slice(0, 150))}
|
|
393
714
|
|
|
394
715
|
返回 JSON:
|
|
395
716
|
{
|
|
@@ -398,14 +719,18 @@ ${JSON.stringify(subFiles.slice(0, 200))}
|
|
|
398
719
|
"potentialEntryFunctions": ["main"],
|
|
399
720
|
"projectSummary": "简短中文描述"
|
|
400
721
|
}`;
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
722
|
+
const subEntryData = await callLlmJson(model, subEntryPrompt);
|
|
723
|
+
if (stopped) break;
|
|
724
|
+
subLang = String(subEntryData.language || sub.language || 'Unknown');
|
|
725
|
+
subEntryFiles = Array.isArray(subEntryData.potentialEntryFiles) ? subEntryData.potentialEntryFiles.slice(0, 3) : [];
|
|
726
|
+
subEntryFunctions = Array.isArray(subEntryData.potentialEntryFunctions) ? subEntryData.potentialEntryFunctions : ['main'];
|
|
727
|
+
subSummary = String(subEntryData.projectSummary || '');
|
|
728
|
+
} else {
|
|
729
|
+
// 从文件扩展名推断语言
|
|
730
|
+
const extLangMap = { '.ts': 'TypeScript', '.tsx': 'TypeScript', '.vue': 'TypeScript', '.js': 'JavaScript', '.mjs': 'JavaScript', '.py': 'Python', '.go': 'Go', '.rs': 'Rust', '.java': 'Java' };
|
|
731
|
+
const firstExt = path.extname(subEntryFiles[0]).toLowerCase();
|
|
732
|
+
subLang = extLangMap[firstExt] || sub.language || 'Unknown';
|
|
733
|
+
}
|
|
409
734
|
|
|
410
735
|
if (!primaryLanguage || primaryLanguage === 'Unknown') primaryLanguage = subLang;
|
|
411
736
|
|
|
@@ -415,7 +740,7 @@ ${JSON.stringify(subFiles.slice(0, 200))}
|
|
|
415
740
|
for (const candidate of subEntryFiles) {
|
|
416
741
|
if (stopped) break;
|
|
417
742
|
const candidatePath = path.resolve(resolved, candidate);
|
|
418
|
-
if (!candidatePath.startsWith(resolved)) continue;
|
|
743
|
+
if (!candidatePath.startsWith(resolved + path.sep) && candidatePath !== resolved) continue;
|
|
419
744
|
const content = await safeReadFile(candidatePath);
|
|
420
745
|
if (content) { entryFile = candidate; entryContent = content; break; }
|
|
421
746
|
}
|
|
@@ -426,44 +751,115 @@ ${JSON.stringify(subFiles.slice(0, 200))}
|
|
|
426
751
|
if (!primaryEntryFile) primaryEntryFile = entryFile;
|
|
427
752
|
if (!overallSummary) overallSummary = subSummary;
|
|
428
753
|
|
|
429
|
-
|
|
754
|
+
// ── Step 4: AI 语义分析(以静态依赖图为骨架) ──────────────────────
|
|
755
|
+
log(`[${subsystemName}] AI 正在基于依赖图做语义分析...`, 'thinking');
|
|
756
|
+
|
|
757
|
+
// 读取 Hub 文件内容(被引用最多的核心模块,每个最多 2500 字符)
|
|
758
|
+
const hubContents = [];
|
|
759
|
+
for (const { file: hubFile } of hubFiles.slice(0, 4)) {
|
|
760
|
+
if (stopped) break;
|
|
761
|
+
const hubPath = path.resolve(resolved, hubFile);
|
|
762
|
+
if (!hubPath.startsWith(resolved)) continue;
|
|
763
|
+
const content = await safeReadFile(hubPath, 80000);
|
|
764
|
+
if (content) hubContents.push({ file: hubFile, content: content.slice(0, 2500) });
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// 构建依赖图文本摘要(展示文件间 import 关系,仅展示有边的节点)
|
|
768
|
+
const graphLines = Object.entries(depGraph)
|
|
769
|
+
.filter(([, deps]) => deps.length > 0)
|
|
770
|
+
.sort((a, b) => b[1].length - a[1].length)
|
|
771
|
+
.slice(0, 60)
|
|
772
|
+
.map(([f, deps]) => ` ${f} (${fileSizes[f] || '?'}行) → [${deps.join(', ')}]`);
|
|
773
|
+
|
|
774
|
+
const hubSummary = hubFiles
|
|
775
|
+
.map(h => ` ${h.file} (被引用${h.inDegree}次, ${h.lines}行)`)
|
|
776
|
+
.join('\n');
|
|
430
777
|
|
|
431
778
|
const entryFunction = subEntryFunctions[0] || 'main';
|
|
432
|
-
const
|
|
779
|
+
const entrySnippet = entryContent.slice(0, 3000);
|
|
433
780
|
|
|
434
|
-
|
|
435
|
-
|
|
781
|
+
const hubContentText = hubContents.map(h =>
|
|
782
|
+
`\n--- ${h.file} ---\n${h.content}`
|
|
783
|
+
).join('\n');
|
|
784
|
+
|
|
785
|
+
// Step 4: 分析调用链(基于真实依赖图)
|
|
786
|
+
const chainPrompt = `你是代码架构分析师。以下是通过静态 import 解析得到的真实模块依赖图,请基于此输出调用图节点和语义描述。
|
|
436
787
|
|
|
437
788
|
语言: ${subLang}
|
|
438
789
|
子系统: ${subsystemName}
|
|
439
|
-
入口文件: ${entryFile}
|
|
440
|
-
入口函数: ${entryFunction}
|
|
790
|
+
入口文件: ${entryFile}(入口函数: ${entryFunction})
|
|
441
791
|
|
|
442
|
-
|
|
443
|
-
${
|
|
792
|
+
## 真实模块依赖图(静态 import 解析结果)
|
|
793
|
+
共 ${subFiles.length} 个文件,${totalEdges} 条 import 关系
|
|
794
|
+
格式:文件(行数) → [它所 import 的文件]
|
|
444
795
|
|
|
445
|
-
|
|
796
|
+
${graphLines.join('\n') || '(无内部 import 关系)'}
|
|
797
|
+
|
|
798
|
+
## 核心模块(按被引用次数排序)
|
|
799
|
+
${hubSummary || '(未检测到高频被引用模块)'}
|
|
800
|
+
|
|
801
|
+
## 入口文件内容(截取前3000字符)
|
|
802
|
+
${entrySnippet}
|
|
803
|
+
|
|
804
|
+
## 核心模块文件内容
|
|
805
|
+
${hubContentText || '(无)'}
|
|
806
|
+
|
|
807
|
+
返回 JSON(基于真实依赖图,不要凭空猜测):
|
|
446
808
|
{
|
|
447
809
|
"entryFunction": "函数名",
|
|
448
810
|
"nodes": [
|
|
449
|
-
{ "id": "n1", "label": "
|
|
811
|
+
{ "id": "n1", "label": "模块/函数名", "file": "文件路径(必须来自依赖图)", "line": 1, "type": "module|function|store|component|api", "importance": "high|medium|low", "description": "职责中文描述(10-30字)" }
|
|
450
812
|
],
|
|
451
813
|
"edges": [{ "source": "n1", "target": "n2" }],
|
|
452
814
|
"techStack": ["Vue3", "TypeScript"],
|
|
453
|
-
"summary": "
|
|
815
|
+
"summary": "子系统架构中文概要(50-100字)"
|
|
454
816
|
}
|
|
455
817
|
|
|
456
818
|
要求:
|
|
457
|
-
1.
|
|
458
|
-
2.
|
|
459
|
-
3.
|
|
819
|
+
1. 节点必须对应依赖图中真实存在的文件,不要编造节点
|
|
820
|
+
2. edges 必须反映真实 import 关系(A import B → A→B 有边)
|
|
821
|
+
3. 最多 25 个节点,优先选择高入度的核心模块
|
|
822
|
+
4. importance: high(核心/hub)/ medium(普通)/ low(叶子/工具)
|
|
823
|
+
5. type 可选: module / function / store / component / api / util / config`;
|
|
460
824
|
|
|
461
825
|
const chainData = await callLlmJson(model, chainPrompt);
|
|
462
826
|
if (stopped) break;
|
|
463
827
|
|
|
464
828
|
// 合并节点/边,加子系统标识,ID 加前缀避免冲突
|
|
465
|
-
|
|
466
|
-
|
|
829
|
+
let rawNodes = Array.isArray(chainData?.nodes) ? chainData.nodes : [];
|
|
830
|
+
let rawEdges = Array.isArray(chainData?.edges) ? chainData.edges : [];
|
|
831
|
+
|
|
832
|
+
// ── 兜底:AI 未返回节点时,直接从静态依赖图生成基础节点 ────────────
|
|
833
|
+
if (rawNodes.length === 0) {
|
|
834
|
+
log(`[${subsystemName}] AI 未返回节点,从静态依赖图生成基础可视化...`, 'info');
|
|
835
|
+
const seen = new Set();
|
|
836
|
+
const fbNodes = [];
|
|
837
|
+
// 入口节点
|
|
838
|
+
for (const f of staticEntryCandidates.slice(0, 3)) {
|
|
839
|
+
if (!seen.has(f)) {
|
|
840
|
+
seen.add(f);
|
|
841
|
+
fbNodes.push({ id: `fe_${fbNodes.length}`, label: path.basename(f, path.extname(f)), file: f, line: 1, type: 'module', importance: 'high', description: inferModuleRole(f, 0) });
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
// Hub 节点(入度最高)
|
|
845
|
+
for (const h of hubFiles.slice(0, 10)) {
|
|
846
|
+
if (!seen.has(h.file)) {
|
|
847
|
+
seen.add(h.file);
|
|
848
|
+
fbNodes.push({ id: `fh_${fbNodes.length}`, label: path.basename(h.file, path.extname(h.file)), file: h.file, line: 1, type: 'module', importance: h.inDegree >= 5 ? 'high' : h.inDegree >= 2 ? 'medium' : 'low', description: inferModuleRole(h.file, h.inDegree) });
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
rawNodes = fbNodes;
|
|
852
|
+
// 从静态依赖图生成边
|
|
853
|
+
const nodeFileMap = {};
|
|
854
|
+
rawNodes.forEach(n => { nodeFileMap[n.file] = n.id; });
|
|
855
|
+
rawEdges = [];
|
|
856
|
+
for (const [srcFile, dstFiles] of Object.entries(depGraph)) {
|
|
857
|
+
if (!nodeFileMap[srcFile]) continue;
|
|
858
|
+
for (const dstFile of dstFiles) {
|
|
859
|
+
if (nodeFileMap[dstFile]) rawEdges.push({ source: nodeFileMap[srcFile], target: nodeFileMap[dstFile] });
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
467
863
|
const idMap = {};
|
|
468
864
|
rawNodes.forEach((n, i) => { idMap[n.id] = `sub${si}_${n.id || i}`; });
|
|
469
865
|
|