mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-25 13:45:03 +08:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a353518e9b | |||
| 6388d24c9a | |||
| a20ff157bf | |||
| da27e22f77 | |||
| 5490a540f1 |
@@ -9,7 +9,6 @@ use crate::codex_config;
|
|||||||
use crate::config::{self, get_claude_settings_path, ConfigStatus};
|
use crate::config::{self, get_claude_settings_path, ConfigStatus};
|
||||||
use crate::settings;
|
use crate::settings;
|
||||||
|
|
||||||
/// 获取 Claude Code 配置状态
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_claude_config_status() -> Result<ConfigStatus, String> {
|
pub async fn get_claude_config_status() -> Result<ConfigStatus, String> {
|
||||||
Ok(config::get_claude_config_status())
|
Ok(config::get_claude_config_status())
|
||||||
@@ -63,13 +62,11 @@ pub async fn get_config_status(app: String) -> Result<ConfigStatus, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取 Claude Code 配置文件路径
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_claude_code_config_path() -> Result<String, String> {
|
pub async fn get_claude_code_config_path() -> Result<String, String> {
|
||||||
Ok(get_claude_settings_path().to_string_lossy().to_string())
|
Ok(get_claude_settings_path().to_string_lossy().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取当前生效的配置目录
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_config_dir(app: String) -> Result<String, String> {
|
pub async fn get_config_dir(app: String) -> Result<String, String> {
|
||||||
let dir = match AppType::from_str(&app).map_err(|e| e.to_string())? {
|
let dir = match AppType::from_str(&app).map_err(|e| e.to_string())? {
|
||||||
@@ -82,7 +79,6 @@ pub async fn get_config_dir(app: String) -> Result<String, String> {
|
|||||||
Ok(dir.to_string_lossy().to_string())
|
Ok(dir.to_string_lossy().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 打开配置文件夹
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn open_config_folder(handle: AppHandle, app: String) -> Result<bool, String> {
|
pub async fn open_config_folder(handle: AppHandle, app: String) -> Result<bool, String> {
|
||||||
let config_dir = match AppType::from_str(&app).map_err(|e| e.to_string())? {
|
let config_dir = match AppType::from_str(&app).map_err(|e| e.to_string())? {
|
||||||
@@ -104,7 +100,6 @@ pub async fn open_config_folder(handle: AppHandle, app: String) -> Result<bool,
|
|||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 弹出系统目录选择器并返回用户选择的路径
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn pick_directory(
|
pub async fn pick_directory(
|
||||||
app: AppHandle,
|
app: AppHandle,
|
||||||
@@ -136,14 +131,12 @@ pub async fn pick_directory(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取应用配置文件路径
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_app_config_path() -> Result<String, String> {
|
pub async fn get_app_config_path() -> Result<String, String> {
|
||||||
let config_path = config::get_app_config_path();
|
let config_path = config::get_app_config_path();
|
||||||
Ok(config_path.to_string_lossy().to_string())
|
Ok(config_path.to_string_lossy().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 打开应用配置文件夹
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn open_app_config_folder(handle: AppHandle) -> Result<bool, String> {
|
pub async fn open_app_config_folder(handle: AppHandle) -> Result<bool, String> {
|
||||||
let config_dir = config::get_app_config_dir();
|
let config_dir = config::get_app_config_dir();
|
||||||
@@ -160,7 +153,6 @@ pub async fn open_app_config_folder(handle: AppHandle) -> Result<bool, String> {
|
|||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取 Claude 通用配置片段(已废弃,使用 get_common_config_snippet)
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_claude_common_config_snippet(
|
pub async fn get_claude_common_config_snippet(
|
||||||
state: tauri::State<'_, crate::store::AppState>,
|
state: tauri::State<'_, crate::store::AppState>,
|
||||||
@@ -171,13 +163,11 @@ pub async fn get_claude_common_config_snippet(
|
|||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 设置 Claude 通用配置片段(已废弃,使用 set_common_config_snippet)
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn set_claude_common_config_snippet(
|
pub async fn set_claude_common_config_snippet(
|
||||||
snippet: String,
|
snippet: String,
|
||||||
state: tauri::State<'_, crate::store::AppState>,
|
state: tauri::State<'_, crate::store::AppState>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
// 验证是否为有效的 JSON(如果不为空)
|
|
||||||
if !snippet.trim().is_empty() {
|
if !snippet.trim().is_empty() {
|
||||||
serde_json::from_str::<serde_json::Value>(&snippet).map_err(invalid_json_format_error)?;
|
serde_json::from_str::<serde_json::Value>(&snippet).map_err(invalid_json_format_error)?;
|
||||||
}
|
}
|
||||||
@@ -195,7 +185,6 @@ pub async fn set_claude_common_config_snippet(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取通用配置片段(统一接口)
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_common_config_snippet(
|
pub async fn get_common_config_snippet(
|
||||||
app_type: String,
|
app_type: String,
|
||||||
@@ -207,25 +196,19 @@ pub async fn get_common_config_snippet(
|
|||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 设置通用配置片段(统一接口)
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn set_common_config_snippet(
|
pub async fn set_common_config_snippet(
|
||||||
app_type: String,
|
app_type: String,
|
||||||
snippet: String,
|
snippet: String,
|
||||||
state: tauri::State<'_, crate::store::AppState>,
|
state: tauri::State<'_, crate::store::AppState>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
// 验证格式(根据应用类型)
|
|
||||||
if !snippet.trim().is_empty() {
|
if !snippet.trim().is_empty() {
|
||||||
match app_type.as_str() {
|
match app_type.as_str() {
|
||||||
"claude" | "gemini" => {
|
"claude" | "gemini" | "omo" => {
|
||||||
// 验证 JSON 格式
|
|
||||||
serde_json::from_str::<serde_json::Value>(&snippet)
|
serde_json::from_str::<serde_json::Value>(&snippet)
|
||||||
.map_err(invalid_json_format_error)?;
|
.map_err(invalid_json_format_error)?;
|
||||||
}
|
}
|
||||||
"codex" => {
|
"codex" => {}
|
||||||
// TOML 格式暂不验证(或可使用 toml crate)
|
|
||||||
// 注意:TOML 验证较为复杂,暂时跳过
|
|
||||||
}
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -240,14 +223,20 @@ pub async fn set_common_config_snippet(
|
|||||||
.db
|
.db
|
||||||
.set_config_snippet(&app_type, value)
|
.set_config_snippet(&app_type, value)
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
if app_type == "omo"
|
||||||
|
&& state
|
||||||
|
.db
|
||||||
|
.get_current_omo_provider("opencode")
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
crate::services::OmoService::write_config_to_file(state.inner())
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 提取通用配置片段
|
|
||||||
///
|
|
||||||
/// 优先从 `settingsConfig`(编辑器当前内容)提取;若未提供,则从当前激活供应商提取。
|
|
||||||
///
|
|
||||||
/// 提取时会自动排除差异化字段(API Key、模型配置、端点等),返回可复用的通用配置片段。
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn extract_common_config_snippet(
|
pub async fn extract_common_config_snippet(
|
||||||
appType: String,
|
appType: String,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ mod global_proxy;
|
|||||||
mod import_export;
|
mod import_export;
|
||||||
mod mcp;
|
mod mcp;
|
||||||
mod misc;
|
mod misc;
|
||||||
|
mod omo;
|
||||||
mod plugin;
|
mod plugin;
|
||||||
mod prompt;
|
mod prompt;
|
||||||
mod provider;
|
mod provider;
|
||||||
@@ -26,6 +27,7 @@ pub use global_proxy::*;
|
|||||||
pub use import_export::*;
|
pub use import_export::*;
|
||||||
pub use mcp::*;
|
pub use mcp::*;
|
||||||
pub use misc::*;
|
pub use misc::*;
|
||||||
|
pub use omo::*;
|
||||||
pub use plugin::*;
|
pub use plugin::*;
|
||||||
pub use prompt::*;
|
pub use prompt::*;
|
||||||
pub use provider::*;
|
pub use provider::*;
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
use tauri::State;
|
||||||
|
|
||||||
|
use crate::services::omo::OmoLocalFileData;
|
||||||
|
use crate::services::OmoService;
|
||||||
|
use crate::store::AppState;
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn read_omo_local_file() -> Result<OmoLocalFileData, String> {
|
||||||
|
OmoService::read_local_file().map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_current_omo_provider_id(state: State<'_, AppState>) -> Result<String, String> {
|
||||||
|
let provider = state
|
||||||
|
.db
|
||||||
|
.get_current_omo_provider("opencode")
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
Ok(provider.map(|p| p.id).unwrap_or_default())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn disable_current_omo(state: State<'_, AppState>) -> Result<(), String> {
|
||||||
|
let providers = state
|
||||||
|
.db
|
||||||
|
.get_all_providers("opencode")
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
for (id, p) in &providers {
|
||||||
|
if p.category.as_deref() == Some("omo") {
|
||||||
|
state
|
||||||
|
.db
|
||||||
|
.clear_omo_provider_current("opencode", id)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OmoService::delete_config_file().map_err(|e| e.to_string())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_omo_provider_count(state: State<'_, AppState>) -> Result<usize, String> {
|
||||||
|
let providers = state
|
||||||
|
.db
|
||||||
|
.get_all_providers("opencode")
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
let count = providers
|
||||||
|
.values()
|
||||||
|
.filter(|p| p.category.as_deref() == Some("omo"))
|
||||||
|
.count();
|
||||||
|
Ok(count)
|
||||||
|
}
|
||||||
@@ -8,7 +8,6 @@ use crate::services::{EndpointLatency, ProviderService, ProviderSortUpdate, Spee
|
|||||||
use crate::store::AppState;
|
use crate::store::AppState;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
/// 获取所有供应商
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_providers(
|
pub fn get_providers(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
@@ -18,14 +17,12 @@ pub fn get_providers(
|
|||||||
ProviderService::list(state.inner(), app_type).map_err(|e| e.to_string())
|
ProviderService::list(state.inner(), app_type).map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取当前供应商ID
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_current_provider(state: State<'_, AppState>, app: String) -> Result<String, String> {
|
pub fn get_current_provider(state: State<'_, AppState>, app: String) -> Result<String, String> {
|
||||||
let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?;
|
let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?;
|
||||||
ProviderService::current(state.inner(), app_type).map_err(|e| e.to_string())
|
ProviderService::current(state.inner(), app_type).map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 添加供应商
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn add_provider(
|
pub fn add_provider(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
@@ -36,7 +33,6 @@ pub fn add_provider(
|
|||||||
ProviderService::add(state.inner(), app_type, provider).map_err(|e| e.to_string())
|
ProviderService::add(state.inner(), app_type, provider).map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 更新供应商
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn update_provider(
|
pub fn update_provider(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
@@ -47,7 +43,6 @@ pub fn update_provider(
|
|||||||
ProviderService::update(state.inner(), app_type, provider).map_err(|e| e.to_string())
|
ProviderService::update(state.inner(), app_type, provider).map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 删除供应商
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn delete_provider(
|
pub fn delete_provider(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
@@ -60,17 +55,18 @@ pub fn delete_provider(
|
|||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove provider from live config only (for additive mode apps like OpenCode)
|
|
||||||
/// Does NOT delete from database - provider remains in the list
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn remove_provider_from_live_config(app: String, id: String) -> Result<bool, String> {
|
pub fn remove_provider_from_live_config(
|
||||||
|
state: tauri::State<'_, AppState>,
|
||||||
|
app: String,
|
||||||
|
id: String,
|
||||||
|
) -> Result<bool, String> {
|
||||||
let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?;
|
let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?;
|
||||||
ProviderService::remove_from_live_config(app_type, &id)
|
ProviderService::remove_from_live_config(state.inner(), app_type, &id)
|
||||||
.map(|_| true)
|
.map(|_| true)
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 切换供应商
|
|
||||||
fn switch_provider_internal(state: &AppState, app_type: AppType, id: &str) -> Result<(), AppError> {
|
fn switch_provider_internal(state: &AppState, app_type: AppType, id: &str) -> Result<(), AppError> {
|
||||||
ProviderService::switch(state, app_type, id)
|
ProviderService::switch(state, app_type, id)
|
||||||
}
|
}
|
||||||
@@ -108,14 +104,12 @@ pub fn import_default_config_test_hook(
|
|||||||
import_default_config_internal(state, app_type)
|
import_default_config_internal(state, app_type)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 导入当前配置为默认供应商
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn import_default_config(state: State<'_, AppState>, app: String) -> Result<bool, String> {
|
pub fn import_default_config(state: State<'_, AppState>, app: String) -> Result<bool, String> {
|
||||||
let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?;
|
let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?;
|
||||||
import_default_config_internal(&state, app_type).map_err(Into::into)
|
import_default_config_internal(&state, app_type).map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 查询供应商用量
|
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn queryProviderUsage(
|
pub async fn queryProviderUsage(
|
||||||
@@ -129,7 +123,6 @@ pub async fn queryProviderUsage(
|
|||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 测试用量脚本(使用当前编辑器中的脚本,不保存)
|
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -162,14 +155,12 @@ pub async fn testUsageScript(
|
|||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 读取当前生效的配置内容
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn read_live_provider_settings(app: String) -> Result<serde_json::Value, String> {
|
pub fn read_live_provider_settings(app: String) -> Result<serde_json::Value, String> {
|
||||||
let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?;
|
let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?;
|
||||||
ProviderService::read_live_settings(app_type).map_err(|e| e.to_string())
|
ProviderService::read_live_settings(app_type).map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 测试第三方/自定义供应商端点的网络延迟
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn test_api_endpoints(
|
pub async fn test_api_endpoints(
|
||||||
urls: Vec<String>,
|
urls: Vec<String>,
|
||||||
@@ -180,7 +171,6 @@ pub async fn test_api_endpoints(
|
|||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取自定义端点列表
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_custom_endpoints(
|
pub fn get_custom_endpoints(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
@@ -192,7 +182,6 @@ pub fn get_custom_endpoints(
|
|||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 添加自定义端点
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn add_custom_endpoint(
|
pub fn add_custom_endpoint(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
@@ -205,7 +194,6 @@ pub fn add_custom_endpoint(
|
|||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 删除自定义端点
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn remove_custom_endpoint(
|
pub fn remove_custom_endpoint(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
@@ -218,7 +206,6 @@ pub fn remove_custom_endpoint(
|
|||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 更新端点最后使用时间
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn update_endpoint_last_used(
|
pub fn update_endpoint_last_used(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
@@ -231,7 +218,6 @@ pub fn update_endpoint_last_used(
|
|||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 更新多个供应商的排序
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn update_providers_sort_order(
|
pub fn update_providers_sort_order(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
@@ -242,24 +228,16 @@ pub fn update_providers_sort_order(
|
|||||||
ProviderService::update_sort_order(state.inner(), app_type, updates).map_err(|e| e.to_string())
|
ProviderService::update_sort_order(state.inner(), app_type, updates).map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 统一供应商(Universal Provider)命令
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
use crate::provider::UniversalProvider;
|
use crate::provider::UniversalProvider;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use tauri::{AppHandle, Emitter};
|
use tauri::{AppHandle, Emitter};
|
||||||
|
|
||||||
/// 统一供应商同步完成事件的 payload
|
|
||||||
#[derive(Clone, serde::Serialize)]
|
#[derive(Clone, serde::Serialize)]
|
||||||
pub struct UniversalProviderSyncedEvent {
|
pub struct UniversalProviderSyncedEvent {
|
||||||
/// 操作类型: "upsert" | "delete" | "sync"
|
|
||||||
pub action: String,
|
pub action: String,
|
||||||
/// 统一供应商 ID
|
|
||||||
pub id: String,
|
pub id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 发送统一供应商同步事件,通知前端刷新供应商列表
|
|
||||||
fn emit_universal_provider_synced(app: &AppHandle, action: &str, id: &str) {
|
fn emit_universal_provider_synced(app: &AppHandle, action: &str, id: &str) {
|
||||||
let _ = app.emit(
|
let _ = app.emit(
|
||||||
"universal-provider-synced",
|
"universal-provider-synced",
|
||||||
@@ -270,7 +248,6 @@ fn emit_universal_provider_synced(app: &AppHandle, action: &str, id: &str) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取所有统一供应商
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_universal_providers(
|
pub fn get_universal_providers(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
@@ -278,7 +255,6 @@ pub fn get_universal_providers(
|
|||||||
ProviderService::list_universal(state.inner()).map_err(|e| e.to_string())
|
ProviderService::list_universal(state.inner()).map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取单个统一供应商
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_universal_provider(
|
pub fn get_universal_provider(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
@@ -287,7 +263,6 @@ pub fn get_universal_provider(
|
|||||||
ProviderService::get_universal(state.inner(), &id).map_err(|e| e.to_string())
|
ProviderService::get_universal(state.inner(), &id).map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 添加或更新统一供应商
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn upsert_universal_provider(
|
pub fn upsert_universal_provider(
|
||||||
app: AppHandle,
|
app: AppHandle,
|
||||||
@@ -298,13 +273,11 @@ pub fn upsert_universal_provider(
|
|||||||
let result =
|
let result =
|
||||||
ProviderService::upsert_universal(state.inner(), provider).map_err(|e| e.to_string())?;
|
ProviderService::upsert_universal(state.inner(), provider).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
// 发送事件通知前端刷新
|
|
||||||
emit_universal_provider_synced(&app, "upsert", &id);
|
emit_universal_provider_synced(&app, "upsert", &id);
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 删除统一供应商
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn delete_universal_provider(
|
pub fn delete_universal_provider(
|
||||||
app: AppHandle,
|
app: AppHandle,
|
||||||
@@ -314,13 +287,11 @@ pub fn delete_universal_provider(
|
|||||||
let result =
|
let result =
|
||||||
ProviderService::delete_universal(state.inner(), &id).map_err(|e| e.to_string())?;
|
ProviderService::delete_universal(state.inner(), &id).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
// 发送事件通知前端刷新
|
|
||||||
emit_universal_provider_synced(&app, "delete", &id);
|
emit_universal_provider_synced(&app, "delete", &id);
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 同步统一供应商到各应用(手动触发)
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn sync_universal_provider(
|
pub fn sync_universal_provider(
|
||||||
app: AppHandle,
|
app: AppHandle,
|
||||||
@@ -330,29 +301,17 @@ pub fn sync_universal_provider(
|
|||||||
let result =
|
let result =
|
||||||
ProviderService::sync_universal_to_apps(state.inner(), &id).map_err(|e| e.to_string())?;
|
ProviderService::sync_universal_to_apps(state.inner(), &id).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
// 发送事件通知前端刷新
|
|
||||||
emit_universal_provider_synced(&app, "sync", &id);
|
emit_universal_provider_synced(&app, "sync", &id);
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// OpenCode 专属命令
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// 从 OpenCode live 配置导入供应商到数据库
|
|
||||||
///
|
|
||||||
/// 这是 OpenCode 特有的功能,因为 OpenCode 使用累加模式,
|
|
||||||
/// 用户可能已经在 opencode.json 中配置了供应商。
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn import_opencode_providers_from_live(state: State<'_, AppState>) -> Result<usize, String> {
|
pub fn import_opencode_providers_from_live(state: State<'_, AppState>) -> Result<usize, String> {
|
||||||
crate::services::provider::import_opencode_providers_from_live(state.inner())
|
crate::services::provider::import_opencode_providers_from_live(state.inner())
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取 OpenCode live 配置中的供应商 ID 列表
|
|
||||||
///
|
|
||||||
/// 用于前端判断供应商是否已添加到 opencode.json
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_opencode_live_provider_ids() -> Result<Vec<String>, String> {
|
pub fn get_opencode_live_provider_ids() -> Result<Vec<String>, String> {
|
||||||
crate::opencode_config::get_providers()
|
crate::opencode_config::get_providers()
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
pub mod failover;
|
pub mod failover;
|
||||||
pub mod mcp;
|
pub mod mcp;
|
||||||
|
pub mod omo;
|
||||||
pub mod prompts;
|
pub mod prompts;
|
||||||
pub mod providers;
|
pub mod providers;
|
||||||
pub mod proxy;
|
pub mod proxy;
|
||||||
@@ -15,3 +16,4 @@ pub mod universal_providers;
|
|||||||
// 所有 DAO 方法都通过 Database impl 提供,无需单独导出
|
// 所有 DAO 方法都通过 Database impl 提供,无需单独导出
|
||||||
// 导出 FailoverQueueItem 供外部使用
|
// 导出 FailoverQueueItem 供外部使用
|
||||||
pub use failover::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::database::{lock_conn, Database};
|
||||||
use crate::error::AppError;
|
use crate::error::AppError;
|
||||||
use crate::provider::{Provider, ProviderMeta};
|
use crate::provider::{Provider, ProviderMeta};
|
||||||
@@ -9,8 +5,18 @@ use indexmap::IndexMap;
|
|||||||
use rusqlite::params;
|
use rusqlite::params;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
type OmoProviderRow = (
|
||||||
|
String,
|
||||||
|
String,
|
||||||
|
String,
|
||||||
|
Option<String>,
|
||||||
|
Option<i64>,
|
||||||
|
Option<usize>,
|
||||||
|
Option<String>,
|
||||||
|
String,
|
||||||
|
);
|
||||||
|
|
||||||
impl Database {
|
impl Database {
|
||||||
/// 获取指定应用类型的所有供应商
|
|
||||||
pub fn get_all_providers(
|
pub fn get_all_providers(
|
||||||
&self,
|
&self,
|
||||||
app_type: &str,
|
app_type: &str,
|
||||||
@@ -66,7 +72,6 @@ impl Database {
|
|||||||
let (id, mut provider) = provider_res.map_err(|e| AppError::Database(e.to_string()))?;
|
let (id, mut provider) = provider_res.map_err(|e| AppError::Database(e.to_string()))?;
|
||||||
provider.id = id.clone();
|
provider.id = id.clone();
|
||||||
|
|
||||||
// 加载 endpoints
|
|
||||||
let mut stmt_endpoints = conn.prepare(
|
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"
|
"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()))?;
|
).map_err(|e| AppError::Database(e.to_string()))?;
|
||||||
@@ -103,7 +108,6 @@ impl Database {
|
|||||||
Ok(providers)
|
Ok(providers)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取当前激活的供应商 ID
|
|
||||||
pub fn get_current_provider(&self, app_type: &str) -> Result<Option<String>, AppError> {
|
pub fn get_current_provider(&self, app_type: &str) -> Result<Option<String>, AppError> {
|
||||||
let conn = lock_conn!(self.conn);
|
let conn = lock_conn!(self.conn);
|
||||||
let mut stmt = conn
|
let mut stmt = conn
|
||||||
@@ -123,7 +127,6 @@ impl Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 根据 ID 获取单个供应商
|
|
||||||
pub fn get_provider_by_id(
|
pub fn get_provider_by_id(
|
||||||
&self,
|
&self,
|
||||||
id: &str,
|
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> {
|
pub fn save_provider(&self, app_type: &str, provider: &Provider) -> Result<(), AppError> {
|
||||||
let mut conn = lock_conn!(self.conn);
|
let mut conn = lock_conn!(self.conn);
|
||||||
let tx = conn
|
let tx = conn
|
||||||
.transaction()
|
.transaction()
|
||||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||||
|
|
||||||
// 处理 meta:取出 endpoints 以便单独处理
|
|
||||||
let mut meta_clone = provider.meta.clone().unwrap_or_default();
|
let mut meta_clone = provider.meta.clone().unwrap_or_default();
|
||||||
let endpoints = std::mem::take(&mut meta_clone.custom_endpoints);
|
let endpoints = std::mem::take(&mut meta_clone.custom_endpoints);
|
||||||
|
|
||||||
// 检查是否存在(用于判断新增/更新,以及保留 is_current 和 in_failover_queue)
|
|
||||||
let existing: Option<(bool, bool)> = tx
|
let existing: Option<(bool, bool)> = tx
|
||||||
.query_row(
|
.query_row(
|
||||||
"SELECT is_current, in_failover_queue FROM providers WHERE id = ?1 AND app_type = ?2",
|
"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));
|
existing.unwrap_or((false, provider.in_failover_queue));
|
||||||
|
|
||||||
if is_update {
|
if is_update {
|
||||||
// 更新模式:使用 UPDATE 避免触发 ON DELETE CASCADE
|
|
||||||
tx.execute(
|
tx.execute(
|
||||||
"UPDATE providers SET
|
"UPDATE providers SET
|
||||||
name = ?1,
|
name = ?1,
|
||||||
@@ -241,7 +237,6 @@ impl Database {
|
|||||||
)
|
)
|
||||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||||
} else {
|
} else {
|
||||||
// 新增模式:使用 INSERT
|
|
||||||
tx.execute(
|
tx.execute(
|
||||||
"INSERT INTO providers (
|
"INSERT INTO providers (
|
||||||
id, app_type, name, settings_config, website_url, category,
|
id, app_type, name, settings_config, website_url, category,
|
||||||
@@ -268,7 +263,6 @@ impl Database {
|
|||||||
)
|
)
|
||||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||||
|
|
||||||
// 只有新增时才同步 endpoints
|
|
||||||
for (url, endpoint) in endpoints {
|
for (url, endpoint) in endpoints {
|
||||||
tx.execute(
|
tx.execute(
|
||||||
"INSERT INTO provider_endpoints (provider_id, app_type, url, added_at)
|
"INSERT INTO provider_endpoints (provider_id, app_type, url, added_at)
|
||||||
@@ -283,7 +277,6 @@ impl Database {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 删除供应商
|
|
||||||
pub fn delete_provider(&self, app_type: &str, id: &str) -> Result<(), AppError> {
|
pub fn delete_provider(&self, app_type: &str, id: &str) -> Result<(), AppError> {
|
||||||
let conn = lock_conn!(self.conn);
|
let conn = lock_conn!(self.conn);
|
||||||
conn.execute(
|
conn.execute(
|
||||||
@@ -294,21 +287,18 @@ impl Database {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 设置当前供应商
|
|
||||||
pub fn set_current_provider(&self, app_type: &str, id: &str) -> Result<(), AppError> {
|
pub fn set_current_provider(&self, app_type: &str, id: &str) -> Result<(), AppError> {
|
||||||
let mut conn = lock_conn!(self.conn);
|
let mut conn = lock_conn!(self.conn);
|
||||||
let tx = conn
|
let tx = conn
|
||||||
.transaction()
|
.transaction()
|
||||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||||
|
|
||||||
// 重置所有为 0
|
|
||||||
tx.execute(
|
tx.execute(
|
||||||
"UPDATE providers SET is_current = 0 WHERE app_type = ?1",
|
"UPDATE providers SET is_current = 0 WHERE app_type = ?1",
|
||||||
params![app_type],
|
params![app_type],
|
||||||
)
|
)
|
||||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||||
|
|
||||||
// 设置新的当前供应商
|
|
||||||
tx.execute(
|
tx.execute(
|
||||||
"UPDATE providers SET is_current = 1 WHERE id = ?1 AND app_type = ?2",
|
"UPDATE providers SET is_current = 1 WHERE id = ?1 AND app_type = ?2",
|
||||||
params![id, app_type],
|
params![id, app_type],
|
||||||
@@ -319,7 +309,6 @@ impl Database {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 更新供应商的 settings_config(仅更新配置,不改变其他字段)
|
|
||||||
pub fn update_provider_settings_config(
|
pub fn update_provider_settings_config(
|
||||||
&self,
|
&self,
|
||||||
app_type: &str,
|
app_type: &str,
|
||||||
@@ -341,7 +330,6 @@ impl Database {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 添加自定义端点
|
|
||||||
pub fn add_custom_endpoint(
|
pub fn add_custom_endpoint(
|
||||||
&self,
|
&self,
|
||||||
app_type: &str,
|
app_type: &str,
|
||||||
@@ -357,7 +345,6 @@ impl Database {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 移除自定义端点
|
|
||||||
pub fn remove_custom_endpoint(
|
pub fn remove_custom_endpoint(
|
||||||
&self,
|
&self,
|
||||||
app_type: &str,
|
app_type: &str,
|
||||||
@@ -372,4 +359,126 @@ impl Database {
|
|||||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||||
Ok(())
|
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 类型导出供外部使用
|
// DAO 类型导出供外部使用
|
||||||
pub use dao::FailoverQueueItem;
|
pub use dao::FailoverQueueItem;
|
||||||
|
pub use dao::OmoGlobalConfig;
|
||||||
|
|
||||||
use crate::config::get_app_config_dir;
|
use crate::config::get_app_config_dir;
|
||||||
use crate::error::AppError;
|
use crate::error::AppError;
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ pub enum AppError {
|
|||||||
},
|
},
|
||||||
#[error("数据库错误: {0}")]
|
#[error("数据库错误: {0}")]
|
||||||
Database(String),
|
Database(String),
|
||||||
|
#[error("OMO 配置文件不存在")]
|
||||||
|
OmoConfigNotFound,
|
||||||
#[error("所有供应商已熔断,无可用渠道")]
|
#[error("所有供应商已熔断,无可用渠道")]
|
||||||
AllProvidersCircuitOpen,
|
AllProvidersCircuitOpen,
|
||||||
#[error("未配置供应商")]
|
#[error("未配置供应商")]
|
||||||
|
|||||||
@@ -503,6 +503,28 @@ pub fn run() {
|
|||||||
Err(e) => log::debug!("○ Failed to import OpenCode providers: {e}"),
|
Err(e) => log::debug!("○ Failed to import OpenCode providers: {e}"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2.2 OMO 配置导入(当数据库中无 OMO provider 时,从本地文件导入)
|
||||||
|
{
|
||||||
|
let has_omo = app_state
|
||||||
|
.db
|
||||||
|
.get_all_providers("opencode")
|
||||||
|
.map(|providers| providers.values().any(|p| p.category.as_deref() == Some("omo")))
|
||||||
|
.unwrap_or(false);
|
||||||
|
if !has_omo {
|
||||||
|
match crate::services::OmoService::import_from_local(&app_state) {
|
||||||
|
Ok(provider) => {
|
||||||
|
log::info!("✓ Imported OMO config from local as provider '{}'", provider.name);
|
||||||
|
}
|
||||||
|
Err(AppError::OmoConfigNotFound) => {
|
||||||
|
log::debug!("○ No OMO config to import");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("✗ Failed to import OMO config from local: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 3. 导入 MCP 服务器配置(表空时触发)
|
// 3. 导入 MCP 服务器配置(表空时触发)
|
||||||
if app_state.db.is_mcp_table_empty().unwrap_or(false) {
|
if app_state.db.is_mcp_table_empty().unwrap_or(false) {
|
||||||
log::info!("MCP table empty, importing from live configurations...");
|
log::info!("MCP table empty, importing from live configurations...");
|
||||||
@@ -958,6 +980,10 @@ pub fn run() {
|
|||||||
commands::scan_local_proxies,
|
commands::scan_local_proxies,
|
||||||
// Window theme control
|
// Window theme control
|
||||||
commands::set_window_theme,
|
commands::set_window_theme,
|
||||||
|
commands::read_omo_local_file,
|
||||||
|
commands::get_current_omo_provider_id,
|
||||||
|
commands::get_omo_provider_count,
|
||||||
|
commands::disable_current_omo,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let app = builder
|
let app = builder
|
||||||
|
|||||||
@@ -1,26 +1,3 @@
|
|||||||
//! OpenCode 配置文件读写模块
|
|
||||||
//!
|
|
||||||
//! 处理 `~/.config/opencode/opencode.json` 配置文件的读写操作。
|
|
||||||
//! OpenCode 使用累加式供应商管理,所有供应商配置共存于同一配置文件中。
|
|
||||||
//!
|
|
||||||
//! ## 配置文件格式
|
|
||||||
//!
|
|
||||||
//! ```json
|
|
||||||
//! {
|
|
||||||
//! "$schema": "https://opencode.ai/config.json",
|
|
||||||
//! "provider": {
|
|
||||||
//! "my-provider": {
|
|
||||||
//! "npm": "@ai-sdk/openai-compatible",
|
|
||||||
//! "options": { "baseURL": "...", "apiKey": "{env:API_KEY}" },
|
|
||||||
//! "models": { "gpt-4o": { "name": "GPT-4o" } }
|
|
||||||
//! }
|
|
||||||
//! },
|
|
||||||
//! "mcp": {
|
|
||||||
//! "my-server": { "type": "local", "command": ["..."] }
|
|
||||||
//! }
|
|
||||||
//! }
|
|
||||||
//! ```
|
|
||||||
|
|
||||||
use crate::config::write_json_file;
|
use crate::config::write_json_file;
|
||||||
use crate::error::AppError;
|
use crate::error::AppError;
|
||||||
use crate::provider::OpenCodeProviderConfig;
|
use crate::provider::OpenCodeProviderConfig;
|
||||||
@@ -29,52 +6,29 @@ use indexmap::IndexMap;
|
|||||||
use serde_json::{json, Map, Value};
|
use serde_json::{json, Map, Value};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Path Functions
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// 获取 OpenCode 配置目录
|
|
||||||
///
|
|
||||||
/// 默认路径: `~/.config/opencode/`
|
|
||||||
/// 可通过 settings.opencode_config_dir 覆盖
|
|
||||||
pub fn get_opencode_dir() -> PathBuf {
|
pub fn get_opencode_dir() -> PathBuf {
|
||||||
if let Some(override_dir) = get_opencode_override_dir() {
|
if let Some(override_dir) = get_opencode_override_dir() {
|
||||||
return override_dir;
|
return override_dir;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 所有平台统一使用 ~/.config/opencode
|
|
||||||
dirs::home_dir()
|
dirs::home_dir()
|
||||||
.map(|h| h.join(".config").join("opencode"))
|
.map(|h| h.join(".config").join("opencode"))
|
||||||
.unwrap_or_else(|| PathBuf::from(".config").join("opencode"))
|
.unwrap_or_else(|| PathBuf::from(".config").join("opencode"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取 OpenCode 配置文件路径
|
|
||||||
///
|
|
||||||
/// 返回 `~/.config/opencode/opencode.json`
|
|
||||||
pub fn get_opencode_config_path() -> PathBuf {
|
pub fn get_opencode_config_path() -> PathBuf {
|
||||||
get_opencode_dir().join("opencode.json")
|
get_opencode_dir().join("opencode.json")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取 OpenCode 环境变量文件路径(如果存在)
|
|
||||||
///
|
|
||||||
/// 返回 `~/.config/opencode/.env`
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn get_opencode_env_path() -> PathBuf {
|
pub fn get_opencode_env_path() -> PathBuf {
|
||||||
get_opencode_dir().join(".env")
|
get_opencode_dir().join(".env")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Core Read/Write Functions
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// 读取 OpenCode 配置文件
|
|
||||||
///
|
|
||||||
/// 返回完整的配置 JSON 对象
|
|
||||||
pub fn read_opencode_config() -> Result<Value, AppError> {
|
pub fn read_opencode_config() -> Result<Value, AppError> {
|
||||||
let path = get_opencode_config_path();
|
let path = get_opencode_config_path();
|
||||||
|
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
// Return empty config with schema
|
|
||||||
return Ok(json!({
|
return Ok(json!({
|
||||||
"$schema": "https://opencode.ai/config.json"
|
"$schema": "https://opencode.ai/config.json"
|
||||||
}));
|
}));
|
||||||
@@ -84,23 +38,14 @@ pub fn read_opencode_config() -> Result<Value, AppError> {
|
|||||||
serde_json::from_str(&content).map_err(|e| AppError::json(&path, e))
|
serde_json::from_str(&content).map_err(|e| AppError::json(&path, e))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 写入 OpenCode 配置文件(原子写入)
|
|
||||||
///
|
|
||||||
/// 使用临时文件 + 重命名确保原子性
|
|
||||||
pub fn write_opencode_config(config: &Value) -> Result<(), AppError> {
|
pub fn write_opencode_config(config: &Value) -> Result<(), AppError> {
|
||||||
let path = get_opencode_config_path();
|
let path = get_opencode_config_path();
|
||||||
// 复用统一的原子写入逻辑(兼容 Windows 上目标文件已存在的情况)
|
|
||||||
write_json_file(&path, config)?;
|
write_json_file(&path, config)?;
|
||||||
|
|
||||||
log::debug!("OpenCode config written to {path:?}");
|
log::debug!("OpenCode config written to {path:?}");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Provider Functions (Untyped - for raw JSON operations)
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// 获取所有供应商配置(原始 JSON)
|
|
||||||
pub fn get_providers() -> Result<Map<String, Value>, AppError> {
|
pub fn get_providers() -> Result<Map<String, Value>, AppError> {
|
||||||
let config = read_opencode_config()?;
|
let config = read_opencode_config()?;
|
||||||
Ok(config
|
Ok(config
|
||||||
@@ -110,7 +55,6 @@ pub fn get_providers() -> Result<Map<String, Value>, AppError> {
|
|||||||
.unwrap_or_default())
|
.unwrap_or_default())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 设置供应商配置(原始 JSON)
|
|
||||||
pub fn set_provider(id: &str, config: Value) -> Result<(), AppError> {
|
pub fn set_provider(id: &str, config: Value) -> Result<(), AppError> {
|
||||||
let mut full_config = read_opencode_config()?;
|
let mut full_config = read_opencode_config()?;
|
||||||
|
|
||||||
@@ -128,7 +72,6 @@ pub fn set_provider(id: &str, config: Value) -> Result<(), AppError> {
|
|||||||
write_opencode_config(&full_config)
|
write_opencode_config(&full_config)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 删除供应商配置
|
|
||||||
pub fn remove_provider(id: &str) -> Result<(), AppError> {
|
pub fn remove_provider(id: &str) -> Result<(), AppError> {
|
||||||
let mut config = read_opencode_config()?;
|
let mut config = read_opencode_config()?;
|
||||||
|
|
||||||
@@ -139,11 +82,6 @@ pub fn remove_provider(id: &str) -> Result<(), AppError> {
|
|||||||
write_opencode_config(&config)
|
write_opencode_config(&config)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Provider Functions (Typed - using OpenCodeProviderConfig)
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// 获取所有供应商配置(类型化)
|
|
||||||
pub fn get_typed_providers() -> Result<IndexMap<String, OpenCodeProviderConfig>, AppError> {
|
pub fn get_typed_providers() -> Result<IndexMap<String, OpenCodeProviderConfig>, AppError> {
|
||||||
let providers = get_providers()?;
|
let providers = get_providers()?;
|
||||||
let mut result = IndexMap::new();
|
let mut result = IndexMap::new();
|
||||||
@@ -155,7 +93,6 @@ pub fn get_typed_providers() -> Result<IndexMap<String, OpenCodeProviderConfig>,
|
|||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::warn!("Failed to parse provider '{id}': {e}");
|
log::warn!("Failed to parse provider '{id}': {e}");
|
||||||
// Skip invalid providers but continue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -163,17 +100,11 @@ pub fn get_typed_providers() -> Result<IndexMap<String, OpenCodeProviderConfig>,
|
|||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 设置供应商配置(类型化)
|
|
||||||
pub fn set_typed_provider(id: &str, config: &OpenCodeProviderConfig) -> Result<(), AppError> {
|
pub fn set_typed_provider(id: &str, config: &OpenCodeProviderConfig) -> Result<(), AppError> {
|
||||||
let value = serde_json::to_value(config).map_err(|e| AppError::JsonSerialize { source: e })?;
|
let value = serde_json::to_value(config).map_err(|e| AppError::JsonSerialize { source: e })?;
|
||||||
set_provider(id, value)
|
set_provider(id, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// MCP Functions
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// 获取所有 MCP 服务器配置
|
|
||||||
pub fn get_mcp_servers() -> Result<Map<String, Value>, AppError> {
|
pub fn get_mcp_servers() -> Result<Map<String, Value>, AppError> {
|
||||||
let config = read_opencode_config()?;
|
let config = read_opencode_config()?;
|
||||||
Ok(config
|
Ok(config
|
||||||
@@ -183,7 +114,6 @@ pub fn get_mcp_servers() -> Result<Map<String, Value>, AppError> {
|
|||||||
.unwrap_or_default())
|
.unwrap_or_default())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 设置 MCP 服务器配置
|
|
||||||
pub fn set_mcp_server(id: &str, config: Value) -> Result<(), AppError> {
|
pub fn set_mcp_server(id: &str, config: Value) -> Result<(), AppError> {
|
||||||
let mut full_config = read_opencode_config()?;
|
let mut full_config = read_opencode_config()?;
|
||||||
|
|
||||||
@@ -198,7 +128,6 @@ pub fn set_mcp_server(id: &str, config: Value) -> Result<(), AppError> {
|
|||||||
write_opencode_config(&full_config)
|
write_opencode_config(&full_config)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 删除 MCP 服务器配置
|
|
||||||
pub fn remove_mcp_server(id: &str) -> Result<(), AppError> {
|
pub fn remove_mcp_server(id: &str) -> Result<(), AppError> {
|
||||||
let mut config = read_opencode_config()?;
|
let mut config = read_opencode_config()?;
|
||||||
|
|
||||||
@@ -208,3 +137,57 @@ pub fn remove_mcp_server(id: &str) -> Result<(), AppError> {
|
|||||||
|
|
||||||
write_opencode_config(&config)
|
write_opencode_config(&config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn add_plugin(plugin_name: &str) -> Result<(), AppError> {
|
||||||
|
let mut config = read_opencode_config()?;
|
||||||
|
|
||||||
|
let plugins = config.get_mut("plugin").and_then(|v| v.as_array_mut());
|
||||||
|
|
||||||
|
match plugins {
|
||||||
|
Some(arr) => {
|
||||||
|
if plugin_name.starts_with("oh-my-opencode")
|
||||||
|
&& !plugin_name.starts_with("oh-my-opencode-slim")
|
||||||
|
{
|
||||||
|
arr.retain(|v| {
|
||||||
|
v.as_str()
|
||||||
|
.map(|s| !s.starts_with("oh-my-opencode-slim"))
|
||||||
|
.unwrap_or(true)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let already_exists = arr.iter().any(|v| v.as_str() == Some(plugin_name));
|
||||||
|
if !already_exists {
|
||||||
|
arr.push(Value::String(plugin_name.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
config["plugin"] = json!([plugin_name]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
write_opencode_config(&config)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_plugin_by_prefix(prefix: &str) -> Result<(), AppError> {
|
||||||
|
let mut config = read_opencode_config()?;
|
||||||
|
|
||||||
|
if let Some(arr) = config.get_mut("plugin").and_then(|v| v.as_array_mut()) {
|
||||||
|
arr.retain(|v| {
|
||||||
|
v.as_str()
|
||||||
|
.map(|s| {
|
||||||
|
if !s.starts_with(prefix) {
|
||||||
|
return true; // Keep: doesn't match prefix at all
|
||||||
|
}
|
||||||
|
let rest = &s[prefix.len()..];
|
||||||
|
rest.starts_with('-')
|
||||||
|
})
|
||||||
|
.unwrap_or(true)
|
||||||
|
});
|
||||||
|
|
||||||
|
if arr.is_empty() {
|
||||||
|
config.as_object_mut().map(|obj| obj.remove("plugin"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
write_opencode_config(&config)
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ pub mod config;
|
|||||||
pub mod env_checker;
|
pub mod env_checker;
|
||||||
pub mod env_manager;
|
pub mod env_manager;
|
||||||
pub mod mcp;
|
pub mod mcp;
|
||||||
|
pub mod omo;
|
||||||
pub mod prompt;
|
pub mod prompt;
|
||||||
pub mod provider;
|
pub mod provider;
|
||||||
pub mod proxy;
|
pub mod proxy;
|
||||||
@@ -12,6 +13,7 @@ pub mod usage_stats;
|
|||||||
|
|
||||||
pub use config::ConfigService;
|
pub use config::ConfigService;
|
||||||
pub use mcp::McpService;
|
pub use mcp::McpService;
|
||||||
|
pub use omo::OmoService;
|
||||||
pub use prompt::PromptService;
|
pub use prompt::PromptService;
|
||||||
pub use provider::{ProviderService, ProviderSortUpdate};
|
pub use provider::{ProviderService, ProviderSortUpdate};
|
||||||
pub use proxy::ProxyService;
|
pub use proxy::ProxyService;
|
||||||
|
|||||||
@@ -0,0 +1,437 @@
|
|||||||
|
use crate::config::write_json_file;
|
||||||
|
use crate::database::OmoGlobalConfig;
|
||||||
|
use crate::error::AppError;
|
||||||
|
use crate::opencode_config::get_opencode_dir;
|
||||||
|
use crate::store::AppState;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::{Map, Value};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct OmoLocalFileData {
|
||||||
|
pub agents: Option<Value>,
|
||||||
|
pub categories: Option<Value>,
|
||||||
|
pub other_fields: Option<Value>,
|
||||||
|
pub global: OmoGlobalConfig,
|
||||||
|
pub file_path: String,
|
||||||
|
pub last_modified: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
type OmoProfileData = (Option<Value>, Option<Value>, Option<Value>, bool);
|
||||||
|
|
||||||
|
pub struct OmoService;
|
||||||
|
|
||||||
|
impl OmoService {
|
||||||
|
fn config_path() -> PathBuf {
|
||||||
|
get_opencode_dir().join("oh-my-opencode.jsonc")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_local_config_path() -> Result<PathBuf, AppError> {
|
||||||
|
let config_path = Self::config_path();
|
||||||
|
if config_path.exists() {
|
||||||
|
return Ok(config_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
let json_path = config_path.with_extension("json");
|
||||||
|
if json_path.exists() {
|
||||||
|
return Ok(json_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(AppError::OmoConfigNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_jsonc_object(path: &Path) -> Result<Map<String, Value>, AppError> {
|
||||||
|
let content = std::fs::read_to_string(path).map_err(|e| AppError::io(path, e))?;
|
||||||
|
let cleaned = Self::strip_jsonc_comments(&content);
|
||||||
|
let parsed: Value = serde_json::from_str(&cleaned)
|
||||||
|
.map_err(|e| AppError::Config(format!("Failed to parse oh-my-opencode config: {e}")))?;
|
||||||
|
parsed
|
||||||
|
.as_object()
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| AppError::Config("Expected JSON object".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_other_fields(obj: &Map<String, Value>) -> Map<String, Value> {
|
||||||
|
const KNOWN_KEYS: [&str; 13] = [
|
||||||
|
"$schema",
|
||||||
|
"agents",
|
||||||
|
"categories",
|
||||||
|
"sisyphus_agent",
|
||||||
|
"disabled_agents",
|
||||||
|
"disabled_mcps",
|
||||||
|
"disabled_hooks",
|
||||||
|
"disabled_skills",
|
||||||
|
"lsp",
|
||||||
|
"experimental",
|
||||||
|
"background_task",
|
||||||
|
"browser_automation_engine",
|
||||||
|
"claude_code",
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut other = Map::new();
|
||||||
|
for (k, v) in obj {
|
||||||
|
if !KNOWN_KEYS.contains(&k.as_str()) {
|
||||||
|
other.insert(k.clone(), v.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
other
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_string_array(val: &Value) -> Vec<String> {
|
||||||
|
val.as_array()
|
||||||
|
.map(|arr| {
|
||||||
|
arr.iter()
|
||||||
|
.filter_map(|v| v.as_str().map(String::from))
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn merge_global_from_obj(obj: &Map<String, Value>, global: &mut OmoGlobalConfig) {
|
||||||
|
if let Some(v) = obj.get("$schema") {
|
||||||
|
global.schema_url = v.as_str().map(|s| s.to_string());
|
||||||
|
}
|
||||||
|
for (key, target) in [
|
||||||
|
("disabled_agents", &mut global.disabled_agents),
|
||||||
|
("disabled_mcps", &mut global.disabled_mcps),
|
||||||
|
("disabled_hooks", &mut global.disabled_hooks),
|
||||||
|
("disabled_skills", &mut global.disabled_skills),
|
||||||
|
] {
|
||||||
|
if let Some(v) = obj.get(key) {
|
||||||
|
*target = Self::extract_string_array(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (key, target) in [
|
||||||
|
("sisyphus_agent", &mut global.sisyphus_agent),
|
||||||
|
("lsp", &mut global.lsp),
|
||||||
|
("experimental", &mut global.experimental),
|
||||||
|
("background_task", &mut global.background_task),
|
||||||
|
(
|
||||||
|
"browser_automation_engine",
|
||||||
|
&mut global.browser_automation_engine,
|
||||||
|
),
|
||||||
|
("claude_code", &mut global.claude_code),
|
||||||
|
] {
|
||||||
|
if let Some(v) = obj.get(key) {
|
||||||
|
*target = Some(v.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_opt_value(result: &mut Map<String, Value>, key: &str, value: &Option<Value>) {
|
||||||
|
if let Some(v) = value {
|
||||||
|
result.insert(key.to_string(), v.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_string_array(result: &mut Map<String, Value>, key: &str, values: &[String]) {
|
||||||
|
if !values.is_empty() {
|
||||||
|
result.insert(
|
||||||
|
key.to_string(),
|
||||||
|
serde_json::to_value(values).unwrap_or(Value::Array(vec![])),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_object_entries(result: &mut Map<String, Value>, value: Option<&Value>) {
|
||||||
|
if let Some(Value::Object(map)) = value {
|
||||||
|
for (k, v) in map {
|
||||||
|
result.insert(k.clone(), v.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_config_file() -> Result<(), AppError> {
|
||||||
|
let config_path = Self::config_path();
|
||||||
|
if config_path.exists() {
|
||||||
|
std::fs::remove_file(&config_path).map_err(|e| AppError::io(&config_path, e))?;
|
||||||
|
log::info!("OMO config file deleted: {config_path:?}");
|
||||||
|
}
|
||||||
|
crate::opencode_config::remove_plugin_by_prefix("oh-my-opencode")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_config_to_file(state: &AppState) -> Result<(), AppError> {
|
||||||
|
let global = state.db.get_omo_global_config()?;
|
||||||
|
let current_omo = state.db.get_current_omo_provider("opencode")?;
|
||||||
|
|
||||||
|
let profile_data = current_omo.as_ref().map(|p| {
|
||||||
|
let agents = p.settings_config.get("agents").cloned();
|
||||||
|
let categories = p.settings_config.get("categories").cloned();
|
||||||
|
let other_fields = p.settings_config.get("otherFields").cloned();
|
||||||
|
let use_common_config = p
|
||||||
|
.settings_config
|
||||||
|
.get("useCommonConfig")
|
||||||
|
.and_then(|v| v.as_bool())
|
||||||
|
.unwrap_or(true);
|
||||||
|
(agents, categories, other_fields, use_common_config)
|
||||||
|
});
|
||||||
|
|
||||||
|
let merged = Self::merge_config(&global, profile_data.as_ref());
|
||||||
|
let config_path = Self::config_path();
|
||||||
|
|
||||||
|
if let Some(parent) = config_path.parent() {
|
||||||
|
std::fs::create_dir_all(parent).map_err(|e| AppError::io(parent, e))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
write_json_file(&config_path, &merged)?;
|
||||||
|
|
||||||
|
crate::opencode_config::add_plugin("oh-my-opencode@latest")?;
|
||||||
|
|
||||||
|
log::info!("OMO config written to {config_path:?}");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn merge_config(global: &OmoGlobalConfig, profile_data: Option<&OmoProfileData>) -> Value {
|
||||||
|
let mut result = Map::new();
|
||||||
|
let use_common_config = profile_data.map(|(_, _, _, v)| *v).unwrap_or(true);
|
||||||
|
|
||||||
|
if use_common_config {
|
||||||
|
if let Some(url) = &global.schema_url {
|
||||||
|
result.insert("$schema".to_string(), Value::String(url.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Self::insert_opt_value(&mut result, "sisyphus_agent", &global.sisyphus_agent);
|
||||||
|
Self::insert_string_array(&mut result, "disabled_agents", &global.disabled_agents);
|
||||||
|
Self::insert_string_array(&mut result, "disabled_mcps", &global.disabled_mcps);
|
||||||
|
Self::insert_string_array(&mut result, "disabled_hooks", &global.disabled_hooks);
|
||||||
|
Self::insert_string_array(&mut result, "disabled_skills", &global.disabled_skills);
|
||||||
|
Self::insert_opt_value(&mut result, "lsp", &global.lsp);
|
||||||
|
Self::insert_opt_value(&mut result, "experimental", &global.experimental);
|
||||||
|
Self::insert_opt_value(&mut result, "background_task", &global.background_task);
|
||||||
|
Self::insert_opt_value(
|
||||||
|
&mut result,
|
||||||
|
"browser_automation_engine",
|
||||||
|
&global.browser_automation_engine,
|
||||||
|
);
|
||||||
|
Self::insert_opt_value(&mut result, "claude_code", &global.claude_code);
|
||||||
|
|
||||||
|
Self::insert_object_entries(&mut result, global.other_fields.as_ref());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((agents, categories, other_fields, _)) = profile_data {
|
||||||
|
Self::insert_opt_value(&mut result, "agents", agents);
|
||||||
|
Self::insert_opt_value(&mut result, "categories", categories);
|
||||||
|
Self::insert_object_entries(&mut result, other_fields.as_ref());
|
||||||
|
}
|
||||||
|
|
||||||
|
Value::Object(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn import_from_local(state: &AppState) -> Result<crate::provider::Provider, AppError> {
|
||||||
|
let actual_path = Self::resolve_local_config_path()?;
|
||||||
|
Self::import_from_path(state, &actual_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn import_from_path(
|
||||||
|
state: &AppState,
|
||||||
|
path: &std::path::Path,
|
||||||
|
) -> Result<crate::provider::Provider, AppError> {
|
||||||
|
let obj = Self::read_jsonc_object(path)?;
|
||||||
|
|
||||||
|
let mut settings = Map::new();
|
||||||
|
if let Some(agents) = obj.get("agents") {
|
||||||
|
settings.insert("agents".to_string(), agents.clone());
|
||||||
|
}
|
||||||
|
if let Some(categories) = obj.get("categories") {
|
||||||
|
settings.insert("categories".to_string(), categories.clone());
|
||||||
|
}
|
||||||
|
settings.insert("useCommonConfig".to_string(), Value::Bool(true));
|
||||||
|
|
||||||
|
let other = Self::extract_other_fields(&obj);
|
||||||
|
if !other.is_empty() {
|
||||||
|
settings.insert("otherFields".to_string(), Value::Object(other));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut global = state.db.get_omo_global_config()?;
|
||||||
|
Self::merge_global_from_obj(&obj, &mut global);
|
||||||
|
global.updated_at = chrono::Utc::now().to_rfc3339();
|
||||||
|
state.db.save_omo_global_config(&global)?;
|
||||||
|
|
||||||
|
let provider_id = format!("omo-{}", uuid::Uuid::new_v4());
|
||||||
|
let name = format!("Imported {}", chrono::Local::now().format("%Y-%m-%d %H:%M"));
|
||||||
|
let settings_config =
|
||||||
|
serde_json::to_value(&settings).unwrap_or_else(|_| serde_json::json!({}));
|
||||||
|
|
||||||
|
let provider = crate::provider::Provider {
|
||||||
|
id: provider_id,
|
||||||
|
name,
|
||||||
|
settings_config,
|
||||||
|
website_url: None,
|
||||||
|
category: Some("omo".to_string()),
|
||||||
|
created_at: Some(chrono::Utc::now().timestamp_millis()),
|
||||||
|
sort_index: None,
|
||||||
|
notes: None,
|
||||||
|
meta: None,
|
||||||
|
icon: None,
|
||||||
|
icon_color: None,
|
||||||
|
in_failover_queue: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
state.db.save_provider("opencode", &provider)?;
|
||||||
|
state
|
||||||
|
.db
|
||||||
|
.set_omo_provider_current("opencode", &provider.id)?;
|
||||||
|
Self::write_config_to_file(state)?;
|
||||||
|
Ok(provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_local_file() -> Result<OmoLocalFileData, AppError> {
|
||||||
|
let actual_path = Self::resolve_local_config_path()?;
|
||||||
|
let metadata = std::fs::metadata(&actual_path).ok();
|
||||||
|
let last_modified = metadata
|
||||||
|
.and_then(|m| m.modified().ok())
|
||||||
|
.map(|t| chrono::DateTime::<chrono::Utc>::from(t).to_rfc3339());
|
||||||
|
|
||||||
|
let obj = Self::read_jsonc_object(&actual_path)?;
|
||||||
|
|
||||||
|
let agents = obj.get("agents").cloned();
|
||||||
|
let categories = obj.get("categories").cloned();
|
||||||
|
|
||||||
|
let other = Self::extract_other_fields(&obj);
|
||||||
|
let other_fields = if other.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(Value::Object(other))
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut global = OmoGlobalConfig::default();
|
||||||
|
Self::merge_global_from_obj(&obj, &mut global);
|
||||||
|
|
||||||
|
Ok(OmoLocalFileData {
|
||||||
|
agents,
|
||||||
|
categories,
|
||||||
|
other_fields,
|
||||||
|
global,
|
||||||
|
file_path: actual_path.to_string_lossy().to_string(),
|
||||||
|
last_modified,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn strip_jsonc_comments(input: &str) -> String {
|
||||||
|
let mut result = String::with_capacity(input.len());
|
||||||
|
let mut chars = input.chars().peekable();
|
||||||
|
let mut in_string = false;
|
||||||
|
let mut escape = false;
|
||||||
|
|
||||||
|
while let Some(&c) = chars.peek() {
|
||||||
|
if in_string {
|
||||||
|
result.push(c);
|
||||||
|
chars.next();
|
||||||
|
if escape {
|
||||||
|
escape = false;
|
||||||
|
} else if c == '\\' {
|
||||||
|
escape = true;
|
||||||
|
} else if c == '"' {
|
||||||
|
in_string = false;
|
||||||
|
}
|
||||||
|
} else if c == '"' {
|
||||||
|
in_string = true;
|
||||||
|
result.push(c);
|
||||||
|
chars.next();
|
||||||
|
} else if c == '/' {
|
||||||
|
chars.next();
|
||||||
|
match chars.peek() {
|
||||||
|
Some('/') => {
|
||||||
|
chars.next();
|
||||||
|
while let Some(&nc) = chars.peek() {
|
||||||
|
if nc == '\n' {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
chars.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some('*') => {
|
||||||
|
chars.next();
|
||||||
|
while let Some(nc) = chars.next() {
|
||||||
|
if nc == '*' {
|
||||||
|
if let Some(&'/') = chars.peek() {
|
||||||
|
chars.next();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
result.push('/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.push(c);
|
||||||
|
chars.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_strip_jsonc_comments() {
|
||||||
|
let input = r#"{
|
||||||
|
// This is a comment
|
||||||
|
"key": "value", // inline comment
|
||||||
|
/* multi
|
||||||
|
line */
|
||||||
|
"key2": "val//ue"
|
||||||
|
}"#;
|
||||||
|
let result = OmoService::strip_jsonc_comments(input);
|
||||||
|
let parsed: Value = serde_json::from_str(&result).unwrap();
|
||||||
|
assert_eq!(parsed["key"], "value");
|
||||||
|
assert_eq!(parsed["key2"], "val//ue");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_merge_config_empty() {
|
||||||
|
let global = OmoGlobalConfig::default();
|
||||||
|
let merged = OmoService::merge_config(&global, None);
|
||||||
|
assert!(merged.is_object());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_merge_config_with_profile() {
|
||||||
|
let global = OmoGlobalConfig {
|
||||||
|
schema_url: Some("https://example.com/schema.json".to_string()),
|
||||||
|
disabled_agents: vec!["explore".to_string()],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let agents = Some(serde_json::json!({
|
||||||
|
"Sisyphus": { "model": "claude-opus-4-5" }
|
||||||
|
}));
|
||||||
|
let categories = None;
|
||||||
|
let other_fields = None;
|
||||||
|
let profile_data = (agents, categories, other_fields, true);
|
||||||
|
let merged = OmoService::merge_config(&global, Some(&profile_data));
|
||||||
|
let obj = merged.as_object().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(obj["$schema"], "https://example.com/schema.json");
|
||||||
|
assert_eq!(obj["disabled_agents"], serde_json::json!(["explore"]));
|
||||||
|
assert!(obj.contains_key("agents"));
|
||||||
|
assert_eq!(obj["agents"]["Sisyphus"]["model"], "claude-opus-4-5");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_merge_config_without_common_config() {
|
||||||
|
let global = OmoGlobalConfig {
|
||||||
|
schema_url: Some("https://example.com/schema.json".to_string()),
|
||||||
|
disabled_agents: vec!["explore".to_string()],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let agents = Some(serde_json::json!({
|
||||||
|
"Sisyphus": { "model": "claude-opus-4-5" }
|
||||||
|
}));
|
||||||
|
let categories = None;
|
||||||
|
let other_fields = None;
|
||||||
|
let profile_data = (agents, categories, other_fields, false);
|
||||||
|
let merged = OmoService::merge_config(&global, Some(&profile_data));
|
||||||
|
let obj = merged.as_object().unwrap();
|
||||||
|
|
||||||
|
assert!(!obj.contains_key("$schema"));
|
||||||
|
assert!(!obj.contains_key("disabled_agents"));
|
||||||
|
assert!(obj.contains_key("agents"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -164,6 +164,12 @@ impl ProviderService {
|
|||||||
|
|
||||||
// OpenCode uses additive mode - always write to live config
|
// OpenCode uses additive mode - always write to live config
|
||||||
if matches!(app_type, AppType::OpenCode) {
|
if matches!(app_type, AppType::OpenCode) {
|
||||||
|
// OMO providers use exclusive mode and write to dedicated config file.
|
||||||
|
if provider.category.as_deref() == Some("omo") {
|
||||||
|
// Do not auto-enable newly added OMO providers.
|
||||||
|
// Users must explicitly switch/apply an OMO provider to activate it.
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
write_live_snapshot(&app_type, &provider)?;
|
write_live_snapshot(&app_type, &provider)?;
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
@@ -197,6 +203,15 @@ impl ProviderService {
|
|||||||
|
|
||||||
// OpenCode uses additive mode - always update in live config
|
// OpenCode uses additive mode - always update in live config
|
||||||
if matches!(app_type, AppType::OpenCode) {
|
if matches!(app_type, AppType::OpenCode) {
|
||||||
|
if provider.category.as_deref() == Some("omo") {
|
||||||
|
let is_omo_current = state
|
||||||
|
.db
|
||||||
|
.is_omo_provider_current(app_type.as_str(), &provider.id)?;
|
||||||
|
if is_omo_current {
|
||||||
|
crate::services::OmoService::write_config_to_file(state)?;
|
||||||
|
}
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
write_live_snapshot(&app_type, &provider)?;
|
write_live_snapshot(&app_type, &provider)?;
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
@@ -242,6 +257,35 @@ impl ProviderService {
|
|||||||
pub fn delete(state: &AppState, app_type: AppType, id: &str) -> Result<(), AppError> {
|
pub fn delete(state: &AppState, app_type: AppType, id: &str) -> Result<(), AppError> {
|
||||||
// OpenCode uses additive mode - no current provider concept
|
// OpenCode uses additive mode - no current provider concept
|
||||||
if matches!(app_type, AppType::OpenCode) {
|
if matches!(app_type, AppType::OpenCode) {
|
||||||
|
let is_omo = state
|
||||||
|
.db
|
||||||
|
.get_provider_by_id(id, app_type.as_str())?
|
||||||
|
.and_then(|p| p.category)
|
||||||
|
.as_deref()
|
||||||
|
== Some("omo");
|
||||||
|
|
||||||
|
if is_omo {
|
||||||
|
let was_current = state.db.is_omo_provider_current(app_type.as_str(), id)?;
|
||||||
|
let omo_count = state
|
||||||
|
.db
|
||||||
|
.get_all_providers(app_type.as_str())?
|
||||||
|
.values()
|
||||||
|
.filter(|p| p.category.as_deref() == Some("omo"))
|
||||||
|
.count();
|
||||||
|
|
||||||
|
if omo_count <= 1 && was_current {
|
||||||
|
return Err(AppError::Message(
|
||||||
|
"无法删除当前启用的最后一个 OMO 配置,请先停用".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
state.db.delete_provider(app_type.as_str(), id)?;
|
||||||
|
if was_current {
|
||||||
|
crate::services::OmoService::delete_config_file()?;
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
// Remove from database
|
// Remove from database
|
||||||
state.db.delete_provider(app_type.as_str(), id)?;
|
state.db.delete_provider(app_type.as_str(), id)?;
|
||||||
// Also remove from live config
|
// Also remove from live config
|
||||||
@@ -267,10 +311,32 @@ impl ProviderService {
|
|||||||
/// Does NOT delete from database - provider remains in the list.
|
/// Does NOT delete from database - provider remains in the list.
|
||||||
/// This is used when user wants to "remove" a provider from active config
|
/// This is used when user wants to "remove" a provider from active config
|
||||||
/// but keep it available for future use.
|
/// but keep it available for future use.
|
||||||
pub fn remove_from_live_config(app_type: AppType, id: &str) -> Result<(), AppError> {
|
pub fn remove_from_live_config(
|
||||||
|
state: &AppState,
|
||||||
|
app_type: AppType,
|
||||||
|
id: &str,
|
||||||
|
) -> Result<(), AppError> {
|
||||||
match app_type {
|
match app_type {
|
||||||
AppType::OpenCode => {
|
AppType::OpenCode => {
|
||||||
remove_opencode_provider_from_live(id)?;
|
let is_omo = state
|
||||||
|
.db
|
||||||
|
.get_provider_by_id(id, app_type.as_str())?
|
||||||
|
.and_then(|p| p.category)
|
||||||
|
.as_deref()
|
||||||
|
== Some("omo");
|
||||||
|
|
||||||
|
if is_omo {
|
||||||
|
state.db.clear_omo_provider_current(app_type.as_str(), id)?;
|
||||||
|
let still_has_current =
|
||||||
|
state.db.get_current_omo_provider("opencode")?.is_some();
|
||||||
|
if still_has_current {
|
||||||
|
crate::services::OmoService::write_config_to_file(state)?;
|
||||||
|
} else {
|
||||||
|
crate::services::OmoService::delete_config_file()?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
remove_opencode_provider_from_live(id)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Future: add other additive mode apps here
|
// Future: add other additive mode apps here
|
||||||
_ => {
|
_ => {
|
||||||
@@ -302,6 +368,11 @@ impl ProviderService {
|
|||||||
.get(id)
|
.get(id)
|
||||||
.ok_or_else(|| AppError::Message(format!("供应商 {id} 不存在")))?;
|
.ok_or_else(|| AppError::Message(format!("供应商 {id} 不存在")))?;
|
||||||
|
|
||||||
|
// OMO providers are switched through their own exclusive path.
|
||||||
|
if matches!(app_type, AppType::OpenCode) && _provider.category.as_deref() == Some("omo") {
|
||||||
|
return Self::switch_normal(state, app_type, id, &providers);
|
||||||
|
}
|
||||||
|
|
||||||
// Check if proxy takeover mode is active AND proxy server is actually running
|
// Check if proxy takeover mode is active AND proxy server is actually running
|
||||||
// Both conditions must be true to use hot-switch mode
|
// Both conditions must be true to use hot-switch mode
|
||||||
// Use blocking wait since this is a sync function
|
// Use blocking wait since this is a sync function
|
||||||
@@ -373,25 +444,28 @@ impl ProviderService {
|
|||||||
.get(id)
|
.get(id)
|
||||||
.ok_or_else(|| AppError::Message(format!("供应商 {id} 不存在")))?;
|
.ok_or_else(|| AppError::Message(format!("供应商 {id} 不存在")))?;
|
||||||
|
|
||||||
|
if matches!(app_type, AppType::OpenCode) && provider.category.as_deref() == Some("omo") {
|
||||||
|
state.db.set_omo_provider_current(app_type.as_str(), id)?;
|
||||||
|
crate::services::OmoService::write_config_to_file(state)?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
// Backfill: Backfill current live config to current provider
|
// Backfill: Backfill current live config to current provider
|
||||||
// Use effective current provider (validated existence) to ensure backfill targets valid provider
|
// Use effective current provider (validated existence) to ensure backfill targets valid provider
|
||||||
let current_id = crate::settings::get_effective_current_provider(&state.db, &app_type)?;
|
let current_id = crate::settings::get_effective_current_provider(&state.db, &app_type)?;
|
||||||
|
|
||||||
if let Some(current_id) = current_id {
|
match (current_id, matches!(app_type, AppType::OpenCode)) {
|
||||||
if current_id != id {
|
(Some(current_id), false) if current_id != id => {
|
||||||
// OpenCode uses additive mode - all providers coexist in the same file,
|
// Only backfill when switching to a different provider.
|
||||||
// no backfill needed (backfill is for exclusive mode apps like Claude/Codex/Gemini)
|
if let Ok(live_config) = read_live_settings(app_type.clone()) {
|
||||||
if !matches!(app_type, AppType::OpenCode) {
|
if let Some(mut current_provider) = providers.get(¤t_id).cloned() {
|
||||||
// Only backfill when switching to a different provider
|
current_provider.settings_config = live_config;
|
||||||
if let Ok(live_config) = read_live_settings(app_type.clone()) {
|
// Ignore backfill failure, don't affect switch flow.
|
||||||
if let Some(mut current_provider) = providers.get(¤t_id).cloned() {
|
let _ = state.db.save_provider(app_type.as_str(), ¤t_provider);
|
||||||
current_provider.settings_config = live_config;
|
|
||||||
// Ignore backfill failure, don't affect switch flow
|
|
||||||
let _ = state.db.save_provider(app_type.as_str(), ¤t_provider);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// OpenCode uses additive mode - skip setting is_current (no such concept)
|
// OpenCode uses additive mode - skip setting is_current (no such concept)
|
||||||
|
|||||||
+25
-62
@@ -8,7 +8,6 @@ import {
|
|||||||
Plus,
|
Plus,
|
||||||
Settings,
|
Settings,
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
// Bot, // TODO: Agents 功能开发中,暂时不需要
|
|
||||||
Book,
|
Book,
|
||||||
Wrench,
|
Wrench,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
@@ -56,6 +55,7 @@ import { UniversalProviderPanel } from "@/components/universal";
|
|||||||
import { McpIcon } from "@/components/BrandIcons";
|
import { McpIcon } from "@/components/BrandIcons";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { SessionManagerPage } from "@/components/sessions/SessionManagerPage";
|
import { SessionManagerPage } from "@/components/sessions/SessionManagerPage";
|
||||||
|
import { useDisableCurrentOmo } from "@/lib/query/omo";
|
||||||
|
|
||||||
type View =
|
type View =
|
||||||
| "providers"
|
| "providers"
|
||||||
@@ -68,7 +68,6 @@ type View =
|
|||||||
| "universal"
|
| "universal"
|
||||||
| "sessions";
|
| "sessions";
|
||||||
|
|
||||||
// macOS Overlay mode needs space for traffic light buttons, Windows/Linux use native titlebar
|
|
||||||
const DRAG_BAR_HEIGHT = isWindows() || isLinux() ? 0 : 28; // px
|
const DRAG_BAR_HEIGHT = isWindows() || isLinux() ? 0 : 28; // px
|
||||||
const HEADER_HEIGHT = 64; // px
|
const HEADER_HEIGHT = 64; // px
|
||||||
const CONTENT_TOP_OFFSET = DRAG_BAR_HEIGHT + HEADER_HEIGHT;
|
const CONTENT_TOP_OFFSET = DRAG_BAR_HEIGHT + HEADER_HEIGHT;
|
||||||
@@ -118,7 +117,6 @@ function App() {
|
|||||||
localStorage.setItem(VIEW_STORAGE_KEY, currentView);
|
localStorage.setItem(VIEW_STORAGE_KEY, currentView);
|
||||||
}, [currentView]);
|
}, [currentView]);
|
||||||
|
|
||||||
// Get settings for visibleApps
|
|
||||||
const { data: settingsData } = useSettingsQuery();
|
const { data: settingsData } = useSettingsQuery();
|
||||||
const visibleApps: VisibleApps = settingsData?.visibleApps ?? {
|
const visibleApps: VisibleApps = settingsData?.visibleApps ?? {
|
||||||
claude: true,
|
claude: true,
|
||||||
@@ -127,7 +125,6 @@ function App() {
|
|||||||
opencode: true,
|
opencode: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get first visible app for fallback
|
|
||||||
const getFirstVisibleApp = (): AppId => {
|
const getFirstVisibleApp = (): AppId => {
|
||||||
if (visibleApps.claude) return "claude";
|
if (visibleApps.claude) return "claude";
|
||||||
if (visibleApps.codex) return "codex";
|
if (visibleApps.codex) return "codex";
|
||||||
@@ -136,7 +133,6 @@ function App() {
|
|||||||
return "claude"; // fallback
|
return "claude"; // fallback
|
||||||
};
|
};
|
||||||
|
|
||||||
// If current active app is hidden, switch to first visible app
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!visibleApps[activeApp]) {
|
if (!visibleApps[activeApp]) {
|
||||||
setActiveApp(getFirstVisibleApp());
|
setActiveApp(getFirstVisibleApp());
|
||||||
@@ -145,7 +141,6 @@ function App() {
|
|||||||
|
|
||||||
const [editingProvider, setEditingProvider] = useState<Provider | null>(null);
|
const [editingProvider, setEditingProvider] = useState<Provider | null>(null);
|
||||||
const [usageProvider, setUsageProvider] = useState<Provider | null>(null);
|
const [usageProvider, setUsageProvider] = useState<Provider | null>(null);
|
||||||
// Confirm action state: 'remove' = remove from live config, 'delete' = delete from database
|
|
||||||
const [confirmAction, setConfirmAction] = useState<{
|
const [confirmAction, setConfirmAction] = useState<{
|
||||||
provider: Provider;
|
provider: Provider;
|
||||||
action: "remove" | "delete";
|
action: "remove" | "delete";
|
||||||
@@ -153,7 +148,6 @@ function App() {
|
|||||||
const [envConflicts, setEnvConflicts] = useState<EnvConflict[]>([]);
|
const [envConflicts, setEnvConflicts] = useState<EnvConflict[]>([]);
|
||||||
const [showEnvBanner, setShowEnvBanner] = useState(false);
|
const [showEnvBanner, setShowEnvBanner] = useState(false);
|
||||||
|
|
||||||
// 使用 Hook 保存最后有效值,用于动画退出期间保持内容显示
|
|
||||||
const effectiveEditingProvider = useLastValidValue(editingProvider);
|
const effectiveEditingProvider = useLastValidValue(editingProvider);
|
||||||
const effectiveUsageProvider = useLastValidValue(usageProvider);
|
const effectiveUsageProvider = useLastValidValue(usageProvider);
|
||||||
|
|
||||||
@@ -164,15 +158,12 @@ function App() {
|
|||||||
const addActionButtonClass =
|
const addActionButtonClass =
|
||||||
"bg-orange-500 hover:bg-orange-600 dark:bg-orange-500 dark:hover:bg-orange-600 text-white shadow-lg shadow-orange-500/30 dark:shadow-orange-500/40 rounded-full w-8 h-8";
|
"bg-orange-500 hover:bg-orange-600 dark:bg-orange-500 dark:hover:bg-orange-600 text-white shadow-lg shadow-orange-500/30 dark:shadow-orange-500/40 rounded-full w-8 h-8";
|
||||||
|
|
||||||
// 获取代理服务状态
|
|
||||||
const {
|
const {
|
||||||
isRunning: isProxyRunning,
|
isRunning: isProxyRunning,
|
||||||
takeoverStatus,
|
takeoverStatus,
|
||||||
status: proxyStatus,
|
status: proxyStatus,
|
||||||
} = useProxyStatus();
|
} = useProxyStatus();
|
||||||
// 当前应用的代理是否开启
|
|
||||||
const isCurrentAppTakeoverActive = takeoverStatus?.[activeApp] || false;
|
const isCurrentAppTakeoverActive = takeoverStatus?.[activeApp] || false;
|
||||||
// 当前应用代理实际使用的供应商 ID(从 active_targets 中获取)
|
|
||||||
const activeProviderId = useMemo(() => {
|
const activeProviderId = useMemo(() => {
|
||||||
const target = proxyStatus?.active_targets?.find(
|
const target = proxyStatus?.active_targets?.find(
|
||||||
(t) => t.app_type === activeApp,
|
(t) => t.app_type === activeApp,
|
||||||
@@ -180,7 +171,6 @@ function App() {
|
|||||||
return target?.provider_id;
|
return target?.provider_id;
|
||||||
}, [proxyStatus?.active_targets, activeApp]);
|
}, [proxyStatus?.active_targets, activeApp]);
|
||||||
|
|
||||||
// 获取供应商列表,当代理服务运行时自动刷新
|
|
||||||
const { data, isLoading, refetch } = useProvidersQuery(activeApp, {
|
const { data, isLoading, refetch } = useProvidersQuery(activeApp, {
|
||||||
isProxyRunning,
|
isProxyRunning,
|
||||||
});
|
});
|
||||||
@@ -188,7 +178,6 @@ function App() {
|
|||||||
const currentProviderId = data?.currentProviderId ?? "";
|
const currentProviderId = data?.currentProviderId ?? "";
|
||||||
const hasSkillsSupport = true;
|
const hasSkillsSupport = true;
|
||||||
|
|
||||||
// 🎯 使用 useProviderActions Hook 统一管理所有 Provider 操作
|
|
||||||
const {
|
const {
|
||||||
addProvider,
|
addProvider,
|
||||||
updateProvider,
|
updateProvider,
|
||||||
@@ -197,7 +186,23 @@ function App() {
|
|||||||
saveUsageScript,
|
saveUsageScript,
|
||||||
} = useProviderActions(activeApp);
|
} = useProviderActions(activeApp);
|
||||||
|
|
||||||
// 监听来自托盘菜单的切换事件
|
const disableOmoMutation = useDisableCurrentOmo();
|
||||||
|
const handleDisableOmo = () => {
|
||||||
|
disableOmoMutation.mutate(undefined, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(t("omo.disabled", { defaultValue: "OMO 已停用" }));
|
||||||
|
},
|
||||||
|
onError: (error: Error) => {
|
||||||
|
toast.error(
|
||||||
|
t("omo.disableFailed", {
|
||||||
|
defaultValue: "停用 OMO 失败: {{error}}",
|
||||||
|
error: extractErrorMessage(error),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let unsubscribe: (() => void) | undefined;
|
let unsubscribe: (() => void) | undefined;
|
||||||
|
|
||||||
@@ -221,7 +226,6 @@ function App() {
|
|||||||
};
|
};
|
||||||
}, [activeApp, refetch]);
|
}, [activeApp, refetch]);
|
||||||
|
|
||||||
// 监听统一供应商同步事件,刷新所有应用的供应商列表
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let unsubscribe: (() => void) | undefined;
|
let unsubscribe: (() => void) | undefined;
|
||||||
|
|
||||||
@@ -229,10 +233,7 @@ function App() {
|
|||||||
try {
|
try {
|
||||||
const { listen } = await import("@tauri-apps/api/event");
|
const { listen } = await import("@tauri-apps/api/event");
|
||||||
unsubscribe = await listen("universal-provider-synced", async () => {
|
unsubscribe = await listen("universal-provider-synced", async () => {
|
||||||
// 统一供应商同步后刷新所有应用的供应商列表
|
|
||||||
// 使用 invalidateQueries 使所有 providers 查询失效
|
|
||||||
await queryClient.invalidateQueries({ queryKey: ["providers"] });
|
await queryClient.invalidateQueries({ queryKey: ["providers"] });
|
||||||
// 同时更新托盘菜单
|
|
||||||
try {
|
try {
|
||||||
await providersApi.updateTrayMenu();
|
await providersApi.updateTrayMenu();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -253,7 +254,6 @@ function App() {
|
|||||||
};
|
};
|
||||||
}, [queryClient]);
|
}, [queryClient]);
|
||||||
|
|
||||||
// 应用启动时检测所有应用的环境变量冲突
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkEnvOnStartup = async () => {
|
const checkEnvOnStartup = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -278,7 +278,6 @@ function App() {
|
|||||||
checkEnvOnStartup();
|
checkEnvOnStartup();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 应用启动时检查是否刚完成了配置迁移
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkMigration = async () => {
|
const checkMigration = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -297,7 +296,6 @@ function App() {
|
|||||||
checkMigration();
|
checkMigration();
|
||||||
}, [t]);
|
}, [t]);
|
||||||
|
|
||||||
// 应用启动时检查是否刚完成了 Skills 自动导入(统一管理 SSOT)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkSkillsMigration = async () => {
|
const checkSkillsMigration = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -326,14 +324,12 @@ function App() {
|
|||||||
checkSkillsMigration();
|
checkSkillsMigration();
|
||||||
}, [t, queryClient]);
|
}, [t, queryClient]);
|
||||||
|
|
||||||
// 切换应用时检测当前应用的环境变量冲突
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkEnvOnSwitch = async () => {
|
const checkEnvOnSwitch = async () => {
|
||||||
try {
|
try {
|
||||||
const conflicts = await checkEnvConflicts(activeApp);
|
const conflicts = await checkEnvConflicts(activeApp);
|
||||||
|
|
||||||
if (conflicts.length > 0) {
|
if (conflicts.length > 0) {
|
||||||
// 合并新检测到的冲突
|
|
||||||
setEnvConflicts((prev) => {
|
setEnvConflicts((prev) => {
|
||||||
const existingKeys = new Set(
|
const existingKeys = new Set(
|
||||||
prev.map((c) => `${c.varName}:${c.sourcePath}`),
|
prev.map((c) => `${c.varName}:${c.sourcePath}`),
|
||||||
@@ -359,7 +355,6 @@ function App() {
|
|||||||
checkEnvOnSwitch();
|
checkEnvOnSwitch();
|
||||||
}, [activeApp]);
|
}, [activeApp]);
|
||||||
|
|
||||||
// 全局键盘快捷键
|
|
||||||
const currentViewRef = useRef(currentView);
|
const currentViewRef = useRef(currentView);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -368,17 +363,14 @@ function App() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
// Cmd/Ctrl + , 打开设置
|
|
||||||
if (event.key === "," && (event.metaKey || event.ctrlKey)) {
|
if (event.key === "," && (event.metaKey || event.ctrlKey)) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setCurrentView("settings");
|
setCurrentView("settings");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ESC 键返回
|
|
||||||
if (event.key !== "Escape" || event.defaultPrevented) return;
|
if (event.key !== "Escape" || event.defaultPrevented) return;
|
||||||
|
|
||||||
// 如果有模态框打开(通过 overflow hidden 判断),则不处理全局 ESC,交给模态框处理
|
|
||||||
if (document.body.style.overflow === "hidden") return;
|
if (document.body.style.overflow === "hidden") return;
|
||||||
|
|
||||||
const view = currentViewRef.current;
|
const view = currentViewRef.current;
|
||||||
@@ -396,7 +388,6 @@ function App() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 打开网站链接
|
|
||||||
const handleOpenWebsite = async (url: string) => {
|
const handleOpenWebsite = async (url: string) => {
|
||||||
try {
|
try {
|
||||||
await settingsApi.openExternal(url);
|
await settingsApi.openExternal(url);
|
||||||
@@ -410,22 +401,17 @@ function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 编辑供应商
|
|
||||||
const handleEditProvider = async (provider: Provider) => {
|
const handleEditProvider = async (provider: Provider) => {
|
||||||
await updateProvider(provider);
|
await updateProvider(provider);
|
||||||
setEditingProvider(null);
|
setEditingProvider(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 确认删除/移除供应商
|
|
||||||
const handleConfirmAction = async () => {
|
const handleConfirmAction = async () => {
|
||||||
if (!confirmAction) return;
|
if (!confirmAction) return;
|
||||||
const { provider, action } = confirmAction;
|
const { provider, action } = confirmAction;
|
||||||
|
|
||||||
if (action === "remove") {
|
if (action === "remove") {
|
||||||
// Remove from live config only (for additive mode apps like OpenCode)
|
|
||||||
// Does NOT delete from database - provider remains in the list
|
|
||||||
await providersApi.removeFromLiveConfig(provider.id, activeApp);
|
await providersApi.removeFromLiveConfig(provider.id, activeApp);
|
||||||
// Invalidate queries to refresh the isInConfig state
|
|
||||||
await queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
queryKey: ["opencodeLiveProviderIds"],
|
queryKey: ["opencodeLiveProviderIds"],
|
||||||
});
|
});
|
||||||
@@ -436,13 +422,11 @@ function App() {
|
|||||||
{ closeButton: true },
|
{ closeButton: true },
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Delete from database
|
|
||||||
await deleteProvider(provider.id);
|
await deleteProvider(provider.id);
|
||||||
}
|
}
|
||||||
setConfirmAction(null);
|
setConfirmAction(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Generate a unique provider key for OpenCode duplication
|
|
||||||
const generateUniqueOpencodeKey = (
|
const generateUniqueOpencodeKey = (
|
||||||
originalKey: string,
|
originalKey: string,
|
||||||
existingKeys: string[],
|
existingKeys: string[],
|
||||||
@@ -453,7 +437,6 @@ function App() {
|
|||||||
return baseKey;
|
return baseKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If -copy already exists, try -copy-2, -copy-3, ...
|
|
||||||
let counter = 2;
|
let counter = 2;
|
||||||
while (existingKeys.includes(`${baseKey}-${counter}`)) {
|
while (existingKeys.includes(`${baseKey}-${counter}`)) {
|
||||||
counter++;
|
counter++;
|
||||||
@@ -461,9 +444,7 @@ function App() {
|
|||||||
return `${baseKey}-${counter}`;
|
return `${baseKey}-${counter}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 复制供应商
|
|
||||||
const handleDuplicateProvider = async (provider: Provider) => {
|
const handleDuplicateProvider = async (provider: Provider) => {
|
||||||
// 1️⃣ 计算新的 sortIndex:如果原供应商有 sortIndex,则复制它
|
|
||||||
const newSortIndex =
|
const newSortIndex =
|
||||||
provider.sortIndex !== undefined ? provider.sortIndex + 1 : undefined;
|
provider.sortIndex !== undefined ? provider.sortIndex + 1 : undefined;
|
||||||
|
|
||||||
@@ -482,7 +463,6 @@ function App() {
|
|||||||
iconColor: provider.iconColor,
|
iconColor: provider.iconColor,
|
||||||
};
|
};
|
||||||
|
|
||||||
// OpenCode: generate unique provider key (used as ID)
|
|
||||||
if (activeApp === "opencode") {
|
if (activeApp === "opencode") {
|
||||||
const existingKeys = Object.keys(providers);
|
const existingKeys = Object.keys(providers);
|
||||||
duplicatedProvider.providerKey = generateUniqueOpencodeKey(
|
duplicatedProvider.providerKey = generateUniqueOpencodeKey(
|
||||||
@@ -491,7 +471,6 @@ function App() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2️⃣ 如果原供应商有 sortIndex,需要将后续所有供应商的 sortIndex +1
|
|
||||||
if (provider.sortIndex !== undefined) {
|
if (provider.sortIndex !== undefined) {
|
||||||
const updates = Object.values(providers)
|
const updates = Object.values(providers)
|
||||||
.filter(
|
.filter(
|
||||||
@@ -505,7 +484,6 @@ function App() {
|
|||||||
sortIndex: p.sortIndex! + 1,
|
sortIndex: p.sortIndex! + 1,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 先更新现有供应商的 sortIndex,为新供应商腾出位置
|
|
||||||
if (updates.length > 0) {
|
if (updates.length > 0) {
|
||||||
try {
|
try {
|
||||||
await providersApi.updateSortOrder(updates, activeApp);
|
await providersApi.updateSortOrder(updates, activeApp);
|
||||||
@@ -521,11 +499,9 @@ function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3️⃣ 添加复制的供应商
|
|
||||||
await addProvider(duplicatedProvider);
|
await addProvider(duplicatedProvider);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 打开提供商终端
|
|
||||||
const handleOpenTerminal = async (provider: Provider) => {
|
const handleOpenTerminal = async (provider: Provider) => {
|
||||||
try {
|
try {
|
||||||
await providersApi.openTerminal(provider.id, activeApp);
|
await providersApi.openTerminal(provider.id, activeApp);
|
||||||
@@ -545,10 +521,8 @@ function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 导入配置成功后刷新
|
|
||||||
const handleImportSuccess = async () => {
|
const handleImportSuccess = async () => {
|
||||||
try {
|
try {
|
||||||
// 导入会影响所有应用的供应商数据:刷新所有 providers 缓存
|
|
||||||
await queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
queryKey: ["providers"],
|
queryKey: ["providers"],
|
||||||
refetchType: "all",
|
refetchType: "all",
|
||||||
@@ -626,7 +600,6 @@ function App() {
|
|||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<div className="px-6 flex flex-col h-[calc(100vh-8rem)] overflow-hidden">
|
<div className="px-6 flex flex-col h-[calc(100vh-8rem)] overflow-hidden">
|
||||||
{/* 独立滚动容器 - 解决 Linux/Ubuntu 下 DndContext 与滚轮事件冲突 */}
|
|
||||||
<div className="flex-1 overflow-y-auto overflow-x-hidden pb-12 px-1">
|
<div className="flex-1 overflow-y-auto overflow-x-hidden pb-12 px-1">
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -648,7 +621,9 @@ function App() {
|
|||||||
}
|
}
|
||||||
activeProviderId={activeProviderId}
|
activeProviderId={activeProviderId}
|
||||||
onSwitch={switchProvider}
|
onSwitch={switchProvider}
|
||||||
onEdit={setEditingProvider}
|
onEdit={(provider) => {
|
||||||
|
setEditingProvider(provider);
|
||||||
|
}}
|
||||||
onDelete={(provider) =>
|
onDelete={(provider) =>
|
||||||
setConfirmAction({ provider, action: "delete" })
|
setConfirmAction({ provider, action: "delete" })
|
||||||
}
|
}
|
||||||
@@ -658,6 +633,9 @@ function App() {
|
|||||||
setConfirmAction({ provider, action: "remove" })
|
setConfirmAction({ provider, action: "remove" })
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
onDisableOmo={
|
||||||
|
activeApp === "opencode" ? handleDisableOmo : undefined
|
||||||
|
}
|
||||||
onDuplicate={handleDuplicateProvider}
|
onDuplicate={handleDuplicateProvider}
|
||||||
onConfigureUsage={setUsageProvider}
|
onConfigureUsage={setUsageProvider}
|
||||||
onOpenWebsite={handleOpenWebsite}
|
onOpenWebsite={handleOpenWebsite}
|
||||||
@@ -695,13 +673,11 @@ function App() {
|
|||||||
className="flex flex-col h-screen overflow-hidden bg-background text-foreground selection:bg-primary/30"
|
className="flex flex-col h-screen overflow-hidden bg-background text-foreground selection:bg-primary/30"
|
||||||
style={{ overflowX: "hidden", paddingTop: CONTENT_TOP_OFFSET }}
|
style={{ overflowX: "hidden", paddingTop: CONTENT_TOP_OFFSET }}
|
||||||
>
|
>
|
||||||
{/* 全局拖拽区域(顶部 28px),避免上边框无法拖动 */}
|
|
||||||
<div
|
<div
|
||||||
className="fixed top-0 left-0 right-0 z-[60]"
|
className="fixed top-0 left-0 right-0 z-[60]"
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
style={{ WebkitAppRegion: "drag", height: DRAG_BAR_HEIGHT } as any}
|
style={{ WebkitAppRegion: "drag", height: DRAG_BAR_HEIGHT } as any}
|
||||||
/>
|
/>
|
||||||
{/* 环境变量警告横幅 */}
|
|
||||||
{showEnvBanner && envConflicts.length > 0 && (
|
{showEnvBanner && envConflicts.length > 0 && (
|
||||||
<EnvWarningBanner
|
<EnvWarningBanner
|
||||||
conflicts={envConflicts}
|
conflicts={envConflicts}
|
||||||
@@ -710,7 +686,6 @@ function App() {
|
|||||||
sessionStorage.setItem("env_banner_dismissed", "true");
|
sessionStorage.setItem("env_banner_dismissed", "true");
|
||||||
}}
|
}}
|
||||||
onDeleted={async () => {
|
onDeleted={async () => {
|
||||||
// 删除后重新检测
|
|
||||||
try {
|
try {
|
||||||
const allConflicts = await checkAllEnvConflicts();
|
const allConflicts = await checkAllEnvConflicts();
|
||||||
const flatConflicts = Object.values(allConflicts).flat();
|
const flatConflicts = Object.values(allConflicts).flat();
|
||||||
@@ -822,7 +797,7 @@ function App() {
|
|||||||
setSettingsDefaultTab("usage");
|
setSettingsDefaultTab("usage");
|
||||||
setCurrentView("settings");
|
setCurrentView("settings");
|
||||||
}}
|
}}
|
||||||
title={t("settings.usage.title", {
|
title={t("usage.title", {
|
||||||
defaultValue: "使用统计",
|
defaultValue: "使用统计",
|
||||||
})}
|
})}
|
||||||
className="hover:bg-black/5 dark:hover:bg-white/5"
|
className="hover:bg-black/5 dark:hover:bg-white/5"
|
||||||
@@ -970,18 +945,6 @@ function App() {
|
|||||||
>
|
>
|
||||||
<Wrench className="flex-shrink-0 w-4 h-4" />
|
<Wrench className="flex-shrink-0 w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
{/* TODO: Agents 功能开发中,暂时隐藏入口 */}
|
|
||||||
{/* {isClaudeApp && (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setCurrentView("agents")}
|
|
||||||
className="text-muted-foreground hover:text-foreground hover:bg-black/5 dark:hover:bg-white/5"
|
|
||||||
title="Agents"
|
|
||||||
>
|
|
||||||
<Bot className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
)} */}
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import { UniversalProviderPanel } from "@/components/universal";
|
|||||||
import { providerPresets } from "@/config/claudeProviderPresets";
|
import { providerPresets } from "@/config/claudeProviderPresets";
|
||||||
import { codexProviderPresets } from "@/config/codexProviderPresets";
|
import { codexProviderPresets } from "@/config/codexProviderPresets";
|
||||||
import { geminiProviderPresets } from "@/config/geminiProviderPresets";
|
import { geminiProviderPresets } from "@/config/geminiProviderPresets";
|
||||||
// Note: opencodeProviderPresets is loaded via ProviderForm, not needed here
|
|
||||||
import type { UniversalProviderPreset } from "@/config/universalProviderPresets";
|
import type { UniversalProviderPreset } from "@/config/universalProviderPresets";
|
||||||
|
|
||||||
interface AddProviderDialogProps {
|
interface AddProviderDialogProps {
|
||||||
@@ -36,7 +35,6 @@ export function AddProviderDialog({
|
|||||||
onSubmit,
|
onSubmit,
|
||||||
}: AddProviderDialogProps) {
|
}: AddProviderDialogProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
// OpenCode doesn't support universal providers
|
|
||||||
const showUniversalTab = appId !== "opencode";
|
const showUniversalTab = appId !== "opencode";
|
||||||
const [activeTab, setActiveTab] = useState<"app-specific" | "universal">(
|
const [activeTab, setActiveTab] = useState<"app-specific" | "universal">(
|
||||||
"app-specific",
|
"app-specific",
|
||||||
@@ -45,7 +43,6 @@ export function AddProviderDialog({
|
|||||||
const [selectedUniversalPreset, setSelectedUniversalPreset] =
|
const [selectedUniversalPreset, setSelectedUniversalPreset] =
|
||||||
useState<UniversalProviderPreset | null>(null);
|
useState<UniversalProviderPreset | null>(null);
|
||||||
|
|
||||||
// Handle universal provider save
|
|
||||||
const handleUniversalProviderSave = useCallback(
|
const handleUniversalProviderSave = useCallback(
|
||||||
async (provider: UniversalProvider) => {
|
async (provider: UniversalProvider) => {
|
||||||
try {
|
try {
|
||||||
@@ -73,7 +70,6 @@ export function AddProviderDialog({
|
|||||||
[t, onOpenChange],
|
[t, onOpenChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Close universal form and return to main dialog
|
|
||||||
const handleUniversalFormClose = useCallback(() => {
|
const handleUniversalFormClose = useCallback(() => {
|
||||||
setUniversalFormOpen(false);
|
setUniversalFormOpen(false);
|
||||||
setSelectedUniversalPreset(null);
|
setSelectedUniversalPreset(null);
|
||||||
@@ -86,7 +82,6 @@ export function AddProviderDialog({
|
|||||||
unknown
|
unknown
|
||||||
>;
|
>;
|
||||||
|
|
||||||
// 构造基础提交数据
|
|
||||||
const providerData: Omit<Provider, "id"> & { providerKey?: string } = {
|
const providerData: Omit<Provider, "id"> & { providerKey?: string } = {
|
||||||
name: values.name.trim(),
|
name: values.name.trim(),
|
||||||
notes: values.notes?.trim() || undefined,
|
notes: values.notes?.trim() || undefined,
|
||||||
@@ -98,7 +93,6 @@ export function AddProviderDialog({
|
|||||||
...(values.meta ? { meta: values.meta } : {}),
|
...(values.meta ? { meta: values.meta } : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
// OpenCode: pass providerKey for ID generation
|
|
||||||
if (appId === "opencode" && values.providerKey) {
|
if (appId === "opencode" && values.providerKey) {
|
||||||
providerData.providerKey = values.providerKey;
|
providerData.providerKey = values.providerKey;
|
||||||
}
|
}
|
||||||
@@ -107,8 +101,7 @@ export function AddProviderDialog({
|
|||||||
providerData.meta?.custom_endpoints &&
|
providerData.meta?.custom_endpoints &&
|
||||||
Object.keys(providerData.meta.custom_endpoints).length > 0;
|
Object.keys(providerData.meta.custom_endpoints).length > 0;
|
||||||
|
|
||||||
if (!hasCustomEndpoints) {
|
if (!hasCustomEndpoints && values.presetCategory !== "omo") {
|
||||||
// 收集端点候选(仅在缺少自定义端点时兜底)
|
|
||||||
const urlSet = new Set<string>();
|
const urlSet = new Set<string>();
|
||||||
|
|
||||||
const addUrl = (rawUrl?: string) => {
|
const addUrl = (rawUrl?: string) => {
|
||||||
@@ -163,7 +156,6 @@ export function AddProviderDialog({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Note: OpenCode doesn't use endpointCandidates - it handles endpoints internally
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (appId === "claude") {
|
if (appId === "claude") {
|
||||||
@@ -187,7 +179,6 @@ export function AddProviderDialog({
|
|||||||
addUrl(env.GOOGLE_GEMINI_BASE_URL);
|
addUrl(env.GOOGLE_GEMINI_BASE_URL);
|
||||||
}
|
}
|
||||||
} else if (appId === "opencode") {
|
} else if (appId === "opencode") {
|
||||||
// OpenCode uses options.baseURL
|
|
||||||
const options = parsedConfig.options as
|
const options = parsedConfig.options as
|
||||||
| Record<string, any>
|
| Record<string, any>
|
||||||
| undefined;
|
| undefined;
|
||||||
@@ -221,7 +212,6 @@ export function AddProviderDialog({
|
|||||||
[appId, onSubmit, onOpenChange],
|
[appId, onSubmit, onOpenChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 动态 footer:根据当前 Tab 显示不同按钮
|
|
||||||
const footer =
|
const footer =
|
||||||
!showUniversalTab || activeTab === "app-specific" ? (
|
!showUniversalTab || activeTab === "app-specific" ? (
|
||||||
<>
|
<>
|
||||||
@@ -296,7 +286,6 @@ export function AddProviderDialog({
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
) : (
|
) : (
|
||||||
// OpenCode: directly show form without tabs
|
|
||||||
<ProviderForm
|
<ProviderForm
|
||||||
appId={appId}
|
appId={appId}
|
||||||
submitLabel={t("common.add")}
|
submitLabel={t("common.add")}
|
||||||
@@ -306,7 +295,6 @@ export function AddProviderDialog({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Universal Provider Form Modal */}
|
|
||||||
{showUniversalTab && (
|
{showUniversalTab && (
|
||||||
<UniversalProviderFormModal
|
<UniversalProviderFormModal
|
||||||
isOpen={universalFormOpen}
|
isOpen={universalFormOpen}
|
||||||
|
|||||||
@@ -19,20 +19,20 @@ import type { AppId } from "@/lib/api";
|
|||||||
interface ProviderActionsProps {
|
interface ProviderActionsProps {
|
||||||
appId?: AppId;
|
appId?: AppId;
|
||||||
isCurrent: boolean;
|
isCurrent: boolean;
|
||||||
/** OpenCode: 是否已添加到配置 */
|
|
||||||
isInConfig?: boolean;
|
isInConfig?: boolean;
|
||||||
isTesting?: boolean;
|
isTesting?: boolean;
|
||||||
isProxyTakeover?: boolean;
|
isProxyTakeover?: boolean;
|
||||||
|
isOmo?: boolean;
|
||||||
|
isLastOmo?: boolean;
|
||||||
onSwitch: () => void;
|
onSwitch: () => void;
|
||||||
onEdit: () => void;
|
onEdit: () => void;
|
||||||
onDuplicate: () => void;
|
onDuplicate: () => void;
|
||||||
onTest?: () => void;
|
onTest?: () => void;
|
||||||
onConfigureUsage: () => void;
|
onConfigureUsage: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
/** OpenCode: remove from live config (not delete from database) */
|
|
||||||
onRemoveFromConfig?: () => void;
|
onRemoveFromConfig?: () => void;
|
||||||
|
onDisableOmo?: () => void;
|
||||||
onOpenTerminal?: () => void;
|
onOpenTerminal?: () => void;
|
||||||
// 故障转移相关
|
|
||||||
isAutoFailoverEnabled?: boolean;
|
isAutoFailoverEnabled?: boolean;
|
||||||
isInFailoverQueue?: boolean;
|
isInFailoverQueue?: boolean;
|
||||||
onToggleFailover?: (enabled: boolean) => void;
|
onToggleFailover?: (enabled: boolean) => void;
|
||||||
@@ -44,6 +44,8 @@ export function ProviderActions({
|
|||||||
isInConfig = false,
|
isInConfig = false,
|
||||||
isTesting,
|
isTesting,
|
||||||
isProxyTakeover = false,
|
isProxyTakeover = false,
|
||||||
|
isOmo = false,
|
||||||
|
isLastOmo = false,
|
||||||
onSwitch,
|
onSwitch,
|
||||||
onEdit,
|
onEdit,
|
||||||
onDuplicate,
|
onDuplicate,
|
||||||
@@ -51,8 +53,8 @@ export function ProviderActions({
|
|||||||
onConfigureUsage,
|
onConfigureUsage,
|
||||||
onDelete,
|
onDelete,
|
||||||
onRemoveFromConfig,
|
onRemoveFromConfig,
|
||||||
|
onDisableOmo,
|
||||||
onOpenTerminal,
|
onOpenTerminal,
|
||||||
// 故障转移相关
|
|
||||||
isAutoFailoverEnabled = false,
|
isAutoFailoverEnabled = false,
|
||||||
isInFailoverQueue = false,
|
isInFailoverQueue = false,
|
||||||
onToggleFailover,
|
onToggleFailover,
|
||||||
@@ -60,19 +62,20 @@ export function ProviderActions({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const iconButtonClass = "h-8 w-8 p-1";
|
const iconButtonClass = "h-8 w-8 p-1";
|
||||||
|
|
||||||
// OpenCode 使用累加模式
|
const isOpenCodeMode = appId === "opencode" && !isOmo;
|
||||||
const isOpenCodeMode = appId === "opencode";
|
|
||||||
|
|
||||||
// 故障转移模式下的按钮逻辑(OpenCode 不支持故障转移)
|
|
||||||
const isFailoverMode =
|
const isFailoverMode =
|
||||||
!isOpenCodeMode && isAutoFailoverEnabled && onToggleFailover;
|
!isOpenCodeMode && !isOmo && isAutoFailoverEnabled && onToggleFailover;
|
||||||
|
|
||||||
// 处理主按钮点击
|
|
||||||
const handleMainButtonClick = () => {
|
const handleMainButtonClick = () => {
|
||||||
if (isOpenCodeMode) {
|
if (isOmo) {
|
||||||
// OpenCode 模式:切换配置状态(添加/移除)
|
if (isCurrent) {
|
||||||
|
onDisableOmo?.();
|
||||||
|
} else {
|
||||||
|
onSwitch();
|
||||||
|
}
|
||||||
|
} else if (isOpenCodeMode) {
|
||||||
if (isInConfig) {
|
if (isInConfig) {
|
||||||
// Use onRemoveFromConfig if available, otherwise fall back to onDelete
|
|
||||||
if (onRemoveFromConfig) {
|
if (onRemoveFromConfig) {
|
||||||
onRemoveFromConfig();
|
onRemoveFromConfig();
|
||||||
} else {
|
} else {
|
||||||
@@ -82,17 +85,33 @@ export function ProviderActions({
|
|||||||
onSwitch(); // 添加到配置
|
onSwitch(); // 添加到配置
|
||||||
}
|
}
|
||||||
} else if (isFailoverMode) {
|
} else if (isFailoverMode) {
|
||||||
// 故障转移模式:切换队列状态
|
|
||||||
onToggleFailover(!isInFailoverQueue);
|
onToggleFailover(!isInFailoverQueue);
|
||||||
} else {
|
} else {
|
||||||
// 普通模式:切换供应商
|
|
||||||
onSwitch();
|
onSwitch();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 主按钮的状态和样式
|
|
||||||
const getMainButtonState = () => {
|
const getMainButtonState = () => {
|
||||||
// OpenCode 累加模式
|
if (isOmo) {
|
||||||
|
if (isCurrent) {
|
||||||
|
return {
|
||||||
|
disabled: false,
|
||||||
|
variant: "secondary" as const,
|
||||||
|
className:
|
||||||
|
"bg-gray-200 text-muted-foreground hover:bg-gray-200 hover:text-muted-foreground dark:bg-gray-700 dark:hover:bg-gray-700",
|
||||||
|
icon: <Check className="h-4 w-4" />,
|
||||||
|
text: t("provider.inUse"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
disabled: false,
|
||||||
|
variant: "default" as const,
|
||||||
|
className: "",
|
||||||
|
icon: <Play className="h-4 w-4" />,
|
||||||
|
text: t("provider.enable"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (isOpenCodeMode) {
|
if (isOpenCodeMode) {
|
||||||
if (isInConfig) {
|
if (isInConfig) {
|
||||||
return {
|
return {
|
||||||
@@ -114,7 +133,6 @@ export function ProviderActions({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 故障转移模式
|
|
||||||
if (isFailoverMode) {
|
if (isFailoverMode) {
|
||||||
if (isInFailoverQueue) {
|
if (isInFailoverQueue) {
|
||||||
return {
|
return {
|
||||||
@@ -136,7 +154,6 @@ export function ProviderActions({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 普通模式
|
|
||||||
if (isCurrent) {
|
if (isCurrent) {
|
||||||
return {
|
return {
|
||||||
disabled: true,
|
disabled: true,
|
||||||
@@ -161,8 +178,11 @@ export function ProviderActions({
|
|||||||
|
|
||||||
const buttonState = getMainButtonState();
|
const buttonState = getMainButtonState();
|
||||||
|
|
||||||
// OpenCode 模式下删除按钮始终可用(主按钮"移除"是从 live 配置移除,删除是从数据库删除)
|
const canDelete = isOmo
|
||||||
const canDelete = isOpenCodeMode ? true : !isCurrent;
|
? !(isLastOmo && isCurrent)
|
||||||
|
: isOpenCodeMode
|
||||||
|
? true
|
||||||
|
: !isCurrent;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
|
|||||||
@@ -27,11 +27,13 @@ interface ProviderCardProps {
|
|||||||
isCurrent: boolean;
|
isCurrent: boolean;
|
||||||
appId: AppId;
|
appId: AppId;
|
||||||
isInConfig?: boolean; // OpenCode: 是否已添加到 opencode.json
|
isInConfig?: boolean; // OpenCode: 是否已添加到 opencode.json
|
||||||
|
isOmo?: boolean;
|
||||||
|
isLastOmo?: boolean;
|
||||||
onSwitch: (provider: Provider) => void;
|
onSwitch: (provider: Provider) => void;
|
||||||
onEdit: (provider: Provider) => void;
|
onEdit: (provider: Provider) => void;
|
||||||
onDelete: (provider: Provider) => void;
|
onDelete: (provider: Provider) => void;
|
||||||
/** OpenCode: remove from live config (not delete from database) */
|
|
||||||
onRemoveFromConfig?: (provider: Provider) => void;
|
onRemoveFromConfig?: (provider: Provider) => void;
|
||||||
|
onDisableOmo?: () => void;
|
||||||
onConfigureUsage: (provider: Provider) => void;
|
onConfigureUsage: (provider: Provider) => void;
|
||||||
onOpenWebsite: (url: string) => void;
|
onOpenWebsite: (url: string) => void;
|
||||||
onDuplicate: (provider: Provider) => void;
|
onDuplicate: (provider: Provider) => void;
|
||||||
@@ -41,7 +43,6 @@ interface ProviderCardProps {
|
|||||||
isProxyRunning: boolean;
|
isProxyRunning: boolean;
|
||||||
isProxyTakeover?: boolean; // 代理接管模式(Live配置已被接管,切换为热切换)
|
isProxyTakeover?: boolean; // 代理接管模式(Live配置已被接管,切换为热切换)
|
||||||
dragHandleProps?: DragHandleProps;
|
dragHandleProps?: DragHandleProps;
|
||||||
// 故障转移相关
|
|
||||||
isAutoFailoverEnabled?: boolean; // 是否开启自动故障转移
|
isAutoFailoverEnabled?: boolean; // 是否开启自动故障转移
|
||||||
failoverPriority?: number; // 故障转移优先级(1 = P1, 2 = P2, ...)
|
failoverPriority?: number; // 故障转移优先级(1 = P1, 2 = P2, ...)
|
||||||
isInFailoverQueue?: boolean; // 是否在故障转移队列中
|
isInFailoverQueue?: boolean; // 是否在故障转移队列中
|
||||||
@@ -50,17 +51,14 @@ interface ProviderCardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const extractApiUrl = (provider: Provider, fallbackText: string) => {
|
const extractApiUrl = (provider: Provider, fallbackText: string) => {
|
||||||
// 优先级 1: 备注
|
|
||||||
if (provider.notes?.trim()) {
|
if (provider.notes?.trim()) {
|
||||||
return provider.notes.trim();
|
return provider.notes.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 优先级 2: 官网地址
|
|
||||||
if (provider.websiteUrl) {
|
if (provider.websiteUrl) {
|
||||||
return provider.websiteUrl;
|
return provider.websiteUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 优先级 3: 从配置中提取请求地址
|
|
||||||
const config = provider.settingsConfig;
|
const config = provider.settingsConfig;
|
||||||
|
|
||||||
if (config && typeof config === "object") {
|
if (config && typeof config === "object") {
|
||||||
@@ -89,10 +87,13 @@ export function ProviderCard({
|
|||||||
isCurrent,
|
isCurrent,
|
||||||
appId,
|
appId,
|
||||||
isInConfig = true,
|
isInConfig = true,
|
||||||
|
isOmo = false,
|
||||||
|
isLastOmo = false,
|
||||||
onSwitch,
|
onSwitch,
|
||||||
onEdit,
|
onEdit,
|
||||||
onDelete,
|
onDelete,
|
||||||
onRemoveFromConfig,
|
onRemoveFromConfig,
|
||||||
|
onDisableOmo,
|
||||||
onConfigureUsage,
|
onConfigureUsage,
|
||||||
onOpenWebsite,
|
onOpenWebsite,
|
||||||
onDuplicate,
|
onDuplicate,
|
||||||
@@ -102,7 +103,6 @@ export function ProviderCard({
|
|||||||
isProxyRunning,
|
isProxyRunning,
|
||||||
isProxyTakeover = false,
|
isProxyTakeover = false,
|
||||||
dragHandleProps,
|
dragHandleProps,
|
||||||
// 故障转移相关
|
|
||||||
isAutoFailoverEnabled = false,
|
isAutoFailoverEnabled = false,
|
||||||
failoverPriority,
|
failoverPriority,
|
||||||
isInFailoverQueue = false,
|
isInFailoverQueue = false,
|
||||||
@@ -111,7 +111,6 @@ export function ProviderCard({
|
|||||||
}: ProviderCardProps) {
|
}: ProviderCardProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
// 获取供应商健康状态
|
|
||||||
const { data: health } = useProviderHealth(provider.id, appId);
|
const { data: health } = useProviderHealth(provider.id, appId);
|
||||||
|
|
||||||
const fallbackUrlText = t("provider.notConfigured", {
|
const fallbackUrlText = t("provider.notConfigured", {
|
||||||
@@ -122,24 +121,18 @@ export function ProviderCard({
|
|||||||
return extractApiUrl(provider, fallbackUrlText);
|
return extractApiUrl(provider, fallbackUrlText);
|
||||||
}, [provider, fallbackUrlText]);
|
}, [provider, fallbackUrlText]);
|
||||||
|
|
||||||
// 判断是否为可点击的 URL(备注不可点击)
|
|
||||||
const isClickableUrl = useMemo(() => {
|
const isClickableUrl = useMemo(() => {
|
||||||
// 如果有备注,则不可点击
|
|
||||||
if (provider.notes?.trim()) {
|
if (provider.notes?.trim()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// 如果显示的是回退文本,也不可点击
|
|
||||||
if (displayUrl === fallbackUrlText) {
|
if (displayUrl === fallbackUrlText) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// 其他情况(官网地址或请求地址)可点击
|
|
||||||
return true;
|
return true;
|
||||||
}, [provider.notes, displayUrl, fallbackUrlText]);
|
}, [provider.notes, displayUrl, fallbackUrlText]);
|
||||||
|
|
||||||
const usageEnabled = provider.meta?.usage_script?.enabled ?? false;
|
const usageEnabled = provider.meta?.usage_script?.enabled ?? false;
|
||||||
|
|
||||||
// 获取用量数据以判断是否有多套餐
|
|
||||||
// OpenCode(累加模式):使用 isInConfig 代替 isCurrent
|
|
||||||
const shouldAutoQuery = appId === "opencode" ? isInConfig : isCurrent;
|
const shouldAutoQuery = appId === "opencode" ? isInConfig : isCurrent;
|
||||||
const autoQueryInterval = shouldAutoQuery
|
const autoQueryInterval = shouldAutoQuery
|
||||||
? provider.meta?.usage_script?.autoQueryInterval || 0
|
? provider.meta?.usage_script?.autoQueryInterval || 0
|
||||||
@@ -153,21 +146,17 @@ export function ProviderCard({
|
|||||||
const hasMultiplePlans =
|
const hasMultiplePlans =
|
||||||
usage?.success && usage.data && usage.data.length > 1;
|
usage?.success && usage.data && usage.data.length > 1;
|
||||||
|
|
||||||
// 多套餐默认展开
|
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
|
||||||
// 操作按钮容器 ref,用于动态计算宽度
|
|
||||||
const actionsRef = useRef<HTMLDivElement>(null);
|
const actionsRef = useRef<HTMLDivElement>(null);
|
||||||
const [actionsWidth, setActionsWidth] = useState(0);
|
const [actionsWidth, setActionsWidth] = useState(0);
|
||||||
|
|
||||||
// 当检测到多套餐时自动展开
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasMultiplePlans) {
|
if (hasMultiplePlans) {
|
||||||
setIsExpanded(true);
|
setIsExpanded(true);
|
||||||
}
|
}
|
||||||
}, [hasMultiplePlans]);
|
}, [hasMultiplePlans]);
|
||||||
|
|
||||||
// 动态获取操作按钮宽度
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (actionsRef.current) {
|
if (actionsRef.current) {
|
||||||
const updateWidth = () => {
|
const updateWidth = () => {
|
||||||
@@ -175,7 +164,6 @@ export function ProviderCard({
|
|||||||
setActionsWidth(width);
|
setActionsWidth(width);
|
||||||
};
|
};
|
||||||
updateWidth();
|
updateWidth();
|
||||||
// 监听窗口大小变化
|
|
||||||
window.addEventListener("resize", updateWidth);
|
window.addEventListener("resize", updateWidth);
|
||||||
return () => window.removeEventListener("resize", updateWidth);
|
return () => window.removeEventListener("resize", updateWidth);
|
||||||
}
|
}
|
||||||
@@ -188,32 +176,27 @@ export function ProviderCard({
|
|||||||
onOpenWebsite(displayUrl);
|
onOpenWebsite(displayUrl);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 判断是否是"当前使用中"的供应商
|
const isActiveProvider = isOmo
|
||||||
// - OpenCode(累加模式):不存在"当前"概念,始终返回 false
|
? isCurrent
|
||||||
// - 故障转移模式:代理实际使用的供应商(activeProviderId)
|
: appId === "opencode"
|
||||||
// - 代理接管模式(非故障转移):isCurrent
|
|
||||||
// - 普通模式:isCurrent
|
|
||||||
const isActiveProvider =
|
|
||||||
appId === "opencode"
|
|
||||||
? false
|
? false
|
||||||
: isAutoFailoverEnabled
|
: isAutoFailoverEnabled
|
||||||
? activeProviderId === provider.id
|
? activeProviderId === provider.id
|
||||||
: isCurrent;
|
: isCurrent;
|
||||||
|
|
||||||
// 判断是否使用绿色(代理接管模式)还是蓝色(普通模式)
|
const shouldUseGreen = !isOmo && isProxyTakeover && isActiveProvider;
|
||||||
const shouldUseGreen = isProxyTakeover && isActiveProvider;
|
const shouldUseBlue =
|
||||||
const shouldUseBlue = !isProxyTakeover && isActiveProvider;
|
(isOmo && isActiveProvider) ||
|
||||||
|
(!isOmo && !isProxyTakeover && isActiveProvider);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative overflow-hidden rounded-xl border border-border p-4 transition-all duration-300",
|
"relative overflow-hidden rounded-xl border border-border p-4 transition-all duration-300",
|
||||||
"bg-card text-card-foreground group",
|
"bg-card text-card-foreground group",
|
||||||
// hover 时的边框效果
|
|
||||||
isAutoFailoverEnabled || isProxyTakeover
|
isAutoFailoverEnabled || isProxyTakeover
|
||||||
? "hover:border-emerald-500/50"
|
? "hover:border-emerald-500/50"
|
||||||
: "hover:border-border-active",
|
: "hover:border-border-active",
|
||||||
// 当前激活的供应商边框样式
|
|
||||||
shouldUseGreen &&
|
shouldUseGreen &&
|
||||||
"border-emerald-500/60 shadow-sm shadow-emerald-500/10",
|
"border-emerald-500/60 shadow-sm shadow-emerald-500/10",
|
||||||
shouldUseBlue && "border-blue-500/60 shadow-sm shadow-blue-500/10",
|
shouldUseBlue && "border-blue-500/60 shadow-sm shadow-blue-500/10",
|
||||||
@@ -225,7 +208,6 @@ export function ProviderCard({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute inset-0 bg-gradient-to-r to-transparent transition-opacity duration-500 pointer-events-none",
|
"absolute inset-0 bg-gradient-to-r to-transparent transition-opacity duration-500 pointer-events-none",
|
||||||
// 代理接管模式使用绿色渐变,普通模式使用蓝色渐变
|
|
||||||
shouldUseGreen && "from-emerald-500/10",
|
shouldUseGreen && "from-emerald-500/10",
|
||||||
shouldUseBlue && "from-blue-500/10",
|
shouldUseBlue && "from-blue-500/10",
|
||||||
!isActiveProvider && "from-primary/10",
|
!isActiveProvider && "from-primary/10",
|
||||||
@@ -248,7 +230,6 @@ export function ProviderCard({
|
|||||||
<GripVertical className="h-4 w-4" />
|
<GripVertical className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* 供应商图标 */}
|
|
||||||
<div className="h-8 w-8 rounded-lg bg-muted flex items-center justify-center border border-border group-hover:scale-105 transition-transform duration-300">
|
<div className="h-8 w-8 rounded-lg bg-muted flex items-center justify-center border border-border group-hover:scale-105 transition-transform duration-300">
|
||||||
<ProviderIcon
|
<ProviderIcon
|
||||||
icon={provider.icon}
|
icon={provider.icon}
|
||||||
@@ -264,14 +245,18 @@ export function ProviderCard({
|
|||||||
{provider.name}
|
{provider.name}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{/* 健康状态徽章 */}
|
{isOmo && (
|
||||||
|
<span className="inline-flex items-center rounded-md bg-violet-100 px-1.5 py-0.5 text-[10px] font-semibold text-violet-700 dark:bg-violet-900/40 dark:text-violet-300">
|
||||||
|
OMO
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
{isProxyRunning && isInFailoverQueue && health && (
|
{isProxyRunning && isInFailoverQueue && health && (
|
||||||
<ProviderHealthBadge
|
<ProviderHealthBadge
|
||||||
consecutiveFailures={health.consecutive_failures}
|
consecutiveFailures={health.consecutive_failures}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 故障转移优先级徽章 */}
|
|
||||||
{isAutoFailoverEnabled &&
|
{isAutoFailoverEnabled &&
|
||||||
isInFailoverQueue &&
|
isInFailoverQueue &&
|
||||||
failoverPriority && (
|
failoverPriority && (
|
||||||
@@ -318,10 +303,8 @@ export function ProviderCard({
|
|||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{/* 用量信息区域 - hover 时向左移动,为操作按钮腾出空间 */}
|
|
||||||
<div className="ml-auto">
|
<div className="ml-auto">
|
||||||
<div className="flex items-center gap-1 transition-transform duration-200 group-hover:-translate-x-[var(--actions-width)] group-focus-within:-translate-x-[var(--actions-width)]">
|
<div className="flex items-center gap-1 transition-transform duration-200 group-hover:-translate-x-[var(--actions-width)] group-focus-within:-translate-x-[var(--actions-width)]">
|
||||||
{/* 多套餐时显示套餐数量,单套餐时显示详细信息 */}
|
|
||||||
{hasMultiplePlans ? (
|
{hasMultiplePlans ? (
|
||||||
<div className="flex items-center gap-2 text-xs text-gray-600 dark:text-gray-400">
|
<div className="flex items-center gap-2 text-xs text-gray-600 dark:text-gray-400">
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
@@ -342,7 +325,6 @@ export function ProviderCard({
|
|||||||
inline={true}
|
inline={true}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/* 展开/折叠按钮 - 仅在有多套餐时显示 */}
|
|
||||||
{hasMultiplePlans && (
|
{hasMultiplePlans && (
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
@@ -366,7 +348,6 @@ export function ProviderCard({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 操作按钮区域 - 绝对定位在右侧,hover 时滑入,与用量信息保持间距 */}
|
|
||||||
<div
|
<div
|
||||||
ref={actionsRef}
|
ref={actionsRef}
|
||||||
className="absolute right-0 top-1/2 -translate-y-1/2 flex items-center gap-1.5 pl-3 opacity-0 pointer-events-none group-hover:opacity-100 group-focus-within:opacity-100 group-hover:pointer-events-auto group-focus-within:pointer-events-auto transition-all duration-200 translate-x-2 group-hover:translate-x-0 group-focus-within:translate-x-0"
|
className="absolute right-0 top-1/2 -translate-y-1/2 flex items-center gap-1.5 pl-3 opacity-0 pointer-events-none group-hover:opacity-100 group-focus-within:opacity-100 group-hover:pointer-events-auto group-focus-within:pointer-events-auto transition-all duration-200 translate-x-2 group-hover:translate-x-0 group-focus-within:translate-x-0"
|
||||||
@@ -377,6 +358,8 @@ export function ProviderCard({
|
|||||||
isInConfig={isInConfig}
|
isInConfig={isInConfig}
|
||||||
isTesting={isTesting}
|
isTesting={isTesting}
|
||||||
isProxyTakeover={isProxyTakeover}
|
isProxyTakeover={isProxyTakeover}
|
||||||
|
isOmo={isOmo}
|
||||||
|
isLastOmo={isLastOmo}
|
||||||
onSwitch={() => onSwitch(provider)}
|
onSwitch={() => onSwitch(provider)}
|
||||||
onEdit={() => onEdit(provider)}
|
onEdit={() => onEdit(provider)}
|
||||||
onDuplicate={() => onDuplicate(provider)}
|
onDuplicate={() => onDuplicate(provider)}
|
||||||
@@ -388,10 +371,10 @@ export function ProviderCard({
|
|||||||
? () => onRemoveFromConfig(provider)
|
? () => onRemoveFromConfig(provider)
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
onDisableOmo={onDisableOmo}
|
||||||
onOpenTerminal={
|
onOpenTerminal={
|
||||||
onOpenTerminal ? () => onOpenTerminal(provider) : undefined
|
onOpenTerminal ? () => onOpenTerminal(provider) : undefined
|
||||||
}
|
}
|
||||||
// 故障转移相关
|
|
||||||
isAutoFailoverEnabled={isAutoFailoverEnabled}
|
isAutoFailoverEnabled={isAutoFailoverEnabled}
|
||||||
isInFailoverQueue={isInFailoverQueue}
|
isInFailoverQueue={isInFailoverQueue}
|
||||||
onToggleFailover={onToggleFailover}
|
onToggleFailover={onToggleFailover}
|
||||||
@@ -400,7 +383,6 @@ export function ProviderCard({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 展开的完整套餐列表 */}
|
|
||||||
{isExpanded && hasMultiplePlans && (
|
{isExpanded && hasMultiplePlans && (
|
||||||
<div className="mt-4 pt-4 border-t border-border-default">
|
<div className="mt-4 pt-4 border-t border-border-default">
|
||||||
<UsageFooter
|
<UsageFooter
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import type { Provider } from "@/types";
|
|||||||
import type { AppId } from "@/lib/api";
|
import type { AppId } from "@/lib/api";
|
||||||
import { providersApi } from "@/lib/api/providers";
|
import { providersApi } from "@/lib/api/providers";
|
||||||
import { useDragSort } from "@/hooks/useDragSort";
|
import { useDragSort } from "@/hooks/useDragSort";
|
||||||
// import { useStreamCheck } from "@/hooks/useStreamCheck"; // 测试功能已隐藏
|
|
||||||
import { ProviderCard } from "@/components/providers/ProviderCard";
|
import { ProviderCard } from "@/components/providers/ProviderCard";
|
||||||
import { ProviderEmptyState } from "@/components/providers/ProviderEmptyState";
|
import { ProviderEmptyState } from "@/components/providers/ProviderEmptyState";
|
||||||
import {
|
import {
|
||||||
@@ -29,6 +28,7 @@ import {
|
|||||||
useAddToFailoverQueue,
|
useAddToFailoverQueue,
|
||||||
useRemoveFromFailoverQueue,
|
useRemoveFromFailoverQueue,
|
||||||
} from "@/lib/query/failover";
|
} from "@/lib/query/failover";
|
||||||
|
import { useCurrentOmoProviderId, useOmoProviderCount } from "@/lib/query/omo";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -40,8 +40,8 @@ interface ProviderListProps {
|
|||||||
onSwitch: (provider: Provider) => void;
|
onSwitch: (provider: Provider) => void;
|
||||||
onEdit: (provider: Provider) => void;
|
onEdit: (provider: Provider) => void;
|
||||||
onDelete: (provider: Provider) => void;
|
onDelete: (provider: Provider) => void;
|
||||||
/** OpenCode: remove from live config (not delete from database) */
|
|
||||||
onRemoveFromConfig?: (provider: Provider) => void;
|
onRemoveFromConfig?: (provider: Provider) => void;
|
||||||
|
onDisableOmo?: () => void;
|
||||||
onDuplicate: (provider: Provider) => void;
|
onDuplicate: (provider: Provider) => void;
|
||||||
onConfigureUsage?: (provider: Provider) => void;
|
onConfigureUsage?: (provider: Provider) => void;
|
||||||
onOpenWebsite: (url: string) => void;
|
onOpenWebsite: (url: string) => void;
|
||||||
@@ -61,6 +61,7 @@ export function ProviderList({
|
|||||||
onEdit,
|
onEdit,
|
||||||
onDelete,
|
onDelete,
|
||||||
onRemoveFromConfig,
|
onRemoveFromConfig,
|
||||||
|
onDisableOmo,
|
||||||
onDuplicate,
|
onDuplicate,
|
||||||
onConfigureUsage,
|
onConfigureUsage,
|
||||||
onOpenWebsite,
|
onOpenWebsite,
|
||||||
@@ -77,14 +78,12 @@ export function ProviderList({
|
|||||||
appId,
|
appId,
|
||||||
);
|
);
|
||||||
|
|
||||||
// OpenCode: 查询 live 配置中的供应商 ID 列表,用于判断 isInConfig
|
|
||||||
const { data: opencodeLiveIds } = useQuery({
|
const { data: opencodeLiveIds } = useQuery({
|
||||||
queryKey: ["opencodeLiveProviderIds"],
|
queryKey: ["opencodeLiveProviderIds"],
|
||||||
queryFn: () => providersApi.getOpenCodeLiveProviderIds(),
|
queryFn: () => providersApi.getOpenCodeLiveProviderIds(),
|
||||||
enabled: appId === "opencode",
|
enabled: appId === "opencode",
|
||||||
});
|
});
|
||||||
|
|
||||||
// OpenCode: 判断供应商是否已添加到 opencode.json
|
|
||||||
const isProviderInConfig = useCallback(
|
const isProviderInConfig = useCallback(
|
||||||
(providerId: string): boolean => {
|
(providerId: string): boolean => {
|
||||||
if (appId !== "opencode") return true; // 非 OpenCode 应用始终返回 true
|
if (appId !== "opencode") return true; // 非 OpenCode 应用始终返回 true
|
||||||
@@ -93,20 +92,18 @@ export function ProviderList({
|
|||||||
[appId, opencodeLiveIds],
|
[appId, opencodeLiveIds],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 流式健康检查 - 功能已隐藏
|
|
||||||
// const { checkProvider, isChecking } = useStreamCheck(appId);
|
|
||||||
|
|
||||||
// 故障转移相关
|
|
||||||
const { data: isAutoFailoverEnabled } = useAutoFailoverEnabled(appId);
|
const { data: isAutoFailoverEnabled } = useAutoFailoverEnabled(appId);
|
||||||
const { data: failoverQueue } = useFailoverQueue(appId);
|
const { data: failoverQueue } = useFailoverQueue(appId);
|
||||||
const addToQueue = useAddToFailoverQueue();
|
const addToQueue = useAddToFailoverQueue();
|
||||||
const removeFromQueue = useRemoveFromFailoverQueue();
|
const removeFromQueue = useRemoveFromFailoverQueue();
|
||||||
|
|
||||||
// 联动状态:只有当前应用开启代理接管且故障转移开启时才启用故障转移模式
|
|
||||||
const isFailoverModeActive =
|
const isFailoverModeActive =
|
||||||
isProxyTakeover === true && isAutoFailoverEnabled === true;
|
isProxyTakeover === true && isAutoFailoverEnabled === true;
|
||||||
|
|
||||||
// 计算供应商在故障转移队列中的优先级(基于 sortIndex 排序)
|
const isOpenCode = appId === "opencode";
|
||||||
|
const { data: currentOmoId } = useCurrentOmoProviderId(isOpenCode);
|
||||||
|
const { data: omoProviderCount } = useOmoProviderCount(isOpenCode);
|
||||||
|
|
||||||
const getFailoverPriority = useCallback(
|
const getFailoverPriority = useCallback(
|
||||||
(providerId: string): number | undefined => {
|
(providerId: string): number | undefined => {
|
||||||
if (!isFailoverModeActive || !failoverQueue) return undefined;
|
if (!isFailoverModeActive || !failoverQueue) return undefined;
|
||||||
@@ -118,7 +115,6 @@ export function ProviderList({
|
|||||||
[isFailoverModeActive, failoverQueue],
|
[isFailoverModeActive, failoverQueue],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 判断供应商是否在故障转移队列中
|
|
||||||
const isInFailoverQueue = useCallback(
|
const isInFailoverQueue = useCallback(
|
||||||
(providerId: string): boolean => {
|
(providerId: string): boolean => {
|
||||||
if (!isFailoverModeActive || !failoverQueue) return false;
|
if (!isFailoverModeActive || !failoverQueue) return false;
|
||||||
@@ -127,7 +123,6 @@ export function ProviderList({
|
|||||||
[isFailoverModeActive, failoverQueue],
|
[isFailoverModeActive, failoverQueue],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 切换供应商的故障转移队列状态
|
|
||||||
const handleToggleFailover = useCallback(
|
const handleToggleFailover = useCallback(
|
||||||
(providerId: string, enabled: boolean) => {
|
(providerId: string, enabled: boolean) => {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
@@ -139,11 +134,6 @@ export function ProviderList({
|
|||||||
[appId, addToQueue, removeFromQueue],
|
[appId, addToQueue, removeFromQueue],
|
||||||
);
|
);
|
||||||
|
|
||||||
// handleTest 功能已隐藏 - 供应商请求格式复杂难以统一测试
|
|
||||||
// const handleTest = (provider: Provider) => {
|
|
||||||
// checkProvider(provider.id, provider.name);
|
|
||||||
// };
|
|
||||||
|
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
||||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||||
@@ -215,36 +205,44 @@ export function ProviderList({
|
|||||||
strategy={verticalListSortingStrategy}
|
strategy={verticalListSortingStrategy}
|
||||||
>
|
>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{filteredProviders.map((provider) => (
|
{filteredProviders.map((provider) => {
|
||||||
<SortableProviderCard
|
const isOmo = provider.category === "omo";
|
||||||
key={provider.id}
|
const isOmoCurrent = isOmo && provider.id === (currentOmoId || "");
|
||||||
provider={provider}
|
return (
|
||||||
isCurrent={provider.id === currentProviderId}
|
<SortableProviderCard
|
||||||
appId={appId}
|
key={provider.id}
|
||||||
isInConfig={isProviderInConfig(provider.id)}
|
provider={provider}
|
||||||
onSwitch={onSwitch}
|
isCurrent={
|
||||||
onEdit={onEdit}
|
isOmo ? isOmoCurrent : provider.id === currentProviderId
|
||||||
onDelete={onDelete}
|
}
|
||||||
onRemoveFromConfig={onRemoveFromConfig}
|
appId={appId}
|
||||||
onDuplicate={onDuplicate}
|
isInConfig={isProviderInConfig(provider.id)}
|
||||||
onConfigureUsage={onConfigureUsage}
|
isOmo={isOmo}
|
||||||
onOpenWebsite={onOpenWebsite}
|
isLastOmo={
|
||||||
onOpenTerminal={onOpenTerminal}
|
isOmo && (omoProviderCount ?? 0) <= 1 && isOmoCurrent
|
||||||
// onTest 功能已隐藏 - 供应商请求格式复杂难以统一测试
|
}
|
||||||
// onTest={appId !== "opencode" ? handleTest : undefined}
|
onSwitch={onSwitch}
|
||||||
isTesting={false} // isChecking(provider.id) - 测试功能已隐藏
|
onEdit={onEdit}
|
||||||
isProxyRunning={isProxyRunning}
|
onDelete={onDelete}
|
||||||
isProxyTakeover={isProxyTakeover}
|
onRemoveFromConfig={onRemoveFromConfig}
|
||||||
// 故障转移相关:联动状态
|
onDisableOmo={onDisableOmo}
|
||||||
isAutoFailoverEnabled={isFailoverModeActive}
|
onDuplicate={onDuplicate}
|
||||||
failoverPriority={getFailoverPriority(provider.id)}
|
onConfigureUsage={onConfigureUsage}
|
||||||
isInFailoverQueue={isInFailoverQueue(provider.id)}
|
onOpenWebsite={onOpenWebsite}
|
||||||
onToggleFailover={(enabled) =>
|
onOpenTerminal={onOpenTerminal}
|
||||||
handleToggleFailover(provider.id, enabled)
|
isTesting={false} // isChecking(provider.id) - 测试功能已隐藏
|
||||||
}
|
isProxyRunning={isProxyRunning}
|
||||||
activeProviderId={activeProviderId}
|
isProxyTakeover={isProxyTakeover}
|
||||||
/>
|
isAutoFailoverEnabled={isFailoverModeActive}
|
||||||
))}
|
failoverPriority={getFailoverPriority(provider.id)}
|
||||||
|
isInFailoverQueue={isInFailoverQueue(provider.id)}
|
||||||
|
onToggleFailover={(enabled) =>
|
||||||
|
handleToggleFailover(provider.id, enabled)
|
||||||
|
}
|
||||||
|
activeProviderId={activeProviderId}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
</DndContext>
|
</DndContext>
|
||||||
@@ -334,11 +332,13 @@ interface SortableProviderCardProps {
|
|||||||
isCurrent: boolean;
|
isCurrent: boolean;
|
||||||
appId: AppId;
|
appId: AppId;
|
||||||
isInConfig: boolean;
|
isInConfig: boolean;
|
||||||
|
isOmo: boolean;
|
||||||
|
isLastOmo: boolean;
|
||||||
onSwitch: (provider: Provider) => void;
|
onSwitch: (provider: Provider) => void;
|
||||||
onEdit: (provider: Provider) => void;
|
onEdit: (provider: Provider) => void;
|
||||||
onDelete: (provider: Provider) => void;
|
onDelete: (provider: Provider) => void;
|
||||||
/** OpenCode: remove from live config (not delete from database) */
|
|
||||||
onRemoveFromConfig?: (provider: Provider) => void;
|
onRemoveFromConfig?: (provider: Provider) => void;
|
||||||
|
onDisableOmo?: () => void;
|
||||||
onDuplicate: (provider: Provider) => void;
|
onDuplicate: (provider: Provider) => void;
|
||||||
onConfigureUsage?: (provider: Provider) => void;
|
onConfigureUsage?: (provider: Provider) => void;
|
||||||
onOpenWebsite: (url: string) => void;
|
onOpenWebsite: (url: string) => void;
|
||||||
@@ -347,7 +347,6 @@ interface SortableProviderCardProps {
|
|||||||
isTesting: boolean;
|
isTesting: boolean;
|
||||||
isProxyRunning: boolean;
|
isProxyRunning: boolean;
|
||||||
isProxyTakeover: boolean;
|
isProxyTakeover: boolean;
|
||||||
// 故障转移相关
|
|
||||||
isAutoFailoverEnabled: boolean;
|
isAutoFailoverEnabled: boolean;
|
||||||
failoverPriority?: number;
|
failoverPriority?: number;
|
||||||
isInFailoverQueue: boolean;
|
isInFailoverQueue: boolean;
|
||||||
@@ -360,10 +359,13 @@ function SortableProviderCard({
|
|||||||
isCurrent,
|
isCurrent,
|
||||||
appId,
|
appId,
|
||||||
isInConfig,
|
isInConfig,
|
||||||
|
isOmo,
|
||||||
|
isLastOmo,
|
||||||
onSwitch,
|
onSwitch,
|
||||||
onEdit,
|
onEdit,
|
||||||
onDelete,
|
onDelete,
|
||||||
onRemoveFromConfig,
|
onRemoveFromConfig,
|
||||||
|
onDisableOmo,
|
||||||
onDuplicate,
|
onDuplicate,
|
||||||
onConfigureUsage,
|
onConfigureUsage,
|
||||||
onOpenWebsite,
|
onOpenWebsite,
|
||||||
@@ -399,10 +401,13 @@ function SortableProviderCard({
|
|||||||
isCurrent={isCurrent}
|
isCurrent={isCurrent}
|
||||||
appId={appId}
|
appId={appId}
|
||||||
isInConfig={isInConfig}
|
isInConfig={isInConfig}
|
||||||
|
isOmo={isOmo}
|
||||||
|
isLastOmo={isLastOmo}
|
||||||
onSwitch={onSwitch}
|
onSwitch={onSwitch}
|
||||||
onEdit={onEdit}
|
onEdit={onEdit}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
onRemoveFromConfig={onRemoveFromConfig}
|
onRemoveFromConfig={onRemoveFromConfig}
|
||||||
|
onDisableOmo={onDisableOmo}
|
||||||
onDuplicate={onDuplicate}
|
onDuplicate={onDuplicate}
|
||||||
onConfigureUsage={
|
onConfigureUsage={
|
||||||
onConfigureUsage ? (item) => onConfigureUsage(item) : () => undefined
|
onConfigureUsage ? (item) => onConfigureUsage(item) : () => undefined
|
||||||
@@ -418,7 +423,6 @@ function SortableProviderCard({
|
|||||||
listeners,
|
listeners,
|
||||||
isDragging,
|
isDragging,
|
||||||
}}
|
}}
|
||||||
// 故障转移相关
|
|
||||||
isAutoFailoverEnabled={isAutoFailoverEnabled}
|
isAutoFailoverEnabled={isAutoFailoverEnabled}
|
||||||
failoverPriority={failoverPriority}
|
failoverPriority={failoverPriority}
|
||||||
isInFailoverQueue={isInFailoverQueue}
|
isInFailoverQueue={isInFailoverQueue}
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { FullScreenPanel } from "@/components/common/FullScreenPanel";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Save, FolderInput, Loader2 } from "lucide-react";
|
||||||
|
import JsonEditor from "@/components/JsonEditor";
|
||||||
|
import {
|
||||||
|
OmoGlobalConfigFields,
|
||||||
|
type OmoGlobalConfigFieldsRef,
|
||||||
|
} from "./OmoGlobalConfigFields";
|
||||||
|
import type { OmoGlobalConfig } from "@/types/omo";
|
||||||
|
|
||||||
|
interface OmoCommonConfigEditorProps {
|
||||||
|
previewValue: string;
|
||||||
|
useCommonConfig: boolean;
|
||||||
|
onCommonConfigToggle: (checked: boolean) => void;
|
||||||
|
isModalOpen: boolean;
|
||||||
|
onEditClick: () => void;
|
||||||
|
onModalClose: () => void;
|
||||||
|
onSave: () => Promise<void>;
|
||||||
|
isSaving: boolean;
|
||||||
|
onGlobalConfigStateChange: (config: OmoGlobalConfig) => void;
|
||||||
|
globalConfigRef: React.RefObject<OmoGlobalConfigFieldsRef | null>;
|
||||||
|
fieldsKey: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function OmoCommonConfigEditor({
|
||||||
|
previewValue,
|
||||||
|
useCommonConfig,
|
||||||
|
onCommonConfigToggle,
|
||||||
|
isModalOpen,
|
||||||
|
onEditClick,
|
||||||
|
onModalClose,
|
||||||
|
onSave,
|
||||||
|
isSaving,
|
||||||
|
onGlobalConfigStateChange,
|
||||||
|
globalConfigRef,
|
||||||
|
fieldsKey,
|
||||||
|
}: OmoCommonConfigEditorProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [isDarkMode, setIsDarkMode] = useState(false);
|
||||||
|
const [isImporting, setIsImporting] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
const syncDarkMode = () =>
|
||||||
|
setIsDarkMode(document.documentElement.classList.contains("dark"));
|
||||||
|
syncDarkMode();
|
||||||
|
const observer = new MutationObserver(syncDarkMode);
|
||||||
|
observer.observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ["class"],
|
||||||
|
});
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, []);
|
||||||
|
const handleImportLocal = async () => {
|
||||||
|
if (!globalConfigRef.current) return;
|
||||||
|
setIsImporting(true);
|
||||||
|
try {
|
||||||
|
await globalConfigRef.current.importFromLocal();
|
||||||
|
} finally {
|
||||||
|
setIsImporting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label>{t("provider.configJson")}</Label>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<label className="inline-flex items-center gap-2 text-sm text-muted-foreground cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={useCommonConfig}
|
||||||
|
onChange={(e) => onCommonConfigToggle(e.target.checked)}
|
||||||
|
className="w-4 h-4 text-blue-500 bg-white dark:bg-gray-800 border-border-default rounded focus:ring-blue-500 dark:focus:ring-blue-400 focus:ring-2"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{t("omo.writeCommonConfig", {
|
||||||
|
defaultValue: "Write to common config",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onEditClick}
|
||||||
|
className="text-xs text-blue-400 dark:text-blue-500 hover:text-blue-500 dark:hover:text-blue-400 transition-colors"
|
||||||
|
>
|
||||||
|
{t("omo.editCommonConfig", { defaultValue: "Edit common config" })}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<JsonEditor
|
||||||
|
value={previewValue}
|
||||||
|
onChange={() => {}}
|
||||||
|
darkMode={isDarkMode}
|
||||||
|
rows={14}
|
||||||
|
showValidation={false}
|
||||||
|
language="json"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<FullScreenPanel
|
||||||
|
isOpen={isModalOpen}
|
||||||
|
title={t("omo.editCommonConfigTitle", {
|
||||||
|
defaultValue: "Edit OMO Common Config",
|
||||||
|
})}
|
||||||
|
onClose={onModalClose}
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleImportLocal}
|
||||||
|
disabled={isImporting}
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
{isImporting ? (
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<FolderInput className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
{t("common.import", { defaultValue: "Import" })}
|
||||||
|
</Button>
|
||||||
|
<Button type="button" variant="outline" onClick={onModalClose}>
|
||||||
|
{t("common.cancel")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={onSave}
|
||||||
|
disabled={isSaving}
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
{isSaving ? (
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Save className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
{t("common.save")}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{t("omo.commonConfigHint", {
|
||||||
|
defaultValue:
|
||||||
|
"OMO common config will be merged into all OMO configs that enable it",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
<OmoGlobalConfigFields
|
||||||
|
key={fieldsKey}
|
||||||
|
ref={globalConfigRef as React.Ref<OmoGlobalConfigFieldsRef>}
|
||||||
|
onStateChange={onGlobalConfigStateChange}
|
||||||
|
hideSaveButtons
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FullScreenPanel>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,739 @@
|
|||||||
|
import {
|
||||||
|
useState,
|
||||||
|
useEffect,
|
||||||
|
useCallback,
|
||||||
|
forwardRef,
|
||||||
|
useImperativeHandle,
|
||||||
|
} from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import {
|
||||||
|
Save,
|
||||||
|
Loader2,
|
||||||
|
X,
|
||||||
|
FolderInput,
|
||||||
|
RotateCcw,
|
||||||
|
ChevronsUpDown,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import type { OmoGlobalConfig } from "@/types/omo";
|
||||||
|
import {
|
||||||
|
OMO_DISABLEABLE_AGENTS,
|
||||||
|
OMO_DISABLEABLE_MCPS,
|
||||||
|
OMO_DISABLEABLE_HOOKS,
|
||||||
|
OMO_DISABLEABLE_SKILLS,
|
||||||
|
OMO_DEFAULT_SCHEMA_URL,
|
||||||
|
OMO_SISYPHUS_AGENT_PLACEHOLDER,
|
||||||
|
OMO_LSP_PLACEHOLDER,
|
||||||
|
OMO_EXPERIMENTAL_PLACEHOLDER,
|
||||||
|
OMO_BACKGROUND_TASK_PLACEHOLDER,
|
||||||
|
OMO_BROWSER_AUTOMATION_PLACEHOLDER,
|
||||||
|
OMO_CLAUDE_CODE_PLACEHOLDER,
|
||||||
|
} from "@/types/omo";
|
||||||
|
import {
|
||||||
|
useOmoGlobalConfig,
|
||||||
|
useSaveOmoGlobalConfig,
|
||||||
|
useReadOmoLocalFile,
|
||||||
|
} from "@/lib/query/omo";
|
||||||
|
|
||||||
|
interface PresetOption {
|
||||||
|
readonly value: string;
|
||||||
|
readonly label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OmoGlobalConfigFieldsRef {
|
||||||
|
buildCurrentConfig: () => OmoGlobalConfig;
|
||||||
|
buildCurrentConfigStrict: () => OmoGlobalConfig;
|
||||||
|
importFromLocal: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OmoGlobalConfigFieldsProps {
|
||||||
|
onStateChange?: (config: OmoGlobalConfig) => void;
|
||||||
|
hideSaveButtons?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
type OmoAdvancedFieldKey =
|
||||||
|
| "lspStr"
|
||||||
|
| "experimentalStr"
|
||||||
|
| "backgroundTaskStr"
|
||||||
|
| "browserStr"
|
||||||
|
| "claudeCodeStr";
|
||||||
|
|
||||||
|
const OMO_ADVANCED_JSON_FIELDS: ReadonlyArray<{
|
||||||
|
key: OmoAdvancedFieldKey;
|
||||||
|
labelKey: string;
|
||||||
|
defaultLabel: string;
|
||||||
|
placeholder: string;
|
||||||
|
minHeight: string;
|
||||||
|
}> = [
|
||||||
|
{
|
||||||
|
key: "lspStr",
|
||||||
|
labelKey: "omo.advancedLsp",
|
||||||
|
defaultLabel: "LSP Config",
|
||||||
|
placeholder: OMO_LSP_PLACEHOLDER,
|
||||||
|
minHeight: "200px",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "experimentalStr",
|
||||||
|
labelKey: "omo.advancedExperimental",
|
||||||
|
defaultLabel: "Experimental Features",
|
||||||
|
placeholder: OMO_EXPERIMENTAL_PLACEHOLDER,
|
||||||
|
minHeight: "120px",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "backgroundTaskStr",
|
||||||
|
labelKey: "omo.advancedBackgroundTask",
|
||||||
|
defaultLabel: "Background Tasks",
|
||||||
|
placeholder: OMO_BACKGROUND_TASK_PLACEHOLDER,
|
||||||
|
minHeight: "250px",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "browserStr",
|
||||||
|
labelKey: "omo.advancedBrowserAutomation",
|
||||||
|
defaultLabel: "Browser Automation",
|
||||||
|
placeholder: OMO_BROWSER_AUTOMATION_PLACEHOLDER,
|
||||||
|
minHeight: "80px",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "claudeCodeStr",
|
||||||
|
labelKey: "omo.advancedClaudeCode",
|
||||||
|
defaultLabel: "Claude Code",
|
||||||
|
placeholder: OMO_CLAUDE_CODE_PLACEHOLDER,
|
||||||
|
minHeight: "180px",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function TagListEditor({
|
||||||
|
label,
|
||||||
|
values,
|
||||||
|
onChange,
|
||||||
|
placeholder,
|
||||||
|
presets,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
values: string[];
|
||||||
|
onChange: (values: string[]) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
presets?: readonly PresetOption[];
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
|
const toggleValue = (v: string) => {
|
||||||
|
if (values.includes(v)) {
|
||||||
|
onChange(values.filter((x) => x !== v));
|
||||||
|
} else {
|
||||||
|
onChange([...values, v]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const customValue = search.trim();
|
||||||
|
const canAddCustom = customValue.length > 0 && !values.includes(customValue);
|
||||||
|
const triggerText =
|
||||||
|
values.length === 0
|
||||||
|
? placeholder || t("omo.selectPlaceholder", { defaultValue: "Select..." })
|
||||||
|
: values.length === 1
|
||||||
|
? values[0]
|
||||||
|
: `${values[0]} +${values.length - 1}`;
|
||||||
|
|
||||||
|
const availablePresets = presets?.filter(
|
||||||
|
(p) =>
|
||||||
|
!search.trim() ||
|
||||||
|
p.label.toLowerCase().includes(search.toLowerCase()) ||
|
||||||
|
p.value.toLowerCase().includes(search.toLowerCase()),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label className="text-sm">{label}</Label>
|
||||||
|
{values.length > 0 && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 px-1.5 text-xs text-muted-foreground"
|
||||||
|
onClick={() => onChange([])}
|
||||||
|
>
|
||||||
|
{t("omo.clear", { defaultValue: "Clear" })}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{values.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{values.map((v, i) => (
|
||||||
|
<Badge
|
||||||
|
key={`${v}-${i}`}
|
||||||
|
variant="secondary"
|
||||||
|
className="text-xs gap-1"
|
||||||
|
>
|
||||||
|
{v}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onChange(values.filter((_, idx) => idx !== i))}
|
||||||
|
className="hover:text-destructive"
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<DropdownMenu open={open} onOpenChange={setOpen} modal={false}>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={cn(
|
||||||
|
"flex items-center justify-between w-full h-8 px-3 rounded-md border border-input bg-background text-sm",
|
||||||
|
"hover:bg-accent hover:text-accent-foreground transition-colors",
|
||||||
|
open && "ring-2 ring-ring",
|
||||||
|
)}
|
||||||
|
aria-expanded={open}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"truncate",
|
||||||
|
values.length > 0 ? "text-foreground" : "text-muted-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{triggerText}
|
||||||
|
</span>
|
||||||
|
<ChevronsUpDown className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||||
|
</button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
align="start"
|
||||||
|
sideOffset={6}
|
||||||
|
className="w-[var(--radix-dropdown-menu-trigger-width)] p-0 z-[120]"
|
||||||
|
>
|
||||||
|
<div className="p-1.5 border-b border-border/30">
|
||||||
|
<Input
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (e.key === "Enter" && canAddCustom) {
|
||||||
|
e.preventDefault();
|
||||||
|
onChange([...values, customValue]);
|
||||||
|
setSearch("");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder={
|
||||||
|
placeholder ||
|
||||||
|
t("omo.searchOrType", {
|
||||||
|
defaultValue: "Search or type custom value...",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="h-7 text-sm"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{canAddCustom && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="w-full px-2.5 py-1.5 text-left text-sm border-b border-border/30 hover:bg-accent"
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
|
onClick={() => {
|
||||||
|
onChange([...values, customValue]);
|
||||||
|
setSearch("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
+ {customValue}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<div className="max-h-48 overflow-auto py-1">
|
||||||
|
{availablePresets && availablePresets.length > 0 ? (
|
||||||
|
availablePresets.map((p) => {
|
||||||
|
const checked = values.includes(p.value);
|
||||||
|
return (
|
||||||
|
<DropdownMenuCheckboxItem
|
||||||
|
key={p.value}
|
||||||
|
checked={checked}
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
onCheckedChange={() => toggleValue(p.value)}
|
||||||
|
className="text-sm"
|
||||||
|
>
|
||||||
|
{p.label}
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<div className="px-2.5 py-2 text-sm text-muted-foreground">
|
||||||
|
{t("omo.noMatches", { defaultValue: "No matches" })}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function JsonTextareaField({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
placeholder,
|
||||||
|
minHeight,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
minHeight?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label className="text-sm">{label}</Label>
|
||||||
|
<Textarea
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
placeholder={placeholder || "{}"}
|
||||||
|
className="font-mono text-sm"
|
||||||
|
style={{ minHeight: minHeight || "100px" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OmoGlobalConfigFields = forwardRef<
|
||||||
|
OmoGlobalConfigFieldsRef,
|
||||||
|
OmoGlobalConfigFieldsProps
|
||||||
|
>(function OmoGlobalConfigFields({ onStateChange, hideSaveButtons }, ref) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { data: config } = useOmoGlobalConfig();
|
||||||
|
const saveMutation = useSaveOmoGlobalConfig();
|
||||||
|
|
||||||
|
const [schemaUrl, setSchemaUrl] = useState(OMO_DEFAULT_SCHEMA_URL);
|
||||||
|
const [sisyphusAgentStr, setSisyphusAgentStr] = useState("");
|
||||||
|
const [disabledAgents, setDisabledAgents] = useState<string[]>([]);
|
||||||
|
const [disabledMcps, setDisabledMcps] = useState<string[]>([]);
|
||||||
|
const [disabledHooks, setDisabledHooks] = useState<string[]>([]);
|
||||||
|
const [disabledSkills, setDisabledSkills] = useState<string[]>([]);
|
||||||
|
const [lspStr, setLspStr] = useState("");
|
||||||
|
const [experimentalStr, setExperimentalStr] = useState("");
|
||||||
|
const [backgroundTaskStr, setBackgroundTaskStr] = useState("");
|
||||||
|
const [browserStr, setBrowserStr] = useState("");
|
||||||
|
const [claudeCodeStr, setClaudeCodeStr] = useState("");
|
||||||
|
const [otherFieldsStr, setOtherFieldsStr] = useState("");
|
||||||
|
const [loaded, setLoaded] = useState(false);
|
||||||
|
|
||||||
|
const applyGlobalState = useCallback((global: OmoGlobalConfig) => {
|
||||||
|
setSchemaUrl(global.schemaUrl || OMO_DEFAULT_SCHEMA_URL);
|
||||||
|
setSisyphusAgentStr(
|
||||||
|
global.sisyphusAgent ? JSON.stringify(global.sisyphusAgent, null, 2) : "",
|
||||||
|
);
|
||||||
|
setDisabledAgents(global.disabledAgents || []);
|
||||||
|
setDisabledMcps(global.disabledMcps || []);
|
||||||
|
setDisabledHooks(global.disabledHooks || []);
|
||||||
|
setDisabledSkills(global.disabledSkills || []);
|
||||||
|
setLspStr(global.lsp ? JSON.stringify(global.lsp, null, 2) : "");
|
||||||
|
setExperimentalStr(
|
||||||
|
global.experimental ? JSON.stringify(global.experimental, null, 2) : "",
|
||||||
|
);
|
||||||
|
setBackgroundTaskStr(
|
||||||
|
global.backgroundTask
|
||||||
|
? JSON.stringify(global.backgroundTask, null, 2)
|
||||||
|
: "",
|
||||||
|
);
|
||||||
|
setBrowserStr(
|
||||||
|
global.browserAutomationEngine
|
||||||
|
? JSON.stringify(global.browserAutomationEngine, null, 2)
|
||||||
|
: "",
|
||||||
|
);
|
||||||
|
setClaudeCodeStr(
|
||||||
|
global.claudeCode ? JSON.stringify(global.claudeCode, null, 2) : "",
|
||||||
|
);
|
||||||
|
setOtherFieldsStr(
|
||||||
|
global.otherFields ? JSON.stringify(global.otherFields, null, 2) : "",
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (config && !loaded) {
|
||||||
|
applyGlobalState(config);
|
||||||
|
setLoaded(true);
|
||||||
|
}
|
||||||
|
}, [config, loaded, applyGlobalState]);
|
||||||
|
|
||||||
|
const parseJsonField = useCallback(
|
||||||
|
(
|
||||||
|
fieldName: string,
|
||||||
|
raw: string,
|
||||||
|
strict: boolean,
|
||||||
|
): Record<string, unknown> | undefined => {
|
||||||
|
if (!raw.trim()) return undefined;
|
||||||
|
try {
|
||||||
|
const parsed: unknown = JSON.parse(raw);
|
||||||
|
if (
|
||||||
|
typeof parsed !== "object" ||
|
||||||
|
parsed === null ||
|
||||||
|
Array.isArray(parsed)
|
||||||
|
) {
|
||||||
|
if (strict) {
|
||||||
|
throw new Error(
|
||||||
|
t("omo.jsonMustBeObject", {
|
||||||
|
field: fieldName,
|
||||||
|
defaultValue: "{{field}} must be a JSON object",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return parsed as Record<string, unknown>;
|
||||||
|
} catch (error) {
|
||||||
|
if (strict) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
t("omo.jsonInvalid", {
|
||||||
|
field: fieldName,
|
||||||
|
defaultValue: "{{field}} contains invalid JSON",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[t],
|
||||||
|
);
|
||||||
|
|
||||||
|
const buildCurrentConfigInternal = useCallback(
|
||||||
|
(strict: boolean): OmoGlobalConfig => {
|
||||||
|
return {
|
||||||
|
id: "global",
|
||||||
|
schemaUrl: schemaUrl || undefined,
|
||||||
|
sisyphusAgent: parseJsonField(
|
||||||
|
t("omo.sisyphusAgentConfig", {
|
||||||
|
defaultValue: "Sisyphus Agent",
|
||||||
|
}),
|
||||||
|
sisyphusAgentStr,
|
||||||
|
strict,
|
||||||
|
),
|
||||||
|
disabledAgents,
|
||||||
|
disabledMcps,
|
||||||
|
disabledHooks,
|
||||||
|
disabledSkills,
|
||||||
|
lsp: parseJsonField(
|
||||||
|
t("omo.advancedLsp", { defaultValue: "LSP" }),
|
||||||
|
lspStr,
|
||||||
|
strict,
|
||||||
|
),
|
||||||
|
experimental: parseJsonField(
|
||||||
|
t("omo.advancedExperimental", { defaultValue: "Experimental" }),
|
||||||
|
experimentalStr,
|
||||||
|
strict,
|
||||||
|
),
|
||||||
|
backgroundTask: parseJsonField(
|
||||||
|
t("omo.advancedBackgroundTask", {
|
||||||
|
defaultValue: "Background Task",
|
||||||
|
}),
|
||||||
|
backgroundTaskStr,
|
||||||
|
strict,
|
||||||
|
),
|
||||||
|
browserAutomationEngine: parseJsonField(
|
||||||
|
t("omo.advancedBrowserAutomation", {
|
||||||
|
defaultValue: "Browser Automation",
|
||||||
|
}),
|
||||||
|
browserStr,
|
||||||
|
strict,
|
||||||
|
),
|
||||||
|
claudeCode: parseJsonField(
|
||||||
|
t("omo.advancedClaudeCode", { defaultValue: "Claude Code" }),
|
||||||
|
claudeCodeStr,
|
||||||
|
strict,
|
||||||
|
),
|
||||||
|
otherFields: parseJsonField(
|
||||||
|
t("omo.otherFields", {
|
||||||
|
defaultValue: "Other Config",
|
||||||
|
}),
|
||||||
|
otherFieldsStr,
|
||||||
|
strict,
|
||||||
|
),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[
|
||||||
|
schemaUrl,
|
||||||
|
sisyphusAgentStr,
|
||||||
|
disabledAgents,
|
||||||
|
disabledMcps,
|
||||||
|
disabledHooks,
|
||||||
|
disabledSkills,
|
||||||
|
lspStr,
|
||||||
|
experimentalStr,
|
||||||
|
backgroundTaskStr,
|
||||||
|
browserStr,
|
||||||
|
claudeCodeStr,
|
||||||
|
otherFieldsStr,
|
||||||
|
parseJsonField,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const buildCurrentConfig = useCallback(
|
||||||
|
() => buildCurrentConfigInternal(false),
|
||||||
|
[buildCurrentConfigInternal],
|
||||||
|
);
|
||||||
|
|
||||||
|
const buildCurrentConfigStrict = useCallback(
|
||||||
|
() => buildCurrentConfigInternal(true),
|
||||||
|
[buildCurrentConfigInternal],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (loaded && onStateChange) {
|
||||||
|
onStateChange(buildCurrentConfig());
|
||||||
|
}
|
||||||
|
}, [loaded, onStateChange, buildCurrentConfig]);
|
||||||
|
|
||||||
|
const handleSaveGlobal = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const result = buildCurrentConfigStrict();
|
||||||
|
await saveMutation.mutateAsync(result);
|
||||||
|
toast.success(
|
||||||
|
t("omo.globalConfigSaved", {
|
||||||
|
defaultValue: "Global config saved",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(String(err));
|
||||||
|
}
|
||||||
|
}, [buildCurrentConfigStrict, saveMutation, t]);
|
||||||
|
|
||||||
|
const disabledCount =
|
||||||
|
disabledAgents.length +
|
||||||
|
disabledMcps.length +
|
||||||
|
disabledHooks.length +
|
||||||
|
disabledSkills.length;
|
||||||
|
const advancedFieldValues: Record<OmoAdvancedFieldKey, string> = {
|
||||||
|
lspStr,
|
||||||
|
experimentalStr,
|
||||||
|
backgroundTaskStr,
|
||||||
|
browserStr,
|
||||||
|
claudeCodeStr,
|
||||||
|
};
|
||||||
|
|
||||||
|
const advancedFieldSetters: Record<
|
||||||
|
OmoAdvancedFieldKey,
|
||||||
|
(value: string) => void
|
||||||
|
> = {
|
||||||
|
lspStr: setLspStr,
|
||||||
|
experimentalStr: setExperimentalStr,
|
||||||
|
backgroundTaskStr: setBackgroundTaskStr,
|
||||||
|
browserStr: setBrowserStr,
|
||||||
|
claudeCodeStr: setClaudeCodeStr,
|
||||||
|
};
|
||||||
|
|
||||||
|
const disabledEditorConfigs = [
|
||||||
|
{
|
||||||
|
key: "agents",
|
||||||
|
label: t("omo.disabledAgents", { defaultValue: "Agents" }),
|
||||||
|
values: disabledAgents,
|
||||||
|
onChange: setDisabledAgents,
|
||||||
|
placeholder: t("omo.disabledAgentsPlaceholder", {
|
||||||
|
defaultValue: "Disabled Agents",
|
||||||
|
}),
|
||||||
|
presets: OMO_DISABLEABLE_AGENTS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "mcps",
|
||||||
|
label: t("omo.disabledMcps", { defaultValue: "MCPs" }),
|
||||||
|
values: disabledMcps,
|
||||||
|
onChange: setDisabledMcps,
|
||||||
|
placeholder: t("omo.disabledMcpsPlaceholder", {
|
||||||
|
defaultValue: "Disabled MCPs",
|
||||||
|
}),
|
||||||
|
presets: OMO_DISABLEABLE_MCPS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "hooks",
|
||||||
|
label: t("omo.disabledHooks", { defaultValue: "Hooks" }),
|
||||||
|
values: disabledHooks,
|
||||||
|
onChange: setDisabledHooks,
|
||||||
|
placeholder: t("omo.disabledHooksPlaceholder", {
|
||||||
|
defaultValue: "Disabled Hooks",
|
||||||
|
}),
|
||||||
|
presets: OMO_DISABLEABLE_HOOKS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "skills",
|
||||||
|
label: t("omo.disabledSkills", { defaultValue: "Skills" }),
|
||||||
|
values: disabledSkills,
|
||||||
|
onChange: setDisabledSkills,
|
||||||
|
placeholder: t("omo.disabledSkillsPlaceholder", {
|
||||||
|
defaultValue: "Disabled Skills",
|
||||||
|
}),
|
||||||
|
presets: OMO_DISABLEABLE_SKILLS,
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const readLocalFile = useReadOmoLocalFile();
|
||||||
|
|
||||||
|
const handleImportGlobalFromLocal = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const data = await readLocalFile.mutateAsync();
|
||||||
|
applyGlobalState(data.global);
|
||||||
|
toast.success(
|
||||||
|
t("omo.importGlobalSuccess", {
|
||||||
|
defaultValue: "Imported global config from local file (unsaved)",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(
|
||||||
|
t("omo.importGlobalFailed", {
|
||||||
|
error: String(err),
|
||||||
|
defaultValue: "Failed to read local file: {{error}}",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [readLocalFile, applyGlobalState, t]);
|
||||||
|
|
||||||
|
useImperativeHandle(
|
||||||
|
ref,
|
||||||
|
() => ({
|
||||||
|
buildCurrentConfig,
|
||||||
|
buildCurrentConfigStrict,
|
||||||
|
importFromLocal: handleImportGlobalFromLocal,
|
||||||
|
}),
|
||||||
|
[buildCurrentConfig, buildCurrentConfigStrict, handleImportGlobalFromLocal],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{!hideSaveButtons && (
|
||||||
|
<div className="flex items-center justify-end gap-1.5">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 text-xs"
|
||||||
|
disabled={readLocalFile.isPending}
|
||||||
|
onClick={handleImportGlobalFromLocal}
|
||||||
|
>
|
||||||
|
{readLocalFile.isPending ? (
|
||||||
|
<Loader2 className="h-3.5 w-3.5 mr-1 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<FolderInput className="h-3.5 w-3.5 mr-1" />
|
||||||
|
)}
|
||||||
|
{t("omo.importLocal", { defaultValue: "Import Local" })}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 text-xs"
|
||||||
|
disabled={saveMutation.isPending}
|
||||||
|
onClick={handleSaveGlobal}
|
||||||
|
>
|
||||||
|
{saveMutation.isPending ? (
|
||||||
|
<Loader2 className="h-3.5 w-3.5 mr-1 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Save className="h-3.5 w-3.5 mr-1" />
|
||||||
|
)}
|
||||||
|
{t("omo.saveGlobalConfig", { defaultValue: "Save Global Config" })}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label className="text-sm">
|
||||||
|
{t("omo.schemaUrl", { defaultValue: "$schema" })}
|
||||||
|
</Label>
|
||||||
|
{schemaUrl !== OMO_DEFAULT_SCHEMA_URL && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 text-xs px-1.5"
|
||||||
|
onClick={() => setSchemaUrl(OMO_DEFAULT_SCHEMA_URL)}
|
||||||
|
>
|
||||||
|
<RotateCcw className="h-3 w-3 mr-0.5" />
|
||||||
|
{t("omo.resetDefault", { defaultValue: "Reset" })}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
value={schemaUrl}
|
||||||
|
onChange={(e) => setSchemaUrl(e.target.value)}
|
||||||
|
placeholder={OMO_DEFAULT_SCHEMA_URL}
|
||||||
|
className="text-sm h-8"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-md border border-border/40 bg-muted/10 p-2 space-y-2">
|
||||||
|
<Label className="text-sm font-semibold">
|
||||||
|
{t("omo.sisyphusAgentConfig", {
|
||||||
|
defaultValue: "Sisyphus Agent",
|
||||||
|
})}
|
||||||
|
</Label>
|
||||||
|
<Textarea
|
||||||
|
value={sisyphusAgentStr}
|
||||||
|
onChange={(e) => setSisyphusAgentStr(e.target.value)}
|
||||||
|
placeholder={OMO_SISYPHUS_AGENT_PLACEHOLDER}
|
||||||
|
className="font-mono text-sm"
|
||||||
|
style={{ minHeight: "140px" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-md border border-border/40 bg-muted/10 p-2 space-y-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Label className="text-sm font-semibold">
|
||||||
|
{t("omo.disabledItems", { defaultValue: "Disabled Items" })}
|
||||||
|
</Label>
|
||||||
|
{disabledCount > 0 && (
|
||||||
|
<Badge variant="secondary" className="text-xs h-5">
|
||||||
|
{disabledCount}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{disabledEditorConfigs.map((editor) => (
|
||||||
|
<TagListEditor
|
||||||
|
key={editor.key}
|
||||||
|
label={editor.label}
|
||||||
|
values={editor.values}
|
||||||
|
onChange={editor.onChange}
|
||||||
|
placeholder={editor.placeholder}
|
||||||
|
presets={editor.presets}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-md border border-border/40 bg-muted/10 p-2 space-y-2">
|
||||||
|
<Label className="text-sm font-semibold">
|
||||||
|
{t("omo.advanced", { defaultValue: "Advanced Settings" })}
|
||||||
|
</Label>
|
||||||
|
{OMO_ADVANCED_JSON_FIELDS.map((field) => (
|
||||||
|
<JsonTextareaField
|
||||||
|
key={field.key}
|
||||||
|
label={t(field.labelKey, { defaultValue: field.defaultLabel })}
|
||||||
|
value={advancedFieldValues[field.key]}
|
||||||
|
onChange={advancedFieldSetters[field.key]}
|
||||||
|
placeholder={field.placeholder}
|
||||||
|
minHeight={field.minHeight}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<JsonTextareaField
|
||||||
|
label={t("omo.otherFields", {
|
||||||
|
defaultValue: "Other Config",
|
||||||
|
})}
|
||||||
|
value={otherFieldsStr}
|
||||||
|
onChange={setOtherFieldsStr}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -40,7 +40,6 @@ export function ProviderPresetSelector({
|
|||||||
}: ProviderPresetSelectorProps) {
|
}: ProviderPresetSelectorProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
// 根据分类获取提示文字
|
|
||||||
const getCategoryHint = (): React.ReactNode => {
|
const getCategoryHint = (): React.ReactNode => {
|
||||||
switch (category) {
|
switch (category) {
|
||||||
case "official":
|
case "official":
|
||||||
@@ -63,6 +62,11 @@ export function ProviderPresetSelector({
|
|||||||
return t("providerForm.customApiKeyHint", {
|
return t("providerForm.customApiKeyHint", {
|
||||||
defaultValue: "💡 自定义配置需手动填写所有必要字段",
|
defaultValue: "💡 自定义配置需手动填写所有必要字段",
|
||||||
});
|
});
|
||||||
|
case "omo":
|
||||||
|
return t("providerForm.omoHint", {
|
||||||
|
defaultValue:
|
||||||
|
"💡 OMO 配置管理 Agent 模型分配,写入 oh-my-opencode.jsonc",
|
||||||
|
});
|
||||||
default:
|
default:
|
||||||
return t("providerPreset.hint", {
|
return t("providerPreset.hint", {
|
||||||
defaultValue: "选择预设后可继续调整下方字段。",
|
defaultValue: "选择预设后可继续调整下方字段。",
|
||||||
@@ -70,7 +74,6 @@ export function ProviderPresetSelector({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 渲染预设按钮的图标
|
|
||||||
const renderPresetIcon = (
|
const renderPresetIcon = (
|
||||||
preset: ProviderPreset | CodexProviderPreset | GeminiProviderPreset,
|
preset: ProviderPreset | CodexProviderPreset | GeminiProviderPreset,
|
||||||
) => {
|
) => {
|
||||||
@@ -91,7 +94,6 @@ export function ProviderPresetSelector({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取预设按钮的样式类名
|
|
||||||
const getPresetButtonClass = (
|
const getPresetButtonClass = (
|
||||||
isSelected: boolean,
|
isSelected: boolean,
|
||||||
preset: ProviderPreset | CodexProviderPreset | GeminiProviderPreset,
|
preset: ProviderPreset | CodexProviderPreset | GeminiProviderPreset,
|
||||||
@@ -100,18 +102,15 @@ export function ProviderPresetSelector({
|
|||||||
"inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors";
|
"inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors";
|
||||||
|
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
// 如果有自定义主题,使用自定义颜色
|
|
||||||
if (preset.theme?.backgroundColor) {
|
if (preset.theme?.backgroundColor) {
|
||||||
return `${baseClass} text-white`;
|
return `${baseClass} text-white`;
|
||||||
}
|
}
|
||||||
// 默认使用主题蓝色
|
|
||||||
return `${baseClass} bg-blue-500 text-white dark:bg-blue-600`;
|
return `${baseClass} bg-blue-500 text-white dark:bg-blue-600`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${baseClass} bg-accent text-muted-foreground hover:bg-accent/80`;
|
return `${baseClass} bg-accent text-muted-foreground hover:bg-accent/80`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取预设按钮的内联样式(用于自定义背景色)
|
|
||||||
const getPresetButtonStyle = (
|
const getPresetButtonStyle = (
|
||||||
isSelected: boolean,
|
isSelected: boolean,
|
||||||
preset: ProviderPreset | CodexProviderPreset | GeminiProviderPreset,
|
preset: ProviderPreset | CodexProviderPreset | GeminiProviderPreset,
|
||||||
@@ -130,7 +129,6 @@ export function ProviderPresetSelector({
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<FormLabel>{t("providerPreset.label")}</FormLabel>
|
<FormLabel>{t("providerPreset.label")}</FormLabel>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{/* 自定义按钮 */}
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onPresetChange("custom")}
|
onClick={() => onPresetChange("custom")}
|
||||||
@@ -143,7 +141,6 @@ export function ProviderPresetSelector({
|
|||||||
{t("providerPreset.custom")}
|
{t("providerPreset.custom")}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* 预设按钮 */}
|
|
||||||
{categoryKeys.map((category) => {
|
{categoryKeys.map((category) => {
|
||||||
const entries = groupedPresets[category];
|
const entries = groupedPresets[category];
|
||||||
if (!entries || entries.length === 0) return null;
|
if (!entries || entries.length === 0) return null;
|
||||||
@@ -174,7 +171,6 @@ export function ProviderPresetSelector({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 统一供应商预设(新的一行) */}
|
|
||||||
{onUniversalPresetSelect && universalProviderPresets.length > 0 && (
|
{onUniversalPresetSelect && universalProviderPresets.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
@@ -196,7 +192,6 @@ export function ProviderPresetSelector({
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
{/* 管理统一供应商按钮 */}
|
|
||||||
{onManageUniversalProviders && (
|
{onManageUniversalProviders && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
/**
|
|
||||||
* OpenCode 预设供应商配置模板
|
|
||||||
* OpenCode 使用 AI SDK npm 包,配置结构与其他应用不同
|
|
||||||
*/
|
|
||||||
import type { ProviderCategory, OpenCodeProviderConfig } from "../types";
|
import type { ProviderCategory, OpenCodeProviderConfig } from "../types";
|
||||||
import type { PresetTheme, TemplateValueConfig } from "./claudeProviderPresets";
|
import type { PresetTheme, TemplateValueConfig } from "./claudeProviderPresets";
|
||||||
|
|
||||||
@@ -9,27 +5,18 @@ export interface OpenCodeProviderPreset {
|
|||||||
name: string;
|
name: string;
|
||||||
websiteUrl: string;
|
websiteUrl: string;
|
||||||
apiKeyUrl?: string;
|
apiKeyUrl?: string;
|
||||||
/** OpenCode settings_config 结构 */
|
|
||||||
settingsConfig: OpenCodeProviderConfig;
|
settingsConfig: OpenCodeProviderConfig;
|
||||||
isOfficial?: boolean;
|
isOfficial?: boolean;
|
||||||
isPartner?: boolean;
|
isPartner?: boolean;
|
||||||
partnerPromotionKey?: string;
|
partnerPromotionKey?: string;
|
||||||
category?: ProviderCategory;
|
category?: ProviderCategory;
|
||||||
/** 模板变量定义 */
|
|
||||||
templateValues?: Record<string, TemplateValueConfig>;
|
templateValues?: Record<string, TemplateValueConfig>;
|
||||||
/** 视觉主题配置 */
|
|
||||||
theme?: PresetTheme;
|
theme?: PresetTheme;
|
||||||
/** 图标名称 */
|
|
||||||
icon?: string;
|
icon?: string;
|
||||||
/** 图标颜色 */
|
|
||||||
iconColor?: string;
|
iconColor?: string;
|
||||||
/** 标记为自定义模板(用于 UI 区分) */
|
|
||||||
isCustomTemplate?: boolean;
|
isCustomTemplate?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* OpenCode npm 包选项(AI SDK 生态)
|
|
||||||
*/
|
|
||||||
export const opencodeNpmPackages = [
|
export const opencodeNpmPackages = [
|
||||||
{ value: "@ai-sdk/openai", label: "OpenAI" },
|
{ value: "@ai-sdk/openai", label: "OpenAI" },
|
||||||
{ value: "@ai-sdk/openai-compatible", label: "OpenAI Compatible" },
|
{ value: "@ai-sdk/openai-compatible", label: "OpenAI Compatible" },
|
||||||
@@ -37,11 +24,7 @@ export const opencodeNpmPackages = [
|
|||||||
{ value: "@ai-sdk/google", label: "Google (Gemini)" },
|
{ value: "@ai-sdk/google", label: "Google (Gemini)" },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
/**
|
|
||||||
* OpenCode 供应商预设列表
|
|
||||||
*/
|
|
||||||
export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
||||||
// ========== 国产官方 ==========
|
|
||||||
{
|
{
|
||||||
name: "DeepSeek",
|
name: "DeepSeek",
|
||||||
websiteUrl: "https://platform.deepseek.com",
|
websiteUrl: "https://platform.deepseek.com",
|
||||||
@@ -474,7 +457,6 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// ========== 聚合网站 ==========
|
|
||||||
{
|
{
|
||||||
name: "AiHubMix",
|
name: "AiHubMix",
|
||||||
websiteUrl: "https://aihubmix.com",
|
websiteUrl: "https://aihubmix.com",
|
||||||
@@ -583,7 +565,6 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// ========== 第三方合作伙伴 ==========
|
|
||||||
{
|
{
|
||||||
name: "PackyCode",
|
name: "PackyCode",
|
||||||
websiteUrl: "https://www.packyapi.com",
|
websiteUrl: "https://www.packyapi.com",
|
||||||
@@ -729,7 +710,6 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// ========== 自定义模板 ==========
|
|
||||||
{
|
{
|
||||||
name: "OpenAI Compatible",
|
name: "OpenAI Compatible",
|
||||||
websiteUrl: "",
|
websiteUrl: "",
|
||||||
@@ -758,4 +738,18 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "Oh My OpenCode",
|
||||||
|
websiteUrl: "https://github.com/code-yeongyu/oh-my-opencode",
|
||||||
|
settingsConfig: {
|
||||||
|
npm: "",
|
||||||
|
options: {},
|
||||||
|
models: {},
|
||||||
|
},
|
||||||
|
category: "omo" as ProviderCategory,
|
||||||
|
icon: "opencode",
|
||||||
|
iconColor: "#8B5CF6",
|
||||||
|
isCustomTemplate: true,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
+101
-1
@@ -32,6 +32,7 @@
|
|||||||
"back": "Back",
|
"back": "Back",
|
||||||
"refresh": "Refresh",
|
"refresh": "Refresh",
|
||||||
"refreshing": "Refreshing...",
|
"refreshing": "Refreshing...",
|
||||||
|
"import": "Import",
|
||||||
"all": "All",
|
"all": "All",
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
"reset": "Reset",
|
"reset": "Reset",
|
||||||
@@ -106,6 +107,10 @@
|
|||||||
"duplicate": "Duplicate",
|
"duplicate": "Duplicate",
|
||||||
"sortUpdateFailed": "Failed to update sort order",
|
"sortUpdateFailed": "Failed to update sort order",
|
||||||
"configureUsage": "Configure usage query",
|
"configureUsage": "Configure usage query",
|
||||||
|
"officialPartner": "Official Partner",
|
||||||
|
"openTerminal": "Open Terminal",
|
||||||
|
"terminalOpened": "Terminal opened",
|
||||||
|
"terminalOpenFailed": "Failed to open terminal",
|
||||||
"name": "Provider Name",
|
"name": "Provider Name",
|
||||||
"namePlaceholder": "e.g., Claude Official",
|
"namePlaceholder": "e.g., Claude Official",
|
||||||
"websiteUrl": "Website URL",
|
"websiteUrl": "Website URL",
|
||||||
@@ -156,7 +161,8 @@
|
|||||||
"deleteFailed": "Failed to delete provider: {{error}}",
|
"deleteFailed": "Failed to delete provider: {{error}}",
|
||||||
"settingsSaved": "Settings saved",
|
"settingsSaved": "Settings saved",
|
||||||
"settingsSaveFailed": "Failed to save settings: {{error}}",
|
"settingsSaveFailed": "Failed to save settings: {{error}}",
|
||||||
"openAIChatFormatHint": "This provider uses OpenAI Chat format and requires the proxy service to be enabled"
|
"openAIChatFormatHint": "This provider uses OpenAI Chat format and requires the proxy service to be enabled",
|
||||||
|
"openLinkFailed": "Failed to open link"
|
||||||
},
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"deleteProvider": "Delete Provider",
|
"deleteProvider": "Delete Provider",
|
||||||
@@ -478,6 +484,7 @@
|
|||||||
"aggregatorApiKeyHint": "💡 Only need to fill in API Key, endpoint is preset",
|
"aggregatorApiKeyHint": "💡 Only need to fill in API Key, endpoint is preset",
|
||||||
"thirdPartyApiKeyHint": "💡 Only need to fill in API Key, endpoint is preset",
|
"thirdPartyApiKeyHint": "💡 Only need to fill in API Key, endpoint is preset",
|
||||||
"customApiKeyHint": "💡 Custom configuration requires manually filling all necessary fields",
|
"customApiKeyHint": "💡 Custom configuration requires manually filling all necessary fields",
|
||||||
|
"omoHint": "💡 OMO config manages Agent model assignments and writes to oh-my-opencode.jsonc",
|
||||||
"officialHint": "💡 Official provider uses browser login, no API Key needed",
|
"officialHint": "💡 Official provider uses browser login, no API Key needed",
|
||||||
"getApiKey": "Get API Key",
|
"getApiKey": "Get API Key",
|
||||||
"partnerPromotion": {
|
"partnerPromotion": {
|
||||||
@@ -1267,6 +1274,9 @@
|
|||||||
"agents": {
|
"agents": {
|
||||||
"title": "Agents"
|
"title": "Agents"
|
||||||
},
|
},
|
||||||
|
"modelTest": {
|
||||||
|
"testProvider": "Test model"
|
||||||
|
},
|
||||||
"health": {
|
"health": {
|
||||||
"operational": "Operational",
|
"operational": "Operational",
|
||||||
"degraded": "Degraded",
|
"degraded": "Degraded",
|
||||||
@@ -1505,6 +1515,7 @@
|
|||||||
"deleted": "Universal provider deleted",
|
"deleted": "Universal provider deleted",
|
||||||
"addSuccess": "Universal provider added successfully",
|
"addSuccess": "Universal provider added successfully",
|
||||||
"addFailed": "Failed to add universal provider",
|
"addFailed": "Failed to add universal provider",
|
||||||
|
"hint": "Cross-app unified config, auto-sync to Claude/Codex/Gemini",
|
||||||
"manage": "Manage",
|
"manage": "Manage",
|
||||||
"loadError": "Failed to load universal providers",
|
"loadError": "Failed to load universal providers",
|
||||||
"saveError": "Failed to save universal provider",
|
"saveError": "Failed to save universal provider",
|
||||||
@@ -1519,5 +1530,94 @@
|
|||||||
"saveAndSyncError": "Failed to save and sync",
|
"saveAndSyncError": "Failed to save and sync",
|
||||||
"configJsonPreview": "Config JSON Preview",
|
"configJsonPreview": "Config JSON Preview",
|
||||||
"configJsonPreviewHint": "The following configurations will be synced to each app (only the displayed fields will be overwritten, other custom settings will be preserved)"
|
"configJsonPreviewHint": "The following configurations will be synced to each app (only the displayed fields will be overwritten, other custom settings will be preserved)"
|
||||||
|
},
|
||||||
|
"omo": {
|
||||||
|
"editProfile": "Edit OMO Config",
|
||||||
|
"newProfile": "New OMO Config",
|
||||||
|
"profileName": "Name",
|
||||||
|
"mainAgents": "Main Agents",
|
||||||
|
"subAgents": "Sub Agents",
|
||||||
|
"categories": "Categories",
|
||||||
|
"customAgents": "Custom Agents",
|
||||||
|
"noCustomAgents": "No custom agents",
|
||||||
|
"otherFields": "Other Config",
|
||||||
|
"globalConfig": "OMO Global Config",
|
||||||
|
"globalConfigShort": "OMO Config",
|
||||||
|
"globalConfigSaved": "Global config saved",
|
||||||
|
"addProfile": "Add OMO Provider",
|
||||||
|
"disabledItems": "Disabled Items",
|
||||||
|
"advanced": "Advanced Settings",
|
||||||
|
"profileCreated": "OMO config created",
|
||||||
|
"profileUpdated": "OMO config updated",
|
||||||
|
"invalidJson": "Other Fields contains invalid JSON",
|
||||||
|
"confirmDelete": "Delete Config",
|
||||||
|
"confirmDeleteMsg": "Delete \"{{name}}\"?",
|
||||||
|
"profileDeleted": "Config deleted",
|
||||||
|
"imported": "Imported as \"{{name}}\"",
|
||||||
|
"import": "Import",
|
||||||
|
"global": "Global",
|
||||||
|
"empty": "No OMO configs yet. Click + Add or Import from local.",
|
||||||
|
"applied": "Applied",
|
||||||
|
"apply": "Apply",
|
||||||
|
"enable": "Enable",
|
||||||
|
"enabled": "Enabled",
|
||||||
|
"disabled": "OMO disabled",
|
||||||
|
"disableFailed": "Failed to disable OMO: {{error}}",
|
||||||
|
"writeCommonConfig": "Write to common config",
|
||||||
|
"editCommonConfig": "Edit common config",
|
||||||
|
"editCommonConfigTitle": "Common Config",
|
||||||
|
"commonConfigHint": "OMO common config will be merged into all OMO configs that enable it",
|
||||||
|
"selectPlaceholder": "Select...",
|
||||||
|
"clear": "Clear",
|
||||||
|
"clearWrapped": "(Clear)",
|
||||||
|
"defaultWrapped": "(Default)",
|
||||||
|
"variantPlaceholder": "variant",
|
||||||
|
"selectEnabledModel": "Select enabled model",
|
||||||
|
"selectModelFirst": "Select model first",
|
||||||
|
"noEnabledModels": "No enabled models",
|
||||||
|
"noVariantsForModel": "No variants for model",
|
||||||
|
"currentValueNotEnabled": "{{value}} (current value, not enabled)",
|
||||||
|
"currentValueUnavailable": "{{value}} (current value, unavailable)",
|
||||||
|
"advancedLabel": "Advanced",
|
||||||
|
"advancedJsonInvalid": "Advanced JSON is invalid",
|
||||||
|
"advancedJsonHint": "temperature, top_p, budgetTokens, prompt_append, permission, etc. Leave empty for defaults",
|
||||||
|
"noEnabledModelsWarning": "No enabled models available. Configure and enable OpenCode models first.",
|
||||||
|
"importLocalReplaceSuccess": "Imported local file and replaced Agents/Categories/Other Fields",
|
||||||
|
"importLocalFailed": "Failed to read local file: {{error}}",
|
||||||
|
"agentKeyPlaceholder": "agent key",
|
||||||
|
"categoryKeyPlaceholder": "category key",
|
||||||
|
"modelNamePlaceholder": "model-name",
|
||||||
|
"custom": "Custom",
|
||||||
|
"customCategories": "Custom Categories",
|
||||||
|
"modelConfiguration": "Model Configuration",
|
||||||
|
"fillRecommended": "Fill Recommended",
|
||||||
|
"configSummary": "{{agents}} agents, {{categories}} categories configured · Click ⚙ for advanced params",
|
||||||
|
"enabledModelsCount": "{{count}} enabled models available",
|
||||||
|
"source": "from:",
|
||||||
|
"otherFieldsJson": "Other Fields (JSON)",
|
||||||
|
"searchOrType": "Search or type custom value...",
|
||||||
|
"noMatches": "No matches",
|
||||||
|
"jsonMustBeObject": "{{field}} must be a JSON object",
|
||||||
|
"jsonInvalid": "{{field}} contains invalid JSON",
|
||||||
|
"importGlobalSuccess": "Imported global config from local file (unsaved)",
|
||||||
|
"importGlobalFailed": "Failed to read local file: {{error}}",
|
||||||
|
"importLocal": "Import Local",
|
||||||
|
"saveGlobalConfig": "Save Global Config",
|
||||||
|
"schemaUrl": "$schema",
|
||||||
|
"resetDefault": "Reset",
|
||||||
|
"sisyphusAgentConfig": "Sisyphus Agent Config",
|
||||||
|
"disabledAgents": "Agents",
|
||||||
|
"disabledAgentsPlaceholder": "Disabled Agents",
|
||||||
|
"disabledMcps": "MCPs",
|
||||||
|
"disabledMcpsPlaceholder": "Disabled MCPs",
|
||||||
|
"disabledHooks": "Hooks",
|
||||||
|
"disabledHooksPlaceholder": "Disabled Hooks",
|
||||||
|
"disabledSkills": "Skills",
|
||||||
|
"disabledSkillsPlaceholder": "Disabled Skills",
|
||||||
|
"advancedLsp": "LSP Config",
|
||||||
|
"advancedExperimental": "Experimental Features",
|
||||||
|
"advancedBackgroundTask": "Background Tasks",
|
||||||
|
"advancedBrowserAutomation": "Browser Automation",
|
||||||
|
"advancedClaudeCode": "Claude Code"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+108
-1
@@ -32,6 +32,7 @@
|
|||||||
"back": "戻る",
|
"back": "戻る",
|
||||||
"refresh": "更新",
|
"refresh": "更新",
|
||||||
"refreshing": "更新中...",
|
"refreshing": "更新中...",
|
||||||
|
"import": "インポート",
|
||||||
"all": "すべて",
|
"all": "すべて",
|
||||||
"search": "検索",
|
"search": "検索",
|
||||||
"reset": "リセット",
|
"reset": "リセット",
|
||||||
@@ -106,6 +107,10 @@
|
|||||||
"duplicate": "複製",
|
"duplicate": "複製",
|
||||||
"sortUpdateFailed": "並び順の更新に失敗しました",
|
"sortUpdateFailed": "並び順の更新に失敗しました",
|
||||||
"configureUsage": "利用状況を設定",
|
"configureUsage": "利用状況を設定",
|
||||||
|
"officialPartner": "公式パートナー",
|
||||||
|
"openTerminal": "ターミナルを開く",
|
||||||
|
"terminalOpened": "ターミナルを開きました",
|
||||||
|
"terminalOpenFailed": "ターミナルを開けませんでした",
|
||||||
"name": "プロバイダー名",
|
"name": "プロバイダー名",
|
||||||
"namePlaceholder": "例: Claude Official",
|
"namePlaceholder": "例: Claude Official",
|
||||||
"websiteUrl": "Web サイト URL",
|
"websiteUrl": "Web サイト URL",
|
||||||
@@ -156,7 +161,8 @@
|
|||||||
"deleteFailed": "プロバイダーの削除に失敗しました: {{error}}",
|
"deleteFailed": "プロバイダーの削除に失敗しました: {{error}}",
|
||||||
"settingsSaved": "設定を保存しました",
|
"settingsSaved": "設定を保存しました",
|
||||||
"settingsSaveFailed": "設定の保存に失敗しました: {{error}}",
|
"settingsSaveFailed": "設定の保存に失敗しました: {{error}}",
|
||||||
"openAIChatFormatHint": "このプロバイダーは OpenAI Chat フォーマットを使用しており、プロキシサービスの有効化が必要です"
|
"openAIChatFormatHint": "このプロバイダーは OpenAI Chat フォーマットを使用しており、プロキシサービスの有効化が必要です",
|
||||||
|
"openLinkFailed": "リンクを開けませんでした"
|
||||||
},
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"deleteProvider": "プロバイダーを削除",
|
"deleteProvider": "プロバイダーを削除",
|
||||||
@@ -478,6 +484,7 @@
|
|||||||
"aggregatorApiKeyHint": "💡 API Key のみ入力すれば OK。エンドポイントはプリセット済みです",
|
"aggregatorApiKeyHint": "💡 API Key のみ入力すれば OK。エンドポイントはプリセット済みです",
|
||||||
"thirdPartyApiKeyHint": "💡 API Key のみ入力すれば OK。エンドポイントはプリセット済みです",
|
"thirdPartyApiKeyHint": "💡 API Key のみ入力すれば OK。エンドポイントはプリセット済みです",
|
||||||
"customApiKeyHint": "💡 カスタム設定では必要な項目をすべて手動で入力してください",
|
"customApiKeyHint": "💡 カスタム設定では必要な項目をすべて手動で入力してください",
|
||||||
|
"omoHint": "💡 OMO 設定は Agent のモデル割り当てを管理し、oh-my-opencode.jsonc に書き込みます",
|
||||||
"officialHint": "💡 公式プロバイダーはブラウザログインで、API Key は不要です",
|
"officialHint": "💡 公式プロバイダーはブラウザログインで、API Key は不要です",
|
||||||
"getApiKey": "API Key を取得",
|
"getApiKey": "API Key を取得",
|
||||||
"partnerPromotion": {
|
"partnerPromotion": {
|
||||||
@@ -1161,6 +1168,13 @@
|
|||||||
"codex": "Codex",
|
"codex": "Codex",
|
||||||
"gemini": "Gemini",
|
"gemini": "Gemini",
|
||||||
"opencode": "OpenCode"
|
"opencode": "OpenCode"
|
||||||
|
},
|
||||||
|
"installFromZip": {
|
||||||
|
"button": "ZIP からインストール",
|
||||||
|
"installing": "インストール中...",
|
||||||
|
"successSingle": "スキル {{name}} をインストールしました",
|
||||||
|
"successMultiple": "{{count}} 件のスキルをインストールしました",
|
||||||
|
"noSkillsFound": "ZIP ファイルにスキルが見つかりません(SKILL.md が必要です)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"deeplink": {
|
"deeplink": {
|
||||||
@@ -1258,6 +1272,9 @@
|
|||||||
"agents": {
|
"agents": {
|
||||||
"title": "エージェント"
|
"title": "エージェント"
|
||||||
},
|
},
|
||||||
|
"modelTest": {
|
||||||
|
"testProvider": "モデルテスト"
|
||||||
|
},
|
||||||
"health": {
|
"health": {
|
||||||
"operational": "正常",
|
"operational": "正常",
|
||||||
"degraded": "低下",
|
"degraded": "低下",
|
||||||
@@ -1479,6 +1496,7 @@
|
|||||||
"deleted": "統合プロバイダーを削除しました",
|
"deleted": "統合プロバイダーを削除しました",
|
||||||
"addSuccess": "統合プロバイダーを追加しました",
|
"addSuccess": "統合プロバイダーを追加しました",
|
||||||
"addFailed": "統合プロバイダーの追加に失敗しました",
|
"addFailed": "統合プロバイダーの追加に失敗しました",
|
||||||
|
"hint": "クロスアプリ統合設定。Claude/Codex/Gemini に自動同期します",
|
||||||
"manage": "管理",
|
"manage": "管理",
|
||||||
"loadError": "統合プロバイダーの読み込みに失敗しました",
|
"loadError": "統合プロバイダーの読み込みに失敗しました",
|
||||||
"saveError": "統合プロバイダーの保存に失敗しました",
|
"saveError": "統合プロバイダーの保存に失敗しました",
|
||||||
@@ -1493,5 +1511,94 @@
|
|||||||
"saveAndSyncError": "保存と同期に失敗しました",
|
"saveAndSyncError": "保存と同期に失敗しました",
|
||||||
"configJsonPreview": "設定 JSON プレビュー",
|
"configJsonPreview": "設定 JSON プレビュー",
|
||||||
"configJsonPreviewHint": "以下は各アプリに同期される設定内容です(表示されているフィールドのみ上書きされ、他のカスタム設定は保持されます)"
|
"configJsonPreviewHint": "以下は各アプリに同期される設定内容です(表示されているフィールドのみ上書きされ、他のカスタム設定は保持されます)"
|
||||||
|
},
|
||||||
|
"omo": {
|
||||||
|
"editProfile": "OMO 設定を編集",
|
||||||
|
"newProfile": "新規 OMO 設定",
|
||||||
|
"profileName": "名前",
|
||||||
|
"mainAgents": "メインエージェント",
|
||||||
|
"subAgents": "サブエージェント",
|
||||||
|
"categories": "カテゴリ",
|
||||||
|
"customAgents": "カスタムエージェント",
|
||||||
|
"noCustomAgents": "カスタムエージェントなし",
|
||||||
|
"otherFields": "その他の設定",
|
||||||
|
"globalConfig": "OMO グローバル設定",
|
||||||
|
"globalConfigShort": "OMO 設定",
|
||||||
|
"globalConfigSaved": "グローバル設定を保存しました",
|
||||||
|
"addProfile": "OMO プロバイダーを追加",
|
||||||
|
"disabledItems": "無効項目設定",
|
||||||
|
"advanced": "詳細設定",
|
||||||
|
"profileCreated": "OMO 設定を作成しました",
|
||||||
|
"profileUpdated": "OMO 設定を更新しました",
|
||||||
|
"invalidJson": "その他のフィールドに無効なJSONが含まれています",
|
||||||
|
"confirmDelete": "設定を削除",
|
||||||
|
"confirmDeleteMsg": "「{{name}}」を削除しますか?",
|
||||||
|
"profileDeleted": "設定を削除しました",
|
||||||
|
"imported": "「{{name}}」としてインポートしました",
|
||||||
|
"import": "インポート",
|
||||||
|
"global": "グローバル",
|
||||||
|
"empty": "OMO 設定がありません。+ 追加またはローカルからインポートしてください。",
|
||||||
|
"applied": "適用済み",
|
||||||
|
"apply": "適用",
|
||||||
|
"enable": "有効化",
|
||||||
|
"enabled": "有効中",
|
||||||
|
"disabled": "OMO を無効化しました",
|
||||||
|
"disableFailed": "OMO の無効化に失敗しました: {{error}}",
|
||||||
|
"writeCommonConfig": "共通設定に書き込む",
|
||||||
|
"editCommonConfig": "共通設定を編集",
|
||||||
|
"editCommonConfigTitle": "共通設定",
|
||||||
|
"commonConfigHint": "OMO 共通設定は有効にしたすべての OMO 設定に統合されます",
|
||||||
|
"selectPlaceholder": "選択してください...",
|
||||||
|
"clear": "クリア",
|
||||||
|
"clearWrapped": "(クリア)",
|
||||||
|
"defaultWrapped": "(デフォルト)",
|
||||||
|
"variantPlaceholder": "variant",
|
||||||
|
"selectEnabledModel": "有効なモデルを選択",
|
||||||
|
"selectModelFirst": "先にモデルを選択",
|
||||||
|
"noEnabledModels": "有効なモデルがありません",
|
||||||
|
"noVariantsForModel": "このモデルには思考レベルがありません",
|
||||||
|
"currentValueNotEnabled": "{{value}} (現在値・未有効)",
|
||||||
|
"currentValueUnavailable": "{{value}} (現在値・利用不可)",
|
||||||
|
"advancedLabel": "詳細",
|
||||||
|
"advancedJsonInvalid": "詳細 JSON が不正です",
|
||||||
|
"advancedJsonHint": "temperature, top_p, budgetTokens, prompt_append, permission など。空欄でデフォルトを使用します",
|
||||||
|
"noEnabledModelsWarning": "利用可能な有効モデルがありません。先に OpenCode モデルを有効化してください。",
|
||||||
|
"importLocalReplaceSuccess": "ローカルファイルから読み込み、Agents/Categories/Other Fields を置き換えました",
|
||||||
|
"importLocalFailed": "ローカルファイルの読み込みに失敗しました: {{error}}",
|
||||||
|
"agentKeyPlaceholder": "agent キー",
|
||||||
|
"categoryKeyPlaceholder": "カテゴリキー",
|
||||||
|
"modelNamePlaceholder": "model-name",
|
||||||
|
"custom": "カスタム",
|
||||||
|
"customCategories": "カスタムカテゴリ",
|
||||||
|
"modelConfiguration": "モデル設定",
|
||||||
|
"fillRecommended": "推奨を入力",
|
||||||
|
"configSummary": "{{agents}} 個の Agent、{{categories}} 個の Category を設定済み · ⚙ で詳細を展開",
|
||||||
|
"enabledModelsCount": "有効モデル {{count}} 件",
|
||||||
|
"source": "出典:",
|
||||||
|
"otherFieldsJson": "その他のフィールド (JSON)",
|
||||||
|
"searchOrType": "検索またはカスタム値を入力...",
|
||||||
|
"noMatches": "一致する項目がありません",
|
||||||
|
"jsonMustBeObject": "{{field}} は JSON オブジェクトである必要があります",
|
||||||
|
"jsonInvalid": "{{field}} に無効な JSON が含まれています",
|
||||||
|
"importGlobalSuccess": "ローカルファイルからグローバル設定を読み込みました(未保存)",
|
||||||
|
"importGlobalFailed": "ローカルファイルの読み込みに失敗しました: {{error}}",
|
||||||
|
"importLocal": "ローカルからインポート",
|
||||||
|
"saveGlobalConfig": "グローバル設定を保存",
|
||||||
|
"schemaUrl": "$schema",
|
||||||
|
"resetDefault": "デフォルトに戻す",
|
||||||
|
"sisyphusAgentConfig": "Sisyphus Agent 設定",
|
||||||
|
"disabledAgents": "Agents",
|
||||||
|
"disabledAgentsPlaceholder": "無効化する Agents",
|
||||||
|
"disabledMcps": "MCPs",
|
||||||
|
"disabledMcpsPlaceholder": "無効化する MCPs",
|
||||||
|
"disabledHooks": "Hooks",
|
||||||
|
"disabledHooksPlaceholder": "無効化する Hooks",
|
||||||
|
"disabledSkills": "Skills",
|
||||||
|
"disabledSkillsPlaceholder": "無効化する Skills",
|
||||||
|
"advancedLsp": "LSP 設定",
|
||||||
|
"advancedExperimental": "実験的機能",
|
||||||
|
"advancedBackgroundTask": "バックグラウンドタスク",
|
||||||
|
"advancedBrowserAutomation": "ブラウザ自動化",
|
||||||
|
"advancedClaudeCode": "Claude Code"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+101
-1
@@ -32,6 +32,7 @@
|
|||||||
"back": "返回",
|
"back": "返回",
|
||||||
"refresh": "刷新",
|
"refresh": "刷新",
|
||||||
"refreshing": "刷新中...",
|
"refreshing": "刷新中...",
|
||||||
|
"import": "导入",
|
||||||
"all": "全部",
|
"all": "全部",
|
||||||
"search": "查询",
|
"search": "查询",
|
||||||
"reset": "重置",
|
"reset": "重置",
|
||||||
@@ -106,6 +107,10 @@
|
|||||||
"duplicate": "复制",
|
"duplicate": "复制",
|
||||||
"sortUpdateFailed": "排序更新失败",
|
"sortUpdateFailed": "排序更新失败",
|
||||||
"configureUsage": "配置用量查询",
|
"configureUsage": "配置用量查询",
|
||||||
|
"officialPartner": "官方合作伙伴",
|
||||||
|
"openTerminal": "打开终端",
|
||||||
|
"terminalOpened": "终端已打开",
|
||||||
|
"terminalOpenFailed": "打开终端失败",
|
||||||
"name": "供应商名称",
|
"name": "供应商名称",
|
||||||
"namePlaceholder": "例如:Claude 官方",
|
"namePlaceholder": "例如:Claude 官方",
|
||||||
"websiteUrl": "官网链接",
|
"websiteUrl": "官网链接",
|
||||||
@@ -156,7 +161,8 @@
|
|||||||
"deleteFailed": "删除供应商失败:{{error}}",
|
"deleteFailed": "删除供应商失败:{{error}}",
|
||||||
"settingsSaved": "设置已保存",
|
"settingsSaved": "设置已保存",
|
||||||
"settingsSaveFailed": "保存设置失败:{{error}}",
|
"settingsSaveFailed": "保存设置失败:{{error}}",
|
||||||
"openAIChatFormatHint": "此供应商使用 OpenAI Chat 格式,需要开启代理服务才能正常使用"
|
"openAIChatFormatHint": "此供应商使用 OpenAI Chat 格式,需要开启代理服务才能正常使用",
|
||||||
|
"openLinkFailed": "链接打开失败"
|
||||||
},
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"deleteProvider": "删除供应商",
|
"deleteProvider": "删除供应商",
|
||||||
@@ -478,6 +484,7 @@
|
|||||||
"aggregatorApiKeyHint": "💡 只需填写 API Key,请求地址已预设",
|
"aggregatorApiKeyHint": "💡 只需填写 API Key,请求地址已预设",
|
||||||
"thirdPartyApiKeyHint": "💡 只需填写 API Key,请求地址已预设",
|
"thirdPartyApiKeyHint": "💡 只需填写 API Key,请求地址已预设",
|
||||||
"customApiKeyHint": "💡 自定义配置需手动填写所有必要字段",
|
"customApiKeyHint": "💡 自定义配置需手动填写所有必要字段",
|
||||||
|
"omoHint": "💡 OMO 配置管理 Agent 模型分配,写入 oh-my-opencode.jsonc",
|
||||||
"officialHint": "💡 官方供应商使用浏览器登录,无需配置 API Key",
|
"officialHint": "💡 官方供应商使用浏览器登录,无需配置 API Key",
|
||||||
"getApiKey": "获取 API Key",
|
"getApiKey": "获取 API Key",
|
||||||
"partnerPromotion": {
|
"partnerPromotion": {
|
||||||
@@ -1267,6 +1274,9 @@
|
|||||||
"agents": {
|
"agents": {
|
||||||
"title": "智能体"
|
"title": "智能体"
|
||||||
},
|
},
|
||||||
|
"modelTest": {
|
||||||
|
"testProvider": "测试模型"
|
||||||
|
},
|
||||||
"health": {
|
"health": {
|
||||||
"operational": "正常",
|
"operational": "正常",
|
||||||
"degraded": "降级",
|
"degraded": "降级",
|
||||||
@@ -1505,6 +1515,7 @@
|
|||||||
"deleted": "统一供应商已删除",
|
"deleted": "统一供应商已删除",
|
||||||
"addSuccess": "统一供应商添加成功",
|
"addSuccess": "统一供应商添加成功",
|
||||||
"addFailed": "统一供应商添加失败",
|
"addFailed": "统一供应商添加失败",
|
||||||
|
"hint": "跨应用统一配置,自动同步到 Claude/Codex/Gemini",
|
||||||
"manage": "管理",
|
"manage": "管理",
|
||||||
"loadError": "加载统一供应商失败",
|
"loadError": "加载统一供应商失败",
|
||||||
"saveError": "保存统一供应商失败",
|
"saveError": "保存统一供应商失败",
|
||||||
@@ -1519,5 +1530,94 @@
|
|||||||
"saveAndSyncError": "保存并同步失败",
|
"saveAndSyncError": "保存并同步失败",
|
||||||
"configJsonPreview": "配置 JSON 预览",
|
"configJsonPreview": "配置 JSON 预览",
|
||||||
"configJsonPreviewHint": "以下是将要同步到各应用的配置内容(仅覆盖显示的字段,保留其他自定义配置)"
|
"configJsonPreviewHint": "以下是将要同步到各应用的配置内容(仅覆盖显示的字段,保留其他自定义配置)"
|
||||||
|
},
|
||||||
|
"omo": {
|
||||||
|
"editProfile": "编辑 OMO 配置",
|
||||||
|
"newProfile": "新建 OMO 配置",
|
||||||
|
"profileName": "名称",
|
||||||
|
"mainAgents": "主 Agent",
|
||||||
|
"subAgents": "子 Agent",
|
||||||
|
"categories": "分类",
|
||||||
|
"customAgents": "自定义 Agent",
|
||||||
|
"noCustomAgents": "暂无自定义 Agent",
|
||||||
|
"otherFields": "其他配置",
|
||||||
|
"globalConfig": "OMO 全局配置",
|
||||||
|
"globalConfigShort": "OMO 配置",
|
||||||
|
"globalConfigSaved": "全局配置已保存",
|
||||||
|
"addProfile": "添加 OMO 配置",
|
||||||
|
"disabledItems": "禁用项设置",
|
||||||
|
"advanced": "高级设置",
|
||||||
|
"profileCreated": "OMO 配置已创建",
|
||||||
|
"profileUpdated": "OMO 配置已更新",
|
||||||
|
"invalidJson": "其他字段包含无效 JSON",
|
||||||
|
"confirmDelete": "删除配置",
|
||||||
|
"confirmDeleteMsg": "确定删除 \"{{name}}\" 吗?",
|
||||||
|
"profileDeleted": "配置已删除",
|
||||||
|
"imported": "已导入为 \"{{name}}\"",
|
||||||
|
"import": "导入",
|
||||||
|
"global": "全局",
|
||||||
|
"empty": "暂无配置。点击 + 添加或从本地导入。",
|
||||||
|
"applied": "已应用",
|
||||||
|
"apply": "应用",
|
||||||
|
"enable": "启用",
|
||||||
|
"enabled": "启用中",
|
||||||
|
"disabled": "OMO 已停用",
|
||||||
|
"disableFailed": "停用 OMO 失败: {{error}}",
|
||||||
|
"writeCommonConfig": "写入通用配置",
|
||||||
|
"editCommonConfig": "编辑通用配置",
|
||||||
|
"editCommonConfigTitle": "通用配置",
|
||||||
|
"commonConfigHint": "OMO 通用配置将合并到所有启用它的 OMO 配置中",
|
||||||
|
"selectPlaceholder": "请选择...",
|
||||||
|
"clear": "清空",
|
||||||
|
"clearWrapped": "(清空)",
|
||||||
|
"defaultWrapped": "(默认)",
|
||||||
|
"variantPlaceholder": "思考等级",
|
||||||
|
"selectEnabledModel": "选择已启用模型",
|
||||||
|
"selectModelFirst": "先选择模型",
|
||||||
|
"noEnabledModels": "暂无已启用模型",
|
||||||
|
"noVariantsForModel": "该模型无思考等级",
|
||||||
|
"currentValueNotEnabled": "{{value}}(当前值,未启用)",
|
||||||
|
"currentValueUnavailable": "{{value}}(当前值,未启用)",
|
||||||
|
"advancedLabel": "高级参数",
|
||||||
|
"advancedJsonInvalid": "高级参数 JSON 无效",
|
||||||
|
"advancedJsonHint": "temperature, top_p, budgetTokens, prompt_append, permission 等,留空使用默认值",
|
||||||
|
"noEnabledModelsWarning": "当前没有可用的已启用模型,请先启用并配置 OpenCode 模型",
|
||||||
|
"importLocalReplaceSuccess": "已从本地文件导入并覆盖 Agent/Category/Other Fields",
|
||||||
|
"importLocalFailed": "读取本地文件失败: {{error}}",
|
||||||
|
"agentKeyPlaceholder": "agent 键名",
|
||||||
|
"categoryKeyPlaceholder": "分类键名",
|
||||||
|
"modelNamePlaceholder": "模型名",
|
||||||
|
"custom": "自定义",
|
||||||
|
"customCategories": "自定义分类",
|
||||||
|
"modelConfiguration": "模型配置",
|
||||||
|
"fillRecommended": "填充推荐",
|
||||||
|
"configSummary": "已配置 {{agents}} 个 Agent,{{categories}} 个 Category · 点击 ⚙ 展开高级参数",
|
||||||
|
"enabledModelsCount": "可选已启用模型 {{count}} 个",
|
||||||
|
"source": "来源:",
|
||||||
|
"otherFieldsJson": "其他字段 (JSON)",
|
||||||
|
"searchOrType": "搜索或输入自定义值...",
|
||||||
|
"noMatches": "无匹配项",
|
||||||
|
"jsonMustBeObject": "{{field}} 必须是 JSON 对象",
|
||||||
|
"jsonInvalid": "{{field}} 包含无效 JSON",
|
||||||
|
"importGlobalSuccess": "已从本地文件导入全局配置(未保存)",
|
||||||
|
"importGlobalFailed": "读取本地文件失败: {{error}}",
|
||||||
|
"importLocal": "从本地导入",
|
||||||
|
"saveGlobalConfig": "保存全局配置",
|
||||||
|
"schemaUrl": "$schema",
|
||||||
|
"resetDefault": "重置默认",
|
||||||
|
"sisyphusAgentConfig": "Sisyphus Agent 设置",
|
||||||
|
"disabledAgents": "Agents",
|
||||||
|
"disabledAgentsPlaceholder": "禁用的 Agents",
|
||||||
|
"disabledMcps": "MCPs",
|
||||||
|
"disabledMcpsPlaceholder": "禁用的 MCPs",
|
||||||
|
"disabledHooks": "Hooks",
|
||||||
|
"disabledHooksPlaceholder": "禁用的 Hooks",
|
||||||
|
"disabledSkills": "Skills",
|
||||||
|
"disabledSkillsPlaceholder": "禁用的 Skills",
|
||||||
|
"advancedLsp": "LSP 配置",
|
||||||
|
"advancedExperimental": "实验性功能",
|
||||||
|
"advancedBackgroundTask": "后台任务",
|
||||||
|
"advancedBrowserAutomation": "浏览器自动化",
|
||||||
|
"advancedClaudeCode": "Claude Code"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-34
@@ -1,9 +1,7 @@
|
|||||||
/* Tailwind CSS v3 指令 */
|
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
/* shadcn/ui 主题变量 - 蓝色主题 */
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--background: 0 0% 100%;
|
--background: 0 0% 100%;
|
||||||
@@ -13,25 +11,20 @@
|
|||||||
--popover: 0 0% 100%;
|
--popover: 0 0% 100%;
|
||||||
--popover-foreground: 240 10% 3.9%;
|
--popover-foreground: 240 10% 3.9%;
|
||||||
|
|
||||||
/* 主色调:macOS 风格系统蓝 */
|
|
||||||
--primary: 210 100% 56%;
|
--primary: 210 100% 56%;
|
||||||
--primary-foreground: 0 0% 100%;
|
--primary-foreground: 0 0% 100%;
|
||||||
|
|
||||||
/* 次要色:淡蓝灰 */
|
|
||||||
--secondary: 240 4.8% 95.9%;
|
--secondary: 240 4.8% 95.9%;
|
||||||
--secondary-foreground: 240 5.9% 10%;
|
--secondary-foreground: 240 5.9% 10%;
|
||||||
|
|
||||||
/* 强调色 */
|
|
||||||
--muted: 240 4.8% 95.9%;
|
--muted: 240 4.8% 95.9%;
|
||||||
--muted-foreground: 240 3.8% 46.1%;
|
--muted-foreground: 240 3.8% 46.1%;
|
||||||
--accent: 240 4.8% 95.9%;
|
--accent: 240 4.8% 95.9%;
|
||||||
--accent-foreground: 240 5.9% 10%;
|
--accent-foreground: 240 5.9% 10%;
|
||||||
|
|
||||||
/* 危险色:红色 */
|
|
||||||
--destructive: 0 84.2% 60.2%;
|
--destructive: 0 84.2% 60.2%;
|
||||||
--destructive-foreground: 0 0% 98%;
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
|
||||||
/* 边框和输入框 */
|
|
||||||
--border: 240 5.9% 90%;
|
--border: 240 5.9% 90%;
|
||||||
--input: 240 5.9% 90%;
|
--input: 240 5.9% 90%;
|
||||||
--ring: 210 100% 56%;
|
--ring: 210 100% 56%;
|
||||||
@@ -40,7 +33,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
/* 背景与卡片:接近 macOS 深色 systemBackground / windowBackground */
|
|
||||||
--background: 240 5% 12%;
|
--background: 240 5% 12%;
|
||||||
--foreground: 0 0% 98%;
|
--foreground: 0 0% 98%;
|
||||||
--card: 240 5% 16%;
|
--card: 240 5% 16%;
|
||||||
@@ -48,7 +40,6 @@
|
|||||||
--popover: 240 5% 16%;
|
--popover: 240 5% 16%;
|
||||||
--popover-foreground: 0 0% 98%;
|
--popover-foreground: 0 0% 98%;
|
||||||
|
|
||||||
/* 暗色模式主色调:macOS 风格系统蓝(略微降低亮度) */
|
|
||||||
--primary: 210 100% 54%;
|
--primary: 210 100% 54%;
|
||||||
--primary-foreground: 0 0% 100%;
|
--primary-foreground: 0 0% 100%;
|
||||||
|
|
||||||
@@ -60,7 +51,6 @@
|
|||||||
--accent: 240 5% 18%;
|
--accent: 240 5% 18%;
|
||||||
--accent-foreground: 0 0% 98%;
|
--accent-foreground: 0 0% 98%;
|
||||||
|
|
||||||
/* 暗色模式危险色 */
|
|
||||||
--destructive: 0 62.8% 30.6%;
|
--destructive: 0 62.8% 30.6%;
|
||||||
--destructive-foreground: 0 0% 98%;
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
|
||||||
@@ -70,7 +60,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Glassmorphism Utilities */
|
|
||||||
.glass {
|
.glass {
|
||||||
background: rgba(255, 255, 255, 0.7);
|
background: rgba(255, 255, 255, 0.7);
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
@@ -100,7 +89,6 @@
|
|||||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
|
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 供应商卡片选中状态 */
|
|
||||||
.glass-card-active {
|
.glass-card-active {
|
||||||
background: rgba(59, 130, 246, 0.08);
|
background: rgba(59, 130, 246, 0.08);
|
||||||
border: 1px solid rgba(59, 130, 246, 0.4);
|
border: 1px solid rgba(59, 130, 246, 0.4);
|
||||||
@@ -125,7 +113,6 @@
|
|||||||
border-top: 2px solid hsl(var(--border));
|
border-top: 2px solid hsl(var(--border));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tauri 拖拽区域 */
|
|
||||||
[data-tauri-drag-region] {
|
[data-tauri-drag-region] {
|
||||||
-webkit-app-region: drag;
|
-webkit-app-region: drag;
|
||||||
}
|
}
|
||||||
@@ -135,19 +122,16 @@
|
|||||||
-webkit-app-region: no-drag;
|
-webkit-app-region: no-drag;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 全局基础样式 */
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
scrollbar-width: none; /* Firefox */
|
scrollbar-width: none;
|
||||||
-ms-overflow-style: none; /* IE 10+ */
|
-ms-overflow-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
@apply font-sans antialiased;
|
@apply font-sans antialiased;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
/* 让原生控件与滚动条随主题切换配色 */
|
|
||||||
color-scheme: light;
|
color-scheme: light;
|
||||||
/* 禁用 overscroll 回弹效果,防止下拉时顶部边框被拉下来 */
|
|
||||||
overscroll-behavior: none;
|
overscroll-behavior: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,24 +141,19 @@ body {
|
|||||||
color: hsl(var(--foreground));
|
color: hsl(var(--foreground));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 暗色模式下启用暗色原生控件/滚动条配色 */
|
|
||||||
html.dark {
|
html.dark {
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 滚动条样式 - 完全隐藏(支持所有浏览器) */
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 焦点样式 */
|
|
||||||
*:focus-visible {
|
*:focus-visible {
|
||||||
@apply outline-2 outline-blue-500 outline-offset-2;
|
@apply outline-2 outline-blue-500 outline-offset-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 统一边框设计系统 - 使用工具类定义 */
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
/* 让滚动条悬浮于内容之上,避免出现/消失时挤压布局 */
|
|
||||||
.scroll-overlay {
|
.scroll-overlay {
|
||||||
scrollbar-gutter: stable both-edges;
|
scrollbar-gutter: stable both-edges;
|
||||||
padding-right: 0.5rem;
|
padding-right: 0.5rem;
|
||||||
@@ -182,13 +161,11 @@ html.dark {
|
|||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 默认边框:1px,使用主题边框颜色 */
|
|
||||||
.border-default {
|
.border-default {
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
border-color: hsl(var(--border));
|
border-color: hsl(var(--border));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 激活边框:2px,使用主色 */
|
|
||||||
.border-active {
|
.border-active {
|
||||||
border-width: 2px;
|
border-width: 2px;
|
||||||
}
|
}
|
||||||
@@ -210,20 +187,17 @@ html.dark {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 禁用 Edge / IE 的密码显示按钮 */
|
|
||||||
input[type="password"]::-ms-reveal,
|
input[type="password"]::-ms-reveal,
|
||||||
input[type="password"]::-ms-clear {
|
input[type="password"]::-ms-clear {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Theme transition animation using View Transitions API */
|
|
||||||
::view-transition-old(root),
|
::view-transition-old(root),
|
||||||
::view-transition-new(root) {
|
::view-transition-new(root) {
|
||||||
animation: none;
|
animation: none;
|
||||||
mix-blend-mode: normal;
|
mix-blend-mode: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Old snapshot stays behind, new snapshot animates on top */
|
|
||||||
::view-transition-old(root) {
|
::view-transition-old(root) {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
@@ -232,25 +206,26 @@ input[type="password"]::-ms-clear {
|
|||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Circular expand animation from click position */
|
|
||||||
@keyframes theme-circle-expand {
|
@keyframes theme-circle-expand {
|
||||||
from {
|
from {
|
||||||
clip-path: circle(0% at var(--theme-transition-x, 50%) var(--theme-transition-y, 50%));
|
clip-path: circle(
|
||||||
|
0% at var(--theme-transition-x, 50%) var(--theme-transition-y, 50%)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
clip-path: circle(150% at var(--theme-transition-x, 50%) var(--theme-transition-y, 50%));
|
clip-path: circle(
|
||||||
|
150% at var(--theme-transition-x, 50%) var(--theme-transition-y, 50%)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Apply animation to new snapshot - works for both light and dark transitions */
|
|
||||||
::view-transition-new(root) {
|
::view-transition-new(root) {
|
||||||
animation: theme-circle-expand 0.4s ease-out;
|
animation: theme-circle-expand 0.4s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Respect user preference for reduced motion */
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
::view-transition-new(root) {
|
::view-transition-new(root) {
|
||||||
animation: none;
|
animation: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// 配置相关 API
|
// 配置相关 API
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
export type AppType = "claude" | "codex" | "gemini";
|
export type AppType = "claude" | "codex" | "gemini" | "omo";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取 Claude 通用配置片段(已废弃,使用 getCommonConfigSnippet)
|
* 获取 Claude 通用配置片段(已废弃,使用 getCommonConfigSnippet)
|
||||||
@@ -63,7 +63,7 @@ export type ExtractCommonConfigSnippetOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function extractCommonConfigSnippet(
|
export async function extractCommonConfigSnippet(
|
||||||
appType: AppType,
|
appType: Exclude<AppType, "omo">,
|
||||||
options?: ExtractCommonConfigSnippetOptions,
|
options?: ExtractCommonConfigSnippetOptions,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const args: Record<string, unknown> = { appType };
|
const args: Record<string, unknown> = { appType };
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import type { OmoLocalFileData } from "@/types/omo";
|
||||||
|
|
||||||
|
export const omoApi = {
|
||||||
|
readLocalFile: (): Promise<OmoLocalFileData> => invoke("read_omo_local_file"),
|
||||||
|
getCurrentOmoProviderId: (): Promise<string> =>
|
||||||
|
invoke("get_current_omo_provider_id"),
|
||||||
|
getOmoProviderCount: (): Promise<number> => invoke("get_omo_provider_count"),
|
||||||
|
disableCurrentOmo: (): Promise<void> => invoke("disable_current_omo"),
|
||||||
|
};
|
||||||
+28
-14
@@ -17,13 +17,15 @@ export const useAddProviderMutation = (appId: AppId) => {
|
|||||||
let id: string;
|
let id: string;
|
||||||
|
|
||||||
if (appId === "opencode") {
|
if (appId === "opencode") {
|
||||||
// OpenCode: use user-provided providerKey as ID
|
if (providerInput.category === "omo") {
|
||||||
if (!providerInput.providerKey) {
|
id = `omo-${generateUUID()}`;
|
||||||
throw new Error("Provider key is required for OpenCode");
|
} else {
|
||||||
|
if (!providerInput.providerKey) {
|
||||||
|
throw new Error("Provider key is required for OpenCode");
|
||||||
|
}
|
||||||
|
id = providerInput.providerKey;
|
||||||
}
|
}
|
||||||
id = providerInput.providerKey;
|
|
||||||
} else {
|
} else {
|
||||||
// Other apps: use random UUID
|
|
||||||
id = generateUUID();
|
id = generateUUID();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,7 +34,6 @@ export const useAddProviderMutation = (appId: AppId) => {
|
|||||||
id,
|
id,
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
};
|
};
|
||||||
// Remove providerKey from the provider object before saving
|
|
||||||
delete (newProvider as any).providerKey;
|
delete (newProvider as any).providerKey;
|
||||||
|
|
||||||
await providersApi.add(newProvider, appId);
|
await providersApi.add(newProvider, appId);
|
||||||
@@ -41,7 +42,15 @@ export const useAddProviderMutation = (appId: AppId) => {
|
|||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await queryClient.invalidateQueries({ queryKey: ["providers", appId] });
|
await queryClient.invalidateQueries({ queryKey: ["providers", appId] });
|
||||||
|
|
||||||
// 更新托盘菜单(失败不影响主操作)
|
if (appId === "opencode") {
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: ["omo", "current-provider-id"],
|
||||||
|
});
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: ["omo", "provider-count"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await providersApi.updateTrayMenu();
|
await providersApi.updateTrayMenu();
|
||||||
} catch (trayError) {
|
} catch (trayError) {
|
||||||
@@ -115,7 +124,15 @@ export const useDeleteProviderMutation = (appId: AppId) => {
|
|||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await queryClient.invalidateQueries({ queryKey: ["providers", appId] });
|
await queryClient.invalidateQueries({ queryKey: ["providers", appId] });
|
||||||
|
|
||||||
// 更新托盘菜单(失败不影响主操作)
|
if (appId === "opencode") {
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: ["omo", "current-provider-id"],
|
||||||
|
});
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: ["omo", "provider-count"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await providersApi.updateTrayMenu();
|
await providersApi.updateTrayMenu();
|
||||||
} catch (trayError) {
|
} catch (trayError) {
|
||||||
@@ -157,14 +174,15 @@ export const useSwitchProviderMutation = (appId: AppId) => {
|
|||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await queryClient.invalidateQueries({ queryKey: ["providers", appId] });
|
await queryClient.invalidateQueries({ queryKey: ["providers", appId] });
|
||||||
|
|
||||||
// OpenCode: also invalidate live provider IDs cache to update button state
|
|
||||||
if (appId === "opencode") {
|
if (appId === "opencode") {
|
||||||
await queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
queryKey: ["opencodeLiveProviderIds"],
|
queryKey: ["opencodeLiveProviderIds"],
|
||||||
});
|
});
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: ["omo", "current-provider-id"],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新托盘菜单(失败不影响主操作)
|
|
||||||
try {
|
try {
|
||||||
await providersApi.updateTrayMenu();
|
await providersApi.updateTrayMenu();
|
||||||
} catch (trayError) {
|
} catch (trayError) {
|
||||||
@@ -173,14 +191,10 @@ export const useSwitchProviderMutation = (appId: AppId) => {
|
|||||||
trayError,
|
trayError,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: Success toast is handled by useProviderActions.switchProvider
|
|
||||||
// to allow customization based on provider properties (e.g., apiFormat)
|
|
||||||
},
|
},
|
||||||
onError: (error: Error) => {
|
onError: (error: Error) => {
|
||||||
const detail = extractErrorMessage(error) || t("common.unknown");
|
const detail = extractErrorMessage(error) || t("common.unknown");
|
||||||
|
|
||||||
// 标题与详情分离,便于扫描 + 一键复制
|
|
||||||
toast.error(
|
toast.error(
|
||||||
t("notifications.switchFailedTitle", { defaultValue: "切换失败" }),
|
t("notifications.switchFailedTitle", { defaultValue: "切换失败" }),
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { omoApi } from "@/lib/api/omo";
|
||||||
|
import * as configApi from "@/lib/api/config";
|
||||||
|
import type { OmoGlobalConfig } from "@/types/omo";
|
||||||
|
|
||||||
|
export const omoKeys = {
|
||||||
|
all: ["omo"] as const,
|
||||||
|
globalConfig: () => [...omoKeys.all, "global-config"] as const,
|
||||||
|
currentProviderId: () => [...omoKeys.all, "current-provider-id"] as const,
|
||||||
|
providerCount: () => [...omoKeys.all, "provider-count"] as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
function invalidateOmoQueries(queryClient: ReturnType<typeof useQueryClient>) {
|
||||||
|
queryClient.invalidateQueries({ queryKey: omoKeys.globalConfig() });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["providers"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: omoKeys.currentProviderId() });
|
||||||
|
queryClient.invalidateQueries({ queryKey: omoKeys.providerCount() });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useOmoGlobalConfig(enabled = true) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: omoKeys.globalConfig(),
|
||||||
|
enabled,
|
||||||
|
queryFn: async (): Promise<OmoGlobalConfig> => {
|
||||||
|
const raw = await configApi.getCommonConfigSnippet("omo");
|
||||||
|
if (!raw) {
|
||||||
|
return {
|
||||||
|
id: "global",
|
||||||
|
disabledAgents: [],
|
||||||
|
disabledMcps: [],
|
||||||
|
disabledHooks: [],
|
||||||
|
disabledSkills: [],
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.parse(raw) as OmoGlobalConfig;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(
|
||||||
|
"[omo] invalid global config json, fallback to defaults",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
id: "global",
|
||||||
|
disabledAgents: [],
|
||||||
|
disabledMcps: [],
|
||||||
|
disabledHooks: [],
|
||||||
|
disabledSkills: [],
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCurrentOmoProviderId(enabled = true) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: omoKeys.currentProviderId(),
|
||||||
|
queryFn: omoApi.getCurrentOmoProviderId,
|
||||||
|
enabled,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useOmoProviderCount(enabled = true) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: omoKeys.providerCount(),
|
||||||
|
queryFn: omoApi.getOmoProviderCount,
|
||||||
|
enabled,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSaveOmoGlobalConfig() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (input: OmoGlobalConfig) => {
|
||||||
|
const jsonStr = JSON.stringify(input);
|
||||||
|
await configApi.setCommonConfigSnippet("omo", jsonStr);
|
||||||
|
},
|
||||||
|
onSuccess: () => invalidateOmoQueries(queryClient),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useReadOmoLocalFile() {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: () => omoApi.readLocalFile(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDisableCurrentOmo() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: () => omoApi.disableCurrentOmo(),
|
||||||
|
onSuccess: () => invalidateOmoQueries(queryClient),
|
||||||
|
});
|
||||||
|
}
|
||||||
+2
-1
@@ -3,7 +3,8 @@ export type ProviderCategory =
|
|||||||
| "cn_official" // 开源官方(原"国产官方")
|
| "cn_official" // 开源官方(原"国产官方")
|
||||||
| "aggregator" // 聚合网站
|
| "aggregator" // 聚合网站
|
||||||
| "third_party" // 第三方供应商
|
| "third_party" // 第三方供应商
|
||||||
| "custom"; // 自定义
|
| "custom" // 自定义
|
||||||
|
| "omo"; // Oh My OpenCode
|
||||||
|
|
||||||
export interface Provider {
|
export interface Provider {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -0,0 +1,361 @@
|
|||||||
|
export interface OmoGlobalConfig {
|
||||||
|
id: string;
|
||||||
|
schemaUrl?: string;
|
||||||
|
sisyphusAgent?: Record<string, unknown>;
|
||||||
|
disabledAgents: string[];
|
||||||
|
disabledMcps: string[];
|
||||||
|
disabledHooks: string[];
|
||||||
|
disabledSkills: string[];
|
||||||
|
lsp?: Record<string, unknown>;
|
||||||
|
experimental?: Record<string, unknown>;
|
||||||
|
backgroundTask?: Record<string, unknown>;
|
||||||
|
browserAutomationEngine?: Record<string, unknown>;
|
||||||
|
claudeCode?: Record<string, unknown>;
|
||||||
|
otherFields?: Record<string, unknown>;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OmoLocalFileData {
|
||||||
|
agents?: Record<string, Record<string, unknown>>;
|
||||||
|
categories?: Record<string, Record<string, unknown>>;
|
||||||
|
otherFields?: Record<string, unknown>;
|
||||||
|
global: OmoGlobalConfig;
|
||||||
|
filePath: string;
|
||||||
|
lastModified?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OmoAgentDef {
|
||||||
|
key: string;
|
||||||
|
display: string;
|
||||||
|
descZh: string;
|
||||||
|
descEn: string;
|
||||||
|
recommended?: string;
|
||||||
|
group: "main" | "sub";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OmoCategoryDef {
|
||||||
|
key: string;
|
||||||
|
display: string;
|
||||||
|
descZh: string;
|
||||||
|
descEn: string;
|
||||||
|
recommended?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OMO_BUILTIN_AGENTS: OmoAgentDef[] = [
|
||||||
|
{
|
||||||
|
key: "Sisyphus",
|
||||||
|
display: "Sisyphus",
|
||||||
|
descZh: "主编排者",
|
||||||
|
descEn: "Main orchestrator",
|
||||||
|
recommended: "claude-opus-4-6",
|
||||||
|
group: "main",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Hephaestus",
|
||||||
|
display: "Hephaestus",
|
||||||
|
descZh: "自主深度工作者",
|
||||||
|
descEn: "Autonomous deep worker",
|
||||||
|
recommended: "gpt-5.3-codex",
|
||||||
|
group: "main",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Prometheus",
|
||||||
|
display: "Prometheus",
|
||||||
|
descZh: "战略规划者",
|
||||||
|
descEn: "Strategic planner",
|
||||||
|
recommended: "claude-opus-4-6",
|
||||||
|
group: "main",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Atlas",
|
||||||
|
display: "Atlas",
|
||||||
|
descZh: "任务管理者",
|
||||||
|
descEn: "Task manager",
|
||||||
|
recommended: "kimi-k2.5",
|
||||||
|
group: "main",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "oracle",
|
||||||
|
display: "Oracle",
|
||||||
|
descZh: "战略顾问",
|
||||||
|
descEn: "Strategic advisor",
|
||||||
|
recommended: "gpt-5.3",
|
||||||
|
group: "sub",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "librarian",
|
||||||
|
display: "Librarian",
|
||||||
|
descZh: "多仓库研究员",
|
||||||
|
descEn: "Multi-repo researcher",
|
||||||
|
recommended: "glm-4.7",
|
||||||
|
group: "sub",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "explore",
|
||||||
|
display: "Explore",
|
||||||
|
descZh: "快速代码搜索",
|
||||||
|
descEn: "Fast code search",
|
||||||
|
recommended: "grok-code-fast-1",
|
||||||
|
group: "sub",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "multimodal-looker",
|
||||||
|
display: "Multimodal-Looker",
|
||||||
|
descZh: "媒体分析器",
|
||||||
|
descEn: "Media analyzer",
|
||||||
|
recommended: "gemini-3-flash",
|
||||||
|
group: "sub",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Metis",
|
||||||
|
display: "Metis",
|
||||||
|
descZh: "规划前分析顾问",
|
||||||
|
descEn: "Pre-plan analysis advisor",
|
||||||
|
recommended: "claude-opus-4-6",
|
||||||
|
group: "sub",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Momus",
|
||||||
|
display: "Momus",
|
||||||
|
descZh: "计划审查者",
|
||||||
|
descEn: "Plan reviewer",
|
||||||
|
recommended: "gpt-5.3",
|
||||||
|
group: "sub",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Sisyphus-Junior",
|
||||||
|
display: "Sisyphus-Junior",
|
||||||
|
descZh: "委托任务执行器",
|
||||||
|
descEn: "Delegated task executor",
|
||||||
|
group: "sub",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const OMO_BUILTIN_CATEGORIES: OmoCategoryDef[] = [
|
||||||
|
{
|
||||||
|
key: "visual-engineering",
|
||||||
|
display: "Visual Engineering",
|
||||||
|
descZh: "视觉/前端工程",
|
||||||
|
descEn: "Visual/frontend engineering",
|
||||||
|
recommended: "gemini-3-pro",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "ultrabrain",
|
||||||
|
display: "Ultrabrain",
|
||||||
|
descZh: "超级思考",
|
||||||
|
descEn: "Ultra thinking",
|
||||||
|
recommended: "claude-opus-4-6",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "deep",
|
||||||
|
display: "Deep",
|
||||||
|
descZh: "深度工作",
|
||||||
|
descEn: "Deep work",
|
||||||
|
recommended: "gpt-5.3-codex",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "artistry",
|
||||||
|
display: "Artistry",
|
||||||
|
descZh: "创意/文艺",
|
||||||
|
descEn: "Creative/artistic",
|
||||||
|
recommended: "claude-opus-4-6",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "quick",
|
||||||
|
display: "Quick",
|
||||||
|
descZh: "快速响应",
|
||||||
|
descEn: "Quick response",
|
||||||
|
recommended: "gemini-3-flash",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "unspecified-low",
|
||||||
|
display: "Unspecified Low",
|
||||||
|
descZh: "通用低配",
|
||||||
|
descEn: "General low tier",
|
||||||
|
recommended: "gemini-3-flash",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "unspecified-high",
|
||||||
|
display: "Unspecified High",
|
||||||
|
descZh: "通用高配",
|
||||||
|
descEn: "General high tier",
|
||||||
|
recommended: "gpt-5.3-codex",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "writing",
|
||||||
|
display: "Writing",
|
||||||
|
descZh: "写作",
|
||||||
|
descEn: "Writing",
|
||||||
|
recommended: "claude-opus-4-6",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const OMO_DISABLEABLE_AGENTS = [
|
||||||
|
{ value: "Prometheus (Planner)", label: "Prometheus (Planner)" },
|
||||||
|
{ value: "Atlas", label: "Atlas" },
|
||||||
|
{ value: "oracle", label: "Oracle" },
|
||||||
|
{ value: "librarian", label: "Librarian" },
|
||||||
|
{ value: "explore", label: "Explore" },
|
||||||
|
{ value: "multimodal-looker", label: "Multimodal Looker" },
|
||||||
|
{ value: "frontend-ui-ux-engineer", label: "Frontend UI/UX Engineer" },
|
||||||
|
{ value: "document-writer", label: "Document Writer" },
|
||||||
|
{ value: "Sisyphus-Junior", label: "Sisyphus-Junior" },
|
||||||
|
{ value: "Metis (Plan Consultant)", label: "Metis (Plan Consultant)" },
|
||||||
|
{ value: "Momus (Plan Reviewer)", label: "Momus (Plan Reviewer)" },
|
||||||
|
{ value: "OpenCode-Builder", label: "OpenCode-Builder" },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const OMO_DISABLEABLE_MCPS = [
|
||||||
|
{ value: "context7", label: "context7" },
|
||||||
|
{ value: "grep_app", label: "grep_app" },
|
||||||
|
{ value: "websearch", label: "websearch" },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const OMO_DISABLEABLE_HOOKS = [
|
||||||
|
{ value: "todo-continuation-enforcer", label: "todo-continuation-enforcer" },
|
||||||
|
{ value: "context-window-monitor", label: "context-window-monitor" },
|
||||||
|
{ value: "session-recovery", label: "session-recovery" },
|
||||||
|
{ value: "session-notification", label: "session-notification" },
|
||||||
|
{ value: "comment-checker", label: "comment-checker" },
|
||||||
|
{ value: "grep-output-truncator", label: "grep-output-truncator" },
|
||||||
|
{ value: "tool-output-truncator", label: "tool-output-truncator" },
|
||||||
|
{
|
||||||
|
value: "directory-agents-injector",
|
||||||
|
label: "directory-agents-injector",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "directory-readme-injector",
|
||||||
|
label: "directory-readme-injector",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "empty-task-response-detector",
|
||||||
|
label: "empty-task-response-detector",
|
||||||
|
},
|
||||||
|
{ value: "think-mode", label: "think-mode" },
|
||||||
|
{
|
||||||
|
value: "anthropic-context-window-limit-recovery",
|
||||||
|
label: "anthropic-context-window-limit-recovery",
|
||||||
|
},
|
||||||
|
{ value: "rules-injector", label: "rules-injector" },
|
||||||
|
{ value: "background-notification", label: "background-notification" },
|
||||||
|
{ value: "auto-update-checker", label: "auto-update-checker" },
|
||||||
|
{ value: "startup-toast", label: "startup-toast" },
|
||||||
|
{ value: "keyword-detector", label: "keyword-detector" },
|
||||||
|
{ value: "agent-usage-reminder", label: "agent-usage-reminder" },
|
||||||
|
{ value: "non-interactive-env", label: "non-interactive-env" },
|
||||||
|
{ value: "interactive-bash-session", label: "interactive-bash-session" },
|
||||||
|
{
|
||||||
|
value: "compaction-context-injector",
|
||||||
|
label: "compaction-context-injector",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "thinking-block-validator",
|
||||||
|
label: "thinking-block-validator",
|
||||||
|
},
|
||||||
|
{ value: "claude-code-hooks", label: "claude-code-hooks" },
|
||||||
|
{ value: "ralph-loop", label: "ralph-loop" },
|
||||||
|
{ value: "preemptive-compaction", label: "preemptive-compaction" },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const OMO_DISABLEABLE_SKILLS = [
|
||||||
|
{ value: "playwright", label: "playwright" },
|
||||||
|
{ value: "agent-browser", label: "agent-browser" },
|
||||||
|
{ value: "git-master", label: "git-master" },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const OMO_DEFAULT_SCHEMA_URL =
|
||||||
|
"https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json";
|
||||||
|
|
||||||
|
export const OMO_SISYPHUS_AGENT_PLACEHOLDER = `{
|
||||||
|
"disabled": false,
|
||||||
|
"default_builder_enabled": false,
|
||||||
|
"planner_enabled": true,
|
||||||
|
"replace_plan": true
|
||||||
|
}`;
|
||||||
|
|
||||||
|
export const OMO_LSP_PLACEHOLDER = `{
|
||||||
|
"typescript-language-server": {
|
||||||
|
"command": ["typescript-language-server", "--stdio"],
|
||||||
|
"extensions": [".ts", ".tsx"],
|
||||||
|
"priority": 10
|
||||||
|
},
|
||||||
|
"pylsp": {
|
||||||
|
"disabled": true
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
export const OMO_EXPERIMENTAL_PLACEHOLDER = `{
|
||||||
|
"truncate_all_tool_outputs": true,
|
||||||
|
"aggressive_truncation": true,
|
||||||
|
"auto_resume": true
|
||||||
|
}`;
|
||||||
|
|
||||||
|
export const OMO_BACKGROUND_TASK_PLACEHOLDER = `{
|
||||||
|
"defaultConcurrency": 5,
|
||||||
|
"providerConcurrency": {
|
||||||
|
"anthropic": 3,
|
||||||
|
"openai": 5,
|
||||||
|
"google": 10
|
||||||
|
},
|
||||||
|
"modelConcurrency": {
|
||||||
|
"anthropic/claude-opus-4-6": 2,
|
||||||
|
"google/gemini-3-flash": 10
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
export const OMO_BROWSER_AUTOMATION_PLACEHOLDER = `{
|
||||||
|
"provider": "playwright"
|
||||||
|
}`;
|
||||||
|
|
||||||
|
export const OMO_CLAUDE_CODE_PLACEHOLDER = `{
|
||||||
|
"mcp": true,
|
||||||
|
"commands": true,
|
||||||
|
"skills": true,
|
||||||
|
"agents": true,
|
||||||
|
"hooks": true,
|
||||||
|
"plugins": true
|
||||||
|
}`;
|
||||||
|
|
||||||
|
export function mergeOmoConfigPreview(
|
||||||
|
global: OmoGlobalConfig,
|
||||||
|
agents: Record<string, Record<string, unknown>>,
|
||||||
|
categories: Record<string, Record<string, unknown>>,
|
||||||
|
otherFieldsStr: string,
|
||||||
|
): Record<string, unknown> {
|
||||||
|
const result: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
if (global.schemaUrl) result["$schema"] = global.schemaUrl;
|
||||||
|
|
||||||
|
if (global.sisyphusAgent) result["sisyphus_agent"] = global.sisyphusAgent;
|
||||||
|
if (global.disabledAgents?.length)
|
||||||
|
result["disabled_agents"] = global.disabledAgents;
|
||||||
|
if (global.disabledMcps?.length)
|
||||||
|
result["disabled_mcps"] = global.disabledMcps;
|
||||||
|
if (global.disabledHooks?.length)
|
||||||
|
result["disabled_hooks"] = global.disabledHooks;
|
||||||
|
if (global.disabledSkills?.length)
|
||||||
|
result["disabled_skills"] = global.disabledSkills;
|
||||||
|
if (global.lsp) result["lsp"] = global.lsp;
|
||||||
|
if (global.experimental) result["experimental"] = global.experimental;
|
||||||
|
if (global.backgroundTask) result["background_task"] = global.backgroundTask;
|
||||||
|
if (global.browserAutomationEngine)
|
||||||
|
result["browser_automation_engine"] = global.browserAutomationEngine;
|
||||||
|
if (global.claudeCode) result["claude_code"] = global.claudeCode;
|
||||||
|
|
||||||
|
if (global.otherFields) {
|
||||||
|
for (const [k, v] of Object.entries(global.otherFields)) {
|
||||||
|
result[k] = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(agents).length > 0) result["agents"] = agents;
|
||||||
|
if (Object.keys(categories).length > 0) result["categories"] = categories;
|
||||||
|
try {
|
||||||
|
const other = JSON.parse(otherFieldsStr || "{}");
|
||||||
|
for (const [k, v] of Object.entries(other)) {
|
||||||
|
result[k] = v;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user