From 395c6434bb2d8aa99a92a746cf8b032afb360f7b Mon Sep 17 00:00:00 2001 From: anzhen9 Date: Sat, 11 Jul 2026 17:18:13 +0800 Subject: [PATCH] =?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)