Files
geo/apps/admin-web/src/lib/publish-account-cards.ts
T
root 67be43319e
Desktop Client Build / Resolve Build Metadata (push) Successful in 1m13s
Desktop Client Build / Build Desktop Client (push) Has been cancelled
Desktop Client Build / Publish Client Artifacts to NAS (push) Has been cancelled
Backend CI / Backend (push) Failing after 13m49s
Frontend CI / Frontend (push) Successful in 6m38s
feat(desktop&tenant): Enhance desktop account and task consumer presence management
- Added new fields to DesktopAccountInfo for tracking account session and task consumer online status, along with queued publish task count and oldest queued publish task timestamp.
- Updated DesktopDispatchEvent to include server time for better synchronization.
- Implemented separate presence management for task consumers, including marking presence and loading presence state.
- Enhanced DesktopAccountService to apply publish queue summaries and manage task consumer presence.
- Introduced new methods for replaying queued publish tasks in DesktopDispatchHandler.
- Updated tests to cover new presence management logic and ensure correct behavior of account session and task consumer online status.
2026-06-07 19:15:20 +08:00

277 lines
8.8 KiB
TypeScript

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<string, string> = {
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<string, MediaPlatform> {
return new Map(
platforms.map((platform) => [normalizePublishPlatformId(platform.platform_id), platform]),
)
}
export function buildPublishAccountCards(
accounts: DesktopAccountInfo[],
platformMap: Map<string, MediaPlatform>,
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<string, MediaPlatform>,
): 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<PublishAccountCard, 'displayName' | 'platformShortName'>,
): 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 || '--'
}