Files
Thing/src-tauri/src/translate/engines/mymemory.rs
T
2026-09-17 17:32:20 +08:00

217 lines
7.8 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.
//! MyMemory 免密钥翻译源。
//!
//! 定位:国内可直接访问的免密兜底。质量不如大模型,但胜在零配置、无代理依赖。
//!
//! 两个必须知道的限制:
//! - **不支持自动检测源语言**。`langpair` 要求显式源语言,因此源语言为 `auto` 时
//! 直接返回可操作的配置错误(而不是发一个注定失败的请求),让「自动」模式降级到下一个源。
//! - 免费额度按**字节**计(约 500 字节/次,匿名另有每日上限)。超限时上游返回
//! `responseStatus: 403` 并在 `responseDetails` 里说明,这里原样透传给用户看。
use std::time::{Duration, Instant};
use serde::Deserialize;
use super::{
is_auto, provider_code, ErrorKind, TranslateEngine, TranslateError, TranslateResult,
};
use crate::translate::settings::TranslateEngineConfig;
const ENDPOINT: &str = "https://api.mymemory.translated.net/get";
/// 单次查询的字节上限(上游按字节限制,且 langpair 也占额度的一部分)
const MAX_QUERY_BYTES: usize = 500;
const PROBE_TEXT: &str = "Hello, world.";
pub struct MyMemoryEngine {
cfg: TranslateEngineConfig,
client: reqwest::Client,
}
impl MyMemoryEngine {
pub fn new(cfg: TranslateEngineConfig, client: reqwest::Client) -> Self {
Self { cfg, client }
}
}
#[async_trait::async_trait]
impl TranslateEngine for MyMemoryEngine {
fn config(&self) -> &TranslateEngineConfig {
&self.cfg
}
async fn translate(
&self,
req: &super::EngineRequest,
) -> Result<TranslateResult, TranslateError> {
let text = req.text.trim();
if text.is_empty() {
return Err(TranslateError::empty());
}
if is_auto(&req.from) {
// 不猜:源语言未知时上游无法工作,明确返回配置类错误(可降级)。
// 文案要同时覆盖两个入口:主面板的源语言下拉、划词弹窗顶部的「自」循环按钮
// ——弹窗的源语言是独立记忆项,与主面板的选择无关,不点明用户会以为
// 「主界面选过了为什么还报错」。
return Err(TranslateError::config(format!(
"「{}」需要显式指定源语言:主面板请在源语言下拉中选具体语言;\
划词弹窗请点击顶部的「自」按钮切换源语言(选择会被记住)",
self.cfg.name
)));
}
if text.len() > MAX_QUERY_BYTES {
return Err(TranslateError::unsupported(format!(
"文本 {} 字节,超过「{}」的单次上限({MAX_QUERY_BYTES} 字节)",
text.len(),
self.cfg.name
)));
}
let target = provider_code(&req.to);
if target.trim().is_empty() {
return Err(TranslateError::config("未指定目标语言"));
}
let pair = format!("{}|{}", provider_code(&req.from), target);
let started = Instant::now();
let resp = self
.client
.get(ENDPOINT)
.query(&[("q", text), ("langpair", pair.as_str())])
.timeout(Duration::from_millis(self.cfg.timeout_ms.max(2_000)))
.send()
.await
.map_err(|e| classify_reqwest(e, self.cfg.name.as_str()))?;
let status = resp.status();
let raw = resp.text().await.map_err(|e| {
TranslateError::network(format!("读取「{}」响应失败: {e}", self.cfg.name))
})?;
if !status.is_success() {
return Err(TranslateError::network(format!(
"「{}」返回 HTTP {status}",
self.cfg.name
))
.with_detail(raw.trim()));
}
let parsed: MyMemoryResponse = serde_json::from_str(&raw).map_err(|e| {
TranslateError::parse(format!("「{}」响应不是预期的 JSON: {e}", self.cfg.name))
.with_detail(&raw)
})?;
// responseStatus 可能是数字 200 也可能是字符串 "403",统一取整再判断
let code = parsed
.response_status
.as_ref()
.and_then(status_as_u64);
if let Some(c) = code {
if c != 200 {
let detail = parsed
.response_details
.clone()
.unwrap_or_else(|| "上游未提供说明".to_string());
let kind = if c == 403 { ErrorKind::RateLimit } else { ErrorKind::Unknown };
return Err(TranslateError::new(
kind,
format!("「{}」拒绝了请求(status={c}", self.cfg.name),
)
.with_detail(detail));
}
}
let text_out = parsed
.response_data
.and_then(|d| d.translated_text)
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
let out = match text_out {
Some(t) => t,
None => {
// 免费额度用尽时也会走到这里,把上游说明带上,用户才知所以然
let detail = parsed
.response_details
.filter(|d| !d.trim().is_empty())
.unwrap_or_else(|| raw.clone());
return Err(TranslateError::new(
ErrorKind::Empty,
format!("「{}」未返回译文(额度用尽或语言对不支持)", self.cfg.name),
)
.with_detail(detail));
}
};
Ok(TranslateResult {
text: out,
// 源语言是用户显式指定的,没有可上报的检测结果
detected: None,
engine_id: self.cfg.id.clone(),
engine_name: self.cfg.name.clone(),
latency_ms: started.elapsed().as_millis() as u64,
usage: None,
})
}
async fn list_models(&self) -> Result<Vec<String>, TranslateError> {
Ok(Vec::new())
}
async fn test(&self) -> Result<String, TranslateError> {
let req = super::EngineRequest {
text: PROBE_TEXT.to_string(),
from: "en".to_string(),
to: "zh-Hans".to_string(),
to_label: "简体中文".to_string(),
from_label: "英语".to_string(),
mode: super::TranslateMode::Translate,
image_png: None,
via: "preview".to_string(),
};
let started = Instant::now();
let result = self.translate(&req).await?;
Ok(format!(
"连通正常 · {}ms · 回显「{}」",
started.elapsed().as_millis(),
result.text
))
}
}
/// 从数字或字符串形式的 status 字段取整
fn status_as_u64(v: &serde_json::Value) -> Option<u64> {
match v {
serde_json::Value::Number(n) => n.as_u64(),
serde_json::Value::String(s) => s.trim().parse::<u64>().ok(),
_ => None,
}
}
fn classify_reqwest(e: reqwest::Error, engine: &str) -> TranslateError {
if e.is_timeout() {
return TranslateError::timeout(format!(
"请求「{engine}」超时,可调大超时时间或检查网络"
));
}
if e.is_connect() {
return TranslateError::network(format!("无法连接「{engine}」:{e},请检查网络"));
}
TranslateError::network(format!("请求「{engine}」失败:{e}"))
}
#[derive(Debug, Deserialize)]
struct MyMemoryResponse {
#[serde(rename = "responseData", default)]
response_data: Option<ResponseData>,
#[serde(rename = "responseStatus", default)]
response_status: Option<serde_json::Value>,
#[serde(rename = "responseDetails", default)]
response_details: Option<String>,
}
#[derive(Debug, Deserialize)]
struct ResponseData {
#[serde(rename = "translatedText", default)]
translated_text: Option<String>,
}