diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock
index d966f0a..3564351 100644
--- a/src-tauri/Cargo.lock
+++ b/src-tauri/Cargo.lock
@@ -579,6 +579,7 @@ dependencies = [
"serial_test",
"tauri",
"tauri-build",
+ "tauri-plugin-deep-link",
"tauri-plugin-dialog",
"tauri-plugin-log",
"tauri-plugin-opener",
@@ -591,6 +592,7 @@ dependencies = [
"tokio",
"toml 0.8.2",
"toml_edit 0.22.27",
+ "url",
]
[[package]]
@@ -665,6 +667,26 @@ dependencies = [
"crossbeam-utils",
]
+[[package]]
+name = "const-random"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
+dependencies = [
+ "const-random-macro",
+]
+
+[[package]]
+name = "const-random-macro"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
+dependencies = [
+ "getrandom 0.2.16",
+ "once_cell",
+ "tiny-keccak",
+]
+
[[package]]
name = "convert_case"
version = "0.4.0"
@@ -754,6 +776,12 @@ version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+[[package]]
+name = "crunchy"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
+
[[package]]
name = "crypto-common"
version = "0.1.6"
@@ -983,6 +1011,15 @@ dependencies = [
"syn 2.0.106",
]
+[[package]]
+name = "dlv-list"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f"
+dependencies = [
+ "const-random",
+]
+
[[package]]
name = "downcast-rs"
version = "1.2.1"
@@ -1671,6 +1708,12 @@ dependencies = [
"ahash",
]
+[[package]]
+name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+
[[package]]
name = "hashbrown"
version = "0.16.0"
@@ -1747,6 +1790,12 @@ dependencies = [
"pin-project-lite",
]
+[[package]]
+name = "http-range"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
+
[[package]]
name = "httparse"
version = "1.10.1"
@@ -2778,6 +2827,16 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+[[package]]
+name = "ordered-multimap"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79"
+dependencies = [
+ "dlv-list",
+ "hashbrown 0.14.5",
+]
+
[[package]]
name = "ordered-stream"
version = "0.2.0"
@@ -3623,6 +3682,16 @@ dependencies = [
"cc",
]
+[[package]]
+name = "rust-ini"
+version = "0.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7"
+dependencies = [
+ "cfg-if",
+ "ordered-multimap",
+]
+
[[package]]
name = "rust_decimal"
version = "1.38.0"
@@ -4362,6 +4431,7 @@ dependencies = [
"gtk",
"heck 0.5.0",
"http",
+ "http-range",
"jni",
"libc",
"log",
@@ -4477,6 +4547,27 @@ dependencies = [
"walkdir",
]
+[[package]]
+name = "tauri-plugin-deep-link"
+version = "2.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e82759f7c7d51de3cbde51c04b3f2332de52436ed84541182cd8944b04e9e73"
+dependencies = [
+ "dunce",
+ "plist",
+ "rust-ini",
+ "serde",
+ "serde_json",
+ "tauri",
+ "tauri-plugin",
+ "tauri-utils",
+ "thiserror 2.0.17",
+ "tracing",
+ "url",
+ "windows-registry",
+ "windows-result 0.3.4",
+]
+
[[package]]
name = "tauri-plugin-dialog"
version = "2.4.0"
@@ -4831,6 +4922,15 @@ dependencies = [
"time-core",
]
+[[package]]
+name = "tiny-keccak"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
+dependencies = [
+ "crunchy",
+]
+
[[package]]
name = "tinystr"
version = "0.8.1"
@@ -5754,6 +5854,17 @@ dependencies = [
"windows-link 0.1.3",
]
+[[package]]
+name = "windows-registry"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e"
+dependencies = [
+ "windows-link 0.1.3",
+ "windows-result 0.3.4",
+ "windows-strings 0.4.2",
+]
+
[[package]]
name = "windows-result"
version = "0.3.4"
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index 066182b..77656d8 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -26,13 +26,14 @@ serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
log = "0.4"
chrono = "0.4"
-tauri = { version = "2.8.2", features = ["tray-icon"] }
+tauri = { version = "2.8.2", features = ["tray-icon", "protocol-asset"] }
tauri-plugin-log = "2"
tauri-plugin-opener = "2"
tauri-plugin-process = "2"
tauri-plugin-updater = "2"
tauri-plugin-dialog = "2"
tauri-plugin-store = "2"
+tauri-plugin-deep-link = "2"
dirs = "5.0"
toml = "0.8"
toml_edit = "0.22"
@@ -42,6 +43,7 @@ futures = "0.3"
regex = "1.10"
rquickjs = { version = "0.8", features = ["array-buffer", "classes"] }
thiserror = "1.0"
+url = "2.5"
[target.'cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))'.dependencies]
tauri-plugin-single-instance = "2"
diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs
index 90d7516..ab99fca 100644
--- a/src-tauri/src/commands/mod.rs
+++ b/src-tauri/src/commands/mod.rs
@@ -1,6 +1,7 @@
#![allow(non_snake_case)]
mod config;
+mod deeplink;
mod import_export;
mod mcp;
mod misc;
@@ -10,6 +11,7 @@ mod provider;
mod settings;
pub use config::*;
+pub use deeplink::*;
pub use import_export::*;
pub use mcp::*;
pub use misc::*;
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index 9e8852e..2f9def5 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -5,6 +5,7 @@ mod claude_plugin;
mod codex_config;
mod commands;
mod config;
+mod deeplink;
mod error;
mod gemini_config; // 新增
mod gemini_mcp;
@@ -22,6 +23,7 @@ pub use app_config::{AppType, McpApps, McpServer, MultiAppConfig};
pub use codex_config::{get_codex_auth_path, get_codex_config_path, write_codex_live_atomic};
pub use commands::*;
pub use config::{get_claude_mcp_path, get_claude_settings_path, read_json_file};
+pub use deeplink::{import_provider_from_deeplink, parse_deeplink_url, DeepLinkImportRequest};
pub use error::AppError;
pub use mcp::{
import_from_claude, import_from_codex, import_from_gemini, remove_server_from_claude,
@@ -35,6 +37,7 @@ pub use services::{
};
pub use settings::{update_settings, AppSettings};
pub use store::AppState;
+use tauri_plugin_deep_link::DeepLinkExt;
use tauri::{
menu::{CheckMenuItem, Menu, MenuBuilder, MenuItem},
@@ -281,6 +284,65 @@ fn handle_tray_menu_event(app: &tauri::AppHandle, event_id: &str) {
}
}
+/// 统一处理 ccswitch:// 深链接 URL
+///
+/// - 解析 URL
+/// - 向前端发射 `deeplink-import` / `deeplink-error` 事件
+/// - 可选:在成功时聚焦主窗口
+fn handle_deeplink_url(
+ app: &tauri::AppHandle,
+ url_str: &str,
+ focus_main_window: bool,
+ source: &str,
+) -> bool {
+ if !url_str.starts_with("ccswitch://") {
+ return false;
+ }
+
+ log::info!("✓ Deep link URL detected from {source}: {url_str}");
+
+ match crate::deeplink::parse_deeplink_url(url_str) {
+ Ok(request) => {
+ log::info!(
+ "✓ Successfully parsed deep link: resource={}, app={}, name={}",
+ request.resource,
+ request.app,
+ request.name
+ );
+
+ if let Err(e) = app.emit("deeplink-import", &request) {
+ log::error!("✗ Failed to emit deeplink-import event: {e}");
+ } else {
+ log::info!("✓ Emitted deeplink-import event to frontend");
+ }
+
+ if focus_main_window {
+ if let Some(window) = app.get_webview_window("main") {
+ let _ = window.unminimize();
+ let _ = window.show();
+ let _ = window.set_focus();
+ log::info!("✓ Window shown and focused");
+ }
+ }
+ }
+ Err(e) => {
+ log::error!("✗ Failed to parse deep link URL: {e}");
+
+ if let Err(emit_err) = app.emit(
+ "deeplink-error",
+ serde_json::json!({
+ "url": url_str,
+ "error": e.to_string()
+ }),
+ ) {
+ log::error!("✗ Failed to emit deeplink-error event: {emit_err}");
+ }
+ }
+ }
+
+ true
+}
+
//
/// 内部切换供应商函数
@@ -346,7 +408,27 @@ pub fn run() {
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
{
- builder = builder.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
+ builder = builder.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
+ log::info!("=== Single Instance Callback Triggered ===");
+ log::info!("Args count: {}", args.len());
+ for (i, arg) in args.iter().enumerate() {
+ log::info!(" arg[{i}]: {arg}");
+ }
+
+ // Check for deep link URL in args (mainly for Windows/Linux command line)
+ let mut found_deeplink = false;
+ for arg in &args {
+ if handle_deeplink_url(app, arg, false, "single_instance args") {
+ found_deeplink = true;
+ break;
+ }
+ }
+
+ if !found_deeplink {
+ log::info!("ℹ No deep link URL found in args (this is expected on macOS when launched via system)");
+ }
+
+ // Show and focus window regardless
if let Some(window) = app.get_webview_window("main") {
let _ = window.unminimize();
let _ = window.show();
@@ -356,6 +438,8 @@ pub fn run() {
}
let builder = builder
+ // 注册 deep-link 插件(处理 macOS AppleEvent 和其他平台的深链接)
+ .plugin(tauri_plugin_deep_link::init())
// 拦截窗口关闭:根据设置决定是否最小化到托盘
.on_window_event(|window, event| {
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
@@ -471,7 +555,40 @@ pub fn run() {
config_guard.ensure_app(&app_config::AppType::Codex);
}
- // 启动阶段不再无条件保存,避免意外覆盖用户配置。
+ // 启动阶段不再无条件保存,避免意外覆盖用户配置。
+
+ // 注册 deep-link URL 处理器(使用正确的 DeepLinkExt API)
+ log::info!("=== Registering deep-link URL handler ===");
+
+ // Linux 和 Windows 调试模式需要显式注册
+ #[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
+ {
+ if let Err(e) = app.deep_link().register_all() {
+ log::error!("✗ Failed to register deep link schemes: {}", e);
+ } else {
+ log::info!("✓ Deep link schemes registered (Linux/Windows)");
+ }
+ }
+
+ // 注册 URL 处理回调(所有平台通用)
+ app.deep_link().on_open_url({
+ let app_handle = app.handle().clone();
+ move |event| {
+ log::info!("=== Deep Link Event Received (on_open_url) ===");
+ let urls = event.urls();
+ log::info!("Received {} URL(s)", urls.len());
+
+ for (i, url) in urls.iter().enumerate() {
+ let url_str = url.as_str();
+ log::info!(" URL[{i}]: {url_str}");
+
+ if handle_deeplink_url(&app_handle, url_str, true, "on_open_url") {
+ break; // Process only first ccswitch:// URL
+ }
+ }
+ }
+ });
+ log::info!("✓ Deep-link URL handler registered");
// 创建动态托盘菜单
let menu = create_tray_menu(app.handle(), &app_state)?;
@@ -572,6 +689,9 @@ pub fn run() {
commands::save_file_dialog,
commands::open_file_dialog,
commands::sync_current_providers_live,
+ // Deep link import
+ commands::parse_deeplink,
+ commands::import_from_deeplink,
update_tray_menu,
]);
@@ -581,17 +701,74 @@ pub fn run() {
app.run(|app_handle, event| {
#[cfg(target_os = "macos")]
- // macOS 在 Dock 图标被点击并重新激活应用时会触发 Reopen 事件,这里手动恢复主窗口
- if let RunEvent::Reopen { .. } = event {
- if let Some(window) = app_handle.get_webview_window("main") {
- #[cfg(target_os = "windows")]
- {
- let _ = window.set_skip_taskbar(false);
+ {
+ match event {
+ // macOS 在 Dock 图标被点击并重新激活应用时会触发 Reopen 事件,这里手动恢复主窗口
+ RunEvent::Reopen { .. } => {
+ if let Some(window) = app_handle.get_webview_window("main") {
+ #[cfg(target_os = "windows")]
+ {
+ let _ = window.set_skip_taskbar(false);
+ }
+ let _ = window.unminimize();
+ let _ = window.show();
+ let _ = window.set_focus();
+ apply_tray_policy(app_handle, true);
+ }
}
- let _ = window.unminimize();
- let _ = window.show();
- let _ = window.set_focus();
- apply_tray_policy(app_handle, true);
+ // 处理通过自定义 URL 协议触发的打开事件(例如 ccswitch://...)
+ RunEvent::Opened { urls } => {
+ if let Some(url) = urls.first() {
+ let url_str = url.to_string();
+ log::info!("RunEvent::Opened with URL: {url_str}");
+
+ if url_str.starts_with("ccswitch://") {
+ // 解析并广播深链接事件,复用与 single_instance 相同的逻辑
+ match crate::deeplink::parse_deeplink_url(&url_str) {
+ Ok(request) => {
+ log::info!(
+ "Successfully parsed deep link from RunEvent::Opened: resource={}, app={}",
+ request.resource,
+ request.app
+ );
+
+ if let Err(e) =
+ app_handle.emit("deeplink-import", &request)
+ {
+ log::error!(
+ "Failed to emit deep link event from RunEvent::Opened: {e}"
+ );
+ }
+ }
+ Err(e) => {
+ log::error!(
+ "Failed to parse deep link URL from RunEvent::Opened: {e}"
+ );
+
+ if let Err(emit_err) = app_handle.emit(
+ "deeplink-error",
+ serde_json::json!({
+ "url": url_str,
+ "error": e.to_string()
+ }),
+ ) {
+ log::error!(
+ "Failed to emit deep link error event from RunEvent::Opened: {emit_err}"
+ );
+ }
+ }
+ }
+
+ // 确保主窗口可见
+ if let Some(window) = app_handle.get_webview_window("main") {
+ let _ = window.unminimize();
+ let _ = window.show();
+ let _ = window.set_focus();
+ }
+ }
+ }
+ }
+ _ => {}
}
}
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
index a66f8d9..ec1585c 100644
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -23,7 +23,11 @@
}
],
"security": {
- "csp": "default-src 'self'; img-src 'self' data:; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' ipc: http://ipc.localhost https: http:"
+ "csp": "default-src 'self'; img-src 'self' data:; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' ipc: http://ipc.localhost https: http:",
+ "assetProtocol": {
+ "enable": true,
+ "scope": []
+ }
}
},
"bundle": {
@@ -41,9 +45,17 @@
"wix": {
"template": "wix/per-user-main.wxs"
}
+ },
+ "macOS": {
+ "minimumSystemVersion": "10.15"
}
},
"plugins": {
+ "deep-link": {
+ "desktop": {
+ "schemes": ["ccswitch"]
+ }
+ },
"updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEM4MDI4QzlBNTczOTI4RTMKUldUaktEbFhtb3dDeUM5US9kT0FmdGR5Ti9vQzcwa2dTMlpibDVDUmQ2M0VGTzVOWnd0SGpFVlEK",
"endpoints": [
diff --git a/src/App.tsx b/src/App.tsx
index d15b0de..45c5857 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -22,6 +22,7 @@ import { UpdateBadge } from "@/components/UpdateBadge";
import UsageScriptModal from "@/components/UsageScriptModal";
import UnifiedMcpPanel from "@/components/mcp/UnifiedMcpPanel";
import PromptPanel from "@/components/prompts/PromptPanel";
+import { DeepLinkImportDialog } from "@/components/DeepLinkImportDialog";
import { Button } from "@/components/ui/button";
function App() {
@@ -303,6 +304,8 @@ function App() {
/>