33"""
44
55import logging
6+ import re
67import shutil
78import subprocess
89import sys
@@ -66,7 +67,27 @@ def _parse_skill_markdown(cls, file_path: Path) -> dict:
6667 if len (parts ) >= 3 :
6768 front_matter = parts [1 ].strip ()
6869 # 用 yaml.safe_load 正确解析多行 description (">" 折叠语法)
69- meta = yaml .safe_load (front_matter )
70+ try :
71+ meta = yaml .safe_load (front_matter )
72+ except yaml .YAMLError :
73+ # YAML 解析失败时(如 description 包含未转义的冒号),
74+ # 使用正则表达式提取 name 和 description
75+ meta = None
76+ name_match = re .search (
77+ r"^name:\s*(.+?)\s*$" , front_matter , re .MULTILINE
78+ )
79+ desc_match = re .search (
80+ r"^description:\s*(.+?)\s*$" , front_matter , re .MULTILINE
81+ )
82+ if name_match or desc_match :
83+ meta = {
84+ "name" : (
85+ name_match .group (1 ).strip () if name_match else None
86+ ),
87+ "description" : (
88+ desc_match .group (1 ).strip () if desc_match else None
89+ ),
90+ }
7091 if isinstance (meta , dict ):
7192 return {
7293 "name" : meta .get ("name" ) or file_path .parent .name ,
@@ -78,7 +99,9 @@ def _parse_skill_markdown(cls, file_path: Path) -> dict:
7899 return {"name" : file_path .parent .name , "description" : "" }
79100
80101 @classmethod
81- async def install_from_github (cls , repo : str , skill_names : list [str ] | None = None , scope : str = "common" ) -> list [dict ]:
102+ async def install_from_github (
103+ cls , repo : str , skill_names : list [str ] | None = None , scope : str = "common"
104+ ) -> list [dict ]:
82105 """从 GitHub 仓库安装技能"""
83106 if scope not in ("common" , "deep" ):
84107 scope = "common"
@@ -89,7 +112,9 @@ async def install_from_github(cls, repo: str, skill_names: list[str] | None = No
89112
90113 for branch in branches :
91114 try :
92- downloaded_skills , temp_dir = await cls ._download_github_skills (repo , branch , skill_names )
115+ downloaded_skills , temp_dir = await cls ._download_github_skills (
116+ repo , branch , skill_names
117+ )
93118 break
94119 except Exception as e :
95120 logger .warning (f"从 { repo } ({ branch } 分支) 下载技能失败: { e } " )
@@ -115,7 +140,9 @@ async def install_from_github(cls, repo: str, skill_names: list[str] | None = No
115140 return installed_skills
116141
117142 @classmethod
118- async def _download_github_skills (cls , repo : str , branch : str , skill_names : list [str ] | None = None ) -> tuple [list [dict ], Path ]:
143+ async def _download_github_skills (
144+ cls , repo : str , branch : str , skill_names : list [str ] | None = None
145+ ) -> tuple [list [dict ], Path ]:
119146 """从 GitHub 下载技能,返回 (skills列表, 临时目录)"""
120147 # 清理 repo 格式
121148 repo = repo .strip ("/" )
@@ -130,12 +157,16 @@ async def _download_github_skills(cls, repo: str, branch: str, skill_names: list
130157 async with httpx .AsyncClient (follow_redirects = True , timeout = 60 ) as client :
131158 response = await client .get (zip_url )
132159 response .raise_for_status ()
133- skills , temp_dir = cls ._extract_skills_from_zip (response .content , skill_names )
160+ skills , temp_dir = cls ._extract_skills_from_zip (
161+ response .content , skill_names
162+ )
134163
135164 return skills , temp_dir
136165
137166 @classmethod
138- def _extract_skills_from_zip (cls , zip_bytes : bytes , skill_names : list [str ] | None = None ) -> tuple [list [dict ], Path ]:
167+ def _extract_skills_from_zip (
168+ cls , zip_bytes : bytes , skill_names : list [str ] | None = None
169+ ) -> tuple [list [dict ], Path ]:
139170 """从 zip 内容中提取技能信息,返回 (skills列表, 临时目录路径)"""
140171 temp_dir = Path (tempfile .mkdtemp ())
141172
@@ -164,7 +195,7 @@ def _extract_skills_from_zip(cls, zip_bytes: bytes, skill_names: list[str] | Non
164195 stripped_name = actual_dir_name
165196 for suffix in ["-main" , "-master" ]:
166197 if stripped_name .endswith (suffix ):
167- stripped_name = stripped_name [:- len (suffix )]
198+ stripped_name = stripped_name [: - len (suffix )]
168199 if stripped_name != expected_dir_name :
169200 logger .warning (
170201 f"技能目录名 '{ actual_dir_name } ' (stripped: '{ stripped_name } ') "
@@ -199,7 +230,11 @@ def _setup_skill_venv(cls, skill_dir: Path) -> bool:
199230 )
200231
201232 # 确定 pip 的路径(跨平台兼容)
202- pip_path = venv_dir / "bin" / "pip" if venv_dir .joinpath ("bin" ).exists () else venv_dir / "Scripts" / "pip.exe"
233+ pip_path = (
234+ venv_dir / "bin" / "pip"
235+ if venv_dir .joinpath ("bin" ).exists ()
236+ else venv_dir / "Scripts" / "pip.exe"
237+ )
203238
204239 # 安装依赖
205240 subprocess .run (
@@ -247,7 +282,9 @@ def _install_skill(cls, skill_info: dict, scope: str = "common") -> bool:
247282 return True
248283
249284 @classmethod
250- def install_from_zip (cls , zip_bytes : bytes , filename : str , scope : str = "common" ) -> list [dict ]:
285+ def install_from_zip (
286+ cls , zip_bytes : bytes , filename : str , scope : str = "common"
287+ ) -> list [dict ]:
251288 """从 zip 安装"""
252289 if scope not in ("common" , "deep" ):
253290 scope = "common"
@@ -273,7 +310,7 @@ def install_from_zip(cls, zip_bytes: bytes, filename: str, scope: str = "common"
273310 stripped_name = actual_dir_name
274311 for suffix in ["-main" , "-master" ]:
275312 if stripped_name .endswith (suffix ):
276- stripped_name = stripped_name [:- len (suffix )]
313+ stripped_name = stripped_name [: - len (suffix )]
277314
278315 # 检查是否只有一个 skill,且目录名不匹配(temp 目录情况)
279316 all_skill_mds = list (temp_dir .rglob ("**/SKILL.md" ))
@@ -282,7 +319,9 @@ def install_from_zip(cls, zip_bytes: bytes, filename: str, scope: str = "common"
282319
283320 if is_single_skill and name_mismatch :
284321 # 单 skill 的 zip,顶层是随机 temp 目录,直接使用
285- logger .info (f"检测到单技能 zip(temp 目录模式),技能名: { skill_info ['name' ]} " )
322+ logger .info (
323+ f"检测到单技能 zip(temp 目录模式),技能名: { skill_info ['name' ]} "
324+ )
286325 elif stripped_name != skill_info ["name" ]:
287326 logger .warning (
288327 f"技能目录名 '{ actual_dir_name } ' 与 SKILL.md 中的 name '{ skill_info ['name' ]} ' 不一致,跳过"
@@ -330,7 +369,9 @@ def uninstall_skill(cls, skill_name: str, scope: str = "common") -> bool:
330369 return False
331370
332371 @classmethod
333- def toggle_skill (cls , skill_name : str , enabled : bool , scope : str = "common" ) -> bool :
372+ def toggle_skill (
373+ cls , skill_name : str , enabled : bool , scope : str = "common"
374+ ) -> bool :
334375 """启用/禁用技能"""
335376 skills_dir = cls .get_skills_dir (scope )
336377 skill_dir = skills_dir / skill_name
@@ -356,7 +397,9 @@ def toggle_skill(cls, skill_name: str, enabled: bool, scope: str = "common") ->
356397 return False
357398
358399 @classmethod
359- def get_enabled_skill_paths (cls , selected_skills : list [str ] | None = None , scope : str = "common" ) -> list [str ]:
400+ def get_enabled_skill_paths (
401+ cls , selected_skills : list [str ] | None = None , scope : str = "common"
402+ ) -> list [str ]:
360403 """获取生效的技能路径"""
361404 if selected_skills is not None :
362405 # 如果指定了技能列表,直接返回对应路径(忽略 .disabled)
0 commit comments