From 77b372a56e78e9eba6c37d7fa735bd677cf80a28 Mon Sep 17 00:00:00 2001 From: anzhen9 Date: Sat, 11 Jul 2026 16:43:37 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat(douyin):=20=E6=B7=BB=E5=8A=A0=E7=9F=AD?= =?UTF-8?q?=E4=BF=A1=E9=AA=8C=E8=AF=81=E7=A0=81=E5=BC=B9=E7=AA=97=E5=A4=84?= =?UTF-8?q?=E7=90=86=E5=B9=B6=E7=A7=BB=E9=99=A4=E5=A4=9A=E4=BD=99=E6=B5=8F?= =?UTF-8?q?=E8=A7=88=E5=99=A8=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- uploader/douyin_uploader/main.py | 51 ++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/uploader/douyin_uploader/main.py b/uploader/douyin_uploader/main.py index 772086b..297442d 100644 --- a/uploader/douyin_uploader/main.py +++ b/uploader/douyin_uploader/main.py @@ -10,7 +10,7 @@ from patchright.async_api import Page from patchright.async_api import Playwright from patchright.async_api import async_playwright -from conf import DEBUG_MODE, LOCAL_CHROME_HEADLESS, LOCAL_CHROME_PATH +from conf import BASE_DIR, DEBUG_MODE, LOCAL_CHROME_HEADLESS, LOCAL_CHROME_PATH from uploader.base_video import BaseVideoUploader from utils.base_social_media import set_init_script from utils.login_qrcode import build_login_qrcode_path @@ -53,7 +53,7 @@ async def cookie_auth(account_file): # 即便有头,页面慢/瞬时跳转仍会让 wait_for_url(精确URL,5s) 误判→重试3次+宽松判定(URL含 content/upload 且无登录文案)。 # 允许 linux server 用户通过 env var 强制无头: DOUYIN_COOKIE_AUTH_HEADLESS=true use_headless = os.environ.get("DOUYIN_COOKIE_AUTH_HEADLESS", "").lower() in ("1", "true", "yes") - launch_kwargs = {"headless": use_headless, "channel": "chrome", "args": ["--no-sandbox", "--disable-blink-features=AutomationControlled"]} + launch_kwargs = {"headless": use_headless, "args": ["--no-sandbox", "--disable-blink-features=AutomationControlled"]} for _attempt in range(3): async with async_playwright() as playwright: browser = await playwright.chromium.launch(**launch_kwargs) @@ -172,6 +172,8 @@ async def _is_douyin_login_completed(page: Page) -> bool: async def _wait_for_douyin_login(page: Page, account_file: str, qrcode_info: dict, qrcode_callback=None, poll_interval: int = 3, max_checks: int = 100) -> dict: qrcode_path = Path(qrcode_info["image_path"]) if qrcode_info.get("image_path") else None + original_url = page.url + saw_2fa = False for _ in range(max_checks): if await _is_douyin_login_completed(page): douyin_logger.info(_msg("🥳", f"扫码成功,已经跳转到登录后页面: {page.url}")) @@ -575,7 +577,7 @@ class DouYinVideo(DouYinBaseUploader): douyin_logger.info(_msg("🏃", "小人正在设置视频封面")) # 先清掉 shepherd 新手引导浮层,否则它会拦截“选择封面”点击导致弹窗打不开 await page.evaluate( - "() => document.querySelectorAll('.shepherd-element,.shepherd-modal-overlay-container').forEach(e=>e.remove())" + "() => { document.querySelectorAll('.shepherd-element, .shepherd-modal-overlay-container, [class*=\"mention-wrapper\"]').forEach(e => e.remove()); }" ) await page.get_by_text("选择封面", exact=True).first.click(force=True) cover_locator_str = 'div.dy-creator-content-modal' @@ -698,6 +700,49 @@ class DouYinVideo(DouYinBaseUploader): await page.evaluate( "() => { document.querySelectorAll('.shepherd-element, .shepherd-modal-overlay-container, [class*=\"mention-wrapper\"]').forEach(e => e.remove()); }" ) + # 检测并处理短信验证码弹窗 + sms_input = page.locator('input[placeholder*="验证码"], input[type="tel"], input[placeholder*="短信"], input[placeholder*="手机号"]').first + if await sms_input.count() and await sms_input.is_visible(): + douyin_logger.warning(_msg("📱", "检测到短信验证码弹窗")) + # 点击「获取验证码」按钮(仅首次) + get_code_btn = page.get_by_text("获取验证码").first + if await get_code_btn.count() and await get_code_btn.is_visible(): + await get_code_btn.click() + douyin_logger.info(_msg("📤", "已点击「获取验证码」,请查看手机短信")) + # 等待验证码文件 + code_file = os.path.join(BASE_DIR, "verify_code.txt") + if os.path.exists(code_file): + code = open(code_file).read().strip() + if code: + douyin_logger.info(_msg("✍️", f"从文件读取到验证码: {code}")) + await sms_input.click() + await sms_input.fill(code) + douyin_logger.info(_msg("✅", "验证码已填入输入框")) + await page.wait_for_timeout(500) + # 点击「验证」按钮(div 类型按钮,用 force=True + JS 兜底) + verify_btn = page.locator('div.uc-ui-verify_sms-verify_button:has-text("验证")').first + if await verify_btn.count() and await verify_btn.is_visible(): + try: + await verify_btn.click(force=True) + douyin_logger.success(_msg("✅", "已点击「验证」按钮 (force)")) + except Exception: + await page.eval_on_selector('div.uc-ui-verify_sms-verify_button', 'el => el.click()') + douyin_logger.success(_msg("✅", "已点击「验证」按钮 (JS)")) + else: + # 兜底:按文本找 + verify_by_text = page.get_by_text("验证", exact=True).first + if await verify_by_text.count(): + await verify_by_text.click(force=True) + douyin_logger.success(_msg("✅", "已点击「验证」按钮 (text)")) + else: + douyin_logger.warning(_msg("⚠️", "未找到验证按钮,尝试按Enter")) + await page.keyboard.press("Enter") + os.remove(code_file) + douyin_logger.info(_msg("🧹", "验证码文件已清理")) + await page.wait_for_timeout(3000) + douyin_logger.info(_msg("🔄", "验证码处理完成,继续发布流程")) + else: + douyin_logger.warning(_msg("⏳", "等待你发送验证码到微信...")) publish_button = page.get_by_role("button", name="发布", exact=True) if await publish_button.count(): await publish_button.click(force=True) From fa59d500302dffedc167548d3bd8515a56da3437 Mon Sep 17 00:00:00 2001 From: anzhen9 Date: Sat, 11 Jul 2026 16:47:04 +0800 Subject: [PATCH 2/4] =?UTF-8?q?Revert=20"feat(douyin):=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E7=9F=AD=E4=BF=A1=E9=AA=8C=E8=AF=81=E7=A0=81=E5=BC=B9?= =?UTF-8?q?=E7=AA=97=E5=A4=84=E7=90=86=E5=B9=B6=E7=A7=BB=E9=99=A4=E5=A4=9A?= =?UTF-8?q?=E4=BD=99=E6=B5=8F=E8=A7=88=E5=99=A8=E5=8F=82=E6=95=B0"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 77b372a56e78e9eba6c37d7fa735bd677cf80a28. --- uploader/douyin_uploader/main.py | 51 ++------------------------------ 1 file changed, 3 insertions(+), 48 deletions(-) diff --git a/uploader/douyin_uploader/main.py b/uploader/douyin_uploader/main.py index 297442d..772086b 100644 --- a/uploader/douyin_uploader/main.py +++ b/uploader/douyin_uploader/main.py @@ -10,7 +10,7 @@ from patchright.async_api import Page from patchright.async_api import Playwright from patchright.async_api import async_playwright -from conf import BASE_DIR, DEBUG_MODE, LOCAL_CHROME_HEADLESS, LOCAL_CHROME_PATH +from conf import DEBUG_MODE, LOCAL_CHROME_HEADLESS, LOCAL_CHROME_PATH from uploader.base_video import BaseVideoUploader from utils.base_social_media import set_init_script from utils.login_qrcode import build_login_qrcode_path @@ -53,7 +53,7 @@ async def cookie_auth(account_file): # 即便有头,页面慢/瞬时跳转仍会让 wait_for_url(精确URL,5s) 误判→重试3次+宽松判定(URL含 content/upload 且无登录文案)。 # 允许 linux server 用户通过 env var 强制无头: DOUYIN_COOKIE_AUTH_HEADLESS=true use_headless = os.environ.get("DOUYIN_COOKIE_AUTH_HEADLESS", "").lower() in ("1", "true", "yes") - launch_kwargs = {"headless": use_headless, "args": ["--no-sandbox", "--disable-blink-features=AutomationControlled"]} + launch_kwargs = {"headless": use_headless, "channel": "chrome", "args": ["--no-sandbox", "--disable-blink-features=AutomationControlled"]} for _attempt in range(3): async with async_playwright() as playwright: browser = await playwright.chromium.launch(**launch_kwargs) @@ -172,8 +172,6 @@ async def _is_douyin_login_completed(page: Page) -> bool: async def _wait_for_douyin_login(page: Page, account_file: str, qrcode_info: dict, qrcode_callback=None, poll_interval: int = 3, max_checks: int = 100) -> dict: qrcode_path = Path(qrcode_info["image_path"]) if qrcode_info.get("image_path") else None - original_url = page.url - saw_2fa = False for _ in range(max_checks): if await _is_douyin_login_completed(page): douyin_logger.info(_msg("🥳", f"扫码成功,已经跳转到登录后页面: {page.url}")) @@ -577,7 +575,7 @@ class DouYinVideo(DouYinBaseUploader): douyin_logger.info(_msg("🏃", "小人正在设置视频封面")) # 先清掉 shepherd 新手引导浮层,否则它会拦截“选择封面”点击导致弹窗打不开 await page.evaluate( - "() => { document.querySelectorAll('.shepherd-element, .shepherd-modal-overlay-container, [class*=\"mention-wrapper\"]').forEach(e => e.remove()); }" + "() => document.querySelectorAll('.shepherd-element,.shepherd-modal-overlay-container').forEach(e=>e.remove())" ) await page.get_by_text("选择封面", exact=True).first.click(force=True) cover_locator_str = 'div.dy-creator-content-modal' @@ -700,49 +698,6 @@ class DouYinVideo(DouYinBaseUploader): await page.evaluate( "() => { document.querySelectorAll('.shepherd-element, .shepherd-modal-overlay-container, [class*=\"mention-wrapper\"]').forEach(e => e.remove()); }" ) - # 检测并处理短信验证码弹窗 - sms_input = page.locator('input[placeholder*="验证码"], input[type="tel"], input[placeholder*="短信"], input[placeholder*="手机号"]').first - if await sms_input.count() and await sms_input.is_visible(): - douyin_logger.warning(_msg("📱", "检测到短信验证码弹窗")) - # 点击「获取验证码」按钮(仅首次) - get_code_btn = page.get_by_text("获取验证码").first - if await get_code_btn.count() and await get_code_btn.is_visible(): - await get_code_btn.click() - douyin_logger.info(_msg("📤", "已点击「获取验证码」,请查看手机短信")) - # 等待验证码文件 - code_file = os.path.join(BASE_DIR, "verify_code.txt") - if os.path.exists(code_file): - code = open(code_file).read().strip() - if code: - douyin_logger.info(_msg("✍️", f"从文件读取到验证码: {code}")) - await sms_input.click() - await sms_input.fill(code) - douyin_logger.info(_msg("✅", "验证码已填入输入框")) - await page.wait_for_timeout(500) - # 点击「验证」按钮(div 类型按钮,用 force=True + JS 兜底) - verify_btn = page.locator('div.uc-ui-verify_sms-verify_button:has-text("验证")').first - if await verify_btn.count() and await verify_btn.is_visible(): - try: - await verify_btn.click(force=True) - douyin_logger.success(_msg("✅", "已点击「验证」按钮 (force)")) - except Exception: - await page.eval_on_selector('div.uc-ui-verify_sms-verify_button', 'el => el.click()') - douyin_logger.success(_msg("✅", "已点击「验证」按钮 (JS)")) - else: - # 兜底:按文本找 - verify_by_text = page.get_by_text("验证", exact=True).first - if await verify_by_text.count(): - await verify_by_text.click(force=True) - douyin_logger.success(_msg("✅", "已点击「验证」按钮 (text)")) - else: - douyin_logger.warning(_msg("⚠️", "未找到验证按钮,尝试按Enter")) - await page.keyboard.press("Enter") - os.remove(code_file) - douyin_logger.info(_msg("🧹", "验证码文件已清理")) - await page.wait_for_timeout(3000) - douyin_logger.info(_msg("🔄", "验证码处理完成,继续发布流程")) - else: - douyin_logger.warning(_msg("⏳", "等待你发送验证码到微信...")) publish_button = page.get_by_role("button", name="发布", exact=True) if await publish_button.count(): await publish_button.click(force=True) From be77aaef8be27913e222300d8f815a9ca3ef8973 Mon Sep 17 00:00:00 2001 From: anzhen9 Date: Sat, 11 Jul 2026 16:52:42 +0800 Subject: [PATCH 3/4] =?UTF-8?q?feat(douyin):=20=E5=A4=84=E7=90=86=E7=9F=AD?= =?UTF-8?q?=E4=BF=A1=E9=AA=8C=E8=AF=81=E7=A0=81=E5=BC=B9=E7=AA=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- uploader/douyin_uploader/main.py | 49 ++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/uploader/douyin_uploader/main.py b/uploader/douyin_uploader/main.py index 772086b..78b0721 100644 --- a/uploader/douyin_uploader/main.py +++ b/uploader/douyin_uploader/main.py @@ -10,7 +10,7 @@ from patchright.async_api import Page from patchright.async_api import Playwright from patchright.async_api import async_playwright -from conf import DEBUG_MODE, LOCAL_CHROME_HEADLESS, LOCAL_CHROME_PATH +from conf import BASE_DIR, DEBUG_MODE, LOCAL_CHROME_HEADLESS, LOCAL_CHROME_PATH from uploader.base_video import BaseVideoUploader from utils.base_social_media import set_init_script from utils.login_qrcode import build_login_qrcode_path @@ -172,6 +172,8 @@ async def _is_douyin_login_completed(page: Page) -> bool: async def _wait_for_douyin_login(page: Page, account_file: str, qrcode_info: dict, qrcode_callback=None, poll_interval: int = 3, max_checks: int = 100) -> dict: qrcode_path = Path(qrcode_info["image_path"]) if qrcode_info.get("image_path") else None + original_url = page.url + saw_2fa = False for _ in range(max_checks): if await _is_douyin_login_completed(page): douyin_logger.info(_msg("🥳", f"扫码成功,已经跳转到登录后页面: {page.url}")) @@ -575,7 +577,7 @@ class DouYinVideo(DouYinBaseUploader): douyin_logger.info(_msg("🏃", "小人正在设置视频封面")) # 先清掉 shepherd 新手引导浮层,否则它会拦截“选择封面”点击导致弹窗打不开 await page.evaluate( - "() => document.querySelectorAll('.shepherd-element,.shepherd-modal-overlay-container').forEach(e=>e.remove())" + "() => { document.querySelectorAll('.shepherd-element, .shepherd-modal-overlay-container, [class*=\"mention-wrapper\"]').forEach(e => e.remove()); }" ) await page.get_by_text("选择封面", exact=True).first.click(force=True) cover_locator_str = 'div.dy-creator-content-modal' @@ -698,6 +700,49 @@ class DouYinVideo(DouYinBaseUploader): await page.evaluate( "() => { document.querySelectorAll('.shepherd-element, .shepherd-modal-overlay-container, [class*=\"mention-wrapper\"]').forEach(e => e.remove()); }" ) + # 检测并处理短信验证码弹窗 + sms_input = page.locator('input[placeholder*="验证码"], input[type="tel"], input[placeholder*="短信"], input[placeholder*="手机号"]').first + if await sms_input.count() and await sms_input.is_visible(): + douyin_logger.warning(_msg("📱", "检测到短信验证码弹窗")) + # 点击「获取验证码」按钮(仅首次) + get_code_btn = page.get_by_text("获取验证码").first + if await get_code_btn.count() and await get_code_btn.is_visible(): + await get_code_btn.click() + douyin_logger.info(_msg("📤", "已点击「获取验证码」,请查看手机短信")) + # 等待验证码文件 + code_file = os.path.join(BASE_DIR, "verify_code.txt") + if os.path.exists(code_file): + code = open(code_file).read().strip() + if code: + douyin_logger.info(_msg("✍️", f"从文件读取到验证码: {code}")) + await sms_input.click() + await sms_input.fill(code) + douyin_logger.info(_msg("✅", "验证码已填入输入框")) + await page.wait_for_timeout(500) + # 点击「验证」按钮(div 类型按钮,用 force=True + JS 兜底) + verify_btn = page.locator('div.uc-ui-verify_sms-verify_button:has-text("验证")').first + if await verify_btn.count() and await verify_btn.is_visible(): + try: + await verify_btn.click(force=True) + douyin_logger.success(_msg("✅", "已点击「验证」按钮 (force)")) + except Exception: + await page.eval_on_selector('div.uc-ui-verify_sms-verify_button', 'el => el.click()') + douyin_logger.success(_msg("✅", "已点击「验证」按钮 (JS)")) + else: + # 兜底:按文本找 + verify_by_text = page.get_by_text("验证", exact=True).first + if await verify_by_text.count(): + await verify_by_text.click(force=True) + douyin_logger.success(_msg("✅", "已点击「验证」按钮 (text)")) + else: + douyin_logger.warning(_msg("⚠️", "未找到验证按钮,尝试按Enter")) + await page.keyboard.press("Enter") + os.remove(code_file) + douyin_logger.info(_msg("🧹", "验证码文件已清理")) + await page.wait_for_timeout(3000) + douyin_logger.info(_msg("🔄", "验证码处理完成,继续发布流程")) + else: + douyin_logger.warning(_msg("⏳", "等待你发送验证码到微信...")) publish_button = page.get_by_role("button", name="发布", exact=True) if await publish_button.count(): await publish_button.click(force=True) From 395c6434bb2d8aa99a92a746cf8b032afb360f7b Mon Sep 17 00:00:00 2001 From: anzhen9 Date: Sat, 11 Jul 2026 17:18:13 +0800 Subject: [PATCH 4/4] =?UTF-8?q?feat(douyin):=20=E6=94=AF=E6=8C=81=E7=BB=88?= =?UTF-8?q?=E7=AB=AF=E4=BA=A4=E4=BA=92=E8=BE=93=E5=85=A5=E7=9F=AD=E4=BF=A1?= =?UTF-8?q?=E9=AA=8C=E8=AF=81=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + docs/CLI.md | 7 +++ docs/install.md | 7 +++ uploader/douyin_uploader/main.py | 88 ++++++++++++++++++++------------ 4 files changed, 70 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index f0a6bc1..198a966 100644 --- a/README.md +++ b/README.md @@ -204,6 +204,7 @@ sau youtube upload-video --account --file videos/demo.mp4 --title - `creator` 之类的名字只是示例值,真正含义是 `account_name` - 一个 `account_name` 对应一个账号文件,可以准备多个账号,也可以按账号名并发执行任务 +- 抖音视频发布若触发短信二次验证,程序会优先读取项目根目录下的 `verify_code.txt`;如果你是在本地交互式终端手动运行 CLI,也可以直接按终端提示输入验证码 - 浏览器平台统一约定: - 视频使用 `title + desc + tags` - 图文使用 `title + note + tags` diff --git a/docs/CLI.md b/docs/CLI.md index 9e65c89..a55fb6e 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -52,6 +52,13 @@ sau douyin upload-video --account --file videos/demo.mp4 --title sau douyin upload-note --account --images videos/1.png videos/2.png --title "图文标题" --note "图文示例" --tags 图文,测试 ``` +抖音短信验证码补充说明: + +- 视频发布过程中如果触发短信二次验证,CLI 会优先读取项目根目录下的 `verify_code.txt` +- 如果未找到 `verify_code.txt`,并且当前命令是在交互式终端中手动运行,CLI 会直接在终端提示输入验证码 +- 对 agent、自动任务、远程桥接这类场景,仍然可以继续用写入 `verify_code.txt` 的方式喂验证码 +- 验证通过后,程序会自动清理 `verify_code.txt` + ## 快手 CLI 子命令 ```bash diff --git a/docs/install.md b/docs/install.md index 5530ee2..d2079f1 100644 --- a/docs/install.md +++ b/docs/install.md @@ -112,6 +112,13 @@ sau douyin upload-note --account --images videos/demo1.png videos sau douyin upload-note --account --images videos/demo1.png videos/demo2.png --title "图文标题" --note $noteText --tags 'tag1,tag2' --bgm '音乐名称' ``` +抖音短信验证码补充说明: + +- 视频发布过程中如果触发短信二次验证,程序会优先读取项目根目录下的 `verify_code.txt` +- 如果当前是你手动运行的交互式终端,没提供 `verify_code.txt` 时,CLI 会直接提示你在终端输入验证码 +- 如果是 agent 或自动化桥接场景,仍然可以继续通过写入 `verify_code.txt` 来提供验证码 +- 验证通过后,程序会自动删除 `verify_code.txt` + 抖音卡login手动获取cookie: - 目标服务器使用vnc diff --git a/uploader/douyin_uploader/main.py b/uploader/douyin_uploader/main.py index 78b0721..9109809 100644 --- a/uploader/douyin_uploader/main.py +++ b/uploader/douyin_uploader/main.py @@ -4,6 +4,7 @@ from datetime import datetime import asyncio import inspect import os +import sys from pathlib import Path from patchright.async_api import Page @@ -48,6 +49,20 @@ def _build_login_result(success: bool, status: str, message: str, account_file: } +async def _read_verify_code(code_file: str) -> str: + if os.path.exists(code_file): + with open(code_file, encoding="utf-8") as file_obj: + return file_obj.read().strip() + + if not sys.stdin or not sys.stdin.isatty(): + return "" + + try: + return (await asyncio.to_thread(input, "请输入抖音短信验证码(直接回车可稍后重试): ")).strip() + except (EOFError, OSError): + return "" + + async def cookie_auth(account_file): # 抖音无头会撞反爬墙→content/upload 跳登录→误判 cookie 失效(间歇性)。校验必须有头。 # 即便有头,页面慢/瞬时跳转仍会让 wait_for_url(精确URL,5s) 误判→重试3次+宽松判定(URL含 content/upload 且无登录文案)。 @@ -534,6 +549,38 @@ class DouYinVideo(DouYinBaseUploader): self.productTitle = productTitle self.desc = desc or "" + async def _submit_sms_verify_code(self, page: Page, sms_input, code: str, code_file: str) -> bool: + douyin_logger.info(_msg("✍️", f"已获取验证码,准备填入: {code}")) + await sms_input.click() + await sms_input.fill(code) + douyin_logger.info(_msg("✅", "验证码已填入输入框")) + await page.wait_for_timeout(500) + + verify_btn = page.locator('div.uc-ui-verify_sms-verify_button:has-text("验证")').first + if await verify_btn.count() and await verify_btn.is_visible(): + try: + await verify_btn.click(force=True) + douyin_logger.success(_msg("✅", "已点击「验证」按钮 (force)")) + except Exception: + await page.eval_on_selector('div.uc-ui-verify_sms-verify_button', 'el => el.click()') + douyin_logger.success(_msg("✅", "已点击「验证」按钮 (JS)")) + else: + verify_by_text = page.get_by_text("验证", exact=True).first + if await verify_by_text.count(): + await verify_by_text.click(force=True) + douyin_logger.success(_msg("✅", "已点击「验证」按钮 (text)")) + else: + douyin_logger.warning(_msg("⚠️", "未找到验证按钮,尝试按Enter")) + await page.keyboard.press("Enter") + + if os.path.exists(code_file): + os.remove(code_file) + douyin_logger.info(_msg("🧹", "验证码文件已清理")) + + await page.wait_for_timeout(3000) + douyin_logger.info(_msg("🔄", "验证码处理完成,继续发布流程")) + return True + async def validate_upload_args(self): await self.validate_base_args() if not self.title or not str(self.title).strip(): @@ -694,6 +741,7 @@ class DouYinVideo(DouYinBaseUploader): if self.publish_strategy == DOUYIN_PUBLISH_STRATEGY_SCHEDULED and self.publish_date != 0: await self.set_schedule_time_douyin(page, self.publish_date) + sms_prompt_logged = False while True: try: # 移除会拦截发布按钮点击的新手引导/话题下拉浮层 @@ -709,40 +757,14 @@ class DouYinVideo(DouYinBaseUploader): if await get_code_btn.count() and await get_code_btn.is_visible(): await get_code_btn.click() douyin_logger.info(_msg("📤", "已点击「获取验证码」,请查看手机短信")) - # 等待验证码文件 code_file = os.path.join(BASE_DIR, "verify_code.txt") - if os.path.exists(code_file): - code = open(code_file).read().strip() - if code: - douyin_logger.info(_msg("✍️", f"从文件读取到验证码: {code}")) - await sms_input.click() - await sms_input.fill(code) - douyin_logger.info(_msg("✅", "验证码已填入输入框")) - await page.wait_for_timeout(500) - # 点击「验证」按钮(div 类型按钮,用 force=True + JS 兜底) - verify_btn = page.locator('div.uc-ui-verify_sms-verify_button:has-text("验证")').first - if await verify_btn.count() and await verify_btn.is_visible(): - try: - await verify_btn.click(force=True) - douyin_logger.success(_msg("✅", "已点击「验证」按钮 (force)")) - except Exception: - await page.eval_on_selector('div.uc-ui-verify_sms-verify_button', 'el => el.click()') - douyin_logger.success(_msg("✅", "已点击「验证」按钮 (JS)")) - else: - # 兜底:按文本找 - verify_by_text = page.get_by_text("验证", exact=True).first - if await verify_by_text.count(): - await verify_by_text.click(force=True) - douyin_logger.success(_msg("✅", "已点击「验证」按钮 (text)")) - else: - douyin_logger.warning(_msg("⚠️", "未找到验证按钮,尝试按Enter")) - await page.keyboard.press("Enter") - os.remove(code_file) - douyin_logger.info(_msg("🧹", "验证码文件已清理")) - await page.wait_for_timeout(3000) - douyin_logger.info(_msg("🔄", "验证码处理完成,继续发布流程")) - else: - douyin_logger.warning(_msg("⏳", "等待你发送验证码到微信...")) + code = await _read_verify_code(code_file) + if code: + sms_prompt_logged = False + await self._submit_sms_verify_code(page, sms_input, code, code_file) + elif not sms_prompt_logged: + douyin_logger.warning(_msg("⏳", f"等待验证码输入;可在交互终端直接输入,或写入文件: {code_file}")) + sms_prompt_logged = True publish_button = page.get_by_role("button", name="发布", exact=True) if await publish_button.count(): await publish_button.click(force=True)