Merge pull request #253 from snow-moonlight1/fix/tencent-qrcode-extraction
fix(tencent): 适配视频号新版 qrconnect 登录二维码 + cookie_auth 误报修复 + 中文路径二维码解码
This commit is contained in:
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user