返回 Social Auto Upload
main.py
1 # -*- coding: utf-8 -*-
2 """百家号(百度百家号)视频上传 + 扫码登录。
3
4 功能:
5 - baijiahao_cookie_gen: headless 扫码登录(百度 passport 二维码)
6 - cookie_auth: 验证 cookie 是否有效
7 - baijiahao_setup: 统一入口(检查/触发登录)
8 - BaiJiaHaoVideo: 视频上传类
9 """
10 from __future__ import annotations
11
12 import asyncio
13 import inspect
14 import json as _json
15 import os
16 import time
17 from pathlib import Path
18
19 from playwright.async_api import Page, Playwright, TimeoutError as PWTimeoutError, async_playwright
20
21 from conf import BASE_DIR, LOCAL_CHROME_HEADLESS, LOCAL_CHROME_PATH
22 from uploader.base_video import BaseVideoUploader
23 from utils.log import baijiahao_logger
24 from utils.login_qrcode import build_login_qrcode_path, decode_qrcode_from_path, print_terminal_qrcode, remove_qrcode_file
25
26
27 BAIJIAHAO_LOGIN_URL = "https://baijiahao.baidu.com/builder/theme/bjh/login"
28 BAIJIAHAO_HOME_URL = "https://baijiahao.baidu.com/builder/rc/home"
29 BAIJIAHAO_PUBLISH_URL = "https://baijiahao.baidu.com/builder/rc/edit?type=videoV2"
30 # 发布成功后跳转到的 URL 前缀
31 BAIJIAHAO_SUCCESS_URL_PREFIX = "https://baijiahao.baidu.com/builder/rc/clue"
32
33 # 百度 passport 二维码图片选择器
34 QR_SELECTOR = 'img[src^="https://passport.baidu.com/v2/api/qrcode"]'
35
36
37 def _msg(emoji: str, text: str) -> str:
38 return f"{emoji} {text}"
39
40
41 def _build_login_result(success: bool, status: str, message: str, account_file: str, qrcode: dict | None = None, current_url: str = "") -> dict:
42 return {
43 "success": success,
44 "status": status,
45 "message": message,
46 "account_file": str(account_file),
47 "qrcode": qrcode,
48 "current_url": current_url,
49 }
50
51
52 async def _emit_qrcode_callback(qrcode_callback, payload: dict):
53 if not qrcode_callback:
54 return
55 callback_result = qrcode_callback(payload)
56 if inspect.isawaitable(callback_result):
57 await callback_result
58
59
60 def _build_launch_kwargs(headless: bool) -> dict:
61 launch_kwargs = {"headless": headless}
62 if LOCAL_CHROME_PATH:
63 launch_kwargs["executable_path"] = LOCAL_CHROME_PATH
64 return launch_kwargs
65
66
67 def _resolve_account_file(account_file: str | Path) -> str:
68 path = Path(account_file).expanduser()
69 if path.is_absolute():
70 return str(path)
71 if len(path.parts) == 1:
72 return str((Path(BASE_DIR) / "cookies" / "baijiahao_uploader" / path).resolve())
73 return str(path.resolve())
74
75
76 async def _grab_qr(page: Page, account_file: str) -> dict:
77 """截取百度 passport 扫码登录二维码。
78
79 百家号登录页点「登录」后弹出百度统一登录框,其中二维码是 img[src] 指向
80 passport.baidu.com 的图片 URL,可以直接下载或截图。
81 """
82 qr = page.locator(QR_SELECTOR).first
83 await qr.wait_for(state="attached", timeout=60000)
84
85 qrcode_path = build_login_qrcode_path(account_file)
86 qrcode_path.parent.mkdir(parents=True, exist_ok=True)
87
88 # 优先直接下载高清图片 URL
89 src = await qr.get_attribute("src")
90 if src and src.startswith("https://"):
91 try:
92 resp = await page.context.request.get(src)
93 qrcode_path.write_bytes(await resp.body())
94 except Exception:
95 await qr.screenshot(path=str(qrcode_path))
96 else:
97 await qr.screenshot(path=str(qrcode_path))
98
99 qrcode_content = decode_qrcode_from_path(qrcode_path)
100 baijiahao_logger.info(_msg("🖼️", f"二维码已保存到: {qrcode_path}"))
101 if qrcode_content:
102 print_terminal_qrcode(qrcode_content, qrcode_path, "百度APP/手机百度")
103 else:
104 baijiahao_logger.warning(_msg("😵", f"终端没法完整显示二维码,请打开 {qrcode_path} 扫码"))
105 return {"image_path": str(qrcode_path), "image_data_url": ""}
106
107
108 async def _is_login_completed(page: Page) -> bool:
109 """判断百度登录是否完成:URL 离开 login 页 或 出现 BDUSS cookie。"""
110 if "login" in page.url.lower():
111 # 还在登录页,检查 cookies
112 cookies = await page.context.cookies()
113 if any(c.get("name") in ("BDUSS", "STOKEN") for c in cookies):
114 return True
115 return False
116 # 跳走了说明登录成功
117 return True
118
119
120 async def baijiahao_cookie_gen(account_file, qrcode_callback=None, poll_interval: int = 3, max_checks: int = 120, headless: bool = LOCAL_CHROME_HEADLESS):
121 """无头/有头扫码登录百家号,保存 cookie。
122
123 流程:打开登录页 → 点「登录」按钮弹出百度 passport 登录框 → 截取二维码 → 等待扫码完成 → 保存 storage_state。
124 返回标准 login result dict。
125 """
126 account_file = _resolve_account_file(account_file)
127 Path(account_file).parent.mkdir(parents=True, exist_ok=True)
128 qrcode_path = None
129 result = _build_login_result(False, "failed", "百家号登录失败", account_file)
130
131 async with async_playwright() as playwright:
132 browser = await playwright.chromium.launch(**_build_launch_kwargs(headless=headless))
133 context = await browser.new_context()
134 try:
135 page = await context.new_page()
136 await page.goto(BAIJIAHAO_LOGIN_URL, timeout=60000, wait_until="domcontentloaded")
137 await page.wait_for_timeout(4000)
138
139 # 点击「登录」按钮触发百度 passport 弹窗
140 login_btn = page.get_by_text("登录", exact=True).first
141 try:
142 await login_btn.click(timeout=10000)
143 except Exception:
144 # 有些情况直接就在登录状态
145 pass
146 await page.wait_for_timeout(4000)
147
148 if headless:
149 baijiahao_logger.info(_msg("🧍", "无头登录中:二维码已存为图片,请用百度APP扫码"))
150 else:
151 baijiahao_logger.info(_msg("🧍", "请在打开的浏览器中扫码登录百家号"))
152
153 # 截取二维码
154 qrcode_info = await _grab_qr(page, account_file)
155 qrcode_path = Path(qrcode_info["image_path"]) if qrcode_info.get("image_path") else None
156 await _emit_qrcode_callback(qrcode_callback, qrcode_info)
157
158 baijiahao_logger.info(_msg("🧍", "请扫码,正在耐心等待登录完成"))
159
160 # 轮询等待登录完成
161 for _ in range(max_checks):
162 if await _is_login_completed(page):
163 baijiahao_logger.info(_msg("🥳", f"扫码成功,当前页面: {page.url}"))
164 result = _build_login_result(True, "success", "百家号扫码登录成功", account_file, qrcode_info, page.url)
165 break
166 await page.wait_for_timeout(poll_interval * 1000)
167 else:
168 result = _build_login_result(False, "timeout", "等待百家号扫码登录超时", account_file, qrcode_info, page.url)
169
170 if result["success"]:
171 await asyncio.sleep(2)
172 await context.storage_state(path=account_file)
173 baijiahao_logger.success(_msg("🥳", f"cookie 已保存: {account_file}"))
174 except Exception as exc:
175 result = _build_login_result(False, "failed", str(exc), account_file, current_url=page.url if "page" in locals() else "")
176 finally:
177 if remove_qrcode_file(qrcode_path):
178 baijiahao_logger.info(_msg("🧹", f"临时二维码文件已清理: {qrcode_path}"))
179 if not result["success"]:
180 baijiahao_logger.error(_msg("😢", f"登录失败: {result['message']}"))
181 await context.close()
182 await browser.close()
183 return result
184
185
186 async def cookie_auth(account_file):
187 """验证百家号 cookie 是否有效。访问后台首页,检测是否出现登录提示。"""
188 account_file = _resolve_account_file(account_file)
189 async with async_playwright() as playwright:
190 browser = await playwright.chromium.launch(**_build_launch_kwargs(headless=True))
191 try:
192 context = await browser.new_context(storage_state=account_file)
193 page = await context.new_page()
194 await page.goto(BAIJIAHAO_HOME_URL, timeout=60000, wait_until="domcontentloaded")
195 await page.wait_for_timeout(5000)
196
197 if await page.get_by_text("注册/登录百家号").count():
198 baijiahao_logger.info(_msg("🥹", "cookie 已失效"))
199 return False
200 else:
201 baijiahao_logger.success(_msg("🥳", "cookie 有效"))
202 return True
203 except Exception as exc:
204 baijiahao_logger.warning(_msg("😵", f"cookie 校验出错,按失效处理: {exc}"))
205 return False
206 finally:
207 await browser.close()
208
209
210 async def baijiahao_setup(account_file, handle=False, return_detail=False, qrcode_callback=None, headless: bool = LOCAL_CHROME_HEADLESS):
211 """统一入口:检查 cookie → 如无效且 handle=True 则触发扫码登录。"""
212 account_file = _resolve_account_file(account_file)
213 if not os.path.exists(account_file) or not await cookie_auth(account_file):
214 if not handle:
215 result = _build_login_result(False, "cookie_invalid", "cookie 文件不存在或已失效", account_file)
216 return result if return_detail else False
217 baijiahao_logger.info(_msg("🥹", "cookie 文件不存在或已失效,自动打开浏览器请扫码登录"))
218 result = await baijiahao_cookie_gen(account_file, qrcode_callback=qrcode_callback, headless=headless)
219 return result if return_detail else result["success"]
220
221 result = _build_login_result(True, "cookie_valid", "cookie 有效", account_file)
222 return result if return_detail else True
223
224
225 class BaiJiaHaoVideo(BaseVideoUploader):
226 """百家号视频上传。
227
228 流程:打开发布页 → 上传视频文件 → 填标题 → 等待上传/转码完成 → 等封面生成 → 点击发布。
229 """
230
231 def __init__(
232 self,
233 title,
234 file_path,
235 tags,
236 account_file,
237 publish_date=0,
238 desc: str | None = None,
239 thumbnail_path: str | None = None,
240 collection_name: str | None = None,
241 debug: bool = True,
242 headless: bool = LOCAL_CHROME_HEADLESS,
243 ):
244 self.title = title
245 self.file_path = file_path
246 self.tags = tags or []
247 self.account_file = _resolve_account_file(account_file)
248 self.publish_date = publish_date
249 self.desc = desc or ""
250 self.thumbnail_path = thumbnail_path
251 self.collection_name = collection_name
252 self.debug = debug
253 self.headless = headless
254 self.local_executable_path = LOCAL_CHROME_PATH
255 self.max_title_length = 30
256
257 async def validate_upload_args(self):
258 if not os.path.exists(self.account_file):
259 raise RuntimeError(f"cookie文件不存在,请先完成百家号登录: {self.account_file}")
260 if not await cookie_auth(self.account_file):
261 raise RuntimeError(f"cookie文件已失效,请先完成百家号登录: {self.account_file}")
262 if not self.title or not str(self.title).strip():
263 raise ValueError("视频标题不能为空")
264 if not self.thumbnail_path:
265 raise ValueError("百家号视频发布必须提供横版封面图(--thumbnail)")
266 self.file_path = str(self.validate_video_file(self.file_path))
267 self.thumbnail_path = str(self.validate_image_file(self.thumbnail_path))
268
269 async def upload(self, playwright: Playwright) -> None:
270 baijiahao_logger.info(_msg("🧍", "先检查 cookie 和视频文件"))
271 await self.validate_upload_args()
272 baijiahao_logger.info(_msg("🥳", "上传前检查通过"))
273
274 browser = await playwright.chromium.launch(**_build_launch_kwargs(headless=self.headless))
275 context = await browser.new_context(storage_state=self.account_file)
276 await context.grant_permissions(["geolocation"])
277
278 try:
279 page = await context.new_page()
280 await page.goto(BAIJIAHAO_PUBLISH_URL, timeout=120000, wait_until="domcontentloaded")
281 baijiahao_logger.info(_msg("🏃", f"开始上传视频: {self.title}"))
282
283 # 等待发布页加载
284 await page.wait_for_timeout(3000)
285
286 # 1) 上传视频文件
287 file_input = page.locator('input[type="file"][accept*="video"], input[type="file"][accept*="mp4"]').first
288 if not await file_input.count():
289 file_input = page.locator("div[class^='video-main-container'] input[type='file']").first
290 if not await file_input.count():
291 file_input = page.locator('input[type="file"]').first
292 await file_input.wait_for(state="attached", timeout=30000)
293 await file_input.set_input_files(self.file_path)
294 baijiahao_logger.info(_msg("🏃", f"已选择视频文件: {self.file_path}"))
295
296 # 2) 等待进入表单页面(contenteditable 标题区出现即表单渲染完毕)
297 title_editor = page.locator('div[class*="contentEditable"]').first
298 await title_editor.wait_for(state="visible", timeout=180000)
299 await page.wait_for_timeout(1000)
300
301 # 3) 填写标题
302 await self._fill_title(page)
303
304 # 4) 等待视频上传完成
305 await self._wait_upload_complete(page)
306
307 # 5) 上传横版封面(必填)
308 await self._upload_thumbnail(page)
309
310 # 6) 勾选「含AI生成内容」
311 await self._check_ai_declaration(page)
312
313 # 7) 选择合集(如有配置)
314 await self._apply_collection(page)
315
316 # 8) 点击发布
317 await self._submit_publish(page)
318
319 # 保存 cookie
320 await context.storage_state(path=self.account_file)
321 baijiahao_logger.success(_msg("🥳", "cookie 更新完毕"))
322 finally:
323 await context.close()
324 await browser.close()
325
326 async def _fill_title(self, page: Page) -> None:
327 title_field = page.locator('div[class*="contentEditable"]').first
328 await title_field.wait_for(state="visible", timeout=15000)
329 title = self.title
330 # 百家号标题最少9字
331 if len(title) <= 8:
332 title += " 你不知道的"
333 title = title[: self.max_title_length]
334 # 清空原有内容(可能自动填了文件名),再输入标题
335 await title_field.click()
336 await page.keyboard.press("Control+a")
337 await page.keyboard.press("Backspace")
338 await title_field.fill(title)
339 baijiahao_logger.info(_msg("🏷️", f"标题已填写: {title}"))
340
341 async def _wait_upload_complete(self, page: Page, timeout: int = 600) -> None:
342 """等待视频真正上传完成。
343
344 百度真实上传进度是一段百分比文字(9%…99%,上传完成后消失,本文件实测约 35s)。
345 旧实现用 'div .cover-overlay:has-text("上传中")' 判断——经实测该元素恒不存在,
346 导致选完文件立即误判"上传完毕"(约 4s)。大文件此时其实还在后台上传,随后点
347 发布会被百度以"确保视频已经上传完毕"拒绝(产出 0 作品)。改为跟踪百分比进度:
348 出现过进度且进度消失/达 100% 才算真正上传完成。
349 """
350 import re as _re
351 start = time.monotonic()
352 seen_progress = False
353 gone_count = 0
354 while True:
355 if time.monotonic() - start > timeout:
356 baijiahao_logger.warning(_msg("⚠️", f"等待上传超时(>{timeout}s),继续后续步骤"))
357 return
358
359 body = ""
360 try:
361 body = await page.inner_text("body")
362 except Exception:
363 pass
364
365 if "上传失败" in body:
366 raise RuntimeError("视频上传失败")
367
368 m = _re.search(r'(\d{1,3})\s*%', body)
369 pct = int(m.group(1)) if m else None
370
371 if pct is not None and pct < 100:
372 seen_progress = True
373 gone_count = 0
374 baijiahao_logger.info(_msg("🏃", f"上传中 {pct}%"))
375 await asyncio.sleep(2)
376 continue
377
378 if seen_progress:
379 # 进度百分比已消失/到 100%,连续两次确认后判为上传完成
380 gone_count += 1
381 if gone_count >= 2:
382 baijiahao_logger.success(_msg("🥳", "视频上传完毕"))
383 return
384 await asyncio.sleep(2)
385 continue
386
387 # 一直没出现过进度:小文件可能秒传完成;给 15s 窗口后放行
388 if time.monotonic() - start > 15:
389 baijiahao_logger.success(_msg("🥳", "视频上传完毕"))
390 return
391 await asyncio.sleep(2)
392
393 async def _upload_thumbnail(self, page: Page) -> None:
394 """上传横版封面(必填)。
395
396 流程:点击「选择封面」→ 弹窗中点「上传」按钮 → 设置图片文件 → 等待上传完成 → 确认。
397 如果没有提供 thumbnail_path,等待系统自动生成封面即可。
398 """
399 if not self.thumbnail_path:
400 # 没有自定义封面,等系统自动生成
401 await self._wait_cover_ready(page)
402 return
403
404 try:
405 # 1) 点击「选择封面」入口
406 cover_entry = page.locator('[data-testid="select-cover"]').first
407 if not await cover_entry.count():
408 # 备选:通过文本定位
409 cover_entry = page.get_by_text("选择封面", exact=True).first
410 await cover_entry.scroll_into_view_if_needed()
411 await cover_entry.click(timeout=10000)
412 baijiahao_logger.info(_msg("🏃", "已点击「选择封面」"))
413 await page.wait_for_timeout(2000)
414
415 # 2) 弹窗中找「上传」按钮并点击
416 # 百家号封面弹窗通常有「上传」tab/按钮
417 upload_btn = page.locator('button:has-text("上传"), div:has-text("上传"):not(:has(*)):visible').first
418 if not await upload_btn.count():
419 upload_btn = page.get_by_text("上传", exact=True).first
420 await upload_btn.click(timeout=8000)
421 await page.wait_for_timeout(1500)
422
423 # 3) 设置图片文件到 file input
424 # 弹窗中会出现 input[type=file]
425 img_input = page.locator('input[type="file"][accept*="image"], input[type="file"][accept*="jpg"], input[type="file"][accept*="png"]').first
426 if not await img_input.count():
427 # 通用 fallback:弹窗内最新出现的 file input
428 img_input = page.locator('input[type="file"]').last
429 await img_input.set_input_files(self.thumbnail_path)
430 baijiahao_logger.info(_msg("🏃", f"已选择封面图片: {self.thumbnail_path}"))
431
432 # 4) 等待并点击确认/完成按钮(如有裁剪弹窗)。
433 # 裁剪弹窗渲染有延迟(图片上传+服务端处理),之前用固定 sleep(3s) 后
434 # 一次性检查 confirm_btn,弹窗还没渲染出来时会被误判为"无需确认"而跳过点击,
435 # 导致封面选择实际未提交,但日志仍打「封面已上传」成功——这是本次线上
436 # 百家号视频没有封面、日志却显示成功的根因。改为轮询等待(不放大超时时长本身
437 # 不算错误:裁剪弹窗本就是可选的,等不到也可能是流程本身没有该弹窗)。
438 confirm_btn = page.locator('button:has-text("确定"), button:has-text("完成"), button:has-text("确认")').first
439 confirmed = False
440 try:
441 await confirm_btn.wait_for(state="visible", timeout=15000)
442 await confirm_btn.click(timeout=8000)
443 await page.wait_for_timeout(1000)
444 confirmed = True
445 except PWTimeoutError:
446 baijiahao_logger.debug("封面确认按钮未出现,可能本次流程无需裁剪确认")
447
448 if confirmed:
449 baijiahao_logger.success(_msg("🖼️", "封面已上传"))
450 else:
451 # 没有等到确认按钮:不确定封面是否真正生效,不再冒充成功,
452 # 交给下方 except 分支同一套"等待系统自动封面"兜底逻辑核实/兜底。
453 raise RuntimeError("封面确认按钮未出现,无法确认封面是否生效")
454 except Exception as exc:
455 baijiahao_logger.warning(_msg("⚠️", f"封面上传失败: {exc},尝试等待系统自动封面"))
456 # fallback:等系统自动生成
457 await self._wait_cover_ready(page)
458
459 async def _check_ai_declaration(self, page: Page) -> None:
460 """选择「含AI生成内容」创作声明。
461
462 点击「请选择创作声明」input → 弹出 modal 弹窗 → 点选「含AI生成内容」→ 点「确定」。
463 """
464 try:
465 # 点击创作声明输入框触发弹窗
466 trigger = page.locator('input[placeholder="请选择创作声明"]').first
467 await trigger.scroll_into_view_if_needed()
468 await trigger.click(force=True, timeout=8000)
469 await page.wait_for_timeout(3000)
470
471 # 弹窗内点选「含AI生成内容」
472 ai_option = page.locator('.cheetah-modal-wrap :text("含AI生成内容")').first
473 if not await ai_option.count():
474 ai_option = page.locator('text="含AI生成内容"').first
475 await ai_option.wait_for(state="visible", timeout=10000)
476 await ai_option.click(timeout=5000)
477 await page.wait_for_timeout(1000)
478
479 # 点「确定」按钮关闭弹窗(弹窗可能在点选后仍存在)
480 modal = page.locator('.cheetah-modal-wrap:visible').first
481 if await modal.count():
482 confirm_btn = modal.locator('button:has-text("确定")').first
483 if await confirm_btn.count() and await confirm_btn.is_visible():
484 await confirm_btn.click(timeout=5000)
485 await page.wait_for_timeout(500)
486 else:
487 # 确定按钮不可见,尝试 force click 或按 Escape 关闭
488 await page.keyboard.press("Escape")
489 await page.wait_for_timeout(500)
490
491 baijiahao_logger.success(_msg("🏷️", "已选择「含AI生成内容」"))
492 except Exception as exc:
493 # 如果失败,尝试关闭可能残留的弹窗
494 try:
495 await page.keyboard.press("Escape")
496 await page.wait_for_timeout(500)
497 except Exception:
498 pass
499 baijiahao_logger.warning(_msg("⚠️", f"选择 AI 声明失败: {exc}"))
500
501 async def _apply_collection(self, page: Page) -> None:
502 """选择合集(cheetah-select 下拉搜索框)。
503
504 placeholder: "选择同主题的合集,可获得更多播放机会"
505 有 collection_name 时点开下拉 → 搜索/选中目标合集;没有则跳过。
506 """
507 if not self.collection_name:
508 return
509 try:
510 # 定位合集下拉框(通过 placeholder 文案)
511 select_box = page.locator('.cheetah-select:has(.cheetah-select-selection-placeholder:has-text("选择同主题的合集"))').first
512 if not await select_box.count():
513 select_box = page.locator('.cheetah-select-selection-placeholder:has-text("合集")').locator('xpath=ancestor::div[contains(@class,"cheetah-select")]').first
514 if not await select_box.count():
515 baijiahao_logger.warning(_msg("⚠️", "未找到合集选择器,跳过"))
516 return
517
518 await select_box.scroll_into_view_if_needed()
519 await select_box.click(timeout=8000)
520 await page.wait_for_timeout(1500)
521
522 # 在搜索框中输入合集名(触发搜索过滤)
523 search_input = select_box.locator('input.cheetah-select-selection-search-input').first
524 if await search_input.count():
525 await search_input.fill(self.collection_name)
526 await page.wait_for_timeout(1500)
527
528 # 从下拉选项中选中目标合集
529 option = page.locator(f'[role="option"]:has-text("{self.collection_name}"), .cheetah-select-item:has-text("{self.collection_name}")').first
530 if await option.count():
531 await option.click(timeout=5000)
532 await page.wait_for_timeout(500)
533 baijiahao_logger.success(_msg("🥳", f"已选择合集:{self.collection_name}"))
534 else:
535 baijiahao_logger.warning(_msg("⚠️", f"账号中无「{self.collection_name}」合集,跳过"))
536 await page.keyboard.press("Escape")
537 except Exception as exc:
538 baijiahao_logger.warning(_msg("⚠️", f"选择合集失败,跳过: {exc}"))
539
540 async def _wait_cover_ready(self, page: Page, timeout: int = 120) -> None:
541 """等待百家号自动生成封面图。"""
542 start = time.monotonic()
543 while True:
544 if time.monotonic() - start > timeout:
545 baijiahao_logger.warning(_msg("⚠️", "等待封面生成超时,继续发布"))
546 return
547 if await page.locator("div.cheetah-spin-container img").count():
548 baijiahao_logger.info(_msg("🖼️", "封面已生成"))
549 return
550 baijiahao_logger.info(_msg("🏃", "等待封面生成..."))
551 await asyncio.sleep(3)
552
553 async def _submit_publish(self, page: Page) -> None:
554 """点击发布按钮并确认成功。"""
555 # 确保没有残留弹窗遮挡
556 modal = page.locator('.cheetah-modal-wrap:visible').first
557 if await modal.count():
558 await page.keyboard.press("Escape")
559 await page.wait_for_timeout(1000)
560
561 # 百家号发布按钮有 data-testid="publish-btn"
562 publish_btn = page.locator('[data-testid="publish-btn"]').first
563 if not await publish_btn.count():
564 publish_btn = page.locator('button:text-is("发布")').first
565 if not await publish_btn.count():
566 publish_btn = page.locator('button:has-text("发布")').last
567 await publish_btn.wait_for(state="visible", timeout=15000)
568 await publish_btn.click(force=True)
569 baijiahao_logger.info(_msg("🏃", "已点击发布按钮"))
570
571 # 等待跳转或成功提示(最多30s)
572 start = time.monotonic()
573 while time.monotonic() - start < 30:
574 url = page.url
575 # 发布成功跳转
576 if BAIJIAHAO_SUCCESS_URL_PREFIX in url or "/rc/content" in url or "/rc/home" in url:
577 baijiahao_logger.success(_msg("🥳", "视频发布成功"))
578 return
579 # 检查是否出现百度安全验证
580 if await page.locator('text="百度安全验证"').count():
581 raise RuntimeError("出现百度安全验证,需人工处理")
582 # 检查是否有错误提示阻止发布
583 error_toast = page.locator('.cheetah-message-error, .cheetah-message-warning').first
584 if await error_toast.count() and await error_toast.is_visible():
585 err_text = await error_toast.inner_text()
586 baijiahao_logger.warning(_msg("⚠️", f"发布提示: {err_text}"))
587 await page.wait_for_timeout(1000)
588
589 # 超时后再检查一次
590 if BAIJIAHAO_SUCCESS_URL_PREFIX in page.url or "/rc/content" in page.url:
591 baijiahao_logger.success(_msg("🥳", "视频发布成功"))
592 else:
593 raise RuntimeError(f"发布后未跳转到成功页面(30s),当前 URL: {page.url}")
594
595 async def main(self):
596 async with async_playwright() as playwright:
597 await self.upload(playwright)
598
598 lines PYTHON