You've already forked agentic-coding-workflow
337 lines
13 KiB
Go
337 lines
13 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"`
|
|
Jobs JobsConfig `mapstructure:"jobs"`
|
|
Notify NotifyConfig `mapstructure:"notify"`
|
|
Acp AcpConfig `mapstructure:"acp"`
|
|
MCP MCPConfig `mapstructure:"mcp"`
|
|
}
|
|
|
|
// 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"`
|
|
AcpEventsPurge AcpEventsPurgeCfg `mapstructure:"acp_events_purge"`
|
|
MCPTokensPurge MCPTokensPurgeCfg `mapstructure:"mcp_tokens_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"`
|
|
}
|
|
|
|
type AcpEventsPurgeCfg struct {
|
|
Enabled bool `mapstructure:"enabled"`
|
|
Interval time.Duration `mapstructure:"interval"`
|
|
Retention time.Duration `mapstructure:"retention"`
|
|
}
|
|
|
|
type MCPTokensPurgeCfg struct {
|
|
Enabled bool `mapstructure:"enabled"`
|
|
Interval time.Duration `mapstructure:"interval"`
|
|
Retention time.Duration `mapstructure:"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"`
|
|
}
|
|
|
|
// AcpConfig 控制 ACP 模块的并发限流、子进程超时、WS 心跳与事件保留。
|
|
type AcpConfig struct {
|
|
Enabled bool `mapstructure:"enabled"`
|
|
MaxActiveGlobal int `mapstructure:"max_active_global"`
|
|
MaxActivePerUser int `mapstructure:"max_active_per_user"`
|
|
SpawnTimeout time.Duration `mapstructure:"spawn_timeout"`
|
|
KillGrace time.Duration `mapstructure:"kill_grace"`
|
|
StderrBufferLines int `mapstructure:"stderr_buffer_lines"`
|
|
StderrTailBytes int `mapstructure:"stderr_tail_bytes"`
|
|
WSPingInterval time.Duration `mapstructure:"ws_ping_interval"`
|
|
WSPongTimeout time.Duration `mapstructure:"ws_pong_timeout"`
|
|
WSSendBuffer int `mapstructure:"ws_send_buffer"`
|
|
EventMaxPayload int `mapstructure:"event_max_payload_bytes"`
|
|
EventTruncateField int `mapstructure:"event_truncate_field_bytes"`
|
|
EventRetentionDays int `mapstructure:"event_retention_days"`
|
|
ShutdownGrace time.Duration `mapstructure:"shutdown_grace"`
|
|
PermissionTimeout time.Duration `mapstructure:"permission_timeout"`
|
|
}
|
|
|
|
// MCPConfig 控制 MCP 模块的开关、对外 URL(注入 ACP 子进程 env)、system token TTL 与速率限制。
|
|
type MCPConfig struct {
|
|
Enabled bool `mapstructure:"enabled"`
|
|
PublicURL string `mapstructure:"public_url"`
|
|
SystemTokenTTL time.Duration `mapstructure:"system_token_ttl"`
|
|
RateLimit MCPRateLimitCfg `mapstructure:"rate_limit"`
|
|
}
|
|
|
|
// MCPRateLimitCfg 控制 token bucket 容量。两层独立桶:per-token + 全局。
|
|
type MCPRateLimitCfg struct {
|
|
PerTokenPerMinute int `mapstructure:"per_token_per_minute"`
|
|
GlobalPerMinute int `mapstructure:"global_per_minute"`
|
|
}
|
|
|
|
// 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("jobs.acp_events_purge.enabled", true)
|
|
v.SetDefault("jobs.acp_events_purge.interval", "24h")
|
|
v.SetDefault("jobs.acp_events_purge.retention", "168h")
|
|
v.SetDefault("jobs.mcp_tokens_purge.enabled", true)
|
|
v.SetDefault("jobs.mcp_tokens_purge.interval", "24h")
|
|
v.SetDefault("jobs.mcp_tokens_purge.retention", "168h")
|
|
v.SetDefault("notify.webhook.enabled", false)
|
|
v.SetDefault("notify.webhook.timeout", "10s")
|
|
v.SetDefault("acp.enabled", true)
|
|
v.SetDefault("acp.max_active_global", 20)
|
|
v.SetDefault("acp.max_active_per_user", 3)
|
|
v.SetDefault("acp.spawn_timeout", "30s")
|
|
v.SetDefault("acp.kill_grace", "5s")
|
|
v.SetDefault("acp.stderr_buffer_lines", 100)
|
|
v.SetDefault("acp.stderr_tail_bytes", 2000)
|
|
v.SetDefault("acp.ws_ping_interval", "30s")
|
|
v.SetDefault("acp.ws_pong_timeout", "60s")
|
|
v.SetDefault("acp.ws_send_buffer", 256)
|
|
v.SetDefault("acp.event_max_payload_bytes", 65536)
|
|
v.SetDefault("acp.event_truncate_field_bytes", 4096)
|
|
v.SetDefault("acp.event_retention_days", 7)
|
|
v.SetDefault("acp.shutdown_grace", "30s")
|
|
v.SetDefault("acp.permission_timeout", "5m")
|
|
v.SetDefault("mcp.enabled", true)
|
|
v.SetDefault("mcp.public_url", "http://localhost:8080/mcp")
|
|
v.SetDefault("mcp.system_token_ttl", "24h")
|
|
v.SetDefault("mcp.rate_limit.per_token_per_minute", 100)
|
|
v.SetDefault("mcp.rate_limit.global_per_minute", 1000)
|
|
|
|
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
|
|
}
|