UNPKG

@yeepay/coderocket-mcp

Version:

CodeRocket MCP - Independent AI-powered code review server for Model Context Protocol

651 lines (647 loc) 24.6 kB
import { execSync } from 'child_process'; import { readFile, writeFile, mkdir } from 'fs/promises'; import { join } from 'path'; import { existsSync } from 'fs'; import { ConfigManager } from '../config/ConfigManager.js'; import { PromptManager } from '../prompts/PromptManager.js'; import { SmartAIManager } from '../ai/SmartAIManager.js'; import { GitError, FileError, logger } from '../logger.js'; /** * CodeRocket 核心服务类 * * 提供所有MCP工具的实现: * - 代码片段审查 * - Git变更审查 * - Git提交审查 * - 文件批量审查 * - AI服务配置管理 * - 服务状态监控 */ export class CodeRocketService { aiManager; constructor() { // 确保配置和提示词系统已初始化 this.ensureInitialized(); this.aiManager = new SmartAIManager(); } /** * 确保所有依赖系统已初始化 */ ensureInitialized() { if (!ConfigManager.isInitialized()) { throw new Error('ConfigManager 未初始化,请先调用 ConfigManager.initialize()'); } } /** * 审查代码片段 */ async reviewCode(request) { logger.info('开始代码片段审查', { codeLength: request.code.length, language: request.language, hasContext: !!request.context, }); try { // 构建审查提示词 const prompt = this.buildCodeReviewPrompt(request); // 确定使用的AI服务 const aiService = request.ai_service || ConfigManager.getAIService(); // 调用AI服务进行审查 const { result, usedService } = await this.aiManager.intelligentCall(aiService, prompt); const response = { status: '✅', summary: '代码审查完成', review: result, ai_service_used: usedService, timestamp: new Date().toISOString(), }; logger.info('代码片段审查完成', { aiService: usedService, reviewLength: result.length, }); return response; } catch (error) { logger.error('代码片段审查失败', error instanceof Error ? error : new Error(String(error))); return { status: '❌', summary: '代码审查失败', review: `审查过程中发生错误: ${error instanceof Error ? error.message : String(error)}`, ai_service_used: request.ai_service || ConfigManager.getAIService(), timestamp: new Date().toISOString(), }; } } /** * 审查Git变更 */ async reviewChanges(request) { logger.info('开始Git变更审查', { repositoryPath: request.repository_path, includeStaged: request.include_staged, includeUnstaged: request.include_unstaged, }); try { const repoPath = request.repository_path || process.cwd(); // 安全检查:验证路径 if (!this.isValidPath(repoPath)) { throw new GitError('无效的仓库路径'); } // 获取Git变更 const changes = await this.getGitChanges(repoPath, request); if (!changes.trim()) { return { status: '📝', summary: '没有发现变更', review: '当前没有需要审查的代码变更。', ai_service_used: request.ai_service || ConfigManager.getAIService(), timestamp: new Date().toISOString(), }; } // 构建审查提示词 const prompt = this.buildChangesReviewPrompt(changes, request); // 确定使用的AI服务 const aiService = request.ai_service || ConfigManager.getAIService(); // 调用AI服务进行审查 const { result, usedService } = await this.aiManager.intelligentCall(aiService, prompt); const response = { status: '✅', summary: 'Git变更审查完成', review: result, ai_service_used: usedService, timestamp: new Date().toISOString(), }; logger.info('Git变更审查完成', { aiService: usedService, changesLength: changes.length, }); return response; } catch (error) { logger.error('Git变更审查失败', error instanceof Error ? error : new Error(String(error))); return { status: '❌', summary: 'Git变更审查失败', review: `审查过程中发生错误: ${error instanceof Error ? error.message : String(error)}`, ai_service_used: request.ai_service || ConfigManager.getAIService(), timestamp: new Date().toISOString(), }; } } /** * 路径安全验证 */ isValidPath(path) { // 基本的路径安全检查 if (!path || typeof path !== 'string') return false; if (path.includes('..')) return false; // 防止路径遍历 if (path.includes('\0')) return false; // 防止空字节注入 return true; } /** * 检查是否为Git仓库 */ async checkGitRepository(repositoryPath = process.cwd()) { try { const result = execSync('git rev-parse --git-dir', { cwd: repositoryPath, encoding: 'utf-8', timeout: 5000, }); return result.trim() !== ''; } catch (error) { return false; } } /** * 安全地执行Git命令 */ async executeGitCommand(command, repositoryPath) { try { const result = execSync(command, { cwd: repositoryPath, encoding: 'utf-8', timeout: 30000, maxBuffer: 10 * 1024 * 1024, }); return result; } catch (error) { throw new GitError(`Git命令执行失败: ${command} - ${error instanceof Error ? error.message : String(error)}`); } } /** * 解析Git状态输出字符串(用于测试) */ parseGitStatus(statusOutput) { const lines = statusOutput.trim().split('\n').filter(line => line.trim()); return lines.map(line => { const status = line.substring(0, 2); const path = line.substring(3); return { path, status, statusDescription: this.getGitStatusDescription(status) }; }); } /** * 获取Git状态描述 */ getGitStatusDescription(status) { const statusMap = { 'M ': '已修改(已暂存)', ' M': '已修改(未暂存)', 'A ': '新增文件(已暂存)', ' A': '新增文件(未暂存)', 'D ': '已删除(已暂存)', ' D': '已删除(未暂存)', 'R ': '重命名(已暂存)', ' R': '重命名(未暂存)', 'C ': '复制(已暂存)', ' C': '复制(未暂存)', 'U ': '未合并(已暂存)', ' U': '未合并(未暂存)', '??': '未跟踪文件', '!!': '已忽略文件' }; return statusMap[status] || '未知状态'; } /** * 解析Git仓库状态 */ async parseGitRepositoryStatus(repositoryPath = process.cwd()) { try { const result = await this.executeGitCommand('git status --porcelain', repositoryPath); const lines = result.split('\n').filter(line => line.trim()); const staged = []; const unstaged = []; const untracked = []; for (const line of lines) { const status = line.substring(0, 2); const file = line.substring(3); if (status[0] !== ' ' && status[0] !== '?') { staged.push(file); } if (status[1] !== ' ' && status[1] !== '?') { unstaged.push(file); } if (status === '??') { untracked.push(file); } } return { staged, unstaged, untracked }; } catch (error) { throw new GitError(`解析Git状态失败: ${error instanceof Error ? error.message : String(error)}`); } } /** * 获取Git变更(安全版本) */ async getGitChanges(repoPath, request) { const commands = []; // 构建安全的Git命令 if (request.include_staged !== false) { commands.push('git diff --cached'); } if (request.include_unstaged !== false) { commands.push('git diff'); } let allChanges = ''; for (const command of commands) { try { const result = execSync(command, { cwd: repoPath, encoding: 'utf-8', timeout: 30000, // 30秒超时 maxBuffer: 10 * 1024 * 1024, // 10MB 缓冲区限制 }); allChanges += result + '\n'; } catch (error) { logger.warn(`Git命令执行失败: ${command}`, { error: error instanceof Error ? error.message : String(error) }); } } return allChanges; } /** * 构建代码审查提示词 */ buildCodeReviewPrompt(request) { const variables = { code: request.code, language: request.language || '未指定', context: request.context || '无额外上下文', }; return PromptManager.buildPrompt('review_code', request.code, request.custom_prompt, ConfigManager.getAILanguage()); } /** * 构建变更审查提示词 */ buildChangesReviewPrompt(changes, request) { // 如果传入的是对象(用于测试),构建详细的提示词 if (typeof changes === 'object' && changes.files) { const fileCount = changes.files.length; const fileList = changes.files.map((f) => `- ${f.path} (${f.statusDescription})`).join('\n'); let prompt = `请审查以下Git变更: 变更文件数量: ${fileCount} 变更文件列表: ${fileList} 变更内容: \`\`\`diff ${changes.diff} \`\`\` Git状态输出: \`\`\` ${changes.statusOutput} \`\`\` `; if (request.custom_prompt) { prompt += `\n特殊要求:${request.custom_prompt}\n`; } prompt += '\n请务必使用中文回复。'; return prompt; } // 原有的字符串处理逻辑 return PromptManager.buildPrompt('review_changes', changes, request.custom_prompt, ConfigManager.getAILanguage()); } /** * 审查Git提交 */ async reviewCommit(request) { logger.info('开始Git提交审查', { repositoryPath: request.repository_path, commitHash: request.commit_hash, }); try { const repoPath = request.repository_path || process.cwd(); // 安全检查:验证路径和提交哈希 if (!this.isValidPath(repoPath)) { throw new GitError('无效的仓库路径'); } if (request.commit_hash && !this.isValidCommitHash(request.commit_hash)) { throw new GitError('无效的提交哈希'); } // 获取提交信息 const commitInfo = await this.getCommitInfo(repoPath, request.commit_hash); if (!commitInfo.trim()) { return { status: '📝', summary: '没有找到提交信息', review: '无法获取指定的提交信息。', ai_service_used: request.ai_service || ConfigManager.getAIService(), timestamp: new Date().toISOString(), }; } // 构建审查提示词 const prompt = this.buildCommitReviewPrompt(commitInfo, request); // 确定使用的AI服务 const aiService = request.ai_service || ConfigManager.getAIService(); // 调用AI服务进行审查 const { result, usedService } = await this.aiManager.intelligentCall(aiService, prompt); const response = { status: '✅', summary: 'Git提交审查完成', review: result, ai_service_used: usedService, timestamp: new Date().toISOString(), }; logger.info('Git提交审查完成', { aiService: usedService, commitHash: request.commit_hash, }); return response; } catch (error) { logger.error('Git提交审查失败', error instanceof Error ? error : new Error(String(error))); return { status: '❌', summary: 'Git提交审查失败', review: `审查过程中发生错误: ${error instanceof Error ? error.message : String(error)}`, ai_service_used: request.ai_service || ConfigManager.getAIService(), timestamp: new Date().toISOString(), }; } } /** * 验证提交哈希格式 */ isValidCommitHash(hash) { // Git提交哈希应该是40个十六进制字符,或者是短哈希(7-40个字符) const hashRegex = /^[a-f0-9]{7,40}$/i; return hashRegex.test(hash); } /** * 获取提交信息(安全版本) */ async getCommitInfo(repoPath, commitHash) { try { const hash = commitHash || 'HEAD'; const command = `git show ${hash} --pretty=fuller --stat`; const result = execSync(command, { cwd: repoPath, encoding: 'utf-8', timeout: 30000, // 30秒超时 maxBuffer: 10 * 1024 * 1024, // 10MB 缓冲区限制 }); return result; } catch (error) { throw new GitError(`获取提交信息失败: ${error instanceof Error ? error.message : String(error)}`); } } /** * 构建提交审查提示词 */ buildCommitReviewPrompt(commitInfo, request) { return PromptManager.buildPrompt('review_commit', commitInfo, request.custom_prompt, ConfigManager.getAILanguage()); } /** * 审查多个文件 */ async reviewFiles(request) { logger.info('开始文件批量审查', { fileCount: request.files.length, repositoryPath: request.repository_path, }); try { const repoPath = request.repository_path || process.cwd(); // 安全检查:验证路径 if (!this.isValidPath(repoPath)) { throw new FileError('无效的仓库路径'); } // 读取并处理文件内容 const fileContents = await this.readMultipleFiles(request.files, repoPath); if (fileContents.length === 0) { return { status: '📝', summary: '没有找到可读取的文件', review: '指定的文件都无法读取或不存在。', ai_service_used: request.ai_service || ConfigManager.getAIService(), timestamp: new Date().toISOString(), }; } // 构建审查内容 const reviewContent = this.buildFileReviewContent(fileContents); // 构建审查提示词 const prompt = this.buildFilesReviewPrompt(reviewContent, request); // 确定使用的AI服务 const aiService = request.ai_service || ConfigManager.getAIService(); // 调用AI服务进行审查 const { result, usedService } = await this.aiManager.intelligentCall(aiService, prompt); const response = { status: '✅', summary: '文件批量审查完成', review: result, ai_service_used: usedService, timestamp: new Date().toISOString(), }; logger.info('文件批量审查完成', { aiService: usedService, fileCount: fileContents.length, }); return response; } catch (error) { logger.error('文件批量审查失败', error instanceof Error ? error : new Error(String(error))); return { status: '❌', summary: '文件批量审查失败', review: `审查过程中发生错误: ${error instanceof Error ? error.message : String(error)}`, ai_service_used: request.ai_service || ConfigManager.getAIService(), timestamp: new Date().toISOString(), }; } } /** * 读取多个文件内容(安全版本) */ async readMultipleFiles(files, basePath) { const results = []; const maxFileSize = 1024 * 1024; // 1MB 限制 const charLimit = parseInt(ConfigManager.get('FILE_CONTENT_CHAR_LIMIT', '5000'), 10); for (const file of files) { try { // 安全检查:验证文件路径 if (!this.isValidPath(file)) { results.push({ path: file, content: '', error: '无效的文件路径', }); continue; } const filePath = join(basePath, file); // 检查文件是否存在 if (!existsSync(filePath)) { results.push({ path: file, content: '', error: '文件不存在', }); continue; } // 读取文件内容 let content = await readFile(filePath, 'utf-8'); // 内容截断处理 if (content.length > charLimit) { content = content.substring(0, charLimit) + '\n\n[... 内容已截断 ...]'; } results.push({ path: file, content, }); } catch (error) { results.push({ path: file, content: '', error: error instanceof Error ? error.message : String(error), }); } } return results.filter(result => result.content || result.error); } /** * 构建文件审查内容 */ buildFileReviewContent(fileContents) { let content = '文件审查内容:\n\n'; for (const file of fileContents) { content += `## 文件: ${file.path}\n\n`; if (file.error) { content += `错误: ${file.error}\n\n`; } else { content += `\`\`\`\n${file.content}\n\`\`\`\n\n`; } } return content; } /** * 构建文件审查提示词 */ buildFilesReviewPrompt(content, request) { return PromptManager.buildPrompt('review_files', content, request.custom_prompt, ConfigManager.getAILanguage()); } /** * 配置AI服务 */ async configureAIService(request) { logger.info('开始配置AI服务', { service: request.service, scope: request.scope, hasApiKey: !!request.api_key, }); try { const service = request.service; const scope = request.scope || 'project'; // 验证服务类型 if (!['gemini', 'claudecode'].includes(service)) { throw new Error(`不支持的AI服务: ${service}`); } // 获取配置路径 const { dir: configDir, file: configFile } = ConfigManager.getConfigPath(scope); // 确保配置目录存在 if (!existsSync(configDir)) { await mkdir(configDir, { recursive: true }); } // 读取现有配置 let existingConfig = {}; if (existsSync(configFile)) { try { const content = await readFile(configFile, 'utf-8'); existingConfig = ConfigManager.parseEnvContent(content); } catch (error) { logger.warn('读取现有配置失败,将创建新配置', { error: error instanceof Error ? error.message : String(error) }); } } // 更新配置 if (request.api_key) { const envVar = ConfigManager.getAPIKeyEnvVar(service); existingConfig[envVar] = request.api_key; } if (request.timeout) { existingConfig['AI_TIMEOUT'] = request.timeout.toString(); } if (request.max_retries) { existingConfig['AI_MAX_RETRIES'] = request.max_retries.toString(); } // 语言配置已移除,使用默认配置 // 写入配置文件 const configContent = Object.entries(existingConfig) .map(([key, value]) => `${key}=${value}`) .join('\n'); await writeFile(configFile, configContent, 'utf-8'); // 重新初始化AI管理器 this.aiManager.reinitialize(); const response = { success: true, message: `AI服务 ${service} 配置成功`, config_path: configFile, restart_required: false, }; logger.info('AI服务配置完成', { service, configPath: configFile, }); return response; } catch (error) { logger.error('AI服务配置失败', error instanceof Error ? error : new Error(String(error))); return { success: false, message: `配置失败: ${error instanceof Error ? error.message : String(error)}`, config_path: '', restart_required: false, }; } } /** * 获取AI服务状态 */ async getAIServiceStatus(request) { logger.info('开始获取AI服务状态'); try { // 刷新服务状态 await this.aiManager.refreshServiceStatus(); // 获取服务状态 const serviceStatus = this.aiManager.getServiceStatus(); const availableServices = this.aiManager.getAvailableServices(); // 构建详细状态信息 const services = Object.entries(serviceStatus).map(([service, available]) => { const apiKey = ConfigManager.getAPIKey(service); return { service: service, available, configured: !!apiKey, }; }); const response = { services, current_service: ConfigManager.getAIService(), auto_switch_enabled: ConfigManager.isAutoSwitchEnabled(), }; logger.info('AI服务状态获取完成', { availableCount: availableServices.length, totalCount: services.length, }); return response; } catch (error) { logger.error('获取AI服务状态失败', error instanceof Error ? error : new Error(String(error))); return { services: [], current_service: 'gemini', auto_switch_enabled: false, }; } } } //# sourceMappingURL=CodeRocketService.js.map