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 clientDeviceName: string | null clientLastSeenAt: 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, clientDeviceName: account.client_device_name, clientLastSeenAt: account.client_last_seen_at, 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.client_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 '未绑定客户端' } return card.clientOnline ? '客户端在线' : '客户端离线' } export function clientStatusColor(card: PublishAccountCard): string { if (!card.clientId) { return 'default' } return card.clientOnline ? 'green' : '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 '客户端在线,RabbitMQ 会立即唤醒桌面端开始发布。' } if (publishState === 'queued') { return '客户端当前离线,任务会先落库排队,等桌面端上线后自动继续发布。' } 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 || '--' }