Ctrl K
ComunidadGuías…

scout/automation/skill_manager.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Skill Manager — 管理内置与用户自定义技能的加载与隔离.

遵循 XDG Base Directory 规范,确保用户技能在代码更新时不受影响。
"""

from __future__ import annotations

import os
import shutil
import sys
from pathlib import Path
from typing import Any

import yaml


class SkillManager:
    """技能管理器 — 负责发现、加载和隔离 Skills."""

    def __init__(self):
        # 1. 内置技能路径 (随代码发布)
        self.builtin_dir = Path(__file__).parent.parent / "skills" / "builtin"
        
        # 2. 用户技能路径 (遵循 XDG 标准,永久保存)
        if sys.platform == "win32":
            base = Path(os.environ.get("APPDATA", "")) / "Scout"
        elif sys.platform == "darwin":
            base = Path.home() / "Library" / "Application Support" / "Scout"
        else:
            base = Path.home() / ".local" / "share" / "scout"
        
        self.user_dir = base / "skills"
        self.user_dir.mkdir(parents=True, exist_ok=True)

        self._skills_cache: dict[str, dict[str, Any]] = {}

    def discover_all(self) -> list[dict[str, Any]]:
        """发现所有可用的技能(内置 + 用户)。"""
        skills = []
        
        # 加载内置技能
        if self.builtin_dir.exists():
            skills.extend(self._scan_directory(self.builtin_dir, prefix="builtin"))
        
        # 加载用户技能 (优先级更高,可覆盖内置)
        if self.user_dir.exists():
            user_skills = self._scan_directory(self.user_dir)
            # 简单的去重逻辑:如果用户定义了同名技能,移除内置的
            user_names = {s["name"] for s in user_skills}
            skills = [s for s in skills if s["name"] not in user_names]
            skills.extend(user_skills)
            
        self._skills_cache = {s["name"]: s for s in skills}
        return skills

    def _scan_directory(self, path: Path, prefix: str = "") -> list[dict[str, Any]]:
        """扫描目录下的 skill.yaml 文件。"""
        skills = []
        for item in path.iterdir():
            if item.is_dir():
                yaml_file = item / "skill.yaml"
                if yaml_file.exists():
                    try:
                        with open(yaml_file, 'r', encoding='utf-8') as f:
                            meta = yaml.safe_load(f)
                        if meta:
                            meta["path"] = str(item)
                            meta["source"] = "user" if not prefix else "builtin"
                            meta["id"] = f"{prefix}.{meta['name']}" if prefix else meta["name"]
                            skills.append(meta)
                    except Exception as e:
                        print(f"[SkillManager] 加载失败 {yaml_file}: {e}")
        return skills

    def get_skill(self, name: str) -> dict[str, Any] | None:
        """根据名称获取技能元数据。"""
        return self._skills_cache.get(name)

    def migrate_old_skills(self, old_path: Path):
        """从旧版本的项目内目录迁移技能到标准用户目录。"""
        if old_path.exists() and old_path.is_dir():
            print(f"[SkillManager] 发现旧版技能目录: {old_path}")
            print(f"[SkillManager] 正在迁移至: {self.user_dir}")
            for item in old_path.iterdir():
                dest = self.user_dir / item.name
                if not dest.exists():
                    if item.is_dir():
                        shutil.copytree(item, dest)
                    else:
                        shutil.copy2(item, dest)
            print("[SkillManager] 迁移完成。")


# 全局单例
_manager = SkillManager()

def get_skill_manager() -> SkillManager:
    return _manager