diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 09bb312b..04bb6da3 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -22,10 +22,10 @@ pub mod skill; mod stream_check; mod sync_support; +mod lightweight; mod usage; mod webdav_sync; mod workspace; -mod lightweight; pub use auth::*; pub use config::*; @@ -48,7 +48,7 @@ pub use settings::*; pub use skill::*; pub use stream_check::*; +pub use lightweight::*; pub use usage::*; pub use webdav_sync::*; pub use workspace::*; -pub use lightweight::*; diff --git a/src-tauri/src/lightweight.rs b/src-tauri/src/lightweight.rs index cd087571..c16af79e 100644 --- a/src-tauri/src/lightweight.rs +++ b/src-tauri/src/lightweight.rs @@ -87,4 +87,4 @@ pub fn exit_lightweight_mode(app: &tauri::AppHandle) -> Result<(), String> { pub fn is_lightweight_mode() -> bool { LIGHTWEIGHT_MODE.load(Ordering::Acquire) -} \ No newline at end of file +} diff --git a/src-tauri/src/opencode_config.rs b/src-tauri/src/opencode_config.rs index 34806d67..36d644a7 100644 --- a/src-tauri/src/opencode_config.rs +++ b/src-tauri/src/opencode_config.rs @@ -6,6 +6,32 @@ use indexmap::IndexMap; use serde_json::{json, Map, Value}; use std::path::PathBuf; +const STANDARD_OMO_PLUGIN_PREFIXES: [&str; 2] = ["oh-my-openagent", "oh-my-opencode"]; +const SLIM_OMO_PLUGIN_PREFIXES: [&str; 1] = ["oh-my-opencode-slim"]; + +fn matches_plugin_prefix(plugin_name: &str, prefix: &str) -> bool { + plugin_name == prefix + || plugin_name + .strip_prefix(prefix) + .map(|suffix| suffix.starts_with('@')) + .unwrap_or(false) +} + +fn matches_any_plugin_prefix(plugin_name: &str, prefixes: &[&str]) -> bool { + prefixes + .iter() + .any(|prefix| matches_plugin_prefix(plugin_name, prefix)) +} + +fn canonicalize_plugin_name(plugin_name: &str) -> String { + if let Some(suffix) = plugin_name.strip_prefix("oh-my-opencode") { + if suffix.is_empty() || suffix.starts_with('@') { + return format!("oh-my-openagent{suffix}"); + } + } + plugin_name.to_string() +} + pub fn get_opencode_dir() -> PathBuf { if let Some(override_dir) = get_opencode_override_dir() { return override_dir; @@ -140,58 +166,56 @@ pub fn remove_mcp_server(id: &str) -> Result<(), AppError> { pub fn add_plugin(plugin_name: &str) -> Result<(), AppError> { let mut config = read_opencode_config()?; + let normalized_plugin_name = canonicalize_plugin_name(plugin_name); let plugins = config.get_mut("plugin").and_then(|v| v.as_array_mut()); match plugins { Some(arr) => { // Mutual exclusion: standard OMO and OMO Slim cannot coexist as plugins - if plugin_name.starts_with("oh-my-opencode") - && !plugin_name.starts_with("oh-my-opencode-slim") - { - // Adding standard OMO -> remove all Slim variants - arr.retain(|v| { - v.as_str() - .map(|s| !s.starts_with("oh-my-opencode-slim")) - .unwrap_or(true) - }); - } else if plugin_name.starts_with("oh-my-opencode-slim") { - // Adding Slim -> remove all standard OMO variants (but keep slim) + if matches_any_plugin_prefix(&normalized_plugin_name, &STANDARD_OMO_PLUGIN_PREFIXES) { arr.retain(|v| { v.as_str() .map(|s| { - !s.starts_with("oh-my-opencode") || s.starts_with("oh-my-opencode-slim") + !matches_any_plugin_prefix(s, &STANDARD_OMO_PLUGIN_PREFIXES) + && !matches_any_plugin_prefix(s, &SLIM_OMO_PLUGIN_PREFIXES) + }) + .unwrap_or(true) + }); + } else if matches_any_plugin_prefix(&normalized_plugin_name, &SLIM_OMO_PLUGIN_PREFIXES) + { + arr.retain(|v| { + v.as_str() + .map(|s| { + !matches_any_plugin_prefix(s, &STANDARD_OMO_PLUGIN_PREFIXES) + && !matches_any_plugin_prefix(s, &SLIM_OMO_PLUGIN_PREFIXES) }) .unwrap_or(true) }); } - let already_exists = arr.iter().any(|v| v.as_str() == Some(plugin_name)); + let already_exists = arr + .iter() + .any(|v| v.as_str() == Some(normalized_plugin_name.as_str())); if !already_exists { - arr.push(Value::String(plugin_name.to_string())); + arr.push(Value::String(normalized_plugin_name)); } } None => { - config["plugin"] = json!([plugin_name]); + config["plugin"] = json!([normalized_plugin_name]); } } write_opencode_config(&config) } -pub fn remove_plugin_by_prefix(prefix: &str) -> Result<(), AppError> { +pub fn remove_plugins_by_prefixes(prefixes: &[&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('-') - }) + .map(|s| !matches_any_plugin_prefix(s, prefixes)) .unwrap_or(true) }); diff --git a/src-tauri/src/proxy/providers/streaming_responses.rs b/src-tauri/src/proxy/providers/streaming_responses.rs index 4ad941f0..ea9274ff 100644 --- a/src-tauri/src/proxy/providers/streaming_responses.rs +++ b/src-tauri/src/proxy/providers/streaming_responses.rs @@ -974,7 +974,9 @@ mod tests { "data: {\"type\":\"response.completed\",\"response\":{\"status\":\"completed\",\"usage\":{\"input_tokens\":5,\"output_tokens\":2}}}\n\n" ); - let upstream = stream::iter(vec![Ok(Bytes::from(input.as_bytes().to_vec()))]); + let upstream = stream::iter(vec![Ok::<_, std::io::Error>(Bytes::from( + input.as_bytes().to_vec(), + ))]); let converted = create_anthropic_sse_stream_from_responses(upstream); let chunks: Vec<_> = converted.collect().await; let events: Vec = chunks diff --git a/src-tauri/src/services/omo.rs b/src-tauri/src/services/omo.rs index 49d5be92..09b06e8b 100644 --- a/src-tauri/src/services/omo.rs +++ b/src-tauri/src/services/omo.rs @@ -21,33 +21,41 @@ type OmoProfileData = (Option, Option, Option); // ── Variant descriptor ───────────────────────────────────────── pub struct OmoVariant { - pub filename: &'static str, + pub preferred_filename: &'static str, + pub config_candidates: &'static [&'static str], pub category: &'static str, pub provider_prefix: &'static str, pub plugin_name: &'static str, - pub plugin_prefix: &'static str, + pub plugin_prefixes: &'static [&'static str], pub has_categories: bool, pub label: &'static str, pub import_label: &'static str, } pub const STANDARD: OmoVariant = OmoVariant { - filename: "oh-my-opencode.jsonc", + preferred_filename: "oh-my-openagent.jsonc", + config_candidates: &[ + "oh-my-opencode.jsonc", + "oh-my-opencode.json", + "oh-my-openagent.jsonc", + "oh-my-openagent.json", + ], category: "omo", provider_prefix: "omo-", - plugin_name: "oh-my-opencode@latest", - plugin_prefix: "oh-my-opencode", + plugin_name: "oh-my-openagent@latest", + plugin_prefixes: &["oh-my-openagent", "oh-my-opencode"], has_categories: true, label: "OMO", import_label: "Imported", }; pub const SLIM: OmoVariant = OmoVariant { - filename: "oh-my-opencode-slim.jsonc", + preferred_filename: "oh-my-opencode-slim.jsonc", + config_candidates: &["oh-my-opencode-slim.jsonc", "oh-my-opencode-slim.json"], category: "omo-slim", provider_prefix: "omo-slim-", plugin_name: "oh-my-opencode-slim@latest", - plugin_prefix: "oh-my-opencode-slim", + plugin_prefixes: &["oh-my-opencode-slim"], has_categories: false, label: "OMO Slim", import_label: "Imported Slim", @@ -60,22 +68,27 @@ pub struct OmoService; impl OmoService { // ── Path helpers ──────────────────────────────────────── + fn config_candidates(v: &OmoVariant, base_dir: &Path) -> Vec { + v.config_candidates + .iter() + .map(|name| base_dir.join(name)) + .collect() + } + + fn find_existing_config_path(v: &OmoVariant, base_dir: &Path) -> Option { + Self::config_candidates(v, base_dir) + .into_iter() + .find(|path| path.exists()) + } + fn config_path(v: &OmoVariant) -> PathBuf { - get_opencode_dir().join(v.filename) + let base_dir = get_opencode_dir(); + Self::find_existing_config_path(v, &base_dir) + .unwrap_or_else(|| base_dir.join(v.preferred_filename)) } fn resolve_local_config_path(v: &OmoVariant) -> Result { - let config_path = Self::config_path(v); - 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) + Self::find_existing_config_path(v, &get_opencode_dir()).ok_or(AppError::OmoConfigNotFound) } fn read_jsonc_object(path: &Path) -> Result, AppError> { @@ -123,12 +136,18 @@ impl OmoService { // ── Public API (variant-parameterized) ───────────────── pub fn delete_config_file(v: &OmoVariant) -> Result<(), AppError> { - let config_path = Self::config_path(v); - if config_path.exists() { - std::fs::remove_file(&config_path).map_err(|e| AppError::io(&config_path, e))?; - log::info!("{} config file deleted: {config_path:?}", v.label); + let base_dir = get_opencode_dir(); + let mut deleted_paths = Vec::new(); + for config_path in Self::config_candidates(v, &base_dir) { + if config_path.exists() { + std::fs::remove_file(&config_path).map_err(|e| AppError::io(&config_path, e))?; + deleted_paths.push(config_path); + } } - crate::opencode_config::remove_plugin_by_prefix(v.plugin_prefix)?; + if !deleted_paths.is_empty() { + log::info!("{} config files deleted: {deleted_paths:?}", v.label); + } + crate::opencode_config::remove_plugins_by_prefixes(v.plugin_prefixes)?; Ok(()) } diff --git a/src-tauri/src/tray.rs b/src-tauri/src/tray.rs index f5b06815..2702cb82 100644 --- a/src-tauri/src/tray.rs +++ b/src-tauri/src/tray.rs @@ -393,11 +393,10 @@ pub fn create_tray_menu( true, crate::lightweight::is_lightweight_mode(), None::<&str>, - ).map_err(|e| AppError::Message(format!("创建轻量模式菜单失败: {e}")))?; + ) + .map_err(|e| AppError::Message(format!("创建轻量模式菜单失败: {e}")))?; - menu_builder = menu_builder - .item(&lightweight_item) - .separator(); + menu_builder = menu_builder.item(&lightweight_item).separator(); // 退出菜单(分隔符已在上面的 section 循环中添加) let quit_item = MenuItem::with_id(app, "quit", tray_texts.quit, true, None::<&str>) diff --git a/src/components/providers/forms/ProviderPresetSelector.tsx b/src/components/providers/forms/ProviderPresetSelector.tsx index 7a97b480..d1e3f7ba 100644 --- a/src/components/providers/forms/ProviderPresetSelector.tsx +++ b/src/components/providers/forms/ProviderPresetSelector.tsx @@ -65,7 +65,7 @@ export function ProviderPresetSelector({ case "omo": return t("providerForm.omoHint", { defaultValue: - "💡 OMO 配置管理 Agent 模型分配,写入 oh-my-opencode.jsonc", + "💡 OMO 配置管理 Agent 模型分配,兼容 oh-my-openagent.jsonc / oh-my-opencode.jsonc", }); default: return t("providerPreset.hint", { diff --git a/src/config/opencodeProviderPresets.ts b/src/config/opencodeProviderPresets.ts index 810de276..18a830fc 100644 --- a/src/config/opencodeProviderPresets.ts +++ b/src/config/opencodeProviderPresets.ts @@ -1343,7 +1343,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [ { name: "Oh My OpenCode", - websiteUrl: "https://github.com/code-yeongyu/oh-my-opencode", + websiteUrl: "https://github.com/code-yeongyu/oh-my-openagent", settingsConfig: { npm: "", options: {}, diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index ca639040..de23a8e7 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -698,7 +698,7 @@ "aggregatorApiKeyHint": "💡 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", - "omoHint": "💡 OMO config manages Agent model assignments and writes to oh-my-opencode.jsonc", + "omoHint": "💡 OMO config manages Agent model assignments and supports both oh-my-openagent.jsonc and oh-my-opencode.jsonc", "officialHint": "💡 Official provider uses browser login, no API Key needed", "getApiKey": "Get API Key", "partnerPromotion": { diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 76f4808a..5cf563dd 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -698,7 +698,7 @@ "aggregatorApiKeyHint": "💡 API Key のみ入力すれば OK。エンドポイントはプリセット済みです", "thirdPartyApiKeyHint": "💡 API Key のみ入力すれば OK。エンドポイントはプリセット済みです", "customApiKeyHint": "💡 カスタム設定では必要な項目をすべて手動で入力してください", - "omoHint": "💡 OMO 設定は Agent のモデル割り当てを管理し、oh-my-opencode.jsonc に書き込みます", + "omoHint": "💡 OMO 設定は Agent のモデル割り当てを管理し、oh-my-openagent.jsonc / oh-my-opencode.jsonc の両方に対応します", "officialHint": "💡 公式プロバイダーはブラウザログインで、API Key は不要です", "getApiKey": "API Key を取得", "partnerPromotion": { diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 1b80ae69..84663905 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -698,7 +698,7 @@ "aggregatorApiKeyHint": "💡 只需填写 API Key,请求地址已预设", "thirdPartyApiKeyHint": "💡 只需填写 API Key,请求地址已预设", "customApiKeyHint": "💡 自定义配置需手动填写所有必要字段", - "omoHint": "💡 OMO 配置管理 Agent 模型分配,写入 oh-my-opencode.jsonc", + "omoHint": "💡 OMO 配置管理 Agent 模型分配,兼容 oh-my-openagent.jsonc / oh-my-opencode.jsonc", "officialHint": "💡 官方供应商使用浏览器登录,无需配置 API Key", "getApiKey": "获取 API Key", "partnerPromotion": { diff --git a/src/types/omo.ts b/src/types/omo.ts index 406b8c02..b2e106bd 100644 --- a/src/types/omo.ts +++ b/src/types/omo.ts @@ -246,7 +246,7 @@ export const OMO_DISABLEABLE_SKILLS = [ ] as const; export const OMO_DEFAULT_SCHEMA_URL = - "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json"; + "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json"; export const OMO_SISYPHUS_AGENT_PLACEHOLDER = `{ "disabled": false,