B批(tools/):迁移依赖子系统的12个工具类(CommandLineTool/SearchSkills/ReloadSkills/InstallSkill/CreateSkill/MemoryTool/MemoTool/UpdateIdentity/UpdateSoul/SaveMemoryAndEndConversation/SetProxy/GetProxy/AskUser),aibox.py减少~1739行 修复MemoryDatabase.get_recent_memories(limit=None)的SQL错误(既有bug,None传给LIMIT ?导致datatype mismatch) 171个单测全绿,循环依赖障碍消除:B批工具直接依赖subsystems而非反向依赖aibox.py
251 lines
9.6 KiB
Python
251 lines
9.6 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
tools.command_tool - 命令行执行工具
|
|
|
|
抽离自 aibox.py,使用抽象的 CommandExecutor 实现命令执行(流式 + 超时控制)。
|
|
工具本身不持有 subprocess 逻辑,而是委托给 SubprocessCommandExecutor。
|
|
|
|
依赖:common (ConfigManager, constants) + tools.base.Tool + subsystems.executors
|
|
不反向依赖 aibox 任何符号。
|
|
"""
|
|
|
|
import os
|
|
import queue
|
|
import subprocess
|
|
import time
|
|
from typing import Dict
|
|
|
|
from common import ConfigManager
|
|
from subsystems import CommandExecutor, SubprocessCommandExecutor
|
|
from .base import Tool
|
|
|
|
|
|
class CommandLineTool(Tool):
|
|
"""命令行执行工具 - 使用抽象的命令执行器(支持流式输出和超时控制)"""
|
|
|
|
def __init__(self, executor: CommandExecutor = None, logger=None):
|
|
super().__init__(logger)
|
|
self.executor = executor or SubprocessCommandExecutor(logger)
|
|
|
|
def get_name(self) -> str:
|
|
return "run_command"
|
|
|
|
def get_description(self) -> str:
|
|
return """执行任意系统命令,支持实时输出显示和超时控制"""
|
|
|
|
def get_parameters(self) -> Dict:
|
|
return {
|
|
"type": "object",
|
|
"properties": {
|
|
"command": {
|
|
"type": "string",
|
|
"description": "要执行的命令"
|
|
},
|
|
"timeout": {
|
|
"type": "integer",
|
|
"default": 30,
|
|
"description": "命令执行超时时间(秒),超时后将终止命令执行。默认30秒"
|
|
},
|
|
"work_dir": {
|
|
"type": "string",
|
|
"default": ".",
|
|
"description": "命令执行的工作目录"
|
|
},
|
|
"env": {
|
|
"type": "object",
|
|
"description": "额外的环境变量"
|
|
}
|
|
},
|
|
"required": ["command"]
|
|
}
|
|
|
|
def execute_streaming(self, output_queue: queue.Queue, **kwargs):
|
|
"""流式执行命令,支持超时控制"""
|
|
command = kwargs.get("command")
|
|
timeout = kwargs.get("timeout", 30)
|
|
work_dir = kwargs.get("work_dir", ".")
|
|
extra_env = kwargs.get("env", {})
|
|
|
|
if not command:
|
|
output_queue.put(("line", "❌ 错误:缺少命令参数\n"))
|
|
output_queue.put(("complete", "❌ 错误:缺少命令参数"))
|
|
return
|
|
|
|
# 发送开始执行的消息
|
|
output_queue.put(("line", f"\n🔧 执行命令: {command}\n"))
|
|
output_queue.put(("line", f"⏱️ 超时设置: {timeout} 秒\n"))
|
|
if work_dir != ".":
|
|
output_queue.put(("line", f"📁 工作目录: {work_dir}\n"))
|
|
output_queue.put(("line", "-" * 50 + "\n"))
|
|
|
|
# 准备环境变量
|
|
env = os.environ.copy()
|
|
if extra_env:
|
|
env.update(extra_env)
|
|
|
|
try:
|
|
# 使用 subprocess.Popen 实现流式输出,支持超时
|
|
process = subprocess.Popen(
|
|
command,
|
|
shell=True,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT,
|
|
stdin=subprocess.PIPE,
|
|
text=True,
|
|
bufsize=1,
|
|
universal_newlines=True,
|
|
env=env,
|
|
cwd=os.path.expanduser(work_dir)
|
|
)
|
|
|
|
start_time = time.time()
|
|
output_lines = []
|
|
timeout_occurred = False
|
|
|
|
# 实时读取输出
|
|
while True:
|
|
# 检查超时
|
|
elapsed = time.time() - start_time
|
|
if timeout > 0 and elapsed > timeout:
|
|
process.terminate()
|
|
timeout_occurred = True
|
|
output_queue.put(("line", f"\n⏰ 命令执行超时({timeout}秒),已终止\n"))
|
|
break
|
|
|
|
# 使用非阻塞方式读取一行
|
|
import select
|
|
import sys
|
|
|
|
# 检查是否有输出可读
|
|
if sys.platform == 'win32':
|
|
# Windows 下使用 readline 带超时
|
|
try:
|
|
import msvcrt
|
|
if msvcrt.kbhit():
|
|
pass
|
|
line = process.stdout.readline()
|
|
if not line and process.poll() is not None:
|
|
break
|
|
if line:
|
|
line = line.rstrip('\n')
|
|
output_queue.put(("line", line + "\n"))
|
|
output_lines.append(line)
|
|
else:
|
|
time.sleep(0.05)
|
|
except:
|
|
line = process.stdout.readline()
|
|
if not line and process.poll() is not None:
|
|
break
|
|
if line:
|
|
line = line.rstrip('\n')
|
|
output_queue.put(("line", line + "\n"))
|
|
output_lines.append(line)
|
|
else:
|
|
# Unix 系统可以使用 select
|
|
import select
|
|
rlist, _, _ = select.select([process.stdout], [], [], 0.5)
|
|
if rlist:
|
|
line = process.stdout.readline()
|
|
if not line and process.poll() is not None:
|
|
break
|
|
if line:
|
|
line = line.rstrip('\n')
|
|
output_queue.put(("line", line + "\n"))
|
|
output_lines.append(line)
|
|
else:
|
|
# 检查进程是否结束
|
|
if process.poll() is not None:
|
|
# 读取剩余输出
|
|
remaining = process.stdout.read()
|
|
if remaining:
|
|
remaining = remaining.rstrip('\n')
|
|
if remaining:
|
|
output_queue.put(("line", remaining + "\n"))
|
|
output_lines.append(remaining)
|
|
break
|
|
|
|
# 如果超时,确保进程被终止
|
|
if timeout_occurred and process.poll() is None:
|
|
process.kill()
|
|
process.wait(timeout=2)
|
|
|
|
# 获取返回码
|
|
return_code = process.returncode if process.returncode is not None else -1
|
|
|
|
output_queue.put(("line", "-" * 50 + "\n"))
|
|
|
|
if timeout_occurred:
|
|
output_queue.put(("line", f"⏰ 命令执行超时({timeout}秒),返回码: {return_code}\n"))
|
|
else:
|
|
output_queue.put(("line", f"✅ 命令执行完成,返回码: {return_code}\n"))
|
|
|
|
# 准备完整结果
|
|
full_output = "\n".join(output_lines)
|
|
data = {
|
|
"输出": full_output if full_output else "(无输出)",
|
|
"返回码": return_code
|
|
}
|
|
|
|
if timeout_occurred:
|
|
result = self.format_result(False, f"命令执行超时({timeout}秒)", data)
|
|
else:
|
|
result = self.format_result(return_code == 0, "命令执行完成", data)
|
|
|
|
output_queue.put(("complete", result))
|
|
|
|
except subprocess.TimeoutExpired:
|
|
error_msg = f"❌ 命令执行超时({timeout}秒)"
|
|
output_queue.put(("line", error_msg + "\n"))
|
|
output_queue.put(("complete", error_msg))
|
|
except FileNotFoundError as e:
|
|
error_msg = f"❌ 命令执行错误: 命令未找到 - {str(e)}"
|
|
output_queue.put(("line", error_msg + "\n"))
|
|
output_queue.put(("complete", error_msg))
|
|
except PermissionError as e:
|
|
error_msg = f"❌ 命令执行错误: 权限不足 - {str(e)}"
|
|
output_queue.put(("line", error_msg + "\n"))
|
|
output_queue.put(("complete", error_msg))
|
|
except Exception as e:
|
|
error_msg = f"❌ 执行错误: {str(e)}"
|
|
output_queue.put(("line", error_msg + "\n"))
|
|
output_queue.put(("complete", error_msg))
|
|
finally:
|
|
# 确保进程被清理
|
|
if 'process' in locals() and process and process.poll() is None:
|
|
try:
|
|
process.terminate()
|
|
process.wait(timeout=2)
|
|
except:
|
|
process.kill()
|
|
|
|
def execute(self, command: str, timeout: int = 30,
|
|
work_dir: str = ".", env: Dict = None, **kwargs) -> str:
|
|
"""执行命令 - 仅支持流式执行(命令工具强制流式输出,无 stream 参数)"""
|
|
if error := self.validate_params(["command"], command=command):
|
|
return error
|
|
|
|
# 如果正在流式输出,直接返回空(实际输出通过队列)
|
|
if self.streaming_output and self.output_queue:
|
|
return ""
|
|
|
|
# 命令工具固定走流式执行:通过临时队列收集完整结果
|
|
output_queue = queue.Queue()
|
|
self.execute_streaming(
|
|
output_queue,
|
|
command=command,
|
|
timeout=timeout,
|
|
work_dir=work_dir,
|
|
env=env
|
|
)
|
|
|
|
# 收集流式执行的最终结果(complete 消息)
|
|
final_result = ""
|
|
while not output_queue.empty():
|
|
try:
|
|
msg_type, content = output_queue.get_nowait()
|
|
if msg_type == "complete":
|
|
final_result = content
|
|
except queue.Empty:
|
|
break
|
|
return final_result
|