268 lines
7.2 KiB
PHP
268 lines
7.2 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\File;
|
|
|
|
class CodeContextService
|
|
{
|
|
private int $contextLines = 10;
|
|
|
|
public function __construct(
|
|
private readonly ConfigService $configService,
|
|
private readonly EnvService $envService
|
|
) {}
|
|
|
|
/**
|
|
* 根据 app_name 获取代码仓库路径
|
|
*
|
|
* @param string $appName
|
|
* @return string|null
|
|
*/
|
|
public function getRepoPath(string $appName): ?string
|
|
{
|
|
$appEnvMap = $this->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;
|
|
}
|
|
}
|