2026-03-22 08:34:28 +08:00
|
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
"""
|
2026-03-23 18:38:23 +08:00
|
|
|
|
Soul 运营全链路技能包(精简版):只打包 SKILL / 脚本 / 小配置,便于另一台机 pip/conda 重装。
|
|
|
|
|
|
大文件、媒体、Cookie、日志等一律不入包。
|
|
|
|
|
|
|
2026-03-22 08:34:28 +08:00
|
|
|
|
用法:
|
|
|
|
|
|
python3 scripts/pack_soul_operation_skills.py
|
2026-03-23 18:38:23 +08:00
|
|
|
|
输出:
|
|
|
|
|
|
~/Downloads/Soul运营全链路技能包_精简_YYYYMMDD.zip
|
2026-03-22 08:34:28 +08:00
|
|
|
|
"""
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
2026-03-23 18:38:23 +08:00
|
|
|
|
import datetime as _dt
|
|
|
|
|
|
import json
|
|
|
|
|
|
import os
|
2026-03-22 08:34:28 +08:00
|
|
|
|
import shutil
|
|
|
|
|
|
import sys
|
|
|
|
|
|
import zipfile
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
2026-03-23 18:38:23 +08:00
|
|
|
|
# 单文件超过此大小则跳过(字节)——非「代码/文档类」扩展名
|
|
|
|
|
|
MAX_FILE_BYTES = 512 * 1024 # 512KB
|
|
|
|
|
|
|
|
|
|
|
|
# 脚本与文档类可放宽(避免误跳过大 .py/.md;仍远小于整包 200MB+)
|
|
|
|
|
|
CODE_DOC_EXT = frozenset(
|
|
|
|
|
|
{
|
|
|
|
|
|
".py",
|
|
|
|
|
|
".md",
|
|
|
|
|
|
".mdc",
|
|
|
|
|
|
".sh",
|
|
|
|
|
|
".bash",
|
|
|
|
|
|
".zsh",
|
|
|
|
|
|
".txt",
|
|
|
|
|
|
".json",
|
|
|
|
|
|
".yaml",
|
|
|
|
|
|
".yml",
|
|
|
|
|
|
".toml",
|
|
|
|
|
|
".cfg",
|
|
|
|
|
|
".ini",
|
|
|
|
|
|
".sql",
|
|
|
|
|
|
".html",
|
|
|
|
|
|
".css",
|
|
|
|
|
|
".js",
|
|
|
|
|
|
".ts",
|
|
|
|
|
|
".tsx",
|
|
|
|
|
|
".jsx",
|
|
|
|
|
|
".svg",
|
|
|
|
|
|
".xml",
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
MAX_CODE_DOC_BYTES = 8 * 1024 * 1024 # 8MB
|
|
|
|
|
|
|
|
|
|
|
|
# 整段目录名匹配则不进包(walk 时不进入)
|
|
|
|
|
|
SKIP_DIR_NAMES = frozenset(
|
|
|
|
|
|
{
|
|
|
|
|
|
"__pycache__",
|
|
|
|
|
|
".git",
|
|
|
|
|
|
".svn",
|
|
|
|
|
|
".browser_state",
|
|
|
|
|
|
"chromium_data",
|
|
|
|
|
|
"node_modules",
|
|
|
|
|
|
"venv",
|
|
|
|
|
|
".venv",
|
|
|
|
|
|
".mypy_cache",
|
|
|
|
|
|
".pytest_cache",
|
|
|
|
|
|
".tox",
|
|
|
|
|
|
"dist",
|
|
|
|
|
|
"build",
|
|
|
|
|
|
"eggs",
|
|
|
|
|
|
".eggs",
|
|
|
|
|
|
"htmlcov",
|
|
|
|
|
|
".ruff_cache",
|
|
|
|
|
|
# Cookie 到新机需重新登录导出,不入包
|
|
|
|
|
|
"cookies",
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 扩展名一律跳过(媒体/模型/压缩包等)
|
|
|
|
|
|
SKIP_EXTENSIONS = frozenset(
|
|
|
|
|
|
{
|
|
|
|
|
|
".mp4",
|
|
|
|
|
|
".mov",
|
|
|
|
|
|
".mkv",
|
|
|
|
|
|
".avi",
|
|
|
|
|
|
".webm",
|
|
|
|
|
|
".m4v",
|
|
|
|
|
|
".flv",
|
|
|
|
|
|
".wmv",
|
|
|
|
|
|
".zip",
|
|
|
|
|
|
".tar",
|
|
|
|
|
|
".gz",
|
|
|
|
|
|
".tgz",
|
|
|
|
|
|
".bz2",
|
|
|
|
|
|
".xz",
|
|
|
|
|
|
".rar",
|
|
|
|
|
|
".7z",
|
|
|
|
|
|
".dmg",
|
|
|
|
|
|
".iso",
|
|
|
|
|
|
".img",
|
|
|
|
|
|
".pt",
|
|
|
|
|
|
".pth",
|
|
|
|
|
|
".onnx",
|
|
|
|
|
|
".ckpt",
|
|
|
|
|
|
".safetensors",
|
|
|
|
|
|
".bin",
|
|
|
|
|
|
".exe",
|
|
|
|
|
|
".dll",
|
|
|
|
|
|
".so",
|
|
|
|
|
|
".dylib",
|
|
|
|
|
|
".wav",
|
|
|
|
|
|
".mp3",
|
|
|
|
|
|
".flac",
|
|
|
|
|
|
".aac",
|
|
|
|
|
|
".m4a",
|
|
|
|
|
|
".npz",
|
|
|
|
|
|
".npy",
|
|
|
|
|
|
".pkl",
|
|
|
|
|
|
".pickle",
|
|
|
|
|
|
".whl",
|
|
|
|
|
|
".parquet",
|
|
|
|
|
|
".arrow",
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 文件名(不含路径)强制跳过
|
|
|
|
|
|
SKIP_FILE_NAMES = frozenset(
|
|
|
|
|
|
{
|
|
|
|
|
|
".DS_Store",
|
|
|
|
|
|
"Thumbs.db",
|
|
|
|
|
|
"publish_log.json", # 分发日志可能巨大
|
|
|
|
|
|
".feishu_tokens.json", # 凭证,到新机用脚本重新获取更安全;若需带走可自行拷贝
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
2026-03-22 08:34:28 +08:00
|
|
|
|
|
|
|
|
|
|
# 卡若AI 根目录(按你本机实际修改)
|
|
|
|
|
|
KARUO_AI = Path("/Users/karuo/Documents/个人/卡若AI")
|
|
|
|
|
|
CURSOR_SKILLS = Path.home() / ".cursor" / "skills"
|
|
|
|
|
|
DOWNLOADS = Path.home() / "Downloads"
|
|
|
|
|
|
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
2026-03-23 18:38:23 +08:00
|
|
|
|
STAMP = _dt.date.today().strftime("%Y%m%d")
|
|
|
|
|
|
BUNDLE_TOP = f"Soul运营全链路技能包_精简_{STAMP}"
|
2026-03-22 08:34:28 +08:00
|
|
|
|
STAGING_PARENT = REPO_ROOT / ".tmp_skill_bundle"
|
|
|
|
|
|
STAGING = STAGING_PARENT / BUNDLE_TOP
|
|
|
|
|
|
|
2026-03-23 18:38:23 +08:00
|
|
|
|
# 统计
|
|
|
|
|
|
_stats: dict[str, int] = {"files": 0, "skipped_size": 0, "skipped_ext": 0, "skipped_dir": 0, "skipped_name": 0}
|
|
|
|
|
|
|
2026-03-22 08:34:28 +08:00
|
|
|
|
|
2026-03-23 18:38:23 +08:00
|
|
|
|
def should_skip_file(path: Path) -> tuple[bool, str]:
|
|
|
|
|
|
name = path.name
|
|
|
|
|
|
if name in SKIP_FILE_NAMES:
|
|
|
|
|
|
return True, "name"
|
|
|
|
|
|
ext = path.suffix.lower()
|
|
|
|
|
|
if ext in SKIP_EXTENSIONS:
|
|
|
|
|
|
return True, "ext"
|
|
|
|
|
|
try:
|
|
|
|
|
|
sz = path.stat().st_size
|
|
|
|
|
|
except OSError:
|
|
|
|
|
|
return True, "stat"
|
|
|
|
|
|
limit = MAX_CODE_DOC_BYTES if ext in CODE_DOC_EXT else MAX_FILE_BYTES
|
|
|
|
|
|
if sz > limit:
|
|
|
|
|
|
return True, "size"
|
|
|
|
|
|
return False, ""
|
2026-03-22 08:34:28 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-03-23 18:38:23 +08:00
|
|
|
|
def copy_tree_selective(src: Path, dst_root: Path, rel_base: Path) -> None:
|
|
|
|
|
|
"""将 src 下文件复制到 dst_root / rel_base,遵守跳过规则。"""
|
|
|
|
|
|
if not src.is_dir():
|
|
|
|
|
|
print(f"SKIP 非目录: {src}", file=sys.stderr)
|
2026-03-22 08:34:28 +08:00
|
|
|
|
return
|
2026-03-23 18:38:23 +08:00
|
|
|
|
for root, dirnames, filenames in os_walk_topdown(src):
|
|
|
|
|
|
root_path = Path(root)
|
|
|
|
|
|
# 过滤要进入的子目录
|
|
|
|
|
|
for d in list(dirnames):
|
|
|
|
|
|
if d in SKIP_DIR_NAMES:
|
|
|
|
|
|
dirnames.remove(d)
|
|
|
|
|
|
_stats["skipped_dir"] += 1
|
|
|
|
|
|
rel = root_path.relative_to(src)
|
|
|
|
|
|
for fname in filenames:
|
|
|
|
|
|
fp = root_path / fname
|
|
|
|
|
|
skip, reason = should_skip_file(fp)
|
|
|
|
|
|
if skip:
|
|
|
|
|
|
if reason == "size":
|
|
|
|
|
|
_stats["skipped_size"] += 1
|
|
|
|
|
|
elif reason == "ext":
|
|
|
|
|
|
_stats["skipped_ext"] += 1
|
|
|
|
|
|
elif reason == "name":
|
|
|
|
|
|
_stats["skipped_name"] += 1
|
|
|
|
|
|
continue
|
|
|
|
|
|
dest_dir = dst_root / rel_base / rel
|
|
|
|
|
|
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
dest = dest_dir / fname
|
|
|
|
|
|
shutil.copy2(fp, dest)
|
|
|
|
|
|
_stats["files"] += 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def os_walk_topdown(src: Path):
|
|
|
|
|
|
"""与 os.walk 相同,但用 Path。"""
|
|
|
|
|
|
for r, dnames, fnames in os.walk(str(src), topdown=True):
|
|
|
|
|
|
yield Path(r), dnames, fnames
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def copy_cursor_skill(name: str) -> None:
|
|
|
|
|
|
src = CURSOR_SKILLS / name
|
|
|
|
|
|
if not src.is_dir():
|
|
|
|
|
|
print(f"SKIP 无 Cursor skill: {src}", file=sys.stderr)
|
|
|
|
|
|
return
|
|
|
|
|
|
copy_tree_selective(src, STAGING, Path(".cursor") / "skills" / name)
|
2026-03-22 08:34:28 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main() -> int:
|
|
|
|
|
|
if not KARUO_AI.is_dir():
|
|
|
|
|
|
print(f"ERROR: 未找到卡若AI目录: {KARUO_AI}", file=sys.stderr)
|
|
|
|
|
|
return 1
|
|
|
|
|
|
|
2026-03-23 18:38:23 +08:00
|
|
|
|
global _stats
|
|
|
|
|
|
_stats = {k: 0 for k in _stats}
|
|
|
|
|
|
|
2026-03-22 08:34:28 +08:00
|
|
|
|
if STAGING.exists():
|
|
|
|
|
|
shutil.rmtree(STAGING)
|
|
|
|
|
|
STAGING.mkdir(parents=True)
|
|
|
|
|
|
|
2026-03-23 18:38:23 +08:00
|
|
|
|
# Cursor 入口(通常只有 SKILL.md)
|
2026-03-22 08:34:28 +08:00
|
|
|
|
for name in ("soul-operation-report", "soul-party-project"):
|
2026-03-23 18:38:23 +08:00
|
|
|
|
copy_cursor_skill(name)
|
2026-03-22 08:34:28 +08:00
|
|
|
|
|
2026-03-23 18:38:23 +08:00
|
|
|
|
kai_rel = Path("卡若AI")
|
|
|
|
|
|
|
|
|
|
|
|
def pack_sub(src_under_karuo: Path, rel_under_kai: Path) -> None:
|
|
|
|
|
|
"""src_under_karuo 为卡若AI下的绝对路径;打入包内 卡若AI/rel_under_kai"""
|
|
|
|
|
|
if not src_under_karuo.exists():
|
|
|
|
|
|
print(f"SKIP 不存在: {src_under_karuo}", file=sys.stderr)
|
|
|
|
|
|
return
|
|
|
|
|
|
copy_tree_selective(src_under_karuo, STAGING, kai_rel / rel_under_kai)
|
|
|
|
|
|
|
|
|
|
|
|
pack_sub(
|
2026-03-22 08:34:28 +08:00
|
|
|
|
KARUO_AI / "02_卡人(水)" / "水岸_项目管理",
|
2026-03-23 18:38:23 +08:00
|
|
|
|
Path("02_卡人(水)") / "水岸_项目管理",
|
2026-03-22 08:34:28 +08:00
|
|
|
|
)
|
|
|
|
|
|
bridge = KARUO_AI / "02_卡人(水)" / "水桥_平台对接"
|
|
|
|
|
|
for sub in ("飞书管理", "智能纪要", "Soul创业实验"):
|
2026-03-23 18:38:23 +08:00
|
|
|
|
pack_sub(bridge / sub, Path("02_卡人(水)") / "水桥_平台对接" / sub)
|
2026-03-22 08:34:28 +08:00
|
|
|
|
|
|
|
|
|
|
wood = KARUO_AI / "03_卡木(木)" / "木叶_视频内容"
|
|
|
|
|
|
for sub in (
|
|
|
|
|
|
"视频切片",
|
|
|
|
|
|
"多平台分发",
|
|
|
|
|
|
"抖音发布",
|
|
|
|
|
|
"B站发布",
|
|
|
|
|
|
"视频号发布",
|
|
|
|
|
|
"小红书发布",
|
|
|
|
|
|
"快手发布",
|
|
|
|
|
|
):
|
2026-03-23 18:38:23 +08:00
|
|
|
|
pack_sub(wood / sub, Path("03_卡木(木)") / "木叶_视频内容" / sub)
|
2026-03-22 08:34:28 +08:00
|
|
|
|
|
|
|
|
|
|
idx = KARUO_AI / "运营中枢" / "工作台" / "00_账号与API索引.md"
|
|
|
|
|
|
if idx.is_file():
|
2026-03-23 18:38:23 +08:00
|
|
|
|
skip, _ = should_skip_file(idx)
|
|
|
|
|
|
if not skip:
|
|
|
|
|
|
dest = STAGING / kai_rel / "运营中枢" / "工作台" / idx.name
|
|
|
|
|
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
shutil.copy2(idx, dest)
|
|
|
|
|
|
_stats["files"] += 1
|
|
|
|
|
|
|
|
|
|
|
|
# 写入 requirements 汇总(若各目录有 requirements.txt,只列路径提示,不合并)
|
|
|
|
|
|
req_hint = STAGING / "重装依赖说明.md"
|
|
|
|
|
|
req_hint.write_text(
|
|
|
|
|
|
f"""# 重装依赖说明(精简包)
|
|
|
|
|
|
|
|
|
|
|
|
本包**不含**大文件与本地状态,到新电脑请:
|
|
|
|
|
|
|
|
|
|
|
|
1. **Python**:建议 3.10+;进入各含 `requirements.txt` 的脚本目录执行 `pip install -r requirements.txt`(以各 SKILL 为准)。
|
|
|
|
|
|
2. **系统**:`ffmpeg`、`ffprobe`(视频切片);视频转录见 SKILL 中的 **conda mlx-whisper** 环境说明。
|
|
|
|
|
|
3. **Playwright**(若飞书脚本需要):`playwright install` 并按脚本说明登录;**`.browser_state` 未打包**。
|
|
|
|
|
|
4. **多平台分发**:包内**不含 `cookies/` 目录**,需在新机各平台重新登录导出 Cookie(见多平台分发 SKILL)。
|
|
|
|
|
|
5. **飞书 Token**:精简包默认**不含** `.feishu_tokens.json`,请在新机用脚本流程重新授权;若你刻意要迁移凭证请单独拷贝(注意安全)。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
打包策略摘要(自动生成):
|
|
|
|
|
|
|
|
|
|
|
|
- 代码/文档类(`.py`、`.md` 等)单文件大于 **{MAX_CODE_DOC_BYTES // (1024 * 1024)} MB** 跳过;其它类型大于 **{MAX_FILE_BYTES // 1024} KB** 跳过
|
|
|
|
|
|
- 跳过扩展名:媒体、压缩包、模型权重等
|
|
|
|
|
|
- 跳过目录:`cookies`、`node_modules`、`.browser_state`、`venv` 等
|
|
|
|
|
|
|
|
|
|
|
|
打包日期:**{STAMP}**
|
|
|
|
|
|
""",
|
|
|
|
|
|
encoding="utf-8",
|
|
|
|
|
|
)
|
2026-03-22 08:34:28 +08:00
|
|
|
|
|
|
|
|
|
|
readme = STAGING / "解压后必读.md"
|
|
|
|
|
|
readme.write_text(
|
2026-03-23 18:38:23 +08:00
|
|
|
|
f"""# Soul 运营全链路技能包(精简版)
|
|
|
|
|
|
|
|
|
|
|
|
## 本包特点
|
|
|
|
|
|
|
|
|
|
|
|
- **体积小**:不含视频/大日志/模型/Cookie 目录等;到新机器按 `重装依赖说明.md` **重装环境与凭证**。
|
|
|
|
|
|
- **日期**:{STAMP}
|
2026-03-22 08:34:28 +08:00
|
|
|
|
|
2026-03-23 18:38:23 +08:00
|
|
|
|
## 包含
|
2026-03-22 08:34:28 +08:00
|
|
|
|
|
|
|
|
|
|
- `.cursor/skills/`:`soul-operation-report`、`soul-party-project`
|
2026-03-23 18:38:23 +08:00
|
|
|
|
- `卡若AI/` 下水岸、飞书管理、智能纪要、Soul创业实验、视频切片、多平台分发与各平台发布目录中的 **SKILL、脚本、小配置**(受大小与类型过滤)
|
2026-03-22 08:34:28 +08:00
|
|
|
|
|
2026-03-23 18:38:23 +08:00
|
|
|
|
## 合并步骤
|
2026-03-22 08:34:28 +08:00
|
|
|
|
|
2026-03-23 18:38:23 +08:00
|
|
|
|
1. 解压后把 `卡若AI/` **合并**进你的卡若AI根目录(先备份)。
|
|
|
|
|
|
2. 将 `.cursor/skills/` 下两个文件夹复制到 `~/.cursor/skills/`。
|
|
|
|
|
|
3. 阅读 **`重装依赖说明.md`**,安装 Python 依赖、FFmpeg、conda 环境等。
|
|
|
|
|
|
4. 配置飞书、妙记、各平台 Cookie、永平 `.env`(见各 SKILL 与 `Soul创业实验/上传/环境与TOKEN配置.md`)。
|
2026-03-22 08:34:28 +08:00
|
|
|
|
|
2026-03-23 18:38:23 +08:00
|
|
|
|
**安全**:勿将含密钥的压缩包上传公开网盘。
|
2026-03-22 08:34:28 +08:00
|
|
|
|
""",
|
|
|
|
|
|
encoding="utf-8",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-03-23 18:38:23 +08:00
|
|
|
|
# 打包统计写入 JSON(便于核对)
|
|
|
|
|
|
(STAGING / "_pack_stats.json").write_text(
|
|
|
|
|
|
json.dumps({**_stats, "max_file_bytes": MAX_FILE_BYTES, "stamp": STAMP}, ensure_ascii=False, indent=2),
|
|
|
|
|
|
encoding="utf-8",
|
|
|
|
|
|
)
|
2026-03-22 08:34:28 +08:00
|
|
|
|
|
|
|
|
|
|
DOWNLOADS.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
zip_path = DOWNLOADS / f"{BUNDLE_TOP}.zip"
|
|
|
|
|
|
|
|
|
|
|
|
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
|
|
|
|
for f in STAGING.rglob("*"):
|
|
|
|
|
|
if f.is_file():
|
|
|
|
|
|
arcname = Path(BUNDLE_TOP) / f.relative_to(STAGING)
|
|
|
|
|
|
zf.write(f, arcname.as_posix())
|
|
|
|
|
|
|
|
|
|
|
|
mb = zip_path.stat().st_size / (1024 * 1024)
|
2026-03-23 18:38:23 +08:00
|
|
|
|
print(f"完成: {zip_path}")
|
|
|
|
|
|
print(f"大小: {mb:.2f} MB | 打入文件数: {_stats['files']}")
|
|
|
|
|
|
print(
|
|
|
|
|
|
f"跳过: 超体积 {_stats['skipped_size']} | 扩展名 {_stats['skipped_ext']} | 文件名 {_stats['skipped_name']} | 目录 {_stats['skipped_dir']}"
|
|
|
|
|
|
)
|
2026-03-22 08:34:28 +08:00
|
|
|
|
print(f"临时目录(可删): {STAGING}")
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
raise SystemExit(main())
|