|
@@ -24,6 +24,12 @@ import time
|
|
|
# 允许通过环境变量手动指定“上传视频入口”的选择器,便于在页面结构频繁变更时快速调整
|
|
# 允许通过环境变量手动指定“上传视频入口”的选择器,便于在页面结构频繁变更时快速调整
|
|
|
WEIXIN_UPLOAD_SELECTOR = os.environ.get("WEIXIN_UPLOAD_SELECTOR", "").strip()
|
|
WEIXIN_UPLOAD_SELECTOR = os.environ.get("WEIXIN_UPLOAD_SELECTOR", "").strip()
|
|
|
|
|
|
|
|
|
|
+# 代理下视频上传持续失败时,可设 WEIXIN_UPLOAD_BYPASS_PROXY=1
|
|
|
|
|
+# 仅对上传 CDN 直连,其余页面仍走代理(解决大文件经代理易「网络出错」)
|
|
|
|
|
+WEIXIN_UPLOAD_BYPASS_PROXY = os.environ.get(
|
|
|
|
|
+ "WEIXIN_UPLOAD_BYPASS_PROXY", "0"
|
|
|
|
|
+).strip() in ("1", "true", "yes")
|
|
|
|
|
+
|
|
|
|
|
|
|
|
def format_short_title(origin_title: str) -> str:
|
|
def format_short_title(origin_title: str) -> str:
|
|
|
"""
|
|
"""
|
|
@@ -345,42 +351,78 @@ class WeixinPublisher(BasePublisher):
|
|
|
return (head + "\n\n<!-- TAIL -->\n\n" + tail)[:20000]
|
|
return (head + "\n\n<!-- TAIL -->\n\n" + tail)[:20000]
|
|
|
|
|
|
|
|
async def init_browser(self, storage_state: str = None):
|
|
async def init_browser(self, storage_state: str = None):
|
|
|
- """初始化浏览器 - 参考 matrix 使用 channel=chrome 避免 H264 编码错误"""
|
|
|
|
|
|
|
+ """
|
|
|
|
|
+ 初始化浏览器 - 参考 matrix 使用 channel=chrome 避免 H264 编码错误
|
|
|
|
|
+
|
|
|
|
|
+ 重要:如果配置了代理,全程都会使用代理(包括页面访问和视频上传)
|
|
|
|
|
+ """
|
|
|
from playwright.async_api import async_playwright
|
|
from playwright.async_api import async_playwright
|
|
|
|
|
|
|
|
playwright = await async_playwright().start()
|
|
playwright = await async_playwright().start()
|
|
|
|
|
+
|
|
|
proxy = (
|
|
proxy = (
|
|
|
self.proxy_config
|
|
self.proxy_config
|
|
|
if isinstance(getattr(self, "proxy_config", None), dict)
|
|
if isinstance(getattr(self, "proxy_config", None), dict)
|
|
|
else None
|
|
else None
|
|
|
)
|
|
)
|
|
|
|
|
+
|
|
|
if proxy and proxy.get("server"):
|
|
if proxy and proxy.get("server"):
|
|
|
- print(f"[{self.platform_name}] 使用代理: {proxy.get('server')}", flush=True)
|
|
|
|
|
|
|
+ # 启用上传 bypass 时:仅对上传 CDN 直连,其余仍走代理
|
|
|
|
|
+ if WEIXIN_UPLOAD_BYPASS_PROXY:
|
|
|
|
|
+ bypass = "findeross.weixin.qq.com,upload.weixin.qq.com,*.cos.qq.com,*.myqcloud.com,*.tencentcloudapi.com"
|
|
|
|
|
+ proxy = dict(proxy)
|
|
|
|
|
+ proxy["bypass"] = bypass
|
|
|
|
|
+ print(
|
|
|
|
|
+ f"[{self.platform_name}] 使用代理(上传 CDN 直连): {proxy.get('server')}",
|
|
|
|
|
+ flush=True,
|
|
|
|
|
+ )
|
|
|
|
|
+ print(
|
|
|
|
|
+ f"[{self.platform_name}] 💡 页面走代理,视频上传 CDN 直连,避免大文件经代理失败",
|
|
|
|
|
+ flush=True,
|
|
|
|
|
+ )
|
|
|
|
|
+ else:
|
|
|
|
|
+ print(
|
|
|
|
|
+ f"[{self.platform_name}] 使用代理(全程): {proxy.get('server')}",
|
|
|
|
|
+ flush=True,
|
|
|
|
|
+ )
|
|
|
|
|
+ print(
|
|
|
|
|
+ f"[{self.platform_name}] 💡 页面访问和视频上传都将通过代理",
|
|
|
|
|
+ flush=True,
|
|
|
|
|
+ )
|
|
|
|
|
|
|
|
# 参考 matrix: 使用系统内的 Chrome 浏览器,避免 H264 编码错误
|
|
# 参考 matrix: 使用系统内的 Chrome 浏览器,避免 H264 编码错误
|
|
|
- # 非 headless 时添加 slow_mo 便于观察点击操作
|
|
|
|
|
launch_opts = {"headless": self.headless}
|
|
launch_opts = {"headless": self.headless}
|
|
|
if not self.headless:
|
|
if not self.headless:
|
|
|
- launch_opts["slow_mo"] = 400 # 每个操作间隔 400ms,便于观看
|
|
|
|
|
|
|
+ launch_opts["slow_mo"] = 400
|
|
|
print(
|
|
print(
|
|
|
f"[{self.platform_name}] 有头模式 + slow_mo=400ms,浏览器将可见",
|
|
f"[{self.platform_name}] 有头模式 + slow_mo=400ms,浏览器将可见",
|
|
|
flush=True,
|
|
flush=True,
|
|
|
)
|
|
)
|
|
|
|
|
+
|
|
|
try:
|
|
try:
|
|
|
launch_opts["channel"] = "chrome"
|
|
launch_opts["channel"] = "chrome"
|
|
|
if proxy and proxy.get("server"):
|
|
if proxy and proxy.get("server"):
|
|
|
launch_opts["proxy"] = proxy
|
|
launch_opts["proxy"] = proxy
|
|
|
|
|
+ # 代理下大文件上传优化:禁用 QUIC,部分代理对 QUIC 支持不佳易导致连接中断
|
|
|
|
|
+ launch_opts.setdefault("args", []).append("--disable-quic")
|
|
|
self.browser = await playwright.chromium.launch(**launch_opts)
|
|
self.browser = await playwright.chromium.launch(**launch_opts)
|
|
|
- print(f"[{self.platform_name}] 使用系统 Chrome 浏览器", flush=True)
|
|
|
|
|
|
|
+ mode = "代理模式" if proxy else "直连模式"
|
|
|
|
|
+ print(
|
|
|
|
|
+ f"[{self.platform_name}] 使用系统 Chrome 浏览器({mode})", flush=True
|
|
|
|
|
+ )
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
print(
|
|
print(
|
|
|
f"[{self.platform_name}] Chrome 不可用,使用 Chromium: {e}", flush=True
|
|
f"[{self.platform_name}] Chrome 不可用,使用 Chromium: {e}", flush=True
|
|
|
)
|
|
)
|
|
|
if "channel" in launch_opts:
|
|
if "channel" in launch_opts:
|
|
|
del launch_opts["channel"]
|
|
del launch_opts["channel"]
|
|
|
|
|
+ if proxy and proxy.get("server"):
|
|
|
|
|
+ launch_opts["proxy"] = proxy
|
|
|
|
|
+ if "--disable-quic" not in (launch_opts.get("args") or []):
|
|
|
|
|
+ launch_opts.setdefault("args", []).append("--disable-quic")
|
|
|
self.browser = await playwright.chromium.launch(**launch_opts)
|
|
self.browser = await playwright.chromium.launch(**launch_opts)
|
|
|
|
|
|
|
|
- # 设置 HTTP Headers 防止重定向
|
|
|
|
|
|
|
+ # 设置 HTTP Headers
|
|
|
headers = {
|
|
headers = {
|
|
|
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
|
"Referer": "https://channels.weixin.qq.com/platform/post/list",
|
|
"Referer": "https://channels.weixin.qq.com/platform/post/list",
|
|
@@ -394,6 +436,11 @@ class WeixinPublisher(BasePublisher):
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
self.page = await self.context.new_page()
|
|
self.page = await self.context.new_page()
|
|
|
|
|
+
|
|
|
|
|
+ # 注入反检测脚本
|
|
|
|
|
+ if hasattr(self, "inject_stealth_if_available"):
|
|
|
|
|
+ await self.inject_stealth_if_available()
|
|
|
|
|
+
|
|
|
return self.page
|
|
return self.page
|
|
|
|
|
|
|
|
async def set_schedule_time(self, publish_date: datetime):
|
|
async def set_schedule_time(self, publish_date: datetime):
|
|
@@ -442,18 +489,41 @@ class WeixinPublisher(BasePublisher):
|
|
|
await self.page.locator("div.input-editor").click()
|
|
await self.page.locator("div.input-editor").click()
|
|
|
|
|
|
|
|
async def handle_upload_error(self, video_path: str):
|
|
async def handle_upload_error(self, video_path: str):
|
|
|
- """处理上传错误"""
|
|
|
|
|
|
|
+ """处理上传错误(含代理下「网络出错」重试优化)"""
|
|
|
if not self.page:
|
|
if not self.page:
|
|
|
return
|
|
return
|
|
|
|
|
|
|
|
|
|
+ using_proxy = isinstance(
|
|
|
|
|
+ getattr(self, "proxy_config", None), dict
|
|
|
|
|
+ ) and self.proxy_config.get("server")
|
|
|
|
|
+
|
|
|
|
|
+ # 代理模式下先等待,给代理/网络恢复时间,避免连续重试加剧失败
|
|
|
|
|
+ if using_proxy:
|
|
|
|
|
+ wait_sec = 25
|
|
|
|
|
+ print(
|
|
|
|
|
+ f"[{self.platform_name}] 代理模式:检测到上传错误,等待 {wait_sec} 秒后重试...",
|
|
|
|
|
+ flush=True,
|
|
|
|
|
+ )
|
|
|
|
|
+ await asyncio.sleep(wait_sec)
|
|
|
|
|
+
|
|
|
print(f"[{self.platform_name}] 视频出错了,重新上传中...")
|
|
print(f"[{self.platform_name}] 视频出错了,重新上传中...")
|
|
|
|
|
|
|
|
# 出错时先截一张当前页面的图,方便排查(代理问题、视频格式问题等)
|
|
# 出错时先截一张当前页面的图,方便排查(代理问题、视频格式问题等)
|
|
|
try:
|
|
try:
|
|
|
timestamp = int(time.time() * 1000)
|
|
timestamp = int(time.time() * 1000)
|
|
|
- screenshot_path = f"weixin_upload_error_{timestamp}.png"
|
|
|
|
|
|
|
+ screenshot_dir = os.path.join(
|
|
|
|
|
+ os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
|
|
|
|
+ "screenshots",
|
|
|
|
|
+ )
|
|
|
|
|
+ os.makedirs(screenshot_dir, exist_ok=True)
|
|
|
|
|
+ screenshot_path = os.path.join(
|
|
|
|
|
+ screenshot_dir, f"weixin_upload_error_{timestamp}.png"
|
|
|
|
|
+ )
|
|
|
await self.page.screenshot(path=screenshot_path, full_page=True)
|
|
await self.page.screenshot(path=screenshot_path, full_page=True)
|
|
|
- print(f"[{self.platform_name}] 上传错误截图已保存: {screenshot_path}", flush=True)
|
|
|
|
|
|
|
+ print(
|
|
|
|
|
+ f"[{self.platform_name}] 上传错误截图已保存: {screenshot_path}",
|
|
|
|
|
+ flush=True,
|
|
|
|
|
+ )
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
print(f"[{self.platform_name}] 保存上传错误截图失败: {e}", flush=True)
|
|
print(f"[{self.platform_name}] 保存上传错误截图失败: {e}", flush=True)
|
|
|
|
|
|
|
@@ -470,6 +540,7 @@ class WeixinPublisher(BasePublisher):
|
|
|
if not self.page:
|
|
if not self.page:
|
|
|
return
|
|
return
|
|
|
|
|
|
|
|
|
|
+ print(f"[{self.platform_name}] 开始添加标题: {params.title}", flush=True)
|
|
|
await self.page.locator("div.input-editor").click()
|
|
await self.page.locator("div.input-editor").click()
|
|
|
await self.page.keyboard.type(params.title)
|
|
await self.page.keyboard.type(params.title)
|
|
|
|
|
|
|
@@ -479,7 +550,124 @@ class WeixinPublisher(BasePublisher):
|
|
|
await self.page.keyboard.type("#" + tag)
|
|
await self.page.keyboard.type("#" + tag)
|
|
|
await self.page.keyboard.press("Space")
|
|
await self.page.keyboard.press("Space")
|
|
|
|
|
|
|
|
- print(f"[{self.platform_name}] 成功添加标题和 {len(params.tags)} 个话题")
|
|
|
|
|
|
|
+ print(
|
|
|
|
|
+ f"[{self.platform_name}] ✓ 成功添加标题和 {len(params.tags)} 个话题",
|
|
|
|
|
+ flush=True,
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ # 🔧 设置位置(使用代理地区或默认位置)
|
|
|
|
|
+ print(f"[{self.platform_name}] 准备设置位置: {params.location}", flush=True)
|
|
|
|
|
+ if params.location:
|
|
|
|
|
+ await self.set_location(params.location)
|
|
|
|
|
+ else:
|
|
|
|
|
+ print(f"[{self.platform_name}] ⚠️ 未设置位置,跳过", flush=True)
|
|
|
|
|
+
|
|
|
|
|
+ async def set_location(self, location: str):
|
|
|
|
|
+ """设置发布位置"""
|
|
|
|
|
+ if not self.page or not location:
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ print(f"[{self.platform_name}] 正在设置位置: {location}", flush=True)
|
|
|
|
|
+
|
|
|
|
|
+ # 等待页面稳定
|
|
|
|
|
+ await asyncio.sleep(1)
|
|
|
|
|
+
|
|
|
|
|
+ # 尝试多种方式找到位置设置元素
|
|
|
|
|
+ location_selectors = [
|
|
|
|
|
+ # 位置输入框
|
|
|
|
|
+ 'input[placeholder*="位置"]',
|
|
|
|
|
+ 'input[placeholder*="所在"]',
|
|
|
|
|
+ 'input[placeholder*="地点"]',
|
|
|
|
|
+ # 位置按钮
|
|
|
|
|
+ 'div:has-text("所在位置")',
|
|
|
|
|
+ 'div:has-text("添加位置")',
|
|
|
|
|
+ 'span:has-text("位置")',
|
|
|
|
|
+ ]
|
|
|
|
|
+
|
|
|
|
|
+ location_element = None
|
|
|
|
|
+ for selector in location_selectors:
|
|
|
|
|
+ try:
|
|
|
|
|
+ element = self.page.locator(selector).first
|
|
|
|
|
+ if await element.count() > 0 and await element.is_visible():
|
|
|
|
|
+ location_element = element
|
|
|
|
|
+ print(
|
|
|
|
|
+ f"[{self.platform_name}] 找到位置元素: {selector}",
|
|
|
|
|
+ flush=True,
|
|
|
|
|
+ )
|
|
|
|
|
+ break
|
|
|
|
|
+ except:
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ if not location_element:
|
|
|
|
|
+ print(f"[{self.platform_name}] 未找到位置设置元素,跳过", flush=True)
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ # 点击位置元素
|
|
|
|
|
+ await location_element.click()
|
|
|
|
|
+ await asyncio.sleep(1)
|
|
|
|
|
+
|
|
|
|
|
+ # 查找位置输入框
|
|
|
|
|
+ input_selectors = [
|
|
|
|
|
+ 'input[placeholder*="搜索"]',
|
|
|
|
|
+ 'input[placeholder*="输入"]',
|
|
|
|
|
+ 'input[type="text"]',
|
|
|
|
|
+ ]
|
|
|
|
|
+
|
|
|
|
|
+ location_input = None
|
|
|
|
|
+ for selector in input_selectors:
|
|
|
|
|
+ try:
|
|
|
|
|
+ element = self.page.locator(selector).first
|
|
|
|
|
+ if await element.count() > 0 and await element.is_visible():
|
|
|
|
|
+ location_input = element
|
|
|
|
|
+ break
|
|
|
|
|
+ except:
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ if location_input:
|
|
|
|
|
+ # 输入位置
|
|
|
|
|
+ await location_input.fill(location)
|
|
|
|
|
+ await asyncio.sleep(1)
|
|
|
|
|
+
|
|
|
|
|
+ # 查找匹配的位置选项并点击
|
|
|
|
|
+ try:
|
|
|
|
|
+ # 等待位置建议出现
|
|
|
|
|
+ await asyncio.sleep(1)
|
|
|
|
|
+
|
|
|
|
|
+ # 查找包含位置文本的选项
|
|
|
|
|
+ option = self.page.locator(f'text="{location}"').first
|
|
|
|
|
+ if await option.count() > 0:
|
|
|
|
|
+ await option.click()
|
|
|
|
|
+ print(
|
|
|
|
|
+ f"[{self.platform_name}] ✓ 位置设置成功: {location}",
|
|
|
|
|
+ flush=True,
|
|
|
|
|
+ )
|
|
|
|
|
+ else:
|
|
|
|
|
+ # 如果没有精确匹配,选择第一个建议
|
|
|
|
|
+ first_option = self.page.locator(
|
|
|
|
|
+ 'div[class*="location"] li, div[class*="suggest"] div'
|
|
|
|
|
+ ).first
|
|
|
|
|
+ if await first_option.count() > 0:
|
|
|
|
|
+ await first_option.click()
|
|
|
|
|
+ print(
|
|
|
|
|
+ f"[{self.platform_name}] ✓ 位置已设置(自动选择)",
|
|
|
|
|
+ flush=True,
|
|
|
|
|
+ )
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ print(f"[{self.platform_name}] ⚠️ 选择位置失败: {e}", flush=True)
|
|
|
|
|
+
|
|
|
|
|
+ # 按 Escape 关闭位置选择器
|
|
|
|
|
+ await self.page.keyboard.press("Escape")
|
|
|
|
|
+ else:
|
|
|
|
|
+ print(f"[{self.platform_name}] 未找到位置输入框", flush=True)
|
|
|
|
|
+ await self.page.keyboard.press("Escape")
|
|
|
|
|
+
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ print(f"[{self.platform_name}] 设置位置失败: {e}", flush=True)
|
|
|
|
|
+ try:
|
|
|
|
|
+ await self.page.keyboard.press("Escape")
|
|
|
|
|
+ except:
|
|
|
|
|
+ pass
|
|
|
|
|
|
|
|
async def add_short_title(self):
|
|
async def add_short_title(self):
|
|
|
"""添加短标题"""
|
|
"""添加短标题"""
|
|
@@ -614,6 +802,14 @@ class WeixinPublisher(BasePublisher):
|
|
|
|
|
|
|
|
self.report_progress(10, "正在打开上传页面...")
|
|
self.report_progress(10, "正在打开上传页面...")
|
|
|
|
|
|
|
|
|
|
+ # 代理模式下拉长超时,避免大文件上传经代理时超时
|
|
|
|
|
+ using_proxy = isinstance(
|
|
|
|
|
+ getattr(self, "proxy_config", None), dict
|
|
|
|
|
+ ) and self.proxy_config.get("server")
|
|
|
|
|
+ if using_proxy:
|
|
|
|
|
+ self.page.set_default_timeout(300000) # 5 分钟
|
|
|
|
|
+ print(f"[{self.platform_name}] 代理模式:已设置 5 分钟操作超时", flush=True)
|
|
|
|
|
+
|
|
|
# 访问上传页面 - 使用 domcontentloaded 替代 networkidle,避免代理慢速导致超时
|
|
# 访问上传页面 - 使用 domcontentloaded 替代 networkidle,避免代理慢速导致超时
|
|
|
await self.page.goto(
|
|
await self.page.goto(
|
|
|
self.publish_url, wait_until="domcontentloaded", timeout=90000
|
|
self.publish_url, wait_until="domcontentloaded", timeout=90000
|
|
@@ -624,6 +820,12 @@ class WeixinPublisher(BasePublisher):
|
|
|
except Exception:
|
|
except Exception:
|
|
|
pass
|
|
pass
|
|
|
await asyncio.sleep(3)
|
|
await asyncio.sleep(3)
|
|
|
|
|
+ # 代理模式下多等几秒,让代理连接稳定后再上传
|
|
|
|
|
+ if using_proxy:
|
|
|
|
|
+ print(
|
|
|
|
|
+ f"[{self.platform_name}] 代理模式:等待 8 秒后开始上传...", flush=True
|
|
|
|
|
+ )
|
|
|
|
|
+ await asyncio.sleep(8)
|
|
|
|
|
|
|
|
# 检查是否跳转到登录页
|
|
# 检查是否跳转到登录页
|
|
|
current_url = self.page.url
|
|
current_url = self.page.url
|
|
@@ -1023,36 +1225,157 @@ class WeixinPublisher(BasePublisher):
|
|
|
|
|
|
|
|
self.report_progress(30, "等待视频上传完成...")
|
|
self.report_progress(30, "等待视频上传完成...")
|
|
|
|
|
|
|
|
- # 等待上传完成(最多约 6 分钟),期间如多次出错会自动尝试重新上传
|
|
|
|
|
|
|
+ # 代理模式下增加重试次数和总时长,应对「网络出错」等不稳定情况
|
|
|
|
|
+ using_proxy = isinstance(
|
|
|
|
|
+ getattr(self, "proxy_config", None), dict
|
|
|
|
|
+ ) and self.proxy_config.get("server")
|
|
|
|
|
+ max_upload_error_retries = 20 if using_proxy else 5
|
|
|
|
|
+ loop_count = 300 if using_proxy else 200 # 代理模式约 15 分钟
|
|
|
|
|
+ if using_proxy:
|
|
|
|
|
+ print(
|
|
|
|
|
+ f"[{self.platform_name}] 代理模式:上传重试上限 {max_upload_error_retries} 次,总等待约 15 分钟",
|
|
|
|
|
+ flush=True,
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
upload_completed = False
|
|
upload_completed = False
|
|
|
- for _ in range(120):
|
|
|
|
|
|
|
+ upload_error_retry_count = 0
|
|
|
|
|
+ for i in range(loop_count):
|
|
|
try:
|
|
try:
|
|
|
- button_info = await self.page.get_by_role(
|
|
|
|
|
- "button", name="发表"
|
|
|
|
|
- ).get_attribute("class")
|
|
|
|
|
- if "weui-desktop-btn_disabled" not in button_info:
|
|
|
|
|
- print(f"[{self.platform_name}] 视频上传完毕")
|
|
|
|
|
-
|
|
|
|
|
- # 上传封面
|
|
|
|
|
- self.report_progress(50, "正在上传封面...")
|
|
|
|
|
- await self.upload_cover(params.cover_path)
|
|
|
|
|
- upload_completed = True
|
|
|
|
|
- break
|
|
|
|
|
- else:
|
|
|
|
|
- # 检查上传错误
|
|
|
|
|
- if await self.page.locator("div.status-msg.error").count():
|
|
|
|
|
- if await self.page.locator(
|
|
|
|
|
- 'div.media-status-content div.tag-inner:has-text("删除")'
|
|
|
|
|
- ).count():
|
|
|
|
|
|
|
+ # 每 30 秒打印一次进度,避免“卡住”的错觉
|
|
|
|
|
+ if i > 0 and i % 10 == 0:
|
|
|
|
|
+ print(
|
|
|
|
|
+ f"[{self.platform_name}] 仍在等待上传完成... ({i * 3}s)",
|
|
|
|
|
+ flush=True,
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ # 尝试多种选择器定位“发表”按钮(页面结构可能变化)
|
|
|
|
|
+ publish_btn = None
|
|
|
|
|
+ for sel in [
|
|
|
|
|
+ 'div.form-btns button:has-text("发表")',
|
|
|
|
|
+ 'button:has-text("发表")',
|
|
|
|
|
+ 'button:has-text("立即发表")',
|
|
|
|
|
+ '[role="button"]:has-text("发表")',
|
|
|
|
|
+ ]:
|
|
|
|
|
+ try:
|
|
|
|
|
+ el = self.page.locator(sel).first
|
|
|
|
|
+ if await el.count() > 0 and await el.is_visible():
|
|
|
|
|
+ publish_btn = el
|
|
|
|
|
+ break
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ if publish_btn:
|
|
|
|
|
+ btn_class = await publish_btn.get_attribute("class") or ""
|
|
|
|
|
+ if (
|
|
|
|
|
+ "weui-desktop-btn_disabled" not in btn_class
|
|
|
|
|
+ and "disabled" not in btn_class.lower()
|
|
|
|
|
+ ):
|
|
|
|
|
+ print(f"[{self.platform_name}] 视频上传完毕")
|
|
|
|
|
+
|
|
|
|
|
+ # 上传封面
|
|
|
|
|
+ self.report_progress(50, "正在上传封面...")
|
|
|
|
|
+ await self.upload_cover(params.cover_path)
|
|
|
|
|
+ upload_completed = True
|
|
|
|
|
+ break
|
|
|
|
|
+
|
|
|
|
|
+ # 检查上传错误(div.status-msg.error,含「网络出错了,请稍候上传」)
|
|
|
|
|
+ has_error = await self.page.locator("div.status-msg.error").count() > 0
|
|
|
|
|
+ has_delete_btn = (
|
|
|
|
|
+ await self.page.locator(
|
|
|
|
|
+ 'div.media-status-content div.tag-inner:has-text("删除")'
|
|
|
|
|
+ ).count()
|
|
|
|
|
+ > 0
|
|
|
|
|
+ )
|
|
|
|
|
+ if has_error and has_delete_btn:
|
|
|
|
|
+ upload_error_retry_count += 1
|
|
|
|
|
+ print(
|
|
|
|
|
+ f"[{self.platform_name}] 检测到上传错误,第 {upload_error_retry_count} 次重试",
|
|
|
|
|
+ flush=True,
|
|
|
|
|
+ )
|
|
|
|
|
+ if upload_error_retry_count >= max_upload_error_retries:
|
|
|
|
|
+ print(
|
|
|
|
|
+ f"[{self.platform_name}] 上传错误重试已达 {max_upload_error_retries} 次,放弃",
|
|
|
|
|
+ flush=True,
|
|
|
|
|
+ )
|
|
|
|
|
+ break
|
|
|
|
|
+ # 代理模式下,第 6 次失败时尝试整页刷新以重建代理连接
|
|
|
|
|
+ if using_proxy and upload_error_retry_count == 6:
|
|
|
|
|
+ print(
|
|
|
|
|
+ f"[{self.platform_name}] 代理模式:尝试整页刷新以重建连接...",
|
|
|
|
|
+ flush=True,
|
|
|
|
|
+ )
|
|
|
|
|
+ try:
|
|
|
|
|
+ await self.page.reload(
|
|
|
|
|
+ wait_until="domcontentloaded", timeout=60000
|
|
|
|
|
+ )
|
|
|
|
|
+ await asyncio.sleep(8)
|
|
|
|
|
+ await self.page.wait_for_selector(
|
|
|
|
|
+ "div.upload-content, input[type='file']", timeout=20000
|
|
|
|
|
+ )
|
|
|
|
|
+ upload_el = self.page.locator("div.upload-content").first
|
|
|
|
|
+ if (
|
|
|
|
|
+ await upload_el.count() > 0
|
|
|
|
|
+ and await upload_el.is_visible()
|
|
|
|
|
+ ):
|
|
|
|
|
+ async with self.page.expect_file_chooser(
|
|
|
|
|
+ timeout=10000
|
|
|
|
|
+ ) as fc:
|
|
|
|
|
+ await upload_el.click()
|
|
|
|
|
+ chooser = await fc.value
|
|
|
|
|
+ await chooser.set_files(params.video_path)
|
|
|
|
|
+ print(
|
|
|
|
|
+ f"[{self.platform_name}] 刷新后重新上传成功",
|
|
|
|
|
+ flush=True,
|
|
|
|
|
+ )
|
|
|
|
|
+ else:
|
|
|
|
|
+ file_input = self.page.locator(
|
|
|
|
|
+ 'input[type="file"]'
|
|
|
|
|
+ ).first
|
|
|
|
|
+ if await file_input.count() > 0:
|
|
|
|
|
+ await file_input.set_input_files(params.video_path)
|
|
|
|
|
+ await asyncio.sleep(2)
|
|
|
|
|
+ await self.add_title_tags(params)
|
|
|
|
|
+ upload_error_retry_count = 0
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ print(
|
|
|
|
|
+ f"[{self.platform_name}] 整页刷新重传失败: {e}",
|
|
|
|
|
+ flush=True,
|
|
|
|
|
+ )
|
|
|
await self.handle_upload_error(params.video_path)
|
|
await self.handle_upload_error(params.video_path)
|
|
|
|
|
+ else:
|
|
|
|
|
+ await self.handle_upload_error(params.video_path)
|
|
|
|
|
+ else:
|
|
|
|
|
+ upload_error_retry_count = 0 # 无错误时重置计数
|
|
|
|
|
|
|
|
- await asyncio.sleep(3)
|
|
|
|
|
- except:
|
|
|
|
|
|
|
+ await asyncio.sleep(3)
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ print(f"[{self.platform_name}] 等待上传时异常: {e}", flush=True)
|
|
|
await asyncio.sleep(3)
|
|
await asyncio.sleep(3)
|
|
|
|
|
|
|
|
# 如果一直没有等到“发表”按钮可用,认为上传失败,直接返回失败结果并附带截图
|
|
# 如果一直没有等到“发表”按钮可用,认为上传失败,直接返回失败结果并附带截图
|
|
|
if not upload_completed:
|
|
if not upload_completed:
|
|
|
- screenshot_base64 = await self.capture_screenshot()
|
|
|
|
|
|
|
+ try:
|
|
|
|
|
+ screenshot_base64 = await self.capture_screenshot()
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ print(f"[{self.platform_name}] 截图失败: {e}", flush=True)
|
|
|
|
|
+ screenshot_base64 = ""
|
|
|
|
|
+ try:
|
|
|
|
|
+ ts = int(time.time() * 1000)
|
|
|
|
|
+ screenshot_dir = os.path.join(
|
|
|
|
|
+ os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
|
|
|
|
+ "screenshots",
|
|
|
|
|
+ )
|
|
|
|
|
+ os.makedirs(screenshot_dir, exist_ok=True)
|
|
|
|
|
+ err_path = os.path.join(
|
|
|
|
|
+ screenshot_dir, f"weixin_upload_timeout_{ts}.png"
|
|
|
|
|
+ )
|
|
|
|
|
+ await self.page.screenshot(path=err_path, full_page=True)
|
|
|
|
|
+ print(
|
|
|
|
|
+ f"[{self.platform_name}] 超时/失败截图已保存: {err_path}",
|
|
|
|
|
+ flush=True,
|
|
|
|
|
+ )
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ print(f"[{self.platform_name}] 保存失败截图到文件失败: {e}", flush=True)
|
|
|
page_url = await self.get_page_url()
|
|
page_url = await self.get_page_url()
|
|
|
return PublishResult(
|
|
return PublishResult(
|
|
|
success=False,
|
|
success=False,
|
|
@@ -2567,8 +2890,14 @@ class WeixinPublisher(BasePublisher):
|
|
|
print(f"[{self.platform_name}] 使用备用选择器加载成功")
|
|
print(f"[{self.platform_name}] 使用备用选择器加载成功")
|
|
|
except:
|
|
except:
|
|
|
# 截图调试
|
|
# 截图调试
|
|
|
- screenshot_path = (
|
|
|
|
|
- f"weixin_private_msg_{int(asyncio.get_event_loop().time())}.png"
|
|
|
|
|
|
|
+ screenshot_dir = os.path.join(
|
|
|
|
|
+ os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
|
|
|
|
+ "screenshots",
|
|
|
|
|
+ )
|
|
|
|
|
+ os.makedirs(screenshot_dir, exist_ok=True)
|
|
|
|
|
+ screenshot_path = os.path.join(
|
|
|
|
|
+ screenshot_dir,
|
|
|
|
|
+ f"weixin_private_msg_{int(asyncio.get_event_loop().time())}.png",
|
|
|
)
|
|
)
|
|
|
await self.page.screenshot(path=screenshot_path)
|
|
await self.page.screenshot(path=screenshot_path)
|
|
|
print(
|
|
print(
|