mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
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.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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<bool, AppError> {
|
||||
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<usize, AppError> {
|
||||
let conn = lock_conn!(self.conn);
|
||||
let max: Option<i64> = 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<usize, AppError> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -999,11 +999,12 @@ pub fn import_default_config(state: &AppState, app_type: AppType) -> Result<bool
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
{
|
||||
let providers = state.db.get_all_providers(app_type.as_str())?;
|
||||
if !providers.is_empty() {
|
||||
return Ok(false); // 已有供应商,跳过
|
||||
}
|
||||
// 允许 "只有官方 seed 预设" 的情况下继续导入 live:
|
||||
// - 启动编排顺序是先 import 后 seed,新用户启动时 providers 为空,导入照常
|
||||
// - 老用户已有非 seed provider,跳过导入(正确)
|
||||
// - 用户手动点 ProviderEmptyState 的导入按钮时,与官方 seed 共存而不被阻塞
|
||||
if state.db.has_non_official_seed_provider(app_type.as_str())? {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let settings_config = match app_type {
|
||||
|
||||
Reference in New Issue
Block a user