feat(keyring): 集成系统密钥环安全存储 API key
This commit is contained in:
@@ -204,12 +204,11 @@ impl ChangelogCommand {
|
||||
messages: &Messages,
|
||||
) -> Result<String> {
|
||||
let manager = ConfigManager::new()?;
|
||||
let config = manager.config();
|
||||
let language = manager.get_language().unwrap_or(Language::English);
|
||||
|
||||
println!("{}", messages.ai_generating_changelog());
|
||||
|
||||
let generator = ContentGenerator::new(&config.llm).await?;
|
||||
let generator = ContentGenerator::new(&manager).await?;
|
||||
generator.generate_changelog_entry(version, commits, language).await
|
||||
}
|
||||
|
||||
|
||||
@@ -257,22 +257,17 @@ impl CommitCommand {
|
||||
|
||||
async fn generate_commit(&self, repo: &GitRepo, format: CommitFormat, messages: &Messages) -> Result<String> {
|
||||
let manager = ConfigManager::new()?;
|
||||
let config = manager.config();
|
||||
|
||||
// Check if LLM is configured
|
||||
let generator = ContentGenerator::new(&config.llm).await
|
||||
let generator = ContentGenerator::new(&manager).await
|
||||
.context("Failed to initialize LLM. Use --manual for manual commit.")?;
|
||||
|
||||
println!("{}", messages.ai_analyzing());
|
||||
|
||||
let language_str = &config.language.output_language;
|
||||
let language = Language::from_str(language_str).unwrap_or(Language::English);
|
||||
let language = manager.get_language().unwrap_or(Language::English);
|
||||
|
||||
let generated = if self.yes {
|
||||
// Non-interactive mode: generate directly
|
||||
generator.generate_commit_from_repo(repo, format, language).await?
|
||||
} else {
|
||||
// Interactive mode: allow user to review and regenerate
|
||||
generator.generate_commit_interactive(repo, format, language).await?
|
||||
};
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,7 @@ use crate::config::{GitProfile, Language};
|
||||
use crate::config::manager::ConfigManager;
|
||||
use crate::config::profile::{GpgConfig, SshConfig};
|
||||
use crate::i18n::Messages;
|
||||
use crate::utils::keyring::{get_supported_providers, get_default_model, provider_needs_api_key};
|
||||
use crate::utils::validators::validate_email;
|
||||
|
||||
/// Initialize quicommit configuration
|
||||
@@ -31,7 +32,6 @@ impl InitCommand {
|
||||
crate::config::AppConfig::default_path().unwrap()
|
||||
});
|
||||
|
||||
// Check if config already exists
|
||||
if config_path.exists() && !self.reset {
|
||||
if !self.yes {
|
||||
let overwrite = Confirm::new()
|
||||
@@ -49,13 +49,11 @@ impl InitCommand {
|
||||
}
|
||||
}
|
||||
|
||||
// Create parent directory if needed
|
||||
if let Some(parent) = config_path.parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to create config directory: {}", e))?;
|
||||
}
|
||||
|
||||
// Create new config manager with fresh config
|
||||
let mut manager = ConfigManager::with_path_fresh(&config_path)?;
|
||||
|
||||
if self.yes {
|
||||
@@ -66,7 +64,6 @@ impl InitCommand {
|
||||
|
||||
manager.save()?;
|
||||
|
||||
// Get configured language for final messages
|
||||
let language = manager.get_language().unwrap_or(Language::English);
|
||||
let messages = Messages::new(language);
|
||||
|
||||
@@ -81,7 +78,6 @@ impl InitCommand {
|
||||
}
|
||||
|
||||
async fn quick_setup(&self, manager: &mut ConfigManager) -> Result<()> {
|
||||
// Try to get git user info
|
||||
let git_config = git2::Config::open_default()?;
|
||||
|
||||
let user_name = git_config.get_string("user.name").unwrap_or_else(|_| "User".to_string());
|
||||
@@ -96,7 +92,6 @@ impl InitCommand {
|
||||
manager.add_profile("default".to_string(), profile)?;
|
||||
manager.set_default_profile(Some("default".to_string()))?;
|
||||
|
||||
// Set default LLM to Ollama
|
||||
manager.set_llm_provider("ollama".to_string());
|
||||
|
||||
Ok(())
|
||||
@@ -106,7 +101,6 @@ impl InitCommand {
|
||||
let messages = Messages::new(Language::English);
|
||||
println!("\n{}", messages.setup_profile().bold());
|
||||
|
||||
// Language selection
|
||||
println!("\n{}", messages.select_output_language().bold());
|
||||
let languages = vec![
|
||||
Language::English,
|
||||
@@ -126,16 +120,13 @@ impl InitCommand {
|
||||
let selected_language = languages[language_idx];
|
||||
manager.set_output_language(selected_language.to_code().to_string());
|
||||
|
||||
// Update messages to selected language
|
||||
let messages = Messages::new(selected_language);
|
||||
|
||||
// Profile name
|
||||
let profile_name: String = Input::new()
|
||||
.with_prompt(messages.profile_name())
|
||||
.default("personal".to_string())
|
||||
.interact_text()?;
|
||||
|
||||
// User info
|
||||
let git_config = git2::Config::open_default().ok();
|
||||
|
||||
let default_name = git_config.as_ref()
|
||||
@@ -177,7 +168,6 @@ impl InitCommand {
|
||||
None
|
||||
};
|
||||
|
||||
// SSH configuration
|
||||
let setup_ssh = Confirm::new()
|
||||
.with_prompt(messages.configure_ssh())
|
||||
.default(false)
|
||||
@@ -189,7 +179,6 @@ impl InitCommand {
|
||||
None
|
||||
};
|
||||
|
||||
// GPG configuration
|
||||
let setup_gpg = Confirm::new()
|
||||
.with_prompt(messages.configure_gpg())
|
||||
.default(false)
|
||||
@@ -201,7 +190,6 @@ impl InitCommand {
|
||||
None
|
||||
};
|
||||
|
||||
// Create profile
|
||||
let mut profile = GitProfile::new(
|
||||
profile_name.clone(),
|
||||
user_name,
|
||||
@@ -220,9 +208,9 @@ impl InitCommand {
|
||||
manager.add_profile(profile_name.clone(), profile)?;
|
||||
manager.set_default_profile(Some(profile_name))?;
|
||||
|
||||
// LLM provider selection
|
||||
println!("\n{}", messages.select_llm_provider().bold());
|
||||
let providers = vec![
|
||||
|
||||
let provider_display_names = vec![
|
||||
"Ollama (local)",
|
||||
"OpenAI",
|
||||
"Anthropic Claude",
|
||||
@@ -230,49 +218,90 @@ impl InitCommand {
|
||||
"DeepSeek",
|
||||
"OpenRouter"
|
||||
];
|
||||
|
||||
let provider_idx = Select::new()
|
||||
.items(&providers)
|
||||
.items(&provider_display_names)
|
||||
.default(0)
|
||||
.interact()?;
|
||||
|
||||
let provider = match provider_idx {
|
||||
0 => "ollama",
|
||||
1 => "openai",
|
||||
2 => "anthropic",
|
||||
3 => "kimi",
|
||||
4 => "deepseek",
|
||||
5 => "openrouter",
|
||||
_ => "ollama",
|
||||
let providers = get_supported_providers();
|
||||
let provider = providers[provider_idx].to_string();
|
||||
|
||||
let keyring = manager.keyring();
|
||||
let keyring_available = keyring.is_available();
|
||||
|
||||
if !keyring_available {
|
||||
println!("\n{}", "⚠ Keyring is not available on this system.".yellow());
|
||||
println!("{}", keyring.get_status_message().yellow());
|
||||
}
|
||||
|
||||
let api_key = if provider_needs_api_key(&provider) {
|
||||
let env_key = std::env::var("QUICOMMIT_API_KEY")
|
||||
.or_else(|_| std::env::var(format!("QUICOMMIT_{}_API_KEY", provider.to_uppercase())))
|
||||
.ok();
|
||||
|
||||
if let Some(key) = env_key {
|
||||
println!("\n{} {}", "✓".green(), "Found API key in environment variable.".green());
|
||||
None
|
||||
} else if keyring_available {
|
||||
let prompt = match provider.as_str() {
|
||||
"openai" => messages.openai_api_key(),
|
||||
"anthropic" => messages.anthropic_api_key(),
|
||||
"kimi" => messages.kimi_api_key(),
|
||||
"deepseek" => messages.deepseek_api_key(),
|
||||
"openrouter" => messages.openrouter_api_key(),
|
||||
_ => "API Key",
|
||||
};
|
||||
|
||||
let key: String = Input::new()
|
||||
.with_prompt(prompt)
|
||||
.interact_text()?;
|
||||
Some(key)
|
||||
} else {
|
||||
println!("\n{}", "Please set the QUICOMMIT_API_KEY environment variable.".yellow());
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
manager.set_llm_provider(provider.to_string());
|
||||
let default_model = get_default_model(&provider);
|
||||
let model: String = Input::new()
|
||||
.with_prompt("Model name")
|
||||
.default(default_model.to_string())
|
||||
.interact_text()?;
|
||||
|
||||
// Configure API key if needed
|
||||
if provider == "openai" {
|
||||
let api_key: String = Input::new()
|
||||
.with_prompt(messages.openai_api_key())
|
||||
let base_url: Option<String> = if provider == "ollama" {
|
||||
let url: String = Input::new()
|
||||
.with_prompt("Ollama server URL")
|
||||
.default("http://localhost:11434".to_string())
|
||||
.interact_text()?;
|
||||
manager.set_openai_api_key(api_key);
|
||||
} else if provider == "anthropic" {
|
||||
let api_key: String = Input::new()
|
||||
.with_prompt(messages.anthropic_api_key())
|
||||
.interact_text()?;
|
||||
manager.set_anthropic_api_key(api_key);
|
||||
} else if provider == "kimi" {
|
||||
let api_key: String = Input::new()
|
||||
.with_prompt(messages.kimi_api_key())
|
||||
.interact_text()?;
|
||||
manager.set_kimi_api_key(api_key);
|
||||
} else if provider == "deepseek" {
|
||||
let api_key: String = Input::new()
|
||||
.with_prompt(messages.deepseek_api_key())
|
||||
.interact_text()?;
|
||||
manager.set_deepseek_api_key(api_key);
|
||||
} else if provider == "openrouter" {
|
||||
let api_key: String = Input::new()
|
||||
.with_prompt(messages.openrouter_api_key())
|
||||
.interact_text()?;
|
||||
manager.set_openrouter_api_key(api_key);
|
||||
Some(url)
|
||||
} else {
|
||||
let use_custom_url = Confirm::new()
|
||||
.with_prompt("Use custom API base URL?")
|
||||
.default(false)
|
||||
.interact()?;
|
||||
|
||||
if use_custom_url {
|
||||
let url: String = Input::new()
|
||||
.with_prompt("Base URL")
|
||||
.interact_text()?;
|
||||
Some(url)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
manager.set_llm_provider(provider.clone());
|
||||
manager.set_llm_model(model);
|
||||
manager.set_llm_base_url(base_url);
|
||||
|
||||
if let Some(key) = api_key {
|
||||
if provider_needs_api_key(&provider) {
|
||||
manager.set_api_key(&key)?;
|
||||
println!("\n{} {}", "✓".green(), "API key stored securely in system keyring.".green());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -270,10 +270,8 @@ impl TagCommand {
|
||||
|
||||
async fn generate_tag_message(&self, repo: &GitRepo, version: &str, messages: &Messages) -> Result<String> {
|
||||
let manager = ConfigManager::new()?;
|
||||
let config = manager.config();
|
||||
let language = manager.get_language().unwrap_or(Language::English);
|
||||
|
||||
// Get commits since last tag
|
||||
let tags = repo.get_tags()?;
|
||||
let commits = if let Some(latest_tag) = tags.first() {
|
||||
repo.get_commits_between(&latest_tag.name, "HEAD")?
|
||||
@@ -287,7 +285,7 @@ impl TagCommand {
|
||||
|
||||
println!("{}", messages.ai_generating_tag(commits.len()));
|
||||
|
||||
let generator = ContentGenerator::new(&config.llm).await?;
|
||||
let generator = ContentGenerator::new(&manager).await?;
|
||||
generator.generate_tag_message(version, &commits, language).await
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user