fix(tencent): 上传卡住时要说话 —— iframe 等待 / 重试上限 / 超时

三个问题都是「失败了但看起来像正常」,实测于 2026-08-11。

1. open_upload_page 不等 iframe 加载完
   上传表单在 micro/content/post/create 这个 iframe 里,domcontentloaded
   时它还是空的。紧接着找 input[type=file] 找不到,报「未找到视频号文件
   上传框」——而失败截图上左栏渲染正常、主内容区一片空白,完全不像是
   加载没完成。加一个 networkidle 等待。

2. 找上传框的重试窗口只有 20 秒
   实测这个 iframe 挂上 input 要几十秒。提到 60 秒,并在最终失败时留
   截图 + url + frames:这个错误的可能原因太多(没登录 / 落到首页 /
   iframe 没加载完 / 平台改版),只看错误字符串没法区分。

3. wait_for_upload_complete 是没有出口的 while True  ← 最严重
   上传出错就删掉重传,失败再删再传,永远循环;中间每 2 秒打一行
   「正在上传视频中...」——这条日志和真的在传一模一样,从外面完全分不出。

   实测 172MB / 上行 ~0.5Mbps:每次传到 4 分钟左右报错然后重来,
   循环了近 2 小时也不会停,进程也不会退。用户看到的只有「正在上传
   视频中」,真相是同一段视频被反复上传了 20 多次。

   改成 1 小时超时 + 3 次重试上限,两个都带现场截图报明确的错。
   进度日志从每 2 秒一行降到 30 秒一行并带上已等时长(原来 50 分钟
   刷了 1600 行同样的话)。
This commit is contained in:
FromTX2SJ
2026-08-11 19:55:52 +08:00
parent 008e4ff66a
commit 9342a343ab
+74 -5
View File
@@ -5,6 +5,7 @@ import asyncio
import base64 import base64
import inspect import inspect
import os import os
import time
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from urllib.parse import urljoin from urllib.parse import urljoin
@@ -556,6 +557,14 @@ class TencentBaseUploader(BaseVideoUploader):
): ):
raise RuntimeError("视频号 cookie 已失效(被跳转到登录页),请重新扫码登录后再发布") raise RuntimeError("视频号 cookie 已失效(被跳转到登录页),请重新扫码登录后再发布")
# 上传表单在 micro/content/post/create 这个 iframe 里,domcontentloaded 时它还是空的。
# 不等网络静默就去找 input[type=file],会误报「未找到视频号文件上传框」——
# 失败截图上左栏渲染正常、主内容区一片空白,看起来完全不像加载没完成。
try:
await page.wait_for_load_state("networkidle", timeout=30000)
except Exception:
pass # 静默不了就算了,下面还有重试兜底
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():
for fr in page.frames: # 主 frame + 所有 iframe(视频号编辑器可能在 iframe 内) for fr in page.frames: # 主 frame + 所有 iframe(视频号编辑器可能在 iframe 内)
@@ -574,12 +583,25 @@ class TencentBaseUploader(BaseVideoUploader):
if await publish_btn.count(): if await publish_btn.count():
await publish_btn.click() await publish_btn.click()
await asyncio.sleep(3) await asyncio.sleep(3)
for _ in range(20): # 60 秒:实测这个 iframe 挂上 input 要几十秒,20 秒会误报「找不到上传框」
for _ in range(60):
fi = await find_file_input() fi = await find_file_input()
if fi is not None: if fi is not None:
break break
await asyncio.sleep(1) await asyncio.sleep(1)
if fi is None: if fi is None:
# 留现场:这个错误的可能原因太多(没登录 / 落到首页 / iframe 没加载完 /
# 平台改版),只看错误字符串没法区分,截图能一眼看出是哪种。
try:
shot = Path(BASE_DIR) / "debug_tencent_no_file_input.png"
await page.screenshot(path=str(shot), full_page=True)
tencent_logger.info(_msg(
"📸",
f"失败现场已截图 {shot}; url={page.url}; "
f"frames={[fr.url[:80] for fr in page.frames]}",
))
except Exception:
pass
raise RuntimeError("未找到视频号文件上传框") raise RuntimeError("未找到视频号文件上传框")
await fi.set_input_files(file_path) await fi.set_input_files(file_path)
@@ -716,8 +738,36 @@ class TencentBaseUploader(BaseVideoUploader):
# 视频号「声明原创」为可选项:页面无对应入口时跳过并继续发布,而非中止。 # 视频号「声明原创」为可选项:页面无对应入口时跳过并继续发布,而非中止。
tencent_logger.warning(_msg("📭", "本视频未声明原创(页面无入口或为可选项),跳过并继续发布")) tencent_logger.warning(_msg("📭", "本视频未声明原创(页面无入口或为可选项),跳过并继续发布"))
async def wait_for_upload_complete(self, page: Page) -> None: async def wait_for_upload_complete(
self, page: Page, timeout_seconds: int = 3600, max_retries: int = 3
) -> None:
"""等上传完成。
**必须有个头,而且重试必须有上限。** 原来是没有出口的 while True:上传出错就
删掉重传,失败再删再传,永远循环;中间每 2 秒打一行「正在上传视频中...」——
这条日志和真的在传一模一样,从外面完全分不出。
实测(2026-08-11,172MB / 上行 ~0.5Mbps):每次传到 4 分钟左右报错,然后重来,
整整循环了近 2 小时也不会停,进程也不会退。用户看到的只有「正在上传视频中」,
真相是同一段视频被反复上传了 20 多次。
默认 1 小时 / 3 次重试:慢网络上大文件确实会传很久,上限要给够;
但到点、或者重试用完,就带现场截图明确报错,不要静默地转下去。
"""
deadline = time.monotonic() + timeout_seconds
last_report = 0.0
retries = 0
while True: while True:
if time.monotonic() > deadline:
try:
shot = Path(BASE_DIR) / "debug_tencent_upload_timeout.png"
await page.screenshot(path=str(shot), full_page=True)
tencent_logger.error(_msg("📸", f"上传超时现场已截图 {shot}"))
except Exception:
pass
raise RuntimeError(
f"视频号上传超过 {timeout_seconds} 秒仍未完成(「发表」按钮一直不可用)"
)
try: try:
publish_button = page.get_by_role("button", name="发表") publish_button = page.get_by_role("button", name="发表")
button_class = await publish_button.get_attribute("class") button_class = await publish_button.get_attribute("class")
@@ -725,16 +775,35 @@ class TencentBaseUploader(BaseVideoUploader):
tencent_logger.info(_msg("🥳", "视频上传完毕")) tencent_logger.info(_msg("🥳", "视频上传完毕"))
break break
tencent_logger.info(_msg("🏃", "正在上传视频中...")) # 每 2 秒刷一行同样的话没有信息量,只是把日志冲爆(实测 50 分钟刷了 1600 行)。
# 30 秒一行,并且带上已等多久——「还要多久」是这里唯一有用的信息。
now = time.monotonic()
if now - last_report >= 30:
waited = int(timeout_seconds - (deadline - now))
tencent_logger.info(_msg("🏃", f"正在上传视频中...(已等 {waited} 秒)"))
last_report = now
await asyncio.sleep(2) await asyncio.sleep(2)
upload_failed = await page.locator("div.status-msg.error").count() upload_failed = await page.locator("div.status-msg.error").count()
delete_button = await page.locator('div.media-status-content div.tag-inner:has-text("删除")').count() delete_button = await page.locator('div.media-status-content div.tag-inner:has-text("删除")').count()
if upload_failed and delete_button: if upload_failed and delete_button:
tencent_logger.error(_msg("😵", "发现上传出错了,准备重试")) retries += 1
if retries > max_retries:
try:
shot = Path(BASE_DIR) / "debug_tencent_upload_failed.png"
await page.screenshot(path=str(shot), full_page=True)
tencent_logger.error(_msg("📸", f"上传反复失败,现场已截图 {shot}"))
except Exception:
pass
raise RuntimeError(
f"视频号上传连续失败 {max_retries} 次,已停止重试"
"(常见原因:文件过大、上行带宽太慢导致平台侧超时,或走了代理/VPN)"
)
tencent_logger.error(_msg("😵", f"发现上传出错了,准备重试(第 {retries}/{max_retries} 次)"))
await self.handle_upload_error(page) await self.handle_upload_error(page)
except RuntimeError:
raise
except Exception: except Exception:
tencent_logger.info(_msg("🏃", "正在上传视频中..."))
await asyncio.sleep(2) await asyncio.sleep(2)
async def submit_publish(self, page: Page) -> None: async def submit_publish(self, page: Page) -> None: