AiTranslateService.php 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711
  1. <?php
  2. namespace App\Services;
  3. use App\Exceptions\SectionTimeoutException;
  4. use App\Exceptions\TaskFailException;
  5. use App\Http\Api\ChannelApi;
  6. use App\Http\Api\MdRender;
  7. use App\Models\AiModel;
  8. use App\Models\PaliSentence;
  9. use App\Models\PaliText;
  10. use App\Models\Sentence;
  11. use App\Models\Task;
  12. use App\Tools\Tools;
  13. use Illuminate\Http\Client\RequestException;
  14. use Illuminate\Support\Facades\Cache;
  15. use Illuminate\Support\Facades\Http;
  16. use Illuminate\Support\Facades\Log;
  17. class DatabaseException extends \Exception {}
  18. class AiTranslateService
  19. {
  20. private $queue = 'ai_translate_v2';
  21. private $modelToken = null;
  22. private $task = null;
  23. protected $mq;
  24. private $apiTimeout = 30;
  25. private $llmTimeout = 300;
  26. private $taskTopicId;
  27. private $stop = false;
  28. private $maxProcessTime = 15 * 60; // 一个句子的最大处理时间
  29. private $mqTimeout = 60;
  30. private $openaiProxy = null;
  31. public function __construct() {}
  32. public function setProxy(string $proxy): self
  33. {
  34. $this->openaiProxy = $proxy;
  35. return $this;
  36. }
  37. /**
  38. * @param array $translateData
  39. */
  40. public function processTranslate(string $messageId, array $messages): bool
  41. {
  42. $start = time();
  43. if (! is_array($messages) || count($messages) === 0) {
  44. Log::error('message is not array');
  45. return false;
  46. }
  47. $first = $messages[0];
  48. $this->task = $first->task->info;
  49. $taskId = $this->task->id;
  50. Cache::put("/task/{$taskId}/message_id", $messageId);
  51. $pointerKey = "/task/{$taskId}/pointer";
  52. $pointer = 0;
  53. if (Cache::has($pointerKey)) {
  54. // 回到上次中断的点
  55. $pointer = Cache::get($pointerKey);
  56. Log::info("last break point {$pointer}");
  57. }
  58. // 获取model token
  59. $this->modelToken = $first->model->token;
  60. $this->setTaskStatus($this->task->id, 'running');
  61. // 设置task discussion topic
  62. $this->taskTopicId = $this->taskDiscussion(
  63. $this->task->id,
  64. 'task',
  65. $this->task->title,
  66. $this->task->category,
  67. null
  68. );
  69. $time = [$this->maxProcessTime];
  70. for ($i = $pointer; $i < count($messages); $i++) {
  71. // 获取当前内存使用量
  72. Log::debug('memory usage: '.memory_get_usage(true) / 1024 / 1024 .' MB');
  73. // 获取峰值内存使用量
  74. Log::debug('memory peak usage: '.memory_get_peak_usage(true) / 1024 / 1024 .' MB');
  75. if ($this->stop) {
  76. Log::info("收到退出信号 pointer={$i}");
  77. return false;
  78. }
  79. if (Tools::isStop()) {
  80. // 检测到停止标记
  81. return false;
  82. }
  83. Cache::put($pointerKey, $i);
  84. $message = $messages[$i];
  85. $taskDiscussionContent = [];
  86. // 推理
  87. $responseLLM = $this->requestLLM($message);
  88. $taskDiscussionContent[] = '- LLM request successful';
  89. if ($this->task->category === 'translate') {
  90. // 写入句子库
  91. $message->sentence->content = $responseLLM['content'];
  92. try {
  93. $this->saveSentence($message->sentence);
  94. } catch (\Exception $e) {
  95. Log::error('sentence', ['message' => $e]);
  96. continue;
  97. }
  98. }
  99. if ($this->task->category === 'suggest') {
  100. // 写入pr
  101. try {
  102. $this->savePr($message->sentence, $responseLLM['content']);
  103. } catch (\Exception $e) {
  104. Log::error('sentence', ['message' => $e]);
  105. continue;
  106. }
  107. }
  108. // 获取句子id
  109. $sUid = $this->getSentenceId($message->sentence);
  110. // 写入句子 discussion
  111. $topicId = $this->taskDiscussion(
  112. $sUid,
  113. 'sentence',
  114. $this->task->title,
  115. $this->task->category,
  116. null
  117. );
  118. if ($topicId) {
  119. Log::info($this->queue.' discussion create topic successful');
  120. $data['parent'] = $topicId;
  121. unset($data['title']);
  122. $topicChildren = [];
  123. // 提示词
  124. $topicChildren[] = $message->prompt;
  125. // 任务结果
  126. $topicChildren[] = $responseLLM['content'];
  127. // 推理过程写入discussion
  128. if (
  129. isset($responseLLM['reasoningContent']) &&
  130. ! empty($responseLLM['reasoningContent'])
  131. ) {
  132. $topicChildren[] = $responseLLM['reasoningContent'];
  133. }
  134. foreach ($topicChildren as $content) {
  135. Log::debug($this->queue.' discussion child request', ['data' => $data]);
  136. $dId = $this->taskDiscussion($sUid, 'sentence', $this->task->title, $content, $topicId);
  137. if ($dId) {
  138. Log::info($this->queue.' discussion child successful');
  139. }
  140. }
  141. } else {
  142. Log::error($this->queue.' discussion create topic response is null');
  143. }
  144. // 修改task 完成度
  145. $progress = $this->setTaskProgress($message->task->progress);
  146. $taskDiscussionContent[] = '- progress='.$progress;
  147. // 写入task discussion
  148. if ($this->taskTopicId) {
  149. $content = implode("\n", $taskDiscussionContent);
  150. $dId = $this->taskDiscussion(
  151. $this->task->id,
  152. 'task',
  153. $this->task->title,
  154. $content,
  155. $this->taskTopicId
  156. );
  157. } else {
  158. Log::error('no task discussion root');
  159. }
  160. // 计算剩余时间是否足够再做一次
  161. $time[] = time() - $start;
  162. rsort($time);
  163. $remain = $this->mqTimeout - (time() - $start);
  164. if ($remain < $time[0]) {
  165. throw new SectionTimeoutException;
  166. }
  167. }
  168. // 任务完成 修改任务状态为 done
  169. if ($i === count($messages)) {
  170. $this->setTaskStatus($this->task->id, 'done');
  171. Cache::forget($pointerKey);
  172. Log::info('ai translate task complete');
  173. }
  174. return true;
  175. }
  176. private function setTaskStatus($taskId, $status)
  177. {
  178. $url = config('app.url').'/api/v2/task-status/'.$taskId;
  179. $data = [
  180. 'status' => $status,
  181. ];
  182. Log::debug('ai_translate task status request', ['url' => $url, 'data' => $data]);
  183. $response = Http::timeout($this->apiTimeout)->withToken($this->modelToken)->patch($url, $data);
  184. // 判断状态码
  185. if ($response->failed()) {
  186. Log::error('ai_translate task status error', ['data' => $response->json()]);
  187. } else {
  188. Log::info('ai_translate task status done');
  189. }
  190. }
  191. private function saveModelLog($token, $data)
  192. {
  193. $url = config('app.url').'/api/v2/model-log';
  194. $response = Http::timeout($this->apiTimeout)->withToken($token)->post($url, $data);
  195. if ($response->failed()) {
  196. Log::error('ai-translate model log create failed', ['data' => $response->json()]);
  197. return false;
  198. }
  199. return true;
  200. }
  201. private function taskDiscussion($resId, $resType, $title, $content, $parentId = null)
  202. {
  203. $url = config('app.url').'/api/v2/discussion';
  204. $taskDiscussionData = [
  205. 'res_id' => $resId,
  206. 'res_type' => $resType,
  207. 'content' => $content,
  208. 'content_type' => 'markdown',
  209. 'type' => 'discussion',
  210. 'notification' => false,
  211. ];
  212. if ($parentId) {
  213. $taskDiscussionData['parent'] = $parentId;
  214. } else {
  215. $taskDiscussionData['title'] = $title;
  216. }
  217. Log::debug($this->queue.' discussion create', ['url' => $url, 'data' => json_encode($taskDiscussionData)]);
  218. $response = Http::timeout($this->apiTimeout)
  219. ->withToken($this->modelToken)
  220. ->post($url, $taskDiscussionData);
  221. if ($response->failed()) {
  222. Log::error($this->queue.' discussion create error', ['data' => $response->json()]);
  223. return false;
  224. }
  225. Log::debug($this->queue.' discussion create', ['data' => json_encode($response->json())]);
  226. if (isset($response->json()['data']['id'])) {
  227. return $response->json()['data']['id'];
  228. }
  229. return false;
  230. }
  231. private function requestLLM($message)
  232. {
  233. $param = [
  234. 'model' => $message->model->model,
  235. 'messages' => [
  236. ['role' => 'system', 'content' => $message->model->system_prompt ?? ''],
  237. ['role' => 'user', 'content' => $message->prompt],
  238. ],
  239. 'temperature' => 0.3, // 低随机性,确保准确
  240. 'top_k' => 20, // 限制候选词范围
  241. 'stream' => false,
  242. ];
  243. if ($this->openaiProxy) {
  244. $requestUrl = $this->openaiProxy;
  245. $body = [
  246. 'open_ai_url' => $message->model->url,
  247. 'api_key' => $message->model->key,
  248. 'payload' => $param,
  249. ];
  250. } else {
  251. $requestUrl = $message->model->url;
  252. $body = $param;
  253. }
  254. Log::info($this->queue.' LLM request '.$message->model->url.' model:'.$param['model']);
  255. Log::debug($this->queue.' LLM api request', [
  256. 'url' => $message->model->url,
  257. 'data' => json_encode($param),
  258. ]);
  259. // 写入 model log
  260. $modelLogData = [
  261. 'model_id' => $message->model->uid,
  262. 'request_at' => now(),
  263. 'request_data' => json_encode($param, JSON_UNESCAPED_UNICODE),
  264. ];
  265. // 失败重试
  266. $maxRetries = 3;
  267. $attempt = 0;
  268. try {
  269. while ($attempt < $maxRetries) {
  270. try {
  271. $response = Http::withToken($message->model->key)
  272. ->timeout($this->llmTimeout)
  273. ->post($requestUrl, $body);
  274. // 如果状态码是 4xx 或 5xx,会自动抛出 RequestException
  275. $response->throw();
  276. Log::info($this->queue.' LLM request successful');
  277. $modelLogData['request_headers'] = json_encode($response->handlerStats(), JSON_UNESCAPED_UNICODE);
  278. $modelLogData['response_headers'] = json_encode($response->headers(), JSON_UNESCAPED_UNICODE);
  279. $modelLogData['status'] = $response->status();
  280. $modelLogData['response_data'] = json_encode($response->json(), JSON_UNESCAPED_UNICODE);
  281. self::saveModelLog($this->modelToken, $modelLogData);
  282. break; // 跳出 while 循环
  283. } catch (RequestException $e) {
  284. Log::error($this->queue.' LLM request exception: '.$e->getMessage());
  285. $failResponse = $e->response;
  286. $modelLogData['request_headers'] = json_encode($failResponse->handlerStats(), JSON_UNESCAPED_UNICODE);
  287. $modelLogData['response_headers'] = json_encode($failResponse->headers(), JSON_UNESCAPED_UNICODE);
  288. $modelLogData['status'] = $failResponse->status();
  289. $modelLogData['response_data'] = $response->body();
  290. $modelLogData['success'] = false;
  291. self::saveModelLog($this->modelToken, $modelLogData);
  292. $attempt++;
  293. $status = $e->response->status();
  294. // 某些错误不需要重试
  295. if (in_array($status, [400, 401, 403, 404, 422])) {
  296. Log::warning("客户端错误,不重试: {$status}\n");
  297. throw new TaskFailException; // 重新抛出异常
  298. }
  299. // 服务器错误或网络错误可以重试
  300. if ($attempt < $maxRetries) {
  301. $delay = pow(2, $attempt); // 指数退避
  302. Log::warning("请求失败(第 {$attempt} 次),{$delay} 秒后重试...\n");
  303. sleep($delay);
  304. } else {
  305. Log::error("达到最大重试次数,请求最终失败\n");
  306. throw new TaskFailException;
  307. }
  308. } catch (\Exception $e) {
  309. throw $e;
  310. }
  311. }
  312. } catch (\Exception $e) {
  313. throw $e;
  314. }
  315. Log::info($this->queue.' model log saved');
  316. $aiData = $response->json();
  317. Log::debug($this->queue.' LLM http response', ['data' => $response->json()]);
  318. $responseContent = $aiData['choices'][0]['message']['content'];
  319. if (isset($aiData['choices'][0]['message']['reasoning_content'])) {
  320. $reasoningContent = $aiData['choices'][0]['message']['reasoning_content'];
  321. }
  322. $output = ['content' => $responseContent];
  323. Log::debug($this->queue.' LLM response content='.$responseContent);
  324. if (empty($reasoningContent)) {
  325. Log::debug($this->queue.' no reasoningContent');
  326. } else {
  327. Log::debug($this->queue.' reasoning='.$reasoningContent);
  328. $output['reasoningContent'] = $reasoningContent;
  329. }
  330. return $output;
  331. }
  332. /**
  333. * 写入句子库
  334. */
  335. private function saveSentence(array $sentence, ?string $token = null)
  336. {
  337. $url = config('app.url').'/api/v2/sentence';
  338. Log::info($this->queue." sentence update {$url}");
  339. $response = Http::timeout($this->apiTimeout)
  340. ->withToken($token ?? $this->modelToken)
  341. ->post($url, [
  342. 'sentences' => [$sentence],
  343. ]);
  344. if ($response->failed()) {
  345. Log::error($this->queue.' sentence update failed', [
  346. 'url' => $url,
  347. 'data' => $response->json(),
  348. ]);
  349. throw new DatabaseException('sentence 数据库写入错误');
  350. }
  351. $count = $response->json()['data']['count'];
  352. Log::info("{$this->queue} sentence update {$count} successful");
  353. }
  354. private function savePr($sentence, $content)
  355. {
  356. $url = config('app.url').'/api/v2/sentpr';
  357. Log::info($this->queue." sentence update {$url}");
  358. $response = Http::timeout($this->apiTimeout)->withToken($this->modelToken)->post($url, [
  359. 'book' => $sentence->book_id,
  360. 'para' => $sentence->paragraph,
  361. 'begin' => $sentence->word_start,
  362. 'end' => $sentence->word_end,
  363. 'channel' => $sentence->channel_uid,
  364. 'text' => $content,
  365. 'notification' => false,
  366. 'webhook' => false,
  367. ]);
  368. if ($response->failed()) {
  369. Log::error($this->queue.' sentence update failed', [
  370. 'url' => $url,
  371. 'data' => $response->json(),
  372. ]);
  373. throw new DatabaseException('pr 数据库写入错误');
  374. }
  375. if ($response->json()['ok']) {
  376. Log::info("{$this->queue} sentence suggest update successful");
  377. } else {
  378. Log::error("{$this->queue} sentence suggest update failed", [
  379. 'url' => $url,
  380. 'data' => $response->json(),
  381. ]);
  382. }
  383. }
  384. private function getSentenceId($sentence)
  385. {
  386. $url = config('app.url').'/api/v2/sentence-info/aa';
  387. Log::info('ai translate', ['url' => $url]);
  388. $response = Http::timeout($this->apiTimeout)->withToken($this->modelToken)->get($url, [
  389. 'book' => $sentence->book_id,
  390. 'par' => $sentence->paragraph,
  391. 'start' => $sentence->word_start,
  392. 'end' => $sentence->word_end,
  393. 'channel' => $sentence->channel_uid,
  394. ]);
  395. if (! $response->json()['ok']) {
  396. Log::error($this->queue.' sentence id error', ['data' => $response->json()]);
  397. return false;
  398. }
  399. $sUid = $response->json()['data']['id'];
  400. Log::debug("sentence id={$sUid}");
  401. return $sUid;
  402. }
  403. private function setTaskProgress($current)
  404. {
  405. $taskProgress = $current;
  406. if ($taskProgress->total > 0) {
  407. $progress = (int) ($taskProgress->current * 100 / $taskProgress->total);
  408. } else {
  409. $progress = 100;
  410. Log::error($this->queue.' progress total is zero', ['task_id' => $this->task->id]);
  411. }
  412. $url = config('app.url').'/api/v2/task/'.$this->task->id;
  413. $data = [
  414. 'progress' => $progress,
  415. ];
  416. Log::debug($this->queue.' task progress request', ['url' => $url, 'data' => $data]);
  417. $response = Http::timeout($this->apiTimeout)->withToken($this->modelToken)->patch($url, $data);
  418. if ($response->failed()) {
  419. Log::error($this->queue.' task progress error', ['data' => $response->json()]);
  420. } else {
  421. Log::info($this->queue.' task progress successful progress='.$response->json()['data']['progress']);
  422. }
  423. return $progress;
  424. }
  425. public function handleFailedTranslate(string $messageId, array $translateData, \Exception $exception): void
  426. {
  427. try {
  428. // 彻底失败时的业务逻辑
  429. // 设置task为失败状态
  430. $this->setTaskStatus($this->task->id, 'stop');
  431. // 将故障信息写入task discussion
  432. if ($this->taskTopicId) {
  433. $dId = $this->taskDiscussion(
  434. $this->task->id,
  435. 'task',
  436. $this->task->title,
  437. "**处理失败ai任务时出错** 请重启任务 message id={$messageId} 错误信息:".$exception->getMessage(),
  438. $this->taskTopicId
  439. );
  440. }
  441. } catch (\Exception $e) {
  442. Log::error('处理失败ai任务时出错', ['error' => $e->getMessage()]);
  443. }
  444. }
  445. /**
  446. * 读取task信息,将任务拆解为单句小任务
  447. *
  448. * @param string $taskId 任务uuid
  449. * @return array 拆解后的提示词数组
  450. */
  451. public static function makeByTask(string $taskId, $aiAssistantId)
  452. {
  453. $task = Task::findOrFail($taskId);
  454. $description = $task->description;
  455. $rows = explode("\n", $description);
  456. $params = [];
  457. foreach ($rows as $key => $row) {
  458. if (strpos($row, '=') !== false) {
  459. $param = explode('=', trim($row, '|'));
  460. $params[$param[0]] = $param[1];
  461. }
  462. }
  463. if (! isset($params['type'])) {
  464. Log::error('no $params.type');
  465. return false;
  466. }
  467. // get sentences in article
  468. $sentences = [];
  469. $totalLen = 0;
  470. switch ($params['type']) {
  471. case 'sentence':
  472. if (! isset($params['id'])) {
  473. Log::error('no $params.id');
  474. return false;
  475. }
  476. $sentences[] = explode('-', $params['id']);
  477. break;
  478. case 'para':
  479. if (! isset($params['book']) || ! isset($params['paragraphs'])) {
  480. Log::error('no $params.book or paragraphs');
  481. return false;
  482. }
  483. $sent = PaliSentence::where('book', $params['book'])
  484. ->where('paragraph', $params['paragraphs'])->orderBy('word_begin')->get();
  485. foreach ($sent as $key => $value) {
  486. $sentences[] = [
  487. 'id' => [
  488. $value->book,
  489. $value->paragraph,
  490. $value->word_begin,
  491. $value->word_end,
  492. ],
  493. 'strlen' => $value->length,
  494. ];
  495. $totalLen += $value->length;
  496. }
  497. break;
  498. case 'chapter':
  499. if (! isset($params['book']) || ! isset($params['paragraphs'])) {
  500. Log::error('no $params.book or paragraphs');
  501. return false;
  502. }
  503. $chapterLen = PaliText::where('book', $params['book'])
  504. ->where('paragraph', $params['paragraphs'])->value('chapter_len');
  505. $sent = PaliSentence::where('book', $params['book'])
  506. ->whereBetween('paragraph', [$params['paragraphs'], $params['paragraphs'] + $chapterLen - 1])
  507. ->orderBy('paragraph')
  508. ->orderBy('word_begin')->get();
  509. foreach ($sent as $key => $value) {
  510. $sentences[] = [
  511. 'id' => [
  512. $value->book,
  513. $value->paragraph,
  514. $value->word_begin,
  515. $value->word_end,
  516. ],
  517. 'strlen' => $value->length,
  518. ];
  519. $totalLen += $value->length;
  520. }
  521. break;
  522. default:
  523. return false;
  524. break;
  525. }
  526. // render prompt
  527. $mdRender = new MdRender([
  528. 'format' => 'prompt',
  529. 'footnote' => false,
  530. 'paragraph' => false,
  531. ]);
  532. $m = new \Mustache_Engine([
  533. 'entity_flags' => ENT_QUOTES,
  534. 'escape' => function ($value) {
  535. return $value;
  536. },
  537. ]);
  538. // ai model
  539. $aiModel = AiModel::findOrFail($aiAssistantId);
  540. $modelToken = AuthService::getUserToken($aiModel->uid);
  541. $aiModel['token'] = $modelToken;
  542. /**
  543. * 返回值会被 ProcessAITranslateJob::publish() 写入缓存。Laravel 13 默认
  544. * 拒绝从缓存反序列化对象(cache.serializable_classes = false),所以这里
  545. * 先把模型摊平成数组。JSON 输出与直接编码模型完全一致。
  546. */
  547. $aiModelData = $aiModel->toArray();
  548. $taskData = $task->toArray();
  549. $sumLen = 0;
  550. $mqData = [];
  551. foreach ($sentences as $key => $sentence) {
  552. $sumLen += $sentence['strlen'];
  553. $sid = implode('-', $sentence['id']);
  554. Log::debug($sid);
  555. $sentChannelInfo = explode('@', $params['channel']);
  556. $channelId = $sentChannelInfo[0];
  557. $data = [];
  558. $data['origin'] = '{{'.$sid.'}}';
  559. $data['translation'] = '{{sent|id='.$sid;
  560. $data['translation'] .= '|channel='.$channelId;
  561. $data['translation'] .= '|text=translation}}';
  562. if (isset($params['nissaya']) && ! empty($params['nissaya'])) {
  563. $nissayaChannel = explode('@', $params['nissaya']);
  564. $channelInfo = ChannelApi::getById($nissayaChannel[0]);
  565. if ($channelInfo) {
  566. // 查看句子是否存在
  567. $nissayaSent = Sentence::where('book_id', $sentence['id'][0])
  568. ->where('paragraph', $sentence['id'][1])
  569. ->where('word_start', $sentence['id'][2])
  570. ->where('word_end', $sentence['id'][3])
  571. ->where('channel_uid', $nissayaChannel[0])->first();
  572. if ($nissayaSent && ! empty($nissayaSent->content)) {
  573. $nissayaData = [];
  574. $nissayaData['channel'] = $channelInfo;
  575. $nissayaData['data'] = '{{sent|id='.$sid;
  576. $nissayaData['data'] .= '|channel='.$nissayaChannel[0];
  577. $nissayaData['data'] .= '|text=translation}}';
  578. $data['nissaya'] = $nissayaData;
  579. }
  580. }
  581. }
  582. $content = $m->render($description, $data);
  583. $prompt = $mdRender->convert($content, []);
  584. // gen mq
  585. $aiMqData = [
  586. 'model' => $aiModelData,
  587. 'task' => [
  588. 'info' => $taskData,
  589. 'progress' => [
  590. 'current' => $sumLen,
  591. 'total' => $totalLen,
  592. ],
  593. ],
  594. 'prompt' => $prompt,
  595. 'sentence' => [
  596. 'book_id' => $sentence['id'][0],
  597. 'paragraph' => $sentence['id'][1],
  598. 'word_start' => $sentence['id'][2],
  599. 'word_end' => $sentence['id'][3],
  600. 'channel_uid' => $channelId,
  601. 'content' => $prompt,
  602. 'content_type' => 'markdown',
  603. 'access_token' => $sentChannelInfo[1] ?? $params['token'],
  604. ],
  605. ];
  606. array_push($mqData, $aiMqData);
  607. }
  608. $output = [
  609. 'model' => $aiModelData,
  610. 'task' => $taskData,
  611. ];
  612. $us = ['openai.com', 'googleapis.com', 'x.ai', 'anthropic.com'];
  613. $found = array_filter($us, function ($value) use ($output) {
  614. return str_contains($output['model']['url'], $value);
  615. });
  616. if ($found) {
  617. $output['area'] = 'us';
  618. } else {
  619. $output['area'] = 'cn';
  620. }
  621. $output['payload'] = $mqData;
  622. return $output;
  623. }
  624. public function stop()
  625. {
  626. $this->stop = true;
  627. }
  628. }