Ver Fonte

学案报告

yemeishu há 4 dias atrás
pai
commit
01265edb3f

+ 18 - 11
app/Filament/Resources/MarkdownImportResource.php

@@ -224,7 +224,7 @@ class MarkdownImportResource extends Resource
                     ->wrap()
                     ->width('200px'),
 
-                // 状态列 - 固定宽度
+                // 状态列 - 固定宽度,只显示基本状态
                 TextColumn::make('current_status')
                     ->label('状态')
                     ->getStateUsing(function (?Model $record): string {
@@ -252,28 +252,35 @@ class MarkdownImportResource extends Resource
                     })
                     ->width('120px'),
 
-                // 详细信息列 - 自适应剩余空间,完整显示所有内容
+                // 详细信息列 - 显示完整进度信息,包括总数
                 TextColumn::make('detailed_progress')
                     ->label('详情')
                     ->getStateUsing(function (?Model $record): string {
                         if (!$record) return '—';
 
                         return match ($record->status) {
-                            'processing' => $record->progress_message ?: 'AI 正在解析题目...',
+                            'processing' => $record->progress_message ?: sprintf(
+                                'AI 解析中:%d/%d 题',
+                                $record->progress_current ?? 0,
+                                $record->progress_total ?? 0
+                            ),
                             'parsed' => sprintf(
-                                '已解析 %d 个候选题,请进入校对环节',
-                                $record->parsed_count ?? 0
+                                '已解析 %d/%d 题,等待校对',
+                                $record->parsed_count ?? 0,
+                                $record->progress_total ?? 0
                             ),
                             'reviewed' => sprintf(
-                                '已校对 %d 个候选题,请确认入库',
-                                $record->accepted_count ?? 0
+                                '已校对 %d/%d 题,待入库',
+                                $record->accepted_count ?? 0,
+                                $record->progress_total ?? 0
                             ),
                             'completed' => sprintf(
-                                '成功入库 %d 个题目',
-                                $record->accepted_count ?? 0
+                                '成功入库 %d/%d 题',
+                                $record->accepted_count ?? 0,
+                                $record->progress_total ?? 0
                             ),
-                            'failed' => $record->error_message ?: '未知错误',
-                            'pending' => '准备就绪,等待开始解析',
+                            'failed' => '处理失败:' . ($record->error_message ?: '未知错误'),
+                            'pending' => '准备就绪,总计 ' . ($record->progress_total ?? 0) . ' 题',
                             default => '—',
                         };
                     })

+ 60 - 5
app/Services/MarkdownQuestionParser.php

@@ -15,6 +15,9 @@ class MarkdownQuestionParser
     private string $openAiBaseUrl;
     private string $openAiModel;
     private int $openAiTimeout;
+    private array $deepseekApiKeys;
+    private array $openAiApiKeys;
+    private bool $rateLimited = false;
 
     public function __construct()
     {
@@ -23,10 +26,16 @@ class MarkdownQuestionParser
         $this->deepseekBaseUrl = rtrim((string) config('ai.deepseek.base_url', 'https://api.deepseek.com/v1'), '/');
         $this->deepseekModel = (string) config('ai.deepseek.model', 'deepseek-chat');
         $this->deepseekTimeout = (int) config('ai.deepseek.timeout', 30);
+        $this->deepseekApiKeys = $this->splitApiKeys(
+            (string) config('ai.deepseek.api_keys', env('DEEPSEEK_API_KEYS', ''))
+        );
 
         $this->openAiBaseUrl = rtrim((string) config('ai.openai.base_url', 'https://api.openai.com/v1'), '/');
         $this->openAiModel = (string) config('ai.openai.model', 'gpt-3.5-turbo');
         $this->openAiTimeout = (int) config('ai.openai.timeout', 30);
+        $this->openAiApiKeys = $this->splitApiKeys(
+            (string) config('ai.openai.api_keys', env('OPENAI_API_KEYS', ''))
+        );
     }
 
     /**
@@ -83,7 +92,9 @@ class MarkdownQuestionParser
             return array_merge($candidate, $aiStructured);
         }
 
-        $this->enhanceWithAi($candidate);
+        if (!$this->rateLimited) {
+            $this->enhanceWithAi($candidate);
+        }
 
         Log::debug('Parse raw_markdown done (heuristic+detect)', [
             'index' => $index,
@@ -195,6 +206,9 @@ class MarkdownQuestionParser
 
             return $normalized;
         } catch (\Throwable $e) {
+            if ($this->isRateLimited($e)) {
+                $this->rateLimited = true;
+            }
             Log::warning('AI structured parse failed, fallback to heuristic', [
                 'index' => $index,
                 'error' => $e->getMessage(),
@@ -297,6 +311,9 @@ class MarkdownQuestionParser
                 $candidate['ai_confidence'] = $result['confidence'] ?? null;
             }
         } catch (\Exception $e) {
+            if ($this->isRateLimited($e)) {
+                $this->rateLimited = true;
+            }
             Log::error('AI question detection failed', [
                 'error' => $e->getMessage(),
                 'block' => substr($candidate['raw_markdown'], 0, 200),
@@ -340,7 +357,10 @@ class MarkdownQuestionParser
      */
     private function callDeepSeek(string $prompt): array
     {
-        $apiKey = config('ai.deepseek.api_key', env('DEEPSEEK_API_KEY'));
+        $apiKey = $this->resolveApiKey(
+            $this->deepseekApiKeys,
+            (string) config('ai.deepseek.api_key', env('DEEPSEEK_API_KEY'))
+        );
 
         $response = Http::withHeaders([
             'Authorization' => "Bearer {$apiKey}",
@@ -354,7 +374,7 @@ class MarkdownQuestionParser
         ]);
 
         if (!$response->successful()) {
-            throw new \Exception('DeepSeek API error: ' . $response->body());
+            throw new \Exception('DeepSeek API error: HTTP ' . $response->status() . ' ' . $response->body());
         }
 
         $content = $response->json('choices.0.message.content');
@@ -367,7 +387,10 @@ class MarkdownQuestionParser
      */
     private function callOpenAI(string $prompt): array
     {
-        $apiKey = config('ai.openai.api_key', env('OPENAI_API_KEY'));
+        $apiKey = $this->resolveApiKey(
+            $this->openAiApiKeys,
+            (string) config('ai.openai.api_key', env('OPENAI_API_KEY'))
+        );
 
         $response = Http::withHeaders([
             'Authorization' => "Bearer {$apiKey}",
@@ -381,7 +404,7 @@ class MarkdownQuestionParser
         ]);
 
         if (!$response->successful()) {
-            throw new \Exception('OpenAI API error: ' . $response->body());
+            throw new \Exception('OpenAI API error: HTTP ' . $response->status() . ' ' . $response->body());
         }
 
         $content = $response->json('choices.0.message.content');
@@ -389,6 +412,23 @@ class MarkdownQuestionParser
         return $this->parseJsonResponse($content);
     }
 
+    private function resolveApiKey(array $keys, string $fallback): string
+    {
+        if (empty($keys)) {
+            return $fallback;
+        }
+
+        $index = (int) (crc32((string) getmypid()) % count($keys));
+        return $keys[$index];
+    }
+
+    private function splitApiKeys(string $raw): array
+    {
+        $items = array_map('trim', explode(',', $raw));
+        $items = array_filter($items, static fn (string $key) => $key !== '');
+        return array_values(array_unique($items));
+    }
+
     /**
      * 精修简答题的解题步骤
      */
@@ -426,4 +466,19 @@ class MarkdownQuestionParser
 
         return $json;
     }
+
+    private function isRateLimited(\Throwable $e): bool
+    {
+        $message = $e->getMessage();
+        if (stripos($message, 'HTTP 429') !== false) {
+            return true;
+        }
+        if (stripos($message, 'rate limit') !== false) {
+            return true;
+        }
+        if (stripos($message, 'too many requests') !== false) {
+            return true;
+        }
+        return false;
+    }
 }