package service import ( "fmt" "AI-Expert-Sidebar/internal/config" "AI-Expert-Sidebar/internal/crypto" "AI-Expert-Sidebar/internal/database" "AI-Expert-Sidebar/internal/models" ) // SettingsDTO is what the frontend reads and writes. type SettingsDTO struct { AIProvider string `json:"ai_provider"` BaseURL string `json:"base_url"` APIKey string `json:"api_key"` Model string `json:"model"` SystemPrompt string `json:"system_prompt"` MaxTokens int `json:"max_tokens"` UsePublicKey bool `json:"use_public_key"` } // GetSettings reads AI config from settings.db key-value store. func GetSettings() *SettingsDTO { sdb := database.GetSettings() if sdb == nil { return defaultDTO() } var rows []models.AppSetting sdb.Find(&rows) m := make(map[string]string, len(rows)) for _, r := range rows { m[r.Key] = r.Value } apiKey, _ := crypto.DecryptAPIKey(m["api_key_encrypted"]) maxTokens := 1024 fmt.Sscanf(m["max_tokens"], "%d", &maxTokens) if maxTokens <= 0 { maxTokens = 1024 } return &SettingsDTO{ AIProvider: strOr(m["ai_provider"], "deepseek"), BaseURL: m["base_url"], APIKey: apiKey, Model: strOr(m["model"], "deepseek-chat"), SystemPrompt: m["system_prompt"], MaxTokens: maxTokens, UsePublicKey: m["use_public_key"] != "false", } } // SaveSettings persists AI config into settings.db. func SaveSettings(dto SettingsDTO) error { sdb := database.GetSettings() if sdb == nil { return fmt.Errorf("settings DB not ready") } upsert := func(k, v string) { sdb.Exec("INSERT INTO app_settings(key,value) VALUES(?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value", k, v) } upsert("ai_provider", dto.AIProvider) upsert("base_url", dto.BaseURL) upsert("model", dto.Model) upsert("system_prompt", dto.SystemPrompt) upsert("max_tokens", fmt.Sprintf("%d", dto.MaxTokens)) usePublic := "true" if !dto.UsePublicKey { usePublic = "false" } upsert("use_public_key", usePublic) if !dto.UsePublicKey && dto.APIKey != "" { enc, err := crypto.EncryptAPIKey(dto.APIKey) if err != nil { return err } upsert("api_key_encrypted", enc) } return nil } // ResolveAIConfig returns the effective AI call config (local settings or global fallback). func ResolveAIConfig() AICallConfig { base := AICallConfig{ BaseURL: "https://api.deepseek.com/chat/completions", APIKey: config.Global.DeepSeek.APIKey, Model: config.Global.DeepSeek.Model, MaxTokens: config.Global.DeepSeek.MaxTokens, } dto := GetSettings() if dto == nil { return base } base.SystemPrompt = dto.SystemPrompt if dto.UsePublicKey || dto.APIKey == "" { return base } providerURL := dto.BaseURL if providerURL == "" { switch dto.AIProvider { case "deepseek": providerURL = "https://api.deepseek.com/chat/completions" case "openai": providerURL = "https://api.openai.com/v1/chat/completions" case "grok": providerURL = "https://api.x.ai/v1/chat/completions" } } maxTok := dto.MaxTokens if maxTok <= 0 { maxTok = 1024 } return AICallConfig{ BaseURL: strOr(providerURL, base.BaseURL), APIKey: dto.APIKey, Model: strOr(dto.Model, base.Model), MaxTokens: maxTok, SystemPrompt: dto.SystemPrompt, } } func strOr(v, def string) string { if v == "" { return def } return v } func defaultDTO() *SettingsDTO { return &SettingsDTO{AIProvider: "deepseek", Model: "deepseek-chat", MaxTokens: 1024, UsePublicKey: true} }