apiKey = self::GPT_PROXY_API_KEY; $this->baseUrl = self::GPT_PROXY_BASE_URL; $this->model = self::GPT_PROXY_MODEL; $this->wireApi = self::GPT_PROXY_WIRE_API; $this->providerLabel = 'OpenAI(GPT中转)'; $this->maxTokens = (int) AiConfig::getVal('max_tokens', '2000'); $this->temperature = (float) AiConfig::getVal('temperature', '0.7'); } /** * 发送聊天请求 */ public function chat(string $systemPrompt, string $userMessage, array $options = []): array { $messages = [ ['role' => 'system', 'content' => $systemPrompt], ['role' => 'user', 'content' => $userMessage], ]; return $this->request($messages, $options); } public function chatMessages(array $messages, array $options = []): array { if (empty($messages)) { return ['success' => false, 'error' => 'messages不能为空', 'tokens' => 0]; } return $this->request($messages, $options); } public function chatWithImages(string $systemPrompt, string $userMessage, array $imageUrls, array $options = []): array { $content = [ ['type' => 'text', 'text' => $userMessage], ]; foreach ($imageUrls as $imageUrl) { if ($imageUrl === '') { continue; } $content[] = [ 'type' => 'image_url', 'image_url' => ['url' => $imageUrl], ]; } $messages = [ ['role' => 'system', 'content' => $systemPrompt], ['role' => 'user', 'content' => $content], ]; return $this->request($messages, $options); } protected function request(array $messages, array $options = []): array { $apiKey = $options['api_key'] ?? $this->apiKey; $providerLabel = $options['provider_label'] ?? $this->providerLabel; if (empty($apiKey)) { return ['success' => false, 'error' => $providerLabel . ' API Key未配置', 'tokens' => 0, 'model' => $options['model'] ?? $this->model, 'provider_label' => $providerLabel]; } $startTime = microtime(true); $baseUrl = rtrim($options['base_url'] ?? $this->baseUrl, '/'); $wireApi = strtolower((string) ($options['wire_api'] ?? $this->wireApi)); $payload = $wireApi === 'responses' ? $this->buildResponsesPayload($messages, $options) : $this->buildChatPayload($messages, $options); $endpoint = $wireApi === 'responses' ? $this->buildResponsesEndpoint($baseUrl) : $this->buildChatEndpoint($baseUrl); $timeout = $this->resolveTimeout($options['timeout'] ?? null); $ch = curl_init($endpoint); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'Authorization: Bearer ' . $apiKey, ], CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE), CURLOPT_RETURNTRANSFER => true, CURLOPT_CONNECTTIMEOUT => min(15, $timeout), CURLOPT_TIMEOUT => $timeout, CURLOPT_SSL_VERIFYPEER => false, ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $error = curl_error($ch); curl_close($ch); $costMs = (int) ((microtime(true) - $startTime) * 1000); if ($error) { return ['success' => false, 'error' => 'cURL错误: ' . $error, 'tokens' => 0, 'cost_ms' => $costMs, 'model' => $payload['model'], 'provider_label' => $providerLabel]; } $data = json_decode($response, true); $content = $wireApi === 'responses' ? $this->extractResponsesContent($data) : ($data['choices'][0]['message']['content'] ?? ''); if ($httpCode !== 200 || $content === '') { $errMsg = $data['error']['message'] ?? ('HTTP ' . $httpCode); return ['success' => false, 'error' => $providerLabel . ' 返回异常: ' . $errMsg, 'tokens' => 0, 'cost_ms' => $costMs, 'model' => $payload['model'], 'provider_label' => $providerLabel]; } $tokens = $data['usage']['total_tokens'] ?? (($data['usage']['input_tokens'] ?? 0) + ($data['usage']['output_tokens'] ?? 0)); return [ 'success' => true, 'content' => $content, 'tokens' => $tokens, 'cost_ms' => $costMs, 'model' => $payload['model'], 'provider_label' => $providerLabel, ]; } protected function buildChatPayload(array $messages, array $options): array { return [ 'model' => $options['model'] ?? $this->model, 'messages' => $messages, 'max_tokens' => $options['max_tokens'] ?? $this->maxTokens, 'temperature' => $options['temperature'] ?? $this->temperature, ]; } protected function buildResponsesPayload(array $messages, array $options): array { $instructions = []; $input = []; foreach ($messages as $message) { $role = (string) ($message['role'] ?? 'user'); $content = $message['content'] ?? ''; if ($role === 'system') { $text = $this->contentToText($content); if ($text !== '') { $instructions[] = $text; } continue; } $input[] = [ 'role' => $role === 'assistant' ? 'assistant' : 'user', 'content' => $this->convertResponsesContent($content), ]; } $payload = [ 'model' => $options['model'] ?? $this->model, 'input' => $input, 'max_output_tokens' => $options['max_tokens'] ?? $this->maxTokens, 'temperature' => $options['temperature'] ?? $this->temperature, ]; if (!empty($instructions)) { $payload['instructions'] = implode("\n\n", $instructions); } return $payload; } protected function convertResponsesContent($content): array { if (is_string($content)) { return [['type' => 'input_text', 'text' => $content]]; } $rows = []; foreach ((array) $content as $item) { $type = (string) ($item['type'] ?? ''); if ($type === 'text') { $rows[] = ['type' => 'input_text', 'text' => (string) ($item['text'] ?? '')]; } elseif ($type === 'image_url') { $imageUrl = $item['image_url']['url'] ?? ''; if ($imageUrl !== '') { $rows[] = ['type' => 'input_image', 'image_url' => (string) $imageUrl]; } } } return $rows ?: [['type' => 'input_text', 'text' => '']]; } protected function contentToText($content): string { if (is_string($content)) { return trim($content); } $texts = []; foreach ((array) $content as $item) { if (($item['type'] ?? '') === 'text') { $texts[] = (string) ($item['text'] ?? ''); } } return trim(implode("\n", $texts)); } protected function extractResponsesContent(array $data): string { if (!empty($data['output_text'])) { return (string) $data['output_text']; } $texts = []; foreach (($data['output'] ?? []) as $output) { foreach (($output['content'] ?? []) as $content) { if (isset($content['text'])) { $texts[] = (string) $content['text']; } } } return trim(implode("\n", $texts)); } protected function buildChatEndpoint(string $baseUrl): string { if (str_ends_with($baseUrl, '/chat/completions')) { return $baseUrl; } if (str_ends_with($baseUrl, '/v1')) { return $baseUrl . '/chat/completions'; } return $baseUrl . '/v1/chat/completions'; } protected function buildResponsesEndpoint(string $baseUrl): string { if (str_ends_with($baseUrl, '/responses')) { return $baseUrl; } if (str_ends_with($baseUrl, '/v1')) { return $baseUrl . '/responses'; } return $baseUrl . '/v1/responses'; } protected function resolveTimeout($timeout): int { $value = (int) ($timeout ?: AiConfig::getVal('ai_request_timeout', '180')); return max(30, min($value, 300)); } /** * 发送请求并解析JSON结果 */ public function chatJson(string $systemPrompt, string $userMessage, array $options = []): array { $result = $this->chat($systemPrompt, $userMessage, $options); if (!$result['success']) { return $result; } $content = $result['content']; // 尝试提取JSON块 if (preg_match('/```json\s*(.*?)\s*```/s', $content, $m)) { $content = $m[1]; } elseif (preg_match('/\{.*\}/s', $content, $m)) { $content = $m[0]; } $parsed = json_decode($content, true); if (json_last_error() !== JSON_ERROR_NONE) { $result['parsed'] = null; $result['raw_content'] = $result['content']; } else { $result['parsed'] = $parsed; } return $result; } }