TaskCenterService.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  1. <?php
  2. namespace App\Services;
  3. use App\Facade\Site;
  4. use App\Models\MpTaskCenter;
  5. use App\Services\Anime\AnimeService;
  6. use Illuminate\Support\Facades\DB;
  7. class TaskCenterService
  8. {
  9. /**
  10. * 创建任务中心记录
  11. *
  12. * @param string $type 任务类型:text/image/video
  13. * @param array $data 任务数据(title/ref_task_id/prompt/params等)
  14. * @return MpTaskCenter
  15. */
  16. public function createTask(string $type, array $data = [])
  17. {
  18. $uid = 0;
  19. $cpid = 0;
  20. try {
  21. $uid = (int)Site::getUid();
  22. $cpid = (int)Site::getCpid();
  23. } catch (\Throwable $e) {
  24. // 非请求上下文(如命令行)下可能无法获取用户ID,置为0
  25. }
  26. return MpTaskCenter::create([
  27. 'uid' => $data['uid'] ?? $uid,
  28. 'cpid' => $data['cpid'] ?? $cpid,
  29. 'task_type' => $type,
  30. 'title' => $data['title'] ?? '',
  31. 'ref_task_id' => $data['ref_task_id'] ?? 0,
  32. 'status' => $data['status'] ?? MpTaskCenter::STATUS_PROCESSING,
  33. 'result' => $data['result'] ?? null,
  34. 'error_message'=> $data['error_message'] ?? null,
  35. 'prompt' => $data['prompt'] ?? null,
  36. 'params' => $data['params'] ?? null,
  37. 'params_md5' => $data['params_md5'] ?? null,
  38. ]);
  39. }
  40. /**
  41. * 更新任务中心记录
  42. *
  43. * @param int $taskId
  44. * @param array $data
  45. * @return bool
  46. */
  47. public function updateTask(int $taskId, array $data = []): bool
  48. {
  49. return (bool)MpTaskCenter::where('id', $taskId)->update($data);
  50. }
  51. /**
  52. * 查询任务详情
  53. *
  54. * @param int $taskId
  55. * @return MpTaskCenter|null
  56. */
  57. public function getTaskDetail(int $taskId)
  58. {
  59. $query = MpTaskCenter::where('id', $taskId);
  60. // 请求上下文下按当前用户过滤
  61. $uid = 0;
  62. try {
  63. $uid = (int)Site::getUid();
  64. } catch (\Throwable $e) {
  65. }
  66. if ($uid > 0) {
  67. $query->where('uid', $uid);
  68. }
  69. return $query->first();
  70. }
  71. /**
  72. * 分页查询任务列表
  73. *
  74. * @param array $params
  75. * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
  76. */
  77. public function getTaskList(array $params = [])
  78. {
  79. $query = MpTaskCenter::query();
  80. // 请求上下文下按当前用户过滤
  81. $uid = 0;
  82. try {
  83. $uid = (int)Site::getUid();
  84. } catch (\Throwable $e) {
  85. }
  86. if ($uid > 0) {
  87. $query->where('uid', $uid);
  88. }
  89. // 按任务ID筛选
  90. if (!empty($params['task_id'])) {
  91. $query->where('id', (int)$params['task_id']);
  92. }
  93. // 按任务状态筛选(支持逗号分隔多状态)
  94. if (!empty($params['status'])) {
  95. $statuses = is_array($params['status'])
  96. ? $params['status']
  97. : array_filter(array_map('trim', explode(',', (string)$params['status'])));
  98. if (!empty($statuses)) {
  99. $query->whereIn('status', $statuses);
  100. }
  101. }
  102. // 按任务类型筛选
  103. if (!empty($params['task_type'])) {
  104. $query->where('task_type', $params['task_type']);
  105. }
  106. $pageSize = (int)($params['page_size'] ?? 20);
  107. if ($pageSize <= 0 || $pageSize > 100) {
  108. $pageSize = 20;
  109. }
  110. return $query->orderBy('created_at', 'desc')->orderBy('id', 'desc')->paginate($pageSize);
  111. }
  112. /**
  113. * 定时同步任务中心状态和结果
  114. *
  115. * 图片任务关联 mp_generate_pic_tasks,视频任务关联 mp_generate_video_tasks,
  116. * 将底层任务的最新状态、结果和错误信息同步到任务中心。
  117. *
  118. * @return int 同步更新的记录数
  119. */
  120. public function syncTaskStatus(): int
  121. {
  122. $updated = 0;
  123. // 只同步尚未结束的任务(避免重复扫描已完成记录)
  124. $tasks = MpTaskCenter::whereIn('status', [
  125. MpTaskCenter::STATUS_PENDING,
  126. MpTaskCenter::STATUS_PROCESSING,
  127. ])
  128. ->where('ref_task_id', '>', 0)
  129. ->orderBy('id', 'desc')
  130. ->limit(500)
  131. ->get();
  132. foreach ($tasks as $task) {
  133. try {
  134. if ($task->task_type === MpTaskCenter::TYPE_IMAGE) {
  135. $updated += $this->syncImageTask($task);
  136. } elseif ($task->task_type === MpTaskCenter::TYPE_VIDEO
  137. || $task->task_type === MpTaskCenter::TYPE_ACT_VIDEO) {
  138. // 文生视频与片段转视频的单任务结果格式一致,共用同步逻辑
  139. $updated += $this->syncVideoTask($task);
  140. } elseif ($task->task_type === MpTaskCenter::TYPE_BATCH_ACT_VIDEOS) {
  141. $updated += $this->syncBatchActVideosTask($task);
  142. }
  143. } catch (\Exception $e) {
  144. dLog('command')->error('任务中心同步失败: ' . $e->getMessage(), [
  145. 'task_id' => $task->id,
  146. 'ref_task_id' => $task->ref_task_id,
  147. 'task_type' => $task->task_type,
  148. ]);
  149. }
  150. }
  151. return $updated;
  152. }
  153. /**
  154. * 同步图片任务状态到任务中心
  155. *
  156. * @param MpTaskCenter $task
  157. * @return int
  158. */
  159. private function syncImageTask(MpTaskCenter $task): int
  160. {
  161. $ref = DB::table('mp_generate_pic_tasks')->where('id', $task->ref_task_id)->first();
  162. if (!$ref) {
  163. return 0;
  164. }
  165. $result = null;
  166. if (!empty($ref->result_url)) {
  167. $urls = $this->normalizeResultUrls($ref->result_url);
  168. $urls = array_values(array_filter($urls));
  169. // 与文生图 completed 返回格式保持一致
  170. $resultData = [
  171. 'msg' => '',
  172. 'code' => 0,
  173. 'data' => $urls[0] ?? '',
  174. 'task_center_id' => $task->id,
  175. ];
  176. if (count($urls) > 1) {
  177. $resultData['image_urls'] = $urls;
  178. }
  179. $result = json_encode($resultData, JSON_UNESCAPED_UNICODE);
  180. }
  181. return $this->applyRefStatus($task, $ref->status, $result, $ref->error_message);
  182. }
  183. /**
  184. * 规范化图片结果URL(兼容字符串/数组/JSON字符串/双重编码)
  185. *
  186. * @param mixed $resultUrl
  187. * @return array
  188. */
  189. private function normalizeResultUrls($resultUrl): array
  190. {
  191. if (is_array($resultUrl)) {
  192. return array_values(array_filter($resultUrl));
  193. }
  194. $value = (string)$resultUrl;
  195. // 最多解析两层 JSON(防御双重编码)
  196. for ($i = 0; $i < 2; $i++) {
  197. if (!is_string($value) || !is_json($value)) {
  198. break;
  199. }
  200. $decoded = json_decode($value, true);
  201. if (!is_array($decoded)) {
  202. // 解码结果是字符串且仍是JSON,继续解析下一层
  203. if (is_string($decoded) && is_json($decoded)) {
  204. $value = $decoded;
  205. continue;
  206. }
  207. $value = $decoded;
  208. break;
  209. }
  210. $value = $decoded;
  211. }
  212. if (is_array($value)) {
  213. return array_values(array_filter($value));
  214. }
  215. if (is_string($value) && $value !== '') {
  216. return [$value];
  217. }
  218. return [];
  219. }
  220. /**
  221. * 同步视频任务状态到任务中心
  222. *
  223. * @param MpTaskCenter $task
  224. * @return int
  225. */
  226. private function syncVideoTask(MpTaskCenter $task): int
  227. {
  228. $ref = DB::table('mp_generate_video_tasks')->where('id', $task->ref_task_id)->first();
  229. if (!$ref) {
  230. return 0;
  231. }
  232. $result = null;
  233. if (!empty($ref->result_url) || !empty($ref->compressed_url) || !empty($ref->last_frame_url)) {
  234. // 与文生视频 completed 返回格式保持一致
  235. $result = json_encode([
  236. 'task_id' => $ref->id,
  237. 'status' => $ref->status,
  238. 'video_url' => $ref->compressed_url ?: $ref->result_url,
  239. 'origin_video_url' => $ref->result_url,
  240. 'last_frame_url' => $ref->last_frame_url,
  241. 'error_message' => $ref->error_message ? mapErrorMessage($ref->error_message) : '',
  242. ], JSON_UNESCAPED_UNICODE);
  243. }
  244. return $this->applyRefStatus($task, $ref->status, $result, $ref->error_message);
  245. }
  246. /**
  247. * 同步片段一键转视频(批量)任务状态到任务中心
  248. *
  249. * ref_task_id 关联首个视频任务,完整任务列表存放在 params.task_ids 中;
  250. * 结果字段与 batchSetActVideos 接口 completed 事件保持一致
  251. * (total_tasks_count / completed_tasks_count / success_tasks_count / episode)。
  252. *
  253. * @param MpTaskCenter $task
  254. * @return int
  255. */
  256. private function syncBatchActVideosTask(MpTaskCenter $task): int
  257. {
  258. $params = json_decode((string)$task->params, true) ?: [];
  259. $animeId = (int)getProp($params, 'anime_id', 0);
  260. $episodeId = (int)getProp($params, 'episode_id', 0);
  261. $taskIds = array_values(array_filter(array_map('intval', (array)getProp($params, 'task_ids', [])), function ($id) {
  262. return $id > 0;
  263. }));
  264. if (!$taskIds) {
  265. return 0;
  266. }
  267. $refs = DB::table('mp_generate_video_tasks')->whereIn('id', $taskIds)->get();
  268. if ($refs->isEmpty()) {
  269. return 0;
  270. }
  271. $totalCount = count($taskIds);
  272. $completedCount = 0;
  273. $successCount = 0;
  274. $failedCount = 0;
  275. $errorMessage = '';
  276. foreach ($refs as $ref) {
  277. if (!in_array($ref->status, ['success', 'failed'], true)) {
  278. continue;
  279. }
  280. $completedCount++;
  281. if ($ref->status === 'success') {
  282. $successCount++;
  283. continue;
  284. }
  285. $failedCount++;
  286. if ($errorMessage === '' && !empty($ref->error_message)) {
  287. $errorMessage = mapErrorMessage($ref->error_message);
  288. }
  289. }
  290. // 未全部结束时仅推进整体状态,等待所有视频任务落终态后再写结果
  291. if ($completedCount < $totalCount) {
  292. $newStatus = $completedCount > 0 ? MpTaskCenter::STATUS_PROCESSING : $task->status;
  293. return $this->updateTask((int)$task->id, ['status' => $newStatus]) ? 1 : 0;
  294. }
  295. $result = [
  296. 'total_tasks_count' => $totalCount,
  297. 'completed_tasks_count' => $completedCount,
  298. 'success_tasks_count' => $successCount,
  299. ];
  300. // 与接口 completed 事件一致,附带剧集完整信息
  301. if ($animeId > 0 && $episodeId > 0) {
  302. try {
  303. $result['episode'] = app(AnimeService::class)->episodeInfoForAce([
  304. 'anime_id' => $animeId,
  305. 'episode_id' => $episodeId,
  306. ]);
  307. } catch (\Throwable $e) {
  308. dLog('command')->error('任务中心同步片段批量视频结果失败: ' . $e->getMessage(), [
  309. 'task_id' => $task->id,
  310. 'anime_id' => $animeId,
  311. 'episode_id' => $episodeId,
  312. ]);
  313. }
  314. }
  315. $updateData = [
  316. 'status' => $failedCount > 0 ? MpTaskCenter::STATUS_FAILED : MpTaskCenter::STATUS_SUCCESS,
  317. 'result' => json_encode($result, JSON_UNESCAPED_UNICODE),
  318. ];
  319. if ($failedCount > 0) {
  320. $updateData['error_message'] = $errorMessage !== '' ? $errorMessage : ('有 ' . $failedCount . ' 个视频生成失败');
  321. }
  322. return $this->updateTask((int)$task->id, $updateData) ? 1 : 0;
  323. }
  324. /**
  325. * 将底层任务状态应用到任务中心记录
  326. *
  327. * @param MpTaskCenter $task
  328. * @param string $refStatus
  329. * @param string|null $result
  330. * @param string|null $errorMessage
  331. * @return int
  332. */
  333. private function applyRefStatus(MpTaskCenter $task, string $refStatus, $result, $errorMessage): int
  334. {
  335. $statusMap = [
  336. 'pending' => MpTaskCenter::STATUS_PENDING,
  337. 'processing' => MpTaskCenter::STATUS_PROCESSING,
  338. 'success' => MpTaskCenter::STATUS_SUCCESS,
  339. 'failed' => MpTaskCenter::STATUS_FAILED,
  340. ];
  341. $newStatus = $statusMap[$refStatus] ?? $task->status;
  342. $updateData = ['status' => $newStatus];
  343. if ($result !== null) {
  344. $updateData['result'] = $result;
  345. }
  346. if ($errorMessage !== null) {
  347. $updateData['error_message'] = $errorMessage;
  348. }
  349. return $this->updateTask((int)$task->id, $updateData) ? 1 : 0;
  350. }
  351. }