🔄 卡若AI 同步 2026-03-10 15:47 | 更新:卡木、运营中枢工作台 | 排除 >20MB: 11 个

This commit is contained in:
2026-03-10 15:47:45 +08:00
parent a2d1019912
commit b0070d5e52
4 changed files with 308 additions and 86 deletions

View File

@@ -1,10 +1,8 @@
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/soul 派对 119场 20260309_output/成片/Soul业务模型 派对+切片+小程序全链路.mp4", "title": "派对获客→AI切片→小程序变现全链路拆解 #商业模式 #一人公司", "success": true, "status": "reviewing", "message": "纯API投稿成功 (7.2s)", "elapsed_sec": 7.174537897109985, "timestamp": "2026-03-10 14:16:20"}
{"platform": "快手", "video_path": "/Users/karuo/Movies/soul视频/soul 派对 119场 20260309_output/成片/Soul业务模型 派对+切片+小程序全链路.mp4", "title": "派对获客→AI切片→小程序变现全链路拆解 #商业模式 #一人公司", "success": true, "status": "published", "message": "发布成功", "screenshot": "/tmp/kuaishou_result.png", "elapsed_sec": 22.87360692024231, "timestamp": "2026-03-10 14:16:36"}
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/soul 派对 119场 20260309_output/成片/Soul业务模型 派对+切片+小程序全链路.mp4", "title": "派对获客→AI切片→小程序变现全链路拆解 #商业模式 #一人公司", "success": true, "status": "reviewing", "message": "已跳转到内容管理(发表成功)", "screenshot": "/tmp/channels_result.png", "elapsed_sec": 24.60638403892517, "timestamp": "2026-03-10 14:16:37"}
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/soul 派对 119场 20260309_output/成片/早起不是为了开派对,是不吵老婆睡觉.mp4", "title": "每天6点起床不是因为自律是因为老婆还在睡 #Soul派对 #创业日记", "success": true, "status": "reviewing", "message": "纯API投稿成功 (7.0s)", "elapsed_sec": 6.964767932891846, "timestamp": "2026-03-10 14:17:08"}
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/soul 派对 119场 20260309_output/成片/早起不是为了开派对,是不吵老婆睡觉.mp4", "title": "每天6点起床不是因为自律是因为老婆还在睡 #Soul派对 #创业日记", "success": true, "status": "reviewing", "message": "已提交,请确认截图", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 33.28828287124634, "timestamp": "2026-03-10 14:17:35"}
{"platform": "快手", "video_path": "/Users/karuo/Movies/soul视频/soul 派对 119场 20260309_output/成片/早起不是为了开派对,是不吵老婆睡觉.mp4", "title": "每天6点起床不是因为自律是因为老婆还在睡 #Soul派对 #创业日记", "success": true, "status": "published", "message": "发布成功", "screenshot": "/tmp/kuaishou_result.png", "elapsed_sec": 41.6892192363739, "timestamp": "2026-03-10 14:17:43"}
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/soul 派对 119场 20260309_output/成片/早起不是为了开派对,是不吵老婆睡觉.mp4", "title": "每天6点起床不是因为自律是因为老婆还在睡 #Soul派对 #创业日记", "success": true, "status": "reviewing", "message": "已跳转到内容管理(发表成功)", "screenshot": "/tmp/channels_result.png", "elapsed_sec": 25.486361026763916, "timestamp": "2026-03-10 14:17:27"}
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/soul 派对 119场 20260309_output/成片/ICU出来一年多 活着要在互联网上留下东西.mp4", "title": "ICU出来一年多活着就要在互联网上留下东西 #人生感悟 #创业觉醒", "success": true, "status": "published", "message": "页面已重置(发布成功)", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 23.01875376701355, "timestamp": "2026-03-10 15:00:58"}
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/soul 派对 119场 20260309_output/成片/MBTI疗愈SOUL 年轻人测MBTI40到60岁走五行八卦.mp4", "title": "20岁测MBTI40岁该学五行八卦了 #MBTI #认知觉醒", "success": true, "status": "reviewing", "message": "已提交,请确认截图", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 22.772515773773193, "timestamp": "2026-03-10 15:01:24"}
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/soul 派对 119场 20260309_output/成片/Soul业务模型 派对+切片+小程序全链路.mp4", "title": "派对获客→AI切片→小程序变现全链路拆解 #商业模式 #一人公司", "success": false, "status": "error", "message": "异常: [Errno 32] Broken pipe", "elapsed_sec": 13.620775938034058, "timestamp": "2026-03-10 15:01:41"}
@@ -47,19 +45,9 @@
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/soul 派对 119场 20260309_output/成片/睡眠不好?每天放下一件事,做减法.mp4", "title": "睡不好不是太累,是脑子装太多,每天做减法 #做减法 #心理健康", "success": true, "status": "reviewing", "message": "纯API投稿成功 (3.0s)", "elapsed_sec": 3.016058921813965, "timestamp": "2026-03-10 15:22:18"}
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/soul 派对 119场 20260309_output/成片/这套体系花了170万但前端几十块就能参与.mp4", "title": "后端花170万搭体系前端几十块就能参与 #商业认知 #体系思维", "success": true, "status": "reviewing", "message": "纯API投稿成功 (6.0s)", "elapsed_sec": 5.999068021774292, "timestamp": "2026-03-10 15:22:27"}
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/soul 派对 119场 20260309_output/成片/金融AI获客体系 后端30人沉淀12年前端丢手机.mp4", "title": "后端30人沉淀12年前端就丢个手机号 #AI获客 #系统思维", "success": true, "status": "reviewing", "message": "纯API投稿成功 (4.1s)", "elapsed_sec": 4.084810972213745, "timestamp": "2026-03-10 15:22:34"}
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/soul 派对 119场 20260309_output/成片/ICU出来一年多 活着要在互联网上留下东西.mp4", "title": "ICU出来一年多活着就要在互联网上留下东西 #人生感悟 #创业觉醒", "success": true, "status": "reviewing", "message": "已跳转到内容管理(发表成功)", "screenshot": "/tmp/channels_result.png", "elapsed_sec": 22.915637969970703, "timestamp": "2026-03-10 15:15:51"}
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/soul 派对 119场 20260309_output/成片/MBTI疗愈SOUL 年轻人测MBTI40到60岁走五行八卦.mp4", "title": "20岁测MBTI40岁该学五行八卦了 #MBTI #认知觉醒", "success": true, "status": "reviewing", "message": "已跳转到内容管理(发表成功)", "screenshot": "/tmp/channels_result.png", "elapsed_sec": 21.477245092391968, "timestamp": "2026-03-10 15:16:16"}
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/soul 派对 119场 20260309_output/成片/Soul切片30秒到8分钟 AI半小时能剪10到30个.mp4", "title": "AI剪辑半小时出10到30条切片内容工厂效率密码 #AI剪辑 #内容效率", "success": true, "status": "reviewing", "message": "已跳转到内容管理(发表成功)", "screenshot": "/tmp/channels_result.png", "elapsed_sec": 23.59407615661621, "timestamp": "2026-03-10 15:16:42"}
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/soul 派对 119场 20260309_output/成片/初期团队先找两个IS比钱好使 ENFJ链接人ENTJ指挥.mp4", "title": "创业初期先找两个IS型人格比融资好使十倍 #MBTI创业 #团队搭建", "success": true, "status": "reviewing", "message": "已跳转到内容管理(发表成功)", "screenshot": "/tmp/channels_result.png", "elapsed_sec": 21.847800970077515, "timestamp": "2026-03-10 15:17:07"}
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/soul 派对 119场 20260309_output/成片/刷牙听业务逻辑 Soul切片变现怎么跑.mp4", "title": "刷牙3分钟听完一套变现逻辑 #碎片创业 #副业逻辑", "success": true, "status": "reviewing", "message": "已跳转到内容管理(发表成功)", "screenshot": "/tmp/channels_result.png", "elapsed_sec": 28.180445194244385, "timestamp": "2026-03-10 15:17:39"}
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/soul 派对 119场 20260309_output/成片/国学易经怎么学 两小时七七八八,召唤作者对话.mp4", "title": "易经两小时学个七七八八,关键是跟古人对话 #国学 #易经入门", "success": true, "status": "reviewing", "message": "已跳转到内容管理(发表成功)", "screenshot": "/tmp/channels_result.png", "elapsed_sec": 22.183687925338745, "timestamp": "2026-03-10 15:18:04"}
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/soul 派对 119场 20260309_output/成片/广点通能投Soul了1000曝光6到10块.mp4", "title": "广点通能投Soul了1000次曝光只要6到10块 #广点通 #低成本获客", "success": true, "status": "reviewing", "message": "已跳转到内容管理(发表成功)", "screenshot": "/tmp/channels_result.png", "elapsed_sec": 22.535322904586792, "timestamp": "2026-03-10 15:18:30"}
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/soul 派对 119场 20260309_output/成片/建立信任不是求来的 卖外挂发邮件三个月拿下德国总代.mp4", "title": "信任不是求来的,发三个月邮件拿下德国总代理 #销售思维 #信任建立", "success": true, "status": "reviewing", "message": "已跳转到内容管理(发表成功)", "screenshot": "/tmp/channels_result.png", "elapsed_sec": 21.166264057159424, "timestamp": "2026-03-10 15:18:54"}
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/soul 派对 119场 20260309_output/成片/懒人的活法 动作简单有利可图正反馈.mp4", "title": "懒人也能赚钱?动作简单、有利可图、正反馈 #Soul派对 #副业思维", "success": true, "status": "reviewing", "message": "已跳转到内容管理(发表成功)", "screenshot": "/tmp/channels_result.png", "elapsed_sec": 19.76073694229126, "timestamp": "2026-03-10 15:19:17"}
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/soul 派对 119场 20260309_output/成片/核心就两个字 筛选。能开派对坚持7天的人再谈.mp4", "title": "核心就两个字筛选。能坚持7天的人才值得深聊 #筛选思维 #创业认知", "success": true, "status": "reviewing", "message": "已跳转到内容管理(发表成功)", "screenshot": "/tmp/channels_result.png", "elapsed_sec": 21.475642919540405, "timestamp": "2026-03-10 15:19:41"}
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/soul 派对 119场 20260309_output/成片/睡眠不好?每天放下一件事,做减法.mp4", "title": "睡不好不是太累,是脑子装太多,每天做减法 #做减法 #心理健康", "success": true, "status": "reviewing", "message": "已跳转到内容管理(发表成功)", "screenshot": "/tmp/channels_result.png", "elapsed_sec": 20.54416298866272, "timestamp": "2026-03-10 15:20:05"}
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/soul 派对 119场 20260309_output/成片/这套体系花了170万但前端几十块就能参与.mp4", "title": "后端花170万搭体系前端几十块就能参与 #商业认知 #体系思维", "success": true, "status": "reviewing", "message": "已跳转到内容管理(发表成功)", "screenshot": "/tmp/channels_result.png", "elapsed_sec": 20.57265615463257, "timestamp": "2026-03-10 15:20:29"}
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/soul 派对 119场 20260309_output/成片/金融AI获客体系 后端30人沉淀12年前端丢手机.mp4", "title": "后端30人沉淀12年前端就丢个手机号 #AI获客 #系统思维", "success": true, "status": "reviewing", "message": "已跳转到内容管理(发表成功)", "screenshot": "/tmp/channels_result.png", "elapsed_sec": 26.00135087966919, "timestamp": "2026-03-10 15:20:58"}
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/soul 派对 119场 20260309_output/成片/Soul业务模型 派对+切片+小程序全链路.mp4", "title": "派对获客→AI切片→小程序变现全链路拆解 #商业模式 #一人公司", "success": true, "status": "reviewing", "message": "已提交,请确认截图", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 32.887428998947144, "timestamp": "2026-03-10 15:16:01"}
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/soul 派对 119场 20260309_output/成片/Soul切片30秒到8分钟 AI半小时能剪10到30个.mp4", "title": "AI剪辑半小时出10到30条切片内容工厂效率密码 #AI剪辑 #内容效率", "success": true, "status": "reviewing", "message": "已提交,请确认截图", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 26.56322979927063, "timestamp": "2026-03-10 15:16:31"}
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/soul 派对 119场 20260309_output/成片/初期团队先找两个IS比钱好使 ENFJ链接人ENTJ指挥.mp4", "title": "创业初期先找两个IS型人格比融资好使十倍 #MBTI创业 #团队搭建", "success": true, "status": "reviewing", "message": "已提交,请确认截图", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 24.749696016311646, "timestamp": "2026-03-10 15:16:59"}

View File

@@ -1,11 +1,14 @@
#!/usr/bin/env python3
"""
视频号发布 - Headless Playwright
上传 → 填描述 → 发表。视频号无公开APIPlaywright为唯一方案。
视频号发布 v2 — API 响应拦截 + 列表验证 + 小程序挂载
- 不再仅靠页面跳转判断成功;拦截 cgi-bin 响应 + 内容列表复核
- 支持扩展链接挂载小程序
"""
import asyncio
import json
import sys
import time
from dataclasses import dataclass, field
from pathlib import Path
SCRIPT_DIR = Path(__file__).parent
@@ -20,6 +23,10 @@ UA = (
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36"
)
MINIPROGRAM_APPID = "wxb8bbb2b10dec74aa"
MINIPROGRAM_TITLE = "Soul创业派对"
MINIPROGRAM_PAGE = "pages/read/read?mid=119"
TITLES = {
"早起不是为了开派对,是不吵老婆睡觉.mp4":
"每天6点起床不是因为自律是因为老婆还在睡 #Soul派对 #创业日记",
@@ -54,7 +61,142 @@ TITLES = {
}
async def publish_one(video_path: str, title: str, idx: int = 1, total: int = 1, skip_dedup: bool = False, scheduled_time=None) -> PublishResult:
# ---------------------------------------------------------------------------
# API Capture
# ---------------------------------------------------------------------------
@dataclass
class ApiCapture:
publish_responses: list = field(default_factory=list)
all_calls: list = field(default_factory=list)
async def handle(self, response):
url = response.url
if "cgi-bin" not in url and "finder-assistant" not in url:
return
record = {"url": url, "status": response.status}
try:
body = await response.json()
record["body"] = body
except Exception:
pass
self.all_calls.append(record)
lower = url.lower()
if any(k in lower for k in ("publish", "post_create", "post_publish", "create_post")):
self.publish_responses.append(record)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
async def _extract_token(page) -> str | None:
url = page.url
if "token=" in url:
return url.split("token=")[1].split("&")[0]
try:
return await page.evaluate(
"window.__wxConfig && window.__wxConfig.token || ''"
) or None
except Exception:
return None
async def _verify_on_list(page, title_keyword: str) -> tuple[bool, str]:
"""Navigate to content-list page and look for the video."""
try:
token = await _extract_token(page)
list_url = "https://channels.weixin.qq.com/platform/post/list"
if token:
list_url += f"?token={token}"
await page.goto(list_url, timeout=20000, wait_until="domcontentloaded")
await asyncio.sleep(6)
body_text = await page.evaluate("document.body.innerText")
kw = title_keyword[:20]
if kw in body_text:
return True, f"标题匹配 ({kw})"
found = await page.evaluate("""(kw) => {
const items = document.querySelectorAll('[class*="post-feed"] [class*="desc"], [class*="post-item"] [class*="desc"]');
for (const el of items) {
if (el.textContent.includes(kw)) return true;
}
return false;
}""", kw)
if found:
return True, f"DOM匹配 ({kw})"
return False, "未在列表前20条中找到"
except Exception as e:
return False, f"验证异常: {str(e)[:60]}"
async def _try_add_miniprogram(page) -> bool:
"""Attempt to attach miniprogram via the publish-page UI."""
try:
found = await page.evaluate("""() => {
const all = [...document.querySelectorAll('span, div, button, a, label')];
for (const el of all) {
const t = el.textContent.trim();
if ((t.includes('扩展链接') || t.includes('添加链接') || t === '短视频带货')
&& el.offsetParent !== null) {
el.click();
return 'clicked';
}
}
return 'not_found';
}""")
if found != "clicked":
print(" [小程序] 未找到「扩展链接」入口", flush=True)
return False
await asyncio.sleep(1.5)
mp_found = await page.evaluate("""() => {
const all = [...document.querySelectorAll('span, div, li, a')];
for (const el of all) {
if (el.textContent.trim() === '小程序' && el.offsetParent !== null) {
el.click();
return true;
}
}
return false;
}""")
if not mp_found:
print(" [小程序] 未找到「小程序」选项", flush=True)
return False
await asyncio.sleep(1.5)
await page.evaluate(f"""(appid) => {{
const inputs = document.querySelectorAll('input[type="text"]');
for (const inp of inputs) {{
if (inp.placeholder && (inp.placeholder.includes('AppID') || inp.placeholder.includes('appid')
|| inp.placeholder.includes('小程序'))) {{
inp.value = appid;
inp.dispatchEvent(new Event('input', {{bubbles:true}}));
return;
}}
}}
}}""", MINIPROGRAM_APPID)
await asyncio.sleep(0.5)
print(" [小程序] 已尝试填入 AppID", flush=True)
return True
except Exception as e:
print(f" [小程序] 异常: {str(e)[:60]}", flush=True)
return False
# ---------------------------------------------------------------------------
# Core publish
# ---------------------------------------------------------------------------
async def publish_one(
video_path: str,
title: str,
idx: int = 1,
total: int = 1,
skip_dedup: bool = False,
scheduled_time=None,
) -> PublishResult:
from playwright.async_api import async_playwright
from publish_result import is_published
@@ -62,17 +204,26 @@ async def publish_one(video_path: str, title: str, idx: int = 1, total: int = 1,
fsize = Path(video_path).stat().st_size
t0 = time.time()
time_hint = f" → 定时 {scheduled_time.strftime('%H:%M')}" if scheduled_time else ""
print(f"\n[{idx}/{total}] {fname} ({fsize/1024/1024:.1f}MB){time_hint}", flush=True)
print(f"\n[{idx}/{total}] {fname} ({fsize / 1024 / 1024:.1f}MB){time_hint}", flush=True)
print(f" 标题: {title[:60]}", flush=True)
if not skip_dedup and is_published("视频号", video_path):
print(f" [跳过] 该视频已发布到视频号", flush=True)
return PublishResult(platform="视频号", video_path=video_path, title=title,
success=True, status="skipped", message="去重跳过(已发布)")
print(" [跳过] 该视频已发布到视频号", flush=True)
return PublishResult(
platform="视频号", video_path=video_path, title=title,
success=True, status="skipped", message="去重跳过(已发布)",
)
if not COOKIE_FILE.exists():
return PublishResult(platform="视频号", video_path=video_path, title=title,
success=False, status="error", message="Cookie 不存在")
return PublishResult(
platform="视频号", video_path=video_path, title=title,
success=False, status="error", message="Cookie 不存在",
)
capture = ApiCapture()
ss_dir = Path("/tmp/channels_ss")
ss_dir.mkdir(exist_ok=True)
ss = lambda n: str(ss_dir / f"{Path(video_path).stem}_{n}.png")
try:
async with async_playwright() as pw:
@@ -81,125 +232,195 @@ async def publish_one(video_path: str, title: str, idx: int = 1, total: int = 1,
args=["--disable-blink-features=AutomationControlled"],
)
ctx = await browser.new_context(
storage_state=str(COOKIE_FILE), user_agent=UA,
viewport={"width": 1280, "height": 900}, locale="zh-CN",
storage_state=str(COOKIE_FILE),
user_agent=UA,
viewport={"width": 1280, "height": 900},
locale="zh-CN",
)
await ctx.add_init_script(
"Object.defineProperty(navigator,'webdriver',{get:()=>undefined})"
)
page = await ctx.new_page()
page.on("response", capture.handle)
# --- Step 1: open publish page ---
print(" [1] 打开发表页...", flush=True)
await page.goto(
"https://channels.weixin.qq.com/platform/post/create",
timeout=30000, wait_until="domcontentloaded",
timeout=30000,
wait_until="domcontentloaded",
)
await asyncio.sleep(5)
body_text = await page.evaluate("document.body.innerText")
if "扫码" in body_text or "login" in page.url.lower():
await page.screenshot(path=ss("login"))
await browser.close()
return PublishResult(
platform="视频号", video_path=video_path, title=title,
success=False, status="error",
message="Cookie 已过期(需重新扫码登录)",
screenshot=ss("login"),
)
await page.screenshot(path=ss("1_page"))
# --- Step 2: upload video ---
print(" [2] 上传视频...", flush=True)
fl = page.locator('input[type="file"][accept*="video"]').first
if await fl.count() == 0:
fl = page.locator('input[type="file"]').first
await fl.set_input_files(video_path)
print(" [2] 文件已选择", flush=True)
# 等视频处理完(封面预览出现)
for i in range(60):
has_cover = await page.locator('text=封面预览').count() > 0
has_delete = await page.locator('text=删除').count() > 0
upload_ok = False
for i in range(90):
has_cover = await page.locator("text=封面预览").count() > 0
has_delete = await page.locator("text=删除").count() > 0
if has_cover or has_delete:
print(f" [2] 上传完成 ({i*2}s)", flush=True)
print(f" [2] 上传完成 ({i * 2}s)", flush=True)
upload_ok = True
break
await asyncio.sleep(2)
await asyncio.sleep(2)
if not upload_ok:
await page.screenshot(path=ss("upload_timeout"))
await browser.close()
return PublishResult(
platform="视频号", video_path=video_path, title=title,
success=False, status="error",
message="视频上传超时 (3 min)",
screenshot=ss("upload_timeout"),
)
await asyncio.sleep(3)
await page.screenshot(path=ss("2_uploaded"))
# --- Step 3: fill description ---
print(" [3] 填写描述...", flush=True)
desc_filled = False
# 尝试点击"添加描述"占位符区域
add_desc = page.locator('text=添加描述').first
add_desc = page.locator("text=添加描述").first
if await add_desc.count() > 0:
await add_desc.click()
await asyncio.sleep(0.5)
active = page.locator('[contenteditable="true"]:visible').first
if await active.count() > 0:
await active.fill(title)
await active.fill("")
await active.type(title, delay=15)
desc_filled = True
else:
await page.keyboard.type(title, delay=20)
await page.keyboard.type(title, delay=15)
desc_filled = True
if not desc_filled:
# JS 兜底
await page.evaluate("""(title) => {
await page.evaluate(
"""(title) => {
const els = document.querySelectorAll('[contenteditable="true"]');
for (const el of els) {
if (el.offsetParent !== null && el.closest('[class*="desc"]')) {
if (el.offsetParent !== null) {
el.focus();
el.textContent = title;
el.dispatchEvent(new Event('input', {bubbles:true}));
return;
}
}
// fallback: 可见的 textarea
const ta = [...document.querySelectorAll('textarea')].find(
t => t.offsetParent !== null && t.placeholder.includes('描述')
);
if (ta) { ta.value = title; ta.dispatchEvent(new Event('input', {bubbles:true})); }
}""", title)
}""",
title,
)
await asyncio.sleep(0.5)
# 定时发布
if scheduled_time:
from schedule_helper import set_scheduled_time
scheduled_ok = await set_scheduled_time(page, scheduled_time, "视频号")
if scheduled_ok:
print(f" [定时] 视频号定时发布已设置", flush=True)
# --- Step 3b: mini-program ---
print(" [3b] 尝试挂载小程序...", flush=True)
await _try_add_miniprogram(page)
# 滚动到底部找发表按钮
# --- Step 4: publish ---
await page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
await asyncio.sleep(1)
print(" [4] 发表...", flush=True)
pub = page.locator('button:has-text("发表")').first
if await pub.count() > 0:
await pub.click()
print(" [4] 点击发表...", flush=True)
pub_btn = page.locator('button:has-text("发表")').first
if await pub_btn.count() > 0:
await pub_btn.click()
else:
await page.evaluate("""() => {
await page.evaluate(
"""() => {
const b = [...document.querySelectorAll('button')];
const p = b.find(e => e.textContent.includes('发表'));
if (p) p.click();
}""")
}"""
)
await asyncio.sleep(3)
# Wait for possible dialog
for _ in range(10):
await asyncio.sleep(1)
dp = page.locator('button:has-text("直接发表")').first
if await dp.count() > 0:
print(" [4b] 原创弹窗 → 直接发表", flush=True)
try:
await dp.click(force=True, timeout=3000)
except Exception:
await page.evaluate(
"""() => {
const btns = [...document.querySelectorAll('button')];
const b = btns.find(e => e.textContent.includes('直接发表'));
if (b) b.click();
}"""
)
break
# 处理"声明原创"弹窗 → 选"直接发表"
direct_pub = page.locator('button:has-text("直接发表")').first
if await direct_pub.count() > 0:
print(" [4b] 原创弹窗 → 直接发表", flush=True)
try:
await direct_pub.click(force=True, timeout=5000)
except Exception:
await page.evaluate("""() => {
const btns = [...document.querySelectorAll('button')];
const b = btns.find(e => e.textContent.includes('直接发表'));
if (b) b.click();
}""")
await asyncio.sleep(5)
# Wait for publish to process
await asyncio.sleep(8)
await page.screenshot(path=ss("4_after_publish"))
# --- Step 5: analyse API responses ---
print(f" [API] 捕获 {len(capture.all_calls)} 个调用", flush=True)
api_ok: bool | None = None
api_msg = ""
for call in capture.publish_responses:
body = call.get("body", {})
code = body.get("errCode", body.get("errcode", body.get("ret", -999)))
print(f" PUBLISH → status={call['status']} errCode={code}", flush=True)
if code == 0 or (isinstance(code, int) and code == 200):
api_ok = True
else:
api_ok = False
api_msg = json.dumps(body, ensure_ascii=False)[:120]
if api_ok is None:
url_now = page.url
text_now = await page.evaluate("document.body.innerText")
if "/post/list" in url_now or "内容管理" in text_now:
api_ok = True
api_msg = "页面已跳转到内容管理"
else:
api_msg = f"未捕获publish响应 (url={url_now[:60]})"
# --- Step 6: verify on content list ---
print(" [5] 列表验证...", flush=True)
kw_for_check = title.split("#")[0].strip()[:20]
verified, verify_msg = await _verify_on_list(page, kw_for_check)
await page.screenshot(path=ss("5_verify"))
await page.screenshot(path="/tmp/channels_result.png")
txt = await page.evaluate("document.body.innerText")
url = page.url
elapsed = time.time() - t0
if "发表成功" in txt or "已发表" in txt or "成功" in txt:
status, msg = "published", "发表成功"
elif "/platform/post/list" in url or ("内容管理" in txt and "视频" in txt):
status, msg = "reviewing", "已跳转到内容管理(发表成功)"
if api_ok and verified:
success, status, msg = True, "published", f"✓ API+列表双重确认 ({verify_msg})"
elif api_ok:
success, status, msg = True, "reviewing", f"API确认列表未匹配 ({verify_msg})"
elif verified:
success, status, msg = True, "reviewing", f"列表匹配 ({verify_msg})"
else:
status, msg = "reviewing", "已提交,请确认截图"
success, status = False, "error"
msg = f"发布失败 — API: {api_msg}; 列表: {verify_msg}"
result = PublishResult(
platform="视频号", video_path=video_path, title=title,
success=True, status=status, message=msg,
screenshot="/tmp/channels_result.png", elapsed_sec=elapsed,
platform="视频号",
video_path=video_path,
title=title,
success=success,
status=status,
message=msg,
screenshot=ss("5_verify"),
elapsed_sec=elapsed,
)
print(f" {result.log_line()}", flush=True)
await ctx.storage_state(path=str(COOKIE_FILE))
@@ -207,16 +428,27 @@ async def publish_one(video_path: str, title: str, idx: int = 1, total: int = 1,
return result
except Exception as e:
return PublishResult(platform="视频号", video_path=video_path, title=title,
success=False, status="error",
message=f"异常: {str(e)[:80]}", elapsed_sec=time.time()-t0)
import traceback
traceback.print_exc()
return PublishResult(
platform="视频号",
video_path=video_path,
title=title,
success=False,
status="error",
message=f"异常: {str(e)[:80]}",
elapsed_sec=time.time() - t0,
)
# ---------------------------------------------------------------------------
# main
# ---------------------------------------------------------------------------
async def main():
from publish_result import print_summary, save_results
if not COOKIE_FILE.exists():
print("[✗] Cookie 不存在")
print("[✗] Cookie 不存在,请先运行 channels_login.py 扫码")
return 1
videos = sorted(VIDEO_DIR.glob("*.mp4"))
@@ -231,7 +463,7 @@ async def main():
r = await publish_one(str(vp), t, i + 1, len(videos))
results.append(r)
if i < len(videos) - 1:
await asyncio.sleep(5)
await asyncio.sleep(8)
actual = [r for r in results if r.status != "skipped"]
print_summary(actual)

View File

@@ -265,3 +265,4 @@
| 2026-03-10 15:02:53 | 🔄 卡若AI 同步 2026-03-10 15:02 | 更新:卡木、运营中枢工作台 | 排除 >20MB: 11 个 |
| 2026-03-10 15:16:56 | 🔄 卡若AI 同步 2026-03-10 15:16 | 更新:水溪整理归档、卡木、运营中枢工作台 | 排除 >20MB: 11 个 |
| 2026-03-10 15:17:46 | 🔄 卡若AI 同步 2026-03-10 15:17 | 更新:卡木、运营中枢工作台 | 排除 >20MB: 11 个 |
| 2026-03-10 15:29:57 | 🔄 卡若AI 同步 2026-03-10 15:29 | 更新:卡木、运营中枢工作台 | 排除 >20MB: 11 个 |

View File

@@ -268,3 +268,4 @@
| 2026-03-10 15:02:53 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-10 15:02 | 更新:卡木、运营中枢工作台 | 排除 >20MB: 11 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
| 2026-03-10 15:16:56 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-10 15:16 | 更新:水溪整理归档、卡木、运营中枢工作台 | 排除 >20MB: 11 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
| 2026-03-10 15:17:46 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-10 15:17 | 更新:卡木、运营中枢工作台 | 排除 >20MB: 11 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
| 2026-03-10 15:29:57 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-10 15:29 | 更新:卡木、运营中枢工作台 | 排除 >20MB: 11 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |