From 1db860eba367cdc3ec7723139af27a0b94c86231 Mon Sep 17 00:00:00 2001 From: XYSK-lilong007 <267018309+XYSK-lilong007@users.noreply.github.com> Date: Fri, 13 Mar 2026 02:26:18 +0800 Subject: [PATCH 1/3] fix(config): load provider env vars (#836) --- pkg/config/config.go | 60 +++++++++++++++++++-------------------- pkg/config/config_test.go | 43 ++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 30 deletions(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index 7a7edb4894..e95d38902b 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -505,29 +505,29 @@ type VoiceConfig struct { } type ProvidersConfig struct { - Anthropic ProviderConfig `json:"anthropic"` - OpenAI OpenAIProviderConfig `json:"openai"` - LiteLLM ProviderConfig `json:"litellm"` - OpenRouter ProviderConfig `json:"openrouter"` - Groq ProviderConfig `json:"groq"` - Zhipu ProviderConfig `json:"zhipu"` - VLLM ProviderConfig `json:"vllm"` - Gemini ProviderConfig `json:"gemini"` - Nvidia ProviderConfig `json:"nvidia"` - Ollama ProviderConfig `json:"ollama"` - Moonshot ProviderConfig `json:"moonshot"` - ShengSuanYun ProviderConfig `json:"shengsuanyun"` - DeepSeek ProviderConfig `json:"deepseek"` - Cerebras ProviderConfig `json:"cerebras"` - Vivgrid ProviderConfig `json:"vivgrid"` - VolcEngine ProviderConfig `json:"volcengine"` - GitHubCopilot ProviderConfig `json:"github_copilot"` - Antigravity ProviderConfig `json:"antigravity"` - Qwen ProviderConfig `json:"qwen"` - Mistral ProviderConfig `json:"mistral"` - Avian ProviderConfig `json:"avian"` - Minimax ProviderConfig `json:"minimax"` - LongCat ProviderConfig `json:"longcat"` + Anthropic ProviderConfig `json:"anthropic" envPrefix:"PICOCLAW_PROVIDERS_ANTHROPIC_"` + OpenAI OpenAIProviderConfig `json:"openai" envPrefix:"PICOCLAW_PROVIDERS_OPENAI_"` + LiteLLM ProviderConfig `json:"litellm" envPrefix:"PICOCLAW_PROVIDERS_LITELLM_"` + OpenRouter ProviderConfig `json:"openrouter" envPrefix:"PICOCLAW_PROVIDERS_OPENROUTER_"` + Groq ProviderConfig `json:"groq" envPrefix:"PICOCLAW_PROVIDERS_GROQ_"` + Zhipu ProviderConfig `json:"zhipu" envPrefix:"PICOCLAW_PROVIDERS_ZHIPU_"` + VLLM ProviderConfig `json:"vllm" envPrefix:"PICOCLAW_PROVIDERS_VLLM_"` + Gemini ProviderConfig `json:"gemini" envPrefix:"PICOCLAW_PROVIDERS_GEMINI_"` + Nvidia ProviderConfig `json:"nvidia" envPrefix:"PICOCLAW_PROVIDERS_NVIDIA_"` + Ollama ProviderConfig `json:"ollama" envPrefix:"PICOCLAW_PROVIDERS_OLLAMA_"` + Moonshot ProviderConfig `json:"moonshot" envPrefix:"PICOCLAW_PROVIDERS_MOONSHOT_"` + ShengSuanYun ProviderConfig `json:"shengsuanyun" envPrefix:"PICOCLAW_PROVIDERS_SHENGSUANYUN_"` + DeepSeek ProviderConfig `json:"deepseek" envPrefix:"PICOCLAW_PROVIDERS_DEEPSEEK_"` + Cerebras ProviderConfig `json:"cerebras" envPrefix:"PICOCLAW_PROVIDERS_CEREBRAS_"` + Vivgrid ProviderConfig `json:"vivgrid" envPrefix:"PICOCLAW_PROVIDERS_VIVGRID_"` + VolcEngine ProviderConfig `json:"volcengine" envPrefix:"PICOCLAW_PROVIDERS_VOLCENGINE_"` + GitHubCopilot ProviderConfig `json:"github_copilot" envPrefix:"PICOCLAW_PROVIDERS_GITHUB_COPILOT_"` + Antigravity ProviderConfig `json:"antigravity" envPrefix:"PICOCLAW_PROVIDERS_ANTIGRAVITY_"` + Qwen ProviderConfig `json:"qwen" envPrefix:"PICOCLAW_PROVIDERS_QWEN_"` + Mistral ProviderConfig `json:"mistral" envPrefix:"PICOCLAW_PROVIDERS_MISTRAL_"` + Avian ProviderConfig `json:"avian" envPrefix:"PICOCLAW_PROVIDERS_AVIAN_"` + Minimax ProviderConfig `json:"minimax" envPrefix:"PICOCLAW_PROVIDERS_MINIMAX_"` + LongCat ProviderConfig `json:"longcat" envPrefix:"PICOCLAW_PROVIDERS_LONGCAT_"` } // IsEmpty checks if all provider configs are empty (no API keys or API bases set) @@ -569,17 +569,17 @@ func (p ProvidersConfig) MarshalJSON() ([]byte, error) { } type ProviderConfig struct { - APIKey string `json:"api_key" env:"PICOCLAW_PROVIDERS_{{.Name}}_API_KEY"` - APIBase string `json:"api_base" env:"PICOCLAW_PROVIDERS_{{.Name}}_API_BASE"` - Proxy string `json:"proxy,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_PROXY"` - RequestTimeout int `json:"request_timeout,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_REQUEST_TIMEOUT"` - AuthMethod string `json:"auth_method,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_AUTH_METHOD"` - ConnectMode string `json:"connect_mode,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_CONNECT_MODE"` // only for Github Copilot, `stdio` or `grpc` + APIKey string `json:"api_key" env:"API_KEY"` + APIBase string `json:"api_base" env:"API_BASE"` + Proxy string `json:"proxy,omitempty" env:"PROXY"` + RequestTimeout int `json:"request_timeout,omitempty" env:"REQUEST_TIMEOUT"` + AuthMethod string `json:"auth_method,omitempty" env:"AUTH_METHOD"` + ConnectMode string `json:"connect_mode,omitempty" env:"CONNECT_MODE"` // only for Github Copilot, `stdio` or `grpc` } type OpenAIProviderConfig struct { ProviderConfig - WebSearch bool `json:"web_search" env:"PICOCLAW_PROVIDERS_OPENAI_WEB_SEARCH"` + WebSearch bool `json:"web_search" env:"WEB_SEARCH"` } // ModelConfig represents a model-centric provider configuration. diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index ad89d6d2e7..13d9c4ed8d 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -460,6 +460,49 @@ func TestLoadConfig_WebToolsProxy(t *testing.T) { } } +func TestLoadConfig_ProviderEnvVarsOverrideFileValues(t *testing.T) { + tmpDir := t.TempDir() + configPath := filepath.Join(tmpDir, "config.json") + configJSON := `{ + "providers": { + "gemini": { + "api_key": "from-file" + } + } +}` + if err := os.WriteFile(configPath, []byte(configJSON), 0o600); err != nil { + t.Fatalf("os.WriteFile() error: %v", err) + } + + t.Setenv("PICOCLAW_PROVIDERS_GEMINI_API_KEY", "from-env") + + cfg, err := LoadConfig(configPath) + if err != nil { + t.Fatalf("LoadConfig() error: %v", err) + } + if cfg.Providers.Gemini.APIKey != "from-env" { + t.Fatalf("Providers.Gemini.APIKey = %q, want %q", cfg.Providers.Gemini.APIKey, "from-env") + } +} + +func TestLoadConfig_OpenAIProviderEnvVarsUseNestedPrefix(t *testing.T) { + tmpDir := t.TempDir() + configPath := filepath.Join(tmpDir, "config.json") + if err := os.WriteFile(configPath, []byte(`{}`), 0o600); err != nil { + t.Fatalf("os.WriteFile() error: %v", err) + } + + t.Setenv("PICOCLAW_PROVIDERS_OPENAI_WEB_SEARCH", "false") + + cfg, err := LoadConfig(configPath) + if err != nil { + t.Fatalf("LoadConfig() error: %v", err) + } + if cfg.Providers.OpenAI.WebSearch { + t.Fatal("Providers.OpenAI.WebSearch should be false when disabled via environment") + } +} + // TestDefaultConfig_DMScope verifies the default dm_scope value // TestDefaultConfig_SummarizationThresholds verifies summarization defaults func TestDefaultConfig_SummarizationThresholds(t *testing.T) { From b02da1a4ccd39f2c108724a58396ce6cc6ae6a14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=BE=99=200668001470?= Date: Mon, 16 Mar 2026 10:04:18 +0800 Subject: [PATCH 2/3] fix(config): apply provider env prefixes during load --- pkg/config/config.go | 14 +++++++++++--- pkg/config/config_test.go | 6 +++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index e95d38902b..2b88f7b11c 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -222,8 +222,8 @@ type AgentDefaults struct { RestrictToWorkspace bool `json:"restrict_to_workspace" env:"PICOCLAW_AGENTS_DEFAULTS_RESTRICT_TO_WORKSPACE"` AllowReadOutsideWorkspace bool `json:"allow_read_outside_workspace" env:"PICOCLAW_AGENTS_DEFAULTS_ALLOW_READ_OUTSIDE_WORKSPACE"` Provider string `json:"provider" env:"PICOCLAW_AGENTS_DEFAULTS_PROVIDER"` - ModelName string `json:"model_name,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL_NAME"` - Model string `json:"model" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL"` // Deprecated: use model_name instead + ModelName string `json:"model_name" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL_NAME"` + Model string `json:"model,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL"` // Deprecated: use model_name instead ModelFallbacks []string `json:"model_fallbacks,omitempty"` ImageModel string `json:"image_model,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_IMAGE_MODEL"` ImageModelFallbacks []string `json:"image_model_fallbacks,omitempty"` @@ -528,6 +528,7 @@ type ProvidersConfig struct { Avian ProviderConfig `json:"avian" envPrefix:"PICOCLAW_PROVIDERS_AVIAN_"` Minimax ProviderConfig `json:"minimax" envPrefix:"PICOCLAW_PROVIDERS_MINIMAX_"` LongCat ProviderConfig `json:"longcat" envPrefix:"PICOCLAW_PROVIDERS_LONGCAT_"` + ModelScope ProviderConfig `json:"modelscope" envPrefix:"PICOCLAW_PROVIDERS_MODELSCOPE_"` } // IsEmpty checks if all provider configs are empty (no API keys or API bases set) @@ -555,7 +556,8 @@ func (p ProvidersConfig) IsEmpty() bool { p.Mistral.APIKey == "" && p.Mistral.APIBase == "" && p.Avian.APIKey == "" && p.Avian.APIBase == "" && p.Minimax.APIKey == "" && p.Minimax.APIBase == "" && - p.LongCat.APIKey == "" && p.LongCat.APIBase == "" + p.LongCat.APIKey == "" && p.LongCat.APIBase == "" && + p.ModelScope.APIKey == "" && p.ModelScope.APIBase == "" } // MarshalJSON implements custom JSON marshaling for ProvidersConfig @@ -711,6 +713,7 @@ type ExecConfig struct { type SkillsToolsConfig struct { ToolConfig ` envPrefix:"PICOCLAW_TOOLS_SKILLS_"` Registries SkillsRegistriesConfig ` json:"registries"` + Github SkillsGithubConfig ` json:"github"` MaxConcurrentSearches int ` json:"max_concurrent_searches" env:"PICOCLAW_TOOLS_SKILLS_MAX_CONCURRENT_SEARCHES"` SearchCache SearchCacheConfig ` json:"search_cache"` } @@ -760,6 +763,11 @@ type SkillsRegistriesConfig struct { ClawHub ClawHubRegistryConfig `json:"clawhub"` } +type SkillsGithubConfig struct { + Token string `json:"token,omitempty" env:"PICOCLAW_TOOLS_SKILLS_GITHUB_AUTH_TOKEN"` + Proxy string `json:"proxy,omitempty" env:"PICOCLAW_TOOLS_SKILLS_GITHUB_PROXY"` +} + type ClawHubRegistryConfig struct { Enabled bool `json:"enabled" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_ENABLED"` BaseURL string `json:"base_url" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_BASE_URL"` diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 13d9c4ed8d..1cc6069305 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -342,8 +342,8 @@ func TestSaveConfig_IncludesEmptyLegacyModelField(t *testing.T) { t.Fatalf("ReadFile failed: %v", err) } - if !strings.Contains(string(data), `"model": ""`) { - t.Fatalf("saved config should include empty legacy model field, got: %s", string(data)) + if !strings.Contains(string(data), `"model_name": ""`) { + t.Fatalf("saved config should include empty legacy model_name field, got: %s", string(data)) } } @@ -444,7 +444,7 @@ func TestLoadConfig_WebToolsProxy(t *testing.T) { configPath := filepath.Join(tmpDir, "config.json") configJSON := `{ "agents": {"defaults":{"workspace":"./workspace","model":"gpt4","max_tokens":8192,"max_tool_iterations":20}}, - "model_list": [{"model_name":"gpt4","model":"openai/gpt-5.2","api_key":"x"}], + "model_list": [{"model_name":"gpt4","model":"openai/gpt-5.4","api_key":"x"}], "tools": {"web":{"proxy":"http://127.0.0.1:7890"}} }` if err := os.WriteFile(configPath, []byte(configJSON), 0o600); err != nil { From 41a898b4211b95393e2003a44744ab7533a70e85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=BE=99=200668001470?= Date: Mon, 16 Mar 2026 10:36:05 +0800 Subject: [PATCH 3/3] fix(config): apply provider env overrides after parse --- pkg/config/config.go | 127 ++++++++++++++++++++++++++++++++----------- 1 file changed, 96 insertions(+), 31 deletions(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index 2b88f7b11c..ffda5e6bda 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -4,6 +4,8 @@ import ( "encoding/json" "fmt" "os" + "reflect" + "strconv" "strings" "sync/atomic" @@ -505,30 +507,30 @@ type VoiceConfig struct { } type ProvidersConfig struct { - Anthropic ProviderConfig `json:"anthropic" envPrefix:"PICOCLAW_PROVIDERS_ANTHROPIC_"` - OpenAI OpenAIProviderConfig `json:"openai" envPrefix:"PICOCLAW_PROVIDERS_OPENAI_"` - LiteLLM ProviderConfig `json:"litellm" envPrefix:"PICOCLAW_PROVIDERS_LITELLM_"` - OpenRouter ProviderConfig `json:"openrouter" envPrefix:"PICOCLAW_PROVIDERS_OPENROUTER_"` - Groq ProviderConfig `json:"groq" envPrefix:"PICOCLAW_PROVIDERS_GROQ_"` - Zhipu ProviderConfig `json:"zhipu" envPrefix:"PICOCLAW_PROVIDERS_ZHIPU_"` - VLLM ProviderConfig `json:"vllm" envPrefix:"PICOCLAW_PROVIDERS_VLLM_"` - Gemini ProviderConfig `json:"gemini" envPrefix:"PICOCLAW_PROVIDERS_GEMINI_"` - Nvidia ProviderConfig `json:"nvidia" envPrefix:"PICOCLAW_PROVIDERS_NVIDIA_"` - Ollama ProviderConfig `json:"ollama" envPrefix:"PICOCLAW_PROVIDERS_OLLAMA_"` - Moonshot ProviderConfig `json:"moonshot" envPrefix:"PICOCLAW_PROVIDERS_MOONSHOT_"` - ShengSuanYun ProviderConfig `json:"shengsuanyun" envPrefix:"PICOCLAW_PROVIDERS_SHENGSUANYUN_"` - DeepSeek ProviderConfig `json:"deepseek" envPrefix:"PICOCLAW_PROVIDERS_DEEPSEEK_"` - Cerebras ProviderConfig `json:"cerebras" envPrefix:"PICOCLAW_PROVIDERS_CEREBRAS_"` - Vivgrid ProviderConfig `json:"vivgrid" envPrefix:"PICOCLAW_PROVIDERS_VIVGRID_"` - VolcEngine ProviderConfig `json:"volcengine" envPrefix:"PICOCLAW_PROVIDERS_VOLCENGINE_"` - GitHubCopilot ProviderConfig `json:"github_copilot" envPrefix:"PICOCLAW_PROVIDERS_GITHUB_COPILOT_"` - Antigravity ProviderConfig `json:"antigravity" envPrefix:"PICOCLAW_PROVIDERS_ANTIGRAVITY_"` - Qwen ProviderConfig `json:"qwen" envPrefix:"PICOCLAW_PROVIDERS_QWEN_"` - Mistral ProviderConfig `json:"mistral" envPrefix:"PICOCLAW_PROVIDERS_MISTRAL_"` - Avian ProviderConfig `json:"avian" envPrefix:"PICOCLAW_PROVIDERS_AVIAN_"` - Minimax ProviderConfig `json:"minimax" envPrefix:"PICOCLAW_PROVIDERS_MINIMAX_"` - LongCat ProviderConfig `json:"longcat" envPrefix:"PICOCLAW_PROVIDERS_LONGCAT_"` - ModelScope ProviderConfig `json:"modelscope" envPrefix:"PICOCLAW_PROVIDERS_MODELSCOPE_"` + Anthropic ProviderConfig `json:"anthropic"` + OpenAI OpenAIProviderConfig `json:"openai"` + LiteLLM ProviderConfig `json:"litellm"` + OpenRouter ProviderConfig `json:"openrouter"` + Groq ProviderConfig `json:"groq"` + Zhipu ProviderConfig `json:"zhipu"` + VLLM ProviderConfig `json:"vllm"` + Gemini ProviderConfig `json:"gemini"` + Nvidia ProviderConfig `json:"nvidia"` + Ollama ProviderConfig `json:"ollama"` + Moonshot ProviderConfig `json:"moonshot"` + ShengSuanYun ProviderConfig `json:"shengsuanyun"` + DeepSeek ProviderConfig `json:"deepseek"` + Cerebras ProviderConfig `json:"cerebras"` + Vivgrid ProviderConfig `json:"vivgrid"` + VolcEngine ProviderConfig `json:"volcengine"` + GitHubCopilot ProviderConfig `json:"github_copilot"` + Antigravity ProviderConfig `json:"antigravity"` + Qwen ProviderConfig `json:"qwen"` + Mistral ProviderConfig `json:"mistral"` + Avian ProviderConfig `json:"avian"` + Minimax ProviderConfig `json:"minimax"` + LongCat ProviderConfig `json:"longcat"` + ModelScope ProviderConfig `json:"modelscope"` } // IsEmpty checks if all provider configs are empty (no API keys or API bases set) @@ -571,17 +573,17 @@ func (p ProvidersConfig) MarshalJSON() ([]byte, error) { } type ProviderConfig struct { - APIKey string `json:"api_key" env:"API_KEY"` - APIBase string `json:"api_base" env:"API_BASE"` - Proxy string `json:"proxy,omitempty" env:"PROXY"` - RequestTimeout int `json:"request_timeout,omitempty" env:"REQUEST_TIMEOUT"` - AuthMethod string `json:"auth_method,omitempty" env:"AUTH_METHOD"` - ConnectMode string `json:"connect_mode,omitempty" env:"CONNECT_MODE"` // only for Github Copilot, `stdio` or `grpc` + APIKey string `json:"api_key" env:"PICOCLAW_PROVIDERS_{{.Name}}_API_KEY"` + APIBase string `json:"api_base" env:"PICOCLAW_PROVIDERS_{{.Name}}_API_BASE"` + Proxy string `json:"proxy,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_PROXY"` + RequestTimeout int `json:"request_timeout,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_REQUEST_TIMEOUT"` + AuthMethod string `json:"auth_method,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_AUTH_METHOD"` + ConnectMode string `json:"connect_mode,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_CONNECT_MODE"` // only for Github Copilot, `stdio` or `grpc` } type OpenAIProviderConfig struct { ProviderConfig - WebSearch bool `json:"web_search" env:"WEB_SEARCH"` + WebSearch bool `json:"web_search" env:"PICOCLAW_PROVIDERS_OPENAI_WEB_SEARCH"` } // ModelConfig represents a model-centric provider configuration. @@ -840,6 +842,7 @@ func LoadConfig(path string) (*Config, error) { if err := env.Parse(cfg); err != nil { return nil, err } + cfg.applyProviderEnvOverrides() // Migrate legacy channel config fields to new unified structures cfg.migrateChannelConfigs() @@ -857,6 +860,68 @@ func LoadConfig(path string) (*Config, error) { return cfg, nil } +func (c *Config) applyProviderEnvOverrides() { + providersValue := reflect.ValueOf(&c.Providers).Elem() + providersType := providersValue.Type() + + for i := 0; i < providersValue.NumField(); i++ { + fieldValue := providersValue.Field(i) + fieldType := providersType.Field(i) + jsonName := strings.Split(fieldType.Tag.Get("json"), ",")[0] + if jsonName == "" { + continue + } + prefix := "PICOCLAW_PROVIDERS_" + strings.ToUpper(jsonName) + "_" + applyProviderConfigEnv(fieldValue, prefix) + } +} + +func applyProviderConfigEnv(fieldValue reflect.Value, prefix string) { + if fieldValue.Kind() != reflect.Struct { + return + } + + if nested := fieldValue.FieldByName("ProviderConfig"); nested.IsValid() { + applyProviderConfigFields(nested, prefix) + } else { + applyProviderConfigFields(fieldValue, prefix) + } + + if webSearch := fieldValue.FieldByName("WebSearch"); webSearch.IsValid() && webSearch.CanSet() { + if raw, ok := os.LookupEnv(prefix + "WEB_SEARCH"); ok { + if parsed, err := strconv.ParseBool(raw); err == nil { + webSearch.SetBool(parsed) + } + } + } +} + +func applyProviderConfigFields(fieldValue reflect.Value, prefix string) { + setStringFieldFromEnv(fieldValue, "APIKey", prefix+"API_KEY") + setStringFieldFromEnv(fieldValue, "APIBase", prefix+"API_BASE") + setStringFieldFromEnv(fieldValue, "Proxy", prefix+"PROXY") + setStringFieldFromEnv(fieldValue, "AuthMethod", prefix+"AUTH_METHOD") + setStringFieldFromEnv(fieldValue, "ConnectMode", prefix+"CONNECT_MODE") + + if requestTimeout := fieldValue.FieldByName("RequestTimeout"); requestTimeout.IsValid() && requestTimeout.CanSet() { + if raw, ok := os.LookupEnv(prefix + "REQUEST_TIMEOUT"); ok { + if parsed, err := strconv.Atoi(raw); err == nil { + requestTimeout.SetInt(int64(parsed)) + } + } + } +} + +func setStringFieldFromEnv(fieldValue reflect.Value, fieldName, envName string) { + target := fieldValue.FieldByName(fieldName) + if !target.IsValid() || !target.CanSet() { + return + } + if raw, ok := os.LookupEnv(envName); ok { + target.SetString(raw) + } +} + func (c *Config) migrateChannelConfigs() { // Discord: mention_only -> group_trigger.mention_only if c.Channels.Discord.MentionOnly && !c.Channels.Discord.GroupTrigger.MentionOnly {