mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-06-10 18:11:00 +00:00
d874266e87
* feat(providers): add Ollama local and Ollama Cloud provider support Adds two new provider variants: ollama — local/self-hosted Ollama instance - Gated on providers.ollama.host in config (or GOCLAW_OLLAMA_HOST env) - No API key required; Ollama's OpenAI-compat endpoint accepts any Bearer value - Defaults to http://localhost:11434/v1 (configurable for LAN/remote hosts) - Default model: llama3.3 ollama-cloud — Ollama Cloud (managed remote inference) - Gated on providers.ollama_cloud.api_key (or GOCLAW_OLLAMA_CLOUD_API_KEY env) - Bearer token from ollama.com/settings/keys - Default base URL: https://ollama.com/v1 (overridable via api_base) - Default model: llama3.3 Both variants use the existing NewOpenAIProvider (OpenAI-compat) — no new provider struct needed. Both are registered from config file and DB (via llm_providers table with ProviderOllama / ProviderOllamaCloud types). OllamaCloud.APIKey follows all existing secret handling patterns: MaskedCopy, StripSecrets, StripMaskedSecrets. * feat(providers): wire Ollama into web UI and fix DB registration - Add ollama + ollama_cloud to PROVIDER_TYPES constants (dropdowns) - Fix setup wizard: skip API key requirement for Ollama local (isOllama) - Fix bootstrap status: recognize Ollama local as no-API-key provider - Add ollama_cloud to config-page KNOWN_PROVIDERS list - Fix gateway_providers.go: move ProviderOllama before APIKey=='' guard so DB-registered local Ollama providers actually register at startup (same pattern as ClaudeCLI, which also needs no API key)
219 lines
6.6 KiB
Go
219 lines
6.6 KiB
Go
package config
|
|
|
|
import "encoding/json"
|
|
|
|
const secretMask = "***"
|
|
|
|
// MaskedCopy returns a deep copy of the config with all secret fields masked.
|
|
// Used by config.get to avoid exposing secrets to WebSocket clients.
|
|
func (c *Config) MaskedCopy() *Config {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
|
|
// Deep copy via JSON round-trip
|
|
data, err := json.Marshal(c)
|
|
if err != nil {
|
|
return &Config{}
|
|
}
|
|
cp := Default()
|
|
if err := json.Unmarshal(data, cp); err != nil {
|
|
return &Config{}
|
|
}
|
|
|
|
// Mask provider API keys
|
|
maskNonEmpty(&cp.Providers.Anthropic.APIKey)
|
|
maskNonEmpty(&cp.Providers.OpenAI.APIKey)
|
|
maskNonEmpty(&cp.Providers.OpenRouter.APIKey)
|
|
maskNonEmpty(&cp.Providers.Groq.APIKey)
|
|
maskNonEmpty(&cp.Providers.DeepSeek.APIKey)
|
|
maskNonEmpty(&cp.Providers.Gemini.APIKey)
|
|
maskNonEmpty(&cp.Providers.Mistral.APIKey)
|
|
maskNonEmpty(&cp.Providers.XAI.APIKey)
|
|
maskNonEmpty(&cp.Providers.MiniMax.APIKey)
|
|
maskNonEmpty(&cp.Providers.Cohere.APIKey)
|
|
maskNonEmpty(&cp.Providers.Perplexity.APIKey)
|
|
maskNonEmpty(&cp.Providers.DashScope.APIKey)
|
|
maskNonEmpty(&cp.Providers.Bailian.APIKey)
|
|
maskNonEmpty(&cp.Providers.Zai.APIKey)
|
|
maskNonEmpty(&cp.Providers.ZaiCoding.APIKey)
|
|
maskNonEmpty(&cp.Providers.OllamaCloud.APIKey)
|
|
|
|
// Mask gateway token
|
|
maskNonEmpty(&cp.Gateway.Token)
|
|
|
|
// Mask channel secrets
|
|
maskNonEmpty(&cp.Channels.Telegram.Token)
|
|
maskNonEmpty(&cp.Channels.Discord.Token)
|
|
maskNonEmpty(&cp.Channels.Slack.BotToken)
|
|
maskNonEmpty(&cp.Channels.Slack.AppToken)
|
|
maskNonEmpty(&cp.Channels.Zalo.Token)
|
|
maskNonEmpty(&cp.Channels.Zalo.WebhookSecret)
|
|
maskNonEmpty(&cp.Channels.Feishu.AppID)
|
|
maskNonEmpty(&cp.Channels.Feishu.AppSecret)
|
|
maskNonEmpty(&cp.Channels.Feishu.EncryptKey)
|
|
maskNonEmpty(&cp.Channels.Feishu.VerificationToken)
|
|
|
|
// Mask TTS API keys
|
|
maskNonEmpty(&cp.Tts.OpenAI.APIKey)
|
|
maskNonEmpty(&cp.Tts.ElevenLabs.APIKey)
|
|
maskNonEmpty(&cp.Tts.MiniMax.APIKey)
|
|
|
|
// Mask web tool keys
|
|
maskNonEmpty(&cp.Tools.Web.Brave.APIKey)
|
|
|
|
// Mask Tailscale auth key
|
|
maskNonEmpty(&cp.Tailscale.AuthKey)
|
|
|
|
return cp
|
|
}
|
|
|
|
// StripSecrets zeros out all secret fields in the config.
|
|
// Used before saving to disk to ensure secrets never persist in config.json.
|
|
func (c *Config) StripSecrets() {
|
|
// Provider API keys
|
|
c.Providers.Anthropic.APIKey = ""
|
|
c.Providers.OpenAI.APIKey = ""
|
|
c.Providers.OpenRouter.APIKey = ""
|
|
c.Providers.Groq.APIKey = ""
|
|
c.Providers.DeepSeek.APIKey = ""
|
|
c.Providers.Gemini.APIKey = ""
|
|
c.Providers.Mistral.APIKey = ""
|
|
c.Providers.XAI.APIKey = ""
|
|
c.Providers.MiniMax.APIKey = ""
|
|
c.Providers.Cohere.APIKey = ""
|
|
c.Providers.Perplexity.APIKey = ""
|
|
c.Providers.DashScope.APIKey = ""
|
|
c.Providers.Bailian.APIKey = ""
|
|
c.Providers.Zai.APIKey = ""
|
|
c.Providers.ZaiCoding.APIKey = ""
|
|
c.Providers.OllamaCloud.APIKey = ""
|
|
|
|
// Gateway token
|
|
c.Gateway.Token = ""
|
|
|
|
// Channel secrets
|
|
c.Channels.Telegram.Token = ""
|
|
c.Channels.Discord.Token = ""
|
|
c.Channels.Slack.BotToken = ""
|
|
c.Channels.Slack.AppToken = ""
|
|
c.Channels.Zalo.Token = ""
|
|
c.Channels.Zalo.WebhookSecret = ""
|
|
c.Channels.Feishu.AppID = ""
|
|
c.Channels.Feishu.AppSecret = ""
|
|
c.Channels.Feishu.EncryptKey = ""
|
|
c.Channels.Feishu.VerificationToken = ""
|
|
|
|
// TTS API keys
|
|
c.Tts.OpenAI.APIKey = ""
|
|
c.Tts.ElevenLabs.APIKey = ""
|
|
c.Tts.MiniMax.APIKey = ""
|
|
|
|
// Web tool keys
|
|
c.Tools.Web.Brave.APIKey = ""
|
|
|
|
// Tailscale auth key
|
|
c.Tailscale.AuthKey = ""
|
|
}
|
|
|
|
// StripMaskedSecrets strips only fields that still contain the mask value "***".
|
|
// Real values (user-entered via UI) are preserved, so that secrets entered
|
|
// via the config UI persist in config.json.
|
|
func (c *Config) StripMaskedSecrets() {
|
|
stripIfMasked := func(s *string) {
|
|
if *s == secretMask {
|
|
*s = ""
|
|
}
|
|
}
|
|
|
|
// Provider API keys
|
|
stripIfMasked(&c.Providers.Anthropic.APIKey)
|
|
stripIfMasked(&c.Providers.OpenAI.APIKey)
|
|
stripIfMasked(&c.Providers.OpenRouter.APIKey)
|
|
stripIfMasked(&c.Providers.Groq.APIKey)
|
|
stripIfMasked(&c.Providers.DeepSeek.APIKey)
|
|
stripIfMasked(&c.Providers.Gemini.APIKey)
|
|
stripIfMasked(&c.Providers.Mistral.APIKey)
|
|
stripIfMasked(&c.Providers.XAI.APIKey)
|
|
stripIfMasked(&c.Providers.MiniMax.APIKey)
|
|
stripIfMasked(&c.Providers.Cohere.APIKey)
|
|
stripIfMasked(&c.Providers.Perplexity.APIKey)
|
|
stripIfMasked(&c.Providers.DashScope.APIKey)
|
|
stripIfMasked(&c.Providers.Bailian.APIKey)
|
|
stripIfMasked(&c.Providers.Zai.APIKey)
|
|
stripIfMasked(&c.Providers.ZaiCoding.APIKey)
|
|
stripIfMasked(&c.Providers.OllamaCloud.APIKey)
|
|
|
|
// Gateway token
|
|
stripIfMasked(&c.Gateway.Token)
|
|
|
|
// Channel secrets
|
|
stripIfMasked(&c.Channels.Telegram.Token)
|
|
stripIfMasked(&c.Channels.Discord.Token)
|
|
stripIfMasked(&c.Channels.Slack.BotToken)
|
|
stripIfMasked(&c.Channels.Slack.AppToken)
|
|
stripIfMasked(&c.Channels.Zalo.Token)
|
|
stripIfMasked(&c.Channels.Zalo.WebhookSecret)
|
|
stripIfMasked(&c.Channels.Feishu.AppID)
|
|
stripIfMasked(&c.Channels.Feishu.AppSecret)
|
|
stripIfMasked(&c.Channels.Feishu.EncryptKey)
|
|
stripIfMasked(&c.Channels.Feishu.VerificationToken)
|
|
|
|
// TTS API keys
|
|
stripIfMasked(&c.Tts.OpenAI.APIKey)
|
|
stripIfMasked(&c.Tts.ElevenLabs.APIKey)
|
|
stripIfMasked(&c.Tts.MiniMax.APIKey)
|
|
|
|
// Web tool keys
|
|
stripIfMasked(&c.Tools.Web.Brave.APIKey)
|
|
|
|
// Tailscale auth key
|
|
stripIfMasked(&c.Tailscale.AuthKey)
|
|
}
|
|
|
|
// ApplyDBSecrets overlays secrets from the config_secrets table onto the config.
|
|
// Called before ApplyEnvOverrides() — env vars take highest precedence.
|
|
// Precedence chain: config.json defaults → DB secrets → env vars.
|
|
func (c *Config) ApplyDBSecrets(secrets map[string]string) {
|
|
apply := func(key string, dst *string) {
|
|
if v, ok := secrets[key]; ok && v != "" {
|
|
*dst = v
|
|
}
|
|
}
|
|
|
|
apply("gateway.token", &c.Gateway.Token)
|
|
apply("tts.openai.api_key", &c.Tts.OpenAI.APIKey)
|
|
apply("tts.elevenlabs.api_key", &c.Tts.ElevenLabs.APIKey)
|
|
apply("tts.minimax.api_key", &c.Tts.MiniMax.APIKey)
|
|
apply("tts.minimax.group_id", &c.Tts.MiniMax.GroupID)
|
|
apply("tools.web.brave.api_key", &c.Tools.Web.Brave.APIKey)
|
|
apply("tailscale.auth_key", &c.Tailscale.AuthKey)
|
|
}
|
|
|
|
// ExtractDBSecrets returns the config_secrets key-value pairs from the config.
|
|
// Saves secrets to the config_secrets table.
|
|
func (c *Config) ExtractDBSecrets() map[string]string {
|
|
secrets := make(map[string]string)
|
|
|
|
collect := func(key, value string) {
|
|
if value != "" && value != secretMask {
|
|
secrets[key] = value
|
|
}
|
|
}
|
|
|
|
collect("gateway.token", c.Gateway.Token)
|
|
collect("tts.openai.api_key", c.Tts.OpenAI.APIKey)
|
|
collect("tts.elevenlabs.api_key", c.Tts.ElevenLabs.APIKey)
|
|
collect("tts.minimax.api_key", c.Tts.MiniMax.APIKey)
|
|
collect("tts.minimax.group_id", c.Tts.MiniMax.GroupID)
|
|
collect("tools.web.brave.api_key", c.Tools.Web.Brave.APIKey)
|
|
collect("tailscale.auth_key", c.Tailscale.AuthKey)
|
|
|
|
return secrets
|
|
}
|
|
|
|
func maskNonEmpty(s *string) {
|
|
if *s != "" {
|
|
*s = secretMask
|
|
}
|
|
}
|