bash.py 4.2 KB

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