2026-02-21 23:01:39 +08:00
|
|
|
package config
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bufio"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type Config struct {
|
2026-03-05 17:44:19 +08:00
|
|
|
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
|
|
|
|
2026-03-05 17:44:19 +08:00
|
|
|
Telegram TelegramConfig
|
|
|
|
|
Feishu FeishuConfig
|
2026-03-10 10:23:53 +08:00
|
|
|
WebUI WebUIConfig
|
2026-03-05 17:44:19 +08:00
|
|
|
LLM LLMConfig
|
|
|
|
|
Security SecurityConfig
|
|
|
|
|
WebSearch WebSearchConfig
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-10 10:23:53 +08:00
|
|
|
type WebUIConfig struct {
|
|
|
|
|
ListenAddr string
|
|
|
|
|
MaxUploadBytes int64
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-21 23:01:39 +08:00
|
|
|
type LLMConfig struct {
|
2026-03-09 17:38:13 +08:00
|
|
|
BaseURL string
|
|
|
|
|
APIKey string
|
|
|
|
|
Model string
|
|
|
|
|
FileModel string
|
|
|
|
|
FilePromptMode string
|
2026-02-21 23:01:39 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type SecurityConfig struct {
|
|
|
|
|
AllowedDirs []string
|
|
|
|
|
AllowedCommands []string
|
|
|
|
|
WorkDir string
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-05 17:44:19 +08:00
|
|
|
type WebSearchConfig struct {
|
|
|
|
|
Engine string // "duckduckgo" or "brave"
|
|
|
|
|
APIKey string
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-21 23:01:39 +08:00
|
|
|
func Load() (Config, error) {
|
2026-02-28 17:48:33 +08:00
|
|
|
agentWorkspaceDir := resolveAgentWorkspaceDir()
|
2026-02-21 23:01:39 +08:00
|
|
|
if err := preloadEnvFiles(); err != nil {
|
|
|
|
|
return Config{}, err
|
|
|
|
|
}
|
2026-02-28 17:48:33 +08:00
|
|
|
defaultWorkSubdir := filepath.Join(agentWorkspaceDir, "workspace")
|
|
|
|
|
defaultDataDir := filepath.Join(agentWorkspaceDir, "data")
|
2026-02-21 23:01:39 +08:00
|
|
|
|
|
|
|
|
cfg := Config{
|
2026-03-05 17:44:19 +08:00
|
|
|
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),
|
2026-02-28 17:48:33 +08:00
|
|
|
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"),
|
|
|
|
|
},
|
2026-03-10 10:23:53 +08:00
|
|
|
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{
|
2026-03-09 17:38:13 +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")),
|
|
|
|
|
FilePromptMode: normalizeFilePromptMode(defaultIfEmpty(os.Getenv("LLM_FILE_PROMPT_MODE"), "user_content_file_parts")),
|
2026-02-21 23:01:39 +08:00
|
|
|
},
|
2026-02-28 17:48:33 +08:00
|
|
|
SQLitePath: defaultIfEmpty(os.Getenv("SQLITE_PATH"), filepath.Join(defaultDataDir, "laodingbot.db")),
|
2026-03-05 17:44:19 +08:00
|
|
|
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{
|
2026-02-28 17:48:33 +08:00
|
|
|
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
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cfg.MessageChannel = strings.ToLower(strings.TrimSpace(cfg.MessageChannel))
|
|
|
|
|
cfg.LogLevel = strings.ToLower(strings.TrimSpace(cfg.LogLevel))
|
2026-03-10 10:23:53 +08:00
|
|
|
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")
|
|
|
|
|
}
|
2026-02-28 17:48:33 +08:00
|
|
|
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")
|
|
|
|
|
}
|
2026-03-10 10:23:53 +08:00
|
|
|
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")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-10 10:23:53 +08:00
|
|
|
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")
|
|
|
|
|
}
|
2026-03-09 17:38:13 +08:00
|
|
|
if cfg.LLM.FilePromptMode != "user_content_file_parts" && cfg.LLM.FilePromptMode != "system_fileid_uri" {
|
|
|
|
|
return Config{}, fmt.Errorf("LLM_FILE_PROMPT_MODE must be one of: user_content_file_parts, system_fileid_uri")
|
|
|
|
|
}
|
2026-02-21 23:01:39 +08:00
|
|
|
|
2026-02-28 17:48:33 +08:00
|
|
|
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 {
|
2026-02-28 17:48:33 +08:00
|
|
|
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
|
|
|
}
|
|
|
|
|
|
2026-02-28 17:48:33 +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
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-28 17:48:33 +08:00
|
|
|
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]
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-28 17:48:33 +08:00
|
|
|
if !override {
|
|
|
|
|
if _, exists := os.LookupEnv(key); exists {
|
|
|
|
|
continue
|
2026-02-21 23:01:39 +08:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-28 17:48:33 +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
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-28 17:48:33 +08:00
|
|
|
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
|
|
|
|
|
}
|
2026-03-09 17:38:13 +08:00
|
|
|
|
|
|
|
|
func normalizeFilePromptMode(v string) string {
|
|
|
|
|
v = strings.ToLower(strings.TrimSpace(v))
|
|
|
|
|
if v == "" {
|
|
|
|
|
return "user_content_file_parts"
|
|
|
|
|
}
|
|
|
|
|
if v == "system_fileid" || v == "system_fileid_url" || v == "system_fileid_uri" {
|
|
|
|
|
return "system_fileid_uri"
|
|
|
|
|
}
|
|
|
|
|
return v
|
|
|
|
|
}
|