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 {
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<DesktopAccountInfo>
}
interface BoundWindowHandle {
window: BrowserWindow
webContents: WebContents
}
interface WeixinGzhConsoleState {
loggedOut: boolean
title: string
@@ -438,7 +448,11 @@ function appendAccountBindDiagnostic(event: Record<string, unknown>): 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<string, unknown>): void {
async function summarizeDoubaoAuthCookies(
session: Session,
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<{
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<ToutiaoMediaInfoResponse>(
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<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 {
if (window.isDestroyed()) {
return
@@ -4230,14 +4327,29 @@ export async function bindPublishAccount(platformId: string): Promise<DesktopAcc
loginUrl: diagnosticUrl(definition.loginUrl),
})
let window: BrowserWindow
let bindWebContents: WebContents
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) {
forgetSessionHandle(handle.accountId)
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) => {
let finished = false
let checking = false
@@ -4285,16 +4397,11 @@ export async function bindPublishAccount(platformId: string): Promise<DesktopAcc
}
const syncDetectionState = () => {
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<DesktopAcc
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(
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<DesktopAcc
const detectAndBind = async () => {
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<DesktopAcc
platform: definition.id,
detectReady,
canProbe,
loading: window.webContents.isLoading(),
loading: contents.isLoading(),
url: diagnosticUrl(currentURL),
partition: handle.partition,
})
@@ -4350,11 +4458,11 @@ export async function bindPublishAccount(platformId: string): Promise<DesktopAcc
platform: definition.id,
detectReady,
canProbe,
loading: window.webContents.isLoading(),
loading: contents.isLoading(),
url: currentURL,
})
const detected = await definition
.detect({ session: handle.session, webContents: window.webContents })
.detect({ session: handle.session, webContents: contents })
.catch((error) => {
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<DesktopAcc
console.info('[desktop-bind] toutiao console verification', {
platform: definition.id,
ready: consoleReady,
url: window.isDestroyed() ? '' : window.webContents.getURL(),
url: getBoundWindowURL(window),
})
if (!consoleReady) {
return
}
const detectedFromWorkbench = await definition
.detect({ session: handle.session, webContents: window.webContents })
.detect({ session: handle.session, webContents: contents })
.catch(() => null)
if (detectedFromWorkbench) {
normalizedDetected = sanitizeDetectedAccount(detectedFromWorkbench)
@@ -4421,14 +4529,14 @@ export async function bindPublishAccount(platformId: string): Promise<DesktopAcc
console.info('[desktop-bind] wangyihao console verification', {
platform: definition.id,
ready: consoleReady,
url: window.isDestroyed() ? '' : window.webContents.getURL(),
url: getBoundWindowURL(window),
})
if (!consoleReady) {
return
}
const detectedFromWorkbench = await definition
.detect({ session: handle.session, webContents: window.webContents })
.detect({ session: handle.session, webContents: contents })
.catch(() => null)
if (detectedFromWorkbench) {
normalizedDetected = sanitizeDetectedAccount(detectedFromWorkbench)
@@ -4437,7 +4545,7 @@ export async function bindPublishAccount(platformId: string): Promise<DesktopAcc
normalizedDetected = await finalizeDetectedAccountForBind(
definition,
{ session: handle.session, webContents: window.webContents },
{ session: handle.session, webContents: contents },
normalizedDetected,
)
@@ -521,7 +521,7 @@ function workbenchErrorDataURL(title: string, targetURL: string, message: string
<body>
<main>
<h1>${escapeHtml(title)}加载失败</h1>
<p>远程工作台暂时无法打开。可以使用窗口左上方的刷新按钮重试。</p>
<p>远程页面暂时无法打开。可以使用窗口左上方的刷新按钮重试。</p>
<code>${escapeHtml(host || targetURL)}
${escapeHtml(message)}</code>
@@ -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<boolean> {
const record = getRecordForWindow(window)
if (!record) {
return false
}
await navigateWorkbenchRemote(record, targetURL)
return true
}
export async function openWorkbenchWindow(options: WorkbenchWindowOptions): Promise<BrowserWindow> {
registerWorkbenchNavigationHandlers()