From f5cb31079cd2f790996549c81fb726afe10471d9 Mon Sep 17 00:00:00 2001 From: changyandaoc Date: Sat, 13 Jun 2026 11:54:29 +0800 Subject: [PATCH] =?UTF-8?q?fix(uploader):=20=E9=80=82=E9=85=8D=E6=8A=96?= =?UTF-8?q?=E9=9F=B3=E4=B8=8E=E8=A7=86=E9=A2=91=E5=8F=B7=E5=8F=91=E5=B8=83?= =?UTF-8?q?=E9=A1=B5=E6=9C=80=E6=96=B0=20DOM=20=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 抖音 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),无头下视频号上传框不渲染。 --- uploader/douyin_uploader/main.py | 34 +++++++++++++++---------------- uploader/tencent_uploader/main.py | 31 ++++++++++++++++++++++++---- 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/uploader/douyin_uploader/main.py b/uploader/douyin_uploader/main.py index 5b25597..b89c5e4 100644 --- a/uploader/douyin_uploader/main.py +++ b/uploader/douyin_uploader/main.py @@ -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, diff --git a/uploader/tencent_uploader/main.py b/uploader/tencent_uploader/main.py index 94cded1..db887f7 100644 --- a/uploader/tencent_uploader/main.py +++ b/uploader/tencent_uploader/main.py @@ -495,12 +495,35 @@ class TencentBaseUploader(BaseVideoUploader): await page.locator("div.input-editor").click() async def open_upload_page(self, page: Page) -> None: - await page.goto(TENCENT_UPLOAD_URL) - await page.wait_for_url(TENCENT_UPLOAD_URL) + await page.goto(TENCENT_UPLOAD_URL, timeout=120000, wait_until="domcontentloaded") + await page.wait_for_url(TENCENT_UPLOAD_URL, timeout=120000) async def upload_video_file(self, page: Page, file_path: str) -> None: - file_input = page.locator('input[type="file"]') - await file_input.set_input_files(file_path) + async def find_file_input(): + for fr in page.frames: # 主 frame + 所有 iframe(视频号编辑器可能在 iframe 内) + try: + fi = fr.locator('input[type="file"]') + if await fi.count(): + return fi.first + except Exception: + continue + return None + + fi = await find_file_input() + if fi is None: + # 助手落在首页:先点「发表视频」唤出编辑器与上传控件 + publish_btn = page.get_by_text("发表视频").first + if await publish_btn.count(): + await publish_btn.click() + await asyncio.sleep(3) + for _ in range(20): + fi = await find_file_input() + if fi is not None: + break + await asyncio.sleep(1) + if fi is None: + raise RuntimeError("未找到视频号文件上传框") + await fi.set_input_files(file_path) async def set_short_title(self, page: Page, title: str, short_title: str | None = None) -> None: short_title_element = (