终端模块初版

This commit is contained in:
zhongluofeng
2026-09-18 18:28:13 +08:00
parent f6c1cc250e
commit b018abd922
63 changed files with 26695 additions and 185 deletions
+91 -23
View File
@@ -43,12 +43,12 @@ impl AiEngine {
/// API 根地址。容错处理:用户常把完整端点(`.../chat/completions`)直接粘进来,
/// 若不在末尾剥掉,就会拼出 `.../chat/completions/chat/completions`
/// 而这类错误在上游表现为 404,排查成本远高于此处一行判断。
fn api_root(&self) -> Result<String, TranslateError> {
let raw = self.cfg.base_url.trim();
fn api_root(cfg: &TranslateEngineConfig) -> Result<String, TranslateError> {
let raw = cfg.base_url.trim();
if raw.is_empty() {
return Err(TranslateError::config(format!(
"引擎「{}」尚未配置 Base URL",
self.cfg.name
cfg.name
)));
}
if !(raw.starts_with("http://") || raw.starts_with("https://")) {
@@ -66,36 +66,36 @@ impl AiEngine {
Ok(root)
}
fn chat_endpoint(&self) -> Result<String, TranslateError> {
Ok(format!("{}/chat/completions", self.api_root()?))
fn chat_endpoint(cfg: &TranslateEngineConfig) -> Result<String, TranslateError> {
Ok(format!("{}/chat/completions", Self::api_root(cfg)?))
}
fn models_endpoint(&self) -> Result<String, TranslateError> {
Ok(format!("{}/models", self.api_root()?))
fn models_endpoint(cfg: &TranslateEngineConfig) -> Result<String, TranslateError> {
Ok(format!("{}/models", Self::api_root(cfg)?))
}
/// 密钥只从系统凭据管理器读,不进配置文件、不经前端。
fn api_key(&self) -> String {
crate::translate::engine_api_key(&self.cfg.id)
fn api_key(cfg: &TranslateEngineConfig) -> String {
crate::translate::engine_api_key(&cfg.id)
}
fn require_key(&self) -> Result<String, TranslateError> {
let key = self.api_key();
fn require_key(cfg: &TranslateEngineConfig) -> Result<String, TranslateError> {
let key = Self::api_key(cfg);
if key.trim().is_empty() {
return Err(TranslateError::auth(format!(
"引擎「{}」尚未配置 API Key,请在翻译设置中填写",
self.cfg.name
cfg.name
)));
}
Ok(key)
}
fn require_model(&self) -> Result<String, TranslateError> {
let model = self.cfg.model.trim();
fn require_model(cfg: &TranslateEngineConfig) -> Result<String, TranslateError> {
let model = cfg.model.trim();
if model.is_empty() {
return Err(TranslateError::config(format!(
"引擎「{}」尚未选择模型,可在设置中拉取模型列表后选择",
self.cfg.name
cfg.name
)));
}
Ok(model.to_string())
@@ -162,9 +162,9 @@ impl TranslateEngine for AiEngine {
if req.text.trim().is_empty() {
return Err(TranslateError::empty());
}
let key = self.require_key()?;
let model = self.require_model()?;
let endpoint = self.chat_endpoint()?;
let key = Self::require_key(&self.cfg)?;
let model = Self::require_model(&self.cfg)?;
let endpoint = Self::chat_endpoint(&self.cfg)?;
let mut body = serde_json::Map::new();
body.insert("model".to_string(), json!(model));
@@ -267,9 +267,9 @@ impl TranslateEngine for AiEngine {
if req.text.trim().is_empty() && req.image_png.is_none() {
return Err(TranslateError::empty());
}
let key = self.require_key()?;
let model = self.require_model()?;
let endpoint = self.chat_endpoint()?;
let key = Self::require_key(&self.cfg)?;
let model = Self::require_model(&self.cfg)?;
let endpoint = Self::chat_endpoint(&self.cfg)?;
let mut body = serde_json::Map::new();
body.insert("model".to_string(), json!(model));
@@ -389,8 +389,8 @@ impl TranslateEngine for AiEngine {
}
async fn list_models(&self) -> Result<Vec<String>, TranslateError> {
let key = self.require_key()?;
let endpoint = self.models_endpoint()?;
let key = Self::require_key(&self.cfg)?;
let endpoint = Self::models_endpoint(&self.cfg)?;
let resp = self
.client
.get(&endpoint)
@@ -591,3 +591,71 @@ struct ModelEntry {
#[serde(default)]
id: String,
}
/// 通用(非翻译语义)的对话补全入口:供终端 AI 助手等模块复用引擎配置。
///
/// 与翻译路径共享端点归一(剥 `/chat/completions` 后缀)、密钥存取
/// (凭据管理器)、`apply_common_params`temperature / max_tokens / extra_body
/// 与响应解析,但 **消息由调用方全量给定**——这里不含任何翻译提示词语义。
///
/// 刻意做成关联函数而不是 `AiEngine` 的实例方法:调用方(终端助手)只持有
/// `TranslateEngineConfig`,为它构造 `AiEngine` 还要 PromptTemplates 与 client
/// 属于无谓的耦合。
pub async fn chat_once(
cfg: &TranslateEngineConfig,
messages: Vec<(&str, String)>,
) -> Result<String, String> {
let key = AiEngine::require_key(cfg).map_err(|e| e.to_string())?;
let model = AiEngine::require_model(cfg).map_err(|e| e.to_string())?;
let endpoint = AiEngine::chat_endpoint(cfg).map_err(|e| e.to_string())?;
let mut body = serde_json::Map::new();
body.insert("model".to_string(), json!(model));
body.insert("stream".to_string(), json!(false));
body.insert(
"messages".to_string(),
json!(messages
.into_iter()
.map(|(role, content)| json!({ "role": role, "content": content }))
.collect::<Vec<_>>()),
);
apply_common_params(&mut body, cfg);
let client = reqwest::Client::new();
let resp = client
.post(&endpoint)
.bearer_auth(&key)
.timeout(Duration::from_millis(cfg.timeout_ms.max(1000)))
.json(&serde_json::Value::Object(body))
.send()
.await
.map_err(|e| classify_reqwest(e, &cfg.name).to_string())?;
let status = resp.status();
let raw = resp
.text()
.await
.map_err(|e| format!("读取「{}」响应失败: {e}", cfg.name))?;
if !status.is_success() {
return Err(classify_http(status.as_u16(), &raw, &cfg.name, &model).to_string());
}
let parsed: ChatResponse = serde_json::from_str(&raw)
.map_err(|e| format!("{}」响应不是预期的 JSON: {e}", cfg.name))?;
if let Some(err) = parsed.error {
let msg = err
.message
.filter(|m| !m.trim().is_empty())
.unwrap_or_else(|| "上游返回了错误对象".to_string());
return Err(format!("{}」返回错误:{msg}", cfg.name));
}
parsed
.choices
.into_iter()
.next()
.and_then(|c| c.message.content)
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.ok_or_else(|| format!("{}」返回了空内容", cfg.name))
}
+4 -1
View File
@@ -50,9 +50,12 @@ pub use engines::{TranslateEngine, TranslateError, TranslateResult};
// `engines::` / `settings::` 下,等真正用到时再提升到此处——提前摆出一堆无人消费的再导出,
// 只会让「谁在用」更难判断。
pub use settings::TranslateSettings;
// 终端 AI 助手(terminal/assistant.rs)复用引擎配置与通用对话补全——
// 「现在真正用到了」,按上面的原则提升到此处。
pub use settings::TranslateEngineConfig;
pub use engines::ai::chat_once;
use engines::EngineRequest;
use settings::TranslateEngineConfig;
use std::path::PathBuf;
use std::sync::Mutex;
use std::time::{Duration, Instant};