🔄 卡若AI 同步 2026-03-23 09:48 | 更新:Cursor规则、金仓、水桥平台对接、卡木、火炬、运营中枢、运营中枢参考资料、运营中枢工作台 | 排除 >20MB: 11 个
This commit is contained in:
@@ -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 字内说明后执行
|
||||
- **常规操作**:优先命令行 + 复用现成流程,不提问
|
||||
- **默认零提问(强制)**:开发、改需求、跑脚本、查日志、部署类任务,**禁止**向卡若发起「是否执行」「要不要我…」「请选一个」等确认式提问。缺信息时:**先读仓库配置 / 代码 / 环境变量 / 文档** → 合理默认 → **直接做完**。仅当 **客观上无法代劳** 时极简说明缺什么(如:本机短信验证码、支付密码、用户明文密钥未配置、明确不可逆删除且规范要求人工确认)。
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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创业",
|
||||
|
||||
@@ -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`。
|
||||
|
||||
---
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -7,14 +7,21 @@ description: >
|
||||
triggers: 多平台分发、一键分发、全平台发布、批量分发、视频分发
|
||||
owner: 木叶
|
||||
group: 木
|
||||
version: "4.0"
|
||||
updated: "2026-03-11"
|
||||
version: "4.1"
|
||||
updated: "2026-03-20"
|
||||
---
|
||||
|
||||
# 多平台分发 Skill(v4.0)
|
||||
# 多平台分发 Skill(v4.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
@@ -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.json(legacy)
|
||||
- 预检读取:多平台分发/cookies/视频号_cookies.json(central)
|
||||
以较新 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, "文件不存在或为空"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 去空格避免字间假空白 |
|
||||
|
||||
@@ -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": "单主题_收尾"}
|
||||
]
|
||||
|
||||
10
03_卡木(木)/木叶_视频内容/视频切片/场次稿/第128场_20260319_highlights.json
Normal file
10
03_卡木(木)/木叶_视频内容/视频切片/场次稿/第128场_20260319_highlights.json
Normal 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": "单主题_派对价值"}
|
||||
]
|
||||
145
03_卡木(木)/木叶_视频内容/视频切片/脚本/build_127_highlights_from_srt.py
Normal file
145
03_卡木(木)/木叶_视频内容/视频切片/脚本/build_127_highlights_from_srt.py
Normal 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()
|
||||
@@ -2,7 +2,7 @@
|
||||
"""
|
||||
Soul切片增强脚本 v2.0
|
||||
功能:
|
||||
1. 封面贴片:高光 hook_3sec 优先(吸睛),竖屏底图为**清晰帧 + 约 10% 轻模糊混入**(非全糊)+ 渐变
|
||||
1. 封面贴片:高光 hook_3sec 优先(吸睛),竖屏底图为**清晰帧 + 约 10% 轻模糊混入**(非全糊)+ 冷色渐变;**顶栏单条 Soul 绿 + 底部电影感渐隐 + 细内框 + 柔阴影标题**(避免粗描边与多条绿边廉价感)
|
||||
2. 烧录字幕(关键词高亮、可选逐字)
|
||||
3. 切除检出的长静音并重映射字幕时间轴
|
||||
4. 片尾 CTA(cta_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")
|
||||
|
||||
151
03_卡木(木)/木叶_视频内容/视频号发布/REFERENCE_开放能力_数据与集成.md
Normal file
151
03_卡木(木)/木叶_视频内容/视频号发布/REFERENCE_开放能力_数据与集成.md
Normal 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 · 木叶*
|
||||
@@ -6,8 +6,8 @@ description: >
|
||||
triggers: 视频号发布、发布到视频号、视频号登录、视频号上传、微信视频号
|
||||
owner: 木叶
|
||||
group: 木
|
||||
version: "3.0"
|
||||
updated: "2026-03-10"
|
||||
version: "3.1"
|
||||
updated: "2026-03-23"
|
||||
---
|
||||
|
||||
# 视频号发布 Skill(v3.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 微信扫码登录 |
|
||||
|
||||
45
03_卡木(木)/木叶_视频内容/视频号发布/credentials/README.md
Normal file
45
03_卡木(木)/木叶_视频内容/视频号发布/credentials/README.md
Normal 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。
|
||||
@@ -0,0 +1,5 @@
|
||||
# 复制为同目录下的 .env.open_platform 后填写(.env.open_platform 已被 .gitignore 忽略)
|
||||
WECHAT_OPEN_APPID=
|
||||
WECHAT_OPEN_APPSECRET=
|
||||
# 可选:长期 stable_token 本地缓存(勿提交)
|
||||
# WECHAT_OPEN_STABLE_TOKEN=
|
||||
@@ -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"],
|
||||
|
||||
@@ -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__":
|
||||
|
||||
99
03_卡木(木)/木叶_视频内容/视频号发布/脚本/channels_open_fetch.py
Normal file
99
03_卡木(木)/木叶_视频内容/视频号发布/脚本/channels_open_fetch.py
Normal 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
@@ -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"
|
||||
}
|
||||
@@ -285,7 +285,7 @@ for (const col of collections) {
|
||||
|
||||
2. **更新本 SKILL 经验库**(本文件末尾 §八)
|
||||
|
||||
3. **发飞书复盘总结**
|
||||
3. **飞书复盘总结(按需)**:仅当用户明说发群或规程要求时调用 `send_review_to_feishu_webhook.py`;默认不每轮自动发,见 `.cursor/rules/karuo-ai.mdc`
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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▶ 下一步"`
|
||||
|
||||
@@ -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` 为准。
|
||||
|
||||
@@ -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 个 |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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/通知策略关联。
|
||||
|
||||
|
||||
@@ -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` 中写明「本步发飞书」,则该步执行时**才算**捆绑明确。
|
||||
|
||||
Reference in New Issue
Block a user