系统架构
Zephyr 采用 Tauri v2 混合架构,以 Rust 后端为核心引擎、原生 JavaScript 前端为交互层、Mihomo 内核为代理执行层,三层协同工作,实现高性能、低资源占用的代理管理体验。本页描述 v2.5.0 的代码结构。
目录
- 架构总览
- 仓库结构
- 核心启动流程
- 订阅下载流程
- 配置热重载流程
- 系统代理设置流程
- 前端初始化顺序
- 加密体系
- Tauri Commands 分类表
- 事件系统
- 数据结构定义
- 数据流设计
- Rust 后端模块详解
- 前端架构
- 并发安全与原子变量
- 应用生命周期
- 依赖版本
架构总览
Zephyr 的架构可以概括为 四层模型,从上到下依次为前端层、IPC 桥接层、Rust 后端层和 Mihomo 内核层。平台无关的纯逻辑下沉到 crates/core(zephyr-core),桌面端 Rust 代码只保留 Tauri 相关的胶水与平台实现。
%%{init: {'themeVariables': {'fontSize': '9px', 'nodeBorder': '1px', 'clusterBorder': '1px', 'edgeLabelHeight': '8px'}}}%%
graph TB
subgraph Frontend["前端层 (Frontend Layer)"]
direction LR
main_js["main.js<br/>应用入口"]
ui_js["src/ui/<br/>71 个 UI 模块"]
api_js["api.js<br/>IPC + REST 封装"]
ws_js["websocket.js<br/>流量流"]
mod_js["src/modules/<br/>连接 / 流量图 / 后端事件"]
i18n_js["i18n.js<br/>14 种语言"]
end
subgraph Bridge["Tauri IPC 桥接层 (Bridge Layer)"]
direction LR
cmd_registry["generate_handler!<br/>180 个命令"]
event_sys["emit_to_main<br/>15 个后端事件"]
state_mgr["Tauri State<br/>托管状态"]
rate_lim["RateLimiter<br/>命令限流"]
end
subgraph Backend["Rust 后端层 (Backend Layer)"]
direction LR
lib_rs["lib.rs<br/>入口 / 设置 / 生命周期"]
core_mgr["core_manager.rs<br/>→ core/ 子模块"]
prism_rs["prism.rs<br/>→ prism/ Prism Engine"]
sys_proxy["sys_proxy.rs<br/>系统代理"]
tray_rs["tray.rs<br/>系统托盘"]
updater["updater.rs<br/>内核 / Geo / 客户端更新"]
more_rs["elevation / process_icon /<br/>network_coordinator / backup ..."]
zcore["crates/core<br/>zephyr-core 纯逻辑"]
end
subgraph Core["Mihomo 内核层 (Core Layer)"]
direction LR
rest_api["RESTful API<br/>127.0.0.1:9090"]
http_stream["HTTP Streaming<br/>/traffic"]
tun["TUN 虚拟网卡"]
rule_engine["规则引擎"]
dns_resolver["DNS 解析"]
end
Frontend -->|"invoke() / listen()"| Bridge
Bridge -->|"Command Handlers"| Backend
Backend -->|"进程启动<br/>REST API<br/>系统 API"| Core
Core -->|"HTTP Streaming<br/>JSON 响应"| Frontend
style Frontend fill:#e3f2fd,stroke:#1565c0,color:#0d47a1
style Bridge fill:#fff3e0,stroke:#e65100,color:#bf360c
style Backend fill:#e8f5e9,stroke:#2e7d32,color:#1b5e20
style Core fill:#fce4ec,stroke:#c62828,color:#b71c1c前端除了通过 IPC 调用 Rust 命令外,还会直接以
Authorization: Bearer <secret>访问 Mihomo 的 REST API(代理组、连接、延迟测试、/traffic流等)。secret与端口由start_core返回。
设计原则
| 原则 | 实现方式 |
|---|---|
| 零前端框架 | 原生 JavaScript(ES Module + // @ts-check JSDoc 类型检查)+ DOM 操作,无 React/Vue/Svelte 依赖;不经打包器,index.html 通过 importmap 解析 @zephyr/shared |
| 零图表库 | 原生 Canvas 2D API 绘制流量面积图 |
| 内存安全 | Rust 所有权系统 + 编译期检查;workspace 级 clippy 规则禁止 unwrap/expect/panic |
| 纵深防御 | SSRF 防护、命令限流、危险配置键清除、AES-256-GCM 加密、严格 CSP、仅 loopback 系统代理 |
| 逻辑下沉 | 纯函数(YAML 合并、清洗、订阅校验、版本解析等)放在 crates/core,可被桌面端与移动端(UniFFI)复用 |
| 跨平台一致 | 单一代码库覆盖 Windows / macOS / Linux,差异化能力(TUN 提权、系统代理、应用图标、WebView 恢复)按平台精准适配 |
MVVM 架构交互时序图
以下时序图展示了从用户点击 UI 元素到视图重新渲染的全链路。
%%{init: {'themeVariables': {'fontSize': '10px', 'actorTextColor': '#ccc', 'signalColor': '#666', 'labelTextColor': '#999', 'labelBoxBkgColor': '#333', 'labelBoxBorderColor': '#555', 'lineColor': '#666', 'actorBkg': '#333', 'actorBorder': '#555', 'actorTextColor': '#ccc', 'signalTextColor': '#ccc', 'noteBkgColor': '#333', 'noteTextColor': '#ccc', 'noteBorderColor': '#555'}}}%%
sequenceDiagram
participant User as 用户
participant View as View (DOM)
participant VM as ViewModel (src/ui/)
participant Store as appStore / Bus
participant API as api.js
participant IPC as Tauri IPC Bridge
participant Rust as Rust Command Handler
participant Core as Mihomo Core / 文件系统
User->>View: 点击 UI 元素
View->>VM: addEventListener 回调
VM->>VM: 状态校验 / 参数组装
VM->>API: invoke(COMMANDS.X, params)
API->>IPC: __TAURI_INTERNALS__.invoke()
IPC->>Rust: Command Handler 分发
Rust->>Rust: 限流检查(部分命令)
Rust->>Core: 操作 Mihomo 或读写文件
Core-->>Rust: 返回结果
Rust-->>IPC: Result<T, String> 序列化
IPC-->>API: Promise resolve / reject
API-->>VM: 返回结果
VM->>Store: appStore.set() / Bus.emit()
Store->>View: bind() 订阅 → 更新 DOM
View-->>User: 界面重新渲染整体数据流
%%{init: {'themeVariables': {'fontSize': '10px', 'nodeBorder': '1px', 'clusterBorder': '1px', 'edgeLabelHeight': '10px'}, 'flowchart': {'curve': 'basis', 'nodeSpacing': 15, 'rankSpacing': 25}}}%%
graph LR
subgraph CommandPath["命令通路 (请求-响应)"]
A["用户操作"] --> B["src/ui/<br/>事件处理"]
B --> C["api.js<br/>invoke"]
C --> D["Tauri IPC Bridge"]
D --> E["Rust Command Handler"]
E --> F["core/ · prism/ · sys_proxy<br/>tray · updater · elevation ..."]
F --> G["Result<T, String><br/>返回前端"]
end
subgraph TrafficPath["流量监控通路 (推送流)"]
H["Mihomo Core"] --> I["GET /traffic<br/>HTTP Streaming"]
I --> J["websocket.js<br/>fetch ReadableStream"]
J --> K["滑动窗口<br/>60 数据点"]
K --> L["modules/traffic-chart.js<br/>Canvas 2D"]
L --> M["requestAnimationFrame<br/>节流渲染"]
end
style CommandPath fill:#e3f2fd,stroke:#1565c0,color:#0d47a1
style TrafficPath fill:#fce4ec,stroke:#c62828,color:#b71c1c仓库结构
Zephyr 是 Cargo workspace + pnpm workspace 组成的 monorepo。
Zephyr/
├── Cargo.toml # Cargo workspace:apps/desktop/src-tauri、crates/core、crates/core-ffi
├── package.json # pnpm 根(dev / build / lint / test / typecheck 脚本)
├── pnpm-workspace.yaml # packages: apps/*, packages/*
├── rust-toolchain.toml # 固定 Rust 1.96.0
├── apps/
│ └── desktop/ # @zephyr/desktop
│ ├── src/ # 前端(frontendDist,直接作为 WebView 根目录)
│ │ ├── index.html · styles.css · main.js · api.js · websocket.js · i18n.js · rules.js
│ │ ├── ui/ # UI 模块(含 settings/、editor/、rule-library/ 子目录)
│ │ ├── modules/ # connections / traffic-chart / backend-events
│ │ ├── utils/ # 工具模块
│ │ ├── types/ # 类型守卫与 .d.ts
│ │ └── cm6.bundle.js · js-yaml.min.js # 预构建的第三方库
│ └── src-tauri/ # Rust 后端(crate 名 Zephyr,lib 名 my_clash_gui_lib)
│ ├── src/ # 见「Rust 后端模块详解」
│ ├── capabilities/ # Tauri v2 权限声明
│ ├── icons/ # 应用图标与托盘图标
│ ├── bundled/ # 随包资源
│ ├── THIRD-PARTY-LICENSES.txt
│ ├── tauri.conf.json · tauri.linux.conf.json
│ └── Cargo.toml
├── crates/
│ ├── core/ # zephyr-core:平台无关纯逻辑(可选 uniffi feature)
│ └── core-ffi/ # zephyr-core-ffi:cdylib/staticlib,供移动端 UniFFI 绑定
├── packages/
│ ├── shared/ # @zephyr/shared:IPC 命令名常量(单一来源)
│ ├── tokens/ # 设计令牌(style-dictionary → CSS 变量)
│ └── scripts/ # check-i18n.js 等脚本
└── scripts/ # 打包与许可证生成脚本构建 CSS 时,
copy-shared把packages/shared/src/index.js复制到apps/desktop/src/_shared/index.js,copy-tokens把设计令牌 CSS 复制到src/,再由 UnoCSS 生成样式。
核心启动流程
start_core(core/core_process.rs)负责安全、可靠地启动 Mihomo 内核进程。命令外层由 catch_unwind 包裹,内部任何 panic 都会转换为错误返回而不会让应用崩溃。
%%{init: {'themeVariables': {'fontSize': '9px', 'nodeBorder': '1px', 'clusterBorder': '1px', 'edgeLabelHeight': '8px'}, 'flowchart': {'curve': 'basis', 'nodeSpacing': 12, 'rankSpacing': 15}}}%%
flowchart TD
A["start_core(config_path, custom_args, secret?, force?)"] --> B{"获取 CORE_STARTING 锁"}
B -->|"被占用"| B2["每 200ms 重试<br/>超过 10s 强制重置"]
B2 --> B
B -->|"获取成功"| C{"macOS:TUN 已开启<br/>且同一配置的 root 内核仍健康?"}
C -->|"是(非 force)"| REUSE["直接复用<br/>返回已记录的 secret + port"]
C -->|"否"| D["连接排空<br/>DELETE /connections"]
D --> E["停止旧内核<br/>kill_mihomo()<br/>(macOS TUN 根进程走提权路径)"]
E --> F{"macOS?"}
F -->|"是"| G["等待 API 端口释放<br/>最多 5 秒"]
F -->|"否"| H
G --> H["生成 / 采用 API Secret<br/>32 位字母数字"]
H --> I["prepare_runtime_config()<br/>注入 external-controller + secret<br/>合并 settings.json 全局偏好"]
I --> J["写入 core/run_config.yaml"]
J --> K["预检:mihomo -t -f run_config.yaml"]
K -->|"失败"| ERR1["返回错误<br/>日志可在错误卡片中查看"]
K -->|"通过"| L{"需要 TUN 且为 macOS?"}
L -->|"是"| ROOT["restart_core_as_root<br/>管理员授权后以 root 启动"]
L -->|"否"| M["spawn_with_cache_retry<br/>启动 zephyr-mihomo<br/>cache.db 锁冲突时重试"]
M --> N["孤儿防护<br/>Windows Job Object<br/>Unix 进程组 + PR_SET_PDEATHSIG"]
N --> O["认证健康检查<br/>GET /version + Bearer<br/>错误令牌必须返回 401<br/>最多 20 次,50ms→1s 退避"]
ROOT --> O
O -->|"失败"| ERR2["终止子进程<br/>返回错误"]
O -->|"通过"| P["写入 CoreData<br/>通知网络协调器 / 刷新托盘图标"]
P --> Q["返回 CoreStartResult<br/>secret + port + active_config"]
style ERR1 fill:#ffcdd2,stroke:#c62828,color:#b71c1c
style ERR2 fill:#ffcdd2,stroke:#c62828,color:#b71c1c
style REUSE fill:#c8e6c9,stroke:#2e7d32,color:#1b5e20
style Q fill:#c8e6c9,stroke:#2e7d32,color:#1b5e20关键细节
| 环节 | 说明 |
|---|---|
| 内核二进制 | 托管二进制名为 zephyr-mihomo(Windows 为 zephyr-mihomo.exe),位于 {app_data_dir}/core/ |
| 运行配置 | 最终写入 {app_data_dir}/core/run_config.yaml;external-controller 被固定为 127.0.0.1:<port>,缺省时补 unified-delay: true |
| 全局偏好 | Settings::to_global_prefs() 把 mode、TUN、端口、IPv6、allow-lan 等用户偏好覆盖到订阅 YAML 上;Windows 在未提权时强制以 TUN 关闭启动 |
| 无订阅 | 无可用订阅时以内置最小配置启动(MATCH,DIRECT),前端进入「无订阅」状态 |
| macOS 回滚 | macOS 上通过 RunConfigRestoreGuard 在启动失败时恢复上一份 run_config.yaml |
| TUN 失败回退 | 前端在 TUN 启动失败时会记录 TUN 关闭(record_tun_enabled)并以非 TUN 方式重试 |
| 启动后 | 通知 NetworkChangeCoordinator 重新评估(Manual 事件),刷新托盘图标,并在后台校验订阅临时全局模式的恢复标记 |
全局原子变量
| 变量 | 类型 | 用途 |
|---|---|---|
TUN_MODE_ACTIVE |
AtomicBool |
标记 TUN 模式是否激活 |
CORE_STARTING |
AtomicBool |
防止内核并发启动(Drop 守卫释放;等待超过 10s 强制重置) |
TUN_TOGGLING |
AtomicBool |
防止主界面与托盘同时切换 TUN |
LOG_CORE_ENABLED |
AtomicBool |
是否把内核 stdout/stderr 持久化到磁盘 |
LOG_MAX_FILE_MB |
AtomicU32 |
单个日志文件大小上限(默认 50 MB) |
订阅下载流程
download_sub(core/subscription.rs)从远程 URL 下载订阅配置,经过安全校验和清洗后写入本地。纯校验逻辑(私有地址判断、名称校验、Base64 检测、文件名解析等)位于 crates/core/src/config/subscription.rs。
%%{init: {'themeVariables': {'fontSize': '10px', 'actorTextColor': '#ccc', 'signalColor': '#666', 'labelTextColor': '#999', 'labelBoxBkgColor': '#333', 'labelBoxBorderColor': '#555', 'lineColor': '#666', 'actorBkg': '#333', 'actorBorder': '#555', 'actorTextColor': '#ccc', 'signalTextColor': '#ccc', 'noteBkgColor': '#333', 'noteTextColor': '#ccc', 'noteBorderColor': '#555'}}}%%
sequenceDiagram
participant FE as 前端 (ui/settings/subscriptions.js)
participant TI as Tauri IPC
participant SUB as core/subscription.rs
participant NET as 网络 (直连 / Mihomo)
participant SAN as zephyr-core 清洗
participant FS as 文件系统
FE->>TI: invoke("download_sub", {name, url?, userAgent?, overwrite?})
TI->>SUB: download_sub()
Note over SUB: 限流:5 秒内只允许一次
SUB->>SUB: URL 为空时从 metadata.json 解析<br/>URL / 名称合法性校验
alt Tier 1:直连
SUB->>NET: DNS 预解析 + IP 校验 + DNS pinning
NET-->>SUB: 响应
else Tier 2:经 Mihomo mixed-port 代理
SUB->>NET: HTTP GET via 托管内核
NET-->>SUB: 响应
else Tier 3:临时切到 global 模式
SUB->>NET: 切换 GLOBAL 到可用节点后重试<br/>完成或中断时自动恢复原模式与选择
NET-->>SUB: 响应
end
SUB->>SUB: 拒绝空响应 / Base64 自动解码
SUB->>SAN: quote_short_id_values + serde_yaml 解析<br/>remove_dangerous_keys
SAN-->>SUB: 清洗后的 YAML
SUB->>SUB: 新订阅:Content-Disposition / 规则推断名称<br/>sanitize_config_file_name + 路径校验
SUB->>FS: 临时文件 → 原子替换<br/>(encrypt_configs 开启时加密写入)<br/>更新 metadata.json(URL 以 AES-256-GCM 加密)
FS-->>SUB: 完成
SUB-->>FE: DownloadSubResult三层下载策略
| 层级 | 通道 | 适用场景 |
|---|---|---|
| Tier 1 | 直连(DNS pinning + SSRF 校验) | 无代理环境、私有 / 局域网订阅(私有地址只走直连) |
| Tier 2 | 通过托管 Mihomo 的 mixed-port 代理 | 直连失败且内核在运行、有可用的托管代理端点 |
| Tier 3 | 临时把 Mihomo 切到 global 模式并选择可用节点 | 规则分流导致不可达(rule 模式下的 403、408/429 等瞬态错误);确定性 4xx 错误不会触发 |
系统代理 / 环境变量代理不作为回退通道:它们可被任意本地程序设置,属于不可信的 SSRF 攻击面。只信任由 Zephyr 自己管理的 Mihomo 代理。
download_sub_batch 供订阅调度器与「全部更新」使用,不受单次限流约束,并优先使用每个订阅在 metadata 中保存的 User-Agent。
SSRF 防护
- DNS 预解析后校验 IP 地址,拦截私有地址、环回地址、链路本地地址等;识别 Mihomo Fake-IP 段
- 重定向链校验,最多跟踪 5 跳
- 协议白名单,仅允许 HTTP/HTTPS
- 响应体大小上限 10 MB(
MAX_RESPONSE_SIZE)
配置热重载流程
update_config(顶层 config_manager.rs)将前端传入的配置补丁合并到运行配置中,并通过 Mihomo API 热重载,无需重启内核。合并与安全键保护逻辑位于 crates/core/src/config/merge.rs。
%%{init: {'themeVariables': {'fontSize': '10px', 'actorTextColor': '#ccc', 'signalColor': '#666', 'labelTextColor': '#999', 'labelBoxBkgColor': '#333', 'labelBoxBorderColor': '#555', 'lineColor': '#666', 'actorBkg': '#333', 'actorBorder': '#555', 'actorTextColor': '#ccc', 'signalTextColor': '#ccc', 'noteBkgColor': '#333', 'noteTextColor': '#ccc', 'noteBorderColor': '#555'}}}%%
sequenceDiagram
participant FE as 前端 (src/ui/)
participant TI as Tauri IPC
participant CFG as config_manager.rs
participant ZC as zephyr-core merge
participant FILE as run_config.yaml / 订阅文件
participant API as Mihomo API
FE->>TI: invoke("update_config", {patch})
TI->>CFG: update_config(patch)
CFG->>ZC: update_config_core(patch)
ZC->>FILE: 读取 run_config.yaml
ZC->>ZC: JSON → YAML
ZC->>ZC: remove_dangerous_keys<br/>提取安全键(external-controller / secret / tun.enable)
ZC->>ZC: merge_yaml(深度上限 50,null 表示删除键)
ZC->>ZC: 再次清洗 + 恢复安全键
ZC->>FILE: 写回 run_config.yaml
CFG->>FILE: 把同一补丁合并进当前订阅文件<br/>(保持其加密形态)
CFG->>API: PUT /configs?force=true {path}
API-->>CFG: 2xx / 错误
CFG-->>FE: ConfigUpdateResult { files_saved, hot_reload_success, message }merge_yaml 递归合并算法
pub fn merge_yaml(base: &mut YamlValue, patch: &YamlValue, depth: usize) -> Result<(), String> {
if depth > 50 {
return Err("YAML nesting depth exceeded limit".to_owned());
}
match (base, patch) {
(YamlValue::Mapping(a), YamlValue::Mapping(b)) => {
for (k, v) in b {
if v.is_null() {
a.remove(k); // null 值表示删除该键
} else if let Some(a_v) = a.get_mut(k) {
merge_yaml(a_v, v, depth + 1)?; // 递归合并
} else {
a.insert(k.clone(), v.clone()); // 新增键
}
}
}
(a, b) => {
*a = b.clone(); // 非映射类型:直接覆盖
}
}
Ok(())
}运行模式(
mode)不通过update_config/patch_settings修改,而是由专用的switch_mode命令先作用于运行中的内核再持久化;全局节点切换同理由switch_global_node负责。
系统代理设置流程
sys_proxy.rs 负责在各平台上配置和清除系统代理。所有实现都先调用 validate_proxy_server,仅允许 loopback 地址作为代理服务器。
%%{init: {'themeVariables': {'fontSize': '9px', 'nodeBorder': '1px', 'clusterBorder': '1px', 'edgeLabelHeight': '8px'}, 'flowchart': {'curve': 'basis', 'nodeSpacing': 12, 'rankSpacing': 15}}}%%
flowchart TD
A["enable_sysproxy(server, bypass?)"] --> B["validate_proxy_server<br/>仅允许 127.0.0.1 / localhost / ::1<br/>拒绝端口 0"]
B --> C{"地址合法?"}
C -->|"不合法"| ERR["返回错误"]
C -->|"合法"| D{"平台"}
D -->|"Windows"| E["Windows 分支"]
D -->|"macOS"| F["macOS 分支"]
D -->|"Linux"| G["Linux 分支"]
subgraph Win["Windows -- 注册表写入"]
E1["备份 HKCU Internet Settings<br/>ProxyEnable / ProxyServer / ProxyOverride"]
E2["写入新值"]
E3["InternetSetOptionW<br/>SETTINGS_CHANGED + REFRESH"]
E4{"成功?"}
E5["失败时回滚备份值"]
E1 --> E2 --> E3 --> E4
E4 -->|"失败"| E5
end
subgraph Mac["macOS -- networksetup 逐服务"]
F1["列出所有网络服务<br/>跳过已禁用服务"]
F2["Web / Secure Web / SOCKS 代理<br/>-setwebproxy / -setsecurewebproxy<br/>-setsocksfirewallproxy"]
F3["打开对应开关"]
F1 --> F2 --> F3
end
subgraph Lin["Linux -- 对所有可用桌面环境逐一尝试"]
G1["GNOME:gsettings<br/>org.gnome.system.proxy"]
G2["KDE:kwriteconfig6/5 写 kioslaverc<br/>dbus 通知 KIODaemon / KWin"]
G3["XFCE:xfconf-query"]
G4{"任一成功?"}
G1 --> G4
G2 --> G4
G3 --> G4
end
E --> Win
F --> Mac
G --> Lin
E4 -->|"成功"| OWN["写入所有权标记<br/>.sys-proxy-ownership"]
F3 --> OWN
G4 -->|"是"| OWN
G4 -->|"否"| ERR
OWN --> DONE["返回 Ok"]
style ERR fill:#ffcdd2,stroke:#c62828,color:#b71c1c
style DONE fill:#c8e6c9,stroke:#2e7d32,color:#1b5e20
style Win fill:#e3f2fd,stroke:#1565c0,color:#0d47a1
style Mac fill:#fff3e0,stroke:#e65100,color:#bf360c
style Lin fill:#e8f5e9,stroke:#2e7d32,color:#1b5e20所有权守卫
- 启用系统代理时写入
{app_data_dir}/.sys-proxy-ownership标记文件(记录代理地址)。 - 前端每 10 秒的统一同步(
ui/tray.js的startUnifiedSync)读取get_sys_proxy与has_sysproxy_ownership:若标记存在但系统代理被外部程序关闭,且内核可达,则调用restore_sys_proxy恢复。 - 启动时若发现上次崩溃遗留的标记,同样立即恢复。
- 正常退出时代理与标记一并清除(
clear_sys_proxy);Windows 管理员重启前会暂停代理并保留标记,交给新实例恢复。
安全验证逻辑
fn validate_proxy_server(server: &str) -> Result<(), String> {
if server.is_empty() { return Err("Proxy server address cannot be empty".to_owned()); }
if server.len() > 512 { return Err("Proxy server address too long".to_owned()); }
if server.contains('\n') || server.contains('\r') || server.contains('\0') {
return Err("Proxy server address contains invalid characters".to_owned());
}
let (host, port_str) = parse_host_port(server)?;
if port_str == "0" { return Err("Port 0 is not allowed ...".to_owned()); }
if let Ok(ip) = host.parse::<IpAddr>() {
if !ip.is_loopback() { return Err("Only loopback addresses ...".to_owned()); }
return Ok(());
}
if host.to_lowercase() == "localhost" { return Ok(()); }
Err(format!("Invalid proxy host '{host}': only localhost, 127.0.0.1, or ::1 are allowed"))
}前端初始化顺序
main.js 在模块加载时即开始注册 backend-event / prism-event 监听(3 秒超时),DOMContentLoaded 后调用 initApp() 编排整个初始化序列。
%%{init: {'themeVariables': {'fontSize': '9px', 'nodeBorder': '1px', 'clusterBorder': '1px', 'edgeLabelHeight': '8px'}, 'flowchart': {'curve': 'basis', 'nodeSpacing': 12, 'rankSpacing': 15}}}%%
flowchart TD
A0["模块加载<br/>initBackendEventListeners()<br/>注册全局 error / unhandledrejection 转发"] --> A["initApp()"]
A --> S0["Step 0: 等待后端事件监听就绪<br/>重置瞬态锁(isNetworkUpdating 等)"]
S0 --> B["Step 1: 禁用右键菜单<br/>(拖拽区除外)"]
B --> C["Step 2: applyTranslations()"]
C --> D["Step 3: initWindowControls()"]
D --> E["Step 4: 50ms 后显示窗口<br/>silent_start 时保持隐藏"]
E --> F["Step 5: startCoreAndConfigure()<br/>invoke(start_core) → 设置 API 基址与 secret<br/>失败时显示错误卡片,重试直到成功"]
F --> G["Step 6: 初始化 UI 模块<br/>initNavigation(各页面懒加载回调)<br/>initNavTooltips / initNoSubscriptionState / initChart<br/>initTrayEventListeners / initProxyToggle / initDnsRewriteToggle<br/>initModeSelector / initTunToggle / initProxyControls / initPlugins<br/>initSettings / initUwpExemption / autoApplyIfNeeded<br/>initNodeWheel / initHomeStats / initHeaderFit / initTablistKeys<br/>initShortcutSettings → initReactiveBindings"]
G --> H["Step 7: Deep Link / 全局快捷键监听<br/>浏览器 online/offline → notify_network_change<br/>监听 network-state-changed"]
H --> I["Step 8: is_machine_key_persisted<br/>未持久化则提示"]
I --> J["Step 9: syncCoreConfig + updateTrayStatus + updateTrayMenu<br/>startUnifiedSync()(10s 周期)"]
J --> K["Step 10: 控制台首页激活(若启用)<br/>scheduleAutoUpdateCheck()"]
K --> L["Step 11: connectTraffic()<br/>/traffic 流 → 图表 + Bus TRAFFIC_UPDATE"]
L --> M["Step 12: 监听 core-restarted-after-resume<br/>HOME_PAGE_MODE_CHANGED 等 Bus 事件<br/>注册 cleanup"]
M --> N["Step 13: 心跳 heartbeat<br/>每 15s 一次 + 窗口获得焦点时"]
style F fill:#fff3e0,stroke:#e65100,color:#bf360c
style N fill:#c8e6c9,stroke:#2e7d32,color:#1b5e20页面与懒加载
主窗口共 9 个页面(data-page):home、console、proxies、subscriptions、rule-library、connections、logs、advanced、settings。首页有两种布局:极简首页(home)和控制台首页(console),由 home_page_mode 设置切换。
| 页面 | 加载方式 | 离开页面时 |
|---|---|---|
高级设置 advanced.js |
动态 import() |
— |
规则库 rule-library.js |
动态 import() |
— |
日志 logs.js |
动态 import() |
destroyLogsPage() 停止轮询 |
连接 connections.js |
静态导入,进入页面时 initConnectionsPage() |
停止轮询 |
代理 proxies.js |
静态导入 | 停止 observed-group 监视与 Provider 轮询 |
控制台 console-home.js |
静态导入,activateConsole() |
deactivateConsole() |
加密体系
Zephyr 使用机器指纹派生密钥 + AES-256-GCM 保护订阅元数据(profiles/metadata.json 中的订阅 URL),并可选地加密订阅配置文件本身。
| 加密对象 | 默认 | 说明 |
|---|---|---|
| 订阅 URL(metadata.json) | 始终加密 | obfuscate_string(),前端只拿到脱敏后的 url_display |
订阅配置文件(.yaml) |
关闭 | 设置项 encrypt_configs 开启后整文件加密,文件无法在其他机器使用;切换时批量加密 / 解密现有文件 |
| 文件权限 | 始终 | secure_io.rs:Unix 0o600,Windows DACL;关键文件经临时文件 + rename 原子写入 |
开启
encrypt_configs要求机器密钥已持久化,否则会被拒绝(会话级密钥在重启后丢失,加密内容将无法恢复)。
%%{init: {'themeVariables': {'fontSize': '9px', 'nodeBorder': '1px', 'clusterBorder': '1px', 'edgeLabelHeight': '8px'}, 'flowchart': {'curve': 'basis', 'nodeSpacing': 12, 'rankSpacing': 15}}}%%
flowchart TD
subgraph Fingerprint["机器指纹采集"]
FP1["Windows: MachineGuid + C 盘卷序列号"]
FP2["macOS: IOPlatformUUID + IOPlatformSerialNumber"]
FP3["Linux: /etc/machine-id + board_serial"]
end
FP1 --> COMBINE{"采集到指纹?"}
FP2 --> COMBINE
FP3 --> COMBINE
COMBINE -->|"是"| JOIN["以 | 拼接"]
JOIN --> PBKDF2["PBKDF2-HMAC-SHA256<br/>盐值: Zephyr_AES256_Key_Derivation<br/>迭代: 100,000 次<br/>输出: 32 字节"]
COMBINE -->|"否"| FILE{"已有 .machine_key 文件?"}
FILE -->|"是"| READ["读取 hex 编码的随机密钥"]
FILE -->|"否"| GEN["生成 32 字节随机密钥<br/>hex 写入 .machine_key"]
GEN -->|"写入失败"| SESSION["会话级密钥<br/>is_machine_key_persisted = false"]
PBKDF2 --> KEY["32 字节 AES 密钥<br/>进程内缓存(OnceLock)"]
READ --> KEY
GEN --> KEY
SESSION --> KEY
KEY --> AES["AES-256-GCM<br/>12 字节随机 Nonce"]
AES --> OUT["Base64( v2: + Nonce + Ciphertext + Tag )"]
style Fingerprint fill:#e3f2fd,stroke:#1565c0,color:#0d47a1
style PBKDF2 fill:#fff3e0,stroke:#e65100,color:#bf360c
style AES fill:#e8f5e9,stroke:#2e7d32,color:#1b5e20
style SESSION fill:#ffcdd2,stroke:#c62828,color:#b71c1c.machine_key 回退文件位于平台配置目录下的 Zephyr/(Windows %APPDATA%,macOS ~/Library/Application Support,Linux $XDG_CONFIG_HOME 或 ~/.config)。
加密数据格式
Base64( "v2:" || Nonce || Ciphertext || Tag )
+------+----------+--------------+----------+
| v2: | Nonce | Ciphertext | Tag |
| 3 B | 12 bytes | N bytes | 16 bytes |
+------+----------+--------------+----------+v2: 前缀位于 Base64 编码之内,因此加密文件内容以 djI6(v2: 的 Base64)开头,is_encrypted_content() 据此判断文件是否已加密。
obfuscate_string 实现
pub(super) fn obfuscate_string(s: &str) -> Result<String, String> {
let key_bytes = get_machine_key();
if key_bytes.len() != 32 {
return Err(format!("Invalid key length {}, expected 32", key_bytes.len()));
}
let cipher = Aes256Gcm::new_from_slice(&key_bytes)
.map_err(|e| format!("Failed to initialize AES cipher: {e}"))?;
let nonce_bytes: [u8; 12] = rand::rng().random();
let nonce: Nonce<_> = nonce_bytes.into();
let ciphertext = cipher
.encrypt(&nonce, s.as_bytes())
.map_err(|e| format!("AES encryption failed: {e}"))?;
let mut result = b"v2:".to_vec();
result.extend(&nonce_bytes);
result.extend(ciphertext); // ciphertext 已附带 16 字节 GCM Tag
Ok(base64_standard.encode(&result))
}Tauri Commands 分类表
lib.rs 的 tauri::generate_handler! 共注册 180 个 Tauri Commands:核心与系统功能 85 个,Prism Engine 95 个。下表按功能分组;「用户参数」列为前端需传入的参数(State、AppHandle、Window 由 Tauri 注入,不列出;前端以 camelCase 传参)。
| 分组 | 数量 |
|---|---|
| 1. 核心进程与运行模式 | 6 |
| 2. 订阅与配置文件 | 14 |
| 3. 运行配置热重载 | 2 |
| 4. 设置 | 9 |
| 5. 系统代理 | 5 |
| 6. TUN 与权限 | 11 |
| 7. 网络优化 | 3 |
| 8. 网络切换协调 | 2 |
| 9. 系统托盘 | 9 |
| 10. 更新 | 8 |
| 11. 日志 | 4 |
| 12. 备份与恢复 | 2 |
| 13. 系统集成 | 6 |
| 14. 应用信息与 WebView 健康 | 4 |
| 15. Prism 核心 | 20 |
| 16. Prism 规则库 | 18 |
| 17. Prism 插件 | 11 |
| 18. Prism 脚本沙箱 | 11 |
| 19. 覆写系统 | 13 |
| 20. Prism 智能选择 | 11 |
| 21. Prism Failover | 5 |
| 22. Prism KV 存储 | 4 |
| 23. Prism Trace | 2 |
| 合计 | 180 |
1. 核心进程与运行模式
| 命令 | 用户参数 | 返回类型 | 说明 |
|---|---|---|---|
start_core |
config_path, custom_args, secret?, force? |
Result<CoreStartResult, String> |
启动 Mihomo 内核 |
stop_core |
-- | Result<String, String> |
停止 Mihomo 内核 |
get_core_version |
-- | Result<String, String> |
获取内核版本(mihomo -v) |
get_core_uptime |
-- | Result<Option<u64>, String> |
内核已运行时长 |
switch_mode |
mode, clear_preference? |
Result<String, String> |
切换 rule/global/direct:先作用于运行中的内核再持久化(mode 的唯一写入入口) |
switch_global_node |
name |
Result<(), String> |
切换 GLOBAL 组节点 |
2. 订阅与配置文件
| 命令 | 用户参数 | 返回类型 | 说明 |
|---|---|---|---|
list_configs |
-- | Result<Vec<ConfigInfo>, String> |
列出所有订阅配置 |
download_sub |
name, url?, user_agent?, overwrite? |
Result<DownloadSubResult, String> |
下载 / 更新订阅(5 秒限流;url 为空时从 metadata 解析) |
download_sub_batch |
items, user_agent? |
Result<Vec<BatchUpdateResult>, String> |
批量更新订阅(不受单次限流) |
delete_config |
name |
Result<String, String> |
删除配置 |
rename_config |
old_name, new_name |
Result<String, String> |
重命名配置(同步迁移节点选择记忆) |
update_config_url |
name, new_url |
Result<(), String> |
更新订阅 URL |
update_subscription_interval |
name, interval |
Result<(), String> |
更新自动更新间隔(0 = 禁用) |
update_subscription_ua |
name, user_agent |
Result<(), String> |
设置单个订阅的 User-Agent |
read_config_file |
config_path |
Result<String, String> |
读取配置文件(自动解密) |
write_config_file |
config_path, content |
Result<String, String> |
写入配置文件 |
open_config_folder |
-- | Result<String, String> |
打开配置文件夹 |
fetch_text |
url |
Result<String, String> |
通用文本下载(同样经过 SSRF 校验) |
get_scheduler_status |
-- | serde_json::Value |
订阅自动更新调度器状态 |
trigger_auto_update |
-- | Result<usize, String> |
立即更新全部订阅,返回成功数量 |
3. 运行配置热重载
| 命令 | 用户参数 | 返回类型 | 说明 |
|---|---|---|---|
read_config |
-- | Result<Value, String> |
读取 run_config.yaml(移除 secret) |
update_config |
patch |
Result<ConfigUpdateResult, String> |
合并补丁并热重载 |
4. 设置
| 命令 | 用户参数 | 返回类型 | 说明 |
|---|---|---|---|
get_settings |
-- | Settings |
获取应用设置 |
save_settings |
settings |
Result<(), String> |
整体保存(mode、tun_enabled、encrypt_configs 及后端专属字段保留后端当前值) |
patch_settings |
patch |
Result<(), String> |
原子部分更新;非 null 的 mode 会被拒绝;encrypt_configs 切换时批量加解密订阅 |
update_proxy_selection |
profile_name, node_name, group_name? |
Result<(), String> |
原子更新每个订阅的节点选择记忆 |
update_primary_group_preference |
profile_name, group_name |
Result<(), String> |
记录每个订阅的主策略组偏好 |
update_subscription_user_agent |
user_agent |
Result<(), String> |
全局订阅 User-Agent |
update_last_config |
config_name |
Result<(), String> |
原子更新 last_config |
set_ui_scale |
scale |
Result<f64, String> |
设置界面缩放 |
is_machine_key_persisted |
-- | bool |
机器密钥是否已持久化 |
5. 系统代理
| 命令 | 用户参数 | 返回类型 | 说明 |
|---|---|---|---|
enable_sysproxy |
server, bypass? |
Result<String, String> |
启用系统代理 |
disable_sysproxy |
-- | Result<String, String> |
禁用系统代理 |
get_sys_proxy |
-- | Result<bool, String> |
当前系统代理状态 |
has_sysproxy_ownership |
-- | bool |
所有权标记是否存在 |
restore_sys_proxy |
server? |
Result<(), String> |
按所有权标记恢复代理 |
6. TUN 与权限
| 命令 | 用户参数 | 返回类型 | 说明 |
|---|---|---|---|
set_tun_enabled |
enable |
Result<(), String> |
启用 / 禁用 TUN |
record_tun_enabled |
enable, already_applied |
Result<(), String> |
只记录 TUN 偏好(供已自行完成切换的流程使用) |
release_tun_toggle |
-- | () |
释放 TUN 切换锁 |
disable_tun_cmd |
-- | 平台相关 | 强制关闭 TUN |
restart_core_as_root_cmd |
enable_tun |
Result<String, String> |
macOS:以 root 重启内核 |
kill_all_mihomo_as_root_cmd |
-- | Result<(), String> |
macOS:提权清理本应用托管的内核 |
grant_linux_tun_permission |
-- | Result<(), String> |
Linux:pkexec 一次授权,setcap 授予 CAP_NET_ADMIN 并安装 polkit 规则 |
is_process_elevated |
-- | bool |
Windows:当前进程是否已提权(其他平台恒为 false) |
can_elevate_as_self |
-- | bool |
Windows:是否为可自我提权的管理员账户 |
restart_as_admin_cmd |
enable_tun |
Result<(), String> |
Windows:通过 UAC 以管理员身份重启整个应用 |
apply_windows_tcp_optimizations |
-- | Result<(), String> |
Windows TCP 参数优化 |
7. 网络优化
| 命令 | 用户参数 | 返回类型 | 说明 |
|---|---|---|---|
apply_network_optimizations |
-- | Result<(), String> |
应用 TCP 参数优化(sysctl / netsh / osascript) |
revert_network_optimizations |
-- | Result<(), String> |
撤销优化 |
check_network_optimizations_status |
-- | Result<NetworkOptimStatus, String> |
查询优化状态 |
8. 网络切换协调
| 命令 | 用户参数 | 返回类型 | 说明 |
|---|---|---|---|
get_network_state |
-- | Result<NetworkState, String> |
当前网络状态(SSID 脱敏) |
notify_network_change |
source |
Result<(), String> |
前端上报 online/offline 等网络变化 |
9. 系统托盘
| 命令 | 用户参数 | 返回类型 | 说明 |
|---|---|---|---|
show_main_window |
-- | () |
显示主窗口(WebView 已失效时重建) |
change_tray_icon |
mode |
Result<(), String> |
更改托盘图标(优先以后端实时状态为准) |
get_tray_status |
-- | Result<String, String> |
托盘状态 |
get_tray_icon_mode |
-- | String |
托盘当前实际显示的图标模式(default / sysproxy / tun) |
update_tray_full_menu |
params |
Result<(), String> |
重建完整托盘菜单(订阅、节点、i18n 文本) |
get_tray_menu_state |
-- | Result<TrayMenuState, String> |
获取托盘菜单状态 |
set_tray_menu_state |
new_state |
Result<(), String> |
设置托盘菜单状态 |
get_tray_proxy_status |
-- | Result<String, String> |
托盘代理状态 |
update_tray_toggle_states |
sys_proxy_enabled, tun_enabled, current_mode |
Result<(), String> |
同步托盘开关状态 |
10. 更新
| 命令 | 用户参数 | 返回类型 | 说明 |
|---|---|---|---|
get_latest_version |
-- | Result<UpdateInfo, String> |
最新 Mihomo 内核版本 |
update_core |
url |
Result<CoreStartResult, String> |
下载并替换内核,随后重启 |
update_geo_data |
-- | Result<String, String> |
更新 GeoIP / GeoSite 数据 |
get_latest_client_versions |
-- | Result<ClientVersions, String> |
各主流客户端最新版本(伪装 UA 用) |
get_latest_client_version |
-- | Result<ClientUpdateInfo, String> |
Zephyr 最新版本 |
update_client |
-- | Result<String, String> |
下载 Zephyr 安装包,SHA-256 + Minisign 校验后打开 |
end_update_hold |
-- | () |
页面结束更新保持(轻量模式下允许销毁 WebView) |
set_page_busy |
busy |
() |
页面正在执行网络操作,关闭窗口时暂不销毁 WebView |
11. 日志
| 命令 | 用户参数 | 返回类型 | 说明 |
|---|---|---|---|
read_core_log |
offset?, limit? |
Result<ReadLogResult, String> |
增量读取内核日志 |
open_log_folder |
-- | Result<String, String> |
打开日志目录 |
export_logs |
log_type, from_date, to_date, level? |
Result<String, String> |
按日期 / 级别导出日志 |
write_frontend_log |
level, source, message |
Result<(), String> |
前端日志写入应用日志 |
12. 备份与恢复
| 命令 | 用户参数 | 返回类型 | 说明 |
|---|---|---|---|
export_backup |
-- | Result<String, String> |
导出 ZIP 备份(含 manifest 与校验和) |
import_backup |
-- | Result<String, String> |
事务性导入(校验 → 暂存 → 提交,失败回滚) |
13. 系统集成
| 命令 | 用户参数 | 返回类型 | 说明 |
|---|---|---|---|
take_pending_deep_links |
-- | Vec<DeepLinkPayload> |
取出排队中的 clash:// 链接(每次最多 1 条) |
rate_limited_register_shortcut |
action, accelerator |
Result<(), String> |
注册全局快捷键(500ms 冷却) |
rate_limited_unregister_shortcut |
action |
Result<(), String> |
注销全局快捷键(500ms 冷却) |
rate_limited_send_notification |
title, body |
Result<(), String> |
原生系统通知(1s 冷却) |
exempt_uwp_apps |
-- | Result<String, String> |
Windows UWP 环回免除(5 分钟冷却) |
get_process_icon |
path, fresh? |
Option<String> |
连接对应进程的真实应用图标(data: URL) |
14. 应用信息与 WebView 健康
| 命令 | 用户参数 | 返回类型 | 说明 |
|---|---|---|---|
get_app_version |
-- | String |
当前版本号 |
get_portable_mode |
-- | bool |
是否便携模式 |
get_third_party_licenses |
-- | &'static str |
编译期嵌入的第三方许可证全文 |
heartbeat |
-- | () |
前端心跳(WebView 存活检测) |
15. Prism 核心
| 命令 | 用户参数 | 返回类型 | 说明 |
|---|---|---|---|
prism_apply |
options |
Result<Value, String> |
应用 Prism 规则并热重载 |
prism_rebuild |
options |
Result<Value, String> |
重新构建 |
prism_status |
-- | Result<Value, String> |
Prism Engine 状态 |
prism_list_rules |
-- | Result<Value, String> |
列出规则 |
prism_preview_rules |
patch_id |
Result<Value, String> |
预览补丁规则 |
prism_get_last_trace |
-- | Result<Value, String> |
上次 apply 的 trace |
prism_is_prism_rule |
index |
Result<Value, String> |
判断规则是否由 Prism 生成 |
prism_insert_rule |
rule, position, group_id? |
Result<usize, String> |
插入规则 |
prism_insert_rule_str |
rule_text, position, group_id? |
Result<usize, String> |
以文本插入规则 |
prism_toggle_group |
group_id, enabled |
Result<bool, String> |
启用 / 禁用规则组 |
prism_trace_report |
patch_id |
Result<Value, String> |
trace 报告 |
prism_trace_report_text |
-- | Result<String, String> |
文本格式 trace 报告 |
prism_validate_config |
config |
Result<bool, String> |
校验 Prism 配置 |
prism_list_profiles |
-- | Result<Value, String> |
列出可用订阅 |
prism_get_core_info |
-- | Result<Value, String> |
内核信息 |
prism_start_watching |
debounce_ms? |
Result<(), String> |
开始监视规则文件 |
prism_stop_watching |
-- | Result<(), String> |
停止监视 |
prism_is_watching |
-- | Result<bool, String> |
是否在监视 |
prism_get_stats |
-- | Result<Value, String> |
统计信息 |
prism_read_raw_profile |
profile_id |
Result<String, String> |
读取原始订阅内容 |
16. Prism 规则库
| 命令 | 用户参数 | 返回类型 | 说明 |
|---|---|---|---|
open_prism_folder |
-- | Result<String, String> |
打开 Prism 目录 |
rule_list |
-- | Result<Vec<RuleFileInfo>, String> |
列出规则文件 |
rule_read |
filename |
Result<String, String> |
读取规则文件 |
rule_create |
name, content |
Result<String, String> |
创建规则文件(统一 YAML 清洗) |
rule_update |
filename, content |
Result<(), String> |
更新规则文件(统一 YAML 清洗) |
rule_delete |
filename |
Result<(), String> |
删除规则文件 |
rule_rename |
old_filename, new_filename |
Result<String, String> |
重命名规则文件 |
rule_extract_from_profile |
profile_id, name |
Result<String, String> |
从订阅提取规则 |
rule_import_text |
text, name |
Result<String, String> |
从文本导入 |
rule_import_file |
file_path, name |
Result<String, String> |
从文件导入 |
rule_import_url |
url, name |
Result<String, String> |
从 URL 导入(5 次 / 10s) |
rule_group_list |
-- | Result<RuleGroups, String> |
列出规则分组 |
rule_group_create |
name |
Result<(), String> |
创建分组 |
rule_group_rename |
old_name, new_name |
Result<(), String> |
重命名分组 |
rule_group_delete |
name |
Result<(), String> |
删除分组 |
rule_group_move |
filename, target_group |
Result<(), String> |
移动规则文件到分组 |
rule_get_auto_apply |
-- | Result<bool, String> |
读取自动应用开关 |
rule_set_auto_apply |
enabled |
Result<(), String> |
设置自动应用开关 |
17. Prism 插件
| 命令 | 用户参数 | 返回类型 | 说明 |
|---|---|---|---|
plugin_discover |
-- | Result<Vec<Value>, String> |
扫描可用插件 |
plugin_load |
plugin_id |
Result<Value, String> |
加载插件 |
plugin_unload |
plugin_id |
Result<(), String> |
卸载插件 |
plugin_enable |
plugin_id, enable |
Result<(), String> |
启用 / 禁用 |
plugin_delete |
plugin_id |
Result<(), String> |
删除插件 |
plugin_list_loaded |
-- | Result<Vec<Value>, String> |
已加载插件 |
plugin_execute |
plugin_id |
Result<Value, String> |
执行插件 |
plugin_execute_hook |
hook_name, config, hook_data? |
Result<Vec<Value>, String> |
执行钩子 |
plugin_list_hooks |
-- | Vec<Value> |
列出钩子 |
plugin_check_permission |
permissions, action |
Result<bool, String> |
权限检查 |
plugin_list_permissions |
-- | Vec<Value> |
列出权限类型 |
18. Prism 脚本沙箱
| 命令 | 用户参数 | 返回类型 | 说明 |
|---|---|---|---|
script_execute |
script, script_name |
Result<Value, String> |
沙箱内执行脚本(10 次 / 10s) |
script_execute_write |
script, script_name |
Result<Value, String> |
执行并写回配置、热重载(5 次 / 10s) |
script_validate |
script |
Result<bool, String> |
校验脚本 |
script_get_sandbox |
-- | Result<Value, String> |
沙箱配置 |
script_set_sandbox |
allow_network, allow_filesystem, allow_child_process, allow_workers |
Result<(), String> |
设置沙箱配置 |
script_get_limits |
-- | Result<Value, String> |
资源限制 |
script_set_limits |
max_execution_time_ms?, max_memory_bytes?, max_loop_iterations?, max_recursion_depth? |
Result<(), String> |
设置资源限制 |
script_grant_plugin |
plugin_id, allow_network, allow_filesystem |
Result<(), String> |
授予插件权限 |
script_revoke_plugin |
plugin_id |
Result<(), String> |
撤销插件权限 |
script_check_plugin_permission |
plugin_id, permission |
Result<bool, String> |
检查插件权限 |
script_is_sandbox_safe |
-- | Result<bool, String> |
沙箱是否处于安全配置 |
19. 覆写系统
| 命令 | 用户参数 | 返回类型 | 说明 |
|---|---|---|---|
override_list |
-- | Result<Vec<OverrideItem>, String> |
列出所有覆写 |
override_create |
name, ext, type, url? |
Result<OverrideItem, String> |
创建覆写(JS 或 Prism YAML,本地或远程) |
override_update |
id, name?, enabled?, global?, profile_ids?, url? |
Result<OverrideItem, String> |
更新覆写元数据与作用范围 |
override_delete |
id |
Result<(), String> |
删除覆写 |
override_get_content |
id |
Result<String, String> |
获取覆写内容 |
override_set_content |
id, content, dry_run?, save_only? |
Result<OverrideLog, String> |
保存内容(Prism YAML 先清除危险键) |
override_reorder |
ids |
Result<(), String> |
调整顺序 |
override_toggle |
id, enabled |
Result<(), String> |
启用 / 禁用 |
override_test |
id |
Result<OverrideLog, String> |
测试单个覆写 |
override_refresh_remote |
id |
Result<OverrideLog, String> |
刷新远程覆写 |
override_apply_all |
-- | Result<Vec<OverrideLog>, String> |
应用所有启用的覆写 |
override_export |
-- | Result<String, String> |
导出为 JSON |
override_import |
json |
Result<u32, String> |
从 JSON 导入,返回导入数量 |
20. Prism 智能选择
| 命令 | 用户参数 | 返回类型 | 说明 |
|---|---|---|---|
smart_score |
node_name, latency_ms, success |
Result<f64, String> |
记录一次测试并返回评分 |
smart_score_at |
node_name, timestamp_ms |
Result<f64, String> |
指定时间点的评分 |
smart_rank |
-- | Result<Vec<Value>, String> |
节点排名 |
smart_select_best |
-- | Result<Option<Value>, String> |
选出最佳节点 |
smart_config |
-- | Result<Value, String> |
读取配置 |
smart_config_save |
config_json |
Result<(), String> |
保存配置 |
smart_validate_config |
-- | Result<Vec<String>, String> |
校验配置 |
smart_next_interval |
network_quality, min_interval?, max_interval? |
Result<u64, String> |
计算下次测试间隔 |
smart_scheduler_config |
-- | Result<Value, String> |
调度配置 |
smart_clear_history |
-- | Result<(), String> |
清除历史 |
smart_trim_history |
max_records |
Result<(), String> |
裁剪历史 |
21–23. Prism Failover / KV / Trace
| 命令 | 用户参数 | 返回类型 | 说明 |
|---|---|---|---|
failover_report |
node_name, success |
Result<Option<Value>, String> |
上报节点成败,必要时返回切换建议 |
failover_get_policy |
-- | Result<Value, String> |
读取策略 |
failover_set_policy |
enabled, threshold, cooldown_secs, fallback_group? |
Result<(), String> |
设置策略 |
failover_failure_count |
node_name |
Result<u32, String> |
节点连续失败次数 |
failover_reset |
-- | Result<(), String> |
重置计数 |
kv_get |
key |
Result<Option<Value>, String> |
读取 KV |
kv_set |
key, value |
Result<(), String> |
写入 KV |
kv_delete |
key |
Result<bool, String> |
删除 KV |
kv_keys |
-- | Result<Vec<String>, String> |
列出所有 key |
trace_statistics |
-- | Result<Value, String> |
trace 统计 |
trace_filter_by_source |
-- | Result<Value, String> |
按来源过滤 trace |
命令限流矩阵
| 命令 | 限制 | 实现 | 防护目标 |
|---|---|---|---|
download_sub |
5 秒 1 次 | RateLimiter(lib.rs) |
防止订阅下载过于频繁 |
rate_limited_register_shortcut |
500ms 1 次 | RateLimiter |
防止快捷键注册过于频繁 |
rate_limited_unregister_shortcut |
500ms 1 次 | RateLimiter |
防止快捷键注销过于频繁 |
rate_limited_send_notification |
1 秒 1 次 | RateLimiter |
防止通知轰炸 |
exempt_uwp_apps |
5 分钟 1 次 | 模块内原子时间戳 | UWP 免除涉及系统级修改 |
script_execute |
10 次 / 10 秒 | Prism 滑动窗口限流器 | 防止脚本执行 DoS |
script_execute_write |
5 次 / 10 秒 | Prism 滑动窗口限流器 | 防止频繁写回与热重载 |
rule_import_url |
5 次 / 10 秒 | Prism 滑动窗口限流器 | 防止远程导入滥用 |
RateLimiter 每次检查时顺带清理 60 秒前的记录,避免无限增长。
事件系统
后端通过 backend_event::emit_to_main() 向主窗口推送 15 个事件;前端用 api.js 中基于 plugin:event|listen 的 listen() 订阅。前端内部模块之间另有一套 ui/events.js 的 Bus 事件总线。
后端到前端事件一览
%%{init: {'themeVariables': {'fontSize': '10px', 'nodeBorder': '1px', 'clusterBorder': '1px', 'edgeLabelHeight': '10px'}, 'flowchart': {'curve': 'basis', 'nodeSpacing': 12, 'rankSpacing': 25}}}%%
graph LR
subgraph Backend["Rust 后端"]
E1["backend-event / prism-event"]
E2["profiles-imported"]
E3["tray-sysproxy-changed / tray-tun-changed<br/>tray-mode-changed / tray-subscription-changed<br/>tray-proxy-changed"]
E4["tray-copy-env-done / tray-copy-env-failed"]
E5["core-download-status"]
E6["core-restarted-after-resume"]
E7["deep-link"]
E8["global-shortcut"]
E9["network-state-changed"]
end
subgraph Frontend["前端监听模块"]
H1["modules/backend-events.js<br/>日志缓冲 + 错误 Toast"]
H2["ui/settings.js<br/>刷新订阅列表"]
H3["ui/tray.js<br/>同步开关 / 模式 / 订阅 / 节点"]
H4["ui/tray.js<br/>复制结果提示"]
H5["ui/settings.js + ui/update-dialog.js<br/>更新进度"]
H6["main.js<br/>切换 API 基址与 secret 并恢复状态"]
H7["ui/deep-link.js<br/>take_pending_deep_links"]
H8["ui/global-shortcut.js<br/>执行快捷键动作"]
H9["main.js<br/>记录网络变化日志"]
end
E1 --> H1
E2 --> H2
E3 --> H3
E4 --> H4
E5 --> H5
E6 --> H6
E7 --> H7
E8 --> H8
E9 --> H9
style Backend fill:#e8f5e9,stroke:#2e7d32,color:#1b5e20
style Frontend fill:#e3f2fd,stroke:#1565c0,color:#0d47a1事件详细说明
| # | 事件名 | 触发场景 | Payload | 前端监听者 |
|---|---|---|---|---|
| 1 | backend-event |
后端 emit_error!/emit_warn!/emit_info! 结构化日志(路径已脱敏) |
{ level, module, code, message, timestamp } |
modules/backend-events.js |
| 2 | prism-event |
Prism Engine 通知 | PrismEvent |
modules/backend-events.js |
| 3 | profiles-imported |
拖拽导入 YAML 完成 | number(导入数量) |
ui/settings.js |
| 4 | tray-sysproxy-changed |
托盘切换系统代理 | bool |
ui/tray.js |
| 5 | tray-tun-changed |
托盘切换 TUN | bool |
ui/tray.js |
| 6 | tray-mode-changed |
托盘切换运行模式 | String(rule/global/direct) |
ui/tray.js |
| 7 | tray-subscription-changed |
托盘切换订阅 | String(配置名) |
ui/tray.js |
| 8 | tray-proxy-changed |
托盘切换节点 | { group, proxy } |
ui/tray.js |
| 9 | tray-copy-env-done |
托盘复制代理环境变量成功 | () |
ui/tray.js |
| 10 | tray-copy-env-failed |
复制失败 | String(错误信息) |
ui/tray.js |
| 11 | core-download-status |
内核 / Geo 数据 / 客户端更新进度 | { component?, status_text, progress },component 为 core / geo_data / client |
ui/settings.js、ui/update-dialog.js |
| 12 | core-restarted-after-resume |
系统唤醒后后端检测内核失联并重启 | { port, secret, active_config } |
main.js |
| 13 | deep-link |
收到 clash:// 链接(已入队) |
(),前端随后调用 take_pending_deep_links |
ui/deep-link.js |
| 14 | global-shortcut |
全局快捷键按下 | { action } |
ui/global-shortcut.js |
| 15 | network-state-changed |
NetworkChangeCoordinator 检测到网络变化 | { reason, old_state, new_state }(SSID 脱敏) |
main.js |
Deep Link 队列
clash://install-config?url=...&name=... 链接(来自命令行参数或第二实例转发)先进入后端队列(最多 8 条,超出丢弃最旧的),再发出无负载的 deep-link 事件。前端在监听注册完成时、每次收到事件时以及处理完一条链接后调用 take_pending_deep_links,每次只取最旧的一条——这样 WebView 尚未就绪时到达的链接也不会丢失。
事件监听示例
import { listen } from './api.js';
// 托盘切换节点
const unlisten = await listen('tray-proxy-changed', (event) => {
const { group, proxy } = event.payload;
updateActiveProxy(group, proxy);
});
// 更新进度(内核 / Geo 数据 / 客户端共用)
await listen('core-download-status', (event) => {
const { component, status_text, progress } = event.payload;
renderProgress(component, status_text, progress);
});后端结构化日志
backend_event.rs 定义 BackendEvent { level, module, code, message, timestamp }:
- 级别:Fatal / Error / Warn / Info
- 模块(11 个):Core、Subscription、Prism、Config、Plugin、System、Updater、Override、Rule、Smart、Frontend
- 错误码:
codes模块中共 97 个,按模块划分号段(Core 1000–1999、Subscription 2000–2999 … Frontend 11000–11999) - 消息中的应用数据路径在发出前被脱敏;开启
log_app_enabled后同时写入logs/app/每日轮转文件
数据结构定义
CoreData -- Mihomo 进程运行时状态
/// 由 MihomoState(Mutex<CoreData>) 托管;字段私有,通过 getter/setter 访问
pub struct CoreData {
process: Option<Child>, // 子进程句柄(macOS TUN 根进程时为 None)
last_secret: String, // API Secret(32 位字母数字)
last_config_path: Option<String>, // 当前订阅文件名
last_custom_args: Option<Vec<String>>, // 自定义启动参数
last_port: Option<u16>, // API 端口
last_proxy_endpoint: Option<ProxyEndpoint>, // 代理监听端点(端口 + 协议)
last_log_path: Option<String>, // 本次运行的内核日志
started_at: Option<Instant>, // 启动时间(用于 uptime / 判断重启)
}ProxyEndpoint -- 代理端点
pub enum ProxyScheme { Http, Socks5h } // mixed-port / port 为 Http,仅 socks-port 时为 Socks5h
pub struct ProxyEndpoint {
pub port: u16,
pub scheme: ProxyScheme,
}
// host_port() → "127.0.0.1:7890";proxy_url() → "http://127.0.0.1:7890" 或 "socks5h://..."系统代理只接受能处理 HTTP 的端点(serves_http()),复制环境变量时按实际协议生成 URL。
AppPaths -- 应用路径集合
pub struct AppPaths {
pub app_data_dir: PathBuf, // 应用数据目录(便携模式下位于程序目录)
pub core_dir: PathBuf, // {app_data_dir}/core:内核、run_config.yaml
pub profiles_dir: PathBuf, // {app_data_dir}/profiles:订阅文件、metadata.json
}ConfigInfo -- 订阅元信息
pub struct ConfigInfo {
pub name: String,
#[serde(skip_serializing)]
pub url: Option<String>, // 完整 URL,绝不发送给前端
pub url_display: Option<String>, // 脱敏后的 URL,供界面展示
pub sub_info: Option<String>, // subscription-userinfo 流量信息
pub last_updated: Option<u64>,
pub auto_update_interval: Option<u64>,
pub user_agent: Option<String>, // 单订阅 User-Agent
}CoreStartResult / ReadLogResult
pub struct CoreStartResult {
pub secret: String,
pub port: u16,
pub active_config: Option<String>, // 实际加载的订阅(None 表示最小配置)
}
pub struct ReadLogResult {
pub lines: Vec<String>,
pub next_offset: u64,
pub file_size: u64,
pub has_more: bool,
pub rotated: bool,
}Settings -- 持久化设置
Settings(lib.rs)序列化为 {app_data_dir}/settings.json,共 40 个字段,带 schema_version 自动迁移(当前版本 1)。主要分组:
| 分组 | 字段 |
|---|---|
| 窗口与启动 | close_to_tray、lightweight_mode、silent_start、autostart、ui_scale、home_page_mode(minimal / console) |
| 外观 | theme、theme_mode、app_opacity、node_scroll |
| 更新 | auto_update(内核)、auto_update_client |
| 订阅 | last_config、config_order、last_proxy_selection、primary_group_preference、subscription_user_agent、hide_timeout_nodes |
| 全局内核偏好 | mode、tun_enabled、mixed_port、socks_port、http_port、ipv6、allow_lan、unified_delay、dns_rewrite_enabled、dns_nameservers、dns_fallbacks、custom_args |
| 功能开关 | auto_apply、failover_enabled、network_optim_auto_apply、encrypt_configs |
| 日志 | log_app_enabled、log_core_enabled、log_retention_days(默认 3)、log_max_file_mb(默认 50) |
| 其他 | copy_env_format(bash/fish/cmd/powershell/nushell)、schema_version |
字段归属:mode 由 switch_mode 写入,tun_enabled 由 TUN 命令写入,encrypt_configs 由 patch_settings 写入;last_config、last_proxy_selection、primary_group_preference、subscription_user_agent 只由各自的专用命令写入。整体 save_settings 不会覆盖这些字段。
TrayMenuState -- 托盘菜单状态
pub struct TrayMenuState {
pub sys_proxy_enabled: bool,
pub tun_enabled: bool,
pub current_mode: String, // rule / global / direct
pub active_config: Option<String>,
pub active_proxy: Option<String>,
#[serde(default)]
pub labels: TrayLabels, // 轻量模式下后端自行重建菜单所需的 i18n 文本
}Tauri 托管状态
| State | 定义位置 | 内容 |
|---|---|---|
MihomoState |
core/mod.rs |
Mutex<CoreData> |
SettingsState |
lib.rs |
Arc<Mutex<Settings>> |
TrayState |
tray.rs |
Arc<Mutex<TrayMenuState>> |
RateLimiter |
lib.rs |
命令限流表 |
ShortcutRegistry |
global_shortcut.rs |
已注册快捷键 |
WebviewHealth |
lib.rs |
前端心跳时间戳(AtomicI64) |
ExplicitExitFlag |
lib.rs |
区分用户主动退出与轻量模式销毁窗口 |
PrismState |
prism.rs |
Prism Engine 扩展、限流器、Smart 状态 |
Arc<SchedulerState> |
core/subscription_scheduler.rs |
订阅自动更新调度器 |
NetworkCoordinatorHandle |
network_coordinator/coordinator.rs |
网络切换协调器句柄 |
数据结构关系图
%%{init: {'themeVariables': {'fontSize': '9px', 'nodeBorder': '1px', 'clusterBorder': '1px', 'edgeLabelHeight': '8px'}}}%%
graph TD
subgraph TauriState["Tauri State (全局注入)"]
MS["MihomoState"]
SS["SettingsState"]
TS["TrayState"]
PS["PrismState"]
SCH["SchedulerState"]
NC["NetworkCoordinatorHandle"]
end
subgraph CoreStructs["核心数据结构"]
CD["CoreData<br/>process / secret / port<br/>proxy_endpoint / started_at"]
PE["ProxyEndpoint<br/>port + scheme"]
ST["Settings<br/>40 个字段"]
TMS["TrayMenuState<br/>sys_proxy / tun / mode<br/>config / proxy / labels"]
AP["AppPaths<br/>app_data / core / profiles"]
CI["ConfigInfo<br/>name / url_display / sub_info"]
end
subgraph ResultStructs["返回值结构"]
CSR["CoreStartResult<br/>secret + port + active_config"]
CUR["ConfigUpdateResult"]
RLR["ReadLogResult"]
end
MS --> CD
CD --> PE
SS --> ST
TS --> TMS
CD --> CSR
AP --> CI
style TauriState fill:#fff3e0,stroke:#e65100,color:#bf360c
style CoreStructs fill:#e3f2fd,stroke:#1565c0,color:#0d47a1
style ResultStructs fill:#e8f5e9,stroke:#2e7d32,color:#1b5e20数据流设计
Zephyr 中存在四种核心数据流:命令调用、配置写入、流量监控与周期同步。
%%{init: {'themeVariables': {'fontSize': '9px', 'nodeBorder': '1px', 'clusterBorder': '1px', 'edgeLabelHeight': '8px'}}}%%
graph TD
subgraph CommandFlow["命令通路 (请求-响应)"]
direction LR
CF1["用户操作"] --> CF2["api.js invoke()"]
CF2 --> CF3["Tauri IPC"]
CF3 --> CF4["Rust Handler"]
CF4 --> CF5["Mihomo / 文件系统"]
CF5 --> CF6["Result 回传"]
CF6 --> CF7["appStore / Bus"]
CF7 --> CF8["DOM 更新"]
end
subgraph ConfigFlow["配置写入通路 (热重载)"]
direction LR
CG1["UI 表单"] --> CG2["update_config(patch)"]
CG2 --> CG3["清洗 + merge_yaml"]
CG3 --> CG4["写 run_config.yaml<br/>+ 当前订阅文件"]
CG4 --> CG5["PUT /configs?force=true"]
end
subgraph SettingsFlow["设置写入通路"]
direction LR
SF1["ui/settings-helpers.js<br/>串行队列"] --> SF2["patch_settings"]
SF2 --> SF3["SettingsState 副本修改"]
SF3 --> SF4["settings.json 原子写入"]
SF4 --> SF5["成功后发布到内存"]
end
subgraph TrafficFlow["流量监控通路 (推送流)"]
direction LR
TF1["Mihomo"] --> TF2["GET /traffic"]
TF2 --> TF3["websocket.js<br/>ReadableStream"]
TF3 --> TF4["60 点滑动窗口"]
TF4 --> TF5["traffic-chart.js<br/>Canvas 2D"]
end
subgraph SyncFlow["周期同步 (10s)"]
direction LR
SY1["startUnifiedSync"] --> SY2["get_sys_proxy<br/>get_tray_proxy_status<br/>has_sysproxy_ownership"]
SY2 --> SY3["修正 UI 状态<br/>按所有权恢复代理<br/>校正托盘图标"]
end
style CommandFlow fill:#e3f2fd,stroke:#1565c0,color:#0d47a1
style ConfigFlow fill:#fff3e0,stroke:#e65100,color:#bf360c
style SettingsFlow fill:#f3e5f5,stroke:#6a1b9a,color:#4a148c
style TrafficFlow fill:#fce4ec,stroke:#c62828,color:#b71c1c
style SyncFlow fill:#e8f5e9,stroke:#2e7d32,color:#1b5e20其他轮询数据源:连接页轮询 GET /connections(递归 setTimeout 动态调整间隔),日志页每 1 秒按字节偏移调用 read_core_log 增量读取(前端环形缓冲 2000 行)。
Rust 后端模块详解
桌面端 Rust 代码位于 apps/desktop/src-tauri/src/,共 66 个 .rs 文件、约 4.2 万行(含测试)。lib.rs 声明 18 个顶层模块。
文件树
src-tauri/src/
├── main.rs # 入口,调用 my_clash_gui_lib::run()
├── lib.rs # run()、Settings、命令注册、窗口/生命周期事件、心跳与唤醒处理
├── backend_event.rs # 结构化事件、错误码、路径脱敏、应用日志写入
├── core_event_bridge.rs # zephyr-core 事件回调 → Tauri 事件
├── core_manager.rs # #[path = "core/mod.rs"] 引入 core/ 并 re-export
├── core/
│ ├── mod.rs # 共享类型(CoreData、ConfigInfo、ProxyEndpoint…)与全局原子变量
│ ├── core_process.rs # 内核进程管理、运行配置、健康检查
│ ├── config_manager.rs # 订阅文件 CRUD、日志目录/导出
│ ├── subscription.rs # 订阅下载、模式/节点切换、临时全局模式恢复
│ ├── subscription_scheduler.rs (+ _test.rs)
│ ├── fetch_util.rs # HTTP 客户端、SSRF 校验、代理回退
│ ├── crypto.rs # 机器密钥、AES-256-GCM、配置文件加解密
│ ├── secure_io.rs # 安全 / 原子文件写入
│ ├── tun_manager.rs # TUN 状态、提权、root 内核管理
│ ├── network_optim.rs (+ _tests.rs)
│ ├── core_log.rs # 内核日志增量读取
│ ├── log_writer.rs # 每日轮转日志与清理
│ └── snapshots/ # insta 快照
├── config_manager.rs # read_config / update_config(热重载)
├── prism.rs # PrismState 与命令层入口
├── prism/ # Prism Engine 集成(21 个 .rs 文件)
├── prism_tests.rs
├── network_coordinator.rs # 协调器入口与命令
├── network_coordinator/ # coordinator / detector / types / tests / platform/{linux,macos,windows}
├── sys_proxy.rs # 系统代理 + 所有权守卫
├── tray.rs (+ tray_tests.rs) # 托盘图标与菜单
├── updater.rs # 内核 / Geo / 客户端更新、WebView 保持
├── elevation.rs # Windows 管理员重启(UAC)
├── process_icon.rs # 连接进程的应用图标
├── deep_link.rs # clash:// 解析与队列
├── global_shortcut.rs # 全局快捷键
├── os_notification.rs (+ _tests.rs)
├── backup.rs # 备份导出 / 导入
├── minisign_verify.rs # Minisign 签名校验
├── uwp_loopback.rs # Windows UWP 环回免除
├── webview_recovery.rs # Windows WebView2 崩溃恢复(仅 Windows 编译)
└── snapshots/ # insta 快照模块依赖关系
%%{init: {'themeVariables': {'fontSize': '9px', 'nodeBorder': '1px', 'clusterBorder': '1px', 'edgeLabelHeight': '8px'}}}%%
graph TB
LIB["lib.rs<br/>入口 · 命令注册 · 状态管理"]
LIB --> CM["core_manager.rs → core/<br/>进程 · 订阅 · 加密 · TUN · 日志"]
LIB --> CFG["config_manager.rs<br/>运行配置热重载"]
LIB --> PRISM["prism.rs → prism/<br/>Prism Engine"]
LIB --> TR["tray.rs<br/>托盘"]
LIB --> UP["updater.rs<br/>更新"]
LIB --> SP["sys_proxy.rs<br/>系统代理"]
LIB --> ELEV["elevation.rs<br/>Windows 提权重启"]
LIB --> PI["process_icon.rs<br/>应用图标"]
LIB --> NCO["network_coordinator/<br/>网络切换协调"]
LIB --> MISC["deep_link · global_shortcut<br/>os_notification · backup<br/>uwp_loopback · webview_recovery"]
LIB --> BE["backend_event.rs<br/>结构化事件"]
TR --> SP
TR --> CM
UP --> CM
UP --> MS["minisign_verify.rs"]
CFG --> CM
PRISM --> CM
NCO --> PRISM
CM --> NCO
ELEV --> SP
CM --> ZC["crates/core<br/>zephyr-core"]
CFG --> ZC
PRISM --> ZC
LIB --> ZC
subgraph TauriPlugins["Tauri 插件"]
P1["autostart(便携模式不加载)"]
P2["dialog · opener"]
P3["global-shortcut · notification"]
P4["window-state"]
P5["single-instance(仅 release)"]
end
LIB --> TauriPlugins
style LIB fill:#fff3e0,stroke:#e65100,color:#bf360c
style CM fill:#e3f2fd,stroke:#1565c0,color:#0d47a1
style CFG fill:#e8f5e9,stroke:#2e7d32,color:#1b5e20
style SP fill:#fce4ec,stroke:#c62828,color:#b71c1c
style TR fill:#f3e5f5,stroke:#6a1b9a,color:#4a148c
style UP fill:#e0f7fa,stroke:#00695c,color:#004d40
style PRISM fill:#e8eaf6,stroke:#283593,color:#1a237e
style ZC fill:#efebe9,stroke:#4e342e,color:#3e2723lib.rs -- 应用入口与生命周期
lib.rs(约 2,640 行)是 Rust 后端的入口文件。
| 职责 | 说明 |
|---|---|
| 应用入口 | run() 构建 Tauri 应用并启动事件循环 |
| 命令注册 | 通过 generate_handler! 注册 180 个命令;其中设置、窗口、快捷键/通知包装、模式切换等命令直接定义在 lib.rs |
| 设置 | Settings 结构、settings.json 读取 / 损坏备份 / schema 迁移 / 持久化;PROFILE_CRYPTO_TRANSFORM 锁串行化加密批处理与订阅写入 |
| 状态管理 | 注入 MihomoState、TrayState、RateLimiter、ShortcutRegistry、WebviewHealth、ExplicitExitFlag,setup 中再注入 SettingsState、PrismState、调度器与网络协调器 |
| 窗口事件 | 关闭到托盘 / 轻量模式销毁 WebView、拖拽导入 YAML、Windows 主题变化时刷新托盘图标 |
| WebView 健康 | 心跳(15s × 3 次未到即视为失效)+ Windows webview_recovery;失效时重建主窗口 |
| 系统唤醒 | RunEvent::Resumed 时探测内核,3 次失败后用上次配置重启并发出 core-restarted-after-resume |
| Panic Hook | 全局 panic 时清理 Mihomo 进程 |
Tauri 应用入口详细流程图
%%{init: {'themeVariables': {'fontSize': '9px', 'nodeBorder': '1px', 'clusterBorder': '1px', 'edgeLabelHeight': '8px'}, 'flowchart': {'curve': 'basis', 'nodeSpacing': 12, 'rankSpacing': 15}}}%%
flowchart TD
A["run()"] --> B["平台预处理<br/>Windows:等待被替换的旧实例退出(提权重启)<br/>Linux:WebKit 渲染 / Wayland 环境变量"]
B --> C["设置 Panic Hook"]
C --> D["加载插件<br/>autostart(非便携)/ dialog / opener<br/>global-shortcut / notification / window-state<br/>single-instance(release,转发 clash:// 参数)"]
D --> E["manage:MihomoState / TrayState / RateLimiter<br/>ShortcutRegistry / WebviewHealth / ExplicitExitFlag"]
E --> F["setup()"]
F --> F1["初始化事件句柄与 core 事件桥<br/>Windows AppUserModelID"]
F1 --> F2["ensure_app_storage + 路径脱敏<br/>按配置初始化 TUN 标志"]
F2 --> F3["读取 / 迁移 settings.json → SettingsState<br/>按设置启用日志持久化"]
F3 --> F4["后台校准订阅文件加密状态"]
F4 --> F5["PrismState / 订阅调度器 / 网络协调器"]
F5 --> F6["init_tray<br/>Windows 监听任务栏主题"]
F6 --> F7["处理命令行 deep link<br/>Windows 武装 WebView2 崩溃恢复"]
F7 --> G["on_window_event + invoke_handler(180 个命令)"]
G --> H["build + run 事件循环<br/>ExitRequested / Exit / Resumed"]
style A fill:#fff3e0,stroke:#e65100,color:#bf360c
style F fill:#e8f5e9,stroke:#2e7d32,color:#1b5e20
style H fill:#c8e6c9,stroke:#2e7d32,color:#1b5e20主窗口在所有平台上都是无边框(decorations: false)+ 透明窗口,由前端绘制标题栏与窗口按钮;窗口尺寸、位置与最大化状态由 window-state 插件记忆。
core_manager.rs → core/ 模块
core_manager.rs(约 100 行)通过 #[path = "core/mod.rs"] pub mod core; 引入 core/ 目录,并 re-export 常用函数、类型与常量。core/ 共约 1.6 万行。
| 子模块 | 行数 | 职责 |
|---|---|---|
mod.rs |
~290 | 共享类型(CoreData、MihomoState、AppPaths、ConfigInfo、ProxyEndpoint、CoreStartResult、ReadLogResult)、全局原子变量、re-export |
core_process.rs |
~3,710 | 内核启动 / 停止 / 版本 / uptime、运行配置生成与全局偏好注入、mihomo -t 预检、认证健康检查、孤儿进程防护(Windows Job Object / Unix 进程组 + PR_SET_PDEATHSIG)、便携模式路径 |
subscription.rs |
~4,010 | 订阅下载(三层策略、DNS pinning、细粒度错误分类、URL 脱敏)、switch_mode / switch_global_node 的实现、临时全局模式的恢复标记与校准 |
subscription_scheduler.rs |
~425 | 按每个订阅的间隔后台自动更新 |
fetch_util.rs |
~1,350 | HTTP 客户端构建、SSRF 错误类型、重定向上限、经托管内核的代理回退 |
tun_manager.rs |
~2,260 | TUN 状态与切换锁、实时 TUN 覆盖状态、macOS root 重启与托管进程清理、Linux pkexec + setcap + polkit 规则授权、Windows TCP 优化 |
crypto.rs |
~1,050 | 机器密钥、AES-256-GCM、订阅文件读写与批量加 / 解密、metadata 加密 |
config_manager.rs |
~1,150 | 订阅文件 CRUD、重命名 / 删除、URL / 间隔 / UA 更新、日志目录与导出 |
network_optim.rs |
~1,090 | TCP 参数优化 apply / revert / status(三个平台各一套实现) |
network_optim_tests.rs |
~45 | Linux 优化脚本快照测试(conservative / balanced / aggressive) |
secure_io.rs |
~285 | 安全写入(Unix 0o600 / Windows DACL)、原子写入与替换 |
core_log.rs |
~185 | 内核日志增量读取(含轮转检测) |
log_writer.rs |
~220 | 每日轮转日志、按保留天数清理、按日期收集 |
prism.rs → prism/ 模块
prism.rs(约 320 行)定义 PrismState,桥接 clash-prism-* crate 到 Zephyr,并注册 Prism 限流规则。prism/ 目录包含 21 个 .rs 文件(约 6,850 行)。
| 子模块 | 行数 | 职责 |
|---|---|---|
commands_core.rs |
~315 | Prism 核心命令(apply / rebuild / status / 规则插入 / watch / trace 报告) |
rule_library.rs |
~550 | 规则库文件 CRUD、导入(文本 / 文件 / URL)、从订阅提取,保存前统一 YAML 清洗 |
rule_groups.rs |
~130 | 规则分组与自动应用开关 |
plugin_commands.rs |
~245 | 插件发现 / 加载 / 启停 / 钩子 / 权限 |
script_commands.rs |
~365 | 脚本沙箱执行、写回、沙箱与资源限制、插件权限 |
smart_commands.rs |
~260 | 智能节点评分 / 排名 / 配置 |
smart_state.rs |
~450 | Smart 状态异步持久化(WAL + DashMap + mpsc) |
failover_commands.rs |
~90 | 自动故障转移策略 |
kv_commands.rs |
~85 | 键值存储 |
trace_commands.rs |
~25 | trace 统计与过滤 |
overrides.rs |
~30 | 覆写系统入口,以 #[path] 引入 overrides_commands.rs |
overrides/overrides_model.rs |
~140 | 覆写数据模型(OverrideItem / OverrideLog 等) |
overrides/overrides_store.rs |
~380 | 覆写持久化(overrides/meta.json) |
overrides_commands.rs |
~1,585 | 覆写 IPC 命令(13 个);Prism YAML 覆写保存前清除危险键;切换配置时重新应用 |
pipeline.rs |
~565 | 脚本执行 + 配置写回 + 热重载的共享管道 |
host.rs |
~495 | ZephyrPrismHost:把 Zephyr 状态桥接给 Prism Engine,发出 prism-event |
types.rs |
~355 | 共享类型与输入校验(文件名、插件 ID、输入大小) |
rate_limiter.rs |
~215 | 滑动窗口限流器 |
*_tests.rs |
— | smart_state_tests.rs、overrides_commands_tests.rs、overrides/overrides_model_tests.rs |
此外 prism_tests.rs(约 1,420 行,121 个测试)覆盖 Prism 命令层。覆写文件存放在 {app_data_dir}/prism/overrides/(meta.json、{id}.js、{id}.prism.yaml、{id}.log.json、exports/)。
network_coordinator/ -- 网络切换协调
| 文件 | 职责 |
|---|---|
network_coordinator.rs |
入口,get_network_state / notify_network_change 命令 |
coordinator.rs |
单飞(single-flight)协调器:去抖、失败计数、网络变化后自动应用 Prism 规则,发出 network-state-changed |
detector.rs |
网络状态检测(接口类型、连通性、SSID)与短时缓存 |
types.rs |
NetworkState、NetworkChangeReason 等类型 |
platform/ |
平台原生网络事件监听(Windows 有原生 watcher) |
tests.rs |
协调器测试 |
elevation.rs -- Windows 管理员重启
Windows 上 TUN 需要提权进程(内核要创建 Wintun 适配器并改写路由,且继承应用的令牌)。由于内核无法在保留管道与 Job Object 的前提下单独提权,Zephyr 通过 ShellExecuteExW + runas 以管理员身份重启整个应用:
- 新实例通过
--zephyr-wait-pid=<pid>等待旧实例退出(最多 15 秒,超时则终止旧实例),避免 single-instance 插件把它交回旧实例。 - 仅对 UAC 拆分令牌的管理员账户(
can_elevate_as_self)提供该功能;标准账户提权会以另一个用户身份运行,丢失原用户的数据目录。 - 提权实例运行时,普通启动会被转发给它。
- macOS 改为以 root 重启内核本身,Linux 为内核二进制授予 capabilities;这两个平台上相关命令为空实现。
process_icon.rs -- 应用图标
把 Mihomo 连接信息中的 processPath 转换为小尺寸 data: URL,供连接列表显示真实应用图标:
| 平台 | 图标来源 |
|---|---|
| Windows | 可执行文件内嵌图标(SHDefExtractIconW) |
| macOS | 最外层 .app 包的图标(读取 Info.plist 的 CFBundleIconFile) |
| Linux | 匹配的 .desktop 条目(或 Flatpak 应用 ID)指向的 hicolor 主题 / pixmaps 图标 |
命中结果在进程生命周期内缓存(最多 256 条),未找到的结果缓存 5 分钟;图标上限 512 KB,网络文件系统上的路径会被跳过。返回 None 时前端显示中性占位图标(ui/process-icon.js)。
config_manager.rs -- 运行配置热重载
顶层 config_manager.rs 只保留 Tauri 胶水:read_config(返回去除 secret 的运行配置)与 update_config(见配置热重载流程)。纯逻辑委托给 zephyr_core::config::merge。
sys_proxy.rs -- 系统代理
sys_proxy.rs(约 1,380 行)在各平台上配置和清除系统代理(Windows 注册表 + WinINet 刷新、macOS networksetup、Linux GNOME / KDE / XFCE),并实现所有权守卫(见所有权守卫)。代理切换由内部互斥锁串行化,避免恢复与关闭交错。
tray.rs -- 系统托盘
tray.rs(约 1,340 行)管理托盘图标和菜单:
- 图标:空闲态 Windows 按任务栏明暗主题使用
tray-idle-dark/tray-idle-light,macOS 使用模板图tray-idle-template;系统代理态tray-proxy,TUN 态tray-tun。后端记录当前实际显示的图标模式(get_tray_icon_mode),供前端周期校正。 - 菜单:显示主窗口、系统代理、TUN、运行模式、订阅切换、节点切换、复制代理环境变量、退出。轻量模式下(无 WebView)后端可依据
TrayMenuState.labels自行重建菜单。 - 复制环境变量:支持 bash / fish / cmd / powershell / nushell 五种格式;除 cmd 外同时输出大小写变量(
http_proxy+HTTP_PROXY等),URL 协议随实际监听端点(http / socks5h)。resolve_proxy_format提供平台默认格式(Windows → powershell,其他 → bash)。测试见tray_tests.rs(insta 快照)。
updater.rs -- 更新
updater.rs(约 2,750 行)负责 Mihomo 内核、Geo 数据与 Zephyr 客户端的更新。所有更新流量都是先直连、失败后经托管内核代理,不使用系统 / 环境代理。
内核更新流程
%%{init: {'themeVariables': {'fontSize': '9px', 'nodeBorder': '1px', 'clusterBorder': '1px', 'edgeLabelHeight': '8px'}, 'flowchart': {'curve': 'basis', 'nodeSpacing': 12, 'rankSpacing': 15}}}%%
flowchart TD
A["get_latest_version()"] --> B["GitHub REST API 获取最新 Release<br/>失败时回退 releases.atom"]
B --> C["按平台 / 架构选择资产<br/>x86_64 支持 v3 时优先 v3"]
C --> D["前端比较版本并调用 update_core(url)"]
D --> E{"URL 是官方<br/>MetaCubeX/mihomo Release?"}
E -->|"否"| ERR1["拒绝"]
E -->|"是"| F["流式下载到随机命名临时文件<br/>emit core-download-status"]
F --> G["SHA-256 校验<br/>(GitHub 资产 digest)"]
G -->|"不匹配"| ERR2["删除临时文件并报错"]
G -->|"通过"| H["解压 .zip(Windows)/ .gz(其他)<br/>拒绝符号链接 / 压缩炸弹 / 非预期文件"]
H --> I["停止内核(失败则中止更新)"]
I --> J["旧二进制 → 备份<br/>新二进制 → 正式路径<br/>设置可执行权限"]
J --> K["start_core 重启内核"]
K -->|"成功"| L["删除备份<br/>emit Core ready"]
K -->|"失败"| M["回滚备份二进制"]
style ERR1 fill:#ffcdd2,stroke:#c62828,color:#b71c1c
style ERR2 fill:#ffcdd2,stroke:#c62828,color:#b71c1c
style M fill:#ffcdd2,stroke:#c62828,color:#b71c1c
style L fill:#c8e6c9,stroke:#2e7d32,color:#1b5e20客户端更新
update_client 下载对应平台的安装包后依次进行 SHA-256 校验(Release 必须带有效 digest)与 Minisign Ed25519 签名校验(minisign_verify.rs,公钥硬编码),再用系统默认程序打开安装包。便携模式不支持自更新。
WebView 保持
更新由页面驱动(持有网络锁、在内核更新后运行安装器并报告结果)。更新进行期间,即使开启轻量模式,关闭窗口也只会隐藏而不会销毁 WebView;页面通过 end_update_hold 结束保持,并用 set_page_busy 声明自身正在进行的网络操作(切换订阅、切换 TUN、重启后恢复等)。
其他模块
| 模块 | 职责 |
|---|---|
backend_event.rs |
BackendEvent、emit_error!/emit_warn!/emit_info! 宏、97 个错误码、路径脱敏、应用日志写入、lock_critical / lock_best_effort 锁辅助 |
core_event_bridge.rs |
实现 zephyr-core 的 CoreEventCallback,把 core crate 事件转发到 Tauri |
deep_link.rs |
解析 clash://install-config,名称清洗(路径穿越、Windows 保留名、长度 128),排队投递 |
global_shortcut.rs |
注册 / 注销全局快捷键,触发时发出 global-shortcut |
os_notification.rs |
原生通知,标题截断至 128 字符、正文 1024 字符 |
backup.rs |
备份导出(settings.json、run_config.yaml、订阅文件、metadata → ZIP + manifest.json + SHA-256)与三阶段事务导入(校验 → 暂存 → 提交,失败回滚) |
minisign_verify.rs |
Minisign Ed25519 签名校验 |
uwp_loopback.rs |
Windows UWP 应用环回免除 |
webview_recovery.rs |
Windows WebView2 崩溃恢复第一层:监听 ProcessFailed,渲染进程崩溃时 reload,浏览器进程退出或崩溃循环时重建窗口 |
crates/core -- zephyr-core
平台无关的纯逻辑库(约 8,000 行),桌面端与移动端共用;开启 uniffi feature 时导出 UniFFI 绑定。
| 模块 | 职责 |
|---|---|
config/merge.rs |
merge_yaml、安全键提取 / 恢复、update_config_core、ConfigIo trait |
config/sanitizer.rs |
危险键清除、配置文件名清洗、路径越界校验 |
config/subscription.rs |
订阅 URL / 名称校验、私有地址与 Fake-IP 判断、Base64 检测、错误分类、URL 脱敏 |
config/fetch_util.rs |
HTTP 客户端配置与 URL 校验 |
config/tun.rs |
TUN 配置纯函数(如 update_tun_in_yaml) |
config/proxy_validation.rs |
代理服务器地址校验 |
config/prism_types.rs · pipeline_types.rs · overrides_model.rs · types.rs |
共享数据类型 |
process.rs |
进程管理 trait、二进制名、默认端口、版本解析 |
updater.rs |
更新相关纯函数与类型 |
crypto.rs |
AES-256-GCM 加解密(v2 格式) |
event.rs |
跨平台事件与错误码(UniFFI 回调接口) |
smart_selector.rs · failover.rs · rate_limiter.rs · state.rs · error.rs |
智能选择、故障转移、限流、状态与错误类型 |
crates/core-ffi(zephyr-core-ffi)只是把 zephyr-core(启用 uniffi)链接为 cdylib / staticlib,供 uniffi-bindgen 生成移动端绑定。
前端架构
Zephyr 前端采用原生 JavaScript 构建,无任何前端框架依赖,也不经过打包器:tauri.conf.json 的 frontendDist 直接指向 apps/desktop/src。自有代码共 104 个 ES Module(不含测试与预构建第三方库):
| 位置 | 数量 | 内容 |
|---|---|---|
src/ 根目录 |
5 | main.js、api.js、websocket.js、i18n.js、rules.js |
src/ui/ |
71 | 58 个顶层模块 + settings/ 7 + editor/ 4 + rule-library/ 2 |
src/modules/ |
3 | connections.js、traffic-chart.js、backend-events.js |
src/utils/ |
24 | 工具模块 |
src/types/ |
1 | guards.js(另有 guards.d.ts、tauri.d.ts) |
预构建第三方库:cm6.bundle.js(CodeMirror 6,由 ui/editor/cm6-entry.js 经 esbuild 打包)、js-yaml.min.js。测试为与源码同目录的 84 个 *.test.js(Vitest + happy-dom),src/test/index-dom.js 为测试辅助。
核心模块(src/ 根目录)
| 模块 | 行数 | 职责 |
|---|---|---|
main.js |
~890 | 应用初始化编排、内核启动与 TUN 失败回退、自动更新检查、唤醒重启后的恢复、心跳 |
api.js |
~940 | Tauri IPC 桥接(window.__TAURI_INTERNALS__)、listen()、Mihomo REST API 封装、ApiError |
websocket.js |
~495 | /traffic 流连接管理(fetch ReadableStream)、状态机、指数退避重连(1s 起,最多 30s、15 次)、30s 静默判定断线 |
i18n.js |
~14,960 | 14 种语言翻译(en、zh、ja、ko、ru、es、fa、tk、my、ar、ur、sw、tr、vi)与运行时切换 |
rules.js |
~95 | Shadowrocket 规则转 Clash 规则 |
UI 模块(src/ui/)
页面与主要功能
| 模块 | 行数 | 职责 |
|---|---|---|
settings.js |
~3,120 | 设置页主逻辑 |
settings/subscriptions.js |
~2,290 | 订阅列表、添加 / 更新 / 排序 |
settings/auto-switch.js |
~395 | 节点自动切换等代理行为开关 |
settings/theme.js |
~290 | 主题色、明暗 / 自动模式 |
settings/tunnels.js |
~300 | 隧道管理 |
settings/client-version-fetch.js |
~190 | 伪装客户端版本查询的重试控制 |
settings/reset-merge.js · settings/sub-download-gate.js |
小 | 恢复默认时的合并规则;订阅下载 5 秒间隔的前端门控 |
settings-nav.js · settings-helpers.js |
~210 / ~90 | 设置页分节索引;patch_settings 串行写入队列 |
proxies.js |
~3,530 | 代理组 / 节点列表、延迟测试、排序 |
proxy-groups.js · proxy-memory.js · observed-group.js · group-mismatch.js |
— | 主策略组判定;按订阅记忆并恢复节点选择;从连接推断实际使用的组;组不一致提示 |
console-home.js |
~2,380 | 控制台首页(信息密集型仪表盘) |
home-stats.js · node-wheel.js · status-ring.js |
— | 极简首页数据条;节点滚轮选择器;环形进度指示 |
no-subscription.js · core-error.js |
— | 无订阅状态;内核启动失败的页内错误卡片(含重试) |
rule-library.js |
~2,250 | Prism 规则库页面 |
rule-library/yaml-utils.js · rule-library/rule-form.js |
~505 / ~170 | 规则文件 YAML 解析与重建;规则表单共享组件 |
plugins.js |
~2,310 | 覆写管理(订阅页) |
plugins-smart.js |
~720 | 插件 / 脚本 / Smart / Failover 页面逻辑 |
prism.js |
~630 | Prism IPC 包装器 |
editor/ |
— | CodeMirror 6 编辑器工厂、Prism DSL 高亮与补全 |
logs.js · log-highlight.js · app-log-format.js |
~1,040 / ~230 / ~215 | 日志页(虚拟滚动);日志行渲染与搜索高亮;应用日志格式化 |
advanced.js · rules.js · dns-shared.js · dns.js |
— | 高级配置编辑;自定义规则;DNS 配置与重写(单一来源) |
update-dialog.js |
~830 | 更新对话框(检查、下载进度、安装各状态) |
运行状态与系统集成
| 模块 | 职责 |
|---|---|
lifecycle.js |
订阅切换、重启后恢复等高层编排(避免 api.js 与 UI 模块循环依赖) |
active-profile.js · run-config-cache.js |
内核实际运行的订阅;run_config 的 TTL 缓存 |
modes.js · tun.js · tun-status.js · sysproxy.js |
运行模式、TUN(含 Windows 管理员重启入口)、系统代理开关 |
tray.js |
托盘事件监听、10s 统一同步、代理环境变量格式 |
network-optim.js |
网络优化开关 |
deep-link.js · link-dialog.js |
clash:// 处理;链接输入对话框 |
global-shortcut.js · os-notification.js |
全局快捷键;系统通知封装 |
process-icon.js |
连接行的应用图标(调用 get_process_icon) |
基础设施与通用组件
| 模块 | 职责 |
|---|---|
state.js |
基于 Proxy 的响应式 appStore(微任务批量通知、localStorage 持久化) |
events.js |
Bus 事件总线 |
bind.js |
基于 store.subscribe() 的 DOM 绑定 |
cache.js |
API 缓存(LRU 50 条、TTL 2s + SWR 5s、in-flight 去重)与托盘菜单缓存(2s) |
navigation.js · nav-tooltip.js · header-fit.js |
页面切换;侧栏提示;标题空间不足时逐级缩小 |
notifications.js · dropdown.js · collapsible.js · seg-control.js |
Toast / 模态框;自定义下拉框;折叠面板;分段控件 |
tablist-keys.js · input-modality.js |
tablist 键盘行为;仅键盘操作时显示焦点环 |
window-controls.js · theme.js · icons.js · 3d-effect.js · console-width.js |
窗口按钮;主题;SVG 图标;悬停 3D 效果;控制台断点 |
功能模块(src/modules/)
| 模块 | 行数 | 职责 |
|---|---|---|
connections.js |
~1,750 | 活动 / 已关闭连接列表、轮询、delta 流量累积、搜索与批量操作 |
traffic-chart.js |
~560 | Canvas 2D 平滑面积图(60 数据点) |
backend-events.js |
~330 | 全局监听 backend-event / prism-event,缓存供日志页渲染,错误级事件弹出 Toast |
工具模块(src/utils/)
| 模块 | 职责 |
|---|---|
sanitize.js |
escapeHtml() / escapeAttr() / 白名单 HTML 清理 |
markdown.js |
轻量 Markdown → HTML(XSS 安全) |
url-validate.js |
对话框中链接的实时校验 |
logger.js · frontend-log.js |
分级日志;转发到后端应用日志 |
format.js · intl-cache.js |
大小 / 速度 / 时间格式化;Intl 实例缓存 |
virtual-scroll.js · keyed-list.js |
虚拟滚动;轮询表格的 keyed 列表协调 |
focus-trap.js · roving-tabindex.js · context-menu.js |
模态框焦点陷阱;列表键盘导航;右键菜单 |
page-visibility.js · cleanup-registry.js |
页面不可见时暂停计时器 / RAF;统一资源清理 |
sequential.js · debounce.js · throttle.js |
同 key 顺序执行;防抖;节流 |
version-utils.js |
SemVer 比较 |
css-var-cache.js · color.js · array.js · rule-utils.js · dom-id.js · clipboard.js |
CSS 变量缓存;颜色;数组;规则辅助;crypto.getRandomValues() 生成 DOM ID;内联粘贴按钮 |
共享包(packages/)
| 包 | 职责 |
|---|---|
@zephyr/shared |
src/index.js 导出 23 个命名空间(CORE、CONFIG_FILES、SYS_PROXY、TUN、TRAY、UPDATER、SETTINGS、PRISM、RULE、OVERRIDE、NETWORK_COORDINATOR 等)共 176 个 IPC 命令名,并合并为 COMMANDS。前端调用 invoke() 时统一引用它,为将来 tauri-specta 自动生成做准备 |
tokens |
设计令牌(primitive / semantic / component JSON),经 style-dictionary 生成 variables.css、theme.css |
scripts |
check-i18n.js:校验各语言翻译键 |
api.js -- API 调用层
// api.js — Tauri IPC 桥接层(withGlobalTauri: false,直接使用内部对象)
const _t = typeof window !== 'undefined' ? window.__TAURI_INTERNALS__ : undefined;
function invoke(cmd, args) {
if (!_t?.invoke) throw new Error('[API] Tauri IPC not available');
return _t.invoke(cmd, args);
}
// 事件监听:transformCallback + plugin:event|listen,目标为 main 窗口
async function listen(event, handler) { /* ... */ }
let BASE_URL = 'http://127.0.0.1:9090'; // start_core 返回端口后更新
// Mihomo REST API 统一经 apiFetch(超时、Bearer secret、ApiError)
export async function getProxies() { /* apiFetch('/proxies') */ }前端模块依赖图
%%{init: {'themeVariables': {'fontSize': '9px', 'nodeBorder': '1px', 'clusterBorder': '1px', 'edgeLabelHeight': '8px'}}}%%
graph TD
MAIN["main.js<br/>应用入口"] --> API["api.js<br/>IPC + REST"]
MAIN --> I18N["i18n.js"]
MAIN --> WS["websocket.js<br/>/traffic 流"]
MAIN --> BEV["modules/backend-events.js"]
MAIN --> CHART["modules/traffic-chart.js"]
MAIN --> CONN["modules/connections.js<br/>进入页面时初始化"]
MAIN --> UI["src/ui/<br/>页面与组件"]
MAIN -.->|"动态 import()"| LAZY["ui/logs.js · ui/rule-library.js<br/>ui/advanced.js · ui/lifecycle.js"]
UI -->|"import"| API
UI -->|"import"| STORE["ui/state.js · ui/events.js<br/>appStore / Bus"]
UI -->|"import"| UTILS["src/utils/"]
UI -->|"import"| SHARED["@zephyr/shared<br/>COMMANDS"]
API --> SHARED
WS --> API
CONN --> API
style MAIN fill:#fff3e0,stroke:#e65100,color:#bf360c
style API fill:#e8f5e9,stroke:#2e7d32,color:#1b5e20
style I18N fill:#f3e5f5,stroke:#6a1b9a,color:#4a148c
style WS fill:#fce4ec,stroke:#c62828,color:#b71c1c
style UI fill:#e3f2fd,stroke:#1565c0,color:#0d47a1
style LAZY fill:#e0f2f1,stroke:#00695c,color:#004d40
style SHARED fill:#efebe9,stroke:#4e342e,color:#3e2723
style UTILS fill:#fff8e1,stroke:#f57f17,color:#e65100缓存系统
- apiCache(
ui/cache.js):LRU 上限 50 条,TTL 2 秒 + SWR 窗口 5 秒,覆盖 config、proxies、settings 等 - Stale-While-Revalidate:过期后先返回旧数据并后台静默刷新
- In-flight 去重:同一 key 的并发请求共享同一个 Promise
- 顺序执行器(
utils/sequential.js):同一 key 的请求按序执行,只采用最新结果 - trayMenuCache:托盘菜单数据缓存 2 秒
- run-config-cache:编译后 run_config 的 TTL 缓存
并发安全与原子变量
Zephyr 使用原子类型、互斥锁与 Drop 守卫保证多线程安全。
并发控制模式
以内核启动锁为例(core_process.rs):
// 等待获取锁:每 200ms 重试,超过 10s 视为卡死并强制重置
let mut wait_ms = 0;
while CORE_STARTING
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.is_err()
{
if wait_ms > 10000 {
CORE_STARTING.store(false, Ordering::SeqCst);
if CORE_STARTING
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.is_ok()
{
break;
}
}
tokio::time::sleep(Duration::from_millis(200)).await;
wait_ms += 200;
}
// Drop 守卫:无论正常返回、提前 return 还是 panic,都会释放锁
struct ResetGuard;
impl Drop for ResetGuard {
fn drop(&mut self) {
CORE_STARTING.store(false, Ordering::SeqCst);
}
}
let _guard = ResetGuard;主要同步原语
| 原语 | 位置 | 作用 |
|---|---|---|
CORE_STARTING |
core/mod.rs |
内核启动互斥 |
TUN_TOGGLING |
core/mod.rs |
TUN 切换互斥(try_acquire_tun_toggle / release_tun_toggle) |
PROFILE_CRYPTO_TRANSFORM |
lib.rs |
串行化订阅加密批处理与所有订阅写入;必须先于设置锁获取 |
SettingsState 互斥锁 |
lib.rs |
设置的内存快照;写入先持久化,成功后才发布到内存 |
RUN_CONFIG_LOCK |
prism/pipeline.rs |
串行化 run_config.yaml 的写回 |
METADATA_LOCK / META_LOCK |
core/crypto.rs / prism/overrides/overrides_store.rs |
订阅 metadata / 覆写 meta.json 读改写 |
GLOBAL_MODE_LOCK |
core/subscription.rs |
订阅下载临时切换全局模式时的互斥 |
RESUME_IN_PROGRESS |
lib.rs |
防止多个唤醒事件并发处理 |
MACHINE_KEY_PERSISTED |
core/crypto.rs |
机器密钥是否已持久化 |
前端侧,appStore 中的 isNetworkUpdating 作为网络操作锁(订阅切换、TUN 切换、唤醒恢复等),周期同步在该锁被持有时不修改托盘图标。
应用生命周期
%%{init: {'themeVariables': {'fontSize': '9px', 'nodeBorder': '1px', 'clusterBorder': '1px', 'edgeLabelHeight': '8px'}}}%%
graph TD
subgraph Startup["启动阶段"]
S1["Rust 初始化<br/>插件 + State + Panic Hook<br/>setup:设置 / Prism / 调度器 / 协调器 / 托盘"]
S2["主窗口(无边框,初始隐藏)"]
S3["前端 initApp<br/>启动内核 → 初始化模块 → 监听事件"]
S4["显示窗口(silent_start 时只留托盘)"]
S1 --> S2 --> S3 --> S4
end
subgraph Running["运行阶段"]
R1["命令处理<br/>invoke → Command → 返回"]
R2["事件推送<br/>emit → listen → UI 更新"]
R3["周期同步 10s<br/>系统代理 / 托盘状态 / 所有权恢复"]
R4["流量监控<br/>/traffic → Canvas"]
R5["订阅调度器<br/>按订阅间隔自动更新"]
R6["网络切换协调<br/>自动应用 Prism 规则"]
R7["心跳 15s<br/>WebView 失效则重建窗口"]
R8["系统唤醒<br/>探测内核,必要时重启"]
end
subgraph Close["关闭窗口"]
C1{"close_to_tray?"}
C2{"轻量模式且<br/>无更新保持?"}
C3["销毁 WebView<br/>托盘点击时重建"]
C4["隐藏窗口"]
C1 -->|"是"| C2
C2 -->|"是"| C3
C2 -->|"否"| C4
end
subgraph Shutdown["退出阶段"]
Q1["托盘「退出」或关闭(close_to_tray 关闭)<br/>设置 ExplicitExitFlag"]
Q2["停止内核<br/>清除系统代理与所有权标记"]
Q3["RunEvent::Exit<br/>关闭订阅调度器<br/>kill_mihomo()"]
Q4["macOS:仅清理本应用确认过的 root 内核"]
Q5["进程退出"]
Q1 --> Q2 --> Q3 --> Q4 --> Q5
end
Startup --> Running
Running --> Close
C1 -->|"否"| Shutdown
Running --> Shutdown
style Startup fill:#e8f5e9,stroke:#2e7d32,color:#1b5e20
style Running fill:#e3f2fd,stroke:#1565c0,color:#0d47a1
style Close fill:#fff3e0,stroke:#e65100,color:#bf360c
style Shutdown fill:#fce4ec,stroke:#c62828,color:#b71c1cExitRequested 在未设置 ExplicitExitFlag 时会被阻止,因此轻量模式销毁最后一个窗口或隐藏到托盘都不会让应用退出。
依赖版本
Rust(apps/desktop/src-tauri/Cargo.toml)
| 依赖 | 版本约束 | 用途 |
|---|---|---|
tauri |
2(features: tray-icon, image-png) | 应用框架 |
tauri-plugin-* |
2 | autostart、opener、global-shortcut、notification、dialog、single-instance、window-state |
reqwest |
0.13(json, stream, gzip, brotli, deflate, blocking, socks) | HTTP 客户端(socks 用于经 socks-port 的代理回退) |
tokio |
1(rt, macros, time, sync, fs) | 异步运行时 |
serde_yaml(serde_yaml_ng) |
0.10 | YAML |
aes-gcm / pbkdf2 / hmac / sha2 |
0.11 / 0.12 / 0.12 / 0.10 | 加密与哈希 |
rand |
0.10.1 | 随机数 |
zip / flate2 / tar |
9.0 / 1 / 0.4 | 解压与备份 |
minisign |
0.9 | 客户端更新签名校验 |
clash-prism-core / -extension / -plugin / -script / -smart |
0.1.5 / 0.1.9(watcher) / 0.1.1 / 0.1.3 / 0.1.1 | Prism Engine |
dashmap / chrono / toml / arboard |
6.0 / 0.4 / 1.1 / 3.3 | 并发 Map、时间、TOML、剪贴板(Linux 启用 Wayland) |
windows-sys / winreg / webview2-com |
0.59 / 0.52 / =0.39.1 | Windows API;webview2-com 固定为与 tauri/wry 一致的版本 |
plist |
1 | macOS:读取 .app 的 Info.plist(应用图标) |
libc / glib |
0.2 / ≥0.20 | Unix:PR_SET_PDEATHSIG 等 |
工具链固定为 Rust 1.96.0(rust-toolchain.toml),crate 声明 rust-version = "1.92"。截至 v2.5.0 的 Cargo.lock 中,tauri 解析为 2.12.4、reqwest 0.13.4、tokio 1.52.3。crates/core 依赖 aes-gcm 0.10、rand 0.9、uniffi 0.28(可选)以及 clash-prism-core / clash-prism-smart。
前端(package.json)
| 依赖 | 版本 | 用途 |
|---|---|---|
@tauri-apps/cli |
2.12.3 | Tauri CLI |
@tauri-apps/plugin-autostart |
2.7.0 | 开机自启 JS 绑定 |
@codemirror/* |
^6(view ^6.43.14、state ^6.7.6、language ^6.13.1、commands ^6.11.1 等) | 规则 / 覆写编辑器 |
vitest / @vitest/coverage-v8 |
5.0.4 | 单元测试与覆盖率 |
happy-dom |
^20.11.15 | 测试 DOM 环境 |
vite |
8.3.4(pnpm overrides 固定为 8.1.6) | Vitest 依赖 |
typescript |
^7.0.2 | tsc --noEmit JSDoc 类型检查 |
eslint |
10.12.0 | 代码检查 |
unocss / @unocss/cli / @unocss/preset-wind4 |
^66.11.0 | 原子化 CSS |
style-dictionary |
^5.6.1 | 设计令牌 |
esbuild |
^0.28.2 | 打包 CodeMirror bundle |
@cyclonedx/cdxgen |
12.8.6 | SBOM 生成 |
包管理器为 [email protected],要求 Node.js ≥ 20.18.1。仓库内各 manifest 中的版本号(1.2.0)为占位值,发布时由 CI 写入实际版本。
返回 Home