v1.4 工具集扩展 + Windows 编码攻坚
- 新增 BashTool(bash 命令执行,30s 超时 + 自愈 + 截断) - 新增 WriteFileTool(文件写入) - Windows GBK 编码自动转 UTF-8(golang.org/x/text) - OpenAIProvider 新增 API 交互日志 - dumpMessages 移除截断,完整展示上下文 - cmd/claw 三段式任务演示
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"go-tiny-claw/internal/schema"
|
||||
)
|
||||
|
||||
type BashTool struct {
|
||||
workDir string // 工作区约束
|
||||
}
|
||||
|
||||
func NewBashTool(workDir string) *BashTool {
|
||||
return &BashTool{workDir: workDir}
|
||||
}
|
||||
|
||||
func (t *BashTool) Name() string {
|
||||
return "bash"
|
||||
}
|
||||
|
||||
func (t *BashTool) Definition() schema.ToolDefinition {
|
||||
return schema.ToolDefinition{
|
||||
Name: t.Name(),
|
||||
Description: "在当前工作区执行任意的 bash 命令。支持链式命令(如 &&)。返回标准输出(stdout)和标准错误(stderr)。",
|
||||
InputSchema: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"command": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "要执行的 bash 命令,例如: ls -la 或 go test ./...",
|
||||
},
|
||||
},
|
||||
"required": []string{"command"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type bashArgs struct {
|
||||
Command string `json:"command"`
|
||||
}
|
||||
|
||||
func (t *BashTool) Execute(ctx context.Context, args json.RawMessage) (string, error) {
|
||||
var input bashArgs
|
||||
if err := json.Unmarshal(args, &input); err != nil {
|
||||
return "", fmt.Errorf("参数解析失败: %w", err)
|
||||
}
|
||||
|
||||
// 【驾驭底线 1】:Time Budgeting (时间预算与超时控制)
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Unix 和 Windows 统一使用 bash -c(Windows 需安装 Git Bash),编码转换在下方处理
|
||||
cmd := exec.CommandContext(timeoutCtx, "bash", "-c", input.Command)
|
||||
|
||||
// 【驾驭底线 2】:绑定执行的工作区目录
|
||||
cmd.Dir = t.workDir
|
||||
|
||||
// 执行并捕获 CombinedOutput (合并 stdout 和 stderr)
|
||||
out, err := cmd.CombinedOutput()
|
||||
|
||||
outputStr := string(out)
|
||||
|
||||
// 如果命令执行超时,返回警告信息让模型知晓
|
||||
if timeoutCtx.Err() == context.DeadlineExceeded {
|
||||
return outputStr + "\n[警告: 命令执行超时(30s),已被系统强制终止。如果是启动常驻服务,请尝试将其转入后台。]", nil
|
||||
}
|
||||
|
||||
// 【驾驭底线 3】:错误原样回传 (Self-Correction 自愈机制)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("执行报错: %v\n输出:\n%s", err, outputStr), nil
|
||||
}
|
||||
|
||||
// 如果没有终端输出,给模型一个明确的执行成功的反馈
|
||||
if outputStr == "" {
|
||||
return "命令执行成功,无终端输出。", nil
|
||||
}
|
||||
|
||||
// 【驾驭底线 4】:长度截断保护 (防 OOM)
|
||||
const maxLen = 8000
|
||||
if len(outputStr) > maxLen {
|
||||
return fmt.Sprintf("%s\n\n...[终端输出过长,已截断至前 %d 字节]...", outputStr[:maxLen], maxLen), nil
|
||||
}
|
||||
|
||||
return outputStr, nil
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"go-tiny-claw/internal/schema"
|
||||
)
|
||||
|
||||
type WriteFileTool struct {
|
||||
workDir string // 工作区约束
|
||||
}
|
||||
|
||||
func NewWriteFileTool(workDir string) *WriteFileTool {
|
||||
return &WriteFileTool{workDir: workDir}
|
||||
}
|
||||
|
||||
func (t *WriteFileTool) Name() string {
|
||||
return "write_file"
|
||||
}
|
||||
|
||||
func (t *WriteFileTool) Definition() schema.ToolDefinition {
|
||||
return schema.ToolDefinition{
|
||||
Name: t.Name(),
|
||||
Description: "创建或覆盖写入一个文件。如果目录不存在会自动创建。请提供相对于工作区的相对路径。",
|
||||
InputSchema: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"path": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "要写入的文件路径,如 src/main.go",
|
||||
},
|
||||
"content": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "要写入的完整文件内容",
|
||||
},
|
||||
},
|
||||
"required": []string{"path", "content"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type writeFileArgs struct {
|
||||
Path string `json:"path"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
func (t *WriteFileTool) Execute(ctx context.Context, args json.RawMessage) (string, error) {
|
||||
var input writeFileArgs
|
||||
if err := json.Unmarshal(args, &input); err != nil {
|
||||
return "", fmt.Errorf("参数解析失败: %w", err)
|
||||
}
|
||||
|
||||
// 【安全防线】:限制在 WorkDir 下执行,防止大模型修改系统级文件
|
||||
fullPath := filepath.Join(t.workDir, input.Path)
|
||||
|
||||
// 自动创建缺失的父级目录
|
||||
if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil {
|
||||
return "", fmt.Errorf("创建父目录失败: %w", err)
|
||||
}
|
||||
|
||||
// 写入文件内容,权限设为 0644
|
||||
err := os.WriteFile(fullPath, []byte(input.Content), 0644)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("写入文件失败: %w", err)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("成功将内容写入到文件: %s", input.Path), nil
|
||||
}
|
||||
Reference in New Issue
Block a user