Compare commits
2 Commits
v0.1.4
...
5638315031
| Author | SHA1 | Date | |
|---|---|---|---|
| 5638315031 | |||
| 2e43a5e396 |
28
CHANGELOG.md
28
CHANGELOG.md
@@ -1,4 +1,11 @@
|
|||||||
## v1.0.1 更新日志
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.1.4] - 2026-02-01
|
||||||
|
|
||||||
### ✨ 新功能
|
### ✨ 新功能
|
||||||
- 新增 `test3.txt`,支持中文输出测试
|
- 新增 `test3.txt`,支持中文输出测试
|
||||||
@@ -19,4 +26,21 @@
|
|||||||
|
|
||||||
### 🔧 其他变更
|
### 🔧 其他变更
|
||||||
- 新增个人访问令牌、使用统计与配置校验功能
|
- 新增个人访问令牌、使用统计与配置校验功能
|
||||||
- 添加 `test2.txt` 占位文件
|
- 添加 `test2.txt` 占位文件
|
||||||
|
|
||||||
|
## [0.1.0] - 2026-01-30
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Initial project structure
|
||||||
|
- Core functionality for git operations
|
||||||
|
- LLM integration
|
||||||
|
- Configuration management
|
||||||
|
- CLI interface
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- **Commit Generation**: Automatically generate conventional commit messages from git diffs
|
||||||
|
- **Profile Management**: Switch between multiple Git identities for different contexts
|
||||||
|
- **Tag Management**: Create annotated tags with AI-generated release notes
|
||||||
|
- **Changelog**: Generate and maintain changelog in Keep a Changelog format
|
||||||
|
- **Security**: Encrypt SSH passphrases and API keys
|
||||||
|
- **Interactive UI**: Beautiful CLI with prompts and previews
|
||||||
@@ -830,14 +830,14 @@ impl ConfigCommand {
|
|||||||
async fn list_models(&self) -> Result<()> {
|
async fn list_models(&self) -> Result<()> {
|
||||||
let manager = ConfigManager::new()?;
|
let manager = ConfigManager::new()?;
|
||||||
let config = manager.config();
|
let config = manager.config();
|
||||||
|
|
||||||
match config.llm.provider.as_str() {
|
match config.llm.provider.as_str() {
|
||||||
"ollama" => {
|
"ollama" => {
|
||||||
let client = crate::llm::OllamaClient::new(
|
let client = crate::llm::OllamaClient::new(
|
||||||
&config.llm.ollama.url,
|
&config.llm.ollama.url,
|
||||||
&config.llm.ollama.model,
|
&config.llm.ollama.model,
|
||||||
);
|
);
|
||||||
|
|
||||||
println!("Fetching available models from Ollama...");
|
println!("Fetching available models from Ollama...");
|
||||||
match client.list_models().await {
|
match client.list_models().await {
|
||||||
Ok(models) => {
|
Ok(models) => {
|
||||||
@@ -859,7 +859,7 @@ impl ConfigCommand {
|
|||||||
key,
|
key,
|
||||||
&config.llm.openai.model,
|
&config.llm.openai.model,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
println!("Fetching available models from OpenAI...");
|
println!("Fetching available models from OpenAI...");
|
||||||
match client.list_models().await {
|
match client.list_models().await {
|
||||||
Ok(models) => {
|
Ok(models) => {
|
||||||
@@ -877,11 +877,110 @@ impl ConfigCommand {
|
|||||||
bail!("OpenAI API key not configured");
|
bail!("OpenAI API key not configured");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"anthropic" => {
|
||||||
|
if let Some(ref key) = config.llm.anthropic.api_key {
|
||||||
|
let client = crate::llm::AnthropicClient::new(
|
||||||
|
key,
|
||||||
|
&config.llm.anthropic.model,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
println!("Fetching available models from Anthropic...");
|
||||||
|
match client.list_models().await {
|
||||||
|
Ok(models) => {
|
||||||
|
println!("\n{}", "Available models:".bold());
|
||||||
|
for model in models {
|
||||||
|
let marker = if model == config.llm.anthropic.model { "●".green() } else { "○".dimmed() };
|
||||||
|
println!("{} {}", marker, model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("{} Failed to fetch models: {}", "✗".red(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bail!("Anthropic API key not configured");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"kimi" => {
|
||||||
|
if let Some(ref key) = config.llm.kimi.api_key {
|
||||||
|
let client = crate::llm::KimiClient::with_base_url(
|
||||||
|
key,
|
||||||
|
&config.llm.kimi.model,
|
||||||
|
&config.llm.kimi.base_url,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
println!("Fetching available models from Kimi...");
|
||||||
|
match client.list_models().await {
|
||||||
|
Ok(models) => {
|
||||||
|
println!("\n{}", "Available models:".bold());
|
||||||
|
for model in models {
|
||||||
|
let marker = if model == config.llm.kimi.model { "●".green() } else { "○".dimmed() };
|
||||||
|
println!("{} {}", marker, model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("{} Failed to fetch models: {}", "✗".red(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bail!("Kimi API key not configured");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"deepseek" => {
|
||||||
|
if let Some(ref key) = config.llm.deepseek.api_key {
|
||||||
|
let client = crate::llm::DeepSeekClient::with_base_url(
|
||||||
|
key,
|
||||||
|
&config.llm.deepseek.model,
|
||||||
|
&config.llm.deepseek.base_url,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
println!("Fetching available models from DeepSeek...");
|
||||||
|
match client.list_models().await {
|
||||||
|
Ok(models) => {
|
||||||
|
println!("\n{}", "Available models:".bold());
|
||||||
|
for model in models {
|
||||||
|
let marker = if model == config.llm.deepseek.model { "●".green() } else { "○".dimmed() };
|
||||||
|
println!("{} {}", marker, model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("{} Failed to fetch models: {}", "✗".red(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bail!("DeepSeek API key not configured");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"openrouter" => {
|
||||||
|
if let Some(ref key) = config.llm.openrouter.api_key {
|
||||||
|
let client = crate::llm::OpenRouterClient::with_base_url(
|
||||||
|
key,
|
||||||
|
&config.llm.openrouter.model,
|
||||||
|
&config.llm.openrouter.base_url,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
println!("Fetching available models from OpenRouter...");
|
||||||
|
match client.list_models().await {
|
||||||
|
Ok(models) => {
|
||||||
|
println!("\n{}", "Available models:".bold());
|
||||||
|
for model in models {
|
||||||
|
let marker = if model == config.llm.openrouter.model { "●".green() } else { "○".dimmed() };
|
||||||
|
println!("{} {}", marker, model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("{} Failed to fetch models: {}", "✗".red(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bail!("OpenRouter API key not configured");
|
||||||
|
}
|
||||||
|
}
|
||||||
provider => {
|
provider => {
|
||||||
println!("Listing models not supported for provider: {}", provider);
|
println!("Listing models not supported for provider: {}", provider);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,15 +22,41 @@ impl GitRepo {
|
|||||||
/// Open a git repository
|
/// Open a git repository
|
||||||
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
|
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
let absolute_path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
|
|
||||||
|
// Enhanced cross-platform path handling
|
||||||
|
let absolute_path = if let Ok(canonical) = path.canonicalize() {
|
||||||
|
canonical
|
||||||
|
} else {
|
||||||
|
// Fallback: convert to absolute path without canonicalization
|
||||||
|
if path.is_absolute() {
|
||||||
|
path.to_path_buf()
|
||||||
|
} else {
|
||||||
|
std::env::current_dir()?.join(path)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Try multiple git repository discovery strategies for cross-platform compatibility
|
||||||
let repo = Repository::discover(&absolute_path)
|
let repo = Repository::discover(&absolute_path)
|
||||||
.or_else(|_| Repository::open(&absolute_path))
|
.or_else(|discover_err| {
|
||||||
|
// Try direct open as fallback
|
||||||
|
Repository::open(&absolute_path).map_err(|open_err| {
|
||||||
|
// Provide detailed error information for debugging
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"Git repository discovery failed:\n\
|
||||||
|
Discovery error: {}\n\
|
||||||
|
Direct open error: {}\n\
|
||||||
|
Path attempted: {:?}\n\
|
||||||
|
Current directory: {:?}",
|
||||||
|
discover_err, open_err, absolute_path, std::env::current_dir()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
.with_context(|| {
|
.with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
"Failed to open git repository at '{:?}'. Please ensure:\n\
|
"Failed to open git repository at '{:?}'. Please ensure:\n\
|
||||||
1. The directory is set as safe (run: git config --global --add safe.directory \"{}\")\n\
|
1. The directory contains a valid '.git' folder\n\
|
||||||
2. The path is correct and contains a valid '.git' folder.",
|
2. The directory is set as safe (run: git config --global --add safe.directory \"{}\")\n\
|
||||||
|
3. You have proper permissions to access the repository",
|
||||||
absolute_path,
|
absolute_path,
|
||||||
absolute_path.display()
|
absolute_path.display()
|
||||||
)
|
)
|
||||||
@@ -689,19 +715,37 @@ impl StatusSummary {
|
|||||||
pub fn find_repo<P: AsRef<Path>>(start_path: P) -> Result<GitRepo> {
|
pub fn find_repo<P: AsRef<Path>>(start_path: P) -> Result<GitRepo> {
|
||||||
let start_path = start_path.as_ref();
|
let start_path = start_path.as_ref();
|
||||||
|
|
||||||
|
// Try the starting path first
|
||||||
if let Ok(repo) = GitRepo::open(start_path) {
|
if let Ok(repo) = GitRepo::open(start_path) {
|
||||||
return Ok(repo);
|
return Ok(repo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Walk up the directory tree to find a git repository
|
||||||
let mut current = start_path;
|
let mut current = start_path;
|
||||||
|
let mut attempted_paths = vec![current.to_string_lossy().to_string()];
|
||||||
|
|
||||||
while let Some(parent) = current.parent() {
|
while let Some(parent) = current.parent() {
|
||||||
|
attempted_paths.push(parent.to_string_lossy().to_string());
|
||||||
|
|
||||||
if let Ok(repo) = GitRepo::open(parent) {
|
if let Ok(repo) = GitRepo::open(parent) {
|
||||||
return Ok(repo);
|
return Ok(repo);
|
||||||
}
|
}
|
||||||
current = parent;
|
current = parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
bail!("No git repository found starting from {:?}", start_path)
|
// Provide detailed error information for debugging
|
||||||
|
bail!(
|
||||||
|
"No git repository found starting from {:?}.\n\
|
||||||
|
Paths attempted:\n {}\n\
|
||||||
|
Current directory: {:?}\n\
|
||||||
|
Please ensure:\n\
|
||||||
|
1. You are in a git repository or its subdirectory\n\
|
||||||
|
2. The repository has a valid .git folder\n\
|
||||||
|
3. You have proper permissions to access the repository",
|
||||||
|
start_path,
|
||||||
|
attempted_paths.join("\n "),
|
||||||
|
std::env::current_dir()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if path is inside a git repository
|
/// Check if path is inside a git repository
|
||||||
|
|||||||
@@ -70,10 +70,16 @@ impl AnthropicClient {
|
|||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// List available models
|
||||||
|
pub async fn list_models(&self) -> Result<Vec<String>> {
|
||||||
|
// Anthropic doesn't have a models API endpoint, return predefined list
|
||||||
|
Ok(ANTHROPIC_MODELS.iter().map(|&m| m.to_string()).collect())
|
||||||
|
}
|
||||||
|
|
||||||
/// Validate API key
|
/// Validate API key
|
||||||
pub async fn validate_key(&self) -> Result<bool> {
|
pub async fn validate_key(&self) -> Result<bool> {
|
||||||
let url = "https://api.anthropic.com/v1/messages";
|
let url = "https://api.anthropic.com/v1/messages";
|
||||||
|
|
||||||
let request = MessagesRequest {
|
let request = MessagesRequest {
|
||||||
model: self.model.clone(),
|
model: self.model.clone(),
|
||||||
max_tokens: 5,
|
max_tokens: 5,
|
||||||
@@ -84,7 +90,7 @@ impl AnthropicClient {
|
|||||||
}],
|
}],
|
||||||
system: None,
|
system: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = self.client
|
let response = self.client
|
||||||
.post(url)
|
.post(url)
|
||||||
.header("x-api-key", &self.api_key)
|
.header("x-api-key", &self.api_key)
|
||||||
@@ -93,7 +99,7 @@ impl AnthropicClient {
|
|||||||
.json(&request)
|
.json(&request)
|
||||||
.send()
|
.send()
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
Ok(resp) => {
|
Ok(resp) => {
|
||||||
if resp.status().is_success() {
|
if resp.status().is_success() {
|
||||||
|
|||||||
@@ -82,25 +82,53 @@ impl DeepSeekClient {
|
|||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validate API key
|
/// List available models
|
||||||
pub async fn validate_key(&self) -> Result<bool> {
|
pub async fn list_models(&self) -> Result<Vec<String>> {
|
||||||
let url = format!("{}/models", self.base_url);
|
let url = format!("{}/models", self.base_url);
|
||||||
|
|
||||||
let response = self.client
|
let response = self.client
|
||||||
.get(&url)
|
.get(&url)
|
||||||
.header("Authorization", format!("Bearer {}", self.api_key))
|
.header("Authorization", format!("Bearer {}", self.api_key))
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.context("Failed to validate DeepSeek API key")?;
|
.context("Failed to list DeepSeek models")?;
|
||||||
|
|
||||||
if response.status().is_success() {
|
if !response.status().is_success() {
|
||||||
Ok(true)
|
|
||||||
} else if response.status().as_u16() == 401 {
|
|
||||||
Ok(false)
|
|
||||||
} else {
|
|
||||||
let status = response.status();
|
let status = response.status();
|
||||||
let text = response.text().await.unwrap_or_default();
|
let text = response.text().await.unwrap_or_default();
|
||||||
bail!("DeepSeek API error: {} - {}", status, text)
|
bail!("DeepSeek API error: {} - {}", status, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct ModelsResponse {
|
||||||
|
data: Vec<Model>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Model {
|
||||||
|
id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: ModelsResponse = response
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.context("Failed to parse DeepSeek response")?;
|
||||||
|
|
||||||
|
Ok(result.data.into_iter().map(|m| m.id).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate API key
|
||||||
|
pub async fn validate_key(&self) -> Result<bool> {
|
||||||
|
match self.list_models().await {
|
||||||
|
Ok(_) => Ok(true),
|
||||||
|
Err(e) => {
|
||||||
|
let err_str = e.to_string();
|
||||||
|
if err_str.contains("401") || err_str.contains("Unauthorized") {
|
||||||
|
Ok(false)
|
||||||
|
} else {
|
||||||
|
Err(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,25 +82,53 @@ impl KimiClient {
|
|||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validate API key
|
/// List available models
|
||||||
pub async fn validate_key(&self) -> Result<bool> {
|
pub async fn list_models(&self) -> Result<Vec<String>> {
|
||||||
let url = format!("{}/models", self.base_url);
|
let url = format!("{}/models", self.base_url);
|
||||||
|
|
||||||
let response = self.client
|
let response = self.client
|
||||||
.get(&url)
|
.get(&url)
|
||||||
.header("Authorization", format!("Bearer {}", self.api_key))
|
.header("Authorization", format!("Bearer {}", self.api_key))
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.context("Failed to validate Kimi API key")?;
|
.context("Failed to list Kimi models")?;
|
||||||
|
|
||||||
if response.status().is_success() {
|
if !response.status().is_success() {
|
||||||
Ok(true)
|
|
||||||
} else if response.status().as_u16() == 401 {
|
|
||||||
Ok(false)
|
|
||||||
} else {
|
|
||||||
let status = response.status();
|
let status = response.status();
|
||||||
let text = response.text().await.unwrap_or_default();
|
let text = response.text().await.unwrap_or_default();
|
||||||
bail!("Kimi API error: {} - {}", status, text)
|
bail!("Kimi API error: {} - {}", status, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct ModelsResponse {
|
||||||
|
data: Vec<Model>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Model {
|
||||||
|
id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: ModelsResponse = response
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.context("Failed to parse Kimi response")?;
|
||||||
|
|
||||||
|
Ok(result.data.into_iter().map(|m| m.id).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate API key
|
||||||
|
pub async fn validate_key(&self) -> Result<bool> {
|
||||||
|
match self.list_models().await {
|
||||||
|
Ok(_) => Ok(true),
|
||||||
|
Err(e) => {
|
||||||
|
let err_str = e.to_string();
|
||||||
|
if err_str.contains("401") || err_str.contains("Unauthorized") {
|
||||||
|
Ok(false)
|
||||||
|
} else {
|
||||||
|
Err(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,10 +82,10 @@ impl OpenRouterClient {
|
|||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validate API key
|
/// List available models
|
||||||
pub async fn validate_key(&self) -> Result<bool> {
|
pub async fn list_models(&self) -> Result<Vec<String>> {
|
||||||
let url = format!("{}/models", self.base_url);
|
let url = format!("{}/models", self.base_url);
|
||||||
|
|
||||||
let response = self.client
|
let response = self.client
|
||||||
.get(&url)
|
.get(&url)
|
||||||
.header("Authorization", format!("Bearer {}", self.api_key))
|
.header("Authorization", format!("Bearer {}", self.api_key))
|
||||||
@@ -93,16 +93,44 @@ impl OpenRouterClient {
|
|||||||
.header("X-Title", "QuiCommit")
|
.header("X-Title", "QuiCommit")
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.context("Failed to validate OpenRouter API key")?;
|
.context("Failed to list OpenRouter models")?;
|
||||||
|
|
||||||
if response.status().is_success() {
|
if !response.status().is_success() {
|
||||||
Ok(true)
|
|
||||||
} else if response.status().as_u16() == 401 {
|
|
||||||
Ok(false)
|
|
||||||
} else {
|
|
||||||
let status = response.status();
|
let status = response.status();
|
||||||
let text = response.text().await.unwrap_or_default();
|
let text = response.text().await.unwrap_or_default();
|
||||||
bail!("OpenRouter API error: {} - {}", status, text)
|
bail!("OpenRouter API error: {} - {}", status, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct ModelsResponse {
|
||||||
|
data: Vec<Model>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Model {
|
||||||
|
id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: ModelsResponse = response
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.context("Failed to parse OpenRouter response")?;
|
||||||
|
|
||||||
|
Ok(result.data.into_iter().map(|m| m.id).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate API key
|
||||||
|
pub async fn validate_key(&self) -> Result<bool> {
|
||||||
|
match self.list_models().await {
|
||||||
|
Ok(_) => Ok(true),
|
||||||
|
Err(e) => {
|
||||||
|
let err_str = e.to_string();
|
||||||
|
if err_str.contains("401") || err_str.contains("Unauthorized") {
|
||||||
|
Ok(false)
|
||||||
|
} else {
|
||||||
|
Err(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user