Coverage for skills / registry.py: 26%

68 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-29 02:55 +0800

1""" 

2Skills 注册表 

3 

4管理所有可用的技能,支持: 

5- 自动发现和加载 

6- 轻量级描述注入(System Prompt) 

7- 完整信息按需加载 

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

9""" 

10 

11import yaml 

12from pathlib import Path 

13from typing import Dict, List, Optional 

14from qrclaw.logger import get_logger 

15 

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

17 

18 

19class 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 

83class 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