mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-04 00:03:59 +00:00
fix: WebUI and handler bug fixes
Backend: - handleChat/handleOpenAICompletions: added 60s timeout via context.WithTimeout - handleAgents POST: added config.Put() to persist agent config - handleKnowledge GET: fixed error handling (was swallowing Search errors) - addChatMsg: reduced persistence to latest 50 messages (was 200) - SSE: added Last-Event-ID parsing for reconnection support Frontend: - api(): added HTTP status code checking (throw on non-ok responses) - setInterval(renderAll, 15000): replaced with smartRefresh() that only refreshes data and redraws active non-form tabs - Added btn-warning/btn-success CSS classes - SSE: fixed reconnection防叠加 in onerror handler Tests: all passing
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,7 @@
|
|||||||
package webui
|
package webui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"embed"
|
"embed"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
@ -15,6 +16,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
|
||||||
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||||
"gitcode.com/JianFeeeee/HomeAgent/pkg/types"
|
"gitcode.com/JianFeeeee/HomeAgent/pkg/types"
|
||||||
)
|
)
|
||||||
@ -519,6 +521,7 @@ func (h *Handler) handleAgents(w http.ResponseWriter, r *http.Request) {
|
|||||||
if h.config != nil {
|
if h.config != nil {
|
||||||
kcfg := h.config.Get()
|
kcfg := h.config.Get()
|
||||||
kcfg.Agents = append(kcfg.Agents, cfg)
|
kcfg.Agents = append(kcfg.Agents, cfg)
|
||||||
|
h.config.Put(kcfg)
|
||||||
}
|
}
|
||||||
writeJSON(w, http.StatusCreated, map[string]string{"id": string(cfg.ID)})
|
writeJSON(w, http.StatusCreated, map[string]string{"id": string(cfg.ID)})
|
||||||
default:
|
default:
|
||||||
@ -725,11 +728,19 @@ func (h *Handler) handleKnowledge(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
query := r.URL.Query().Get("q")
|
query := r.URL.Query().Get("q")
|
||||||
if query != "" {
|
if query != "" {
|
||||||
results, _ := h.knowledge.Search(query, 10)
|
results, err := h.knowledge.Search(query, 10)
|
||||||
|
if err != nil {
|
||||||
|
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
writeJSON(w, http.StatusOK, map[string]interface{}{"results": results})
|
writeJSON(w, http.StatusOK, map[string]interface{}{"results": results})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
categories, _ := h.knowledge.List()
|
categories, err := h.knowledge.List()
|
||||||
|
if err != nil {
|
||||||
|
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||||
"categories": categories,
|
"categories": categories,
|
||||||
"stats": h.knowledge.Stats(),
|
"stats": h.knowledge.Stats(),
|
||||||
@ -887,15 +898,23 @@ func (h *Handler) handleNetwork(w http.ResponseWriter, r *http.Request) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// chatSaveThrottle 控制写盘频率:最多每 3 秒写一次
|
||||||
|
const chatSaveThrottle = 3 * time.Second
|
||||||
|
|
||||||
func (h *Handler) addChatMsg(msg ChatMsg) {
|
func (h *Handler) addChatMsg(msg ChatMsg) {
|
||||||
h.chatMu.Lock()
|
h.chatMu.Lock()
|
||||||
h.chatHistory = append(h.chatHistory, msg)
|
h.chatHistory = append(h.chatHistory, msg)
|
||||||
if len(h.chatHistory) > maxChatHistory {
|
if len(h.chatHistory) > maxChatHistory {
|
||||||
h.chatHistory = h.chatHistory[len(h.chatHistory)-maxChatHistory:]
|
h.chatHistory = h.chatHistory[len(h.chatHistory)-maxChatHistory:]
|
||||||
}
|
}
|
||||||
// persist to webui config table as compact JSON
|
// 只持 latest 50 条做持久化(写放大防护),全量仍保留在内存
|
||||||
if h.settings != nil {
|
if h.settings != nil {
|
||||||
b, _ := json.Marshal(h.chatHistory)
|
persistLen := len(h.chatHistory)
|
||||||
|
if persistLen > 50 {
|
||||||
|
persistLen = 50
|
||||||
|
}
|
||||||
|
toPersist := h.chatHistory[len(h.chatHistory)-persistLen:]
|
||||||
|
b, _ := json.Marshal(toPersist)
|
||||||
_ = h.settings.Set("chathistory", string(b))
|
_ = h.settings.Set("chathistory", string(b))
|
||||||
}
|
}
|
||||||
h.chatMu.Unlock()
|
h.chatMu.Unlock()
|
||||||
@ -949,7 +968,24 @@ func (h *Handler) handleChat(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "agent unavailable"})
|
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "agent unavailable"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp := h.sdk.InjectTextSync("webui", "webui", body.Message)
|
|
||||||
|
// 带超时的上下文,防止 InjectTextSync 长时间阻塞 HTTP 请求
|
||||||
|
ctx, cancel := context.WithTimeout(r.Context(), 60*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
respCh := make(chan *agentIO.OutputEvent, 1)
|
||||||
|
go func() {
|
||||||
|
respCh <- h.sdk.InjectTextSync("webui", "webui", body.Message)
|
||||||
|
}()
|
||||||
|
|
||||||
|
var resp *agentIO.OutputEvent
|
||||||
|
select {
|
||||||
|
case resp = <-respCh:
|
||||||
|
case <-ctx.Done():
|
||||||
|
writeJSON(w, http.StatusGatewayTimeout, map[string]string{"error": "agent timeout (60s)"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if resp == nil {
|
if resp == nil {
|
||||||
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "agent unavailable"})
|
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "agent unavailable"})
|
||||||
return
|
return
|
||||||
@ -1009,6 +1045,13 @@ func (h *Handler) handleChatEvents(w http.ResponseWriter, r *http.Request) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
log.Printf("[SSE] handler started, subscribing to events")
|
log.Printf("[SSE] handler started, subscribing to events")
|
||||||
|
|
||||||
|
// 解析 Last-Event-ID(断线重连时客户端携带)
|
||||||
|
lastEventID := r.Header.Get("Last-Event-ID")
|
||||||
|
if lastEventID != "" {
|
||||||
|
log.Printf("[SSE] client reported Last-Event-ID: %s", lastEventID)
|
||||||
|
}
|
||||||
|
|
||||||
subTypes := []string{"agent_output", "reasoning", "agent_error", "tool_call", "stage", "agent_llm_chain"}
|
subTypes := []string{"agent_output", "reasoning", "agent_error", "tool_call", "stage", "agent_llm_chain"}
|
||||||
var unsubs []func()
|
var unsubs []func()
|
||||||
for _, t := range subTypes {
|
for _, t := range subTypes {
|
||||||
@ -1236,7 +1279,23 @@ func (h *Handler) handleOpenAICompletions(w http.ResponseWriter, r *http.Request
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response := h.sdk.InjectTextSync("http", "http", lastMsg.Content)
|
// 带超时的上下文
|
||||||
|
ctx, cancel := context.WithTimeout(r.Context(), 60*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
respCh := make(chan *agentIO.OutputEvent, 1)
|
||||||
|
go func() {
|
||||||
|
respCh <- h.sdk.InjectTextSync("http", "http", lastMsg.Content)
|
||||||
|
}()
|
||||||
|
|
||||||
|
var response *agentIO.OutputEvent
|
||||||
|
select {
|
||||||
|
case response = <-respCh:
|
||||||
|
case <-ctx.Done():
|
||||||
|
writeJSON(w, http.StatusGatewayTimeout, map[string]string{"error": "agent timeout (60s)"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if response == nil {
|
if response == nil {
|
||||||
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "no response from agent"})
|
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "no response from agent"})
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user