webui: 修复 '删除源/服务器' 只置 <nil> 不真删的问题

前端 deleteSource/deleteMCPServer 通过 PUT value:null 删除,但后端 handleSettings
PUT 只 SetCore/SetPlugin(fmt.Sprint(nil) → 字面 '<nil>'),导致 core.llm.sources.<name>.*
等键残留不可达的 <nil> 行,污染 LLM 源与探活。

- SettingsAPI 新增 RemoveCore/RemovePlugin;settingsImpl 接入 ConfigRegistry.Delete
- webui handleSettings PUT:body.Value==null 时改走删除分支(核心表/插件表均适配)
- 现状验证:mocktest.* 残留已从本机 config.db 清除,sources=3,无无效源
This commit is contained in:
root
2026-08-10 09:35:29 +08:00
parent 171e6f233b
commit cd2a27a8cc
3 changed files with 106 additions and 30 deletions

View File

@ -49,8 +49,8 @@ type Handler struct {
status sdk.StatusAPI
llm sdk.LLMAPI
sessionMu sync.Mutex
sessions map[string]time.Time
sessionMu sync.Mutex
sessions map[string]time.Time
chatMu sync.Mutex
chatHistory []ChatMsg
@ -282,12 +282,18 @@ func (h *Handler) getWebUIConfig() (apiKey, username, password string, ttl time.
if v, _ := h.settings.Get("session_ttl_hours"); v != nil {
switch n := v.(type) {
case float64:
if n > 0 { ttl = time.Duration(n) * time.Hour }
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 i, err := strconv.Atoi(n); err == nil && i > 0 {
ttl = time.Duration(i) * time.Hour
}
}
}
if username == "" { username = "admin" }
if username == "" {
username = "admin"
}
return
}
@ -426,7 +432,10 @@ func (h *Handler) handleLogin(w http.ResponseWriter, r *http.Request) {
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"` }
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
@ -1156,16 +1165,29 @@ func (h *Handler) handleSettings(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"})
return
}
deleting := body.Value == nil
if strings.HasPrefix(body.Key, "plugin.") {
parts := strings.SplitN(body.Key, ".", 3)
if len(parts) >= 3 {
if err := h.settings.SetPlugin(parts[1], parts[2], body.Value); err != nil {
var err error
if deleting {
err = h.settings.RemovePlugin(parts[1], parts[2])
} else {
err = h.settings.SetPlugin(parts[1], parts[2], body.Value)
}
if err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
}
} else {
if err := h.settings.SetCore(body.Key, body.Value); err != nil {
var err error
if deleting {
err = h.settings.RemoveCore(body.Key)
} else {
err = h.settings.SetCore(body.Key, body.Value)
}
if err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
@ -1320,8 +1342,8 @@ func (h *Handler) writeOpenAIStream(w http.ResponseWriter, model, content, reaso
"model": model,
"choices": []map[string]interface{}{
{
"index": 0,
"delta": map[string]interface{}{},
"index": 0,
"delta": map[string]interface{}{},
"finish_reason": "stop",
},
},