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:
@@ -16,6 +16,8 @@ import type {
|
||||
LeaseDesktopTaskRequest,
|
||||
LeaseDesktopTaskResponse,
|
||||
ListDesktopPublishTasksParams,
|
||||
ReportDesktopAccountHealthRequest,
|
||||
ReportDesktopAccountHealthResponse,
|
||||
MonitoringLeaseTasksPayload,
|
||||
MonitoringLeaseTasksResponse,
|
||||
MonitoringResumeTasksPayload,
|
||||
@@ -65,6 +67,16 @@ let transportSession: DesktopTransportSession = {
|
||||
baseURL: process.env.DESKTOP_API_BASE_URL ?? "http://localhost:8080",
|
||||
clientToken: null,
|
||||
};
|
||||
const ACCOUNT_UPSERT_CACHE_TTL_MS = 5 * 60_000;
|
||||
|
||||
interface AccountUpsertCacheEntry {
|
||||
signature: string;
|
||||
expiresAt: number;
|
||||
response: DesktopAccountInfo;
|
||||
inFlight: Promise<DesktopAccountInfo> | null;
|
||||
}
|
||||
|
||||
const accountUpsertCache = new Map<string, AccountUpsertCacheEntry>();
|
||||
|
||||
const transportState = {
|
||||
baseURL: transportSession.baseURL,
|
||||
@@ -135,6 +147,43 @@ function createDesktopClient(baseURL: string): ApiClient {
|
||||
return client;
|
||||
}
|
||||
|
||||
function stableJson(value: unknown): string {
|
||||
if (Array.isArray(value)) {
|
||||
return `[${value.map((item) => stableJson(item)).join(",")}]`;
|
||||
}
|
||||
if (value && typeof value === "object") {
|
||||
return `{${Object.entries(value as Record<string, unknown>)
|
||||
.sort(([left], [right]) => left.localeCompare(right))
|
||||
.map(([key, item]) => `${JSON.stringify(key)}:${stableJson(item)}`)
|
||||
.join(",")}}`;
|
||||
}
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
function accountUpsertCacheKey(payload: UpsertDesktopAccountRequest): string {
|
||||
return [
|
||||
payload.platform.trim(),
|
||||
payload.platform_uid.trim(),
|
||||
].join("::");
|
||||
}
|
||||
|
||||
function accountUpsertSignature(payload: UpsertDesktopAccountRequest): string {
|
||||
return stableJson({
|
||||
platform: payload.platform.trim(),
|
||||
platform_uid: payload.platform_uid.trim(),
|
||||
account_fingerprint: payload.account_fingerprint ?? null,
|
||||
display_name: payload.display_name.trim(),
|
||||
avatar_url: payload.avatar_url ?? null,
|
||||
health: payload.health,
|
||||
tags: payload.tags ?? [],
|
||||
if_sync_version: payload.if_sync_version ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
function clearAccountUpsertCache(): void {
|
||||
accountUpsertCache.clear();
|
||||
}
|
||||
|
||||
function currentRequestDebugMode(): "main-process" | "renderer-devtools" {
|
||||
return canUseRendererDevtoolsProxy() ? "renderer-devtools" : "main-process";
|
||||
}
|
||||
@@ -319,6 +368,7 @@ export function configureTransport(session: DesktopRuntimeSessionSyncRequest | n
|
||||
transportState.auth = "pending";
|
||||
transportState.dispatchState = "idle";
|
||||
desktopApiClient = null;
|
||||
clearAccountUpsertCache();
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -332,6 +382,7 @@ export function configureTransport(session: DesktopRuntimeSessionSyncRequest | n
|
||||
transportState.dispatchState = session.client_token ? "connecting" : "idle";
|
||||
transportState.initializedAt = Date.now();
|
||||
desktopApiClient = createDesktopClient(transportSession.baseURL);
|
||||
clearAccountUpsertCache();
|
||||
return desktopApiClient;
|
||||
}
|
||||
|
||||
@@ -425,10 +476,52 @@ export async function retryDesktopPublishTask(taskId: string): Promise<CreatePub
|
||||
export async function upsertDesktopAccount(
|
||||
payload: UpsertDesktopAccountRequest,
|
||||
): Promise<DesktopAccountInfo> {
|
||||
return desktopPost<DesktopAccountInfo, UpsertDesktopAccountRequest>(
|
||||
const now = Date.now();
|
||||
const cacheKey = accountUpsertCacheKey(payload);
|
||||
const signature = accountUpsertSignature(payload);
|
||||
const cached = accountUpsertCache.get(cacheKey);
|
||||
if (cached && cached.signature === signature && cached.expiresAt > now) {
|
||||
if (cached.inFlight) {
|
||||
return cached.inFlight;
|
||||
}
|
||||
return cached.response;
|
||||
}
|
||||
|
||||
const request = desktopPost<DesktopAccountInfo, UpsertDesktopAccountRequest>(
|
||||
"/api/desktop/accounts",
|
||||
payload,
|
||||
);
|
||||
accountUpsertCache.set(cacheKey, {
|
||||
signature,
|
||||
expiresAt: now + ACCOUNT_UPSERT_CACHE_TTL_MS,
|
||||
response: cached?.response ?? ({} as DesktopAccountInfo),
|
||||
inFlight: request,
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await request;
|
||||
accountUpsertCache.set(cacheKey, {
|
||||
signature,
|
||||
expiresAt: Date.now() + ACCOUNT_UPSERT_CACHE_TTL_MS,
|
||||
response,
|
||||
inFlight: null,
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (accountUpsertCache.get(cacheKey)?.inFlight === request) {
|
||||
accountUpsertCache.delete(cacheKey);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function reportDesktopAccountHealth(
|
||||
payload: ReportDesktopAccountHealthRequest,
|
||||
): Promise<ReportDesktopAccountHealthResponse> {
|
||||
return desktopPost<ReportDesktopAccountHealthResponse, ReportDesktopAccountHealthRequest>(
|
||||
"/api/desktop/accounts/health-reports",
|
||||
payload,
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteDesktopAccount(
|
||||
@@ -436,9 +529,11 @@ export async function deleteDesktopAccount(
|
||||
ifSyncVersion: number,
|
||||
): Promise<DesktopAccountInfo> {
|
||||
const params = new URLSearchParams({ if_sync_version: String(ifSyncVersion) });
|
||||
return desktopDelete<DesktopAccountInfo>(
|
||||
const response = await desktopDelete<DesktopAccountInfo>(
|
||||
`/api/desktop/accounts/${encodeURIComponent(desktopId)}?${params.toString()}`,
|
||||
);
|
||||
clearAccountUpsertCache();
|
||||
return response;
|
||||
}
|
||||
|
||||
export async function leaseDesktopTask(
|
||||
|
||||
Reference in New Issue
Block a user