configService->get('log_analysis.app_env_map', []); if (!isset($appEnvMap[$appName])) { return null; } $mapping = $appEnvMap[$appName]; $project = $mapping['project'] ?? null; $env = $mapping['env'] ?? null; if (!$project || !$env) { return null; } try { $envContent = $this->envService->getEnvContent($project, $env); $repoPath = $this->parseEnvValue($envContent, 'LOG_ANALYSIS_CODE_REPO_PATH'); if ($repoPath && is_dir($repoPath)) { return $repoPath; } } catch (\Exception $e) { // 忽略错误,返回 null } return null; } /** * 从日志中提取相关代码片段 * * @param string $repoPath 代码仓库路径 * @param Collection $logs 日志集合 * @return string|null 代码上下文 */ public function extractRelevantCode(string $repoPath, Collection $logs): ?string { $codeSnippets = []; $processedFiles = []; foreach ($logs as $log) { $file = $log['file'] ?? null; $line = $log['line'] ?? null; if (!$file || !$line) { // 尝试从 trace 中提取 $extracted = $this->extractFileLineFromTrace($log['trace'] ?? ''); if ($extracted) { $file = $extracted['file']; $line = $extracted['line']; } } if (!$file || !$line) { continue; } // 构建完整路径 $fullPath = $this->resolveFilePath($repoPath, $file); if (!$fullPath || !File::exists($fullPath)) { continue; } // 避免重复处理同一文件的同一位置 $key = "{$fullPath}:{$line}"; if (isset($processedFiles[$key])) { continue; } $processedFiles[$key] = true; // 提取代码片段 $snippet = $this->extractCodeSnippet($fullPath, (int) $line); if ($snippet) { $codeSnippets[] = $snippet; } // 限制代码片段数量 if (count($codeSnippets) >= 5) { break; } } if (empty($codeSnippets)) { return null; } return implode("\n\n---\n\n", $codeSnippets); } /** * 提取指定文件指定行附近的代码 * * @param string $filePath 文件路径 * @param int $line 行号 * @return string|null */ public function extractCodeSnippet(string $filePath, int $line): ?string { if (!File::exists($filePath)) { return null; } $content = File::get($filePath); $lines = explode("\n", $content); $totalLines = count($lines); $startLine = max(1, $line - $this->contextLines); $endLine = min($totalLines, $line + $this->contextLines); $snippet = []; $snippet[] = "// File: " . basename($filePath); $snippet[] = "// Lines: {$startLine}-{$endLine} (target: {$line})"; $snippet[] = ""; for ($i = $startLine - 1; $i < $endLine; $i++) { $lineNum = $i + 1; $marker = ($lineNum === $line) ? '>>>' : ' '; $snippet[] = sprintf("%s %4d | %s", $marker, $lineNum, $lines[$i] ?? ''); } return implode("\n", $snippet); } /** * 从堆栈跟踪中提取文件和行号 * * @param string $trace * @return array|null ['file' => string, 'line' => int] */ private function extractFileLineFromTrace(string $trace): ?array { // 匹配常见的堆栈跟踪格式 // PHP: at /path/to/file.php:123 // PHP: #0 /path/to/file.php(123): function() // Java: at com.example.Class.method(File.java:123) $patterns = [ '/at\s+([^\s:]+):(\d+)/', '/#\d+\s+([^\s(]+)\((\d+)\)/', '/([^\s:]+\.php):(\d+)/', '/\(([^:]+):(\d+)\)/', ]; foreach ($patterns as $pattern) { if (preg_match($pattern, $trace, $matches)) { return [ 'file' => $matches[1], 'line' => (int) $matches[2], ]; } } return null; } /** * 解析文件路径 * * @param string $repoPath 仓库根路径 * @param string $file 文件路径(可能是相对路径或绝对路径) * @return string|null */ private function resolveFilePath(string $repoPath, string $file): ?string { // 如果是绝对路径且存在 if (str_starts_with($file, '/') && File::exists($file)) { return $file; } // 尝试作为相对路径 $fullPath = rtrim($repoPath, '/') . '/' . ltrim($file, '/'); if (File::exists($fullPath)) { return $fullPath; } // 尝试在仓库中搜索文件名 $filename = basename($file); $found = $this->findFileInRepo($repoPath, $filename); return $found; } /** * 在仓库中搜索文件 * * @param string $repoPath * @param string $filename * @return string|null */ private function findFileInRepo(string $repoPath, string $filename): ?string { $iterator = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($repoPath, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::SELF_FIRST ); foreach ($iterator as $file) { if ($file->isFile() && $file->getFilename() === $filename) { return $file->getPathname(); } } return null; } /** * 从 .env 内容中解析指定键的值 * * @param string $envContent * @param string $key * @return string|null */ private function parseEnvValue(string $envContent, string $key): ?string { $lines = explode("\n", $envContent); foreach ($lines as $line) { $line = trim($line); // 跳过注释和空行 if (empty($line) || str_starts_with($line, '#')) { continue; } if (str_starts_with($line, "{$key}=")) { $value = substr($line, strlen($key) + 1); // 移除引号 $value = trim($value, '"\''); return $value ?: null; } } return null; } /** * 设置上下文行数 */ public function setContextLines(int $lines): void { $this->contextLines = $lines; } }