Merge pull request #253 from snow-moonlight1/fix/tencent-qrcode-extraction

fix(tencent): 适配视频号新版 qrconnect 登录二维码 + cookie_auth 误报修复 + 中文路径二维码解码
This commit is contained in:
Alleria
2026-07-23 08:58:36 +08:00
committed by GitHub
2 changed files with 82 additions and 24 deletions
+75 -23
View File
@@ -2,10 +2,12 @@
from __future__ import annotations
import asyncio
import base64
import inspect
import os
from datetime import datetime
from pathlib import Path
from urllib.parse import urljoin
from patchright.async_api import Page
from patchright.async_api import Playwright
@@ -111,18 +113,22 @@ async def cookie_auth(account_file):
context = await browser.new_context(storage_state=account_file)
context = await set_init_script(context)
page = await context.new_page()
await page.goto(TENCENT_UPLOAD_URL)
await page.wait_for_url(TENCENT_UPLOAD_URL, timeout=5000)
await page.goto(TENCENT_UPLOAD_URL, wait_until="domcontentloaded")
login_markers = [
page.get_by_text("扫码登录", exact=True).first,
page.get_by_text("发表视频", exact=True).first,
page.get_by_role("button", name="发表").first,
]
if await login_markers[0].count():
tencent_logger.info(_msg("🥹", "cookie 已失效,得重新登录一下"))
# cookie 失效时, 页面先停在 post/create, 随后由前端 JS 跳转到登录页;
# 必须等待跳转完成再判断, 否则会误报"cookie 有效"
try:
await page.wait_for_url("**/login.html**", timeout=8000)
tencent_logger.info(_msg("🥹", "cookie 已失效(页面跳转到登录页),得重新登录一下"))
return False
except Exception:
pass # 8 秒内未跳转, 大概率已登录
# 双保险: 页面里出现微信扫码登录 iframe 也视为失效
for fr in page.frames:
if "open.weixin.qq.com/connect/qrconnect" in fr.url:
tencent_logger.info(_msg("🥹", "cookie 已失效(页面出现扫码登录框),得重新登录一下"))
return False
tencent_logger.success(_msg("🥳", "cookie 有效"))
return True
@@ -138,13 +144,40 @@ async def _extract_tencent_qrcode_src(page: Page) -> str:
try:
iframe_locator = page.frame_locator('[src*="login-for-iframe"]')
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")
if src and src.startswith("data:image/"):
return src
except Exception:
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 = [
"div.login-qrcode-wrap img.qrcode",
"div.qrcode-wrap img.qrcode",
@@ -306,12 +339,12 @@ async def _refresh_tencent_qrcode(page: Page) -> None:
async def _wait_for_tencent_login(
page: Page,
account_file: str,
qrcode_info: dict,
qrcode_info: dict | None,
qrcode_callback=None,
poll_interval: int = 3,
max_checks: int = 100,
) -> dict:
qrcode_path = Path(qrcode_info["image_path"])
qrcode_path = Path(qrcode_info["image_path"]) if qrcode_info else None
scanned_logged = False
for _ in range(max_checks):
if await _is_tencent_login_completed(page):
@@ -326,13 +359,16 @@ async def _wait_for_tencent_login(
tencent_logger.warning(_msg("😵", "二维码失效了,小人马上去刷新"))
await _refresh_tencent_qrcode(page)
await asyncio.sleep(1)
qrcode_info = await _save_tencent_qrcode(
page,
account_file,
previous_qrcode_path=qrcode_path,
qrcode_callback=qrcode_callback,
)
qrcode_path = Path(qrcode_info["image_path"])
try:
qrcode_info = await _save_tencent_qrcode(
page,
account_file,
previous_qrcode_path=qrcode_path,
qrcode_callback=qrcode_callback,
)
qrcode_path = Path(qrcode_info["image_path"])
except Exception as exc:
tencent_logger.warning(_msg("⚠️", f"刷新后未能重新提取二维码({exc}),请直接在浏览器窗口中扫码"))
await asyncio.sleep(poll_interval)
@@ -357,8 +393,15 @@ async def tencent_cookie_gen(
try:
page = await context.new_page()
await page.goto(TENCENT_LOGIN_URL)
qrcode_info = await _save_tencent_qrcode(page, account_file, qrcode_callback=qrcode_callback)
qrcode_path = Path(qrcode_info["image_path"])
try:
qrcode_info = await _save_tencent_qrcode(page, account_file, qrcode_callback=qrcode_callback)
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("🧍", "请扫码,小人正在耐心等待登录完成"))
result = await _wait_for_tencent_login(
page,
@@ -502,7 +545,16 @@ class TencentBaseUploader(BaseVideoUploader):
async def open_upload_page(self, page: Page) -> None:
await page.goto(TENCENT_UPLOAD_URL, timeout=120000, wait_until="domcontentloaded")
await page.wait_for_url(TENCENT_UPLOAD_URL, timeout=120000)
# cookie 失效时前端 JS 会跳转到登录页, 提前发现并报明确的错误
redirected = True
try:
await page.wait_for_url("**/login.html**", timeout=8000)
except Exception:
redirected = False # 8 秒内未跳转, 正常
if redirected or any(
"open.weixin.qq.com/connect/qrconnect" in fr.url for fr in page.frames
):
raise RuntimeError("视频号 cookie 已失效(被跳转到登录页),请重新扫码登录后再发布")
async def upload_video_file(self, page: Page, file_path: str) -> None:
async def find_file_input():
+7 -1
View File
@@ -35,7 +35,13 @@ def remove_qrcode_file(qrcode_path: Path | None) -> bool:
def decode_qrcode_from_path(qrcode_path: Path) -> str | None:
image = cv2.imread(str(qrcode_path))
# Windows 下 cv2.imread 不支持中文路径, 改用 np.fromfile + imdecode
import numpy as np
try:
image = cv2.imdecode(np.fromfile(str(qrcode_path), dtype=np.uint8), cv2.IMREAD_COLOR)
except Exception:
return None
if image is None:
return None