From 53bebb46be624c949c0d9dff195120286aae1095 Mon Sep 17 00:00:00 2001 From: liangxu Date: Sun, 26 Apr 2026 21:57:26 +0800 Subject: [PATCH] perf(bind): speed up account bind detection and runtime sync Replace the 1.8s polling interval with a 650ms tick plus a debounced navigation-driven detect, listen for page-title-updated to catch SPA transitions, and flush the session asynchronously. The bind handler now optimistically seeds the runtime account list via noteRuntimeAccountBound and fires the full server refresh in the background, so the UI reflects the new binding without waiting on a round-trip. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../desktop-client/src/main/account-binder.ts | 59 ++++++++++--------- apps/desktop-client/src/main/bootstrap.ts | 8 ++- .../src/main/runtime-controller.ts | 31 ++++++++++ .../src/renderer/views/AiPlatformsView.vue | 4 +- 4 files changed, 72 insertions(+), 30 deletions(-) diff --git a/apps/desktop-client/src/main/account-binder.ts b/apps/desktop-client/src/main/account-binder.ts index 8bf0a8f..1fdf021 100644 --- a/apps/desktop-client/src/main/account-binder.ts +++ b/apps/desktop-client/src/main/account-binder.ts @@ -1989,30 +1989,11 @@ export async function resolvePublishAccountProfile( } async function finalizeDetectedAccountForBind( - definition: PublishPlatformBindingDefinition, - context: DetectContext, + _definition: PublishPlatformBindingDefinition, + _context: DetectContext, detected: DetectedAccount, ): Promise { - if (detected.avatarUrl) { - return detected; - } - - let resolved = detected; - for (let attempt = 0; attempt < 3; attempt += 1) { - await sleep(400 + attempt * 250); - const retried = await definition.detect(context).catch(() => null); - if (!retried) { - continue; - } - - const sanitized = sanitizeDetectedAccount(retried); - resolved = sanitized; - if (sanitized.avatarUrl) { - break; - } - } - - return resolved; + return detected; } function isToutiaoLoginURL(url: string): boolean { @@ -2722,9 +2703,14 @@ export async function bindPublishAccount(platformId: string): Promise | null = null; const finalizeOperation = () => { clearInterval(intervalHandle); + if (detectDebounceHandle) { + clearTimeout(detectDebounceHandle); + detectDebounceHandle = null; + } clearActiveBindOperation(platformId, window); }; @@ -2772,9 +2758,28 @@ export async function bindPublishAccount(platformId: string): Promise { + if (finished || window.isDestroyed()) { + return; + } + if (detectDebounceHandle) { + clearTimeout(detectDebounceHandle); + } + detectDebounceHandle = setTimeout(() => { + detectDebounceHandle = null; + void detectAndBind(); + }, delayMs); + }; + + const handleNavigationReady = () => { + syncDetectionState(); + scheduleDetectAndBind(); + }; + + window.webContents.on("did-finish-load", handleNavigationReady); + window.webContents.on("did-navigate", handleNavigationReady); + window.webContents.on("did-navigate-in-page", handleNavigationReady); + window.webContents.on("page-title-updated", () => scheduleDetectAndBind(250)); window.webContents.on( "did-fail-load", (_event, errorCode, _errorDescription, _validatedURL, isMainFrame) => { @@ -2844,7 +2849,7 @@ export async function bindPublishAccount(platformId: string): Promise { void detectAndBind(); - }, 1800); + }, 650); window.on("closed", () => { clearActiveBindOperation(platformId, window); diff --git a/apps/desktop-client/src/main/bootstrap.ts b/apps/desktop-client/src/main/bootstrap.ts index ce93198..9a70f19 100644 --- a/apps/desktop-client/src/main/bootstrap.ts +++ b/apps/desktop-client/src/main/bootstrap.ts @@ -18,6 +18,7 @@ import { getPlaywrightCDPPort, startHiddenPlaywrightReaper } from "./playwright- import { registerRendererDevtoolsProxyTarget } from "./renderer-devtools-proxy"; import { onRuntimeInvalidated } from "./runtime-events"; import { + noteRuntimeAccountBound, refreshRuntimeAccounts, releaseRuntimeSession, syncRuntimeSession, @@ -289,7 +290,12 @@ function registerBridgeHandlers(): void { ipcMain.handle("desktop:runtime-snapshot", () => createRuntimeSnapshot()); safeHandle("desktop:bind-publish-account", async (_event, platformId: string) => { const account = (await bindPublishAccount(platformId)) as DesktopAccountInfo; - await refreshRuntimeAccounts(); + noteRuntimeAccountBound(account); + void refreshRuntimeAccounts().catch((error) => { + console.warn("[desktop-bind] background account refresh failed", { + message: error instanceof Error ? error.message : String(error), + }); + }); return account; }); safeHandle("desktop:refresh-runtime-accounts", async () => { diff --git a/apps/desktop-client/src/main/runtime-controller.ts b/apps/desktop-client/src/main/runtime-controller.ts index 57e6e0b..a0e3835 100644 --- a/apps/desktop-client/src/main/runtime-controller.ts +++ b/apps/desktop-client/src/main/runtime-controller.ts @@ -1058,6 +1058,37 @@ export async function refreshRuntimeAccounts(): Promise { await syncAccounts("manual"); } +export function noteRuntimeAccountBound(account: DesktopAccountInfo): void { + const normalizedAccount: DesktopAccountInfo = { + ...account, + platform_uid: normalizeAccountPlatformUid(account.platform_uid) || account.platform_uid, + }; + const existingIndex = state.accounts.findIndex((item) => + item.id === normalizedAccount.id + || ( + item.platform === normalizedAccount.platform + && sameAccountPlatformUid(item.platform_uid, normalizedAccount.platform_uid) + ), + ); + + if (existingIndex >= 0) { + state.accounts = state.accounts.map((item, index) => + index === existingIndex ? normalizedAccount : item, + ); + } else { + state.accounts = [normalizedAccount, ...state.accounts]; + } + + state.accountProfiles.set(normalizedAccount.id, { + platformUid: normalizedAccount.platform_uid, + displayName: normalizedAccount.display_name, + avatarUrl: normalizedAccount.avatar_url, + }); + state.lastAccountsSyncAt = Date.now(); + syncTrackedAccounts(state.accounts.map((item) => accountIdentityFromDesktopAccount(item))); + refreshAccountNames(); +} + export async function unbindRuntimeAccount(accountId: string, syncVersion: number): Promise { const matchedAccount = state.accounts.find((item) => item.id === accountId) ?? null; diff --git a/apps/desktop-client/src/renderer/views/AiPlatformsView.vue b/apps/desktop-client/src/renderer/views/AiPlatformsView.vue index 30fae0a..27d756d 100644 --- a/apps/desktop-client/src/renderer/views/AiPlatformsView.vue +++ b/apps/desktop-client/src/renderer/views/AiPlatformsView.vue @@ -17,7 +17,7 @@ import type { RuntimeAccount } from "../types"; type AccountRow = Readonly> & { tags: readonly string[] }; -const { snapshot, refreshAccounts, loading } = useDesktopRuntime(); +const { snapshot, refresh, refreshAccounts, loading } = useDesktopRuntime(); const bindPendingPlatformId = ref(null); const openPendingAccountId = ref(null); @@ -156,7 +156,7 @@ async function bindPlatform(platformId: string) { const account = await window.desktopBridge.app.bindPublishAccount(platformId); const platformLabel = desktopMonitoringMediaCatalog.find((item) => item.id === platformId)?.label ?? platformId; showActionNotification("success", "绑定成功", `${account.display_name} 已绑定到 ${platformLabel}`); - await refreshAccounts(); + await refresh(); } catch (error) { showClientActionError("bind-account", error); } finally {