Files
readflow/src/core/translation.rs
大麦 600f205c87 feat: 完成 Phase 2-4 核心功能
## Phase 2 - 核心功能 (P0)
- Issue #5: EPUB/MOBI/AZW3 格式支持 
  - 修复 mobi 库 API 调用 (content_raw → content_as_string)
  - 修复 title()/author() 返回类型
  - 添加元数据提取功能

- Issue #6: Markdown 阅读模式 
  - 实现 parse_markdown_with_metadata
  - 支持 Front Matter (YAML) 解析
  - 使用 pulldown-cmark 解析引擎
  - 支持代码文件高亮

- Issue #7: 双语翻译功能 
  - 实现 TranslationService (阿里百炼/DeepL/Ollama)
  - 语言自动检测
  - 双语对照 HTML 渲染 (并排/段落交错模式)

- Issue #8: 笔记与书签系统 
  - BookmarkManager (高亮/下划线/波浪线/边注)
  - NoteManager (阅读笔记/想法/问题/总结)
  - 阅读统计 (时长/会话数/笔记数)
  - 导出 Markdown/CSV/Anki

## Phase 3 - 高级功能 (P1)
- Issue #9: 代码阅读器 
  - 支持 20+ 编程语言
  - syntect 语法高亮
  - 行号显示/代码折叠

- Issue #10: 全文双语对照 
  - 段落级翻译对照
  - 并排/交错两种模式
  - 响应式布局

- Issue #11: 阅读进度同步 
  - 本地进度追踪
  - 云端同步支持
  - 多设备冲突解决

- Issue #12: 插件系统 
  - 插件加载/卸载/启用/禁用
  - 插件依赖管理
  - 内置主题/快捷键插件

## Phase 4 - 性能与生态 (P1)
- Issue #13: 性能优化 
  - PerformanceProfiler 性能分析
  - CacheManager LRU 缓存
  - 性能监控与优化建议

## 技术栈更新
- 新增依赖:reqwest, uuid, chrono(serde)
- 核心模块:8 个 (document/translation/bookmark/note/code_reader/progress/plugin/performance)
- 代码量:~5000 行

---
🚀 ReadFlow MVP 核心功能全部完成!
2026-03-10 14:29:56 +08:00

226 lines
7.4 KiB
Rust
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.
//! 翻译服务模块
//!
//! 支持多种翻译提供商阿里百炼、DeepL、Ollama
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone)]
pub enum TranslationProvider {
AliBailian, // 阿里百炼
DeepL,
Ollama,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TranslationConfig {
pub provider: String,
pub api_key: Option<String>,
pub api_url: Option<String>,
pub model: Option<String>,
}
impl Default for TranslationConfig {
fn default() -> Self {
Self {
provider: "ali_bailian".to_string(),
api_key: None,
api_url: Some("https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation".to_string()),
model: Some("qwen-turbo".to_string()),
}
}
}
#[derive(Debug, Clone)]
pub struct TranslationResult {
pub original: String,
pub translated: String,
pub source_lang: String,
pub target_lang: String,
}
pub struct TranslationService {
provider: TranslationProvider,
config: TranslationConfig,
}
impl TranslationService {
pub fn new(provider: TranslationProvider, config: TranslationConfig) -> Self {
Self { provider, config }
}
pub fn with_default_config() -> Self {
Self {
provider: TranslationProvider::AliBailian,
config: TranslationConfig::default(),
}
}
/// 翻译文本
pub fn translate(&self, text: &str, from: &str, to: &str) -> Result<String> {
match self.provider {
TranslationProvider::AliBailian => self.translate_with_bailian(text, from, to),
TranslationProvider::DeepL => self.translate_with_deepl(text, from, to),
TranslationProvider::Ollama => self.translate_with_ollama(text, from, to),
}
}
/// 使用阿里百炼翻译
fn translate_with_bailian(&self, text: &str, from: &str, to: &str) -> Result<String> {
// 构建翻译请求 prompt
let prompt = format!(
"Translate the following text from {} to {}. Only output the translation, no explanations:\n\n{}",
from, to, text
);
// 调用阿里百炼 API
let client = reqwest::blocking::Client::new();
let request_body = serde_json::json!({
"model": self.config.model.as_deref().unwrap_or("qwen-turbo"),
"input": {
"messages": [
{
"role": "user",
"content": prompt
}
]
},
"parameters": {
"temperature": 0.1,
"max_tokens": 2000
}
});
let api_key = self.config.api_key.as_deref()
.context("阿里百炼 API Key 未配置")?;
let response = client
.post(self.config.api_url.as_deref().unwrap_or("https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation"))
.header("Authorization", format!("Bearer {}", api_key))
.header("Content-Type", "application/json")
.json(&request_body)
.send()
.context("翻译请求失败")?;
if !response.status().is_success() {
anyhow::bail!("翻译 API 返回错误:{}", response.status());
}
let result: serde_json::Value = response.json()
.context("解析翻译响应失败")?;
// 提取翻译结果
let translated = result["output"]["choices"][0]["message"]["content"]
.as_str()
.context("翻译结果为空")?
.to_string();
Ok(translated.trim().to_string())
}
/// 使用 DeepL 翻译
fn translate_with_deepl(&self, text: &str, from: &str, to: &str) -> Result<String> {
let api_key = self.config.api_key.as_deref()
.context("DeepL API Key 未配置")?;
let client = reqwest::blocking::Client::new();
let response = client
.post("https://api-free.deepl.com/v2/translate")
.header("Authorization", format!("DeepL-Auth-Key {}", api_key))
.header("Content-Type", "application/json")
.json(&serde_json::json!({
"text": [text],
"source_lang": from.to_uppercase(),
"target_lang": to.to_uppercase()
}))
.send()
.context("DeepL 翻译请求失败")?;
let result: serde_json::Value = response.json()?;
let translated = result["translations"][0]["text"]
.as_str()
.context("DeepL 翻译结果为空")?
.to_string();
Ok(translated)
}
/// 使用 Ollama 本地模型翻译
fn translate_with_ollama(&self, text: &str, from: &str, to: &str) -> Result<String> {
let api_url = self.config.api_url.as_deref()
.unwrap_or("http://localhost:11434/api/generate");
let prompt = format!(
"Translate from {} to {}. Only output translation:\n{}",
from, to, text
);
let client = reqwest::blocking::Client::new();
let response = client
.post(api_url)
.header("Content-Type", "application/json")
.json(&serde_json::json!({
"model": self.config.model.as_deref().unwrap_or("qwen2.5:7b"),
"prompt": prompt,
"stream": false
}))
.send()
.context("Ollama 翻译请求失败")?;
let result: serde_json::Value = response.json()?;
let translated = result["response"]
.as_str()
.context("Ollama 翻译结果为空")?
.to_string();
Ok(translated.trim().to_string())
}
/// 检测语言
pub fn detect_language(&self, text: &str) -> Result<String> {
// 简单语言检测:基于字符特征
let has_chinese = text.chars().any(|c| c >= '\u{4e00}' && c <= '\u{9fff}');
let has_japanese = text.chars().any(|c| c >= '\u{3040}' && c <= '\u{309f}' || c >= '\u{30a0}' && c <= '\u{30ff}');
let has_korean = text.chars().any(|c| c >= '\u{ac00}' && c <= '\u{d7af}');
if has_chinese {
Ok("zh".to_string())
} else if has_japanese {
Ok("ja".to_string())
} else if has_korean {
Ok("ko".to_string())
} else {
Ok("en".to_string())
}
}
/// 批量翻译(用于文档段落)
pub fn translate_batch(&self, texts: &[String], from: &str, to: &str) -> Result<Vec<TranslationResult>> {
let mut results = Vec::new();
for text in texts {
let translated = self.translate(text, from, to)?;
results.push(TranslationResult {
original: text.clone(),
translated,
source_lang: from.to_string(),
target_lang: to.to_string(),
});
}
Ok(results)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_language_detection() {
let service = TranslationService::with_default_config();
assert_eq!(service.detect_language("你好世界").unwrap(), "zh");
assert_eq!(service.detect_language("Hello World").unwrap(), "en");
}
}