package settings import ( "encoding/json" "net/http" "strings" "time" authn "ds2api/internal/auth" "ds2api/internal/config" ) func (h *Handler) updateSettings(w http.ResponseWriter, r *http.Request) { var req map[string]any if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, map[string]any{"detail": "invalid json"}) return } adminCfg, runtimeCfg, responsesCfg, embeddingsCfg, autoDeleteCfg, currentInputCfg, thinkingInjCfg, aliasMap, err := parseSettingsUpdateRequest(req) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]any{"detail": err.Error()}) return } if runtimeCfg != nil { if err := validateMergedRuntimeSettings(h.Store.Snapshot().Runtime, runtimeCfg); err != nil { writeJSON(w, http.StatusBadRequest, map[string]any{"detail": err.Error()}) return } } currentInputEnabledSet := hasNestedSettingsKey(req, "current_input_file", "enabled") currentInputMinCharsSet := hasNestedSettingsKey(req, "current_input_file", "min_chars") thinkingInjectionEnabledSet := hasNestedSettingsKey(req, "thinking_injection", "enabled") thinkingInjectionPromptSet := hasNestedSettingsKey(req, "thinking_injection", "prompt") if err := h.Store.Update(func(c *config.Config) error { if adminCfg != nil { if adminCfg.JWTExpireHours > 0 { c.Admin.JWTExpireHours = adminCfg.JWTExpireHours } } if runtimeCfg != nil { if runtimeCfg.AccountMaxInflight > 0 { c.Runtime.AccountMaxInflight = runtimeCfg.AccountMaxInflight } if runtimeCfg.AccountMaxQueue > 0 { c.Runtime.AccountMaxQueue = runtimeCfg.AccountMaxQueue } if runtimeCfg.GlobalMaxInflight > 0 { c.Runtime.GlobalMaxInflight = runtimeCfg.GlobalMaxInflight } if runtimeCfg.TokenRefreshIntervalHours > 0 { c.Runtime.TokenRefreshIntervalHours = runtimeCfg.TokenRefreshIntervalHours } } if responsesCfg != nil && responsesCfg.StoreTTLSeconds > 0 { c.Responses.StoreTTLSeconds = responsesCfg.StoreTTLSeconds } if embeddingsCfg != nil && strings.TrimSpace(embeddingsCfg.Provider) != "" { c.Embeddings.Provider = strings.TrimSpace(embeddingsCfg.Provider) } if autoDeleteCfg != nil { c.AutoDelete.Mode = autoDeleteCfg.Mode c.AutoDelete.Sessions = autoDeleteCfg.Sessions } if currentInputCfg != nil { if currentInputEnabledSet { c.CurrentInputFile.Enabled = currentInputCfg.Enabled } if currentInputMinCharsSet { c.CurrentInputFile.MinChars = currentInputCfg.MinChars } } if thinkingInjCfg != nil { if thinkingInjectionEnabledSet { c.ThinkingInjection.Enabled = thinkingInjCfg.Enabled } if thinkingInjectionPromptSet { c.ThinkingInjection.Prompt = thinkingInjCfg.Prompt } } if aliasMap != nil { c.ModelAliases = aliasMap } return nil }); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]any{"detail": err.Error()}) return } h.applyRuntimeSettings() needsSync := config.IsVercel() || h.Store.IsEnvBacked() writeJSON(w, http.StatusOK, map[string]any{ "success": true, "message": "settings updated and hot reloaded", "env_backed": h.Store.IsEnvBacked(), "needs_vercel_sync": needsSync, "manual_sync_message": "配置已保存。Vercel 部署请在 Vercel Sync 页面手动同步。", }) } func (h *Handler) updateSettingsPassword(w http.ResponseWriter, r *http.Request) { var req map[string]any if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, map[string]any{"detail": "invalid json"}) return } newPassword := strings.TrimSpace(fieldString(req, "new_password")) if newPassword == "" { newPassword = strings.TrimSpace(fieldString(req, "password")) } if len(newPassword) < 4 { writeJSON(w, http.StatusBadRequest, map[string]any{"detail": "new password must be at least 4 characters"}) return } now := time.Now().Unix() hash := authn.HashAdminPassword(newPassword) if err := h.Store.Update(func(c *config.Config) error { c.Admin.PasswordHash = hash c.Admin.JWTValidAfterUnix = now return nil }); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]any{"detail": err.Error()}) return } writeJSON(w, http.StatusOK, map[string]any{ "success": true, "message": "password updated", "force_relogin": true, "jwt_valid_after_unix": now, }) } func hasNestedSettingsKey(req map[string]any, section, key string) bool { raw, ok := req[section].(map[string]any) if !ok { return false } _, exists := raw[key] return exists }