// Package config 提供基于 viper 的应用配置加载,按 默认值 → 配置文件 → 环境变量 的优先级合并。 package config import ( "encoding/hex" "fmt" "log/slog" "strings" "time" "github.com/spf13/viper" ) // MasterKey wraps a 32-byte AES master key with redacted formatting to prevent // accidental log/fmt exposure. type MasterKey []byte // String redacts when used with %s / fmt.Println. func (k MasterKey) String() string { return "[REDACTED 32B]" } // GoString redacts when used with %#v. func (k MasterKey) GoString() string { return "[REDACTED 32B]" } // MarshalJSON redacts when serialized to JSON. func (k MasterKey) MarshalJSON() ([]byte, error) { return []byte(`"[REDACTED]"`), nil } // LogValue redacts when logged via slog. func (k MasterKey) LogValue() slog.Value { return slog.StringValue("[REDACTED 32B]") } // Config 是应用全局配置的根结构。 type Config struct { Env string `mapstructure:"env"` DataDir string `mapstructure:"data_dir"` MasterKey MasterKey `mapstructure:"-"` // 解码后的 32 字节,单独从 master_key 解析 HTTP HTTPConfig `mapstructure:"http"` DB DBConfig `mapstructure:"db"` Bootstrap BootstrapConfig `mapstructure:"bootstrap"` Workspace Workspace `mapstructure:"workspace"` Git Git `mapstructure:"git"` Chat Chat `mapstructure:"chat"` Storage Storage `mapstructure:"storage"` Jobs JobsConfig `mapstructure:"jobs"` Notify NotifyConfig `mapstructure:"notify"` } // HTTPConfig 描述 HTTP 服务监听参数。 type HTTPConfig struct { Addr string `mapstructure:"addr"` } // DBConfig 描述数据库连接参数。 type DBConfig struct { DSN string `mapstructure:"dsn"` } // BootstrapConfig 描述首次启动注入的管理员账号。 type BootstrapConfig struct { AdminEmail string `mapstructure:"admin_email"` AdminPassword string `mapstructure:"admin_password"` } // Workspace 控制 workspace 模块的磁盘与超时(数据目录、每个 git verb 的超时)。 // DataDir 是 workspace clone/worktree 落盘根目录;五个 timeout 直接灌进 git.Config。 type Workspace struct { DataDir string `mapstructure:"data_dir"` CloneTimeout time.Duration `mapstructure:"clone_timeout"` FetchTimeout time.Duration `mapstructure:"fetch_timeout"` PushTimeout time.Duration `mapstructure:"push_timeout"` OpsTimeout time.Duration `mapstructure:"ops_timeout"` } // Git 控制 git CLI 二进制位置(默认 "git")。 type Git struct { Binary string `mapstructure:"binary"` } // Chat 配置 chat 模块的附件、流式、历史与定时清理策略。 type Chat struct { Attachment ChatAttachment `mapstructure:"attachment"` Stream ChatStream `mapstructure:"stream"` History ChatHistory `mapstructure:"history"` } // ChatAttachment 控制单文件 / 单消息上限与允许的 MIME 白名单。 type ChatAttachment struct { MaxFileSizeMB int64 `mapstructure:"max_file_size_mb"` MaxMessageSizeMB int64 `mapstructure:"max_message_size_mb"` MimeWhitelist []string `mapstructure:"mime_whitelist"` } // ChatStream 控制 SSE ring buffer 保留时长与取消宽限期。 type ChatStream struct { RingBufferRetentionSeconds int `mapstructure:"ring_buffer_retention_seconds"` } // ChatHistory 控制 token 预算的安全余量百分比(如 5 表示保留 5% 余量)。 type ChatHistory struct { SafetyMarginPct float64 `mapstructure:"safety_margin_pct"` } // Storage 选择附件落盘驱动与对应参数。 type Storage struct { Driver string `mapstructure:"driver"` // 当前仅支持 "localfs" LocalFS StorageLocalFS `mapstructure:"localfs"` } // StorageLocalFS 是 driver=localfs 时的根目录。 type StorageLocalFS struct { BasePath string `mapstructure:"base_path"` } // JobsConfig 控制后台 job runner 的总开关、worker 池与各定时任务参数。 type JobsConfig struct { Enabled bool `mapstructure:"enabled"` Workers int `mapstructure:"workers"` ShutdownGrace time.Duration `mapstructure:"shutdown_grace"` WorkspaceFetch RunnerCfg `mapstructure:"workspace_fetch"` WorktreePrune WorktreePruneCfg `mapstructure:"worktree_prune"` AttachmentCleanup AttachmentCleanupCfg `mapstructure:"attachment_cleanup"` JobReaper JobReaperCfg `mapstructure:"job_reaper"` JobPurge JobPurgeCfg `mapstructure:"job_purge"` } type RunnerCfg struct { Enabled bool `mapstructure:"enabled"` Interval time.Duration `mapstructure:"interval"` } type WorktreePruneCfg struct { Enabled bool `mapstructure:"enabled"` Interval time.Duration `mapstructure:"interval"` IdleThreshold time.Duration `mapstructure:"idle_threshold"` WarningLead time.Duration `mapstructure:"warning_lead"` } type AttachmentCleanupCfg struct { Enabled bool `mapstructure:"enabled"` Interval time.Duration `mapstructure:"interval"` Retention time.Duration `mapstructure:"retention"` } type JobReaperCfg struct { Enabled bool `mapstructure:"enabled"` Interval time.Duration `mapstructure:"interval"` LeaseTimeout time.Duration `mapstructure:"lease_timeout"` } type JobPurgeCfg struct { Enabled bool `mapstructure:"enabled"` Interval time.Duration `mapstructure:"interval"` CompletedRetention time.Duration `mapstructure:"completed_retention"` } // NotifyConfig 是 notify 模块的可选外发通道配置。当前仅 webhook。 type NotifyConfig struct { Webhook WebhookCfg `mapstructure:"webhook"` } // WebhookCfg 是 webhook 通道的配置。enabled=false 时上层 dispatcher 仍会丢弃所有 webhook 消息。 type WebhookCfg struct { Enabled bool `mapstructure:"enabled"` URL string `mapstructure:"url"` Secret string `mapstructure:"secret"` Timeout time.Duration `mapstructure:"timeout"` Topics []string `mapstructure:"topics"` } // Load 从(按优先级递增)默认值 → 配置文件(如有)→ 环境变量 加载配置。 // configFile 可为空字符串,仅用环境变量。 func Load(configFile string) (*Config, error) { v := viper.New() // 默认值 v.SetDefault("env", "development") v.SetDefault("data_dir", "./data") v.SetDefault("http.addr", ":8080") v.SetDefault("workspace.data_dir", "./data") v.SetDefault("workspace.clone_timeout", "30m") v.SetDefault("workspace.fetch_timeout", "5m") v.SetDefault("workspace.push_timeout", "5m") v.SetDefault("workspace.ops_timeout", "60s") v.SetDefault("git.binary", "git") v.SetDefault("chat.attachment.max_file_size_mb", 20) v.SetDefault("chat.attachment.max_message_size_mb", 50) v.SetDefault("chat.attachment.mime_whitelist", []string{ "image/png", "image/jpeg", "image/webp", "image/gif", "application/pdf", "text/*", }) v.SetDefault("chat.stream.ring_buffer_retention_seconds", 300) v.SetDefault("chat.history.safety_margin_pct", 5.0) v.SetDefault("storage.driver", "localfs") v.SetDefault("storage.localfs.base_path", "./data/uploads") v.SetDefault("jobs.enabled", true) v.SetDefault("jobs.workers", 2) v.SetDefault("jobs.shutdown_grace", "30s") v.SetDefault("jobs.workspace_fetch.enabled", true) v.SetDefault("jobs.workspace_fetch.interval", "5m") v.SetDefault("jobs.worktree_prune.enabled", true) v.SetDefault("jobs.worktree_prune.interval", "24h") v.SetDefault("jobs.worktree_prune.idle_threshold", "720h") v.SetDefault("jobs.worktree_prune.warning_lead", "72h") v.SetDefault("jobs.attachment_cleanup.enabled", true) v.SetDefault("jobs.attachment_cleanup.interval", "24h") v.SetDefault("jobs.attachment_cleanup.retention", "720h") v.SetDefault("jobs.job_reaper.enabled", true) v.SetDefault("jobs.job_reaper.interval", "1m") v.SetDefault("jobs.job_reaper.lease_timeout", "5m") v.SetDefault("jobs.job_purge.enabled", true) v.SetDefault("jobs.job_purge.interval", "24h") v.SetDefault("jobs.job_purge.completed_retention", "168h") v.SetDefault("notify.webhook.enabled", false) v.SetDefault("notify.webhook.timeout", "10s") if configFile != "" { v.SetConfigFile(configFile) if err := v.ReadInConfig(); err != nil { return nil, fmt.Errorf("read config file: %w", err) } } v.SetEnvPrefix("APP") v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) v.AutomaticEnv() // 显式绑定无默认值的嵌套键,以便 Unmarshal 能从环境变量取值。 // (AutomaticEnv 仅对 v.Get* 直接查询生效;Unmarshal 只看 viper 已知的键集合。) for _, key := range []string{ "db.dsn", "bootstrap.admin_email", "bootstrap.admin_password", "master_key", } { if err := v.BindEnv(key); err != nil { return nil, fmt.Errorf("bind env %s: %w", key, err) } } var cfg Config if err := v.Unmarshal(&cfg); err != nil { return nil, fmt.Errorf("unmarshal config: %w", err) } // MasterKey 是 hex 字符串,需要单独解码 rawKey := v.GetString("master_key") if rawKey == "" { return nil, fmt.Errorf("APP_MASTER_KEY required") } key, err := hex.DecodeString(rawKey) if err != nil { return nil, fmt.Errorf("APP_MASTER_KEY invalid hex: %w", err) } if len(key) != 32 { return nil, fmt.Errorf("APP_MASTER_KEY must be 32 bytes (got %d)", len(key)) } cfg.MasterKey = MasterKey(key) if cfg.DB.DSN == "" { return nil, fmt.Errorf("APP_DB_DSN required") } return &cfg, nil }