//! 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 { 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, TranslateError> { Ok(Vec::new()) } async fn test(&self) -> Result { 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 { match v { serde_json::Value::Number(n) => n.as_u64(), serde_json::Value::String(s) => s.trim().parse::().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, #[serde(rename = "responseStatus", default)] response_status: Option, #[serde(rename = "responseDetails", default)] response_details: Option, } #[derive(Debug, Deserialize)] struct ResponseData { #[serde(rename = "translatedText", default)] translated_text: Option, }