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.
Files changed (41) hide show
  1. package/README.md +48 -0
  2. package/dist/cli.js +319 -0
  3. package/dist/index.js +22 -0
  4. package/jest.config.js +27 -0
  5. package/package.json +59 -0
  6. package/src/adapters/command.schemas.ts +21 -0
  7. package/src/application/analysis.app.service.ts +272 -0
  8. package/src/application/bootstrap.ts +35 -0
  9. package/src/application/services/llm.analysis.service.ts +237 -0
  10. package/src/cli.ts +297 -0
  11. package/src/common/config.ts +209 -0
  12. package/src/common/constants.ts +8 -0
  13. package/src/common/errors.ts +34 -0
  14. package/src/common/logger.ts +82 -0
  15. package/src/common/types.ts +385 -0
  16. package/src/common/ui.ts +228 -0
  17. package/src/common/utils.ts +81 -0
  18. package/src/domain/index.ts +1 -0
  19. package/src/domain/interfaces.ts +188 -0
  20. package/src/domain/services/analysis.service.ts +735 -0
  21. package/src/domain/services/incremental.service.ts +50 -0
  22. package/src/index.ts +6 -0
  23. package/src/infrastructure/blacklist.service.ts +37 -0
  24. package/src/infrastructure/cache/file.hash.cache.ts +119 -0
  25. package/src/infrastructure/git/git.service.ts +120 -0
  26. package/src/infrastructure/git.service.ts +121 -0
  27. package/src/infrastructure/index.service.ts +94 -0
  28. package/src/infrastructure/llm/llm.usage.tracker.ts +65 -0
  29. package/src/infrastructure/llm/openai.client.ts +162 -0
  30. package/src/infrastructure/llm/prompt.template.ts +175 -0
  31. package/src/infrastructure/llm.service.ts +70 -0
  32. package/src/infrastructure/skill/skill.generator.ts +53 -0
  33. package/src/infrastructure/skill/templates/resolve.script.ts +97 -0
  34. package/src/infrastructure/skill/templates/skill.md.template.ts +45 -0
  35. package/src/infrastructure/splitter/code.splitter.ts +176 -0
  36. package/src/infrastructure/storage.service.ts +413 -0
  37. package/src/infrastructure/worker-pool/parse.worker.impl.ts +135 -0
  38. package/src/infrastructure/worker-pool/parse.worker.ts +9 -0
  39. package/src/infrastructure/worker-pool/worker-pool.service.ts +173 -0
  40. package/tsconfig.json +24 -0
  41. 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
+ }