返回 AiToEarn
channelManagerStore.ts
根目录 / project / aitoearn-web / src / components / ChannelManager / channelManagerStore.ts
1 /**
2 * ChannelManager - 频道管理器状态管理
3 * 管理三页面视图状态、授权状态、侧边栏状态等
4 */
5
6 import type {
7 AuthState,
8 AuthUrlResponse,
9 ChannelManagerMethods,
10 ChannelManagerState,
11 ChannelManagerView,
12 } from './types'
13 import type { SocialAccount } from '@/api/accounts/account.types'
14 import type { ChannelAccountAuthStatus } from '@/api/channels/channel.types'
15 import type { PlatType } from '@/app/config/platConfig'
16 import type { PluginPlatformType } from '@/store/plugin'
17 import lodash from 'lodash'
18 import { create } from 'zustand'
19 import { combine } from 'zustand/middleware'
20 import { getChannelAccountAuthStatusApi, startChannelAccountAuthApi } from '@/api/channels/channel.api'
21 import i18next from '@/app/i18n/client'
22 import { useAccountStore } from '@/store/account'
23 import { getPlatformInfoSync, isPlatformDisabledSync, isPlatformEnabledSync } from '@/store/platformMetadata'
24 import {
25 isPluginPlatformAccountReady,
26 PLUGIN_SUPPORTED_PLATFORMS,
27 PluginStatus,
28 usePluginStore,
29 } from '@/store/plugin'
30 import { confirmPlatformRegionRedirect } from '@/utils/region/redirect'
31 import { toast } from '@/utils/ui/toast'
32 import { DEFAULT_AUTH_COUNTDOWN, POLLING_INTERVAL } from './types'
33
34 /**
35 * 获取翻译文本
36 */
37 function t(key: string, options?: Record<string, string>): string {
38 return i18next.t(key, { ns: 'account', ...options })
39 }
40
41 function getPlatformName(platform: PlatType) {
42 return getPlatformInfoSync(platform)?.name || platform
43 }
44
45 function notifyPlatformComingSoon(platform: PlatType) {
46 toast.warning(t('channelManager.platformComingSoon', { platform: getPlatformName(platform) }))
47 }
48
49 /**
50 * 检查平台是否为插件支持的平台
51 */
52 function isPluginSupportedPlatform(platform: PlatType): platform is PluginPlatformType {
53 return PLUGIN_SUPPORTED_PLATFORMS.includes(platform as PluginPlatformType)
54 }
55
56 /** 初始授权状态 */
57 const initialAuthState: AuthState = {
58 platform: null,
59 sessionId: null,
60 authUrl: null,
61 authMode: 'oauth',
62 qrCodeDataUrl: null,
63 qrCodePath: null,
64 countdown: DEFAULT_AUTH_COUNTDOWN,
65 isPolling: false,
66 error: null,
67 isTimeout: false,
68 }
69
70 /** 初始状态 */
71 const initialState: ChannelManagerState = {
72 open: false,
73 currentView: 'main',
74 selectedPlatform: 'all',
75 authState: { ...initialAuthState },
76 targetSpaceId: null,
77 onAuthSuccess: null,
78 isNewUser: false,
79 }
80
81 function getInitialState(): ChannelManagerState {
82 return lodash.cloneDeep(initialState)
83 }
84
85 /** 轮询定时器ID */
86 let pollingTimerId: ReturnType<typeof setTimeout> | null = null
87 /** 倒计时定时器ID */
88 let countdownIntervalId: ReturnType<typeof setInterval> | null = null
89
90 /**
91 * 清理所有定时器
92 */
93 function clearAllTimers() {
94 if (pollingTimerId) {
95 clearTimeout(pollingTimerId)
96 pollingTimerId = null
97 }
98 if (countdownIntervalId) {
99 clearInterval(countdownIntervalId)
100 countdownIntervalId = null
101 }
102 }
103
104 /**
105 * 根据平台类型获取授权URL
106 */
107 async function getAuthUrl(platform: PlatType, spaceId?: string): Promise<AuthUrlResponse | null> {
108 try {
109 const res = await startChannelAccountAuthApi(platform, { groupId: spaceId })
110
111 if (res?.code === 0 && res.data?.url && res.data.sessionId) {
112 return {
113 url: res.data.url,
114 sessionId: res.data.sessionId,
115 expiresAt: res.data.expiresAt,
116 }
117 }
118
119 return null
120 }
121 catch (error) {
122 console.error(`Failed to get auth URL for ${platform}:`, error)
123 return null
124 }
125 }
126
127 function getAuthCountdown(expiresAt?: string) {
128 if (!expiresAt)
129 return DEFAULT_AUTH_COUNTDOWN
130
131 const expiresTime = new Date(expiresAt).getTime()
132 if (Number.isNaN(expiresTime))
133 return DEFAULT_AUTH_COUNTDOWN
134
135 return Math.max(0, Math.ceil((expiresTime - Date.now()) / 1000))
136 }
137
138 interface AuthStatusResult {
139 status: ChannelAccountAuthStatus['status']
140 data?: ChannelAccountAuthStatus
141 message?: string
142 }
143
144 async function checkAuthStatus(
145 platform: PlatType,
146 sessionId: string,
147 ): Promise<AuthStatusResult | null> {
148 try {
149 const res = await getChannelAccountAuthStatusApi(platform, sessionId)
150
151 if (res?.code === 0 && res.data) {
152 return {
153 status: res.data.status,
154 data: res.data,
155 message: res.data.status === 'failed' ? res.message : undefined,
156 }
157 }
158
159 if (res?.message) {
160 return { status: 'failed', message: res.message }
161 }
162
163 return null
164 }
165 catch (error) {
166 console.error(`Failed to check auth status for ${platform}:`, error)
167 return null
168 }
169 }
170
171 function getAuthAccountIds(data?: ChannelAccountAuthStatus) {
172 return [
173 data?.accountId,
174 ...(data?.accountIds ?? []),
175 ...(data?.accounts?.map(account => account.accountId) ?? []),
176 ].filter((accountId): accountId is string => Boolean(accountId))
177 }
178
179 function findAuthorizedAccount(
180 accountList: SocialAccount[],
181 platform: PlatType,
182 data?: ChannelAccountAuthStatus,
183 ) {
184 const accountIds = getAuthAccountIds(data)
185
186 if (accountIds.length > 0) {
187 const accountIdSet = new Set(accountIds)
188 const matchedAccount = accountList.find(account => accountIdSet.has(account.id))
189
190 if (matchedAccount)
191 return matchedAccount
192 }
193
194 return accountList.find(account => account.type === platform)
195 }
196
197 export const useChannelManagerStore = create(
198 combine(getInitialState(), (set, get) => {
199 const methods: ChannelManagerMethods = {
200 /** 打开弹框(默认显示主页) */
201 openModal() {
202 const accountList = useAccountStore.getState().accountList
203 const isNewUser = accountList.length === 0
204
205 set({
206 open: true,
207 currentView: isNewUser ? 'connect-list' : 'main',
208 isNewUser,
209 })
210 },
211
212 /** 关闭弹框 */
213 closeModal() {
214 // 停止所有授权相关的定时器
215 methods.stopAuth()
216 set({
217 open: false,
218 currentView: 'main',
219 selectedPlatform: 'all',
220 authState: { ...initialAuthState },
221 targetSpaceId: null,
222 })
223 },
224
225 /** 直接打开并进入指定平台的授权流程 */
226 openAndAuth(platform: PlatType, spaceId?: string) {
227 if (isPlatformDisabledSync(platform)) {
228 notifyPlatformComingSoon(platform)
229 return
230 }
231
232 if (!isPlatformEnabledSync(platform)) {
233 confirmPlatformRegionRedirect(getPlatformName(platform))
234 return
235 }
236
237 // 如果没有指定空间,使用默认空间
238 const accountGroupList = useAccountStore.getState().accountGroupList
239 const defaultSpace = accountGroupList.find(g => g.isDefault)
240 const targetSpaceId = spaceId || defaultSpace?.id || null
241
242 set({
243 open: true,
244 currentView: 'auth-loading',
245 targetSpaceId,
246 })
247
248 // 开始授权流程
249 methods.startAuth(platform, targetSpaceId || undefined)
250 },
251
252 /** 直接打开并进入连接新频道列表页 */
253 openConnectList(spaceId?: string) {
254 // 如果没有指定空间,使用默认空间
255 const accountGroupList = useAccountStore.getState().accountGroupList
256 const defaultSpace = accountGroupList.find(g => g.isDefault)
257 const targetSpaceId = spaceId || defaultSpace?.id || null
258
259 set({
260 open: true,
261 currentView: 'connect-list',
262 targetSpaceId,
263 isNewUser: false,
264 })
265 },
266
267 /** 设置授权成功回调 */
268 setOnAuthSuccess(callback) {
269 set({ onAuthSuccess: callback })
270 },
271
272 /** 切换视图 */
273 setCurrentView(view: ChannelManagerView) {
274 set({ currentView: view })
275 },
276
277 /** 设置侧边栏选中的平台 */
278 setSelectedPlatform(platform) {
279 set({ selectedPlatform: platform })
280 },
281
282 /** 设置目标空间ID */
283 setTargetSpaceId(spaceId) {
284 set({ targetSpaceId: spaceId })
285 },
286
287 /** 开始授权流程 */
288 async startAuth(platform: PlatType, spaceId?: string) {
289 if (isPlatformDisabledSync(platform)) {
290 notifyPlatformComingSoon(platform)
291 set({
292 currentView: 'connect-list',
293 authState: { ...initialAuthState },
294 })
295 return
296 }
297
298 if (!isPlatformEnabledSync(platform)) {
299 confirmPlatformRegionRedirect(getPlatformName(platform))
300 set({
301 currentView: 'connect-list',
302 authState: { ...initialAuthState },
303 })
304 return
305 }
306
307 // 清理之前的定时器
308 clearAllTimers()
309
310 const platformInfo = getPlatformInfoSync(platform)
311 const authMode: AuthState['authMode'] = platformInfo?.authType === 'qrcode'
312 ? 'miniappQr'
313 : 'oauth'
314 const targetSpaceId = spaceId || get().targetSpaceId || undefined
315
316 // 设置授权状态
317 set({
318 currentView: 'auth-loading',
319 authState: {
320 ...initialAuthState,
321 platform,
322 authMode,
323 isPolling: true,
324 },
325 targetSpaceId: targetSpaceId || null,
326 })
327
328 // 检查是否为插件支持的平台
329 if (authMode !== 'miniappQr' && isPluginSupportedPlatform(platform)) {
330 await methods.handlePluginPlatformAuth(platform, targetSpaceId)
331 return
332 }
333
334 // OAuth授权流程
335 // 先同步打开空白窗口,避免 Safari 拦截弹窗
336 const authWindow = authMode === 'oauth' ? window.open('about:blank') : null
337 try {
338 // 获取授权URL
339 const authData = await getAuthUrl(platform, targetSpaceId)
340
341 if (!authData) {
342 authWindow?.close()
343 set({
344 authState: {
345 ...get().authState,
346 error: t('channelManager.authFailedTip'),
347 isPolling: false,
348 },
349 })
350 return
351 }
352
353 // 更新状态并打开授权窗口/二维码
354 set({
355 authState: {
356 ...get().authState,
357 sessionId: authData.sessionId,
358 authUrl: authData.url,
359 countdown: getAuthCountdown(authData.expiresAt),
360 qrCodeDataUrl: authMode === 'miniappQr' ? authData.url : null,
361 },
362 })
363
364 // 设置授权窗口地址
365 if (authMode === 'oauth') {
366 if (authWindow) {
367 authWindow.location.href = authData.url
368 }
369 else {
370 window.open(authData.url)
371 }
372 }
373
374 // 启动倒计时
375 countdownIntervalId = setInterval(() => {
376 const currentState = get()
377 const newCountdown = currentState.authState.countdown - 1
378
379 if (newCountdown <= 0) {
380 // 超时
381 methods.stopAuth()
382 set({
383 authState: {
384 ...currentState.authState,
385 isTimeout: true,
386 isPolling: false,
387 countdown: 0,
388 },
389 })
390 }
391 else {
392 set({
393 authState: {
394 ...currentState.authState,
395 countdown: newCountdown,
396 },
397 })
398 }
399 }, 1000)
400
401 const pollAuthStatus = async () => {
402 const authState = get().authState
403
404 if (
405 authState.sessionId !== authData.sessionId
406 || authState.platform !== platform
407 || !authState.isPolling
408 ) {
409 return
410 }
411
412 const result = await checkAuthStatus(platform, authData.sessionId)
413 const latestAuthState = get().authState
414
415 if (
416 latestAuthState.sessionId !== authData.sessionId
417 || latestAuthState.platform !== platform
418 || !latestAuthState.isPolling
419 ) {
420 return
421 }
422
423 if (result?.status === 'completed') {
424 // 授权成功
425 clearAllTimers()
426
427 // 刷新账户列表
428 await useAccountStore.getState().getAccountList()
429 const completedAuthState = get().authState
430
431 if (
432 completedAuthState.sessionId !== authData.sessionId
433 || completedAuthState.platform !== platform
434 || !completedAuthState.isPolling
435 ) {
436 return
437 }
438
439 // 获取新添加的账户
440 const accountList = useAccountStore.getState().accountList
441 const newAccount = findAuthorizedAccount(accountList, platform, result.data)
442
443 if (newAccount) {
444 methods.handleAuthSuccess(newAccount)
445 }
446 else {
447 // 没找到新账户,但也算成功
448 set({
449 currentView: 'main',
450 selectedPlatform: platform,
451 authState: { ...initialAuthState },
452 isNewUser: false,
453 })
454 }
455 }
456 else if (result?.status === 'failed' || result?.message) {
457 clearAllTimers()
458 set({
459 authState: {
460 ...latestAuthState,
461 error: result.message || t('channelManager.authFailedTip'),
462 isPolling: false,
463 },
464 })
465 }
466 else {
467 pollingTimerId = setTimeout(() => {
468 void pollAuthStatus()
469 }, POLLING_INTERVAL)
470 }
471 }
472
473 // 启动轮询:每次请求完成后再安排下一轮,避免接口重叠请求
474 pollingTimerId = setTimeout(() => {
475 void pollAuthStatus()
476 }, POLLING_INTERVAL)
477 }
478 catch (error) {
479 authWindow?.close()
480 console.error('Auth error:', error)
481 set({
482 authState: {
483 ...get().authState,
484 error: error instanceof Error ? error.message : t('channelManager.authFailedTip'),
485 isPolling: false,
486 },
487 })
488 }
489 },
490
491 /**
492 * 处理插件平台的授权(小红书、抖音等)
493 * 这些平台通过浏览器插件同步账号,而非OAuth
494 */
495 async handlePluginPlatformAuth(platform: PluginPlatformType, spaceId?: string) {
496 const pluginStore = usePluginStore.getState()
497 const platformName = getPlatformInfoSync(platform)?.name || platform
498
499 // 检查插件是否就绪
500 if (pluginStore.status !== PluginStatus.READY) {
501 // 插件未就绪,重置状态并打开插件弹框
502 set({
503 currentView: 'connect-list',
504 authState: { ...initialAuthState },
505 })
506 toast.warning(t('channelManager.pluginNotReady', { platform: platformName }))
507 // 打开插件弹框引导用户安装/授权
508 pluginStore.openPluginModal()
509 return
510 }
511
512 // 检查是否有账号
513 const account = pluginStore.platformAccounts[platform]
514 if (!account || !isPluginPlatformAccountReady(account)) {
515 // 平台未登录,重置状态并打开插件弹框
516 set({
517 currentView: 'connect-list',
518 authState: { ...initialAuthState },
519 })
520 toast.warning(t('channelManager.platformNotLoggedIn', { platform: platformName }))
521 // 打开插件弹框引导用户登录
522 pluginStore.openPluginModal()
523 return
524 }
525
526 // 同步账号到数据库
527 try {
528 const result = await pluginStore.syncAccountToDatabase(platform, spaceId)
529
530 if (result) {
531 // 同步成功
532 toast.success(t('channelManager.syncSuccess'))
533
534 // 刷新账户列表
535 await useAccountStore.getState().getAccountList()
536
537 // 处理授权成功
538 methods.handleAuthSuccess(result)
539 }
540 else {
541 set({
542 currentView: 'connect-list',
543 authState: { ...initialAuthState },
544 })
545 toast.error(t('channelManager.syncFailed'))
546 }
547 }
548 catch (error) {
549 console.error('Plugin auth error:', error)
550 set({
551 currentView: 'connect-list',
552 authState: { ...initialAuthState },
553 })
554 toast.error(t('channelManager.syncFailed'))
555 }
556 },
557
558 /** 停止授权(取消/超时) */
559 stopAuth() {
560 clearAllTimers()
561 set({
562 authState: {
563 ...get().authState,
564 isPolling: false,
565 },
566 })
567 },
568
569 /** 重新打开授权页面 */
570 reopenAuthWindow() {
571 const { authState } = get()
572 if (authState.authMode === 'miniappQr' && authState.platform) {
573 methods.startAuth(authState.platform, get().targetSpaceId || undefined)
574 return
575 }
576 if (authState.authUrl) {
577 window.open(authState.authUrl)
578 }
579 },
580
581 /** 授权成功处理 */
582 handleAuthSuccess(account: SocialAccount) {
583 const { authState, onAuthSuccess, selectedPlatform } = get()
584 const platform = authState.platform
585
586 // 调用外部回调
587 if (onAuthSuccess && platform) {
588 onAuthSuccess(account, platform)
589 }
590
591 // 移动端保持原筛选状态,PC 端切换到授权平台
592 const isMobile
593 = typeof window !== 'undefined'
594 && window.matchMedia('(max-width: 767px)').matches
595
596 // 重置状态,跳转到主页
597 set({
598 currentView: 'main',
599 selectedPlatform: isMobile
600 ? selectedPlatform
601 : (platform || 'all'),
602 authState: { ...initialAuthState },
603 isNewUser: false,
604 })
605 },
606
607 /** 重置状态 */
608 reset() {
609 clearAllTimers()
610 set(getInitialState())
611 },
612
613 /** 检查是否为新用户 */
614 checkIsNewUser() {
615 const accountList = useAccountStore.getState().accountList
616 set({ isNewUser: accountList.length === 0 })
617 },
618 }
619
620 return methods
621 }),
622 )
623
623 lines TYPESCRIPT