13 Commits

Author SHA1 Message Date
95338962cb fix: Windows 交叉编译添加 CGO_ENABLED=0
All checks were successful
Release / build (push) Successful in 6m28s
2026-04-08 03:14:30 +08:00
063b14add7 refactor: 重构版本号管理规则,更新文档
All checks were successful
Release / build (push) Successful in 6m29s
- 扩展 AGENTS.md 版本号管理规则
- 整理 changelog.md,添加 v1.2.0 版本记录
- 更新 taolun.md,添加版本管理讨论
- 更新 memory.md,添加 CI 构建经验
2026-04-08 02:57:21 +08:00
e6ff8a887c feat: release 自动使用 tag 注释作为 Release 说明
All checks were successful
Release / build (push) Successful in 6m27s
2026-04-08 02:30:33 +08:00
5b7475860a fix: build.sh 添加 -buildvcs=false 避免VCS错误
Some checks failed
Release / build (push) Has been cancelled
2026-04-08 02:24:48 +08:00
7a758aa549 fix: release.yaml 安装 bash 支持 build.sh
Some checks failed
Release / build (push) Failing after 37s
2026-04-08 02:21:21 +08:00
13298931fd fix: release.yaml 构建时切换到正确目录
Some checks failed
Release / build (push) Failing after 35s
2026-04-08 02:14:12 +08:00
0a40258d9a feat: TUI帮助功能与样式改进
Some checks failed
Release / build (push) Failing after 37s
- 添加帮助信息栏 (bubbles help组件, Ctrl+H切换)
- 翻译卡片样式优化 (Padding空隙、上方内边距)
- 扩展build.sh支持跨平台编译
- release.yaml使用build.sh构建
2026-04-08 02:08:34 +08:00
a9b7a69224 feat: Logo模块化与渐变色统一
- 修复 --help/-h/-?/--version 在交互模式下无响应的问题
- 新增 internal/logo/logo.go 统一管理logo展示
- 新增 build.sh 自动注入git版本号
- TUI头部与CLI使用统一logo模块
- 移除TUI头部的 [Ctrl+C 退出] 显示
- 统一版本号格式: ( v1.1.1-dirty )
2026-04-08 01:08:47 +08:00
9acbc834a4 fix: 修改release.yaml从main分支构建
All checks were successful
Release / build (push) Successful in 10m56s
2026-04-07 23:58:01 +08:00
2ea749e0a8 Merge branch 'onboard' into main: 修复配置路径BUG并迁移onboard到huh 2026-04-07 23:52:32 +08:00
c0156a88d6 feat: 修复配置路径BUG并迁移onboard到huh
All checks were successful
Release / build (push) Successful in 12m14s
- 新增路径解析工具 internal/config/path.go
- 配置查找优先级: --config > ~/.config/yoyo/config.yaml > ./configs/config.yaml
- onboard配置保存到 ~/.config/yoyo/config.yaml (符合XDG规范)
- .env文件从 ~/.config/yoyo/.env 加载
- onboard使用huh替代survey库,更现代的交互体验
- 添加Ctrl+C取消支持,打印'你已取消本次配置'
- 保存前增加确认步骤
- 版本号 v0.5.1 -> v1.1.0
2026-04-07 23:51:33 +08:00
21e4710829 fix: upload release assets with correct API endpoint
All checks were successful
Release / build (push) Successful in 11m33s
2026-04-07 09:17:11 +08:00
0b102dcb2a chore: add yoo to gitignore 2026-04-07 09:06:59 +08:00
15 changed files with 901 additions and 757 deletions

View File

@@ -15,8 +15,8 @@ jobs:
steps:
- name: Checkout
run: |
apk add git
git clone -b dev https://hub.gaomia.site/titor/yoyo.git /workspace/titor/yoyo/project
apk add git bash
git clone 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
@@ -25,12 +25,14 @@ jobs:
- name: Build
run: |
cd /workspace/titor/yoyo
chmod +x ./build.sh
for p in linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64; do
os=${p%/*}
arch=${p#*/}
ext=""
[ "$os" = "windows" ] && ext=".exe"
GOOS=$os GOARCH=$arch go build -buildvcs=false -o "yoo-${os}-${arch}${ext}" ./cmd/yoyo
./build.sh "$p" -o "yoo-${os}-${arch}${ext}"
done
- name: Checksums
@@ -40,18 +42,24 @@ jobs:
env:
GITEA_TOKEN: ${{ secrets.release_token }}
run: |
apk add curl
apk add curl jq
TAG_NAME="${GITHUB_REF#refs/tags/}"
# 创建 Release
curl -s -X POST "https://hub.gaomia.site/api/v1/repos/titor/yoyo/releases" \
# 获取 tag 注释或 commit message 作为 release 说明
RELEASE_BODY=$(git tag -l --format='%(contents)' "$TAG_NAME" 2>/dev/null || git log -1 --format="%s%n%n%b" "$TAG_NAME" 2>/dev/null || echo "Release $TAG_NAME")
# 创建 Release 并获取 release_id
RELEASE_RESPONSE=$(curl -s -X POST "https://hub.gaomia.site/api/v1/repos/titor/yoyo/releases" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"tag_name\":\"${TAG_NAME}\",\"name\":\"${TAG_NAME}\",\"body\":\"Automated release\"}"
-d "{\"tag_name\":\"${TAG_NAME}\",\"name\":\"${TAG_NAME}\",\"body\":\"${RELEASE_BODY}\"}")
# 上传产物
RELEASE_ID=$(echo "$RELEASE_RESPONSE" | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)
# 上传附件
for f in yoo-* checksums.txt; do
[ -f "$f" ] && curl -s -X POST "https://hub.gaomia.site/api/v1/repos/titor/yoyo/releases/upload?name=${TAG_NAME}" \
[ -f "$f" ] && curl -s -X POST "https://hub.gaomia.site/api/v1/repos/titor/yoyo/releases/${RELEASE_ID}/assets" \
-H "Authorization: token ${GITEA_TOKEN}" \
-F "attachment=@$f"
done

4
.gitignore vendored
View File

@@ -5,6 +5,7 @@ yoyo
*.dll
*.so
*.dylib
yoo
# 测试
*.test
@@ -27,6 +28,7 @@ vendor/
# 构建输出
dist/
build/
project/
# Go工作区
go.work
@@ -34,4 +36,4 @@ go.work.sum
# 本地配置文件
configs/local.yaml
configs/*.local.yaml
configs/*.local.yaml

View File

@@ -590,9 +590,58 @@ func TestTranslator_Translate(t *testing.T) {
- 知识纠正: [memory.md](memory.md)
### 版本号管理
- 格式:主版本.次版本.修订版本(00-99)
- 更新时机测试完成后git操作前
- 递增规则:小修复第三位+1新功能第二位+1重大变更第一位+1
#### 版本格式
- 格式:`主版本.次版本.修订版本`(如 1.2.0
- 修订版本00-99超过99时递增次版本
#### 版本阶段定义
- **开发版 (0.x)**: 功能开发中可能有API变更
- **测试版 (x.0.0-beta/N)**: 功能冻结,准备测试
- **正式版 (1.0+)**: 稳定版API向后兼容
#### 版本号含义
| 位置 | 含义 | 何时递增 |
|------|------|----------|
| 主版本 | 重大架构变化/不兼容变更 | 重大重构、API不兼容 |
| 次版本 | 新功能、向后兼容 | 添加新功能模块 |
| 修订版本 | Bug修复、细节优化 | 小修复、样式调整 |
#### v1.2.0 发布标准TUI基础功能完成
- ✅ TUI交互界面
- ✅ 多行输入textarea + Ctrl+J换行
- ✅ 翻译卡片展示
- ✅ 状态栏信息
- ✅ 帮助信息Ctrl+H
- ✅ Logo版本号自动注入
- ✅ 支持至少1个厂商SiliconFlow
- ✅ 跨平台编译支持build.sh
- ✅ CI自动构建Release
#### 分支策略(简化版)
- **main**: 稳定分支,所有功能合并到这里
- **feature-xxx**: 功能分支从main创建开发完成后合并回main
- **合并方式**: 使用 `--no-ff` 保留分支历史
- **流程**: feature -> 合并main -> 测试 -> 打标签 -> 推送 -> 删除feature
#### 标签命名规范
- 正式版:`v1.2.3`
- 测试版:`v1.0.0-beta``v1.0.0-rc1`
#### 版本发布流程
1. 功能开发完成
2. 更新 changelog.md添加版本记录
3. 更新 taolun.md添加讨论记录
4. 本地测试通过
5. 合并到 main使用 --no-ff
6. 创建 annotated 标签:`git tag -a v1.2.0 -m "版本说明"`
7. 推送代码和标签:`git push && git push origin v1.2.0`
8. CI 自动构建 Release
#### 禁止事项
- 禁止删除已发布的正式版本标签
- 禁止修改已发布的 Release 内容
- 禁止在 main 分支直接发布正式版(需经过功能分支测试)
### 分支策略
- main: 稳定上线版

97
build.sh Executable file
View File

@@ -0,0 +1,97 @@
#!/bin/bash
set -e
cd "$(dirname "$0")"
VERSION=$(git describe --tags --always --dirty 2>/dev/null || echo "dev")
show_help() {
echo "YOYO 编译脚本"
echo ""
echo "用法:"
echo " ./build.sh 构建当前平台,输出 yoyo"
echo " ./build.sh <平台> 交叉编译指定平台"
echo " ./build.sh -h 显示帮助"
echo ""
echo "参数:"
echo " -h 显示帮助信息"
echo " -o <文件名> 指定输出文件名"
echo ""
echo "支持的平台:"
echo " linux/amd64, linux/arm64"
echo " darwin/amd64, darwin/arm64"
echo " windows/amd64"
echo ""
echo "示例:"
echo " ./build.sh # 构建当前平台"
echo " ./build.sh linux/amd64 # 编译 Linux x64"
echo " ./build.sh darwin/arm64 # 编译 macOS ARM"
echo " ./build.sh windows/amd64 -o app.exe # 编译 Windows x64自定义输出名"
echo ""
echo "输出文件名格式: yoo-<os>-<arch>[.exe]"
echo " yoo-linux-amd64"
echo " yoo-darwin-arm64"
echo " yoo-windows-amd64.exe"
}
OUTPUT_NAME="yoyo"
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
show_help
exit 0
;;
-o|--output)
OUTPUT_NAME="$2"
shift 2
;;
linux/amd64)
GOOS=linux GOARCH=amd64
PLATFORM="linux-amd64"
shift
;;
linux/arm64)
GOOS=linux GOARCH=arm64
PLATFORM="linux-arm64"
shift
;;
darwin/amd64)
GOOS=darwin GOARCH=amd64
PLATFORM="darwin-amd64"
shift
;;
darwin/arm64)
GOOS=darwin GOARCH=arm64
PLATFORM="darwin-arm64"
shift
;;
windows/amd64)
GOOS=windows GOARCH=amd64
CGO_ENABLED=0
PLATFORM="windows-amd64"
if [[ "$OUTPUT_NAME" == "yoyo" ]]; then
OUTPUT_NAME="yoo-windows-amd64.exe"
fi
shift
;;
*)
echo "未知平台: $1"
show_help
exit 1
;;
esac
done
echo "Building yoyo version: $VERSION"
BUILD_CMD="go build -buildvcs=false -ldflags \"-s -w -X github.com/titor/fanyi/internal/logo.version=${VERSION}\" -o \"$OUTPUT_NAME\" ./cmd/yoyo"
if [ -n "$GOOS" ]; then
echo "Target: $PLATFORM"
eval CGO_ENABLED=${CGO_ENABLED:-0} $BUILD_CMD
else
eval CGO_ENABLED=${CGO_ENABLED:-0} $BUILD_CMD
fi
echo "Build complete: ./$OUTPUT_NAME"

View File

@@ -2,470 +2,76 @@
> 本文档记录所有版本变更,包含功能、修复和讨论链接。
## 使用说明
- 版本号格式:`主版本.次版本.修订版本`(如 `0.0.1`
- 第三位为00-99超过99时增加第二位
- 每个版本包含指向讨论记录的链接
## 版本号格式
- 格式:`主版本.次版本.修订版本`(如 `1.2.0`
- 修订版本:00-99超过99时递增次版本
## 未来架构想法
- [ ] 支持流式翻译输出
- [x] 添加本地缓存减少API调用 ✅ 已完成
- [x] 添加本地缓存减少API调用 ✅
- [ ] 实现插件系统支持自定义厂商
- [ ] 支持批量翻译文件
- [ ] 添加Web界面可选
## 待实现
- [ ] 实现硅基流动厂商
- [ ] 实现火山引擎厂商
- [ ] 实现国家超算厂商
- [ ] 实现Qwen厂商
- [ ] 实现OpenAI兼容厂商
## 待实现功能
- [ ] 弹出框组件通用modal
- [ ] 斜杠命令菜单(/ 触发命令选择器)
- [ ] 复制功能clipboard集成
- [ ] 配置文件热重载
- [ ] 翻译历史记录
## 当前正实现
- [x] TUI界面模块拆分计划 ✅ 已制定
- [x] 模块1: TUI框架搭建 ✅ 已完成
- [x] 模块2: 输入组件 ✅ 已完成
- [x] 模块3: 翻译显示区 ✅ 已完成
- [x] 模块4: 状态栏/主题 ✅ 已完成
- [x] 模块5: 快捷键系统 ✅ 已完成
- [x] 模块6: 集成翻译 ✅ 已完成
## TUI界面实现计划 (v0.6.0) - 全部完成!
| 步骤 | 模块 | 内容 | 状态 |
|------|------|------|------|
| 1 | TUI框架搭建 | bubbletea基础App结构、运行循环 | ✅ 已完成 |
| 2 | 输入组件 | 文本输入框,光标、基础编辑 | ✅ 已完成 |
| 3 | 翻译显示区 | 结果展示、格式化、滚动 | ✅ 已完成 |
| 4 | 状态栏/主题 | 底部状态栏、语言选择、主题配色 | ✅ 已完成 |
| 5 | 快捷键系统 | 退出、清空、切换语言等 | ✅ 已完成 |
| 6 | 集成翻译 | 对接现有Translator、加载动画 | ✅ 已完成 |
## TUI界面重构计划 (v0.8.0) - 聊天风格
| 步骤 | 模块 | 内容 | 状态 |
|------|------|------|------|
| 1 | 模块结构拆分 | 创建 view/components/styles/keys 子目录 | ✅ 已完成 |
| 2 | 消息数据结构 | ChatMessage, ChatGroup 结构定义 | ✅ 已完成 |
| 3 | 消息列表组件 | 可滚动的消息历史展示 (viewport) | ✅ 已完成 |
| 4 | 原文+译文样式 | 区分显示用户输入和翻译结果 | ✅ 已完成 |
| 5 | 固定底部输入框 | textarea + Ctrl+J 换行 + 固定高度5行 | ✅ 已完成 |
| 6 | 状态栏 | 完整信息显示 | ✅ 已完成 |
| 7 | 翻译逻辑集成 | 对接 Translator | ✅ 已完成 |
| 8 | 输入框背景色 | 使用 FocusedStyle/BlurredStyle 设置背景 | ✅ 已完成 |
| 9 | 输入框修复 | 修复Ctrl+J换行后第一行被遮住的问题 | ✅ 已完成 |
## TUI界面改进计划 (v0.7.0)
| 步骤 | 模块 | 内容 | 状态 |
|------|------|------|------|
| 7 | 多行输入 | textarea组件替换textinput | ✅ 已完成 |
| 8 | 弹出框组件 | 通用modal组件 | ⏳ 待实现 |
| 9 | 斜杠命令菜单 | / 触发命令选择器,模糊匹配 | ⏳ 待实现 |
| 10 | 翻译结果滚动 | viewport组件支持长文本 | ⏳ 待实现 |
| 11 | 复制功能 | clipboard集成 | ⏳ 待实现 |
| 12 | 状态栏扩展 | 显示耗时、token用量 | ⏳ 待实现 |
## 待修复BUG
-
## TUI基础功能 (v1.2.0) ✅
| 模块 | 内容 | 状态 |
|------|------|------|
| TUI框架 | bubbletea基础App结构 | ✅ |
| 多行输入 | textarea + Ctrl+J换行 | ✅ |
| 翻译显示 | viewport + 卡片样式 | ✅ |
| 状态栏 | 语言/模型/记录数 | ✅ |
| 帮助信息 | Ctrl+H切换 | ✅ |
| 快捷键 | 退出/清空/切换语言 | ✅ |
| 版本注入 | build.sh + ldflags | ✅ |
| CI构建 | 自动Release | ✅ |
## 版本历史
### 0.7.0 (2026-04-06) - TUI界面改进
**类型**: 功能版本
**状态**: 开发中
**改进内容**:
- ✅ 模块7: 多行输入 - textarea组件替换textinput
- ✅ 模块7补充: 布局和样式优化 - 全宽/自适应高度/深色背景
- ⏳ 模块8: 弹出框组件 - 通用modal
- ⏳ 模块9: 斜杠命令菜单 - / 命令选择器
- ⏳ 模块10: 翻译结果滚动 - viewport
- ⏳ 模块11: 复制功能 - clipboard
- ⏳ 模块12: 状态栏扩展 - 耗时/token
**讨论记录**:
- [TUI界面改进计划](taolun.md#2026-04-06-1400-版本-070---tui界面改进计划)
**下一步**:
- 实现模块8: 弹出框组件
---
### 0.6.0 (2026-04-06) - TUI交互界面
**类型**: 功能版本
**状态**: 已完成
**变更内容**:
- ✅ 模块1: TUI框架搭建 - 添加bubbletea依赖实现基础App结构
- ✅ 模块2: 输入组件 - textinput组件、基础输入处理
- ✅ 模块3: 翻译显示区 - 结果显示区域、样式定义
- ✅ 模块4: 状态栏/主题 - 底部状态栏、语言显示、配色完善
- ✅ 模块5: 快捷键系统 - Ctrl+L清空、Ctrl+T切换语言
- ✅ 模块6: 集成翻译 - Enter触发翻译、异步执行、加载状态、错误处理
**技术实现**:
- 使用 `github.com/charmbracelet/bubbletea` v1.3.10
- 使用 `github.com/charmbracelet/bubbles` (textinput组件)
- 使用 `github.com/charmbracelet/lipgloss` v1.1.0
- 基础model结构: config、translator字段
**讨论记录**:
- [TUI界面模块拆分计划](taolun.md#2026-04-06-1000-版本-060---tui界面模块拆分计划)
**下一步**:
- 实现模块7: 多行输入
---
### 0.5.0 (2026-03-29) - 本地缓存功能
**类型**: 功能版本
### v1.2.0 (2026-04-08) - TUI基础功能完成
**类型**: 正式版
**状态**: 已发布
**变更内容**:
-添加本地缓存模块 (internal/cache/)
-实现SQLite缓存存储层支持高效查询和存储
-实现缓存键生成策略(基于原文+语言对的SHA256哈希
-修改Translator类集成缓存功能先查缓存再调用API
-添加缓存配置到Config结构支持自定义缓存参数
-实现缓存管理命令cache clear, cache stats, cache cleanup
-添加组合缓存清理策略(数量限制+时间过期)
-更新配置文件模板,添加缓存配置示例
- ✅ 更新帮助文档,添加缓存相关命令说明
**功能完成**:
-TUI交互界面 (bubbletea)
-多行输入 (textarea + Ctrl+J换行)
-翻译卡片展示 (viewport + 卡片样式)
-状态栏信息 (语言/模型/记录数)
-帮助信息栏 (Ctrl+H切换)
-Logo版本号自动注入 (build.sh + ldflags)
-跨平台编译支持 (build.sh)
-CI自动构建Release
**技术实现**:
- 使用 `github.com/mattn/go-sqlite3` 作为SQLite驱动
- 实现缓存接口 (Cache interface),支持多种存储后端
- 缓存表包含完整字段原文、译文、语言对、模型、Prompt、用量统计
- 自动清理过期缓存和超出数量限制的缓存
- 异步保存缓存,不阻塞翻译结果返回
**构建改进**:
- ✅ Windows 交叉编译添加 CGO_ENABLED=0
**使用示例**:
```bash
# 基本翻译(自动使用缓存)
yoyo "Hello world"
yoyo "Hello world" # 第二次调用将从缓存返回
**构建改进**:
- ✅ 扩展 build.sh 支持跨平台编译
- ✅ 添加 -h 帮助选项、-o 自定义输出文件名
- ✅ 支持 linux/darwin/windows amd64/arm64
- ✅ 修复 CI 环境兼容性 (安装bash、添加-buildvcs=false)
- ✅ Release 说明自动获取 tag 注释
# 缓存管理命令
yoyo cache clear # 清空翻译缓存
yoyo cache stats # 查看缓存统计信息
yoyo cache cleanup # 清理过期缓存
```
**配置示例**:
```yaml
cache:
enabled: true # 是否启用缓存
max_records: 10000 # 最大缓存记录数
expire_days: 30 # 缓存过期天数
db_path: "~/.config/yoyo/cache.db" # 缓存数据库文件路径
```
**样式优化**:
- ✅ 翻译结果增加上方空隙 (Padding(1,3,1,3))
- ✅ Viewport 第一个卡片添加上边距
- ✅ 翻译卡片使用紫→青渐变Logo
**讨论记录**:
- [本地缓存功能设计](taolun.md#2026-03-29-1500-版本-050-本地缓存功能设计)
- [帮助功能和样式改进](taolun.md#2026-04-08-tui界面帮助功能与样式改进)
- [版本号管理规则制定](taolun.md#2026-04-08-版本号管理规则制定)
**下一步**:
- 完善缓存功能测试
- 添加缓存命中率统计
- 实现按语言清理缓存功能
- 优化缓存性能
**关联版本**: v1.2.0
---
### 0.5.1 (2026-03-29) - 缓存功能修复
**类型**: 修复版本
**状态**: 已发布
**变更内容**:
- ✅ 修复缓存清空命令中的VACUUM事务错误
- ✅ 修复缓存统计中的NULL值处理错误
- ✅ 修复缓存过期清理策略支持expire_days=0时清理所有记录
- ✅ 添加缓存模块单元测试
- ✅ 更新版本号到v0.5.1
**修复问题**:
- 缓存清空命令执行时出现"cannot VACUUM from within a transaction"错误
- 缓存统计查询在空表时出现NULL值转换错误
- 缓存过期清理策略在expire_days=0时不工作
**测试结果**:
- 所有缓存模块测试通过
- 缓存命令功能正常
- 缓存集成功能正常
**下一步**:
- 完善缓存功能测试
- 添加缓存命中率统计
- 实现按语言清理缓存功能
- 优化缓存性能
---
### 0.4.0 (2026-03-29) - 管道符功能
**类型**: 功能版本
**状态**: 已发布
**变更内容**:
- ✅ 添加管道符支持,允许与其他命令行工具联合使用
- ✅ 实现管道输入检测 (isPipeInput函数)
- ✅ 实现从标准输入读取 (readFromStdin函数)
- ✅ 添加 --quiet 和 -q 参数控制统计信息输出
- ✅ 更新帮助文档,添加管道使用示例
- ✅ 修复 content/filter.go 中的正则表达式错误
- ✅ 更新版本号到 v0.3.0
**使用示例**:
```bash
# 管道翻译文件
cat file.txt | yoyo
cat file.txt | yoyo --lang=en
# 管道翻译命令输出
echo "Hello world" | yoyo --lang=cn
# 静默模式,只输出翻译结果
echo "Hello world" | yoyo -q
# 与其他命令组合使用
cat file.txt | yoyo | grep "关键词"
yoyo "Hello" | wc -l
```
**讨论记录**:
- [管道符功能设计](taolun.md#管道符功能设计)
**下一步**:
- 实现更多厂商火山引擎、国家超算、Qwen、OpenAI兼容
- 添加配置文件路径查找机制
- 实现配置文件迁移工具
- 完善错误处理和用户体验
### 0.3.0 (2026-03-29) - 内容过滤与代码处理
**类型**: 功能版本
**状态**: 已发布
**变更内容**:
- ✅ 添加内容过滤模块 (internal/content/)
- ✅ 实现基础字符过滤(移除控制字符、规范化换行符、截断超长符号)
- ✅ 实现代码块和行内代码识别
- ✅ 实现代码注释智能识别(支持 JS/TS/Java/Python/Go/HTML 等 30+ 语言)
- ✅ 添加 go-enry 依赖实现编程语言智能检测
- ✅ 添加 SkipKeywords 配置项,默认保留 TODO/FIXME/HACK 等关键词不翻译
- ✅ 集成内容处理到 Translator 模块
**新增文件**:
- `internal/content/content.go` - 模块入口
- `internal/content/filter.go` - 基础字符过滤
- `internal/content/parser.go` - 内容解析器和语言检测
**配置更新**:
- `configs/config.yaml` 新增 `skip_keywords` 配置项
- 支持用户自定义不翻译的关键词列表
**使用示例**:
```bash
# 翻译包含代码的文档,自动识别代码和注释
yoyo "这是一个文档 ```js // TODO: fix this ```"
# 代码块保持不变,只翻译注释中的词汇
# TODO: 修复这个
```
**讨论记录**:
- [内容过滤与代码处理设计](taolun.md#内容过滤与代码处理设计)
**下一步**:
- 实现更多厂商火山引擎、国家超算、Qwen、OpenAI兼容
- 添加配置文件路径查找机制
- 实现配置文件迁移工具
- 完善错误处理和用户体验
### 0.2.0 (2026-03-29) - 语言支持和配置向导
**类型**: 功能版本
**状态**: 开发中
**变更内容**:
- ✅ 添加语言代码智能解析模块 (internal/lang)
- ✅ 支持 `--lang` 参数指定目标语言
- ✅ 支持多种语言代码格式标准BCP47、别名、中文名称
- ✅ 实现 onboard 交互式配置向导
- ✅ 更新配置结构添加语言字段
- ✅ 添加 survey 库依赖用于交互式界面
- ✅ 改进CLI命令行接口
- ✅ 添加语言模块单元测试
**新增文件**:
- `internal/lang/lang.go` - 语言代码解析模块
- `internal/lang/lang_test.go` - 语言模块测试
- `internal/onboard/onboard.go` - 配置向导实现
**支持的语言代码**:
- 标准格式: zh-CN, zh-TW, en-US, en-GB, ja, ko, es, fr, de 等
- 简短别名: cn(中文), en(英文), jp(日文), kr(韩文) 等
- 中文名称: chinese(中文), english(英文), japanese(日文) 等
**使用示例**:
```bash
# 基本翻译
yoyo "Hello world"
yoyo --lang=cn "Hello world"
yoyo --lang=en "你好世界"
yoyo --lang=zh-TW "Hello world"
# 配置向导
yoyo onboard
yoyo onboard --force
```
**讨论记录**:
- [语言代码解析设计](taolun.md#语言代码解析设计)
- [onboard配置向导](taolun.md#onboard配置向导)
**下一步**:
- 实现更多厂商火山引擎、国家超算、Qwen、OpenAI兼容
- 添加配置文件路径查找机制
- 实现配置文件迁移工具
- 完善错误处理和用户体验
### 0.0.3 (2026-03-29) - 环境变量加载修复
**类型**: 修复版本
**状态**: 开发中
**变更内容**:
- ✅ 修复环境变量加载问题
- ✅ 添加godotenv依赖
- ✅ 更新memory.md记录踩坑经验
- ✅ 测试CLI基本功能
**讨论记录**:
- [环境变量加载修复](taolun.md#2026-03-29-0000-版本-003-环境变量加载修复)
**下一步**:
- 实现更多厂商
- 添加更多测试
- 完善错误处理
### 0.0.2 (2026-03-28) - 核心架构实现
**类型**: 功能版本
**状态**: 开发中
**变更内容**:
- ✅ 实现Config配置类internal/config/config.go
- ✅ 实现Provider接口和工厂模式internal/provider/
- ✅ 实现硅基流动厂商internal/provider/siliconflow.go
- ✅ 实现Translator核心翻译类internal/translator/
- ✅ 实现Prompt管理器internal/translator/prompt.go
- ✅ 创建CLI入口点cmd/yoyo/main.go
- ✅ 添加配置文件模板configs/config.yaml
- ✅ 添加单元测试internal/config/config_test.go
- ✅ 初始化Git仓库和版本标签
**讨论记录**:
- [实现核心架构](taolun.md#2026-03-28-2350-版本-002-实现核心架构)
**下一步**:
- 实现其他厂商火山引擎、国家超算、Qwen、OpenAI兼容
- 添加更多测试
- 实现批量翻译功能
- 添加翻译历史记录
- 实现配置文件热重载
### 0.0.1 (2026-03-28) - 项目初始化
**类型**: 初始化版本
**状态**: 开发中
**变更内容**:
- ✅ 确定技术栈为Go语言
- ✅ 设计OOP架构Config、Provider、Translator
- ✅ 制定开发规范taolun.md、changelog.md、memory.md、why.md
- ✅ 确定分支策略main、dev
- ✅ 设计项目结构
- ✅ 创建项目初衷文档why.md
**讨论记录**:
- [确定技术栈](taolun.md#2026-03-28-2230-版本-001-确定技术栈)
- [设计OOP架构](taolun.md#2026-03-28-2300-版本-001-设计oop架构)
- [制定开发规范](taolun.md#2026-03-28-2330-版本-001-制定开发规范)
- [创建项目初衷文档](taolun.md#2026-03-28-2345-版本-001-创建项目初衷文档)
**下一步**:
- 创建项目目录结构
- 初始化Go模块
- 实现Config类
- 实现Provider接口
- 用户填写why.md内容
---
## 版本号管理规则
### 版本号格式
`主版本.次版本.修订版本`(例如:`1.2.3`
### 更新规则
1. **主版本**(第一位):重大架构变更、不兼容更新
2. **次版本**(第二位):新功能、重要特性
3. **修订版本**第三位小修复、优化00-99
### 更新流程
1. 完成开发并测试
2. 更新changelog.md
3. 更新taolun.md如有讨论
4. 更新memory.md如有新知识
5. 更新版本号
6. 提交到dev分支
7. 测试通过后合并到main
8. 创建版本标签:`git tag v0.0.1`
### 示例版本递增
- `0.0.1``0.0.2`:小修复
- `0.0.99``0.1.0`:新功能(修订版本溢出)
- `1.2.3``2.0.0`:重大架构变更
---
## v0.8.1 (2026-04-07)
### 新功能
- 使用 lipgloss v2 设计翻译结果卡片组件
- 卡片包含三部分元信息行Tokens/耗时/模型)、用户输入(碳黑背景)、翻译结果
### 升级
- 升级 `charm.land/lipgloss/v2` v1.1.0 → v2.0.2
- 升级 `charm.land/bubbletea/v2` v1.3.10 → v2.0.2
- 升级 `charm.land/bubbles/v2` → v2.1.0
- 更新所有模块路径为 `charm.land/xxx/v2` 格式
### 技术细节
- 背景色: #1A1A1A (碳黑色)
- 用户输入区域无边框,纯背景色
- 组件间距: 5px marginBottom
- View() 方法返回 `tea.View` 类型
- KeyMsg 改为 KeyPressMsg使用 `msg.String()` 判断键位
**讨论记录**: [taolun.md#版本-0.8.1-翻译结果卡片组件设计](taolun.md#版本-081---翻译结果卡片组件设计)
---
## v1.0.0-beta (2026-04-07)
### 新功能
- ASCII艺术Logo标题带紫色→粉色渐变效果
- 输入框改造:
- 使用 `:::` 紫色分隔符替代上下边框
- Ctrl+J 换行功能
- 信息栏改造:
- 合并显示:语言、模型名、缓存记录数
- 添加翻译状态 Spinner 动画 (MiniDot)
- 翻译结果卡片优化:
- 底部 `▣` 图标左侧边距从3减少到2
- `▣` 与文字间距从3减少到2
### 技术细节
- 使用 lipgloss 实现 True Color 渐变效果
- 使用 charmbracelet/bubbles spinner 组件实现加载动画
- 版本号显示在Logo右侧 [v1.0.0-beta]
- 动态调整 viewport 高度适应终端
### 版本号规则
- 版本号需与 git 标签、changelog.md 中的版本号保持三方同步
**讨论记录**: [taolun.md#版本-100-beta-Logo和信息栏改造](taolun.md#版本-100-beta-logo和信息栏改造)
- 实现弹出框组件
- 实现斜杠命令菜单
- 添加复制功能

View File

@@ -14,6 +14,7 @@ import (
"github.com/titor/fanyi/internal/cache"
"github.com/titor/fanyi/internal/config"
"github.com/titor/fanyi/internal/lang"
"github.com/titor/fanyi/internal/logo"
"github.com/titor/fanyi/internal/onboard"
"github.com/titor/fanyi/internal/provider"
"github.com/titor/fanyi/internal/translator"
@@ -25,6 +26,7 @@ var (
version = flag.Bool("version", false, "显示版本信息")
help = flag.Bool("help", false, "显示帮助信息")
h = flag.Bool("h", false, "显示帮助信息")
question = flag.Bool("?", false, "显示帮助信息")
langFlag = flag.String("lang", "", "目标语言代码(如 zh-CN, en-US, cn, en 等)")
langLong = flag.String("language", "", "目标语言代码(--lang的长格式")
configFile = flag.String("config", "", "配置文件路径")
@@ -38,8 +40,6 @@ var (
interactiveShort = flag.Bool("i", false, "启动交互式翻译界面(-i的短格式")
)
const versionString = "YOYO翻译工具 v0.5.1"
// isPipeInput 检测是否有管道输入
func isPipeInput() bool {
fileInfo, err := os.Stdin.Stat()
@@ -104,6 +104,17 @@ func main() {
return
}
if *help || *h || *question {
printHelp()
return
}
// 处理 -? 作为位置参数的情况
if flag.NArg() > 0 && (flag.Arg(0) == "-?" || flag.Arg(0) == "?") {
printHelp()
return
}
// 处理交互式模式
if *interactive || *interactiveShort || shouldStartInteractive() {
startInteractiveMode()
@@ -112,14 +123,8 @@ func main() {
// 处理管道输入情况
if isPipeInput() {
// 管道模式下,即使没有参数也继续执行
if *help || *h {
printHelp()
return
}
} else {
// 非管道模式下,没有参数则显示帮助
if *help || *h || flag.NArg() == 0 {
// 管道模式下,没有参数则显示帮助
if flag.NArg() == 0 {
printHelp()
return
}
@@ -192,12 +197,13 @@ func main() {
}
// 加载环境变量文件
_ = godotenv.Load() // 忽略错误,如果文件不存在
_ = godotenv.Load(config.GetUserEnvPath())
// 加载配置
configPath := *configFile
if configPath == "" {
configPath = "configs/config.yaml" // 默认配置文件路径
configPath, err := config.ResolveConfigPath(*configFile)
if err != nil {
fmt.Printf("解析配置路径失败: %v\n", err)
os.Exit(1)
}
configLoader := &config.YAMLConfigLoader{}
@@ -300,10 +306,14 @@ func runOnboard(force bool) {
// runCacheCommand 运行缓存命令
func runCacheCommand(subcommand string) {
// 加载环境变量文件
_ = godotenv.Load()
_ = godotenv.Load(config.GetUserEnvPath())
// 加载配置
configPath := "configs/config.yaml"
configPath, err := config.ResolveConfigPath("")
if err != nil {
fmt.Printf("解析配置路径失败: %v\n", err)
os.Exit(1)
}
configLoader := &config.YAMLConfigLoader{}
cfg, err := configLoader.Load(configPath)
if err != nil {
@@ -367,12 +377,14 @@ func runCacheCommand(subcommand string) {
// printVersion 显示版本信息
func printVersion() {
fmt.Println(versionString)
logo.PrintLogoWithVersion()
}
// printHelp 显示帮助信息
func printHelp() {
fmt.Printf(`%s
logo.PrintLogoWithVersion()
fmt.Printf(`
使用方法:
yoyo [选项] <文本>
@@ -434,14 +446,14 @@ func printHelp() {
yoyo --interactive # 启动交互式翻译界面
yoyo -i # 启动交互式翻译界面(短格式)
配置:
- 配置文件: configs/config.yaml
- 环境变量: .env 文件
配置:
- 配置文件: ~/.config/yoyo/config.yaml
- 环境变量: ~/.config/yoyo/.env
- 默认厂商: siliconflow
- 默认目标语言: zh-CN (简体中文)
更多信息请访问: https://github.com/titor/fanyi
`, versionString)
更多信息请访问: https://github.com/titor/fanyi
`)
}
// shouldStartInteractive 判断是否应该启动交互式模式
@@ -463,12 +475,13 @@ func startInteractiveMode() {
}
// 加载环境变量文件
_ = godotenv.Load() // 忽略错误,如果文件不存在
_ = godotenv.Load(config.GetUserEnvPath())
// 加载配置
configPath := *configFile
if configPath == "" {
configPath = "configs/config.yaml" // 默认配置文件路径
configPath, err := config.ResolveConfigPath(*configFile)
if err != nil {
fmt.Printf("解析配置路径失败: %v\n", err)
os.Exit(1)
}
configLoader := &config.YAMLConfigLoader{}

22
go.mod
View File

@@ -5,9 +5,8 @@ go 1.26.1
require (
charm.land/bubbles/v2 v2.1.0
charm.land/bubbletea/v2 v2.0.2
charm.land/huh/v2 v2.0.3
charm.land/lipgloss/v2 v2.0.2
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/charmbracelet/bubbles v1.0.0
github.com/go-enry/go-enry/v2 v2.9.5
github.com/joho/godotenv v1.5.1
github.com/mattn/go-sqlite3 v1.14.37
@@ -16,34 +15,25 @@ require (
require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/bubbletea v1.3.10 // indirect
github.com/catppuccin/go v0.2.0 // indirect
github.com/charmbracelet/colorprofile v0.4.2 // indirect
github.com/charmbracelet/lipgloss v1.1.0 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect
github.com/charmbracelet/x/ansi v0.11.6 // indirect
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/exp/ordered v0.1.0 // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/charmbracelet/x/termios v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/go-enry/go-oniguruma v1.2.1 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/mattn/go-colorable v0.1.2 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.21 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/text v0.4.0 // indirect
)

85
go.sum
View File

@@ -2,84 +2,69 @@ charm.land/bubbles/v2 v2.1.0 h1:YSnNh5cPYlYjPxRrzs5VEn3vwhtEn3jVGRBT3M7/I0g=
charm.land/bubbles/v2 v2.1.0/go.mod h1:l97h4hym2hvWBVfmJDtrEHHCtkIKeTEb3TTJ4ZOB3wY=
charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0=
charm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=
charm.land/huh/v2 v2.0.3 h1:2cJsMqEPwSywGHvdlKsJyQKPtSJLVnFKyFbsYZTlLkU=
charm.land/huh/v2 v2.0.3/go.mod h1:93eEveeeqn47MwiC3tf+2atZ2l7Is88rAtmZNZ8x9Wc=
charm.land/lipgloss/v2 v2.0.2 h1:xFolbF8JdpNkM2cEPTfXEcW1p6NRzOWTSamRfYEw8cs=
charm.land/lipgloss/v2 v2.0.2/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM=
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA=
github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA=
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98=
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
github.com/charmbracelet/x/conpty v0.1.1 h1:s1bUxjoi7EpqiXysVtC+a8RrvPPNcNvAjfi4jxsAuEs=
github.com/charmbracelet/x/conpty v0.1.1/go.mod h1:OmtR77VODEFbiTzGE9G1XiRJAga6011PIm4u5fTNZpk=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
github.com/charmbracelet/x/exp/ordered v0.1.0 h1:55/qLwjIh0gL0Vni+QAWk7T/qRVP6sBf+2agPBgnOFE=
github.com/charmbracelet/x/exp/ordered v0.1.0/go.mod h1:5UHwmG+is5THxMyCJHNPCn2/ecI07aKNrW+LcResjJ8=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
github.com/charmbracelet/x/xpty v0.1.3 h1:eGSitii4suhzrISYH50ZfufV3v085BXQwIytcOdFSsw=
github.com/charmbracelet/x/xpty v0.1.3/go.mod h1:poPYpWuLDBFCKmKLDnhBp51ATa0ooD8FhypRwEFtH3Y=
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/go-enry/go-enry/v2 v2.9.5 h1:HPhAQQHYwJgihL2PxBZiUMFWiROsGwOBdB6/D8zCUhY=
github.com/go-enry/go-enry/v2 v2.9.5/go.mod h1:9yrj4ES1YrbNb1Wb7/PWYr2bpaCXUGRt0uafN0ISyG8=
github.com/go-enry/go-oniguruma v1.2.1 h1:k8aAMuJfMrqm/56SG2lV9Cfti6tC4x8673aHCcBk+eo=
github.com/go-enry/go-oniguruma v1.2.1/go.mod h1:bWDhYP+S6xZQgiRL7wlTScFYBe023B6ilRZbCAD5Hf4=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=
github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-sqlite3 v1.14.37 h1:3DOZp4cXis1cUIpCfXLtmlGolNLp2VEqhiB/PARNBIg=
github.com/mattn/go-sqlite3 v1.14.37/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
@@ -87,48 +72,18 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

76
internal/config/path.go Normal file
View File

@@ -0,0 +1,76 @@
package config
import (
"fmt"
"os"
"path/filepath"
"strings"
)
const (
// ConfigDirName 配置目录名称
ConfigDirName = "yoyo"
// ConfigFileName 配置文件名
ConfigFileName = "config.yaml"
// EnvFileName 环境变量文件名
EnvFileName = ".env"
)
// ResolveConfigPath 解析配置文件路径
// 优先级: 用户指定路径 > ~/.config/yoyo/config.yaml > ./configs/config.yaml
func ResolveConfigPath(userPath string) (string, error) {
// 1. 用户通过 --config 指定的路径
if userPath != "" {
return expandPath(userPath)
}
// 2. 标准用户配置目录 ~/.config/yoyo/config.yaml
userConfigPath := GetUserConfigPath()
if _, err := os.Stat(userConfigPath); err == nil {
return userConfigPath, nil
}
// 3. 项目本地配置 ./configs/config.yaml向后兼容
localConfigPath := "configs/config.yaml"
if _, err := os.Stat(localConfigPath); err == nil {
return localConfigPath, nil
}
// 4. 都不存在返回标准路径onboard 会创建)
return userConfigPath, nil
}
// GetUserConfigDir 获取用户配置目录路径
// 返回 ~/.config/yoyo
func GetUserConfigDir() string {
home, err := os.UserHomeDir()
if err != nil {
// 降级到当前目录
return ".config/" + ConfigDirName
}
return filepath.Join(home, ".config", ConfigDirName)
}
// GetUserConfigPath 获取用户配置文件路径
// 返回 ~/.config/yoyo/config.yaml
func GetUserConfigPath() string {
return filepath.Join(GetUserConfigDir(), ConfigFileName)
}
// GetUserEnvPath 获取用户环境变量文件路径
// 返回 ~/.config/yoyo/.env
func GetUserEnvPath() string {
return filepath.Join(GetUserConfigDir(), EnvFileName)
}
// expandPath 展开路径中的 ~ 符号
func expandPath(path string) (string, error) {
if strings.HasPrefix(path, "~") {
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("无法获取用户主目录: %w", err)
}
path = filepath.Join(home, path[1:])
}
return path, nil
}

79
internal/logo/logo.go Normal file
View File

@@ -0,0 +1,79 @@
package logo
import (
"fmt"
"strings"
)
var version string
func SetVersion(v string) { version = v }
func GetVersion() string { return version }
func GradientText(text string, startColor, endColor string) string {
startR, startG, startB := parseHexColor(startColor)
endR, endG, endB := parseHexColor(endColor)
lines := strings.Split(text, "\n")
if len(lines) == 0 {
return text
}
var result []string
for _, line := range lines {
if len(line) == 0 {
result = append(result, line)
continue
}
var coloredLine string
for i, char := range line {
ratio := float64(i) / float64(len(line)-1)
r := int(float64(startR) + float64(endR-startR)*ratio)
g := int(float64(startG) + float64(endG-startG)*ratio)
b := int(float64(startB) + float64(endB-startB)*ratio)
coloredLine += fmt.Sprintf("\033[38;2;%d;%d;%dm%s\033[0m", r, g, b, string(char))
}
result = append(result, coloredLine)
}
return strings.Join(result, "\n")
}
func parseHexColor(hex string) (int, int, int) {
hex = strings.TrimPrefix(hex, "#")
if len(hex) != 6 {
return 0, 0, 0
}
var r, g, b int
fmt.Sscanf(hex, "%02x%02x%02x", &r, &g, &b)
return r, g, b
}
func GetLogoPattern() string {
return " _ _ _____ _____ " + "\n" +
"( \\/ ( _ ( _ ) " + "\n" +
" \\ / )(_)( )(_)( " + "\n" +
" (__)(_____(_____("
}
func GetVersionSuffix() string {
v := GetVersion()
if v != "" {
return " (" + v + " )"
}
return " ( )"
}
func PrintLogoWithVersion() {
logoPattern := " _ _ _____ _____\n" +
"( \\/ ( _ ( _ )\n" +
" \\ / )(_)( )(_)( \n" +
" (__)(_____(_____("
patternWithVersion := logoPattern + GetVersionSuffix()
colored := GradientText(patternWithVersion, "#B413DC", "#00C8C8")
fmt.Println(colored)
}

View File

@@ -1,30 +1,39 @@
package onboard
import (
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/AlecAivazis/survey/v2"
"charm.land/huh/v2"
"github.com/titor/fanyi/internal/config"
"github.com/titor/fanyi/internal/lang"
)
// RunOnboard 启动配置向导
func RunOnboard(force bool) error {
fmt.Println("欢迎使用YOYO翻译工具配置向导!")
fmt.Println("这个向导将帮助您配置翻译工具。")
fmt.Println()
configPath := config.GetUserConfigPath()
// 检查配置文件是否存在
configPath := "configs/config.yaml"
if _, err := os.Stat(configPath); err == nil && !force {
overwrite := false
prompt := &survey.Confirm{
Message: "检测到配置文件已存在,是否要重新配置?",
Default: false,
}
if err := survey.AskOne(prompt, &overwrite); err != nil {
var overwrite bool
form := huh.NewForm(
huh.NewGroup(
huh.NewConfirm().
Title("检测到配置文件已存在,是否要重新配置?").
Affirmative("是").
Negative("否").
Value(&overwrite),
),
)
if err := form.Run(); err != nil {
if errors.Is(err, huh.ErrUserAborted) {
fmt.Println("\n你已取消本次配置")
return nil
}
return fmt.Errorf("用户输入错误: %w", err)
}
if !overwrite {
@@ -34,30 +43,75 @@ func RunOnboard(force bool) error {
}
// 步骤1: 选择主要厂商
fmt.Println("步骤1: 选择主要翻译服务提供商")
providerName, err := SelectProvider()
if err != nil {
var providerName string
providerForm := huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().
Title("请选择要使用的翻译服务提供商").
Options(
huh.NewOption("硅基流动 (推荐,免费额度)", "siliconflow"),
huh.NewOption("火山引擎", "volcano"),
huh.NewOption("国家超算", "national"),
huh.NewOption("Qwen (通义千问)", "qwen"),
huh.NewOption("OpenAI兼容格式", "openai"),
).
Value(&providerName),
),
)
if err := providerForm.Run(); err != nil {
if errors.Is(err, huh.ErrUserAborted) {
fmt.Println("\n你已取消本次配置")
return nil
}
return fmt.Errorf("选择厂商失败: %w", err)
}
// 步骤2: 配置主要厂商
fmt.Println("\n步骤2: 配置主要厂商")
providerConfig, err := ConfigureProvider(providerName)
providerConfig, err := ConfigureProviderHuh(providerName)
if err != nil {
if errors.Is(err, huh.ErrUserAborted) {
fmt.Println("\n你已取消本次配置")
return nil
}
return fmt.Errorf("配置厂商失败: %w", err)
}
// 步骤3: 全局设置
fmt.Println("\n步骤3: 全局设置")
globalConfig, err := GlobalSettings()
globalConfig, err := GlobalSettingsHuh()
if err != nil {
if errors.Is(err, huh.ErrUserAborted) {
fmt.Println("\n你已取消本次配置")
return nil
}
return fmt.Errorf("全局设置失败: %w", err)
}
// 步骤4: 确认并保存配置
fmt.Println("\n步骤4: 保存配置")
configData := BuildConfig(providerName, providerConfig, globalConfig)
var confirmSave bool
confirmForm := huh.NewForm(
huh.NewGroup(
huh.NewConfirm().
Title("确认保存配置?").
Description(fmt.Sprintf("配置文件将保存到: %s", configPath)).
Affirmative("是,保存").
Negative("否,取消").
Value(&confirmSave),
),
)
if err := confirmForm.Run(); err != nil {
if errors.Is(err, huh.ErrUserAborted) {
fmt.Println("\n你已取消本次配置")
return nil
}
return fmt.Errorf("用户输入错误: %w", err)
}
if !confirmSave {
fmt.Println("配置已取消。")
return nil
}
if err := SaveConfig(configData, configPath); err != nil {
return fmt.Errorf("保存配置失败: %w", err)
}
@@ -71,54 +125,17 @@ func RunOnboard(force bool) error {
return nil
}
// SelectProvider 选择主要厂商
func SelectProvider() (string, error) {
providers := []string{
"siliconflow",
"volcano",
"national",
"qwen",
"openai",
}
providerNames := map[string]string{
"siliconflow": "硅基流动 (推荐,免费额度)",
"volcano": "火山引擎",
"national": "国家超算",
"qwen": "Qwen (通义千问)",
"openai": "OpenAI兼容格式",
}
var selected string
prompt := &survey.Select{
Message: "请选择要使用的翻译服务提供商:",
Options: func() []string {
var opts []string
for _, p := range providers {
opts = append(opts, providerNames[p])
}
return opts
}(),
Default: providerNames["siliconflow"],
}
if err := survey.AskOne(prompt, &selected); err != nil {
return "", err
}
// 返回对应的厂商名称
for name, displayName := range providerNames {
if displayName == selected {
return name, nil
}
}
return "siliconflow", nil
// GlobalConfig 全局设置配置
type GlobalConfig struct {
DefaultProvider string
DefaultModel string
Timeout int
DefaultSourceLang string
DefaultTargetLang string
}
// ConfigureProvider 配置厂商
func ConfigureProvider(providerName string) (config.ProviderConfig, error) {
// 厂商默认配置
// ConfigureProviderHuh 使用 huh 配置厂商
func ConfigureProviderHuh(providerName string) (config.ProviderConfig, error) {
defaults := map[string]config.ProviderConfig{
"siliconflow": {
APIHost: "https://api.siliconflow.cn/v1",
@@ -154,47 +171,43 @@ func ConfigureProvider(providerName string) (config.ProviderConfig, error) {
Enabled: defaultConfig.Enabled,
}
// 输入API密钥
apiKeyPrompt := &survey.Input{
Message: fmt.Sprintf("请输入 %s 的API密钥:", providerName),
Help: "API密钥用于身份验证将存储在配置文件中",
}
if err := survey.AskOne(apiKeyPrompt, &cfg.APIKey, survey.WithValidator(survey.Required)); err != nil {
return config.ProviderConfig{}, err
}
// 确认API HOST
apiHostPrompt := &survey.Input{
Message: "API HOST (直接回车使用默认值):",
Default: cfg.APIHost,
}
if err := survey.AskOne(apiHostPrompt, &cfg.APIHost); err != nil {
return config.ProviderConfig{}, err
}
// 确认默认模型
modelPrompt := &survey.Input{
Message: "默认模型 (直接回车使用默认值):",
Default: cfg.Model,
}
if err := survey.AskOne(modelPrompt, &cfg.Model); err != nil {
var apiKey string
apiKeyForm := huh.NewForm(
huh.NewGroup(
huh.NewInput().
Title(fmt.Sprintf("请输入 %s 的API密钥", providerName)).
Description("API密钥用于身份验证将存储在配置文件中").
Value(&apiKey).
Validate(func(str string) error {
if strings.TrimSpace(str) == "" {
return fmt.Errorf("API密钥不能为空")
}
return nil
}),
huh.NewInput().
Title("API HOST").
Description("直接回车使用默认值").
Value(&cfg.APIHost).
Placeholder(defaultConfig.APIHost),
huh.NewInput().
Title("默认模型").
Description("直接回车使用默认值").
Value(&cfg.Model).
Placeholder(defaultConfig.Model),
),
)
if err := apiKeyForm.Run(); err != nil {
return config.ProviderConfig{}, err
}
cfg.APIKey = apiKey
return cfg, nil
}
// GlobalSettings 全局设置
type GlobalConfig struct {
DefaultProvider string
DefaultModel string
Timeout int
DefaultSourceLang string
DefaultTargetLang string
}
// GlobalSettings 全局设置
func GlobalSettings() (*GlobalConfig, error) {
// GlobalSettingsHuh 使用 huh 进行全局设置
func GlobalSettingsHuh() (*GlobalConfig, error) {
cfg := &GlobalConfig{
DefaultProvider: "siliconflow",
DefaultModel: "siliconflow-base",
@@ -203,43 +216,33 @@ func GlobalSettings() (*GlobalConfig, error) {
DefaultTargetLang: "zh-CN",
}
// 选择默认语言
targetLangOptions := lang.GetCommonLanguages()
var targetLangDisplay []string
var options []huh.Option[string]
for _, code := range targetLangOptions {
targetLangDisplay = append(targetLangDisplay, fmt.Sprintf("%s (%s)", code, lang.GetLanguageName(code)))
options = append(options, huh.NewOption(
fmt.Sprintf("%s (%s)", code, lang.GetLanguageName(code)),
code,
))
}
targetLangPrompt := &survey.Select{
Message: "请选择默认目标语言:",
Options: targetLangDisplay,
Default: fmt.Sprintf("%s (%s)", "zh-CN", lang.GetLanguageName("zh-CN")),
}
var selectedTarget string
if err := survey.AskOne(targetLangPrompt, &selectedTarget); err != nil {
return nil, err
}
// 从选择中提取语言代码
for i, display := range targetLangDisplay {
if display == selectedTarget {
cfg.DefaultTargetLang = targetLangOptions[i]
break
}
}
// 设置超时时间
timeoutPrompt := &survey.Input{
Message: "API超时时间(秒):",
Default: fmt.Sprintf("%d", cfg.Timeout),
}
var timeoutStr string
if err := survey.AskOne(timeoutPrompt, &timeoutStr); err != nil {
form := huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().
Title("请选择默认目标语言").
Options(options...).
Value(&cfg.DefaultTargetLang),
huh.NewInput().
Title("API超时时间(秒)").
Value(&timeoutStr).
Placeholder("30"),
),
)
if err := form.Run(); err != nil {
return nil, err
}
// 解析超时时间
if timeout := parseIntOrDefault(timeoutStr, 30); timeout > 0 {
cfg.Timeout = timeout
}
@@ -249,12 +252,10 @@ func GlobalSettings() (*GlobalConfig, error) {
// BuildConfig 构建配置对象
func BuildConfig(providerName string, providerConfig config.ProviderConfig, globalConfig *GlobalConfig) *config.Config {
// 创建厂商配置
providers := map[string]config.ProviderConfig{
providerName: providerConfig,
}
// 创建Prompt配置
prompts := map[string]string{
"technical": "你是一位专业的技术翻译,请准确翻译以下技术文档,保持专业术语的准确性。",
"creative": "你是一位富有创造力的翻译家,请用优美流畅的语言翻译以下内容。",
@@ -275,25 +276,24 @@ func BuildConfig(providerName string, providerConfig config.ProviderConfig, glob
// SaveConfig 保存配置文件
func SaveConfig(cfg *config.Config, path string) error {
// 确保目录存在
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("创建配置目录失败: %w", err)
}
// 使用config包的Save方法
loader := &config.YAMLConfigLoader{}
return loader.Save(cfg, path)
}
// parseIntOrDefault 解析整数,失败时返回默认值
func parseIntOrDefault(s string, defaultValue int) int {
s = strings.TrimSpace(s)
if s == "" {
return defaultValue
}
var result int
if _, err := fmt.Sscanf(s, "%d", &result); err != nil {
result, err := strconv.Atoi(s)
if err != nil {
return defaultValue
}

View File

@@ -12,6 +12,7 @@ type KeyMap struct {
ScrollDown key.Binding
ScrollTop key.Binding
ScrollBottom key.Binding
Help key.Binding
}
func NewKeyMap() KeyMap {
@@ -44,5 +45,21 @@ func NewKeyMap() KeyMap {
key.WithKeys("end"),
key.WithHelp("End", "底部"),
),
Help: key.NewBinding(
key.WithKeys("ctrl+h"),
key.WithHelp("Ctrl+H", "帮助"),
),
}
}
func (k KeyMap) ShortHelp() []key.Binding {
return []key.Binding{k.Help, k.Quit}
}
func (k KeyMap) FullHelp() [][]key.Binding {
return [][]key.Binding{
{k.Quit, k.Clear, k.SwitchLang},
{k.ScrollUp, k.ScrollDown, k.ScrollTop, k.ScrollBottom},
{k.Help},
}
}

View File

@@ -7,23 +7,20 @@ import (
"strings"
"time"
"charm.land/bubbles/v2/help"
"charm.land/bubbles/v2/key"
"charm.land/bubbles/v2/spinner"
"charm.land/bubbles/v2/textarea"
"charm.land/bubbles/v2/viewport"
"charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
"github.com/titor/fanyi/internal/config"
"github.com/titor/fanyi/internal/content"
"github.com/titor/fanyi/internal/logo"
"github.com/titor/fanyi/internal/translator"
)
var supportedLangs = []string{"zh-CN", "en-US", "ja", "ko", "zh-TW", "es", "fr", "de"}
var logoPattern = "l_ _ _____ _____ " + "\n" +
"( \\/ ( _ ( _ ) " + "\n" +
" \\ / )(_)( )(_)( " + "\n" +
" (__)(_____(_____((() [v" + content.Version + "]"
type translateMsg struct {
result string
tokens int
@@ -38,6 +35,7 @@ type model struct {
input textarea.Model
viewport viewport.Model
spinner spinner.Model
help help.Model
keys KeyMap
targetLang string
@@ -72,16 +70,22 @@ func NewApp(cfg *config.Config, t *translator.Translator) *tea.Program {
sp.Spinner = spinner.MiniDot
sp.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("#8B5CF6"))
return tea.NewProgram(model{
hp := help.New()
m := model{
config: cfg,
translator: t,
messages: make([]ChatMessage, 0),
input: ta,
viewport: vp,
spinner: sp,
help: hp,
keys: keys,
targetLang: getDefaultLang(cfg),
})
}
p := tea.NewProgram(m)
return p
}
func getDefaultLang(cfg *config.Config) string {
@@ -105,6 +109,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
m.help.SetWidth(msg.Width)
m.updateLayout()
case translateMsg:
@@ -142,6 +147,9 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case "end":
m.viewport.GotoBottom()
}
if key.Matches(msg, m.keys.Help) {
m.help.ShowAll = !m.help.ShowAll
}
}
m.input, cmd = m.input.Update(msg)
@@ -211,7 +219,12 @@ func (m *model) updateLayout() {
m.input.SetWidth(contentWidth)
m.viewport.SetWidth(contentWidth)
m.viewport.SetHeight(m.height - 12)
helpLines := 2
if m.help.ShowAll {
helpLines = 4
}
m.viewport.SetHeight(m.height - 12 - helpLines)
if m.viewport.Height() < 5 {
m.viewport.SetHeight(10)
}
@@ -222,6 +235,10 @@ func (m *model) updateLayout() {
func (m *model) updateViewportContent() {
var b strings.Builder
if len(m.messages) > 0 {
b.WriteString("\n")
}
for _, msg := range m.messages {
b.WriteString(m.renderTranslationCard(msg))
}
@@ -257,7 +274,7 @@ func (m *model) renderTranslationCard(msg ChatMessage) string {
Render(msg.Error)
outputBlock = lipgloss.NewStyle().
Foreground(lipgloss.Color("#F87171")).
Padding(0, 3, 1, 3).
Padding(1, 3, 1, 3).
Width(m.viewport.Width()).
Render(outputContent)
} else {
@@ -265,7 +282,7 @@ func (m *model) renderTranslationCard(msg ChatMessage) string {
Width(m.viewport.Width() - 2).
Render(msg.Output)
outputBlock = lipgloss.NewStyle().
Padding(0, 3, 1, 3).
Padding(1, 3, 1, 3).
Width(m.viewport.Width()).
Render(outputContent)
}
@@ -304,67 +321,26 @@ func (m model) View() tea.View {
messages := m.viewport.View()
inputArea := m.renderInputArea()
infoBar := m.renderInfoBar()
spinnerView := m.renderSpinner()
helpView := m.help.View(m.keys)
content := header + "\n" + messages + inputArea + infoBar + spinnerView
content := header + "\n" + messages + inputArea + infoBar + helpView
v := tea.NewView(content)
v.AltScreen = true
return v
}
func gradientText(text string, startColor, endColor string) string {
startR, startG, startB := parseHexColor(startColor)
endR, endG, endB := parseHexColor(endColor)
lines := strings.Split(text, "\n")
if len(lines) == 0 {
return text
}
var result []string
for _, line := range lines {
if len(line) == 0 {
result = append(result, line)
continue
}
var coloredLine string
for i, char := range line {
ratio := float64(i) / float64(len(line)-1)
r := int(float64(startR) + float64(endR-startR)*ratio)
g := int(float64(startG) + float64(endG-startG)*ratio)
b := int(float64(startB) + float64(endB-startB)*ratio)
coloredLine += fmt.Sprintf("\033[38;2;%d;%d;%dm%s\033[0m", r, g, b, string(char))
}
result = append(result, coloredLine)
}
return strings.Join(result, "\n")
}
func parseHexColor(hex string) (int, int, int) {
hex = strings.TrimPrefix(hex, "#")
r, _ := strconv.ParseInt(hex[0:2], 16, 64)
g, _ := strconv.ParseInt(hex[2:4], 16, 64)
b, _ := strconv.ParseInt(hex[4:6], 16, 64)
return int(r), int(g), int(b)
}
func (m model) renderHeader() string {
title := gradientText(logoPattern, "#8B5CF6", "#EC4899")
title := logo.GradientText(logo.GetLogoPattern(), "#B413DC", "#00C8C8")
titleWithVersion := title + logo.GetVersionSuffix()
width := m.width - 4
if width < 20 {
width = 60
}
right := lipgloss.NewStyle().
Foreground(lipgloss.Color("#6B7280")).
Render("[Ctrl+C 退出]")
return lipgloss.NewStyle().
Width(width).
Render(title + strings.Repeat(" ", width-29-len(right)-1) + right)
Render(titleWithVersion)
}
func (m model) renderInputArea() string {

View File

@@ -894,4 +894,95 @@ v2:
```go
p := tea.NewProgram(model{})
p.Run()
```
```
---
### Logo模块化设计
**决策**: 创建 `internal/logo/logo.go` 统一管理logoTUI和CLI共享
**原因**:
1. 避免代码重复TUI和CLI都需要显示logo
2. 统一渐变色方案:紫→青 (`#B413DC``#00C8C8`)
3. 统一版本号格式:` ( v1.x.x )`` ( )`
**实现**:
```go
// 导出函数供外部调用
func GradientText(text string, startColor, endColor string) string
func GetLogoPattern() string // ASCII art图案
func GetVersionSuffix() string // " (v1.x.x )" 或 " ( )"
func PrintLogoWithVersion() // 打印完整logo
```
**版本注入**:
```go
// 编译时通过 ldflags 注入
// -X packagepath.variable=value
go build -ldflags "-X github.com/titor/fanyi/internal/logo.version=${VERSION}" -o yoyo ./cmd/yoyo
```
---
### CI构建环境问题
**问题1**: `./build.sh: not found
**原因**:
1. 远程CI使用 `golang:1.26-alpine` 镜像,默认没有 bash
2. build.sh 脚本 shebang 是 `#!/bin/bash`
**解决方案**:
```yaml
# .gitea/workflows/release.yaml
- name: Checkout
run: |
apk add git bash # 添加 bash
git clone ...
```
---
**问题2**: `error obtaining VCS status: exit status 128`
**原因**: CI 中 git 仓库信息不完整,导致 Go 获取 VCS 状态失败
**解决方案**:
```bash
# build.sh 中添加 -buildvcs=false
go build -buildvcs=false -ldflags "-s -w -X ..." -o yoyo ./cmd/yoyo
```
---
**问题3**: Release 说明只有 "Automated release"
**原因**: 创建 release 时 body 写死了固定文本
**解决方案**:
使用 annotated tag 的注释内容作为 release 说明:
```bash
git tag -a v1.2.0 -m "版本说明\n- 功能1\n- 功能2"
```
CI 中获取:
```bash
RELEASE_BODY=$(git tag -l --format='%(contents)' "$TAG_NAME")
```
---
### 终端颜色输出问题
**问题**: 在非TTY环境如管道ANSI转义序列可能显示为明文
**观察**:
- 使用 `script` 命令可以正确显示颜色
- `od -c` 检查输出包含正确的 `\033` 转义字符
- zsh 可能对某些颜色转义处理不同
**解决方案**:
- 使用 `GradientText` 函数逐字符应用渐变
- 每个字符后使用 `\033[0m` 重置
- 渐变使用 24-bit 颜色 `\033[38;2;R;G;Bm`

187
taolun.md
View File

@@ -794,4 +794,189 @@ ta.SetHeight(5) // 固定高度,不动态调整
- 遵循语义化版本:主版本.次版本.修订版本
- beta版使用 `-beta` 后缀
**关联版本**: [changelog.md#1.0.0-beta](changelog.md#100-beta-2026-04-07)
**关联版本**: [changelog.md#1.0.0-beta](changelog.md#100-beta-2026-04-07)
---
### [2026-04-07] 版本 1.1.0 - 配置路径修复和huh迁移
**原因**:
1. 管道模式下(如 `cd /docs && cat readme.md | yoyo`)找不到配置文件
2. onboard配置保存到错误的相对路径
3. 希望用 `charmbracelet/huh` 替代 `survey` 获得更好的UX
**分析**:
- 所有配置路径硬编码为 `configs/config.yaml`相对CWD
- 从不同目录运行程序时路径解析失败
- survey库API较老huh提供更现代的表单体验
**解决方案**:
1. 新增 `internal/config/path.go` 路径解析工具
2. 配置查找优先级:`--config` > `~/.config/yoyo/config.yaml` > `./configs/config.yaml`
3. onboard保存到 `~/.config/yoyo/config.yaml`
4. .env从 `~/.config/yoyo/.env` 加载
5. onboard使用huh重写Form+Group模式链式API泛型支持
**技术细节**:
```go
// 路径解析
config.ResolveConfigPath(userPath) // 智能查找配置
config.GetUserConfigPath() // ~/.config/yoyo/config.yaml
config.GetUserEnvPath() // ~/.config/yoyo/.env
```
**huh迁移要点**:
- `survey.Select` → `huh.NewSelect[string]().Options(huh.NewOption(...)...)`
- `survey.Input` → `huh.NewInput().Value(&var).Validate(fn)`
- `survey.Confirm` → `huh.NewConfirm().Affirmative("是").Negative("否")`
- 分步表单 → `huh.NewForm(huh.NewGroup(...), huh.NewGroup(...))`
**关联版本**: [changelog.md#1.1.0](changelog.md#110-2026-04-07)
---
### [2026-04-08 00:40] 版本 1.1.1 - Logo模块化与渐变色统一
**原因**:
1. `--help` `-h` `-?` `--version` 在默认交互式模式下无响应
2. 帮助信息头部需要彩色logo展示
**问题分析**:
1. 交互模式判断优先于help/version检查导致flags被忽略
2. 帮助信息使用硬编码版本号 `YOYO翻译工具 v1.1.0`无logo
**解决方案**:
1. 修复flag检查顺序在进入交互模式前先检查 `-h` `--help` `-?` `--version`
2. 新增 `internal/logo/logo.go` 模块统一管理logo
3. 编译时通过 `ldflags` 注入版本号
4. 渐变色使用与TUI一致的紫→青色方案
**技术实现**:
```go
// logo模块核心函数
func GradientText(text string, startColor, endColor string) string { ... }
func GetLogoPattern() string // 返回4行ascii art
func GetVersionSuffix() string // 返回 " (v1.1.1-dirty )" 或 " ( )"
func PrintLogoWithVersion() // 打印完整logo--help/--version使用
```
**build.sh版本注入**:
```bash
VERSION=$(git describe --tags --always --dirty 2>/dev/null || echo "")
go build -ldflags "-X github.com/titor/fanyi/internal/logo.version=${VERSION}" -o yoyo ./cmd/yoyo
```
**TUI整合**:
- 移除 `internal/tui/model.go` 中的本地 `logoPattern` 和 `gradientText`
- 直接调用 `logo.GradientText(logo.GetLogoPattern(), "#B413DC", "#00C8C8")`
- 移除 `[Ctrl+C 退出]` 显示
**最终输出格式**(所有位置统一):
```
_ _ _____ _____
( \/ ( _ ( _ )
\ / )(_)( )(_)(
(__)(_____(_____( v1.1.1-dirty )
```
---
### [2026-04-08] TUI界面帮助功能与样式改进
**原因**:
1. 用户需要在TUI底部添加帮助信息栏
2. 优化翻译卡片的显示样式
**分析**:
- 使用 bubbletea 的 help 组件实现帮助栏
- 翻译卡片需要与输入区域保持间距
**解决方案**:
1. **帮助功能实现**:
- 导入 `charm.land/bubbles/v2/help`
- 在 `model` 结构体添加 `help help.Model` 字段
- `KeyMap` 实现 `ShortHelp()` 和 `FullHelp()` 接口
- 按键绑定: Help -> Ctrl+H (原 ? 和 ctrl+? 不兼容)
- View 中添加 help.View(m.keys) 到底部
2. **翻译卡片样式**:
- 翻译结果 outputBlock 的 Padding 从 `(0,3,1,3)` 改为 `(1,3,1,3)` 增加上方空隙
- viewport 内容区域第一个卡片前添加 `"\n"` 作为上边距
3. **版本号注入**:
- 扩展 build.sh 支持跨平台编译
- 添加 -h 帮助选项
- 添加 -o 自定义输出文件名
- release.yaml 使用 build.sh 构建
**按键绑定历史**:
- 初始: `?` → shift+? 切换,且会输入到文本框
- 尝试: `ctrl+?` → 不兼容,无响应
- 最终: `ctrl+h` → 正常工作
**相关文件**:
- `internal/tui/keys.go` - KeyMap 定义和 ShortHelp/FullHelp
- `internal/tui/model.go` - help 组件集成、样式调整
- `build.sh` - 跨平台编译支持
- `.gitea/workflows/release.yaml` - CI 构建脚本
**关联版本**: [changelog.md#0.7.1](changelog.md#071-2026-04-08---tui界面改进)
**关联版本**: [changelog.md#1.1.1](changelog.md#111-2026-04-08)
---
### [2026-04-08] 版本号管理规则制定
**原因**: 版本号历史混乱,需要规范化管理
**讨论内容**:
1. **版本现状分析**:
- 历史标签混乱: v0.0.1 → v1.1.1 → v6.0 等
- changelog 中版本状态标记混乱
2. **确定版本规则**:
- 当前版本: v1.2.0 (TUI基础功能完成)
- 采用简化分支策略: main + feature 分支
- 合并时使用 --no-ff 保留分支历史
- 在 AGENTS.md 中详细定义版本号规则
3. **v1.2.0 发布标准**:
- TUI交互界面 ✅
- 多行输入 ✅
- 翻译卡片展示 ✅
- 状态栏 ✅
- 帮助信息 ✅
- 版本注入 ✅
- 跨平台编译 ✅
- CI构建 ✅
4. **保留历史标签**:
- 过去的版本作为来时的路,不再删除
- 从现在开始规范维护
**相关文件**:
- `AGENTS.md` - 版本号管理规则(扩展)
- `changelog.md` - 版本历史整理
- `build.sh` - 跨平台编译脚本
**关联版本**: [changelog.md#v1.2.0](changelog.md#v120-2026-04-08)
---
### [2026-04-08] Windows 交叉编译修复
**问题**: 交叉编译的 Windows 版本在 Windows 上提示"不支持16位应用程序"
**原因**:
- Linux CI 交叉编译 Windows 时,缺少 `CGO_ENABLED=0` 参数
- 原生编译go build 在 Windows 上)没有这个问题
**解决方案**:
- 在 build.sh 的 `windows/amd64` 分支添加 `CGO_ENABLED=0`
- 统一使用 `CGO_ENABLED=0` 进行构建
**相关文件**:
- `build.sh` - 添加 CGO_ENABLED=0
**关联版本**: [changelog.md#v1.2.0](changelog.md#v120-2026-04-08)