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
@@ -11,7 +11,7 @@ import type {
JsonValue,
LeaseDesktopTaskResponse,
} from "@geo/shared-types";
import { isAIPlatformId } from "@geo/shared-types";
import { getAIPlatformCatalogItem, isAIPlatformId } from "@geo/shared-types";
import {
doubaoAdapter,
@@ -22,10 +22,17 @@ import {
type PublishAdapter,
} from "./adapters";
import {
ensurePublishAccountSessionHandle,
inspectPublishAccountLocalSession,
type PublishAccountIdentity,
type PublishAccountProfile,
} from "./account-binder";
import {
ensureAccountReady,
forgetTrackedAccountHealth,
getProjectedAccountHealth,
probeTrackedAccount,
reportAccountFailure,
syncTrackedAccounts,
} from "./account-health";
import {
buildAccountIdentityKey,
normalizeAccountPlatformUid,
@@ -33,6 +40,21 @@ import {
} from "../shared/account-identity";
import { releaseHotView, retainHotView } from "./view-pool";
import { getLeaseManagerSnapshot, noteLeaseExtended, noteLeaseReleased, setActiveLease } from "./lease-manager";
import {
enqueueMonitorTaskFromEvent,
getMonitorSchedulerSnapshot,
initMonitorScheduler,
noteMonitorTaskCanceled,
noteMonitorTaskCompleted,
noteMonitorTaskLeaseMiss,
noteMonitorTaskLeased,
selectNextMonitorTask,
} from "./monitor-scheduler";
import {
retainHiddenPlaywrightPage,
startHiddenPlaywrightReaper,
} from "./playwright-cdp";
import { getProcessMetricsSnapshot } from "./process-metrics";
import {
initScheduler,
noteSchedulerAccountsSync,
@@ -47,7 +69,7 @@ import {
setSchedulerPhase,
setSchedulerQueueDepth,
} from "./scheduler";
import { clearSessionHandle, createSessionHandle, getSessionHandle } from "./session-registry";
import { clearSessionHandle, createSessionHandle } from "./session-registry";
import {
completeDesktopTask,
configureTransport,
@@ -136,19 +158,26 @@ interface PendingTaskRequest {
routing: RuntimeTaskRouting;
}
interface ActiveRuntimeExecution {
taskId: string;
kind: "publish" | "monitor";
platform: string;
accountId: string;
abortController: AbortController;
}
interface RuntimeState {
session: DesktopRuntimeSessionSyncRequest | null;
client: DesktopClientInfo | null;
running: boolean;
sseConnected: boolean;
queue: PendingTaskRequest[];
queuedTaskIds: Set<string>;
publishQueue: PendingTaskRequest[];
queuedPublishTaskIds: Set<string>;
tasks: Map<string, RuntimeTaskRecord>;
accounts: DesktopAccountInfo[];
accountProfiles: Map<string, PublishAccountProfile>;
activity: RuntimeControllerActivity[];
currentTaskId: string | null;
currentExecutionAbort: AbortController | null;
activeExecutions: Map<string, ActiveRuntimeExecution>;
leaseInFlight: boolean;
heartbeatTimer: ReturnType<typeof setInterval> | null;
pullTimer: ReturnType<typeof setInterval> | null;
@@ -168,14 +197,13 @@ const state: RuntimeState = {
client: null,
running: false,
sseConnected: false,
queue: [],
queuedTaskIds: new Set<string>(),
publishQueue: [],
queuedPublishTaskIds: new Set<string>(),
tasks: new Map<string, RuntimeTaskRecord>(),
accounts: [],
accountProfiles: new Map<string, PublishAccountProfile>(),
activity: [],
currentTaskId: null,
currentExecutionAbort: null,
activeExecutions: new Map<string, ActiveRuntimeExecution>(),
leaseInFlight: false,
heartbeatTimer: null,
pullTimer: null,
@@ -190,6 +218,41 @@ const state: RuntimeState = {
activitySeq: 0,
};
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 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);
@@ -256,7 +319,7 @@ export function getRuntimeControllerSnapshot(): RuntimeControllerSnapshot {
client: state.client ? { ...state.client } : null,
running: state.running,
sseConnected: state.sseConnected,
queueDepth: state.queue.length,
queueDepth: localQueueDepth(),
accounts: state.accounts.map((item) => ({ ...item })),
accountProfiles: Object.fromEntries(
[...state.accountProfiles.entries()].map(([accountId, profile]) => [accountId, { ...profile }]),
@@ -281,6 +344,8 @@ function startRuntime(): void {
}
initScheduler();
initMonitorScheduler();
startHiddenPlaywrightReaper();
state.running = true;
setSchedulerPhase("running");
setSchedulerError(null);
@@ -292,16 +357,17 @@ function startRuntime(): void {
void sendHeartbeat("startup");
void syncAccounts("startup");
processQueue();
pumpExecutionLoop();
}
function stopRuntime(): void {
state.running = false;
state.sseConnected = false;
state.leaseInFlight = false;
state.currentExecutionAbort?.abort();
state.currentExecutionAbort = null;
state.currentTaskId = null;
for (const execution of state.activeExecutions.values()) {
execution.abortController.abort();
}
state.activeExecutions.clear();
if (state.heartbeatTimer) {
clearInterval(state.heartbeatTimer);
@@ -319,7 +385,7 @@ function stopRuntime(): void {
setTransportSseState("idle");
setSchedulerPhase("idle");
setSchedulerCurrentTask(null);
setSchedulerQueueDepth(0);
setSchedulerQueueDepth(localQueueDepth());
setSchedulerNextHeartbeat(null);
setSchedulerNextPull(null);
@@ -329,12 +395,12 @@ function stopRuntime(): void {
}
function clearRuntimeState(): void {
state.queue = [];
state.queuedTaskIds.clear();
state.publishQueue = [];
state.queuedPublishTaskIds.clear();
state.tasks.clear();
state.accounts = [];
state.accountProfiles.clear();
state.currentTaskId = null;
state.activeExecutions.clear();
state.lastHeartbeatAt = 0;
state.lastHeartbeatStatus = "idle";
state.lastPullAt = 0;
@@ -342,7 +408,7 @@ function clearRuntimeState(): void {
state.lastAccountsSyncAt = 0;
state.lastServerTime = null;
state.lastError = null;
setSchedulerQueueDepth(0);
setSchedulerQueueDepth(localQueueDepth());
}
function scheduleHeartbeatLoop(): void {
@@ -365,7 +431,7 @@ function schedulePullLoop(): void {
setSchedulerNextPull(Date.now() + pullIntervalMs);
state.pullTimer = setInterval(() => {
setSchedulerNextPull(Date.now() + pullIntervalMs);
processQueue();
pumpExecutionLoop();
}, pullIntervalMs);
}
@@ -478,9 +544,20 @@ function handleTaskEvent(event: DesktopTaskEventMessage): void {
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.type === "task_available") {
enqueueTask(event.task_id, "rabbitmq-primary");
processQueue();
if (event.kind === "publish") {
enqueuePublishTask(event.task_id, "rabbitmq-primary");
}
pumpExecutionLoop();
}
}
@@ -498,7 +575,16 @@ async function sendHeartbeat(source: "startup" | "timer"): Promise<void> {
cpu_arch: process.arch,
client_version: state.client?.client_version ?? "0.1.0-dev",
channel: state.client?.channel ?? "dev",
account_ids: state.accounts.filter((account) => account.health === "live").map((account) => account.id),
account_ids: state.accounts
.filter((account) =>
getProjectedAccountHealth({
accountId: account.id,
platform: account.platform,
health: account.health,
verifiedAt: account.verified_at,
}).health === "live",
)
.map((account) => account.id),
});
state.client = response.client;
@@ -547,28 +633,41 @@ async function syncAccounts(source: "startup" | "manual"): Promise<void> {
noteSchedulerAccountsSync();
setSchedulerError(null);
const identities = accounts.map((account) => accountIdentityFromDesktopAccount(account));
syncTrackedAccounts(identities);
if (source === "manual") {
await Promise.allSettled(
identities.map((account) =>
probeTrackedAccount(account, {
trigger: "manual",
allowSilentRefresh: true,
}),
),
);
}
state.accountProfiles.clear();
const syncedAccounts: Array<DesktopAccountInfo | null> = await Promise.all(accounts.map(async (account) => {
const normalizedPlatformUid = normalizeAccountPlatformUid(account.platform_uid) || account.platform_uid;
const isAIPlatform = isAIPlatformId(account.platform);
const identity = {
id: account.id,
const identity = accountIdentityFromDesktopAccount(account);
const projected = getProjectedAccountHealth({
accountId: account.id,
platform: account.platform,
platformUid: normalizedPlatformUid,
displayName: account.display_name,
} as const;
const local = await inspectPublishAccountLocalSession(identity);
health: account.health,
verifiedAt: account.verified_at,
});
if (local.availability === "missing") {
return null;
if (projected.profile?.subjectUid && projected.profile.displayName) {
state.accountProfiles.set(account.id, {
platformUid: projected.profile.subjectUid,
displayName: projected.profile.displayName,
avatarUrl: projected.profile.avatarUrl,
});
}
if (local.profile) {
state.accountProfiles.set(account.id, local.profile);
}
const normalizedLocalPlatformUid = local.profile
? (normalizeAccountPlatformUid(local.profile.platformUid) || local.profile.platformUid)
const normalizedLocalPlatformUid = projected.profile?.subjectUid
? (normalizeAccountPlatformUid(projected.profile.subjectUid) || projected.profile.subjectUid)
: null;
const hasPlatformUidMismatch = Boolean(
normalizedLocalPlatformUid && !sameAccountPlatformUid(normalizedLocalPlatformUid, normalizedPlatformUid),
@@ -585,22 +684,27 @@ async function syncAccounts(source: "startup" | "manual"): Promise<void> {
let resolvedAccount: DesktopAccountInfo = {
...account,
platform_uid: normalizedPlatformUid,
display_name: local.profile?.displayName ?? account.display_name,
avatar_url: local.profile?.avatarUrl ?? account.avatar_url,
health: local.availability === "expired" ? ("expired" as const) : ("live" as const),
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,
};
const shouldRelinkClient = Boolean(currentClientId && resolvedAccount.client_id !== currentClientId);
if (shouldRelinkClient) {
const shouldMirrorHealth =
resolvedAccount.health !== account.health
&& isDefinitiveAuthState(projected.authState);
if (shouldRelinkClient || shouldMirrorHealth) {
try {
const updated = await upsertDesktopAccount({
platform: resolvedAccount.platform,
platform_uid: normalizedPlatformUid,
display_name: local.profile?.displayName ?? resolvedAccount.display_name,
avatar_url: local.profile?.avatarUrl ?? resolvedAccount.avatar_url,
display_name: projected.profile?.displayName ?? resolvedAccount.display_name,
avatar_url: projected.profile?.avatarUrl ?? resolvedAccount.avatar_url,
account_fingerprint: resolvedAccount.account_fingerprint ?? normalizedPlatformUid,
health: resolvedAccount.health,
verified_at: new Date().toISOString(),
verified_at: isoFromTimestamp(projected.lastVerifiedAt) ?? resolvedAccount.verified_at,
tags: resolvedAccount.tags,
});
@@ -694,6 +798,7 @@ export async function unbindRuntimeAccount(accountId: string, syncVersion: numbe
await deleteDesktopAccount(accountId, syncVersion);
await clearSessionHandle(accountId);
forgetTrackedAccountHealth(accountId);
state.accounts = state.accounts.filter((item) => item.id !== accountId);
state.accountProfiles.delete(accountId);
@@ -710,41 +815,74 @@ export async function unbindRuntimeAccount(accountId: string, syncVersion: numbe
await syncAccounts("manual");
}
function enqueueTask(taskId: string, routing: RuntimeTaskRouting): void {
if (!taskId || state.currentTaskId === taskId || state.queuedTaskIds.has(taskId)) {
function enqueuePublishTask(taskId: string, routing: RuntimeTaskRouting): void {
if (!taskId || state.activeExecutions.has(taskId) || state.queuedPublishTaskIds.has(taskId)) {
return;
}
state.queue.push({ taskId, routing });
state.queuedTaskIds.add(taskId);
setSchedulerQueueDepth(state.queue.length);
state.publishQueue.push({ taskId, routing });
state.queuedPublishTaskIds.add(taskId);
syncSchedulerSurface();
}
function dequeueTask(): PendingTaskRequest | null {
const next = state.queue.shift() ?? null;
function dequeuePublishTask(): PendingTaskRequest | null {
const next = state.publishQueue.shift() ?? null;
if (next) {
state.queuedTaskIds.delete(next.taskId);
state.queuedPublishTaskIds.delete(next.taskId);
}
setSchedulerQueueDepth(state.queue.length);
syncSchedulerSurface();
return next;
}
function processQueue(): void {
if (!state.running || state.currentTaskId || state.leaseInFlight) {
function pumpExecutionLoop(): void {
if (!state.running || state.leaseInFlight) {
return;
}
const queued = dequeueTask();
if (queued) {
void leaseSpecificTask(queued);
syncSchedulerSurface();
if (hasActivePublishExecution()) {
return;
}
void pullNextTask();
if (state.publishQueue.length > 0) {
if (state.activeExecutions.size === 0) {
const queuedPublish = dequeuePublishTask();
if (queuedPublish) {
void leaseSpecificTask(queuedPublish, "publish");
}
}
return;
}
if (canStartAnotherMonitorExecution()) {
const selected = selectNextMonitorTask({
maxConcurrency: resolveAdaptiveMonitorConcurrency(),
activePlatforms: activeMonitorPlatforms(),
activeQuestionKeys: activeMonitorQuestionKeys(),
activeCount: activeMonitorCount(),
});
if (selected) {
void leaseSpecificTask(selected, "monitor");
return;
}
}
if (state.activeExecutions.size === 0) {
void pullNextTask("publish");
return;
}
if (canStartAnotherMonitorExecution()) {
void pullNextTask("monitor");
}
}
async function leaseSpecificTask(request: PendingTaskRequest): Promise<void> {
if (!canRunLive(state.session) || state.currentTaskId || state.leaseInFlight) {
async function leaseSpecificTask(
request: PendingTaskRequest,
expectedKind: "publish" | "monitor",
): Promise<void> {
if (!canRunLive(state.session) || state.leaseInFlight) {
return;
}
@@ -753,11 +891,18 @@ async function leaseSpecificTask(request: PendingTaskRequest): Promise<void> {
try {
const leased = await leaseDesktopTask({ task_id: request.taskId });
if (!leased.task) {
if (expectedKind === "monitor") {
noteMonitorTaskLeaseMiss(request.taskId);
}
return;
}
state.leaseInFlight = false;
await executeLeasedTask(leased, request.routing);
} catch (error) {
if (expectedKind === "monitor") {
noteMonitorTaskLeaseMiss(request.taskId);
}
if (isApiClientError(error, 401)) {
handleAuthExpired(error);
return;
@@ -769,22 +914,24 @@ async function leaseSpecificTask(request: PendingTaskRequest): Promise<void> {
} finally {
if (state.leaseInFlight) {
state.leaseInFlight = false;
if (state.queue.length > 0) {
processQueue();
syncSchedulerSurface();
if (state.publishQueue.length > 0 || localQueueDepth() > 0) {
pumpExecutionLoop();
}
}
}
}
async function pullNextTask(): Promise<void> {
if (!canRunLive(state.session) || state.currentTaskId || state.leaseInFlight) {
async function pullNextTask(kind: "publish" | "monitor"): Promise<void> {
if (!canRunLive(state.session) || state.leaseInFlight) {
return;
}
state.leaseInFlight = true;
let leasedTask = false;
try {
const leased = await leaseDesktopTask({});
const leased = await leaseDesktopTask({ kind });
state.lastPullAt = Date.now();
state.lastPullStatus = "success";
noteTransportPull(true);
@@ -794,6 +941,7 @@ async function pullNextTask(): Promise<void> {
return;
}
leasedTask = true;
state.leaseInFlight = false;
await executeLeasedTask(leased, "db-recovery");
} catch (error) {
@@ -812,6 +960,11 @@ async function pullNextTask(): Promise<void> {
recordActivity("warn", "兜底拉取失败", state.lastError);
} finally {
state.leaseInFlight = false;
syncSchedulerSurface();
if (!leasedTask && kind === "publish" && canStartAnotherMonitorExecution()) {
void pullNextTask("monitor");
}
}
}
@@ -825,8 +978,6 @@ async function executeLeasedTask(
const task = leased.task;
state.currentTaskId = task.id;
setSchedulerCurrentTask(task.id);
setSchedulerPhase("running");
setActiveLease({
@@ -849,7 +1000,18 @@ async function executeLeasedTask(
);
const abortController = new AbortController();
state.currentExecutionAbort = abortController;
state.activeExecutions.set(taskRecord.id, {
taskId: taskRecord.id,
kind: taskRecord.kind,
platform: taskRecord.platform,
accountId: taskRecord.accountId,
abortController,
});
syncSchedulerSurface();
if (task.kind === "monitor") {
noteMonitorTaskLeased(task, routing);
}
const extendHandle = setInterval(() => {
void extendActiveLease(taskRecord.id, leased.lease_token as string);
@@ -864,7 +1026,7 @@ async function executeLeasedTask(
error: execution.error,
});
noteLeaseReleased(execution.status === "failed" ? "failed" : "completed");
noteLeaseReleased(execution.status === "failed" ? "failed" : "completed", taskRecord.id);
upsertTaskFromInfo(completed, {
routing,
summary: execution.summary,
@@ -881,9 +1043,28 @@ async function executeLeasedTask(
: "任务执行成功",
`${taskRecord.title} ${execution.summary}`,
);
if (task.kind === "monitor") {
noteMonitorTaskCompleted(taskRecord.id);
}
} catch (error) {
const failureMessage = errorMessage(error);
const structuredError = toStructuredError(error);
const accountIdentity = accountIdentityFromTask(taskRecord);
if (accountIdentity) {
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, {
@@ -909,15 +1090,18 @@ async function executeLeasedTask(
}
}
noteLeaseReleased("failed");
noteLeaseReleased("failed", taskRecord.id);
recordActivity("danger", "任务执行失败", `${taskRecord.title} 执行失败:${failureMessage || "未知错误"}`);
if (task.kind === "monitor") {
noteMonitorTaskCompleted(taskRecord.id);
}
} finally {
clearInterval(extendHandle);
state.currentExecutionAbort = null;
state.currentTaskId = null;
setSchedulerCurrentTask(null);
state.activeExecutions.delete(taskRecord.id);
syncSchedulerSurface();
noteSchedulerTaskFinished();
processQueue();
pumpExecutionLoop();
}
}
@@ -925,7 +1109,7 @@ async function extendActiveLease(taskId: string, leaseToken: string): Promise<vo
try {
const task = await extendDesktopTask(taskId, { lease_token: leaseToken });
noteLeaseExtended(task.lease_expires_at ?? null);
noteLeaseExtended(task.lease_expires_at ?? null, taskId);
const existing = state.tasks.get(taskId);
if (existing) {
@@ -946,6 +1130,14 @@ async function executeTaskAdapter(
task: RuntimeTaskRecord,
signal: AbortSignal,
): Promise<AdapterExecutionResult> {
const accountIdentity = accountIdentityFromTask(task);
if (accountIdentity) {
const readiness = await ensureAccountReady(accountIdentity);
if (readiness.authState !== "active") {
return buildAccountBlockedResult(task, readiness);
}
}
const sessionHandle = createSessionHandle(task.accountId || task.id);
const payload = task.payload;
@@ -959,14 +1151,29 @@ async function executeTaskAdapter(
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 {
return await adapter.publish(
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),
@@ -976,7 +1183,10 @@ async function executeTaskAdapter(
},
payload,
);
await maybeReportTaskAuthFailure(task, result, accountIdentity);
return result;
} finally {
await playwrightLease?.release();
if (viewHandle) {
releaseHotView(viewHandle.accountId);
}
@@ -1010,14 +1220,29 @@ async function executeTaskAdapter(
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 {
return await adapter.query(
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) {
@@ -1026,13 +1251,91 @@ async function executeTaskAdapter(
},
payload,
);
await maybeReportTaskAuthFailure(task, result, accountIdentity);
return result;
} finally {
await playwrightLease?.release();
if (viewHandle) {
releaseHotView(viewHandle.accountId);
}
}
}
function buildAccountBlockedResult(
task: RuntimeTaskRecord,
readiness: Awaited<ReturnType<typeof ensureAccountReady>>,
): AdapterExecutionResult {
if (readiness.authState === "challenge_required") {
return {
status: "failed",
summary: `${task.accountName} 需要完成人机验证后才能继续执行。`,
error: {
code: "desktop_account_challenge_required",
message: "desktop account challenge required",
},
};
}
if (readiness.authState === "expired" || readiness.authState === "revoked") {
return {
status: "failed",
summary: `${task.accountName} 登录态已失效,请重新授权后再试。`,
error: {
code: "desktop_account_auth_expired",
message: "desktop account auth expired",
},
};
}
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;
}
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),
});
});
}
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>,
@@ -1096,6 +1399,23 @@ function toPositiveInt(value: JsonValue | null): number | 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) {
@@ -1219,6 +1539,90 @@ function recordActivity(
}
}
function syncSchedulerSurface(): void {
setSchedulerQueueDepth(localQueueDepth());
setSchedulerCurrentTask(primaryActiveTaskId());
}
function localQueueDepth(): number {
const snapshot = getMonitorSchedulerSnapshot();
return state.publishQueue.length + snapshot.queuedCount + snapshot.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 hasActivePublishExecution(): boolean {
return [...state.activeExecutions.values()].some((item) => item.kind === "publish");
}
function activeMonitorCount(): number {
return [...state.activeExecutions.values()].filter((item) => item.kind === "monitor").length;
}
function activeMonitorPlatforms(): Set<string> {
return new Set(
[...state.activeExecutions.values()]
.filter((item) => item.kind === "monitor")
.map((item) => item.platform),
);
}
function activeMonitorQuestionKeys(): Set<string> {
return new Set(
getMonitorSchedulerSnapshot().tasks
.filter((task) => task.state === "active" && Boolean(task.questionKey))
.map((task) => task.questionKey as string),
);
}
function canStartAnotherMonitorExecution(): boolean {
if (state.publishQueue.length > 0 || hasActivePublishExecution()) {
return false;
}
const limit = resolveAdaptiveMonitorConcurrency();
return limit > 0 && activeMonitorCount() < limit;
}
function resolveAdaptiveMonitorConcurrency(): number {
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 monitorAccountCount;
}
const metrics = getProcessMetricsSnapshot().latestSample;
if (!metrics) {
return 1;
}
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 1;
}
return 2;
}
function handleAuthExpired(error: unknown): void {
const message = errorMessage(error);
state.lastError = message;