feat(desktop-client): queue account probes and report health to server
Replace serial probe-on-lock with a bounded probe queue (concurrency 3) that adds a "queued" probe state, manual cooldown, and retry backoff. After each probe the runtime debounces a batched POST to the new /desktop/accounts/health-reports endpoint so the server learns about live, expired, and risk transitions without waiting for a re-bind. Adds an account-scoped IPC (probeRuntimeAccount + runtimeAccountSnapshot) so the renderer can refresh a single row instead of replaying the full snapshot, and exposes the resulting "重新校验" action in AccountsView/AiPlatformsView. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -32,11 +32,83 @@ function parseTimestamp(value: string | null | undefined): number | null {
|
||||
return Number.isNaN(timestamp) ? null : timestamp;
|
||||
}
|
||||
|
||||
type RuntimeControllerSnapshot = ReturnType<typeof getRuntimeControllerSnapshot>;
|
||||
type RuntimeControllerAccount = RuntimeControllerSnapshot["accounts"][number];
|
||||
|
||||
function createRuntimeAccountView(
|
||||
controller: RuntimeControllerSnapshot,
|
||||
account: RuntimeControllerAccount,
|
||||
context: {
|
||||
hotViews: ReturnType<typeof listHotViews>;
|
||||
primaryClientId: string;
|
||||
clientOnline: boolean;
|
||||
lastAccountsSyncAt: number;
|
||||
now: number;
|
||||
},
|
||||
) {
|
||||
const sessionHandle = getSessionHandle(account.id);
|
||||
const isHot = context.hotViews.some((item) => item.accountId === account.id);
|
||||
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: projected.profile?.displayName ?? account.display_name,
|
||||
platformUid: normalizeAccountPlatformUid(account.platform_uid) || account.platform_uid,
|
||||
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 ?? context.primaryClientId,
|
||||
partition: projected.partition || sessionHandle?.partition || `persist:acc-${account.id}`,
|
||||
sessionState: isHot ? "hot" : sessionHandle ? "warm" : "cold",
|
||||
lastSyncAt:
|
||||
context.lastAccountsSyncAt
|
||||
|| parseTimestamp(account.verified_at)
|
||||
|| context.now,
|
||||
lastVerifiedAt: projected.lastVerifiedAt,
|
||||
lastProbeAt: projected.lastProbeAt,
|
||||
nextProbeAt: projected.nextProbeAt,
|
||||
queueDepth: accountQueueDepth,
|
||||
online: context.clientOnline,
|
||||
};
|
||||
}
|
||||
|
||||
export function createRuntimeSnapshot() {
|
||||
return createLiveRuntimeSnapshot(getRuntimeControllerSnapshot());
|
||||
}
|
||||
|
||||
function createLiveRuntimeSnapshot(controller: ReturnType<typeof getRuntimeControllerSnapshot>) {
|
||||
export function createRuntimeAccountSnapshot(accountId: string) {
|
||||
const controller = getRuntimeControllerSnapshot();
|
||||
const now = Date.now();
|
||||
const primaryClientId = controller.client?.id ?? controller.session?.desktop_client?.id ?? "desktop-self";
|
||||
const clientOnline = controller.running && controller.lastHeartbeatStatus !== "failed";
|
||||
const account = controller.accounts.find((item) => item.id === accountId);
|
||||
if (!account) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return createRuntimeAccountView(controller, account, {
|
||||
hotViews: listHotViews(),
|
||||
primaryClientId,
|
||||
clientOnline,
|
||||
lastAccountsSyncAt: controller.lastAccountsSyncAt,
|
||||
now,
|
||||
});
|
||||
}
|
||||
|
||||
function createLiveRuntimeSnapshot(controller: RuntimeControllerSnapshot) {
|
||||
const now = Date.now();
|
||||
// Electron Session instances are not safe to ship across IPC to the renderer.
|
||||
const sessions = listSessionHandleSnapshots();
|
||||
@@ -70,45 +142,15 @@ function createLiveRuntimeSnapshot(controller: ReturnType<typeof getRuntimeContr
|
||||
},
|
||||
] as const;
|
||||
|
||||
const accounts = controller.accounts.map((account) => {
|
||||
const sessionHandle = getSessionHandle(account.id);
|
||||
const isHot = hotViews.some((item) => item.accountId === account.id);
|
||||
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: projected.profile?.displayName ?? account.display_name,
|
||||
platformUid: normalizeAccountPlatformUid(account.platform_uid) || account.platform_uid,
|
||||
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: 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,
|
||||
};
|
||||
});
|
||||
const accounts = controller.accounts.map((account) =>
|
||||
createRuntimeAccountView(controller, account, {
|
||||
hotViews,
|
||||
primaryClientId,
|
||||
clientOnline,
|
||||
lastAccountsSyncAt: controller.lastAccountsSyncAt,
|
||||
now,
|
||||
}),
|
||||
);
|
||||
|
||||
const tasks = controller.tasks.map((task) => ({ ...task }));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user