mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-22 01:48:11 +00:00
feat(clawhubadapter): rename plugin tools, universal uninstall; feat(mcp): add dynamic server management; feat(prompt): long-task guidance
This commit is contained in:
@ -49,7 +49,8 @@ func (a *Agent) buildSystemPrompt(memContext string, userInput string) string {
|
|||||||
prompt += "- payload 参数是消息载荷(文本直接填文字),type 指定载荷类型(text/voice/image/file),meta 是 JSON 发送元数据(群号/用户号等)。\n"
|
prompt += "- payload 参数是消息载荷(文本直接填文字),type 指定载荷类型(text/voice/image/file),meta 是 JSON 发送元数据(群号/用户号等)。\n"
|
||||||
prompt += "- 用 output_send__{通道名}_help 查看该通道的 meta 格式和 type 枚举。\n"
|
prompt += "- 用 output_send__{通道名}_help 查看该通道的 meta 格式和 type 枚举。\n"
|
||||||
prompt += "- 同一轮对话中可多次调用输出门工具。长消息应当分多次发出,而不是一口气发完。\n"
|
prompt += "- 同一轮对话中可多次调用输出门工具。长消息应当分多次发出,而不是一口气发完。\n"
|
||||||
prompt += "- 直接返回纯文本不会到达任何用户端。"
|
prompt += "- 直接返回纯文本不会到达任何用户端。\n"
|
||||||
|
prompt += "- 需要多步执行的长任务:**必须先**用 output_send__ 发一条确认消息告诉用户已收到(如「好的我去看看~」),**然后再**执行具体排查工具。确认消息不代表任务完成,发出后仍需继续执行实际工具并最终汇报结果。"
|
||||||
|
|
||||||
if a.skills != nil {
|
if a.skills != nil {
|
||||||
if sp := a.skills.GetInjectedPrompt(); sp != "" {
|
if sp := a.skills.GetInjectedPrompt(); sp != "" {
|
||||||
|
|||||||
@ -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)
|
// Register plugin management tools that talk to the manager (always, even if skills dir is empty)
|
||||||
tp := p.name + "_"
|
tp := p.name + "_"
|
||||||
s.RegisterTool(tp+"npm_install", sdk.ToolDef{
|
s.RegisterTool(tp+"plugin_install", sdk.ToolDef{
|
||||||
Name: tp + "npm_install",
|
Name: tp + "plugin_install",
|
||||||
Description: "安装 ClawHub 适配器插件管理器中的插件。支持 npm: 前缀(npm 包)和 clawhub: 前缀(ClawHub 市场)。安装后立即可用。",
|
Description: "安装插件。支持 npm: 前缀(npm 包)、clawhub: 前缀(ClawHub 市场,如 clawhub:openclaw-codex-app-server)。安装后立即可用。",
|
||||||
Parameters: map[string]interface{}{
|
Parameters: map[string]interface{}{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": map[string]interface{}{
|
"properties": map[string]interface{}{
|
||||||
"package": map[string]interface{}{"type": "string", "description": "插件包标识。npm:<pkg> 从 npm 安装,clawhub:<pkg> 从 ClawHub 市场安装(如 clawhub:openclaw-codex-app-server)"},
|
"package": map[string]interface{}{"type": "string", "description": "插件包标识。npm:<pkg> 从 npm 安装,clawhub:<pkg> 从 ClawHub 市场安装"},
|
||||||
},
|
},
|
||||||
"required": []string{"package"},
|
"required": []string{"package"},
|
||||||
},
|
},
|
||||||
}, p.handlePluginInstall)
|
}, p.handlePluginInstall)
|
||||||
|
|
||||||
s.RegisterTool(tp+"npm_uninstall", sdk.ToolDef{
|
s.RegisterTool(tp+"plugin_uninstall", sdk.ToolDef{
|
||||||
Name: tp + "npm_uninstall",
|
Name: tp + "plugin_uninstall",
|
||||||
Description: "从插件管理器中移除已安装的插件。",
|
Description: "卸载已安装的插件,支持所有类型(sidecar、skill、manager 插件、ClawHub 安装的插件)。会停止进程、删除目录并清理注册。",
|
||||||
Parameters: map[string]interface{}{
|
Parameters: map[string]interface{}{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": map[string]interface{}{
|
"properties": map[string]interface{}{
|
||||||
"name": map[string]interface{}{"type": "string", "description": "要移除的插件名称"},
|
"name": map[string]interface{}{"type": "string", "description": "要卸载的插件名称(目录名,如 my-plugin)"},
|
||||||
},
|
},
|
||||||
"required": []string{"name"},
|
"required": []string{"name"},
|
||||||
},
|
},
|
||||||
@ -377,7 +377,7 @@ func (p *Plugin) handleClawHubSearch(args map[string]interface{}) (interface{},
|
|||||||
if name == "" {
|
if name == "" {
|
||||||
name = pkg.Slug
|
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))
|
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
|
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()
|
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()
|
p.mu.Unlock()
|
||||||
|
|
||||||
if mgr == nil {
|
// 3. Try manager for OC plugins
|
||||||
return errorResult("plugin manager not available"), nil
|
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{}{
|
// 4. Delete directory from disk
|
||||||
"name": name,
|
if _, statErr := os.Stat(pluginDir); statErr == nil {
|
||||||
})
|
if err := os.RemoveAll(pluginDir); err != nil {
|
||||||
if err != nil {
|
logs = append(logs, fmt.Sprintf("删除目录失败: %v", err))
|
||||||
return errorResult(fmt.Sprintf("uninstall failed: %v", err)), nil
|
} 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{}{
|
return map[string]interface{}{
|
||||||
"content": fmt.Sprintf("已卸载插件: %s\n %s", name, string(data)),
|
"content": result,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -79,6 +79,47 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
|||||||
log.Printf("[mcp] register restart tool: %v", err)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -253,6 +294,123 @@ func (p *Plugin) restartServerHandler(args map[string]interface{}) (interface{},
|
|||||||
return fmt.Sprintf("MCP 服务器 [%s] 已成功重启", name), nil
|
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) {
|
func newTransport(cfg serverConfig) (Transport, error) {
|
||||||
if cfg.URL != "" {
|
if cfg.URL != "" {
|
||||||
return NewSSETransport(cfg.URL), nil
|
return NewSSETransport(cfg.URL), nil
|
||||||
|
|||||||
Reference in New Issue
Block a user