From 21be6e63af527da79ac272773292efbc673773ba Mon Sep 17 00:00:00 2001 From: kevin Date: Fri, 14 Aug 2026 17:35:54 +0800 Subject: [PATCH] =?UTF-8?q?=E5=86=85=E7=BD=AE=E5=B7=A5=E5=85=B7=E6=8E=A5?= =?UTF-8?q?=E5=85=A5=E7=8B=AC=E7=AB=8B=E9=85=8D=E7=BD=AE=EF=BC=9Aenabled?= =?UTF-8?q?=20=E5=BC=80=E5=85=B3=E4=B8=8E=20prompt=20=E6=8F=90=E7=A4=BA?= =?UTF-8?q?=E8=AF=8D=E8=A6=86=E7=9B=96=E6=8F=8F=E8=BF=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/bot/bot.go | 2 +- internal/tools/builtin/calculator_tool.go | 38 ++++++++++++++--- internal/tools/builtin/random_tool.go | 38 ++++++++++++++--- internal/tools/builtin/time_tool.go | 51 +++++++++++++++++++--- internal/tools/builtin_test.go | 52 ++++++++++++++++++++--- internal/tools/initconfigs_test.go | 31 +++++++++++++- internal/tools/tools.go | 8 ++++ 7 files changed, 191 insertions(+), 29 deletions(-) diff --git a/internal/bot/bot.go b/internal/bot/bot.go index b03c8c0..b597d49 100644 --- a/internal/bot/bot.go +++ b/internal/bot/bot.go @@ -41,7 +41,7 @@ func New(cfg *config.Config) (*Bot, error) { clients: make(map[string]*openai.Client), cfg: cfg, systemPrompt: cfg.SystemPrompt, - toolRegistry: tools.NewRegistry(builtin.TimeTool{}, builtin.CalculatorTool{}, builtin.RandomTool{}), + toolRegistry: tools.NewRegistry(builtin.NewTimeTool(), builtin.NewCalculatorTool(), builtin.NewRandomTool()), } b.provider = config.FindProvider(cfg.DefaultProvider) b.model = cfg.DefaultModel diff --git a/internal/tools/builtin/calculator_tool.go b/internal/tools/builtin/calculator_tool.go index 9c8e668..4cc2dc2 100644 --- a/internal/tools/builtin/calculator_tool.go +++ b/internal/tools/builtin/calculator_tool.go @@ -9,13 +9,37 @@ import ( "github.com/expr-lang/expr" ) -type CalculatorTool struct{} - -func (CalculatorTool) Name() string { return "calculate" } -func (CalculatorTool) Description() string { - return "计算数学表达式,如 \"(12 + 3) * 4\"、\"2^10\"、\"sqrt(9)\"" +type calculatorTool struct { + enabled bool + prompt string } -func (CalculatorTool) Parameters() map[string]any { + +func NewCalculatorTool() *calculatorTool { + return &calculatorTool{ + enabled: true, + prompt: "计算数学表达式,如 \"(12 + 3) * 4\"、\"2^10\"、\"sqrt(9)\"", + } +} + +func (t *calculatorTool) Name() string { return "calculate" } +func (t *calculatorTool) Description() string { return t.prompt } +func (t *calculatorTool) Enabled() bool { return t.enabled } +func (t *calculatorTool) DefaultConfig() map[string]any { + return map[string]any{"enabled": true, "prompt": t.prompt} +} + +func (t *calculatorTool) Configure(cfg map[string]any) error { + var err error + if t.enabled, err = parseEnabled(cfg); err != nil { + return err + } + if p, ok := cfg["prompt"].(string); ok && p != "" { + t.prompt = p + } + return nil +} + +func (t *calculatorTool) Parameters() map[string]any { return map[string]any{ "type": "object", "properties": map[string]any{ @@ -25,7 +49,7 @@ func (CalculatorTool) Parameters() map[string]any { } } -func (CalculatorTool) Execute(args json.RawMessage) (string, error) { +func (t *calculatorTool) Execute(args json.RawMessage) (string, error) { var p struct { Expression string `json:"expression"` } diff --git a/internal/tools/builtin/random_tool.go b/internal/tools/builtin/random_tool.go index 4294ed6..0bd1c17 100644 --- a/internal/tools/builtin/random_tool.go +++ b/internal/tools/builtin/random_tool.go @@ -6,13 +6,37 @@ import ( "math/rand/v2" ) -type RandomTool struct{} - -func (RandomTool) Name() string { return "random_number" } -func (RandomTool) Description() string { - return "生成指定范围内的随机整数,默认 0 到 100(含端点)" +type randomTool struct { + enabled bool + prompt string } -func (RandomTool) Parameters() map[string]any { + +func NewRandomTool() *randomTool { + return &randomTool{ + enabled: true, + prompt: "生成指定范围内的随机整数,默认 0 到 100(含端点)", + } +} + +func (t *randomTool) Name() string { return "random_number" } +func (t *randomTool) Description() string { return t.prompt } +func (t *randomTool) Enabled() bool { return t.enabled } +func (t *randomTool) DefaultConfig() map[string]any { + return map[string]any{"enabled": true, "prompt": t.prompt} +} + +func (t *randomTool) Configure(cfg map[string]any) error { + var err error + if t.enabled, err = parseEnabled(cfg); err != nil { + return err + } + if p, ok := cfg["prompt"].(string); ok && p != "" { + t.prompt = p + } + return nil +} + +func (t *randomTool) Parameters() map[string]any { return map[string]any{ "type": "object", "properties": map[string]any{ @@ -22,7 +46,7 @@ func (RandomTool) Parameters() map[string]any { } } -func (RandomTool) Execute(args json.RawMessage) (string, error) { +func (t *randomTool) Execute(args json.RawMessage) (string, error) { var p struct { Min *int `json:"min"` Max *int `json:"max"` diff --git a/internal/tools/builtin/time_tool.go b/internal/tools/builtin/time_tool.go index 44177af..de77554 100644 --- a/internal/tools/builtin/time_tool.go +++ b/internal/tools/builtin/time_tool.go @@ -2,16 +2,41 @@ package builtin import ( "encoding/json" + "fmt" "time" ) -type TimeTool struct{} - -func (TimeTool) Name() string { return "get_current_time" } -func (TimeTool) Description() string { - return "获取当前日期和时间,可选指定时区(如 Asia/Shanghai,默认本地时区)" +type timeTool struct { + enabled bool + prompt string } -func (TimeTool) Parameters() map[string]any { + +func NewTimeTool() *timeTool { + return &timeTool{ + enabled: true, + prompt: "获取当前日期和时间,可选指定时区(如 Asia/Shanghai,默认本地时区)", + } +} + +func (t *timeTool) Name() string { return "get_current_time" } +func (t *timeTool) Description() string { return t.prompt } +func (t *timeTool) Enabled() bool { return t.enabled } +func (t *timeTool) DefaultConfig() map[string]any { + return map[string]any{"enabled": true, "prompt": t.prompt} +} + +func (t *timeTool) Configure(cfg map[string]any) error { + var err error + if t.enabled, err = parseEnabled(cfg); err != nil { + return err + } + if p, ok := cfg["prompt"].(string); ok && p != "" { + t.prompt = p + } + return nil +} + +func (t *timeTool) Parameters() map[string]any { return map[string]any{ "type": "object", "properties": map[string]any{ @@ -20,7 +45,7 @@ func (TimeTool) Parameters() map[string]any { } } -func (TimeTool) Execute(args json.RawMessage) (string, error) { +func (t *timeTool) Execute(args json.RawMessage) (string, error) { var p struct { Timezone string `json:"timezone"` } @@ -36,3 +61,15 @@ func (TimeTool) Execute(args json.RawMessage) (string, error) { now := time.Now().In(loc) return now.Format("2006-01-02 15:04:05 Monday MST"), nil } + +func parseEnabled(cfg map[string]any) (bool, error) { + v, ok := cfg["enabled"] + if !ok { + return true, nil + } + b, ok := v.(bool) + if !ok { + return false, fmt.Errorf("enabled 必须是布尔值,实际为 %T", v) + } + return b, nil +} diff --git a/internal/tools/builtin_test.go b/internal/tools/builtin_test.go index 5e62f76..0836e52 100644 --- a/internal/tools/builtin_test.go +++ b/internal/tools/builtin_test.go @@ -23,7 +23,7 @@ func TestCalculator(t *testing.T) { for _, c := range cases { t.Run(c.name, func(t *testing.T) { args, _ := json.Marshal(map[string]string{"expression": c.expr}) - got, err := builtin.CalculatorTool{}.Execute(args) + got, err := builtin.NewCalculatorTool().Execute(args) if err != nil { t.Fatalf("Execute 出错: %v", err) } @@ -36,7 +36,7 @@ func TestCalculator(t *testing.T) { func TestCalculatorInvalid(t *testing.T) { args, _ := json.Marshal(map[string]string{"expression": "1 +"}) - if _, err := (builtin.CalculatorTool{}).Execute(args); err == nil { + if _, err := builtin.NewCalculatorTool().Execute(args); err == nil { t.Error("非法表达式应返回错误") } } @@ -44,7 +44,7 @@ func TestCalculatorInvalid(t *testing.T) { func TestRandom(t *testing.T) { args, _ := json.Marshal(map[string]any{"min": 1, "max": 10}) for i := 0; i < 100; i++ { - out, err := builtin.RandomTool{}.Execute(args) + out, err := builtin.NewRandomTool().Execute(args) if err != nil { t.Fatalf("Execute 出错: %v", err) } @@ -57,13 +57,13 @@ func TestRandom(t *testing.T) { } } bad, _ := json.Marshal(map[string]any{"min": 10, "max": 1}) - if _, err := (builtin.RandomTool{}).Execute(bad); err == nil { + if _, err := builtin.NewRandomTool().Execute(bad); err == nil { t.Error("min>max 应返回错误") } } func TestTimeTool(t *testing.T) { - out, err := builtin.TimeTool{}.Execute(nil) + out, err := builtin.NewTimeTool().Execute(nil) if err != nil { t.Fatalf("Execute 出错: %v", err) } @@ -71,3 +71,45 @@ func TestTimeTool(t *testing.T) { t.Errorf("时间输出异常: %q", out) } } + +func TestConfigurePrompt(t *testing.T) { + tool := builtin.NewTimeTool() + err := tool.Configure(map[string]any{"enabled": true, "prompt": "自定义提示词"}) + if err != nil { + t.Fatalf("Configure 出错: %v", err) + } + if tool.Description() != "自定义提示词" { + t.Errorf("Description = %q, want 自定义提示词", tool.Description()) + } + if !tool.Enabled() { + t.Error("enabled 应为 true") + } +} + +func TestConfigureDisable(t *testing.T) { + tool := builtin.NewCalculatorTool() + if err := tool.Configure(map[string]any{"enabled": false}); err != nil { + t.Fatalf("Configure 出错: %v", err) + } + if tool.Enabled() { + t.Error("enabled 应为 false") + } +} + +func TestConfigureInvalid(t *testing.T) { + tool := builtin.NewRandomTool() + if err := tool.Configure(map[string]any{"enabled": "yes"}); err == nil { + t.Error("enabled 非布尔值应报错") + } +} + +func TestDefaultConfig(t *testing.T) { + tool := builtin.NewTimeTool() + cfg := tool.DefaultConfig() + if cfg["enabled"] != true { + t.Errorf("默认 enabled 应为 true, got %v", cfg["enabled"]) + } + if p, ok := cfg["prompt"].(string); !ok || p == "" { + t.Errorf("默认 prompt 缺失: %v", cfg["prompt"]) + } +} diff --git a/internal/tools/initconfigs_test.go b/internal/tools/initconfigs_test.go index 9be5c6e..11f9031 100644 --- a/internal/tools/initconfigs_test.go +++ b/internal/tools/initconfigs_test.go @@ -12,6 +12,7 @@ import ( type stubConfigurable struct { configured map[string]any fail bool + enabled bool } func (s *stubConfigurable) Name() string { return "db" } @@ -22,13 +23,17 @@ func (s *stubConfigurable) Parameters() map[string]any { func (s *stubConfigurable) Execute(args json.RawMessage) (string, error) { return "ok", nil } +func (s *stubConfigurable) Enabled() bool { return s.enabled } func (s *stubConfigurable) DefaultConfig() map[string]any { - return map[string]any{"host": "127.0.0.1", "password": "请填写"} + return map[string]any{"enabled": true, "password": "请填写"} } func (s *stubConfigurable) Configure(cfg map[string]any) error { if s.fail { return fmt.Errorf("密码为空") } + if v, ok := cfg["enabled"].(bool); ok { + s.enabled = v + } s.configured = cfg return nil } @@ -61,7 +66,7 @@ func TestInitConfigsOK(t *testing.T) { if err := os.WriteFile(path, []byte("host: localhost\npassword: secret\n"), 0o644); err != nil { t.Fatal(err) } - stub := &stubConfigurable{} + stub := &stubConfigurable{enabled: true} if err := NewRegistry(stub).InitConfigs(); err != nil { t.Fatalf("InitConfigs 出错: %v", err) } @@ -87,3 +92,25 @@ func TestInitConfigsSkipsPlain(t *testing.T) { t.Fatalf("普通工具不应报错: %v", err) } } + +func TestInitConfigsDisables(t *testing.T) { + t.Chdir(t.TempDir()) + if err := os.MkdirAll(filepath.Join("data", "tools"), 0o755); err != nil { + t.Fatal(err) + } + path := filepath.Join("data", "tools", "db.yaml") + if err := os.WriteFile(path, []byte("enabled: false\n"), 0o644); err != nil { + t.Fatal(err) + } + stub := &stubConfigurable{enabled: true} + r := NewRegistry(stub) + if err := r.InitConfigs(); err != nil { + t.Fatalf("InitConfigs 出错: %v", err) + } + if stub.Enabled() { + t.Fatal("工具应被禁用") + } + if _, ok := r.Get("db"); ok { + t.Error("禁用的工具应被移出注册表") + } +} diff --git a/internal/tools/tools.go b/internal/tools/tools.go index cc0c601..5977af1 100644 --- a/internal/tools/tools.go +++ b/internal/tools/tools.go @@ -27,6 +27,11 @@ type Configurable interface { Configure(cfg map[string]any) error } +// Enabler 是可开关工具的接口:Configure 后返回 false 的工具会被移出注册表。 +type Enabler interface { + Enabled() bool +} + type Registry struct { tools map[string]Tool } @@ -102,6 +107,9 @@ func (r *Registry) InitConfigs() error { if err := c.Configure(cfg); err != nil { return fmt.Errorf("工具 %s 配置无效: %w", name, err) } + if e, ok := t.(Enabler); ok && !e.Enabled() { + delete(r.tools, name) + } } if len(missing) > 0 { return fmt.Errorf("%s", strings.Join(missing, "\n"))