diff --git a/internal/plugins/webui/dashboard.html b/internal/plugins/webui/dashboard.html
index 6d8f984..b10c457 100644
--- a/internal/plugins/webui/dashboard.html
+++ b/internal/plugins/webui/dashboard.html
@@ -98,7 +98,7 @@ code{font-family:monospace;font-size:12px;color:#a5b4fc}
-
@@ -111,7 +111,7 @@ code{font-family:monospace;font-size:12px;color:#a5b4fc}
diff --git a/internal/plugins/webui/handler.go b/internal/plugins/webui/handler.go
index 97c4381..cd8155d 100644
--- a/internal/plugins/webui/handler.go
+++ b/internal/plugins/webui/handler.go
@@ -1,7 +1,9 @@
package webui
import (
+ "crypto/rand"
"embed"
+ "encoding/hex"
"encoding/json"
"fmt"
"io"
@@ -10,6 +12,7 @@ import (
"sort"
"strconv"
"strings"
+ "sync"
"time"
agentCore "gitcode.com/JianFeeeee/HomeAgent/internal/agent/core"
@@ -32,6 +35,8 @@ var dashboardFS embed.FS
var dashboardHTML string
+const loginHTML = `
HomeAgent Login`
+
func init() {
data, err := dashboardFS.ReadFile("dashboard.html")
if err == nil {
@@ -40,21 +45,23 @@ func init() {
}
type Handler struct {
- supervisor *supervisor.Daemon
- memory *memory.GraphDB
- indexer *memory.Indexer
- skills *skill.Manager
- lua *luaVM.VM
- config *types.Config
- startTime time.Time
- iom *agentIO.IOManager
- textMem *text.Memory
- knowledge *knowledge.Store
- tracker *tracker.Tracker
- cfgReg *internalConfig.ConfigRegistry
- pluginReg *plugin.Registry
- eventBus *events.Bus
+ supervisor *supervisor.Daemon
+ memory *memory.GraphDB
+ indexer *memory.Indexer
+ skills *skill.Manager
+ lua *luaVM.VM
+ config *types.Config
+ startTime time.Time
+ iom *agentIO.IOManager
+ textMem *text.Memory
+ knowledge *knowledge.Store
+ tracker *tracker.Tracker
+ cfgReg *internalConfig.ConfigRegistry
+ pluginReg *plugin.Registry
+ eventBus *events.Bus
statusProvider agentCore.StatusProvider
+ sessionMu sync.Mutex
+ sessions map[string]time.Time
}
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 {
@@ -78,36 +85,199 @@ func NewHandler(sup *supervisor.Daemon, mem *memory.GraphDB, sk *skill.Manager,
pluginReg: pr,
eventBus: evBus,
statusProvider: sp,
+ sessions: make(map[string]time.Time),
+ }
+}
+
+func (h *Handler) getWebUIConfig() (apiKey, username, password string, ttl time.Duration) {
+ ttl = 24 * time.Hour
+ if h.cfgReg == nil {
+ return
+ }
+ ps := h.cfgReg.PluginConfig("webui")
+ if v, _ := ps.Get("api_key"); v != nil {
+ apiKey, _ = v.(string)
+ }
+ if v, _ := ps.Get("username"); v != nil {
+ username, _ = v.(string)
+ }
+ if v, _ := ps.Get("password"); v != nil {
+ password, _ = v.(string)
+ }
+ if v, _ := ps.Get("session_ttl_hours"); v != nil {
+ switch n := v.(type) {
+ case float64:
+ if n > 0 { ttl = time.Duration(n) * time.Hour }
+ case string:
+ if i, err := strconv.Atoi(n); err == nil && i > 0 { ttl = time.Duration(i) * time.Hour }
+ }
+ }
+ if username == "" { username = "admin" }
+ return
+}
+
+func (h *Handler) createSession() (string, time.Time, error) {
+ buf := make([]byte, 32)
+ if _, err := rand.Read(buf); err != nil {
+ return "", time.Time{}, err
+ }
+ _, _, _, ttl := h.getWebUIConfig()
+ expires := time.Now().Add(ttl)
+ token := hex.EncodeToString(buf)
+ h.sessionMu.Lock()
+ h.sessions[token] = expires
+ h.sessionMu.Unlock()
+ return token, expires, nil
+}
+
+func (h *Handler) validSession(r *http.Request) bool {
+ cookie, err := r.Cookie("homeagent_session")
+ if err != nil || cookie.Value == "" {
+ return false
+ }
+ h.sessionMu.Lock()
+ defer h.sessionMu.Unlock()
+ expires, ok := h.sessions[cookie.Value]
+ if !ok {
+ return false
+ }
+ if time.Now().After(expires) {
+ delete(h.sessions, cookie.Value)
+ return false
+ }
+ return true
+}
+
+func (h *Handler) validAPIKey(r *http.Request) bool {
+ apiKey, _, _, _ := h.getWebUIConfig()
+ if apiKey == "" {
+ return false
+ }
+ got := strings.TrimSpace(r.Header.Get("X-API-Key"))
+ if got == "" {
+ auth := strings.TrimSpace(r.Header.Get("Authorization"))
+ if strings.HasPrefix(auth, "Bearer ") {
+ got = strings.TrimSpace(strings.TrimPrefix(auth, "Bearer "))
+ }
+ }
+ return got != "" && got == apiKey
+}
+
+func (h *Handler) requireAPI(fn http.HandlerFunc) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ apiKey, _, _, _ := h.getWebUIConfig()
+ if apiKey == "" && !h.validSession(r) {
+ writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "webui api_key not configured"})
+ return
+ }
+ if h.validAPIKey(r) || h.validSession(r) {
+ fn(w, r)
+ return
+ }
+ writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"})
+ }
+}
+
+func (h *Handler) requireWeb(fn http.HandlerFunc) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ _, username, password, _ := h.getWebUIConfig()
+ if username == "" || password == "" {
+ http.Error(w, "webui username/password not configured", http.StatusServiceUnavailable)
+ return
+ }
+ if h.validSession(r) {
+ fn(w, r)
+ return
+ }
+ http.Redirect(w, r, "/login", http.StatusFound)
}
}
func (h *Handler) RegisterRoutes(mux *http.ServeMux) {
- mux.HandleFunc("/api/v1/status", h.handleStatus)
- mux.HandleFunc("/api/v1/agents", h.handleAgents)
- mux.HandleFunc("/api/v1/agents/", h.handleAgentByID)
- mux.HandleFunc("/api/v1/skills", h.handleSkills)
- mux.HandleFunc("/api/v1/memory", h.handleMemory)
- mux.HandleFunc("/api/v1/memory/", h.handleMemory)
- mux.HandleFunc("/api/v1/memory/context", h.handleMemoryContext)
- mux.HandleFunc("/api/v1/memory/tools", h.handleMemoryTools)
- mux.HandleFunc("/api/v1/memory/text", h.handleTextMemory)
- mux.HandleFunc("/api/v1/network", h.handleNetwork)
- mux.HandleFunc("/api/v1/config", h.handleConfig)
- mux.HandleFunc("/api/v1/settings", h.handleSettings)
- mux.HandleFunc("/api/v1/settings/", h.handleSettings)
- mux.HandleFunc("/api/v1/knowledge", h.handleKnowledge)
- mux.HandleFunc("/api/v1/knowledge/", h.handleKnowledge)
- mux.HandleFunc("/api/v1/adapters", h.handleAdapters)
- mux.HandleFunc("/api/v1/adapters/", h.handleAdapterByID)
- mux.HandleFunc("/api/v1/tracker", h.handleTracker)
- mux.HandleFunc("/api/v1/tracker/", h.handleTracker)
- mux.HandleFunc("/api/v1/chat", h.handleChat)
- mux.HandleFunc("/api/v1/chat/events", h.handleChatEvents)
- mux.HandleFunc("/api/v1/kernel", h.handleKernel)
- mux.HandleFunc("/api/v1/plugins", h.handlePlugins)
- mux.HandleFunc("/api/v1/plugins/", h.handlePluginByID)
- mux.HandleFunc("/v1/chat/completions", h.handleOpenAICompletions)
- mux.HandleFunc("/", h.handleStatic)
+ mux.HandleFunc("/login", h.handleLoginPage)
+ mux.HandleFunc("/api/v1/login", h.handleLogin)
+ mux.HandleFunc("/api/v1/logout", h.handleLogout)
+ mux.HandleFunc("/api/v1/status", h.requireAPI(h.handleStatus))
+ mux.HandleFunc("/api/v1/agents", h.requireAPI(h.handleAgents))
+ mux.HandleFunc("/api/v1/agents/", h.requireAPI(h.handleAgentByID))
+ 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/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))
+ mux.HandleFunc("/api/v1/network", h.requireAPI(h.handleNetwork))
+ mux.HandleFunc("/api/v1/config", h.requireAPI(h.handleConfig))
+ mux.HandleFunc("/api/v1/settings", h.requireAPI(h.handleSettings))
+ mux.HandleFunc("/api/v1/settings/", h.requireAPI(h.handleSettings))
+ mux.HandleFunc("/api/v1/knowledge", h.requireAPI(h.handleKnowledge))
+ mux.HandleFunc("/api/v1/knowledge/", h.requireAPI(h.handleKnowledge))
+ mux.HandleFunc("/api/v1/adapters", h.requireAPI(h.handleAdapters))
+ mux.HandleFunc("/api/v1/adapters/", h.requireAPI(h.handleAdapterByID))
+ 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/events", h.requireAPI(h.handleChatEvents))
+ 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))
+ mux.HandleFunc("/v1/chat/completions", h.requireAPI(h.handleOpenAICompletions))
+ mux.HandleFunc("/", h.requireWeb(h.handleStatic))
+}
+
+func (h *Handler) handleLoginPage(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodGet {
+ http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+ if h.validSession(r) {
+ http.Redirect(w, r, "/", http.StatusFound)
+ return
+ }
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ _, _ = w.Write([]byte(loginHTML))
+}
+
+func (h *Handler) handleLogin(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+ _, username, password, _ := h.getWebUIConfig()
+ if username == "" || password == "" {
+ writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "webui username/password not configured"})
+ return
+ }
+ var body struct { Username string `json:"username"`; Password string `json:"password"` }
+ if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
+ writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"})
+ return
+ }
+ if body.Username != username || body.Password != password {
+ writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "用户名或密码错误"})
+ return
+ }
+ token, expires, err := h.createSession()
+ if err != nil {
+ writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+ return
+ }
+ http.SetCookie(w, &http.Cookie{Name: "homeagent_session", Value: token, Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode, Expires: expires})
+ writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
+}
+
+func (h *Handler) handleLogout(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+ if cookie, err := r.Cookie("homeagent_session"); err == nil {
+ h.sessionMu.Lock()
+ delete(h.sessions, cookie.Value)
+ h.sessionMu.Unlock()
+ http.SetCookie(w, &http.Cookie{Name: "homeagent_session", Value: "", Path: "/", Expires: time.Unix(0, 0), MaxAge: -1, HttpOnly: true, SameSite: http.SameSiteLaxMode})
+ }
+ writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
func (h *Handler) handleStatus(w http.ResponseWriter, r *http.Request) {
diff --git a/internal/plugins/webui/plugin.go b/internal/plugins/webui/plugin.go
index 844aab1..1be864e 100644
--- a/internal/plugins/webui/plugin.go
+++ b/internal/plugins/webui/plugin.go
@@ -1,6 +1,9 @@
package webui
import (
+ "crypto/rand"
+ "encoding/hex"
+ "fmt"
"log"
"net/http"
@@ -109,9 +112,45 @@ func New(name, addr string,
}
}
+func randomSecret(n int) string {
+ buf := make([]byte, n)
+ if _, err := rand.Read(buf); err != nil {
+ return ""
+ }
+ return hex.EncodeToString(buf)
+}
+
+func (p *Plugin) ensureAuthBootstrap(s *sdk.PluginSDK) {
+ sett := s.Settings()
+ if sett == nil {
+ return
+ }
+ if v, _ := sett.Get("username"); v == nil || fmt.Sprint(v) == "" {
+ _ = sett.Set("username", "admin")
+ }
+ if v, _ := sett.Get("password"); v == nil || fmt.Sprint(v) == "" {
+ pw := randomSecret(12)
+ _ = sett.Set("password", pw)
+ log.Printf("[webui] bootstrap password generated for user admin: %s", pw)
+ }
+ if v, _ := sett.Get("api_key"); v == nil || fmt.Sprint(v) == "" {
+ key := randomSecret(16)
+ _ = sett.Set("api_key", key)
+ log.Printf("[webui] bootstrap api_key generated: %s", key)
+ }
+ if v, _ := sett.Get("session_ttl_hours"); v == nil || fmt.Sprint(v) == "" {
+ _ = sett.Set("session_ttl_hours", "24")
+ }
+}
+
func (p *Plugin) Name() string { return p.name }
func (p *Plugin) Start(s *sdk.PluginSDK) error {
+ s.Settings().RegisterDef(sdk.ConfigDef{Key: "api_key", Default: "", Type: "password", DisplayName: "API 密钥", Description: "访问 API 时需要的密钥", Category: "webui"})
+ s.Settings().RegisterDef(sdk.ConfigDef{Key: "username", Default: "admin", Type: "string", DisplayName: "登录用户名", Description: "Web 控制台登录用户名", Category: "webui"})
+ 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)
p.handler = h
h.RegisterRoutes(p.mux)