where('enabled', 1)->find(); } else { $model = AiModel::where('is_default', 1)->where('enabled', 1)->find(); } if (!$model) { $model = AiModel::where('enabled', 1)->order('sort_order')->find(); } if (!$model) { throw new HttpResponseException(json([ 'code' => 1, 'message' => '未配置可用的 AI 模型', 'data' => null, ], 500)); } return $model; } public static function streamChat(AiModel $model, array $messages, callable $onChunk): void { $url = rtrim($model->api_base_url, '/') . '/chat/completions'; $payload = [ 'model' => $model->model_id, 'messages' => $messages, 'stream' => true, 'max_tokens' => (int) $model->max_tokens, 'temperature' => (float) $model->temperature, ]; $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($payload), CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'Authorization: Bearer ' . $model->api_key, ], CURLOPT_RETURNTRANSFER => false, CURLOPT_WRITEFUNCTION => function ($ch, $data) use ($onChunk) { $lines = explode("\n", $data); foreach ($lines as $line) { $line = trim($line); if ($line === '' || $line === 'data: [DONE]') { continue; } if (str_starts_with($line, 'data: ')) { $json = json_decode(substr($line, 6), true); if ($json) { $onChunk($json); } } } return strlen($data); }, CURLOPT_TIMEOUT => 120, CURLOPT_SSL_VERIFYPEER => false, ]); $result = curl_exec($ch); if ($result === false) { $error = curl_error($ch); curl_close($ch); self::sseEvent('error', ['message' => 'AI 请求失败: ' . $error]); return; } curl_close($ch); } public static function chat(AiModel $model, array $messages): array { $url = rtrim($model->api_base_url, '/') . '/chat/completions'; $payload = [ 'model' => $model->model_id, 'messages' => $messages, 'stream' => false, 'max_tokens' => (int) $model->max_tokens, 'temperature' => (float) $model->temperature, ]; $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($payload), CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'Authorization: Bearer ' . $model->api_key, ], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 120, CURLOPT_SSL_VERIFYPEER => false, ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode !== 200) { throw new HttpResponseException(json([ 'code' => 1, 'message' => 'AI 请求失败: HTTP ' . $httpCode, 'data' => null, ], 502)); } $data = json_decode($response, true); if (!$data) { throw new HttpResponseException(json([ 'code' => 1, 'message' => 'AI 响应解析失败', 'data' => null, ], 502)); } return $data; } public static function sseHeaders(): void { header('Content-Type: text/event-stream'); header('Cache-Control: no-cache'); header('Connection: keep-alive'); header('X-Accel-Buffering: no'); } public static function sseEvent(string $event, mixed $data): void { echo "event: {$event}\n"; echo 'data: ' . json_encode($data, JSON_UNESCAPED_UNICODE) . "\n\n"; if (ob_get_level() > 0) { ob_flush(); } flush(); } }