lh пре 1 месец
родитељ
комит
becd04ef12
1 измењених фајлова са 240 додато и 96 уклоњено
  1. 240 96
      app/Services/DeepSeek/DeepSeekService.php

+ 240 - 96
app/Services/DeepSeek/DeepSeekService.php

@@ -1298,6 +1298,97 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
     }
 
     /**
+     * GPT 重试次数(env 可配置,默认 5 次)
+     */
+    private function gptRetryTimes()
+    {
+        return (int)env('GPT_RETRY_TIMES', 5);
+    }
+
+    /**
+     * GPT 重试间隔秒数(env 可配置,默认 10 秒)
+     */
+    private function gptRetryInterval()
+    {
+        return (int)env('GPT_RETRY_INTERVAL', 10);
+    }
+
+    /**
+     * 判断 GPT 请求异常是否可重试
+     * 仅连接失败/超时等传输层异常、5xx、429 可重试;其余 4xx(401/400 等)不重试
+     */
+    private function isGptRetryableException(\Exception $e)
+    {
+        if (!($e instanceof \GuzzleHttp\Exception\GuzzleException)) {
+            return false;
+        }
+
+        if ($e instanceof \GuzzleHttp\Exception\BadResponseException && $e->getResponse()) {
+            $status = $e->getResponse()->getStatusCode();
+            return $status >= 500 || $status == 429;
+        }
+
+        return true;
+    }
+
+    /**
+     * GPT 中转站探活:GET /v1/models,5 秒短超时
+     * 连接成功(含 401/403/404)视为可用;连接失败/超时/5xx/429 视为不可用
+     */
+    private function checkGptServiceAvailable()
+    {
+        $apiKey = env('GPT_54_API_KEY');
+        if (empty($apiKey)) {
+            return false;
+        }
+
+        try {
+            $checkClient = new Client(['timeout' => 5, 'verify' => false, 'http_errors' => false]);
+            $response = $checkClient->get('https://ai-api.kkidc.com/v1/models', [
+                'headers' => [
+                    'Authorization' => 'Bearer ' . $apiKey,
+                    'Content-Type'  => 'application/json'
+                ]
+            ]);
+            $status = $response->getStatusCode();
+            return $status < 500 && $status != 429;
+        } catch (\Exception $e) {
+            return false;
+        }
+    }
+
+    /**
+     * 探活并重试:最多重试 gptRetryTimes() 次,间隔 gptRetryInterval() 秒
+     * 全部失败抛出友好错误
+     */
+    private function ensureGptServiceAvailable()
+    {
+        $maxRetries = $this->gptRetryTimes();
+        $interval = $this->gptRetryInterval();
+
+        for ($attempt = 0; $attempt <= $maxRetries; $attempt++) {
+            if ($this->checkGptServiceAvailable()) {
+                return;
+            }
+
+            if ($attempt < $maxRetries) {
+                dLog('deepseek')->warning('GPT 中转站探活失败,准备重试', [
+                    'retry'       => $attempt + 1,
+                    'max_retries' => $maxRetries
+                ]);
+                sleep($interval);
+            }
+        }
+
+        dLog('deepseek')->error('GPT 中转站探活失败,重试次数已用完');
+        logDB('deepseek', 'error', 'GPT 中转站服务不可用', [
+            'max_retries' => $maxRetries,
+            'interval'    => $interval
+        ]);
+        Utils::throwError('20003:GPT 中转站服务暂时不可用,请稍后重试');
+    }
+
+    /**
      * GPT 流式输出处理方法
      * 
      * @param array $post_data GPT API请求参数
@@ -1310,6 +1401,9 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
             Utils::throwError('20003:GPT API Key未配置');
         }
 
+        // 先探活,服务不可用时自动重试
+        $this->ensureGptServiceAvailable();
+
         $client = new Client(['timeout' => 1800, 'verify' => false]);
         $headers = [
             'Authorization' => 'Bearer ' . $apiKey,
@@ -1325,96 +1419,121 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
         }
         $post_data['max_completion_tokens'] = 100000;
 
-        // 备用中转站地址1: https://token.ithinkai.cn/v1/chat/completions
-        // 备用中转站地址2: https://api.nonelinear.com/v1/chat/completions
-        $response = $client->post('https://ai-api.kkidc.com/v1/chat/completions', [
-            'json' => $post_data, 
-            'headers' => $headers,
-            'stream' => true  // 启用流式响应
-        ]);
-        
-        $body = $response->getBody();
-        $fullContent = '';
-        $usage = [];
-        $buffer = '';
+        $maxRetries = $this->gptRetryTimes();
+        $interval = $this->gptRetryInterval();
+        $attempt = 0;
 
-        dLog('deepseek')->info('开始读取 GPT 流式响应');
+        while (true) {
+            $fullContent = '';
+            $usage = [];
+            $buffer = '';
 
-        // 逐行读取流式响应
-        while (!$body->eof()) {
-            $chunk = $body->read(1024); // 每次读取 1KB
-            $buffer .= $chunk;
-            
-            // 按行分割
-            $lines = explode("\n", $buffer);
-            // 保留最后一个不完整的行
-            $buffer = array_pop($lines);
-            
-            foreach ($lines as $line) {
-                $line = trim($line);
-                
-                // 跳过空行
-                if (empty($line)) {
-                    continue;
-                }
+            try {
+                // 备用中转站地址1: https://token.ithinkai.cn/v1/chat/completions
+                // 备用中转站地址2: https://api.nonelinear.com/v1/chat/completions
+                $response = $client->post('https://ai-api.kkidc.com/v1/chat/completions', [
+                    'json' => $post_data, 
+                    'headers' => $headers,
+                    'stream' => true  // 启用流式响应
+                ]);
                 
-                // 检查是否是 SSE 数据行
-                if (strpos($line, 'data: ') !== 0) {
-                    dLog('deepseek')->warning('非 SSE 数据行', ['line' => $line]);
-                    continue;
-                }
+                $body = $response->getBody();
 
-                $data = substr($line, 6); // 移除 "data: " 前缀
-                
-                if ($data === '[DONE]') {
-                    dLog('deepseek')->info('收到结束标记');
-                    break 2; // 跳出两层循环
-                }
+                dLog('deepseek')->info('开始读取 GPT 流式响应');
 
-                try {
-                    $json = json_decode($data, true);
+                // 逐行读取流式响应
+                while (!$body->eof()) {
+                    $chunk = $body->read(1024); // 每次读取 1KB
+                    $buffer .= $chunk;
                     
-                    if (json_last_error() !== JSON_ERROR_NONE) {
-                        dLog('deepseek')->warning('JSON 解析错误: ' . json_last_error_msg(), ['data' => $data]);
-                        continue;
-                    }
+                    // 按行分割
+                    $lines = explode("\n", $buffer);
+                    // 保留最后一个不完整的行
+                    $buffer = array_pop($lines);
                     
-                    if (isset($json['choices'][0]['delta'])) {
-                        $delta = $json['choices'][0]['delta'];
+                    foreach ($lines as $line) {
+                        $line = trim($line);
                         
-                        // 处理回答内容
-                        if (isset($delta['content'])) {
-                            $fullContent .= $delta['content'];
-                            yield [
-                                'type' => 'content',
-                                'content' => $delta['content'],
-                            ];
+                        // 跳过空行
+                        if (empty($line)) {
+                            continue;
+                        }
+                        
+                        // 检查是否是 SSE 数据行
+                        if (strpos($line, 'data: ') !== 0) {
+                            dLog('deepseek')->warning('非 SSE 数据行', ['line' => $line]);
+                            continue;
+                        }
+
+                        $data = substr($line, 6); // 移除 "data: " 前缀
+                        
+                        if ($data === '[DONE]') {
+                            dLog('deepseek')->info('收到结束标记');
+                            break 2; // 跳出 foreach 和流读取循环
+                        }
+
+                        try {
+                            $json = json_decode($data, true);
+                            
+                            if (json_last_error() !== JSON_ERROR_NONE) {
+                                dLog('deepseek')->warning('JSON 解析错误: ' . json_last_error_msg(), ['data' => $data]);
+                                continue;
+                            }
+                            
+                            if (isset($json['choices'][0]['delta'])) {
+                                $delta = $json['choices'][0]['delta'];
+                                
+                                // 处理回答内容
+                                if (isset($delta['content'])) {
+                                    $fullContent .= $delta['content'];
+                                    yield [
+                                        'type' => 'content',
+                                        'content' => $delta['content'],
+                                    ];
+                                }
+                            }
+                            
+                            // 获取使用统计信息
+                            if (isset($json['usage'])) {
+                                $usage = $json['usage'];
+                                dLog('deepseek')->info('收到使用统计', ['usage' => $usage]);
+                            }
+                            
+                        } catch (\Exception $e) {
+                            dLog('deepseek')->error('解析流式响应失败: ' . $e->getMessage(), ['line' => $line]);
+                            continue;
                         }
                     }
-                    
-                    // 获取使用统计信息
-                    if (isset($json['usage'])) {
-                        $usage = $json['usage'];
-                        dLog('deepseek')->info('收到使用统计', ['usage' => $usage]);
-                    }
-                    
-                } catch (\Exception $e) {
-                    dLog('deepseek')->error('解析流式响应失败: ' . $e->getMessage(), ['line' => $line]);
-                    continue;
                 }
-            }
-        }
 
-        dLog('deepseek')->info('流式读取完成', [
-            'content_length' => strlen($fullContent)
-        ]);
+                dLog('deepseek')->info('流式读取完成', [
+                    'content_length' => strlen($fullContent)
+                ]);
 
-        yield [
-            'type' => 'done',
-            'full_content' => $fullContent,
-            'full_reasoning' => '',
-            'usage' => $usage
-        ];
+                yield [
+                    'type' => 'done',
+                    'full_content' => $fullContent,
+                    'full_reasoning' => '',
+                    'usage' => $usage
+                ];
+                return;
+            } catch (\Exception $e) {
+                // 已输出内容不重试(避免重复);非可重试异常不重试;重试次数用完则抛出
+                if (!empty($fullContent) || !$this->isGptRetryableException($e) || $attempt >= $maxRetries) {
+                    dLog('deepseek')->error('GPT 流式请求失败: ' . $e->getMessage());
+                    logDB('deepseek', 'error', 'GPT 流式请求失败', ['error' => $e->getMessage()]);
+                    throw $e;
+                }
+
+                dLog('deepseek')->warning('GPT 流式请求失败,准备重试', [
+                    'retry'       => $attempt + 1,
+                    'max_retries' => $maxRetries,
+                    'error'       => $e->getMessage()
+                ]);
+                sleep($interval);
+                $attempt++;
+            }
+        }
     }
 
     // 与推理模型对话
@@ -11755,6 +11874,9 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
             Utils::throwError('20003:GPT-5.4 API Key未配置');
         }
 
+        // 先探活,服务不可用时自动重试
+        $this->ensureGptServiceAvailable();
+
         $client = new Client(['timeout' => 1800, 'verify' => false]);
         $headers = [
             'Authorization' => 'Bearer ' . $apiKey,
@@ -11773,29 +11895,51 @@ Q版卡通风格,头大身小,造型圆润可爱,线条简单,色彩明
         // 确保 stream 为 false
         $post_data['stream'] = false;
 
-        // 备用中转站地址: https://token.ithinkai.cn/v1/chat/completions
-        // 备用中转站地址2: https://api.nonelinear.com/v1/chat/completions
-        $result = $client->post('https://ai-api.kkidc.com/v1/chat/completions', [
-            'json' => $post_data, 
-            'headers' => $headers
-        ]);
+        $maxRetries = $this->gptRetryTimes();
+        $interval = $this->gptRetryInterval();
+        $attempt = 0;
 
-        $response = $result->getBody()->getContents();
-        $response_arr = json_decode($response, true);
-        dLog('deepseek')->info('GPT-5.4请求完成', ['response' => $response_arr]);
+        while (true) {
+            try {
+                // 备用中转站地址: https://token.ithinkai.cn/v1/chat/completions
+                // 备用中转站地址2: https://api.nonelinear.com/v1/chat/completions
+                $result = $client->post('https://ai-api.kkidc.com/v1/chat/completions', [
+                    'json' => $post_data, 
+                    'headers' => $headers
+                ]);
 
-        $fullContent = '';
-        $usage = isset($response_arr['usage']) ? $response_arr['usage'] : [];
+                $response = $result->getBody()->getContents();
+                $response_arr = json_decode($response, true);
+                dLog('deepseek')->info('GPT-5.4请求完成', ['response' => $response_arr]);
 
-        if (isset($response_arr['choices']) && count($response_arr['choices']) > 0) {
-            $fullContent = isset($response_arr['choices'][0]['message']['content']) ? $response_arr['choices'][0]['message']['content'] : '';
-        }
+                $fullContent = '';
+                $usage = isset($response_arr['usage']) ? $response_arr['usage'] : [];
 
-        return [
-            'fullContent' => $fullContent,
-            'fullReasoningContent' => '', // GPT-5.4 不支持思考链
-            'usage' => $usage
-        ];
+                if (isset($response_arr['choices']) && count($response_arr['choices']) > 0) {
+                    $fullContent = isset($response_arr['choices'][0]['message']['content']) ? $response_arr['choices'][0]['message']['content'] : '';
+                }
+
+                return [
+                    'fullContent' => $fullContent,
+                    'fullReasoningContent' => '', // GPT-5.4 不支持思考链
+                    'usage' => $usage
+                ];
+            } catch (\Exception $e) {
+                if (!$this->isGptRetryableException($e) || $attempt >= $maxRetries) {
+                    dLog('deepseek')->error('GPT-5.4 请求失败: ' . $e->getMessage());
+                    logDB('deepseek', 'error', 'GPT-5.4 请求失败', ['error' => $e->getMessage()]);
+                    throw $e;
+                }
+
+                dLog('deepseek')->warning('GPT-5.4 请求失败,准备重试', [
+                    'retry'       => $attempt + 1,
+                    'max_retries' => $maxRetries,
+                    'error'       => $e->getMessage()
+                ]);
+                sleep($interval);
+                $attempt++;
+            }
+        }
     }
 
     private function splitContent($content) {