init the project
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
# Skill 管理服务:扫描、验证、列出所有可用 Skill
|
||||
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
from server.config import settings
|
||||
|
||||
logger = logging.getLogger("testflow")
|
||||
|
||||
REQUIRED_SKILL_FILES = ["skill.yaml", "ir_schema.json", "extract_ir_prompt.j2", "gen_cases_prompt.j2"]
|
||||
|
||||
|
||||
class SkillManager:
|
||||
"""Discover and validate skill packages from .zeekerwatchmen/skills/."""
|
||||
|
||||
def __init__(self):
|
||||
self._skills_dir = settings.SKILLS_DIR
|
||||
self._cache: dict[str, dict] = {}
|
||||
self._scan()
|
||||
|
||||
def _scan(self):
|
||||
"""Scan the skills directory and cache valid skills."""
|
||||
self._cache = {}
|
||||
if not self._skills_dir.exists():
|
||||
logger.warning("[SKILL] s directory not found: %s", self._skills_dir)
|
||||
return
|
||||
|
||||
for skill_dir in sorted(self._skills_dir.iterdir()):
|
||||
if not skill_dir.is_dir():
|
||||
continue
|
||||
|
||||
skill_name = skill_dir.name
|
||||
skill = self._load_skill(skill_name)
|
||||
if skill:
|
||||
self._cache[skill_name] = skill
|
||||
logger.info("[SKILL] 加载: %s (v%s)", skill_name, skill.get("version", "?"))
|
||||
|
||||
logger.info("[SKILL] 发现 %d skills: %s", len(self._cache), list(self._cache.keys()))
|
||||
|
||||
def _load_skill(self, skill_name: str) -> dict | None:
|
||||
"""Load and validate a single skill package."""
|
||||
skill_dir = self._skills_dir / skill_name
|
||||
|
||||
# Check required files
|
||||
missing = []
|
||||
for filename in REQUIRED_SKILL_FILES:
|
||||
if not (skill_dir / filename).exists():
|
||||
missing.append(filename)
|
||||
|
||||
if missing:
|
||||
logger.warning("[SKILL] '%s' missing files: %s", skill_name, missing)
|
||||
return None
|
||||
|
||||
# Load metadata
|
||||
try:
|
||||
with open(skill_dir / "skill.yaml", "r", encoding="utf-8") as f:
|
||||
meta = yaml.safe_load(f) or {}
|
||||
except yaml.YAMLError as e:
|
||||
logger.error("[SKILL] '%s': invalid skill.yaml: %s", skill_name, e)
|
||||
return None
|
||||
|
||||
# Load schema
|
||||
try:
|
||||
with open(skill_dir / "ir_schema.json", "r", encoding="utf-8") as f:
|
||||
schema = json.load(f)
|
||||
except (json.JSONDecodeError, IOError) as e:
|
||||
logger.error("[SKILL] '%s': invalid ir_schema.json: %s", skill_name, e)
|
||||
return None
|
||||
|
||||
return {
|
||||
"name": skill_name,
|
||||
"display_name": meta.get("display_name", skill_name),
|
||||
"description": meta.get("description", ""),
|
||||
"version": meta.get("version", "0.0.0"),
|
||||
"domain": meta.get("domain", "general"),
|
||||
"author": meta.get("author", ""),
|
||||
"auto_match": meta.get("auto_match", {}),
|
||||
"schema_fields": list(schema.get("properties", {}).get("features", {}).get("items", {}).get("properties", {}).keys()),
|
||||
}
|
||||
|
||||
def list_skills(self) -> list[dict]:
|
||||
"""Return metadata for all discovered skills."""
|
||||
return [
|
||||
{
|
||||
"name": s["name"],
|
||||
"display_name": s["display_name"],
|
||||
"description": s["description"],
|
||||
"version": s["version"],
|
||||
"domain": s["domain"],
|
||||
}
|
||||
for s in self._cache.values()
|
||||
]
|
||||
|
||||
def get_skill(self, name: str) -> dict | None:
|
||||
"""Get a specific skill's full metadata."""
|
||||
return self._cache.get(name)
|
||||
|
||||
def reload(self):
|
||||
"""Re-scan the skills directory (for hot-reload)."""
|
||||
logger.info("[SKILL] 热重载 skills...")
|
||||
self._cache = {}
|
||||
self._scan()
|
||||
|
||||
|
||||
skill_manager = SkillManager()
|
||||
Reference in New Issue
Block a user