diff --git a/astrbot/core/skills/skill_manager.py b/astrbot/core/skills/skill_manager.py index 58c119155..ec3ba8f03 100644 --- a/astrbot/core/skills/skill_manager.py +++ b/astrbot/core/skills/skill_manager.py @@ -6,6 +6,7 @@ import re import shlex import shutil import tempfile +import uuid import zipfile from dataclasses import dataclass from datetime import datetime, timezone @@ -58,6 +59,29 @@ def _is_ignored_zip_entry(name: str) -> bool: return parts[0] == "__MACOSX" +def _normalize_skill_markdown_path(skill_dir: Path) -> Path | None: + """Return the canonical `SKILL.md` path for a skill directory. + + If only legacy `skill.md` exists, it is renamed to `SKILL.md` in-place. + """ + canonical = skill_dir / "SKILL.md" + entries = set() + if skill_dir.exists(): + entries = {entry.name for entry in skill_dir.iterdir()} + if "SKILL.md" in entries: + return canonical + legacy = skill_dir / "skill.md" + if "skill.md" not in entries: + return None + try: + tmp = skill_dir / f".{uuid.uuid4().hex}.tmp_skill_md" + legacy.rename(tmp) + tmp.rename(canonical) + except OSError: + return legacy + return canonical + + @dataclass class SkillInfo: name: str @@ -363,8 +387,8 @@ class SkillManager: if not entry.is_dir(): continue skill_name = entry.name - skill_md = entry / "SKILL.md" - if not skill_md.exists(): + skill_md = _normalize_skill_markdown_path(entry) + if skill_md is None: continue active = skill_configs.get(skill_name, {}).get("active", True) if skill_name not in skill_configs: @@ -445,7 +469,7 @@ class SkillManager: def is_sandbox_only_skill(self, name: str) -> bool: skill_dir = Path(self.skills_root) / name - skill_md_exists = (skill_dir / "SKILL.md").exists() + skill_md_exists = _normalize_skill_markdown_path(skill_dir) is not None if skill_md_exists: return False cache = self._load_sandbox_skills_cache() @@ -559,6 +583,7 @@ class SkillManager: continue zf.extract(member, tmp_dir) src_dir = Path(tmp_dir) / skill_name + _normalize_skill_markdown_path(src_dir) if not src_dir.exists(): raise ValueError("Skill folder not found after extraction.") dest_dir = Path(self.skills_root) / skill_name