terminal_tool.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. import subprocess
  2. import shlex
  3. import os
  4. class TerminalTool:
  5. name = "terminal_exec"
  6. description = "执行终端命令查看目录、文件和系统信息(支持:pwd, ls, cat, echo, whoami, date等)"
  7. def __init__(self, security_mode="strict"):
  8. """
  9. 初始化终端工具
  10. Args:
  11. security_mode: "strict"(严格模式,直接拒绝) 或 "warning"(警告模式,给出提示)
  12. """
  13. self.security_mode = security_mode
  14. # 扩展的白名单命令列表(无参数或安全参数的命令)
  15. self.allowed_commands = {
  16. "ls": [], # ls 可以带参数如 -l, -a
  17. "pwd": [],
  18. "echo": ["*"], # echo 允许任何参数
  19. "whoami": [],
  20. "cat": ["*"], # cat 允许文件名参数
  21. "head": ["-n"], # head 允许 -n 参数
  22. "tail": ["-n"],
  23. "wc": ["-l", "-w"],
  24. "date": [],
  25. "uname": ["-a"],
  26. "find": ["."], # 限制搜索起点
  27. # 新增的常用安全命令
  28. "cd": [], # 目录切换
  29. "mkdir": ["-p"], # 创建目录
  30. "touch": [], # 创建文件
  31. "grep": ["-i", "-n", "-r"], # 文本搜索
  32. "which": [], # 查找命令位置
  33. "whereis": [], # 查找程序位置
  34. "du": ["-h", "-s"], # 磁盘使用情况
  35. "df": ["-h"], # 文件系统信息
  36. }
  37. # 危险关键词,用于额外安全检查
  38. self.dangerous_keywords = [
  39. "rm", "delete", "del", "format", "mkfs",
  40. "sudo", "su", "passwd", "chmod", "chown",
  41. "dd", "mkfs", "fdisk", ">", ">>", "|",
  42. ";", "&&", "||", "`", "$(", "eval"
  43. ]
  44. def get_parameters(self):
  45. return {
  46. "input": {
  47. "type": "str",
  48. "description": "输入终端命令,如:pwd, ls -la, cat filename.txt",
  49. "required": True,
  50. "examples": ["pwd", "ls -la", "cat README.md", "echo hello", "whoami", "date"]
  51. }
  52. }
  53. def _check_command_safety(self, cmd):
  54. """检查命令安全性
  55. Returns:
  56. tuple: (is_safe, error_msg, warning_msg)
  57. is_safe: bool - 是否安全
  58. error_msg: str - 错误消息
  59. warning_msg: str - 警告消息
  60. """
  61. # 检查危险关键词
  62. cmd_lower = cmd.lower()
  63. for keyword in self.dangerous_keywords:
  64. if keyword in cmd_lower:
  65. error_msg = f"检测到不安全的操作:{keyword}"
  66. warning_msg = f"⚠️ 警告:此命令包含 '{keyword}' 操作,可能导致系统损坏或数据丢失!"
  67. return False, error_msg, warning_msg
  68. # 检查是否包含管道、重定向等操作
  69. operators = ["|", ">", "<", "&", "&&", "||", ";"]
  70. for op in operators:
  71. if op in cmd:
  72. error_msg = f"检测到不安全的操作符:{op}"
  73. warning_msg = f"⚠️ 警告:此命令包含 '{op}' 操作符,可能导致意外行为!"
  74. return False, error_msg, warning_msg
  75. return True, None, None
  76. def run(self, parameters):
  77. # 确保参数处理的安全性
  78. if isinstance(parameters, dict):
  79. # 统一使用 {"input": command} 格式
  80. cmd = parameters.get("input", "")
  81. else:
  82. cmd = str(parameters) if parameters else ""
  83. cmd = cmd.strip() if cmd else ""
  84. if not cmd:
  85. return "错误: 命令不能为空"
  86. # 安全检查
  87. is_safe, error_msg, warning_msg = self._check_command_safety(cmd)
  88. if not is_safe:
  89. if self.security_mode == "strict":
  90. return f"🚫 安全拒绝: {error_msg}"
  91. else: # warning mode
  92. return f"{warning_msg}\n\n命令: {cmd}\n\n如需继续执行,请确认操作的安全性。\n(当前为警告模式,尚未真正执行)"
  93. # 分割命令和参数
  94. parts = shlex.split(cmd)
  95. if not parts:
  96. return "错误: 无效的命令"
  97. command_name = parts[0]
  98. args = parts[1:] if len(parts) > 1 else []
  99. # 检查命令是否在白名单中
  100. if command_name not in self.allowed_commands:
  101. allowed_list = ", ".join(sorted(self.allowed_commands.keys()))
  102. similar_commands = self._find_similar_commands(command_name)
  103. error_msg = f"🚫 命令 '{command_name}' 不在允许列表中。"
  104. error_msg += f"\n\n✅ 允许的命令: {allowed_list}"
  105. if similar_commands:
  106. error_msg += f"\n💡 您是否想使用: {', '.join(similar_commands)}?"
  107. error_msg += f"\n\n📖 输入 'help' 或 '?' 查看命令帮助"
  108. return error_msg
  109. # 检查参数
  110. allowed_args = self.allowed_commands[command_name]
  111. # 改进的参数验证逻辑
  112. if "*" not in allowed_args and args:
  113. validation_result = self._validate_parameters(command_name, args)
  114. if not validation_result[0]: # 验证失败
  115. return validation_result[1]
  116. # 如果允许任何参数,进行基本安全检查
  117. elif "*" in allowed_args and args:
  118. validation_result = self._validate_wildcard_args(command_name, args)
  119. if not validation_result[0]: # 验证失败
  120. return validation_result[1]
  121. # 执行命令(使用 shell=False 提高安全性)
  122. try:
  123. # 使用 shlex.split 可以正确处理带引号的参数
  124. result = subprocess.run(
  125. cmd,
  126. shell=True, # 保持向后兼容,但需要更严格的白名单
  127. capture_output=True,
  128. text=True,
  129. timeout=15,
  130. cwd=None # 限制在安全目录执行
  131. )
  132. # 组合标准输出和标准错误
  133. output = result.stdout
  134. if result.stderr:
  135. output += f"\n[标准错误]\n{result.stderr}"
  136. # 返回执行结果
  137. if result.returncode == 0:
  138. return output.strip() if output.strip() else "命令执行成功(无输出)"
  139. else:
  140. return f"命令执行失败 (返回码: {result.returncode})\n{output.strip()}"
  141. except subprocess.TimeoutExpired:
  142. return "命令执行超时(超过15秒)。"
  143. except subprocess.CalledProcessError as e:
  144. error_output = e.stderr.decode() if isinstance(e.stderr, bytes) else e.stderr
  145. return f"命令执行错误: {error_output or str(e)}"
  146. except Exception as e:
  147. return f"执行异常: {str(e)}"
  148. def _validate_parameters(self, command_name, args):
  149. """验证特定命令的参数
  150. Args:
  151. command_name: 命令名称
  152. args: 参数列表
  153. Returns:
  154. tuple: (is_valid, error_message)
  155. """
  156. allowed_args = self.allowed_commands[command_name]
  157. # 验证选项参数
  158. option_args = [arg for arg in args if arg.startswith("-")]
  159. for arg in option_args:
  160. if arg not in allowed_args and arg != "-p": # -p 是特殊的,允许mkdir使用
  161. help_text = self._get_command_help(command_name)
  162. return False, f"参数 '{arg}' 不被允许。\n{help_text}"
  163. # 验证非选项参数(通常是文件路径)
  164. file_args = [arg for arg in args if not arg.startswith("-")]
  165. for arg in file_args:
  166. if self._is_dangerous_path(arg):
  167. return False, f"危险路径: {arg}\n只允许访问当前目录及其子目录"
  168. return True, None
  169. def _validate_wildcard_args(self, command_name, args):
  170. """验证通配符参数(适用于cat、echo等)
  171. Args:
  172. command_name: 命令名称
  173. args: 参数列表
  174. Returns:
  175. tuple: (is_valid, error_message)
  176. """
  177. # 对于文件操作命令,进行路径安全检查
  178. if command_name in ["cat", "head", "tail", "grep"]:
  179. for arg in args:
  180. if not arg.startswith("-") and self._is_dangerous_path(arg):
  181. return False, f"危险路径: {arg}\n只允许访问当前目录及其子目录"
  182. return True, None
  183. def _is_dangerous_path(self, path):
  184. """检查路径是否危险
  185. Args:
  186. path: 要检查的路径
  187. Returns:
  188. bool: 是否为危险路径
  189. """
  190. # 检查绝对路径
  191. if os.path.isabs(path):
  192. return True
  193. # 检查包含危险字符的路径
  194. dangerous_patterns = ["../", "..\\", "~/", "/etc", "/bin", "/usr", "/var", "/sys"]
  195. for pattern in dangerous_patterns:
  196. if pattern in path:
  197. return True
  198. return False
  199. def _get_command_help(self, command_name):
  200. """返回命令的使用帮助
  201. Args:
  202. command_name: 命令名称
  203. Returns:
  204. str: 帮助信息
  205. """
  206. help_text = {
  207. "pwd": "用法: pwd\n功能: 显示当前工作目录",
  208. "ls": "用法: ls [-la] [路径]\n功能: 列出目录内容\n选项: -l(详细信息), -a(显示隐藏文件)",
  209. "cat": "用法: cat <文件名>\n功能: 显示文件内容",
  210. "head": "用法: head [-n 行数] <文件>\n功能: 显示文件开头内容",
  211. "tail": "用法: tail [-n 行数] <文件>\n功能: 显示文件末尾内容",
  212. "wc": "用法: wc [-l|-w] <文件>\n功能: 统计文件行数、字数\n选项: -l(行数), -w(字数)",
  213. "echo": "用法: echo <文本>\n功能: 输出文本",
  214. "whoami": "用法: whoami\n功能: 显示当前用户名",
  215. "date": "用法: date\n功能: 显示当前日期时间",
  216. "uname": "用法: uname [-a]\n功能: 显示系统信息\n选项: -a(所有信息)",
  217. "find": "用法: find . [选项]\n功能: 查找文件\n注意: 只能在当前目录搜索",
  218. "cd": "用法: cd <目录>\n功能: 切换到指定目录",
  219. "mkdir": "用法: mkdir [-p] <目录名>\n功能: 创建目录\n选项: -p(递归创建)",
  220. "touch": "用法: touch <文件名>\n功能: 创建空文件",
  221. "grep": "用法: grep [-inr] '模式' <文件>\n功能: 搜索文本\n选项: -i(忽略大小写), -n(显示行号), -r(递归)",
  222. "which": "用法: which <命令>\n功能: 查找命令位置",
  223. "whereis": "用法: whereis <程序>\n功能: 查找程序位置",
  224. "du": "用法: du [-hs] [路径]\n功能: 显示磁盘使用情况\n选项: -h(人类可读), -s(总计)",
  225. "df": "用法: df [-h]\n功能: 显示文件系统信息\n选项: -h(人类可读)"
  226. }
  227. return help_text.get(command_name, f"命令 '{command_name}' 暂无帮助信息")
  228. def _find_similar_commands(self, command_name):
  229. """查找相似的命令名称
  230. Args:
  231. command_name: 输入的命令名称
  232. Returns:
  233. list: 相似命令列表
  234. """
  235. import difflib
  236. # 获取所有允许的命令
  237. allowed_commands = list(self.allowed_commands.keys())
  238. # 使用difflib查找相似命令
  239. similar = difflib.get_close_matches(command_name, allowed_commands, n=3, cutoff=0.6)
  240. return similar