feat(desktop): add monitor scheduler, Playwright CDP layer, and account health

Move monitor-task scheduling authority onto the client with a durable
file-backed queue that survives restart, drops stale cross-day tasks,
enforces per-platform serialism, and adapts global concurrency from
Electron process metrics. Publish tasks keep their existing FIFO.

Add a hidden Playwright CDP manager that attaches to Electron Chromium
on account session partitions, lets adapters opt into `executionMode:
"playwright"`, and leaves the existing hidden WebContentsView path in
place for current adapters.

Introduce an account-health subsystem with silent probes, projected
health/auth states, and IPC invalidation events so the renderer can
show accurate auth/probe status and verification timestamps.

Server-side, derive and forward title/business_date/scheduler_group_key/
question_text metadata on desktop task events so the local scheduler
can defer same-question fan-out before leasing.
This commit is contained in:
2026-04-20 17:16:15 +08:00
parent 07881a66d0
commit 55e1c2e74b
26 changed files with 3001 additions and 153 deletions
@@ -1,10 +1,13 @@
import { app } from "electron/main";
import { getProjectedAccountHealth } from "./account-health";
import { getProcessMetricsSnapshot } from "./process-metrics";
import { getCircuitBreakerState } from "./circuit-breaker";
import { collectRecoverySnapshot } from "./crash-recovery";
import { getKeepAlivePlan } from "./keep-alive";
import { getLeaseManagerSnapshot } from "./lease-manager";
import { getMonitorSchedulerSnapshot } from "./monitor-scheduler";
import { getHiddenPlaywrightSnapshot } from "./playwright-cdp";
import { getRateLimiterSnapshot } from "./rate-limiter";
import { getRuntimeControllerSnapshot } from "./runtime-controller";
import { getSchedulerState } from "./scheduler";
@@ -71,23 +74,35 @@ function createLiveRuntimeSnapshot(controller: ReturnType<typeof getRuntimeContr
const accountQueueDepth = controller.tasks.filter((task) =>
task.accountId === account.id && ["queued", "in_progress"].includes(task.status),
).length;
const projected = getProjectedAccountHealth({
accountId: account.id,
platform: account.platform,
health: account.health,
verifiedAt: account.verified_at,
});
return {
id: account.id,
platform: account.platform,
displayName: account.display_name,
displayName: projected.profile?.displayName ?? account.display_name,
platformUid: normalizeAccountPlatformUid(account.platform_uid) || account.platform_uid,
avatarUrl: account.avatar_url ?? null,
health: account.health,
avatarUrl: projected.profile?.avatarUrl ?? account.avatar_url ?? null,
health: projected.health,
authState: projected.authState,
probeState: projected.probeState,
authReason: projected.authReason,
tags: account.tags,
syncVersion: account.sync_version,
clientId: account.client_id ?? primaryClientId,
partition: sessionHandle?.partition ?? `persist:acc-${account.id}`,
partition: projected.partition || sessionHandle?.partition || `persist:acc-${account.id}`,
sessionState: isHot ? "hot" : sessionHandle ? "warm" : "cold",
lastSyncAt:
controller.lastAccountsSyncAt
|| parseTimestamp(account.verified_at)
|| now,
lastVerifiedAt: projected.lastVerifiedAt,
lastProbeAt: projected.lastProbeAt,
nextProbeAt: projected.nextProbeAt,
queueDepth: accountQueueDepth,
online: clientOnline,
};
@@ -150,9 +165,11 @@ function createLiveRuntimeSnapshot(controller: ReturnType<typeof getRuntimeContr
lastError: controller.lastError,
},
scheduler,
monitorScheduler: getMonitorSchedulerSnapshot(),
sessions,
hotViews,
hotViewPool: getHotViewPoolPolicy(),
playwright: getHiddenPlaywrightSnapshot(),
processMetrics: getProcessMetricsSnapshot(),
vault: describeVaultBackend(),
},