codeContextService->getRepoPath($appName); if (!$repoPath) { return [ 'success' => false, 'error' => "未配置项目路径: {$appName}", ]; } $tool = $this->getConfiguredTool(); try { $prompt = $this->buildPrompt($aiAnalysisResult); $output = $this->runTool($tool, $repoPath, $prompt); return [ 'success' => true, 'output' => $output, 'repo_path' => $repoPath, 'tool' => $tool, ]; } catch (ProcessFailedException $e) { Log::error("{$tool} execution failed", [ 'app_name' => $appName, 'repo_path' => $repoPath, 'tool' => $tool, 'error' => $e->getMessage(), ]); return [ 'success' => false, 'error' => "{$tool} 执行失败: " . $e->getProcess()->getErrorOutput(), 'tool' => $tool, ]; } catch (\Exception $e) { Log::error("Code analysis failed", [ 'app_name' => $appName, 'repo_path' => $repoPath, 'tool' => $tool, 'error' => $e->getMessage(), ]); return [ 'success' => false, 'error' => $e->getMessage(), 'tool' => $tool, ]; } } /** * 获取配置的分析工具 */ public function getConfiguredTool(): string { $tool = $this->configService->get('log_analysis.code_analysis_tool', self::TOOL_GEMINI); if (!in_array($tool, [self::TOOL_GEMINI, self::TOOL_CLAUDE, self::TOOL_CODEX])) { return self::TOOL_GEMINI; } return $tool; } /** * 设置分析工具 */ public function setTool(string $tool): void { if (!in_array($tool, [self::TOOL_GEMINI, self::TOOL_CLAUDE, self::TOOL_CODEX])) { throw new \InvalidArgumentException("不支持的工具: {$tool}"); } $this->configService->set( 'log_analysis.code_analysis_tool', $tool, '代码分析工具 (gemini/claude/codex)' ); } /** * 获取可用的工具列表 */ public function getAvailableTools(): array { return [ self::TOOL_GEMINI => [ 'name' => 'Gemini CLI', 'description' => 'Google Gemini 命令行工具', ], self::TOOL_CLAUDE => [ 'name' => 'Claude CLI', 'description' => 'Anthropic Claude 命令行工具', ], self::TOOL_CODEX => [ 'name' => 'Codex CLI', 'description' => 'OpenAI Codex 命令行工具', ], ]; } /** * 构建提示词(基于 AI 分析汇总结果) */ private function buildPrompt(array $aiAnalysisResult): string { $prompt = "根据以下日志分析结果,在代码库中排查根本原因并给出具体优化方案:\n\n"; // 影响级别 $impact = $aiAnalysisResult['impact'] ?? 'unknown'; $prompt .= "=== 影响级别 ===\n{$impact}\n\n"; // AI 摘要 $summary = $aiAnalysisResult['summary'] ?? ''; if ($summary) { $prompt .= "=== 问题摘要 ===\n{$summary}\n\n"; } // 核心异常列表 $anomalies = $aiAnalysisResult['core_anomalies'] ?? []; if (!empty($anomalies)) { $prompt .= "=== 异常列表 ===\n"; foreach ($anomalies as $idx => $anomaly) { $num = $idx + 1; $type = $anomaly['type'] ?? 'unknown'; $classification = $anomaly['classification'] ?? ''; $count = $anomaly['count'] ?? 1; $cause = $anomaly['possible_cause'] ?? ''; $sample = $anomaly['sample'] ?? ''; $prompt .= "{$num}. [{$type}] {$classification} (出现 {$count} 次)\n"; if ($cause) { $prompt .= " 可能原因: {$cause}\n"; } if ($sample) { // 限制样本长度,避免过长 $sampleTruncated = mb_strlen($sample) > 500 ? mb_substr($sample, 0, 500) . '...' : $sample; $prompt .= " 日志样本: {$sampleTruncated}\n"; } } $prompt .= "\n"; } $prompt .= "请:\n"; $prompt .= "1. 定位相关代码文件\n"; $prompt .= "2. 分析根本原因\n"; $prompt .= "3. 给出具体修复方案\n"; $prompt .= "\n注意:仅进行分析和提供建议,不要修改任何代码文件。\n"; return $prompt; } /** * 执行分析工具 */ private function runTool(string $tool, string $workingDirectory, string $prompt): string { return match ($tool) { self::TOOL_CLAUDE => $this->runClaude($workingDirectory, $prompt), self::TOOL_CODEX => $this->runCodex($workingDirectory, $prompt), default => $this->runGemini($workingDirectory, $prompt), }; } /** * 执行 Gemini CLI 命令 */ private function runGemini(string $workingDirectory, string $prompt): string { $process = new Process( ['gemini', '--approval-mode', 'plan', '-o', 'json', $prompt], $workingDirectory, $this->getEnvWithPath() ); $process->setTimeout($this->timeout); $process->mustRun(); $output = trim($process->getOutput()); // 解析 JSON 格式输出,提取完整的分析结果 $json = json_decode($output, true); if ($json && isset($json['response'])) { return $json['response']; } // 如果解析失败,返回原始输出 return $output; } /** * 执行 Claude CLI 命令 */ private function runClaude(string $workingDirectory, string $prompt): string { $process = new Process( ['claude', '--print', '--output-format', 'json', $prompt], $workingDirectory, $this->getEnvWithPath() ); $process->setTimeout($this->timeout); $process->mustRun(); $output = trim($process->getOutput()); // 解析 JSON 格式输出,提取完整的分析结果 $json = json_decode($output, true); if ($json && isset($json['result'])) { return $json['result']; } // 如果解析失败,返回原始输出 return $output; } /** * 执行 Codex CLI 命令 */ private function runCodex(string $workingDirectory, string $prompt): string { // 使用临时文件保存最终消息,避免输出被截断 $outputFile = sys_get_temp_dir() . '/codex_output_' . uniqid() . '.txt'; $process = new Process( ['codex', 'exec', '--sandbox', 'read-only', '-o', $outputFile, $prompt], $workingDirectory, $this->getEnvWithPath() ); $process->setTimeout($this->timeout); $process->mustRun(); // 从输出文件读取完整结果 if (file_exists($outputFile)) { $output = trim(file_get_contents($outputFile)); @unlink($outputFile); return $output; } // 如果文件不存在,回退到标准输出 return trim($process->getOutput()); } /** * 获取包含用户 PATH 的环境变量 * 确保 nvm、npm 全局安装的命令可以被找到 */ private function getEnvWithPath(): array { $env = getenv(); $homeDir = getenv('HOME') ?: '/home/' . get_current_user(); // 添加常见的用户级 bin 目录到 PATH $additionalPaths = [ "{$homeDir}/.local/bin", "{$homeDir}/.npm-global/bin", '/usr/local/bin', ]; // 查找 nvm 当前使用的 Node.js 版本的 bin 目录 $nvmDir = "{$homeDir}/.nvm/versions/node"; if (is_dir($nvmDir)) { // 获取最新版本的 Node.js(按版本号排序) $versions = @scandir($nvmDir); if ($versions) { $versions = array_filter($versions, fn($v) => $v !== '.' && $v !== '..'); if (!empty($versions)) { usort($versions, 'version_compare'); $latestVersion = end($versions); $additionalPaths[] = "{$nvmDir}/{$latestVersion}/bin"; } } } $currentPath = $env['PATH'] ?? '/usr/bin:/bin'; $env['PATH'] = implode(':', $additionalPaths) . ':' . $currentPath; // 添加 Gemini API Key(用于非交互式模式) $geminiApiKey = $this->configService->get('log_analysis.gemini_api_key') ?: config('services.gemini.api_key'); if ($geminiApiKey) { $env['GEMINI_API_KEY'] = $geminiApiKey; } // 确保代理环境变量被传递(后台任务可能没有继承) $proxyUrl = config('services.proxy.url'); if ($proxyUrl) { $env['HTTP_PROXY'] = $proxyUrl; $env['HTTPS_PROXY'] = $proxyUrl; $env['http_proxy'] = $proxyUrl; $env['https_proxy'] = $proxyUrl; } return $env; } /** * 设置 Gemini API Key */ public function setGeminiApiKey(string $apiKey): void { $this->configService->set( 'log_analysis.gemini_api_key', $apiKey, 'Gemini CLI API Key (用于非交互式模式)' ); } /** * 获取 Gemini API Key */ public function getGeminiApiKey(): ?string { return $this->configService->get('log_analysis.gemini_api_key') ?: config('services.gemini.api_key'); } /** * 设置超时时间 */ public function setTimeout(int $timeout): void { $this->timeout = $timeout; } }