From f4693db9d724c5a962806396ff7bc254f7509a01 Mon Sep 17 00:00:00 2001 From: snow-moonlight1 Date: Tue, 21 Jul 2026 13:42:10 +0800 Subject: [PATCH 1/3] fix(tencent): adapt QR code extraction to new qrconnect login iframe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- uploader/tencent_uploader/main.py | 63 +++++++++++++++++++++++++------ 1 file changed, 51 insertions(+), 12 deletions(-) diff --git a/uploader/tencent_uploader/main.py b/uploader/tencent_uploader/main.py index 32feaa2..0143760 100644 --- a/uploader/tencent_uploader/main.py +++ b/uploader/tencent_uploader/main.py @@ -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 @@ -138,13 +140,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 +335,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 +355,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 +389,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, From bb1f12977a738e9963dacddf6b35a97130da3c85 Mon Sep 17 00:00:00 2001 From: snow-moonlight1 Date: Tue, 21 Jul 2026 14:03:11 +0800 Subject: [PATCH 2/3] fix(utils): decode QR image via np.fromfile for non-ASCII paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cv2.imread cannot open files under non-ASCII (e.g. Chinese) paths on Windows, so decode_qrcode_from_path() always returned None when the project lived under a directory like D:\视频全平台同步\, and the terminal QR re-print silently failed. Use np.fromfile + cv2.imdecode instead, which handles arbitrary paths. --- utils/login_qrcode.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/utils/login_qrcode.py b/utils/login_qrcode.py index dda3ed3..13c1cec 100644 --- a/utils/login_qrcode.py +++ b/utils/login_qrcode.py @@ -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 From 662633bb3d52fdf5469861bcfc46f7d6f4456412 Mon Sep 17 00:00:00 2001 From: snow-moonlight1 Date: Thu, 23 Jul 2026 00:32:57 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix(tencent):=20cookie=5Fauth=20=E8=AF=AF?= =?UTF-8?q?=E6=8A=A5=E6=9C=89=E6=95=88=20+=20=E4=B8=8A=E4=BC=A0=E9=A1=B5?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E8=B7=B3=E8=BD=AC=E5=89=8D=E7=BD=AE=E6=A3=80?= =?UTF-8?q?=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 视频号 cookie 失效时, post/create 页面先正常加载, 数秒后才被前端 JS 跳转到 login.html。原 cookie_auth 在跳转前检查, 误报 'cookie 有效', 放行后在上传页报出无关错误(如 '未找到视频号文件上传框')。 - cookie_auth: 等待 8 秒确认是否被踢到 login.html, 外加 qrconnect 登录 iframe 双保险, 消除误报 - open_upload_page: 同样的跳转检测前置, cookie 失效时报明确错误 '请重新扫码登录后再发布' --- uploader/tencent_uploader/main.py | 35 +++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/uploader/tencent_uploader/main.py b/uploader/tencent_uploader/main.py index 0143760..c053970 100644 --- a/uploader/tencent_uploader/main.py +++ b/uploader/tencent_uploader/main.py @@ -113,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 @@ -541,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():