🔄 卡若AI 同步 2026-03-23 13:35 | 更新:水桥平台对接、卡木、运营中枢工作台 | 排除 >20MB: 11 个

This commit is contained in:
2026-03-23 13:35:59 +08:00
parent 0ef2212160
commit cd15f98449
27 changed files with 1137 additions and 421 deletions

View File

@@ -1,6 +1,6 @@
{
"access_token": "u-fNvjYaC8h9SWRJjTGmOKuulh3c3xghUjWMGaJA4023lE",
"refresh_token": "ur-eSq5MlNZ5d9GkVd9PbvCDGlh1A3xghUjrwGaUQ4027hE",
"access_token": "u-ddWeP4OxxfuqcXBDh7p2jTlh3eb1ghorOMGaUBk023hI",
"refresh_token": "ur-eMcHDsoKl9gEs0DER_14ailh1CxxghohpMGaIRk022lI",
"name": "飞书用户",
"auth_time": "2026-03-23T08:28:12.167751"
}

View File

@@ -73,11 +73,13 @@ ROWS = {
'129': [ 'AI手机金融坏账投流', 200, 0, 250, 14, 187, 4, 561, 21, 31 ],
# 130场 2026-03-21视频号直播结束页 02:25:49≈146min观众总数2278、最高在线355、新增关注4、总热度3、送礼1Soul推流无截图数据填0→脚本跳过第5行保留空
'130': [ 'Soul爆量脸视频号问微信', 146, 0, 2278, 0, 3, 1, 3, 4, 355 ],
# 131场 2026-03-23结束页 02:05:55≈126min观众总数1144、最高在线75、新增关注4点赞1595+评论498+分享12=2105礼物/灵魂力/人均未给填0Soul推流无填0→跳过第5行
'131': [ '视频号中枢Soul哨兵', 126, 0, 1144, 0, 2105, 0, 0, 4, 75 ],
}
# 场次→按日期列填写时的日期(表头为当月日期 1~31
SESSION_DATE_COLUMN = {'105': '20', '106': '21', '107': '23', '113': '2', '114': '3', '115': '4', '116': '5', '117': '6', '118': '7', '119': '8', '124': '14', '126': '17', '127': '18', '128': '19', '129': '20', '130': '21'}
SESSION_DATE_COLUMN = {'105': '20', '106': '21', '107': '23', '113': '2', '114': '3', '115': '4', '116': '5', '117': '6', '118': '7', '119': '8', '124': '14', '126': '17', '127': '18', '128': '19', '129': '20', '130': '21', '131': '23'}
# 场次→月份(用于选择 2月/3月 等工作表标签,避免写入错月)
SESSION_MONTH = {'105': 2, '106': 2, '107': 2, '113': 3, '114': 3, '115': 3, '116': 3, '117': 3, '118': 3, '119': 3, '124': 3, '126': 3, '127': 3, '128': 3, '129': 3, '130': 3}
SESSION_MONTH = {'105': 2, '106': 2, '107': 2, '113': 3, '114': 3, '115': 3, '116': 3, '117': 3, '118': 3, '119': 3, '124': 3, '126': 3, '127': 3, '128': 3, '129': 3, '130': 3, '131': 3}
# 派对录屏(飞书妙记)链接:场次 → 完整 URL填表时写入「派对录屏」行对应列
# 从飞书妙记复制链接后填入,新场次需补全
@@ -133,6 +135,7 @@ MINIPROGRAM_EXTRA_3 = {
'19': {'访问次数': 0, '访客': 0, '交易金额': 0}, # 3月19日 128场
'20': {'访问次数': 0, '访客': 0, '交易金额': 0}, # 3月20日 129场
'21': {'访问次数': 0, '访客': 0, '交易金额': 0}, # 3月21日 130场从小程序后台更新
'23': {'访问次数': 0, '访客': 0, '交易金额': 0}, # 3月23日 131场从小程序后台更新
}
@@ -414,7 +417,7 @@ def main():
session = (sys.argv[1] if len(sys.argv) > 1 else '104').strip()
row = ROWS.get(session)
if not row:
print('❌ 未知场次,可用: 96, 97, 98, 99, 100, 103, 104, 105, 106, 107, 113, 114, 115, 116, 117, 118, 119, 124, 126, 127, 128, 129, 130')
print('❌ 未知场次,可用: 96, 97, 98, 99, 100, 103, 104, 105, 106, 107, 113, 114, 115, 116, 117, 118, 119, 124, 126, 127, 128, 129, 130, 131')
sys.exit(1)
token = load_token() or refresh_and_load_token()
if not token:
@@ -471,9 +474,9 @@ def main():
if os.environ.get('SOUL_PARTY_PUSH_GROUP', '').strip() != '1':
print('⏭️ 已跳过飞书群推送(需 export SOUL_PARTY_PUSH_GROUP=1 后重跑本脚本才会推送运营数据到群)')
return
if sess not in ('105', '106', '107', '113', '114', '115', '116', '117', '118', '119', '124', '126', '127', '128', '129', '130'):
if sess not in ('105', '106', '107', '113', '114', '115', '116', '117', '118', '119', '124', '126', '127', '128', '129', '130', '131'):
return
date_label = {'105': '2月20日', '106': '2月21日', '107': '2月23日', '113': '3月2日', '114': '3月3日', '115': '3月4日', '116': '3月5日', '117': '3月6日', '118': '3月7日', '119': '3月8日', '124': '3月14日', '126': '3月17日', '127': '3月18日', '128': '3月19日', '129': '3月20日', '130': '3月21日'}.get(sess, sess + '')
date_label = {'105': '2月20日', '106': '2月21日', '107': '2月23日', '113': '3月2日', '114': '3月3日', '115': '3月4日', '116': '3月5日', '117': '3月6日', '118': '3月7日', '119': '3月8日', '124': '3月14日', '126': '3月17日', '127': '3月18日', '128': '3月19日', '129': '3月20日', '130': '3月21日', '131': '3月23日'}.get(sess, sess + '')
report_link = OPERATION_REPORT_LINK if sheet_id == SHEET_ID else f'https://cunkebao.feishu.cn/wiki/wikcnIgAGSNHo0t36idHJ668Gfd?sheet={sheet_id}'
lines = [
'【Soul 派对运营报表】',
@@ -484,7 +487,7 @@ def main():
for i, label in enumerate(LABELS_GROUP):
val = raw_vals[i] if i < len(raw_vals) else ''
lines.append(f'{label}{val}')
src_date = {'105': '20260220', '106': '20260221', '107': '20260223', '113': '20260302', '114': '20260303', '115': '20260304', '116': '20260305', '117': '20260306', '118': '20260307', '119': '20260308', '124': '20260314', '126': '20260317', '127': '20260318', '128': '20260319', '129': '20260320', '130': '20260321'}.get(sess, '20260220')
src_date = {'105': '20260220', '106': '20260221', '107': '20260223', '113': '20260302', '114': '20260303', '115': '20260304', '116': '20260305', '117': '20260306', '118': '20260307', '119': '20260308', '124': '20260314', '126': '20260317', '127': '20260318', '128': '20260319', '129': '20260320', '130': '20260321', '131': '20260323'}.get(sess, '20260220')
lines.append(f'数据来源soul 派对 {sess}{src_date}.txt')
msg = '\n'.join(lines)
ok, _ = send_feishu_group_message(FEISHU_GROUP_WEBHOOK, msg)

View File

@@ -6,6 +6,7 @@ B站视频发布 — 纯 API 优先 + Playwright 兜底
"""
import asyncio
import json
import os
import sys
import time
from pathlib import Path
@@ -22,6 +23,16 @@ UA = (
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36"
)
def _playwright_headless() -> bool:
"""默认 True无窗口PUBLISH_PLAYWRIGHT_HEADLESS=0 时有头调试。"""
return os.environ.get("PUBLISH_PLAYWRIGHT_HEADLESS", "1").strip().lower() not in (
"0",
"false",
"no",
"off",
)
TITLES = {
"AI最大的缺点是上下文太短这样来解决.mp4":
"AI的短板是记忆太短上下文一长就废了这个方法能解决 #AI工具 #效率提升 #小程序 卡若创业派对",
@@ -84,8 +95,16 @@ async def _api_publish(video_path: str, title: str, scheduled_time=None) -> Publ
if scheduled_time:
dtime = int(scheduled_time.timestamp())
now_ts = int(time.time())
# B站 API发布时间须 ≥5 分钟且 ≤15 天21173
min_d = now_ts + 330
if dtime < min_d:
dtime = min_d
meta["dtime"] = dtime
print(f" [API] 定时发布: {scheduled_time.strftime('%Y-%m-%d %H:%M')}", flush=True)
print(
f" [API] 定时发布: {time.strftime('%Y-%m-%d %H:%M', time.localtime(dtime))}",
flush=True,
)
page = video_uploader.VideoUploaderPage(
path=video_path,
@@ -131,14 +150,15 @@ async def _api_publish(video_path: str, title: str, scheduled_time=None) -> Publ
async def _playwright_publish(video_path: str, title: str) -> PublishResult:
"""方案二Playwright 可见浏览器(兜底)"""
"""方案二Playwright 兜底(默认 headless无弹窗"""
from playwright.async_api import async_playwright
t0 = time.time()
hl = _playwright_headless()
async with async_playwright() as pw:
browser = await pw.chromium.launch(
headless=False,
headless=hl,
args=["--disable-blink-features=AutomationControlled"],
)
ctx = await browser.new_context(
@@ -281,7 +301,10 @@ async def publish_one(video_path: str, title: str, idx: int = 1, total: int = 1,
print(f" [方案一失败] {err_msg}", flush=True)
# 方案二Playwright 兜底
print(" [方案二] 降级到 Playwright 可见浏览器...", flush=True)
print(
f" [方案二] 降级 Playwright{'headless 无窗口' if _playwright_headless() else '有头浏览器'}...",
flush=True,
)
try:
result = await _playwright_publish(video_path, title)
print(f" {result.log_line()}", flush=True)

File diff suppressed because one or more lines are too long

View File

@@ -3,25 +3,28 @@ name: 多平台分发
description: >
一键将视频分发到 5 个平台抖音、B站、视频号、小红书、快手
API 优先策略:视频号纯 API、B站 bilibili-api-python、抖音纯 API。
支持定时排期(第1条立即发后续 30-120 分钟随机间隔)、并行分发、去重、失败自动重试。
支持定时排期(默认智能错峰;可选 legacy、默认静默不自动弹窗登录视频号、并行分发、去重、失败自动重试。
triggers: 多平台分发、一键分发、全平台发布、批量分发、视频分发
owner: 木叶
group: 木
version: "4.1"
updated: "2026-03-20"
version: "4.5"
updated: "2026-03-23"
---
# 多平台分发 Skillv4.1
# 多平台分发 Skillv4.5
> **核心原则**API 发布为主Playwright 为辅。确保确定性地分发到各平台。
> **v4.1 变更**:视频号 Cookie 双路径自动对齐;指定 `--platforms 视频号` 时 Cookie 失效自动调起扫码登录;登录完成即写入并同步中央存储;执行上**先直达目的**(跑命令、保存 Cookie、再发**不对用户反问**。
> **v4.5 视频号铁律****必须先等微信扫码完成Cookie 连续校验通过),再上传。** `video_channels_resume.py` **默认弹出 Chromium 窗口**扫码,扫完再自动上传;`--silent-login` 才是无头。
> **v4.4**默认无窗口、B 站 headless、定时 ≥5min、`--until-success`、二维码路径标记。
> **v4.3**:默认不自动弹视频号登录。**v4.2**:智能排期与去重下标对齐。
## 〇、执行原则(第一性原理)
- **目标优先**:用户要「发到视频号 / 全平台」→ 直接执行 `distribute_all.py` 与必要登录脚本,再简短汇报结果
- **视频号两步(强制)**:① 微信扫码登录助手并落盘 Cookie② 再跑上传。**禁止**在 Cookie 未确认有效时上传。一键:`video_channels_resume.py`**默认弹窗** Chromium`--silent-login` 无头;`--step1-only` 只登录)
- **目标优先**:全平台时若含视频号且 Cookie 无效 → 先完成第①步,再 `distribute_all`;或直接跑 `video_channels_resume.py` 仅发视频号。
- **Cookie 优先**:任何登录成功 → **必须落盘**;视频号同时写入 `视频号发布/脚本/channels_storage_state.json``多平台分发/cookies/视频号_cookies.json`(脚本已自动同步)。
- **少问多做**:缺 Cookie 时自动打开 `channels_login.py`(仅发视频号场景);除非环境无法弹窗,否则不先停下来问「要不要登录」
- **扫码只在 Cursor 里**`channels_login.py``cursor://…/simple-browser` 打开登录页,**不**唤起 Safari/Chrome要**完全避免**回退 Chromium请用带 `--remote-debugging-port=9223` 的方式启动 Cursor见脚本内说明
- **默认静默**:批量分发**不弹窗**、不自动拉起登录流程,适合 Cursor 内无人值守跑命令
- **需要扫码时**:显式 `python3 channels_login.py`,或 `distribute_all.py --auto-channels-login`,或 `CHANNELS_AUTO_LOGIN=1 python3 channels_api_publish.py``channels_login.py` `cursor://…/simple-browser`;完全避免 Chromium 回退见脚本内 CDP 说明。
---
@@ -39,6 +42,9 @@ updated: "2026-03-20"
> 按《视频号与腾讯相关 API 整理》结论,微信官方目前**没有开放「短视频上传/发布」接口**;本 Skill 中的视频号发布能力,属于对 `https://channels.weixin.qq.com` 视频号助手网页协议的逆向封装DFS 上传 + `post_create`),仅在你本机使用,需自行承担协议变更与合规风险。
> 官方可控能力(直播记录、橱窗、留资、罗盘数据、本地生活等)的服务端 API 入口为:`https://developers.weixin.qq.com/doc/channels/api/`。**整合脑图与接口速查**见同木叶的 `视频号发布/REFERENCE_开放能力_数据与集成.md`;开放平台凭证约定见 `视频号发布/credentials/README.md``.env.open_platform`)。
> **「视频号 API token」与成片上传**:微信公众号 **`access_token`**`cgi-bin/token`)用于开放平台文档中的各类接口,**不能**替代本链路里的 **视频号助手网页态**。`distribute_all` → `channels_api_publish.py` 发表短视频,依赖的是 **`channels_storage_state.json`**Cookie + `localStorage`,如 `__ml::aid`、`_finger_print_device_id`),经 `auth/auth_data` 校验通过后方可 DFS 上传与 `post_create`。`channels_token.json` 只是登录脚本写出的摘要字段,**不能单独当「发视频 token」用**。若要用 **appid+secret** 拉直播/罗盘等数据,走 `视频号发布/脚本/channels_open_fetch.py`**与上传 127 场成片无关**。
> **127 场全平台(静默)**(助手态与各平台 Cookie 已就绪):`python3 distribute_all.py --video-dir "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片"`
---
## 二、一键命令
@@ -46,10 +52,13 @@ updated: "2026-03-20"
```bash
cd /Users/karuo/Documents/个人/卡若AI/03_卡木/木叶_视频内容/多平台分发/脚本
# 定时排期:第1条立即后续 30-120min 随机间隔
# 定时排期:默认智能错峰(条数自适应 + 尽量避开本地 07 点)
python3 distribute_all.py
# 立即全部发布
# 旧版:固定 30120min 随机间隔
python3 distribute_all.py --legacy-schedule
# 立即全部发布(不排期)
python3 distribute_all.py --now
# 只发指定平台
@@ -62,22 +71,42 @@ python3 distribute_all.py --video-dir "/path/to/videos/"
python3 distribute_all.py --check
python3 distribute_all.py --retry
# 视频号Cookie 失效时禁止自动弹窗登录CI/无头环境
python3 distribute_all.py --platforms 视频号 --no-auto-channels-login --video-dir "/path/to/成片"
# 或环境变量NO_AUTO_CHANNELS_LOGIN=1
# 视频号 Cookie 失效且希望脚本自动弹窗扫码(默认不弹窗
python3 distribute_all.py --platforms 视频号 --auto-channels-login --video-dir "/path/to/成片"
# 强制静默(即使写了 --auto-channels-login 也不弹窗NO_AUTO_CHANNELS_LOGIN=1
# 独立跑视频号脚本且允许自动登录CHANNELS_AUTO_LOGIN=1 python3 ../视频号发布/脚本/channels_api_publish.py
# 视频号仅要二维码到对话:无窗口,终端会打印 SOUL_QR_IMAGE_FOR_CHAT 路径
cd ../视频号发布/脚本 && CHANNELS_SILENT_QR=1 python3 channels_login.py
# 全平台直到全部成功(间隔 90s无限轮加 --until-success-max-rounds 20 可封顶)
python3 distribute_all.py --video-dir "/path/to/成片" --until-success
# 仅查看断点:各平台已成功/待传(不执行上传);加 --resume-report-detail 列出文件名
python3 distribute_all.py --resume-report --resume-report-detail --video-dir "/path/to/成片"
# 视频号一条龙:后台静默登录 → 终端打印二维码路径 → 你在对话里让助手读图扫码 → Cookie 好后自动仅发视频号(断点+until-success
python3 video_channels_resume.py --video-dir "/path/to/成片"
# 无头(不弹窗):加 --silent-login
# 只做扫码不上传:加 --step1-only
```
---
## 三、定时排期v4.0 优化
## 三、定时排期v4.2 默认智能错峰
### 3.1 排期规则
- **第 1 条**:立即发布`first_delay=0`
- **第 2 条起**:前一条 + random(30, 120) 分钟
- 若总跨度 > 24h自动按比例压缩
- 12 条视频典型跨度 ~10-14h
### 3.1 默认规则(`schedule_generator.generate_smart_schedule`
- **第 1 条**:立即(`first_delay=0`;视频号侧 2 分钟内仍视为立即(`_scheduled_ts_for_channels`
- **间隔与总跨度**:随条数 `n` 自适应(`suggest_stagger_params`):条数越多略缩短单条间隔、允许更长总跨度(如 8 条约 2848h 量级,具体随机)
- **凌晨规避**:本地时间 07 点附近的点会挪到当日/次日 12:xx`refine_avoid_late_night`);关闭:`SCHEDULE_NO_NIGHT_REFINE=1`
- **回退旧逻辑**`python3 distribute_all.py --legacy-schedule`(仍可用 `--min-gap` / `--max-gap` / `--max-hours`
- **去重对齐**:排期与 `videos` 列表下标一致;中间已发布的文件跳过,其余仍按原文件名顺序对应原定时间
### 3.2 各平台定时实现
### 3.2 独立跑 `channels_api_publish.py` 时
- 与上相同的 `generate_smart_schedule` → Unix 定时,与分发器一致
### 3.3 各平台定时实现
| 平台 | 定时方式 | 参数 |
|------|----------|------|
@@ -134,8 +163,9 @@ meta.hashtags("视频号") # … + #小程序卡若创业派对 #公众号卡
- 中央存储:`多平台分发/cookies/{平台}_cookies.json`
- **视频号双路径**`sync_channels_cookie_files()`;登录脚本写 legacy 后 **copy**`cookies/视频号_cookies.json`
- **登录页只在 Cursor 内打开**`channels_login.py` v7 用 `cursor://vscode.simple-browser/show?url=…` 唤起 **Simple Browser****不**用系统默认浏览器。
- **不落盘会话**:在 Cursor 已开 `--remote-debugging-port`(默认脚本连 `CHANNELS_CDP_URL=http://127.0.0.1:9223`Playwright **CDP 附着** Cursor从 Simple Browser 上下文导出 `storage_state`**无 CDP** 时回退独立 Chromium`--playwright-only` 可强制只走 Chromium
- `distribute_all.py` 指定 `--platforms 视频号` 且 Cookie 失效时自动跑 `channels_login.py`(可用 `--no-auto-channels-login` 关闭)
- **不落盘会话**:在 Cursor 已开 `--remote-debugging-port`(默认脚本连 `CHANNELS_CDP_URL=http://127.0.0.1:9223`Playwright **CDP 附着** Cursor从 Simple Browser 上下文导出 `storage_state`**无 CDP** 时回退 Chromium;默认 **持久化用户目录** `~/.soul-channels-playwright-profile``CHANNELS_PERSISTENT_LOGIN=0` / `--no-persistent` 可关),减少重复扫码
- **无 Cookie 发片**:微信助手 API **无**官方「access_token 上传短视频」接口,见 `视频号发布/脚本/channels_open_platform_publish.py``REFERENCE_开放能力_数据与集成.md`
- **`distribute_all.py`**:默认**不**自动跑 `channels_login.py`;仅 `--auto-channels-login` 且 Cookie 无效时才调起。历史参数 `--no-auto-channels-login` 仍接受(无效果,与默认一致)。
- API 预检:各平台 auth API
---

File diff suppressed because one or more lines are too long

View File

@@ -1,24 +1,24 @@
#!/usr/bin/env python3
"""
多平台一键分发 v3 — 全链路自动化 + 定时排期
- 定时排期30-120 分钟随机间隔,超 24h 自动压缩
- 定时排期默认generate_smart_schedule — 按条数自适应间隔/总跨度,并尽量避开本地 07 点
- 旧排期:--legacy-schedule + --min-gap / --max-gap / --max-hours原 30120min 随机)
- 并行分发5 平台同时上传asyncio.gather
- 去重机制:已成功发布的视频自动跳过
- 失败重试:--retry 自动重跑历史失败任务
- Cookie 预警:过期/即将过期自动通知
- 智能标题:优先手动字典,否则文件名自动生成
- 结果持久化JSON Lines 日志 + 控制台汇总
- 去重:每条视频按其在目录中的序号对齐排期(不因前面跳过而错位)
- 失败重试:--retryCookie 预警;结果写入 publish_log.json
- 视频号登录:默认静默(仅同步 Cookie 路径,不弹窗);需要自动扫码时加 --auto-channels-loginNO_AUTO_CHANNELS_LOGIN=1 强制静默
用法:
python3 distribute_all.py # 定时排期并行分发
python3 distribute_all.py # 智能错峰定时排期
python3 distribute_all.py --now # 立即发布(不排期)
python3 distribute_all.py --legacy-schedule # 固定随机间隔(旧逻辑)
python3 distribute_all.py --platforms B站 快手 # 只发指定平台
python3 distribute_all.py --check # 检查 Cookie
python3 distribute_all.py --retry # 重试失败任务
python3 distribute_all.py --video /path/to.mp4 # 发单条视频
python3 distribute_all.py --no-dedup # 跳过去重检查
python3 distribute_all.py --serial # 串行模式(调试用)
python3 distribute_all.py --min-gap 30 --max-gap 120 # 自定义间隔
python3 distribute_all.py --min-gap 30 --max-gap 120 # 仅与 --legacy-schedule 联用
"""
import argparse
import asyncio
@@ -44,18 +44,23 @@ from cookie_manager import (
from publish_result import (PublishResult, print_summary, save_results,
load_published_set, load_failed_tasks)
from title_generator import generate_title
from schedule_generator import generate_schedule, format_schedule
from schedule_generator import (
generate_schedule,
generate_smart_schedule,
format_schedule,
)
from video_metadata import VideoMeta
CHANNELS_LOGIN_SCRIPT = BASE_DIR / "视频号发布" / "脚本" / "channels_login.py"
def _ensure_channels_cookie_or_login(skip_auto: bool) -> None:
"""指定发视频号时:先对齐双路径 Cookie;无效则直接调起扫码登录(保存后继续)"""
if skip_auto or os.environ.get("NO_AUTO_CHANNELS_LOGIN"):
sync_channels_cookie_files()
return
def _ensure_channels_cookie_or_login(*, auto_login: bool) -> None:
"""发视频号对齐双路径 Cookie。默认静默;仅 auto_login 且未设 NO_AUTO_CHANNELS_LOGIN 时才调起扫码"""
sync_channels_cookie_files()
if os.environ.get("NO_AUTO_CHANNELS_LOGIN", "").strip().lower() in ("1", "true", "yes"):
return
if not auto_login:
return
ok, _ = check_cookie_valid("视频号")
if ok:
return
@@ -139,6 +144,34 @@ def check_cookies_with_alert() -> tuple[list[str], list[str]]:
return available, alerts
def print_resume_report(
targets: list[str],
videos: list[Path],
published_set: set,
*,
detail: bool = False,
) -> None:
"""
断点续传说明publish_log.json 里 success=true 的 (平台, 文件名) 会跳过,其余重传。
"""
print(f"\n{'' * 60}")
print(" 断点续传 / 待传清单(已成功条目自动跳过)")
print(f"{'' * 60}")
total_pending = 0
for p in targets:
pending = [v for v in videos if (p, v.name) not in published_set]
done = len(videos) - len(pending)
total_pending += len(pending)
print(f" [{p}] 已成功 {done}/{len(videos)} | 待上传 {len(pending)}")
if detail and pending:
for v in pending[:50]:
print(f" · {v.name}")
if len(pending) > 50:
print(f" … 另有 {len(pending) - 50}")
print(f" 合计待传任务: {total_pending} 条(多平台分别计数)")
print(f"{'' * 60}\n")
def send_feishu_alert(alerts: list[str]):
"""通过飞书 Webhook 发送 Cookie 过期预警"""
import os
@@ -217,19 +250,19 @@ async def distribute_to_platform(
success=True, status="skipped", message="去重跳过(已发布)",
))
publish_schedule = None
if schedule_times and len(to_publish) > 0:
if len(schedule_times) >= len(to_publish):
publish_schedule = schedule_times[:len(to_publish)]
else:
publish_schedule = generate_schedule(len(to_publish))
idx_by_vp = {vp: j for j, vp in enumerate(videos)}
schedule_ok = bool(schedule_times) and len(schedule_times) == len(videos)
total = len(to_publish)
pub_fn = getattr(module, "publish_one_compat", None) or module.publish_one
for i, vp in enumerate(to_publish):
vmeta = VideoMeta.from_filename(str(vp))
title = vmeta.title(platform)
stime = publish_schedule[i] if publish_schedule else None
stime = (
schedule_times[idx_by_vp[vp]]
if schedule_ok
else None
)
try:
r = await pub_fn(str(vp), title, i + 1, total, scheduled_time=stime)
if isinstance(r, PublishResult):
@@ -348,29 +381,98 @@ async def main():
parser.add_argument("--no-dedup", action="store_true", help="跳过去重")
parser.add_argument("--serial", action="store_true", help="串行模式")
parser.add_argument("--now", action="store_true", help="立即发布(不排期)")
parser.add_argument("--min-gap", type=int, default=30, help="最小间隔(分钟)")
parser.add_argument("--max-gap", type=int, default=120, help="最大间隔(分钟)")
parser.add_argument("--max-hours", type=float, default=24.0, help="最大排期跨度(小时)")
parser.add_argument("--min-gap", type=int, default=30, help="最小间隔(分钟),仅 --legacy-schedule 生效")
parser.add_argument("--max-gap", type=int, default=120, help="最大间隔(分钟),仅 --legacy-schedule 生效")
parser.add_argument("--max-hours", type=float, default=24.0, help="最大排期跨度(小时),仅 --legacy-schedule 生效")
parser.add_argument(
"--legacy-schedule",
action="store_true",
help="使用固定随机间隔(--min-gap/--max-gap/--max-hours默认智能错峰排期",
)
parser.add_argument(
"--auto-channels-login",
action="store_true",
help="视频号 Cookie 失效时自动调起扫码登录(默认静默,不弹窗)",
)
parser.add_argument(
"--no-auto-channels-login",
action="store_true",
help="禁用「仅发视频号时」Cookie 失效自动弹窗登录",
help=argparse.SUPPRESS,
)
parser.add_argument(
"--allow-ui-browser",
action="store_true",
help="B站 Playwright 兜底使用有头浏览器(默认无窗口 headless",
)
parser.add_argument(
"--until-success",
action="store_true",
help="失败时每轮间隔后整表重试,直到全部成功或达 --until-success-max-rounds",
)
parser.add_argument(
"--until-success-sleep",
type=int,
default=90,
help="--until-success 每轮间隔秒数(默认 90",
)
parser.add_argument(
"--until-success-max-rounds",
type=int,
default=0,
help="--until-success 最大轮数0 表示不限制",
)
parser.add_argument(
"--resume-report",
action="store_true",
help="仅打印各平台已成功/待传条数与清单,不执行上传",
)
parser.add_argument(
"--resume-report-detail",
action="store_true",
help="与 --resume-report 合用,列出待传文件名",
)
args = parser.parse_args()
if (
if not args.allow_ui_browser:
os.environ.setdefault("PUBLISH_PLAYWRIGHT_HEADLESS", "1")
will_touch_channels = (
not args.check
and not args.retry
and args.platforms
and "视频号" in args.platforms
):
_ensure_channels_cookie_or_login(args.no_auto_channels_login)
available, alerts = check_cookies_with_alert()
if alerts:
send_feishu_alert(alerts)
and (not args.platforms or "视频号" in args.platforms)
)
if will_touch_channels:
_ensure_channels_cookie_or_login(
auto_login=bool(args.auto_channels_login) and not args.no_auto_channels_login,
)
if args.check:
available, alerts = check_cookies_with_alert()
if alerts:
send_feishu_alert(alerts)
return 0
if args.resume_report:
available, alerts = check_cookies_with_alert()
if alerts:
send_feishu_alert(alerts)
targets = args.platforms if args.platforms else available
targets = [t for t in targets if t in available]
video_dir = Path(args.video_dir) if args.video_dir else DEFAULT_VIDEO_DIR
if args.video:
videos = [Path(args.video)]
else:
videos = sorted(video_dir.glob("*.mp4"))
if not videos:
print(f"\n[✗] 未找到视频: {video_dir}")
return 1
published_set = set() if args.no_dedup else load_published_set()
if not targets:
print("\n[✗] 无可用平台,无法生成续传报告")
return 1
print_resume_report(
targets, videos, published_set, detail=args.resume_report_detail
)
return 0
if args.retry:
@@ -380,18 +482,59 @@ async def main():
save_results(results)
return 0
round_num = 0
while True:
round_num += 1
if args.until_success and round_num > 1:
print(
f"\n{'#' * 20} until-success 第 {round_num}"
f"{args.until_success_sleep}s 后开始){'#' * 20}\n",
flush=True,
)
await asyncio.sleep(args.until_success_sleep)
exit_code, failed_count = await _publish_one_round(args)
if not args.until_success:
return exit_code
# 无可发平台 / 无视频等致命错误,勿无限重试
if failed_count >= 9990:
return exit_code
if failed_count == 0:
print("\n[✓] until-success本轮无失败条目结束。", flush=True)
return 0
if args.until_success_max_rounds and round_num >= args.until_success_max_rounds:
print(
f"\n[✗] until-success已达最大轮数 {args.until_success_max_rounds}"
f"仍有约 {failed_count} 条失败。",
flush=True,
)
return 1
print(
f"\n[i] until-success仍有失败{failed_count} 条;"
f"{args.until_success_sleep}s 后重试(已成功写入日志会去重跳过)…",
flush=True,
)
async def _publish_one_round(args: argparse.Namespace) -> tuple[int, int]:
"""执行一轮分发。返回 (exit_code, 非跳过且失败的条数)。"""
available, alerts = check_cookies_with_alert()
if alerts:
send_feishu_alert(alerts)
if not available:
print("\n[✗] 没有可用平台,请先登录:")
for p, c in PLATFORM_CONFIG.items():
login = str(c["script"]).replace("publish", "login").replace("pure_api", "login")
print(f" {p}: python3 {login}")
return 1
return 1, 9999
targets = args.platforms if args.platforms else available
targets = [t for t in targets if t in available]
if not targets:
print("\n[✗] 指定的平台均不可用")
return 1
return 1, 9999
video_dir = Path(args.video_dir) if args.video_dir else DEFAULT_VIDEO_DIR
if args.video:
@@ -400,10 +543,13 @@ async def main():
videos = sorted(video_dir.glob("*.mp4"))
if not videos:
print(f"\n[✗] 未找到视频: {video_dir}")
return 1
return 1, 9999
published_set = set() if args.no_dedup else load_published_set()
if not args.no_dedup:
print_resume_report(targets, videos, published_set, detail=False)
mode = "串行" if args.serial else "并行"
total_new = 0
for p in targets:
@@ -411,15 +557,17 @@ async def main():
if (p, v.name) not in published_set:
total_new += 1
# 生成排期
schedule_times = None
if not args.now and total_new > 1:
schedule_times = generate_schedule(
len(videos),
min_gap=args.min_gap,
max_gap=args.max_gap,
max_hours=args.max_hours,
)
if args.legacy_schedule:
schedule_times = generate_schedule(
len(videos),
min_gap=args.min_gap,
max_gap=args.max_gap,
max_hours=args.max_hours,
)
else:
schedule_times = generate_smart_schedule(len(videos))
print(f"\n{'='*60}")
print(f" 分发计划 ({mode})")
@@ -427,7 +575,10 @@ async def main():
print(f" 视频数: {len(videos)}")
print(f" 目标平台: {', '.join(targets)}")
print(f" 新任务: {total_new}")
print(f" 发布方式: {'立即发布' if args.now or not schedule_times else '定时排期'}")
sched_label = "立即发布"
if schedule_times:
sched_label = "定时排期(智能错峰)" if not args.legacy_schedule else "定时排期legacy 随机间隔)"
print(f" 发布方式: {sched_label if not args.now else '立即发布'}")
if not args.no_dedup:
skipped = len(videos) * len(targets) - total_new
if skipped > 0:
@@ -440,7 +591,7 @@ async def main():
if total_new == 0:
print("[i] 所有视频已发布到所有平台,无新任务")
return 0
return 0, 0
t0 = time.time()
if args.serial:
@@ -461,7 +612,8 @@ async def main():
if failed_count > 0:
print(f"\n{failed_count} 条失败,可执行: python3 distribute_all.py --retry")
return 0 if ok == total else 1
failed_non_success = sum(1 for r in actual_results if not r.success)
return (0 if ok == total else 1), failed_non_success
if __name__ == "__main__":

View File

@@ -285,3 +285,115 @@
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/群主和交付师不一样.mp4", "title": "群主和交付师不一样", "success": false, "status": "error", "message": "post_create errCode=300002 request failed", "elapsed_sec": 21.92174482345581, "timestamp": "2026-03-23 07:10:26"}
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/职场课为什么卖不动.mp4", "title": "职场课为什么卖不动", "success": false, "status": "error", "message": "post_create errCode=300002 request failed", "elapsed_sec": 21.752349138259888, "timestamp": "2026-03-23 07:10:50"}
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/链接要落到具体事.mp4", "title": "链接要落到具体事", "success": false, "status": "error", "message": "post_create errCode=300002 request failed", "elapsed_sec": 48.84101176261902, "timestamp": "2026-03-23 07:11:42"}
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/Soul上像开老茶馆.mp4", "title": "Soul上像开老茶馆", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 1.4709701538085938, "timestamp": "2026-03-23 11:09:12"}
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/三百七十万罚单亲历.mp4", "title": "三百七十万罚单亲历", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.24030470848083496, "timestamp": "2026-03-23 11:09:15"}
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/三百简历只要两三个.mp4", "title": "三百简历只要两三个", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.13891935348510742, "timestamp": "2026-03-23 11:09:18"}
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/三角洲模型怎么卖.mp4", "title": "三角洲模型怎么卖", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.15856194496154785, "timestamp": "2026-03-23 11:09:21"}
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/上麦讲你上月做啥.mp4", "title": "上麦讲你上月做啥", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.2452690601348877, "timestamp": "2026-03-23 11:09:25"}
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/二百七十万推流从哪来.mp4", "title": "二百七十万推流从哪来", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.15202879905700684, "timestamp": "2026-03-23 11:09:28"}
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/什么话题最好起量.mp4", "title": "什么话题很好起量", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.24415278434753418, "timestamp": "2026-03-23 11:09:31"}
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/保镖业务先讲清模式.mp4", "title": "保镖业务先讲清模式", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.16850519180297852, "timestamp": "2026-03-23 11:09:34"}
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/分他靠自己赚不到的那块.mp4", "title": "分他靠自己赚不到的那块", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.12019515037536621, "timestamp": "2026-03-23 11:09:37"}
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/场景里拍视频就链接.mp4", "title": "场景里拍视频就链接", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.13576292991638184, "timestamp": "2026-03-23 11:09:41"}
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/学历不如可验证实操.mp4", "title": "学历不如可验证实操", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.16388773918151855, "timestamp": "2026-03-23 11:09:44"}
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/性格不对团队白搭.mp4", "title": "性格不对团队白搭", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.1238703727722168, "timestamp": "2026-03-23 11:09:47"}
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/想两万月薪先看月烧多少.mp4", "title": "想两万月薪先看月烧多少", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.158128023147583, "timestamp": "2026-03-23 11:09:50"}
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/政商金融矿产华侨.mp4", "title": "政商金融矿产华侨", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.14139485359191895, "timestamp": "2026-03-23 11:09:53"}
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/最大产值不是写代码.mp4", "title": "很大产值不是写代码", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.1378459930419922, "timestamp": "2026-03-23 11:09:56"}
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/现在缺的是流量.mp4", "title": "现在缺的是流量", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.18117499351501465, "timestamp": "2026-03-23 11:09:59"}
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/筛对了人能跟十二年.mp4", "title": "筛对了人能跟十二年", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.12108397483825684, "timestamp": "2026-03-23 11:10:03"}
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/线上更适合阿米巴.mp4", "title": "线上更适合阿米巴", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.2141871452331543, "timestamp": "2026-03-23 11:10:06"}
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/群主和交付师不一样.mp4", "title": "群主和交付师不一样", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.1297130584716797, "timestamp": "2026-03-23 11:10:09"}
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/职场课为什么卖不动.mp4", "title": "职场课为什么卖不动", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.11802816390991211, "timestamp": "2026-03-23 11:10:12"}
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/链接要落到具体事.mp4", "title": "链接要落到具体事", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.13814401626586914, "timestamp": "2026-03-23 11:10:15"}
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/Soul上像开老茶馆.mp4", "title": "Soul上像开老茶馆", "success": false, "status": "failed", "message": "Playwright: 投稿超时", "screenshot": "/tmp/bilibili_result.png", "elapsed_sec": 108.12666606903076, "timestamp": "2026-03-23 11:11:04"}
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/三百七十万罚单亲历.mp4", "title": "三百七十万罚单亲历", "success": true, "status": "reviewing", "message": "纯API投稿成功 (3.8s)", "elapsed_sec": 3.753028154373169, "timestamp": "2026-03-23 11:11:10"}
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/三百简历只要两三个.mp4", "title": "三百简历只要两三个", "success": true, "status": "reviewing", "message": "纯API投稿成功 (3.3s)", "elapsed_sec": 3.284956932067871, "timestamp": "2026-03-23 11:11:17"}
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/三角洲模型怎么卖.mp4", "title": "三角洲模型怎么卖", "success": true, "status": "reviewing", "message": "纯API投稿成功 (2.9s)", "elapsed_sec": 2.9468610286712646, "timestamp": "2026-03-23 11:11:23"}
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/上麦讲你上月做啥.mp4", "title": "上麦讲你上月做啥", "success": true, "status": "reviewing", "message": "纯API投稿成功 (2.3s)", "elapsed_sec": 2.2636637687683105, "timestamp": "2026-03-23 11:11:28"}
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/二百七十万推流从哪来.mp4", "title": "二百七十万推流从哪来", "success": true, "status": "reviewing", "message": "纯API投稿成功 (1.9s)", "elapsed_sec": 1.9184420108795166, "timestamp": "2026-03-23 11:11:33"}
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/什么话题最好起量.mp4", "title": "什么话题很好起量", "success": true, "status": "reviewing", "message": "纯API投稿成功 (2.4s)", "elapsed_sec": 2.3539421558380127, "timestamp": "2026-03-23 11:11:38"}
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/保镖业务先讲清模式.mp4", "title": "保镖业务先讲清模式", "success": true, "status": "reviewing", "message": "纯API投稿成功 (2.7s)", "elapsed_sec": 2.7068850994110107, "timestamp": "2026-03-23 11:11:44"}
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/分他靠自己赚不到的那块.mp4", "title": "分他靠自己赚不到的那块", "success": true, "status": "reviewing", "message": "纯API投稿成功 (5.7s)", "elapsed_sec": 5.657810211181641, "timestamp": "2026-03-23 11:11:53"}
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/场景里拍视频就链接.mp4", "title": "场景里拍视频就链接", "success": false, "status": "error", "message": "双方案均失败: Locator.click: Timeout 30000ms exceeded.\nCall log:\n - waiting for locator(\"inpu", "elapsed_sec": 0.0, "timestamp": "2026-03-23 11:16:35"}
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/学历不如可验证实操.mp4", "title": "学历不如可验证实操", "success": false, "status": "error", "message": "双方案均失败: Locator.click: Timeout 30000ms exceeded.\nCall log:\n - waiting for locator(\"inpu", "elapsed_sec": 0.0, "timestamp": "2026-03-23 11:21:18"}
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/性格不对团队白搭.mp4", "title": "性格不对团队白搭", "success": true, "status": "reviewing", "message": "纯API投稿成功 (4.4s)", "elapsed_sec": 4.356952905654907, "timestamp": "2026-03-23 11:21:25"}
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/想两万月薪先看月烧多少.mp4", "title": "想两万月薪先看月烧多少", "success": true, "status": "reviewing", "message": "纯API投稿成功 (2.9s)", "elapsed_sec": 2.901702880859375, "timestamp": "2026-03-23 11:21:31"}
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/政商金融矿产华侨.mp4", "title": "政商金融矿产华侨", "success": true, "status": "reviewing", "message": "纯API投稿成功 (5.3s)", "elapsed_sec": 5.3057701587677, "timestamp": "2026-03-23 11:21:39"}
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/最大产值不是写代码.mp4", "title": "很大产值不是写代码", "success": true, "status": "reviewing", "message": "纯API投稿成功 (3.3s)", "elapsed_sec": 3.320775270462036, "timestamp": "2026-03-23 11:21:46"}
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/现在缺的是流量.mp4", "title": "现在缺的是流量", "success": true, "status": "reviewing", "message": "纯API投稿成功 (8.7s)", "elapsed_sec": 8.691925048828125, "timestamp": "2026-03-23 11:21:57"}
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/筛对了人能跟十二年.mp4", "title": "筛对了人能跟十二年", "success": true, "status": "reviewing", "message": "纯API投稿成功 (2.6s)", "elapsed_sec": 2.6495907306671143, "timestamp": "2026-03-23 11:22:03"}
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/线上更适合阿米巴.mp4", "title": "线上更适合阿米巴", "success": true, "status": "reviewing", "message": "纯API投稿成功 (2.1s)", "elapsed_sec": 2.1320738792419434, "timestamp": "2026-03-23 11:22:08"}
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/群主和交付师不一样.mp4", "title": "群主和交付师不一样", "success": true, "status": "reviewing", "message": "纯API投稿成功 (2.8s)", "elapsed_sec": 2.7808477878570557, "timestamp": "2026-03-23 11:22:14"}
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/职场课为什么卖不动.mp4", "title": "职场课为什么卖不动", "success": false, "status": "error", "message": "双方案均失败: Locator.click: Timeout 30000ms exceeded.\nCall log:\n - waiting for locator(\"inpu", "elapsed_sec": 0.0, "timestamp": "2026-03-23 11:26:56"}
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/链接要落到具体事.mp4", "title": "链接要落到具体事", "success": false, "status": "error", "message": "双方案均失败: Locator.click: Timeout 30000ms exceeded.\nCall log:\n - waiting for locator(\"inpu", "elapsed_sec": 0.0, "timestamp": "2026-03-23 11:31:39"}
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/Soul上像开老茶馆.mp4", "title": "Soul上像开老茶馆", "success": true, "status": "likely_published", "message": "发布按钮+确认已点击,视频可能仍在处理", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 50.02102184295654, "timestamp": "2026-03-23 11:10:25"}
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/三百七十万罚单亲历.mp4", "title": "三百七十万罚单亲历", "success": true, "status": "likely_published", "message": "发布按钮+确认已点击,视频可能仍在处理", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 49.1489679813385, "timestamp": "2026-03-23 11:11:41"}
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/三百简历只要两三个.mp4", "title": "三百简历只要两三个", "success": true, "status": "likely_published", "message": "发布按钮+确认已点击,视频可能仍在处理", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 49.1381778717041, "timestamp": "2026-03-23 11:12:56"}
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/三角洲模型怎么卖.mp4", "title": "三角洲模型怎么卖", "success": true, "status": "likely_published", "message": "发布按钮+确认已点击,视频可能仍在处理", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 49.14575791358948, "timestamp": "2026-03-23 11:14:12"}
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/上麦讲你上月做啥.mp4", "title": "上麦讲你上月做啥", "success": true, "status": "likely_published", "message": "发布按钮+确认已点击,视频可能仍在处理", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 48.655869007110596, "timestamp": "2026-03-23 11:15:28"}
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/二百七十万推流从哪来.mp4", "title": "二百七十万推流从哪来", "success": true, "status": "likely_published", "message": "发布按钮+确认已点击,视频可能仍在处理", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 48.75113916397095, "timestamp": "2026-03-23 11:16:43"}
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/什么话题最好起量.mp4", "title": "什么话题很好起量", "success": true, "status": "likely_published", "message": "发布按钮+确认已点击,视频可能仍在处理", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 48.504446029663086, "timestamp": "2026-03-23 11:17:58"}
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/保镖业务先讲清模式.mp4", "title": "保镖业务先讲清模式", "success": true, "status": "likely_published", "message": "发布按钮+确认已点击,视频可能仍在处理", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 48.730623960494995, "timestamp": "2026-03-23 11:19:13"}
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/分他靠自己赚不到的那块.mp4", "title": "分他靠自己赚不到的那块", "success": true, "status": "likely_published", "message": "发布按钮+确认已点击,视频可能仍在处理", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 48.763344287872314, "timestamp": "2026-03-23 11:20:29"}
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/场景里拍视频就链接.mp4", "title": "场景里拍视频就链接", "success": true, "status": "likely_published", "message": "发布按钮+确认已点击,视频可能仍在处理", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 48.68367004394531, "timestamp": "2026-03-23 11:21:44"}
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/学历不如可验证实操.mp4", "title": "学历不如可验证实操", "success": true, "status": "likely_published", "message": "发布按钮+确认已点击,视频可能仍在处理", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 48.9231276512146, "timestamp": "2026-03-23 11:22:59"}
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/性格不对团队白搭.mp4", "title": "性格不对团队白搭", "success": true, "status": "likely_published", "message": "发布按钮+确认已点击,视频可能仍在处理", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 48.8387770652771, "timestamp": "2026-03-23 11:24:15"}
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/想两万月薪先看月烧多少.mp4", "title": "想两万月薪先看月烧多少", "success": true, "status": "likely_published", "message": "发布按钮+确认已点击,视频可能仍在处理", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 48.93092584609985, "timestamp": "2026-03-23 11:25:30"}
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/政商金融矿产华侨.mp4", "title": "政商金融矿产华侨", "success": true, "status": "likely_published", "message": "发布按钮+确认已点击,视频可能仍在处理", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 48.95681095123291, "timestamp": "2026-03-23 11:26:46"}
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/最大产值不是写代码.mp4", "title": "很大产值不是写代码", "success": true, "status": "likely_published", "message": "发布按钮+确认已点击,视频可能仍在处理", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 48.8231360912323, "timestamp": "2026-03-23 11:28:01"}
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/现在缺的是流量.mp4", "title": "现在缺的是流量", "success": true, "status": "likely_published", "message": "发布按钮+确认已点击,视频可能仍在处理", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 48.829030990600586, "timestamp": "2026-03-23 11:29:16"}
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/筛对了人能跟十二年.mp4", "title": "筛对了人能跟十二年", "success": true, "status": "likely_published", "message": "发布按钮+确认已点击,视频可能仍在处理", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 48.778688192367554, "timestamp": "2026-03-23 11:30:32"}
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/线上更适合阿米巴.mp4", "title": "线上更适合阿米巴", "success": true, "status": "likely_published", "message": "发布按钮+确认已点击,视频可能仍在处理", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 48.829610109329224, "timestamp": "2026-03-23 11:31:47"}
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/群主和交付师不一样.mp4", "title": "群主和交付师不一样", "success": true, "status": "likely_published", "message": "发布按钮+确认已点击,视频可能仍在处理", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 48.864104986190796, "timestamp": "2026-03-23 11:33:02"}
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/职场课为什么卖不动.mp4", "title": "职场课为什么卖不动", "success": true, "status": "likely_published", "message": "发布按钮+确认已点击,视频可能仍在处理", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 48.94530510902405, "timestamp": "2026-03-23 11:34:18"}
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/链接要落到具体事.mp4", "title": "链接要落到具体事", "success": true, "status": "likely_published", "message": "发布按钮+确认已点击,视频可能仍在处理", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 48.90583395957947, "timestamp": "2026-03-23 11:35:34"}
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/Soul上像开老茶馆.mp4", "title": "Soul上像开老茶馆", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 1.4672038555145264, "timestamp": "2026-03-23 11:43:08"}
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/三百七十万罚单亲历.mp4", "title": "三百七十万罚单亲历", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.14421701431274414, "timestamp": "2026-03-23 11:43:11"}
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/三百简历只要两三个.mp4", "title": "三百简历只要两三个", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.1285099983215332, "timestamp": "2026-03-23 11:43:14"}
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/三角洲模型怎么卖.mp4", "title": "三角洲模型怎么卖", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.12726902961730957, "timestamp": "2026-03-23 11:43:18"}
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/上麦讲你上月做啥.mp4", "title": "上麦讲你上月做啥", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.7475981712341309, "timestamp": "2026-03-23 11:43:21"}
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/二百七十万推流从哪来.mp4", "title": "二百七十万推流从哪来", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.13030028343200684, "timestamp": "2026-03-23 11:43:24"}
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/什么话题最好起量.mp4", "title": "什么话题很好起量", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.13919687271118164, "timestamp": "2026-03-23 11:43:28"}
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/保镖业务先讲清模式.mp4", "title": "保镖业务先讲清模式", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.1264021396636963, "timestamp": "2026-03-23 11:43:31"}
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/分他靠自己赚不到的那块.mp4", "title": "分他靠自己赚不到的那块", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.15427303314208984, "timestamp": "2026-03-23 11:43:34"}
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/场景里拍视频就链接.mp4", "title": "场景里拍视频就链接", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.3109300136566162, "timestamp": "2026-03-23 11:43:37"}
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/学历不如可验证实操.mp4", "title": "学历不如可验证实操", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.19164180755615234, "timestamp": "2026-03-23 11:43:40"}
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/性格不对团队白搭.mp4", "title": "性格不对团队白搭", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.14249396324157715, "timestamp": "2026-03-23 11:43:43"}
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/想两万月薪先看月烧多少.mp4", "title": "想两万月薪先看月烧多少", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.16753792762756348, "timestamp": "2026-03-23 11:43:47"}
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/政商金融矿产华侨.mp4", "title": "政商金融矿产华侨", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.148911714553833, "timestamp": "2026-03-23 11:43:50"}
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/最大产值不是写代码.mp4", "title": "很大产值不是写代码", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.24453401565551758, "timestamp": "2026-03-23 11:43:53"}
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/现在缺的是流量.mp4", "title": "现在缺的是流量", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.2290198802947998, "timestamp": "2026-03-23 11:43:56"}
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/筛对了人能跟十二年.mp4", "title": "筛对了人能跟十二年", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.15469002723693848, "timestamp": "2026-03-23 11:43:59"}
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/线上更适合阿米巴.mp4", "title": "线上更适合阿米巴", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.18268704414367676, "timestamp": "2026-03-23 11:44:03"}
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/群主和交付师不一样.mp4", "title": "群主和交付师不一样", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.1401972770690918, "timestamp": "2026-03-23 11:44:06"}
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/职场课为什么卖不动.mp4", "title": "职场课为什么卖不动", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.15677309036254883, "timestamp": "2026-03-23 11:44:09"}
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/链接要落到具体事.mp4", "title": "链接要落到具体事", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.18844985961914062, "timestamp": "2026-03-23 11:44:12"}
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/Soul上像开老茶馆.mp4", "title": "Soul上像开老茶馆", "success": true, "status": "reviewing", "message": "纯API投稿成功 (4.2s)", "elapsed_sec": 4.178177118301392, "timestamp": "2026-03-23 11:43:12"}
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/场景里拍视频就链接.mp4", "title": "场景里拍视频就链接", "success": true, "status": "reviewing", "message": "纯API投稿成功 (2.2s)", "elapsed_sec": 2.2191109657287598, "timestamp": "2026-03-23 11:43:17"}
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/学历不如可验证实操.mp4", "title": "学历不如可验证实操", "success": true, "status": "reviewing", "message": "纯API投稿成功 (2.2s)", "elapsed_sec": 2.229668140411377, "timestamp": "2026-03-23 11:43:22"}
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/职场课为什么卖不动.mp4", "title": "职场课为什么卖不动", "success": true, "status": "reviewing", "message": "纯API投稿成功 (2.4s)", "elapsed_sec": 2.3824262619018555, "timestamp": "2026-03-23 11:43:27"}
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/链接要落到具体事.mp4", "title": "链接要落到具体事", "success": true, "status": "reviewing", "message": "纯API投稿成功 (2.8s)", "elapsed_sec": 2.760422945022583, "timestamp": "2026-03-23 11:43:33"}
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/Soul上像开老茶馆.mp4", "title": "Soul上像开老茶馆 #Soul派对 #创业日记 #小程序 卡若创业派对", "success": false, "status": "error", "message": "异常: Locator.set_input_files: Timeout 10000ms exceeded.\nCall log:\n - waiting for loc", "elapsed_sec": 23.14011812210083, "timestamp": "2026-03-23 13:01:37"}
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/三百七十万罚单亲历.mp4", "title": "三百七十万罚单亲历 #Soul派对 #创业日记 #小程序 卡若创业派对", "success": false, "status": "error", "message": "异常: Page.goto: Timeout 30000ms exceeded.\nCall log:\n - navigating to \"https://channe", "elapsed_sec": 30.034138917922974, "timestamp": "2026-03-23 13:02:14"}
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/Soul上像开老茶馆.mp4", "title": "Soul上像开老茶馆 #Soul派对 #创业日记", "success": true, "status": "reviewing", "message": "API确认列表未匹配 (未在列表前20条中找到)", "screenshot": "/tmp/channels_ss/Soul上像开老茶馆_5_verify.png", "elapsed_sec": 30.81173086166382, "timestamp": "2026-03-23 13:02:52"}
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/三百七十万罚单亲历.mp4", "title": "三百七十万罚单亲历 #Soul派对 #创业日记", "success": true, "status": "reviewing", "message": "API确认列表未匹配 (未在列表前20条中找到)", "screenshot": "/tmp/channels_ss/三百七十万罚单亲历_5_verify.png", "elapsed_sec": 30.594141244888306, "timestamp": "2026-03-23 13:03:31"}
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/三百简历只要两三个.mp4", "title": "三百简历只要两三个 #Soul派对 #创业日记", "success": true, "status": "reviewing", "message": "API确认列表未匹配 (未在列表前20条中找到)", "screenshot": "/tmp/channels_ss/三百简历只要两三个_5_verify.png", "elapsed_sec": 31.392528772354126, "timestamp": "2026-03-23 13:04:10"}
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/三角洲模型怎么卖.mp4", "title": "三角洲模型怎么卖 #Soul派对 #创业日记", "success": true, "status": "reviewing", "message": "API确认列表未匹配 (未在列表前20条中找到)", "screenshot": "/tmp/channels_ss/三角洲模型怎么卖_5_verify.png", "elapsed_sec": 31.27242684364319, "timestamp": "2026-03-23 13:04:50"}
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/上麦讲你上月做啥.mp4", "title": "上麦讲你上月做啥 #Soul派对 #创业日记", "success": true, "status": "reviewing", "message": "API确认列表未匹配 (未在列表前20条中找到)", "screenshot": "/tmp/channels_ss/上麦讲你上月做啥_5_verify.png", "elapsed_sec": 31.388854026794434, "timestamp": "2026-03-23 13:05:29"}
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/二百七十万推流从哪来.mp4", "title": "二百七十万推流从哪来 #Soul派对 #创业日记", "success": true, "status": "reviewing", "message": "API确认列表未匹配 (未在列表前20条中找到)", "screenshot": "/tmp/channels_ss/二百七十万推流从哪来_5_verify.png", "elapsed_sec": 32.773988008499146, "timestamp": "2026-03-23 13:06:10"}
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/什么话题最好起量.mp4", "title": "什么话题最好起量 #Soul派对 #创业日记", "success": true, "status": "reviewing", "message": "API确认列表未匹配 (未在列表前20条中找到)", "screenshot": "/tmp/channels_ss/什么话题最好起量_5_verify.png", "elapsed_sec": 32.07719111442566, "timestamp": "2026-03-23 13:06:50"}
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/保镖业务先讲清模式.mp4", "title": "保镖业务先讲清模式 #Soul派对 #创业日记", "success": true, "status": "reviewing", "message": "API确认列表未匹配 (未在列表前20条中找到)", "screenshot": "/tmp/channels_ss/保镖业务先讲清模式_5_verify.png", "elapsed_sec": 31.477304935455322, "timestamp": "2026-03-23 13:07:30"}
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/分他靠自己赚不到的那块.mp4", "title": "分他靠自己赚不到的那块 #Soul派对 #创业日记", "success": true, "status": "reviewing", "message": "API确认列表未匹配 (未在列表前20条中找到)", "screenshot": "/tmp/channels_ss/分他靠自己赚不到的那块_5_verify.png", "elapsed_sec": 32.22468280792236, "timestamp": "2026-03-23 13:08:10"}
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/场景里拍视频就链接.mp4", "title": "场景里拍视频就链接 #Soul派对 #创业日记", "success": true, "status": "reviewing", "message": "API确认列表未匹配 (未在列表前20条中找到)", "screenshot": "/tmp/channels_ss/场景里拍视频就链接_5_verify.png", "elapsed_sec": 31.937192916870117, "timestamp": "2026-03-23 13:08:50"}
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/学历不如可验证实操.mp4", "title": "学历不如可验证实操 #Soul派对 #创业日记", "success": true, "status": "reviewing", "message": "API确认列表未匹配 (未在列表前20条中找到)", "screenshot": "/tmp/channels_ss/学历不如可验证实操_5_verify.png", "elapsed_sec": 29.13404893875122, "timestamp": "2026-03-23 13:09:27"}
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/性格不对团队白搭.mp4", "title": "性格不对团队白搭 #Soul派对 #创业日记", "success": true, "status": "reviewing", "message": "API确认列表未匹配 (未在列表前20条中找到)", "screenshot": "/tmp/channels_ss/性格不对团队白搭_5_verify.png", "elapsed_sec": 30.99875521659851, "timestamp": "2026-03-23 13:10:06"}
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/想两万月薪先看月烧多少.mp4", "title": "想两万月薪先看月烧多少 #Soul派对 #创业日记", "success": true, "status": "reviewing", "message": "API确认列表未匹配 (未在列表前20条中找到)", "screenshot": "/tmp/channels_ss/想两万月薪先看月烧多少_5_verify.png", "elapsed_sec": 30.932358026504517, "timestamp": "2026-03-23 13:10:45"}
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/政商金融矿产华侨.mp4", "title": "政商金融矿产华侨 #Soul派对 #创业日记", "success": true, "status": "reviewing", "message": "API确认列表未匹配 (未在列表前20条中找到)", "screenshot": "/tmp/channels_ss/政商金融矿产华侨_5_verify.png", "elapsed_sec": 30.35584306716919, "timestamp": "2026-03-23 13:11:24"}
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/最大产值不是写代码.mp4", "title": "最大产值不是写代码 #Soul派对 #创业日记", "success": true, "status": "reviewing", "message": "API确认列表未匹配 (未在列表前20条中找到)", "screenshot": "/tmp/channels_ss/最大产值不是写代码_5_verify.png", "elapsed_sec": 30.229849815368652, "timestamp": "2026-03-23 13:12:02"}
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/现在缺的是流量.mp4", "title": "现在缺的是流量 #Soul派对 #创业日记", "success": true, "status": "reviewing", "message": "API确认列表未匹配 (未在列表前20条中找到)", "screenshot": "/tmp/channels_ss/现在缺的是流量_5_verify.png", "elapsed_sec": 30.722687244415283, "timestamp": "2026-03-23 13:12:41"}
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/筛对了人能跟十二年.mp4", "title": "筛对了人能跟十二年 #Soul派对 #创业日记", "success": true, "status": "reviewing", "message": "API确认列表未匹配 (未在列表前20条中找到)", "screenshot": "/tmp/channels_ss/筛对了人能跟十二年_5_verify.png", "elapsed_sec": 30.178684949874878, "timestamp": "2026-03-23 13:13:19"}
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/线上更适合阿米巴.mp4", "title": "线上更适合阿米巴 #Soul派对 #创业日记", "success": true, "status": "reviewing", "message": "API确认列表未匹配 (未在列表前20条中找到)", "screenshot": "/tmp/channels_ss/线上更适合阿米巴_5_verify.png", "elapsed_sec": 30.138293981552124, "timestamp": "2026-03-23 13:13:57"}
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/群主和交付师不一样.mp4", "title": "群主和交付师不一样 #Soul派对 #创业日记", "success": true, "status": "reviewing", "message": "API确认列表未匹配 (未在列表前20条中找到)", "screenshot": "/tmp/channels_ss/群主和交付师不一样_5_verify.png", "elapsed_sec": 30.115286827087402, "timestamp": "2026-03-23 13:14:36"}
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/职场课为什么卖不动.mp4", "title": "职场课为什么卖不动 #Soul派对 #创业日记", "success": true, "status": "reviewing", "message": "API确认列表未匹配 (未在列表前20条中找到)", "screenshot": "/tmp/channels_ss/职场课为什么卖不动_5_verify.png", "elapsed_sec": 30.429933071136475, "timestamp": "2026-03-23 13:15:14"}
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/链接要落到具体事.mp4", "title": "链接要落到具体事 #Soul派对 #创业日记", "success": true, "status": "reviewing", "message": "API确认列表未匹配 (未在列表前20条中找到)", "screenshot": "/tmp/channels_ss/链接要落到具体事_5_verify.png", "elapsed_sec": 30.445733070373535, "timestamp": "2026-03-23 13:15:53"}

View File

@@ -1,16 +1,94 @@
#!/usr/bin/env python3
"""
定时排期生成器 v2
规则:
1. 第一条视频 **立即发布**first_delay=0
2. 第二条起,每条间隔 30~120 分钟(随机)
3. 若总时长超过 max_hours按比例压缩
4. 支持各平台原生定时 API 所需的 datetime 对象
定时排期生成器 v3
- v2固定 30~120min 随机间隔
- v3generate_smart_schedule — 按视频条数自适应间隔/总跨度,并可选避开本地凌晨低活跃时段(适合视频号等多条错峰发
"""
import os
import random
from datetime import datetime, timedelta
def suggest_stagger_params(n: int) -> tuple[int, int, float]:
"""
根据待发布条数给出建议min_gap, max_gap(分钟), max_hours。
条数越多,略缩短间隔、允许更长总跨度,避免全挤在 24h 内过密。
"""
if n <= 1:
return 30, 120, 24.0
if n <= 4:
return 45, 150, 28.0
if n <= 8:
return 38, 125, max(28.0, min(48.0, n * 3.2))
if n <= 15:
return 32, 105, max(36.0, min(60.0, n * 2.8))
if n <= 25:
return 28, 95, max(48.0, min(72.0, n * 2.4))
return 25, 85, min(96.0, max(56.0, n * 2.0))
def refine_avoid_late_night(
times: list[datetime],
min_after_prev: int = 25,
) -> list[datetime]:
"""
将落在 **本机本地时间** 00:0007:59 的排期顺延到当日/次日 12:xx
并保持与上一条至少 min_after_prev 分钟(请把系统时区设为发布面向地区,如中国用北京时间)。
关闭:环境变量 SCHEDULE_NO_NIGHT_REFINE=1在 generate_smart_schedule 层已读)。
"""
if len(times) <= 1:
return list(times)
def strip_tz(t: datetime) -> datetime:
return t.replace(tzinfo=None) if t.tzinfo else t
out = [strip_tz(times[0])]
for raw in times[1:]:
t = max(strip_tz(raw), out[-1] + timedelta(minutes=min_after_prev))
if 0 <= t.hour < 8:
minute_slot = random.randint(0, 50)
candidate = t.replace(hour=12, minute=minute_slot, second=0, microsecond=0)
if candidate <= out[-1] + timedelta(minutes=min_after_prev):
nd = out[-1].date() + timedelta(days=1)
candidate = datetime(nd.year, nd.month, nd.day, 12, random.randint(0, 50), 0)
t = candidate
while t <= out[-1] + timedelta(minutes=min_after_prev):
t += timedelta(minutes=30)
out.append(t)
return out
def generate_smart_schedule(
n: int,
start_time: datetime | None = None,
avoid_late_night: bool | None = None,
) -> list[datetime]:
"""
推荐默认:按条数自适应间隔 + 总跨度,并避开凌晨。
avoid_late_night 默认 TrueSCHEDULE_NO_NIGHT_REFINE=1 可关闭。
"""
if n <= 0:
return []
if avoid_late_night is None:
avoid_late_night = os.environ.get("SCHEDULE_NO_NIGHT_REFINE", "").strip() not in (
"1",
"true",
"yes",
)
min_gap, max_gap, max_hours = suggest_stagger_params(n)
times = generate_schedule(
n,
min_gap=min_gap,
max_gap=max_gap,
max_hours=max_hours,
first_delay=0,
start_time=start_time,
)
if avoid_late_night and n > 1:
times = refine_avoid_late_night(times)
return times
def generate_schedule(
n: int,
min_gap: int = 30,

View File

@@ -0,0 +1,224 @@
#!/usr/bin/env python3
"""
视频号:严格两步——① 等微信扫码完成Cookie 校验通过)② 再上传。
默认:弹出 Chromium 窗口扫码(与手机微信同一套实时码,易成功);扫完并保存 Cookie 后自动进入上传。
无头:加 --silent-login仅终端/对话看图,易与真实会话不一致)。
用法:
cd 多平台分发/脚本
python3 video_channels_resume.py --video-dir "/path/to/成片"
python3 video_channels_resume.py --video-dir "/path/to/成片" --silent-login
铁律:
- 未检测到有效 Cookie 前,绝不会启动 distribute_all。
- Cookie 须「连续两次」间隔数秒检测通过,避免半写入就开传。
- 仅第一步:--step1-only
"""
from __future__ import annotations
import os
import subprocess
import sys
import time
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
BASE = SCRIPT_DIR.parent.parent
CHANNELS_LOGIN = BASE / "视频号发布" / "脚本" / "channels_login.py"
DISTRIBUTE = SCRIPT_DIR / "distribute_all.py"
QR_PATH = Path("/tmp/channels_qr.png")
LOGIN_LOG = Path("/tmp/channels_login_last.log")
def _cookie_ok() -> bool:
sys.path.insert(0, str(SCRIPT_DIR))
from cookie_manager import check_cookie_valid, sync_channels_cookie_files
sync_channels_cookie_files()
ok, _ = check_cookie_valid("视频号")
return ok
def _step1_confirm_cookie() -> bool:
if not _cookie_ok():
return False
time.sleep(4)
return _cookie_ok()
def _run_visible_login() -> int:
"""阻塞运行 channels_login --playwright-only弹窗 Chromium 扫码。"""
env = os.environ.copy()
env.pop("CHANNELS_SILENT_QR", None)
env["PYTHONUNBUFFERED"] = "1"
print(
"\n[i] ▶ 即将弹出 Chromium 窗口:请用 **微信扫一扫** 扫窗口里的码,"
"并在手机上确认登录。\n"
" 登录成功并写入 Cookie 后,本脚本会自动进入下一步上传。\n",
flush=True,
)
return subprocess.run(
[sys.executable, str(CHANNELS_LOGIN), "--playwright-only"],
cwd=str(CHANNELS_LOGIN.parent),
env=env,
).returncode
def _run_silent_login_poll(*, poll_interval: int, poll_max: int) -> bool:
"""后台 headless 登录 + 轮询 Cookie。"""
env = os.environ.copy()
env["CHANNELS_SILENT_QR"] = "1"
env["PYTHONUNBUFFERED"] = "1"
LOGIN_LOG.parent.mkdir(parents=True, exist_ok=True)
log_f = open(LOGIN_LOG, "w", encoding="utf-8")
print(f"[i] 已启动静默登录headless日志{LOGIN_LOG}", flush=True)
proc = subprocess.Popen(
[sys.executable, str(CHANNELS_LOGIN)],
cwd=str(CHANNELS_LOGIN.parent),
env=env,
stdout=log_f,
stderr=subprocess.STDOUT,
)
log_f.close()
time.sleep(14)
if QR_PATH.exists():
desk = Path.home() / "Desktop" / "channels_login_qr.png"
try:
desk.write_bytes(QR_PATH.read_bytes())
except Exception:
desk = None
print("\n========== SOUL_QR_IMAGE_FOR_CHAT ==========", flush=True)
print(str(QR_PATH.resolve()), flush=True)
if desk and desk.exists():
print(f"file://{desk.resolve()}", flush=True)
print("→ 请扫 **与 headless 会话对应** 的码(见上图路径)。", flush=True)
print("========== END_SOUL_QR_MARKER ==========\n", flush=True)
print(
f"\n[i] STEP 1轮询 Cookie{poll_interval}s最长约 "
f"{poll_max * poll_interval // 60} 分钟…\n",
flush=True,
)
ok_phase = False
for i in range(poll_max):
time.sleep(poll_interval)
if _cookie_ok():
time.sleep(4)
if _cookie_ok():
print(
f"\n[✓] STEP 1 完成Cookie 已落盘(约 {(i + 1) * poll_interval}s\n",
flush=True,
)
ok_phase = True
break
if i % 5 == 4:
print(
f" …仍等待扫码({(i + 1) * poll_interval}s",
flush=True,
)
try:
proc.terminate()
except Exception:
pass
return ok_phase
def main() -> int:
import argparse
p = argparse.ArgumentParser(
description="视频号:先扫码登录,再仅发视频号(断点 + until-success",
)
p.add_argument("--video-dir", required=True, help="成片目录(含 mp4")
p.add_argument("--poll-interval", type=int, default=12, help="静默模式轮询秒数")
p.add_argument("--poll-max", type=int, default=150, help="静默模式最多轮询次数")
p.add_argument("--until-success-sleep", type=int, default=90, help="until-success 轮间隔")
p.add_argument(
"--silent-login",
action="store_true",
help="无头登录(不弹窗);默认弹 Chromium 窗口扫码",
)
p.add_argument(
"--step1-only",
action="store_true",
help="只做第一步Cookie 有效即退出,不上传",
)
args = p.parse_args()
vd = Path(args.video_dir)
if not vd.is_dir():
print(f"[✗] 目录不存在: {vd}", flush=True)
return 1
print("\n" + "=" * 60, flush=True)
print(" 【STEP 1 / 2】微信扫码登录视频号助手", flush=True)
if args.silent_login:
print(" 模式静默headless", flush=True)
else:
print(" 模式:弹窗 Chromium推荐", flush=True)
print(" 未完成本步之前,不会开始上传。", flush=True)
print("=" * 60 + "\n", flush=True)
if _step1_confirm_cookie():
print(
"[✓] STEP 1Cookie 已连续两次校验通过,跳过登录。\n",
flush=True,
)
else:
if not CHANNELS_LOGIN.exists():
print(f"[✗] 未找到 {CHANNELS_LOGIN}", flush=True)
return 1
if args.silent_login:
if not _run_silent_login_poll(
poll_interval=args.poll_interval,
poll_max=args.poll_max,
):
print("[✗] STEP 1 超时:仍未获得有效 Cookie。", flush=True)
return 1
else:
rc = _run_visible_login()
if rc != 0:
print(f"[✗] 登录进程退出码 {rc},未保存 Cookie。", flush=True)
return 1
time.sleep(2)
if not _step1_confirm_cookie():
print(
"[✗] 弹窗登录结束但 Cookie 仍未通过连续校验,请重试或改用 --silent-login。",
flush=True,
)
return 1
print("[✓] STEP 1 完成:弹窗扫码已落盘。\n", flush=True)
if args.step1_only:
print("[i] --step1-only不上传结束。", flush=True)
return 0
print("\n" + "=" * 60, flush=True)
print(" 【STEP 2 / 2】扫码已完成 → 开始仅视频号上传", flush=True)
print("=" * 60 + "\n", flush=True)
time.sleep(2)
r = subprocess.run(
[
sys.executable,
str(DISTRIBUTE),
"--platforms",
"视频号",
"--video-dir",
str(vd),
"--until-success",
"--until-success-sleep",
str(args.until_success_sleep),
],
cwd=str(SCRIPT_DIR),
)
return r.returncode
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,11 +1,11 @@
---
name: Soul竖屏切片
description: Soul 派对视频→竖屏成片;高光**单主题完整**30s5min/条,每场约 830 条)、去穿插话题;封面底**约 10% 轻模糊**(非全糊)。字幕暖色条+金边、纠错+关键词高亮、concat 前导空白、音轨 PTS推荐 --typewriter-subs。流程裁剪检查→soul_enhance。MLX→visual_enhance v8 可选。
triggers: Soul竖屏切片、视频切片、热点切片、竖屏成片、派对切片、裁剪检查、重新截图、全画面标定、竖屏裁剪、全画面成片、letterbox、画面显示全、白边、飞书录屏、LTX、AI生成视频、Retake重剪、字幕优化、字幕同步、逐字字幕
triggers: Soul竖屏切片、视频切片、热点切片、竖屏成片、派对切片、裁剪检查、重新截图、全画面标定、竖屏裁剪、全画面成片、letterbox、画面显示全、白边、飞书录屏、LTX、AI生成视频、Retake重剪、字幕优化、字幕同步、逐字字幕、上麦项目、月营收、融资、前三秒钩子
owner: 木叶
group: 木
version: "2.1"
updated: "2026-03-22"
version: "2.3"
updated: "2026-03-23"
---
# Soul 竖屏切片 · 专用 Skill
@@ -30,7 +30,10 @@ updated: "2026-03-22"
## 二、视频结构:提问→回答 + 前3秒高光 + 去语助词
- **每个话题前均优先提问→回答**:先看片段有没有人提问;**有提问**则把**提问的问题**放到前3秒封面/前贴),先展示问题再播回答;无提问则从**该片段口播里**抽**吸睛句或反问**作 `hook_3sec`,禁止与正片内容脱节。
- **上麦案例项目与钱优先占满前3秒高于泛提问**
当片段里是**对方在讲自己的项目/业务**(做什么、服务谁、阶段),且口播中出现 **月收入/营收/融资/投资/到账/定价/预算/工资档位/烧多少钱** 等**可量化钱**或**项目身份**信息时:`hook_3sec` **优先**写成一句 **「项目一句话 + 钱/阶段信号」**优质上麦案例逻辑让用户前3秒知道「这人是谁、项目多硬、钱的故事是什么」。
**在此之后**再考虑「主持人提问」作 hook若前3秒用项目+钱已足够抓人,可将 `question` 保留为片中互动句,**不必**把泛提问顶掉项目钩子。仅当片段**没有**清晰项目/钱信息时,才回到「有提问用提问、无提问用金句」的旧规则。
- **每个话题前均优先提问→回答(与上条叠加时的裁决)**:默认先看有没有人提问;**但若本段核心是对方分享项目**,则 **hook = 项目+钱摘要** 优先,**正片**再展开主持人追问与回答禁止前3秒与「对方在讲的事」脱节。
- **成片链路**前3秒展示问题或 hook→ 正片展开 → **整片去除语助词**(提问与回答部分均由 soul_enhance 清理)。
- **节奏与高光**:竖屏段内全程**高光清晰、节奏清楚**(剪去拖沓静音与跑题可在 enhance 链路的静音裁剪中体现;**话题边界以 highlights 起止为准,剪辑阶段就要裁干净**)。
- **单主题、完整、有趣****一条成片只服务一个主题**;口播里若**临时插进其他话题**,在 `highlights` 对应时间段上**剪掉无关段**(或拆成另一条高光),保证**有头有尾、逻辑闭环**;选题要**有传播点**(反差、数字、痛点、金句),忌又长又平。
@@ -49,7 +52,7 @@ updated: "2026-03-22"
```
- **batch_clip**:输出到 `clips/`
- **soul_enhance -o 成片/ --vertical --title-only****文件名 = 封面标题 = highlights 的 title**(去杠:`:|、—、/` 等替换为空格),名字与标题一致、无序号无杠;字幕烧录(随语音走动);完整去语助词;竖裁剪直出到 `成片/`
- **soul_enhance -o 成片/ --title-only****推荐仍写 `--vertical`**):自 v2.3 起,只要带了 **`--title-only` 和/或 `--crop-vf` / `--vertical-fit-full`**,脚本会**默认启用竖屏直出**,避免漏写 `--vertical` 误出 1920×1080 横版。**文件名 = 封面标题 = highlights 的 title**(去杠:`:|、—、/` 等替换为空格);字幕烧录;去语助词;竖裁剪直出到 `成片/`
### 3.1 全画面参数(必做约定)
@@ -133,6 +136,7 @@ python3 soul_enhance.py \
- **封面**:竖条画布内**不超出界面****冷色半透明渐变**`VERTICAL_COVER_ALPHA` 约 148+ **底部电影感渐隐**`STYLE['cover']``vignette_*`+ **顶栏单条 Soul 绿 + 可选 1px 淡金线** + **细白内框**;主标题为**柔阴影暖白字**(非粗描边),字体优先思源黑体 Bold左上角双圈 Soul 标。**封面文案**优先 `hook_3sec`(见 `pick_cover_hook_text`)。成片文件名仍与 `highlights.title` 规则一致。
- **封面底层模糊(重要)****不要全屏强糊**。`soul_enhance.py` 默认 **`STYLE['cover']['bg_blur_mix']=0.1`**:清晰视频帧与一层高斯模糊按 **约 10% 混合**`bg_blur_radius` 生成模糊层),界面仍大致可辨,仅轻微虚化衬托文字。若需更强/更弱,改脚本内两常量,勿回到「整帧 radius=50+ 全糊」。
- **竖条封面取景 = 成片取景(重要)**:竖条模式(非 `--vertical-fit-full`)下,封面底图必须用与最终成片 **相同的 `--crop-vf`** 从横版切片抽帧,**禁止**用整幅 1920×1080 再压成竖条——否则分发/素材库里「封面」与「成片第一帧」不是同一条竖条,观感错位。脚本内由 `create_cover_image(..., cover_extract_vf=vf_use)` 保证与成片一致。
- **字幕****封面一结束即叠字幕**无额外「空几秒再等字」SRT 安全起点为封面结束 + **约 0.05s** epsilon避免与最后一帧封面打架。字幕**居中**在竖条内。先尝试**单次 FFmpeg 通道**(一次 pass 完成所有字幕叠加最快若失败自动回退到分批模式batch_size=40语助词在解析阶段已由 `clean_filler_words` 去除。重新加字幕时加 `--force-burn-subs`。⚠️ 注意:当前 FFmpeg 不支持 drawtext/subtitles 滤镜,只能用 PIL 图像 overlay 方案。(脚本常量:`SUBS_START_AFTER_COVER_SEC`**默认 0.0**
- **字幕字形**Whisper 词级轴常在**中日文之间插空格**,逐字/逐词显字时会像「字与字被撑开」;脚本在 `improve_subtitle_punctuation` 路径对 **CJK 相邻空白**做折叠(`_collapse_cjk_interchar_spaces`),保证整句显示正常、无异常中空。
- **封面标题**:高光 `title` 建议 **46 个汉字**;成片内封面主标题最多显示 **6 个汉字**(超长由 `soul_enhance` 自动截断,与文件名 `--title-only` 一致)。

View File

@@ -10,17 +10,18 @@
---
## 二、提问→回答 结构
## 二、提问→回答 结构(含:上麦项目+钱优先)
| 步骤 | 动作 | 说明 |
|------|------|------|
| 0 | **上麦是否在讲自己的项目+钱** | 若有「做什么项目 + 营收/融资/薪资/预算/API消耗」等**前3秒优先** `hook_3sec` = 一句话项目优质案例逻辑;`question` 可记主持人追问,不必挤掉 hook |
| 1 | 看片段是否有人提问 | 高光识别时判断:该段时间内是否有观众/连麦者提出问题 |
| 2 | 若有提问 | 提取**提问原文**(可去语助词、精简一句),填 `question``hook_3sec` 与之一致 |
| 3 | 前3秒 | 封面/前贴展示「提问的问题」,让用户先看到问题 |
| 4 | 正片 | 从回答开始,或「先问后答」完整保留,形成完整提问→回答链路 |
| 5 | 若无提问 | 前3秒用金句/悬念/数据等 hook_3sec无 question 字段 |
| 2 | 若有提问且未执行第0步 | 提取**提问原文**(可去语助词、精简一句),填 `question``hook_3sec` 与之一致 |
| 3 | 前3秒 | 封面/前贴展示 **hook_3sec**(项目+钱 或 提问) |
| 4 | 正片 | 从回答开始,或「先问后答」完整保留,形成完整链路 |
| 5 | 若无提问且无上麦项目句 | 前3秒用金句/悬念/数据等 hook_3sec无 question 字段 |
**目的**:用户先被「问题」抓住,再听回答,完播率更高;提问与回答中的语助词在成片阶段**完整去除**。
**目的**:用户前3秒先看到「这人项目多硬/钱的故事」或「问题」,再听展开;语助词在成片阶段**完整去除**。
---

View File

@@ -49,17 +49,30 @@
- 吊人胃口的开场
- "接下来这段更精彩"
# 提问→回答 结构(每个话题前均优先
# 上麦优质项目 → 前3秒优先高于泛提问
**每个话题/高光片段均优先采用提问→回答形式**:先展示**提问**前3秒再进入**回答**(正片)。若片段里有人提问,必须把**提问的问题**放到前3秒。
当高光段以**连麦者/嘉宾自述**为主,且出现以下任一,**优先**把前3秒留给「项目+钱/阶段」摘要:
- **项目身份**在做什么、给谁用、OPC/社群/代运营/孵化/产品名等可一句话说清的业态;
- **钱与规模**:月收入、营收、融资/投资/条款/到账、定价、预算、岗位薪资区间、每月烧 API/投流金额等**具体数字或档位**。
**写法**`hook_3sec` 用一句 **1224 字内**的「优质上麦案例」钩子(项目 + 钱/阶段例如「北京AI项目刚拿机构投资」「人事总助写一块招不到人」。
仍可在 JSON 里填 `question` 记录主持人后面的追问,但 **封面/前3秒优先烧 `hook_3sec`**`pick_cover_hook_text` 已优先 hook
**仅当本段没有清晰项目/钱信息时**,再完全沿用下方「提问→回答」规则。
---
# 提问→回答 结构(每个话题前均优先 — 与上条叠加时)
**默认**:每个话题/高光片段采用提问→回答形式:先展示**提问**前3秒再进入**回答**(正片)。**若已触发上条「项目+钱优先」**前3秒用 hook正片内再问再答。
1. **判断**:看该高光片段文字稿中是否有人提问(观众/连麦者问的问题)。
2. **若有提问**
2. **若有提问且未用「项目+钱」占前3秒**
- 提取**提问的原文**(可去语助词、精简为一句),填入 `question` 字段;
- `hook_3sec` 优先用该提问内容(与 question 一致成片前3秒封面/贴片展示「问题」,正片从回答开始或先问后答。
3. **若无提问**`hook_3sec` 用金句/悬念/数据等抓眼球开场,`question` 可省略。
**目的**前3秒让用户看到「问题」再听回答形成完整**提问→回答**链路;提问与回答部分的语助词在成片时完整去除。
**目的**前3秒让用户看到「问题」或「项目+钱」,再听回答,形成完整链路;语助词在成片时完整去除。
# 前3秒Hook设计原则

View File

@@ -0,0 +1,10 @@
[
{"title": "百条分发怎么铺", "start_time": "00:10:28", "end_time": "00:13:10", "hook_3sec": "十条乘十平台一天百条分发", "question": "多平台分发怎么测?", "cta_ending": "今天就到这里,点个关注下次不迷路", "transcript_excerpt": "内容产出与一键分发", "reason": "项目_分发规模+数字"},
{"title": "派对百二十场价值", "start_time": "00:32:10", "end_time": "00:34:55", "hook_3sec": "派对开到一百二十场图啥", "question": "坚持百场背后价值?", "cta_ending": "今天就到这里,点个关注下次不迷路", "transcript_excerpt": "方向与执行", "reason": "项目_派对复盘"},
{"title": "老板群九十五人链", "start_time": "00:44:38", "end_time": "00:47:15", "hook_3sec": "一个群能挤九十五个老板", "question": "怎么进高质量群?", "cta_ending": "今天就到这里,点个关注下次不迷路", "transcript_excerpt": "老板群与私域", "reason": "项目_资源链接"},
{"title": "AI切片背后人力", "start_time": "00:51:14", "end_time": "00:54:00", "hook_3sec": "磨AI和token几十人在扛", "question": "简单按钮背后多少成本?", "cta_ending": "今天就到这里,点个关注下次不迷路", "transcript_excerpt": "研发运营与token", "reason": "项目+钱_成本"},
{"title": "ENTP孩子与AI教育", "start_time": "01:15:52", "end_time": "01:18:45", "hook_3sec": "ENTP爱表现是啥产业人格", "question": "孩子性格怎么接AI", "cta_ending": "今天就到这里,点个关注下次不迷路", "transcript_excerpt": "ENTP与表现欲", "reason": "上麦案例_人格+教育"},
{"title": "派对场观五到十万", "start_time": "01:42:15", "end_time": "01:45:05", "hook_3sec": "派对一天场观五到十万", "question": "曝光和进房怎么算?", "cta_ending": "今天就到这里,点个关注下次不迷路", "transcript_excerpt": "场观与深链", "reason": "月营收级_曝光数字"},
{"title": "特步模型投资流水", "start_time": "02:21:38", "end_time": "02:25:45", "hook_3sec": "七千店复制拿投资月流水五十万", "question": "大品牌合作怎么拆?", "cta_ending": "今天就到这里,点个关注下次不迷路", "transcript_excerpt": "7500店+50万流水干股", "reason": "优质项目_规模+月营收+投资"},
{"title": "创始人决定能谈成", "start_time": "02:32:03", "end_time": "02:35:00", "hook_3sec": "创始人硬才能谈特步营收", "question": "合作先看人还是模型?", "cta_ending": "今天就到这里,点个关注下次不迷路", "transcript_excerpt": "创始人与营收数据", "reason": "优质项目_创始人+营收"}
]

View File

@@ -2,7 +2,7 @@
"""
Soul切片增强脚本 v2.0
功能:
1. 封面贴片:高光 hook_3sec 优先(吸睛),竖屏底图为**清晰帧 + 约 10% 轻模糊混入**(非全糊)+ 冷色渐变;**顶栏单条 Soul 绿 + 底部电影感渐隐 + 细内框 + 柔阴影标题**(避免粗描边与多条绿边廉价感)
1. 封面贴片:高光 hook_3sec 优先(吸睛),竖屏底图为**清晰帧 + 约 10% 轻模糊混入**(非全糊)+ 冷色渐变;**顶栏单条 Soul 绿 + 底部电影感渐隐 + 细内框 + 柔阴影标题**(避免粗描边与多条绿边廉价感)。**竖条成片**:封面取帧须与最终成片同一 `-vf`(如 `crop=598:1080:493:0`),禁止用整幅 1920 横版再压成竖条(会与正片取景不一致)。
2. 烧录字幕(关键词高亮、可选逐字)
3. 切除检出的长静音并重映射字幕时间轴
4. 片尾 CTActa_ending字幕条
@@ -1060,8 +1060,18 @@ def _strip_cover_number_prefix(text):
return text.strip()
def create_cover_image(hook_text, width, height, output_path, video_path=None):
"""创建封面贴片。竖条:视频底 + 冷色渐变 + 底栏渐隐 + 顶栏 Soul 绿与细金线 + 内框 + 柔阴影标题 + 左上角标。"""
def create_cover_image(
hook_text,
width,
height,
output_path,
video_path=None,
cover_extract_vf=None,
):
"""创建封面贴片。竖条:视频底 + 冷色渐变 + 底栏渐隐 + 顶栏 Soul 绿与细金线 + 内框 + 柔阴影标题 + 左上角标。
cover_extract_vf竖条模式时与成片最终 `-vf` 一致(如 crop 竖条),从横版切片中截取与成片同取景的底图;不传则仍用整幅帧拉伸(易与成片不一致,仅兼容旧行为)。
"""
hook_text = _to_simplified(str(hook_text or "").strip())
hook_text = _strip_cover_number_prefix(hook_text)
if not hook_text:
@@ -1075,25 +1085,26 @@ def create_cover_image(hook_text, width, height, output_path, video_path=None):
base = Image.new("RGBA", (width, height), (*VERTICAL_COVER_TOP, 255))
if video_path and os.path.exists(video_path):
temp_frame = output_path.replace(".png", "_vframe.jpg")
subprocess.run(
[
"ffmpeg",
"-y",
"-ss",
"0.35",
"-i",
video_path,
"-vframes",
"1",
"-q:v",
"3",
temp_frame,
],
capture_output=True,
)
vf = (cover_extract_vf or "").strip()
cmd = [
"ffmpeg",
"-y",
"-ss",
"0.35",
"-i",
video_path,
]
if vf:
cmd.extend(["-vf", vf])
cmd.extend(["-vframes", "1", "-q:v", "3", temp_frame])
subprocess.run(cmd, capture_output=True)
if os.path.exists(temp_frame):
try:
sharp = Image.open(temp_frame).convert("RGBA").resize((width, height))
sharp = Image.open(temp_frame).convert("RGBA")
if sharp.size != (width, height):
sharp = sharp.resize(
(width, height), Image.Resampling.LANCZOS
)
mix = float(style.get("bg_blur_mix", 0.10))
r = float(style.get("bg_blur_radius", 14))
if mix > 0.001:
@@ -1785,10 +1796,15 @@ def enhance_clip(clip_path, output_path, highlight_info, temp_dir, transcript_pa
flush=True,
)
# 3. 封面图(与去静音后首帧一致)
# 3. 封面图(与去静音后首帧一致;竖条须与成片同 crop-vf 取底图,勿整幅横版压竖条
print(f" [1/5] 封面生成中…", flush=True)
cover_img = os.path.join(temp_dir, "cover.png")
create_cover_image(hook_text, out_w, out_h, cover_img, working_clip)
cover_extract_vf = None
if vertical and not vertical_fit_full:
cover_extract_vf = vf_use
create_cover_image(
hook_text, out_w, out_h, cover_img, working_clip, cover_extract_vf=cover_extract_vf
)
print(f" ✓ 封面生成", flush=True)
if subtitles:
@@ -1964,8 +1980,10 @@ def enhance_clip(clip_path, output_path, highlight_info, temp_dir, transcript_pa
# 5.4 输出:竖条(宽由 vf或全画面 letterbox
if vertical and not vertical_fit_full:
print(f" [5/5] 竖屏输出({out_w}×{out_h})…", flush=True)
elif vertical and vertical_fit_full:
print(f" [5/5] 竖屏输出(全画面 letterbox", flush=True)
else:
print(f" [5/5] 竖屏输出", flush=True)
print(f" [5/5] 横版输出(未开竖屏)", flush=True)
if vertical and vertical_fit_full:
r = subprocess.run([
'ffmpeg', '-y', '-i', current_video,
@@ -2065,14 +2083,22 @@ def main():
vertical = getattr(args, 'vertical', False)
title_only = getattr(args, 'title_only', False)
print("="*60)
print("🎬 Soul切片增强" + ("(成片竖屏直出)" if vertical else ""))
print("="*60)
crop_vf_arg = (getattr(args, "crop_vf", "") or "").strip()
overlay_x_arg = getattr(args, "overlay_x", -1)
overlay_x_arg = None if overlay_x_arg < 0 else overlay_x_arg
typewriter = getattr(args, "typewriter_subs", False)
vfit = getattr(args, "vertical_fit_full", False)
# Soul 成片:--title-only 或塑形相关参数时默认竖屏直出,避免只传 --crop-vf 却漏 --vertical 误出 1920×1080 横版
if not vertical and (title_only or crop_vf_arg or vfit):
vertical = True
print(
" 已默认启用竖屏直出(因 --title-only 和/或 --crop-vf / --vertical-fit-full"
"显式加 --vertical 亦同。",
flush=True,
)
print("="*60)
print("🎬 Soul切片增强" + ("(成片竖屏直出)" if vertical else ""))
print("="*60)
print(
f"功能: 封面+字幕+加速10%+去语气词"
+ ("+去长静音" if not getattr(args, "no_trim_silence", False) else "")

View File

@@ -17,6 +17,10 @@
微信**未开放**「用开放平台 API 直接上传/发布短视频」;短视频自动化目前只可走 **A 轨**
**B 轨**负责经营数据、带货、留资、直播场次等**合规官方能力**
**无法规避助手 Cookie 的原因(结论):** 官方接口列表页当前**无**「上传视频 / 发表动态 / 创建短视频」路径;社区答复亦明确**不支持**接口直发视频号 Feed。
**缓解重复扫码(工程侧)**`channels_login.py` 默认启用 **Playwright 持久化用户目录**`~/.soul-channels-playwright-profile`),同机同账号在腾讯会话有效期内通常**无需每次扫码**;禁用:`CHANNELS_PERSISTENT_LOGIN=0``--no-persistent`
占位与自检脚本:`脚本/channels_open_platform_publish.py`
---
## 二、助手后台「开放能力」能联想到的产品动作(想象力清单)

View File

@@ -12,18 +12,19 @@ updated: "2026-03-23"
# 视频号发布 Skillv3.0
> **核心能力**纯 APIhttpx视频号发布零 Playwright 依赖
> **核心能力**发布链路纯 **httpx****登录**阶段用 Playwright默认持久化 Chromium减少重复扫码
> **实测**120 场 12 条切片全部 API 直发成功,单条 5~9 秒。
> **去重**:基于 publish_log.jsonl同一视频不重复发。
> **Cookie 有效期**~24-48h通过 channels_login.py 扫码刷新。
> **Cookie 有效期**~24-48h过期需刷新。`channels_login.py` 默认 **持久化 Chromium 用户目录**`~/.soul-channels-playwright-profile`),同机同账号在腾讯会话未失效时**通常不必每次扫码**`CHANNELS_PERSISTENT_LOGIN=0` 或 `--no-persistent` 可关。
> **开放平台 access_token****不能**替代助手 Cookie 发短视频(官方助手 API 列表无上传发表接口),见 `REFERENCE_开放能力_数据与集成.md` 与 `脚本/channels_open_platform_publish.py`。
---
## 一、纯 API 完整流程5 步)
```
[Step 1] Cookie 认证(一次性
Playwright 微信扫码 → channels_storage_state.json
[Step 1] Cookie 认证(助手态
Playwright(默认持久化 profile减少重复扫码→ channels_storage_state.json
登录地址: https://channels.weixin.qq.com/login
获取: cookies, localStorage (_finger_print_device_id, __ml::aid)

View File

@@ -15,6 +15,7 @@ import asyncio
import hashlib
import json
import math
import os
import random
import subprocess
import sys
@@ -31,6 +32,7 @@ VIDEO_DIR = Path("/Users/karuo/Movies/soul视频/soul_派对_121场_20260311_out
sys.path.insert(0, str(SCRIPT_DIR.parent.parent / "多平台分发" / "脚本"))
from publish_result import PublishResult, is_published, save_results, print_summary
from schedule_generator import generate_smart_schedule
UA = (
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
@@ -265,6 +267,19 @@ def _gen_task_id() -> str:
return str(ts_ms * 10**7 + rand)
def _payload_raw_key_buff(finder_raw: str | None):
"""localStorage finder_raw → post_create / clip 的 rawKeyBuff字符串或已解析 JSON"""
if not finder_raw or not str(finder_raw).strip():
return None
s = str(finder_raw).strip()
if (s.startswith("{") and s.endswith("}")) or (s.startswith("[") and s.endswith("]")):
try:
return json.loads(s)
except json.JSONDecodeError:
pass
return s
def _micro_headers(cookie_str: str, uin: str, finger_print: str = "") -> dict:
h = {
"Cookie": cookie_str,
@@ -405,8 +420,10 @@ async def create_post(
upload_cost: int,
scheduled_ts: int = 0,
original: bool = True,
finder_raw: str | None = None,
) -> dict:
now_ts = int(time.time())
rkb = _payload_raw_key_buff(finder_raw)
payload = {
"objectType": 0,
@@ -460,7 +477,7 @@ async def create_post(
"timestamp": str(int(time.time() * 1000)),
"_log_finder_uin": "",
"_log_finder_id": finder_id,
"rawKeyBuff": None,
"rawKeyBuff": rkb,
"pluginSessionId": None,
"scene": 7,
"reqScene": 7,
@@ -469,6 +486,13 @@ async def create_post(
if scheduled_ts > 0:
payload["postTimingInfo"] = {"timing": 1, "postTime": scheduled_ts}
if not rkb:
print(
" [!] 警告: Cookie 文件缺少 finder_rawpost_create 可能返回 300002"
"请用 channels_login 完整登录(勿提前关浏览器)。",
flush=True,
)
# 尝试 CGI_PREFIX标准助手端点和 MICRO_PREFIX 两个路径
for prefix, referer in [
(CGI_PREFIX, "https://channels.weixin.qq.com/platform/post/create"),
@@ -517,6 +541,7 @@ async def publish_one(
idx: int,
total: int,
scheduled_ts: int = 0,
finder_raw: str | None = None,
) -> PublishResult:
fname = Path(video_path).name
real_path = Path(video_path).resolve()
@@ -611,6 +636,7 @@ async def publish_one(
vinfo, fsize, finder_id, uin, finger_print, aid,
clip_key, draft_id, upload_cost,
scheduled_ts=scheduled_ts, original=True,
finder_raw=finder_raw,
)
elapsed = time.time() - t0
@@ -680,6 +706,19 @@ async def _ensure_ctx() -> dict:
if not up_params:
raise RuntimeError("获取 upload_params 失败")
finder_raw_val = ls.get("finder_raw") or ""
if isinstance(finder_raw_val, str):
finder_raw_val = finder_raw_val.strip()
else:
finder_raw_val = str(finder_raw_val or "").strip()
if not finder_raw_val:
raise RuntimeError(
"localStorage 缺少 finder_rawrawKeyBuffpost_create 会报 300002。"
"请运行: python3 视频号发布/脚本/channels_login.py --playwright-only "
"扫码登录后勿关 Chromium直至终端出现「rawKeyBuff 已就绪」或「Cookie 已保存」。"
)
_ctx.update({
"ready": True,
"cookie_str": cookie_str,
@@ -688,6 +727,7 @@ async def _ensure_ctx() -> dict:
"finger_print": finger_print,
"aid": aid,
"up_params": up_params,
"finder_raw": finder_raw_val or None,
})
return _ctx
@@ -724,6 +764,7 @@ async def publish_one_compat(
ctx["uin"], ctx["finger_print"], ctx["aid"],
ctx["up_params"], video_path, title, idx, total,
scheduled_ts=sched_ts,
finder_raw=ctx.get("finder_raw"),
)
if result.success:
new_p = await get_upload_params(ctx["cookie_str"], ctx["finder_id"])
@@ -736,7 +777,18 @@ async def publish_one_compat(
# Main
# ---------------------------------------------------------------------------
def _run_login_then_retry():
def _env_truthy(name: str) -> bool:
return os.environ.get(name, "").strip().lower() in ("1", "true", "yes")
def _channels_auto_login_allowed() -> bool:
"""默认静默;仅 CHANNELS_AUTO_LOGIN=1 且未设 NO_AUTO_CHANNELS_LOGIN 时才允许自动弹窗登录。"""
if _env_truthy("NO_AUTO_CHANNELS_LOGIN"):
return False
return _env_truthy("CHANNELS_AUTO_LOGIN")
def _run_login_then_retry() -> bool:
"""Cookie 无效时自动调起登录,写回 storage 后由调用方重试。"""
login_script = SCRIPT_DIR / "channels_login.py"
if not login_script.exists():
@@ -747,18 +799,18 @@ def _run_login_then_retry():
return COOKIE_FILE.exists() and COOKIE_FILE.stat().st_size > 100
def _gen_schedule(count: int) -> list[int]:
"""生成定时发布时间戳列表:第一条立即(0)后续30-120分钟递增"""
def _maybe_login_then_retry() -> bool:
if not _channels_auto_login_allowed():
return False
return _run_login_then_retry()
def _gen_schedule_unix(count: int) -> list[int]:
"""与 distribute_all 一致:智能错峰 datetime → 视频号定时 Unix近未来视为立即(0)。"""
if count <= 0:
return []
result = [0]
base = int(time.time())
accumulated = 0
for _ in range(1, count):
gap = random.randint(30, 120) * 60
accumulated += gap
result.append(base + accumulated)
return result
times = generate_smart_schedule(count)
return [_scheduled_ts_for_channels(dt) for dt in times]
async def main():
@@ -766,10 +818,14 @@ async def main():
state = load_state()
if not state:
if _run_login_then_retry():
if _maybe_login_then_retry():
state = load_state()
if not state:
print("[!] Cookie 文件不存在且登录未完成", flush=True)
print(
"[!] Cookie 文件不存在。默认静默:请先手动执行同目录 channels_login.py"
"或设置 CHANNELS_AUTO_LOGIN=1 后再运行本脚本。",
flush=True,
)
return 1
cookie_str = get_cookie_str(state)
@@ -778,19 +834,22 @@ async def main():
aid = ls.get("__ml::aid", "").strip('"')
finger_print = ls.get("_finger_print_device_id", "")
if not aid:
if _run_login_then_retry():
if _maybe_login_then_retry():
state = load_state()
cookie_str = get_cookie_str(state)
ls = get_local_storage(state)
aid = ls.get("__ml::aid", "").strip('"')
finger_print = ls.get("_finger_print_device_id", "")
if not aid:
print("[!] localStorage 缺少 __ml::aid", flush=True)
print(
"[!] localStorage 缺少 __ml::aid助手态不完整。默认静默请手动 channels_login 或 CHANNELS_AUTO_LOGIN=1。",
flush=True,
)
return 1
auth = await auth_check(cookie_str)
if not auth:
if _run_login_then_retry():
if _maybe_login_then_retry():
state = load_state()
cookie_str = get_cookie_str(state)
ls = get_local_storage(state)
@@ -798,7 +857,10 @@ async def main():
finger_print = ls.get("_finger_print_device_id", "")
auth = await auth_check(cookie_str)
if not auth:
print("[!] Cookie 仍无效,请稍后重试发布", flush=True)
print(
"[!] Cookie 仍无效。默认静默:请手动 channels_login 或设置 CHANNELS_AUTO_LOGIN=1 后重试。",
flush=True,
)
return 1
fu = auth.get("finderUser", {})
@@ -823,7 +885,7 @@ async def main():
print("[OK] 全部已发布", flush=True)
return 0
schedule = _gen_schedule(len(need_pub))
schedule = _gen_schedule_unix(len(need_pub))
results = []
consecutive_fail = 0

View File

@@ -1,5 +1,8 @@
#!/usr/bin/env python3
"""视频号登录 v7 — 优先 Cursor 内置 Simple Browser 扫码;会话落盘优先 CDP 附着 Cursor失败再回退 Playwright。"""
"""视频号登录 v7 — Simple Browser / CDP / Playwright。
静默二维码:`CHANNELS_SILENT_QR=1` 或 `--silent-qr` → headless、不弹窗二维码写入 /tmp/channels_qr.png
stdout 打印 SOUL_QR_IMAGE_FOR_CHAT 标记路径,便于在 Cursor 对话中由助手展示给你扫。
"""
import asyncio
import json
import os
@@ -18,6 +21,10 @@ LOGIN_URL = "https://channels.weixin.qq.com/login"
QR_SCREENSHOT = Path("/tmp/channels_qr.png")
DEFAULT_CDP = os.environ.get("CHANNELS_CDP_URL", "http://127.0.0.1:9223")
# 持久化 Chromium同目录保留登录态显著减少重复扫码腾讯侧过期/风控时仍需重登)
PERSISTENT_PROFILE_DIR = Path(
os.environ.get("CHANNELS_CHROMIUM_USER_DATA", str(Path.home() / ".soul-channels-playwright-profile"))
)
UA = (
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
@@ -30,6 +37,11 @@ REQUIRED_LS_KEYS = [
]
def _ls_ready_for_publish(merged_items: dict) -> bool:
"""发表 API 需要 finder_raw仅有 __ml::aid 不够post_create 会 300002"""
return bool((merged_items.get("finder_raw") or "").strip())
def open_cursor_simple_browser(url: str) -> None:
"""在 Cursor 编辑器内打开 Simple Browser不唤起系统默认外部浏览器"""
enc = urllib.parse.quote(url, safe="")
@@ -74,6 +86,13 @@ async def extract_ls(page, keys):
return {}
def _use_persistent_chromium() -> bool:
if "--no-persistent" in sys.argv:
return False
v = os.environ.get("CHANNELS_PERSISTENT_LOGIN", "1").strip().lower()
return v not in ("0", "false", "no", "off")
async def wait_until_logged_in(page, label: str = "") -> bool:
prefix = f"[{label}] " if label else ""
for i in range(120):
@@ -117,20 +136,39 @@ async def save_session_from_context(context, page, ls_vals: dict) -> bool:
COOKIE_FILE.write_text(json.dumps(state, ensure_ascii=False, indent=2))
_sync_to_central_cookie_store()
cookies = await context.cookies()
cookie_dict = {c["name"]: c["value"] for c in cookies}
cookie_dict: dict[str, str] = {}
try:
cookies = await context.cookies()
cookie_dict = {c["name"]: c["value"] for c in cookies}
except Exception:
cookie_dict = {c["name"]: c["value"] for c in state.get("cookies", []) if c.get("name")}
page_url = ""
try:
page_url = page.url or ""
except Exception:
page_url = ""
token_data = {
"sessionid": cookie_dict.get("sessionid", ""),
"wxuin": cookie_dict.get("wxuin", ""),
"cookie_str": "; ".join(f'{c["name"]}={c["value"]}' for c in cookies),
"cookie_str": "; ".join(f"{k}={v}" for k, v in cookie_dict.items()),
"finder_raw": ls_vals.get("finder_raw", ""),
"finder_username": ls_vals.get("finder_username", ""),
"finder_uin": ls_vals.get("finder_uin", ""),
"finder_login_token": ls_vals.get("finder_login_token", ""),
"url": page.url,
"url": page_url,
}
TOKEN_FILE.write_text(json.dumps(token_data, ensure_ascii=False, indent=2))
return bool(ls_vals.get("finder_raw"))
ready = _ls_ready_for_publish(merged_items)
if not ready:
print(
"[!] 已写入 Cookie但 localStorage 仍缺 finder_rawrawKeyBuff"
"勿关窗口,继续等待直至本项为 ✓。",
flush=True,
)
return ready
except Exception as e:
print(f"[!] 保存会话异常: {e}", flush=True)
return False
@@ -210,72 +248,167 @@ async def try_capture_via_cdp(pw, cdp_url: str) -> bool:
return ok
async def capture_via_playwright_external() -> bool:
"""回退:独立 Chromium 窗口(仅当 CDP 不可用时)。"""
print("\n[i] 未使用 CDP → 将打开本机 Chromium 窗口仅用于写入 Cookie 文件(可扫码后尽快关闭)\n", flush=True)
ls_vals = {}
def _silent_qr_mode() -> bool:
return "--silent-qr" in sys.argv or os.environ.get("CHANNELS_SILENT_QR", "").strip().lower() in (
"1",
"true",
"yes",
)
def _print_qr_for_chat_dialog() -> None:
""" stdout 标记:便于把路径复制到对话,由助手 Read 图片展示给你扫。"""
p = QR_SCREENSHOT.resolve()
print("\n========== SOUL_QR_IMAGE_FOR_CHAT ==========", flush=True)
print(str(p), flush=True)
print(
"→ 用微信扫视频号登录码:在 Cursor 里打开上述路径图片,或把路径发给助手展示。",
flush=True,
)
print("========== END_SOUL_QR_MARKER ==========\n", flush=True)
async def _assistant_login_flow_save(context, page) -> bool:
"""在已有 context/page 上完成:打开登录页 → 等跳转 → 抽 localStorage → 写 COOKIE_FILE。"""
ls_vals: dict = {}
await page.goto(LOGIN_URL, timeout=60000)
await asyncio.sleep(3)
await page.screenshot(path=str(QR_SCREENSHOT))
print(f"[QR] 二维码截图: {QR_SCREENSHOT}", flush=True)
if _silent_qr_mode():
_print_qr_for_chat_dialog()
if not await wait_until_logged_in(page, "Playwright"):
return False
print(
"[!] 请勿立即关闭 Chromium须等脚本写入 Cookie"
"看到「Cookie 已保存」或本进程退出后再关窗。",
flush=True,
)
print("[i] 登录成功2 秒后先落盘一轮会话(防止提前关窗导致 Cookie 未写入)…", flush=True)
await asyncio.sleep(2)
ls_vals = await extract_ls(page, REQUIRED_LS_KEYS)
snapshot_ready = await save_session_from_context(context, page, ls_vals)
if snapshot_ready:
print("[✓] 早期快照已含上传所需 localStorage 关键项", flush=True)
elif COOKIE_FILE.exists():
print("[i] 已写入部分会话,将继续等待 rawKeyBuff / __ml::aid 补全…", flush=True)
print("[i] 等待平台 JS 加载完成...", flush=True)
await asyncio.sleep(10)
for attempt in range(60):
try:
_ = page.url
except Exception:
print("[!] 页面或浏览器已关闭,使用此前快照结束。", flush=True)
return snapshot_ready
ls_vals = await extract_ls(page, REQUIRED_LS_KEYS)
if ls_vals.get("finder_raw"):
print(f"[✓] rawKeyBuff 已就绪 (等待 {attempt}s)", flush=True)
break
await asyncio.sleep(1)
if attempt % 15 == 14:
print(f" 等待 localStorage 写入... ({attempt + 1}s)", flush=True)
if not ls_vals.get("finder_raw"):
print("[i] rawKeyBuff 未出现,尝试访问内容列表页...", flush=True)
try:
await page.goto(
"https://channels.weixin.qq.com/platform/post/list",
timeout=15000,
wait_until="domcontentloaded",
)
await asyncio.sleep(8)
for _ in range(30):
try:
_ = page.url
except Exception:
return snapshot_ready
ls_vals = await extract_ls(page, REQUIRED_LS_KEYS)
if ls_vals.get("finder_raw"):
print("[✓] 导航后 rawKeyBuff 已就绪", flush=True)
break
await asyncio.sleep(1)
except Exception as e:
print(f"[!] 导航异常: {e}", flush=True)
print("\n[i] localStorage 提取结果:", flush=True)
for k in REQUIRED_LS_KEYS:
v = ls_vals.get(k, "")
status = "" if v else ""
display = f"{v[:60]}..." if len(v) > 60 else (v or "(空)")
print(f" {status} {k}: {display}", flush=True)
final_ok = await save_session_from_context(context, page, ls_vals)
return final_ok or snapshot_ready
async def capture_via_playwright_external(*, headless: bool) -> bool:
"""CDP 不可用时:本机 Chromiumheadless=True 时不弹窗口(配合 CHANNELS_SILENT_QR 输出二维码路径)。"""
persistent = _use_persistent_chromium()
if persistent:
print(
f"\n[i] 持久化 Chromiumheadless={headless}),目录: {PERSISTENT_PROFILE_DIR}\n"
f" 关闭持久化CHANNELS_PERSISTENT_LOGIN=0 或 --no-persistent\n",
flush=True,
)
else:
print(f"\n[i] 临时 Chromium 会话headless={headless}\n", flush=True)
async with async_playwright() as pw:
browser = await pw.chromium.launch(headless=False)
if persistent:
PERSISTENT_PROFILE_DIR.mkdir(parents=True, exist_ok=True)
context = await pw.chromium.launch_persistent_context(
str(PERSISTENT_PROFILE_DIR),
headless=headless,
user_agent=UA,
viewport={"width": 1280, "height": 720},
args=["--disable-blink-features=AutomationControlled"],
)
await context.add_init_script(
"Object.defineProperty(navigator,'webdriver',{get:()=>undefined})"
)
page = context.pages[0] if context.pages else await context.new_page()
try:
ok = await _assistant_login_flow_save(context, page)
finally:
await context.close()
return ok
browser = await pw.chromium.launch(
headless=headless,
args=["--disable-blink-features=AutomationControlled"],
)
context = await browser.new_context(user_agent=UA, viewport={"width": 1280, "height": 720})
await context.add_init_script(
"Object.defineProperty(navigator,'webdriver',{get:()=>undefined})"
)
page = await context.new_page()
await page.goto(LOGIN_URL, timeout=60000)
await asyncio.sleep(3)
await page.screenshot(path=str(QR_SCREENSHOT))
print(f"[QR] 二维码截图: {QR_SCREENSHOT}", flush=True)
if not await wait_until_logged_in(page, "Playwright"):
try:
ok = await _assistant_login_flow_save(context, page)
finally:
await browser.close()
return False
print("[i] 等待平台 JS 加载完成...", flush=True)
await asyncio.sleep(10)
for attempt in range(60):
ls_vals = await extract_ls(page, REQUIRED_LS_KEYS)
if ls_vals.get("finder_raw"):
print(f"[✓] rawKeyBuff 已就绪 (等待 {attempt}s)", flush=True)
break
await asyncio.sleep(1)
if attempt % 15 == 14:
print(f" 等待 localStorage 写入... ({attempt + 1}s)", flush=True)
if not ls_vals.get("finder_raw"):
print("[i] rawKeyBuff 未出现,尝试访问内容列表页...", flush=True)
try:
await page.goto(
"https://channels.weixin.qq.com/platform/post/list",
timeout=15000,
wait_until="domcontentloaded",
)
await asyncio.sleep(8)
for _ in range(30):
ls_vals = await extract_ls(page, REQUIRED_LS_KEYS)
if ls_vals.get("finder_raw"):
print("[✓] 导航后 rawKeyBuff 已就绪", flush=True)
break
await asyncio.sleep(1)
except Exception as e:
print(f"[!] 导航异常: {e}", flush=True)
print("\n[i] localStorage 提取结果:", flush=True)
for k in REQUIRED_LS_KEYS:
v = ls_vals.get(k, "")
status = "" if v else ""
display = f"{v[:60]}..." if len(v) > 60 else (v or "(空)")
print(f" {status} {k}: {display}", flush=True)
ok = await save_session_from_context(context, page, ls_vals)
await browser.close()
return ok
async def main() -> int:
# --playwright-only直接弹 Chromium 窗口扫码(与 headless / Simple Browser 分流)
force_pw = "--playwright-only" in sys.argv or os.environ.get("CHANNELS_LOGIN_PLAYWRIGHT_ONLY")
skip_cursor_tab = "--no-cursor-tab" in sys.argv
cdp_url = DEFAULT_CDP
silent_qr = _silent_qr_mode()
if silent_qr:
print(
"[i] 静默扫码模式CHANNELS_SILENT_QR=1 或 --silent-qr"
"不打开 Simple Browser / 不弹 Chromium 窗口,二维码见标记路径。\n",
flush=True,
)
ok = await capture_via_playwright_external(headless=True)
print(f"\n[{'' if ok else ''}] Cookie 保存: {COOKIE_FILE}", flush=True)
return 0 if ok else 1
if not force_pw:
if not skip_cursor_tab:
@@ -303,7 +436,7 @@ async def main() -> int:
print(f"\n[✓] Cookie 已保存: {COOKIE_FILE}", flush=True)
return 0
ok = await capture_via_playwright_external()
ok = await capture_via_playwright_external(headless=False)
print(f"\n[{'' if ok else ''}] Cookie 保存: {COOKIE_FILE}", flush=True)
return 0 if ok else 1

View File

@@ -0,0 +1,40 @@
#!/usr/bin/env python3
"""
视频号「开放平台 access_token」能否上传发表短视频——不能截至官方文档现状
官方「视频号助手 API」列表
https://developers.weixin.qq.com/doc/channels/api/channels/index.html
仅含:直播记录/预约、橱窗、留资、大屏、罗盘等,**无**短视频上传/发表接口。
因此:
- 无法用 AppID/AppSecret 换得的 access_token 替代 channels_storage_state.json助手 Cookie + localStorage
- 自动化发片仍须走 channels_api_publish.py助手 Web 协议)
若微信未来在文档中新增「内容上传」类接口,请在此模块实现并与 distribute_all 对接。
缓解重复扫码:见 channels_login.py 持久化 ChromiumCHANNELS_PERSISTENT_LOGIN 默认开启)。
"""
from __future__ import annotations
class OpenPlatformShortVideoNotSupported(RuntimeError):
"""微信未开放助手 API + access_token 的短视频发表能力。"""
DOC = "https://developers.weixin.qq.com/doc/channels/api/channels/index.html"
LOCAL = "视频号发布/REFERENCE_开放能力_数据与集成.md"
def assert_can_publish_without_assistant_session() -> None:
"""占位:若误调用「无 Cookie 发片」入口,立即给出明确错误。"""
raise OpenPlatformShortVideoNotSupported(
"微信未提供「仅用 access_token、无需助手 Cookie」的短视频上传/发表官方接口;"
f"{OpenPlatformShortVideoNotSupported.DOC} 与仓库内 {OpenPlatformShortVideoNotSupported.LOCAL}"
)
if __name__ == "__main__":
try:
assert_can_publish_without_assistant_session()
except OpenPlatformShortVideoNotSupported as e:
print(e, flush=True)
raise SystemExit(2) from e

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,7 @@
{
"sessionid": "BgAAO0pkRXR1Cnpkg8qKYYMziAr6J6R%2FMVqC2Jat0R1fmRXawRqzqlxIdOy47RaOqNJ8YCfDBsY9VFSx0UMt4JmiGxqBL6Wa6v0LDXbmykU%3D",
"wxuin": "1519919758",
"cookie_str": "sessionid=BgAAO0pkRXR1Cnpkg8qKYYMziAr6J6R%2FMVqC2Jat0R1fmRXawRqzqlxIdOy47RaOqNJ8YCfDBsY9VFSx0UMt4JmiGxqBL6Wa6v0LDXbmykU%3D; wxuin=1519919758",
"sessionid": "BgAANwiqhAKk98isXZLPiw2icF1oS7Vpofo9%2FFNByPRqbhQJqyNGTUHji1WoozeYLHGzcAKaWZZYCf2AF88%2FpVZsF3XuTF9TVnLk%2Bi9kXac%3D",
"wxuin": "1187669785",
"cookie_str": "sessionid=BgAANwiqhAKk98isXZLPiw2icF1oS7Vpofo9%2FFNByPRqbhQJqyNGTUHji1WoozeYLHGzcAKaWZZYCf2AF88%2FpVZsF3XuTF9TVnLk%2Bi9kXac%3D; wxuin=1187669785",
"finder_raw": "",
"finder_username": "v2_060000231003b20faec8c5e48919cbd5cb05e53db077dd1924028a806c10cffd891eb5a80ce7@finder",
"finder_uin": "",

View File

@@ -421,3 +421,4 @@
| 2026-03-22 13:23:40 | 🔄 卡若AI 同步 2026-03-22 13:23 | 更新:金仓、运营中枢参考资料、运营中枢工作台 | 排除 >20MB: 11 个 |
| 2026-03-22 14:38:11 | 🔄 卡若AI 同步 2026-03-22 14:38 | 更新Cursor规则、金仓、总索引与入口、运营中枢参考资料、运营中枢工作台 | 排除 >20MB: 11 个 |
| 2026-03-22 21:22:06 | 🔄 卡若AI 同步 2026-03-22 21:22 | 更新Cursor规则、金仓、卡木、总索引与入口、运营中枢工作台 | 排除 >20MB: 11 个 |
| 2026-03-23 09:48:42 | [强制] 🔄 卡若AI 同步 2026-03-23 09:48 | 更新Cursor规则、金仓、水桥平台对接、卡木、火炬、运营中枢、运营中枢参考资料、运营中枢工作台 | 排除 >20MB: 11 个 |

View File

@@ -424,3 +424,4 @@
| 2026-03-22 13:23:40 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-22 13:23 | 更新:金仓、运营中枢参考资料、运营中枢工作台 | 排除 >20MB: 11 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
| 2026-03-22 14:38:11 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-22 14:38 | 更新Cursor规则、金仓、总索引与入口、运营中枢参考资料、运营中枢工作台 | 排除 >20MB: 11 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
| 2026-03-22 21:22:06 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-22 21:22 | 更新Cursor规则、金仓、卡木、总索引与入口、运营中枢工作台 | 排除 >20MB: 11 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
| 2026-03-23 09:48:42 | 成功(强制) | 成功 | 🔄 卡若AI 同步 2026-03-23 09:48 | 更新Cursor规则、金仓、水桥平台对接、卡木、火炬、运营中枢、运营中枢参考资料、运营中枢工作台 | 排除 >20MB: 11 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |