From c2c3b11d35410d0d14d725a411405362cae625c6 Mon Sep 17 00:00:00 2001 From: titor Date: Tue, 7 Apr 2026 08:46:15 +0800 Subject: [PATCH] add: cmd directory --- cmd/yoyo/main.go | 553 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 553 insertions(+) create mode 100644 cmd/yoyo/main.go diff --git a/cmd/yoyo/main.go b/cmd/yoyo/main.go new file mode 100644 index 0000000..0a181ad --- /dev/null +++ b/cmd/yoyo/main.go @@ -0,0 +1,553 @@ +package main + +import ( + "bufio" + "context" + "flag" + "fmt" + "os" + "os/signal" + "strings" + "syscall" + + "github.com/joho/godotenv" + "github.com/titor/fanyi/internal/cache" + "github.com/titor/fanyi/internal/config" + "github.com/titor/fanyi/internal/lang" + "github.com/titor/fanyi/internal/onboard" + "github.com/titor/fanyi/internal/provider" + "github.com/titor/fanyi/internal/translator" + "github.com/titor/fanyi/internal/tui" +) + +var ( + // 命令行参数 + version = flag.Bool("version", false, "显示版本信息") + help = flag.Bool("help", false, "显示帮助信息") + h = flag.Bool("h", false, "显示帮助信息") + langFlag = flag.String("lang", "", "目标语言代码(如 zh-CN, en-US, cn, en 等)") + langLong = flag.String("language", "", "目标语言代码(--lang的长格式)") + configFile = flag.String("config", "", "配置文件路径") + providerFlag = flag.String("provider", "", "指定翻译厂商") + promptFlag = flag.String("prompt", "", "指定Prompt模式") + onboardFlag = flag.Bool("onboard", false, "启动交互式配置向导") + onboardForce = flag.Bool("onboard-force", false, "强制重新配置") + quietFlag = flag.Bool("quiet", false, "静默模式,不显示统计信息") + quietShort = flag.Bool("q", false, "静默模式,不显示统计信息(-q的短格式)") + interactive = flag.Bool("interactive", false, "启动交互式翻译界面") + interactiveShort = flag.Bool("i", false, "启动交互式翻译界面(-i的短格式)") +) + +const versionString = "YOYO翻译工具 v0.5.1" + +// isPipeInput 检测是否有管道输入 +func isPipeInput() bool { + fileInfo, err := os.Stdin.Stat() + if err != nil { + return false + } + // 检查是否是管道(字符设备且不是普通文件) + return (fileInfo.Mode() & os.ModeCharDevice) == 0 +} + +// readFromStdin 从标准输入读取所有内容 +func readFromStdin() (string, error) { + scanner := bufio.NewScanner(os.Stdin) + var lines []string + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + if err := scanner.Err(); err != nil { + return "", fmt.Errorf("读取标准输入失败: %w", err) + } + return strings.Join(lines, "\n"), nil +} + +// isQuiet 检查是否处于静默模式 +func isQuiet() bool { + return *quietFlag || *quietShort +} + +// isTTY 检查是否在TTY环境中 +func isTTY() bool { + // 检查标准输出是否是TTY + if fi, err := os.Stdout.Stat(); err == nil { + if (fi.Mode() & os.ModeCharDevice) != 0 { + return true + } + } + + // 检查标准错误是否是TTY + if fi, err := os.Stderr.Stat(); err == nil { + if (fi.Mode() & os.ModeCharDevice) != 0 { + return true + } + } + + // 检查标准输入是否是TTY + if fi, err := os.Stdin.Stat(); err == nil { + if (fi.Mode() & os.ModeCharDevice) != 0 { + return true + } + } + + return false +} + +func main() { + // 解析命令行参数 + flag.Parse() + + // 处理版本和帮助 + if *version { + printVersion() + return + } + + // 处理交互式模式 + if *interactive || *interactiveShort || shouldStartInteractive() { + startInteractiveMode() + return + } + + // 处理管道输入情况 + if isPipeInput() { + // 管道模式下,即使没有参数也继续执行 + if *help || *h { + printHelp() + return + } + } else { + // 非管道模式下,没有参数则显示帮助 + if *help || *h || flag.NArg() == 0 { + printHelp() + return + } + } + + // 获取第一个参数作为命令或文本 + firstArg := flag.Arg(0) + + // 处理子命令 + if firstArg == "onboard" { + force := false + // 检查是否有--force参数 + for _, arg := range os.Args[1:] { + if arg == "--force" || arg == "-f" { + force = true + break + } + } + runOnboard(force) + return + } + + // 处理缓存命令 + if firstArg == "cache" { + if flag.NArg() < 2 { + fmt.Fprintln(os.Stderr, "错误: cache命令需要子命令") + fmt.Fprintln(os.Stderr, "可用子命令: clear, stats, cleanup") + os.Exit(1) + } + cacheSubcommand := flag.Arg(1) + runCacheCommand(cacheSubcommand) + return + } + + // 创建上下文,支持Ctrl+C中断 + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // 处理中断信号 + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + go func() { + <-sigChan + fmt.Println("\n收到中断信号,正在退出...") + cancel() + }() + + // 获取要翻译的文本 + var text string + var err error + if isPipeInput() { + // 从stdin读取 + text, err = readFromStdin() + if err != nil { + fmt.Fprintf(os.Stderr, "读取管道输入失败: %v\n", err) + os.Exit(1) + } + // 如果没有输入内容,显示错误 + if strings.TrimSpace(text) == "" { + fmt.Fprintln(os.Stderr, "错误: 管道输入为空") + os.Exit(1) + } + } else { + // 从命令行参数读取 + text = firstArg + if text == "" { + printHelp() + os.Exit(1) + } + } + + // 加载环境变量文件 + _ = godotenv.Load() // 忽略错误,如果文件不存在 + + // 加载配置 + configPath := *configFile + if configPath == "" { + configPath = "configs/config.yaml" // 默认配置文件路径 + } + + configLoader := &config.YAMLConfigLoader{} + cfg, err := configLoader.Load(configPath) + if err != nil { + fmt.Printf("加载配置失败: %v\n", err) + fmt.Println("提示: 运行 'yoyo onboard' 进行配置") + os.Exit(1) + } + + // 解析语言参数 + targetLang := parseLanguageFlag() + if targetLang == "" { + targetLang = cfg.DefaultTargetLang + } + + // 解析Prompt参数 + promptName := *promptFlag + if promptName == "" { + promptName = "simple" + } + + // 获取厂商配置 + providerName := *providerFlag + if providerName == "" { + providerName = cfg.DefaultProvider + } + + providerConfig, err := cfg.GetProviderConfig(providerName) + if err != nil { + fmt.Printf("获取厂商配置失败: %v\n", err) + os.Exit(1) + } + + // 创建厂商实例 + providerInstance, err := provider.CreateProvider(providerName, provider.ProviderConfig{ + APIHost: providerConfig.APIHost, + APIKey: providerConfig.APIKey, + Model: providerConfig.Model, + }) + if err != nil { + fmt.Printf("创建厂商实例失败: %v\n", err) + os.Exit(1) + } + + // 创建翻译器 + translatorInstance := translator.NewTranslator(cfg, providerInstance) + + // 设置翻译选项 + options := &translator.TranslateOptions{ + ToLang: targetLang, + PromptName: promptName, + } + + // 执行翻译 + result, err := translatorInstance.Translate(ctx, text, options) + if err != nil { + fmt.Printf("翻译失败: %v\n", err) + os.Exit(1) + } + + // 输出结果 + fmt.Println(result.Translated) + + // 根据quiet参数决定是否显示统计信息 + if !isQuiet() && result.Usage != nil { + fmt.Fprintf(os.Stderr, "\n--- 用量统计 ---\n") + fmt.Fprintf(os.Stderr, "模型: %s\n", result.Model) + fmt.Fprintf(os.Stderr, "提示词: %d tokens\n", result.Usage.PromptTokens) + fmt.Fprintf(os.Stderr, "完成: %d tokens\n", result.Usage.CompletionTokens) + fmt.Fprintf(os.Stderr, "总计: %d tokens\n", result.Usage.TotalTokens) + } +} + +// parseLanguageFlag 解析语言参数 +func parseLanguageFlag() string { + // 优先使用--lang参数,然后是--language参数 + language := *langFlag + if language == "" { + language = *langLong + } + + // 如果没有指定语言参数,返回空字符串(将使用配置文件中的默认值) + if language == "" { + return "" + } + + // 使用语言解析模块处理语言代码 + return lang.ParseLanguageCode(language) +} + +// runOnboard 运行配置向导 +func runOnboard(force bool) { + if err := onboard.RunOnboard(force); err != nil { + fmt.Printf("配置向导失败: %v\n", err) + os.Exit(1) + } +} + +// runCacheCommand 运行缓存命令 +func runCacheCommand(subcommand string) { + // 加载环境变量文件 + _ = godotenv.Load() + + // 加载配置 + configPath := "configs/config.yaml" + configLoader := &config.YAMLConfigLoader{} + cfg, err := configLoader.Load(configPath) + if err != nil { + fmt.Printf("加载配置失败: %v\n", err) + os.Exit(1) + } + + // 创建缓存实例 + if !cfg.Cache.Enabled { + fmt.Fprintln(os.Stderr, "缓存功能未启用") + os.Exit(1) + } + + cacheConfig := &cache.CacheConfig{ + Enabled: cfg.Cache.Enabled, + MaxRecords: cfg.Cache.MaxRecords, + ExpireDays: cfg.Cache.ExpireDays, + DBPath: cfg.Cache.DBPath, + } + cacheInstance, err := cache.NewSQLiteCache(cacheConfig) + if err != nil { + fmt.Printf("创建缓存实例失败: %v\n", err) + os.Exit(1) + } + defer cacheInstance.Close() + + cleanupManager := cache.NewCleanupManager(cacheInstance) + ctx := context.Background() + + switch subcommand { + case "clear": + if err := cleanupManager.ClearAll(ctx); err != nil { + fmt.Printf("清空缓存失败: %v\n", err) + os.Exit(1) + } + fmt.Println("缓存已清空") + case "stats": + stats, err := cleanupManager.GetStats(ctx) + if err != nil { + fmt.Printf("获取缓存统计失败: %v\n", err) + os.Exit(1) + } + fmt.Printf("缓存统计:\n") + fmt.Printf(" 总记录数: %d\n", stats.TotalRecords) + fmt.Printf(" 总大小: %.2f MB\n", float64(stats.TotalSizeBytes)/1024/1024) + fmt.Printf(" 最早记录: %v\n", stats.OldestRecord) + fmt.Printf(" 最新记录: %v\n", stats.NewestRecord) + fmt.Printf(" 平均tokens/记录: %.2f\n", stats.AvgTokensPerRecord) + case "cleanup": + if err := cleanupManager.CleanupManual(ctx); err != nil { + fmt.Printf("清理缓存失败: %v\n", err) + os.Exit(1) + } + fmt.Println("缓存清理完成") + default: + fmt.Fprintf(os.Stderr, "未知的缓存子命令: %s\n", subcommand) + fmt.Fprintln(os.Stderr, "可用子命令: clear, stats, cleanup") + os.Exit(1) + } +} + +// printVersion 显示版本信息 +func printVersion() { + fmt.Println(versionString) +} + +// printHelp 显示帮助信息 +func printHelp() { + fmt.Printf(`%s + +使用方法: + yoyo [选项] <文本> + yoyo onboard [选项] + +基本翻译: + yoyo "Hello world" # 翻译为中文(默认) + yoyo --lang=cn "Hello world" # 指定翻译为简体中文 + yoyo --lang=en "你好" # 翻译为英文 + yoyo --lang=zh-TW "Hello world" # 翻译为繁体中文 + +管道使用: + cat file.txt | yoyo # 翻译文件内容 + cat file.txt | yoyo --lang=en # 翻译为英文 + cat file.txt | yoyo -q # 静默模式,只输出翻译结果 + echo "Hello" | yoyo --lang=cn # 翻译命令输出 + yoyo "Hello" | grep "你好" # 与其他命令组合使用 + +语言代码支持: + - 标准格式: zh-CN, zh-TW, en-US, en-GB, ja, ko, es, fr, de 等 + - 简短别名: cn(中文), en(英文), jp(日文), kr(韩文) 等 + - 中文名称: chinese(中文), english(英文), japanese(日文) 等 + +命令: + onboard 启动交互式配置向导 + onboard --force 强制重新配置 + cache clear 清空翻译缓存 + cache stats 查看缓存统计信息 + cache cleanup 清理过期缓存 + +翻译选项: + --lang=<语言代码> 指定目标语言 + --language=<语言代码> 指定目标语言(长格式) + --config=<路径> 指定配置文件路径 + --provider=<厂商> 指定翻译厂商 + --prompt=<模式> 指定Prompt模式 + --quiet, -q 静默模式,不显示统计信息 + --interactive, -i 启动交互式翻译界面 + +通用选项: + -h, --help 显示帮助信息 + --version 显示版本信息 + +示例: + yoyo "Hello world" + yoyo --lang=cn "Hello world" + yoyo --lang=en "你好世界" + yoyo --lang=jp "Hello world" + yoyo --lang=ko "Hello world" + yoyo --provider=siliconflow --lang=cn "Hello" + yoyo --prompt=technical --lang=zh-CN "API documentation" + echo "Hello" | yoyo --lang=cn -q + cat file.txt | yoyo --lang=en + yoyo onboard + yoyo onboard --force + yoyo cache clear # 清空翻译缓存 + yoyo cache stats # 查看缓存统计信息 + yoyo cache cleanup # 清理过期缓存 + yoyo --interactive # 启动交互式翻译界面 + yoyo -i # 启动交互式翻译界面(短格式) + +配置: + - 配置文件: configs/config.yaml + - 环境变量: .env 文件 + - 默认厂商: siliconflow + - 默认目标语言: zh-CN (简体中文) + +更多信息请访问: https://github.com/titor/fanyi +`, versionString) +} + +// shouldStartInteractive 判断是否应该启动交互式模式 +func shouldStartInteractive() bool { + // 如果没有参数,且不是管道输入,则启动交互模式 + if flag.NArg() == 0 && !isPipeInput() { + return true + } + return false +} + +// startInteractiveMode 启动交互式模式 +func startInteractiveMode() { + // 检查是否在TTY环境中 + if !isTTY() { + fmt.Fprintln(os.Stderr, "错误: 交互式模式需要在TTY环境中运行") + fmt.Fprintln(os.Stderr, "请直接使用传统模式: yoyo \"要翻译的文本\"") + os.Exit(1) + } + + // 加载环境变量文件 + _ = godotenv.Load() // 忽略错误,如果文件不存在 + + // 加载配置 + configPath := *configFile + if configPath == "" { + configPath = "configs/config.yaml" // 默认配置文件路径 + } + + configLoader := &config.YAMLConfigLoader{} + cfg, err := configLoader.Load(configPath) + if err != nil { + fmt.Printf("加载配置失败: %v\n", err) + fmt.Println("提示: 运行 'yoyo onboard' 进行配置") + fmt.Println("提示: 使用默认配置启动交互模式...") + + // 使用默认配置 + cfg = &config.Config{ + DefaultProvider: "siliconflow", + DefaultModel: "gpt-3.5-turbo", + Timeout: 30, + DefaultTargetLang: "zh-CN", + Providers: make(map[string]config.ProviderConfig), + Prompts: make(map[string]string), + } + } + + // 解析语言参数 + targetLang := parseLanguageFlag() + if targetLang == "" { + targetLang = cfg.DefaultTargetLang + } + + // 解析Prompt参数 + promptName := *promptFlag + if promptName == "" { + promptName = "simple" + } + + // 获取厂商配置 + providerName := *providerFlag + if providerName == "" { + providerName = cfg.DefaultProvider + } + + providerConfig, err := cfg.GetProviderConfig(providerName) + if err != nil { + fmt.Printf("获取厂商配置失败: %v\n", err) + fmt.Println("使用默认厂商配置...") + + // 使用默认配置 + providerConfig = config.ProviderConfig{ + APIHost: "https://api.siliconflow.cn/v1", + APIKey: "", + Model: "gpt-3.5-turbo", + Enabled: true, + } + } + + // 创建厂商实例 + providerInstance, err := provider.CreateProvider(providerName, provider.ProviderConfig{ + APIHost: providerConfig.APIHost, + APIKey: providerConfig.APIKey, + Model: providerConfig.Model, + }) + if err != nil { + fmt.Printf("创建厂商实例失败: %v\n", err) + fmt.Println("使用模拟翻译功能...") + + // 这里可以创建一个模拟厂商实例 + // 暂时直接返回错误 + fmt.Println("交互式模式需要有效的厂商配置") + return + } + + // 创建翻译器 + translatorInstance := translator.NewTranslator(cfg, providerInstance) + + // 启动TUI界面 + fmt.Println("正在启动交互式翻译界面...") + fmt.Println("使用 Ctrl+C 退出") + + // 创建并运行TUI应用程序 + app := tui.NewApp(cfg, translatorInstance) + if _, err := app.Run(); err != nil { + fmt.Fprintf(os.Stderr, "运行TUI界面失败: %v\n", err) + os.Exit(1) + } +}