1use async_trait::async_trait;
2use dialoguer::{Confirm, Select};
3use log::{debug, info, warn};
4use crate::config::{AIService, Config, AIServiceConfig};
5use crate::terminal_format::print_progress;
6
7#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
8pub struct Message {
9 pub role: String,
10 pub content: String,
11}
12use copilot_client::CopilotClient;
13
14#[async_trait]
15pub trait AiService: Send + Sync {
16 async fn translate(&self, text: &str, direction: &crate::config::TranslateDirection) -> anyhow::Result<String> {
17 let system_prompt = get_translation_prompt(text, direction);
19 Ok(self.chat(&system_prompt, text).await?)
20 }
21
22 async fn chat(&self, system_prompt: &str, user_content: &str) -> anyhow::Result<String>;
23}
24
25pub use AiService as Translator; pub struct DeepSeekTranslator {
28 api_key: String,
29 endpoint: String,
30 model: String,
31 timeout_seconds: u64,
32 max_tokens: u64,
33}
34
35pub struct OpenAITranslator {
36 api_key: String,
37 endpoint: String,
38 model: String,
39 timeout_seconds: u64,
40 max_tokens: u64,
41}
42
43pub struct ClaudeTranslator {
44 api_key: String,
45 endpoint: String,
46 model: String,
47 timeout_seconds: u64,
48 max_tokens: u64,
49}
50
51pub struct CopilotTranslator {
52 client: CopilotClient,
53 model: String,
54}
55
56pub struct GeminiTranslator {
57 api_key: String,
58 endpoint: String,
59 model: String,
60 timeout_seconds: u64,
61 max_tokens: u64,
62}
63
64pub struct GrokTranslator {
65 api_key: String,
66 endpoint: String,
67 model: String,
68 timeout_seconds: u64,
69 max_tokens: u64,
70}
71
72pub struct QwenTranslator {
73 api_key: String,
74 endpoint: String,
75 model: String,
76 timeout_seconds: u64,
77 max_tokens: u64,
78}
79
80impl DeepSeekTranslator {
81 pub fn new(config: &AIServiceConfig) -> Self {
82 Self {
83 api_key: config.api_key.clone(),
84 endpoint: config.api_endpoint.clone()
85 .unwrap_or_else(|| "https://api.deepseek.com/v1".into()),
86 model: config.model.clone()
87 .unwrap_or_else(|| "deepseek-chat".into()),
88 timeout_seconds: crate::config::Config::load()
89 .map(|c| c.timeout_seconds)
90 .unwrap_or(20),
91 max_tokens: crate::config::Config::load()
92 .map(|c| c.max_tokens)
93 .unwrap_or(2048),
94 }
95 }
96}
97
98impl OpenAITranslator {
99 pub fn new(config: &AIServiceConfig) -> Self {
100 Self {
101 api_key: config.api_key.clone(),
102 endpoint: config.api_endpoint.clone()
103 .unwrap_or_else(|| "https://api.openai.com/v1".into()),
104 model: config.model.clone()
105 .unwrap_or_else(|| "gpt-3.5-turbo".into()),
106 timeout_seconds: crate::config::Config::load()
107 .map(|c| c.timeout_seconds)
108 .unwrap_or(20),
109 max_tokens: crate::config::Config::load()
110 .map(|c| c.max_tokens)
111 .unwrap_or(2048),
112 }
113 }
114}
115
116impl ClaudeTranslator {
117 pub fn new(config: &AIServiceConfig) -> Self {
118 Self {
119 api_key: config.api_key.clone(),
120 endpoint: config.api_endpoint.clone()
121 .unwrap_or_else(|| "https://api.anthropic.com/v1".into()),
122 model: config.model.clone()
123 .unwrap_or_else(|| "claude-3-sonnet-20240229".into()),
124 timeout_seconds: crate::config::Config::load()
125 .map(|c| c.timeout_seconds)
126 .unwrap_or(20),
127 max_tokens: crate::config::Config::load()
128 .map(|c| c.max_tokens)
129 .unwrap_or(2048),
130 }
131 }
132}
133
134impl CopilotTranslator {
135 pub fn new(client: CopilotClient, model: String) -> Self {
136 Self { client, model }
137 }
138}
139
140impl GeminiTranslator {
141 pub fn new(config: &AIServiceConfig) -> Self {
142 Self {
143 api_key: config.api_key.clone(),
144 endpoint: config.api_endpoint.clone()
145 .unwrap_or_else(|| "https://generativelanguage.googleapis.com/v1beta".into()),
146 model: config.model.clone()
147 .unwrap_or_else(|| "gemini-2.0-flash".into()),
148 timeout_seconds: crate::config::Config::load()
149 .map(|c| c.timeout_seconds)
150 .unwrap_or(20),
151 max_tokens: crate::config::Config::load()
152 .map(|c| c.max_tokens)
153 .unwrap_or(2048),
154 }
155 }
156}
157
158impl GrokTranslator {
159 pub fn new(config: &AIServiceConfig) -> Self {
160 Self {
161 api_key: config.api_key.clone(),
162 endpoint: config.api_endpoint.clone()
163 .unwrap_or_else(|| "https://api.x.ai/v1".into()),
164 model: config.model.clone()
165 .unwrap_or_else(|| "grok-3-latest".into()),
166 timeout_seconds: crate::config::Config::load()
167 .map(|c| c.timeout_seconds)
168 .unwrap_or(20),
169 max_tokens: crate::config::Config::load()
170 .map(|c| c.max_tokens)
171 .unwrap_or(2048),
172 }
173 }
174}
175
176impl QwenTranslator {
177 pub fn new(config: &AIServiceConfig) -> Self {
178 Self {
179 api_key: config.api_key.clone(),
180 endpoint: config.api_endpoint.clone()
181 .unwrap_or_else(|| "https://dashscope.aliyuncs.com/compatible-mode/v1".into()),
182 model: config.model.clone()
183 .unwrap_or_else(|| "qwen-plus".into()),
184 timeout_seconds: crate::config::Config::load()
185 .map(|c| c.timeout_seconds)
186 .unwrap_or(20),
187 max_tokens: crate::config::Config::load()
188 .map(|c| c.max_tokens)
189 .unwrap_or(2048),
190 }
191 }
192}
193
194fn wrap_chinese_text(text: &str, max_width: usize) -> String {
196 let mut result = String::new();
197 let mut current_line = String::new();
198 let mut current_width = 0;
199
200 for c in text.chars() {
201 let char_width = if c.is_ascii() { 1 } else { 2 };
202
203 if current_width + char_width > max_width {
204 result.push_str(¤t_line);
205 result.push('\n');
206 current_line.clear();
207 current_width = 0;
208 }
209
210 current_line.push(c);
211 current_width += char_width;
212 }
213
214 if !current_line.is_empty() {
215 result.push_str(¤t_line);
216 }
217
218 result
219}
220
221fn get_translation_prompt(text: &str, direction: &crate::config::TranslateDirection) -> String {
222 use crate::config::TranslateDirection;
223
224 let prompt = match direction {
225 TranslateDirection::ChineseToEnglish => format!(
226 r#"You are a professional translator. Please translate the following Chinese text to English.
227 Important rules:
228 1. Keep all English content, numbers, and English punctuation unchanged
229 2. Do not translate any content inside English double quotes
230 3. Preserve the case of all English words
231 4. Only return the English translation, DO NOT include the original Chinese text
232 5. Keep simple and concise, no need to rewrite or expand the content
233
234 Example response format:
235 feat: add support for external plugins
236
237 1. Implement plugin loading mechanism
238 2. Add plugin configuration interface
239 3. Setup plugin discovery path: "/插件"
240
241 Text to translate:
242 {}"#,
243 wrap_chinese_text(text, 72)
244 ),
245 TranslateDirection::EnglishToChinese => format!(
246 r#"You are a professional translator. Please translate the following English text to Chinese.
247 Important rules:
248 1. Keep all Chinese content, numbers, and Chinese punctuation unchanged
249 2. Do not translate any content inside quotes
250 3. Maintain the original text structure and formatting
251 4. Only return the Chinese translation, DO NOT include the original English text
252 5. Keep simple and concise, no need to rewrite or expand the content
253 6. Use appropriate Chinese punctuation marks
254
255 Example response format:
256 feat: 添加外部插件支持
257
258 1. 实现插件加载机制
259 2. 添加插件配置接口
260 3. 设置插件发现路径: "/plugins"
261
262 Text to translate:
263 {}"#,
264 text
265 ),
266 };
267
268 debug!("生成的提示词:\n{}", prompt);
269 prompt
270}
271
272#[async_trait]
273impl AiService for DeepSeekTranslator {
274 async fn chat(&self, system_prompt: &str, user_content: &str) -> anyhow::Result<String> {
275 debug!("使用 DeepSeek,API Endpoint: {}", self.endpoint);
276 let client = reqwest::Client::builder()
277 .timeout(std::time::Duration::from_secs(self.timeout_seconds))
278 .build()?;
279
280 let url = format!("{}/chat/completions", self.endpoint);
281 let messages = vec![
282 serde_json::json!({
283 "role": "system",
284 "content": system_prompt
285 }),
286 serde_json::json!({
287 "role": "user",
288 "content": user_content
289 })
290 ];
291 debug!("发送给 DeepSeek 的消息:\n{}", serde_json::to_string_pretty(&messages)?);
292 let body = serde_json::json!({
293 "model": self.model,
294 "messages": messages,
295 "max_tokens": self.max_tokens
296 });
297
298 let ai_host = match url.split('/').nth(2) {
299 Some(host) => host,
300 None => "api.deepseek.com",
301 };
302 print_progress(&format!("正在请求 {} 进行AI对话", ai_host), None);
303
304 loop {
305 match client
306 .post(&url)
307 .header("Authorization", format!("Bearer {}", self.api_key))
308 .json(&body)
309 .send()
310 .await
311 {
312 Ok(response) => {
313 print_progress(&format!("正在请求 {} 进行AI对话", ai_host), Some(100));
314 debug!("收到响应: {:#?}", response);
315
316 if !response.status().is_success() {
317 let error_json = response.json::<serde_json::Value>().await?;
318 debug!("响应内容: {}", serde_json::to_string_pretty(&error_json)?);
319 return Err(anyhow::anyhow!("API 调用失败: {}",
320 error_json["error"]["message"].as_str().unwrap_or("未知错误")));
321 }
322
323 let result = response.json::<serde_json::Value>().await?;
324 debug!("响应内容: {}", serde_json::to_string_pretty(&result)?);
325
326 let response = result["choices"][0]["message"]["content"]
327 .as_str()
328 .unwrap_or_default();
329 return Ok(response.to_string());
330 }
331 Err(e) if e.is_timeout() => {
332 warn!("请求超时: {}", e);
333 if !Confirm::with_theme(&dialoguer::theme::ColorfulTheme::default())
334 .with_prompt("请求超时,是否重试?")
335 .default(true)
336 .interact()? {
337 return Err(anyhow::anyhow!("请求超时"));
338 }
339 continue;
340 }
341 Err(e) => return Err(e.into()),
342 }
343 }
344 }
345}
346
347#[async_trait]
348impl AiService for OpenAITranslator {
349 async fn chat(&self, system_prompt: &str, user_content: &str) -> anyhow::Result<String> {
350 debug!("使用 OpenAI,API Endpoint: {}", self.endpoint);
351 let client = reqwest::Client::builder()
352 .timeout(std::time::Duration::from_secs(self.timeout_seconds))
353 .build()?;
354
355 let url = format!("{}/chat/completions", self.endpoint);
356 let messages = vec![
357 serde_json::json!({
358 "role": "system",
359 "content": system_prompt
360 }),
361 serde_json::json!({
362 "role": "user",
363 "content": user_content
364 })
365 ];
366 debug!("发送给 OpenAI 的消息:\n{}", serde_json::to_string_pretty(&messages)?);
367 let body = serde_json::json!({
368 "model": self.model,
369 "messages": messages,
370 "max_tokens": self.max_tokens
371 });
372
373 let ai_host = match url.split('/').nth(2) {
374 Some(host) => host,
375 None => "api.openai.com",
376 };
377 print_progress(&format!("正在请求 {} 进行AI对话", ai_host), None);
378
379 loop {
380 match client
381 .post(&url)
382 .header("Authorization", format!("Bearer {}", self.api_key))
383 .json(&body)
384 .send()
385 .await
386 {
387 Ok(response) => {
388 print_progress(&format!("正在请求 {} 进行AI对话", ai_host), Some(100));
389 debug!("收到响应: {:#?}", response);
390
391 if !response.status().is_success() {
392 let error_json = response.json::<serde_json::Value>().await?;
393 debug!("响应内容: {}", serde_json::to_string_pretty(&error_json)?);
394 return Err(anyhow::anyhow!("API 调用失败: {}",
395 error_json["error"]["message"].as_str().unwrap_or("未知错误")));
396 }
397
398 let result = response.json::<serde_json::Value>().await?;
399 debug!("响应内容: {}", serde_json::to_string_pretty(&result)?);
400
401 let response = result["choices"][0]["message"]["content"]
402 .as_str()
403 .unwrap_or_default();
404 return Ok(response.to_string());
405 }
406 Err(e) if e.is_timeout() => {
407 warn!("请求超时: {}", e);
408 if !Confirm::with_theme(&dialoguer::theme::ColorfulTheme::default())
409 .with_prompt("请求超时,是否重试?")
410 .default(true)
411 .interact()? {
412 return Err(anyhow::anyhow!("请求超时"));
413 }
414 continue;
415 }
416 Err(e) => return Err(e.into()),
417 }
418 }
419 }
420}
421
422#[async_trait]
423impl AiService for ClaudeTranslator {
424 async fn chat(&self, system_prompt: &str, user_content: &str) -> anyhow::Result<String> {
425 debug!("使用 Claude,API Endpoint: {}", self.endpoint);
426 let client = reqwest::Client::builder()
427 .timeout(std::time::Duration::from_secs(self.timeout_seconds))
428 .build()?;
429
430 let url = format!("{}/messages", self.endpoint);
431 let messages = vec![
432 serde_json::json!({
433 "role": "system",
434 "content": system_prompt
435 }),
436 serde_json::json!({
437 "role": "user",
438 "content": user_content
439 })
440 ];
441 debug!("发送给 Claude 的消息:\n{}", serde_json::to_string_pretty(&messages)?);
442 let body = serde_json::json!({
443 "model": self.model,
444 "messages": messages,
445 "max_tokens": self.max_tokens
446 });
447
448 let ai_host = match url.split('/').nth(2) {
449 Some(host) => host,
450 None => "api.anthropic.com",
451 };
452 print_progress(&format!("正在请求 {} 进行AI对话", ai_host), None);
453
454 loop {
455 match client
456 .post(&url)
457 .header("Authorization", format!("Bearer {}", self.api_key))
458 .header("anthropic-version", "2023-06-01")
459 .json(&body)
460 .send()
461 .await
462 {
463 Ok(response) => {
464 print_progress(&format!("正在请求 {} 进行AI对话", ai_host), Some(100));
465 debug!("收到响应: {:#?}", response);
466
467 if !response.status().is_success() {
468 let error_json = response.json::<serde_json::Value>().await?;
469 debug!("响应内容: {}", serde_json::to_string_pretty(&error_json)?);
470 return Err(anyhow::anyhow!("API 调用失败: {}",
471 error_json["error"]["message"].as_str().unwrap_or("未知错误")));
472 }
473
474 let result = response.json::<serde_json::Value>().await?;
475 debug!("响应内容: {}", serde_json::to_string_pretty(&result)?);
476
477 let response = result["content"][0]["text"]
478 .as_str()
479 .unwrap_or_default();
480 return Ok(response.to_string());
481 }
482 Err(e) if e.is_timeout() => {
483 warn!("请求超时: {}", e);
484 if !Confirm::with_theme(&dialoguer::theme::ColorfulTheme::default())
485 .with_prompt("请求超时,是否重试?")
486 .default(true)
487 .interact()? {
488 return Err(anyhow::anyhow!("请求超时"));
489 }
490 continue;
491 }
492 Err(e) => return Err(e.into()),
493 }
494 }
495 }
496}
497
498#[async_trait]
499impl AiService for CopilotTranslator {
500 async fn chat(&self, system_prompt: &str, user_content: &str) -> anyhow::Result<String> {
501 debug!("使用 Copilot");
502 let ai_host = "copilot.local";
503 print_progress(&format!("正在请求 {} 进行AI对话", ai_host), None);
504
505 let messages = vec![
506 copilot_client::Message {
507 role: "system".to_string(),
508 content: system_prompt.to_string(),
509 },
510 copilot_client::Message {
511 role: "user".to_string(),
512 content: user_content.to_string(),
513 },
514 ];
515 debug!("发送给 Copilot 的消息:\n{}", serde_json::to_string_pretty(&messages)?);
516 let response = self.client.chat_completion(messages, self.model.clone()).await?;
517 print_progress(&format!("正在请求 {} 进行AI对话", ai_host), Some(100));
518 let result = response.choices.get(0)
519 .map(|choice| choice.message.content.clone())
520 .unwrap_or_default();
521 Ok(result)
522 }
523}
524
525#[async_trait]
526impl AiService for GeminiTranslator {
527 async fn chat(&self, system_prompt: &str, user_content: &str) -> anyhow::Result<String> {
528 debug!("使用 Gemini,API Endpoint: {}", self.endpoint);
529 let client = reqwest::Client::builder()
530 .timeout(std::time::Duration::from_secs(self.timeout_seconds))
531 .build()?;
532
533 let url = format!("{}/models/{}:generateContent?key={}", self.endpoint, self.model, self.api_key);
534 let prompt = format!("{}\n\n{}", system_prompt, user_content);
535 debug!("发送给 Gemini 的内容:\n{}", prompt);
536 let body = serde_json::json!({
537 "contents": [{
538 "parts": [{
539 "text": prompt
540 }]
541 }],
542 "generationConfig": {
543 "maxOutputTokens": self.max_tokens
544 }
545 });
546
547 let ai_host = match url.split('/').nth(2) {
548 Some(host) => host,
549 None => "generativelanguage.googleapis.com",
550 };
551 print_progress(&format!("正在请求 {} 进行AI对话", ai_host), None);
552
553 loop {
554 match client
555 .post(&url)
556 .json(&body)
557 .send()
558 .await
559 {
560 Ok(response) => {
561 print_progress(&format!("正在请求 {} 进行AI对话", ai_host), Some(100));
562 debug!("收到响应: {:#?}", response);
563
564 if !response.status().is_success() {
565 let error_json = response.json::<serde_json::Value>().await?;
566 debug!("响应内容: {}", serde_json::to_string_pretty(&error_json)?);
567 return Err(anyhow::anyhow!("API 调用失败: {}",
568 error_json["error"]["message"].as_str().unwrap_or("未知错误")));
569 }
570
571 let result = response.json::<serde_json::Value>().await?;
572 debug!("响应内容: {}", serde_json::to_string_pretty(&result)?);
573
574 let response = result["candidates"][0]["content"]["parts"][0]["text"]
575 .as_str()
576 .unwrap_or_default();
577 return Ok(response.to_string());
578 }
579 Err(e) if e.is_timeout() => {
580 warn!("请求超时: {}", e);
581 if !Confirm::with_theme(&dialoguer::theme::ColorfulTheme::default())
582 .with_prompt("请求超时,是否重试?")
583 .default(true)
584 .interact()? {
585 return Err(anyhow::anyhow!("请求超时"));
586 }
587 continue;
588 }
589 Err(e) => return Err(e.into()),
590 }
591 }
592 }
593}
594
595#[async_trait]
596impl AiService for GrokTranslator {
597 async fn chat(&self, system_prompt: &str, user_content: &str) -> anyhow::Result<String> {
598 debug!("使用 Grok,API Endpoint: {}", self.endpoint);
599 let client = reqwest::Client::builder()
600 .timeout(std::time::Duration::from_secs(self.timeout_seconds))
601 .build()?;
602
603 let url = format!("{}/chat/completions", self.endpoint);
604 let messages = vec![
605 serde_json::json!({
606 "role": "system",
607 "content": system_prompt
608 }),
609 serde_json::json!({
610 "role": "user",
611 "content": user_content
612 })
613 ];
614 debug!("发送给 Grok 的消息:\n{}", serde_json::to_string_pretty(&messages)?);
615 let body = serde_json::json!({
616 "model": self.model,
617 "messages": messages,
618 "max_tokens": self.max_tokens
619 });
620
621 let ai_host = match url.split('/').nth(2) {
622 Some(host) => host,
623 None => "api.x.ai",
624 };
625 print_progress(&format!("正在请求 {} 进行AI对话", ai_host), None);
626
627 loop {
628 match client
629 .post(&url)
630 .header("Authorization", format!("Bearer {}", self.api_key))
631 .json(&body)
632 .send()
633 .await
634 {
635 Ok(response) => {
636 print_progress(&format!("正在请求 {} 进行AI对话", ai_host), Some(100));
637 debug!("收到响应: {:#?}", response);
638
639 if !response.status().is_success() {
640 let error_json = response.json::<serde_json::Value>().await?;
641 debug!("响应内容: {}", serde_json::to_string_pretty(&error_json)?);
642 return Err(anyhow::anyhow!("API 调用失败: {}",
643 error_json["error"]["message"].as_str().unwrap_or("未知错误")));
644 }
645
646 let result = response.json::<serde_json::Value>().await?;
647 debug!("响应内容: {}", serde_json::to_string_pretty(&result)?);
648
649 let response = result["choices"][0]["message"]["content"]
650 .as_str()
651 .unwrap_or_default();
652 return Ok(response.to_string());
653 }
654 Err(e) if e.is_timeout() => {
655 warn!("请求超时: {}", e);
656 if !Confirm::with_theme(&dialoguer::theme::ColorfulTheme::default())
657 .with_prompt("请求超时,是否重试?")
658 .default(true)
659 .interact()? {
660 return Err(anyhow::anyhow!("请求超时"));
661 }
662 continue;
663 }
664 Err(e) => return Err(e.into()),
665 }
666 }
667 }
668}
669
670#[async_trait]
671impl AiService for QwenTranslator {
672 async fn chat(&self, system_prompt: &str, user_content: &str) -> anyhow::Result<String> {
673 debug!("使用 Qwen,API Endpoint: {}", self.endpoint);
674 let client = reqwest::Client::builder()
675 .timeout(std::time::Duration::from_secs(self.timeout_seconds))
676 .build()?;
677
678 let url = format!("{}/chat/completions", self.endpoint);
679 let messages = vec![
680 serde_json::json!({
681 "role": "system",
682 "content": system_prompt
683 }),
684 serde_json::json!({
685 "role": "user",
686 "content": user_content
687 })
688 ];
689 debug!("发送给 Qwen 的消息:\n{}", serde_json::to_string_pretty(&messages)?);
690 let body = serde_json::json!({
691 "model": self.model,
692 "messages": messages,
693 "temperature": 0.1,
694 "max_tokens": self.max_tokens
695 });
696
697 let ai_host = match url.split('/').nth(2) {
698 Some(host) => host,
699 None => "dashscope.aliyuncs.com",
700 };
701 print_progress(&format!("正在请求 {} 进行AI对话", ai_host), None);
702
703 loop {
704 match client
705 .post(&url)
706 .header("Authorization", format!("Bearer {}", self.api_key))
707 .json(&body)
708 .send()
709 .await
710 {
711 Ok(response) => {
712 print_progress(&format!("正在请求 {} 进行AI对话", ai_host), Some(100));
713 debug!("收到响应: {:#?}", response);
714
715 if !response.status().is_success() {
716 let error_json = response.json::<serde_json::Value>().await?;
717 debug!("响应内容: {}", serde_json::to_string_pretty(&error_json)?);
718 return Err(anyhow::anyhow!("API 调用失败: {}",
719 error_json["error"]["message"].as_str().unwrap_or("未知错误")));
720 }
721
722 let result = response.json::<serde_json::Value>().await?;
723 debug!("响应内容: {}", serde_json::to_string_pretty(&result)?);
724
725 let response = result["choices"][0]["message"]["content"]
726 .as_str()
727 .unwrap_or_default();
728 return Ok(response.to_string());
729 }
730 Err(e) if e.is_timeout() => {
731 warn!("请求超时: {}", e);
732 if !Confirm::with_theme(&dialoguer::theme::ColorfulTheme::default())
733 .with_prompt("请求超时,是否重试?")
734 .default(true)
735 .interact()? {
736 return Err(anyhow::anyhow!("请求超时"));
737 }
738 continue;
739 }
740 Err(e) => return Err(e.into()),
741 }
742 }
743 }
744}
745
746pub async fn create_translator(config: &Config) -> anyhow::Result<Box<dyn Translator>> {
747 info!("创建 {:?} AI服务", config.default_service);
748 let service_config = config.services.iter()
749 .find(|s| s.service == config.default_service)
750 .ok_or_else(|| anyhow::anyhow!("找不到默认服务的配置"))?;
751 create_translator_for_service(service_config).await
752}
753
754pub async fn translate_with_fallback(config: &Config, text: &str, direction: &crate::config::TranslateDirection) -> anyhow::Result<String> {
755 let mut tried_services = Vec::new();
756
757 if std::env::var("GIT_COMMIT_HELPER_NO_TRANSLATE").is_ok() {
759 return Ok(text.trim().to_string());
760 }
761
762 debug!("尝试使用默认服务 {:?}", config.default_service);
763 if let Some(result) = try_translate(&config.default_service, config, text, direction).await {
764 return result;
765 }
766 tried_services.push(config.default_service.clone());
767
768 for service_config in &config.services {
769 if tried_services.contains(&service_config.service) {
770 continue;
771 }
772
773 debug!("尝试使用备选服务 {:?}", service_config.service);
774 if let Some(result) = try_translate(&service_config.service, config, text, direction).await {
775 return result;
776 }
777 tried_services.push(service_config.service.clone());
778 }
779
780 while let Some(service) = select_retry_service(config, &tried_services)? {
781 debug!("用户选择使用 {:?} 重试", service);
782 if let Some(result) = try_translate(&service, config, text, direction).await {
783 return result;
784 }
785 tried_services.push(service);
786 }
787
788 Err(anyhow::anyhow!("所有AI服务均失败"))
789}
790
791async fn try_translate(service: &AIService, config: &Config, text: &str, direction: &crate::config::TranslateDirection) -> Option<anyhow::Result<String>> {
792 let service_config = config.services.iter()
793 .find(|s| s.service == *service)?;
794
795 let translator = create_translator_for_service(service_config).await.ok()?;
796 match translator.translate(text, direction).await {
797 Ok(result) => Some(Ok(result)),
798 Err(e) => {
799 warn!("{:?} 服务翻译失败: {}", service, e);
800 None
801 }
802 }
803}
804
805fn select_retry_service(config: &Config, tried_services: &[AIService]) -> anyhow::Result<Option<AIService>> {
806 let available_services: Vec<_> = config.services.iter()
807 .filter(|s| !tried_services.contains(&s.service))
808 .collect();
809
810 if available_services.is_empty() {
811 return Ok(None);
812 }
813
814 let options: Vec<String> = available_services.iter()
815 .map(|s| format!("{:?}", s.service))
816 .collect();
817
818 println!("\n之前的翻译尝试都失败了,是否要使用其他服务重试?");
819 if !Confirm::with_theme(&dialoguer::theme::ColorfulTheme::default())
820 .default(true)
821 .interact()? {
822 return Ok(None);
823 }
824
825 let selection = Select::with_theme(&dialoguer::theme::ColorfulTheme::default())
826 .with_prompt("请选择要使用的服务")
827 .items(&options)
828 .default(0)
829 .interact()?;
830
831 Ok(Some(available_services[selection].service.clone()))
832}
833
834pub async fn create_translator_for_service(service_config: &AIServiceConfig) -> anyhow::Result<Box<dyn Translator>> {
835 let config = crate::config::Config::load().ok();
837 let timeout = config.as_ref().map(|c| c.timeout_seconds).unwrap_or(20);
838
839 Ok(match service_config.service {
840 AIService::DeepSeek => {
841 let mut translator = DeepSeekTranslator::new(service_config);
842 translator.timeout_seconds = timeout;
843 Box::new(translator)
844 },
845 AIService::OpenAI => {
846 let mut translator = OpenAITranslator::new(service_config);
847 translator.timeout_seconds = timeout;
848 Box::new(translator)
849 },
850 AIService::Claude => {
851 let mut translator = ClaudeTranslator::new(service_config);
852 translator.timeout_seconds = timeout;
853 Box::new(translator)
854 },
855 AIService::Copilot => {
856 let editor_version = "1.0.0".to_string();
857 let client = CopilotClient::new_with_models(service_config.api_key.clone(), editor_version).await?;
858 let model_id = service_config.model.clone().unwrap_or_else(|| "copilot-chat".to_string());
859 Box::new(CopilotTranslator::new(client, model_id))
860 },
861 AIService::Gemini => {
862 let mut translator = GeminiTranslator::new(service_config);
863 translator.timeout_seconds = timeout;
864 Box::new(translator)
865 },
866 AIService::Grok => {
867 let mut translator = GrokTranslator::new(service_config);
868 translator.timeout_seconds = timeout;
869 Box::new(translator)
870 },
871 AIService::Qwen => {
872 let mut translator = QwenTranslator::new(service_config);
873 translator.timeout_seconds = timeout;
874 Box::new(translator)
875 },
876 })
877}