返回 AiToEarn
store.ts
根目录 / project / aitoearn-web / src / store / plugin / store.ts
1 /**
2 * 浏览器插件状态管理 Store
3 */
4
5 import type {
6 PlatAccountInfo,
7 PlatformPublishTask,
8 PluginPlatformType,
9 ProgressCallback,
10 ProgressEvent,
11 PublishParams,
12 PublishResult,
13 PublishTask,
14 PublishTaskListConfig,
15 WxSphLinkAnchor,
16 } from './types/baseTypes'
17 import type { CreateChannelAccountParams, SocialAccount } from '@/api/accounts/account.types'
18 import type { ChannelCreatePublishFlowParams } from '@/api/channels/channel.types'
19 import type { IPubParams } from '@/components/PublishDialog/publishDialog.type'
20 import dayjs from 'dayjs'
21 import utc from 'dayjs/plugin/utc'
22 import lodash from 'lodash'
23 import { create } from 'zustand'
24 import { combine } from 'zustand/middleware'
25 import { createChannelAccountApi } from '@/api/accounts/account.api'
26 import { createChannelPublishFlowApi } from '@/api/channels/channel.api'
27 import { PlatType } from '@/app/config/platConfig'
28 import { directTrans } from '@/app/i18n/client'
29 import { buildChannelPublishFlowParams, getPublishRecordIdFromFlow, isPublishTitleSupported } from '@/components/PublishDialog/PublishDialog.util'
30 import { PluginVersionLast } from '@/constant'
31 import { useAccountStore } from '@/store/account'
32 import { isPlatformEnabledSync } from '@/store/platformMetadata'
33 import { useUserStore } from '@/store/user'
34 import { parseTopicString } from '@/utils/common'
35 import { getOssUrl } from '@/utils/oss'
36 import { toast } from '@/utils/ui/toast'
37 import { isPluginPlatformAccountReady, mergePluginAccountStatus } from './account.utils'
38 import { DEFAULT_POLLING_INTERVAL } from './constants'
39 import {
40 buildPluginPlatformConfig,
41 calculateOverallStatus,
42 createInitialPlatformAccounts,
43 generateId,
44 } from './plugin.utils'
45 import {
46 PlatformTaskStatus,
47 PLUGIN_SUPPORTED_PLATFORMS,
48 PluginStatus as Status,
49 WX_SPH_LOGIN_EXPIRED_CODE,
50 } from './types/baseTypes'
51
52 // 启用 dayjs utc 插件
53 dayjs.extend(utc)
54
55 /**
56 * 插件发布项接口
57 * 描述单个平台的发布内容
58 */
59 export interface PluginPublishItem {
60 /** 账号信息 */
61 account: SocialAccount
62 /** 发布参数 */
63 params: IPubParams
64 }
65
66 /**
67 * 单个平台发布进度事件(扩展 ProgressEvent,附带账号信息)
68 */
69 export interface PlatformProgressEvent extends ProgressEvent {
70 /** 账号ID */
71 accountId: string
72 /** 平台类型 */
73 platform: PluginPlatformType
74 /** 请求ID */
75 requestId: string
76 }
77
78 /**
79 * 发布进度回调类型
80 */
81 export type ExecuteProgressCallback = (event: PlatformProgressEvent) => void
82
83 /**
84 * 执行插件发布的参数
85 */
86 export interface ExecutePluginPublishParams {
87 /** 需要发布的项目列表 */
88 items: PluginPublishItem[]
89 /** 账号ID 到 requestId 的映射(用于进度匹配) */
90 platformTaskIdMap: Map<string, string>
91 /** 发布时间(ISO 格式字符串,可选,不传则立即发布) */
92 publishTime?: string
93 /** 发布进度回调(可选,每个平台发布时都会触发) */
94 onProgress?: ExecuteProgressCallback
95 /** 发布完成后的回调(可选,传入发布记录ID) */
96 onComplete?: (publishRecordId?: string) => void
97 /** 关联的用户任务ID(如果是从任务流程发布) */
98 userTaskId?: string
99 /** 关联的素材组 ID(如果是从任务流程发布) */
100 materialGroupId?: string
101 /** 关联的草稿素材 ID(如果是从任务流程发布且存在推荐草稿) */
102 materialId?: string
103 /** 是否跳过添加发布任务到 store(用于 PluginPublishCard 内联发布,避免触发弹框) */
104 skipAddTask?: boolean
105 }
106
107 /** 平台账号信息映射 */
108 export type PlatformAccountsMap = Record<PluginPlatformType, PlatAccountInfo | null>
109
110 interface RefreshPlatformAccountsOptions {
111 syncAccountStore?: boolean
112 }
113
114 interface AccountStatusSnapshotOptions {
115 waitForPluginApi?: boolean
116 }
117
118 /** 错误消息 */
119 const ERROR_MESSAGES = {
120 PLUGIN_NOT_INSTALLED: '请先安装 Aitoearn 浏览器插件',
121 PLUGIN_NOT_READY: '插件未就绪,请先授权插件权限',
122 PUBLISHING_IN_PROGRESS: '当前正在发布中,请稍后再试',
123 PLATFORM_REGION_RESTRICTED: '该平台在当前区域不可用',
124 } as const
125
126 const PLUGIN_API_INJECTION_WAIT_TIMEOUT_MS = 1500
127 const PLUGIN_API_INJECTION_WAIT_INTERVAL_MS = 50
128
129 function hasPluginApi() {
130 return typeof window !== 'undefined' && !!window.AIToEarnPlugin
131 }
132
133 function wait(ms: number) {
134 return new Promise<void>(resolve => setTimeout(resolve, ms))
135 }
136
137 async function waitForPluginApiInjection() {
138 if (hasPluginApi() || typeof window === 'undefined')
139 return
140
141 const startedAt = Date.now()
142 while (Date.now() - startedAt < PLUGIN_API_INJECTION_WAIT_TIMEOUT_MS) {
143 const remainingMs = PLUGIN_API_INJECTION_WAIT_TIMEOUT_MS - (Date.now() - startedAt)
144 await wait(Math.min(PLUGIN_API_INJECTION_WAIT_INTERVAL_MS, remainingMs))
145
146 if (hasPluginApi())
147 return
148 }
149 }
150
151 /**
152 * 生成发布标识key(用于区分不同账号的发布)
153 * @param platform 平台类型
154 * @param accountId 账号ID(可选)
155 */
156 function getPublishKey(platform: PluginPlatformType, accountId?: string): string {
157 return accountId ? `${platform}-${accountId}` : platform
158 }
159
160 function getPluginErrorCode(error: unknown) {
161 if (!error || typeof error !== 'object') {
162 return undefined
163 }
164
165 return (error as { code?: string, errorCode?: string }).code
166 || (error as { code?: string, errorCode?: string }).errorCode
167 }
168 function isFinalProgressEvent(progress: ProgressEvent) {
169 return progress.stage === 'complete' || progress.stage === 'error'
170 }
171
172 function getWxSphLinkAnchor(result: { workId?: string, platformData?: unknown }) {
173 const platformData = result.platformData as WxSphLinkAnchor | undefined
174 const mediaMd5sum = platformData?.mediaMd5sum || result.workId
175
176 if (!mediaMd5sum) {
177 return null
178 }
179
180 return {
181 mediaMd5sum,
182 videoClipTaskId: platformData?.videoClipTaskId,
183 scheduledTime: platformData?.scheduledTime,
184 }
185 }
186
187 async function createPluginPublishRecord(params: {
188 item: PluginPublishItem
189 result: PublishResult
190 publishTime?: string
191 userTaskId?: string
192 materialGroupId?: string
193 materialId?: string
194 }) {
195 const flowParams = buildChannelPublishFlowParams([params.item], {
196 publishAt: params.publishTime || dayjs(Date.now()).utc().format(),
197 userTaskId: params.userTaskId,
198 materialGroupId: params.materialGroupId,
199 materialId: params.materialId,
200 source: 'web',
201 })
202
203 if (!flowParams)
204 return undefined
205
206 applyPluginPublishResultToFlow(
207 flowParams,
208 params.item.account.type as PluginPlatformType,
209 params.result,
210 )
211
212 const recordRes = await createChannelPublishFlowApi(flowParams)
213 if (recordRes?.code !== 0)
214 return undefined
215
216 return getPublishRecordIdFromFlow(recordRes.data, params.item.account.id)
217 }
218
219 function applyPluginPublishResultToFlow(
220 flowParams: ChannelCreatePublishFlowParams,
221 platform: PluginPlatformType,
222 result: PublishResult,
223 ) {
224 const flowItem = flowParams.items[0]
225 if (!flowItem)
226 return
227
228 if (platform === PlatType.Xhs && result.shareLink) {
229 flowItem.option = {
230 ...(flowItem.option || {}),
231 workLink: result.shareLink,
232 }
233 return
234 }
235
236 if (platform === PlatType.WxSph) {
237 const wxSphAnchor = getWxSphLinkAnchor(result)
238 flowItem.option = {
239 ...(flowItem.option || {}),
240 workId: wxSphAnchor?.mediaMd5sum || result.workId,
241 workLink: result.shareLink,
242 linkStatus: wxSphAnchor && !result.shareLink ? 'pending' : 'ready',
243 linkMeta: wxSphAnchor || undefined,
244 }
245 }
246 }
247
248 function getPluginApiBaseUrl() {
249 const apiBaseUrl = process.env.NEXT_PUBLIC_API_URL
250 if (!apiBaseUrl)
251 return undefined
252
253 if (apiBaseUrl.startsWith('http://') || apiBaseUrl.startsWith('https://'))
254 return apiBaseUrl
255
256 if (typeof window === 'undefined')
257 return apiBaseUrl
258
259 return new URL(apiBaseUrl, window.location.origin).toString()
260 }
261
262 async function notifyWxSphLoginRequired() {
263 await usePluginStore.getState().refreshAllPlatformAccounts()
264 toast.warning(directTrans('publish', 'messages.wxSphLinkPollingLoginRequired'), {
265 id: 'wx-sph-link-polling-login-required',
266 duration: 6,
267 })
268 }
269
270 async function startWxSphLinkPolling(params: {
271 recordId: string
272 accountId: string
273 result: { workId?: string, platformData?: unknown }
274 }) {
275 const anchor = getWxSphLinkAnchor(params.result)
276 if (!anchor || !window.AIToEarnPlugin?.wxSphStartLinkPolling)
277 return
278
279 const token = useUserStore.getState().token
280 const apiBaseUrl = getPluginApiBaseUrl()
281 try {
282 const response = await window.AIToEarnPlugin.wxSphStartLinkPolling({
283 recordId: params.recordId,
284 mediaMd5sum: anchor.mediaMd5sum,
285 videoClipTaskId: anchor.videoClipTaskId,
286 scheduledTime: anchor.scheduledTime,
287 accountId: params.accountId,
288 apiBaseUrl,
289 authToken: token,
290 })
291
292 if (response?.code === WX_SPH_LOGIN_EXPIRED_CODE) {
293 await notifyWxSphLoginRequired()
294 }
295 }
296 catch (error) {
297 const errorCode = getPluginErrorCode(error)
298 if (errorCode === WX_SPH_LOGIN_EXPIRED_CODE) {
299 await notifyWxSphLoginRequired()
300 }
301 console.error('Failed to start WxSph link polling:', error)
302 }
303 }
304
305 /** 平台发布进度映射,key 为 platform 或 platform-accountId */
306 export type PlatformProgressMap = Map<string, ProgressEvent>
307 export type PluginVersionLoadStatus = 'idle' | 'loading' | 'ready' | 'unavailable'
308 const FALLBACK_PLUGIN_VERSION = '3.0.0'
309
310 function comparePluginVersions(currentVersion: string, latestVersion: string) {
311 const currentParts = currentVersion.split('.').map(part => Number.parseInt(part, 10) || 0)
312 const latestParts = latestVersion.split('.').map(part => Number.parseInt(part, 10) || 0)
313 const maxLength = Math.max(currentParts.length, latestParts.length)
314
315 for (let index = 0; index < maxLength; index += 1) {
316 const current = currentParts[index] ?? 0
317 const latest = latestParts[index] ?? 0
318
319 if (current > latest)
320 return 1
321 if (current < latest)
322 return -1
323 }
324
325 return 0
326 }
327
328 /** 插件 Store 状态接口(只定义属性) */
329 export interface IPluginStore {
330 status: Status
331 /** 插件是否已授予站点访问权限 */
332 hostAccessGranted: boolean | null
333 pollingTimer: NodeJS.Timeout | null
334 /** 插件是否正在初始化(检测插件、权限、账号登录状态) */
335 isInitializing: boolean
336 /** 是否正在发布(任意平台) */
337 isPublishing: boolean
338 /** 正在发布的集合,key 为 platform 或 platform-accountId,支持同一平台多账号同时发布 */
339 publishingPlatforms: Set<string>
340 /** 当前发布进度(最新一个) */
341 publishProgress: ProgressEvent | null
342 /** 各平台发布进度,key 为 platform 或 platform-accountId */
343 platformProgress: PlatformProgressMap
344 publishTasks: PublishTask[]
345 taskListConfig: PublishTaskListConfig
346 platformAccounts: PlatformAccountsMap
347 /** 插件弹框是否可见 */
348 pluginModalVisible: boolean
349 /** 当前打开中的发布详情弹框数量,用于全局悬浮入口避让 */
350 publishDetailModalOpenCount: number
351 /** 是否正在创建发布记录 */
352 isCreatingRecord: boolean
353 /** 当前插件版本 */
354 pluginVersion: string | null
355 /** 插件版本获取状态 */
356 pluginVersionStatus: PluginVersionLoadStatus
357 /** 插件是否可更新 */
358 pluginNeedsUpdate: boolean
359 }
360
361 const store: IPluginStore = {
362 status: Status.UNKNOWN,
363 hostAccessGranted: null,
364 pollingTimer: null,
365 isInitializing: true, // 初始状态为正在初始化
366 isPublishing: false,
367 publishingPlatforms: new Set(),
368 publishProgress: null,
369 platformProgress: new Map(),
370 publishTasks: [],
371 taskListConfig: {
372 maxTasks: 100,
373 autoCleanCompleted: false,
374 cleanAfter: 24 * 60 * 60 * 1000,
375 },
376 platformAccounts: createInitialPlatformAccounts(),
377 pluginModalVisible: false,
378 publishDetailModalOpenCount: 0,
379 isCreatingRecord: false,
380 pluginVersion: null,
381 pluginVersionStatus: 'idle',
382 pluginNeedsUpdate: false,
383 }
384
385 function getStore() {
386 return lodash.cloneDeep(store)
387 }
388
389 /** 创建插件管理 Store */
390 export const usePluginStore = create(
391 combine({ ...getStore() }, (set, get) => {
392 const methods = {
393 clear() {
394 set({ ...getStore() })
395 },
396
397 /** 打开插件弹框 */
398 openPluginModal() {
399 set({ pluginModalVisible: true })
400 },
401
402 /** 关闭插件弹框 */
403 closePluginModal() {
404 set({ pluginModalVisible: false })
405 },
406
407 /** 标记发布详情弹框已打开 */
408 registerPublishDetailModalOpen() {
409 set(state => ({
410 publishDetailModalOpenCount: state.publishDetailModalOpenCount + 1,
411 }))
412 },
413
414 /** 标记发布详情弹框已关闭 */
415 unregisterPublishDetailModalOpen() {
416 set(state => ({
417 publishDetailModalOpenCount: Math.max(0, state.publishDetailModalOpenCount - 1),
418 }))
419 },
420
421 /** 清空插件版本信息 */
422 clearPluginVersion() {
423 set({
424 pluginVersion: null,
425 pluginVersionStatus: 'idle',
426 pluginNeedsUpdate: false,
427 })
428 },
429
430 /** 获取插件版本信息 */
431 async fetchPluginVersion(force = false) {
432 const plugin = typeof window !== 'undefined' ? window.AIToEarnPlugin : undefined
433 const isInstalled = !!plugin
434 const { pluginVersion, pluginVersionStatus, status } = get()
435
436 if (!isInstalled || status !== Status.READY) {
437 methods.clearPluginVersion()
438 return null
439 }
440
441 if (!force && pluginVersionStatus === 'loading')
442 return pluginVersion
443
444 if (!force && pluginVersionStatus === 'ready' && pluginVersion)
445 return pluginVersion
446
447 set({ pluginVersionStatus: 'loading' })
448
449 try {
450 const version = (await plugin?.getVersion?.())?.version?.trim() || FALLBACK_PLUGIN_VERSION
451
452 set({
453 pluginVersion: version,
454 pluginVersionStatus: 'ready',
455 pluginNeedsUpdate: comparePluginVersions(version, PluginVersionLast) < 0,
456 })
457
458 return version
459 }
460 catch (error) {
461 console.error('获取插件版本失败:', error)
462 set({
463 pluginVersion: FALLBACK_PLUGIN_VERSION,
464 pluginVersionStatus: 'ready',
465 pluginNeedsUpdate: comparePluginVersions(FALLBACK_PLUGIN_VERSION, PluginVersionLast) < 0,
466 })
467 return FALLBACK_PLUGIN_VERSION
468 }
469 },
470
471 /** 检查插件是否安装 */
472 checkPlugin() {
473 const isAvailable = typeof window !== 'undefined' && !!window.AIToEarnPlugin
474
475 if (!isAvailable) {
476 methods.clearPluginVersion()
477 set({ status: Status.NOT_INSTALLED, hostAccessGranted: null })
478 return false
479 }
480 const currentStatus = get().status
481 if (currentStatus === Status.UNKNOWN || currentStatus === Status.NOT_INSTALLED) {
482 set({ status: Status.CHECKING, hostAccessGranted: null })
483 }
484 return true
485 },
486
487 /** 检查插件权限 */
488 async checkPermission() {
489 const isInstalled = typeof window !== 'undefined' && !!window.AIToEarnPlugin
490 if (!isInstalled) {
491 methods.clearPluginVersion()
492 set({ status: Status.NOT_INSTALLED, hostAccessGranted: null })
493 return false
494 }
495 try {
496 const result = await window.AIToEarnPlugin!.checkPermission()
497 const hostAccessGranted = typeof result.hostAccess === 'boolean' ? result.hostAccess : null
498 if (result.granted && result.hostAccess !== false) {
499 set({ status: Status.READY, hostAccessGranted: true })
500 void methods.fetchPluginVersion(true)
501 return true
502 }
503 else {
504 methods.clearPluginVersion()
505 set({ status: Status.INSTALLED_NO_PERMISSION, hostAccessGranted })
506 return false
507 }
508 }
509 catch (error) {
510 console.error('权限检查失败:', error)
511 methods.clearPluginVersion()
512 set({ status: Status.INSTALLED_NO_PERMISSION, hostAccessGranted: null })
513 return false
514 }
515 },
516
517 /** 开始轮询插件状态 */
518 startPolling(interval = DEFAULT_POLLING_INTERVAL) {
519 const { pollingTimer } = get()
520 if (pollingTimer)
521 methods.stopPolling()
522
523 set({ status: Status.CHECKING })
524
525 const poll = async () => {
526 const isInstalled = methods.checkPlugin()
527 if (!isInstalled)
528 return
529
530 const hasPermission = await methods.checkPermission()
531 // 已安装且已授权,停止轮询并刷新账号信息
532 if (hasPermission) {
533 methods.stopPolling()
534 await methods.refreshAllPlatformAccounts()
535 }
536 }
537
538 poll()
539 const timer = setInterval(poll, interval)
540 set({ pollingTimer: timer })
541 },
542
543 /** 停止轮询插件状态 */
544 stopPolling() {
545 const { pollingTimer } = get()
546 if (pollingTimer) {
547 clearInterval(pollingTimer)
548 set({ pollingTimer: null })
549 }
550 },
551
552 /**
553 * 初始化方法
554 * 1. 先将所有抖音和小红书账号设为离线
555 * 2. 检查插件状态,未安装或未授权则轮询,已就绪则刷新账号
556 */
557 async init() {
558 // 设置初始化状态
559 set({ isInitializing: true })
560
561 // 先将所有插件支持的平台账号设为离线
562 methods.setAllPluginAccountsOffline()
563
564 const isInstalled = methods.checkPlugin()
565 if (!isInstalled) {
566 // 未安装,开始轮询,初始化完成
567 set({ isInitializing: false })
568 methods.startPolling()
569 return
570 }
571
572 const hasPermission = await methods.checkPermission()
573 if (!hasPermission) {
574 // 未授权,开始轮询,初始化完成
575 set({ isInitializing: false })
576 methods.startPolling()
577 return
578 }
579
580 // 已就绪,刷新账号信息
581 await methods.refreshAllPlatformAccounts()
582 // 初始化完成
583 set({ isInitializing: false })
584 },
585
586 /** 将所有插件支持的平台账号设为离线 */
587 setAllPluginAccountsOffline() {
588 const { accountList } = useAccountStore.getState()
589 const mergedAccountState = mergePluginAccountStatus(accountList, createInitialPlatformAccounts())
590 const hasChange = mergedAccountState.accountList.some((account, index) => account !== accountList[index])
591
592 if (hasChange)
593 useAccountStore.setState(mergedAccountState)
594 },
595
596 /**
597 * 同步账号状态(仅当插件已就绪时执行)
598 * 用于刷新账号列表后,不重新授权,只同步在线/离线状态
599 */
600 async syncAccountStatus() {
601 methods.setAllPluginAccountsOffline()
602 const { status } = get()
603 if (status === Status.READY) {
604 await methods.refreshAllPlatformAccounts()
605 }
606 },
607
608 /** 获取插件平台账号状态快照,不直接回写 accountList */
609 async getAccountStatusSnapshot(isBackground = false, options?: AccountStatusSnapshotOptions) {
610 if (isBackground) {
611 set({ isInitializing: true })
612 }
613
614 const finish = (accounts: PlatformAccountsMap) => {
615 if (isBackground) {
616 set({ isInitializing: false })
617 }
618 return accounts
619 }
620
621 const offlineAccounts = createInitialPlatformAccounts()
622 if (options?.waitForPluginApi) {
623 await waitForPluginApiInjection()
624 }
625
626 const isInstalled = methods.checkPlugin()
627 if (!isInstalled) {
628 if (isBackground) {
629 methods.startPolling()
630 }
631 return finish(offlineAccounts)
632 }
633
634 const hasPermission = await methods.checkPermission()
635 if (!hasPermission) {
636 if (isBackground) {
637 methods.startPolling()
638 }
639 return finish(offlineAccounts)
640 }
641
642 const accounts = await methods.refreshAllPlatformAccounts({ syncAccountStore: false })
643 return finish(accounts)
644 },
645
646 /** 刷新所有平台账号信息,并同步更新 accountList 中的在线/离线状态 */
647 async refreshAllPlatformAccounts(options?: RefreshPlatformAccountsOptions) {
648 const syncAccountStore = options?.syncAccountStore ?? true
649 const { status } = get()
650 if (status !== Status.READY)
651 return createInitialPlatformAccounts()
652
653 const accounts: PlatformAccountsMap = createInitialPlatformAccounts()
654
655 await Promise.all(
656 PLUGIN_SUPPORTED_PLATFORMS.map(async (platform) => {
657 try {
658 accounts[platform] = await window.AIToEarnPlugin!.login(platform)
659 }
660 catch {
661 accounts[platform] = null
662 }
663 }),
664 )
665
666 set({ platformAccounts: accounts as PlatformAccountsMap })
667
668 if (syncAccountStore) {
669 const { accountList } = useAccountStore.getState()
670 const mergedAccountState = mergePluginAccountStatus(accountList, accounts)
671 const hasChange = mergedAccountState.accountList.some((account, index) => account !== accountList[index])
672
673 if (hasChange) {
674 useAccountStore.setState(mergedAccountState)
675 }
676 }
677
678 return accounts
679 },
680
681 /** 同步插件账号到数据库 */
682 async syncAccountToDatabase(platform: PluginPlatformType, groupId?: string) {
683 if (!isPlatformEnabledSync(platform)) {
684 console.warn('同步账号失败:该平台在当前区域不可用', platform)
685 return null
686 }
687
688 const { platformAccounts } = get()
689 const account = platformAccounts[platform]
690
691 if (!account || !isPluginPlatformAccountReady(account)) {
692 console.warn('同步账号失败:该平台未完成登录', platform)
693 return null
694 }
695
696 try {
697 const accountData: CreateChannelAccountParams = {
698 type: platform,
699 uid: account.uid,
700 nickname: account.nickname,
701 loginCookie: account.loginCookie,
702 }
703
704 if (account.avatar)
705 accountData.avatar = account.avatar
706
707 if (groupId)
708 accountData.groupId = groupId
709
710 const result = await createChannelAccountApi(accountData)
711
712 if (result?.code === 0) {
713 await useAccountStore.getState().getAccountList()
714 return result.data || null
715 }
716 else {
717 console.error('同步账号失败:', result?.message)
718 return null
719 }
720 }
721 catch (error) {
722 console.error('同步账号到数据库失败:', error)
723 return null
724 }
725 },
726
727 /** 登录到指定平台 */
728 async login(platform: PluginPlatformType) {
729 const { status, platformAccounts } = get()
730
731 if (status === Status.NOT_INSTALLED)
732 throw new Error(ERROR_MESSAGES.PLUGIN_NOT_INSTALLED)
733
734 if (status !== Status.READY)
735 throw new Error(ERROR_MESSAGES.PLUGIN_NOT_READY)
736
737 try {
738 const result = await window.AIToEarnPlugin!.login(platform)
739 set({
740 platformAccounts: { ...platformAccounts, [platform]: result },
741 })
742 await methods.syncAccountToDatabase(platform)
743 return result
744 }
745 catch (error) {
746 console.error('登录失败:', error)
747 throw error
748 }
749 },
750
751 /** 发布内容到指定平台 */
752 async publish(params: PublishParams, onProgress?: ProgressCallback) {
753 const { status, publishingPlatforms, platformProgress } = get()
754 const platform = params.platform
755
756 if (!isPlatformEnabledSync(platform))
757 throw new Error(ERROR_MESSAGES.PLATFORM_REGION_RESTRICTED)
758
759 // 解析话题
760 const { topics, cleanedString } = parseTopicString(params.desc || '')
761 params.topics = [...new Set(params.topics?.concat(topics))]
762 params.desc = cleanedString
763
764 const accountId = params.accountId
765 // 使用 platform + accountId 作为唯一标识,支持同一平台多账号同时发布
766 const publishKey = getPublishKey(platform, accountId)
767
768 if (status === Status.NOT_INSTALLED)
769 throw new Error(ERROR_MESSAGES.PLUGIN_NOT_INSTALLED)
770
771 if (status !== Status.READY)
772 throw new Error(ERROR_MESSAGES.PLUGIN_NOT_READY)
773
774 // 检查该账号是否正在发布(同一平台不同账号可以同时发布)
775 if (publishingPlatforms.has(publishKey))
776 throw new Error(`${platform} ${ERROR_MESSAGES.PUBLISHING_IN_PROGRESS}`)
777
778 // 标记该账号正在发布,并初始化进度
779 const newPublishingPlatforms = new Set(publishingPlatforms)
780 newPublishingPlatforms.add(publishKey)
781 const newPlatformProgress = new Map(platformProgress)
782 const initialProgress: ProgressEvent = {
783 stage: 'download',
784 progress: 0,
785 message: '准备发布...',
786 timestamp: Date.now(),
787 }
788 newPlatformProgress.set(publishKey, initialProgress)
789
790 set({
791 isPublishing: newPublishingPlatforms.size > 0,
792 publishingPlatforms: newPublishingPlatforms,
793 publishProgress: initialProgress,
794 platformProgress: newPlatformProgress,
795 })
796
797 try {
798 const result = await window.AIToEarnPlugin!.publish(params, (progress) => {
799 // 更新该账号的进度
800 const updatedProgress = new Map(get().platformProgress)
801 updatedProgress.set(publishKey, progress)
802 set({
803 publishProgress: progress,
804 platformProgress: updatedProgress,
805 })
806 onProgress?.(progress)
807 })
808
809 // 发布完成,移除该账号的发布状态,更新进度为完成
810 const updatedPlatforms = new Set(get().publishingPlatforms)
811 updatedPlatforms.delete(publishKey)
812 const completedProgress: ProgressEvent = {
813 stage: 'complete',
814 progress: 100,
815 message: '发布成功',
816 timestamp: Date.now(),
817 }
818 const updatedPlatformProgress = new Map(get().platformProgress)
819 updatedPlatformProgress.set(publishKey, completedProgress)
820
821 set({
822 isPublishing: updatedPlatforms.size > 0,
823 publishingPlatforms: updatedPlatforms,
824 publishProgress: completedProgress,
825 platformProgress: updatedPlatformProgress,
826 })
827
828 return result
829 }
830 catch (error) {
831 // 发布失败,移除该账号的发布状态,更新进度为错误
832 const errorCode = getPluginErrorCode(error)
833 if (platform === PlatType.WxSph && errorCode === WX_SPH_LOGIN_EXPIRED_CODE) {
834 await methods.refreshAllPlatformAccounts()
835 }
836
837 const updatedPlatforms = new Set(get().publishingPlatforms)
838 updatedPlatforms.delete(publishKey)
839 const errorProgress: ProgressEvent = {
840 stage: 'error',
841 progress: 0,
842 message: error instanceof Error ? error.message : '发布失败',
843 timestamp: Date.now(),
844 data: {
845 code: errorCode,
846 error: error instanceof Error ? error : new Error('发布失败'),
847 },
848 }
849 const updatedPlatformProgress = new Map(get().platformProgress)
850 updatedPlatformProgress.set(publishKey, errorProgress)
851
852 set({
853 isPublishing: updatedPlatforms.size > 0,
854 publishingPlatforms: updatedPlatforms,
855 publishProgress: errorProgress,
856 platformProgress: updatedPlatformProgress,
857 })
858 console.error('发布失败:', error)
859 throw error
860 }
861 },
862
863 /** 重置发布状态 */
864 resetPublishState() {
865 set({
866 isPublishing: false,
867 publishingPlatforms: new Set(),
868 publishProgress: null,
869 platformProgress: new Map(),
870 })
871 },
872
873 /** 获取指定平台/账号的发布进度 */
874 getPlatformProgress(platform: PluginPlatformType, accountId?: string) {
875 const publishKey = getPublishKey(platform, accountId)
876 return get().platformProgress.get(publishKey) || null
877 },
878
879 /** 清除指定平台/账号的发布进度 */
880 clearPlatformProgress(platform: PluginPlatformType, accountId?: string) {
881 const publishKey = getPublishKey(platform, accountId)
882 const updatedProgress = new Map(get().platformProgress)
883 updatedProgress.delete(publishKey)
884 set({ platformProgress: updatedProgress })
885 },
886
887 /** 添加发布任务 */
888 addPublishTask(task: Omit<PublishTask, 'id' | 'createdAt' | 'updatedAt' | 'overallStatus'>) {
889 const id = generateId()
890 const now = Date.now()
891
892 const newTask: PublishTask = {
893 ...task,
894 id,
895 createdAt: now,
896 updatedAt: now,
897 overallStatus: calculateOverallStatus(task.platformTasks),
898 }
899
900 set((state) => {
901 const tasks = [newTask, ...state.publishTasks]
902 if (state.taskListConfig.maxTasks && tasks.length > state.taskListConfig.maxTasks) {
903 tasks.splice(state.taskListConfig.maxTasks)
904 }
905 return { publishTasks: tasks }
906 })
907
908 return id
909 },
910
911 /**
912 * 更新平台任务(使用平台任务ID精确匹配)
913 * @param taskId 发布任务ID
914 * @param platformTaskId 平台任务ID(精确匹配)
915 * @param updates 更新内容
916 */
917 updatePlatformTask(
918 taskId: string,
919 platformTaskId: string,
920 updates: Partial<PlatformPublishTask>,
921 ) {
922 set((state) => {
923 const tasks = state.publishTasks.map((task) => {
924 if (task.id !== taskId)
925 return task
926
927 const platformTasks = task.platformTasks.map((pt: PlatformPublishTask) => {
928 // 使用平台任务ID精确匹配
929 if (pt.id !== platformTaskId)
930 return pt
931 return { ...pt, ...updates }
932 })
933
934 return {
935 ...task,
936 platformTasks,
937 updatedAt: Date.now(),
938 overallStatus: calculateOverallStatus(platformTasks),
939 }
940 })
941 return { publishTasks: tasks }
942 })
943 },
944
945 /**
946 * 通过 requestId 更新平台任务进度(插件回调使用)
947 * @param requestId 插件返回的请求ID
948 * @param updates 更新内容
949 */
950 updatePlatformTaskByRequestId(requestId: string, updates: Partial<PlatformPublishTask>) {
951 set((state) => {
952 const tasks = state.publishTasks.map((task) => {
953 // 在该任务的所有平台任务中查找匹配的 requestId
954 const hasMatch = task.platformTasks.some(pt => pt.requestId === requestId)
955
956 if (!hasMatch)
957 return task
958
959 const platformTasks = task.platformTasks.map((pt: PlatformPublishTask) => {
960 // 使用 requestId 精确匹配
961 if (pt.requestId !== requestId)
962 return pt
963 return { ...pt, ...updates }
964 })
965
966 return {
967 ...task,
968 platformTasks,
969 updatedAt: Date.now(),
970 overallStatus: calculateOverallStatus(platformTasks),
971 }
972 })
973 return { publishTasks: tasks }
974 })
975 },
976
977 /** 删除发布任务 */
978 deletePublishTask(taskId: string) {
979 set(state => ({
980 publishTasks: state.publishTasks.filter(task => task.id !== taskId),
981 }))
982 },
983
984 /** 清空所有任务 */
985 clearPublishTasks() {
986 set({ publishTasks: [] })
987 },
988
989 /** 获取任务详情 */
990 getPublishTask(taskId: string) {
991 return get().publishTasks.find(task => task.id === taskId)
992 },
993
994 /** 更新任务列表配置 */
995 updateTaskListConfig(config: Partial<PublishTaskListConfig>) {
996 set(state => ({
997 taskListConfig: { ...state.taskListConfig, ...config },
998 }))
999 },
1000
1001 /**
1002 * 执行插件发布(封装完整的发布流程)
1003 * 支持并行发布多个平台,支持定时发布
1004 * @param params 发布参数
1005 * @returns Promise<void>
1006 */
1007 async executePluginPublish(params: ExecutePluginPublishParams): Promise<void> {
1008 const { items, platformTaskIdMap, publishTime, onProgress, onComplete, userTaskId, materialGroupId, materialId } = params
1009 let firstPublishRecordId: string | undefined
1010
1011 // 创建发布任务
1012 const platformTasks: PlatformPublishTask[] = items.map((item) => {
1013 const platform = item.account.type as PluginPlatformType
1014 const accountId = item.account.id
1015 const requestId = platformTaskIdMap.get(accountId) || ''
1016
1017 // 构造 PublishParams
1018 const publishParams: PublishParams = {
1019 platform,
1020 accountId,
1021 requestId,
1022 type: item.params.video ? 'video' : 'image',
1023 desc: item.params.des || '',
1024 topics: item.params.topics || [],
1025 platformConfig: buildPluginPlatformConfig(platform, item.params),
1026 }
1027
1028 if (isPublishTitleSupported(item.account.type))
1029 publishParams.title = item.params.title || ''
1030
1031 // 添加视频或图片参数
1032 if (item.params.video) {
1033 publishParams.video = item.params.video.ossUrl
1034 if (item.params.video.cover?.ossUrl) {
1035 publishParams.cover = item.params.video.cover.ossUrl
1036 }
1037 }
1038 else if (item.params.images && item.params.images.length > 0) {
1039 publishParams.images = item.params.images
1040 .map(img => img.ossUrl)
1041 .filter((url): url is string => typeof url === 'string' && url.length > 0)
1042 }
1043
1044 return {
1045 id: generateId(),
1046 platform,
1047 accountId,
1048 requestId,
1049 params: publishParams,
1050 status: PlatformTaskStatus.PENDING,
1051 progress: null,
1052 result: null,
1053 startTime: Date.now(),
1054 endTime: null,
1055 error: null,
1056 }
1057 })
1058
1059 // Avoid adding a duplicate task when the same requestId was already registered.
1060 const existingRequestIds = new Set(
1061 platformTasks
1062 .map(platformTask => platformTask.requestId)
1063 .filter((requestId): requestId is string => !!requestId),
1064 )
1065 const hasExistingPublishTask = existingRequestIds.size > 0 && get().publishTasks.some(task =>
1066 task.platformTasks.some(platformTask => (
1067 !!platformTask.requestId && existingRequestIds.has(platformTask.requestId)
1068 )),
1069 )
1070
1071 let taskId: string | undefined
1072 if (!params.skipAddTask && !hasExistingPublishTask) {
1073 taskId = methods.addPublishTask({
1074 title: items[0]?.params.title || '插件发布任务',
1075 description: `发布到 ${items.length} 个平台`,
1076 platformTasks,
1077 })
1078 }
1079
1080 // 并行执行插件发布(不等待,同时发布多个平台)
1081 const publishPromises = items.map(async (item) => {
1082 const platform = item.account.type as PluginPlatformType
1083 const accountId = item.account.id
1084 // 获取该账号对应的 requestId(用于进度匹配)
1085 const requestId = platformTaskIdMap.get(accountId)
1086
1087 if (!requestId) {
1088 console.error('未找到账号对应的 requestId:', accountId)
1089 return
1090 }
1091
1092 // 更新任务状态为发布中
1093 methods.updatePlatformTaskByRequestId(requestId, {
1094 status: PlatformTaskStatus.PUBLISHING,
1095 startTime: Date.now(),
1096 })
1097
1098 try {
1099 // 构建插件发布参数
1100 // 优先传递 File 对象,避免插件需要重新下载
1101 const publishParams: PublishParams = {
1102 platform,
1103 accountId, // 传入账号ID,用于区分同一平台的多个账号
1104 requestId, // 传入 requestId,插件回调时带回用于匹配
1105 type: item.params.video ? 'video' : 'image',
1106 desc: item.params.des || '',
1107 topics: item.params.topics || [],
1108 platformConfig: buildPluginPlatformConfig(platform, item.params),
1109 }
1110
1111 if (isPublishTitleSupported(item.account.type))
1112 publishParams.title = item.params.title || ''
1113
1114 // 如果有定时发布时间,则传入
1115 if (publishTime) {
1116 publishParams.scheduledTime = dayjs(publishTime).valueOf()
1117 }
1118
1119 // 视频发布 - 优先传 ossUrl,没有 ossUrl 才传 file(避免空占位 Blob 产生 blob URL)
1120 if (item.params.video) {
1121 // 视频:优先传 ossUrl,file 仅在无 ossUrl 且 file 有内容时使用
1122 if (item.params.video.ossUrl) {
1123 publishParams.video = getOssUrl(item.params.video.ossUrl)
1124 }
1125 else if (item.params.video.file && item.params.video.file.size > 0) {
1126 const videoFile = new File(
1127 [item.params.video.file],
1128 item.params.video.filename || 'video.mp4',
1129 { type: item.params.video.file.type },
1130 )
1131 publishParams.video = videoFile
1132 }
1133
1134 // 封面:优先传 ossUrl,file 仅在无 ossUrl 且 file 有内容时使用
1135 if (item.params.video.cover?.ossUrl) {
1136 publishParams.cover = getOssUrl(item.params.video.cover.ossUrl)
1137 }
1138 else if (item.params.video.cover?.file && item.params.video.cover.file.size > 0) {
1139 publishParams.cover = item.params.video.cover.file
1140 }
1141 }
1142 // 图文发布 - 优先传 ossUrl,没有 ossUrl 才传 file
1143 else if (item.params.images && item.params.images.length > 0) {
1144 publishParams.images = item.params.images
1145 .map((img) => {
1146 if (img.ossUrl)
1147 return getOssUrl(img.ossUrl)
1148 if (img.file && img.file.size > 0)
1149 return img.file
1150 return ''
1151 })
1152 .filter(v => v !== '')
1153 }
1154
1155 // 执行发布,通过 requestId 匹配进度
1156 const result = await methods.publish(publishParams, (progress) => {
1157 // 使用 requestId 精确更新进度
1158 methods.updatePlatformTaskByRequestId(requestId, {
1159 progress,
1160 })
1161
1162 // 触发外部进度回调(最终态由后续统一回调,避免重复通知)
1163 if (!isFinalProgressEvent(progress)) {
1164 onProgress?.({
1165 ...progress,
1166 accountId,
1167 platform,
1168 requestId,
1169 })
1170 }
1171 })
1172
1173 // 发布成功,更新任务状态
1174 methods.updatePlatformTaskByRequestId(requestId, {
1175 status: PlatformTaskStatus.COMPLETED,
1176 result: {
1177 success: true,
1178 workId: result.workId,
1179 shareLink: result.shareLink,
1180 platformData: result.platformData,
1181 },
1182 endTime: Date.now(),
1183 })
1184
1185 // 触发成功进度回调
1186 onProgress?.({
1187 stage: 'complete',
1188 progress: 100,
1189 message: '发布成功',
1190 timestamp: Date.now(),
1191 accountId,
1192 platform,
1193 requestId,
1194 data: {
1195 workId: result.workId,
1196 shareLink: result.shareLink,
1197 platformData: result.platformData,
1198 },
1199 })
1200
1201 // 发布成功后,创建发布记录
1202 set({ isCreatingRecord: true })
1203 try {
1204 const publishRecordId = await createPluginPublishRecord({
1205 item,
1206 result,
1207 publishTime,
1208 userTaskId,
1209 materialGroupId,
1210 materialId,
1211 })
1212 // 发布记录创建成功,记录首个 publishRecordId
1213 if (publishRecordId) {
1214 if (!firstPublishRecordId) {
1215 firstPublishRecordId = publishRecordId
1216 }
1217 if (platform === PlatType.WxSph) {
1218 startWxSphLinkPolling({
1219 recordId: publishRecordId,
1220 accountId,
1221 result,
1222 })
1223 }
1224 }
1225 }
1226 catch (recordError) {
1227 console.error('创建发布记录失败:', recordError)
1228 }
1229 finally {
1230 set({ isCreatingRecord: false })
1231 }
1232 }
1233 catch (error) {
1234 // 发布失败
1235 const errorMessage = error instanceof Error ? error.message : '发布失败'
1236 const errorCode = getPluginErrorCode(error)
1237
1238 methods.updatePlatformTaskByRequestId(requestId, {
1239 status: PlatformTaskStatus.ERROR,
1240 error: errorMessage,
1241 result: {
1242 success: false,
1243 failReason: errorMessage,
1244 errorCode,
1245 },
1246 endTime: Date.now(),
1247 })
1248
1249 // 触发失败进度回调
1250 onProgress?.({
1251 stage: 'error',
1252 progress: 0,
1253 message: errorMessage,
1254 timestamp: Date.now(),
1255 accountId,
1256 platform,
1257 requestId,
1258 data: {
1259 code: errorCode,
1260 error: error instanceof Error ? error : new Error(errorMessage),
1261 },
1262 })
1263 }
1264 })
1265
1266 // 并行执行所有发布任务
1267 await Promise.all(publishPromises)
1268
1269 // 发布完成后的回调,传入发布记录ID
1270 onComplete?.(firstPublishRecordId)
1271 },
1272 }
1273
1274 return methods
1275 }),
1276 )
1277
1277 lines TYPESCRIPT