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
+58 -13
View File
@@ -1,6 +1,8 @@
interface LeaseSnapshot {
activeLeaseId: string | null;
activeTaskId: string | null;
activeTaskIds: string[];
activeCount: number;
attemptId: string | null;
startedAt: number | null;
leaseExpiresAt: number | null;
@@ -13,11 +15,16 @@ interface ActiveLeaseState {
taskId: string;
attemptId: string | null;
leaseExpiresAt: number | null;
startedAt: number;
lastExtendedAt: number | null;
}
const activeLeases = new Map<string, ActiveLeaseState>();
const leaseState: LeaseSnapshot = {
activeLeaseId: null,
activeTaskId: null,
activeTaskIds: [],
activeCount: 0,
attemptId: null,
startedAt: null,
leaseExpiresAt: null,
@@ -45,8 +52,11 @@ export function setActiveLease(input: {
leaseExpiresAt?: number | string | null;
} | null): void {
if (!input) {
activeLeases.clear();
leaseState.activeLeaseId = null;
leaseState.activeTaskId = null;
leaseState.activeTaskIds = [];
leaseState.activeCount = 0;
leaseState.attemptId = null;
leaseState.startedAt = null;
leaseState.leaseExpiresAt = null;
@@ -59,28 +69,51 @@ export function setActiveLease(input: {
taskId: input.taskId,
attemptId: input.attemptId ?? null,
leaseExpiresAt: normalizeLeaseExpiresAt(input.leaseExpiresAt),
startedAt: Date.now(),
lastExtendedAt: null,
};
leaseState.activeLeaseId = normalized.taskId;
leaseState.activeTaskId = normalized.taskId;
leaseState.attemptId = normalized.attemptId;
leaseState.startedAt = Date.now();
leaseState.leaseExpiresAt = normalized.leaseExpiresAt;
activeLeases.set(normalized.taskId, normalized);
syncLeaseSnapshot();
leaseState.lastOutcome = "leased";
}
export function noteLeaseExtended(leaseExpiresAt?: number | string | null): void {
export function noteLeaseExtended(
leaseExpiresAt?: number | string | null,
taskId?: string,
): void {
if (taskId) {
const active = activeLeases.get(taskId);
if (active) {
active.leaseExpiresAt = normalizeLeaseExpiresAt(leaseExpiresAt) ?? active.leaseExpiresAt;
active.lastExtendedAt = Date.now();
activeLeases.set(taskId, active);
}
} else {
const first = activeLeases.values().next().value as ActiveLeaseState | undefined;
if (first) {
first.leaseExpiresAt = normalizeLeaseExpiresAt(leaseExpiresAt) ?? first.leaseExpiresAt;
first.lastExtendedAt = Date.now();
activeLeases.set(first.taskId, first);
}
}
syncLeaseSnapshot();
leaseState.lastExtendedAt = Date.now();
leaseState.leaseExpiresAt = normalizeLeaseExpiresAt(leaseExpiresAt) ?? leaseState.leaseExpiresAt;
leaseState.lastOutcome = "extended";
}
export function noteLeaseReleased(reason: Exclude<LeaseSnapshot["lastOutcome"], "leased" | "extended" | null>): void {
leaseState.activeLeaseId = null;
leaseState.activeTaskId = null;
leaseState.attemptId = null;
leaseState.startedAt = null;
leaseState.leaseExpiresAt = null;
export function noteLeaseReleased(
reason: Exclude<LeaseSnapshot["lastOutcome"], "leased" | "extended" | null>,
taskId?: string,
): void {
if (taskId) {
activeLeases.delete(taskId);
} else {
activeLeases.clear();
}
syncLeaseSnapshot();
leaseState.lastReleasedAt = Date.now();
leaseState.lastOutcome = reason;
}
@@ -88,3 +121,15 @@ export function noteLeaseReleased(reason: Exclude<LeaseSnapshot["lastOutcome"],
export function getLeaseManagerSnapshot(): LeaseSnapshot {
return { ...leaseState };
}
function syncLeaseSnapshot(): void {
const first = activeLeases.values().next().value as ActiveLeaseState | undefined;
leaseState.activeTaskIds = [...activeLeases.keys()];
leaseState.activeCount = activeLeases.size;
leaseState.activeLeaseId = first?.taskId ?? null;
leaseState.activeTaskId = first?.taskId ?? null;
leaseState.attemptId = first?.attemptId ?? null;
leaseState.startedAt = first?.startedAt ?? null;
leaseState.leaseExpiresAt = first?.leaseExpiresAt ?? null;
}