Files
karuo-ai/05_卡土(土)/土砖_技能复制/基因胶囊/脚本/gene_capsule.py

367 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
基因胶囊 · 打包/解包/列表
卡若AI 基因胶囊管理脚本,归属土砖。
"""
import argparse
import hashlib
import json
import os
import platform
import sys
from datetime import datetime
from pathlib import Path
# 卡若AI 工作台根目录
KARUO_AI_ROOT = Path(__file__).resolve().parent.parent.parent.parent.parent
EXPORT_DIR = Path("/Users/karuo/Documents/卡若Ai的文件夹/导出/基因胶囊")
CAPSULE_INDEX = Path(__file__).resolve().parent.parent / "capsule_index.json"
def _collect_env():
"""收集环境指纹(可选)"""
return {
"python_version": f"{sys.version_info.major}.{sys.version_info.minor}",
"platform": platform.system().lower(),
}
def _calc_capsule_id(skill_path: str, content: str) -> str:
"""计算资产 IDSHA256(skill_path + content 前 8192 字符)"""
data = (skill_path + content[:8192]).encode("utf-8")
h = hashlib.sha256(data).hexdigest()
return f"sha256:{h}"
def _find_skill_path(name_or_path: str) -> Path:
"""根据技能名或路径找到 SKILL.md"""
if name_or_path.endswith("SKILL.md"):
p = KARUO_AI_ROOT / name_or_path
elif os.path.sep in name_or_path or name_or_path.startswith("0"):
# 形如 05_卡土/土砖_技能复制/技能工厂
p = KARUO_AI_ROOT / name_or_path
if p.is_dir():
p = p / "SKILL.md"
else:
# 按名称在 SKILL_REGISTRY 中查找(简化:扫描常见路径)
registry = KARUO_AI_ROOT / "SKILL_REGISTRY.md"
if registry.exists():
text = registry.read_text(encoding="utf-8")
# 简单匹配:找包含 name_or_path 的 SKILL 路径
for line in text.splitlines():
if "SKILL.md" in line and name_or_path in line:
# 提取路径,如 `05_卡土/土砖_技能复制/技能工厂/SKILL.md`
start = line.find("`") + 1
end = line.rfind("`")
if start > 0 and end > start:
rel = line[start:end].strip()
p = KARUO_AI_ROOT / rel
if p.exists():
return p
raise FileNotFoundError(f"未找到技能: {name_or_path}")
if not p.exists():
raise FileNotFoundError(f"SKILL 不存在: {p}")
return p
def _parse_frontmatter(content: str) -> tuple:
"""解析 YAML frontmatter返回 (frontmatter_dict, body)。无 PyYAML 时用简单解析。"""
if not content.strip().startswith("---"):
return {}, content
parts = content.split("---", 2)
if len(parts) < 3:
return {}, content
fm = {}
try:
import yaml
fm = yaml.safe_load(parts[1]) or {}
except Exception:
for line in parts[1].strip().splitlines():
if ":" in line:
k, v = line.split(":", 1)
k, v = k.strip(), v.strip().strip("'\"").strip()
if k == "triggers":
fm[k] = [x.strip() for x in v.replace("", ",").split(",") if x.strip()]
else:
fm[k] = v
return fm, parts[2].strip()
EXPORT_README = "README_基因胶囊导出说明.md"
def _render_flowchart_mermaid() -> str:
"""返回基因胶囊功能流程图 Mermaid 源码"""
return """```mermaid
flowchart TB
subgraph internal["卡若AI 内部流程"]
A1[用户需求/任务] --> A2{查 SKILL_REGISTRY}
A2 --> A3[读 SKILL.md 执行]
A3 --> A4[执行完成 + 复盘]
A4 --> A5{经验有价值?}
A5 -->|是| A6[写入经验库/待沉淀]
A5 -->|否| A7[结束]
A6 --> A8{要打包为胶囊?}
end
subgraph capsule["基因胶囊 核心流程"]
B1[pack: Skill 转 胶囊JSON] --> B2[导出目录 + 说明文档]
B2 --> B3[list: 查看本地胶囊]
B3 --> B4{需要继承?}
B4 -->|是| B5[unpack: 胶囊 转 Skill]
B4 -->|否| B6[保留备用]
B5 --> B7[写入对应成员目录]
B7 --> B8[更新 SKILL_REGISTRY]
end
subgraph factory["技能工厂联动"]
C1[创建 Skill 前] --> C2[list 查本地胶囊]
C2 --> C3{有匹配?}
C3 -->|是| C4[unpack 继承]
C3 -->|否| C5[新建 Skill]
C5 --> C6[创建 Skill 后]
C6 --> C7[pack 打包为胶囊]
C4 --> B7
end
subgraph external["未来对外流通"]
D1[EvoMap Market] --> D2[上传胶囊]
D2 --> D3[全球 Agent 继承]
D3 --> B5
B2 -.->|可选| D2
end
A8 -->|是| B1
A8 -->|否| A7
```"""
def _write_export_readme() -> str:
"""生成/更新导出说明文档(含流程图),返回说明文档路径"""
EXPORT_DIR.mkdir(parents=True, exist_ok=True)
readme_path = EXPORT_DIR / EXPORT_README
# 收集已导出胶囊列表(按创建时间倒序)
caps = []
if EXPORT_DIR.exists():
for f in sorted(EXPORT_DIR.glob("*.json")):
try:
data = json.loads(f.read_text(encoding="utf-8"))
m = data.get("manifest", {})
caps.append({
"file": f.name,
"name": m.get("name", ""),
"capsule_id": data.get("capsule_id", "")[:19],
"created_at": data.get("created_at", ""),
})
except Exception:
pass
caps.sort(key=lambda x: x.get("created_at", ""), reverse=True)
lines = [
"# 基因胶囊 · 导出说明",
"",
"> **生成时间**{now} ",
"> **导出目录**`卡若Ai的文件夹/导出/基因胶囊/` ",
"> **规范文档**`运营中枢/参考资料/基因胶囊规范.md`",
"",
"---",
"",
"## 一、基因胶囊功能总览",
"",
"**基因胶囊**将验证过的 Skill + 环境指纹 + 审计记录打包为可遗传能力单元,支持:",
"",
"- **pack**Skill → 胶囊 JSON导出",
"- **unpack**:胶囊 JSON → Skill继承",
"- **list**:查看本地所有胶囊",
"",
"---",
"",
"## 二、基因胶囊完整流程图",
"",
"以下流程图展示卡若AI 中基因胶囊的完整工作流程、与技能工厂的联动,以及未来与 EvoMap 的流通路径。",
"",
_render_flowchart_mermaid(),
"",
"---",
"",
"## 三、已导出胶囊清单",
"",
"| 技能名 | 胶囊文件 | capsule_id | 创建时间 |",
"|:---|:---|:---|:---|",
]
for c in caps:
lines.append(f"| {c.get('name', '')} | {c.get('file', '')} | {c.get('capsule_id', '')} | {c.get('created_at', '')} |")
if not caps:
lines.append("| (暂无) | — | — | — |")
lines.extend([
"",
"---",
"",
"## 四、使用方法",
"",
"### 解包(继承能力)",
"",
"```bash",
"cd /Users/karuo/Documents/个人/卡若AI",
'python3 "05_卡土/土砖_技能复制/基因胶囊/脚本/gene_capsule.py" unpack 技能名_xxxx.json',
'python3 .../gene_capsule.py unpack 技能名_xxxx.json -o 目标目录/ # 指定输出目录',
"```",
"",
"### 列表",
"",
"```bash",
'python3 .../gene_capsule.py list',
"```",
"",
"---",
"",
"## 五、引用",
"",
"- 规范:`运营中枢/参考资料/基因胶囊规范.md`",
"- 技能:`05_卡土/土砖_技能复制/基因胶囊/SKILL.md`",
"- 流程图亦可于规范文档中查看",
"",
])
now = datetime.now().strftime("%Y-%m-%d %H:%M")
content = "\n".join(lines).replace("{now}", now)
readme_path.write_text(content, encoding="utf-8")
return str(readme_path)
def pack(skill_ref: str, include_audit: bool = True) -> str:
"""打包:将 SKILL 转为基因胶囊 JSON并生成导出说明文档含流程图"""
skill_path = _find_skill_path(skill_ref)
content = skill_path.read_text(encoding="utf-8")
rel_path = str(skill_path.relative_to(KARUO_AI_ROOT))
fm, body = _parse_frontmatter(content)
manifest = {
"name": fm.get("name", skill_path.stem),
"description": fm.get("description", ""),
"triggers": fm.get("triggers", [])
if isinstance(fm.get("triggers"), list)
else (fm.get("triggers", "") or "").split(""),
"owner": fm.get("owner", ""),
"group": fm.get("group", ""),
"skill_path": rel_path,
}
capsule_id = _calc_capsule_id(rel_path, content)
now = datetime.now().strftime("%Y-%m-%dT%H:%M:%S+08:00")
capsule = {
"version": "1.0",
"capsule_id": capsule_id,
"manifest": manifest,
"skill_content": content,
"created_at": now,
"updated_at": now,
"environment": _collect_env(),
"source": "karuo-ai",
}
if include_audit:
capsule["audit"] = {"last_retro": "", "source": "pack"}
EXPORT_DIR.mkdir(parents=True, exist_ok=True)
safe_name = manifest["name"].replace(" ", "_").replace("/", "_")[:30]
out_file = EXPORT_DIR / f"{safe_name}_{capsule_id[7:15]}.json"
out_file.write_text(json.dumps(capsule, ensure_ascii=False, indent=2), encoding="utf-8")
# 生成导出说明文档(含完整流程图)
readme_path = _write_export_readme()
return str(out_file) + "\n📄 说明文档: " + readme_path
def unpack(capsule_path: str, target_dir: str | None = None) -> str:
"""解包:将胶囊 JSON 解压为 SKILL.md"""
p = Path(capsule_path)
if not p.exists():
p = EXPORT_DIR / capsule_path
if not p.exists():
raise FileNotFoundError(f"胶囊不存在: {capsule_path}")
data = json.loads(p.read_text(encoding="utf-8"))
manifest = data.get("manifest", {})
skill_content = data.get("skill_content", "")
if target_dir:
dest = Path(target_dir)
else:
# 按 manifest.skill_path 写回
rel = manifest.get("skill_path", "")
dest = KARUO_AI_ROOT / Path(rel).parent
dest.mkdir(parents=True, exist_ok=True)
skill_file = dest / "SKILL.md"
skill_file.write_text(skill_content, encoding="utf-8")
# 更新 capsule_index
idx = {}
if CAPSULE_INDEX.exists():
idx = json.loads(CAPSULE_INDEX.read_text(encoding="utf-8"))
cid = data.get("capsule_id", "")[:19]
idx[cid] = {
"name": manifest.get("name", ""),
"source": data.get("source", "local"),
"unpacked_at": dest,
"created_at": data.get("created_at", ""),
}
CAPSULE_INDEX.write_text(json.dumps(idx, ensure_ascii=False, indent=2), encoding="utf-8")
return str(skill_file)
def list_capsules() -> list[dict]:
"""列表:扫描导出目录与索引"""
result = []
if EXPORT_DIR.exists():
for f in sorted(EXPORT_DIR.glob("*.json")):
try:
data = json.loads(f.read_text(encoding="utf-8"))
m = data.get("manifest", {})
result.append({
"file": f.name,
"name": m.get("name", ""),
"capsule_id": data.get("capsule_id", "")[:19],
"created_at": data.get("created_at", ""),
})
except Exception:
pass
return result
def main():
parser = argparse.ArgumentParser(description="基因胶囊 · pack/unpack/list")
sub = parser.add_subparsers(dest="cmd", required=True)
# pack
p_pack = sub.add_parser("pack", help="将 SKILL 打包为基因胶囊")
p_pack.add_argument("skill", help="技能路径或名称,如 技能工厂 或 05_卡土/土砖_技能复制/技能工厂/SKILL.md")
p_pack.add_argument("--no-audit", action="store_true", help="不包含审计信息")
# unpack
p_unpack = sub.add_parser("unpack", help="将胶囊解包为 SKILL")
p_unpack.add_argument("capsule", help="胶囊文件路径或文件名")
p_unpack.add_argument("-o", "--output", help="输出目录,默认按 manifest.skill_path")
# list
p_list = sub.add_parser("list", help="列出本地胶囊")
args = parser.parse_args()
if args.cmd == "pack":
out = pack(args.skill, include_audit=not args.no_audit)
parts = out.split("\n")
print(f"✅ 已打包: {parts[0]}")
if len(parts) > 1:
print(parts[1])
elif args.cmd == "unpack":
out = unpack(args.capsule, target_dir=args.output)
print(f"✅ 已解包: {out}")
elif args.cmd == "list":
items = list_capsules()
for i in items:
print(f" {i['name']} {i['capsule_id']} {i['created_at']}")
print(f"{len(items)} 个胶囊")
if __name__ == "__main__":
main()