406 lines
12 KiB
PHP
406 lines
12 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Facades\Cache;
|
|
|
|
class AIService
|
|
{
|
|
/**
|
|
* 当前使用的服务
|
|
*/
|
|
protected $currentService;
|
|
|
|
/**
|
|
* 服务配置
|
|
*/
|
|
protected $config;
|
|
|
|
/**
|
|
* 构造函数
|
|
*/
|
|
public function __construct()
|
|
{
|
|
$this->config = config('ai');
|
|
$this->currentService = $this->config['default'];
|
|
}
|
|
|
|
/**
|
|
* 聊天对话
|
|
*/
|
|
public function chat(array $messages, array $options = []): array
|
|
{
|
|
$cacheKey = $this->getCacheKey('chat', $messages, $options);
|
|
|
|
// 检查缓存
|
|
if ($this->config['cache']['enabled']) {
|
|
$cached = Cache::get($cacheKey);
|
|
if ($cached) {
|
|
return $cached;
|
|
}
|
|
}
|
|
|
|
$result = $this->callService('chat', $messages, $options);
|
|
|
|
// 缓存结果
|
|
if ($this->config['cache']['enabled'] && $result['success']) {
|
|
Cache::put($cacheKey, $result, $this->config['cache']['ttl']);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* 调用AI服务
|
|
*/
|
|
protected function callService(string $action, array $data, array $options = []): array
|
|
{
|
|
$serviceConfig = $this->getServiceConfig();
|
|
|
|
// 记录请求日志
|
|
if ($this->config['logging']['enabled'] && $this->config['logging']['log_requests']) {
|
|
Log::info('AI服务请求', [
|
|
'service' => $this->currentService,
|
|
'action' => $action,
|
|
'data' => $this->filterSensitiveData($data),
|
|
]);
|
|
}
|
|
|
|
try {
|
|
$response = $this->makeRequest($serviceConfig, $data, $options);
|
|
|
|
// 记录响应日志
|
|
if ($this->config['logging']['enabled'] && $this->config['logging']['log_responses']) {
|
|
Log::info('AI服务响应', [
|
|
'service' => $this->currentService,
|
|
'action' => $action,
|
|
'response' => $this->filterSensitiveData($response),
|
|
]);
|
|
}
|
|
|
|
return $response;
|
|
} catch (\Exception $e) {
|
|
// 记录错误日志
|
|
if ($this->config['logging']['enabled'] && $this->config['logging']['log_errors']) {
|
|
Log::error('AI服务错误', [
|
|
'service' => $this->currentService,
|
|
'action' => $action,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
}
|
|
|
|
// 尝试降级服务
|
|
if ($this->config['fallback']['enabled']) {
|
|
return $this->fallbackService($action, $data, $options);
|
|
}
|
|
|
|
return [
|
|
'success' => false,
|
|
'error' => 'AI服务暂时不可用: ' . $e->getMessage(),
|
|
'service' => $this->currentService,
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 发送HTTP请求
|
|
*/
|
|
protected function makeRequest(array $serviceConfig, array $data, array $options): array
|
|
{
|
|
$url = $serviceConfig['api_url'];
|
|
$timeout = $serviceConfig['timeout'] ?? 30;
|
|
|
|
$headers = $this->buildHeaders($serviceConfig);
|
|
$body = $this->buildRequestBody($serviceConfig, $data, $options);
|
|
|
|
$response = Http::withHeaders($headers)
|
|
->timeout($timeout)
|
|
->post($url, $body);
|
|
|
|
if ($response->successful()) {
|
|
return $this->parseResponse($serviceConfig, $response->json());
|
|
} else {
|
|
throw new \Exception('API请求失败: ' . $response->status() . ' - ' . $response->body());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 构建请求头
|
|
*/
|
|
protected function buildHeaders(array $serviceConfig): array
|
|
{
|
|
$headers = [
|
|
'Content-Type' => 'application/json',
|
|
];
|
|
|
|
switch ($this->currentService) {
|
|
case 'openai':
|
|
$headers['Authorization'] = 'Bearer ' . $serviceConfig['api_key'];
|
|
break;
|
|
case 'azure_openai':
|
|
$headers['api-key'] = $serviceConfig['api_key'];
|
|
break;
|
|
case 'anthropic':
|
|
$headers['x-api-key'] = $serviceConfig['api_key'];
|
|
$headers['anthropic-version'] = '2023-06-01';
|
|
break;
|
|
case 'aliyun_qwen':
|
|
$headers['Authorization'] = 'Bearer ' . $serviceConfig['api_key'];
|
|
break;
|
|
}
|
|
|
|
return $headers;
|
|
}
|
|
|
|
/**
|
|
* 构建请求体
|
|
*/
|
|
protected function buildRequestBody(array $serviceConfig, array $data, array $options): array
|
|
{
|
|
$body = [];
|
|
|
|
switch ($this->currentService) {
|
|
case 'openai':
|
|
case 'local':
|
|
$body = [
|
|
'model' => $serviceConfig['model'],
|
|
'messages' => $data['messages'] ?? [],
|
|
'max_tokens' => $options['max_tokens'] ?? $serviceConfig['max_tokens'],
|
|
'temperature' => $options['temperature'] ?? $serviceConfig['temperature'],
|
|
'stream' => $options['stream'] ?? false,
|
|
];
|
|
break;
|
|
case 'azure_openai':
|
|
$body = [
|
|
'messages' => $data['messages'] ?? [],
|
|
'max_tokens' => $options['max_tokens'] ?? $serviceConfig['max_tokens'],
|
|
'temperature' => $options['temperature'] ?? $serviceConfig['temperature'],
|
|
'stream' => $options['stream'] ?? false,
|
|
];
|
|
break;
|
|
case 'anthropic':
|
|
$body = [
|
|
'model' => $serviceConfig['model'],
|
|
'messages' => $data['messages'] ?? [],
|
|
'max_tokens' => $options['max_tokens'] ?? $serviceConfig['max_tokens'],
|
|
'temperature' => $options['temperature'] ?? $serviceConfig['temperature'],
|
|
'stream' => $options['stream'] ?? false,
|
|
];
|
|
break;
|
|
case 'aliyun_qwen':
|
|
$body = [
|
|
'model' => $serviceConfig['model'],
|
|
'messages' => $data['messages'] ?? [],
|
|
'max_tokens' => $options['max_tokens'] ?? $serviceConfig['max_tokens'],
|
|
'temperature' => $options['temperature'] ?? $serviceConfig['temperature'],
|
|
'stream' => $options['stream'] ?? false,
|
|
];
|
|
break;
|
|
}
|
|
|
|
return $body;
|
|
}
|
|
|
|
/**
|
|
* 解析响应
|
|
*/
|
|
protected function parseResponse(array $serviceConfig, array $response): array
|
|
{
|
|
switch ($this->currentService) {
|
|
case 'openai':
|
|
case 'azure_openai':
|
|
case 'local':
|
|
$content = $response['choices'][0]['message']['content'] ?? '';
|
|
$tokens = $response['usage']['total_tokens'] ?? 0;
|
|
break;
|
|
case 'anthropic':
|
|
$content = $response['content'][0]['text'] ?? '';
|
|
$tokens = $response['usage']['input_tokens'] + $response['usage']['output_tokens'] ?? 0;
|
|
break;
|
|
case 'aliyun_qwen':
|
|
$content = $response['choices'][0]['message']['content'] ?? '';
|
|
$tokens = $response['usage']['total_tokens'] ?? 0;
|
|
break;
|
|
default:
|
|
$content = '';
|
|
$tokens = 0;
|
|
}
|
|
|
|
return [
|
|
'success' => true,
|
|
'content' => $content,
|
|
'tokens' => $tokens,
|
|
'raw_response' => $response,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 降级服务
|
|
*/
|
|
protected function fallbackService(string $action, array $data, array $options): array
|
|
{
|
|
$services = $this->config['fallback']['service_order'];
|
|
$currentIndex = array_search($this->currentService, $services);
|
|
|
|
if ($currentIndex === false || $currentIndex >= count($services) - 1) {
|
|
return [
|
|
'success' => false,
|
|
'error' => '所有AI服务都不可用',
|
|
];
|
|
}
|
|
|
|
// 切换到下一个服务
|
|
$nextService = $services[$currentIndex + 1];
|
|
$this->currentService = $nextService;
|
|
|
|
Log::warning('AI服务降级', [
|
|
'from' => $services[$currentIndex],
|
|
'to' => $nextService,
|
|
'action' => $action,
|
|
]);
|
|
|
|
// 重试请求
|
|
return $this->callService($action, $data, $options);
|
|
}
|
|
|
|
/**
|
|
* 获取服务配置
|
|
*/
|
|
protected function getServiceConfig(): array
|
|
{
|
|
$serviceConfig = $this->config['services'][$this->currentService] ?? [];
|
|
|
|
if (empty($serviceConfig)) {
|
|
throw new \Exception("AI服务配置不存在: {$this->currentService}");
|
|
}
|
|
|
|
return $serviceConfig;
|
|
}
|
|
|
|
/**
|
|
* 过滤敏感数据
|
|
*/
|
|
protected function filterSensitiveData(array $data): array
|
|
{
|
|
if (!$this->config['security']['filter_sensitive_data']) {
|
|
return $data;
|
|
}
|
|
|
|
$filtered = $data;
|
|
$sensitiveKeywords = $this->config['security']['sensitive_keywords'];
|
|
|
|
array_walk_recursive($filtered, function (&$value, $key) use ($sensitiveKeywords) {
|
|
foreach ($sensitiveKeywords as $keyword) {
|
|
if (stripos($key, $keyword) !== false || stripos($value, $keyword) !== false) {
|
|
$value = '***FILTERED***';
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
return $filtered;
|
|
}
|
|
|
|
/**
|
|
* 生成缓存键
|
|
*/
|
|
protected function getCacheKey(string $action, array $data, array $options): string
|
|
{
|
|
$prefix = $this->config['cache']['prefix'];
|
|
$hash = md5(serialize([$action, $data, $options, $this->currentService]));
|
|
|
|
return $prefix . $hash;
|
|
}
|
|
|
|
/**
|
|
* 获取当前服务
|
|
*/
|
|
public function getCurrentService(): string
|
|
{
|
|
return $this->currentService;
|
|
}
|
|
|
|
/**
|
|
* 设置服务
|
|
*/
|
|
public function setService(string $service): void
|
|
{
|
|
if (isset($this->config['services'][$service])) {
|
|
$this->currentService = $service;
|
|
} else {
|
|
throw new \Exception("不支持的AI服务: {$service}");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取可用服务列表
|
|
*/
|
|
public function getAvailableServices(): array
|
|
{
|
|
return array_keys($this->config['services']);
|
|
}
|
|
|
|
/**
|
|
* 测试服务连接
|
|
*/
|
|
public function testConnection(): array
|
|
{
|
|
try {
|
|
$testMessages = [
|
|
['role' => 'system', 'content' => '你是一个测试助手'],
|
|
['role' => 'user', 'content' => '请回复"OK"'],
|
|
];
|
|
|
|
$result = $this->chat($testMessages, [
|
|
'max_tokens' => 10,
|
|
'temperature' => 0.1,
|
|
]);
|
|
|
|
return [
|
|
'success' => $result['success'],
|
|
'service' => $this->currentService,
|
|
'response_time' => '正常',
|
|
'message' => $result['success'] ? '连接正常' : ($result['error'] ?? '连接失败'),
|
|
];
|
|
} catch (\Exception $e) {
|
|
return [
|
|
'success' => false,
|
|
'service' => $this->currentService,
|
|
'error' => $e->getMessage(),
|
|
'message' => '连接失败',
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取服务状态
|
|
*/
|
|
public function getServiceStatus(): array
|
|
{
|
|
$services = [];
|
|
|
|
foreach ($this->config['services'] as $name => $config) {
|
|
$originalService = $this->currentService;
|
|
$this->currentService = $name;
|
|
|
|
try {
|
|
$testResult = $this->testConnection();
|
|
$services[$name] = [
|
|
'status' => $testResult['success'] ? 'online' : 'offline',
|
|
'message' => $testResult['message'],
|
|
];
|
|
} catch (\Exception $e) {
|
|
$services[$name] = [
|
|
'status' => 'error',
|
|
'message' => $e->getMessage(),
|
|
];
|
|
}
|
|
|
|
$this->currentService = $originalService;
|
|
}
|
|
|
|
return $services;
|
|
}
|
|
} |