fix(tencent): adapt QR code extraction to new qrconnect login iframe

The WeChat Channels login page now embeds the QR code in an
open.weixin.qq.com/connect/qrconnect iframe whose img.qrcode src is a
relative path (e.g. /connect/qrcode/xxxx) instead of an inline
data:image/ URL. The previous logic looked for a login-for-iframe
frame and only accepted data:image/ sources, so login always failed
with '未获取到视频号登录二维码地址'.

Changes:
- Detect the qrconnect iframe and resolve the relative img src,
  download the QR image via the browser context and convert it to a
  data URL so downstream save/decode logic stays unchanged
- Keep the legacy login-for-iframe and selector fallbacks
- Tolerate QR extraction failure: the login flow now continues and
  lets the user scan directly in the headed browser instead of
  aborting the whole login
This commit is contained in:
snow-moonlight1
2026-07-21 13:42:10 +08:00
parent d32280c98b
commit f4693db9d7
+42 -3
View File
@@ -2,10 +2,12 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import base64
import inspect import inspect
import os import os
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from urllib.parse import urljoin
from patchright.async_api import Page from patchright.async_api import Page
from patchright.async_api import Playwright from patchright.async_api import Playwright
@@ -138,13 +140,40 @@ async def _extract_tencent_qrcode_src(page: Page) -> str:
try: try:
iframe_locator = page.frame_locator('[src*="login-for-iframe"]') iframe_locator = page.frame_locator('[src*="login-for-iframe"]')
qr_code_img = iframe_locator.locator('div#app img.qrcode').first qr_code_img = iframe_locator.locator('div#app img.qrcode').first
await qr_code_img.wait_for(state="visible", timeout=30000) await qr_code_img.wait_for(state="visible", timeout=8000)
src = await qr_code_img.get_attribute("src") src = await qr_code_img.get_attribute("src")
if src and src.startswith("data:image/"): if src and src.startswith("data:image/"):
return src return src
except Exception: except Exception:
pass pass
# 2026 新版登录页: 二维码在 open.weixin.qq.com/connect/qrconnect 的 iframe 里,
# img.qrcode 的 src 是相对路径(如 /connect/qrcode/xxxx), 需要下载后转成 data URL
for frame in page.frames:
if "open.weixin.qq.com/connect/qrconnect" not in frame.url:
continue
try:
qr_img = frame.locator("img.qrcode").first
await qr_img.wait_for(state="attached", timeout=15000)
src = None
for _ in range(20):
src = await qr_img.get_attribute("src")
if src:
break
await page.wait_for_timeout(500)
if not src:
continue
if src.startswith("data:image/"):
return src
abs_url = urljoin(frame.url, src)
resp = await page.context.request.get(abs_url)
if resp.ok:
body = await resp.body()
content_type = resp.headers.get("content-type", "image/png").split(";")[0]
return f"data:{content_type};base64,{base64.b64encode(body).decode()}"
except Exception:
continue
selector_candidates = [ selector_candidates = [
"div.login-qrcode-wrap img.qrcode", "div.login-qrcode-wrap img.qrcode",
"div.qrcode-wrap img.qrcode", "div.qrcode-wrap img.qrcode",
@@ -306,12 +335,12 @@ async def _refresh_tencent_qrcode(page: Page) -> None:
async def _wait_for_tencent_login( async def _wait_for_tencent_login(
page: Page, page: Page,
account_file: str, account_file: str,
qrcode_info: dict, qrcode_info: dict | None,
qrcode_callback=None, qrcode_callback=None,
poll_interval: int = 3, poll_interval: int = 3,
max_checks: int = 100, max_checks: int = 100,
) -> dict: ) -> dict:
qrcode_path = Path(qrcode_info["image_path"]) qrcode_path = Path(qrcode_info["image_path"]) if qrcode_info else None
scanned_logged = False scanned_logged = False
for _ in range(max_checks): for _ in range(max_checks):
if await _is_tencent_login_completed(page): if await _is_tencent_login_completed(page):
@@ -326,6 +355,7 @@ async def _wait_for_tencent_login(
tencent_logger.warning(_msg("😵", "二维码失效了,小人马上去刷新")) tencent_logger.warning(_msg("😵", "二维码失效了,小人马上去刷新"))
await _refresh_tencent_qrcode(page) await _refresh_tencent_qrcode(page)
await asyncio.sleep(1) await asyncio.sleep(1)
try:
qrcode_info = await _save_tencent_qrcode( qrcode_info = await _save_tencent_qrcode(
page, page,
account_file, account_file,
@@ -333,6 +363,8 @@ async def _wait_for_tencent_login(
qrcode_callback=qrcode_callback, qrcode_callback=qrcode_callback,
) )
qrcode_path = Path(qrcode_info["image_path"]) qrcode_path = Path(qrcode_info["image_path"])
except Exception as exc:
tencent_logger.warning(_msg("⚠️", f"刷新后未能重新提取二维码({exc}),请直接在浏览器窗口中扫码"))
await asyncio.sleep(poll_interval) await asyncio.sleep(poll_interval)
@@ -357,8 +389,15 @@ async def tencent_cookie_gen(
try: try:
page = await context.new_page() page = await context.new_page()
await page.goto(TENCENT_LOGIN_URL) await page.goto(TENCENT_LOGIN_URL)
try:
qrcode_info = await _save_tencent_qrcode(page, account_file, qrcode_callback=qrcode_callback) qrcode_info = await _save_tencent_qrcode(page, account_file, qrcode_callback=qrcode_callback)
qrcode_path = Path(qrcode_info["image_path"]) qrcode_path = Path(qrcode_info["image_path"])
except Exception as exc:
tencent_logger.warning(
_msg("⚠️", f"提取二维码图片失败({exc}),请直接在弹出的浏览器窗口中扫码,登录流程不受影响")
)
qrcode_info = None
qrcode_path = None
tencent_logger.info(_msg("🧍", "请扫码,小人正在耐心等待登录完成")) tencent_logger.info(_msg("🧍", "请扫码,小人正在耐心等待登录完成"))
result = await _wait_for_tencent_login( result = await _wait_for_tencent_login(
page, page,