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 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
@@ -111,18 +113,22 @@ async def cookie_auth(account_file):
context = await browser.new_context(storage_state=account_file) context = await browser.new_context(storage_state=account_file)
context = await set_init_script(context) context = await set_init_script(context)
page = await context.new_page() page = await context.new_page()
await page.goto(TENCENT_UPLOAD_URL) await page.goto(TENCENT_UPLOAD_URL, wait_until="domcontentloaded")
await page.wait_for_url(TENCENT_UPLOAD_URL, timeout=5000)
login_markers = [ # cookie 失效时, 页面先停在 post/create, 随后由前端 JS 跳转到登录页;
page.get_by_text("扫码登录", exact=True).first, # 必须等待跳转完成再判断, 否则会误报"cookie 有效"
page.get_by_text("发表视频", exact=True).first, try:
page.get_by_role("button", name="发表").first, await page.wait_for_url("**/login.html**", timeout=8000)
] tencent_logger.info(_msg("🥹", "cookie 已失效(页面跳转到登录页),得重新登录一下"))
if await login_markers[0].count():
tencent_logger.info(_msg("🥹", "cookie 已失效,得重新登录一下"))
return False 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 有效")) tencent_logger.success(_msg("🥳", "cookie 有效"))
return True return True
@@ -138,13 +144,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 +339,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,13 +359,16 @@ 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)
qrcode_info = await _save_tencent_qrcode( try:
page, qrcode_info = await _save_tencent_qrcode(
account_file, page,
previous_qrcode_path=qrcode_path, account_file,
qrcode_callback=qrcode_callback, previous_qrcode_path=qrcode_path,
) 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 +393,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)
qrcode_info = await _save_tencent_qrcode(page, account_file, qrcode_callback=qrcode_callback) try:
qrcode_path = Path(qrcode_info["image_path"]) 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("🧍", "请扫码,小人正在耐心等待登录完成")) tencent_logger.info(_msg("🧍", "请扫码,小人正在耐心等待登录完成"))
result = await _wait_for_tencent_login( result = await _wait_for_tencent_login(
page, page,
@@ -502,7 +545,16 @@ class TencentBaseUploader(BaseVideoUploader):
async def open_upload_page(self, page: Page) -> None: async def open_upload_page(self, page: Page) -> None:
await page.goto(TENCENT_UPLOAD_URL, timeout=120000, wait_until="domcontentloaded") 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 upload_video_file(self, page: Page, file_path: str) -> None:
async def find_file_input(): 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: 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: if image is None:
return None return None