Phase 6.1: ToolRegistry infrastructure - Add ToolManifest with ToolCategory, PermissionClass, SideEffectScope - Add ToolRegistry singleton with register/get/unregister/list/search - Add BaseTool abstract class with ReadTool/WriteTool/DBWriteTool/ExternalTool/NetworkTool subclasses - Add migration layer for backward compatibility Phase 6.2: Hook interception system - Add HookType (PRE_TOOL_USE, POST_TOOL_USE, TOOL_ERROR, TOOL_SKIP) - Add HookManager with singleton for hook registration - Add HookExecutor for pre/post/error hook execution Phase 6.3: Streaming execution - Add StreamingToolExecutor with batch execution support Phase 6.4: New builtin tools - Add file_tools: GlobTool, GrepTool, ReadFileTool, WriteFileTool - Add system_tools: BashTool, PowerShellTool - Add dev_tools: LSPTools, GitTool - Add collaboration_tools: TeamAgentTool, TaskBroadcastTool Tests: 29 passed
194 lines
5.8 KiB
Python
194 lines
5.8 KiB
Python
"""系统工具 - Phase 6.4"""
|
|
|
|
import asyncio
|
|
import shlex
|
|
from typing import Any
|
|
|
|
from app.agents.tools.base import ExternalTool
|
|
from app.agents.tools.manifest import (
|
|
PermissionClass,
|
|
SideEffectScope,
|
|
)
|
|
|
|
|
|
class BashTool(ExternalTool):
|
|
"""Bash 命令执行工具"""
|
|
|
|
def __init__(self, working_dir: str = "."):
|
|
super().__init__(
|
|
name="bash",
|
|
description="执行 Bash 命令",
|
|
permission_class=PermissionClass.EXTERNAL,
|
|
side_effect_scope=SideEffectScope.LOCAL_STATE,
|
|
requires_confirmation=True,
|
|
tags=["system", "bash", "shell"],
|
|
)
|
|
self.working_dir = working_dir
|
|
|
|
def get_parameters(self) -> dict[str, Any]:
|
|
return {
|
|
"type": "object",
|
|
"properties": {
|
|
"command": {
|
|
"type": "string",
|
|
"description": "要执行的 Bash 命令",
|
|
},
|
|
"timeout": {
|
|
"type": "integer",
|
|
"description": "超时时间(秒)",
|
|
},
|
|
"working_dir": {
|
|
"type": "string",
|
|
"description": "工作目录(可选)",
|
|
},
|
|
},
|
|
"required": ["command"],
|
|
}
|
|
|
|
def get_return_schema(self) -> dict[str, Any]:
|
|
return {
|
|
"type": "object",
|
|
"properties": {
|
|
"stdout": {"type": "string"},
|
|
"stderr": {"type": "string"},
|
|
"returncode": {"type": "integer"},
|
|
},
|
|
}
|
|
|
|
async def execute(
|
|
self, command: str, timeout: int = 30, working_dir: str | None = None
|
|
) -> dict[str, Any]:
|
|
import os
|
|
|
|
cwd = working_dir or self.working_dir
|
|
|
|
try:
|
|
process = await asyncio.create_subprocess_shell(
|
|
command,
|
|
stdout=asyncio.subprocess.PIPE,
|
|
stderr=asyncio.subprocess.PIPE,
|
|
cwd=cwd,
|
|
)
|
|
|
|
try:
|
|
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=timeout)
|
|
except asyncio.TimeoutError:
|
|
process.kill()
|
|
await process.wait()
|
|
return {
|
|
"stdout": "",
|
|
"stderr": f"Command timed out after {timeout} seconds",
|
|
"returncode": -1,
|
|
}
|
|
|
|
return {
|
|
"stdout": stdout.decode("utf-8", errors="replace"),
|
|
"stderr": stderr.decode("utf-8", errors="replace"),
|
|
"returncode": process.returncode,
|
|
}
|
|
|
|
except Exception as e:
|
|
return {
|
|
"stdout": "",
|
|
"stderr": str(e),
|
|
"returncode": -1,
|
|
}
|
|
|
|
|
|
class PowerShellTool(ExternalTool):
|
|
"""PowerShell 命令执行工具"""
|
|
|
|
def __init__(self, working_dir: str = "."):
|
|
super().__init__(
|
|
name="powershell",
|
|
description="执行 PowerShell 命令",
|
|
permission_class=PermissionClass.EXTERNAL,
|
|
side_effect_scope=SideEffectScope.LOCAL_STATE,
|
|
requires_confirmation=True,
|
|
tags=["system", "powershell", "shell"],
|
|
)
|
|
self.working_dir = working_dir
|
|
|
|
def get_parameters(self) -> dict[str, Any]:
|
|
return {
|
|
"type": "object",
|
|
"properties": {
|
|
"command": {
|
|
"type": "string",
|
|
"description": "要执行的 PowerShell 命令",
|
|
},
|
|
"timeout": {
|
|
"type": "integer",
|
|
"description": "超时时间(秒)",
|
|
},
|
|
"working_dir": {
|
|
"type": "string",
|
|
"description": "工作目录(可选)",
|
|
},
|
|
},
|
|
"required": ["command"],
|
|
}
|
|
|
|
def get_return_schema(self) -> dict[str, Any]:
|
|
return {
|
|
"type": "object",
|
|
"properties": {
|
|
"stdout": {"type": "string"},
|
|
"stderr": {"type": "string"},
|
|
"returncode": {"type": "integer"},
|
|
},
|
|
}
|
|
|
|
async def execute(
|
|
self, command: str, timeout: int = 30, working_dir: str | None = None
|
|
) -> dict[str, Any]:
|
|
import platform
|
|
|
|
# 检测是否是 Windows 平台
|
|
is_windows = platform.system() == "Windows"
|
|
|
|
if not is_windows:
|
|
# 非 Windows 平台,可能没有 PowerShell
|
|
return {
|
|
"stdout": "",
|
|
"stderr": "PowerShell is not available on this platform",
|
|
"returncode": -1,
|
|
}
|
|
|
|
cwd = working_dir or self.working_dir
|
|
|
|
try:
|
|
process = await asyncio.create_subprocess_exec(
|
|
"powershell.exe",
|
|
"-NoProfile",
|
|
"-Command",
|
|
command,
|
|
stdout=asyncio.subprocess.PIPE,
|
|
stderr=asyncio.subprocess.PIPE,
|
|
cwd=cwd,
|
|
)
|
|
|
|
try:
|
|
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=timeout)
|
|
except asyncio.TimeoutError:
|
|
process.kill()
|
|
await process.wait()
|
|
return {
|
|
"stdout": "",
|
|
"stderr": f"Command timed out after {timeout} seconds",
|
|
"returncode": -1,
|
|
}
|
|
|
|
return {
|
|
"stdout": stdout.decode("utf-8", errors="replace"),
|
|
"stderr": stderr.decode("utf-8", errors="replace"),
|
|
"returncode": process.returncode,
|
|
}
|
|
|
|
except Exception as e:
|
|
return {
|
|
"stdout": "",
|
|
"stderr": str(e),
|
|
"returncode": -1,
|
|
}
|