diff --git a/.github/workflows/frontend-check.yml b/.github/workflows/frontend-check.yml index 8cf1fdae4..46f02f2fe 100644 --- a/.github/workflows/frontend-check.yml +++ b/.github/workflows/frontend-check.yml @@ -102,6 +102,12 @@ jobs: filters: | contract: - 'src/services/contract.ts' + - 'src/services/bindings.ts' + - 'Cargo.lock' + - 'Cargo.toml' + - 'src-tauri/Cargo.toml' + - 'crates/**' + - 'src-tauri/src/**' - 'scripts/gen-contract.mjs' - 'src-tauri/src/core/notify/**' - 'src-tauri/src/bin/**' @@ -163,4 +169,4 @@ jobs: if: steps.check_inputs.outputs.contract == 'true' run: | pnpm gen:contract - git diff --exit-code -- src/services/contract.ts + git diff --exit-code -- src/services/bindings.ts diff --git a/Cargo.lock b/Cargo.lock index 56333e428..b8517d3e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" + [[package]] name = "adler2" version = "2.0.1" @@ -1135,6 +1141,8 @@ dependencies = [ "sha2 0.11.0", "smartstring", "socket2", + "specta", + "specta-typescript", "sysproxy", "tauri", "tauri-build", @@ -1155,6 +1163,7 @@ dependencies = [ "tauri-plugin-updater", "tauri-plugin-wdio-webdriver", "tauri-plugin-window-state", + "tauri-specta", "tokio", "tokio-rustls", "tokio-stream", @@ -1213,6 +1222,7 @@ dependencies = [ "rust_iso3166", "serde", "serde_json", + "specta", "tokio", ] @@ -6805,6 +6815,58 @@ dependencies = [ "system-deps 6.2.2", ] +[[package]] +name = "specta" +version = "2.0.0-rc.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f9a30cbcbb7011f1da7d73483983bf838af123883e45f2b36ed76328df9c50" +dependencies = [ + "paste", + "rustc_version 0.4.1", + "serde_json", + "specta-macros", +] + +[[package]] +name = "specta-macros" +version = "2.0.0-rc.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ce14957ecc2897f1f848b8255b6531d13ddf49cbcf506b7c2c9fb1d005593bb" +dependencies = [ + "Inflector", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "specta-serde" +version = "0.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8a72b755ddb8949fd8f17c5db43f0e8a806ea587d9bc602ee3f73240c00029" +dependencies = [ + "specta", + "specta-macros", +] + +[[package]] +name = "specta-typescript" +version = "0.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "639404ee95557f2f8b7e4cb773ffefd45304c7ab8ba21ac83b69051595e083c0" +dependencies = [ + "specta", +] + +[[package]] +name = "specta-util" +version = "0.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29b1fc02b446f7244a92924fe68c0555921209f1d342990cd1539e9138e69502" +dependencies = [ + "specta", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -7160,6 +7222,7 @@ dependencies = [ "serde_json", "serde_repr", "serialize-to-javascript", + "specta", "swift-rs", "tauri-build", "tauri-macros", @@ -7277,6 +7340,7 @@ dependencies = [ "libc", "parking_lot", "serde", + "specta", "sysinfo", "tauri", "tauri-plugin-clipboard-manager", @@ -7656,6 +7720,37 @@ dependencies = [ "wry", ] +[[package]] +name = "tauri-specta" +version = "2.0.0-rc.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee080f36d2ac17ce2f3a82fb53f02d664e8345457de51b56dad3c394dacc41a2" +dependencies = [ + "heck 0.5.0", + "serde", + "serde_json", + "specta", + "specta-serde", + "specta-typescript", + "specta-util", + "tauri", + "tauri-specta-macros", + "thiserror 2.0.20", +] + +[[package]] +name = "tauri-specta-macros" +version = "2.0.0-rc.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a59dfdce06c98d8d211619bea5fdb39486d8a8c558e12b2d2ce255972320012" +dependencies = [ + "darling", + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "tauri-utils" version = "2.10.1" diff --git a/crates/clash-verge-media-unlock/Cargo.toml b/crates/clash-verge-media-unlock/Cargo.toml index eec2c2e0c..2a1395b4e 100644 --- a/crates/clash-verge-media-unlock/Cargo.toml +++ b/crates/clash-verge-media-unlock/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] +specta = { version = "=2.0.0-rc.25", features = ["derive", "function"] } clash-verge-logging = { workspace = true } chrono = { workspace = true } log = { workspace = true } diff --git a/crates/clash-verge-media-unlock/src/types.rs b/crates/clash-verge-media-unlock/src/types.rs index f643cc3b9..58b41374c 100644 --- a/crates/clash-verge-media-unlock/src/types.rs +++ b/crates/clash-verge-media-unlock/src/types.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use super::utils::{country_code_to_emoji, get_local_date_string}; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, specta::Type)] pub struct UnlockItem { pub name: String, pub status: String, diff --git a/crates/tauri-plugin-clash-verge-sysinfo/Cargo.toml b/crates/tauri-plugin-clash-verge-sysinfo/Cargo.toml index 1536d7096..aa5f1ba02 100644 --- a/crates/tauri-plugin-clash-verge-sysinfo/Cargo.toml +++ b/crates/tauri-plugin-clash-verge-sysinfo/Cargo.toml @@ -5,6 +5,7 @@ edition = "2024" rust-version = "1.91" [dependencies] +specta = { version = "=2.0.0-rc.25", features = ["derive", "function"] } tauri = { workspace = true } tauri-plugin-clipboard-manager = { workspace = true } parking_lot = { workspace = true } diff --git a/crates/tauri-plugin-clash-verge-sysinfo/src/commands.rs b/crates/tauri-plugin-clash-verge-sysinfo/src/commands.rs index 0c2a8f56c..5634877e4 100644 --- a/crates/tauri-plugin-clash-verge-sysinfo/src/commands.rs +++ b/crates/tauri-plugin-clash-verge-sysinfo/src/commands.rs @@ -1,11 +1,11 @@ use parking_lot::RwLock; use serde::Serialize; use tauri::{AppHandle, Runtime, State, command}; -use tauri_plugin_clipboard_manager::{ClipboardExt as _, Error}; +use tauri_plugin_clipboard_manager::ClipboardExt as _; use crate::Platform; -#[derive(Serialize)] +#[derive(Serialize, specta::Type)] pub struct SystemInfo { pub system_name: String, pub system_version: String, @@ -31,23 +31,26 @@ impl From for SystemInfo { } #[command] -pub fn get_system_info(state: State<'_, RwLock>) -> Result { +#[specta::specta] +pub fn get_system_info(state: State<'_, RwLock>) -> Result { let platform = state.inner().read(); Ok(SystemInfo::from(platform.clone())) } /// 获取应用的运行时间(毫秒) #[command] -pub fn get_app_uptime(state: State<'_, RwLock>) -> Result { +#[specta::specta] +pub fn get_app_uptime(state: State<'_, RwLock>) -> Result { Ok(state.inner().read().appinfo.app_startup_time.elapsed().as_millis()) } #[command] +#[specta::specta] pub fn export_diagnostic_info( app_handle: AppHandle, state: State<'_, RwLock>, -) -> Result<(), Error> { +) -> Result<(), String> { let info = state.inner().read().to_string(); let clipboard = app_handle.clipboard(); - clipboard.write_text(info) + clipboard.write_text(info).map_err(|error| error.to_string()) } diff --git a/knip.json b/knip.json index 9be0c22de..c311c55bb 100644 --- a/knip.json +++ b/knip.json @@ -18,6 +18,7 @@ "ignoreDependencies": ["lint-staged"], "ignoreIssues": { "src/types/generated/**": ["exports", "types"], + "src/services/bindings.ts": ["exports", "types"], "src/services/contract.ts": ["exports", "types"] }, "cycles": { diff --git a/scripts/gen-contract.mjs b/scripts/gen-contract.mjs index c2afba095..b05b6562e 100644 --- a/scripts/gen-contract.mjs +++ b/scripts/gen-contract.mjs @@ -1,91 +1,22 @@ import { execFileSync } from 'node:child_process' -import fs from 'node:fs' import path from 'node:path' -/** - * Regenerates src/services/contract.ts from the backend wire contract. - * Source of truth: `cargo run -p clash-verge --bin frontend-contract` inside - * src-tauri, which serializes the same enum variants and event payloads the - * app emits at runtime. CI reruns this and fails on a non-empty diff. - */ - const ROOT = path.resolve(import.meta.dirname, '..') -const OUT = path.join(ROOT, 'src/services/contract.ts') - -const contract = JSON.parse( - execFileSync( - 'cargo', - ['run', '-p', 'clash-verge', '--bin', 'frontend-contract', '-q'], - { - cwd: path.join(ROOT, 'src-tauri'), - encoding: 'utf8', - maxBuffer: 16 * 1024 * 1024, - }, - ), -) - -if (contract.version !== 1) { - throw new Error(`unsupported wire contract version: ${contract.version}`) -} - -/** TS type per event, derived from the serialized sample payload's JSON shape. */ -const payloadType = (name, sample) => { - if (sample === null) return 'null' - if (typeof sample === 'string') return 'string' - if (Array.isArray(sample)) { - if (name === 'verge://notice-message') return '[NoticeStatus, string]' - throw new Error(`unexpected array payload for ${name}`) - } - if (typeof sample === 'object') { - const keys = Object.keys(sample) - if (keys.length === 1 && keys[0] === 'uid') return '{ uid: string }' - if (name === 'verge://run-state-changed') return 'RunState' - throw new Error(`unexpected object payload for ${name}: ${keys.join(',')}`) - } - throw new Error(`unexpected payload for ${name}`) -} - -const needsRunState = contract.events.some( - (e) => e.name === 'verge://run-state-changed', -) -if (!needsRunState) - throw new Error('run-state-changed event missing from contract') - -const lines = [] -lines.push( - '// @generated by scripts/gen-contract.mjs from the backend wire contract. DO NOT EDIT.', -) -lines.push('// Regen: pnpm gen:contract') -lines.push('') -lines.push("import type { RunState } from './cmds'") -lines.push('') -lines.push( - '/** Wire status strings from NoticeStatus; keep in sync via `pnpm gen:contract`. */', -) -lines.push('export type NoticeStatus =') -for (const status of contract.noticeStatuses) { - lines.push(` | ${JSON.stringify(status)}`) -} -lines.push('') -lines.push('/** Event name → payload type for every backend-emitted event. */') -lines.push('export interface VergeEventPayloads {') -for (const event of contract.events) { - const key = JSON.stringify(event.name) - lines.push(` ${key}: ${payloadType(event.name, event.sample)}`) -} -lines.push('}') -lines.push('') - -const content = `${lines.join('\n')}\n` -fs.writeFileSync(OUT, content) - -// Match the repo's biome style so CI parity (`git diff --exit-code`) is stable. execFileSync( - process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm', - ['exec', 'biome', 'format', '--write', path.relative(ROOT, OUT)], - { cwd: ROOT, stdio: 'ignore' }, + 'cargo', + ['run', '-p', 'clash-verge', '--bin', 'frontend-contract', '--locked', '-q'], + { + cwd: ROOT, + stdio: 'inherit', + }, ) - -console.log( - `[gen-contract] wrote ${path.relative(ROOT, OUT)}: ${contract.events.length} events, ${contract.noticeStatuses.length} statuses`, +execFileSync( + process.execPath, + [ + path.join(ROOT, 'node_modules/@biomejs/biome/bin/biome'), + 'format', + '--write', + 'src/services/bindings.ts', + ], + { cwd: ROOT, stdio: 'inherit' }, ) diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 49a555be6..0e8dcc4b7 100755 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -109,6 +109,9 @@ webpki-roots = "1.0.9" dark-light = "3.0.0" bytes = "1.12.1" tokio-util = { version = "0.7.19", features = ["time"] } +specta = { version = "=2.0.0-rc.25", features = ["derive", "function", "serde_json"] } +tauri-specta = { version = "=2.0.0-rc.25", features = ["derive", "typescript"] } +specta-typescript = "=0.0.12" [target.'cfg(unix)'.dependencies] libc = "0.2.190" diff --git a/src-tauri/src/bin/frontend-contract.rs b/src-tauri/src/bin/frontend-contract.rs index 2ce0ecede..aa9b445d8 100644 --- a/src-tauri/src/bin/frontend-contract.rs +++ b/src-tauri/src/bin/frontend-contract.rs @@ -1,8 +1,7 @@ -//! Prints the frontend wire contract as JSON to stdout. Zero new dependencies: -//! the payload is derived from the same serializers the app uses at runtime. -//! Consumed by scripts/gen-contract.mjs; CI regenerates and diffs the committed -//! TypeScript artifact. - -fn main() { - println!("{}", app_lib::frontend_wire_contract()); +fn main() -> Result<(), Box> { + app_lib::ipc::builder().export( + specta_typescript::Typescript::default(), + std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../src/services/bindings.ts"), + )?; + Ok(()) } diff --git a/src-tauri/src/cmd/app.rs b/src-tauri/src/cmd/app.rs index 425d31f7b..e043dbc4e 100644 --- a/src-tauri/src/cmd/app.rs +++ b/src-tauri/src/cmd/app.rs @@ -9,12 +9,14 @@ use smartstring::alias::String; use tauri::{AppHandle, Manager as _, ipc::Channel}; #[tauri::command] +#[specta::specta] pub async fn open_app_dir() -> CmdResult<()> { let app_dir = dirs::app_home_dir().stringify_err()?; open::that(app_dir).stringify_err() } #[tauri::command] +#[specta::specta] pub async fn open_core_dir() -> CmdResult<()> { let core_dir = tauri::utils::platform::current_exe().stringify_err()?; let core_dir = core_dir.parent().ok_or("failed to get core dir")?; @@ -22,12 +24,14 @@ pub async fn open_core_dir() -> CmdResult<()> { } #[tauri::command] +#[specta::specta] pub async fn open_logs_dir() -> CmdResult<()> { let log_dir = dirs::app_logs_dir().stringify_err()?; open::that(log_dir).stringify_err() } #[tauri::command] +#[specta::specta] pub fn open_devtools(app_handle: AppHandle) { if let Some(window) = app_handle.get_webview_window("main") { if !window.is_devtools_open() { @@ -39,22 +43,26 @@ pub fn open_devtools(app_handle: AppHandle) { } #[tauri::command] +#[specta::specta] pub async fn exit_app() { feat::quit().await; } #[tauri::command] +#[specta::specta] pub async fn restart_app() -> CmdResult<()> { feat::restart_app().await; Ok(()) } #[tauri::command] +#[specta::specta] pub async fn install_update( app_handle: AppHandle, - version: String, + version: std::string::String, on_event: Channel, ) -> CmdResult { + let version: String = version.into(); SilentUpdater::global() .install_update(&app_handle, &version, |event| { let _ = on_event.send(event); @@ -64,22 +72,32 @@ pub async fn install_update( } #[tauri::command] +#[specta::specta] pub fn cancel_update_download() { SilentUpdater::global().cancel_download(); } #[tauri::command] -pub fn get_app_dir() -> CmdResult { +#[specta::specta] +pub fn get_app_dir() -> CmdResult { let app_home_dir = dirs::app_home_dir().stringify_err()?.to_string_lossy().into(); Ok(app_home_dir) } #[tauri::command] -pub async fn download_icon_cache(url: String, name: String) -> CmdResult { - feat::download_icon_cache(url, name).await +#[specta::specta] +pub async fn download_icon_cache( + url: std::string::String, + name: std::string::String, +) -> CmdResult { + let url: String = url.into(); + let name: String = name.into(); + feat::download_icon_cache(url, name).await.map(Into::into) } #[tauri::command] -pub async fn copy_icon_file(path: String, icon_info: feat::IconInfo) -> CmdResult { - feat::copy_icon_file(path, icon_info).await +#[specta::specta] +pub async fn copy_icon_file(path: std::string::String, icon_info: feat::IconInfo) -> CmdResult { + let path: String = path.into(); + feat::copy_icon_file(path, icon_info).await.map(Into::into) } diff --git a/src-tauri/src/cmd/backup.rs b/src-tauri/src/cmd/backup.rs index 52aa2f0d9..349c869b5 100644 --- a/src-tauri/src/cmd/backup.rs +++ b/src-tauri/src/cmd/backup.rs @@ -4,33 +4,44 @@ use feat::LocalBackupFile; use smartstring::alias::String; #[tauri::command] +#[specta::specta] pub async fn create_local_backup() -> CmdResult<()> { feat::create_local_backup().await.stringify_err() } #[tauri::command] +#[specta::specta] pub async fn list_local_backup() -> CmdResult> { feat::list_local_backup().await.stringify_err() } #[tauri::command] -pub async fn delete_local_backup(filename: String) -> CmdResult<()> { +#[specta::specta] +pub async fn delete_local_backup(filename: std::string::String) -> CmdResult<()> { + let filename: String = filename.into(); feat::delete_local_backup(filename).await.stringify_err() } #[tauri::command] -pub async fn restore_local_backup(filename: String) -> CmdResult<()> { +#[specta::specta] +pub async fn restore_local_backup(filename: std::string::String) -> CmdResult<()> { + let filename: String = filename.into(); feat::restore_local_backup(filename) .await .map_err(|error| super::proxy_aware_error(&error)) } #[tauri::command] -pub async fn import_local_backup(source: String) -> CmdResult { - feat::import_local_backup(source).await.stringify_err() +#[specta::specta] +pub async fn import_local_backup(source: std::string::String) -> CmdResult { + let source: String = source.into(); + feat::import_local_backup(source).await.map(Into::into).stringify_err() } #[tauri::command] -pub async fn export_local_backup(filename: String, destination: String) -> CmdResult<()> { +#[specta::specta] +pub async fn export_local_backup(filename: std::string::String, destination: std::string::String) -> CmdResult<()> { + let filename: String = filename.into(); + let destination: String = destination.into(); feat::export_local_backup(filename, destination).await.stringify_err() } diff --git a/src-tauri/src/cmd/clash.rs b/src-tauri/src/cmd/clash.rs index 2d5a567db..803889dc5 100644 --- a/src-tauri/src/cmd/clash.rs +++ b/src-tauri/src/cmd/clash.rs @@ -12,30 +12,34 @@ use crate::{ }, }; use clash_verge_logging::{Type, logging, logging_error}; -use serde_yaml_ng::Mapping; use smartstring::alias::String; use tokio::fs; #[tauri::command] +#[specta::specta] pub async fn copy_clash_env() -> CmdResult { feat::copy_clash_env().await; Ok(()) } #[tauri::command] +#[specta::specta] pub async fn get_clash_info() -> CmdResult { Ok(Config::clash().await.data_arc().get_client_info()) } #[tauri::command] -pub async fn patch_clash_config(payload: Mapping) -> CmdResult { - feat::patch_clash(&payload) +#[specta::specta] +pub async fn patch_clash_config(payload: crate::ipc::YamlMapping) -> CmdResult { + feat::patch_clash(&payload.0) .await .map_err(|error| proxy_aware_coded_error(&error, "CLASH_CONFIG_UPDATE_FAILED")) } #[tauri::command] -pub async fn patch_clash_mode(payload: String) -> CmdResult { +#[specta::specta] +pub async fn patch_clash_mode(payload: std::string::String) -> CmdResult { + let payload: String = payload.into(); feat::change_clash_mode(payload) .await .with_error_code("CLASH_MODE_UPDATE_FAILED") @@ -43,12 +47,15 @@ pub async fn patch_clash_mode(payload: String) -> CmdResult { /// Reads the saved mode without depending on strict mihomo `/configs` deserialization. #[tauri::command] -pub async fn get_clash_mode() -> CmdResult> { - Ok(Config::clash().await.data_arc().get_mode().map(Into::into)) +#[specta::specta] +pub async fn get_clash_mode() -> CmdResult> { + Ok(Config::clash().await.data_arc().get_mode()) } #[tauri::command] -pub async fn change_clash_core(clash_core: String) -> CmdResult> { +#[specta::specta] +pub async fn change_clash_core(clash_core: std::string::String) -> CmdResult> { + let clash_core: String = clash_core.into(); logging!(info, Type::Config, "changing core to {clash_core}"); match CoreManager::global().change_core(&clash_core).await { @@ -80,6 +87,7 @@ pub async fn change_clash_core(clash_core: String) -> CmdResult CmdResult { logging_error!(Type::Core, profiles_save_file_safe().await); let result = CoreManager::global() @@ -94,6 +102,7 @@ pub async fn restart_core() -> CmdResult { /// Replaces the managed core binary atomically instead of letting mihomo overwrite itself. #[tauri::command] +#[specta::specta] pub async fn upgrade_clash_core(force: bool) -> CmdResult { let report = feat::upgrade_core(force).await.with_error_code("CORE_UPGRADE_FAILED")?; if report.upgraded { @@ -103,7 +112,9 @@ pub async fn upgrade_clash_core(force: bool) -> CmdResult CmdResult { +#[specta::specta] +pub async fn test_delay(url: std::string::String) -> CmdResult { + let url: String = url.into(); let result = match feat::test_delay(url).await { Ok(delay) => delay, Err(e) => { @@ -115,13 +126,14 @@ pub async fn test_delay(url: String) -> CmdResult { } #[tauri::command] -pub async fn save_dns_config(dns_config: Mapping) -> CmdResult { +#[specta::specta] +pub async fn save_dns_config(dns_config: crate::ipc::YamlMapping) -> CmdResult { use crate::utils::dirs; use tokio::fs; let dns_path = dirs::app_home_dir().stringify_err()?.join(constants::files::DNS_CONFIG); - let yaml_str = yaml_emitter::to_mihomo_config_string(&dns_config).stringify_err()?; + let yaml_str = yaml_emitter::to_mihomo_config_string(&dns_config.0).stringify_err()?; fs::write(&dns_path, yaml_str).await.stringify_err()?; logging!(info, Type::Config, "DNS config saved to {dns_path:?}"); @@ -129,22 +141,27 @@ pub async fn save_dns_config(dns_config: Mapping) -> CmdResult { } #[tauri::command] +#[specta::specta] pub fn take_dns_override_notice() -> bool { crate::config::dns::take_dns_override_notice() } #[tauri::command] +#[specta::specta] pub async fn set_dns_override( - profile_uid: String, + profile_uid: std::string::String, enabled: bool, - confirmation: Option, + confirmation: Option, ) -> CmdResult { + let profile_uid: String = profile_uid.into(); + let confirmation = confirmation.map(String::from); feat::set_dns_override(profile_uid, enabled, confirmation) .await .map_err(|error| proxy_aware_coded_error(&error, "DNS_OVERRIDE_UPDATE_FAILED")) } #[tauri::command] +#[specta::specta] pub async fn apply_dns_config(apply: bool) -> CmdResult { if apply { let dns_path = dirs::app_home_dir().stringify_err()?.join(constants::files::DNS_CONFIG); @@ -202,20 +219,22 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult { } #[tauri::command] -pub async fn get_dns_config_content() -> CmdResult> { +#[specta::specta] +pub async fn get_dns_config_content() -> CmdResult> { use crate::utils::dirs; use tokio::fs; let dns_path = dirs::app_home_dir().stringify_err()?.join(constants::files::DNS_CONFIG); match fs::read_to_string(&dns_path).await { - Ok(content) => Ok(Some(content.into())), + Ok(content) => Ok(Some(content)), Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None), Err(error) => Err(CommandFailure::plain(error)), } } #[tauri::command] +#[specta::specta] pub async fn validate_dns_config() -> CmdResult { let app_dir = dirs::app_home_dir().stringify_err()?; let dns_path = app_dir.join(constants::files::DNS_CONFIG); @@ -232,6 +251,7 @@ pub async fn validate_dns_config() -> CmdResult { } #[tauri::command] +#[specta::specta] pub async fn get_clash_logs() -> CmdResult> { let logs = CoreManager::global().get_clash_logs().await.unwrap_or_default(); Ok(logs) diff --git a/src-tauri/src/cmd/lightweight.rs b/src-tauri/src/cmd/lightweight.rs index af1fe7ad9..c6be85136 100644 --- a/src-tauri/src/cmd/lightweight.rs +++ b/src-tauri/src/cmd/lightweight.rs @@ -3,6 +3,7 @@ use crate::core::lightweight; use super::CmdResult; #[tauri::command] +#[specta::specta] pub async fn entry_lightweight_mode() -> CmdResult { lightweight::entry_lightweight_mode().await; Ok(()) diff --git a/src-tauri/src/cmd/listener.rs b/src-tauri/src/cmd/listener.rs index 610a2628f..c54f913a7 100644 --- a/src-tauri/src/cmd/listener.rs +++ b/src-tauri/src/cmd/listener.rs @@ -5,11 +5,13 @@ use crate::{ }; #[tauri::command] +#[specta::specta] pub async fn probe_listener(request: ListenerProbe) -> CmdResult { feat::probe_listener(request).await.stringify_err() } #[tauri::command] +#[specta::specta] pub async fn save_proxy_ports(settings: ProxyPortSettings) -> CmdResult { feat::save_proxy_ports(settings) .await diff --git a/src-tauri/src/cmd/media_unlock_checker/mod.rs b/src-tauri/src/cmd/media_unlock_checker/mod.rs index 42862117e..e5aa71560 100644 --- a/src-tauri/src/cmd/media_unlock_checker/mod.rs +++ b/src-tauri/src/cmd/media_unlock_checker/mod.rs @@ -4,6 +4,7 @@ use std::time::Duration; use tauri::{command, ipc::Channel}; #[command] +#[specta::specta] pub fn get_unlock_items() -> Vec { clash_verge_media_unlock::default_unlock_items() } @@ -26,6 +27,7 @@ async fn create_client() -> Result { } #[command] +#[specta::specta] pub async fn check_media_unlock(on_complete: Channel) -> Result, String> { let client = create_client().await?; Ok(clash_verge_media_unlock::check_media_unlock(&client, |item| { @@ -35,6 +37,7 @@ pub async fn check_media_unlock(on_complete: Channel) -> Result Result { let client = create_client().await?; clash_verge_media_unlock::check_media_unlock_item(&client, &name).await diff --git a/src-tauri/src/cmd/mod.rs b/src-tauri/src/cmd/mod.rs index a8c6bca68..1bb2d3041 100644 --- a/src-tauri/src/cmd/mod.rs +++ b/src-tauri/src/cmd/mod.rs @@ -4,7 +4,7 @@ use smartstring::alias::String; pub type CmdResult = Result; /// Structured command failure returned to the frontend. -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, specta::Type)] pub struct CommandFailure { /// Stable frontend identifier, when classified. #[serde(skip_serializing_if = "Option::is_none")] diff --git a/src-tauri/src/cmd/network.rs b/src-tauri/src/cmd/network.rs index dd3475336..889413376 100644 --- a/src-tauri/src/cmd/network.rs +++ b/src-tauri/src/cmd/network.rs @@ -3,13 +3,12 @@ use crate::cmd::StringifyErr as _; use crate::core::{proxy_control, sysopt::Sysopt}; use gethostname::gethostname; -use network_interface::NetworkInterface; -use serde_yaml_ng::Mapping; use sysproxy::{Autoproxy, Sysproxy}; use tauri_plugin_clash_verge_sysinfo; #[tauri::command] -pub async fn get_sys_proxy() -> CmdResult { +#[specta::specta] +pub async fn get_sys_proxy() -> CmdResult { Sysopt::global().wait_idle().await; // With no network service there is no proxy configured anywhere, which reads as disabled. let sys_proxy = match Sysproxy::get_system_proxy() { @@ -23,15 +22,16 @@ pub async fn get_sys_proxy() -> CmdResult { ref enable, } = sys_proxy; - let mut map = Mapping::new(); - map.insert("enable".into(), (*enable).into()); - map.insert("server".into(), format!("{}:{}", host, port).into()); - map.insert("bypass".into(), bypass.as_str().into()); - Ok(map) + Ok(SystemProxy { + enable: *enable, + server: format!("{host}:{port}"), + bypass: bypass.to_string(), + }) } #[tauri::command] -pub async fn get_auto_proxy() -> CmdResult { +#[specta::specta] +pub async fn get_auto_proxy() -> CmdResult { Sysopt::global().wait_idle().await; let auto_proxy = match Autoproxy::get_auto_proxy() { Err(error) if proxy_control::is_missing_network_service(&error) => Autoproxy::default(), @@ -39,18 +39,20 @@ pub async fn get_auto_proxy() -> CmdResult { }; let Autoproxy { ref enable, ref url } = auto_proxy; - let mut map = Mapping::new(); - map.insert("enable".into(), (*enable).into()); - map.insert("url".into(), url.as_str().into()); - Ok(map) + Ok(AutoProxy { + enable: *enable, + url: url.to_string(), + }) } #[tauri::command] +#[specta::specta] pub fn get_embedded_server_port() -> CmdResult { crate::utils::server::embedded_server_port().stringify_err() } #[tauri::command] +#[specta::specta] pub fn get_system_hostname() -> String { match gethostname().into_string() { Ok(name) => name, @@ -62,12 +64,14 @@ pub fn get_system_hostname() -> String { } #[tauri::command] +#[specta::specta] pub fn get_network_interfaces() -> Vec { tauri_plugin_clash_verge_sysinfo::list_network_interfaces() } #[tauri::command] -pub fn get_network_interfaces_info() -> CmdResult> { +#[specta::specta] +pub fn get_network_interfaces_info() -> CmdResult> { use network_interface::{NetworkInterface, NetworkInterfaceConfig as _}; let names = get_network_interfaces(); @@ -77,9 +81,22 @@ pub fn get_network_interfaces_info() -> CmdResult> { for interface in interfaces { if names.contains(&interface.name) { - result.push(interface); + result.push(interface.into()); } } Ok(result) } + +#[derive(serde::Serialize, specta::Type)] +pub struct SystemProxy { + pub enable: bool, + pub server: String, + pub bypass: String, +} + +#[derive(serde::Serialize, specta::Type)] +pub struct AutoProxy { + pub enable: bool, + pub url: String, +} diff --git a/src-tauri/src/cmd/profile.rs b/src-tauri/src/cmd/profile.rs index b89545296..0adbe1ce8 100644 --- a/src-tauri/src/cmd/profile.rs +++ b/src-tauri/src/cmd/profile.rs @@ -33,6 +33,7 @@ fn profile_import_error(err: &anyhow::Error) -> std::string::String { } #[tauri::command] +#[specta::specta] pub async fn get_profiles() -> CmdResult> { let draft = Config::profiles().await; let data = draft.data_arc(); @@ -40,6 +41,7 @@ pub async fn get_profiles() -> CmdResult> { } #[tauri::command] +#[specta::specta] pub async fn enhance_profiles() -> CmdResult { match feat::enhance_profiles().await { Ok(outcome) if outcome.is_valid() => { @@ -65,6 +67,7 @@ pub async fn enhance_profiles() -> CmdResult { #[tauri::command] #[tracing::instrument(skip_all, level = "info", fields(url = %help::mask_url(&url), uid = tracing::field::Empty))] +#[specta::specta] pub async fn import_profile(url: std::string::String, option: Option) -> CmdResult { let item = &mut match PrfItem::from_url(&url, None, None, option.as_ref()).await { Ok(it) => it, @@ -94,7 +97,10 @@ pub async fn import_profile(url: std::string::String, option: Option) } #[tauri::command] -pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult { +#[specta::specta] +pub async fn reorder_profile(active_id: std::string::String, over_id: std::string::String) -> CmdResult { + let active_id: String = active_id.into(); + let over_id: String = over_id.into(); match profiles_reorder_safe(&active_id, &over_id).await { Ok(_) => { logging!(info, Type::Cmd, "重新排序配置文件: {} -> {}", active_id, over_id); @@ -114,7 +120,9 @@ pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult { } #[tauri::command] -pub async fn create_profile(item: PrfItem, file_data: Option) -> CmdResult { +#[specta::specta] +pub async fn create_profile(item: PrfItem, file_data: Option) -> CmdResult { + let file_data = file_data.map(String::from); match profiles_append_item_with_filedata_safe(&item, file_data).await { Ok(_) => { profiles_save_file_safe() @@ -131,7 +139,9 @@ pub async fn create_profile(item: PrfItem, file_data: Option) -> CmdResu } #[tauri::command] -pub async fn update_profile(index: String, option: Option) -> CmdResult { +#[specta::specta] +pub async fn update_profile(index: std::string::String, option: Option) -> CmdResult { + let index: String = index.into(); match feat::update_profile(&index, option.as_ref(), true).await { Ok(_) => Ok(()), Err(e) => { @@ -142,7 +152,9 @@ pub async fn update_profile(index: String, option: Option) -> CmdResu } #[tauri::command] -pub async fn delete_profile(index: String) -> CmdResult { +#[specta::specta] +pub async fn delete_profile(index: std::string::String) -> CmdResult { + let index: String = index.into(); let profile_write_guard = PROFILE_WRITE_LOCK.lock().await; let profiles = Config::profiles().await; @@ -315,6 +327,7 @@ async fn perform_config_update( #[tauri::command] #[tracing::instrument(skip_all, level = "info", fields(target = ?profiles.current))] +#[specta::specta] pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { if CURRENT_SWITCHING_PROFILE .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed) @@ -345,7 +358,9 @@ pub async fn patch_profiles_config_by_profile_index(profile_index: String) -> Cm } #[tauri::command] -pub async fn patch_profile(index: String, profile: PrfItem) -> CmdResult { +#[specta::specta] +pub async fn patch_profile(index: std::string::String, profile: PrfItem) -> CmdResult { + let index: String = index.into(); let profiles = Config::profiles().await; let should_refresh_timer = if let Ok(old_profile) = profiles.latest_arc().get_item(&index) && let Some(new_option) = profile.option.as_ref() @@ -385,7 +400,9 @@ pub async fn patch_profile(index: String, profile: PrfItem) -> CmdResult { } #[tauri::command] -pub async fn view_profile(index: String) -> CmdResult { +#[specta::specta] +pub async fn view_profile(index: std::string::String) -> CmdResult { + let index: String = index.into(); let profiles = Config::profiles().await; let profiles_ref = profiles.latest_arc(); let file = profiles_ref @@ -409,7 +426,9 @@ pub async fn view_profile(index: String) -> CmdResult { } #[tauri::command] -pub async fn read_profile_file(index: String) -> CmdResult { +#[specta::specta] +pub async fn read_profile_file(index: std::string::String) -> CmdResult { + let index: String = index.into(); let item = { let profiles = Config::profiles().await; let profiles_ref = profiles.latest_arc(); @@ -429,7 +448,7 @@ pub async fn read_profile_file(index: String) -> CmdResult { .join(file.as_str()); match tokio::fs::try_exists(&path).await { Ok(true) => {} - Ok(false) => return Ok(String::new()), + Ok(false) => return Ok(std::string::String::new()), Err(err) => { return Err(coded_error( "PROFILE_READ_FAILED", @@ -440,11 +459,13 @@ pub async fn read_profile_file(index: String) -> CmdResult { } let data = item.read_file().await.with_error_code("PROFILE_READ_FAILED")?; - Ok(data) + Ok(data.into()) } #[tauri::command] -pub async fn get_next_update_time(uid: String) -> CmdResult> { +#[specta::specta] +pub async fn get_next_update_time(uid: std::string::String) -> CmdResult> { + let uid: String = uid.into(); let timer = Timer::global(); let next_time = timer.get_next_update_time(&uid).await; Ok(next_time) diff --git a/src-tauri/src/cmd/proxy.rs b/src-tauri/src/cmd/proxy.rs index d91ae9f4e..aa5521833 100644 --- a/src-tauri/src/cmd/proxy.rs +++ b/src-tauri/src/cmd/proxy.rs @@ -18,6 +18,7 @@ use std::{ /// Merges one selection pair into fresh backend state to avoid stale-list overwrites. #[tauri::command] +#[specta::specta] pub async fn record_selected_node(group_name: String, node: String) -> CmdResult<()> { crate::config::profiles::record_selected_node(&group_name, &node) .await @@ -25,6 +26,7 @@ pub async fn record_selected_node(group_name: String, node: String) -> CmdResult } #[tauri::command] +#[specta::specta] pub async fn forget_selected_node(group_name: String) -> CmdResult<()> { crate::config::profiles::forget_selected_node(&group_name) .await @@ -104,6 +106,7 @@ fn runtime_group_scopes(config: Option<&Mapping>) -> BTreeMap CmdResult { let runtime = Config::runtime().await; let latest_runtime = runtime.latest_arc(); @@ -123,6 +126,7 @@ pub async fn get_proxy_view() -> CmdResult { } #[tauri::command] +#[specta::specta] pub async fn sync_tray_proxy_selection() -> CmdResult<()> { if TRAY_SYNC_RUNNING .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) diff --git a/src-tauri/src/cmd/runtime.rs b/src-tauri/src/cmd/runtime.rs index 13e0e7c5f..566af79e6 100644 --- a/src-tauri/src/cmd/runtime.rs +++ b/src-tauri/src/cmd/runtime.rs @@ -2,43 +2,58 @@ use super::CmdResult; use crate::{cmd::StringifyErr as _, config::Config, core::CoreManager, utils::yaml_emitter}; use anyhow::{Context as _, anyhow}; use clash_verge_logging::{Type, logging}; -use serde_yaml_ng::Mapping; use smartstring::alias::String; use std::collections::HashMap; #[tauri::command] -pub async fn get_runtime_config() -> CmdResult> { - Ok(Config::runtime().await.latest_arc().config.clone()) +#[specta::specta] +pub async fn get_runtime_config() -> CmdResult> { + Ok(Config::runtime() + .await + .latest_arc() + .config + .clone() + .map(crate::ipc::YamlMapping)) } #[tauri::command] -pub async fn get_runtime_yaml() -> CmdResult { +#[specta::specta] +pub async fn get_runtime_yaml() -> CmdResult { let runtime = Config::runtime().await; let runtime = runtime.latest_arc(); let config = runtime.config.as_ref(); config .ok_or_else(|| anyhow!("failed to parse config to yaml file")) - .and_then(|config| { - yaml_emitter::to_mihomo_config_string(config) - .context("failed to convert config to yaml") - .map(|s| s.into()) - }) + .and_then(|config| yaml_emitter::to_mihomo_config_string(config).context("failed to convert config to yaml")) .stringify_err() } #[tauri::command] -pub async fn get_runtime_logs() -> CmdResult>> { - Ok(Config::runtime().await.latest_arc().chain_logs.clone()) +#[specta::specta] +pub async fn get_runtime_logs() -> CmdResult { + Ok(RuntimeLogs(Config::runtime().await.latest_arc().chain_logs.clone())) +} + +#[derive(serde::Serialize, specta::Type)] +#[serde(transparent)] +pub struct RuntimeLogs( + #[specta(type = HashMap>)] + HashMap>, +); + +#[tauri::command] +#[specta::specta] +pub fn take_discarded_keys_notice() -> Option { + crate::enhance::take_discarded_keys_notice().map(Into::into) } #[tauri::command] -pub fn take_discarded_keys_notice() -> Option { - crate::enhance::take_discarded_keys_notice() -} - -#[tauri::command] -pub async fn get_runtime_proxy_chain_config(proxy_chain_exit_node: String) -> CmdResult { +#[specta::specta] +pub async fn get_runtime_proxy_chain_config( + proxy_chain_exit_node: std::string::String, +) -> CmdResult { + let proxy_chain_exit_node: String = proxy_chain_exit_node.into(); let runtime = Config::runtime().await; let runtime = runtime.latest_arc(); @@ -79,7 +94,6 @@ pub async fn get_runtime_proxy_chain_config(proxy_chain_exit_node: String) -> Cm yaml_emitter::to_mihomo_config_string(&config) .context("YAML generation failed") - .map(|s| s.into()) .stringify_err() } else { Err("failed to get proxies or proxy-groups".into()) @@ -87,7 +101,9 @@ pub async fn get_runtime_proxy_chain_config(proxy_chain_exit_node: String) -> Cm } #[tauri::command] -pub async fn update_proxy_chain_config_in_runtime(proxy_chain_config: Option) -> CmdResult<()> { +#[specta::specta] +pub async fn update_proxy_chain_config_in_runtime(proxy_chain_config: Option) -> CmdResult<()> { + let proxy_chain_config = proxy_chain_config.map(|value| value.0); match CoreManager::global() .update_runtime_config(|d| d.update_proxy_chain_config(proxy_chain_config)) .await diff --git a/src-tauri/src/cmd/save_profile.rs b/src-tauri/src/cmd/save_profile.rs index cfefc7133..984af1100 100644 --- a/src-tauri/src/cmd/save_profile.rs +++ b/src-tauri/src/cmd/save_profile.rs @@ -16,7 +16,13 @@ use tokio::fs; /// 保存profiles的配置 #[tauri::command] -pub async fn save_profile_file(index: String, file_data: Option) -> CmdResult { +#[specta::specta] +pub async fn save_profile_file( + index: std::string::String, + file_data: Option, +) -> CmdResult { + let index: String = index.into(); + let file_data = file_data.map(String::from); let file_data = match file_data { Some(d) => d, None => return Ok(ValidationOutcome::Valid), diff --git a/src-tauri/src/cmd/service.rs b/src-tauri/src/cmd/service.rs index 2f96ce98d..592df0417 100644 --- a/src-tauri/src/cmd/service.rs +++ b/src-tauri/src/cmd/service.rs @@ -8,11 +8,11 @@ use crate::{ }, }; -#[derive(Debug, PartialEq, Eq, serde::Serialize)] +#[derive(Debug, PartialEq, Eq, serde::Serialize, specta::Type)] #[serde(tag = "status", rename_all = "camelCase")] pub enum ServiceInstallOutcome { Installed, - #[cfg(any(windows, test))] + #[cfg_attr(not(any(windows, test)), allow(dead_code))] Sidecar { reason: String, }, @@ -82,11 +82,13 @@ where } #[tauri::command] +#[specta::specta] pub async fn install_service() -> CmdResult { execute_service_operation_sync(ServiceStatus::InstallRequired, "SERVICE_INSTALL_FAILED").await } #[tauri::command] +#[specta::specta] pub async fn uninstall_service() -> CmdResult { CoreManager::global() .uninstall_service_and_start_sidecar() @@ -95,16 +97,19 @@ pub async fn uninstall_service() -> CmdResult { } #[tauri::command] +#[specta::specta] pub async fn reinstall_service() -> CmdResult { execute_service_operation_sync(ServiceStatus::ReinstallRequired, "SERVICE_REINSTALL_FAILED").await } #[tauri::command] +#[specta::specta] pub async fn repair_service() -> CmdResult { execute_service_operation_sync(ServiceStatus::ForceReinstallRequired, "SERVICE_REPAIR_FAILED").await } #[tauri::command] +#[specta::specta] pub async fn continue_with_sidecar() -> CmdResult { crate::core::CoreManager::global() .continue_with_sidecar(None) @@ -113,31 +118,37 @@ pub async fn continue_with_sidecar() -> CmdResult { } #[tauri::command] +#[specta::specta] pub fn get_sidecar_failure() -> crate::core::notification::SidecarFailureSnapshot { crate::core::notification::sidecar_failure_snapshot() } #[tauri::command] +#[specta::specta] pub fn take_service_fallback_notice() -> Option { crate::core::service::take_service_fallback_notice() } #[tauri::command] +#[specta::specta] pub fn get_core_startup_error() -> Option { crate::core::CoreManager::global().get_startup_error() } #[tauri::command] +#[specta::specta] pub fn take_service_repair_notice() -> bool { crate::core::service::take_service_repair_notice() } #[tauri::command] +#[specta::specta] pub fn take_service_owner_notice() -> Option { crate::core::service::take_service_owner_notice() } #[tauri::command] +#[specta::specta] pub fn sync_runtime_providers() { if matches!(*CoreManager::global().get_running_mode(), RunningMode::Service) { request_runtime_provider_sync(timing::RUNTIME_PROVIDER_SETTLE); diff --git a/src-tauri/src/cmd/system.rs b/src-tauri/src/cmd/system.rs index dc5ac6cf8..e3f263cb9 100644 --- a/src-tauri/src/cmd/system.rs +++ b/src-tauri/src/cmd/system.rs @@ -5,11 +5,13 @@ use crate::core::{ /// Returns one coherent core/service snapshot instead of independently refreshed state. #[tauri::command] +#[specta::specta] pub async fn get_runtime_state() -> Result { Ok(RUN_STATE.settled().await.to_view()) } #[tauri::command] +#[specta::specta] pub async fn get_pending_failures() -> Vec { notification::pending_failures() } diff --git a/src-tauri/src/cmd/uwp.rs b/src-tauri/src/cmd/uwp.rs index 5337a5a7b..8637bfefc 100644 --- a/src-tauri/src/cmd/uwp.rs +++ b/src-tauri/src/cmd/uwp.rs @@ -22,6 +22,7 @@ mod platform { } #[tauri::command] +#[specta::specta] pub async fn invoke_uwp_tool() -> CmdResult { platform::invoke_uwp_tool() } diff --git a/src-tauri/src/cmd/verge.rs b/src-tauri/src/cmd/verge.rs index 2ab25fd3e..8fc2c6c70 100644 --- a/src-tauri/src/cmd/verge.rs +++ b/src-tauri/src/cmd/verge.rs @@ -9,12 +9,14 @@ use clash_verge_draft::SharedDraft; /// 获取Verge配置 #[tauri::command] +#[specta::specta] pub async fn get_verge_config() -> CmdResult> { feat::fetch_verge_config().await.stringify_err() } /// 修改Verge配置 #[tauri::command] +#[specta::specta] pub async fn patch_verge_config(payload: IVerge) -> CmdResult { let operation = system_proxy_operation(&payload); let result = match operation { diff --git a/src-tauri/src/cmd/webdav.rs b/src-tauri/src/cmd/webdav.rs index f4a1e6f25..dad1de657 100644 --- a/src-tauri/src/cmd/webdav.rs +++ b/src-tauri/src/cmd/webdav.rs @@ -4,11 +4,18 @@ use crate::{ config::{Config, IVerge}, core, feat, }; -use reqwest_dav::list_cmd::ListFile; use smartstring::alias::String; #[tauri::command] -pub async fn save_webdav_config(url: String, username: String, password: String) -> CmdResult<()> { +#[specta::specta] +pub async fn save_webdav_config( + url: std::string::String, + username: std::string::String, + password: std::string::String, +) -> CmdResult<()> { + let url: String = url.into(); + let username: String = username.into(); + let password: String = password.into(); let patch = IVerge { webdav_url: Some(url), webdav_username: Some(username), @@ -25,22 +32,31 @@ pub async fn save_webdav_config(url: String, username: String, password: String) } #[tauri::command] +#[specta::specta] pub async fn create_webdav_backup() -> CmdResult<()> { feat::create_backup_and_upload_webdav().await.stringify_err() } #[tauri::command] -pub async fn list_webdav_backup() -> CmdResult> { - feat::list_wevdav_backup().await.stringify_err() +#[specta::specta] +pub async fn list_webdav_backup() -> CmdResult> { + feat::list_wevdav_backup() + .await + .map(|files| files.into_iter().map(Into::into).collect()) + .stringify_err() } #[tauri::command] -pub async fn delete_webdav_backup(filename: String) -> CmdResult<()> { +#[specta::specta] +pub async fn delete_webdav_backup(filename: std::string::String) -> CmdResult<()> { + let filename: String = filename.into(); feat::delete_webdav_backup(filename).await.stringify_err() } #[tauri::command] -pub async fn restore_webdav_backup(filename: String) -> CmdResult<()> { +#[specta::specta] +pub async fn restore_webdav_backup(filename: std::string::String) -> CmdResult<()> { + let filename: String = filename.into(); feat::restore_webdav_backup(filename) .await .map_err(|error| super::proxy_aware_error(&error)) diff --git a/src-tauri/src/config/clash.rs b/src-tauri/src/config/clash.rs index 8495572ee..106c55d9f 100644 --- a/src-tauri/src/config/clash.rs +++ b/src-tauri/src/config/clash.rs @@ -308,7 +308,7 @@ impl IClashTemp { } } -#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)] pub struct ClashInfo { /// clash core port pub mixed_port: u16, @@ -364,14 +364,14 @@ fn test_clash_info() { assert_eq!(get_case(8888, "192.168.1.1:80800"), get_result(8888, "127.0.0.1:9097")); } -#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)] #[serde(rename_all = "kebab-case")] pub struct IClashExternalControllerCors { pub allow_origins: Option>, pub allow_private_network: Option, } -#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)] #[serde(rename_all = "kebab-case")] pub struct IClash { pub mixed_port: Option, @@ -387,7 +387,7 @@ pub struct IClash { pub external_controller_cors: Option, } -#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)] #[serde(rename_all = "kebab-case")] pub struct IClashTUN { pub enable: Option, @@ -397,7 +397,7 @@ pub struct IClashTUN { pub dns_hijack: Option>, } -#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)] #[serde(rename_all = "kebab-case")] pub struct IClashDNS { pub enable: Option, @@ -414,7 +414,7 @@ pub struct IClashDNS { pub nameserver_policy: Option>, } -#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)] #[serde(rename_all = "kebab-case")] pub struct IClashFallbackFilter { pub geoip: Option, diff --git a/src-tauri/src/config/dns.rs b/src-tauri/src/config/dns.rs index 7108b70c3..86727ab76 100644 --- a/src-tauri/src/config/dns.rs +++ b/src-tauri/src/config/dns.rs @@ -106,10 +106,11 @@ impl DnsOverrideState { } } -#[derive(Debug, Clone, Default, Deserialize, Serialize)] +#[derive(Debug, Clone, Default, Deserialize, Serialize, specta::Type)] pub struct ProfileDnsSettings { pub enabled: bool, #[serde(default, skip_serializing_if = "Option::is_none")] + #[specta(type = Option)] pub confirmation: Option, } diff --git a/src-tauri/src/config/prfitem.rs b/src-tauri/src/config/prfitem.rs index f7c84de55..da8728d21 100644 --- a/src-tauri/src/config/prfitem.rs +++ b/src-tauri/src/config/prfitem.rs @@ -27,22 +27,27 @@ pub(super) fn normalize_profile_home_url(raw: &str) -> Option { Some(url.to_string().into()) } -#[derive(Debug, Clone, Deserialize, Serialize, Default)] +#[derive(Debug, Clone, Deserialize, Serialize, Default, specta::Type)] +#[serde(default)] pub struct PrfItem { + #[specta(type = Option)] pub uid: Option, /// enum value: remote | local | script | merge #[serde(rename = "type")] + #[specta(type = Option)] pub itype: Option, - + #[specta(type = Option)] pub name: Option, - + #[specta(type = Option)] pub file: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[specta(type = Option)] pub desc: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[specta(type = Option)] pub url: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -57,19 +62,24 @@ pub struct PrfItem { pub option: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[specta(type = Option)] pub home: Option, #[serde(skip)] + #[specta(type = Option)] pub file_data: Option, } -#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)] +#[serde(default)] pub struct PrfSelected { + #[specta(type = Option)] pub name: Option, + #[specta(type = Option)] pub now: Option, } -#[derive(Default, Debug, Clone, Copy, Deserialize, Serialize)] +#[derive(Default, Debug, Clone, Copy, Deserialize, Serialize, specta::Type)] pub struct PrfExtra { pub upload: u64, pub download: u64, @@ -77,9 +87,11 @@ pub struct PrfExtra { pub expire: u64, } -#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)] +#[serde(default)] pub struct PrfOption { #[serde(skip_serializing_if = "Option::is_none")] + #[specta(type = Option)] pub user_agent: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -102,15 +114,15 @@ pub struct PrfOption { #[serde(skip_serializing_if = "Option::is_none")] pub allow_auto_update: Option, - + #[specta(type = Option)] pub merge: Option, - + #[specta(type = Option)] pub script: Option, - + #[specta(type = Option)] pub rules: Option, - + #[specta(type = Option)] pub proxies: Option, - + #[specta(type = Option)] pub groups: Option, } diff --git a/src-tauri/src/config/profiles.rs b/src-tauri/src/config/profiles.rs index 30f1c9c1b..4b149b716 100644 --- a/src-tauri/src/config/profiles.rs +++ b/src-tauri/src/config/profiles.rs @@ -48,8 +48,10 @@ const SELECTED_NODES_SETTLE_INTERVAL: Duration = Duration::from_secs(1); const SELECTED_NODES_FIRST_PASS_BUDGET: Duration = Duration::from_secs(3); /// Define the `profiles.yaml` schema -#[derive(Default, Debug, Clone, Deserialize, Serialize)] +#[derive(Default, Debug, Clone, Deserialize, Serialize, specta::Type)] +#[serde(default)] pub struct IProfiles { + #[specta(type = Option)] pub current: Option, pub items: Option>, diff --git a/src-tauri/src/config/verge.rs b/src-tauri/src/config/verge.rs index d6d6b8dfe..45b0e18ec 100644 --- a/src-tauri/src/config/verge.rs +++ b/src-tauri/src/config/verge.rs @@ -11,26 +11,30 @@ use serde::{Deserialize, Serialize}; use smartstring::alias::String; /// ### `verge.yaml` schema -#[derive(Default, Debug, Clone, Deserialize, Serialize)] +#[derive(Default, Debug, Clone, Deserialize, Serialize, specta::Type)] +#[serde(default)] pub struct IVerge { /// silent | error | warn | info | debug | trace + #[specta(type = Option)] pub app_log_level: Option, /// Maximum app log size in KiB. pub app_log_max_size: Option, pub app_log_max_count: Option, - + #[specta(type = Option)] pub language: Option, /// `light` or `dark` or `system` + #[specta(type = Option)] pub theme_mode: Option, - + #[specta(type = Option)] pub tray_event: Option, - + #[specta(type = Option)] pub env_type: Option, - + #[specta(type = Option)] pub start_page: Option, + #[specta(type = Option)] pub startup_script: Option, pub traffic_graph: Option, @@ -48,18 +52,23 @@ pub struct IVerge { #[cfg(target_os = "macos")] #[serde(skip_serializing_if = "Option::is_none")] + #[specta(type = Option)] pub tray_icon: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[specta(type = Option)] pub menu_icon: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[specta(type = Option>)] pub menu_order: Option>, #[serde(skip_serializing_if = "Option::is_none")] + #[specta(type = Option)] pub proxy_group_tools_position: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[specta(type = Option)] pub notice_position: Option, pub collapse_navbar: Option, @@ -84,39 +93,43 @@ pub struct IVerge { pub enable_dns_settings: Option, #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")] + #[specta(type = std::collections::BTreeMap)] pub profile_dns_settings: std::collections::BTreeMap, pub use_default_bypass: Option, - + #[specta(type = Option)] pub system_proxy_bypass: Option, pub proxy_guard_duration: Option, pub proxy_auto_config: Option, - + #[specta(type = Option)] pub pac_file_content: Option, - + #[specta(type = Option)] pub proxy_host: Option, pub theme_setting: Option, - + #[specta(type = Option>)] pub web_ui_list: Option>, #[serde(skip_serializing_if = "Option::is_none")] + #[specta(type = Option)] pub clash_core: Option, /// format: {func},{key} #[serde(skip_serializing_if = "Option::is_none")] + #[specta(type = Option>)] pub hotkeys: Option>, pub enable_global_hotkey: Option, + #[specta(type = Option)] pub home_cards: Option, pub auto_close_connection: Option, pub auto_check_update: Option, - + #[specta(type = Option)] pub default_latency_test: Option, pub default_latency_timeout: Option, @@ -171,6 +184,7 @@ pub struct IVerge { skip_serializing_if = "Option::is_none", default )] + #[specta(type = Option)] pub webdav_url: Option, #[serde( @@ -179,6 +193,7 @@ pub struct IVerge { skip_serializing_if = "Option::is_none", default )] + #[specta(type = Option)] pub webdav_username: Option, #[serde( @@ -187,12 +202,14 @@ pub struct IVerge { skip_serializing_if = "Option::is_none", default )] + #[specta(type = Option)] pub webdav_password: Option, #[cfg(target_os = "macos")] pub enable_tray_speed: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[specta(type = Option)] pub tray_proxy_groups_display_mode: Option, pub tray_inline_outbound_modes: Option, @@ -209,27 +226,41 @@ pub struct IVerge { pub enable_external_controller: Option, } -#[derive(Default, Debug, Clone, Deserialize, Serialize)] +#[derive(Default, Debug, Clone, Deserialize, Serialize, specta::Type)] +#[serde(default)] pub struct IVergeTestItem { + #[specta(type = Option)] pub uid: Option, + #[specta(type = Option)] pub name: Option, + #[specta(type = Option)] pub icon: Option, + #[specta(type = Option)] pub url: Option, } -#[derive(Default, Debug, Clone, Deserialize, Serialize)] +#[derive(Default, Debug, Clone, Deserialize, Serialize, specta::Type)] +#[serde(default)] pub struct IVergeTheme { + #[specta(type = Option)] pub primary_color: Option, + #[specta(type = Option)] pub secondary_color: Option, + #[specta(type = Option)] pub primary_text: Option, + #[specta(type = Option)] pub secondary_text: Option, - + #[specta(type = Option)] pub info_color: Option, + #[specta(type = Option)] pub error_color: Option, + #[specta(type = Option)] pub warning_color: Option, + #[specta(type = Option)] pub success_color: Option, - + #[specta(type = Option)] pub font_family: Option, + #[specta(type = Option)] pub css_injection: Option, } diff --git a/src-tauri/src/core/listener.rs b/src-tauri/src/core/listener.rs index fa330d7f9..8fb97c82e 100644 --- a/src-tauri/src/core/listener.rs +++ b/src-tauri/src/core/listener.rs @@ -38,7 +38,7 @@ pub(crate) fn proxy_listener_keys() -> impl Iterator { PROXY_LISTENERS.iter().map(|(key, _, _)| *key) } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, specta::Type)] #[serde(rename_all = "camelCase")] pub struct ProxyPortSettings { pub mixed_port: u16, @@ -48,27 +48,27 @@ pub struct ProxyPortSettings { pub tproxy: ToggleableProxyPort, } -#[derive(Clone, Copy, Debug, Deserialize)] +#[derive(Clone, Copy, Debug, Deserialize, specta::Type)] pub struct ToggleableProxyPort { pub enabled: bool, pub port: u16, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, specta::Type)] #[serde(rename_all = "camelCase")] pub struct ListenerProbe { pub address: String, pub transports: Vec, } -#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Hash, Serialize)] +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Hash, Serialize, specta::Type)] #[serde(rename_all = "lowercase")] pub enum ListenerTransport { Tcp, Udp, } -#[derive(Debug, PartialEq, Eq, Serialize)] +#[derive(Debug, PartialEq, Eq, Serialize, specta::Type)] #[serde(tag = "status", rename_all = "camelCase")] pub enum ListenerProbeOutcome { Available, @@ -77,7 +77,7 @@ pub enum ListenerProbeOutcome { Indeterminate { message: String }, } -#[derive(Debug, PartialEq, Eq, Serialize)] +#[derive(Debug, PartialEq, Eq, Serialize, specta::Type)] #[serde(tag = "status", rename_all = "camelCase")] pub enum SaveProxyPortsOutcome { Saved, diff --git a/src-tauri/src/core/manager/mod.rs b/src-tauri/src/core/manager/mod.rs index b9ea77b04..1110540b1 100644 --- a/src-tauri/src/core/manager/mod.rs +++ b/src-tauri/src/core/manager/mod.rs @@ -57,7 +57,7 @@ fn claim_core_readiness_generation(state: &AtomicU64, captured_generation: u64) .is_ok() } -#[derive(Clone, Copy, Debug, serde::Serialize, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, serde::Serialize, PartialEq, Eq, specta::Type)] pub enum RunningMode { Service, Sidecar, @@ -75,7 +75,7 @@ impl fmt::Display for RunningMode { } /// Why the Core is not running, kept until a Core starts again. -#[derive(Clone, Debug, serde::Serialize, PartialEq, Eq)] +#[derive(Clone, Debug, serde::Serialize, PartialEq, Eq, specta::Type)] #[serde(tag = "kind", content = "detail", rename_all = "camelCase")] pub enum CoreFailure { StartFailed(String), diff --git a/src-tauri/src/core/notify/handle.rs b/src-tauri/src/core/notify/handle.rs index 0a0cbfe8b..6a05ab138 100644 --- a/src-tauri/src/core/notify/handle.rs +++ b/src-tauri/src/core/notify/handle.rs @@ -52,10 +52,7 @@ impl Handle { /// Sent on every transition, so the frontend does not have to poll to notice that the Core /// stopped or that the Service came back. pub fn notify_run_state(state: &crate::core::runstate::RunStateView) { - let Ok(state) = serde_json::to_value(state) else { - return; - }; - Self::send_event(FrontendEvent::RunStateChanged { state }); + Self::send_event(FrontendEvent::RunStateChanged { state: state.clone() }); } pub fn notify_profile_changed(profile_id: &String) { diff --git a/src-tauri/src/core/notify/mod.rs b/src-tauri/src/core/notify/mod.rs index 1387dbc81..9a6b7699f 100644 --- a/src-tauri/src/core/notify/mod.rs +++ b/src-tauri/src/core/notify/mod.rs @@ -3,9 +3,8 @@ pub use announce::{Refresh, announce}; pub mod desktop; pub mod handle; pub mod notification; -pub use notification::frontend_wire_contract; -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, specta::Type)] pub enum NoticeStatus { #[serde(rename = "info")] Info, @@ -83,6 +82,7 @@ impl NoticeStatus { /// Every variant in golden order. The wire-contract export and the golden /// test both derive their variant list from here; expected strings stay /// hand-written in the test so renames cannot slip through. + #[cfg(test)] pub const ALL: [Self; 34] = [ Self::Info, Self::ImportSubUrlOk, diff --git a/src-tauri/src/core/notify/notification.rs b/src-tauri/src/core/notify/notification.rs index 95eaa13da..3f6e92c43 100644 --- a/src-tauri/src/core/notify/notification.rs +++ b/src-tauri/src/core/notify/notification.rs @@ -1,6 +1,7 @@ use super::NoticeStatus; use clash_verge_logging::{Type, logging}; use parking_lot::Mutex; +#[cfg(test)] use serde_json::json; use smartstring::alias::String; use std::sync::Arc; @@ -31,7 +32,7 @@ pub enum FrontendEvent<'a> { uid: &'a String, }, RunStateChanged { - state: serde_json::Value, + state: crate::core::runstate::RunStateView, }, PendingFailuresChanged, #[cfg(target_os = "linux")] @@ -42,7 +43,7 @@ pub enum FrontendEvent<'a> { /// Operation associated with a pending failure. #[allow(clippy::enum_variant_names)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, specta::Type)] #[serde(rename_all = "camelCase")] pub enum FailedOperation { SystemProxyEnable, @@ -72,12 +73,14 @@ impl FailedOperation { } /// Latest unresolved failure for one stable code. -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, specta::Type)] #[serde(rename_all = "camelCase")] pub struct PendingFailure { /// Stable code used as the table key. + #[specta(type = std::string::String)] pub code: String, /// Full diagnostic context chain. + #[specta(type = std::string::String)] pub detail: String, pub operation: FailedOperation, /// Monotonic identity for repeated failures under the same code. @@ -241,9 +244,10 @@ pub fn what_was_asked() -> FailedOperation { static PENDING_FAILURES: LazyLock = LazyLock::new(FailureTable::default); /// A removal has a revision too, so a delayed read cannot restore a resolved failure. -#[derive(Debug, Default, Clone, serde::Serialize)] +#[derive(Debug, Default, Clone, serde::Serialize, specta::Type)] pub struct SidecarFailureSnapshot { pub revision: u64, + #[specta(type = Option)] pub detail: Option, } @@ -327,24 +331,7 @@ impl NotificationSystem { } fn serialize_event(event: FrontendEvent) -> (&'static str, Result) { - match event { - FrontendEvent::RefreshClash => ("verge://refresh-clash-config", Ok(json!("yes"))), - FrontendEvent::RefreshVerge => ("verge://refresh-verge-config", Ok(json!("yes"))), - FrontendEvent::RefreshProfiles => ("verge://refresh-profiles", Ok(json!("yes"))), - FrontendEvent::RefreshProxyConfig => ("verge://refresh-proxy-config", Ok(serde_json::Value::Null)), - FrontendEvent::NoticeMessage { status, message } => ( - "verge://notice-message", - serde_json::to_value((status, message.as_ref())), - ), - FrontendEvent::ProfileChanged { current_profile_id } => ("profile-changed", Ok(json!(current_profile_id))), - FrontendEvent::TimerUpdated { profile_index } => ("verge://timer-updated", Ok(json!(profile_index))), - FrontendEvent::ProfileUpdateStarted { uid } => ("profile-update-started", Ok(json!({ "uid": uid }))), - FrontendEvent::ProfileUpdateCompleted { uid } => ("profile-update-completed", Ok(json!({ "uid": uid }))), - FrontendEvent::RunStateChanged { state } => ("verge://run-state-changed", Ok(state)), - FrontendEvent::PendingFailuresChanged => ("verge://pending-failures-changed", Ok(serde_json::Value::Null)), - #[cfg(target_os = "linux")] - FrontendEvent::ThemeChanged { theme } => ("tauri://theme-changed", serde_json::to_value(theme)), - } + serialize_frontend_event(event) } pub(crate) fn send_event(app_handle: AppHandle, event: FrontendEvent) { @@ -381,8 +368,17 @@ mod wire_tests { } /// One sample per `FrontendEvent` variant, paired with its wire name and payload. -/// Shared by the wire golden test and the contract export so the two cannot drift. +#[cfg(test)] fn sample_frontend_events(uid: &String) -> Vec<(FrontendEvent<'_>, &'static str, serde_json::Value)> { + let state = crate::core::runstate::RunState { + health: Default::default(), + pending: None, + sidecar_allowed: false, + mode: crate::core::manager::RunningMode::NotRunning, + is_admin: false, + op_in_flight: false, + } + .to_view(); vec![ ( FrontendEvent::RefreshClash, @@ -414,11 +410,9 @@ fn sample_frontend_events(uid: &String) -> Vec<(FrontendEvent<'_>, &'static str, json!("profile"), ), ( - FrontendEvent::RunStateChanged { - state: json!({"running": true}), - }, + FrontendEvent::RunStateChanged { state: state.clone() }, "verge://run-state-changed", - json!({"running": true}), + serde_json::to_value(state).unwrap_or(serde_json::Value::Null), ), ( FrontendEvent::PendingFailuresChanged, @@ -445,24 +439,41 @@ fn sample_frontend_events(uid: &String) -> Vec<(FrontendEvent<'_>, &'static str, ] } -/// Machine-readable form of the frontend wire contract: every event name with a -/// serialized sample payload, plus every notice status string. The sample -/// payload's JSON shape (string / null / array / object) is what the frontend -/// generator maps to TypeScript types. `ThemeChanged` is a Tauri built-in -/// linux-only window event, not part of this contract. -pub fn frontend_wire_contract() -> serde_json::Value { - let uid = String::from("sample"); - let events: Vec<_> = sample_frontend_events(&uid) - .into_iter() - .map(|(event, _, _)| { - let (name, payload) = NotificationSystem::serialize_event(event); - json!({ "name": name, "sample": payload.unwrap_or(serde_json::Value::Null) }) - }) - .collect(); - let statuses: Vec<_> = NoticeStatus::ALL - .iter() - // A unit variant with a string rename cannot fail to serialize. - .filter_map(|status| serde_json::to_value(status).ok()) - .collect(); - json!({ "version": 1, "events": events, "noticeStatuses": statuses }) +macro_rules! frontend_events { + ($( $field:ident: $payload:ty = $name:literal, $pattern:pat => $value:expr; )*) => { + #[derive(serde::Serialize, specta::Type)] + pub struct VergeEventPayloads { + $(#[serde(rename = $name)] pub $field: $payload,)* + } + + fn serialize_frontend_event(event: FrontendEvent) -> (&'static str, Result) { + match event { + $($pattern => { + let payload: $payload = $value; + ($name, serde_json::to_value(payload)) + },)* + #[cfg(target_os = "linux")] + FrontendEvent::ThemeChanged { theme } => ("tauri://theme-changed", serde_json::to_value(theme)), + } + } + }; +} + +frontend_events! { + refresh_clash: std::string::String = "verge://refresh-clash-config", FrontendEvent::RefreshClash => "yes".to_owned(); + refresh_verge: std::string::String = "verge://refresh-verge-config", FrontendEvent::RefreshVerge => "yes".to_owned(); + refresh_profiles: std::string::String = "verge://refresh-profiles", FrontendEvent::RefreshProfiles => "yes".to_owned(); + refresh_proxy: () = "verge://refresh-proxy-config", FrontendEvent::RefreshProxyConfig => (); + notice: (NoticeStatus, std::string::String) = "verge://notice-message", FrontendEvent::NoticeMessage { status, ref message } => (status, message.to_string()); + profile_changed: std::string::String = "profile-changed", FrontendEvent::ProfileChanged { current_profile_id } => current_profile_id.to_string(); + timer_updated: std::string::String = "verge://timer-updated", FrontendEvent::TimerUpdated { profile_index } => profile_index.to_string(); + profile_started: ProfileUpdatePayload = "profile-update-started", FrontendEvent::ProfileUpdateStarted { uid } => ProfileUpdatePayload { uid: uid.to_string() }; + profile_completed: ProfileUpdatePayload = "profile-update-completed", FrontendEvent::ProfileUpdateCompleted { uid } => ProfileUpdatePayload { uid: uid.to_string() }; + run_state: crate::core::runstate::RunStateView = "verge://run-state-changed", FrontendEvent::RunStateChanged { state } => state; + pending_failures: () = "verge://pending-failures-changed", FrontendEvent::PendingFailuresChanged => (); +} + +#[derive(serde::Serialize, specta::Type)] +pub struct ProfileUpdatePayload { + pub uid: std::string::String, } diff --git a/src-tauri/src/core/proxy_view.rs b/src-tauri/src/core/proxy_view.rs index 4506bd0ce..9056b521b 100644 --- a/src-tauri/src/core/proxy_view.rs +++ b/src-tauri/src/core/proxy_view.rs @@ -20,7 +20,7 @@ pub struct GroupScope { pub struct ProxyViewBuilder; -#[derive(Debug, PartialEq, Eq, Serialize)] +#[derive(Debug, PartialEq, Eq, Serialize, specta::Type)] #[serde(rename_all = "camelCase")] pub struct ProxyViewV1 { pub schema_version: u8, @@ -34,21 +34,21 @@ pub struct ProxyViewV1 { pub providers: Vec, } -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, specta::Type)] #[serde(rename_all = "lowercase")] pub enum ProxyViewOrderSource { Runtime, Fallback, } -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, specta::Type)] #[serde(rename_all = "lowercase")] pub enum ProxyViewProviderState { Ready, Unavailable, } -#[derive(Debug, PartialEq, Eq, Serialize)] +#[derive(Debug, PartialEq, Eq, Serialize, specta::Type)] pub struct ProxyCapabilities { pub udp: bool, pub xudp: bool, @@ -57,11 +57,12 @@ pub struct ProxyCapabilities { pub smux: bool, } -#[derive(Debug, PartialEq, Eq, Serialize)] +#[derive(Debug, PartialEq, Eq, Serialize, specta::Type)] #[serde(rename_all = "camelCase")] pub struct ProxyGroupView { pub name: String, #[serde(rename = "type")] + #[specta(type = String)] pub proxy_type: ProxyType, pub alive: bool, #[serde(skip_serializing_if = "Option::is_none")] @@ -74,20 +75,23 @@ pub struct ProxyGroupView { pub icon: Option, #[serde(skip_serializing_if = "Option::is_none")] pub test_url: Option, + #[specta(type = Vec)] pub history: Vec, #[serde(flatten)] pub capabilities: ProxyCapabilities, pub members: Vec, } -#[derive(Debug, PartialEq, Eq, Serialize)] +#[derive(Debug, PartialEq, Eq, Serialize, specta::Type)] #[serde(rename_all = "camelCase")] pub struct ProxyNodeView { pub record_id: String, pub name: String, #[serde(rename = "type")] + #[specta(type = String)] pub proxy_type: ProxyType, pub alive: bool, + #[specta(type = Vec)] pub history: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub id: Option, @@ -102,7 +106,7 @@ pub struct ProxyNodeView { pub source: ProxyNodeSource, } -#[derive(Debug, PartialEq, Eq, Serialize)] +#[derive(Debug, PartialEq, Eq, Serialize, specta::Type)] #[serde(tag = "kind", rename_all = "camelCase")] pub enum ProxyNodeSource { Core { @@ -117,7 +121,7 @@ pub enum ProxyNodeSource { }, } -#[derive(Debug, PartialEq, Eq, Serialize)] +#[derive(Debug, PartialEq, Eq, Serialize, specta::Type)] #[serde(tag = "kind", rename_all = "camelCase")] pub enum ProxyMemberRef { Group { @@ -134,7 +138,7 @@ pub enum ProxyMemberRef { }, } -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, specta::Type)] pub enum ProxyMemberUnresolvedReason { #[serde(rename = "missing")] Missing, @@ -144,7 +148,7 @@ pub enum ProxyMemberUnresolvedReason { ProviderUnavailable, } -#[derive(Debug, PartialEq, Eq, Serialize)] +#[derive(Debug, PartialEq, Eq, Serialize, specta::Type)] #[serde(rename_all = "camelCase")] pub struct ProxyProviderView { pub name: String, @@ -156,7 +160,7 @@ pub struct ProxyProviderView { pub proxy_record_ids: Vec, } -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, specta::Type)] pub enum ProxyProviderVehicleType { #[serde(rename = "HTTP")] Http, @@ -166,7 +170,7 @@ pub enum ProxyProviderVehicleType { Inline, } -#[derive(Debug, PartialEq, Eq, Serialize)] +#[derive(Debug, PartialEq, Eq, Serialize, specta::Type)] pub struct ProxySubscriptionInfo { pub upload: i64, pub download: i64, diff --git a/src-tauri/src/core/runstate/health.rs b/src-tauri/src/core/runstate/health.rs index 6bb3505a0..1a9ff685e 100644 --- a/src-tauri/src/core/runstate/health.rs +++ b/src-tauri/src/core/runstate/health.rs @@ -14,13 +14,13 @@ pub enum ServiceHealth { } impl ServiceHealth { - const fn kind(&self) -> &'static str { + const fn kind(&self) -> ServiceHealthKind { match self { - Self::Unknown => "unknown", - Self::Ready => "ready", - Self::NotInstalled => "notInstalled", - Self::VersionMismatch => "versionMismatch", - Self::Unavailable(_) => "unavailable", + Self::Unknown => ServiceHealthKind::Unknown, + Self::Ready => ServiceHealthKind::Ready, + Self::NotInstalled => ServiceHealthKind::NotInstalled, + Self::VersionMismatch => ServiceHealthKind::VersionMismatch, + Self::Unavailable(_) => ServiceHealthKind::Unavailable, } } @@ -32,7 +32,7 @@ impl ServiceHealth { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, specta::Type)] #[serde(rename_all = "camelCase")] pub enum PendingAction { Install, @@ -118,11 +118,11 @@ impl RunState { } } -#[derive(Debug, Clone, serde::Serialize)] +#[derive(Debug, Clone, serde::Serialize, specta::Type)] #[serde(rename_all = "camelCase")] pub struct RunStateView { pub mode: RunningMode, - pub service: &'static str, + pub service: ServiceHealthKind, pub service_unavailable_reason: Option, pub pending_action: Option, pub sidecar_allowed: bool, @@ -232,7 +232,7 @@ mod tests { let view = run_state.to_view(); - assert_eq!(view.service, "unavailable"); + assert_eq!(view.service, ServiceHealthKind::Unavailable); assert_eq!(view.service_unavailable_reason.as_deref(), Some("socket refused")); assert_eq!(view.pending_action, Some(PendingAction::Reinstall)); assert!(view.tun_capable, "elevation alone makes TUN possible"); @@ -244,7 +244,7 @@ mod tests { fn a_healthy_view_reports_no_unavailable_reason() { let view = state(ServiceHealth::Ready, false, false).to_view(); - assert_eq!(view.service, "ready"); + assert_eq!(view.service, ServiceHealthKind::Ready); assert_eq!(view.service_unavailable_reason, None); assert!(view.service_usable); assert!(!view.service_needs_attention); @@ -350,3 +350,13 @@ mod tests { assert!(settled.tun_should_be_disabled(true)); } } + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, specta::Type)] +#[serde(rename_all = "camelCase")] +pub enum ServiceHealthKind { + Unknown, + Ready, + NotInstalled, + VersionMismatch, + Unavailable, +} diff --git a/src-tauri/src/core/service.rs b/src-tauri/src/core/service.rs index 4b0704300..7d9242087 100644 --- a/src-tauri/src/core/service.rs +++ b/src-tauri/src/core/service.rs @@ -44,7 +44,7 @@ static PENDING_SERVICE_REPAIR_NOTICE: AtomicBool = AtomicBool::new(false); static PENDING_SERVICE_OWNER_NOTICE: Mutex> = Mutex::new(None); /// Why the Service was unavailable when the core fell back to Sidecar. -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, specta::Type)] #[serde(tag = "kind", content = "reason", rename_all = "camelCase")] pub enum ServiceFallbackNotice { Unavailable, diff --git a/src-tauri/src/core/updater.rs b/src-tauri/src/core/updater.rs index 0948a3801..c017582eb 100644 --- a/src-tauri/src/core/updater.rs +++ b/src-tauri/src/core/updater.rs @@ -34,7 +34,7 @@ impl SilentUpdater { } } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, specta::Type)] struct UpdateCacheMeta { version: String, downloaded_at: String, @@ -214,7 +214,7 @@ fn nsis_language_id(app_language: &str) -> &'static str { } /// Same wire shape as the updater plugin's JS `DownloadEvent`. -#[derive(Clone, Serialize)] +#[derive(Clone, Serialize, specta::Type)] #[serde(tag = "event", content = "data")] pub enum DownloadEvent { #[serde(rename_all = "camelCase")] diff --git a/src-tauri/src/core/validate.rs b/src-tauri/src/core/validate.rs index 90517bb44..b244127d1 100644 --- a/src-tauri/src/core/validate.rs +++ b/src-tauri/src/core/validate.rs @@ -51,7 +51,7 @@ pub struct CoreConfigValidator { is_processing: AtomicBool, } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, specta::Type)] #[serde(rename_all = "camelCase")] pub enum ValidationErrorKind { FileMissing, @@ -94,7 +94,7 @@ impl ValidationErrorKind { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, specta::Type)] #[serde(rename_all = "camelCase")] pub enum ValidationSkipReason { Exiting, @@ -110,12 +110,18 @@ impl fmt::Display for ValidationSkipReason { } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, specta::Type)] #[serde(tag = "status", rename_all = "camelCase")] pub enum ValidationOutcome { Valid, - Invalid { kind: ValidationErrorKind, message: String }, - Skipped { reason: ValidationSkipReason }, + Invalid { + kind: ValidationErrorKind, + #[specta(type = std::string::String)] + message: String, + }, + Skipped { + reason: ValidationSkipReason, + }, Busy, } diff --git a/src-tauri/src/feat/backup.rs b/src-tauri/src/feat/backup.rs index 32a6e1de6..7c246d36a 100644 --- a/src-tauri/src/feat/backup.rs +++ b/src-tauri/src/feat/backup.rs @@ -16,10 +16,13 @@ use smartstring::alias::String; use std::path::PathBuf; use tokio::fs; -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, specta::Type)] pub struct LocalBackupFile { + #[specta(type = std::string::String)] pub filename: String, + #[specta(type = std::string::String)] pub path: String, + #[specta(type = std::string::String)] pub last_modified: String, pub content_length: u64, } diff --git a/src-tauri/src/feat/core_upgrade.rs b/src-tauri/src/feat/core_upgrade.rs index 805dbdb59..388696f66 100644 --- a/src-tauri/src/feat/core_upgrade.rs +++ b/src-tauri/src/feat/core_upgrade.rs @@ -34,7 +34,7 @@ static STAGING_GENERATION: AtomicU64 = AtomicU64::new(0); /// `.rollback` and `.old` are fixed paths, so two upgrades must not overlap. static UPGRADE_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(()); -#[derive(Debug, serde::Serialize)] +#[derive(Debug, serde::Serialize, specta::Type)] pub struct CoreUpgradeReport { /// False when the managed core was already at the latest version. pub upgraded: bool, diff --git a/src-tauri/src/feat/dns.rs b/src-tauri/src/feat/dns.rs index 9092676e1..a897b1b59 100644 --- a/src-tauri/src/feat/dns.rs +++ b/src-tauri/src/feat/dns.rs @@ -15,11 +15,14 @@ use crate::{ utils::dirs, }; -#[derive(Serialize)] +#[derive(Serialize, specta::Type)] #[serde(tag = "status", rename_all = "snake_case")] pub enum DnsOverrideOutcome { Applied, - ConfirmationRequired { source: String }, + ConfirmationRequired { + #[specta(type = std::string::String)] + source: String, + }, } pub async fn set_dns_override( diff --git a/src-tauri/src/feat/icon.rs b/src-tauri/src/feat/icon.rs index a69577cee..2ac973002 100644 --- a/src-tauri/src/feat/icon.rs +++ b/src-tauri/src/feat/icon.rs @@ -8,10 +8,13 @@ use std::path::{Component, Path, PathBuf}; use tokio::fs; use tokio::io::AsyncWriteExt as _; -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, serde::Serialize, serde::Deserialize, specta::Type)] pub struct IconInfo { + #[specta(type = std::string::String)] name: String, + #[specta(type = std::string::String)] previous_t: String, + #[specta(type = std::string::String)] current_t: String, } diff --git a/src-tauri/src/ipc.rs b/src-tauri/src/ipc.rs new file mode 100644 index 000000000..19bf03264 --- /dev/null +++ b/src-tauri/src/ipc.rs @@ -0,0 +1,236 @@ +use crate::cmd; + +pub fn builder() -> tauri_specta::Builder { + tauri_specta::Builder::new() + .types(&platform_types()) + .commands(tauri_specta::collect_commands![ + tauri_plugin_clash_verge_sysinfo::commands::get_system_info, + tauri_plugin_clash_verge_sysinfo::commands::get_app_uptime, + tauri_plugin_clash_verge_sysinfo::commands::export_diagnostic_info::, + cmd::probe_listener, + cmd::save_proxy_ports, + cmd::get_sys_proxy, + cmd::get_auto_proxy, + cmd::get_embedded_server_port, + cmd::open_app_dir, + cmd::open_logs_dir, + cmd::open_core_dir, + cmd::get_network_interfaces, + cmd::get_system_hostname, + cmd::restart_app, + cmd::install_update, + cmd::cancel_update_download, + cmd::restart_core, + cmd::upgrade_clash_core, + cmd::get_runtime_state, + cmd::get_pending_failures, + cmd::get_sidecar_failure, + cmd::entry_lightweight_mode, + cmd::install_service, + cmd::uninstall_service, + cmd::reinstall_service, + cmd::repair_service, + cmd::continue_with_sidecar, + cmd::sync_runtime_providers, + cmd::get_clash_info, + cmd::patch_clash_config, + cmd::patch_clash_mode, + cmd::get_clash_mode, + cmd::change_clash_core, + cmd::get_runtime_config, + cmd::get_proxy_view, + cmd::get_runtime_yaml, + cmd::get_runtime_logs, + cmd::get_runtime_proxy_chain_config, + cmd::update_proxy_chain_config_in_runtime, + cmd::invoke_uwp_tool, + cmd::copy_clash_env, + cmd::sync_tray_proxy_selection, + cmd::record_selected_node, + cmd::forget_selected_node, + cmd::save_dns_config, + cmd::apply_dns_config, + cmd::set_dns_override, + cmd::take_dns_override_notice, + cmd::take_service_fallback_notice, + cmd::get_core_startup_error, + cmd::take_service_repair_notice, + cmd::take_service_owner_notice, + cmd::take_discarded_keys_notice, + cmd::get_dns_config_content, + cmd::validate_dns_config, + cmd::get_clash_logs, + cmd::get_verge_config, + cmd::patch_verge_config, + cmd::test_delay, + cmd::get_app_dir, + cmd::copy_icon_file, + cmd::download_icon_cache, + cmd::open_devtools, + cmd::exit_app, + cmd::get_network_interfaces_info, + cmd::get_profiles, + cmd::enhance_profiles, + cmd::patch_profiles_config, + cmd::view_profile, + cmd::patch_profile, + cmd::create_profile, + cmd::import_profile, + cmd::reorder_profile, + cmd::update_profile, + cmd::delete_profile, + cmd::read_profile_file, + cmd::save_profile_file, + cmd::get_next_update_time, + cmd::create_local_backup, + cmd::list_local_backup, + cmd::delete_local_backup, + cmd::restore_local_backup, + cmd::import_local_backup, + cmd::export_local_backup, + cmd::create_webdav_backup, + cmd::save_webdav_config, + cmd::list_webdav_backup, + cmd::delete_webdav_backup, + cmd::restore_webdav_backup, + cmd::get_unlock_items, + cmd::check_media_unlock, + cmd::check_media_unlock_item, + ]) + .error_handling(tauri_specta::ErrorHandlingMode::Throw) + .dangerously_cast_bigints_to_number() + .typ::() +} + +// Tauri transports YAML values as JSON; keep YAML storage out of the IPC type graph. +#[derive(serde::Serialize, serde::Deserialize, specta::Type)] +#[serde(transparent)] +pub struct YamlMapping(#[specta(type = std::collections::HashMap)] pub serde_yaml_ng::Mapping); + +#[derive(serde::Serialize, serde::Deserialize, specta::Type)] +#[serde(transparent)] +pub struct YamlValue(#[specta(type = JsonValue)] pub serde_yaml_ng::Value); + +#[derive(serde::Serialize, specta::Type)] +pub struct WebDavFile { + pub href: String, + #[specta(type = String)] + pub last_modified: chrono::DateTime, + pub content_length: i64, + pub content_type: String, + pub tag: Option, +} + +impl From for WebDavFile { + fn from(file: reqwest_dav::list_cmd::ListFile) -> Self { + Self { + href: file.href, + last_modified: file.last_modified, + content_length: file.content_length, + content_type: file.content_type, + tag: file.tag, + } + } +} + +#[derive(serde::Serialize, specta::Type)] +pub struct NetworkInterfaceView { + pub name: String, + #[specta(type = Vec)] + pub addr: Vec, + pub mac_addr: Option, + pub index: u32, + pub internal: bool, +} + +#[derive(serde::Serialize, serde::Deserialize, specta::Type)] +pub enum InterfaceAddress { + V4(IpAddress), + V6(IpAddress), +} + +#[derive(serde::Serialize, serde::Deserialize, specta::Type)] +pub struct IpAddress { + pub ip: String, + pub broadcast: Option, + pub netmask: Option, +} + +impl From for NetworkInterfaceView { + fn from(interface: network_interface::NetworkInterface) -> Self { + Self { + name: interface.name, + addr: interface.addr, + mac_addr: interface.mac_addr, + index: interface.index, + internal: interface.internal, + } + } +} + +#[derive(serde::Serialize, serde::Deserialize, specta::Type)] +pub struct DelayHistory { + pub time: String, + pub delay: u16, +} + +fn platform_types() -> specta::Types { + use specta::datatype::{DataType, Fields}; + let mut types = specta::Types::default().register::(); + ::definition(&mut types); + let platform = types + .into_unsorted_iter() + .find(|ty| ty.name == "PlatformVergeFields") + .and_then(|ty| ty.ty.clone()); + let Some(DataType::Struct(platform)) = platform else { + unreachable!() + }; + let Fields::Named(platform) = platform.fields else { + unreachable!() + }; + types.iter_mut(|ty| { + if ty.name != "IVerge" { + return; + } + let Some(DataType::Struct(config)) = &mut ty.ty else { + return; + }; + let Fields::Named(fields) = &mut config.fields else { + return; + }; + fields + .fields + .retain(|(name, _)| !platform.fields.iter().any(|(platform_name, _)| name == platform_name)); + fields.fields.extend(platform.fields.iter().cloned()); + }); + types +} + +// These fields exist only on some hosts; IPC generation covers every desktop target. +#[derive(serde::Serialize, serde::Deserialize, specta::Type)] +struct PlatformVergeFields { + #[specta(optional)] + tray_icon: Option, + #[specta(optional)] + verge_redir_port: Option, + #[specta(optional)] + verge_redir_enabled: Option, + #[specta(optional)] + verge_tproxy_port: Option, + #[specta(optional)] + verge_tproxy_enabled: Option, + #[specta(optional)] + enable_tray_speed: Option, +} + +#[derive(serde::Serialize, serde::Deserialize, specta::Type)] +#[serde(untagged)] +pub enum JsonValue { + Null(()), + Bool(bool), + Number(f64), + String(String), + // Specta resolves fields in nested functions, where Self is unavailable. + Array(#[specta(type = Vec)] Vec), + Object(#[specta(type = std::collections::HashMap)] std::collections::HashMap), +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 1df5b4582..f2d61f427 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -7,6 +7,7 @@ mod constants; mod core; mod enhance; mod feat; +pub mod ipc; mod process; pub mod utils; @@ -26,9 +27,6 @@ use tauri_plugin_deep_link::DeepLinkExt as _; pub static APP_HANDLE: OnceLock = OnceLock::new(); -// Re-exported for src/bin/frontend-contract.rs; the wire contract stays owned -// by core::notify. -pub use crate::core::notify::frontend_wire_contract; /// Application initialization helper functions mod app_init { use super::*; @@ -127,100 +125,10 @@ mod app_init { } pub fn generate_handlers() -> impl Fn(tauri::ipc::Invoke) -> bool + Send + Sync + 'static { - tauri::generate_handler![ - tauri_plugin_clash_verge_sysinfo::commands::get_system_info, - tauri_plugin_clash_verge_sysinfo::commands::get_app_uptime, - tauri_plugin_clash_verge_sysinfo::commands::export_diagnostic_info, - cmd::probe_listener, - cmd::save_proxy_ports, - cmd::get_sys_proxy, - cmd::get_auto_proxy, - cmd::get_embedded_server_port, - cmd::open_app_dir, - cmd::open_logs_dir, - cmd::open_core_dir, - cmd::get_network_interfaces, - cmd::get_system_hostname, - cmd::restart_app, - cmd::install_update, - cmd::cancel_update_download, - cmd::restart_core, - cmd::upgrade_clash_core, - cmd::get_runtime_state, - cmd::get_pending_failures, - cmd::get_sidecar_failure, - cmd::entry_lightweight_mode, - cmd::install_service, - cmd::uninstall_service, - cmd::reinstall_service, - cmd::repair_service, - cmd::continue_with_sidecar, - cmd::sync_runtime_providers, - cmd::get_clash_info, - cmd::patch_clash_config, - cmd::patch_clash_mode, - cmd::get_clash_mode, - cmd::change_clash_core, - cmd::get_runtime_config, - cmd::get_proxy_view, - cmd::get_runtime_yaml, - cmd::get_runtime_logs, - cmd::get_runtime_proxy_chain_config, - cmd::update_proxy_chain_config_in_runtime, - cmd::invoke_uwp_tool, - cmd::copy_clash_env, - cmd::sync_tray_proxy_selection, - cmd::record_selected_node, - cmd::forget_selected_node, - cmd::save_dns_config, - cmd::apply_dns_config, - cmd::set_dns_override, - cmd::take_dns_override_notice, - cmd::take_service_fallback_notice, - cmd::get_core_startup_error, - cmd::take_service_repair_notice, - cmd::take_service_owner_notice, - cmd::take_discarded_keys_notice, - cmd::get_dns_config_content, - cmd::validate_dns_config, - cmd::get_clash_logs, - cmd::get_verge_config, - cmd::patch_verge_config, - cmd::test_delay, - cmd::get_app_dir, - cmd::copy_icon_file, - cmd::download_icon_cache, - cmd::open_devtools, - cmd::exit_app, - cmd::get_network_interfaces_info, - cmd::get_profiles, - cmd::enhance_profiles, - cmd::patch_profiles_config, - cmd::view_profile, - cmd::patch_profile, - cmd::create_profile, - cmd::import_profile, - cmd::reorder_profile, - cmd::update_profile, - cmd::delete_profile, - cmd::read_profile_file, - cmd::save_profile_file, - cmd::get_next_update_time, - cmd::create_local_backup, - cmd::list_local_backup, - cmd::delete_local_backup, - cmd::restore_local_backup, - cmd::import_local_backup, - cmd::export_local_backup, - cmd::create_webdav_backup, - cmd::save_webdav_config, - cmd::list_webdav_backup, - cmd::delete_webdav_backup, - cmd::restore_webdav_backup, - cmd::get_unlock_items, - cmd::check_media_unlock, - cmd::check_media_unlock_item, - ] + { + let builder = crate::ipc::builder(); + move |invoke| builder.invoke_handler()(invoke) + } } } diff --git a/src/components/home/current-proxy-card.tsx b/src/components/home/current-proxy-card.tsx index e9768b7e4..4b891a623 100644 --- a/src/components/home/current-proxy-card.tsx +++ b/src/components/home/current-proxy-card.tsx @@ -420,7 +420,7 @@ interface PersistentProxySelectProps value: string selectedName: string groupName: string - fixed?: string + fixed?: string | null selectedDelay: number options: ProxyOption[] onChange: (value: string) => void @@ -768,8 +768,8 @@ export const CurrentProxyCard = () => { // The profile selection is the durable source across core restarts and run modes. handleSelectChange( selectedGroup.name, - previousProxy, - selectedGroup.fixed, + previousProxy!, + selectedGroup.fixed!, )({ target: { value: nextName }, }) @@ -917,7 +917,7 @@ export const CurrentProxyCard = () => { unsortedProxyOptions, sortType, delays.of, - defaultLatencyTimeout, + defaultLatencyTimeout!, ), [ delays, diff --git a/src/components/home/home-profile-card.tsx b/src/components/home/home-profile-card.tsx index ad9b4bb82..b41b7fef9 100644 --- a/src/components/home/home-profile-card.tsx +++ b/src/components/home/home-profile-card.tsx @@ -56,26 +56,7 @@ const openProfileHome = (url: string) => { void openExternalUrl(url).catch(showNotice.error) } -// 使用类型定义,而不是导入 -interface ProfileExtra { - upload: number - download: number - total: number - expire: number -} - -interface ProfileItem { - uid: string - type?: 'local' | 'remote' | 'merge' | 'script' - name?: string - desc?: string - file?: string - url?: string - updated?: number - extra?: ProfileExtra - home?: string - option?: any -} +type ProfileItem = IProfileItem interface HomeProfileCardProps { current: ProfileItem | null | undefined @@ -298,10 +279,13 @@ export const HomeProfileCard = ({ setUpdating(true) try { - await mutate(() => updateProfile(current.uid, current.option), { - id: `update-profile:${current.uid}`, - errorNotice: false, - }) + await mutate( + () => updateProfile(current.uid!, current.option ?? undefined), + { + id: `update-profile:${current.uid}`, + errorNotice: false, + }, + ) onProfileUpdated?.() // 刷新首页数据 @@ -345,7 +329,7 @@ export const HomeProfileCard = ({ flex: 1, }, }} - title={current.name} + title={current.name!} > {current.name} { onClick={() => handleTabChange('tun')} icon={TroubleshootRounded} label={t('settings.sections.system.toggles.tunMode')} - hasIndicator={enable_tun_mode && isTunModeAvailable} + hasIndicator={enable_tun_mode! && isTunModeAvailable} /> diff --git a/src/components/home/test-card.tsx b/src/components/home/test-card.tsx index 5081c477a..5371f995e 100644 --- a/src/components/home/test-card.tsx +++ b/src/components/home/test-card.tsx @@ -164,7 +164,7 @@ export const TestCard = () => { }} > {testList.map((item, index) => ( - + viewerRef.current?.edit(item)} diff --git a/src/components/profile/profile-item.tsx b/src/components/profile/profile-item.tsx index ed3c430ca..bc001d157 100644 --- a/src/components/profile/profile-item.tsx +++ b/src/components/profile/profile-item.tsx @@ -120,7 +120,7 @@ const ProfileItemBase = (props: ProfileItemProps) => { ) const setLoading = useCallback( (loading: boolean) => { - setProfileLoading([itemData.uid], loading) + setProfileLoading([itemData.uid!], loading) }, [itemData.uid, setProfileLoading], ) @@ -140,7 +140,7 @@ const ProfileItemBase = (props: ProfileItemProps) => { debugLog(`强制刷新定时器任务`) } - const nextUpdate = await getNextUpdateTime(itemData.uid) + const nextUpdate = await getNextUpdateTime(itemData.uid!) debugLog(`获取到下次更新时间结果:`, nextUpdate) if (nextUpdate) { @@ -245,7 +245,7 @@ const ProfileItemBase = (props: ProfileItemProps) => { const hasHome = !!itemData.home // only subscription url has home page const { upload = 0, download = 0, total = 0 } = extra ?? {} - const from = parseUrl(itemData.url) + const from = parseUrl(itemData.url!) const description = itemData.desc const expire = parseExpire(extra?.expire) const progress = Math.min( @@ -253,7 +253,7 @@ const ProfileItemBase = (props: ProfileItemProps) => { 100, ) - const loading = loadingCache.has(itemData.uid) + const loading = loadingCache.has(itemData.uid!) const [, forceRefresh] = useReducer((value: number) => value + 1, 0) useEffect(() => { @@ -263,7 +263,7 @@ const ProfileItemBase = (props: ProfileItemProps) => { const handler = () => { const now = Date.now() - const lastUpdate = updated * 1000 + const lastUpdate = updated! * 1000 if (now - lastUpdate >= 24 * 36e5) return const wait = now - lastUpdate >= 36e5 ? 30e5 : 5e4 @@ -293,7 +293,7 @@ const ProfileItemBase = (props: ProfileItemProps) => { const [confirmOpen, setConfirmOpen] = useState(false) const [qrOpen, setQrOpen] = useState(false) - const loadProfileDocument = useCallback(() => readProfileFile(uid), [uid]) + const loadProfileDocument = useCallback(() => readProfileFile(uid!), [uid]) const loadMergeDocument = useCallback( () => readProfileFile(option?.merge ?? ''), [option?.merge], @@ -370,7 +370,7 @@ const ProfileItemBase = (props: ProfileItemProps) => { const onOpenFile = useLockFn(async () => { setAnchorEl(null) try { - await mutate(() => viewProfile(itemData.uid), { + await mutate(() => viewProfile(itemData.uid!), { id: 'view-profile', errorNotice: false, }) @@ -402,7 +402,7 @@ const ProfileItemBase = (props: ProfileItemProps) => { try { const payload = Object.keys(option).length > 0 ? option : undefined - await mutate(() => updateProfile(itemData.uid, payload), { + await mutate(() => updateProfile(itemData.uid!, payload), { id: `update-profile:${itemData.uid}`, errorNotice: false, }) @@ -593,7 +593,7 @@ const ProfileItemBase = (props: ProfileItemProps) => { const handleSaveProfileDocument = useLockFn(async () => { const currentValue = profileDocument.value - const result = await mutate(() => saveProfileFile(uid, currentValue), { + const result = await mutate(() => saveProfileFile(uid!, currentValue), { id: `save-profile-file:${uid}`, errorNotice: false, }) @@ -728,7 +728,7 @@ const ProfileItemBase = (props: ProfileItemProps) => { variant="h6" component="h2" noWrap - title={name} + title={name!} > {name} @@ -794,7 +794,7 @@ const ProfileItemBase = (props: ProfileItemProps) => { title={ showNextUpdate ? t('profiles.components.profileItem.tooltips.showLast') - : `${t('shared.labels.updateTime')}: ${parseExpire(updated)}\n${t('profiles.components.profileItem.tooltips.showNext')}` + : `${t('shared.labels.updateTime')}: ${parseExpire(updated!)}\n${t('profiles.components.profileItem.tooltips.showNext')}` } sx={{ fontSize: 14, @@ -812,8 +812,8 @@ const ProfileItemBase = (props: ProfileItemProps) => { > {showNextUpdate ? nextUpdateTime - : updated > 0 - ? dayjs(updated * 1000).fromNow() + : updated! > 0 + ? dayjs(updated! * 1000).fromNow() : ''} @@ -831,7 +831,7 @@ const ProfileItemBase = (props: ProfileItemProps) => { ) : ( - {parseExpire(updated)} + {parseExpire(updated!)} )} @@ -896,7 +896,7 @@ const ProfileItemBase = (props: ProfileItemProps) => { { )} {proxiesOpen && ( { { {qrOpen && itemData.url && ( setQrOpen(false)} /> )} diff --git a/src/components/profile/profile-viewer.tsx b/src/components/profile/profile-viewer.tsx index e40b9f4d5..e8344960a 100644 --- a/src/components/profile/profile-viewer.tsx +++ b/src/components/profile/profile-viewer.tsx @@ -139,7 +139,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) { t('profiles.modals.profileForm.errors.uidMissing'), ) } - await mutate(() => patchProfile(form.uid, item), { + await mutate(() => patchProfile(form.uid!, item), { id: 'patch-profile', errorNotice: false, }) @@ -157,7 +157,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) { t('profiles.modals.profileForm.errors.uidMissing'), ) } - await mutate(() => patchProfile(form.uid, item), { + await mutate(() => patchProfile(form.uid!, item), { id: 'patch-profile', errorNotice: false, }) @@ -190,13 +190,13 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) { t('profiles.modals.profileForm.errors.uidMissing'), ) } - await mutate(() => patchProfile(form.uid, retryItem), { + await mutate(() => patchProfile(form.uid!, retryItem), { id: 'patch-profile', errorNotice: false, }) await mutate( - () => patchProfile(form.uid, { option: originalOptions }), + () => patchProfile(form.uid!, { option: originalOptions }), { id: 'patch-profile', errorNotice: false, @@ -413,7 +413,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) { {t('profiles.modals.profileForm.fields.useSystemProxy')} - + )} /> @@ -426,7 +426,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) { {t('profiles.modals.profileForm.fields.useClashProxy')} - + )} /> @@ -439,7 +439,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) { {t('profiles.modals.profileForm.fields.acceptInvalidCerts')} - + )} /> diff --git a/src/components/proxy/proxy-empty-state-model.ts b/src/components/proxy/proxy-empty-state-model.ts index a3f731dd1..17c6817db 100644 --- a/src/components/proxy/proxy-empty-state-model.ts +++ b/src/components/proxy/proxy-empty-state-model.ts @@ -5,11 +5,11 @@ export type ProxyEmptyStateReason = | 'no-proxy-info' type ProfileSummary = { - current?: string + current?: string | null items?: Array<{ - uid?: string - type?: string - }> + uid?: string | null + type?: string | null + }> | null } interface ProxyListStateInput { @@ -20,7 +20,7 @@ interface ProxyListStateInput { isRunningModePending: boolean } -const isSubscriptionProfile = (item: { type?: string }): boolean => +const isSubscriptionProfile = (item: { type?: string | null }): boolean => item.type === 'remote' || item.type === 'local' /** Decides only pre-render states; the built list owns whether it has renderable content. */ diff --git a/src/components/proxy/proxy-render.tsx b/src/components/proxy/proxy-render.tsx index f30479127..5881295a3 100644 --- a/src/components/proxy/proxy-render.tsx +++ b/src/components/proxy/proxy-render.tsx @@ -198,7 +198,7 @@ export const ProxyRender = memo(function ProxyRender(props: RenderProps) { {toolsOnLeft && proxyCount} onLocation(group)} @@ -247,7 +247,7 @@ export const ProxyRender = memo(function ProxyRender(props: RenderProps) { return ( onLocation(group)} diff --git a/src/components/proxy/use-render-list.ts b/src/components/proxy/use-render-list.ts index a47f44c92..2e215bdf4 100644 --- a/src/components/proxy/use-render-list.ts +++ b/src/components/proxy/use-render-list.ts @@ -237,7 +237,7 @@ export const useRenderList = ( selected?.name ?? CHAIN_DELAY_GROUP, '', 0, - latencyTimeout, + latencyTimeout!, ) if (col > 1) { return groupOccurrences(occurrences, col).map((memberCol) => ({ @@ -289,8 +289,8 @@ export const useRenderList = ( key: group.name, group, headState, - icon: group.icon, - testUrl: group.testUrl, + icon: group.icon!, + testUrl: group.testUrl!, }, ] @@ -300,7 +300,7 @@ export const useRenderList = ( group.name, headState.filterText, headState.sortType, - latencyTimeout, + latencyTimeout!, { matchCase: headState.filterMatchCase, matchWholeWord: headState.filterMatchWholeWord, @@ -337,11 +337,11 @@ export const useRenderList = ( } cache.set(group.name, { - now: group.now, + now: group.now!, members: group.members, headState, col, - latencyTimeout, + latencyTimeout: latencyTimeout!, delays: groupDelays.get(group.name), items: ret, }) diff --git a/src/components/setting/mods/backup-config-viewer.tsx b/src/components/setting/mods/backup-config-viewer.tsx index 0dbce6954..dd4972628 100644 --- a/src/components/setting/mods/backup-config-viewer.tsx +++ b/src/components/setting/mods/backup-config-viewer.tsx @@ -50,9 +50,9 @@ export const BackupConfigViewer = memo( const { register, handleSubmit, watch } = useForm({ defaultValues: { - url: webdav_url, - username: webdav_username, - password: webdav_password, + url: webdav_url!, + username: webdav_username!, + password: webdav_password!, }, }) const url = watch('url') diff --git a/src/components/setting/mods/dns-viewer.tsx b/src/components/setting/mods/dns-viewer.tsx index 15493da24..1ab82ba7f 100644 --- a/src/components/setting/mods/dns-viewer.tsx +++ b/src/components/setting/mods/dns-viewer.tsx @@ -196,7 +196,7 @@ export function DnsViewer({ ref }: { ref?: Ref }) { const { verge } = useVerge() const { current: currentProfile } = useProfiles() const dnsEnabled = currentProfile - ? (verge?.profile_dns_settings?.[currentProfile.uid]?.enabled ?? + ? (verge?.profile_dns_settings?.[currentProfile.uid!]?.enabled ?? verge?.enable_dns_settings ?? false) : false diff --git a/src/components/setting/mods/sysproxy-viewer.tsx b/src/components/setting/mods/sysproxy-viewer.tsx index e96a96bc4..84f789ba0 100644 --- a/src/components/setting/mods/sysproxy-viewer.tsx +++ b/src/components/setting/mods/sysproxy-viewer.tsx @@ -438,7 +438,7 @@ export const SysproxyViewer = forwardRef((props, ref) => { setValue((v) => ({ ...v, pac: e }))} /> @@ -455,7 +455,7 @@ export const SysproxyViewer = forwardRef((props, ref) => { setValue((v) => ({ ...v, guard: e }))} sx={{ marginLeft: 'auto' }} /> diff --git a/src/components/setting/mods/update-viewer.tsx b/src/components/setting/mods/update-viewer.tsx index 6704f93a9..d179f737f 100644 --- a/src/components/setting/mods/update-viewer.tsx +++ b/src/components/setting/mods/update-viewer.tsx @@ -1,5 +1,4 @@ import { alpha, Box, Button, LinearProgress } from '@mui/material' -import type { DownloadEvent } from '@tauri-apps/plugin-updater' import { useLockFn } from 'ahooks' import type { Ref } from 'react' import { @@ -15,6 +14,7 @@ import type { Options as ReactMarkdownOptions } from 'react-markdown' import { BaseDialog, DialogRef } from '@/components/base' import { useUpdate } from '@/hooks/use-update' +import type { DownloadEvent } from '@/services/bindings' import { cancelUpdateDownload, installUpdate, diff --git a/src/components/setting/setting-clash.tsx b/src/components/setting/setting-clash.tsx index c4ab0f458..048aa97da 100644 --- a/src/components/setting/setting-clash.tsx +++ b/src/components/setting/setting-clash.tsx @@ -40,7 +40,7 @@ const SettingClash = ({ onError }: Props) => { const { verge, mutateVerge } = useVerge() const { current: currentProfile } = useProfiles() const dnsEnabled = currentProfile - ? (verge?.profile_dns_settings?.[currentProfile.uid]?.enabled ?? + ? (verge?.profile_dns_settings?.[currentProfile.uid!]?.enabled ?? verge?.enable_dns_settings ?? false) : false diff --git a/src/components/test/test-item.tsx b/src/components/test/test-item.tsx index 27e11dcbf..94760d0ad 100644 --- a/src/components/test/test-item.tsx +++ b/src/components/test/test-item.tsx @@ -25,11 +25,11 @@ export const TestItem = ({ itemData, onEdit, onDelete: removeTest }: Props) => { const [position, setPosition] = useState({ left: 0, top: 0 }) const [delay, setDelay] = useState(-1) const { uid, name, icon, url } = itemData - const iconCachePath = useIconCache({ icon, cacheKey: uid }) + const iconCachePath = useIconCache({ icon: icon!, cacheKey: uid! }) const onDelay = useCallback(async () => { setDelay(-2) - const result = await cmdTestDelay(url) + const result = await cmdTestDelay(url!) setDelay(result) }, [url]) @@ -41,7 +41,7 @@ export const TestItem = ({ itemData, onEdit, onDelete: removeTest }: Props) => { const onDelete = useLockFn(async () => { setAnchorEl(null) try { - removeTest(uid) + removeTest(uid!) } catch (err: any) { showNotice.error(err) } @@ -81,17 +81,17 @@ export const TestItem = ({ itemData, onEdit, onDelete: removeTest }: Props) => { {icon.trim().startsWith('http') && ( {name} )} {icon.trim().startsWith('data') && ( - {name} + {name!} )} {icon.trim().startsWith(' diff --git a/src/hooks/use-proxy-selection.ts b/src/hooks/use-proxy-selection.ts index e3a1d40e6..3b70daa82 100644 --- a/src/hooks/use-proxy-selection.ts +++ b/src/hooks/use-proxy-selection.ts @@ -166,10 +166,10 @@ export const useProxySelection = (options: ProxySelectionOptions = {}) => { const handleProxyGroupChange = useCallback( ( - group: { name: string; now?: string; fixed?: string }, + group: { name: string; now?: string | null; fixed?: string | null }, proxy: { name: string }, ) => { - changeProxy(group.name, proxy.name, group.now, group.fixed) + changeProxy(group.name, proxy.name, group.now!, group.fixed!) }, [changeProxy], ) diff --git a/src/pages/profiles.tsx b/src/pages/profiles.tsx index 85d7d78b2..6599b2a57 100644 --- a/src/pages/profiles.tsx +++ b/src/pages/profiles.tsx @@ -117,7 +117,7 @@ const ProfilePage = () => { } = useProfiles() useEffect(() => { - currentProfileRef.current = profiles.current + currentProfileRef.current = profiles.current! }, [profiles]) useEffect(() => { @@ -574,7 +574,7 @@ const ProfilePage = () => { const onUpdateAll = useLockFn(async () => { const items = profileItems.filter((e) => e.type === 'remote') const target = items - .map((item) => item.uid) + .map((item) => item.uid!) .filter((uid) => !loadingCache.has(uid)) setLoadingProfiles(target, true) @@ -606,7 +606,7 @@ const ProfilePage = () => { } const selectAllProfiles = () => { - setSelectedProfiles(new Set(profileItems.map((item) => item.uid))) + setSelectedProfiles(new Set(profileItems.map((item) => item.uid!))) } const clearAllSelections = () => { @@ -896,20 +896,20 @@ const ProfilePage = () => { {profileItems.map((item, index) => ( onSelect(item.uid, f)} + onSelect={(f) => onSelect(item.uid!, f)} onEdit={() => viewerRef.current?.edit(item)} onSave={async (prev, curr) => { if (prev !== curr && profiles.current === item.uid) { @@ -918,14 +918,14 @@ const ProfilePage = () => { }} onDelete={() => { if (batchMode) { - toggleProfileSelection(item.uid) + toggleProfileSelection(item.uid!) } else { - onDelete(item.uid) + onDelete(item.uid!) } }} batchMode={batchMode} - isSelected={selectedProfiles.has(item.uid)} - onSelectionChange={() => toggleProfileSelection(item.uid)} + isSelected={selectedProfiles.has(item.uid!)} + onSelectionChange={() => toggleProfileSelection(item.uid!)} /> ))} diff --git a/src/services/bindings.ts b/src/services/bindings.ts new file mode 100644 index 000000000..c2a7d19d6 --- /dev/null +++ b/src/services/bindings.ts @@ -0,0 +1,1016 @@ +// This file has been generated by Tauri Specta. Do not edit this file manually. + +import { invoke as __TAURI_INVOKE, Channel } from '@tauri-apps/api/core' + +/** Commands */ +export const commands = { + getSystemInfo: () => __TAURI_INVOKE('get_system_info'), + /** 获取应用的运行时间(毫秒) */ + getAppUptime: () => __TAURI_INVOKE('get_app_uptime'), + exportDiagnosticInfo: () => __TAURI_INVOKE('export_diagnostic_info'), + probeListener: (request: ListenerProbe) => + __TAURI_INVOKE('probe_listener', { request }), + saveProxyPorts: (settings: ProxyPortSettings) => + __TAURI_INVOKE('save_proxy_ports', { settings }), + getSysProxy: () => __TAURI_INVOKE('get_sys_proxy'), + getAutoProxy: () => __TAURI_INVOKE('get_auto_proxy'), + getEmbeddedServerPort: () => + __TAURI_INVOKE('get_embedded_server_port'), + openAppDir: () => __TAURI_INVOKE('open_app_dir'), + openLogsDir: () => __TAURI_INVOKE('open_logs_dir'), + openCoreDir: () => __TAURI_INVOKE('open_core_dir'), + getNetworkInterfaces: () => + __TAURI_INVOKE('get_network_interfaces'), + getSystemHostname: () => __TAURI_INVOKE('get_system_hostname'), + restartApp: () => __TAURI_INVOKE('restart_app'), + installUpdate: (version: string, onEvent: Channel) => + __TAURI_INVOKE('install_update', { version, onEvent }), + cancelUpdateDownload: () => __TAURI_INVOKE('cancel_update_download'), + restartCore: () => __TAURI_INVOKE('restart_core'), + /** Replaces the managed core binary atomically instead of letting mihomo overwrite itself. */ + upgradeClashCore: (force: boolean) => + __TAURI_INVOKE('upgrade_clash_core', { force }), + /** Returns one coherent core/service snapshot instead of independently refreshed state. */ + getRuntimeState: () => __TAURI_INVOKE('get_runtime_state'), + getPendingFailures: () => + __TAURI_INVOKE('get_pending_failures'), + getSidecarFailure: () => + __TAURI_INVOKE('get_sidecar_failure'), + entryLightweightMode: () => __TAURI_INVOKE('entry_lightweight_mode'), + installService: () => + __TAURI_INVOKE('install_service'), + uninstallService: () => __TAURI_INVOKE('uninstall_service'), + reinstallService: () => + __TAURI_INVOKE('reinstall_service'), + repairService: () => __TAURI_INVOKE('repair_service'), + continueWithSidecar: () => __TAURI_INVOKE('continue_with_sidecar'), + syncRuntimeProviders: () => __TAURI_INVOKE('sync_runtime_providers'), + getClashInfo: () => __TAURI_INVOKE('get_clash_info'), + patchClashConfig: (payload: YamlMapping) => + __TAURI_INVOKE('patch_clash_config', { payload }), + patchClashMode: (payload: string) => + __TAURI_INVOKE('patch_clash_mode', { payload }), + /** Reads the saved mode without depending on strict mihomo `/configs` deserialization. */ + getClashMode: () => __TAURI_INVOKE('get_clash_mode'), + changeClashCore: (clashCore: string) => + __TAURI_INVOKE<{ + /** Stable frontend identifier, when classified. */ + code?: string | null + /** Human-readable diagnostic detail. */ + detail: string + /** Requested operation when known by the command. */ + operation?: FailedOperation | null + } | null>('change_clash_core', { clashCore }), + getRuntimeConfig: () => + __TAURI_INVOKE<{ [key in string]: JsonValue } | null>('get_runtime_config'), + getProxyView: () => __TAURI_INVOKE('get_proxy_view'), + getRuntimeYaml: () => __TAURI_INVOKE('get_runtime_yaml'), + getRuntimeLogs: () => __TAURI_INVOKE('get_runtime_logs'), + getRuntimeProxyChainConfig: (proxyChainExitNode: string) => + __TAURI_INVOKE('get_runtime_proxy_chain_config', { + proxyChainExitNode, + }), + updateProxyChainConfigInRuntime: (proxyChainConfig: JsonValue | null) => + __TAURI_INVOKE('update_proxy_chain_config_in_runtime', { + proxyChainConfig, + }), + invokeUwpTool: () => __TAURI_INVOKE('invoke_uwp_tool'), + copyClashEnv: () => __TAURI_INVOKE('copy_clash_env'), + syncTrayProxySelection: () => + __TAURI_INVOKE('sync_tray_proxy_selection'), + /** Merges one selection pair into fresh backend state to avoid stale-list overwrites. */ + recordSelectedNode: (groupName: string, node: string) => + __TAURI_INVOKE('record_selected_node', { groupName, node }), + forgetSelectedNode: (groupName: string) => + __TAURI_INVOKE('forget_selected_node', { groupName }), + saveDnsConfig: (dnsConfig: YamlMapping) => + __TAURI_INVOKE('save_dns_config', { dnsConfig }), + applyDnsConfig: (apply: boolean) => + __TAURI_INVOKE('apply_dns_config', { apply }), + setDnsOverride: ( + profileUid: string, + enabled: boolean, + confirmation: string | null, + ) => + __TAURI_INVOKE('set_dns_override', { + profileUid, + enabled, + confirmation, + }), + takeDnsOverrideNotice: () => + __TAURI_INVOKE('take_dns_override_notice'), + takeServiceFallbackNotice: () => + __TAURI_INVOKE< + | { kind: 'unavailable' } + | { kind: 'coreRejected'; reason: string } + | { kind: 'notAutoStarted' } + | null + >('take_service_fallback_notice'), + getCoreStartupError: () => + __TAURI_INVOKE< + | { kind: 'startFailed'; detail: string } + | { kind: 'serviceCoreStopped'; detail: string } + | null + >('get_core_startup_error'), + takeServiceRepairNotice: () => + __TAURI_INVOKE('take_service_repair_notice'), + takeServiceOwnerNotice: () => + __TAURI_INVOKE('take_service_owner_notice'), + takeDiscardedKeysNotice: () => + __TAURI_INVOKE('take_discarded_keys_notice'), + getDnsConfigContent: () => + __TAURI_INVOKE('get_dns_config_content'), + validateDnsConfig: () => + __TAURI_INVOKE('validate_dns_config'), + getClashLogs: () => __TAURI_INVOKE('get_clash_logs'), + /** 获取Verge配置 */ + getVergeConfig: () => __TAURI_INVOKE('get_verge_config'), + /** 修改Verge配置 */ + patchVergeConfig: (payload: IVerge_Deserialize) => + __TAURI_INVOKE('patch_verge_config', { payload }), + testDelay: (url: string) => __TAURI_INVOKE('test_delay', { url }), + getAppDir: () => __TAURI_INVOKE('get_app_dir'), + copyIconFile: (path: string, iconInfo: IconInfo) => + __TAURI_INVOKE('copy_icon_file', { path, iconInfo }), + downloadIconCache: (url: string, name: string) => + __TAURI_INVOKE('download_icon_cache', { url, name }), + openDevtools: () => __TAURI_INVOKE('open_devtools'), + exitApp: () => __TAURI_INVOKE('exit_app'), + getNetworkInterfacesInfo: () => + __TAURI_INVOKE('get_network_interfaces_info'), + getProfiles: () => __TAURI_INVOKE('get_profiles'), + enhanceProfiles: () => __TAURI_INVOKE('enhance_profiles'), + patchProfilesConfig: (profiles: IProfiles_Deserialize) => + __TAURI_INVOKE('patch_profiles_config', { profiles }), + viewProfile: (index: string) => + __TAURI_INVOKE('view_profile', { index }), + patchProfile: (index: string, profile: PrfItem_Deserialize) => + __TAURI_INVOKE('patch_profile', { index, profile }), + createProfile: (item: PrfItem_Deserialize, fileData: string | null) => + __TAURI_INVOKE('create_profile', { item, fileData }), + importProfile: ( + url: string, + option: { + user_agent?: string | null + with_proxy?: boolean | null + self_proxy?: boolean | null + update_interval?: number | null + /** + * HTTP request timeout in seconds + * default is 60 seconds + */ + timeout_seconds?: number | null + /** default is `false` */ + danger_accept_invalid_certs?: boolean | null + allow_auto_update?: boolean | null + merge?: string | null + script?: string | null + rules?: string | null + proxies?: string | null + groups?: string | null + } | null, + ) => __TAURI_INVOKE('import_profile', { url, option }), + reorderProfile: (activeId: string, overId: string) => + __TAURI_INVOKE('reorder_profile', { activeId, overId }), + updateProfile: ( + index: string, + option: { + user_agent?: string | null + with_proxy?: boolean | null + self_proxy?: boolean | null + update_interval?: number | null + /** + * HTTP request timeout in seconds + * default is 60 seconds + */ + timeout_seconds?: number | null + /** default is `false` */ + danger_accept_invalid_certs?: boolean | null + allow_auto_update?: boolean | null + merge?: string | null + script?: string | null + rules?: string | null + proxies?: string | null + groups?: string | null + } | null, + ) => __TAURI_INVOKE('update_profile', { index, option }), + deleteProfile: (index: string) => + __TAURI_INVOKE('delete_profile', { index }), + readProfileFile: (index: string) => + __TAURI_INVOKE('read_profile_file', { index }), + /** 保存profiles的配置 */ + saveProfileFile: (index: string, fileData: string | null) => + __TAURI_INVOKE('save_profile_file', { index, fileData }), + getNextUpdateTime: (uid: string) => + __TAURI_INVOKE('get_next_update_time', { uid }), + createLocalBackup: () => __TAURI_INVOKE('create_local_backup'), + listLocalBackup: () => __TAURI_INVOKE('list_local_backup'), + deleteLocalBackup: (filename: string) => + __TAURI_INVOKE('delete_local_backup', { filename }), + restoreLocalBackup: (filename: string) => + __TAURI_INVOKE('restore_local_backup', { filename }), + importLocalBackup: (source: string) => + __TAURI_INVOKE('import_local_backup', { source }), + exportLocalBackup: (filename: string, destination: string) => + __TAURI_INVOKE('export_local_backup', { filename, destination }), + createWebdavBackup: () => __TAURI_INVOKE('create_webdav_backup'), + saveWebdavConfig: (url: string, username: string, password: string) => + __TAURI_INVOKE('save_webdav_config', { url, username, password }), + listWebdavBackup: () => __TAURI_INVOKE('list_webdav_backup'), + deleteWebdavBackup: (filename: string) => + __TAURI_INVOKE('delete_webdav_backup', { filename }), + restoreWebdavBackup: (filename: string) => + __TAURI_INVOKE('restore_webdav_backup', { filename }), + getUnlockItems: () => __TAURI_INVOKE('get_unlock_items'), + checkMediaUnlock: (onComplete: Channel) => + __TAURI_INVOKE('check_media_unlock', { onComplete }), + checkMediaUnlockItem: (name: string) => + __TAURI_INVOKE('check_media_unlock_item', { name }), +} + +/* Types */ +export type AutoProxy = { + enable: boolean + url: string +} + +export type ClashInfo = { + /** clash core port */ + mixed_port: number + socks_port: number + port: number + /** same as `external-controller` */ + server: string + /** clash secret */ + secret: string | null +} + +/** Structured command failure returned to the frontend. */ +export type CommandFailure = + | CommandFailure_Serialize + | CommandFailure_Deserialize + +/** Structured command failure returned to the frontend. */ +export type CommandFailure_Deserialize = { + /** Stable frontend identifier, when classified. */ + code: string | null + /** Human-readable diagnostic detail. */ + detail: string + /** Requested operation when known by the command. */ + operation: FailedOperation | null +} + +/** Structured command failure returned to the frontend. */ +export type CommandFailure_Serialize = { + /** Stable frontend identifier, when classified. */ + code?: string | null + /** Human-readable diagnostic detail. */ + detail: string + /** Requested operation when known by the command. */ + operation?: FailedOperation | null +} + +/** Why the Core is not running, kept until a Core starts again. */ +export type CoreFailure = + | { kind: 'startFailed'; detail: string } + | { kind: 'serviceCoreStopped'; detail: string } + +export type CoreUpgradeReport = { + /** False when the managed core was already at the latest version. */ + upgraded: boolean + from: string + to: string +} + +export type DelayHistory = { + time: string + delay: number +} + +export type DnsOverrideOutcome = + | { status: 'applied' } + | { status: 'confirmation_required'; source: string } + +/** Same wire shape as the updater plugin's JS `DownloadEvent`. */ +export type DownloadEvent = + | { + event: 'Started' + data: { + contentLength: number | null + } + } + | { + event: 'Progress' + data: { + chunkLength: number + } + } + | { event: 'Finished' } + +/** Operation associated with a pending failure. */ +export type FailedOperation = + | 'systemProxyEnable' + | 'systemProxyDisable' + | 'systemProxyRestore' + | 'systemProxyGuard' + +/** Define the `profiles.yaml` schema */ +export type IProfiles = IProfiles_Serialize | IProfiles_Deserialize + +/** Define the `profiles.yaml` schema */ +export type IProfiles_Deserialize = { + current?: string | null + items?: PrfItem_Deserialize[] | null +} + +/** Define the `profiles.yaml` schema */ +export type IProfiles_Serialize = { + current: string | null + items: PrfItem_Serialize[] | null +} + +/** ### `verge.yaml` schema */ +export type IVerge = IVerge_Serialize | IVerge_Deserialize + +export type IVergeTestItem = { + uid?: string | null + name?: string | null + icon?: string | null + url?: string | null +} + +export type IVergeTheme = { + primary_color?: string | null + secondary_color?: string | null + primary_text?: string | null + secondary_text?: string | null + info_color?: string | null + error_color?: string | null + warning_color?: string | null + success_color?: string | null + font_family?: string | null + css_injection?: string | null +} + +/** ### `verge.yaml` schema */ +export type IVerge_Deserialize = { + /** silent | error | warn | info | debug | trace */ + app_log_level?: string | null + /** Maximum app log size in KiB. */ + app_log_max_size?: number | null + app_log_max_count?: number | null + language?: string | null + /** `light` or `dark` or `system` */ + theme_mode?: string | null + tray_event?: string | null + env_type?: string | null + start_page?: string | null + startup_script?: string | null + traffic_graph?: boolean | null + /** show memory info (only for Clash Meta) */ + enable_memory_usage?: boolean | null + enable_group_icon?: boolean | null + pause_render_traffic_stats_on_blur?: boolean | null + common_tray_icon?: boolean | null + menu_icon?: string | null + menu_order?: string[] | null + proxy_group_tools_position?: string | null + notice_position?: string | null + collapse_navbar?: boolean | null + sysproxy_tray_icon?: boolean | null + tun_tray_icon?: boolean | null + enable_tun_mode?: boolean | null + enable_auto_launch?: boolean | null + enable_silent_start?: boolean | null + enable_system_proxy?: boolean | null + enable_proxy_guard?: boolean | null + enable_bypass_check?: boolean | null + /** Initial DNS override preference for profiles without a saved setting. */ + enable_dns_settings?: boolean | null + profile_dns_settings?: { [key in string]: ProfileDnsSettings_Deserialize } + use_default_bypass?: boolean | null + system_proxy_bypass?: string | null + proxy_guard_duration?: number | null + proxy_auto_config?: boolean | null + pac_file_content?: string | null + proxy_host?: string | null + theme_setting?: IVergeTheme | null + web_ui_list?: string[] | null + clash_core?: string | null + /** format: {func},{key} */ + hotkeys?: string[] | null + enable_global_hotkey?: boolean | null + home_cards?: JsonValue | null + auto_close_connection?: boolean | null + auto_check_update?: boolean | null + default_latency_test?: string | null + default_latency_timeout?: number | null + enable_auto_delay_detection?: boolean | null + /** 自动检测当前节点延迟的间隔(分钟) */ + auto_delay_detection_interval_minutes?: number | null + enable_builtin_enhanced?: boolean | null + proxy_layout_column?: number | null + test_list?: IVergeTestItem[] | null + /** 0: 不清理; 1: 1天;2: 7天; 3: 30天; 4: 90天 */ + auto_log_clean?: number | null + enable_auto_backup_schedule?: boolean | null + /** Automatic backup interval in hours */ + auto_backup_interval_hours?: number | null + auto_backup_on_change?: boolean | null + verge_mixed_port?: number | null + verge_socks_port?: number | null + verge_socks_enabled?: boolean | null + verge_port?: number | null + verge_http_enabled?: boolean | null + webdav_url?: string | null + webdav_username?: string | null + webdav_password?: string | null + tray_proxy_groups_display_mode?: string | null + tray_inline_outbound_modes?: boolean | null + enable_auto_light_weight_mode?: boolean | null + /** 自动进入轻量模式的延迟(分钟) */ + auto_light_weight_minutes?: number | null + enable_hover_jump_navigator?: boolean | null + /** 代理页面自动滚动延迟(毫秒) */ + hover_jump_navigator_delay?: number | null + enable_external_controller?: boolean | null + tray_icon?: string | null + verge_redir_port?: number | null + verge_redir_enabled?: boolean | null + verge_tproxy_port?: number | null + verge_tproxy_enabled?: boolean | null + enable_tray_speed?: boolean | null +} + +/** ### `verge.yaml` schema */ +export type IVerge_Serialize = { + /** silent | error | warn | info | debug | trace */ + app_log_level: string | null + /** Maximum app log size in KiB. */ + app_log_max_size: number | null + app_log_max_count: number | null + language: string | null + /** `light` or `dark` or `system` */ + theme_mode: string | null + tray_event: string | null + env_type: string | null + start_page: string | null + startup_script: string | null + traffic_graph: boolean | null + /** show memory info (only for Clash Meta) */ + enable_memory_usage: boolean | null + enable_group_icon?: boolean | null + pause_render_traffic_stats_on_blur: boolean | null + common_tray_icon?: boolean | null + menu_icon?: string | null + menu_order?: string[] | null + proxy_group_tools_position?: string | null + notice_position?: string | null + collapse_navbar: boolean | null + sysproxy_tray_icon: boolean | null + tun_tray_icon: boolean | null + enable_tun_mode: boolean | null + enable_auto_launch: boolean | null + enable_silent_start: boolean | null + enable_system_proxy: boolean | null + enable_proxy_guard: boolean | null + enable_bypass_check: boolean | null + /** Initial DNS override preference for profiles without a saved setting. */ + enable_dns_settings: boolean | null + profile_dns_settings?: { [key in string]: ProfileDnsSettings_Serialize } + use_default_bypass: boolean | null + system_proxy_bypass: string | null + proxy_guard_duration: number | null + proxy_auto_config: boolean | null + pac_file_content: string | null + proxy_host: string | null + theme_setting: IVergeTheme | null + web_ui_list: string[] | null + clash_core?: string | null + /** format: {func},{key} */ + hotkeys?: string[] | null + enable_global_hotkey: boolean | null + home_cards: JsonValue | null + auto_close_connection: boolean | null + auto_check_update: boolean | null + default_latency_test: string | null + default_latency_timeout: number | null + enable_auto_delay_detection: boolean | null + /** 自动检测当前节点延迟的间隔(分钟) */ + auto_delay_detection_interval_minutes: number | null + enable_builtin_enhanced: boolean | null + proxy_layout_column: number | null + test_list: IVergeTestItem[] | null + /** 0: 不清理; 1: 1天;2: 7天; 3: 30天; 4: 90天 */ + auto_log_clean: number | null + enable_auto_backup_schedule: boolean | null + /** Automatic backup interval in hours */ + auto_backup_interval_hours: number | null + auto_backup_on_change: boolean | null + verge_mixed_port: number | null + verge_socks_port: number | null + verge_socks_enabled: boolean | null + verge_port: number | null + verge_http_enabled: boolean | null + webdav_url?: string | null + webdav_username?: string | null + webdav_password?: string | null + tray_proxy_groups_display_mode?: string | null + tray_inline_outbound_modes: boolean | null + enable_auto_light_weight_mode: boolean | null + /** 自动进入轻量模式的延迟(分钟) */ + auto_light_weight_minutes: number | null + enable_hover_jump_navigator: boolean | null + /** 代理页面自动滚动延迟(毫秒) */ + hover_jump_navigator_delay: number | null + enable_external_controller: boolean | null + tray_icon?: string | null + verge_redir_port?: number | null + verge_redir_enabled?: boolean | null + verge_tproxy_port?: number | null + verge_tproxy_enabled?: boolean | null + enable_tray_speed?: boolean | null +} + +export type IconInfo = { + name: string + previous_t: string + current_t: string +} + +export type InterfaceAddress = + | ({ V4: IpAddress } & { V6?: never }) + | ({ V6: IpAddress } & { V4?: never }) + +export type IpAddress = { + ip: string + broadcast: string | null + netmask: string | null +} + +export type JsonValue = + | null + | boolean + | number + | null + | string + | JsonValue[] + | { [key in string]: JsonValue } + +export type ListenerProbe = { + address: string + transports: ListenerTransport[] +} + +export type ListenerProbeOutcome = + | { status: 'available' } + | { status: 'conflict'; port: number; transport: ListenerTransport } + | { status: 'invalid'; message: string } + | { status: 'indeterminate'; message: string } + +export type ListenerTransport = 'tcp' | 'udp' + +export type LocalBackupFile = { + filename: string + path: string + last_modified: string + content_length: number +} + +export type NetworkInterfaceView = { + name: string + addr: InterfaceAddress[] + mac_addr: string | null + index: number + internal: boolean +} + +export type NoticeStatus = + | 'info' + | 'import_sub_url::ok' + | 'import_sub_url::error' + | 'set_config::error' + | 'core_start::error' + | 'service_core::repair_required' + | 'service_core::app_data_not_owned' + | 'service_core::sidecar_fallback' + | 'dns_override::auto_disabled' + | 'enhance::discarded_keys' + | 'tun_mode::auto_disabled' + | 'tun_mode::auto_disable_failed' + | 'app_restart::core_stop_failed' + | 'app_quit::core_stop_failed' + | 'update_with_clash_proxy' + | 'update_failed_even_with_clash' + | 'reactivate_profiles::error' + | 'update_failed' + | 'config_validate::boot_error' + | 'config_validate::error' + | 'config_validate::process_terminated' + | 'config_validate::script_error' + | 'config_validate::script_syntax_error' + | 'config_validate::script_missing_main' + | 'config_validate::file_not_found' + | 'config_validate::yaml_syntax_error' + | 'config_validate::yaml_read_error' + | 'config_validate::yaml_mapping_error' + | 'config_validate::merge_syntax_error' + | 'config_validate::merge_mapping_error' + | 'config_core::change_success' + | 'config_core::change_error' + | 'mixed_port::fallback' + | 'mixed_port::fallback_error' + +export type PendingAction = + | 'install' + | 'uninstall' + | 'reinstall' + | 'forceReinstall' + +/** Latest unresolved failure for one stable code. */ +export type PendingFailure = { + /** Stable code used as the table key. */ + code: string + /** Full diagnostic context chain. */ + detail: string + operation: FailedOperation + /** Monotonic identity for repeated failures under the same code. */ + sequence: number +} + +export type PlatformVergeFields = { + tray_icon?: string | null + verge_redir_port?: number | null + verge_redir_enabled?: boolean | null + verge_tproxy_port?: number | null + verge_tproxy_enabled?: boolean | null + enable_tray_speed?: boolean | null +} + +export type PrfExtra = { + upload: number + download: number + total: number + expire: number +} + +export type PrfItem = PrfItem_Serialize | PrfItem_Deserialize + +export type PrfItem_Deserialize = { + uid?: string | null + /** enum value: remote | local | script | merge */ + type?: string | null + name?: string | null + file?: string | null + desc?: string | null + url?: string | null + selected?: PrfSelected[] | null + extra?: PrfExtra | null + updated?: number | null + option?: PrfOption_Deserialize | null + home?: string | null +} + +export type PrfItem_Serialize = { + uid: string | null + /** enum value: remote | local | script | merge */ + type: string | null + name: string | null + file: string | null + desc?: string | null + url?: string | null + selected?: PrfSelected[] | null + extra?: PrfExtra | null + updated: number | null + option?: PrfOption_Serialize | null + home?: string | null +} + +export type PrfOption = PrfOption_Serialize | PrfOption_Deserialize + +export type PrfOption_Deserialize = { + user_agent?: string | null + with_proxy?: boolean | null + self_proxy?: boolean | null + update_interval?: number | null + /** + * HTTP request timeout in seconds + * default is 60 seconds + */ + timeout_seconds?: number | null + /** default is `false` */ + danger_accept_invalid_certs?: boolean | null + allow_auto_update?: boolean | null + merge?: string | null + script?: string | null + rules?: string | null + proxies?: string | null + groups?: string | null +} + +export type PrfOption_Serialize = { + user_agent?: string | null + with_proxy?: boolean | null + self_proxy?: boolean | null + update_interval?: number | null + /** + * HTTP request timeout in seconds + * default is 60 seconds + */ + timeout_seconds?: number | null + /** default is `false` */ + danger_accept_invalid_certs?: boolean | null + allow_auto_update?: boolean | null + merge: string | null + script: string | null + rules: string | null + proxies: string | null + groups: string | null +} + +export type PrfSelected = { + name?: string | null + now?: string | null +} + +export type ProfileDnsSettings = + | ProfileDnsSettings_Serialize + | ProfileDnsSettings_Deserialize + +export type ProfileDnsSettings_Deserialize = { + enabled: boolean + confirmation?: string | null +} + +export type ProfileDnsSettings_Serialize = { + enabled: boolean + confirmation?: string | null +} + +export type ProfileUpdatePayload = { + uid: string +} + +export type ProxyCapabilities = { + udp: boolean + xudp: boolean + tfo: boolean + mptcp: boolean + smux: boolean +} + +export type ProxyGroupView = + | ProxyGroupView_Serialize + | ProxyGroupView_Deserialize + +export type ProxyGroupView_Deserialize = { + name: string + type: string + alive: boolean + now: string | null + fixed: string | null + hidden: boolean | null + icon: string | null + testUrl: string | null + history: DelayHistory[] + members: ProxyMemberRef[] +} & ProxyCapabilities + +export type ProxyGroupView_Serialize = { + name: string + type: string + alive: boolean + now?: string | null + fixed?: string | null + hidden?: boolean | null + icon?: string | null + testUrl?: string | null + history: DelayHistory[] + members: ProxyMemberRef[] +} & ProxyCapabilities + +export type ProxyMemberRef = + | { kind: 'group'; name: string } + | { kind: 'node'; name: string; recordId: string } + | { kind: 'unresolved'; name: string; reason: ProxyMemberUnresolvedReason } + +export type ProxyMemberUnresolvedReason = + | 'missing' + | 'ambiguous' + | 'provider-unavailable' + +export type ProxyNodeSource = + | { kind: 'core'; proxyName: string } + | { kind: 'provider'; providerName: string; proxyName: string } + +export type ProxyNodeView = ProxyNodeView_Serialize | ProxyNodeView_Deserialize + +export type ProxyNodeView_Deserialize = { + recordId: string + name: string + type: string + alive: boolean + history: DelayHistory[] + id: string | null + hidden: boolean | null + icon: string | null + testUrl: string | null + source: ProxyNodeSource +} & ProxyCapabilities + +export type ProxyNodeView_Serialize = { + recordId: string + name: string + type: string + alive: boolean + history: DelayHistory[] + id?: string | null + hidden?: boolean | null + icon?: string | null + testUrl?: string | null + source: ProxyNodeSource +} & ProxyCapabilities + +export type ProxyPortSettings = { + mixedPort: number + socks: ToggleableProxyPort + http: ToggleableProxyPort + redir: ToggleableProxyPort + tproxy: ToggleableProxyPort +} + +export type ProxyProviderVehicleType = 'HTTP' | 'File' | 'Inline' + +export type ProxyProviderView = + | ProxyProviderView_Serialize + | ProxyProviderView_Deserialize + +export type ProxyProviderView_Deserialize = { + name: string + vehicleType: ProxyProviderVehicleType + updatedAt: string | null + subscriptionInfo: ProxySubscriptionInfo | null + proxyRecordIds: string[] +} + +export type ProxyProviderView_Serialize = { + name: string + vehicleType: ProxyProviderVehicleType + updatedAt?: string | null + subscriptionInfo?: ProxySubscriptionInfo | null + proxyRecordIds: string[] +} + +export type ProxySubscriptionInfo = { + upload: number + download: number + total: number + expire: number +} + +export type ProxyViewOrderSource = 'runtime' | 'fallback' + +export type ProxyViewProviderState = 'ready' | 'unavailable' + +export type ProxyViewV1 = ProxyViewV1_Serialize | ProxyViewV1_Deserialize + +export type ProxyViewV1_Deserialize = { + schemaVersion: number + orderSource: ProxyViewOrderSource + providerState: ProxyViewProviderState + global: ProxyGroupView_Deserialize | null + direct: string | null + groups: ProxyGroupView_Deserialize[] + records: { [key in string]: ProxyNodeView_Deserialize } + standalone: string[] + providers: ProxyProviderView_Deserialize[] +} + +export type ProxyViewV1_Serialize = { + schemaVersion: number + orderSource: ProxyViewOrderSource + providerState: ProxyViewProviderState + global: ProxyGroupView_Serialize | null + direct: string | null + groups: ProxyGroupView_Serialize[] + records: { [key in string]: ProxyNodeView_Serialize } + standalone: string[] + providers: ProxyProviderView_Serialize[] +} + +export type RunStateView = { + mode: RunningMode + service: ServiceHealthKind + serviceUnavailableReason: string | null + pendingAction: PendingAction | null + sidecarAllowed: boolean + isAdmin: boolean + opInFlight: boolean + serviceUsable: boolean + tunCapable: boolean + serviceNeedsAttention: boolean +} + +export type RunningMode = 'Service' | 'Sidecar' | 'NotRunning' + +export type RuntimeLogs = { [key in string]: [string, string][] } + +export type SaveProxyPortsOutcome = + | { status: 'saved' } + | { status: 'conflict'; port: number; transport: ListenerTransport } + +/** Why the Service was unavailable when the core fell back to Sidecar. */ +export type ServiceFallbackNotice = + | { kind: 'unavailable' } + | { kind: 'coreRejected'; reason: string } + | { kind: 'notAutoStarted' } + +export type ServiceHealthKind = + | 'unknown' + | 'ready' + | 'notInstalled' + | 'versionMismatch' + | 'unavailable' + +export type ServiceInstallOutcome = + | { status: 'installed' } + | { status: 'sidecar'; reason: string } + +/** A removal has a revision too, so a delayed read cannot restore a resolved failure. */ +export type SidecarFailureSnapshot = { + revision: number + detail: string | null +} + +export type SystemInfo = { + system_name: string + system_version: string + system_kernel_version: string + system_arch: string + app_version: string + app_core_mode: string + app_is_admin: boolean +} + +export type SystemProxy = { + enable: boolean + server: string + bypass: string +} + +export type ToggleableProxyPort = { + enabled: boolean + port: number +} + +export type UnlockItem = { + name: string + status: string + region: string | null + check_time: string | null +} + +export type ValidationErrorKind = + | 'fileMissing' + | 'fileRead' + | 'yamlSyntax' + | 'yamlMapping' + | 'scriptSyntax' + | 'scriptMissingMain' + | 'coreRejected' + | 'processTerminated' + | 'timeout' + +export type ValidationOutcome = + | { status: 'valid' } + | { status: 'invalid'; kind: ValidationErrorKind; message: string } + | { status: 'skipped'; reason: ValidationSkipReason } + | { status: 'busy' } + +export type ValidationSkipReason = 'exiting' | 'debounced' + +export type VergeEventPayloads = { + 'verge://refresh-clash-config': string + 'verge://refresh-verge-config': string + 'verge://refresh-profiles': string + 'verge://refresh-proxy-config': null + 'verge://notice-message': [NoticeStatus, string] + 'profile-changed': string + 'verge://timer-updated': string + 'profile-update-started': ProfileUpdatePayload + 'profile-update-completed': ProfileUpdatePayload + 'verge://run-state-changed': RunStateView + 'verge://pending-failures-changed': null +} + +export type WebDavFile = { + href: string + last_modified: string + content_length: number + content_type: string + tag: string | null +} + +export type YamlMapping = { [key in string]: JsonValue } + +export type YamlValue = JsonValue diff --git a/src/services/cmds.ts b/src/services/cmds.ts index c5d7c1197..7a5d97dca 100644 --- a/src/services/cmds.ts +++ b/src/services/cmds.ts @@ -1,141 +1,148 @@ -import { Channel, invoke } from '@tauri-apps/api/core' -import type { DownloadEvent } from '@tauri-apps/plugin-updater' +import { Channel } from '@tauri-apps/api/core' import dayjs from 'dayjs' -import type { CommandFailure } from '@/services/notice-service' import { showNotice } from '@/services/notice-service' -import type { ProxyViewV1 } from '@/types/proxy-view' import { debugLog } from '@/utils/debug' +import { commands } from './bindings' +import type { + DownloadEvent, + JsonValue, + ListenerProbe, + ProxyPortSettings, + UnlockItem, + ProxyViewV1_Serialize as ProxyViewV1, + IProfiles_Deserialize, + PrfItem_Deserialize, +} from './bindings' + +export type { + RunningMode, + RunStateView as RunState, + FailedOperation, + PendingFailure, + SidecarFailureSnapshot, + ServiceInstallOutcome, + ProxyPortSettings, + ListenerTransport, + ListenerProbe, + UnlockItem, +} from './bindings' + export async function copyClashEnv() { - return invoke('copy_clash_env') + return commands.copyClashEnv() } -export async function getProfiles() { - return invoke('get_profiles') +export async function getProfiles(): Promise { + return commands.getProfiles() } export async function enhanceProfiles() { - return ( - (await invoke('enhance_profiles')).status === 'valid' - ) + return (await commands.enhanceProfiles()).status === 'valid' } -export async function patchProfilesConfig(profiles: IProfilesConfig) { - return invoke('patch_profiles_config', { profiles }) +export async function patchProfilesConfig(profiles: IProfiles_Deserialize) { + return commands.patchProfilesConfig(profiles) } export async function createProfile( - item: Partial, + item: PrfItem_Deserialize, fileData?: string | null, ) { - return invoke('create_profile', { item, fileData }) + return commands.createProfile(item, fileData ?? null) } export async function viewProfile(index: string) { - return invoke('view_profile', { index }) + return commands.viewProfile(index) } export async function readProfileFile(index: string) { - return invoke('read_profile_file', { index }) + return commands.readProfileFile(index) } export async function saveProfileFile(index: string, fileData: string) { - return ( - ( - await invoke('save_profile_file', { - index, - fileData, - }) - ).status === 'valid' - ) + return (await commands.saveProfileFile(index, fileData)).status === 'valid' } export async function importProfile(url: string, option?: IProfileOption) { - return invoke('import_profile', { - url, - option: option || { with_proxy: true }, - }) + return commands.importProfile(url, option || { with_proxy: true }) } export async function reorderProfile(activeId: string, overId: string) { - return invoke('reorder_profile', { - activeId, - overId, - }) + return commands.reorderProfile(activeId, overId) } export async function updateProfile(index: string, option?: IProfileOption) { - return invoke('update_profile', { index, option }) + return commands.updateProfile(index, option ?? null) } export async function deleteProfile(index: string) { - return invoke('delete_profile', { index }) + return commands.deleteProfile(index) } export async function patchProfile( index: string, - profile: Partial, + profile: PrfItem_Deserialize, ) { - return invoke('patch_profile', { index, profile }) + return commands.patchProfile(index, profile) } export async function getClashInfo() { - return invoke('get_clash_info') + return commands.getClashInfo() } // Fallback mode read independent of strict mihomo `/configs` deserialization. export async function getClashMode() { - return invoke('get_clash_mode') + return commands.getClashMode() } export async function getRuntimeConfig() { - return invoke('get_runtime_config') + return (await commands.getRuntimeConfig()) as IConfigData | null } export async function getRuntimeYaml() { - return invoke('get_runtime_yaml') + return commands.getRuntimeYaml() } export async function getRuntimeLogs() { - return invoke>('get_runtime_logs') + return commands.getRuntimeLogs() } export async function getRuntimeProxyChainConfig(proxyChainExitNode: string) { - return invoke('get_runtime_proxy_chain_config', { - proxyChainExitNode, - }) + return commands.getRuntimeProxyChainConfig(proxyChainExitNode) } -export async function updateProxyChainConfigInRuntime(proxyChainConfig: any) { - return invoke('update_proxy_chain_config_in_runtime', { - proxyChainConfig, - }) +export async function updateProxyChainConfigInRuntime( + proxyChainConfig: unknown, +) { + return commands.updateProxyChainConfigInRuntime( + proxyChainConfig == null ? null : (proxyChainConfig as JsonValue), + ) } export async function patchClashConfig(payload: Partial) { - return invoke('patch_clash_config', { payload }) + return commands.patchClashConfig(payload as Record) } export async function patchClashMode(payload: string) { - return invoke('patch_clash_mode', { payload }) + return commands.patchClashMode(payload) } export async function syncTrayProxySelection() { - return invoke('sync_tray_proxy_selection') + return commands.syncTrayProxySelection() } /** Sends one selection pair so the backend can merge against current profile state. */ export async function recordSelectedNode(groupName: string, node: string) { - return invoke('record_selected_node', { groupName, node }) + return commands.recordSelectedNode(groupName, node) } export async function forgetSelectedNode(groupName: string) { - return invoke('forget_selected_node', { groupName }) + return commands.forgetSelectedNode(groupName) } export async function getProxyView(): Promise { - const view = await invoke('get_proxy_view') + const view = await commands.getProxyView() if (view.schemaVersion !== 1) { throw new Error('Unsupported proxy view schema: ' + view.schemaVersion) } @@ -145,7 +152,7 @@ export async function getProxyView(): Promise { export async function getClashLogs() { const regex = /time="(.+?)"\s+level=(.+?)\s+msg="(.+?)"/ const newRegex = /(.+?)\s+(.+?)\s+(.+)/ - const logs = await invoke('get_clash_logs') + const logs = await commands.getClashLogs() return logs.reduce((acc, log) => { const result = log.match(regex) @@ -165,12 +172,12 @@ export async function getClashLogs() { }, []) } -export async function getVergeConfig() { - return invoke('get_verge_config') +export async function getVergeConfig(): Promise { + return commands.getVergeConfig() } export async function patchVergeConfig(payload: IVergeConfig) { - return invoke('patch_verge_config', { payload }) + return commands.patchVergeConfig(payload) } export async function setDnsOverride( @@ -178,76 +185,57 @@ export async function setDnsOverride( enabled: boolean, confirmation?: string, ) { - return invoke< - { status: 'applied' } | { status: 'confirmation_required'; source: string } - >('set_dns_override', { profileUid, enabled, confirmation }) + return commands.setDnsOverride(profileUid, enabled, confirmation ?? null) } export async function getDnsConfigContent() { - return invoke('get_dns_config_content') + return commands.getDnsConfigContent() } export async function saveDnsConfig(dnsConfig: Record) { - return invoke('save_dns_config', { dnsConfig }) + return commands.saveDnsConfig(dnsConfig as Record) } export async function validateDnsConfig() { - return invoke('validate_dns_config') + return commands.validateDnsConfig() } export async function applyDnsConfig(apply: boolean) { - return invoke('apply_dns_config', { apply }) + return commands.applyDnsConfig(apply) } export async function takeDnsOverrideNotice() { - return invoke('take_dns_override_notice') + return commands.takeDnsOverrideNotice() } -export type ServiceFallbackNotice = - | { kind: 'unavailable' } - | { kind: 'coreRejected'; reason: string } - | { kind: 'notAutoStarted' } - export async function takeServiceFallbackNotice() { - return invoke('take_service_fallback_notice') -} - -export interface CoreFailure { - kind: 'startFailed' | 'serviceCoreStopped' - detail: string + return commands.takeServiceFallbackNotice() } export async function getCoreStartupError() { - return invoke('get_core_startup_error') + return commands.getCoreStartupError() } export async function takeServiceRepairNotice() { - return invoke('take_service_repair_notice') + return commands.takeServiceRepairNotice() } export async function takeServiceOwnerNotice() { - return invoke('take_service_owner_notice') + return commands.takeServiceOwnerNotice() } export async function takeDiscardedKeysNotice() { - return invoke('take_discarded_keys_notice') + return commands.takeDiscardedKeysNotice() } export async function getSystemProxy() { - return invoke<{ - enable: boolean - server: string - bypass: string - }>('get_sys_proxy') + return commands.getSysProxy() } export async function getAutotemProxy() { try { debugLog('[API] 开始调用 get_auto_proxy') - const result = await invoke<{ - enable: boolean - url: string - }>('get_auto_proxy') + const result = await commands.getAutoProxy() debugLog('[API] get_auto_proxy 调用成功:', result) return result } catch (error) { @@ -260,30 +248,23 @@ export async function getAutotemProxy() { } export async function getEmbeddedServerPort() { - return invoke('get_embedded_server_port') + return commands.getEmbeddedServerPort() } export async function changeClashCore(clashCore: string) { - return invoke('change_clash_core', { clashCore }) + return commands.changeClashCore(clashCore) } export async function restartCore() { - return invoke('restart_core') -} - -export interface CoreUpgradeReport { - /** False when the managed core was already at the latest version. */ - upgraded: boolean - from: string - to: string + return commands.restartCore() } export async function upgradeClashCore(force = false) { - return invoke('upgrade_clash_core', { force }) + return commands.upgradeClashCore(force) } export async function restartApp() { - return invoke('restart_app') + return commands.restartApp() } export async function installUpdate( @@ -292,69 +273,57 @@ export async function installUpdate( ) { const channel = new Channel() channel.onmessage = onEvent - return invoke('install_update', { version, onEvent: channel }) + return commands.installUpdate(version, channel) } export async function cancelUpdateDownload() { - return invoke('cancel_update_download') + return commands.cancelUpdateDownload() } export async function getAppDir() { - return invoke('get_app_dir') + return commands.getAppDir() } export async function openAppDir() { - return invoke('open_app_dir').catch((err) => showNotice.error(err)) + return commands.openAppDir().catch((err) => showNotice.error(err)) } export async function openCoreDir() { - return invoke('open_core_dir').catch((err) => showNotice.error(err)) + return commands.openCoreDir().catch((err) => showNotice.error(err)) } export async function openLogsDir() { - return invoke('open_logs_dir').catch((err) => showNotice.error(err)) + return commands.openLogsDir().catch((err) => showNotice.error(err)) } export async function syncRuntimeProviders() { - return invoke('sync_runtime_providers').catch((err) => { + return commands.syncRuntimeProviders().catch((err) => { console.warn('failed to queue the provider cache sync', err) }) } export async function cmdTestDelay(url: string) { - return invoke('test_delay', { url }) + return commands.testDelay(url) } export async function invoke_uwp_tool() { - return invoke('invoke_uwp_tool').catch((err) => - showNotice.error(err, 1500), - ) + return commands.invokeUwpTool().catch((err) => showNotice.error(err, 1500)) } export async function openDevTools() { - return invoke('open_devtools') + return commands.openDevtools() } export async function exitApp() { - return invoke('exit_app') + return commands.exitApp() } export async function exportDiagnosticInfo() { - return invoke('export_diagnostic_info') -} - -interface SystemInfo { - system_name: string - system_version: string - system_kernel_version: string - system_arch: string - app_version: string - app_core_mode: string - app_is_admin: boolean + return commands.exportDiagnosticInfo() } export async function getSystemInfo() { - return invoke('get_system_info') + return commands.getSystemInfo() } export async function copyIconFile( @@ -373,55 +342,55 @@ export async function copyIconFile( current_t: currentTime, } - return invoke('copy_icon_file', { path, iconInfo }) + return commands.copyIconFile(path, iconInfo) } export async function downloadIconCache(url: string, name: string) { - return invoke('download_icon_cache', { url, name }) + return commands.downloadIconCache(url, name) } export async function getNetworkInterfaces() { - return invoke('get_network_interfaces') + return commands.getNetworkInterfaces() } export async function getSystemHostname() { - return invoke('get_system_hostname') + return commands.getSystemHostname() } export async function getNetworkInterfacesInfo() { - return invoke('get_network_interfaces_info') + return commands.getNetworkInterfacesInfo() } export async function createWebdavBackup() { - return invoke('create_webdav_backup') + return commands.createWebdavBackup() } export async function createLocalBackup() { - return invoke('create_local_backup') + return commands.createLocalBackup() } export async function deleteWebdavBackup(filename: string) { - return invoke('delete_webdav_backup', { filename }) + return commands.deleteWebdavBackup(filename) } export async function deleteLocalBackup(filename: string) { - return invoke('delete_local_backup', { filename }) + return commands.deleteLocalBackup(filename) } export async function restoreWebDavBackup(filename: string) { - return invoke('restore_webdav_backup', { filename }) + return commands.restoreWebdavBackup(filename) } export async function restoreLocalBackup(filename: string) { - return invoke('restore_local_backup', { filename }) + return commands.restoreLocalBackup(filename) } export async function importLocalBackup(source: string) { - return invoke('import_local_backup', { source }) + return commands.importLocalBackup(source) } export async function exportLocalBackup(filename: string, destination: string) { - return invoke('export_local_backup', { filename, destination }) + return commands.exportLocalBackup(filename, destination) } export async function saveWebdavConfig( @@ -429,177 +398,83 @@ export async function saveWebdavConfig( username: string, password: string, ) { - return invoke('save_webdav_config', { - url, - username, - password, - }) + return commands.saveWebdavConfig(url, username, password) } export async function listWebDavBackup() { - const list: IWebDavFile[] = await invoke('list_webdav_backup') - list.forEach((item) => { - item.filename = item.href.split('/').pop() as string - }) - return list + return (await commands.listWebdavBackup()).map((item) => ({ + ...item, + filename: item.href.split('/').pop() ?? '', + tag: item.tag, + })) } export async function listLocalBackup() { - return invoke('list_local_backup') + return commands.listLocalBackup() } -export type RunningMode = 'Service' | 'Sidecar' | 'NotRunning' - -type ServiceHealth = - | 'unknown' - | 'ready' - | 'notInstalled' - | 'versionMismatch' - | 'unavailable' - -type PendingServiceAction = - | 'install' - | 'uninstall' - | 'reinstall' - | 'forceReinstall' - /** Consistent core/service snapshot with backend-derived availability flags. */ -export interface RunState { - mode: RunningMode - service: ServiceHealth - serviceUnavailableReason: string | null - pendingAction: PendingServiceAction | null - sidecarAllowed: boolean - isAdmin: boolean - opInFlight: boolean - serviceUsable: boolean - tunCapable: boolean - serviceNeedsAttention: boolean -} export const getRuntimeState = async () => { - return invoke('get_runtime_state') -} - -export type FailedOperation = - | 'systemProxyEnable' - | 'systemProxyDisable' - | 'systemProxyRestore' - | 'systemProxyGuard' - -export interface PendingFailure { - code: string - detail: string - operation: FailedOperation - sequence: number + return commands.getRuntimeState() } export const getPendingFailures = async () => { - return invoke('get_pending_failures') -} - -export interface SidecarFailureSnapshot { - revision: number - detail: string | null + return commands.getPendingFailures() } export const getSidecarFailure = async () => { - return invoke('get_sidecar_failure') + return commands.getSidecarFailure() } export const getAppUptime = async () => { - return invoke('get_app_uptime') + return commands.getAppUptime() } -export type ServiceInstallOutcome = - | { status: 'installed' } - | { status: 'sidecar'; reason: string } - export const installService = async () => { - return invoke('install_service') + return commands.installService() } export const uninstallService = async () => { - return invoke('uninstall_service') + return commands.uninstallService() } export const reinstallService = async () => { - return invoke('reinstall_service') + return commands.reinstallService() } export const repairService = async () => { - return invoke('repair_service') + return commands.repairService() } export const continueWithSidecar = async () => { - return invoke('continue_with_sidecar') + return commands.continueWithSidecar() } export const entry_lightweight_mode = async () => { - return invoke('entry_lightweight_mode') + return commands.entryLightweightMode() } export async function getNextUpdateTime(uid: string) { - return invoke('get_next_update_time', { uid }) + return commands.getNextUpdateTime(uid) } -interface ToggleableProxyPort { - enabled: boolean - port: number -} - -export interface ProxyPortSettings { - mixedPort: number - socks: ToggleableProxyPort - http: ToggleableProxyPort - redir: ToggleableProxyPort - tproxy: ToggleableProxyPort -} - -export type ListenerTransport = 'tcp' | 'udp' - -export interface ListenerProbe { - address: string - transports: ListenerTransport[] -} - -export type ListenerProbeOutcome = - | { status: 'available' } - | { - status: 'conflict' - port: number - transport: ListenerTransport - } - | { status: 'invalid'; message: string } - | { status: 'indeterminate'; message: string } - -export type SaveProxyPortsOutcome = - | { status: 'saved' } - | { status: 'conflict'; port: number; transport: ListenerTransport } - export const probeListener = async (request: ListenerProbe) => { - return invoke('probe_listener', { request }) + return commands.probeListener(request) } export const saveProxyPorts = async (settings: ProxyPortSettings) => { - return invoke('save_proxy_ports', { settings }) -} - -export interface UnlockItem { - name: string - status: string - region?: string | null - check_time?: string | null + return commands.saveProxyPorts(settings) } export async function getUnlockItems() { - return invoke('get_unlock_items') + return commands.getUnlockItems() } export async function checkMediaUnlock(onComplete: Channel) { - return invoke('check_media_unlock', { onComplete }) + return commands.checkMediaUnlock(onComplete) } export async function checkMediaUnlockItem(name: string) { - return invoke('check_media_unlock_item', { name }) + return commands.checkMediaUnlockItem(name) } diff --git a/src/services/contract.ts b/src/services/contract.ts index 093d9fcd8..5c1b83957 100644 --- a/src/services/contract.ts +++ b/src/services/contract.ts @@ -1,56 +1 @@ -// @generated by scripts/gen-contract.mjs from the backend wire contract. DO NOT EDIT. -// Regen: pnpm gen:contract - -import type { RunState } from './cmds' - -/** Wire status strings from NoticeStatus; keep in sync via `pnpm gen:contract`. */ -export type NoticeStatus = - | 'info' - | 'import_sub_url::ok' - | 'import_sub_url::error' - | 'set_config::error' - | 'core_start::error' - | 'service_core::repair_required' - | 'service_core::app_data_not_owned' - | 'service_core::sidecar_fallback' - | 'dns_override::auto_disabled' - | 'enhance::discarded_keys' - | 'tun_mode::auto_disabled' - | 'tun_mode::auto_disable_failed' - | 'app_restart::core_stop_failed' - | 'app_quit::core_stop_failed' - | 'update_with_clash_proxy' - | 'update_failed_even_with_clash' - | 'reactivate_profiles::error' - | 'update_failed' - | 'config_validate::boot_error' - | 'config_validate::error' - | 'config_validate::process_terminated' - | 'config_validate::script_error' - | 'config_validate::script_syntax_error' - | 'config_validate::script_missing_main' - | 'config_validate::file_not_found' - | 'config_validate::yaml_syntax_error' - | 'config_validate::yaml_read_error' - | 'config_validate::yaml_mapping_error' - | 'config_validate::merge_syntax_error' - | 'config_validate::merge_mapping_error' - | 'config_core::change_success' - | 'config_core::change_error' - | 'mixed_port::fallback' - | 'mixed_port::fallback_error' - -/** Event name → payload type for every backend-emitted event. */ -export interface VergeEventPayloads { - 'verge://refresh-clash-config': string - 'verge://refresh-verge-config': string - 'verge://refresh-profiles': string - 'verge://refresh-proxy-config': null - 'verge://notice-message': [NoticeStatus, string] - 'verge://timer-updated': string - 'verge://run-state-changed': RunState - 'verge://pending-failures-changed': null - 'profile-changed': string - 'profile-update-started': { uid: string } - 'profile-update-completed': { uid: string } -} +export type { NoticeStatus, VergeEventPayloads } from './bindings' diff --git a/src/services/notice-service.ts b/src/services/notice-service.ts index 34157a1ea..c36a0a85b 100644 --- a/src/services/notice-service.ts +++ b/src/services/notice-service.ts @@ -305,11 +305,7 @@ function extractDisplayText(input: unknown): string | undefined { } } -export interface CommandFailure { - code?: string - operation?: FailedOperation - detail: string -} +import type { CommandFailure_Serialize as CommandFailure } from './bindings' function isCommandFailure(input: unknown): input is CommandFailure { if (typeof input !== 'object' || input === null) return false @@ -495,11 +491,11 @@ const baseShowNotice = ( } function failureCode(input: unknown): string | undefined { - return isCommandFailure(input) ? input.code : undefined + return isCommandFailure(input) ? input.code! : undefined } function failureOperation(input: unknown): FailedOperation | undefined { - return isCommandFailure(input) ? input.operation : undefined + return isCommandFailure(input) ? input.operation! : undefined } /** diff --git a/src/types/global.d.ts b/src/types/global.d.ts index 13da2141e..d86c1673d 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -13,10 +13,7 @@ type Platform = declare const OS_PLATFORM: Platform -type ValidationOutcome = - | { status: 'valid' | 'busy' } - | { status: 'invalid'; kind: string; message: string } - | { status: 'skipped'; reason: string } +type ValidationOutcome = import('@/services/bindings').ValidationOutcome interface IConfigData { port: number @@ -207,82 +204,19 @@ interface IConnectionSetting { layout: 'table' | 'list' } -interface IClashInfo { - mixed_port?: number // clash mixed port - socks_port?: number // clash socks port - redir_port?: number // clash redir port - tproxy_port?: number // clash tproxy port - port?: number // clash http port - server?: string // external-controller - secret?: string -} +type IClashInfo = import('@/services/bindings').ClashInfo -interface IProfileItem { - uid: string - type?: 'local' | 'remote' | 'merge' | 'script' - name?: string - desc?: string - file?: string - url?: string - updated?: number - selected?: { - name?: string - now?: string - }[] - extra?: { - upload: number - download: number - total: number - expire: number - } - option?: IProfileOption - home?: string -} +type IProfileItem = import('@/services/bindings').PrfItem_Serialize -interface IProfileOption { - user_agent?: string - with_proxy?: boolean - self_proxy?: boolean - update_interval?: number - timeout_seconds?: number - danger_accept_invalid_certs?: boolean - allow_auto_update?: boolean - merge?: string - script?: string - rules?: string - proxies?: string - groups?: string -} +type IProfileOption = import('@/services/bindings').PrfOption_Deserialize -interface IProfilesConfig { - current?: string - items?: IProfileItem[] -} +type IProfilesConfig = Partial< + import('@/services/bindings').IProfiles_Serialize +> -interface IVergeTestItem { - uid: string - name?: string - icon?: string - url: string -} -interface IAddress { - V4?: { - ip: string - broadcast?: string - netmask?: string - } - V6?: { - ip: string - broadcast?: string - netmask?: string - } -} -interface INetworkInterface { - name: string - addr: IAddress[] - mac_addr?: string - index: number -} +type IVergeTestItem = import('@/services/bindings').IVergeTestItem +type IAddress = import('@/services/bindings').InterfaceAddress +type INetworkInterface = import('@/services/bindings').NetworkInterfaceView interface ISeqProfileConfig { prepend: [] @@ -850,119 +784,24 @@ interface IProxyConfig | 'sudoku' } -interface IVergeConfig { - app_log_level?: 'trace' | 'debug' | 'info' | 'warn' | 'error' | string - app_log_max_size?: number // KB - app_log_max_count?: number - language?: string - tray_event?: - | 'main_window' - | 'tray_menu' - | 'system_proxy' - | 'tun_mode' - | string - env_type?: 'bash' | 'cmd' | 'powershell' | 'fish' | string - startup_script?: string - start_page?: string - clash_core?: string - theme_mode?: 'light' | 'dark' | 'system' - traffic_graph?: boolean - enable_memory_usage?: boolean - enable_group_icon?: boolean - pause_render_traffic_stats_on_blur?: boolean - menu_icon?: 'monochrome' | 'colorful' | 'disable' - menu_order?: string[] - proxy_group_tools_position?: 'left' | 'right' - notice_position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' - collapse_navbar?: boolean - tray_icon?: 'monochrome' | 'colorful' - common_tray_icon?: boolean - sysproxy_tray_icon?: boolean - tun_tray_icon?: boolean - enable_tray_speed?: boolean - tray_proxy_groups_display_mode?: 'default' | 'inline' | 'disable' - tray_inline_outbound_modes?: boolean - enable_tun_mode?: boolean - enable_auto_light_weight_mode?: boolean - auto_light_weight_minutes?: number - enable_auto_launch?: boolean - enable_silent_start?: boolean - enable_system_proxy?: boolean - enable_global_hotkey?: boolean - enable_dns_settings?: boolean - profile_dns_settings?: Record - proxy_auto_config?: boolean - pac_file_content?: string - proxy_host?: string - enable_random_port?: boolean - verge_mixed_port?: number - verge_socks_port?: number - verge_redir_port?: number - verge_tproxy_port?: number - verge_port?: number - verge_redir_enabled?: boolean - verge_tproxy_enabled?: boolean - verge_socks_enabled?: boolean - verge_http_enabled?: boolean - enable_proxy_guard?: boolean - enable_bypass_check?: boolean - use_default_bypass?: boolean - proxy_guard_duration?: number - system_proxy_bypass?: string - web_ui_list?: string[] - hotkeys?: string[] - theme_setting?: { - primary_color?: string - secondary_color?: string - primary_text?: string - secondary_text?: string - info_color?: string - error_color?: string - warning_color?: string - success_color?: string - font_family?: string - css_injection?: string - background_image?: string - background_blend_mode?: string - background_opacity?: number - } - auto_close_connection?: boolean - auto_check_update?: boolean - default_latency_test?: string - default_latency_timeout?: number - enable_auto_delay_detection?: boolean - auto_delay_detection_interval_minutes?: number - enable_builtin_enhanced?: boolean - auto_log_clean?: 0 | 1 | 2 | 3 | 4 - enable_auto_backup_schedule?: boolean - auto_backup_interval_hours?: number - auto_backup_on_change?: boolean - proxy_layout_column?: number - test_list?: IVergeTestItem[] - webdav_url?: string - webdav_username?: string - webdav_password?: string - home_cards?: Record - enable_hover_jump_navigator?: boolean - hover_jump_navigator_delay?: number - enable_external_controller?: boolean +type IVergeConfig = Omit< + Partial, + 'theme_setting' +> & { + theme_setting?: + | (import('@/services/bindings').IVergeTheme & { + background_image?: string + background_blend_mode?: string + background_opacity?: number + }) + | null } -interface IWebDavFile { +type IWebDavFile = import('@/services/bindings').WebDavFile & { filename: string - href: string - last_modified: string - content_length: number - content_type: string - tag: string } -interface ILocalBackupFile { - filename: string - path: string - last_modified: string - content_length: number -} +type ILocalBackupFile = import('@/services/bindings').LocalBackupFile interface IWebDavConfig { url: string diff --git a/src/types/proxy-view.ts b/src/types/proxy-view.ts index 3f4522f52..d65b1fa89 100644 --- a/src/types/proxy-view.ts +++ b/src/types/proxy-view.ts @@ -1,84 +1,15 @@ -export interface ProxyViewV1 { - schemaVersion: 1 - orderSource: 'runtime' | 'fallback' - providerState: 'ready' | 'unavailable' - global: ProxyGroupView | null - direct: string | null - groups: ProxyGroupView[] - records: Record - standalone: string[] - providers: ProxyProviderView[] -} - -interface ProxyCapabilities { - udp: boolean - xudp: boolean - tfo: boolean - mptcp: boolean - smux: boolean -} - -interface DelayHistory { - time: string - delay: number -} - -export interface ProxyGroupView extends ProxyCapabilities { - name: string - type: string - alive: boolean - now?: string - fixed?: string - hidden?: boolean - icon?: string - testUrl?: string - history: DelayHistory[] - members: ProxyMemberRef[] -} - -export interface ProxyNodeView extends ProxyCapabilities { - recordId: string - name: string - type: string - alive: boolean - history: DelayHistory[] - id?: string - hidden?: boolean - icon?: string - testUrl?: string - source: - | { kind: 'core'; proxyName: string } - | { kind: 'provider'; providerName: string; proxyName: string } -} - -type ProxyMemberUnresolvedReason = - | 'missing' - | 'ambiguous' - | 'provider-unavailable' - -export type ProxyMemberRef = - | { kind: 'group'; name: string } - | { kind: 'node'; name: string; recordId: string } - | { - kind: 'unresolved' - name: string - reason: ProxyMemberUnresolvedReason - } - -interface ProxyProviderView { - name: string - vehicleType: 'HTTP' | 'File' | 'Inline' - updatedAt?: string - subscriptionInfo?: ProxySubscriptionInfo - proxyRecordIds: string[] -} - -interface ProxySubscriptionInfo { - upload: number - download: number - total: number - expire: number -} +import type { + ProxyViewV1_Serialize as ProxyViewV1, + ProxyGroupView_Serialize as ProxyGroupView, + ProxyNodeView_Serialize as ProxyNodeView, + ProxyMemberRef, +} from '@/services/bindings' +export type { + ProxyViewV1_Serialize as ProxyViewV1, + ProxyGroupView_Serialize as ProxyGroupView, + ProxyNodeView_Serialize as ProxyNodeView, + ProxyMemberRef, +} from '@/services/bindings' export type ResolvedProxyMember = | {