From 9b4d0cb50da9752f5324557a0a9b89f7aa57b11c Mon Sep 17 00:00:00 2001 From: root Date: Wed, 22 Jul 2026 17:32:45 +0800 Subject: [PATCH] feat(clawhubadapter): rename plugin tools, universal uninstall; feat(mcp): add dynamic server management; feat(prompt): long-task guidance --- internal/agent/core/tooldefs.go | 3 +- internal/plugins/clawhubadapter/plugin.go | 75 +++++++--- internal/plugins/mcp/plugin.go | 158 ++++++++++++++++++++++ 3 files changed, 217 insertions(+), 19 deletions(-) diff --git a/internal/agent/core/tooldefs.go b/internal/agent/core/tooldefs.go index 764f6e5..e0fbdf2 100644 --- a/internal/agent/core/tooldefs.go +++ b/internal/agent/core/tooldefs.go @@ -49,7 +49,8 @@ func (a *Agent) buildSystemPrompt(memContext string, userInput string) string { prompt += "- payload 参数是消息载荷(文本直接填文字),type 指定载荷类型(text/voice/image/file),meta 是 JSON 发送元数据(群号/用户号等)。\n" prompt += "- 用 output_send__{通道名}_help 查看该通道的 meta 格式和 type 枚举。\n" prompt += "- 同一轮对话中可多次调用输出门工具。长消息应当分多次发出,而不是一口气发完。\n" - prompt += "- 直接返回纯文本不会到达任何用户端。" + prompt += "- 直接返回纯文本不会到达任何用户端。\n" + prompt += "- 需要多步执行的长任务:**必须先**用 output_send__ 发一条确认消息告诉用户已收到(如「好的我去看看~」),**然后再**执行具体排查工具。确认消息不代表任务完成,发出后仍需继续执行实际工具并最终汇报结果。" if a.skills != nil { if sp := a.skills.GetInjectedPrompt(); sp != "" { diff --git a/internal/plugins/clawhubadapter/plugin.go b/internal/plugins/clawhubadapter/plugin.go index ed9a548..418396c 100644 --- a/internal/plugins/clawhubadapter/plugin.go +++ b/internal/plugins/clawhubadapter/plugin.go @@ -157,25 +157,25 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error { // Register plugin management tools that talk to the manager (always, even if skills dir is empty) tp := p.name + "_" - s.RegisterTool(tp+"npm_install", sdk.ToolDef{ - Name: tp + "npm_install", - Description: "安装 ClawHub 适配器插件管理器中的插件。支持 npm: 前缀(npm 包)和 clawhub: 前缀(ClawHub 市场)。安装后立即可用。", + s.RegisterTool(tp+"plugin_install", sdk.ToolDef{ + Name: tp + "plugin_install", + Description: "安装插件。支持 npm: 前缀(npm 包)、clawhub: 前缀(ClawHub 市场,如 clawhub:openclaw-codex-app-server)。安装后立即可用。", Parameters: map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ - "package": map[string]interface{}{"type": "string", "description": "插件包标识。npm: 从 npm 安装,clawhub: 从 ClawHub 市场安装(如 clawhub:openclaw-codex-app-server)"}, + "package": map[string]interface{}{"type": "string", "description": "插件包标识。npm: 从 npm 安装,clawhub: 从 ClawHub 市场安装"}, }, "required": []string{"package"}, }, }, p.handlePluginInstall) - s.RegisterTool(tp+"npm_uninstall", sdk.ToolDef{ - Name: tp + "npm_uninstall", - Description: "从插件管理器中移除已安装的插件。", + s.RegisterTool(tp+"plugin_uninstall", sdk.ToolDef{ + Name: tp + "plugin_uninstall", + Description: "卸载已安装的插件,支持所有类型(sidecar、skill、manager 插件、ClawHub 安装的插件)。会停止进程、删除目录并清理注册。", Parameters: map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ - "name": map[string]interface{}{"type": "string", "description": "要移除的插件名称"}, + "name": map[string]interface{}{"type": "string", "description": "要卸载的插件名称(目录名,如 my-plugin)"}, }, "required": []string{"name"}, }, @@ -377,7 +377,7 @@ func (p *Plugin) handleClawHubSearch(args map[string]interface{}) (interface{}, if name == "" { name = pkg.Slug } - lines = append(lines, fmt.Sprintf("- %s (%s) v%s | ⬇ %d\n %s\n 安装: clawhubadapter_npm_install package=clawhub:%s", + lines = append(lines, fmt.Sprintf("- %s (%s) v%s | ⬇ %d\n %s\n 安装: clawhubadapter_plugin_install package=clawhub:%s", name, pkg.Slug, ver, pkg.Downloads, pkg.Summary, pkg.Slug)) } @@ -392,23 +392,62 @@ func (p *Plugin) handlePluginUninstall(args map[string]interface{}) (interface{} return errorResult("name is required"), nil } + var logs []string + pluginDir := filepath.Join(p.skillsDir, name) + + // 1. Stop & remove sidecar process if running p.mu.Lock() - mgr := p.manager + var aliveSidecars []*sidecarProcess + for _, sp := range p.sidecars { + if sp.name == name { + sp.Close() + logs = append(logs, fmt.Sprintf("已停止 sidecar 进程: %s", name)) + } else { + aliveSidecars = append(aliveSidecars, sp) + } + } + p.sidecars = aliveSidecars + + // 2. Remove from SKILL list if present + var aliveSkills []*plugin.SKILLPlugin + for _, sk := range p.skills { + if sk.Name() != name { + aliveSkills = append(aliveSkills, sk) + } else { + logs = append(logs, fmt.Sprintf("已移除 SKILL 插件: %s v%s", sk.Name(), sk.Version())) + } + } + p.skills = aliveSkills p.mu.Unlock() - if mgr == nil { - return errorResult("plugin manager not available"), nil + // 3. Try manager for OC plugins + mgr := p.manager + if mgr != nil { + data, err := mgr.call("plugins/uninstall", map[string]interface{}{ + "name": name, + }) + if err == nil { + logs = append(logs, fmt.Sprintf("管理器已卸载: %s", string(data))) + } } - data, err := mgr.call("plugins/uninstall", map[string]interface{}{ - "name": name, - }) - if err != nil { - return errorResult(fmt.Sprintf("uninstall failed: %v", err)), nil + // 4. Delete directory from disk + if _, statErr := os.Stat(pluginDir); statErr == nil { + if err := os.RemoveAll(pluginDir); err != nil { + logs = append(logs, fmt.Sprintf("删除目录失败: %v", err)) + } else { + logs = append(logs, fmt.Sprintf("已删除目录: %s", pluginDir)) + } } + if len(logs) == 0 { + return errorResult(fmt.Sprintf("未找到插件 '%s'", name)), nil + } + + result := fmt.Sprintf("已卸载插件: %s\n%s", name, strings.Join(logs, "\n")) + result += "\n提示: 部分工具注册信息将在下次重启后完全清理。如需立即生效,请使用 reload 命令。" return map[string]interface{}{ - "content": fmt.Sprintf("已卸载插件: %s\n %s", name, string(data)), + "content": result, }, nil } diff --git a/internal/plugins/mcp/plugin.go b/internal/plugins/mcp/plugin.go index 348cf42..c984c82 100644 --- a/internal/plugins/mcp/plugin.go +++ b/internal/plugins/mcp/plugin.go @@ -79,6 +79,47 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error { log.Printf("[mcp] register restart tool: %v", err) } + // MCP 服务器动态管理工具 + addDef := sdk.ToolDef{ + Name: "mcp_add_server", + Description: "动态添加并连接一个新的 MCP 服务器。支持 stdio 模式(指定 command)和 SSE 模式(指定 url)。添加后该服务器的所有工具立即可用。", + Parameters: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "name": map[string]interface{}{"type": "string", "description": "服务器名称(唯一标识,如 my-service)"}, + "command": map[string]interface{}{"type": "string", "description": "stdio 模式:可执行文件路径(如 npx)"}, + "args": map[string]interface{}{"type": "string", "description": "命令行参数,JSON 字符串数组(如 [\"-y\", \"@modelcontextprotocol/server-everything\"])"}, + "url": map[string]interface{}{"type": "string", "description": "SSE 模式:服务器 URL(如 https://api.example.com/mcp)"}, + "env": map[string]interface{}{"type": "string", "description": "环境变量,JSON 字符串对象(如 {\"KEY\": \"value\"})"}, + }, + "required": []string{"name"}, + }, + } + s.RegisterTool("mcp_add_server", addDef, p.addServerHandler) + + removeDef := sdk.ToolDef{ + Name: "mcp_remove_server", + Description: "断开并移除一个已连接的 MCP 服务器。会关闭连接并清理注册的工具(部分清理在重启后完全生效)。", + Parameters: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "name": map[string]interface{}{"type": "string", "description": "要移除的 MCP 服务器名称"}, + }, + "required": []string{"name"}, + }, + } + s.RegisterTool("mcp_remove_server", removeDef, p.removeServerHandler) + + listDef := sdk.ToolDef{ + Name: "mcp_list_servers", + Description: "列出所有已连接的 MCP 服务器及其工具。", + Parameters: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{}, + }, + } + s.RegisterTool("mcp_list_servers", listDef, p.listServersHandler) + return nil } @@ -253,6 +294,123 @@ func (p *Plugin) restartServerHandler(args map[string]interface{}) (interface{}, return fmt.Sprintf("MCP 服务器 [%s] 已成功重启", name), nil } +func (p *Plugin) addServerHandler(args map[string]interface{}) (interface{}, error) { + name, _ := args["name"].(string) + if name == "" { + return "参数 name 不能为空", nil + } + + cfg := serverConfig{Name: name} + if v, _ := args["command"].(string); v != "" { + cfg.Command = v + } + if v, _ := args["url"].(string); v != "" { + cfg.URL = v + } + if v, _ := args["args"].(string); v != "" { + json.Unmarshal([]byte(v), &cfg.Args) + } + if v, _ := args["env"].(string); v != "" { + // 支持 JSON 对象和 JSON 字符串数组两种格式 + var envObj map[string]string + if err := json.Unmarshal([]byte(v), &envObj); err == nil { + for k, val := range envObj { + cfg.Env = append(cfg.Env, k+"="+val) + } + } else { + json.Unmarshal([]byte(v), &cfg.Env) + } + } + + if cfg.Command == "" && cfg.URL == "" { + return "必须指定 command(stdio 模式)或 url(SSE 模式)", nil + } + + // 检查是否已存在同名服务器 + p.mu.Lock() + for _, s := range p.servers { + if s.Name() == name { + p.mu.Unlock() + return fmt.Sprintf("MCP 服务器 [%s] 已存在。如要重启请使用 mcp_restart_server,如要替换请先 mcp_remove_server", name), nil + } + } + p.mu.Unlock() + + if err := p.connectAndRegister(cfg); err != nil { + return fmt.Sprintf("连接 MCP 服务器 [%s] 失败: %v", name, err), nil + } + + p.mu.Lock() + p.configs = append(p.configs, cfg) + p.mu.Unlock() + + return fmt.Sprintf("MCP 服务器 [%s] 已成功连接并注册所有工具", name), nil +} + +func (p *Plugin) removeServerHandler(args map[string]interface{}) (interface{}, error) { + name, _ := args["name"].(string) + if name == "" { + return "参数 name 不能为空", nil + } + + p.mu.Lock() + var keptServers []*Server + var removed bool + for _, s := range p.servers { + if s.Name() == name { + s.Close() + removed = true + } else { + keptServers = append(keptServers, s) + } + } + p.servers = keptServers + + var keptConfigs []serverConfig + for _, c := range p.configs { + if c.Name != name { + keptConfigs = append(keptConfigs, c) + } + } + p.configs = keptConfigs + p.mu.Unlock() + + if !removed { + return fmt.Sprintf("MCP 服务器 [%s] 不存在", name), nil + } + + return fmt.Sprintf("MCP 服务器 [%s] 已断开连接。工具注册信息将在重启后完全清理。", name), nil +} + +func (p *Plugin) listServersHandler(args map[string]interface{}) (interface{}, error) { + p.mu.Lock() + defer p.mu.Unlock() + + if len(p.servers) == 0 { + return "没有已连接的 MCP 服务器。", nil + } + + var lines []string + lines = append(lines, fmt.Sprintf("已连接的 MCP 服务器 (%d):", len(p.servers))) + for _, s := range p.servers { + tools, err := s.ListTools() + var toolInfo string + if err == nil && len(tools) > 0 { + var names []string + for _, t := range tools { + names = append(names, t.Name) + } + toolInfo = strings.Join(names, ", ") + } else if err != nil { + toolInfo = fmt.Sprintf("(查询工具失败: %v)", err) + } else { + toolInfo = "(无工具)" + } + lines = append(lines, fmt.Sprintf(" %s: %s", s.Name(), toolInfo)) + } + return strings.Join(lines, "\n"), nil +} + func newTransport(cfg serverConfig) (Transport, error) { if cfg.URL != "" { return NewSSETransport(cfg.URL), nil