style: 格式化代码并优化导入顺序

This commit is contained in:
2026-05-27 15:15:15 +08:00
parent b8182e7538
commit 90074e6e32
34 changed files with 2931 additions and 1648 deletions

View File

@@ -1,21 +1,21 @@
use anyhow::{bail, Context, Result};
use crate::config::Language;
use anyhow::{Context, Result, bail};
use async_trait::async_trait;
use std::time::Duration;
use crate::config::Language;
pub mod anthropic;
pub mod deepseek;
pub mod kimi;
pub mod ollama;
pub mod openai;
pub mod anthropic;
pub mod kimi;
pub mod deepseek;
pub mod openrouter;
pub mod thinking;
pub use anthropic::AnthropicClient;
pub use deepseek::DeepSeekClient;
pub use kimi::KimiClient;
pub use ollama::OllamaClient;
pub use openai::OpenAiClient;
pub use anthropic::AnthropicClient;
pub use kimi::KimiClient;
pub use deepseek::DeepSeekClient;
pub use openrouter::OpenRouterClient;
/// LLM provider trait
@@ -23,13 +23,13 @@ pub use openrouter::OpenRouterClient;
pub trait LlmProvider: Send + Sync {
/// Generate text from prompt
async fn generate(&self, prompt: &str) -> Result<String>;
/// Generate with system prompt
async fn generate_with_system(&self, system: &str, user: &str) -> Result<String>;
/// Check if provider is available
async fn is_available(&self) -> bool;
/// Get provider name
fn name(&self) -> &str;
}
@@ -84,13 +84,14 @@ impl LlmClient {
let api_key = manager.get_api_key();
let provider: Box<dyn LlmProvider> = match provider {
"ollama" => {
Box::new(OllamaClient::new(&base_url, model)
"ollama" => Box::new(
OllamaClient::new(&base_url, model)
.with_max_tokens(client_config.max_tokens)
.with_temperature(client_config.temperature))
}
.with_temperature(client_config.temperature),
),
"openai" => {
let key = api_key.as_ref()
let key = api_key
.as_ref()
.ok_or_else(|| anyhow::anyhow!("OpenAI API key not configured"))?;
let thinking_state = if thinking_enabled {
Some(thinking::create_console_thinking_state())
@@ -108,7 +109,8 @@ impl LlmClient {
Box::new(client)
}
"anthropic" => {
let key = api_key.as_ref()
let key = api_key
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Anthropic API key not configured"))?;
let thinking_state = if thinking_enabled {
Some(thinking::create_console_thinking_state())
@@ -128,7 +130,8 @@ impl LlmClient {
Box::new(client)
}
"kimi" => {
let key = api_key.as_ref()
let key = api_key
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Kimi API key not configured"))?;
let thinking_state = if thinking_enabled {
Some(thinking::create_console_thinking_state())
@@ -146,7 +149,8 @@ impl LlmClient {
Box::new(client)
}
"deepseek" => {
let key = api_key.as_ref()
let key = api_key
.as_ref()
.ok_or_else(|| anyhow::anyhow!("DeepSeek API key not configured"))?;
let thinking_state = if thinking_enabled {
Some(thinking::create_console_thinking_state())
@@ -164,12 +168,15 @@ impl LlmClient {
Box::new(client)
}
"openrouter" => {
let key = api_key.as_ref()
let key = api_key
.as_ref()
.ok_or_else(|| anyhow::anyhow!("OpenRouter API key not configured"))?;
Box::new(OpenRouterClient::with_base_url(key, model, &base_url)?
.with_max_tokens(client_config.max_tokens)
.with_temperature(client_config.temperature)
.with_timeout(client_config.timeout)?)
Box::new(
OpenRouterClient::with_base_url(key, model, &base_url)?
.with_max_tokens(client_config.max_tokens)
.with_temperature(client_config.temperature)
.with_timeout(client_config.timeout)?,
)
}
_ => bail!("Unknown LLM provider: {}", provider),
};
@@ -196,7 +203,7 @@ impl LlmClient {
language: Language,
) -> Result<GeneratedCommit> {
let system_prompt = get_commit_system_prompt(format, language);
// Add language instruction to the prompt
let language_instruction = match language {
Language::Chinese => "\n\n请用中文生成提交消息。",
@@ -207,10 +214,13 @@ impl LlmClient {
Language::German => "\n\nBitte generieren Sie die Commit-Nachricht auf Deutsch.",
Language::English => "",
};
let prompt = format!("{}{}", diff, language_instruction);
let response = self.provider.generate_with_system(system_prompt, &prompt).await?;
let response = self
.provider
.generate_with_system(system_prompt, &prompt)
.await?;
self.parse_commit_response(&response, format)
}
@@ -223,7 +233,7 @@ impl LlmClient {
) -> Result<String> {
let system_prompt = get_tag_system_prompt(language);
let commits_text = commits.join("\n");
// Add language instruction to the prompt
let language_instruction = match language {
Language::Chinese => "\n\n请用中文生成标签消息。",
@@ -234,10 +244,15 @@ impl LlmClient {
Language::German => "\n\nBitte generieren Sie die Tag-Nachricht auf Deutsch.",
Language::English => "",
};
let prompt = format!("Version: {}\n\nCommits:\n{}{}", version, commits_text, language_instruction);
self.provider.generate_with_system(system_prompt, &prompt).await
let prompt = format!(
"Version: {}\n\nCommits:\n{}{}",
version, commits_text, language_instruction
);
self.provider
.generate_with_system(system_prompt, &prompt)
.await
}
/// Generate changelog entry
@@ -248,13 +263,13 @@ impl LlmClient {
language: Language,
) -> Result<String> {
let system_prompt = get_changelog_system_prompt(language);
let commits_text = commits
.iter()
.map(|(t, m)| format!("- [{}] {}", t, m))
.collect::<Vec<_>>()
.join("\n");
// Add language instruction to the prompt
let language_instruction = match language {
Language::Chinese => "\n\n请用中文生成变更日志。",
@@ -265,10 +280,15 @@ impl LlmClient {
Language::German => "\n\nBitte generieren Sie das Changelog auf Deutsch.",
Language::English => "",
};
let prompt = format!("Version: {}\n\nCommits:\n{}{}", version, commits_text, language_instruction);
self.provider.generate_with_system(system_prompt, &prompt).await
let prompt = format!(
"Version: {}\n\nCommits:\n{}{}",
version, commits_text, language_instruction
);
self.provider
.generate_with_system(system_prompt, &prompt)
.await
}
/// Check if provider is available
@@ -277,8 +297,16 @@ impl LlmClient {
}
/// Parse commit response from LLM
fn parse_commit_response(&self, response: &str, format: crate::config::CommitFormat) -> Result<GeneratedCommit> {
let lines: Vec<&str> = response.lines()
fn parse_commit_response(
&self,
response: &str,
format: crate::config::CommitFormat,
) -> Result<GeneratedCommit> {
// Clean markdown code fences from the response
let cleaned = Self::strip_code_fences(response);
let lines: Vec<&str> = cleaned
.lines()
.map(|l| l.trim())
.filter(|l| !l.is_empty())
.collect();
@@ -295,28 +323,89 @@ impl LlmClient {
);
}
let first_line = lines[0];
// Find the line most likely to be the commit subject
let first_line = Self::find_commit_subject_line(&lines, format);
// Parse based on format
match format {
crate::config::CommitFormat::Conventional => {
self.parse_conventional_commit(first_line, lines)
self.parse_conventional_commit(first_line, &lines, response)
}
crate::config::CommitFormat::Commitlint => {
self.parse_commitlint_commit(first_line, lines)
self.parse_commitlint_commit(first_line, &lines, response)
}
}
}
/// Remove surrounding markdown code fences (```) from LLM output
fn strip_code_fences(response: &str) -> String {
let mut lines: Vec<&str> = response.lines().collect();
// Strip leading fence lines (``` or ```lang)
while lines.first().map_or(false, |l| l.trim().starts_with("```")) {
lines.remove(0);
}
// Strip trailing fence lines
while lines.last().map_or(false, |l| l.trim() == "```") {
lines.pop();
}
lines.join("\n")
}
/// Find the line that is most likely the commit subject among extracted lines
fn find_commit_subject_line<'a>(
lines: &[&'a str],
format: crate::config::CommitFormat,
) -> &'a str {
let valid_types = crate::utils::validators::get_commit_types(matches!(
format,
crate::config::CommitFormat::Commitlint
));
// First pass: line starting with a known type that also has proper syntax
// (e.g. "type:", "type(scope):", "type!:")
for &line in lines {
let trimmed = line.trim();
for &t in valid_types {
if let Some(rest) = trimmed.strip_prefix(t) {
if rest.starts_with(':') || rest.starts_with('(') || rest.starts_with("!:") {
return trimmed;
}
}
}
}
// Second pass: any line containing a colon (generic "prefix: description")
for &line in lines {
if line.contains(':') {
return line.trim();
}
}
// Fallback: return the first line as-is
lines[0].trim()
}
fn parse_conventional_commit(
&self,
first_line: &str,
lines: Vec<&str>,
lines: &[&str],
raw_response: &str,
) -> Result<GeneratedCommit> {
// Parse: type(scope)!: description
let parts: Vec<&str> = first_line.splitn(2, ':').collect();
if parts.len() != 2 {
bail!("Invalid conventional commit format: missing colon");
let preview: String = raw_response.chars().take(300).collect();
bail!(
"Invalid conventional commit format: missing colon.\n\
Parsed subject line: '{}'\n\
Raw response preview: '{}'\n\
Expected: <type>[optional scope]: <description>",
first_line,
preview
);
}
let type_part = parts[0];
@@ -339,7 +428,7 @@ impl LlmClient {
};
// Extract body and footer
let (body, footer) = self.extract_body_footer(&lines);
let (body, footer) = self.extract_body_footer(lines);
Ok(GeneratedCommit {
commit_type,
@@ -354,12 +443,21 @@ impl LlmClient {
fn parse_commitlint_commit(
&self,
first_line: &str,
lines: Vec<&str>,
lines: &[&str],
raw_response: &str,
) -> Result<GeneratedCommit> {
// Similar parsing but with commitlint rules
let parts: Vec<&str> = first_line.splitn(2, ':').collect();
if parts.len() != 2 {
bail!("Invalid commit format: missing colon");
let preview: String = raw_response.chars().take(300).collect();
bail!(
"Invalid commit format: missing colon.\n\
Parsed subject line: '{}'\n\
Raw response preview: '{}'\n\
Expected: <type>[optional scope]: <subject>",
first_line,
preview
);
}
let type_part = parts[0];
@@ -405,8 +503,14 @@ impl LlmClient {
}
// Look for footer markers
let footer_markers = ["BREAKING CHANGE:", "Closes", "Fixes", "Refs", "Co-authored-by:"];
let footer_markers = [
"BREAKING CHANGE:",
"Closes",
"Fixes",
"Refs",
"Co-authored-by:",
];
let mut body_lines = vec![];
let mut footer_lines = vec![];
let mut in_footer = false;
@@ -415,7 +519,7 @@ impl LlmClient {
if footer_markers.iter().any(|m| line.starts_with(m)) {
in_footer = true;
}
if in_footer {
footer_lines.push(*line);
} else {
@@ -485,17 +589,34 @@ pub(crate) fn create_http_client(timeout: Duration) -> Result<reqwest::Client> {
}
/// Get commit system prompt based on format and language
fn get_commit_system_prompt(format: crate::config::CommitFormat, language: Language) -> &'static str {
fn get_commit_system_prompt(
format: crate::config::CommitFormat,
language: Language,
) -> &'static str {
match (format, language) {
(crate::config::CommitFormat::Conventional, Language::Chinese) => CONVENTIONAL_COMMIT_SYSTEM_PROMPT_ZH,
(crate::config::CommitFormat::Conventional, Language::Japanese) => CONVENTIONAL_COMMIT_SYSTEM_PROMPT_JA,
(crate::config::CommitFormat::Conventional, Language::Korean) => CONVENTIONAL_COMMIT_SYSTEM_PROMPT_KO,
(crate::config::CommitFormat::Conventional, Language::Spanish) => CONVENTIONAL_COMMIT_SYSTEM_PROMPT_ES,
(crate::config::CommitFormat::Conventional, Language::French) => CONVENTIONAL_COMMIT_SYSTEM_PROMPT_FR,
(crate::config::CommitFormat::Conventional, Language::German) => CONVENTIONAL_COMMIT_SYSTEM_PROMPT_DE,
(crate::config::CommitFormat::Conventional, Language::Chinese) => {
CONVENTIONAL_COMMIT_SYSTEM_PROMPT_ZH
}
(crate::config::CommitFormat::Conventional, Language::Japanese) => {
CONVENTIONAL_COMMIT_SYSTEM_PROMPT_JA
}
(crate::config::CommitFormat::Conventional, Language::Korean) => {
CONVENTIONAL_COMMIT_SYSTEM_PROMPT_KO
}
(crate::config::CommitFormat::Conventional, Language::Spanish) => {
CONVENTIONAL_COMMIT_SYSTEM_PROMPT_ES
}
(crate::config::CommitFormat::Conventional, Language::French) => {
CONVENTIONAL_COMMIT_SYSTEM_PROMPT_FR
}
(crate::config::CommitFormat::Conventional, Language::German) => {
CONVENTIONAL_COMMIT_SYSTEM_PROMPT_DE
}
(crate::config::CommitFormat::Conventional, _) => CONVENTIONAL_COMMIT_SYSTEM_PROMPT,
(crate::config::CommitFormat::Commitlint, Language::Chinese) => COMMITLINT_SYSTEM_PROMPT_ZH,
(crate::config::CommitFormat::Commitlint, Language::Japanese) => COMMITLINT_SYSTEM_PROMPT_JA,
(crate::config::CommitFormat::Commitlint, Language::Japanese) => {
COMMITLINT_SYSTEM_PROMPT_JA
}
(crate::config::CommitFormat::Commitlint, Language::Korean) => COMMITLINT_SYSTEM_PROMPT_KO,
(crate::config::CommitFormat::Commitlint, Language::Spanish) => COMMITLINT_SYSTEM_PROMPT_ES,
(crate::config::CommitFormat::Commitlint, Language::French) => COMMITLINT_SYSTEM_PROMPT_FR,