feat: restructure plugin system, add Lua plugin support, update docs

This commit is contained in:
JianFeeeee
2026-07-13 21:48:13 +08:00
parent e49bdca9ff
commit 950090959f
44 changed files with 3098 additions and 1097 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -15,6 +15,7 @@ import (
"sync"
"time"
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
agentCore "gitcode.com/JianFeeeee/HomeAgent/internal/agent/core"
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
internalConfig "gitcode.com/JianFeeeee/HomeAgent/internal/config"
@ -61,16 +62,55 @@ type Handler struct {
pluginReg *plugin.Registry
eventBus *events.Bus
statusProvider agentCore.StatusProvider
providerMgr *agentAPI.ProviderManager
baseAPIKey string
sessionMu sync.Mutex
sessions map[string]time.Time
chatMu sync.Mutex
chatHistory []ChatMsg
cmdMu sync.Mutex
cmdHistory []CmdExec
termMu sync.Mutex
termStates map[string]*termState
}
func NewHandler(sup *supervisor.Daemon, mem *memory.GraphDB, sk *skill.Manager, lua *luaVM.VM, cfg *types.Config, iom *agentIO.IOManager, tm *text.Memory, ks *knowledge.Store, tr *tracker.Tracker, cr *internalConfig.ConfigRegistry, pr *plugin.Registry, evBus *events.Bus, sp agentCore.StatusProvider) *Handler {
type ChatMsg struct {
Role string `json:"role"`
Content string `json:"content"`
ReasoningContent string `json:"reasoning_content,omitempty"`
Time string `json:"time"`
}
type CmdExec struct {
Command string `json:"command"`
Stdout string `json:"stdout"`
Stderr string `json:"stderr"`
ExitCode int `json:"exit_code"`
Status string `json:"status"`
Time string `json:"time"`
}
type termState struct {
ID string `json:"id"`
Command string `json:"command"`
Running bool `json:"running"`
Output string `json:"output"`
CreatedAt string `json:"created_at"`
Uptime string `json:"uptime"`
created time.Time
}
const maxChatHistory = 200
const maxCmdHistory = 100
const maxTerminals = 50
func NewHandler(sup *supervisor.Daemon, mem *memory.GraphDB, sk *skill.Manager, lua *luaVM.VM, cfg *types.Config, iom *agentIO.IOManager, tm *text.Memory, ks *knowledge.Store, tr *tracker.Tracker, cr *internalConfig.ConfigRegistry, pr *plugin.Registry, evBus *events.Bus, sp agentCore.StatusProvider, pm *agentAPI.ProviderManager, baseKey string) *Handler {
var idx *memory.Indexer
if mem != nil {
idx = memory.NewIndexer(mem)
}
return &Handler{
h := &Handler{
supervisor: sup,
memory: mem,
indexer: idx,
@ -86,8 +126,83 @@ func NewHandler(sup *supervisor.Daemon, mem *memory.GraphDB, sk *skill.Manager,
pluginReg: pr,
eventBus: evBus,
statusProvider: sp,
providerMgr: pm,
baseAPIKey: baseKey,
sessions: make(map[string]time.Time),
termStates: make(map[string]*termState),
}
if evBus != nil {
go h.trackToolEvents()
}
return h
}
func (h *Handler) trackToolEvents() {
h.eventBus.Subscribe(events.EventToolCall, func(ev *events.Event) {
h.handleToolEvent(ev)
})
}
func (h *Handler) handleToolEvent(ev *events.Event) {
payload := ev.Payload
tool, _ := payload["tool"].(string)
args, _ := payload["args"].(map[string]interface{})
status, _ := payload["status"].(string)
ts := time.Now()
switch tool {
case "cmd_run":
exec := CmdExec{
Command: getStr(args, "command"),
Status: status,
Time: ts.Format(time.RFC3339),
}
h.cmdMu.Lock()
h.cmdHistory = append(h.cmdHistory, exec)
if len(h.cmdHistory) > maxCmdHistory {
h.cmdHistory = h.cmdHistory[len(h.cmdHistory)-maxCmdHistory:]
}
h.cmdMu.Unlock()
case "terminal_create":
id := getStr(args, "id")
cmd := getStr(args, "command")
now := time.Now()
term := &termState{
ID: id,
Command: cmd,
Running: true,
CreatedAt: now.Format(time.RFC3339),
created: now,
}
h.termMu.Lock()
h.termStates[id] = term
if len(h.termStates) > maxTerminals {
for k := range h.termStates {
delete(h.termStates, k)
break
}
}
h.termMu.Unlock()
case "terminal_close":
id := getStr(args, "id")
if id != "" {
h.termMu.Lock()
if t, ok := h.termStates[id]; ok {
t.Running = false
}
h.termMu.Unlock()
}
}
}
func getStr(m map[string]interface{}, key string) string {
if m == nil {
return ""
}
v, _ := m[key].(string)
return v
}
func (h *Handler) getWebUIConfig() (apiKey, username, password string, ttl time.Duration) {
@ -204,6 +319,7 @@ func (h *Handler) RegisterRoutes(mux *http.ServeMux) {
mux.HandleFunc("/api/v1/skills", h.requireAPI(h.handleSkills))
mux.HandleFunc("/api/v1/memory", h.requireAPI(h.handleMemory))
mux.HandleFunc("/api/v1/memory/", h.requireAPI(h.handleMemory))
mux.HandleFunc("/api/v1/memory/graph", h.requireAPI(h.handleMemoryGraph))
mux.HandleFunc("/api/v1/memory/context", h.requireAPI(h.handleMemoryContext))
mux.HandleFunc("/api/v1/memory/tools", h.requireAPI(h.handleMemoryTools))
mux.HandleFunc("/api/v1/memory/text", h.requireAPI(h.handleTextMemory))
@ -218,7 +334,10 @@ func (h *Handler) RegisterRoutes(mux *http.ServeMux) {
mux.HandleFunc("/api/v1/tracker", h.requireAPI(h.handleTracker))
mux.HandleFunc("/api/v1/tracker/", h.requireAPI(h.handleTracker))
mux.HandleFunc("/api/v1/chat", h.requireAPI(h.handleChat))
mux.HandleFunc("/api/v1/chat/history", h.requireAPI(h.handleChatHistory))
mux.HandleFunc("/api/v1/chat/events", h.requireAPI(h.handleChatEvents))
mux.HandleFunc("/api/v1/terminals", h.requireAPI(h.handleTerminals))
mux.HandleFunc("/api/v1/cmd/history", h.requireAPI(h.handleCmdHistory))
mux.HandleFunc("/api/v1/kernel", h.requireAPI(h.handleKernel))
mux.HandleFunc("/api/v1/plugins", h.requireAPI(h.handlePlugins))
mux.HandleFunc("/api/v1/plugins/", h.requireAPI(h.handlePluginByID))
@ -528,6 +647,23 @@ func (h *Handler) handleMemoryTools(w http.ResponseWriter, r *http.Request) {
})
}
func (h *Handler) handleMemoryGraph(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
if h.memory == nil {
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "memory system not available"})
return
}
data, err := h.memory.GraphData()
if err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{"success": true, "data": data})
}
func (h *Handler) handleKnowledge(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
@ -652,7 +788,7 @@ func (h *Handler) handleAdapters(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
if err := h.lua.ReloadAll(); err != nil {
if err := h.lua.LoadAdapter(path); err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
@ -687,7 +823,7 @@ func (h *Handler) handleAdapterByID(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusNotFound, map[string]string{"error": "adapter not found"})
return
}
h.lua.ReloadAll()
h.lua.RemoveAdapter(name)
writeJSON(w, http.StatusOK, map[string]string{"status": "deleted", "name": name})
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
@ -705,6 +841,42 @@ func (h *Handler) handleNetwork(w http.ResponseWriter, r *http.Request) {
})
}
func (h *Handler) addChatMsg(msg ChatMsg) {
h.chatMu.Lock()
defer h.chatMu.Unlock()
h.chatHistory = append(h.chatHistory, msg)
if len(h.chatHistory) > maxChatHistory {
h.chatHistory = h.chatHistory[len(h.chatHistory)-maxChatHistory:]
}
}
func (h *Handler) handleChatHistory(w http.ResponseWriter, r *http.Request) {
h.chatMu.Lock()
result := make([]ChatMsg, len(h.chatHistory))
copy(result, h.chatHistory)
h.chatMu.Unlock()
writeJSON(w, http.StatusOK, map[string]interface{}{"messages": result})
}
func (h *Handler) handleTerminals(w http.ResponseWriter, r *http.Request) {
h.termMu.Lock()
terms := make([]*termState, 0, len(h.termStates))
for _, ts := range h.termStates {
ts.Uptime = time.Since(ts.created).Round(time.Second).String()
terms = append(terms, ts)
}
h.termMu.Unlock()
writeJSON(w, http.StatusOK, map[string]interface{}{"terminals": terms})
}
func (h *Handler) handleCmdHistory(w http.ResponseWriter, r *http.Request) {
h.cmdMu.Lock()
result := make([]CmdExec, len(h.cmdHistory))
copy(result, h.cmdHistory)
h.cmdMu.Unlock()
writeJSON(w, http.StatusOK, map[string]interface{}{"history": result})
}
func (h *Handler) handleChat(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
@ -722,15 +894,22 @@ func (h *Handler) handleChat(w http.ResponseWriter, r *http.Request) {
return
}
h.addChatMsg(ChatMsg{Role: "user", Content: body.Message, Time: time.Now().Format(time.RFC3339)})
resp := h.iom.InjectTextSync("cli", body.Message)
if resp == nil {
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "agent unavailable"})
return
}
content, _ := resp.Payload["content"].(string)
writeJSON(w, http.StatusOK, map[string]interface{}{
reasoning, _ := resp.Payload["reasoning_content"].(string)
result := map[string]interface{}{
"response": content,
})
}
if reasoning != "" {
result["reasoning_content"] = reasoning
}
h.addChatMsg(ChatMsg{Role: "assistant", Content: content, ReasoningContent: reasoning, Time: time.Now().Format(time.RFC3339)})
writeJSON(w, http.StatusOK, result)
}
func (h *Handler) handleChatEvents(w http.ResponseWriter, r *http.Request) {
@ -771,15 +950,17 @@ func (h *Handler) handleChatEvents(w http.ResponseWriter, r *http.Request) {
}
}()
unsub := h.eventBus.Subscribe(events.EventAll, func(evt *events.Event) {
data, _ := json.Marshal(evt)
select {
case writeCh <- fmt.Sprintf("event: %s\ndata: %s", evt.Type, string(data)):
default:
}
})
defer unsub()
subTypes := []string{"agent_output", "reasoning", "agent_error"}
for _, t := range subTypes {
t2 := t
_ = h.eventBus.Subscribe(events.EventType(t2), func(evt *events.Event) {
data, _ := json.Marshal(evt)
select {
case writeCh <- fmt.Sprintf("event: %s\ndata: %s", evt.Type, string(data)):
default:
}
})
}
for {
select {
case <-done:
@ -844,10 +1025,30 @@ func (h *Handler) handleSettings(w http.ResponseWriter, r *http.Request) {
defs := h.cfgReg.ListDefs(prefix)
for _, d := range defs {
meta[d.Key] = d
// 有 def 但 DB 中尚无值的 key用 default 填充以便在 WebUI 中显示和编辑
if _, exists := values[d.Key]; !exists {
values[d.Key] = d.Default
}
}
// 无前缀时同时加载所有插件配置
if prefix == "" && h.pluginReg != nil {
for _, p := range h.pluginReg.List() {
ps := h.cfgReg.PluginConfig(p)
pkeys, _ := ps.List("")
for _, k := range pkeys {
v, _ := ps.Get(k)
fullKey := "plugin." + p + "." + k
values[fullKey] = v
if def := h.cfgReg.GetDef(fullKey); def != nil {
meta[fullKey] = def
}
}
}
}
}
plugins := []string{"core"}
pm := h.pluginReg.PluginMetas()
if h.pluginReg != nil {
for _, p := range h.pluginReg.List() {
plugins = append(plugins, "plugin."+p)
@ -855,9 +1056,10 @@ func (h *Handler) handleSettings(w http.ResponseWriter, r *http.Request) {
}
sort.Strings(plugins)
writeJSON(w, http.StatusOK, map[string]interface{}{
"settings": values,
"meta": meta,
"plugins": plugins,
"settings": values,
"meta": meta,
"plugins": plugins,
"plugin_meta": pm,
})
case http.MethodPut:
var body struct {
@ -883,12 +1085,37 @@ func (h *Handler) handleSettings(w http.ResponseWriter, r *http.Request) {
return
}
}
if strings.HasPrefix(body.Key, "core.llm.") && h.providerMgr != nil && h.lua != nil {
h.reloadLLMProviders()
}
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
}
func (h *Handler) reloadLLMProviders() {
cfg := h.cfgReg.ToConfig()
h.providerMgr.Reset()
for _, src := range cfg.LLM.Sources {
key := src.APIKey
if key == "" {
key = h.baseAPIKey
}
provider := agentAPI.NewLuaAdaptedProvider(agentAPI.BaseConfig{
Model: src.Model,
BaseURL: src.BaseURL,
APIKey: key,
Temperature: cfg.LLM.Temperature,
MaxTokens: cfg.LLM.MaxTokens,
}, h.lua, src.Adapter)
h.providerMgr.Register(src.Name, provider)
}
if cfg.LLM.Provider != "" {
_ = h.providerMgr.SetDefault(cfg.LLM.Provider)
}
}
func (h *Handler) handleOpenAICompletions(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
@ -1169,6 +1396,9 @@ func (h *Handler) handlePluginByID(w http.ResponseWriter, r *http.Request) {
func (h *Handler) handleStatic(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Expires", "0")
w.Write([]byte(dashboardHTML))
return
}

View File

@ -7,6 +7,7 @@ import (
"log"
"net/http"
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
agentCore "gitcode.com/JianFeeeee/HomeAgent/internal/agent/core"
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
internalConfig "gitcode.com/JianFeeeee/HomeAgent/internal/config"
@ -39,6 +40,8 @@ var (
webuiPR *plugin.Registry
webuiEvBus *events.Bus
webuiStatusProvider agentCore.StatusProvider
webuiProviderMgr *agentAPI.ProviderManager
webuiBaseAPIKey string
)
// Configure 注入 WebUI 插件需要的内核依赖。必须在 Load() 之前调用。
@ -47,16 +50,19 @@ func Configure(addr string,
lua *luaVM.VM, cfg *types.Config, iom *agentIO.IOManager,
tm *text.Memory, ks *knowledge.Store, tr *tracker.Tracker,
cr *internalConfig.ConfigRegistry, pr *plugin.Registry, evBus *events.Bus,
sp agentCore.StatusProvider,
sp agentCore.StatusProvider, pm *agentAPI.ProviderManager, baseKey string,
) {
webuiAddr = addr
webuiSup, webuiMem, webuiSK, webuiLua = sup, mem, sk, lua
webuiCfg, webuiIOM, webuiTM, webuiKS = cfg, iom, tm, ks
webuiTR, webuiCR, webuiPR, webuiEvBus = tr, cr, pr, evBus
webuiStatusProvider = sp
webuiProviderMgr = pm
webuiBaseAPIKey = baseKey
}
func init() {
plugin.RegisterPluginMeta("webui", "Web 控制台", "WebUI")
plugin.RegisterFactory("webui", func(name string, config map[string]interface{}) (sdk.Plugin, error) {
if webuiSup == nil {
return nil, nil // 未 Configure 则跳过(不给日志警告)
@ -69,6 +75,7 @@ func init() {
webuiSup, webuiMem, webuiSK, webuiLua,
webuiCfg, webuiIOM, webuiTM, webuiKS,
webuiTR, webuiCR, webuiPR, webuiEvBus, webuiStatusProvider,
webuiProviderMgr, webuiBaseAPIKey,
), nil
})
}
@ -93,6 +100,8 @@ type Plugin struct {
pr *plugin.Registry
evBus *events.Bus
statusProvider agentCore.StatusProvider
providerMgr *agentAPI.ProviderManager
baseAPIKey string
}
func New(name, addr string,
@ -100,7 +109,7 @@ func New(name, addr string,
lua *luaVM.VM, cfg *types.Config, iom *agentIO.IOManager,
tm *text.Memory, ks *knowledge.Store, tr *tracker.Tracker,
cr *internalConfig.ConfigRegistry, pr *plugin.Registry, evBus *events.Bus,
sp agentCore.StatusProvider,
sp agentCore.StatusProvider, pm *agentAPI.ProviderManager, baseKey string,
) *Plugin {
return &Plugin{
name: name,
@ -108,7 +117,7 @@ func New(name, addr string,
mux: http.NewServeMux(),
sup: sup, mem: mem, sk: sk, lua: lua, cfg: cfg,
iom: iom, tm: tm, ks: ks, tr: tr, cr: cr, pr: pr, evBus: evBus,
statusProvider: sp,
statusProvider: sp, providerMgr: pm, baseAPIKey: baseKey,
}
}
@ -151,7 +160,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
s.Settings().RegisterDef(sdk.ConfigDef{Key: "password", Default: "", Type: "password", DisplayName: "Web 控制台登录密码", Description: "Web 控制台登录密码", Category: "webui"})
s.Settings().RegisterDef(sdk.ConfigDef{Key: "session_ttl_hours", Default: "24", Type: "int", DisplayName: "会话时长(小时)", Description: "登录 cookie 有效时长", Category: "webui"})
p.ensureAuthBootstrap(s)
h := NewHandler(p.sup, p.mem, p.sk, p.lua, p.cfg, p.iom, p.tm, p.ks, p.tr, p.cr, p.pr, p.evBus, p.statusProvider)
h := NewHandler(p.sup, p.mem, p.sk, p.lua, p.cfg, p.iom, p.tm, p.ks, p.tr, p.cr, p.pr, p.evBus, p.statusProvider, p.providerMgr, p.baseAPIKey)
p.handler = h
h.RegisterRoutes(p.mux)