返回 AiToEarn
action.handlers.ts
根目录 / project / aitoearn-web / src / store / agent / handlers / action.handlers.ts
1 /**
2 * Agent Store - Action 处理器模块
3 * 使用策略模式处理不同的任务结果操作
4 *
5 * 从 public/AgentGenerator/actionHandlers.ts 移植并增强
6 */
7
8 import type { ActionType, IActionContext, IMediaItem, ITaskData } from '../agent.types'
9 import type { PluginPublishItem } from '@/store/plugin/store'
10 import { driver } from 'driver.js'
11 import { MediaType } from '@/api/ai/ai.constants'
12 import { apiCreateMaterial, apiGetMaterialGroupList } from '@/api/materials/material.api'
13 import { PubType } from '@/app/config/publishConfig'
14 import { useAccountStore } from '@/store/account'
15 import { usePluginStore } from '@/store/plugin'
16 import { PluginStatus } from '@/store/plugin/types/baseTypes'
17 import { confirm } from '@/utils/ui/confirm'
18 import { toast } from '@/utils/ui/toast'
19
20 // ============ Action Handler 接口 ============
21
22 /** Action Handler 接口 */
23 export interface IActionHandler {
24 /** Action 类型 */
25 type: ActionType
26 /** 判断是否能处理该任务 */
27 canHandle: (taskData: ITaskData) => boolean
28 /** 执行 Action */
29 execute: (taskData: ITaskData, context: IActionContext) => Promise<void>
30 }
31
32 // ============ 工具函数 ============
33
34 /**
35 * 构建发布 URL 参数
36 */
37 function buildPublishQueryParams(taskData: ITaskData): URLSearchParams {
38 const params = new URLSearchParams()
39
40 params.set('action', 'publish')
41 params.set('aiGenerated', 'true')
42
43 // 只添加非空值
44 if (taskData.platform)
45 params.set('platform', taskData.platform)
46 if (taskData.accountId)
47 params.set('accountId', taskData.accountId)
48 if (taskData.taskId)
49 params.set('taskId', taskData.taskId)
50 if (taskData.title)
51 params.set('title', taskData.title)
52 if (taskData.description)
53 params.set('description', taskData.description)
54 if (taskData.tags && taskData.tags.length > 0) {
55 params.set('tags', JSON.stringify(taskData.tags))
56 }
57 if (taskData.medias && taskData.medias.length > 0) {
58 params.set('medias', JSON.stringify(taskData.medias))
59 }
60
61 return params
62 }
63
64 /**
65 * 构建插件发布项
66 */
67 export function buildPluginPublishItem(taskData: ITaskData, account: any): PluginPublishItem {
68 const medias = taskData.medias || []
69 const hasVideo = medias.some((m: IMediaItem) => m.type === 'VIDEO')
70 const video = hasVideo ? medias.find((m: IMediaItem) => m.type === 'VIDEO') : null
71
72 const images = medias
73 .filter((m: IMediaItem) => m.type === 'IMAGE')
74 .map((m: IMediaItem) => ({
75 id: '',
76 imgPath: m.url,
77 ossUrl: m.url,
78 size: 0,
79 imgUrl: m.url,
80 filename: '',
81 width: 0,
82 height: 0,
83 })) as any[]
84
85 return {
86 account,
87 params: {
88 title: taskData.title || '',
89 des: taskData.description || '',
90 topics: taskData.tags || [],
91 video: video
92 ? ({
93 size: 0,
94 videoUrl: video.url,
95 ossUrl: video.url,
96 filename: '',
97 width: 0,
98 height: 0,
99 duration: 0,
100 cover: {
101 id: '',
102 imgPath: video.coverUrl || video.thumbUrl || '',
103 ossUrl: video.coverUrl || video.thumbUrl,
104 size: 0,
105 imgUrl: video.coverUrl || video.thumbUrl || '',
106 filename: '',
107 width: 0,
108 height: 0,
109 },
110 } as any)
111 : undefined,
112 images: images.length > 0 ? images : undefined,
113 option: {},
114 },
115 }
116 }
117
118 /**
119 * 显示插件引导
120 */
121 function showPluginGuide(t: (key: string) => string) {
122 setTimeout(() => {
123 const pluginButton = document.querySelector(
124 '[data-driver-target="plugin-button"]',
125 ) as HTMLElement
126 if (!pluginButton) {
127 console.warn('[ActionHandler] Plugin button not found')
128 return
129 }
130
131 const driverObj = driver({
132 showProgress: false,
133 showButtons: ['next'],
134 nextBtnText: t('aiGeneration.gotIt' as any),
135 doneBtnText: t('aiGeneration.gotIt' as any),
136 popoverOffset: 10,
137 stagePadding: 4,
138 stageRadius: 12,
139 allowClose: true,
140 smoothScroll: true,
141 steps: [
142 {
143 element: '[data-driver-target="plugin-button"]',
144 popover: {
145 title: t('plugin.authorizePluginTitle' as any),
146 description: t('plugin.authorizePluginDescription' as any),
147 side: 'bottom',
148 align: 'start',
149 onPopoverRender: () => {
150 setTimeout(() => {
151 const nextBtn = document.querySelector(
152 '.driver-popover-next-btn',
153 ) as HTMLButtonElement
154 const doneBtn = document.querySelector(
155 '.driver-popover-done-btn',
156 ) as HTMLButtonElement
157 const btn = nextBtn || doneBtn
158 if (btn) {
159 btn.textContent = t('aiGeneration.gotIt' as any)
160 const handleClick = (e: MouseEvent) => {
161 e.preventDefault()
162 e.stopPropagation()
163 driverObj.destroy()
164 btn.removeEventListener('click', handleClick)
165 }
166 btn.addEventListener('click', handleClick)
167 }
168 }, 50)
169 },
170 },
171 },
172 ],
173 onNextClick: () => {
174 driverObj.destroy()
175 return false
176 },
177 })
178
179 driverObj.drive()
180 }, 1500)
181 }
182
183 // ============ Action Handlers 实现 ============
184
185 /**
186 * 导航到发布页面 - 处理插件平台(xhs, douyin)
187 */
188 const navigateToPublishPluginHandler: IActionHandler = {
189 type: 'navigateToPublish',
190
191 canHandle: (taskData) => {
192 return (
193 taskData.type === 'fullContent'
194 && taskData.action === 'navigateToPublish'
195 && (taskData.platform === 'xhs' || taskData.platform === 'douyin')
196 )
197 },
198
199 async execute(taskData, context) {
200 const { t } = context
201 const pluginStatus = usePluginStore.getState().status
202 const isPluginReady = pluginStatus === PluginStatus.READY
203
204 if (!isPluginReady) {
205 toast.warning(t('plugin.platformNeedsPlugin' as any))
206 showPluginGuide(t as (key: string) => string)
207 return
208 }
209
210 // 插件已就绪,执行发布
211 try {
212 const accountGroupList = useAccountStore.getState().accountGroupList
213 const allAccounts = accountGroupList.reduce<any[]>((acc, group) => {
214 return [...acc, ...group.children]
215 }, [])
216
217 // 根据 accountId 或 platform 查找目标账号
218 let targetAccounts: any[] = []
219 if (taskData.accountId) {
220 const targetAccount = allAccounts.find(account => account.id === taskData.accountId)
221 if (targetAccount) {
222 targetAccounts = [targetAccount]
223 }
224 else {
225 console.warn(`[ActionHandler] Account not found: ${taskData.accountId}`)
226 }
227 }
228 else {
229 targetAccounts = allAccounts.filter(account => account.type === taskData.platform)
230 }
231
232 if (targetAccounts.length === 0) {
233 console.warn(`[ActionHandler] No accounts found for platform: ${taskData.platform}`)
234 toast.warning(t('aiGeneration.noAccountFound' as any) || '未找到可发布的账号')
235 return
236 }
237
238 // 构建发布项
239 const allPluginPublishItems: PluginPublishItem[] = []
240 const platformTaskIdMap = new Map<string, string>()
241
242 targetAccounts.forEach((account) => {
243 const publishItem = buildPluginPublishItem(taskData, account)
244 // @ts-ignore
245 allPluginPublishItems.push(publishItem)
246
247 const requestId = `req-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
248 platformTaskIdMap.set(account.id, requestId)
249 })
250
251 if (allPluginPublishItems.length > 0) {
252 usePluginStore.getState().executePluginPublish({
253 items: allPluginPublishItems,
254 platformTaskIdMap,
255 onProgress: (event) => {
256 const { stage, progress, message: progressMessage, accountId, platform } = event
257 if (stage === 'error') {
258 toast.error(progressMessage)
259 }
260 },
261 onComplete: () => {
262 toast.info(t('plugin.publishTaskSubmitted' as any))
263 },
264 })
265 }
266 }
267 catch (error: any) {
268 console.error('[ActionHandler] Plugin publish error:', error)
269 toast.error(
270 `${t('plugin.publishFailed' as any)}: ${error.message || t('aiGeneration.unknownError' as any)}`,
271 )
272 }
273 },
274 }
275
276 /**
277 * 导航到发布页面 - 处理其他平台(快手等)
278 */
279 const navigateToPublishOtherHandler: IActionHandler = {
280 type: 'navigateToPublish',
281
282 canHandle: (taskData) => {
283 return (
284 taskData.type === 'fullContent'
285 && taskData.action === 'navigateToPublish'
286 && taskData.platform !== 'xhs'
287 && taskData.platform !== 'douyin'
288 )
289 },
290
291 async execute(taskData, context) {
292 const { router, lng } = context
293 const queryParams = buildPublishQueryParams(taskData)
294
295 setTimeout(() => {
296 router.push(`/accounts?${queryParams.toString()}`)
297 }, 1500)
298 },
299 }
300
301 /**
302 * 导航到草稿箱
303 */
304 const navigateToDraftHandler: IActionHandler = {
305 type: 'navigateToDraft',
306
307 canHandle: (taskData) => {
308 return taskData.type === 'fullContent' && taskData.action === 'navigateToDraft'
309 },
310
311 async execute(_taskData, context) {
312 const { router, lng } = context
313
314 setTimeout(() => {
315 router.push(`/cgmaterial`)
316 }, 1500)
317 },
318 }
319
320 /**
321 * 保存草稿
322 */
323 const saveDraftHandler: IActionHandler = {
324 type: 'saveDraft',
325
326 canHandle: (taskData) => {
327 return taskData.type === 'fullContent' && taskData.action === 'saveDraft'
328 },
329
330 async execute(taskData, context) {
331 const { router, lng, t } = context
332
333 try {
334 // 转换 medias 格式
335 const medias = taskData.medias || []
336 const materialMediaList = medias.map((media: IMediaItem) => {
337 const pubType = media.type === MediaType.Video ? PubType.VIDEO : PubType.ImageText
338 return {
339 url: media.url,
340 type: pubType === PubType.VIDEO ? 'video' as const : pubType === PubType.ImageText ? 'img' as const : 'video' as const,
341 content: media.coverUrl || undefined,
342 }
343 })
344
345 // 确定封面URL
346 const coverUrl
347 = medias.find((m: IMediaItem) => m.coverUrl)?.coverUrl
348 || medias.find((m: IMediaItem) => m.type === 'IMAGE')?.url
349 || undefined
350
351 // 获取分组列表
352 const groupListRes = await apiGetMaterialGroupList(1, 100)
353 const groups = groupListRes?.data?.list || []
354
355 if (groups.length === 0) {
356 toast.warning(t('aiGeneration.noDraftGroupFound' as any))
357 return
358 }
359
360 // 根据 medias 类型选择默认分组
361 const hasVideo = medias.some((m: IMediaItem) => m.type === 'VIDEO')
362 const targetGroupType = hasVideo ? PubType.VIDEO : PubType.ImageText
363 const defaultGroup = groups.find((g: any) => g.type === targetGroupType) || groups[0]
364 const finalGroupId = defaultGroup.id
365
366 if (!finalGroupId) {
367 toast.warning(t('aiGeneration.noDraftGroup' as any))
368 return
369 }
370
371 // 创建草稿
372 const createResult = await apiCreateMaterial({
373 groupId: finalGroupId,
374 coverUrl,
375 mediaList: materialMediaList,
376 title: taskData.title || '',
377 desc: taskData.description || '',
378 type: targetGroupType,
379 })
380
381 if (createResult) {
382 toast.success(t('aiGeneration.saveDraftSuccess' as any))
383 setTimeout(() => {
384 router.push(`/cgmaterial`)
385 }, 1500)
386 }
387 else {
388 toast.error(t('aiGeneration.saveDraftFailed' as any))
389 }
390 }
391 catch (error: any) {
392 console.error('[ActionHandler] Save draft error:', error)
393 toast.error(
394 `${t('aiGeneration.saveDraftFailed' as any)}: ${error.message || t('aiGeneration.unknownError' as any)}`,
395 )
396 }
397 },
398 }
399
400 /**
401 * 更新频道授权
402 */
403 const updateChannelHandler: IActionHandler = {
404 type: 'updateChannel',
405
406 canHandle: (taskData) => {
407 return taskData.type === 'fullContent' && taskData.action === 'updateChannel'
408 },
409
410 async execute(taskData, context) {
411 const { router, lng, t } = context
412 const platform = taskData.platform
413
414 toast.warning(t('aiGeneration.channelAuthExpired' as any))
415
416 confirm({
417 title: t('aiGeneration.channelAuthExpiredTitle' as any),
418 content: t('aiGeneration.channelAuthExpiredContent' as any),
419 okText: t('aiGeneration.reauthorize' as any),
420 cancelText: t('aiGeneration.cancel' as any),
421 onOk: () => {
422 router.push(`/accounts?updateChannel=${platform}`)
423 },
424 })
425 },
426 }
427
428 /**
429 * 登录频道
430 */
431 const loginChannelHandler: IActionHandler = {
432 type: 'loginChannel',
433
434 canHandle: (taskData) => {
435 return taskData.type === 'fullContent' && taskData.action === 'loginChannel'
436 },
437
438 async execute(taskData, context) {
439 const { router, lng, t } = context
440 const platform = taskData.platform
441
442 toast.info(t('aiGeneration.needLoginChannel' as any))
443
444 confirm({
445 title: t('aiGeneration.needLogin' as any),
446 content: t('aiGeneration.pleaseLoginChannel' as any),
447 okText: t('aiGeneration.goLogin' as any),
448 cancelText: t('aiGeneration.cancel' as any),
449 onOk: () => {
450 router.push(`/accounts?loginChannel=${platform}`)
451 },
452 })
453 },
454 }
455
456 /**
457 * 创建频道(需要先绑定账号)
458 */
459 const createChannelHandler: IActionHandler = {
460 type: 'createChannel',
461
462 canHandle: (taskData) => {
463 return !!(taskData.type === 'fullContent' && taskData.action === 'createChannel')
464 },
465
466 async execute(taskData, context) {
467 const { router, lng, t } = context
468 const platform = taskData.platform
469 const platformName = platform
470 ? platform.charAt(0).toUpperCase() + platform.slice(1)
471 : 'Platform'
472
473 toast.warning(t('aiGeneration.needBindChannel' as any) as string)
474
475 confirm({
476 title: t('aiGeneration.needBindChannelTitle' as any) as string,
477 content: t('aiGeneration.needBindChannelContent' as any, {
478 platform: platformName,
479 }) as string,
480 okText: t('aiGeneration.goBind' as any) as string,
481 cancelText: undefined, // 不显示取消按钮
482 onOk: () => {
483 router.push(`/accounts?addChannel=${platform}`)
484 },
485 })
486 },
487 }
488
489 /**
490 * 默认发布处理(无 action 时)
491 */
492 const defaultPublishHandler: IActionHandler = {
493 type: 'navigateToPublish',
494
495 canHandle: (taskData) => {
496 return taskData.type === 'fullContent' && !taskData.action
497 },
498
499 async execute(taskData, context) {
500 const { router, lng } = context
501 const queryParams = buildPublishQueryParams(taskData)
502
503 setTimeout(() => {
504 router.push(`/accounts?${queryParams.toString()}`)
505 }, 1500)
506 },
507 }
508
509 // ============ Action Registry ============
510
511 /** 所有注册的 Action Handlers */
512 const actionHandlers: IActionHandler[] = [
513 navigateToPublishPluginHandler,
514 navigateToPublishOtherHandler,
515 navigateToDraftHandler,
516 saveDraftHandler,
517 updateChannelHandler,
518 loginChannelHandler,
519 createChannelHandler,
520 defaultPublishHandler,
521 ]
522
523 /**
524 * Action 注册表
525 * 使用策略模式管理和执行不同的 Action
526 */
527 export const ActionRegistry = {
528 /**
529 * 注册新的 Action Handler
530 * @param handler Action Handler
531 */
532 register(handler: IActionHandler): void {
533 // 添加到列表开头,确保新注册的优先匹配
534 actionHandlers.unshift(handler)
535 },
536
537 /**
538 * 执行 Action
539 * @param taskData 任务数据
540 * @param context Action 上下文
541 * @returns 是否成功执行
542 */
543 async execute(taskData: ITaskData, context: IActionContext): Promise<boolean> {
544 // 跳过纯媒体类型
545 if (
546 taskData.type === 'imageOnly'
547 || taskData.type === 'videoOnly'
548 || taskData.type === 'mediaOnly'
549 ) {
550 return false
551 }
552
553 // 查找匹配的 Handler
554 const handler = actionHandlers.find(h => h.canHandle(taskData))
555
556 if (handler) {
557 await handler.execute(taskData, context)
558 return true
559 }
560
561 console.warn('[ActionRegistry] No handler found for task:', taskData)
562 return false
563 },
564
565 /**
566 * 批量执行 Actions(处理多个任务结果)
567 * @param taskDataList 任务数据列表
568 * @param context Action 上下文
569 */
570 async executeBatch(taskDataList: ITaskData[], context: IActionContext): Promise<void> {
571 // 分离插件平台任务和其他任务
572 const pluginTasks: ITaskData[] = []
573 const otherTasks: ITaskData[] = []
574
575 taskDataList.forEach((taskData) => {
576 // 跳过纯媒体类型
577 if (
578 taskData.type === 'imageOnly'
579 || taskData.type === 'videoOnly'
580 || taskData.type === 'mediaOnly'
581 ) {
582 return
583 }
584
585 if (taskData.type === 'fullContent' && taskData.action === 'navigateToPublish') {
586 if (taskData.platform === 'xhs' || taskData.platform === 'douyin') {
587 pluginTasks.push(taskData)
588 }
589 else {
590 otherTasks.push(taskData)
591 }
592 }
593 else {
594 otherTasks.push(taskData)
595 }
596 })
597
598 // 批量处理插件平台任务
599 if (pluginTasks.length > 0) {
600 await ActionRegistry.executePluginBatch(pluginTasks, context)
601 }
602
603 // 逐个处理其他任务
604 for (const taskData of otherTasks) {
605 await ActionRegistry.execute(taskData, context)
606 }
607 },
608
609 /**
610 * 批量执行插件平台发布
611 * @param pluginTasks 插件平台任务列表
612 * @param context Action 上下文
613 */
614 async executePluginBatch(pluginTasks: ITaskData[], context: IActionContext): Promise<void> {
615 const pluginStatus = usePluginStore.getState().status
616 const isPluginReady = pluginStatus === PluginStatus.READY
617 const { t } = context
618
619 if (!isPluginReady) {
620 toast.warning(t('plugin.platformNeedsPlugin' as any))
621 showPluginGuide(t as (key: string) => string)
622 return
623 }
624
625 try {
626 const accountGroupList = useAccountStore.getState().accountGroupList
627 const allAccounts = accountGroupList.reduce<any[]>((acc, group) => {
628 return [...acc, ...group.children]
629 }, [])
630
631 const allPluginPublishItems: PluginPublishItem[] = []
632 const platformTaskIdMap = new Map<string, string>()
633
634 pluginTasks.forEach((taskData) => {
635 let targetAccounts: any[] = []
636 if (taskData.accountId) {
637 const targetAccount = allAccounts.find(account => account.id === taskData.accountId)
638 if (targetAccount) {
639 targetAccounts = [targetAccount]
640 }
641 }
642 else {
643 targetAccounts = allAccounts.filter(account => account.type === taskData.platform)
644 }
645
646 if (targetAccounts.length === 0) {
647 console.warn(`[ActionRegistry] No accounts found for platform: ${taskData.platform}`)
648 return
649 }
650
651 targetAccounts.forEach((account) => {
652 const publishItem = buildPluginPublishItem(taskData, account)
653 // @ts-ignore
654 allPluginPublishItems.push(publishItem)
655
656 const requestId = `req-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
657 platformTaskIdMap.set(account.id, requestId)
658 })
659 })
660
661 if (allPluginPublishItems.length > 0) {
662 usePluginStore.getState().executePluginPublish({
663 items: allPluginPublishItems,
664 platformTaskIdMap,
665 onProgress: (event) => {
666 const { stage, progress, message: progressMessage, accountId, platform } = event
667
668 if (stage === 'error') {
669 toast.error(progressMessage)
670 }
671 },
672 onComplete: () => {
673 toast.info(t('plugin.publishTaskSubmitted' as any))
674 },
675 })
676 }
677 else {
678 toast.warning(t('aiGeneration.noAccountFound' as any) || '未找到可发布的账号')
679 }
680 }
681 catch (error: any) {
682 console.error('[ActionRegistry] Plugin batch publish error:', error)
683 toast.error(
684 `${t('plugin.publishFailed' as any)}: ${error.message || t('aiGeneration.unknownError' as any)}`,
685 )
686 }
687 },
688
689 /**
690 * 获取所有已注册的 Action 类型
691 */
692 getRegisteredTypes(): ActionType[] {
693 return [...new Set(actionHandlers.map(h => h.type))]
694 },
695 }
696
697 export default ActionRegistry
698
698 lines TYPESCRIPT