mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-13 00:54:53 +08:00
153 lines
3.2 KiB
Go
153 lines
3.2 KiB
Go
package tools
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"clawgo/pkg/config"
|
|
)
|
|
|
|
type ExecTool struct {
|
|
workingDir string
|
|
timeout time.Duration
|
|
sandboxEnabled bool
|
|
sandboxImage string
|
|
}
|
|
|
|
func NewExecTool(cfg config.ShellConfig, workspace string) *ExecTool {
|
|
return &ExecTool{
|
|
workingDir: workspace,
|
|
timeout: cfg.Timeout,
|
|
sandboxEnabled: cfg.Sandbox.Enabled,
|
|
sandboxImage: cfg.Sandbox.Image,
|
|
}
|
|
}
|
|
|
|
func (t *ExecTool) Name() string {
|
|
return "exec"
|
|
}
|
|
|
|
func (t *ExecTool) Description() string {
|
|
return "Execute a shell command and return its output. Use with caution."
|
|
}
|
|
|
|
func (t *ExecTool) Parameters() map[string]interface{} {
|
|
return map[string]interface{}{
|
|
"type": "object",
|
|
"properties": map[string]interface{}{
|
|
"command": map[string]interface{}{
|
|
"type": "string",
|
|
"description": "The shell command to execute",
|
|
},
|
|
"working_dir": map[string]interface{}{
|
|
"type": "string",
|
|
"description": "Optional working directory for the command",
|
|
},
|
|
},
|
|
"required": []string{"command"},
|
|
}
|
|
}
|
|
|
|
func (t *ExecTool) Execute(ctx context.Context, args map[string]interface{}) (string, error) {
|
|
command, ok := args["command"].(string)
|
|
if !ok {
|
|
return "", fmt.Errorf("command is required")
|
|
}
|
|
|
|
cwd := t.workingDir
|
|
if wd, ok := args["working_dir"].(string); ok && wd != "" {
|
|
cwd = wd
|
|
}
|
|
|
|
if cwd == "" {
|
|
wd, err := os.Getwd()
|
|
if err == nil {
|
|
cwd = wd
|
|
}
|
|
}
|
|
|
|
if t.sandboxEnabled {
|
|
return t.executeInSandbox(ctx, command, cwd)
|
|
}
|
|
|
|
return t.executeCommand(ctx, command, cwd)
|
|
}
|
|
|
|
func (t *ExecTool) executeInSandbox(ctx context.Context, command, cwd string) (string, error) {
|
|
// Execute command inside Docker sandbox
|
|
absCwd, _ := filepath.Abs(cwd)
|
|
dockerArgs := []string{
|
|
"run", "--rm",
|
|
"-v", fmt.Sprintf("%s:/app", absCwd),
|
|
"-w", "/app",
|
|
t.sandboxImage,
|
|
"sh", "-c", command,
|
|
}
|
|
|
|
cmdCtx, cancel := context.WithTimeout(ctx, t.timeout)
|
|
defer cancel()
|
|
|
|
cmd := exec.CommandContext(cmdCtx, "docker", dockerArgs...)
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
|
|
err := cmd.Run()
|
|
output := stdout.String()
|
|
if stderr.Len() > 0 {
|
|
output += "\nSTDERR:\n" + stderr.String()
|
|
}
|
|
|
|
if err != nil {
|
|
output += fmt.Sprintf("\nSandbox Exit code: %v", err)
|
|
}
|
|
|
|
return output, nil
|
|
}
|
|
|
|
func (t *ExecTool) SetTimeout(timeout time.Duration) {
|
|
t.timeout = timeout
|
|
}
|
|
|
|
func (t *ExecTool) executeCommand(ctx context.Context, command, cwd string) (string, error) {
|
|
cmdCtx, cancel := context.WithTimeout(ctx, t.timeout)
|
|
defer cancel()
|
|
|
|
cmd := exec.CommandContext(cmdCtx, "sh", "-c", command)
|
|
if cwd != "" {
|
|
cmd.Dir = cwd
|
|
}
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
|
|
err := cmd.Run()
|
|
output := stdout.String()
|
|
if stderr.Len() > 0 {
|
|
output += "\nSTDERR:\n" + stderr.String()
|
|
}
|
|
|
|
if err != nil {
|
|
if cmdCtx.Err() == context.DeadlineExceeded {
|
|
return fmt.Sprintf("Error: Command timed out after %v", t.timeout), nil
|
|
}
|
|
output += fmt.Sprintf("\nExit code: %v", err)
|
|
}
|
|
|
|
if output == "" {
|
|
output = "(no output)"
|
|
}
|
|
|
|
maxLen := 10000
|
|
if len(output) > maxLen {
|
|
output = output[:maxLen] + fmt.Sprintf("\n... (truncated, %d more chars)", len(output)-maxLen)
|
|
}
|
|
return output, nil
|
|
}
|