From 653dd3429968166e4c849a25352f9617bce4b1b6 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Tue, 18 Aug 2026 09:07:09 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8F=92=E4=BB=B6=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E6=8C=89=E9=9C=80=E6=8B=89=E5=8F=96(get=5Fplugin=5Ftools)=20+?= =?UTF-8?q?=20=E8=AE=BE=E5=A4=87=E5=91=BD=E4=BB=A4=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E6=8B=86=E5=88=86(shell-cmd/homeagent-cmd)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 提示词去污染: buildToolCatalog 从全量工具定义改为按插件分组摘要 (插件名 + 工具数 + 能力概览), 完整工具定义由新工具 get_plugin_tools(plugin_name) 动态拉取; 新增 executeGetPluginTools 支持按插件过滤 stageHost/io 工具 2. 设备命令类型: device_ctl_cmdrun 的 command 支持前缀区分 - shell-cmd -> 设备端执行原生 shell - homeagent- -> 设备端 HomeAgent 内置能力(如 camerasue/screensue) - homeagent-cmd -> 同上(兼容写法) PushCmd 增加 cmd_type 字段下发给设备端分发 3. 测试: TestExecuteGetPluginTools 验证按插件拉取/全部摘要/未知插件 --- internal/agent/core/gpttest_test.go | 33 ++++++++++++ internal/agent/core/plugins.go | 59 +++++++++++++++++++++ internal/agent/core/toolcall.go | 3 ++ internal/agent/core/tooldefs.go | 62 ++++++++++++++++------- internal/plugins/remotedevice/device.go | 25 +++++++-- internal/plugins/remotedevice/registry.go | 12 +++-- 6 files changed, 169 insertions(+), 25 deletions(-) create mode 100644 internal/agent/core/gpttest_test.go diff --git a/internal/agent/core/gpttest_test.go b/internal/agent/core/gpttest_test.go new file mode 100644 index 0000000..9e79de5 --- /dev/null +++ b/internal/agent/core/gpttest_test.go @@ -0,0 +1,33 @@ +package core + +import ( + "strings" + "testing" + + "gitcode.com/JianFeeeee/HomeAgent/internal/sdk" +) + +func TestExecuteGetPluginTools(tmp *testing.T) { + sh := NewStageHost() + sh.RegisterTool("weather_current", sdk.ToolDef{Name: "weather_current", Plugin: "weather", Description: "当前天气", Parameters: map[string]interface{}{"type": "object"}}, nil) + sh.RegisterTool("weather_forecast", sdk.ToolDef{Name: "weather_forecast", Plugin: "weather", Description: "天气预报"}, nil) + sh.RegisterTool("device_ctl_cmdrun", sdk.ToolDef{Name: "device_ctl_cmdrun", Plugin: "remotedevice", Description: "下发命令"}, nil) + + a := New(AgentConfig{ID: "t", StageHost: sh}) + + out := a.executeGetPluginTools("weather") + if !strings.Contains(out, "weather_current") || !strings.Contains(out, "weather_forecast") { + tmp.Fatalf("weather tools missing:\n%s", out) + } + if strings.Contains(out, "device_ctl_cmdrun") { + tmp.Fatalf("should not contain other plugin:\n%s", out) + } + all := a.executeGetPluginTools("") + if !strings.Contains(all, "weather_current") || !strings.Contains(all, "device_ctl_cmdrun") { + tmp.Fatalf("all tools missing:\n%s", all) + } + none := a.executeGetPluginTools("nope") + if !strings.Contains(none, "没有可用的工具") { + tmp.Fatalf("unknown plugin msg:\n%s", none) + } +} diff --git a/internal/agent/core/plugins.go b/internal/agent/core/plugins.go index ca030e5..fb7cee0 100644 --- a/internal/agent/core/plugins.go +++ b/internal/agent/core/plugins.go @@ -1,6 +1,7 @@ package core import ( + "encoding/json" "fmt" "log" "strings" @@ -50,3 +51,61 @@ func (a *Agent) resolveToolPlugin(name string) string { } return "core" } + +// executeGetPluginTools 返回指定插件的完整工具定义(名称/参数/用途)。 +// 支持按插件名拉取, 未指定时返回全部插件的工具摘要。 +func (a *Agent) executeGetPluginTools(pluginName string) string { + type toolItem struct { + name, desc string + params interface{} + } + var items []toolItem + + // 收集 StageHost(SDK 插件)工具 + if a.stageHost != nil { + for _, d := range a.stageHost.GetToolDefs() { + plg := d.Plugin + if plg == "" { + plg = a.resolveToolPlugin(d.Name) + } + if pluginName != "" && plg != pluginName { + continue + } + items = append(items, toolItem{d.Name, d.Description, d.Parameters}) + } + } + // 收集 IOManager(设备/通道)工具 + if a.io != nil { + for _, d := range a.io.GetAllTools() { + plg := a.resolveToolPlugin(d.Name) + if pluginName != "" && plg != pluginName { + continue + } + items = append(items, toolItem{d.Name, d.Description, d.Parameters}) + } + } + if len(items) == 0 { + if pluginName != "" { + return fmt.Sprintf("插件 %s 没有可用的工具定义", pluginName) + } + return "当前没有可用的工具定义" + } + var sb strings.Builder + if pluginName == "" { + sb.WriteString(fmt.Sprintf("共 %d 个工具:\n", len(items))) + } else { + sb.WriteString(fmt.Sprintf("插件 %s 共 %d 个工具:\n", pluginName, len(items))) + } + for _, it := range items { + sb.WriteString(fmt.Sprintf("\n### %s\n", it.name)) + if it.desc != "" { + sb.WriteString(it.desc + "\n") + } + if it.params != nil { + if b, err := json.Marshal(it.params); err == nil && len(b) < 600 { + sb.WriteString("参数: " + string(b) + "\n") + } + } + } + return sb.String() +} diff --git a/internal/agent/core/toolcall.go b/internal/agent/core/toolcall.go index f584969..3c26b8b 100644 --- a/internal/agent/core/toolcall.go +++ b/internal/agent/core/toolcall.go @@ -61,6 +61,9 @@ func (a *Agent) executeToolCallInner(tc agentAPI.ToolCall) string { return a.executeOutputListChannels() case tc.Name == "plgreload": return a.executePluginReload() + case tc.Name == "get_plugin_tools": + pluginName, _ := tc.Arguments["plugin_name"].(string) + return a.executeGetPluginTools(pluginName) case tc.Name == "spawn_child": return a.executeSpawnChild(tc) case tc.Name == "child_result": diff --git a/internal/agent/core/tooldefs.go b/internal/agent/core/tooldefs.go index 81800b0..83b9363 100644 --- a/internal/agent/core/tooldefs.go +++ b/internal/agent/core/tooldefs.go @@ -98,31 +98,42 @@ func (a *Agent) buildToolCatalog() string { if len(defs) == 0 { return "" } - var sb strings.Builder - sb.WriteString("\n\n【可用工具列表】") - seen := make(map[string]bool) - for _, d := range defs { - t, ok := d.(map[string]interface{}) - if !ok { - continue - } - fn, ok := t["function"].(map[string]interface{}) + // 仅注入插件/通道能力摘要,避免全量工具定义污染 system prompt。 + // 每个插件列:名称 + 能力描述 + 工具数。完整工具定义由 get_plugin_tools 按需拉取。 + byPlugin := map[string]int{} // plugin -> 工具数 + pluginDesc := map[string]string{} // plugin -> 首个工具描述(作能力概览) + var order []string + for _, t := range defs { + fn, ok := t.(map[string]interface{})["function"].(map[string]interface{}) if !ok { continue } name, _ := fn["name"].(string) - if name == "" || seen[name] { + if name == "" { continue } - seen[name] = true - desc, _ := fn["description"].(string) - sb.WriteString(fmt.Sprintf("\n- %s", name)) - if desc != "" { - if len(desc) > 80 { - desc = desc[:80] + "..." - } - sb.WriteString(": " + desc) + plg := a.resolveToolPlugin(name) + if _, seen := byPlugin[plg]; !seen { + order = append(order, plg) } + byPlugin[plg]++ + if pluginDesc[plg] == "" { + desc, _ := fn["description"].(string) + if len(desc) > 60 { + desc = desc[:60] + "..." + } + pluginDesc[plg] = desc + } + } + var sb strings.Builder + sb.WriteString("\n\n【可用工具能力】\n") + sb.WriteString("工具按插件分组注册。需要某个插件的具体工具时,调用 get_plugin_tools(\"{插件名}\") 获取该插件的完整工具定义(名称/参数/用途)。\n") + for _, plg := range order { + sb.WriteString(fmt.Sprintf("- %s (%d 个工具)", plg, byPlugin[plg])) + if d := pluginDesc[plg]; d != "" { + sb.WriteString(": " + d) + } + sb.WriteString("\n") } return sb.String() } @@ -422,6 +433,21 @@ func (a *Agent) buildToolDefs() []interface{} { }) } + // 按插件动态拉取工具定义(避免全量注入提示词污染) + tools = append(tools, map[string]interface{}{ + "type": "function", + "function": map[string]interface{}{ + "name": "get_plugin_tools", + "description": "获取指定插件的完整工具定义(名称/参数/用途)。参数 plugin_name 传插件名(见系统提示的【可用工具能力】列表)。省略时返回全部插件的工具摘要。", + "parameters": map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "plugin_name": map[string]interface{}{"type": "string", "description": "插件名,如 qq / remotedevice / weather", "default": ""}, + }, + }, + }, + }) + tools = append(tools, map[string]interface{}{ "type": "function", "function": map[string]interface{}{ diff --git a/internal/plugins/remotedevice/device.go b/internal/plugins/remotedevice/device.go index 94f5c43..c4c71d3 100644 --- a/internal/plugins/remotedevice/device.go +++ b/internal/plugins/remotedevice/device.go @@ -56,7 +56,11 @@ func (d *devicectlDevice) Tools() []agentIO.ToolDef { }, { Name: "device_ctl_cmdrun", - Description: "向一台已授权且在线设备发送命令执行请求(如开机/重启/播放/自定义命令)。" + + Description: "向设备下发命令/操作(异步,accepted=true 后用 device_ctl_cmdresult 轮询结果)。" + + "command 支持两类(前缀区分):\n" + + "- shell-cmd: 在设备上执行原生 shell 命令,如 shell-cmd ls -la /tmp\n" + + "- homeagent-cmd: 调用设备端 HomeAgent 内置能力,如 homeagent-camerasue(调用用户侧摄像头)、" + + "homeagent-screensue(用户侧屏幕显示内容)\n" + "⚡ 高危:设备必须已授权,且该操作会改变设备行为。" + "返回 accepted=true 表示已下发并等待设备执行,之后可用 device_ctl_cmdresult 查询结果。" + "若设备未授权或离线,返回错误信息。", @@ -64,7 +68,7 @@ func (d *devicectlDevice) Tools() []agentIO.ToolDef { "type": "object", "properties": map[string]interface{}{ "device_id": map[string]interface{}{"type": "string", "description": "目标设备 ID"}, - "command": map[string]interface{}{"type": "string", "description": "要执行的命令(设备自定义语义,如 power_on、play:xxx 或 shell 命令)"}, + "command": map[string]interface{}{"type": "string", "description": "以 shell-cmd 或 homeagent-cmd 前缀开头。如 shell-cmd pwd、homeagent-camerasue"}, }, "required": []interface{}{"device_id", "command"}, }, @@ -191,8 +195,23 @@ func (d *devicectlDevice) cmdrun(args map[string]interface{}) (interface{}, erro if !m.Online { return nil, fmt.Errorf("device %s 不在线,无法执行命令", id) } + // 命令类型:shell-cmd / homeagent-* 前缀区分;无前缀按 shell 处理(兼容旧格式) + cmdType := "shell" + switch { + case strings.HasPrefix(cmd, "shell-cmd"): + cmdType = "shell" + cmd = strings.TrimSpace(strings.TrimPrefix(cmd, "shell-cmd")) + case strings.HasPrefix(cmd, "homeagent-cmd"): + cmdType = "homeagent" + cmd = strings.TrimSpace(strings.TrimPrefix(cmd, "homeagent-cmd")) + case strings.HasPrefix(cmd, "homeagent-"): + cmdType = "homeagent" + cmd = strings.TrimSpace(strings.TrimPrefix(cmd, "homeagent-")) + default: + cmdType = "shell" + } reqID := newReqID() - if err := d.reg.PushCmd(id, reqID, cmd); err != nil { + if err := d.reg.PushCmd(id, reqID, cmd, cmdType); err != nil { return nil, fmt.Errorf("下发命令失败: %w", err) } // 阻塞等待设备结果(带超时);结果同时由 registry 留档。 diff --git a/internal/plugins/remotedevice/registry.go b/internal/plugins/remotedevice/registry.go index bdd91a7..a196364 100644 --- a/internal/plugins/remotedevice/registry.go +++ b/internal/plugins/remotedevice/registry.go @@ -248,11 +248,15 @@ func (r *Registry) PushJSON(deviceID string, payload map[string]interface{}) err } // PushCmd 向设备发送命令执行请求。 -func (r *Registry) PushCmd(deviceID, reqID, command string) error { +func (r *Registry) PushCmd(deviceID, reqID, command, cmdType string) error { + if cmdType == "" { + cmdType = "shell" + } return r.PushJSON(deviceID, map[string]interface{}{ - "op": "cmd", - "req_id": reqID, - "command": command, + "op": "cmd", + "req_id": reqID, + "command": command, + "cmd_type": cmdType, }) }