Coverage for skills / registry.py: 26%
68 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-29 02:55 +0800
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-29 02:55 +0800
1"""
2Skills 注册表
4管理所有可用的技能,支持:
5- 自动发现和加载
6- 轻量级描述注入(System Prompt)
7- 完整信息按需加载
8- 完全兼容 OpenClaw 的 SKILL.md 格式
9"""
11import yaml
12from pathlib import Path
13from typing import Dict, List, Optional
14from qrclaw.logger import get_logger
16logger = get_logger("qrclaw.skills.registry")
19class Skill:
20 """技能定义(OpenClaw SKILL.md 格式)"""
22 def __init__(self, name: str, description: str, version: str = "1.0.0",
23 metadata: Dict = None, content: str = "", path: Path = None):
24 self.name = name
25 self.description = description
26 self.version = version
27 self.metadata = metadata or {}
28 self.content = content
29 self.path = path
31 @classmethod
32 def from_skill_md(cls, skill_md_path: Path) -> "Skill":
33 """从 SKILL.md 文件加载技能"""
34 try:
35 content = skill_md_path.read_text(encoding="utf-8")
37 # 解析 YAML frontmatter
38 metadata = {}
39 if content.startswith("---"):
40 parts = content.split("---", 2)
41 if len(parts) >= 3:
42 metadata = yaml.safe_load(parts[1]) or {}
43 markdown_content = parts[2].strip()
44 else:
45 markdown_content = content
46 else:
47 markdown_content = content
49 skill = cls(
50 name=metadata.get("name", skill_md_path.parent.name),
51 description=metadata.get("description", ""),
52 version=metadata.get("version", "1.0.0"),
53 metadata=metadata,
54 content=markdown_content,
55 path=skill_md_path.parent
56 )
58 logger.info(f"加载技能: {skill.name} - {skill.description}")
59 return skill
61 except Exception as e:
62 logger.error(f"加载技能失败: {skill_md_path}, 错误: {e}", exc_info=True)
63 raise
65 def get_lightweight_info(self) -> str:
66 """获取轻量级信息(用于 System Prompt)"""
67 return f"{self.name} - {self.description}"
69 def get_full_info(self) -> str:
70 """获取完整信息(按需加载)"""
71 lines = [
72 f"## 技能:{self.name}",
73 f"**描述**:{self.description}",
74 f"**版本**:{self.version}",
75 "",
76 "---",
77 "",
78 self.content
79 ]
80 return "\n".join(lines)
83class SkillRegistry:
84 """技能注册表"""
86 def __init__(self):
87 self.skills: Dict[str, Skill] = {}
89 def load_from_dir(self, skills_dir: Path):
90 """从目录加载所有技能(skills_dir 由 Workspace 提供)"""
91 if not skills_dir.exists():
92 logger.info(f"Skills 目录不存在,创建:{skills_dir}")
93 skills_dir.mkdir(parents=True, exist_ok=True)
94 return
96 # 扫描所有技能目录
97 for skill_dir in skills_dir.iterdir():
98 if not skill_dir.is_dir():
99 continue
101 # 查找 SKILL.md 或 skill.md
102 skill_md = skill_dir / "SKILL.md"
103 if not skill_md.exists():
104 skill_md = skill_dir / "skill.md"
106 if not skill_md.exists():
107 logger.debug(f"跳过目录(缺少 SKILL.md):{skill_dir.name}")
108 continue
110 try:
111 skill = Skill.from_skill_md(skill_md)
112 self.skills[skill.name] = skill
113 logger.debug(f"注册技能:{skill.name}")
114 except Exception as e:
115 logger.error(f"加载技能失败:{skill_dir}, 错误:{e}", exc_info=True)
117 logger.info(f"加载了 {len(self.skills)} 个技能")
119 def get_skills_list(self) -> List[str]:
120 """获取技能列表(轻量级描述)"""
121 return [skill.get_lightweight_info() for skill in self.skills.values()]
123 def get_skill(self, name: str) -> Optional[Skill]:
124 """获取技能完整信息"""
125 return self.skills.get(name)
127 def has_skill(self, name: str) -> bool:
128 """检查技能是否存在"""
129 return name in self.skills
131 def get_all_skills(self) -> Dict[str, Skill]:
132 """获取所有技能"""
133 return self.skills