From 5dbea70eb1e6c7a367186f012c8e369012ab45d2 Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 8 Apr 2026 23:17:09 +0800 Subject: [PATCH] feat(providers): seed an official preset on startup for Claude/Codex/Gemini New and existing users now see a built-in "Claude Official" / "OpenAI Official" / "Google Official" entry in their provider list, so switching back to the official endpoint is one click away instead of buried in the README. - New providers_seed.rs holds the three seeds (id, name, settings_config, icon) keyed by AppType, with a single is_official_seed_id() helper that scans OFFICIAL_SEEDS so the id list has one source of truth. - Database::init_default_official_providers() runs once per database (gated by an official_providers_seeded setting flag), appends each seed to the end of the sort order, and never touches is_current. - Startup also auto-imports the live config (settings.json / auth.json / .env) as a "default" provider before seeding, so users with an existing manual config don't lose it when they click the official preset. - Database::has_non_official_seed_provider() replaces the get_all_providers call in import_default_config's gating check with an id-only scan, dropping the N+1 endpoint sub-queries from every startup. --- src-tauri/src/database/dao/mod.rs | 1 + src-tauri/src/database/dao/providers.rs | 99 ++++++++++++++++++++ src-tauri/src/database/dao/providers_seed.rs | 66 +++++++++++++ src-tauri/src/lib.rs | 35 +++++++ src-tauri/src/services/provider/live.rs | 11 ++- 5 files changed, 207 insertions(+), 5 deletions(-) create mode 100644 src-tauri/src/database/dao/providers_seed.rs diff --git a/src-tauri/src/database/dao/mod.rs b/src-tauri/src/database/dao/mod.rs index 4aa335a82..93ca2f578 100644 --- a/src-tauri/src/database/dao/mod.rs +++ b/src-tauri/src/database/dao/mod.rs @@ -6,6 +6,7 @@ pub mod failover; pub mod mcp; pub mod prompts; pub mod providers; +pub mod providers_seed; pub mod proxy; pub mod settings; pub mod skills; diff --git a/src-tauri/src/database/dao/providers.rs b/src-tauri/src/database/dao/providers.rs index bf0a44361..1c5188839 100644 --- a/src-tauri/src/database/dao/providers.rs +++ b/src-tauri/src/database/dao/providers.rs @@ -501,4 +501,103 @@ impl Database { in_failover_queue: false, })) } + + /// 判断指定 app 下是否存在非官方种子的供应商。 + /// + /// 比 `get_all_providers` 轻量得多:只读 id 列、无 endpoint 子查询、首条命中即返回。 + /// 用于 `import_default_config` 决定是否跳过 live 导入。 + pub fn has_non_official_seed_provider(&self, app_type: &str) -> Result { + use crate::database::dao::providers_seed::is_official_seed_id; + let conn = lock_conn!(self.conn); + let mut stmt = conn + .prepare("SELECT id FROM providers WHERE app_type = ?1") + .map_err(|e| AppError::Database(e.to_string()))?; + let mut rows = stmt + .query(params![app_type]) + .map_err(|e| AppError::Database(e.to_string()))?; + while let Some(row) = rows.next().map_err(|e| AppError::Database(e.to_string()))? { + let id: String = row.get(0).map_err(|e| AppError::Database(e.to_string()))?; + if !is_official_seed_id(&id) { + return Ok(true); + } + } + Ok(false) + } + + /// 计算指定 app 下一个可用的 sort_index(追加到末尾)。 + fn next_sort_index_for_app(&self, app_type: &str) -> Result { + let conn = lock_conn!(self.conn); + let max: Option = conn + .query_row( + "SELECT MAX(sort_index) FROM providers WHERE app_type = ?1", + params![app_type], + |row| row.get(0), + ) + .map_err(|e| AppError::Database(e.to_string()))?; + Ok(max.map(|v| (v + 1) as usize).unwrap_or(0)) + } + + /// 启动时调用:补齐缺失的官方预设供应商(Claude / Codex / Gemini)。 + /// + /// 使用 settings flag `official_providers_seeded` 保证每个数据库只执行一次: + /// - 全新用户:seed 三条官方预设 + /// - 老用户升级:同样会触发一次(flag 不存在),追加到末尾,不影响已有排序 + /// - 用户删除 seed 后:不再重建(flag 已为 true),尊重用户意图 + /// + /// 与 `Database::save_provider` 的 UPSERT 语义配合,即使被意外重复调用 + /// 也不会覆盖用户当前激活的供应商(is_current 字段会被保留)。 + pub fn init_default_official_providers(&self) -> Result { + use crate::database::dao::providers_seed::OFFICIAL_SEEDS; + + // flag 检查:已执行过则跳过 + if let Ok(Some(flag)) = self.get_setting("official_providers_seeded") { + if flag == "true" || flag == "1" { + return Ok(0); + } + } + + let mut inserted = 0_usize; + let now_ms = chrono::Utc::now().timestamp_millis(); + + for seed in OFFICIAL_SEEDS { + let app_type_str = seed.app_type.as_str(); + + // 若该 id 已存在(极端情况:用户曾手动用过同 id),跳过 + if self.get_provider_by_id(seed.id, app_type_str)?.is_some() { + continue; + } + + let next_sort_index = self.next_sort_index_for_app(app_type_str)?; + + let settings_config: serde_json::Value = + serde_json::from_str(seed.settings_config_json).map_err(|e| { + AppError::Database(format!("Seed JSON parse failed for {}: {e}", seed.id)) + })?; + + let mut provider = Provider::with_id( + seed.id.to_string(), + seed.name.to_string(), + settings_config, + Some(seed.website_url.to_string()), + ); + provider.category = Some("official".to_string()); + provider.icon = Some(seed.icon.to_string()); + provider.icon_color = Some(seed.icon_color.to_string()); + provider.sort_index = Some(next_sort_index); + provider.created_at = Some(now_ms); + + self.save_provider(app_type_str, &provider)?; + inserted += 1; + log::info!( + "✓ Seeded official provider: {} ({})", + seed.name, + app_type_str + ); + } + + // 即使 inserted=0(例如用户手动创建过同 id)也设置 flag 防止反复检查 + self.set_setting("official_providers_seeded", "true")?; + + Ok(inserted) + } } diff --git a/src-tauri/src/database/dao/providers_seed.rs b/src-tauri/src/database/dao/providers_seed.rs new file mode 100644 index 000000000..055e9d294 --- /dev/null +++ b/src-tauri/src/database/dao/providers_seed.rs @@ -0,0 +1,66 @@ +//! 官方供应商种子数据 +//! +//! 启动时调用 `Database::init_default_official_providers` 把这些条目 +//! 写入 `providers` 表,让所有用户都能看到一个"一键切回官方"的入口。 +//! +//! 字段与前端预设保持一致,参见: +//! - `src/config/claudeProviderPresets.ts`("Claude Official") +//! - `src/config/codexProviderPresets.ts`("OpenAI Official") +//! - `src/config/geminiProviderPresets.ts`("Google Official") + +use crate::app_config::AppType; + +/// 单条官方供应商种子定义。 +pub(crate) struct OfficialProviderSeed { + pub id: &'static str, + pub app_type: AppType, + pub name: &'static str, + pub website_url: &'static str, + pub icon: &'static str, + pub icon_color: &'static str, + /// settings_config 的 JSON 字符串,每个 app 结构不同。 + pub settings_config_json: &'static str, +} + +/// Claude / Codex / Gemini 三个应用的官方预设。 +/// +/// id 固定,便于幂等检查;name 直接用英文原名(与前端预设一致),不做 i18n。 +pub(crate) const OFFICIAL_SEEDS: &[OfficialProviderSeed] = &[ + OfficialProviderSeed { + id: "claude-official", + app_type: AppType::Claude, + name: "Claude Official", + website_url: "https://www.anthropic.com/claude-code", + icon: "anthropic", + icon_color: "#D4915D", + // 空 env 让用户走 Claude CLI 默认认证流程 + settings_config_json: r#"{"env":{}}"#, + }, + OfficialProviderSeed { + id: "codex-official", + app_type: AppType::Codex, + name: "OpenAI Official", + website_url: "https://chatgpt.com/codex", + icon: "openai", + icon_color: "#00A67E", + // 空 auth + 空 config 让用户走 ChatGPT Plus/Pro OAuth + settings_config_json: r#"{"auth":{},"config":""}"#, + }, + OfficialProviderSeed { + id: "gemini-official", + app_type: AppType::Gemini, + name: "Google Official", + website_url: "https://ai.google.dev/", + icon: "gemini", + icon_color: "#4285F4", + // 空 env + 空 config 让用户走 Google OAuth + settings_config_json: r#"{"env":{},"config":{}}"#, + }, +]; + +/// 判断给定的 provider id 是否属于内置官方种子。 +/// +/// 单一事实源:直接扫描 `OFFICIAL_SEEDS`,避免在多处重复维护 id 列表。 +pub(crate) fn is_official_seed_id(id: &str) -> bool { + OFFICIAL_SEEDS.iter().any(|seed| seed.id == id) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 393c88346..67de2bf90 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -467,6 +467,41 @@ pub fn run() { Err(e) => log::warn!("✗ Failed to read skills migration flag: {e}"), } + // 1.5. 自动导入 live 配置 + seed 官方预设供应商(Claude / Codex / Gemini) + // + // 先 import 后 seed 是有意为之:先把用户手动配置的 settings.json / auth.json / .env + // 落成 "default" provider 设为 current,再追加官方预设(is_current=false)。 + // 这样用户切到官方预设时,回填机制会保护原 live 配置不丢失。 + for app_type in + crate::app_config::AppType::all().filter(|t| !t.is_additive_mode()) + { + match crate::services::provider::import_default_config( + &app_state, + app_type.clone(), + ) { + Ok(true) => log::info!( + "✓ Imported live config for {} as default provider", + app_type.as_str() + ), + Ok(false) => log::debug!( + "○ {} already has providers; live import skipped", + app_type.as_str() + ), + Err(e) => log::debug!( + "○ No live config to import for {}: {e}", + app_type.as_str() + ), + } + } + + match app_state.db.init_default_official_providers() { + Ok(count) if count > 0 => { + log::info!("✓ Seeded {count} official provider(s)"); + } + Ok(_) => {} + Err(e) => log::warn!("✗ Failed to seed official providers: {e}"), + } + // 2. OMO 配置导入(当数据库中无 OMO provider 时,从本地文件导入) { let has_omo = app_state diff --git a/src-tauri/src/services/provider/live.rs b/src-tauri/src/services/provider/live.rs index d040f4ea6..f577892a3 100644 --- a/src-tauri/src/services/provider/live.rs +++ b/src-tauri/src/services/provider/live.rs @@ -999,11 +999,12 @@ pub fn import_default_config(state: &AppState, app_type: AppType) -> Result