diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..73e764be9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,102 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +env: + CARGO_TERM_COLOR: always + +jobs: + lint: + name: Lint & Format + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + with: + components: clippy, rustfmt + + - name: Setup Rust cache + uses: Swatinem/rust-cache@v2 + with: + workspaces: src-tauri + shared-key: "rust-cache-lint" + + - name: Install system dependencies (Linux) + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libssl-dev + + - name: Check formatting + working-directory: src-tauri + run: cargo fmt --all -- --check + + - name: Run Clippy + working-directory: src-tauri + run: cargo clippy --all-targets --all-features -- -D warnings + + frontend-lint: + name: Frontend Lint + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: TypeScript check + run: npx tsc --noEmit + + build-check: + name: Build Check + runs-on: ubuntu-latest + needs: [lint, frontend-lint] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Setup Rust cache + uses: Swatinem/rust-cache@v2 + with: + workspaces: src-tauri + shared-key: "rust-cache-build" + + - name: Install system dependencies (Linux) + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libssl-dev + + - name: Install frontend dependencies + run: npm ci + + - name: Build frontend + run: npm run build + + - name: Check Rust build + working-directory: src-tauri + run: cargo check --all-targets diff --git a/src-tauri/src/converter/anthropic_to_openai.rs b/src-tauri/src/converter/anthropic_to_openai.rs index ac04466d0..bab222bf7 100644 --- a/src-tauri/src/converter/anthropic_to_openai.rs +++ b/src-tauri/src/converter/anthropic_to_openai.rs @@ -1,12 +1,12 @@ //! Anthropic 格式转换为 OpenAI 格式 (支持 Claude Code) -use crate::models::openai::*; use crate::models::anthropic::*; +use crate::models::openai::*; use uuid::Uuid; /// 将 Anthropic MessagesRequest 转换为 OpenAI ChatCompletionRequest pub fn convert_anthropic_to_openai(request: &AnthropicMessagesRequest) -> ChatCompletionRequest { let mut openai_messages: Vec = Vec::new(); - + // 处理 system prompt if let Some(system) = &request.system { let system_text = extract_system_text(system); @@ -19,27 +19,28 @@ pub fn convert_anthropic_to_openai(request: &AnthropicMessagesRequest) -> ChatCo }); } } - + // 转换消息 for msg in &request.messages { let converted = convert_anthropic_message(msg); openai_messages.extend(converted); } - + // 转换 tools let tools = request.tools.as_ref().map(|tools| { - tools.iter().map(|t| { - Tool { + tools + .iter() + .map(|t| Tool { tool_type: "function".to_string(), function: FunctionDef { name: t.name.clone(), description: t.description.clone(), parameters: t.input_schema.clone(), }, - } - }).collect() + }) + .collect() }); - + ChatCompletionRequest { model: request.model.clone(), messages: openai_messages, @@ -54,25 +55,26 @@ pub fn convert_anthropic_to_openai(request: &AnthropicMessagesRequest) -> ChatCo fn extract_system_text(system: &serde_json::Value) -> String { match system { serde_json::Value::String(s) => s.clone(), - serde_json::Value::Array(arr) => { - arr.iter() - .filter_map(|item| { - if item.get("type") == Some(&serde_json::Value::String("text".to_string())) { - item.get("text").and_then(|t| t.as_str()).map(|s| s.to_string()) - } else { - None - } - }) - .collect::>() - .join("\n") - } + serde_json::Value::Array(arr) => arr + .iter() + .filter_map(|item| { + if item.get("type") == Some(&serde_json::Value::String("text".to_string())) { + item.get("text") + .and_then(|t| t.as_str()) + .map(|s| s.to_string()) + } else { + None + } + }) + .collect::>() + .join("\n"), _ => String::new(), } } fn convert_anthropic_message(msg: &AnthropicMessage) -> Vec { let mut result: Vec = Vec::new(); - + match &msg.content { serde_json::Value::String(s) => { result.push(ChatMessage { @@ -86,10 +88,10 @@ fn convert_anthropic_message(msg: &AnthropicMessage) -> Vec { let mut text_parts: Vec = Vec::new(); let mut tool_calls: Vec = Vec::new(); let mut tool_results: Vec<(String, String)> = Vec::new(); // (tool_use_id, content) - + for part in parts { let part_type = part.get("type").and_then(|t| t.as_str()).unwrap_or(""); - + match part_type { "text" => { if let Some(text) = part.get("text").and_then(|t| t.as_str()) { @@ -98,11 +100,13 @@ fn convert_anthropic_message(msg: &AnthropicMessage) -> Vec { } "tool_use" => { let default_id = format!("call_{}", &Uuid::new_v4().to_string()[..8]); - let id = part.get("id").and_then(|i| i.as_str()) + let id = part + .get("id") + .and_then(|i| i.as_str()) .unwrap_or(&default_id); let name = part.get("name").and_then(|n| n.as_str()).unwrap_or(""); let input = part.get("input").cloned().unwrap_or(serde_json::json!({})); - + tool_calls.push(ToolCall { id: id.to_string(), call_type: "function".to_string(), @@ -113,21 +117,30 @@ fn convert_anthropic_message(msg: &AnthropicMessage) -> Vec { }); } "tool_result" => { - let tool_use_id = part.get("tool_use_id").and_then(|i| i.as_str()).unwrap_or(""); + let tool_use_id = part + .get("tool_use_id") + .and_then(|i| i.as_str()) + .unwrap_or(""); let content = extract_tool_result_content(part.get("content")); tool_results.push((tool_use_id.to_string(), content)); } _ => {} } } - + // 处理 assistant 消息 if msg.role == "assistant" { - let content = if text_parts.is_empty() { None } else { - Some(MessageContent::Text(text_parts.join(""))) + let content = if text_parts.is_empty() { + None + } else { + Some(MessageContent::Text(text_parts.join(""))) }; - let tc = if tool_calls.is_empty() { None } else { Some(tool_calls) }; - + let tc = if tool_calls.is_empty() { + None + } else { + Some(tool_calls) + }; + result.push(ChatMessage { role: "assistant".to_string(), content, @@ -146,7 +159,7 @@ fn convert_anthropic_message(msg: &AnthropicMessage) -> Vec { tool_call_id: Some(tool_use_id), }); } - + // 添加文本内容 if !text_parts.is_empty() { result.push(ChatMessage { @@ -160,25 +173,26 @@ fn convert_anthropic_message(msg: &AnthropicMessage) -> Vec { } _ => {} } - + result } fn extract_tool_result_content(content: Option<&serde_json::Value>) -> String { match content { Some(serde_json::Value::String(s)) => s.clone(), - Some(serde_json::Value::Array(arr)) => { - arr.iter() - .filter_map(|item| { - if item.get("type") == Some(&serde_json::Value::String("text".to_string())) { - item.get("text").and_then(|t| t.as_str()).map(|s| s.to_string()) - } else { - None - } - }) - .collect::>() - .join("\n") - } + Some(serde_json::Value::Array(arr)) => arr + .iter() + .filter_map(|item| { + if item.get("type") == Some(&serde_json::Value::String("text".to_string())) { + item.get("text") + .and_then(|t| t.as_str()) + .map(|s| s.to_string()) + } else { + None + } + }) + .collect::>() + .join("\n"), _ => String::new(), } } diff --git a/src-tauri/src/converter/cw_to_openai.rs b/src-tauri/src/converter/cw_to_openai.rs index 483ec15ac..d26527c63 100644 --- a/src-tauri/src/converter/cw_to_openai.rs +++ b/src-tauri/src/converter/cw_to_openai.rs @@ -1,6 +1,8 @@ //! CodeWhisperer 响应转换为 OpenAI 格式 -use crate::models::openai::*; +#![allow(dead_code)] + use crate::models::codewhisperer::*; +use crate::models::openai::*; use std::time::{SystemTime, UNIX_EPOCH}; use uuid::Uuid; @@ -14,7 +16,7 @@ pub fn convert_cw_event_to_openai_chunk( .duration_since(UNIX_EPOCH) .unwrap() .as_secs(); - + if let Some(resp_event) = &event.assistant_response_event { // 文本内容 if let Some(content) = &resp_event.content { @@ -34,7 +36,7 @@ pub fn convert_cw_event_to_openai_chunk( }], }); } - + // Tool use if let Some(tool_use) = &resp_event.tool_use { return Some(ChatCompletionChunk { @@ -52,7 +54,8 @@ pub fn convert_cw_event_to_openai_chunk( call_type: "function".to_string(), function: FunctionCall { name: tool_use.name.clone(), - arguments: serde_json::to_string(&tool_use.input).unwrap_or_default(), + arguments: serde_json::to_string(&tool_use.input) + .unwrap_or_default(), }, }]), }, @@ -61,7 +64,7 @@ pub fn convert_cw_event_to_openai_chunk( }); } } - + None } @@ -77,9 +80,13 @@ pub fn create_openai_response( .duration_since(UNIX_EPOCH) .unwrap() .as_secs(); - - let finish_reason = if tool_calls.is_some() { "tool_calls" } else { "stop" }; - + + let finish_reason = if tool_calls.is_some() { + "tool_calls" + } else { + "stop" + }; + ChatCompletionResponse { id: format!("chatcmpl-{}", Uuid::new_v4()), object: "chat.completion".to_string(), @@ -89,7 +96,11 @@ pub fn create_openai_response( index: 0, message: ResponseMessage { role: "assistant".to_string(), - content: if content.is_empty() { None } else { Some(content.to_string()) }, + content: if content.is_empty() { + None + } else { + Some(content.to_string()) + }, tool_calls, }, finish_reason: finish_reason.to_string(), @@ -108,7 +119,7 @@ pub fn create_stream_end_chunk(model: &str, response_id: &str) -> ChatCompletion .duration_since(UNIX_EPOCH) .unwrap() .as_secs(); - + ChatCompletionChunk { id: response_id.to_string(), object: "chat.completion.chunk".to_string(), diff --git a/src-tauri/src/converter/mod.rs b/src-tauri/src/converter/mod.rs index ccbf7a870..cc9e44361 100644 --- a/src-tauri/src/converter/mod.rs +++ b/src-tauri/src/converter/mod.rs @@ -1,7 +1,10 @@ -pub mod openai_to_cw; pub mod anthropic_to_openai; pub mod cw_to_openai; +pub mod openai_to_cw; -pub use openai_to_cw::*; +#[allow(unused_imports)] pub use anthropic_to_openai::*; +#[allow(unused_imports)] pub use cw_to_openai::*; +#[allow(unused_imports)] +pub use openai_to_cw::*; diff --git a/src-tauri/src/converter/openai_to_cw.rs b/src-tauri/src/converter/openai_to_cw.rs index 3f4d639e7..0634a7938 100644 --- a/src-tauri/src/converter/openai_to_cw.rs +++ b/src-tauri/src/converter/openai_to_cw.rs @@ -1,8 +1,8 @@ //! OpenAI 格式转换为 CodeWhisperer 格式 -use crate::models::openai::*; use crate::models::codewhisperer::*; -use uuid::Uuid; +use crate::models::openai::*; use std::collections::HashMap; +use uuid::Uuid; /// 模型映射表 pub fn get_model_map() -> HashMap<&'static str, &'static str> { @@ -10,11 +10,23 @@ pub fn get_model_map() -> HashMap<&'static str, &'static str> { map.insert("claude-opus-4-5", "claude-opus-4.5"); map.insert("claude-haiku-4-5", "claude-haiku-4.5"); map.insert("claude-sonnet-4-5", "CLAUDE_SONNET_4_5_20250929_V1_0"); - map.insert("claude-sonnet-4-5-20250929", "CLAUDE_SONNET_4_5_20250929_V1_0"); + map.insert( + "claude-sonnet-4-5-20250929", + "CLAUDE_SONNET_4_5_20250929_V1_0", + ); map.insert("claude-sonnet-4-20250514", "CLAUDE_SONNET_4_20250514_V1_0"); - map.insert("claude-3-7-sonnet-20250219", "CLAUDE_3_7_SONNET_20250219_V1_0"); - map.insert("claude-3-5-sonnet-20241022", "CLAUDE_3_7_SONNET_20250219_V1_0"); - map.insert("claude-3-5-sonnet-latest", "CLAUDE_3_7_SONNET_20250219_V1_0"); + map.insert( + "claude-3-7-sonnet-20250219", + "CLAUDE_3_7_SONNET_20250219_V1_0", + ); + map.insert( + "claude-3-5-sonnet-20241022", + "CLAUDE_3_7_SONNET_20250219_V1_0", + ); + map.insert( + "claude-3-5-sonnet-latest", + "CLAUDE_3_7_SONNET_20250219_V1_0", + ); map } @@ -26,16 +38,17 @@ pub fn convert_openai_to_codewhisperer( profile_arn: Option, ) -> CodeWhispererRequest { let model_map = get_model_map(); - let cw_model = model_map.get(request.model.as_str()) + let cw_model = model_map + .get(request.model.as_str()) .map(|s| s.to_string()) .unwrap_or_else(|| DEFAULT_MODEL.to_string()); - + let conversation_id = Uuid::new_v4().to_string(); - + // 提取 system prompt 和消息 let mut system_prompt = String::new(); let mut messages: Vec<&ChatMessage> = Vec::new(); - + for msg in &request.messages { if msg.role == "system" { system_prompt = msg.get_content_text(); @@ -43,15 +56,15 @@ pub fn convert_openai_to_codewhisperer( messages.push(msg); } } - + // 构建历史记录 let mut history: Vec = Vec::new(); let mut start_idx = 0; - + // 处理 system prompt - 合并到第一条用户消息 if !system_prompt.is_empty() && !messages.is_empty() && messages[0].role == "user" { let first_content = messages[0].get_content_text(); - let combined = format!("{}\n\n{}", system_prompt, first_content); + let combined = format!("{system_prompt}\n\n{first_content}"); history.push(HistoryItem::User(UserHistoryItem { user_input_message: UserInputMessage { content: combined, @@ -63,33 +76,41 @@ pub fn convert_openai_to_codewhisperer( })); start_idx = 1; } - + // 处理历史消息(除最后一条) - for i in start_idx..messages.len().saturating_sub(1) { - let msg = messages[i]; + for msg in messages + .iter() + .take(messages.len().saturating_sub(1)) + .skip(start_idx) + { match msg.role.as_str() { "user" => { let content = msg.get_content_text(); let tool_results = extract_tool_results(msg); - + let mut user_msg = UserInputMessage { - content: if content.is_empty() { - if tool_results.is_some() { "Tool results provided.".to_string() } - else { "Continue".to_string() } - } else { content }, + content: if content.is_empty() { + if tool_results.is_some() { + "Tool results provided.".to_string() + } else { + "Continue".to_string() + } + } else { + content + }, model_id: cw_model.clone(), origin: "AI_EDITOR".to_string(), images: None, user_input_message_context: None, }; - + if tool_results.is_some() { user_msg.user_input_message_context = Some(UserInputMessageContext { tools: None, tool_results, }); } - + history.push(HistoryItem::User(UserHistoryItem { user_input_message: user_msg, })); @@ -97,10 +118,14 @@ pub fn convert_openai_to_codewhisperer( "assistant" => { let content = msg.get_content_text(); let tool_uses = extract_tool_uses(msg); - + history.push(HistoryItem::Assistant(AssistantHistoryItem { assistant_response_message: AssistantResponseMessage { - content: if content.is_empty() { "I understand.".to_string() } else { content }, + content: if content.is_empty() { + "I understand.".to_string() + } else { + content + }, tool_uses, }, })); @@ -108,10 +133,13 @@ pub fn convert_openai_to_codewhisperer( "tool" => { let tool_content = msg.get_content_text(); let tool_id = msg.tool_call_id.clone().unwrap_or_default(); - + history.push(HistoryItem::User(UserHistoryItem { user_input_message: UserInputMessage { - content: format!("Tool result: {}", &tool_content[..tool_content.len().min(200)]), + content: format!( + "Tool result: {}", + &tool_content[..tool_content.len().min(200)] + ), model_id: cw_model.clone(), origin: "AI_EDITOR".to_string(), images: None, @@ -129,10 +157,10 @@ pub fn convert_openai_to_codewhisperer( _ => {} } } - + // 修复历史记录交替顺序 let history = fix_history_alternation(history, &cw_model); - + // 构建当前消息 let current_content = if messages.is_empty() { "Continue".to_string() @@ -142,36 +170,54 @@ pub fn convert_openai_to_codewhisperer( "Continue".to_string() } else { let content = last_msg.get_content_text(); - if content.is_empty() { "Continue".to_string() } else { content } + if content.is_empty() { + "Continue".to_string() + } else { + content + } } }; - + // 构建 tools let tools = request.tools.as_ref().map(|tools| { - tools.iter().take(50).map(|t| { - let params = t.function.parameters.clone() - .unwrap_or_else(|| serde_json::json!({"type": "object", "properties": {}})); - - let desc = t.function.description.clone() - .unwrap_or_else(|| format!("Tool: {}", t.function.name)); - - CWTool { - tool_specification: ToolSpecification { - name: t.function.name.clone(), - description: if desc.len() > 500 { format!("{}...", &desc[..497]) } else { desc }, - input_schema: InputSchema { json: params }, - }, - } - }).collect() + tools + .iter() + .take(50) + .map(|t| { + let params = t + .function + .parameters + .clone() + .unwrap_or_else(|| serde_json::json!({"type": "object", "properties": {}})); + + let desc = t + .function + .description + .clone() + .unwrap_or_else(|| format!("Tool: {}", t.function.name)); + + CWTool { + tool_specification: ToolSpecification { + name: t.function.name.clone(), + description: if desc.len() > 500 { + format!("{}...", &desc[..497]) + } else { + desc + }, + input_schema: InputSchema { json: params }, + }, + } + }) + .collect() }); - + let current_tool_results = if !messages.is_empty() { let last_msg = messages.last().unwrap(); extract_tool_results(last_msg) } else { None }; - + let user_input_message_context = if tools.is_some() || current_tool_results.is_some() { Some(UserInputMessageContext { tools, @@ -180,7 +226,7 @@ pub fn convert_openai_to_codewhisperer( } else { None }; - + CodeWhispererRequest { conversation_state: ConversationState { chat_trigger_type: "MANUAL".to_string(), @@ -194,7 +240,11 @@ pub fn convert_openai_to_codewhisperer( user_input_message_context, }, }, - history: if history.is_empty() { None } else { Some(history) }, + history: if history.is_empty() { + None + } else { + Some(history) + }, }, profile_arn, } @@ -215,15 +265,18 @@ fn extract_tool_results(msg: &ChatMessage) -> Option> { fn extract_tool_uses(msg: &ChatMessage) -> Option> { msg.tool_calls.as_ref().map(|calls| { - calls.iter().map(|tc| { - let input: serde_json::Value = serde_json::from_str(&tc.function.arguments) - .unwrap_or(serde_json::json!({})); - CWToolUse { - input, - name: tc.function.name.clone(), - tool_use_id: tc.id.clone(), - } - }).collect() + calls + .iter() + .map(|tc| { + let input: serde_json::Value = + serde_json::from_str(&tc.function.arguments).unwrap_or(serde_json::json!({})); + CWToolUse { + input, + name: tc.function.name.clone(), + tool_use_id: tc.id.clone(), + } + }) + .collect() }) } @@ -232,9 +285,9 @@ fn fix_history_alternation(history: Vec, model_id: &str) -> Vec = Vec::new(); - + for item in history { match &item { HistoryItem::User(_) => { @@ -278,7 +331,7 @@ fn fix_history_alternation(history: Vec, model_id: &str) -> Vec, model_id: &str) -> Vec>; pub type LogState = Arc>; #[tauri::command] -async fn start_server(state: tauri::State<'_, AppState>, logs: tauri::State<'_, LogState>) -> Result { +async fn start_server( + state: tauri::State<'_, AppState>, + logs: tauri::State<'_, LogState>, +) -> Result { let mut s = state.write().await; logs.write().await.add("info", "Starting server..."); - s.start(logs.inner().clone()).await.map_err(|e| e.to_string())?; - logs.write().await.add("info", &format!("Server started on {}:{}", s.config.server.host, s.config.server.port)); + s.start(logs.inner().clone()) + .await + .map_err(|e| e.to_string())?; + logs.write().await.add( + "info", + &format!( + "Server started on {}:{}", + s.config.server.host, s.config.server.port + ), + ); Ok("Server started".to_string()) } #[tauri::command] -async fn stop_server(state: tauri::State<'_, AppState>, logs: tauri::State<'_, LogState>) -> Result { +async fn stop_server( + state: tauri::State<'_, AppState>, + logs: tauri::State<'_, LogState>, +) -> Result { let mut s = state.write().await; s.stop().await; logs.write().await.add("info", "Server stopped"); @@ -29,7 +43,9 @@ async fn stop_server(state: tauri::State<'_, AppState>, logs: tauri::State<'_, L } #[tauri::command] -async fn get_server_status(state: tauri::State<'_, AppState>) -> Result { +async fn get_server_status( + state: tauri::State<'_, AppState>, +) -> Result { let s = state.read().await; Ok(s.status()) } @@ -41,7 +57,10 @@ async fn get_config(state: tauri::State<'_, AppState>) -> Result, config: config::Config) -> Result<(), String> { +async fn save_config( + state: tauri::State<'_, AppState>, + config: config::Config, +) -> Result<(), String> { let mut s = state.write().await; s.config = config.clone(); config::save_config(&config).map_err(|e| e.to_string()) @@ -61,33 +80,53 @@ async fn set_default_provider( ) -> Result { let valid_providers = ["kiro", "gemini", "qwen", "openai", "claude"]; if !valid_providers.contains(&provider.as_str()) { - return Err(format!("Invalid provider: {}", provider)); + return Err(format!("Invalid provider: {provider}")); } - + let mut s = state.write().await; s.config.default_provider = provider.clone(); config::save_config(&s.config).map_err(|e| e.to_string())?; - logs.write().await.add("info", &format!("默认 Provider 已切换为: {}", provider)); + logs.write() + .await + .add("info", &format!("默认 Provider 已切换为: {provider}")); Ok(provider) } #[tauri::command] -async fn refresh_kiro_token(state: tauri::State<'_, AppState>, logs: tauri::State<'_, LogState>) -> Result { +async fn refresh_kiro_token( + state: tauri::State<'_, AppState>, + logs: tauri::State<'_, LogState>, +) -> Result { let mut s = state.write().await; logs.write().await.add("info", "Refreshing Kiro token..."); - let result = s.kiro_provider.refresh_token().await.map_err(|e| e.to_string()); + let result = s + .kiro_provider + .refresh_token() + .await + .map_err(|e| e.to_string()); match &result { - Ok(_) => logs.write().await.add("info", "Token refreshed successfully"), - Err(e) => logs.write().await.add("error", &format!("Token refresh failed: {}", e)), + Ok(_) => logs + .write() + .await + .add("info", "Token refreshed successfully"), + Err(e) => logs + .write() + .await + .add("error", &format!("Token refresh failed: {e}")), } result } #[tauri::command] -async fn reload_credentials(state: tauri::State<'_, AppState>, logs: tauri::State<'_, LogState>) -> Result { +async fn reload_credentials( + state: tauri::State<'_, AppState>, + logs: tauri::State<'_, LogState>, +) -> Result { let mut s = state.write().await; logs.write().await.add("info", "Reloading credentials..."); - s.kiro_provider.load_credentials().map_err(|e| e.to_string())?; + s.kiro_provider + .load_credentials() + .map_err(|e| e.to_string())?; logs.write().await.add("info", "Credentials reloaded"); Ok("Credentials reloaded".to_string()) } @@ -104,11 +143,13 @@ struct KiroCredentialStatus { } #[tauri::command] -async fn get_kiro_credentials(state: tauri::State<'_, AppState>) -> Result { +async fn get_kiro_credentials( + state: tauri::State<'_, AppState>, +) -> Result { let s = state.read().await; let creds = &s.kiro_provider.credentials; let path = providers::kiro::KiroProvider::default_creds_path(); - + Ok(KiroCredentialStatus { loaded: creds.access_token.is_some() || creds.refresh_token.is_some(), has_access_token: creds.access_token.is_some(), @@ -132,7 +173,7 @@ async fn get_env_variables(state: tauri::State<'_, AppState>) -> Result) -> Result String { if token.len() <= 12 { "****".to_string() } else { - format!("{}****{}", &token[..6], &token[token.len()-4..]) + format!("{}****{}", &token[..6], &token[token.len() - 4..]) } } @@ -200,7 +241,7 @@ async fn get_token_file_hash() -> Result { if !path.exists() { return Ok("".to_string()); } - + let content = std::fs::read(&path).map_err(|e| e.to_string())?; let hash = format!("{:x}", md5::compute(&content)); Ok(hash) @@ -214,7 +255,7 @@ async fn check_and_reload_credentials( last_hash: String, ) -> Result { let path = providers::kiro::KiroProvider::default_creds_path(); - + if !path.exists() { return Ok(CheckResult { changed: false, @@ -222,17 +263,21 @@ async fn check_and_reload_credentials( reloaded: false, }); } - + let content = std::fs::read(&path).map_err(|e| e.to_string())?; let new_hash = format!("{:x}", md5::compute(&content)); - + if !last_hash.is_empty() && new_hash != last_hash { - logs.write().await.add("info", "[自动检测] 凭证文件已变化,正在重新加载..."); - + logs.write() + .await + .add("info", "[自动检测] 凭证文件已变化,正在重新加载..."); + let mut s = state.write().await; match s.kiro_provider.load_credentials() { Ok(_) => { - logs.write().await.add("info", "[自动检测] 凭证重新加载成功"); + logs.write() + .await + .add("info", "[自动检测] 凭证重新加载成功"); Ok(CheckResult { changed: true, new_hash, @@ -240,7 +285,9 @@ async fn check_and_reload_credentials( }) } Err(e) => { - logs.write().await.add("error", &format!("[自动检测] 凭证重新加载失败: {}", e)); + logs.write() + .await + .add("error", &format!("[自动检测] 凭证重新加载失败: {e}")); Ok(CheckResult { changed: true, new_hash, @@ -277,11 +324,13 @@ struct GeminiCredentialStatus { } #[tauri::command] -async fn get_gemini_credentials(state: tauri::State<'_, AppState>) -> Result { +async fn get_gemini_credentials( + state: tauri::State<'_, AppState>, +) -> Result { let s = state.read().await; let creds = &s.gemini_provider.credentials; let path = providers::gemini::GeminiProvider::default_creds_path(); - + Ok(GeminiCredentialStatus { loaded: creds.access_token.is_some() || creds.refresh_token.is_some(), has_access_token: creds.access_token.is_some(), @@ -293,32 +342,49 @@ async fn get_gemini_credentials(state: tauri::State<'_, AppState>) -> Result, logs: tauri::State<'_, LogState>) -> Result { +async fn reload_gemini_credentials( + state: tauri::State<'_, AppState>, + logs: tauri::State<'_, LogState>, +) -> Result { let mut s = state.write().await; logs.write().await.add("info", "[Gemini] 正在加载凭证..."); - s.gemini_provider.load_credentials().map_err(|e| e.to_string())?; + s.gemini_provider + .load_credentials() + .map_err(|e| e.to_string())?; logs.write().await.add("info", "[Gemini] 凭证加载成功"); Ok("Gemini credentials reloaded".to_string()) } #[tauri::command] -async fn refresh_gemini_token(state: tauri::State<'_, AppState>, logs: tauri::State<'_, LogState>) -> Result { +async fn refresh_gemini_token( + state: tauri::State<'_, AppState>, + logs: tauri::State<'_, LogState>, +) -> Result { let mut s = state.write().await; logs.write().await.add("info", "[Gemini] 正在刷新 Token..."); - let result = s.gemini_provider.refresh_token().await.map_err(|e| e.to_string()); + let result = s + .gemini_provider + .refresh_token() + .await + .map_err(|e| e.to_string()); match &result { Ok(_) => logs.write().await.add("info", "[Gemini] Token 刷新成功"), - Err(e) => logs.write().await.add("error", &format!("[Gemini] Token 刷新失败: {}", e)), + Err(e) => logs + .write() + .await + .add("error", &format!("[Gemini] Token 刷新失败: {e}")), } result } #[tauri::command] -async fn get_gemini_env_variables(state: tauri::State<'_, AppState>) -> Result, String> { +async fn get_gemini_env_variables( + state: tauri::State<'_, AppState>, +) -> Result, String> { let s = state.read().await; let creds = &s.gemini_provider.credentials; let mut vars = Vec::new(); - + if let Some(token) = &creds.access_token { vars.push(EnvVariable { key: "GEMINI_ACCESS_TOKEN".to_string(), @@ -341,7 +407,7 @@ async fn get_gemini_env_variables(state: tauri::State<'_, AppState>) -> Result Result { if !path.exists() { return Ok("".to_string()); } - + let content = std::fs::read(&path).map_err(|e| e.to_string())?; let hash = format!("{:x}", md5::compute(&content)); Ok(hash) @@ -364,7 +430,7 @@ async fn check_and_reload_gemini_credentials( last_hash: String, ) -> Result { let path = providers::gemini::GeminiProvider::default_creds_path(); - + if !path.exists() { return Ok(CheckResult { changed: false, @@ -372,17 +438,21 @@ async fn check_and_reload_gemini_credentials( reloaded: false, }); } - + let content = std::fs::read(&path).map_err(|e| e.to_string())?; let new_hash = format!("{:x}", md5::compute(&content)); - + if !last_hash.is_empty() && new_hash != last_hash { - logs.write().await.add("info", "[Gemini][自动检测] 凭证文件已变化,正在重新加载..."); - + logs.write() + .await + .add("info", "[Gemini][自动检测] 凭证文件已变化,正在重新加载..."); + let mut s = state.write().await; match s.gemini_provider.load_credentials() { Ok(_) => { - logs.write().await.add("info", "[Gemini][自动检测] 凭证重新加载成功"); + logs.write() + .await + .add("info", "[Gemini][自动检测] 凭证重新加载成功"); Ok(CheckResult { changed: true, new_hash, @@ -390,7 +460,10 @@ async fn check_and_reload_gemini_credentials( }) } Err(e) => { - logs.write().await.add("error", &format!("[Gemini][自动检测] 凭证重新加载失败: {}", e)); + logs.write().await.add( + "error", + &format!("[Gemini][自动检测] 凭证重新加载失败: {e}"), + ); Ok(CheckResult { changed: true, new_hash, @@ -420,11 +493,13 @@ struct QwenCredentialStatus { } #[tauri::command] -async fn get_qwen_credentials(state: tauri::State<'_, AppState>) -> Result { +async fn get_qwen_credentials( + state: tauri::State<'_, AppState>, +) -> Result { let s = state.read().await; let creds = &s.qwen_provider.credentials; let path = providers::qwen::QwenProvider::default_creds_path(); - + Ok(QwenCredentialStatus { loaded: creds.access_token.is_some() || creds.refresh_token.is_some(), has_access_token: creds.access_token.is_some(), @@ -436,32 +511,49 @@ async fn get_qwen_credentials(state: tauri::State<'_, AppState>) -> Result, logs: tauri::State<'_, LogState>) -> Result { +async fn reload_qwen_credentials( + state: tauri::State<'_, AppState>, + logs: tauri::State<'_, LogState>, +) -> Result { let mut s = state.write().await; logs.write().await.add("info", "[Qwen] 正在加载凭证..."); - s.qwen_provider.load_credentials().map_err(|e| e.to_string())?; + s.qwen_provider + .load_credentials() + .map_err(|e| e.to_string())?; logs.write().await.add("info", "[Qwen] 凭证加载成功"); Ok("Qwen credentials reloaded".to_string()) } #[tauri::command] -async fn refresh_qwen_token(state: tauri::State<'_, AppState>, logs: tauri::State<'_, LogState>) -> Result { +async fn refresh_qwen_token( + state: tauri::State<'_, AppState>, + logs: tauri::State<'_, LogState>, +) -> Result { let mut s = state.write().await; logs.write().await.add("info", "[Qwen] 正在刷新 Token..."); - let result = s.qwen_provider.refresh_token().await.map_err(|e| e.to_string()); + let result = s + .qwen_provider + .refresh_token() + .await + .map_err(|e| e.to_string()); match &result { Ok(_) => logs.write().await.add("info", "[Qwen] Token 刷新成功"), - Err(e) => logs.write().await.add("error", &format!("[Qwen] Token 刷新失败: {}", e)), + Err(e) => logs + .write() + .await + .add("error", &format!("[Qwen] Token 刷新失败: {e}")), } result } #[tauri::command] -async fn get_qwen_env_variables(state: tauri::State<'_, AppState>) -> Result, String> { +async fn get_qwen_env_variables( + state: tauri::State<'_, AppState>, +) -> Result, String> { let s = state.read().await; let creds = &s.qwen_provider.credentials; let mut vars = Vec::new(); - + if let Some(token) = &creds.access_token { vars.push(EnvVariable { key: "QWEN_ACCESS_TOKEN".to_string(), @@ -491,7 +583,7 @@ async fn get_qwen_env_variables(state: tauri::State<'_, AppState>) -> Result Result { if !path.exists() { return Ok("".to_string()); } - + let content = std::fs::read(&path).map_err(|e| e.to_string())?; let hash = format!("{:x}", md5::compute(&content)); Ok(hash) @@ -514,7 +606,7 @@ async fn check_and_reload_qwen_credentials( last_hash: String, ) -> Result { let path = providers::qwen::QwenProvider::default_creds_path(); - + if !path.exists() { return Ok(CheckResult { changed: false, @@ -522,17 +614,21 @@ async fn check_and_reload_qwen_credentials( reloaded: false, }); } - + let content = std::fs::read(&path).map_err(|e| e.to_string())?; let new_hash = format!("{:x}", md5::compute(&content)); - + if !last_hash.is_empty() && new_hash != last_hash { - logs.write().await.add("info", "[Qwen][自动检测] 凭证文件已变化,正在重新加载..."); - + logs.write() + .await + .add("info", "[Qwen][自动检测] 凭证文件已变化,正在重新加载..."); + let mut s = state.write().await; match s.qwen_provider.load_credentials() { Ok(_) => { - logs.write().await.add("info", "[Qwen][自动检测] 凭证重新加载成功"); + logs.write() + .await + .add("info", "[Qwen][自动检测] 凭证重新加载成功"); Ok(CheckResult { changed: true, new_hash, @@ -540,7 +636,9 @@ async fn check_and_reload_qwen_credentials( }) } Err(e) => { - logs.write().await.add("error", &format!("[Qwen][自动检测] 凭证重新加载失败: {}", e)); + logs.write() + .await + .add("error", &format!("[Qwen][自动检测] 凭证重新加载失败: {e}")); Ok(CheckResult { changed: true, new_hash, @@ -567,7 +665,9 @@ struct OpenAICustomStatus { } #[tauri::command] -async fn get_openai_custom_status(state: tauri::State<'_, AppState>) -> Result { +async fn get_openai_custom_status( + state: tauri::State<'_, AppState>, +) -> Result { let s = state.read().await; let config = &s.openai_custom_provider.config; Ok(OpenAICustomStatus { @@ -589,7 +689,10 @@ async fn set_openai_custom_config( s.openai_custom_provider.config.api_key = api_key; s.openai_custom_provider.config.base_url = base_url; s.openai_custom_provider.config.enabled = enabled; - logs.write().await.add("info", &format!("[OpenAI Custom] 配置已更新, enabled={}", enabled)); + logs.write().await.add( + "info", + &format!("[OpenAI Custom] 配置已更新, enabled={enabled}"), + ); Ok("OpenAI Custom config updated".to_string()) } @@ -603,7 +706,9 @@ struct ClaudeCustomStatus { } #[tauri::command] -async fn get_claude_custom_status(state: tauri::State<'_, AppState>) -> Result { +async fn get_claude_custom_status( + state: tauri::State<'_, AppState>, +) -> Result { let s = state.read().await; let config = &s.claude_custom_provider.config; Ok(ClaudeCustomStatus { @@ -625,7 +730,10 @@ async fn set_claude_custom_config( s.claude_custom_provider.config.api_key = api_key; s.claude_custom_provider.config.base_url = base_url; s.claude_custom_provider.config.enabled = enabled; - logs.write().await.add("info", &format!("[Claude Custom] 配置已更新, enabled={}", enabled)); + logs.write().await.add( + "info", + &format!("[Claude Custom] 配置已更新, enabled={enabled}"), + ); Ok("Claude Custom config updated".to_string()) } @@ -659,22 +767,78 @@ struct ModelInfo { async fn get_available_models() -> Result, String> { Ok(vec![ // Kiro/Claude models - ModelInfo { id: "claude-sonnet-4-5".to_string(), object: "model".to_string(), owned_by: "anthropic".to_string() }, - ModelInfo { id: "claude-sonnet-4-5-20250514".to_string(), object: "model".to_string(), owned_by: "anthropic".to_string() }, - ModelInfo { id: "claude-sonnet-4-5-20250929".to_string(), object: "model".to_string(), owned_by: "anthropic".to_string() }, - ModelInfo { id: "claude-3-7-sonnet-20250219".to_string(), object: "model".to_string(), owned_by: "anthropic".to_string() }, - ModelInfo { id: "claude-3-5-sonnet-latest".to_string(), object: "model".to_string(), owned_by: "anthropic".to_string() }, - ModelInfo { id: "claude-opus-4-5-20250514".to_string(), object: "model".to_string(), owned_by: "anthropic".to_string() }, - ModelInfo { id: "claude-haiku-4-5-20250514".to_string(), object: "model".to_string(), owned_by: "anthropic".to_string() }, + ModelInfo { + id: "claude-sonnet-4-5".to_string(), + object: "model".to_string(), + owned_by: "anthropic".to_string(), + }, + ModelInfo { + id: "claude-sonnet-4-5-20250514".to_string(), + object: "model".to_string(), + owned_by: "anthropic".to_string(), + }, + ModelInfo { + id: "claude-sonnet-4-5-20250929".to_string(), + object: "model".to_string(), + owned_by: "anthropic".to_string(), + }, + ModelInfo { + id: "claude-3-7-sonnet-20250219".to_string(), + object: "model".to_string(), + owned_by: "anthropic".to_string(), + }, + ModelInfo { + id: "claude-3-5-sonnet-latest".to_string(), + object: "model".to_string(), + owned_by: "anthropic".to_string(), + }, + ModelInfo { + id: "claude-opus-4-5-20250514".to_string(), + object: "model".to_string(), + owned_by: "anthropic".to_string(), + }, + ModelInfo { + id: "claude-haiku-4-5-20250514".to_string(), + object: "model".to_string(), + owned_by: "anthropic".to_string(), + }, // Gemini models - ModelInfo { id: "gemini-2.5-flash".to_string(), object: "model".to_string(), owned_by: "google".to_string() }, - ModelInfo { id: "gemini-2.5-flash-lite".to_string(), object: "model".to_string(), owned_by: "google".to_string() }, - ModelInfo { id: "gemini-2.5-pro".to_string(), object: "model".to_string(), owned_by: "google".to_string() }, - ModelInfo { id: "gemini-2.5-pro-preview-06-05".to_string(), object: "model".to_string(), owned_by: "google".to_string() }, - ModelInfo { id: "gemini-3-pro-preview".to_string(), object: "model".to_string(), owned_by: "google".to_string() }, + ModelInfo { + id: "gemini-2.5-flash".to_string(), + object: "model".to_string(), + owned_by: "google".to_string(), + }, + ModelInfo { + id: "gemini-2.5-flash-lite".to_string(), + object: "model".to_string(), + owned_by: "google".to_string(), + }, + ModelInfo { + id: "gemini-2.5-pro".to_string(), + object: "model".to_string(), + owned_by: "google".to_string(), + }, + ModelInfo { + id: "gemini-2.5-pro-preview-06-05".to_string(), + object: "model".to_string(), + owned_by: "google".to_string(), + }, + ModelInfo { + id: "gemini-3-pro-preview".to_string(), + object: "model".to_string(), + owned_by: "google".to_string(), + }, // Qwen models - ModelInfo { id: "qwen3-coder-plus".to_string(), object: "model".to_string(), owned_by: "alibaba".to_string() }, - ModelInfo { id: "qwen3-coder-flash".to_string(), object: "model".to_string(), owned_by: "alibaba".to_string() }, + ModelInfo { + id: "qwen3-coder-plus".to_string(), + object: "model".to_string(), + owned_by: "alibaba".to_string(), + }, + ModelInfo { + id: "qwen3-coder-flash".to_string(), + object: "model".to_string(), + owned_by: "alibaba".to_string(), + }, ]) } @@ -696,7 +860,7 @@ async fn test_api( .build() .map_err(|e| e.to_string())?; - let url = format!("{}{}", base_url, path); + let url = format!("{base_url}{path}"); tracing::info!("Testing API: {} {}", method, url); @@ -711,7 +875,7 @@ async fn test_api( req = req.header("Content-Type", "application/json"); if auth { - req = req.header("Authorization", format!("Bearer {}", api_key)); + req = req.header("Authorization", format!("Bearer {api_key}")); } if let Some(b) = body { @@ -724,10 +888,14 @@ async fn test_api( let body = resp.text().await.unwrap_or_default(); let time_ms = start.elapsed().as_millis() as u64; - tracing::info!("API test result: status={}, body_len={}", status, body.len()); + tracing::info!( + "API test result: status={}, body_len={}", + status, + body.len() + ); Ok(TestResult { - success: status >= 200 && status < 300, + success: (200..300).contains(&status), status, body, time_ms, diff --git a/src-tauri/src/logger.rs b/src-tauri/src/logger.rs index f332c16f6..747175d21 100644 --- a/src-tauri/src/logger.rs +++ b/src-tauri/src/logger.rs @@ -1,8 +1,8 @@ //! 日志管理模块 +use chrono::Utc; use serde::{Deserialize, Serialize}; use std::sync::Arc; use tokio::sync::RwLock; -use chrono::Utc; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LogEntry { @@ -16,13 +16,19 @@ pub struct LogStore { max_logs: usize, } -impl LogStore { - pub fn new() -> Self { +impl Default for LogStore { + fn default() -> Self { Self { logs: Vec::new(), max_logs: 1000, } } +} + +impl LogStore { + pub fn new() -> Self { + Self::default() + } pub fn add(&mut self, level: &str, message: &str) { let entry = LogEntry { @@ -30,9 +36,9 @@ impl LogStore { level: level.to_string(), message: message.to_string(), }; - + self.logs.push(entry); - + // 保持日志数量在限制内 if self.logs.len() > self.max_logs { self.logs.remove(0); @@ -48,4 +54,5 @@ impl LogStore { } } +#[allow(dead_code)] pub type SharedLogStore = Arc>; diff --git a/src-tauri/src/models/anthropic.rs b/src-tauri/src/models/anthropic.rs index 0a4d1a14b..3991e7c88 100644 --- a/src-tauri/src/models/anthropic.rs +++ b/src-tauri/src/models/anthropic.rs @@ -18,9 +18,7 @@ pub enum AnthropicContentBlock { content: serde_json::Value, }, #[serde(rename = "image")] - Image { - source: ImageSource, - }, + Image { source: ImageSource }, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -89,13 +87,19 @@ pub enum AnthropicStreamEvent { #[serde(rename = "message_start")] MessageStart { message: AnthropicMessageStart }, #[serde(rename = "content_block_start")] - ContentBlockStart { index: u32, content_block: AnthropicContentBlock }, + ContentBlockStart { + index: u32, + content_block: AnthropicContentBlock, + }, #[serde(rename = "content_block_delta")] ContentBlockDelta { index: u32, delta: AnthropicDelta }, #[serde(rename = "content_block_stop")] ContentBlockStop { index: u32 }, #[serde(rename = "message_delta")] - MessageDelta { delta: AnthropicMessageDelta, usage: AnthropicUsage }, + MessageDelta { + delta: AnthropicMessageDelta, + usage: AnthropicUsage, + }, #[serde(rename = "message_stop")] MessageStop, } diff --git a/src-tauri/src/models/codewhisperer.rs b/src-tauri/src/models/codewhisperer.rs index 12fe7b73c..ab97fe5a4 100644 --- a/src-tauri/src/models/codewhisperer.rs +++ b/src-tauri/src/models/codewhisperer.rs @@ -1,6 +1,5 @@ //! CodeWhisperer/Kiro API 数据模型 use serde::{Deserialize, Serialize}; -use std::collections::HashMap; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] diff --git a/src-tauri/src/models/mod.rs b/src-tauri/src/models/mod.rs index 0e6aa6305..6e288c903 100644 --- a/src-tauri/src/models/mod.rs +++ b/src-tauri/src/models/mod.rs @@ -1,7 +1,10 @@ -pub mod openai; pub mod anthropic; pub mod codewhisperer; +pub mod openai; -pub use openai::*; +#[allow(unused_imports)] pub use anthropic::*; +#[allow(unused_imports)] pub use codewhisperer::*; +#[allow(unused_imports)] +pub use openai::*; diff --git a/src-tauri/src/models/openai.rs b/src-tauri/src/models/openai.rs index 55d4d6e4f..66a139bc9 100644 --- a/src-tauri/src/models/openai.rs +++ b/src-tauri/src/models/openai.rs @@ -1,6 +1,5 @@ //! OpenAI API 数据模型 use serde::{Deserialize, Serialize}; -use std::collections::HashMap; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ImageUrl { @@ -54,15 +53,17 @@ impl ChatMessage { pub fn get_content_text(&self) -> String { match &self.content { Some(MessageContent::Text(s)) => s.clone(), - Some(MessageContent::Parts(parts)) => { - parts.iter().filter_map(|p| { + Some(MessageContent::Parts(parts)) => parts + .iter() + .filter_map(|p| { if let ContentPart::Text { text } = p { Some(text.clone()) } else { None } - }).collect::>().join("") - } + }) + .collect::>() + .join(""), None => String::new(), } } diff --git a/src-tauri/src/providers/claude_custom.rs b/src-tauri/src/providers/claude_custom.rs index 7f0a4a50e..6c2e602b6 100644 --- a/src-tauri/src/providers/claude_custom.rs +++ b/src-tauri/src/providers/claude_custom.rs @@ -1,7 +1,7 @@ //! Claude Custom Provider (自定义 Claude API) +use reqwest::Client; use serde::{Deserialize, Serialize}; use std::error::Error; -use reqwest::Client; #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct ClaudeCustomConfig { @@ -15,16 +15,23 @@ pub struct ClaudeCustomProvider { pub client: Client, } -impl ClaudeCustomProvider { - pub fn new() -> Self { +impl Default for ClaudeCustomProvider { + fn default() -> Self { Self { config: ClaudeCustomConfig::default(), client: Client::new(), } } +} + +impl ClaudeCustomProvider { + pub fn new() -> Self { + Self::default() + } pub fn get_base_url(&self) -> String { - self.config.base_url + self.config + .base_url .clone() .unwrap_or_else(|| "https://api.anthropic.com".to_string()) } @@ -37,13 +44,17 @@ impl ClaudeCustomProvider { &self, request: &serde_json::Value, ) -> Result> { - let api_key = self.config.api_key.as_ref() + let api_key = self + .config + .api_key + .as_ref() .ok_or("Claude API key not configured")?; let base_url = self.get_base_url(); - let url = format!("{}/v1/messages", base_url); + let url = format!("{base_url}/v1/messages"); - let resp = self.client + let resp = self + .client .post(&url) .header("x-api-key", api_key) .header("anthropic-version", "2023-06-01") @@ -59,13 +70,17 @@ impl ClaudeCustomProvider { &self, request: &serde_json::Value, ) -> Result> { - let api_key = self.config.api_key.as_ref() + let api_key = self + .config + .api_key + .as_ref() .ok_or("Claude API key not configured")?; let base_url = self.get_base_url(); - let url = format!("{}/v1/messages/count_tokens", base_url); + let url = format!("{base_url}/v1/messages/count_tokens"); - let resp = self.client + let resp = self + .client .post(&url) .header("x-api-key", api_key) .header("anthropic-version", "2023-06-01") @@ -77,7 +92,7 @@ impl ClaudeCustomProvider { if !resp.status().is_success() { let status = resp.status(); let body = resp.text().await.unwrap_or_default(); - return Err(format!("Failed to count tokens: {} - {}", status, body).into()); + return Err(format!("Failed to count tokens: {status} - {body}").into()); } let data: serde_json::Value = resp.json().await?; diff --git a/src-tauri/src/providers/gemini.rs b/src-tauri/src/providers/gemini.rs index 6e38da191..3918ff287 100644 --- a/src-tauri/src/providers/gemini.rs +++ b/src-tauri/src/providers/gemini.rs @@ -1,8 +1,8 @@ //! Gemini CLI OAuth Provider +use reqwest::Client; use serde::{Deserialize, Serialize}; use std::error::Error; use std::path::PathBuf; -use reqwest::Client; // Constants const CODE_ASSIST_ENDPOINT: &str = "https://cloudcode-pa.googleapis.com"; @@ -21,9 +21,10 @@ fn get_oauth_client_secret() -> Option { std::env::var("GEMINI_OAUTH_CLIENT_SECRET").ok() } +#[allow(dead_code)] pub const GEMINI_MODELS: &[&str] = &[ "gemini-2.5-flash", - "gemini-2.5-flash-lite", + "gemini-2.5-flash-lite", "gemini-2.5-pro", "gemini-2.5-pro-preview-06-05", "gemini-2.5-flash-preview-09-2025", @@ -122,14 +123,20 @@ pub struct GeminiProvider { pub client: Client, } -impl GeminiProvider { - pub fn new() -> Self { +impl Default for GeminiProvider { + fn default() -> Self { Self { credentials: GeminiCredentials::default(), project_id: None, client: Client::new(), } } +} + +impl GeminiProvider { + pub fn new() -> Self { + Self::default() + } pub fn default_creds_path() -> PathBuf { dirs::home_dir() @@ -140,13 +147,13 @@ impl GeminiProvider { pub fn load_credentials(&mut self) -> Result<(), Box> { let path = Self::default_creds_path(); - + if path.exists() { let content = std::fs::read_to_string(&path)?; let creds: GeminiCredentials = serde_json::from_str(&content)?; self.credentials = creds; } - + Ok(()) } @@ -173,14 +180,16 @@ impl GeminiProvider { } pub async fn refresh_token(&mut self) -> Result> { - let refresh_token = self.credentials.refresh_token.as_ref() + let refresh_token = self + .credentials + .refresh_token + .as_ref() .ok_or("No refresh token available")?; - let client_id = get_oauth_client_id() - .ok_or("GEMINI_OAUTH_CLIENT_ID not set")?; - let client_secret = get_oauth_client_secret() - .ok_or("GEMINI_OAUTH_CLIENT_SECRET not set")?; - + let client_id = get_oauth_client_id().ok_or("GEMINI_OAUTH_CLIENT_ID not set")?; + let client_secret = + get_oauth_client_secret().ok_or("GEMINI_OAUTH_CLIENT_SECRET not set")?; + let params = [ ("client_id", client_id.as_str()), ("client_secret", client_secret.as_str()), @@ -188,7 +197,8 @@ impl GeminiProvider { ("grant_type", "refresh_token"), ]; - let resp = self.client + let resp = self + .client .post("https://oauth2.googleapis.com/token") .form(¶ms) .send() @@ -197,21 +207,20 @@ impl GeminiProvider { if !resp.status().is_success() { let status = resp.status(); let body = resp.text().await.unwrap_or_default(); - return Err(format!("Token refresh failed: {} - {}", status, body).into()); + return Err(format!("Token refresh failed: {status} - {body}").into()); } let data: serde_json::Value = resp.json().await?; - + let new_token = data["access_token"] .as_str() .ok_or("No access token in response")?; - + self.credentials.access_token = Some(new_token.to_string()); - + if let Some(expires_in) = data["expires_in"].as_i64() { - self.credentials.expiry_date = Some( - chrono::Utc::now().timestamp_millis() + expires_in * 1000 - ); + self.credentials.expiry_date = + Some(chrono::Utc::now().timestamp_millis() + expires_in * 1000); } // Save refreshed credentials @@ -221,7 +230,7 @@ impl GeminiProvider { } pub fn get_api_url(&self, action: &str) -> String { - format!("{}/{}:{}", CODE_ASSIST_ENDPOINT, CODE_ASSIST_API_VERSION, action) + format!("{CODE_ASSIST_ENDPOINT}/{CODE_ASSIST_API_VERSION}:{action}") } pub async fn call_api( @@ -229,14 +238,18 @@ impl GeminiProvider { action: &str, body: &serde_json::Value, ) -> Result> { - let token = self.credentials.access_token.as_ref() + let token = self + .credentials + .access_token + .as_ref() .ok_or("No access token")?; let url = self.get_api_url(action); - let resp = self.client + let resp = self + .client .post(&url) - .header("Authorization", format!("Bearer {}", token)) + .header("Authorization", format!("Bearer {token}")) .header("Content-Type", "application/json") .json(body) .send() @@ -245,7 +258,7 @@ impl GeminiProvider { if !resp.status().is_success() { let status = resp.status(); let body = resp.text().await.unwrap_or_default(); - return Err(format!("API call failed: {} - {}", status, body).into()); + return Err(format!("API call failed: {status} - {body}").into()); } let data: serde_json::Value = resp.json().await?; @@ -268,7 +281,7 @@ impl GeminiProvider { }); let resp = self.call_api("loadCodeAssist", &body).await?; - + if let Some(project) = resp["cloudaicompanionProject"].as_str() { if !project.is_empty() { self.project_id = Some(project.to_string()); @@ -289,7 +302,7 @@ impl GeminiProvider { }); let mut lro_resp = self.call_api("onboardUser", &onboard_body).await?; - + // Poll until done for _ in 0..30 { if lro_resp["done"].as_bool().unwrap_or(false) { diff --git a/src-tauri/src/providers/kiro.rs b/src-tauri/src/providers/kiro.rs index a7ee37d0a..c8386f399 100644 --- a/src-tauri/src/providers/kiro.rs +++ b/src-tauri/src/providers/kiro.rs @@ -1,11 +1,10 @@ //! Kiro/CodeWhisperer Provider +use crate::converter::openai_to_cw::convert_openai_to_codewhisperer; +use crate::models::openai::*; +use reqwest::Client; use serde::{Deserialize, Serialize}; use std::error::Error; use std::path::PathBuf; -use reqwest::Client; -use crate::models::openai::*; -use crate::models::codewhisperer::*; -use crate::converter::openai_to_cw::convert_openai_to_codewhisperer; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -40,14 +39,20 @@ pub struct KiroProvider { pub client: Client, } -impl KiroProvider { - pub fn new() -> Self { +impl Default for KiroProvider { + fn default() -> Self { Self { credentials: KiroCredentials::default(), client: Client::new(), } } - +} + +impl KiroProvider { + pub fn new() -> Self { + Self::default() + } + pub fn default_creds_path() -> PathBuf { dirs::home_dir() .unwrap_or_else(|| PathBuf::from(".")) @@ -56,27 +61,27 @@ impl KiroProvider { .join("cache") .join("kiro-auth-token.json") } - + pub fn load_credentials(&mut self) -> Result<(), Box> { let path = Self::default_creds_path(); let dir = path.parent().unwrap(); - + let mut merged = KiroCredentials::default(); - + // 读取主凭证文件 if path.exists() { let content = std::fs::read_to_string(&path)?; let creds: KiroCredentials = serde_json::from_str(&content)?; merge_credentials(&mut merged, &creds); } - + // 读取目录中其他 JSON 文件 if dir.is_dir() { for entry in std::fs::read_dir(dir)? { let entry = entry?; let file_path = entry.path(); - if file_path.extension().map(|e| e == "json").unwrap_or(false) - && file_path != path { + if file_path.extension().map(|e| e == "json").unwrap_or(false) && file_path != path + { if let Ok(content) = std::fs::read_to_string(&file_path) { if let Ok(creds) = serde_json::from_str::(&content) { merge_credentials(&mut merged, &creds); @@ -85,34 +90,47 @@ impl KiroProvider { } } } - + self.credentials = merged; Ok(()) } - + pub fn get_base_url(&self) -> String { let region = self.credentials.region.as_deref().unwrap_or("us-east-1"); - format!("https://codewhisperer.{}.amazonaws.com/generateAssistantResponse", region) + format!("https://codewhisperer.{region}.amazonaws.com/generateAssistantResponse") } - + pub fn get_refresh_url(&self) -> String { let region = self.credentials.region.as_deref().unwrap_or("us-east-1"); - let auth_method = self.credentials.auth_method.as_deref().unwrap_or("social"); - + let auth_method = self + .credentials + .auth_method + .as_deref() + .unwrap_or("social") + .to_lowercase(); + if auth_method == "idc" { - format!("https://oidc.{}.amazonaws.com/token", region) + format!("https://oidc.{region}.amazonaws.com/token") } else { - format!("https://prod.{}.auth.desktop.kiro.dev/refreshToken", region) + format!("https://prod.{region}.auth.desktop.kiro.dev/refreshToken") } } - + pub async fn refresh_token(&mut self) -> Result> { - let refresh_token = self.credentials.refresh_token.as_ref() + let refresh_token = self + .credentials + .refresh_token + .as_ref() .ok_or("No refresh token")?; - - let auth_method = self.credentials.auth_method.as_deref().unwrap_or("social"); + + let auth_method = self + .credentials + .auth_method + .as_deref() + .unwrap_or("social") + .to_lowercase(); let refresh_url = self.get_refresh_url(); - + let body = if auth_method == "idc" { serde_json::json!({ "refreshToken": refresh_token, @@ -123,60 +141,101 @@ impl KiroProvider { } else { serde_json::json!({ "refreshToken": refresh_token }) }; - - let resp = self.client + + let resp = self + .client .post(&refresh_url) + .header("Content-Type", "application/json") + .header("Accept", "application/json") .json(&body) .send() .await?; - + if !resp.status().is_success() { - return Err(format!("Refresh failed: {}", resp.status()).into()); + let status = resp.status(); + let body_text = resp.text().await.unwrap_or_default(); + return Err(format!("Refresh failed: {status} {body_text}").into()); } - + let data: serde_json::Value = resp.json().await?; let new_token = data["accessToken"] .as_str() .ok_or("No access token in response")?; - + self.credentials.access_token = Some(new_token.to_string()); - + if let Some(rt) = data["refreshToken"].as_str() { self.credentials.refresh_token = Some(rt.to_string()); } if let Some(arn) = data["profileArn"].as_str() { self.credentials.profile_arn = Some(arn.to_string()); } - + + // 保存更新后的凭证到文件 + self.save_credentials()?; + Ok(new_token.to_string()) } - + + pub fn save_credentials(&self) -> Result<(), Box> { + let path = Self::default_creds_path(); + + // 读取现有文件内容 + let mut existing: serde_json::Value = if path.exists() { + let content = std::fs::read_to_string(&path)?; + serde_json::from_str(&content).unwrap_or(serde_json::json!({})) + } else { + serde_json::json!({}) + }; + + // 更新字段 + if let Some(token) = &self.credentials.access_token { + existing["accessToken"] = serde_json::json!(token); + } + if let Some(token) = &self.credentials.refresh_token { + existing["refreshToken"] = serde_json::json!(token); + } + if let Some(arn) = &self.credentials.profile_arn { + existing["profileArn"] = serde_json::json!(arn); + } + + // 写回文件 + let content = serde_json::to_string_pretty(&existing)?; + std::fs::write(&path, content)?; + + Ok(()) + } + pub async fn call_api( &self, request: &ChatCompletionRequest, ) -> Result> { - let token = self.credentials.access_token.as_ref() + let token = self + .credentials + .access_token + .as_ref() .ok_or("No access token")?; - + let profile_arn = if self.credentials.auth_method.as_deref() == Some("social") { self.credentials.profile_arn.clone() } else { None }; - + let cw_request = convert_openai_to_codewhisperer(request, profile_arn); let url = self.get_base_url(); - - let resp = self.client + + let resp = self + .client .post(&url) - .header("Authorization", format!("Bearer {}", token)) + .header("Authorization", format!("Bearer {token}")) .header("Content-Type", "application/json") .header("Accept", "application/json") .header("amz-sdk-invocation-id", uuid::Uuid::new_v4().to_string()) .json(&cw_request) .send() .await?; - + Ok(resp) } } diff --git a/src-tauri/src/providers/mod.rs b/src-tauri/src/providers/mod.rs index 58a68db69..441186c95 100644 --- a/src-tauri/src/providers/mod.rs +++ b/src-tauri/src/providers/mod.rs @@ -1,11 +1,16 @@ -pub mod kiro; -pub mod gemini; -pub mod qwen; -pub mod openai_custom; pub mod claude_custom; +pub mod gemini; +pub mod kiro; +pub mod openai_custom; +pub mod qwen; -pub use kiro::KiroProvider; -pub use gemini::GeminiProvider; -pub use qwen::QwenProvider; -pub use openai_custom::OpenAICustomProvider; +#[allow(unused_imports)] pub use claude_custom::ClaudeCustomProvider; +#[allow(unused_imports)] +pub use gemini::GeminiProvider; +#[allow(unused_imports)] +pub use kiro::KiroProvider; +#[allow(unused_imports)] +pub use openai_custom::OpenAICustomProvider; +#[allow(unused_imports)] +pub use qwen::QwenProvider; diff --git a/src-tauri/src/providers/openai_custom.rs b/src-tauri/src/providers/openai_custom.rs index df603fd30..c9ea3ad31 100644 --- a/src-tauri/src/providers/openai_custom.rs +++ b/src-tauri/src/providers/openai_custom.rs @@ -1,7 +1,7 @@ //! OpenAI Custom Provider (自定义 OpenAI 兼容 API) +use reqwest::Client; use serde::{Deserialize, Serialize}; use std::error::Error; -use reqwest::Client; #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct OpenAICustomConfig { @@ -15,16 +15,23 @@ pub struct OpenAICustomProvider { pub client: Client, } -impl OpenAICustomProvider { - pub fn new() -> Self { +impl Default for OpenAICustomProvider { + fn default() -> Self { Self { config: OpenAICustomConfig::default(), client: Client::new(), } } +} + +impl OpenAICustomProvider { + pub fn new() -> Self { + Self::default() + } pub fn get_base_url(&self) -> String { - self.config.base_url + self.config + .base_url .clone() .unwrap_or_else(|| "https://api.openai.com/v1".to_string()) } @@ -37,15 +44,19 @@ impl OpenAICustomProvider { &self, request: &serde_json::Value, ) -> Result> { - let api_key = self.config.api_key.as_ref() + let api_key = self + .config + .api_key + .as_ref() .ok_or("OpenAI API key not configured")?; let base_url = self.get_base_url(); - let url = format!("{}/chat/completions", base_url); + let url = format!("{base_url}/chat/completions"); - let resp = self.client + let resp = self + .client .post(&url) - .header("Authorization", format!("Bearer {}", api_key)) + .header("Authorization", format!("Bearer {api_key}")) .header("Content-Type", "application/json") .json(request) .send() @@ -55,22 +66,26 @@ impl OpenAICustomProvider { } pub async fn list_models(&self) -> Result> { - let api_key = self.config.api_key.as_ref() + let api_key = self + .config + .api_key + .as_ref() .ok_or("OpenAI API key not configured")?; let base_url = self.get_base_url(); - let url = format!("{}/models", base_url); + let url = format!("{base_url}/models"); - let resp = self.client + let resp = self + .client .get(&url) - .header("Authorization", format!("Bearer {}", api_key)) + .header("Authorization", format!("Bearer {api_key}")) .send() .await?; if !resp.status().is_success() { let status = resp.status(); let body = resp.text().await.unwrap_or_default(); - return Err(format!("Failed to list models: {} - {}", status, body).into()); + return Err(format!("Failed to list models: {status} - {body}").into()); } let data: serde_json::Value = resp.json().await?; diff --git a/src-tauri/src/providers/qwen.rs b/src-tauri/src/providers/qwen.rs index fcf858697..008f5f36e 100644 --- a/src-tauri/src/providers/qwen.rs +++ b/src-tauri/src/providers/qwen.rs @@ -1,18 +1,15 @@ //! Qwen (通义千问) OAuth Provider +use reqwest::Client; use serde::{Deserialize, Serialize}; use std::error::Error; use std::path::PathBuf; -use reqwest::Client; // Constants const QWEN_DIR: &str = ".qwen"; const CREDENTIALS_FILE: &str = "oauth_creds.json"; const QWEN_BASE_URL: &str = "https://portal.qwen.ai/v1"; -pub const QWEN_MODELS: &[&str] = &[ - "qwen3-coder-plus", - "qwen3-coder-flash", -]; +pub const QWEN_MODELS: &[&str] = &["qwen3-coder-plus", "qwen3-coder-flash"]; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct QwenCredentials { @@ -40,13 +37,19 @@ pub struct QwenProvider { pub client: Client, } -impl QwenProvider { - pub fn new() -> Self { +impl Default for QwenProvider { + fn default() -> Self { Self { credentials: QwenCredentials::default(), client: Client::new(), } } +} + +impl QwenProvider { + pub fn new() -> Self { + Self::default() + } pub fn default_creds_path() -> PathBuf { dirs::home_dir() @@ -57,13 +60,13 @@ impl QwenProvider { pub fn load_credentials(&mut self) -> Result<(), Box> { let path = Self::default_creds_path(); - + if path.exists() { let content = std::fs::read_to_string(&path)?; let creds: QwenCredentials = serde_json::from_str(&content)?; self.credentials = creds; } - + Ok(()) } @@ -90,25 +93,29 @@ impl QwenProvider { } pub fn get_base_url(&self) -> String { - self.credentials.resource_url + self.credentials + .resource_url .as_ref() .map(|url| { let normalized = if url.starts_with("http") { url.clone() } else { - format!("https://{}", url) + format!("https://{url}") }; if normalized.ends_with("/v1") { normalized } else { - format!("{}/v1", normalized) + format!("{normalized}/v1") } }) .unwrap_or_else(|| QWEN_BASE_URL.to_string()) } pub async fn refresh_token(&mut self) -> Result> { - let refresh_token = self.credentials.refresh_token.as_ref() + let refresh_token = self + .credentials + .refresh_token + .as_ref() .ok_or("No refresh token available")?; let client_id = std::env::var("QWEN_OAUTH_CLIENT_ID") @@ -122,7 +129,8 @@ impl QwenProvider { "client_id": client_id }); - let resp = self.client + let resp = self + .client .post("https://chat.qwen.ai/api/v1/oauth2/token") .header("Content-Type", "application/json") .json(&body) @@ -132,29 +140,28 @@ impl QwenProvider { if !resp.status().is_success() { let status = resp.status(); let body = resp.text().await.unwrap_or_default(); - return Err(format!("Token refresh failed: {} - {}", status, body).into()); + return Err(format!("Token refresh failed: {status} - {body}").into()); } let data: serde_json::Value = resp.json().await?; - + let new_token = data["access_token"] .as_str() .ok_or("No access token in response")?; - + self.credentials.access_token = Some(new_token.to_string()); - + if let Some(rt) = data["refresh_token"].as_str() { self.credentials.refresh_token = Some(rt.to_string()); } - + if let Some(resource_url) = data["resource_url"].as_str() { self.credentials.resource_url = Some(resource_url.to_string()); } - + if let Some(expires_in) = data["expires_in"].as_i64() { - self.credentials.expiry_date = Some( - chrono::Utc::now().timestamp_millis() + expires_in * 1000 - ); + self.credentials.expiry_date = + Some(chrono::Utc::now().timestamp_millis() + expires_in * 1000); } // Save refreshed credentials @@ -167,11 +174,14 @@ impl QwenProvider { &self, request: &serde_json::Value, ) -> Result> { - let token = self.credentials.access_token.as_ref() + let token = self + .credentials + .access_token + .as_ref() .ok_or("No access token")?; let base_url = self.get_base_url(); - let url = format!("{}/chat/completions", base_url); + let url = format!("{base_url}/chat/completions"); // Ensure model is valid let mut req_body = request.clone(); @@ -181,9 +191,10 @@ impl QwenProvider { } } - let resp = self.client + let resp = self + .client .post(&url) - .header("Authorization", format!("Bearer {}", token)) + .header("Authorization", format!("Bearer {token}")) .header("Content-Type", "application/json") .header("X-DashScope-AuthType", "qwen-oauth") .json(&req_body) diff --git a/src-tauri/src/server.rs b/src-tauri/src/server.rs index c9b21c8d7..1e5d81cb9 100644 --- a/src-tauri/src/server.rs +++ b/src-tauri/src/server.rs @@ -1,24 +1,24 @@ //! HTTP API 服务器 use crate::config::Config; -use crate::models::openai::*; -use crate::models::anthropic::*; use crate::converter::anthropic_to_openai::convert_anthropic_to_openai; -use crate::providers::kiro::KiroProvider; -use crate::providers::gemini::GeminiProvider; -use crate::providers::qwen::QwenProvider; -use crate::providers::openai_custom::OpenAICustomProvider; -use crate::providers::claude_custom::ClaudeCustomProvider; use crate::logger::LogStore; +use crate::models::anthropic::*; +use crate::models::openai::*; +use crate::providers::claude_custom::ClaudeCustomProvider; +use crate::providers::gemini::GeminiProvider; +use crate::providers::kiro::KiroProvider; +use crate::providers::openai_custom::OpenAICustomProvider; +use crate::providers::qwen::QwenProvider; +use axum::{ + extract::State, + http::{HeaderMap, StatusCode}, + response::{IntoResponse, Response}, + routing::{get, post}, + Json, Router, +}; use serde::{Deserialize, Serialize}; use std::sync::Arc; use tokio::sync::{oneshot, RwLock}; -use axum::{ - routing::{get, post}, - Router, Json, - extract::State, - http::{StatusCode, HeaderMap}, - response::{IntoResponse, Response}, -}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ServerStatus { @@ -46,16 +46,16 @@ impl ServerState { pub fn new(config: Config) -> Self { let mut kiro = KiroProvider::new(); let _ = kiro.load_credentials(); - + let mut gemini = GeminiProvider::new(); let _ = gemini.load_credentials(); - + let mut qwen = QwenProvider::new(); let _ = qwen.load_credentials(); - + let openai_custom = OpenAICustomProvider::new(); let claude_custom = ClaudeCustomProvider::new(); - + Self { config, running: false, @@ -80,7 +80,10 @@ impl ServerState { } } - pub async fn start(&mut self, logs: Arc>) -> Result<(), Box> { + pub async fn start( + &mut self, + logs: Arc>, + ) -> Result<(), Box> { if self.running { return Ok(()); } @@ -91,7 +94,7 @@ impl ServerState { let host = self.config.server.host.clone(); let port = self.config.server.port; let api_key = self.config.server.api_key.clone(); - + // 重新加载凭证 let _ = self.kiro_provider.load_credentials(); let kiro = self.kiro_provider.clone(); @@ -154,9 +157,9 @@ async fn run_server( .route("/v1/messages/count_tokens", post(count_tokens)) .with_state(state); - let addr: std::net::SocketAddr = format!("{}:{}", host, port).parse()?; + let addr: std::net::SocketAddr = format!("{host}:{port}").parse()?; let listener = tokio::net::TcpListener::bind(addr).await?; - + tracing::info!("Server listening on {}", addr); axum::serve(listener, app) @@ -197,27 +200,33 @@ async fn models() -> impl IntoResponse { })) } -async fn verify_api_key(headers: &HeaderMap, expected_key: &str) -> Result<(), (StatusCode, Json)> { - let auth = headers.get("authorization") +async fn verify_api_key( + headers: &HeaderMap, + expected_key: &str, +) -> Result<(), (StatusCode, Json)> { + let auth = headers + .get("authorization") .or_else(|| headers.get("x-api-key")) .and_then(|v| v.to_str().ok()); - + let key = match auth { Some(s) if s.starts_with("Bearer ") => &s[7..], Some(s) => s, - None => return Err(( - StatusCode::UNAUTHORIZED, - Json(serde_json::json!({"error": {"message": "No API key provided"}})) - )), + None => { + return Err(( + StatusCode::UNAUTHORIZED, + Json(serde_json::json!({"error": {"message": "No API key provided"}})), + )) + } }; - + if key != expected_key { return Err(( StatusCode::UNAUTHORIZED, - Json(serde_json::json!({"error": {"message": "Invalid API key"}})) + Json(serde_json::json!({"error": {"message": "Invalid API key"}})), )); } - + Ok(()) } @@ -227,21 +236,32 @@ async fn chat_completions( Json(request): Json, ) -> Response { if let Err(e) = verify_api_key(&headers, &state.api_key).await { - state.logs.write().await.add("warn", "Unauthorized request to /v1/chat/completions"); + state + .logs + .write() + .await + .add("warn", "Unauthorized request to /v1/chat/completions"); return e.into_response(); } - - state.logs.write().await.add("info", &format!("POST /v1/chat/completions model={}", request.model)); - + + state.logs.write().await.add( + "info", + &format!("POST /v1/chat/completions model={}", request.model), + ); + let kiro = state.kiro.read().await; - + match kiro.call_api(&request).await { Ok(resp) => { if resp.status().is_success() { // 解析 CodeWhisperer 响应并转换 match resp.text().await { Ok(body) => { - state.logs.write().await.add("info", "Request completed successfully"); + state + .logs + .write() + .await + .add("info", "Request completed successfully"); let response = serde_json::json!({ "id": format!("chatcmpl-{}", uuid::Uuid::new_v4()), "object": "chat.completion", @@ -268,8 +288,9 @@ async fn chat_completions( } Err(e) => ( StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({"error": {"message": e.to_string()}})) - ).into_response() + Json(serde_json::json!({"error": {"message": e.to_string()}})), + ) + .into_response(), } } else { let status = resp.status(); @@ -282,8 +303,9 @@ async fn chat_completions( } Err(e) => ( StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({"error": {"message": e.to_string()}})) - ).into_response() + Json(serde_json::json!({"error": {"message": e.to_string()}})), + ) + .into_response(), } } @@ -293,23 +315,34 @@ async fn anthropic_messages( Json(request): Json, ) -> Response { if let Err(e) = verify_api_key(&headers, &state.api_key).await { - state.logs.write().await.add("warn", "Unauthorized request to /v1/messages"); + state + .logs + .write() + .await + .add("warn", "Unauthorized request to /v1/messages"); return e.into_response(); } - - state.logs.write().await.add("info", &format!("POST /v1/messages (Anthropic) model={}", request.model)); - + + state.logs.write().await.add( + "info", + &format!("POST /v1/messages (Anthropic) model={}", request.model), + ); + // 转换为 OpenAI 格式 let openai_request = convert_anthropic_to_openai(&request); let kiro = state.kiro.read().await; - + match kiro.call_api(&openai_request).await { Ok(resp) => { if resp.status().is_success() { match resp.text().await { Ok(body) => { let content = extract_content_from_cw_response(&body); - state.logs.write().await.add("info", "Anthropic request completed successfully"); + state + .logs + .write() + .await + .add("info", "Anthropic request completed successfully"); // 返回 Anthropic 格式响应 let response = serde_json::json!({ "id": format!("msg_{}", uuid::Uuid::new_v4()), @@ -326,17 +359,29 @@ async fn anthropic_messages( Json(response).into_response() } Err(e) => { - state.logs.write().await.add("error", &format!("Response parse error: {}", e)); + state + .logs + .write() + .await + .add("error", &format!("Response parse error: {e}")); ( StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({"error": {"message": e.to_string()}})) - ).into_response() + Json(serde_json::json!({"error": {"message": e.to_string()}})), + ) + .into_response() } } } else { let status = resp.status(); let body = resp.text().await.unwrap_or_default(); - state.logs.write().await.add("error", &format!("Upstream error {}: {}", status, &body[..body.len().min(200)])); + state.logs.write().await.add( + "error", + &format!( + "Upstream error {}: {}", + status, + &body[..body.len().min(200)] + ), + ); ( StatusCode::from_u16(status.as_u16()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR), Json(serde_json::json!({"error": {"message": format!("Upstream error: {}", body)}})) @@ -344,11 +389,16 @@ async fn anthropic_messages( } } Err(e) => { - state.logs.write().await.add("error", &format!("API call failed: {}", e)); + state + .logs + .write() + .await + .add("error", &format!("API call failed: {e}")); ( StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({"error": {"message": e.to_string()}})) - ).into_response() + Json(serde_json::json!({"error": {"message": e.to_string()}})), + ) + .into_response() } } } @@ -361,26 +411,28 @@ async fn count_tokens( if let Err(e) = verify_api_key(&headers, &state.api_key).await { return e.into_response(); } - + // Claude Code 需要这个端点,返回估算值 Json(serde_json::json!({ "input_tokens": 100 - })).into_response() + })) + .into_response() } fn extract_content_from_cw_response(body: &str) -> String { // CodeWhisperer 返回 AWS Event Stream 格式 // 使用正则提取 JSON 内容 let mut content = String::new(); - + // 查找所有 {"content":"..."} 模式 let re = regex::Regex::new(r#"\{"content":"([^"\\]*(\\.[^"\\]*)*)"\}"#).ok(); - + if let Some(re) = re { for cap in re.captures_iter(body) { if let Some(text) = cap.get(1) { // 处理转义字符 - let unescaped = text.as_str() + let unescaped = text + .as_str() .replace("\\n", "\n") .replace("\\t", "\t") .replace("\\\"", "\"") @@ -389,7 +441,7 @@ fn extract_content_from_cw_response(body: &str) -> String { } } } - + if content.is_empty() { // 备用方案:查找 assistantResponseEvent if let Some(start) = body.find(r#""content":""#) { @@ -399,7 +451,7 @@ fn extract_content_from_cw_response(body: &str) -> String { } } } - + if content.is_empty() { "Response received but could not parse content".to_string() } else {