Explorar o código

Merge branch 'development' of github.com:iapt-platform/mint into development

Jeremy Zheng hai 10 meses
pai
achega
90d791092b

+ 2 - 2
api-v8/app/Console/Commands/ProcessDeadLetterQueue.php

@@ -30,13 +30,13 @@ class ProcessDeadLetterQueue extends Command
         $requeue = $this->option('requeue');
         $requeue = $this->option('requeue');
         $delete = $this->option('delete');
         $delete = $this->option('delete');
 
 
-        $config = config('rabbitmq.connection');
+        $config = config('queue.connections.rabbitmq');
         $connection = new AMQPStreamConnection(
         $connection = new AMQPStreamConnection(
             $config['host'],
             $config['host'],
             $config['port'],
             $config['port'],
             $config['user'],
             $config['user'],
             $config['password'],
             $config['password'],
-            $config['vhost']
+            $config['virtual_host']
         );
         );
 
 
         $channel = $connection->channel();
         $channel = $connection->channel();

+ 28 - 59
api-v8/app/Console/Commands/RabbitMQWorker.php

@@ -3,9 +3,7 @@
 namespace App\Console\Commands;
 namespace App\Console\Commands;
 
 
 use Illuminate\Console\Command;
 use Illuminate\Console\Command;
-use PhpAmqpLib\Connection\AMQPStreamConnection;
 use PhpAmqpLib\Message\AMQPMessage;
 use PhpAmqpLib\Message\AMQPMessage;
-use PhpAmqpLib\Channel\AMQPChannel;
 use App\Jobs\ProcessAITranslateJob;
 use App\Jobs\ProcessAITranslateJob;
 use App\Jobs\BaseRabbitMQJob;
 use App\Jobs\BaseRabbitMQJob;
 use Illuminate\Support\Facades\Log;
 use Illuminate\Support\Facades\Log;
@@ -17,7 +15,7 @@ class RabbitMQWorker extends Command
 {
 {
     /**
     /**
      * The name and signature of the console command.
      * The name and signature of the console command.
-     * php artisan rabbitmq:consume ai_translate
+     * php -d memory_limit=128M artisan rabbitmq:consume ai_translate
      * @var string
      * @var string
      */
      */
     protected $signature = 'rabbitmq:consume {queue} {--reset-loop-count}';
     protected $signature = 'rabbitmq:consume {queue} {--reset-loop-count}';
@@ -31,8 +29,13 @@ class RabbitMQWorker extends Command
     private $queueConfig;
     private $queueConfig;
     private $shouldStop = false;
     private $shouldStop = false;
     private $timeout = 15;
     private $timeout = 15;
-    public function handle(RabbitMQService $consume)
+    private $job = null;
+
+    public function handle()
     {
     {
+        if (\App\Tools\Tools::isStop()) {
+            return 0;
+        }
         $this->queueName = $this->argument('queue');
         $this->queueName = $this->argument('queue');
         $this->queueConfig = config("mint.rabbitmq.queues.{$this->queueName}");
         $this->queueConfig = config("mint.rabbitmq.queues.{$this->queueName}");
 
 
@@ -47,7 +50,7 @@ class RabbitMQWorker extends Command
         $this->info("队列: {$this->queueName}");
         $this->info("队列: {$this->queueName}");
         $this->info("最大循环次数: {$this->maxLoopCount}");
         $this->info("最大循环次数: {$this->maxLoopCount}");
         $this->info("重试次数: {$this->queueConfig['retry_times']}");
         $this->info("重试次数: {$this->queueConfig['retry_times']}");
-
+        $consume = app(RabbitMQService::class);
         try {
         try {
             $consume->setupQueue($this->queueName);
             $consume->setupQueue($this->queueName);
             $this->channel = $consume->getChannel();
             $this->channel = $consume->getChannel();
@@ -66,50 +69,6 @@ class RabbitMQWorker extends Command
         return 0;
         return 0;
     }
     }
 
 
-    /*
-    private function setupQueues()
-    {
-        // 声明主队列
-        $arguments = new AMQPTable([
-            'x-dead-letter-exchange' => '',
-            'x-dead-letter-routing-key' => $this->queueConfig['dead_letter_queue'], // 死信路由键
-        ]);
-        $this->channel->queue_declare(
-            $this->queueName,
-            false,  // passive
-            true,   // durable
-            false,  // exclusive
-            false,  // auto_delete
-            false,  // nowait
-            $arguments
-        );
-
-        // 声明死信队列
-        $dlqName = $this->queueConfig['dead_letter_queue'];
-        $dlqConfig = config("mint.rabbitmq.dead_letter_queues.{$dlqName}", []);
-
-        $dlqArgs = [];
-        if (isset($dlqConfig['ttl'])) {
-            $dlqArgs['x-message-ttl'] =  $dlqConfig['ttl'];
-        }
-        if (isset($dlqConfig['max_length'])) {
-            $dlqArgs['x-max-length'] =  $dlqConfig['max_length'];
-        }
-        $dlqArguments = new AMQPTable($dlqArgs);
-
-        $this->channel->queue_declare(
-            $dlqName,
-            false,  // passive
-            true,   // durable
-            false,  // exclusive
-            false,  // auto_delete
-            false,  // nowait
-            $dlqArguments
-        );
-
-        $this->info("队列设置完成,死信队列: {$dlqName}");
-    }
-*/
     private function startConsuming()
     private function startConsuming()
     {
     {
         $callback = function (AMQPMessage $msg) {
         $callback = function (AMQPMessage $msg) {
@@ -151,15 +110,21 @@ class RabbitMQWorker extends Command
                 $this->info("达到最大循环次数 ({$this->maxLoopCount}),Worker 自动退出");
                 $this->info("达到最大循环次数 ({$this->maxLoopCount}),Worker 自动退出");
                 break;
                 break;
             }
             }
+            if (\App\Tools\Tools::isStop()) {
+                //检测到停止标记
+                break;
+            }
         }
         }
     }
     }
 
 
     private function processMessage(AMQPMessage $msg)
     private function processMessage(AMQPMessage $msg)
     {
     {
         try {
         try {
+
             Log::info('processMessage start', ['message_id' => $msg->get('message_id')]);
             Log::info('processMessage start', ['message_id' => $msg->get('message_id')]);
 
 
-            $data = json_decode($msg->getBody(), true);
+            $data = json_decode($msg->getBody());
+            $this->info("processMessage start " . $msg->get('message_id') . '[' . count($data) . ']');
 
 
             if (json_last_error() !== JSON_ERROR_NONE) {
             if (json_last_error() !== JSON_ERROR_NONE) {
                 throw new \Exception("JSON 解析失败: " . json_last_error_msg());
                 throw new \Exception("JSON 解析失败: " . json_last_error_msg());
@@ -173,14 +138,16 @@ class RabbitMQWorker extends Command
             }
             }
 
 
             // 根据队列类型创建对应的 Job
             // 根据队列类型创建对应的 Job
-            $job = $this->createJob($data, $retryCount);
+            $this->job = $this->createJob($msg->get('message_id'), $data, $retryCount);
 
 
             try {
             try {
                 // 执行业务逻辑
                 // 执行业务逻辑
-                $job->handle();
+                $successful = $this->job->handle();
+                if ($successful) {
+                    // 成功处理,确认消息
+                    $msg->ack();
+                }
 
 
-                // 成功处理,确认消息
-                $msg->ack();
                 $this->processedCount++;
                 $this->processedCount++;
 
 
                 $this->info("消息处理成功 [{$this->processedCount}/{$this->maxLoopCount}]");
                 $this->info("消息处理成功 [{$this->processedCount}/{$this->maxLoopCount}]");
@@ -204,12 +171,12 @@ class RabbitMQWorker extends Command
         }
         }
     }
     }
 
 
-    private function createJob(array $data, int $retryCount): BaseRabbitMQJob
+    private function createJob(string $messageId, array $data, int $retryCount): BaseRabbitMQJob
     {
     {
         // 根据队列名称创建对应的 Job 实例
         // 根据队列名称创建对应的 Job 实例
         switch ($this->queueName) {
         switch ($this->queueName) {
             case 'ai_translate':
             case 'ai_translate':
-                return new ProcessAITranslateJob($this->queueName, $data, $retryCount);
+                return new ProcessAITranslateJob($this->queueName, $messageId, $data, $retryCount);
                 // 可以添加更多队列类型
                 // 可以添加更多队列类型
             default:
             default:
                 throw new \Exception("未知的队列类型: {$this->queueName}");
                 throw new \Exception("未知的队列类型: {$this->queueName}");
@@ -228,7 +195,8 @@ class RabbitMQWorker extends Command
             // 超过重试次数,发送到死信队列
             // 超过重试次数,发送到死信队列
             $this->sendToDeadLetterQueue($data, $e);
             $this->sendToDeadLetterQueue($data, $e);
             $msg->ack(); // 确认原消息以避免重复
             $msg->ack(); // 确认原消息以避免重复
-            $this->error("消息超过最大重试次数,已发送到死信队列");
+            $this->error("消息超过最大重试次数,已发送到死信队列 ");
+            Log::error("消息超过最大重试次数,已发送到死信队列 message_id=" . $msg->get('message_id'));
         }
         }
 
 
         $this->processedCount++;
         $this->processedCount++;
@@ -284,7 +252,6 @@ class RabbitMQWorker extends Command
         Log::error("消息发送到死信队列", [
         Log::error("消息发送到死信队列", [
             'original_queue' => $this->queueName,
             'original_queue' => $this->queueName,
             'dead_letter_queue' => $dlqName,
             'dead_letter_queue' => $dlqName,
-            'data' => $data,
             'error' => $e->getMessage()
             'error' => $e->getMessage()
         ]);
         ]);
     }
     }
@@ -293,7 +260,9 @@ class RabbitMQWorker extends Command
     {
     {
         $this->info("接收到退出信号,正在优雅关闭...");
         $this->info("接收到退出信号,正在优雅关闭...");
         $this->shouldStop = true;
         $this->shouldStop = true;
-
+        if ($this->job) {
+            $this->job->stop();
+        }
         if ($this->channel && $this->channel->is_consuming()) {
         if ($this->channel && $this->channel->is_consuming()) {
             //$this->channel->basic_cancel_on_shutdown(true);
             //$this->channel->basic_cancel_on_shutdown(true);
             $this->channel->basic_cancel('');
             $this->channel->basic_cancel('');

+ 3 - 1
api-v8/app/Console/Commands/TestAiTask.php

@@ -4,6 +4,7 @@ namespace App\Console\Commands;
 
 
 use Illuminate\Console\Command;
 use Illuminate\Console\Command;
 use App\Http\Api\AiTaskPrepare;
 use App\Http\Api\AiTaskPrepare;
+use App\Services\AiTranslateService;
 
 
 class TestAiTask extends Command
 class TestAiTask extends Command
 {
 {
@@ -40,7 +41,8 @@ class TestAiTask extends Command
     public function handle()
     public function handle()
     {
     {
         $taskId = $this->argument('id');
         $taskId = $this->argument('id');
-        $params = AiTaskPrepare::translate($taskId, !$this->option('test'));
+        $ai = app(AiTranslateService::class);
+        $params = $ai->makeByTask($taskId, !$this->option('test'));
         var_dump($params);
         var_dump($params);
         var_dump($this->option('test'));
         var_dump($this->option('test'));
         $this->info('total:' . count($params));
         $this->info('total:' . count($params));

+ 3 - 2
api-v8/app/Console/Commands/TestMq.php

@@ -33,9 +33,8 @@ class TestMq extends Command
      *
      *
      * @return void
      * @return void
      */
      */
-    public function __construct(RabbitMQService $publish)
+    public function __construct()
     {
     {
-        $this->publish = $publish;
         parent::__construct();
         parent::__construct();
     }
     }
 
 
@@ -49,6 +48,8 @@ class TestMq extends Command
         if (\App\Tools\Tools::isStop()) {
         if (\App\Tools\Tools::isStop()) {
             return 0;
             return 0;
         }
         }
+        $publish = app(RabbitMQService::class);
+        $this->publish = $publish;
         $this->publish->publishMessage('ai_translate', ['text' => 'hello']);
         $this->publish->publishMessage('ai_translate', ['text' => 'hello']);
 
 
         Mq::publish('hello', ['hello world']);
         Mq::publish('hello', ['hello world']);

+ 1 - 1
api-v8/app/Http/Api/AiTaskPrepare.php

@@ -150,7 +150,7 @@ class AiTaskPrepare
                 }
                 }
             }
             }
 
 
-            Log::debug('mustache render', ['tpl' => $description, 'data' => $data]);
+            //Log::debug('mustache render', ['tpl' => $description, 'data' => $data]);
             $content = $m->render($description, $data);
             $content = $m->render($description, $data);
             $prompt = $mdRender->convert($content, []);
             $prompt = $mdRender->convert($content, []);
             //gen mq
             //gen mq

+ 17 - 8
api-v8/app/Http/Controllers/TaskStatusController.php

@@ -13,7 +13,8 @@ use App\Http\Resources\TaskResource;
 use App\Http\Api\AuthApi;
 use App\Http\Api\AuthApi;
 use App\Http\Api\WatchApi;
 use App\Http\Api\WatchApi;
 use App\Models\AiModel;
 use App\Models\AiModel;
-use App\Http\Api\AiTaskPrepare;
+use App\Services\AiTranslateService;
+
 
 
 class TaskStatusController extends Controller
 class TaskStatusController extends Controller
 {
 {
@@ -187,13 +188,21 @@ class TaskStatusController extends Controller
                 ->select('assignee_id')->get();
                 ->select('assignee_id')->get();
             $aiAssistant = AiModel::whereIn('uid', $taskAssignee)->first();
             $aiAssistant = AiModel::whereIn('uid', $taskAssignee)->first();
             if ($aiAssistant) {
             if ($aiAssistant) {
-                $aiTask = Task::find($taskId);
-                $aiTask->executor_id = $aiAssistant->uid;
-                $aiTask->status = 'queue';
-                $aiTask->save();
-                $this->pushChange('queue', $taskId);
-                $params = AiTaskPrepare::translate($taskId);
-                Log::debug('ai task', ['message' => count($params)]);
+                try {
+                    $ai = app(AiTranslateService::class);
+                    $params = $ai->makeByTask($taskId, $aiAssistant->uid);
+                    Log::debug('ai task', ['message' => count($params)]);
+                    $aiTask = Task::find($taskId);
+                    $aiTask->executor_id = $aiAssistant->uid;
+                    $aiTask->status = 'queue';
+                    $aiTask->save();
+                    $this->pushChange('queue', $taskId);
+                } catch (\Exception $e) {
+                    Log::error('ai assistant start fail', [
+                        'task' => $taskId,
+                        'error' => $e->getMessage()
+                    ]);
+                }
             }
             }
         }
         }
 
 

+ 4 - 1
api-v8/app/Http/Resources/AiModelResource.php

@@ -3,6 +3,7 @@
 namespace App\Http\Resources;
 namespace App\Http\Resources;
 
 
 use Illuminate\Http\Resources\Json\JsonResource;
 use Illuminate\Http\Resources\Json\JsonResource;
+use App\Http\Api\AiAssistantApi;
 
 
 class AiModelResource extends JsonResource
 class AiModelResource extends JsonResource
 {
 {
@@ -14,6 +15,8 @@ class AiModelResource extends JsonResource
      */
      */
     public function toArray($request)
     public function toArray($request)
     {
     {
-        return parent::toArray($request);
+        $data = parent::toArray($request);
+        $data['user'] = AiAssistantApi::userInfo($this);
+        return $data;
     }
     }
 }
 }

+ 18 - 7
api-v8/app/Jobs/BaseRabbitMQJob.php

@@ -19,12 +19,15 @@ abstract class BaseRabbitMQJob implements ShouldQueue
     protected $currentRetryCount = 0;
     protected $currentRetryCount = 0;
     protected $tries = 0;
     protected $tries = 0;
     protected $timeout = 0;
     protected $timeout = 0;
+    protected $messageId = null;
+    protected $stop = false;
 
 
-    public function __construct(string $queueName, array $messageData, int $retryCount = 0)
+    public function __construct(string $queueName, string $messageId, array $messageData, int $retryCount = 0)
     {
     {
         $this->queueName = $queueName;
         $this->queueName = $queueName;
         $this->messageData = $messageData;
         $this->messageData = $messageData;
         $this->currentRetryCount = $retryCount;
         $this->currentRetryCount = $retryCount;
+        $this->messageId = $messageId;
 
 
         // 从配置读取重试次数和超时时间
         // 从配置读取重试次数和超时时间
         $queueConfig = config("mint.rabbitmq.queues.{$queueName}");
         $queueConfig = config("mint.rabbitmq.queues.{$queueName}");
@@ -32,12 +35,12 @@ abstract class BaseRabbitMQJob implements ShouldQueue
         $this->timeout = $queueConfig['timeout'] ?? 300;
         $this->timeout = $queueConfig['timeout'] ?? 300;
     }
     }
 
 
-    public function handle($messageId = null)
+    public function handle()
     {
     {
         try {
         try {
             Log::info("开始处理队列消息", [
             Log::info("开始处理队列消息", [
                 'queue' => $this->queueName,
                 'queue' => $this->queueName,
-                'message_id' => $this->messageData['id'] ?? 'unknown',
+                'message_id' => $this->messageId ?? 'unknown',
                 'retry_count' => $this->currentRetryCount
                 'retry_count' => $this->currentRetryCount
             ]);
             ]);
 
 
@@ -46,7 +49,7 @@ abstract class BaseRabbitMQJob implements ShouldQueue
 
 
             Log::info("队列消息处理完成", [
             Log::info("队列消息处理完成", [
                 'queue' => $this->queueName,
                 'queue' => $this->queueName,
-                'message_id' => $this->messageData['id'] ?? 'unknown',
+                'message_id' => $this->messageId ?? 'unknown',
                 'result' => $result
                 'result' => $result
             ]);
             ]);
 
 
@@ -54,7 +57,7 @@ abstract class BaseRabbitMQJob implements ShouldQueue
         } catch (\Exception $e) {
         } catch (\Exception $e) {
             Log::error("队列消息处理失败", [
             Log::error("队列消息处理失败", [
                 'queue' => $this->queueName,
                 'queue' => $this->queueName,
-                'message_id' => $this->messageData['id'] ?? 'unknown',
+                'message_id' => $this->messageId ?? 'unknown',
                 'error' => $e->getMessage(),
                 'error' => $e->getMessage(),
                 'retry_count' => $this->currentRetryCount,
                 'retry_count' => $this->currentRetryCount,
                 'max_retries' => $this->tries
                 'max_retries' => $this->tries
@@ -73,7 +76,7 @@ abstract class BaseRabbitMQJob implements ShouldQueue
     {
     {
         Log::error("队列消息最终失败", [
         Log::error("队列消息最终失败", [
             'queue' => $this->queueName,
             'queue' => $this->queueName,
-            'message_id' => $this->messageData['id'] ?? 'unknown',
+            'message_id' => $this->messageId ?? 'unknown',
             'error' => $exception->getMessage(),
             'error' => $exception->getMessage(),
             'retry_count' => $this->currentRetryCount
             'retry_count' => $this->currentRetryCount
         ]);
         ]);
@@ -90,7 +93,7 @@ abstract class BaseRabbitMQJob implements ShouldQueue
         // 默认实现:记录日志
         // 默认实现:记录日志
         Log::error("消息处理最终失败,准备发送到死信队列", [
         Log::error("消息处理最终失败,准备发送到死信队列", [
             'queue' => $this->queueName,
             'queue' => $this->queueName,
-            'message_data' => $messageData,
+            'message_id' => $this->messageId ?? 'unknown',
             'error' => $exception->getMessage()
             'error' => $exception->getMessage()
         ]);
         ]);
     }
     }
@@ -104,4 +107,12 @@ abstract class BaseRabbitMQJob implements ShouldQueue
     {
     {
         return $this->currentRetryCount;
         return $this->currentRetryCount;
     }
     }
+    public function stop()
+    {
+        $this->stop = true;
+    }
+    public function isStop()
+    {
+        return $this->stop;
+    }
 }
 }

+ 6 - 5
api-v8/app/Jobs/ProcessAITranslateJob.php

@@ -7,12 +7,14 @@ use Illuminate\Support\Facades\Log;
 
 
 class ProcessAITranslateJob extends BaseRabbitMQJob
 class ProcessAITranslateJob extends BaseRabbitMQJob
 {
 {
+    private $aiService;
     protected function processMessage(array $messageData)
     protected function processMessage(array $messageData)
     {
     {
         $startTime = microtime(true);
         $startTime = microtime(true);
         try {
         try {
-            $translateService = app(AiTranslateService::class);
-            return $translateService->processTranslate($messageData);
+            // Laravel会自动注入
+            $this->aiService = app(AiTranslateService::class);
+            return $this->aiService->processTranslate($this->messageId, $messageData, $this);
         } catch (\Exception $e) {
         } catch (\Exception $e) {
             // 记录失败指标
             // 记录失败指标
 
 
@@ -28,8 +30,7 @@ class ProcessAITranslateJob extends BaseRabbitMQJob
     {
     {
         parent::handleFinalFailure($messageData, $exception);
         parent::handleFinalFailure($messageData, $exception);
 
 
-        // 订单特定的失败处理
-        $orderService = app(AiTranslateService::class);
-        $orderService->handleFailedTranslate($messageData, $exception);
+        // 消息处理最终失败,准备发送到死信队列
+        $this->aiService->handleFailedTranslate($this->messageId, $messageData, $exception);
     }
     }
 }
 }

+ 612 - 9
api-v8/app/Services/AiTranslateService.php

@@ -3,27 +3,630 @@
 namespace App\Services;
 namespace App\Services;
 
 
 use Illuminate\Support\Facades\Log;
 use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Facades\Http;
+use Illuminate\Http\Client\RequestException;
+use App\Tools\RedisClusters;
+
+use App\Models\Task;
+use App\Models\PaliText;
+use App\Models\PaliSentence;
+use App\Models\AiModel;
+use App\Models\Sentence;
+
+use App\Http\Api\ChannelApi;
+
+use App\Http\Controllers\AuthController;
+
+use App\Http\Api\MdRender;
+use App\Jobs\ProcessAITranslateJob;
+
+class DatabaseException extends \Exception {}
 
 
 class AiTranslateService
 class AiTranslateService
 {
 {
+    private $queue = 'ai_translate';
+    private $modelToken = null;
+    private $task = null;
+    protected $mq;
+    private $apiTimeout = 100;
+    private $llmTimeout = 300;
+    private $taskTopicId;
+    public function __construct()
+    {
+        $this->mq = app(RabbitMQService::class);
+    }
+
+    /**
+     * @param string $messageId
+     * @param array $translateData
+     */
+    public function processTranslate(string $messageId, array $messages, ProcessAITranslateJob $job): bool
+    {
+
+        if (!is_array($messages) || count($messages) === 0) {
+            Log::error('message is not array');
+            return false;
+        }
+
+
+        $first = $messages[0];
+        $this->task = $first->task->info;
+        $taskId = $this->task->id;
+        RedisClusters::put("/task/{$taskId}/message_id", $messageId);
+        $pointerKey = "/task/{$taskId}/pointer";
+        $pointer = 0;
+        if (RedisClusters::has($pointerKey)) {
+            //回到上次中断的点
+            $pointer = RedisClusters::get($pointerKey);
+            Log::info("last break point {$pointer}");
+        }
+
+        //获取model token
+        $this->modelToken = $first->model->token;
+        Log::debug($this->queue . ' ai assistant token', ['token' => $this->modelToken]);
+
+        $this->setTaskStatus($this->task->id, 'running');
+
+        // 设置task discussion topic
+        $this->taskTopicId = $this->taskDiscussion(
+            $this->task->id,
+            'task',
+            $this->task->title,
+            $this->task->category,
+            null
+        );
+
+        for ($i = $pointer; $i < count($messages); $i++) {
+            // 获取当前内存使用量
+            Log::debug("memory usage: " . memory_get_usage(true) / 1024 / 1024 . " MB");
+            // 获取峰值内存使用量
+            Log::debug("memory peak usage: " . memory_get_peak_usage(true) / 1024 / 1024 . " MB");
+            if ($job->isStop()) {
+                Log::info("收到退出信号 pointer={$i}");
+                return false;
+            }
+            if (\App\Tools\Tools::isStop()) {
+                //检测到停止标记
+                return false;
+            }
+            //$this->mq->publishMessage('heartbeat_queue', ['delivery_mode' => 2]);
+            RedisClusters::put($pointerKey, $i);
+            $message = $messages[$i];
+            $taskDiscussionContent = [];
+
+            //推理
+            try {
+                $responseLLM = $this->requestLLM($message);
+                $taskDiscussionContent[] = '- LLM request successful';
+            } catch (RequestException $e) {
+                throw $e;
+            }
+
+
+            if ($this->task->category === 'translate') {
+                //写入句子库
+                $message->sentence->content = $responseLLM['content'];
+                try {
+                    $this->saveSentence($message->sentence);
+                } catch (\Exception $e) {
+                    Log::error('sentence', ['message' => $e]);
+                    continue;
+                }
+            }
+            if ($this->task->category === 'suggest') {
+                //写入pr
+                try {
+                    $this->savePr($message->sentence, $responseLLM['content']);
+                } catch (\Exception $e) {
+                    Log::error('sentence', ['message' => $e]);
+                    continue;
+                }
+            }
+
+            #获取句子id
+            $sUid = $this->getSentenceId($message->sentence);
+
+            //写入句子 discussion
+            $topicId = $this->taskDiscussion(
+                $sUid,
+                'sentence',
+                $this->task->title,
+                $this->task->category,
+                null
+            );
+
+            if ($topicId) {
+                Log::info($this->queue . ' discussion create topic successful');
+                $data['parent'] = $topicId;
+                unset($data['title']);
+                $topicChildren = [];
+                //提示词
+                $topicChildren[] = $message->prompt;
+                //任务结果
+                $topicChildren[] = $responseLLM['content'];
+                //推理过程写入discussion
+                if (
+                    isset($responseLLM['reasoningContent']) &&
+                    !empty($responseLLM['reasoningContent'])
+                ) {
+                    $topicChildren[] = $responseLLM['reasoningContent'];
+                }
+                foreach ($topicChildren as  $content) {
+                    Log::debug($this->queue . ' discussion child request', ['data' => $data]);
+
+                    $dId = $this->taskDiscussion($sUid, 'sentence', $this->task->title, $content, $topicId);
+                    if ($dId) {
+                        Log::info($this->queue . ' discussion child successful');
+                    }
+                }
+            } else {
+                Log::error($this->queue . ' discussion create topic response is null');
+            }
+
+
+            //修改task 完成度
+            $progress = $this->setTaskProgress($message->task->progress);
+            $taskDiscussionContent[] = "- progress=" . $progress;
+            //写入task discussion
+            if ($this->taskTopicId) {
+                $content = implode('\n', $taskDiscussionContent);
+                $dId = $this->taskDiscussion(
+                    $this->task->id,
+                    'task',
+                    $this->task->title,
+                    $content,
+                    $this->taskTopicId
+                );
+            } else {
+                Log::error('no task discussion root');
+            }
+        }
+        //任务完成 修改任务状态为 done
+        if ($i === count($messages)) {
+            $this->setTaskStatus($this->task->id, 'done');
+        }
+        RedisClusters::forget($pointerKey);
+        Log::info('ai translate task complete');
+        return true;
+    }
+    private function setTaskStatus($taskId, $status)
+    {
+        $url = config('app.url') . '/api/v2/task-status/' . $taskId;
+        $data = [
+            'status' => $status,
+        ];
+        Log::debug('ai_translate task status request', ['url' => $url, 'data' => $data]);
+        $response = Http::timeout($this->apiTimeout)->withToken($this->modelToken)->patch($url, $data);
+        //判断状态码
+        if ($response->failed()) {
+            Log::error('ai_translate task status error', ['data' => $response->json()]);
+        } else {
+            Log::info('ai_translate task status done');
+        }
+    }
+
+    private function saveModelLog($token, $data)
+    {
+        $url = config('app.url') . '/api/v2/model-log';
+
+        $response = Http::timeout($this->apiTimeout)->withToken($token)->post($url, $data);
+        if ($response->failed()) {
+            Log::error('ai-translate model log create failed', ['data' => $response->json()]);
+            return false;
+        }
+        return true;
+    }
+
+    private function taskDiscussion($resId, $resType, $title, $content, $parentId = null)
+    {
+        $url = config('app.url') . '/api/v2/discussion';
+        $taskDiscussionData = [
+            'res_id' => $resId,
+            'res_type' => $resType,
+            'content' => $content,
+            'content_type' => 'markdown',
+            'type' => 'discussion',
+            'notification' => false,
+        ];
+        if ($parentId) {
+            $taskDiscussionData['parent'] = $parentId;
+        } else {
+            $taskDiscussionData['title'] = $title;
+        }
+        Log::debug($this->queue . ' discussion create', ['url' => $url, 'data' => json_encode($taskDiscussionData)]);
+
+        $response = Http::timeout($this->apiTimeout)
+            ->withToken($this->modelToken)
+            ->post($url, $taskDiscussionData);
+        if ($response->failed()) {
+            Log::error($this->queue . ' discussion create error', ['data' => $response->json()]);
+            return false;
+        }
+        Log::debug($this->queue . ' discussion create', ['data' => json_encode($response->json())]);
+
+        if (isset($response->json()['data']['id'])) {
+            return $response->json()['data']['id'];
+        }
+        return false;
+    }
+
+    private function requestLLM($message)
+    {
+        $param = [
+            "model" => $message->model->model,
+            "messages" => [
+                ["role" => "system", "content" => $message->model->system_prompt ?? ''],
+                ["role" => "user", "content" => $message->prompt],
+            ],
+            "temperature" => 0.7,
+            "stream" => false
+        ];
+        Log::info($this->queue . ' LLM request' . $message->model->url . ' model:' . $param['model']);
+        Log::debug($this->queue . ' LLM api request', [
+            'url' => $message->model->url,
+            'data' => json_encode($param),
+        ]);
+
+        //写入 model log
+        $modelLogData = [
+            'model_id' => $message->model->uid,
+            'request_at' => now(),
+            'request_data' => json_encode($param, JSON_UNESCAPED_UNICODE),
+        ];
+        //失败重试
+        $maxRetries = 3;
+        $attempt = 0;
+        try {
+            while ($attempt < $maxRetries) {
+                try {
+                    $response = Http::withToken($message->model->key)
+                        ->timeout($this->llmTimeout)
+                        ->post($message->model->url, $param);
+
+                    // 如果状态码是 4xx 或 5xx,会自动抛出 RequestException
+                    $response->throw();
+
+                    Log::info($this->queue . ' LLM request successful');
+
+                    $modelLogData['request_headers'] = json_encode($response->handlerStats(), JSON_UNESCAPED_UNICODE);
+                    $modelLogData['response_headers'] = json_encode($response->headers(), JSON_UNESCAPED_UNICODE);
+                    $modelLogData['status'] = $response->status();
+                    $modelLogData['response_data'] = json_encode($response->json(), JSON_UNESCAPED_UNICODE);
+                    self::saveModelLog($this->modelToken, $modelLogData);
+                    break; // 跳出 while 循环
+                } catch (RequestException $e) {
+                    $attempt++;
+                    $status = $e->response->status();
+
+                    // 某些错误不需要重试
+                    if (in_array($status, [400, 401, 403, 404, 422])) {
+                        Log::warning("客户端错误,不重试: {$status}\n");
+                        throw $e; // 重新抛出异常
+                    }
+                    // 服务器错误或网络错误可以重试
+                    if ($attempt < $maxRetries) {
+                        $delay = pow(2, $attempt); // 指数退避
+                        Log::warning("请求失败(第 {$attempt} 次),{$delay} 秒后重试...\n");
+                        sleep($delay);
+                    } else {
+                        Log::error("达到最大重试次数,请求最终失败\n");
+                        throw $e;
+                    }
+                }
+            }
+        } catch (RequestException $e) {
+            Log::error($this->queue . ' LLM request exception: ' . $e->getMessage());
+            $failResponse = $e->response;
+            $modelLogData['request_headers'] = json_encode($failResponse->handlerStats(), JSON_UNESCAPED_UNICODE);
+            $modelLogData['response_headers'] = json_encode($failResponse->headers(), JSON_UNESCAPED_UNICODE);
+            $modelLogData['status'] = $failResponse->status();
+            $modelLogData['response_data'] = $response->body();
+            $modelLogData['success'] = false;
+            self::saveModelLog($this->modelToken, $modelLogData);
+            throw $e;
+        }
 
 
+        Log::info($this->queue . ' model log saved');
 
 
-    public function __construct() {}
+        $aiData = $response->json();
+        Log::debug($this->queue . ' LLM http response', ['data' => $response->json()]);
+        $responseContent = $aiData['choices'][0]['message']['content'];
+        if (isset($aiData['choices'][0]['message']['reasoning_content'])) {
+            $reasoningContent = $aiData['choices'][0]['message']['reasoning_content'];
+        }
+        $output = ['content' => $responseContent];
+        Log::debug($this->queue . ' LLM response content=' . $responseContent);
+        if (empty($reasoningContent)) {
+            Log::debug($this->queue . ' no reasoningContent');
+        } else {
+            Log::debug($this->queue . ' reasoning=' . $reasoningContent);
+            $output['reasoningContent'] = $reasoningContent;
+        }
+
+        return $output;
+    }
+
+    /**
+     * 写入句子库
+     */
+    private function saveSentence($sentence)
+    {
+        $url = config('app.url') . '/api/v2/sentence';
+
+        Log::info($this->queue . " sentence update {$url}");
+        $response = Http::timeout($this->apiTimeout)->withToken($this->modelToken)->post($url, [
+            'sentences' => [$sentence],
+        ]);
+        if ($response->failed()) {
+            Log::error($this->queue . ' sentence update failed', [
+                'url' => $url,
+                'data' => $response->json(),
+            ]);
+            throw new DatabaseException("sentence 数据库写入错误");
+        }
+        $count = $response->json()['data']['count'];
+        Log::info("{$this->queue} sentence update {$count} successful");
+    }
+
+    private function savePr($sentence, $content)
+    {
+        $url = config('app.url') . '/api/v2/sentpr';
+        Log::info($this->queue . " sentence update {$url}");
+        $response = Http::timeout($this->apiTimeout)->withToken($this->modelToken)->post($url, [
+            'book' => $sentence->book_id,
+            'para' => $sentence->paragraph,
+            'begin' => $sentence->word_start,
+            'end' => $sentence->word_end,
+            'channel' => $sentence->channel_uid,
+            'text' => $content,
+            'notification' => false,
+            'webhook' => false,
+        ]);
+        if ($response->failed()) {
+            Log::error($this->queue . ' sentence update failed', [
+                'url' => $url,
+                'data' => $response->json(),
+            ]);
+            throw new DatabaseException("pr 数据库写入错误");
+        }
+        if ($response->json()['ok']) {
+            Log::info("{$this->queue} sentence suggest update successful");
+        } else {
+            Log::error("{$this->queue} sentence suggest update failed", [
+                'url' => $url,
+                'data' => $response->json(),
+            ]);
+        }
+    }
 
 
-    public function processTranslate(array $translateData): array
+    private function getSentenceId($sentence)
     {
     {
-        $a = $translateData['count'] / 10;
-        Log::debug('AiTranslateService processOrder', $translateData);
-        return [];
+        $url = config('app.url') . '/api/v2/sentence-info/aa';
+        Log::info('ai translate', ['url' => $url]);
+        $response = Http::timeout($this->apiTimeout)->withToken($this->modelToken)->get($url, [
+            'book' => $sentence->book_id,
+            'par' => $sentence->paragraph,
+            'start' => $sentence->word_start,
+            'end' => $sentence->word_end,
+            'channel' => $sentence->channel_uid
+        ]);
+        if (!$response->json()['ok']) {
+            Log::error($this->queue . ' sentence id error', ['data' => $response->json()]);
+            return false;
+        }
+        $sUid = $response->json()['data']['id'];
+        Log::debug("sentence id={$sUid}");
+        return $sUid;
     }
     }
 
 
-    public function handleFailedTranslate(array $translateData, \Exception $exception): void
+    private function setTaskProgress($current)
+    {
+        $taskProgress = $current;
+        if ($taskProgress->total > 0) {
+            $progress = (int)($taskProgress->current * 100 / $taskProgress->total);
+        } else {
+            $progress = 100;
+            Log::error($this->queue . ' progress total is zero', ['task_id' => $this->task->id]);
+        }
+
+        $url = config('app.url') . '/api/v2/task/' . $this->task->id;
+        $data = [
+            'progress' => $progress,
+        ];
+        Log::debug($this->queue . ' task progress request', ['url' => $url, 'data' => $data]);
+        $response = Http::timeout($this->apiTimeout)->withToken($this->modelToken)->patch($url, $data);
+        if ($response->failed()) {
+            Log::error($this->queue . ' task progress error', ['data' => $response->json()]);
+        } else {
+
+            Log::info($this->queue . ' task progress successful progress=' . $response->json()['data']['progress']);
+        }
+        return $progress;
+    }
+    public function handleFailedTranslate(string $messageId, array $translateData, \Exception $exception): void
     {
     {
         try {
         try {
-            //失败时的业务逻辑
-            // 发送失败通知
+            // 彻底失败时的业务逻辑
+            // 设置task为失败状态
+            $this->setTaskStatus($this->task->id, 'stop');
+            //将故障信息写入task discussion
+            if ($this->taskTopicId) {
+                $dId = $this->taskDiscussion(
+                    $this->task->id,
+                    'task',
+                    $this->task->title,
+                    "**处理失败ai任务时出错** 请重启任务 message id={$messageId} 错误信息:" . $exception->getMessage(),
+                    $this->taskTopicId
+                );
+            }
         } catch (\Exception $e) {
         } catch (\Exception $e) {
-            Log::error('处理失败订单时出错', ['error' => $e->getMessage()]);
+            Log::error('处理失败ai任务时出错', ['error' => $e->getMessage()]);
+        }
+    }
+
+    /**
+     * 读取task信息,将任务拆解为单句小任务
+     *
+     * @param  string  $taskId 任务uuid
+     * @return array 拆解后的提示词数组
+     */
+    public function makeByTask(string $taskId, $aiAssistantId, bool $send = true)
+    {
+        $task = Task::findOrFail($taskId);
+        $description = $task->description;
+        $rows = explode("\n", $description);
+        $params = [];
+        foreach ($rows as $key => $row) {
+            if (strpos($row, '=') !== false) {
+                $param = explode('=', trim($row, '|'));
+                $params[$param[0]] = $param[1];
+            }
+        }
+        if (!isset($params['type'])) {
+            Log::error('no $params.type');
+            return false;
+        }
+
+        //get sentences in article
+        $sentences = array();
+        $totalLen = 0;
+        switch ($params['type']) {
+            case 'sentence':
+                if (!isset($params['id'])) {
+                    Log::error('no $params.id');
+                    return false;
+                }
+                $sentences[] = explode('-', $params['id']);
+                break;
+            case 'para':
+                if (!isset($params['book']) || !isset($params['paragraphs'])) {
+                    Log::error('no $params.book or paragraphs');
+                    return false;
+                }
+                $sent = PaliSentence::where('book', $params['book'])
+                    ->where('paragraph', $params['paragraphs'])->orderBy('word_begin')->get();
+                foreach ($sent as $key => $value) {
+                    $sentences[] = [
+                        'id' => [
+                            $value->book,
+                            $value->paragraph,
+                            $value->word_begin,
+                            $value->word_end,
+                        ],
+                        'strlen' => $value->length
+                    ];
+                    $totalLen += $value->length;
+                }
+                break;
+            case 'chapter':
+                if (!isset($params['book']) || !isset($params['paragraphs'])) {
+                    Log::error('no $params.book or paragraphs');
+                    return false;
+                }
+                $chapterLen = PaliText::where('book', $params['book'])
+                    ->where('paragraph', $params['paragraphs'])->value('chapter_len');
+                $sent = PaliSentence::where('book', $params['book'])
+                    ->whereBetween('paragraph', [$params['paragraphs'], $params['paragraphs'] + $chapterLen - 1])
+                    ->orderBy('paragraph')
+                    ->orderBy('word_begin')->get();
+                foreach ($sent as $key => $value) {
+                    $sentences[] = [
+                        'id' => [
+                            $value->book,
+                            $value->paragraph,
+                            $value->word_begin,
+                            $value->word_end,
+                        ],
+                        'strlen' => $value->length
+                    ];
+                    $totalLen += $value->length;
+                }
+                break;
+            default:
+                return false;
+                break;
+        }
+
+        //render prompt
+        $mdRender = new MdRender([
+            'format' => 'prompt',
+            'footnote' => false,
+            'paragraph' => false,
+        ]);
+        $m = new \Mustache_Engine(array(
+            'entity_flags' => ENT_QUOTES,
+            'escape' => function ($value) {
+                return $value;
+            }
+        ));
+
+        # ai model
+        $aiModel = AiModel::findOrFail($aiAssistantId);
+        $modelToken = AuthController::getUserToken($aiModel->uid);
+        $aiModel['token'] = $modelToken;
+        $sumLen = 0;
+        $mqData = [];
+        foreach ($sentences as $key => $sentence) {
+            $sumLen += $sentence['strlen'];
+            $sid = implode('-', $sentence['id']);
+            Log::debug($sid);
+            $sentChannelInfo = explode('@', $params['channel']);
+            $channelId = $sentChannelInfo[0];
+            $data = [];
+            $data['origin'] = '{{' . $sid . '}}';
+            $data['translation'] = '{{sent|id=' . $sid;
+            $data['translation'] .= '|channel=' . $channelId;
+            $data['translation'] .= '|text=translation}}';
+            if (isset($params['nissaya']) && !empty($params['nissaya'])) {
+                $nissayaChannel = explode('@', $params['nissaya']);
+                $channelInfo = ChannelApi::getById($nissayaChannel[0]);
+                if ($channelInfo) {
+                    //查看句子是否存在
+                    $nissayaSent = Sentence::where('book_id', $sentence['id'][0])
+                        ->where('paragraph', $sentence['id'][1])
+                        ->where('word_start', $sentence['id'][2])
+                        ->where('word_end', $sentence['id'][3])
+                        ->where('channel_uid', $nissayaChannel[0])->first();
+                    if ($nissayaSent && !empty($nissayaSent->content)) {
+                        $nissayaData = [];
+                        $nissayaData['channel'] = $channelInfo;
+                        $nissayaData['data'] = '{{sent|id=' . $sid;
+                        $nissayaData['data'] .= '|channel=' . $nissayaChannel[0];
+                        $nissayaData['data'] .= '|text=translation}}';
+                        $data['nissaya'] = $nissayaData;
+                    }
+                }
+            }
+
+            $content = $m->render($description, $data);
+            $prompt = $mdRender->convert($content, []);
+            //gen mq
+            $aiMqData = [
+                'model' => $aiModel,
+                'task' => [
+                    'info' => $task,
+                    'progress' => [
+                        'current' => $sumLen,
+                        'total' => $totalLen
+                    ],
+                ],
+                'prompt' => $prompt,
+                'sentence' => [
+                    'book_id' => $sentence['id'][0],
+                    'paragraph' => $sentence['id'][1],
+                    'word_start' => $sentence['id'][2],
+                    'word_end' => $sentence['id'][3],
+                    'channel_uid' => $channelId,
+                    'content' => $prompt,
+                    'content_type' => 'markdown',
+                    'access_token' => $sentChannelInfo[1] ?? $params['token'],
+                ],
+            ];
+            array_push($mqData, $aiMqData);
+        }
+        if ($send) {
+            $this->mq->publishMessage('ai_translate', $mqData);
         }
         }
+        return $mqData;
     }
     }
 }
 }

+ 51 - 38
api-v8/app/Services/RabbitMQService.php

@@ -7,6 +7,7 @@ use PhpAmqpLib\Channel\AMQPChannel;
 use PhpAmqpLib\Message\AMQPMessage;
 use PhpAmqpLib\Message\AMQPMessage;
 use PhpAmqpLib\Wire\AMQPTable;
 use PhpAmqpLib\Wire\AMQPTable;
 use Illuminate\Support\Facades\Log;
 use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Str;
 
 
 class RabbitMQService
 class RabbitMQService
 {
 {
@@ -49,47 +50,59 @@ class RabbitMQService
 
 
 
 
         // 创建死信交换机
         // 创建死信交换机
-        $this->channel->exchange_declare(
-            $queueConfig['dead_letter_exchange'],
-            'direct',
-            false,
-            true,
-            false
-        );
+        if (isset($queueConfig['dead_letter_exchange'])) {
+            $this->channel->exchange_declare(
+                $queueConfig['dead_letter_exchange'],
+                'direct',
+                false,
+                true,
+                false
+            );
 
 
-        $dlqName = $queueConfig['dead_letter_queue'];
-        $dlqConfig = config("mint.rabbitmq.dead_letter_queues.{$dlqName}", []);
-        $dlqArgs = [];
-        if (isset($dlqConfig['ttl'])) {
-            $dlqArgs['x-message-ttl'] =  $dlqConfig['ttl'];
-        }
-        if (isset($dlqConfig['max_length'])) {
-            $dlqArgs['x-max-length'] =  $dlqConfig['max_length'];
-        }
-        $dlqArguments = new AMQPTable($dlqArgs);
+            $dlqName = $queueConfig['dead_letter_queue'];
+            $dlqConfig = config("mint.rabbitmq.dead_letter_queues.{$dlqName}", []);
+            $dlqArgs = [];
+            if (isset($dlqConfig['ttl'])) {
+                $dlqArgs['x-message-ttl'] =  $dlqConfig['ttl'];
+            }
+            if (isset($dlqConfig['max_length'])) {
+                $dlqArgs['x-max-length'] =  $dlqConfig['max_length'];
+            }
+            $dlqArguments = new AMQPTable($dlqArgs);
+
+            // 创建死信队列
+            $this->channel->queue_declare(
+                $dlqName,
+                false,  // passive
+                true,   // durable
+                false,  // exclusive
+                false,  // auto_delete
+                false,  // nowait
+                $dlqArguments
+            );
 
 
-        // 创建死信队列
-        $this->channel->queue_declare(
-            $dlqName,
-            false,  // passive
-            true,   // durable
-            false,  // exclusive
-            false,  // auto_delete
-            false,  // nowait
-            $dlqArguments
-        );
+            // 绑定死信队列到死信交换机
+            $this->channel->queue_bind(
+                $queueConfig['dead_letter_queue'],
+                $queueConfig['dead_letter_exchange']
+            );
 
 
-        // 绑定死信队列到死信交换机
-        $this->channel->queue_bind(
-            $queueConfig['dead_letter_queue'],
-            $queueConfig['dead_letter_exchange']
-        );
+            // 创建主队列,配置死信
+            $arguments = new AMQPTable([
+                'x-dead-letter-exchange' => $queueConfig['dead_letter_exchange'],
+                'x-dead-letter-routing-key' => $queueConfig['dead_letter_queue'], // 死信路由键
+            ]);
+        } else {
+            $workerArgs = [];
+            if (isset($queueConfig['ttl'])) {
+                $workerArgs['x-message-ttl'] =  $queueConfig['ttl'];
+            }
+            if (isset($queueConfig['max_length'])) {
+                $workerArgs['x-max-length'] =  $queueConfig['max_length'];
+            }
+            $arguments = new AMQPTable($workerArgs);
+        }
 
 
-        // 创建主队列,配置死信
-        $arguments = new AMQPTable([
-            'x-dead-letter-exchange' => $queueConfig['dead_letter_exchange'],
-            'x-dead-letter-routing-key' => $queueConfig['dead_letter_queue'], // 死信路由键
-        ]);
 
 
         $this->channel->queue_declare(
         $this->channel->queue_declare(
             $queueName,
             $queueName,
@@ -112,7 +125,7 @@ class RabbitMQService
                 [
                 [
                     'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT,
                     'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT,
                     'timestamp' => time(),
                     'timestamp' => time(),
-                    'message_id' => uniqid(),
+                    'message_id' => Str::uuid(),
                     "content_type" => 'application/json; charset=utf-8'
                     "content_type" => 'application/json; charset=utf-8'
                 ]
                 ]
             );
             );

+ 4 - 0
api-v8/config/mint.php

@@ -133,6 +133,10 @@ return [
                 'dead_letter_queue' => 'ai_translate_dlq',
                 'dead_letter_queue' => 'ai_translate_dlq',
                 'dead_letter_exchange' => 'ai_translate_dlx',
                 'dead_letter_exchange' => 'ai_translate_dlx',
             ],
             ],
+            'heartbeat_queue' => [
+                'ttl' => 86400000, // 24小时 TTL (毫秒)
+                'max_length' => 10000,
+            ]
         ],
         ],
 
 
         // 死信队列配置
         // 死信队列配置

+ 1 - 0
api-v8/config/queue.php

@@ -77,6 +77,7 @@ return [
             'user' => env('RABBITMQ_USER', 'guest'),
             'user' => env('RABBITMQ_USER', 'guest'),
             'password' => env('RABBITMQ_PASSWORD', 'guest'),
             'password' => env('RABBITMQ_PASSWORD', 'guest'),
             'virtual_host' => env('RABBITMQ_VIRTUAL_HOST', '/'),
             'virtual_host' => env('RABBITMQ_VIRTUAL_HOST', '/'),
+            'heartbeat' => env('RABBITMQ_HEARTBEAT', 60), // 心跳时间(秒)
         ],
         ],
 
 
     ],
     ],

+ 9 - 0
api-v8/resources/views/library/book/read.blade.php

@@ -65,6 +65,15 @@
             .content-area {
             .content-area {
                 width: 100%;
                 width: 100%;
             }
             }
+
+            .main-container {
+                padding: 0;
+
+            }
+
+            .card {
+                border: none;
+            }
         }
         }
 
 
         /* Tablet: Show TOC and content */
         /* Tablet: Show TOC and content */

+ 10 - 4
dashboard-v4/dashboard/src/components/ai/AiModelList.tsx

@@ -15,6 +15,7 @@ import AiModelCreate from "./AiModelCreate";
 import PublicityIcon from "../studio/PublicityIcon";
 import PublicityIcon from "../studio/PublicityIcon";
 import ShareModal from "../share/ShareModal";
 import ShareModal from "../share/ShareModal";
 import { EResType } from "../share/Share";
 import { EResType } from "../share/Share";
+import User from "../auth/User";
 
 
 interface IWidget {
 interface IWidget {
   studioName?: string;
   studioName?: string;
@@ -73,9 +74,14 @@ const AiModelList = ({ studioName }: IWidget) => {
             dataIndex: "name",
             dataIndex: "name",
             render(dom, entity, index, action, schema) {
             render(dom, entity, index, action, schema) {
               return (
               return (
-                <Link to={`/studio/${studioName}/ai/models/${entity.uid}/edit`}>
-                  {entity.name}
-                </Link>
+                <Space>
+                  <PublicityIcon value={entity.privacy} />
+                  <Link
+                    to={`/studio/${studioName}/ai/models/${entity.uid}/edit`}
+                  >
+                    {entity.name}
+                  </Link>
+                </Space>
               );
               );
             },
             },
           },
           },
@@ -94,7 +100,7 @@ const AiModelList = ({ studioName }: IWidget) => {
           },
           },
           avatar: {
           avatar: {
             render(dom, entity, index, action, schema) {
             render(dom, entity, index, action, schema) {
-              return <PublicityIcon value={entity.privacy} />;
+              return <User {...entity.user} showName={false} />;
             },
             },
           },
           },
           actions: {
           actions: {

+ 1 - 0
dashboard-v4/dashboard/src/components/api/ai.ts

@@ -50,6 +50,7 @@ export interface IAiModel {
   privacy: TPrivacy;
   privacy: TPrivacy;
   owner: IStudio;
   owner: IStudio;
   editor: IUser;
   editor: IUser;
+  user: IUser;
   created_at: string;
   created_at: string;
   updated_at: string;
   updated_at: string;
 }
 }

+ 3 - 0
dashboard-v4/dashboard/src/components/task/TaskStatus.tsx

@@ -55,6 +55,9 @@ const TaskStatus = ({ task }: IWidget) => {
     case "requested_restart":
     case "requested_restart":
       color = "warning";
       color = "warning";
       break;
       break;
+    case "stop":
+      color = "error";
+      break;
   }
   }
 
 
   return (
   return (

+ 3 - 0
dashboard-v4/dashboard/src/components/task/TaskStatusButton.tsx

@@ -105,6 +105,9 @@ const TaskStatusButton = ({
     case "queue":
     case "queue":
       menuEnable = ["stop"];
       menuEnable = ["stop"];
       break;
       break;
+    case "stop":
+      menuEnable = ["restarted"];
+      break;
   }
   }
 
 
   const items: IStatusMenu[] = StatusButtons.map((item) => {
   const items: IStatusMenu[] = StatusButtons.map((item) => {

+ 1 - 0
dashboard-v4/dashboard/src/locales/en-US/label.ts

@@ -68,6 +68,7 @@ const items = {
   "labels.task.status.canceled": "canceled",
   "labels.task.status.canceled": "canceled",
   "labels.task.status.expired": "expired",
   "labels.task.status.expired": "expired",
   "labels.task.status.queue": "queue",
   "labels.task.status.queue": "queue",
+  "labels.task.status.stop": "stop",
   "labels.filter": "filter",
   "labels.filter": "filter",
   "labels.participants": "participants",
   "labels.participants": "participants",
   "labels.task.category": "task category",
   "labels.task.category": "task category",

+ 1 - 0
dashboard-v4/dashboard/src/locales/zh-Hans/label.ts

@@ -76,6 +76,7 @@ const items = {
   "labels.task.status.canceled": "已取消",
   "labels.task.status.canceled": "已取消",
   "labels.task.status.expired": "已过期",
   "labels.task.status.expired": "已过期",
   "labels.task.status.queue": "排队中",
   "labels.task.status.queue": "排队中",
+  "labels.task.status.stop": "停止",
   "labels.filter": "过滤器",
   "labels.filter": "过滤器",
   "labels.participants": "参与者",
   "labels.participants": "参与者",
   "labels.task.category": "任务类型",
   "labels.task.category": "任务类型",