mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-08-17 06:23:12 +00:00
- Add bgalert package: classify provider errors (auth, billing, model_not_found), store alert in system_configs, broadcast WS event - Wire AlertDeps into consolidation (episodic, semantic, dreaming) and vault enrich workers to report failures after retry exhaustion - Auto-clear alert when admin changes provider-related system configs - Add BackgroundErrorBanner component with dismiss + "Fix in Settings" - Lift settings modal state to AppLayout so banner can open it - Add EventBackgroundError to admin-only WS event filter - Add i18n translations (en/vi/zh) for alert messages and reasons
124 lines
4.1 KiB
Go
124 lines
4.1 KiB
Go
package http
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"regexp"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/bgalert"
|
|
"github.com/nextlevelbuilder/goclaw/internal/bus"
|
|
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
)
|
|
|
|
// SystemConfigsHandler handles system config CRUD endpoints.
|
|
type SystemConfigsHandler struct {
|
|
store store.SystemConfigStore
|
|
msgBus *bus.MessageBus
|
|
}
|
|
|
|
func NewSystemConfigsHandler(s store.SystemConfigStore, msgBus *bus.MessageBus) *SystemConfigsHandler {
|
|
return &SystemConfigsHandler{store: s, msgBus: msgBus}
|
|
}
|
|
|
|
// validKeyRe allows alphanumeric, dots, underscores, hyphens (1-100 chars).
|
|
var validKeyRe = regexp.MustCompile(`^[a-zA-Z0-9._-]{1,100}$`)
|
|
|
|
// providerRelatedKeys lists system config keys whose changes should clear
|
|
// background worker error alerts (user is likely fixing the root cause).
|
|
var providerRelatedKeys = map[string]bool{
|
|
"background.provider": true,
|
|
"background.model": true,
|
|
"agent.default_provider": true,
|
|
"agent.default_model": true,
|
|
"embedding.provider": true,
|
|
"embedding.model": true,
|
|
}
|
|
|
|
// RegisterRoutes registers system config endpoints on the given mux.
|
|
func (h *SystemConfigsHandler) RegisterRoutes(mux *http.ServeMux) {
|
|
mux.HandleFunc("GET /v1/system-configs", requireAuth("", h.handleList))
|
|
mux.HandleFunc("GET /v1/system-configs/{key}", requireAuth("", h.handleGet))
|
|
mux.HandleFunc("PUT /v1/system-configs/{key}", requireAuth("admin", h.handleSet))
|
|
mux.HandleFunc("DELETE /v1/system-configs/{key}", requireAuth("admin", h.handleDelete))
|
|
}
|
|
|
|
func (h *SystemConfigsHandler) handleList(w http.ResponseWriter, r *http.Request) {
|
|
configs, err := h.store.List(r.Context())
|
|
if err != nil {
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, configs)
|
|
}
|
|
|
|
func (h *SystemConfigsHandler) handleGet(w http.ResponseWriter, r *http.Request) {
|
|
locale := extractLocale(r)
|
|
key := r.PathValue("key")
|
|
val, err := h.store.Get(r.Context(), key)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusNotFound, map[string]string{"error": i18n.T(locale, i18n.MsgNotFound, "config", key)})
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]string{"key": key, "value": val})
|
|
}
|
|
|
|
func (h *SystemConfigsHandler) handleSet(w http.ResponseWriter, r *http.Request) {
|
|
locale := extractLocale(r)
|
|
key := r.PathValue("key")
|
|
|
|
if !validKeyRe.MatchString(key) {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid key format (alphanumeric, dots, hyphens, 1-100 chars)"})
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
Value string `json:"value"`
|
|
}
|
|
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<16)).Decode(&req); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidJSON)})
|
|
return
|
|
}
|
|
|
|
if err := h.store.Set(r.Context(), key, req.Value); err != nil {
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Auto-clear background error alert when provider settings change.
|
|
if providerRelatedKeys[key] {
|
|
bgalert.ClearProviderError(r.Context(), h.store)
|
|
}
|
|
|
|
// Broadcast change with tenant context so in-memory config refreshes for the right tenant.
|
|
// Use fresh context with tenant ID — request context may be canceled before subscriber runs.
|
|
if h.msgBus != nil {
|
|
freshCtx := store.WithTenantID(context.Background(), store.TenantIDFromContext(r.Context()))
|
|
h.msgBus.Broadcast(bus.Event{
|
|
Name: bus.TopicSystemConfigChanged,
|
|
Payload: freshCtx,
|
|
})
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]string{"key": key, "value": req.Value})
|
|
}
|
|
|
|
func (h *SystemConfigsHandler) handleDelete(w http.ResponseWriter, r *http.Request) {
|
|
key := r.PathValue("key")
|
|
if err := h.store.Delete(r.Context(), key); err != nil {
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
if h.msgBus != nil {
|
|
freshCtx := store.WithTenantID(context.Background(), store.TenantIDFromContext(r.Context()))
|
|
h.msgBus.Broadcast(bus.Event{
|
|
Name: bus.TopicSystemConfigChanged,
|
|
Payload: freshCtx,
|
|
})
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|