终端模块初版
This commit is contained in:
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
Reference in New Issue
Block a user