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

220 lines
7.8 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()
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")
return str(out_file)
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)
print(f"✅ 已打包: {out}")
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()