🔄 卡若AI 同步 2026-03-23 09:48 | 更新:Cursor规则、金仓、水桥平台对接、卡木、火炬、运营中枢、运营中枢参考资料、运营中枢工作台 | 排除 >20MB: 11 个

This commit is contained in:
2026-03-23 09:48:28 +08:00
parent a5060ae4a8
commit 0ef2212160
32 changed files with 1285 additions and 269 deletions

View File

@@ -37,7 +37,8 @@ alwaysApply: true
- **每日对话收集**(每天仅一次):检查 `last_chat_collect_date.txt` → 非今日则执行 `python collect_chat_daily.py`
- **Gitea 同步**:对话结束前有文件变更时执行 `bash 自动同步.sh`(单文件 >20MB 不提交)
- **飞书复盘发群**:对话完成后,执行 `python3 send_review_to_feishu_webhook.py "简洁复盘"`≤500 字)
- **飞书复盘发群(默认关闭)****不**在每次对话结束时自动执行 `send_review_to_feishu_webhook.py`。仅当**同时**满足:① **捆绑明确**——用户本轮**明说**要发(如「复盘发飞书」「发群」),或**正在执行的某条 `SKILL.md` 步骤**写明本步须发飞书复盘Agent 须在执行前在正文点明:**依据哪份 Skill、哪一步**);② **工作区绑定**——本轮主任务落在 **`/Users/karuo/Documents/个人/卡若AI`** 仓库内(多根工作区时以**当前对话主要改动的仓库**为准主战场不是卡若AI 则**不发**,避免误推)。满足后再执行:
`python3 "/Users/karuo/Documents/个人/卡若AI/02_卡人/水桥_平台对接/飞书管理/脚本/send_review_to_feishu_webhook.py" "简洁复盘"`≤500 字)
- **终端命令**一律直接执行不询问50 字内说明后执行
- **常规操作**:优先命令行 + 复用现成流程,不提问
- **默认零提问(强制)**:开发、改需求、跑脚本、查日志、部署类任务,**禁止**向卡若发起「是否执行」「要不要我…」「请选一个」等确认式提问。缺信息时:**先读仓库配置 / 代码 / 环境变量 / 文档** → 合理默认 → **直接做完**。仅当 **客观上无法代劳** 时极简说明缺什么(如:本机短信验证码、支付密码、用户明文密钥未配置、明确不可逆删除且规范要求人工确认)。

View File

@@ -40,7 +40,7 @@ Soul发到素材库、录屏文字、团队管理、运营分发、飞书写群
| Excel→飞书表格→日报图 | `02_卡人/水桥_平台对接/飞书管理/Excel表格与日报_SKILL.md` | W13 |
| 飞书 JSON 块格式 | `02_卡人/水桥_平台对接/飞书管理/飞书JSON格式_SKILL.md` | W16 |
| 卡猫 / 婼瑄复盘发群 | `02_卡人/水桥_平台对接/飞书管理/卡猫复盘/SKILL.md` | W14 |
| 复盘 webhook 脚本 | `02_卡人/水桥_平台对接/飞书管理/脚本/send_review_to_feishu_webhook.py` | |
| 复盘 webhook 脚本 | `02_卡人/水桥_平台对接/飞书管理/脚本/send_review_to_feishu_webhook.py` | 按需;对话结束**不**默认调用,见 `karuo-ai.mdc` |
| Gitea / Git 推送 | `01_卡资/金仓_存储备份/Gitea管理/SKILL.md` | G02 |
| 语音转写纠错(闽南口音等) | `02_卡人/水溪_整理归档/语音转写纠错/SKILL.md` | W03b |
| 切片脚本 soul_enhance | `03_卡木/木叶_视频内容/视频切片/脚本/soul_enhance.py` | 联动 M01 |

View File

@@ -1,7 +1,39 @@
{
"updated": "2026-03-22T13:21:51.214081+00:00",
"updated": "2026-03-23T01:48:25.039999+00:00",
"conversations": [
{
"对话ID": "98abcf81-b237-44b7-af5a-dfe6d5bcb0ed",
"名称": "卡路亚复盘推送功能讨论",
"项目": "飞书",
"首条消息": "卡路亚不默认把复盘推到指定的群内。不强制,每一次对话都推,现在先取消这个功能,那已推的一定是有捆绑的业务,捆绑确定清楚在这个工作区才推",
"创建时间": "2026-03-23T01:46:48.301000+00:00",
"消息数量": 43
},
{
"对话ID": "8af5b60d-31d1-4486-b8e3-4f59cff74c8d",
"名称": "这个是视频号的那个直播的视频号直播的那个接口。直播的这个接口看一下这个直播的接口有哪一些我可以直接直播接口的开放能力看一下这个开放能力具体能做些什么事情以及咱们在咱们这个项目里面在视频号smart 这个项目里面能做哪一些操作?",
"项目": "微信管理",
"首条消息": "这个是视频号的那个直播的视频号直播的那个接口。直播的这个接口看一下这个直播的接口有哪一些我可以直接直播接口的开放能力看一下这个开放能力具体能做些什么事情以及咱们在咱们这个项目里面在视频号smart 这个项目里面能做哪一些操作?",
"创建时间": "2026-03-23T01:14:52.624000+00:00",
"消息数量": 81
},
{
"对话ID": "6a7c9cba-36ab-423a-a3a4-178da1908343",
"名称": "微信豆充值网页发一个给我",
"项目": "微信管理",
"首条消息": "微信豆充值网页发一个给我",
"创建时间": "2026-03-23T00:27:19.558000+00:00",
"消息数量": 22
},
{
"对话ID": "d932a36a-3023-425a-971b-bddaf36bebd8",
"名称": "soul 身份证注销 现在一张身份证只能使用3个帮我看一下注销的一个形式我的是我的号还被封了怎么样解决这个无法登录、无法注销怎么操作",
"项目": "Soul创业",
"首条消息": "soul 身份证注销 现在一张身份证只能使用3个帮我看一下注销的一个形式我的是我的号还被封了怎么样解决这个无法登录、无法注销怎么操作",
"创建时间": "2026-03-22T21:31:52.443000+00:00",
"消息数量": 10
},
{
"对话ID": "9f39025b-f695-4d7b-aff7-c124226e307e",
"名称": "Exploration of soul project codebase",
"项目": "Soul创业",

View File

@@ -1,16 +1,16 @@
---
name: 复盘总结发飞书群
description: 每次对话完成后将简洁复盘总结发到指定飞书群webhook长对话必发每次完成的任务都发。卡若AI 强制规则之一
description: 按需将简洁复盘发到飞书群webhook。默认不自动发用户明说「复盘发飞书」或某 SKILL 步骤书面要求时才发且须以卡若AI 工作区为主战场
triggers: 复盘发飞书、飞书复盘、对话总结发群、复盘总结发群、FEISHU_REVIEW_WEBHOOK
owner: 水桥
group: 水
version: "1.0"
updated: "2026-03-12"
version: "1.1"
updated: "2026-03-23"
---
# 复盘总结发飞书群(水桥)
> 对话结束、复盘写完后,将简洁复盘总结发到飞书群。**每次对话都发,长对话必发。**
> **默认不自动发。** 用户口令或**其他 Skill 步骤中写明须发**时才执行执行前在对话中注明依据Skill 名 + 步骤)。
> **卡罗拉 = 卡若AI**:飞书文案里两种叫法均可,与《卡若复盘格式》五块一致即可。
---
@@ -22,10 +22,10 @@ updated: "2026-03-12"
---
## 规则(卡若AI 强制
## 规则(按需
- **时机**每次对话完成、完成「卡若复盘」后。
- **频率**每次对话完成都发;**长对话尤其必须发**
- **时机**:完成「卡若复盘」后,且满足 `.cursor/rules/karuo-ai.mdc` 中「捆绑明确 + 卡若AI 主工作区」
- **频率**非每轮;仅用户要求或绑定的 Skill 流程要求时
- **内容**:精简版复盘(不必五块全写,保留:时间、目标·结果·达成率、完成要点、下一步)。
---
@@ -52,8 +52,8 @@ python3 .../send_review_to_feishu_webhook.py --file /path/to/summary.txt
## 与其他规则的关系
- **Cursor 规则**`.cursor/rules/karuo-ai.mdc`「飞书复盘总结发群」— 对话结束后执行发送
- **对话沉淀与优化规则**`运营中枢/使用手册/对话沉淀与优化规则.md` 二、2.1 飞书复盘总结发群(必做项)
- **Cursor 规则**`.cursor/rules/karuo-ai.mdc`「飞书复盘发群(默认关闭)」
- **对话沉淀与优化规则**`运营中枢/使用手册/对话沉淀与优化规则.md` §2.1 按需
- **工作台说明**`运营中枢/工作台/飞书复盘总结发群说明.md`
---

View File

@@ -1,6 +1,6 @@
{
"access_token": "u-fVns2XR7hbJ9Yeux1CLUrAlh3I31ghipPwGaYB4026lV",
"refresh_token": "ur-cDDIZhXHR2Iqc9jmEk39hQlh3KbxghgpU0GaJx4023hI",
"access_token": "u-fNvjYaC8h9SWRJjTGmOKuulh3c3xghUjWMGaJA4023lE",
"refresh_token": "ur-eSq5MlNZ5d9GkVd9PbvCDGlh1A3xghUjrwGaUQ4027hE",
"name": "飞书用户",
"auth_time": "2026-03-21T15:26:20.093375"
"auth_time": "2026-03-23T08:28:12.167751"
}

View File

@@ -7,14 +7,21 @@ description: >
triggers: 多平台分发、一键分发、全平台发布、批量分发、视频分发
owner: 木叶
group: 木
version: "4.0"
updated: "2026-03-11"
version: "4.1"
updated: "2026-03-20"
---
# 多平台分发 Skillv4.0
# 多平台分发 Skillv4.1
> **核心原则**API 发布为主Playwright 为辅。确保确定性地分发到各平台。
> **v4.0 变更**:视频号已切换为纯 API、统一元数据生成器、定时排期优化、简介/标签/分区自动填充
> **v4.1 变更**:视频号 Cookie 双路径自动对齐;指定 `--platforms 视频号` 时 Cookie 失效自动调起扫码登录;登录完成即写入并同步中央存储;执行上**先直达目的**(跑命令、保存 Cookie、再发**不对用户反问**
## 〇、执行原则(第一性原理)
- **目标优先**:用户要「发到视频号 / 全平台」→ 直接执行 `distribute_all.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见脚本内说明
---
@@ -30,7 +37,7 @@ updated: "2026-03-11"
> **关于视频号官方 API 边界**
> 按《视频号与腾讯相关 API 整理》结论,微信官方目前**没有开放「短视频上传/发布」接口**;本 Skill 中的视频号发布能力,属于对 `https://channels.weixin.qq.com` 视频号助手网页协议的逆向封装DFS 上传 + `post_create`),仅在你本机使用,需自行承担协议变更与合规风险。
> 官方可控能力(直播记录、橱窗、留资、罗盘数据、本地生活等)的服务端 API 入口为:`https://developers.weixin.qq.com/doc/channels/api/`,如需做直播/橱窗/留资集成,可基于该文档在单独 Skill 中扩展
> 官方可控能力(直播记录、橱窗、留资、罗盘数据、本地生活等)的服务端 API 入口为:`https://developers.weixin.qq.com/doc/channels/api/`。**整合脑图与接口速查**见同木叶的 `视频号发布/REFERENCE_开放能力_数据与集成.md`;开放平台凭证约定见 `视频号发布/credentials/README.md``.env.open_platform`
---
@@ -54,6 +61,10 @@ python3 distribute_all.py --video-dir "/path/to/videos/"
# 检查 Cookie / 重试失败
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
```
---
@@ -71,7 +82,7 @@ python3 distribute_all.py --retry
| 平台 | 定时方式 | 参数 |
|------|----------|------|
| B站 | API `meta.dtime` | Unix 时间戳(秒) |
| 视频号 | API 暂不支持原生定时 | 描述中标注时间/手动设置 |
| 视频号 | API `postTimingInfo.postTime`(秒级 Unix首条若时间过近则立即发 | `channels_api_publish._scheduled_ts_for_channels` |
| 抖音 | API `timing_ts` | Unix 时间戳 |
| 快手 | Playwright UI | `schedule_helper.py` |
| 小红书 | Playwright UI | `schedule_helper.py` |
@@ -91,12 +102,12 @@ meta.description("B站") # 标题 + 标签 + 品牌标记
meta.tags_str("B站") # AI工具,效率提升,Soul派对,...
meta.bilibili_meta() # B站投稿完整 meta含 tid/tag/desc
meta.title_short() # 小红书短标题≤20字
meta.hashtags("视频号") # #AI工具 #效率提升 ... #小程序 卡若创业派对
meta.hashtags("视频号") # … + #小程序卡若创业派对 #公众号卡若-4点起床的男人
```
### 4.1 内容结构
- **标题**:手工优化标题库优先,否则从文件名智能提取
- **简介**:标题 + 换行 + 话题标签 + `#小程序 卡若创业派对`
- **简介**:标题 + 换行 + 话题标签**视频号**固定追加 `#小程序卡若创业派对` `#公众号卡若-4点起床的男人`
- **标签**基于关键词匹配AI/创业/副业/Soul 等 12 类)+ 通用标签
- **分区**B站 tid=160生活>日常)
- **风控过滤**`content_filter.py` 自动替换敏感词70+ 映射,严格/宽松分级)
@@ -121,9 +132,11 @@ meta.hashtags("视频号") # #AI工具 #效率提升 ... #小程序 卡若创
`cookie_manager.py` 统一管理:
- 中央存储:`多平台分发/cookies/{平台}_cookies.json`
- 自动迁移:旧路径 → 中央存储(首次使用时)
- API 预检5 平台各自 auth API 校验有效性
- 防重复登录:有效 Cookie 不触发重新获取
- **视频号双路径**`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` 关闭)
- API 预检:各平台 auth API
---

File diff suppressed because one or more lines are too long

View File

@@ -7,6 +7,7 @@
- 视频号保存时同步至 channels_storage_state.json 以兼容旧脚本
"""
import json
import shutil
import time
from pathlib import Path
from datetime import datetime
@@ -236,11 +237,39 @@ def _check_platform_stub(platform: str, cookies: dict[str, str]) -> tuple[bool,
return True, "存在(未做接口校验)"
def sync_channels_cookie_files() -> None:
"""
视频号 Cookie 双路径对齐:
- 登录脚本写入:视频号发布/脚本/channels_storage_state.jsonlegacy
- 预检读取:多平台分发/cookies/视频号_cookies.jsoncentral
以较新 mtime 为准互相覆盖,避免一份过期、一份未更新导致「能登录却校验失败」。
"""
legacy = PLATFORM_LEGACY_PATHS.get("视频号")
if not legacy:
return
_ensure_cookie_dir()
central = get_cookie_path("视频号")
if not legacy.exists() and not central.exists():
return
if legacy.exists() and not central.exists():
shutil.copy2(legacy, central)
return
if central.exists() and not legacy.exists():
shutil.copy2(central, legacy)
return
if legacy.stat().st_mtime >= central.stat().st_mtime:
shutil.copy2(legacy, central)
else:
shutil.copy2(central, legacy)
def check_cookie_valid(platform: str) -> tuple[bool, str]:
"""
校验平台 cookie 是否有效,调用平台特定 auth API。
返回 (is_valid, message)。
"""
if platform == "视频号":
sync_channels_cookie_files()
cookies = load_cookies(platform)
if not cookies:
return False, "文件不存在或为空"

View File

@@ -24,6 +24,8 @@ import argparse
import asyncio
import importlib.util
import json
import os
import subprocess
import sys
import time
from pathlib import Path
@@ -33,13 +35,47 @@ BASE_DIR = SCRIPT_DIR.parent.parent
DEFAULT_VIDEO_DIR = Path("/Users/karuo/Movies/soul视频/soul 派对 120场 20260320_output/成片_大师版")
sys.path.insert(0, str(SCRIPT_DIR))
from cookie_manager import check_cookie_valid, load_cookies, SUPPORTED_PLATFORMS
from cookie_manager import (
check_cookie_valid,
load_cookies,
SUPPORTED_PLATFORMS,
sync_channels_cookie_files,
)
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 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
sync_channels_cookie_files()
ok, _ = check_cookie_valid("视频号")
if ok:
return
if not CHANNELS_LOGIN_SCRIPT.exists():
return
print(
"\n[*] 视频号 Cookie 无效 → 打开浏览器扫码登录(登录完成即写入并同步 Cookie\n",
flush=True,
)
try:
subprocess.run(
[sys.executable, str(CHANNELS_LOGIN_SCRIPT)],
cwd=str(CHANNELS_LOGIN_SCRIPT.parent),
timeout=600,
)
except subprocess.TimeoutExpired:
print("[!] 登录流程超时600s", flush=True)
sync_channels_cookie_files()
PLATFORM_CONFIG = {
"抖音": {
"script": BASE_DIR / "抖音发布" / "脚本" / "douyin_pure_api.py",
@@ -315,8 +351,21 @@ async def main():
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(
"--no-auto-channels-login",
action="store_true",
help="禁用「仅发视频号时」Cookie 失效自动弹窗登录",
)
args = parser.parse_args()
if (
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)

View File

@@ -264,3 +264,24 @@
{"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.58630394935608, "timestamp": "2026-03-20 16:45:03"}
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/游戏辅助AI模型高收益高风险3到6个月必须收手.mp4", "title": "游戏辅助AI模型高收益高风险3到6个月必须收手", "success": true, "status": "likely_published", "message": "发布按钮+确认已点击,视频可能仍在处理", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 49.622374057769775, "timestamp": "2026-03-20 16:46:20"}
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/面试三面流程 简历+测试+试岗300份筛到2到3个人.mp4", "title": "面试三面流程 简历+测试+试岗300份筛到2到3个人", "success": true, "status": "likely_published", "message": "发布按钮+确认已点击,视频可能仍在处理", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 49.556177854537964, "timestamp": "2026-03-20 16:47:36"}
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/Soul上像开老茶馆.mp4", "title": "Soul上像开老茶馆", "success": false, "status": "error", "message": "post_create errCode=300002 request failed", "elapsed_sec": 78.16861295700073, "timestamp": "2026-03-23 06:59: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": 21.983049869537354, "timestamp": "2026-03-23 07:00:15"}
{"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": 22.617194890975952, "timestamp": "2026-03-23 07:00:41"}
{"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": 49.14002799987793, "timestamp": "2026-03-23 07:01:33"}
{"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": 49.91433119773865, "timestamp": "2026-03-23 07:02: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.54202175140381, "timestamp": "2026-03-23 07:02: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": 70.53687381744385, "timestamp": "2026-03-23 07:04:04"}
{"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": 22.21481990814209, "timestamp": "2026-03-23 07:04:29"}
{"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": 22.66372299194336, "timestamp": "2026-03-23 07:04:55"}
{"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": 22.18739891052246, "timestamp": "2026-03-23 07:05:20"}
{"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": 22.074921131134033, "timestamp": "2026-03-23 07:05:45"}
{"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.877957105636597, "timestamp": "2026-03-23 07:06:10"}
{"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": 27.32966685295105, "timestamp": "2026-03-23 07:06:40"}
{"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": 44.0236439704895, "timestamp": "2026-03-23 07:07:27"}
{"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": 22.436315059661865, "timestamp": "2026-03-23 07:07:52"}
{"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.646218061447144, "timestamp": "2026-03-23 07:08:44"}
{"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.89371585845947, "timestamp": "2026-03-23 07:09:36"}
{"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.64088201522827, "timestamp": "2026-03-23 07:10:01"}
{"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"}

View File

@@ -20,6 +20,8 @@ from content_filter import filter_for_platform
BRAND_TAG = "#卡若创业派对"
MINI_PROGRAM = "#小程序 卡若创业派对"
# 视频号描述末尾固定话题(与微信搜一搜/话题展示一致,无空格写法)
CHANNELS_FIXED_TAGS = ("#小程序卡若创业派对", "#公众号卡若-4点起床的男人")
PLATFORM_CATEGORIES = {
"B站": {"tid": 160, "name": "生活 > 日常"},
@@ -116,6 +118,8 @@ class VideoMeta:
tags_extra=curated.get("tags_extra", []),
)
stem = Path(fname).stem
stem = re.sub(r"^soul\d+_\d+_", "", stem, flags=re.I)
stem = re.sub(r"^\d+场", "", stem)
stem = re.sub(r'^\d+[._\-\s]*', '', stem)
stem = stem.replace('_', ' ').replace(' ', ' ').strip()
@@ -149,7 +153,10 @@ class VideoMeta:
"""# 标签字符串"""
tags = self._smart_tags(platform)
parts = [f"#{t}" for t in tags]
parts.append(MINI_PROGRAM)
if platform == "视频号":
parts.extend(CHANNELS_FIXED_TAGS)
else:
parts.append(MINI_PROGRAM)
return " ".join(parts)
def description(self, platform: str, max_len: int = 500) -> str:

View File

@@ -131,7 +131,7 @@ python3 soul_enhance.py \
## 五、成片:封面 + 字幕 + 竖屏
- **封面**:竖条画布内**不超出界面****半透明质感**(背景 alpha=165深色渐变、左上角 Soul logo**封面显示标题 = 成片文件名 = highlights.title**(去杠、去下划线后一致,无 `:|—/_`、无序号);标题严格居中、多行自动换行。透明度由 `VERTICAL_COVER_ALPHA` 调节
- **封面**:竖条画布内**不超出界面****冷色半透明渐变**`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+ 全糊」。
- **字幕****封面一结束即叠字幕**无额外「空几秒再等字」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`),保证整句显示正常、无异常中空。
@@ -142,7 +142,7 @@ python3 soul_enhance.py \
| 项 | 约定 |
|----|------|
| **与封面对比** | 封面为**半透明墨绿渐变**;字幕为**暖深棕圆角条 + 琥珀色描边**,避免与主题绿混成一团 |
| **与封面对比** | 封面为**冷灰青渐变 + 底渐隐 + 顶栏绿**;字幕为**暖深棕圆角条 + 琥珀色描边**,避免与顶栏绿混成一团 |
| **纠错** | `transcript.srt` 解析时走 `_improve_subtitle_text`繁转简、CORRECTIONS 错词、违禁替换、去语助词);**渲染每一帧前**再走 `improve_subtitle_punctuation`,与口播稿对齐 |
| **重点词** | `KEYWORDS` 列表命中则**亮金色高亮**(同字号同基线,仅颜色区分,避免大字号造成"两排字"),长词优先匹配 |
| **逐字渐显** | 推荐成片加 **`--typewriter-subs`**:同一条字幕时间内前缀逐步加长,更贴人声节奏;配合 CJK 去空格避免字间假空白 |

View File

@@ -1,82 +1,23 @@
[
{
"title": "高薪先看消耗",
"start_time": "00:07:30",
"end_time": "00:12:00",
"hook_3sec": "想两万月薪先看AI月烧多少",
"question": "高薪硬指标是什么?",
"cta_ending": "今天就到这里,点个关注下次不迷路",
"transcript_excerpt": "想拿2万工资AI月消耗至少1000块以上。消耗多说明你是实实在在用AI在解决职业里的问题",
"reason": "TOKEN消耗硬指标"
},
{
"title": "辅助暴利快收",
"start_time": "00:12:00",
"end_time": "00:18:00",
"hook_3sec": "游戏辅助来钱快,定性也狠",
"question": "做游戏AI能挣多少",
"cta_ending": "今天就到这里,点个关注下次不迷路",
"transcript_excerpt": "三角洲辅助瞄准AI训练人物识别模型。闲鱼抖音直播分销单价500块一个人一个月",
"reason": "高收益高风险"
},
{
"title": "保镖钱在后端",
"start_time": "00:18:00",
"end_time": "00:23:00",
"hook_3sec": "真赚的不是保镖费,是后端",
"question": "保镖怎么赚大钱?",
"cta_ending": "今天就到这里,点个关注下次不迷路",
"transcript_excerpt": "初级2万一个月中级3万高级4万。真正赚钱的是商务中介、介绍投资、拉业务合作",
"reason": "信任关系变现"
},
{
"title": "别两头扛流量",
"start_time": "00:23:00",
"end_time": "00:28:00",
"hook_3sec": "流量交付同时扛,必崩",
"question": "做流量还是交付?",
"cta_ending": "今天就到这里,点个关注下次不迷路",
"transcript_excerpt": "现在这条赛道缺的是流量,不是交付。和群主合作要分钱,不分必被排挤",
"reason": "一端打穿"
},
{
"title": "分钱分缺口",
"start_time": "00:28:00",
"end_time": "00:33:00",
"hook_3sec": "分他靠自己赚不到的那块",
"question": "招人怎么分钱?",
"cta_ending": "今天就到这里,点个关注下次不迷路",
"transcript_excerpt": "员工自己只能挣5000到8000你给他1万他多拿2000他才愿意跟你干",
"reason": "分钱逻辑"
},
{
"title": "推流就三板斧",
"start_time": "00:33:00",
"end_time": "00:38:00",
"hook_3sec": "二百七十万推流,密码就几条",
"question": "派对流量怎么来?",
"cta_ending": "今天就到这里,点个关注下次不迷路",
"transcript_excerpt": "流量密码就那几个职场、搞钱、MBTI性格匹配最容易共鸣",
"reason": "Soul数据复盘"
},
{
"title": "面试三面定人",
"start_time": "00:38:00",
"end_time": "00:43:00",
"hook_3sec": "三百简历,最后只要两三个",
"question": "怎么筛人?",
"cta_ending": "今天就到这里,点个关注下次不迷路",
"transcript_excerpt": "二面线上做题、跟团队开25分钟会看配合。三面定薪资岗位7天试岗",
"reason": "面试流程"
},
{
"title": "实操碾压学历",
"start_time": "00:43:00",
"end_time": "00:48:00",
"hook_3sec": "不学AI连班都难上",
"question": null,
"cta_ending": "今天就到这里,点个关注下次不迷路",
"transcript_excerpt": "想拿2万工资AI月消耗至少1000。这不是卡学历是卡你有没有真在用AI干活",
"reason": "实操门槛"
}
{"title": "Soul本场数据复盘", "start_time": "00:08:14", "end_time": "00:10:10", "hook_3sec": "二百七十万推流从哪来", "question": "派对数据怎么看?", "cta_ending": "今天就到这里,点个关注下次不迷路", "transcript_excerpt": "127场推流与进房去重", "reason": "单主题_数据"},
{"title": "高薪先看AI消耗", "start_time": "00:10:10", "end_time": "00:12:35", "hook_3sec": "想两万月薪先看月烧多少", "question": "高薪硬指标是什么?", "cta_ending": "今天就到这里,点个关注下次不迷路", "transcript_excerpt": "TOKEN消耗与岗位问法", "reason": "单主题_AI招人"},
{"title": "性格测评怎么配团队", "start_time": "00:12:35", "end_time": "00:15:05", "hook_3sec": "性格不对团队白搭", "question": "性格怎么测?", "cta_ending": "今天就到这里,点个关注下次不迷路", "transcript_excerpt": "MBTI PDP 筛选", "reason": "单主题_测评"},
{"title": "面试三面怎么筛人", "start_time": "00:28:22", "end_time": "00:30:55", "hook_3sec": "三百简历只要两三个", "question": "面试怎么筛?", "cta_ending": "今天就到这里,点个关注下次不迷路", "transcript_excerpt": "一面真二面测三面试岗", "reason": "单主题_面试"},
{"title": "线上阿米巴分钱", "start_time": "00:30:55", "end_time": "00:33:18", "hook_3sec": "线上更适合阿米巴", "question": "线上怎么分钱?", "cta_ending": "今天就到这里,点个关注下次不迷路", "transcript_excerpt": "小结果分钱与阿米巴", "reason": "单主题_阿米巴"},
{"title": "筛选精细化案例", "start_time": "00:33:18", "end_time": "00:36:08", "hook_3sec": "筛对了人能跟十二年", "question": "怎么减少试错?", "cta_ending": "今天就到这里,点个关注下次不迷路", "transcript_excerpt": "前端筛选与性格合群", "reason": "单主题_筛选"},
{"title": "老茶馆与情绪价值", "start_time": "00:36:08", "end_time": "00:39:05", "hook_3sec": "Soul上像开老茶馆", "question": "怎么做深度社群?", "cta_ending": "今天就到这里,点个关注下次不迷路", "transcript_excerpt": "线上茶馆与参与感", "reason": "单主题_社群"},
{"title": "链接与场景获客", "start_time": "00:39:05", "end_time": "00:42:05", "hook_3sec": "场景里拍视频就链接", "question": "怎么自然链接人?", "cta_ending": "今天就到这里,点个关注下次不迷路", "transcript_excerpt": "业务链接与场景", "reason": "单主题_链接"},
{"title": "场景链接续聊", "start_time": "00:42:05", "end_time": "00:45:05", "hook_3sec": "链接要落到具体事", "question": "链接怎么变现?", "cta_ending": "今天就到这里,点个关注下次不迷路", "transcript_excerpt": "案例延伸", "reason": "单主题_链接续"},
{"title": "派对上麦怎么玩", "start_time": "00:45:05", "end_time": "00:48:20", "hook_3sec": "上麦讲你上月做啥", "question": "怎么让别人记住你?", "cta_ending": "今天就到这里,点个关注下次不迷路", "transcript_excerpt": "上麦规则与业务曝光", "reason": "单主题_上麦"},
{"title": "保镖业务怎么合作", "start_time": "00:48:45", "end_time": "00:51:15", "hook_3sec": "保镖业务先讲清模式", "question": "保镖合作聊什么?", "cta_ending": "今天就到这里,点个关注下次不迷路", "transcript_excerpt": "上麦介绍保镖业务", "reason": "单主题_保镖入口"},
{"title": "保镖报价与客群", "start_time": "00:51:15", "end_time": "00:54:45", "hook_3sec": "政商金融矿产华侨", "question": "服务谁怎么报价?", "cta_ending": "今天就到这里,点个关注下次不迷路", "transcript_excerpt": "套餐与团队规模", "reason": "单主题_保镖报价"},
{"title": "游戏模型怎么变现", "start_time": "00:54:50", "end_time": "00:57:25", "hook_3sec": "三角洲模型怎么卖", "question": "游戏AI能挣多少", "cta_ending": "今天就到这里,点个关注下次不迷路", "transcript_excerpt": "分销与单价", "reason": "单主题_游戏变现"},
{"title": "辅助风险与止损", "start_time": "00:57:25", "end_time": "00:59:55", "hook_3sec": "三百七十万罚单亲历", "question": "辅助最大的坑?", "cta_ending": "今天就到这里,点个关注下次不迷路", "transcript_excerpt": "定性与收手", "reason": "单主题_风险"},
{"title": "别两头扛流量交付", "start_time": "01:23:55", "end_time": "01:26:25", "hook_3sec": "群主和交付师不一样", "question": "做流量还是交付?", "cta_ending": "今天就到这里,点个关注下次不迷路", "transcript_excerpt": "流量端交付端", "reason": "单主题_定位"},
{"title": "赛道缺流量不缺交付", "start_time": "01:26:25", "end_time": "01:29:10", "hook_3sec": "现在缺的是流量", "question": "为什么难做?", "cta_ending": "今天就到这里,点个关注下次不迷路", "transcript_excerpt": "老师多流量贵", "reason": "单主题_赛道"},
{"title": "摄影课与个人成长选择", "start_time": "01:36:10", "end_time": "01:38:50", "hook_3sec": "职场课为什么卖不动", "question": "摄影和成长怎么选?", "cta_ending": "今天就到这里,点个关注下次不迷路", "transcript_excerpt": "纪实与职场付费", "reason": "单主题_摄影"},
{"title": "CTO值钱在链接信息", "start_time": "01:40:58", "end_time": "01:43:35", "hook_3sec": "最大产值不是写代码", "question": "CTO该干什么", "cta_ending": "今天就到这里,点个关注下次不迷路", "transcript_excerpt": "前沿信息与链接", "reason": "单主题_CTO"},
{"title": "职场搞钱话题密码", "start_time": "01:49:48", "end_time": "01:52:33", "hook_3sec": "什么话题最好起量", "question": "推流密码是啥?", "cta_ending": "今天就到这里,点个关注下次不迷路", "transcript_excerpt": "职场搞钱MBTI", "reason": "单主题_话题"},
{"title": "分钱分缺口", "start_time": "01:52:33", "end_time": "01:55:18", "hook_3sec": "分他靠自己赚不到的那块", "question": "招人怎么分钱?", "cta_ending": "今天就到这里,点个关注下次不迷路", "transcript_excerpt": "分润逻辑", "reason": "单主题_分钱"},
{"title": "实操碾压学历", "start_time": "02:08:20", "end_time": "02:11:05", "hook_3sec": "学历不如可验证实操", "question": null, "cta_ending": "今天就到这里,点个关注下次不迷路", "transcript_excerpt": "收尾金句", "reason": "单主题_收尾"}
]

View File

@@ -0,0 +1,10 @@
[
{"title": "128场开门聊AI", "start_time": "00:04:33", "end_time": "00:07:22", "hook_3sec": "今天128场先聊筛选", "question": "独立开发怎么选模型?", "cta_ending": "今天就到这里,点个关注下次不迷路", "transcript_excerpt": "派对开场+API月耗筛选", "reason": "单主题_开场"},
{"title": "AI落地还有空间", "start_time": "00:07:22", "end_time": "00:09:55", "hook_3sec": "说个案例你就懂", "question": "AI辅助怎么放大", "cta_ending": "今天就到这里,点个关注下次不迷路", "transcript_excerpt": "实战案例与卖token", "reason": "单主题_AI案例"},
{"title": "抖音加AI怎么剪", "start_time": "00:23:12", "end_time": "00:26:05", "hook_3sec": "我之前抖音就这么做", "question": "人效怎么拉高?", "cta_ending": "今天就到这里,点个关注下次不迷路", "transcript_excerpt": "抖音录制+AI结构", "reason": "单主题_抖音"},
{"title": "一万次优于一万时", "start_time": "00:35:06", "end_time": "00:37:55", "hook_3sec": "我不认一万小时定律", "question": "刻意练习怎么算?", "cta_ending": "今天就到这里,点个关注下次不迷路", "transcript_excerpt": "一万次优化迭代", "reason": "单主题_方法论"},
{"title": "百场派对堆数据", "start_time": "00:39:50", "end_time": "00:42:35", "hook_3sec": "开一百多场在干嘛", "question": "内容怎么瘦出来?", "cta_ending": "今天就到这里,点个关注下次不迷路", "transcript_excerpt": "话题统计与试错成本", "reason": "单主题_派对数据"},
{"title": "最赚月是不是运气", "start_time": "00:46:40", "end_time": "00:49:25", "hook_3sec": "分享最赚钱那一个月", "question": "漏洞钱算不算能力?", "cta_ending": "今天就到这里,点个关注下次不迷路", "transcript_excerpt": "运气与偏踩案例", "reason": "单主题_赚钱节奏"},
{"title": "十一月项目怎么推", "start_time": "00:52:50", "end_time": "00:55:45", "hook_3sec": "普通人能做啥值钱", "question": "ID和物料怎么组织", "cta_ending": "今天就到这里,点个关注下次不迷路", "transcript_excerpt": "22年11月水平时间", "reason": "单主题_组织"},
{"title": "派对链接月万人", "start_time": "01:19:35", "end_time": "01:21:50", "hook_3sec": "月进房一两万人", "question": "派对直接赚钱吗?", "cta_ending": "今天就到这里,点个关注下次不迷路", "transcript_excerpt": "曝光与链接资源", "reason": "单主题_派对价值"}
]

View File

@@ -0,0 +1,145 @@
#!/usr/bin/env python3
"""
根据第127场 transcript.srt 关键词锚点,生成「单主题、短时长、条数多」的 highlights.json。
"""
from __future__ import annotations
import json
import re
import sys
from pathlib import Path
# (锚点正则, 条目标题, hook_3sec, question)
ANCHORS: list[tuple[str, str, str, str | None]] = [
(r"三个东西|三样东西|三个指标|招人.*看", "招人三指标总览", "现在招人我看三样", "招人最看重什么?"),
(r"TOKEN|消耗.*1000|一千块|月消耗", "高薪先看AI消耗", "想两万月薪先看月烧多少", "高薪硬指标是什么?"),
(r"MBTI|PDP|DISC|盖洛普|性格", "性格测评怎么配团队", "性格不对团队白搭", "性格怎么测?"),
(r"一面|二面|三面|试岗|简历.*真", "面试三面怎么筛人", "三百简历只要两三个", "面试怎么筛?"),
(r"三角洲|辅助瞄准|人物识别|闲鱼|五百.*月", "游戏模型怎么变现", "辅助来钱快渠道要狠", "游戏AI能挣多少"),
(r"破坏计算机|370万|十八万|定性|收手|虚拟货币", "辅助风险与止损", "高收益是定时炸弹", "辅助最大的坑是什么?"),
(r"保镖|女保镖|初级.*两万|中级.*三万", "保镖报价与档位", "真赚的不是保镖费", "保镖能赚多少?"),
(r"后端|中介|投资|拉业务|信任", "保镖钱在后端关系", "高端信任才值钱", "保镖怎么赚大钱?"),
(r"流量端|交付端|同时做|非常累", "别两头扛流量交付", "流量交付同时扛必崩", "做流量还是交付?"),
(r"缺.*流量|老师太多", "赛道缺流量不缺交付", "这条赛道缺流量", "现在缺什么?"),
(r"群主|分钱|排挤", "和群主合作要分钱", "不分钱就被排挤", "群主合作怎么分?"),
(r"273万|推流|进房|进群|去重", "Soul本场数据复盘", "二百七十万推流从哪来", "派对数据怎么看?"),
(r"职场|搞钱|MBTI.*共鸣|流量密码", "推流三板斧话题", "流量密码就这几条", "什么话题最好起量?"),
(r"兴趣群|三十个群|五十人|开播", "新号冲人笨办法", "三十个群堆出五十人", "新号怎么破冷启动?"),
(r"分他.*挣不到|五千.*八千|给.*一万", "分钱分缺口", "分他靠自己赚不到的那块", "招人怎么分钱?"),
(r"CTO|链接.*人|前沿信息", "CTO值钱在链接信息", "最大产值不是写代码", "CTO该干什么"),
(r"摄影|纪实|职场.*课|All in", "摄影课与个人成长选择", "职场课为什么卖不动", "摄影和成长怎么选?"),
(r"老茶馆|情绪价值|西西", "老茶馆与情绪价值", "Soul上情绪价值怎么做", "怎么做线上社群?"),
(r"不学.*AI|学历不重要|实操", "实操碾压学历", "不学AI连班都难上", None),
]
def parse_srt_times(path: Path) -> list[tuple[float, float, str]]:
raw = path.read_text(encoding="utf-8", errors="ignore")
blocks = re.split(r"\n\s*\n", raw.strip())
out: list[tuple[float, float, str]] = []
ts = re.compile(
r"(\d{2}):(\d{2}):(\d{2})[,.](\d{3})\s*-->\s*(\d{2}):(\d{2}):(\d{2})[,.](\d{3})"
)
def to_sec(h, m, s, ms):
return int(h) * 3600 + int(m) * 60 + int(s) + int(ms) / 1000.0
for b in blocks:
lines = [ln.strip() for ln in b.splitlines() if ln.strip()]
if len(lines) < 2:
continue
cue_line = next((ln for ln in lines if "-->" in ln), "")
m = ts.match(cue_line)
if not m:
continue
g = m.groups()
st = to_sec(g[0], g[1], g[2], g[3])
et = to_sec(g[4], g[5], g[6], g[7])
idx = lines.index(cue_line)
text = " ".join(lines[idx + 1 :])
out.append((st, et, text))
return out
def fmt_hms(sec: float) -> str:
sec = max(0, sec)
h = int(sec // 3600)
m = int((sec % 3600) // 60)
s = int(sec % 60)
return f"{h:02d}:{m:02d}:{s:02d}"
def find_anchor_events(subs: list[tuple[float, float, str]]) -> list[tuple[float, str, str, str | None]]:
"""每条字幕至多触发一个锚点;同一锚点类型 90s 内重复忽略。"""
events: list[tuple[float, str, str, str | None]] = []
last_idx: int | None = None
last_t = -999.0
for st, _et, text in subs:
t = re.sub(r"\s+", "", text)
for idx, (pat, title, hook, q) in enumerate(ANCHORS):
if re.search(pat, t):
if last_idx == idx and st - last_t < 90:
break
events.append((st, title, hook, q))
last_idx = idx
last_t = st
break
return events
def main():
srt = Path(sys.argv[1]) if len(sys.argv) > 1 else None
out_json = Path(sys.argv[2]) if len(sys.argv) > 2 else Path("highlights.json")
if not srt or not srt.is_file():
print("用法: build_127_highlights_from_srt.py <transcript.srt> <out_highlights.json>")
sys.exit(1)
subs = parse_srt_times(srt)
if not subs:
print("❌ SRT 无有效条目")
sys.exit(1)
events = find_anchor_events(subs)
if len(events) < 4:
print("❌ 锚点过少,请检查 SRT")
sys.exit(1)
video_end = min(subs[-1][1] + 3.0, 8000.0)
MIN_SEC = 50
MAX_SEC = 195
CTA = "今天就到这里,点个关注下次不迷路"
clips: list[dict] = []
for i, (st, title, hook, q) in enumerate(events):
en = events[i + 1][0] if i + 1 < len(events) else video_end
if en - st > MAX_SEC:
en = st + MAX_SEC
if en - st < MIN_SEC:
if i + 1 < len(events):
en = min(events[i + 1][0], st + MIN_SEC)
else:
en = st + MIN_SEC
if en <= st:
continue
clips.append(
{
"title": title,
"start_time": fmt_hms(st),
"end_time": fmt_hms(en),
"hook_3sec": hook,
"question": q,
"cta_ending": CTA,
"transcript_excerpt": "",
"reason": "srt_anchor_v1",
}
)
out_json.parent.mkdir(parents=True, exist_ok=True)
out_json.write_text(
json.dumps(clips, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
)
print(f"{len(clips)} 条 → {out_json}")
if __name__ == "__main__":
main()

View File

@@ -2,7 +2,7 @@
"""
Soul切片增强脚本 v2.0
功能:
1. 封面贴片:高光 hook_3sec 优先(吸睛),竖屏底图为**清晰帧 + 约 10% 轻模糊混入**(非全糊)+ 渐变
1. 封面贴片:高光 hook_3sec 优先(吸睛),竖屏底图为**清晰帧 + 约 10% 轻模糊混入**(非全糊)+ 冷色渐变;**顶栏单条 Soul 绿 + 底部电影感渐隐 + 细内框 + 柔阴影标题**(避免粗描边与多条绿边廉价感)
2. 烧录字幕(关键词高亮、可选逐字)
3. 切除检出的长静音并重映射字幕时间轴
4. 片尾 CTActa_ending字幕条
@@ -351,19 +351,22 @@ FONT_PRIORITY = [
"/Library/Fonts/Arial Unicode.ttf",
]
COVER_FONT_PRIORITY = [
"/System/Library/Fonts/PingFang.ttc", # 苹方,封面优先
FONT_BOLD, # 思源黑体 Bold标题更有海报感
"/System/Library/Fonts/PingFang.ttc",
"/System/Library/Fonts/Supplemental/Songti.ttc",
]
# Soul 品牌绿(绿点/绿色社交)
SOUL_GREEN = (0, 210, 106) # #00D26A
SOUL_GREEN_DARK = (0, 160, 80)
# 竖屏封面高级背景:深色渐变(不超出界面
VERTICAL_COVER_TOP = (12, 32, 24) # 深墨绿
VERTICAL_COVER_BOTTOM = (8, 48, 36) # 略亮绿
VERTICAL_COVER_PADDING = 44 # 左右留白,保证文字不贴边、不超出
# 成片封面半透明质感:背景层 alpha便于透出底层画面
VERTICAL_COVER_ALPHA = 165 # 0~255越大越不透明
# 竖屏封面高级背景:冷灰青渐变(比纯墨绿更显质感,与 Soul 绿顶栏形成对比
VERTICAL_COVER_TOP = (18, 26, 34) # 深板岩
VERTICAL_COVER_BOTTOM = (8, 14, 22) # 近黑蓝
VERTICAL_COVER_PADDING = 48 # 左右留白,保证文字不贴边、不超出
# 成片封面半透明质感:背景层 alpha便于透出底层画面(略降,减少发灰)
VERTICAL_COVER_ALPHA = 148 # 0~255越大越不透明
# 点缀金(细线用,低存在感)
COVER_ACCENT_GOLD = (201, 169, 98)
# 样式配置
STYLE = {
@@ -373,12 +376,21 @@ STYLE = {
'bg_blur_radius': 14, # 仅用于生成模糊层的高斯半径,再与清晰帧 blend
'overlay_alpha': 200,
'duration': 2.5,
# 竖屏「高级封面」装饰(横版仍走原 overlay 逻辑)
'dim_alpha': 88, # 首帧压暗,略提亮画面层次
'top_accent_px': 6, # 顶栏 Soul 绿实条高度
'gold_hairline': True, # 顶栏下 1px 淡金线
'vignette_from_ratio': 0.46, # 从下往上渐隐起点(占画面高度比例)
'vignette_max_alpha': 138,
'frame_inset_alpha': 42, # 内框白边透明度
},
'hook': {
'font_size': 82, # 更大更清晰
'color': (255, 255, 255),
'outline_color': (30, 30, 50),
'outline_width': 5,
# 竖屏主标题:略暖白,与冷底对比
'vertical_fill': (252, 252, 250),
},
'subtitle': {
'font_size': 44,
@@ -439,6 +451,43 @@ def draw_text_with_outline(draw, pos, text, font, color, outline_color, outline_
# 主体
draw.text((x, y), text, font=font, fill=color)
def draw_text_with_soft_shadow(draw, pos, text, font, fill_rgb):
"""竖屏封面标题:右下柔阴影 + 主字,比粗描边更偏杂志/海报质感"""
x, y = pos
if len(fill_rgb) == 4:
main = fill_rgb
else:
main = (*fill_rgb, 255)
layers = [
(6, 6, 72),
(5, 5, 100),
(4, 4, 130),
(3, 3, 155),
(2, 2, 175),
(1, 1, 195),
]
for dx, dy, a in layers:
draw.text((x + dx, y + dy), text, font=font, fill=(0, 0, 0, a))
draw.text((x, y), text, font=font, fill=main)
def _apply_cover_bottom_vignette(img_rgba, from_ratio: float, max_alpha: int):
"""底部电影感渐隐,压暗杂边、托住标题区;返回合成后的新图"""
w, h = img_rgba.size
y0 = int(max(0, min(1, from_ratio)) * h)
if y0 >= h - 2:
return img_rgba
layer = Image.new("RGBA", (w, h), (0, 0, 0, 0))
ld = ImageDraw.Draw(layer)
span = max(h - y0, 1)
for y in range(y0, h):
t = (y - y0) / span
a = int(max_alpha * (t ** 1.35))
a = max(0, min(255, a))
ld.rectangle([0, y, w, y + 1], fill=(0, 0, 0, a))
return Image.alpha_composite(img_rgba, layer)
def _normalize_title_for_display(title: str) -> str:
"""标题去杠去下划线:将 :|、—、/、_ 等全部替换为空格,避免文件名和封面出现杂符号"""
if not title:
@@ -1012,7 +1061,7 @@ def _strip_cover_number_prefix(text):
def create_cover_image(hook_text, width, height, output_path, video_path=None):
"""创建封面贴片。竖条(高 1080、宽由塑形半透明渐变、文字在条内居中、左上角 Soul logo不显示切片序号前缀"""
"""创建封面贴片。竖条:视频底 + 冷色渐变 + 底栏渐隐 + 顶栏 Soul 绿与细金线 + 内框 + 柔阴影标题 + 左上角标"""
hook_text = _to_simplified(str(hook_text or "").strip())
hook_text = _strip_cover_number_prefix(hook_text)
if not hook_text:
@@ -1052,7 +1101,8 @@ def create_cover_image(hook_text, width, height, output_path, video_path=None):
bf = Image.blend(sharp, blurred, mix)
else:
bf = sharp
dim = Image.new("RGBA", (width, height), (0, 0, 0, 115))
da = int(style.get("dim_alpha", 88))
dim = Image.new("RGBA", (width, height), (0, 0, 0, da))
base = Image.alpha_composite(bf, dim)
finally:
try:
@@ -1065,9 +1115,25 @@ def create_cover_image(hook_text, width, height, output_path, video_path=None):
gdraw, width, height, VERTICAL_COVER_TOP, VERTICAL_COVER_BOTTOM, alpha=VERTICAL_COVER_ALPHA
)
img = Image.alpha_composite(base, grad)
overlay = Image.new("RGBA", (width, height), (0, 0, 0, 60))
overlay = Image.new("RGBA", (width, height), (0, 0, 0, 36))
img = Image.alpha_composite(img, overlay)
img = _apply_cover_bottom_vignette(
img,
float(style.get("vignette_from_ratio", 0.46)),
int(style.get("vignette_max_alpha", 138)),
)
draw = ImageDraw.Draw(img)
apx = int(style.get("top_accent_px", 6))
draw.rectangle([0, 0, width, apx], fill=(*SOUL_GREEN, 255))
if style.get("gold_hairline", True):
draw.rectangle([0, apx, width, apx + 1], fill=(*COVER_ACCENT_GOLD, 105))
fa = int(style.get("frame_inset_alpha", 42))
if fa > 0:
draw.rectangle(
[2, 2, width - 3, height - 3],
outline=(255, 255, 255, fa),
width=1,
)
else:
# 横版:清晰帧 + 少量模糊混入(与竖条封面一致)
if video_path and os.path.exists(video_path):
@@ -1095,29 +1161,59 @@ def create_cover_image(hook_text, width, height, output_path, video_path=None):
img = Image.alpha_composite(img, overlay)
draw = ImageDraw.Draw(img)
# Soul 绿装饰线(顶部、底部)
for i in range(3):
alpha = 180 - i * 50
draw.rectangle([0, i * 3, width, i * 3 + 2], fill=(*SOUL_GREEN, alpha))
for i in range(3):
alpha = 180 - i * 50
draw.rectangle([0, height - i * 3 - 2, width, height - i * 3], fill=(*SOUL_GREEN, alpha))
# 左上角 Soul logo 小图标(绿圆 + 白字 S保证在界面内
logo_x, logo_y = 28, 28
logo_r = 20
draw.ellipse([logo_x - logo_r, logo_y - logo_r, logo_x + logo_r, logo_y + logo_r], fill=SOUL_GREEN, outline=(255, 255, 255))
# 横版保留原「上下 Soul 绿条」;竖屏已用顶栏 + 渐隐,不再叠多条绿边
if not is_vertical:
for i in range(3):
alpha = 180 - i * 50
draw.rectangle([0, i * 3, width, i * 3 + 2], fill=(*SOUL_GREEN, alpha))
for i in range(3):
alpha = 180 - i * 50
draw.rectangle([0, height - i * 3 - 2, width, height - i * 3], fill=(*SOUL_GREEN, alpha))
apx_v = int(style.get("top_accent_px", 6)) if is_vertical else 0
hair_v = (1 if style.get("gold_hairline", True) else 0) if is_vertical else 0
top_bar_h = apx_v + hair_v
if is_vertical:
logo_x, logo_y = 30, max(34, top_bar_h + 18)
logo_r = 21
draw.ellipse(
[
logo_x - logo_r - 2,
logo_y - logo_r - 2,
logo_x + logo_r + 2,
logo_y + logo_r + 2,
],
outline=(255, 255, 255, 100),
width=2,
)
draw.ellipse(
[logo_x - logo_r, logo_y - logo_r, logo_x + logo_r, logo_y + logo_r],
fill=SOUL_GREEN,
outline=(255, 255, 255, 165),
width=1,
)
else:
logo_x, logo_y = 28, 28
logo_r = 20
draw.ellipse(
[logo_x - logo_r, logo_y - logo_r, logo_x + logo_r, logo_y + logo_r],
fill=SOUL_GREEN,
outline=(255, 255, 255),
)
try:
logo_font = get_cover_font(26)
draw.text((logo_x - 5, logo_y - 12), "S", font=logo_font, fill=(255, 255, 255))
logo_font = get_cover_font(27 if is_vertical else 26)
sx = logo_x - (6 if is_vertical else 5)
sy = logo_y - (13 if is_vertical else 12)
draw.text((sx, sy), "S", font=logo_font, fill=(255, 255, 255))
except Exception:
pass
# 标题文字:竖屏时严格限制在 padding 内,多行居中,绝不超出界面
if is_vertical:
max_text_width = width - 2 * VERTICAL_COVER_PADDING
cover_font_size = 48
cover_font_size = 50
font = get_cover_font(cover_font_size)
vfill = hook_style.get("vertical_fill", (252, 252, 250))
lines = []
for _ in range(20):
current_line = ""
@@ -1145,12 +1241,7 @@ def create_cover_image(hook_text, width, height, output_path, video_path=None):
x = (width - line_w) // 2
x = max(VERTICAL_COVER_PADDING, min(width - VERTICAL_COVER_PADDING - line_w, x))
y = start_y + i * line_height
draw_text_with_outline(
draw, (x, y), line, font,
hook_style['color'],
hook_style['outline_color'],
min(hook_style['outline_width'], 3)
)
draw_text_with_soft_shadow(draw, (x, y), line, font, vfill)
else:
cover_font_size = hook_style['font_size']
font = get_cover_font(cover_font_size)
@@ -2074,7 +2165,7 @@ def generate_index(highlights, output_dir):
with open(index_path, 'w', encoding='utf-8') as f:
f.write("# Soul派对 - 成片目录\n\n")
f.write(
"**优化**: 高光 Hook 封面(模糊底)+逐字字幕+去长静音+片尾 CTA+加速10%(竖屏宽随 crop-vf\n\n"
"**优化**: 高光 Hook 封面(模糊底+冷色渐变+底渐隐+顶栏品牌色+逐字字幕+去长静音+片尾 CTA+加速10%(竖屏宽随 crop-vf\n\n"
)
f.write("## 切片列表\n\n")
f.write("| 序号 | 标题 | Hook | CTA |\n")

View File

@@ -0,0 +1,151 @@
# 视频号:开放能力 × 助手 Web API × 本项目集成参考
> 用途:把**视频号助手后台「开放能力」**、**微信开放平台「视频号助手 API」**与木叶现有**短视频纯 API 发布**放在一张图里,便于排需求、写脚本、对接运营数据。
> 官方总入口:<https://developers.weixin.qq.com/doc/channels/api/>
> 接口列表页:<https://developers.weixin.qq.com/doc/channels/api/channels/index.html>
---
## 一、两条轨道(不要混用)
| 轨道 | 凭证 | 典型用途 | 本项目落点 |
|------|------|----------|------------|
| **A. 视频号助手网页(逆向/会话)** | `channels_storage_state.json``channels_token.json`、扫码 Cookie | **短视频上传与发布**DFS + `post_create` | `脚本/channels_api_publish.py``channels_login.py` |
| **B. 开放平台「视频号助手 API」** | **AppID + AppSecret**`access_token` / `stable_token` | **直播记录、预约、橱窗、留资、大屏、罗盘**等官方服务端接口 | 见下文「官方接口清单」;凭证见 `credentials/README.md` |
**边界(务必对齐多平台分发 Skill**
微信**未开放**「用开放平台 API 直接上传/发布短视频」;短视频自动化目前只可走 **A 轨**
**B 轨**负责经营数据、带货、留资、直播场次等**合规官方能力**
---
## 二、助手后台「开放能力」能联想到的产品动作(想象力清单)
以下需在后台开通对应权限;是否开放以微信后台与文档为准。
### 2.1 直播全链路
| 阶段 | 可设想动作 | 依赖的官方能力方向 |
|------|------------|-------------------|
| 播前 | 把「直播预约」同步到飞书日历 / 派对排期表 | 直播预约记录 API |
| 播中 | 电商直播间指标看板(内部大屏,非替代微信客户端开播) | 直播大屏(**仅电商直播间** |
| 播后 | 场次列表与 Soul 第 N 场对齐、自动写运营报表 | 直播记录 API |
| 转化 | 留资进多维表 / Mongo再分给私域跟进 | 留资组件 + 留资直播数据 API |
| 带货 | 排品脚本:按策略上下架橱窗 | 橱窗管理 API |
| 复盘 | 周/月带货与人群报告自动生成 | 罗盘达人版 API |
### 2.2 与小程序/私域联动(另一条文档线)
- 同主体或关联主体小程序:`wx.getChannelsLiveInfo``wx.openChannelsLive``wx.reserveChannelsLive``channel-live` 等(打开/预约/内嵌直播),与 **A/B 轨互补**
- 文档:<https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/channels-live>
### 2.3 当前**未**在官方「助手 API」里直接等价的能力预期管理
- 用 HTTP **代替手机/OBS 发起推流、开关播**(一般仍走客户端)。
- **实时弹幕/评论流**全量拉取(若有,以单独公告/文档为准,勿与下表混为一谈)。
---
## 三、视频号助手 API官方— 与直播/经营强相关的路径速查
摘自接口列表页(路径以文档最新版为准)。
### 3.1 直播基础信息
| 说明 | 路径 |
|------|------|
| 获取当前直播记录 | `/channels/ec/finderlive/getfinderliverecordlist` |
| 获取当前直播预约记录 | `/channels/ec/finderlive/getfinderlivenoticerecordlist` |
### 3.2 橱窗管理(带货)
| 说明 | 路径 |
|------|------|
| 上架到橱窗 | `/channels/ec/window/product/add` |
| 橱窗商品详情 | `/channels/ec/window/product/get` |
| 橱窗商品列表 | `/channels/ec/window/product/list/get` |
| 下架 | `/channels/ec/window/product/off` |
### 3.3 留资组件
| 说明 | 路径 |
|------|------|
| 按直播场次取留资详情 | `/channels/leads/get_leads_info_by_request_id` |
| 按时间取留资详情 | `/channels/leads/get_leads_info_by_component_id` |
| 留资 request_id 列表 | `/channels/leads/get_leads_request_id` |
| 留资组件直播推广记录 | `/channels/leads/get_leads_component_promote_record` |
| 留资组件 ID 列表 | `/channels/leads/get_leads_component_id` |
### 3.4 留资相关直播数据
| 说明 | 路径 |
|------|------|
| 视频号账号信息 | `/channels/finderlive/get_finder_attr_by_appid` |
| 留资直播数据详情 | `/channels/finderlive/get_finder_live_data_list` |
| 账号留资数量 | `/channels/finderlive/get_finder_live_leads_data` |
### 3.5 直播大屏
| 说明 | 路径 |
|------|------|
| 大屏直播列表(文档注明:仅电商直播间) | `/channels/livedashboard/getlivelist` |
| 大屏数据 | `/channels/livedashboard/getlivedata` |
### 3.6 罗盘达人版(带货复盘)
| 说明 | 路径 |
|------|------|
| 电商概览 | `/channels/ec/compass/finder/overall/get` |
| 带货商品数据 | `/channels/ec/compass/finder/product/data/get` |
| 带货商品列表 | `/channels/ec/compass/finder/product/list/get` |
| 带货人群数据 | `/channels/ec/compass/finder/sale/profile/data/get` |
### 3.7 通用基础
- 取 token`/cgi-bin/token``/cgi-bin/stable_token`(服务端长期跑任务建议了解 stable_token 策略)。
- 额度:`/cgi-bin/openapi/quota/*`(以文档为准)。
---
## 四、A 轨:短视频「上传/发布」在本项目中的真实步骤(非开放平台)
`SKILL.md` 一致,便于和 B 轨对照:
1. `channels_login.py` → 落盘 `channels_storage_state.json`(及中央 Cookie 同步,见多平台分发 Skill
2. `helper_upload_params` → DFS `applyuploaddfs` / `uploadpartdfs` / `completepartuploaddfs`
3. `post_create`(需 `finger-print-device-id``x-wechat-uin` 等)。
4. 去重:`多平台分发/脚本/publish_log.jsonl`(若走 distribute 链路)。
---
## 五、推荐集成顺序给「smart / 运营自动化」排期用)
1. **已有**A 轨短视频发布 + 分发日志(保持现状)。
2. **先做低风险**`getfinderliverecordlist` + `getfinderlivenoticerecordlist` → 写入飞书运营表或本地 JSONL。
3. **有留资组件时**:接 `/channels/leads/*``get_finder_live_*`
4. **带货号**:橱窗 + 罗盘 + 大屏(确认账号类型是否电商直播)。
5. **小程序**:与永平/soul 小程序需求单独立评审(主体绑定条件)。
---
## 六、凭证与操作约定
- **开放平台 AppID / AppSecret**:只放在 `credentials/.env.open_platform`(已被仓库根 `.gitignore``.env.*` 规则忽略),**永不提交 Git**。
- **网页会话**:继续用 `脚本/channels_storage_state.json` 等;轮换策略见 `视频号发布/SKILL.md`
- 详细变量名与加载方式:`credentials/README.md`
---
## 七、相关文件索引
| 文件 | 作用 |
|------|------|
| `视频号发布/SKILL.md` | A 轨发布流程与边界 |
| `视频号发布/REFERENCE_开放能力_数据与集成.md` | 本文B 轨 + 集成脑图 |
| `视频号发布/credentials/README.md` | TOKEN 与环境变量约定 |
| `多平台分发/SKILL.md` | 全平台分发与视频号 Cookie 策略 |
| `脚本/channels_api_publish.py` | A 轨主脚本 |
---
*文档版本2026-03-23 · 木叶*

View File

@@ -6,8 +6,8 @@ description: >
triggers: 视频号发布、发布到视频号、视频号登录、视频号上传、微信视频号
owner: 木叶
group: 木
version: "3.0"
updated: "2026-03-10"
version: "3.1"
updated: "2026-03-23"
---
# 视频号发布 Skillv3.0
@@ -115,6 +115,10 @@ python3 channels_api_publish.py
| 文件 | 说明 |
|------|------|
| `REFERENCE_开放能力_数据与集成.md` | **开放能力 + 官方助手 API + 直播/数据/橱窗/留资** 整合参考(与 A 轨发布对照) |
| `credentials/README.md` | **开放平台 AppID/AppSecret** 存放约定(`.env.open_platform`,勿提交) |
| `credentials/open_platform.env.example` | 环境变量模板 |
| `脚本/channels_open_fetch.py` | **开放平台**:拉账号/直播记录/预约/罗盘 GMV无单条短视频播放接口 |
| `脚本/channels_api_publish.py` | **主脚本**:纯 API 视频上传+发布 (v5) |
| `脚本/channels_publish.py` | 旧版 Playwright 发布(备用) |
| `脚本/channels_login.py` | Playwright 微信扫码登录 |

View File

@@ -0,0 +1,45 @@
# 视频号凭证目录(开放平台)
## 用途
- **微信开放平台**绑定的视频号业务:用 **AppID + AppSecret** 换取 `access_token` / `stable_token`,再调用 [视频号助手 API](https://developers.weixin.qq.com/doc/channels/api/)。
-**网页扫码**得到的 `../脚本/channels_storage_state.json` **不是同一套凭证**,不要混在一个文件里。
## 标准做法(以后都按这个来)
1. 在本目录维护 **`.env.open_platform`**(文件名以 `.env.` 开头已被卡若AI 仓库根 `.gitignore``.env.*` 忽略,**不会进 Git**)。
2. 需要示例可复制 **`open_platform.env.example`** 为 `.env.open_platform` 再填值。
3. 脚本中读取示例Python
```python
from pathlib import Path
def load_open_platform_env():
p = Path(__file__).resolve().parent.parent / "credentials" / ".env.open_platform"
if not p.exists():
return {}
out = {}
for line in p.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
if "=" in line:
k, _, v = line.partition("=")
out[k.strip()] = v.strip().strip('"').strip("'")
return out
```
4. 取 token按官方文档调用 `https://api.weixin.qq.com/cgi-bin/stable_token``token`grant_type=client_credential**不要把 token 写进本仓库**;可只在内存或本机另一个忽略文件里缓存。
## 变量说明
| 变量 | 必填 | 说明 |
|------|------|------|
| `WECHAT_OPEN_APPID` | 是 | 开放平台应用 AppID |
| `WECHAT_OPEN_APPSECRET` | 是 | 开放平台应用 AppSecret |
| `WECHAT_OPEN_STABLE_TOKEN` | 否 | 若你手动缓存 stable_token可放这里仍勿提交否则留空每次用 Secret 换 |
## 安全
- **AppSecret 泄露 = 立刻到公众平台重置**,并更新本文件。
- 勿把本目录任何含 Secret 的文件拖进聊天、截图、PR。

View File

@@ -0,0 +1,5 @@
# 复制为同目录下的 .env.open_platform 后填写(.env.open_platform 已被 .gitignore 忽略)
WECHAT_OPEN_APPID=
WECHAT_OPEN_APPSECRET=
# 可选:长期 stable_token 本地缓存(勿提交)
# WECHAT_OPEN_STABLE_TOKEN=

View File

@@ -42,7 +42,7 @@ try:
except ImportError:
VideoMeta = None
DESC_SUFFIX = " #小程序 卡若创业派对"
DESC_SUFFIX = " #小程序卡若创业派对 #公众号卡若-4点起床的男人"
MINI_PROGRAM_LINK = "#小程序://卡若创业派对/gF4V8Vo4Ws4IiJa"
CHUNK_SIZE = 8 * 1024 * 1024
@@ -692,13 +692,33 @@ async def _ensure_ctx() -> dict:
return _ctx
def _scheduled_ts_for_channels(scheduled_time) -> int:
"""
distribute_all 传入 datetime首条为「立即」时时间≈当前应走 postTimingInfo 省略(立即发表)。
与 schedule_generator 一致:仅当发布时间明显在未来时才传定时。
"""
if scheduled_time is None:
return 0
from datetime import datetime
if isinstance(scheduled_time, datetime):
ts = int(scheduled_time.timestamp())
else:
ts = int(scheduled_time)
now = int(time.time())
# 2 分钟内视为「立即」,避免 postTime 过近被服务端拒绝
if ts <= now + 120:
return 0
return ts
async def publish_one_compat(
video_path: str, title: str, idx: int, total: int,
scheduled_time=None,
) -> PublishResult:
"""distribute_all.py 调用的简化接口"""
ctx = await _ensure_ctx()
sched_ts = int(scheduled_time) if scheduled_time else 0
sched_ts = _scheduled_ts_for_channels(scheduled_time)
result = await publish_one(
ctx["cookie_str"], ctx["finder_id"],
ctx["uin"], ctx["finger_print"], ctx["aid"],

View File

@@ -1,8 +1,12 @@
#!/usr/bin/env python3
"""视频号登录 v6等待完整登录流程完成后提取 Cookie + rawKeyBuff"""
"""视频号登录 v7优先 Cursor 内置 Simple Browser 扫码;会话落盘优先 CDP 附着 Cursor失败再回退 Playwright。"""
import asyncio
import json
import os
import shutil
import subprocess
import sys
import urllib.parse
from pathlib import Path
from playwright.async_api import async_playwright
@@ -13,6 +17,8 @@ TOKEN_FILE = SCRIPT_DIR / "channels_token.json"
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")
UA = (
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
@@ -24,6 +30,35 @@ REQUIRED_LS_KEYS = [
]
def open_cursor_simple_browser(url: str) -> None:
"""在 Cursor 编辑器内打开 Simple Browser不唤起系统默认外部浏览器"""
enc = urllib.parse.quote(url, safe="")
deeplink = f"cursor://vscode.simple-browser/show?url={enc}"
try:
if sys.platform == "darwin":
subprocess.run(["open", deeplink], check=False)
elif sys.platform == "win32":
os.startfile(deeplink) # type: ignore[attr-defined]
else:
subprocess.run(["xdg-open", deeplink], check=False)
print(f"[✓] 已在 Cursor 内请求打开 Simple Browser若未弹出Cmd/Ctrl+Shift+P → Simple Browser: Show → 粘贴 URL", flush=True)
print(f" URL: {url}", flush=True)
except Exception as e:
print(f"[!] 打开 Cursor Simple Browser 失败: {e}", flush=True)
print(f" 请手动在 Cursor 命令面板执行 Simple Browser: Show粘贴: {url}", flush=True)
def _sync_to_central_cookie_store() -> None:
try:
_central = SCRIPT_DIR.parent.parent / "多平台分发" / "cookies" / "视频号_cookies.json"
_central.parent.mkdir(parents=True, exist_ok=True)
if COOKIE_FILE.exists():
shutil.copy2(COOKIE_FILE, _central)
print(f"[✓] 已同步中央 Cookie: {_central}", flush=True)
except Exception as e:
print(f"[!] 同步中央 Cookie 失败: {e}", flush=True)
async def extract_ls(page, keys):
try:
return await page.evaluate("""(keys) => {
@@ -39,99 +74,51 @@ async def extract_ls(page, keys):
return {}
async def main():
print("即将弹出浏览器,请用微信扫码登录视频号助手。", flush=True)
print("登录后请等待页面跳转到「内容管理」页面,不要手动关闭浏览器。\n", flush=True)
async with async_playwright() as pw:
browser = await pw.chromium.launch(headless=False)
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)
async def wait_until_logged_in(page, label: str = "") -> bool:
prefix = f"[{label}] " if label else ""
for i in range(120):
await asyncio.sleep(3)
await page.screenshot(path=str(QR_SCREENSHOT))
print(f"[QR] 二维码截图已保存: {QR_SCREENSHOT}", flush=True)
print("请用微信扫描浏览器中的二维码...\n", flush=True)
# 只等待 URL 跳转到平台页面(不提前因 cookie 退出循环)
logged_in = False
for i in range(120):
await asyncio.sleep(3)
try:
url = page.url
except Exception:
break
if "platform" in url and "login" not in url:
logged_in = True
print(f"[✓] 登录成功,已跳转到: {url[:100]}", flush=True)
break
if i % 10 == 0:
print(f" 等待扫码并跳转中... ({i * 3}s)", flush=True)
if not logged_in:
print("[✗] 6 分钟超时。", flush=True)
await browser.close()
return 1
# 等待页面 JS 执行完成微前端加载、localStorage 写入)
print("[i] 等待平台 JS 加载完成...", flush=True)
await asyncio.sleep(10)
# 提取 localStorage
ls_vals = {}
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)
# 如果 finder_raw 还是空,尝试点击内容管理触发加载
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)
# 保存 storage_state
try:
await context.storage_state(path=str(COOKIE_FILE))
except Exception as e:
print(f"[!] 保存 storage_state 异常: {e}", flush=True)
url = page.url
except Exception:
break
if "platform" in url and "login" not in url:
print(f"{prefix}[✓] 登录成功,已跳转到: {url[:100]}", flush=True)
return True
if i % 10 == 0:
print(f"{prefix} 等待扫码并跳转中... ({i * 3}s)", flush=True)
print(f"{prefix}[✗] 6 分钟超时。", flush=True)
return False
async def save_session_from_context(context, page, ls_vals: dict) -> bool:
try:
state = await context.storage_state()
# 合并我们显式抓到的 finder keys防止 storage_state 未含 localStorage
origins = list(state.get("origins") or [])
merged_items = {}
for origin_block in origins:
if origin_block.get("origin") == "https://channels.weixin.qq.com":
for it in origin_block.get("localStorage") or []:
merged_items[it["name"]] = it.get("value", "")
for k, v in ls_vals.items():
if v:
merged_items[k] = str(v)
new_ls = [{"name": k, "value": v} for k, v in merged_items.items()]
replaced = False
for ob in origins:
if ob.get("origin") == "https://channels.weixin.qq.com":
ob["localStorage"] = new_ls
replaced = True
break
if not replaced and new_ls:
origins.append({"origin": "https://channels.weixin.qq.com", "localStorage": new_ls})
state["origins"] = origins
COOKIE_FILE.write_text(json.dumps(state, ensure_ascii=False, indent=2))
_sync_to_central_cookie_store()
# 保存 token 信息
cookies = await context.cookies()
cookie_dict = {c["name"]: c["value"] for c in cookies}
token_data = {
"sessionid": cookie_dict.get("sessionid", ""),
"wxuin": cookie_dict.get("wxuin", ""),
@@ -143,19 +130,182 @@ async def main():
"url": page.url,
}
TOKEN_FILE.write_text(json.dumps(token_data, ensure_ascii=False, indent=2))
return bool(ls_vals.get("finder_raw"))
except Exception as e:
print(f"[!] 保存会话异常: {e}", flush=True)
return False
await browser.close()
print(f"\n[✓] Cookie 已保存: {COOKIE_FILE}", flush=True)
print(f"[✓] Token 已保存: {TOKEN_FILE}", flush=True)
async def try_capture_via_cdp(pw, cdp_url: str) -> bool:
"""附着已带 --remote-debugging-port 的 Cursor/Chromium从 Simple Browser 所在上下文导出。"""
try:
sc = json.loads(COOKIE_FILE.read_text()).get("cookies", [])
print(f" Cookie 数量: {len(sc)}", flush=True)
except Exception:
pass
raw = ls_vals.get("finder_raw", "")
print(f" rawKeyBuff: {'' + raw[:30] + '...' if raw else '✗ 未提取到'}", flush=True)
return 0 if ls_vals.get("finder_raw") else 1
browser = await pw.chromium.connect_over_cdp(cdp_url, timeout=8000)
except Exception as e:
print(f"[i] CDP 未连接 ({cdp_url}): {e}", flush=True)
return False
target_page = None
for ctx in browser.contexts:
for page in ctx.pages:
u = page.url or ""
if "channels.weixin.qq.com" in u:
target_page = page
break
if target_page:
break
if not target_page:
print("[!] CDP 已连接但未找到 channels.weixin.qq.com 页面(请确认已在 Cursor Simple Browser 打开视频号助手)", flush=True)
await browser.close()
return False
ctx = target_page.context
print("[i] 已附着 Cursor/内置浏览器上下文,等待进入内容管理页…", flush=True)
for _ in range(120):
try:
u = target_page.url or ""
if "platform" in u and "login" not in u:
break
except Exception:
pass
await asyncio.sleep(3)
print("[i] 等待 rawKeyBuff / localStorage 写入…", flush=True)
ls_vals = {}
for attempt in range(120):
ls_vals = await extract_ls(target_page, REQUIRED_LS_KEYS)
if ls_vals.get("finder_raw"):
print(f"[✓] rawKeyBuff 已就绪 (CDP, {attempt}s)", flush=True)
break
try:
u = target_page.url or ""
if "login" in u and attempt > 20:
pass
except Exception:
pass
if attempt % 15 == 0 and attempt > 0:
print(f" 等待 localStorage… ({attempt}s)", flush=True)
await asyncio.sleep(1)
if not ls_vals.get("finder_raw"):
try:
await target_page.goto(
"https://channels.weixin.qq.com/platform/post/list",
timeout=20000,
wait_until="domcontentloaded",
)
await asyncio.sleep(8)
for _ in range(40):
ls_vals = await extract_ls(target_page, REQUIRED_LS_KEYS)
if ls_vals.get("finder_raw"):
print("[✓] 导航后 rawKeyBuff 已就绪 (CDP)", flush=True)
break
await asyncio.sleep(1)
except Exception as e:
print(f"[!] CDP 导航补全失败: {e}", flush=True)
ok = await save_session_from_context(ctx, target_page, ls_vals)
await browser.close()
return ok
async def capture_via_playwright_external() -> bool:
"""回退:独立 Chromium 窗口(仅当 CDP 不可用时)。"""
print("\n[i] 未使用 CDP → 将打开本机 Chromium 窗口仅用于写入 Cookie 文件(可扫码后尽快关闭)\n", flush=True)
ls_vals = {}
async with async_playwright() as pw:
browser = await pw.chromium.launch(headless=False)
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"):
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:
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
if not force_pw:
if not skip_cursor_tab:
print("→ 优先在 Cursor 内置 Simple Browser 打开视频号登录页(不打开系统默认浏览器)。\n", flush=True)
open_cursor_simple_browser(LOGIN_URL)
print(
"\n请在上一步打开的 **Cursor 编辑器内** Simple Browser 中用微信扫码并进入「内容管理」。\n"
f"若要让脚本**不弹额外 Chromium**:请用带远程调试端口的方式启动 Cursor例如 macOS\n"
f" /Applications/Cursor.app/Contents/MacOS/Cursor --remote-debugging-port=9223\n"
f"然后本脚本会通过 CDP ({cdp_url}) 读取会话并写入 Cookie。\n",
flush=True,
)
if sys.stdin.isatty():
try:
input("登录完成并进入内容管理后,按回车继续拉取 Cookie…\n")
except EOFError:
pass
else:
print("[i] 非交互终端:将轮询 CDP 最多约 6 分钟…", flush=True)
await asyncio.sleep(5)
async with async_playwright() as pw:
if not force_pw:
if await try_capture_via_cdp(pw, cdp_url):
print(f"\n[✓] Cookie 已保存: {COOKIE_FILE}", flush=True)
return 0
ok = await capture_via_playwright_external()
print(f"\n[{'' if ok else ''}] Cookie 保存: {COOKIE_FILE}", flush=True)
return 0 if ok else 1
if __name__ == "__main__":

View File

@@ -0,0 +1,99 @@
#!/usr/bin/env python3
"""
视频号开放平台(助手 API一键拉数账号信息、直播记录、直播预约、罗盘日 GMV含短视频成交字段
凭证:../credentials/.env.open_platform勿提交
说明:官方「视频号助手 API」不提供单条短视频播放量/列表;短视频维度仅有罗盘里的带货成交等经营指标(若有)。
"""
from __future__ import annotations
import json
import os
import sys
import urllib.parse
import urllib.request
from datetime import datetime, timedelta, timezone
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
CRED_PATH = SCRIPT_DIR.parent / "credentials" / ".env.open_platform"
TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token"
API = "https://api.weixin.qq.com"
def load_env(path: Path) -> dict[str, str]:
if not path.exists():
print(f"[!] 缺少 {path}", file=sys.stderr)
sys.exit(1)
out: dict[str, str] = {}
for line in path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
k, _, v = line.partition("=")
out[k.strip()] = v.strip().strip('"').strip("'")
return out
def http_json(url: str, method: str = "GET", body: dict | None = None) -> dict:
data = None if body is None else json.dumps(body).encode("utf-8")
req = urllib.request.Request(url, data=data, method=method)
if body is not None:
req.add_header("Content-Type", "application/json")
with urllib.request.urlopen(req, timeout=45) as r:
return json.loads(r.read().decode())
def get_access_token(appid: str, secret: str) -> str:
q = urllib.parse.urlencode(
{"grant_type": "client_credential", "appid": appid, "secret": secret}
)
j = http_json(f"{TOKEN_URL}?{q}")
if "access_token" not in j:
raise RuntimeError(json.dumps(j, ensure_ascii=False))
return j["access_token"]
def post_channels(at: str, path: str, body: dict | None = None) -> dict:
url = f"{API}{path}?access_token={at}"
return http_json(url, "POST", body or {})
def main() -> None:
env = load_env(CRED_PATH)
appid = env.get("WECHAT_OPEN_APPID")
secret = env.get("WECHAT_OPEN_APPSECRET")
if not appid or not secret:
print("[!] .env.open_platform 需含 WECHAT_OPEN_APPID / WECHAT_OPEN_APPSECRET", file=sys.stderr)
sys.exit(1)
at = get_access_token(appid, secret)
tz8 = timezone(timedelta(hours=8))
today = datetime.now(tz8).strftime("%Y%m%d")
yesterday = (datetime.now(tz8) - timedelta(days=1)).strftime("%Y%m%d")
out: dict = {
"fetched_at": datetime.now(tz8).isoformat(),
"finder_attr": post_channels(at, "/channels/finderlive/get_finder_attr_by_appid", {}),
"live_records": post_channels(at, "/channels/ec/finderlive/getfinderliverecordlist", {}),
"live_notices": post_channels(at, "/channels/ec/finderlive/getfinderlivenoticerecordlist", {}),
"compass_overall_yesterday": post_channels(
at, "/channels/ec/compass/finder/overall/get", {"ds": yesterday}
),
"compass_overall_today": post_channels(
at, "/channels/ec/compass/finder/overall/get", {"ds": today}
),
"livedashboard_list": post_channels(at, "/channels/livedashboard/getlivelist", {}),
}
# stdout给管道 / 飞书脚本用
print(json.dumps(out, ensure_ascii=False, indent=2))
out_path = os.environ.get("CHANNELS_OPEN_FETCH_OUT")
if out_path:
Path(out_path).write_text(json.dumps(out, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"\n# 已写入 {out_path}", file=sys.stderr)
if __name__ == "__main__":
main()

File diff suppressed because one or more lines are too long

View File

@@ -1,13 +1,10 @@
{
"sessionid": "BgAALavrrg26toSTr%2Fh%2BEjYhRF5AJMeU6M75ATmitpiad6Wdkb0bz%2F3pP5boxKqKwq6I8OPHiRN%2Bd5Ry%2BzSpNDTgyEqs%2BBPuLXWl3V3aQLE%3D",
"wxuin": "1532657520",
"cookie_str": "sessionid=BgAALavrrg26toSTr%2Fh%2BEjYhRF5AJMeU6M75ATmitpiad6Wdkb0bz%2F3pP5boxKqKwq6I8OPHiRN%2Bd5Ry%2BzSpNDTgyEqs%2BBPuLXWl3V3aQLE%3D; wxuin=1532657520",
"raw_cookies": "sessionid=BgAALavrrg26toSTr%2Fh%2BEjYhRF5AJMeU6M75ATmitpiad6Wdkb0bz%2F3pP5boxKqKwq6I8OPHiRN%2Bd5Ry%2BzSpNDTgyEqs%2BBPuLXWl3V3aQLE%3D; wxuin=1532657520",
"sessionid": "BgAAO0pkRXR1Cnpkg8qKYYMziAr6J6R%2FMVqC2Jat0R1fmRXawRqzqlxIdOy47RaOqNJ8YCfDBsY9VFSx0UMt4JmiGxqBL6Wa6v0LDXbmykU%3D",
"wxuin": "1519919758",
"cookie_str": "sessionid=BgAAO0pkRXR1Cnpkg8qKYYMziAr6J6R%2FMVqC2Jat0R1fmRXawRqzqlxIdOy47RaOqNJ8YCfDBsY9VFSx0UMt4JmiGxqBL6Wa6v0LDXbmykU%3D; wxuin=1519919758",
"finder_raw": "",
"finder_username": "v2_060000231003b20faec8c5e48919cbd5cb05e53db077dd1924028a806c10cffd891eb5a80ce7@finder",
"finder_uin": "",
"finder_login_token": "",
"url": "https://channels.weixin.qq.com/platform",
"finder_route_meta": "micro.content/post/create;index;1;1773648359203",
"finder_ua_report_data": "{\"browser\":\"Chrome\",\"browserVersion\":\"143.0.0.0\",\"engine\":\"Webkit\",\"engineVersion\":\"537.36\",\"os\":\"Mac OS X\",\"osVersion\":\"10.15.7\",\"device\":\"desktop\",\"darkmode\":0}"
"url": "https://channels.weixin.qq.com/platform/post/list"
}

View File

@@ -285,7 +285,7 @@ for (const col of collections) {
2. **更新本 SKILL 经验库**(本文件末尾 §八)
3. **飞书复盘总结**
3. **飞书复盘总结(按需)**:仅当用户明说发群或规程要求时调用 `send_review_to_feishu_webhook.py`;默认不每轮自动发,见 `.cursor/rules/karuo-ai.mdc`
---

View File

@@ -14,9 +14,9 @@
## 二、对话后沉淀(必做项)
### 2.1 飞书复盘总结发群(强制
### 2.1 飞书复盘总结发群(按需 · 非默认
每次对话完成、复盘写完后,将**简洁复盘总结**(建议 ≤500 字)发到指定飞书群;**长对话必发**,每次完成的任务都发。
**默认不发群。** 仅在**捆绑明确**且**工作区为卡若AI 主战场**时发送简洁复盘(建议 ≤500 字)。条件与 Cursor 规则 `.cursor/rules/karuo-ai.mdc`「飞书复盘发群」一致:用户**明说**要发,或**某 Skill 步骤**书面要求发(执行时注明 Skill 与步骤多根工作区时主任务不在卡若AI 仓库则不发。
- **脚本**`02_卡人/水桥_平台对接/飞书管理/脚本/send_review_to_feishu_webhook.py`
- **用法**`python3 send_review_to_feishu_webhook.py "【卡若AI复盘】YYYY-MM-DD HH:mm\n🎯 目标·结果·达成率\n📌 完成要点\n▶ 下一步"`

View File

@@ -54,5 +54,5 @@ python3 "/Users/karuo/Documents/个人/卡若AI/01_卡资/金仓_存储
## 五、与飞书 / Gitea 的相对顺序(建议)
- **Mongo 同步** → **飞书复盘 webhook**若有)→ **Gitea 自动同步**(若本轮改仓库文件)→ **复盘块收尾**。
- **Mongo 同步** → **飞书复盘 webhook****仅当**用户或 Skill 步骤明确要求,见 `karuo-ai.mdc`)→ **Gitea 自动同步**(若本轮改仓库文件)→ **复盘块收尾**。
具体以 `.cursor/rules/karuo-ai.mdc` 为准。

View File

@@ -420,3 +420,4 @@
| 2026-03-22 13:22:16 | 🔄 卡若AI 同步 2026-03-22 13:22 | 更新:运营中枢参考资料、运营中枢工作台 | 排除 >20MB: 11 个 |
| 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 个 |

View File

@@ -423,3 +423,4 @@
| 2026-03-22 13:22:16 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-22 13:22 | 更新:运营中枢参考资料、运营中枢工作台 | 排除 >20MB: 11 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
| 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) |

View File

@@ -9,7 +9,7 @@
| key | 类型 | 名称 | enabled | 网关路径 | 外部地址 / webhook | 说明(可编辑) |
|:---|:---|:---|:---:|:---|:---|:---|
| feishu_review | 飞书群机器人 | 卡若AI复盘 | ✅ | `/api/integrations/webhook/feishu-review` | `https://open.feishu.cn/open-apis/bot/v2/hook/8b7f996e-2892-4075-989f-aa5593ea4fbc` | 每次对话完成后发送简洁复盘总结;脚本`send_review_to_feishu_webhook.py` |
| feishu_review | 飞书群机器人 | 卡若AI复盘 | ✅ | `/api/integrations/webhook/feishu-review` | `https://open.feishu.cn/open-apis/bot/v2/hook/8b7f996e-2892-4075-989f-aa5593ea4fbc` | **按需**:用户明说或 Skill 步骤要求时,由脚本 `send_review_to_feishu_webhook.py` 发简洁复盘;非每轮自动 |
| github_push | GitHub Webhook | 代码推送通知 | ✅ | `/api/integrations/webhook/github-push` | GitHub 仓库 Webhook 推送地址) | main 分支推送时通知到卡若AI 网关或飞书群,用于代码变更追踪 |
> 说明列可自由编辑用于给卡若AI / 人类解释该集成的用途、触发条件与注意点。
@@ -28,6 +28,6 @@
## 三、与其他文档的关系
- **账号与 API**:具体凭证仍以 `00_账号与API索引.md` 为准,本文件只记录「集成级别」的信息(开关、路径、用途)。
- **飞书复盘总结发群**`feishu_review` 集成`send_review_to_feishu_webhook.py``复盘总结发飞书群_SKILL.md`Cursor 规则中的「飞书复盘总结发群」一一对应
- **飞书复盘总结发群**`feishu_review``send_review_to_feishu_webhook.py``复盘总结发飞书群_SKILL.md``.cursor/rules/karuo-ai.mdc` 中按需发群规则对应(默认不自动发)
- **Gitea / GitHub 推送**`github_push` 集成与 `Gitea管理` Skill 中的仓库推送策略、CI/通知策略关联。

View File

@@ -1,16 +1,16 @@
# 飞书复盘总结发群(对话结束后
# 飞书复盘总结发群(按需
> **强制**:每次对话完成、复盘写完后,将**简洁复盘总结**发到指定飞书群。长对话必发,每次完成的任务都发。
> 更新2026-03-12
> **默认不发。** 仅当业务与卡若AI 工作区**捆绑清楚**(用户明说或 Skill 步骤要求)时才发。
> 更新2026-03-23取消「每轮必发」
---
## 一、规则
- **时机**:对话结束、完成「卡若复盘」后。
- **时机**:对话结束、完成「卡若复盘」后**且**满足 Cursor 规则中的「捆绑 + 工作区」条件(见 `.cursor/rules/karuo-ai.mdc`
- **内容**:简洁版复盘总结(建议 ≤500 字),包含:日期时间、目标·结果·达成率、完成的任务要点、下一步(若有)。
- **对象**:飞书群(通过 bot v2 webhook 推送)。
- **频率**每次对话完成都发;**长对话尤其必须发**
- **频率****非默认**;用户口令或 Skill 步骤要求时才发。多根工作区时以**本轮主改动的仓库**为准主战场非卡若AI 不发
---
@@ -41,6 +41,6 @@ python3 .../send_review_to_feishu_webhook.py --file /path/to/summary.txt
## 四、与规则 / Skill 的对应
- **Cursor 规则**`.cursor/rules/karuo-ai.mdc`对话结束复盘后,执行发飞书群(见「复盘(所有对话强制)」后追加的「飞书复盘总结发群」)
- **对话沉淀与优化规则**`运营中枢/使用手册/对话沉淀与优化规则.md`沉淀必做项中增加「发简洁复盘总结到飞书群」
- **飞书管理 Skill**已记录「复盘总结发飞书群」能力与脚本路径(`飞书管理/复盘总结发飞书群_SKILL.md` 或飞书相关 SKILL 内章节)
- **Cursor 规则**`.cursor/rules/karuo-ai.mdc`飞书发群为**按需**,见「飞书复盘发群(默认关闭)」
- **对话沉淀与优化规则**`运营中枢/使用手册/对话沉淀与优化规则.md`§2.1 为按需项,非每轮必做
- **飞书管理 Skill**:能力与脚本见 `飞书管理/复盘总结发飞书群_SKILL.md`;某运营闭环若在自身 `SKILL.md` 中写明「本步发飞书」,则该步执行时**才算**捆绑明确