|
|
|
|
@ -2,6 +2,8 @@ package com.claudecode.tool.impl; |
|
|
|
|
|
|
|
|
|
import com.claudecode.tool.Tool; |
|
|
|
|
import com.claudecode.tool.ToolContext; |
|
|
|
|
import org.slf4j.Logger; |
|
|
|
|
import org.slf4j.LoggerFactory; |
|
|
|
|
|
|
|
|
|
import java.io.BufferedReader; |
|
|
|
|
import java.io.InputStreamReader; |
|
|
|
|
@ -10,15 +12,47 @@ import java.util.Map; |
|
|
|
|
import java.util.concurrent.TimeUnit; |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Bash 工具 —— 对应 claude-code/src/tools/bash/BashTool.ts。 |
|
|
|
|
* Shell 工具 —— 对应 claude-code/src/tools/bash/BashTool.ts。 |
|
|
|
|
* <p> |
|
|
|
|
* 在指定工作目录中执行 shell 命令,返回 stdout/stderr 输出。 |
|
|
|
|
* 自动检测最佳可用 shell: |
|
|
|
|
* <ul> |
|
|
|
|
* <li>Windows: PowerShell > cmd.exe</li> |
|
|
|
|
* <li>Unix/Linux/macOS: bash(或 SHELL 环境变量)</li> |
|
|
|
|
* </ul> |
|
|
|
|
*/ |
|
|
|
|
public class BashTool implements Tool { |
|
|
|
|
|
|
|
|
|
private static final Logger log = LoggerFactory.getLogger(BashTool.class); |
|
|
|
|
|
|
|
|
|
/** 默认超时(秒) */ |
|
|
|
|
private static final int DEFAULT_TIMEOUT = 120; |
|
|
|
|
|
|
|
|
|
/** 检测到的 shell 类型 */ |
|
|
|
|
public enum ShellType { |
|
|
|
|
POWERSHELL("PowerShell", "pwsh", "-NoProfile", "-Command"), |
|
|
|
|
POWERSHELL_WINDOWS("PowerShell", "powershell.exe", "-NoProfile", "-Command"), |
|
|
|
|
CMD("cmd.exe", "cmd.exe", "/c", null), |
|
|
|
|
BASH("Bash", "bash", "-c", null), |
|
|
|
|
SH("sh", "sh", "-c", null); |
|
|
|
|
|
|
|
|
|
final String displayName; |
|
|
|
|
final String executable; |
|
|
|
|
final String flag1; |
|
|
|
|
final String flag2; // 可选的额外 flag
|
|
|
|
|
|
|
|
|
|
ShellType(String displayName, String executable, String flag1, String flag2) { |
|
|
|
|
this.displayName = displayName; |
|
|
|
|
this.executable = executable; |
|
|
|
|
this.flag1 = flag1; |
|
|
|
|
this.flag2 = flag2; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private static final boolean IS_WINDOWS = System.getProperty("os.name", "").toLowerCase().contains("win"); |
|
|
|
|
private static final ShellType DETECTED_SHELL = detectShell(); |
|
|
|
|
private static final String SHELL_HINT = buildShellHint(); |
|
|
|
|
|
|
|
|
|
@Override |
|
|
|
|
public String name() { |
|
|
|
|
return "Bash"; |
|
|
|
|
@ -26,6 +60,19 @@ public class BashTool implements Tool { |
|
|
|
|
|
|
|
|
|
@Override |
|
|
|
|
public String description() { |
|
|
|
|
if (IS_WINDOWS && DETECTED_SHELL.displayName.equals("PowerShell")) { |
|
|
|
|
return """ |
|
|
|
|
Execute a command in the working directory using PowerShell. \ |
|
|
|
|
Use PowerShell syntax (Get-ChildItem, Select-String, Get-Content, etc). \ |
|
|
|
|
Common equivalents: ls→Get-ChildItem, grep→Select-String, cat→Get-Content, \ |
|
|
|
|
rm→Remove-Item, cp→Copy-Item, mv→Move-Item, find→Get-ChildItem -Recurse. \ |
|
|
|
|
Commands run in a subprocess with timeout protection."""; |
|
|
|
|
} else if (IS_WINDOWS) { |
|
|
|
|
return """ |
|
|
|
|
Execute a command in the working directory using cmd.exe. \ |
|
|
|
|
Use Windows cmd syntax (dir, type, find, etc). \ |
|
|
|
|
Commands run in a subprocess with timeout protection."""; |
|
|
|
|
} |
|
|
|
|
return """ |
|
|
|
|
Execute a bash command in the working directory. \ |
|
|
|
|
Use this for file operations, running scripts, installing packages, \ |
|
|
|
|
@ -60,15 +107,7 @@ public class BashTool implements Tool { |
|
|
|
|
Path workDir = context.getWorkDir(); |
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
// 根据操作系统选择 shell
|
|
|
|
|
boolean isWindows = System.getProperty("os.name").toLowerCase().contains("win"); |
|
|
|
|
ProcessBuilder pb; |
|
|
|
|
if (isWindows) { |
|
|
|
|
pb = new ProcessBuilder("cmd", "/c", command); |
|
|
|
|
} else { |
|
|
|
|
pb = new ProcessBuilder("bash", "-c", command); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
ProcessBuilder pb = buildProcess(command); |
|
|
|
|
pb.directory(workDir.toFile()); |
|
|
|
|
pb.redirectErrorStream(true); |
|
|
|
|
|
|
|
|
|
@ -104,10 +143,101 @@ public class BashTool implements Tool { |
|
|
|
|
@Override |
|
|
|
|
public String activityDescription(Map<String, Object> input) { |
|
|
|
|
String cmd = (String) input.getOrDefault("command", ""); |
|
|
|
|
// 截断过长的命令
|
|
|
|
|
if (cmd.length() > 60) { |
|
|
|
|
cmd = cmd.substring(0, 57) + "..."; |
|
|
|
|
} |
|
|
|
|
return "⚡ " + cmd; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** 构建 ProcessBuilder,根据检测到的 shell 类型 */ |
|
|
|
|
private ProcessBuilder buildProcess(String command) { |
|
|
|
|
return switch (DETECTED_SHELL) { |
|
|
|
|
case POWERSHELL -> new ProcessBuilder("pwsh", "-NoProfile", "-Command", command); |
|
|
|
|
case POWERSHELL_WINDOWS -> new ProcessBuilder("powershell.exe", "-NoProfile", "-Command", command); |
|
|
|
|
case CMD -> new ProcessBuilder("cmd.exe", "/c", command); |
|
|
|
|
case BASH -> new ProcessBuilder("bash", "-c", command); |
|
|
|
|
case SH -> new ProcessBuilder("sh", "-c", command); |
|
|
|
|
}; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** 检测最佳可用 shell */ |
|
|
|
|
private static ShellType detectShell() { |
|
|
|
|
if (IS_WINDOWS) { |
|
|
|
|
// 优先 pwsh (PowerShell 7+)
|
|
|
|
|
if (isCommandAvailable("pwsh", "--version")) { |
|
|
|
|
log.info("Detected shell: PowerShell 7+ (pwsh)"); |
|
|
|
|
return ShellType.POWERSHELL; |
|
|
|
|
} |
|
|
|
|
// 回退到 Windows PowerShell 5.x
|
|
|
|
|
if (isCommandAvailable("powershell.exe", "-NoProfile", "-Command", "echo ok")) { |
|
|
|
|
log.info("Detected shell: Windows PowerShell (powershell.exe)"); |
|
|
|
|
return ShellType.POWERSHELL_WINDOWS; |
|
|
|
|
} |
|
|
|
|
// 最终回退到 cmd
|
|
|
|
|
log.info("Detected shell: cmd.exe (fallback)"); |
|
|
|
|
return ShellType.CMD; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Unix: 优先 bash
|
|
|
|
|
String shellEnv = System.getenv("SHELL"); |
|
|
|
|
if (shellEnv != null && shellEnv.contains("bash")) { |
|
|
|
|
return ShellType.BASH; |
|
|
|
|
} |
|
|
|
|
if (isCommandAvailable("bash", "--version")) { |
|
|
|
|
return ShellType.BASH; |
|
|
|
|
} |
|
|
|
|
return ShellType.SH; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** 检查命令是否可用 */ |
|
|
|
|
private static boolean isCommandAvailable(String... cmd) { |
|
|
|
|
try { |
|
|
|
|
Process p = new ProcessBuilder(cmd) |
|
|
|
|
.redirectErrorStream(true) |
|
|
|
|
.start(); |
|
|
|
|
p.getInputStream().readAllBytes(); |
|
|
|
|
return p.waitFor(5, TimeUnit.SECONDS) && p.exitValue() == 0; |
|
|
|
|
} catch (Exception e) { |
|
|
|
|
return false; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** 获取 shell 提示信息(供系统提示词使用) */ |
|
|
|
|
public static String getShellHint() { |
|
|
|
|
return SHELL_HINT; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** 获取检测到的 shell 显示名 */ |
|
|
|
|
public static String getDetectedShellName() { |
|
|
|
|
return DETECTED_SHELL.displayName; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private static String buildShellHint() { |
|
|
|
|
if (IS_WINDOWS && DETECTED_SHELL.displayName.equals("PowerShell")) { |
|
|
|
|
return """ |
|
|
|
|
- Shell: %s (detected on Windows) |
|
|
|
|
- IMPORTANT: The Bash tool executes commands via PowerShell, NOT bash or cmd.exe. |
|
|
|
|
- Use PowerShell native cmdlets and syntax: |
|
|
|
|
- List files: Get-ChildItem (or ls/dir aliases) |
|
|
|
|
- Search text: Select-String -Pattern "xxx" -Path *.java |
|
|
|
|
- Read file: Get-Content file.txt |
|
|
|
|
- Delete: Remove-Item path |
|
|
|
|
- Copy: Copy-Item src dst |
|
|
|
|
- Move: Move-Item src dst |
|
|
|
|
- Find files: Get-ChildItem -Recurse -Filter "*.java" |
|
|
|
|
- Current dir: Get-Location (or pwd) |
|
|
|
|
- Environment vars: $env:PATH |
|
|
|
|
- Pipe: cmd1 | cmd2 |
|
|
|
|
- String comparison: -eq, -ne, -like, -match |
|
|
|
|
- Standard tools (git, mvn, npm, java, python) work normally. |
|
|
|
|
""".formatted(DETECTED_SHELL.displayName); |
|
|
|
|
} else if (IS_WINDOWS) { |
|
|
|
|
return """ |
|
|
|
|
- Shell: cmd.exe (Windows) |
|
|
|
|
- Use Windows cmd syntax: dir, type, find, etc. |
|
|
|
|
- Standard tools (git, mvn, npm) work normally. |
|
|
|
|
"""; |
|
|
|
|
} |
|
|
|
|
return "- Shell: " + DETECTED_SHELL.displayName + "\n"; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|