本文档以中文撰写。

系统架构

Zephyr 采用 Tauri v2 混合架构,以 Rust 后端为核心引擎、原生 JavaScript 前端为交互层、Mihomo 内核为代理执行层,三层协同工作,实现高性能、低资源占用的代理管理体验。本页描述 v2.5.0 的代码结构。


目录


架构总览

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&lt;T, String&gt;<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 递归合并算法

rust
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 管理员重启前会暂停代理并保留标记,交给新实例恢复。

安全验证逻辑

rust
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 实现

rust
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

clash://install-config?url=...&name=... 链接(来自命令行参数或第二实例转发)先进入后端队列(最多 8 条,超出丢弃最旧的),再发出无负载的 deep-link 事件。前端在监听注册完成时、每次收到事件时以及处理完一条链接后调用 take_pending_deep_links,每次只取最旧的一条——这样 WebView 尚未就绪时到达的链接也不会丢失。

事件监听示例

javascript
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 进程运行时状态

rust
/// 由 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 -- 代理端点

rust
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 -- 应用路径集合

rust
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 -- 订阅元信息

rust
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

rust
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 -- 托盘菜单状态

rust
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:#3e2723

lib.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 调用层

javascript
// 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):

rust
// 等待获取锁:每 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:#b71c1c

ExitRequested 在未设置 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

↑↓ 选择↵ 打开