Compare commits
3 Commits
v0.0.1-tes
...
v1.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
| d26558ad0a | |||
| c2c3b11d35 | |||
| 34a7e7d208 |
@@ -16,9 +16,9 @@ jobs:
|
||||
- name: Checkout
|
||||
run: |
|
||||
apk add git
|
||||
git clone -b dev https://hub.gaomia.site/titor/yoyo.git project
|
||||
cp -r project/* .
|
||||
cp -r project/.* . 2>/dev/null || true
|
||||
git clone -b dev https://hub.gaomia.site/titor/yoyo.git /workspace/titor/yoyo/project
|
||||
cp -r /workspace/titor/yoyo/project/* /workspace/titor/yoyo/
|
||||
cp -r /workspace/titor/yoyo/project/.* /workspace/titor/yoyo/ 2>/dev/null || true
|
||||
|
||||
- name: Download dependencies
|
||||
run: go mod download
|
||||
@@ -40,6 +40,7 @@ jobs:
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.release_token }}
|
||||
run: |
|
||||
apk add curl
|
||||
TAG_NAME="${GITHUB_REF#refs/tags/}"
|
||||
|
||||
# 创建 Release
|
||||
|
||||
553
cmd/yoyo/main.go
Normal file
553
cmd/yoyo/main.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user