返回 AiToEarn
useDouyinPublishSession.ts
根目录 / project / aitoearn-web / src / hooks / useDouyinPublishSession.ts
1 /**
2 * useDouyinPublishSession - 抖音发布会话轮询 Hook
3 * 基于 publishRecordId 轮询发布结果,并支持 IndexedDB 恢复
4 */
5
6 'use client'
7
8 import type { DouyinPublishSessionRecord } from '@/store/douyinPublishSession'
9 import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
10 import { getPublishRecordDetailById } from '@/api/platforms/publish.api'
11 import { useDouyinPublishSessionStore } from '@/store/douyinPublishSession'
12
13 const DEFAULT_POLL_INTERVAL = 5000
14
15 function normalizeStatus(status: string | number | undefined) {
16 return String(status ?? '').toLowerCase()
17 }
18
19 function isPublishedStatus(status: string | number | undefined) {
20 const normalized = normalizeStatus(status)
21 return normalized === '1' || normalized === 'published' || normalized === 'released'
22 }
23
24 function isFailedStatus(status: string | number | undefined) {
25 const normalized = normalizeStatus(status)
26 return normalized === '-1' || normalized === 'fail' || normalized === 'failed' || normalized === 'updated_failed'
27 }
28
29 interface UseDouyinPublishSessionOptions {
30 sessionKey: string
31 enabled?: boolean
32 pollInterval?: number
33 onPublished?: (session: DouyinPublishSessionRecord) => Promise<boolean | void> | boolean | void
34 }
35
36 export function useDouyinPublishSession({
37 sessionKey,
38 enabled = true,
39 pollInterval = DEFAULT_POLL_INTERVAL,
40 onPublished,
41 }: UseDouyinPublishSessionOptions) {
42 const record = useDouyinPublishSessionStore(state => state.records[sessionKey])
43 const [polling, setPolling] = useState(false)
44 const timerRef = useRef<ReturnType<typeof setInterval> | null>(null)
45 const requestRef = useRef(false)
46 const callbackRef = useRef(false)
47 const activeRecordIdRef = useRef<string | null>(null)
48
49 const session = useMemo(() => {
50 if (!record || record.expiresAt <= Date.now())
51 return null
52 return record
53 }, [record])
54
55 const clearSession = useCallback(() => {
56 useDouyinPublishSessionStore.getState().clearSession(sessionKey)
57 }, [sessionKey])
58
59 const createSession = useCallback((payload: {
60 publishRecordId: string
61 shortLink?: string | null
62 permalink?: string | null
63 status?: DouyinPublishSessionRecord['status']
64 workLink?: string
65 errorMsg?: string
66 }) => {
67 return useDouyinPublishSessionStore.getState().setSession(sessionKey, payload)
68 }, [sessionKey])
69
70 const markManualSubmitted = useCallback(() => {
71 useDouyinPublishSessionStore.getState().markSubmitted(sessionKey, 'manual')
72 }, [sessionKey])
73
74 const stopPolling = useCallback(() => {
75 if (timerRef.current) {
76 clearInterval(timerRef.current)
77 timerRef.current = null
78 }
79 setPolling(false)
80 }, [])
81
82 const pollOnce = useCallback(async () => {
83 const current = useDouyinPublishSessionStore.getState().getSession(sessionKey)
84 if (!current?.publishRecordId || requestRef.current)
85 return current
86
87 const now = Date.now()
88 if (current.lastPolledAt && now - current.lastPolledAt < pollInterval - 100)
89 return current
90
91 useDouyinPublishSessionStore.getState().setSession(sessionKey, {
92 publishRecordId: current.publishRecordId,
93 lastPolledAt: now,
94 })
95
96 requestRef.current = true
97 try {
98 const response = await getPublishRecordDetailById(current.publishRecordId)
99 const data = response?.data
100 if (!data) {
101 useDouyinPublishSessionStore.getState().markPolling(sessionKey)
102 return current
103 }
104
105 if (isPublishedStatus(data.status)) {
106 if (!data.workLink) {
107 useDouyinPublishSessionStore.getState().markPolling(sessionKey, {
108 errorMsg: data.errorMsg,
109 })
110 return useDouyinPublishSessionStore.getState().getSession(sessionKey)
111 }
112
113 useDouyinPublishSessionStore.getState().markPublished(sessionKey, {
114 workLink: data.workLink,
115 errorMsg: data.errorMsg,
116 })
117 const latest = useDouyinPublishSessionStore.getState().getSession(sessionKey)
118 let shouldStop = !onPublished
119
120 if (latest?.manualSubmittedAt || latest?.autoSubmittedAt)
121 shouldStop = true
122
123 if (latest && onPublished && !latest.autoSubmittedAt && !latest.manualSubmittedAt && !callbackRef.current) {
124 callbackRef.current = true
125 try {
126 const result = await onPublished(latest)
127 if (result !== false) {
128 useDouyinPublishSessionStore.getState().markSubmitted(sessionKey, 'auto')
129 shouldStop = true
130 }
131 else {
132 shouldStop = false
133 }
134 }
135 finally {
136 callbackRef.current = false
137 }
138 }
139
140 if (shouldStop)
141 stopPolling()
142
143 return useDouyinPublishSessionStore.getState().getSession(sessionKey)
144 }
145
146 if (isFailedStatus(data.status)) {
147 useDouyinPublishSessionStore.getState().markFailed(sessionKey, data.errorMsg)
148 stopPolling()
149 return useDouyinPublishSessionStore.getState().getSession(sessionKey)
150 }
151
152 useDouyinPublishSessionStore.getState().markPolling(sessionKey, {
153 workLink: data.workLink,
154 errorMsg: data.errorMsg,
155 })
156 return useDouyinPublishSessionStore.getState().getSession(sessionKey)
157 }
158 catch {
159 return useDouyinPublishSessionStore.getState().getSession(sessionKey)
160 }
161 finally {
162 requestRef.current = false
163 }
164 }, [onPublished, sessionKey, stopPolling])
165
166 const startPolling = useCallback(() => {
167 const current = useDouyinPublishSessionStore.getState().getSession(sessionKey)
168 if (!enabled || !current || current.status === 'failed')
169 return
170
171 if (timerRef.current && activeRecordIdRef.current === current.publishRecordId)
172 return
173
174 if (timerRef.current) {
175 clearInterval(timerRef.current)
176 timerRef.current = null
177 }
178
179 activeRecordIdRef.current = current.publishRecordId
180 setPolling(true)
181 void pollOnce()
182 timerRef.current = setInterval(() => {
183 void pollOnce()
184 }, pollInterval)
185 }, [enabled, pollInterval, pollOnce, sessionKey])
186
187 useEffect(() => {
188 useDouyinPublishSessionStore.getState().clearExpiredSessions()
189 }, [])
190
191 useEffect(() => {
192 if (!enabled) {
193 stopPolling()
194 activeRecordIdRef.current = null
195 return
196 }
197
198 if (!session) {
199 stopPolling()
200 activeRecordIdRef.current = null
201 return
202 }
203
204 if (session.status === 'failed' || session.autoSubmittedAt || session.manualSubmittedAt) {
205 stopPolling()
206 activeRecordIdRef.current = null
207 return
208 }
209
210 startPolling()
211 return () => {
212 stopPolling()
213 activeRecordIdRef.current = null
214 }
215 }, [enabled, session?.publishRecordId, session?.status, session?.autoSubmittedAt, session?.manualSubmittedAt, startPolling, stopPolling])
216
217 return {
218 session,
219 polling,
220 createSession,
221 clearSession,
222 markManualSubmitted,
223 pollOnce,
224 startPolling,
225 stopPolling,
226 }
227 }
228
228 lines TYPESCRIPT