bash.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. """
  2. Bash Tool - 命令执行工具
  3. 参考 OpenCode bash.ts 完整实现。
  4. 核心功能:
  5. - 执行 shell 命令
  6. - 超时控制
  7. - 工作目录设置
  8. - 环境变量传递
  9. """
  10. import subprocess
  11. import asyncio
  12. from pathlib import Path
  13. from typing import Optional, Dict
  14. from agent.tools import tool, ToolResult, ToolContext
  15. # 常量
  16. DEFAULT_TIMEOUT = 120 # 2 分钟
  17. MAX_OUTPUT_LENGTH = 50000 # 最大输出长度
  18. @tool(description="执行 bash 命令")
  19. async def bash_command(
  20. command: str,
  21. timeout: Optional[int] = None,
  22. workdir: Optional[str] = None,
  23. env: Optional[Dict[str, str]] = None,
  24. description: str = "",
  25. context: Optional[ToolContext] = None
  26. ) -> ToolResult:
  27. """
  28. 执行 bash 命令
  29. Args:
  30. command: 要执行的命令
  31. timeout: 超时时间(秒),默认 120 秒
  32. workdir: 工作目录,默认为当前目录
  33. env: 环境变量字典(会合并到系统环境变量)
  34. description: 命令描述(5-10 个词)
  35. context: 工具上下文
  36. Returns:
  37. ToolResult: 命令输出
  38. """
  39. # 参数验证
  40. if timeout is not None and timeout < 0:
  41. return ToolResult(
  42. title="参数错误",
  43. output=f"无效的 timeout 值: {timeout}。必须是正数。",
  44. error="Invalid timeout"
  45. )
  46. timeout_sec = timeout or DEFAULT_TIMEOUT
  47. # 工作目录
  48. cwd = Path(workdir) if workdir else Path.cwd()
  49. if not cwd.exists():
  50. return ToolResult(
  51. title="目录不存在",
  52. output=f"工作目录不存在: {workdir}",
  53. error="Directory not found"
  54. )
  55. # 准备环境变量
  56. import os
  57. process_env = os.environ.copy()
  58. if env:
  59. process_env.update(env)
  60. # 执行命令
  61. try:
  62. process = await asyncio.create_subprocess_shell(
  63. command,
  64. stdout=asyncio.subprocess.PIPE,
  65. stderr=asyncio.subprocess.PIPE,
  66. cwd=str(cwd),
  67. env=process_env
  68. )
  69. # 等待命令完成(带超时)
  70. try:
  71. stdout, stderr = await asyncio.wait_for(
  72. process.communicate(),
  73. timeout=timeout_sec
  74. )
  75. except asyncio.TimeoutError:
  76. # 超时,终止进程
  77. process.kill()
  78. await process.wait()
  79. return ToolResult(
  80. title="命令超时",
  81. output=f"命令执行超时(>{timeout_sec}s): {command[:100]}",
  82. error="Timeout",
  83. metadata={"command": command, "timeout": timeout_sec}
  84. )
  85. # 解码输出
  86. stdout_text = stdout.decode('utf-8', errors='replace') if stdout else ""
  87. stderr_text = stderr.decode('utf-8', errors='replace') if stderr else ""
  88. # 截断过长输出
  89. truncated = False
  90. if len(stdout_text) > MAX_OUTPUT_LENGTH:
  91. stdout_text = stdout_text[:MAX_OUTPUT_LENGTH] + f"\n\n(输出被截断,总长度: {len(stdout_text)} 字符)"
  92. truncated = True
  93. # 组合输出
  94. output = ""
  95. if stdout_text:
  96. output += stdout_text
  97. if stderr_text:
  98. if output:
  99. output += "\n\n--- stderr ---\n"
  100. output += stderr_text
  101. if not output:
  102. output = "(命令无输出)"
  103. # 检查退出码
  104. exit_code = process.returncode
  105. success = exit_code == 0
  106. title = description or f"命令: {command[:50]}"
  107. if not success:
  108. title += f" (exit code: {exit_code})"
  109. return ToolResult(
  110. title=title,
  111. output=output,
  112. metadata={
  113. "exit_code": exit_code,
  114. "success": success,
  115. "truncated": truncated,
  116. "command": command,
  117. "cwd": str(cwd)
  118. },
  119. error=None if success else f"Command failed with exit code {exit_code}"
  120. )
  121. except Exception as e:
  122. return ToolResult(
  123. title="执行错误",
  124. output=f"命令执行失败: {str(e)}",
  125. error=str(e),
  126. metadata={"command": command}
  127. )