feat: 区分 server 与 desktop 配置加载入口,取消自动创建配置文件
- config.go 重构:抽取 loadConfig 共享逻辑,新增 LoadServerConfig/LoadDesktopConfig/LoadDesktopConfigAtPath,LoadConfig 保持向后兼容 - setupConfigFile 移除 SafeWriteConfigAs 自动创建逻辑,文件不存在时仅使用默认值 - cmd/desktop 切换为 LoadDesktopConfig,端口/HTTP/浏览器/托盘统一使用 cfg.Server.Port - cmd/server 显式使用 LoadServerConfig 明确入口语义 - 提取 desktop 可测 helper:desktopListenAddr/desktopURL/desktopPortMenuTitle/desktopConfigErrorMessage - 新增测试:desktop 忽略 CLI/env/未知参数、配置快照不变、无效配置文件不静默回退、端口 helper 一致性 - README 区分 server/desktop 配置源,移除首次启动自动创建配置文件描述 - 同步 delta specs 到 openspec/specs/ 主规范
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -225,68 +224,71 @@ func setupConfigFile(v *viper.Viper, configPath string) error {
|
||||
v.SetConfigFile(configPath)
|
||||
v.SetConfigType("yaml")
|
||||
|
||||
// 尝试读取配置文件,如果不存在则忽略
|
||||
if err := v.ReadInConfig(); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return appErrors.Wrap(appErrors.ErrInternal, err)
|
||||
}
|
||||
// 配置文件不存在,创建默认配置文件
|
||||
writeErr := v.SafeWriteConfigAs(configPath)
|
||||
if writeErr == nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
|
||||
var alreadyExistsErr viper.ConfigFileAlreadyExistsError
|
||||
if errors.As(writeErr, &alreadyExistsErr) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return appErrors.Wrap(appErrors.ErrInternal, writeErr)
|
||||
return appErrors.Wrap(appErrors.ErrInternal, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadConfig loads config from YAML file, creates default if not exists
|
||||
func LoadConfig() (*Config, error) {
|
||||
configPath, err := GetConfigPath()
|
||||
if err != nil {
|
||||
return nil, appErrors.Wrap(appErrors.ErrInternal, err)
|
||||
}
|
||||
return LoadConfigFromPath(configPath)
|
||||
// loadOptions 控制配置加载器行为
|
||||
type loadOptions struct {
|
||||
configPathOverride string
|
||||
useCLI bool
|
||||
useEnv bool
|
||||
useConfigFlag bool
|
||||
}
|
||||
|
||||
// LoadConfigFromPath 从指定路径加载配置
|
||||
func LoadConfigFromPath(configPath string) (*Config, error) {
|
||||
// 1. 创建 Viper 实例
|
||||
// resolveConfigPath 根据 loadOptions 解析 CLI 参数并返回最终配置文件路径
|
||||
func resolveConfigPath(v *viper.Viper, opts loadOptions) (string, error) {
|
||||
configPath := opts.configPathOverride
|
||||
|
||||
if !opts.useCLI && !opts.useConfigFlag {
|
||||
return configPath, nil
|
||||
}
|
||||
|
||||
flagSet := pflag.NewFlagSet("config", pflag.ContinueOnError)
|
||||
if opts.useConfigFlag {
|
||||
flagSet.String("config", opts.configPathOverride, "配置文件路径")
|
||||
}
|
||||
if opts.useCLI {
|
||||
setupFlags(v, flagSet)
|
||||
}
|
||||
|
||||
if err := flagSet.Parse(os.Args[1:]); err != nil {
|
||||
return "", appErrors.Wrap(appErrors.ErrInvalidRequest, err)
|
||||
}
|
||||
|
||||
if opts.useConfigFlag {
|
||||
if f, err := flagSet.GetString("config"); err == nil && f != "" {
|
||||
configPath = f
|
||||
}
|
||||
}
|
||||
|
||||
return configPath, nil
|
||||
}
|
||||
|
||||
// loadConfig 共享配置加载逻辑,通过 loadOptions 控制是否启用 CLI、环境变量和 --config 覆盖
|
||||
func loadConfig(opts loadOptions) (*Config, error) {
|
||||
v := viper.New()
|
||||
|
||||
// 2. 定义 CLI 参数
|
||||
flagSet := pflag.NewFlagSet("config", pflag.ContinueOnError)
|
||||
flagSet.String("config", configPath, "配置文件路径")
|
||||
setupFlags(v, flagSet)
|
||||
|
||||
// 3. 解析 CLI 参数(忽略错误,因为可能没有参数)
|
||||
if err := flagSet.Parse(os.Args[1:]); err != nil {
|
||||
return nil, appErrors.Wrap(appErrors.ErrInvalidRequest, err)
|
||||
}
|
||||
|
||||
// 4. 获取配置文件路径(可能被 --config 参数覆盖)
|
||||
if configPathFlag, err := flagSet.GetString("config"); err == nil && configPathFlag != "" {
|
||||
configPath = configPathFlag
|
||||
}
|
||||
|
||||
// 5. 设置默认值
|
||||
setupDefaults(v)
|
||||
|
||||
// 6. 绑定环境变量
|
||||
setupEnv(v)
|
||||
configPath, err := resolveConfigPath(v, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if opts.useEnv {
|
||||
setupEnv(v)
|
||||
}
|
||||
|
||||
// 7. 读取配置文件
|
||||
if err := setupConfigFile(v, configPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 8. 反序列化到结构体
|
||||
cfg := &Config{}
|
||||
if err := v.Unmarshal(cfg, viper.DecodeHook(mapstructure.ComposeDecodeHookFunc(
|
||||
mapstructure.StringToTimeDurationHookFunc(),
|
||||
@@ -295,7 +297,6 @@ func LoadConfigFromPath(configPath string) (*Config, error) {
|
||||
return nil, appErrors.Wrap(appErrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
// 9. 验证配置
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -303,6 +304,61 @@ func LoadConfigFromPath(configPath string) (*Config, error) {
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// LoadServerConfig 为 server 入口加载配置,支持 CLI 参数、环境变量和 --config
|
||||
func LoadServerConfig() (*Config, error) {
|
||||
configPath, err := GetConfigPath()
|
||||
if err != nil {
|
||||
return nil, appErrors.Wrap(appErrors.ErrInternal, err)
|
||||
}
|
||||
return loadConfig(loadOptions{
|
||||
configPathOverride: configPath,
|
||||
useCLI: true,
|
||||
useEnv: true,
|
||||
useConfigFlag: true,
|
||||
})
|
||||
}
|
||||
|
||||
// LoadDesktopConfig 为 desktop 入口加载配置,固定使用默认配置文件,不支持 CLI、环境变量和 --config
|
||||
func LoadDesktopConfig() (*Config, error) {
|
||||
configPath, err := GetConfigPath()
|
||||
if err != nil {
|
||||
return nil, appErrors.Wrap(appErrors.ErrInternal, err)
|
||||
}
|
||||
return loadConfig(loadOptions{
|
||||
configPathOverride: configPath,
|
||||
useCLI: false,
|
||||
useEnv: false,
|
||||
useConfigFlag: false,
|
||||
})
|
||||
}
|
||||
|
||||
// LoadConfig loads config from YAML file.
|
||||
// 向后兼容,等同于 LoadServerConfig。
|
||||
func LoadConfig() (*Config, error) {
|
||||
return LoadServerConfig()
|
||||
}
|
||||
|
||||
// LoadConfigFromPath 从指定路径加载配置。
|
||||
// 保留向后兼容,沿用 server 语义(支持 CLI、env 和 --config 覆盖)。
|
||||
func LoadConfigFromPath(configPath string) (*Config, error) {
|
||||
return loadConfig(loadOptions{
|
||||
configPathOverride: configPath,
|
||||
useCLI: true,
|
||||
useEnv: true,
|
||||
useConfigFlag: true,
|
||||
})
|
||||
}
|
||||
|
||||
// LoadDesktopConfigAtPath 从指定路径以 desktop 语义加载配置(仅配置文件和默认值),用于测试场景。
|
||||
func LoadDesktopConfigAtPath(configPath string) (*Config, error) {
|
||||
return loadConfig(loadOptions{
|
||||
configPathOverride: configPath,
|
||||
useCLI: false,
|
||||
useEnv: false,
|
||||
useConfigFlag: false,
|
||||
})
|
||||
}
|
||||
|
||||
// SaveConfig saves config to YAML file
|
||||
func SaveConfig(cfg *Config) error {
|
||||
configPath, err := GetConfigPath()
|
||||
|
||||
Reference in New Issue
Block a user