327 lines
8.9 KiB
JavaScript
327 lines
8.9 KiB
JavaScript
import fs from 'node:fs/promises'
|
|
|
|
import { downloadRemoteQrImage, logBrowserSessionDebug, waitForFrame } from './session-login-shared.js'
|
|
|
|
export async function ensureWxLoginReady(page) {
|
|
try {
|
|
const frame = await waitForFrame(
|
|
page,
|
|
(item) => item.url().includes('open.weixin.qq.com/connect/qrconnect'),
|
|
10_000,
|
|
)
|
|
|
|
for (let attempt = 1; attempt <= 12; attempt += 1) {
|
|
const viewState = await inspectWxLoginView(frame)
|
|
logBrowserSessionDebug('wx.ensureQrMode.viewState', {
|
|
attempt,
|
|
frameUrl: frame.url(),
|
|
qrVisible: viewState.qrVisible,
|
|
quickLoginVisible: viewState.quickLoginVisible,
|
|
switchToNormalVisible: viewState.switchToNormalVisible,
|
|
qrCandidates: viewState.qrCandidates,
|
|
bodyText: viewState.bodyText.slice(0, 160),
|
|
})
|
|
|
|
if (viewState.qrVisible) {
|
|
return
|
|
}
|
|
|
|
if (
|
|
viewState.quickLoginVisible ||
|
|
viewState.switchToNormalVisible ||
|
|
/使用其他头像、昵称或账号|微信快捷登录/.test(viewState.bodyText)
|
|
) {
|
|
const switched = await switchWxQuickLoginToQr(frame)
|
|
logBrowserSessionDebug('wx.ensureQrMode.switchToNormal', {
|
|
attempt,
|
|
switched,
|
|
})
|
|
|
|
if (switched) {
|
|
await frame.waitForTimeout(700)
|
|
continue
|
|
}
|
|
}
|
|
|
|
await frame.waitForTimeout(400)
|
|
}
|
|
} catch {
|
|
// ignore wx quick-login switch failures; the screenshot retry path will try again
|
|
}
|
|
}
|
|
|
|
export async function captureWxQrImage(page, qrImagePath) {
|
|
const qrLocator = await findWxQrLocator(page)
|
|
const frame = await waitForFrame(
|
|
page,
|
|
(item) => item.url().includes('open.weixin.qq.com/connect/qrconnect'),
|
|
10_000,
|
|
)
|
|
|
|
const qrUrl = await resolveWxQrImageUrl(frame)
|
|
logBrowserSessionDebug('wx.capture.qrUrl', {
|
|
qrUrl,
|
|
frameUrl: frame.url(),
|
|
})
|
|
|
|
if (qrUrl) {
|
|
const downloaded = await downloadRemoteQrImage(page, qrUrl, qrImagePath, frame.url(), 'wx')
|
|
logBrowserSessionDebug('wx.capture.downloadResult', {
|
|
qrUrl,
|
|
downloaded: Boolean(downloaded?.length),
|
|
qrImagePath,
|
|
})
|
|
|
|
if (downloaded?.length) {
|
|
await fs.writeFile(qrImagePath, downloaded)
|
|
return
|
|
}
|
|
}
|
|
|
|
logBrowserSessionDebug('wx.capture.fallbackScreenshot', {
|
|
qrImagePath,
|
|
})
|
|
await qrLocator.screenshot({ path: qrImagePath })
|
|
}
|
|
|
|
export async function findWxQrLocator(page) {
|
|
await ensureWxLoginReady(page)
|
|
|
|
const frame = await waitForFrame(
|
|
page,
|
|
(item) => item.url().includes('open.weixin.qq.com/connect/qrconnect'),
|
|
)
|
|
const viewState = await inspectWxLoginView(frame)
|
|
logBrowserSessionDebug('wx.findQrLocator.beforeWait', {
|
|
frameUrl: frame.url(),
|
|
qrVisible: viewState.qrVisible,
|
|
qrCandidates: viewState.qrCandidates,
|
|
bodyText: viewState.bodyText.slice(0, 160),
|
|
})
|
|
|
|
const locator = frame.locator('.js_qrcode_img:visible').first()
|
|
await locator.waitFor({ state: 'visible', timeout: 15_000 })
|
|
logBrowserSessionDebug('wx.findQrLocator.visibleReady', {
|
|
frameUrl: frame.url(),
|
|
})
|
|
return locator
|
|
}
|
|
|
|
export async function extractWxQrState(page) {
|
|
const frame = page.frames().find((item) => item.url().includes('open.weixin.qq.com/connect/qrconnect'))
|
|
|
|
if (!frame) {
|
|
return {
|
|
visible: false,
|
|
scanned: false,
|
|
expired: false,
|
|
message: '',
|
|
}
|
|
}
|
|
|
|
try {
|
|
const qrVisible = await frame
|
|
.locator('.js_qrcode_img')
|
|
.first()
|
|
.isVisible()
|
|
.catch(() => false)
|
|
const bodyText = String(await frame.locator('body').textContent()).replace(/\s+/g, ' ').trim()
|
|
|
|
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 inspectWxLoginView(frame) {
|
|
return 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 quickLogin = document.querySelector('.js_quick_login')
|
|
const switchToNormal = document.querySelector('.js_switchToNormal')
|
|
const qrCandidates = Array.from(document.querySelectorAll('.js_qrcode_img')).map((item, index) => {
|
|
if (!(item instanceof HTMLImageElement)) {
|
|
return {
|
|
index,
|
|
visible: false,
|
|
src: '',
|
|
naturalWidth: 0,
|
|
}
|
|
}
|
|
|
|
return {
|
|
index,
|
|
visible: isVisible(item),
|
|
src: String(item.currentSrc || item.src || item.getAttribute('src') || '').trim(),
|
|
naturalWidth: Number(item.naturalWidth || 0),
|
|
}
|
|
})
|
|
const visibleQrCandidate = qrCandidates.find((item) => item.visible && item.naturalWidth > 0)
|
|
|
|
return {
|
|
qrVisible: Boolean(visibleQrCandidate),
|
|
quickLoginVisible: isVisible(quickLogin),
|
|
switchToNormalVisible: isVisible(switchToNormal),
|
|
qrCandidates,
|
|
bodyText: String(document.body?.textContent || '').replace(/\s+/g, ' ').trim(),
|
|
}
|
|
})
|
|
.catch(() => ({
|
|
qrVisible: false,
|
|
quickLoginVisible: false,
|
|
switchToNormalVisible: false,
|
|
qrCandidates: [],
|
|
bodyText: '',
|
|
}))
|
|
}
|
|
|
|
async function switchWxQuickLoginToQr(frame) {
|
|
const switcherCandidates = [
|
|
frame.locator('.js_switchToNormal:visible').first(),
|
|
frame.locator('.js_switchToNormal').first(),
|
|
frame.locator('button:has-text("使用其他头像、昵称或账号")').first(),
|
|
frame.locator('text=使用其他头像、昵称或账号').first(),
|
|
]
|
|
|
|
for (const candidate of switcherCandidates) {
|
|
const visible = await candidate.isVisible().catch(() => false)
|
|
|
|
if (!visible) {
|
|
continue
|
|
}
|
|
|
|
logBrowserSessionDebug('wx.switchToNormal.candidateVisible', {
|
|
candidateIndex: switcherCandidates.indexOf(candidate),
|
|
})
|
|
|
|
const clicked =
|
|
(await candidate
|
|
.click({ timeout: 2_000 })
|
|
.then(() => true)
|
|
.catch(() => false)) ||
|
|
(await candidate
|
|
.evaluate((element) => {
|
|
if (!(element instanceof HTMLElement)) {
|
|
return false
|
|
}
|
|
|
|
element.click()
|
|
return true
|
|
})
|
|
.catch(() => false))
|
|
|
|
if (clicked) {
|
|
logBrowserSessionDebug('wx.switchToNormal.clicked', {
|
|
candidateIndex: switcherCandidates.indexOf(candidate),
|
|
})
|
|
return true
|
|
}
|
|
}
|
|
|
|
return frame
|
|
.evaluate(() => {
|
|
const candidates = Array.from(document.querySelectorAll('button, a, div')).filter((element) => {
|
|
if (!(element instanceof HTMLElement)) {
|
|
return false
|
|
}
|
|
|
|
const text = String(element.textContent || '').replace(/\s+/g, ' ').trim()
|
|
const style = window.getComputedStyle(element)
|
|
const rect = element.getBoundingClientRect()
|
|
|
|
return (
|
|
/使用其他头像、昵称或账号/.test(text) &&
|
|
style.display !== 'none' &&
|
|
style.visibility !== 'hidden' &&
|
|
style.opacity !== '0' &&
|
|
rect.width > 0 &&
|
|
rect.height > 0
|
|
)
|
|
})
|
|
|
|
const target = candidates[0]
|
|
|
|
if (!(target instanceof HTMLElement)) {
|
|
return false
|
|
}
|
|
|
|
target.click()
|
|
return true
|
|
})
|
|
.catch(() => false)
|
|
}
|
|
|
|
async function resolveWxQrImageUrl(frame) {
|
|
return 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 qrImages = Array.from(document.querySelectorAll('.js_qrcode_img'))
|
|
const qrImage =
|
|
qrImages.find((item) => item instanceof HTMLImageElement && isVisible(item)) ||
|
|
qrImages.find((item) => item instanceof HTMLImageElement) ||
|
|
null
|
|
|
|
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(() => '')
|
|
}
|