Files
toolbox/app/Services/CodeContextService.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;
}
}