feat(desktop&tenant): Enhance desktop account and task consumer presence management
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
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.
This commit is contained in:
@@ -25,8 +25,12 @@ export interface PublishAccountCard {
|
||||
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
|
||||
@@ -113,8 +117,12 @@ export function buildPublishAccountCard(
|
||||
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),
|
||||
@@ -128,7 +136,7 @@ export function resolvePublishState(account: DesktopAccountInfo): PublishState {
|
||||
if (!account.client_id) {
|
||||
return 'unavailable'
|
||||
}
|
||||
return account.client_online === true ? 'immediate' : 'queued'
|
||||
return account.task_consumer_online === true ? 'immediate' : 'queued'
|
||||
}
|
||||
|
||||
export function publishRank(state: PublishState): number {
|
||||
@@ -195,9 +203,10 @@ export function clientStatusLabel(card: PublishAccountCard): string {
|
||||
return '未绑定客户端'
|
||||
}
|
||||
|
||||
const state = card.clientOnline ? '在线' : '离线'
|
||||
const state = card.clientOnline ? '心跳在线' : '心跳离线'
|
||||
const consumerState = card.taskConsumerOnline ? '任务通道在线' : '任务通道离线'
|
||||
const deviceName = card.clientDeviceName?.trim()
|
||||
return deviceName ? `${state} · ${deviceName}` : `客户端${state}`
|
||||
return deviceName ? `${state} · ${consumerState} · ${deviceName}` : `${state} · ${consumerState}`
|
||||
}
|
||||
|
||||
export function clientStatusTooltip(card: PublishAccountCard): string {
|
||||
@@ -205,16 +214,20 @@ export function clientStatusTooltip(card: PublishAccountCard): string {
|
||||
return '未绑定桌面客户端'
|
||||
}
|
||||
|
||||
const state = card.clientOnline ? '在线' : '离线'
|
||||
const state = card.clientOnline ? '心跳在线' : '心跳离线'
|
||||
const consumerState = card.taskConsumerOnline ? '任务通道在线' : '任务通道离线'
|
||||
const deviceName = card.clientDeviceName?.trim()
|
||||
return deviceName ? `${state} · ${deviceName}` : `客户端${state}`
|
||||
return deviceName ? `${state} · ${consumerState} · ${deviceName}` : `${state} · ${consumerState}`
|
||||
}
|
||||
|
||||
export function clientStatusColor(card: PublishAccountCard): string {
|
||||
if (!card.clientId) {
|
||||
return 'default'
|
||||
}
|
||||
return card.clientOnline ? 'green' : 'gold'
|
||||
if (card.taskConsumerOnline) {
|
||||
return 'green'
|
||||
}
|
||||
return card.clientOnline ? 'orange' : 'gold'
|
||||
}
|
||||
|
||||
export function accountInitial(
|
||||
@@ -238,10 +251,15 @@ export function resolvePlatformLogoUrl(
|
||||
|
||||
function resolvePublishStatusText(account: DesktopAccountInfo, publishState: PublishState): string {
|
||||
if (publishState === 'immediate') {
|
||||
return '客户端在线,RabbitMQ 会立即唤醒桌面端开始发布。'
|
||||
return '任务消费通道在线,发布任务会立即被桌面端领取执行。'
|
||||
}
|
||||
if (publishState === 'queued') {
|
||||
return '客户端当前离线,任务会先落库排队,等桌面端上线后自动继续发布。'
|
||||
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 '该账号授权状态异常,请先在桌面端重新校验或重新授权。'
|
||||
|
||||
@@ -60,8 +60,12 @@ interface MediaAccountCard {
|
||||
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
|
||||
publishHint: string
|
||||
deleteRequestedAt: string | null
|
||||
@@ -251,8 +255,12 @@ const mediaAccounts = computed<MediaAccountCard[]>(() => {
|
||||
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,
|
||||
publishHint: resolvePublishHint(account, publishState),
|
||||
deleteRequestedAt: account.delete_requested_at,
|
||||
@@ -322,7 +330,7 @@ function resolvePublishState(account: DesktopAccountInfo): PublishState {
|
||||
if (!account.client_id) {
|
||||
return 'unavailable'
|
||||
}
|
||||
return account.client_online === true ? 'immediate' : 'queued'
|
||||
return account.task_consumer_online === true ? 'immediate' : 'queued'
|
||||
}
|
||||
|
||||
function resolvePublishHint(account: DesktopAccountInfo, publishState: PublishState): string {
|
||||
@@ -330,7 +338,12 @@ function resolvePublishHint(account: DesktopAccountInfo, publishState: PublishSt
|
||||
return ''
|
||||
}
|
||||
if (publishState === 'queued') {
|
||||
return '客户端当前离线,任务会先进入发布队列,等桌面端上线后自动继续。'
|
||||
const count = account.queued_publish_task_count ?? 0
|
||||
const prefix = count > 0 ? `当前已有 ${count} 个发布任务排队。` : ''
|
||||
if (account.client_online === true) {
|
||||
return `${prefix}客户端心跳在线,但任务消费通道未就绪;任务会进入发布队列,通道恢复后自动继续。`
|
||||
}
|
||||
return `${prefix}客户端当前离线,任务会先进入发布队列,等桌面端上线后自动继续。`
|
||||
}
|
||||
if (resolveAccountHealth(account) !== 'live') {
|
||||
return '授权状态异常,先在桌面端重新校验后再发布。'
|
||||
@@ -402,14 +415,20 @@ function clientStatusLabel(account: MediaAccountCard): string {
|
||||
if (!account.clientId) {
|
||||
return '未绑定'
|
||||
}
|
||||
return account.clientOnline ? '在线' : '离线'
|
||||
if (account.taskConsumerOnline) {
|
||||
return '任务通道在线'
|
||||
}
|
||||
return account.clientOnline ? '心跳在线,任务通道离线' : '离线'
|
||||
}
|
||||
|
||||
function clientStatusColor(account: MediaAccountCard): string {
|
||||
if (!account.clientId) {
|
||||
return 'default'
|
||||
}
|
||||
return account.clientOnline ? 'green' : 'gold'
|
||||
if (account.taskConsumerOnline) {
|
||||
return 'green'
|
||||
}
|
||||
return account.clientOnline ? 'orange' : 'gold'
|
||||
}
|
||||
|
||||
function clientStatusText(account: MediaAccountCard): string {
|
||||
@@ -426,7 +445,7 @@ function publishStateLabel(state: PublishState): string {
|
||||
case 'immediate':
|
||||
return '可立即发布'
|
||||
case 'queued':
|
||||
return '离线排队'
|
||||
return '排队等待'
|
||||
default:
|
||||
return '不可发布'
|
||||
}
|
||||
@@ -715,7 +734,7 @@ function releaseSize(value?: number | null): string {
|
||||
</svg>
|
||||
</div>
|
||||
<div class="chip-copy">
|
||||
<span>离线排队</span>
|
||||
<span>排队等待</span>
|
||||
<strong>{{ overview.queued }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -149,7 +149,8 @@ type MonitorExecutionPhase =
|
||||
|
||||
const heartbeatIntervalMs = 15_000
|
||||
const pullIntervalMs = 60_000
|
||||
const pumpWatchdogIntervalMs = 15_000
|
||||
const publishPullIntervalMs = 30_000
|
||||
const pumpWatchdogIntervalMs = 30_000
|
||||
const leaseExtendIntervalMs = 60_000
|
||||
const defaultPublishTaskTimeoutMs = 5 * 60_000
|
||||
const publishTaskStaleProgressMs = 2 * 60_000
|
||||
@@ -816,11 +817,11 @@ function schedulePullLoop(): void {
|
||||
clearInterval(state.pullTimer)
|
||||
}
|
||||
|
||||
setSchedulerNextPull(Date.now() + pullIntervalMs)
|
||||
setSchedulerNextPull(Date.now() + publishPullIntervalMs)
|
||||
state.pullTimer = setInterval(() => {
|
||||
setSchedulerNextPull(Date.now() + pullIntervalMs)
|
||||
setSchedulerNextPull(Date.now() + publishPullIntervalMs)
|
||||
pumpExecutionLoop()
|
||||
}, pullIntervalMs)
|
||||
}, publishPullIntervalMs)
|
||||
}
|
||||
|
||||
function schedulePumpWatchdogLoop(): void {
|
||||
@@ -859,6 +860,7 @@ function connectDispatchWs(): void {
|
||||
setTransportDispatchState('streaming')
|
||||
setSchedulerError(null)
|
||||
recordActivity('success', '任务分发通道已连接', '统一 WebSocket 分发通道已就绪。')
|
||||
pumpExecutionLoop()
|
||||
void pullNextTask('monitor')
|
||||
if (hasConnectedOnce) {
|
||||
void resumeLeasedMonitoringTasks('reconnect')
|
||||
@@ -872,6 +874,7 @@ function connectDispatchWs(): void {
|
||||
if (isConnectedEvent(payload)) {
|
||||
state.lastServerTime = payload.server_time
|
||||
}
|
||||
pumpExecutionLoop()
|
||||
})
|
||||
|
||||
const dispatchTaskHandler = (payload: unknown) => {
|
||||
@@ -1162,6 +1165,7 @@ async function sendHeartbeat(source: 'startup' | 'timer'): Promise<void> {
|
||||
if (source === 'startup' || previousStatus === 'failed') {
|
||||
recordActivity('success', '心跳连接正常', '客户端心跳已回写到服务端。')
|
||||
}
|
||||
pumpExecutionLoop()
|
||||
} catch (error) {
|
||||
state.lastHeartbeatAt = Date.now()
|
||||
state.lastHeartbeatStatus = 'failed'
|
||||
@@ -1911,7 +1915,7 @@ async function pullNextTask(kind: 'publish' | 'monitor'): Promise<void> {
|
||||
noteSchedulerPull()
|
||||
|
||||
if (!leased.task) {
|
||||
state.publishFallbackBackoffUntil = Date.now() + pullIntervalMs
|
||||
state.publishFallbackBackoffUntil = Date.now() + publishPullIntervalMs
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1923,7 +1927,7 @@ async function pullNextTask(kind: 'publish' | 'monitor'): Promise<void> {
|
||||
state.lastPullAt = Date.now()
|
||||
state.lastPullStatus = 'failed'
|
||||
state.lastError = errorMessage(error)
|
||||
state.publishFallbackBackoffUntil = Date.now() + pullIntervalMs
|
||||
state.publishFallbackBackoffUntil = Date.now() + publishPullIntervalMs
|
||||
|
||||
noteTransportPull(false)
|
||||
setSchedulerError(state.lastError)
|
||||
@@ -3505,7 +3509,6 @@ function canStartAnotherPublishExecution(): boolean {
|
||||
|
||||
function shouldPullPublishFallback(): boolean {
|
||||
return (
|
||||
!state.dispatchWsConnected &&
|
||||
Date.now() >= state.publishFallbackBackoffUntil &&
|
||||
!isLeaseInFlight('publish') &&
|
||||
!hasPublishBacklog() &&
|
||||
|
||||
Reference in New Issue
Block a user