diff --git a/crates/tauri-plugin-mihomo/.env b/crates/tauri-plugin-mihomo/.env new file mode 100644 index 000000000..d530fe30d --- /dev/null +++ b/crates/tauri-plugin-mihomo/.env @@ -0,0 +1 @@ +MIHOMO_SOCKET=1 diff --git a/crates/tauri-plugin-mihomo/.gitignore b/crates/tauri-plugin-mihomo/.gitignore new file mode 100644 index 000000000..055f20d3c --- /dev/null +++ b/crates/tauri-plugin-mihomo/.gitignore @@ -0,0 +1,14 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ +WixTools +frontend + +Cargo.lock + +# Generated by Tauri +# will have schema files for capabilities auto-completion +/gen/schemas + +permissions/* +!permissions/default.toml diff --git a/crates/tauri-plugin-mihomo/.pre-commit-config.yaml b/crates/tauri-plugin-mihomo/.pre-commit-config.yaml new file mode 100644 index 000000000..56037b220 --- /dev/null +++ b/crates/tauri-plugin-mihomo/.pre-commit-config.yaml @@ -0,0 +1,48 @@ +fail_fast: true + +repos: + # prek has build-in pre-commit hooks + # - repo: "https://github.com/pre-commit/pre-commit-hooks" + # rev: v5.0.0 + # hooks: + # - id: trailing-whitespace + # - id: end-of-file-fixer + # - id: check-yaml + # - id: check-toml + # - id: check-added-large-files + # this will check typos and fix file automatically, but need to re-add file by using git + - repo: https://github.com/crate-ci/typos + rev: v1.36.2 + hooks: + - id: typos + - repo: local + hooks: + # wait for `running multiple commands` feature to complete (https://github.com/j178/prek/issues/431) + # cargo fmt + - id: cargo-fmt + name: cargo fmt + description: Format files with rustfmt. + entry: cargo +nightly fmt -- --check + language: system + types: + - rust + + # cargo check + - id: cargo-check + name: cargo check + description: Check the package for errors. + entry: cargo check --all + language: system + types: + - rust + pass_filenames: false + + # cargo clippy + - id: cargo-clippy + name: cargo clippy + description: Lint rust sources + entry: cargo clippy --all-targets --all-features --tests --benches -- -D warnings + language: system + types: + - rust + pass_filenames: false diff --git a/crates/tauri-plugin-mihomo/Cargo.toml b/crates/tauri-plugin-mihomo/Cargo.toml new file mode 100644 index 000000000..d9928dce8 --- /dev/null +++ b/crates/tauri-plugin-mihomo/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "tauri-plugin-mihomo" +version = "0.1.0" +authors = ["oomeow"] +description = "http or ipc api for mihomo" +edition = "2024" +exclude = ["/examples", "/dist-js", "/guest-js", "/node_modules"] +links = "tauri-plugin-mihomo" + +[dependencies] +tauri = { version = "2" } +thiserror = "2" +reqwest = { version = "0.12", features = ["json"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1" +futures-util = "0.3" +tokio = { version = "1", features = ["net", "sync", "macros"] } +tokio-tungstenite = { version = "0.27" } +rand = "0.9" +http = "1.3" +base64 = "0.22" +httparse = "1.10" +log = "0.4" +urlencoding = "2.1" +ts-rs = "11.0" +pin-project = "1.1.10" + +[target.'cfg(windows)'.dependencies] +windows-sys = "0.59" + +[build-dependencies] +tauri-plugin = { version = "2", features = ["build"] } + +[dev-dependencies] +dotenvy = "=0.15.7" diff --git a/crates/tauri-plugin-mihomo/README.md b/crates/tauri-plugin-mihomo/README.md new file mode 100644 index 000000000..9ed1ee658 --- /dev/null +++ b/crates/tauri-plugin-mihomo/README.md @@ -0,0 +1,23 @@ +# Tauri Plugin mihomo + +一个基于 Tauri 框架的调用 Mihomo API 插件,支持 Mihomo 的 HTTP 和 Socket 通信 + +### 测试 Mimoho 所有 API 的接口状态 + +推荐使用 [nextest](https://github.com/nextest-rs/nextest) (一款更干净美观、速度更快的跨平台测试运行器)进行单元测试 + +默认使用 socket 连接 Mihomo 测试,可通过设置 `MIHOMO_SOCKET` 环境变量来使用 http 连接 Mihomo 测试 + +> 修改 `.env` 配置文件,将 `MIHOMO_SOCKET` 设置为 `0`, 再执行单元测试 + +```shell +# 此命令会排除 restart/reload_config 方法, 因为这两个接口都会让内核重新加载配置文件,会导致其他测试用例错误 +cargo nextest run mihomo_ + +# -------------------------- +# 测试 reload_config 方法 +cargo nextest run reload + +# 测试 restart 方法 +cargo nextest run restart +``` diff --git a/crates/tauri-plugin-mihomo/build.rs b/crates/tauri-plugin-mihomo/build.rs new file mode 100644 index 000000000..dd5ad5e62 --- /dev/null +++ b/crates/tauri-plugin-mihomo/build.rs @@ -0,0 +1,47 @@ +const COMMANDS: &[&str] = &[ + "update_controller", + "update_secret", + "get_version", + "flush_fakeip", + "flush_dns", + "get_connections", + "close_all_connections", + "close_connections", + "get_groups", + "get_group_by_name", + "delay_group", + "get_proxy_providers", + "get_proxy_provider_by_name", + "update_proxy_provider", + "healthcheck_proxy_provider", + "healthcheck_node_in_provider", + "get_proxies", + "get_proxy_by_name", + "select_node_for_group", + "unfixed_proxy", + "delay_proxy_by_name", + "get_rules", + "get_rule_providers", + "update_rule_provider", + "get_base_config", + "reload_config", + "patch_base_config", + "update_geo", + "restart", + "upgrade_core", + "upgrade_ui", + "upgrade_geo", + "ws_traffic", + "ws_memory", + "ws_connections", + "ws_logs", + "ws_disconnect", + "clear_all_ws_connections", +]; + +fn main() { + tauri_plugin::Builder::new(COMMANDS) + .android_path("android") + .ios_path("ios") + .build(); +} diff --git a/crates/tauri-plugin-mihomo/dist-js/bindings/BaseConfig.d.ts b/crates/tauri-plugin-mihomo/dist-js/bindings/BaseConfig.d.ts new file mode 100644 index 000000000..ab644f23a --- /dev/null +++ b/crates/tauri-plugin-mihomo/dist-js/bindings/BaseConfig.d.ts @@ -0,0 +1,46 @@ +import type { ClashMode } from "./ClashMode"; +import type { FindProcessMode } from "./FindProcessMode"; +import type { GeoXUrl } from "./GeoXUrl"; +import type { LogLevel } from "./LogLevel"; +import type { TuicServer } from "./TuicServer"; +import type { TunConfig } from "./TunConfig"; +export type BaseConfig = { + port: number; + socksPort: number; + redirPort: number; + tproxyPort: number; + mixedPort: number; + tun: TunConfig; + tuicServer: TuicServer; + ssConfig: string; + vmessConfig: string; + authentication: Array | null; + skipAuthPrefixes: Array | null; + lanAllowedIps: Array | null; + lanDisallowedIps: Array | null; + allowLan: boolean; + bindAddress: string; + inboundTfo: boolean; + inboundMptcp: boolean; + mode: ClashMode; + unifiedDelay: boolean; + logLevel: LogLevel; + ipv6: boolean; + interfaceName: string; + routingMark: number; + geoxUrl: GeoXUrl; + geoAutoUpdate: boolean; + geoUpdateInterval: number; + geodataMode: boolean; + geodataLoader: string; + geositeMatcher: string; + tcpConcurrent: boolean; + findProcessMode: FindProcessMode; + sniffing: boolean; + globalClientFingerprint: string; + globalUa: string; + etagSupport: boolean; + keepAliveInterval: number; + keepAliveIdle: number; + disableKeepAlive: boolean; +}; diff --git a/crates/tauri-plugin-mihomo/dist-js/bindings/BrutalOption.d.ts b/crates/tauri-plugin-mihomo/dist-js/bindings/BrutalOption.d.ts new file mode 100644 index 000000000..5dbb9d351 --- /dev/null +++ b/crates/tauri-plugin-mihomo/dist-js/bindings/BrutalOption.d.ts @@ -0,0 +1,5 @@ +export type BrutalOption = { + enabled: boolean; + up?: string; + down?: string; +}; diff --git a/crates/tauri-plugin-mihomo/dist-js/bindings/ClashMode.d.ts b/crates/tauri-plugin-mihomo/dist-js/bindings/ClashMode.d.ts new file mode 100644 index 000000000..03e7e6452 --- /dev/null +++ b/crates/tauri-plugin-mihomo/dist-js/bindings/ClashMode.d.ts @@ -0,0 +1,4 @@ +/** + * clash mode enum + */ +export type ClashMode = "rule" | "global" | "direct"; diff --git a/crates/tauri-plugin-mihomo/dist-js/bindings/Connection.d.ts b/crates/tauri-plugin-mihomo/dist-js/bindings/Connection.d.ts new file mode 100644 index 000000000..a81176cad --- /dev/null +++ b/crates/tauri-plugin-mihomo/dist-js/bindings/Connection.d.ts @@ -0,0 +1,11 @@ +import type { ConnectionMetaData } from "./ConnectionMetaData"; +export type Connection = { + id: string; + metadata: ConnectionMetaData; + upload: bigint; + download: bigint; + start: string; + chains: Array; + rule: string; + rulePayload: string; +}; diff --git a/crates/tauri-plugin-mihomo/dist-js/bindings/ConnectionMetaData.d.ts b/crates/tauri-plugin-mihomo/dist-js/bindings/ConnectionMetaData.d.ts new file mode 100644 index 000000000..9929ce92d --- /dev/null +++ b/crates/tauri-plugin-mihomo/dist-js/bindings/ConnectionMetaData.d.ts @@ -0,0 +1,29 @@ +import type { ConnectionType } from "./ConnectionType"; +import type { DNSMode } from "./DNSMode"; +import type { Network } from "./Network"; +export type ConnectionMetaData = { + network: Network; + type: ConnectionType; + sourceIP: string; + destinationIP: string; + sourceGeoIP: Array | null; + destinationGeoIP: Array | null; + sourceIPASN: string; + destinationIPASN: string; + sourcePort: string; + destinationPort: string; + inboundIP: string; + inboundPort: string; + inboundName: string; + inboundUser: string; + host: string; + dnsMode: DNSMode; + uid: number; + process: string; + processPath: string; + specialProxy: string; + specialRules: string; + remoteDestination: string; + dscp: number; + sniffHost: string; +}; diff --git a/crates/tauri-plugin-mihomo/dist-js/bindings/ConnectionType.d.ts b/crates/tauri-plugin-mihomo/dist-js/bindings/ConnectionType.d.ts new file mode 100644 index 000000000..8dacdde3b --- /dev/null +++ b/crates/tauri-plugin-mihomo/dist-js/bindings/ConnectionType.d.ts @@ -0,0 +1 @@ +export type ConnectionType = "HTTP" | "HTTPS" | "Socks4" | "Socks5" | "ShadowSocks" | "Vmess" | "Vless" | "Redir" | "TProxy" | "Trojan" | "Tunnel" | "Tun" | "Tuic" | "Hysteria2" | "AnyTLS" | "Inner"; diff --git a/crates/tauri-plugin-mihomo/dist-js/bindings/Connections.d.ts b/crates/tauri-plugin-mihomo/dist-js/bindings/Connections.d.ts new file mode 100644 index 000000000..a6ae88cd2 --- /dev/null +++ b/crates/tauri-plugin-mihomo/dist-js/bindings/Connections.d.ts @@ -0,0 +1,10 @@ +import type { Connection } from "./Connection"; +/** + * connections + */ +export type Connections = { + downloadTotal: bigint; + uploadTotal: bigint; + connections: Array | null; + memory: number; +}; diff --git a/crates/tauri-plugin-mihomo/dist-js/bindings/CoreUpdaterChannel.d.ts b/crates/tauri-plugin-mihomo/dist-js/bindings/CoreUpdaterChannel.d.ts new file mode 100644 index 000000000..53627280d --- /dev/null +++ b/crates/tauri-plugin-mihomo/dist-js/bindings/CoreUpdaterChannel.d.ts @@ -0,0 +1 @@ +export type CoreUpdaterChannel = "release" | "alpha" | "auto"; diff --git a/crates/tauri-plugin-mihomo/dist-js/bindings/DNSMode.d.ts b/crates/tauri-plugin-mihomo/dist-js/bindings/DNSMode.d.ts new file mode 100644 index 000000000..e0c1704db --- /dev/null +++ b/crates/tauri-plugin-mihomo/dist-js/bindings/DNSMode.d.ts @@ -0,0 +1 @@ +export type DNSMode = "normal" | "fake-ip" | "redir-host" | "hosts"; diff --git a/crates/tauri-plugin-mihomo/dist-js/bindings/DelayHistory.d.ts b/crates/tauri-plugin-mihomo/dist-js/bindings/DelayHistory.d.ts new file mode 100644 index 000000000..a73fc82ac --- /dev/null +++ b/crates/tauri-plugin-mihomo/dist-js/bindings/DelayHistory.d.ts @@ -0,0 +1,4 @@ +export type DelayHistory = { + time: string; + delay: number; +}; diff --git a/crates/tauri-plugin-mihomo/dist-js/bindings/Extra.d.ts b/crates/tauri-plugin-mihomo/dist-js/bindings/Extra.d.ts new file mode 100644 index 000000000..a90973202 --- /dev/null +++ b/crates/tauri-plugin-mihomo/dist-js/bindings/Extra.d.ts @@ -0,0 +1,5 @@ +import type { DelayHistory } from "./DelayHistory"; +export type Extra = { + alive: boolean; + history: Array; +}; diff --git a/crates/tauri-plugin-mihomo/dist-js/bindings/FindProcessMode.d.ts b/crates/tauri-plugin-mihomo/dist-js/bindings/FindProcessMode.d.ts new file mode 100644 index 000000000..7ddefa3a0 --- /dev/null +++ b/crates/tauri-plugin-mihomo/dist-js/bindings/FindProcessMode.d.ts @@ -0,0 +1 @@ +export type FindProcessMode = "strict" | "always" | "off"; diff --git a/crates/tauri-plugin-mihomo/dist-js/bindings/GeoXUrl.d.ts b/crates/tauri-plugin-mihomo/dist-js/bindings/GeoXUrl.d.ts new file mode 100644 index 000000000..c93769c81 --- /dev/null +++ b/crates/tauri-plugin-mihomo/dist-js/bindings/GeoXUrl.d.ts @@ -0,0 +1,6 @@ +export type GeoXUrl = { + geoIp: string; + mmdb: string; + asn: string; + geoSite: string; +}; diff --git a/crates/tauri-plugin-mihomo/dist-js/bindings/Groups.d.ts b/crates/tauri-plugin-mihomo/dist-js/bindings/Groups.d.ts new file mode 100644 index 000000000..0cd994b34 --- /dev/null +++ b/crates/tauri-plugin-mihomo/dist-js/bindings/Groups.d.ts @@ -0,0 +1,7 @@ +import type { Proxy } from "./Proxy"; +/** + * group proxies + */ +export type Groups = { + proxies: Array; +}; diff --git a/crates/tauri-plugin-mihomo/dist-js/bindings/Log.d.ts b/crates/tauri-plugin-mihomo/dist-js/bindings/Log.d.ts new file mode 100644 index 000000000..c58d7d6f8 --- /dev/null +++ b/crates/tauri-plugin-mihomo/dist-js/bindings/Log.d.ts @@ -0,0 +1,4 @@ +export type Log = { + type: string; + payload: string; +}; diff --git a/crates/tauri-plugin-mihomo/dist-js/bindings/LogLevel.d.ts b/crates/tauri-plugin-mihomo/dist-js/bindings/LogLevel.d.ts new file mode 100644 index 000000000..5851dbc95 --- /dev/null +++ b/crates/tauri-plugin-mihomo/dist-js/bindings/LogLevel.d.ts @@ -0,0 +1 @@ +export type LogLevel = "debug" | "info" | "warning" | "error" | "silent"; diff --git a/crates/tauri-plugin-mihomo/dist-js/bindings/Memory.d.ts b/crates/tauri-plugin-mihomo/dist-js/bindings/Memory.d.ts new file mode 100644 index 000000000..58ff0ad44 --- /dev/null +++ b/crates/tauri-plugin-mihomo/dist-js/bindings/Memory.d.ts @@ -0,0 +1,4 @@ +export type Memory = { + inuse: number; + oslimit: number; +}; diff --git a/crates/tauri-plugin-mihomo/dist-js/bindings/MihomoVersion.d.ts b/crates/tauri-plugin-mihomo/dist-js/bindings/MihomoVersion.d.ts new file mode 100644 index 000000000..95b00e8c3 --- /dev/null +++ b/crates/tauri-plugin-mihomo/dist-js/bindings/MihomoVersion.d.ts @@ -0,0 +1,7 @@ +/** + * mihomo version + */ +export type MihomoVersion = { + meta: boolean; + version: string; +}; diff --git a/crates/tauri-plugin-mihomo/dist-js/bindings/MuxOption.d.ts b/crates/tauri-plugin-mihomo/dist-js/bindings/MuxOption.d.ts new file mode 100644 index 000000000..26281d168 --- /dev/null +++ b/crates/tauri-plugin-mihomo/dist-js/bindings/MuxOption.d.ts @@ -0,0 +1,5 @@ +import type { BrutalOption } from "./BrutalOption"; +export type MuxOption = { + padding?: boolean; + brutal?: BrutalOption; +}; diff --git a/crates/tauri-plugin-mihomo/dist-js/bindings/Network.d.ts b/crates/tauri-plugin-mihomo/dist-js/bindings/Network.d.ts new file mode 100644 index 000000000..f1eaf8b11 --- /dev/null +++ b/crates/tauri-plugin-mihomo/dist-js/bindings/Network.d.ts @@ -0,0 +1 @@ +export type Network = "tcp" | "udp" | "all"; diff --git a/crates/tauri-plugin-mihomo/dist-js/bindings/ProviderType.d.ts b/crates/tauri-plugin-mihomo/dist-js/bindings/ProviderType.d.ts new file mode 100644 index 000000000..c6483a02c --- /dev/null +++ b/crates/tauri-plugin-mihomo/dist-js/bindings/ProviderType.d.ts @@ -0,0 +1 @@ +export type ProviderType = "Proxy" | "Rule"; diff --git a/crates/tauri-plugin-mihomo/dist-js/bindings/Proxies.d.ts b/crates/tauri-plugin-mihomo/dist-js/bindings/Proxies.d.ts new file mode 100644 index 000000000..63c5b62ff --- /dev/null +++ b/crates/tauri-plugin-mihomo/dist-js/bindings/Proxies.d.ts @@ -0,0 +1,9 @@ +import type { Proxy } from "./Proxy"; +/** + * proxies + */ +export type Proxies = { + proxies: { + [key in string]?: Proxy; + }; +}; diff --git a/crates/tauri-plugin-mihomo/dist-js/bindings/Proxy.d.ts b/crates/tauri-plugin-mihomo/dist-js/bindings/Proxy.d.ts new file mode 100644 index 000000000..c62a87777 --- /dev/null +++ b/crates/tauri-plugin-mihomo/dist-js/bindings/Proxy.d.ts @@ -0,0 +1,29 @@ +import type { DelayHistory } from "./DelayHistory"; +import type { Extra } from "./Extra"; +import type { ProxyType } from "./ProxyType"; +export type Proxy = { + all?: Array; + expectedStatus?: string; + fixed?: string; + hidden?: boolean; + icon?: string; + now?: string; + testUrl?: string; + id?: string; + alive: boolean; + history: Array; + extra: { + [key in string]?: Extra; + }; + name: string; + udp: boolean; + uot: boolean; + type: ProxyType; + xudp: boolean; + tfo: boolean; + mptcp: boolean; + smux: boolean; + interface: string; + dialerProxy: string; + routingMark: number; +}; diff --git a/crates/tauri-plugin-mihomo/dist-js/bindings/ProxyDelay.d.ts b/crates/tauri-plugin-mihomo/dist-js/bindings/ProxyDelay.d.ts new file mode 100644 index 000000000..5b71ba85b --- /dev/null +++ b/crates/tauri-plugin-mihomo/dist-js/bindings/ProxyDelay.d.ts @@ -0,0 +1,8 @@ +/** + * proxy delay result + * + * displays a message if it times out, otherwise it only displays the delay + */ +export type ProxyDelay = { + delay: number; +}; diff --git a/crates/tauri-plugin-mihomo/dist-js/bindings/ProxyProvider.d.ts b/crates/tauri-plugin-mihomo/dist-js/bindings/ProxyProvider.d.ts new file mode 100644 index 000000000..317d4832f --- /dev/null +++ b/crates/tauri-plugin-mihomo/dist-js/bindings/ProxyProvider.d.ts @@ -0,0 +1,14 @@ +import type { ProviderType } from "./ProviderType"; +import type { Proxy } from "./Proxy"; +import type { SubScriptionInfo } from "./SubScriptionInfo"; +import type { VehicleType } from "./VehicleType"; +export type ProxyProvider = { + name: string; + type: ProviderType; + vehicleType: VehicleType; + proxies: Array; + testUrl: string; + expectedStatus: string; + updatedAt: string | null; + subscriptionInfo: SubScriptionInfo | null; +}; diff --git a/crates/tauri-plugin-mihomo/dist-js/bindings/ProxyProviders.d.ts b/crates/tauri-plugin-mihomo/dist-js/bindings/ProxyProviders.d.ts new file mode 100644 index 000000000..3f4f243f5 --- /dev/null +++ b/crates/tauri-plugin-mihomo/dist-js/bindings/ProxyProviders.d.ts @@ -0,0 +1,6 @@ +import type { ProxyProvider } from "./ProxyProvider"; +export type ProxyProviders = { + providers: { + [key in string]?: ProxyProvider; + }; +}; diff --git a/crates/tauri-plugin-mihomo/dist-js/bindings/ProxyType.d.ts b/crates/tauri-plugin-mihomo/dist-js/bindings/ProxyType.d.ts new file mode 100644 index 000000000..dfafc2935 --- /dev/null +++ b/crates/tauri-plugin-mihomo/dist-js/bindings/ProxyType.d.ts @@ -0,0 +1 @@ +export type ProxyType = "Direct" | "Reject" | "RejectDrop" | "Compatible" | "Pass" | "Dns" | "Shadowsocks" | "ShadowsocksR" | "Snell" | "Socks5" | "Http" | "Vmess" | "Vless" | "Trojan" | "Hysteria" | "Hysteria2" | "WireGuard" | "Tuic" | "Ssh" | "Mieru" | "AnyTLS" | "Relay" | "Selector" | "Fallback" | "URLTest" | "LoadBalance"; diff --git a/crates/tauri-plugin-mihomo/dist-js/bindings/Rule.d.ts b/crates/tauri-plugin-mihomo/dist-js/bindings/Rule.d.ts new file mode 100644 index 000000000..48d9f727f --- /dev/null +++ b/crates/tauri-plugin-mihomo/dist-js/bindings/Rule.d.ts @@ -0,0 +1,7 @@ +import type { RuleType } from "./RuleType"; +export type Rule = { + type: RuleType; + payload: string; + proxy: string; + size: number; +}; diff --git a/crates/tauri-plugin-mihomo/dist-js/bindings/RuleBehavior.d.ts b/crates/tauri-plugin-mihomo/dist-js/bindings/RuleBehavior.d.ts new file mode 100644 index 000000000..6894251d9 --- /dev/null +++ b/crates/tauri-plugin-mihomo/dist-js/bindings/RuleBehavior.d.ts @@ -0,0 +1 @@ +export type RuleBehavior = "Domain" | "IPCIDR" | "Classical"; diff --git a/crates/tauri-plugin-mihomo/dist-js/bindings/RuleFormat.d.ts b/crates/tauri-plugin-mihomo/dist-js/bindings/RuleFormat.d.ts new file mode 100644 index 000000000..ae16d7fe8 --- /dev/null +++ b/crates/tauri-plugin-mihomo/dist-js/bindings/RuleFormat.d.ts @@ -0,0 +1 @@ +export type RuleFormat = "YamlRule" | "TextRule" | "MrsRule"; diff --git a/crates/tauri-plugin-mihomo/dist-js/bindings/RuleProvider.d.ts b/crates/tauri-plugin-mihomo/dist-js/bindings/RuleProvider.d.ts new file mode 100644 index 000000000..9b41593a9 --- /dev/null +++ b/crates/tauri-plugin-mihomo/dist-js/bindings/RuleProvider.d.ts @@ -0,0 +1,13 @@ +import type { ProviderType } from "./ProviderType"; +import type { RuleBehavior } from "./RuleBehavior"; +import type { RuleFormat } from "./RuleFormat"; +import type { VehicleType } from "./VehicleType"; +export type RuleProvider = { + behavior: RuleBehavior; + format: RuleFormat; + name: string; + ruleCount: number; + type: ProviderType; + updatedAt: string; + vehicleType: VehicleType; +}; diff --git a/crates/tauri-plugin-mihomo/dist-js/bindings/RuleProviders.d.ts b/crates/tauri-plugin-mihomo/dist-js/bindings/RuleProviders.d.ts new file mode 100644 index 000000000..62dcb525a --- /dev/null +++ b/crates/tauri-plugin-mihomo/dist-js/bindings/RuleProviders.d.ts @@ -0,0 +1,6 @@ +import type { RuleProvider } from "./RuleProvider"; +export type RuleProviders = { + providers: { + [key in string]?: RuleProvider; + }; +}; diff --git a/crates/tauri-plugin-mihomo/dist-js/bindings/RuleType.d.ts b/crates/tauri-plugin-mihomo/dist-js/bindings/RuleType.d.ts new file mode 100644 index 000000000..0a128ac4f --- /dev/null +++ b/crates/tauri-plugin-mihomo/dist-js/bindings/RuleType.d.ts @@ -0,0 +1 @@ +export type RuleType = "Domain" | "DomainSuffix" | "DomainKeyword" | "DomainRegex" | "GeoSite" | "GeoIP" | "SrcGeoIP" | "IPASN" | "SrcIPASN" | "IPCIDR" | "SrcIPCIDR" | "IPSuffix" | "SrcIPSuffix" | "SrcPort" | "DstPort" | "InPort" | "InUser" | "InName" | "InType" | "ProcessName" | "ProcessPath" | "ProcessNameRegex" | "ProcessPathRegex" | "Match" | "RuleSet" | "Network" | "DSCP" | "Uid" | "SubRules" | "AND" | "OR" | "NOT"; diff --git a/crates/tauri-plugin-mihomo/dist-js/bindings/Rules.d.ts b/crates/tauri-plugin-mihomo/dist-js/bindings/Rules.d.ts new file mode 100644 index 000000000..f20557799 --- /dev/null +++ b/crates/tauri-plugin-mihomo/dist-js/bindings/Rules.d.ts @@ -0,0 +1,7 @@ +import type { Rule } from "./Rule"; +/** + * rules + */ +export type Rules = { + rules: Array; +}; diff --git a/crates/tauri-plugin-mihomo/dist-js/bindings/SubScriptionInfo.d.ts b/crates/tauri-plugin-mihomo/dist-js/bindings/SubScriptionInfo.d.ts new file mode 100644 index 000000000..148a3ec8e --- /dev/null +++ b/crates/tauri-plugin-mihomo/dist-js/bindings/SubScriptionInfo.d.ts @@ -0,0 +1,6 @@ +export type SubScriptionInfo = { + Upload: bigint; + Download: bigint; + Total: bigint; + Expire: bigint; +}; diff --git a/crates/tauri-plugin-mihomo/dist-js/bindings/Traffic.d.ts b/crates/tauri-plugin-mihomo/dist-js/bindings/Traffic.d.ts new file mode 100644 index 000000000..953ac9816 --- /dev/null +++ b/crates/tauri-plugin-mihomo/dist-js/bindings/Traffic.d.ts @@ -0,0 +1,4 @@ +export type Traffic = { + up: bigint; + down: bigint; +}; diff --git a/crates/tauri-plugin-mihomo/dist-js/bindings/TuicServer.d.ts b/crates/tauri-plugin-mihomo/dist-js/bindings/TuicServer.d.ts new file mode 100644 index 000000000..82baf657a --- /dev/null +++ b/crates/tauri-plugin-mihomo/dist-js/bindings/TuicServer.d.ts @@ -0,0 +1,20 @@ +import type { MuxOption } from "./MuxOption"; +export type TuicServer = { + enable: boolean; + listen: string; + token?: Array; + users?: { + [key in string]?: string; + }; + certificate: string; + privateKey: string; + echKey: string; + congestionController?: string; + maxIdleTime?: number; + authenticationTimeout?: number; + alpn?: Array; + maxUdpRelayPacketSize?: number; + maxDatagramFrameSize?: number; + cwnd?: number; + muxOption?: MuxOption; +}; diff --git a/crates/tauri-plugin-mihomo/dist-js/bindings/TunConfig.d.ts b/crates/tauri-plugin-mihomo/dist-js/bindings/TunConfig.d.ts new file mode 100644 index 000000000..33d95e591 --- /dev/null +++ b/crates/tauri-plugin-mihomo/dist-js/bindings/TunConfig.d.ts @@ -0,0 +1,47 @@ +import type { TunStack } from "./TunStack"; +export type TunConfig = { + enable: boolean; + device: string; + stack: TunStack; + dnsHijack: Array; + autoRoute: boolean; + autoDetectInterface: boolean; + mtu?: number; + gso?: boolean; + gsoMaxSize?: number; + inet4Address?: Array; + inet6Address?: Array; + iproute2TableIndex?: number; + iproute2RuleIndex?: number; + autoRedirect?: boolean; + autoRedirectInputMark?: number; + autoRedirectOutputMark?: number; + loopbackAddress?: string; + strictRoute?: boolean; + routeAddress?: Array; + routeAddressSet?: Array; + routeExcludeAddress?: Array; + routeExcludeAddressSet?: Array; + includeInterface?: Array; + excludeInterface?: Array; + includeUid?: Array; + includeUidRange?: Array; + excludeUid?: Array; + excludeUidRange?: Array; + excludeSrcPort?: Array; + excludeSrcPortRange?: Array; + excludeDstPort?: Array; + excludeDstPortRange?: Array; + includeAndroidUser?: Array; + includePackage?: Array; + excludePackage?: Array; + endpointIndependentNat?: boolean; + udpTimeout?: bigint; + fileDescriptor: number; + inet4RouteAddress?: Array; + inet6RouteAddress?: Array; + inet4RouteExcludeAddress?: Array; + inet6RouteExcludeAddress?: Array; + recvmsgx?: boolean; + sendmsgx?: boolean; +}; diff --git a/crates/tauri-plugin-mihomo/dist-js/bindings/TunStack.d.ts b/crates/tauri-plugin-mihomo/dist-js/bindings/TunStack.d.ts new file mode 100644 index 000000000..b169e5d87 --- /dev/null +++ b/crates/tauri-plugin-mihomo/dist-js/bindings/TunStack.d.ts @@ -0,0 +1,4 @@ +/** + * tun stack enum + */ +export type TunStack = "Mixed" | "gVisor" | "System"; diff --git a/crates/tauri-plugin-mihomo/dist-js/bindings/VehicleType.d.ts b/crates/tauri-plugin-mihomo/dist-js/bindings/VehicleType.d.ts new file mode 100644 index 000000000..15d50e751 --- /dev/null +++ b/crates/tauri-plugin-mihomo/dist-js/bindings/VehicleType.d.ts @@ -0,0 +1 @@ +export type VehicleType = "File" | "HTTP" | "Compatible" | "Inline"; diff --git a/crates/tauri-plugin-mihomo/dist-js/bindings/index.d.ts b/crates/tauri-plugin-mihomo/dist-js/bindings/index.d.ts new file mode 100644 index 000000000..ddf91a6d0 --- /dev/null +++ b/crates/tauri-plugin-mihomo/dist-js/bindings/index.d.ts @@ -0,0 +1,40 @@ +export * from "./BaseConfig"; +export * from "./BrutalOption"; +export * from "./ClashMode"; +export * from "./Connection"; +export * from "./ConnectionMetaData"; +export * from "./Connections"; +export * from "./ConnectionType"; +export * from "./CoreUpdaterChannel"; +export * from "./DelayHistory"; +export * from "./DNSMode"; +export * from "./Extra"; +export * from "./FindProcessMode"; +export * from "./GeoXUrl"; +export * from "./Groups"; +export * from "./Log"; +export * from "./LogLevel"; +export * from "./Memory"; +export * from "./MihomoVersion"; +export * from "./MuxOption"; +export * from "./Network"; +export * from "./ProviderType"; +export * from "./Proxies"; +export * from "./Proxy"; +export * from "./ProxyDelay"; +export * from "./ProxyProvider"; +export * from "./ProxyProviders"; +export * from "./ProxyType"; +export * from "./Rule"; +export * from "./RuleBehavior"; +export * from "./RuleFormat"; +export * from "./RuleProvider"; +export * from "./RuleProviders"; +export * from "./Rules"; +export * from "./RuleType"; +export * from "./SubScriptionInfo"; +export * from "./Traffic"; +export * from "./TuicServer"; +export * from "./TunConfig"; +export * from "./TunStack"; +export * from "./VehicleType"; diff --git a/crates/tauri-plugin-mihomo/dist-js/index.cjs b/crates/tauri-plugin-mihomo/dist-js/index.cjs new file mode 100644 index 000000000..5f349b5ad --- /dev/null +++ b/crates/tauri-plugin-mihomo/dist-js/index.cjs @@ -0,0 +1,474 @@ +'use strict'; + +var core = require('@tauri-apps/api/core'); + +// ======================= functions ======================= +/** + * 更新控制器地址 + * @param controller 控制器地址, 例如:127.0.0.1:9090 + */ +async function updateController(controller) { + const [host, portStr] = controller.trim().split(":"); + const port = parseInt(portStr); + await core.invoke("plugin:mihomo|update_controller", { host, port }); +} +/** + * 更新控制器的密钥 + * @param secret 控制器的密钥 + */ +async function updateSecret(secret) { + await core.invoke("plugin:mihomo|update_secret", { secret }); +} +/** + * 获取Mihomo版本信息 + */ +async function getVersion() { + return await core.invoke("plugin:mihomo|get_version"); +} +/** + * 清除 FakeIP 缓存 + */ +async function flushFakeIp() { + await core.invoke("plugin:mihomo|flush_fakeip"); +} +/** + * 清除 DNS 缓存 + */ +async function flushDNS() { + await core.invoke("plugin:mihomo|flush_dns"); +} +// connections +/** + * 获取所有连接信息 + * @returns 所有连接信息 + */ +async function getConnections() { + return await core.invoke("plugin:mihomo|get_connections"); +} +/** + * 关闭所有连接 + */ +async function closeAllConnections() { + await core.invoke("plugin:mihomo|close_all_connections"); +} +/** + * 关闭指定连接 + * @param connectionId 连接 ID + */ +async function closeConnections(connectionId) { + await core.invoke("plugin:mihomo|close_connections", { connectionId }); +} +// groups +/** + * 获取所有代理组信息 + * @returns 所有代理组信息 + */ +async function getGroups() { + return await core.invoke("plugin:mihomo|get_groups"); +} +/** + * 获取指定代理组信息 + * @param groupName 代理组名称 + * @returns 指定代理组信息 + */ +async function getGroupByName(groupName) { + return await core.invoke("plugin:mihomo|get_group_by_name", { + groupName, + }); +} +/** + * 对指定代理组进行延迟测试 + * + * 注:返回值中不包含超时的节点 + * @param groupName 代理组名称 + * @param testUrl 测试 url + * @param timeout 超时时间(毫秒) + * @param keepFixed 是否保留已固定的节点, 默认 false + * @returns 代理组里代理节点的延迟 + */ +async function delayGroup(groupName, testUrl, timeout, keepFixed = false) { + return await core.invoke("plugin:mihomo|delay_group", { + groupName, + testUrl, + timeout, + keepFixed, + }); +} +// providers +/** + * 获取所有代理提供者信息 + * @returns 所有代理提供者信息 + */ +async function getProxyProviders() { + return await core.invoke("plugin:mihomo|get_proxy_providers"); +} +/** + * 获取指定的代理提供者信息 + * @param providerName 代理提供者名称 + * @returns 代理提供者信息 + */ +async function getProxyProviderByName(providerName) { + return await core.invoke("plugin:mihomo|get_proxy_provider_by_name", { providerName }); +} +/** + * 更新代理提供者信息 + * @param providerName 代理提供者名称 + */ +async function updateProxyProvider(providerName) { + await core.invoke("plugin:mihomo|update_proxy_provider", { + providerName, + }); +} +/** + * 对指定的代理提供者进行健康检查 + * @param providerName 代理提供者名称 + */ +async function healthcheckProxyProvider(providerName) { + await core.invoke("plugin:mihomo|healthcheck_proxy_provider", { + providerName, + }); +} +/** + * 对指定代理提供者下的指定节点(非代理组)进行健康检查, 并返回新的延迟信息 + * @param providerName 代理提供者名称 + * @param proxyName 代理节点名称 (非代理组) + * @param testUrl 测试 url + * @param timeout 超时时间 + * @returns 该代理节点的延迟 + */ +async function healthcheckNodeInProvider(providerName, proxyName, testUrl, timeout) { + return await core.invoke("plugin:mihomo|healthcheck_node_in_provider", { + providerName, + proxyName, + testUrl, + timeout, + }); +} +// proxies +/** + * 获取所有代理信息 + * @returns 所有代理信息 + */ +async function getProxies() { + return await core.invoke("plugin:mihomo|get_proxies"); +} +/** + * 获取指定代理信息 + * @param proxyName 代理名称 + * @returns 代理信息 + */ +async function getProxyByName(proxyName) { + return await core.invoke("plugin:mihomo|get_proxy_by_name", { + proxiesName: proxyName, + }); +} +/** + * 为指定代理选择节点 + * + * 一般为指定代理组下使用指定的代理节点 【代理组/节点】 + * @param groupName 代理组名称 + * @param node 代理节点 + */ +async function selectNodeForGroup(groupName, node) { + await core.invoke("plugin:mihomo|select_node_for_group", { + groupName, + node, + }); +} +/** + * 指定代理组下不再使用固定的代理节点 + * + * 一般用于自动选择的代理组(例如:URLTest 类型的代理组)下的节点 + * @param groupName 代理组名称 + */ +async function unfixedProxy(groupName) { + await core.invoke("plugin:mihomo|unfixed_proxy", { + groupName, + }); +} +/** + * 对指定代理进行延迟测试 + * + * 一般用于代理节点的延迟测试,也可传代理组名称(只会测试代理组下选中的代理节点) + * @param proxyName 代理节点名称 + * @param testUrl 测试 url + * @param timeout 超时时间 + * @returns 该代理节点的延迟信息 + */ +async function delayProxyByName(proxyName, testUrl, timeout) { + return await core.invoke("plugin:mihomo|delay_proxy_by_name", { + proxyName, + testUrl, + timeout, + }); +} +// rules +/** + * 获取所有规则信息 + * @returns 所有规则信息 + */ +async function getRules() { + return await core.invoke("plugin:mihomo|get_rules"); +} +/** + * 获取所有规则提供者信息 + * @returns 所有规则提供者信息 + */ +async function getRuleProviders() { + return await core.invoke("plugin:mihomo|get_rule_providers"); +} +/** + * 更新规则提供者信息 + * @param providerName 规则提供者名称 + */ +async function updateRuleProvider(providerName) { + await core.invoke("plugin:mihomo|update_rule_provider", { + providerName, + }); +} +// runtime config +/** + * 获取基础配置 + * @returns 基础配置 + */ +async function getBaseConfig() { + return await core.invoke("plugin:mihomo|get_base_config"); +} +/** + * 重新加载配置 + * @param force 强制更新 + * @param configPath 配置文件路径 + */ +async function reloadConfig(force, configPath) { + await core.invoke("plugin:mihomo|reload_config", { + force, + configPath, + }); +} +/** + * 更改基础配置 + * @param data 基础配置更改后的内容, 例如:{"tun": {"enabled": true}} + */ +async function patchBaseConfig(data) { + await core.invoke("plugin:mihomo|patch_base_config", { + data, + }); +} +/** + * 更新 Geo + */ +async function updateGeo() { + await core.invoke("plugin:mihomo|update_geo"); +} +/** + * 重启核心 + */ +async function restart() { + await core.invoke("plugin:mihomo|restart"); +} +// upgrade +/** + * 升级核心,将当前运行中的核心升级到选择的通道的最新版 + * @param channel 升级通道, 默认 auto + * - release: 稳定版 + * - alpha: 测试版 + * - auto: 根据当前运行的核心版本自动选择升级通道 + * @param force 是否强制升级,默认 false + * - false: 若当前版本为最新版,返回当前为最新版的错误,不再执行升级操作, 否则下载最新版,覆盖升级 + * - true: 直接下载最新版,强制覆盖升级 + */ +async function upgradeCore(channel = "auto", force = false) { + await core.invoke("plugin:mihomo|upgrade_core", { channel, force }); +} +/** + * 更新 UI + */ +async function upgradeUi() { + await core.invoke("plugin:mihomo|upgrade_ui"); +} +/** + * 更新 Geo + */ +async function upgradeGeo() { + await core.invoke("plugin:mihomo|upgrade_geo"); +} +/** + * 清除 Rust 侧中所有的 WebSocket 连接 + */ +async function clearAllWsConnections() { + await core.invoke("plugin:mihomo|clear_all_ws_connections"); +} +class MihomoWebSocket { + constructor(id, listeners) { + this.id = id; + this.listeners = listeners; + } + /** + * 创建一个新的 WebSocket 连接,用于 Mihomo 的流量监控 + * @returns WebSocket 实例 + */ + static async connect_traffic() { + const listeners = new Set(); + const onMessage = new core.Channel(); + onMessage.onmessage = (message) => { + listeners.forEach((l) => { + l(message); + }); + }; + const id = await core.invoke("plugin:mihomo|ws_traffic", { + onMessage, + }); + const instance = new MihomoWebSocket(id, listeners); + MihomoWebSocket.instances.add(instance); + return instance; + } + /** + * 创建一个新的 WebSocket 连接,用于 Mihomo 的内存监控 + * @returns WebSocket 实例 + */ + static async connect_memory() { + const listeners = new Set(); + const onMessage = new core.Channel(); + onMessage.onmessage = (message) => { + listeners.forEach((l) => { + l(message); + }); + }; + const id = await core.invoke("plugin:mihomo|ws_memory", { + onMessage, + }); + const instance = new MihomoWebSocket(id, listeners); + MihomoWebSocket.instances.add(instance); + return instance; + } + /** + * 创建一个新的 WebSocket 连接,用于 Mihomo 的连接监控 + * @returns WebSocket 实例 + */ + static async connect_connections() { + const listeners = new Set(); + const onMessage = new core.Channel(); + onMessage.onmessage = (message) => { + listeners.forEach((l) => { + l(message); + }); + }; + const id = await core.invoke("plugin:mihomo|ws_connections", { + onMessage, + }); + const instance = new MihomoWebSocket(id, listeners); + MihomoWebSocket.instances.add(instance); + return instance; + } + /** + * 创建一个新的 WebSocket 连接,用于 Mihomo 的日志监控 + * @returns WebSocket 实例 + */ + static async connect_logs(level) { + const listeners = new Set(); + const onMessage = new core.Channel(); + onMessage.onmessage = (message) => { + listeners.forEach((l) => { + l(message); + }); + }; + const id = await core.invoke("plugin:mihomo|ws_logs", { + level, + onMessage, + }); + const instance = new MihomoWebSocket(id, listeners); + MihomoWebSocket.instances.add(instance); + return instance; + } + /** + * 添加处理 WebSocket 连接后接受的数据的回调函数 + * @param cb 回调函数 + */ + addListener(cb) { + this.listeners.add(cb); + return () => { + this.listeners.delete(cb); + }; + } + // /** + // * 发送消息到 WebSocket 连接 + // * @param message 发送的消息 + // */ + // async send(message: Message | string | number[]): Promise { + // let m: Message; + // if (typeof message === "string") { + // m = { type: "Text", data: message }; + // } else if (typeof message === "object" && "type" in message) { + // m = message; + // } else if (Array.isArray(message)) { + // m = { type: "Binary", data: message }; + // } else { + // throw new Error( + // "invalid `message` type, expected a `{ type: string, data: any }` object, a string or a numeric array", + // ); + // } + // await invoke("plugin:mihomo|ws_send", { id: this.id, message: m }); + // } + /** + * 关闭 WebSocket 连接 + * @param forceTimeout 强制关闭 WebSocket 连接等待的时间,单位: 毫秒, 默认为 0 + */ + async close() { + try { + await core.invoke("plugin:mihomo|ws_disconnect", { + id: this.id, + forceTimeout: 0, + }); + } + catch (ignore) { + // ignore + } + finally { + MihomoWebSocket.instances.delete(this); + } + } + /** + * 清理全部的 websocket 连接资源 + */ + static async cleanupAll() { + await Promise.all(Array.from(MihomoWebSocket.instances).map((instance) => instance.close())); + this.instances.clear(); + await clearAllWsConnections(); + } +} +MihomoWebSocket.instances = new Set(); + +exports.MihomoWebSocket = MihomoWebSocket; +exports.clearAllWsConnections = clearAllWsConnections; +exports.closeAllConnections = closeAllConnections; +exports.closeConnections = closeConnections; +exports.delayGroup = delayGroup; +exports.delayProxyByName = delayProxyByName; +exports.flushDNS = flushDNS; +exports.flushFakeIp = flushFakeIp; +exports.getBaseConfig = getBaseConfig; +exports.getConnections = getConnections; +exports.getGroupByName = getGroupByName; +exports.getGroups = getGroups; +exports.getProxies = getProxies; +exports.getProxyByName = getProxyByName; +exports.getProxyProviderByName = getProxyProviderByName; +exports.getProxyProviders = getProxyProviders; +exports.getRuleProviders = getRuleProviders; +exports.getRules = getRules; +exports.getVersion = getVersion; +exports.healthcheckNodeInProvider = healthcheckNodeInProvider; +exports.healthcheckProxyProvider = healthcheckProxyProvider; +exports.patchBaseConfig = patchBaseConfig; +exports.reloadConfig = reloadConfig; +exports.restart = restart; +exports.selectNodeForGroup = selectNodeForGroup; +exports.unfixedProxy = unfixedProxy; +exports.updateController = updateController; +exports.updateGeo = updateGeo; +exports.updateProxyProvider = updateProxyProvider; +exports.updateRuleProvider = updateRuleProvider; +exports.updateSecret = updateSecret; +exports.upgradeCore = upgradeCore; +exports.upgradeGeo = upgradeGeo; +exports.upgradeUi = upgradeUi; diff --git a/crates/tauri-plugin-mihomo/dist-js/index.d.ts b/crates/tauri-plugin-mihomo/dist-js/index.d.ts new file mode 100644 index 000000000..603f7957d --- /dev/null +++ b/crates/tauri-plugin-mihomo/dist-js/index.d.ts @@ -0,0 +1,238 @@ +import { BaseConfig, Connections, CoreUpdaterChannel, Groups, LogLevel, MihomoVersion, Proxies, Proxy, ProxyDelay, ProxyProvider, ProxyProviders, RuleProviders, Rules } from "./bindings"; +export * from "./bindings"; +export type MihomoGroupDelay = Record; +/** + * 更新控制器地址 + * @param controller 控制器地址, 例如:127.0.0.1:9090 + */ +export declare function updateController(controller: string): Promise; +/** + * 更新控制器的密钥 + * @param secret 控制器的密钥 + */ +export declare function updateSecret(secret: string): Promise; +/** + * 获取Mihomo版本信息 + */ +export declare function getVersion(): Promise; +/** + * 清除 FakeIP 缓存 + */ +export declare function flushFakeIp(): Promise; +/** + * 清除 DNS 缓存 + */ +export declare function flushDNS(): Promise; +/** + * 获取所有连接信息 + * @returns 所有连接信息 + */ +export declare function getConnections(): Promise; +/** + * 关闭所有连接 + */ +export declare function closeAllConnections(): Promise; +/** + * 关闭指定连接 + * @param connectionId 连接 ID + */ +export declare function closeConnections(connectionId: string): Promise; +/** + * 获取所有代理组信息 + * @returns 所有代理组信息 + */ +export declare function getGroups(): Promise; +/** + * 获取指定代理组信息 + * @param groupName 代理组名称 + * @returns 指定代理组信息 + */ +export declare function getGroupByName(groupName: string): Promise; +/** + * 对指定代理组进行延迟测试 + * + * 注:返回值中不包含超时的节点 + * @param groupName 代理组名称 + * @param testUrl 测试 url + * @param timeout 超时时间(毫秒) + * @param keepFixed 是否保留已固定的节点, 默认 false + * @returns 代理组里代理节点的延迟 + */ +export declare function delayGroup(groupName: string, testUrl: string, timeout: number, keepFixed?: boolean): Promise; +/** + * 获取所有代理提供者信息 + * @returns 所有代理提供者信息 + */ +export declare function getProxyProviders(): Promise; +/** + * 获取指定的代理提供者信息 + * @param providerName 代理提供者名称 + * @returns 代理提供者信息 + */ +export declare function getProxyProviderByName(providerName: string): Promise; +/** + * 更新代理提供者信息 + * @param providerName 代理提供者名称 + */ +export declare function updateProxyProvider(providerName: string): Promise; +/** + * 对指定的代理提供者进行健康检查 + * @param providerName 代理提供者名称 + */ +export declare function healthcheckProxyProvider(providerName: string): Promise; +/** + * 对指定代理提供者下的指定节点(非代理组)进行健康检查, 并返回新的延迟信息 + * @param providerName 代理提供者名称 + * @param proxyName 代理节点名称 (非代理组) + * @param testUrl 测试 url + * @param timeout 超时时间 + * @returns 该代理节点的延迟 + */ +export declare function healthcheckNodeInProvider(providerName: string, proxyName: string, testUrl: string, timeout: number): Promise; +/** + * 获取所有代理信息 + * @returns 所有代理信息 + */ +export declare function getProxies(): Promise; +/** + * 获取指定代理信息 + * @param proxyName 代理名称 + * @returns 代理信息 + */ +export declare function getProxyByName(proxyName: string): Promise; +/** + * 为指定代理选择节点 + * + * 一般为指定代理组下使用指定的代理节点 【代理组/节点】 + * @param groupName 代理组名称 + * @param node 代理节点 + */ +export declare function selectNodeForGroup(groupName: string, node: string): Promise; +/** + * 指定代理组下不再使用固定的代理节点 + * + * 一般用于自动选择的代理组(例如:URLTest 类型的代理组)下的节点 + * @param groupName 代理组名称 + */ +export declare function unfixedProxy(groupName: string): Promise; +/** + * 对指定代理进行延迟测试 + * + * 一般用于代理节点的延迟测试,也可传代理组名称(只会测试代理组下选中的代理节点) + * @param proxyName 代理节点名称 + * @param testUrl 测试 url + * @param timeout 超时时间 + * @returns 该代理节点的延迟信息 + */ +export declare function delayProxyByName(proxyName: string, testUrl: string, timeout: number): Promise; +/** + * 获取所有规则信息 + * @returns 所有规则信息 + */ +export declare function getRules(): Promise; +/** + * 获取所有规则提供者信息 + * @returns 所有规则提供者信息 + */ +export declare function getRuleProviders(): Promise; +/** + * 更新规则提供者信息 + * @param providerName 规则提供者名称 + */ +export declare function updateRuleProvider(providerName: string): Promise; +/** + * 获取基础配置 + * @returns 基础配置 + */ +export declare function getBaseConfig(): Promise; +/** + * 重新加载配置 + * @param force 强制更新 + * @param configPath 配置文件路径 + */ +export declare function reloadConfig(force: boolean, configPath: string): Promise; +/** + * 更改基础配置 + * @param data 基础配置更改后的内容, 例如:{"tun": {"enabled": true}} + */ +export declare function patchBaseConfig(data: Record): Promise; +/** + * 更新 Geo + */ +export declare function updateGeo(): Promise; +/** + * 重启核心 + */ +export declare function restart(): Promise; +/** + * 升级核心,将当前运行中的核心升级到选择的通道的最新版 + * @param channel 升级通道, 默认 auto + * - release: 稳定版 + * - alpha: 测试版 + * - auto: 根据当前运行的核心版本自动选择升级通道 + * @param force 是否强制升级,默认 false + * - false: 若当前版本为最新版,返回当前为最新版的错误,不再执行升级操作, 否则下载最新版,覆盖升级 + * - true: 直接下载最新版,强制覆盖升级 + */ +export declare function upgradeCore(channel?: CoreUpdaterChannel, force?: boolean): Promise; +/** + * 更新 UI + */ +export declare function upgradeUi(): Promise; +/** + * 更新 Geo + */ +export declare function upgradeGeo(): Promise; +/** + * 清除 Rust 侧中所有的 WebSocket 连接 + */ +export declare function clearAllWsConnections(): Promise; +export interface MessageKind { + type: T; + data: D; +} +export interface CloseFrame { + code: number; + reason: string; +} +export type Message = MessageKind<"Text", string> | MessageKind<"Binary", number[]> | MessageKind<"Ping", number[]> | MessageKind<"Pong", number[]> | MessageKind<"Close", CloseFrame | null>; +export declare class MihomoWebSocket { + id: number; + private readonly listeners; + private static instances; + constructor(id: number, listeners: Set<(arg: Message) => void>); + /** + * 创建一个新的 WebSocket 连接,用于 Mihomo 的流量监控 + * @returns WebSocket 实例 + */ + static connect_traffic(): Promise; + /** + * 创建一个新的 WebSocket 连接,用于 Mihomo 的内存监控 + * @returns WebSocket 实例 + */ + static connect_memory(): Promise; + /** + * 创建一个新的 WebSocket 连接,用于 Mihomo 的连接监控 + * @returns WebSocket 实例 + */ + static connect_connections(): Promise; + /** + * 创建一个新的 WebSocket 连接,用于 Mihomo 的日志监控 + * @returns WebSocket 实例 + */ + static connect_logs(level: LogLevel): Promise; + /** + * 添加处理 WebSocket 连接后接受的数据的回调函数 + * @param cb 回调函数 + */ + addListener(cb: (arg: Message) => void): () => void; + /** + * 关闭 WebSocket 连接 + * @param forceTimeout 强制关闭 WebSocket 连接等待的时间,单位: 毫秒, 默认为 0 + */ + close(): Promise; + /** + * 清理全部的 websocket 连接资源 + */ + static cleanupAll(): Promise; +} diff --git a/crates/tauri-plugin-mihomo/dist-js/index.js b/crates/tauri-plugin-mihomo/dist-js/index.js new file mode 100644 index 000000000..bebf41381 --- /dev/null +++ b/crates/tauri-plugin-mihomo/dist-js/index.js @@ -0,0 +1,439 @@ +import { invoke, Channel } from '@tauri-apps/api/core'; + +// ======================= functions ======================= +/** + * 更新控制器地址 + * @param controller 控制器地址, 例如:127.0.0.1:9090 + */ +async function updateController(controller) { + const [host, portStr] = controller.trim().split(":"); + const port = parseInt(portStr); + await invoke("plugin:mihomo|update_controller", { host, port }); +} +/** + * 更新控制器的密钥 + * @param secret 控制器的密钥 + */ +async function updateSecret(secret) { + await invoke("plugin:mihomo|update_secret", { secret }); +} +/** + * 获取Mihomo版本信息 + */ +async function getVersion() { + return await invoke("plugin:mihomo|get_version"); +} +/** + * 清除 FakeIP 缓存 + */ +async function flushFakeIp() { + await invoke("plugin:mihomo|flush_fakeip"); +} +/** + * 清除 DNS 缓存 + */ +async function flushDNS() { + await invoke("plugin:mihomo|flush_dns"); +} +// connections +/** + * 获取所有连接信息 + * @returns 所有连接信息 + */ +async function getConnections() { + return await invoke("plugin:mihomo|get_connections"); +} +/** + * 关闭所有连接 + */ +async function closeAllConnections() { + await invoke("plugin:mihomo|close_all_connections"); +} +/** + * 关闭指定连接 + * @param connectionId 连接 ID + */ +async function closeConnections(connectionId) { + await invoke("plugin:mihomo|close_connections", { connectionId }); +} +// groups +/** + * 获取所有代理组信息 + * @returns 所有代理组信息 + */ +async function getGroups() { + return await invoke("plugin:mihomo|get_groups"); +} +/** + * 获取指定代理组信息 + * @param groupName 代理组名称 + * @returns 指定代理组信息 + */ +async function getGroupByName(groupName) { + return await invoke("plugin:mihomo|get_group_by_name", { + groupName, + }); +} +/** + * 对指定代理组进行延迟测试 + * + * 注:返回值中不包含超时的节点 + * @param groupName 代理组名称 + * @param testUrl 测试 url + * @param timeout 超时时间(毫秒) + * @param keepFixed 是否保留已固定的节点, 默认 false + * @returns 代理组里代理节点的延迟 + */ +async function delayGroup(groupName, testUrl, timeout, keepFixed = false) { + return await invoke("plugin:mihomo|delay_group", { + groupName, + testUrl, + timeout, + keepFixed, + }); +} +// providers +/** + * 获取所有代理提供者信息 + * @returns 所有代理提供者信息 + */ +async function getProxyProviders() { + return await invoke("plugin:mihomo|get_proxy_providers"); +} +/** + * 获取指定的代理提供者信息 + * @param providerName 代理提供者名称 + * @returns 代理提供者信息 + */ +async function getProxyProviderByName(providerName) { + return await invoke("plugin:mihomo|get_proxy_provider_by_name", { providerName }); +} +/** + * 更新代理提供者信息 + * @param providerName 代理提供者名称 + */ +async function updateProxyProvider(providerName) { + await invoke("plugin:mihomo|update_proxy_provider", { + providerName, + }); +} +/** + * 对指定的代理提供者进行健康检查 + * @param providerName 代理提供者名称 + */ +async function healthcheckProxyProvider(providerName) { + await invoke("plugin:mihomo|healthcheck_proxy_provider", { + providerName, + }); +} +/** + * 对指定代理提供者下的指定节点(非代理组)进行健康检查, 并返回新的延迟信息 + * @param providerName 代理提供者名称 + * @param proxyName 代理节点名称 (非代理组) + * @param testUrl 测试 url + * @param timeout 超时时间 + * @returns 该代理节点的延迟 + */ +async function healthcheckNodeInProvider(providerName, proxyName, testUrl, timeout) { + return await invoke("plugin:mihomo|healthcheck_node_in_provider", { + providerName, + proxyName, + testUrl, + timeout, + }); +} +// proxies +/** + * 获取所有代理信息 + * @returns 所有代理信息 + */ +async function getProxies() { + return await invoke("plugin:mihomo|get_proxies"); +} +/** + * 获取指定代理信息 + * @param proxyName 代理名称 + * @returns 代理信息 + */ +async function getProxyByName(proxyName) { + return await invoke("plugin:mihomo|get_proxy_by_name", { + proxiesName: proxyName, + }); +} +/** + * 为指定代理选择节点 + * + * 一般为指定代理组下使用指定的代理节点 【代理组/节点】 + * @param groupName 代理组名称 + * @param node 代理节点 + */ +async function selectNodeForGroup(groupName, node) { + await invoke("plugin:mihomo|select_node_for_group", { + groupName, + node, + }); +} +/** + * 指定代理组下不再使用固定的代理节点 + * + * 一般用于自动选择的代理组(例如:URLTest 类型的代理组)下的节点 + * @param groupName 代理组名称 + */ +async function unfixedProxy(groupName) { + await invoke("plugin:mihomo|unfixed_proxy", { + groupName, + }); +} +/** + * 对指定代理进行延迟测试 + * + * 一般用于代理节点的延迟测试,也可传代理组名称(只会测试代理组下选中的代理节点) + * @param proxyName 代理节点名称 + * @param testUrl 测试 url + * @param timeout 超时时间 + * @returns 该代理节点的延迟信息 + */ +async function delayProxyByName(proxyName, testUrl, timeout) { + return await invoke("plugin:mihomo|delay_proxy_by_name", { + proxyName, + testUrl, + timeout, + }); +} +// rules +/** + * 获取所有规则信息 + * @returns 所有规则信息 + */ +async function getRules() { + return await invoke("plugin:mihomo|get_rules"); +} +/** + * 获取所有规则提供者信息 + * @returns 所有规则提供者信息 + */ +async function getRuleProviders() { + return await invoke("plugin:mihomo|get_rule_providers"); +} +/** + * 更新规则提供者信息 + * @param providerName 规则提供者名称 + */ +async function updateRuleProvider(providerName) { + await invoke("plugin:mihomo|update_rule_provider", { + providerName, + }); +} +// runtime config +/** + * 获取基础配置 + * @returns 基础配置 + */ +async function getBaseConfig() { + return await invoke("plugin:mihomo|get_base_config"); +} +/** + * 重新加载配置 + * @param force 强制更新 + * @param configPath 配置文件路径 + */ +async function reloadConfig(force, configPath) { + await invoke("plugin:mihomo|reload_config", { + force, + configPath, + }); +} +/** + * 更改基础配置 + * @param data 基础配置更改后的内容, 例如:{"tun": {"enabled": true}} + */ +async function patchBaseConfig(data) { + await invoke("plugin:mihomo|patch_base_config", { + data, + }); +} +/** + * 更新 Geo + */ +async function updateGeo() { + await invoke("plugin:mihomo|update_geo"); +} +/** + * 重启核心 + */ +async function restart() { + await invoke("plugin:mihomo|restart"); +} +// upgrade +/** + * 升级核心,将当前运行中的核心升级到选择的通道的最新版 + * @param channel 升级通道, 默认 auto + * - release: 稳定版 + * - alpha: 测试版 + * - auto: 根据当前运行的核心版本自动选择升级通道 + * @param force 是否强制升级,默认 false + * - false: 若当前版本为最新版,返回当前为最新版的错误,不再执行升级操作, 否则下载最新版,覆盖升级 + * - true: 直接下载最新版,强制覆盖升级 + */ +async function upgradeCore(channel = "auto", force = false) { + await invoke("plugin:mihomo|upgrade_core", { channel, force }); +} +/** + * 更新 UI + */ +async function upgradeUi() { + await invoke("plugin:mihomo|upgrade_ui"); +} +/** + * 更新 Geo + */ +async function upgradeGeo() { + await invoke("plugin:mihomo|upgrade_geo"); +} +/** + * 清除 Rust 侧中所有的 WebSocket 连接 + */ +async function clearAllWsConnections() { + await invoke("plugin:mihomo|clear_all_ws_connections"); +} +class MihomoWebSocket { + constructor(id, listeners) { + this.id = id; + this.listeners = listeners; + } + /** + * 创建一个新的 WebSocket 连接,用于 Mihomo 的流量监控 + * @returns WebSocket 实例 + */ + static async connect_traffic() { + const listeners = new Set(); + const onMessage = new Channel(); + onMessage.onmessage = (message) => { + listeners.forEach((l) => { + l(message); + }); + }; + const id = await invoke("plugin:mihomo|ws_traffic", { + onMessage, + }); + const instance = new MihomoWebSocket(id, listeners); + MihomoWebSocket.instances.add(instance); + return instance; + } + /** + * 创建一个新的 WebSocket 连接,用于 Mihomo 的内存监控 + * @returns WebSocket 实例 + */ + static async connect_memory() { + const listeners = new Set(); + const onMessage = new Channel(); + onMessage.onmessage = (message) => { + listeners.forEach((l) => { + l(message); + }); + }; + const id = await invoke("plugin:mihomo|ws_memory", { + onMessage, + }); + const instance = new MihomoWebSocket(id, listeners); + MihomoWebSocket.instances.add(instance); + return instance; + } + /** + * 创建一个新的 WebSocket 连接,用于 Mihomo 的连接监控 + * @returns WebSocket 实例 + */ + static async connect_connections() { + const listeners = new Set(); + const onMessage = new Channel(); + onMessage.onmessage = (message) => { + listeners.forEach((l) => { + l(message); + }); + }; + const id = await invoke("plugin:mihomo|ws_connections", { + onMessage, + }); + const instance = new MihomoWebSocket(id, listeners); + MihomoWebSocket.instances.add(instance); + return instance; + } + /** + * 创建一个新的 WebSocket 连接,用于 Mihomo 的日志监控 + * @returns WebSocket 实例 + */ + static async connect_logs(level) { + const listeners = new Set(); + const onMessage = new Channel(); + onMessage.onmessage = (message) => { + listeners.forEach((l) => { + l(message); + }); + }; + const id = await invoke("plugin:mihomo|ws_logs", { + level, + onMessage, + }); + const instance = new MihomoWebSocket(id, listeners); + MihomoWebSocket.instances.add(instance); + return instance; + } + /** + * 添加处理 WebSocket 连接后接受的数据的回调函数 + * @param cb 回调函数 + */ + addListener(cb) { + this.listeners.add(cb); + return () => { + this.listeners.delete(cb); + }; + } + // /** + // * 发送消息到 WebSocket 连接 + // * @param message 发送的消息 + // */ + // async send(message: Message | string | number[]): Promise { + // let m: Message; + // if (typeof message === "string") { + // m = { type: "Text", data: message }; + // } else if (typeof message === "object" && "type" in message) { + // m = message; + // } else if (Array.isArray(message)) { + // m = { type: "Binary", data: message }; + // } else { + // throw new Error( + // "invalid `message` type, expected a `{ type: string, data: any }` object, a string or a numeric array", + // ); + // } + // await invoke("plugin:mihomo|ws_send", { id: this.id, message: m }); + // } + /** + * 关闭 WebSocket 连接 + * @param forceTimeout 强制关闭 WebSocket 连接等待的时间,单位: 毫秒, 默认为 0 + */ + async close() { + try { + await invoke("plugin:mihomo|ws_disconnect", { + id: this.id, + forceTimeout: 0, + }); + } + catch (ignore) { + // ignore + } + finally { + MihomoWebSocket.instances.delete(this); + } + } + /** + * 清理全部的 websocket 连接资源 + */ + static async cleanupAll() { + await Promise.all(Array.from(MihomoWebSocket.instances).map((instance) => instance.close())); + this.instances.clear(); + await clearAllWsConnections(); + } +} +MihomoWebSocket.instances = new Set(); + +export { MihomoWebSocket, clearAllWsConnections, closeAllConnections, closeConnections, delayGroup, delayProxyByName, flushDNS, flushFakeIp, getBaseConfig, getConnections, getGroupByName, getGroups, getProxies, getProxyByName, getProxyProviderByName, getProxyProviders, getRuleProviders, getRules, getVersion, healthcheckNodeInProvider, healthcheckProxyProvider, patchBaseConfig, reloadConfig, restart, selectNodeForGroup, unfixedProxy, updateController, updateGeo, updateProxyProvider, updateRuleProvider, updateSecret, upgradeCore, upgradeGeo, upgradeUi }; diff --git a/crates/tauri-plugin-mihomo/examples/check-mihomo-api/.gitignore b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/.gitignore new file mode 100644 index 000000000..a547bf36d --- /dev/null +++ b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/crates/tauri-plugin-mihomo/examples/check-mihomo-api/README.md b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/README.md new file mode 100644 index 000000000..102e36689 --- /dev/null +++ b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/README.md @@ -0,0 +1,7 @@ +# Tauri + React + Typescript + +This template should help get you started developing with Tauri, React and Typescript in Vite. + +## Recommended IDE Setup + +- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) diff --git a/crates/tauri-plugin-mihomo/examples/check-mihomo-api/index.html b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/index.html new file mode 100644 index 000000000..ff93803bb --- /dev/null +++ b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/index.html @@ -0,0 +1,14 @@ + + + + + + + Tauri + React + Typescript + + + +
+ + + diff --git a/crates/tauri-plugin-mihomo/examples/check-mihomo-api/package.json b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/package.json new file mode 100644 index 000000000..86b5023f2 --- /dev/null +++ b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/package.json @@ -0,0 +1,28 @@ +{ + "name": "check-mihomo-api", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "tauri": "tauri" + }, + "dependencies": { + "@codemirror/lang-json": "^6.0.2", + "@tauri-apps/api": "^2", + "@uiw/react-codemirror": "^4.25.1", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "tauri-plugin-mihomo-api": "file:../.." + }, + "devDependencies": { + "@tauri-apps/cli": "^2", + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", + "@vitejs/plugin-react": "^4.6.0", + "typescript": "~5.8.3", + "vite": "^7.0.4" + } +} diff --git a/crates/tauri-plugin-mihomo/examples/check-mihomo-api/pnpm-lock.yaml b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/pnpm-lock.yaml new file mode 100644 index 000000000..8e2acc2da --- /dev/null +++ b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/pnpm-lock.yaml @@ -0,0 +1,1849 @@ +lockfileVersion: "9.0" + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + .: + dependencies: + "@codemirror/lang-json": + specifier: ^6.0.2 + version: 6.0.2 + "@tauri-apps/api": + specifier: ^2 + version: 2.8.0 + "@uiw/react-codemirror": + specifier: ^4.25.1 + version: 4.25.1(@babel/runtime@7.28.3)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.11.3)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.38.1)(codemirror@6.0.2)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + react: + specifier: ^19.1.0 + version: 19.1.1 + react-dom: + specifier: ^19.1.0 + version: 19.1.1(react@19.1.1) + tauri-plugin-mihomo-api: + specifier: file:../.. + version: file:../.. + devDependencies: + "@tauri-apps/cli": + specifier: ^2 + version: 2.8.3 + "@types/react": + specifier: ^19.1.8 + version: 19.1.12 + "@types/react-dom": + specifier: ^19.1.6 + version: 19.1.8(@types/react@19.1.12) + "@vitejs/plugin-react": + specifier: ^4.6.0 + version: 4.7.0(vite@7.1.3) + typescript: + specifier: ~5.8.3 + version: 5.8.3 + vite: + specifier: ^7.0.4 + version: 7.1.3 + +packages: + "@ampproject/remapping@2.3.0": + resolution: + { + integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==, + } + engines: { node: ">=6.0.0" } + + "@babel/code-frame@7.27.1": + resolution: + { + integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==, + } + engines: { node: ">=6.9.0" } + + "@babel/compat-data@7.28.0": + resolution: + { + integrity: sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==, + } + engines: { node: ">=6.9.0" } + + "@babel/core@7.28.3": + resolution: + { + integrity: sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==, + } + engines: { node: ">=6.9.0" } + + "@babel/generator@7.28.3": + resolution: + { + integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==, + } + engines: { node: ">=6.9.0" } + + "@babel/helper-compilation-targets@7.27.2": + resolution: + { + integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==, + } + engines: { node: ">=6.9.0" } + + "@babel/helper-globals@7.28.0": + resolution: + { + integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==, + } + engines: { node: ">=6.9.0" } + + "@babel/helper-module-imports@7.27.1": + resolution: + { + integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==, + } + engines: { node: ">=6.9.0" } + + "@babel/helper-module-transforms@7.28.3": + resolution: + { + integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==, + } + engines: { node: ">=6.9.0" } + peerDependencies: + "@babel/core": ^7.0.0 + + "@babel/helper-plugin-utils@7.27.1": + resolution: + { + integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==, + } + engines: { node: ">=6.9.0" } + + "@babel/helper-string-parser@7.27.1": + resolution: + { + integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==, + } + engines: { node: ">=6.9.0" } + + "@babel/helper-validator-identifier@7.27.1": + resolution: + { + integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==, + } + engines: { node: ">=6.9.0" } + + "@babel/helper-validator-option@7.27.1": + resolution: + { + integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==, + } + engines: { node: ">=6.9.0" } + + "@babel/helpers@7.28.3": + resolution: + { + integrity: sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==, + } + engines: { node: ">=6.9.0" } + + "@babel/parser@7.28.3": + resolution: + { + integrity: sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==, + } + engines: { node: ">=6.0.0" } + hasBin: true + + "@babel/plugin-transform-react-jsx-self@7.27.1": + resolution: + { + integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==, + } + engines: { node: ">=6.9.0" } + peerDependencies: + "@babel/core": ^7.0.0-0 + + "@babel/plugin-transform-react-jsx-source@7.27.1": + resolution: + { + integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==, + } + engines: { node: ">=6.9.0" } + peerDependencies: + "@babel/core": ^7.0.0-0 + + "@babel/runtime@7.28.3": + resolution: + { + integrity: sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==, + } + engines: { node: ">=6.9.0" } + + "@babel/template@7.27.2": + resolution: + { + integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==, + } + engines: { node: ">=6.9.0" } + + "@babel/traverse@7.28.3": + resolution: + { + integrity: sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==, + } + engines: { node: ">=6.9.0" } + + "@babel/types@7.28.2": + resolution: + { + integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==, + } + engines: { node: ">=6.9.0" } + + "@codemirror/autocomplete@6.18.6": + resolution: + { + integrity: sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==, + } + + "@codemirror/commands@6.8.1": + resolution: + { + integrity: sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==, + } + + "@codemirror/lang-json@6.0.2": + resolution: + { + integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==, + } + + "@codemirror/language@6.11.3": + resolution: + { + integrity: sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==, + } + + "@codemirror/lint@6.8.5": + resolution: + { + integrity: sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==, + } + + "@codemirror/search@6.5.11": + resolution: + { + integrity: sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==, + } + + "@codemirror/state@6.5.2": + resolution: + { + integrity: sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==, + } + + "@codemirror/theme-one-dark@6.1.3": + resolution: + { + integrity: sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==, + } + + "@codemirror/view@6.38.1": + resolution: + { + integrity: sha512-RmTOkE7hRU3OVREqFVITWHz6ocgBjv08GoePscAakgVQfciA3SGCEk7mb9IzwW61cKKmlTpHXG6DUE5Ubx+MGQ==, + } + + "@esbuild/aix-ppc64@0.25.9": + resolution: + { + integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==, + } + engines: { node: ">=18" } + cpu: [ppc64] + os: [aix] + + "@esbuild/android-arm64@0.25.9": + resolution: + { + integrity: sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==, + } + engines: { node: ">=18" } + cpu: [arm64] + os: [android] + + "@esbuild/android-arm@0.25.9": + resolution: + { + integrity: sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==, + } + engines: { node: ">=18" } + cpu: [arm] + os: [android] + + "@esbuild/android-x64@0.25.9": + resolution: + { + integrity: sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==, + } + engines: { node: ">=18" } + cpu: [x64] + os: [android] + + "@esbuild/darwin-arm64@0.25.9": + resolution: + { + integrity: sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==, + } + engines: { node: ">=18" } + cpu: [arm64] + os: [darwin] + + "@esbuild/darwin-x64@0.25.9": + resolution: + { + integrity: sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==, + } + engines: { node: ">=18" } + cpu: [x64] + os: [darwin] + + "@esbuild/freebsd-arm64@0.25.9": + resolution: + { + integrity: sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==, + } + engines: { node: ">=18" } + cpu: [arm64] + os: [freebsd] + + "@esbuild/freebsd-x64@0.25.9": + resolution: + { + integrity: sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==, + } + engines: { node: ">=18" } + cpu: [x64] + os: [freebsd] + + "@esbuild/linux-arm64@0.25.9": + resolution: + { + integrity: sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==, + } + engines: { node: ">=18" } + cpu: [arm64] + os: [linux] + + "@esbuild/linux-arm@0.25.9": + resolution: + { + integrity: sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==, + } + engines: { node: ">=18" } + cpu: [arm] + os: [linux] + + "@esbuild/linux-ia32@0.25.9": + resolution: + { + integrity: sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==, + } + engines: { node: ">=18" } + cpu: [ia32] + os: [linux] + + "@esbuild/linux-loong64@0.25.9": + resolution: + { + integrity: sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==, + } + engines: { node: ">=18" } + cpu: [loong64] + os: [linux] + + "@esbuild/linux-mips64el@0.25.9": + resolution: + { + integrity: sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==, + } + engines: { node: ">=18" } + cpu: [mips64el] + os: [linux] + + "@esbuild/linux-ppc64@0.25.9": + resolution: + { + integrity: sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==, + } + engines: { node: ">=18" } + cpu: [ppc64] + os: [linux] + + "@esbuild/linux-riscv64@0.25.9": + resolution: + { + integrity: sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==, + } + engines: { node: ">=18" } + cpu: [riscv64] + os: [linux] + + "@esbuild/linux-s390x@0.25.9": + resolution: + { + integrity: sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==, + } + engines: { node: ">=18" } + cpu: [s390x] + os: [linux] + + "@esbuild/linux-x64@0.25.9": + resolution: + { + integrity: sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==, + } + engines: { node: ">=18" } + cpu: [x64] + os: [linux] + + "@esbuild/netbsd-arm64@0.25.9": + resolution: + { + integrity: sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==, + } + engines: { node: ">=18" } + cpu: [arm64] + os: [netbsd] + + "@esbuild/netbsd-x64@0.25.9": + resolution: + { + integrity: sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==, + } + engines: { node: ">=18" } + cpu: [x64] + os: [netbsd] + + "@esbuild/openbsd-arm64@0.25.9": + resolution: + { + integrity: sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==, + } + engines: { node: ">=18" } + cpu: [arm64] + os: [openbsd] + + "@esbuild/openbsd-x64@0.25.9": + resolution: + { + integrity: sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==, + } + engines: { node: ">=18" } + cpu: [x64] + os: [openbsd] + + "@esbuild/openharmony-arm64@0.25.9": + resolution: + { + integrity: sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==, + } + engines: { node: ">=18" } + cpu: [arm64] + os: [openharmony] + + "@esbuild/sunos-x64@0.25.9": + resolution: + { + integrity: sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==, + } + engines: { node: ">=18" } + cpu: [x64] + os: [sunos] + + "@esbuild/win32-arm64@0.25.9": + resolution: + { + integrity: sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==, + } + engines: { node: ">=18" } + cpu: [arm64] + os: [win32] + + "@esbuild/win32-ia32@0.25.9": + resolution: + { + integrity: sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==, + } + engines: { node: ">=18" } + cpu: [ia32] + os: [win32] + + "@esbuild/win32-x64@0.25.9": + resolution: + { + integrity: sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==, + } + engines: { node: ">=18" } + cpu: [x64] + os: [win32] + + "@jridgewell/gen-mapping@0.3.13": + resolution: + { + integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==, + } + + "@jridgewell/resolve-uri@3.1.2": + resolution: + { + integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==, + } + engines: { node: ">=6.0.0" } + + "@jridgewell/sourcemap-codec@1.5.5": + resolution: + { + integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==, + } + + "@jridgewell/trace-mapping@0.3.30": + resolution: + { + integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==, + } + + "@lezer/common@1.2.3": + resolution: + { + integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==, + } + + "@lezer/highlight@1.2.1": + resolution: + { + integrity: sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==, + } + + "@lezer/json@1.0.3": + resolution: + { + integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==, + } + + "@lezer/lr@1.4.2": + resolution: + { + integrity: sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==, + } + + "@marijn/find-cluster-break@1.0.2": + resolution: + { + integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==, + } + + "@rolldown/pluginutils@1.0.0-beta.27": + resolution: + { + integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==, + } + + "@rollup/rollup-android-arm-eabi@4.49.0": + resolution: + { + integrity: sha512-rlKIeL854Ed0e09QGYFlmDNbka6I3EQFw7iZuugQjMb11KMpJCLPFL4ZPbMfaEhLADEL1yx0oujGkBQ7+qW3eA==, + } + cpu: [arm] + os: [android] + + "@rollup/rollup-android-arm64@4.49.0": + resolution: + { + integrity: sha512-cqPpZdKUSQYRtLLr6R4X3sD4jCBO1zUmeo3qrWBCqYIeH8Q3KRL4F3V7XJ2Rm8/RJOQBZuqzQGWPjjvFUcYa/w==, + } + cpu: [arm64] + os: [android] + + "@rollup/rollup-darwin-arm64@4.49.0": + resolution: + { + integrity: sha512-99kMMSMQT7got6iYX3yyIiJfFndpojBmkHfTc1rIje8VbjhmqBXE+nb7ZZP3A5skLyujvT0eIUCUsxAe6NjWbw==, + } + cpu: [arm64] + os: [darwin] + + "@rollup/rollup-darwin-x64@4.49.0": + resolution: + { + integrity: sha512-y8cXoD3wdWUDpjOLMKLx6l+NFz3NlkWKcBCBfttUn+VGSfgsQ5o/yDUGtzE9HvsodkP0+16N0P4Ty1VuhtRUGg==, + } + cpu: [x64] + os: [darwin] + + "@rollup/rollup-freebsd-arm64@4.49.0": + resolution: + { + integrity: sha512-3mY5Pr7qv4GS4ZvWoSP8zha8YoiqrU+e0ViPvB549jvliBbdNLrg2ywPGkgLC3cmvN8ya3za+Q2xVyT6z+vZqA==, + } + cpu: [arm64] + os: [freebsd] + + "@rollup/rollup-freebsd-x64@4.49.0": + resolution: + { + integrity: sha512-C9KzzOAQU5gU4kG8DTk+tjdKjpWhVWd5uVkinCwwFub2m7cDYLOdtXoMrExfeBmeRy9kBQMkiyJ+HULyF1yj9w==, + } + cpu: [x64] + os: [freebsd] + + "@rollup/rollup-linux-arm-gnueabihf@4.49.0": + resolution: + { + integrity: sha512-OVSQgEZDVLnTbMq5NBs6xkmz3AADByCWI4RdKSFNlDsYXdFtlxS59J+w+LippJe8KcmeSSM3ba+GlsM9+WwC1w==, + } + cpu: [arm] + os: [linux] + + "@rollup/rollup-linux-arm-musleabihf@4.49.0": + resolution: + { + integrity: sha512-ZnfSFA7fDUHNa4P3VwAcfaBLakCbYaxCk0jUnS3dTou9P95kwoOLAMlT3WmEJDBCSrOEFFV0Y1HXiwfLYJuLlA==, + } + cpu: [arm] + os: [linux] + + "@rollup/rollup-linux-arm64-gnu@4.49.0": + resolution: + { + integrity: sha512-Z81u+gfrobVK2iV7GqZCBfEB1y6+I61AH466lNK+xy1jfqFLiQ9Qv716WUM5fxFrYxwC7ziVdZRU9qvGHkYIJg==, + } + cpu: [arm64] + os: [linux] + + "@rollup/rollup-linux-arm64-musl@4.49.0": + resolution: + { + integrity: sha512-zoAwS0KCXSnTp9NH/h9aamBAIve0DXeYpll85shf9NJ0URjSTzzS+Z9evmolN+ICfD3v8skKUPyk2PO0uGdFqg==, + } + cpu: [arm64] + os: [linux] + + "@rollup/rollup-linux-loongarch64-gnu@4.49.0": + resolution: + { + integrity: sha512-2QyUyQQ1ZtwZGiq0nvODL+vLJBtciItC3/5cYN8ncDQcv5avrt2MbKt1XU/vFAJlLta5KujqyHdYtdag4YEjYQ==, + } + cpu: [loong64] + os: [linux] + + "@rollup/rollup-linux-ppc64-gnu@4.49.0": + resolution: + { + integrity: sha512-k9aEmOWt+mrMuD3skjVJSSxHckJp+SiFzFG+v8JLXbc/xi9hv2icSkR3U7uQzqy+/QbbYY7iNB9eDTwrELo14g==, + } + cpu: [ppc64] + os: [linux] + + "@rollup/rollup-linux-riscv64-gnu@4.49.0": + resolution: + { + integrity: sha512-rDKRFFIWJ/zJn6uk2IdYLc09Z7zkE5IFIOWqpuU0o6ZpHcdniAyWkwSUWE/Z25N/wNDmFHHMzin84qW7Wzkjsw==, + } + cpu: [riscv64] + os: [linux] + + "@rollup/rollup-linux-riscv64-musl@4.49.0": + resolution: + { + integrity: sha512-FkkhIY/hYFVnOzz1WeV3S9Bd1h0hda/gRqvZCMpHWDHdiIHn6pqsY3b5eSbvGccWHMQ1uUzgZTKS4oGpykf8Tw==, + } + cpu: [riscv64] + os: [linux] + + "@rollup/rollup-linux-s390x-gnu@4.49.0": + resolution: + { + integrity: sha512-gRf5c+A7QiOG3UwLyOOtyJMD31JJhMjBvpfhAitPAoqZFcOeK3Kc1Veg1z/trmt+2P6F/biT02fU19GGTS529A==, + } + cpu: [s390x] + os: [linux] + + "@rollup/rollup-linux-x64-gnu@4.49.0": + resolution: + { + integrity: sha512-BR7+blScdLW1h/2hB/2oXM+dhTmpW3rQt1DeSiCP9mc2NMMkqVgjIN3DDsNpKmezffGC9R8XKVOLmBkRUcK/sA==, + } + cpu: [x64] + os: [linux] + + "@rollup/rollup-linux-x64-musl@4.49.0": + resolution: + { + integrity: sha512-hDMOAe+6nX3V5ei1I7Au3wcr9h3ktKzDvF2ne5ovX8RZiAHEtX1A5SNNk4zt1Qt77CmnbqT+upb/umzoPMWiPg==, + } + cpu: [x64] + os: [linux] + + "@rollup/rollup-win32-arm64-msvc@4.49.0": + resolution: + { + integrity: sha512-wkNRzfiIGaElC9kXUT+HLx17z7D0jl+9tGYRKwd8r7cUqTL7GYAvgUY++U2hK6Ar7z5Z6IRRoWC8kQxpmM7TDA==, + } + cpu: [arm64] + os: [win32] + + "@rollup/rollup-win32-ia32-msvc@4.49.0": + resolution: + { + integrity: sha512-gq5aW/SyNpjp71AAzroH37DtINDcX1Qw2iv9Chyz49ZgdOP3NV8QCyKZUrGsYX9Yyggj5soFiRCgsL3HwD8TdA==, + } + cpu: [ia32] + os: [win32] + + "@rollup/rollup-win32-x64-msvc@4.49.0": + resolution: + { + integrity: sha512-gEtqFbzmZLFk2xKh7g0Rlo8xzho8KrEFEkzvHbfUGkrgXOpZ4XagQ6n+wIZFNh1nTb8UD16J4nFSFKXYgnbdBg==, + } + cpu: [x64] + os: [win32] + + "@tauri-apps/api@2.8.0": + resolution: + { + integrity: sha512-ga7zdhbS2GXOMTIZRT0mYjKJtR9fivsXzsyq5U3vjDL0s6DTMwYRm0UHNjzTY5dh4+LSC68Sm/7WEiimbQNYlw==, + } + + "@tauri-apps/cli-darwin-arm64@2.8.3": + resolution: + { + integrity: sha512-+X/DjTlH9ZLT9kWrU+Mk9TSu8vVpv30GgfAOKUxlCQobaRSOzN0cxgZfRcgWaDLu80/gWsJ7Ktk9jLfJ9h9CVA==, + } + engines: { node: ">= 10" } + cpu: [arm64] + os: [darwin] + + "@tauri-apps/cli-darwin-x64@2.8.3": + resolution: + { + integrity: sha512-Bs+DK+gGinSj373DEeAuZMUrvTE1m7X5Ef2jC2lU2X8ZhQf4VBV+gNMRoOlSuwIlSTU2eKDQsExtKeFFSpbc8A==, + } + engines: { node: ">= 10" } + cpu: [x64] + os: [darwin] + + "@tauri-apps/cli-linux-arm-gnueabihf@2.8.3": + resolution: + { + integrity: sha512-9pri7KWES6x0M0DWCr5RIsGtXD4yy83Zsf8xuSmn8z6xboFquSnfJZmFsfPz25G8awLFIhxUkxP0YtZGiIUy7g==, + } + engines: { node: ">= 10" } + cpu: [arm] + os: [linux] + + "@tauri-apps/cli-linux-arm64-gnu@2.8.3": + resolution: + { + integrity: sha512-2+qRdUgnFJ7pDW69dFZxYduWEZPya3U2YA6GaDhrYTHBq8/ypPSpuUT+BZ6n9r68+ij7tFMTj+vwNDgNp3M/0w==, + } + engines: { node: ">= 10" } + cpu: [arm64] + os: [linux] + + "@tauri-apps/cli-linux-arm64-musl@2.8.3": + resolution: + { + integrity: sha512-DJHW1vcqmLMqZCBiu9qv7/oYAygNC6xvrxwrUWvWMvaz/qKNy9NVXZm/EUx+sLTCcOxWHyJe+CII1kW3ouI18Q==, + } + engines: { node: ">= 10" } + cpu: [arm64] + os: [linux] + + "@tauri-apps/cli-linux-riscv64-gnu@2.8.3": + resolution: + { + integrity: sha512-+CbLaQXAqd5lPJnfXGyitbgp/q5mnsvCoToGspeVMBYNGd04ES/6XDEcXH7EwNCTgXBTJVRYf3qhI8a8/x31Aw==, + } + engines: { node: ">= 10" } + cpu: [riscv64] + os: [linux] + + "@tauri-apps/cli-linux-x64-gnu@2.8.3": + resolution: + { + integrity: sha512-FGjLnA+3PTJwoN5KEMAi6Q8I6SkuW5w8qSFKldGx2Mma8GqtttXqIDw1BzxcIw/LMcr6JrxjbIRULzmV05q/QA==, + } + engines: { node: ">= 10" } + cpu: [x64] + os: [linux] + + "@tauri-apps/cli-linux-x64-musl@2.8.3": + resolution: + { + integrity: sha512-tWRX3rQJCeUq9mR0Rc0tUV+pdgGL94UqVIzPn0/VmhDehdiDouRdXOUPggJrYUz2Aj/4RvVa83J6B8Hg37s8RQ==, + } + engines: { node: ">= 10" } + cpu: [x64] + os: [linux] + + "@tauri-apps/cli-win32-arm64-msvc@2.8.3": + resolution: + { + integrity: sha512-UQHDmbSMIeWs/Yr3KmtfZFs5m6b+NWUe2+NE7fHu3o4EzPrvNa/Uf4U2XsYKOr0V/yetxZH0/fc+xovcnsqyqA==, + } + engines: { node: ">= 10" } + cpu: [arm64] + os: [win32] + + "@tauri-apps/cli-win32-ia32-msvc@2.8.3": + resolution: + { + integrity: sha512-aIP38i2KeADboPD1wsBFYdodEQ9PIJe0HW2urd3ocHpGxF8gX/KMiGOwGVSobu9gFlCpFNoVwCX6J1S5pJCRIQ==, + } + engines: { node: ">= 10" } + cpu: [ia32] + os: [win32] + + "@tauri-apps/cli-win32-x64-msvc@2.8.3": + resolution: + { + integrity: sha512-Z+H+PwK+3yMffG1rN7iqs+uPo6FkPyHJ4MTtFhnEvvGzc3aH711bwFb6+PXwMXfOb/jPR/LB+o6kEXghBu9ynQ==, + } + engines: { node: ">= 10" } + cpu: [x64] + os: [win32] + + "@tauri-apps/cli@2.8.3": + resolution: + { + integrity: sha512-5IlcOtVBI6HYcTRFH4tuLZV+FX09Psi4Xi+TyFf/S8T8w+ZzPNnrehHz6KUGRbuXHfJhtmRDoUULXNEhpdVkfA==, + } + engines: { node: ">= 10" } + hasBin: true + + "@types/babel__core@7.20.5": + resolution: + { + integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==, + } + + "@types/babel__generator@7.27.0": + resolution: + { + integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==, + } + + "@types/babel__template@7.4.4": + resolution: + { + integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==, + } + + "@types/babel__traverse@7.28.0": + resolution: + { + integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==, + } + + "@types/estree@1.0.8": + resolution: + { + integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==, + } + + "@types/react-dom@19.1.8": + resolution: + { + integrity: sha512-xG7xaBMJCpcK0RpN8jDbAACQo54ycO6h4dSSmgv8+fu6ZIAdANkx/WsawASUjVXYfy+J9AbUpRMNNEsXCDfDBQ==, + } + peerDependencies: + "@types/react": ^19.0.0 + + "@types/react@19.1.12": + resolution: + { + integrity: sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==, + } + + "@uiw/codemirror-extensions-basic-setup@4.25.1": + resolution: + { + integrity: sha512-zxgA2QkvP3ZDKxTBc9UltNFTrSeFezGXcZtZj6qcsBxiMzowoEMP5mVwXcKjpzldpZVRuY+JCC+RsekEgid4vg==, + } + peerDependencies: + "@codemirror/autocomplete": ">=6.0.0" + "@codemirror/commands": ">=6.0.0" + "@codemirror/language": ">=6.0.0" + "@codemirror/lint": ">=6.0.0" + "@codemirror/search": ">=6.0.0" + "@codemirror/state": ">=6.0.0" + "@codemirror/view": ">=6.0.0" + + "@uiw/react-codemirror@4.25.1": + resolution: + { + integrity: sha512-eESBKHndoYkaEGlKCwRO4KrnTw1HkWBxVpEeqntoWTpoFEUYxdLWUYmkPBVk4/u8YzVy9g91nFfIRpqe5LjApg==, + } + peerDependencies: + "@babel/runtime": ">=7.11.0" + "@codemirror/state": ">=6.0.0" + "@codemirror/theme-one-dark": ">=6.0.0" + "@codemirror/view": ">=6.0.0" + codemirror: ">=6.0.0" + react: ">=17.0.0" + react-dom: ">=17.0.0" + + "@vitejs/plugin-react@4.7.0": + resolution: + { + integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==, + } + engines: { node: ^14.18.0 || >=16.0.0 } + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + browserslist@4.25.3: + resolution: + { + integrity: sha512-cDGv1kkDI4/0e5yON9yM5G/0A5u8sf5TnmdX5C9qHzI9PPu++sQ9zjm1k9NiOrf3riY4OkK0zSGqfvJyJsgCBQ==, + } + engines: { node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7 } + hasBin: true + + caniuse-lite@1.0.30001737: + resolution: + { + integrity: sha512-BiloLiXtQNrY5UyF0+1nSJLXUENuhka2pzy2Fx5pGxqavdrxSCW4U6Pn/PoG3Efspi2frRbHpBV2XsrPE6EDlw==, + } + + codemirror@6.0.2: + resolution: + { + integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==, + } + + convert-source-map@2.0.0: + resolution: + { + integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==, + } + + crelt@1.0.6: + resolution: + { + integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==, + } + + csstype@3.1.3: + resolution: + { + integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==, + } + + debug@4.4.1: + resolution: + { + integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==, + } + engines: { node: ">=6.0" } + peerDependencies: + supports-color: "*" + peerDependenciesMeta: + supports-color: + optional: true + + electron-to-chromium@1.5.209: + resolution: + { + integrity: sha512-Xoz0uMrim9ZETCQt8UgM5FxQF9+imA7PBpokoGcZloA1uw2LeHzTlip5cb5KOAsXZLjh/moN2vReN3ZjJmjI9A==, + } + + esbuild@0.25.9: + resolution: + { + integrity: sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==, + } + engines: { node: ">=18" } + hasBin: true + + escalade@3.2.0: + resolution: + { + integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==, + } + engines: { node: ">=6" } + + fdir@6.5.0: + resolution: + { + integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==, + } + engines: { node: ">=12.0.0" } + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: + { + integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==, + } + engines: { node: ^8.16.0 || ^10.6.0 || >=11.0.0 } + os: [darwin] + + gensync@1.0.0-beta.2: + resolution: + { + integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==, + } + engines: { node: ">=6.9.0" } + + js-tokens@4.0.0: + resolution: + { + integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==, + } + + jsesc@3.1.0: + resolution: + { + integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==, + } + engines: { node: ">=6" } + hasBin: true + + json5@2.2.3: + resolution: + { + integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==, + } + engines: { node: ">=6" } + hasBin: true + + lru-cache@5.1.1: + resolution: + { + integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==, + } + + ms@2.1.3: + resolution: + { + integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==, + } + + nanoid@3.3.11: + resolution: + { + integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==, + } + engines: { node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1 } + hasBin: true + + node-releases@2.0.19: + resolution: + { + integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==, + } + + picocolors@1.1.1: + resolution: + { + integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==, + } + + picomatch@4.0.3: + resolution: + { + integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==, + } + engines: { node: ">=12" } + + postcss@8.5.6: + resolution: + { + integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==, + } + engines: { node: ^10 || ^12 || >=14 } + + react-dom@19.1.1: + resolution: + { + integrity: sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==, + } + peerDependencies: + react: ^19.1.1 + + react-refresh@0.17.0: + resolution: + { + integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==, + } + engines: { node: ">=0.10.0" } + + react@19.1.1: + resolution: + { + integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==, + } + engines: { node: ">=0.10.0" } + + rollup@4.49.0: + resolution: + { + integrity: sha512-3IVq0cGJ6H7fKXXEdVt+RcYvRCt8beYY9K1760wGQwSAHZcS9eot1zDG5axUbcp/kWRi5zKIIDX8MoKv/TzvZA==, + } + engines: { node: ">=18.0.0", npm: ">=8.0.0" } + hasBin: true + + scheduler@0.26.0: + resolution: + { + integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==, + } + + semver@6.3.1: + resolution: + { + integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==, + } + hasBin: true + + source-map-js@1.2.1: + resolution: + { + integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==, + } + engines: { node: ">=0.10.0" } + + style-mod@4.1.2: + resolution: + { + integrity: sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==, + } + + tauri-plugin-mihomo-api@file:../..: + resolution: { directory: ../.., type: directory } + + tinyglobby@0.2.14: + resolution: + { + integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==, + } + engines: { node: ">=12.0.0" } + + typescript@5.8.3: + resolution: + { + integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==, + } + engines: { node: ">=14.17" } + hasBin: true + + update-browserslist-db@1.1.3: + resolution: + { + integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==, + } + hasBin: true + peerDependencies: + browserslist: ">= 4.21.0" + + vite@7.1.3: + resolution: + { + integrity: sha512-OOUi5zjkDxYrKhTV3V7iKsoS37VUM7v40+HuwEmcrsf11Cdx9y3DIr2Px6liIcZFwt3XSRpQvFpL3WVy7ApkGw==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + hasBin: true + peerDependencies: + "@types/node": ^20.19.0 || >=22.12.0 + jiti: ">=1.21.0" + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: ">=0.54.8" + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + "@types/node": + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + w3c-keyname@2.2.8: + resolution: + { + integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==, + } + + yallist@3.1.1: + resolution: + { + integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==, + } + +snapshots: + "@ampproject/remapping@2.3.0": + dependencies: + "@jridgewell/gen-mapping": 0.3.13 + "@jridgewell/trace-mapping": 0.3.30 + + "@babel/code-frame@7.27.1": + dependencies: + "@babel/helper-validator-identifier": 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + "@babel/compat-data@7.28.0": {} + + "@babel/core@7.28.3": + dependencies: + "@ampproject/remapping": 2.3.0 + "@babel/code-frame": 7.27.1 + "@babel/generator": 7.28.3 + "@babel/helper-compilation-targets": 7.27.2 + "@babel/helper-module-transforms": 7.28.3(@babel/core@7.28.3) + "@babel/helpers": 7.28.3 + "@babel/parser": 7.28.3 + "@babel/template": 7.27.2 + "@babel/traverse": 7.28.3 + "@babel/types": 7.28.2 + convert-source-map: 2.0.0 + debug: 4.4.1 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + "@babel/generator@7.28.3": + dependencies: + "@babel/parser": 7.28.3 + "@babel/types": 7.28.2 + "@jridgewell/gen-mapping": 0.3.13 + "@jridgewell/trace-mapping": 0.3.30 + jsesc: 3.1.0 + + "@babel/helper-compilation-targets@7.27.2": + dependencies: + "@babel/compat-data": 7.28.0 + "@babel/helper-validator-option": 7.27.1 + browserslist: 4.25.3 + lru-cache: 5.1.1 + semver: 6.3.1 + + "@babel/helper-globals@7.28.0": {} + + "@babel/helper-module-imports@7.27.1": + dependencies: + "@babel/traverse": 7.28.3 + "@babel/types": 7.28.2 + transitivePeerDependencies: + - supports-color + + "@babel/helper-module-transforms@7.28.3(@babel/core@7.28.3)": + dependencies: + "@babel/core": 7.28.3 + "@babel/helper-module-imports": 7.27.1 + "@babel/helper-validator-identifier": 7.27.1 + "@babel/traverse": 7.28.3 + transitivePeerDependencies: + - supports-color + + "@babel/helper-plugin-utils@7.27.1": {} + + "@babel/helper-string-parser@7.27.1": {} + + "@babel/helper-validator-identifier@7.27.1": {} + + "@babel/helper-validator-option@7.27.1": {} + + "@babel/helpers@7.28.3": + dependencies: + "@babel/template": 7.27.2 + "@babel/types": 7.28.2 + + "@babel/parser@7.28.3": + dependencies: + "@babel/types": 7.28.2 + + "@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.3)": + dependencies: + "@babel/core": 7.28.3 + "@babel/helper-plugin-utils": 7.27.1 + + "@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.3)": + dependencies: + "@babel/core": 7.28.3 + "@babel/helper-plugin-utils": 7.27.1 + + "@babel/runtime@7.28.3": {} + + "@babel/template@7.27.2": + dependencies: + "@babel/code-frame": 7.27.1 + "@babel/parser": 7.28.3 + "@babel/types": 7.28.2 + + "@babel/traverse@7.28.3": + dependencies: + "@babel/code-frame": 7.27.1 + "@babel/generator": 7.28.3 + "@babel/helper-globals": 7.28.0 + "@babel/parser": 7.28.3 + "@babel/template": 7.27.2 + "@babel/types": 7.28.2 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + "@babel/types@7.28.2": + dependencies: + "@babel/helper-string-parser": 7.27.1 + "@babel/helper-validator-identifier": 7.27.1 + + "@codemirror/autocomplete@6.18.6": + dependencies: + "@codemirror/language": 6.11.3 + "@codemirror/state": 6.5.2 + "@codemirror/view": 6.38.1 + "@lezer/common": 1.2.3 + + "@codemirror/commands@6.8.1": + dependencies: + "@codemirror/language": 6.11.3 + "@codemirror/state": 6.5.2 + "@codemirror/view": 6.38.1 + "@lezer/common": 1.2.3 + + "@codemirror/lang-json@6.0.2": + dependencies: + "@codemirror/language": 6.11.3 + "@lezer/json": 1.0.3 + + "@codemirror/language@6.11.3": + dependencies: + "@codemirror/state": 6.5.2 + "@codemirror/view": 6.38.1 + "@lezer/common": 1.2.3 + "@lezer/highlight": 1.2.1 + "@lezer/lr": 1.4.2 + style-mod: 4.1.2 + + "@codemirror/lint@6.8.5": + dependencies: + "@codemirror/state": 6.5.2 + "@codemirror/view": 6.38.1 + crelt: 1.0.6 + + "@codemirror/search@6.5.11": + dependencies: + "@codemirror/state": 6.5.2 + "@codemirror/view": 6.38.1 + crelt: 1.0.6 + + "@codemirror/state@6.5.2": + dependencies: + "@marijn/find-cluster-break": 1.0.2 + + "@codemirror/theme-one-dark@6.1.3": + dependencies: + "@codemirror/language": 6.11.3 + "@codemirror/state": 6.5.2 + "@codemirror/view": 6.38.1 + "@lezer/highlight": 1.2.1 + + "@codemirror/view@6.38.1": + dependencies: + "@codemirror/state": 6.5.2 + crelt: 1.0.6 + style-mod: 4.1.2 + w3c-keyname: 2.2.8 + + "@esbuild/aix-ppc64@0.25.9": + optional: true + + "@esbuild/android-arm64@0.25.9": + optional: true + + "@esbuild/android-arm@0.25.9": + optional: true + + "@esbuild/android-x64@0.25.9": + optional: true + + "@esbuild/darwin-arm64@0.25.9": + optional: true + + "@esbuild/darwin-x64@0.25.9": + optional: true + + "@esbuild/freebsd-arm64@0.25.9": + optional: true + + "@esbuild/freebsd-x64@0.25.9": + optional: true + + "@esbuild/linux-arm64@0.25.9": + optional: true + + "@esbuild/linux-arm@0.25.9": + optional: true + + "@esbuild/linux-ia32@0.25.9": + optional: true + + "@esbuild/linux-loong64@0.25.9": + optional: true + + "@esbuild/linux-mips64el@0.25.9": + optional: true + + "@esbuild/linux-ppc64@0.25.9": + optional: true + + "@esbuild/linux-riscv64@0.25.9": + optional: true + + "@esbuild/linux-s390x@0.25.9": + optional: true + + "@esbuild/linux-x64@0.25.9": + optional: true + + "@esbuild/netbsd-arm64@0.25.9": + optional: true + + "@esbuild/netbsd-x64@0.25.9": + optional: true + + "@esbuild/openbsd-arm64@0.25.9": + optional: true + + "@esbuild/openbsd-x64@0.25.9": + optional: true + + "@esbuild/openharmony-arm64@0.25.9": + optional: true + + "@esbuild/sunos-x64@0.25.9": + optional: true + + "@esbuild/win32-arm64@0.25.9": + optional: true + + "@esbuild/win32-ia32@0.25.9": + optional: true + + "@esbuild/win32-x64@0.25.9": + optional: true + + "@jridgewell/gen-mapping@0.3.13": + dependencies: + "@jridgewell/sourcemap-codec": 1.5.5 + "@jridgewell/trace-mapping": 0.3.30 + + "@jridgewell/resolve-uri@3.1.2": {} + + "@jridgewell/sourcemap-codec@1.5.5": {} + + "@jridgewell/trace-mapping@0.3.30": + dependencies: + "@jridgewell/resolve-uri": 3.1.2 + "@jridgewell/sourcemap-codec": 1.5.5 + + "@lezer/common@1.2.3": {} + + "@lezer/highlight@1.2.1": + dependencies: + "@lezer/common": 1.2.3 + + "@lezer/json@1.0.3": + dependencies: + "@lezer/common": 1.2.3 + "@lezer/highlight": 1.2.1 + "@lezer/lr": 1.4.2 + + "@lezer/lr@1.4.2": + dependencies: + "@lezer/common": 1.2.3 + + "@marijn/find-cluster-break@1.0.2": {} + + "@rolldown/pluginutils@1.0.0-beta.27": {} + + "@rollup/rollup-android-arm-eabi@4.49.0": + optional: true + + "@rollup/rollup-android-arm64@4.49.0": + optional: true + + "@rollup/rollup-darwin-arm64@4.49.0": + optional: true + + "@rollup/rollup-darwin-x64@4.49.0": + optional: true + + "@rollup/rollup-freebsd-arm64@4.49.0": + optional: true + + "@rollup/rollup-freebsd-x64@4.49.0": + optional: true + + "@rollup/rollup-linux-arm-gnueabihf@4.49.0": + optional: true + + "@rollup/rollup-linux-arm-musleabihf@4.49.0": + optional: true + + "@rollup/rollup-linux-arm64-gnu@4.49.0": + optional: true + + "@rollup/rollup-linux-arm64-musl@4.49.0": + optional: true + + "@rollup/rollup-linux-loongarch64-gnu@4.49.0": + optional: true + + "@rollup/rollup-linux-ppc64-gnu@4.49.0": + optional: true + + "@rollup/rollup-linux-riscv64-gnu@4.49.0": + optional: true + + "@rollup/rollup-linux-riscv64-musl@4.49.0": + optional: true + + "@rollup/rollup-linux-s390x-gnu@4.49.0": + optional: true + + "@rollup/rollup-linux-x64-gnu@4.49.0": + optional: true + + "@rollup/rollup-linux-x64-musl@4.49.0": + optional: true + + "@rollup/rollup-win32-arm64-msvc@4.49.0": + optional: true + + "@rollup/rollup-win32-ia32-msvc@4.49.0": + optional: true + + "@rollup/rollup-win32-x64-msvc@4.49.0": + optional: true + + "@tauri-apps/api@2.8.0": {} + + "@tauri-apps/cli-darwin-arm64@2.8.3": + optional: true + + "@tauri-apps/cli-darwin-x64@2.8.3": + optional: true + + "@tauri-apps/cli-linux-arm-gnueabihf@2.8.3": + optional: true + + "@tauri-apps/cli-linux-arm64-gnu@2.8.3": + optional: true + + "@tauri-apps/cli-linux-arm64-musl@2.8.3": + optional: true + + "@tauri-apps/cli-linux-riscv64-gnu@2.8.3": + optional: true + + "@tauri-apps/cli-linux-x64-gnu@2.8.3": + optional: true + + "@tauri-apps/cli-linux-x64-musl@2.8.3": + optional: true + + "@tauri-apps/cli-win32-arm64-msvc@2.8.3": + optional: true + + "@tauri-apps/cli-win32-ia32-msvc@2.8.3": + optional: true + + "@tauri-apps/cli-win32-x64-msvc@2.8.3": + optional: true + + "@tauri-apps/cli@2.8.3": + optionalDependencies: + "@tauri-apps/cli-darwin-arm64": 2.8.3 + "@tauri-apps/cli-darwin-x64": 2.8.3 + "@tauri-apps/cli-linux-arm-gnueabihf": 2.8.3 + "@tauri-apps/cli-linux-arm64-gnu": 2.8.3 + "@tauri-apps/cli-linux-arm64-musl": 2.8.3 + "@tauri-apps/cli-linux-riscv64-gnu": 2.8.3 + "@tauri-apps/cli-linux-x64-gnu": 2.8.3 + "@tauri-apps/cli-linux-x64-musl": 2.8.3 + "@tauri-apps/cli-win32-arm64-msvc": 2.8.3 + "@tauri-apps/cli-win32-ia32-msvc": 2.8.3 + "@tauri-apps/cli-win32-x64-msvc": 2.8.3 + + "@types/babel__core@7.20.5": + dependencies: + "@babel/parser": 7.28.3 + "@babel/types": 7.28.2 + "@types/babel__generator": 7.27.0 + "@types/babel__template": 7.4.4 + "@types/babel__traverse": 7.28.0 + + "@types/babel__generator@7.27.0": + dependencies: + "@babel/types": 7.28.2 + + "@types/babel__template@7.4.4": + dependencies: + "@babel/parser": 7.28.3 + "@babel/types": 7.28.2 + + "@types/babel__traverse@7.28.0": + dependencies: + "@babel/types": 7.28.2 + + "@types/estree@1.0.8": {} + + "@types/react-dom@19.1.8(@types/react@19.1.12)": + dependencies: + "@types/react": 19.1.12 + + "@types/react@19.1.12": + dependencies: + csstype: 3.1.3 + + "@uiw/codemirror-extensions-basic-setup@4.25.1(@codemirror/autocomplete@6.18.6)(@codemirror/commands@6.8.1)(@codemirror/language@6.11.3)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/view@6.38.1)": + dependencies: + "@codemirror/autocomplete": 6.18.6 + "@codemirror/commands": 6.8.1 + "@codemirror/language": 6.11.3 + "@codemirror/lint": 6.8.5 + "@codemirror/search": 6.5.11 + "@codemirror/state": 6.5.2 + "@codemirror/view": 6.38.1 + + "@uiw/react-codemirror@4.25.1(@babel/runtime@7.28.3)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.11.3)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.38.1)(codemirror@6.0.2)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)": + dependencies: + "@babel/runtime": 7.28.3 + "@codemirror/commands": 6.8.1 + "@codemirror/state": 6.5.2 + "@codemirror/theme-one-dark": 6.1.3 + "@codemirror/view": 6.38.1 + "@uiw/codemirror-extensions-basic-setup": 4.25.1(@codemirror/autocomplete@6.18.6)(@codemirror/commands@6.8.1)(@codemirror/language@6.11.3)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/view@6.38.1) + codemirror: 6.0.2 + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + transitivePeerDependencies: + - "@codemirror/autocomplete" + - "@codemirror/language" + - "@codemirror/lint" + - "@codemirror/search" + + "@vitejs/plugin-react@4.7.0(vite@7.1.3)": + dependencies: + "@babel/core": 7.28.3 + "@babel/plugin-transform-react-jsx-self": 7.27.1(@babel/core@7.28.3) + "@babel/plugin-transform-react-jsx-source": 7.27.1(@babel/core@7.28.3) + "@rolldown/pluginutils": 1.0.0-beta.27 + "@types/babel__core": 7.20.5 + react-refresh: 0.17.0 + vite: 7.1.3 + transitivePeerDependencies: + - supports-color + + browserslist@4.25.3: + dependencies: + caniuse-lite: 1.0.30001737 + electron-to-chromium: 1.5.209 + node-releases: 2.0.19 + update-browserslist-db: 1.1.3(browserslist@4.25.3) + + caniuse-lite@1.0.30001737: {} + + codemirror@6.0.2: + dependencies: + "@codemirror/autocomplete": 6.18.6 + "@codemirror/commands": 6.8.1 + "@codemirror/language": 6.11.3 + "@codemirror/lint": 6.8.5 + "@codemirror/search": 6.5.11 + "@codemirror/state": 6.5.2 + "@codemirror/view": 6.38.1 + + convert-source-map@2.0.0: {} + + crelt@1.0.6: {} + + csstype@3.1.3: {} + + debug@4.4.1: + dependencies: + ms: 2.1.3 + + electron-to-chromium@1.5.209: {} + + esbuild@0.25.9: + optionalDependencies: + "@esbuild/aix-ppc64": 0.25.9 + "@esbuild/android-arm": 0.25.9 + "@esbuild/android-arm64": 0.25.9 + "@esbuild/android-x64": 0.25.9 + "@esbuild/darwin-arm64": 0.25.9 + "@esbuild/darwin-x64": 0.25.9 + "@esbuild/freebsd-arm64": 0.25.9 + "@esbuild/freebsd-x64": 0.25.9 + "@esbuild/linux-arm": 0.25.9 + "@esbuild/linux-arm64": 0.25.9 + "@esbuild/linux-ia32": 0.25.9 + "@esbuild/linux-loong64": 0.25.9 + "@esbuild/linux-mips64el": 0.25.9 + "@esbuild/linux-ppc64": 0.25.9 + "@esbuild/linux-riscv64": 0.25.9 + "@esbuild/linux-s390x": 0.25.9 + "@esbuild/linux-x64": 0.25.9 + "@esbuild/netbsd-arm64": 0.25.9 + "@esbuild/netbsd-x64": 0.25.9 + "@esbuild/openbsd-arm64": 0.25.9 + "@esbuild/openbsd-x64": 0.25.9 + "@esbuild/openharmony-arm64": 0.25.9 + "@esbuild/sunos-x64": 0.25.9 + "@esbuild/win32-arm64": 0.25.9 + "@esbuild/win32-ia32": 0.25.9 + "@esbuild/win32-x64": 0.25.9 + + escalade@3.2.0: {} + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fsevents@2.3.3: + optional: true + + gensync@1.0.0-beta.2: {} + + js-tokens@4.0.0: {} + + jsesc@3.1.0: {} + + json5@2.2.3: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + node-releases@2.0.19: {} + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + react-dom@19.1.1(react@19.1.1): + dependencies: + react: 19.1.1 + scheduler: 0.26.0 + + react-refresh@0.17.0: {} + + react@19.1.1: {} + + rollup@4.49.0: + dependencies: + "@types/estree": 1.0.8 + optionalDependencies: + "@rollup/rollup-android-arm-eabi": 4.49.0 + "@rollup/rollup-android-arm64": 4.49.0 + "@rollup/rollup-darwin-arm64": 4.49.0 + "@rollup/rollup-darwin-x64": 4.49.0 + "@rollup/rollup-freebsd-arm64": 4.49.0 + "@rollup/rollup-freebsd-x64": 4.49.0 + "@rollup/rollup-linux-arm-gnueabihf": 4.49.0 + "@rollup/rollup-linux-arm-musleabihf": 4.49.0 + "@rollup/rollup-linux-arm64-gnu": 4.49.0 + "@rollup/rollup-linux-arm64-musl": 4.49.0 + "@rollup/rollup-linux-loongarch64-gnu": 4.49.0 + "@rollup/rollup-linux-ppc64-gnu": 4.49.0 + "@rollup/rollup-linux-riscv64-gnu": 4.49.0 + "@rollup/rollup-linux-riscv64-musl": 4.49.0 + "@rollup/rollup-linux-s390x-gnu": 4.49.0 + "@rollup/rollup-linux-x64-gnu": 4.49.0 + "@rollup/rollup-linux-x64-musl": 4.49.0 + "@rollup/rollup-win32-arm64-msvc": 4.49.0 + "@rollup/rollup-win32-ia32-msvc": 4.49.0 + "@rollup/rollup-win32-x64-msvc": 4.49.0 + fsevents: 2.3.3 + + scheduler@0.26.0: {} + + semver@6.3.1: {} + + source-map-js@1.2.1: {} + + style-mod@4.1.2: {} + + tauri-plugin-mihomo-api@file:../..: + dependencies: + "@tauri-apps/api": 2.8.0 + + tinyglobby@0.2.14: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + typescript@5.8.3: {} + + update-browserslist-db@1.1.3(browserslist@4.25.3): + dependencies: + browserslist: 4.25.3 + escalade: 3.2.0 + picocolors: 1.1.1 + + vite@7.1.3: + dependencies: + esbuild: 0.25.9 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.49.0 + tinyglobby: 0.2.14 + optionalDependencies: + fsevents: 2.3.3 + + w3c-keyname@2.2.8: {} + + yallist@3.1.1: {} diff --git a/crates/tauri-plugin-mihomo/examples/check-mihomo-api/public/tauri.svg b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/public/tauri.svg new file mode 100644 index 000000000..31b62c928 --- /dev/null +++ b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/public/tauri.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/crates/tauri-plugin-mihomo/examples/check-mihomo-api/public/vite.svg b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/public/vite.svg new file mode 100644 index 000000000..ee9fadaf9 --- /dev/null +++ b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/public/vite.svg @@ -0,0 +1 @@ + diff --git a/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/.gitignore b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/.gitignore new file mode 100644 index 000000000..b21bd681d --- /dev/null +++ b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/.gitignore @@ -0,0 +1,7 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Generated by Tauri +# will have schema files for capabilities auto-completion +/gen/schemas diff --git a/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/Cargo.toml b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/Cargo.toml new file mode 100644 index 000000000..125fcd5ed --- /dev/null +++ b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "check-mihomo-api" +version = "0.1.0" +description = "A Tauri App to test Mihomo api" +authors = ["oomeow"] +edition = "2024" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +# The `_lib` suffix may seem redundant but it is necessary +# to make the lib name unique and wouldn't conflict with the bin name. +# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 +name = "mihomo_api_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +formatjson = "0.3.1" +tauri = { version = "2", features = [] } +tauri-plugin-mihomo = { path = "../../../" } diff --git a/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/build.rs b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/build.rs new file mode 100644 index 000000000..d860e1e6a --- /dev/null +++ b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/capabilities/default.json b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/capabilities/default.json new file mode 100644 index 000000000..e5a230d1f --- /dev/null +++ b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/capabilities/default.json @@ -0,0 +1,7 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Capability for the main window", + "windows": ["main"], + "permissions": ["core:default", "mihomo:default"] +} diff --git a/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/128x128.png b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/128x128.png new file mode 100644 index 000000000..6be5e50e9 Binary files /dev/null and b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/128x128.png differ diff --git a/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/128x128@2x.png b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/128x128@2x.png new file mode 100644 index 000000000..e81becee5 Binary files /dev/null and b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/128x128@2x.png differ diff --git a/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/32x32.png b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/32x32.png new file mode 100644 index 000000000..a437dd517 Binary files /dev/null and b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/32x32.png differ diff --git a/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/Square107x107Logo.png b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 000000000..0ca4f2719 Binary files /dev/null and b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/Square107x107Logo.png differ diff --git a/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/Square142x142Logo.png b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 000000000..b81f82039 Binary files /dev/null and b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/Square142x142Logo.png differ diff --git a/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/Square150x150Logo.png b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/Square150x150Logo.png new file mode 100644 index 000000000..624c7bfba Binary files /dev/null and b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/Square150x150Logo.png differ diff --git a/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/Square284x284Logo.png b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 000000000..c021d2ba7 Binary files /dev/null and b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/Square284x284Logo.png differ diff --git a/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/Square30x30Logo.png b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 000000000..621970023 Binary files /dev/null and b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/Square30x30Logo.png differ diff --git a/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/Square310x310Logo.png b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 000000000..f9bc04839 Binary files /dev/null and b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/Square310x310Logo.png differ diff --git a/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/Square44x44Logo.png b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 000000000..d5fbfb2ab Binary files /dev/null and b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/Square44x44Logo.png differ diff --git a/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/Square71x71Logo.png b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 000000000..63440d798 Binary files /dev/null and b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/Square71x71Logo.png differ diff --git a/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/Square89x89Logo.png b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/Square89x89Logo.png new file mode 100644 index 000000000..f3f705af2 Binary files /dev/null and b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/Square89x89Logo.png differ diff --git a/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/StoreLogo.png b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/StoreLogo.png new file mode 100644 index 000000000..455638826 Binary files /dev/null and b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/StoreLogo.png differ diff --git a/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/icon.icns b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/icon.icns new file mode 100644 index 000000000..12a5bcee2 Binary files /dev/null and b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/icon.icns differ diff --git a/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/icon.ico b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/icon.ico new file mode 100644 index 000000000..b3636e4b2 Binary files /dev/null and b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/icon.ico differ diff --git a/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/icon.png b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/icon.png new file mode 100644 index 000000000..e1cd2619e Binary files /dev/null and b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/icons/icon.png differ diff --git a/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/src/lib.rs b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/src/lib.rs new file mode 100644 index 000000000..1e78e6c2c --- /dev/null +++ b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/src/lib.rs @@ -0,0 +1,27 @@ +use tauri_plugin_mihomo::Protocol; + +// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ +#[tauri::command] +fn greet(name: &str) -> String { + format!("Hello, {}! You've been greeted from Rust!", name) +} + +#[tauri::command] +async fn cmd_format_json(text: &str) -> Result { + Ok(formatjson::format_json(text).map_err(|_| "failed to format json".to_string())?) + // Ok(format_json(text, " ")) +} + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .plugin( + tauri_plugin_mihomo::Builder::new() + .protocol(Protocol::LocalSocket) + .socket_path("/tmp/verge-mihomo.sock") + .build(), + ) + .invoke_handler(tauri::generate_handler![greet, cmd_format_json]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/src/main.rs b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/src/main.rs new file mode 100644 index 000000000..7885d70ab --- /dev/null +++ b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/src/main.rs @@ -0,0 +1,6 @@ +// Prevents additional console window on Windows in release, DO NOT REMOVE!! +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + mihomo_api_lib::run() +} diff --git a/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/tauri.conf.json b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/tauri.conf.json new file mode 100644 index 000000000..b7fc10e61 --- /dev/null +++ b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src-tauri/tauri.conf.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "check-mihomo-api", + "version": "0.1.0", + "identifier": "com.oomeow.check-mihomo-api", + "build": { + "beforeDevCommand": "pnpm dev", + "devUrl": "http://localhost:1420", + "beforeBuildCommand": "pnpm build", + "frontendDist": "../dist" + }, + "app": { + "windows": [ + { + "title": "check-mihomo-api", + "width": 800, + "height": 600 + } + ], + "security": { + "csp": null + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ] + } +} diff --git a/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src/App.css b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src/App.css new file mode 100644 index 000000000..85f7a4a1c --- /dev/null +++ b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src/App.css @@ -0,0 +1,116 @@ +.logo.vite:hover { + filter: drop-shadow(0 0 2em #747bff); +} + +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafb); +} +:root { + font-family: Inter, Avenir, Helvetica, Arial, sans-serif; + font-size: 16px; + line-height: 24px; + font-weight: 400; + + color: #0f0f0f; + background-color: #f6f6f6; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +.container { + margin: 0; + padding-top: 10vh; + display: flex; + flex-direction: column; + justify-content: center; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: 0.75s; +} + +.logo.tauri:hover { + filter: drop-shadow(0 0 2em #24c8db); +} + +.row { + display: flex; + justify-content: center; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} + +a:hover { + color: #535bf2; +} + +h1 { + text-align: center; +} + +input, +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + color: #0f0f0f; + background-color: #ffffff; + transition: border-color 0.25s; + box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); +} + +button { + cursor: pointer; +} + +button:hover { + border-color: #396cd8; +} +button:active { + border-color: #396cd8; + background-color: #e8e8e8; +} + +input, +button { + outline: none; +} + +#greet-input { + margin-right: 5px; +} + +@media (prefers-color-scheme: dark) { + :root { + color: #f6f6f6; + background-color: #2f2f2f; + } + + a:hover { + color: #24c8db; + } + + input, + button { + color: #ffffff; + background-color: #0f0f0f98; + } + button:active { + background-color: #0f0f0f69; + } +} diff --git a/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src/App.tsx b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src/App.tsx new file mode 100644 index 000000000..8bcf3e6d5 --- /dev/null +++ b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src/App.tsx @@ -0,0 +1,49 @@ +import { useState } from "react"; +import { invoke } from "@tauri-apps/api/core"; +import "./App.css"; +import { getGroups } from "tauri-plugin-mihomo-api"; +import CodeMirror from "@uiw/react-codemirror"; +import { json } from "@codemirror/lang-json"; + +function App() { + const [response, setResponse] = useState(""); + + async function format_json(text: string) { + return await invoke("cmd_format_json", { text }); + } + + async function check() { + try { + // await upgradeCore(); + let data = await getGroups(); + const formattedJson = await format_json(JSON.stringify(data)); + setResponse(formattedJson); + } catch (err: any) { + setResponse(err.toString()); + } + } + + return ( +
+
+ +
+ +
+ ); +} + +export default App; diff --git a/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src/assets/react.svg b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src/assets/react.svg new file mode 100644 index 000000000..8e0e0f15c --- /dev/null +++ b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src/assets/react.svg @@ -0,0 +1 @@ + diff --git a/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src/main.tsx b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src/main.tsx new file mode 100644 index 000000000..2be325ed2 --- /dev/null +++ b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src/main.tsx @@ -0,0 +1,9 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App"; + +ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( + + + , +); diff --git a/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src/vite-env.d.ts b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/crates/tauri-plugin-mihomo/examples/check-mihomo-api/tsconfig.json b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/tsconfig.json new file mode 100644 index 000000000..a7fc6fbf2 --- /dev/null +++ b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/crates/tauri-plugin-mihomo/examples/check-mihomo-api/tsconfig.node.json b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/tsconfig.node.json new file mode 100644 index 000000000..42872c59f --- /dev/null +++ b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/crates/tauri-plugin-mihomo/examples/check-mihomo-api/vite.config.ts b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/vite.config.ts new file mode 100644 index 000000000..ddad22a3e --- /dev/null +++ b/crates/tauri-plugin-mihomo/examples/check-mihomo-api/vite.config.ts @@ -0,0 +1,32 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +// @ts-expect-error process is a nodejs global +const host = process.env.TAURI_DEV_HOST; + +// https://vite.dev/config/ +export default defineConfig(async () => ({ + plugins: [react()], + + // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` + // + // 1. prevent Vite from obscuring rust errors + clearScreen: false, + // 2. tauri expects a fixed port, fail if that port is not available + server: { + port: 1420, + strictPort: true, + host: host || false, + hmr: host + ? { + protocol: "ws", + host, + port: 1421, + } + : undefined, + watch: { + // 3. tell Vite to ignore watching `src-tauri` + ignored: ["**/src-tauri/**"], + }, + }, +})); diff --git a/crates/tauri-plugin-mihomo/guest-js/bindings/BaseConfig.ts b/crates/tauri-plugin-mihomo/guest-js/bindings/BaseConfig.ts new file mode 100644 index 000000000..2a64fe378 --- /dev/null +++ b/crates/tauri-plugin-mihomo/guest-js/bindings/BaseConfig.ts @@ -0,0 +1,9 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ClashMode } from "./ClashMode"; +import type { FindProcessMode } from "./FindProcessMode"; +import type { GeoXUrl } from "./GeoXUrl"; +import type { LogLevel } from "./LogLevel"; +import type { TuicServer } from "./TuicServer"; +import type { TunConfig } from "./TunConfig"; + +export type BaseConfig = { port: number, socksPort: number, redirPort: number, tproxyPort: number, mixedPort: number, tun: TunConfig, tuicServer: TuicServer, ssConfig: string, vmessConfig: string, authentication: Array | null, skipAuthPrefixes: Array | null, lanAllowedIps: Array | null, lanDisallowedIps: Array | null, allowLan: boolean, bindAddress: string, inboundTfo: boolean, inboundMptcp: boolean, mode: ClashMode, unifiedDelay: boolean, logLevel: LogLevel, ipv6: boolean, interfaceName: string, routingMark: number, geoxUrl: GeoXUrl, geoAutoUpdate: boolean, geoUpdateInterval: number, geodataMode: boolean, geodataLoader: string, geositeMatcher: string, tcpConcurrent: boolean, findProcessMode: FindProcessMode, sniffing: boolean, globalClientFingerprint: string, globalUa: string, etagSupport: boolean, keepAliveInterval: number, keepAliveIdle: number, disableKeepAlive: boolean, }; diff --git a/crates/tauri-plugin-mihomo/guest-js/bindings/BrutalOption.ts b/crates/tauri-plugin-mihomo/guest-js/bindings/BrutalOption.ts new file mode 100644 index 000000000..ab8f92a22 --- /dev/null +++ b/crates/tauri-plugin-mihomo/guest-js/bindings/BrutalOption.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type BrutalOption = { enabled: boolean, up?: string, down?: string, }; diff --git a/crates/tauri-plugin-mihomo/guest-js/bindings/ClashMode.ts b/crates/tauri-plugin-mihomo/guest-js/bindings/ClashMode.ts new file mode 100644 index 000000000..4536881be --- /dev/null +++ b/crates/tauri-plugin-mihomo/guest-js/bindings/ClashMode.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * clash mode enum + */ +export type ClashMode = "rule" | "global" | "direct"; diff --git a/crates/tauri-plugin-mihomo/guest-js/bindings/Connection.ts b/crates/tauri-plugin-mihomo/guest-js/bindings/Connection.ts new file mode 100644 index 000000000..d22eb423d --- /dev/null +++ b/crates/tauri-plugin-mihomo/guest-js/bindings/Connection.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ConnectionMetaData } from "./ConnectionMetaData"; + +export type Connection = { id: string, metadata: ConnectionMetaData, upload: bigint, download: bigint, start: string, chains: Array, rule: string, rulePayload: string, }; diff --git a/crates/tauri-plugin-mihomo/guest-js/bindings/ConnectionMetaData.ts b/crates/tauri-plugin-mihomo/guest-js/bindings/ConnectionMetaData.ts new file mode 100644 index 000000000..53796663e --- /dev/null +++ b/crates/tauri-plugin-mihomo/guest-js/bindings/ConnectionMetaData.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ConnectionType } from "./ConnectionType"; +import type { DNSMode } from "./DNSMode"; +import type { Network } from "./Network"; + +export type ConnectionMetaData = { network: Network, type: ConnectionType, sourceIP: string, destinationIP: string, sourceGeoIP: Array | null, destinationGeoIP: Array | null, sourceIPASN: string, destinationIPASN: string, sourcePort: string, destinationPort: string, inboundIP: string, inboundPort: string, inboundName: string, inboundUser: string, host: string, dnsMode: DNSMode, uid: number, process: string, processPath: string, specialProxy: string, specialRules: string, remoteDestination: string, dscp: number, sniffHost: string, }; diff --git a/crates/tauri-plugin-mihomo/guest-js/bindings/ConnectionType.ts b/crates/tauri-plugin-mihomo/guest-js/bindings/ConnectionType.ts new file mode 100644 index 000000000..1ed19da47 --- /dev/null +++ b/crates/tauri-plugin-mihomo/guest-js/bindings/ConnectionType.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ConnectionType = "HTTP" | "HTTPS" | "Socks4" | "Socks5" | "ShadowSocks" | "Vmess" | "Vless" | "Redir" | "TProxy" | "Trojan" | "Tunnel" | "Tun" | "Tuic" | "Hysteria2" | "AnyTLS" | "Inner"; diff --git a/crates/tauri-plugin-mihomo/guest-js/bindings/Connections.ts b/crates/tauri-plugin-mihomo/guest-js/bindings/Connections.ts new file mode 100644 index 000000000..0eceacea1 --- /dev/null +++ b/crates/tauri-plugin-mihomo/guest-js/bindings/Connections.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Connection } from "./Connection"; + +/** + * connections + */ +export type Connections = { downloadTotal: bigint, uploadTotal: bigint, connections: Array | null, memory: number, }; diff --git a/crates/tauri-plugin-mihomo/guest-js/bindings/CoreUpdaterChannel.ts b/crates/tauri-plugin-mihomo/guest-js/bindings/CoreUpdaterChannel.ts new file mode 100644 index 000000000..62e7837fa --- /dev/null +++ b/crates/tauri-plugin-mihomo/guest-js/bindings/CoreUpdaterChannel.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type CoreUpdaterChannel = "release" | "alpha" | "auto"; diff --git a/crates/tauri-plugin-mihomo/guest-js/bindings/DNSMode.ts b/crates/tauri-plugin-mihomo/guest-js/bindings/DNSMode.ts new file mode 100644 index 000000000..febcc5ed3 --- /dev/null +++ b/crates/tauri-plugin-mihomo/guest-js/bindings/DNSMode.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type DNSMode = "normal" | "fake-ip" | "redir-host" | "hosts"; diff --git a/crates/tauri-plugin-mihomo/guest-js/bindings/DelayHistory.ts b/crates/tauri-plugin-mihomo/guest-js/bindings/DelayHistory.ts new file mode 100644 index 000000000..032458e8a --- /dev/null +++ b/crates/tauri-plugin-mihomo/guest-js/bindings/DelayHistory.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type DelayHistory = { time: string, delay: number, }; diff --git a/crates/tauri-plugin-mihomo/guest-js/bindings/Extra.ts b/crates/tauri-plugin-mihomo/guest-js/bindings/Extra.ts new file mode 100644 index 000000000..bba1baaa0 --- /dev/null +++ b/crates/tauri-plugin-mihomo/guest-js/bindings/Extra.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { DelayHistory } from "./DelayHistory"; + +export type Extra = { alive: boolean, history: Array, }; diff --git a/crates/tauri-plugin-mihomo/guest-js/bindings/FindProcessMode.ts b/crates/tauri-plugin-mihomo/guest-js/bindings/FindProcessMode.ts new file mode 100644 index 000000000..e1b57a67f --- /dev/null +++ b/crates/tauri-plugin-mihomo/guest-js/bindings/FindProcessMode.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type FindProcessMode = "strict" | "always" | "off"; diff --git a/crates/tauri-plugin-mihomo/guest-js/bindings/GeoXUrl.ts b/crates/tauri-plugin-mihomo/guest-js/bindings/GeoXUrl.ts new file mode 100644 index 000000000..9e5d3df15 --- /dev/null +++ b/crates/tauri-plugin-mihomo/guest-js/bindings/GeoXUrl.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type GeoXUrl = { geoIp: string, mmdb: string, asn: string, geoSite: string, }; diff --git a/crates/tauri-plugin-mihomo/guest-js/bindings/Groups.ts b/crates/tauri-plugin-mihomo/guest-js/bindings/Groups.ts new file mode 100644 index 000000000..71b549de7 --- /dev/null +++ b/crates/tauri-plugin-mihomo/guest-js/bindings/Groups.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Proxy } from "./Proxy"; + +/** + * group proxies + */ +export type Groups = { proxies: Array, }; diff --git a/crates/tauri-plugin-mihomo/guest-js/bindings/Log.ts b/crates/tauri-plugin-mihomo/guest-js/bindings/Log.ts new file mode 100644 index 000000000..26a42ae39 --- /dev/null +++ b/crates/tauri-plugin-mihomo/guest-js/bindings/Log.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type Log = { type: string, payload: string, }; diff --git a/crates/tauri-plugin-mihomo/guest-js/bindings/LogLevel.ts b/crates/tauri-plugin-mihomo/guest-js/bindings/LogLevel.ts new file mode 100644 index 000000000..e1b912cb9 --- /dev/null +++ b/crates/tauri-plugin-mihomo/guest-js/bindings/LogLevel.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type LogLevel = "debug" | "info" | "warning" | "error" | "silent"; diff --git a/crates/tauri-plugin-mihomo/guest-js/bindings/Memory.ts b/crates/tauri-plugin-mihomo/guest-js/bindings/Memory.ts new file mode 100644 index 000000000..e7fcc4477 --- /dev/null +++ b/crates/tauri-plugin-mihomo/guest-js/bindings/Memory.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type Memory = { inuse: number, oslimit: number, }; diff --git a/crates/tauri-plugin-mihomo/guest-js/bindings/MihomoVersion.ts b/crates/tauri-plugin-mihomo/guest-js/bindings/MihomoVersion.ts new file mode 100644 index 000000000..ef4ae789e --- /dev/null +++ b/crates/tauri-plugin-mihomo/guest-js/bindings/MihomoVersion.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * mihomo version + */ +export type MihomoVersion = { meta: boolean, version: string, }; diff --git a/crates/tauri-plugin-mihomo/guest-js/bindings/MuxOption.ts b/crates/tauri-plugin-mihomo/guest-js/bindings/MuxOption.ts new file mode 100644 index 000000000..78ba2b54d --- /dev/null +++ b/crates/tauri-plugin-mihomo/guest-js/bindings/MuxOption.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { BrutalOption } from "./BrutalOption"; + +export type MuxOption = { padding?: boolean, brutal?: BrutalOption, }; diff --git a/crates/tauri-plugin-mihomo/guest-js/bindings/Network.ts b/crates/tauri-plugin-mihomo/guest-js/bindings/Network.ts new file mode 100644 index 000000000..f7615ee4b --- /dev/null +++ b/crates/tauri-plugin-mihomo/guest-js/bindings/Network.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type Network = "tcp" | "udp" | "all"; diff --git a/crates/tauri-plugin-mihomo/guest-js/bindings/ProviderType.ts b/crates/tauri-plugin-mihomo/guest-js/bindings/ProviderType.ts new file mode 100644 index 000000000..01a3728b2 --- /dev/null +++ b/crates/tauri-plugin-mihomo/guest-js/bindings/ProviderType.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ProviderType = "Proxy" | "Rule"; diff --git a/crates/tauri-plugin-mihomo/guest-js/bindings/Proxies.ts b/crates/tauri-plugin-mihomo/guest-js/bindings/Proxies.ts new file mode 100644 index 000000000..f1eaf2304 --- /dev/null +++ b/crates/tauri-plugin-mihomo/guest-js/bindings/Proxies.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Proxy } from "./Proxy"; + +/** + * proxies + */ +export type Proxies = { proxies: { [key in string]?: Proxy }, }; diff --git a/crates/tauri-plugin-mihomo/guest-js/bindings/Proxy.ts b/crates/tauri-plugin-mihomo/guest-js/bindings/Proxy.ts new file mode 100644 index 000000000..684f7a0ac --- /dev/null +++ b/crates/tauri-plugin-mihomo/guest-js/bindings/Proxy.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { DelayHistory } from "./DelayHistory"; +import type { Extra } from "./Extra"; +import type { ProxyType } from "./ProxyType"; + +export type Proxy = { all?: Array, expectedStatus?: string, fixed?: string, hidden?: boolean, icon?: string, now?: string, testUrl?: string, id?: string, alive: boolean, history: Array, extra: { [key in string]?: Extra }, name: string, udp: boolean, uot: boolean, type: ProxyType, xudp: boolean, tfo: boolean, mptcp: boolean, smux: boolean, interface: string, dialerProxy: string, routingMark: number, }; diff --git a/crates/tauri-plugin-mihomo/guest-js/bindings/ProxyDelay.ts b/crates/tauri-plugin-mihomo/guest-js/bindings/ProxyDelay.ts new file mode 100644 index 000000000..7ab1ba0b6 --- /dev/null +++ b/crates/tauri-plugin-mihomo/guest-js/bindings/ProxyDelay.ts @@ -0,0 +1,8 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * proxy delay result + * + * displays a message if it times out, otherwise it only displays the delay + */ +export type ProxyDelay = { delay: number, }; diff --git a/crates/tauri-plugin-mihomo/guest-js/bindings/ProxyProvider.ts b/crates/tauri-plugin-mihomo/guest-js/bindings/ProxyProvider.ts new file mode 100644 index 000000000..96951e84e --- /dev/null +++ b/crates/tauri-plugin-mihomo/guest-js/bindings/ProxyProvider.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ProviderType } from "./ProviderType"; +import type { Proxy } from "./Proxy"; +import type { SubScriptionInfo } from "./SubScriptionInfo"; +import type { VehicleType } from "./VehicleType"; + +export type ProxyProvider = { name: string, type: ProviderType, vehicleType: VehicleType, proxies: Array, testUrl: string, expectedStatus: string, updatedAt: string | null, subscriptionInfo: SubScriptionInfo | null, }; diff --git a/crates/tauri-plugin-mihomo/guest-js/bindings/ProxyProviders.ts b/crates/tauri-plugin-mihomo/guest-js/bindings/ProxyProviders.ts new file mode 100644 index 000000000..4cc18c36b --- /dev/null +++ b/crates/tauri-plugin-mihomo/guest-js/bindings/ProxyProviders.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ProxyProvider } from "./ProxyProvider"; + +export type ProxyProviders = { providers: { [key in string]?: ProxyProvider }, }; diff --git a/crates/tauri-plugin-mihomo/guest-js/bindings/ProxyType.ts b/crates/tauri-plugin-mihomo/guest-js/bindings/ProxyType.ts new file mode 100644 index 000000000..b0aa6a60d --- /dev/null +++ b/crates/tauri-plugin-mihomo/guest-js/bindings/ProxyType.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ProxyType = "Direct" | "Reject" | "RejectDrop" | "Compatible" | "Pass" | "Dns" | "Shadowsocks" | "ShadowsocksR" | "Snell" | "Socks5" | "Http" | "Vmess" | "Vless" | "Trojan" | "Hysteria" | "Hysteria2" | "WireGuard" | "Tuic" | "Ssh" | "Mieru" | "AnyTLS" | "Relay" | "Selector" | "Fallback" | "URLTest" | "LoadBalance"; diff --git a/crates/tauri-plugin-mihomo/guest-js/bindings/Rule.ts b/crates/tauri-plugin-mihomo/guest-js/bindings/Rule.ts new file mode 100644 index 000000000..f10cc4056 --- /dev/null +++ b/crates/tauri-plugin-mihomo/guest-js/bindings/Rule.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { RuleType } from "./RuleType"; + +export type Rule = { type: RuleType, payload: string, proxy: string, size: number, }; diff --git a/crates/tauri-plugin-mihomo/guest-js/bindings/RuleBehavior.ts b/crates/tauri-plugin-mihomo/guest-js/bindings/RuleBehavior.ts new file mode 100644 index 000000000..4be49be69 --- /dev/null +++ b/crates/tauri-plugin-mihomo/guest-js/bindings/RuleBehavior.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type RuleBehavior = "Domain" | "IPCIDR" | "Classical"; diff --git a/crates/tauri-plugin-mihomo/guest-js/bindings/RuleFormat.ts b/crates/tauri-plugin-mihomo/guest-js/bindings/RuleFormat.ts new file mode 100644 index 000000000..5239e2428 --- /dev/null +++ b/crates/tauri-plugin-mihomo/guest-js/bindings/RuleFormat.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type RuleFormat = "YamlRule" | "TextRule" | "MrsRule"; diff --git a/crates/tauri-plugin-mihomo/guest-js/bindings/RuleProvider.ts b/crates/tauri-plugin-mihomo/guest-js/bindings/RuleProvider.ts new file mode 100644 index 000000000..254649f49 --- /dev/null +++ b/crates/tauri-plugin-mihomo/guest-js/bindings/RuleProvider.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ProviderType } from "./ProviderType"; +import type { RuleBehavior } from "./RuleBehavior"; +import type { RuleFormat } from "./RuleFormat"; +import type { VehicleType } from "./VehicleType"; + +export type RuleProvider = { behavior: RuleBehavior, format: RuleFormat, name: string, ruleCount: number, type: ProviderType, updatedAt: string, vehicleType: VehicleType, }; diff --git a/crates/tauri-plugin-mihomo/guest-js/bindings/RuleProviders.ts b/crates/tauri-plugin-mihomo/guest-js/bindings/RuleProviders.ts new file mode 100644 index 000000000..9603bc17d --- /dev/null +++ b/crates/tauri-plugin-mihomo/guest-js/bindings/RuleProviders.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { RuleProvider } from "./RuleProvider"; + +export type RuleProviders = { providers: { [key in string]?: RuleProvider }, }; diff --git a/crates/tauri-plugin-mihomo/guest-js/bindings/RuleType.ts b/crates/tauri-plugin-mihomo/guest-js/bindings/RuleType.ts new file mode 100644 index 000000000..b41c68920 --- /dev/null +++ b/crates/tauri-plugin-mihomo/guest-js/bindings/RuleType.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type RuleType = "Domain" | "DomainSuffix" | "DomainKeyword" | "DomainRegex" | "GeoSite" | "GeoIP" | "SrcGeoIP" | "IPASN" | "SrcIPASN" | "IPCIDR" | "SrcIPCIDR" | "IPSuffix" | "SrcIPSuffix" | "SrcPort" | "DstPort" | "InPort" | "InUser" | "InName" | "InType" | "ProcessName" | "ProcessPath" | "ProcessNameRegex" | "ProcessPathRegex" | "Match" | "RuleSet" | "Network" | "DSCP" | "Uid" | "SubRules" | "AND" | "OR" | "NOT"; diff --git a/crates/tauri-plugin-mihomo/guest-js/bindings/Rules.ts b/crates/tauri-plugin-mihomo/guest-js/bindings/Rules.ts new file mode 100644 index 000000000..8d54d4d25 --- /dev/null +++ b/crates/tauri-plugin-mihomo/guest-js/bindings/Rules.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Rule } from "./Rule"; + +/** + * rules + */ +export type Rules = { rules: Array, }; diff --git a/crates/tauri-plugin-mihomo/guest-js/bindings/SubScriptionInfo.ts b/crates/tauri-plugin-mihomo/guest-js/bindings/SubScriptionInfo.ts new file mode 100644 index 000000000..29aad11d9 --- /dev/null +++ b/crates/tauri-plugin-mihomo/guest-js/bindings/SubScriptionInfo.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type SubScriptionInfo = { Upload: bigint, Download: bigint, Total: bigint, Expire: bigint, }; diff --git a/crates/tauri-plugin-mihomo/guest-js/bindings/Traffic.ts b/crates/tauri-plugin-mihomo/guest-js/bindings/Traffic.ts new file mode 100644 index 000000000..6b540f284 --- /dev/null +++ b/crates/tauri-plugin-mihomo/guest-js/bindings/Traffic.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type Traffic = { up: bigint, down: bigint, }; diff --git a/crates/tauri-plugin-mihomo/guest-js/bindings/TuicServer.ts b/crates/tauri-plugin-mihomo/guest-js/bindings/TuicServer.ts new file mode 100644 index 000000000..30e483197 --- /dev/null +++ b/crates/tauri-plugin-mihomo/guest-js/bindings/TuicServer.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { MuxOption } from "./MuxOption"; + +export type TuicServer = { enable: boolean, listen: string, token?: Array, users?: { [key in string]?: string }, certificate: string, privateKey: string, echKey: string, congestionController?: string, maxIdleTime?: number, authenticationTimeout?: number, alpn?: Array, maxUdpRelayPacketSize?: number, maxDatagramFrameSize?: number, cwnd?: number, muxOption?: MuxOption, }; diff --git a/crates/tauri-plugin-mihomo/guest-js/bindings/TunConfig.ts b/crates/tauri-plugin-mihomo/guest-js/bindings/TunConfig.ts new file mode 100644 index 000000000..a38469481 --- /dev/null +++ b/crates/tauri-plugin-mihomo/guest-js/bindings/TunConfig.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { TunStack } from "./TunStack"; + +export type TunConfig = { enable: boolean, device: string, stack: TunStack, dnsHijack: Array, autoRoute: boolean, autoDetectInterface: boolean, mtu?: number, gso?: boolean, gsoMaxSize?: number, inet4Address?: Array, inet6Address?: Array, iproute2TableIndex?: number, iproute2RuleIndex?: number, autoRedirect?: boolean, autoRedirectInputMark?: number, autoRedirectOutputMark?: number, loopbackAddress?: string, strictRoute?: boolean, routeAddress?: Array, routeAddressSet?: Array, routeExcludeAddress?: Array, routeExcludeAddressSet?: Array, includeInterface?: Array, excludeInterface?: Array, includeUid?: Array, includeUidRange?: Array, excludeUid?: Array, excludeUidRange?: Array, excludeSrcPort?: Array, excludeSrcPortRange?: Array, excludeDstPort?: Array, excludeDstPortRange?: Array, includeAndroidUser?: Array, includePackage?: Array, excludePackage?: Array, endpointIndependentNat?: boolean, udpTimeout?: bigint, fileDescriptor: number, inet4RouteAddress?: Array, inet6RouteAddress?: Array, inet4RouteExcludeAddress?: Array, inet6RouteExcludeAddress?: Array, recvmsgx?: boolean, sendmsgx?: boolean, }; diff --git a/crates/tauri-plugin-mihomo/guest-js/bindings/TunStack.ts b/crates/tauri-plugin-mihomo/guest-js/bindings/TunStack.ts new file mode 100644 index 000000000..4ee3992b3 --- /dev/null +++ b/crates/tauri-plugin-mihomo/guest-js/bindings/TunStack.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * tun stack enum + */ +export type TunStack = "Mixed" | "gVisor" | "System"; diff --git a/crates/tauri-plugin-mihomo/guest-js/bindings/VehicleType.ts b/crates/tauri-plugin-mihomo/guest-js/bindings/VehicleType.ts new file mode 100644 index 000000000..8fa8795f9 --- /dev/null +++ b/crates/tauri-plugin-mihomo/guest-js/bindings/VehicleType.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type VehicleType = "File" | "HTTP" | "Compatible" | "Inline"; diff --git a/crates/tauri-plugin-mihomo/guest-js/bindings/index.ts b/crates/tauri-plugin-mihomo/guest-js/bindings/index.ts new file mode 100644 index 000000000..ddf91a6d0 --- /dev/null +++ b/crates/tauri-plugin-mihomo/guest-js/bindings/index.ts @@ -0,0 +1,40 @@ +export * from "./BaseConfig"; +export * from "./BrutalOption"; +export * from "./ClashMode"; +export * from "./Connection"; +export * from "./ConnectionMetaData"; +export * from "./Connections"; +export * from "./ConnectionType"; +export * from "./CoreUpdaterChannel"; +export * from "./DelayHistory"; +export * from "./DNSMode"; +export * from "./Extra"; +export * from "./FindProcessMode"; +export * from "./GeoXUrl"; +export * from "./Groups"; +export * from "./Log"; +export * from "./LogLevel"; +export * from "./Memory"; +export * from "./MihomoVersion"; +export * from "./MuxOption"; +export * from "./Network"; +export * from "./ProviderType"; +export * from "./Proxies"; +export * from "./Proxy"; +export * from "./ProxyDelay"; +export * from "./ProxyProvider"; +export * from "./ProxyProviders"; +export * from "./ProxyType"; +export * from "./Rule"; +export * from "./RuleBehavior"; +export * from "./RuleFormat"; +export * from "./RuleProvider"; +export * from "./RuleProviders"; +export * from "./Rules"; +export * from "./RuleType"; +export * from "./SubScriptionInfo"; +export * from "./Traffic"; +export * from "./TuicServer"; +export * from "./TunConfig"; +export * from "./TunStack"; +export * from "./VehicleType"; diff --git a/crates/tauri-plugin-mihomo/guest-js/index.ts b/crates/tauri-plugin-mihomo/guest-js/index.ts new file mode 100644 index 000000000..119fbcc7c --- /dev/null +++ b/crates/tauri-plugin-mihomo/guest-js/index.ts @@ -0,0 +1,552 @@ +import { Channel, invoke } from "@tauri-apps/api/core"; +import { + BaseConfig, + Connections, + CoreUpdaterChannel, + Groups, + LogLevel, + MihomoVersion, + Proxies, + Proxy, + ProxyDelay, + ProxyProvider, + ProxyProviders, + RuleProviders, + Rules, +} from "./bindings"; + +export * from "./bindings"; +export type MihomoGroupDelay = Record; + +// ======================= functions ======================= + +/** + * 更新控制器地址 + * @param controller 控制器地址, 例如:127.0.0.1:9090 + */ +export async function updateController(controller: string): Promise { + const [host, portStr] = controller.trim().split(":"); + const port = parseInt(portStr); + await invoke("plugin:mihomo|update_controller", { host, port }); +} + +/** + * 更新控制器的密钥 + * @param secret 控制器的密钥 + */ +export async function updateSecret(secret: string): Promise { + await invoke("plugin:mihomo|update_secret", { secret }); +} + +/** + * 获取Mihomo版本信息 + */ +export async function getVersion(): Promise { + return await invoke("plugin:mihomo|get_version"); +} + +/** + * 清除 FakeIP 缓存 + */ +export async function flushFakeIp(): Promise { + await invoke("plugin:mihomo|flush_fakeip"); +} + +/** + * 清除 DNS 缓存 + */ +export async function flushDNS(): Promise { + await invoke("plugin:mihomo|flush_dns"); +} + +// connections +/** + * 获取所有连接信息 + * @returns 所有连接信息 + */ +export async function getConnections(): Promise { + return await invoke("plugin:mihomo|get_connections"); +} + +/** + * 关闭所有连接 + */ +export async function closeAllConnections(): Promise { + await invoke("plugin:mihomo|close_all_connections"); +} + +/** + * 关闭指定连接 + * @param connectionId 连接 ID + */ +export async function closeConnections(connectionId: string): Promise { + await invoke("plugin:mihomo|close_connections", { connectionId }); +} + +// groups +/** + * 获取所有代理组信息 + * @returns 所有代理组信息 + */ +export async function getGroups(): Promise { + return await invoke("plugin:mihomo|get_groups"); +} + +/** + * 获取指定代理组信息 + * @param groupName 代理组名称 + * @returns 指定代理组信息 + */ +export async function getGroupByName(groupName: string): Promise { + return await invoke("plugin:mihomo|get_group_by_name", { + groupName, + }); +} + +/** + * 对指定代理组进行延迟测试 + * + * 注:返回值中不包含超时的节点 + * @param groupName 代理组名称 + * @param testUrl 测试 url + * @param timeout 超时时间(毫秒) + * @param keepFixed 是否保留已固定的节点, 默认 false + * @returns 代理组里代理节点的延迟 + */ +export async function delayGroup( + groupName: string, + testUrl: string, + timeout: number, + keepFixed = false, +): Promise { + return await invoke("plugin:mihomo|delay_group", { + groupName, + testUrl, + timeout, + keepFixed, + }); +} + +// providers +/** + * 获取所有代理提供者信息 + * @returns 所有代理提供者信息 + */ +export async function getProxyProviders(): Promise { + return await invoke("plugin:mihomo|get_proxy_providers"); +} + +/** + * 获取指定的代理提供者信息 + * @param providerName 代理提供者名称 + * @returns 代理提供者信息 + */ +export async function getProxyProviderByName( + providerName: string, +): Promise { + return await invoke( + "plugin:mihomo|get_proxy_provider_by_name", + { providerName }, + ); +} + +/** + * 更新代理提供者信息 + * @param providerName 代理提供者名称 + */ +export async function updateProxyProvider(providerName: string): Promise { + await invoke("plugin:mihomo|update_proxy_provider", { + providerName, + }); +} + +/** + * 对指定的代理提供者进行健康检查 + * @param providerName 代理提供者名称 + */ +export async function healthcheckProxyProvider( + providerName: string, +): Promise { + await invoke("plugin:mihomo|healthcheck_proxy_provider", { + providerName, + }); +} + +/** + * 对指定代理提供者下的指定节点(非代理组)进行健康检查, 并返回新的延迟信息 + * @param providerName 代理提供者名称 + * @param proxyName 代理节点名称 (非代理组) + * @param testUrl 测试 url + * @param timeout 超时时间 + * @returns 该代理节点的延迟 + */ +export async function healthcheckNodeInProvider( + providerName: string, + proxyName: string, + testUrl: string, + timeout: number, +): Promise { + return await invoke( + "plugin:mihomo|healthcheck_node_in_provider", + { + providerName, + proxyName, + testUrl, + timeout, + }, + ); +} + +// proxies +/** + * 获取所有代理信息 + * @returns 所有代理信息 + */ +export async function getProxies(): Promise { + return await invoke("plugin:mihomo|get_proxies"); +} + +/** + * 获取指定代理信息 + * @param proxyName 代理名称 + * @returns 代理信息 + */ +export async function getProxyByName(proxyName: string): Promise { + return await invoke("plugin:mihomo|get_proxy_by_name", { + proxiesName: proxyName, + }); +} + +/** + * 为指定代理选择节点 + * + * 一般为指定代理组下使用指定的代理节点 【代理组/节点】 + * @param groupName 代理组名称 + * @param node 代理节点 + */ +export async function selectNodeForGroup( + groupName: string, + node: string, +): Promise { + await invoke("plugin:mihomo|select_node_for_group", { + groupName, + node, + }); +} + +/** + * 指定代理组下不再使用固定的代理节点 + * + * 一般用于自动选择的代理组(例如:URLTest 类型的代理组)下的节点 + * @param groupName 代理组名称 + */ +export async function unfixedProxy(groupName: string): Promise { + await invoke("plugin:mihomo|unfixed_proxy", { + groupName, + }); +} + +/** + * 对指定代理进行延迟测试 + * + * 一般用于代理节点的延迟测试,也可传代理组名称(只会测试代理组下选中的代理节点) + * @param proxyName 代理节点名称 + * @param testUrl 测试 url + * @param timeout 超时时间 + * @returns 该代理节点的延迟信息 + */ +export async function delayProxyByName( + proxyName: string, + testUrl: string, + timeout: number, +): Promise { + return await invoke("plugin:mihomo|delay_proxy_by_name", { + proxyName, + testUrl, + timeout, + }); +} + +// rules +/** + * 获取所有规则信息 + * @returns 所有规则信息 + */ +export async function getRules(): Promise { + return await invoke("plugin:mihomo|get_rules"); +} + +/** + * 获取所有规则提供者信息 + * @returns 所有规则提供者信息 + */ +export async function getRuleProviders(): Promise { + return await invoke("plugin:mihomo|get_rule_providers"); +} + +/** + * 更新规则提供者信息 + * @param providerName 规则提供者名称 + */ +export async function updateRuleProvider(providerName: string): Promise { + await invoke("plugin:mihomo|update_rule_provider", { + providerName, + }); +} + +// runtime config +/** + * 获取基础配置 + * @returns 基础配置 + */ +export async function getBaseConfig(): Promise { + return await invoke("plugin:mihomo|get_base_config"); +} + +/** + * 重新加载配置 + * @param force 强制更新 + * @param configPath 配置文件路径 + */ +export async function reloadConfig( + force: boolean, + configPath: string, +): Promise { + await invoke("plugin:mihomo|reload_config", { + force, + configPath, + }); +} + +/** + * 更改基础配置 + * @param data 基础配置更改后的内容, 例如:{"tun": {"enabled": true}} + */ +export async function patchBaseConfig( + data: Record, +): Promise { + await invoke("plugin:mihomo|patch_base_config", { + data, + }); +} + +/** + * 更新 Geo + */ +export async function updateGeo(): Promise { + await invoke("plugin:mihomo|update_geo"); +} + +/** + * 重启核心 + */ +export async function restart(): Promise { + await invoke("plugin:mihomo|restart"); +} + +// upgrade +/** + * 升级核心,将当前运行中的核心升级到选择的通道的最新版 + * @param channel 升级通道, 默认 auto + * - release: 稳定版 + * - alpha: 测试版 + * - auto: 根据当前运行的核心版本自动选择升级通道 + * @param force 是否强制升级,默认 false + * - false: 若当前版本为最新版,返回当前为最新版的错误,不再执行升级操作, 否则下载最新版,覆盖升级 + * - true: 直接下载最新版,强制覆盖升级 + */ +export async function upgradeCore( + channel: CoreUpdaterChannel = "auto", + force = false, +): Promise { + await invoke("plugin:mihomo|upgrade_core", { channel, force }); +} + +/** + * 更新 UI + */ +export async function upgradeUi(): Promise { + await invoke("plugin:mihomo|upgrade_ui"); +} + +/** + * 更新 Geo + */ +export async function upgradeGeo(): Promise { + await invoke("plugin:mihomo|upgrade_geo"); +} + +/** + * 清除 Rust 侧中所有的 WebSocket 连接 + */ +export async function clearAllWsConnections(): Promise { + await invoke("plugin:mihomo|clear_all_ws_connections"); +} + +export interface MessageKind { + type: T; + data: D; +} + +export interface CloseFrame { + code: number; + reason: string; +} + +export type Message = + | MessageKind<"Text", string> + | MessageKind<"Binary", number[]> + | MessageKind<"Ping", number[]> + | MessageKind<"Pong", number[]> + | MessageKind<"Close", CloseFrame | null>; + +export class MihomoWebSocket { + id: number; + private readonly listeners: Set<(arg: Message) => void>; + private static instances = new Set(); + + constructor(id: number, listeners: Set<(arg: Message) => void>) { + this.id = id; + this.listeners = listeners; + } + + /** + * 创建一个新的 WebSocket 连接,用于 Mihomo 的流量监控 + * @returns WebSocket 实例 + */ + static async connect_traffic(): Promise { + const listeners: Set<(arg: Message) => void> = new Set(); + const onMessage = new Channel(); + onMessage.onmessage = (message: Message): void => { + listeners.forEach((l) => { + l(message); + }); + }; + const id = await invoke("plugin:mihomo|ws_traffic", { + onMessage, + }); + const instance = new MihomoWebSocket(id, listeners); + MihomoWebSocket.instances.add(instance); + return instance; + } + + /** + * 创建一个新的 WebSocket 连接,用于 Mihomo 的内存监控 + * @returns WebSocket 实例 + */ + static async connect_memory(): Promise { + const listeners: Set<(arg: Message) => void> = new Set(); + const onMessage = new Channel(); + onMessage.onmessage = (message: Message): void => { + listeners.forEach((l) => { + l(message); + }); + }; + const id = await invoke("plugin:mihomo|ws_memory", { + onMessage, + }); + const instance = new MihomoWebSocket(id, listeners); + MihomoWebSocket.instances.add(instance); + return instance; + } + + /** + * 创建一个新的 WebSocket 连接,用于 Mihomo 的连接监控 + * @returns WebSocket 实例 + */ + static async connect_connections(): Promise { + const listeners: Set<(arg: Message) => void> = new Set(); + const onMessage = new Channel(); + onMessage.onmessage = (message: Message): void => { + listeners.forEach((l) => { + l(message); + }); + }; + const id = await invoke("plugin:mihomo|ws_connections", { + onMessage, + }); + const instance = new MihomoWebSocket(id, listeners); + MihomoWebSocket.instances.add(instance); + return instance; + } + + /** + * 创建一个新的 WebSocket 连接,用于 Mihomo 的日志监控 + * @returns WebSocket 实例 + */ + static async connect_logs(level: LogLevel): Promise { + const listeners: Set<(arg: Message) => void> = new Set(); + const onMessage = new Channel(); + onMessage.onmessage = (message: Message): void => { + listeners.forEach((l) => { + l(message); + }); + }; + const id = await invoke("plugin:mihomo|ws_logs", { + level, + onMessage, + }); + const instance = new MihomoWebSocket(id, listeners); + MihomoWebSocket.instances.add(instance); + return instance; + } + + /** + * 添加处理 WebSocket 连接后接受的数据的回调函数 + * @param cb 回调函数 + */ + addListener(cb: (arg: Message) => void): () => void { + this.listeners.add(cb); + return () => { + this.listeners.delete(cb); + }; + } + + // /** + // * 发送消息到 WebSocket 连接 + // * @param message 发送的消息 + // */ + // async send(message: Message | string | number[]): Promise { + // let m: Message; + // if (typeof message === "string") { + // m = { type: "Text", data: message }; + // } else if (typeof message === "object" && "type" in message) { + // m = message; + // } else if (Array.isArray(message)) { + // m = { type: "Binary", data: message }; + // } else { + // throw new Error( + // "invalid `message` type, expected a `{ type: string, data: any }` object, a string or a numeric array", + // ); + // } + // await invoke("plugin:mihomo|ws_send", { id: this.id, message: m }); + // } + + /** + * 关闭 WebSocket 连接 + * @param forceTimeout 强制关闭 WebSocket 连接等待的时间,单位: 毫秒, 默认为 0 + */ + async close(): Promise { + try { + await invoke("plugin:mihomo|ws_disconnect", { + id: this.id, + forceTimeout: 0, + }); + } catch (ignore) { + // ignore + } finally { + MihomoWebSocket.instances.delete(this); + } + } + + /** + * 清理全部的 websocket 连接资源 + */ + static async cleanupAll() { + await Promise.all( + Array.from(MihomoWebSocket.instances).map((instance) => instance.close()), + ); + this.instances.clear(); + await clearAllWsConnections(); + } +} diff --git a/crates/tauri-plugin-mihomo/package.json b/crates/tauri-plugin-mihomo/package.json new file mode 100644 index 000000000..fd889e16e --- /dev/null +++ b/crates/tauri-plugin-mihomo/package.json @@ -0,0 +1,33 @@ +{ + "name": "tauri-plugin-mihomo-api", + "version": "0.1.0", + "author": "oomeow", + "description": "Tauri plugin for Mihomo", + "type": "module", + "types": "./dist-js/index.d.ts", + "main": "./dist-js/index.cjs", + "module": "./dist-js/index.js", + "exports": { + "types": "./dist-js/index.d.ts", + "import": "./dist-js/index.js", + "require": "./dist-js/index.cjs" + }, + "files": [ + "dist-js", + "README.md" + ], + "scripts": { + "build": "rollup -c", + "prepublishOnly": "pnpm build", + "pretest": "pnpm build" + }, + "dependencies": { + "@tauri-apps/api": ">=2.0.0-beta.6" + }, + "devDependencies": { + "@rollup/plugin-typescript": "12.1.4", + "rollup": "4.52.3", + "typescript": "5.9.2", + "tslib": "2.8.1" + } +} diff --git a/crates/tauri-plugin-mihomo/permissions/default.toml b/crates/tauri-plugin-mihomo/permissions/default.toml new file mode 100644 index 000000000..415750821 --- /dev/null +++ b/crates/tauri-plugin-mihomo/permissions/default.toml @@ -0,0 +1,42 @@ +[default] +description = "Default permissions for the plugin" +permissions = [ + "allow-update-controller", + "allow-update-secret", + "allow-get-version", + "allow-flush-fakeip", + "allow-flush-dns", + "allow-get-connections", + "allow-close-all-connections", + "allow-close-connections", + "allow-get-groups", + "allow-get-group-by-name", + "allow-delay-group", + "allow-get-proxy-providers", + "allow-get-proxy-provider-by-name", + "allow-update-proxy-provider", + "allow-healthcheck-proxy-provider", + "allow-healthcheck-node-in-provider", + "allow-get-proxies", + "allow-get-proxy-by-name", + "allow-select-node-for-group", + "allow-unfixed-proxy", + "allow-delay-proxy-by-name", + "allow-get-rules", + "allow-get-rule-providers", + "allow-update-rule-provider", + "allow-get-base-config", + "allow-reload-config", + "allow-patch-base-config", + "allow-update-geo", + "allow-restart", + "allow-upgrade-core", + "allow-upgrade-ui", + "allow-upgrade-geo", + "allow-ws-traffic", + "allow-ws-memory", + "allow-ws-connections", + "allow-ws-logs", + "allow-ws-disconnect", + "allow-clear-all-ws-connections", +] diff --git a/crates/tauri-plugin-mihomo/pnpm-lock.yaml b/crates/tauri-plugin-mihomo/pnpm-lock.yaml new file mode 100644 index 000000000..f3be39b2d --- /dev/null +++ b/crates/tauri-plugin-mihomo/pnpm-lock.yaml @@ -0,0 +1,362 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@tauri-apps/api': + specifier: '>=2.0.0-beta.6' + version: 2.8.0 + devDependencies: + '@rollup/plugin-typescript': + specifier: 12.1.4 + version: 12.1.4(rollup@4.52.3)(tslib@2.8.1)(typescript@5.9.2) + rollup: + specifier: 4.52.3 + version: 4.52.3 + tslib: + specifier: 2.8.1 + version: 2.8.1 + typescript: + specifier: 5.9.2 + version: 5.9.2 + +packages: + + '@rollup/plugin-typescript@12.1.4': + resolution: {integrity: sha512-s5Hx+EtN60LMlDBvl5f04bEiFZmAepk27Q+mr85L/00zPDn1jtzlTV6FWn81MaIwqfWzKxmOJrBWHU6vtQyedQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.14.0||^3.0.0||^4.0.0 + tslib: '*' + typescript: '>=3.7.0' + peerDependenciesMeta: + rollup: + optional: true + tslib: + optional: true + + '@rollup/pluginutils@5.1.4': + resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.52.3': + resolution: {integrity: sha512-h6cqHGZ6VdnwliFG1NXvMPTy/9PS3h8oLh7ImwR+kl+oYnQizgjxsONmmPSb2C66RksfkfIxEVtDSEcJiO0tqw==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.52.3': + resolution: {integrity: sha512-wd+u7SLT/u6knklV/ifG7gr5Qy4GUbH2hMWcDauPFJzmCZUAJ8L2bTkVXC2niOIxp8lk3iH/QX8kSrUxVZrOVw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.52.3': + resolution: {integrity: sha512-lj9ViATR1SsqycwFkJCtYfQTheBdvlWJqzqxwc9f2qrcVrQaF/gCuBRTiTolkRWS6KvNxSk4KHZWG7tDktLgjg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.52.3': + resolution: {integrity: sha512-+Dyo7O1KUmIsbzx1l+4V4tvEVnVQqMOIYtrxK7ncLSknl1xnMHLgn7gddJVrYPNZfEB8CIi3hK8gq8bDhb3h5A==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.52.3': + resolution: {integrity: sha512-u9Xg2FavYbD30g3DSfNhxgNrxhi6xVG4Y6i9Ur1C7xUuGDW3banRbXj+qgnIrwRN4KeJ396jchwy9bCIzbyBEQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.52.3': + resolution: {integrity: sha512-5M8kyi/OX96wtD5qJR89a/3x5x8x5inXBZO04JWhkQb2JWavOWfjgkdvUqibGJeNNaz1/Z1PPza5/tAPXICI6A==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.52.3': + resolution: {integrity: sha512-IoerZJ4l1wRMopEHRKOO16e04iXRDyZFZnNZKrWeNquh5d6bucjezgd+OxG03mOMTnS1x7hilzb3uURPkJ0OfA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.52.3': + resolution: {integrity: sha512-ZYdtqgHTDfvrJHSh3W22TvjWxwOgc3ThK/XjgcNGP2DIwFIPeAPNsQxrJO5XqleSlgDux2VAoWQ5iJrtaC1TbA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.52.3': + resolution: {integrity: sha512-NcViG7A0YtuFDA6xWSgmFb6iPFzHlf5vcqb2p0lGEbT+gjrEEz8nC/EeDHvx6mnGXnGCC1SeVV+8u+smj0CeGQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.52.3': + resolution: {integrity: sha512-d3pY7LWno6SYNXRm6Ebsq0DJGoiLXTb83AIPCXl9fmtIQs/rXoS8SJxxUNtFbJ5MiOvs+7y34np77+9l4nfFMw==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.52.3': + resolution: {integrity: sha512-3y5GA0JkBuirLqmjwAKwB0keDlI6JfGYduMlJD/Rl7fvb4Ni8iKdQs1eiunMZJhwDWdCvrcqXRY++VEBbvk6Eg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.52.3': + resolution: {integrity: sha512-AUUH65a0p3Q0Yfm5oD2KVgzTKgwPyp9DSXc3UA7DtxhEb/WSPfbG4wqXeSN62OG5gSo18em4xv6dbfcUGXcagw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.52.3': + resolution: {integrity: sha512-1makPhFFVBqZE+XFg3Dkq+IkQ7JvmUrwwqaYBL2CE+ZpxPaqkGaiWFEWVGyvTwZace6WLJHwjVh/+CXbKDGPmg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.52.3': + resolution: {integrity: sha512-OOFJa28dxfl8kLOPMUOQBCO6z3X2SAfzIE276fwT52uXDWUS178KWq0pL7d6p1kz7pkzA0yQwtqL0dEPoVcRWg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.52.3': + resolution: {integrity: sha512-jMdsML2VI5l+V7cKfZx3ak+SLlJ8fKvLJ0Eoa4b9/vCUrzXKgoKxvHqvJ/mkWhFiyp88nCkM5S2v6nIwRtPcgg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.52.3': + resolution: {integrity: sha512-tPgGd6bY2M2LJTA1uGq8fkSPK8ZLYjDjY+ZLK9WHncCnfIz29LIXIqUgzCR0hIefzy6Hpbe8Th5WOSwTM8E7LA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.52.3': + resolution: {integrity: sha512-BCFkJjgk+WFzP+tcSMXq77ymAPIxsX9lFJWs+2JzuZTLtksJ2o5hvgTdIcZ5+oKzUDMwI0PfWzRBYAydAHF2Mw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openharmony-arm64@4.52.3': + resolution: {integrity: sha512-KTD/EqjZF3yvRaWUJdD1cW+IQBk4fbQaHYJUmP8N4XoKFZilVL8cobFSTDnjTtxWJQ3JYaMgF4nObY/+nYkumA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.52.3': + resolution: {integrity: sha512-+zteHZdoUYLkyYKObGHieibUFLbttX2r+58l27XZauq0tcWYYuKUwY2wjeCN9oK1Um2YgH2ibd6cnX/wFD7DuA==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.52.3': + resolution: {integrity: sha512-of1iHkTQSo3kr6dTIRX6t81uj/c/b15HXVsPcEElN5sS859qHrOepM5p9G41Hah+CTqSh2r8Bm56dL2z9UQQ7g==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.52.3': + resolution: {integrity: sha512-s0hybmlHb56mWVZQj8ra9048/WZTPLILKxcvcq+8awSZmyiSUZjjem1AhU3Tf4ZKpYhK4mg36HtHDOe8QJS5PQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.52.3': + resolution: {integrity: sha512-zGIbEVVXVtauFgl3MRwGWEN36P5ZGenHRMgNw88X5wEhEBpq0XrMEZwOn07+ICrwM17XO5xfMZqh0OldCH5VTA==} + cpu: [x64] + os: [win32] + + '@tauri-apps/api@2.8.0': + resolution: {integrity: sha512-ga7zdhbS2GXOMTIZRT0mYjKJtR9fivsXzsyq5U3vjDL0s6DTMwYRm0UHNjzTY5dh4+LSC68Sm/7WEiimbQNYlw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + + rollup@4.52.3: + resolution: {integrity: sha512-RIDh866U8agLgiIcdpB+COKnlCreHJLfIhWC3LVflku5YHfpnsIKigRZeFfMfCc4dVcqNVfQQ5gO/afOck064A==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.9.2: + resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} + engines: {node: '>=14.17'} + hasBin: true + +snapshots: + + '@rollup/plugin-typescript@12.1.4(rollup@4.52.3)(tslib@2.8.1)(typescript@5.9.2)': + dependencies: + '@rollup/pluginutils': 5.1.4(rollup@4.52.3) + resolve: 1.22.10 + typescript: 5.9.2 + optionalDependencies: + rollup: 4.52.3 + tslib: 2.8.1 + + '@rollup/pluginutils@5.1.4(rollup@4.52.3)': + dependencies: + '@types/estree': 1.0.8 + estree-walker: 2.0.2 + picomatch: 4.0.2 + optionalDependencies: + rollup: 4.52.3 + + '@rollup/rollup-android-arm-eabi@4.52.3': + optional: true + + '@rollup/rollup-android-arm64@4.52.3': + optional: true + + '@rollup/rollup-darwin-arm64@4.52.3': + optional: true + + '@rollup/rollup-darwin-x64@4.52.3': + optional: true + + '@rollup/rollup-freebsd-arm64@4.52.3': + optional: true + + '@rollup/rollup-freebsd-x64@4.52.3': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.52.3': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.52.3': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.52.3': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.52.3': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.52.3': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.52.3': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.52.3': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.52.3': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.52.3': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.52.3': + optional: true + + '@rollup/rollup-linux-x64-musl@4.52.3': + optional: true + + '@rollup/rollup-openharmony-arm64@4.52.3': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.52.3': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.52.3': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.52.3': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.52.3': + optional: true + + '@tauri-apps/api@2.8.0': {} + + '@types/estree@1.0.8': {} + + estree-walker@2.0.2: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + path-parse@1.0.7: {} + + picomatch@4.0.2: {} + + resolve@1.22.10: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + rollup@4.52.3: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.52.3 + '@rollup/rollup-android-arm64': 4.52.3 + '@rollup/rollup-darwin-arm64': 4.52.3 + '@rollup/rollup-darwin-x64': 4.52.3 + '@rollup/rollup-freebsd-arm64': 4.52.3 + '@rollup/rollup-freebsd-x64': 4.52.3 + '@rollup/rollup-linux-arm-gnueabihf': 4.52.3 + '@rollup/rollup-linux-arm-musleabihf': 4.52.3 + '@rollup/rollup-linux-arm64-gnu': 4.52.3 + '@rollup/rollup-linux-arm64-musl': 4.52.3 + '@rollup/rollup-linux-loong64-gnu': 4.52.3 + '@rollup/rollup-linux-ppc64-gnu': 4.52.3 + '@rollup/rollup-linux-riscv64-gnu': 4.52.3 + '@rollup/rollup-linux-riscv64-musl': 4.52.3 + '@rollup/rollup-linux-s390x-gnu': 4.52.3 + '@rollup/rollup-linux-x64-gnu': 4.52.3 + '@rollup/rollup-linux-x64-musl': 4.52.3 + '@rollup/rollup-openharmony-arm64': 4.52.3 + '@rollup/rollup-win32-arm64-msvc': 4.52.3 + '@rollup/rollup-win32-ia32-msvc': 4.52.3 + '@rollup/rollup-win32-x64-gnu': 4.52.3 + '@rollup/rollup-win32-x64-msvc': 4.52.3 + fsevents: 2.3.3 + + supports-preserve-symlinks-flag@1.0.0: {} + + tslib@2.8.1: {} + + typescript@5.9.2: {} diff --git a/crates/tauri-plugin-mihomo/rollup.config.js b/crates/tauri-plugin-mihomo/rollup.config.js new file mode 100644 index 000000000..810223aff --- /dev/null +++ b/crates/tauri-plugin-mihomo/rollup.config.js @@ -0,0 +1,31 @@ +import { readFileSync } from "fs"; +import { join } from "path"; +import { cwd } from "process"; +import typescript from "@rollup/plugin-typescript"; + +const pkg = JSON.parse(readFileSync(join(cwd(), "package.json"), "utf8")); + +export default { + input: "guest-js/index.ts", + output: [ + { + file: pkg.exports.import, + format: "esm", + }, + { + file: pkg.exports.require, + format: "cjs", + }, + ], + plugins: [ + typescript({ + declaration: true, + declarationDir: `./${pkg.exports.import.replace("./", "").split("/")[0]}`, + }), + ], + external: [ + /^@tauri-apps\/api/, + ...Object.keys(pkg.dependencies || {}), + ...Object.keys(pkg.peerDependencies || {}), + ], +}; diff --git a/crates/tauri-plugin-mihomo/src/commands.rs b/crates/tauri-plugin-mihomo/src/commands.rs new file mode 100644 index 000000000..c16aec157 --- /dev/null +++ b/crates/tauri-plugin-mihomo/src/commands.rs @@ -0,0 +1,311 @@ +use std::collections::HashMap; + +use tauri::{State, async_runtime::RwLock, command, ipc::Channel}; + +use crate::{Result, mihomo::Mihomo, models::*}; + +#[command] +pub(crate) async fn update_controller( + state: State<'_, RwLock>, + host: Option, + port: Option, +) -> Result<()> { + let mut mihomo = state.write().await; + mihomo.update_external_host(host); + mihomo.update_external_port(port); + Ok(()) +} + +#[command] +pub(crate) async fn update_secret(state: State<'_, RwLock>, secret: Option) -> Result<()> { + state.write().await.update_secret(secret); + Ok(()) +} + +#[command] +pub(crate) async fn get_version(state: State<'_, RwLock>) -> Result { + state.read().await.get_version().await +} + +#[command] +pub(crate) async fn flush_fakeip(state: State<'_, RwLock>) -> Result<()> { + state.read().await.flush_fakeip().await +} + +#[command] +pub(crate) async fn flush_dns(state: State<'_, RwLock>) -> Result<()> { + state.read().await.flush_dns().await +} + +// connections +#[command] +pub(crate) async fn get_connections(state: State<'_, RwLock>) -> Result { + state.read().await.get_connections().await +} + +#[command] +pub(crate) async fn close_all_connections(state: State<'_, RwLock>) -> Result<()> { + state.read().await.close_all_connections().await +} + +#[command] +pub(crate) async fn close_connections(state: State<'_, RwLock>, connection_id: String) -> Result<()> { + state.read().await.close_connection(&connection_id).await +} + +// groups +#[command] +pub(crate) async fn get_groups(state: State<'_, RwLock>) -> Result { + state.read().await.get_groups().await +} + +#[command] +pub(crate) async fn get_group_by_name(state: State<'_, RwLock>, group_name: String) -> Result { + state.read().await.get_group_by_name(&group_name).await +} + +#[command] +pub(crate) async fn delay_group( + state: State<'_, RwLock>, + group_name: String, + test_url: String, + timeout: u32, + keep_fixed: bool, +) -> Result> { + let fixed = if keep_fixed { + state.read().await.get_group_by_name(&group_name).await?.fixed + } else { + None + }; + log::debug!("delay group, fixed: {fixed:?}"); + let res = state.read().await.delay_group(&group_name, &test_url, timeout).await?; + if keep_fixed + && let Some(fixed) = fixed + && !fixed.is_empty() + { + state.read().await.select_node_for_group(&group_name, &fixed).await?; + } + Ok(res) +} + +// providers +#[command] +pub(crate) async fn get_proxy_providers(state: State<'_, RwLock>) -> Result { + state.read().await.get_proxy_providers().await +} + +#[command] +pub(crate) async fn get_proxy_provider_by_name( + state: State<'_, RwLock>, + provider_name: String, +) -> Result { + state.read().await.get_proxy_provider_by_name(&provider_name).await +} + +#[command] +pub(crate) async fn update_proxy_provider(state: State<'_, RwLock>, provider_name: String) -> Result<()> { + state.read().await.update_proxy_provider(&provider_name).await +} + +#[command] +pub(crate) async fn healthcheck_proxy_provider(state: State<'_, RwLock>, provider_name: String) -> Result<()> { + state.read().await.healthcheck_proxy_provider(&provider_name).await +} + +#[command] +pub(crate) async fn healthcheck_node_in_provider( + state: State<'_, RwLock>, + provider_name: String, + proxy_name: String, + test_url: String, + timeout: u32, +) -> Result { + state + .read() + .await + .healthcheck_node_in_provider(&provider_name, &proxy_name, &test_url, timeout) + .await +} + +// proxies +#[command] +pub(crate) async fn get_proxies(state: State<'_, RwLock>) -> Result { + state.read().await.get_proxies().await +} + +#[command] +pub(crate) async fn get_proxy_by_name(state: State<'_, RwLock>, proxy_name: String) -> Result { + state.read().await.get_proxy_by_name(&proxy_name).await +} + +#[command] +pub(crate) async fn select_node_for_group( + state: State<'_, RwLock>, + group_name: String, + node: String, +) -> Result<()> { + state.read().await.select_node_for_group(&group_name, &node).await +} + +#[command] +pub(crate) async fn unfixed_proxy(state: State<'_, RwLock>, group_name: String) -> Result<()> { + state.read().await.unfixed_proxy(&group_name).await +} + +#[command] +pub(crate) async fn delay_proxy_by_name( + state: State<'_, RwLock>, + proxy_name: String, + test_url: String, + timeout: u32, +) -> Result { + state + .read() + .await + .delay_proxy_by_name(&proxy_name, &test_url, timeout) + .await +} + +// rules +#[command] +pub(crate) async fn get_rules(state: State<'_, RwLock>) -> Result { + state.read().await.get_rules().await +} + +#[command] +pub(crate) async fn get_rule_providers(state: State<'_, RwLock>) -> Result { + state.read().await.get_rule_providers().await +} + +#[command] +pub(crate) async fn update_rule_provider(state: State<'_, RwLock>, provider_name: String) -> Result<()> { + state.read().await.update_rule_provider(&provider_name).await +} + +// runtime config +#[command] +pub(crate) async fn get_base_config(state: State<'_, RwLock>) -> Result { + state.read().await.get_base_config().await +} + +#[command] +pub(crate) async fn reload_config(state: State<'_, RwLock>, force: bool, config_path: String) -> Result<()> { + state.read().await.reload_config(force, &config_path).await +} + +#[command] +pub(crate) async fn patch_base_config(state: State<'_, RwLock>, data: serde_json::Value) -> Result<()> { + state.read().await.patch_base_config(&data).await +} + +#[command] +pub(crate) async fn update_geo(state: State<'_, RwLock>) -> Result<()> { + state.read().await.update_geo().await +} + +#[command] +pub(crate) async fn restart(state: State<'_, RwLock>) -> Result<()> { + state.read().await.restart().await +} + +// upgrade +#[command] +pub(crate) async fn upgrade_core( + state: State<'_, RwLock>, + channel: CoreUpdaterChannel, + force: bool, +) -> Result<()> { + state.read().await.upgrade_core(channel, force).await +} + +#[command] +pub(crate) async fn upgrade_ui(state: State<'_, RwLock>) -> Result<()> { + state.read().await.upgrade_ui().await +} + +#[command] +pub(crate) async fn upgrade_geo(state: State<'_, RwLock>) -> Result<()> { + state.read().await.upgrade_geo().await +} + +// mihomo websocket +#[command] +pub(crate) async fn ws_traffic( + state: State<'_, RwLock>, + on_message: Channel, +) -> Result { + state + .read() + .await + .ws_traffic(move |data| { + let _ = on_message.send(data); + }) + .await +} + +#[command] +pub(crate) async fn ws_memory( + state: State<'_, RwLock>, + on_message: Channel, +) -> Result { + state + .read() + .await + .ws_memory(move |data| { + let _ = on_message.send(data); + }) + .await +} + +#[command] +pub(crate) async fn ws_connections( + state: State<'_, RwLock>, + on_message: Channel, +) -> Result { + state + .read() + .await + .ws_connections(move |data| { + let _ = on_message.send(data); + }) + .await +} + +#[command] +pub(crate) async fn ws_logs( + state: State<'_, RwLock>, + level: LogLevel, + on_message: Channel, +) -> Result { + state + .read() + .await + .ws_logs(level, move |data| { + let _ = on_message.send(data); + }) + .await +} + +// mihomo 的 websocket 应该只读取数据,没必要发送数据 +// #[command] +// pub(crate) async fn ws_send( +// state: State<'_, RwLock>, +// id: u32, +// message: WebSocketMessage, +// ) -> Result<()> { +// state.read().await.send(id, message).await +// } + +#[command] +pub(crate) async fn ws_disconnect( + state: State<'_, RwLock>, + id: ConnectionId, + force_timeout: Option, +) -> Result<()> { + state.read().await.disconnect(id, force_timeout).await +} + +#[command] +pub(crate) async fn clear_all_ws_connections(state: State<'_, RwLock>) -> Result<()> { + state.write().await.clear_all_ws_connections().await +} diff --git a/crates/tauri-plugin-mihomo/src/error.rs b/crates/tauri-plugin-mihomo/src/error.rs new file mode 100644 index 000000000..e55d78aca --- /dev/null +++ b/crates/tauri-plugin-mihomo/src/error.rs @@ -0,0 +1,76 @@ +use serde::{Serialize, ser::Serializer}; + +use crate::models::ConnectionId; + +pub type Result = std::result::Result; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("I/O error: {0}")] + Io(#[from] std::io::Error), + #[error(transparent)] + Reqwest(#[from] reqwest::Error), + #[error(transparent)] + Json(#[from] serde_json::Error), + #[error("Websocket error: {0}")] + Websocket(String), + #[error("Connection not found for the given id: {0}")] + ConnectionNotFound(ConnectionId), + #[error(transparent)] + InvalidHeaderValue(#[from] tokio_tungstenite::tungstenite::http::header::InvalidHeaderValue), + #[error(transparent)] + InvalidHeaderName(#[from] tokio_tungstenite::tungstenite::http::header::InvalidHeaderName), + #[error("IPC send timeout")] + Timeout(#[from] tokio::time::error::Elapsed), + #[error("The {0} method not supported")] + MethodNotSupported(String), + #[error("Failed Response, {0}")] + FailedResponse(String), + #[error(transparent)] + HttpError(#[from] http::Error), + #[error("Http Parse failed, {0}")] + HttpParseError(String), + #[error("Parse error, {0}")] + ParseError(String), +} + +impl Serialize for Error { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_ref()) + } +} + +impl From for Error { + fn from(e: tokio_tungstenite::tungstenite::Error) -> Self { + crate::Error::Websocket(e.to_string()) + } +} + +impl From for Error { + fn from(e: std::string::FromUtf8Error) -> Self { + crate::Error::ParseError(e.to_string()) + } +} + +impl From for Error { + fn from(e: std::num::ParseIntError) -> Self { + crate::Error::ParseError(e.to_string()) + } +} + +#[macro_export] +macro_rules! failed_resp { + ($($arg: tt)*) => { + $crate::Error::FailedResponse(format!($($arg)*)) + }; +} + +#[macro_export] +macro_rules! ret_failed_resp { + ($($arg: tt)*) => { + return Err($crate::failed_resp!($($arg)*)) + }; +} diff --git a/crates/tauri-plugin-mihomo/src/ipc.rs b/crates/tauri-plugin-mihomo/src/ipc.rs new file mode 100644 index 000000000..b89844c20 --- /dev/null +++ b/crates/tauri-plugin-mihomo/src/ipc.rs @@ -0,0 +1,310 @@ +use std::{ + pin::Pin, + task::{Context, Poll}, +}; + +use http::{ + Version, + header::{CONTENT_LENGTH, CONTENT_TYPE}, +}; +use httparse::EMPTY_HEADER; +use pin_project::pin_project; +use reqwest::RequestBuilder; +use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, BufReader}; +#[cfg(unix)] +use tokio::net::UnixStream; +#[cfg(windows)] +use tokio::net::windows::named_pipe::{ClientOptions, NamedPipeClient}; +#[cfg(windows)] +use windows_sys::Win32::Foundation::ERROR_PIPE_BUSY; + +use crate::{Error, Result}; + +#[pin_project(project = WrapStreamProj)] +pub enum WrapStream { + #[cfg(unix)] + Unix(#[pin] UnixStream), + #[cfg(windows)] + NamedPipe(#[pin] NamedPipeClient), +} + +impl WrapStream { + pub async fn readable(&self) -> std::io::Result<()> { + match self { + #[cfg(unix)] + WrapStream::Unix(s) => s.readable().await, + #[cfg(windows)] + WrapStream::NamedPipe(s) => s.readable().await, + } + } + pub async fn writable(&self) -> std::io::Result<()> { + match self { + #[cfg(unix)] + WrapStream::Unix(s) => s.writable().await, + #[cfg(windows)] + WrapStream::NamedPipe(s) => s.writable().await, + } + } +} + +impl AsyncRead for WrapStream { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut tokio::io::ReadBuf<'_>, + ) -> Poll> { + match self.project() { + #[cfg(unix)] + WrapStreamProj::Unix(s) => s.poll_read(cx, buf), + #[cfg(windows)] + WrapStreamProj::NamedPipe(s) => s.poll_read(cx, buf), + } + } +} + +impl AsyncWrite for WrapStream { + fn poll_write(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll> { + match self.project() { + #[cfg(unix)] + WrapStreamProj::Unix(s) => s.poll_write(cx, buf), + #[cfg(windows)] + WrapStreamProj::NamedPipe(s) => s.poll_write(cx, buf), + } + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match self.project() { + #[cfg(unix)] + WrapStreamProj::Unix(s) => s.poll_flush(cx), + #[cfg(windows)] + WrapStreamProj::NamedPipe(s) => s.poll_flush(cx), + } + } + + fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match self.project() { + #[cfg(unix)] + WrapStreamProj::Unix(s) => s.poll_shutdown(cx), + #[cfg(windows)] + WrapStreamProj::NamedPipe(s) => s.poll_shutdown(cx), + } + } +} + +pub async fn connect_to_socket(socket_path: &str) -> Result { + #[cfg(unix)] + { + if !std::path::Path::new(socket_path).exists() { + log::error!("socket path is not exists: {socket_path}"); + return Err(Error::Io(std::io::Error::new( + std::io::ErrorKind::NotFound, + format!("socket path: {socket_path} not found"), + ))); + } + Ok(WrapStream::Unix(UnixStream::connect(socket_path).await?)) + } + + #[cfg(windows)] + { + let client = loop { + match ClientOptions::new().open(socket_path) { + Ok(client) => break client, + Err(e) if e.raw_os_error() == Some(ERROR_PIPE_BUSY as i32) => (), + Err(e) => { + log::error!("failed to connect to named pipe: {socket_path}, {e}"); + return Err(Error::FailedResponse(format!( + "Failed to connect to named pipe: {socket_path}, {e}" + ))); + } + } + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + }; + Ok(WrapStream::NamedPipe(client)) + } +} + +fn generate_socket_request(req: reqwest::Request) -> Result { + let method = req.method().as_str(); + let mut path = req.url().path().to_string(); + if let Some(query) = req.url().query() { + path.push_str(&format!("?{query}")); + } + let request_line = format!("{method} {path} HTTP/1.1\r\n"); + + // 添加头部信息 + let mut headers = String::new(); + let missing_content_length = + req.headers().contains_key(CONTENT_TYPE) && !req.headers().contains_key(CONTENT_LENGTH); + let body = req + .body() + .and_then(|b| b.as_bytes()) + .map(|b| String::from_utf8_lossy(b).to_string()) + .unwrap_or_default(); + for (name, value) in req.headers() { + if let Ok(value) = value.to_str() { + headers.push_str(&format!("{name}: {value}\r\n")); + if name == CONTENT_TYPE && missing_content_length { + headers.push_str(&format!("{}: {}\r\n", CONTENT_LENGTH, body.len())); + } + } + } + + // 拼接完整请求, 格式: 请求行 + 头部 + 空行 + Body + let raw = format!("{request_line}{headers}\r\n{body}"); + + Ok(raw) +} + +fn generate_socket_response(header: String, body: String) -> Result { + log::debug!("parsing socket response"); + let mut headers = [EMPTY_HEADER; 16]; + let mut res = httparse::Response::new(&mut headers); + let response_str = format!("{header}{body}"); + // println!("response str: {response_str:?}"); + let raw_response = response_str.as_bytes(); + match res.parse(raw_response) { + Ok(httparse::Status::Complete(_)) => { + let mut res_builder = http::Response::builder() + .version(Version::HTTP_11) + .status(res.code.unwrap_or(400)); + for header in res.headers.iter() { + let header_name = header.name; + let header_value = str::from_utf8(header.value).unwrap_or_default(); + res_builder = res_builder.header(header_name, header_value); + } + // { + // use std::io::Write; + // let mut file = std::fs::File::create("body.json")?; + // file.write_all(body.as_bytes())?; + // } + let response = res_builder.body(body.to_string())?; + Ok(reqwest::Response::from(response)) + } + Ok(httparse::Status::Partial) => { + log::error!("Partial response, need more data"); + Err(Error::HttpParseError("Partial response, need more data".to_string())) + } + Err(e) => { + log::error!("Failed to parse response: {e}"); + Err(Error::HttpParseError(format!("Failed to parse response: {e}"))) + } + } +} + +async fn read_header(reader: &mut BufReader) -> Result { + let mut header = String::new(); + loop { + let mut line = String::new(); + if let Ok(size) = reader.read_line(&mut line).await + && size == 0 + { + return Err(Error::HttpParseError("no response".to_string())); + } + header.push_str(&line); + if line == "\r\n" { + break; + } + } + log::debug!("read header done: {header:?}"); + + Ok(header) +} + +async fn read_chunked_data(reader: &mut BufReader) -> Result { + let mut body = Vec::new(); + loop { + // 读 chunk size + let mut size_line = String::new(); + reader.read_line(&mut size_line).await?; + let size_line = size_line.trim(); + if size_line.is_empty() { + continue; + } + let chunk_size = usize::from_str_radix(size_line, 16) + .map_err(|e| Error::HttpParseError(format!("Failed to parse chunk size: {e}")))?; + + if chunk_size == 0 { + // 读掉最后的 CRLF + let mut end = String::new(); + reader.read_line(&mut end).await?; + break; + } + + // 读 chunk data + let mut chunk_data = vec![0u8; chunk_size]; + reader.read_exact(&mut chunk_data).await?; + body.extend_from_slice(&chunk_data); + + // 读掉结尾 CRLF + let mut crlf = String::new(); + reader.read_line(&mut crlf).await?; + } + log::debug!("read chunked data done"); + Ok(String::from_utf8(body)?) +} + +pub trait LocalSocket { + async fn send_by_local_socket(self, socket_path: &str) -> Result; +} + +impl LocalSocket for RequestBuilder { + async fn send_by_local_socket(self, socket_path: &str) -> Result { + let request = self.build()?; + let timeout = request.timeout().cloned(); + + let process = async move { + let mut stream = connect_to_socket(socket_path).await?; + log::debug!("building socket request"); + let req_str = generate_socket_request(request)?; + log::debug!("request string: {req_str:?}"); + stream.writable().await?; + log::debug!("send request"); + stream.write_all(req_str.as_bytes()).await?; + log::debug!("wait for response"); + stream.readable().await?; + + let mut reader = BufReader::new(stream); + + // 读取解析 header + let header = read_header(&mut reader).await?; + // 解析 Content-Length, 判断是否是 chunked 响应 + let mut content_length: Option = None; + let mut is_chunked = false; + for line in header.lines() { + if let Some(v) = line.to_lowercase().strip_prefix("content-length: ") { + content_length = Some(v.trim().parse()?); + } + if line.to_lowercase().contains("transfer-encoding: chunked") { + is_chunked = true; + } + } + + // 读取 body + let body = if is_chunked { + read_chunked_data(&mut reader).await? + } else if let Some(content_length) = content_length { + log::debug!("content length: {content_length}"); + let mut body_buf = vec![0u8; content_length]; + reader.read_exact(&mut body_buf).await?; + String::from_utf8_lossy(&body_buf).to_string() + } else { + // 使用空的 body + String::new() + }; + log::debug!("receive response success, shut down stream"); + reader.shutdown().await?; + generate_socket_response(header, body) + }; + + match timeout { + Some(duration) => { + log::debug!("Timeout duration: {:?}", duration); + tokio::time::timeout(duration, process).await? + } + None => { + log::debug!("No timeout specified"); + process.await + } + } + } +} diff --git a/crates/tauri-plugin-mihomo/src/lib.rs b/crates/tauri-plugin-mihomo/src/lib.rs new file mode 100644 index 000000000..fe9ebbfe5 --- /dev/null +++ b/crates/tauri-plugin-mihomo/src/lib.rs @@ -0,0 +1,151 @@ +pub use mihomo::Mihomo; +use tauri::{ + Manager, Runtime, + async_runtime::RwLock, + plugin::{Builder as PluginBuilder, TauriPlugin}, +}; + +mod commands; +mod error; +mod ipc; +mod mihomo; +pub mod models; +mod utils; + +pub use error::{Error, Result}; + +use crate::models::Protocol; + +/// Extensions to [`tauri::App`], [`tauri::AppHandle`] and [`tauri::Window`] to access the mihomo APIs. +pub trait MihomoExt { + fn mihomo(&self) -> &RwLock; +} + +impl> crate::MihomoExt for T { + fn mihomo(&self) -> &RwLock { + self.state::>().inner() + } +} + +#[derive(Debug)] +pub struct Builder { + protocol: Protocol, + external_host: Option, + external_port: Option, + secret: Option, + socket_path: Option, +} + +impl Default for Builder { + fn default() -> Self { + Self { + protocol: Protocol::Http, + external_host: Some(String::from("127.0.0.1")), + external_port: Some(9090), + secret: None, + socket_path: None, + } + } +} + +impl Builder { + pub fn new() -> Self { + Self::default() + } + + pub fn protocol(mut self, protocol: Protocol) -> Self { + self.protocol = protocol; + self + } + + pub fn external_host>(mut self, external_host: S) -> Self { + self.external_host = Some(external_host.into()); + self + } + + pub fn external_port(mut self, external_port: u32) -> Self { + self.external_port = Some(external_port); + self + } + + pub fn secret>(mut self, secret: S) -> Self { + self.secret = Some(secret.into()); + self + } + + pub fn socket_path>(mut self, socket_path: S) -> Self { + self.socket_path = Some(socket_path.into()); + self + } + + pub fn build(self) -> TauriPlugin { + let protocol = self.protocol; + let external_host = self.external_host; + let external_port = self.external_port; + let secret = self.secret; + let socket_path = self.socket_path; + + PluginBuilder::new("mihomo") + .invoke_handler(tauri::generate_handler![ + commands::update_controller, + commands::update_secret, + commands::get_version, + commands::flush_fakeip, + commands::flush_dns, + // connections + commands::get_connections, + commands::close_all_connections, + commands::close_connections, + // groups + commands::get_groups, + commands::get_group_by_name, + commands::delay_group, + // providers + commands::get_proxy_providers, + commands::get_proxy_provider_by_name, + commands::update_proxy_provider, + commands::healthcheck_proxy_provider, + commands::healthcheck_node_in_provider, + // proxies + commands::get_proxies, + commands::get_proxy_by_name, + commands::select_node_for_group, + commands::unfixed_proxy, + commands::delay_proxy_by_name, + // rules + commands::get_rules, + commands::get_rule_providers, + commands::update_rule_provider, + // runtime config + commands::get_base_config, + commands::reload_config, + commands::patch_base_config, + commands::update_geo, + commands::restart, + // upgrade + commands::upgrade_core, + commands::upgrade_ui, + commands::upgrade_geo, + // ws + commands::ws_traffic, + commands::ws_memory, + commands::ws_connections, + commands::ws_logs, + commands::ws_disconnect, + commands::clear_all_ws_connections, + // commands::ws_send, + ]) + .setup(move |app, _api| { + app.manage(RwLock::new(Mihomo { + protocol, + external_host, + external_port, + secret, + socket_path, + connection_manager: Default::default(), + })); + Ok(()) + }) + .build() + } +} diff --git a/crates/tauri-plugin-mihomo/src/mihomo.rs b/crates/tauri-plugin-mihomo/src/mihomo.rs new file mode 100644 index 000000000..255123754 --- /dev/null +++ b/crates/tauri-plugin-mihomo/src/mihomo.rs @@ -0,0 +1,746 @@ +#![allow(dead_code)] +use std::{collections::HashMap, sync::Arc, time::Duration}; + +use futures_util::StreamExt; +use http::{ + HeaderMap, HeaderValue, Request, + header::{AUTHORIZATION, CONNECTION, CONTENT_TYPE, HOST, SEC_WEBSOCKET_KEY, SEC_WEBSOCKET_VERSION, UPGRADE}, +}; +use reqwest::{Method, RequestBuilder}; +use serde_json::json; +use tokio_tungstenite::{ + client_async, connect_async, + tungstenite::{Message, client::IntoClientRequest, protocol::CloseFrame as ProtocolCloseFrame}, +}; + +use crate::{ + Error, Result, failed_resp, + ipc::LocalSocket, + models::{ + BaseConfig, CloseFrame, ConnectionId, ConnectionManager, Connections, CoreUpdaterChannel, ErrorResponse, + Groups, LogLevel, MihomoVersion, Protocol, Proxies, Proxy, ProxyDelay, ProxyProvider, ProxyProviders, + RuleProviders, Rules, WebSocketMessage, WebSocketWriter, + }, + ret_failed_resp, utils, +}; + +const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60); + +pub struct Mihomo { + pub protocol: Protocol, + pub external_host: Option, + pub external_port: Option, + pub secret: Option, + pub socket_path: Option, + pub connection_manager: Arc, +} + +impl Mihomo { + pub fn update_protocol(&mut self, protocol: Protocol) { + self.protocol = protocol; + } + + pub fn update_external_host(&mut self, host: Option) { + self.external_host = host; + } + + pub fn update_external_port(&mut self, port: Option) { + self.external_port = port; + } + + pub fn update_secret(&mut self, secret: Option) { + self.secret = secret; + } + + pub fn update_socket_path>(&mut self, socket_path: S) { + self.socket_path = Some(socket_path.into()); + } + + fn get_req_url(&self, suffix_url: &str) -> Result { + let suffix_url = suffix_url.trim_start_matches("/"); + match self.protocol { + Protocol::Http => { + if let Some(host) = self.external_host.as_ref() { + let port = self.external_port.unwrap_or(9090); + Ok(format!("http://{host}:{port}/{suffix_url}")) + } else { + log::error!("missing external host parameter"); + Err(Error::Io(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "missing external host".to_string(), + ))) + } + } + Protocol::LocalSocket => Ok(format!("http://localhost/{suffix_url}")), + } + } + + fn get_req_headers(&self) -> Result> { + let mut headers = HeaderMap::new(); + headers.insert(HOST, HeaderValue::from_str("localhost")?); + headers.insert(CONTENT_TYPE, HeaderValue::from_str("application/json")?); + if matches!(self.protocol, Protocol::Http) + && let Some(secret) = &self.secret + { + let auth_value = HeaderValue::from_str(&format!("Bearer {secret}"))?; + headers.insert(AUTHORIZATION, auth_value); + } + Ok(headers) + } + + fn build_request(&self, method: Method, suffix_url: &str) -> Result { + let url = self.get_req_url(suffix_url)?; + let headers = self.get_req_headers()?; + let client = reqwest::ClientBuilder::new().build()?; + let req = match method { + Method::POST => Ok(client.post(url).headers(headers)), + Method::GET => Ok(client.get(url).headers(headers)), + Method::PUT => Ok(client.put(url).headers(headers)), + Method::PATCH => Ok(client.patch(url).headers(headers)), + Method::DELETE => Ok(client.delete(url).headers(headers)), + _ => { + let method_str = method.as_str().to_string(); + log::error!("method not supported: {method_str}"); + Err(Error::MethodNotSupported(method_str)) + } + }; + // 在此设置 timeout,以供构建 local socket 连接时,获取到 timeout 属性 + Ok(req?.timeout(DEFAULT_TIMEOUT)) + } + + async fn send_by_protocol(&self, client: RequestBuilder) -> Result { + match self.protocol { + Protocol::Http => client.send().await.map_err(Error::Reqwest), + Protocol::LocalSocket => { + if let Some(socket_path) = self.socket_path.as_ref() { + log::debug!("send to local socket: {socket_path}"); + client.send_by_local_socket(socket_path).await + } else { + log::error!("missing socket path parameter"); + Err(Error::Io(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "missing socket path".to_string(), + ))) + } + } + } + } + + fn get_websocket_url(&self, suffix_url: &str) -> Result { + let suffix_url = suffix_url.trim_start_matches("/"); + match self.protocol { + Protocol::Http => { + if let Some(host) = self.external_host.as_ref() { + let port = self.external_port.unwrap_or(9090); + let secret = self.secret.as_deref().unwrap_or_default(); + Ok(format!("ws://{host}:{port}/{suffix_url}?token={secret}")) + } else { + log::error!("missing external host parameter"); + Err(Error::Io(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "missing external host".to_string(), + ))) + } + } + Protocol::LocalSocket => Ok(format!("ws://localhost/{suffix_url}")), + } + } + + /// 连接 WebSocket + async fn connect(&self, url: String, on_message: F) -> Result + where + F: Fn(serde_json::Value) + Send + 'static, + { + let id = rand::random(); + log::info!("connecting to websocket: {url}, id: {id}"); + let manager = self.connection_manager.clone(); + let handle_message = |message| { + // log::trace!("handle message {message:?}"); + match message { + Ok(Message::Text(t)) => serde_json::to_value(WebSocketMessage::Text(t.to_string())).unwrap(), + Ok(Message::Binary(t)) => serde_json::to_value(WebSocketMessage::Binary(t.to_vec())).unwrap(), + Ok(Message::Ping(t)) => serde_json::to_value(WebSocketMessage::Ping(t.to_vec())).unwrap(), + Ok(Message::Pong(t)) => serde_json::to_value(WebSocketMessage::Pong(t.to_vec())).unwrap(), + Ok(Message::Close(t)) => serde_json::to_value(WebSocketMessage::Close(t.map(|v| CloseFrame { + code: v.code.into(), + reason: v.reason.to_string(), + }))) + .unwrap(), + Ok(Message::Frame(_)) => serde_json::Value::Null, // This value can't be received. + Err(e) => { + log::error!("websocket error: {e}"); + serde_json::to_value(WebSocketMessage::Text(Error::from(e).to_string())).unwrap() + } + } + }; + + match self.protocol { + Protocol::Http => { + log::debug!("starting connect to websocket by using http"); + let request = url.into_client_request()?; + let (ws_stream, _) = connect_async(request).await?; + let (writer, mut reader) = ws_stream.split(); + + manager + .0 + .write() + .await + .insert(id, WebSocketWriter::TcpStreamWriter(writer)); + + tokio::spawn(async move { + let manager_ = manager.clone(); + loop { + let ids: Vec = manager_.0.read().await.keys().cloned().collect(); + log::trace!("waiting for websocket message, connection_id: {id}, manager_ids: {ids:?}",); + if !ids.contains(&id) { + log::debug!("connection [{id}] is removed from manager"); + break; + } + if let Some(message) = reader.next().await { + if let Ok(Message::Close(_)) = message { + log::debug!("connection [{id}] is closed"); + manager_.0.write().await.remove(&id); + } + let response = handle_message(message); + on_message(response); + } + } + }); + + Ok(id) + } + Protocol::LocalSocket => { + if let Some(socket_path) = self.socket_path.as_ref() { + log::debug!("starting connect to websocket by using local socket: {socket_path}"); + let stream = crate::ipc::connect_to_socket(socket_path).await?; + + let request = Request::builder() + .uri(url) + .header(HOST, "clash-verge") + .header(SEC_WEBSOCKET_KEY, utils::generate_websocket_key()) + .header(CONNECTION, "Upgrade") + .header(UPGRADE, "websocket") + .header(SEC_WEBSOCKET_VERSION, "13") + .body(())?; + let (ws_stream, _) = client_async(request, stream).await?; + let (writer, mut reader) = ws_stream.split(); + + manager + .0 + .write() + .await + .insert(id, WebSocketWriter::SocketStreamWriter(writer)); + + tokio::spawn(async move { + let manager_ = manager.clone(); + loop { + let ids: Vec = manager_.0.read().await.keys().cloned().collect(); + log::trace!("waiting for websocket message, connection_id: {id}, manager_ids: {ids:?}",); + if !ids.contains(&id) { + log::debug!("connection [{id}] is removed from manager"); + break; + } + if let Some(message) = reader.next().await { + if let Ok(Message::Close(_)) = message { + log::debug!("connection [{id}] closed"); + manager_.0.write().await.remove(&id); + } + let response = handle_message(message); + on_message(response); + } + } + }); + Ok(id) + } else { + log::error!("missing socket path parameter"); + Err(Error::Io(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "missing socket path".to_string(), + ))) + } + } + } + } + + /// 向指定 WebSocket 连接发送消息 (暂无使用该方法的地方) + async fn send(&self, id: ConnectionId, message: WebSocketMessage) -> Result<()> { + let manager = self.connection_manager.clone(); + let mut manager = manager.0.write().await; + if let Some(writer) = manager.get_mut(&id) { + let data = match message { + WebSocketMessage::Text(t) => Message::Text(t.into()), + WebSocketMessage::Binary(t) => Message::Binary(t.into()), + WebSocketMessage::Ping(t) => Message::Ping(t.into()), + WebSocketMessage::Pong(t) => Message::Pong(t.into()), + WebSocketMessage::Close(t) => Message::Close(t.map(|v| ProtocolCloseFrame { + code: v.code.into(), + reason: v.reason.into(), + })), + }; + writer.send(data).await?; + Ok(()) + } else { + log::error!("connection not found: {id}"); + Err(Error::ConnectionNotFound(id)) + } + } + + /// 取消 WebSocket 连接 + pub async fn disconnect(&self, id: ConnectionId, force_timeout: Option) -> Result<()> { + log::debug!("disconnecting connection: {id}"); + let mut manager = self.connection_manager.0.write().await; + if let Some(writer) = manager.get_mut(&id) { + let close_message = Message::Close(Some(ProtocolCloseFrame { + code: 1000.into(), + reason: "Disconnected by client".into(), + })); + writer.send(close_message).await?; + if let Some(timeout) = force_timeout { + let manager_ = self.connection_manager.clone(); + tokio::spawn(async move { + tokio::time::sleep(Duration::from_millis(timeout)).await; + log::debug!("force close websocket connection"); + manager_.0.write().await.remove(&id); + }); + } + Ok(()) + } else { + log::error!("connection not found: {id}"); + Err(Error::ConnectionNotFound(id)) + } + } + + pub async fn clear_all_ws_connections(&self) -> Result<()> { + log::debug!("start to clear all websocket connections"); + let mut manager = self.connection_manager.0.write().await; + log::debug!("manage_ids: {:?}", manager.keys()); + manager.clear(); + log::debug!("clear all done, manager_ids: {:?}", manager.keys()); + Ok(()) + } + + // ------------------------------------------------------ + // | Mihomo API | + // ------------------------------------------------------ + /// WebSocket: Mihomo 流量数据 + pub async fn ws_traffic(&self, on_message: F) -> Result + where + F: Fn(serde_json::Value) + Send + 'static, + { + let ws_url = self.get_websocket_url("/traffic")?; + let websocket_id = self.connect(ws_url, on_message).await?; + Ok(websocket_id) + } + + /// WebSocket: Mihomo 内存使用数据 + pub async fn ws_memory(&self, on_message: F) -> Result + where + F: Fn(serde_json::Value) + Send + 'static, + { + let ws_url = self.get_websocket_url("/memory")?; + let websocket_id = self.connect(ws_url, on_message).await?; + Ok(websocket_id) + } + + /// WebSocket: Mihomo 连接信息数据 + pub async fn ws_connections(&self, on_message: F) -> Result + where + F: Fn(serde_json::Value) + Send + 'static, + { + let ws_url = self.get_websocket_url("/connections")?; + let websocket_id = self.connect(ws_url, on_message).await?; + Ok(websocket_id) + } + + /// WebSocket: Mihomo 日志数据 + pub async fn ws_logs(&self, level: LogLevel, on_message: F) -> Result + where + F: Fn(serde_json::Value) + Send + 'static, + { + let ws_url = self.get_websocket_url("/logs")?; + let ws_url = match self.protocol { + // url 后面添加 format=structured 参数的日志格式如下: + // {"time":"11:49:58","level":"debug","message":"[DNS] hijack udp:192.168.2.1:53 from 198.18.0.1:42761","fields":[]} + Protocol::Http => format!("{ws_url}&level={level}"), + Protocol::LocalSocket => format!("{ws_url}?level={level}"), + }; + let websocket_id = self.connect(ws_url, on_message).await?; + Ok(websocket_id) + } + + // clash api + /// 获取 Mihomo 版本信息 + pub async fn get_version(&self) -> Result { + let client = self.build_request(Method::GET, "/version")?; + let response = self.send_by_protocol(client).await?; + if !response.status().is_success() { + ret_failed_resp!("get mihomo version error, {}", response.text().await?); + } + Ok(response.json::().await?) + } + + /// 清理 FakeIP 缓存 + pub async fn flush_fakeip(&self) -> Result<()> { + let client = self.build_request(Method::POST, "/cache/fakeip/flush")?; + let response = self.send_by_protocol(client).await?; + if !response.status().is_success() { + ret_failed_resp!("flush fakeip cache error, {}", response.text().await?); + } + Ok(()) + } + + /// 清理 DNS 缓存 + pub async fn flush_dns(&self) -> Result<()> { + let client = self.build_request(Method::POST, "/cache/dns/flush")?; + let response = self.send_by_protocol(client).await?; + if !response.status().is_success() { + ret_failed_resp!("flush dns cache error, {}", response.text().await?); + } + Ok(()) + } + + /// 获取全部连接信息 + pub async fn get_connections(&self) -> Result { + let client = self.build_request(Method::GET, "/connections")?; + let response = self.send_by_protocol(client).await?; + if !response.status().is_success() { + ret_failed_resp!("get connections failed, {}", response.text().await?); + } + Ok(response.json::().await?) + } + + /// 关闭全部连接 + pub async fn close_all_connections(&self) -> Result<()> { + let client = self.build_request(Method::DELETE, "/connections")?; + let response = self.send_by_protocol(client).await?; + if !response.status().is_success() { + ret_failed_resp!("close all connections failed, {}", response.text().await?); + } + Ok(()) + } + + /// 关闭指定 ID 的连接 + pub async fn close_connection(&self, connection_id: &str) -> Result<()> { + let client = self.build_request(Method::DELETE, &format!("/connections/{connection_id}"))?; + let response = self.send_by_protocol(client).await?; + if !response.status().is_success() { + ret_failed_resp!("close connection failed, {}", response.text().await?); + } + Ok(()) + } + + /// 获取所有的代理组 + pub async fn get_groups(&self) -> Result { + let client = self.build_request(Method::GET, "/group")?; + let response = self.send_by_protocol(client).await?; + if !response.status().is_success() { + ret_failed_resp!("get group error, {}", response.text().await?); + } + Ok(response.json::().await?) + } + + /// 获取指定名称的代理组 + pub async fn get_group_by_name(&self, group_name: &str) -> Result { + let group_name = urlencoding::encode(group_name); + let client = self.build_request(Method::GET, &format!("/group/{group_name}"))?; + let response = self.send_by_protocol(client).await?; + if !response.status().is_success() { + ret_failed_resp!("get group error, {}", response.text().await?); + } + Ok(response.json::().await?) + } + + /// 对指定代理组进行延迟测试, 同时清理代理组已固定的节点 + pub async fn delay_group(&self, group_name: &str, test_url: &str, timeout: u32) -> Result> { + let group_name = urlencoding::encode(group_name); + let test_url = urlencoding::encode(test_url); + let suffix_url = format!("/group/{group_name}/delay?url={test_url}&timeout={timeout}"); + let client = self.build_request(Method::GET, &suffix_url)?; + let response = self.send_by_protocol(client).await?; + if !response.status().is_success() { + ret_failed_resp!("get group error, {}", response.text().await?); + } + Ok(response.json::>().await?) + } + + /// 获取代理提供者信息 + pub async fn get_proxy_providers(&self) -> Result { + let client = self.build_request(Method::GET, "/providers/proxies")?; + let response = self.send_by_protocol(client).await?; + if !response.status().is_success() { + ret_failed_resp!("get providers proxy failed, {}", response.text().await?); + } + Ok(response.json::().await?) + } + + /// 获取指定代理提供者信息 + pub async fn get_proxy_provider_by_name(&self, provider_name: &str) -> Result { + let provider_name = urlencoding::encode(provider_name); + let client = self.build_request(Method::GET, &format!("/providers/proxies/{provider_name}"))?; + let response = self.send_by_protocol(client).await?; + if !response.status().is_success() { + ret_failed_resp!("get providers proxy failed, {}", response.text().await?); + } + Ok(response.json::().await?) + } + + /// 更新指定代理提供者信息 + pub async fn update_proxy_provider(&self, provider_name: &str) -> Result<()> { + let provider_name = urlencoding::encode(provider_name); + let client = self.build_request(Method::PUT, &format!("/providers/proxies/{provider_name}"))?; + let response = self.send_by_protocol(client).await?; + if !response.status().is_success() { + ret_failed_resp!("update providers proxy failed, {}", response.text().await?); + } + Ok(()) + } + + /// 对指定代理提供者进行健康检查 + pub async fn healthcheck_proxy_provider(&self, provider_name: &str) -> Result<()> { + let provider_name = urlencoding::encode(provider_name); + let suffix_url = format!("/providers/proxies/{provider_name}/healthcheck"); + let client = self.build_request(Method::GET, &suffix_url)?; + let response = self.send_by_protocol(client).await?; + if !response.status().is_success() { + ret_failed_resp!("healthcheck providers failed, {}", response.text().await?); + } + Ok(()) + } + + /// 对指定代理提供者下的指定节点(非代理组)进行健康检查, 并返回新的延迟信息 + pub async fn healthcheck_node_in_provider( + &self, + provider_name: &str, + proxy_name: &str, + test_url: &str, + timeout: u32, + ) -> Result { + let provider_name = urlencoding::encode(provider_name); + let proxy_name = urlencoding::encode(proxy_name); + let suffix_url = format!("/providers/proxies/{provider_name}/{proxy_name}/healthcheck",); + let client = self + .build_request(Method::GET, &suffix_url)? + .query(&[("url", test_url), ("timeout", &timeout.to_string())]); + let response = self.send_by_protocol(client).await?; + if !response.status().is_success() { + // maybe proxy delay is timeout response, try parse it. + match response.json::().await { + Ok(err_response) => { + log::debug!("delay error: {}", err_response.message); + return Ok(ProxyDelay { delay: 0 }); + } + Err(e) => { + ret_failed_resp!("healthcheck providers failed, {}", e); + } + } + } + Ok(response.json::().await?) + } + + /// 获取所有代理信息 + pub async fn get_proxies(&self) -> Result { + let client = self.build_request(Method::GET, "/proxies")?; + let response = self.send_by_protocol(client).await?; + if !response.status().is_success() { + ret_failed_resp!("get proxies failed, {}", response.text().await?); + } + Ok(response.json::().await?) + } + + /// 获取指定代理信息 + pub async fn get_proxy_by_name(&self, proxy_name: &str) -> Result { + let proxy_name = urlencoding::encode(proxy_name); + let client = self.build_request(Method::GET, &format!("/proxies/{proxy_name}"))?; + let response = self.send_by_protocol(client).await?; + if !response.status().is_success() { + ret_failed_resp!("get proxy by name failed, {}", response.text().await?); + } + Ok(response.json::().await?) + } + + /// 为指定代理选择节点 + /// + /// 一般为指定代理组下使用指定的代理节点 【代理组/节点】 + pub async fn select_node_for_group(&self, group_name: &str, node: &str) -> Result<()> { + let group_name = urlencoding::encode(group_name); + let body = json!({ "name": node }); + let client = self + .build_request(Method::PUT, &format!("/proxies/{group_name}"))? + .json(&body); + let response = self.send_by_protocol(client).await?; + if !response.status().is_success() { + ret_failed_resp!("select node for proxy failed, {}", response.text().await?); + } + Ok(()) + } + + /// 指定代理组下不再使用固定的代理节点 + /// + /// 一般用于自动选择的代理组(例如:URLTest 类型的代理组)下的节点 + pub async fn unfixed_proxy(&self, group_name: &str) -> Result<()> { + let group_name = urlencoding::encode(group_name); + let client = self.build_request(Method::DELETE, &format!("/proxies/{group_name}"))?; + let response = self.send_by_protocol(client).await?; + if !response.status().is_success() { + ret_failed_resp!("unfixed proxy failed, {}", response.text().await?); + } + Ok(()) + } + + /// 对指定代理进行延迟测试 + /// + /// 一般用于代理节点的延迟测试,也可传代理组名称(只会测试代理组下选中的代理节点) + pub async fn delay_proxy_by_name(&self, proxy_name: &str, test_url: &str, timeout: u32) -> Result { + let proxy_name = urlencoding::encode(proxy_name); + let suffix_url = format!("/proxies/{proxy_name}/delay"); + let client = self + .build_request(Method::GET, &suffix_url)? + .query(&[("timeout", &timeout.to_string()), ("url", &test_url.to_string())]); + let response = self.send_by_protocol(client).await?; + if !response.status().is_success() { + match response.json::().await { + Ok(err_response) => { + log::debug!("delay error: {}", err_response.message); + return Ok(ProxyDelay { delay: 0 }); + } + Err(e) => { + ret_failed_resp!("get proxy by name failed, {}", e); + } + } + } + Ok(response.json::().await?) + } + + /// 获取所有规则信息 + pub async fn get_rules(&self) -> Result { + let client = self.build_request(Method::GET, "/rules")?; + let response = self.send_by_protocol(client).await?; + if !response.status().is_success() { + ret_failed_resp!("get rules failed, {}", response.text().await?); + } + Ok(response.json::().await?) + } + + /// 获取所有规则提供者信息 + pub async fn get_rule_providers(&self) -> Result { + let client = self.build_request(Method::GET, "/providers/rules")?; + let response = self.send_by_protocol(client).await?; + if !response.status().is_success() { + ret_failed_resp!("get rules providers failed, {}", response.text().await?); + } + Ok(response.json::().await?) + } + + /// 更新规则提供者信息 + pub async fn update_rule_provider(&self, provider_name: &str) -> Result<()> { + let provider_name = urlencoding::encode(provider_name); + let client = self.build_request(Method::PUT, &format!("/providers/rules/{provider_name}"))?; + let response = self.send_by_protocol(client).await?; + if !response.status().is_success() { + ret_failed_resp!("update rule provider failed, {}", response.text().await?); + } + Ok(()) + } + + /// 获取基础配置 + pub async fn get_base_config(&self) -> Result { + let client = self.build_request(Method::GET, "/configs")?; + let response = self.send_by_protocol(client).await?; + if !response.status().is_success() { + ret_failed_resp!("get base config error, {}", response.text().await?); + } + Ok(response.json::().await?) + } + + /// 重新加载配置 + pub async fn reload_config(&self, force: bool, config_path: &str) -> Result<()> { + let body = json!({ "path": config_path }); + let client = self + .build_request(Method::PUT, "/configs")? + .query(&[("force", force)]) + .json(&body); + let response = self.send_by_protocol(client).await?; + if !response.status().is_success() { + ret_failed_resp!("reload base config error, {}", response.text().await?); + } + Ok(()) + } + + /// 更新基础配置 + pub async fn patch_base_config(&self, data: &D) -> Result<()> { + let client = self.build_request(Method::PATCH, "/configs")?.json(&data); + let response = self.send_by_protocol(client).await?; + if !response.status().is_success() { + ret_failed_resp!("patch base config error, {}", response.text().await?); + } + Ok(()) + } + + /// 更新 Geo, 同 [`upgrade_geo`](crate::mihomo::Mihomo::upgrade_geo) + pub async fn update_geo(&self) -> Result<()> { + let client = self.build_request(Method::POST, "/configs/geo")?; + let response = self.send_by_protocol(client).await?; + if !response.status().is_success() { + failed_resp!("update geo database error, {}", response.text().await?); + } + Ok(()) + } + + /// 重启核心 + pub async fn restart(&self) -> Result<()> { + let client = self.build_request(Method::POST, "/restart")?; + let response = self.send_by_protocol(client).await?; + if !response.status().is_success() { + failed_resp!("restart core failed, {}", response.text().await?); + } + Ok(()) + } + + /// 升级核心 + pub async fn upgrade_core(&self, channel: CoreUpdaterChannel, force: bool) -> Result<()> { + let client = self + .build_request(Method::POST, "/upgrade")? + .query(&[("channel", &channel.to_string()), ("force", &force.to_string())]); + let response = self.send_by_protocol(client).await?; + if !response.status().is_success() { + match response.json::>().await { + Ok(res) => match res.get("message") { + Some(msg) => { + if msg.to_lowercase().contains("already using latest version") { + ret_failed_resp!("already using latest version"); + } else { + ret_failed_resp!("{}", msg.clone()); + } + } + None => { + ret_failed_resp!("upgrade core failed"); + } + }, + Err(e) => { + ret_failed_resp!("upgrade core failed, {}", e); + } + } + } + Ok(()) + } + + /// 更新 UI + pub async fn upgrade_ui(&self) -> Result<()> { + let client = self.build_request(Method::POST, "/upgrade/ui")?; + let response = self.send_by_protocol(client).await?; + if !response.status().is_success() { + ret_failed_resp!("upgrade ui failed, {}", response.text().await?); + } + Ok(()) + } + + /// 更新 Geo + pub async fn upgrade_geo(&self) -> Result<()> { + let client = self.build_request(Method::POST, "/upgrade/geo")?; + let response = self.send_by_protocol(client).await?; + if !response.status().is_success() { + ret_failed_resp!("upgrade geo failed, {}", response.text().await?); + } + Ok(()) + } +} diff --git a/crates/tauri-plugin-mihomo/src/models.rs b/crates/tauri-plugin-mihomo/src/models.rs new file mode 100644 index 000000000..b57eb92eb --- /dev/null +++ b/crates/tauri-plugin-mihomo/src/models.rs @@ -0,0 +1,898 @@ +use std::{collections::HashMap, fmt::Display}; + +use futures_util::{SinkExt, stream::SplitSink}; +use serde::{Deserialize, Serialize}; +use tokio::{net::TcpStream, sync::RwLock}; +use tokio_tungstenite::{MaybeTlsStream, WebSocketStream, tungstenite::Message}; +use ts_rs::TS; + +use crate::ipc::WrapStream; + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Protocol { + #[default] + Http, + LocalSocket, +} + +impl Display for Protocol { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Protocol::Http => write!(f, "http"), + Protocol::LocalSocket => { + if cfg!(windows) { + write!(f, "named pipe") + } else { + write!(f, "unix socket") + } + } + } + } +} + +#[derive(Debug, Serialize, Deserialize, TS)] +#[ts(export, rename_all = "camelCase")] +#[serde(rename_all(serialize = "camelCase", deserialize = "kebab-case"))] +pub struct BaseConfig { + pub port: u32, + pub socks_port: u32, + pub redir_port: u32, + pub tproxy_port: u32, + pub mixed_port: u32, + pub tun: TunConfig, + pub tuic_server: TuicServer, + pub ss_config: String, + pub vmess_config: String, + pub authentication: Option>, + pub skip_auth_prefixes: Option>, + pub lan_allowed_ips: Option>, + pub lan_disallowed_ips: Option>, + pub allow_lan: bool, + pub bind_address: String, + pub inbound_tfo: bool, + pub inbound_mptcp: bool, + pub mode: ClashMode, + pub unified_delay: bool, + pub log_level: LogLevel, + pub ipv6: bool, + pub interface_name: String, + pub routing_mark: isize, + pub geox_url: GeoXUrl, + pub geo_auto_update: bool, + pub geo_update_interval: isize, + pub geodata_mode: bool, + pub geodata_loader: String, + pub geosite_matcher: String, + pub tcp_concurrent: bool, + pub find_process_mode: FindProcessMode, + pub sniffing: bool, + pub global_client_fingerprint: String, + pub global_ua: String, + pub etag_support: bool, + pub keep_alive_interval: isize, + pub keep_alive_idle: isize, + pub disable_keep_alive: bool, +} + +#[derive(Debug, Serialize, Deserialize, TS)] +#[ts(export, rename_all = "camelCase")] +#[serde(rename_all(serialize = "camelCase", deserialize = "kebab-case"))] +pub struct TunConfig { + pub enable: bool, + pub device: String, + pub stack: TunStack, + pub dns_hijack: Vec, + pub auto_route: bool, + pub auto_detect_interface: bool, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub mtu: Option, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub gso: Option, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub gso_max_size: Option, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub inet4_address: Option>, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub inet6_address: Option>, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub iproute2_table_index: Option, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub iproute2_rule_index: Option, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub auto_redirect: Option, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub auto_redirect_input_mark: Option, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub auto_redirect_output_mark: Option, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub loopback_address: Option, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub strict_route: Option, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub route_address: Option>, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub route_address_set: Option>, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub route_exclude_address: Option>, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub route_exclude_address_set: Option>, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub include_interface: Option>, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub exclude_interface: Option>, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub include_uid: Option>, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub include_uid_range: Option>, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub exclude_uid: Option>, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub exclude_uid_range: Option>, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub exclude_src_port: Option>, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub exclude_src_port_range: Option>, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub exclude_dst_port: Option>, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub exclude_dst_port_range: Option>, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub include_android_user: Option>, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub include_package: Option>, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub exclude_package: Option>, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub endpoint_independent_nat: Option, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub udp_timeout: Option, + + pub file_descriptor: u32, + + // The following `inet*` fields will be deprecated + // refer: https://wiki.metacubex.one/config/inbound/tun/#_1 + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub inet4_route_address: Option>, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub inet6_route_address: Option>, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub inet4_route_exclude_address: Option>, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub inet6_route_exclude_address: Option>, + + // darwin special config + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub recvmsgx: Option, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub sendmsgx: Option, +} + +#[derive(Debug, Serialize, Deserialize, TS)] +#[ts(export, rename_all = "camelCase")] +#[serde(rename_all(serialize = "camelCase", deserialize = "kebab-case"))] +pub struct TuicServer { + pub enable: bool, + pub listen: String, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub token: Option>, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub users: Option>, + + pub certificate: String, + pub private_key: String, + pub ech_key: String, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub congestion_controller: Option, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub max_idle_time: Option, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub authentication_timeout: Option, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub alpn: Option>, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub max_udp_relay_packet_size: Option, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub max_datagram_frame_size: Option, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub cwnd: Option, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub mux_option: Option, +} + +#[derive(Debug, Serialize, Deserialize, TS)] +#[ts(export)] +pub struct MuxOption { + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub padding: Option, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub brutal: Option, +} + +#[derive(Debug, Serialize, Deserialize, TS)] +#[ts(export)] +pub struct BrutalOption { + pub enabled: bool, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub up: Option, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub down: Option, +} + +#[derive(Debug, Serialize, Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "lowercase")] +pub enum LogLevel { + DEBUG, + INFO, + WARNING, + ERROR, + SILENT, +} + +impl Display for LogLevel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + LogLevel::DEBUG => write!(f, "debug"), + LogLevel::INFO => write!(f, "info"), + LogLevel::WARNING => write!(f, "warning"), + LogLevel::ERROR => write!(f, "error"), + LogLevel::SILENT => write!(f, "silent"), + } + } +} + +#[derive(Debug, Serialize, Deserialize, TS)] +#[ts(export, rename_all = "camelCase")] +#[serde(rename_all(serialize = "camelCase", deserialize = "kebab-case"))] +pub struct GeoXUrl { + pub geo_ip: String, + pub mmdb: String, + pub asn: String, + pub geo_site: String, +} + +#[derive(Debug, Serialize, Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "lowercase")] +pub enum FindProcessMode { + Strict, + Always, + Off, +} + +/// mihomo version +#[derive(Debug, Serialize, Deserialize, TS)] +#[ts(export)] +pub struct MihomoVersion { + pub meta: bool, + pub version: String, +} + +#[derive(Debug, Serialize, Deserialize, TS)] +#[ts(export)] +pub enum CoreUpdaterChannel { + #[serde(rename = "release")] + ReleaseChannel, + #[serde(rename = "alpha")] + AlphaChannel, + #[serde(rename = "auto")] + Auto, +} + +impl Display for CoreUpdaterChannel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CoreUpdaterChannel::ReleaseChannel => write!(f, "release"), + CoreUpdaterChannel::AlphaChannel => write!(f, "alpha"), + CoreUpdaterChannel::Auto => write!(f, "auto"), + } + } +} + +/// clash mode enum +#[derive(Debug, Clone, Serialize, Deserialize, TS, PartialEq, Eq)] +#[ts(export)] +#[serde(rename_all = "lowercase")] +pub enum ClashMode { + Rule, + Global, + Direct, +} + +impl Display for ClashMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ClashMode::Rule => write!(f, "rule"), + ClashMode::Global => write!(f, "global"), + ClashMode::Direct => write!(f, "direct"), + } + } +} + +/// tun stack enum +#[derive(Debug, Serialize, Deserialize, TS)] +#[ts(export)] +pub enum TunStack { + Mixed, + #[serde(rename = "gVisor")] + Gvisor, + System, +} + +impl Display for TunStack { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TunStack::Mixed => write!(f, "Mixed"), + TunStack::Gvisor => write!(f, "gVisor"), + TunStack::System => write!(f, "System"), + } + } +} + +/// group proxies +#[derive(Debug, Serialize, Deserialize, TS)] +#[ts(export)] +pub struct Groups { + pub proxies: Vec, +} + +#[derive(Debug, Serialize, Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub struct Proxy { + // group type need + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub all: Option>, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub expected_status: Option, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub fixed: Option, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub hidden: Option, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub icon: Option, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub now: Option, + + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub test_url: Option, + + // single proxy type need + #[ts(optional)] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub id: Option, + + // basic fields + pub alive: bool, + pub history: Vec, + pub extra: HashMap, + pub name: String, + pub udp: bool, + pub uot: bool, + #[serde(rename = "type")] + pub proxy_type: ProxyType, + pub xudp: bool, + pub tfo: bool, + pub mptcp: bool, + pub smux: bool, + pub interface: String, + + #[serde(rename(serialize = "dialerProxy", deserialize = "dialer-proxy"))] + pub dialer_proxy: String, + + #[serde(rename(serialize = "routingMark", deserialize = "routing-mark"))] + pub routing_mark: i8, +} + +#[derive(Debug, Serialize, Deserialize, TS)] +#[ts(export)] +pub enum ProxyType { + Direct, + Reject, + RejectDrop, + Compatible, + Pass, + Dns, + Shadowsocks, + ShadowsocksR, + Snell, + Socks5, + Http, + Vmess, + Vless, + Trojan, + Hysteria, + Hysteria2, + WireGuard, + Tuic, + Ssh, + Mieru, + AnyTLS, + Relay, + Selector, + Fallback, + URLTest, + LoadBalance, +} + +#[derive(Debug, Serialize, Deserialize, TS)] +#[ts(export)] +pub struct Extra { + pub alive: bool, + pub history: Vec, +} + +#[derive(Debug, Serialize, Deserialize, TS)] +#[ts(export)] +pub struct DelayHistory { + pub time: String, + pub delay: u16, +} + +/// proxies +#[derive(Debug, Serialize, Deserialize, TS)] +#[ts(export)] +pub struct Proxies { + pub proxies: HashMap, +} + +/// proxy delay result +/// +/// displays a message if it times out, otherwise it only displays the delay +#[derive(Debug, Serialize, Deserialize, TS)] +#[ts(export)] +pub struct ProxyDelay { + pub delay: u32, +} + +#[derive(Debug, Serialize, Deserialize, TS)] +#[ts(export)] +pub struct ProxyProviders { + pub providers: HashMap, +} + +#[derive(Debug, Serialize, Deserialize, TS)] +#[ts(export)] +pub enum ProviderType { + Proxy, + Rule, +} + +#[derive(Debug, Serialize, Deserialize, TS)] +#[ts(export)] +pub enum VehicleType { + File, + HTTP, + Compatible, + Inline, +} + +#[derive(Debug, Serialize, Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub struct ProxyProvider { + pub name: String, + #[serde(rename = "type")] + pub provider_type: ProviderType, + pub vehicle_type: VehicleType, + pub proxies: Vec, + pub test_url: String, + pub expected_status: String, + pub updated_at: Option, + pub subscription_info: Option, +} + +#[derive(Debug, Serialize, Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "PascalCase")] +pub struct SubScriptionInfo { + pub upload: i64, + pub download: i64, + pub total: i64, + pub expire: i64, +} + +/// rules +#[derive(Debug, Serialize, Deserialize, TS)] +#[ts(export)] +pub struct Rules { + pub rules: Vec, +} + +#[derive(Debug, Serialize, Deserialize, TS)] +#[ts(export)] +pub struct Rule { + #[serde(rename = "type")] + pub rule_type: RuleType, + pub payload: String, + pub proxy: String, + pub size: i32, +} + +#[derive(Debug, Serialize, Deserialize, TS)] +#[ts(export)] +pub enum RuleType { + Domain, + DomainSuffix, + DomainKeyword, + DomainRegex, + GeoSite, + GeoIP, + SrcGeoIP, + IPASN, + SrcIPASN, + IPCIDR, + SrcIPCIDR, + IPSuffix, + SrcIPSuffix, + SrcPort, + DstPort, + InPort, + InUser, + InName, + InType, + ProcessName, + ProcessPath, + ProcessNameRegex, + ProcessPathRegex, + Match, + RuleSet, + Network, + DSCP, + Uid, + SubRules, + AND, + OR, + NOT, +} + +#[derive(Debug, Serialize, Deserialize, TS)] +#[ts(export)] +pub struct RuleProviders { + pub providers: HashMap, +} + +#[derive(Debug, Serialize, Deserialize, TS)] +#[ts(export)] +pub enum RuleBehavior { + Domain, + #[serde(rename = "IPCIDR")] + IpCidr, + Classical, +} + +#[derive(Debug, Serialize, Deserialize, TS)] +#[ts(export)] +pub enum RuleFormat { + #[serde(rename = "YamlRule")] + Yaml, + #[serde(rename = "TextRule")] + Text, + #[serde(rename = "MrsRule")] + Mrs, +} + +#[derive(Debug, Serialize, Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub struct RuleProvider { + pub behavior: RuleBehavior, + pub format: RuleFormat, + pub name: String, + pub rule_count: u32, + #[serde(rename = "type")] + pub provider_type: ProviderType, + pub updated_at: String, + pub vehicle_type: VehicleType, +} + +/// connections +#[derive(Debug, Serialize, Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub struct Connections { + pub download_total: u64, + pub upload_total: u64, + pub connections: Option>, + pub memory: u32, +} + +#[derive(Debug, Serialize, Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub struct Connection { + pub id: String, + pub metadata: ConnectionMetaData, + pub upload: u64, + pub download: u64, + pub start: String, + pub chains: Vec, + pub rule: String, + pub rule_payload: String, +} + +#[derive(Debug, Serialize, Deserialize, TS)] +#[ts(export)] +pub enum Network { + #[serde(rename = "tcp")] + TCP, + #[serde(rename = "udp")] + UDP, + #[serde(rename = "all")] + ALLNet, +} + +#[derive(Debug, Serialize, Deserialize, TS)] +#[ts(export)] +pub enum ConnectionType { + HTTP, + HTTPS, + #[serde(rename = "Socks4")] + SOCKS4, + #[serde(rename = "Socks5")] + SOCKS5, + #[serde(rename = "ShadowSocks")] + SHADOWSOCKS, + #[serde(rename = "Vmess")] + VMESS, + #[serde(rename = "Vless")] + VLESS, + #[serde(rename = "Redir")] + REDIR, + #[serde(rename = "TProxy")] + TPROXY, + #[serde(rename = "Trojan")] + TROJAN, + #[serde(rename = "Tunnel")] + TUNNEL, + #[serde(rename = "Tun")] + TUN, + #[serde(rename = "Tuic")] + TUIC, + #[serde(rename = "Hysteria2")] + HYSTERIA2, + #[serde(rename = "AnyTLS")] + ANYTLS, + #[serde(rename = "Inner")] + INNER, +} + +#[derive(Debug, Serialize, Deserialize, TS)] +#[ts(export)] +pub enum DNSMode { + #[serde(rename = "normal")] + Normal, + #[serde(rename = "fake-ip")] + FakeIP, + #[serde(rename = "redir-host")] + Mapping, + #[serde(rename = "hosts")] + Hosts, +} + +#[derive(Debug, Serialize, Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub struct ConnectionMetaData { + pub network: Network, + + #[serde(rename = "type")] + pub connection_type: ConnectionType, + + #[serde(rename = "sourceIP")] + pub source_ip: String, + + #[serde(rename = "destinationIP")] + pub destination_ip: String, + + #[serde(rename = "sourceGeoIP")] + pub source_geo_ip: Option>, + + #[serde(rename = "destinationGeoIP")] + pub destination_geo_ip: Option>, + + #[serde(rename = "sourceIPASN")] + pub source_ip_asn: String, + + #[serde(rename = "destinationIPASN")] + pub destination_ip_asn: String, + + pub source_port: String, + pub destination_port: String, + + #[serde(rename = "inboundIP")] + pub inbound_ip: String, + + pub inbound_port: String, + pub inbound_name: String, + pub inbound_user: String, + pub host: String, + pub dns_mode: DNSMode, + pub uid: u32, + pub process: String, + pub process_path: String, + pub special_proxy: String, + pub special_rules: String, + pub remote_destination: String, + pub dscp: u8, + pub sniff_host: String, +} + +#[derive(Debug, Serialize, Deserialize, TS)] +#[ts(export)] +pub struct Traffic { + pub up: u64, + pub down: u64, +} + +#[derive(Debug, Serialize, Deserialize, TS)] +#[ts(export)] +pub struct Memory { + pub inuse: u32, + pub oslimit: u32, +} + +#[derive(Debug, Serialize, Deserialize, TS)] +#[ts(export)] +pub struct Log { + #[serde(rename = "type")] + pub log_type: String, + pub payload: String, +} + +// ------------- use in rust, no need export to typescript ----------------- +#[derive(Deserialize, Serialize)] +pub struct ErrorResponse { + pub message: String, +} + +#[derive(Deserialize, Serialize)] +pub struct CloseFrame { + pub code: u16, + pub reason: String, +} + +#[derive(Deserialize, Serialize)] +#[serde(tag = "type", content = "data")] +pub enum WebSocketMessage { + Text(String), + Binary(Vec), + Ping(Vec), + Pong(Vec), + Close(Option), +} + +pub type ConnectionId = u32; +pub enum WebSocketWriter { + TcpStreamWriter(SplitSink>, Message>), + SocketStreamWriter(SplitSink, Message>), +} + +impl WebSocketWriter { + pub async fn send(&mut self, message: Message) -> crate::Result<()> { + match self { + WebSocketWriter::TcpStreamWriter(write) => { + write.send(message).await?; + } + WebSocketWriter::SocketStreamWriter(write) => { + write.send(message).await?; + } + } + Ok(()) + } +} + +#[derive(Default)] +pub struct ConnectionManager(pub RwLock>); diff --git a/crates/tauri-plugin-mihomo/src/utils.rs b/crates/tauri-plugin-mihomo/src/utils.rs new file mode 100644 index 000000000..e66e3fece --- /dev/null +++ b/crates/tauri-plugin-mihomo/src/utils.rs @@ -0,0 +1,42 @@ +#![allow(dead_code)] + +use base64::{Engine, engine::general_purpose}; +use rand::Rng; + +/// 生成 WebSocket 握手密钥 +pub fn generate_websocket_key() -> String { + // 生成 16 字节随机数 + let mut rng = rand::rng(); + let mut key = [0u8; 16]; + rng.fill(&mut key); + // Base64 编码 + general_purpose::STANDARD.encode(key) +} + +// 解析 chunked 数据, https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Transfer-Encoding#examples +// fn decode_chunked(data: &str) -> Result { +// let mut reader = BufReader::new(Cursor::new(data.as_bytes())); +// let mut result = Vec::new(); + +// loop { +// let mut line = String::new(); +// reader.read_line(&mut line)?; +// // 解析块大小(十六进制) +// if let Ok(chunk_size) = usize::from_str_radix(line.trim(), 16) { +// if chunk_size == 0 { +// break; +// } +// // 读取块数据 +// let mut chunk = vec![0; chunk_size]; +// reader.read_exact(&mut chunk)?; +// result.extend_from_slice(&chunk); +// // 跳过 \r\n 分隔符 +// reader.read_line(&mut String::new())?; +// } else { +// log::error!("Failed to parse chunk size: {line}"); +// return Err(Error::HttpParseError(format!("Failed to parse chunk size: {line}"))); +// } +// } +// let body = String::from_utf8(result)?; +// Ok(body) +// } diff --git a/crates/tauri-plugin-mihomo/tests/common/mod.rs b/crates/tauri-plugin-mihomo/tests/common/mod.rs new file mode 100644 index 000000000..96206ef18 --- /dev/null +++ b/crates/tauri-plugin-mihomo/tests/common/mod.rs @@ -0,0 +1,43 @@ +use std::sync::Arc; + +use tauri_plugin_mihomo::{Mihomo, models::Protocol}; + +#[allow(dead_code)] +pub const TEST_URL: &str = "http://www.gstatic.com/generate_204"; +#[allow(dead_code)] +pub const TIMEOUT: u32 = 3000; + +pub fn mihomo() -> Mihomo { + dotenvy::dotenv().unwrap(); + let mihomo_socket = std::env::var("MIHOMO_SOCKET").unwrap_or(String::from("0")); + if mihomo_socket == "1" { + println!("connect to mihomo by local socket"); + // use local socket + let socket_path = if cfg!(unix) { + "/tmp/verge-mihomo.sock".to_string() + // "/tmp/clash-rs.sock".to_string() + } else { + r"\\.\pipe\verge-mihomo".to_string() + // r"\\.\pipe\clash-rs".to_string() + }; + Mihomo { + protocol: Protocol::LocalSocket, + external_host: None, + external_port: None, + secret: None, + socket_path: Some(socket_path), + connection_manager: Arc::new(Default::default()), + } + } else { + println!("connect to mihomo by http"); + // use http + Mihomo { + protocol: Protocol::Http, + external_host: Some("127.0.0.1".into()), + external_port: Some(9090), + secret: Some("yPMJk9i7UaR1hv3-2BkPy".into()), + socket_path: None, + connection_manager: Arc::new(Default::default()), + } + } +} diff --git a/crates/tauri-plugin-mihomo/tests/common_test.rs b/crates/tauri-plugin-mihomo/tests/common_test.rs new file mode 100644 index 000000000..313c5d50f --- /dev/null +++ b/crates/tauri-plugin-mihomo/tests/common_test.rs @@ -0,0 +1,85 @@ +use serde_json::json; +use tauri_plugin_mihomo::{Error, Result, models::ClashMode}; + +mod common; + +#[tokio::test] +async fn mihomo_common_get_version() -> Result<()> { + let mihomo = common::mihomo(); + let version = mihomo.get_version().await?; + println!("{version:?}"); + Ok(()) +} + +#[tokio::test] +async fn mihomo_common_get_base_config() -> Result<()> { + let mihomo = common::mihomo(); + let config = mihomo.get_base_config().await?; + println!("{config:?}"); + Ok(()) +} + +#[tokio::test] +async fn mihomo_common_patch_base_config() -> Result<()> { + let mihomo = common::mihomo(); + let mut base_config = mihomo.get_base_config().await?; + let origin_mode = base_config.mode.clone(); + println!("before changed mode: {}", base_config.mode); + + let next_mode = ClashMode::Global; + let body = json!({ "mode": next_mode }); + mihomo.patch_base_config(&body).await?; + base_config = mihomo.get_base_config().await?; + println!("changed mode: {}", base_config.mode); + assert_eq!(base_config.mode, next_mode); + + let body = json!({ "mode": origin_mode }); + mihomo.patch_base_config(&body).await?; + base_config = mihomo.get_base_config().await?; + println!("reset mode: {}", base_config.mode); + assert_eq!(base_config.mode, origin_mode); + Ok(()) +} + +#[tokio::test] +async fn mihomo_common_update_geo() -> Result<()> { + let mihomo = common::mihomo(); + if let Err(err) = mihomo.update_geo().await { + if let Error::FailedResponse(msg) = err { + println!("{msg}") + } else { + return Err(err); + } + } + Ok(()) +} + +#[tokio::test] +async fn mihomo_common_flush_fakeip() -> Result<()> { + let mihomo = common::mihomo(); + mihomo.flush_fakeip().await?; + Ok(()) +} + +#[tokio::test] +async fn mihomo_common_flush_dns() -> Result<()> { + let mihomo = common::mihomo(); + mihomo.flush_dns().await?; + Ok(()) +} + +// 需要单独测试,避免其他测试时,内核还未完全加载配置文件 +#[tokio::test] +async fn reload_config() -> Result<()> { + let mihomo = common::mihomo(); + mihomo.reload_config(true, "").await?; + Ok(()) +} + +// 需要单独测试,避免其他测试时,内核还未完全启动 +#[tokio::test] +async fn restart() -> Result<()> { + let mihomo = common::mihomo(); + mihomo.restart().await?; + Ok(()) +} diff --git a/crates/tauri-plugin-mihomo/tests/connection_test.rs b/crates/tauri-plugin-mihomo/tests/connection_test.rs new file mode 100644 index 000000000..b4fc7b82b --- /dev/null +++ b/crates/tauri-plugin-mihomo/tests/connection_test.rs @@ -0,0 +1,41 @@ +use tauri_plugin_mihomo::{Result, ret_failed_resp}; + +mod common; + +#[tokio::test] +async fn mihomo_connection_list() -> Result<()> { + let mihomo = common::mihomo(); + let connections = mihomo.get_connections().await?; + println!("{connections:?}"); + Ok(()) +} + +#[tokio::test] +async fn mihomo_connection_close() -> Result<()> { + let mihomo = common::mihomo(); + let mut max_count = 20; + let connections = loop { + if max_count == 0 { + ret_failed_resp!("no connections"); + } + let conns = mihomo.get_connections().await?; + if conns.connections.is_some() { + break conns; + } + max_count -= 1; + tokio::time::sleep(std::time::Duration::from_millis(300)).await; + }; + if let Some(connections) = connections.connections + && let Some(first) = connections.first() + { + mihomo.close_connection(&first.id).await?; + } + Ok(()) +} + +#[tokio::test] +async fn mihomo_connection_close_all() -> Result<()> { + let mihomo = common::mihomo(); + mihomo.close_all_connections().await?; + Ok(()) +} diff --git a/crates/tauri-plugin-mihomo/tests/group_test.rs b/crates/tauri-plugin-mihomo/tests/group_test.rs new file mode 100644 index 000000000..8b918b640 --- /dev/null +++ b/crates/tauri-plugin-mihomo/tests/group_test.rs @@ -0,0 +1,196 @@ +use tauri_plugin_mihomo::{ + Result, failed_resp, + models::{ProxyType, VehicleType}, +}; + +use crate::common::{TEST_URL, TIMEOUT}; + +mod common; + +#[tokio::test] +async fn mihomo_group_list() -> Result<()> { + let mihomo = common::mihomo(); + let groups = mihomo.get_groups().await?; + println!("{groups:?}"); + Ok(()) +} + +#[tokio::test] +async fn mihomo_group_get_by_name() -> Result<()> { + let mihomo = common::mihomo(); + let groups = mihomo.get_groups().await?; + let proxy = groups.proxies.first().ok_or(failed_resp!("no proxies"))?; + let group = mihomo.get_group_by_name(&proxy.name).await?; + println!("{group:?}"); + Ok(()) +} + +#[tokio::test] +async fn mihomo_group_delay() -> Result<()> { + let mihomo = common::mihomo(); + let groups = mihomo.get_groups().await?; + let proxy = groups.proxies.first().ok_or(failed_resp!("no proxies"))?; + let group_name = proxy.name.as_ref(); + let response = mihomo.delay_group(group_name, TEST_URL, TIMEOUT).await?; + println!("group [{}] delay, {response:#?}", group_name); + Ok(()) +} + +#[tokio::test] +async fn mihomo_group_select_node() -> Result<()> { + let mihomo = common::mihomo(); + let groups = mihomo.get_groups().await?; + let selector_group = groups + .proxies + .iter() + .find(|i| matches!(i.proxy_type, ProxyType::Selector)) + .ok_or(failed_resp!("not fount selector group"))?; + let origin_now = selector_group.now.clone().unwrap(); + println!("[{}], before select: {:?}", selector_group.name, selector_group.now); + + let node = selector_group + .all + .as_ref() + .ok_or(failed_resp!("field `all` is empty"))? + .iter() + .find(|i| **i != origin_now) + .ok_or(failed_resp!("empty node"))?; + mihomo.select_node_for_group(&selector_group.name, node).await?; + let groups = mihomo.get_groups().await?; + let selector_group = groups + .proxies + .iter() + .find(|&i| i.name == selector_group.name) + .ok_or(failed_resp!("not fount selector group"))?; + println!("[{}], selected: {:?}", selector_group.name, selector_group.now); + + mihomo.select_node_for_group(&selector_group.name, &origin_now).await?; + let groups = mihomo.get_groups().await?; + let selector_group = groups + .proxies + .iter() + .find(|&i| i.name == selector_group.name) + .ok_or(failed_resp!("not fount selector group"))?; + println!("[{}], reset selector: {:?}", selector_group.name, selector_group.now); + Ok(()) +} + +#[tokio::test] +async fn mihomo_group_unfixed() -> Result<()> { + let mihomo = common::mihomo(); + let groups = mihomo.get_groups().await?; + let urlmihomo_group = groups + .proxies + .iter() + .find(|i| matches!(i.proxy_type, ProxyType::URLTest)) + .ok_or(failed_resp!("not fount url test group"))?; + println!("[{}], before fixed: {:?}", urlmihomo_group.name, urlmihomo_group.fixed); + + let node = urlmihomo_group + .all + .as_ref() + .ok_or(failed_resp!("field `all` is empty"))? + .iter() + .next() + .ok_or(failed_resp!("empty node"))?; + mihomo.select_node_for_group(&urlmihomo_group.name, node).await?; + let groups = mihomo.get_groups().await?; + let urlmihomo_group = groups + .proxies + .iter() + .find(|&i| i.name == urlmihomo_group.name) + .ok_or(failed_resp!("not fount url test group"))?; + println!("[{}], fixed: {:?}", urlmihomo_group.name, urlmihomo_group.fixed); + + mihomo.unfixed_proxy(&urlmihomo_group.name).await?; + let groups = mihomo.get_groups().await?; + let urlmihomo_group = groups + .proxies + .iter() + .find(|&i| i.name == urlmihomo_group.name) + .ok_or(failed_resp!("not fount url test group"))?; + println!("[{}], after unfixed: {:?}", urlmihomo_group.name, urlmihomo_group.fixed); + Ok(()) +} + +// group providers +#[tokio::test] +async fn mihomo_group_providers() -> Result<()> { + let mihomo = common::mihomo(); + let providers = mihomo.get_proxy_providers().await?; + println!("{:?}", providers.providers); + Ok(()) +} + +#[tokio::test] +async fn mihomo_group_get_provider_by_name() -> Result<()> { + let mihomo = common::mihomo(); + let providers = mihomo.get_proxy_providers().await?; + let (provider_name, _provider) = providers + .providers + .iter() + .find(|(_name, provider)| matches!(provider.vehicle_type, VehicleType::HTTP | VehicleType::File)) + .ok_or(failed_resp!("not found provider"))?; + let provider = mihomo.get_proxy_provider_by_name(provider_name).await?; + println!("provider [{provider:?}]"); + Ok(()) +} + +#[tokio::test] +async fn mihomo_group_update_provider() -> Result<()> { + let mihomo = common::mihomo(); + let providers = mihomo.get_proxy_providers().await?; + let (provider_name, provider) = providers + .providers + .iter() + .find(|(_name, provider)| matches!(provider.vehicle_type, VehicleType::HTTP | VehicleType::File)) + .ok_or(failed_resp!("not found provider"))?; + println!( + "provider [{}], update time: {}", + provider_name, + provider.updated_at.as_ref().unwrap() + ); + mihomo.update_proxy_provider(provider_name).await?; + let provider = mihomo.get_proxy_provider_by_name(provider_name).await?; + println!( + "provider [{}], update time: {}", + provider_name, + provider.updated_at.unwrap() + ); + Ok(()) +} + +#[tokio::test] +async fn mihomo_group_health_check_provider() -> Result<()> { + let mihomo = common::mihomo(); + let providers = mihomo.get_proxy_providers().await?; + let (provider_name, _provider) = providers + .providers + .iter() + .find(|(_name, provider)| matches!(provider.vehicle_type, VehicleType::HTTP | VehicleType::File)) + .ok_or(failed_resp!("not found provider"))?; + println!("health check provider [{}]", provider_name); + mihomo.healthcheck_proxy_provider(provider_name).await?; + Ok(()) +} + +#[tokio::test] +async fn mihomo_group_health_check_node_in_provider() -> Result<()> { + let mihomo = common::mihomo(); + let providers = mihomo.get_proxy_providers().await?; + let (provider_name, provider) = providers + .providers + .iter() + .find(|(_name, provider)| matches!(provider.vehicle_type, VehicleType::HTTP | VehicleType::File)) + .ok_or(failed_resp!("not found provider"))?; + + let proxy = provider.proxies.first().ok_or(failed_resp!("provider proxies empty"))?; + + println!("health check provider [{}] proxy [{}]", provider_name, proxy.name); + let delay = mihomo + .healthcheck_node_in_provider(provider_name, &proxy.name, TEST_URL, TIMEOUT) + .await?; + println!("health check delay: {:?}", delay); + + Ok(()) +} diff --git a/crates/tauri-plugin-mihomo/tests/proxy_test.rs b/crates/tauri-plugin-mihomo/tests/proxy_test.rs new file mode 100644 index 000000000..7cbb7cd97 --- /dev/null +++ b/crates/tauri-plugin-mihomo/tests/proxy_test.rs @@ -0,0 +1,72 @@ +use tauri_plugin_mihomo::{Result, failed_resp}; + +use crate::common::{TEST_URL, TIMEOUT}; + +mod common; + +#[tokio::test] +async fn mihomo_proxy_list() -> Result<()> { + let mihomo = common::mihomo(); + let proxies = mihomo.get_proxies().await?; + println!("{proxies:?}"); + Ok(()) +} + +#[tokio::test] +async fn mihomo_proxy_get_by_name() -> Result<()> { + let mihomo = common::mihomo(); + let proxies = mihomo.get_proxies().await?; + let proxy_name = proxies.proxies.keys().next().ok_or(failed_resp!("empty proxies"))?; + let proxy = mihomo.get_proxy_by_name(proxy_name).await?; + println!("{proxy:?}"); + Ok(()) +} + +#[tokio::test] +async fn mihomo_proxy_delay() -> Result<()> { + let mihomo = common::mihomo(); + let groups = mihomo.get_groups().await?; + let proxy = groups + .proxies + .iter() + .find(|&i| i.all.as_ref().is_some_and(|a| !a.is_empty())) + .ok_or(failed_resp!("not found group"))?; + let proxy_name = proxy + .all + .as_ref() + .ok_or(failed_resp!("field `all` is empty"))? + .first() + .ok_or(failed_resp!("get first node failed"))?; + let delay = mihomo.delay_proxy_by_name(proxy_name, TEST_URL, TIMEOUT).await?; + println!("proxy [{}] delay: {:?}", proxy_name, delay); + Ok(()) +} + +// 并发测试节点延迟 +// #[tokio::test] +// async fn mihomo_proxy_spawn_delay() -> Result<()> { +// let mihomo = common::mihomo(); +// let groups = mihomo.get_groups().await?; +// let proxies = groups.proxies[0].all.as_ref().unwrap(); +// let mut tasks = Vec::new(); +// let arc_mihomo = Arc::new(mihomo); +// for _ in 0..=10 { +// for proxy in proxies.clone().into_iter() { +// let mihomo_ = Arc::clone(&arc_mihomo); +// tasks.push(tokio::spawn(async move { +// match mihomo_.delay_proxy_by_name(&proxy, TEST_URL, TIMEOUT).await { +// Ok(delay) => { +// println!("{proxy}: {delay:?}"); +// } +// Err(e) => { +// println!("{proxy}: error: {e}"); +// } +// } +// })); +// } +// } +// for task in tasks.into_iter() { +// task.await.unwrap(); +// } +// Ok(()) +// } diff --git a/crates/tauri-plugin-mihomo/tests/rule_test.rs b/crates/tauri-plugin-mihomo/tests/rule_test.rs new file mode 100644 index 000000000..a597ae648 --- /dev/null +++ b/crates/tauri-plugin-mihomo/tests/rule_test.rs @@ -0,0 +1,33 @@ +use tauri_plugin_mihomo::{Result, failed_resp}; + +mod common; + +#[tokio::test] +async fn mihomo_rule_list() -> Result<()> { + let mihomo = common::mihomo(); + let rules = mihomo.get_rules().await?; + println!("{:?}", rules.rules); + Ok(()) +} + +#[tokio::test] +async fn mihomo_rule_providers() -> Result<()> { + let mihomo = common::mihomo(); + let providers = mihomo.get_rule_providers().await?; + println!("{:?}", providers.providers); + Ok(()) +} + +#[tokio::test] +async fn mihomo_rule_update_provider() -> Result<()> { + let mihomo = common::mihomo(); + let providers = mihomo.get_rule_providers().await?; + let provider_name = providers + .providers + .keys() + .next() + .ok_or(failed_resp!("no rule provider"))?; + println!("update rule provider: {}", provider_name); + mihomo.update_rule_provider(provider_name).await?; + Ok(()) +} diff --git a/crates/tauri-plugin-mihomo/tests/upgrade_test.rs b/crates/tauri-plugin-mihomo/tests/upgrade_test.rs new file mode 100644 index 000000000..91d4078ad --- /dev/null +++ b/crates/tauri-plugin-mihomo/tests/upgrade_test.rs @@ -0,0 +1,42 @@ +use tauri_plugin_mihomo::{Error, Result, models::CoreUpdaterChannel}; + +mod common; + +#[tokio::test] +async fn mihomo_upgrade_geo() -> Result<()> { + let mihomo = common::mihomo(); + if let Err(err) = mihomo.upgrade_geo().await { + if let Error::FailedResponse(msg) = err { + println!("{msg}"); + } else { + return Err(err); + } + } + Ok(()) +} + +#[tokio::test] +async fn mihomo_upgrade_ui() -> Result<()> { + let mihomo = common::mihomo(); + if let Err(err) = mihomo.upgrade_ui().await { + if let Error::FailedResponse(msg) = err { + println!("{msg}"); + } else { + return Err(err); + } + } + Ok(()) +} + +#[tokio::test] +async fn mihomo_upgrade_core() -> Result<()> { + let mihomo = common::mihomo(); + if let Err(err) = mihomo.upgrade_core(CoreUpdaterChannel::Auto, false).await { + if let Error::FailedResponse(msg) = err { + println!("{msg}"); + } else { + return Err(err); + } + } + Ok(()) +} diff --git a/crates/tauri-plugin-mihomo/tests/websocket_test.rs b/crates/tauri-plugin-mihomo/tests/websocket_test.rs new file mode 100644 index 000000000..de1c33513 --- /dev/null +++ b/crates/tauri-plugin-mihomo/tests/websocket_test.rs @@ -0,0 +1,135 @@ +use std::{fmt::Debug, time::Duration}; + +use serde::de::DeserializeOwned; +use tauri::ipc::{Channel, InvokeResponseBody}; +use tauri_plugin_mihomo::{ + Result, + models::{Connections, Log, LogLevel, Memory, Traffic, WebSocketMessage}, +}; + +mod common; + +fn handle_message() -> Channel { + Channel::new(|message| { + match message { + InvokeResponseBody::Json(msg) => { + if let Ok(WebSocketMessage::Text(data)) = serde_json::from_str(&msg) { + if data.starts_with("websocket error") { + println!("received error: {data}"); + } else { + let data = serde_json::from_str::(&data).unwrap(); + println!("{data:?}"); + } + } + } + InvokeResponseBody::Raw(raw) => { + println!("{}", String::from_utf8(raw).unwrap()); + } + } + Ok(()) + }) +} + +#[tokio::test] +async fn mihomo_websocket_memory() -> Result<()> { + let mihomo = common::mihomo(); + let on_message = handle_message::(); + let websocket_id = mihomo + .ws_memory(move |data| { + let _ = on_message.send(data); + }) + .await?; + println!("WebSocket ID: {websocket_id}"); + tokio::time::sleep(Duration::from_millis(5000)).await; + mihomo.disconnect(websocket_id, Some(0)).await?; + for i in 0..10 { + println!("check connection exist {i}"); + let manager = mihomo.connection_manager.clone(); + let manager = manager.0.read().await; + if manager.get(&websocket_id).is_none() { + println!("connection exist"); + break; + } + tokio::time::sleep(Duration::from_secs(1)).await; + } + tokio::time::sleep(Duration::from_secs(3)).await; + Ok(()) +} + +#[tokio::test] +async fn mihomo_websocket_traffic() -> Result<()> { + let mihomo = common::mihomo(); + let on_message = handle_message::(); + let websocket_id = mihomo + .ws_traffic(move |data| { + let _ = on_message.send(data); + }) + .await?; + println!("WebSocket ID: {websocket_id}"); + tokio::time::sleep(Duration::from_millis(5000)).await; + mihomo.disconnect(websocket_id, Some(0)).await?; + for i in 0..10 { + println!("check connection exist {i}"); + let manager = mihomo.connection_manager.clone(); + let manager = manager.0.read().await; + if manager.get(&websocket_id).is_none() { + println!("connection exist"); + break; + } + tokio::time::sleep(Duration::from_secs(1)).await; + } + tokio::time::sleep(Duration::from_secs(3)).await; + Ok(()) +} + +#[tokio::test] +async fn mihomo_websocket_log() -> Result<()> { + let mihomo = common::mihomo(); + let on_message = handle_message::(); + let websocket_id = mihomo + .ws_logs(LogLevel::DEBUG, move |data| { + let _ = on_message.send(data); + }) + .await?; + println!("WebSocket ID: {websocket_id}"); + tokio::time::sleep(Duration::from_millis(5000)).await; + mihomo.disconnect(websocket_id, Some(0)).await?; + for i in 0..10 { + println!("check connection exist {i}"); + let manager = mihomo.connection_manager.clone(); + let manager = manager.0.read().await; + if manager.get(&websocket_id).is_none() { + println!("connection exist"); + break; + } + tokio::time::sleep(Duration::from_secs(1)).await; + } + tokio::time::sleep(Duration::from_secs(3)).await; + Ok(()) +} + +#[tokio::test] +async fn mihomo_websocket_connections() -> Result<()> { + let mihomo = common::mihomo(); + let on_message = handle_message::(); + let websocket_id = mihomo + .ws_connections(move |data| { + let _ = on_message.send(data); + }) + .await?; + println!("WebSocket ID: {websocket_id}"); + tokio::time::sleep(Duration::from_millis(5000)).await; + mihomo.disconnect(websocket_id, Some(0)).await?; + for i in 0..10 { + println!("check connection exist {i}"); + let manager = mihomo.connection_manager.clone(); + let manager = manager.0.read().await; + if manager.get(&websocket_id).is_none() { + println!("connection exist"); + break; + } + tokio::time::sleep(Duration::from_secs(1)).await; + } + tokio::time::sleep(Duration::from_secs(3)).await; + Ok(()) +} diff --git a/crates/tauri-plugin-mihomo/tsconfig.json b/crates/tauri-plugin-mihomo/tsconfig.json new file mode 100644 index 000000000..059112270 --- /dev/null +++ b/crates/tauri-plugin-mihomo/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "es2021", + "module": "esnext", + "moduleResolution": "bundler", + "skipLibCheck": true, + "strict": true, + "noUnusedLocals": true, + "noImplicitAny": true, + "noEmit": true + }, + "include": ["guest-js/*.ts"], + "exclude": ["dist-js", "node_modules"] +}