Files
order_site/apps/backend/src/services/session/session-wx.js
T

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(() => '')
}