From 1b36b8bd9bac20873ee28a440edb65def2238fec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=BA=E8=A1=8Colo?= Date: Sat, 30 May 2026 18:50:49 -0700 Subject: [PATCH 1/2] =?UTF-8?q?fix(xiaohongshu):=20=E8=AF=9D=E9=A2=98?= =?UTF-8?q?=E5=80=99=E9=80=89=E7=AD=89=E4=B8=8D=E5=88=B0=E6=97=B6=E8=B7=B3?= =?UTF-8?q?=E8=BF=87=E8=AF=A5=E6=A0=87=E7=AD=BE=E7=BB=A7=E7=BB=AD=E5=8F=91?= =?UTF-8?q?=E5=B8=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 话题依赖小红书联想接口实时返回,网络抖动或无匹配时等不到候选框,会让整条 发布失败。改为 try/except:放宽超时(3s/2s→6s/4s),仍等不到就记 warning、 退格清掉已键入的 #tag 残留文本后 continue。标签为可选增强项,不应阻断主流程。 Co-Authored-By: Claude Opus 4.8 --- uploader/xiaohongshu_uploader/main.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/uploader/xiaohongshu_uploader/main.py b/uploader/xiaohongshu_uploader/main.py index 7ae0e8e..fad7846 100644 --- a/uploader/xiaohongshu_uploader/main.py +++ b/uploader/xiaohongshu_uploader/main.py @@ -400,14 +400,25 @@ class XiaoHongShuBaseUploader(BaseVideoUploader): await desc.click() for tag in self.tags: # 循环处理所有 tags - await page.keyboard.type("#" + tag, delay=30) - await page.locator('#creator-editor-topic-container').wait_for( - state="visible", - timeout=3000 - ) - first_item = page.locator('#creator-editor-topic-container .item').first - await first_item.wait_for(state="visible", timeout=2000) - await first_item.click() + # 话题候选下拉框依赖小红书联想接口实时返回,网络抖动/无匹配时会等不到。 + # 标签是可选增强项:等不到候选框就跳过该标签继续,不让整条发布因此失败。 + try: + await page.keyboard.type("#" + tag, delay=30) + await page.locator('#creator-editor-topic-container').wait_for( + state="visible", + timeout=6000 + ) + first_item = page.locator('#creator-editor-topic-container .item').first + await first_item.wait_for(state="visible", timeout=4000) + await first_item.click() + except Exception as exc: + xiaohongshu_logger.warning( + _msg("🏷️", f"话题『{tag}』未出现候选,跳过该标签继续发布: {exc}") + ) + # 清掉已键入但未成词的 "#tag" 文本,避免它残留进正文 + for _ in range(len("#" + tag)): + await page.keyboard.press("Backspace") + continue async def fill_meta(self, page: Page) -> None: await self.fill_title(page) From 7ae6943627ef5f56773d2e684c6f46a3dd420dfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=BA=E8=A1=8Colo?= Date: Sat, 30 May 2026 18:50:49 -0700 Subject: [PATCH 2/2] =?UTF-8?q?feat(douyin):=20=E3=80=8C=E8=87=AA=E4=B8=BB?= =?UTF-8?q?=E5=A3=B0=E6=98=8E=E3=80=8D=E9=BB=98=E8=AE=A4=E5=8B=BE=E9=80=89?= =?UTF-8?q?=E3=80=8C=E5=86=85=E5=AE=B9=E4=B8=BA=E4=B8=AA=E4=BA=BA=E8=A7=82?= =?UTF-8?q?=E7=82=B9=E6=88=96=E8=A7=81=E8=A7=A3=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 抖音发布页「自主声明」为必选项,此前自动化未填,是发布流程的缺口。新增 DouYinBaseUploader.set_self_declaration:点开占位「请选择自主声明」→ 弹窗 「对作品内容添加声明」中单选「内容为个人观点或见解」(exact 避免误点预览 文案) → 确定;在 DouYinVideo 贴完封面后调用。入口/弹窗均异步渲染,等不到 则 warning 跳过、继续发布,与小红书话题、视频号声明原创的容错策略一致。 当前仅覆盖视频(DouYinVideo),图文 DouYinNote 未涉及。 Co-Authored-By: Claude Opus 4.8 --- uploader/douyin_uploader/main.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/uploader/douyin_uploader/main.py b/uploader/douyin_uploader/main.py index 83160a4..5b25597 100644 --- a/uploader/douyin_uploader/main.py +++ b/uploader/douyin_uploader/main.py @@ -367,6 +367,35 @@ class DouYinBaseUploader(BaseVideoUploader): douyin_logger.error(_msg("😢", f"设置商品链接时出错: {str(e)}")) return False + async def set_self_declaration(self, page: Page, declaration: str = "内容为个人观点或见解") -> None: + """抖音「自主声明」为发布必选项:打开声明弹窗 → 选指定类型 → 确定。 + + 入口和弹窗都是异步渲染,等不到就记 warning 跳过、继续发布,绝不因此中断 + (与小红书话题、视频号声明原创的容错策略保持一致)。 + """ + try: + # 发布页底部「自主声明」行,未选时显示占位文案「请选择自主声明」 + entry = page.get_by_text("请选择自主声明").first + await entry.wait_for(state="visible", timeout=6000) + await entry.click() + + # 弹窗标题「对作品内容添加声明」 + dialog = page.locator(".semi-modal-content").filter(has_text="对作品内容添加声明").first + await dialog.wait_for(state="visible", timeout=6000) + + # 单选项:Semi 的文字是 .semi-radio-addon(常带 pointer-events:none,直接点会卡 30s 超时), + # 要点可交互的 .semi-radio 外层;找不到外层再退回 force 强制点文字。exact 避免误命中预览「作者声明:…」。 + option = dialog.locator(".semi-radio").filter(has_text=declaration).first + if await option.count(): + await option.click(timeout=6000) + else: + await dialog.get_by_text(declaration, exact=True).first.click(timeout=6000, force=True) + await dialog.get_by_role("button", name="确定").click(timeout=6000) + await dialog.wait_for(state="hidden", timeout=6000) + douyin_logger.info(_msg("🧾", f"自主声明已选择「{declaration}」")) + except Exception as exc: + douyin_logger.warning(_msg("🧾", f"自主声明设置失败,跳过该步骤继续发布:{exc}")) + class DouYinVideo(DouYinBaseUploader): def __init__( @@ -532,6 +561,8 @@ class DouYinVideo(DouYinBaseUploader): await self.set_thumbnail(page) + await self.set_self_declaration(page) + third_part_element = '[class^="info"] > [class^="first-part"] div div.semi-switch' if await page.locator(third_part_element).count(): if "semi-switch-checked" not in await page.eval_on_selector(third_part_element, "div => div.className"):