Files
yoyo/AGENTS.md
z.to b71f76c8b3 feat: 添加本地缓存功能,减少API调用
- 实现SQLite缓存模块,支持高效查询和存储
- 添加缓存键生成策略(基于原文+语言对的SHA256哈希)
- 集成缓存到Translator类,先查缓存再调用API
- 添加缓存管理命令:cache clear, cache stats, cache cleanup
- 实现组合缓存清理策略(数量限制+时间过期)
- 添加完整的单元测试
- 更新配置文件模板,添加缓存配置
- 更新文档和版本记录

版本: v0.5.1
2026-03-29 21:10:28 +08:00

886 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# AGENTS.md - YOYO翻译工具开发指南
## 项目概述
YOYO是一个命令行翻译工具使用Go语言编写采用面向对象设计模式。它通过调用在线大模型API结合不同的Prompt配置实现多样化的翻译特色。
## 开发流程
1. AI接手项目后先阅读 `AGENTS.md` `changelog.md` `taolun.md` `memory.md`,无需读整个项目的所有文件;
2. 每次开始执行操作前,先更新 `taolun.md`
3. 执行完开发后,更新 `changelog.md` `memory.md` `AGENTS.md`;
4. 最后,执行 git 操作,发布本地的 版本号,而先不提交远程;
## 文档规范
- 讨论的内容:只应该保存在 `taolun.md``changelod.md`中,根据 文档该写什么该链接什么,分门别类的放置
- 不应该再创建其他任何 md 类说明文件。
## OOP设计模式
### 核心类设计
项目采用以下面向对象设计,结合工厂模式和策略模式:
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Config │ │ Provider │ │ Translator │
│ (全局配置) │──────│ (厂商接口) │──────│ (核心翻译) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
│ │ │
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ ConfigLoader │ │ ProviderFactory │ │ TranslationTask │
│ (配置加载) │ │ (工厂模式) │ │ (任务管理) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
```
### 1. 全局配置类 (Config)
负责读取YAML配置文件提供默认值。
### 2. 缓存类 (Cache)
负责本地缓存管理减少API调用。
```go
// internal/cache/cache.go
package cache
// Cache 缓存接口
type Cache interface {
Get(ctx context.Context, key string) (*CacheEntry, error)
Set(ctx context.Context, entry *CacheEntry) error
Delete(ctx context.Context, key string) error
Clear(ctx context.Context) error
Stats(ctx context.Context) (*CacheStats, error)
Cleanup(ctx context.Context) error
Close() error
}
```
**缓存功能特点**:
- **存储层**: 使用SQLite数据库
- **缓存键**: 基于原文+语言对的SHA256哈希
- **清理策略**: 组合策略(数量限制+时间过期)
- **异步保存**: 不阻塞翻译结果返回
- **自动清理**: 定时清理过期缓存
```go
// 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)
定义统一的厂商接口,采用策略模式。
```go
// 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. 具体厂商实现示例
```go
// 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
```go
// 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)
```go
// 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
│ ├── cache/ # 本地缓存
│ │ ├── cache.go # 缓存接口
│ │ ├── sqlite.go # SQLite实现
│ │ ├── key.go # 缓存键生成
│ │ ├── cleanup.go # 缓存清理
│ │ └── cache_test.go # 单元测试
│ └── prompt/ # Prompt管理
├── pkg/ # 公共工具
├── configs/ # 配置文件目录
│ └── config.yaml
├── go.mod
└── go.sum
```
## 配置文件示例 (YAML)
```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: "请用简单易懂的语言翻译以下内容。"
# 缓存配置
cache:
enabled: true # 是否启用缓存
max_records: 10000 # 最大缓存记录数
expire_days: 30 # 缓存过期天数
db_path: "~/.config/yoyo/cache.db" # 缓存数据库文件路径
```
## 开发顺序建议
1. 实现 `Config``ConfigLoader`
2. 实现 `Provider` 接口和工厂
3. 实现至少一个厂商如SiliconFlow
4. 实现 `Translator` 核心类
5. 集成测试
6. 实现其他厂商
## 测试策略
```go
// 内部测试示例
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的内容结构但不强制
### 文档链接
- 项目初衷: [why.md](why.md)
- 讨论记录: [taolun.md](taolun.md)
- 版本记录: [changelog.md](changelog.md)
- 知识纠正: [memory.md](memory.md)
### 版本号管理
- 格式:主版本.次版本.修订版本(00-99)
- 更新时机测试完成后git操作前
- 递增规则:小修复第三位+1新功能第二位+1重大变更第一位+1
### 分支策略
- main: 稳定上线版
- dev: 开发分支
- 功能开发从dev创建功能分支
### 提交规范
- 提交前更新相关文档
- 使用清晰的提交信息
- 版本更新时打标签
## 开发命令
### 构建
```bash
# 构建二进制文件
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
```
### 测试
```bash
# 运行所有测试
go test ./...
# 运行特定包的测试
go test ./internal/translator
# 运行单个测试函数
go test -run TestTranslate ./internal/translator
# 运行测试并显示详细输出
go test -v ./...
# 运行基准测试
go test -bench=. ./...
```
### 代码检查与格式化
```bash
# 代码格式化(必须)
gofmt -w .
# 代码检查
go vet ./...
# 使用golangci-lint推荐
golangci-lint run
# 生成依赖图
go mod graph
```
### 运行工具
```bash
# 直接运行
go run ./cmd/yoyo "This is translation content..."
# 使用构建的二进制
./yoyo "This is translation content..."
# 指定翻译模式
./yoyo --mode=technical "API documentation text"
# 管道功能(与其他命令行工具联合使用)
cat file.txt | ./yoyo # 翻译文件内容
echo "Hello" | ./yoyo --lang=cn # 翻译命令输出
./yoyo "Hello" | grep "你好" # 与其他命令组合
cat file.txt | ./yoyo -q # 静默模式,只输出翻译结果
# 缓存管理命令
./yoyo cache clear # 清空翻译缓存
./yoyo cache stats # 查看缓存统计信息
./yoyo cache cleanup # 清理过期缓存
```
## 代码风格指南
### 命名约定
- **包名**:小写单词,简洁明了(如`translator``config`
- **函数/方法名**:驼峰命名,动词开头(如`TranslateText``LoadConfig`
- **变量名**:驼峰命名,简洁(如`inputText``apiResponse`
- **常量**:全大写加下划线(如`MAX_RETRY_COUNT`
- **接口名**:以`-er`结尾或描述性名称(如`Translator``ConfigLoader`
### 导入顺序
```go
import (
// 标准库
"context"
"fmt"
// 第三方包
"github.com/spf13/cobra"
// 项目内部包
"github.com/username/yoyo/internal/api"
"github.com/username/yoyo/internal/config"
)
```
### 错误处理
```go
// 包装错误以提供更多上下文
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{}`,使用泛型或具体类型
- 结构体字段使用大写开头(导出)或小写开头(私有)
- 为复杂类型添加文档注释
### 并发处理
```go
// 使用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`结尾
```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.mod``go.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`读取环境变量
## 构建和发布
```bash
# 发布版本
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`(日文) 等
### 语言解析函数
```go
// 解析语言代码
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`
### 使用方法
```bash
yoyo onboard # 启动配置向导
yoyo onboard --force # 强制重新配置
```
### 配置向导实现
- 使用 `github.com/AlecAivazis/survey/v2` 实现交互式界面
- 支持厂商选择、API配置、语言设置
- 生成标准YAML配置文件
## 分阶段迁移策略
### 第一阶段:开发阶段(当前)
- API密钥存储在 `.env` 文件
- 复杂配置存储在 `configs/config.yaml`
- 支持环境变量替换
### 第二阶段:上线前
- 实现配置文件路径查找机制
- 支持用户配置目录 `~/.config/yoo/yoo.yml`
- 提供配置迁移工具
### 第三阶段:最终优化
- 移除对 `.env` 文件依赖
- 完全使用配置文件
## 参考资源
- [Effective Go](https://go.dev/doc/effective_go)
- [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments)
- [Go Style Guide](https://google.github.io/styleguide/go/)
- [Survey库文档](https://github.com/AlecAivazis/survey)