diff --git a/changelog.md b/changelog.md index de62855..7cf4dfe 100644 --- a/changelog.md +++ b/changelog.md @@ -468,4 +468,34 @@ yoyo onboard --force ### 版本号规则 - 版本号需与 git 标签、changelog.md 中的版本号保持三方同步 -**讨论记录**: [taolun.md#版本-100-beta-Logo和信息栏改造](taolun.md#版本-100-beta-logo和信息栏改造) \ No newline at end of file +**讨论记录**: [taolun.md#版本-100-beta-Logo和信息栏改造](taolun.md#版本-100-beta-logo和信息栏改造) + +--- + +## v1.1.0 (2026-04-07) + +### BUG修复 +- 修复配置文件路径使用相对路径导致管道模式下无法找到配置的问题 +- 修复onboard配置保存到错误路径的问题 +- 修复.env文件只在当前目录加载的问题 + +### 新功能 +- 配置文件路径智能解析:`~/.config/yoyo/config.yaml` → `./configs/config.yaml` +- onboard配置向导迁移到 `charm.land/huh/v2`,替代 `survey` 库 +- 新增路径解析工具 `internal/config/path.go` + +### 改进 +- 配置查找优先级:`--config` 参数 > `~/.config/yoyo/config.yaml` > `./configs/config.yaml` +- 配置文件统一保存到 `~/.config/yoyo/config.yaml`(符合XDG规范) +- .env文件统一从 `~/.config/yoyo/.env` 加载 +- onboard使用huh的Form+Group模式,更美观的交互体验 +- 移除 `github.com/AlecAivazis/survey/v2` 依赖 + +### 技术细节 +- 新增 `config.ResolveConfigPath()` 函数处理路径解析 +- 新增 `config.GetUserConfigPath()` 返回标准配置路径 +- 新增 `config.GetUserEnvPath()` 返回标准环境变量路径 +- 支持 `~` 路径展开 +- huh使用v2版本,支持泛型和链式API + +**讨论记录**: [taolun.md#2026-04-07-配置路径修复和huh迁移](taolun.md) \ No newline at end of file diff --git a/cmd/yoyo/main.go b/cmd/yoyo/main.go index 0a181ad..179811d 100644 --- a/cmd/yoyo/main.go +++ b/cmd/yoyo/main.go @@ -38,7 +38,7 @@ var ( interactiveShort = flag.Bool("i", false, "启动交互式翻译界面(-i的短格式)") ) -const versionString = "YOYO翻译工具 v0.5.1" +const versionString = "YOYO翻译工具 v1.1.0" // isPipeInput 检测是否有管道输入 func isPipeInput() bool { @@ -192,12 +192,13 @@ func main() { } // 加载环境变量文件 - _ = godotenv.Load() // 忽略错误,如果文件不存在 + _ = godotenv.Load(config.GetUserEnvPath()) // 加载配置 - configPath := *configFile - if configPath == "" { - configPath = "configs/config.yaml" // 默认配置文件路径 + configPath, err := config.ResolveConfigPath(*configFile) + if err != nil { + fmt.Printf("解析配置路径失败: %v\n", err) + os.Exit(1) } configLoader := &config.YAMLConfigLoader{} @@ -300,10 +301,14 @@ func runOnboard(force bool) { // runCacheCommand 运行缓存命令 func runCacheCommand(subcommand string) { // 加载环境变量文件 - _ = godotenv.Load() + _ = godotenv.Load(config.GetUserEnvPath()) // 加载配置 - configPath := "configs/config.yaml" + configPath, err := config.ResolveConfigPath("") + if err != nil { + fmt.Printf("解析配置路径失败: %v\n", err) + os.Exit(1) + } configLoader := &config.YAMLConfigLoader{} cfg, err := configLoader.Load(configPath) if err != nil { @@ -434,9 +439,9 @@ func printHelp() { yoyo --interactive # 启动交互式翻译界面 yoyo -i # 启动交互式翻译界面(短格式) -配置: - - 配置文件: configs/config.yaml - - 环境变量: .env 文件 + 配置: + - 配置文件: ~/.config/yoyo/config.yaml + - 环境变量: ~/.config/yoyo/.env - 默认厂商: siliconflow - 默认目标语言: zh-CN (简体中文) @@ -463,12 +468,13 @@ func startInteractiveMode() { } // 加载环境变量文件 - _ = godotenv.Load() // 忽略错误,如果文件不存在 + _ = godotenv.Load(config.GetUserEnvPath()) // 加载配置 - configPath := *configFile - if configPath == "" { - configPath = "configs/config.yaml" // 默认配置文件路径 + configPath, err := config.ResolveConfigPath(*configFile) + if err != nil { + fmt.Printf("解析配置路径失败: %v\n", err) + os.Exit(1) } configLoader := &config.YAMLConfigLoader{} diff --git a/go.mod b/go.mod index 738e44b..b12ed93 100644 --- a/go.mod +++ b/go.mod @@ -5,9 +5,8 @@ go 1.26.1 require ( charm.land/bubbles/v2 v2.1.0 charm.land/bubbletea/v2 v2.0.2 + charm.land/huh/v2 v2.0.3 charm.land/lipgloss/v2 v2.0.2 - github.com/AlecAivazis/survey/v2 v2.3.7 - github.com/charmbracelet/bubbles v1.0.0 github.com/go-enry/go-enry/v2 v2.9.5 github.com/joho/godotenv v1.5.1 github.com/mattn/go-sqlite3 v1.14.37 @@ -16,34 +15,25 @@ require ( require ( github.com/atotto/clipboard v0.1.4 // indirect - github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/charmbracelet/bubbletea v1.3.10 // indirect + github.com/catppuccin/go v0.2.0 // indirect github.com/charmbracelet/colorprofile v0.4.2 // indirect - github.com/charmbracelet/lipgloss v1.1.0 // indirect github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect github.com/charmbracelet/x/ansi v0.11.6 // indirect - github.com/charmbracelet/x/cellbuf v0.0.15 // indirect + github.com/charmbracelet/x/exp/ordered v0.1.0 // indirect + github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect github.com/charmbracelet/x/termios v0.1.1 // indirect github.com/charmbracelet/x/windows v0.2.2 // indirect github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect - github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/go-enry/go-oniguruma v1.2.1 // indirect - github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect - github.com/mattn/go-colorable v0.1.2 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.21 // indirect - github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect - github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/muesli/cancelreader v0.2.2 // indirect - github.com/muesli/termenv v0.16.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.42.0 // indirect - golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect - golang.org/x/text v0.4.0 // indirect ) diff --git a/go.sum b/go.sum index 99e180a..8f12bef 100644 --- a/go.sum +++ b/go.sum @@ -2,84 +2,69 @@ charm.land/bubbles/v2 v2.1.0 h1:YSnNh5cPYlYjPxRrzs5VEn3vwhtEn3jVGRBT3M7/I0g= charm.land/bubbles/v2 v2.1.0/go.mod h1:l97h4hym2hvWBVfmJDtrEHHCtkIKeTEb3TTJ4ZOB3wY= charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0= charm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ= +charm.land/huh/v2 v2.0.3 h1:2cJsMqEPwSywGHvdlKsJyQKPtSJLVnFKyFbsYZTlLkU= +charm.land/huh/v2 v2.0.3/go.mod h1:93eEveeeqn47MwiC3tf+2atZ2l7Is88rAtmZNZ8x9Wc= charm.land/lipgloss/v2 v2.0.2 h1:xFolbF8JdpNkM2cEPTfXEcW1p6NRzOWTSamRfYEw8cs= charm.land/lipgloss/v2 v2.0.2/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM= -github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= -github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= -github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= -github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= -github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o= github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w= -github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc= -github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E= -github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= -github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= +github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA= +github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY= github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8= -github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= -github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA= github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98= github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= -github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= -github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= +github.com/charmbracelet/x/conpty v0.1.1 h1:s1bUxjoi7EpqiXysVtC+a8RrvPPNcNvAjfi4jxsAuEs= +github.com/charmbracelet/x/conpty v0.1.1/go.mod h1:OmtR77VODEFbiTzGE9G1XiRJAga6011PIm4u5fTNZpk= +github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA= +github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0= github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA= github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I= +github.com/charmbracelet/x/exp/ordered v0.1.0 h1:55/qLwjIh0gL0Vni+QAWk7T/qRVP6sBf+2agPBgnOFE= +github.com/charmbracelet/x/exp/ordered v0.1.0/go.mod h1:5UHwmG+is5THxMyCJHNPCn2/ecI07aKNrW+LcResjJ8= +github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4= +github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM= github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k= +github.com/charmbracelet/x/xpty v0.1.3 h1:eGSitii4suhzrISYH50ZfufV3v085BXQwIytcOdFSsw= +github.com/charmbracelet/x/xpty v0.1.3/go.mod h1:poPYpWuLDBFCKmKLDnhBp51ATa0ooD8FhypRwEFtH3Y= github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= -github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= -github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/go-enry/go-enry/v2 v2.9.5 h1:HPhAQQHYwJgihL2PxBZiUMFWiROsGwOBdB6/D8zCUhY= github.com/go-enry/go-enry/v2 v2.9.5/go.mod h1:9yrj4ES1YrbNb1Wb7/PWYr2bpaCXUGRt0uafN0ISyG8= github.com/go-enry/go-oniguruma v1.2.1 h1:k8aAMuJfMrqm/56SG2lV9Cfti6tC4x8673aHCcBk+eo= github.com/go-enry/go-oniguruma v1.2.1/go.mod h1:bWDhYP+S6xZQgiRL7wlTScFYBe023B6ilRZbCAD5Hf4= -github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= -github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= -github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= -github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w= github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-sqlite3 v1.14.37 h1:3DOZp4cXis1cUIpCfXLtmlGolNLp2VEqhiB/PARNBIg= github.com/mattn/go-sqlite3 v1.14.37/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= -github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= +github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= -github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= -github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= @@ -87,48 +72,18 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/config/path.go b/internal/config/path.go new file mode 100644 index 0000000..a215f91 --- /dev/null +++ b/internal/config/path.go @@ -0,0 +1,76 @@ +package config + +import ( + "fmt" + "os" + "path/filepath" + "strings" +) + +const ( + // ConfigDirName 配置目录名称 + ConfigDirName = "yoyo" + // ConfigFileName 配置文件名 + ConfigFileName = "config.yaml" + // EnvFileName 环境变量文件名 + EnvFileName = ".env" +) + +// ResolveConfigPath 解析配置文件路径 +// 优先级: 用户指定路径 > ~/.config/yoyo/config.yaml > ./configs/config.yaml +func ResolveConfigPath(userPath string) (string, error) { + // 1. 用户通过 --config 指定的路径 + if userPath != "" { + return expandPath(userPath) + } + + // 2. 标准用户配置目录 ~/.config/yoyo/config.yaml + userConfigPath := GetUserConfigPath() + if _, err := os.Stat(userConfigPath); err == nil { + return userConfigPath, nil + } + + // 3. 项目本地配置 ./configs/config.yaml(向后兼容) + localConfigPath := "configs/config.yaml" + if _, err := os.Stat(localConfigPath); err == nil { + return localConfigPath, nil + } + + // 4. 都不存在,返回标准路径(onboard 会创建) + return userConfigPath, nil +} + +// GetUserConfigDir 获取用户配置目录路径 +// 返回 ~/.config/yoyo +func GetUserConfigDir() string { + home, err := os.UserHomeDir() + if err != nil { + // 降级到当前目录 + return ".config/" + ConfigDirName + } + return filepath.Join(home, ".config", ConfigDirName) +} + +// GetUserConfigPath 获取用户配置文件路径 +// 返回 ~/.config/yoyo/config.yaml +func GetUserConfigPath() string { + return filepath.Join(GetUserConfigDir(), ConfigFileName) +} + +// GetUserEnvPath 获取用户环境变量文件路径 +// 返回 ~/.config/yoyo/.env +func GetUserEnvPath() string { + return filepath.Join(GetUserConfigDir(), EnvFileName) +} + +// expandPath 展开路径中的 ~ 符号 +func expandPath(path string) (string, error) { + if strings.HasPrefix(path, "~") { + home, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("无法获取用户主目录: %w", err) + } + path = filepath.Join(home, path[1:]) + } + return path, nil +} diff --git a/internal/onboard/onboard.go b/internal/onboard/onboard.go index bb286ba..e47a2f8 100644 --- a/internal/onboard/onboard.go +++ b/internal/onboard/onboard.go @@ -1,30 +1,39 @@ package onboard import ( + "errors" "fmt" "os" "path/filepath" + "strconv" + "strings" - "github.com/AlecAivazis/survey/v2" + "charm.land/huh/v2" "github.com/titor/fanyi/internal/config" "github.com/titor/fanyi/internal/lang" ) // RunOnboard 启动配置向导 func RunOnboard(force bool) error { - fmt.Println("欢迎使用YOYO翻译工具配置向导!") - fmt.Println("这个向导将帮助您配置翻译工具。") - fmt.Println() + configPath := config.GetUserConfigPath() // 检查配置文件是否存在 - configPath := "configs/config.yaml" if _, err := os.Stat(configPath); err == nil && !force { - overwrite := false - prompt := &survey.Confirm{ - Message: "检测到配置文件已存在,是否要重新配置?", - Default: false, - } - if err := survey.AskOne(prompt, &overwrite); err != nil { + var overwrite bool + form := huh.NewForm( + huh.NewGroup( + huh.NewConfirm(). + Title("检测到配置文件已存在,是否要重新配置?"). + Affirmative("是"). + Negative("否"). + Value(&overwrite), + ), + ) + if err := form.Run(); err != nil { + if errors.Is(err, huh.ErrUserAborted) { + fmt.Println("\n你已取消本次配置") + return nil + } return fmt.Errorf("用户输入错误: %w", err) } if !overwrite { @@ -34,30 +43,75 @@ func RunOnboard(force bool) error { } // 步骤1: 选择主要厂商 - fmt.Println("步骤1: 选择主要翻译服务提供商") - providerName, err := SelectProvider() - if err != nil { + var providerName string + providerForm := huh.NewForm( + huh.NewGroup( + huh.NewSelect[string](). + Title("请选择要使用的翻译服务提供商"). + Options( + huh.NewOption("硅基流动 (推荐,免费额度)", "siliconflow"), + huh.NewOption("火山引擎", "volcano"), + huh.NewOption("国家超算", "national"), + huh.NewOption("Qwen (通义千问)", "qwen"), + huh.NewOption("OpenAI兼容格式", "openai"), + ). + Value(&providerName), + ), + ) + if err := providerForm.Run(); err != nil { + if errors.Is(err, huh.ErrUserAborted) { + fmt.Println("\n你已取消本次配置") + return nil + } return fmt.Errorf("选择厂商失败: %w", err) } // 步骤2: 配置主要厂商 - fmt.Println("\n步骤2: 配置主要厂商") - providerConfig, err := ConfigureProvider(providerName) + providerConfig, err := ConfigureProviderHuh(providerName) if err != nil { + if errors.Is(err, huh.ErrUserAborted) { + fmt.Println("\n你已取消本次配置") + return nil + } return fmt.Errorf("配置厂商失败: %w", err) } // 步骤3: 全局设置 - fmt.Println("\n步骤3: 全局设置") - globalConfig, err := GlobalSettings() + globalConfig, err := GlobalSettingsHuh() if err != nil { + if errors.Is(err, huh.ErrUserAborted) { + fmt.Println("\n你已取消本次配置") + return nil + } return fmt.Errorf("全局设置失败: %w", err) } // 步骤4: 确认并保存配置 - fmt.Println("\n步骤4: 保存配置") configData := BuildConfig(providerName, providerConfig, globalConfig) + var confirmSave bool + confirmForm := huh.NewForm( + huh.NewGroup( + huh.NewConfirm(). + Title("确认保存配置?"). + Description(fmt.Sprintf("配置文件将保存到: %s", configPath)). + Affirmative("是,保存"). + Negative("否,取消"). + Value(&confirmSave), + ), + ) + if err := confirmForm.Run(); err != nil { + if errors.Is(err, huh.ErrUserAborted) { + fmt.Println("\n你已取消本次配置") + return nil + } + return fmt.Errorf("用户输入错误: %w", err) + } + if !confirmSave { + fmt.Println("配置已取消。") + return nil + } + if err := SaveConfig(configData, configPath); err != nil { return fmt.Errorf("保存配置失败: %w", err) } @@ -71,54 +125,17 @@ func RunOnboard(force bool) error { return nil } -// SelectProvider 选择主要厂商 -func SelectProvider() (string, error) { - providers := []string{ - "siliconflow", - "volcano", - "national", - "qwen", - "openai", - } - - providerNames := map[string]string{ - "siliconflow": "硅基流动 (推荐,免费额度)", - "volcano": "火山引擎", - "national": "国家超算", - "qwen": "Qwen (通义千问)", - "openai": "OpenAI兼容格式", - } - - var selected string - prompt := &survey.Select{ - Message: "请选择要使用的翻译服务提供商:", - Options: func() []string { - var opts []string - for _, p := range providers { - opts = append(opts, providerNames[p]) - } - return opts - }(), - Default: providerNames["siliconflow"], - } - - if err := survey.AskOne(prompt, &selected); err != nil { - return "", err - } - - // 返回对应的厂商名称 - for name, displayName := range providerNames { - if displayName == selected { - return name, nil - } - } - - return "siliconflow", nil +// GlobalConfig 全局设置配置 +type GlobalConfig struct { + DefaultProvider string + DefaultModel string + Timeout int + DefaultSourceLang string + DefaultTargetLang string } -// ConfigureProvider 配置厂商 -func ConfigureProvider(providerName string) (config.ProviderConfig, error) { - // 厂商默认配置 +// ConfigureProviderHuh 使用 huh 配置厂商 +func ConfigureProviderHuh(providerName string) (config.ProviderConfig, error) { defaults := map[string]config.ProviderConfig{ "siliconflow": { APIHost: "https://api.siliconflow.cn/v1", @@ -154,47 +171,43 @@ func ConfigureProvider(providerName string) (config.ProviderConfig, error) { Enabled: defaultConfig.Enabled, } - // 输入API密钥 - apiKeyPrompt := &survey.Input{ - Message: fmt.Sprintf("请输入 %s 的API密钥:", providerName), - Help: "API密钥用于身份验证,将存储在配置文件中", - } - if err := survey.AskOne(apiKeyPrompt, &cfg.APIKey, survey.WithValidator(survey.Required)); err != nil { - return config.ProviderConfig{}, err - } - - // 确认API HOST - apiHostPrompt := &survey.Input{ - Message: "API HOST (直接回车使用默认值):", - Default: cfg.APIHost, - } - if err := survey.AskOne(apiHostPrompt, &cfg.APIHost); err != nil { - return config.ProviderConfig{}, err - } - - // 确认默认模型 - modelPrompt := &survey.Input{ - Message: "默认模型 (直接回车使用默认值):", - Default: cfg.Model, - } - if err := survey.AskOne(modelPrompt, &cfg.Model); err != nil { + var apiKey string + apiKeyForm := huh.NewForm( + huh.NewGroup( + huh.NewInput(). + Title(fmt.Sprintf("请输入 %s 的API密钥", providerName)). + Description("API密钥用于身份验证,将存储在配置文件中"). + Value(&apiKey). + Validate(func(str string) error { + if strings.TrimSpace(str) == "" { + return fmt.Errorf("API密钥不能为空") + } + return nil + }), + + huh.NewInput(). + Title("API HOST"). + Description("直接回车使用默认值"). + Value(&cfg.APIHost). + Placeholder(defaultConfig.APIHost), + + huh.NewInput(). + Title("默认模型"). + Description("直接回车使用默认值"). + Value(&cfg.Model). + Placeholder(defaultConfig.Model), + ), + ) + if err := apiKeyForm.Run(); err != nil { return config.ProviderConfig{}, err } + cfg.APIKey = apiKey return cfg, nil } -// GlobalSettings 全局设置 -type GlobalConfig struct { - DefaultProvider string - DefaultModel string - Timeout int - DefaultSourceLang string - DefaultTargetLang string -} - -// GlobalSettings 全局设置 -func GlobalSettings() (*GlobalConfig, error) { +// GlobalSettingsHuh 使用 huh 进行全局设置 +func GlobalSettingsHuh() (*GlobalConfig, error) { cfg := &GlobalConfig{ DefaultProvider: "siliconflow", DefaultModel: "siliconflow-base", @@ -203,43 +216,33 @@ func GlobalSettings() (*GlobalConfig, error) { DefaultTargetLang: "zh-CN", } - // 选择默认语言 targetLangOptions := lang.GetCommonLanguages() - var targetLangDisplay []string + var options []huh.Option[string] for _, code := range targetLangOptions { - targetLangDisplay = append(targetLangDisplay, fmt.Sprintf("%s (%s)", code, lang.GetLanguageName(code))) + options = append(options, huh.NewOption( + fmt.Sprintf("%s (%s)", code, lang.GetLanguageName(code)), + code, + )) } - targetLangPrompt := &survey.Select{ - Message: "请选择默认目标语言:", - Options: targetLangDisplay, - Default: fmt.Sprintf("%s (%s)", "zh-CN", lang.GetLanguageName("zh-CN")), - } - - var selectedTarget string - if err := survey.AskOne(targetLangPrompt, &selectedTarget); err != nil { - return nil, err - } - - // 从选择中提取语言代码 - for i, display := range targetLangDisplay { - if display == selectedTarget { - cfg.DefaultTargetLang = targetLangOptions[i] - break - } - } - - // 设置超时时间 - timeoutPrompt := &survey.Input{ - Message: "API超时时间(秒):", - Default: fmt.Sprintf("%d", cfg.Timeout), - } var timeoutStr string - if err := survey.AskOne(timeoutPrompt, &timeoutStr); err != nil { + form := huh.NewForm( + huh.NewGroup( + huh.NewSelect[string](). + Title("请选择默认目标语言"). + Options(options...). + Value(&cfg.DefaultTargetLang), + + huh.NewInput(). + Title("API超时时间(秒)"). + Value(&timeoutStr). + Placeholder("30"), + ), + ) + if err := form.Run(); err != nil { return nil, err } - // 解析超时时间 if timeout := parseIntOrDefault(timeoutStr, 30); timeout > 0 { cfg.Timeout = timeout } @@ -249,12 +252,10 @@ func GlobalSettings() (*GlobalConfig, error) { // BuildConfig 构建配置对象 func BuildConfig(providerName string, providerConfig config.ProviderConfig, globalConfig *GlobalConfig) *config.Config { - // 创建厂商配置 providers := map[string]config.ProviderConfig{ providerName: providerConfig, } - // 创建Prompt配置 prompts := map[string]string{ "technical": "你是一位专业的技术翻译,请准确翻译以下技术文档,保持专业术语的准确性。", "creative": "你是一位富有创造力的翻译家,请用优美流畅的语言翻译以下内容。", @@ -275,25 +276,24 @@ func BuildConfig(providerName string, providerConfig config.ProviderConfig, glob // SaveConfig 保存配置文件 func SaveConfig(cfg *config.Config, path string) error { - // 确保目录存在 dir := filepath.Dir(path) if err := os.MkdirAll(dir, 0755); err != nil { return fmt.Errorf("创建配置目录失败: %w", err) } - // 使用config包的Save方法 loader := &config.YAMLConfigLoader{} return loader.Save(cfg, path) } // parseIntOrDefault 解析整数,失败时返回默认值 func parseIntOrDefault(s string, defaultValue int) int { + s = strings.TrimSpace(s) if s == "" { return defaultValue } - var result int - if _, err := fmt.Sscanf(s, "%d", &result); err != nil { + result, err := strconv.Atoi(s) + if err != nil { return defaultValue } diff --git a/taolun.md b/taolun.md index 0659837..3a2e91e 100644 --- a/taolun.md +++ b/taolun.md @@ -794,4 +794,41 @@ ta.SetHeight(5) // 固定高度,不动态调整 - 遵循语义化版本:主版本.次版本.修订版本 - beta版使用 `-beta` 后缀 -**关联版本**: [changelog.md#1.0.0-beta](changelog.md#100-beta-2026-04-07) \ No newline at end of file +**关联版本**: [changelog.md#1.0.0-beta](changelog.md#100-beta-2026-04-07) + +--- + +### [2026-04-07] 版本 1.1.0 - 配置路径修复和huh迁移 + +**原因**: +1. 管道模式下(如 `cd /docs && cat readme.md | yoyo`)找不到配置文件 +2. onboard配置保存到错误的相对路径 +3. 希望用 `charmbracelet/huh` 替代 `survey` 获得更好的UX + +**分析**: +- 所有配置路径硬编码为 `configs/config.yaml`(相对CWD) +- 从不同目录运行程序时路径解析失败 +- survey库API较老,huh提供更现代的表单体验 + +**解决方案**: +1. 新增 `internal/config/path.go` 路径解析工具 +2. 配置查找优先级:`--config` > `~/.config/yoyo/config.yaml` > `./configs/config.yaml` +3. onboard保存到 `~/.config/yoyo/config.yaml` +4. .env从 `~/.config/yoyo/.env` 加载 +5. onboard使用huh重写:Form+Group模式,链式API,泛型支持 + +**技术细节**: +```go +// 路径解析 +config.ResolveConfigPath(userPath) // 智能查找配置 +config.GetUserConfigPath() // ~/.config/yoyo/config.yaml +config.GetUserEnvPath() // ~/.config/yoyo/.env +``` + +**huh迁移要点**: +- `survey.Select` → `huh.NewSelect[string]().Options(huh.NewOption(...)...)` +- `survey.Input` → `huh.NewInput().Value(&var).Validate(fn)` +- `survey.Confirm` → `huh.NewConfirm().Affirmative("是").Negative("否")` +- 分步表单 → `huh.NewForm(huh.NewGroup(...), huh.NewGroup(...))` + +**关联版本**: [changelog.md#1.1.0](changelog.md#110-2026-04-07) \ No newline at end of file