Files
geo/apps/desktop-client/src/main/runtime-controller.ts
T
root 8ea4a2ffff
Desktop Client Build / Resolve Build Metadata (push) Successful in 20s
Frontend CI / Frontend (push) Successful in 2m57s
Desktop Client Build / Build Desktop Client (push) Successful in 25m11s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 34s
Backend CI / Backend (push) Successful in 50m56s
fix(publish): require submit marker before platform write
2026-05-30 22:29:28 +08:00

4066 lines
125 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { cpus, hostname, totalmem } from 'node:os'
import { ApiClientError } from '@geo/http-client'
import type {
DesktopAccountHealthReport,
DesktopAccountInfo,
DesktopArticleContent,
DesktopClientInfo,
DesktopRuntimeSessionSyncRequest,
DesktopTaskEventMessage,
DesktopTaskInfo,
JsonValue,
LeaseDesktopTaskResponse,
MonitoringLeaseTask,
MonitoringSourceItem,
MonitoringTaskResultPayload,
} from '@geo/shared-types'
import { aiPlatformCatalog, getAIPlatformCatalogItem, isAIPlatformId } from '@geo/shared-types'
import {
buildAccountIdentityKey,
normalizeAccountPlatformUid,
sameAccountPlatformUid,
} from '../shared/account-identity'
import { normalizeDesktopApiBaseURL } from '../shared/api-base-url'
import { type PublishAccountIdentity, type PublishAccountProfile } from './account-binder'
import {
ensureAccountReady,
forgetTrackedAccountHealth,
getAccountHealthSnapshot,
getProjectedAccountHealth,
markTrackedAccountBound,
markTrackedAccountMissingPartition,
probeTrackedAccount,
reportAccountFailure,
syncTrackedAccounts,
} from './account-health'
import {
baijiahaoAdapter,
bilibiliAdapter,
dongchediAdapter,
getMonitorAdapter,
jianshuAdapter,
juejinAdapter,
qiehaoAdapter,
smzdmAdapter,
sohuhaoAdapter,
toutiaoAdapter,
wangyihaoAdapter,
weixinGzhAdapter,
zhihuAdapter,
zolAdapter,
type AdapterExecutionResult,
type MonitorAdapter,
type PublishAdapter,
} from './adapters'
import { getDesktopAppVersion, getDesktopReleaseChannel } from './app-version'
import {
getLeaseManagerSnapshot,
noteLeaseExtended,
noteLeaseReleased,
setActiveLease,
} from './lease-manager'
import {
enqueueMonitorLeaseTask,
enqueueMonitorTaskFromEvent,
getMonitorSchedulerSnapshot,
initMonitorScheduler,
noteMonitorTaskActivated,
noteMonitorTaskCanceled,
noteMonitorTaskCompleted,
noteMonitorTaskLeaseMiss,
noteMonitorTaskLeased,
selectNextMonitorTask,
} from './monitor-scheduler'
import { retainHiddenPlaywrightPage, startHiddenPlaywrightReaper } from './playwright-cdp'
import { getProcessMetricsSnapshot } from './process-metrics'
import {
enqueuePublishTask as enqueuePublishSchedulerTask,
getPublishSchedulerSnapshot,
initPublishScheduler,
notePublishTaskActivated,
notePublishTaskCanceled,
notePublishTaskCompleted,
notePublishTaskLeaseMiss,
selectNextPublishTask,
} from './publish-scheduler'
import { emitRuntimeInvalidated, onRuntimeInvalidated } from './runtime-events'
import {
initScheduler,
noteSchedulerAccountsSync,
noteSchedulerHeartbeat,
noteSchedulerPull,
noteSchedulerSseEvent,
noteSchedulerTaskFinished,
setSchedulerCurrentTask,
setSchedulerError,
setSchedulerNextHeartbeat,
setSchedulerNextPull,
setSchedulerPhase,
setSchedulerQueueDepth,
} from './scheduler'
import {
clearSessionHandle,
createSessionHandle,
getPersistedPartition,
getSessionHandle,
} from './session-registry'
import {
cancelDesktopTask,
cancelMonitoringTask,
completeDesktopTask,
configureTransport,
deleteDesktopAccount,
extendDesktopTask,
getDesktopArticleContent,
heartbeatDesktopClient,
leaseDesktopTask,
leaseMonitoringTasks,
listDesktopAccounts,
markDesktopTaskPublishSubmitStarted,
noteTransportHeartbeat,
noteTransportPull,
offlineDesktopClient,
reportDesktopAccountHealth,
resumeMonitoringTasks,
revokeDesktopClient,
setTransportAuthState,
setTransportDispatchState,
skipMonitoringTask,
submitMonitoringTaskResult,
upsertDesktopAccount,
} from './transport/api-client'
import { DispatchWsClient } from './transport/dispatch-ws-client'
import { releaseHotView, retainHotView } from './view-pool'
type RuntimeTone = 'info' | 'success' | 'warn' | 'danger'
type RuntimeTaskStatus = 'queued' | 'in_progress' | 'succeeded' | 'failed' | 'unknown' | 'aborted'
type RuntimeTaskRouting = 'rabbitmq-primary' | 'db-recovery'
type MonitorExecutionPhase =
| 'PREPARING'
| 'NAVIGATING'
| 'AUTHENTICATING'
| 'INPUTTING'
| 'SUBMITTING'
| 'WAITING'
| 'PARSING'
| 'POSTING'
const heartbeatIntervalMs = 15_000
const pullIntervalMs = 60_000
const pumpWatchdogIntervalMs = 15_000
const leaseExtendIntervalMs = 60_000
const defaultPublishTaskTimeoutMs = 5 * 60_000
const publishTaskStaleProgressMs = 2 * 60_000
const publishLongRunningNotificationMs = 15 * 60_000
const maxActivityItems = 60
const monitorBusinessTimeZone = 'Asia/Shanghai'
const authRequiredMonitorPlatforms = new Set(aiPlatformCatalog.map((platform) => platform.id))
const publishReservedSlots = 1
const minimumForegroundTotalConcurrency = 2
const maximumRuntimeTotalConcurrency = 4
const BAIJIAHAO_RISK_CONTROL_PROMPT = '您触发平台风控,请手动发稿一篇并完成验证,方可继续使用'
const accountHealthReportDebounceMs = 1_000
const accountHealthReportMaxBatchSize = 100
interface RuntimeTaskRecord {
id: string
jobId: string
title: string
kind: 'publish' | 'monitor'
platform: string
accountId: string
accountName: string
clientId: string
status: RuntimeTaskStatus
routing: RuntimeTaskRouting
leaseExpiresAt: number | null
startedAt: number | null
updatedAt: number
summary: string
payload: Record<string, JsonValue>
error: Record<string, JsonValue> | null
result: Record<string, JsonValue> | null
attemptId: string | null
leaseToken: string | null
}
export interface RuntimeControllerActivity {
id: string
severity: RuntimeTone
title: string
detail: string
at: number
}
export interface RuntimeControllerSnapshot {
session: DesktopRuntimeSessionSyncRequest | null
client: DesktopClientInfo | null
running: boolean
dispatchConnected: boolean
queueDepth: number
accounts: DesktopAccountInfo[]
accountProfiles: Record<string, PublishAccountProfile>
tasks: Array<Omit<RuntimeTaskRecord, 'leaseToken'>>
activity: RuntimeControllerActivity[]
lastHeartbeatAt: number
lastHeartbeatStatus: 'idle' | 'success' | 'failed'
lastPullAt: number
lastPullStatus: 'idle' | 'success' | 'failed'
lastAccountsSyncAt: number
lastServerTime: string | null
onlineClientCount: number | null
lastError: string | null
}
interface PendingTaskRequest {
taskId: string
routing: RuntimeTaskRouting
platform?: string
source?: 'desktop_task' | 'monitoring_lease'
}
interface ActiveRuntimeExecution {
taskId: string
kind: 'publish' | 'monitor'
platform: string
accountId: string
abortController: AbortController
executionPhase: MonitorExecutionPhase | null
interruptRequested: boolean
interruptGeneration: number
interruptReason: string | null
lastSafePointAt: number | null
startedAt: number
lastProgressAt: number
lastProgressSummary: string | null
deadlineAt: number | null
deadlineHandle: ReturnType<typeof setTimeout> | null
longRunningNotifiedAt: number | null
// Set the moment a publish adapter enters its irreversible submit/save_draft stage. Once set,
// an interrupted publish is completed as 'unknown' (manual reconcile) instead of 'failed', so a
// possibly-already-published article is never silently re-posted by a retry.
submitStartedAt: number | null
}
interface RuntimeState {
session: DesktopRuntimeSessionSyncRequest | null
client: DesktopClientInfo | null
running: boolean
tasks: Map<string, RuntimeTaskRecord>
accounts: DesktopAccountInfo[]
accountProfiles: Map<string, PublishAccountProfile>
activity: RuntimeControllerActivity[]
activeExecutions: Map<string, ActiveRuntimeExecution>
leaseInFlightKinds: Set<'publish' | 'monitor'>
publishFallbackBackoffUntil: number
heartbeatTimer: ReturnType<typeof setInterval> | null
pullTimer: ReturnType<typeof setInterval> | null
pumpWatchdogTimer: ReturnType<typeof setInterval> | null
accountHealthReportTimer: ReturnType<typeof setTimeout> | null
accountHealthReportQueue: Map<string, DesktopAccountHealthReport>
accountHealthReportBadAccounts: Set<string>
accountRemoteHealth: Map<string, DesktopAccountInfo['health']>
dispatchWsClient: DispatchWsClient | null
dispatchWsConnected: boolean
lastHeartbeatAt: number
lastHeartbeatStatus: 'idle' | 'success' | 'failed'
lastPullAt: number
lastPullStatus: 'idle' | 'success' | 'failed'
lastAccountsSyncAt: number
lastServerTime: string | null
onlineClientCount: number | null
lastError: string | null
activitySeq: number
}
const state: RuntimeState = {
session: null,
client: null,
running: false,
tasks: new Map<string, RuntimeTaskRecord>(),
accounts: [],
accountProfiles: new Map<string, PublishAccountProfile>(),
activity: [],
activeExecutions: new Map<string, ActiveRuntimeExecution>(),
leaseInFlightKinds: new Set<'publish' | 'monitor'>(),
publishFallbackBackoffUntil: 0,
heartbeatTimer: null,
pullTimer: null,
pumpWatchdogTimer: null,
accountHealthReportTimer: null,
accountHealthReportQueue: new Map<string, DesktopAccountHealthReport>(),
accountHealthReportBadAccounts: new Set<string>(),
accountRemoteHealth: new Map<string, DesktopAccountInfo['health']>(),
dispatchWsClient: null,
dispatchWsConnected: false,
lastHeartbeatAt: 0,
lastHeartbeatStatus: 'idle',
lastPullAt: 0,
lastPullStatus: 'idle',
lastAccountsSyncAt: 0,
lastServerTime: null,
onlineClientCount: null,
lastError: null,
activitySeq: 0,
}
let accountHealthReportBridgeUnsubscribe: (() => void) | null = null
function accountIdentityFromDesktopAccount(account: DesktopAccountInfo): PublishAccountIdentity {
return {
id: account.id,
platform: account.platform,
platformUid: normalizeAccountPlatformUid(account.platform_uid) || account.platform_uid,
displayName: account.display_name,
}
}
function accountIdentityFromTask(task: RuntimeTaskRecord): PublishAccountIdentity | null {
if (!task.accountId) {
return null
}
const matched = state.accounts.find((account) => account.id === task.accountId) ?? null
if (matched) {
return accountIdentityFromDesktopAccount(matched)
}
return {
id: task.accountId,
platform: task.platform,
platformUid: '',
displayName: task.accountName || '待同步账号',
}
}
function isDefinitiveAuthState(authState: string): boolean {
return ['active', 'expired', 'challenge_required', 'revoked'].includes(authState)
}
function hasLocalAccountSession(accountId: string): boolean {
return Boolean(getSessionHandle(accountId) || getPersistedPartition(accountId))
}
function isAccountOwnedByCurrentClient(account: DesktopAccountInfo): boolean {
return Boolean(account.client_id && account.client_id === state.client?.id)
}
function hasLocalAccountAuthorization(account: DesktopAccountInfo): boolean {
return isAccountOwnedByCurrentClient(account) && hasLocalAccountSession(account.id)
}
function accountRequiresLocalAuthorization(account: DesktopAccountInfo): boolean {
return Boolean(account.client_id)
}
function shouldReportAccountPresence(account: DesktopAccountInfo): boolean {
return hasLocalAccountSession(account.id) && isAccountOwnedByCurrentClient(account)
}
function buildRuntimeAccountDedupeKey(account: DesktopAccountInfo): string {
if (isAIPlatformId(account.platform)) {
return `ai-platform:${account.platform}`
}
return buildAccountIdentityKey({
platform: account.platform,
platformUid: account.platform_uid,
displayName: account.display_name,
})
}
function accountHealthRank(health: DesktopAccountInfo['health']): number {
switch (health) {
case 'live':
return 4
case 'risk':
return 3
case 'captcha':
return 2
case 'expired':
return 1
default:
return 0
}
}
function accountTimestamp(value: string | null | undefined): number {
return parseTimestamp(value) ?? 0
}
function shouldPreferRuntimeAccount(
candidate: DesktopAccountInfo,
current: DesktopAccountInfo,
): boolean {
const candidateHealthRank = accountHealthRank(candidate.health)
const currentHealthRank = accountHealthRank(current.health)
if (candidateHealthRank !== currentHealthRank) {
return candidateHealthRank > currentHealthRank
}
const candidateVerifiedAt = accountTimestamp(candidate.verified_at)
const currentVerifiedAt = accountTimestamp(current.verified_at)
if (candidateVerifiedAt !== currentVerifiedAt) {
return candidateVerifiedAt > currentVerifiedAt
}
return candidate.sync_version > current.sync_version
}
function runtimePlatformLabel(platform: string): string {
return getAIPlatformCatalogItem(platform)?.label ?? platform
}
function accountActionRequiredSummary(
platform: string,
authReason: string | null | undefined,
): string {
const label = runtimePlatformLabel(platform)
if (platform === 'baijiahao') {
return BAIJIAHAO_RISK_CONTROL_PROMPT
}
if (authReason === 'risk_control') {
return `${label} 账号疑似触发风控,请打开平台处理验证或等待限制解除后再继续执行。`
}
return `${label} 账号需要完成人机验证后才能继续执行。`
}
function markAuthorizationFailureNonRetryable(
error: Record<string, JsonValue>,
category: 'ai_platform_authorization' | 'media_account_authorization',
): Record<string, JsonValue> {
return {
...error,
retryable: false,
non_retryable: true,
failure_category: category,
}
}
function isAuthorizationFailureCode(code: string | null | undefined): boolean {
if (!code) {
return false
}
const normalized = code.trim().toLowerCase()
return (
normalized === 'desktop_account_auth_expired' ||
normalized === 'desktop_account_challenge_required' ||
normalized === 'desktop_account_risk_control' ||
normalized.endsWith('_not_logged_in') ||
normalized.endsWith('_login_required') ||
normalized.endsWith('_login_expired') ||
normalized.endsWith('_challenge_required')
)
}
function isAuthorizationFailureMessage(message: string | null | undefined): boolean {
if (!message) {
return false
}
const normalized = message.trim().toLowerCase()
return (
normalized.includes('login required') ||
normalized.includes('login expired') ||
normalized.includes('not logged in') ||
normalized.includes('unauthorized') ||
normalized.includes('登录态失效') ||
normalized.includes('登录态已失效') ||
normalized.includes('登录已失效') ||
normalized.includes('请重新登录') ||
normalized.includes('请先登录') ||
normalized.includes('未登录')
)
}
function isAuthorizationFailureCategory(category: string | null | undefined): boolean {
return category === 'ai_platform_authorization' || category === 'media_account_authorization'
}
function isNonRetryableAuthorizationFailure(
task: RuntimeTaskRecord,
result: AdapterExecutionResult,
): boolean {
if (result.status !== 'failed') {
return false
}
const code = typeof result.error?.code === 'string' ? result.error.code : null
const message = typeof result.error?.message === 'string' ? result.error.message : null
const category =
typeof result.error?.failure_category === 'string' ? result.error.failure_category : null
if (isAuthorizationFailureCategory(category)) {
return true
}
if (!isAuthorizationFailureCode(code) && !isAuthorizationFailureMessage(message)) {
return false
}
return task.kind === 'publish' || isAIPlatformId(task.platform)
}
function withAuthorizationRetryPolicy(
task: RuntimeTaskRecord,
result: AdapterExecutionResult,
): AdapterExecutionResult {
if (!isNonRetryableAuthorizationFailure(task, result)) {
return result
}
const category = isAIPlatformId(task.platform)
? 'ai_platform_authorization'
: 'media_account_authorization'
const baseError = result.error ?? {
code: 'desktop_account_authorization_failed',
message: result.summary,
}
return {
...result,
error: markAuthorizationFailureNonRetryable(baseError, category),
}
}
async function resolveAuthorizationPreflightBlock(
task: RuntimeTaskRecord,
): Promise<AdapterExecutionResult | null> {
const accountIdentity = accountIdentityFromTask(task)
if (!accountIdentity || !shouldEnforceActiveAccount(task)) {
return null
}
const previousSnapshot = getAccountHealthSnapshot(accountIdentity.id)
const readiness = await ensureAccountReady(accountIdentity)
if (readiness.authState === 'active') {
return null
}
const result = withAuthorizationRetryPolicy(task, buildAccountBlockedResult(task, readiness))
maybeRecordAccountAccessAlert(task, accountIdentity, previousSnapshot, readiness)
enqueueAccountHealthReport(accountIdentity.id)
recordAuthorizationPreflightBlock(task, result)
return result
}
function recordAuthorizationPreflightBlock(
task: RuntimeTaskRecord,
result: AdapterExecutionResult,
): void {
if (!isNonRetryableAuthorizationFailure(task, result)) {
return
}
const scope = task.kind === 'monitor' && isAIPlatformId(task.platform) ? '同模型' : '同账号'
recordActivity(
'warn',
'授权失效,任务未执行',
`${runtimePlatformLabel(task.platform)} ${scope}授权不是 active${task.title} 已直接回写 non-retryable 授权失败。`,
)
}
function maybeRecordAccountAccessAlert(
task: RuntimeTaskRecord,
accountIdentity: PublishAccountIdentity | null,
previous: ReturnType<typeof getAccountHealthSnapshot>,
next: ReturnType<typeof getAccountHealthSnapshot>,
): void {
if (!accountIdentity || !next) {
return
}
if (previous?.authState === next.authState && previous?.authReason === next.authReason) {
return
}
if (next.authState === 'challenge_required' && next.authReason === 'risk_control') {
if (accountIdentity.platform === 'baijiahao') {
recordActivity('warn', '百家号触发平台风控', BAIJIAHAO_RISK_CONTROL_PROMPT)
return
}
recordActivity(
'warn',
`${runtimePlatformLabel(accountIdentity.platform)} 触发风控`,
`${accountIdentity.displayName || task.accountName || '当前账号'} 已被平台限制,请打开平台处理验证或等待限制解除后再刷新状态。`,
)
return
}
if (next.authState === 'challenge_required') {
if (accountIdentity.platform === 'baijiahao') {
recordActivity('warn', '百家号触发平台风控', BAIJIAHAO_RISK_CONTROL_PROMPT)
return
}
recordActivity(
'warn',
`${runtimePlatformLabel(accountIdentity.platform)} 需要人工验证`,
`${accountIdentity.displayName || task.accountName || '当前账号'} 需要打开平台完成人工验证后再继续执行任务。`,
)
}
}
function isoFromTimestamp(value: number | null): string | null {
return value ? new Date(value).toISOString() : null
}
export function syncRuntimeSession(next: DesktopRuntimeSessionSyncRequest | null): void {
const normalized = normalizeSession(next)
const changed = !sameSession(state.session, normalized)
state.session = normalized
state.client = normalized?.desktop_client ?? null
if (!canRunLive(normalized)) {
stopRuntime()
clearRuntimeState()
configureTransport(null)
setTransportAuthState('pending')
return
}
configureTransport(normalized)
setTransportAuthState('registered')
if (changed) {
stopRuntime()
clearRuntimeState()
recordActivity(
'info',
'任务消费端已就绪',
`客户端 ${normalized.desktop_client?.device_name ?? normalized.desktop_client?.id ?? '未知客户端'} 已准备消费桌面任务。`,
)
}
if (!state.running) {
startRuntime()
}
}
export async function releaseRuntimeSession(options: { revoke?: boolean } = {}): Promise<void> {
const currentSession = state.session
const shouldReleaseRemote = canRunLive(currentSession)
if (shouldReleaseRemote) {
try {
if (options.revoke) {
await revokeDesktopClient()
} else {
await offlineDesktopClient()
}
} catch (error) {
console.warn('[desktop-runtime] release runtime session failed', {
revoke: Boolean(options.revoke),
message: errorMessage(error),
})
}
}
stopRuntime()
clearRuntimeState()
configureTransport(null)
setTransportAuthState('pending')
state.session = null
state.client = null
}
export function getRuntimeControllerSnapshot(): RuntimeControllerSnapshot {
return {
session: state.session ? { ...state.session } : null,
client: state.client ? { ...state.client } : null,
running: state.running,
dispatchConnected: state.dispatchWsConnected,
queueDepth: localQueueDepth(),
accounts: state.accounts.map((item) => ({ ...item })),
accountProfiles: Object.fromEntries(
[...state.accountProfiles.entries()].map(([accountId, profile]) => [
accountId,
{ ...profile },
]),
),
tasks: [...state.tasks.values()]
.sort((left, right) => right.updatedAt - left.updatedAt)
.map(({ leaseToken: _leaseToken, ...task }) => ({
...task,
startedAt: state.activeExecutions.get(task.id)?.startedAt ?? task.startedAt,
})),
activity: [...state.activity],
lastHeartbeatAt: state.lastHeartbeatAt,
lastHeartbeatStatus: state.lastHeartbeatStatus,
lastPullAt: state.lastPullAt,
lastPullStatus: state.lastPullStatus,
lastAccountsSyncAt: state.lastAccountsSyncAt,
lastServerTime: state.lastServerTime,
onlineClientCount: state.onlineClientCount,
lastError: state.lastError,
}
}
function startRuntime(): void {
if (!canRunLive(state.session) || state.running) {
return
}
initScheduler()
initMonitorScheduler()
initPublishScheduler()
startHiddenPlaywrightReaper()
ensureAccountHealthReportBridge()
state.running = true
setSchedulerPhase('running')
setSchedulerError(null)
recordActivity('success', '调度节点已启动', '已启动 WebSocket 分发、心跳上报和兜底拉取。')
connectDispatchWs()
scheduleHeartbeatLoop()
schedulePullLoop()
schedulePumpWatchdogLoop()
void sendHeartbeat('startup')
void syncAccounts('startup')
void resumeLeasedMonitoringTasks('startup')
pumpExecutionLoop()
}
function ensureAccountHealthReportBridge(): void {
if (accountHealthReportBridgeUnsubscribe) {
return
}
accountHealthReportBridgeUnsubscribe = onRuntimeInvalidated((event) => {
if (event.reason !== 'account-health' || !event.accountId) {
return
}
enqueueAccountHealthReport(event.accountId)
})
}
function stopRuntime(): void {
state.running = false
state.leaseInFlightKinds.clear()
for (const execution of state.activeExecutions.values()) {
if (execution.deadlineHandle) {
clearTimeout(execution.deadlineHandle)
execution.deadlineHandle = null
}
execution.abortController.abort()
}
state.activeExecutions.clear()
if (state.heartbeatTimer) {
clearInterval(state.heartbeatTimer)
state.heartbeatTimer = null
}
if (state.pullTimer) {
clearInterval(state.pullTimer)
state.pullTimer = null
}
if (state.pumpWatchdogTimer) {
clearInterval(state.pumpWatchdogTimer)
state.pumpWatchdogTimer = null
}
if (state.accountHealthReportTimer) {
clearTimeout(state.accountHealthReportTimer)
state.accountHealthReportTimer = null
}
if (state.dispatchWsClient) {
state.dispatchWsClient.stop()
state.dispatchWsClient = null
}
state.dispatchWsConnected = false
setTransportDispatchState('idle')
setSchedulerPhase('idle')
setSchedulerCurrentTask(null)
setSchedulerQueueDepth(localQueueDepth())
setSchedulerNextHeartbeat(null)
setSchedulerNextPull(null)
if (getLeaseManagerSnapshot().activeTaskId) {
noteLeaseReleased('cleared')
}
}
function clearRuntimeState(): void {
initPublishScheduler()
state.tasks.clear()
state.accounts = []
state.accountProfiles.clear()
state.accountHealthReportQueue.clear()
state.accountHealthReportBadAccounts.clear()
state.accountRemoteHealth.clear()
state.activeExecutions.clear()
state.lastHeartbeatAt = 0
state.lastHeartbeatStatus = 'idle'
state.lastPullAt = 0
state.lastPullStatus = 'idle'
state.lastAccountsSyncAt = 0
state.lastServerTime = null
state.onlineClientCount = null
state.lastError = null
state.publishFallbackBackoffUntil = 0
setSchedulerQueueDepth(localQueueDepth())
}
function scheduleHeartbeatLoop(): void {
if (state.heartbeatTimer) {
clearInterval(state.heartbeatTimer)
}
setSchedulerNextHeartbeat(Date.now() + heartbeatIntervalMs)
state.heartbeatTimer = setInterval(() => {
setSchedulerNextHeartbeat(Date.now() + heartbeatIntervalMs)
void sendHeartbeat('timer')
}, heartbeatIntervalMs)
}
function schedulePullLoop(): void {
if (state.pullTimer) {
clearInterval(state.pullTimer)
}
setSchedulerNextPull(Date.now() + pullIntervalMs)
state.pullTimer = setInterval(() => {
setSchedulerNextPull(Date.now() + pullIntervalMs)
pumpExecutionLoop()
}, pullIntervalMs)
}
function schedulePumpWatchdogLoop(): void {
if (state.pumpWatchdogTimer) {
clearInterval(state.pumpWatchdogTimer)
}
state.pumpWatchdogTimer = setInterval(() => {
notifyLongRunningPublishTasks()
pumpExecutionLoop()
}, pumpWatchdogIntervalMs)
}
function connectDispatchWs(): void {
if (!canRunLive(state.session)) {
return
}
state.dispatchWsClient?.stop()
setTransportDispatchState('connecting')
const wsUrl = buildApiUrl(state.session.api_base_url, '/api/desktop/dispatch')
.replace(/^http:\/\//, 'ws://')
.replace(/^https:\/\//, 'wss://')
const client = new DispatchWsClient({
url: wsUrl,
headers: {
Authorization: `Bearer ${state.session.client_token}`,
},
})
let hasConnectedOnce = false
client.on('open', () => {
state.dispatchWsConnected = true
setTransportDispatchState('streaming')
setSchedulerError(null)
recordActivity('success', '任务分发通道已连接', '统一 WebSocket 分发通道已就绪。')
void pullNextTask('monitor')
if (hasConnectedOnce) {
void resumeLeasedMonitoringTasks('reconnect')
}
hasConnectedOnce = true
})
client.on('connected', (payload) => {
state.dispatchWsConnected = true
setTransportDispatchState('streaming')
if (isConnectedEvent(payload)) {
state.lastServerTime = payload.server_time
}
})
const dispatchTaskHandler = (payload: unknown) => {
state.dispatchWsConnected = true
noteSchedulerSseEvent()
if (isMonitoringTaskControlEvent(payload)) {
void handleMonitoringTaskControl(payload)
return
}
if (isMonitoringDispatchSignal(payload)) {
void handleMonitoringDispatchSignal(payload)
return
}
if (!isDesktopTaskEvent(payload)) {
return
}
handleTaskEvent(payload)
}
client.on('task_available', dispatchTaskHandler)
client.on('task_leased', dispatchTaskHandler)
client.on('task_extended', dispatchTaskHandler)
client.on('task_completed', dispatchTaskHandler)
client.on('task_canceled', dispatchTaskHandler)
client.on('task_reconciled', dispatchTaskHandler)
client.on('task_control', dispatchTaskHandler)
client.on('error', (payload) => {
state.dispatchWsConnected = false
setTransportDispatchState('connecting')
const message = errorMessage(payload)
state.lastError = message
setSchedulerError(message)
recordActivity('warn', '任务分发通道异常', message)
})
client.on('close', () => {
state.dispatchWsConnected = false
setTransportDispatchState(state.running ? 'connecting' : 'idle')
})
client.on('reconnect', (payload) => {
if (!isReconnectPayload(payload)) {
return
}
setTransportDispatchState('connecting')
recordActivity('info', '任务分发通道退避等待', `将在 ${payload.delayMs}ms 后重连 WebSocket。`)
})
state.dispatchWsClient = client
client.start()
}
function handleTaskEvent(event: DesktopTaskEventMessage): void {
if (event.type === 'task_control' || event.signal_only || !event.task_id.trim()) {
return
}
const existing = state.tasks.get(event.task_id)
const eventUpdatedAt = parseTimestamp(event.updated_at) ?? Date.now()
if (existing && eventUpdatedAt < existing.updatedAt) {
return
}
const next: RuntimeTaskRecord = {
id: event.task_id,
jobId: event.job_id,
title: existing?.title ?? defaultTaskTitle(event.kind, event.platform || existing?.platform),
kind: event.kind,
platform:
event.platform || existing?.platform || inferPlatformFromAccount(event.target_account_id),
accountId: event.target_account_id || existing?.accountId || '',
accountName: existing?.accountName ?? '待同步账号',
clientId: event.target_client_id,
status: event.status,
routing: existing?.routing ?? 'rabbitmq-primary',
leaseExpiresAt: event.status === 'in_progress' ? (existing?.leaseExpiresAt ?? null) : null,
startedAt: event.status === 'in_progress' ? (existing?.startedAt ?? eventUpdatedAt) : null,
updatedAt: eventUpdatedAt,
summary: summaryFromEventType(event.type, event.status),
payload: existing?.payload ?? {},
error: existing?.error ?? null,
result: existing?.result ?? null,
attemptId: event.status === 'in_progress' ? (existing?.attemptId ?? null) : null,
leaseToken: event.status === 'in_progress' ? (existing?.leaseToken ?? null) : null,
}
state.tasks.set(event.task_id, next)
if (event.kind === 'monitor') {
if (event.type === 'task_available') {
enqueueMonitorTaskFromEvent(event, 'rabbitmq-primary')
}
if (
event.type === 'task_completed' ||
event.type === 'task_reconciled' ||
event.type === 'task_canceled'
) {
noteMonitorTaskCanceled(event.task_id)
}
}
if (
event.kind === 'publish' &&
(event.type === 'task_completed' ||
event.type === 'task_reconciled' ||
event.type === 'task_canceled')
) {
notePublishTaskCanceled(event.task_id)
}
if (event.type === 'task_available') {
if (event.kind === 'publish') {
enqueuePublishTask(next.id, next.platform, 'rabbitmq-primary')
}
pumpExecutionLoop()
}
}
async function handleMonitoringDispatchSignal(event: DesktopTaskEventMessage): Promise<void> {
if (event.target_client_id !== currentClientID()) {
return
}
if ((event.lane ?? '').trim() === 'high') {
recordActivity(
'info',
'收到监控高优先级信号',
'客户端将按平台隔离和并发上限,优先领取 collect-now 桌面监控任务。',
)
}
if (event.task_id.trim()) {
enqueueMonitorTaskFromEvent(event, 'rabbitmq-primary')
pumpExecutionLoop()
return
}
await pullNextTask('monitor')
}
async function handleMonitoringTaskControl(event: DesktopTaskEventMessage): Promise<void> {
if (
event.target_client_id !== currentClientID() ||
event.control !== 'interrupt_requested' ||
!event.task_id.trim()
) {
return
}
const taskId = event.task_id.trim()
const generation = toInterruptGeneration(event.interrupt_generation)
const existing = state.tasks.get(taskId) ?? null
if (existing && generation < toInterruptGeneration(existing.payload.interrupt_generation)) {
return
}
const active = state.activeExecutions.get(taskId)
if (active?.kind === 'monitor') {
if (isDesktopTaskID(taskId)) {
if (existing) {
existing.summary =
'已收到立即收集请求;当前执行继续保留,high lane 任务会按平台隔离和并发上限调度。'
existing.updatedAt = Date.now()
state.tasks.set(taskId, existing)
}
recordActivity(
'info',
'监控任务保留当前执行',
`${existing?.title ?? taskId} 当前执行继续保留;若 high lane 属于其他平台可使用第二个窗口并行,否则等待当前平台释放。`,
)
return
}
if (existing) {
existing.summary = '已收到抢占请求;当前监控执行将自然完成,随后优先处理 high lane 任务。'
existing.updatedAt = Date.now()
state.tasks.set(taskId, existing)
}
recordActivity(
'warn',
'监控任务收到抢占请求',
`${existing?.title ?? taskId} 当前已在执行,Phase 1 将等待自然完成。`,
)
return
}
if (existing) {
existing.payload = {
...existing.payload,
interrupt_requested: true,
interrupt_generation: generation,
interrupt_reason: event.reason?.trim() || 'collect_now_preempt',
replacement_task_id: event.replacement_task_id?.trim() || null,
}
existing.updatedAt = Date.now()
state.tasks.set(taskId, existing)
}
if (!existing?.leaseToken) {
noteMonitorTaskCanceled(taskId)
if (existing) {
existing.status = 'aborted'
existing.summary = '已收到抢占请求,本地排队中的旧监控任务已让路。'
existing.updatedAt = Date.now()
state.tasks.set(taskId, existing)
}
await pullMonitoringTasks('rabbitmq-primary')
return
}
const monitoringTaskID = Number.parseInt(taskId, 10)
if (!Number.isFinite(monitoringTaskID)) {
noteMonitorTaskCanceled(taskId)
if (existing) {
existing.status = 'aborted'
existing.leaseToken = null
existing.summary =
'已收到抢占请求,但任务不是 legacy monitoring lease,已从本地调度队列移除。'
existing.updatedAt = Date.now()
state.tasks.set(taskId, existing)
}
await pullMonitoringTasks('rabbitmq-primary')
return
}
try {
await cancelMonitoringTask(monitoringTaskID, {
lease_token: existing.leaseToken,
reason: event.reason?.trim() || 'collect_now_preempt',
})
noteMonitorTaskCanceled(taskId)
existing.status = 'aborted'
existing.leaseToken = null
existing.summary = '已响应抢占请求,释放旧监控租约并为高优任务让路。'
existing.updatedAt = Date.now()
state.tasks.set(taskId, existing)
recordActivity('info', '已取消旧监控租约', `${existing.title} 已从本地排队队列中移除。`)
await pullMonitoringTasks('rabbitmq-primary')
} catch (error) {
const message = errorMessage(error)
if (existing) {
existing.summary = `收到抢占请求,但取消租约失败:${message}`
existing.updatedAt = Date.now()
state.tasks.set(taskId, existing)
}
recordActivity(
'warn',
'监控租约抢占失败',
`${existing?.title ?? taskId} 取消租约失败:${message}`,
)
}
}
async function sendHeartbeat(source: 'startup' | 'timer'): Promise<void> {
if (!canRunLive(state.session)) {
return
}
const previousStatus = state.lastHeartbeatStatus
try {
const response = await heartbeatDesktopClient({
device_name: hostname() || state.client?.device_name || `Desktop ${process.platform}`,
os: process.platform,
cpu_arch: process.arch,
client_version: getDesktopAppVersion(),
channel: state.client?.channel ?? getDesktopReleaseChannel(),
startup: source === 'startup',
account_ids: state.accounts
.filter((account) => shouldReportAccountPresence(account))
.map((account) => account.id),
})
state.client = response.client
state.lastHeartbeatAt = Date.now()
state.lastHeartbeatStatus = 'success'
state.lastServerTime = response.server_time
state.onlineClientCount =
response.current_user_online_client_count ?? response.online_client_count ?? null
state.lastError = null
noteTransportHeartbeat(true)
noteSchedulerHeartbeat()
setSchedulerError(null)
if (source === 'startup' || previousStatus === 'failed') {
recordActivity('success', '心跳连接正常', '客户端心跳已回写到服务端。')
}
} catch (error) {
state.lastHeartbeatAt = Date.now()
state.lastHeartbeatStatus = 'failed'
state.lastError = errorMessage(error)
noteTransportHeartbeat(false)
setSchedulerError(state.lastError)
if (source === 'startup' || previousStatus !== 'failed') {
recordActivity('danger', '心跳连接失败', state.lastError)
}
}
}
async function syncAccounts(source: 'startup' | 'manual'): Promise<void> {
if (!canRunLive(state.session)) {
return
}
try {
const accounts = await listDesktopAccounts()
state.lastAccountsSyncAt = Date.now()
state.lastError = null
noteSchedulerAccountsSync()
setSchedulerError(null)
const identities = accounts
.filter((account) => hasLocalAccountAuthorization(account))
.map((account) => accountIdentityFromDesktopAccount(account))
syncTrackedAccounts(identities)
state.accountRemoteHealth = new Map(accounts.map((account) => [account.id, account.health]))
state.accountProfiles.clear()
const syncedAccounts: Array<DesktopAccountInfo | null> = accounts.map((account) => {
const normalizedPlatformUid =
normalizeAccountPlatformUid(account.platform_uid) || account.platform_uid
const isAIPlatform = isAIPlatformId(account.platform)
const accountIdentity = accountIdentityFromDesktopAccount({
...account,
platform_uid: normalizedPlatformUid,
})
if (accountRequiresLocalAuthorization(account) && !hasLocalAccountAuthorization(account)) {
markTrackedAccountMissingPartition(accountIdentity)
}
const projected = getProjectedAccountHealth({
accountId: account.id,
platform: account.platform,
health: account.health,
verifiedAt: account.verified_at,
})
if (projected.profile?.subjectUid && projected.profile.displayName) {
state.accountProfiles.set(account.id, {
platformUid: projected.profile.subjectUid,
displayName: projected.profile.displayName,
avatarUrl: projected.profile.avatarUrl,
})
}
const normalizedLocalPlatformUid = projected.profile?.subjectUid
? normalizeAccountPlatformUid(projected.profile.subjectUid) || projected.profile.subjectUid
: null
const hasPlatformUidMismatch = Boolean(
normalizedLocalPlatformUid &&
!sameAccountPlatformUid(normalizedLocalPlatformUid, normalizedPlatformUid),
)
if (hasPlatformUidMismatch && !isAIPlatform) {
return {
...account,
platform_uid: normalizedPlatformUid,
health: 'expired' as const,
}
}
const resolvedAccount: DesktopAccountInfo = {
...account,
platform_uid: normalizedPlatformUid,
display_name: projected.profile?.displayName ?? account.display_name,
avatar_url: projected.profile?.avatarUrl ?? account.avatar_url,
health: projected.health,
verified_at: isoFromTimestamp(projected.lastVerifiedAt) ?? account.verified_at,
}
return resolvedAccount
})
const duplicateAccounts: DesktopAccountInfo[] = []
const dedupedAccounts: DesktopAccountInfo[] = []
const seenAccountIndexes = new Map<string, number>()
for (const account of syncedAccounts) {
if (!account) {
continue
}
const identityKey = buildRuntimeAccountDedupeKey(account)
const existingIndex = seenAccountIndexes.get(identityKey)
if (existingIndex !== undefined) {
const existingAccount = dedupedAccounts[existingIndex]
if (shouldPreferRuntimeAccount(account, existingAccount)) {
duplicateAccounts.push(existingAccount)
dedupedAccounts[existingIndex] = account
} else {
duplicateAccounts.push(account)
}
continue
}
seenAccountIndexes.set(identityKey, dedupedAccounts.length)
dedupedAccounts.push(account)
}
state.accounts = dedupedAccounts
if (duplicateAccounts.length > 0) {
void cleanupDuplicateDesktopAccounts(duplicateAccounts)
}
if (source === 'startup') {
recordActivity('info', '本地账号同步', `已同步 ${state.accounts.length} 个桌面账号。`)
}
refreshAccountNames()
pumpExecutionLoop()
void sendHeartbeat('timer')
} catch (error) {
const message = errorMessage(error)
state.lastError = message
setSchedulerError(message)
recordActivity('warn', '账号同步失败', message)
}
}
async function cleanupDuplicateDesktopAccounts(accounts: DesktopAccountInfo[]): Promise<void> {
for (const account of accounts) {
try {
await deleteDesktopAccount(account.id, account.sync_version)
await clearSessionHandle(account.id)
forgetTrackedAccountHealth(account.id)
} catch (error) {
console.warn('[desktop-runtime] duplicate desktop account cleanup failed', {
accountId: account.id,
platform: account.platform,
platformUid: account.platform_uid,
message: errorMessage(error),
})
}
}
}
function buildAccountHealthReport(account: DesktopAccountInfo): DesktopAccountHealthReport {
const projected = getProjectedAccountHealth({
accountId: account.id,
platform: account.platform,
health: account.health,
verifiedAt: account.verified_at,
})
const health =
projected.authState === 'expiring_soon' && projected.probeState !== 'network_error'
? 'live'
: projected.health
return {
account_id: account.id,
platform: account.platform,
platform_uid: normalizeAccountPlatformUid(account.platform_uid) || account.platform_uid,
health,
auth_state: projected.authState,
probe_state: projected.probeState,
auth_reason: projected.authReason,
display_name: projected.profile?.displayName ?? account.display_name,
avatar_url: projected.profile?.avatarUrl ?? account.avatar_url,
verified_at: isoFromTimestamp(projected.lastVerifiedAt) ?? account.verified_at,
checked_at: new Date().toISOString(),
}
}
function isAccountHealthFailureReport(report: DesktopAccountHealthReport): boolean {
return (
report.health !== 'live' ||
report.auth_state === 'expired' ||
report.auth_state === 'challenge_required' ||
report.auth_state === 'revoked' ||
report.probe_state === 'network_error'
)
}
function shouldQueueAccountHealthReport(
account: DesktopAccountInfo,
report: DesktopAccountHealthReport,
): boolean {
if (isAccountHealthFailureReport(report)) {
return true
}
if (state.accountHealthReportBadAccounts.has(account.id)) {
return true
}
return (state.accountRemoteHealth.get(account.id) ?? account.health) !== 'live'
}
function enqueueAccountHealthReport(accountId: string): void {
const account = state.accounts.find((item) => item.id === accountId) ?? null
if (!account) {
return
}
if (!hasLocalAccountAuthorization(account)) {
return
}
const report = buildAccountHealthReport(account)
if (!shouldQueueAccountHealthReport(account, report)) {
return
}
if (isAccountHealthFailureReport(report)) {
state.accountHealthReportBadAccounts.add(account.id)
}
state.accountHealthReportQueue.set(account.id, report)
if (state.accountHealthReportTimer) {
return
}
state.accountHealthReportTimer = setTimeout(() => {
state.accountHealthReportTimer = null
void flushAccountHealthReports()
}, accountHealthReportDebounceMs)
}
async function flushAccountHealthReports(): Promise<void> {
if (state.accountHealthReportQueue.size === 0 || !canRunLive(state.session)) {
return
}
const reports = [...state.accountHealthReportQueue.values()].slice(
0,
accountHealthReportMaxBatchSize,
)
for (const report of reports) {
state.accountHealthReportQueue.delete(report.account_id)
}
try {
await reportDesktopAccountHealth({ reports })
for (const report of reports) {
state.accountRemoteHealth.set(report.account_id, report.health)
if (isAccountHealthFailureReport(report)) {
state.accountHealthReportBadAccounts.add(report.account_id)
} else {
state.accountHealthReportBadAccounts.delete(report.account_id)
}
}
} catch (error) {
console.warn('[desktop-runtime] account health report failed', {
count: reports.length,
message: errorMessage(error),
})
for (const report of reports) {
state.accountHealthReportQueue.set(report.account_id, report)
}
} finally {
if (state.accountHealthReportQueue.size > 0 && !state.accountHealthReportTimer) {
state.accountHealthReportTimer = setTimeout(() => {
state.accountHealthReportTimer = null
void flushAccountHealthReports()
}, accountHealthReportDebounceMs)
}
}
}
async function mirrorProjectedAccountProfile(accountId: string): Promise<void> {
const account = state.accounts.find((item) => item.id === accountId) ?? null
if (!account) {
return
}
const projected = getProjectedAccountHealth({
accountId: account.id,
platform: account.platform,
health: account.health,
verifiedAt: account.verified_at,
})
if (!isDefinitiveAuthState(projected.authState)) {
return
}
const nextDisplayName = projected.profile?.displayName ?? account.display_name
const nextAvatarUrl = projected.profile?.avatarUrl ?? account.avatar_url
const shouldMirrorProfile =
account.display_name !== nextDisplayName || account.avatar_url !== nextAvatarUrl
if (!shouldMirrorProfile) {
return
}
const normalizedPlatformUid =
normalizeAccountPlatformUid(account.platform_uid) || account.platform_uid
const updated = await upsertDesktopAccount({
platform: account.platform,
platform_uid: normalizedPlatformUid,
display_name: nextDisplayName,
avatar_url: nextAvatarUrl,
account_fingerprint: account.account_fingerprint ?? normalizedPlatformUid,
health: account.health,
verified_at: account.verified_at,
tags: account.tags,
})
state.accounts = state.accounts.map((item) =>
item.id === account.id
? {
...updated,
platform_uid: normalizeAccountPlatformUid(updated.platform_uid) || updated.platform_uid,
health: account.health,
}
: item,
)
state.accountProfiles.set(account.id, {
platformUid: normalizedPlatformUid,
displayName: nextDisplayName,
avatarUrl: nextAvatarUrl,
})
refreshAccountNames()
}
export async function refreshRuntimeAccounts(): Promise<void> {
await syncAccounts('manual')
}
export async function requestRuntimeAccountProbe(
accountId: string,
options: { account?: PublishAccountIdentity; force?: boolean } = {},
): Promise<void> {
const existingAccount = state.accounts.find((item) => item.id === accountId) ?? null
const account =
options.account ?? (existingAccount ? accountIdentityFromDesktopAccount(existingAccount) : null)
if (!account) {
throw new Error('runtime_account_not_found')
}
if (existingAccount && !hasLocalAccountAuthorization(existingAccount)) {
markTrackedAccountMissingPartition(account)
return
}
try {
await probeTrackedAccount(account, {
trigger: 'manual',
allowSilentRefresh: true,
force: options.force ?? true,
})
enqueueAccountHealthReport(account.id)
await mirrorProjectedAccountProfile(account.id)
} catch (error) {
console.warn('[desktop-runtime] account probe failed', {
accountId: account.id,
platform: account.platform,
message: errorMessage(error),
})
throw error
}
}
export function noteRuntimeAccountBound(account: DesktopAccountInfo): void {
const verifiedAt = new Date().toISOString()
const normalizedAccount: DesktopAccountInfo = {
...account,
platform_uid: normalizeAccountPlatformUid(account.platform_uid) || account.platform_uid,
health: 'live',
verified_at: account.verified_at ?? verifiedAt,
}
const existingIndex = state.accounts.findIndex(
(item) =>
item.id === normalizedAccount.id ||
(isAIPlatformId(normalizedAccount.platform) &&
item.platform === normalizedAccount.platform) ||
(item.platform === normalizedAccount.platform &&
sameAccountPlatformUid(item.platform_uid, normalizedAccount.platform_uid)),
)
const replacedAccount =
existingIndex >= 0 && state.accounts[existingIndex].id !== normalizedAccount.id
? state.accounts[existingIndex]
: null
if (existingIndex >= 0) {
state.accounts = state.accounts.map((item, index) =>
index === existingIndex ? normalizedAccount : item,
)
} else {
state.accounts = [normalizedAccount, ...state.accounts]
}
state.accountProfiles.set(normalizedAccount.id, {
platformUid: normalizedAccount.platform_uid,
displayName: normalizedAccount.display_name,
avatarUrl: normalizedAccount.avatar_url,
})
if (replacedAccount) {
state.accountProfiles.delete(replacedAccount.id)
forgetTrackedAccountHealth(replacedAccount.id)
void clearSessionHandle(replacedAccount.id)
void deleteDesktopAccount(replacedAccount.id, replacedAccount.sync_version).catch((error) => {
console.warn('[desktop-runtime] replaced desktop account cleanup failed', {
accountId: replacedAccount.id,
platform: replacedAccount.platform,
platformUid: replacedAccount.platform_uid,
message: errorMessage(error),
})
})
}
state.lastAccountsSyncAt = Date.now()
syncTrackedAccounts(
state.accounts
.filter((item) => hasLocalAccountAuthorization(item))
.map((item) => accountIdentityFromDesktopAccount(item)),
)
markTrackedAccountBound(accountIdentityFromDesktopAccount(normalizedAccount), {
subjectUid: normalizedAccount.platform_uid,
displayName: normalizedAccount.display_name,
avatarUrl: normalizedAccount.avatar_url,
})
enqueueAccountHealthReport(normalizedAccount.id)
refreshAccountNames()
}
export async function unbindRuntimeAccount(accountId: string, syncVersion: number): Promise<void> {
const matchedAccount = state.accounts.find((item) => item.id === accountId) ?? null
await deleteDesktopAccount(accountId, syncVersion)
await clearSessionHandle(accountId)
forgetTrackedAccountHealth(accountId)
state.accounts = state.accounts.filter((item) => item.id !== accountId)
state.accountProfiles.delete(accountId)
refreshAccountNames()
if (matchedAccount) {
recordActivity(
'info',
'账号已解绑',
`${matchedAccount.display_name} 已解绑,并清理了当前机器上的本地会话缓存。`,
)
}
await syncAccounts('manual')
}
function enqueuePublishTask(taskId: string, platform: string, routing: RuntimeTaskRouting): void {
if (!taskId || state.activeExecutions.has(taskId)) {
return
}
enqueuePublishSchedulerTask({ taskId, platform, routing })
syncSchedulerSurface()
}
function createActiveExecution(
taskRecord: RuntimeTaskRecord,
abortController: AbortController,
): ActiveRuntimeExecution {
const now = Date.now()
return {
taskId: taskRecord.id,
kind: taskRecord.kind,
platform: taskRecord.platform,
accountId: taskRecord.accountId,
abortController,
executionPhase: taskRecord.kind === 'monitor' ? 'PREPARING' : null,
interruptRequested:
taskRecord.kind === 'monitor' && Boolean(taskRecord.payload.interrupt_requested),
interruptGeneration:
taskRecord.kind === 'monitor'
? toInterruptGeneration(taskRecord.payload.interrupt_generation)
: 0,
interruptReason: taskRecord.kind === 'monitor' ? interruptReasonFromTask(taskRecord) : null,
lastSafePointAt: taskRecord.kind === 'monitor' ? now : null,
startedAt: now,
lastProgressAt: now,
lastProgressSummary: taskRecord.summary,
deadlineAt: null,
deadlineHandle: null,
longRunningNotifiedAt: null,
submitStartedAt: null,
}
}
function armPublishTaskDeadline(
taskRecord: RuntimeTaskRecord,
active: ActiveRuntimeExecution,
): void {
if (taskRecord.kind !== 'publish') {
return
}
const taskDeadlineMs = resolvePublishTaskTimeoutMs(taskRecord.platform)
active.deadlineAt = active.startedAt + taskDeadlineMs
active.deadlineHandle = setTimeout(() => {
if (active.abortController.signal.aborted) {
return
}
const timeoutError = new Error('publish_task_timeout')
recordActivity(
'warn',
'任务超时',
`${taskRecord.title} 执行超过 ${Math.round(taskDeadlineMs / 1000)}s,已中止并释放。`,
)
updateTaskProgress(taskRecord.id, `${taskRecord.title} 执行超时,正在释放任务。`)
active.abortController.abort(timeoutError)
}, taskDeadlineMs)
}
function resolvePublishTaskTimeoutMs(platform: string): number {
const envKey = `GEO_DESKTOP_PUBLISH_TIMEOUT_${platform.toUpperCase().replace(/[^A-Z0-9]/g, '_')}_MS`
return (
resolvePositiveEnvInteger(envKey) ??
resolvePositiveEnvInteger('GEO_DESKTOP_PUBLISH_TIMEOUT_MS') ??
defaultPublishTaskTimeoutMs
)
}
function resolvePositiveEnvInteger(key: string): number | null {
const raw = process.env[key]?.trim()
if (!raw) {
return null
}
const parsed = Number.parseInt(raw, 10)
return Number.isFinite(parsed) && parsed > 0 ? parsed : null
}
// Stages emitted by an adapter right before it performs an irreversible platform write (creating
// the article/draft). After this point the article may exist on the platform regardless of whether
// the local task records it, so an interrupted publish must not be auto-retried.
function isIrreversiblePublishStage(stage: string): boolean {
const normalized = stage.trim().toLowerCase()
return (
normalized.endsWith('.create_note') ||
normalized.endsWith('.create_article_id') ||
normalized.endsWith('.create_draft') ||
normalized.endsWith('.publish') ||
normalized.endsWith('.publish_draft') ||
normalized.endsWith('.save_content') ||
normalized.endsWith('.submit') ||
normalized.endsWith('.submit_editor') ||
normalized.endsWith('.save_draft') ||
normalized === 'create_note' ||
normalized === 'create_article_id' ||
normalized === 'create_draft' ||
normalized === 'publish' ||
normalized === 'publish_draft' ||
normalized === 'save_content' ||
normalized === 'submit' ||
normalized === 'submit_editor' ||
normalized === 'save_draft'
)
}
// Persist a durable "submit started" marker before the irreversible platform write. The local flag
// is set only after the server confirms the marker, so recovery can trust publish_submit_started_at.
async function markPublishSubmitStartedIfNeeded(taskId: string, stage: string): Promise<void> {
if (!isIrreversiblePublishStage(stage)) {
return
}
const active = state.activeExecutions.get(taskId)
if (!active || active.kind !== 'publish' || active.submitStartedAt !== null) {
return
}
const leaseToken = state.tasks.get(taskId)?.leaseToken
if (!leaseToken) {
throw new Error('publish_submit_marker_lease_missing')
}
await markDesktopTaskPublishSubmitStarted(taskId, leaseToken)
active.submitStartedAt = Date.now()
state.activeExecutions.set(taskId, active)
}
function asSubmitUncertainExecution(result: AdapterExecutionResult): AdapterExecutionResult {
if (result.status !== 'failed') {
return result
}
return {
...result,
status: 'unknown',
summary: '发布可能已提交到平台,结果待人工确认,已避免自动重复发布。',
error: {
...(result.error ?? {}),
publish_submit_uncertain: true,
original_status: 'failed',
original_summary: result.summary,
},
}
}
function pumpExecutionLoop(): void {
if (!state.running) {
return
}
syncSchedulerSurface()
if (canStartAnotherPublishExecution()) {
const selected = selectNextPublishTask({
maxConcurrency: resolveAdaptiveTotalConcurrency(),
activePlatforms: activePublishPlatforms(),
activeCount: activePublishCount(),
})
if (selected) {
void leaseSpecificTask(selected, 'publish')
return
}
}
if (shouldPullPublishFallback()) {
void pullNextTask('publish')
return
}
if (hasPublishBacklog()) {
return
}
if (canStartAnotherMonitorExecution()) {
const selected = selectNextMonitorTask({
maxConcurrency: resolveAdaptiveMonitorConcurrency(),
activePlatforms: activeMonitorPlatforms(),
activeQuestionKeys: activeMonitorQuestionKeys(),
activeCount: activeMonitorCount(),
})
if (selected) {
void leaseSpecificTask(selected, 'monitor')
return
}
}
return
}
async function leaseSpecificTask(
request: PendingTaskRequest,
expectedKind: 'publish' | 'monitor',
): Promise<void> {
if (!canRunLive(state.session) || isLeaseInFlight(expectedKind)) {
return
}
state.leaseInFlightKinds.add(expectedKind)
try {
if (expectedKind === 'monitor' && request.source === 'monitoring_lease') {
state.leaseInFlightKinds.delete(expectedKind)
await executeLeasedMonitoringTask(request.taskId, request.routing)
return
}
const leased = await leaseDesktopTask({ task_id: request.taskId })
if (!leased.task) {
if (expectedKind === 'monitor') {
noteMonitorTaskLeaseMiss(request.taskId)
} else {
notePublishTaskLeaseMiss(request.taskId)
}
return
}
state.leaseInFlightKinds.delete(expectedKind)
await executeLeasedTask(leased, request.routing)
} catch (error) {
if (expectedKind === 'monitor') {
if (error instanceof ApiClientError && error.code === 40991) {
noteMonitorTaskCanceled(request.taskId)
const existing = state.tasks.get(request.taskId)
if (existing) {
existing.status = 'aborted'
existing.leaseToken = null
existing.summary = '本地缓存的监控任务已不在服务端 queued,已从调度队列移除。'
existing.updatedAt = Date.now()
state.tasks.set(request.taskId, existing)
}
} else {
noteMonitorTaskLeaseMiss(request.taskId)
}
} else {
notePublishTaskLeaseMiss(request.taskId)
}
state.lastError = errorMessage(error)
setSchedulerError(state.lastError)
recordActivity('warn', '指定任务领取失败', `${request.taskId} 未能成功领取:${state.lastError}`)
} finally {
if (state.leaseInFlightKinds.has(expectedKind)) {
state.leaseInFlightKinds.delete(expectedKind)
syncSchedulerSurface()
if (localQueueDepth() > 0) {
pumpExecutionLoop()
}
}
}
}
async function pullNextTask(kind: 'publish' | 'monitor'): Promise<void> {
if (kind === 'monitor') {
if (!canRunLive(state.session) || isLeaseInFlight('monitor')) {
return
}
state.leaseInFlightKinds.add('monitor')
try {
const leased = await leaseDesktopTask({ kind: 'monitor' })
state.lastPullAt = Date.now()
state.lastPullStatus = 'success'
noteTransportPull(true)
noteSchedulerPull()
if (leased.task) {
state.leaseInFlightKinds.delete('monitor')
await executeLeasedTask(leased, 'rabbitmq-primary')
return
}
} catch (error) {
state.lastPullAt = Date.now()
state.lastPullStatus = 'failed'
state.lastError = errorMessage(error)
noteTransportPull(false)
setSchedulerError(state.lastError)
} finally {
state.leaseInFlightKinds.delete('monitor')
syncSchedulerSurface()
}
await pullMonitoringTasks('db-recovery')
return
}
if (!canRunLive(state.session) || isLeaseInFlight('publish')) {
return
}
let leasedPublishTask = false
state.leaseInFlightKinds.add('publish')
try {
const leased = await leaseDesktopTask({ kind })
state.lastPullAt = Date.now()
state.lastPullStatus = 'success'
noteTransportPull(true)
noteSchedulerPull()
if (!leased.task) {
state.publishFallbackBackoffUntil = Date.now() + pullIntervalMs
return
}
state.publishFallbackBackoffUntil = 0
leasedPublishTask = true
state.leaseInFlightKinds.delete('publish')
await executeLeasedTask(leased, 'db-recovery')
} catch (error) {
state.lastPullAt = Date.now()
state.lastPullStatus = 'failed'
state.lastError = errorMessage(error)
state.publishFallbackBackoffUntil = Date.now() + pullIntervalMs
noteTransportPull(false)
setSchedulerError(state.lastError)
recordActivity('warn', '兜底拉取失败', state.lastError)
} finally {
state.leaseInFlightKinds.delete('publish')
syncSchedulerSurface()
if (!leasedPublishTask && state.running) {
queueMicrotask(() => pumpExecutionLoop())
}
}
}
async function pullMonitoringTasks(routing: RuntimeTaskRouting): Promise<void> {
const leaseLimit = resolveLegacyMonitoringLeaseLimit()
if (!canRunLive(state.session) || isLeaseInFlight('monitor') || leaseLimit <= 0) {
return
}
state.leaseInFlightKinds.add('monitor')
try {
const leased = await leaseMonitoringTasks({
limit: leaseLimit,
})
state.lastPullAt = Date.now()
state.lastPullStatus = 'success'
noteTransportPull(true)
noteSchedulerPull()
enqueueLeasedMonitoringTasks(leased.tasks, routing)
} catch (error) {
state.lastPullAt = Date.now()
state.lastPullStatus = 'failed'
state.lastError = errorMessage(error)
noteTransportPull(false)
setSchedulerError(state.lastError)
recordActivity('warn', '监控任务拉取失败', state.lastError)
} finally {
state.leaseInFlightKinds.delete('monitor')
syncSchedulerSurface()
}
}
async function resumeLeasedMonitoringTasks(source: 'startup' | 'reconnect'): Promise<void> {
if (!canRunLive(state.session)) {
return
}
try {
const resumed = await resumeMonitoringTasks({})
enqueueLeasedMonitoringTasks(resumed.tasks, 'db-recovery')
if (resumed.tasks.length > 0) {
recordActivity(
'info',
'已恢复监控租约',
`从服务端恢复了 ${resumed.tasks.length} 条未完成监控任务(${source})。`,
)
}
void pullNextTask('monitor')
} catch (error) {
recordActivity('warn', '恢复监控租约失败', errorMessage(error))
}
}
function enqueueLeasedMonitoringTasks(
tasks: MonitoringLeaseTask[],
routing: RuntimeTaskRouting,
): void {
if (tasks.length === 0) {
return
}
for (const task of tasks) {
const taskId = String(task.task_id)
const accountId = resolveMonitoringAccountID(task.ai_platform_id)
const now = Date.now()
state.tasks.set(taskId, {
id: taskId,
jobId: taskId,
title: buildMonitoringTaskTitle(task),
kind: 'monitor',
platform: task.ai_platform_id,
accountId,
accountName: accountId
? resolveAccountName(accountId, task.platform_name)
: task.platform_name,
clientId: state.client?.id ?? taskId,
status: 'queued',
routing,
leaseExpiresAt: parseTimestamp(task.lease_expires_at),
startedAt: null,
updatedAt: now,
summary: `已领取监控租约,等待本地调度执行(${describeRouting(routing)})。`,
payload: {
title: buildMonitoringTaskTitle(task),
question_id: task.question_id,
question_hash: task.question_hash,
question_text: task.question_text,
ai_platform_id: task.ai_platform_id,
platform_name: task.platform_name,
collector_type: task.collector_type,
run_mode: task.run_mode,
trigger_source: task.trigger_source,
business_date: task.business_date,
priority: typeof task.priority === 'number' ? task.priority : null,
lane: typeof task.lane === 'string' ? task.lane : null,
monitoring_task_id: task.task_id,
lease_token: task.lease_token,
lease_expires_at: task.lease_expires_at,
scheduler_group_key: task.question_hash || `qid:${task.question_id}`,
legacy_monitoring_task: true,
},
error: null,
result: null,
attemptId: null,
leaseToken: task.lease_token,
})
enqueueMonitorLeaseTask({
taskId,
jobId: taskId,
accountId,
clientId: state.client?.id ?? taskId,
platform: task.ai_platform_id,
routing,
priority:
typeof task.priority === 'number'
? task.priority
: task.trigger_source === 'manual'
? 5000
: 100,
lane:
typeof task.lane === 'string'
? task.lane
: task.trigger_source === 'manual'
? 'high'
: 'normal',
title: buildMonitoringTaskTitle(task),
businessDate: task.business_date,
questionKey: task.question_hash || `qid:${task.question_id}`,
questionText: task.question_text,
})
}
syncSchedulerSurface()
pumpExecutionLoop()
}
async function executeLeasedMonitoringTask(
taskId: string,
_routing: RuntimeTaskRouting,
): Promise<void> {
const taskRecord = state.tasks.get(taskId)
if (!taskRecord || taskRecord.kind !== 'monitor' || !taskRecord.leaseToken) {
noteMonitorTaskLeaseMiss(taskId)
return
}
const monitoringTaskID = Number.parseInt(taskId, 10)
if (!Number.isFinite(monitoringTaskID)) {
noteMonitorTaskCanceled(taskId)
state.tasks.delete(taskId)
return
}
setSchedulerPhase('running')
setActiveLease({
taskId,
attemptId: null,
leaseExpiresAt: isoFromTimestamp(taskRecord.leaseExpiresAt),
})
recordActivity('info', '监控任务开始执行', `${taskRecord.title} 已进入本地执行队列。`)
const abortController = new AbortController()
state.activeExecutions.set(taskRecord.id, createActiveExecution(taskRecord, abortController))
noteMonitorTaskActivated(taskId)
syncSchedulerSurface()
queueMicrotask(() => pumpExecutionLoop())
try {
const execution =
(await resolveAuthorizationPreflightBlock(taskRecord)) ??
withAuthorizationRetryPolicy(
taskRecord,
await executeTaskAdapter(taskRecord, abortController.signal),
)
if (execution.status === 'unknown') {
const skipReason = monitoringSkipReasonFromExecution(execution)
await skipMonitoringTask(monitoringTaskID, {
lease_token: taskRecord.leaseToken,
skip_reason: skipReason,
error_message: execution.summary,
completed_at: new Date().toISOString(),
})
const existing = state.tasks.get(taskId)
if (existing) {
existing.status = 'aborted'
existing.summary = execution.summary
existing.error = execution.error ?? null
existing.updatedAt = Date.now()
existing.leaseToken = null
state.tasks.set(taskId, existing)
}
noteLeaseReleased('completed', taskId)
noteMonitorTaskCompleted(taskId)
recordActivity(
skipReason === 'stale_business_date' ? 'info' : 'warn',
skipReason === 'stale_business_date' ? '监控任务已按跨日策略丢弃' : '监控任务结果待确认',
`${taskRecord.title} ${execution.summary}`,
)
return
}
await submitMonitoringTaskResult(
monitoringTaskID,
buildMonitoringResultPayload(taskRecord.leaseToken, execution),
)
const existing = state.tasks.get(taskId)
if (existing) {
existing.status = execution.status
existing.summary = execution.summary
existing.result = execution.payload ?? null
existing.error = execution.error ?? null
existing.updatedAt = Date.now()
existing.leaseToken = null
state.tasks.set(taskId, existing)
}
noteLeaseReleased(execution.status === 'failed' ? 'failed' : 'completed', taskId)
noteMonitorTaskCompleted(taskId)
recordActivity(
execution.status === 'failed' ? 'danger' : 'success',
execution.status === 'failed' ? '监控任务执行失败' : '监控任务执行成功',
`${taskRecord.title} ${execution.summary}`,
)
} catch (error) {
if (isMonitorAbortReason(taskRecord, error, 'stale_business_date')) {
let dropSummary = staleMonitoringDropSummary(taskRecord)
try {
await skipMonitoringTask(monitoringTaskID, {
lease_token: taskRecord.leaseToken,
skip_reason: 'stale_business_date',
error_message: dropSummary,
completed_at: new Date().toISOString(),
})
} catch (skipError) {
dropSummary = `${dropSummary}skip 回写失败:${errorMessage(skipError)}`
}
const existing = state.tasks.get(taskId)
if (existing) {
existing.status = 'aborted'
existing.summary = dropSummary
existing.error = {
...toStructuredError(error),
dropped_reason: 'stale_business_date',
}
existing.updatedAt = Date.now()
existing.leaseToken = null
state.tasks.set(taskId, existing)
}
noteLeaseReleased('completed', taskId)
noteMonitorTaskCompleted(taskId)
recordActivity('info', '监控任务已按跨日策略丢弃', dropSummary)
return
}
const message = errorMessage(error)
const existing = state.tasks.get(taskId)
if (existing) {
existing.status = 'failed'
existing.summary = message
existing.error = toStructuredError(error)
existing.updatedAt = Date.now()
existing.leaseToken = null
state.tasks.set(taskId, existing)
}
noteLeaseReleased('failed', taskId)
noteMonitorTaskCompleted(taskId)
recordActivity('danger', '监控任务执行失败', `${taskRecord.title} 执行失败:${message}`)
} finally {
state.activeExecutions.delete(taskId)
syncSchedulerSurface()
noteSchedulerTaskFinished()
pumpExecutionLoop()
}
}
function buildMonitoringTaskTitle(task: MonitoringLeaseTask): string {
const base = task.question_text.trim()
if (!base) {
return `监控任务 · ${task.platform_name}`
}
return `${task.platform_name} · ${base}`
}
function resolveMonitoringAccountID(platform: string): string {
const live = state.accounts.find(
(account) =>
account.platform === platform &&
getProjectedAccountHealth({
accountId: account.id,
platform: account.platform,
health: account.health,
verifiedAt: account.verified_at,
}).health === 'live',
)
if (live) {
return live.id
}
return state.accounts.find((account) => account.platform === platform)?.id ?? ''
}
function buildMonitoringResultPayload(
leaseToken: string,
execution: AdapterExecutionResult,
): MonitoringTaskResultPayload {
const payload = execution.payload ?? {}
const status = execution.status === 'succeeded' ? 'succeeded' : 'failed'
const rawResponseJSON = asRecord(payload.raw_response_json)
const failedRawResponseJSON =
status === 'failed' && execution.error
? {
...rawResponseJSON,
runtime_error: execution.error,
}
: rawResponseJSON
return {
lease_token: leaseToken,
status,
provider_model: asOptionalString(payload.provider_model),
provider_request_id: asOptionalString(payload.provider_request_id),
request_id: asOptionalString(payload.request_id),
answer: asOptionalString(payload.answer),
raw_response_json: failedRawResponseJSON,
citations: asSourceItems(payload.citations),
search_results: asSourceItems(payload.search_results),
brand_mentioned: asOptionalBoolean(payload.brand_mentioned),
brand_mention_position: asOptionalString(payload.brand_mention_position),
first_recommended: asOptionalBoolean(payload.first_recommended),
sentiment_label: asOptionalString(payload.sentiment_label),
matched_brand_terms: asStringArray(payload.matched_brand_terms),
error_message: status === 'failed' ? execution.summary : undefined,
completed_at: new Date().toISOString(),
}
}
function buildMonitoringFailurePayload(
leaseToken: string,
failureMessage: string,
structuredError: Record<string, JsonValue>,
task?: RuntimeTaskRecord,
): MonitoringTaskResultPayload {
const runtimeError = task
? (withAuthorizationRetryPolicy(task, {
status: 'failed',
summary: failureMessage,
error: structuredError,
}).error ?? structuredError)
: structuredError
return {
lease_token: leaseToken,
status: 'failed',
raw_response_json: {
runtime_error: runtimeError,
},
error_message: failureMessage,
completed_at: new Date().toISOString(),
}
}
async function executeLeasedDesktopMonitorTask(
taskRecord: RuntimeTaskRecord,
leased: LeaseDesktopTaskResponse,
routing: RuntimeTaskRouting,
signal: AbortSignal,
): Promise<void> {
const monitoringTaskID = extractMonitoringTaskID(taskRecord.payload)
if (!monitoringTaskID) {
const missingMessage = 'monitor desktop task is missing monitoring_task_id'
const completed = await completeDesktopTask(taskRecord.id, {
lease_token: leased.lease_token as string,
status: 'failed',
error: {
code: 'monitoring_task_id_missing',
message: missingMessage,
},
})
upsertTaskFromInfo(completed, {
routing,
summary: missingMessage,
attemptId: null,
leaseToken: null,
})
noteLeaseReleased('failed', taskRecord.id)
noteMonitorTaskCompleted(taskRecord.id)
recordActivity(
'danger',
'监控任务执行失败',
`${taskRecord.title} 缺少 monitoring_task_id,无法回写结果。`,
)
return
}
try {
const execution =
(await resolveAuthorizationPreflightBlock(taskRecord)) ??
withAuthorizationRetryPolicy(taskRecord, await executeTaskAdapter(taskRecord, signal))
if (execution.status === 'unknown') {
const skipReason = monitoringSkipReasonFromExecution(execution)
await skipMonitoringTask(monitoringTaskID, {
lease_token: leased.lease_token as string,
skip_reason: skipReason,
error_message: execution.summary,
completed_at: new Date().toISOString(),
})
if (skipReason === 'stale_business_date') {
const canceled = await cancelDesktopTask(taskRecord.id, {
lease_token: leased.lease_token as string,
reason: skipReason,
}).catch((cancelError) => {
const existing = state.tasks.get(taskRecord.id)
if (existing) {
existing.status = 'aborted'
existing.error = {
...(execution.error ?? {}),
cancel_error: errorMessage(cancelError),
dropped_reason: 'stale_business_date',
}
existing.summary = `${execution.summary}desktop task 取消失败:${errorMessage(cancelError)}`
existing.updatedAt = Date.now()
existing.leaseToken = null
state.tasks.set(taskRecord.id, existing)
}
return null
})
if (canceled) {
upsertTaskFromInfo(canceled, {
routing,
summary: execution.summary,
attemptId: null,
leaseToken: null,
})
}
noteLeaseReleased('completed', taskRecord.id)
noteMonitorTaskCompleted(taskRecord.id)
recordActivity(
'info',
'监控任务已按跨日策略丢弃',
`${taskRecord.title} ${execution.summary}`,
)
return
}
const completed = await completeDesktopTask(taskRecord.id, {
lease_token: leased.lease_token as string,
status: 'unknown',
error: execution.error ?? {
code: 'runtime_unknown',
message: execution.summary,
},
})
upsertTaskFromInfo(completed, {
routing,
summary: execution.summary,
attemptId: null,
leaseToken: null,
})
noteLeaseReleased('completed', taskRecord.id)
noteMonitorTaskCompleted(taskRecord.id)
recordActivity('warn', '监控任务结果待确认', `${taskRecord.title} ${execution.summary}`)
return
}
await submitMonitoringTaskResult(
monitoringTaskID,
buildMonitoringResultPayload(leased.lease_token as string, execution),
)
const existing = state.tasks.get(taskRecord.id)
if (existing) {
existing.status = execution.status
existing.summary = execution.summary
existing.result = execution.payload ?? null
existing.error = execution.error ?? null
existing.updatedAt = Date.now()
existing.attemptId = null
existing.leaseToken = null
state.tasks.set(taskRecord.id, existing)
}
noteLeaseReleased(execution.status === 'failed' ? 'failed' : 'completed', taskRecord.id)
noteMonitorTaskCompleted(taskRecord.id)
recordActivity(
execution.status === 'failed' ? 'danger' : 'success',
execution.status === 'failed' ? '监控任务执行失败' : '监控任务执行成功',
`${taskRecord.title} ${execution.summary}`,
)
} catch (error) {
if (isMonitorAbortReason(taskRecord, error, 'stale_business_date')) {
let dropSummary = staleMonitoringDropSummary(taskRecord)
try {
await skipMonitoringTask(monitoringTaskID, {
lease_token: leased.lease_token as string,
skip_reason: 'stale_business_date',
error_message: dropSummary,
completed_at: new Date().toISOString(),
})
} catch (skipError) {
dropSummary = `${dropSummary}monitoring skip 回写失败:${errorMessage(skipError)}`
}
const canceled = await cancelDesktopTask(taskRecord.id, {
lease_token: leased.lease_token as string,
reason: 'stale_business_date',
}).catch((cancelError) => {
const existing = state.tasks.get(taskRecord.id)
if (existing) {
existing.status = 'aborted'
existing.error = {
...toStructuredError(error),
cancel_error: errorMessage(cancelError),
dropped_reason: 'stale_business_date',
}
existing.summary = `${dropSummary}desktop task 取消失败:${errorMessage(cancelError)}`
existing.updatedAt = Date.now()
existing.leaseToken = null
state.tasks.set(taskRecord.id, existing)
}
return null
})
if (canceled) {
upsertTaskFromInfo(canceled, {
routing,
summary: dropSummary,
attemptId: null,
leaseToken: null,
})
}
noteLeaseReleased('completed', taskRecord.id)
noteMonitorTaskCompleted(taskRecord.id)
recordActivity('info', '监控任务已按跨日策略丢弃', dropSummary)
return
}
if (isDesktopTaskMonitorInterrupt(taskRecord, error)) {
const canceled = await cancelDesktopTask(taskRecord.id, {
lease_token: leased.lease_token as string,
reason: interruptReasonFromTask(taskRecord),
})
upsertTaskFromInfo(canceled, {
routing,
summary: '已响应 Phase 2 抢占请求,当前监控任务已回队等待 retry。',
attemptId: null,
leaseToken: null,
})
noteLeaseReleased('completed', taskRecord.id)
noteMonitorTaskCompleted(taskRecord.id)
recordActivity('warn', '监控任务已让路', `${taskRecord.title} 已安全中止,并回队为 retry。`)
return
}
const failureMessage = errorMessage(error)
const structuredError = toStructuredError(error)
let monitoringResultWriteError: unknown = null
try {
await submitMonitoringTaskResult(
monitoringTaskID,
buildMonitoringFailurePayload(
leased.lease_token as string,
failureMessage || '监控任务执行失败。',
structuredError,
taskRecord,
),
)
} catch (submitError) {
monitoringResultWriteError = submitError
}
const failureSummary = monitoringResultWriteError
? `${failureMessage || '监控任务执行失败。'}monitoring 失败回传失败:${errorMessage(monitoringResultWriteError)}`
: failureMessage || '监控任务执行失败。'
const existing = state.tasks.get(taskRecord.id)
if (existing) {
existing.status = 'failed'
existing.error = structuredError
existing.summary = failureSummary
existing.updatedAt = Date.now()
existing.attemptId = null
existing.leaseToken = null
state.tasks.set(taskRecord.id, existing)
}
noteLeaseReleased('failed', taskRecord.id)
noteMonitorTaskCompleted(taskRecord.id)
recordActivity(
'danger',
'监控任务执行失败',
`${taskRecord.title} 执行失败:${failureSummary || '未知错误'}`,
)
}
}
function extractMonitoringTaskID(payload: Record<string, JsonValue>): number | null {
const raw = payload.monitoring_task_id
if (typeof raw === 'number' && Number.isFinite(raw)) {
return raw
}
if (typeof raw === 'string' && raw.trim()) {
const parsed = Number.parseInt(raw.trim(), 10)
if (Number.isFinite(parsed)) {
return parsed
}
}
return null
}
function noteMonitorExecutionSafePoint(taskId: string, phase: MonitorExecutionPhase | null): void {
const active = state.activeExecutions.get(taskId)
if (!active || active.kind !== 'monitor') {
return
}
if (phase) {
active.executionPhase = phase
}
active.lastSafePointAt = Date.now()
active.lastProgressAt = Date.now()
state.activeExecutions.set(taskId, active)
const taskRecord = state.tasks.get(taskId)
if (taskRecord?.kind === 'monitor') {
const staleBusinessDate = resolveStaleMonitoringBusinessDate(taskRecord.payload)
if (staleBusinessDate) {
requestMonitorStaleBusinessDateDrop(taskId, staleBusinessDate)
return
}
}
if (active.interruptRequested && !active.abortController.signal.aborted) {
active.abortController.abort()
}
}
function requestMonitorStaleBusinessDateDrop(taskId: string, staleBusinessDate: string): void {
const active = state.activeExecutions.get(taskId)
if (!active || active.kind !== 'monitor') {
return
}
active.interruptReason = 'stale_business_date'
active.lastSafePointAt = Date.now()
active.lastProgressAt = Date.now()
state.activeExecutions.set(taskId, active)
const existing = state.tasks.get(taskId)
if (existing) {
existing.payload = {
...existing.payload,
interrupt_reason: 'stale_business_date',
dropped_reason: 'stale_business_date',
}
existing.summary = staleMonitoringDropSummary(existing, staleBusinessDate)
existing.updatedAt = Date.now()
state.tasks.set(taskId, existing)
}
if (!active.abortController.signal.aborted) {
active.abortController.abort()
}
}
function monitorExecutionPhaseFromStage(stage: string): MonitorExecutionPhase {
const normalized = stage.trim().toLowerCase()
if (!normalized) {
return 'WAITING'
}
if (normalized.includes('bootstrap') || normalized.includes('page_ready')) {
return 'NAVIGATING'
}
if (normalized.includes('login') || normalized.includes('runtime_state')) {
return 'AUTHENTICATING'
}
if (normalized.includes('input')) {
return 'INPUTTING'
}
if (normalized.includes('query') || normalized.includes('submit')) {
return 'SUBMITTING'
}
if (normalized.includes('parse')) {
return 'PARSING'
}
return 'WAITING'
}
function isDesktopTaskMonitorInterrupt(taskRecord: RuntimeTaskRecord, error: unknown): boolean {
return (
isDesktopTaskID(taskRecord.id) && isMonitorAbortReason(taskRecord, error, 'collect_now_preempt')
)
}
function isMonitorAbortReason(
taskRecord: RuntimeTaskRecord,
error: unknown,
reason: string,
): boolean {
return (
errorMessage(error).includes('adapter_aborted') && monitorAbortReason(taskRecord) === reason
)
}
function monitorAbortReason(taskRecord: RuntimeTaskRecord): string | null {
const active = state.activeExecutions.get(taskRecord.id)
if (active?.interruptReason?.trim()) {
return active.interruptReason.trim()
}
const raw = taskRecord.payload.interrupt_reason
if (typeof raw === 'string' && raw.trim()) {
return raw.trim()
}
if (Boolean(taskRecord.payload.interrupt_requested) || Boolean(active?.interruptRequested)) {
return 'collect_now_preempt'
}
return null
}
function interruptReasonFromTask(taskRecord: RuntimeTaskRecord): string {
return monitorAbortReason(taskRecord) ?? 'collect_now_preempt'
}
function isDesktopTaskID(taskId: string): boolean {
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
taskId.trim(),
)
}
async function executeLeasedTask(
leased: LeaseDesktopTaskResponse,
routing: RuntimeTaskRouting,
): Promise<void> {
if (!leased.task || !leased.lease_token) {
return
}
const task = leased.task
setSchedulerPhase('running')
setActiveLease({
taskId: task.id,
attemptId: leased.attempt_id ?? null,
leaseExpiresAt: leased.lease_expires_at ?? task.lease_expires_at ?? null,
})
const taskRecord = upsertTaskFromInfo(task, {
routing,
attemptId: leased.attempt_id ?? null,
leaseToken: leased.lease_token,
summary: `已领取租约,开始执行 ${routing === 'rabbitmq-primary' ? 'MQ 唤醒' : '兜底拉取'} 消费。`,
})
recordActivity(
'info',
'任务已领取',
`${taskRecord.title} 已由当前客户端领取,路由:${describeRouting(routing)}`,
)
const abortController = new AbortController()
const activeExecution = createActiveExecution(taskRecord, abortController)
armPublishTaskDeadline(taskRecord, activeExecution)
state.activeExecutions.set(taskRecord.id, activeExecution)
if (task.kind === 'monitor') {
noteMonitorTaskLeased(task, routing)
} else {
notePublishTaskActivated(taskRecord.id, taskRecord.platform)
emitRuntimeInvalidated('publish-task-lease', { taskId: taskRecord.id })
}
syncSchedulerSurface()
queueMicrotask(() => pumpExecutionLoop())
const extendHandle = setInterval(() => {
void extendActiveLease(taskRecord.id, leased.lease_token as string)
}, leaseExtendIntervalMs)
try {
if (task.kind === 'monitor') {
await executeLeasedDesktopMonitorTask(taskRecord, leased, routing, abortController.signal)
return
}
const execution =
(await resolveAuthorizationPreflightBlock(taskRecord)) ??
withAuthorizationRetryPolicy(
taskRecord,
await executeTaskAdapter(taskRecord, abortController.signal),
)
const completed = await completeDesktopTask(taskRecord.id, {
lease_token: leased.lease_token,
status: execution.status,
payload: execution.payload,
error: execution.error,
})
noteLeaseReleased(execution.status === 'failed' ? 'failed' : 'completed', taskRecord.id)
upsertTaskFromInfo(completed, {
routing,
summary: execution.summary,
attemptId: null,
leaseToken: null,
})
recordActivity(
execution.status === 'failed'
? 'danger'
: execution.status === 'unknown'
? 'warn'
: 'success',
execution.status === 'failed'
? '任务执行失败'
: execution.status === 'unknown'
? '任务执行结果待确认'
: '任务执行成功',
`${taskRecord.title} ${execution.summary}`,
)
} catch (error) {
const failureMessage = errorMessage(error)
const structuredError = toStructuredError(error)
const accountIdentity = accountIdentityFromTask(taskRecord)
// If a publish task was interrupted AFTER it may have entered the irreversible submit phase,
// the article might already be live on the platform. Completing as a clean 'failed' would drop
// it from the server dedup set and let a retry double-post. Complete as 'unknown' instead so it
// stays for manual reconcile. (A pre-submit interruption is a true failure and stays 'failed'.)
const submitMaybeStarted =
taskRecord.kind === 'publish' && activeExecution.submitStartedAt !== null
const terminalStatus: 'failed' | 'unknown' = submitMaybeStarted ? 'unknown' : 'failed'
const interruptedSummary = '发布可能已提交到平台,结果待人工确认,已避免自动重复发布。'
// Tag the persisted error so the desktop UI can require explicit confirmation before a manual
// retry of a possibly-already-published task (mirrors the server recovery payload marker).
const terminalError = submitMaybeStarted
? { ...structuredError, publish_submit_uncertain: true }
: structuredError
// A submit-phase interruption is typically a network/timeout stall, not an auth problem — don't
// wrongly degrade account health for it.
if (accountIdentity && !submitMaybeStarted) {
await reportAccountFailure(accountIdentity, {
summary: failureMessage,
error: structuredError,
}).catch((reportError) => {
console.warn('[desktop-runtime] thrown failure report failed', {
taskId: taskRecord.id,
accountId: accountIdentity.id,
platform: accountIdentity.platform,
message: reportError instanceof Error ? reportError.message : String(reportError),
})
})
}
try {
const completed = await completeDesktopTask(taskRecord.id, {
lease_token: leased.lease_token,
status: terminalStatus,
error: terminalError,
})
upsertTaskFromInfo(completed, {
routing,
summary: submitMaybeStarted ? interruptedSummary : failureMessage || '桌面任务执行失败。',
attemptId: null,
leaseToken: null,
})
} catch (completeError) {
const existing = state.tasks.get(taskRecord.id)
if (existing) {
existing.status = terminalStatus
existing.error = terminalError
existing.summary = submitMaybeStarted
? `${interruptedSummary}result 回写失败:${errorMessage(completeError)}`
: `${failureMessage || '桌面任务执行失败。'}result 回写失败:${errorMessage(completeError)}`
existing.updatedAt = Date.now()
state.tasks.set(taskRecord.id, existing)
}
}
noteLeaseReleased('failed', taskRecord.id)
recordActivity(
submitMaybeStarted ? 'warn' : 'danger',
submitMaybeStarted ? '发布结果待确认' : '任务执行失败',
submitMaybeStarted
? `${taskRecord.title} 进入提交阶段后中断,可能已发布,已转人工确认以避免重复发文。`
: `${taskRecord.title} 执行失败:${failureMessage || '未知错误'}`,
)
} finally {
clearInterval(extendHandle)
if (activeExecution.deadlineHandle) {
clearTimeout(activeExecution.deadlineHandle)
activeExecution.deadlineHandle = null
}
if (taskRecord.kind === 'publish') {
notePublishTaskCompleted(taskRecord.id, taskRecord.platform)
}
state.activeExecutions.delete(taskRecord.id)
syncSchedulerSurface()
noteSchedulerTaskFinished()
pumpExecutionLoop()
}
}
async function extendActiveLease(taskId: string, leaseToken: string): Promise<void> {
const existing = state.tasks.get(taskId)
const active = state.activeExecutions.get(taskId)
if (existing?.kind === 'monitor') {
const staleBusinessDate = resolveStaleMonitoringBusinessDate(existing.payload)
if (staleBusinessDate) {
requestMonitorStaleBusinessDateDrop(taskId, staleBusinessDate)
return
}
}
if (existing?.kind === 'publish' && active) {
const idleMs = Date.now() - active.lastProgressAt
if (idleMs >= publishTaskStaleProgressMs) {
const detail = active.lastProgressSummary ? `,最后进度:${active.lastProgressSummary}` : ''
existing.summary = `发布任务超过 ${Math.round(idleMs / 1000)} 秒没有新进度,已中止并释放,准备由服务端重排${detail}`
existing.updatedAt = Date.now()
state.tasks.set(taskId, existing)
recordActivity('warn', '发布任务疑似卡住', `${existing.title} ${existing.summary}`)
// Abort the local execution instead of merely letting the lease lapse: this
// releases the slot immediately and, crucially, closes the window where the
// server could re-queue the task while a stalled adapter is still mid-publish
// (which would double-post a non-idempotent article).
if (!active.abortController.signal.aborted) {
active.abortController.abort(new Error('publish_stale_progress'))
}
return
}
}
try {
const task = await extendDesktopTask(taskId, { lease_token: leaseToken })
noteLeaseExtended(task.lease_expires_at ?? null, taskId)
const existing = state.tasks.get(taskId)
if (existing) {
existing.leaseExpiresAt = parseTimestamp(task.lease_expires_at) ?? existing.leaseExpiresAt
existing.updatedAt = Date.now()
existing.summary =
existing.kind === 'publish'
? (active?.lastProgressSummary ?? '租约已自动续期,发布任务仍在执行。')
: '租约已自动续期,任务仍在执行。'
state.tasks.set(taskId, existing)
}
} catch (error) {
const message = errorMessage(error)
state.lastError = message
setSchedulerError(message)
recordActivity('warn', '租约续期失败', `${taskId} 续租失败:${message}`)
}
}
async function executeTaskAdapter(
task: RuntimeTaskRecord,
signal: AbortSignal,
): Promise<AdapterExecutionResult> {
const accountIdentity = accountIdentityFromTask(task)
if (accountIdentity && shouldEnforceActiveAccount(task)) {
const readiness = await ensureAccountReady(accountIdentity)
if (readiness.authState !== 'active') {
return buildAccountBlockedResult(task, readiness)
}
}
const sessionHandle = createSessionHandle(task.accountId || task.id)
const payload = task.payload
if (task.kind === 'publish') {
const adapter = selectPublishAdapter(task.platform)
if (!adapter) {
return buildScaffoldResult(task, payload, 'publish adapter is not implemented yet')
}
const viewHandle =
adapter.executionMode === 'session' || adapter.executionMode === 'playwright'
? null
: retainHotView(task.accountId || task.id)
const playwrightLease =
adapter.executionMode === 'playwright'
? await retainHiddenPlaywrightPage({
accountId: task.accountId || task.id,
session: sessionHandle.session,
targetURL: resolvePlaywrightTargetURL(task.platform, payload),
title: `${task.title} · Hidden Playwright`,
})
: null
try {
const result = await adapter.publish(
{
taskId: task.id,
accountId: task.accountId || task.id,
session: sessionHandle.session,
view: viewHandle?.view ?? null,
playwright: playwrightLease
? {
browser: playwrightLease.browser,
page: playwrightLease.page,
}
: null,
signal,
phase: 'initial',
article: await loadPublishArticle(task),
async reportProgress(stage: string) {
noteMonitorExecutionSafePoint(task.id, null)
updateTaskProgress(task.id, `publish adapter progress: ${stage}`)
await markPublishSubmitStartedIfNeeded(task.id, stage)
},
},
payload,
)
const active = state.activeExecutions.get(task.id)
const normalizedResult =
active?.submitStartedAt != null ? asSubmitUncertainExecution(result) : result
await maybeReportTaskAuthFailure(task, normalizedResult, accountIdentity)
return normalizedResult
} finally {
await playwrightLease?.release()
if (viewHandle) {
releaseHotView(viewHandle.accountId)
}
}
}
const staleBusinessDate = resolveStaleMonitoringBusinessDate(payload)
if (staleBusinessDate) {
return {
status: 'unknown',
payload: {
...payload,
dropped_by_client: true,
dropped_reason: 'stale_business_date',
},
error: {
code: 'desktop_monitor_task_stale',
message: 'monitor task is stale and has been dropped by desktop client',
business_date: staleBusinessDate,
},
summary: `${task.title} 已过业务日 ${staleBusinessDate},按漏采策略直接丢弃。`,
}
}
const adapter = selectMonitorAdapter(task.platform)
if (!adapter) {
return buildScaffoldResult(task, payload, 'monitor adapter is not implemented yet')
}
const viewHandle =
adapter.executionMode === 'session' || adapter.executionMode === 'playwright'
? null
: retainHotView(task.accountId || task.id)
const playwrightLease =
adapter.executionMode === 'playwright'
? await retainHiddenPlaywrightPage({
accountId: task.accountId || task.id,
session: sessionHandle.session,
targetURL: resolvePlaywrightTargetURL(task.platform, payload),
title: `${task.title} · Hidden Playwright`,
})
: null
try {
const result = await adapter.query(
{
taskId: task.id,
accountId: task.accountId || task.id,
session: sessionHandle.session,
view: viewHandle?.view ?? null,
playwright: playwrightLease
? {
browser: playwrightLease.browser,
page: playwrightLease.page,
}
: null,
signal,
phase: 'initial',
reportProgress(stage: string) {
noteMonitorExecutionSafePoint(task.id, monitorExecutionPhaseFromStage(stage))
updateTaskProgress(task.id, `monitor adapter progress: ${stage}`)
},
},
payload,
)
await maybeReportTaskAuthFailure(task, result, accountIdentity)
return result
} finally {
await playwrightLease?.release()
if (viewHandle) {
releaseHotView(viewHandle.accountId)
}
}
}
function shouldEnforceActiveAccount(task: RuntimeTaskRecord): boolean {
if (task.kind === 'publish') {
return true
}
if (!isAIPlatformId(task.platform)) {
return true
}
return authRequiredMonitorPlatforms.has(task.platform)
}
function buildAccountBlockedResult(
task: RuntimeTaskRecord,
readiness: Awaited<ReturnType<typeof ensureAccountReady>>,
): AdapterExecutionResult {
if (readiness.authState === 'challenge_required') {
return {
status: 'failed',
summary: accountActionRequiredSummary(task.platform, readiness.authReason),
error: markAuthorizationFailureNonRetryable(
{
code:
readiness.authReason === 'risk_control'
? 'desktop_account_risk_control'
: 'desktop_account_challenge_required',
message:
readiness.authReason === 'risk_control'
? 'desktop account risk control triggered'
: 'desktop account challenge required',
},
isAIPlatformId(task.platform) ? 'ai_platform_authorization' : 'media_account_authorization',
),
}
}
if (readiness.authState === 'expired' || readiness.authState === 'revoked') {
return {
status: 'failed',
summary: `${task.accountName} 登录态已失效,请重新授权后再试。`,
error: markAuthorizationFailureNonRetryable(
{
code: 'desktop_account_auth_expired',
message: 'desktop account auth expired',
},
isAIPlatformId(task.platform) ? 'ai_platform_authorization' : 'media_account_authorization',
),
}
}
return {
status: 'unknown',
summary: `${task.accountName} 最近校验失败,已暂缓执行任务。`,
error: {
code: 'desktop_account_probe_pending',
message: 'desktop account readiness pending',
},
}
}
async function maybeReportTaskAuthFailure(
task: RuntimeTaskRecord,
result: AdapterExecutionResult,
accountIdentity: PublishAccountIdentity | null,
): Promise<void> {
if (!accountIdentity) {
return
}
const previousSnapshot = getAccountHealthSnapshot(accountIdentity.id)
const nextSnapshot = await reportAccountFailure(accountIdentity, {
summary: result.summary,
error: result.error ?? null,
}).catch((error) => {
console.warn('[desktop-runtime] account failure report failed', {
taskId: task.id,
accountId: accountIdentity.id,
platform: accountIdentity.platform,
message: error instanceof Error ? error.message : String(error),
})
return null
})
maybeRecordAccountAccessAlert(task, accountIdentity, previousSnapshot, nextSnapshot)
if (nextSnapshot && nextSnapshot.authState !== 'active') {
enqueueAccountHealthReport(accountIdentity.id)
}
}
function resolvePlaywrightTargetURL(platform: string, payload: Record<string, JsonValue>): string {
const payloadURL = resolveStringField(payload, [
'target_url',
'targetUrl',
'console_url',
'consoleUrl',
'url',
])
if (payloadURL) {
return payloadURL
}
const aiPlatform = getAIPlatformCatalogItem(platform)
if (aiPlatform?.consoleUrl) {
return aiPlatform.consoleUrl
}
return 'about:blank'
}
function buildScaffoldResult(
task: RuntimeTaskRecord,
adapterPayload: Record<string, JsonValue>,
detail: string,
): AdapterExecutionResult {
return {
status: 'failed',
payload: {
...adapterPayload,
runtime_mode: 'desktop-scaffold',
task_kind: task.kind,
task_platform: task.platform,
},
error: {
code: 'desktop_task_adapter_missing',
message: 'desktop runtime adapter is not implemented for this platform',
detail,
},
summary: `${task.title} 执行失败:当前平台的 desktop ${task.kind === 'publish' ? '发布' : '监测'}适配器尚未实现。`,
}
}
async function loadPublishArticle(task: RuntimeTaskRecord): Promise<DesktopArticleContent> {
const articleId = resolveArticleId(task.payload)
if (articleId === null) {
throw new Error('desktop_publish_article_id_missing')
}
const article = await getDesktopArticleContent(articleId)
return {
...article,
title: article.title?.trim() || task.title,
}
}
function resolveArticleId(payload: Record<string, JsonValue>): number | null {
const direct = toPositiveInt(payload.article_id)
if (direct !== null) {
return direct
}
const contentRef = payload.content_ref
if (!contentRef || typeof contentRef !== 'object' || Array.isArray(contentRef)) {
return null
}
const typed = contentRef as Record<string, JsonValue>
return toPositiveInt(typed.id ?? typed.article_id ?? null)
}
function toPositiveInt(value: JsonValue | null): number | null {
if (typeof value === 'number' && Number.isInteger(value) && value > 0) {
return value
}
if (typeof value === 'string' && /^\d+$/.test(value.trim())) {
const parsed = Number.parseInt(value.trim(), 10)
return parsed > 0 ? parsed : null
}
return null
}
function resolveStringField(payload: Record<string, JsonValue>, keys: string[]): string | null {
for (const key of keys) {
const value = payload[key]
if (typeof value !== 'string') {
continue
}
const normalized = value.trim()
if (normalized) {
return normalized
}
}
return null
}
function resolveStaleMonitoringBusinessDate(payload: Record<string, JsonValue>): string | null {
const businessDate = resolveMonitoringBusinessDate(payload)
if (!businessDate) {
return null
}
return businessDate < currentMonitoringBusinessDate() ? businessDate : null
}
function monitoringSkipReasonFromExecution(execution: AdapterExecutionResult): string {
return isStaleMonitoringExecutionResult(execution) ? 'stale_business_date' : 'runtime_unknown'
}
function isStaleMonitoringExecutionResult(execution: AdapterExecutionResult): boolean {
return execution.payload?.dropped_reason === 'stale_business_date'
}
function staleMonitoringDropSummary(
task: RuntimeTaskRecord,
staleBusinessDate?: string | null,
): string {
const businessDate = staleBusinessDate ?? resolveMonitoringBusinessDate(task.payload)
if (businessDate) {
return `${task.title} 已过业务日 ${businessDate},按漏采策略直接丢弃。`
}
return `${task.title} 已跨业务日,按漏采策略直接丢弃。`
}
function resolveMonitoringBusinessDate(payload: Record<string, JsonValue>): string | null {
const candidates = [
payload.business_date,
payload.businessDate,
payload.metric_date,
payload.date,
]
for (const candidate of candidates) {
if (typeof candidate !== 'string') {
continue
}
const normalized = candidate.trim()
if (/^\d{4}-\d{2}-\d{2}$/.test(normalized)) {
return normalized
}
}
return null
}
function currentMonitoringBusinessDate(now = new Date()): string {
const parts = new Intl.DateTimeFormat('en-US', {
timeZone: monitorBusinessTimeZone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
}).formatToParts(now)
const year = parts.find((part) => part.type === 'year')?.value ?? ''
const month = parts.find((part) => part.type === 'month')?.value ?? ''
const day = parts.find((part) => part.type === 'day')?.value ?? ''
return `${year}-${month}-${day}`
}
function updateTaskProgress(taskId: string, summary: string): void {
const existing = state.tasks.get(taskId)
if (!existing) {
return
}
existing.summary = summary
existing.updatedAt = Date.now()
state.tasks.set(taskId, existing)
const active = state.activeExecutions.get(taskId)
if (active) {
active.lastProgressAt = Date.now()
active.lastProgressSummary = summary
state.activeExecutions.set(taskId, active)
}
if (existing.kind === 'publish') {
emitRuntimeInvalidated('publish-task-progress', { taskId })
}
}
function upsertTaskFromInfo(
task: DesktopTaskInfo,
options: {
routing?: RuntimeTaskRouting
summary?: string
attemptId?: string | null
leaseToken?: string | null
} = {},
): RuntimeTaskRecord {
const existing = state.tasks.get(task.id)
const payload = normalizeJsonObject(task.payload)
const record: RuntimeTaskRecord = {
id: task.id,
jobId: task.job_id,
title: resolveTaskTitle(task, payload, existing),
kind: task.kind,
platform: task.platform,
accountId: task.target_account_id,
accountName: resolveAccountName(task.target_account_id, existing?.accountName),
clientId: task.target_client_id,
status: task.status,
routing: options.routing ?? existing?.routing ?? 'db-recovery',
leaseExpiresAt: parseTimestamp(task.lease_expires_at) ?? null,
startedAt: task.status === 'in_progress' ? (existing?.startedAt ?? Date.now()) : null,
updatedAt: parseTimestamp(task.updated_at) ?? Date.now(),
summary: options.summary ?? existing?.summary ?? summaryFromTaskStatus(task.status),
payload,
error: normalizeJsonObjectOrNull(task.error),
result: normalizeJsonObjectOrNull(task.result),
attemptId:
task.status === 'in_progress'
? (options.attemptId ?? existing?.attemptId ?? task.active_attempt_id ?? null)
: null,
leaseToken:
task.status === 'in_progress' ? (options.leaseToken ?? existing?.leaseToken ?? null) : null,
}
state.tasks.set(task.id, record)
return record
}
function refreshAccountNames(): void {
for (const [taskId, task] of state.tasks.entries()) {
const nextName = resolveAccountName(task.accountId, task.accountName)
if (nextName === task.accountName) {
continue
}
state.tasks.set(taskId, { ...task, accountName: nextName })
}
}
function recordActivity(severity: RuntimeTone, title: string, detail: string): void {
const next: RuntimeControllerActivity = {
id: `rt-${Date.now()}-${(state.activitySeq += 1)}`,
severity,
title,
detail,
at: Date.now(),
}
state.activity.unshift(next)
if (state.activity.length > maxActivityItems) {
state.activity.length = maxActivityItems
}
}
function syncSchedulerSurface(): void {
setSchedulerQueueDepth(localQueueDepth())
setSchedulerCurrentTask(primaryActiveTaskId())
}
function notifyLongRunningPublishTasks(): void {
const now = Date.now()
for (const active of state.activeExecutions.values()) {
if (active.kind !== 'publish' || active.longRunningNotifiedAt) {
continue
}
if (now - active.startedAt < publishLongRunningNotificationMs) {
continue
}
active.longRunningNotifiedAt = now
state.activeExecutions.set(active.taskId, active)
const task = state.tasks.get(active.taskId)
if (task) {
task.summary = `${task.summary || '发布任务仍在执行。'}(已执行 ${formatDurationText(now - active.startedAt)},如网络异常可取消后重试。)`
task.updatedAt = now
state.tasks.set(task.id, task)
recordActivity(
'warn',
'发布等待较久',
`${task.title} 已执行 ${formatDurationText(now - active.startedAt)},可能存在网络异常。`,
)
emitRuntimeInvalidated('publish-task-progress', { taskId: task.id })
}
}
}
function formatDurationText(ms: number): string {
const seconds = Math.max(0, Math.floor(ms / 1000))
const minutes = Math.floor(seconds / 60)
const restSeconds = seconds % 60
if (minutes <= 0) {
return `${restSeconds}`
}
if (restSeconds === 0) {
return `${minutes} 分钟`
}
return `${minutes}${restSeconds}`
}
function localQueueDepth(): number {
const publish = getPublishSchedulerSnapshot()
const monitor = getMonitorSchedulerSnapshot()
return publish.queueDepth + monitor.queuedCount + monitor.leasingCount
}
function primaryActiveTaskId(): string | null {
const active = [...state.activeExecutions.values()]
const publish = active.find((item) => item.kind === 'publish') ?? null
return publish?.taskId ?? active[0]?.taskId ?? null
}
function isLeaseInFlight(kind: 'publish' | 'monitor'): boolean {
return state.leaseInFlightKinds.has(kind)
}
function activePublishCount(): number {
return [...state.activeExecutions.values()].filter((item) => item.kind === 'publish').length
}
function activePublishPlatforms(): Set<string> {
const platforms = new Set(
[...state.activeExecutions.values()]
.filter((item) => item.kind === 'publish')
.map((item) => item.platform),
)
for (const task of getPublishSchedulerSnapshot().tasks) {
if (task.state === 'active' || task.state === 'leasing') {
platforms.add(task.platform)
}
}
return platforms
}
function activeMonitorCount(): number {
return [...state.activeExecutions.values()].filter((item) => item.kind === 'monitor').length
}
function activeMonitorPlatforms(): Set<string> {
const platforms = new Set(
[...state.activeExecutions.values()]
.filter((item) => item.kind === 'monitor')
.map((item) => item.platform),
)
for (const task of getMonitorSchedulerSnapshot().tasks) {
if (task.state === 'active' || task.state === 'leasing') {
platforms.add(task.platform)
}
}
return platforms
}
function activeMonitorQuestionKeys(): Set<string> {
return new Set(
getMonitorSchedulerSnapshot()
.tasks.filter(
(task) =>
(task.state === 'active' || task.state === 'leasing') && Boolean(task.questionKey),
)
.map((task) => task.questionKey as string),
)
}
function hasPublishBacklog(): boolean {
const snapshot = getPublishSchedulerSnapshot()
return snapshot.queuedCount + snapshot.leasingCount > 0
}
function activeAdmissionCount(): number {
const publish = getPublishSchedulerSnapshot()
const monitor = getMonitorSchedulerSnapshot()
return (
state.activeExecutions.size +
publish.leasingCount +
monitor.leasingCount +
state.leaseInFlightKinds.size
)
}
function hasTotalCapacityForNewExecution(): boolean {
return (
activeAdmissionCount() < resolveAdaptiveTotalConcurrency() &&
shouldAdmitNewExecutionUnderRuntimePressure()
)
}
function canStartAnotherPublishExecution(): boolean {
const snapshot = getPublishSchedulerSnapshot()
return (
snapshot.queuedCount > 0 && !isLeaseInFlight('publish') && hasTotalCapacityForNewExecution()
)
}
function shouldPullPublishFallback(): boolean {
return (
!state.dispatchWsConnected &&
Date.now() >= state.publishFallbackBackoffUntil &&
!isLeaseInFlight('publish') &&
!hasPublishBacklog() &&
activePublishCount() === 0 &&
hasTotalCapacityForNewExecution()
)
}
function canStartAnotherMonitorExecution(): boolean {
if (hasPublishBacklog() || isLeaseInFlight('publish') || isLeaseInFlight('monitor')) {
return false
}
const limit = resolveAdaptiveMonitorConcurrency()
if (limit <= 0) {
return false
}
const scheduler = getMonitorSchedulerSnapshot()
return activeMonitorCount() + scheduler.leasingCount < limit && hasTotalCapacityForNewExecution()
}
function resolveLegacyMonitoringLeaseLimit(): number {
if (!canStartAnotherMonitorExecution()) {
return 0
}
const snapshot = getMonitorSchedulerSnapshot()
if (snapshot.queuedCount > 0 || snapshot.leasingCount > 0) {
return 0
}
// Legacy monitoring leases cannot be extended while they sit in the local
// scheduler, so only pull one when it can start immediately.
if (activeMonitorCount() > 0) {
return 0
}
return 1
}
function resolveAdaptiveMonitorConcurrency(): number {
const backgroundBudget = resolveMonitorBackgroundBudget()
if (backgroundBudget <= 0) {
return 0
}
const monitorAccountCount = state.accounts.filter(
(account) =>
isAIPlatformId(account.platform) &&
getProjectedAccountHealth({
accountId: account.id,
platform: account.platform,
health: account.health,
verifiedAt: account.verified_at,
}).health === 'live',
).length
if (monitorAccountCount <= 1) {
return Math.min(monitorAccountCount, backgroundBudget)
}
const metrics = getProcessMetricsSnapshot().latestSample
if (!metrics) {
return Math.min(1, backgroundBudget)
}
const totalCPUUsage = metrics.processes.reduce((sum, item) => sum + item.percentCPUUsage, 0)
const totalPrivateBytesKB = metrics.totals.privateBytes
const processCount = metrics.totals.processCount
const mainPrivateBytesKB = metrics.mainProcess.memory?.private ?? 0
if (
totalCPUUsage >= 180 ||
totalPrivateBytesKB >= 2_500_000 ||
mainPrivateBytesKB >= 1_000_000 ||
processCount >= 40
) {
return Math.min(1, backgroundBudget)
}
return Math.min(2, backgroundBudget)
}
function resolveMonitorBackgroundBudget(): number {
if (hasPublishBacklog() || isLeaseInFlight('publish')) {
return 0
}
return Math.max(0, resolveAdaptiveTotalConcurrency() - publishReservedSlots)
}
function resolveAdaptiveTotalConcurrency(): number {
const hardwareCap = resolveHardwareTotalConcurrency()
const runtimeCap = resolveRuntimeHealthTotalConcurrency(hardwareCap)
const featureFlagCap = resolveFeatureFlagTotalConcurrency() ?? maximumRuntimeTotalConcurrency
return clampInteger(
Math.min(hardwareCap, runtimeCap, featureFlagCap),
featureFlagCap <= 1 ? 1 : minimumForegroundTotalConcurrency,
maximumRuntimeTotalConcurrency,
)
}
function resolveHardwareTotalConcurrency(): number {
const cpuCount = Math.max(1, cpus().length)
const memoryGB = totalmem() / 1024 / 1024 / 1024
if (cpuCount >= 10 && memoryGB >= 32) {
return 4
}
if (cpuCount >= 6 && memoryGB >= 16) {
return 3
}
return 2
}
function resolveRuntimeHealthTotalConcurrency(hardwareCap: number): number {
return hardwareCap
}
function shouldAdmitNewExecutionUnderRuntimePressure(): boolean {
const metrics = getProcessMetricsSnapshot().latestSample
if (!metrics) {
return true
}
const totalCPUUsage = metrics.processes.reduce((sum, item) => sum + item.percentCPUUsage, 0)
const totalPrivateBytesKB = metrics.totals.privateBytes
const processCount = metrics.totals.processCount
const mainPrivateBytesKB = metrics.mainProcess.memory?.private ?? 0
if (
totalCPUUsage >= 180 ||
totalPrivateBytesKB >= 2_500_000 ||
mainPrivateBytesKB >= 1_000_000 ||
processCount >= 40
) {
return activeAdmissionCount() < minimumForegroundTotalConcurrency
}
return true
}
function resolveFeatureFlagTotalConcurrency(): number | null {
const raw = process.env.GEO_DESKTOP_MAX_TOTAL_CONCURRENCY?.trim()
if (!raw) {
return null
}
const parsed = Number.parseInt(raw, 10)
if (!Number.isFinite(parsed)) {
return null
}
return clampInteger(parsed, 1, maximumRuntimeTotalConcurrency)
}
function clampInteger(value: number, min: number, max: number): number {
return Math.min(max, Math.max(min, Math.floor(value)))
}
function selectPublishAdapter(platform: string): PublishAdapter | null {
if (platform === toutiaoAdapter.platform) {
return toutiaoAdapter
}
if (platform === baijiahaoAdapter.platform) {
return baijiahaoAdapter
}
if (platform === sohuhaoAdapter.platform) {
return sohuhaoAdapter
}
if (platform === zhihuAdapter.platform) {
return zhihuAdapter
}
if (platform === wangyihaoAdapter.platform) {
return wangyihaoAdapter
}
if (platform === jianshuAdapter.platform) {
return jianshuAdapter
}
if (platform === juejinAdapter.platform) {
return juejinAdapter
}
if (platform === qiehaoAdapter.platform) {
return qiehaoAdapter
}
if (platform === bilibiliAdapter.platform) {
return bilibiliAdapter
}
if (platform === smzdmAdapter.platform) {
return smzdmAdapter
}
if (platform === weixinGzhAdapter.platform) {
return weixinGzhAdapter
}
if (platform === zolAdapter.platform) {
return zolAdapter
}
if (platform === dongchediAdapter.platform) {
return dongchediAdapter
}
return null
}
function selectMonitorAdapter(platform: string): MonitorAdapter | null {
return getMonitorAdapter(platform)
}
function normalizeSession(
session: DesktopRuntimeSessionSyncRequest | null,
): DesktopRuntimeSessionSyncRequest | null {
if (!session) {
return null
}
return {
api_base_url: normalizeDesktopApiBaseURL(session.api_base_url),
mode: session.mode,
client_token: session.client_token?.trim() || null,
desktop_client: session.desktop_client ? { ...session.desktop_client } : null,
}
}
function sameSession(
left: DesktopRuntimeSessionSyncRequest | null,
right: DesktopRuntimeSessionSyncRequest | null,
): boolean {
if (!left && !right) {
return true
}
if (!left || !right) {
return false
}
return (
left.api_base_url === right.api_base_url &&
left.mode === right.mode &&
left.client_token === right.client_token &&
left.desktop_client?.id === right.desktop_client?.id
)
}
function canRunLive(
session: DesktopRuntimeSessionSyncRequest | null,
): session is DesktopRuntimeSessionSyncRequest {
return Boolean(
session && session.mode === 'authenticated' && session.client_token && session.desktop_client,
)
}
function resolveAccountName(accountId: string, fallback = '待同步账号'): string {
const account = state.accounts.find((item) => item.id === accountId)
return account?.display_name ?? fallback
}
function currentClientID(): string {
return state.client?.id ?? state.session?.desktop_client?.id ?? ''
}
function resolveTaskTitle(
task: DesktopTaskInfo,
payload: Record<string, JsonValue>,
existing?: RuntimeTaskRecord,
): string {
const title = typeof payload.title === 'string' ? payload.title.trim() : ''
if (title) {
return title
}
return existing?.title ?? defaultTaskTitle(task.kind, task.platform)
}
function describeRouting(routing: RuntimeTaskRouting): string {
return routing === 'rabbitmq-primary' ? '主消息队列通道' : '数据库兜底拉取'
}
function defaultTaskTitle(kind: 'publish' | 'monitor', platform?: string): string {
return `${kind === 'publish' ? '发布任务' : '监控任务'}${platform ? ` · ${platform}` : ''}`
}
function summaryFromTaskStatus(status: RuntimeTaskStatus): string {
switch (status) {
case 'queued':
return '任务已排队,等待客户端领取租约。'
case 'in_progress':
return '任务正在执行,结果回写需要携带有效租约凭证。'
case 'succeeded':
return '任务执行成功。'
case 'failed':
return '任务执行失败。'
case 'aborted':
return '任务已被取消。'
default:
return '任务执行异常,已按失败处理。'
}
}
function summaryFromEventType(
type: DesktopTaskEventMessage['type'],
status: RuntimeTaskStatus,
): string {
switch (type) {
case 'task_available':
return '主消息队列事件已送达当前客户端,等待领取租约。'
case 'task_leased':
return '服务端已确认租约归属。'
case 'task_extended':
return '任务租约已续期。'
case 'task_canceled':
return '任务已被取消。'
case 'task_reconciled':
return '任务已完成人工对账并回写。'
default:
return summaryFromTaskStatus(status)
}
}
function inferPlatformFromAccount(accountId: string | null): string {
if (!accountId) {
return 'desktop'
}
return state.accounts.find((item) => item.id === accountId)?.platform ?? 'desktop'
}
function buildApiUrl(baseURL: string, pathname: string): string {
const normalized = baseURL.endsWith('/') ? baseURL : `${baseURL}/`
return new URL(pathname.replace(/^\//, ''), normalized).toString()
}
function normalizeJsonObject(
value: Record<string, JsonValue> | null | undefined,
): Record<string, JsonValue> {
if (!value) {
return {}
}
return { ...value }
}
function normalizeJsonObjectOrNull(
value: Record<string, JsonValue> | null | undefined,
): Record<string, JsonValue> | null {
if (!value) {
return null
}
return { ...value }
}
function asOptionalString(value: unknown): string | undefined {
if (typeof value !== 'string') {
return undefined
}
const normalized = value.trim()
return normalized || undefined
}
function asOptionalBoolean(value: unknown): boolean | undefined {
return typeof value === 'boolean' ? value : undefined
}
function asOptionalNumber(value: unknown): number | undefined {
return typeof value === 'number' && Number.isFinite(value) ? value : undefined
}
function asStringArray(value: unknown): string[] | undefined {
if (!Array.isArray(value)) {
return undefined
}
const normalized = value
.filter((item): item is string => typeof item === 'string')
.map((item) => item.trim())
.filter((item) => item.length > 0)
return normalized.length > 0 ? normalized : undefined
}
function asRecord(value: unknown): Record<string, JsonValue> | undefined {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return undefined
}
return value as Record<string, JsonValue>
}
function asSourceItems(value: unknown): MonitoringSourceItem[] | undefined {
if (!Array.isArray(value)) {
return undefined
}
const items: MonitoringSourceItem[] = []
for (const entry of value) {
const record = asRecord(entry)
const url = asOptionalString(record?.url)
if (!record || !url) {
continue
}
const item: MonitoringSourceItem = { url }
const title = asOptionalString(record.title)
const siteName = asOptionalString(record.site_name)
const siteKey = asOptionalString(record.site_key)
const normalizedURL = asOptionalString(record.normalized_url)
const host = asOptionalString(record.host)
const registrableDomain = asOptionalString(record.registrable_domain)
const subdomain = asOptionalString(record.subdomain)
const suffix = asOptionalString(record.suffix)
const articleID = asOptionalNumber(record.article_id)
const publishRecordID = asOptionalNumber(record.publish_record_id)
const resolutionStatus = asOptionalString(record.resolution_status)
const resolutionConfidence = asOptionalString(record.resolution_confidence)
if (title !== undefined) {
item.title = title
}
if (siteName !== undefined) {
item.site_name = siteName
}
if (siteKey !== undefined) {
item.site_key = siteKey
}
if (normalizedURL !== undefined) {
item.normalized_url = normalizedURL
}
if (host !== undefined) {
item.host = host
}
if (registrableDomain !== undefined) {
item.registrable_domain = registrableDomain
}
if (subdomain !== undefined) {
item.subdomain = subdomain
}
if (suffix !== undefined) {
item.suffix = suffix
}
if (articleID !== undefined) {
item.article_id = articleID
}
if (publishRecordID !== undefined) {
item.publish_record_id = publishRecordID
}
if (resolutionStatus !== undefined) {
item.resolution_status = resolutionStatus
}
if (resolutionConfidence !== undefined) {
item.resolution_confidence = resolutionConfidence
}
items.push(item)
}
return items.length > 0 ? items : undefined
}
function toInterruptGeneration(value: unknown): number {
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0
}
function toStructuredError(error: unknown): Record<string, JsonValue> {
if (error instanceof ApiClientError) {
return {
code: error.code,
message: error.message,
detail: error.detail ?? null,
request_id: error.requestId ?? null,
status: error.status ?? null,
}
}
if (error instanceof Error) {
return {
code: 'desktop_runtime_error',
message: error.message,
name: error.name,
}
}
return {
code: 'desktop_runtime_error',
message: String(error),
}
}
function errorMessage(error: unknown): string {
if (error instanceof ApiClientError) {
return error.detail ? `${error.message}: ${error.detail}` : error.message
}
if (error instanceof Error) {
return error.message
}
return String(error)
}
function parseTimestamp(value: string | null | undefined): number | null {
if (!value) {
return null
}
const timestamp = Date.parse(value)
return Number.isNaN(timestamp) ? null : timestamp
}
function isDesktopTaskEvent(value: unknown): value is DesktopTaskEventMessage {
if (typeof value !== 'object' || value === null) {
return false
}
const event = value as Partial<DesktopTaskEventMessage>
return Boolean(
typeof event.type === 'string' &&
event.type !== 'task_control' &&
typeof event.task_id === 'string' &&
event.task_id.trim() !== '' &&
typeof event.target_client_id === 'string' &&
event.target_client_id.trim() !== '' &&
(event.kind === 'publish' || event.kind === 'monitor') &&
typeof event.status === 'string' &&
typeof event.updated_at === 'string',
)
}
function isMonitoringDispatchSignal(value: unknown): value is DesktopTaskEventMessage {
if (typeof value !== 'object' || value === null) {
return false
}
const event = value as Partial<DesktopTaskEventMessage>
return (
event.kind === 'monitor' &&
event.type === 'task_available' &&
event.signal_only === true &&
typeof event.target_client_id === 'string' &&
event.target_client_id.trim() !== ''
)
}
function isMonitoringTaskControlEvent(value: unknown): value is DesktopTaskEventMessage {
if (typeof value !== 'object' || value === null) {
return false
}
const event = value as Partial<DesktopTaskEventMessage>
return (
event.kind === 'monitor' &&
event.type === 'task_control' &&
event.control === 'interrupt_requested' &&
typeof event.task_id === 'string' &&
event.task_id.trim() !== '' &&
typeof event.target_client_id === 'string' &&
event.target_client_id.trim() !== ''
)
}
function isConnectedEvent(value: unknown): value is { server_time: string } {
if (typeof value !== 'object' || value === null) {
return false
}
return typeof (value as { server_time?: string }).server_time === 'string'
}
function isReconnectPayload(value: unknown): value is { delayMs: number } {
if (typeof value !== 'object' || value === null) {
return false
}
return typeof (value as { delayMs?: number }).delayMs === 'number'
}