From 2114e1a53fcd89e7beecfb27a1f9c4efee8a109b Mon Sep 17 00:00:00 2001 From: SiYue-ZO <2835601846@qq.com> Date: Sun, 26 Apr 2026 12:44:05 +0800 Subject: [PATCH] feat(config): wire serial tool into runtime and dashboard --- config/config.example.json | 3 ++ pkg/agent/agent_init.go | 3 ++ pkg/config/config.go | 3 ++ pkg/config/defaults.go | 3 ++ web/backend/api/tools.go | 22 +++++++++++ web/backend/api/tools_test.go | 57 +++++++++++++++++++++++++++ web/frontend/src/i18n/locales/en.json | 1 + web/frontend/src/i18n/locales/zh.json | 1 + 8 files changed, 93 insertions(+) diff --git a/config/config.example.json b/config/config.example.json index 30460c231..4205b8e8a 100644 --- a/config/config.example.json +++ b/config/config.example.json @@ -437,6 +437,9 @@ "enabled": true, "mode": "bytes" }, + "serial": { + "enabled": false + }, "send_tts": { "enabled": false }, diff --git a/pkg/agent/agent_init.go b/pkg/agent/agent_init.go index 611d634e8..335fd8537 100644 --- a/pkg/agent/agent_init.go +++ b/pkg/agent/agent_init.go @@ -128,6 +128,9 @@ func registerSharedTools( if cfg.Tools.IsToolEnabled("spi") { agent.Tools.Register(tools.NewSPITool()) } + if cfg.Tools.IsToolEnabled("serial") { + agent.Tools.Register(tools.NewSerialTool()) + } // Message tool if cfg.Tools.IsToolEnabled("message") { diff --git a/pkg/config/config.go b/pkg/config/config.go index 16497b4ac..dc9e88949 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -823,6 +823,7 @@ type ToolsConfig struct { ListDir ToolConfig `json:"list_dir" yaml:"-" envPrefix:"PICOCLAW_TOOLS_LIST_DIR_"` Message ToolConfig `json:"message" yaml:"-" envPrefix:"PICOCLAW_TOOLS_MESSAGE_"` ReadFile ReadFileToolConfig `json:"read_file" yaml:"-" envPrefix:"PICOCLAW_TOOLS_READ_FILE_"` + Serial ToolConfig `json:"serial" yaml:"-" envPrefix:"PICOCLAW_TOOLS_SERIAL_"` SendFile ToolConfig `json:"send_file" yaml:"-" envPrefix:"PICOCLAW_TOOLS_SEND_FILE_"` SendTTS ToolConfig `json:"send_tts" yaml:"-" envPrefix:"PICOCLAW_TOOLS_SEND_TTS_"` Spawn ToolConfig `json:"spawn" yaml:"-" envPrefix:"PICOCLAW_TOOLS_SPAWN_"` @@ -1548,6 +1549,8 @@ func (t *ToolsConfig) IsToolEnabled(name string) bool { return t.Message.Enabled case "read_file": return t.ReadFile.Enabled + case "serial": + return t.Serial.Enabled case "spawn": return t.Spawn.Enabled case "spawn_status": diff --git a/pkg/config/defaults.go b/pkg/config/defaults.go index f3aaca7ab..be8c32495 100644 --- a/pkg/config/defaults.go +++ b/pkg/config/defaults.go @@ -435,6 +435,9 @@ func DefaultConfig() *Config { Mode: ReadFileModeBytes, MaxReadFileSize: 64 * 1024, // 64KB }, + Serial: ToolConfig{ + Enabled: false, // Hardware tool - requires host serial ports + }, Spawn: ToolConfig{ Enabled: true, }, diff --git a/web/backend/api/tools.go b/web/backend/api/tools.go index c6c2deaae..3476e3c53 100644 --- a/web/backend/api/tools.go +++ b/web/backend/api/tools.go @@ -171,6 +171,12 @@ var toolCatalog = []toolCatalogEntry{ Category: "hardware", ConfigKey: "spi", }, + { + Name: "serial", + Description: "Interact with serial ports exposed on the host.", + Category: "hardware", + ConfigKey: "serial", + }, { Name: "tool_search_tool_regex", Description: "Discover hidden MCP tools by regex search when tool discovery is enabled.", @@ -265,6 +271,8 @@ func buildToolSupport(cfg *config.Config) []toolSupportItem { status, reasonCode = resolveWebSearchToolSupport(cfg) case "i2c", "spi": status, reasonCode = resolveHardwareToolSupport(cfg.Tools.IsToolEnabled(entry.ConfigKey)) + case "serial": + status, reasonCode = resolveSerialToolSupport(cfg.Tools.IsToolEnabled(entry.ConfigKey)) default: if cfg.Tools.IsToolEnabled(entry.ConfigKey) { status = "enabled" @@ -293,6 +301,18 @@ func resolveHardwareToolSupport(enabled bool) (string, string) { return "enabled", "" } +func resolveSerialToolSupport(enabled bool) (string, string) { + if !enabled { + return "disabled", "" + } + switch runtime.GOOS { + case "linux", "darwin", "windows": + return "enabled", "" + default: + return "blocked", "requires_serial_platform" + } +} + func resolveDiscoveryToolSupport(cfg *config.Config, methodEnabled bool) (string, string) { if !cfg.Tools.IsToolEnabled("mcp") { return "disabled", "" @@ -362,6 +382,8 @@ func applyToolState(cfg *config.Config, toolName string, enabled bool) error { cfg.Tools.I2C.Enabled = enabled case "spi": cfg.Tools.SPI.Enabled = enabled + case "serial": + cfg.Tools.Serial.Enabled = enabled case "tool_search_tool_regex": cfg.Tools.MCP.Discovery.UseRegex = enabled if enabled { diff --git a/web/backend/api/tools_test.go b/web/backend/api/tools_test.go index c98067e41..a09a49fd6 100644 --- a/web/backend/api/tools_test.go +++ b/web/backend/api/tools_test.go @@ -92,9 +92,36 @@ func TestHandleListTools(t *testing.T) { if gotTools["i2c"].Status != "disabled" { t.Fatalf("i2c status = %q, want disabled on linux when config is off", gotTools["i2c"].Status) } + if gotTools["serial"].Status != "disabled" { + t.Fatalf("serial status = %q, want disabled when config is off", gotTools["serial"].Status) + } + + cfg.Tools.Serial.Enabled = true + if err := config.SaveConfig(configPath, cfg); err != nil { + t.Fatalf("SaveConfig() error = %v", err) + } + + rec = httptest.NewRecorder() + req = httptest.NewRequest(http.MethodGet, "/api/tools", nil) + mux.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String()) + } + + if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { + t.Fatalf("Unmarshal() error = %v", err) + } + gotTools = make(map[string]toolSupportItem, len(resp.Tools)) + for _, tool := range resp.Tools { + gotTools[tool.Name] = tool + } + if gotTools["serial"].Status != "enabled" { + t.Fatalf("serial = %#v, want enabled on linux when config is on", gotTools["serial"]) + } } else { cfg.Tools.I2C.Enabled = true cfg.Tools.SPI.Enabled = true + cfg.Tools.Serial.Enabled = true if err := config.SaveConfig(configPath, cfg); err != nil { t.Fatalf("SaveConfig() error = %v", err) } @@ -120,6 +147,16 @@ func TestHandleListTools(t *testing.T) { if gotTools["spi"].Status != "blocked" || gotTools["spi"].ReasonCode != "requires_linux" { t.Fatalf("spi = %#v, want blocked/requires_linux", gotTools["spi"]) } + switch runtime.GOOS { + case "darwin", "windows": + if gotTools["serial"].Status != "enabled" { + t.Fatalf("serial = %#v, want enabled on supported host", gotTools["serial"]) + } + default: + if gotTools["serial"].Status != "blocked" || gotTools["serial"].ReasonCode != "requires_serial_platform" { + t.Fatalf("serial = %#v, want blocked/requires_serial_platform", gotTools["serial"]) + } + } } } @@ -195,6 +232,26 @@ func TestHandleUpdateToolState(t *testing.T) { if !updated.Tools.Cron.Enabled { t.Fatalf("cron should be enabled: %#v", updated.Tools.Cron) } + + rec4 := httptest.NewRecorder() + req4 := httptest.NewRequest( + http.MethodPut, + "/api/tools/serial/state", + bytes.NewBufferString(`{"enabled":true}`), + ) + req4.Header.Set("Content-Type", "application/json") + mux.ServeHTTP(rec4, req4) + if rec4.Code != http.StatusOK { + t.Fatalf("serial status = %d, want %d, body=%s", rec4.Code, http.StatusOK, rec4.Body.String()) + } + + updated, err = config.LoadConfig(configPath) + if err != nil { + t.Fatalf("LoadConfig(updated serial) error = %v", err) + } + if !updated.Tools.Serial.Enabled { + t.Fatalf("serial should be enabled: %#v", updated.Tools.Serial) + } } func TestHandleListTools_ReportsWebSearchEnabledWhenToolIsOn(t *testing.T) { diff --git a/web/frontend/src/i18n/locales/en.json b/web/frontend/src/i18n/locales/en.json index a30dc0471..50b3ad089 100644 --- a/web/frontend/src/i18n/locales/en.json +++ b/web/frontend/src/i18n/locales/en.json @@ -593,6 +593,7 @@ }, "reasons": { "requires_linux": "This tool only works on Linux hosts with the required device files exposed.", + "requires_serial_platform": "This tool currently supports Linux, macOS, and Windows hosts with accessible serial ports.", "requires_skills": "Enable `tools.skills` before this skill-registry tool can be used.", "requires_subagent": "Enable `tools.subagent` before the spawn tool can delegate work.", "requires_mcp_discovery": "Enable `tools.mcp.discovery` before MCP discovery tools become available.", diff --git a/web/frontend/src/i18n/locales/zh.json b/web/frontend/src/i18n/locales/zh.json index efe3dc32a..47f0eda21 100644 --- a/web/frontend/src/i18n/locales/zh.json +++ b/web/frontend/src/i18n/locales/zh.json @@ -593,6 +593,7 @@ }, "reasons": { "requires_linux": "该工具仅在 Linux 主机上可用,并且需要暴露对应的设备文件。", + "requires_serial_platform": "该工具当前支持 Linux、macOS 和 Windows,且要求主机可访问对应串口。", "requires_skills": "需要先启用 `tools.skills`,该技能注册表工具才能使用。", "requires_subagent": "需要先启用 `tools.subagent`,`spawn` 才能委派任务。", "requires_mcp_discovery": "需要先启用 `tools.mcp.discovery`,MCP 发现工具才会可用。",