`,
选中后对应 `woo-radio-shadow` 追加 `woo-radio-checked`。
"""
label = page.locator('label.woo-radio-main:has(span.woo-radio-text:text-is("二创"))').first
await label.wait_for(state="visible", timeout=20000)
await label.click()
await page.wait_for_timeout(500)
checked_sel = 'label.woo-radio-main:has(span.woo-radio-text:text-is("二创")) span.woo-radio-checked'
if not await page.locator(checked_sel).count():
# 兜底:直接勾选 radio input
try:
await label.locator('input.woo-radio-input').check()
await page.wait_for_timeout(300)
except Exception:
pass
if not await page.locator(checked_sel).count():
raise RuntimeError("类型「二创」未选中")
weibo_logger.info(_msg("🏷️", "类型已选:二创"))
async def _select_declaration(self, page: Page) -> None:
"""内容声明(必选):选择「含AI生成内容」。
真实 DOM:
- 触发下拉:`
` 内 `.woo-pop-ctrl`(带 caretDown 的 wbpro-select)
- 弹层:`
`,选项 ``
- 选中后该 button 内 `._check_nsgmr_237` 追加 `_checkActive_nsgmr_251`(带 _checkMark)
- 底部 `._footer_nsgmr_270 button`("确定")关闭弹层
"""
# 打开下拉
trigger = page.locator('div[class*="_gap1_nsgmr"] .woo-pop-ctrl').first
if not await trigger.count():
trigger = page.locator('div:has(> div[class*="_tit1_nsgmr"]) .woo-pop-ctrl').first
await trigger.wait_for(state="visible", timeout=15000)
await trigger.click()
await page.wait_for_timeout(1000)
panel = page.locator('div[class*="_panel_nsgmr"]').first
if await panel.count():
try:
await panel.wait_for(state="visible", timeout=8000)
except PWTimeoutError:
panel = None
else:
panel = None
scope = panel if panel is not None else page
ai_opt = scope.locator('button:has(span:text-is("含AI生成内容"))').first
await ai_opt.wait_for(state="visible", timeout=8000)
await ai_opt.click()
await page.wait_for_timeout(500)
# 校验选中态
if not await ai_opt.locator('[class*="_checkActive"]').count():
weibo_logger.warning(_msg("⚠️", "内容声明「含AI生成内容」疑似未激活,仍尝试点确定"))
# 点确定关闭弹层
confirm = scope.locator('div[class*="_footer_nsgmr"] button:has(span:text-is("确定"))').first
if not await confirm.count():
confirm = scope.locator('button:has(span:text-is("确定"))').last
if await confirm.count():
await confirm.click()
await page.wait_for_timeout(500)
weibo_logger.info(_msg("🏷️", "内容声明已选:含AI生成内容"))
async def _fill_description(self, page: Page) -> None:
"""填写描述区域(正文 + 标签)。
微博描述区 placeholder: "有什么新鲜事想分享给大家?"
标签用 #话题# 格式插入到描述末尾。
"""
desc_field = page.get_by_placeholder("有什么新鲜事想分享给大家?")
if not await desc_field.count():
weibo_logger.warning(_msg("⚠️", "未找到描述输入框"))
return
# 组装描述内容:正文 + 标签
content = self.desc
if self.tags:
tag_str = " ".join(f"#{t}#" for t in self.tags)
content = f"{content}\n{tag_str}" if content else tag_str
if content:
await desc_field.click()
await desc_field.fill(content)
weibo_logger.info(_msg("📝", f"描述已填写({len(content)}字)"))
async def _apply_collection(self, page: Page) -> None:
"""合集:选已有,没有则新建。
真实 DOM:打开「合集」开关后出现合集面板 `._scroll_19x8d_143`——已有合集每行一个
`woo-checkbox` + 只读 `input value="名字(共N集)"`;末尾 `._add_19x8d_63`(「新建合集」)。
- 已有:勾选名字匹配(去掉"(共N集)"后缀后)那一行的 checkbox。
- 没有:点「新建合集」→ 新增一行(自动勾选)且带可编辑 input → 填合集名(≤12)。
"""
target = (self.collection_name or "").strip()
if not target:
return
# 1) 打开合集开关
block = page.locator('div[class*="_switch_"]:has(div[class*="_tit1_"]:text-is("合集"))').first
if not await block.count():
block = page.locator('div:has(> div:text-is("合集")):has(label.woo-switch-main)').first
try:
switch_input = block.locator('label.woo-switch-main input.woo-switch-input').first
try:
already = await switch_input.is_checked()
except Exception:
already = False
if not already:
for sw in (
block.locator('label.woo-switch-main span[role="switch"]').first,
block.locator('label.woo-switch-main').first,
):
try:
await sw.click(timeout=6000)
except Exception:
try:
await sw.click(timeout=4000, force=True)
except Exception:
continue
await page.wait_for_timeout(1000)
try:
if await switch_input.is_checked():
break
except Exception:
break
except Exception as exc:
weibo_logger.warning(_msg("⚠️", f"打开合集开关异常,仍尝试找面板: {exc}"))
# 2) 合集面板
panel = page.locator('div[class*="_scroll_"]:has(div[class*="_add_"])').first
if not await panel.count():
panel = page.locator('div:has(> div[class*="_add_"]:has-text("新建合集"))').first
try:
await panel.wait_for(state="visible", timeout=8000)
except PWTimeoutError:
weibo_logger.warning(_msg("⚠️", "未见合集面板,跳过合集"))
return
# 3) 匹配已有合集(去掉"(共N集)"后缀)
rows = panel.locator('div[class*="_top2_"]')
n = await rows.count()
matched = False
for i in range(n):
row = rows.nth(i)
inp = row.locator('input[type="text"]').first
if not await inp.count():
continue
val = (await inp.get_attribute("value")) or ""
name = re.sub(r"\(共\d+集\)\s*$", "", val).strip()
if name and name == target:
await row.locator('label.woo-checkbox-main').first.click()
await page.wait_for_timeout(400)
matched = True
weibo_logger.info(_msg("🥳", f"已选已有合集:{target}"))
break
# 4) 没有则新建(best-effort:新建失败只跳过合集,绝不中断发布)
if not matched:
try:
add_btn = panel.locator('div[class*="_add_"]:has-text("新建合集")').first
if not await add_btn.count():
add_btn = page.locator('div:has-text("新建合集")').last
# 「新建合集」整行 598px 宽、可点的"+新建合集"文字在左侧;点整行几何中心会落到
# 右侧空白、不触发。改为点内部"新建合集"文字 span(在左侧、必命中 onClick)。
add_target = add_btn.get_by_text("新建合集", exact=True).first
if not await add_target.count():
add_target = add_btn
# 新建行的可编辑 input(已有行的 input 都带 disabled,新建行的没有)
new_inp = panel.locator('div[class*="_top2_"] input[type="text"]:not([disabled])').last
created = False
for _ in range(3):
try:
await add_target.scroll_into_view_if_needed(timeout=2000)
except Exception:
pass
try:
await add_target.click(timeout=4000)
except Exception:
try:
await add_target.click(timeout=3000, force=True)
except Exception:
try:
await add_target.evaluate("el => el.click()")
except Exception:
pass
await page.wait_for_timeout(800)
if await new_inp.count() and await new_inp.is_visible():
created = True
break
if not created:
weibo_logger.warning(_msg("⚠️", f"「新建合集」未出现输入行,跳过合集继续发布:{target[:12]}"))
return
await new_inp.click()
await new_inp.fill(target[:12])
await page.wait_for_timeout(500)
weibo_logger.info(_msg("🥳", f"已新建合集:{target[:12]}"))
except Exception as exc:
weibo_logger.warning(_msg("⚠️", f"新建合集失败,跳过合集继续发布:{exc}"))
return
async def _submit_publish(self, page: Page) -> None:
"""点击发布并校验真成功。
真实 DOM:
- 发布按钮:`._check_2z30i_81 button`(内容"发布")。按钮中心可能被空 div 覆盖,
用 JS 触发按钮自身 click 绕过遮罩。
- 成功唯一可靠判据:隐藏成功层 `_layer1_9a8j7_2` 由 `display:none` 变**可见**,
其中含"再发一条视频"按钮 → 用它/该按钮可见判定真成功。
("视频已上传成功,将在转码后发布"文字是恒存在的隐藏模板,不能作判据。)
- 60s 内判不到成功 → 抛错(不再冒充成功),交由上层记失败。
"""
# 关掉可能残留的下拉/弹层
try:
await page.keyboard.press("Escape")
await page.wait_for_timeout(300)
except Exception:
pass
publish_btn = page.locator('div[class*="_check_2z30i"] button:has(span:text-is("发布"))').first
if not await publish_btn.count():
publish_btn = page.get_by_role("button", name="发布").first
await publish_btn.wait_for(state="visible", timeout=15000)
await publish_btn.evaluate("el => el.click()")
weibo_logger.info(_msg("🏃", "已点击发布按钮(JS)"))
success_layer = page.locator('div[class*="_layer1_9a8j7"]').first
again_btn = page.locator('button:has(span:text-is("再发一条视频"))').first
start = time.monotonic()
while time.monotonic() - start < 60:
try:
if await again_btn.is_visible():
weibo_logger.success(_msg("🥳", "视频发布成功(出现「再发一条视频」)"))
return
except Exception:
pass
try:
if await success_layer.is_visible():
weibo_logger.success(_msg("🥳", "视频发布成功(成功层可见)"))
return
except Exception:
pass
# 处理可能的二次确认对话框
try:
dialog = page.locator('.woo-dialog-main, .woo-modal-wrap, [class*="Dialog"]').first
if await dialog.count() and await dialog.is_visible():
for name in ("确定", "确认", "继续", "仍然发布", "发布"):
cb = dialog.locator(f'button:has(span:text-is("{name}"))').first
if await cb.count() and await cb.is_visible():
await cb.evaluate("el => el.click()")
weibo_logger.info(_msg("🏃", f"已确认对话框:{name}"))
break
except Exception:
pass
await page.wait_for_timeout(1500)
raise RuntimeError("发布后 60s 未见成功层/「再发一条视频」,判定发布未成功(未入库)")
async def main(self):
async with async_playwright() as playwright:
await self.upload(playwright)