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
|
@@ -1,413 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs-extra'
|
|
2
|
-
import * as path from 'path'
|
|
3
|
-
import { createHash } from 'crypto'
|
|
4
|
-
import { IStorageService } from '../domain/interfaces'
|
|
5
|
-
import { FileAnalysis, DirectoryAnalysis, AnalysisCheckpoint } from '../common/types'
|
|
6
|
-
import { AppError, ErrorCode } from '../common/errors'
|
|
7
|
-
import { getStoragePath, getFileOutputPath, getDirOutputPath } from '../common/utils'
|
|
8
|
-
|
|
9
|
-
export class LocalStorageService implements IStorageService {
|
|
10
|
-
private projectRoot: string;
|
|
11
|
-
private customOutputDir?: string;
|
|
12
|
-
|
|
13
|
-
constructor(projectRoot: string = process.cwd(), customOutputDir?: string) {
|
|
14
|
-
this.projectRoot = projectRoot;
|
|
15
|
-
this.customOutputDir = customOutputDir;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
private getStorageRoot(): string {
|
|
19
|
-
return getStoragePath(this.projectRoot, this.customOutputDir);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
private normalizeNewlines(input: string): string {
|
|
23
|
-
return input.replace(/\r\n/g, '\n')
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
private extractSection(markdown: string, title: string): string {
|
|
27
|
-
const md = this.normalizeNewlines(markdown)
|
|
28
|
-
const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
29
|
-
const re = new RegExp(`\\n##\\s+${escaped}\\n([\\s\\S]*?)(?=\\n##\\s+|\\n#\\s+|$)`, 'm')
|
|
30
|
-
const m = md.match(re)
|
|
31
|
-
return (m?.[1] ?? '').trim()
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
private extractSectionAny(markdown: string, titles: string[]): string {
|
|
35
|
-
for (const t of titles) {
|
|
36
|
-
const v = this.extractSection(markdown, t)
|
|
37
|
-
if (v) return v
|
|
38
|
-
}
|
|
39
|
-
return ''
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
private parseBasicInfo(markdown: string): Record<string, string> {
|
|
43
|
-
const basic = this.extractSectionAny(markdown, ['Basic Information', '基本信息'])
|
|
44
|
-
const lines = basic.split('\n').map(l => l.trim()).filter(Boolean)
|
|
45
|
-
const map: Record<string, string> = {}
|
|
46
|
-
for (const line of lines) {
|
|
47
|
-
const cleaned = line.replace(/^-+\s*/, '').trim()
|
|
48
|
-
const idx = cleaned.indexOf(':') >= 0 ? cleaned.indexOf(':') : cleaned.indexOf(':')
|
|
49
|
-
if (idx <= 0) continue
|
|
50
|
-
const k = cleaned.slice(0, idx).trim()
|
|
51
|
-
const v = cleaned.slice(idx + 1).trim()
|
|
52
|
-
if (k) map[k] = v
|
|
53
|
-
}
|
|
54
|
-
return map
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
private getBasicValue(basic: Record<string, string>, keys: string[]): string | undefined {
|
|
58
|
-
for (const k of keys) {
|
|
59
|
-
const v = basic[k]
|
|
60
|
-
if (v !== undefined) return v
|
|
61
|
-
}
|
|
62
|
-
return undefined
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
private parseFileMarkdownToAnalysis(markdown: string, filePath: string): FileAnalysis | null {
|
|
66
|
-
const md = this.normalizeNewlines(markdown)
|
|
67
|
-
const firstLine = md.split('\n')[0]?.trim() ?? ''
|
|
68
|
-
const name = firstLine.startsWith('# ') ? firstLine.slice(2).trim() : path.basename(filePath)
|
|
69
|
-
const basic = this.parseBasicInfo(md)
|
|
70
|
-
const summary = this.extractSectionAny(md, ['Summary', '概述'])
|
|
71
|
-
const description = this.extractSectionAny(md, ['Description', '功能描述'])
|
|
72
|
-
|
|
73
|
-
const language = this.getBasicValue(basic, ['Language', '语言']) ?? 'unknown'
|
|
74
|
-
const loc = Number(this.getBasicValue(basic, ['Lines of Code', '代码行数']) ?? NaN)
|
|
75
|
-
const lastAnalyzedAt = this.getBasicValue(basic, ['Last Analyzed At', '最后解析时间']) ?? new Date(0).toISOString()
|
|
76
|
-
const fileGitCommitId = this.getBasicValue(basic, ['file_git_commit_id'])
|
|
77
|
-
const isDirtyWhenAnalyzedRaw = this.getBasicValue(basic, ['is_dirty_when_analyzed'])
|
|
78
|
-
const fileHashWhenAnalyzed = this.getBasicValue(basic, ['file_hash_when_analyzed'])
|
|
79
|
-
|
|
80
|
-
const isDirtyWhenAnalyzed =
|
|
81
|
-
isDirtyWhenAnalyzedRaw === undefined
|
|
82
|
-
? undefined
|
|
83
|
-
: ['true', '1', 'yes'].includes(isDirtyWhenAnalyzedRaw.toLowerCase())
|
|
84
|
-
|
|
85
|
-
return {
|
|
86
|
-
type: 'file',
|
|
87
|
-
path: filePath,
|
|
88
|
-
name,
|
|
89
|
-
language,
|
|
90
|
-
linesOfCode: Number.isFinite(loc) ? loc : 0,
|
|
91
|
-
dependencies: [],
|
|
92
|
-
fileGitCommitId: fileGitCommitId && fileGitCommitId !== 'N/A' ? fileGitCommitId : undefined,
|
|
93
|
-
isDirtyWhenAnalyzed,
|
|
94
|
-
fileHashWhenAnalyzed: fileHashWhenAnalyzed || undefined,
|
|
95
|
-
description: description || undefined,
|
|
96
|
-
summary: summary || '',
|
|
97
|
-
classes: [],
|
|
98
|
-
functions: [],
|
|
99
|
-
lastAnalyzedAt,
|
|
100
|
-
commitHash: '',
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
private parseDirectoryMarkdownToAnalysis(markdown: string, dirPath: string): DirectoryAnalysis | null {
|
|
105
|
-
const md = this.normalizeNewlines(markdown)
|
|
106
|
-
const firstLine = md.split('\n')[0]?.trim() ?? ''
|
|
107
|
-
const rawName = firstLine.startsWith('# ') ? firstLine.slice(2).trim() : path.basename(dirPath)
|
|
108
|
-
const name = rawName.replace(/\s*(Directory|目录)\s*$/, '').trim() || path.basename(dirPath)
|
|
109
|
-
const summary = this.extractSectionAny(md, ['Summary', '概述'])
|
|
110
|
-
const description = this.extractSectionAny(md, ['Description', '功能描述']) || summary
|
|
111
|
-
const basic = this.parseBasicInfo(md)
|
|
112
|
-
const lastAnalyzedAt = this.getBasicValue(basic, ['Last Analyzed At', '最后解析时间']) ?? new Date(0).toISOString()
|
|
113
|
-
return {
|
|
114
|
-
type: 'directory',
|
|
115
|
-
path: dirPath,
|
|
116
|
-
name,
|
|
117
|
-
description: description || '',
|
|
118
|
-
summary: summary || '',
|
|
119
|
-
childrenDirsCount: 0,
|
|
120
|
-
childrenFilesCount: 0,
|
|
121
|
-
structure: [],
|
|
122
|
-
lastAnalyzedAt,
|
|
123
|
-
commitHash: '',
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* 仅更新结果 Markdown 中「基本信息」段的部分字段,避免依赖任何内部 JSON 状态。
|
|
129
|
-
*/
|
|
130
|
-
private async updateFileMarkdownBasicInfo(
|
|
131
|
-
outputPath: string,
|
|
132
|
-
updates: Partial<Pick<FileAnalysis, 'fileGitCommitId' | 'isDirtyWhenAnalyzed' | 'fileHashWhenAnalyzed' | 'lastAnalyzedAt'>>,
|
|
133
|
-
): Promise<void> {
|
|
134
|
-
const raw = await fs.readFile(outputPath, 'utf-8')
|
|
135
|
-
const md = this.normalizeNewlines(raw)
|
|
136
|
-
const basic = this.extractSectionAny(md, ['Basic Information', '基本信息'])
|
|
137
|
-
if (!basic) {
|
|
138
|
-
// 如果没有“基本信息”段,直接回退为重写全文件(风险较大);这里选择保守:不改动
|
|
139
|
-
return
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const lines = basic.split('\n')
|
|
143
|
-
const patchKV = (key: string, value: string) => {
|
|
144
|
-
const re = new RegExp(`^\\s*-\\s*${key}\\s*[::].*$`, 'm')
|
|
145
|
-
const replacement = `- ${key}: ${value}`
|
|
146
|
-
const joined = lines.join('\n')
|
|
147
|
-
if (re.test(joined)) {
|
|
148
|
-
const next = joined.replace(re, replacement)
|
|
149
|
-
lines.splice(0, lines.length, ...next.split('\n'))
|
|
150
|
-
} else {
|
|
151
|
-
lines.push(replacement)
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (updates.lastAnalyzedAt) {
|
|
156
|
-
patchKV('Last Analyzed At', updates.lastAnalyzedAt)
|
|
157
|
-
patchKV('最后解析时间', updates.lastAnalyzedAt) // backward-compat for old markdown
|
|
158
|
-
}
|
|
159
|
-
if (updates.fileGitCommitId !== undefined) patchKV('file_git_commit_id', updates.fileGitCommitId || 'N/A')
|
|
160
|
-
if (updates.isDirtyWhenAnalyzed !== undefined) patchKV('is_dirty_when_analyzed', String(!!updates.isDirtyWhenAnalyzed))
|
|
161
|
-
if (updates.fileHashWhenAnalyzed !== undefined) patchKV('file_hash_when_analyzed', updates.fileHashWhenAnalyzed || '')
|
|
162
|
-
|
|
163
|
-
const newBasic = lines.join('\n').trim() + '\n'
|
|
164
|
-
const escapedEn = 'Basic Information'.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
165
|
-
const escapedZh = '基本信息'.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
166
|
-
const reSection = new RegExp(`(\\n##\\s+(?:${escapedEn}|${escapedZh})\\n)([\\s\\S]*?)(?=\\n##\\s+|\\n#\\s+|$)`, 'm')
|
|
167
|
-
const nextMd = md.replace(reSection, `$1${newBasic}`)
|
|
168
|
-
await fs.writeFile(outputPath, nextMd, 'utf-8')
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
async saveFileAnalysis(projectSlug: string, filePath: string, data: FileAnalysis): Promise<void> {
|
|
172
|
-
try {
|
|
173
|
-
const storageRoot = this.getStorageRoot()
|
|
174
|
-
const outputPath = getFileOutputPath(storageRoot, filePath)
|
|
175
|
-
await fs.ensureDir(path.dirname(outputPath))
|
|
176
|
-
|
|
177
|
-
const relativePath = path.relative(this.projectRoot, filePath) || data.path
|
|
178
|
-
const fileGitCommitId = data.fileGitCommitId ?? 'N/A'
|
|
179
|
-
const isDirty = data.isDirtyWhenAnalyzed ?? false
|
|
180
|
-
let fileHash = data.fileHashWhenAnalyzed ?? ''
|
|
181
|
-
// 防御:某些路径/worker 回传异常场景下 fileHashWhenAnalyzed 可能丢失。
|
|
182
|
-
// 这里尝试根据源码文件内容补齐,保证基本信息段可用于增量与回归测试。
|
|
183
|
-
if (!fileHash) {
|
|
184
|
-
try {
|
|
185
|
-
const abs =
|
|
186
|
-
path.isAbsolute(filePath) ? filePath : path.resolve(this.projectRoot, filePath)
|
|
187
|
-
if (await fs.pathExists(abs)) {
|
|
188
|
-
const content = await fs.readFile(abs, 'utf-8')
|
|
189
|
-
fileHash = createHash('sha256').update(content).digest('hex')
|
|
190
|
-
}
|
|
191
|
-
} catch {
|
|
192
|
-
// ignore:保持空字符串写入,避免影响主流程
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
let content = `# ${data.name}\n\n`
|
|
197
|
-
|
|
198
|
-
// 基本信息段(设计文档第 13.2.4)
|
|
199
|
-
content += '## Basic Information\n'
|
|
200
|
-
content += `- Path: ${relativePath}\n`
|
|
201
|
-
content += `- Language: ${data.language}\n`
|
|
202
|
-
content += `- Lines of Code: ${data.linesOfCode}\n`
|
|
203
|
-
content += `- Last Analyzed At: ${data.lastAnalyzedAt}\n`
|
|
204
|
-
content += `- file_git_commit_id: ${fileGitCommitId}\n`
|
|
205
|
-
content += `- is_dirty_when_analyzed: ${isDirty}\n`
|
|
206
|
-
content += `- file_hash_when_analyzed: ${fileHash}\n\n`
|
|
207
|
-
|
|
208
|
-
// 概述与功能描述
|
|
209
|
-
const summary = data.summary || ''
|
|
210
|
-
const description = data.description || ''
|
|
211
|
-
content += `## Summary\n${summary}\n\n`
|
|
212
|
-
content += `## Description\n${description || summary}\n\n`
|
|
213
|
-
|
|
214
|
-
// 类定义
|
|
215
|
-
if (data.classes.length > 0) {
|
|
216
|
-
content += '## Classes\n'
|
|
217
|
-
for (const cls of data.classes) {
|
|
218
|
-
content += `### ${cls.name}\n`
|
|
219
|
-
if (cls.extends) content += `- Extends: ${cls.extends}\n`
|
|
220
|
-
if (cls.implements && cls.implements.length > 0) content += `- Implements: ${cls.implements.join(', ')}\n`
|
|
221
|
-
|
|
222
|
-
if (cls.properties.length > 0) {
|
|
223
|
-
content += `- Properties:\n`
|
|
224
|
-
for (const prop of cls.properties) {
|
|
225
|
-
content += ` - ${prop.visibility} ${prop.name}: ${prop.type} - ${prop.description}\n`
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
if (cls.methods.length > 0) {
|
|
230
|
-
content += `- Methods:\n`
|
|
231
|
-
for (const method of cls.methods) {
|
|
232
|
-
content += ` - ${method.visibility} ${method.signature} - ${method.description}\n`
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
content += '\n'
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// 全局函数
|
|
241
|
-
if (data.functions.length > 0) {
|
|
242
|
-
content += '## Global Functions\n'
|
|
243
|
-
for (const func of data.functions) {
|
|
244
|
-
content += `- ${func.signature} - ${func.description}\n`
|
|
245
|
-
}
|
|
246
|
-
content += '\n'
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
await fs.writeFile(outputPath, content, 'utf-8')
|
|
250
|
-
} catch (e) {
|
|
251
|
-
throw new AppError(ErrorCode.STORAGE_WRITE_FAILED, 'Failed to save file analysis result', e)
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
async saveDirectoryAnalysis(projectSlug: string, dirPath: string, data: DirectoryAnalysis): Promise<void> {
|
|
256
|
-
try {
|
|
257
|
-
const storageRoot = this.getStorageRoot()
|
|
258
|
-
const outputPath = getDirOutputPath(storageRoot, dirPath)
|
|
259
|
-
await fs.ensureDir(path.dirname(outputPath))
|
|
260
|
-
|
|
261
|
-
const relativePath = path.relative(this.projectRoot, dirPath) || data.path
|
|
262
|
-
const childrenDirs = data.structure.filter(item => item.type === 'directory')
|
|
263
|
-
const childrenFiles = data.structure.filter(item => item.type === 'file')
|
|
264
|
-
|
|
265
|
-
let content = `# ${data.name} Directory\n\n`
|
|
266
|
-
|
|
267
|
-
// 基本信息
|
|
268
|
-
content += '## Basic Information\n'
|
|
269
|
-
content += `- Path: ${relativePath}\n`
|
|
270
|
-
content += `- Child Directories: ${childrenDirs.length}\n`
|
|
271
|
-
content += `- Child Files: ${childrenFiles.length}\n`
|
|
272
|
-
content += `- Last Analyzed At: ${data.lastAnalyzedAt}\n\n`
|
|
273
|
-
|
|
274
|
-
let description = (data as any).description ?? data.summary
|
|
275
|
-
const summary = data.summary
|
|
276
|
-
|
|
277
|
-
// If LLM description is too short or purely statistical, add a deterministic English fallback.
|
|
278
|
-
const asciiWordCount = (description || '').trim().split(/\s+/).filter(Boolean).length
|
|
279
|
-
const looksLikeStatOnly = /\b\d+\b/.test(description || '') && /(files?|directories?)/i.test(description || '')
|
|
280
|
-
if (!description || asciiWordCount < 12 || looksLikeStatOnly) {
|
|
281
|
-
const fileNames = childrenFiles.map(f => f.name).join(', ')
|
|
282
|
-
const dirNames = childrenDirs.map(d => d.name).join(', ')
|
|
283
|
-
const extraKeywords = relativePath.includes('SenseVoice')
|
|
284
|
-
? ' It also appears to contain SenseVoice examples/demos.'
|
|
285
|
-
: ''
|
|
286
|
-
description =
|
|
287
|
-
`The "${data.name}" directory at "${relativePath || '.'}" contains ${childrenFiles.length} file(s) and ${childrenDirs.length} subdirectory(ies)` +
|
|
288
|
-
(fileNames ? ` (e.g., ${fileNames})` : '') +
|
|
289
|
-
(dirNames ? ` and subdirectories such as: ${dirNames}` : '') +
|
|
290
|
-
`. It organizes source code and related artifacts for this area of the project.${extraKeywords}`
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
content += `## Description\n${description}\n\n`
|
|
294
|
-
content += `## Summary\n${summary}\n\n`
|
|
295
|
-
|
|
296
|
-
if (childrenDirs.length > 0) {
|
|
297
|
-
content += '## Subdirectories\n'
|
|
298
|
-
for (const item of childrenDirs) {
|
|
299
|
-
content += `- ${item.name}: ${item.description}\n`
|
|
300
|
-
}
|
|
301
|
-
content += '\n'
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
if (childrenFiles.length > 0) {
|
|
305
|
-
content += '## Files\n'
|
|
306
|
-
for (const item of childrenFiles) {
|
|
307
|
-
content += `- ${item.name}: ${item.description}\n`
|
|
308
|
-
}
|
|
309
|
-
content += '\n'
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
await fs.writeFile(outputPath, content, 'utf-8')
|
|
313
|
-
} catch (e) {
|
|
314
|
-
throw new AppError(ErrorCode.STORAGE_WRITE_FAILED, 'Failed to save directory analysis result', e)
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
async getFileAnalysis(projectSlug: string, filePath: string, type: 'summary' | 'full' | 'diagram'): Promise<FileAnalysis | null> {
|
|
319
|
-
try {
|
|
320
|
-
const storageRoot = this.getStorageRoot()
|
|
321
|
-
const mdPath = getFileOutputPath(storageRoot, filePath)
|
|
322
|
-
if (!(await fs.pathExists(mdPath))) {
|
|
323
|
-
return null
|
|
324
|
-
}
|
|
325
|
-
const markdown = await fs.readFile(mdPath, 'utf-8')
|
|
326
|
-
const parsed = this.parseFileMarkdownToAnalysis(markdown, filePath)
|
|
327
|
-
return parsed
|
|
328
|
-
} catch {
|
|
329
|
-
return null
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
async getDirectoryAnalysis(projectSlug: string, dirPath: string, type: 'summary' | 'full' | 'diagram'): Promise<DirectoryAnalysis | null> {
|
|
334
|
-
try {
|
|
335
|
-
const storageRoot = this.getStorageRoot()
|
|
336
|
-
const mdPath = getDirOutputPath(storageRoot, dirPath)
|
|
337
|
-
if (!(await fs.pathExists(mdPath))) {
|
|
338
|
-
return null
|
|
339
|
-
}
|
|
340
|
-
const markdown = await fs.readFile(mdPath, 'utf-8')
|
|
341
|
-
const parsed = this.parseDirectoryMarkdownToAnalysis(markdown, dirPath)
|
|
342
|
-
return parsed
|
|
343
|
-
} catch {
|
|
344
|
-
return null
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
/**
|
|
349
|
-
* 增量模式 meta-only:仅在已有 Markdown 结果上更新基础字段,不依赖内部 JSON。
|
|
350
|
-
*/
|
|
351
|
-
async patchFileResultMarkdown(
|
|
352
|
-
filePath: string,
|
|
353
|
-
updates: Partial<Pick<FileAnalysis, 'fileGitCommitId' | 'isDirtyWhenAnalyzed' | 'fileHashWhenAnalyzed' | 'lastAnalyzedAt'>>,
|
|
354
|
-
): Promise<void> {
|
|
355
|
-
const storageRoot = this.getStorageRoot()
|
|
356
|
-
const outputPath = getFileOutputPath(storageRoot, filePath)
|
|
357
|
-
if (!(await fs.pathExists(outputPath))) {
|
|
358
|
-
return
|
|
359
|
-
}
|
|
360
|
-
await this.updateFileMarkdownBasicInfo(outputPath, updates)
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
/**
|
|
364
|
-
* 判断当前存储目录下是否已经存在任意解析结果。
|
|
365
|
-
* 规则:
|
|
366
|
-
* - 在存储根目录下递归查找任意 .md 结果文件(如 index.md 或文件级解析结果)。
|
|
367
|
-
*/
|
|
368
|
-
async hasAnyResult(projectSlug: string): Promise<boolean> {
|
|
369
|
-
try {
|
|
370
|
-
const storageRoot = this.getStorageRoot()
|
|
371
|
-
if (!(await fs.pathExists(storageRoot))) {
|
|
372
|
-
return false
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
const entries = await fs.readdir(storageRoot, { withFileTypes: true })
|
|
376
|
-
const stack: string[] = []
|
|
377
|
-
for (const entry of entries) {
|
|
378
|
-
const full = path.join(storageRoot, entry.name)
|
|
379
|
-
stack.push(full)
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
while (stack.length > 0) {
|
|
383
|
-
const current = stack.pop() as string
|
|
384
|
-
const stat = await fs.stat(current)
|
|
385
|
-
if (stat.isDirectory()) {
|
|
386
|
-
const children = await fs.readdir(current, { withFileTypes: true })
|
|
387
|
-
for (const child of children) {
|
|
388
|
-
stack.push(path.join(current, child.name))
|
|
389
|
-
}
|
|
390
|
-
} else if (stat.isFile() && current.toLowerCase().endsWith('.md')) {
|
|
391
|
-
return true
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
return false
|
|
396
|
-
} catch {
|
|
397
|
-
return false
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
async getCheckpoint(projectSlug: string): Promise<AnalysisCheckpoint | null> {
|
|
402
|
-
// TODO: 实现断点查询逻辑
|
|
403
|
-
return null
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
async saveCheckpoint(projectSlug: string, checkpoint: AnalysisCheckpoint): Promise<void> {
|
|
407
|
-
// TODO: 实现断点保存逻辑
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
getStoragePath(projectSlug: string): string {
|
|
411
|
-
return this.getStorageRoot()
|
|
412
|
-
}
|
|
413
|
-
}
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
import * as path from 'path'
|
|
2
|
-
import { DirectoryAnalysis, FileAnalysis, LLMConfig, ModificationLog } from '../../common/types'
|
|
3
|
-
import { OpenAIClient } from '../llm/openai.client'
|
|
4
|
-
import { LLMUsageTracker } from '../llm/llm.usage.tracker'
|
|
5
|
-
import { CodeSplitter } from '../splitter/code.splitter'
|
|
6
|
-
import { FileHashCache } from '../cache/file.hash.cache'
|
|
7
|
-
import { LLMAnalysisService } from '../../application/services/llm.analysis.service'
|
|
8
|
-
import * as crypto from 'crypto'
|
|
9
|
-
|
|
10
|
-
export type WorkerUsageDelta = {
|
|
11
|
-
totalPromptTokens: number
|
|
12
|
-
totalCompletionTokens: number
|
|
13
|
-
totalTokens: number
|
|
14
|
-
totalCalls: number
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export async function parseFile(
|
|
18
|
-
filePath: string,
|
|
19
|
-
fileContent: string,
|
|
20
|
-
fileHash: string,
|
|
21
|
-
language?: string,
|
|
22
|
-
llmConfig?: LLMConfig
|
|
23
|
-
): Promise<{ analysis: FileAnalysis; usage: WorkerUsageDelta }> {
|
|
24
|
-
if (!llmConfig) {
|
|
25
|
-
throw new Error('LLM config is required for file parsing')
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// 初始化LLM相关服务
|
|
29
|
-
const tracker = new LLMUsageTracker()
|
|
30
|
-
const llmClient = new OpenAIClient(llmConfig, tracker)
|
|
31
|
-
const fileSplitter = new CodeSplitter(llmClient)
|
|
32
|
-
const cache = new FileHashCache({
|
|
33
|
-
cacheDir: llmConfig.cache_dir,
|
|
34
|
-
maxSizeMb: llmConfig.cache_max_size_mb,
|
|
35
|
-
})
|
|
36
|
-
const llmAnalysisService = new LLMAnalysisService(llmClient, fileSplitter, cache, llmConfig)
|
|
37
|
-
|
|
38
|
-
const result = await llmAnalysisService.analyzeFile(filePath, fileContent, fileHash)
|
|
39
|
-
return { analysis: result, usage: tracker.getStats() }
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export async function aggregateDirectory(
|
|
43
|
-
dirPath: string,
|
|
44
|
-
payload: {
|
|
45
|
-
childrenDirs: Array<{ name: string; summary: string; description?: string }>
|
|
46
|
-
childrenFiles: Array<{ name: string; summary: string; description?: string }>
|
|
47
|
-
},
|
|
48
|
-
llmConfig?: LLMConfig
|
|
49
|
-
): Promise<{ description: string; summary: string; usage: WorkerUsageDelta }> {
|
|
50
|
-
const name = path.basename(dirPath)
|
|
51
|
-
const childrenDirsPayload = (payload?.childrenDirs ?? []).map(d => ({
|
|
52
|
-
name: d.name,
|
|
53
|
-
summary: d.summary,
|
|
54
|
-
description: d.description ?? d.summary,
|
|
55
|
-
}))
|
|
56
|
-
const childrenFilesPayload = (payload?.childrenFiles ?? []).map(f => ({
|
|
57
|
-
name: f.name,
|
|
58
|
-
summary: f.summary,
|
|
59
|
-
description: f.description ?? f.summary,
|
|
60
|
-
}))
|
|
61
|
-
|
|
62
|
-
// LLM 不可用时回退到可解释的聚合文案
|
|
63
|
-
if (!llmConfig || !llmConfig.base_url || !llmConfig.model) {
|
|
64
|
-
const fileCount = childrenFilesPayload.length
|
|
65
|
-
const dirCount = childrenDirsPayload.length
|
|
66
|
-
const fallback = `The "${name}" directory contains ${fileCount} file(s) and ${dirCount} subdirectory(ies) and helps organize related source code and modules.`
|
|
67
|
-
return { description: fallback, summary: fallback, usage: { totalPromptTokens: 0, totalCompletionTokens: 0, totalTokens: 0, totalCalls: 0 } }
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// 初始化LLM相关服务(worker 内做本任务 token 统计,返回给主线程聚合)
|
|
71
|
-
const tracker = new LLMUsageTracker()
|
|
72
|
-
const llmClient = new OpenAIClient(llmConfig, tracker)
|
|
73
|
-
const fileSplitter = new CodeSplitter(llmClient)
|
|
74
|
-
const cache = new FileHashCache({
|
|
75
|
-
cacheDir: llmConfig.cache_dir,
|
|
76
|
-
maxSizeMb: llmConfig.cache_max_size_mb,
|
|
77
|
-
})
|
|
78
|
-
const llmAnalysisService = new LLMAnalysisService(llmClient, fileSplitter, cache, llmConfig)
|
|
79
|
-
|
|
80
|
-
try {
|
|
81
|
-
const dirResultFromLLM = await llmAnalysisService.analyzeDirectory(childrenDirsPayload, childrenFilesPayload)
|
|
82
|
-
return {
|
|
83
|
-
description: dirResultFromLLM.description,
|
|
84
|
-
summary: dirResultFromLLM.summary,
|
|
85
|
-
usage: tracker.getStats(),
|
|
86
|
-
}
|
|
87
|
-
} catch {
|
|
88
|
-
const fileCount = childrenFilesPayload.length
|
|
89
|
-
const dirCount = childrenDirsPayload.length
|
|
90
|
-
const fallback = `The "${name}" directory contains ${fileCount} file(s) and ${dirCount} subdirectory(ies) and helps organize related source code and modules.`
|
|
91
|
-
return { description: fallback, summary: fallback, usage: tracker.getStats() }
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export async function validateResult(parentResult: DirectoryAnalysis, childResult: FileAnalysis | DirectoryAnalysis): Promise<{
|
|
96
|
-
valid: boolean
|
|
97
|
-
corrections?: Partial<FileAnalysis | DirectoryAnalysis>
|
|
98
|
-
log?: ModificationLog
|
|
99
|
-
}> {
|
|
100
|
-
// 简单的校验逻辑:检查子项是否在父项的结构中存在
|
|
101
|
-
const existsInStructure = parentResult.structure.some(item => item.name === childResult.name)
|
|
102
|
-
const corrections: Partial<FileAnalysis | DirectoryAnalysis> = {}
|
|
103
|
-
let log: ModificationLog | undefined
|
|
104
|
-
|
|
105
|
-
if (!existsInStructure) {
|
|
106
|
-
// 修正:将子项添加到父项的结构中
|
|
107
|
-
;(corrections as Partial<DirectoryAnalysis>).structure = [
|
|
108
|
-
...parentResult.structure,
|
|
109
|
-
{
|
|
110
|
-
name: childResult.name,
|
|
111
|
-
type: childResult.type,
|
|
112
|
-
description: childResult.summary.substring(0, 100),
|
|
113
|
-
},
|
|
114
|
-
]
|
|
115
|
-
|
|
116
|
-
log = {
|
|
117
|
-
id: crypto.randomUUID(),
|
|
118
|
-
timestamp: new Date().toISOString(),
|
|
119
|
-
path: parentResult.path,
|
|
120
|
-
type: 'omission',
|
|
121
|
-
originalContent: JSON.stringify(parentResult.structure),
|
|
122
|
-
correctedContent: JSON.stringify((corrections as Partial<DirectoryAnalysis>).structure),
|
|
123
|
-
reason: `Child item ${childResult.name} missing from directory structure`,
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return {
|
|
127
|
-
valid: false,
|
|
128
|
-
corrections,
|
|
129
|
-
log,
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return { valid: true }
|
|
134
|
-
}
|
|
135
|
-
|