diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 818150cde..d42c188d9 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -6672,6 +6672,7 @@ dependencies = [ "proxycast-core", "proxycast-infra", "proxycast-providers", + "proxycast-services", "rand 0.8.5", "regex", "reqwest 0.12.28", @@ -6808,6 +6809,44 @@ dependencies = [ "uuid", ] +[[package]] +name = "proxycast-services" +version = "0.60.0" +dependencies = [ + "anyhow", + "aster", + "async-trait", + "base64 0.22.1", + "chrono", + "dashmap 5.5.3", + "dirs 5.0.1", + "glob", + "indexmap 2.13.0", + "md5", + "once_cell", + "parking_lot", + "proptest", + "proxycast-core", + "proxycast-providers", + "regex", + "reqwest 0.12.28", + "rusqlite", + "serde", + "serde_json", + "serde_yaml", + "sha2", + "sysinfo", + "tempfile", + "thiserror 1.0.69", + "tokio", + "tracing", + "url", + "urlencoding", + "uuid", + "whoami", + "zip", +] + [[package]] name = "psl-types" version = "2.0.11" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 28e8b2502..5e370544f 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -14,6 +14,7 @@ homepage = "https://github.com/aiclientproxy/proxycast" proxycast-core = { path = "crates/core" } proxycast-infra = { path = "crates/infra" } proxycast-providers = { path = "crates/providers" } +proxycast-services = { path = "crates/services" } voice-core = { path = "crates/voice-core" } # 序列化 @@ -187,6 +188,7 @@ tauri-build.workspace = true proxycast-core.workspace = true proxycast-infra.workspace = true proxycast-providers.workspace = true +proxycast-services.workspace = true voice-core.workspace = true # Tauri diff --git a/src-tauri/crates/core/src/models/client_type.rs b/src-tauri/crates/core/src/models/client_type.rs new file mode 100644 index 000000000..8bcf7a486 --- /dev/null +++ b/src-tauri/crates/core/src/models/client_type.rs @@ -0,0 +1,164 @@ +//! 客户端类型检测模块 +//! +//! 通过解析 HTTP 请求的 User-Agent 头来识别客户端类型。 + +#![allow(dead_code)] + +use serde::{Deserialize, Serialize}; + +/// 客户端类型枚举 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ClientType { + /// Cursor 编辑器 + Cursor, + /// Claude Code 客户端 + ClaudeCode, + /// OpenAI Codex CLI + Codex, + /// Windsurf 编辑器 + Windsurf, + /// Kiro IDE + Kiro, + /// 未识别的客户端 + Other, +} + +impl ClientType { + /// 从 User-Agent 字符串检测客户端类型 + pub fn from_user_agent(user_agent: &str) -> Self { + let ua_lower = user_agent.to_lowercase(); + + if ua_lower.contains("cursor") { + ClientType::Cursor + } else if ua_lower.contains("claude-code") || ua_lower.contains("claude_code") { + ClientType::ClaudeCode + } else if ua_lower.contains("codex") { + ClientType::Codex + } else if ua_lower.contains("windsurf") { + ClientType::Windsurf + } else if ua_lower.contains("kiro") { + ClientType::Kiro + } else { + ClientType::Other + } + } + + /// 获取配置键名 + pub fn config_key(&self) -> &'static str { + match self { + ClientType::Cursor => "cursor", + ClientType::ClaudeCode => "claude_code", + ClientType::Codex => "codex", + ClientType::Windsurf => "windsurf", + ClientType::Kiro => "kiro", + ClientType::Other => "other", + } + } + + /// 获取所有客户端类型 + pub fn all() -> &'static [ClientType] { + &[ + ClientType::Cursor, + ClientType::ClaudeCode, + ClientType::Codex, + ClientType::Windsurf, + ClientType::Kiro, + ClientType::Other, + ] + } + + /// 从配置键名解析客户端类型 + pub fn from_config_key(key: &str) -> Option { + match key { + "cursor" => Some(ClientType::Cursor), + "claude_code" => Some(ClientType::ClaudeCode), + "codex" => Some(ClientType::Codex), + "windsurf" => Some(ClientType::Windsurf), + "kiro" => Some(ClientType::Kiro), + "other" => Some(ClientType::Other), + _ => None, + } + } +} + +impl std::fmt::Display for ClientType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.config_key()) + } +} + +/// 根据客户端类型和端点配置选择 Provider +pub fn select_provider( + _client_type: ClientType, + endpoint_provider: Option<&String>, + default_provider: &str, +) -> String { + match endpoint_provider { + Some(provider) => provider.clone(), + None => default_provider.to_string(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_from_user_agent_cursor() { + assert_eq!( + ClientType::from_user_agent("Cursor/1.0"), + ClientType::Cursor + ); + assert_eq!(ClientType::from_user_agent("cursor"), ClientType::Cursor); + assert_eq!(ClientType::from_user_agent("CURSOR"), ClientType::Cursor); + } + + #[test] + fn test_from_user_agent_claude_code() { + assert_eq!( + ClientType::from_user_agent("Claude-Code/2.0"), + ClientType::ClaudeCode + ); + assert_eq!( + ClientType::from_user_agent("claude-code"), + ClientType::ClaudeCode + ); + assert_eq!( + ClientType::from_user_agent("claude_code"), + ClientType::ClaudeCode + ); + } + + #[test] + fn test_from_user_agent_other() { + assert_eq!(ClientType::from_user_agent("Unknown"), ClientType::Other); + assert_eq!(ClientType::from_user_agent(""), ClientType::Other); + } + + #[test] + fn test_config_key() { + assert_eq!(ClientType::Cursor.config_key(), "cursor"); + assert_eq!(ClientType::ClaudeCode.config_key(), "claude_code"); + } + + #[test] + fn test_from_config_key() { + assert_eq!( + ClientType::from_config_key("cursor"), + Some(ClientType::Cursor) + ); + assert_eq!(ClientType::from_config_key("invalid"), None); + } + + #[test] + fn test_all_client_types() { + assert_eq!(ClientType::all().len(), 6); + } + + #[test] + fn test_serialization() { + let json = serde_json::to_string(&ClientType::Cursor).unwrap(); + assert_eq!(json, "\"cursor\""); + } +} diff --git a/src-tauri/crates/core/src/models/mod.rs b/src-tauri/crates/core/src/models/mod.rs index a1fe1ab82..cdfb36b71 100644 --- a/src-tauri/crates/core/src/models/mod.rs +++ b/src-tauri/crates/core/src/models/mod.rs @@ -4,6 +4,7 @@ pub mod anthropic; pub mod app_type; +pub mod client_type; pub mod codewhisperer; pub mod injection_types; pub mod kiro_fingerprint; @@ -23,6 +24,7 @@ pub mod vertex_model; #[allow(unused_imports)] pub use anthropic::*; pub use app_type::AppType; +pub use client_type::{select_provider, ClientType}; #[allow(unused_imports)] pub use codewhisperer::*; pub use injection_types::{InjectionMode, InjectionRule}; diff --git a/src-tauri/crates/services/Cargo.toml b/src-tauri/crates/services/Cargo.toml new file mode 100644 index 000000000..14d926eea --- /dev/null +++ b/src-tauri/crates/services/Cargo.toml @@ -0,0 +1,61 @@ +[package] +name = "proxycast-services" +version.workspace = true +edition.workspace = true +authors.workspace = true +repository.workspace = true + +[dependencies] +# 项目内 crate +proxycast-core.workspace = true +proxycast-providers.workspace = true + +# 序列化 +serde.workspace = true +serde_json.workspace = true +serde_yaml.workspace = true + +# 异步运行时 +tokio.workspace = true +async-trait.workspace = true + +# 错误处理 +thiserror.workspace = true +anyhow.workspace = true + +# 日志 +tracing.workspace = true + +# HTTP 客户端 +reqwest.workspace = true + +# 时间和 UUID +chrono.workspace = true +uuid.workspace = true + +# 工具库 +dirs.workspace = true +parking_lot.workspace = true +dashmap.workspace = true +indexmap.workspace = true +glob.workspace = true +sysinfo.workspace = true +whoami.workspace = true +regex.workspace = true +sha2.workspace = true +base64.workspace = true +md5.workspace = true +once_cell.workspace = true +url.workspace = true +urlencoding.workspace = true +zip.workspace = true + +# Aster Agent Framework +aster.workspace = true + +# 数据库 +rusqlite.workspace = true + +[dev-dependencies] +tempfile.workspace = true +proptest.workspace = true diff --git a/src-tauri/src/services/api_key_provider_service.rs b/src-tauri/crates/services/src/api_key_provider_service.rs similarity index 96% rename from src-tauri/src/services/api_key_provider_service.rs rename to src-tauri/crates/services/src/api_key_provider_service.rs index f02a42ffa..e772ac581 100644 --- a/src-tauri/src/services/api_key_provider_service.rs +++ b/src-tauri/crates/services/src/api_key_provider_service.rs @@ -5,15 +5,17 @@ //! **Feature: provider-ui-refactor** //! **Validates: Requirements 7.3, 9.1, 9.2, 9.3** -use crate::database::dao::api_key_provider::{ +use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; +use chrono::Utc; +use proxycast_core::database::dao::api_key_provider::{ ApiKeyEntry, ApiKeyProvider, ApiKeyProviderDao, ApiProviderType, ProviderGroup, ProviderWithKeys, }; -use crate::database::system_providers::{get_system_providers, to_api_key_provider}; -use crate::database::DbConnection; -use crate::models::{CredentialData, CredentialSource, PoolProviderType, ProviderCredential}; -use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; -use chrono::Utc; +use proxycast_core::database::system_providers::{get_system_providers, to_api_key_provider}; +use proxycast_core::database::DbConnection; +use proxycast_core::models::{ + CredentialData, CredentialSource, PoolProviderType, ProviderCredential, +}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use std::collections::HashMap; @@ -247,8 +249,8 @@ impl ApiKeyProviderService { model: &str, prompt: &str, ) -> Result<(String, String), String> { - use crate::models::openai::{ChatCompletionRequest, ChatMessage, MessageContent}; - use crate::providers::openai_custom::OpenAICustomProvider; + use proxycast_core::models::openai::{ChatCompletionRequest, ChatMessage, MessageContent}; + use proxycast_providers::providers::openai_custom::OpenAICustomProvider; let provider = OpenAICustomProvider::with_config(api_key.to_string(), Some(api_host.to_string())); @@ -457,7 +459,7 @@ impl ApiKeyProviderService { /// 检查数据库中是否存在系统 Provider,如果不存在则插入 /// **Validates: Requirements 9.3** pub fn initialize_system_providers(&self, db: &DbConnection) -> Result { - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; let system_providers = get_system_providers(); let mut inserted_count = 0; @@ -487,7 +489,7 @@ impl ApiKeyProviderService { // 首先确保系统 Provider 已初始化 self.initialize_system_providers(db)?; - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; let providers = ApiKeyProviderDao::get_all_providers_with_keys(&conn).map_err(|e| e.to_string())?; @@ -514,7 +516,7 @@ impl ApiKeyProviderService { db: &DbConnection, id: &str, ) -> Result, String> { - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; let provider = ApiKeyProviderDao::get_provider_by_id(&conn, id).map_err(|e| e.to_string())?; @@ -564,7 +566,7 @@ impl ApiKeyProviderService { updated_at: now, }; - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; ApiKeyProviderDao::insert_provider(&conn, &provider).map_err(|e| e.to_string())?; Ok(provider) @@ -586,7 +588,7 @@ impl ApiKeyProviderService { region: Option, custom_models: Option>, ) -> Result { - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; let mut provider = ApiKeyProviderDao::get_provider_by_id(&conn, id) .map_err(|e| e.to_string())? .ok_or_else(|| format!("Provider not found: {id}"))?; @@ -636,7 +638,7 @@ impl ApiKeyProviderService { /// 删除自定义 Provider /// 系统 Provider 不允许删除 pub fn delete_custom_provider(&self, db: &DbConnection, id: &str) -> Result { - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; // 检查是否为系统 Provider let provider = ApiKeyProviderDao::get_provider_by_id(&conn, id) @@ -668,7 +670,7 @@ impl ApiKeyProviderService { provider_id ); - let mut conn = crate::database::lock_db(db)?; + let mut conn = proxycast_core::database::lock_db(db)?; // 使用事务确保操作的原子性 let tx = conn @@ -754,7 +756,7 @@ impl ApiKeyProviderService { /// 删除 API Key pub fn delete_api_key(&self, db: &DbConnection, key_id: &str) -> Result { - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; ApiKeyProviderDao::delete_api_key(&conn, key_id).map_err(|e| e.to_string()) } @@ -765,7 +767,7 @@ impl ApiKeyProviderService { key_id: &str, enabled: bool, ) -> Result { - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; let mut key = ApiKeyProviderDao::get_api_key_by_id(&conn, key_id) .map_err(|e| e.to_string())? .ok_or_else(|| format!("API Key not found: {key_id}"))?; @@ -783,7 +785,7 @@ impl ApiKeyProviderService { key_id: &str, alias: Option, ) -> Result { - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; let mut key = ApiKeyProviderDao::get_api_key_by_id(&conn, key_id) .map_err(|e| e.to_string())? .ok_or_else(|| format!("API Key not found: {key_id}"))?; @@ -803,7 +805,7 @@ impl ApiKeyProviderService { db: &DbConnection, provider_id: &str, ) -> Result, String> { - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; // 获取所有启用的 API Keys let keys = ApiKeyProviderDao::get_enabled_api_keys_by_provider(&conn, provider_id) @@ -836,7 +838,7 @@ impl ApiKeyProviderService { db: &DbConnection, provider_id: &str, ) -> Result, String> { - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; // 获取所有启用的 API Keys let keys = ApiKeyProviderDao::get_enabled_api_keys_by_provider(&conn, provider_id) @@ -865,7 +867,7 @@ impl ApiKeyProviderService { /// 记录 API Key 使用 pub fn record_usage(&self, db: &DbConnection, key_id: &str) -> Result<(), String> { - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; let key = ApiKeyProviderDao::get_api_key_by_id(&conn, key_id) .map_err(|e| e.to_string())? .ok_or_else(|| format!("API Key not found: {key_id}"))?; @@ -881,7 +883,7 @@ impl ApiKeyProviderService { db: &DbConnection, provider_id: &str, ) -> Result, String> { - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; // 获取 Provider 信息 let provider = match ApiKeyProviderDao::get_provider_by_id(&conn, provider_id) @@ -928,7 +930,7 @@ impl ApiKeyProviderService { db: &DbConnection, provider_type: ApiProviderType, ) -> Result, String> { - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; // 获取所有启用的 API Keys(按类型) let keys = ApiKeyProviderDao::get_enabled_api_keys_by_type(&conn, provider_type) @@ -958,7 +960,7 @@ impl ApiKeyProviderService { /// 记录 API Key 错误 pub fn record_error(&self, db: &DbConnection, key_id: &str) -> Result<(), String> { - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; ApiKeyProviderDao::increment_api_key_error(&conn, key_id).map_err(|e| e.to_string()) } @@ -983,13 +985,13 @@ impl ApiKeyProviderService { /// 获取 UI 状态 pub fn get_ui_state(&self, db: &DbConnection, key: &str) -> Result, String> { - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; ApiKeyProviderDao::get_ui_state(&conn, key).map_err(|e| e.to_string()) } /// 设置 UI 状态 pub fn set_ui_state(&self, db: &DbConnection, key: &str, value: &str) -> Result<(), String> { - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; ApiKeyProviderDao::set_ui_state(&conn, key, value).map_err(|e| e.to_string()) } @@ -1000,7 +1002,7 @@ impl ApiKeyProviderService { db: &DbConnection, sort_orders: Vec<(String, i32)>, ) -> Result<(), String> { - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; ApiKeyProviderDao::update_provider_sort_orders(&conn, &sort_orders) .map_err(|e| e.to_string()) } @@ -1013,7 +1015,7 @@ impl ApiKeyProviderService { db: &DbConnection, include_keys: bool, ) -> Result { - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; let providers = ApiKeyProviderDao::get_all_providers_with_keys(&conn).map_err(|e| e.to_string())?; @@ -1073,7 +1075,7 @@ impl ApiKeyProviderService { .as_array() .ok_or_else(|| "配置格式错误: 缺少 providers 数组".to_string())?; - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; let mut imported_providers = 0; let mut skipped_providers = 0; let mut errors = Vec::new(); @@ -1145,7 +1147,7 @@ impl ApiKeyProviderService { db: &DbConnection, pool_type: &PoolProviderType, provider_id_hint: Option<&str>, - client_type: Option<&crate::server::client_detector::ClientType>, + client_type: Option<&proxycast_core::models::client_type::ClientType>, ) -> Result, String> { eprintln!( "[get_fallback_credential] 开始查找: pool_type={pool_type:?}, provider_id_hint={provider_id_hint:?}" @@ -1222,7 +1224,7 @@ impl ApiKeyProviderService { pool_type: &PoolProviderType, api_type: &ApiProviderType, ) -> Result, String> { - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; // 查找该类型的启用的 Provider(按 sort_order 排序) let providers = ApiKeyProviderDao::get_all_providers(&conn).map_err(|e| e.to_string())?; @@ -1288,11 +1290,11 @@ impl ApiKeyProviderService { &self, db: &DbConnection, provider_id: &str, - client_type: Option<&crate::server::client_detector::ClientType>, + client_type: Option<&proxycast_core::models::client_type::ClientType>, ) -> Result, String> { // First, get all data we need while holding the lock let (provider, keys) = { - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; // 直接按 provider_id 查找 let provider = ApiKeyProviderDao::get_provider_by_id(&conn, provider_id) @@ -1359,7 +1361,7 @@ impl ApiKeyProviderService { // 对于 Claude Code 客户端,可以使用任何 Claude 凭证 if matches!( client, - crate::server::client_detector::ClientType::ClaudeCode + proxycast_core::models::client_type::ClientType::ClaudeCode ) { selected_key = Some(candidate_key); break; @@ -1680,7 +1682,7 @@ impl ApiKeyProviderService { api_key: &str, api_host: &str, ) -> Result, String> { - use crate::providers::openai_custom::OpenAICustomProvider; + use proxycast_providers::providers::openai_custom::OpenAICustomProvider; let provider = OpenAICustomProvider::with_config(api_key.to_string(), Some(api_host.to_string())); @@ -1725,7 +1727,7 @@ impl ApiKeyProviderService { api_key: &str, api_host: &str, ) -> Result<(), String> { - use crate::providers::claude_custom::ClaudeCustomProvider; + use proxycast_providers::providers::claude_custom::ClaudeCustomProvider; let provider = ClaudeCustomProvider::with_config(api_key.to_string(), Some(api_host.to_string())); @@ -1764,7 +1766,7 @@ impl ApiKeyProviderService { api_host: &str, model: &str, ) -> Result, String> { - use crate::providers::claude_custom::ClaudeCustomProvider; + use proxycast_providers::providers::claude_custom::ClaudeCustomProvider; let provider = ClaudeCustomProvider::with_config(api_key.to_string(), Some(api_host.to_string())); diff --git a/src-tauri/src/services/aster_session_store.rs b/src-tauri/crates/services/src/aster_session_store.rs similarity index 99% rename from src-tauri/src/services/aster_session_store.rs rename to src-tauri/crates/services/src/aster_session_store.rs index a59baaabe..eea5780da 100644 --- a/src-tauri/src/services/aster_session_store.rs +++ b/src-tauri/crates/services/src/aster_session_store.rs @@ -5,7 +5,6 @@ //! //! 这是应用层接管框架层存储的关键桥接模块。 -use crate::database::DbConnection; use anyhow::{anyhow, Result}; use aster::conversation::message::{Message, MessageContent}; use aster::conversation::Conversation; @@ -17,6 +16,7 @@ use aster::session::{ }; use async_trait::async_trait; use chrono::Utc; +use proxycast_core::database::DbConnection; use std::collections::HashMap; use std::fs; use std::path::PathBuf; diff --git a/src-tauri/src/services/backup_service.rs b/src-tauri/crates/services/src/backup_service.rs similarity index 98% rename from src-tauri/src/services/backup_service.rs rename to src-tauri/crates/services/src/backup_service.rs index c94b096bd..20da3b82d 100644 --- a/src-tauri/src/services/backup_service.rs +++ b/src-tauri/crates/services/src/backup_service.rs @@ -4,8 +4,8 @@ #![allow(dead_code)] -use crate::database::{get_db_path, DbConnection}; use chrono::{DateTime, Duration, Utc}; +use proxycast_core::database::{get_db_path, DbConnection}; use rusqlite::DatabaseName; use std::path::{Path, PathBuf}; diff --git a/src-tauri/src/services/content_creator/README.md b/src-tauri/crates/services/src/content_creator/README.md similarity index 100% rename from src-tauri/src/services/content_creator/README.md rename to src-tauri/crates/services/src/content_creator/README.md diff --git a/src-tauri/src/services/content_creator/mod.rs b/src-tauri/crates/services/src/content_creator/mod.rs similarity index 100% rename from src-tauri/src/services/content_creator/mod.rs rename to src-tauri/crates/services/src/content_creator/mod.rs diff --git a/src-tauri/src/services/content_creator/progress_store.rs b/src-tauri/crates/services/src/content_creator/progress_store.rs similarity index 100% rename from src-tauri/src/services/content_creator/progress_store.rs rename to src-tauri/crates/services/src/content_creator/progress_store.rs diff --git a/src-tauri/src/services/content_creator/step_executor.rs b/src-tauri/crates/services/src/content_creator/step_executor.rs similarity index 100% rename from src-tauri/src/services/content_creator/step_executor.rs rename to src-tauri/crates/services/src/content_creator/step_executor.rs diff --git a/src-tauri/src/services/content_creator/types.rs b/src-tauri/crates/services/src/content_creator/types.rs similarity index 100% rename from src-tauri/src/services/content_creator/types.rs rename to src-tauri/crates/services/src/content_creator/types.rs diff --git a/src-tauri/src/services/content_creator/workflow_service.rs b/src-tauri/crates/services/src/content_creator/workflow_service.rs similarity index 100% rename from src-tauri/src/services/content_creator/workflow_service.rs rename to src-tauri/crates/services/src/content_creator/workflow_service.rs diff --git a/src-tauri/src/services/context_memory_service.rs b/src-tauri/crates/services/src/context_memory_service.rs similarity index 100% rename from src-tauri/src/services/context_memory_service.rs rename to src-tauri/crates/services/src/context_memory_service.rs diff --git a/src-tauri/src/services/general_chat/README.md b/src-tauri/crates/services/src/general_chat/README.md similarity index 100% rename from src-tauri/src/services/general_chat/README.md rename to src-tauri/crates/services/src/general_chat/README.md diff --git a/src-tauri/src/services/general_chat/mod.rs b/src-tauri/crates/services/src/general_chat/mod.rs similarity index 100% rename from src-tauri/src/services/general_chat/mod.rs rename to src-tauri/crates/services/src/general_chat/mod.rs diff --git a/src-tauri/src/services/general_chat/session_service.rs b/src-tauri/crates/services/src/general_chat/session_service.rs similarity index 99% rename from src-tauri/src/services/general_chat/session_service.rs rename to src-tauri/crates/services/src/general_chat/session_service.rs index cf64f3cee..dfc72e3c4 100644 --- a/src-tauri/src/services/general_chat/session_service.rs +++ b/src-tauri/crates/services/src/general_chat/session_service.rs @@ -186,7 +186,7 @@ impl SessionService { #[cfg(test)] mod tests { use super::*; - use crate::services::general_chat::MessageRole; + use crate::general_chat::MessageRole; #[test] fn test_create_session_with_name() { diff --git a/src-tauri/src/services/kiro_event_service.rs b/src-tauri/crates/services/src/kiro_event_service.rs similarity index 99% rename from src-tauri/src/services/kiro_event_service.rs rename to src-tauri/crates/services/src/kiro_event_service.rs index 49323af20..9617cd1dc 100644 --- a/src-tauri/src/services/kiro_event_service.rs +++ b/src-tauri/crates/services/src/kiro_event_service.rs @@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; use tokio::sync::{broadcast, RwLock}; -use crate::websocket::{KiroTokenInfo, WsKiroEvent}; +use proxycast_core::websocket::{KiroTokenInfo, WsKiroEvent}; /// Kiro 事件服务 #[derive(Debug)] diff --git a/src-tauri/crates/services/src/lib.rs b/src-tauri/crates/services/src/lib.rs new file mode 100644 index 000000000..ee4160962 --- /dev/null +++ b/src-tauri/crates/services/src/lib.rs @@ -0,0 +1,76 @@ +//! ProxyCast Services Crate +//! +//! 业务服务层,包含所有不依赖 Tauri 的业务逻辑。 +//! +//! ## 模块结构 +//! - `context_memory_service` - 上下文记忆服务 +//! - `file_browser_service` - 文件浏览服务 +//! - `sysinfo_service` - 系统信息服务 +//! - `update_check_service` - 更新检查服务 +//! - `update_window` - 更新窗口 +//! - `usage_service` - 使用统计服务 +//! - `machine_id_service` - 机器 ID 服务 +//! - `live_sync` - 实时同步 +//! - `mcp_sync` - MCP 同步 +//! - `prompt_sync` - Prompt 同步 +//! - `skill_service` - 技能服务 +//! - `backup_service` - 备份服务 +//! - `material_service` - 素材服务 +//! - `persona_service` - 人设服务 +//! - `template_service` - 模板服务 +//! - `model_registry_service` - 模型注册服务 +//! - `model_service` - 模型服务 +//! - `prompt_service` - Prompt 服务 +//! - `mcp_service` - MCP 服务 +//! - `switch` - Provider 切换 +//! - `aster_session_store` - Aster 会话存储 +//! - `general_chat` - 通用聊天 +//! - `content_creator` - 内容创作 +//! - `session_context_service` - 会话上下文服务 +//! - `project_context_builder` - 项目上下文构建器 +//! - `tool_hooks_service` - 工具钩子服务 +//! - `kiro_event_service` - Kiro 事件服务 +//! - `api_key_provider_service` - API Key Provider 服务 +//! - `provider_pool_service` - Provider 池服务 +//! - `token_cache_service` - Token 缓存服务 + +// 无外部依赖的服务 +pub mod context_memory_service; +pub mod update_check_service; +pub mod usage_service; + +// 依赖 models 的服务 +pub mod live_sync; +pub mod machine_id_service; +pub mod mcp_sync; +pub mod prompt_sync; +pub mod skill_service; + +// 依赖 database + models 的服务 +pub mod aster_session_store; +pub mod backup_service; +pub mod material_service; +pub mod mcp_service; +pub mod model_registry_service; +pub mod model_service; +pub mod persona_service; +pub mod prompt_service; +pub mod switch; +pub mod template_service; + +// 子模块 +pub mod content_creator; +pub mod general_chat; + +// 依赖其他 services 的服务 +pub mod project_context_builder; +pub mod session_context_service; +pub mod tool_hooks_service; + +// 事件服务 +pub mod kiro_event_service; + +// 依赖 providers 的服务 +pub mod api_key_provider_service; +pub mod provider_pool_service; +pub mod token_cache_service; diff --git a/src-tauri/src/services/live_sync.rs b/src-tauri/crates/services/src/live_sync.rs similarity index 99% rename from src-tauri/src/services/live_sync.rs rename to src-tauri/crates/services/src/live_sync.rs index 8808b1137..abf3c3616 100644 --- a/src-tauri/src/services/live_sync.rs +++ b/src-tauri/crates/services/src/live_sync.rs @@ -1,4 +1,4 @@ -use crate::models::{AppType, Provider}; +use proxycast_core::models::{AppType, Provider}; use serde_json::{json, Value}; use std::fs; use std::io::Write; diff --git a/src-tauri/src/services/live_sync_tests.rs b/src-tauri/crates/services/src/live_sync_tests.rs similarity index 100% rename from src-tauri/src/services/live_sync_tests.rs rename to src-tauri/crates/services/src/live_sync_tests.rs diff --git a/src-tauri/src/services/machine_id_service.rs b/src-tauri/crates/services/src/machine_id_service.rs similarity index 99% rename from src-tauri/src/services/machine_id_service.rs rename to src-tauri/crates/services/src/machine_id_service.rs index e6fed6a48..d3719ad1b 100644 --- a/src-tauri/src/services/machine_id_service.rs +++ b/src-tauri/crates/services/src/machine_id_service.rs @@ -1,7 +1,7 @@ #![allow(dead_code)] -use crate::models::machine_id::*; use dirs; +use proxycast_core::models::machine_id::*; use serde_json; use std::fs; use std::path::PathBuf; diff --git a/src-tauri/src/services/material_service.rs b/src-tauri/crates/services/src/material_service.rs similarity index 99% rename from src-tauri/src/services/material_service.rs rename to src-tauri/crates/services/src/material_service.rs index 28d55c2f0..ad9eded72 100644 --- a/src-tauri/src/services/material_service.rs +++ b/src-tauri/crates/services/src/material_service.rs @@ -20,9 +20,9 @@ use rusqlite::Connection; use tracing::{debug, error, info, warn}; use uuid::Uuid; -use crate::database::dao::material_dao::MaterialDao; -use crate::errors::project_error::MaterialError; -use crate::models::project_model::{ +use proxycast_core::database::dao::material_dao::MaterialDao; +use proxycast_core::errors::project_error::MaterialError; +use proxycast_core::models::project_model::{ Material, MaterialFilter, MaterialUpdate, UploadMaterialRequest, }; @@ -619,7 +619,7 @@ impl MaterialService { #[cfg(test)] mod tests { use super::*; - use crate::database::schema::create_tables; + use proxycast_core::database::schema::create_tables; use std::io::Write; use tempfile::TempDir; diff --git a/src-tauri/src/services/mcp_service.rs b/src-tauri/crates/services/src/mcp_service.rs similarity index 96% rename from src-tauri/src/services/mcp_service.rs rename to src-tauri/crates/services/src/mcp_service.rs index 399a44339..3ec1dd336 100644 --- a/src-tauri/src/services/mcp_service.rs +++ b/src-tauri/crates/services/src/mcp_service.rs @@ -1,8 +1,8 @@ -use crate::database::dao::mcp::McpDao; -use crate::database::DbConnection; -use crate::models::mcp_model::ConfigValidationError; -use crate::models::{AppType, McpServer}; -use crate::services::mcp_sync; +use crate::mcp_sync; +use proxycast_core::database::dao::mcp::McpDao; +use proxycast_core::database::DbConnection; +use proxycast_core::models::mcp_model::ConfigValidationError; +use proxycast_core::models::{AppType, McpServer}; pub struct McpService; diff --git a/src-tauri/src/services/mcp_sync.rs b/src-tauri/crates/services/src/mcp_sync.rs similarity index 96% rename from src-tauri/src/services/mcp_sync.rs rename to src-tauri/crates/services/src/mcp_sync.rs index ca5cd48c0..e06e8e8d4 100644 --- a/src-tauri/src/services/mcp_sync.rs +++ b/src-tauri/crates/services/src/mcp_sync.rs @@ -1,4 +1,4 @@ -use crate::models::{AppType, McpServer}; +use proxycast_core::models::{AppType, McpServer}; use serde_json::{json, Map, Value}; use std::path::PathBuf; @@ -361,7 +361,7 @@ pub fn remove_mcp_from_all_apps( /// Import MCP servers from Claude's ~/.claude/settings.json pub fn import_mcp_from_claude( -) -> Result, Box> { +) -> Result, Box> { let home = dirs::home_dir().ok_or("Cannot find home directory")?; let config_path = home.join(".claude").join("settings.json"); @@ -376,7 +376,7 @@ pub fn import_mcp_from_claude( if let Some(mcp_servers) = settings.get("mcpServers").and_then(|v| v.as_object()) { for (id, config) in mcp_servers { - let server = crate::models::McpServer { + let server = proxycast_core::models::McpServer { id: id.clone(), name: id.clone(), server_config: config.clone(), @@ -396,7 +396,7 @@ pub fn import_mcp_from_claude( /// Import MCP servers from Codex's config.toml pub fn import_mcp_from_codex( -) -> Result, Box> { +) -> Result, Box> { let home = dirs::home_dir().ok_or("Cannot find home directory")?; let config_path = home.join(".codex").join("config.toml"); @@ -421,7 +421,7 @@ pub fn import_mcp_from_codex( if !current_env.is_empty() { current_config.insert("env".to_string(), Value::Object(current_env.clone())); } - let server = crate::models::McpServer { + let server = proxycast_core::models::McpServer { id: id.clone(), name: id.clone(), server_config: Value::Object(current_config.clone()), @@ -455,7 +455,7 @@ pub fn import_mcp_from_codex( if !current_env.is_empty() { current_config.insert("env".to_string(), Value::Object(current_env.clone())); } - let server = crate::models::McpServer { + let server = proxycast_core::models::McpServer { id: id.clone(), name: id.clone(), server_config: Value::Object(current_config.clone()), @@ -503,7 +503,7 @@ pub fn import_mcp_from_codex( if !current_env.is_empty() { current_config.insert("env".to_string(), Value::Object(current_env)); } - let server = crate::models::McpServer { + let server = proxycast_core::models::McpServer { id: id.clone(), name: id.clone(), server_config: Value::Object(current_config), @@ -522,7 +522,7 @@ pub fn import_mcp_from_codex( /// Import MCP servers from Gemini's settings.json pub fn import_mcp_from_gemini( -) -> Result, Box> { +) -> Result, Box> { let home = dirs::home_dir().ok_or("Cannot find home directory")?; let settings_path = home.join(".gemini").join("settings.json"); @@ -537,7 +537,7 @@ pub fn import_mcp_from_gemini( if let Some(mcp_servers) = settings.get("mcpServers").and_then(|v| v.as_object()) { for (id, config) in mcp_servers { - let server = crate::models::McpServer { + let server = proxycast_core::models::McpServer { id: id.clone(), name: id.clone(), server_config: config.clone(), @@ -558,7 +558,7 @@ pub fn import_mcp_from_gemini( /// Import MCP servers from a specific app pub fn import_mcp_from_app( app_type: &AppType, -) -> Result, Box> { +) -> Result, Box> { match app_type { AppType::Claude => import_mcp_from_claude(), AppType::Codex => import_mcp_from_codex(), diff --git a/src-tauri/src/services/model_registry_service.rs b/src-tauri/crates/services/src/model_registry_service.rs similarity index 99% rename from src-tauri/src/services/model_registry_service.rs rename to src-tauri/crates/services/src/model_registry_service.rs index 5cf3628bf..5ee685c7c 100644 --- a/src-tauri/src/services/model_registry_service.rs +++ b/src-tauri/crates/services/src/model_registry_service.rs @@ -3,8 +3,8 @@ //! 从内嵌资源加载模型数据,管理本地缓存,提供模型搜索等功能 //! 模型数据在构建时从 aiclientproxy/models 仓库打包进应用 -use crate::database::DbConnection; -use crate::models::model_registry::{ +use proxycast_core::database::DbConnection; +use proxycast_core::models::model_registry::{ EnhancedModelMetadata, ModelCapabilities, ModelLimits, ModelPricing, ModelSource, ModelStatus, ModelSyncState, ModelTier, ProviderAliasConfig, UserModelPreference, }; diff --git a/src-tauri/src/services/model_service.rs b/src-tauri/crates/services/src/model_service.rs similarity index 98% rename from src-tauri/src/services/model_service.rs rename to src-tauri/crates/services/src/model_service.rs index f909e4a5a..517ed9ab1 100644 --- a/src-tauri/src/services/model_service.rs +++ b/src-tauri/crates/services/src/model_service.rs @@ -2,9 +2,11 @@ //! //! 提供统一的模型获取、缓存和查询接口,支持从不同 Provider 获取模型列表。 -use crate::database::dao::provider_pool::ProviderPoolDao; -use crate::database::DbConnection; -use crate::models::provider_pool_model::{CredentialData, PoolProviderType, ProviderCredential}; +use proxycast_core::database::dao::provider_pool::ProviderPoolDao; +use proxycast_core::database::DbConnection; +use proxycast_core::models::provider_pool_model::{ + CredentialData, PoolProviderType, ProviderCredential, +}; use reqwest::Client; use serde::{Deserialize, Serialize}; use std::collections::HashMap; diff --git a/src-tauri/src/services/persona_service.rs b/src-tauri/crates/services/src/persona_service.rs similarity index 98% rename from src-tauri/src/services/persona_service.rs rename to src-tauri/crates/services/src/persona_service.rs index fc52af373..0db9704ab 100644 --- a/src-tauri/src/services/persona_service.rs +++ b/src-tauri/crates/services/src/persona_service.rs @@ -16,10 +16,10 @@ use rusqlite::Connection; -use crate::database::dao::brand_persona_dao::BrandPersonaDao; -use crate::database::dao::persona_dao::PersonaDao; -use crate::errors::project_error::PersonaError; -use crate::models::project_model::{ +use proxycast_core::database::dao::brand_persona_dao::BrandPersonaDao; +use proxycast_core::database::dao::persona_dao::PersonaDao; +use proxycast_core::errors::project_error::PersonaError; +use proxycast_core::models::project_model::{ BrandPersona, BrandPersonaExtension, BrandPersonaTemplate, CreateBrandExtensionRequest, CreatePersonaRequest, Persona, PersonaTemplate, PersonaUpdate, UpdateBrandExtensionRequest, }; @@ -401,7 +401,7 @@ impl PersonaService { #[cfg(test)] mod tests { use super::*; - use crate::database::schema::create_tables; + use proxycast_core::database::schema::create_tables; /// 创建测试数据库连接 fn setup_test_db() -> Connection { @@ -693,7 +693,7 @@ mod tests { #[test] fn test_get_brand_persona() { - use crate::models::project_model::{BrandTone, DesignConfig}; + use proxycast_core::models::project_model::{BrandTone, DesignConfig}; let conn = setup_test_db(); create_test_project(&conn, "project-1"); @@ -742,7 +742,7 @@ mod tests { #[test] fn test_save_brand_extension_creates_new() { - use crate::models::project_model::BrandTone; + use proxycast_core::models::project_model::BrandTone; let conn = setup_test_db(); create_test_project(&conn, "project-1"); @@ -782,7 +782,7 @@ mod tests { #[test] fn test_save_brand_extension_updates_existing() { - use crate::models::project_model::BrandTone; + use proxycast_core::models::project_model::BrandTone; let conn = setup_test_db(); create_test_project(&conn, "project-1"); diff --git a/src-tauri/src/services/project_context_builder.rs b/src-tauri/crates/services/src/project_context_builder.rs similarity index 97% rename from src-tauri/src/services/project_context_builder.rs rename to src-tauri/crates/services/src/project_context_builder.rs index 4b23b61d0..1910d77da 100644 --- a/src-tauri/src/services/project_context_builder.rs +++ b/src-tauri/crates/services/src/project_context_builder.rs @@ -19,12 +19,12 @@ use chrono::Utc; use rusqlite::Connection; use tracing::{debug, warn}; -use crate::errors::project_error::ProjectError; -use crate::models::project_model::{Material, Persona, ProjectContext, Template}; -use crate::services::material_service::MaterialService; -use crate::services::persona_service::PersonaService; -use crate::services::template_service::TemplateService; -use crate::workspace::{Workspace, WorkspaceSettings, WorkspaceType}; +use crate::material_service::MaterialService; +use crate::persona_service::PersonaService; +use crate::template_service::TemplateService; +use proxycast_core::errors::project_error::ProjectError; +use proxycast_core::models::project_model::{Material, Persona, ProjectContext, Template}; +use proxycast_core::workspace::{Workspace, WorkspaceSettings, WorkspaceType}; // ============================================================================ // 项目上下文构建器 @@ -453,8 +453,8 @@ impl ProjectContextBuilder { #[cfg(test)] mod tests { use super::*; - use crate::database::schema::create_tables; - use crate::models::project_model::CreatePersonaRequest; + use proxycast_core::database::schema::create_tables; + use proxycast_core::models::project_model::CreatePersonaRequest; /// 创建测试数据库连接 fn setup_test_db() -> Connection { @@ -584,7 +584,7 @@ mod tests { create_test_project(&conn, "project-1", "测试项目"); // 创建素材 - use crate::models::project_model::UploadMaterialRequest; + use proxycast_core::models::project_model::UploadMaterialRequest; let req = UploadMaterialRequest { project_id: "project-1".to_string(), name: "参考文档".to_string(), @@ -614,7 +614,7 @@ mod tests { create_test_project(&conn, "project-1", "测试项目"); // 创建模板 - use crate::models::project_model::CreateTemplateRequest; + use proxycast_core::models::project_model::CreateTemplateRequest; let req = CreateTemplateRequest { project_id: "project-1".to_string(), name: "小红书模板".to_string(), @@ -665,7 +665,7 @@ mod tests { PersonaService::set_default_persona(&conn, "project-1", &persona.id).unwrap(); // 创建素材 - use crate::models::project_model::UploadMaterialRequest; + use proxycast_core::models::project_model::UploadMaterialRequest; let material_req = UploadMaterialRequest { project_id: "project-1".to_string(), name: "素材1".to_string(), @@ -678,7 +678,7 @@ mod tests { MaterialService::upload_material(&conn, material_req).unwrap(); // 创建模板 - use crate::models::project_model::CreateTemplateRequest; + use proxycast_core::models::project_model::CreateTemplateRequest; let template_req = CreateTemplateRequest { project_id: "project-1".to_string(), name: "测试模板".to_string(), diff --git a/src-tauri/src/services/prompt_service.rs b/src-tauri/crates/services/src/prompt_service.rs similarity index 98% rename from src-tauri/src/services/prompt_service.rs rename to src-tauri/crates/services/src/prompt_service.rs index 84acf8573..a7db01618 100644 --- a/src-tauri/src/services/prompt_service.rs +++ b/src-tauri/crates/services/src/prompt_service.rs @@ -1,7 +1,7 @@ -use crate::database::dao::prompts::PromptDao; -use crate::database::DbConnection; -use crate::models::{AppType, Prompt}; -use crate::services::prompt_sync; +use crate::prompt_sync; +use proxycast_core::database::dao::prompts::PromptDao; +use proxycast_core::database::DbConnection; +use proxycast_core::models::{AppType, Prompt}; use std::collections::HashMap; pub struct PromptService; diff --git a/src-tauri/src/services/prompt_sync.rs b/src-tauri/crates/services/src/prompt_sync.rs similarity index 98% rename from src-tauri/src/services/prompt_sync.rs rename to src-tauri/crates/services/src/prompt_sync.rs index debf97402..5eaefbe2d 100644 --- a/src-tauri/src/services/prompt_sync.rs +++ b/src-tauri/crates/services/src/prompt_sync.rs @@ -1,4 +1,4 @@ -use crate::models::AppType; +use proxycast_core::models::AppType; use std::fs; use std::io::Write; use std::path::PathBuf; diff --git a/src-tauri/src/services/provider_pool_service.rs b/src-tauri/crates/services/src/provider_pool_service.rs similarity index 96% rename from src-tauri/src/services/provider_pool_service.rs rename to src-tauri/crates/services/src/provider_pool_service.rs index c954c6566..74ca3c455 100644 --- a/src-tauri/src/services/provider_pool_service.rs +++ b/src-tauri/crates/services/src/provider_pool_service.rs @@ -4,19 +4,19 @@ #![allow(dead_code)] -use crate::database::dao::provider_pool::ProviderPoolDao; -use crate::database::DbConnection; -use crate::models::provider_pool_model::{ +use crate::api_key_provider_service::ApiKeyProviderService; +use chrono::Utc; +use proxycast_core::database::dao::provider_pool::ProviderPoolDao; +use proxycast_core::database::DbConnection; +use proxycast_core::models::client_type::ClientType; +use proxycast_core::models::provider_pool_model::{ get_default_check_model, get_oauth_creds_path, CredentialData, CredentialDisplay, HealthCheckResult, OAuthStatus, PoolProviderType, PoolStats, ProviderCredential, ProviderPoolOverview, }; -use crate::models::route_model::RouteInfo; -use crate::providers::antigravity::TokenRefreshError; -use crate::providers::kiro::KiroProvider; -use crate::server::client_detector::ClientType; -use crate::services::api_key_provider_service::ApiKeyProviderService; -use chrono::Utc; +use proxycast_core::models::route_model::RouteInfo; +use proxycast_providers::providers::antigravity::TokenRefreshError; +use proxycast_providers::providers::kiro::KiroProvider; use reqwest::Client; use serde::{Deserialize, Serialize}; @@ -107,7 +107,7 @@ impl ProviderPoolService { /// 获取所有凭证概览 pub fn get_overview(&self, db: &DbConnection) -> Result, String> { - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; let grouped = ProviderPoolDao::get_grouped(&conn).map_err(|e| e.to_string())?; let mut overview = Vec::new(); @@ -141,7 +141,7 @@ impl ProviderPoolService { provider_type: &str, ) -> Result, String> { let pt: PoolProviderType = provider_type.parse().map_err(|e: String| e)?; - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; let mut credentials = ProviderPoolDao::get_by_type(&conn, &pt).map_err(|e| e.to_string())?; @@ -172,7 +172,7 @@ impl ProviderPoolService { cred.check_health = check_health.unwrap_or(true); cred.check_model_name = check_model_name; - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; ProviderPoolDao::insert(&conn, &cred).map_err(|e| e.to_string())?; Ok(cred) @@ -190,7 +190,7 @@ impl ProviderPoolService { not_supported_models: Option>, proxy_url: Option, ) -> Result { - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; let mut cred = ProviderPoolDao::get_by_uuid(&conn, uuid) .map_err(|e| e.to_string())? .ok_or_else(|| format!("Credential not found: {uuid}"))?; @@ -224,7 +224,7 @@ impl ProviderPoolService { /// 删除凭证 pub fn delete_credential(&self, db: &DbConnection, uuid: &str) -> Result { - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; ProviderPoolDao::delete(&conn, uuid).map_err(|e| e.to_string()) } @@ -252,7 +252,7 @@ impl ProviderPoolService { db: &DbConnection, provider_type: &str, model: Option<&str>, - client_type: Option<&crate::server::client_detector::ClientType>, + client_type: Option<&proxycast_core::models::client_type::ClientType>, ) -> Result, String> { // 对于未知的 provider_type,直接返回 None(不是错误) // 这样可以让 select_credential_with_fallback 继续尝试智能降级 @@ -265,7 +265,7 @@ impl ProviderPoolService { return Ok(None); } }; - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; // 获取凭证,对于 AI Provider 类型,也查找 Assistant 类型的凭证 let mut credentials = @@ -408,7 +408,7 @@ impl ProviderPoolService { provider_type: &str, model: Option<&str>, provider_id_hint: Option<&str>, - client_type: Option<&crate::server::client_detector::ClientType>, + client_type: Option<&proxycast_core::models::client_type::ClientType>, ) -> Result, String> { eprintln!( "[select_credential_with_fallback] 开始: provider_type={provider_type}, model={model:?}, provider_id_hint={provider_id_hint:?}" @@ -554,7 +554,7 @@ impl ProviderPoolService { /// 记录凭证使用 pub fn record_usage(&self, db: &DbConnection, uuid: &str) -> Result<(), String> { - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; let cred = ProviderPoolDao::get_by_uuid(&conn, uuid) .map_err(|e| e.to_string())? .ok_or_else(|| format!("Credential not found: {uuid}"))?; @@ -570,7 +570,7 @@ impl ProviderPoolService { uuid: &str, check_model: Option<&str>, ) -> Result<(), String> { - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; ProviderPoolDao::update_health_status( &conn, uuid, @@ -591,7 +591,7 @@ impl ProviderPoolService { uuid: &str, error_message: Option<&str>, ) -> Result<(), String> { - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; let cred = ProviderPoolDao::get_by_uuid(&conn, uuid) .map_err(|e| e.to_string())? .ok_or_else(|| format!("Credential not found: {uuid}"))?; @@ -614,7 +614,7 @@ impl ProviderPoolService { /// 重置凭证计数器 pub fn reset_counters(&self, db: &DbConnection, uuid: &str) -> Result<(), String> { - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; ProviderPoolDao::reset_counters(&conn, uuid).map_err(|e| e.to_string()) } @@ -625,7 +625,7 @@ impl ProviderPoolService { provider_type: &str, ) -> Result { let pt: PoolProviderType = provider_type.parse().map_err(|e: String| e)?; - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; ProviderPoolDao::reset_health_by_type(&conn, &pt).map_err(|e| e.to_string()) } @@ -636,7 +636,7 @@ impl ProviderPoolService { db: &DbConnection, uuid: &str, ) -> Result, String> { - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; let cred = ProviderPoolDao::get_by_uuid(&conn, uuid).map_err(|e| e.to_string())?; Ok(cred.map(|c| CredentialHealthInfo { @@ -661,7 +661,7 @@ impl ProviderPoolService { &self, db: &DbConnection, ) -> Result, String> { - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; let credentials = ProviderPoolDao::get_all(&conn).map_err(|e| e.to_string())?; Ok(credentials @@ -694,7 +694,7 @@ impl ProviderPoolService { let error_message = error.user_message(); let requires_reauth = error.requires_reauth(); - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; let cred = ProviderPoolDao::get_by_uuid(&conn, uuid) .map_err(|e| e.to_string())? .ok_or_else(|| format!("Credential not found: {uuid}"))?; @@ -737,7 +737,8 @@ impl ProviderPoolService { let pt: PoolProviderType = provider_type .parse() .map_err(|_| SelectionError::NoCredentials)?; - let conn = crate::database::lock_db(db).map_err(|_| SelectionError::NoCredentials)?; + let conn = + proxycast_core::database::lock_db(db).map_err(|_| SelectionError::NoCredentials)?; let credentials = ProviderPoolDao::get_by_type(&conn, &pt).map_err(|_| SelectionError::NoCredentials)?; drop(conn); @@ -847,7 +848,7 @@ impl ProviderPoolService { uuid: &str, ) -> Result { let cred = { - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; ProviderPoolDao::get_by_uuid(&conn, uuid) .map_err(|e| e.to_string())? .ok_or_else(|| format!("Credential not found: {uuid}"))? @@ -887,7 +888,7 @@ impl ProviderPoolService { // 重新获取凭证(token 已更新) let updated_cred = { - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; ProviderPoolDao::get_by_uuid(&conn, uuid) .map_err(|e| e.to_string())? .ok_or_else(|| format!("Credential not found: {uuid}"))? @@ -961,7 +962,7 @@ impl ProviderPoolService { ) -> Result, String> { let pt: PoolProviderType = provider_type.parse().map_err(|e: String| e)?; let credentials = { - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; ProviderPoolDao::get_by_type(&conn, &pt).map_err(|e| e.to_string())? }; @@ -1419,7 +1420,7 @@ impl ProviderPoolService { override_base_url: Option<&str>, model: &str, ) -> Result<(), String> { - use crate::providers::codex::CodexProvider; + use proxycast_providers::providers::codex::CodexProvider; let mut provider = CodexProvider::new(); provider @@ -1519,7 +1520,7 @@ impl ProviderPoolService { // Claude OAuth 健康检查 async fn check_claude_oauth_health(&self, creds_path: &str, model: &str) -> Result<(), String> { - use crate::providers::claude_oauth::ClaudeOAuthProvider; + use proxycast_providers::providers::claude_oauth::ClaudeOAuthProvider; let mut provider = ClaudeOAuthProvider::new(); provider @@ -1564,7 +1565,7 @@ impl ProviderPoolService { db: &DbConnection, name: &str, ) -> Result, String> { - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; ProviderPoolDao::get_by_name(&conn, name).map_err(|e| e.to_string()) } @@ -1574,7 +1575,7 @@ impl ProviderPoolService { db: &DbConnection, uuid: &str, ) -> Result, String> { - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; ProviderPoolDao::get_by_uuid(&conn, uuid).map_err(|e| e.to_string()) } @@ -1584,7 +1585,7 @@ impl ProviderPoolService { db: &DbConnection, base_url: &str, ) -> Result, String> { - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; let grouped = ProviderPoolDao::get_grouped(&conn).map_err(|e| e.to_string())?; drop(conn); @@ -1701,7 +1702,7 @@ impl ProviderPoolService { /// 使用副本文件中的凭证进行刷新,副本文件应包含完整的 client_id/client_secret。 /// 支持多账号场景,每个副本文件完全独立。 pub async fn refresh_kiro_token(&self, creds_path: &str) -> Result { - let mut provider = crate::providers::kiro::KiroProvider::new(); + let mut provider = proxycast_providers::providers::kiro::KiroProvider::new(); provider .load_credentials_from_path(creds_path) .await @@ -1716,7 +1717,7 @@ impl ProviderPoolService { /// 刷新 OAuth Token (Gemini) pub async fn refresh_gemini_token(&self, creds_path: &str) -> Result { - let mut provider = crate::providers::gemini::GeminiProvider::new(); + let mut provider = proxycast_providers::providers::gemini::GeminiProvider::new(); provider .load_credentials_from_path(creds_path) .await @@ -1729,7 +1730,7 @@ impl ProviderPoolService { /// 刷新 OAuth Token (Antigravity) pub async fn refresh_antigravity_token(&self, creds_path: &str) -> Result { - let mut provider = crate::providers::antigravity::AntigravityProvider::new(); + let mut provider = proxycast_providers::providers::antigravity::AntigravityProvider::new(); provider .load_credentials_from_path(creds_path) .await @@ -1747,7 +1748,7 @@ impl ProviderPoolService { uuid: &str, ) -> Result { let cred = { - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; ProviderPoolDao::get_by_uuid(&conn, uuid) .map_err(|e| e.to_string())? .ok_or_else(|| format!("Credential not found: {uuid}"))? @@ -1774,7 +1775,7 @@ impl ProviderPoolService { uuid: &str, ) -> Result { let cred = { - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; ProviderPoolDao::get_by_uuid(&conn, uuid) .map_err(|e| e.to_string())? .ok_or_else(|| format!("Credential not found: {uuid}"))? @@ -1795,7 +1796,7 @@ impl ProviderPoolService { name: Option, check_health: Option, check_model_name: Option, - source: crate::models::provider_pool_model::CredentialSource, + source: proxycast_core::models::provider_pool_model::CredentialSource, ) -> Result { let pt: PoolProviderType = provider_type.parse().map_err(|e: String| e)?; @@ -1804,7 +1805,7 @@ impl ProviderPoolService { cred.check_health = check_health.unwrap_or(true); cred.check_model_name = check_model_name; - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; ProviderPoolDao::insert(&conn, &cred).map_err(|e| e.to_string())?; Ok(cred) @@ -1816,10 +1817,10 @@ impl ProviderPoolService { pub fn migrate_private_config( &self, db: &DbConnection, - config: &crate::config::Config, + config: &proxycast_core::config::Config, ) -> Result { - use crate::config::expand_tilde; - use crate::models::provider_pool_model::CredentialSource; + use proxycast_core::config::expand_tilde; + use proxycast_core::models::provider_pool_model::CredentialSource; let mut result = MigrationResult::default(); @@ -1936,7 +1937,7 @@ impl ProviderPoolService { /// 检查是否存在相同路径的凭证 fn credential_exists_by_path(&self, db: &DbConnection, path: &str) -> Result { - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; let all_creds = ProviderPoolDao::get_all(&conn).map_err(|e| e.to_string())?; for cred in all_creds { @@ -1955,7 +1956,7 @@ impl ProviderPoolService { db: &DbConnection, api_key: &str, ) -> Result { - let conn = crate::database::lock_db(db)?; + let conn = proxycast_core::database::lock_db(db)?; let all_creds = ProviderPoolDao::get_all(&conn).map_err(|e| e.to_string())?; for cred in all_creds { diff --git a/src-tauri/src/services/session_context_service.rs b/src-tauri/crates/services/src/session_context_service.rs similarity index 99% rename from src-tauri/src/services/session_context_service.rs rename to src-tauri/crates/services/src/session_context_service.rs index 8d054072a..19ce754b0 100644 --- a/src-tauri/src/services/session_context_service.rs +++ b/src-tauri/crates/services/src/session_context_service.rs @@ -2,8 +2,8 @@ //! //! 提供会话上下文的持久化、恢复和智能管理功能,解决 AI 对话中的上下文丢失问题 -use crate::database::dao::general_chat::GeneralChatDao; -use crate::services::general_chat::{ChatMessage, MessageRole}; +use crate::general_chat::{ChatMessage, MessageRole}; +use proxycast_core::database::dao::general_chat::GeneralChatDao; use rusqlite::Connection; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -410,7 +410,7 @@ pub struct SessionStats { #[cfg(test)] mod tests { use super::*; - use crate::services::general_chat::ChatSession; + use crate::general_chat::ChatSession; use rusqlite::Connection; fn setup_test_db() -> Connection { diff --git a/src-tauri/src/services/skill_service.rs b/src-tauri/crates/services/src/skill_service.rs similarity index 99% rename from src-tauri/src/services/skill_service.rs rename to src-tauri/crates/services/src/skill_service.rs index ae9c003a0..3e3632af4 100644 --- a/src-tauri/src/services/skill_service.rs +++ b/src-tauri/crates/services/src/skill_service.rs @@ -6,7 +6,7 @@ use std::path::{Path, PathBuf}; use std::time::Duration; use tokio::time::timeout; -use crate::models::{AppType, Skill, SkillMetadata, SkillRepo, SkillState}; +use proxycast_core::models::{AppType, Skill, SkillMetadata, SkillRepo, SkillState}; const DOWNLOAD_TIMEOUT: Duration = Duration::from_secs(60); diff --git a/src-tauri/src/services/switch.rs b/src-tauri/crates/services/src/switch.rs similarity index 98% rename from src-tauri/src/services/switch.rs rename to src-tauri/crates/services/src/switch.rs index bb9753e1a..c462c7b10 100644 --- a/src-tauri/src/services/switch.rs +++ b/src-tauri/crates/services/src/switch.rs @@ -1,8 +1,8 @@ -use crate::database::dao::providers::ProviderDao; -use crate::database::DbConnection; -use crate::models::{AppType, Provider}; -use crate::services::live_sync; +use crate::live_sync; use once_cell::sync::Lazy; +use proxycast_core::database::dao::providers::ProviderDao; +use proxycast_core::database::DbConnection; +use proxycast_core::models::{AppType, Provider}; use tokio::sync::Mutex; pub struct SwitchService; diff --git a/src-tauri/src/services/template_service.rs b/src-tauri/crates/services/src/template_service.rs similarity index 98% rename from src-tauri/src/services/template_service.rs rename to src-tauri/crates/services/src/template_service.rs index ef7cf4d99..383e672e8 100644 --- a/src-tauri/src/services/template_service.rs +++ b/src-tauri/crates/services/src/template_service.rs @@ -13,9 +13,9 @@ use rusqlite::Connection; -use crate::database::dao::template_dao::TemplateDao; -use crate::errors::project_error::TemplateError; -use crate::models::project_model::{CreateTemplateRequest, Template, TemplateUpdate}; +use proxycast_core::database::dao::template_dao::TemplateDao; +use proxycast_core::errors::project_error::TemplateError; +use proxycast_core::models::project_model::{CreateTemplateRequest, Template, TemplateUpdate}; // ============================================================================ // 排版模板服务 @@ -211,7 +211,7 @@ impl TemplateService { #[cfg(test)] mod tests { use super::*; - use crate::database::schema::create_tables; + use proxycast_core::database::schema::create_tables; /// 创建测试数据库连接 fn setup_test_db() -> Connection { diff --git a/src-tauri/src/services/token_cache_service.rs b/src-tauri/crates/services/src/token_cache_service.rs similarity index 98% rename from src-tauri/src/services/token_cache_service.rs rename to src-tauri/crates/services/src/token_cache_service.rs index c501f4131..c0bdd9989 100644 --- a/src-tauri/src/services/token_cache_service.rs +++ b/src-tauri/crates/services/src/token_cache_service.rs @@ -8,16 +8,16 @@ //! - 按需刷新即将过期的 Token //! - 处理 401/403 错误时的强制刷新 -use crate::database::dao::provider_pool::ProviderPoolDao; -use crate::database::DbConnection; -use crate::models::provider_pool_model::{ - CachedTokenInfo, CredentialData, PoolProviderType, ProviderCredential, -}; -use crate::providers::gemini::GeminiProvider; -use crate::providers::kiro::KiroProvider; -use crate::services::kiro_event_service::KiroEventService; +use crate::kiro_event_service::KiroEventService; use chrono::Utc; use dashmap::DashMap; +use proxycast_core::database::dao::provider_pool::ProviderPoolDao; +use proxycast_core::database::DbConnection; +use proxycast_core::models::provider_pool_model::{ + CachedTokenInfo, CredentialData, PoolProviderType, ProviderCredential, +}; +use proxycast_providers::providers::gemini::GeminiProvider; +use proxycast_providers::providers::kiro::KiroProvider; use std::sync::Arc; use tokio::sync::Mutex; @@ -556,7 +556,7 @@ impl TokenCacheService { /// 刷新 Antigravity Token async fn refresh_antigravity(&self, creds_path: &str) -> Result { - use crate::providers::antigravity::AntigravityProvider; + use proxycast_providers::providers::antigravity::AntigravityProvider; let mut provider = AntigravityProvider::new(); provider @@ -588,7 +588,7 @@ impl TokenCacheService { /// 刷新 Codex Token async fn refresh_codex(&self, creds_path: &str) -> Result { - use crate::providers::codex::CodexProvider; + use proxycast_providers::providers::codex::CodexProvider; let mut provider = CodexProvider::new(); provider @@ -622,7 +622,7 @@ impl TokenCacheService { /// 刷新 Claude OAuth Token async fn refresh_claude_oauth(&self, creds_path: &str) -> Result { - use crate::providers::claude_oauth::ClaudeOAuthProvider; + use proxycast_providers::providers::claude_oauth::ClaudeOAuthProvider; let mut provider = ClaudeOAuthProvider::new(); provider @@ -1029,8 +1029,10 @@ impl TokenCacheService { // 首先检查缓存 let cached = { let conn = db.lock().map_err(|e| e.to_string())?; - crate::database::dao::provider_pool::ProviderPoolDao::get_token_cache(&conn, uuid) - .map_err(|e| e.to_string())? + proxycast_core::database::dao::provider_pool::ProviderPoolDao::get_token_cache( + &conn, uuid, + ) + .map_err(|e| e.to_string())? }; // 检查是否需要提前刷新(使用指定的分钟数阈值) diff --git a/src-tauri/src/services/tool_hooks_service.rs b/src-tauri/crates/services/src/tool_hooks_service.rs similarity index 99% rename from src-tauri/src/services/tool_hooks_service.rs rename to src-tauri/crates/services/src/tool_hooks_service.rs index 13073c430..50db3870c 100644 --- a/src-tauri/src/services/tool_hooks_service.rs +++ b/src-tauri/crates/services/src/tool_hooks_service.rs @@ -2,7 +2,7 @@ //! //! 提供工具执行前后的钩子机制,用于自动化上下文记忆管理 -use crate::services::context_memory_service::{ContextMemoryService, MemoryEntry, MemoryFileType}; +use crate::context_memory_service::{ContextMemoryService, MemoryEntry, MemoryFileType}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::{Arc, Mutex}; @@ -566,7 +566,7 @@ impl ToolHooksService { #[cfg(test)] mod tests { use super::*; - use crate::services::context_memory_service::ContextMemoryConfig; + use crate::context_memory_service::ContextMemoryConfig; use tempfile::TempDir; fn create_test_services() -> (Arc, ToolHooksService, TempDir) { diff --git a/src-tauri/src/services/update_check_service.rs b/src-tauri/crates/services/src/update_check_service.rs similarity index 100% rename from src-tauri/src/services/update_check_service.rs rename to src-tauri/crates/services/src/update_check_service.rs diff --git a/src-tauri/src/services/usage_service.rs b/src-tauri/crates/services/src/usage_service.rs similarity index 100% rename from src-tauri/src/services/usage_service.rs rename to src-tauri/crates/services/src/usage_service.rs diff --git a/src-tauri/src/models/mod.rs b/src-tauri/src/models/mod.rs index 7c6aa8218..c031a8689 100644 --- a/src-tauri/src/models/mod.rs +++ b/src-tauri/src/models/mod.rs @@ -20,8 +20,6 @@ pub use proxycast_core::models::prompt_model; pub use proxycast_core::models::provider_model; pub use proxycast_core::models::provider_pool_model; pub use proxycast_core::models::route_model; -pub use proxycast_core::models::skill_model; - // project_model 已迁移到 core pub use proxycast_core::models::project_model; @@ -38,6 +36,4 @@ pub use proxycast_core::models::prompt_model::Prompt; pub use proxycast_core::models::provider_model::Provider; #[allow(unused_imports)] pub use proxycast_core::models::provider_pool_model::*; -pub use proxycast_core::models::skill_model::{ - Skill, SkillMetadata, SkillRepo, SkillState, SkillStates, -}; +pub use proxycast_core::models::skill_model::{Skill, SkillMetadata, SkillRepo, SkillState}; diff --git a/src-tauri/src/server/client_detector.rs b/src-tauri/src/server/client_detector.rs index f26b709d3..768872f8b 100644 --- a/src-tauri/src/server/client_detector.rs +++ b/src-tauri/src/server/client_detector.rs @@ -1,307 +1,8 @@ //! 客户端类型检测模块 //! -//! 通过解析 HTTP 请求的 User-Agent 头来识别客户端类型。 +//! 已迁移到 proxycast-core crate,本文件仅做重新导出。 -#![allow(dead_code)] - -use serde::{Deserialize, Serialize}; - -/// 客户端类型枚举 -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum ClientType { - /// Cursor 编辑器 - Cursor, - /// Claude Code 客户端 - ClaudeCode, - /// OpenAI Codex CLI - Codex, - /// Windsurf 编辑器 - Windsurf, - /// Kiro IDE - Kiro, - /// 未识别的客户端 - Other, -} - -impl ClientType { - /// 从 User-Agent 字符串检测客户端类型 - /// - /// 支持大小写不敏感匹配。 - /// - /// # 参数 - /// - `user_agent`: HTTP 请求的 User-Agent 头值 - /// - /// # 返回 - /// 检测到的客户端类型 - /// - /// # 示例 - /// ```ignore - /// use proxycast_lib::server::client_detector::ClientType; - /// - /// assert_eq!(ClientType::from_user_agent("Cursor/1.0"), ClientType::Cursor); - /// assert_eq!(ClientType::from_user_agent("claude-code/2.0"), ClientType::ClaudeCode); - /// assert_eq!(ClientType::from_user_agent("Unknown"), ClientType::Other); - /// ``` - pub fn from_user_agent(user_agent: &str) -> Self { - let ua_lower = user_agent.to_lowercase(); - - if ua_lower.contains("cursor") { - ClientType::Cursor - } else if ua_lower.contains("claude-code") || ua_lower.contains("claude_code") { - ClientType::ClaudeCode - } else if ua_lower.contains("codex") { - ClientType::Codex - } else if ua_lower.contains("windsurf") { - ClientType::Windsurf - } else if ua_lower.contains("kiro") { - ClientType::Kiro - } else { - ClientType::Other - } - } - - /// 获取配置键名 - /// - /// 返回用于配置文件中的键名。 - /// - /// # 返回 - /// 配置键名字符串 - pub fn config_key(&self) -> &'static str { - match self { - ClientType::Cursor => "cursor", - ClientType::ClaudeCode => "claude_code", - ClientType::Codex => "codex", - ClientType::Windsurf => "windsurf", - ClientType::Kiro => "kiro", - ClientType::Other => "other", - } - } - - /// 获取所有客户端类型 - /// - /// 返回所有支持的客户端类型列表。 - pub fn all() -> &'static [ClientType] { - &[ - ClientType::Cursor, - ClientType::ClaudeCode, - ClientType::Codex, - ClientType::Windsurf, - ClientType::Kiro, - ClientType::Other, - ] - } - - /// 从配置键名解析客户端类型 - /// - /// # 参数 - /// - `key`: 配置键名 - /// - /// # 返回 - /// 如果键名有效,返回对应的客户端类型;否则返回 None - pub fn from_config_key(key: &str) -> Option { - match key { - "cursor" => Some(ClientType::Cursor), - "claude_code" => Some(ClientType::ClaudeCode), - "codex" => Some(ClientType::Codex), - "windsurf" => Some(ClientType::Windsurf), - "kiro" => Some(ClientType::Kiro), - "other" => Some(ClientType::Other), - _ => None, - } - } -} - -impl std::fmt::Display for ClientType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.config_key()) - } -} - -/// 根据客户端类型和端点配置选择 Provider -/// -/// **Validates: Requirements 1.3, 1.4, 3.4** -/// -/// 优先级:端点 Provider 配置 > 默认 Provider -/// -/// # 参数 -/// - `client_type`: 检测到的客户端类型 -/// - `endpoint_provider`: 端点配置中该客户端类型对应的 Provider(可选) -/// - `default_provider`: 默认 Provider -/// -/// # 返回 -/// 选择的 Provider 名称 -pub fn select_provider( - _client_type: ClientType, - endpoint_provider: Option<&String>, - default_provider: &str, -) -> String { - match endpoint_provider { - Some(provider) => provider.clone(), - None => default_provider.to_string(), - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_from_user_agent_cursor() { - assert_eq!( - ClientType::from_user_agent("Cursor/1.0"), - ClientType::Cursor - ); - assert_eq!(ClientType::from_user_agent("cursor"), ClientType::Cursor); - assert_eq!(ClientType::from_user_agent("CURSOR"), ClientType::Cursor); - assert_eq!( - ClientType::from_user_agent("Mozilla/5.0 Cursor"), - ClientType::Cursor - ); - } - - #[test] - fn test_from_user_agent_claude_code() { - assert_eq!( - ClientType::from_user_agent("Claude-Code/2.0"), - ClientType::ClaudeCode - ); - assert_eq!( - ClientType::from_user_agent("claude-code"), - ClientType::ClaudeCode - ); - assert_eq!( - ClientType::from_user_agent("CLAUDE-CODE"), - ClientType::ClaudeCode - ); - assert_eq!( - ClientType::from_user_agent("claude_code"), - ClientType::ClaudeCode - ); - assert_eq!( - ClientType::from_user_agent("CLAUDE_CODE"), - ClientType::ClaudeCode - ); - } - - #[test] - fn test_from_user_agent_codex() { - assert_eq!(ClientType::from_user_agent("Codex/1.0"), ClientType::Codex); - assert_eq!(ClientType::from_user_agent("codex"), ClientType::Codex); - assert_eq!(ClientType::from_user_agent("CODEX"), ClientType::Codex); - } - - #[test] - fn test_from_user_agent_windsurf() { - assert_eq!( - ClientType::from_user_agent("Windsurf/1.0"), - ClientType::Windsurf - ); - assert_eq!( - ClientType::from_user_agent("windsurf"), - ClientType::Windsurf - ); - assert_eq!( - ClientType::from_user_agent("WINDSURF"), - ClientType::Windsurf - ); - } - - #[test] - fn test_from_user_agent_kiro() { - assert_eq!(ClientType::from_user_agent("Kiro/1.0"), ClientType::Kiro); - assert_eq!(ClientType::from_user_agent("kiro"), ClientType::Kiro); - assert_eq!(ClientType::from_user_agent("KIRO"), ClientType::Kiro); - } - - #[test] - fn test_from_user_agent_other() { - assert_eq!(ClientType::from_user_agent("Unknown"), ClientType::Other); - assert_eq!(ClientType::from_user_agent(""), ClientType::Other); - assert_eq!( - ClientType::from_user_agent("Mozilla/5.0"), - ClientType::Other - ); - } - - #[test] - fn test_config_key() { - assert_eq!(ClientType::Cursor.config_key(), "cursor"); - assert_eq!(ClientType::ClaudeCode.config_key(), "claude_code"); - assert_eq!(ClientType::Codex.config_key(), "codex"); - assert_eq!(ClientType::Windsurf.config_key(), "windsurf"); - assert_eq!(ClientType::Kiro.config_key(), "kiro"); - assert_eq!(ClientType::Other.config_key(), "other"); - } - - #[test] - fn test_from_config_key() { - assert_eq!( - ClientType::from_config_key("cursor"), - Some(ClientType::Cursor) - ); - assert_eq!( - ClientType::from_config_key("claude_code"), - Some(ClientType::ClaudeCode) - ); - assert_eq!( - ClientType::from_config_key("codex"), - Some(ClientType::Codex) - ); - assert_eq!( - ClientType::from_config_key("windsurf"), - Some(ClientType::Windsurf) - ); - assert_eq!(ClientType::from_config_key("kiro"), Some(ClientType::Kiro)); - assert_eq!( - ClientType::from_config_key("other"), - Some(ClientType::Other) - ); - assert_eq!(ClientType::from_config_key("invalid"), None); - } - - #[test] - fn test_all_client_types() { - let all = ClientType::all(); - assert_eq!(all.len(), 6); - assert!(all.contains(&ClientType::Cursor)); - assert!(all.contains(&ClientType::ClaudeCode)); - assert!(all.contains(&ClientType::Codex)); - assert!(all.contains(&ClientType::Windsurf)); - assert!(all.contains(&ClientType::Kiro)); - assert!(all.contains(&ClientType::Other)); - } - - #[test] - fn test_display() { - assert_eq!(format!("{}", ClientType::Cursor), "cursor"); - assert_eq!(format!("{}", ClientType::ClaudeCode), "claude_code"); - } - - #[test] - fn test_serialization() { - let cursor = ClientType::Cursor; - let json = serde_json::to_string(&cursor).unwrap(); - assert_eq!(json, "\"cursor\""); - - let claude_code = ClientType::ClaudeCode; - let json = serde_json::to_string(&claude_code).unwrap(); - assert_eq!(json, "\"claude_code\""); - } - - #[test] - fn test_deserialization() { - let cursor: ClientType = serde_json::from_str("\"cursor\"").unwrap(); - assert_eq!(cursor, ClientType::Cursor); - - let claude_code: ClientType = serde_json::from_str("\"claude_code\"").unwrap(); - assert_eq!(claude_code, ClientType::ClaudeCode); - } -} - -// ============================================================================ -// Property 2: Provider 选择优先级属性测试 -// ============================================================================ +pub use proxycast_core::models::client_type::*; #[cfg(test)] mod property_tests { @@ -309,7 +10,6 @@ mod property_tests { use crate::config::EndpointProvidersConfig; use proptest::prelude::*; - /// 生成随机的客户端类型 fn arb_client_type() -> impl Strategy { prop_oneof![ Just(ClientType::Cursor), @@ -321,7 +21,6 @@ mod property_tests { ] } - /// 生成随机的 Provider 名称 fn arb_provider_name() -> impl Strategy { prop_oneof![ Just("kiro".to_string()), @@ -333,12 +32,10 @@ mod property_tests { ] } - /// 生成可选的 Provider 名称 fn arb_optional_provider() -> impl Strategy> { prop_oneof![Just(None), arb_provider_name().prop_map(Some),] } - /// 生成随机的 EndpointProvidersConfig fn arb_endpoint_providers_config() -> impl Strategy { ( arb_optional_provider(), @@ -363,90 +60,22 @@ mod property_tests { proptest! { #![proptest_config(ProptestConfig::with_cases(100))] - /// **Feature: endpoint-provider-config, Property 2: Provider 选择优先级** - /// *对于任意* 客户端类型和配置: - /// - 当 endpoint_providers[client_type] 有值时,应使用该 Provider - /// - 当 endpoint_providers[client_type] 为空时,应使用 default_provider - /// **Validates: Requirements 1.3, 1.4, 3.4** #[test] fn prop_provider_selection_priority( client_type in arb_client_type(), endpoint_config in arb_endpoint_providers_config(), default_provider in arb_provider_name() ) { - // 获取端点配置中该客户端类型对应的 Provider let endpoint_provider = endpoint_config.get_provider(client_type.config_key()); - - // 调用 select_provider 函数 let selected = select_provider(client_type, endpoint_provider, &default_provider); - - // 验证选择逻辑 match endpoint_provider { Some(provider) => { - // 当端点配置有值时,应使用端点配置的 Provider - prop_assert_eq!( - selected, - provider.clone(), - "当端点配置有值时,应使用端点配置的 Provider" - ); + prop_assert_eq!(selected, provider.clone()); } None => { - // 当端点配置为空时,应使用默认 Provider - prop_assert_eq!( - selected, - default_provider, - "当端点配置为空时,应使用默认 Provider" - ); + prop_assert_eq!(selected, default_provider); } } } - - /// **Feature: endpoint-provider-config, Property 2: Provider 选择优先级(端点配置优先)** - /// *对于任意* 客户端类型,当端点配置有值时,应始终使用端点配置的 Provider, - /// 而不是默认 Provider。 - /// **Validates: Requirements 1.3, 3.4** - #[test] - fn prop_endpoint_config_takes_priority( - client_type in arb_client_type(), - endpoint_provider in arb_provider_name(), - default_provider in arb_provider_name() - ) { - // 调用 select_provider 函数,端点配置有值 - let selected = select_provider( - client_type, - Some(&endpoint_provider), - &default_provider - ); - - // 验证:端点配置优先于默认配置 - prop_assert_eq!( - selected, - endpoint_provider, - "端点配置应优先于默认配置" - ); - } - - /// **Feature: endpoint-provider-config, Property 2: Provider 选择优先级(回退到默认)** - /// *对于任意* 客户端类型,当端点配置为空时,应使用默认 Provider。 - /// **Validates: Requirements 1.4** - #[test] - fn prop_fallback_to_default_provider( - client_type in arb_client_type(), - default_provider in arb_provider_name() - ) { - // 调用 select_provider 函数,端点配置为空 - let selected = select_provider( - client_type, - None, - &default_provider - ); - - // 验证:回退到默认 Provider - prop_assert_eq!( - selected, - default_provider, - "当端点配置为空时,应回退到默认 Provider" - ); - } } } diff --git a/src-tauri/src/server/handlers/api.rs b/src-tauri/src/server/handlers/api.rs index 697678d06..93ef7ec8b 100644 --- a/src-tauri/src/server/handlers/api.rs +++ b/src-tauri/src/server/handlers/api.rs @@ -22,7 +22,6 @@ use axum::{ response::{IntoResponse, Response}, Json, }; -use chrono::Utc; use serde_json::json; use std::collections::HashMap; @@ -494,13 +493,13 @@ pub async fn chat_completions( // 尝试将 selected_provider 解析为 ProviderType // 构建 Flow Metadata,同时保存 provider_type 和实际的 provider_id - let provider_type = selected_provider + let _provider_type = selected_provider .parse::() .unwrap_or(ProviderType::OpenAI); // 从凭证名称中提取 Provider 显示名称 // 凭证名称格式:Some("[降级] DeepSeek") 或 Some("DeepSeek") - let provider_display_name = cred.name.as_ref().map(|name| { + let _provider_display_name = cred.name.as_ref().map(|name| { // 去掉 "[降级] " 前缀 if name.starts_with("[降级] ") { &name[9..] // "[降级] " 是 9 个字节 @@ -521,7 +520,7 @@ pub async fn chat_completions( // 记录请求统计 let is_success = response.status().is_success(); - let status_code = response.status().as_u16(); + let _status_code = response.status().as_u16(); let status = if is_success { crate::telemetry::RequestStatus::Success } else { @@ -564,7 +563,7 @@ pub async fn chat_completions( // 启动 Flow 捕获(legacy mode) // 使用实际的 provider ID 构建 Flow Metadata - let provider_type = selected_provider + let _provider_type = selected_provider .parse::() .unwrap_or(ProviderType::OpenAI); @@ -1113,11 +1112,11 @@ pub async fn anthropic_messages( // 使用凭证的实际 provider_type(支持自定义 Provider) // 对于自定义 Provider ID,凭证的 provider_type 已通过数据库查询正确设置 - let provider_type = cred.provider_type; + let _provider_type = cred.provider_type; // 从凭证名称中提取 Provider 显示名称 // 凭证名称格式:Some("[降级] DeepSeek") 或 Some("DeepSeek") - let provider_display_name = cred.name.as_ref().map(|name| { + let _provider_display_name = cred.name.as_ref().map(|name| { // 去掉 "[降级] " 前缀 if name.starts_with("[降级] ") { &name[9..] // "[降级] " 是 9 个字节 @@ -1205,7 +1204,7 @@ pub async fn anthropic_messages( // 启动 Flow 捕获(legacy mode) // 使用实际的 provider ID 构建 Flow Metadata - let provider_type = selected_provider + let _provider_type = selected_provider .parse::() .unwrap_or(ProviderType::OpenAI); diff --git a/src-tauri/src/server/handlers/provider_calls.rs b/src-tauri/src/server/handlers/provider_calls.rs index e8b3fb031..ef1504da5 100644 --- a/src-tauri/src/server/handlers/provider_calls.rs +++ b/src-tauri/src/server/handlers/provider_calls.rs @@ -2156,7 +2156,7 @@ pub fn get_stream_format_for_credential(credential: &ProviderCredential) -> Stre /// - 需求 4.2: 调用 process_chunk 更新流重建器 /// - 需求 5.1: 在收到 chunk 后立即转发给客户端 pub async fn handle_streaming_response( - state: &AppState, + _state: &AppState, flow_id: Option<&str>, source_stream: StreamResponse, source_format: StreamingFormat, @@ -2229,7 +2229,7 @@ pub async fn handle_streaming_response( /// - 需求 6.2: 超时错误处理 /// - 需求 6.5: 可配置的流式响应超时 pub async fn handle_streaming_response_with_timeout( - state: &AppState, + _state: &AppState, flow_id: Option<&str>, source_stream: StreamResponse, source_format: StreamingFormat, @@ -2328,7 +2328,7 @@ pub fn response_to_stream(response: reqwest::Response) -> StreamResponse { /// # 需求覆盖 /// - 需求 5.4: 客户端断开时取消上游请求 pub async fn handle_streaming_with_disconnect_detection( - state: &AppState, + _state: &AppState, flow_id: Option<&str>, source_stream: StreamResponse, source_format: StreamingFormat, diff --git a/src-tauri/src/server_utils.rs b/src-tauri/src/server_utils.rs index c7c4e2f68..a9de576ec 100644 --- a/src-tauri/src/server_utils.rs +++ b/src-tauri/src/server_utils.rs @@ -114,6 +114,7 @@ impl CWParsedResponse { /// /// # 返回 /// (input_tokens, output_tokens) 元组 + #[allow(dead_code)] pub fn estimate_tokens(&self) -> (u32, u32) { // 估算 output tokens: 基于响应内容长度 (约 4 字符 = 1 token) let mut output_tokens: u32 = (self.content.len() / 4) as u32; diff --git a/src-tauri/src/services/mod.rs b/src-tauri/src/services/mod.rs index 2b238c47d..aa8e9b8c1 100644 --- a/src-tauri/src/services/mod.rs +++ b/src-tauri/src/services/mod.rs @@ -1,29 +1,38 @@ -pub mod api_key_provider_service; -pub mod aster_session_store; -pub mod backup_service; -pub mod context_memory_service; +//! 业务服务模块 +//! +//! 核心业务逻辑已迁移到 proxycast-services crate。 +//! 本模块保留 Tauri 相关服务和重新导出。 + +// 从 proxycast-services crate 重新导出 +pub use proxycast_services::api_key_provider_service; +pub use proxycast_services::aster_session_store; +pub use proxycast_services::backup_service; +pub use proxycast_services::content_creator; +pub use proxycast_services::context_memory_service; +pub use proxycast_services::general_chat; +pub use proxycast_services::kiro_event_service; +pub use proxycast_services::live_sync; +pub use proxycast_services::machine_id_service; +pub use proxycast_services::material_service; +pub use proxycast_services::mcp_service; +pub use proxycast_services::mcp_sync; +pub use proxycast_services::model_registry_service; +pub use proxycast_services::model_service; +pub use proxycast_services::persona_service; +pub use proxycast_services::project_context_builder; +pub use proxycast_services::prompt_service; +pub use proxycast_services::prompt_sync; +pub use proxycast_services::provider_pool_service; +pub use proxycast_services::session_context_service; +pub use proxycast_services::skill_service; +pub use proxycast_services::switch; +pub use proxycast_services::template_service; +pub use proxycast_services::token_cache_service; +pub use proxycast_services::tool_hooks_service; +pub use proxycast_services::update_check_service; +pub use proxycast_services::usage_service; + +// 保留在主 crate 的 Tauri 相关服务 pub mod file_browser_service; -pub mod general_chat; -pub mod kiro_event_service; -pub mod live_sync; -pub mod machine_id_service; -pub mod material_service; -pub mod mcp_service; -pub mod mcp_sync; -pub mod model_registry_service; -pub mod model_service; -pub mod persona_service; -pub mod project_context_builder; -pub mod prompt_service; -pub mod prompt_sync; -pub mod provider_pool_service; -pub mod session_context_service; -pub mod skill_service; -pub mod switch; pub mod sysinfo_service; -pub mod template_service; -pub mod token_cache_service; -pub mod tool_hooks_service; -pub mod update_check_service; pub mod update_window; -pub mod usage_service; diff --git a/src-tauri/src/websocket/mod.rs b/src-tauri/src/websocket/mod.rs index d53992b3f..b5bed24c6 100644 --- a/src-tauri/src/websocket/mod.rs +++ b/src-tauri/src/websocket/mod.rs @@ -13,10 +13,8 @@ mod lifecycle; mod processor; mod stream; -// types 已迁移到 proxycast-core -pub use proxycast_core::websocket::types; - pub use processor::MessageProcessor; +pub use proxycast_core::websocket::types; pub use proxycast_core::websocket::{ KiroTokenInfo, WsApiRequest, WsApiResponse, WsConfig, WsConnection, WsEndpoint, WsError, WsKiroEvent, WsMessage, WsStats, WsStatsSnapshot, WsStreamChunk, WsStreamEnd,