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:
@@ -1,14 +1,16 @@
|
||||
import { computed, onMounted, onUnmounted, readonly, shallowRef } from "vue";
|
||||
|
||||
import type { DesktopRuntimeSnapshot } from "../types";
|
||||
import type { DesktopRuntimeSnapshot, RuntimeAccount } from "../types";
|
||||
|
||||
const RUNTIME_POLL_INTERVAL_MS = 15_000;
|
||||
const INVALIDATION_DEBOUNCE_MS = 250;
|
||||
|
||||
const snapshot = shallowRef<DesktopRuntimeSnapshot | null>(null);
|
||||
const loading = shallowRef(false);
|
||||
const error = shallowRef<string | null>(null);
|
||||
|
||||
let intervalHandle: ReturnType<typeof setInterval> | null = null;
|
||||
let invalidationRefreshHandle: ReturnType<typeof setTimeout> | null = null;
|
||||
let subscribers = 0;
|
||||
let visibilityListenerBound = false;
|
||||
let runtimeInvalidationUnsubscribe: (() => void) | null = null;
|
||||
@@ -17,8 +19,54 @@ function isPageVisible(): boolean {
|
||||
return typeof document === "undefined" || document.visibilityState === "visible";
|
||||
}
|
||||
|
||||
async function refreshRuntimeSnapshot() {
|
||||
loading.value = true;
|
||||
function accountSummaryPatch(
|
||||
current: DesktopRuntimeSnapshot,
|
||||
accounts: RuntimeAccount[],
|
||||
): DesktopRuntimeSnapshot["summary"] {
|
||||
const healthCounts = accounts.reduce<Record<string, number>>((acc, item) => {
|
||||
acc[item.health] = (acc[item.health] ?? 0) + 1;
|
||||
return acc;
|
||||
}, {});
|
||||
const blockingAccountCount = accounts.filter((item) =>
|
||||
["expired", "revoked", "challenge_required"].includes(item.authState),
|
||||
).length;
|
||||
|
||||
return {
|
||||
...current.summary,
|
||||
accountsBound: accounts.length,
|
||||
healthCounts,
|
||||
issuesOpen:
|
||||
current.tasks.filter((item) => ["unknown", "failed"].includes(item.status)).length
|
||||
+ blockingAccountCount,
|
||||
};
|
||||
}
|
||||
|
||||
function patchRuntimeAccount(accountId: string, account: RuntimeAccount | null): void {
|
||||
const current = snapshot.value;
|
||||
if (!current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const existingIndex = current.accounts.findIndex((item) => item.id === accountId);
|
||||
const accounts =
|
||||
account && existingIndex >= 0
|
||||
? current.accounts.map((item, index) => (index === existingIndex ? account : item))
|
||||
: account
|
||||
? [account, ...current.accounts]
|
||||
: current.accounts.filter((item) => item.id !== accountId);
|
||||
|
||||
snapshot.value = {
|
||||
...current,
|
||||
generatedAt: Date.now(),
|
||||
summary: accountSummaryPatch(current, accounts),
|
||||
accounts,
|
||||
};
|
||||
}
|
||||
|
||||
async function refreshRuntimeSnapshot(options: { silent?: boolean } = {}) {
|
||||
if (!options.silent) {
|
||||
loading.value = true;
|
||||
}
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
@@ -32,7 +80,9 @@ async function refreshRuntimeSnapshot() {
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : "runtime snapshot unavailable";
|
||||
} finally {
|
||||
loading.value = false;
|
||||
if (!options.silent) {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +105,48 @@ async function refreshAccounts() {
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshRuntimeAccount(accountId: string) {
|
||||
if (!window.desktopBridge?.app?.runtimeAccountSnapshot) {
|
||||
await refreshRuntimeSnapshot({ silent: true });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const account = await window.desktopBridge.app.runtimeAccountSnapshot(accountId);
|
||||
patchRuntimeAccount(accountId, account);
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : "runtime account snapshot unavailable";
|
||||
}
|
||||
}
|
||||
|
||||
async function probeAccount(accountId: string) {
|
||||
if (!window.desktopBridge?.app?.probeRuntimeAccount) {
|
||||
await refreshAccounts();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const account = await window.desktopBridge.app.probeRuntimeAccount(accountId);
|
||||
patchRuntimeAccount(accountId, account);
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : "probe account failed";
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleSilentSnapshotRefresh() {
|
||||
if (invalidationRefreshHandle) {
|
||||
clearTimeout(invalidationRefreshHandle);
|
||||
}
|
||||
|
||||
invalidationRefreshHandle = setTimeout(() => {
|
||||
invalidationRefreshHandle = null;
|
||||
if (isPageVisible()) {
|
||||
void refreshRuntimeSnapshot({ silent: true });
|
||||
}
|
||||
}, INVALIDATION_DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
if (!intervalHandle) {
|
||||
return;
|
||||
@@ -75,13 +167,13 @@ function syncPollingState() {
|
||||
}
|
||||
|
||||
intervalHandle = setInterval(() => {
|
||||
void refreshRuntimeSnapshot();
|
||||
void refreshRuntimeSnapshot({ silent: true });
|
||||
}, RUNTIME_POLL_INTERVAL_MS);
|
||||
}
|
||||
|
||||
function handleVisibilityChange() {
|
||||
if (isPageVisible()) {
|
||||
void refreshRuntimeSnapshot();
|
||||
void refreshRuntimeSnapshot({ silent: true });
|
||||
}
|
||||
|
||||
syncPollingState();
|
||||
@@ -101,10 +193,17 @@ function bindRuntimeInvalidationListener() {
|
||||
return;
|
||||
}
|
||||
|
||||
runtimeInvalidationUnsubscribe = window.desktopBridge.app.onRuntimeInvalidated(() => {
|
||||
if (isPageVisible()) {
|
||||
void refreshRuntimeSnapshot();
|
||||
runtimeInvalidationUnsubscribe = window.desktopBridge.app.onRuntimeInvalidated((event) => {
|
||||
if (!isPageVisible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.accountId) {
|
||||
void refreshRuntimeAccount(event.accountId);
|
||||
return;
|
||||
}
|
||||
|
||||
scheduleSilentSnapshotRefresh();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -120,6 +219,10 @@ function unbindVisibilityListener() {
|
||||
function unbindRuntimeInvalidationListener() {
|
||||
runtimeInvalidationUnsubscribe?.();
|
||||
runtimeInvalidationUnsubscribe = null;
|
||||
if (invalidationRefreshHandle) {
|
||||
clearTimeout(invalidationRefreshHandle);
|
||||
invalidationRefreshHandle = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function useDesktopRuntime() {
|
||||
@@ -151,6 +254,7 @@ export function useDesktopRuntime() {
|
||||
error: readonly(error),
|
||||
refresh: refreshRuntimeSnapshot,
|
||||
refreshAccounts,
|
||||
probeAccount,
|
||||
hasSnapshot: computed(() => snapshot.value !== null),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user