fix(uploader): 适配抖音与视频号发布页最新 DOM 结构

抖音 douyin_uploader:
- 填标题/描述:改用 input[placeholder*=填写作品标题] 与 div.zone-container[contenteditable] 定位
- 话题输入后按 Escape 收起下拉,避免浮层拦截
- '选择封面'与'发布'改用 force 点击,并移除拦截点击的新手引导(shepherd)浮层
- 封面弹窗适配:div.dy-creator-content-modal + input.semi-upload-hidden-input;横竖版切换改用'设置竖封面'按钮;完成后等弹窗关闭

视频号 tencent_uploader:
- upload_video_file 遍历所有 frame 查找文件框 + 重试等待

说明:两平台发布均需有头模式(--headed),无头下视频号上传框不渲染。
This commit is contained in:
changyandaoc
2026-06-13 12:01:24 +08:00
parent 90e01c0106
commit f5cb31079c
2 changed files with 44 additions and 21 deletions
+17 -17
View File
@@ -263,17 +263,12 @@ class DouYinBaseUploader(BaseVideoUploader):
await asyncio.sleep(1)
async def fill_title_and_description(self, page: Page, title: str, description: str, tags: list[str] | None = None):
description_section = (
page.get_by_text("作品描述", exact=True)
.locator("xpath=ancestor::div[2]")
.locator("xpath=following-sibling::div[1]")
)
title_input = description_section.locator('input[type="text"]').first
# 2026-06 抖音发布页 DOM:标题=input[placeholder*=填写作品标题],描述=div.zone-container[contenteditable]
title_input = page.locator('input[placeholder*="填写作品标题"]').first
await title_input.wait_for(state="visible", timeout=10000)
await title_input.fill(title[:30])
description_editor = description_section.locator('.zone-container[contenteditable="true"]').first
description_editor = page.locator('div.zone-container[contenteditable="true"]').first
await description_editor.wait_for(state="visible", timeout=10000)
await description_editor.click()
await page.keyboard.press("Control+KeyA")
@@ -283,6 +278,7 @@ class DouYinBaseUploader(BaseVideoUploader):
for tag in tags or []:
await page.keyboard.type(" #" + tag)
await page.keyboard.press("Space")
await page.keyboard.press("Escape") # 收起话题下拉,避免浮层拦截后续点击
async def set_location(self, page: Page, location: str = ""):
if not location:
@@ -471,12 +467,12 @@ class DouYinVideo(DouYinBaseUploader):
return
douyin_logger.info(_msg("🏃", "小人正在设置视频封面"))
await page.click('text="选择封面"')
cover_locator_str = 'div[id*="creator-content-modal"]'
cover_locator = page.locator(cover_locator_str)
await page.wait_for_selector(cover_locator_str)
await page.click('text="选择封面"', force=True)
cover_locator_str = 'div.dy-creator-content-modal'
cover_locator = page.locator(cover_locator_str).first
await page.wait_for_selector(cover_locator_str, timeout=15000)
upload_input = cover_locator.locator("div[class^='semi-upload upload'] >> input.semi-upload-hidden-input")
upload_input = cover_locator.locator("input.semi-upload-hidden-input").first
if self.thumbnail_landscape_path:
await page.wait_for_timeout(1000)
@@ -485,15 +481,15 @@ class DouYinVideo(DouYinBaseUploader):
douyin_logger.info(_msg("🖼️", "横版封面上传完成"))
if self.thumbnail_portrait_path:
await cover_locator.locator("div[class*='steps'] div").nth(1).click()
await cover_locator.locator('button:has-text("设置竖封面")').first.click()
await page.wait_for_timeout(1000)
await upload_input.set_input_files(self.thumbnail_portrait_path)
await page.wait_for_timeout(2000)
douyin_logger.info(_msg("🖼️", "竖版封面上传完成"))
await cover_locator.locator('button:visible:has-text("完成")').click()
await cover_locator.locator('button:has-text("完成")').first.click()
douyin_logger.info(_msg("🥳", "视频封面设置完成"))
await page.wait_for_selector("div.extractFooter", state="detached")
await cover_locator.wait_for(state="detached", timeout=15000)
async def upload(self, playwright: Playwright) -> None:
douyin_logger.info(_msg("🧍", "小人先检查 cookie、视频文件、封面和发布时间"))
@@ -573,9 +569,13 @@ class DouYinVideo(DouYinBaseUploader):
while True:
try:
# 移除会拦截发布按钮点击的新手引导/话题下拉浮层
await page.evaluate(
"() => { document.querySelectorAll('.shepherd-element, .shepherd-modal-overlay-container, [class*=\"mention-wrapper\"]').forEach(e => e.remove()); }"
)
publish_button = page.get_by_role("button", name="发布", exact=True)
if await publish_button.count():
await publish_button.click()
await publish_button.click(force=True)
await page.wait_for_url(
"https://creator.douyin.com/creator-micro/content/manage**",
timeout=3000,