Files
agentic-coding-workflow/internal/config/config.go
T

184 lines
6.1 KiB
Go

// 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"`
}
// 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"`
}
// 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")
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
}