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