"""SkillInstallTool - Agent 可调用的技能安装工具""" import asyncio import logging import os from typing import Any, Callable, Awaitable from agentkit.tools.base import Tool logger = logging.getLogger(__name__) class SkillInstallTool(Tool): """技能安装工具 让 Agent 可以通过正确的命令安装技能,而不是用 shell 执行 npm install。 使用 `npx skills install ` 安装技能,然后注册到 skill_registry。 Usage: tool = SkillInstallTool() result = await tool.execute(name="find-skills", source="vercel-labs/skills@find-skills") """ def __init__( self, name: str = "skill_install", description: str = ( "安装 Agent 技能包。使用 npx skills install 安装指定技能,不要用 npm install。" "重要:安装前应先用 skill_search 工具搜索确认技能名称和来源(source)。" "如果用户只提供了模糊名称,先用 skill_search 搜索,再根据搜索结果安装。" ), input_schema: dict[str, Any] | None = None, output_schema: dict[str, Any] | None = None, version: str = "1.0.0", tags: list[str] | None = None, confirm_callback: Callable[[str], Awaitable[bool]] | None = None, skill_registry=None, tool_registry=None, ): schema = input_schema or { "type": "object", "properties": { "name": { "type": "string", "description": "技能名称,如 find-skills", }, "source": { "type": "string", "description": "技能来源,格式为 owner/repo@skill,如 vercel-labs/skills@find-skills。如果不提供则用 name 搜索", }, }, "required": ["name"], } super().__init__( name=name, description=description, input_schema=schema, output_schema=output_schema, version=version, tags=tags, ) self._confirm_callback = confirm_callback self._skill_registry = skill_registry self._tool_registry = tool_registry async def execute(self, **kwargs) -> dict: name = kwargs.get("name", "").strip() source = kwargs.get("source", "").strip() if not name: return { "output": "错误: 必须提供 name 参数", "exit_code": 1, "is_error": True, } # Build the install command if source: install_target = source else: install_target = name # Request confirmation before installing if self._confirm_callback: confirmed = await self._confirm_callback(f"npx skills install {install_target}") if not confirmed: return { "output": f"技能安装被用户拒绝: {install_target}", "exit_code": 126, "is_error": True, } try: proc = await asyncio.create_subprocess_exec( "npx", "skills@latest", "install", install_target, "-y", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=120) output = stdout.decode("utf-8", errors="replace") error_output = stderr.decode("utf-8", errors="replace") if proc.returncode == 0: # Try to register the skill into skill_registry registration_msg = "" if self._skill_registry: registration_msg = self._try_register_skill(name) return { "output": f"技能 {name} 安装成功\n{output}\n{registration_msg}", "exit_code": 0, "is_error": False, } else: return { "output": f"技能 {name} 安装失败 (exit={proc.returncode})\n{error_output}\n{output}", "exit_code": proc.returncode, "is_error": True, "suggestions": [ "检查技能名称是否正确", "使用 source 参数指定完整路径,如 vercel-labs/skills@find-skills", "运行 npx skills search 搜索可用技能", ], } except asyncio.TimeoutError: return { "output": f"技能 {name} 安装超时(120s)", "exit_code": -1, "is_error": True, } except Exception as e: return { "output": f"技能 {name} 安装异常: {e}", "exit_code": -1, "is_error": True, } def _try_register_skill(self, name: str) -> str: """Try to find and register the installed skill YAML into skill_registry.""" try: from agentkit.skills.loader import SkillLoader for search_dir in [os.path.join(os.getcwd(), ".agents", "skills"), os.path.join(os.path.expanduser("~"), ".agents", "skills"), os.path.join(os.getcwd(), "configs", "skills")]: yaml_path = os.path.join(search_dir, f"{name}.yaml") if os.path.exists(yaml_path): loader = SkillLoader( skill_registry=self._skill_registry, tool_registry=self._tool_registry, ) loader.load_from_file(yaml_path) return f"技能已注册到系统(来源: {yaml_path})" # Also check for directory-based skills for search_dir in [os.path.join(os.getcwd(), ".agents", "skills"), os.path.join(os.path.expanduser("~"), ".agents", "skills")]: skill_dir = os.path.join(search_dir, name) if os.path.isdir(skill_dir): for fname in os.listdir(skill_dir): if fname.endswith((".yaml", ".yml")): yaml_path = os.path.join(skill_dir, fname) loader = SkillLoader( skill_registry=self._skill_registry, tool_registry=self._tool_registry, ) loader.load_from_file(yaml_path) return f"技能已注册到系统(来源: {yaml_path})" return "技能文件已下载,但未找到 YAML 配置文件进行注册。可能需要重启服务。" except Exception as e: logger.warning(f"Failed to register skill {name}: {e}") return f"技能文件已下载,但注册失败: {e}"