feat(cli): CLI 插件能力对齐 WebUI(memory/knowledge/config/tracker/adapters/network)

原来 CLI 插件只覆盖 WebUI 的一小部分:/memory 只有 query、/knowledge 只有
list、没有 config/tracker/adapters/network,结构化输出还各拼一套文本格式。

按 WebUI 的 REST 面对齐:
- /memory query|graph|text [n]     (对应 /memory、/memory/graph、/memory/text)
- /knowledge | delete <name> | stats(对应 GET/DELETE /knowledge)
- /config                          (对应 GET /config)
- /tracker | rollback              (对应 GET /tracker、POST /tracker/rollback)
- /adapters | remove <name>        (对应 GET/DELETE /adapters)
- /network                         (对应 GET /network)
- 统一 writeJSONContent:结构化数据一律缩进 JSON,与 WebUI 同口径。

waiter 侧同步:把上述命令在 local(发 CLI 插件)与 remote(走 REST)两条路
都接上,/help 补齐;两条路语义一致。
This commit is contained in:
JianFeeeee
2026-09-15 12:14:25 +08:00
parent e4e112ce92
commit d9b2f2c81c
2 changed files with 262 additions and 35 deletions

View File

@ -36,8 +36,16 @@ Server commands (local 与 remote 行为一致):
/plugin enable <name> enable plugin /plugin enable <name> enable plugin
/plugin info <name> plugin details /plugin info <name> plugin details
/memory query <text> query graph memory /memory query <text> query graph memory
/knowledge list knowledge base /memory graph dump full graph memory snapshot
/memory text [n] recent text-memory events + stats
/knowledge list knowledge base (+stats)
/knowledge delete <name> delete knowledge item /knowledge delete <name> delete knowledge item
/config dump kernel config (JSON)
/tracker change-tracking stats
/tracker rollback roll back this session's file changes
/adapters list loaded Lua adapters
/adapters remove <name> remove a Lua adapter
/network network status + LLM endpoints
/agents list agents /agents list agents
/chat <text> send to agent /chat <text> send to agent
@ -238,11 +246,23 @@ Any other text is sent to the agent directly.`)
} }
return true return true
case strings.HasPrefix(cmd, "/memory query "): case strings.HasPrefix(cmd, "/memory "):
q := strings.TrimSpace(cmd[14:]) sub := strings.TrimSpace(cmd[8:])
if rc := state.RemoteConn(); rc != nil { if rc := state.RemoteConn(); rc != nil {
d, _ := rc.DoAPI("GET", "/api/v1/memory?query="+q, "") switch {
printJSON(out, d) case strings.HasPrefix(sub, "query "):
q := strings.TrimSpace(strings.TrimPrefix(sub, "query "))
d, _ := rc.DoAPI("GET", "/api/v1/memory?query="+q, "")
printJSON(out, d)
case sub == "graph":
d, _ := rc.DoAPI("GET", "/api/v1/memory/graph", "")
printJSON(out, d)
case sub == "text" || strings.HasPrefix(sub, "text "):
d, _ := rc.DoAPI("GET", "/api/v1/memory/text", "")
printJSON(out, d)
default:
fmt.Fprintln(out, "usage: /memory query <text> | /memory graph | /memory text [n]")
}
} else { } else {
state.Send(cmd) state.Send(cmd)
} }
@ -258,12 +278,63 @@ Any other text is sent to the agent directly.`)
} }
return true return true
case cmd == "/knowledge": case cmd == "/knowledge" || cmd == "/knowledge list" || cmd == "/knowledge stats":
if rc := state.RemoteConn(); rc != nil { if rc := state.RemoteConn(); rc != nil {
d, _ := rc.DoAPI("GET", "/api/v1/knowledge", "") d, _ := rc.DoAPI("GET", "/api/v1/knowledge", "")
printJSON(out, d) printJSON(out, d)
} else { } else {
state.Send("/knowledge") state.Send(cmd)
}
return true
case cmd == "/config":
if rc := state.RemoteConn(); rc != nil {
d, _ := rc.DoAPI("GET", "/api/v1/config", "")
printJSON(out, d)
} else {
state.Send("/config")
}
return true
case cmd == "/tracker":
if rc := state.RemoteConn(); rc != nil {
d, _ := rc.DoAPI("GET", "/api/v1/tracker", "")
printJSON(out, d)
} else {
state.Send("/tracker")
}
return true
case cmd == "/tracker rollback":
if rc := state.RemoteConn(); rc != nil {
d, _ := rc.DoAPI("POST", "/api/v1/tracker/rollback", "")
printJSON(out, d)
} else {
state.Send("/tracker rollback")
}
return true
case cmd == "/adapters" || strings.HasPrefix(cmd, "/adapters remove "):
if rc := state.RemoteConn(); rc != nil {
if strings.HasPrefix(cmd, "/adapters remove ") {
name := strings.TrimSpace(cmd[17:])
d, _ := rc.DoAPI("DELETE", "/api/v1/adapters/"+name, "")
printJSON(out, d)
} else {
d, _ := rc.DoAPI("GET", "/api/v1/adapters", "")
printJSON(out, d)
}
} else {
state.Send(cmd)
}
return true
case cmd == "/network":
if rc := state.RemoteConn(); rc != nil {
d, _ := rc.DoAPI("GET", "/api/v1/network", "")
printJSON(out, d)
} else {
state.Send("/network")
} }
return true return true

View File

@ -9,6 +9,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"sort" "sort"
"strconv"
"strings" "strings"
"sync" "sync"
@ -301,9 +302,17 @@ func (p *Plugin) handleBuiltin(conn net.Conn, line string, s *sdk.PluginSDK) boo
case "/memory": case "/memory":
p.cmdMemory(conn, parts, s) p.cmdMemory(conn, parts, s)
case "/knowledge": case "/knowledge":
p.cmdKnowledge(conn, s) p.cmdKnowledge(conn, parts, s)
case "/agents": case "/agents":
p.cmdAgents(conn, s) p.cmdAgents(conn, s)
case "/config":
p.cmdConfig(conn, s)
case "/tracker":
p.cmdTracker(conn, parts, s)
case "/adapters":
p.cmdAdapters(conn, parts, s)
case "/network":
p.cmdNetwork(conn, s)
default: default:
return false return false
} }
@ -327,9 +336,20 @@ func (p *Plugin) cmdHelp(conn net.Conn) {
/plugin disable <name> 禁用插件 /plugin disable <name> 禁用插件
/plugin enable <name> 启用插件 /plugin enable <name> 启用插件
/plugin info <name> 查看插件详情 /plugin info <name> 查看插件详情
/plugin reload 重载插件
/memory query <关键词> 查询图记忆 /memory query <关键词> 查询图记忆
/memory graph 导出整张图记忆快照
/memory text [n] 最近 n 条文本记忆事件 + 统计
/knowledge 列出知识库 /knowledge 列出知识库
/agents 列出 Agent /knowledge delete <name> 删除一条知识
/knowledge stats 知识库统计
/config [prefix] 导出内核配置(可按前缀筛选)
/tracker 变更追踪统计
/tracker rollback 回滚本次会话的文件变更
/adapters 列出已加载的 Lua 适配器
/adapters remove <name> 卸载适配器
/network 网络状态与 LLM 端点
/agents 当前 Agent 信息
其他文本直接发送给 Agent 处理。`, 其他文本直接发送给 Agent 处理。`,
}) })
@ -621,50 +641,186 @@ func (p *Plugin) cmdPlugin(conn net.Conn, parts []string, s *sdk.PluginSDK) {
// ======== /memory ======== // ======== /memory ========
func (p *Plugin) cmdMemory(conn net.Conn, parts []string, s *sdk.PluginSDK) { // writeJSONContent 把一个结构以缩进 JSON 写入 response 帧。
if len(parts) < 3 || parts[1] != "query" { // CLI 与 WebUI 对齐的口径:结构化数据一律 JSON(带缩进便于人读),
writeLine(conn, map[string]interface{}{"type": "response", "content": "用法: /memory query <关键词>"}) // 不再每个子命令各拼一种文本格式。
return func writeJSONContent(conn net.Conn, v interface{}) {
} data, err := json.MarshalIndent(v, "", " ")
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 { if err != nil {
writeLine(conn, map[string]interface{}{"type": "error", "error": err.Error()}) writeLine(conn, map[string]interface{}{"type": "error", "error": err.Error()})
return return
} }
result := map[string]interface{}{
"entities": entities,
"relations": relations,
}
data, _ := json.MarshalIndent(result, "", " ")
writeLine(conn, map[string]interface{}{"type": "response", "content": string(data)}) writeLine(conn, map[string]interface{}{"type": "response", "content": string(data)})
} }
func (p *Plugin) cmdMemory(conn net.Conn, parts []string, s *sdk.PluginSDK) {
sub := ""
if len(parts) >= 2 {
sub = parts[1]
}
switch sub {
case "query":
if len(parts) < 3 {
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
}
writeJSONContent(conn, map[string]interface{}{
"entities": entities,
"relations": relations,
})
case "graph":
mem := s.Memory()
if mem == nil {
writeLine(conn, map[string]interface{}{"type": "error", "error": "memory not available"})
return
}
data, err := mem.GraphData()
if err != nil {
writeLine(conn, map[string]interface{}{"type": "error", "error": err.Error()})
return
}
writeJSONContent(conn, data)
case "text":
tm := s.TextMemory()
if tm == nil {
writeLine(conn, map[string]interface{}{"type": "error", "error": "text memory not available"})
return
}
n := 20
if len(parts) >= 3 {
if v, err := strconv.Atoi(parts[2]); err == nil && v > 0 {
n = v
}
}
events, err := tm.RecentEvents(n)
if err != nil {
writeLine(conn, map[string]interface{}{"type": "error", "error": err.Error()})
return
}
writeJSONContent(conn, map[string]interface{}{
"events": events,
"stats": tm.Stats(),
})
default:
writeLine(conn, map[string]interface{}{"type": "response", "content": "用法: /memory query <关键词> | /memory graph | /memory text [n]"})
}
}
// ======== /knowledge ======== // ======== /knowledge ========
func (p *Plugin) cmdKnowledge(conn net.Conn, s *sdk.PluginSDK) { func (p *Plugin) cmdKnowledge(conn net.Conn, parts []string, s *sdk.PluginSDK) {
ks := s.Knowledge() ks := s.Knowledge()
if ks == nil { if ks == nil {
writeLine(conn, map[string]interface{}{"type": "error", "error": "knowledge not available"}) writeLine(conn, map[string]interface{}{"type": "error", "error": "knowledge not available"})
return return
} }
items, err := ks.List() sub := "list"
if err != nil { if len(parts) >= 2 {
writeLine(conn, map[string]interface{}{"type": "error", "error": err.Error()}) sub = parts[1]
}
switch sub {
case "list":
items, err := ks.List()
if err != nil {
writeLine(conn, map[string]interface{}{"type": "error", "error": err.Error()})
return
}
writeJSONContent(conn, items)
case "delete", "remove":
if len(parts) < 3 {
writeLine(conn, map[string]interface{}{"type": "response", "content": "用法: /knowledge delete <name>"})
return
}
if err := ks.Remove(parts[2]); err != nil {
writeLine(conn, map[string]interface{}{"type": "error", "error": err.Error()})
return
}
writeLine(conn, map[string]interface{}{"type": "response", "content": "已删除知识: " + parts[2]})
case "stats":
writeJSONContent(conn, ks.Stats())
default:
writeLine(conn, map[string]interface{}{"type": "response", "content": "用法: /knowledge | /knowledge delete <name> | /knowledge stats"})
}
}
// ======== /config ========
func (p *Plugin) cmdConfig(conn net.Conn, s *sdk.PluginSDK) {
cfg := s.Config()
if cfg == nil {
writeLine(conn, map[string]interface{}{"type": "error", "error": "config not available"})
return return
} }
if len(items) == 0 { writeJSONContent(conn, cfg.Get())
writeLine(conn, map[string]interface{}{"type": "response", "content": "知识库为空"}) }
// ======== /tracker ========
func (p *Plugin) cmdTracker(conn net.Conn, parts []string, s *sdk.PluginSDK) {
tr := s.Tracker()
if tr == nil {
writeLine(conn, map[string]interface{}{"type": "error", "error": "tracker not available"})
return return
} }
data, _ := json.MarshalIndent(items, "", " ") if len(parts) >= 2 && parts[1] == "rollback" {
writeLine(conn, map[string]interface{}{"type": "response", "content": string(data)}) if err := tr.Rollback(); err != nil {
writeLine(conn, map[string]interface{}{"type": "error", "error": err.Error()})
return
}
writeLine(conn, map[string]interface{}{"type": "response", "content": "已回滚文件变更"})
return
}
writeJSONContent(conn, map[string]interface{}{
"stats": tr.Stats(),
"has_changes": tr.HasChanges(),
"changesets": tr.ChangeSets(),
})
}
// ======== /adapters ========
func (p *Plugin) cmdAdapters(conn net.Conn, parts []string, s *sdk.PluginSDK) {
ad := s.Adapter()
if ad == nil {
writeLine(conn, map[string]interface{}{"type": "error", "error": "lua adapter not available"})
return
}
if len(parts) >= 3 && (parts[1] == "remove" || parts[1] == "delete") {
if err := ad.Remove(parts[2]); err != nil {
writeLine(conn, map[string]interface{}{"type": "error", "error": err.Error()})
return
}
writeLine(conn, map[string]interface{}{"type": "response", "content": "已卸载适配器: " + parts[2]})
return
}
writeJSONContent(conn, map[string]interface{}{"adapters": ad.List()})
}
// ======== /network ========
// cmdNetwork 与 WebUI 的 GET /api/v1/network 同口径:网络状态 + LLM 端点。
func (p *Plugin) cmdNetwork(conn net.Conn, s *sdk.PluginSDK) {
var endpoints interface{}
if cfg := s.Config(); cfg != nil {
if c := cfg.Get(); c != nil {
endpoints = c.Defaults.LLMEndpoints
}
}
writeJSONContent(conn, map[string]interface{}{
"network_status": "monitoring",
"endpoints": endpoints,
})
} }
// ======== /agents ======== // ======== /agents ========