import { runtimeConfig } from '../../config/runtime.js' import { logDebug } from '../../utils/logger.js' const SESSION_DEBUG_ENABLED = Boolean(runtimeConfig.session.debug) type FrameLike = unknown type PageWithFrames = { frames: () => FrameLike[] waitForTimeout: (milliseconds: number) => Promise } type QrDownloadResponse = { ok: () => boolean status: () => number body: () => Promise } type PageWithRequestContext = { context: () => { request: { get: (url: string, options?: { failOnStatusCode?: boolean headers?: Record }) => Promise } } } export function logBrowserSessionDebug(scope: string, detail?: unknown): void { if (!SESSION_DEBUG_ENABLED) { return } logDebug('[browser/session]', scope, detail) } export async function waitForFrame( page: PageWithFrames, predicate: (frame: TFrame) => boolean, timeoutMs = 15_000, ): Promise { const startedAt = Date.now() while (Date.now() - startedAt < timeoutMs) { const matched = page.frames().find((frame): frame is TFrame => { try { return predicate(frame as TFrame) } catch { return false } }) if (matched) { return matched } await page.waitForTimeout(300) } throw new Error('未找到登录二维码 frame') } export async function downloadRemoteQrImage( page: PageWithRequestContext, qrUrl: string, qrImagePath: string, referer = '', loginType = 'unknown', ): Promise { try { const response = await page.context().request.get(qrUrl, { failOnStatusCode: false, headers: { referer, }, }) if (!response.ok()) { logBrowserSessionDebug(`${loginType}.capture.downloadNotOk`, { qrUrl, status: response.status(), }) return false } const body = await response.body() if (!body?.length) { return false } return body } catch (error) { logBrowserSessionDebug(`${loginType}.capture.downloadError`, { qrUrl, message: error instanceof Error ? error.message : String(error), }) return null } } export function isRetryableQrCaptureError(error: unknown): boolean { const message = error instanceof Error ? error.message : String(error || '') return ( message.includes('Frame was detached') || message.includes('Execution context was destroyed') || message.includes('Target page, context or browser has been closed') || message.includes('waiting for') || message.includes('Timeout') ) }