From b96de7a2d2c18c3cb86124e540063d1bace68a51 Mon Sep 17 00:00:00 2001 From: Qiufeng Date: Fri, 21 Aug 2026 18:30:26 +0800 Subject: [PATCH] =?UTF-8?q?fix(tencent):=20=E7=99=BB=E5=BD=95=E6=80=81?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C=E5=8C=BA=E5=88=86=E6=8E=A5=E5=8F=A3=E5=B1=82?= =?UTF-8?q?/=E9=A1=B5=E9=9D=A2=E5=B1=82=E2=80=94=E2=80=94=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=20auth=5Fdata=20=E5=88=A4=E5=BF=AB=E9=80=9F=E5=A4=B1?= =?UTF-8?q?=E6=95=88=EF=BC=8C=E9=A1=B5=E9=9D=A2=E9=AA=8C=E8=AF=81=E3=80=8C?= =?UTF-8?q?=E5=8F=91=E8=A1=A8=E8=A7=86=E9=A2=91=E3=80=8D=E6=8C=89=E9=92=AE?= =?UTF-8?q?=E5=88=A4=E7=99=BB=E5=BD=95=E5=AE=8C=E6=95=B4=EF=BC=9B=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E6=89=AB=E7=A0=81=E5=90=8E=E8=BF=87=E6=97=A9=E4=BF=9D?= =?UTF-8?q?=E5=AD=98=E4=B8=8D=E5=AE=8C=E6=95=B4=20cookie=EF=BC=88finder=5F?= =?UTF-8?q?uin=20=E7=A9=BA=EF=BC=89=E5=AF=BC=E8=87=B4=E5=8F=91=E5=B8=83?= =?UTF-8?q?=E5=BC=B9=E6=89=AB=E7=A0=81=E6=A1=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- uploader/tencent_uploader/main.py | 99 ++++++++++++++++++------------- 1 file changed, 59 insertions(+), 40 deletions(-) diff --git a/uploader/tencent_uploader/main.py b/uploader/tencent_uploader/main.py index 7ff7bfd..1632828 100644 --- a/uploader/tencent_uploader/main.py +++ b/uploader/tencent_uploader/main.py @@ -111,8 +111,10 @@ def format_str_for_short_title(origin_title: str) -> str: return formatted_string -# 视频号助手登录态探测接口:errCode==0 表示登录有效,非 0(如 300330)表示失效/未登录 -_TENCENT_AUTH_INFO_URL = "https://channels.weixin.qq.com/cgi-bin/mmfinderassistant-bin/auth/get_auth_info" +# 视频号助手登录态探测接口:auth_data 会返回 data.finderUser.finderUsername(视频号身份) +# errCode==0 且 finderUsername 非空 => 登录完整有效; +# get_auth_info 只返回 authType,无法区分「已登录但未绑定视频号」,不能作为唯一判据 +_TENCENT_AUTH_DATA_URL = "https://channels.weixin.qq.com/cgi-bin/mmfinderassistant-bin/auth/auth_data" _TENCENT_AUTH_HEADERS = { "Content-Type": "application/json", "Origin": "https://channels.weixin.qq.com", @@ -124,7 +126,7 @@ _TENCENT_AUTH_HEADERS = { def _cookie_auth_via_api(account_file) -> bool | None: """纯 HTTP 接口探测登录态(不开浏览器,快且稳)。 - 返回 True=有效、False=失效、None=探测失败(网络/响应异常,需降级页面行为)。 + 返回 True=有效(已登录且绑定视频号身份)、False=失效/未绑定、None=探测失败(需降级页面行为)。 """ try: import json @@ -134,62 +136,55 @@ def _cookie_auth_via_api(account_file) -> bool | None: state = json.load(fobj) cookies = {c["name"]: c["value"] for c in state.get("cookies", [])} resp = requests.post( - _TENCENT_AUTH_INFO_URL, + _TENCENT_AUTH_DATA_URL, headers=_TENCENT_AUTH_HEADERS, cookies=cookies, json={}, timeout=10, ) body = resp.json() - if body.get("errCode") == 0: - return True - if "errCode" in body: - return False - return None + if body.get("errCode") != 0: + return False # 未登录 / session 失效 + finder_user = (body.get("data") or {}).get("finderUser") or {} + if finder_user.get("finderUsername"): + return True # 已登录且绑定视频号身份 + return False # 有 session 但未绑定视频号(登录不完整) except Exception as exc: tencent_logger.warning(_msg("⚠️", f"接口探测登录态异常({exc}),降级为页面行为检测")) return None async def cookie_auth(account_file): - """视频号登录态校验:接口探测优先(统一「测试」与「发布」口径),失败再降级页面行为。""" + """视频号登录态校验:接口快速判失效 + 页面验证「发表视频」按钮判登录完整。 + + 关键:sessionid 有效时接口 auth_data 仍能返回 finder 信息,但 localStorage 登录态 + 不完整(finder_uin 空)时页面会主动弹扫码框、无发表入口——接口层与页面层不一致。 + 因此接口只用于快速判失效,页面验证「发表视频」按钮才是登录完整的可靠标志。 + """ account_file = _resolve_account_file(account_file) - # 1) 纯 HTTP 接口探测(快、准、统一,不开浏览器) + # 1) 接口快速判失效(errCode≠0 或未绑定 finder => 一定失效,无需开浏览器) via_api = _cookie_auth_via_api(account_file) - if via_api is True: - tencent_logger.success(_msg("🥳", "cookie 有效(接口探测 errCode=0)")) - return True if via_api is False: - tencent_logger.info(_msg("🥹", "cookie 已失效(接口探测 errCode≠0),得重新登录一下")) + tencent_logger.info(_msg("🥹", "cookie 已失效或未绑定视频号身份(接口探测),得重新登录一下")) return False - # 2) 接口探测失败,降级为页面行为检测 + # 2) 页面验证:登录完整 = 平台首页出现「发表视频」按钮 async with async_playwright() as playwright: browser = await playwright.chromium.launch(**_build_launch_kwargs(headless=True)) try: 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, wait_until="domcontentloaded") - - # cookie 失效时, 页面先停在 post/create, 随后由前端 JS 跳转到登录页; - # 必须等待跳转完成再判断, 否则会误报"cookie 有效" + await page.goto(TENCENT_HOME_URL, timeout=120000, wait_until="domcontentloaded") + publish_entry = page.locator("button.weui-desktop-btn", has_text="发表视频").first try: - await page.wait_for_url("**/login.html**", timeout=8000) - tencent_logger.info(_msg("🥹", "cookie 已失效(页面跳转到登录页),得重新登录一下")) - return False + await publish_entry.wait_for(state="visible", timeout=10000) + tencent_logger.success(_msg("🥳", "cookie 有效(平台首页发表入口已加载)")) + return True 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 + tencent_logger.info(_msg("🥹", "cookie 失效(平台首页未加载出发表入口,登录态不完整)")) + return False except Exception as exc: tencent_logger.warning(_msg("😵", f"cookie 校验时出错,按失效处理: {exc}")) return False @@ -302,17 +297,37 @@ async def _has_tencent_login_cookie(context) -> bool: async def _is_tencent_login_completed(page: Page, context=None) -> bool: - # 首选:登录态 cookie 出现即视为成功(扫码+手机确认后浏览器必种 cookie) - if context is not None and await _has_tencent_login_cookie(context): - return True - current_url = page.url or "" - # 仍在登录页 => 未登录 + # 1) 仍在登录页 => 未完成(即使 sessionid 已种下,登录流程尚未走完:可能在选号/跳转中) if "login.html" in current_url: return False - # 已跳到平台页(首页/发布页/列表页):只要没有可见的二维码容器就算成功 - # 注意:残留的扫码 iframe 只隐藏未移除时也会在 page.frames 里,不能作为否决条件 + # 2) 页面仍有 qrconnect 扫码 iframe => 未完成登录(登录态不完整会残留扫码框) + try: + for fr in page.frames: + if "open.weixin.qq.com/connect/qrconnect" in (fr.url or ""): + return False + except Exception: + pass + + # 3) 平台首页出现「发表视频」按钮 => 登录完整(最可靠标志) + # (不完整登录时页面会延迟弹扫码框、且无发表入口) + try: + publish_entry = page.locator("button.weui-desktop-btn", has_text="发表视频").first + if await publish_entry.count() and await publish_entry.is_visible(): + return True + except Exception: + pass + + # 4) localStorage 已有视频号身份(finder_username 非空)=> 登录完整 + try: + finder_username = await page.evaluate("localStorage.getItem('finder_username')") + if finder_username: + return True + except Exception: + pass + + # 5) 已跳到平台页(首页/发布页/列表页)且无可见二维码容器 => 成功 if current_url.startswith(TENCENT_HOME_URL) or current_url.startswith(TENCENT_UPLOAD_URL) or current_url.startswith(TENCENT_MANAGE_URL): for marker in [ page.locator("div.login-qrcode-wrap").first, @@ -326,6 +341,10 @@ async def _is_tencent_login_completed(page: Page, context=None) -> bool: continue return True + # 6) cookie 兜底(已离开登录页且无扫码框) + if context is not None and await _has_tencent_login_cookie(context): + return True + return False