feat(agents): Phase 6 tool system refactoring
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
This commit is contained in:
43
backend/app/agents/tools/builtins/__init__.py
Normal file
43
backend/app/agents/tools/builtins/__init__.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""内置工具集 - Phase 6.4
|
||||
|
||||
新的内置工具,使用 BaseTool 基类。
|
||||
"""
|
||||
|
||||
from app.agents.tools.builtins.file_tools import (
|
||||
GlobTool,
|
||||
GrepTool,
|
||||
ReadFileTool,
|
||||
WriteFileTool,
|
||||
)
|
||||
|
||||
from app.agents.tools.builtins.system_tools import (
|
||||
BashTool,
|
||||
PowerShellTool,
|
||||
)
|
||||
|
||||
from app.agents.tools.builtins.dev_tools import (
|
||||
LSPTools,
|
||||
GitTool,
|
||||
)
|
||||
|
||||
from app.agents.tools.builtins.collaboration_tools import (
|
||||
TeamAgentTool,
|
||||
TaskBroadcastTool,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# File tools
|
||||
"GlobTool",
|
||||
"GrepTool",
|
||||
"ReadFileTool",
|
||||
"WriteFileTool",
|
||||
# System tools
|
||||
"BashTool",
|
||||
"PowerShellTool",
|
||||
# Dev tools
|
||||
"LSPTools",
|
||||
"GitTool",
|
||||
# Collaboration tools
|
||||
"TeamAgentTool",
|
||||
"TaskBroadcastTool",
|
||||
]
|
||||
129
backend/app/agents/tools/builtins/collaboration_tools.py
Normal file
129
backend/app/agents/tools/builtins/collaboration_tools.py
Normal file
@@ -0,0 +1,129 @@
|
||||
"""协作工具 - Phase 6.4"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from app.agents.tools.base import WriteTool
|
||||
from app.agents.tools.manifest import (
|
||||
PermissionClass,
|
||||
SideEffectScope,
|
||||
)
|
||||
|
||||
|
||||
class TeamAgentTool(WriteTool):
|
||||
"""团队 Agent 通信工具
|
||||
|
||||
用于与其他 Agent 进行消息传递和协作。
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
name="team_agent",
|
||||
description="向团队 Agent 发送消息或请求协作",
|
||||
permission_class=PermissionClass.WRITE,
|
||||
side_effect_scope=SideEffectScope.LOCAL_STATE,
|
||||
tags=["collaboration", "team", "agent"],
|
||||
)
|
||||
|
||||
def get_parameters(self) -> dict[str, Any]:
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"agent_name": {
|
||||
"type": "string",
|
||||
"description": "目标 Agent 名称",
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "要发送的消息",
|
||||
},
|
||||
"action": {
|
||||
"type": "string",
|
||||
"enum": ["send", "request", "delegate"],
|
||||
"description": "操作类型",
|
||||
},
|
||||
},
|
||||
"required": ["agent_name", "message"],
|
||||
}
|
||||
|
||||
def get_return_schema(self) -> dict[str, Any]:
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {"type": "boolean"},
|
||||
"response": {"type": "string"},
|
||||
},
|
||||
}
|
||||
|
||||
async def execute(self, agent_name: str, message: str, action: str = "send") -> dict[str, Any]:
|
||||
# 注意:实际实现需要通过 Agent 通信协议
|
||||
# 这里只是一个框架实现
|
||||
return {
|
||||
"success": True,
|
||||
"response": f"Message '{action}' to agent '{agent_name}': {message}",
|
||||
"agent_name": agent_name,
|
||||
"action": action,
|
||||
}
|
||||
|
||||
|
||||
class TaskBroadcastTool(WriteTool):
|
||||
"""任务广播工具
|
||||
|
||||
向多个 Agent 广播任务。
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
name="task_broadcast",
|
||||
description="向多个 Agent 广播任务",
|
||||
permission_class=PermissionClass.WRITE,
|
||||
side_effect_scope=SideEffectScope.LOCAL_STATE,
|
||||
tags=["collaboration", "broadcast", "task"],
|
||||
)
|
||||
|
||||
def get_parameters(self) -> dict[str, Any]:
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"agent_names": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "目标 Agent 列表",
|
||||
},
|
||||
"task": {
|
||||
"type": "string",
|
||||
"description": "要广播的任务描述",
|
||||
},
|
||||
"priority": {
|
||||
"type": "string",
|
||||
"enum": ["low", "normal", "high", "urgent"],
|
||||
"description": "任务优先级",
|
||||
},
|
||||
},
|
||||
"required": ["agent_names", "task"],
|
||||
}
|
||||
|
||||
def get_return_schema(self) -> dict[str, Any]:
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {"type": "boolean"},
|
||||
"broadcast_to": {"type": "array", "items": {"type": "string"}},
|
||||
"responses": {"type": "array"},
|
||||
},
|
||||
}
|
||||
|
||||
async def execute(
|
||||
self,
|
||||
agent_names: list[str],
|
||||
task: str,
|
||||
priority: str = "normal",
|
||||
) -> dict[str, Any]:
|
||||
# 注意:实际实现需要通过 Agent 通信协议
|
||||
# 这里只是一个框架实现
|
||||
return {
|
||||
"success": True,
|
||||
"broadcast_to": agent_names,
|
||||
"task": task,
|
||||
"priority": priority,
|
||||
"responses": [f"Acknowledged by {agent}" for agent in agent_names],
|
||||
}
|
||||
155
backend/app/agents/tools/builtins/dev_tools.py
Normal file
155
backend/app/agents/tools/builtins/dev_tools.py
Normal file
@@ -0,0 +1,155 @@
|
||||
"""开发工具 - Phase 6.4"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from app.agents.tools.base import ReadTool, WriteTool
|
||||
from app.agents.tools.manifest import (
|
||||
PermissionClass,
|
||||
SideEffectScope,
|
||||
)
|
||||
|
||||
|
||||
class LSPTools(ReadTool):
|
||||
"""语言服务器协议工具集
|
||||
|
||||
提供代码导航、查找引用等 LSP 功能。
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
name="lsp_tools",
|
||||
description="LSP 代码导航和查找引用",
|
||||
permission_class=PermissionClass.READ,
|
||||
side_effect_scope=SideEffectScope.NONE,
|
||||
tags=["development", "lsp", "code"],
|
||||
)
|
||||
|
||||
def get_parameters(self) -> dict[str, Any]:
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"type": "string",
|
||||
"enum": ["goto_definition", "find_references", "document_symbols"],
|
||||
"description": "LSP 操作类型",
|
||||
},
|
||||
"file": {
|
||||
"type": "string",
|
||||
"description": "文件路径",
|
||||
},
|
||||
"line": {
|
||||
"type": "integer",
|
||||
"description": "行号(1-based)",
|
||||
},
|
||||
"character": {
|
||||
"type": "integer",
|
||||
"description": "列号(0-based)",
|
||||
},
|
||||
},
|
||||
"required": ["action", "file"],
|
||||
}
|
||||
|
||||
def get_return_schema(self) -> dict[str, Any]:
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {"type": "boolean"},
|
||||
"results": {"type": "array"},
|
||||
},
|
||||
}
|
||||
|
||||
async def execute(
|
||||
self,
|
||||
action: str,
|
||||
file: str,
|
||||
line: int = 1,
|
||||
character: int = 0,
|
||||
) -> dict[str, Any]:
|
||||
# 注意:实际 LSP 调用需要通过 lsp-utils 或类似库
|
||||
# 这里只是一个框架实现
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"LSP action '{action}' not fully implemented - requires LSP server integration",
|
||||
"action": action,
|
||||
"file": file,
|
||||
"position": {"line": line, "character": character},
|
||||
}
|
||||
|
||||
|
||||
class GitTool(ReadTool):
|
||||
"""Git 操作工具
|
||||
|
||||
提供常用的 Git 操作。
|
||||
"""
|
||||
|
||||
def __init__(self, repo_path: str = "."):
|
||||
super().__init__(
|
||||
name="git",
|
||||
description="执行 Git 命令",
|
||||
permission_class=PermissionClass.EXTERNAL,
|
||||
side_effect_scope=SideEffectScope.LOCAL_STATE,
|
||||
requires_confirmation=True,
|
||||
tags=["development", "git", "version-control"],
|
||||
)
|
||||
self.repo_path = repo_path
|
||||
|
||||
def get_parameters(self) -> dict[str, Any]:
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "Git 子命令和参数,如 'status' 或 'log --oneline -10'",
|
||||
},
|
||||
"repo_path": {
|
||||
"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, repo_path: str | None = None) -> dict[str, Any]:
|
||||
import asyncio
|
||||
import os
|
||||
import platform
|
||||
|
||||
repo = repo_path or self.repo_path
|
||||
|
||||
# 构建完整的 git 命令
|
||||
if platform.system() == "Windows":
|
||||
full_command = f'git -C "{repo}" {command}'
|
||||
else:
|
||||
full_command = f"git -C '{repo}' {command}"
|
||||
|
||||
try:
|
||||
process = await asyncio.create_subprocess_shell(
|
||||
full_command,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
|
||||
stdout, stderr = await process.communicate()
|
||||
|
||||
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,
|
||||
}
|
||||
255
backend/app/agents/tools/builtins/file_tools.py
Normal file
255
backend/app/agents/tools/builtins/file_tools.py
Normal file
@@ -0,0 +1,255 @@
|
||||
"""文件操作工具 - Phase 6.4"""
|
||||
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from app.agents.tools.base import ExternalTool, ReadTool, WriteTool
|
||||
from app.agents.tools.manifest import (
|
||||
PermissionClass,
|
||||
SideEffectScope,
|
||||
ToolCategory,
|
||||
)
|
||||
|
||||
|
||||
class GlobTool(ReadTool):
|
||||
"""文件路径匹配工具
|
||||
|
||||
使用 glob 模式查找文件。
|
||||
"""
|
||||
|
||||
def __init__(self, root_dir: str = "."):
|
||||
super().__init__(
|
||||
name="glob",
|
||||
description="使用 glob 模式查找文件路径",
|
||||
permission_class=PermissionClass.READ,
|
||||
side_effect_scope=SideEffectScope.NONE,
|
||||
tags=["file", "search", "glob"],
|
||||
)
|
||||
self.root_dir = root_dir
|
||||
|
||||
def get_parameters(self) -> dict[str, Any]:
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pattern": {
|
||||
"type": "string",
|
||||
"description": "Glob 模式,如 **/*.py",
|
||||
},
|
||||
"root_dir": {
|
||||
"type": "string",
|
||||
"description": "搜索根目录(可选)",
|
||||
},
|
||||
},
|
||||
"required": ["pattern"],
|
||||
}
|
||||
|
||||
def get_return_schema(self) -> dict[str, Any]:
|
||||
return {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
}
|
||||
|
||||
async def execute(self, pattern: str, root_dir: str | None = None) -> list[str]:
|
||||
import glob as glob_module
|
||||
|
||||
root = root_dir or self.root_dir
|
||||
return glob_module.glob(pattern, root_dir=root, recursive=True)
|
||||
|
||||
|
||||
class GrepTool(ReadTool):
|
||||
"""文件内容搜索工具
|
||||
|
||||
在文件中搜索匹配的行。
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
name="grep",
|
||||
description="在文件中搜索匹配的文本行",
|
||||
permission_class=PermissionClass.READ,
|
||||
side_effect_scope=SideEffectScope.NONE,
|
||||
tags=["file", "search", "text"],
|
||||
)
|
||||
|
||||
def get_parameters(self) -> dict[str, Any]:
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pattern": {
|
||||
"type": "string",
|
||||
"description": "正则表达式模式",
|
||||
},
|
||||
"paths": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "要搜索的文件路径列表",
|
||||
},
|
||||
"case_sensitive": {
|
||||
"type": "boolean",
|
||||
"description": "是否区分大小写",
|
||||
},
|
||||
},
|
||||
"required": ["pattern", "paths"],
|
||||
}
|
||||
|
||||
def get_return_schema(self) -> dict[str, Any]:
|
||||
return {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file": {"type": "string"},
|
||||
"line": {"type": "integer"},
|
||||
"content": {"type": "string"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
async def execute(
|
||||
self, pattern: str, paths: list[str], case_sensitive: bool = True
|
||||
) -> list[dict[str, Any]]:
|
||||
import re
|
||||
|
||||
flags = 0 if case_sensitive else re.IGNORECASE
|
||||
regex = re.compile(pattern, flags)
|
||||
results = []
|
||||
|
||||
for path in paths:
|
||||
if not os.path.isfile(path):
|
||||
continue
|
||||
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
for line_num, line in enumerate(f, 1):
|
||||
if regex.search(line):
|
||||
results.append(
|
||||
{
|
||||
"file": path,
|
||||
"line": line_num,
|
||||
"content": line.rstrip(),
|
||||
}
|
||||
)
|
||||
except (UnicodeDecodeError, PermissionError):
|
||||
continue
|
||||
|
||||
return results
|
||||
|
||||
|
||||
class ReadFileTool(ReadTool):
|
||||
"""文件读取工具"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
name="read_file",
|
||||
description="读取文件内容",
|
||||
permission_class=PermissionClass.READ,
|
||||
side_effect_scope=SideEffectScope.NONE,
|
||||
tags=["file", "read"],
|
||||
)
|
||||
|
||||
def get_parameters(self) -> dict[str, Any]:
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "文件路径",
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "最大行数",
|
||||
},
|
||||
"offset": {
|
||||
"type": "integer",
|
||||
"description": "起始行号",
|
||||
},
|
||||
},
|
||||
"required": ["path"],
|
||||
}
|
||||
|
||||
def get_return_schema(self) -> dict[str, Any]:
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string"},
|
||||
"lines": {"type": "integer"},
|
||||
},
|
||||
}
|
||||
|
||||
async def execute(self, path: str, limit: int | None = None, offset: int = 0) -> dict[str, Any]:
|
||||
if not os.path.isfile(path):
|
||||
raise FileNotFoundError(f"File not found: {path}")
|
||||
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
lines = f.readlines()
|
||||
|
||||
total_lines = len(lines)
|
||||
start = max(0, offset)
|
||||
end = len(lines) if limit is None else min(start + limit, len(lines))
|
||||
|
||||
content = "".join(lines[start:end])
|
||||
|
||||
return {
|
||||
"content": content,
|
||||
"lines": total_lines,
|
||||
"truncated": limit is not None and end < len(lines),
|
||||
}
|
||||
|
||||
|
||||
class WriteFileTool(WriteTool):
|
||||
"""文件写入工具"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
name="write_file",
|
||||
description="写入文件内容",
|
||||
permission_class=PermissionClass.WRITE,
|
||||
side_effect_scope=SideEffectScope.LOCAL_STATE,
|
||||
requires_confirmation=True,
|
||||
tags=["file", "write"],
|
||||
)
|
||||
|
||||
def get_parameters(self) -> dict[str, Any]:
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "文件路径",
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "文件内容",
|
||||
},
|
||||
"append": {
|
||||
"type": "boolean",
|
||||
"description": "是否追加模式",
|
||||
},
|
||||
},
|
||||
"required": ["path", "content"],
|
||||
}
|
||||
|
||||
def get_return_schema(self) -> dict[str, Any]:
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {"type": "boolean"},
|
||||
"bytes_written": {"type": "integer"},
|
||||
},
|
||||
}
|
||||
|
||||
async def execute(self, path: str, content: str, append: bool = False) -> dict[str, Any]:
|
||||
mode = "a" if append else "w"
|
||||
|
||||
# 确保目录存在
|
||||
directory = os.path.dirname(path)
|
||||
if directory and not os.path.exists(directory):
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
|
||||
with open(path, mode, encoding="utf-8") as f:
|
||||
bytes_written = f.write(content)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"bytes_written": bytes_written,
|
||||
}
|
||||
193
backend/app/agents/tools/builtins/system_tools.py
Normal file
193
backend/app/agents/tools/builtins/system_tools.py
Normal file
@@ -0,0 +1,193 @@
|
||||
"""系统工具 - 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,
|
||||
}
|
||||
Reference in New Issue
Block a user