import type { DesktopAccountInfo, MediaPlatform } from '@geo/shared-types' import { resolveApiURL } from '@/lib/api' import { resolveAccountCheckedAt, resolveAccountHealth } from '@/lib/desktop-account-runtime' import { getPublishPlatformMeta, isMediaPublishAccount, isPublishPlatformId, normalizePublishPlatformId, } from '@/lib/publish-platforms' export type PublishState = 'immediate' | 'queued' | 'unavailable' export interface PublishAccountCard { id: string platformId: string platformName: string platformShortName: string platformAccent: string platformLogoUrl: string | null displayName: string platformUid: string avatarUrl: string | null health: DesktopAccountInfo['health'] verifiedAt: string | null clientId: string | null clientOnline: boolean | null accountSessionOnline: boolean | null taskConsumerOnline: boolean | null clientDeviceName: string | null clientLastSeenAt: string | null queuedPublishTaskCount: number oldestQueuedPublishTaskAt: string | null publishState: PublishState selectable: boolean statusText: string } const platformLogoCatalog: Record = { toutiaohao: '/logos/logo_toutiao.png', baijiahao: '/logos/logo_baijiahao.png', sohuhao: '/logos/logo_souhu.png', qiehao: '/logos/logo_qiehao.png', zhihu: '/logos/logo_zhihu.png', wangyihao: '/logos/logo_wangyihao.png', jianshu: '/logos/logo_jianshu.svg', bilibili: '/logos/logo_bilibili.png', juejin: '/logos/logo_juejin.png', smzdm: '/logos/logo_smzdm.svg', weixin_gzh: '/logos/logo_weixin_gzh.svg', zol: '/logos/logo_zol.png', dongchedi: '/logos/logo_dongchedi.svg', } export function buildPublishPlatformMap(platforms: MediaPlatform[]): Map { return new Map( platforms.map((platform) => [normalizePublishPlatformId(platform.platform_id), platform]), ) } export function buildPublishAccountCards( accounts: DesktopAccountInfo[], platformMap: Map, preferredPlatformIds: string[] = [], ): PublishAccountCard[] { const preferredPlatforms = new Set(preferredPlatformIds.map(normalizePublishPlatformId)) return accounts .filter((account) => { if (!isMediaPublishAccount(account)) { return false } const platformId = normalizePublishPlatformId(account.platform) return platformMap.has(platformId) || isPublishPlatformId(platformId) }) .map((account) => buildPublishAccountCard(account, platformMap)) .sort((left, right) => { const preferredGap = Number(preferredPlatforms.has(right.platformId)) - Number(preferredPlatforms.has(left.platformId)) if (preferredGap !== 0) { return preferredGap } const publishGap = publishRank(left.publishState) - publishRank(right.publishState) if (publishGap !== 0) { return publishGap } const leftVerifiedAt = Date.parse(left.verifiedAt ?? '') || 0 const rightVerifiedAt = Date.parse(right.verifiedAt ?? '') || 0 return rightVerifiedAt - leftVerifiedAt }) } export function buildPublishAccountCard( account: DesktopAccountInfo, platformMap: Map, ): PublishAccountCard { const platformId = normalizePublishPlatformId(account.platform) const platform = platformMap.get(platformId) const fallback = getPublishPlatformMeta(platformId) const publishState = resolvePublishState(account) const health = resolveAccountHealth(account) return { id: account.id, platformId, platformName: platform?.name || fallback.name, platformShortName: platform?.short_name || fallback.shortName, platformAccent: platform?.accent_color || fallback.accent, platformLogoUrl: resolvePlatformLogoUrl(platformId, platform?.logo_url), displayName: account.display_name, platformUid: normalizePlatformUid(account.platform_uid), avatarUrl: resolveApiURL(account.avatar_url), health, verifiedAt: resolveAccountCheckedAt(account), clientId: account.client_id, clientOnline: account.client_online, accountSessionOnline: account.account_session_online ?? null, taskConsumerOnline: account.task_consumer_online ?? null, clientDeviceName: account.client_device_name, clientLastSeenAt: account.client_last_seen_at, queuedPublishTaskCount: account.queued_publish_task_count ?? 0, oldestQueuedPublishTaskAt: account.oldest_queued_publish_task_at ?? null, publishState, selectable: publishState !== 'unavailable', statusText: resolvePublishStatusText(account, publishState), } } export function resolvePublishState(account: DesktopAccountInfo): PublishState { if (resolveAccountHealth(account) !== 'live') { return 'unavailable' } if (!account.client_id) { return 'unavailable' } return account.task_consumer_online === true ? 'immediate' : 'queued' } export function publishRank(state: PublishState): number { switch (state) { case 'immediate': return 0 case 'queued': return 1 default: return 2 } } export function publishStateLabel(state: PublishState): string { switch (state) { case 'immediate': return '立即发布' case 'queued': return '离线排队' default: return '不可发布' } } export function publishStatusShortLabel(state: PublishState): string { switch (state) { case 'immediate': return '客户端在线' case 'queued': return '离线可排队' default: return '不可发布' } } export function healthLabel(health: DesktopAccountInfo['health']): string { switch (health) { case 'live': return '授权正常' case 'captcha': return '待处理' case 'risk': return '风险观察' default: return '授权异常' } } export function healthColor(health: DesktopAccountInfo['health']): string { switch (health) { case 'live': return 'green' case 'captcha': return 'orange' case 'risk': return 'gold' default: return 'red' } } export function clientStatusLabel(card: PublishAccountCard): string { if (!card.clientId) { return '未绑定客户端' } const state = card.clientOnline ? '心跳在线' : '心跳离线' const consumerState = card.taskConsumerOnline ? '任务通道在线' : '任务通道离线' const deviceName = card.clientDeviceName?.trim() return deviceName ? `${state} · ${consumerState} · ${deviceName}` : `${state} · ${consumerState}` } export function clientStatusTooltip(card: PublishAccountCard): string { if (!card.clientId) { return '未绑定桌面客户端' } const state = card.clientOnline ? '心跳在线' : '心跳离线' const consumerState = card.taskConsumerOnline ? '任务通道在线' : '任务通道离线' const deviceName = card.clientDeviceName?.trim() return deviceName ? `${state} · ${consumerState} · ${deviceName}` : `${state} · ${consumerState}` } export function clientStatusColor(card: PublishAccountCard): string { if (!card.clientId) { return 'default' } if (card.taskConsumerOnline) { return 'green' } return card.clientOnline ? 'orange' : 'gold' } export function accountInitial( card: Pick, ): string { const trimmed = card.displayName.trim() return trimmed ? trimmed.slice(0, 1).toUpperCase() : card.platformShortName } export function resolvePlatformLogoUrl( platformId: string, remoteLogoUrl?: string | null, ): string | null { const localLogoUrl = platformLogoCatalog[platformId] if (localLogoUrl) { return localLogoUrl } const resolvedRemoteLogoUrl = resolveApiURL(remoteLogoUrl) return resolvedRemoteLogoUrl || null } function resolvePublishStatusText(account: DesktopAccountInfo, publishState: PublishState): string { if (publishState === 'immediate') { return '任务消费通道在线,发布任务会立即被桌面端领取执行。' } if (publishState === 'queued') { const count = account.queued_publish_task_count ?? 0 const queuePrefix = count > 0 ? `当前已有 ${count} 个发布任务排队。` : '' if (account.client_online === true) { return `${queuePrefix}客户端心跳在线,但任务消费通道未就绪;任务会落库排队,通道恢复后自动继续发布。` } return `${queuePrefix}客户端当前离线,任务会先落库排队,等桌面端上线后自动继续发布。` } if (resolveAccountHealth(account) !== 'live') { return '该账号授权状态异常,请先在桌面端重新校验或重新授权。' } return '该账号还没有绑定桌面客户端,当前不能进入发布队列。' } function normalizePlatformUid(value?: string | null): string { let normalized = String(value ?? '').trim() while (normalized.toLowerCase().startsWith('platform:')) { normalized = normalized.slice('platform:'.length).trim() } return normalized || '--' }