Add browser toolbar to auth windows
This commit is contained in:
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user