import fs from 'node:fs/promises' import { downloadRemoteQrImage, logBrowserSessionDebug, waitForFrame } from './session-login-shared.js' const QQ_QR_TARGET_SIZE = 144 export async function ensureQqLoginReady(page) { try { const frame = await waitForFrame( page, (item) => item.url().includes('xui.ptlogin2.qq.com/cgi-bin/xlogin'), 10_000, ) const switcher = frame.locator('#switcher_qlogin') if (!(await switcher.count())) { return } await switcher.click({ timeout: 3_000 }).catch(() => null) await frame.waitForTimeout(250) } catch { // ignore qq inner tab switch failures; the screenshot retry path will try again } } export async function captureQqQrImage(page, qrImagePath) { const frame = await waitForFrame( page, (item) => item.url().includes('xui.ptlogin2.qq.com/cgi-bin/xlogin'), 10_000, ) const qrUrl = await resolveQqQrImageUrl(frame) const effectiveQrUrl = upgradeQqQrImageUrl(qrUrl) logBrowserSessionDebug('qq.capture.qrUrl', { qrUrl, effectiveQrUrl, frameUrl: frame.url(), }) if (effectiveQrUrl) { const body = await downloadRemoteQrImage(page, effectiveQrUrl, qrImagePath, frame.url(), 'qq') logBrowserSessionDebug('qq.capture.downloadResult', { qrUrl: effectiveQrUrl, downloaded: Boolean(body?.length), qrImagePath, }) if (body?.length) { await fs.writeFile(qrImagePath, body) return } } logBrowserSessionDebug('qq.capture.fallbackScreenshot', { qrImagePath, }) const qrLocator = await findQqQrLocator(page, frame) try { await qrLocator.screenshot({ path: qrImagePath }) return } catch { const iframeLocator = page.locator('#milo-qcwx-frame-qc').first() await iframeLocator.waitFor({ state: 'visible', timeout: 15_000 }) await iframeLocator.screenshot({ path: qrImagePath }) } } export async function findQqQrLocator(page, frame = null) { const effectiveFrame = frame || await waitForFrame(page, (item) => item.url().includes('xui.ptlogin2.qq.com/cgi-bin/xlogin')) const locator = effectiveFrame.locator('#qrlogin_img') await locator.waitFor({ state: 'visible', timeout: 15_000 }) logBrowserSessionDebug('qq.findQrLocator.visibleReady', { frameUrl: effectiveFrame.url(), }) return locator } export async function extractQqQrState(page) { const frame = page.frames().find((item) => item.url().includes('xui.ptlogin2.qq.com/cgi-bin/xlogin')) if (!frame) { return { visible: false, scanned: false, expired: false, message: '', } } try { const { qrVisible, bodyText } = await frame .evaluate(() => { const isVisible = (element) => { if (!(element instanceof HTMLElement)) { return false } const style = window.getComputedStyle(element) const rect = element.getBoundingClientRect() return ( style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0' && rect.width > 0 && rect.height > 0 ) } const qrImage = document.querySelector('#qrlogin_img') const bodyText = String(document.body?.textContent || '').replace(/\s+/g, ' ').trim() return { qrVisible: qrImage instanceof HTMLImageElement && isVisible(qrImage) && Number(qrImage.naturalWidth || 0) > 0 && Number(qrImage.naturalHeight || 0) > 0, bodyText, } }) .catch(() => ({ qrVisible: false, bodyText: '', })) return { visible: true, qrVisible, scanned: !qrVisible && /扫描成功|请在手机上确认登录/.test(bodyText), expired: qrVisible ? false : /二维码失效|已失效/.test(bodyText), message: bodyText.slice(0, 200), frameUrl: frame.url(), } } catch { return { visible: true, qrVisible: false, scanned: false, expired: false, message: '', frameUrl: frame.url(), } } } async function resolveQqQrImageUrl(frame) { return frame .evaluate(() => { const qrImage = document.querySelector('#qrlogin_img') if (!(qrImage instanceof HTMLImageElement)) { return '' } return String(qrImage.currentSrc || qrImage.src || qrImage.getAttribute('src') || '').trim() }) .then((src) => { if (!src) { return '' } try { return new URL(src, frame.url()).toString() } catch { return '' } }) .catch(() => '') } function upgradeQqQrImageUrl(qrUrl) { if (!qrUrl) { return '' } try { const url = new URL(qrUrl) if (!/xui\.ptlogin2\.qq\.com$/i.test(url.hostname) || !/\/ptqrshow$/i.test(url.pathname)) { return qrUrl } const currentSize = Number(url.searchParams.get('d') || 0) if (!Number.isFinite(currentSize) || currentSize < QQ_QR_TARGET_SIZE) { url.searchParams.set('d', String(QQ_QR_TARGET_SIZE)) } return url.toString() } catch { return qrUrl } }