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 inspect
import os
import time
from datetime import datetime
from pathlib import Path
from urllib.parse import urljoin
@@ -556,6 +557,14 @@ class TencentBaseUploader(BaseVideoUploader):
):
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 find_file_input():
for fr in page.frames: # 主 frame + 所有 iframe(视频号编辑器可能在 iframe 内)
@@ -574,12 +583,25 @@ class TencentBaseUploader(BaseVideoUploader):
if await publish_btn.count():
await publish_btn.click()
await asyncio.sleep(3)
for _ in range(20):
# 60 秒:实测这个 iframe 挂上 input 要几十秒,20 秒会误报「找不到上传框」
for _ in range(60):
fi = await find_file_input()
if fi is not None:
break
await asyncio.sleep(1)
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("未找到视频号文件上传框")
await fi.set_input_files(file_path)
@@ -716,8 +738,36 @@ class TencentBaseUploader(BaseVideoUploader):
# 视频号「声明原创」为可选项:页面无对应入口时跳过并继续发布,而非中止。
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:
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:
publish_button = page.get_by_role("button", name="发表")
button_class = await publish_button.get_attribute("class")
@@ -725,16 +775,35 @@ class TencentBaseUploader(BaseVideoUploader):
tencent_logger.info(_msg("🥳", "视频上传完毕"))
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)
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()
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)
except RuntimeError:
raise
except Exception:
tencent_logger.info(_msg("🏃", "正在上传视频中..."))
await asyncio.sleep(2)
async def submit_publish(self, page: Page) -> None: