Просмотр исходного кода

feat: upgrade:ai.term 支持断点重入与 --thinking 参数

- channel 模式逐词写入断点缓存,可重入续跑,--fresh 清除
- 新增 --thinking,经 AITermService::setThinking 透传至 LLM 调用

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
visuddhinanda 4 недель назад
Родитель
Сommit
ac806244b5

+ 79 - 22
api-v13/app/Console/Commands/UpgradeAITerm.php

@@ -2,25 +2,34 @@
 
 namespace App\Console\Commands;
 
-use Illuminate\Console\Command;
-
-use App\Services\AIModelService;
-use App\Services\TermService;
+use App\Http\Resources\AiModelResource;
 use App\Services\AIAssistant\AITermService;
+use App\Services\AIModelService;
 use App\Services\AuthService;
-
-use App\Http\Resources\AiModelResource;
-
+use App\Services\TermService;
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Cache;
 
 class UpgradeAITerm extends Command
 {
+    // 断点缓存键:记录 channel 模式下最后一个已完成的术语游标 ['index'=>int,'word'=>string],
+    // 使命令可重入——中断后重跑自动跳过已完成的术语
+    private const CURSOR_KEY = 'upgrade:ai.term:cursor';
+
     /**
      * The name and signature of the console command.
-     * php artisan upgrade:ai.term  --id=e07bf5b8-bd81-4f0a-9d2c-8e0128b954d7
-     * php artisan upgrade:ai.term
+     * php artisan upgrade:ai.term {model} --id=e07bf5b8-bd81-4f0a-9d2c-8e0128b954d7
+     * php artisan upgrade:ai.term {model} --channel={channelId}
+     *
      * @var string
      */
-    protected $signature = 'upgrade:ai.term {--id=} {--resume} {--model=} ';
+    protected $signature = 'upgrade:ai.term
+    {model : LLM 模型 id}
+    {--word= : 保留参数}
+    {--channel= : 遍历术语表,逐词生成/更新到该 channel}
+    {--id= : 仅处理该术语 guid}
+    {--thinking= : 开启/关闭 deepseek thinking,true | false}
+    {--fresh : 清除断点缓存,从头开始}';
 
     /**
      * The console command description.
@@ -30,9 +39,15 @@ class UpgradeAITerm extends Command
     protected $description = 'Command description';
 
     protected AiModelResource $model;
+
     protected $modelToken;
+
     protected $workChannel;
+
     protected $accessToken;
+
+    protected ?bool $thinking = null;
+
     /**
      * Create a new command instance.
      *
@@ -54,28 +69,70 @@ class UpgradeAITerm extends Command
      */
     public function handle()
     {
-        if (!$this->option('model')) {
-            $modelId = $this->ask('请输入 llm model id');
-        } else {
-            $modelId = $this->option('model');
-        }
+        $modelId = $this->argument('model');
+
         $this->aiTermService->setModel($modelId);
         $this->model = $this->modelService->getModelById($modelId);
         $this->info("model:{$this->model['model']}");
         $this->modelToken = AuthService::getUserToken($modelId);
 
+        if ($this->option('thinking') !== null) {
+            $this->thinking = $this->option('thinking') === 'true';
+        }
+        $this->aiTermService->setThinking($this->thinking);
+
+        if ($this->option('fresh')) {
+            Cache::forget(self::CURSOR_KEY);
+            $this->info('cleared checkpoint');
+        }
+
         if ($this->option('id')) {
-            $terms = [['guid' => $this->option('id'), 'word' => 'word']];
+            $this->info($this->option('id').' running');
+            $result = $this->aiTermService->update($this->option('id'));
+            $this->info('done content '.substr($result, 0, 30));
+        } elseif ($this->option('channel')) {
+            $this->runChannel($this->option('channel'));
         } else {
-            $terms = $this->termService->getCommunityGlossary('zh-Hans')['items']->toArray();
-        }
+            $this->error('id or channel is request');
 
-        foreach ($terms as $key => $term) {
-            $this->info("[{$term['word']}] running");
-            $result = $this->aiTermService->update($term['guid']);
-            $this->info("[{$term['word']}]content " . substr($result, 0, 30));
+            return 1;
         }
 
         return 0;
     }
+
+    /**
+     * 遍历术语表,逐词生成/更新到指定 channel;逐词写入断点以支持重入。
+     *
+     * 断点游标记录最后一个已完成术语的 word 与 index;重跑时优先按 word 在当前术语表中
+     * 定位续跑位置(术语表尾部新增也能正确接续),找不到则回退到 index。
+     */
+    private function runChannel(string $channelId): void
+    {
+        $terms = $this->termService->getGlossary();
+        $total = count($terms);
+        $this->info("{$total} terms");
+
+        $startIndex = 0;
+        $cursor = Cache::get(self::CURSOR_KEY, []);
+        if (! empty($cursor)) {
+            $pos = array_search($cursor['word'] ?? null, $terms, true);
+            $startIndex = $pos !== false ? (int) $pos + 1 : (int) ($cursor['index'] ?? -1) + 1;
+            $this->info("resume from index {$startIndex} (after word [{$cursor['word']}])");
+        }
+
+        for ($key = $startIndex; $key < $total; $key++) {
+            $term = $terms[$key];
+            $this->info("[{$key}/{$total}][{$term}] running");
+            $result = $this->aiTermService->updateOrCreate($channelId, $term);
+            $this->info("[{$term}]content ".substr($result, 0, 30));
+
+            // 该术语完成后写入断点(中途中断时不会误标记未完成的术语)
+            Cache::put(self::CURSOR_KEY, ['index' => $key, 'word' => $term], now()->addHours(48));
+        }
+
+        // 全部完成,清空断点
+        Cache::forget(self::CURSOR_KEY);
+        $this->info('all terms completed');
+    }
 }

+ 27 - 9
api-v13/app/Services/AIAssistant/AITermService.php

@@ -19,6 +19,8 @@ class AITermService
 
     protected $modelToken;
 
+    protected ?bool $thinking = null;
+
     private $sysPrompt = <<<'md'
     请根据提供的文献搜素结果,撰写一个巴利术语的简体中文百科词条。
 
@@ -221,6 +223,16 @@ class AITermService
         return $this;
     }
 
+    /**
+     * 设置 deepseek thinking 开关;null 表示不显式设置,沿用模型默认。
+     */
+    public function setThinking(?bool $thinking): static
+    {
+        $this->thinking = $thinking;
+
+        return $this;
+    }
+
     private function query(string $word): array
     {
         $search = app(OpenSearchService::class);
@@ -245,7 +257,7 @@ class AITermService
                 'link' => $item->getParaLink(),
             ];
         }
-        Log::debug('query ' . count($res));
+        Log::debug('query '.count($res));
 
         return $res;
     }
@@ -289,21 +301,24 @@ class AITermService
         $resText = "# 搜索结果\n```json\n{$res}\n```\n";
         $termText = "# 巴利术语\n\n{$word}\n\n";
         // LLM 生成
-        $response = $this->openAIService->setApiUrl($this->model['url'])
+        $llm = $this->openAIService->setApiUrl($this->model['url'])
             ->setModel($this->model['model'])
             ->setApiKey($this->model['key'])
             ->setSystemPrompt($this->sysPrompt)
             ->setTemperature(0.5)
-            ->setStream(false)
-            ->send($resText . $termText);
+            ->setStream(false);
+        if ($this->thinking !== null) {
+            $llm = $llm->setThinking($this->thinking);
+        }
+        $response = $llm->send($resText.$termText);
 
         $content = $response['choices'][0]['message']['content'] ?? '';
-        $content = str_replace("{{quality|pending}}\n", "{{quality|pending}}", $content);
+        $content = str_replace("{{quality|pending}}\n", '{{quality|pending}}', $content);
         // 输出自检报告
         Log::debug('llm response', ['strlen' => $content]);
         $paraIds = $this->extractAllParaIds($content);
         Log::debug('has paragraph ref ', ['total' => count($paraIds), 'id' => $paraIds]);
-        $searchPid = array_map(fn($item) => $item['pid'], $query);
+        $searchPid = array_map(fn ($item) => $item['pid'], $query);
         $diff = array_values(array_diff($paraIds, $searchPid));
         Log::debug('diff', ['total' => count($diff), 'data' => $diff]);
 
@@ -320,13 +335,16 @@ class AITermService
         $termText = "# 巴利术语\n\n{$word}\n\n";
         $noteText = "# 百科词条正文\n\n{$content}\n";
 
-        $response = $this->openAIService->setApiUrl($this->model['url'])
+        $llm = $this->openAIService->setApiUrl($this->model['url'])
             ->setModel($this->model['model'])
             ->setApiKey($this->model['key'])
             ->setSystemPrompt($this->sysPromptMeaning)
             ->setTemperature(0.3)
-            ->setStream(false)
-            ->send($termText . $noteText);
+            ->setStream(false);
+        if ($this->thinking !== null) {
+            $llm = $llm->setThinking($this->thinking);
+        }
+        $response = $llm->send($termText.$noteText);
 
         $meaning = trim($response['choices'][0]['message']['content'] ?? '');