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