#feature: add AI log analysis & some bugfix

This commit is contained in:
2026-01-14 13:58:50 +08:00
parent e479ed02ea
commit ae6c169f5f
33 changed files with 3898 additions and 164 deletions

275
app/Clients/AiClient.php Normal file
View File

@@ -0,0 +1,275 @@
<?php
namespace App\Clients;
use App\Services\ConfigService;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\RateLimiter;
use RuntimeException;
class AiClient
{
private ConfigService $configService;
private ?array $currentProvider = null;
public function __construct(ConfigService $configService)
{
$this->configService = $configService;
}
/**
* 获取所有配置的 AI 提供商
*/
public function getProviders(): array
{
$providers = $this->configService->get('log_analysis.ai_providers', []);
// 如果数据库没有配置,使用 .env 默认配置
if (empty($providers)) {
$envConfig = config('services.ai');
if (!empty($envConfig['api_key'])) {
$providers = [
'default' => [
'name' => '默认 (环境变量)',
'endpoint' => $envConfig['endpoint'],
'api_key' => $envConfig['api_key'],
'model' => $envConfig['model'],
'temperature' => $envConfig['temperature'],
'timeout' => $envConfig['timeout'],
'max_tokens' => $envConfig['max_tokens'],
'enabled' => true,
],
];
}
}
return $providers;
}
/**
* 获取当前激活的 AI 提供商
*/
public function getActiveProvider(): ?array
{
if ($this->currentProvider !== null) {
return $this->currentProvider;
}
$activeKey = $this->configService->get('log_analysis.active_ai_provider');
$providers = $this->getProviders();
if ($activeKey && isset($providers[$activeKey]) && ($providers[$activeKey]['enabled'] ?? true)) {
$this->currentProvider = $providers[$activeKey];
$this->currentProvider['key'] = $activeKey;
return $this->currentProvider;
}
// 返回第一个启用的提供商
foreach ($providers as $key => $provider) {
if ($provider['enabled'] ?? true) {
$this->currentProvider = $provider;
$this->currentProvider['key'] = $key;
return $this->currentProvider;
}
}
return null;
}
/**
* 设置当前使用的提供商
*/
public function setActiveProvider(string $providerKey): void
{
$providers = $this->getProviders();
if (!isset($providers[$providerKey])) {
throw new RuntimeException("AI 提供商不存在: {$providerKey}");
}
$this->configService->set('log_analysis.active_ai_provider', $providerKey);
$this->currentProvider = $providers[$providerKey];
$this->currentProvider['key'] = $providerKey;
}
/**
* 检查是否已配置
*/
public function isConfigured(): bool
{
return $this->getActiveProvider() !== null;
}
/**
* 发送聊天请求 (OpenAI 兼容接口)
*
* @param array $messages 消息数组
* @param string|null $systemPrompt 系统提示词
* @return string AI 响应内容
*/
public function chat(array $messages, ?string $systemPrompt = null): string
{
$provider = $this->getActiveProvider();
if (!$provider) {
throw new RuntimeException('未配置 AI 服务,请在设置页面配置 AI 提供商');
}
// 速率限制
$this->waitForRateLimit($provider['key'] ?? 'default');
$allMessages = [];
if ($systemPrompt) {
$allMessages[] = ['role' => 'system', 'content' => $systemPrompt];
}
$allMessages = array_merge($allMessages, $messages);
$endpoint = rtrim($provider['endpoint'], '/') . '/chat/completions';
$response = Http::timeout($provider['timeout'] ?? 120)
->withHeaders([
'Authorization' => 'Bearer ' . $provider['api_key'],
'Content-Type' => 'application/json',
])
->post($endpoint, [
'model' => $provider['model'],
'messages' => $allMessages,
'temperature' => $provider['temperature'] ?? 0.3,
'max_tokens' => $provider['max_tokens'] ?? 4096,
]);
if (!$response->successful()) {
$error = $response->json('error.message') ?? $response->body();
throw new RuntimeException("AI 请求失败: {$error}");
}
return $response->json('choices.0.message.content', '');
}
/**
* 分析日志并返回结构化结果
*
* @param string $logsContent 日志内容
* @param string|null $codeContext 相关代码上下文
* @return array 分析结果
*/
public function analyzeLogs(string $logsContent, ?string $codeContext = null): array
{
$systemPrompt = $this->buildAnalysisSystemPrompt();
$userContent = $this->buildAnalysisUserPrompt($logsContent, $codeContext);
$response = $this->chat([
['role' => 'user', 'content' => $userContent],
], $systemPrompt);
return $this->parseAnalysisResponse($response);
}
/**
* 测试连接
*/
public function testConnection(): array
{
$provider = $this->getActiveProvider();
if (!$provider) {
return [
'success' => false,
'message' => '未配置 AI 服务',
];
}
try {
$response = $this->chat([
['role' => 'user', 'content' => 'Hello, please respond with "OK" only.'],
]);
return [
'success' => true,
'message' => '连接成功',
'provider' => $provider['name'] ?? $provider['key'],
'model' => $provider['model'],
];
} catch (\Exception $e) {
return [
'success' => false,
'message' => $e->getMessage(),
];
}
}
private function buildAnalysisSystemPrompt(): string
{
return <<<'PROMPT'
你是一个专业的日志分析专家。你的任务是分析提供的日志内容,识别异常和问题,并给出分类和建议。
请按照以下 JSON 格式返回分析结果:
{
"core_anomalies": [
{
"type": "error|warning|performance|security",
"classification": "database|network|application|configuration|resource|other",
"count": 数量,
"sample": "示例日志内容",
"possible_cause": "可能的原因",
"suggestion": "建议的解决方案"
}
],
"summary": "整体分析摘要",
"impact": "high|medium|low",
"trends": "趋势分析(可选)"
}
分类说明:
- type: error(错误), warning(警告), performance(性能问题), security(安全问题)
- classification: database(数据库), network(网络), application(应用逻辑), configuration(配置), resource(资源), other(其他)
- impact: high(高影响), medium(中等影响), low(低影响)
请确保返回有效的 JSON 格式。
PROMPT;
}
private function buildAnalysisUserPrompt(string $logsContent, ?string $codeContext): string
{
$prompt = "请分析以下日志内容:\n\n```\n{$logsContent}\n```";
if ($codeContext) {
$prompt .= "\n\n相关代码上下文:\n\n```\n{$codeContext}\n```";
}
return $prompt;
}
private function parseAnalysisResponse(string $response): array
{
// 尝试提取 JSON
if (preg_match('/\{[\s\S]*}/', $response, $matches)) {
$json = $matches[0];
$decoded = json_decode($json, true);
if (json_last_error() === JSON_ERROR_NONE) {
return $decoded;
}
}
// 如果无法解析 JSON返回原始响应
return [
'core_anomalies' => [],
'summary' => $response,
'impact' => 'unknown',
'raw_response' => $response,
];
}
private function waitForRateLimit(string $providerKey): void
{
$key = "ai_rate_limit:{$providerKey}";
// 每分钟最多 60 次请求
$maxAttempts = 60;
$decaySeconds = 60;
if (RateLimiter::tooManyAttempts($key, $maxAttempts)) {
$seconds = RateLimiter::availableIn($key);
sleep($seconds);
}
RateLimiter::hit($key, $decaySeconds);
}
}