Files
yoyo/AGENTS.md
z.to 24ba405d55 feat: add language support and onboard configuration wizard (v0.2.0)
- Add language code intelligent parsing module (internal/lang)
- Support --lang parameter for target language specification
- Support multiple language code formats (BCP47, aliases, Chinese names)
- Implement interactive onboard configuration wizard
- Update Config struct with language fields
- Add survey library dependency for interactive UI
- Improve CLI command interface
- Add comprehensive unit tests for language module
- Update documentation (AGENTS.md, changelog.md, taolun.md, memory.md)

Supported language codes:
- Standard: zh-CN, zh-TW, en-US, en-GB, ja, ko, es, fr, de
- Aliases: cn, en, jp, kr, es, fr, de
- Chinese names: chinese, english, japanese

Commands:
- yoyo "Hello world" - basic translation
- yoyo --lang=cn "Hello world" - specify target language
- yoyo onboard - start configuration wizard
- yoyo onboard --force - force reconfiguration

Version: 0.2.0
2026-03-29 01:30:42 +08:00

22 KiB
Raw Permalink Blame History

AGENTS.md - YOYO翻译工具开发指南

项目概述

YOYO是一个命令行翻译工具使用Go语言编写采用面向对象设计模式。它通过调用在线大模型API结合不同的Prompt配置实现多样化的翻译特色。

OOP设计模式

核心类设计

项目采用以下面向对象设计,结合工厂模式和策略模式:

┌─────────────────┐      ┌─────────────────┐      ┌─────────────────┐
│     Config      │      │    Provider     │      │   Translator    │
│  (全局配置)     │──────│    (厂商接口)    │──────│   (核心翻译)     │
└─────────────────┘      └─────────────────┘      └─────────────────┘
         │                        │                        │
         │                        │                        │
┌─────────────────┐      ┌─────────────────┐      ┌─────────────────┐
│  ConfigLoader   │      │ ProviderFactory │      │ TranslationTask │
│  (配置加载)     │      │  (工厂模式)     │      │   (任务管理)    │
└─────────────────┘      └─────────────────┘      └─────────────────┘

1. 全局配置类 (Config)

负责读取YAML配置文件提供默认值。

// internal/config/config.go
package config

import (
    "os"
    "path/filepath"
    
    "gopkg.in/yaml.v3"
)

// Config 全局配置结构
type Config struct {
    // 全局设置
    DefaultProvider string            `yaml:"default_provider"`
    DefaultModel    string            `yaml:"default_model"`
    Timeout         int               `yaml:"timeout"` // 秒
    
    // 厂商配置
    Providers map[string]ProviderConfig `yaml:"providers"`
    
    // Prompt配置
    Prompts map[string]string `yaml:"prompts"`
}

// ProviderConfig 厂商配置
type ProviderConfig struct {
    APIHost string `yaml:"api_host"`
    APIKey  string `yaml:"api_key"`
    Model   string `yaml:"model"`
    Enabled bool   `yaml:"enabled"`
}

// ConfigLoader 配置加载器接口
type ConfigLoader interface {
    Load(path string) (*Config, error)
    Save(config *Config, path string) error
}

// YAMLConfigLoader YAML配置加载器实现
type YAMLConfigLoader struct{}

// Load 加载YAML配置文件
func (l *YAMLConfigLoader) Load(path string) (*Config, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, fmt.Errorf("读取配置文件失败: %w", err)
    }
    
    config := &Config{}
    if err := yaml.Unmarshal(data, config); err != nil {
        return nil, fmt.Errorf("解析配置文件失败: %w", err)
    }
    
    // 设置默认值
    config.setDefaults()
    return config, nil
}

// setDefaults 设置默认值
func (c *Config) setDefaults() {
    if c.DefaultProvider == "" {
        c.DefaultProvider = "siliconflow"
    }
    if c.Timeout <= 0 {
        c.Timeout = 30
    }
    // 为每个厂商设置默认值
    for name, provider := range c.Providers {
        if provider.Model == "" {
            provider.Model = "gpt-3.5-turbo"
            c.Providers[name] = provider
        }
    }
}

2. 大模型厂商接口 (Provider)

定义统一的厂商接口,采用策略模式。

// internal/provider/provider.go
package provider

import (
    "context"
)

// Provider 厂商接口
type Provider interface {
    // Translate 调用厂商API进行翻译
    Translate(ctx context.Context, req *TranslateRequest) (*TranslateResponse, error)
    
    // Name 返回厂商名称
    Name() string
    
    // Validate 验证配置是否有效
    Validate() error
}

// TranslateRequest 翻译请求
type TranslateRequest struct {
    Text      string            `json:"text"`
    FromLang  string            `json:"from_lang"`
    ToLang    string            `json:"to_lang"`
    Prompt    string            `json:"prompt"`
    Model     string            `json:"model"`
    Options   map[string]interface{} `json:"options"`
}

// TranslateResponse 翻译响应
type TranslateResponse struct {
    Text       string `json:"text"`
    FromLang   string `json:"from_lang"`
    ToLang     string `json:"to_lang"`
    Model      string `json:"model"`
    Usage      *Usage `json:"usage"`
    RawResponse []byte `json:"raw_response,omitempty"`
}

// Usage 用量统计
type Usage struct {
    PromptTokens     int `json:"prompt_tokens"`
    CompletionTokens int `json:"completion_tokens"`
    TotalTokens      int `json:"total_tokens"`
}

3. 具体厂商实现示例

// internal/provider/siliconflow.go
package provider

import (
    "context"
    "encoding/json"
    "net/http"
)

// SiliconFlowProvider 硅基流动厂商实现
type SiliconFlowProvider struct {
    config ProviderConfig
    client *http.Client
}

// ProviderConfig 厂商配置从config包导入
type ProviderConfig struct {
    APIHost string
    APIKey  string
    Model   string
}

// NewSiliconFlowProvider 创建硅基流动厂商实例
func NewSiliconFlowProvider(config ProviderConfig) *SiliconFlowProvider {
    return &SiliconFlowProvider{
        config: config,
        client: &http.Client{},
    }
}

// Name 返回厂商名称
func (p *SiliconFlowProvider) Name() string {
    return "siliconflow"
}

// Validate 验证配置
func (p *SiliconFlowProvider) Validate() error {
    if p.config.APIKey == "" {
        return fmt.Errorf("siliconflow: API key 不能为空")
    }
    if p.config.APIHost == "" {
        p.config.APIHost = "https://api.siliconflow.cn/v1"
    }
    return nil
}

// Translate 调用硅基流动API
func (p *SiliconFlowProvider) Translate(ctx context.Context, req *TranslateRequest) (*TranslateResponse, error) {
    // 实现具体的API调用逻辑
    // 1. 构建请求体
    // 2. 发送HTTP请求
    // 3. 解析响应
    
    // 示例代码(简化)
    url := p.config.APIHost + "/chat/completions"
    requestBody := map[string]interface{}{
        "model": p.config.Model,
        "messages": []map[string]string{
            {"role": "user", "content": req.Text},
        },
    }
    
    // 实际实现需要完整的HTTP客户端代码
    return &TranslateResponse{
        Text:     "翻译结果",
        FromLang: req.FromLang,
        ToLang:   req.ToLang,
        Model:    p.config.Model,
    }, nil
}

4. 工厂模式 - ProviderFactory

// internal/provider/factory.go
package provider

import (
    "fmt"
)

// ProviderFactory 厂商工厂
type ProviderFactory struct {
    providers map[string]func(ProviderConfig) Provider
}

// NewProviderFactory 创建工厂实例
func NewProviderFactory() *ProviderFactory {
    factory := &ProviderFactory{
        providers: make(map[string]func(ProviderConfig) Provider),
    }
    
    // 注册所有厂商
    factory.Register("siliconflow", NewSiliconFlowProvider)
    factory.Register("volcano", NewVolcanoEngineProvider)
    factory.Register("national", NewNationalSupercomputingProvider)
    factory.Register("qwen", NewQwenProvider)
    factory.Register("openai", NewOpenAICompatibleProvider)
    
    return factory
}

// Register 注册厂商构造函数
func (f *ProviderFactory) Register(name string, creator func(ProviderConfig) Provider) {
    f.providers[name] = creator
}

// Create 创建厂商实例
func (f *ProviderFactory) Create(name string, config ProviderConfig) (Provider, error) {
    creator, exists := f.providers[name]
    if !exists {
        return nil, fmt.Errorf("不支持的厂商: %s", name)
    }
    
    provider := creator(config)
    if err := provider.Validate(); err != nil {
        return nil, fmt.Errorf("厂商配置验证失败: %w", err)
    }
    
    return provider, nil
}

5. 核心翻译类 (Translator)

// internal/translator/translator.go
package translator

import (
    "context"
    "time"
)

// Translator 核心翻译类
type Translator struct {
    config   *config.Config
    provider provider.Provider
    prompt   *PromptManager
}

// NewTranslator 创建翻译器实例
func NewTranslator(config *config.Config, provider provider.Provider) *Translator {
    return &Translator{
        config:   config,
        provider: provider,
        prompt:   NewPromptManager(config.Prompts),
    }
}

// Translate 执行翻译
func (t *Translator) Translate(ctx context.Context, text string, options *TranslateOptions) (*TranslateResult, error) {
    // 设置超时
    timeoutCtx, cancel := context.WithTimeout(ctx, time.Duration(t.config.Timeout)*time.Second)
    defer cancel()
    
    // 构建请求
    req := &provider.TranslateRequest{
        Text:     text,
        FromLang: options.FromLang,
        ToLang:   options.ToLang,
        Prompt:   t.prompt.GetPrompt(options.PromptName),
        Model:    t.selectModel(options.Model),
    }
    
    // 调用厂商API
    resp, err := t.provider.Translate(timeoutCtx, req)
    if err != nil {
        return nil, fmt.Errorf("翻译失败: %w", err)
    }
    
    // 构建结果
    return &TranslateResult{
        Original:   text,
        Translated: resp.Text,
        FromLang:   resp.FromLang,
        ToLang:     resp.ToLang,
        Model:      resp.Model,
        Usage:      resp.Usage,
    }, nil
}

// selectModel 选择模型
func (t *Translator) selectModel(model string) string {
    if model != "" {
        return model
    }
    return t.config.DefaultModel
}

// TranslateOptions 翻译选项
type TranslateOptions struct {
    FromLang    string
    ToLang      string
    PromptName  string
    Model       string
    Temperature float64
}

// TranslateResult 翻译结果
type TranslateResult struct {
    Original   string
    Translated string
    FromLang   string
    ToLang     string
    Model      string
    Usage      *provider.Usage
}

6. 设计模式应用

工厂模式

  • ProviderFactory 用于创建不同厂商实例
  • 统一创建接口,隐藏具体实现细节

策略模式

  • Provider 接口定义统一行为
  • 每个厂商实现不同的API调用策略
  • 运行时动态选择策略

单例模式

  • 全局配置通常只需要一个实例
  • 可通过包级变量或sync.Once实现

依赖注入

  • Translator 依赖 Provider 接口,而不是具体实现
  • 通过构造函数注入依赖

项目结构

yoyo/
├── cmd/
│   └── yoyo/              # CLI入口
├── internal/
│   ├── config/            # 全局配置
│   │   ├── config.go
│   │   └── loader.go
│   ├── provider/          # 厂商实现
│   │   ├── provider.go    # 接口定义
│   │   ├── factory.go     # 工厂模式
│   │   ├── siliconflow.go
│   │   ├── volcano.go
│   │   ├── national.go
│   │   ├── qwen.go
│   │   └── openai.go
│   ├── translator/        # 核心翻译
│   │   ├── translator.go
│   │   └── prompt.go
│   └── prompt/            # Prompt管理
├── pkg/                   # 公共工具
├── configs/               # 配置文件目录
│   └── config.yaml
├── go.mod
└── go.sum

配置文件示例 (YAML)

# config.yaml
default_provider: "siliconflow"
default_model: "gpt-3.5-turbo"
timeout: 30

providers:
  siliconflow:
    api_host: "https://api.siliconflow.cn/v1"
    api_key: "${SILICONFLOW_API_KEY}"
    model: "siliconflow-base"
    enabled: true
    
  volcano:
    api_host: "https://api.volcengine.com/v1"
    api_key: "${VOLCANO_API_KEY}"
    model: "volcano-chat"
    enabled: true
    
  national:
    api_host: "https://api.nsc.gov.cn/v1"
    api_key: "${NATIONAL_API_KEY}"
    model: "nsc-base"
    enabled: false
    
  qwen:
    api_host: "https://dashscope.aliyuncs.com/compatible-mode/v1"
    api_key: "${QWEN_API_KEY}"
    model: "qwen-turbo"
    enabled: true
    
  openai:
    api_host: "https://api.openai.com/v1"
    api_key: "${OPENAI_API_KEY}"
    model: "gpt-3.5-turbo"
    enabled: true

prompts:
  technical: "你是一位专业的技术翻译,请准确翻译以下技术文档,保持专业术语的准确性。"
  creative: "你是一位富有创造力的翻译家,请用优美流畅的语言翻译以下内容。"
  academic: "你是一位学术翻译专家,请用严谨的学术语言翻译以下内容。"
  simple: "请用简单易懂的语言翻译以下内容。"

开发顺序建议

  1. 实现 ConfigConfigLoader
  2. 实现 Provider 接口和工厂
  3. 实现至少一个厂商如SiliconFlow
  4. 实现 Translator 核心类
  5. 集成测试
  6. 实现其他厂商

测试策略

// 内部测试示例
func TestTranslator_Translate(t *testing.T) {
    // Mock provider
    mockProvider := &MockProvider{
        TranslateFunc: func(ctx context.Context, req *provider.TranslateRequest) (*provider.TranslateResponse, error) {
            return &provider.TranslateResponse{Text: "翻译结果"}, nil
        },
    }
    
    translator := NewTranslator(testConfig, mockProvider)
    result, err := translator.Translate(context.Background(), "Hello", &TranslateOptions{
        ToLang: "zh",
    })
    
    if err != nil {
        t.Fatalf("翻译失败: %v", err)
    }
    
    if result.Translated != "翻译结果" {
        t.Errorf("期望 '翻译结果', 得到 '%s'", result.Translated)
    }
}

错误处理最佳实践

  • 每个厂商实现应定义具体的错误类型
  • 使用 %w 包装错误以保留原始错误信息
  • 提供清晰的错误消息帮助调试

安全注意事项

  • API密钥使用环境变量
  • 配置文件中不存储真实密钥
  • 使用 os.Getenv 读取敏感信息
  • 定期轮换API密钥

开发规范

文档管理

文档文件列表

  1. why.md - 项目初衷文档(仅用户编辑)
  2. taolun.md - 讨论记录(时间轴格式)
  3. changelog.md - 版本记录(包含讨论链接)
  4. memory.md - 知识纠正(踩坑记录)

文档协作规范

文档 编辑者 内容 更新频率
why.md 用户 项目初衷、愿景、目标 随时
taolun.md AI+用户 讨论记录、决策过程 每次重要讨论后
changelog.md AI 版本变更、任务状态 每次版本更新
memory.md AI 经验总结、术语定义 遇到问题后

特殊文档说明

why.md:

  • 项目初衷文档,只能由用户编辑
  • AI不应修改此文件内容
  • 用于记录创始人的个人想法和项目愿景
  • 位置:项目根目录

AI责任边界:

  • 可以编辑taolun.md、changelog.md、memory.md
  • 不应编辑why.md、用户个人配置文件
  • 可以建议why.md的内容结构但不强制

文档链接

版本号管理

  • 格式:主版本.次版本.修订版本(00-99)
  • 更新时机测试完成后git操作前
  • 递增规则:小修复第三位+1新功能第二位+1重大变更第一位+1

分支策略

  • main: 稳定上线版
  • dev: 开发分支
  • 功能开发从dev创建功能分支

提交规范

  • 提交前更新相关文档
  • 使用清晰的提交信息
  • 版本更新时打标签

开发命令

构建

# 构建二进制文件
go build -o yoyo ./cmd/yoyo

# 交叉编译Linux
GOOS=linux GOARCH=amd64 go build -o yoyo-linux ./cmd/yoyo

# 交叉编译macOS
GOOS=darwin GOARCH=amd64 go build -o yoyo-mac ./cmd/yoyo

测试

# 运行所有测试
go test ./...

# 运行特定包的测试
go test ./internal/translator

# 运行单个测试函数
go test -run TestTranslate ./internal/translator

# 运行测试并显示详细输出
go test -v ./...

# 运行基准测试
go test -bench=. ./...

代码检查与格式化

# 代码格式化(必须)
gofmt -w .

# 代码检查
go vet ./...

# 使用golangci-lint推荐
golangci-lint run

# 生成依赖图
go mod graph

运行工具

# 直接运行
go run ./cmd/yoyo "This is translation content..."

# 使用构建的二进制
./yoyo "This is translation content..."

# 指定翻译模式
./yoyo --mode=technical "API documentation text"

代码风格指南

命名约定

  • 包名:小写单词,简洁明了(如translatorconfig
  • 函数/方法名:驼峰命名,动词开头(如TranslateTextLoadConfig
  • 变量名:驼峰命名,简洁(如inputTextapiResponse
  • 常量:全大写加下划线(如MAX_RETRY_COUNT
  • 接口名:以-er结尾或描述性名称(如TranslatorConfigLoader

导入顺序

import (
    // 标准库
    "context"
    "fmt"
    
    // 第三方包
    "github.com/spf13/cobra"
    
    // 项目内部包
    "github.com/username/yoyo/internal/api"
    "github.com/username/yoyo/internal/config"
)

错误处理

// 包装错误以提供更多上下文
if err := loadConfig(); err != nil {
    return fmt.Errorf("failed to load config: %w", err)
}

// 定义哨兵错误
var (
    ErrInvalidAPIKey = errors.New("invalid API key")
    ErrNetworkFailed = errors.New("network request failed")
)

// 错误检查使用errors.Is
if errors.Is(err, ErrInvalidAPIKey) {
    // 处理特定错误
}

类型使用

  • 优先使用具体类型,必要时使用接口
  • 避免使用interface{},使用泛型或具体类型
  • 结构体字段使用大写开头(导出)或小写开头(私有)
  • 为复杂类型添加文档注释

并发处理

// 使用context传递取消信号
func Translate(ctx context.Context, text string) (string, error) {
    select {
    case <-ctx.Done():
        return "", ctx.Err()
    default:
        // 继续翻译
    }
}

// 使用goroutine和channel时注意资源清理
go func() {
    defer close(doneCh)
    // 执行任务
}()

注释规范

  • 包注释:描述包的功能和用途
  • 函数注释:描述函数功能、参数、返回值
  • 导出类型/函数必须有注释
  • 使用Godoc格式

测试指南

  • 每个公共函数都应有对应的测试
  • 测试函数命名:TestFunctionName
  • 使用表格驱动测试
  • Mock外部依赖
  • 测试文件放在同一目录,以_test.go结尾
// 示例测试
func TestTranslator_Translate(t *testing.T) {
    tests := []struct {
        name     string
        input    string
        expected string
    }{
        {"simple text", "hello", "你好"},
        {"empty string", "", ""},
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result, err := translator.Translate(tt.input)
            if err != nil {
                t.Fatalf("unexpected error: %v", err)
            }
            if result != tt.expected {
                t.Errorf("got %v, want %v", result, tt.expected)
            }
        })
    }
}

依赖管理

  • 使用Go Modules管理依赖
  • 保持go.modgo.sum整洁
  • 定期更新依赖:go get -u ./...
  • 移除未使用的依赖:go mod tidy

提交前检查清单

  1. 运行gofmt -w .格式化代码
  2. 运行go vet ./...检查代码
  3. 运行go test ./...确保测试通过
  4. 确保所有导出函数有注释
  5. 检查错误处理是否完善
  6. 验证API密钥等敏感信息已忽略

敏感信息处理

  • API密钥使用环境变量或配置文件
  • .env添加到.gitignore
  • 不要在日志中打印敏感信息
  • 使用os.Getenv读取环境变量

构建和发布

# 发布版本
git tag v1.0.0
git push origin v1.0.0

# 使用GoReleaser推荐
goreleaser release --clean

常见问题

Q: 如何添加新的翻译模式?

A: 在internal/prompt/目录下创建新的Prompt配置然后在internal/translator/中注册。

Q: 如何处理API限流

A: 使用指数退避重试,并在internal/api/中实现限流器。

Q: 如何支持更多语言?

A: 在配置文件中添加语言映射,并更新翻译逻辑。

语言代码处理

支持的语言代码格式

项目支持多种语言代码格式,通过 internal/lang 模块处理:

  1. 标准BCP47格式: zh-CN, zh-TW, en-US, en-GB, ja, ko
  2. 简短别名: cn(中文), en(英文), jp(日文), kr(韩文) 等
  3. 中文名称: chinese(中文), english(英文), japanese(日文) 等

语言解析函数

// 解析语言代码
lang.ParseLanguageCode("cn")    // 返回 "zh-CN"
lang.ParseLanguageCode("en")    // 返回 "en-US"
lang.ParseLanguageCode("zh-TW") // 返回 "zh-TW"

// 获取语言名称(用于显示)
lang.GetLanguageName("zh-CN")   // 返回 "中文(简体)"
lang.GetLanguageName("en-US")   // 返回 "English (US)"

Onboard配置向导

配置流程

  1. 选择主要翻译厂商
  2. 配置厂商API密钥、HOST、模型
  3. 设置全局配置(默认语言、超时)
  4. 保存配置到 configs/config.yaml

使用方法

yoyo onboard          # 启动配置向导
yoyo onboard --force  # 强制重新配置

配置向导实现

  • 使用 github.com/AlecAivazis/survey/v2 实现交互式界面
  • 支持厂商选择、API配置、语言设置
  • 生成标准YAML配置文件

分阶段迁移策略

第一阶段:开发阶段(当前)

  • API密钥存储在 .env 文件
  • 复杂配置存储在 configs/config.yaml
  • 支持环境变量替换

第二阶段:上线前

  • 实现配置文件路径查找机制
  • 支持用户配置目录 ~/.config/yoo/yoo.yml
  • 提供配置迁移工具

第三阶段:最终优化

  • 移除对 .env 文件依赖
  • 完全使用配置文件

参考资源