skill-any-code 1.0.0 → 1.0.1
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/dist/adapters/command.schemas.js +18 -0
- package/dist/application/analysis.app.service.js +264 -0
- package/dist/application/bootstrap.js +21 -0
- package/dist/application/services/llm.analysis.service.js +170 -0
- package/dist/common/config.js +213 -0
- package/dist/common/constants.js +11 -0
- package/dist/common/errors.js +37 -0
- package/dist/common/logger.js +77 -0
- package/dist/common/types.js +2 -0
- package/dist/common/ui.js +201 -0
- package/dist/common/utils.js +117 -0
- package/dist/domain/index.js +17 -0
- package/dist/domain/interfaces.js +2 -0
- package/dist/domain/services/analysis.service.js +696 -0
- package/dist/domain/services/incremental.service.js +81 -0
- package/dist/infrastructure/blacklist.service.js +71 -0
- package/dist/infrastructure/cache/file.hash.cache.js +140 -0
- package/dist/infrastructure/git/git.service.js +159 -0
- package/dist/infrastructure/git.service.js +157 -0
- package/dist/infrastructure/index.service.js +108 -0
- package/dist/infrastructure/llm/llm.usage.tracker.js +58 -0
- package/dist/infrastructure/llm/openai.client.js +141 -0
- package/{src/infrastructure/llm/prompt.template.ts → dist/infrastructure/llm/prompt.template.js} +31 -36
- package/dist/infrastructure/llm.service.js +61 -0
- package/dist/infrastructure/skill/skill.generator.js +83 -0
- package/{src/infrastructure/skill/templates/resolve.script.ts → dist/infrastructure/skill/templates/resolve.script.js} +18 -15
- package/dist/infrastructure/skill/templates/skill.md.template.js +47 -0
- package/dist/infrastructure/splitter/code.splitter.js +137 -0
- package/dist/infrastructure/storage.service.js +409 -0
- package/dist/infrastructure/worker-pool/parse.worker.impl.js +137 -0
- package/dist/infrastructure/worker-pool/parse.worker.js +43 -0
- package/dist/infrastructure/worker-pool/worker-pool.service.js +171 -0
- package/package.json +5 -1
- package/jest.config.js +0 -27
- package/src/adapters/command.schemas.ts +0 -21
- package/src/application/analysis.app.service.ts +0 -272
- package/src/application/bootstrap.ts +0 -35
- package/src/application/services/llm.analysis.service.ts +0 -237
- package/src/cli.ts +0 -297
- package/src/common/config.ts +0 -209
- package/src/common/constants.ts +0 -8
- package/src/common/errors.ts +0 -34
- package/src/common/logger.ts +0 -82
- package/src/common/types.ts +0 -385
- package/src/common/ui.ts +0 -228
- package/src/common/utils.ts +0 -81
- package/src/domain/index.ts +0 -1
- package/src/domain/interfaces.ts +0 -188
- package/src/domain/services/analysis.service.ts +0 -735
- package/src/domain/services/incremental.service.ts +0 -50
- package/src/index.ts +0 -6
- package/src/infrastructure/blacklist.service.ts +0 -37
- package/src/infrastructure/cache/file.hash.cache.ts +0 -119
- package/src/infrastructure/git/git.service.ts +0 -120
- package/src/infrastructure/git.service.ts +0 -121
- package/src/infrastructure/index.service.ts +0 -94
- package/src/infrastructure/llm/llm.usage.tracker.ts +0 -65
- package/src/infrastructure/llm/openai.client.ts +0 -162
- package/src/infrastructure/llm.service.ts +0 -70
- package/src/infrastructure/skill/skill.generator.ts +0 -53
- package/src/infrastructure/skill/templates/skill.md.template.ts +0 -45
- package/src/infrastructure/splitter/code.splitter.ts +0 -176
- package/src/infrastructure/storage.service.ts +0 -413
- package/src/infrastructure/worker-pool/parse.worker.impl.ts +0 -135
- package/src/infrastructure/worker-pool/parse.worker.ts +0 -9
- package/src/infrastructure/worker-pool/worker-pool.service.ts +0 -173
- package/tsconfig.json +0 -24
- package/tsconfig.test.json +0 -5
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ProjectCodeQuerySkillSchema = exports.AnalyzeProjectCommandSchema = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
exports.AnalyzeProjectCommandSchema = zod_1.z.object({
|
|
6
|
+
path: zod_1.z.string().optional().describe('Project root path to analyze (default: current working directory)'),
|
|
7
|
+
mode: zod_1.z.enum(['full', 'incremental', 'auto']).default('auto').describe('Analysis mode'),
|
|
8
|
+
depth: zod_1.z.number().int().min(1).optional().describe('Max directory depth (-1 = unlimited)'),
|
|
9
|
+
concurrency: zod_1.z.number().int().min(1).optional().describe('Max concurrency (default: CPU cores * 2)'),
|
|
10
|
+
outputDir: zod_1.z.string().optional().describe('Output directory for analysis results'),
|
|
11
|
+
skillsProviders: zod_1.z.array(zod_1.z.string()).optional().describe('AI tool providers to deploy the skill to'),
|
|
12
|
+
noSkills: zod_1.z.boolean().optional().describe('Skip skill generation')
|
|
13
|
+
});
|
|
14
|
+
exports.ProjectCodeQuerySkillSchema = zod_1.z.object({
|
|
15
|
+
path: zod_1.z.string().describe('Relative path of the file/directory from the project root'),
|
|
16
|
+
type: zod_1.z.enum(['summary', 'full', 'diagram']).default('summary').describe('Query type'),
|
|
17
|
+
projectSlug: zod_1.z.string().optional().describe('Project identifier (default: current project)')
|
|
18
|
+
});
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.AnalysisAppService = void 0;
|
|
37
|
+
const git_service_1 = require("../infrastructure/git.service");
|
|
38
|
+
const storage_service_1 = require("../infrastructure/storage.service");
|
|
39
|
+
const blacklist_service_1 = require("../infrastructure/blacklist.service");
|
|
40
|
+
const skill_generator_1 = require("../infrastructure/skill/skill.generator");
|
|
41
|
+
const analysis_service_1 = require("../domain/services/analysis.service");
|
|
42
|
+
const utils_1 = require("../common/utils");
|
|
43
|
+
const errors_1 = require("../common/errors");
|
|
44
|
+
const constants_1 = require("../common/constants");
|
|
45
|
+
const logger_1 = require("../common/logger");
|
|
46
|
+
const fs = __importStar(require("fs-extra"));
|
|
47
|
+
const crypto_1 = require("crypto");
|
|
48
|
+
class AnalysisAppService {
|
|
49
|
+
totalObjects = 0;
|
|
50
|
+
completedObjects = 0;
|
|
51
|
+
activeObjects = new Set();
|
|
52
|
+
progressEnabled = false;
|
|
53
|
+
onProgress;
|
|
54
|
+
concurrency = constants_1.DEFAULT_CONCURRENCY;
|
|
55
|
+
lastRenderedCurrentKey = null;
|
|
56
|
+
async runAnalysis(params) {
|
|
57
|
+
const projectRoot = params.path || process.cwd();
|
|
58
|
+
logger_1.logger.info(`Analysis started. Project root: ${projectRoot}`);
|
|
59
|
+
this.progressEnabled = typeof params.onProgress === 'function';
|
|
60
|
+
this.onProgress = params.onProgress;
|
|
61
|
+
this.concurrency = params.concurrency || constants_1.DEFAULT_CONCURRENCY;
|
|
62
|
+
this.totalObjects = 0;
|
|
63
|
+
this.completedObjects = 0;
|
|
64
|
+
this.activeObjects = new Set();
|
|
65
|
+
this.lastRenderedCurrentKey = null;
|
|
66
|
+
const outputDir = params.outputDir;
|
|
67
|
+
const gitService = new git_service_1.GitService(projectRoot);
|
|
68
|
+
const storageService = new storage_service_1.LocalStorageService(projectRoot, outputDir);
|
|
69
|
+
// 检测是否为Git项目
|
|
70
|
+
const isGit = await gitService.isGitProject();
|
|
71
|
+
let projectSlug;
|
|
72
|
+
let currentCommit = '';
|
|
73
|
+
let currentBranch = '';
|
|
74
|
+
logger_1.logger.debug(`Project path: ${projectRoot}, isGit: ${isGit}`);
|
|
75
|
+
if (isGit) {
|
|
76
|
+
currentCommit = await gitService.getCurrentCommit();
|
|
77
|
+
currentBranch = await gitService.getCurrentBranch();
|
|
78
|
+
const gitSlug = await gitService.getProjectSlug();
|
|
79
|
+
projectSlug = (0, utils_1.generateProjectSlug)(projectRoot, true, gitSlug);
|
|
80
|
+
logger_1.logger.debug(`Git info: branch=${currentBranch}, commit=${currentCommit}, slug=${gitSlug}`);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
projectSlug = (0, utils_1.generateProjectSlug)(projectRoot, false);
|
|
84
|
+
}
|
|
85
|
+
// 检测解析模式
|
|
86
|
+
let mode = params.mode === 'full' ? 'full' : 'incremental';
|
|
87
|
+
if (params.mode === 'auto') {
|
|
88
|
+
const hasAnyResult = await storageService.hasAnyResult(projectSlug);
|
|
89
|
+
mode = hasAnyResult ? 'incremental' : 'full';
|
|
90
|
+
logger_1.logger.debug(`Auto mode detection: hasAnyResult=${hasAnyResult}, selected=${mode}`);
|
|
91
|
+
}
|
|
92
|
+
logger_1.logger.info(`Analysis mode: ${mode}`);
|
|
93
|
+
let runConfig;
|
|
94
|
+
try {
|
|
95
|
+
const { configManager } = await Promise.resolve().then(() => __importStar(require('../common/config')));
|
|
96
|
+
runConfig = configManager.getConfig();
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
const { configManager } = await Promise.resolve().then(() => __importStar(require('../common/config')));
|
|
100
|
+
runConfig = await configManager.load();
|
|
101
|
+
}
|
|
102
|
+
const blacklistService = new blacklist_service_1.BlacklistService();
|
|
103
|
+
await blacklistService.load(runConfig.analyze.blacklist, projectRoot);
|
|
104
|
+
const llmConfig = params.llmConfig;
|
|
105
|
+
const storageRoot = (0, utils_1.getStoragePath)(projectRoot, outputDir);
|
|
106
|
+
if (!params.noSkills) {
|
|
107
|
+
try {
|
|
108
|
+
const skillGenerator = new skill_generator_1.SkillGenerator();
|
|
109
|
+
const providers = (params.skillsProviders ?? runConfig.skills.default_providers);
|
|
110
|
+
await skillGenerator.generate({ projectRoot, storageRoot, providers });
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
114
|
+
logger_1.logger.warn(`Skill generation failed: ${msg}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const analysisService = new analysis_service_1.AnalysisService(gitService, storageService, blacklistService, projectSlug, currentCommit, llmConfig, params.onTokenUsageSnapshot);
|
|
118
|
+
const startTime = Date.now();
|
|
119
|
+
// ===================================================================
|
|
120
|
+
// 构建文件过滤器(全量 vs 增量唯一的差异点)
|
|
121
|
+
// ===================================================================
|
|
122
|
+
let fileFilter;
|
|
123
|
+
let commitHash = currentCommit;
|
|
124
|
+
if (mode === 'full') {
|
|
125
|
+
// 全量:所有文件均需解析
|
|
126
|
+
fileFilter = async () => true;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
// 增量:优先对比 commitId,不可用时 fallback 到内容 hash
|
|
130
|
+
const lastCommit = null;
|
|
131
|
+
logger_1.logger.debug(`Last analyzed commit: ${lastCommit || 'N/A'}`);
|
|
132
|
+
// Git 项目:使用 git diff 批量获取变更文件集合(等价于逐文件 commitId 对比的批量优化)
|
|
133
|
+
let gitChangedFiles = null;
|
|
134
|
+
if (isGit && lastCommit) {
|
|
135
|
+
try {
|
|
136
|
+
const diffFiles = await gitService.diffCommits(lastCommit, currentCommit);
|
|
137
|
+
logger_1.logger.debug(`git diff detected changed files: ${diffFiles.length}`);
|
|
138
|
+
gitChangedFiles = new Set(diffFiles);
|
|
139
|
+
// 将未提交变更也纳入候选集合
|
|
140
|
+
const uncommitted = await gitService.getUncommittedChanges();
|
|
141
|
+
if (uncommitted.length > 0) {
|
|
142
|
+
logger_1.logger.debug(`Incremental mode detected uncommitted changes: ${uncommitted.length}`);
|
|
143
|
+
for (const f of uncommitted)
|
|
144
|
+
gitChangedFiles.add(f);
|
|
145
|
+
}
|
|
146
|
+
logger_1.logger.debug(`Merged changed files count: ${gitChangedFiles.size}`);
|
|
147
|
+
}
|
|
148
|
+
catch (e) {
|
|
149
|
+
logger_1.logger.warn(`Commit diff failed; falling back to hash comparison: ${e.message}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// 非 Git 项目:需要逐文件 hash 对比,预先无法批量确定变更集
|
|
153
|
+
// 直接在 filter 内逐文件处理
|
|
154
|
+
fileFilter = async (relPath, absPath) => {
|
|
155
|
+
// 检查已有结果是否存在
|
|
156
|
+
const existing = await storageService.getFileAnalysis(projectSlug, relPath, 'summary');
|
|
157
|
+
if (!existing)
|
|
158
|
+
return true; // 结果缺失 → 需要解析
|
|
159
|
+
if (isGit) {
|
|
160
|
+
if (gitChangedFiles !== null) {
|
|
161
|
+
// 批量优化:git diff 隐式对比了 commitId
|
|
162
|
+
return gitChangedFiles.has(relPath);
|
|
163
|
+
}
|
|
164
|
+
// git diff 不可用,回退到逐文件 commitId 对比
|
|
165
|
+
const currentFileCommitId = await gitService.getFileLastCommit(projectRoot, relPath);
|
|
166
|
+
if (currentFileCommitId && existing.fileGitCommitId) {
|
|
167
|
+
return currentFileCommitId !== existing.fileGitCommitId;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// 非 Git 或 commitId 不可用:回退到内容 hash 对比
|
|
171
|
+
if (existing.fileHashWhenAnalyzed) {
|
|
172
|
+
const content = await fs.readFile(absPath, 'utf-8');
|
|
173
|
+
const currentHash = (0, crypto_1.createHash)('sha256').update(content).digest('hex');
|
|
174
|
+
return existing.fileHashWhenAnalyzed !== currentHash;
|
|
175
|
+
}
|
|
176
|
+
return true; // 无法判断 → 安全起见重新解析
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
// ===================================================================
|
|
180
|
+
// 调用统一解析管线
|
|
181
|
+
// ===================================================================
|
|
182
|
+
logger_1.logger.debug(`Analysis params: depth=${params.depth}, concurrency=${params.concurrency || constants_1.DEFAULT_CONCURRENCY}`);
|
|
183
|
+
const analysisResult = await analysisService.analyze({
|
|
184
|
+
projectRoot,
|
|
185
|
+
depth: params.depth,
|
|
186
|
+
concurrency: params.concurrency || constants_1.DEFAULT_CONCURRENCY,
|
|
187
|
+
mode,
|
|
188
|
+
commitHash,
|
|
189
|
+
fileFilter,
|
|
190
|
+
onTotalKnown: (total) => {
|
|
191
|
+
this.totalObjects = total;
|
|
192
|
+
params.onTotalKnown?.(total);
|
|
193
|
+
},
|
|
194
|
+
onObjectPlanned: obj => this.handleObjectPlanned(obj),
|
|
195
|
+
onObjectStarted: obj => this.handleObjectStarted(obj),
|
|
196
|
+
onObjectCompleted: (obj, meta) => this.handleObjectCompleted(obj, meta, params),
|
|
197
|
+
onScanProgress: params.onScanProgress,
|
|
198
|
+
});
|
|
199
|
+
const duration = Date.now() - startTime;
|
|
200
|
+
const summaryPath = analysisResult.summaryPath;
|
|
201
|
+
const tokenUsage = analysisService.getTokenUsage();
|
|
202
|
+
return {
|
|
203
|
+
success: analysisResult.success,
|
|
204
|
+
code: analysisResult.success ? errors_1.ErrorCode.SUCCESS : errors_1.ErrorCode.ANALYSIS_EXCEPTION,
|
|
205
|
+
message: analysisResult.success ? 'Analysis completed' : `Analysis completed with ${analysisResult.errors.length} error(s)`,
|
|
206
|
+
data: {
|
|
207
|
+
projectName: projectSlug,
|
|
208
|
+
mode,
|
|
209
|
+
analyzedFilesCount: analysisResult.analyzedFilesCount,
|
|
210
|
+
duration,
|
|
211
|
+
summaryPath,
|
|
212
|
+
tokenUsage,
|
|
213
|
+
},
|
|
214
|
+
errors: analysisResult.errors.length > 0 ? analysisResult.errors : undefined
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
handleObjectPlanned(_obj) {
|
|
218
|
+
// totalObjects 由 analyze 内部的 onTotalKnown 回调设置
|
|
219
|
+
}
|
|
220
|
+
handleObjectStarted(obj) {
|
|
221
|
+
if (!this.progressEnabled)
|
|
222
|
+
return;
|
|
223
|
+
const normalized = this.normalizeObjectPath(obj);
|
|
224
|
+
this.activeObjects.add(normalized);
|
|
225
|
+
this.emitProgressSnapshot(new Set(this.activeObjects), normalized);
|
|
226
|
+
}
|
|
227
|
+
handleObjectCompleted(obj, _meta, params) {
|
|
228
|
+
this.completedObjects++;
|
|
229
|
+
if (!this.progressEnabled) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const normalized = this.normalizeObjectPath(obj);
|
|
233
|
+
this.activeObjects.delete(normalized);
|
|
234
|
+
this.concurrency = params.concurrency || constants_1.DEFAULT_CONCURRENCY;
|
|
235
|
+
this.emitProgressSnapshot(new Set(this.activeObjects), normalized);
|
|
236
|
+
}
|
|
237
|
+
normalizeObjectPath(obj) {
|
|
238
|
+
const p = obj.path.replace(/\\/g, '/');
|
|
239
|
+
if (obj.type === 'directory') {
|
|
240
|
+
if (p === '.')
|
|
241
|
+
return './';
|
|
242
|
+
return p.endsWith('/') ? p : `${p}/`;
|
|
243
|
+
}
|
|
244
|
+
return p;
|
|
245
|
+
}
|
|
246
|
+
emitProgressSnapshot(snapshot, fallbackNormalized) {
|
|
247
|
+
if (!this.onProgress)
|
|
248
|
+
return;
|
|
249
|
+
const activePaths = Array.from(snapshot)
|
|
250
|
+
.map(p => p.replace(/\\/g, '/'))
|
|
251
|
+
.sort();
|
|
252
|
+
const topN = activePaths.slice(0, this.concurrency);
|
|
253
|
+
const displayLines = topN;
|
|
254
|
+
const key = displayLines.join('\n');
|
|
255
|
+
if (key === this.lastRenderedCurrentKey) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
this.lastRenderedCurrentKey = key;
|
|
259
|
+
this.onProgress(this.completedObjects, this.totalObjects, {
|
|
260
|
+
path: key,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
exports.AnalysisAppService = AnalysisAppService;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createAppServices = createAppServices;
|
|
4
|
+
const analysis_app_service_1 = require("./analysis.app.service");
|
|
5
|
+
const git_service_1 = require("../infrastructure/git.service");
|
|
6
|
+
const storage_service_1 = require("../infrastructure/storage.service");
|
|
7
|
+
const incremental_service_1 = require("../domain/services/incremental.service");
|
|
8
|
+
function createAppServices(projectRoot, llmConfig, onTokenUsageSnapshot) {
|
|
9
|
+
const root = projectRoot || process.cwd();
|
|
10
|
+
const gitService = new git_service_1.GitService(root);
|
|
11
|
+
const storageService = new storage_service_1.LocalStorageService(root);
|
|
12
|
+
const incrementalService = new incremental_service_1.IncrementalService(gitService, storageService);
|
|
13
|
+
const analysisAppService = new analysis_app_service_1.AnalysisAppService();
|
|
14
|
+
return {
|
|
15
|
+
analysisAppService,
|
|
16
|
+
analysisService: {},
|
|
17
|
+
incrementalService,
|
|
18
|
+
gitService,
|
|
19
|
+
storageService,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.LLMAnalysisService = void 0;
|
|
7
|
+
const prompt_template_1 = require("../../infrastructure/llm/prompt.template");
|
|
8
|
+
const mustache_1 = __importDefault(require("mustache"));
|
|
9
|
+
const errors_1 = require("../../common/errors");
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
/** 从 LLM 返回中解析单字段:支持 {"key": "value"} 或纯字符串 */
|
|
12
|
+
function parseSingleField(content, field) {
|
|
13
|
+
const trimmed = content.trim();
|
|
14
|
+
try {
|
|
15
|
+
const o = JSON.parse(trimmed);
|
|
16
|
+
if (o && typeof o[field] === 'string')
|
|
17
|
+
return o[field];
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// 非 JSON 则整体视为该字段内容
|
|
21
|
+
}
|
|
22
|
+
return trimmed || '';
|
|
23
|
+
}
|
|
24
|
+
class LLMAnalysisService {
|
|
25
|
+
llmClient;
|
|
26
|
+
fileSplitter;
|
|
27
|
+
cache;
|
|
28
|
+
config;
|
|
29
|
+
constructor(llmClient, fileSplitter, cache, config) {
|
|
30
|
+
this.llmClient = llmClient;
|
|
31
|
+
this.fileSplitter = fileSplitter;
|
|
32
|
+
this.cache = cache;
|
|
33
|
+
this.config = config;
|
|
34
|
+
}
|
|
35
|
+
async analyzeFile(filePath, fileContent, fileHash) {
|
|
36
|
+
// 先查缓存
|
|
37
|
+
if (this.config.cache_enabled) {
|
|
38
|
+
const cachedResult = await this.cache.get(fileHash);
|
|
39
|
+
if (cachedResult) {
|
|
40
|
+
cachedResult.path = filePath;
|
|
41
|
+
cachedResult.lastAnalyzedAt = new Date().toISOString();
|
|
42
|
+
return cachedResult;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
let result;
|
|
46
|
+
// 检查文件大小是否超过上下文窗口
|
|
47
|
+
if (fileContent.length > this.config.context_window_size * 0.8) {
|
|
48
|
+
// 大文件分片解析
|
|
49
|
+
result = await this.analyzeLargeFile(filePath, fileContent);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
// 小文件直接解析
|
|
53
|
+
result = await this.analyzeSmallFile(filePath, fileContent);
|
|
54
|
+
}
|
|
55
|
+
// 保存缓存
|
|
56
|
+
if (this.config.cache_enabled) {
|
|
57
|
+
await this.cache.set(fileHash, result);
|
|
58
|
+
}
|
|
59
|
+
result.path = filePath;
|
|
60
|
+
result.lastAnalyzedAt = new Date().toISOString();
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
/** 三步协议(需求 10.5.3 / 10.9.1):结构 → 功能描述 → 概述,程序组装为完整 FileAnalysis;某次解析失败仅重试当次(10.9.2)。 */
|
|
64
|
+
async analyzeSmallFile(filePath, fileContent) {
|
|
65
|
+
const opts = { temperature: 0.1 };
|
|
66
|
+
// 第一步:仅提取结构(classes / globalVariables / globalFunctions),不包含基础信息
|
|
67
|
+
const structure = await this.callWithParseRetry(mustache_1.default.render(prompt_template_1.FILE_STRUCTURE_PROMPT, { filePath, fileContent }), opts, (content) => {
|
|
68
|
+
const o = JSON.parse(content);
|
|
69
|
+
return {
|
|
70
|
+
classes: Array.isArray(o.classes) ? o.classes : [],
|
|
71
|
+
functions: Array.isArray(o.functions) ? o.functions : []
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
const structureJson = JSON.stringify(structure, null, 2);
|
|
75
|
+
// 第二步:仅生成功能描述
|
|
76
|
+
const description = await this.callWithParseRetry(mustache_1.default.render(prompt_template_1.FILE_DESCRIPTION_PROMPT, { structureJson }), opts, (content) => parseSingleField(content, 'description'));
|
|
77
|
+
// 第三步:仅生成概述
|
|
78
|
+
const summary = await this.callWithParseRetry(mustache_1.default.render(prompt_template_1.FILE_SUMMARY_PROMPT, { structureJson, description }), opts, (content) => parseSingleField(content, 'summary'));
|
|
79
|
+
// 基础信息由程序侧负责(设计文档 13.2.2)
|
|
80
|
+
const name = path_1.default.basename(filePath);
|
|
81
|
+
const language = this.detectLanguage(filePath);
|
|
82
|
+
const linesOfCode = fileContent.split(/\r?\n/).length;
|
|
83
|
+
return {
|
|
84
|
+
type: 'file',
|
|
85
|
+
path: filePath,
|
|
86
|
+
name,
|
|
87
|
+
language,
|
|
88
|
+
linesOfCode,
|
|
89
|
+
dependencies: [],
|
|
90
|
+
description,
|
|
91
|
+
summary,
|
|
92
|
+
classes: structure.classes,
|
|
93
|
+
functions: structure.functions,
|
|
94
|
+
lastAnalyzedAt: new Date().toISOString(),
|
|
95
|
+
commitHash: ''
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
detectLanguage(filePath) {
|
|
99
|
+
const ext = path_1.default.extname(filePath).toLowerCase();
|
|
100
|
+
switch (ext) {
|
|
101
|
+
case '.ts':
|
|
102
|
+
case '.tsx':
|
|
103
|
+
return 'TypeScript';
|
|
104
|
+
case '.js':
|
|
105
|
+
case '.jsx':
|
|
106
|
+
return 'JavaScript';
|
|
107
|
+
case '.py':
|
|
108
|
+
return 'Python';
|
|
109
|
+
case '.java':
|
|
110
|
+
return 'Java';
|
|
111
|
+
case '.go':
|
|
112
|
+
return 'Go';
|
|
113
|
+
case '.cs':
|
|
114
|
+
return 'C#';
|
|
115
|
+
default:
|
|
116
|
+
return '';
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/** 单次调用:解析失败则仅重试该次一次,不重做已成功步骤(需求 10.9.2)。 */
|
|
120
|
+
async callWithParseRetry(prompt, options, parseFn) {
|
|
121
|
+
let lastError;
|
|
122
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
123
|
+
try {
|
|
124
|
+
const response = await this.llmClient.call(attempt === 1 ? prompt + prompt_template_1.PARSE_RETRY_HINT : prompt, { ...options, retries: 0 });
|
|
125
|
+
return parseFn(response.content);
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
lastError = e;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
throw new errors_1.AppError(errors_1.ErrorCode.LLM_RESPONSE_PARSE_FAILED, `Failed to parse LLM response after retry: ${lastError?.message}`, lastError);
|
|
132
|
+
}
|
|
133
|
+
async analyzeLargeFile(filePath, fileContent) {
|
|
134
|
+
// 分片
|
|
135
|
+
const chunks = await this.fileSplitter.split(fileContent, this.config.context_window_size * 0.7);
|
|
136
|
+
// 并行解析所有分片(仅提取结构)
|
|
137
|
+
const chunkAnalysisPromises = chunks.map(async (chunk) => {
|
|
138
|
+
const prompt = mustache_1.default.render(prompt_template_1.CHUNK_ANALYSIS_PROMPT, {
|
|
139
|
+
filePath,
|
|
140
|
+
chunkId: chunk.id,
|
|
141
|
+
chunkContent: chunk.content,
|
|
142
|
+
context: chunk.context || ''
|
|
143
|
+
});
|
|
144
|
+
const response = await this.llmClient.call(prompt, { temperature: 0.1 });
|
|
145
|
+
const parsed = JSON.parse(response.content);
|
|
146
|
+
return {
|
|
147
|
+
chunkId: chunk.id,
|
|
148
|
+
classes: Array.isArray(parsed.classes) ? parsed.classes : [],
|
|
149
|
+
functions: Array.isArray(parsed.functions) ? parsed.functions : []
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
const chunkResults = await Promise.all(chunkAnalysisPromises);
|
|
153
|
+
// 合并分片结果
|
|
154
|
+
return this.fileSplitter.merge(chunkResults, filePath);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* 目录两步协议(需求 10.6.3 / 10.9.3):基于子项精简信息先生成 description,再生成 summary。
|
|
158
|
+
*/
|
|
159
|
+
async analyzeDirectory(childrenDirs, childrenFiles) {
|
|
160
|
+
const opts = { temperature: 0.1 };
|
|
161
|
+
const payload = { childrenDirs, childrenFiles };
|
|
162
|
+
const childrenJson = JSON.stringify(payload, null, 2);
|
|
163
|
+
// 第一步:功能描述
|
|
164
|
+
const description = await this.callWithParseRetry(mustache_1.default.render(prompt_template_1.DIRECTORY_DESCRIPTION_PROMPT, { childrenJson }), opts, (content) => parseSingleField(content, 'description'));
|
|
165
|
+
// 第二步:概述
|
|
166
|
+
const summary = await this.callWithParseRetry(mustache_1.default.render(prompt_template_1.DIRECTORY_SUMMARY_PROMPT, { description, childrenJson }), opts, (content) => parseSingleField(content, 'summary'));
|
|
167
|
+
return { description, summary };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
exports.LLMAnalysisService = LLMAnalysisService;
|