diff --git a/src-tauri/src/opencode_config.rs b/src-tauri/src/opencode_config.rs index 34806d678..36d644a78 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/services/omo.rs b/src-tauri/src/services/omo.rs index 49d5be92c..9e534cee4 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-openagent.jsonc", + "oh-my-openagent.json", + "oh-my-opencode.jsonc", + "oh-my-opencode.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(()) } @@ -451,4 +470,38 @@ mod tests { assert!(obj.contains_key("agents")); assert!(obj.contains_key("disabled_agents")); } + + #[test] + fn test_find_existing_config_prefers_new_name_over_old() { + let dir = tempfile::tempdir().unwrap(); + let old_path = dir.path().join("oh-my-opencode.jsonc"); + let new_path = dir.path().join("oh-my-openagent.jsonc"); + + // Create both old and new files + std::fs::write(&old_path, r#"{"agents":{}}"#).unwrap(); + std::fs::write(&new_path, r#"{"agents":{}}"#).unwrap(); + + let found = OmoService::find_existing_config_path(&STANDARD, dir.path()); + assert_eq!( + found.unwrap(), + new_path, + "When both old and new config files exist, the new name (oh-my-openagent) must be preferred" + ); + } + + #[test] + fn test_find_existing_config_falls_back_to_old_name() { + let dir = tempfile::tempdir().unwrap(); + let old_path = dir.path().join("oh-my-opencode.jsonc"); + + // Only old file exists + std::fs::write(&old_path, r#"{"agents":{}}"#).unwrap(); + + let found = OmoService::find_existing_config_path(&STANDARD, dir.path()); + assert_eq!( + found.unwrap(), + old_path, + "When only the old config file exists, it should still be found" + ); + } } diff --git a/src/components/providers/forms/ProviderPresetSelector.tsx b/src/components/providers/forms/ProviderPresetSelector.tsx index 7a97b480b..d1e3f7bad 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 810de2760..18a830fca 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 df104df18..70b7f85f5 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -714,7 +714,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 e663263b9..82fed222f 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -714,7 +714,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 76b94e8ce..8c435e5fe 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -714,7 +714,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 406b8c021..b2e106bd3 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,