Files
colleague-skill/tools/skill_writer.py
titanwings 6a0b31aa6c refactor: restructure to official AgentSkills/Claude Code skill format
- Flatten colleague-creator/ to repo root (repo = skill directory)
- Update SKILL.md frontmatter with official fields: name, description,
  argument-hint, version, user-invocable, allowed-tools
- Move PRD.md → docs/PRD.md
- Add .gitignore, requirements.txt, LICENSE
- Update README and INSTALL docs to reflect new structure and git clone install

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 13:37:54 +08:00

381 lines
12 KiB
Python
Raw Permalink 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
"""
Skill 文件写入器
负责将生成的 work.md、persona.md 写入到正确的目录结构,
并生成 meta.json 和完整的 SKILL.md。
用法:
python3 skill_writer.py --action create --slug zhangsan --meta meta.json \
--work work_content.md --persona persona_content.md \
--base-dir ./colleagues
python3 skill_writer.py --action update --slug zhangsan \
--work-patch work_patch.md --persona-patch persona_patch.md \
--base-dir ./colleagues
python3 skill_writer.py --action list --base-dir ./colleagues
"""
from __future__ import annotations
import json
import shutil
import argparse
import sys
from pathlib import Path
from datetime import datetime, timezone
from typing import Optional
SKILL_MD_TEMPLATE = """\
---
name: colleague_{slug}
description: {name}{identity}
user-invocable: true
---
# {name}
{identity}
---
## PART A工作能力
{work_content}
---
## PART B人物性格
{persona_content}
---
## 运行规则
接收到任何任务或问题时:
1. **先由 PART B 判断**:你会不会接这个任务?用什么态度接?
2. **再由 PART A 执行**:用你的技术能力和工作方法完成任务
3. **输出时保持 PART B 的表达风格**:你说话的方式、用词习惯、句式
**PART B 的 Layer 0 规则永远优先,任何情况下不得违背。**
"""
def slugify(name: str) -> str:
"""
将姓名转为 slug。
优先尝试 pypinyin如已安装否则 fallback 到简单处理。
"""
# 尝试用 pypinyin 转拼音
try:
from pypinyin import lazy_pinyin
parts = lazy_pinyin(name)
slug = "_".join(parts)
except ImportError:
# fallback保留 ASCII 字母数字,中文直接去掉
import unicodedata
result = []
for char in name.lower():
cat = unicodedata.category(char)
if char.isascii() and (char.isalnum() or char in ("-", "_")):
result.append(char)
elif char == " ":
result.append("_")
# 中文字符跳过(无 pypinyin 时无法转换)
slug = "".join(result)
# 清理:去掉连续下划线,首尾下划线
import re
slug = re.sub(r"_+", "_", slug).strip("_")
return slug if slug else "colleague"
def build_identity_string(meta: dict) -> str:
"""从 meta 构建身份描述字符串"""
profile = meta.get("profile", {})
parts = []
company = profile.get("company", "")
level = profile.get("level", "")
role = profile.get("role", "")
if company:
parts.append(company)
if level:
parts.append(level)
if role:
parts.append(role)
identity = " ".join(parts) if parts else "同事"
mbti = profile.get("mbti", "")
if mbti:
identity += f"MBTI {mbti}"
return identity
def create_skill(
base_dir: Path,
slug: str,
meta: dict,
work_content: str,
persona_content: str,
) -> Path:
"""创建新的同事 Skill 目录结构"""
skill_dir = base_dir / slug
skill_dir.mkdir(parents=True, exist_ok=True)
# 创建子目录
(skill_dir / "versions").mkdir(exist_ok=True)
(skill_dir / "knowledge" / "docs").mkdir(parents=True, exist_ok=True)
(skill_dir / "knowledge" / "messages").mkdir(parents=True, exist_ok=True)
(skill_dir / "knowledge" / "emails").mkdir(parents=True, exist_ok=True)
# 写入 work.md
(skill_dir / "work.md").write_text(work_content, encoding="utf-8")
# 写入 persona.md
(skill_dir / "persona.md").write_text(persona_content, encoding="utf-8")
# 生成并写入 SKILL.md
name = meta.get("name", slug)
identity = build_identity_string(meta)
skill_md = SKILL_MD_TEMPLATE.format(
slug=slug,
name=name,
identity=identity,
work_content=work_content,
persona_content=persona_content,
)
(skill_dir / "SKILL.md").write_text(skill_md, encoding="utf-8")
# 写入 work-only skill
work_only = (
f"---\nname: colleague_{slug}_work\n"
f"description: {name} 的工作能力(仅 Work无 Persona\n"
f"user-invocable: true\n---\n\n{work_content}\n"
)
(skill_dir / "work_skill.md").write_text(work_only, encoding="utf-8")
# 写入 persona-only skill
persona_only = (
f"---\nname: colleague_{slug}_persona\n"
f"description: {name} 的人物性格(仅 Persona无工作能力\n"
f"user-invocable: true\n---\n\n{persona_content}\n"
)
(skill_dir / "persona_skill.md").write_text(persona_only, encoding="utf-8")
# 写入 meta.json
now = datetime.now(timezone.utc).isoformat()
meta["slug"] = slug
meta.setdefault("created_at", now)
meta["updated_at"] = now
meta["version"] = "v1"
meta.setdefault("corrections_count", 0)
(skill_dir / "meta.json").write_text(
json.dumps(meta, ensure_ascii=False, indent=2),
encoding="utf-8",
)
return skill_dir
def update_skill(
skill_dir: Path,
work_patch: Optional[str] = None,
persona_patch: Optional[str] = None,
correction: Optional[dict] = None,
) -> str:
"""更新现有 Skill先存档当前版本再写入更新"""
meta_path = skill_dir / "meta.json"
meta = json.loads(meta_path.read_text(encoding="utf-8"))
current_version = meta.get("version", "v1")
try:
version_num = int(current_version.lstrip("v").split("_")[0]) + 1
except ValueError:
version_num = 2
new_version = f"v{version_num}"
# 存档当前版本
version_dir = skill_dir / "versions" / current_version
version_dir.mkdir(parents=True, exist_ok=True)
for fname in ("SKILL.md", "work.md", "persona.md"):
src = skill_dir / fname
if src.exists():
shutil.copy2(src, version_dir / fname)
# 应用 work patch
if work_patch:
current_work = (skill_dir / "work.md").read_text(encoding="utf-8")
new_work = current_work + "\n\n" + work_patch
(skill_dir / "work.md").write_text(new_work, encoding="utf-8")
# 应用 persona patch 或 correction
if persona_patch or correction:
current_persona = (skill_dir / "persona.md").read_text(encoding="utf-8")
if correction:
correction_line = (
f"\n- [{correction.get('scene', '通用')}] "
f"不应该 {correction['wrong']},应该 {correction['correct']}"
)
target = "## Correction 记录"
if target in current_persona:
insert_pos = current_persona.index(target) + len(target)
# 跳过紧跟的空行和"暂无"占位行
rest = current_persona[insert_pos:]
skip = "\n\n(暂无记录)"
if rest.startswith(skip):
rest = rest[len(skip):]
new_persona = current_persona[:insert_pos] + correction_line + rest
else:
new_persona = (
current_persona
+ f"\n\n## Correction 记录\n{correction_line}\n"
)
meta["corrections_count"] = meta.get("corrections_count", 0) + 1
else:
new_persona = current_persona + "\n\n" + persona_patch
(skill_dir / "persona.md").write_text(new_persona, encoding="utf-8")
# 重新生成 SKILL.md
work_content = (skill_dir / "work.md").read_text(encoding="utf-8")
persona_content = (skill_dir / "persona.md").read_text(encoding="utf-8")
name = meta.get("name", skill_dir.name)
identity = build_identity_string(meta)
skill_md = SKILL_MD_TEMPLATE.format(
slug=skill_dir.name,
name=name,
identity=identity,
work_content=work_content,
persona_content=persona_content,
)
(skill_dir / "SKILL.md").write_text(skill_md, encoding="utf-8")
# 更新 meta
meta["version"] = new_version
meta["updated_at"] = datetime.now(timezone.utc).isoformat()
meta_path.write_text(json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8")
return new_version
def list_colleagues(base_dir: Path) -> list:
"""列出所有已创建的同事 Skill"""
colleagues = []
if not base_dir.exists():
return colleagues
for skill_dir in sorted(base_dir.iterdir()):
if not skill_dir.is_dir():
continue
meta_path = skill_dir / "meta.json"
if not meta_path.exists():
continue
try:
meta = json.loads(meta_path.read_text(encoding="utf-8"))
except Exception:
continue
colleagues.append({
"slug": meta.get("slug", skill_dir.name),
"name": meta.get("name", skill_dir.name),
"identity": build_identity_string(meta),
"version": meta.get("version", "v1"),
"updated_at": meta.get("updated_at", ""),
"corrections_count": meta.get("corrections_count", 0),
})
return colleagues
def main() -> None:
parser = argparse.ArgumentParser(description="Skill 文件写入器")
parser.add_argument("--action", required=True, choices=["create", "update", "list"])
parser.add_argument("--slug", help="同事 slug用于目录名")
parser.add_argument("--name", help="同事姓名")
parser.add_argument("--meta", help="meta.json 文件路径")
parser.add_argument("--work", help="work.md 内容文件路径")
parser.add_argument("--persona", help="persona.md 内容文件路径")
parser.add_argument("--work-patch", help="work.md 增量更新内容文件路径")
parser.add_argument("--persona-patch", help="persona.md 增量更新内容文件路径")
parser.add_argument(
"--base-dir",
default="./colleagues",
help="同事 Skill 根目录(默认:./colleagues",
)
args = parser.parse_args()
base_dir = Path(args.base_dir).expanduser()
if args.action == "list":
colleagues = list_colleagues(base_dir)
if not colleagues:
print("暂无已创建的同事 Skill")
else:
print(f"已创建 {len(colleagues)} 个同事 Skill\n")
for c in colleagues:
updated = c["updated_at"][:10] if c["updated_at"] else "未知"
print(f" [{c['slug']}] {c['name']}{c['identity']}")
print(f" 版本: {c['version']} 纠正次数: {c['corrections_count']} 更新: {updated}")
print()
elif args.action == "create":
if not args.slug and not args.name:
print("错误create 操作需要 --slug 或 --name", file=sys.stderr)
sys.exit(1)
meta: dict = {}
if args.meta:
meta = json.loads(Path(args.meta).read_text(encoding="utf-8"))
if args.name:
meta["name"] = args.name
slug = args.slug or slugify(meta.get("name", "colleague"))
work_content = ""
if args.work:
work_content = Path(args.work).read_text(encoding="utf-8")
persona_content = ""
if args.persona:
persona_content = Path(args.persona).read_text(encoding="utf-8")
skill_dir = create_skill(base_dir, slug, meta, work_content, persona_content)
print(f"✅ Skill 已创建:{skill_dir}")
print(f" 触发词:/{slug}")
elif args.action == "update":
if not args.slug:
print("错误update 操作需要 --slug", file=sys.stderr)
sys.exit(1)
skill_dir = base_dir / args.slug
if not skill_dir.exists():
print(f"错误:找不到 Skill 目录 {skill_dir}", file=sys.stderr)
sys.exit(1)
work_patch = Path(args.work_patch).read_text(encoding="utf-8") if args.work_patch else None
persona_patch = Path(args.persona_patch).read_text(encoding="utf-8") if args.persona_patch else None
new_version = update_skill(skill_dir, work_patch, persona_patch)
print(f"✅ Skill 已更新到 {new_version}{skill_dir}")
if __name__ == "__main__":
main()