1use anyhow::{Context, Result};
2use dialoguer::{Confirm, Input};
3use directories::ProjectDirs;
4use serde::{Deserialize, Serialize};
5use std::path::PathBuf;
6use std::fs;
7use copilot_client::CopilotClient;
8use copilot_client::get_github_token;
9use log::{debug, info, warn};
10use dialoguer::console::Term;
11use crate::ai_service;
12
13#[derive(Debug, Serialize, Deserialize, Clone)]
14pub struct Config {
15 pub default_service: AIService,
16 pub services: Vec<AIServiceConfig>,
17 #[serde(default = "default_ai_review")]
18 pub ai_review: bool, #[serde(default = "default_timeout")]
20 pub timeout_seconds: u64, #[serde(default = "default_max_tokens")]
22 pub max_tokens: u64, #[serde(default)]
24 pub gerrit: Option<GerritConfig>, #[serde(default = "default_only_chinese")]
26 pub only_chinese: bool, #[serde(default = "default_only_english")]
28 pub only_english: bool, #[serde(default = "default_translate_direction")]
30 pub translate_direction: TranslateDirection, }
32
33fn default_only_chinese() -> bool {
35 false
36}
37
38fn default_only_english() -> bool {
39 false
40}
41
42fn default_translate_direction() -> TranslateDirection {
43 TranslateDirection::ChineseToEnglish
44}
45
46#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
47pub enum TranslateDirection {
48 ChineseToEnglish, EnglishToChinese, }
51
52#[derive(Debug, Serialize, Deserialize, Clone, Default)]
53pub struct GerritConfig {
54 pub username: Option<String>,
55 pub password: Option<String>,
56 pub token: Option<String>,
57}
58
59fn default_ai_review() -> bool {
61 true
62}
63
64fn default_timeout() -> u64 {
66 20
67}
68
69fn default_max_tokens() -> u64 {
71 2048
72}
73
74#[derive(Debug, Serialize, Deserialize, Clone)]
75pub struct AIServiceConfig {
76 pub service: AIService,
77 pub api_key: String,
78 pub api_endpoint: Option<String>,
79 pub model: Option<String>, }
81
82#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
83pub enum AIService {
84 DeepSeek,
85 OpenAI, Claude,
87 Copilot,
88 Gemini, Grok, Qwen, }
92
93impl Config {
94 pub fn new() -> Self {
95 Self {
96 default_service: AIService::OpenAI, services: Vec::new(),
98 ai_review: true, timeout_seconds: default_timeout(),
100 max_tokens: default_max_tokens(),
101 gerrit: None,
102 only_chinese: false, only_english: false, translate_direction: default_translate_direction(), }
106 }
107
108 pub fn load() -> Result<Self> {
109 let config_path = Self::config_path()?;
110 debug!("尝试加载配置文件: {}", config_path.display());
111
112 if !config_path.exists() { warn!("配置文件不存在: {}", config_path.display());
114 return Err(anyhow::anyhow!("配置文件不存在,请先运行 'git-commit-helper config' 进行配置"));
115 }
116
117 let config_str = fs::read_to_string(&config_path)
118 .context("读取配置文件失败")?;
119 let config: Config = serde_json::from_str(&config_str)
120 .context("解析配置文件失败")?;
121
122 info!("已加载配置,使用 {:?} 服务", config.default_service);
123
124 Ok(config)
125 }
126
127 pub async fn interactive_config() -> Result<()> {
128 Box::pin(Self::interactive_config_impl()).await
129 }
130
131 pub async fn setup_gerrit(&mut self) -> Result<()> {
132 println!("\nGerrit 认证配置");
133 println!("选择认证方式:");
134 println!("1) 用户名密码");
135 println!("2) Token");
136 println!("3) 跳过 (不配置)");
137
138 let selection: usize = Input::new()
139 .with_prompt("请选择认证方式")
140 .default(3)
141 .validate_with(|input: &usize| -> Result<(), &str> {
142 if *input >= 1 && *input <= 3 {
143 Ok(())
144 } else {
145 Err("请输入 1-3 之间的数字")
146 }
147 })
148 .interact()?;
149
150 let mut gerrit_config = GerritConfig::default();
151
152 match selection {
153 1 => {
154 let username: String = Input::new()
155 .with_prompt("请输入 Gerrit 用户名")
156 .interact_text()?;
157
158 let password: String = Input::new()
159 .with_prompt("请输入 Gerrit 密码")
160 .interact_text()?;
161
162 gerrit_config.username = Some(username);
163 gerrit_config.password = Some(password);
164 }
165 2 => {
166 let token: String = Input::new()
167 .with_prompt("请输入 Gerrit Token")
168 .interact_text()?;
169
170 gerrit_config.token = Some(token);
171 }
172 _ => {
173 self.gerrit = None;
175 return Ok(());
176 }
177 }
178
179 self.gerrit = Some(gerrit_config);
180 self.save()?;
181
182 println!("✅ Gerrit 认证信息已保存");
183 Ok(())
184 }
185
186 pub async fn interactive_config_impl() -> Result<()> {
187 info!("开始交互式配置...");
188 let default_path = Self::default_config_path()?;
190 println!("\n配置文件存放位置选项:");
191 println!("1) 系统默认位置: {}", default_path.display());
192 println!("2) 自定义路径");
193
194 let selection: usize = Input::new()
195 .with_prompt("请选择配置文件存放位置")
196 .validate_with(|input: &usize| -> Result<(), &str> {
197 if *input >= 1 && *input <= 2 {
198 Ok(())
199 } else {
200 Err("请输入 1-2 之间的数字")
201 }
202 })
203 .interact()?;
204
205 let config_path = if selection == 1 {
206 default_path
207 } else {
208 let custom_path: String = Input::new()
209 .with_prompt("请输入配置文件路径 (相对路径将基于可执行文件所在目录)")
210 .interact_text()?;
211
212 let path = PathBuf::from(&custom_path);
213 if path.is_relative() {
214 let exe_dir = std::env::current_exe()?
215 .parent()
216 .ok_or_else(|| anyhow::anyhow!("无法获取可执行文件目录"))?
217 .to_path_buf();
218 exe_dir.join(path)
219 } else {
220 path
221 }
222 };
223
224 std::env::set_var("GIT_COMMIT_HELPER_CONFIG", config_path.to_string_lossy().to_string());
226
227 let mut services: Vec<AIServiceConfig> = Vec::new();
228
229 loop {
230 println!("\n当前已配置的 AI 服务:");
231 for (i, s) in services.iter().enumerate() {
232 println!("{}. {:?}", i + 1, s.service);
233 }
234
235 if !Confirm::with_theme(&dialoguer::theme::ColorfulTheme::default())
236 .with_prompt("是否继续添加 AI 服务?")
237 .default(services.is_empty())
238 .interact()?
239 {
240 break;
241 }
242
243 println!("\n请选择要添加的 AI 服务:");
244 println!("1) DeepSeek");
245 println!("2) OpenAI");
246 println!("3) Claude");
247 println!("4) Copilot");
248 println!("5) Gemini");
249 println!("6) Grok");
250 println!("7) Qwen");
251
252 let selection = Input::<String>::new()
253 .with_prompt("请输入对应的数字")
254 .report(true)
255 .validate_with(|input: &String| -> Result<(), &str> {
256 match input.parse::<usize>() {
257 Ok(n) if n >= 1 && n <= 7 => Ok(()),
258 _ => Err("请输入 1-7 之间的数字")
259 }
260 })
261 .interact()?
262 .parse::<usize>()?;
263
264 let service = match selection {
265 1 => AIService::DeepSeek,
266 2 => AIService::OpenAI,
267 3 => AIService::Claude,
268 4 => AIService::Copilot,
269 5 => AIService::Gemini,
270 6 => AIService::Grok,
271 7 => AIService::Qwen,
272 _ => unreachable!(),
273 };
274
275 let config = Config::input_service_config(service).await?;
276 services.push(config);
277 }
278
279 if services.is_empty() {
280 return Err(anyhow::anyhow!("至少需要配置一个 AI 服务"));
281 }
282
283 println!("\n请选择默认的 AI 服务:");
284 for (i, s) in services.iter().enumerate() {
285 println!("{}. {:?}", i + 1, s.service);
286 }
287
288 let services_len = services.len();
289 let default_index: usize = Input::new()
290 .with_prompt("请输入对应的数字")
291 .validate_with(|input: &usize| -> Result<(), &str> {
292 if *input >= 1 && *input <= services_len {
293 Ok(())
294 } else {
295 Err("输入的数字超出范围")
296 }
297 })
298 .interact()?;
299
300 let mut config = Config {
301 default_service: services[default_index - 1].service.clone(),
302 services,
303 ai_review: true, timeout_seconds: default_timeout(),
305 max_tokens: default_max_tokens(),
306 gerrit: None,
307 only_chinese: false, only_english: false, translate_direction: default_translate_direction(), };
311
312 if let Some(parent) = config_path.parent() {
314 fs::create_dir_all(parent)?;
315 }
316
317 fs::write(&config_path, serde_json::to_string_pretty(&config)?)?;
319 info!("配置已保存: {}", config_path.display());
320 println!("配置已保存到: {}", config_path.display());
321
322 if Confirm::new()
324 .with_prompt("是否要测试翻译功能?")
325 .default(true)
326 .interact()?
327 {
328 println!("正在测试翻译功能...");
329 let test_config = Config {
331 default_service: config.default_service.clone(),
332 services: vec![config.services[default_index - 1].clone()],
333 ai_review: true,
334 timeout_seconds: config.timeout_seconds,
335 max_tokens: config.max_tokens,
336 gerrit: None,
337 only_chinese: false,
338 only_english: false,
339 translate_direction: default_translate_direction(),
340 };
341 let translator = ai_service::create_translator(&test_config).await?;
342 match translator.translate("这是一个测试消息,用于验证翻译功能是否正常。", &TranslateDirection::ChineseToEnglish).await {
343 Ok(result) => {
344 println!("\n测试结果:");
345 println!("原文: 这是一个测试消息,用于验证翻译功能是否正常。");
346 println!("译文: {}\n", result);
347 println!("测试成功!配置已完成。");
348 },
349 Err(e) => {
350 println!("\n测试失败!错误信息:");
351 println!("{}", e);
352 println!("\n请检查以下内容:");
353 println!("1. API Key 是否正确");
354 println!("2. API Endpoint 是否可访问");
355 println!("3. 网络连接是否正常");
356
357 println!("\n请选择操作:");
358 println!("1. 重新修改配置");
359 println!("2. 强制保存配置");
360 println!("3. 退出");
361
362 let selection: usize = Input::new()
363 .with_prompt("请输入对应的数字")
364 .validate_with(|input: &usize| -> Result<(), &str> {
365 if *input >= 1 && *input <= 3 {
366 Ok(())
367 } else {
368 Err("请输入 1-3 之间的数字")
369 }
370 })
371 .interact()?;
372
373 match selection {
374 1 => {
375 let new_config = Config::input_service_config(config.default_service.clone()).await?;
377 config.services.pop(); config.services.push(new_config); fs::write(&config_path, serde_json::to_string_pretty(&config)?)?;
381 return Box::pin(Config::interactive_config_impl()).await;
383 },
384 2 => {
385 println!("配置已强制保存,但可能无法正常工作。");
386 return Ok(());
387 },
388 _ => return Err(e),
389 }
390 }
391 }
392 }
393
394 Ok(())
395 }
396
397 pub async fn add_service(&mut self, service: AIService) -> Result<()> {
398 Box::pin(self.add_service_impl(service)).await
399 }
400
401 async fn add_service_impl(&mut self, service: AIService) -> Result<()> {
402 let config = match service {
404 AIService::Copilot => {
405 println!("Copilot 服务需要 GitHub 身份验证...");
406
407 match get_github_token() {
409 Ok(token) => {
410 println!("✅ 已成功获取 GitHub 令牌");
411 let editor_version = "1.0.0".to_string();
413 let client = CopilotClient::new_with_models(token.clone(), editor_version).await;
414 match client {
415 Ok(client) => {
416 println!("✅ GitHub Copilot 认证成功!");
417 let models = client.get_models().await?;
419 if !models.is_empty() {
420 println!("\n可用模型:");
421 for (i, model) in models.iter().enumerate() {
422 println!(" {}. {} ({})", i+1, model.name, model.id);
423 }
424
425 let model_count = models.len();
427 let selection = Input::<String>::new()
428 .with_prompt("请选择要使用的模型编号 (留空使用默认)")
429 .allow_empty(true)
430 .validate_with(|input: &String| -> Result<(), &str> {
431 if input.is_empty() {
432 return Ok(());
433 }
434 match input.parse::<usize>() {
435 Ok(n) if n >= 1 && n <= model_count => Ok(()),
436 _ => Err("请输入有效的模型编号或留空")
437 }
438 })
439 .interact()?;
440
441 let model_id = if selection.is_empty() {
443 "copilot-chat".to_string()
444 } else {
445 let idx = selection.parse::<usize>().unwrap() - 1;
446 models[idx].id.clone()
447 };
448
449 AIServiceConfig {
451 service: AIService::Copilot,
452 api_key: token,
453 api_endpoint: None,
454 model: Some(model_id),
455 }
456 } else {
457 AIServiceConfig {
459 service: AIService::Copilot,
460 api_key: token,
461 api_endpoint: None,
462 model: Some("copilot-chat".to_string()),
463 }
464 }
465 },
466 Err(e) => {
467 println!("❌ Copilot API 连接失败: {}", e);
468 println!("请确保您已订阅 GitHub Copilot 服务并拥有有效权限。");
469 return Err(anyhow::anyhow!("Copilot 认证失败"));
470 }
471 }
472 },
473 Err(e) => {
474 println!("❌ 无法获取 GitHub 令牌: {}", e);
475 println!("\n请按照以下步骤获取 GitHub 令牌:");
476 println!("可使用QtCreator中的Copilot插件获取到copilot的token,或直接使用copilot.nvim在nvim中获取token:https://github.com/github/copilot.vim");
477 println!("\n按回车键继续...");
478 Term::stdout().read_line()?;
479 return Err(anyhow::anyhow!("无法获取 GitHub 令牌"));
480 }
481 }
482 },
483 _ => Config::input_service_config_with_default(&AIServiceConfig {
484 service: service.clone(),
485 api_key: String::new(),
486 api_endpoint: None,
487 model: None,
488 }).await?,
489 };
490
491 if self.services.is_empty() {
493 self.default_service = config.service.clone();
494 }
495 self.services.push(config.clone());
496
497 if Confirm::new()
499 .with_prompt("是否要测试该服务?")
500 .default(true)
501 .interact()?
502 {
503 println!("正在测试 {:?} 服务...", config.service);
504 let test_config = Config {
506 default_service: config.service.clone(),
507 services: vec![config.clone()],
508 ai_review: true,
509 timeout_seconds: self.timeout_seconds,
510 max_tokens: self.max_tokens,
511 gerrit: None,
512 only_chinese: false,
513 only_english: false,
514 translate_direction: default_translate_direction(),
515 };
516 let translator = ai_service::create_translator(&test_config).await?;
517 let text = "这是一个测试消息,用于验证翻译功能是否正常。";
518 debug!("开始发送翻译请求");
519 match translator.translate(text, &TranslateDirection::ChineseToEnglish).await {
520 Ok(result) => {
521 debug!("收到翻译响应");
522 println!("\n测试结果:");
523 println!("原文: {}", text);
524 if result.is_empty() {
525 println!("警告: 收到空的翻译结果!");
526 }
527 println!("译文: {}", result);
528 println!("\n✅ 测试成功!服务已添加并可正常使用。");
529 self.save()?;
530 },
531 Err(e) => {
532 println!("\n❌ 测试失败!错误信息:");
533 println!("{}", e);
534 println!("\n请检查:");
535 println!("1. API Key 是否正确");
536 println!("2. API Endpoint 是否可访问");
537 println!("3. 网络连接是否正常");
538 println!("4. 查看日志获取详细信息(设置 RUST_LOG=debug)");
539
540 println!("\n请选择操作:");
541 println!("1. 重新配置服务");
542 println!("2. 强制保存配置");
543 println!("3. 放弃添加");
544
545 let selection: usize = Input::new()
546 .with_prompt("请输入对应的数字")
547 .validate_with(|input: &usize| -> Result<(), &str> {
548 if *input >= 1 && *input <= 3 {
549 Ok(())
550 } else {
551 Err("请输入 1-3 之间的数字")
552 }
553 })
554 .interact()?;
555
556 match selection {
557 1 => {
558 self.services.pop();
560 return Box::pin(self.add_service_impl(service)).await;
562 },
563 2 => {
564 println!("配置已强制保存,但服务可能无法正常工作。");
565 self.save()?;
566 },
567 _ => {
568 self.services.pop(); return Err(anyhow::anyhow!("已取消添加服务"));
570 }
571 }
572 }
573 }
574 } else {
575 self.save()?;
576 println!("✅ {:?} 服务已添加(未测试)", service);
577 }
578
579 info!("AI 服务已添加");
580 Ok(())
581 }
582
583 pub async fn edit_service(&mut self) -> Result<()> {
584 if self.services.is_empty() {
585 return Err(anyhow::anyhow!("没有可编辑的 AI 服务"));
586 }
587
588 println!("\n已配置的 AI 服务:");
589 for (i, s) in self.services.iter().enumerate() {
590 println!("{}. {:?}", i + 1, s.service);
591 }
592
593 let selection = Input::<String>::with_theme(&dialoguer::theme::ColorfulTheme::default())
594 .with_prompt("请输入要编辑的服务编号")
595 .report(true)
596 .interact()?
597 .parse::<usize>()?;
598
599 if selection < 1 || selection > self.services.len() {
601 return Err(anyhow::anyhow!("无效的服务编号"));
602 }
603
604 let old_config = &self.services[selection - 1];
605 let new_config = Config::input_service_config_with_default(old_config).await?;
606
607 self.services[selection - 1] = new_config;
609 self.save()?;
610
611 println!("✅ 服务配置已更新。请稍后使用 'git-commit-helper test' 命令测试该服务。");
612 info!("AI 服务已修改(未测试)");
613
614 Ok(())
615 }
616
617 pub async fn remove_service(&mut self) -> Result<()> {
618 if self.services.is_empty() {
619 return Err(anyhow::anyhow!("没有可删除的 AI 服务"));
620 }
621
622 println!("\n已配置的 AI 服务:");
623 for (i, s) in self.services.iter().enumerate() {
624 println!("{}. {:?}", i + 1, s.service);
625 }
626
627 let services_len = self.services.len();
628 let selection = Input::<String>::new()
629 .with_prompt("请输入要删除的服务编号")
630 .report(true)
631 .validate_with(|input: &String| -> Result<(), &str> {
632 match input.parse::<usize>() {
633 Ok(n) if n >= 1 && n <= services_len => Ok(()),
634 _ => Err("输入的数字超出范围")
635 }
636 })
637 .interact()?
638 .parse::<usize>()?;
639
640 let removed = self.services.remove(selection - 1);
641
642 if removed.service == self.default_service && !self.services.is_empty() {
643 self.default_service = self.services[0].service.clone();
644 }
645
646 self.save()?;
647 info!("AI 服务删除成功");
648 Ok(())
649 }
650
651 pub async fn set_default_service(&mut self) -> Result<()> {
652 if self.services.is_empty() {
653 return Err(anyhow::anyhow!("没有可选择的 AI 服务"));
654 }
655
656 println!("\n已配置的 AI 服务:");
657 for (i, s) in self.services.iter().enumerate() {
658 println!("{}. {:?}", i + 1, s.service);
659 }
660
661 let services_len = self.services.len();
662 let selection = Input::<String>::new()
663 .with_prompt("请输入要设为默认的服务编号")
664 .report(true)
665 .validate_with(|input: &String| -> Result<(), &str> {
666 match input.parse::<usize>() {
667 Ok(n) if n >= 1 && n <= services_len => Ok(()),
668 _ => Err("输入的数字超出范围")
669 }
670 })
671 .interact()?
672 .parse::<usize>()?;
673
674 self.default_service = self.services[selection - 1].service.clone();
675 self.save()?;
676 info!("默认 AI 服务设置成功");
677 Ok(())
678 }
679
680 pub async fn input_service_config(service: AIService) -> Result<AIServiceConfig> {
681 Config::input_service_config_with_default(&AIServiceConfig {
683 service,
684 api_key: String::new(),
685 api_endpoint: None,
686 model: None,
687 }).await
688 }
689
690 pub async fn input_service_config_with_default(default: &AIServiceConfig) -> Result<AIServiceConfig> {
691 if default.service == AIService::Copilot {
693 if !default.api_key.is_empty() {
695 let editor_version = "1.0.0".to_string();
697 match CopilotClient::new_with_models(default.api_key.clone(), editor_version).await {
698 Ok(client) => {
699 let models = client.get_models().await?;
700 if !models.is_empty() {
701 println!("\n可用模型:");
702 for (i, model) in models.iter().enumerate() {
703 println!(" {}. {} ({})", i+1, model.name, model.id);
704 }
705
706 let current_model = default.model.as_deref().unwrap_or("copilot-chat");
708 println!("\n当前选择的模型: {}", current_model);
709
710 let model_count = models.len();
712 let selection = Input::<String>::new()
713 .with_prompt("请选择要使用的模型编号 (留空保持当前选择)")
714 .allow_empty(true)
715 .validate_with(|input: &String| -> Result<(), &str> {
716 if input.is_empty() {
717 return Ok(());
718 }
719 match input.parse::<usize>() {
720 Ok(n) if n >= 1 && n <= model_count => Ok(()),
721 _ => Err("请输入有效的模型编号或留空")
722 }
723 })
724 .interact()?;
725
726 let model_id = if selection.is_empty() {
728 default.model.clone().unwrap_or_else(|| "copilot-chat".to_string())
729 } else {
730 let idx = selection.parse::<usize>().unwrap() - 1;
731 models[idx].id.clone()
732 };
733
734 return Ok(AIServiceConfig {
735 service: default.service.clone(),
736 api_key: default.api_key.clone(),
737 api_endpoint: None,
738 model: Some(model_id),
739 });
740 }
741 },
742 Err(e) => {
743 println!("⚠️ 无法获取模型列表: {}", e);
744 println!("将使用之前配置的模型或默认模型。");
745 }
746 }
747
748 let model: String = Input::new()
749 .with_prompt("请输入模型名称 (可选,直接回车使用默认值) [copilot-chat]")
750 .with_initial_text(default.model.as_deref().unwrap_or("copilot-chat"))
751 .allow_empty(true)
752 .interact_text()?;
753
754 return Ok(AIServiceConfig {
755 service: default.service.clone(),
756 api_key: default.api_key.clone(), api_endpoint: None,
758 model: if model.is_empty() { Some("copilot-chat".to_string()) } else { Some(model) },
759 });
760 } else {
761 println!("Copilot 服务需要 GitHub 身份验证...");
763
764 match get_github_token() {
766 Ok(token) => {
767 println!("✅ 已成功获取 GitHub 令牌");
768 let editor_version = "1.0.0".to_string();
770 let client = CopilotClient::new_with_models(token.clone(), editor_version).await;
771 match client {
772 Ok(client) => {
773 println!("✅ GitHub Copilot 认证成功!");
774 let models = client.get_models().await?;
776 if !models.is_empty() {
777 println!("\n可用模型:");
778 for (i, model) in models.iter().enumerate() {
779 println!(" {}. {} ({})", i+1, model.name, model.id);
780 }
781
782 let model_count = models.len();
784 let selection = Input::<String>::new()
785 .with_prompt("请选择要使用的模型编号 (留空使用默认)")
786 .allow_empty(true)
787 .validate_with(|input: &String| -> Result<(), &str> {
788 if input.is_empty() {
789 return Ok(());
790 }
791 match input.parse::<usize>() {
792 Ok(n) if n >= 1 && n <= model_count => Ok(()),
793 _ => Err("请输入有效的模型编号或留空")
794 }
795 })
796 .interact()?;
797
798 let model_id = if selection.is_empty() {
800 "copilot-chat".to_string()
801 } else {
802 let idx = selection.parse::<usize>().unwrap() - 1;
803 models[idx].id.clone()
804 };
805
806 return Ok(AIServiceConfig {
808 service: AIService::Copilot,
809 api_key: token,
810 api_endpoint: None,
811 model: Some(model_id),
812 });
813 } else {
814 return Ok(AIServiceConfig {
816 service: AIService::Copilot,
817 api_key: token,
818 api_endpoint: None,
819 model: Some("copilot-chat".to_string()),
820 });
821 }
822 },
823 Err(e) => {
824 println!("❌ Copilot API 连接失败: {}", e);
825 println!("请确保您已订阅 GitHub Copilot 服务并拥有有效权限。");
826 return Err(anyhow::anyhow!("Copilot 认证失败"));
827 }
828 }
829 },
830 Err(e) => {
831 println!("❌ 无法获取 GitHub 令牌: {}", e);
832 println!("\n请按照以下步骤获取 GitHub 令牌:");
833 println!("可使用QtCreator中的Copilot插件获取到copilot的token,或直接使用copilot.nvim在nvim中获取token:https://github.com/github/copilot.vim");
834 println!("\n按回车键继续...");
835 Term::stdout().read_line()?;
836 return Err(anyhow::anyhow!("无法获取 GitHub 令牌"));
837 }
838 }
839 }
840 }
841
842 let api_key: String = Input::new()
844 .with_prompt("请输入 API Key")
845 .with_initial_text(&default.api_key)
846 .interact_text()?;
847
848 let default_endpoint = match default.service {
849 AIService::DeepSeek => "https://api.deepseek.com/v1",
850 AIService::OpenAI => "https://api.openai.com/v1",
851 AIService::Claude => "https://api.anthropic.com/v1",
852 AIService::Copilot => "", AIService::Gemini => "https://generativelanguage.googleapis.com/v1beta",
854 AIService::Grok => "https://api.x.ai/v1",
855 AIService::Qwen => "https://dashscope.aliyuncs.com/compatible-mode/v1",
856 };
857 let api_endpoint: String = Input::new()
858 .with_prompt(format!("请输入 API Endpoint (可选,直接回车使用默认值) [{}]", default_endpoint))
859 .with_initial_text(default.api_endpoint.as_deref().unwrap_or(""))
860 .allow_empty(true)
861 .interact_text()?;
862
863 let default_model_name = match default.service {
864 AIService::DeepSeek => "deepseek-chat",
865 AIService::OpenAI => "gpt-3.5-turbo",
866 AIService::Claude => "claude-3-sonnet-20240229",
867 AIService::Copilot => "copilot-chat",
868 AIService::Gemini => "gemini-2.0-flash",
869 AIService::Grok => "grok-3-latest",
870 AIService::Qwen => "qwen-plus",
871 };
872 let model: String = Input::new()
873 .with_prompt(format!("请输入模型名称 (可选,直接回车使用默认值) [{}]", default_model_name))
874 .with_initial_text(default.model.as_deref().unwrap_or(""))
875 .allow_empty(true)
876 .interact_text()?;
877
878 Ok(AIServiceConfig {
879 service: default.service.clone(),
880 api_key,
881 api_endpoint: if api_endpoint.is_empty() { None } else { Some(api_endpoint) },
882 model: if model.is_empty() { None } else { Some(model) },
883 })
884 }
885
886 pub fn get_default_service(&self) -> Result<&AIServiceConfig> {
887 if self.services.is_empty() {
888 return Err(anyhow::anyhow!("没有配置任何 AI 服务"));
889 }
890
891 if let Some(service) = self.services.iter().find(|s| s.service == self.default_service) {
893 return Ok(service);
894 }
895
896 Ok(&self.services[0])
898 }
899
900 pub fn save(&self) -> Result<()> {
901 let config_path = Self::config_path()?;
902 if let Some(parent) = config_path.parent() {
903 fs::create_dir_all(parent)?;
904 }
905 fs::write(&config_path, serde_json::to_string_pretty(&self)?)?;
906 Ok(())
907 }
908
909 pub fn config_path() -> Result<PathBuf> {
910 if let Ok(path) = std::env::var("GIT_COMMIT_HELPER_CONFIG") {
911 return Ok(PathBuf::from(path));
912 }
913 Self::default_config_path()
914 }
915
916 fn default_config_path() -> Result<PathBuf> {
917 let proj_dirs = ProjectDirs::from("com", "githelper", "git-commit-helper")
918 .context("无法确定配置文件路径")?;
919 Ok(proj_dirs.config_dir().join("config.json"))
920 }
921}