From df467b63b4a380ba29bd0ec668391f85e97ddfd8 Mon Sep 17 00:00:00 2001 From: liangxu Date: Wed, 6 May 2026 21:34:13 +0800 Subject: [PATCH] feat(desktop): refresh publish tasks instantly on lease activation Emit a 'publish-task-lease' runtime invalidation event whenever the scheduler activates a publish task, and have PublishManagementView listen for it to trigger an immediate refresh. The view also switches to a 5s active-task poll only while a task is in_progress, replacing the unconditional 20s timer. --- .../src/main/runtime-controller.ts | 3 +- .../desktop-client/src/main/runtime-events.ts | 5 +- apps/desktop-client/src/preload/bridge.ts | 14 +++- apps/desktop-client/src/renderer/env.d.ts | 7 +- .../renderer/views/PublishManagementView.vue | 66 +++++++++++++++++-- 5 files changed, 82 insertions(+), 13 deletions(-) diff --git a/apps/desktop-client/src/main/runtime-controller.ts b/apps/desktop-client/src/main/runtime-controller.ts index 88d4236..1362221 100644 --- a/apps/desktop-client/src/main/runtime-controller.ts +++ b/apps/desktop-client/src/main/runtime-controller.ts @@ -84,7 +84,7 @@ import { notePublishTaskLeaseMiss, selectNextPublishTask, } from './publish-scheduler' -import { onRuntimeInvalidated } from './runtime-events' +import { emitRuntimeInvalidated, onRuntimeInvalidated } from './runtime-events' import { initScheduler, noteSchedulerAccountsSync, @@ -2426,6 +2426,7 @@ async function executeLeasedTask( noteMonitorTaskLeased(task, routing) } else { notePublishTaskActivated(taskRecord.id, taskRecord.platform) + emitRuntimeInvalidated('publish-task-lease', { taskId: taskRecord.id }) } syncSchedulerSurface() queueMicrotask(() => pumpExecutionLoop()) diff --git a/apps/desktop-client/src/main/runtime-events.ts b/apps/desktop-client/src/main/runtime-events.ts index 91d06e7..75c81b9 100644 --- a/apps/desktop-client/src/main/runtime-events.ts +++ b/apps/desktop-client/src/main/runtime-events.ts @@ -1,16 +1,17 @@ -type RuntimeInvalidationReason = 'account-health' +type RuntimeInvalidationReason = 'account-health' | 'publish-task-lease' type RuntimeInvalidationListener = (event: { reason: RuntimeInvalidationReason at: number accountId?: string + taskId?: string }) => void const listeners = new Set() export function emitRuntimeInvalidated( reason: RuntimeInvalidationReason, - details: { accountId?: string } = {}, + details: { accountId?: string; taskId?: string } = {}, ): void { const event = { reason, diff --git a/apps/desktop-client/src/preload/bridge.ts b/apps/desktop-client/src/preload/bridge.ts index f214474..2065c09 100644 --- a/apps/desktop-client/src/preload/bridge.ts +++ b/apps/desktop-client/src/preload/bridge.ts @@ -145,11 +145,21 @@ const desktopBridge = { setWindowMode: (mode: 'login' | 'main', request?: WindowModeRequest) => ipcRenderer.invoke('desktop:set-window-mode', mode, request) as Promise, onRuntimeInvalidated: ( - listener: (event: { reason: 'account-health'; at: number; accountId?: string }) => void, + listener: (event: { + reason: 'account-health' | 'publish-task-lease' + at: number + accountId?: string + taskId?: string + }) => void, ) => { const wrapped = ( _event: IpcRendererEvent, - payload: { reason: 'account-health'; at: number; accountId?: string }, + payload: { + reason: 'account-health' | 'publish-task-lease' + at: number + accountId?: string + taskId?: string + }, ) => { listener(payload) } diff --git a/apps/desktop-client/src/renderer/env.d.ts b/apps/desktop-client/src/renderer/env.d.ts index 25e87f3..3048ad8 100644 --- a/apps/desktop-client/src/renderer/env.d.ts +++ b/apps/desktop-client/src/renderer/env.d.ts @@ -64,7 +64,12 @@ declare global { }, ): Promise onRuntimeInvalidated( - listener: (event: { reason: 'account-health'; at: number; accountId?: string }) => void, + listener: (event: { + reason: 'account-health' | 'publish-task-lease' + at: number + accountId?: string + taskId?: string + }) => void, ): () => void onNetworkObserved(listener: (event: DesktopObservedRequest) => void): () => void } diff --git a/apps/desktop-client/src/renderer/views/PublishManagementView.vue b/apps/desktop-client/src/renderer/views/PublishManagementView.vue index 17df328..2d01847 100644 --- a/apps/desktop-client/src/renderer/views/PublishManagementView.vue +++ b/apps/desktop-client/src/renderer/views/PublishManagementView.vue @@ -26,6 +26,7 @@ import { const PAGE_SIZE = 10 const INLINE_ERROR_HEAD_CHARS = 48 const INLINE_ERROR_TAIL_CHARS = 10 +const ACTIVE_TASK_POLL_INTERVAL_MS = 5_000 const publishPlatformIndex: Record = Object.fromEntries(desktopPublishMediaCatalog.map((item) => [item.id, item])) @@ -52,6 +53,7 @@ interface PublishTaskItem { } let refreshTimer: ReturnType | null = null +let runtimeInvalidationUnsubscribe: (() => void) | null = null function platformMeta(platform: string) { const key = (platform ?? '').toLowerCase() @@ -104,6 +106,12 @@ function isPendingStatus(status: DesktopTaskInfo['status']): boolean { return status === 'queued' || status === 'in_progress' } +function hasActivePublishingTask(page: DesktopPublishTaskListResponse | null): boolean { + return Boolean( + page?.items.some((task) => normalizePublishTaskStatus(task.status) === 'in_progress'), + ) +} + function statusTone(status: DesktopTaskInfo['status']): 'info' | 'warn' | 'success' | 'danger' { switch (status) { case 'queued': @@ -359,13 +367,61 @@ async function refreshTasks(page = currentPage.value) { taskPage.value = response currentPage.value = response.page + syncActiveTaskPolling(response) } catch (err) { error.value = normalizeTaskErrorMessage(unwrapClientErrorMessage(err, '发布任务暂时不可用')) + if (!hasActivePublishingTask(taskPage.value)) { + stopActiveTaskPolling() + } } finally { loading.value = false } } +function startActiveTaskPolling() { + if (refreshTimer) { + return + } + refreshTimer = setInterval(() => { + void refreshTasks() + }, ACTIVE_TASK_POLL_INTERVAL_MS) +} + +function stopActiveTaskPolling() { + if (!refreshTimer) { + return + } + clearInterval(refreshTimer) + refreshTimer = null +} + +function syncActiveTaskPolling(page = taskPage.value) { + if (hasActivePublishingTask(page)) { + startActiveTaskPolling() + return + } + stopActiveTaskPolling() +} + +function bindPublishLeaseListener() { + if (runtimeInvalidationUnsubscribe || !window.desktopBridge?.app?.onRuntimeInvalidated) { + return + } + + runtimeInvalidationUnsubscribe = window.desktopBridge.app.onRuntimeInvalidated((event) => { + if (event.reason !== 'publish-task-lease') { + return + } + + void refreshTasks(1) + }) +} + +function unbindPublishLeaseListener() { + runtimeInvalidationUnsubscribe?.() + runtimeInvalidationUnsubscribe = null +} + async function retryTask(taskId: string) { actionPendingTaskId.value = taskId @@ -480,17 +536,13 @@ watch(searchTitle, (value) => { }) onMounted(() => { + bindPublishLeaseListener() void refreshTasks() - refreshTimer = setInterval(() => { - void refreshTasks() - }, 20_000) }) onUnmounted(() => { - if (refreshTimer) { - clearInterval(refreshTimer) - refreshTimer = null - } + unbindPublishLeaseListener() + stopActiveTaskPolling() }) const accountNameMap = computed(() => {