diff --git a/apps/desktop-client/src/main/account-binder.ts b/apps/desktop-client/src/main/account-binder.ts index 892e106..baa474b 100644 --- a/apps/desktop-client/src/main/account-binder.ts +++ b/apps/desktop-client/src/main/account-binder.ts @@ -2,8 +2,8 @@ import { createHash } from 'node:crypto' import { appendFileSync, mkdirSync, - readFileSync, readdirSync, + readFileSync, statSync, writeFileSync, } from 'node:fs' @@ -12,7 +12,7 @@ import { dirname, join } from 'node:path' import type { DesktopAccountInfo } from '@geo/shared-types' import { aiPlatformCatalog } from '@geo/shared-types' import type { Cookie, Session, WebContents } from 'electron/main' -import { BrowserWindow, app, session as electronSession, nativeTheme } from 'electron/main' +import { app, BrowserWindow, session as electronSession, nativeTheme } from 'electron/main' import { normalizeAccountPlatformUid, sameAccountPlatformUid } from '../shared/account-identity' import { @@ -72,7 +72,12 @@ import { } from './session-registry' import { upsertDesktopAccount } from './transport/api-client' import { STANDARD_USER_AGENT } from './user-agent' -import { openWorkbenchWindow } from './workbench-window' +import { + getWorkbenchWindowWebContents, + isWorkbenchWindowReadyForDetection, + loadWorkbenchWindowURL, + openWorkbenchWindow, +} from './workbench-window' interface DetectedAccount { platformUid: string @@ -118,6 +123,11 @@ interface ActiveBindOperation { promise: Promise } +interface BoundWindowHandle { + window: BrowserWindow + webContents: WebContents +} + interface WeixinGzhConsoleState { loggedOut: boolean title: string @@ -438,7 +448,11 @@ function appendAccountBindDiagnostic(event: Record): void { const target = join(app.getPath('userData'), ACCOUNT_BIND_DIAGNOSTIC_FILE) try { mkdirSync(dirname(target), { recursive: true }) - appendFileSync(target, `${JSON.stringify({ at: new Date().toISOString(), ...event })}\n`, 'utf8') + appendFileSync( + target, + `${JSON.stringify({ at: new Date().toISOString(), ...event })}\n`, + 'utf8', + ) } catch { // Best-effort diagnostics. Never block binding on local log writes. } @@ -447,7 +461,9 @@ function appendAccountBindDiagnostic(event: Record): void { async function summarizeDoubaoAuthCookies( session: Session, urls: Iterable, -): Promise> { +): Promise< + Array<{ url: string | null; cookieCount: number; authCookieCount: number; authCookies: string[] }> +> { const summaries: Array<{ url: string | null cookieCount: number @@ -2498,14 +2514,16 @@ export async function probeAIAccountSession( }) } - if (definitionLooksLikeLoginRedirect(currentURL, { - id: platformMeta.id, - label: platformMeta.label, - loginUrl: platformMeta.loginUrl, - consoleUrl: platformMeta.consoleUrl, - detect: (context: DetectContext) => - detectAIPlatformAccount(platformMeta.id, platformMeta.label, context), - })) { + if ( + definitionLooksLikeLoginRedirect(currentURL, { + id: platformMeta.id, + label: platformMeta.label, + loginUrl: platformMeta.loginUrl, + consoleUrl: platformMeta.consoleUrl, + detect: (context: DetectContext) => + detectAIPlatformAccount(platformMeta.id, platformMeta.label, context), + }) + ) { return { verdict: 'expired', reason: 'login_redirect', @@ -3082,13 +3100,13 @@ async function waitForWindowNavigationSettled( let stableSince = Date.now() while (!window.isDestroyed() && Date.now() - startedAt < timeoutMs) { - const currentURL = window.webContents.getURL() + const currentURL = getBoundWindowURL(window) if (currentURL !== lastURL) { lastURL = currentURL stableSince = Date.now() } - if (!window.webContents.isLoading() && Date.now() - stableSince >= 450) { + if (!isBoundWindowLoading(window) && Date.now() - stableSince >= 450) { return } @@ -3130,14 +3148,18 @@ async function verifyToutiaoConsoleAccess( return false } - await loadWindowURLSafely(window, TOUTIAO_CONSOLE_HOME_URL, '头条号创作台校验') + await loadBoundWindowURLSafely(window, TOUTIAO_CONSOLE_HOME_URL, '头条号创作台校验') await waitForWindowNavigationSettled(window) if (window.isDestroyed()) { return false } - const currentURL = window.webContents.getURL() + const webContents = getBoundWindowWebContents(window) + if (!webContents) { + return false + } + const currentURL = webContents.getURL() if (isToutiaoLoginURL(currentURL)) { return false } @@ -3147,7 +3169,7 @@ async function verifyToutiaoConsoleAccess( } const pageResponse = await pageFetchJson( - window.webContents, + webContents, 'https://mp.toutiao.com/mp/agw/media/get_media_info', { headers: { @@ -3228,7 +3250,11 @@ async function verifyWangyihaoConsoleAccess( return false } - const currentURL = window.webContents.getURL() + const webContents = getBoundWindowWebContents(window) + if (!webContents) { + return false + } + const currentURL = webContents.getURL() if (!currentURL || currentURL.startsWith('data:text/html')) { return false } @@ -3238,7 +3264,7 @@ async function verifyWangyihaoConsoleAccess( return false } - const pageState = await readWangyihaoConsoleState(window.webContents) + const pageState = await readWangyihaoConsoleState(webContents) if (pageState?.loggedOut) { return false } @@ -3272,7 +3298,11 @@ async function verifyWangyihaoBindWindowReady( return false } - const currentURL = window.webContents.getURL() + const webContents = getBoundWindowWebContents(window) + if (!webContents) { + return false + } + const currentURL = webContents.getURL() const definition = platformBindingDefinitions.wangyihao if (!currentURL || currentURL.startsWith('data:text/html')) { return false @@ -3281,7 +3311,7 @@ async function verifyWangyihaoBindWindowReady( return false } - const pageState = await readWangyihaoConsoleState(window.webContents) + const pageState = await readWangyihaoConsoleState(webContents) if (pageState?.loggedOut) { return false } @@ -3325,9 +3355,14 @@ async function verifyZolConsoleAccess( return false } + const webContents = getBoundWindowWebContents(window) + if (!webContents) { + return false + } + const detected = await detectZol({ session, - webContents: window.webContents, + webContents, }).catch(() => null) return detected ? detectedAccountMatchesIdentity(detected, account) : false } @@ -4173,6 +4208,68 @@ function createBoundWindow( return window } +async function createVisibleBoundWindow( + title: string, + targetURL: string, + sessionHandle: Session, +): Promise { + const window = await openWorkbenchWindow({ + title, + targetURL, + session: sessionHandle, + }) + return { + window, + webContents: getWorkbenchWindowWebContents(window) ?? window.webContents, + } +} + +function getBoundWindowWebContents(window: BrowserWindow): WebContents | null { + if (window.isDestroyed()) { + return null + } + + const contents = getWorkbenchWindowWebContents(window) ?? window.webContents + return contents && !contents.isDestroyed() ? contents : null +} + +function getBoundWindowURL(window: BrowserWindow): string { + return getBoundWindowWebContents(window)?.getURL() ?? '' +} + +function isBoundWindowLoading(window: BrowserWindow): boolean { + return getBoundWindowWebContents(window)?.isLoading() ?? false +} + +function isBoundWindowReadyForDetection(window: BrowserWindow): boolean { + const contents = getBoundWindowWebContents(window) + if (!contents) { + return false + } + + const currentURL = contents.getURL() + const fallbackReady = + /^https?:\/\//i.test(currentURL) && + !currentURL.startsWith('data:text/html') && + !contents.isLoading() + + return ( + isWorkbenchWindowReadyForDetection(window) || isWindowReadyForDetection(window) || fallbackReady + ) +} + +async function loadBoundWindowURLSafely( + window: BrowserWindow, + targetURL: string, + context: string, +): Promise { + if (await loadWorkbenchWindowURL(window, targetURL)) { + return + } + + await loadWindowURLSafely(window, targetURL, context) +} + function focusBindWindow(window: BrowserWindow): void { if (window.isDestroyed()) { return @@ -4230,14 +4327,29 @@ export async function bindPublishAccount(platformId: string): Promise { + if (window.isDestroyed()) { + return null + } + const contents = getWorkbenchWindowWebContents(window) ?? bindWebContents + return contents && !contents.isDestroyed() ? contents : null + } + const bindPromise = new Promise((resolve, reject) => { let finished = false let checking = false @@ -4285,16 +4397,11 @@ export async function bindPublishAccount(platformId: string): Promise { - if (window.isDestroyed()) { + if (!currentBindWebContents()) { detectReady = false return } - const currentURL = window.webContents.getURL() - const fallbackReady = - /^https?:\/\//i.test(currentURL) && - !currentURL.startsWith('data:text/html') && - !window.webContents.isLoading() - detectReady = isWindowReadyForDetection(window) || fallbackReady + detectReady = isBoundWindowReadyForDetection(window) } const scheduleDetectAndBind = (delayMs = 120) => { @@ -4315,11 +4422,11 @@ export async function bindPublishAccount(platformId: string): Promise scheduleDetectAndBind(250)) - window.webContents.on( + bindWebContents.on('did-finish-load', handleNavigationReady) + bindWebContents.on('did-navigate', handleNavigationReady) + bindWebContents.on('did-navigate-in-page', handleNavigationReady) + bindWebContents.on('page-title-updated', () => scheduleDetectAndBind(250)) + bindWebContents.on( 'did-fail-load', (_event, errorCode, _errorDescription, _validatedURL, isMainFrame) => { if (isMainFrame && errorCode !== -3) { @@ -4330,9 +4437,10 @@ export async function bindPublishAccount(platformId: string): Promise { syncDetectionState() - const currentURL = window.isDestroyed() ? '' : window.webContents.getURL() - const canProbe = detectReady || /^https?:\/\//i.test(currentURL) - if (finished || checking || !canProbe || window.isDestroyed()) { + const contents = currentBindWebContents() + const currentURL = contents?.getURL() ?? '' + const canProbe = Boolean(contents) && (detectReady || /^https?:\/\//i.test(currentURL)) + if (finished || checking || !canProbe || !contents || window.isDestroyed()) { return } checking = true @@ -4342,7 +4450,7 @@ export async function bindPublishAccount(platformId: string): Promise { console.warn(`[desktop-bind] ${definition.id} detect failed`, { message: error instanceof Error ? error.message : String(error), @@ -4398,14 +4506,14 @@ export async function bindPublishAccount(platformId: string): Promise null) if (detectedFromWorkbench) { normalizedDetected = sanitizeDetectedAccount(detectedFromWorkbench) @@ -4421,14 +4529,14 @@ export async function bindPublishAccount(platformId: string): Promise null) if (detectedFromWorkbench) { normalizedDetected = sanitizeDetectedAccount(detectedFromWorkbench) @@ -4437,7 +4545,7 @@ export async function bindPublishAccount(platformId: string): Promise

${escapeHtml(title)}加载失败

-

远程工作台暂时无法打开。可以使用窗口左上方的刷新按钮重试。

+

远程页面暂时无法打开。可以使用窗口左上方的刷新按钮重试。

${escapeHtml(host || targetURL)} ${escapeHtml(message)} @@ -563,6 +563,19 @@ function getRecordForSender(sender: WebContents): WorkbenchWindowRecord | null { return record } +function getRecordForWindow(window: BrowserWindow): WorkbenchWindowRecord | null { + const record = workbenchWindows.get(window.webContents.id) + if ( + !record || + record.window !== window || + record.window.isDestroyed() || + record.view.webContents.isDestroyed() + ) { + return null + } + return record +} + function defaultNavigationState(): WorkbenchNavigationState { return { canGoBack: false, @@ -740,6 +753,38 @@ async function navigateWorkbenchRemote( } } +export function getWorkbenchWindowWebContents(window: BrowserWindow): WebContents | null { + return getRecordForWindow(window)?.view.webContents ?? null +} + +export function isWorkbenchWindowReadyForDetection(window: BrowserWindow): boolean { + const record = getRecordForWindow(window) + if (!record) { + return false + } + + const currentURL = record.view.webContents.getURL() + return ( + isRemoteHTTPURL(currentURL) && + !isLocalWorkbenchPage(currentURL) && + !record.loading && + !record.view.webContents.isLoading() + ) +} + +export async function loadWorkbenchWindowURL( + window: BrowserWindow, + targetURL: string, +): Promise { + const record = getRecordForWindow(window) + if (!record) { + return false + } + + await navigateWorkbenchRemote(record, targetURL) + return true +} + export async function openWorkbenchWindow(options: WorkbenchWindowOptions): Promise { registerWorkbenchNavigationHandlers()