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,162 +0,0 @@
|
|
|
1
|
-
import OpenAI from 'openai';
|
|
2
|
-
import { ILLMClient } from '../../domain/interfaces';
|
|
3
|
-
import { LLMConfig, LLMCallOptions, LLMResponse } from '../../common/types';
|
|
4
|
-
import { LLMUsageTracker } from './llm.usage.tracker';
|
|
5
|
-
import { AppError, ErrorCode } from '../../common/errors';
|
|
6
|
-
import { logger } from '../../common/logger';
|
|
7
|
-
|
|
8
|
-
export class OpenAIClient implements ILLMClient {
|
|
9
|
-
private client: OpenAI;
|
|
10
|
-
private config: LLMConfig;
|
|
11
|
-
private tracker?: LLMUsageTracker;
|
|
12
|
-
|
|
13
|
-
constructor(config: LLMConfig, tracker?: LLMUsageTracker) {
|
|
14
|
-
this.config = config;
|
|
15
|
-
this.tracker = tracker;
|
|
16
|
-
this.client = new OpenAI({
|
|
17
|
-
apiKey: config.api_key,
|
|
18
|
-
baseURL: config.base_url,
|
|
19
|
-
timeout: config.timeout,
|
|
20
|
-
dangerouslyAllowBrowser: true,
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* 连接可用性校验(V2.5)
|
|
26
|
-
* - 在进入任何解析流程前调用;
|
|
27
|
-
* - 配置不完整或服务不可用时抛出带有明确 ErrorCode 的 AppError。
|
|
28
|
-
*/
|
|
29
|
-
async testConnection(config: LLMConfig): Promise<void> {
|
|
30
|
-
// 保持与最新配置一致(允许运行时通过 CLI/环境变量覆盖)
|
|
31
|
-
this.config = config;
|
|
32
|
-
// 基本配置校验:base_url / api_key / model 不能为空
|
|
33
|
-
if (!this.config.base_url || !this.config.api_key || !this.config.model) {
|
|
34
|
-
throw new AppError(
|
|
35
|
-
ErrorCode.LLM_INVALID_CONFIG,
|
|
36
|
-
'Incomplete LLM config. Please set base_url/api_key/model via config file, env vars, or CLI options.',
|
|
37
|
-
{
|
|
38
|
-
missing: {
|
|
39
|
-
base_url: !this.config.base_url,
|
|
40
|
-
api_key: !this.config.api_key,
|
|
41
|
-
model: !this.config.model,
|
|
42
|
-
},
|
|
43
|
-
},
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
try {
|
|
48
|
-
const res = await this.client.chat.completions.create({
|
|
49
|
-
model: this.config.model,
|
|
50
|
-
temperature: 0,
|
|
51
|
-
max_tokens: 1,
|
|
52
|
-
messages: [{ role: 'system', content: 'health-check' }],
|
|
53
|
-
} as any);
|
|
54
|
-
|
|
55
|
-
const status = (res as any).status ?? 200;
|
|
56
|
-
if (status === 401) {
|
|
57
|
-
throw new AppError(ErrorCode.LLM_CALL_FAILED, 'LLM authentication failed (401)', { status });
|
|
58
|
-
}
|
|
59
|
-
if (status === 404) {
|
|
60
|
-
throw new AppError(ErrorCode.LLM_CALL_FAILED, 'LLM model not found (404)', { status });
|
|
61
|
-
}
|
|
62
|
-
if (status < 200 || status >= 300) {
|
|
63
|
-
throw new AppError(
|
|
64
|
-
ErrorCode.LLM_CALL_FAILED,
|
|
65
|
-
`LLM connectivity check returned non-2xx status: ${status}`,
|
|
66
|
-
{ status },
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
} catch (e: any) {
|
|
70
|
-
if (e instanceof AppError) {
|
|
71
|
-
throw e;
|
|
72
|
-
}
|
|
73
|
-
const code = e?.code || e?.status;
|
|
74
|
-
if (code === 'ETIMEDOUT') {
|
|
75
|
-
throw new AppError(ErrorCode.LLM_TIMEOUT, 'LLM connectivity check timed out', e);
|
|
76
|
-
}
|
|
77
|
-
if (code === 'ENOTFOUND' || code === 'ECONNREFUSED' || code === 'ECONNRESET') {
|
|
78
|
-
throw new AppError(ErrorCode.LLM_CALL_FAILED, 'Unable to reach LLM service. Check network or base_url.', e);
|
|
79
|
-
}
|
|
80
|
-
throw new AppError(
|
|
81
|
-
ErrorCode.LLM_CALL_FAILED,
|
|
82
|
-
`LLM connectivity check failed: ${e?.message || String(e)}`,
|
|
83
|
-
e,
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* 向后兼容旧版本/测试中使用的 connectTest 名称。
|
|
90
|
-
* 内部直接代理到 V2.5 的 testConnection。
|
|
91
|
-
*/
|
|
92
|
-
async connectTest(): Promise<void> {
|
|
93
|
-
await this.testConnection(this.config);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
async call(prompt: string, options?: LLMCallOptions): Promise<LLMResponse> {
|
|
97
|
-
const startTime = Date.now();
|
|
98
|
-
const retries = options?.retries ?? this.config.max_retries;
|
|
99
|
-
|
|
100
|
-
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
101
|
-
try {
|
|
102
|
-
const response = await this.client.chat.completions.create({
|
|
103
|
-
model: options?.model ?? this.config.model,
|
|
104
|
-
temperature: options?.temperature ?? this.config.temperature,
|
|
105
|
-
max_tokens: options?.maxTokens ?? this.config.max_tokens,
|
|
106
|
-
messages: [
|
|
107
|
-
{ role: 'user', content: prompt }
|
|
108
|
-
]
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
const content = response.choices[0].message.content || '';
|
|
112
|
-
const usage = response.usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
|
|
113
|
-
|
|
114
|
-
const normalizedUsage = {
|
|
115
|
-
promptTokens: usage.prompt_tokens ?? 0,
|
|
116
|
-
completionTokens: usage.completion_tokens ?? 0,
|
|
117
|
-
totalTokens: usage.total_tokens ?? 0,
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
if (this.tracker) {
|
|
121
|
-
this.tracker.addUsage(normalizedUsage);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return {
|
|
125
|
-
content,
|
|
126
|
-
usage: normalizedUsage,
|
|
127
|
-
model: response.model,
|
|
128
|
-
responseTime: Date.now() - startTime,
|
|
129
|
-
};
|
|
130
|
-
} catch (error: any) {
|
|
131
|
-
const errorMessage = error?.message || 'Unknown LLM call error';
|
|
132
|
-
logger.debug(`LLM call attempt ${attempt + 1} failed: ${errorMessage}`);
|
|
133
|
-
|
|
134
|
-
if (attempt === retries) {
|
|
135
|
-
if (error?.status === 429) {
|
|
136
|
-
throw new AppError(ErrorCode.LLM_RATE_LIMITED, 'LLM service rate limited', error);
|
|
137
|
-
} else if (error?.code === 'ETIMEDOUT') {
|
|
138
|
-
throw new AppError(ErrorCode.LLM_TIMEOUT, 'LLM call timeout', error);
|
|
139
|
-
} else {
|
|
140
|
-
throw new AppError(ErrorCode.LLM_CALL_FAILED, `LLM call failed: ${errorMessage}`, error);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
await this.sleep(this.config.retry_delay * Math.pow(2, attempt));
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
throw new AppError(ErrorCode.LLM_CALL_FAILED, 'Max retries exceeded');
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
async batchCall(prompts: string[], options?: LLMCallOptions): Promise<LLMResponse[]> {
|
|
152
|
-
const results: LLMResponse[] = [];
|
|
153
|
-
for (const prompt of prompts) {
|
|
154
|
-
results.push(await this.call(prompt, options));
|
|
155
|
-
}
|
|
156
|
-
return results;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
private sleep(ms: number): Promise<void> {
|
|
160
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
161
|
-
}
|
|
162
|
-
}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import OpenAI from 'openai';
|
|
2
|
-
import { configManager } from '../common/config';
|
|
3
|
-
import { AppError, ErrorCode } from '../common/errors';
|
|
4
|
-
import { logger } from '../common/logger';
|
|
5
|
-
|
|
6
|
-
export interface LLMService {
|
|
7
|
-
generateCompletion(prompt: string, systemPrompt?: string): Promise<string>;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export class OpenAILLMService implements LLMService {
|
|
11
|
-
private client: OpenAI | null = null;
|
|
12
|
-
|
|
13
|
-
private async getClient(): Promise<OpenAI> {
|
|
14
|
-
if (!this.client) {
|
|
15
|
-
let config: any;
|
|
16
|
-
try {
|
|
17
|
-
config = configManager.getConfig();
|
|
18
|
-
} catch (e) {
|
|
19
|
-
await configManager.load();
|
|
20
|
-
config = configManager.getConfig();
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (!config.llm.api_key) {
|
|
24
|
-
throw new AppError(
|
|
25
|
-
ErrorCode.ANALYSIS_EXCEPTION,
|
|
26
|
-
'LLM API key not configured. Please set it in config file or via SKILL_ANY_CODE_LLM_API_KEY environment variable.',
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
this.client = new OpenAI({
|
|
31
|
-
baseURL: config.llm.base_url,
|
|
32
|
-
apiKey: config.llm.api_key,
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
return this.client;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
async generateCompletion(prompt: string, systemPrompt: string = 'You are a code analysis expert. You help analyze code files, generate summaries, class diagrams, and other analysis results. Be concise and accurate.'): Promise<string> {
|
|
39
|
-
const client = await this.getClient();
|
|
40
|
-
const config = configManager.getConfig();
|
|
41
|
-
|
|
42
|
-
try {
|
|
43
|
-
logger.debug(`Calling LLM model ${config.llm.model} with prompt length: ${prompt.length}`);
|
|
44
|
-
|
|
45
|
-
const response = await client.chat.completions.create({
|
|
46
|
-
model: config.llm.model,
|
|
47
|
-
messages: [
|
|
48
|
-
{ role: 'system', content: systemPrompt },
|
|
49
|
-
{ role: 'user', content: prompt },
|
|
50
|
-
],
|
|
51
|
-
temperature: 0.1,
|
|
52
|
-
max_tokens: 2048,
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
const result = response.choices[0]?.message?.content?.trim() || '';
|
|
56
|
-
|
|
57
|
-
if (!result) {
|
|
58
|
-
throw new AppError(ErrorCode.ANALYSIS_EXCEPTION, 'LLM returned empty response');
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
logger.debug(`LLM response received, length: ${result.length}`);
|
|
62
|
-
return result;
|
|
63
|
-
} catch (error: any) {
|
|
64
|
-
logger.error('LLM call failed:', error);
|
|
65
|
-
throw new AppError(ErrorCode.ANALYSIS_EXCEPTION, `LLM call failed: ${error.message}`);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export const llmService = new OpenAILLMService();
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs-extra'
|
|
2
|
-
import * as path from 'path'
|
|
3
|
-
import { ISkillGenerator, SkillGenerateOptions, SkillProvider } from '../../domain/interfaces'
|
|
4
|
-
import { getSkillMdContent } from './templates/skill.md.template'
|
|
5
|
-
import { getResolveScriptContent } from './templates/resolve.script'
|
|
6
|
-
import { logger } from '../../common/logger'
|
|
7
|
-
|
|
8
|
-
const PROVIDER_DIRECTORY_MAP: Record<SkillProvider, string> = {
|
|
9
|
-
opencode: '.agents/skills/skill-any-code',
|
|
10
|
-
cursor: '.agents/skills/skill-any-code',
|
|
11
|
-
codex: '.agents/skills/skill-any-code',
|
|
12
|
-
claude: '.claude/skills/skill-any-code',
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export class SkillGenerator implements ISkillGenerator {
|
|
16
|
-
async generate(options: SkillGenerateOptions): Promise<string[]> {
|
|
17
|
-
const { projectRoot, providers } = options
|
|
18
|
-
const deployedPaths: string[] = []
|
|
19
|
-
|
|
20
|
-
const uniqueDirs = new Set<string>()
|
|
21
|
-
for (const p of providers) {
|
|
22
|
-
const lowerProvider = p.toLowerCase() as SkillProvider
|
|
23
|
-
const dir = PROVIDER_DIRECTORY_MAP[lowerProvider]
|
|
24
|
-
if (dir) {
|
|
25
|
-
uniqueDirs.add(dir)
|
|
26
|
-
} else {
|
|
27
|
-
logger.warn(`Unknown provider: ${p}. Skipped.`)
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const skillMd = getSkillMdContent()
|
|
32
|
-
const resolveScript = getResolveScriptContent()
|
|
33
|
-
|
|
34
|
-
for (const relativeDir of uniqueDirs) {
|
|
35
|
-
const targetDir = path.join(projectRoot, relativeDir)
|
|
36
|
-
try {
|
|
37
|
-
await fs.ensureDir(targetDir)
|
|
38
|
-
await fs.ensureDir(path.join(targetDir, 'scripts'))
|
|
39
|
-
|
|
40
|
-
await fs.writeFile(path.join(targetDir, 'SKILL.md'), skillMd, 'utf-8')
|
|
41
|
-
await fs.writeFile(path.join(targetDir, 'scripts', 'get-summary.py'), resolveScript, 'utf-8')
|
|
42
|
-
|
|
43
|
-
deployedPaths.push(targetDir)
|
|
44
|
-
logger.debug(`Skill deployed to: ${targetDir}`)
|
|
45
|
-
} catch (error: unknown) {
|
|
46
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
47
|
-
logger.warn(`Failed to deploy skill (${targetDir}): ${msg}`)
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return deployedPaths
|
|
52
|
-
}
|
|
53
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
export function getSkillMdContent(): string {
|
|
2
|
-
const lines = [
|
|
3
|
-
'---',
|
|
4
|
-
'name: code-atlas-navigator',
|
|
5
|
-
'description: Use this skill when you need to explore, understand, or search this codebase. Input a file or directory path relative to the project root to retrieve the path to its detailed natural language summary. Use this progressively to navigate from the root directory down to specific target files without reading the full raw source code.',
|
|
6
|
-
'---',
|
|
7
|
-
'',
|
|
8
|
-
'# Codebase Navigation Guide',
|
|
9
|
-
'',
|
|
10
|
-
'This repository has been pre-analyzed and summarized into natural language Markdown files. To save context window and improve accuracy, **do not read the raw source code directly**. Instead, use this skill to progressively navigate the repository layer by layer.',
|
|
11
|
-
'',
|
|
12
|
-
'## 🧭 How to Explore the Codebase',
|
|
13
|
-
'',
|
|
14
|
-
"1. **Start High-Level**: If you don't know the exact file location, begin by querying the summary of the root directory (`.`).",
|
|
15
|
-
'2. **Progressive Disclosure**: Read the directory summary to understand its sub-components. Identify the next relevant sub-directory or file based on your current task.',
|
|
16
|
-
'3. **Drill Down**: Query the summaries of those specific sub-components. Repeat this until you locate the target function, class, or logic.',
|
|
17
|
-
'',
|
|
18
|
-
'## 🛠️ How to Locate a Summary File',
|
|
19
|
-
'',
|
|
20
|
-
'Use the provided Python script to map the original codebase path to its corresponding Markdown summary path.',
|
|
21
|
-
'',
|
|
22
|
-
'**Script Specification:**',
|
|
23
|
-
'* **Input Parameter**: The relative path of the target file or directory with respect to the project root.',
|
|
24
|
-
'* **Output**: The relative path of the Markdown summary file with respect to the project root.',
|
|
25
|
-
'* **Fallback**: If the summary Markdown file cannot be found, the script will strictly output `N/A`.',
|
|
26
|
-
'',
|
|
27
|
-
'### Execution Commands',
|
|
28
|
-
'',
|
|
29
|
-
'Since the execution environment may vary, please use the appropriate command based on your current operating system:',
|
|
30
|
-
'',
|
|
31
|
-
'**For Linux / macOS (Bash/Zsh):**',
|
|
32
|
-
'```bash',
|
|
33
|
-
'python3 scripts/get_summary.py <relative/path/to/target>',
|
|
34
|
-
'```',
|
|
35
|
-
'(Note: If python3 is not found, fallback to python).',
|
|
36
|
-
'',
|
|
37
|
-
'**For Windows (CMD/PowerShell):**',
|
|
38
|
-
'```dos',
|
|
39
|
-
'python scripts\\get_summary.py <relative\\path\\to\\target>',
|
|
40
|
-
'```',
|
|
41
|
-
'(Note: You may also use py or python3 depending on the Windows environment setup).',
|
|
42
|
-
]
|
|
43
|
-
|
|
44
|
-
return lines.join('\n') + '\n'
|
|
45
|
-
}
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import { IFileSplitter } from '../../domain/interfaces';
|
|
2
|
-
import { FileChunk, FileChunkAnalysis, FileAnalysis } from '../../common/types';
|
|
3
|
-
import { AppError, ErrorCode } from '../../common/errors';
|
|
4
|
-
import { ILLMClient } from '../../domain/interfaces';
|
|
5
|
-
import {
|
|
6
|
-
MERGE_STRUCTURE_PROMPT,
|
|
7
|
-
FILE_DESCRIPTION_PROMPT,
|
|
8
|
-
FILE_SUMMARY_PROMPT,
|
|
9
|
-
PARSE_RETRY_HINT
|
|
10
|
-
} from '../llm/prompt.template';
|
|
11
|
-
import Mustache from 'mustache';
|
|
12
|
-
import path from 'path';
|
|
13
|
-
|
|
14
|
-
/** 从 LLM 返回中解析单字段:支持 {"key": "value"} 或纯字符串 */
|
|
15
|
-
function parseSingleField(content: string, field: 'description' | 'summary'): string {
|
|
16
|
-
const trimmed = content.trim();
|
|
17
|
-
try {
|
|
18
|
-
const o = JSON.parse(trimmed);
|
|
19
|
-
if (o && typeof o[field] === 'string') return o[field];
|
|
20
|
-
} catch {
|
|
21
|
-
// 非 JSON 则整体视为该字段内容
|
|
22
|
-
}
|
|
23
|
-
return trimmed || '';
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export class CodeSplitter implements IFileSplitter {
|
|
27
|
-
private llmClient: ILLMClient;
|
|
28
|
-
|
|
29
|
-
constructor(llmClient: ILLMClient) {
|
|
30
|
-
this.llmClient = llmClient;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async split(fileContent: string, maxChunkSize: number): Promise<FileChunk[]> {
|
|
34
|
-
try {
|
|
35
|
-
const lines = fileContent.split('\n');
|
|
36
|
-
const chunks: FileChunk[] = [];
|
|
37
|
-
let currentChunkLines: string[] = [];
|
|
38
|
-
let currentChunkLength = 0;
|
|
39
|
-
let startLine = 0;
|
|
40
|
-
|
|
41
|
-
for (let i = 0; i < lines.length; i++) {
|
|
42
|
-
const line = lines[i];
|
|
43
|
-
const lineLength = line.length + 1; // +1 for newline
|
|
44
|
-
|
|
45
|
-
// 检查是否是语义边界:类/函数定义、空行、注释块结束等
|
|
46
|
-
const isSemanticBoundary = /^(class|function|interface|type|enum|export\s+(class|function|interface)|\/\/\s*|\/\*\*|\*\/|\s*$)/.test(line.trim());
|
|
47
|
-
|
|
48
|
-
if (currentChunkLength + lineLength > maxChunkSize && isSemanticBoundary && currentChunkLines.length > 0) {
|
|
49
|
-
// 保存当前分片
|
|
50
|
-
chunks.push({
|
|
51
|
-
id: chunks.length,
|
|
52
|
-
content: currentChunkLines.join('\n'),
|
|
53
|
-
startLine,
|
|
54
|
-
endLine: i - 1,
|
|
55
|
-
context: this.extractContext(currentChunkLines)
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
// 开始新分片
|
|
59
|
-
currentChunkLines = [];
|
|
60
|
-
currentChunkLength = 0;
|
|
61
|
-
startLine = i;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
currentChunkLines.push(line);
|
|
65
|
-
currentChunkLength += lineLength;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// 添加最后一个分片
|
|
69
|
-
if (currentChunkLines.length > 0) {
|
|
70
|
-
chunks.push({
|
|
71
|
-
id: chunks.length,
|
|
72
|
-
content: currentChunkLines.join('\n'),
|
|
73
|
-
startLine,
|
|
74
|
-
endLine: lines.length - 1,
|
|
75
|
-
context: this.extractContext(currentChunkLines)
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return chunks;
|
|
80
|
-
} catch (error: any) {
|
|
81
|
-
throw new AppError(ErrorCode.FILE_SPLIT_FAILED, `Failed to split file: ${error.message}`, error);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* 合并阶段按需求 10.7.1 / 10.9.4:三步 LLM 调用(结构 → 功能描述 → 概述),与单文件非分片协议一致;
|
|
87
|
-
* 每次解析失败仅重试当次。
|
|
88
|
-
*/
|
|
89
|
-
async merge(chunks: FileChunkAnalysis[], filePath: string): Promise<FileAnalysis> {
|
|
90
|
-
const opts = { temperature: 0.1 };
|
|
91
|
-
|
|
92
|
-
// 第一步:合并分片结果为统一结构
|
|
93
|
-
const structure = await this.callWithParseRetry(
|
|
94
|
-
Mustache.render(MERGE_STRUCTURE_PROMPT, {
|
|
95
|
-
filePath,
|
|
96
|
-
chunkResults: JSON.stringify(chunks, null, 2)
|
|
97
|
-
}),
|
|
98
|
-
opts,
|
|
99
|
-
(content) => {
|
|
100
|
-
const o = JSON.parse(content);
|
|
101
|
-
return {
|
|
102
|
-
name: o.name ?? path.basename(filePath),
|
|
103
|
-
classes: Array.isArray(o.classes) ? o.classes : [],
|
|
104
|
-
functions: Array.isArray(o.functions) ? o.functions : []
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
const structureJson = JSON.stringify(structure, null, 2);
|
|
110
|
-
|
|
111
|
-
// 第二步:生成功能描述
|
|
112
|
-
const description = await this.callWithParseRetry(
|
|
113
|
-
Mustache.render(FILE_DESCRIPTION_PROMPT, { structureJson }),
|
|
114
|
-
opts,
|
|
115
|
-
(content) => parseSingleField(content, 'description')
|
|
116
|
-
);
|
|
117
|
-
|
|
118
|
-
// 第三步:生成概述
|
|
119
|
-
const summary = await this.callWithParseRetry(
|
|
120
|
-
Mustache.render(FILE_SUMMARY_PROMPT, { structureJson, description }),
|
|
121
|
-
opts,
|
|
122
|
-
(content) => parseSingleField(content, 'summary')
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
// 基础信息由程序侧负责,此处仅返回语义部分,路径等由调用方补充
|
|
126
|
-
const name = path.basename(filePath);
|
|
127
|
-
|
|
128
|
-
return {
|
|
129
|
-
type: 'file',
|
|
130
|
-
path: filePath,
|
|
131
|
-
name,
|
|
132
|
-
language: '',
|
|
133
|
-
linesOfCode: 0,
|
|
134
|
-
dependencies: [],
|
|
135
|
-
description,
|
|
136
|
-
summary,
|
|
137
|
-
classes: structure.classes,
|
|
138
|
-
functions: structure.functions,
|
|
139
|
-
lastAnalyzedAt: new Date().toISOString(),
|
|
140
|
-
commitHash: ''
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/** 单次调用:解析失败则仅重试该次一次(需求 10.9.2)。 */
|
|
145
|
-
private async callWithParseRetry<T>(
|
|
146
|
-
prompt: string,
|
|
147
|
-
options: { temperature?: number },
|
|
148
|
-
parseFn: (content: string) => T
|
|
149
|
-
): Promise<T> {
|
|
150
|
-
let lastError: unknown;
|
|
151
|
-
for (let attempt = 0; attempt < 2; attempt++) {
|
|
152
|
-
try {
|
|
153
|
-
const response = await this.llmClient.call(attempt === 1 ? prompt + PARSE_RETRY_HINT : prompt, {
|
|
154
|
-
...options,
|
|
155
|
-
retries: 0
|
|
156
|
-
});
|
|
157
|
-
return parseFn(response.content);
|
|
158
|
-
} catch (e) {
|
|
159
|
-
lastError = e;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
throw new AppError(
|
|
163
|
-
ErrorCode.CHUNK_MERGE_FAILED,
|
|
164
|
-
`Failed to parse merge response after retry: ${(lastError as Error)?.message}`,
|
|
165
|
-
lastError
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
private extractContext(lines: string[]): string {
|
|
170
|
-
// 提取分片的上下文信息:导入语句、类/函数定义开头
|
|
171
|
-
const contextLines = lines.filter(line =>
|
|
172
|
-
/^(import|export|class|function|interface|type|enum)/.test(line.trim())
|
|
173
|
-
).slice(0, 10); // 最多取前10行上下文
|
|
174
|
-
return contextLines.join('\n');
|
|
175
|
-
}
|
|
176
|
-
}
|