fix(tencent): 登录态校验区分接口层/页面层——接口 auth_data 判快速失效,页面验证「发表视频」按钮判登录完整;修复扫码后过早保存不完整 cookie(finder_uin 空)导致发布弹扫码框

This commit is contained in:
Qiufeng
2026-08-21 18:30:26 +08:00
parent 2ed56bb144
commit b96de7a2d2
+59 -40
View File
@@ -111,8 +111,10 @@ def format_str_for_short_title(origin_title: str) -> str:
return formatted_string return formatted_string
# 视频号助手登录态探测接口:errCode==0 表示登录有效,非 0(如 300330)表示失效/未登录 # 视频号助手登录态探测接口:auth_data 会返回 data.finderUser.finderUsername(视频号身份)
_TENCENT_AUTH_INFO_URL = "https://channels.weixin.qq.com/cgi-bin/mmfinderassistant-bin/auth/get_auth_info" # 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 = { _TENCENT_AUTH_HEADERS = {
"Content-Type": "application/json", "Content-Type": "application/json",
"Origin": "https://channels.weixin.qq.com", "Origin": "https://channels.weixin.qq.com",
@@ -124,7 +126,7 @@ _TENCENT_AUTH_HEADERS = {
def _cookie_auth_via_api(account_file) -> bool | None: def _cookie_auth_via_api(account_file) -> bool | None:
"""纯 HTTP 接口探测登录态(不开浏览器,快且稳)。 """纯 HTTP 接口探测登录态(不开浏览器,快且稳)。
返回 True=有效、False=失效、None=探测失败(网络/响应异常,需降级页面行为)。 返回 True=有效(已登录且绑定视频号身份)、False=失效/未绑定、None=探测失败(需降级页面行为)。
""" """
try: try:
import json import json
@@ -134,62 +136,55 @@ def _cookie_auth_via_api(account_file) -> bool | None:
state = json.load(fobj) state = json.load(fobj)
cookies = {c["name"]: c["value"] for c in state.get("cookies", [])} cookies = {c["name"]: c["value"] for c in state.get("cookies", [])}
resp = requests.post( resp = requests.post(
_TENCENT_AUTH_INFO_URL, _TENCENT_AUTH_DATA_URL,
headers=_TENCENT_AUTH_HEADERS, headers=_TENCENT_AUTH_HEADERS,
cookies=cookies, cookies=cookies,
json={}, json={},
timeout=10, timeout=10,
) )
body = resp.json() body = resp.json()
if body.get("errCode") == 0: if body.get("errCode") != 0:
return True return False # 未登录 / session 失效
if "errCode" in body: finder_user = (body.get("data") or {}).get("finderUser") or {}
return False if finder_user.get("finderUsername"):
return None return True # 已登录且绑定视频号身份
return False # 有 session 但未绑定视频号(登录不完整)
except Exception as exc: except Exception as exc:
tencent_logger.warning(_msg("⚠️", f"接口探测登录态异常({exc}),降级为页面行为检测")) tencent_logger.warning(_msg("⚠️", f"接口探测登录态异常({exc}),降级为页面行为检测"))
return None return None
async def cookie_auth(account_file): async def cookie_auth(account_file):
"""视频号登录态校验:接口探测优先(统一「测试」与「发布」口径),失败再降级页面行为。""" """视频号登录态校验:接口快速判失效 + 页面验证「发表视频」按钮判登录完整。
关键:sessionid 有效时接口 auth_data 仍能返回 finder 信息,但 localStorage 登录态
不完整(finder_uin 空)时页面会主动弹扫码框、无发表入口——接口层与页面层不一致。
因此接口只用于快速判失效,页面验证「发表视频」按钮才是登录完整的可靠标志。
"""
account_file = _resolve_account_file(account_file) account_file = _resolve_account_file(account_file)
# 1) 纯 HTTP 接口探测(快、准、统一,不开浏览器) # 1) 接口快速判失效(errCode≠0 或未绑定 finder => 一定失效,无需开浏览器)
via_api = _cookie_auth_via_api(account_file) 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: if via_api is False:
tencent_logger.info(_msg("🥹", "cookie 已失效(接口探测 errCode≠0),得重新登录一下")) tencent_logger.info(_msg("🥹", "cookie 已失效或未绑定视频号身份(接口探测),得重新登录一下"))
return False return False
# 2) 接口探测失败,降级为页面行为检测 # 2) 页面验证:登录完整 = 平台首页出现「发表视频」按钮
async with async_playwright() as playwright: async with async_playwright() as playwright:
browser = await playwright.chromium.launch(**_build_launch_kwargs(headless=True)) browser = await playwright.chromium.launch(**_build_launch_kwargs(headless=True))
try: try:
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, wait_until="domcontentloaded") await page.goto(TENCENT_HOME_URL, timeout=120000, wait_until="domcontentloaded")
publish_entry = page.locator("button.weui-desktop-btn", has_text="发表视频").first
# cookie 失效时, 页面先停在 post/create, 随后由前端 JS 跳转到登录页;
# 必须等待跳转完成再判断, 否则会误报"cookie 有效"
try: try:
await page.wait_for_url("**/login.html**", timeout=8000) await publish_entry.wait_for(state="visible", timeout=10000)
tencent_logger.info(_msg("🥹", "cookie 已失效(页面跳转到登录页),得重新登录一下")) tencent_logger.success(_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 return True
except Exception:
tencent_logger.info(_msg("🥹", "cookie 失效(平台首页未加载出发表入口,登录态不完整)"))
return False
except Exception as exc: except Exception as exc:
tencent_logger.warning(_msg("😵", f"cookie 校验时出错,按失效处理: {exc}")) tencent_logger.warning(_msg("😵", f"cookie 校验时出错,按失效处理: {exc}"))
return False 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: 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 "" current_url = page.url or ""
# 仍在登录页 => 未登录 # 1) 仍在登录页 => 未完成(即使 sessionid 已种下,登录流程尚未走完:可能在选号/跳转中)
if "login.html" in current_url: if "login.html" in current_url:
return False return False
# 已跳到平台页(首页/发布页/列表页):只要没有可见的二维码容器就算成功 # 2) 页面仍有 qrconnect 扫码 iframe => 未完成登录(登录态不完整会残留扫码框)
# 注意:残留的扫码 iframe 只隐藏未移除时也会在 page.frames 里,不能作为否决条件 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): if current_url.startswith(TENCENT_HOME_URL) or current_url.startswith(TENCENT_UPLOAD_URL) or current_url.startswith(TENCENT_MANAGE_URL):
for marker in [ for marker in [
page.locator("div.login-qrcode-wrap").first, page.locator("div.login-qrcode-wrap").first,
@@ -326,6 +341,10 @@ async def _is_tencent_login_completed(page: Page, context=None) -> bool:
continue continue
return True return True
# 6) cookie 兜底(已离开登录页且无扫码框)
if context is not None and await _has_tencent_login_cookie(context):
return True
return False return False