Files
LaodingBot/internal/config/config.go

431 lines
13 KiB
Go
Raw Normal View History

2026-02-21 23:01:39 +08:00
package config
import (
"bufio"
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
)
type Config struct {
MessageChannel string
LogLevel string
SoulPath string
SkillsDir string
ReactMaxSteps int
ToolCallTimeoutSec int
ToolOutputMaxChars int
EnableCapabilityGap bool
AutoSkillDir string
GapDraftTriggerCount int
GapClusterLookbackHours int
2026-02-21 23:01:39 +08:00
Telegram TelegramConfig
Feishu FeishuConfig
WebUI WebUIConfig
LLM LLMConfig
Security SecurityConfig
WebSearch WebSearchConfig
feat: implement streaming chat, skill routing, and SAFe PI planning tools - Add /api/chat/stream endpoint with Server-Sent Events (SSE) for real-time message streaming * Implement StreamEvent types (thought, tool_call, tool_result, final, error) * Add StreamEventCallback mechanism for event propagation * Create StreamChatHandler in webui/bot with proper HTTP headers and flushing - Implement LLM-based skill router for intelligent capability selection * Add optional routerLLM client for semantic routing * Implement routeSkillsWithLLM() to match user intent to available skills * Add matchSkillsByName() for fuzzy skill matching * Update buildUnifiedSystemPrompt() to use routed skills - Add streaming support to ReAct pipeline * Implement runUnifiedReActStream() for streaming thought/action/observation * Emit StreamEvent at each ReAct step * Support callback error handling in streaming mode - Integrate three new DevOps tools * tools/filedoc: Extract document content from file_id via OpenAI * tools/giteaticket: Create Gitea issues from PI plan items with SAFe metadata * tools/piplan: Publish PI planning blueprints with dependency tracking - Add SAFe PI Planning skill * Implement PM/SA/RTE (iron triangle) workflow * Support for Feature, Enabler, and Dependency definition * Automatic task decomposition and Gitea integration - Create frontend integration documentation * Complete SSE protocol specification * TypeScript fetch + ReadableStream example * LLM-ready refactoring template for other projects - Simplify file handling * Remove legacy file context structures and dual-mode processing * Consolidate file operations into UploadAndCacheFiles() * Remove FilePromptMode configuration and related complexity - Update configuration * Add Router model support (LLM_ROUTER_MODEL) * Add Gitea configuration (BaseURL, Token, Owner, Repo) * WebSearch and additional tool infrastructure Tests: All 22 test packages passing, 8/8 webui tests including 3 new stream tests
2026-03-11 17:58:19 +08:00
Gitea GiteaConfig
2026-02-21 23:01:39 +08:00
SQLitePath string
}
type TelegramConfig struct {
Token string
PollTimeoutSeconds int
}
type FeishuConfig struct {
AppID string
AppSecret string
VerifyToken string
ListenAddr string
EventPath string
}
type WebUIConfig struct {
ListenAddr string
MaxUploadBytes int64
}
2026-02-21 23:01:39 +08:00
type LLMConfig struct {
feat: implement streaming chat, skill routing, and SAFe PI planning tools - Add /api/chat/stream endpoint with Server-Sent Events (SSE) for real-time message streaming * Implement StreamEvent types (thought, tool_call, tool_result, final, error) * Add StreamEventCallback mechanism for event propagation * Create StreamChatHandler in webui/bot with proper HTTP headers and flushing - Implement LLM-based skill router for intelligent capability selection * Add optional routerLLM client for semantic routing * Implement routeSkillsWithLLM() to match user intent to available skills * Add matchSkillsByName() for fuzzy skill matching * Update buildUnifiedSystemPrompt() to use routed skills - Add streaming support to ReAct pipeline * Implement runUnifiedReActStream() for streaming thought/action/observation * Emit StreamEvent at each ReAct step * Support callback error handling in streaming mode - Integrate three new DevOps tools * tools/filedoc: Extract document content from file_id via OpenAI * tools/giteaticket: Create Gitea issues from PI plan items with SAFe metadata * tools/piplan: Publish PI planning blueprints with dependency tracking - Add SAFe PI Planning skill * Implement PM/SA/RTE (iron triangle) workflow * Support for Feature, Enabler, and Dependency definition * Automatic task decomposition and Gitea integration - Create frontend integration documentation * Complete SSE protocol specification * TypeScript fetch + ReadableStream example * LLM-ready refactoring template for other projects - Simplify file handling * Remove legacy file context structures and dual-mode processing * Consolidate file operations into UploadAndCacheFiles() * Remove FilePromptMode configuration and related complexity - Update configuration * Add Router model support (LLM_ROUTER_MODEL) * Add Gitea configuration (BaseURL, Token, Owner, Repo) * WebSearch and additional tool infrastructure Tests: All 22 test packages passing, 8/8 webui tests including 3 new stream tests
2026-03-11 17:58:19 +08:00
BaseURL string
APIKey string
Model string
FileModel string
RouterModel string // 轻量路由模型,用于技能意图路由;为空则仅用关键词匹配
2026-02-21 23:01:39 +08:00
}
type SecurityConfig struct {
AllowedDirs []string
AllowedCommands []string
WorkDir string
}
type WebSearchConfig struct {
Engine string // "duckduckgo" or "brave"
APIKey string
}
feat: implement streaming chat, skill routing, and SAFe PI planning tools - Add /api/chat/stream endpoint with Server-Sent Events (SSE) for real-time message streaming * Implement StreamEvent types (thought, tool_call, tool_result, final, error) * Add StreamEventCallback mechanism for event propagation * Create StreamChatHandler in webui/bot with proper HTTP headers and flushing - Implement LLM-based skill router for intelligent capability selection * Add optional routerLLM client for semantic routing * Implement routeSkillsWithLLM() to match user intent to available skills * Add matchSkillsByName() for fuzzy skill matching * Update buildUnifiedSystemPrompt() to use routed skills - Add streaming support to ReAct pipeline * Implement runUnifiedReActStream() for streaming thought/action/observation * Emit StreamEvent at each ReAct step * Support callback error handling in streaming mode - Integrate three new DevOps tools * tools/filedoc: Extract document content from file_id via OpenAI * tools/giteaticket: Create Gitea issues from PI plan items with SAFe metadata * tools/piplan: Publish PI planning blueprints with dependency tracking - Add SAFe PI Planning skill * Implement PM/SA/RTE (iron triangle) workflow * Support for Feature, Enabler, and Dependency definition * Automatic task decomposition and Gitea integration - Create frontend integration documentation * Complete SSE protocol specification * TypeScript fetch + ReadableStream example * LLM-ready refactoring template for other projects - Simplify file handling * Remove legacy file context structures and dual-mode processing * Consolidate file operations into UploadAndCacheFiles() * Remove FilePromptMode configuration and related complexity - Update configuration * Add Router model support (LLM_ROUTER_MODEL) * Add Gitea configuration (BaseURL, Token, Owner, Repo) * WebSearch and additional tool infrastructure Tests: All 22 test packages passing, 8/8 webui tests including 3 new stream tests
2026-03-11 17:58:19 +08:00
type GiteaConfig struct {
BaseURL string // Gitea 实例地址
Token string // Personal Access Token
Owner string // 仓库所有者
Repo string // 仓库名称
}
2026-02-21 23:01:39 +08:00
func Load() (Config, error) {
agentWorkspaceDir := resolveAgentWorkspaceDir()
2026-02-21 23:01:39 +08:00
if err := preloadEnvFiles(); err != nil {
return Config{}, err
}
defaultWorkSubdir := filepath.Join(agentWorkspaceDir, "workspace")
defaultDataDir := filepath.Join(agentWorkspaceDir, "data")
2026-02-21 23:01:39 +08:00
cfg := Config{
MessageChannel: defaultIfEmpty(os.Getenv("MESSAGE_CHANNEL"), "telegram"),
LogLevel: defaultIfEmpty(os.Getenv("LOG_LEVEL"), "info"),
SoulPath: defaultIfEmpty(os.Getenv("SOUL_PATH"), filepath.Join(agentWorkspaceDir, "bot_context", "soul.md")),
SkillsDir: defaultIfEmpty(os.Getenv("SKILLS_DIR"), filepath.Join(agentWorkspaceDir, "skills")),
ReactMaxSteps: intFromEnv("REACT_MAX_STEPS", 0),
ToolCallTimeoutSec: intFromEnv("TOOL_CALL_TIMEOUT_SEC", 15),
ToolOutputMaxChars: intFromEnv("TOOL_OUTPUT_MAX_CHARS", 4000),
EnableCapabilityGap: boolFromEnv("ENABLE_CAPABILITY_GAP", true),
AutoSkillDir: defaultIfEmpty(os.Getenv("AUTO_SKILL_DIR"), filepath.Join(agentWorkspaceDir, "skills")),
GapDraftTriggerCount: intFromEnv("GAP_DRAFT_TRIGGER_COUNT", 3),
GapClusterLookbackHours: intFromEnv("GAP_CLUSTER_LOOKBACK_HOURS", 168),
2026-02-21 23:01:39 +08:00
Telegram: TelegramConfig{
Token: strings.TrimSpace(os.Getenv("TELEGRAM_BOT_TOKEN")),
PollTimeoutSeconds: intFromEnv("TELEGRAM_POLL_TIMEOUT_SECONDS", 30),
},
Feishu: FeishuConfig{
AppID: strings.TrimSpace(os.Getenv("FEISHU_APP_ID")),
AppSecret: strings.TrimSpace(os.Getenv("FEISHU_APP_SECRET")),
VerifyToken: strings.TrimSpace(os.Getenv("FEISHU_VERIFY_TOKEN")),
ListenAddr: defaultIfEmpty(os.Getenv("FEISHU_LISTEN_ADDR"), ":8080"),
EventPath: defaultIfEmpty(os.Getenv("FEISHU_EVENT_PATH"), "/feishu/events"),
},
WebUI: WebUIConfig{
ListenAddr: defaultIfEmpty(os.Getenv("WEBUI_LISTEN_ADDR"), ":8090"),
MaxUploadBytes: int64(intFromEnv("WEBUI_MAX_UPLOAD_MB", 20)) * 1024 * 1024,
},
2026-02-21 23:01:39 +08:00
LLM: LLMConfig{
feat: implement streaming chat, skill routing, and SAFe PI planning tools - Add /api/chat/stream endpoint with Server-Sent Events (SSE) for real-time message streaming * Implement StreamEvent types (thought, tool_call, tool_result, final, error) * Add StreamEventCallback mechanism for event propagation * Create StreamChatHandler in webui/bot with proper HTTP headers and flushing - Implement LLM-based skill router for intelligent capability selection * Add optional routerLLM client for semantic routing * Implement routeSkillsWithLLM() to match user intent to available skills * Add matchSkillsByName() for fuzzy skill matching * Update buildUnifiedSystemPrompt() to use routed skills - Add streaming support to ReAct pipeline * Implement runUnifiedReActStream() for streaming thought/action/observation * Emit StreamEvent at each ReAct step * Support callback error handling in streaming mode - Integrate three new DevOps tools * tools/filedoc: Extract document content from file_id via OpenAI * tools/giteaticket: Create Gitea issues from PI plan items with SAFe metadata * tools/piplan: Publish PI planning blueprints with dependency tracking - Add SAFe PI Planning skill * Implement PM/SA/RTE (iron triangle) workflow * Support for Feature, Enabler, and Dependency definition * Automatic task decomposition and Gitea integration - Create frontend integration documentation * Complete SSE protocol specification * TypeScript fetch + ReadableStream example * LLM-ready refactoring template for other projects - Simplify file handling * Remove legacy file context structures and dual-mode processing * Consolidate file operations into UploadAndCacheFiles() * Remove FilePromptMode configuration and related complexity - Update configuration * Add Router model support (LLM_ROUTER_MODEL) * Add Gitea configuration (BaseURL, Token, Owner, Repo) * WebSearch and additional tool infrastructure Tests: All 22 test packages passing, 8/8 webui tests including 3 new stream tests
2026-03-11 17:58:19 +08:00
BaseURL: strings.TrimRight(defaultIfEmpty(os.Getenv("LLM_BASE_URL"), "https://api.openai.com/v1"), "/"),
APIKey: strings.TrimSpace(os.Getenv("LLM_API_KEY")),
Model: defaultIfEmpty(os.Getenv("LLM_MODEL"), "gpt-4o-mini"),
FileModel: defaultIfEmpty(os.Getenv("LLM_FILE_MODEL"), defaultIfEmpty(os.Getenv("LLM_MODEL"), "gpt-4o-mini")),
RouterModel: strings.TrimSpace(os.Getenv("LLM_ROUTER_MODEL")),
2026-02-21 23:01:39 +08:00
},
SQLitePath: defaultIfEmpty(os.Getenv("SQLITE_PATH"), filepath.Join(defaultDataDir, "laodingbot.db")),
WebSearch: WebSearchConfig{
Engine: defaultIfEmpty(os.Getenv("WEB_SEARCH_ENGINE"), "duckduckgo"),
APIKey: strings.TrimSpace(os.Getenv("WEB_SEARCH_API_KEY")),
},
2026-02-21 23:01:39 +08:00
Security: SecurityConfig{
AllowedDirs: splitCSV(defaultIfEmpty(os.Getenv("ALLOWED_DIRS"), strings.Join([]string{agentWorkspaceDir, defaultDataDir, defaultWorkSubdir}, ","))),
AllowedCommands: splitCSV(defaultIfEmpty(os.Getenv("ALLOWED_COMMANDS"), "pwd,ls,cat,echo,grep,find,head,tail,go")),
WorkDir: defaultIfEmpty(os.Getenv("WORK_DIR"), defaultWorkSubdir),
2026-02-21 23:01:39 +08:00
},
feat: implement streaming chat, skill routing, and SAFe PI planning tools - Add /api/chat/stream endpoint with Server-Sent Events (SSE) for real-time message streaming * Implement StreamEvent types (thought, tool_call, tool_result, final, error) * Add StreamEventCallback mechanism for event propagation * Create StreamChatHandler in webui/bot with proper HTTP headers and flushing - Implement LLM-based skill router for intelligent capability selection * Add optional routerLLM client for semantic routing * Implement routeSkillsWithLLM() to match user intent to available skills * Add matchSkillsByName() for fuzzy skill matching * Update buildUnifiedSystemPrompt() to use routed skills - Add streaming support to ReAct pipeline * Implement runUnifiedReActStream() for streaming thought/action/observation * Emit StreamEvent at each ReAct step * Support callback error handling in streaming mode - Integrate three new DevOps tools * tools/filedoc: Extract document content from file_id via OpenAI * tools/giteaticket: Create Gitea issues from PI plan items with SAFe metadata * tools/piplan: Publish PI planning blueprints with dependency tracking - Add SAFe PI Planning skill * Implement PM/SA/RTE (iron triangle) workflow * Support for Feature, Enabler, and Dependency definition * Automatic task decomposition and Gitea integration - Create frontend integration documentation * Complete SSE protocol specification * TypeScript fetch + ReadableStream example * LLM-ready refactoring template for other projects - Simplify file handling * Remove legacy file context structures and dual-mode processing * Consolidate file operations into UploadAndCacheFiles() * Remove FilePromptMode configuration and related complexity - Update configuration * Add Router model support (LLM_ROUTER_MODEL) * Add Gitea configuration (BaseURL, Token, Owner, Repo) * WebSearch and additional tool infrastructure Tests: All 22 test packages passing, 8/8 webui tests including 3 new stream tests
2026-03-11 17:58:19 +08:00
Gitea: GiteaConfig{
BaseURL: strings.TrimRight(strings.TrimSpace(os.Getenv("GITEA_BASE_URL")), "/"),
Token: strings.TrimSpace(os.Getenv("GITEA_TOKEN")),
Owner: strings.TrimSpace(os.Getenv("GITEA_OWNER")),
Repo: strings.TrimSpace(os.Getenv("GITEA_REPO")),
},
2026-02-21 23:01:39 +08:00
}
cfg.MessageChannel = strings.ToLower(strings.TrimSpace(cfg.MessageChannel))
cfg.LogLevel = strings.ToLower(strings.TrimSpace(cfg.LogLevel))
if cfg.MessageChannel != "telegram" && cfg.MessageChannel != "feishu" && cfg.MessageChannel != "webui" {
return Config{}, fmt.Errorf("MESSAGE_CHANNEL must be telegram, feishu, or webui")
2026-02-21 23:01:39 +08:00
}
if cfg.LogLevel != "debug" && cfg.LogLevel != "info" && cfg.LogLevel != "warn" && cfg.LogLevel != "error" {
return Config{}, fmt.Errorf("LOG_LEVEL must be debug, info, warn, or error")
}
if cfg.ReactMaxSteps < 1 || cfg.ReactMaxSteps > 8 {
return Config{}, fmt.Errorf("REACT_MAX_STEPS must be between 1 and 8")
}
if cfg.ToolCallTimeoutSec < 1 || cfg.ToolCallTimeoutSec > 300 {
return Config{}, fmt.Errorf("TOOL_CALL_TIMEOUT_SEC must be between 1 and 300")
}
if cfg.ToolOutputMaxChars < 256 || cfg.ToolOutputMaxChars > 200000 {
return Config{}, fmt.Errorf("TOOL_OUTPUT_MAX_CHARS must be between 256 and 200000")
}
if cfg.GapDraftTriggerCount < 1 || cfg.GapDraftTriggerCount > 100 {
return Config{}, fmt.Errorf("GAP_DRAFT_TRIGGER_COUNT must be between 1 and 100")
}
if cfg.GapClusterLookbackHours < 1 || cfg.GapClusterLookbackHours > 24*365 {
return Config{}, fmt.Errorf("GAP_CLUSTER_LOOKBACK_HOURS must be between 1 and 8760")
}
if cfg.WebUI.MaxUploadBytes < 1024 || cfg.WebUI.MaxUploadBytes > 200*1024*1024 {
return Config{}, fmt.Errorf("WEBUI_MAX_UPLOAD_MB must be between 1 and 200")
}
2026-02-21 23:01:39 +08:00
if cfg.MessageChannel == "telegram" {
if cfg.Telegram.Token == "" {
return Config{}, fmt.Errorf("TELEGRAM_BOT_TOKEN is required when MESSAGE_CHANNEL=telegram")
}
if cfg.Feishu.AppID != "" || cfg.Feishu.AppSecret != "" {
return Config{}, fmt.Errorf("feishu config must be empty when MESSAGE_CHANNEL=telegram")
}
}
if cfg.MessageChannel == "feishu" {
if cfg.Feishu.AppID == "" || cfg.Feishu.AppSecret == "" {
return Config{}, fmt.Errorf("FEISHU_APP_ID and FEISHU_APP_SECRET are required when MESSAGE_CHANNEL=feishu")
}
if cfg.Telegram.Token != "" {
return Config{}, fmt.Errorf("TELEGRAM_BOT_TOKEN must be empty when MESSAGE_CHANNEL=feishu")
}
}
if cfg.MessageChannel == "webui" {
if strings.TrimSpace(cfg.WebUI.ListenAddr) == "" {
return Config{}, fmt.Errorf("WEBUI_LISTEN_ADDR is required when MESSAGE_CHANNEL=webui")
}
}
2026-02-21 23:01:39 +08:00
if cfg.LLM.APIKey == "" {
return Config{}, fmt.Errorf("LLM_API_KEY is required")
}
cfg.SoulPath = resolvePathInWorkspace(cfg.SoulPath, agentWorkspaceDir)
cfg.SkillsDir = resolvePathInWorkspace(cfg.SkillsDir, agentWorkspaceDir)
cfg.AutoSkillDir = resolvePathInWorkspace(cfg.AutoSkillDir, agentWorkspaceDir)
cfg.SQLitePath = resolvePathInWorkspace(cfg.SQLitePath, agentWorkspaceDir)
cfg.Security.WorkDir = resolvePathInWorkspace(cfg.Security.WorkDir, agentWorkspaceDir)
cfg.Security.AllowedDirs = resolveDirsInWorkspace(cfg.Security.AllowedDirs, agentWorkspaceDir)
cfg.Security.AllowedDirs = ensureAllowedDirs(cfg.Security.AllowedDirs,
filepath.Clean(agentWorkspaceDir),
filepath.Join(agentWorkspaceDir, "skills"),
filepath.Join(agentWorkspaceDir, "data"),
filepath.Join(agentWorkspaceDir, "workspace"),
)
cfg.Security.AllowedCommands = ensureAllowedCommands(cfg.Security.AllowedCommands, "go", "curl", "curl.exe")
2026-02-21 23:01:39 +08:00
return cfg, nil
}
func preloadEnvFiles() error {
explicit := strings.TrimSpace(os.Getenv("CONFIG_ENV_FILE"))
if explicit != "" {
if err := tryLoadEnvFile(explicit, true); err != nil {
return fmt.Errorf("load env file %s failed: %w", explicit, err)
}
2026-02-21 23:01:39 +08:00
}
workspaceDir := resolveAgentWorkspaceDir()
workspaceEnv := filepath.Join(workspaceDir, "configs", "env")
workspaceDotEnv := filepath.Join(workspaceDir, ".env")
if err := tryLoadEnvFile(workspaceEnv, true); err != nil {
return fmt.Errorf("load env file %s failed: %w", workspaceEnv, err)
}
if err := tryLoadEnvFile(workspaceDotEnv, true); err != nil {
return fmt.Errorf("load env file %s failed: %w", workspaceDotEnv, err)
}
if err := tryLoadEnvFile("configs/env", false); err != nil {
return fmt.Errorf("load env file %s failed: %w", "configs/env", err)
}
if err := tryLoadEnvFile(".env", false); err != nil {
return fmt.Errorf("load env file %s failed: %w", ".env", err)
2026-02-21 23:01:39 +08:00
}
return nil
}
func tryLoadEnvFile(path string, override bool) error {
err := loadEnvFile(path, override)
if err != nil && errors.Is(err, os.ErrNotExist) {
return nil
}
return err
}
func resolveAgentWorkspaceDir() string {
raw := strings.TrimSpace(os.Getenv("AGENT_WORKSPACE_DIR"))
if raw == "" {
raw = filepath.Join(".", "workspace", "agent_runtime")
}
abs, err := filepath.Abs(raw)
if err != nil {
return raw
}
return abs
}
func resolvePathInWorkspace(path, workspaceDir string) string {
path = strings.TrimSpace(path)
if path == "" {
return path
}
if filepath.IsAbs(path) {
return filepath.Clean(path)
}
return filepath.Clean(filepath.Join(workspaceDir, path))
}
func resolveDirsInWorkspace(dirs []string, workspaceDir string) []string {
out := make([]string, 0, len(dirs))
for _, d := range dirs {
resolved := resolvePathInWorkspace(d, workspaceDir)
if strings.TrimSpace(resolved) != "" {
out = append(out, resolved)
}
}
if len(out) == 0 {
out = append(out, filepath.Clean(workspaceDir))
}
return out
}
func ensureAllowedDirs(existing []string, dirs ...string) []string {
set := map[string]struct{}{}
out := make([]string, 0, len(existing)+len(dirs))
for _, d := range existing {
clean := filepath.Clean(strings.TrimSpace(d))
if clean == "" {
continue
}
if _, ok := set[clean]; ok {
continue
}
set[clean] = struct{}{}
out = append(out, clean)
}
for _, d := range dirs {
clean := filepath.Clean(strings.TrimSpace(d))
if clean == "" {
continue
}
if _, ok := set[clean]; ok {
continue
}
set[clean] = struct{}{}
out = append(out, clean)
}
return out
}
func ensureAllowedCommands(existing []string, commands ...string) []string {
set := map[string]struct{}{}
out := make([]string, 0, len(existing)+len(commands))
for _, c := range existing {
cmd := strings.ToLower(strings.TrimSpace(c))
if cmd == "" {
continue
}
if _, ok := set[cmd]; ok {
continue
}
set[cmd] = struct{}{}
out = append(out, cmd)
}
for _, c := range commands {
cmd := strings.ToLower(strings.TrimSpace(c))
if cmd == "" {
continue
}
if _, ok := set[cmd]; ok {
continue
}
set[cmd] = struct{}{}
out = append(out, cmd)
}
return out
}
func loadEnvFile(path string, override bool) error {
2026-02-21 23:01:39 +08:00
absPath, err := filepath.Abs(path)
if err != nil {
absPath = path
}
f, err := os.Open(absPath)
if err != nil {
return err
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
line = strings.TrimPrefix(line, "export ")
idx := strings.Index(line, "=")
if idx <= 0 {
continue
}
key := strings.TrimSpace(line[:idx])
if key == "" {
continue
}
val := strings.TrimSpace(line[idx+1:])
if len(val) >= 2 {
if (val[0] == '"' && val[len(val)-1] == '"') || (val[0] == '\'' && val[len(val)-1] == '\'') {
val = val[1 : len(val)-1]
}
}
if !override {
if _, exists := os.LookupEnv(key); exists {
continue
2026-02-21 23:01:39 +08:00
}
}
if err := os.Setenv(key, val); err != nil {
return err
}
2026-02-21 23:01:39 +08:00
}
return scanner.Err()
}
func defaultIfEmpty(v, d string) string {
v = strings.TrimSpace(v)
if v == "" {
return d
}
return v
}
func intFromEnv(name string, d int) int {
raw := strings.TrimSpace(os.Getenv(name))
if raw == "" {
return d
}
v, err := strconv.Atoi(raw)
if err != nil {
return d
}
return v
}
func boolFromEnv(name string, d bool) bool {
raw := strings.ToLower(strings.TrimSpace(os.Getenv(name)))
if raw == "" {
return d
}
if raw == "1" || raw == "true" || raw == "yes" || raw == "on" {
return true
}
if raw == "0" || raw == "false" || raw == "no" || raw == "off" {
return false
}
return d
}
2026-02-21 23:01:39 +08:00
func splitCSV(raw string) []string {
parts := strings.Split(raw, ",")
out := make([]string, 0, len(parts))
for _, p := range parts {
v := strings.TrimSpace(p)
if v != "" {
out = append(out, v)
}
}
return out
}