Coverage for skills/registry.py: 26%

1"""

2 Skills 注册表

3

4管理所有可用的技能，支持：

5- 自动发现和加载

6- 轻量级描述注入（System Prompt）

7- 完整信息按需加载

8- 完全兼容 OpenClaw 的 SKILL.md 格式

9"""

10

11 import yaml

12 from pathlib import Path

13 from typing import Dict, List, Optional

14 from qrclaw.logger import get_logger

15

16 logger = get_logger("qrclaw.skills.registry")

17

18

19 class Skill:

20 """技能定义（OpenClaw SKILL.md 格式）"""

21

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

30

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")

36

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

48

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 )

57

58 logger.info(f"加载技能: {skill.name} - {skill.description}")

59 return skill

60

61 except Exception as e:

62 logger.error(f"加载技能失败: {skill_md_path}, 错误: {e}", exc_info=True)

63 raise

64

65 def get_lightweight_info(self) -> str:

66 """获取轻量级信息（用于 System Prompt）"""

67 return f"{self.name} - {self.description}"

68

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)

81

82

83 class SkillRegistry:

84 """技能注册表"""

85

86 def __init__(self):

87 self.skills: Dict[str, Skill] = {}

88

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

95

96 # 扫描所有技能目录

97 for skill_dir in skills_dir.iterdir():

98 if not skill_dir.is_dir():

99 continue

100

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"

105

106 if not skill_md.exists():

107 logger.debug(f"跳过目录（缺少 SKILL.md）：{skill_dir.name}")

108 continue

109

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)

116

117 logger.info(f"加载了 {len(self.skills)} 个技能")

118

119 def get_skills_list(self) -> List[str]:

120 """获取技能列表（轻量级描述）"""

121 return [skill.get_lightweight_info() for skill in self.skills.values()]

122

123 def get_skill(self, name: str) -> Optional[Skill]:

124 """获取技能完整信息"""

125 return self.skills.get(name)

126

127 def has_skill(self, name: str) -> bool:

128 """检查技能是否存在"""

129 return name in self.skills

130

131 def get_all_skills(self) -> Dict[str, Skill]:

132 """获取所有技能"""

133 return self.skills