Source code for skill_creator_mcp.utils.validators

"""验证器工具函数."""

import re
from pathlib import Path

from skill_creator_mcp.logging_config import get_logger

logger = get_logger(__name__)

# 模板特定必需文件映射
TEMPLATE_REQUIREMENTS = {
    "minimal": [],
    "tool-based": ["tool-integration.md", "usage-examples.md"],
    "workflow-based": ["workflow-steps.md", "decision-points.md"],
    "analyzer-based": ["analysis-methods.md", "metrics.md"],
}

# 有效工具名称列表
VALID_TOOLS = [
    "Read",
    "Write",
    "Edit",
    "Bash",
    "Glob",
    "Grep",
    "AskUserQuestion",
    "TodoWrite",
    "Skill",
]


[docs] def validate_skill_name(name: str) -> None: """验证技能名称符合规范. 规范: - 只能包含小写字母、数字、连字符 - 不能以连字符开头或结尾 - 不能有连续的连字符 Args: name: 技能名称 Raises: ValueError: 名称不符合规范时抛出 """ pattern = r"^[a-z0-9]+(?:-[a-z0-9]+)*$" if not re.match(pattern, name): raise ValueError( f"技能名称 '{name}' 不符合规范。" "要求:小写字母、数字、单个连字符,不能以连字符开头或结尾,不能有连续连字符" )
[docs] def validate_skill_directory(skill_dir: Path) -> None: """验证技能目录是否存在且结构正确. Args: skill_dir: 技能目录路径 Raises: ValueError: 目录结构不正确时抛出 """ if not skill_dir.exists(): raise ValueError(f"技能目录不存在: {skill_dir}") skill_md = skill_dir / "SKILL.md" if not skill_md.exists(): raise ValueError(f"SKILL.md 不存在于: {skill_dir}")
[docs] def validate_template_type(template: str) -> str: """验证模板类型是否有效. Args: template: 模板类型 Returns: 验证通过的模板类型 Raises: ValueError: 模板类型无效时抛出 """ valid_templates = ["minimal", "tool-based", "workflow-based", "analyzer-based"] if template not in valid_templates: raise ValueError(f"无效的模板类型: {template}。有效值: {', '.join(valid_templates)}") return template
# ==================== validate_skill 专用验证函数 ==================== def _validate_structure(skill_dir: Path) -> list[str]: """验证技能目录结构. Args: skill_dir: 技能目录路径 Returns: 错误列表,空列表表示无错误 """ logger.debug("Validating structure for: %s", skill_dir) errors = [] # 检查必需文件 required_files = ["SKILL.md"] for file_name in required_files: if not (skill_dir / file_name).exists(): error_msg = f"缺少必需文件: {file_name}" logger.warning(error_msg) errors.append(error_msg) # 检查必需目录 required_dirs = ["references", "examples", "scripts", ".claude"] for dir_name in required_dirs: if not (skill_dir / dir_name).exists(): error_msg = f"缺少必需目录: {dir_name}" logger.warning(error_msg) errors.append(error_msg) if not errors: logger.info("Structure validation passed for: %s", skill_dir) return errors def _validate_naming(skill_dir: Path) -> list[str]: """验证技能命名规范. Args: skill_dir: 技能目录路径 Returns: 错误列表,空列表表示无错误 """ errors = [] skill_name = skill_dir.name # 验证目录名格式 try: validate_skill_name(skill_name) except ValueError as e: errors.append(str(e)) # 验证 SKILL.md 中的 name 字段与目录名一致 skill_md = skill_dir / "SKILL.md" if skill_md.exists(): content = skill_md.read_text(encoding="utf-8") # 提取 YAML frontmatter 中的 name 字段 for line in content.split("\n"): if line.startswith("name:"): yaml_name = line.split(":", 1)[1].strip().strip("\"'") if yaml_name != skill_name: errors.append( f"SKILL.md 中的 name 字段 '{yaml_name}' 与目录名 '{skill_name}' 不一致" ) break return errors def _validate_skill_md(skill_dir: Path) -> tuple[list[str], list[str], str | None]: """验证 SKILL.md 内容格式. Args: skill_dir: 技能目录路径 Returns: (错误列表, 警告列表, 模板类型) """ errors: list[str] = [] warnings: list[str] = [] template_type = None skill_md = skill_dir / "SKILL.md" if not skill_md.exists(): errors.append("SKILL.md 文件不存在") return errors, warnings, template_type content = skill_md.read_text(encoding="utf-8") # 检查 YAML frontmatter if not content.startswith("---"): errors.append("SKILL.md 缺少 YAML frontmatter(应以 --- 开头)") return errors, warnings, template_type # 解析 frontmatter 内容 frontmatter_lines = [] for line in content.split("\n")[1:]: if line.strip() == "---": break frontmatter_lines.append(line) frontmatter = "\n".join(frontmatter_lines) # 检查必需字段 required_fields = { "name": "name:", "description": "description:", "allowed-tools": "allowed-tools:", } for field_name, field_marker in required_fields.items(): if field_marker not in frontmatter: errors.append(f"SKILL.md 缺少必需字段: {field_name}") # 提取模板类型(如果有) for line in frontmatter_lines: if line.strip().startswith("template:"): template_value = line.split(":", 1)[1].strip().strip("\"'") if template_value in TEMPLATE_REQUIREMENTS: template_type = template_value break # 验证 allowed-tools if "allowed-tools:" in frontmatter: # 提取 allowed-tools 的值 in_allowed_tools = False tools_value = "" for line in frontmatter_lines: if line.strip().startswith("allowed-tools:"): in_allowed_tools = True if ":" in line: tools_value = line.split(":", 1)[1].strip() continue if in_allowed_tools: if line.startswith(" ") or line.startswith("\t"): tools_value += " " + line.strip() else: break # 解析工具列表 if tools_value: # 处理方括号格式 [Read, Write, Edit] tools_value = tools_value.strip("[]") tools_list = [t.strip().strip(",'\"") for t in tools_value.split(",") if t.strip()] invalid_tools = [t for t in tools_list if t and t not in VALID_TOOLS] if invalid_tools: warnings.append(f"allowed-tools 包含可能无效的工具: {', '.join(invalid_tools)}") return errors, warnings, template_type def _validate_template_requirements(skill_dir: Path, template_type: str | None) -> list[str]: """验证模板特定要求. Args: skill_dir: 技能目录路径 template_type: 模板类型 Returns: 错误列表,空列表表示无错误 """ errors: list[str] = [] if not template_type or template_type not in TEMPLATE_REQUIREMENTS: return errors required_files = TEMPLATE_REQUIREMENTS[template_type] refs_dir = skill_dir / "references" for file_name in required_files: ref_file = refs_dir / file_name if not ref_file.exists(): errors.append(f"模板 '{template_type}' 缺少必需引用文件: references/{file_name}") return errors