From b0070d5e52a0c03e64a112087a02384f8023bc53 Mon Sep 17 00:00:00 2001 From: karuo Date: Tue, 10 Mar 2026 15:47:45 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=84=20=E5=8D=A1=E8=8B=A5AI=20=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=202026-03-10=2015:47=20|=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=EF=BC=9A=E5=8D=A1=E6=9C=A8=E3=80=81=E8=BF=90=E8=90=A5=E4=B8=AD?= =?UTF-8?q?=E6=9E=A2=E5=B7=A5=E4=BD=9C=E5=8F=B0=20|=20=E6=8E=92=E9=99=A4?= =?UTF-8?q?=20>20MB:=2011=20=E4=B8=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../木叶_视频内容/多平台分发/脚本/publish_log.json | 12 - .../视频号发布/脚本/channels_publish.py | 380 ++++++++++++++---- 运营中枢/工作台/gitea_push_log.md | 1 + 运营中枢/工作台/代码管理.md | 1 + 4 files changed, 308 insertions(+), 86 deletions(-) diff --git a/03_卡木(木)/木叶_视频内容/多平台分发/脚本/publish_log.json b/03_卡木(木)/木叶_视频内容/多平台分发/脚本/publish_log.json index a9e65f87..8821e9a6 100644 --- a/03_卡木(木)/木叶_视频内容/多平台分发/脚本/publish_log.json +++ b/03_卡木(木)/木叶_视频内容/多平台分发/脚本/publish_log.json @@ -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 年轻人测MBTI,40到60岁走五行八卦.mp4", "title": "20岁测MBTI,40岁该学五行八卦了 #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 年轻人测MBTI,40到60岁走五行八卦.mp4", "title": "20岁测MBTI,40岁该学五行八卦了 #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"} diff --git a/03_卡木(木)/木叶_视频内容/视频号发布/脚本/channels_publish.py b/03_卡木(木)/木叶_视频内容/视频号发布/脚本/channels_publish.py index 1b3e6900..255e2eb4 100644 --- a/03_卡木(木)/木叶_视频内容/视频号发布/脚本/channels_publish.py +++ b/03_卡木(木)/木叶_视频内容/视频号发布/脚本/channels_publish.py @@ -1,11 +1,14 @@ #!/usr/bin/env python3 """ -视频号发布 - Headless Playwright -上传 → 填描述 → 发表。视频号无公开API,Playwright为唯一方案。 +视频号发布 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) diff --git a/运营中枢/工作台/gitea_push_log.md b/运营中枢/工作台/gitea_push_log.md index 6fc39f45..e4e1d075 100644 --- a/运营中枢/工作台/gitea_push_log.md +++ b/运营中枢/工作台/gitea_push_log.md @@ -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 个 | diff --git a/运营中枢/工作台/代码管理.md b/运营中枢/工作台/代码管理.md index 3abadb69..de0891ec 100644 --- a/运营中枢/工作台/代码管理.md +++ b/运营中枢/工作台/代码管理.md @@ -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) |