Files
toolbox/app/Services/CodeAnalysisService.php

352 lines
11 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Services;
use Illuminate\Support\Facades\Log;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;
class CodeAnalysisService
{
public const TOOL_GEMINI = 'gemini';
public const TOOL_CLAUDE = 'claude';
public const TOOL_CODEX = 'codex';
private int $timeout = 300;
public function __construct(
private readonly CodeContextService $codeContextService,
private readonly ConfigService $configService
) {}
/**
* 使用配置的工具在项目中分析日志问题
*
* @param string $appName 应用名称
* @param array $aiAnalysisResult AI 分析结果(包含 summary, impact, core_anomalies 等)
* @return array 分析结果
*/
public function analyze(string $appName, array $aiAnalysisResult): array
{
$repoPath = $this->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;
}
}