From 04045c79a12c0cc492deab68599846bfc24ea2cc Mon Sep 17 00:00:00 2001 From: coso Date: Tue, 6 Jan 2026 09:10:13 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E6=9B=B4=E6=96=B0=20antigravity-provi?= =?UTF-8?q?der=20=E7=89=88=E6=9C=AC=E5=88=B0=20v0.4.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/Cargo.lock | 2 +- src-tauri/src/app/bootstrap.rs | 6 + src-tauri/src/app/runner.rs | 40 + src-tauri/src/commands/mod.rs | 1 + src-tauri/src/commands/model_registry_cmd.rs | 155 ++++ src-tauri/src/commands/orchestrator_cmd.rs | 60 +- .../src/converter/openai_to_antigravity.rs | 22 +- src-tauri/src/credential/registry.rs | 48 ++ src-tauri/src/data/local_models.rs | 761 ++++++++++++++++++ src-tauri/src/data/mod.rs | 7 + src-tauri/src/database/schema.rs | 83 ++ src-tauri/src/lib.rs | 1 + src-tauri/src/models/mod.rs | 1 + src-tauri/src/models/model_registry.rs | 608 ++++++++++++++ src-tauri/src/orchestrator/orchestrator.rs | 2 + src-tauri/src/orchestrator/pool_builder.rs | 80 +- .../src/server/handlers/provider_calls.rs | 43 +- src-tauri/src/services/mod.rs | 1 + .../src/services/model_registry_service.rs | 657 +++++++++++++++ .../agent/chat/components/ChatNavbar.tsx | 376 ++------- .../agent/chat/hooks/useAgentChat.ts | 12 + src/components/agent/chat/types.ts | 97 ++- src/components/api-server/ApiServerPage.tsx | 7 +- .../api-server/EnhancedModelsTab.tsx | 370 +++++++++ .../model-selector/EnhancedModelList.tsx | 380 +++++++++ src/components/model-selector/ModelList.tsx | 4 +- .../model-selector/ModelSelector.tsx | 2 +- .../provider-pool/OAuthPluginTab.tsx | 190 +++-- .../provider-pool/ProviderPoolPage.tsx | 152 +--- src/components/provider-pool/README.md | 8 +- .../provider-pool/RelayProvidersSection.tsx | 257 ++++++ src/hooks/index.ts | 1 + src/hooks/useDeepLink.ts | 6 + src/hooks/useModelRegistry.ts | 333 ++++++++ src/lib/api/modelRegistry.ts | 114 +++ src/lib/api/orchestrator.ts | 42 +- src/lib/plugin-loader/PluginUIRenderer.tsx | 18 + src/lib/plugin-loader/index.ts | 53 +- src/lib/types/modelRegistry.ts | 144 ++++ 39 files changed, 4564 insertions(+), 580 deletions(-) create mode 100644 src-tauri/src/commands/model_registry_cmd.rs create mode 100644 src-tauri/src/data/local_models.rs create mode 100644 src-tauri/src/data/mod.rs create mode 100644 src-tauri/src/models/model_registry.rs create mode 100644 src-tauri/src/services/model_registry_service.rs create mode 100644 src/components/api-server/EnhancedModelsTab.tsx create mode 100644 src/components/model-selector/EnhancedModelList.tsx create mode 100644 src/components/provider-pool/RelayProvidersSection.tsx create mode 100644 src/hooks/useModelRegistry.ts create mode 100644 src/lib/api/modelRegistry.ts create mode 100644 src/lib/types/modelRegistry.ts diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 3618e216e..fcc92db03 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3674,7 +3674,7 @@ dependencies = [ [[package]] name = "proxycast" -version = "0.29.0" +version = "0.30.0" dependencies = [ "anyhow", "arboard", diff --git a/src-tauri/src/app/bootstrap.rs b/src-tauri/src/app/bootstrap.rs index 0667b76a8..8f1e04288 100644 --- a/src-tauri/src/app/bootstrap.rs +++ b/src-tauri/src/app/bootstrap.rs @@ -14,6 +14,7 @@ use crate::commands::flow_monitor_cmd::{ SessionManagerState, }; use crate::commands::machine_id_cmd::MachineIdState; +use crate::commands::model_registry_cmd::ModelRegistryState; use crate::commands::orchestrator_cmd::OrchestratorState; use crate::commands::plugin_cmd::PluginManagerState; use crate::commands::plugin_install_cmd::PluginInstallerState; @@ -132,6 +133,7 @@ pub struct AppStates { pub oauth_plugin_manager: crate::commands::oauth_plugin_cmd::OAuthPluginManagerState, pub orchestrator: OrchestratorState, pub connect_state: ConnectStateWrapper, + pub model_registry: ModelRegistryState, // 用于 setup hook 的共享实例 pub shared_stats: Arc>, pub shared_tokens: Arc>, @@ -207,6 +209,9 @@ pub fn init_states(config: &Config) -> Result { // 初始化 Connect 状态(延迟初始化,在 setup hook 中完成) let connect_state = ConnectStateWrapper(Arc::new(RwLock::new(None))); + // 初始化 Model Registry 状态(延迟初始化,在 setup hook 中完成) + let model_registry_state: ModelRegistryState = Arc::new(RwLock::new(None)); + // 初始化默认技能仓库 { let conn = db.lock().expect("Failed to lock database"); @@ -242,6 +247,7 @@ pub fn init_states(config: &Config) -> Result { oauth_plugin_manager: oauth_plugin_manager_state, orchestrator: orchestrator_state, connect_state, + model_registry: model_registry_state, shared_stats, shared_tokens, shared_logger, diff --git a/src-tauri/src/app/runner.rs b/src-tauri/src/app/runner.rs index 44a8242c6..74474dd9e 100644 --- a/src-tauri/src/app/runner.rs +++ b/src-tauri/src/app/runner.rs @@ -71,6 +71,7 @@ pub fn run() { oauth_plugin_manager: oauth_plugin_manager_state, orchestrator: orchestrator_state, connect_state: connect_state, + model_registry: model_registry_state, shared_stats, shared_tokens, shared_logger, @@ -145,6 +146,7 @@ pub fn run() { .manage(oauth_plugin_manager_state) .manage(orchestrator_state) .manage(connect_state) + .manage(model_registry_state) .on_window_event(move |window, event| { // 处理窗口关闭事件 if let tauri::WindowEvent::CloseRequested { api, .. } = event { @@ -219,6 +221,33 @@ pub fn run() { }); } + // 初始化 Model Registry 服务 + { + let app_handle = app.handle().clone(); + let db_clone = db_clone.clone(); + tauri::async_runtime::spawn(async move { + // 创建 ModelRegistryService + let service = crate::services::model_registry_service::ModelRegistryService::new(db_clone); + + // 初始化服务 + match service.initialize().await { + Ok(()) => { + tracing::info!("[启动] Model Registry 服务初始化成功"); + // 更新状态 + if let Some(state) = app_handle + .try_state::() + { + let mut guard = state.write().await; + *guard = Some(service); + } + } + Err(e) => { + tracing::error!("[启动] Model Registry 服务初始化失败: {}", e); + } + } + }); + } + // 注册 Deep Link 事件处理器(仅 macOS) // _Requirements: 1.4_ #[cfg(target_os = "macos")] @@ -984,6 +1013,17 @@ pub fn run() { commands::connect_cmd::refresh_relay_registry, commands::connect_cmd::list_relay_providers, commands::connect_cmd::send_connect_callback, + // Model Registry commands + commands::model_registry_cmd::get_model_registry, + commands::model_registry_cmd::refresh_model_registry, + commands::model_registry_cmd::search_models, + commands::model_registry_cmd::get_model_preferences, + commands::model_registry_cmd::toggle_model_favorite, + commands::model_registry_cmd::hide_model, + commands::model_registry_cmd::record_model_usage, + commands::model_registry_cmd::get_model_sync_state, + commands::model_registry_cmd::get_models_for_provider, + commands::model_registry_cmd::get_models_by_tier, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 5a634cda3..eeb387093 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -9,6 +9,7 @@ pub mod injection_cmd; pub mod kiro_local; pub mod machine_id_cmd; pub mod mcp_cmd; +pub mod model_registry_cmd; pub mod models_cmd; pub mod native_agent_cmd; pub mod network_cmd; diff --git a/src-tauri/src/commands/model_registry_cmd.rs b/src-tauri/src/commands/model_registry_cmd.rs new file mode 100644 index 000000000..0324be79d --- /dev/null +++ b/src-tauri/src/commands/model_registry_cmd.rs @@ -0,0 +1,155 @@ +//! 模型注册表 Tauri 命令 +//! +//! 提供模型注册表相关的前端 API + +use crate::models::model_registry::{ + EnhancedModelMetadata, ModelSyncState, ModelTier, UserModelPreference, +}; +use crate::services::model_registry_service::ModelRegistryService; +use std::sync::Arc; +use tauri::State; +use tokio::sync::RwLock; + +/// 模型注册服务状态 +pub type ModelRegistryState = Arc>>; + +/// 获取所有模型 +#[tauri::command] +pub async fn get_model_registry( + state: State<'_, ModelRegistryState>, +) -> Result, String> { + let guard = state.read().await; + let service = guard + .as_ref() + .ok_or_else(|| "模型注册服务未初始化".to_string())?; + + Ok(service.get_all_models().await) +} + +/// 刷新模型注册表(从 models.dev 获取最新数据) +#[tauri::command] +pub async fn refresh_model_registry( + state: State<'_, ModelRegistryState>, +) -> Result<(), String> { + let guard = state.read().await; + let service = guard + .as_ref() + .ok_or_else(|| "模型注册服务未初始化".to_string())?; + + service.refresh_from_models_dev().await +} + +/// 搜索模型 +#[tauri::command] +pub async fn search_models( + state: State<'_, ModelRegistryState>, + query: String, + limit: Option, +) -> Result, String> { + let guard = state.read().await; + let service = guard + .as_ref() + .ok_or_else(|| "模型注册服务未初始化".to_string())?; + + Ok(service.search_models(&query, limit.unwrap_or(50)).await) +} + +/// 获取用户模型偏好 +#[tauri::command] +pub async fn get_model_preferences( + state: State<'_, ModelRegistryState>, +) -> Result, String> { + let guard = state.read().await; + let service = guard + .as_ref() + .ok_or_else(|| "模型注册服务未初始化".to_string())?; + + service.get_all_preferences().await +} + +/// 切换模型收藏状态 +#[tauri::command] +pub async fn toggle_model_favorite( + state: State<'_, ModelRegistryState>, + model_id: String, +) -> Result { + let guard = state.read().await; + let service = guard + .as_ref() + .ok_or_else(|| "模型注册服务未初始化".to_string())?; + + service.toggle_favorite(&model_id).await +} + +/// 隐藏模型 +#[tauri::command] +pub async fn hide_model( + state: State<'_, ModelRegistryState>, + model_id: String, +) -> Result<(), String> { + let guard = state.read().await; + let service = guard + .as_ref() + .ok_or_else(|| "模型注册服务未初始化".to_string())?; + + service.hide_model(&model_id).await +} + +/// 记录模型使用 +#[tauri::command] +pub async fn record_model_usage( + state: State<'_, ModelRegistryState>, + model_id: String, +) -> Result<(), String> { + let guard = state.read().await; + let service = guard + .as_ref() + .ok_or_else(|| "模型注册服务未初始化".to_string())?; + + service.record_usage(&model_id).await +} + +/// 获取模型同步状态 +#[tauri::command] +pub async fn get_model_sync_state( + state: State<'_, ModelRegistryState>, +) -> Result { + let guard = state.read().await; + let service = guard + .as_ref() + .ok_or_else(|| "模型注册服务未初始化".to_string())?; + + Ok(service.get_sync_state().await) +} + +/// 按 Provider 获取模型 +#[tauri::command] +pub async fn get_models_for_provider( + state: State<'_, ModelRegistryState>, + provider_id: String, +) -> Result, String> { + let guard = state.read().await; + let service = guard + .as_ref() + .ok_or_else(|| "模型注册服务未初始化".to_string())?; + + Ok(service.get_models_by_provider(&provider_id).await) +} + +/// 按服务等级获取模型 +#[tauri::command] +pub async fn get_models_by_tier( + state: State<'_, ModelRegistryState>, + tier: String, +) -> Result, String> { + let guard = state.read().await; + let service = guard + .as_ref() + .ok_or_else(|| "模型注册服务未初始化".to_string())?; + + let tier: ModelTier = tier + .parse() + .map_err(|_| format!("无效的服务等级: {}", tier))?; + + Ok(service.get_models_by_tier(tier).await) +} diff --git a/src-tauri/src/commands/orchestrator_cmd.rs b/src-tauri/src/commands/orchestrator_cmd.rs index d7569cae1..31299b7dc 100644 --- a/src-tauri/src/commands/orchestrator_cmd.rs +++ b/src-tauri/src/commands/orchestrator_cmd.rs @@ -65,10 +65,13 @@ pub async fn init_orchestrator( .map(|c| { // 从 credential 中提取支持的模型列表 let supported_models = extract_supported_models(&c.credential); + // 保存原始的 provider_type 字符串(如 "antigravity"、"kiro" 等) + let original_provider_type = c.provider_type.to_string(); CredentialInfo { id: c.uuid.clone(), - provider_type: map_pool_provider_type(&c.provider_type.to_string()), + provider_type: map_pool_provider_type(&original_provider_type), + original_provider_type: Some(original_provider_type), supported_models, is_healthy: c.is_healthy, current_load: None, @@ -95,34 +98,47 @@ fn extract_supported_models( CredentialData::ClaudeKey { .. } | CredentialData::ClaudeOAuth { .. } => { vec![ "claude-opus-4-5-20251101".to_string(), + "claude-opus-4-20250514".to_string(), "claude-sonnet-4-5-20250929".to_string(), "claude-sonnet-4-20250514".to_string(), + "claude-haiku-4-5-20251001".to_string(), + "claude-3-7-sonnet-20250219".to_string(), "claude-3-5-haiku-20241022".to_string(), ] } CredentialData::OpenAIKey { .. } => { vec![ + "gpt-5.2-codex".to_string(), + "gpt-5.2".to_string(), + "gpt-5.1-codex-max".to_string(), + "gpt-5.1-codex".to_string(), + "gpt-5.1-codex-mini".to_string(), + "gpt-5.1".to_string(), + "gpt-5-codex".to_string(), + "gpt-5-codex-mini".to_string(), + "gpt-5".to_string(), "gpt-4o".to_string(), "gpt-4o-mini".to_string(), - "gpt-4-turbo".to_string(), - "o1".to_string(), - "o1-mini".to_string(), ] } CredentialData::GeminiOAuth { .. } => { vec![ - "gemini-2.0-flash-exp".to_string(), - "gemini-1.5-pro".to_string(), - "gemini-1.5-flash".to_string(), + "gemini-3-pro-preview".to_string(), + "gemini-3-flash-preview".to_string(), + "gemini-2.5-pro".to_string(), + "gemini-2.5-flash".to_string(), + "gemini-2.5-flash-lite".to_string(), ] } CredentialData::GeminiApiKey { excluded_models, .. } => { let all_models = vec![ - "gemini-2.0-flash-exp".to_string(), - "gemini-1.5-pro".to_string(), - "gemini-1.5-flash".to_string(), + "gemini-3-pro-preview".to_string(), + "gemini-3-flash-preview".to_string(), + "gemini-2.5-pro".to_string(), + "gemini-2.5-flash".to_string(), + "gemini-2.5-flash-lite".to_string(), ]; all_models .into_iter() @@ -131,8 +147,13 @@ fn extract_supported_models( } CredentialData::KiroOAuth { .. } => { vec![ + "claude-opus-4-5".to_string(), + "claude-opus-4-5-20251101".to_string(), + "claude-haiku-4-5".to_string(), + "claude-sonnet-4-5".to_string(), "claude-sonnet-4-5-20250929".to_string(), "claude-sonnet-4-20250514".to_string(), + "claude-3-7-sonnet-20250219".to_string(), ] } CredentialData::CodexOAuth { .. } => { @@ -140,16 +161,23 @@ fn extract_supported_models( } CredentialData::QwenOAuth { .. } => { vec![ - "qwen-max".to_string(), - "qwen-plus".to_string(), - "qwen-turbo".to_string(), + "qwen3-coder-plus".to_string(), + "qwen3-coder-flash".to_string(), ] } CredentialData::AntigravityOAuth { .. } => { vec![ + // Max 等级 + "gemini-3-pro-preview".to_string(), + "gemini-3-pro-image-preview".to_string(), + "gemini-claude-opus-4-5-thinking".to_string(), + // Pro 等级 + "gemini-2.5-flash".to_string(), + "gemini-2.5-computer-use-preview-10-2025".to_string(), "gemini-claude-sonnet-4-5".to_string(), "gemini-claude-sonnet-4-5-thinking".to_string(), - "gemini-claude-opus-4-5-thinking".to_string(), + // Mini 等级 + "gemini-3-flash-preview".to_string(), ] } _ => vec![], @@ -165,6 +193,7 @@ fn map_pool_provider_type(pool_type: &str) -> ProviderType { "kiro" => ProviderType::Kiro, "codex" => ProviderType::OpenAI, "qwen" => ProviderType::Custom, + "antigravity" => ProviderType::Antigravity, _ => ProviderType::Custom, } } @@ -233,10 +262,13 @@ pub struct CredentialInfoRequest { impl From for CredentialInfo { fn from(req: CredentialInfoRequest) -> Self { + // 保存原始的 provider_type 字符串 + let original_provider_type = req.provider_type.clone(); CredentialInfo { id: req.id, provider_type: ProviderType::from_str(&req.provider_type) .unwrap_or(ProviderType::Custom), + original_provider_type: Some(original_provider_type), supported_models: req.supported_models, is_healthy: req.is_healthy, current_load: req.current_load, diff --git a/src-tauri/src/converter/openai_to_antigravity.rs b/src-tauri/src/converter/openai_to_antigravity.rs index ed53ffd9a..4a2539f14 100644 --- a/src-tauri/src/converter/openai_to_antigravity.rs +++ b/src-tauri/src/converter/openai_to_antigravity.rs @@ -589,13 +589,10 @@ pub fn convert_openai_to_antigravity_with_context( } // 转换工具定义 - // 注意:Antigravity API 统一使用 Gemini 格式(function_declarations) - // Claude 模型在 Antigravity 内部会自动转换 + // 注意:Antigravity API 统一使用 functionDeclarations 格式 + // Claude 和 Gemini 模型都使用相同的结构,但字段名可能不同 let tools: Option = request.tools.as_ref().and_then(|tools| { let is_claude = is_claude_model(actual_model); - - // Gemini 模型使用 function_declarations + parametersJsonSchema - // Claude 模型使用 function_declarations + inputSchema(注意字段名不同) let mut function_declarations: Vec = Vec::new(); for t in tools { @@ -605,7 +602,8 @@ pub fn convert_openai_to_antigravity_with_context( .parameters .as_ref() .map(|p| { - let mut schema = clean_parameters(Some(p.clone())).unwrap_or_default(); + let mut schema = + clean_parameters(Some(p.clone())).unwrap_or_default(); // 确保有 type 和 properties if schema.get("type").is_none() { schema["type"] = serde_json::json!("object"); @@ -615,20 +613,22 @@ pub fn convert_openai_to_antigravity_with_context( } schema }) - .unwrap_or_else(|| serde_json::json!({"type": "object", "properties": {}})); + .unwrap_or_else(|| { + serde_json::json!({"type": "object", "properties": {}}) + }); if is_claude { - // Claude 模型使用 inputSchema 字段名 + // Claude 模型使用 parameters 字段(标准 Gemini 格式) function_declarations.push(serde_json::json!({ "name": function.name, "description": function.description.clone().unwrap_or_default(), - "inputSchema": params_schema + "parameters": params_schema })); } else { - // Gemini 模型使用 parametersJsonSchema 字段名 + // Gemini 模型使用 parametersJsonSchema 字段 function_declarations.push(serde_json::json!({ "name": function.name, - "description": function.description.clone(), + "description": function.description.clone().unwrap_or_default(), "parametersJsonSchema": params_schema })); } diff --git a/src-tauri/src/credential/registry.rs b/src-tauri/src/credential/registry.rs index 8ba9f5a01..f902df922 100644 --- a/src-tauri/src/credential/registry.rs +++ b/src-tauri/src/credential/registry.rs @@ -539,6 +539,54 @@ impl CredentialProviderRegistry { info!("Plugin installed to: {:?}", target_dir); + // 尝试下载 UI 资源包(如果存在) + let ui_url = if version_tag == "latest" { + format!( + "https://github.com/{}/{}/releases/latest/download/{}-ui.zip", + owner, repo, repo + ) + } else { + format!( + "https://github.com/{}/{}/releases/download/{}/{}-ui.zip", + owner, repo, version_tag, repo + ) + }; + + info!("Trying to download UI assets from: {}", ui_url); + + if let Ok(ui_response) = client.get(&ui_url).send().await { + if ui_response.status().is_success() { + if let Ok(ui_bytes) = ui_response.bytes().await { + let ui_cursor = std::io::Cursor::new(ui_bytes); + if let Ok(mut ui_archive) = zip::ZipArchive::new(ui_cursor) { + for i in 0..ui_archive.len() { + if let Ok(mut file) = ui_archive.by_index(i) { + let outpath = target_dir.join(file.name()); + + if file.name().ends_with('/') { + let _ = std::fs::create_dir_all(&outpath); + } else { + if let Some(p) = outpath.parent() { + if !p.exists() { + let _ = std::fs::create_dir_all(p); + } + } + if let Ok(mut outfile) = std::fs::File::create(&outpath) { + let _ = std::io::copy(&mut file, &mut outfile); + } + } + } + } + info!("UI assets installed for plugin: {}", plugin_id); + } + } + } else { + info!("No UI assets available for plugin: {} (HTTP {})", plugin_id, ui_response.status()); + } + } else { + info!("No UI assets available for plugin: {}", plugin_id); + } + // 注册插件(创建 PluginInstance) self.register_from_dir(&target_dir, &plugin_id).await?; diff --git a/src-tauri/src/data/local_models.rs b/src-tauri/src/data/local_models.rs new file mode 100644 index 000000000..4d8928b03 --- /dev/null +++ b/src-tauri/src/data/local_models.rs @@ -0,0 +1,761 @@ +//! 本地硬编码的国内模型数据 +//! +//! 这些模型数据用于补充 models.dev API 未覆盖的国内模型 + +use crate::models::model_registry::{ + EnhancedModelMetadata, ModelCapabilities, ModelLimits, ModelPricing, ModelSource, ModelStatus, + ModelTier, +}; + +/// 获取所有本地硬编码的国内模型 +pub fn get_local_models() -> Vec { + let mut models = Vec::new(); + models.extend(get_dashscope_models()); + models.extend(get_zhipu_models()); + models.extend(get_baichuan_models()); + models.extend(get_moonshot_models()); + models.extend(get_deepseek_models()); + models.extend(get_doubao_models()); + models.extend(get_minimax_models()); + models.extend(get_yi_models()); + models.extend(get_stepfun_models()); + models +} + +/// 通义千问系列模型 (阿里云百炼) +fn get_dashscope_models() -> Vec { + let now = chrono::Utc::now().timestamp(); + vec![ + EnhancedModelMetadata { + id: "qwen3-coder-plus".to_string(), + display_name: "通义千问 Coder Plus".to_string(), + provider_id: "dashscope".to_string(), + provider_name: "阿里云百炼".to_string(), + family: Some("qwen-coder".to_string()), + tier: ModelTier::Pro, + capabilities: ModelCapabilities { + vision: false, tools: true, streaming: true, + json_mode: true, function_calling: true, reasoning: false, + }, + pricing: Some(ModelPricing { + input_per_million: Some(4.0), output_per_million: Some(16.0), + cache_read_per_million: None, cache_write_per_million: None, + currency: "CNY".to_string(), + }), + limits: ModelLimits { + context_length: Some(131072), max_output_tokens: Some(8192), + requests_per_minute: None, tokens_per_minute: None, + }, + status: ModelStatus::Active, + release_date: Some("2025-01-01".to_string()), + is_latest: true, + description: Some("阿里云通义千问代码模型".to_string()), + source: ModelSource::Local, + created_at: now, updated_at: now, + }, + EnhancedModelMetadata { + id: "qwen-max".to_string(), + display_name: "通义千问 Max".to_string(), + provider_id: "dashscope".to_string(), + provider_name: "阿里云百炼".to_string(), + family: Some("qwen".to_string()), + tier: ModelTier::Max, + capabilities: ModelCapabilities { + vision: true, tools: true, streaming: true, + json_mode: true, function_calling: true, reasoning: true, + }, + pricing: Some(ModelPricing { + input_per_million: Some(20.0), output_per_million: Some(60.0), + cache_read_per_million: None, cache_write_per_million: None, + currency: "CNY".to_string(), + }), + limits: ModelLimits { + context_length: Some(32768), max_output_tokens: Some(8192), + requests_per_minute: None, tokens_per_minute: None, + }, + status: ModelStatus::Active, + release_date: Some("2024-12-01".to_string()), + is_latest: true, + description: Some("通义千问旗舰模型".to_string()), + source: ModelSource::Local, + created_at: now, updated_at: now, + }, + EnhancedModelMetadata { + id: "qwen-plus".to_string(), + display_name: "通义千问 Plus".to_string(), + provider_id: "dashscope".to_string(), + provider_name: "阿里云百炼".to_string(), + family: Some("qwen".to_string()), + tier: ModelTier::Pro, + capabilities: ModelCapabilities { + vision: true, tools: true, streaming: true, + json_mode: true, function_calling: true, reasoning: false, + }, + pricing: Some(ModelPricing { + input_per_million: Some(4.0), output_per_million: Some(12.0), + cache_read_per_million: None, cache_write_per_million: None, + currency: "CNY".to_string(), + }), + limits: ModelLimits { + context_length: Some(131072), max_output_tokens: Some(8192), + requests_per_minute: None, tokens_per_minute: None, + }, + status: ModelStatus::Active, + release_date: Some("2024-12-01".to_string()), + is_latest: true, + description: Some("通义千问增强版".to_string()), + source: ModelSource::Local, + created_at: now, updated_at: now, + }, + EnhancedModelMetadata { + id: "qwen-turbo".to_string(), + display_name: "通义千问 Turbo".to_string(), + provider_id: "dashscope".to_string(), + provider_name: "阿里云百炼".to_string(), + family: Some("qwen".to_string()), + tier: ModelTier::Mini, + capabilities: ModelCapabilities { + vision: false, tools: true, streaming: true, + json_mode: true, function_calling: true, reasoning: false, + }, + pricing: Some(ModelPricing { + input_per_million: Some(0.3), output_per_million: Some(0.6), + cache_read_per_million: None, cache_write_per_million: None, + currency: "CNY".to_string(), + }), + limits: ModelLimits { + context_length: Some(131072), max_output_tokens: Some(8192), + requests_per_minute: None, tokens_per_minute: None, + }, + status: ModelStatus::Active, + release_date: Some("2024-12-01".to_string()), + is_latest: true, + description: Some("通义千问快速版".to_string()), + source: ModelSource::Local, + created_at: now, updated_at: now, + }, + ] +} + +/// 智谱 GLM 系列模型 +fn get_zhipu_models() -> Vec { + let now = chrono::Utc::now().timestamp(); + vec![ + EnhancedModelMetadata { + id: "glm-4-plus".to_string(), + display_name: "GLM-4 Plus".to_string(), + provider_id: "zhipu".to_string(), + provider_name: "智谱 AI".to_string(), + family: Some("glm-4".to_string()), + tier: ModelTier::Max, + capabilities: ModelCapabilities { + vision: true, tools: true, streaming: true, + json_mode: true, function_calling: true, reasoning: true, + }, + pricing: Some(ModelPricing { + input_per_million: Some(50.0), output_per_million: Some(50.0), + cache_read_per_million: None, cache_write_per_million: None, + currency: "CNY".to_string(), + }), + limits: ModelLimits { + context_length: Some(128000), max_output_tokens: Some(4096), + requests_per_minute: None, tokens_per_minute: None, + }, + status: ModelStatus::Active, + release_date: Some("2024-08-01".to_string()), + is_latest: true, + description: Some("智谱旗舰模型".to_string()), + source: ModelSource::Local, + created_at: now, updated_at: now, + }, + EnhancedModelMetadata { + id: "glm-4-air".to_string(), + display_name: "GLM-4 Air".to_string(), + provider_id: "zhipu".to_string(), + provider_name: "智谱 AI".to_string(), + family: Some("glm-4".to_string()), + tier: ModelTier::Pro, + capabilities: ModelCapabilities { + vision: false, tools: true, streaming: true, + json_mode: true, function_calling: true, reasoning: false, + }, + pricing: Some(ModelPricing { + input_per_million: Some(1.0), output_per_million: Some(1.0), + cache_read_per_million: None, cache_write_per_million: None, + currency: "CNY".to_string(), + }), + limits: ModelLimits { + context_length: Some(128000), max_output_tokens: Some(4096), + requests_per_minute: None, tokens_per_minute: None, + }, + status: ModelStatus::Active, + release_date: Some("2024-06-01".to_string()), + is_latest: true, + description: Some("智谱高性价比模型".to_string()), + source: ModelSource::Local, + created_at: now, updated_at: now, + }, + EnhancedModelMetadata { + id: "glm-4-flash".to_string(), + display_name: "GLM-4 Flash".to_string(), + provider_id: "zhipu".to_string(), + provider_name: "智谱 AI".to_string(), + family: Some("glm-4".to_string()), + tier: ModelTier::Mini, + capabilities: ModelCapabilities { + vision: false, tools: true, streaming: true, + json_mode: true, function_calling: true, reasoning: false, + }, + pricing: Some(ModelPricing { + input_per_million: Some(0.1), output_per_million: Some(0.1), + cache_read_per_million: None, cache_write_per_million: None, + currency: "CNY".to_string(), + }), + limits: ModelLimits { + context_length: Some(128000), max_output_tokens: Some(4096), + requests_per_minute: None, tokens_per_minute: None, + }, + status: ModelStatus::Active, + release_date: Some("2024-06-01".to_string()), + is_latest: true, + description: Some("智谱快速模型".to_string()), + source: ModelSource::Local, + created_at: now, updated_at: now, + }, + ] +} + +/// 百川系列模型 +fn get_baichuan_models() -> Vec { + let now = chrono::Utc::now().timestamp(); + vec![ + EnhancedModelMetadata { + id: "Baichuan4".to_string(), + display_name: "百川 4".to_string(), + provider_id: "baichuan".to_string(), + provider_name: "百川智能".to_string(), + family: Some("baichuan".to_string()), + tier: ModelTier::Max, + capabilities: ModelCapabilities { + vision: false, tools: true, streaming: true, + json_mode: true, function_calling: true, reasoning: false, + }, + pricing: Some(ModelPricing { + input_per_million: Some(100.0), output_per_million: Some(100.0), + cache_read_per_million: None, cache_write_per_million: None, + currency: "CNY".to_string(), + }), + limits: ModelLimits { + context_length: Some(32768), max_output_tokens: Some(4096), + requests_per_minute: None, tokens_per_minute: None, + }, + status: ModelStatus::Active, + release_date: Some("2024-10-01".to_string()), + is_latest: true, + description: Some("百川旗舰模型".to_string()), + source: ModelSource::Local, + created_at: now, updated_at: now, + }, + EnhancedModelMetadata { + id: "Baichuan3-Turbo".to_string(), + display_name: "百川 3 Turbo".to_string(), + provider_id: "baichuan".to_string(), + provider_name: "百川智能".to_string(), + family: Some("baichuan".to_string()), + tier: ModelTier::Pro, + capabilities: ModelCapabilities { + vision: false, tools: true, streaming: true, + json_mode: true, function_calling: true, reasoning: false, + }, + pricing: Some(ModelPricing { + input_per_million: Some(12.0), output_per_million: Some(12.0), + cache_read_per_million: None, cache_write_per_million: None, + currency: "CNY".to_string(), + }), + limits: ModelLimits { + context_length: Some(32768), max_output_tokens: Some(4096), + requests_per_minute: None, tokens_per_minute: None, + }, + status: ModelStatus::Active, + release_date: Some("2024-06-01".to_string()), + is_latest: true, + description: Some("百川高性价比模型".to_string()), + source: ModelSource::Local, + created_at: now, updated_at: now, + }, + ] +} + +/// 月之暗面 Moonshot 系列模型 +fn get_moonshot_models() -> Vec { + let now = chrono::Utc::now().timestamp(); + vec![ + EnhancedModelMetadata { + id: "moonshot-v1-128k".to_string(), + display_name: "Moonshot V1 128K".to_string(), + provider_id: "moonshot".to_string(), + provider_name: "月之暗面".to_string(), + family: Some("moonshot".to_string()), + tier: ModelTier::Pro, + capabilities: ModelCapabilities { + vision: false, tools: true, streaming: true, + json_mode: true, function_calling: true, reasoning: false, + }, + pricing: Some(ModelPricing { + input_per_million: Some(60.0), output_per_million: Some(60.0), + cache_read_per_million: None, cache_write_per_million: None, + currency: "CNY".to_string(), + }), + limits: ModelLimits { + context_length: Some(128000), max_output_tokens: Some(4096), + requests_per_minute: None, tokens_per_minute: None, + }, + status: ModelStatus::Active, + release_date: Some("2024-03-01".to_string()), + is_latest: true, + description: Some("月之暗面长上下文模型".to_string()), + source: ModelSource::Local, + created_at: now, updated_at: now, + }, + EnhancedModelMetadata { + id: "moonshot-v1-32k".to_string(), + display_name: "Moonshot V1 32K".to_string(), + provider_id: "moonshot".to_string(), + provider_name: "月之暗面".to_string(), + family: Some("moonshot".to_string()), + tier: ModelTier::Pro, + capabilities: ModelCapabilities { + vision: false, tools: true, streaming: true, + json_mode: true, function_calling: true, reasoning: false, + }, + pricing: Some(ModelPricing { + input_per_million: Some(24.0), output_per_million: Some(24.0), + cache_read_per_million: None, cache_write_per_million: None, + currency: "CNY".to_string(), + }), + limits: ModelLimits { + context_length: Some(32000), max_output_tokens: Some(4096), + requests_per_minute: None, tokens_per_minute: None, + }, + status: ModelStatus::Active, + release_date: Some("2024-03-01".to_string()), + is_latest: true, + description: Some("月之暗面标准模型".to_string()), + source: ModelSource::Local, + created_at: now, updated_at: now, + }, + EnhancedModelMetadata { + id: "moonshot-v1-8k".to_string(), + display_name: "Moonshot V1 8K".to_string(), + provider_id: "moonshot".to_string(), + provider_name: "月之暗面".to_string(), + family: Some("moonshot".to_string()), + tier: ModelTier::Mini, + capabilities: ModelCapabilities { + vision: false, tools: true, streaming: true, + json_mode: true, function_calling: true, reasoning: false, + }, + pricing: Some(ModelPricing { + input_per_million: Some(12.0), output_per_million: Some(12.0), + cache_read_per_million: None, cache_write_per_million: None, + currency: "CNY".to_string(), + }), + limits: ModelLimits { + context_length: Some(8000), max_output_tokens: Some(4096), + requests_per_minute: None, tokens_per_minute: None, + }, + status: ModelStatus::Active, + release_date: Some("2024-03-01".to_string()), + is_latest: true, + description: Some("月之暗面快速模型".to_string()), + source: ModelSource::Local, + created_at: now, updated_at: now, + }, + ] +} + +/// DeepSeek 系列模型 +fn get_deepseek_models() -> Vec { + let now = chrono::Utc::now().timestamp(); + vec![ + EnhancedModelMetadata { + id: "deepseek-chat".to_string(), + display_name: "DeepSeek Chat".to_string(), + provider_id: "deepseek".to_string(), + provider_name: "DeepSeek".to_string(), + family: Some("deepseek".to_string()), + tier: ModelTier::Pro, + capabilities: ModelCapabilities { + vision: false, tools: true, streaming: true, + json_mode: true, function_calling: true, reasoning: false, + }, + pricing: Some(ModelPricing { + input_per_million: Some(1.0), output_per_million: Some(2.0), + cache_read_per_million: Some(0.1), cache_write_per_million: Some(1.0), + currency: "CNY".to_string(), + }), + limits: ModelLimits { + context_length: Some(64000), max_output_tokens: Some(8192), + requests_per_minute: None, tokens_per_minute: None, + }, + status: ModelStatus::Active, + release_date: Some("2024-12-01".to_string()), + is_latest: true, + description: Some("DeepSeek V3 对话模型".to_string()), + source: ModelSource::Local, + created_at: now, updated_at: now, + }, + EnhancedModelMetadata { + id: "deepseek-reasoner".to_string(), + display_name: "DeepSeek Reasoner".to_string(), + provider_id: "deepseek".to_string(), + provider_name: "DeepSeek".to_string(), + family: Some("deepseek".to_string()), + tier: ModelTier::Max, + capabilities: ModelCapabilities { + vision: false, tools: false, streaming: true, + json_mode: false, function_calling: false, reasoning: true, + }, + pricing: Some(ModelPricing { + input_per_million: Some(4.0), output_per_million: Some(16.0), + cache_read_per_million: Some(0.4), cache_write_per_million: Some(4.0), + currency: "CNY".to_string(), + }), + limits: ModelLimits { + context_length: Some(64000), max_output_tokens: Some(8192), + requests_per_minute: None, tokens_per_minute: None, + }, + status: ModelStatus::Active, + release_date: Some("2025-01-01".to_string()), + is_latest: true, + description: Some("DeepSeek R1 推理模型".to_string()), + source: ModelSource::Local, + created_at: now, updated_at: now, + }, + EnhancedModelMetadata { + id: "deepseek-coder".to_string(), + display_name: "DeepSeek Coder".to_string(), + provider_id: "deepseek".to_string(), + provider_name: "DeepSeek".to_string(), + family: Some("deepseek-coder".to_string()), + tier: ModelTier::Pro, + capabilities: ModelCapabilities { + vision: false, tools: true, streaming: true, + json_mode: true, function_calling: true, reasoning: false, + }, + pricing: Some(ModelPricing { + input_per_million: Some(1.0), output_per_million: Some(2.0), + cache_read_per_million: Some(0.1), cache_write_per_million: Some(1.0), + currency: "CNY".to_string(), + }), + limits: ModelLimits { + context_length: Some(64000), max_output_tokens: Some(8192), + requests_per_minute: None, tokens_per_minute: None, + }, + status: ModelStatus::Active, + release_date: Some("2024-06-01".to_string()), + is_latest: true, + description: Some("DeepSeek 代码模型".to_string()), + source: ModelSource::Local, + created_at: now, updated_at: now, + }, + ] +} + +/// 字节豆包系列模型 +fn get_doubao_models() -> Vec { + let now = chrono::Utc::now().timestamp(); + vec![ + EnhancedModelMetadata { + id: "doubao-pro-256k".to_string(), + display_name: "豆包 Pro 256K".to_string(), + provider_id: "doubao".to_string(), + provider_name: "字节跳动".to_string(), + family: Some("doubao".to_string()), + tier: ModelTier::Max, + capabilities: ModelCapabilities { + vision: true, tools: true, streaming: true, + json_mode: true, function_calling: true, reasoning: false, + }, + pricing: Some(ModelPricing { + input_per_million: Some(5.0), output_per_million: Some(9.0), + cache_read_per_million: None, cache_write_per_million: None, + currency: "CNY".to_string(), + }), + limits: ModelLimits { + context_length: Some(256000), max_output_tokens: Some(4096), + requests_per_minute: None, tokens_per_minute: None, + }, + status: ModelStatus::Active, + release_date: Some("2024-10-01".to_string()), + is_latest: true, + description: Some("豆包旗舰长上下文模型".to_string()), + source: ModelSource::Local, + created_at: now, updated_at: now, + }, + EnhancedModelMetadata { + id: "doubao-pro-32k".to_string(), + display_name: "豆包 Pro 32K".to_string(), + provider_id: "doubao".to_string(), + provider_name: "字节跳动".to_string(), + family: Some("doubao".to_string()), + tier: ModelTier::Pro, + capabilities: ModelCapabilities { + vision: true, tools: true, streaming: true, + json_mode: true, function_calling: true, reasoning: false, + }, + pricing: Some(ModelPricing { + input_per_million: Some(0.8), output_per_million: Some(2.0), + cache_read_per_million: None, cache_write_per_million: None, + currency: "CNY".to_string(), + }), + limits: ModelLimits { + context_length: Some(32000), max_output_tokens: Some(4096), + requests_per_minute: None, tokens_per_minute: None, + }, + status: ModelStatus::Active, + release_date: Some("2024-06-01".to_string()), + is_latest: true, + description: Some("豆包标准模型".to_string()), + source: ModelSource::Local, + created_at: now, updated_at: now, + }, + EnhancedModelMetadata { + id: "doubao-lite-32k".to_string(), + display_name: "豆包 Lite 32K".to_string(), + provider_id: "doubao".to_string(), + provider_name: "字节跳动".to_string(), + family: Some("doubao".to_string()), + tier: ModelTier::Mini, + capabilities: ModelCapabilities { + vision: false, tools: true, streaming: true, + json_mode: true, function_calling: true, reasoning: false, + }, + pricing: Some(ModelPricing { + input_per_million: Some(0.3), output_per_million: Some(0.6), + cache_read_per_million: None, cache_write_per_million: None, + currency: "CNY".to_string(), + }), + limits: ModelLimits { + context_length: Some(32000), max_output_tokens: Some(4096), + requests_per_minute: None, tokens_per_minute: None, + }, + status: ModelStatus::Active, + release_date: Some("2024-06-01".to_string()), + is_latest: true, + description: Some("豆包轻量模型".to_string()), + source: ModelSource::Local, + created_at: now, updated_at: now, + }, + ] +} + +/// MiniMax 系列模型 +fn get_minimax_models() -> Vec { + let now = chrono::Utc::now().timestamp(); + vec![ + EnhancedModelMetadata { + id: "abab6.5s-chat".to_string(), + display_name: "MiniMax abab6.5s".to_string(), + provider_id: "minimax".to_string(), + provider_name: "MiniMax".to_string(), + family: Some("abab".to_string()), + tier: ModelTier::Pro, + capabilities: ModelCapabilities { + vision: false, tools: true, streaming: true, + json_mode: true, function_calling: true, reasoning: false, + }, + pricing: Some(ModelPricing { + input_per_million: Some(1.0), output_per_million: Some(1.0), + cache_read_per_million: None, cache_write_per_million: None, + currency: "CNY".to_string(), + }), + limits: ModelLimits { + context_length: Some(245760), max_output_tokens: Some(8192), + requests_per_minute: None, tokens_per_minute: None, + }, + status: ModelStatus::Active, + release_date: Some("2024-06-01".to_string()), + is_latest: true, + description: Some("MiniMax 长上下文模型".to_string()), + source: ModelSource::Local, + created_at: now, updated_at: now, + }, + ] +} + +/// 零一万物 Yi 系列模型 +fn get_yi_models() -> Vec { + let now = chrono::Utc::now().timestamp(); + vec![ + EnhancedModelMetadata { + id: "yi-large".to_string(), + display_name: "Yi Large".to_string(), + provider_id: "yi".to_string(), + provider_name: "零一万物".to_string(), + family: Some("yi".to_string()), + tier: ModelTier::Max, + capabilities: ModelCapabilities { + vision: false, tools: true, streaming: true, + json_mode: true, function_calling: true, reasoning: false, + }, + pricing: Some(ModelPricing { + input_per_million: Some(20.0), output_per_million: Some(20.0), + cache_read_per_million: None, cache_write_per_million: None, + currency: "CNY".to_string(), + }), + limits: ModelLimits { + context_length: Some(32768), max_output_tokens: Some(4096), + requests_per_minute: None, tokens_per_minute: None, + }, + status: ModelStatus::Active, + release_date: Some("2024-05-01".to_string()), + is_latest: true, + description: Some("零一万物旗舰模型".to_string()), + source: ModelSource::Local, + created_at: now, updated_at: now, + }, + EnhancedModelMetadata { + id: "yi-medium".to_string(), + display_name: "Yi Medium".to_string(), + provider_id: "yi".to_string(), + provider_name: "零一万物".to_string(), + family: Some("yi".to_string()), + tier: ModelTier::Pro, + capabilities: ModelCapabilities { + vision: false, tools: true, streaming: true, + json_mode: true, function_calling: true, reasoning: false, + }, + pricing: Some(ModelPricing { + input_per_million: Some(2.5), output_per_million: Some(2.5), + cache_read_per_million: None, cache_write_per_million: None, + currency: "CNY".to_string(), + }), + limits: ModelLimits { + context_length: Some(16384), max_output_tokens: Some(4096), + requests_per_minute: None, tokens_per_minute: None, + }, + status: ModelStatus::Active, + release_date: Some("2024-05-01".to_string()), + is_latest: true, + description: Some("零一万物标准模型".to_string()), + source: ModelSource::Local, + created_at: now, updated_at: now, + }, + EnhancedModelMetadata { + id: "yi-spark".to_string(), + display_name: "Yi Spark".to_string(), + provider_id: "yi".to_string(), + provider_name: "零一万物".to_string(), + family: Some("yi".to_string()), + tier: ModelTier::Mini, + capabilities: ModelCapabilities { + vision: false, tools: true, streaming: true, + json_mode: true, function_calling: true, reasoning: false, + }, + pricing: Some(ModelPricing { + input_per_million: Some(1.0), output_per_million: Some(1.0), + cache_read_per_million: None, cache_write_per_million: None, + currency: "CNY".to_string(), + }), + limits: ModelLimits { + context_length: Some(16384), max_output_tokens: Some(4096), + requests_per_minute: None, tokens_per_minute: None, + }, + status: ModelStatus::Active, + release_date: Some("2024-05-01".to_string()), + is_latest: true, + description: Some("零一万物快速模型".to_string()), + source: ModelSource::Local, + created_at: now, updated_at: now, + }, + ] +} + +/// 阶跃星辰 Step 系列模型 +fn get_stepfun_models() -> Vec { + let now = chrono::Utc::now().timestamp(); + vec![ + EnhancedModelMetadata { + id: "step-2-16k".to_string(), + display_name: "Step 2 16K".to_string(), + provider_id: "stepfun".to_string(), + provider_name: "阶跃星辰".to_string(), + family: Some("step".to_string()), + tier: ModelTier::Max, + capabilities: ModelCapabilities { + vision: true, tools: true, streaming: true, + json_mode: true, function_calling: true, reasoning: false, + }, + pricing: Some(ModelPricing { + input_per_million: Some(38.0), output_per_million: Some(120.0), + cache_read_per_million: None, cache_write_per_million: None, + currency: "CNY".to_string(), + }), + limits: ModelLimits { + context_length: Some(16384), max_output_tokens: Some(4096), + requests_per_minute: None, tokens_per_minute: None, + }, + status: ModelStatus::Active, + release_date: Some("2024-09-01".to_string()), + is_latest: true, + description: Some("阶跃星辰旗舰模型".to_string()), + source: ModelSource::Local, + created_at: now, updated_at: now, + }, + EnhancedModelMetadata { + id: "step-1-128k".to_string(), + display_name: "Step 1 128K".to_string(), + provider_id: "stepfun".to_string(), + provider_name: "阶跃星辰".to_string(), + family: Some("step".to_string()), + tier: ModelTier::Pro, + capabilities: ModelCapabilities { + vision: false, tools: true, streaming: true, + json_mode: true, function_calling: true, reasoning: false, + }, + pricing: Some(ModelPricing { + input_per_million: Some(40.0), output_per_million: Some(100.0), + cache_read_per_million: None, cache_write_per_million: None, + currency: "CNY".to_string(), + }), + limits: ModelLimits { + context_length: Some(128000), max_output_tokens: Some(4096), + requests_per_minute: None, tokens_per_minute: None, + }, + status: ModelStatus::Active, + release_date: Some("2024-06-01".to_string()), + is_latest: true, + description: Some("阶跃星辰长上下文模型".to_string()), + source: ModelSource::Local, + created_at: now, updated_at: now, + }, + EnhancedModelMetadata { + id: "step-1-flash".to_string(), + display_name: "Step 1 Flash".to_string(), + provider_id: "stepfun".to_string(), + provider_name: "阶跃星辰".to_string(), + family: Some("step".to_string()), + tier: ModelTier::Mini, + capabilities: ModelCapabilities { + vision: false, tools: true, streaming: true, + json_mode: true, function_calling: true, reasoning: false, + }, + pricing: Some(ModelPricing { + input_per_million: Some(1.0), output_per_million: Some(4.0), + cache_read_per_million: None, cache_write_per_million: None, + currency: "CNY".to_string(), + }), + limits: ModelLimits { + context_length: Some(8192), max_output_tokens: Some(4096), + requests_per_minute: None, tokens_per_minute: None, + }, + status: ModelStatus::Active, + release_date: Some("2024-06-01".to_string()), + is_latest: true, + description: Some("阶跃星辰快速模型".to_string()), + source: ModelSource::Local, + created_at: now, updated_at: now, + }, + ] +} diff --git a/src-tauri/src/data/mod.rs b/src-tauri/src/data/mod.rs new file mode 100644 index 000000000..390169bb4 --- /dev/null +++ b/src-tauri/src/data/mod.rs @@ -0,0 +1,7 @@ +//! 静态数据模块 +//! +//! 包含本地硬编码的模型数据等 + +pub mod local_models; + +pub use local_models::get_local_models; diff --git a/src-tauri/src/database/schema.rs b/src-tauri/src/database/schema.rs index 168dbf025..06dea7ee5 100644 --- a/src-tauri/src/database/schema.rs +++ b/src-tauri/src/database/schema.rs @@ -454,6 +454,89 @@ pub fn create_tables(conn: &Connection) -> Result<(), rusqlite::Error> { // ProxyCast Connect 相关表 // ============================================================================ + // ============================================================================ + // Model Registry 相关表 (借鉴 opencode 的模型管理方式) + // ============================================================================ + + // 增强的模型注册表 + // 存储从 models.dev API 获取的模型数据 + 本地补充的国内模型数据 + conn.execute( + "CREATE TABLE IF NOT EXISTS model_registry ( + id TEXT PRIMARY KEY, + display_name TEXT NOT NULL, + provider_id TEXT NOT NULL, + provider_name TEXT NOT NULL, + family TEXT, + tier TEXT NOT NULL DEFAULT 'pro', + capabilities TEXT NOT NULL DEFAULT '{}', + pricing TEXT, + limits TEXT NOT NULL DEFAULT '{}', + status TEXT NOT NULL DEFAULT 'active', + release_date TEXT, + is_latest INTEGER DEFAULT 0, + description TEXT, + source TEXT NOT NULL DEFAULT 'local', + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + )", + [], + )?; + + // 创建 model_registry 索引 + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_model_registry_provider ON model_registry(provider_id)", + [], + )?; + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_model_registry_tier ON model_registry(tier)", + [], + )?; + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_model_registry_family ON model_registry(family)", + [], + )?; + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_model_registry_source ON model_registry(source)", + [], + )?; + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_model_registry_status ON model_registry(status)", + [], + )?; + + // 用户模型偏好表 + // 存储用户的收藏、隐藏、使用统计等偏好 + conn.execute( + "CREATE TABLE IF NOT EXISTS user_model_preferences ( + model_id TEXT PRIMARY KEY, + is_favorite INTEGER DEFAULT 0, + is_hidden INTEGER DEFAULT 0, + custom_alias TEXT, + usage_count INTEGER DEFAULT 0, + last_used_at INTEGER, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + )", + [], + )?; + + // 创建 user_model_preferences 索引 + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_user_model_preferences_favorite ON user_model_preferences(is_favorite)", + [], + )?; + + // 模型同步状态表 + // 记录 models.dev API 同步状态 + conn.execute( + "CREATE TABLE IF NOT EXISTS model_sync_state ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + updated_at INTEGER NOT NULL + )", + [], + )?; + Ok(()) } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index de1d1cad1..a9b793006 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -31,6 +31,7 @@ pub mod websocket; mod commands; mod config; mod converter; +mod data; mod logger; mod models; mod providers; diff --git a/src-tauri/src/models/mod.rs b/src-tauri/src/models/mod.rs index 5600dadee..c7ee73c62 100644 --- a/src-tauri/src/models/mod.rs +++ b/src-tauri/src/models/mod.rs @@ -4,6 +4,7 @@ pub mod codewhisperer; pub mod kiro_fingerprint; pub mod machine_id; pub mod mcp_model; +pub mod model_registry; pub mod openai; pub mod prompt_model; pub mod provider_model; diff --git a/src-tauri/src/models/model_registry.rs b/src-tauri/src/models/model_registry.rs new file mode 100644 index 000000000..38f0e9e64 --- /dev/null +++ b/src-tauri/src/models/model_registry.rs @@ -0,0 +1,608 @@ +//! 模型注册表数据结构 +//! +//! 借鉴 opencode 的模型管理方式,定义增强的模型元数据结构 + +use serde::{Deserialize, Serialize}; + +/// 模型能力 +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct ModelCapabilities { + /// 是否支持视觉输入 + pub vision: bool, + /// 是否支持工具调用 + pub tools: bool, + /// 是否支持流式输出 + pub streaming: bool, + /// 是否支持 JSON 模式 + pub json_mode: bool, + /// 是否支持函数调用 + pub function_calling: bool, + /// 是否支持推理/思考 + pub reasoning: bool, +} + +/// 模型定价 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ModelPricing { + /// 输入价格(每百万 token) + pub input_per_million: Option, + /// 输出价格(每百万 token) + pub output_per_million: Option, + /// 缓存读取价格(每百万 token) + pub cache_read_per_million: Option, + /// 缓存写入价格(每百万 token) + pub cache_write_per_million: Option, + /// 货币单位 ("USD" | "CNY") + pub currency: String, +} + +impl Default for ModelPricing { + fn default() -> Self { + Self { + input_per_million: None, + output_per_million: None, + cache_read_per_million: None, + cache_write_per_million: None, + currency: "USD".to_string(), + } + } +} + +/// 模型限制 +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct ModelLimits { + /// 上下文长度 + pub context_length: Option, + /// 最大输出 token 数 + pub max_output_tokens: Option, + /// 每分钟请求数限制 + pub requests_per_minute: Option, + /// 每分钟 token 数限制 + pub tokens_per_minute: Option, +} + +/// 模型状态 +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum ModelStatus { + /// 活跃可用 + Active, + /// 预览版 + Preview, + /// Alpha 测试 + Alpha, + /// Beta 测试 + Beta, + /// 已弃用 + Deprecated, + /// 旧版本 + Legacy, +} + +impl Default for ModelStatus { + fn default() -> Self { + Self::Active + } +} + +impl std::fmt::Display for ModelStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Active => write!(f, "active"), + Self::Preview => write!(f, "preview"), + Self::Alpha => write!(f, "alpha"), + Self::Beta => write!(f, "beta"), + Self::Deprecated => write!(f, "deprecated"), + Self::Legacy => write!(f, "legacy"), + } + } +} + +impl std::str::FromStr for ModelStatus { + type Err = String; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "active" => Ok(Self::Active), + "preview" => Ok(Self::Preview), + "alpha" => Ok(Self::Alpha), + "beta" => Ok(Self::Beta), + "deprecated" => Ok(Self::Deprecated), + "legacy" => Ok(Self::Legacy), + _ => Err(format!("Unknown model status: {}", s)), + } + } +} + +/// 模型服务等级 +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum ModelTier { + /// 快速响应,适合简单任务 + Mini, + /// 均衡性能,适合大多数任务 + Pro, + /// 最强能力,适合复杂任务 + Max, +} + +impl Default for ModelTier { + fn default() -> Self { + Self::Pro + } +} + +impl std::fmt::Display for ModelTier { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Mini => write!(f, "mini"), + Self::Pro => write!(f, "pro"), + Self::Max => write!(f, "max"), + } + } +} + +impl std::str::FromStr for ModelTier { + type Err = String; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "mini" => Ok(Self::Mini), + "pro" => Ok(Self::Pro), + "max" => Ok(Self::Max), + _ => Err(format!("Unknown model tier: {}", s)), + } + } +} + +/// 模型数据来源 +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum ModelSource { + /// 从 models.dev API 获取 + ModelsDev, + /// 本地硬编码(国内模型等) + Local, + /// 用户自定义 + Custom, +} + +impl Default for ModelSource { + fn default() -> Self { + Self::Local + } +} + +impl std::fmt::Display for ModelSource { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::ModelsDev => write!(f, "models.dev"), + Self::Local => write!(f, "local"), + Self::Custom => write!(f, "custom"), + } + } +} + +impl std::str::FromStr for ModelSource { + type Err = String; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "models.dev" | "modelsdev" => Ok(Self::ModelsDev), + "local" => Ok(Self::Local), + "custom" => Ok(Self::Custom), + _ => Err(format!("Unknown model source: {}", s)), + } + } +} + +/// 增强的模型元数据 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EnhancedModelMetadata { + /// 模型 ID (如 "claude-sonnet-4-5-20250514") + pub id: String, + /// 显示名称 (如 "Claude Sonnet 4.5") + pub display_name: String, + /// Provider ID (如 "anthropic", "openai", "dashscope") + pub provider_id: String, + /// Provider 显示名称 + pub provider_name: String, + /// 模型家族 (如 "sonnet", "gpt-4", "qwen") + pub family: Option, + /// 服务等级 + pub tier: ModelTier, + /// 模型能力 + pub capabilities: ModelCapabilities, + /// 定价信息 + pub pricing: Option, + /// 限制信息 + pub limits: ModelLimits, + /// 模型状态 + pub status: ModelStatus, + /// 发布日期 + pub release_date: Option, + /// 是否为最新版本 + pub is_latest: bool, + /// 描述 + pub description: Option, + /// 数据来源 + pub source: ModelSource, + /// 创建时间 (Unix 时间戳) + pub created_at: i64, + /// 最后更新时间 (Unix 时间戳) + pub updated_at: i64, +} + +impl EnhancedModelMetadata { + /// 创建新的模型元数据 + pub fn new( + id: String, + display_name: String, + provider_id: String, + provider_name: String, + ) -> Self { + let now = chrono::Utc::now().timestamp(); + Self { + id, + display_name, + provider_id, + provider_name, + family: None, + tier: ModelTier::Pro, + capabilities: ModelCapabilities::default(), + pricing: None, + limits: ModelLimits::default(), + status: ModelStatus::Active, + release_date: None, + is_latest: false, + description: None, + source: ModelSource::Local, + created_at: now, + updated_at: now, + } + } + + /// 设置模型家族 + pub fn with_family(mut self, family: impl Into) -> Self { + self.family = Some(family.into()); + self + } + + /// 设置服务等级 + pub fn with_tier(mut self, tier: ModelTier) -> Self { + self.tier = tier; + self + } + + /// 设置模型能力 + pub fn with_capabilities(mut self, capabilities: ModelCapabilities) -> Self { + self.capabilities = capabilities; + self + } + + /// 设置定价信息 + pub fn with_pricing(mut self, pricing: ModelPricing) -> Self { + self.pricing = Some(pricing); + self + } + + /// 设置限制信息 + pub fn with_limits(mut self, limits: ModelLimits) -> Self { + self.limits = limits; + self + } + + /// 设置模型状态 + pub fn with_status(mut self, status: ModelStatus) -> Self { + self.status = status; + self + } + + /// 设置发布日期 + pub fn with_release_date(mut self, date: impl Into) -> Self { + self.release_date = Some(date.into()); + self + } + + /// 设置是否为最新版本 + pub fn with_is_latest(mut self, is_latest: bool) -> Self { + self.is_latest = is_latest; + self + } + + /// 设置描述 + pub fn with_description(mut self, description: impl Into) -> Self { + self.description = Some(description.into()); + self + } + + /// 设置数据来源 + pub fn with_source(mut self, source: ModelSource) -> Self { + self.source = source; + self + } +} + +/// 用户模型偏好 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UserModelPreference { + /// 模型 ID + pub model_id: String, + /// 是否收藏 + pub is_favorite: bool, + /// 是否隐藏 + pub is_hidden: bool, + /// 自定义别名 + pub custom_alias: Option, + /// 使用次数 + pub usage_count: u32, + /// 最后使用时间 (Unix 时间戳) + pub last_used_at: Option, + /// 创建时间 (Unix 时间戳) + pub created_at: i64, + /// 更新时间 (Unix 时间戳) + pub updated_at: i64, +} + +impl UserModelPreference { + /// 创建新的用户偏好 + pub fn new(model_id: String) -> Self { + let now = chrono::Utc::now().timestamp(); + Self { + model_id, + is_favorite: false, + is_hidden: false, + custom_alias: None, + usage_count: 0, + last_used_at: None, + created_at: now, + updated_at: now, + } + } +} + +/// 模型同步状态 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ModelSyncState { + /// 最后同步时间 (Unix 时间戳) + pub last_sync_at: Option, + /// 同步的模型数量 + pub model_count: u32, + /// 是否正在同步 + pub is_syncing: bool, + /// 最后同步错误 + pub last_error: Option, +} + +impl Default for ModelSyncState { + fn default() -> Self { + Self { + last_sync_at: None, + model_count: 0, + is_syncing: false, + last_error: None, + } + } +} + +/// models.dev API 响应中的 Provider 结构 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ModelsDevProvider { + pub id: String, + pub name: String, + #[serde(default)] + pub api: Option, + #[serde(default)] + pub npm: Option, + #[serde(default)] + pub models: std::collections::HashMap, +} + +/// models.dev API 响应中的 Model 结构 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ModelsDevModel { + pub id: String, + pub name: String, + #[serde(default)] + pub family: Option, + #[serde(default)] + pub release_date: Option, + #[serde(default)] + pub attachment: bool, + #[serde(default)] + pub reasoning: bool, + #[serde(default)] + pub temperature: bool, + #[serde(default)] + pub tool_call: bool, + #[serde(default)] + pub cost: Option, + #[serde(default)] + pub limit: Option, + #[serde(default)] + pub modalities: Option, + #[serde(default)] + pub experimental: Option, + #[serde(default)] + pub status: Option, +} + +/// models.dev API 响应中的 Cost 结构 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ModelsDevCost { + #[serde(default)] + pub input: Option, + #[serde(default)] + pub output: Option, + #[serde(default)] + pub cache_read: Option, + #[serde(default)] + pub cache_write: Option, +} + +/// models.dev API 响应中的 Limit 结构 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ModelsDevLimit { + #[serde(default)] + pub context: Option, + #[serde(default)] + pub output: Option, +} + +/// models.dev API 响应中的 Modalities 结构 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ModelsDevModalities { + #[serde(default)] + pub input: Vec, + #[serde(default)] + pub output: Vec, +} + +impl ModelsDevModel { + /// 转换为 EnhancedModelMetadata + pub fn to_enhanced_metadata( + &self, + provider_id: &str, + provider_name: &str, + ) -> EnhancedModelMetadata { + let now = chrono::Utc::now().timestamp(); + + // 判断是否支持视觉 + let supports_vision = self + .modalities + .as_ref() + .map(|m| m.input.iter().any(|i| i == "image" || i == "video")) + .unwrap_or(false) + || self.attachment; + + // 根据模型名称推断服务等级 + let tier = infer_model_tier(&self.id, &self.name); + + // 解析状态 + let status = self + .status + .as_ref() + .and_then(|s| s.parse().ok()) + .unwrap_or(ModelStatus::Active); + + // 判断是否为最新版本 + let is_latest = self.id.contains("latest"); + + EnhancedModelMetadata { + id: self.id.clone(), + display_name: self.name.clone(), + provider_id: provider_id.to_string(), + provider_name: provider_name.to_string(), + family: self.family.clone(), + tier, + capabilities: ModelCapabilities { + vision: supports_vision, + tools: self.tool_call, + streaming: true, // 大多数模型都支持流式 + json_mode: true, // 大多数模型都支持 JSON 模式 + function_calling: self.tool_call, + reasoning: self.reasoning, + }, + pricing: self.cost.as_ref().map(|c| ModelPricing { + input_per_million: c.input, + output_per_million: c.output, + cache_read_per_million: c.cache_read, + cache_write_per_million: c.cache_write, + currency: "USD".to_string(), + }), + limits: ModelLimits { + context_length: self.limit.as_ref().and_then(|l| l.context), + max_output_tokens: self.limit.as_ref().and_then(|l| l.output), + requests_per_minute: None, + tokens_per_minute: None, + }, + status, + release_date: self.release_date.clone(), + is_latest, + description: None, + source: ModelSource::ModelsDev, + created_at: now, + updated_at: now, + } + } +} + +/// 根据模型 ID 和名称推断服务等级 +fn infer_model_tier(model_id: &str, model_name: &str) -> ModelTier { + let id_lower = model_id.to_lowercase(); + let name_lower = model_name.to_lowercase(); + + // Max 等级模型 + let max_patterns = [ + "opus", + "gpt-4o", + "gpt-4-turbo", + "gemini-2.5-pro", + "gemini-ultra", + "claude-3-opus", + "qwen-max", + "glm-4-plus", + "deepseek-v3", + ]; + for pattern in max_patterns { + if id_lower.contains(pattern) || name_lower.contains(pattern) { + return ModelTier::Max; + } + } + + // Mini 等级模型 + let mini_patterns = [ + "mini", + "nano", + "lite", + "flash", + "haiku", + "gpt-4o-mini", + "gemini-flash", + "qwen-turbo", + "glm-4-flash", + ]; + for pattern in mini_patterns { + if id_lower.contains(pattern) || name_lower.contains(pattern) { + return ModelTier::Mini; + } + } + + // 默认为 Pro 等级 + ModelTier::Pro +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_model_tier_inference() { + assert_eq!( + infer_model_tier("claude-opus-4-5-20250514", "Claude Opus 4.5"), + ModelTier::Max + ); + assert_eq!( + infer_model_tier("gpt-4o-mini", "GPT-4o Mini"), + ModelTier::Mini + ); + assert_eq!( + infer_model_tier("claude-sonnet-4-5", "Claude Sonnet 4.5"), + ModelTier::Pro + ); + assert_eq!( + infer_model_tier("gemini-2.5-flash", "Gemini 2.5 Flash"), + ModelTier::Mini + ); + } + + #[test] + fn test_model_status_parsing() { + assert_eq!("active".parse::().unwrap(), ModelStatus::Active); + assert_eq!( + "deprecated".parse::().unwrap(), + ModelStatus::Deprecated + ); + assert_eq!("beta".parse::().unwrap(), ModelStatus::Beta); + } +} diff --git a/src-tauri/src/orchestrator/orchestrator.rs b/src-tauri/src/orchestrator/orchestrator.rs index 39b3ab9d1..45d30760e 100644 --- a/src-tauri/src/orchestrator/orchestrator.rs +++ b/src-tauri/src/orchestrator/orchestrator.rs @@ -324,6 +324,7 @@ mod tests { .update_credentials(vec![CredentialInfo { id: "cred-1".to_string(), provider_type: ProviderType::Anthropic, + original_provider_type: None, supported_models: vec![ "claude-sonnet-4-5-20250514".to_string(), "claude-3-5-haiku-20241022".to_string(), @@ -351,6 +352,7 @@ mod tests { .update_credentials(vec![CredentialInfo { id: "cred-1".to_string(), provider_type: ProviderType::Anthropic, + original_provider_type: None, supported_models: vec![ "claude-sonnet-4-5-20250514".to_string(), "claude-3-5-haiku-20241022".to_string(), diff --git a/src-tauri/src/orchestrator/pool_builder.rs b/src-tauri/src/orchestrator/pool_builder.rs index c4fcf4b8a..d6ee2dd10 100644 --- a/src-tauri/src/orchestrator/pool_builder.rs +++ b/src-tauri/src/orchestrator/pool_builder.rs @@ -16,6 +16,7 @@ pub enum ProviderType { Kiro, Azure, Bedrock, + Antigravity, Custom, } @@ -29,6 +30,7 @@ impl ProviderType { "kiro" | "codewhisperer" => Some(ProviderType::Kiro), "azure" => Some(ProviderType::Azure), "bedrock" => Some(ProviderType::Bedrock), + "antigravity" => Some(ProviderType::Antigravity), _ => Some(ProviderType::Custom), } } @@ -42,6 +44,7 @@ impl ProviderType { ProviderType::Kiro => "Kiro", ProviderType::Azure => "Azure", ProviderType::Bedrock => "Bedrock", + ProviderType::Antigravity => "Antigravity", ProviderType::Custom => "Custom", } } @@ -83,13 +86,30 @@ impl ProviderDefinition { // 简单的 glob 匹配 let parts: Vec<&str> = pattern_lower.split('*').collect(); if parts.len() == 2 { + // 模式如 "claude-*" 或 "*sonnet" let prefix = parts[0]; let suffix = parts[1]; - model_lower.starts_with(prefix) && model_lower.ends_with(suffix) + if prefix.is_empty() { + model_lower.ends_with(suffix) + } else if suffix.is_empty() { + model_lower.starts_with(prefix) + } else { + model_lower.starts_with(prefix) && model_lower.ends_with(suffix) + } + } else if parts.len() == 3 { + // 模式如 "claude-*opus*" (prefix*middle*suffix) + let prefix = parts[0]; + let middle = parts[1]; + let suffix = parts[2]; + let starts_ok = prefix.is_empty() || model_lower.starts_with(prefix); + let ends_ok = suffix.is_empty() || model_lower.ends_with(suffix); + let contains_middle = middle.is_empty() || model_lower.contains(middle); + starts_ok && ends_ok && contains_middle } else if parts.len() == 1 { model_lower.starts_with(parts[0]) } else { - false + // 复杂模式,回退到简单包含检查 + parts.iter().all(|p| p.is_empty() || model_lower.contains(p)) } } else { model_lower.contains(&pattern_lower) @@ -221,6 +241,47 @@ pub fn builtin_provider_definitions() -> Vec { ], default_base_url: None, }, + // Antigravity (Google Cloud Code Assist) + ProviderDefinition { + provider_type: ProviderType::Antigravity, + display_name: "Antigravity".to_string(), + families: vec![ + // Max 等级:Gemini 3 Pro 和 Claude Opus + ModelFamily { + name: "gemini-3-pro".to_string(), + pattern: "gemini-3-pro*".to_string(), + tier: 3, + description: Some("Gemini 3 Pro via Antigravity".to_string()), + }, + ModelFamily { + name: "opus".to_string(), + pattern: "*opus*".to_string(), + tier: 3, + description: Some("Claude Opus via Antigravity".to_string()), + }, + // Pro 等级:Claude Sonnet 和 Gemini 2.5 + ModelFamily { + name: "sonnet".to_string(), + pattern: "*sonnet*".to_string(), + tier: 2, + description: Some("Claude Sonnet via Antigravity".to_string()), + }, + ModelFamily { + name: "gemini-2.5".to_string(), + pattern: "gemini-2.5*".to_string(), + tier: 2, + description: Some("Gemini 2.5 via Antigravity".to_string()), + }, + // Mini 等级:Flash 模型 + ModelFamily { + name: "gemini-3-flash".to_string(), + pattern: "gemini-3-flash*".to_string(), + tier: 1, + description: Some("Gemini 3 Flash via Antigravity".to_string()), + }, + ], + default_base_url: None, + }, ] } @@ -395,8 +456,10 @@ pub fn builtin_model_metadata() -> Vec { pub struct CredentialInfo { /// 凭证 ID pub id: String, - /// Provider 类型 + /// Provider 类型(用于模型分类) pub provider_type: ProviderType, + /// 原始 Provider 类型字符串(用于前端识别,如 "antigravity"、"kiro" 等) + pub original_provider_type: Option, /// 支持的模型列表 pub supported_models: Vec, /// 是否健康 @@ -476,13 +539,19 @@ impl DynamicPoolBuilder { .or_else(|| metadata.as_ref().and_then(|m| m.family.clone())); // 构建 AvailableModel + // 优先使用原始 provider 类型(如 "antigravity"),否则使用枚举名称 + let provider_type_str = credential + .original_provider_type + .clone() + .unwrap_or_else(|| format!("{:?}", credential.provider_type).to_lowercase()); + let available_model = AvailableModel { id: model_id.clone(), display_name: metadata .as_ref() .map(|m| m.display_name.clone()) .unwrap_or_else(|| model_id.clone()), - provider_type: format!("{:?}", credential.provider_type).to_lowercase(), + provider_type: provider_type_str, family, credential_id: credential.id.clone(), context_length: metadata.as_ref().and_then(|m| m.context_length), @@ -590,6 +659,7 @@ mod tests { CredentialInfo { id: "cred-1".to_string(), provider_type: ProviderType::Anthropic, + original_provider_type: None, supported_models: vec![ "claude-opus-4-5-20251101".to_string(), "claude-sonnet-4-5-20250514".to_string(), @@ -601,6 +671,7 @@ mod tests { CredentialInfo { id: "cred-2".to_string(), provider_type: ProviderType::OpenAI, + original_provider_type: None, supported_models: vec!["gpt-4o".to_string(), "gpt-3.5-turbo".to_string()], is_healthy: true, current_load: Some(20), @@ -622,6 +693,7 @@ mod tests { let credentials = vec![CredentialInfo { id: "cred-1".to_string(), provider_type: ProviderType::Anthropic, + original_provider_type: None, supported_models: vec![ "claude-sonnet-4-5-20250514".to_string(), "claude-3-5-sonnet-20241022".to_string(), diff --git a/src-tauri/src/server/handlers/provider_calls.rs b/src-tauri/src/server/handlers/provider_calls.rs index ee83e4478..ac8ca9770 100644 --- a/src-tauri/src/server/handlers/provider_calls.rs +++ b/src-tauri/src/server/handlers/provider_calls.rs @@ -2946,6 +2946,7 @@ fn extract_content_from_json(json: &serde_json::Value) -> Option<(String, Vec<(S } let mut text = String::new(); + let mut thinking_text = String::new(); let mut images = Vec::new(); for candidate in candidates { @@ -2955,8 +2956,35 @@ fn extract_content_from_json(json: &serde_json::Value) -> Option<(String, Vec<(S .and_then(|p| p.as_array()) { for part in parts { + // 检查是否是思维内容 + let is_thought = part + .get("thought") + .and_then(|t| t.as_bool()) + .unwrap_or(false); + + // 跳过纯 thoughtSignature 部分 + let has_thought_signature = part + .get("thoughtSignature") + .or_else(|| part.get("thought_signature")) + .and_then(|s| s.as_str()) + .map(|s| !s.is_empty()) + .unwrap_or(false); + + let has_content = part.get("text").is_some() + || part.get("inlineData").is_some() + || part.get("inline_data").is_some(); + + if has_thought_signature && !has_content { + continue; + } + if let Some(t) = part.get("text").and_then(|t| t.as_str()) { - text.push_str(t); + if is_thought { + // 思维内容 + thinking_text.push_str(t); + } else { + text.push_str(t); + } } if let Some(inline_data) = part.get("inlineData").or_else(|| part.get("inline_data")) @@ -2974,10 +3002,19 @@ fn extract_content_from_json(json: &serde_json::Value) -> Option<(String, Vec<(S } } - if text.is_empty() && images.is_empty() { + // 如果有 thinking 内容,用 标签包裹并放在前面 + let mut final_text = String::new(); + if !thinking_text.is_empty() { + final_text.push_str(""); + final_text.push_str(&thinking_text); + final_text.push_str("\n\n"); + } + final_text.push_str(&text); + + if final_text.is_empty() && images.is_empty() { None } else { - Some((text, images)) + Some((final_text, images)) } } diff --git a/src-tauri/src/services/mod.rs b/src-tauri/src/services/mod.rs index e6f5d5229..129f9af7c 100644 --- a/src-tauri/src/services/mod.rs +++ b/src-tauri/src/services/mod.rs @@ -5,6 +5,7 @@ pub mod live_sync; pub mod machine_id_service; pub mod mcp_service; pub mod mcp_sync; +pub mod model_registry_service; pub mod prompt_service; pub mod prompt_sync; pub mod provider_pool_service; diff --git a/src-tauri/src/services/model_registry_service.rs b/src-tauri/src/services/model_registry_service.rs new file mode 100644 index 000000000..13bcc3aff --- /dev/null +++ b/src-tauri/src/services/model_registry_service.rs @@ -0,0 +1,657 @@ +//! 模型注册服务 +//! +//! 负责从 models.dev API 获取模型数据、管理本地缓存、提供模型搜索等功能 + +use crate::data::get_local_models; +use crate::database::DbConnection; +use crate::models::model_registry::{ + EnhancedModelMetadata, ModelSource, ModelStatus, + ModelSyncState, ModelTier, ModelsDevProvider, UserModelPreference, +}; +use rusqlite::params; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::RwLock; + +const MODELS_DEV_API_URL: &str = "https://models.dev/api.json"; +const CACHE_DURATION_SECS: i64 = 3600; // 1 小时 + +/// 模型注册服务 +pub struct ModelRegistryService { + /// 数据库连接 + db: DbConnection, + /// 内存缓存的模型数据 + models_cache: Arc>>, + /// 同步状态 + sync_state: Arc>, +} + +impl ModelRegistryService { + /// 创建新的模型注册服务 + pub fn new(db: DbConnection) -> Self { + Self { + db, + models_cache: Arc::new(RwLock::new(Vec::new())), + sync_state: Arc::new(RwLock::new(ModelSyncState::default())), + } + } + + /// 初始化服务 + pub async fn initialize(&self) -> Result<(), String> { + tracing::info!("[ModelRegistry] 初始化模型注册服务"); + + // 1. 尝试从数据库加载缓存 + match self.load_from_db().await { + Ok(models) if !models.is_empty() => { + tracing::info!("[ModelRegistry] 从数据库加载了 {} 个模型", models.len()); + let mut cache = self.models_cache.write().await; + *cache = models; + + // 检查是否需要后台刷新 + if self.should_refresh().await { + tracing::info!("[ModelRegistry] 缓存已过期,启动后台刷新"); + self.spawn_background_refresh(); + } + return Ok(()); + } + Ok(_) => { + tracing::info!("[ModelRegistry] 数据库中没有缓存数据"); + } + Err(e) => { + tracing::warn!("[ModelRegistry] 从数据库加载失败: {}", e); + } + } + + // 2. 使用本地硬编码数据作为初始数据 + let local_models = get_local_models(); + tracing::info!( + "[ModelRegistry] 使用 {} 个本地硬编码模型作为初始数据", + local_models.len() + ); + + { + let mut cache = self.models_cache.write().await; + *cache = local_models.clone(); + } + + // 保存到数据库 + if let Err(e) = self.save_models_to_db(&local_models).await { + tracing::warn!("[ModelRegistry] 保存本地模型到数据库失败: {}", e); + } + + // 3. 后台获取 models.dev 数据 + self.spawn_background_refresh(); + + Ok(()) + } + + /// 检查是否需要刷新 + async fn should_refresh(&self) -> bool { + let state = self.sync_state.read().await; + match state.last_sync_at { + Some(last_sync) => { + let now = chrono::Utc::now().timestamp(); + now - last_sync > CACHE_DURATION_SECS + } + None => true, + } + } + + /// 启动后台刷新任务 + fn spawn_background_refresh(&self) { + let db = self.db.clone(); + let models_cache = self.models_cache.clone(); + let sync_state = self.sync_state.clone(); + + tokio::spawn(async move { + let service = ModelRegistryService { + db, + models_cache, + sync_state, + }; + if let Err(e) = service.refresh_from_models_dev().await { + tracing::error!("[ModelRegistry] 后台刷新失败: {}", e); + } + }); + } + + /// 从 models.dev API 刷新数据 + pub async fn refresh_from_models_dev(&self) -> Result<(), String> { + tracing::info!("[ModelRegistry] 开始从 models.dev 获取数据"); + + // 设置同步状态 + { + let mut state = self.sync_state.write().await; + state.is_syncing = true; + state.last_error = None; + } + + // 获取数据 + let result = self.fetch_models_dev_data().await; + + match result { + Ok(models_dev_models) => { + // 合并本地模型 + let local_models = get_local_models(); + let merged = self.merge_models(models_dev_models, local_models); + + tracing::info!( + "[ModelRegistry] 获取并合并了 {} 个模型", + merged.len() + ); + + // 更新缓存 + { + let mut cache = self.models_cache.write().await; + *cache = merged.clone(); + } + + // 保存到数据库 + self.save_models_to_db(&merged).await?; + + // 更新同步状态 + { + let mut state = self.sync_state.write().await; + state.is_syncing = false; + state.last_sync_at = Some(chrono::Utc::now().timestamp()); + state.model_count = merged.len() as u32; + state.last_error = None; + } + + // 保存同步状态到数据库 + self.save_sync_state().await?; + + Ok(()) + } + Err(e) => { + tracing::error!("[ModelRegistry] 从 models.dev 获取数据失败: {}", e); + + // 更新同步状态 + { + let mut state = self.sync_state.write().await; + state.is_syncing = false; + state.last_error = Some(e.clone()); + } + + Err(e) + } + } + } + + /// 从 models.dev API 获取数据 + async fn fetch_models_dev_data(&self) -> Result, String> { + let client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(30)) + .build() + .map_err(|e| format!("创建 HTTP 客户端失败: {}", e))?; + + let response = client + .get(MODELS_DEV_API_URL) + .header("User-Agent", "ProxyCast/1.0") + .send() + .await + .map_err(|e| format!("请求 models.dev 失败: {}", e))?; + + if !response.status().is_success() { + return Err(format!( + "models.dev 返回错误状态码: {}", + response.status() + )); + } + + let data: HashMap = response + .json() + .await + .map_err(|e| format!("解析 models.dev 响应失败: {}", e))?; + + // 转换为内部格式 + let mut models = Vec::new(); + for (provider_id, provider) in data { + for (_, model) in provider.models { + let enhanced = model.to_enhanced_metadata(&provider_id, &provider.name); + models.push(enhanced); + } + } + + tracing::info!( + "[ModelRegistry] 从 models.dev 获取了 {} 个模型", + models.len() + ); + + Ok(models) + } + + /// 合并 models.dev 数据和本地数据 + fn merge_models( + &self, + models_dev: Vec, + local: Vec, + ) -> Vec { + let mut merged: HashMap = HashMap::new(); + + // 先添加 models.dev 数据 + for model in models_dev { + merged.insert(model.id.clone(), model); + } + + // 本地数据覆盖或补充 + for model in local { + // 如果 models.dev 没有这个模型,或者本地数据更新,则使用本地数据 + if !merged.contains_key(&model.id) { + merged.insert(model.id.clone(), model); + } + } + + let mut result: Vec<_> = merged.into_values().collect(); + // 按 provider_id 和 display_name 排序 + result.sort_by(|a, b| { + a.provider_id + .cmp(&b.provider_id) + .then(a.display_name.cmp(&b.display_name)) + }); + + result + } + + /// 从数据库加载模型 + async fn load_from_db(&self) -> Result, String> { + let (models, sync_rows) = { + let conn = self.db.lock().map_err(|e| e.to_string())?; + + let mut stmt = conn + .prepare( + "SELECT id, display_name, provider_id, provider_name, family, tier, + capabilities, pricing, limits, status, release_date, is_latest, + description, source, created_at, updated_at + FROM model_registry", + ) + .map_err(|e| e.to_string())?; + + let models = stmt + .query_map([], |row| { + let capabilities_json: String = row.get(6)?; + let pricing_json: Option = row.get(7)?; + let limits_json: String = row.get(8)?; + let status_str: String = row.get(9)?; + let tier_str: String = row.get(5)?; + let source_str: String = row.get(13)?; + + Ok(EnhancedModelMetadata { + id: row.get(0)?, + display_name: row.get(1)?, + provider_id: row.get(2)?, + provider_name: row.get(3)?, + family: row.get(4)?, + tier: tier_str.parse().unwrap_or(ModelTier::Pro), + capabilities: serde_json::from_str(&capabilities_json) + .unwrap_or_default(), + pricing: pricing_json + .and_then(|s| serde_json::from_str(&s).ok()), + limits: serde_json::from_str(&limits_json).unwrap_or_default(), + status: status_str.parse().unwrap_or(ModelStatus::Active), + release_date: row.get(10)?, + is_latest: row.get::<_, i32>(11)? != 0, + description: row.get(12)?, + source: source_str.parse().unwrap_or(ModelSource::Local), + created_at: row.get(14)?, + updated_at: row.get(15)?, + }) + }) + .map_err(|e| e.to_string())? + .collect::, _>>() + .map_err(|e| e.to_string())?; + + // 加载同步状态数据 + let mut sync_stmt = conn + .prepare("SELECT key, value FROM model_sync_state") + .map_err(|e| e.to_string())?; + + let sync_rows: Vec<(String, String)> = sync_stmt + .query_map([], |row| Ok((row.get(0)?, row.get(1)?))) + .map_err(|e| e.to_string())? + .collect::, _>>() + .map_err(|e| e.to_string())?; + + (models, sync_rows) + }; // conn 锁在这里释放 + + // 更新同步状态(在锁释放后) + { + let mut state = self.sync_state.write().await; + for (key, value) in sync_rows { + match key.as_str() { + "last_sync_at" => { + state.last_sync_at = value.parse().ok(); + } + "model_count" => { + state.model_count = value.parse().unwrap_or(0); + } + "last_error" => { + state.last_error = if value.is_empty() { None } else { Some(value) }; + } + _ => {} + } + } + } + + Ok(models) + } + + /// 保存模型到数据库 + async fn save_models_to_db(&self, models: &[EnhancedModelMetadata]) -> Result<(), String> { + let conn = self.db.lock().map_err(|e| e.to_string())?; + + // 开始事务 + conn.execute("BEGIN TRANSACTION", []) + .map_err(|e| e.to_string())?; + + // 清空现有数据 + conn.execute("DELETE FROM model_registry", []) + .map_err(|e| e.to_string())?; + + // 插入新数据 + let mut stmt = conn + .prepare( + "INSERT INTO model_registry ( + id, display_name, provider_id, provider_name, family, tier, + capabilities, pricing, limits, status, release_date, is_latest, + description, source, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + ) + .map_err(|e| e.to_string())?; + + for model in models { + let capabilities_json = + serde_json::to_string(&model.capabilities).unwrap_or_default(); + let pricing_json = model + .pricing + .as_ref() + .map(|p| serde_json::to_string(p).unwrap_or_default()); + let limits_json = serde_json::to_string(&model.limits).unwrap_or_default(); + + stmt.execute(params![ + model.id, + model.display_name, + model.provider_id, + model.provider_name, + model.family, + model.tier.to_string(), + capabilities_json, + pricing_json, + limits_json, + model.status.to_string(), + model.release_date, + model.is_latest as i32, + model.description, + model.source.to_string(), + model.created_at, + model.updated_at, + ]) + .map_err(|e| e.to_string())?; + } + + // 提交事务 + conn.execute("COMMIT", []).map_err(|e| e.to_string())?; + + tracing::info!("[ModelRegistry] 保存了 {} 个模型到数据库", models.len()); + + Ok(()) + } + + /// 保存同步状态 + async fn save_sync_state(&self) -> Result<(), String> { + let (last_sync_at, model_count, last_error) = { + let state = self.sync_state.read().await; + (state.last_sync_at, state.model_count, state.last_error.clone()) + }; + + let conn = self.db.lock().map_err(|e| e.to_string())?; + let now = chrono::Utc::now().timestamp(); + + let mut stmt = conn + .prepare( + "INSERT OR REPLACE INTO model_sync_state (key, value, updated_at) + VALUES (?, ?, ?)", + ) + .map_err(|e| e.to_string())?; + + if let Some(last_sync) = last_sync_at { + stmt.execute(params!["last_sync_at", last_sync.to_string(), now]) + .map_err(|e| e.to_string())?; + } + + stmt.execute(params!["model_count", model_count.to_string(), now]) + .map_err(|e| e.to_string())?; + + if let Some(ref error) = last_error { + stmt.execute(params!["last_error", error, now]) + .map_err(|e| e.to_string())?; + } + + Ok(()) + } + + /// 获取所有模型 + pub async fn get_all_models(&self) -> Vec { + self.models_cache.read().await.clone() + } + + /// 获取同步状态 + pub async fn get_sync_state(&self) -> ModelSyncState { + self.sync_state.read().await.clone() + } + + /// 按 Provider 获取模型 + pub async fn get_models_by_provider(&self, provider_id: &str) -> Vec { + self.models_cache + .read() + .await + .iter() + .filter(|m| m.provider_id == provider_id) + .cloned() + .collect() + } + + /// 按服务等级获取模型 + pub async fn get_models_by_tier(&self, tier: ModelTier) -> Vec { + self.models_cache + .read() + .await + .iter() + .filter(|m| m.tier == tier) + .cloned() + .collect() + } + + /// 搜索模型(简单的模糊匹配) + pub async fn search_models(&self, query: &str, limit: usize) -> Vec { + let models = self.models_cache.read().await; + + if query.is_empty() { + return models.iter().take(limit).cloned().collect(); + } + + let query_lower = query.to_lowercase(); + let mut scored: Vec<(f64, &EnhancedModelMetadata)> = models + .iter() + .filter_map(|m| { + let score = self.calculate_search_score(m, &query_lower); + if score > 0.0 { + Some((score, m)) + } else { + None + } + }) + .collect(); + + // 按分数降序排序 + scored.sort_by(|a, b| { + b.0.partial_cmp(&a.0) + .unwrap_or(std::cmp::Ordering::Equal) + }); + + scored + .into_iter() + .take(limit) + .map(|(_, m)| m.clone()) + .collect() + } + + /// 计算搜索匹配分数 + fn calculate_search_score(&self, model: &EnhancedModelMetadata, query: &str) -> f64 { + let mut score = 0.0; + + // 精确匹配 ID + if model.id.to_lowercase() == query { + score += 100.0; + } else if model.id.to_lowercase().contains(query) { + score += 50.0; + } + + // 显示名称匹配 + if model.display_name.to_lowercase().contains(query) { + score += 30.0; + } + + // Provider 匹配 + if model.provider_name.to_lowercase().contains(query) { + score += 20.0; + } + + // 家族匹配 + if let Some(family) = &model.family { + if family.to_lowercase().contains(query) { + score += 15.0; + } + } + + // 最新版本加分 + if model.is_latest { + score += 5.0; + } + + // 活跃状态加分 + if model.status == ModelStatus::Active { + score += 3.0; + } + + score + } + + // ========== 用户偏好相关方法 ========== + + /// 获取所有用户偏好 + pub async fn get_all_preferences(&self) -> Result, String> { + let conn = self.db.lock().map_err(|e| e.to_string())?; + + let mut stmt = conn + .prepare( + "SELECT model_id, is_favorite, is_hidden, custom_alias, + usage_count, last_used_at, created_at, updated_at + FROM user_model_preferences", + ) + .map_err(|e| e.to_string())?; + + let prefs = stmt + .query_map([], |row| { + Ok(UserModelPreference { + model_id: row.get(0)?, + is_favorite: row.get::<_, i32>(1)? != 0, + is_hidden: row.get::<_, i32>(2)? != 0, + custom_alias: row.get(3)?, + usage_count: row.get::<_, i32>(4)? as u32, + last_used_at: row.get(5)?, + created_at: row.get(6)?, + updated_at: row.get(7)?, + }) + }) + .map_err(|e| e.to_string())? + .collect::, _>>() + .map_err(|e| e.to_string())?; + + Ok(prefs) + } + + /// 切换收藏状态 + pub async fn toggle_favorite(&self, model_id: &str) -> Result { + let conn = self.db.lock().map_err(|e| e.to_string())?; + let now = chrono::Utc::now().timestamp(); + + // 检查是否存在 + let exists: bool = conn + .query_row( + "SELECT 1 FROM user_model_preferences WHERE model_id = ?", + params![model_id], + |_| Ok(true), + ) + .unwrap_or(false); + + if exists { + // 切换状态 + conn.execute( + "UPDATE user_model_preferences + SET is_favorite = NOT is_favorite, updated_at = ? + WHERE model_id = ?", + params![now, model_id], + ) + .map_err(|e| e.to_string())?; + } else { + // 创建新记录 + conn.execute( + "INSERT INTO user_model_preferences + (model_id, is_favorite, is_hidden, usage_count, created_at, updated_at) + VALUES (?, 1, 0, 0, ?, ?)", + params![model_id, now, now], + ) + .map_err(|e| e.to_string())?; + } + + // 返回新状态 + let new_state: bool = conn + .query_row( + "SELECT is_favorite FROM user_model_preferences WHERE model_id = ?", + params![model_id], + |row| Ok(row.get::<_, i32>(0)? != 0), + ) + .unwrap_or(false); + + Ok(new_state) + } + + /// 隐藏模型 + pub async fn hide_model(&self, model_id: &str) -> Result<(), String> { + let conn = self.db.lock().map_err(|e| e.to_string())?; + let now = chrono::Utc::now().timestamp(); + + conn.execute( + "INSERT INTO user_model_preferences + (model_id, is_favorite, is_hidden, usage_count, created_at, updated_at) + VALUES (?, 0, 1, 0, ?, ?) + ON CONFLICT(model_id) DO UPDATE SET is_hidden = 1, updated_at = ?", + params![model_id, now, now, now], + ) + .map_err(|e| e.to_string())?; + + Ok(()) + } + + /// 记录模型使用 + pub async fn record_usage(&self, model_id: &str) -> Result<(), String> { + let conn = self.db.lock().map_err(|e| e.to_string())?; + let now = chrono::Utc::now().timestamp(); + + conn.execute( + "INSERT INTO user_model_preferences + (model_id, is_favorite, is_hidden, usage_count, last_used_at, created_at, updated_at) + VALUES (?, 0, 0, 1, ?, ?, ?) + ON CONFLICT(model_id) DO UPDATE SET + usage_count = usage_count + 1, + last_used_at = ?, + updated_at = ?", + params![model_id, now, now, now, now, now], + ) + .map_err(|e| e.to_string())?; + + Ok(()) + } +} diff --git a/src/components/agent/chat/components/ChatNavbar.tsx b/src/components/agent/chat/components/ChatNavbar.tsx index 8c6368595..f54f2f90d 100644 --- a/src/components/agent/chat/components/ChatNavbar.tsx +++ b/src/components/agent/chat/components/ChatNavbar.tsx @@ -1,15 +1,5 @@ -import React, { useState, useEffect } from "react"; -import { - Bot, - ChevronDown, - Check, - Box, - Settings2, - Zap, - Sparkles, - Crown, - Wand2, -} from "lucide-react"; +import React, { useState } from "react"; +import { Bot, ChevronDown, Check, Box, Settings2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Popover, @@ -17,55 +7,9 @@ import { PopoverTrigger, } from "@/components/ui/popover"; import { ScrollArea } from "@/components/ui/scroll-area"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip"; import { Navbar } from "../styles"; import { PROVIDER_CONFIG } from "../types"; import { cn } from "@/lib/utils"; -import { - orchestratorApi, - type ServiceTier, - type PoolStats, -} from "@/lib/api/orchestrator"; - -// 服务等级配置 -const TIER_CONFIG: Record< - ServiceTier, - { - label: string; - description: string; - icon: React.ReactNode; - color: string; - bgColor: string; - } -> = { - mini: { - label: "Mini", - description: "快速响应", - icon: , - color: "text-green-600 dark:text-green-400", - bgColor: "bg-green-500/10", - }, - pro: { - label: "Pro", - description: "均衡性能", - icon: , - color: "text-blue-600 dark:text-blue-400", - bgColor: "bg-blue-500/10", - }, - max: { - label: "Max", - description: "最强能力", - icon: , - color: "text-purple-600 dark:text-purple-400", - bgColor: "bg-purple-500/10", - }, -}; - -type SelectionMode = "simple" | "expert"; interface ChatNavbarProps { providerType: string; @@ -89,62 +33,10 @@ export const ChatNavbar: React.FC = ({ onToggleSettings, }) => { const [open, setOpen] = useState(false); - const [mode, setMode] = useState("simple"); - const [tier, setTier] = useState("pro"); - const [poolStats, setPoolStats] = useState(null); - const [orchestratorReady, setOrchestratorReady] = useState(false); - - // 初始化 orchestrator - useEffect(() => { - const init = async () => { - try { - await orchestratorApi.init(); - setOrchestratorReady(true); - const stats = await orchestratorApi.getPoolStats(); - setPoolStats(stats); - } catch (err) { - console.warn("Orchestrator 初始化失败,使用专家模式:", err); - setMode("expert"); - } - }; - init(); - }, []); - - // 简单模式下选择等级时自动选择模型 - const handleTierSelect = async (selectedTier: ServiceTier) => { - setTier(selectedTier); - setOpen(false); - - if (!orchestratorReady) return; - - try { - const result = await orchestratorApi.selectModel({ tier: selectedTier }); - // 映射 orchestrator 的 provider_type 到 PROVIDER_CONFIG 的 key - const providerKey = mapProviderType(result.provider_type); - setProviderType(providerKey); - setModel(result.model_id); - } catch (err) { - console.error("模型选择失败:", err); - } - }; - - // 映射 provider type - const mapProviderType = (orchestratorType: string): string => { - const mapping: Record = { - anthropic: "claude", - openai: "openai", - google: "gemini", - gemini: "gemini", - kiro: "kiro", - codex: "codex", - }; - return mapping[orchestratorType.toLowerCase()] || orchestratorType; - }; const selectedProviderLabel = PROVIDER_CONFIG[providerType]?.label || providerType; const currentModels = PROVIDER_CONFIG[providerType]?.models || []; - const tierConfig = TIER_CONFIG[tier]; return ( @@ -170,22 +62,10 @@ export const ChatNavbar: React.FC = ({ aria-expanded={open} className="h-9 px-3 gap-2 font-normal hover:bg-muted text-foreground" > - {mode === "simple" && orchestratorReady ? ( - <> - {tierConfig.icon} - {tierConfig.label} - - ({tierConfig.description}) - - - ) : ( - <> - - {selectedProviderLabel} - / - {model || "Select Model"} - - )} + + {selectedProviderLabel} + / + {model || "Select Model"} @@ -193,183 +73,77 @@ export const ChatNavbar: React.FC = ({ className="w-[420px] p-0 bg-background/95 backdrop-blur-sm border-border shadow-lg" align="center" > - {/* Mode Toggle */} -
- - 选择模式 - -
- - - - - Mini/Pro/Max 三档智能选择 - - - - - - 直接选择 Provider 和模型 - + {/* Provider/Model Selection */} +
+ {/* Left Column: Providers */} +
+
+ Providers +
+ {Object.entries(PROVIDER_CONFIG).map(([key, config]) => ( + + ))} +
+ + {/* Right Column: Models */} +
+
+ Models +
+ +
+ {currentModels.length === 0 ? ( +
+ No models available +
+ ) : ( + currentModels.map((m) => ( + + )) + )} +
+
- - {mode === "simple" && orchestratorReady ? ( - /* Simple Mode: Tier Selection */ -
-
- {(Object.keys(TIER_CONFIG) as ServiceTier[]).map((t) => { - const config = TIER_CONFIG[t]; - const count = - poolStats?.[`${t}_count` as keyof PoolStats] ?? 0; - const isSelected = tier === t; - - return ( - - ); - })} -
- - {/* Current Selection Info */} - {model && ( -
- 当前模型: - - {selectedProviderLabel} / {model} - -
- )} -
- ) : ( - /* Expert Mode: Provider/Model Selection */ -
- {/* Left Column: Providers */} -
-
- Providers -
- {Object.entries(PROVIDER_CONFIG).map(([key, config]) => ( - - ))} -
- - {/* Right Column: Models */} -
-
- Models -
- -
- {currentModels.length === 0 ? ( -
- No models available -
- ) : ( - currentModels.map((m) => ( - - )) - )} -
-
-
-
- )}
diff --git a/src/components/agent/chat/hooks/useAgentChat.ts b/src/components/agent/chat/hooks/useAgentChat.ts index 4446b391f..14ee571bd 100644 --- a/src/components/agent/chat/hooks/useAgentChat.ts +++ b/src/components/agent/chat/hooks/useAgentChat.ts @@ -137,6 +137,18 @@ export function useAgentChat() { savePersisted("agent_pref_model", model); }, [model]); + // 当 provider 改变时,检查当前模型是否兼容 + // 如果不兼容,自动切换到新 provider 的第一个模型 + useEffect(() => { + const currentProviderModels = providerConfig[providerType]?.models || []; + if (currentProviderModels.length > 0 && !currentProviderModels.includes(model)) { + console.log( + `[useAgentChat] 模型 ${model} 不在 ${providerType} 支持列表中,自动切换到 ${currentProviderModels[0]}` + ); + setModel(currentProviderModels[0]); + } + }, [providerType, providerConfig, model]); + useEffect(() => { saveTransient("agent_curr_sessionId", sessionId); }, [sessionId]); diff --git a/src/components/agent/chat/types.ts b/src/components/agent/chat/types.ts index d5bcce48a..080bb0e51 100644 --- a/src/components/agent/chat/types.ts +++ b/src/components/agent/chat/types.ts @@ -56,42 +56,91 @@ export const PROVIDER_CONFIG: Record< claude: { label: "Claude", models: [ + "claude-opus-4-5", "claude-opus-4-5-20251101", + "claude-opus-4-1", + "claude-opus-4-1-20250805", + "claude-opus-4-20250514", + "claude-sonnet-4-5", "claude-sonnet-4-5-20250929", + "claude-sonnet-4-0", "claude-sonnet-4-20250514", + "claude-haiku-4-5", + "claude-haiku-4-5-20251001", + "claude-3-7-sonnet-20250219", + "claude-3-5-haiku-20241022", ], }, anthropic: { label: "Anthropic", models: [ + "claude-opus-4-5", "claude-opus-4-5-20251101", + "claude-opus-4-1", + "claude-opus-4-1-20250805", + "claude-opus-4-20250514", + "claude-sonnet-4-5", "claude-sonnet-4-5-20250929", + "claude-sonnet-4-0", "claude-sonnet-4-20250514", + "claude-haiku-4-5", + "claude-haiku-4-5-20251001", + "claude-3-7-sonnet-20250219", + "claude-3-5-haiku-20241022", ], }, kiro: { label: "Kiro", - models: ["claude-sonnet-4-5-20250929", "claude-sonnet-4-20250514"], + models: [ + "claude-opus-4-5", + "claude-opus-4-5-20251101", + "claude-haiku-4-5", + "claude-sonnet-4-5", + "claude-sonnet-4-5-20250929", + "claude-sonnet-4-20250514", + "claude-3-7-sonnet-20250219", + ], }, openai: { label: "OpenAI", models: [ - "gpt-4o", - "gpt-4o-mini", - "gpt-4-turbo", - "o1", - "o1-mini", + "gpt-5.2", + "gpt-5.2-codex", + "gpt-5.1", + "gpt-5.1-codex-max", + "gpt-5.1-codex", + "gpt-5.1-codex-mini", + "gpt-5", + "gpt-5-pro", + "gpt-5-codex", + "gpt-5-codex-mini", + "gpt-5-mini", + "gpt-5-nano", + "o4-mini", "o3", "o3-mini", + "o1", + "o1-mini", + "gpt-4.1", + "gpt-4.1-mini", + "gpt-4.1-nano", + "gpt-4o", + "gpt-4o-mini", ], }, gemini: { label: "Gemini", - models: ["gemini-2.0-flash-exp", "gemini-1.5-pro", "gemini-1.5-flash"], + models: [ + "gemini-3-pro-preview", + "gemini-3-flash-preview", + "gemini-2.5-pro", + "gemini-2.5-flash", + "gemini-2.5-flash-lite", + ], }, qwen: { label: "通义千问", - models: ["qwen-max", "qwen-plus", "qwen-turbo"], + models: ["qwen3-coder-plus", "qwen3-coder-flash"], }, codex: { label: "Codex", @@ -99,11 +148,38 @@ export const PROVIDER_CONFIG: Record< }, claude_oauth: { label: "Claude OAuth", - models: ["claude-sonnet-4-5-20250929", "claude-3-5-sonnet-20241022"], + models: [ + "claude-opus-4-5", + "claude-opus-4-5-20251101", + "claude-sonnet-4-5", + "claude-sonnet-4-5-20250929", + "claude-sonnet-4-20250514", + "claude-haiku-4-5", + "claude-3-7-sonnet-20250219", + ], }, iflow: { label: "iFlow", - models: [], + models: [ + "tstars2.0", + "qwen3-coder-plus", + "qwen3-max", + "qwen3-vl-plus", + "qwen3-max-preview", + "kimi-k2-0905", + "glm-4.7", + "glm-4.6", + "kimi-k2", + "kimi-k2-thinking", + "deepseek-v3.2-chat", + "deepseek-v3.2-reasoner", + "deepseek-v3.2", + "deepseek-v3.1", + "deepseek-r1", + "deepseek-v3", + "minimax-m2.1", + "minimax-m2", + ], }, antigravity: { label: "Antigravity", @@ -111,6 +187,7 @@ export const PROVIDER_CONFIG: Record< "gemini-3-pro-preview", "gemini-3-pro-image-preview", "gemini-3-flash-preview", + "gemini-2.5-flash", "gemini-2.5-computer-use-preview-10-2025", "gemini-claude-sonnet-4-5", "gemini-claude-sonnet-4-5-thinking", diff --git a/src/components/api-server/ApiServerPage.tsx b/src/components/api-server/ApiServerPage.tsx index 08deecf35..44ffc510b 100644 --- a/src/components/api-server/ApiServerPage.tsx +++ b/src/components/api-server/ApiServerPage.tsx @@ -9,6 +9,7 @@ import { } from "lucide-react"; import { LogsTab } from "./LogsTab"; import { RoutesTab } from "./RoutesTab"; +import { EnhancedModelsTab } from "./EnhancedModelsTab"; import { ProviderIcon } from "@/icons/providers"; import { startServer, @@ -40,7 +41,7 @@ interface TestState { httpStatus?: number; } -type TabId = "server" | "routes" | "logs"; +type TabId = "server" | "routes" | "models" | "logs"; // 可用的 Provider 信息(合并 OAuth 凭证池和 API Key Provider) interface AvailableProvider { @@ -670,6 +671,7 @@ export function ApiServerPage() { {[ { id: "server" as TabId, name: "服务器控制" }, { id: "routes" as TabId, name: "路由端点" }, + { id: "models" as TabId, name: "模型列表" }, { id: "logs" as TabId, name: "系统日志" }, ].map((tab) => (
diff --git a/src/components/api-server/EnhancedModelsTab.tsx b/src/components/api-server/EnhancedModelsTab.tsx new file mode 100644 index 000000000..195348564 --- /dev/null +++ b/src/components/api-server/EnhancedModelsTab.tsx @@ -0,0 +1,370 @@ +/** + * 增强版模型列表页面 + * + * 使用 model_registry 数据,支持搜索、收藏、分组等功能 + */ + +import { useState } from "react"; +import { + Cpu, + RefreshCw, + Copy, + Check, + Search, + Star, + Clock, + Filter, + Eye, + Wrench, + Brain, + DollarSign, +} from "lucide-react"; +import { cn } from "@/lib/utils"; +import { useModelRegistry } from "@/hooks/useModelRegistry"; +import type { EnhancedModelMetadata, ModelTier } from "@/lib/types/modelRegistry"; + +export function EnhancedModelsTab() { + const { + models, + preferences, + loading, + error, + lastSyncAt, + refresh, + search, + toggleFavorite, + groupedByProvider, + } = useModelRegistry(); + + const [searchQuery, setSearchQuery] = useState(""); + const [selectedProvider, setSelectedProvider] = useState(null); + const [selectedTier, setSelectedTier] = useState(null); + const [copied, setCopied] = useState(null); + const [showFavoritesOnly, setShowFavoritesOnly] = useState(false); + + // 搜索和过滤 + const filteredModels = (() => { + let result = searchQuery ? search(searchQuery) : models; + if (selectedProvider) { + result = result.filter((m) => m.provider_id === selectedProvider); + } + if (selectedTier) { + result = result.filter((m) => m.tier === selectedTier); + } + if (showFavoritesOnly) { + result = result.filter((m) => preferences.get(m.id)?.is_favorite); + } + return result; + })(); + + const providers = Array.from(groupedByProvider.keys()); + + const copyModelId = (id: string) => { + navigator.clipboard.writeText(id); + setCopied(id); + setTimeout(() => setCopied(null), 2000); + }; + + const formatSyncTime = (timestamp: number | null) => { + if (!timestamp) return "从未同步"; + return new Date(timestamp * 1000).toLocaleString("zh-CN"); + }; + + return ( +
+ {error && ( +
+ {error} +
+ )} + + {/* 头部信息 */} +
+
+ + 上次同步: {formatSyncTime(lastSyncAt)} +
+ +
+ + {/* 搜索和过滤 */} +
+
+
+ + setSearchQuery(e.target.value)} + className="w-full rounded-lg border bg-background pl-10 pr-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary/20" + /> +
+ +
+ + {/* Provider 过滤 */} +
+ + {providers.map((providerId) => { + const providerModels = groupedByProvider.get(providerId) || []; + const providerName = providerModels[0]?.provider_name || providerId; + return ( + + ); + })} +
+ + {/* Tier 过滤 */} +
+ + 等级: + {(["mini", "pro", "max"] as ModelTier[]).map((tier) => ( + + ))} +
+
+ + {/* 模型列表 */} +
+
+
+ 模型列表 + {filteredModels.length} 个模型 +
+
+ + {loading ? ( +
+ +
+ ) : filteredModels.length === 0 ? ( +
+ +

暂无模型数据

+
+ ) : ( +
+ {filteredModels.map((model) => ( + copyModelId(model.id)} + onToggleFavorite={() => toggleFavorite(model.id)} + /> + ))} +
+ )} +
+ + {/* 使用说明 */} +
+

使用说明

+
+

• 模型数据来自 models.dev API 和本地配置

+

• 点击星标可收藏常用模型,收藏的模型会优先显示

+

• 支持按 Provider、服务等级筛选模型

+
+
+
+ ); +} + +/** 单个模型行 */ +function ModelRow({ + model, + isFavorite, + usageCount, + copied, + onCopy, + onToggleFavorite, +}: { + model: EnhancedModelMetadata; + isFavorite: boolean; + usageCount: number; + copied: boolean; + onCopy: () => void; + onToggleFavorite: () => void; +}) { + return ( +
+
+ +
+
+ {model.id} + + {model.is_latest && ( + + 最新 + + )} +
+
+ {model.provider_name} + {model.limits.context_length && ( + <> + · + {formatContextLength(model.limits.context_length)} + + )} + {usageCount > 0 && ( + <> + · + 使用 {usageCount} 次 + + )} +
+
+
+ + {/* 能力图标 */} +
+ {model.capabilities.vision && ( + + + + )} + {model.capabilities.tools && ( + + + + )} + {model.capabilities.reasoning && ( + + + + )} +
+ + {/* 定价 */} + {model.pricing && model.pricing.input_per_million && ( +
+ + {model.pricing.input_per_million.toFixed(2)} +
+ )} + + {/* 操作按钮 */} +
+ + +
+
+ ); +} + +/** 服务等级徽章 */ +function TierBadge({ tier }: { tier: string }) { + const config = { + mini: { label: "Mini", color: "bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300" }, + pro: { label: "Pro", color: "bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300" }, + max: { label: "Max", color: "bg-purple-100 text-purple-700 dark:bg-purple-900 dark:text-purple-300" }, + }[tier] || { label: tier, color: "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300" }; + + return ( + + {config.label} + + ); +} + +/** 获取 Tier 按钮激活状态的样式 */ +function getTierButtonActiveClass(tier: ModelTier): string { + const classes = { + mini: "bg-green-100 text-green-700 border-green-300 dark:bg-green-900 dark:text-green-300 dark:border-green-700", + pro: "bg-blue-100 text-blue-700 border-blue-300 dark:bg-blue-900 dark:text-blue-300 dark:border-blue-700", + max: "bg-purple-100 text-purple-700 border-purple-300 dark:bg-purple-900 dark:text-purple-300 dark:border-purple-700", + }; + return classes[tier] || "bg-primary text-primary-foreground"; +} + +/** 格式化上下文长度 */ +function formatContextLength(length: number): string { + if (length >= 1000000) { + return `${(length / 1000000).toFixed(1)}M`; + } + if (length >= 1000) { + return `${(length / 1000).toFixed(0)}K`; + } + return String(length); +} diff --git a/src/components/model-selector/EnhancedModelList.tsx b/src/components/model-selector/EnhancedModelList.tsx new file mode 100644 index 000000000..0e7dcf80e --- /dev/null +++ b/src/components/model-selector/EnhancedModelList.tsx @@ -0,0 +1,380 @@ +/** + * 增强版模型列表组件 + * + * 使用 model_registry 数据,支持搜索、收藏、分组等功能 + */ + +import { useState, useMemo } from "react"; +import { + Check, + AlertCircle, + Loader2, + Star, + Search, + ChevronDown, + ChevronRight, + Eye, + Wrench, + Brain, + DollarSign, +} from "lucide-react"; +import { cn } from "@/lib/utils"; +import type { EnhancedModelMetadata } from "@/lib/types/modelRegistry"; + +interface EnhancedModelListProps { + /** 模型列表 */ + models: EnhancedModelMetadata[]; + /** 选中的模型 ID */ + selectedModelId?: string; + /** 选择模型回调 */ + onSelectModel?: (model: EnhancedModelMetadata) => void; + /** 收藏模型回调 */ + onToggleFavorite?: (modelId: string) => void; + /** 收藏的模型 ID 集合 */ + favorites?: Set; + /** 是否加载中 */ + loading?: boolean; + /** 错误信息 */ + error?: string | null; + /** 是否按 Provider 分组 */ + groupByProvider?: boolean; + /** 是否显示搜索框 */ + showSearch?: boolean; + /** 是否显示定价信息 */ + showPricing?: boolean; + /** 自定义类名 */ + className?: string; +} + +export function EnhancedModelList({ + models, + selectedModelId, + onSelectModel, + onToggleFavorite, + favorites = new Set(), + loading = false, + error = null, + groupByProvider = true, + showSearch = true, + showPricing = false, + className, +}: EnhancedModelListProps) { + const [searchQuery, setSearchQuery] = useState(""); + const [expandedGroups, setExpandedGroups] = useState>( + new Set(["favorites"]) + ); + + // 过滤模型 + const filteredModels = useMemo(() => { + if (!searchQuery.trim()) return models; + + const query = searchQuery.toLowerCase(); + return models.filter( + (m) => + m.id.toLowerCase().includes(query) || + m.display_name.toLowerCase().includes(query) || + m.provider_name.toLowerCase().includes(query) || + m.family?.toLowerCase().includes(query) + ); + }, [models, searchQuery]); + + // 按 Provider 分组 + const groupedModels = useMemo(() => { + if (!groupByProvider) { + return { all: filteredModels }; + } + + const groups: Record = {}; + + // 先添加收藏组 + const favoriteModels = filteredModels.filter((m) => favorites.has(m.id)); + if (favoriteModels.length > 0) { + groups["favorites"] = favoriteModels; + } + + // 按 Provider 分组 + for (const model of filteredModels) { + if (!groups[model.provider_id]) { + groups[model.provider_id] = []; + } + groups[model.provider_id].push(model); + } + + return groups; + }, [filteredModels, groupByProvider, favorites]); + + // 切换分组展开状态 + const toggleGroup = (groupId: string) => { + setExpandedGroups((prev) => { + const next = new Set(prev); + if (next.has(groupId)) { + next.delete(groupId); + } else { + next.add(groupId); + } + return next; + }); + }; + + if (loading) { + return ( +
+ + 加载模型列表... +
+ ); + } + + if (error) { + return ( +
+ + {error} +
+ ); + } + + if (models.length === 0) { + return ( +
+

暂无可用模型

+

请等待模型数据加载

+
+ ); + } + + return ( +
+ {/* 搜索框 */} + {showSearch && ( +
+ + setSearchQuery(e.target.value)} + className="w-full rounded-lg border bg-background pl-10 pr-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary/20" + /> +
+ )} + + {/* 模型列表 */} +
+ {Object.entries(groupedModels).map(([groupId, groupModels]) => { + const isExpanded = expandedGroups.has(groupId); + const groupName = getGroupName(groupId, groupModels[0]); + + return ( +
+ {/* 分组头部 */} + {groupByProvider && ( + + )} + + {/* 模型列表 */} + {(!groupByProvider || isExpanded) && ( +
+ {groupModels.map((model) => ( + onSelectModel?.(model)} + onToggleFavorite={() => onToggleFavorite?.(model.id)} + showPricing={showPricing} + /> + ))} +
+ )} +
+ ); + })} +
+ + {/* 无搜索结果 */} + {filteredModels.length === 0 && searchQuery && ( +
+

未找到匹配的模型

+

尝试其他搜索词

+
+ )} +
+ ); +} + +/** 单个模型项 */ +function ModelItem({ + model, + isSelected, + isFavorite, + onSelect, + onToggleFavorite, + showPricing, +}: { + model: EnhancedModelMetadata; + isSelected: boolean; + isFavorite: boolean; + onSelect: () => void; + onToggleFavorite: () => void; + showPricing: boolean; +}) { + return ( +
+
+ {/* 选中指示器 */} +
+ {isSelected && } +
+ + {/* 模型信息 */} +
+
+ + {model.display_name} + + {model.is_latest && ( + + 最新 + + )} + +
+
+ {model.id} + {model.limits.context_length && ( + <> + · + {formatContextLength(model.limits.context_length)} + + )} +
+
+
+ + {/* 能力标签和操作 */} +
+ {/* 能力图标 */} +
+ {model.capabilities.vision && ( + + + + )} + {model.capabilities.tools && ( + + + + )} + {model.capabilities.reasoning && ( + + + + )} +
+ + {/* 定价 */} + {showPricing && model.pricing && ( +
+ + + {model.pricing.input_per_million?.toFixed(2) || "?"} + +
+ )} + + {/* 收藏按钮 */} + +
+
+ ); +} + +/** 服务等级徽章 */ +function TierBadge({ tier }: { tier: string }) { + const config = { + mini: { label: "Mini", color: "bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300" }, + pro: { label: "Pro", color: "bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300" }, + max: { label: "Max", color: "bg-purple-100 text-purple-700 dark:bg-purple-900 dark:text-purple-300" }, + }[tier] || { label: tier, color: "bg-gray-100 text-gray-700" }; + + return ( + + {config.label} + + ); +} + +/** 获取分组名称 */ +function getGroupName( + groupId: string, + firstModel: EnhancedModelMetadata +): string { + if (groupId === "favorites") return "收藏"; + if (groupId === "all") return "全部模型"; + return firstModel?.provider_name || groupId; +} + +/** 格式化上下文长度 */ +function formatContextLength(length: number): string { + if (length >= 1000000) { + return `${(length / 1000000).toFixed(1)}M`; + } + if (length >= 1000) { + return `${(length / 1000).toFixed(0)}K`; + } + return String(length); +} diff --git a/src/components/model-selector/ModelList.tsx b/src/components/model-selector/ModelList.tsx index 188d4ecc4..26d16a023 100644 --- a/src/components/model-selector/ModelList.tsx +++ b/src/components/model-selector/ModelList.tsx @@ -64,11 +64,11 @@ export function ModelList({ return (
{models.map((model) => { - const isSelected = model.model_id === selectedModelId; + const isSelected = model.id === selectedModelId; return ( + {plugin.available ? ( + + ) : ( + + )} ); @@ -433,7 +457,10 @@ const PluginDetailView: React.FC<{ fallback={
-

该插件没有提供 UI

+

该插件暂无 UI 界面

+

+ 请通过凭证池页面的「OAuth 凭证」标签管理此插件的凭证 +

} /> @@ -678,16 +705,53 @@ export const OAuthPluginTab: React.FC = () => { )} - {/* 推荐插件 */} + {/* 已安装插件 - 放在上面 */} + {filteredPlugins.length > 0 && ( +
+
+

+ + 已安装插件 +

+

+ 点击卡片进入插件详情,管理凭证 +

+
+
+
+ {filteredPlugins.map((plugin) => ( + u.pluginId === plugin.id)} + onSelect={() => setSelectedPluginId(plugin.id)} + onToggle={() => handleToggle(plugin)} + onUninstall={() => { + setPluginToUninstall(plugin.id); + setUninstallDialogOpen(true); + }} + onUpdate={ + updates.find((u) => u.pluginId === plugin.id) + ? () => update(plugin.id) + : undefined + } + /> + ))} +
+
+
+ )} + + {/* 推荐插件 - 放在下面 */} {uninstalledRecommendedPlugins.length > 0 && (

- 推荐 OAuth Provider 插件 + 可安装的 OAuth Provider 插件

- 一键安装推荐的 OAuth Provider 插件,快速扩展支持的 AI 服务 + 一键安装 OAuth Provider 插件,快速扩展支持的 AI 服务

@@ -705,45 +769,25 @@ export const OAuthPluginTab: React.FC = () => {
)} - {/* 插件列表 */} - {filteredPlugins.length === 0 ? ( -
- -

- {searchQuery ? "没有找到匹配的插件" : "暂无已安装的插件"} -

- {!searchQuery && ( - - )} -
- ) : ( -
- {filteredPlugins.map((plugin) => ( - u.pluginId === plugin.id)} - onSelect={() => setSelectedPluginId(plugin.id)} - onToggle={() => handleToggle(plugin)} - onUninstall={() => { - setPluginToUninstall(plugin.id); - setUninstallDialogOpen(true); - }} - onUpdate={ - updates.find((u) => u.pluginId === plugin.id) - ? () => update(plugin.id) - : undefined - } - /> - ))} -
- )} + {/* 空状态 - 没有已安装插件且没有推荐插件 */} + {filteredPlugins.length === 0 && + uninstalledRecommendedPlugins.length === 0 && ( +
+ +

+ {searchQuery ? "没有找到匹配的插件" : "暂无可用的插件"} +

+ {!searchQuery && ( + + )} +
+ )} {/* 安装对话框 */} = { gemini_api_key: "Gemini", }; -const configTabLabels: Record = { - vertex: "Vertex AI", - amp: "Amp CLI", -}; - // 判断是否为配置类型 tab const isConfigTab = (tab: TabType): tab is ConfigTabType => { - return ["vertex", "amp"].includes(tab); + return tab === "connect"; }; // 分类类型 -type CategoryType = "oauth" | "apikey" | "plugins" | "config"; +type CategoryType = "oauth" | "apikey" | "plugins" | "connect"; export const ProviderPoolPage = forwardRef( (_props, ref) => { @@ -137,45 +131,6 @@ export const ProviderPoolPage = forwardRef( // Kiro 本地活跃凭证 UUID const [localActiveUuid, setLocalActiveUuid] = useState(null); - // 配置 tab 相关状态 - const [config, setConfig] = useState(null); - const [configLoading, setConfigLoading] = useState(false); - const [configSaving, setConfigSaving] = useState(false); - - // 加载配置 - const loadConfig = async () => { - setConfigLoading(true); - try { - const c = await getConfig(); - setConfig(c); - } catch (e) { - console.error("Failed to load config:", e); - showError("加载配置失败", "config"); - } - setConfigLoading(false); - }; - - // 保存配置 - const handleSaveConfig = async () => { - if (!config) return; - setConfigSaving(true); - try { - await saveConfig(config); - showSuccess("配置已保存"); - } catch (e) { - showError(e instanceof Error ? e.message : String(e), "config"); - } - setConfigSaving(false); - }; - - // 切换到配置 tab 时加载配置 - useEffect(() => { - if (isConfigTab(activeTab)) { - loadConfig(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [activeTab]); - // 获取本地活跃的 Kiro 凭证 UUID const fetchLocalActiveUuid = async () => { try { @@ -443,17 +398,17 @@ export const ProviderPoolPage = forwardRef(
@@ -497,26 +452,10 @@ export const ProviderPoolPage = forwardRef(
)} - {/* 其他配置分类 - 配置 Tab 选择 */} - {activeCategory === "config" && ( -
- {(["vertex", "amp"] as const).map((tabId) => { - const isActive = activeTab === tabId; - return ( - - ); - })} + {/* Connect 分类 - 中转商列表 */} + {activeCategory === "connect" && ( +
+
)} @@ -539,67 +478,6 @@ export const ProviderPoolPage = forwardRef(
)} - {/* 配置 Tab 内容 */} - {activeCategory === "config" && - isConfigTab(activeTab) && - (configLoading ? ( -
- -
- ) : config ? ( -
- {activeTab === "vertex" && ( - <> - - setConfig({ - ...config, - credential_pool: { - ...config.credential_pool, - vertex_api_keys: entries, - }, - }) - } - /> - {(config.credential_pool?.vertex_api_keys?.length ?? 0) > - 0 && ( - - )} - - )} - - {activeTab === "amp" && ( - - setConfig({ - ...config, - ampcode: ampConfig, - }) - } - onSave={handleSaveConfig} - /> - )} -
- ) : ( -
- 加载配置失败 -
- ))} - {/* OAuth 凭证内容 - 卡片布局 */} {activeCategory === "oauth" && !isConfigTab(activeTab) && diff --git a/src/components/provider-pool/README.md b/src/components/provider-pool/README.md index 43f286fcc..7a21064e7 100644 --- a/src/components/provider-pool/README.md +++ b/src/components/provider-pool/README.md @@ -13,11 +13,13 @@ | `EditCredentialModal.tsx` | 编辑凭证模态框组件 | | `ErrorDisplay.tsx` | 错误显示组件 | | `UsageDisplay.tsx` | 用量显示组件 | +| `RelayProvidersSection.tsx` | Connect 中转商列表组件,展示已验证的中转服务商 | | `VertexAISection.tsx` | Vertex AI 配置区域组件 | | `AmpConfigSection.tsx` | Amp CLI 配置区域组件 | | `GeminiApiKeySection.tsx` | Gemini API Key 配置区域组件 | | `CodexSection.tsx` | Codex 配置区域组件 | | `IFlowSection.tsx` | iFlow 配置区域组件 | +| `OAuthPluginTab.tsx` | OAuth 插件标签页组件 | | `index.ts` | 组件导出入口 | ## 子目录 @@ -50,10 +52,12 @@ function App() { - Requirements 1.1: API Key Provider 左右分栏布局 - Requirements 2.1, 2.2, 2.3: OAuth 凭证保持卡片布局 - Requirements 3.1-3.6: 完整支持 System Provider 类型 +- Connect: 中转商浏览和一键添加功能 ## 架构说明 -ProviderPoolPage 支持三种分类: +ProviderPoolPage 支持四种分类: 1. **OAuth 凭证** - 使用卡片式布局显示 OAuth 类型凭证 2. **API Key** - 使用左右分栏布局(ApiKeyProviderSection) -3. **其他配置** - Vertex AI 和 Amp CLI 配置 +3. **OAuth 插件** - 第三方 OAuth 插件管理 +4. **Connect** - 中转商列表,支持浏览和一键获取 API Key diff --git a/src/components/provider-pool/RelayProvidersSection.tsx b/src/components/provider-pool/RelayProvidersSection.tsx new file mode 100644 index 000000000..54ccd8626 --- /dev/null +++ b/src/components/provider-pool/RelayProvidersSection.tsx @@ -0,0 +1,257 @@ +/** + * @file RelayProvidersSection 组件 + * @description 中转商列表展示组件,支持浏览和一键跳转获取 API Key + * @module components/provider-pool/RelayProvidersSection + * + * _Requirements: Connect 中转商浏览功能_ + */ + +import { useState } from "react"; +import { + RefreshCw, + ExternalLink, + Globe, + Mail, + MessageCircle, + Shield, + ShieldCheck, + Zap, + Clock, + AlertCircle, +} from "lucide-react"; +import { useRelayRegistry } from "@/hooks/useRelayRegistry"; +import type { RelayInfo } from "@/hooks/useDeepLink"; +import { open } from "@tauri-apps/plugin-shell"; + +/** + * 中转商卡片组件 + */ +function RelayProviderCard({ provider }: { provider: RelayInfo }) { + const [imageError, setImageError] = useState(false); + + // 打开外部链接 + const handleOpenLink = async (url: string) => { + try { + await open(url); + } catch (e) { + console.error("打开链接失败:", e); + // 回退到 window.open + window.open(url, "_blank"); + } + }; + + // 获取 API Key 的链接(优先使用 dashboard,其次 website) + const getApiKeyLink = () => { + return provider.links.dashboard || provider.links.website; + }; + + return ( +
+ {/* 头部:Logo + 名称 */} +
+ {/* Logo */} +
+ {provider.branding.logo && !imageError ? ( + {provider.name} setImageError(true)} + /> + ) : ( + + )} +
+ + {/* 名称和描述 */} +
+
+

+ {provider.name} +

+ {provider.features.verified && ( + + )} +
+

+ {provider.description} +

+
+
+ + {/* API 信息 */} +
+ + + {provider.api.protocol.toUpperCase()} + + {provider.features.streaming && ( + + 流式响应 + + )} + {provider.features.models && provider.features.models.length > 0 && ( + + {provider.features.models.length} 个模型 + + )} +
+ + {/* 功能特性 */} + {provider.features.models && provider.features.models.length > 0 && ( +
+

支持模型:

+
+ {provider.features.models.slice(0, 5).map((model) => ( + + {model} + + ))} + {provider.features.models.length > 5 && ( + + +{provider.features.models.length - 5} + + )} +
+
+ )} + + {/* 操作按钮 */} +
+ {getApiKeyLink() && ( + + )} + + {provider.links.docs && ( + + )} + + {provider.contact.email && ( + + )} + + {provider.contact.discord && ( + + )} +
+
+ ); +} + +/** + * 中转商列表组件 + */ +export function RelayProvidersSection() { + const { providers, isLoading, error, refresh } = useRelayRegistry(); + + return ( +
+ {/* 头部说明 */} +
+
+
+ +

ProxyCast Connect

+
+

+ 浏览已验证的 AI API 中转服务商,获取 API Key + 后可通过链接一键添加到凭证池 +

+
+ +
+ + {/* 错误提示 */} + {error && ( +
+ + {error.message} + +
+ )} + + {/* 加载状态 */} + {isLoading && providers.length === 0 && ( +
+ +
+ )} + + {/* 空状态 */} + {!isLoading && providers.length === 0 && !error && ( +
+ +

暂无中转商

+

点击刷新按钮加载中转商列表

+ +
+ )} + + {/* 中转商列表 */} + {providers.length > 0 && ( +
+ {providers.map((provider) => ( + + ))} +
+ )} + + {/* 底部说明 */} + {providers.length > 0 && ( +
+

+ 获取 API Key 后,中转商会提供一个 proxycast://{" "} + 链接,点击即可一键添加到凭证池 +

+
+ )} +
+ ); +} + +export default RelayProvidersSection; diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 1727f7e68..960be11d2 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1,6 +1,7 @@ export { useFlowEvents } from "./useFlowEvents"; export { useOAuthPlugins, useSingleOAuthPlugin } from "./useOAuthPlugins"; export { useDeepLink } from "./useDeepLink"; +export { useModelRegistry } from "./useModelRegistry"; export type { ConnectPayload, RelayInfo, diff --git a/src/hooks/useDeepLink.ts b/src/hooks/useDeepLink.ts index 2157f215d..c9c155c41 100644 --- a/src/hooks/useDeepLink.ts +++ b/src/hooks/useDeepLink.ts @@ -54,6 +54,10 @@ export interface RelayLinks { docs?: string; /** 状态页链接 */ status?: string; + /** 控制台/仪表盘链接 */ + dashboard?: string; + /** 网站链接 */ + website?: string; } /** @@ -92,6 +96,8 @@ export interface RelayFeatures { function_calling: boolean; /** 是否支持视觉模型 */ vision: boolean; + /** 是否已验证 */ + verified?: boolean; } /** diff --git a/src/hooks/useModelRegistry.ts b/src/hooks/useModelRegistry.ts new file mode 100644 index 000000000..e2737354b --- /dev/null +++ b/src/hooks/useModelRegistry.ts @@ -0,0 +1,333 @@ +/** + * 模型注册表 Hook + * + * 提供模型数据管理、搜索、收藏等功能 + */ + +import { useState, useEffect, useCallback, useMemo } from "react"; +import { modelRegistryApi } from "@/lib/api/modelRegistry"; +import type { + EnhancedModelMetadata, + UserModelPreference, + ModelTier, +} from "@/lib/types/modelRegistry"; + +interface UseModelRegistryOptions { + /** 自动加载 */ + autoLoad?: boolean; + /** 过滤的 Provider ID 列表 */ + providerFilter?: string[]; + /** 过滤的服务等级 */ + tierFilter?: ModelTier[]; + /** 只显示收藏 */ + favoritesOnly?: boolean; +} + +interface UseModelRegistryReturn { + /** 模型列表 */ + models: EnhancedModelMetadata[]; + /** 用户偏好 */ + preferences: Map; + /** 是否加载中 */ + loading: boolean; + /** 错误信息 */ + error: string | null; + /** 最后同步时间 */ + lastSyncAt: number | null; + /** 刷新模型列表 */ + refresh: () => Promise; + /** 搜索模型 */ + search: (query: string) => EnhancedModelMetadata[]; + /** 切换收藏 */ + toggleFavorite: (modelId: string) => Promise; + /** 隐藏模型 */ + hideModel: (modelId: string) => Promise; + /** 获取模型详情 */ + getModel: (modelId: string) => EnhancedModelMetadata | undefined; + /** 按 Provider 分组 */ + groupedByProvider: Map; + /** 按等级分组 */ + groupedByTier: Map; +} + +/** + * 智能排序函数 + */ +function sortModels( + models: EnhancedModelMetadata[], + preferences: Map +): EnhancedModelMetadata[] { + return [...models].sort((a, b) => { + const prefA = preferences.get(a.id); + const prefB = preferences.get(b.id); + + // 1. 收藏优先 + if (prefA?.is_favorite && !prefB?.is_favorite) return -1; + if (!prefA?.is_favorite && prefB?.is_favorite) return 1; + + // 2. 最新版本优先 + if (a.is_latest && !b.is_latest) return -1; + if (!a.is_latest && b.is_latest) return 1; + + // 3. 活跃状态优先 + if (a.status === "active" && b.status !== "active") return -1; + if (a.status !== "active" && b.status === "active") return 1; + + // 4. 使用频率 + const usageA = prefA?.usage_count || 0; + const usageB = prefB?.usage_count || 0; + if (usageA !== usageB) return usageB - usageA; + + // 5. 按名称字母序 + return a.display_name.localeCompare(b.display_name); + }); +} + +/** + * 简单的模糊搜索 + */ +function fuzzySearch( + models: EnhancedModelMetadata[], + query: string +): EnhancedModelMetadata[] { + if (!query.trim()) { + return models; + } + + const queryLower = query.toLowerCase(); + + return models + .map((model) => { + let score = 0; + + // 精确匹配 ID + if (model.id.toLowerCase() === queryLower) { + score += 100; + } else if (model.id.toLowerCase().includes(queryLower)) { + score += 50; + } + + // 显示名称匹配 + if (model.display_name.toLowerCase().includes(queryLower)) { + score += 30; + } + + // Provider 匹配 + if (model.provider_name.toLowerCase().includes(queryLower)) { + score += 20; + } + + // 家族匹配 + if (model.family?.toLowerCase().includes(queryLower)) { + score += 15; + } + + // 最新版本加分 + if (model.is_latest) { + score += 5; + } + + // 活跃状态加分 + if (model.status === "active") { + score += 3; + } + + return { model, score }; + }) + .filter(({ score }) => score > 0) + .sort((a, b) => b.score - a.score) + .map(({ model }) => model); +} + +export function useModelRegistry( + options: UseModelRegistryOptions = {} +): UseModelRegistryReturn { + const { + autoLoad = true, + providerFilter, + tierFilter, + favoritesOnly = false, + } = options; + + const [allModels, setAllModels] = useState([]); + const [preferences, setPreferences] = useState< + Map + >(new Map()); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [lastSyncAt, setLastSyncAt] = useState(null); + + // 加载模型数据 + const loadModels = useCallback(async () => { + setLoading(true); + setError(null); + + try { + const [models, prefs, syncState] = await Promise.all([ + modelRegistryApi.getModelRegistry(), + modelRegistryApi.getModelPreferences(), + modelRegistryApi.getModelSyncState(), + ]); + + setAllModels(models); + setPreferences(new Map(prefs.map((p) => [p.model_id, p]))); + setLastSyncAt(syncState.last_sync_at); + } catch (e) { + setError(e instanceof Error ? e.message : String(e)); + } finally { + setLoading(false); + } + }, []); + + // 刷新(强制从 models.dev 获取) + const refresh = useCallback(async () => { + setLoading(true); + setError(null); + + try { + await modelRegistryApi.refreshModelRegistry(); + await loadModels(); + } catch (e) { + setError(e instanceof Error ? e.message : String(e)); + } finally { + setLoading(false); + } + }, [loadModels]); + + // 过滤后的模型列表 + const models = useMemo(() => { + let filtered = allModels; + + // Provider 过滤 + if (providerFilter && providerFilter.length > 0) { + filtered = filtered.filter((m) => providerFilter.includes(m.provider_id)); + } + + // 等级过滤 + if (tierFilter && tierFilter.length > 0) { + filtered = filtered.filter((m) => + tierFilter.includes(m.tier as ModelTier) + ); + } + + // 收藏过滤 + if (favoritesOnly) { + filtered = filtered.filter((m) => preferences.get(m.id)?.is_favorite); + } + + // 隐藏过滤 + filtered = filtered.filter((m) => !preferences.get(m.id)?.is_hidden); + + // 智能排序 + return sortModels(filtered, preferences); + }, [allModels, providerFilter, tierFilter, favoritesOnly, preferences]); + + // 模糊搜索 + const search = useCallback( + (query: string): EnhancedModelMetadata[] => { + return fuzzySearch(models, query); + }, + [models] + ); + + // 切换收藏 + const toggleFavorite = useCallback(async (modelId: string) => { + try { + const newState = await modelRegistryApi.toggleModelFavorite(modelId); + setPreferences((prev) => { + const newPrefs = new Map(prev); + const current = newPrefs.get(modelId); + if (current) { + newPrefs.set(modelId, { + ...current, + is_favorite: newState, + }); + } else { + newPrefs.set(modelId, { + model_id: modelId, + is_favorite: newState, + is_hidden: false, + custom_alias: null, + usage_count: 0, + last_used_at: null, + created_at: Date.now() / 1000, + updated_at: Date.now() / 1000, + }); + } + return newPrefs; + }); + } catch (e) { + console.error("Failed to toggle favorite:", e); + } + }, []); + + // 隐藏模型 + const hideModel = useCallback(async (modelId: string) => { + try { + await modelRegistryApi.hideModel(modelId); + setPreferences((prev) => { + const newPrefs = new Map(prev); + const current = newPrefs.get(modelId); + if (current) { + newPrefs.set(modelId, { ...current, is_hidden: true }); + } + return newPrefs; + }); + } catch (e) { + console.error("Failed to hide model:", e); + } + }, []); + + // 获取单个模型 + const getModel = useCallback( + (modelId: string) => { + return allModels.find((m) => m.id === modelId); + }, + [allModels] + ); + + // 按 Provider 分组 + const groupedByProvider = useMemo(() => { + const groups = new Map(); + for (const model of models) { + const existing = groups.get(model.provider_id) || []; + existing.push(model); + groups.set(model.provider_id, existing); + } + return groups; + }, [models]); + + // 按等级分组 + const groupedByTier = useMemo(() => { + const groups = new Map(); + for (const model of models) { + const tier = model.tier as ModelTier; + const existing = groups.get(tier) || []; + existing.push(model); + groups.set(tier, existing); + } + return groups; + }, [models]); + + // 自动加载 + useEffect(() => { + if (autoLoad) { + loadModels(); + } + }, [autoLoad, loadModels]); + + return { + models, + preferences, + loading, + error, + lastSyncAt, + refresh, + search, + toggleFavorite, + hideModel, + getModel, + groupedByProvider, + groupedByTier, + }; +} diff --git a/src/lib/api/modelRegistry.ts b/src/lib/api/modelRegistry.ts new file mode 100644 index 000000000..f74c24b94 --- /dev/null +++ b/src/lib/api/modelRegistry.ts @@ -0,0 +1,114 @@ +/** + * 模型注册表 API + * + * 提供与后端 ModelRegistryService 交互的 API + */ + +import { invoke } from "@tauri-apps/api/core"; +import type { + EnhancedModelMetadata, + ModelSyncState, + ModelTier, + UserModelPreference, +} from "@/lib/types/modelRegistry"; + +/** + * 获取所有模型 + */ +export async function getModelRegistry(): Promise { + return invoke("get_model_registry"); +} + +/** + * 刷新模型注册表(从 models.dev 获取最新数据) + */ +export async function refreshModelRegistry(): Promise { + return invoke("refresh_model_registry"); +} + +/** + * 搜索模型 + * @param query 搜索关键词 + * @param limit 返回数量限制 + */ +export async function searchModels( + query: string, + limit?: number +): Promise { + return invoke("search_models", { query, limit }); +} + +/** + * 获取用户模型偏好 + */ +export async function getModelPreferences(): Promise { + return invoke("get_model_preferences"); +} + +/** + * 切换模型收藏状态 + * @param modelId 模型 ID + * @returns 新的收藏状态 + */ +export async function toggleModelFavorite(modelId: string): Promise { + return invoke("toggle_model_favorite", { modelId }); +} + +/** + * 隐藏模型 + * @param modelId 模型 ID + */ +export async function hideModel(modelId: string): Promise { + return invoke("hide_model", { modelId }); +} + +/** + * 记录模型使用 + * @param modelId 模型 ID + */ +export async function recordModelUsage(modelId: string): Promise { + return invoke("record_model_usage", { modelId }); +} + +/** + * 获取模型同步状态 + */ +export async function getModelSyncState(): Promise { + return invoke("get_model_sync_state"); +} + +/** + * 按 Provider 获取模型 + * @param providerId Provider ID + */ +export async function getModelsForProvider( + providerId: string +): Promise { + return invoke("get_models_for_provider", { providerId }); +} + +/** + * 按服务等级获取模型 + * @param tier 服务等级 + */ +export async function getModelsByTier( + tier: ModelTier +): Promise { + return invoke("get_models_by_tier", { tier }); +} + +/** + * 模型注册表 API 对象 + */ +export const modelRegistryApi = { + getModelRegistry, + refreshModelRegistry, + searchModels, + getModelPreferences, + toggleModelFavorite, + hideModel, + recordModelUsage, + getModelSyncState, + getModelsForProvider, + getModelsByTier, +}; diff --git a/src/lib/api/orchestrator.ts b/src/lib/api/orchestrator.ts index 08aab530e..339d085d6 100644 --- a/src/lib/api/orchestrator.ts +++ b/src/lib/api/orchestrator.ts @@ -27,23 +27,29 @@ export type TaskHint = /** 可用模型 */ export interface AvailableModel { /** 模型 ID */ - model_id: string; + id: string; /** 显示名称 */ display_name: string; /** Provider 类型 */ provider_type: string; + /** 模型家族 */ + family?: string; /** 凭证 ID */ credential_id: string; + /** 上下文长度 */ + context_length?: number; + /** 是否支持视觉 */ + supports_vision: boolean; + /** 是否支持工具 */ + supports_tools: boolean; + /** 输入价格(每 1M tokens) */ + input_cost_per_million?: number; + /** 输出价格(每 1M tokens) */ + output_cost_per_million?: number; /** 是否健康 */ is_healthy: boolean; /** 当前负载 (0-100) */ current_load?: number; - /** 上下文长度 */ - context_length?: number; - /** 是否支持视觉 */ - supports_vision?: boolean; - /** 是否支持工具 */ - supports_tools?: boolean; } /** 模型池统计 */ @@ -62,16 +68,20 @@ export interface PoolStats { /** 选择结果 */ export interface SelectionResult { - /** 选中的模型 ID */ - model_id: string; - /** Provider 类型 */ - provider_type: string; - /** 凭证 ID */ - credential_id: string; - /** 使用的策略 */ - strategy_used: string; + /** 选中的模型 */ + model: AvailableModel; + /** 使用的策略 ID */ + strategy_id: string; /** 选择原因 */ - reason?: string; + reason: string; + /** 置信度 (0-100) */ + confidence: number; + /** 服务等级 */ + tier: ServiceTier; + /** 是否是降级选择 */ + is_fallback: boolean; + /** 降级原因(如果是降级) */ + fallback_reason?: string; } /** 选择请求 */ diff --git a/src/lib/plugin-loader/PluginUIRenderer.tsx b/src/lib/plugin-loader/PluginUIRenderer.tsx index da25759d7..e7bd33356 100644 --- a/src/lib/plugin-loader/PluginUIRenderer.tsx +++ b/src/lib/plugin-loader/PluginUIRenderer.tsx @@ -117,6 +117,24 @@ export function PluginUIRenderer({ // 错误 if (error) { + // 检查是否是文件不存在的错误 + const isFileNotFound = error.includes("读取插件 UI 文件失败") || + error.includes("No such file") || + error.includes("not found") || + error.includes("没有找到有效的组件导出") || + error.includes("插件加载失败"); + + if (isFileNotFound) { + // UI 文件不存在时显示友好提示 + return fallback ? <>{fallback} : ( +
+ +

该插件暂无 UI 界面

+

请通过命令行或 API 使用此插件

+
+ ); + } + return (
(); +/** + * 插件 ID 到全局变量名的映射 + * 格式: pluginId -> GlobalVariableName + */ +const PLUGIN_GLOBAL_NAMES: Record = { + "kiro-provider": "KiroProviderPlugin", + "droid-provider": "DroidProviderPlugin", + "claude-provider": "ClaudeProviderPlugin", + "gemini-provider": "GeminiProviderPlugin", + "antigravity-provider": "AntigravityProviderPlugin", +}; + +/** + * 根据插件 ID 获取全局变量名 + * 如果没有预定义,则尝试从路径推断 + */ +function getPluginGlobalName(pluginPath: string): string { + // 从路径中提取插件 ID + const parts = pluginPath.split("/"); + const pluginIdIndex = parts.findIndex((p) => p.endsWith("-provider")); + const pluginId = pluginIdIndex >= 0 ? parts[pluginIdIndex] : null; + + // 查找预定义的全局变量名 + if (pluginId && PLUGIN_GLOBAL_NAMES[pluginId]) { + return PLUGIN_GLOBAL_NAMES[pluginId]; + } + + // 尝试从插件 ID 推断全局变量名 + // 例如: my-plugin -> MyPluginPlugin + if (pluginId) { + const camelCase = pluginId + .split("-") + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(""); + return camelCase; + } + + // 默认回退 + return "KiroProviderPlugin"; +} + /** * 读取插件文件内容 */ @@ -92,22 +133,26 @@ export async function loadPluginUI( // 读取插件文件内容 const content = await readPluginFile(pluginPath); + // 获取插件的全局变量名 + const globalName = getPluginGlobalName(pluginPath); + console.log(`[PluginLoader] 加载插件: ${pluginPath}`); + console.log(`[PluginLoader] 全局变量名: ${globalName}`); console.log( `[PluginLoader] 全局变量检查: React=${typeof (window as unknown as Record).React}, ProxyCastPluginComponents=${typeof (window as unknown as Record).ProxyCastPluginComponents}`, ); // 执行插件代码 - // IIFE 格式会自动将结果赋值给 window.KiroProviderPlugin await executeScript(content); // 获取插件模块 - const pluginExports = (window as unknown as Record) - .KiroProviderPlugin as Record | undefined; + const pluginExports = (window as unknown as Record)[ + globalName + ] as Record | undefined; if (!pluginExports) { console.error( - `[PluginLoader] 插件 ${pluginPath} 没有导出到 window.KiroProviderPlugin`, + `[PluginLoader] 插件 ${pluginPath} 没有导出到 window.${globalName}`, ); return null; } diff --git a/src/lib/types/modelRegistry.ts b/src/lib/types/modelRegistry.ts new file mode 100644 index 000000000..d5b8d3eb0 --- /dev/null +++ b/src/lib/types/modelRegistry.ts @@ -0,0 +1,144 @@ +/** + * 模型注册表类型定义 + * + * 借鉴 opencode 的模型管理方式,定义增强的模型元数据结构 + */ + +/** 模型能力 */ +export interface ModelCapabilities { + /** 是否支持视觉输入 */ + vision: boolean; + /** 是否支持工具调用 */ + tools: boolean; + /** 是否支持流式输出 */ + streaming: boolean; + /** 是否支持 JSON 模式 */ + json_mode: boolean; + /** 是否支持函数调用 */ + function_calling: boolean; + /** 是否支持推理/思考 */ + reasoning: boolean; +} + +/** 模型定价 */ +export interface ModelPricing { + /** 输入价格(每百万 token) */ + input_per_million: number | null; + /** 输出价格(每百万 token) */ + output_per_million: number | null; + /** 缓存读取价格(每百万 token) */ + cache_read_per_million: number | null; + /** 缓存写入价格(每百万 token) */ + cache_write_per_million: number | null; + /** 货币单位 */ + currency: string; +} + +/** 模型限制 */ +export interface ModelLimits { + /** 上下文长度 */ + context_length: number | null; + /** 最大输出 token 数 */ + max_output_tokens: number | null; + /** 每分钟请求数限制 */ + requests_per_minute: number | null; + /** 每分钟 token 数限制 */ + tokens_per_minute: number | null; +} + +/** 模型状态 */ +export type ModelStatus = + | "active" + | "preview" + | "alpha" + | "beta" + | "deprecated" + | "legacy"; + +/** 模型服务等级 */ +export type ModelTier = "mini" | "pro" | "max"; + +/** 模型数据来源 */ +export type ModelSource = "models.dev" | "local" | "custom"; + +/** 增强的模型元数据 */ +export interface EnhancedModelMetadata { + /** 模型 ID (如 "claude-sonnet-4-5-20250514") */ + id: string; + /** 显示名称 (如 "Claude Sonnet 4.5") */ + display_name: string; + /** Provider ID (如 "anthropic", "openai", "dashscope") */ + provider_id: string; + /** Provider 显示名称 */ + provider_name: string; + /** 模型家族 (如 "sonnet", "gpt-4", "qwen") */ + family: string | null; + /** 服务等级 */ + tier: ModelTier; + /** 模型能力 */ + capabilities: ModelCapabilities; + /** 定价信息 */ + pricing: ModelPricing | null; + /** 限制信息 */ + limits: ModelLimits; + /** 模型状态 */ + status: ModelStatus; + /** 发布日期 */ + release_date: string | null; + /** 是否为最新版本 */ + is_latest: boolean; + /** 描述 */ + description: string | null; + /** 数据来源 */ + source: ModelSource; + /** 创建时间 (Unix 时间戳) */ + created_at: number; + /** 最后更新时间 (Unix 时间戳) */ + updated_at: number; +} + +/** 用户模型偏好 */ +export interface UserModelPreference { + /** 模型 ID */ + model_id: string; + /** 是否收藏 */ + is_favorite: boolean; + /** 是否隐藏 */ + is_hidden: boolean; + /** 自定义别名 */ + custom_alias: string | null; + /** 使用次数 */ + usage_count: number; + /** 最后使用时间 (Unix 时间戳) */ + last_used_at: number | null; + /** 创建时间 (Unix 时间戳) */ + created_at: number; + /** 更新时间 (Unix 时间戳) */ + updated_at: number; +} + +/** 模型同步状态 */ +export interface ModelSyncState { + /** 最后同步时间 (Unix 时间戳) */ + last_sync_at: number | null; + /** 同步的模型数量 */ + model_count: number; + /** 是否正在同步 */ + is_syncing: boolean; + /** 最后同步错误 */ + last_error: string | null; +} + +/** 模型注册表状态 */ +export interface ModelRegistryState { + /** 模型列表 */ + models: EnhancedModelMetadata[]; + /** 用户偏好 */ + preferences: Map; + /** 最后同步时间 */ + lastSyncAt: number | null; + /** 是否加载中 */ + isLoading: boolean; + /** 错误信息 */ + error: string | null; +}