feat: 修复配置路径BUG并迁移onboard到huh
All checks were successful
Release / build (push) Successful in 12m14s
All checks were successful
Release / build (push) Successful in 12m14s
- 新增路径解析工具 internal/config/path.go - 配置查找优先级: --config > ~/.config/yoyo/config.yaml > ./configs/config.yaml - onboard配置保存到 ~/.config/yoyo/config.yaml (符合XDG规范) - .env文件从 ~/.config/yoyo/.env 加载 - onboard使用huh替代survey库,更现代的交互体验 - 添加Ctrl+C取消支持,打印'你已取消本次配置' - 保存前增加确认步骤 - 版本号 v0.5.1 -> v1.1.0
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user