mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 17:38:10 +00:00
feat: expand local operator controls across CLI and WebUI
- Add structured CLI commands for status/kernel/settings/plugins/memory/knowledge/agents - Inject core dependencies directly into CLI plugin for non-HTTP operator workflows - Add plugin management panel and API proxy endpoints to WebUI - Let cmd_run inherit default workdir from core.agent.workdir - Use fixed loopback address for pluginmgr API - Remove stale MaxToolTurns config usage from homed wiring
This commit is contained in:
@ -8,16 +8,35 @@ import (
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
agentCore "gitcode.com/JianFeeeee/HomeAgent/internal/agent/core"
|
||||
internalConfig "gitcode.com/JianFeeeee/HomeAgent/internal/config"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
|
||||
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||
)
|
||||
|
||||
// DefaultSocket 由 main.go 在 Load() 前设置,覆盖默认 socket 路径。
|
||||
// 若为空,factory 使用 "<dataDir>/cli.sock"。
|
||||
var DefaultSocket string
|
||||
|
||||
// 以下通过 Configure() 注入内核依赖
|
||||
var (
|
||||
pluginReg *plugin.Registry
|
||||
cfgReg *internalConfig.ConfigRegistry
|
||||
statusProv agentCore.StatusProvider
|
||||
pluginDir string
|
||||
)
|
||||
|
||||
// Configure 由 main.go 在 Load() 前调用,注入内核依赖供结构化命令使用。
|
||||
func Configure(pr *plugin.Registry, cr *internalConfig.ConfigRegistry, sp agentCore.StatusProvider, pDir string) {
|
||||
pluginReg = pr
|
||||
cfgReg = cr
|
||||
statusProv = sp
|
||||
pluginDir = pDir
|
||||
}
|
||||
|
||||
func init() {
|
||||
plugin.RegisterFactory("cli", func(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||
sock := DefaultSocket
|
||||
@ -95,6 +114,12 @@ func (p *Plugin) handleConn(conn net.Conn, s *sdk.PluginSDK) {
|
||||
continue
|
||||
}
|
||||
|
||||
if line[0] == '/' {
|
||||
if p.handleBuiltin(conn, line, s) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
resp := s.InjectTextSync("cli", "cli", line)
|
||||
if resp != nil {
|
||||
content, _ := resp.Payload["content"].(string)
|
||||
@ -111,6 +136,291 @@ func (p *Plugin) handleConn(conn net.Conn, s *sdk.PluginSDK) {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Plugin) handleBuiltin(conn net.Conn, line string, s *sdk.PluginSDK) bool {
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
switch parts[0] {
|
||||
case "/help":
|
||||
p.cmdHelp(conn)
|
||||
case "/status":
|
||||
p.cmdStatus(conn)
|
||||
case "/kernel":
|
||||
p.cmdKernel(conn)
|
||||
case "/settings":
|
||||
p.cmdSettings(conn, parts)
|
||||
case "/plugin":
|
||||
p.cmdPlugin(conn, parts)
|
||||
case "/memory":
|
||||
p.cmdMemory(conn, parts, s)
|
||||
case "/knowledge":
|
||||
p.cmdKnowledge(conn, s)
|
||||
case "/agents":
|
||||
p.cmdAgents(conn)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *Plugin) cmdHelp(conn net.Conn) {
|
||||
writeLine(conn, map[string]interface{}{
|
||||
"type": "response",
|
||||
"content": `内置命令(直接对话内核,不依赖网络):
|
||||
/help 显示此帮助
|
||||
/status 系统运行状态
|
||||
/kernel 内核状态(插件、工具、LLM、记忆)
|
||||
/settings 列出所有配置
|
||||
/settings set <key> <val> 修改配置项
|
||||
/settings core.llm 按前缀筛选
|
||||
/plugin list 列出已安装插件
|
||||
/plugin install <url> 安装插件(需回环网络)
|
||||
/plugin remove <name> 卸载插件
|
||||
/plugin info <name> 查看插件详情
|
||||
/memory query <关键词> 查询图记忆
|
||||
/knowledge 列出知识库
|
||||
/agents 列出 Agent
|
||||
|
||||
其他文本直接发送给 Agent 处理。`,
|
||||
})
|
||||
}
|
||||
|
||||
// ======== /status ========
|
||||
|
||||
func (p *Plugin) cmdStatus(conn net.Conn) {
|
||||
if statusProv == nil {
|
||||
writeLine(conn, map[string]interface{}{"type": "error", "error": "status provider not available"})
|
||||
return
|
||||
}
|
||||
ks := statusProv.GetKernelStatus()
|
||||
|
||||
llmStatus := "不可用"
|
||||
if ks.LLM.Available {
|
||||
llmStatus = fmt.Sprintf("%s (%d sources)", ks.LLM.Provider, ks.LLM.Sources)
|
||||
}
|
||||
memInfo := "未初始化"
|
||||
if ks.Memory.Available {
|
||||
memInfo = fmt.Sprintf("%d entities, %d relations", ks.Memory.EntityCount, ks.Memory.RelationCount)
|
||||
}
|
||||
runtime := fmt.Sprintf("goroutines=%d mem=%dMB", ks.Runtime.Goroutines, ks.Runtime.MemoryMB)
|
||||
uptime := ks.Uptime
|
||||
|
||||
writeLine(conn, map[string]interface{}{
|
||||
"type": "response",
|
||||
"content": fmt.Sprintf(`HomeAgent 内核状态
|
||||
状态: running
|
||||
运行: %s
|
||||
LLM: %s
|
||||
记忆: %s
|
||||
运行时: %s
|
||||
插件: %d loaded
|
||||
工具: %d registered`, uptime, llmStatus, memInfo, runtime,
|
||||
len(ks.Plugins), len(ks.Tools)),
|
||||
})
|
||||
}
|
||||
|
||||
// ======== /kernel ========
|
||||
|
||||
func (p *Plugin) cmdKernel(conn net.Conn) {
|
||||
if statusProv == nil {
|
||||
writeLine(conn, map[string]interface{}{"type": "error", "error": "status provider not available"})
|
||||
return
|
||||
}
|
||||
data, _ := json.MarshalIndent(statusProv.GetKernelStatus(), "", " ")
|
||||
writeLine(conn, map[string]interface{}{"type": "response", "content": string(data)})
|
||||
}
|
||||
|
||||
// ======== /settings ========
|
||||
|
||||
func (p *Plugin) cmdSettings(conn net.Conn, parts []string) {
|
||||
if cfgReg == nil {
|
||||
writeLine(conn, map[string]interface{}{"type": "error", "error": "config registry not available"})
|
||||
return
|
||||
}
|
||||
|
||||
if len(parts) >= 2 && parts[1] == "set" {
|
||||
if len(parts) < 4 {
|
||||
writeLine(conn, map[string]interface{}{"type": "response", "content": "用法: /settings set <key> <value>"})
|
||||
return
|
||||
}
|
||||
key := parts[2]
|
||||
val := strings.Join(parts[3:], " ")
|
||||
if err := cfgReg.Set(key, val); err != nil {
|
||||
writeLine(conn, map[string]interface{}{"type": "error", "error": err.Error()})
|
||||
return
|
||||
}
|
||||
writeLine(conn, map[string]interface{}{"type": "response", "content": fmt.Sprintf("已设置: %s = %s", key, val)})
|
||||
return
|
||||
}
|
||||
|
||||
prefix := ""
|
||||
if len(parts) >= 2 {
|
||||
prefix = parts[1]
|
||||
}
|
||||
keys := cfgReg.List(prefix)
|
||||
sort.Strings(keys)
|
||||
if len(keys) == 0 {
|
||||
writeLine(conn, map[string]interface{}{"type": "response", "content": "无匹配配置项"})
|
||||
return
|
||||
}
|
||||
var lines []string
|
||||
for _, k := range keys {
|
||||
v, _ := cfgReg.Get(k)
|
||||
lines = append(lines, fmt.Sprintf(" %s = %v", k, v))
|
||||
}
|
||||
writeLine(conn, map[string]interface{}{
|
||||
"type": "response",
|
||||
"content": fmt.Sprintf("配置 (%d 项):\n%s", len(keys), strings.Join(lines, "\n")),
|
||||
})
|
||||
}
|
||||
|
||||
// ======== /plugin ========
|
||||
|
||||
func (p *Plugin) cmdPlugin(conn net.Conn, parts []string) {
|
||||
if len(parts) < 2 {
|
||||
writeLine(conn, map[string]interface{}{"type": "response", "content": "用法: /plugin list|install <url>|remove <name>|info <name>"})
|
||||
return
|
||||
}
|
||||
|
||||
switch parts[1] {
|
||||
case "list":
|
||||
if pluginReg == nil {
|
||||
writeLine(conn, map[string]interface{}{"type": "error", "error": "plugin registry not available"})
|
||||
return
|
||||
}
|
||||
names := pluginReg.List()
|
||||
if len(names) == 0 {
|
||||
writeLine(conn, map[string]interface{}{"type": "response", "content": "无已加载插件"})
|
||||
return
|
||||
}
|
||||
writeLine(conn, map[string]interface{}{
|
||||
"type": "response",
|
||||
"content": fmt.Sprintf("已加载插件 (%d):\n %s", len(names), strings.Join(names, "\n ")),
|
||||
})
|
||||
|
||||
case "install":
|
||||
if len(parts) < 3 {
|
||||
writeLine(conn, map[string]interface{}{"type": "response", "content": "用法: /plugin install <url>"})
|
||||
return
|
||||
}
|
||||
writeLine(conn, map[string]interface{}{
|
||||
"type": "response",
|
||||
"content": "安装插件需要网络,本环境可能受限。请通过 WebUI 或使用 agent 对话安装。",
|
||||
})
|
||||
|
||||
case "remove":
|
||||
if len(parts) < 3 {
|
||||
writeLine(conn, map[string]interface{}{"type": "response", "content": "用法: /plugin remove <name>"})
|
||||
return
|
||||
}
|
||||
name := parts[2]
|
||||
if pluginDir == "" {
|
||||
writeLine(conn, map[string]interface{}{"type": "error", "error": "plugin dir not configured"})
|
||||
return
|
||||
}
|
||||
dir := filepath.Join(pluginDir, name)
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
writeLine(conn, map[string]interface{}{"type": "error", "error": err.Error()})
|
||||
return
|
||||
}
|
||||
writeLine(conn, map[string]interface{}{"type": "response", "content": fmt.Sprintf("插件 %s 已删除,执行 /plugin reload 生效", name)})
|
||||
|
||||
case "info":
|
||||
if len(parts) < 3 {
|
||||
writeLine(conn, map[string]interface{}{"type": "response", "content": "用法: /plugin info <name>"})
|
||||
return
|
||||
}
|
||||
if pluginReg == nil {
|
||||
writeLine(conn, map[string]interface{}{"type": "error", "error": "plugin registry not available"})
|
||||
return
|
||||
}
|
||||
plg := pluginReg.Get(parts[2])
|
||||
if plg == nil {
|
||||
writeLine(conn, map[string]interface{}{"type": "response", "content": fmt.Sprintf("插件 %q 未加载", parts[2])})
|
||||
return
|
||||
}
|
||||
writeLine(conn, map[string]interface{}{"type": "response", "content": fmt.Sprintf("名称: %s\n状态: 已加载", plg.Name())})
|
||||
|
||||
case "reload":
|
||||
if pluginReg == nil {
|
||||
writeLine(conn, map[string]interface{}{"type": "error", "error": "plugin registry not available"})
|
||||
return
|
||||
}
|
||||
if _, err := pluginReg.Reload(pluginDir); err != nil {
|
||||
writeLine(conn, map[string]interface{}{"type": "error", "error": err.Error()})
|
||||
return
|
||||
}
|
||||
writeLine(conn, map[string]interface{}{"type": "response", "content": "插件已重载"})
|
||||
|
||||
default:
|
||||
writeLine(conn, map[string]interface{}{"type": "response", "content": "未知: /plugin " + parts[1] + "。支持: list, install, remove, info, reload"})
|
||||
}
|
||||
}
|
||||
|
||||
// ======== /memory ========
|
||||
|
||||
func (p *Plugin) cmdMemory(conn net.Conn, parts []string, s *sdk.PluginSDK) {
|
||||
if len(parts) < 3 || parts[1] != "query" {
|
||||
writeLine(conn, map[string]interface{}{"type": "response", "content": "用法: /memory query <关键词>"})
|
||||
return
|
||||
}
|
||||
q := strings.Join(parts[2:], " ")
|
||||
|
||||
mem := s.Memory()
|
||||
if mem == nil {
|
||||
writeLine(conn, map[string]interface{}{"type": "error", "error": "memory not available"})
|
||||
return
|
||||
}
|
||||
entities, relations, err := mem.Recall([]string{q}, 2)
|
||||
if err != nil {
|
||||
writeLine(conn, map[string]interface{}{"type": "error", "error": err.Error()})
|
||||
return
|
||||
}
|
||||
result := map[string]interface{}{
|
||||
"entities": entities,
|
||||
"relations": relations,
|
||||
}
|
||||
data, _ := json.MarshalIndent(result, "", " ")
|
||||
writeLine(conn, map[string]interface{}{"type": "response", "content": string(data)})
|
||||
}
|
||||
|
||||
// ======== /knowledge ========
|
||||
|
||||
func (p *Plugin) cmdKnowledge(conn net.Conn, s *sdk.PluginSDK) {
|
||||
ks := s.Knowledge()
|
||||
if ks == nil {
|
||||
writeLine(conn, map[string]interface{}{"type": "error", "error": "knowledge not available"})
|
||||
return
|
||||
}
|
||||
items, err := ks.List()
|
||||
if err != nil {
|
||||
writeLine(conn, map[string]interface{}{"type": "error", "error": err.Error()})
|
||||
return
|
||||
}
|
||||
if len(items) == 0 {
|
||||
writeLine(conn, map[string]interface{}{"type": "response", "content": "知识库为空"})
|
||||
return
|
||||
}
|
||||
data, _ := json.MarshalIndent(items, "", " ")
|
||||
writeLine(conn, map[string]interface{}{"type": "response", "content": string(data)})
|
||||
}
|
||||
|
||||
// ======== /agents ========
|
||||
|
||||
func (p *Plugin) cmdAgents(conn net.Conn) {
|
||||
if statusProv == nil {
|
||||
writeLine(conn, map[string]interface{}{"type": "error", "error": "status provider not available"})
|
||||
return
|
||||
}
|
||||
ks := statusProv.GetKernelStatus()
|
||||
data, _ := json.MarshalIndent(map[string]string{"agent_id": ks.AgentID}, "", " ")
|
||||
writeLine(conn, map[string]interface{}{"type": "response", "content": string(data)})
|
||||
}
|
||||
|
||||
// ======== helpers ========
|
||||
|
||||
func writeLine(conn net.Conn, v interface{}) {
|
||||
data, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user