mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-28 08:44:41 +08:00
feat(omo): integrate Oh My OpenCode profile management (#972)
* feat(omo): integrate Oh My OpenCode profile management into Provider system Adds full-stack OMO support: backend config read/write/import, OMO-specific provider CRUD with exclusive switching, frontend profile editor with agent/category/model configuration, global config management, and i18n support. * feat(omo): add model/variant dropdowns from enabled providers Replace model text inputs with Select dropdowns sourced from enabled OpenCode providers, add thinking-level variant selection, and prevent auto-enabling newly added OMO providers. * fix(omo): use standard provider action styles for OMO switch button * fix(omo): replace hardcoded isZh strings with proper i18n t() calls
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
|
||||
pub mod failover;
|
||||
pub mod mcp;
|
||||
pub mod omo;
|
||||
pub mod prompts;
|
||||
pub mod providers;
|
||||
pub mod proxy;
|
||||
@@ -15,3 +16,4 @@ pub mod universal_providers;
|
||||
// 所有 DAO 方法都通过 Database impl 提供,无需单独导出
|
||||
// 导出 FailoverQueueItem 供外部使用
|
||||
pub use failover::FailoverQueueItem;
|
||||
pub use omo::OmoGlobalConfig;
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
use crate::database::Database;
|
||||
use crate::error::AppError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct OmoGlobalConfig {
|
||||
pub id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub schema_url: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub sisyphus_agent: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
pub disabled_agents: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub disabled_mcps: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub disabled_hooks: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub disabled_skills: Vec<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub lsp: Option<serde_json::Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub experimental: Option<serde_json::Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub background_task: Option<serde_json::Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub browser_automation_engine: Option<serde_json::Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub claude_code: Option<serde_json::Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub other_fields: Option<serde_json::Value>,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
impl Default for OmoGlobalConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: "global".to_string(),
|
||||
schema_url: None,
|
||||
sisyphus_agent: None,
|
||||
disabled_agents: vec![],
|
||||
disabled_mcps: vec![],
|
||||
disabled_hooks: vec![],
|
||||
disabled_skills: vec![],
|
||||
lsp: None,
|
||||
experimental: None,
|
||||
background_task: None,
|
||||
browser_automation_engine: None,
|
||||
claude_code: None,
|
||||
other_fields: None,
|
||||
updated_at: chrono::Utc::now().to_rfc3339(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Database {
|
||||
pub fn get_omo_global_config(&self) -> Result<OmoGlobalConfig, AppError> {
|
||||
let json_str = self.get_setting("common_config_omo")?;
|
||||
match json_str {
|
||||
Some(s) => serde_json::from_str::<OmoGlobalConfig>(&s)
|
||||
.map_err(|e| AppError::Config(format!("Failed to parse common_config_omo: {e}"))),
|
||||
None => Ok(OmoGlobalConfig::default()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save_omo_global_config(&self, config: &OmoGlobalConfig) -> Result<(), AppError> {
|
||||
let json_str = serde_json::to_string(config)
|
||||
.map_err(|e| AppError::Config(format!("JSON serialization failed: {e}")))?;
|
||||
self.set_setting("common_config_omo", &json_str)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,3 @@
|
||||
//! 供应商数据访问对象
|
||||
//!
|
||||
//! 提供供应商(Provider)的 CRUD 操作。
|
||||
|
||||
use crate::database::{lock_conn, Database};
|
||||
use crate::error::AppError;
|
||||
use crate::provider::{Provider, ProviderMeta};
|
||||
@@ -9,8 +5,18 @@ use indexmap::IndexMap;
|
||||
use rusqlite::params;
|
||||
use std::collections::HashMap;
|
||||
|
||||
type OmoProviderRow = (
|
||||
String,
|
||||
String,
|
||||
String,
|
||||
Option<String>,
|
||||
Option<i64>,
|
||||
Option<usize>,
|
||||
Option<String>,
|
||||
String,
|
||||
);
|
||||
|
||||
impl Database {
|
||||
/// 获取指定应用类型的所有供应商
|
||||
pub fn get_all_providers(
|
||||
&self,
|
||||
app_type: &str,
|
||||
@@ -66,7 +72,6 @@ impl Database {
|
||||
let (id, mut provider) = provider_res.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
provider.id = id.clone();
|
||||
|
||||
// 加载 endpoints
|
||||
let mut stmt_endpoints = conn.prepare(
|
||||
"SELECT url, added_at FROM provider_endpoints WHERE provider_id = ?1 AND app_type = ?2 ORDER BY added_at ASC, url ASC"
|
||||
).map_err(|e| AppError::Database(e.to_string()))?;
|
||||
@@ -103,7 +108,6 @@ impl Database {
|
||||
Ok(providers)
|
||||
}
|
||||
|
||||
/// 获取当前激活的供应商 ID
|
||||
pub fn get_current_provider(&self, app_type: &str) -> Result<Option<String>, AppError> {
|
||||
let conn = lock_conn!(self.conn);
|
||||
let mut stmt = conn
|
||||
@@ -123,7 +127,6 @@ impl Database {
|
||||
}
|
||||
}
|
||||
|
||||
/// 根据 ID 获取单个供应商
|
||||
pub fn get_provider_by_id(
|
||||
&self,
|
||||
id: &str,
|
||||
@@ -174,21 +177,15 @@ impl Database {
|
||||
}
|
||||
}
|
||||
|
||||
/// 保存供应商(新增或更新)
|
||||
///
|
||||
/// 注意:更新模式下不同步 endpoints,因为编辑模式下端点通过单独的 API 管理
|
||||
/// (add_custom_endpoint / remove_custom_endpoint),避免覆盖用户的修改。
|
||||
pub fn save_provider(&self, app_type: &str, provider: &Provider) -> Result<(), AppError> {
|
||||
let mut conn = lock_conn!(self.conn);
|
||||
let tx = conn
|
||||
.transaction()
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
|
||||
// 处理 meta:取出 endpoints 以便单独处理
|
||||
let mut meta_clone = provider.meta.clone().unwrap_or_default();
|
||||
let endpoints = std::mem::take(&mut meta_clone.custom_endpoints);
|
||||
|
||||
// 检查是否存在(用于判断新增/更新,以及保留 is_current 和 in_failover_queue)
|
||||
let existing: Option<(bool, bool)> = tx
|
||||
.query_row(
|
||||
"SELECT is_current, in_failover_queue FROM providers WHERE id = ?1 AND app_type = ?2",
|
||||
@@ -202,7 +199,6 @@ impl Database {
|
||||
existing.unwrap_or((false, provider.in_failover_queue));
|
||||
|
||||
if is_update {
|
||||
// 更新模式:使用 UPDATE 避免触发 ON DELETE CASCADE
|
||||
tx.execute(
|
||||
"UPDATE providers SET
|
||||
name = ?1,
|
||||
@@ -241,7 +237,6 @@ impl Database {
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
} else {
|
||||
// 新增模式:使用 INSERT
|
||||
tx.execute(
|
||||
"INSERT INTO providers (
|
||||
id, app_type, name, settings_config, website_url, category,
|
||||
@@ -268,7 +263,6 @@ impl Database {
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
|
||||
// 只有新增时才同步 endpoints
|
||||
for (url, endpoint) in endpoints {
|
||||
tx.execute(
|
||||
"INSERT INTO provider_endpoints (provider_id, app_type, url, added_at)
|
||||
@@ -283,7 +277,6 @@ impl Database {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 删除供应商
|
||||
pub fn delete_provider(&self, app_type: &str, id: &str) -> Result<(), AppError> {
|
||||
let conn = lock_conn!(self.conn);
|
||||
conn.execute(
|
||||
@@ -294,21 +287,18 @@ impl Database {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 设置当前供应商
|
||||
pub fn set_current_provider(&self, app_type: &str, id: &str) -> Result<(), AppError> {
|
||||
let mut conn = lock_conn!(self.conn);
|
||||
let tx = conn
|
||||
.transaction()
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
|
||||
// 重置所有为 0
|
||||
tx.execute(
|
||||
"UPDATE providers SET is_current = 0 WHERE app_type = ?1",
|
||||
params![app_type],
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
|
||||
// 设置新的当前供应商
|
||||
tx.execute(
|
||||
"UPDATE providers SET is_current = 1 WHERE id = ?1 AND app_type = ?2",
|
||||
params![id, app_type],
|
||||
@@ -319,7 +309,6 @@ impl Database {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 更新供应商的 settings_config(仅更新配置,不改变其他字段)
|
||||
pub fn update_provider_settings_config(
|
||||
&self,
|
||||
app_type: &str,
|
||||
@@ -341,7 +330,6 @@ impl Database {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 添加自定义端点
|
||||
pub fn add_custom_endpoint(
|
||||
&self,
|
||||
app_type: &str,
|
||||
@@ -357,7 +345,6 @@ impl Database {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 移除自定义端点
|
||||
pub fn remove_custom_endpoint(
|
||||
&self,
|
||||
app_type: &str,
|
||||
@@ -372,4 +359,126 @@ impl Database {
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_omo_provider_current(
|
||||
&self,
|
||||
app_type: &str,
|
||||
provider_id: &str,
|
||||
) -> Result<(), AppError> {
|
||||
let mut conn = lock_conn!(self.conn);
|
||||
let tx = conn
|
||||
.transaction()
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
tx.execute(
|
||||
"UPDATE providers SET is_current = 0 WHERE app_type = ?1 AND category = 'omo'",
|
||||
params![app_type],
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
let updated = tx
|
||||
.execute(
|
||||
"UPDATE providers SET is_current = 1 WHERE id = ?1 AND app_type = ?2 AND category = 'omo'",
|
||||
params![provider_id, app_type],
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
if updated != 1 {
|
||||
return Err(AppError::Database(format!(
|
||||
"Failed to set OMO provider current: provider '{provider_id}' not found in app '{app_type}'"
|
||||
)));
|
||||
}
|
||||
tx.commit().map_err(|e| AppError::Database(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_omo_provider_current(
|
||||
&self,
|
||||
app_type: &str,
|
||||
provider_id: &str,
|
||||
) -> Result<bool, AppError> {
|
||||
let conn = lock_conn!(self.conn);
|
||||
match conn.query_row(
|
||||
"SELECT is_current FROM providers
|
||||
WHERE id = ?1 AND app_type = ?2 AND category = 'omo'",
|
||||
params![provider_id, app_type],
|
||||
|row| row.get(0),
|
||||
) {
|
||||
Ok(is_current) => Ok(is_current),
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(false),
|
||||
Err(e) => Err(AppError::Database(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_omo_provider_current(
|
||||
&self,
|
||||
app_type: &str,
|
||||
provider_id: &str,
|
||||
) -> Result<(), AppError> {
|
||||
let conn = lock_conn!(self.conn);
|
||||
conn.execute(
|
||||
"UPDATE providers SET is_current = 0
|
||||
WHERE id = ?1 AND app_type = ?2 AND category = 'omo'",
|
||||
params![provider_id, app_type],
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_current_omo_provider(&self, app_type: &str) -> Result<Option<Provider>, AppError> {
|
||||
let conn = lock_conn!(self.conn);
|
||||
let row_data: Result<OmoProviderRow, rusqlite::Error> = conn.query_row(
|
||||
"SELECT id, name, settings_config, category, created_at, sort_index, notes, meta
|
||||
FROM providers
|
||||
WHERE app_type = ?1 AND category = 'omo' AND is_current = 1
|
||||
LIMIT 1",
|
||||
params![app_type],
|
||||
|row| {
|
||||
Ok((
|
||||
row.get(0)?,
|
||||
row.get(1)?,
|
||||
row.get(2)?,
|
||||
row.get(3)?,
|
||||
row.get(4)?,
|
||||
row.get(5)?,
|
||||
row.get(6)?,
|
||||
row.get(7)?,
|
||||
))
|
||||
},
|
||||
);
|
||||
|
||||
let (id, name, settings_config_str, category, created_at, sort_index, notes, meta_str) =
|
||||
match row_data {
|
||||
Ok(v) => v,
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => return Ok(None),
|
||||
Err(e) => return Err(AppError::Database(e.to_string())),
|
||||
};
|
||||
|
||||
let settings_config = serde_json::from_str(&settings_config_str).map_err(|e| {
|
||||
AppError::Database(format!(
|
||||
"Failed to parse OMO provider settings_config (provider_id={id}): {e}"
|
||||
))
|
||||
})?;
|
||||
let meta: crate::provider::ProviderMeta = if meta_str.trim().is_empty() {
|
||||
crate::provider::ProviderMeta::default()
|
||||
} else {
|
||||
serde_json::from_str(&meta_str).map_err(|e| {
|
||||
AppError::Database(format!(
|
||||
"Failed to parse OMO provider meta (provider_id={id}): {e}"
|
||||
))
|
||||
})?
|
||||
};
|
||||
|
||||
Ok(Some(Provider {
|
||||
id,
|
||||
name,
|
||||
settings_config,
|
||||
website_url: None,
|
||||
category,
|
||||
created_at,
|
||||
sort_index,
|
||||
notes,
|
||||
meta: Some(meta),
|
||||
icon: None,
|
||||
icon_color: None,
|
||||
in_failover_queue: false,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ mod tests;
|
||||
|
||||
// DAO 类型导出供外部使用
|
||||
pub use dao::FailoverQueueItem;
|
||||
pub use dao::OmoGlobalConfig;
|
||||
|
||||
use crate::config::get_app_config_dir;
|
||||
use crate::error::AppError;
|
||||
|
||||
Reference in New Issue
Block a user