// Package config 定义全局配置类型和多级设置系统. // // 实现多级配置加载(默认值 -> 用户级 -> 项目级 -> 本地级 -> 环境变量 -> CLI 参数), // 后面的覆盖前面的. // // 配置文件路径: // - 用户级: ~/.flyto/settings.json // - 项目级: /.flyto/settings.json // - 本地级: /.flyto/settings.local.json package config import ( "encoding/json" "os" "path/filepath" "sync" "time" ) // MCPServerConfig 是 MCP 服务器配置. type MCPServerConfig struct { Name string `json:"name"` Transport string `json:"transport"` // stdio / sse / http / ws Command string `json:"command,omitempty"` Args []string `json:"args,omitempty"` URL string `json:"url,omitempty"` Env map[string]string `json:"env,omitempty"` } // PermissionSettings 是权限规则配置. type PermissionSettings struct { // AllowedTools 始终允许的工具列表(支持通配符) AllowedTools []string `json:"allowed_tools,omitempty"` // DeniedTools 始终拒绝的工具列表 DeniedTools []string `json:"denied_tools,omitempty"` // DefaultMode 默认权限模式 DefaultMode string `json:"default_mode,omitempty"` } // HookDef 是单个 hook 的定义(与 hooks 包保持结构一致). type HookDef struct { Command string `json:"command"` // shell 命令 Timeout int `json:"timeout"` // 超时(秒),默认 30 } // Settings 是多级设置系统的主结构体,收敛所有可配置项. type Settings struct { // 权限规则 Permissions PermissionSettings `json:"permissions,omitempty"` // MCP 服务器配置 MCPServers map[string]MCPServerConfig `json:"mcp_servers,omitempty"` // 自定义指令(追加到系统提示词) CustomInstructions string `json:"custom_instructions,omitempty"` // Hook 定义,key 是 hook 类型(pre_tool_use, post_tool_use 等) Hooks map[string][]HookDef `json:"hooks,omitempty"` // 默认模型 Model string `json:"model,omitempty"` // API Base URL 覆盖 APIBaseURL string `json:"api_base_url,omitempty"` // 最大轮次 MaxTurns int `json:"max_turns,omitempty"` // 最大预算(美元) MaxBudgetUSD float64 `json:"max_budget_usd,omitempty"` // 详细日志 Verbose bool `json:"verbose,omitempty"` } // Scope 是配置的作用域枚举. type Scope string const ( ScopeDefault Scope = "default" // 内置默认值 ScopeUser Scope = "user" // ~/.flyto/settings.json ScopeProject Scope = "project" // .flyto/settings.json ScopeLocal Scope = "local" // .flyto/settings.local.json ) // LoadSettings 按优先级加载并合并设置. // 加载顺序:默认值 -> 用户级 -> 项目级 -> 本地级 -> 环境变量覆盖. // cwd 是项目工作目录,用于定位项目级和本地级配置文件. func LoadSettings(cwd string) (*Settings, error) { // 1. 默认值 settings := defaultSettings() // 2. 用户级: ~/.flyto/settings.json home, err := os.UserHomeDir() if err == nil { userPath := filepath.Join(home, ".flyto", "settings.json") if userSettings, err := loadSettingsFile(userPath); err == nil { mergeSettings(settings, userSettings) } } // 3. 项目级: /.flyto/settings.json if cwd != "" { projectPath := filepath.Join(cwd, ".flyto", "settings.json") if projectSettings, err := loadSettingsFile(projectPath); err == nil { mergeSettings(settings, projectSettings) } } // 4. 本地级: /.flyto/settings.local.json if cwd != "" { localPath := filepath.Join(cwd, ".flyto", "settings.local.json") if localSettings, err := loadSettingsFile(localPath); err == nil { mergeSettings(settings, localSettings) } } // 5. 环境变量覆盖 applyEnvOverrides(settings) return settings, nil } // SaveSettings 将设置保存到指定作用域的配置文件. func SaveSettings(scope Scope, cwd string, settings *Settings) error { var path string switch scope { case ScopeUser: home, err := os.UserHomeDir() if err != nil { return err } path = filepath.Join(home, ".flyto", "settings.json") case ScopeProject: path = filepath.Join(cwd, ".flyto", "settings.json") case ScopeLocal: path = filepath.Join(cwd, ".flyto", "settings.local.json") default: return nil // 不能保存到 default scope } // 确保目录存在 dir := filepath.Dir(path) if err := os.MkdirAll(dir, 0755); err != nil { return err } data, err := json.MarshalIndent(settings, "", " ") if err != nil { return err } return os.WriteFile(path, data, 0644) } // Watcher 监听配置文件变化(基于轮询实现). type Watcher struct { cwd string interval time.Duration onChange func(*Settings) stopCh chan struct{} mu sync.Mutex lastMods map[string]time.Time } // NewWatcher 创建配置文件监听器. // interval 是轮询间隔,onChange 在配置发生变化时被调用. func NewWatcher(cwd string, interval time.Duration, onChange func(*Settings)) *Watcher { if interval <= 0 { interval = 2 * time.Second } return &Watcher{ cwd: cwd, interval: interval, onChange: onChange, stopCh: make(chan struct{}), lastMods: make(map[string]time.Time), } } // Start 开始监听配置文件变化. // 在后台 goroutine 中运行. func (w *Watcher) Start() { // 记录初始修改时间 w.updateModTimes() go w.pollLoop() } // Stop 停止监听. func (w *Watcher) Stop() { close(w.stopCh) } // pollLoop 轮询循环,检测配置文件变化. func (w *Watcher) pollLoop() { ticker := time.NewTicker(w.interval) defer ticker.Stop() for { select { case <-w.stopCh: return case <-ticker.C: if w.hasChanged() { w.updateModTimes() // 重新加载配置 settings, err := LoadSettings(w.cwd) if err == nil && w.onChange != nil { w.onChange(settings) } } } } } // configPaths 返回所有需要监听的配置文件路径. func (w *Watcher) configPaths() []string { paths := make([]string, 0, 3) home, err := os.UserHomeDir() if err == nil { paths = append(paths, filepath.Join(home, ".flyto", "settings.json")) } if w.cwd != "" { paths = append(paths, filepath.Join(w.cwd, ".flyto", "settings.json"), filepath.Join(w.cwd, ".flyto", "settings.local.json"), ) } return paths } // hasChanged 检查配置文件是否有变化. func (w *Watcher) hasChanged() bool { w.mu.Lock() defer w.mu.Unlock() for _, path := range w.configPaths() { info, err := os.Stat(path) if err != nil { // 文件不存在或无法读取 if _, existed := w.lastMods[path]; existed { return true // 文件被删除 } continue } modTime := info.ModTime() if lastMod, ok := w.lastMods[path]; ok { if !modTime.Equal(lastMod) { return true } } else { return true // 新文件 } } return false } // updateModTimes 更新所有配置文件的修改时间快照. func (w *Watcher) updateModTimes() { w.mu.Lock() defer w.mu.Unlock() w.lastMods = make(map[string]time.Time) for _, path := range w.configPaths() { info, err := os.Stat(path) if err == nil { w.lastMods[path] = info.ModTime() } } } // --- 内部函数 --- // defaultSettings 返回默认设置. func defaultSettings() *Settings { return &Settings{ Permissions: PermissionSettings{ DefaultMode: "default", }, MCPServers: make(map[string]MCPServerConfig), Hooks: make(map[string][]HookDef), } } // loadSettingsFile 从 JSON 文件加载设置. func loadSettingsFile(path string) (*Settings, error) { data, err := os.ReadFile(path) if err != nil { return nil, err } var settings Settings if err := json.Unmarshal(data, &settings); err != nil { return nil, err } return &settings, nil } // mergeSettings 将 src 的非零值字段合并到 dst. // src 中的非零值会覆盖 dst 中的对应字段. func mergeSettings(dst, src *Settings) { // 权限设置 if len(src.Permissions.AllowedTools) > 0 { dst.Permissions.AllowedTools = src.Permissions.AllowedTools } if len(src.Permissions.DeniedTools) > 0 { dst.Permissions.DeniedTools = src.Permissions.DeniedTools } if src.Permissions.DefaultMode != "" { dst.Permissions.DefaultMode = src.Permissions.DefaultMode } // MCP 服务器(合并,同名覆盖) if len(src.MCPServers) > 0 { if dst.MCPServers == nil { dst.MCPServers = make(map[string]MCPServerConfig) } for k, v := range src.MCPServers { dst.MCPServers[k] = v } } // 自定义指令(覆盖) if src.CustomInstructions != "" { dst.CustomInstructions = src.CustomInstructions } // Hooks(合并,同类型覆盖) if len(src.Hooks) > 0 { if dst.Hooks == nil { dst.Hooks = make(map[string][]HookDef) } for k, v := range src.Hooks { dst.Hooks[k] = v } } // 标量字段(非零值覆盖) if src.Model != "" { dst.Model = src.Model } if src.APIBaseURL != "" { dst.APIBaseURL = src.APIBaseURL } if src.MaxTurns > 0 { dst.MaxTurns = src.MaxTurns } if src.MaxBudgetUSD > 0 { dst.MaxBudgetUSD = src.MaxBudgetUSD } if src.Verbose { dst.Verbose = true } } // applyEnvOverrides 从环境变量覆盖设置. func applyEnvOverrides(s *Settings) { if v := os.Getenv("FLYTO_MODEL"); v != "" { s.Model = v } if v := os.Getenv("FLYTO_API_BASE_URL"); v != "" { s.APIBaseURL = v } if v := os.Getenv("FLYTO_CUSTOM_INSTRUCTIONS"); v != "" { s.CustomInstructions = v } }