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
- 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.
277 lines
8.8 KiB
TypeScript
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 || '--'
|
|
}
|