mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-05-12 23:23:20 +08:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 34a90b1c35 | |||
| 46b7f3d07a | |||
| 67e074c0a7 | |||
| b1c7fe5563 |
@@ -0,0 +1,14 @@
|
|||||||
|
#[tauri::command]
|
||||||
|
pub fn enter_lightweight_mode(app: tauri::AppHandle) -> Result<(), String> {
|
||||||
|
crate::lightweight::enter_lightweight_mode(&app)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn exit_lightweight_mode(app: tauri::AppHandle) -> Result<(), String> {
|
||||||
|
crate::lightweight::exit_lightweight_mode(&app)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn is_lightweight_mode() -> bool {
|
||||||
|
crate::lightweight::is_lightweight_mode()
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ pub mod skill;
|
|||||||
mod stream_check;
|
mod stream_check;
|
||||||
mod sync_support;
|
mod sync_support;
|
||||||
|
|
||||||
|
mod lightweight;
|
||||||
mod usage;
|
mod usage;
|
||||||
mod webdav_sync;
|
mod webdav_sync;
|
||||||
mod workspace;
|
mod workspace;
|
||||||
@@ -47,6 +48,7 @@ pub use settings::*;
|
|||||||
pub use skill::*;
|
pub use skill::*;
|
||||||
pub use stream_check::*;
|
pub use stream_check::*;
|
||||||
|
|
||||||
|
pub use lightweight::*;
|
||||||
pub use usage::*;
|
pub use usage::*;
|
||||||
pub use webdav_sync::*;
|
pub use webdav_sync::*;
|
||||||
pub use workspace::*;
|
pub use workspace::*;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ mod error;
|
|||||||
mod gemini_config;
|
mod gemini_config;
|
||||||
mod gemini_mcp;
|
mod gemini_mcp;
|
||||||
mod init_status;
|
mod init_status;
|
||||||
|
mod lightweight;
|
||||||
mod mcp;
|
mod mcp;
|
||||||
mod openclaw_config;
|
mod openclaw_config;
|
||||||
mod opencode_config;
|
mod opencode_config;
|
||||||
@@ -204,6 +205,12 @@ pub fn run() {
|
|||||||
log::debug!(" arg[{i}]: {}", redact_url_for_log(arg));
|
log::debug!(" arg[{i}]: {}", redact_url_for_log(arg));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if crate::lightweight::is_lightweight_mode() {
|
||||||
|
if let Err(e) = crate::lightweight::exit_lightweight_mode(app) {
|
||||||
|
log::error!("退出轻量模式重建窗口失败: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check for deep link URL in args (mainly for Windows/Linux command line)
|
// Check for deep link URL in args (mainly for Windows/Linux command line)
|
||||||
let mut found_deeplink = false;
|
let mut found_deeplink = false;
|
||||||
for arg in &args {
|
for arg in &args {
|
||||||
@@ -615,6 +622,12 @@ pub fn run() {
|
|||||||
let urls = event.urls();
|
let urls = event.urls();
|
||||||
log::info!("Received {} URL(s)", urls.len());
|
log::info!("Received {} URL(s)", urls.len());
|
||||||
|
|
||||||
|
if crate::lightweight::is_lightweight_mode() {
|
||||||
|
if let Err(e) = crate::lightweight::exit_lightweight_mode(&app_handle) {
|
||||||
|
log::error!("退出轻量模式重建窗口失败: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (i, url) in urls.iter().enumerate() {
|
for (i, url) in urls.iter().enumerate() {
|
||||||
let url_str = url.as_str();
|
let url_str = url.as_str();
|
||||||
log::debug!(" URL[{i}]: {}", redact_url_for_log(url_str));
|
log::debug!(" URL[{i}]: {}", redact_url_for_log(url_str));
|
||||||
@@ -1085,6 +1098,10 @@ pub fn run() {
|
|||||||
commands::delete_daily_memory_file,
|
commands::delete_daily_memory_file,
|
||||||
commands::search_daily_memory_files,
|
commands::search_daily_memory_files,
|
||||||
commands::open_workspace_directory,
|
commands::open_workspace_directory,
|
||||||
|
// lightweight mode (for testing or low-resource environments)
|
||||||
|
commands::enter_lightweight_mode,
|
||||||
|
commands::exit_lightweight_mode,
|
||||||
|
commands::is_lightweight_mode,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let app = builder
|
let app = builder
|
||||||
@@ -1135,6 +1152,10 @@ pub fn run() {
|
|||||||
let _ = window.show();
|
let _ = window.show();
|
||||||
let _ = window.set_focus();
|
let _ = window.set_focus();
|
||||||
tray::apply_tray_policy(app_handle, true);
|
tray::apply_tray_policy(app_handle, true);
|
||||||
|
} else if crate::lightweight::is_lightweight_mode() {
|
||||||
|
if let Err(e) = crate::lightweight::exit_lightweight_mode(app_handle) {
|
||||||
|
log::error!("退出轻量模式重建窗口失败: {e}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 处理通过自定义 URL 协议触发的打开事件(例如 ccswitch://...)
|
// 处理通过自定义 URL 协议触发的打开事件(例如 ccswitch://...)
|
||||||
@@ -1144,6 +1165,13 @@ pub fn run() {
|
|||||||
log::info!("RunEvent::Opened with URL: {url_str}");
|
log::info!("RunEvent::Opened with URL: {url_str}");
|
||||||
|
|
||||||
if url_str.starts_with("ccswitch://") {
|
if url_str.starts_with("ccswitch://") {
|
||||||
|
if crate::lightweight::is_lightweight_mode() {
|
||||||
|
if let Err(e) = crate::lightweight::exit_lightweight_mode(app_handle)
|
||||||
|
{
|
||||||
|
log::error!("退出轻量模式重建窗口失败: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 解析并广播深链接事件,复用与 single_instance 相同的逻辑
|
// 解析并广播深链接事件,复用与 single_instance 相同的逻辑
|
||||||
match crate::deeplink::parse_deeplink_url(&url_str) {
|
match crate::deeplink::parse_deeplink_url(&url_str) {
|
||||||
Ok(request) => {
|
Ok(request) => {
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
|
||||||
|
use tauri::Manager;
|
||||||
|
|
||||||
|
static LIGHTWEIGHT_MODE: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
|
pub fn enter_lightweight_mode(app: &tauri::AppHandle) -> Result<(), String> {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
|
let _ = window.set_skip_taskbar(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
crate::tray::apply_tray_policy(app, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
|
window
|
||||||
|
.destroy()
|
||||||
|
.map_err(|e| format!("销毁主窗口失败: {e}"))?;
|
||||||
|
}
|
||||||
|
// else: already in lightweight mode or window not found, just set the flag
|
||||||
|
|
||||||
|
LIGHTWEIGHT_MODE.store(true, Ordering::Release);
|
||||||
|
crate::tray::refresh_tray_menu(app);
|
||||||
|
log::info!("进入轻量模式");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn exit_lightweight_mode(app: &tauri::AppHandle) -> Result<(), String> {
|
||||||
|
use tauri::WebviewWindowBuilder;
|
||||||
|
|
||||||
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
|
let _ = window.unminimize();
|
||||||
|
let _ = window.show();
|
||||||
|
let _ = window.set_focus();
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
let _ = window.set_skip_taskbar(false);
|
||||||
|
}
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
crate::tray::apply_tray_policy(app, true);
|
||||||
|
}
|
||||||
|
LIGHTWEIGHT_MODE.store(false, Ordering::Release);
|
||||||
|
crate::tray::refresh_tray_menu(app);
|
||||||
|
log::info!("退出轻量模式");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let window_config = app
|
||||||
|
.config()
|
||||||
|
.app
|
||||||
|
.windows
|
||||||
|
.iter()
|
||||||
|
.find(|w| w.label == "main")
|
||||||
|
.ok_or("主窗口配置未找到")?;
|
||||||
|
|
||||||
|
WebviewWindowBuilder::from_config(app, window_config)
|
||||||
|
.map_err(|e| format!("加载主窗口配置失败: {e}"))?
|
||||||
|
.visible(true)
|
||||||
|
.build()
|
||||||
|
.map_err(|e| format!("创建主窗口失败: {e}"))?;
|
||||||
|
|
||||||
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
|
let _ = window.set_focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
|
let _ = window.set_skip_taskbar(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
crate::tray::apply_tray_policy(app, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
LIGHTWEIGHT_MODE.store(false, Ordering::Release);
|
||||||
|
crate::tray::refresh_tray_menu(app);
|
||||||
|
log::info!("退出轻量模式");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_lightweight_mode() -> bool {
|
||||||
|
LIGHTWEIGHT_MODE.load(Ordering::Acquire)
|
||||||
|
}
|
||||||
@@ -6,6 +6,32 @@ use indexmap::IndexMap;
|
|||||||
use serde_json::{json, Map, Value};
|
use serde_json::{json, Map, Value};
|
||||||
use std::path::PathBuf;
|
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 {
|
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;
|
||||||
@@ -140,58 +166,56 @@ pub fn remove_mcp_server(id: &str) -> Result<(), AppError> {
|
|||||||
|
|
||||||
pub fn add_plugin(plugin_name: &str) -> Result<(), AppError> {
|
pub fn add_plugin(plugin_name: &str) -> Result<(), AppError> {
|
||||||
let mut config = read_opencode_config()?;
|
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());
|
let plugins = config.get_mut("plugin").and_then(|v| v.as_array_mut());
|
||||||
|
|
||||||
match plugins {
|
match plugins {
|
||||||
Some(arr) => {
|
Some(arr) => {
|
||||||
// Mutual exclusion: standard OMO and OMO Slim cannot coexist as plugins
|
// Mutual exclusion: standard OMO and OMO Slim cannot coexist as plugins
|
||||||
if plugin_name.starts_with("oh-my-opencode")
|
if matches_any_plugin_prefix(&normalized_plugin_name, &STANDARD_OMO_PLUGIN_PREFIXES) {
|
||||||
&& !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)
|
|
||||||
arr.retain(|v| {
|
arr.retain(|v| {
|
||||||
v.as_str()
|
v.as_str()
|
||||||
.map(|s| {
|
.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)
|
.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 {
|
if !already_exists {
|
||||||
arr.push(Value::String(plugin_name.to_string()));
|
arr.push(Value::String(normalized_plugin_name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
config["plugin"] = json!([plugin_name]);
|
config["plugin"] = json!([normalized_plugin_name]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
write_opencode_config(&config)
|
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()?;
|
let mut config = read_opencode_config()?;
|
||||||
|
|
||||||
if let Some(arr) = config.get_mut("plugin").and_then(|v| v.as_array_mut()) {
|
if let Some(arr) = config.get_mut("plugin").and_then(|v| v.as_array_mut()) {
|
||||||
arr.retain(|v| {
|
arr.retain(|v| {
|
||||||
v.as_str()
|
v.as_str()
|
||||||
.map(|s| {
|
.map(|s| !matches_any_plugin_prefix(s, prefixes))
|
||||||
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)
|
.unwrap_or(true)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -974,7 +974,9 @@ mod tests {
|
|||||||
"data: {\"type\":\"response.completed\",\"response\":{\"status\":\"completed\",\"usage\":{\"input_tokens\":5,\"output_tokens\":2}}}\n\n"
|
"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 converted = create_anthropic_sse_stream_from_responses(upstream);
|
||||||
let chunks: Vec<_> = converted.collect().await;
|
let chunks: Vec<_> = converted.collect().await;
|
||||||
let events: Vec<Value> = chunks
|
let events: Vec<Value> = chunks
|
||||||
|
|||||||
@@ -21,33 +21,41 @@ type OmoProfileData = (Option<Value>, Option<Value>, Option<Value>);
|
|||||||
// ── Variant descriptor ─────────────────────────────────────────
|
// ── Variant descriptor ─────────────────────────────────────────
|
||||||
|
|
||||||
pub struct OmoVariant {
|
pub struct OmoVariant {
|
||||||
pub filename: &'static str,
|
pub preferred_filename: &'static str,
|
||||||
|
pub config_candidates: &'static [&'static str],
|
||||||
pub category: &'static str,
|
pub category: &'static str,
|
||||||
pub provider_prefix: &'static str,
|
pub provider_prefix: &'static str,
|
||||||
pub plugin_name: &'static str,
|
pub plugin_name: &'static str,
|
||||||
pub plugin_prefix: &'static str,
|
pub plugin_prefixes: &'static [&'static str],
|
||||||
pub has_categories: bool,
|
pub has_categories: bool,
|
||||||
pub label: &'static str,
|
pub label: &'static str,
|
||||||
pub import_label: &'static str,
|
pub import_label: &'static str,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const STANDARD: OmoVariant = OmoVariant {
|
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",
|
category: "omo",
|
||||||
provider_prefix: "omo-",
|
provider_prefix: "omo-",
|
||||||
plugin_name: "oh-my-opencode@latest",
|
plugin_name: "oh-my-openagent@latest",
|
||||||
plugin_prefix: "oh-my-opencode",
|
plugin_prefixes: &["oh-my-openagent", "oh-my-opencode"],
|
||||||
has_categories: true,
|
has_categories: true,
|
||||||
label: "OMO",
|
label: "OMO",
|
||||||
import_label: "Imported",
|
import_label: "Imported",
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const SLIM: OmoVariant = OmoVariant {
|
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",
|
category: "omo-slim",
|
||||||
provider_prefix: "omo-slim-",
|
provider_prefix: "omo-slim-",
|
||||||
plugin_name: "oh-my-opencode-slim@latest",
|
plugin_name: "oh-my-opencode-slim@latest",
|
||||||
plugin_prefix: "oh-my-opencode-slim",
|
plugin_prefixes: &["oh-my-opencode-slim"],
|
||||||
has_categories: false,
|
has_categories: false,
|
||||||
label: "OMO Slim",
|
label: "OMO Slim",
|
||||||
import_label: "Imported Slim",
|
import_label: "Imported Slim",
|
||||||
@@ -60,22 +68,27 @@ pub struct OmoService;
|
|||||||
impl OmoService {
|
impl OmoService {
|
||||||
// ── Path helpers ────────────────────────────────────────
|
// ── Path helpers ────────────────────────────────────────
|
||||||
|
|
||||||
|
fn config_candidates(v: &OmoVariant, base_dir: &Path) -> Vec<PathBuf> {
|
||||||
|
v.config_candidates
|
||||||
|
.iter()
|
||||||
|
.map(|name| base_dir.join(name))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_existing_config_path(v: &OmoVariant, base_dir: &Path) -> Option<PathBuf> {
|
||||||
|
Self::config_candidates(v, base_dir)
|
||||||
|
.into_iter()
|
||||||
|
.find(|path| path.exists())
|
||||||
|
}
|
||||||
|
|
||||||
fn config_path(v: &OmoVariant) -> PathBuf {
|
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<PathBuf, AppError> {
|
fn resolve_local_config_path(v: &OmoVariant) -> Result<PathBuf, AppError> {
|
||||||
let config_path = Self::config_path(v);
|
Self::find_existing_config_path(v, &get_opencode_dir()).ok_or(AppError::OmoConfigNotFound)
|
||||||
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> {
|
fn read_jsonc_object(path: &Path) -> Result<Map<String, Value>, AppError> {
|
||||||
@@ -123,12 +136,18 @@ impl OmoService {
|
|||||||
// ── Public API (variant-parameterized) ─────────────────
|
// ── Public API (variant-parameterized) ─────────────────
|
||||||
|
|
||||||
pub fn delete_config_file(v: &OmoVariant) -> Result<(), AppError> {
|
pub fn delete_config_file(v: &OmoVariant) -> Result<(), AppError> {
|
||||||
let config_path = Self::config_path(v);
|
let base_dir = get_opencode_dir();
|
||||||
if config_path.exists() {
|
let mut deleted_paths = Vec::new();
|
||||||
std::fs::remove_file(&config_path).map_err(|e| AppError::io(&config_path, e))?;
|
for config_path in Self::config_candidates(v, &base_dir) {
|
||||||
log::info!("{} config file deleted: {config_path:?}", v.label);
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -451,4 +470,38 @@ mod tests {
|
|||||||
assert!(obj.contains_key("agents"));
|
assert!(obj.contains_key("agents"));
|
||||||
assert!(obj.contains_key("disabled_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"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ use crate::store::AppState;
|
|||||||
pub struct TrayTexts {
|
pub struct TrayTexts {
|
||||||
pub show_main: &'static str,
|
pub show_main: &'static str,
|
||||||
pub no_provider_hint: &'static str,
|
pub no_provider_hint: &'static str,
|
||||||
|
pub lightweight_mode: &'static str,
|
||||||
pub quit: &'static str,
|
pub quit: &'static str,
|
||||||
pub _auto_label: &'static str,
|
pub _auto_label: &'static str,
|
||||||
}
|
}
|
||||||
@@ -24,6 +25,7 @@ impl TrayTexts {
|
|||||||
"en" => Self {
|
"en" => Self {
|
||||||
show_main: "Open main window",
|
show_main: "Open main window",
|
||||||
no_provider_hint: " (No providers yet, please add them from the main window)",
|
no_provider_hint: " (No providers yet, please add them from the main window)",
|
||||||
|
lightweight_mode: "Lightweight Mode",
|
||||||
quit: "Quit",
|
quit: "Quit",
|
||||||
_auto_label: "Auto (Failover)",
|
_auto_label: "Auto (Failover)",
|
||||||
},
|
},
|
||||||
@@ -31,12 +33,14 @@ impl TrayTexts {
|
|||||||
show_main: "メインウィンドウを開く",
|
show_main: "メインウィンドウを開く",
|
||||||
no_provider_hint:
|
no_provider_hint:
|
||||||
" (プロバイダーがまだありません。メイン画面から追加してください)",
|
" (プロバイダーがまだありません。メイン画面から追加してください)",
|
||||||
|
lightweight_mode: "軽量モード",
|
||||||
quit: "終了",
|
quit: "終了",
|
||||||
_auto_label: "自動 (フェイルオーバー)",
|
_auto_label: "自動 (フェイルオーバー)",
|
||||||
},
|
},
|
||||||
_ => Self {
|
_ => Self {
|
||||||
show_main: "打开主界面",
|
show_main: "打开主界面",
|
||||||
no_provider_hint: " (无供应商,请在主界面添加)",
|
no_provider_hint: " (无供应商,请在主界面添加)",
|
||||||
|
lightweight_mode: "轻量模式",
|
||||||
quit: "退出",
|
quit: "退出",
|
||||||
_auto_label: "自动 (故障转移)",
|
_auto_label: "自动 (故障转移)",
|
||||||
},
|
},
|
||||||
@@ -382,6 +386,18 @@ pub fn create_tray_menu(
|
|||||||
menu_builder = menu_builder.separator();
|
menu_builder = menu_builder.separator();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let lightweight_item = CheckMenuItem::with_id(
|
||||||
|
app,
|
||||||
|
"lightweight_mode",
|
||||||
|
tray_texts.lightweight_mode,
|
||||||
|
true,
|
||||||
|
crate::lightweight::is_lightweight_mode(),
|
||||||
|
None::<&str>,
|
||||||
|
)
|
||||||
|
.map_err(|e| AppError::Message(format!("创建轻量模式菜单失败: {e}")))?;
|
||||||
|
|
||||||
|
menu_builder = menu_builder.item(&lightweight_item).separator();
|
||||||
|
|
||||||
// 退出菜单(分隔符已在上面的 section 循环中添加)
|
// 退出菜单(分隔符已在上面的 section 循环中添加)
|
||||||
let quit_item = MenuItem::with_id(app, "quit", tray_texts.quit, true, None::<&str>)
|
let quit_item = MenuItem::with_id(app, "quit", tray_texts.quit, true, None::<&str>)
|
||||||
.map_err(|e| AppError::Message(format!("创建退出菜单失败: {e}")))?;
|
.map_err(|e| AppError::Message(format!("创建退出菜单失败: {e}")))?;
|
||||||
@@ -393,6 +409,20 @@ pub fn create_tray_menu(
|
|||||||
.map_err(|e| AppError::Message(format!("构建菜单失败: {e}")))
|
.map_err(|e| AppError::Message(format!("构建菜单失败: {e}")))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn refresh_tray_menu(app: &tauri::AppHandle) {
|
||||||
|
use crate::store::AppState;
|
||||||
|
|
||||||
|
if let Some(state) = app.try_state::<AppState>() {
|
||||||
|
if let Ok(new_menu) = create_tray_menu(app, state.inner()) {
|
||||||
|
if let Some(tray) = app.tray_by_id("main") {
|
||||||
|
if let Err(e) = tray.set_menu(Some(new_menu)) {
|
||||||
|
log::error!("刷新托盘菜单失败: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
pub fn apply_tray_policy(app: &tauri::AppHandle, dock_visible: bool) {
|
pub fn apply_tray_policy(app: &tauri::AppHandle, dock_visible: bool) {
|
||||||
use tauri::ActivationPolicy;
|
use tauri::ActivationPolicy;
|
||||||
@@ -430,6 +460,21 @@ pub fn handle_tray_menu_event(app: &tauri::AppHandle, event_id: &str) {
|
|||||||
{
|
{
|
||||||
apply_tray_policy(app, true);
|
apply_tray_policy(app, true);
|
||||||
}
|
}
|
||||||
|
} else if crate::lightweight::is_lightweight_mode() {
|
||||||
|
if let Err(e) = crate::lightweight::exit_lightweight_mode(app) {
|
||||||
|
log::error!("退出轻量模式重建窗口失败: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"lightweight_mode" => {
|
||||||
|
if crate::lightweight::is_lightweight_mode() {
|
||||||
|
if let Err(e) = crate::lightweight::exit_lightweight_mode(app) {
|
||||||
|
log::error!("退出轻量模式失败: {e}");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let Err(e) = crate::lightweight::enter_lightweight_mode(app) {
|
||||||
|
log::error!("进入轻量模式失败: {e}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"quit" => {
|
"quit" => {
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export function ProviderPresetSelector({
|
|||||||
case "omo":
|
case "omo":
|
||||||
return t("providerForm.omoHint", {
|
return t("providerForm.omoHint", {
|
||||||
defaultValue:
|
defaultValue:
|
||||||
"💡 OMO 配置管理 Agent 模型分配,写入 oh-my-opencode.jsonc",
|
"💡 OMO 配置管理 Agent 模型分配,兼容 oh-my-openagent.jsonc / oh-my-opencode.jsonc",
|
||||||
});
|
});
|
||||||
default:
|
default:
|
||||||
return t("providerPreset.hint", {
|
return t("providerPreset.hint", {
|
||||||
|
|||||||
@@ -1343,7 +1343,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
name: "Oh My OpenCode",
|
name: "Oh My OpenCode",
|
||||||
websiteUrl: "https://github.com/code-yeongyu/oh-my-opencode",
|
websiteUrl: "https://github.com/code-yeongyu/oh-my-openagent",
|
||||||
settingsConfig: {
|
settingsConfig: {
|
||||||
npm: "",
|
npm: "",
|
||||||
options: {},
|
options: {},
|
||||||
|
|||||||
@@ -698,7 +698,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",
|
"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",
|
"officialHint": "💡 Official provider uses browser login, no API Key needed",
|
||||||
"getApiKey": "Get API Key",
|
"getApiKey": "Get API Key",
|
||||||
"partnerPromotion": {
|
"partnerPromotion": {
|
||||||
|
|||||||
@@ -698,7 +698,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 に書き込みます",
|
"omoHint": "💡 OMO 設定は Agent のモデル割り当てを管理し、oh-my-openagent.jsonc / oh-my-opencode.jsonc の両方に対応します",
|
||||||
"officialHint": "💡 公式プロバイダーはブラウザログインで、API Key は不要です",
|
"officialHint": "💡 公式プロバイダーはブラウザログインで、API Key は不要です",
|
||||||
"getApiKey": "API Key を取得",
|
"getApiKey": "API Key を取得",
|
||||||
"partnerPromotion": {
|
"partnerPromotion": {
|
||||||
|
|||||||
@@ -698,7 +698,7 @@
|
|||||||
"aggregatorApiKeyHint": "💡 只需填写 API Key,请求地址已预设",
|
"aggregatorApiKeyHint": "💡 只需填写 API Key,请求地址已预设",
|
||||||
"thirdPartyApiKeyHint": "💡 只需填写 API Key,请求地址已预设",
|
"thirdPartyApiKeyHint": "💡 只需填写 API Key,请求地址已预设",
|
||||||
"customApiKeyHint": "💡 自定义配置需手动填写所有必要字段",
|
"customApiKeyHint": "💡 自定义配置需手动填写所有必要字段",
|
||||||
"omoHint": "💡 OMO 配置管理 Agent 模型分配,写入 oh-my-opencode.jsonc",
|
"omoHint": "💡 OMO 配置管理 Agent 模型分配,兼容 oh-my-openagent.jsonc / oh-my-opencode.jsonc",
|
||||||
"officialHint": "💡 官方供应商使用浏览器登录,无需配置 API Key",
|
"officialHint": "💡 官方供应商使用浏览器登录,无需配置 API Key",
|
||||||
"getApiKey": "获取 API Key",
|
"getApiKey": "获取 API Key",
|
||||||
"partnerPromotion": {
|
"partnerPromotion": {
|
||||||
|
|||||||
+1
-1
@@ -246,7 +246,7 @@ export const OMO_DISABLEABLE_SKILLS = [
|
|||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const OMO_DEFAULT_SCHEMA_URL =
|
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 = `{
|
export const OMO_SISYPHUS_AGENT_PLACEHOLDER = `{
|
||||||
"disabled": false,
|
"disabled": false,
|
||||||
|
|||||||
Reference in New Issue
Block a user