feat:Add 3 new LLM providers and optimize the readme.

This commit is contained in:
2026-01-30 16:47:19 +08:00
parent f610c0af8b
commit 2a57946421
13 changed files with 1483 additions and 377 deletions

View File

@@ -6,6 +6,20 @@ use dialoguer::{Confirm, Input, Select};
use crate::config::manager::ConfigManager;
use crate::config::{CommitFormat, LlmConfig};
/// Mask API key with asterisks for security
fn mask_api_key(key: Option<&str>) -> String {
match key {
Some(k) => {
if k.len() <= 8 {
"*".repeat(k.len())
} else {
format!("{}***{}", &k[..4], &k[k.len()-4..])
}
}
None => "✗ not set".red().to_string(),
}
}
/// Manage configuration settings
#[derive(Parser)]
pub struct ConfigCommand {
@@ -18,6 +32,9 @@ enum ConfigSubcommand {
/// Show current configuration
Show,
/// List all configuration information (with masked API keys)
List,
/// Edit configuration file
Edit,
@@ -54,6 +71,24 @@ enum ConfigSubcommand {
key: String,
},
/// Set Kimi API key
SetKimiKey {
/// API key
key: String,
},
/// Set DeepSeek API key
SetDeepSeekKey {
/// API key
key: String,
},
/// Set OpenRouter API key
SetOpenRouterKey {
/// API key
key: String,
},
/// Configure Ollama settings
SetOllama {
/// Ollama server URL
@@ -64,6 +99,36 @@ enum ConfigSubcommand {
model: Option<String>,
},
/// Configure Kimi settings
SetKimi {
/// API base URL (for custom endpoints)
#[arg(short, long)]
base_url: Option<String>,
/// Model name
#[arg(short, long)]
model: Option<String>,
},
/// Configure DeepSeek settings
SetDeepSeek {
/// API base URL (for custom endpoints)
#[arg(short, long)]
base_url: Option<String>,
/// Model name
#[arg(short, long)]
model: Option<String>,
},
/// Configure OpenRouter settings
SetOpenRouter {
/// API base URL (for custom endpoints)
#[arg(short, long)]
base_url: Option<String>,
/// Model name
#[arg(short, long)]
model: Option<String>,
},
/// Set commit format
SetCommitFormat {
/// Format (conventional, commitlint)
@@ -114,13 +179,20 @@ impl ConfigCommand {
pub async fn execute(&self) -> Result<()> {
match &self.command {
Some(ConfigSubcommand::Show) => self.show_config().await,
Some(ConfigSubcommand::List) => self.list_config().await,
Some(ConfigSubcommand::Edit) => self.edit_config().await,
Some(ConfigSubcommand::Set { key, value }) => self.set_value(key, value).await,
Some(ConfigSubcommand::Get { key }) => self.get_value(key).await,
Some(ConfigSubcommand::SetLlm { provider }) => self.set_llm(provider.as_deref()).await,
Some(ConfigSubcommand::SetOpenAiKey { key }) => self.set_openai_key(key).await,
Some(ConfigSubcommand::SetAnthropicKey { key }) => self.set_anthropic_key(key).await,
Some(ConfigSubcommand::SetKimiKey { key }) => self.set_kimi_key(key).await,
Some(ConfigSubcommand::SetDeepSeekKey { key }) => self.set_deepseek_key(key).await,
Some(ConfigSubcommand::SetOpenRouterKey { key }) => self.set_openrouter_key(key).await,
Some(ConfigSubcommand::SetOllama { url, model }) => self.set_ollama(url.as_deref(), model.as_deref()).await,
Some(ConfigSubcommand::SetKimi { base_url, model }) => self.set_kimi(base_url.as_deref(), model.as_deref()).await,
Some(ConfigSubcommand::SetDeepSeek { base_url, model }) => self.set_deepseek(base_url.as_deref(), model.as_deref()).await,
Some(ConfigSubcommand::SetOpenRouter { base_url, model }) => self.set_openrouter(base_url.as_deref(), model.as_deref()).await,
Some(ConfigSubcommand::SetCommitFormat { format }) => self.set_commit_format(format).await,
Some(ConfigSubcommand::SetVersionPrefix { prefix }) => self.set_version_prefix(prefix).await,
Some(ConfigSubcommand::SetChangelogPath { path }) => self.set_changelog_path(path).await,
@@ -137,7 +209,7 @@ impl ConfigCommand {
let manager = ConfigManager::new()?;
let config = manager.config();
println!("{}", "\nQuicCommit Configuration".bold());
println!("{}", "\nQuiCommit Configuration".bold());
println!("{}", "".repeat(60));
println!("\n{}", "General:".bold());
@@ -159,13 +231,27 @@ impl ConfigCommand {
}
"openai" => {
println!(" Model: {}", config.llm.openai.model.cyan());
println!(" API key: {}",
if config.llm.openai.api_key.is_some() { "✓ set".green() } else { "✗ not set".red() });
println!(" Base URL: {}", config.llm.openai.base_url);
println!(" API key: {}", mask_api_key(config.llm.openai.api_key.as_deref()));
}
"anthropic" => {
println!(" Model: {}", config.llm.anthropic.model.cyan());
println!(" API key: {}",
if config.llm.anthropic.api_key.is_some() { "✓ set".green() } else { "✗ not set".red() });
println!(" API key: {}", mask_api_key(config.llm.anthropic.api_key.as_deref()));
}
"kimi" => {
println!(" Model: {}", config.llm.kimi.model.cyan());
println!(" Base URL: {}", config.llm.kimi.base_url);
println!(" API key: {}", mask_api_key(config.llm.kimi.api_key.as_deref()));
}
"deepseek" => {
println!(" Model: {}", config.llm.deepseek.model.cyan());
println!(" Base URL: {}", config.llm.deepseek.base_url);
println!(" API key: {}", mask_api_key(config.llm.deepseek.api_key.as_deref()));
}
"openrouter" => {
println!(" Model: {}", config.llm.openrouter.model.cyan());
println!(" Base URL: {}", config.llm.openrouter.base_url);
println!(" API key: {}", mask_api_key(config.llm.openrouter.api_key.as_deref()));
}
_ => {}
}
@@ -192,6 +278,105 @@ impl ConfigCommand {
Ok(())
}
/// List all configuration information with masked API keys
async fn list_config(&self) -> Result<()> {
let manager = ConfigManager::new()?;
let config = manager.config();
println!("{}", "\nQuiCommit Configuration".bold());
println!("{}", "".repeat(80));
println!("\n{}", "📁 General Configuration:".bold().blue());
println!(" Config file: {}", manager.path().display());
println!(" Default profile: {}",
config.default_profile.as_deref().unwrap_or("(none)").cyan());
println!(" Profiles: {} profile(s)", config.profiles.len());
println!(" Repository mappings: {} mapping(s)", config.repo_profiles.len());
println!("\n{}", "🤖 LLM Configuration:".bold().blue());
println!(" Provider: {}", config.llm.provider.cyan());
println!(" Max tokens: {}", config.llm.max_tokens);
println!(" Temperature: {}", config.llm.temperature);
println!(" Timeout: {}s", config.llm.timeout);
println!("\n{}", " LLM Provider Details:".dimmed());
// OpenAI
println!(" 🔹 OpenAI:");
println!(" Model: {}", config.llm.openai.model.cyan());
println!(" Base URL: {}", config.llm.openai.base_url);
println!(" API Key: {}", mask_api_key(config.llm.openai.api_key.as_deref()));
// Anthropic
println!(" 🔹 Anthropic:");
println!(" Model: {}", config.llm.anthropic.model.cyan());
println!(" API Key: {}", mask_api_key(config.llm.anthropic.api_key.as_deref()));
// Kimi
println!(" 🔹 Kimi (Moonshot AI):");
println!(" Model: {}", config.llm.kimi.model.cyan());
println!(" Base URL: {}", config.llm.kimi.base_url);
println!(" API Key: {}", mask_api_key(config.llm.kimi.api_key.as_deref()));
// DeepSeek
println!(" 🔹 DeepSeek:");
println!(" Model: {}", config.llm.deepseek.model.cyan());
println!(" Base URL: {}", config.llm.deepseek.base_url);
println!(" API Key: {}", mask_api_key(config.llm.deepseek.api_key.as_deref()));
// OpenRouter
println!(" 🔹 OpenRouter:");
println!(" Model: {}", config.llm.openrouter.model.cyan());
println!(" Base URL: {}", config.llm.openrouter.base_url);
println!(" API Key: {}", mask_api_key(config.llm.openrouter.api_key.as_deref()));
// Ollama
println!(" 🔹 Ollama:");
println!(" URL: {}", config.llm.ollama.url);
println!(" Model: {}", config.llm.ollama.model.cyan());
println!("\n{}", "📝 Commit Configuration:".bold().blue());
println!(" Format: {}", config.commit.format.to_string().cyan());
println!(" Auto-generate: {}", if config.commit.auto_generate { "✓ yes".green() } else { "✗ no".red() });
println!(" Allow empty: {}", if config.commit.allow_empty { "✓ yes".green() } else { "✗ no".red() });
println!(" GPG sign: {}", if config.commit.gpg_sign { "✓ yes".green() } else { "✗ no".red() });
println!(" Default scope: {}", config.commit.default_scope.as_deref().unwrap_or("(none)").cyan());
println!(" Max subject length: {}", config.commit.max_subject_length);
println!(" Require scope: {}", if config.commit.require_scope { "✓ yes".green() } else { "✗ no".red() });
println!(" Require body: {}", if config.commit.require_body { "✓ yes".green() } else { "✗ no".red() });
if !config.commit.body_required_types.is_empty() {
println!(" Body required types: {}", config.commit.body_required_types.join(", ").cyan());
}
println!("\n{}", "🏷️ Tag Configuration:".bold().blue());
println!(" Version prefix: '{}'", config.tag.version_prefix.cyan());
println!(" Auto-generate: {}", if config.tag.auto_generate { "✓ yes".green() } else { "✗ no".red() });
println!(" GPG sign: {}", if config.tag.gpg_sign { "✓ yes".green() } else { "✗ no".red() });
println!(" Include changelog: {}", if config.tag.include_changelog { "✓ yes".green() } else { "✗ no".red() });
println!(" Annotation template: {}", config.tag.annotation_template.as_deref().unwrap_or("(none)").cyan());
println!("\n{}", "📋 Changelog Configuration:".bold().blue());
println!(" Path: {}", config.changelog.path);
println!(" Auto-generate: {}", if config.changelog.auto_generate { "✓ yes".green() } else { "✗ no".red() });
println!(" Format: {}", format!("{:?}", config.changelog.format).cyan());
println!(" Include hashes: {}", if config.changelog.include_hashes { "✓ yes".green() } else { "✗ no".red() });
println!(" Include authors: {}", if config.changelog.include_authors { "✓ yes".green() } else { "✗ no".red() });
println!(" Group by type: {}", if config.changelog.group_by_type { "✓ yes".green() } else { "✗ no".red() });
if !config.changelog.custom_categories.is_empty() {
println!(" Custom categories: {} category(ies)", config.changelog.custom_categories.len());
}
println!("\n{}", "🎨 Theme Configuration:".bold().blue());
println!(" Colors: {}", if config.theme.colors { "✓ enabled".green() } else { "✗ disabled".red() });
println!(" Icons: {}", if config.theme.icons { "✓ enabled".green() } else { "✗ disabled".red() });
println!(" Date format: {}", config.theme.date_format.cyan());
println!("\n{}", "🔒 Security:".bold().blue());
println!(" Encrypt sensitive: {}", if config.encrypt_sensitive { "✓ yes".green() } else { "✗ no".red() });
Ok(())
}
async fn edit_config(&self) -> Result<()> {
let manager = ConfigManager::new()?;
crate::utils::editor::edit_file(manager.path())?;
@@ -263,7 +448,7 @@ impl ConfigCommand {
let provider = if let Some(p) = provider {
p.to_string()
} else {
let providers = vec!["ollama", "openai", "anthropic"];
let providers = vec!["ollama", "openai", "anthropic", "kimi", "deepseek", "openrouter"];
let idx = Select::new()
.with_prompt("Select LLM provider")
.items(&providers)
@@ -287,12 +472,86 @@ impl ConfigCommand {
.default("gpt-4".to_string())
.interact_text()?;
manager.config_mut().llm.openai.model = model;
let base_url: String = Input::new()
.with_prompt("Base URL (optional)")
.default("https://api.openai.com/v1".to_string())
.interact_text()?;
if base_url != "https://api.openai.com/v1" {
manager.config_mut().llm.openai.base_url = base_url;
}
}
"anthropic" => {
let api_key: String = Input::new()
.with_prompt("Anthropic API key")
.interact_text()?;
manager.set_anthropic_api_key(api_key);
let model: String = Input::new()
.with_prompt("Model")
.default("claude-3-sonnet-20240229".to_string())
.interact_text()?;
manager.config_mut().llm.anthropic.model = model;
}
"kimi" => {
let api_key: String = Input::new()
.with_prompt("Kimi API key")
.interact_text()?;
manager.set_kimi_api_key(api_key);
let model: String = Input::new()
.with_prompt("Model")
.default("moonshot-v1-8k".to_string())
.interact_text()?;
manager.config_mut().llm.kimi.model = model;
let base_url: String = Input::new()
.with_prompt("Base URL (optional)")
.default("https://api.moonshot.cn/v1".to_string())
.interact_text()?;
if base_url != "https://api.moonshot.cn/v1" {
manager.set_kimi_base_url(base_url);
}
}
"deepseek" => {
let api_key: String = Input::new()
.with_prompt("DeepSeek API key")
.interact_text()?;
manager.set_deepseek_api_key(api_key);
let model: String = Input::new()
.with_prompt("Model")
.default("deepseek-chat".to_string())
.interact_text()?;
manager.config_mut().llm.deepseek.model = model;
let base_url: String = Input::new()
.with_prompt("Base URL (optional)")
.default("https://api.deepseek.com/v1".to_string())
.interact_text()?;
if base_url != "https://api.deepseek.com/v1" {
manager.set_deepseek_base_url(base_url);
}
}
"openrouter" => {
let api_key: String = Input::new()
.with_prompt("OpenRouter API key")
.interact_text()?;
manager.set_openrouter_api_key(api_key);
let model: String = Input::new()
.with_prompt("Model")
.default("openai/gpt-3.5-turbo".to_string())
.interact_text()?;
manager.config_mut().llm.openrouter.model = model;
let base_url: String = Input::new()
.with_prompt("Base URL (optional)")
.default("https://openrouter.ai/api/v1".to_string())
.interact_text()?;
if base_url != "https://openrouter.ai/api/v1" {
manager.set_openrouter_base_url(base_url);
}
}
"ollama" => {
let url: String = Input::new()
@@ -332,6 +591,75 @@ impl ConfigCommand {
Ok(())
}
async fn set_kimi_key(&self, key: &str) -> Result<()> {
let mut manager = ConfigManager::new()?;
manager.set_kimi_api_key(key.to_string());
manager.save()?;
println!("{} Kimi API key set", "".green());
Ok(())
}
async fn set_deepseek_key(&self, key: &str) -> Result<()> {
let mut manager = ConfigManager::new()?;
manager.set_deepseek_api_key(key.to_string());
manager.save()?;
println!("{} DeepSeek API key set", "".green());
Ok(())
}
async fn set_openrouter_key(&self, key: &str) -> Result<()> {
let mut manager = ConfigManager::new()?;
manager.set_openrouter_api_key(key.to_string());
manager.save()?;
println!("{} OpenRouter API key set", "".green());
Ok(())
}
async fn set_kimi(&self, base_url: Option<&str>, model: Option<&str>) -> Result<()> {
let mut manager = ConfigManager::new()?;
if let Some(url) = base_url {
manager.set_kimi_base_url(url.to_string());
}
if let Some(m) = model {
manager.config_mut().llm.kimi.model = m.to_string();
}
manager.save()?;
println!("{} Kimi configuration updated", "".green());
Ok(())
}
async fn set_deepseek(&self, base_url: Option<&str>, model: Option<&str>) -> Result<()> {
let mut manager = ConfigManager::new()?;
if let Some(url) = base_url {
manager.set_deepseek_base_url(url.to_string());
}
if let Some(m) = model {
manager.config_mut().llm.deepseek.model = m.to_string();
}
manager.save()?;
println!("{} DeepSeek configuration updated", "".green());
Ok(())
}
async fn set_openrouter(&self, base_url: Option<&str>, model: Option<&str>) -> Result<()> {
let mut manager = ConfigManager::new()?;
if let Some(url) = base_url {
manager.set_openrouter_base_url(url.to_string());
}
if let Some(m) = model {
manager.config_mut().llm.openrouter.model = m.to_string();
}
manager.save()?;
println!("{} OpenRouter configuration updated", "".green());
Ok(())
}
async fn set_ollama(&self, url: Option<&str>, model: Option<&str>) -> Result<()> {
let mut manager = ConfigManager::new()?;