Add browser toolbar to auth windows

This commit is contained in:
2026-05-21 23:57:20 +08:00
parent 49b1ccc961
commit 53ba8ff4cf
2 changed files with 201 additions and 48 deletions
+155 -47
View File
@@ -2,8 +2,8 @@ import { createHash } from 'node:crypto'
import { import {
appendFileSync, appendFileSync,
mkdirSync, mkdirSync,
readFileSync,
readdirSync, readdirSync,
readFileSync,
statSync, statSync,
writeFileSync, writeFileSync,
} from 'node:fs' } from 'node:fs'
@@ -12,7 +12,7 @@ import { dirname, join } from 'node:path'
import type { DesktopAccountInfo } from '@geo/shared-types' import type { DesktopAccountInfo } from '@geo/shared-types'
import { aiPlatformCatalog } from '@geo/shared-types' import { aiPlatformCatalog } from '@geo/shared-types'
import type { Cookie, Session, WebContents } from 'electron/main' 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 { normalizeAccountPlatformUid, sameAccountPlatformUid } from '../shared/account-identity'
import { import {
@@ -72,7 +72,12 @@ import {
} from './session-registry' } from './session-registry'
import { upsertDesktopAccount } from './transport/api-client' import { upsertDesktopAccount } from './transport/api-client'
import { STANDARD_USER_AGENT } from './user-agent' import { STANDARD_USER_AGENT } from './user-agent'
import { openWorkbenchWindow } from './workbench-window' import {
getWorkbenchWindowWebContents,
isWorkbenchWindowReadyForDetection,
loadWorkbenchWindowURL,
openWorkbenchWindow,
} from './workbench-window'
interface DetectedAccount { interface DetectedAccount {
platformUid: string platformUid: string
@@ -118,6 +123,11 @@ interface ActiveBindOperation {
promise: Promise<DesktopAccountInfo> promise: Promise<DesktopAccountInfo>
} }
interface BoundWindowHandle {
window: BrowserWindow
webContents: WebContents
}
interface WeixinGzhConsoleState { interface WeixinGzhConsoleState {
loggedOut: boolean loggedOut: boolean
title: string title: string
@@ -438,7 +448,11 @@ function appendAccountBindDiagnostic(event: Record<string, unknown>): void {
const target = join(app.getPath('userData'), ACCOUNT_BIND_DIAGNOSTIC_FILE) const target = join(app.getPath('userData'), ACCOUNT_BIND_DIAGNOSTIC_FILE)
try { try {
mkdirSync(dirname(target), { recursive: true }) 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 { } catch {
// Best-effort diagnostics. Never block binding on local log writes. // Best-effort diagnostics. Never block binding on local log writes.
} }
@@ -447,7 +461,9 @@ function appendAccountBindDiagnostic(event: Record<string, unknown>): void {
async function summarizeDoubaoAuthCookies( async function summarizeDoubaoAuthCookies(
session: Session, session: Session,
urls: Iterable<string>, urls: Iterable<string>,
): Promise<Array<{ url: string | null; cookieCount: number; authCookieCount: number; authCookies: string[] }>> { ): Promise<
Array<{ url: string | null; cookieCount: number; authCookieCount: number; authCookies: string[] }>
> {
const summaries: Array<{ const summaries: Array<{
url: string | null url: string | null
cookieCount: number cookieCount: number
@@ -2498,14 +2514,16 @@ export async function probeAIAccountSession(
}) })
} }
if (definitionLooksLikeLoginRedirect(currentURL, { if (
id: platformMeta.id, definitionLooksLikeLoginRedirect(currentURL, {
label: platformMeta.label, id: platformMeta.id,
loginUrl: platformMeta.loginUrl, label: platformMeta.label,
consoleUrl: platformMeta.consoleUrl, loginUrl: platformMeta.loginUrl,
detect: (context: DetectContext) => consoleUrl: platformMeta.consoleUrl,
detectAIPlatformAccount(platformMeta.id, platformMeta.label, context), detect: (context: DetectContext) =>
})) { detectAIPlatformAccount(platformMeta.id, platformMeta.label, context),
})
) {
return { return {
verdict: 'expired', verdict: 'expired',
reason: 'login_redirect', reason: 'login_redirect',
@@ -3082,13 +3100,13 @@ async function waitForWindowNavigationSettled(
let stableSince = Date.now() let stableSince = Date.now()
while (!window.isDestroyed() && Date.now() - startedAt < timeoutMs) { while (!window.isDestroyed() && Date.now() - startedAt < timeoutMs) {
const currentURL = window.webContents.getURL() const currentURL = getBoundWindowURL(window)
if (currentURL !== lastURL) { if (currentURL !== lastURL) {
lastURL = currentURL lastURL = currentURL
stableSince = Date.now() stableSince = Date.now()
} }
if (!window.webContents.isLoading() && Date.now() - stableSince >= 450) { if (!isBoundWindowLoading(window) && Date.now() - stableSince >= 450) {
return return
} }
@@ -3130,14 +3148,18 @@ async function verifyToutiaoConsoleAccess(
return false return false
} }
await loadWindowURLSafely(window, TOUTIAO_CONSOLE_HOME_URL, '头条号创作台校验') await loadBoundWindowURLSafely(window, TOUTIAO_CONSOLE_HOME_URL, '头条号创作台校验')
await waitForWindowNavigationSettled(window) await waitForWindowNavigationSettled(window)
if (window.isDestroyed()) { if (window.isDestroyed()) {
return false return false
} }
const currentURL = window.webContents.getURL() const webContents = getBoundWindowWebContents(window)
if (!webContents) {
return false
}
const currentURL = webContents.getURL()
if (isToutiaoLoginURL(currentURL)) { if (isToutiaoLoginURL(currentURL)) {
return false return false
} }
@@ -3147,7 +3169,7 @@ async function verifyToutiaoConsoleAccess(
} }
const pageResponse = await pageFetchJson<ToutiaoMediaInfoResponse>( const pageResponse = await pageFetchJson<ToutiaoMediaInfoResponse>(
window.webContents, webContents,
'https://mp.toutiao.com/mp/agw/media/get_media_info', 'https://mp.toutiao.com/mp/agw/media/get_media_info',
{ {
headers: { headers: {
@@ -3228,7 +3250,11 @@ async function verifyWangyihaoConsoleAccess(
return false 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')) { if (!currentURL || currentURL.startsWith('data:text/html')) {
return false return false
} }
@@ -3238,7 +3264,7 @@ async function verifyWangyihaoConsoleAccess(
return false return false
} }
const pageState = await readWangyihaoConsoleState(window.webContents) const pageState = await readWangyihaoConsoleState(webContents)
if (pageState?.loggedOut) { if (pageState?.loggedOut) {
return false return false
} }
@@ -3272,7 +3298,11 @@ async function verifyWangyihaoBindWindowReady(
return false return false
} }
const currentURL = window.webContents.getURL() const webContents = getBoundWindowWebContents(window)
if (!webContents) {
return false
}
const currentURL = webContents.getURL()
const definition = platformBindingDefinitions.wangyihao const definition = platformBindingDefinitions.wangyihao
if (!currentURL || currentURL.startsWith('data:text/html')) { if (!currentURL || currentURL.startsWith('data:text/html')) {
return false return false
@@ -3281,7 +3311,7 @@ async function verifyWangyihaoBindWindowReady(
return false return false
} }
const pageState = await readWangyihaoConsoleState(window.webContents) const pageState = await readWangyihaoConsoleState(webContents)
if (pageState?.loggedOut) { if (pageState?.loggedOut) {
return false return false
} }
@@ -3325,9 +3355,14 @@ async function verifyZolConsoleAccess(
return false return false
} }
const webContents = getBoundWindowWebContents(window)
if (!webContents) {
return false
}
const detected = await detectZol({ const detected = await detectZol({
session, session,
webContents: window.webContents, webContents,
}).catch(() => null) }).catch(() => null)
return detected ? detectedAccountMatchesIdentity(detected, account) : false return detected ? detectedAccountMatchesIdentity(detected, account) : false
} }
@@ -4173,6 +4208,68 @@ function createBoundWindow(
return window return window
} }
async function createVisibleBoundWindow(
title: string,
targetURL: string,
sessionHandle: Session,
): Promise<BoundWindowHandle> {
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<void> {
if (await loadWorkbenchWindowURL(window, targetURL)) {
return
}
await loadWindowURLSafely(window, targetURL, context)
}
function focusBindWindow(window: BrowserWindow): void { function focusBindWindow(window: BrowserWindow): void {
if (window.isDestroyed()) { if (window.isDestroyed()) {
return return
@@ -4230,14 +4327,29 @@ export async function bindPublishAccount(platformId: string): Promise<DesktopAcc
loginUrl: diagnosticUrl(definition.loginUrl), loginUrl: diagnosticUrl(definition.loginUrl),
}) })
let window: BrowserWindow let window: BrowserWindow
let bindWebContents: WebContents
try { try {
window = createBoundWindow(`绑定 ${definition.label}`, definition.loginUrl, handle.session) const boundWindow = await createVisibleBoundWindow(
`绑定 ${definition.label}`,
definition.loginUrl,
handle.session,
)
window = boundWindow.window
bindWebContents = boundWindow.webContents
} catch (error) { } catch (error) {
forgetSessionHandle(handle.accountId) forgetSessionHandle(handle.accountId)
throw error instanceof Error ? error : new Error('desktop_account_bind_failed') throw error instanceof Error ? error : new Error('desktop_account_bind_failed')
} }
const currentBindWebContents = (): WebContents | null => {
if (window.isDestroyed()) {
return null
}
const contents = getWorkbenchWindowWebContents(window) ?? bindWebContents
return contents && !contents.isDestroyed() ? contents : null
}
const bindPromise = new Promise<DesktopAccountInfo>((resolve, reject) => { const bindPromise = new Promise<DesktopAccountInfo>((resolve, reject) => {
let finished = false let finished = false
let checking = false let checking = false
@@ -4285,16 +4397,11 @@ export async function bindPublishAccount(platformId: string): Promise<DesktopAcc
} }
const syncDetectionState = () => { const syncDetectionState = () => {
if (window.isDestroyed()) { if (!currentBindWebContents()) {
detectReady = false detectReady = false
return return
} }
const currentURL = window.webContents.getURL() detectReady = isBoundWindowReadyForDetection(window)
const fallbackReady =
/^https?:\/\//i.test(currentURL) &&
!currentURL.startsWith('data:text/html') &&
!window.webContents.isLoading()
detectReady = isWindowReadyForDetection(window) || fallbackReady
} }
const scheduleDetectAndBind = (delayMs = 120) => { const scheduleDetectAndBind = (delayMs = 120) => {
@@ -4315,11 +4422,11 @@ export async function bindPublishAccount(platformId: string): Promise<DesktopAcc
scheduleDetectAndBind() scheduleDetectAndBind()
} }
window.webContents.on('did-finish-load', handleNavigationReady) bindWebContents.on('did-finish-load', handleNavigationReady)
window.webContents.on('did-navigate', handleNavigationReady) bindWebContents.on('did-navigate', handleNavigationReady)
window.webContents.on('did-navigate-in-page', handleNavigationReady) bindWebContents.on('did-navigate-in-page', handleNavigationReady)
window.webContents.on('page-title-updated', () => scheduleDetectAndBind(250)) bindWebContents.on('page-title-updated', () => scheduleDetectAndBind(250))
window.webContents.on( bindWebContents.on(
'did-fail-load', 'did-fail-load',
(_event, errorCode, _errorDescription, _validatedURL, isMainFrame) => { (_event, errorCode, _errorDescription, _validatedURL, isMainFrame) => {
if (isMainFrame && errorCode !== -3) { if (isMainFrame && errorCode !== -3) {
@@ -4330,9 +4437,10 @@ export async function bindPublishAccount(platformId: string): Promise<DesktopAcc
const detectAndBind = async () => { const detectAndBind = async () => {
syncDetectionState() syncDetectionState()
const currentURL = window.isDestroyed() ? '' : window.webContents.getURL() const contents = currentBindWebContents()
const canProbe = detectReady || /^https?:\/\//i.test(currentURL) const currentURL = contents?.getURL() ?? ''
if (finished || checking || !canProbe || window.isDestroyed()) { const canProbe = Boolean(contents) && (detectReady || /^https?:\/\//i.test(currentURL))
if (finished || checking || !canProbe || !contents || window.isDestroyed()) {
return return
} }
checking = true checking = true
@@ -4342,7 +4450,7 @@ export async function bindPublishAccount(platformId: string): Promise<DesktopAcc
platform: definition.id, platform: definition.id,
detectReady, detectReady,
canProbe, canProbe,
loading: window.webContents.isLoading(), loading: contents.isLoading(),
url: diagnosticUrl(currentURL), url: diagnosticUrl(currentURL),
partition: handle.partition, partition: handle.partition,
}) })
@@ -4350,11 +4458,11 @@ export async function bindPublishAccount(platformId: string): Promise<DesktopAcc
platform: definition.id, platform: definition.id,
detectReady, detectReady,
canProbe, canProbe,
loading: window.webContents.isLoading(), loading: contents.isLoading(),
url: currentURL, url: currentURL,
}) })
const detected = await definition const detected = await definition
.detect({ session: handle.session, webContents: window.webContents }) .detect({ session: handle.session, webContents: contents })
.catch((error) => { .catch((error) => {
console.warn(`[desktop-bind] ${definition.id} detect failed`, { console.warn(`[desktop-bind] ${definition.id} detect failed`, {
message: error instanceof Error ? error.message : String(error), message: error instanceof Error ? error.message : String(error),
@@ -4398,14 +4506,14 @@ export async function bindPublishAccount(platformId: string): Promise<DesktopAcc
console.info('[desktop-bind] toutiao console verification', { console.info('[desktop-bind] toutiao console verification', {
platform: definition.id, platform: definition.id,
ready: consoleReady, ready: consoleReady,
url: window.isDestroyed() ? '' : window.webContents.getURL(), url: getBoundWindowURL(window),
}) })
if (!consoleReady) { if (!consoleReady) {
return return
} }
const detectedFromWorkbench = await definition const detectedFromWorkbench = await definition
.detect({ session: handle.session, webContents: window.webContents }) .detect({ session: handle.session, webContents: contents })
.catch(() => null) .catch(() => null)
if (detectedFromWorkbench) { if (detectedFromWorkbench) {
normalizedDetected = sanitizeDetectedAccount(detectedFromWorkbench) normalizedDetected = sanitizeDetectedAccount(detectedFromWorkbench)
@@ -4421,14 +4529,14 @@ export async function bindPublishAccount(platformId: string): Promise<DesktopAcc
console.info('[desktop-bind] wangyihao console verification', { console.info('[desktop-bind] wangyihao console verification', {
platform: definition.id, platform: definition.id,
ready: consoleReady, ready: consoleReady,
url: window.isDestroyed() ? '' : window.webContents.getURL(), url: getBoundWindowURL(window),
}) })
if (!consoleReady) { if (!consoleReady) {
return return
} }
const detectedFromWorkbench = await definition const detectedFromWorkbench = await definition
.detect({ session: handle.session, webContents: window.webContents }) .detect({ session: handle.session, webContents: contents })
.catch(() => null) .catch(() => null)
if (detectedFromWorkbench) { if (detectedFromWorkbench) {
normalizedDetected = sanitizeDetectedAccount(detectedFromWorkbench) normalizedDetected = sanitizeDetectedAccount(detectedFromWorkbench)
@@ -4437,7 +4545,7 @@ export async function bindPublishAccount(platformId: string): Promise<DesktopAcc
normalizedDetected = await finalizeDetectedAccountForBind( normalizedDetected = await finalizeDetectedAccountForBind(
definition, definition,
{ session: handle.session, webContents: window.webContents }, { session: handle.session, webContents: contents },
normalizedDetected, normalizedDetected,
) )
@@ -521,7 +521,7 @@ function workbenchErrorDataURL(title: string, targetURL: string, message: string
<body> <body>
<main> <main>
<h1>${escapeHtml(title)}加载失败</h1> <h1>${escapeHtml(title)}加载失败</h1>
<p>远程工作台暂时无法打开。可以使用窗口左上方的刷新按钮重试。</p> <p>远程页面暂时无法打开。可以使用窗口左上方的刷新按钮重试。</p>
<code>${escapeHtml(host || targetURL)} <code>${escapeHtml(host || targetURL)}
${escapeHtml(message)}</code> ${escapeHtml(message)}</code>
@@ -563,6 +563,19 @@ function getRecordForSender(sender: WebContents): WorkbenchWindowRecord | null {
return record 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 { function defaultNavigationState(): WorkbenchNavigationState {
return { return {
canGoBack: false, 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<boolean> {
const record = getRecordForWindow(window)
if (!record) {
return false
}
await navigateWorkbenchRemote(record, targetURL)
return true
}
export async function openWorkbenchWindow(options: WorkbenchWindowOptions): Promise<BrowserWindow> { export async function openWorkbenchWindow(options: WorkbenchWindowOptions): Promise<BrowserWindow> {
registerWorkbenchNavigationHandlers() registerWorkbenchNavigationHandlers()