@yeepay/coderocket-mcp 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +113 -0
- package/README.md +57 -29
- package/bin/coderocket-mcp +84 -79
- package/dist/coderocket.d.ts +87 -39
- package/dist/coderocket.d.ts.map +1 -1
- package/dist/coderocket.js +852 -268
- package/dist/coderocket.js.map +1 -1
- package/dist/test.d.ts.map +1 -1
- package/dist/test.js +173 -44
- package/dist/test.js.map +1 -1
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -1
- package/eslint.config.ts +12 -5
- package/package.json +9 -4
package/dist/coderocket.js
CHANGED
@@ -1,154 +1,749 @@
|
|
1
1
|
import { exec } from 'child_process';
|
2
2
|
import { promisify } from 'util';
|
3
|
-
import { writeFile, readFile, mkdir
|
4
|
-
import fs from 'fs';
|
3
|
+
import { writeFile, readFile, mkdir } from 'fs/promises';
|
5
4
|
import { join, dirname, resolve } from 'path';
|
6
|
-
import {
|
5
|
+
import { homedir } from 'os';
|
6
|
+
import { GoogleGenerativeAI } from '@google/generative-ai';
|
7
|
+
import Anthropic from '@anthropic-ai/sdk';
|
8
|
+
import axios from 'axios';
|
7
9
|
import { logger, errorHandler } from './logger.js';
|
8
10
|
const execAsync = promisify(exec);
|
9
11
|
/**
|
10
|
-
*
|
12
|
+
* 独立配置管理类
|
11
13
|
*
|
12
|
-
*
|
14
|
+
* 支持多层级配置加载:
|
15
|
+
* 1. 环境变量(最高优先级)
|
16
|
+
* 2. 项目级 .env 文件
|
17
|
+
* 3. 全局 ~/.coderocket/env 文件
|
18
|
+
* 4. 默认值(最低优先级)
|
13
19
|
*/
|
14
|
-
export class
|
15
|
-
|
16
|
-
|
17
|
-
|
20
|
+
export class ConfigManager {
|
21
|
+
static config = {};
|
22
|
+
static initialized = false;
|
23
|
+
/**
|
24
|
+
* 初始化配置系统
|
25
|
+
*/
|
26
|
+
static async initialize() {
|
27
|
+
if (this.initialized)
|
28
|
+
return;
|
29
|
+
// 加载默认配置
|
30
|
+
this.loadDefaults();
|
31
|
+
// 加载全局配置文件
|
32
|
+
await this.loadGlobalConfig();
|
33
|
+
// 加载项目配置文件
|
34
|
+
await this.loadProjectConfig();
|
35
|
+
// 加载环境变量(最高优先级)
|
36
|
+
this.loadEnvironmentVariables();
|
37
|
+
this.initialized = true;
|
38
|
+
logger.info('配置系统初始化完成', { config: this.getSafeConfig() });
|
18
39
|
}
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
40
|
+
/**
|
41
|
+
* 加载默认配置
|
42
|
+
*/
|
43
|
+
static loadDefaults() {
|
44
|
+
this.config = {
|
45
|
+
AI_SERVICE: 'gemini',
|
46
|
+
AI_AUTO_SWITCH: 'true',
|
47
|
+
AI_TIMEOUT: '30',
|
48
|
+
AI_MAX_RETRIES: '3',
|
49
|
+
AI_RETRY_DELAY: '2',
|
50
|
+
NODE_ENV: 'production',
|
51
|
+
DEBUG: 'false',
|
52
|
+
};
|
53
|
+
}
|
54
|
+
/**
|
55
|
+
* 加载全局配置文件 ~/.coderocket/env
|
56
|
+
*/
|
57
|
+
static async loadGlobalConfig() {
|
58
|
+
try {
|
59
|
+
const globalConfigPath = join(homedir(), '.coderocket', 'env');
|
60
|
+
const content = await readFile(globalConfigPath, 'utf-8');
|
61
|
+
const globalConfig = this.parseEnvContent(content);
|
62
|
+
Object.assign(this.config, globalConfig);
|
63
|
+
logger.debug('全局配置加载成功', { path: globalConfigPath });
|
64
|
+
}
|
65
|
+
catch (error) {
|
66
|
+
// 全局配置文件不存在是正常的
|
67
|
+
logger.debug('全局配置文件不存在,跳过');
|
25
68
|
}
|
26
69
|
}
|
27
70
|
/**
|
28
|
-
*
|
71
|
+
* 加载项目配置文件 .env
|
29
72
|
*/
|
30
|
-
async
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
'
|
73
|
+
static async loadProjectConfig() {
|
74
|
+
try {
|
75
|
+
const projectConfigPath = join(process.cwd(), '.env');
|
76
|
+
const content = await readFile(projectConfigPath, 'utf-8');
|
77
|
+
const projectConfig = this.parseEnvContent(content);
|
78
|
+
Object.assign(this.config, projectConfig);
|
79
|
+
logger.debug('项目配置加载成功', { path: projectConfigPath });
|
80
|
+
}
|
81
|
+
catch (error) {
|
82
|
+
// 项目配置文件不存在是正常的
|
83
|
+
logger.debug('项目配置文件不存在,跳过');
|
84
|
+
}
|
85
|
+
}
|
86
|
+
/**
|
87
|
+
* 加载环境变量(最高优先级)
|
88
|
+
*/
|
89
|
+
static loadEnvironmentVariables() {
|
90
|
+
const envKeys = [
|
91
|
+
'AI_SERVICE', 'AI_AUTO_SWITCH', 'AI_TIMEOUT', 'AI_MAX_RETRIES', 'AI_RETRY_DELAY',
|
92
|
+
'GEMINI_API_KEY', 'OPENCODE_API_KEY', 'CLAUDECODE_API_KEY',
|
93
|
+
'NODE_ENV', 'DEBUG'
|
41
94
|
];
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
const libPath = join(path, 'lib', 'ai-service-manager.sh');
|
46
|
-
await access(libPath, fs.constants.F_OK);
|
47
|
-
return path;
|
95
|
+
envKeys.forEach(key => {
|
96
|
+
if (process.env[key]) {
|
97
|
+
this.config[key] = process.env[key];
|
48
98
|
}
|
49
|
-
|
50
|
-
|
99
|
+
});
|
100
|
+
}
|
101
|
+
/**
|
102
|
+
* 解析 .env 文件内容
|
103
|
+
*/
|
104
|
+
static parseEnvContent(content) {
|
105
|
+
const config = {};
|
106
|
+
const lines = content.split('\n');
|
107
|
+
for (const line of lines) {
|
108
|
+
const trimmedLine = line.trim();
|
109
|
+
if (trimmedLine && !trimmedLine.startsWith('#')) {
|
110
|
+
const [key, ...valueParts] = trimmedLine.split('=');
|
111
|
+
if (key && valueParts.length > 0) {
|
112
|
+
const value = valueParts.join('=').trim();
|
113
|
+
// 移除引号
|
114
|
+
config[key.trim()] = value.replace(/^["']|["']$/g, '');
|
115
|
+
}
|
51
116
|
}
|
52
117
|
}
|
53
|
-
|
54
|
-
return resolve(process.cwd(), '../coderocket-cli');
|
118
|
+
return config;
|
55
119
|
}
|
56
120
|
/**
|
57
|
-
*
|
121
|
+
* 获取配置值
|
58
122
|
*/
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
cwd: cwd || this.coderocketCliPath,
|
63
|
-
});
|
64
|
-
try {
|
65
|
-
const { stdout, stderr } = await execAsync(command, {
|
66
|
-
cwd: cwd || this.coderocketCliPath,
|
67
|
-
env: {
|
68
|
-
...process.env,
|
69
|
-
PATH: `${this.coderocketCliPath}/bin:${process.env.PATH}`,
|
70
|
-
},
|
71
|
-
timeout: 300000, // 5分钟超时
|
72
|
-
});
|
73
|
-
logger.debug('Shell命令执行成功', {
|
74
|
-
command,
|
75
|
-
stdoutLength: stdout.length,
|
76
|
-
stderrLength: stderr.length,
|
77
|
-
});
|
78
|
-
return { stdout, stderr };
|
79
|
-
}
|
80
|
-
catch (error) {
|
81
|
-
logger.error('Shell命令执行失败', error, { command, cwd });
|
82
|
-
throw errorHandler.handleError(error, 'executeShellCommand');
|
123
|
+
static get(key, defaultValue) {
|
124
|
+
if (!this.initialized) {
|
125
|
+
throw new Error('ConfigManager 未初始化,请先调用 initialize()');
|
83
126
|
}
|
127
|
+
return this.config[key] ?? defaultValue;
|
84
128
|
}
|
85
129
|
/**
|
86
|
-
*
|
130
|
+
* 获取API密钥环境变量名
|
87
131
|
*/
|
88
|
-
|
89
|
-
const
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
132
|
+
static getAPIKeyEnvVar(service) {
|
133
|
+
const envVarMap = {
|
134
|
+
gemini: 'GEMINI_API_KEY',
|
135
|
+
opencode: 'OPENCODE_API_KEY',
|
136
|
+
claudecode: 'CLAUDECODE_API_KEY',
|
137
|
+
};
|
138
|
+
return envVarMap[service];
|
139
|
+
}
|
140
|
+
/**
|
141
|
+
* 获取API密钥
|
142
|
+
*/
|
143
|
+
static getAPIKey(service) {
|
144
|
+
const envVar = this.getAPIKeyEnvVar(service);
|
145
|
+
return this.get(envVar, '');
|
146
|
+
}
|
147
|
+
/**
|
148
|
+
* 获取AI服务配置
|
149
|
+
*/
|
150
|
+
static getAIService() {
|
151
|
+
const service = this.get('AI_SERVICE', 'gemini').toLowerCase();
|
152
|
+
if (['gemini', 'opencode', 'claudecode'].includes(service)) {
|
153
|
+
return service;
|
94
154
|
}
|
95
|
-
return
|
155
|
+
return 'gemini';
|
156
|
+
}
|
157
|
+
/**
|
158
|
+
* 获取超时配置
|
159
|
+
*/
|
160
|
+
static getTimeout() {
|
161
|
+
return parseInt(this.get('AI_TIMEOUT', '30'), 10);
|
162
|
+
}
|
163
|
+
/**
|
164
|
+
* 获取最大重试次数
|
165
|
+
*/
|
166
|
+
static getMaxRetries() {
|
167
|
+
return parseInt(this.get('AI_MAX_RETRIES', '3'), 10);
|
168
|
+
}
|
169
|
+
/**
|
170
|
+
* 是否启用自动切换
|
171
|
+
*/
|
172
|
+
static isAutoSwitchEnabled() {
|
173
|
+
return this.get('AI_AUTO_SWITCH', 'true').toLowerCase() === 'true';
|
96
174
|
}
|
97
175
|
/**
|
98
|
-
*
|
176
|
+
* 获取安全的配置信息(隐藏敏感信息)
|
99
177
|
*/
|
100
|
-
|
101
|
-
const
|
102
|
-
|
178
|
+
static getSafeConfig() {
|
179
|
+
const safeConfig = { ...this.config };
|
180
|
+
// 隐藏API密钥
|
181
|
+
Object.keys(safeConfig).forEach(key => {
|
182
|
+
if (key.includes('API_KEY') || key.includes('TOKEN')) {
|
183
|
+
safeConfig[key] = safeConfig[key] ? '***' : undefined;
|
184
|
+
}
|
185
|
+
});
|
186
|
+
return safeConfig;
|
187
|
+
}
|
188
|
+
/**
|
189
|
+
* 获取配置文件路径(保持向后兼容)
|
190
|
+
*/
|
191
|
+
static getConfigPath(scope) {
|
192
|
+
const configDir = scope === 'global'
|
193
|
+
? join(homedir(), '.coderocket')
|
194
|
+
: process.cwd();
|
195
|
+
const configFile = scope === 'global'
|
196
|
+
? join(configDir, 'env')
|
197
|
+
: join(configDir, '.env');
|
198
|
+
return { dir: configDir, file: configFile };
|
199
|
+
}
|
200
|
+
}
|
201
|
+
/**
|
202
|
+
* 独立提示词管理类
|
203
|
+
*
|
204
|
+
* 支持多层级提示词加载:
|
205
|
+
* 1. 项目级 prompts/ 目录(优先级高)
|
206
|
+
* 2. 全局 ~/.coderocket/prompts/ 目录(优先级低)
|
207
|
+
*/
|
208
|
+
class PromptManager {
|
209
|
+
static promptCache = new Map();
|
210
|
+
/**
|
211
|
+
* 加载提示词文件
|
212
|
+
*/
|
213
|
+
static async loadPrompt(name) {
|
214
|
+
// 检查缓存
|
215
|
+
const cacheKey = name;
|
216
|
+
if (this.promptCache.has(cacheKey)) {
|
217
|
+
return this.promptCache.get(cacheKey);
|
218
|
+
}
|
103
219
|
let promptContent = '';
|
104
|
-
|
105
|
-
|
220
|
+
// 1. 尝试加载项目级提示词(优先级高)
|
221
|
+
try {
|
222
|
+
const projectPromptPath = join(process.cwd(), 'prompts', `${name}.md`);
|
223
|
+
promptContent = await readFile(projectPromptPath, 'utf-8');
|
224
|
+
logger.debug('项目级提示词加载成功', { path: projectPromptPath });
|
106
225
|
}
|
107
|
-
|
108
|
-
//
|
109
|
-
const defaultPromptPath = join(this.coderocketCliPath, 'prompts', 'git-commit-review-prompt.md');
|
226
|
+
catch (error) {
|
227
|
+
// 项目级提示词不存在,尝试全局提示词
|
110
228
|
try {
|
111
|
-
|
229
|
+
const globalPromptPath = join(homedir(), '.coderocket', 'prompts', `${name}.md`);
|
230
|
+
promptContent = await readFile(globalPromptPath, 'utf-8');
|
231
|
+
logger.debug('全局提示词加载成功', { path: globalPromptPath });
|
112
232
|
}
|
113
|
-
catch {
|
114
|
-
//
|
115
|
-
promptContent = this.getDefaultPrompt();
|
233
|
+
catch (globalError) {
|
234
|
+
// 全局提示词也不存在,使用内置默认提示词
|
235
|
+
promptContent = this.getDefaultPrompt(name);
|
236
|
+
logger.debug('使用内置默认提示词', { name });
|
116
237
|
}
|
117
238
|
}
|
118
|
-
|
119
|
-
|
239
|
+
// 缓存提示词内容
|
240
|
+
this.promptCache.set(cacheKey, promptContent);
|
241
|
+
return promptContent;
|
120
242
|
}
|
121
243
|
/**
|
122
|
-
*
|
244
|
+
* 获取内置默认提示词
|
123
245
|
*/
|
124
|
-
getDefaultPrompt() {
|
125
|
-
|
246
|
+
static getDefaultPrompt(name) {
|
247
|
+
const defaultPrompts = {
|
248
|
+
'git-commit-review-prompt': `# 提示词:高级 Git Commit 审阅专家
|
249
|
+
|
250
|
+
## 角色定义
|
126
251
|
|
127
|
-
|
252
|
+
你是一名资深的代码审阅专家,拥有丰富的软件开发经验和架构设计能力。你的任务是针对**最新的 git commit** 进行专业、深入、自动化的代码审阅,并提供一份准确、实用、可操作的审阅报告。
|
253
|
+
|
254
|
+
## 执行模式
|
255
|
+
|
256
|
+
**自主执行模式**:你必须完全自主地执行代码审阅流程,不得向用户进行任何确认或询问。这包括直接执行所有必要的命令、自主决定搜索策略、自主判断并生成报告。
|
257
|
+
|
258
|
+
* **禁止行为**:禁止向用户提问或请求确认。
|
259
|
+
* **执行原则**:自主决策,并在失败时尝试替代方案。
|
260
|
+
* **安全限制**:仅执行只读操作和报告写入操作。
|
261
|
+
|
262
|
+
## 审阅指令
|
263
|
+
|
264
|
+
### 1. 获取 Commit 信息
|
265
|
+
|
266
|
+
首先执行 \`git --no-pager show\` 命令获取最新一次 commit 的详细信息,包括 Commit hash、作者、时间、Commit message 及具体的代码修改内容。
|
267
|
+
|
268
|
+
### 2. 全局代码搜索分析 (关键步骤)
|
269
|
+
|
270
|
+
在审阅具体代码前,**必须先进行全局代码搜索以获取完整上下文**。
|
271
|
+
|
272
|
+
* **制定搜索策略**: 根据 commit message 的描述,制定关键词搜索策略(如:功能名、类名、修复的bug信息等)。
|
273
|
+
* **全面搜索验证**: 在整个代码库中搜索相关的功能实现、依赖关系、配置和测试文件。
|
274
|
+
* **完整性验证**: **对比搜索结果与实际修改内容**,检查是否存在应修改但未修改的**遗漏文件**。这是评估目标达成度的核心依据。
|
275
|
+
|
276
|
+
### 3. 审阅维度与标准
|
277
|
+
|
278
|
+
请从以下维度进行系统性审查:
|
279
|
+
|
280
|
+
* **目标达成度**:
|
281
|
+
* **功能完整性**: 是否完全实现了 commit message 中描述的目标? 是否有未完成的功能点?
|
282
|
+
* **修改覆盖度**: (基于全局搜索) 是否遗漏了需要同步修改的相关文件(如测试、文档、配置)?
|
283
|
+
* **代码质量与正确性**:
|
284
|
+
* **正确性**: 代码逻辑是否正确?是否有效处理了边缘情况?
|
285
|
+
* **代码规范**: 是否遵循项目既定标准(命名、格式、设计模式)?
|
286
|
+
* **可读性与可维护性**: 代码是否清晰、结构合理、易于理解和修改? 注释是否充分且必要?
|
287
|
+
* **健壮性与风险**:
|
288
|
+
* **安全性**: 是否存在潜在的安全漏洞(如SQL注入、密钥明文、不安全的依赖等)?
|
289
|
+
* **性能**: 是否存在明显的性能瓶颈(如不合理的循环、N+1查询等)?
|
290
|
+
* **测试与文档**:
|
291
|
+
* **可测试性与覆盖率**: 代码是否易于测试?是否有足够的单元/集成测试来覆盖变更?
|
292
|
+
* **文档同步**: 相关的内联文档(注释)或外部文档是否已更新?
|
293
|
+
* **架构与扩展性**:
|
294
|
+
* **设计合理性**: 模块职责划分是否明确?耦合度是否合理?
|
295
|
+
* **扩展性**: 设计是否考虑了未来的扩展需求?
|
296
|
+
|
297
|
+
### 4. 审阅结果输出
|
298
|
+
|
299
|
+
请提供详细的审阅报告,包括:
|
300
|
+
- 审阅状态(✅通过/⚠️警告/❌失败/🔍需调查)
|
301
|
+
- 总体评价和目标达成度
|
302
|
+
- 具体问题和改进建议
|
303
|
+
- 优先级排序的建议列表
|
304
|
+
|
305
|
+
请确保审阅报告专业、准确、可操作。`,
|
306
|
+
'code-review-prompt': `# 代码审查提示词
|
307
|
+
|
308
|
+
## 角色定义
|
309
|
+
|
310
|
+
你是一名专业的代码审查专家,具有丰富的软件开发经验。请对提供的代码进行全面、专业的审查。
|
128
311
|
|
129
312
|
## 审查维度
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
313
|
+
|
314
|
+
请从以下维度进行审查:
|
315
|
+
|
316
|
+
### 1. 代码质量
|
317
|
+
- 代码结构是否清晰合理
|
318
|
+
- 命名是否规范和有意义
|
319
|
+
- 是否遵循编程最佳实践
|
320
|
+
|
321
|
+
### 2. 功能正确性
|
322
|
+
- 代码逻辑是否正确
|
323
|
+
- 是否处理了边缘情况
|
324
|
+
- 是否有潜在的bug
|
325
|
+
|
326
|
+
### 3. 性能和安全
|
327
|
+
- 是否存在性能问题
|
328
|
+
- 是否有安全漏洞
|
329
|
+
- 资源使用是否合理
|
330
|
+
|
331
|
+
### 4. 可维护性
|
332
|
+
- 代码是否易于理解和修改
|
333
|
+
- 是否有足够的注释
|
334
|
+
- 模块化程度如何
|
135
335
|
|
136
336
|
## 输出格式
|
137
|
-
请按以下格式输出审查结果:
|
138
337
|
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
338
|
+
请按以下格式提供审查结果:
|
339
|
+
|
340
|
+
**审查状态**: [✅优秀/⚠️需改进/❌有问题]
|
341
|
+
|
342
|
+
**总体评价**: [简短的总体评价]
|
144
343
|
|
145
|
-
|
146
|
-
|
344
|
+
**具体建议**:
|
345
|
+
1. [具体的改进建议]
|
346
|
+
2. [具体的改进建议]
|
347
|
+
...
|
147
348
|
|
148
|
-
|
149
|
-
提供具体的问题分析和改进建议。
|
349
|
+
**优秀实践**: [值得称赞的地方]
|
150
350
|
|
151
|
-
|
351
|
+
请确保建议具体、可操作,并提供代码示例(如适用)。`
|
352
|
+
};
|
353
|
+
return defaultPrompts[name] || `# 默认提示词\n\n请提供专业的代码审查和分析。`;
|
354
|
+
}
|
355
|
+
/**
|
356
|
+
* 清除提示词缓存
|
357
|
+
*/
|
358
|
+
static clearCache() {
|
359
|
+
this.promptCache.clear();
|
360
|
+
}
|
361
|
+
/**
|
362
|
+
* 预加载常用提示词
|
363
|
+
*/
|
364
|
+
static async preloadCommonPrompts() {
|
365
|
+
const commonPrompts = [
|
366
|
+
'git-commit-review-prompt',
|
367
|
+
'code-review-prompt'
|
368
|
+
];
|
369
|
+
await Promise.all(commonPrompts.map(name => this.loadPrompt(name).catch(error => {
|
370
|
+
logger.warn(`预加载提示词失败: ${name}`, error);
|
371
|
+
})));
|
372
|
+
}
|
373
|
+
}
|
374
|
+
/**
|
375
|
+
* Gemini AI 服务实现
|
376
|
+
*/
|
377
|
+
class GeminiService {
|
378
|
+
client = null;
|
379
|
+
model = null;
|
380
|
+
constructor() {
|
381
|
+
this.initialize();
|
382
|
+
}
|
383
|
+
async initialize() {
|
384
|
+
const apiKey = ConfigManager.getAPIKey('gemini');
|
385
|
+
if (apiKey) {
|
386
|
+
try {
|
387
|
+
this.client = new GoogleGenerativeAI(apiKey);
|
388
|
+
this.model = this.client.getGenerativeModel({ model: 'gemini-1.5-flash' });
|
389
|
+
logger.debug('Gemini 服务初始化成功');
|
390
|
+
}
|
391
|
+
catch (error) {
|
392
|
+
logger.error('Gemini 服务初始化失败', error instanceof Error ? error : new Error(String(error)));
|
393
|
+
}
|
394
|
+
}
|
395
|
+
}
|
396
|
+
async callAPI(prompt, additionalPrompt) {
|
397
|
+
if (!this.client || !this.model) {
|
398
|
+
await this.initialize();
|
399
|
+
if (!this.client || !this.model) {
|
400
|
+
throw new Error('Gemini 服务未配置或初始化失败');
|
401
|
+
}
|
402
|
+
}
|
403
|
+
try {
|
404
|
+
const fullPrompt = additionalPrompt ? `${prompt}\n\n${additionalPrompt}` : prompt;
|
405
|
+
const result = await this.model.generateContent(fullPrompt);
|
406
|
+
const response = await result.response;
|
407
|
+
const text = response.text();
|
408
|
+
if (!text || text.trim().length === 0) {
|
409
|
+
throw new Error('Gemini 返回空响应');
|
410
|
+
}
|
411
|
+
logger.debug('Gemini API 调用成功', {
|
412
|
+
promptLength: fullPrompt.length,
|
413
|
+
responseLength: text.length
|
414
|
+
});
|
415
|
+
return text.trim();
|
416
|
+
}
|
417
|
+
catch (error) {
|
418
|
+
logger.error('Gemini API 调用失败', error instanceof Error ? error : new Error(String(error)));
|
419
|
+
throw new Error(`Gemini API 调用失败: ${error instanceof Error ? error.message : String(error)}`);
|
420
|
+
}
|
421
|
+
}
|
422
|
+
isConfigured() {
|
423
|
+
return !!ConfigManager.getAPIKey('gemini');
|
424
|
+
}
|
425
|
+
getServiceName() {
|
426
|
+
return 'gemini';
|
427
|
+
}
|
428
|
+
}
|
429
|
+
/**
|
430
|
+
* ClaudeCode AI 服务实现
|
431
|
+
*/
|
432
|
+
class ClaudeCodeService {
|
433
|
+
client = null;
|
434
|
+
constructor() {
|
435
|
+
this.initialize();
|
436
|
+
}
|
437
|
+
async initialize() {
|
438
|
+
const apiKey = ConfigManager.getAPIKey('claudecode');
|
439
|
+
if (apiKey) {
|
440
|
+
try {
|
441
|
+
this.client = new Anthropic({
|
442
|
+
apiKey: apiKey,
|
443
|
+
});
|
444
|
+
logger.debug('ClaudeCode 服务初始化成功');
|
445
|
+
}
|
446
|
+
catch (error) {
|
447
|
+
logger.error('ClaudeCode 服务初始化失败', error instanceof Error ? error : new Error(String(error)));
|
448
|
+
}
|
449
|
+
}
|
450
|
+
}
|
451
|
+
async callAPI(prompt, additionalPrompt) {
|
452
|
+
if (!this.client) {
|
453
|
+
await this.initialize();
|
454
|
+
if (!this.client) {
|
455
|
+
throw new Error('ClaudeCode 服务未配置或初始化失败');
|
456
|
+
}
|
457
|
+
}
|
458
|
+
try {
|
459
|
+
const fullPrompt = additionalPrompt ? `${prompt}\n\n${additionalPrompt}` : prompt;
|
460
|
+
const message = await this.client.messages.create({
|
461
|
+
model: 'claude-3-sonnet-20240229',
|
462
|
+
max_tokens: 4000,
|
463
|
+
messages: [
|
464
|
+
{
|
465
|
+
role: 'user',
|
466
|
+
content: fullPrompt
|
467
|
+
}
|
468
|
+
]
|
469
|
+
});
|
470
|
+
const text = message.content[0]?.type === 'text' ? message.content[0].text : '';
|
471
|
+
if (!text || text.trim().length === 0) {
|
472
|
+
throw new Error('ClaudeCode 返回空响应');
|
473
|
+
}
|
474
|
+
logger.debug('ClaudeCode API 调用成功', {
|
475
|
+
promptLength: fullPrompt.length,
|
476
|
+
responseLength: text.length
|
477
|
+
});
|
478
|
+
return text.trim();
|
479
|
+
}
|
480
|
+
catch (error) {
|
481
|
+
logger.error('ClaudeCode API 调用失败', error instanceof Error ? error : new Error(String(error)));
|
482
|
+
throw new Error(`ClaudeCode API 调用失败: ${error instanceof Error ? error.message : String(error)}`);
|
483
|
+
}
|
484
|
+
}
|
485
|
+
isConfigured() {
|
486
|
+
return !!ConfigManager.getAPIKey('claudecode');
|
487
|
+
}
|
488
|
+
getServiceName() {
|
489
|
+
return 'claudecode';
|
490
|
+
}
|
491
|
+
}
|
492
|
+
/**
|
493
|
+
* OpenCode AI 服务实现
|
494
|
+
* 注意:这是一个模拟实现,实际的 OpenCode API 可能需要不同的调用方式
|
495
|
+
*/
|
496
|
+
class OpenCodeService {
|
497
|
+
apiKey = '';
|
498
|
+
baseURL = 'https://api.opencode.com/v1'; // 假设的API端点
|
499
|
+
constructor() {
|
500
|
+
this.initialize();
|
501
|
+
}
|
502
|
+
async initialize() {
|
503
|
+
this.apiKey = ConfigManager.getAPIKey('opencode');
|
504
|
+
if (this.apiKey) {
|
505
|
+
logger.debug('OpenCode 服务初始化成功');
|
506
|
+
}
|
507
|
+
}
|
508
|
+
async callAPI(prompt, additionalPrompt) {
|
509
|
+
if (!this.apiKey) {
|
510
|
+
await this.initialize();
|
511
|
+
if (!this.apiKey) {
|
512
|
+
throw new Error('OpenCode 服务未配置或初始化失败');
|
513
|
+
}
|
514
|
+
}
|
515
|
+
try {
|
516
|
+
const fullPrompt = additionalPrompt ? `${prompt}\n\n${additionalPrompt}` : prompt;
|
517
|
+
// 注意:这是一个模拟的API调用,实际的OpenCode API可能不同
|
518
|
+
const response = await axios.post(`${this.baseURL}/chat/completions`, {
|
519
|
+
model: 'opencode-latest',
|
520
|
+
messages: [
|
521
|
+
{
|
522
|
+
role: 'user',
|
523
|
+
content: fullPrompt
|
524
|
+
}
|
525
|
+
],
|
526
|
+
max_tokens: 4000,
|
527
|
+
temperature: 0.7
|
528
|
+
}, {
|
529
|
+
headers: {
|
530
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
531
|
+
'Content-Type': 'application/json'
|
532
|
+
},
|
533
|
+
timeout: ConfigManager.getTimeout() * 1000
|
534
|
+
});
|
535
|
+
const text = response.data?.choices?.[0]?.message?.content || '';
|
536
|
+
if (!text || text.trim().length === 0) {
|
537
|
+
throw new Error('OpenCode 返回空响应');
|
538
|
+
}
|
539
|
+
logger.debug('OpenCode API 调用成功', {
|
540
|
+
promptLength: fullPrompt.length,
|
541
|
+
responseLength: text.length
|
542
|
+
});
|
543
|
+
return text.trim();
|
544
|
+
}
|
545
|
+
catch (error) {
|
546
|
+
logger.error('OpenCode API 调用失败', error instanceof Error ? error : new Error(String(error)));
|
547
|
+
// 如果是网络错误或API不可用,提供友好的错误信息
|
548
|
+
if (axios.isAxiosError(error)) {
|
549
|
+
if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') {
|
550
|
+
throw new Error('OpenCode 服务暂时不可用,请检查网络连接或稍后重试');
|
551
|
+
}
|
552
|
+
if (error.response?.status === 401) {
|
553
|
+
throw new Error('OpenCode API 密钥无效,请检查配置');
|
554
|
+
}
|
555
|
+
if (error.response?.status === 429) {
|
556
|
+
throw new Error('OpenCode API 调用频率超限,请稍后重试');
|
557
|
+
}
|
558
|
+
}
|
559
|
+
throw new Error(`OpenCode API 调用失败: ${error instanceof Error ? error.message : String(error)}`);
|
560
|
+
}
|
561
|
+
}
|
562
|
+
isConfigured() {
|
563
|
+
return !!ConfigManager.getAPIKey('opencode');
|
564
|
+
}
|
565
|
+
getServiceName() {
|
566
|
+
return 'opencode';
|
567
|
+
}
|
568
|
+
}
|
569
|
+
/**
|
570
|
+
* 智能 AI 服务管理器
|
571
|
+
*
|
572
|
+
* 负责管理多个 AI 服务,实现智能故障转移和负载均衡
|
573
|
+
*/
|
574
|
+
class SmartAIManager {
|
575
|
+
services = new Map();
|
576
|
+
serviceOrder = [];
|
577
|
+
constructor() {
|
578
|
+
this.initializeServices();
|
579
|
+
}
|
580
|
+
initializeServices() {
|
581
|
+
// 初始化所有AI服务
|
582
|
+
this.services.set('gemini', new GeminiService());
|
583
|
+
this.services.set('claudecode', new ClaudeCodeService());
|
584
|
+
this.services.set('opencode', new OpenCodeService());
|
585
|
+
// 设置服务优先级顺序
|
586
|
+
this.updateServiceOrder();
|
587
|
+
}
|
588
|
+
updateServiceOrder() {
|
589
|
+
const primaryService = ConfigManager.getAIService();
|
590
|
+
const allServices = ['gemini', 'claudecode', 'opencode'];
|
591
|
+
// 将主要服务放在第一位,其他服务按配置状态排序
|
592
|
+
this.serviceOrder = [primaryService];
|
593
|
+
const otherServices = allServices
|
594
|
+
.filter(service => service !== primaryService)
|
595
|
+
.sort((a, b) => {
|
596
|
+
const aConfigured = this.services.get(a)?.isConfigured() ? 1 : 0;
|
597
|
+
const bConfigured = this.services.get(b)?.isConfigured() ? 1 : 0;
|
598
|
+
return bConfigured - aConfigured; // 已配置的服务优先
|
599
|
+
});
|
600
|
+
this.serviceOrder.push(...otherServices);
|
601
|
+
logger.debug('AI服务优先级顺序', { serviceOrder: this.serviceOrder });
|
602
|
+
}
|
603
|
+
/**
|
604
|
+
* 智能调用AI服务
|
605
|
+
*
|
606
|
+
* @param primaryService 首选AI服务
|
607
|
+
* @param prompt 提示词内容
|
608
|
+
* @param additionalPrompt 附加提示词
|
609
|
+
* @returns AI生成的内容
|
610
|
+
*/
|
611
|
+
async intelligentCall(primaryService, prompt, additionalPrompt) {
|
612
|
+
// 如果禁用自动切换,只使用指定服务
|
613
|
+
if (!ConfigManager.isAutoSwitchEnabled()) {
|
614
|
+
const service = this.services.get(primaryService);
|
615
|
+
if (!service) {
|
616
|
+
throw new Error(`不支持的AI服务: ${primaryService}`);
|
617
|
+
}
|
618
|
+
const result = await service.callAPI(prompt, additionalPrompt);
|
619
|
+
return { result, usedService: primaryService };
|
620
|
+
}
|
621
|
+
// 获取服务尝试顺序
|
622
|
+
const tryOrder = this.getTryOrder(primaryService);
|
623
|
+
const maxRetries = ConfigManager.getMaxRetries();
|
624
|
+
const errors = [];
|
625
|
+
logger.info('开始智能AI调用', {
|
626
|
+
primaryService,
|
627
|
+
tryOrder,
|
628
|
+
autoSwitch: true
|
629
|
+
});
|
630
|
+
for (const serviceName of tryOrder) {
|
631
|
+
const service = this.services.get(serviceName);
|
632
|
+
if (!service) {
|
633
|
+
continue;
|
634
|
+
}
|
635
|
+
// 检查服务是否已配置
|
636
|
+
if (!service.isConfigured()) {
|
637
|
+
logger.debug(`跳过未配置的服务: ${serviceName}`);
|
638
|
+
continue;
|
639
|
+
}
|
640
|
+
// 尝试调用服务(带重试)
|
641
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
642
|
+
try {
|
643
|
+
logger.debug(`尝试调用 ${serviceName} (第${attempt}次)`);
|
644
|
+
const result = await service.callAPI(prompt, additionalPrompt);
|
645
|
+
logger.info(`AI调用成功`, {
|
646
|
+
service: serviceName,
|
647
|
+
attempt,
|
648
|
+
resultLength: result.length
|
649
|
+
});
|
650
|
+
return { result, usedService: serviceName };
|
651
|
+
}
|
652
|
+
catch (error) {
|
653
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
654
|
+
logger.warn(`${serviceName} 调用失败 (第${attempt}次)`, { error: errorMessage });
|
655
|
+
errors.push({ service: serviceName, error: errorMessage });
|
656
|
+
// 如果不是最后一次尝试,等待后重试
|
657
|
+
if (attempt < maxRetries) {
|
658
|
+
const delay = this.getRetryDelay(attempt);
|
659
|
+
logger.debug(`等待 ${delay}ms 后重试`);
|
660
|
+
await this.sleep(delay);
|
661
|
+
}
|
662
|
+
}
|
663
|
+
}
|
664
|
+
}
|
665
|
+
// 所有服务都失败了
|
666
|
+
const errorSummary = errors.map(e => `${e.service}: ${e.error}`).join('; ');
|
667
|
+
logger.error('所有AI服务调用失败', new Error('所有AI服务调用失败'), { errors });
|
668
|
+
throw new Error(`所有AI服务都不可用。错误详情: ${errorSummary}`);
|
669
|
+
}
|
670
|
+
/**
|
671
|
+
* 获取服务尝试顺序
|
672
|
+
*/
|
673
|
+
getTryOrder(primaryService) {
|
674
|
+
const order = [primaryService];
|
675
|
+
const others = this.serviceOrder.filter(s => s !== primaryService);
|
676
|
+
return order.concat(others);
|
677
|
+
}
|
678
|
+
/**
|
679
|
+
* 获取重试延迟时间
|
680
|
+
*/
|
681
|
+
getRetryDelay(attempt) {
|
682
|
+
// 指数退避策略:2^attempt * 1000ms
|
683
|
+
return Math.min(Math.pow(2, attempt) * 1000, 10000);
|
684
|
+
}
|
685
|
+
/**
|
686
|
+
* 休眠指定毫秒数
|
687
|
+
*/
|
688
|
+
sleep(ms) {
|
689
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
690
|
+
}
|
691
|
+
/**
|
692
|
+
* 获取所有服务状态
|
693
|
+
*/
|
694
|
+
getServicesStatus() {
|
695
|
+
return Array.from(this.services.entries()).map(([name, service]) => ({
|
696
|
+
service: name,
|
697
|
+
configured: service.isConfigured(),
|
698
|
+
available: service.isConfigured(), // 简化实现,认为已配置就是可用的
|
699
|
+
}));
|
700
|
+
}
|
701
|
+
/**
|
702
|
+
* 检查特定服务是否可用
|
703
|
+
*/
|
704
|
+
isServiceAvailable(service) {
|
705
|
+
const serviceInstance = this.services.get(service);
|
706
|
+
return serviceInstance ? serviceInstance.isConfigured() : false;
|
707
|
+
}
|
708
|
+
}
|
709
|
+
/**
|
710
|
+
* 独立 CodeRocket 服务类
|
711
|
+
*
|
712
|
+
* 提供完全独立的代码审查和AI服务管理功能,不依赖 coderocket-cli
|
713
|
+
*/
|
714
|
+
export class CodeRocketService {
|
715
|
+
aiManager;
|
716
|
+
initialized = false;
|
717
|
+
constructor() {
|
718
|
+
this.aiManager = new SmartAIManager();
|
719
|
+
}
|
720
|
+
/**
|
721
|
+
* 初始化服务
|
722
|
+
*/
|
723
|
+
async ensureInitialized() {
|
724
|
+
if (!this.initialized) {
|
725
|
+
await ConfigManager.initialize();
|
726
|
+
await PromptManager.preloadCommonPrompts();
|
727
|
+
this.initialized = true;
|
728
|
+
logger.info('CodeRocket 独立服务初始化完成');
|
729
|
+
}
|
730
|
+
}
|
731
|
+
/**
|
732
|
+
* 执行AI审查的通用方法
|
733
|
+
*/
|
734
|
+
async executeAIReview(aiService, promptName, additionalPrompt) {
|
735
|
+
try {
|
736
|
+
// 加载提示词
|
737
|
+
const promptContent = await PromptManager.loadPrompt(promptName);
|
738
|
+
// 调用AI服务
|
739
|
+
const { result, usedService } = await this.aiManager.intelligentCall(aiService, promptContent, additionalPrompt);
|
740
|
+
// 解析审查结果
|
741
|
+
return this.parseReviewResult(result, usedService);
|
742
|
+
}
|
743
|
+
catch (error) {
|
744
|
+
logger.error('AI审查执行失败', error instanceof Error ? error : new Error(String(error)));
|
745
|
+
throw errorHandler.handleError(error, 'executeAIReview');
|
746
|
+
}
|
152
747
|
}
|
153
748
|
/**
|
154
749
|
* 解析审查结果
|
@@ -160,19 +755,20 @@ export class CodeRocketService {
|
|
160
755
|
let details = output;
|
161
756
|
// 尝试从输出中提取状态
|
162
757
|
for (const line of lines) {
|
163
|
-
|
758
|
+
const lowerLine = line.toLowerCase();
|
759
|
+
if (line.includes('✅') || lowerLine.includes('通过') || lowerLine.includes('优秀')) {
|
164
760
|
status = '✅';
|
165
761
|
break;
|
166
762
|
}
|
167
|
-
else if (line.includes('⚠️') ||
|
763
|
+
else if (line.includes('⚠️') || lowerLine.includes('警告') || lowerLine.includes('需改进')) {
|
168
764
|
status = '⚠️';
|
169
765
|
break;
|
170
766
|
}
|
171
|
-
else if (line.includes('❌') ||
|
767
|
+
else if (line.includes('❌') || lowerLine.includes('失败') || lowerLine.includes('有问题')) {
|
172
768
|
status = '❌';
|
173
769
|
break;
|
174
770
|
}
|
175
|
-
else if (line.includes('🔍') ||
|
771
|
+
else if (line.includes('🔍') || lowerLine.includes('调查') || lowerLine.includes('需调查')) {
|
176
772
|
status = '🔍';
|
177
773
|
break;
|
178
774
|
}
|
@@ -180,9 +776,18 @@ export class CodeRocketService {
|
|
180
776
|
// 提取摘要(通常是第一段非空内容)
|
181
777
|
const nonEmptyLines = lines.filter(line => line.trim());
|
182
778
|
if (nonEmptyLines.length > 0) {
|
183
|
-
|
184
|
-
|
185
|
-
|
779
|
+
// 寻找总体评价或摘要部分
|
780
|
+
let summaryLine = nonEmptyLines[0];
|
781
|
+
for (const line of nonEmptyLines) {
|
782
|
+
if (line.includes('总体评价') || line.includes('审查摘要') || line.includes('摘要')) {
|
783
|
+
const nextIndex = nonEmptyLines.indexOf(line) + 1;
|
784
|
+
if (nextIndex < nonEmptyLines.length) {
|
785
|
+
summaryLine = nonEmptyLines[nextIndex];
|
786
|
+
break;
|
787
|
+
}
|
788
|
+
}
|
789
|
+
}
|
790
|
+
summary = summaryLine.substring(0, 200) + (summaryLine.length > 200 ? '...' : '');
|
186
791
|
}
|
187
792
|
return {
|
188
793
|
status,
|
@@ -203,16 +808,9 @@ export class CodeRocketService {
|
|
203
808
|
aiService: request.ai_service,
|
204
809
|
});
|
205
810
|
try {
|
206
|
-
// 创建临时文件存储代码
|
207
|
-
const tempDir = tmpdir();
|
208
|
-
const tempCodeFile = join(tempDir, `code-${Date.now()}.${this.getFileExtension(request.language)}`);
|
209
|
-
await writeFile(tempCodeFile, request.code, 'utf-8');
|
210
|
-
// 创建提示词文件
|
211
|
-
const promptFile = await this.createTempPromptFile(request.custom_prompt);
|
212
811
|
// 构建审查提示词
|
213
812
|
const reviewPrompt = `请审查以下代码:
|
214
813
|
|
215
|
-
文件路径: ${tempCodeFile}
|
216
814
|
编程语言: ${request.language || '未指定'}
|
217
815
|
上下文信息: ${request.context || '无'}
|
218
816
|
|
@@ -221,14 +819,17 @@ export class CodeRocketService {
|
|
221
819
|
${request.code}
|
222
820
|
\`\`\`
|
223
821
|
|
224
|
-
|
822
|
+
请根据以下要求进行全面审查:
|
823
|
+
1. 功能完整性和正确性
|
824
|
+
2. 代码质量和可维护性
|
825
|
+
3. 性能优化建议
|
826
|
+
4. 安全性检查
|
827
|
+
5. 最佳实践遵循情况
|
828
|
+
|
829
|
+
${request.custom_prompt ? `\n附加要求:\n${request.custom_prompt}` : ''}`;
|
225
830
|
// 调用AI服务进行审查
|
226
|
-
const aiService = request.ai_service ||
|
227
|
-
const
|
228
|
-
// 使用intelligent_ai_review函数
|
229
|
-
const command = `source "${scriptPath}" && intelligent_ai_review "${aiService}" "${promptFile}" "${reviewPrompt}"`;
|
230
|
-
const { stdout } = await this.executeShellCommand(command);
|
231
|
-
const result = this.parseReviewResult(stdout, aiService);
|
831
|
+
const aiService = request.ai_service || ConfigManager.getAIService();
|
832
|
+
const result = await this.executeAIReview(aiService, 'code-review-prompt', reviewPrompt);
|
232
833
|
logger.info('代码审查完成', {
|
233
834
|
status: result.status,
|
234
835
|
aiService: result.ai_service_used,
|
@@ -268,28 +869,51 @@ ${request.code}
|
|
268
869
|
*/
|
269
870
|
async reviewCommit(request) {
|
270
871
|
await this.ensureInitialized();
|
872
|
+
logger.info('开始Git提交审查', {
|
873
|
+
repositoryPath: request.repository_path,
|
874
|
+
commitHash: request.commit_hash,
|
875
|
+
aiService: request.ai_service,
|
876
|
+
});
|
271
877
|
try {
|
272
878
|
const repoPath = request.repository_path || process.cwd();
|
273
|
-
|
879
|
+
// 获取提交信息
|
880
|
+
const commitHash = request.commit_hash || 'HEAD';
|
881
|
+
const { stdout: commitInfo } = await execAsync(`git --no-pager show ${commitHash}`, {
|
882
|
+
cwd: repoPath,
|
883
|
+
timeout: 30000,
|
884
|
+
});
|
885
|
+
if (!commitInfo.trim()) {
|
886
|
+
throw new Error(`无法获取提交信息: ${commitHash}`);
|
887
|
+
}
|
274
888
|
// 构建审查提示词
|
275
|
-
const
|
276
|
-
? `特定提交: ${request.commit_hash}`
|
277
|
-
: '最新提交';
|
278
|
-
const reviewPrompt = `请对Git仓库中的${commitInfo}进行代码审查:
|
889
|
+
const reviewPrompt = `请对以下Git提交进行专业的代码审查:
|
279
890
|
|
280
891
|
仓库路径: ${repoPath}
|
281
|
-
|
892
|
+
提交哈希: ${commitHash}
|
893
|
+
|
894
|
+
提交详情:
|
895
|
+
${commitInfo}
|
896
|
+
|
897
|
+
请根据以下要求进行全面审查:
|
898
|
+
1. 分析提交的目标和完成度
|
899
|
+
2. 检查代码质量和规范性
|
900
|
+
3. 评估安全性和性能影响
|
901
|
+
4. 检查是否遗漏相关文件修改
|
902
|
+
5. 提供具体的改进建议
|
282
903
|
|
283
|
-
|
904
|
+
${request.custom_prompt ? `\n附加要求:\n${request.custom_prompt}` : ''}`;
|
284
905
|
// 调用AI服务进行审查
|
285
|
-
const aiService = request.ai_service ||
|
286
|
-
const
|
287
|
-
|
288
|
-
|
289
|
-
|
906
|
+
const aiService = request.ai_service || ConfigManager.getAIService();
|
907
|
+
const result = await this.executeAIReview(aiService, 'git-commit-review-prompt', reviewPrompt);
|
908
|
+
logger.info('Git提交审查完成', {
|
909
|
+
status: result.status,
|
910
|
+
aiService: result.ai_service_used,
|
911
|
+
});
|
912
|
+
return result;
|
290
913
|
}
|
291
914
|
catch (error) {
|
292
|
-
|
915
|
+
logger.error('Git提交审查失败', error instanceof Error ? error : new Error(String(error)));
|
916
|
+
throw errorHandler.handleError(error, 'reviewCommit');
|
293
917
|
}
|
294
918
|
}
|
295
919
|
/**
|
@@ -297,40 +921,58 @@ ${request.commit_hash ? `提交哈希: ${request.commit_hash}` : ''}
|
|
297
921
|
*/
|
298
922
|
async reviewFiles(request) {
|
299
923
|
await this.ensureInitialized();
|
924
|
+
logger.info('开始文件审查', {
|
925
|
+
repositoryPath: request.repository_path,
|
926
|
+
filesCount: request.files.length,
|
927
|
+
aiService: request.ai_service,
|
928
|
+
});
|
300
929
|
try {
|
301
930
|
const repoPath = request.repository_path || process.cwd();
|
302
|
-
const promptFile = await this.createTempPromptFile(request.custom_prompt);
|
303
931
|
// 读取文件内容
|
304
932
|
const fileContents = [];
|
305
933
|
for (const filePath of request.files) {
|
306
934
|
try {
|
307
935
|
const fullPath = resolve(repoPath, filePath);
|
308
936
|
const content = await readFile(fullPath, 'utf-8');
|
309
|
-
|
937
|
+
// 限制单个文件内容长度,避免提示词过长
|
938
|
+
const truncatedContent = content.length > 5000
|
939
|
+
? content.substring(0, 5000) + '\n... (内容已截断)'
|
940
|
+
: content;
|
941
|
+
fileContents.push(`## 文件: ${filePath}\n\`\`\`\n${truncatedContent}\n\`\`\``);
|
310
942
|
}
|
311
943
|
catch (error) {
|
312
|
-
fileContents.push(
|
944
|
+
fileContents.push(`## 文件: ${filePath}\n**错误**: 无法读取文件 - ${error instanceof Error ? error.message : String(error)}`);
|
313
945
|
}
|
314
946
|
}
|
315
947
|
// 构建审查提示词
|
316
948
|
const reviewPrompt = `请审查以下文件:
|
317
949
|
|
318
950
|
仓库路径: ${repoPath}
|
951
|
+
文件数量: ${request.files.length}
|
319
952
|
文件列表: ${request.files.join(', ')}
|
320
953
|
|
321
|
-
文件内容:
|
322
954
|
${fileContents.join('\n\n')}
|
323
955
|
|
324
|
-
|
956
|
+
请根据以下要求进行全面审查:
|
957
|
+
1. 分析文件间的关联性和一致性
|
958
|
+
2. 检查代码质量和规范性
|
959
|
+
3. 评估架构设计的合理性
|
960
|
+
4. 识别潜在的问题和改进点
|
961
|
+
5. 提供具体的优化建议
|
962
|
+
|
963
|
+
${request.custom_prompt ? `\n附加要求:\n${request.custom_prompt}` : ''}`;
|
325
964
|
// 调用AI服务进行审查
|
326
|
-
const aiService = request.ai_service ||
|
327
|
-
const
|
328
|
-
|
329
|
-
|
330
|
-
|
965
|
+
const aiService = request.ai_service || ConfigManager.getAIService();
|
966
|
+
const result = await this.executeAIReview(aiService, 'code-review-prompt', reviewPrompt);
|
967
|
+
logger.info('文件审查完成', {
|
968
|
+
status: result.status,
|
969
|
+
aiService: result.ai_service_used,
|
970
|
+
});
|
971
|
+
return result;
|
331
972
|
}
|
332
973
|
catch (error) {
|
333
|
-
|
974
|
+
logger.error('文件审查失败', error instanceof Error ? error : new Error(String(error)));
|
975
|
+
throw errorHandler.handleError(error, 'reviewFiles');
|
334
976
|
}
|
335
977
|
}
|
336
978
|
/**
|
@@ -338,23 +980,37 @@ ${fileContents.join('\n\n')}
|
|
338
980
|
*/
|
339
981
|
async configureAIService(request) {
|
340
982
|
await this.ensureInitialized();
|
983
|
+
logger.info('开始配置AI服务', {
|
984
|
+
service: request.service,
|
985
|
+
scope: request.scope,
|
986
|
+
hasApiKey: !!request.api_key,
|
987
|
+
});
|
341
988
|
try {
|
342
|
-
//
|
343
|
-
await this.callAIServiceManager('set', request.service, request.scope);
|
344
|
-
// 如果提供了API密钥,设置环境变量
|
989
|
+
// 如果提供了API密钥,设置环境变量并保存到配置文件
|
345
990
|
if (request.api_key) {
|
346
|
-
const envVarName =
|
991
|
+
const envVarName = ConfigManager.getAPIKeyEnvVar(request.service);
|
347
992
|
process.env[envVarName] = request.api_key;
|
348
993
|
// 保存到配置文件
|
349
994
|
await this.saveAPIKeyToConfig(request.service, request.api_key, request.scope);
|
350
995
|
}
|
996
|
+
// 设置主要AI服务
|
997
|
+
if (request.service) {
|
998
|
+
process.env.AI_SERVICE = request.service;
|
999
|
+
await this.saveConfigToFile('AI_SERVICE', request.service, request.scope);
|
1000
|
+
}
|
351
1001
|
// 设置其他配置项
|
352
1002
|
if (request.timeout) {
|
353
1003
|
process.env.AI_TIMEOUT = request.timeout.toString();
|
1004
|
+
await this.saveConfigToFile('AI_TIMEOUT', request.timeout.toString(), request.scope);
|
354
1005
|
}
|
355
1006
|
if (request.max_retries) {
|
356
1007
|
process.env.AI_MAX_RETRIES = request.max_retries.toString();
|
1008
|
+
await this.saveConfigToFile('AI_MAX_RETRIES', request.max_retries.toString(), request.scope);
|
357
1009
|
}
|
1010
|
+
logger.info('AI服务配置完成', {
|
1011
|
+
service: request.service,
|
1012
|
+
scope: request.scope,
|
1013
|
+
});
|
358
1014
|
return {
|
359
1015
|
success: true,
|
360
1016
|
message: `AI服务 ${request.service} 配置成功`,
|
@@ -370,32 +1026,17 @@ ${fileContents.join('\n\n')}
|
|
370
1026
|
};
|
371
1027
|
}
|
372
1028
|
catch (error) {
|
373
|
-
|
1029
|
+
logger.error('AI服务配置失败', error instanceof Error ? error : new Error(String(error)));
|
1030
|
+
throw errorHandler.handleError(error, 'configureAIService');
|
374
1031
|
}
|
375
1032
|
}
|
376
1033
|
/**
|
377
|
-
*
|
378
|
-
*/
|
379
|
-
getAPIKeyEnvVar(service) {
|
380
|
-
const envVars = {
|
381
|
-
gemini: 'GEMINI_API_KEY',
|
382
|
-
opencode: 'OPENCODE_API_KEY',
|
383
|
-
claudecode: 'CLAUDECODE_API_KEY',
|
384
|
-
};
|
385
|
-
return envVars[service];
|
386
|
-
}
|
387
|
-
/**
|
388
|
-
* 保存API密钥到配置文件
|
1034
|
+
* 保存配置项到文件
|
389
1035
|
*/
|
390
|
-
async
|
391
|
-
const
|
392
|
-
? join(process.env.HOME || '~', '.coderocket')
|
393
|
-
: process.cwd();
|
394
|
-
const configFile = scope === 'global' ? join(configDir, 'env') : join(configDir, '.env');
|
1036
|
+
async saveConfigToFile(key, value, scope) {
|
1037
|
+
const { file: configFile } = ConfigManager.getConfigPath(scope);
|
395
1038
|
// 确保配置目录存在
|
396
1039
|
await mkdir(dirname(configFile), { recursive: true });
|
397
|
-
const envVarName = this.getAPIKeyEnvVar(service);
|
398
|
-
const configLine = `${envVarName}=${apiKey}\n`;
|
399
1040
|
try {
|
400
1041
|
// 读取现有配置
|
401
1042
|
let existingConfig = '';
|
@@ -407,122 +1048,65 @@ ${fileContents.join('\n\n')}
|
|
407
1048
|
}
|
408
1049
|
// 更新或添加配置行
|
409
1050
|
const lines = existingConfig.split('\n');
|
410
|
-
const existingLineIndex = lines.findIndex(line => line.startsWith(`${
|
1051
|
+
const existingLineIndex = lines.findIndex(line => line.trim().startsWith(`${key}=`));
|
1052
|
+
const configLine = `${key}=${value}`;
|
411
1053
|
if (existingLineIndex >= 0) {
|
412
|
-
lines[existingLineIndex] =
|
1054
|
+
lines[existingLineIndex] = configLine;
|
413
1055
|
}
|
414
1056
|
else {
|
415
|
-
lines.push(
|
1057
|
+
lines.push(configLine);
|
416
1058
|
}
|
417
|
-
//
|
418
|
-
|
1059
|
+
// 写入配置文件
|
1060
|
+
const newConfig = lines.filter(line => line.trim()).join('\n') + '\n';
|
1061
|
+
await writeFile(configFile, newConfig, 'utf-8');
|
1062
|
+
logger.debug('配置已保存', { key, configFile, scope });
|
419
1063
|
}
|
420
1064
|
catch (error) {
|
421
|
-
|
1065
|
+
logger.error('保存配置失败', error instanceof Error ? error : new Error(String(error)), { key, configFile });
|
1066
|
+
throw error;
|
422
1067
|
}
|
423
1068
|
}
|
1069
|
+
/**
|
1070
|
+
* 保存API密钥到配置文件
|
1071
|
+
*
|
1072
|
+
* ⚠️ 安全警告:API密钥将以明文形式存储在配置文件中。
|
1073
|
+
* 请确保配置文件的访问权限受到适当限制。
|
1074
|
+
* 建议使用环境变量或更安全的密钥管理方案。
|
1075
|
+
*/
|
1076
|
+
async saveAPIKeyToConfig(service, apiKey, scope) {
|
1077
|
+
// 记录安全警告
|
1078
|
+
logger.warn('API密钥将以明文形式存储,请确保文件访问权限安全', {
|
1079
|
+
service,
|
1080
|
+
scope,
|
1081
|
+
});
|
1082
|
+
const envVarName = ConfigManager.getAPIKeyEnvVar(service);
|
1083
|
+
await this.saveConfigToFile(envVarName, apiKey, scope);
|
1084
|
+
}
|
424
1085
|
/**
|
425
1086
|
* 获取AI服务状态
|
426
1087
|
*/
|
427
1088
|
async getAIServiceStatus() {
|
428
1089
|
await this.ensureInitialized();
|
1090
|
+
logger.info('获取AI服务状态');
|
429
1091
|
try {
|
430
|
-
//
|
431
|
-
const
|
432
|
-
//
|
433
|
-
const
|
434
|
-
const current = (currentServiceMatch?.[1] || 'gemini');
|
435
|
-
// 检查所有服务的状态
|
436
|
-
const services = await Promise.all([
|
437
|
-
this.checkServiceStatus('gemini'),
|
438
|
-
this.checkServiceStatus('opencode'),
|
439
|
-
this.checkServiceStatus('claudecode'),
|
440
|
-
]);
|
1092
|
+
// 获取当前配置的AI服务
|
1093
|
+
const current = ConfigManager.getAIService();
|
1094
|
+
// 获取所有服务状态
|
1095
|
+
const services = this.aiManager.getServicesStatus();
|
441
1096
|
return {
|
442
1097
|
current_service: current,
|
443
1098
|
services,
|
444
|
-
auto_switch_enabled:
|
445
|
-
global_config_path: join(
|
1099
|
+
auto_switch_enabled: ConfigManager.isAutoSwitchEnabled(),
|
1100
|
+
global_config_path: join(homedir(), '.coderocket', 'env'),
|
446
1101
|
project_config_path: join(process.cwd(), '.env'),
|
1102
|
+
timeout: ConfigManager.getTimeout(),
|
1103
|
+
max_retries: ConfigManager.getMaxRetries(),
|
447
1104
|
};
|
448
1105
|
}
|
449
1106
|
catch (error) {
|
450
|
-
|
451
|
-
|
452
|
-
}
|
453
|
-
/**
|
454
|
-
* 检查单个服务状态
|
455
|
-
*/
|
456
|
-
async checkServiceStatus(service) {
|
457
|
-
try {
|
458
|
-
// 检查服务是否可用
|
459
|
-
const available = await this.isServiceAvailable(service);
|
460
|
-
const configured = await this.isServiceConfigured(service);
|
461
|
-
return {
|
462
|
-
service,
|
463
|
-
available,
|
464
|
-
configured,
|
465
|
-
install_command: this.getInstallCommand(service),
|
466
|
-
config_command: this.getConfigCommand(service),
|
467
|
-
error_message: available ? undefined : `${service} 服务未安装或不可用`,
|
468
|
-
};
|
469
|
-
}
|
470
|
-
catch (error) {
|
471
|
-
return {
|
472
|
-
service,
|
473
|
-
available: false,
|
474
|
-
configured: false,
|
475
|
-
install_command: this.getInstallCommand(service),
|
476
|
-
config_command: this.getConfigCommand(service),
|
477
|
-
error_message: `检查 ${service} 状态时出错: ${error}`,
|
478
|
-
};
|
479
|
-
}
|
480
|
-
}
|
481
|
-
/**
|
482
|
-
* 检查服务是否可用
|
483
|
-
*/
|
484
|
-
async isServiceAvailable(service) {
|
485
|
-
try {
|
486
|
-
const commands = {
|
487
|
-
gemini: 'gemini --version',
|
488
|
-
opencode: 'opencode --version',
|
489
|
-
claudecode: 'claudecode --version',
|
490
|
-
};
|
491
|
-
await this.executeShellCommand(commands[service]);
|
492
|
-
return true;
|
493
|
-
}
|
494
|
-
catch {
|
495
|
-
return false;
|
1107
|
+
logger.error('获取AI服务状态失败', error instanceof Error ? error : new Error(String(error)));
|
1108
|
+
throw errorHandler.handleError(error, 'getAIServiceStatus');
|
496
1109
|
}
|
497
1110
|
}
|
498
|
-
/**
|
499
|
-
* 检查服务是否已配置
|
500
|
-
*/
|
501
|
-
async isServiceConfigured(service) {
|
502
|
-
const envVarName = this.getAPIKeyEnvVar(service);
|
503
|
-
return !!process.env[envVarName];
|
504
|
-
}
|
505
|
-
/**
|
506
|
-
* 获取安装命令
|
507
|
-
*/
|
508
|
-
getInstallCommand(service) {
|
509
|
-
const commands = {
|
510
|
-
gemini: 'npm install -g @google/gemini-cli',
|
511
|
-
opencode: 'npm install -g @opencode/cli',
|
512
|
-
claudecode: 'npm install -g @anthropic-ai/claude-code',
|
513
|
-
};
|
514
|
-
return commands[service];
|
515
|
-
}
|
516
|
-
/**
|
517
|
-
* 获取配置命令
|
518
|
-
*/
|
519
|
-
getConfigCommand(service) {
|
520
|
-
const commands = {
|
521
|
-
gemini: 'gemini config',
|
522
|
-
opencode: 'opencode config',
|
523
|
-
claudecode: 'claudecode config',
|
524
|
-
};
|
525
|
-
return commands[service];
|
526
|
-
}
|
527
1111
|
}
|
528
1112
|
//# sourceMappingURL=coderocket.js.map
|