返回 AiToEarn
usePublishActions.ts
根目录 / project / aitoearn-web / src / components / PublishDialog / hooks / usePublishActions.ts
1 /**
2 * 发布操作 Hook
3 * 处理发布内容的核心逻辑
4 */
5
6 import type { ChannelPublishUserActionVo } from '@/api/channels/channel.types'
7 import type { PubItem } from '@/components/PublishDialog/publishDialog.type'
8 import type {
9 PlatformPublishMode,
10 PlatformPublishTask,
11 PluginPlatformType,
12 PublishParams as PluginPublishParams,
13 UnifiedPublishParams,
14 } from '@/store/plugin'
15 import { useCallback } from 'react'
16 import { createChannelPublishFlowApi, getChannelPublishUserActionApi } from '@/api/channels/channel.api'
17 import { getPublishRecordDetailById } from '@/api/platforms/publish.api'
18 import { PublishStatus } from '@/api/platforms/publish.constants'
19 import {
20 getDays,
21 getUtcDays,
22 } from '@/app/[lng]/accounts/components/CalendarTiming/calendarTiming.utils'
23 import { useCalendarTiming } from '@/app/[lng]/accounts/components/CalendarTiming/useCalendarTiming'
24 import { AccountStatus } from '@/app/config/accountConfig'
25 import { PlatType } from '@/app/config/platConfig'
26 import {
27 buildChannelPublishFlowParams,
28 getPublishRecordIdFromFlow,
29 isPublishTitleSupported,
30 } from '@/components/PublishDialog/PublishDialog.util'
31 import { usePublishDialogStorageStore } from '@/components/PublishDialog/usePublishDialogStorageStore'
32 import { getPlatformInfoSync, isPlatformDisabledSync, isPlatformEnabledSync } from '@/store/platformMetadata'
33 import { PlatformTaskStatus, PLUGIN_SUPPORTED_PLATFORMS, usePluginStore } from '@/store/plugin'
34 import { sleep } from '@/utils/common'
35 import { toast } from '@/utils/ui/toast'
36
37 const DOUYIN_RECORD_POLL_INTERVAL_MS = 5000
38 const DOUYIN_RECORD_POLL_MAX_COUNT = 36
39 const douyinRecordPollingStatuses: readonly number[] = [
40 PublishStatus.UNPUBLISH,
41 PublishStatus.PUB_LOADING,
42 PublishStatus.QUEUED,
43 ]
44 const douyinRecordFailedStatuses: readonly number[] = [
45 PublishStatus.FAIL,
46 PublishStatus.UPDATED_FAILED,
47 PublishStatus.CANCELED,
48 ]
49
50 interface UsePublishActionsParams {
51 pubListChoosed: PubItem[]
52 pubTime?: string
53 suppressAutoPublish?: boolean
54 taskIdForPublish?: string
55 materialGroupIdForPublish?: string
56 materialIdForPublish?: string
57 onPublishConfirmed?: (taskId?: string, publishRecordId?: string) => void
58 onPublishStart?: () => void
59 onClose: () => void
60 onPubSuccess?: () => void
61 setCreateLoading: (loading: boolean) => void
62 setCurrentPublishTaskId: (taskId: string | undefined) => void
63 setPublishDetailVisible: (visible: boolean) => void
64 t: (key: string, params?: Record<string, string>) => string
65 }
66
67 /**
68 * 检查平台是否由插件支持
69 */
70 export function isPluginSupportedPlatform(platType: PlatType | string): boolean {
71 return PLUGIN_SUPPORTED_PLATFORMS.includes(platType as PluginPlatformType)
72 }
73
74 function getPlatformTaskId(item: PubItem, publishMode: PlatformPublishMode) {
75 return `${publishMode}-${item.account.type}-${item.account.id}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
76 }
77
78 function buildTaskPublishParams(item: PubItem): UnifiedPublishParams {
79 const params: UnifiedPublishParams = {
80 platform: item.account.type as PlatType,
81 accountId: item.account.id,
82 type: item.params.video ? 'video' : 'image',
83 desc: item.params.des || '',
84 topics: item.params.topics || [],
85 }
86
87 if (isPublishTitleSupported(item.account.type))
88 params.title = item.params.title || ''
89
90 return params
91 }
92
93 function buildUnifiedPlatformTask(item: PubItem, publishMode: PlatformPublishMode): PlatformPublishTask {
94 return {
95 id: getPlatformTaskId(item, publishMode),
96 platform: item.account.type as PlatType,
97 accountId: item.account.id,
98 publishMode,
99 params: buildTaskPublishParams(item),
100 status: PlatformTaskStatus.PENDING,
101 progress: null,
102 result: null,
103 startTime: null,
104 endTime: null,
105 error: null,
106 }
107 }
108
109 function hasDouyinUserAction(
110 data: ChannelPublishUserActionVo | null | undefined,
111 ): data is ChannelPublishUserActionVo & { schemeUrl: string, shortLink: string } {
112 return !!data?.schemeUrl && !!data.shortLink
113 }
114
115 function normalizePublishStatus(status: string | number | undefined) {
116 const normalized = Number(status)
117 return Number.isFinite(normalized) ? normalized : undefined
118 }
119
120 function isDouyinUserActionReadyStatus(status: string | number | undefined) {
121 return normalizePublishStatus(status) === PublishStatus.WAITING_FOR_USER_ACTION
122 }
123
124 function shouldPollDouyinRecord(status: string | number | undefined) {
125 const normalized = normalizePublishStatus(status)
126 return normalized !== undefined && douyinRecordPollingStatuses.includes(normalized)
127 }
128
129 function isDouyinRecordFailedStatus(status: string | number | undefined) {
130 const normalized = normalizePublishStatus(status)
131 return normalized !== undefined && douyinRecordFailedStatuses.includes(normalized)
132 }
133
134 async function pollDouyinRecordUntilUserActionReady(publishRecordId: string) {
135 let latestRes: Awaited<ReturnType<typeof getPublishRecordDetailById>> | null = null
136
137 for (let pollIndex = 0; pollIndex < DOUYIN_RECORD_POLL_MAX_COUNT; pollIndex++) {
138 if (pollIndex > 0)
139 await sleep(DOUYIN_RECORD_POLL_INTERVAL_MS)
140
141 latestRes = await getPublishRecordDetailById(publishRecordId)
142 const record = latestRes?.data
143 if (!record)
144 continue
145
146 if (isDouyinUserActionReadyStatus(record.status))
147 return latestRes
148
149 if (isDouyinRecordFailedStatus(record.status) || !shouldPollDouyinRecord(record.status))
150 return latestRes
151 }
152
153 return latestRes
154 }
155
156 /**
157 * 发布操作 Hook
158 */
159 export function usePublishActions({
160 pubListChoosed,
161 pubTime,
162 suppressAutoPublish,
163 taskIdForPublish,
164 materialGroupIdForPublish,
165 materialIdForPublish,
166 onPublishConfirmed,
167 onPublishStart,
168 onClose,
169 onPubSuccess,
170 setCreateLoading,
171 setCurrentPublishTaskId,
172 setPublishDetailVisible,
173 t,
174 }: UsePublishActionsParams) {
175 /**
176 * 执行发布
177 * 1. API 发布与插件发布共用一个详情任务
178 * 2. 插件发布立即启动,不等待 API 发布
179 * 3. API 发布后台执行,只更新自己的平台任务状态
180 */
181 const pubClick = useCallback(async () => {
182 const offlineItem = pubListChoosed.find(item => item.account.status === AccountStatus.DISABLE)
183 if (offlineItem) {
184 toast.error(t('tips.accountOffline'))
185 return
186 }
187
188 const disabledItem = pubListChoosed.find(item => isPlatformDisabledSync(item.account.type))
189 if (disabledItem) {
190 toast.error(
191 t('tips.platformComingSoon', {
192 platform: getPlatformInfoSync(disabledItem.account.type)?.name || disabledItem.account.type,
193 }),
194 )
195 return
196 }
197
198 const restrictedItem = pubListChoosed.find(item => !isPlatformEnabledSync(item.account.type))
199 if (restrictedItem) {
200 toast.error(
201 t('tips.regionRestricted', {
202 platform: getPlatformInfoSync(restrictedItem.account.type)?.name || restrictedItem.account.type,
203 }),
204 )
205 return
206 }
207
208 setCreateLoading(true)
209 onPublishStart?.()
210
211 const publishTime = getUtcDays(pubTime || getDays().add(5, 'second')).format()
212
213 // 分离发布任务和自动发布列表
214 const apiPublishItems = pubListChoosed.filter(
215 item => !isPluginSupportedPlatform(item.account.type),
216 )
217 const pluginPublishItems = pubListChoosed.filter(item =>
218 isPluginSupportedPlatform(item.account.type),
219 )
220
221 const pluginPlatformTasks: PlatformPublishTask[] = []
222 const apiPlatformTaskMap = new Map<string, PlatformPublishTask>()
223 const platformTaskIdMap = new Map<string, string>()
224
225 pluginPublishItems.forEach((item) => {
226 const requestId = `req-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
227 platformTaskIdMap.set(item.account.id, requestId)
228
229 const pluginPublishParams: PluginPublishParams = {
230 platform: item.account.type as PluginPlatformType,
231 type: item.params.video ? 'video' : 'image',
232 desc: item.params.des || '',
233 topics: item.params.topics || [],
234 }
235 if (isPublishTitleSupported(item.account.type))
236 pluginPublishParams.title = item.params.title || ''
237
238 pluginPlatformTasks.push({
239 ...buildUnifiedPlatformTask(item, 'auto'),
240 requestId,
241 params: pluginPublishParams,
242 })
243 })
244
245 apiPublishItems.forEach((item) => {
246 const publishMode: PlatformPublishMode = item.account.type === PlatType.Douyin ? 'user_action' : 'task'
247 apiPlatformTaskMap.set(item.account.id, {
248 ...buildUnifiedPlatformTask(item, publishMode),
249 status: PlatformTaskStatus.PUBLISHING,
250 startTime: Date.now(),
251 })
252 })
253
254 const { addPublishTask, updatePlatformTask } = usePluginStore.getState()
255 const taskTitle = pubListChoosed[0]?.params.title
256 || pubListChoosed[0]?.params.des?.slice(0, 20)
257 || t('title')
258 const taskDescription = pubListChoosed[0]?.params.des?.slice(0, 100)
259 const platformTasks = [
260 ...pluginPlatformTasks,
261 ...Array.from(apiPlatformTaskMap.values()),
262 ]
263
264 if (platformTasks.length === 0) {
265 setCreateLoading(false)
266 return
267 }
268
269 const taskId = addPublishTask({
270 title: taskTitle,
271 description: taskDescription,
272 platformTasks,
273 })
274
275 setCurrentPublishTaskId(taskId)
276 setPublishDetailVisible(true)
277 onClose()
278
279 const updateApiTask = (item: PubItem, updates: Partial<PlatformPublishTask>) => {
280 const platformTask = apiPlatformTaskMap.get(item.account.id)
281 if (!platformTask)
282 return
283
284 updatePlatformTask(taskId, platformTask.id, updates)
285 }
286
287 const updateDouyinUserActionError = (item: PubItem, publishRecordId: string, message?: string) => {
288 const errorMessage = message || t('messages.userActionFetchFailed')
289 updateApiTask(item, {
290 status: PlatformTaskStatus.ERROR,
291 publishRecordId,
292 error: errorMessage,
293 endTime: Date.now(),
294 result: {
295 success: false,
296 failReason: errorMessage,
297 },
298 })
299 }
300
301 const fetchAndUpdateDouyinUserAction = async (item: PubItem, publishRecordId: string) => {
302 try {
303 const recordRes = await pollDouyinRecordUntilUserActionReady(publishRecordId)
304 const record = recordRes?.data
305 if (!record || !isDouyinUserActionReadyStatus(record.status)) {
306 updateDouyinUserActionError(item, publishRecordId, record?.errorMsg || recordRes?.message)
307 return
308 }
309
310 await useCalendarTiming.getState().refreshPubRecordDetail(publishRecordId)
311
312 const userActionRes = await getChannelPublishUserActionApi(publishRecordId)
313 if (userActionRes?.code !== 0 || !hasDouyinUserAction(userActionRes.data)) {
314 updateDouyinUserActionError(item, publishRecordId, userActionRes?.message)
315 return
316 }
317
318 const userActionPublishRecordId = userActionRes.data.recordId || publishRecordId
319 updateApiTask(item, {
320 status: PlatformTaskStatus.PENDING,
321 publishRecordId: userActionPublishRecordId,
322 userAction: {
323 schemeUrl: userActionRes.data.schemeUrl,
324 shortLink: userActionRes.data.shortLink,
325 expiresAt: userActionRes.data.expiresAt,
326 },
327 progress: null,
328 result: null,
329 endTime: Date.now(),
330 })
331 }
332 catch {
333 updateDouyinUserActionError(item, publishRecordId)
334 }
335 }
336
337 let firstPublishRecordId: string | undefined
338 const hasPluginItems = pluginPublishItems.length > 0
339
340 // 插件发布与 API 发布分线执行:插件发布不等待 API 发布任务创建结果
341 if (hasPluginItems) {
342 void usePluginStore.getState().executePluginPublish({
343 items: pluginPublishItems,
344 platformTaskIdMap,
345 publishTime,
346 userTaskId: taskIdForPublish, // 传递任务ID用于关联发布记录
347 ...(materialGroupIdForPublish ? { materialGroupId: materialGroupIdForPublish } : {}),
348 ...(materialIdForPublish ? { materialId: materialIdForPublish } : {}),
349 skipAddTask: true,
350 onComplete: (pluginPublishRecordId) => {
351 useCalendarTiming.getState().getPubRecord()
352 if (suppressAutoPublish && onPublishConfirmed) {
353 try {
354 onPublishConfirmed(taskIdForPublish, pluginPublishRecordId || firstPublishRecordId)
355 }
356 catch (e) {
357 console.error('onPublishConfirmed callback failed', e)
358 }
359 }
360 },
361 })
362 }
363
364 const executeApiPublish = async () => {
365 if (apiPublishItems.length === 0)
366 return true
367
368 const flowParams = buildChannelPublishFlowParams(apiPublishItems, {
369 publishAt: publishTime,
370 userTaskId: taskIdForPublish,
371 materialGroupId: materialGroupIdForPublish,
372 materialId: materialIdForPublish,
373 source: 'web',
374 })
375
376 if (!flowParams) {
377 apiPublishItems.forEach(item => updateApiTask(item, {
378 status: PlatformTaskStatus.ERROR,
379 error: t('messages.publishFailed'),
380 endTime: Date.now(),
381 result: {
382 success: false,
383 failReason: t('messages.publishFailed'),
384 },
385 }))
386 return false
387 }
388
389 let res: Awaited<ReturnType<typeof createChannelPublishFlowApi>>
390 try {
391 res = await createChannelPublishFlowApi(flowParams)
392 }
393 catch {
394 apiPublishItems.forEach(item => updateApiTask(item, {
395 status: PlatformTaskStatus.ERROR,
396 error: t('messages.publishFailed'),
397 endTime: Date.now(),
398 result: {
399 success: false,
400 failReason: t('messages.publishFailed'),
401 },
402 }))
403 return false
404 }
405
406 if (res?.code !== 0) {
407 apiPublishItems.forEach(item => updateApiTask(item, {
408 status: PlatformTaskStatus.ERROR,
409 error: res?.message || t('messages.publishFailed'),
410 endTime: Date.now(),
411 result: {
412 success: false,
413 failReason: res?.message || t('messages.publishFailed'),
414 },
415 }))
416 return false
417 }
418
419 firstPublishRecordId = getPublishRecordIdFromFlow(res.data)
420
421 apiPublishItems.forEach((item) => {
422 const publishRecordId = getPublishRecordIdFromFlow(res.data, item.account.id)
423 if (!publishRecordId) {
424 updateApiTask(item, {
425 status: PlatformTaskStatus.ERROR,
426 error: t('messages.publishFailed'),
427 endTime: Date.now(),
428 result: {
429 success: false,
430 failReason: t('messages.publishFailed'),
431 },
432 })
433 return
434 }
435
436 if (item.account.type === PlatType.Douyin) {
437 updateApiTask(item, { publishRecordId })
438 void fetchAndUpdateDouyinUserAction(item, publishRecordId)
439 return
440 }
441
442 updateApiTask(item, {
443 status: PlatformTaskStatus.COMPLETED,
444 publishRecordId,
445 progress: {
446 stage: 'complete',
447 progress: 100,
448 message: t('messages.publishTaskCreated'),
449 timestamp: Date.now(),
450 },
451 result: {
452 success: true,
453 workId: publishRecordId,
454 },
455 endTime: Date.now(),
456 })
457 })
458
459 return true
460 }
461
462 const apiPublishPromise = executeApiPublish()
463 if (!hasPluginItems) {
464 const apiPublishSuccess = await apiPublishPromise
465 if (!apiPublishSuccess) {
466 setCreateLoading(false)
467 return
468 }
469 }
470 else {
471 void apiPublishPromise
472 }
473
474 if (suppressAutoPublish) {
475 if (!hasPluginItems && onPublishConfirmed) {
476 try {
477 onPublishConfirmed(taskIdForPublish, firstPublishRecordId)
478 }
479 catch (e) {
480 console.error('onPublishConfirmed callback failed', e)
481 }
482 }
483 }
484
485 setCreateLoading(false)
486 if (onPubSuccess) {
487 onPubSuccess()
488 }
489 usePublishDialogStorageStore.getState().clearPubData()
490 }, [
491 pubListChoosed,
492 pubTime,
493 suppressAutoPublish,
494 taskIdForPublish,
495 materialGroupIdForPublish,
496 materialIdForPublish,
497 onPublishConfirmed,
498 onPublishStart,
499 onClose,
500 onPubSuccess,
501 setCreateLoading,
502 setCurrentPublishTaskId,
503 setPublishDetailVisible,
504 t,
505 ])
506
507 return {
508 pubClick,
509 isPluginSupportedPlatform,
510 }
511 }
512
512 lines TYPESCRIPT