feat: 插件工具按需拉取(get_plugin_tools) + 设备命令类型拆分(shell-cmd/homeagent-cmd)

1. 提示词去污染: buildToolCatalog 从全量工具定义改为按插件分组摘要
   (插件名 + 工具数 + 能力概览), 完整工具定义由新工具 get_plugin_tools(plugin_name)
   动态拉取; 新增 executeGetPluginTools 支持按插件过滤 stageHost/io 工具
2. 设备命令类型: device_ctl_cmdrun 的 command 支持前缀区分
   - shell-cmd <cmd>      -> 设备端执行原生 shell
   - homeagent-<cap>      -> 设备端 HomeAgent 内置能力(如 camerasue/screensue)
   - homeagent-cmd <cap>  -> 同上(兼容写法)
   PushCmd 增加 cmd_type 字段下发给设备端分发
3. 测试: TestExecuteGetPluginTools 验证按插件拉取/全部摘要/未知插件
This commit is contained in:
JianFeeeee
2026-08-18 09:07:09 +08:00
parent 8172b776b9
commit 653dd34299
6 changed files with 169 additions and 25 deletions

View File

@ -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)
}
}

View File

@ -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()
}

View File

@ -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":

View File

@ -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{}{

View File

@ -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 留档。

View File

@ -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,
})
}