Files
yoyo/taolun.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

12 KiB
Raw Blame History

讨论记录 (taolun.md)

本文档记录开发过程中的重要讨论,以时间轴方式存储,便于版本追溯。

使用说明

  • 每次重要讨论后更新此文件
  • 使用上下文压缩总结,突出重点
  • 格式:时间 - 版本号 - 主题

时间轴记录

[2026-03-28 22:30] 版本 0.0.1 - 确定技术栈

原因: 项目启动,需要确定技术栈 分析:

  • 比较Go、Deno+TS、Node.js+TS
  • Go优势原生二进制、性能好、CLI工具友好
  • 用户不会Go但愿意学习

解决方案:

  • 使用Go语言开发
  • 采用面向对象设计模式
  • 支持多个大模型厂商

相关决策:

  • 项目结构采用cmd/internal/布局
  • 使用YAML配置文件
  • 实现工厂模式和策略模式

关联版本: changelog.md#0.0.1


[2026-03-28 23:00] 版本 0.0.1 - 设计OOP架构

原因: 用户要求面向对象开发模式 分析:

  • Go不是传统OOP语言但可通过结构体和接口实现
  • 需要三个核心类:配置、厂商、翻译器

解决方案:

  • Config类:全局配置管理
  • Provider接口:厂商抽象
  • Translator类:核心翻译逻辑
  • ProviderFactory:工厂模式创建厂商实例

相关链接:


[2026-03-28 23:30] 版本 0.0.1 - 制定开发规范

原因: 建立规范的开发流程 分析:

  • 需要记录讨论过程、版本变更和知识积累
  • 版本号需要遵循语义化版本规范

解决方案:

  • 创建taolun.md记录讨论
  • 创建changelog.md记录版本
  • 创建memory.md记录知识纠正
  • 版本号格式:大版本.新功能.小修复(00-99)

关联文档:


[2026-03-28 23:45] 版本 0.0.1 - 创建项目初衷文档

原因: 需要一个地方记录项目初衷和愿景 分析:

  • 项目需要明确的目标和方向
  • 创始人需要记录个人想法和灵感
  • 与其他文档taolun.md、changelog.md、memory.md区分

解决方案:

  • 创建why.md文件专门记录项目初衷
  • 规定只能由项目所有者编辑
  • 提供基本结构建议,但不强制内容

文档规范:

  • 文件位置:项目根目录
  • 权限:仅用户可编辑
  • 内容:项目愿景、目标、个人笔记等

关联文档:


[2026-03-28 23:50] 版本 0.0.2 - 实现核心架构

原因: 开始实现项目核心功能 分析:

  • 根据OOP设计模式实现三个核心类
  • 需要先实现配置加载和厂商接口
  • 创建基本的CLI入口点

解决方案:

  1. Config类实现

    • 支持YAML配置文件加载
    • 环境变量替换
    • 配置验证和默认值
  2. Provider接口实现

    • 定义统一的翻译接口
    • 工厂模式创建厂商实例
    • 实现硅基流动厂商作为示例
  3. Translator类实现

    • 核心翻译逻辑
    • Prompt管理
    • 超时控制
  4. CLI入口点

    • 命令行参数解析
    • 配置加载
    • 翻译执行

技术细节:

  • 使用gopkg.in/yaml.v3处理YAML
  • 实现工厂模式注册机制
  • 使用context处理超时和取消
  • 添加基本单元测试

关联文档:


[2026-03-29 00:00] 版本 0.0.3 - 环境变量加载修复

原因: 测试CLI时发现环境变量没有正确加载 分析:

  • 配置文件中使用${ENV_VAR}语法
  • Go的os.ExpandEnv只在加载时替换
  • 需要先加载.env文件到环境变量

解决方案:

  1. 添加github.com/joho/godotenv依赖
  2. 在main函数开始时调用godotenv.Load()
  3. 更新memory.md记录踩坑经验

技术细节:

  • godotenv会自动查找当前目录的.env文件
  • 如果文件不存在会返回错误,可以忽略
  • 不影响已有的环境变量

关联文档:


[2026-03-29 10:00] 版本 0.2.0 - 语言代码解析设计

原因: 用户需要通过 --lang 参数指定目标语言,支持多种语言代码格式 分析:

  • 需要支持标准BCP47格式zh-CNen-US
  • 需要支持简短别名(如 cnen
  • 需要支持中文名称(如 chineseenglish
  • 需要智能解析和错误提示

解决方案:

  1. 创建 internal/lang/lang.go 模块
  2. 实现语言代码映射表和解析函数
  3. 支持大小写不敏感和模糊匹配
  4. 提供语言名称获取和建议功能

技术细节:

  • 使用 map[string]string 存储语言代码映射
  • 实现 ParseLanguageCode() 函数进行智能解析
  • 支持30+种语言和变体
  • 添加完整的单元测试

关联文档:


[2026-03-29 10:30] 版本 0.2.0 - onboard配置向导

原因: 用户需要友好的配置界面,特别是第一次使用时 分析:

  • 需要交互式配置向导
  • 需要支持选择厂商、输入API密钥、设置默认值
  • 需要生成标准的YAML配置文件
  • 需要支持强制重新配置

解决方案:

  1. 使用 github.com/AlecAivazis/survey/v2
  2. 实现分步配置流程:选择厂商 → 配置厂商 → 全局设置 → 保存
  3. 提供友好的错误处理和用户提示
  4. 支持 --force 参数强制重新配置

技术细节:

  • 使用 survey.Selectsurvey.Inputsurvey.Confirm 组件
  • 实现厂商默认配置和自定义选项
  • 生成完整的配置文件包含所有必要字段
  • 支持配置文件存在性检查

关联文档:


[2026-03-29 11:00] 版本 0.2.0 - 分阶段迁移策略

原因: 需要平衡开发便利性和最终上线需求 分析:

  • 开发阶段需要简单配置方式(.env + configs/config.yaml
  • 上线前需要迁移到用户配置目录(~/.config/yoo/yoo.yml
  • 需要平滑的迁移路径和向后兼容性

解决方案:

  1. 第一阶段(当前): 继续使用 .env + configs/config.yaml
  2. 第二阶段(上线前): 实现配置文件路径查找和迁移工具
  3. 第三阶段(最终): 移除 .env 依赖,完全使用配置文件

技术细节:

  • 配置文件路径优先级:命令行 > 环境变量 > 用户目录 > 当前目录
  • 保持向后兼容性,支持旧配置格式
  • 提供配置验证和错误提示
  • 实现配置迁移工具(计划)

关联文档:


[2026-03-29 12:00] 版本 0.4.0 - 管道符功能

原因: 用户需要与其他命令行工具联合使用 分析:

  • 用户希望支持管道符功能,如 cat a.txt | yoyo | grep "who are you"
  • 需要检测管道输入并从stdin读取内容
  • 需要控制统计信息输出,避免污染管道输出
  • 需要保持向后兼容性

解决方案:

  1. 管道输入检测: 实现 isPipeInput() 函数,使用 os.Stdin.Stat() 检测管道
  2. stdin读取: 实现 readFromStdin() 函数,使用 bufio.Scanner 读取所有输入
  3. 静默模式: 添加 --quiet-q 参数,控制统计信息输出
  4. 输出重定向: 将统计信息输出到stderr避免污染管道输出

技术细节:

  • 使用 os.ModeCharDevice 检测是否为管道设备
  • 使用 strings.Join() 合并多行输入为单个字符串
  • 统计信息输出到 os.Stderr 而不是 os.Stdout
  • 修复 content/filter.go 中的正则表达式转义问题

使用示例:

# 基本管道功能
echo "Hello world" | yoyo
cat file.txt | yoyo --lang=en

# 静默模式
echo "Hello world" | yoyo -q
echo "Hello world" | yoyo --quiet

# 与其他命令组合
cat file.txt | yoyo | grep "你好"
yoyo "Hello" | wc -l

关联文档:


[2026-03-29 15:00] 版本 0.5.0 - 本地缓存功能设计

原因: 用户希望减少API调用添加本地缓存功能 分析:

  • 需要存储翻译结果避免重复调用API
  • 需要设计缓存键策略,确保缓存准确性
  • 需要考虑数据库选择、事务处理、性能优化
  • 需要设计缓存管理策略

解决方案:

  1. 数据库选择: 使用SQLite

    • 轻量级,无需服务器
    • 支持ACID事务
    • Go生态支持良好 (github.com/mattn/go-sqlite3)
    • 适合嵌入式应用
  2. 缓存键设计:

    • 使用文本内容 + 源语言 + 目标语言
    • 生成SHA256哈希作为缓存键
    • 规范化输入:移除多余空白字符,统一语言代码格式
  3. 事务处理:

    • 使用事务保证数据一致性
    • 插入操作在事务中执行
    • 查询操作不需要显式事务
  4. 保存时机:

    • 在输出结果之前保存到数据库
    • 确保数据持久化
    • 异步保存,不阻塞翻译结果返回
  5. 性能优化:

    • 为缓存键创建索引
    • 使用哈希键减少存储空间
    • 限制缓存表大小(可配置)
    • 使用WAL模式提高并发性能
  6. 缓存策略:

    • 采用组合策略:数量限制+时间过期
    • 默认启用缓存功能
    • 提供手动清理命令
  7. 存储位置:

    • 数据库文件存储在用户配置目录 ~/.config/yoyo/cache.db
    • 符合XDG规范
    • 支持自定义路径配置

技术细节:

  • 使用 github.com/mattn/go-sqlite3 驱动
  • 实现 internal/cache/cache.go 模块
  • 缓存表结构:id, cache_key, original_text, translated_text, from_lang, to_lang, model, prompt, created_at
  • 缓存键生成:sha256(text + "|" + fromLang + "|" + toLang)
  • 查询缓存时使用 SELECT translated_text FROM cache WHERE cache_key = ?
  • 插入缓存时使用 INSERT OR IGNORE INTO cache (...) VALUES (...)

关联文档:


[2026-03-29 20:00] 版本 0.5.1 - 缓存功能修复

原因: 缓存功能测试中发现的问题 分析:

  1. VACUUM事务错误: 缓存清空命令中VACUUM不能在事务中执行
  2. NULL值转换错误: 缓存统计查询在空表时MIN(created_at)返回NULL导致转换错误
  3. 过期清理策略: 当expire_days=0时清理逻辑不工作

解决方案:

  1. 修复VACUUM事务错误:

    • 将VACUUM移到事务之外执行
    • 先删除记录再执行VACUUM
  2. 修复NULL值转换错误:

    • 使用 sql.NullStringsql.NullFloat64 类型
    • 检查 Valid 字段判断是否为NULL
    • 只在有记录时才查询时间范围
  3. 修复过期清理策略:

    • 当cleanupTTL为0时清理所有记录
    • 添加条件判断:if c.cleanupTTL == 0 { ... }

技术细节:

// 修复VACUUM事务错误
func (c *SQLiteCache) Clear(ctx context.Context) error {
    // 先删除所有记录
    _, err := c.db.ExecContext(ctx, `DELETE FROM translation_cache`)
    if err != nil {
        return fmt.Errorf("清空缓存失败: %w", err)
    }
    // 然后执行VACUUM不能在事务中执行
    _, err = c.db.ExecContext(ctx, `VACUUM`)
    if err != nil {
        return fmt.Errorf("清理数据库失败: %w", err)
    }
    return nil
}

// 修复NULL值转换错误
var oldestStr, newestStr sql.NullString
var avgTokens sql.NullFloat64
err = c.db.QueryRowContext(ctx, `SELECT MIN(created_at), MAX(created_at), AVG(total_tokens) FROM translation_cache`).Scan(&oldestStr, &newestStr, &avgTokens)

if oldestStr.Valid {
    stats.OldestRecord, _ = time.Parse("2006-01-02 15:04:05", oldestStr.String)
}

关联文档: