本文档以中文撰写。

贡献指南

感谢你对 Zephyr 项目的关注!Zephyr 是基于 Tauri v2 的 Mihomo GUI 代理客户端。本文档介绍贡献流程、开发环境、本地检查、测试与代码规范、翻译(i18n)贡献、CI/CD 流水线以及发布流程。

仓库根目录的 CONTRIBUTING.md 是贡献规则的权威来源;本页在其基础上补充了各项工具与流水线的实际配置。

目录


贡献流程

项目采用标准的 GitHub Fork + Pull Request 流程:

  1. Fork 仓库到你的 GitHub 账号
  2. 克隆你的 Fork:git clone https://github.com/<你的用户名>/Zephyr.git
  3. 创建分支:git checkout -b feature/your-feature-name
  4. 按照下文的代码规范修改代码
  5. 在本地运行检查与测试(见本地检查命令速查)
  6. 使用清晰的提交信息提交
  7. 推送到你的 Fork:git push origin feature/your-feature-name
  8. 向 main 分支发起 Pull Request
%%{init: {'themeVariables': {'fontSize': '10px', 'nodeBorder': '1px', 'clusterBorder': '1px', 'edgeLabelHeight': '10px'}, 'flowchart': {'curve': 'basis', 'nodeSpacing': 15, 'rankSpacing': 25}}}%%
flowchart LR
    A[Fork & 克隆] --> B[创建分支]
    B --> C[修改代码 + 测试]
    C --> D["git commit<br/>(pre-commit hook)"]
    D --> E["git push<br/>(pre-push hook)"]
    E --> F[发起 PR → main]
    F --> G["CI / 安全扫描<br/>pre-commit.ci / Codecov"]
    G --> H[维护者审查]
    H --> I[合并]

Pull Request 要求

  • 每个 PR 只做一件事(一个功能或一个修复),保持改动聚焦
  • 清楚描述改了什么、为什么改
  • 新增的主要功能必须附带自动化测试;修复 Bug 时应补充或更新能覆盖该行为的测试
  • 所有提交在合并进 main 之前都需要经过维护者审查,审查关注:正确性与完整性、是否符合代码规范、新功能的测试覆盖、安全影响、必要的文档更新
  • 任意一项 CI 检查失败的 PR 不会被合并

提交信息约定

CONTRIBUTING.md 要求使用 Conventional Commits 格式,例如:

前缀 用途
feat: 新功能
fix: Bug 修复
docs: 文档
refactor: 重构(不改变行为)
test: 测试
ci: CI 配置
chore: 杂项(依赖、构建脚本等)

可以带作用域,例如 fix(tun): ...、feat(settings): ...。


开发环境

工具链版本

工具 版本 来源
Rust 1.96.0(含 rustfmt、clippy 组件) rust-toolchain.toml(CI 中与之保持一致)
Node.js >= 20.18.1 根 package.json 的 engines.node
pnpm 10.12.1 根 package.json 的 packageManager(可通过 corepack enable 自动使用)

Linux 上还需要 Tauri 的系统依赖(libwebkit2gtk-4.1-dev、libappindicator3-dev、librsvg2-dev、patchelf 等),详见 BuildGuide。

仓库提供了 Dev Container 配置(.devcontainer/,基于 Ubuntu 22.04),容器创建时会自动安装上述系统依赖,并以 pnpm install --ignore-scripts 安装前端依赖。注意:--ignore-scripts 会跳过 prepare 脚本,因此容器内不会自动安装 Husky hooks,需要时请手动执行 pnpm exec husky。

常用命令(均在仓库根目录执行):

bash
pnpm install          # 安装依赖(同时通过 prepare 脚本安装 Husky hooks)
pnpm dev              # 开发模式(= pnpm --filter @zephyr/desktop tauri dev)
pnpm build            # 生产构建(先构建 CSS,再 tauri build)
pnpm build:css        # 仅构建 CSS(复制 shared 包、生成 design tokens、UnoCSS)

新增 IPC 命令时,前端使用的命令名常量集中定义在 packages/shared/src/index.js(COMMANDS),构建 CSS 时会被复制到 apps/desktop/src/_shared/。

Git Hooks (Husky)

pnpm install 时根 package.json 的 "prepare": "husky" 会安装 .husky/ 下的两个 hook:

Hook 执行内容
pre-commit cargo fmt --check --all → pnpm lint → pnpm typecheck
pre-push cargo clippy --workspace --all-targets -- -D warnings → cargo test --workspace → pnpm test

pre-push 在本次推送只包含删除分支(例如 git push --delete origin <branch>)时会自动跳过。

pre-commit 与 pre-commit.ci

.pre-commit-config.yaml 只包含快速的文件卫生检查(来自 pre-commit/pre-commit-hooks):

Hook 说明
trailing-whitespace 去除行尾空白(排除 .snap、.md 和 THIRD-PARTY-LICENSES.txt)
end-of-file-fixer 保证文件以换行结尾(排除 .snap 和 THIRD-PARTY-LICENSES.txt)
check-yaml / check-toml / check-json 语法校验
check-added-large-files 阻止误提交大文件(排除生成的许可证文件)

这些 hook 由 pre-commit.ci 在云端对 PR 运行,并会自动向 PR 推送修复提交(autofix_prs: true)。Rust/JS 的格式化与 lint 不在这里重复,统一交给 Husky hook 与 CI。hook 版本由 Renovate 更新,pre-commit.ci 自身的 autoupdate 设为最慢的 quarterly。

如需在本地运行:pre-commit run --all-files(需先安装 pre-commit)。

本地检查命令速查

提交 PR 前建议至少跑一遍下表的命令,它们与 CI 执行的检查一一对应:

检查 命令 对应 CI
Rust 格式 cargo fmt --all -- --check ci.yml / security.yml
Clippy cargo clippy --workspace --all-targets --locked -- -D warnings ci.yml / security.yml
Rust 测试 cargo test --workspace --locked ci.yml
i18n 完整性 pnpm check:i18n ci.yml / security.yml
ESLint pnpm lint ci.yml / security.yml
JSDoc 类型检查 pnpm typecheck ci.yml / security.yml
前端测试 pnpm test ci.yml / security.yml
第三方许可证 ./scripts/generate-licenses.sh(改动依赖后) ci.yml(licenses job)

测试

所有主要的新功能都必须附带加入测试套件的自动化测试。 这是一条通用策略(正式但不繁琐):新增功能或修复 Bug 时,请添加或更新能验证该行为的测试。

前端测试 (Vitest)

项目 配置
框架 Vitest 5.x
运行环境 happy-dom(apps/desktop/vitest.config.js)
测试文件 apps/desktop/src/**/*.test.js,与被测源码放在同一目录
测试辅助 apps/desktop/src/test/index-dom.js:readSrc() 读取源码文件、parseIndex() 以惰性方式解析 index.html(不加载脚本和样式),用于针对真实页面结构的测试
运行 pnpm test(或 pnpm --filter @zephyr/desktop test)
覆盖率 pnpm --filter @zephyr/desktop run test:coverage(@vitest/coverage-v8,输出 lcov + 文本报告)

测试文件与 src/test/ 不参与 tsc 类型检查(见 jsconfig.json 的 exclude),在 ESLint 中也放开了 no-console 与 no-unused-vars。

javascript
// 示例:apps/desktop/src/utils/format.test.js 风格
import { describe, it, expect } from 'vitest';
import { formatBytes } from './format.js';

describe('formatBytes', () => {
    it('把非数字输入视为 0 B', () => {
        expect(formatBytes(NaN)).toBe('0 B');
    });
});

Rust 测试

bash
# 运行整个 workspace 的测试(debug,与 CI 的 ci.yml 一致)
cargo test --workspace --locked

# 与 security.yml 的构建验证一致(release,排除无测试的 FFI crate)
cargo test --workspace --exclude zephyr-core-ffi --release --locked

# 更新 Golden 快照(insta)
INSTA_UPDATE=always cargo test --workspace --exclude zephyr-core-ffi
  • Workspace 成员:apps/desktop/src-tauri(crate 名 Zephyr)、crates/core(zephyr-core)、crates/core-ffi(zephyr-core-ffi)
  • 单元测试以内联 #[cfg(test)] mod tests 为主,部分模块使用独立的 *_tests.rs 文件(如 tray_tests.rs、prism_tests.rs)
  • Golden(快照)测试使用 insta,.snap 文件位于源码旁的 snapshots/ 目录。快照输入必须是确定性的(不能含时间戳、随机值或依赖环境的字符串);新增或变化的快照必须提交——CI 中缺失或过期的快照会直接导致失败
  • 测试中可以使用 unwrap/expect/索引/panic:desktop crate 在 lib.rs 顶部通过 #![cfg_attr(test, allow(...))] 统一放行,其他测试模块可用 #[allow(clippy::unwrap_used)] 等局部放行
  • 测试辅助依赖:mockall、tempfile、insta、tokio(test-util)

Windows 注意:desktop crate 的 lib 测试二进制在 Windows 上缺少 Tauri 为应用嵌入的清单,加载时会以 STATUS_ENTRYPOINT_NOT_FOUND 退出。因此 CI 在 Windows 上只运行其他 workspace 成员的测试(cargo test --workspace --exclude Zephyr),desktop crate 仅编译测试(cargo test -p Zephyr --no-run)。在 Windows 本地开发时也请这样运行。

覆盖率 (Codecov)

覆盖率在 ci.yml 中生成并通过 OIDC 上传到 Codecov:

来源 工具 Codecov flag
Rust cargo llvm-cov --workspace --locked --lcov rust
前端 Vitest test:coverage(apps/desktop/coverage/lcov.info) frontend

codecov.yml 中 project 与 patch 两种状态都设为 informational: true:覆盖率会显示在 PR 上,但不会阻止合并。目前没有设置强制的覆盖率阈值。


代码规范

Rust 代码规范 (rustfmt + Clippy)

工具 配置 命令
rustfmt 默认规则(仓库中没有 rustfmt.toml) cargo fmt --all
Clippy 根 Cargo.toml 的 [workspace.lints.clippy] cargo clippy --workspace --all-targets -- -D warnings
Clippy 全局参数 .clippy.toml:avoid-breaking-exported-api = true、cognitive-complexity-threshold = 25 —

所有成员 crate 通过 [lints] workspace = true 继承同一套规则,保证本地与 CI 完全一致。[workspace.lints.clippy] 共配置了 169 条 deny 级规则(含 correctness、suspicious 两个完整 lint 组)和 4 条 allow(needless_pass_by_value、multiple_crate_versions、missing_errors_doc、missing_panics_doc),按 9 层组织:

层级 主题 deny 数 代表规则
第一层 正确性 & 安全性 16 correctness/suspicious 组、unwrap_used、expect_used、panic、indexing_slicing、undocumented_unsafe_blocks、multiple_unsafe_ops_per_block、map_err_ignore、cast_possible_truncation
第二层 并发 & 内存安全 12 await_holding_lock、future_not_send、rc_mutex、clone_on_ref_ptr、large_futures
第三层 性能(内存分配) 23 redundant_clone、vec_init_then_push、large_enum_variant、format_push_string
第四层 性能(CPU & 算法) 20 uninlined_format_args、trivially_copy_pass_by_ref、missing_const_for_fn
第五层 性能(迭代器 & 闭包 & 循环) 27 redundant_closure、manual_filter_map、needless_collect
第六层 性能(内存操作 & 数据结构) 9 significant_drop_tightening、let_underscore_lock
第七层 性能(类型系统 & 编译) 13 derive_partial_eq_without_eq 等
第八层 代码质量(冗余 & 可读性 & 风格) 43 冗余与风格类规则
第九层 正确性辅助(防止静默丢弃 & 潜在 bug) 6 manual_ok_or、wildcard_enum_match_arm、used_underscore_binding

补充说明:

  • needless_pass_by_value 在 workspace 层为 allow(crates/core、crates/core-ffi 通过 UniFFI 导出,参数必须是 owned 类型),desktop crate 在 src/lib.rs 顶部用 #![deny(clippy::needless_pass_by_value)] 重新启用
  • 所有 unsafe 块必须写 // SAFETY: 注释,且每个块只做一个 unsafe 操作
  • CI 中 Clippy 以 -D warnings 运行,任何警告都会导致失败

Source: 根 Cargo.toml、.clippy.toml、apps/desktop/src-tauri/src/lib.rs

前端代码规范 (ESLint + 类型检查)

工具 配置 命令
ESLint 10 apps/desktop/eslint.config.mjs(Flat Config) pnpm lint
TypeScript(JSDoc 类型检查) apps/desktop/jsconfig.json:checkJs: true、strict: true pnpm typecheck
Prettier 仓库中没有 Prettier 配置,使用编辑器默认格式化 —

ESLint 配置要点:

  • 内联 no-unsanitized 插件(替代外部 eslint-plugin-no-unsanitized,避免 pnpm 严格模式下的依赖解析问题):
    • no-unsanitized/property:禁止把未经转义的内容赋给 innerHTML / outerHTML
    • no-unsanitized/method:禁止把未经转义的内容传给 insertAdjacentHTML / document.write / writeln
    • 视为安全的写法:字面量、无插值模板字符串、html / safeHtml 标签模板,以及 escapeHtml、escapeAttr、sanitizeHtml、_esc 等转义函数的调用结果
  • 安全规则:no-eval、no-implied-eval、no-new-func、no-script-url
  • 正确性规则:no-undef、no-redeclare、no-unreachable、no-implicit-globals、no-throw-literal、no-prototype-builtins、no-unsafe-optional-chaining 等 40 余条
  • 冗余操作:no-useless-* 系列、no-duplicate-imports
  • 代码卫生:no-console: error(仅 src/utils/logger.js 与测试文件例外,请使用 utils/logger.js 提供的 logger);no-unused-vars: error,以 _ 开头的变量/参数/catch 变量除外;no-empty 允许空 catch
  • 刻意关闭:eqeqeq、no-var、prefer-const
  • 全局变量来自 globals.browser,另声明了 jsyaml、__TAURI__、__TAURI_INTERNALS__
  • 忽略:node_modules、src-tauri、target、dist、src/js-yaml.min.js、src/_shared/**、src/cm6.bundle.js 以及生成的 CSS 文件

确有必要绕过 no-unsanitized 时,必须使用带理由的 // eslint-disable-next-line no-unsanitized/property -- <理由> 注释,并确保插值已经过 escapeHtml() 处理。


翻译 (i18n) 贡献

所有界面文案都集中在 apps/desktop/src/i18n.js 的 translations 对象中,当前包含 14 种语言:

代码 语言 方向
en English(源语言) LTR
zh 简体中文 LTR
ja 日本語 LTR
ko 한국어 LTR
ru Русский LTR
es Español LTR
fa فارسی RTL
tk Türkmen LTR
my မြန်မာ LTR
ar العربية RTL
ur اردو RTL
sw Kiswahili LTR
tr Türkçe LTR
vi Tiếng Việt LTR

i18n 系统特性:@@变量@@ 插值、基于 Intl.PluralRules 的 CLDR 复数(复数文案写成 { one: ..., other: ... } 这类对象)、缺失键回退到英文、QA 模式(显示原始键名)、RTL 自动检测(isRTL() 对 ar、he、fa、ur 返回 true,并同步 <html> 的 lang/dir 属性)。首次启动时根据系统语言自动选择,没有对应翻译时使用英文。

修改或新增文案

  1. 先在 en 中添加或修改键(英文是源语言,检查脚本以它为基准)
  2. 为全部 14 种语言补齐同名键——CI 要求每种语言都完整
  3. HTML 中通过 data-i18n / data-i18n-placeholder 等属性引用的键,必须存在于 en 中
  4. 删除控件时同时删除其不再使用的键(检查脚本会报告"孤儿键")
  5. 运行 pnpm check:i18n 确认 Total issues: 0

pnpm check:i18n(packages/scripts/check-i18n.js)以静态递归下降解析器读取 translations(不执行任何代码),以下任何一项都会让它以退出码 1 失败(CI 阻断):

检查项 说明
缺失键 en 有而目标语言没有(运行时会回退到英文)
空字符串 键存在但值为空(运行时显示空白,不会回退)
类型不匹配 en 是字符串而目标语言不是(运行时会显示原始键名)
孤儿键 目标语言有而 en 没有
重复键 同一语言块内重复定义
复数类别 目标语言缺少其 CLDR 规则要求的复数形式
data-i18n 覆盖 HTML/JS 中使用的键在 en 中不存在

"定义了但该语言复数规则永远用不到的复数形式"只作为提示信息输出,不算问题。

新增一种语言

  1. 在 translations 中新增该语言代码的完整语言块
  2. 在 apps/desktop/src/index.html 的语言选择器中添加该语言(自定义下拉列表中的 data-lang-value 按钮,以及对应的 <option>,两处都要加)
  3. 若为从右到左书写的语言,确认 isRTL() 覆盖了它
  4. 更新 i18n.js 中 JSDoc 的语言代码联合类型(如 'en'|'zh'|...,位于 i18n.js 与 ui/logs.js),否则 pnpm typecheck 可能报错
  5. 运行 pnpm check:i18n、pnpm typecheck、pnpm test

第三方许可证文件

每个发布包都附带 apps/desktop/src-tauri/THIRD-PARTY-LICENSES.txt(也是应用内 设置 → 关于 → 第三方许可证 查看器的数据来源)。该文件由 scripts/generate-licenses.sh 生成,请勿手动编辑:

部分 来源
Part 1 — Rust 组件 cargo about(配置 about.toml / 模板 about.hbs,基于 Cargo.lock)
Part 2 — JavaScript 组件 scripts/generate-licenses-js.mjs(基于 pnpm-lock.yaml)
Part 3 — 内置组件与其他材料 手工维护的 scripts/third-party-assets-licenses.txt(mihomo 内核、Geo 数据、子集化字体、内置库等)

改动了 Rust 或 JS 依赖(包括 lockfile)后,请在本地重新生成并提交:

bash
# 前置:cargo-about(CI 使用 0.9.2)、Rust 工具链、Node.js、pnpm,并已执行 pnpm install
./scripts/generate-licenses.sh

ci.yml 的 licenses job 会重新生成该文件,若与提交的版本不一致则失败。about.toml 的 accepted 列表决定 cargo about 接受哪些许可证;新增使用其他许可证的依赖时需要同时评估并更新它与 deny.toml。


CI/CD 流水线

CI 工作流 (ci.yml)

ci.yml 在所有 PR 以及推送到 main 时运行,提供快速反馈(debug 构建):

Job 平台 内容
frontend ubuntu-latest pnpm install --frozen-lockfile --ignore-scripts → pnpm check:i18n → pnpm lint → pnpm typecheck → test:coverage → 上传 Codecov(flag frontend)
licenses ubuntu-latest 运行 scripts/generate-licenses.sh,THIRD-PARTY-LICENSES.txt 有差异即失败
rust-linux ubuntu-latest cargo fmt --all -- --check → cargo clippy --workspace --all-targets --locked -- -D warnings → cargo llvm-cov --workspace --locked(测试 + 覆盖率)→ 上传 Codecov(flag rust)
rust-platform windows-latest、macos-latest cargo test --workspace --locked(Windows 上排除 desktop crate 的测试运行,仅编译,见上文)

insta 在 CI 中默认 INSTA_UPDATE=no,缺失或过期的快照会直接导致测试失败。

安全扫描工作流 (security.yml)

security.yml 在推送到 main / dev、向 main 发起的 PR、每日 UTC 03:00(0 3 * * *)以及手动触发时运行。所有 Action 都固定到不可变的 commit SHA。

%%{init: {'themeVariables': {'fontSize': '10px', 'nodeBorder': '1px', 'clusterBorder': '1px', 'edgeLabelHeight': '10px'}, 'flowchart': {'curve': 'basis', 'nodeSpacing': 15, 'rankSpacing': 25}}}%%
graph LR
    A[Push / PR / Cron] --> R
    A --> S
    A --> B

    subgraph R[Rust 检查]
        R1[rust-dependency-audit]
        R2[cargo-deny]
        R3[rust-clippy]
        R4[rust-fmt]
    end

    subgraph S[依赖与安全]
        S1["frontend-audit (仅 PR)"]
        S2["dependency-review (仅 PR)"]
        S3[semgrep]
        S4[secret-scan]
        S5[scorecard]
        S6[tauri-security]
        S7[sbom]
    end

    subgraph B[构建验证]
        B1[build-verification]
        B2[macos-build-check]
        B3[windows-build-check]
    end

    R3 --> B
    R4 --> B
Job 说明
rust-dependency-audit cargo audit(忽略列表见 .cargo/audit.toml)
cargo-deny cargo deny check:许可证、来源、禁用项与安全公告(见 deny.toml)
rust-clippy cargo clippy --all-targets -- -D warnings
rust-fmt cargo fmt --all -- --check
frontend-audit 仅 PR:actions/dependency-review-action,moderate 及以上漏洞失败,并拒绝 GPL-3.0 / AGPL-3.0 许可证的新依赖
dependency-review 仅 PR:同一 Action,high 及以上漏洞失败,同样拒绝 GPL-3.0 / AGPL-3.0
semgrep Semgrep 1.170.0,--config auto + .semgrep.yml,结果以 SARIF 上传到 GitHub Security(该步骤 continue-on-error,不阻断)
secret-scan Trufflehog + 正则源码扫描(见 Secret Detection)
scorecard OSSF Scorecard 供应链健康检查,SARIF 上传 + 保留 90 天的 artifact
tauri-security 检查 tauri.conf.json(devtools、通配 WebSocket 端口、withGlobalTauri)、capabilities 文件以及 lockfile 是否存在(缺少 pnpm-lock.yaml 时失败)
build-verification 依赖 clippy/fmt:check:i18n → ESLint → typecheck → build:css → cargo build --workspace --exclude zephyr-core-ffi --release --locked → release 测试并校验 Golden 快照与 Cargo.lock 未被改动 → 前端测试
macos-build-check 依赖 clippy/fmt:在 macOS runner 上 cargo check x86_64 与 aarch64 两个目标
windows-build-check 依赖 clippy/fmt:在 Windows runner 上 cargo check --target x86_64-pc-windows-msvc,覆盖 Windows 专属代码
sbom 用 cargo-cyclonedx 与 cdxgen 生成 Rust + NPM 的 CycloneDX SBOM,合并后作为 artifact 保留 90 天

其他工作流

工作流 触发 说明
codeql.yml 推送 / PR 到 main、每周一 UTC 02:00、手动 CodeQL 分析 javascript 与 rust
gitguardian.yml 推送到 main/dev、PR 到 main、手动 ggshield 密钥扫描;未配置 GITGUARDIAN_API_KEY 时跳过
snyk.yml package.json / pnpm-lock.yaml / pnpm-workspace.yaml 变化时的推送与 PR、每周一 UTC 04:00、手动 snyk test --all-projects(high 及以上)+ Snyk Code;未配置 SNYK_TOKEN 时跳过
test-badges.yml 推送到 main、手动 更新 README 中 Rust / JS 测试数徽章
deploy-demo.yml apps/desktop/src/** 等变化时推送到 main、手动 部署 GitHub Pages 演示页
release.yml 推送 v* tag 构建并发布,见发布流程
update-winget.yml / update-scoop.yml / update-aur.yml Release 发布后 更新 winget、Scoop、AUR 包
mac-debug.yml 仅手动 维护者用:在 macOS 26 runner 上构建当前 ref,并通过 Tailscale 提供 SSH + VNC 远程桌面,用于在真机环境点测未发布代码(依赖仓库所有者的密钥)
test-signing.yml / mobile.yml 仅手动 代码签名流程测试 / 移动端构建

Semgrep 自定义规则

CI 通过 semgrep scan --config auto --config .semgrep.yml 同时加载官方规则集和项目的自定义规则。.semgrep.yml 当前包含 4 条自定义规则:

# 规则 ID 语言 严重程度 说明
1 rust-unsafe-block Rust INFO 审计所有 unsafe 块(排除测试与示例)
2 rust-hardcoded-crypto-constant Rust WARNING 检测硬编码的密钥 / 凭证常量
3 js-insertadjacenthtml JS/TS INFO 标记 insertAdjacentHTML 的使用
4 js-document-write JS/TS WARNING 检测 document.write() 调用

innerHTML 等 DOM 注入由 ESLint 的内联 no-unsanitized 规则覆盖(能识别安全写法);Rust 侧的命令执行与 unsafe 用法主要由 Clippy 的 AST 级 lint 把关。

Source: .semgrep.yml


Secret Detection 多层检测策略

%%{init: {'themeVariables': {'fontSize': '9px', 'nodeBorder': '1px', 'clusterBorder': '1px', 'edgeLabelHeight': '8px'}, 'flowchart': {'curve': 'basis', 'nodeSpacing': 12, 'rankSpacing': 15}}}%%
flowchart TD
    A[代码推送 / PR] --> B["Trufflehog<br/>完整 Git 历史<br/>--only-verified"]
    A --> C["正则源码扫描<br/>Rust / JS 硬编码凭证"]
    A --> D["GitGuardian ggshield<br/>(配置密钥后启用)"]
    B --> E{发现泄露?}
    C --> E
    D --> E
    E -->|是| F[检查失败]
    E -->|否| G[通过]
层 位置 说明
Trufflehog security.yml secret-scan 扫描完整 Git 历史(fetch-depth: 0),--only-verified 只报告经验证的真实泄露
正则扫描 security.yml secret-scan Rust:(password|passwd|pwd|secret) 赋值 8 位以上字符串、(api_key|apikey|secret_key|private_key|access_token) 赋值 20 位以上字符串;JS:(token|auth|bearer|api_key) 赋值 20 位以上字符串(排除 test/example/placeholder/localhost 等);同时检查 .gitignore 是否包含 *.env、*.pem、*.key 等模式(缺失仅警告)
GitGuardian gitguardian.yml ggshield 扫描推送与 PR 的提交范围

Source: .github/workflows/security.yml、.github/workflows/gitguardian.yml


许可证合规 (deny.toml)

cargo-deny 通过根目录的 deny.toml 管理依赖审计策略:

toml
[graph]
targets = ["x86_64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-pc-windows-msvc", "aarch64-apple-darwin"]

[advisories]
# Review by: 2027-12-31
ignore = [
  # gtk-rs GTK3 绑定不再维护;修复需要 Tauri 迁移到 GTK4
  "RUSTSEC-2024-0411", "RUSTSEC-2024-0412", "RUSTSEC-2024-0413", "RUSTSEC-2024-0414",
  "RUSTSEC-2024-0415", "RUSTSEC-2024-0416", "RUSTSEC-2024-0417", "RUSTSEC-2024-0418",
  "RUSTSEC-2024-0419", "RUSTSEC-2024-0420",
  # glib 0.18.x VariantStrIter(被 gtk-rs GTK3 锁定)
  "RUSTSEC-2024-0429",
  # h2 空 DATA 帧(经 Tauri 的 reqwest/hyper 间接依赖,等待上游)
  "RUSTSEC-2026-0258",
]
unmaintained = "workspace"

[licenses]
allow = [
  "MIT", "Apache-2.0", "Apache-2.0 WITH LLVM-exception",
  "BSD-2-Clause", "BSD-3-Clause", "ISC",
  "Unicode-DFS-2016", "Unicode-3.0", "OpenSSL", "Zlib", "MPL-2.0",
  "LGPL-3.0", "0BSD",          # Tauri / gtk-rs 生态
  "CC0-1.0",                   # notify(经 clash-prism-core)
  "bzip2-1.0.6",               # libbz2-rs-sys(经 zip → bzip2)
  "BSL-1.0",                   # clipboard-win(经 arboard)
]

[bans]
multiple-versions = "warn"
wildcards = "deny"

[sources]
unknown-registry = "deny"
unknown-git = "deny"
部分 策略
[advisories] 安全漏洞默认阻断;unmaintained = "workspace" 只对 workspace 直接依赖的"不再维护"公告报错,间接依赖不阻断;ignore 中的公告均附有原因说明,复查期限为 2027-12-31(与 osv-scanner.toml 的 ignoreUntil 对齐,.cargo/audit.toml 中维护同一组忽略项)
[licenses] 仅允许白名单中的许可证。MPL-2.0 为文件级弱 Copyleft,LGPL-3.0 用于 GTK 系统库相关依赖
[bans] 禁止通配符版本依赖;同一 crate 存在多个版本时仅警告
[sources] 只允许 crates.io,拒绝未知注册源与 Git 依赖

Source: deny.toml


依赖更新 (Renovate)

依赖更新由 Renovate 负责(仓库中已不再有 Dependabot 配置),配置文件为根目录的 renovate.json:

配置 值
预设 config:recommended、:enablePreCommit(同时更新 .pre-commit-config.yaml 中的 hook 版本)
时间表 Asia/Shanghai 时区,每周一早 6 点前
Dependency Dashboard 启用
公共标签 dependencies
规则 说明
minor + patch 更新 合并到 patch-and-minor 组,自动合并(PR 方式,平台自动合并)
GitHub Actions 的 minor + patch 单独成组 github-actions-minor-patch,不自动合并;所有 Action 固定到 commit digest(pinDigests)
Cargo 额外标签 rust
npm 额外标签 javascript,发布满 3 天后才会提出更新(minimumReleaseAge)
GitHub Actions 额外标签 github-actions
webview2-com 禁用更新:该 crate 必须与 tauri/wry 使用的版本完全一致(见 apps/desktop/src-tauri/Cargo.toml 中的说明),升级 Tauri 时手动同步

此外,pnpm-workspace.yaml 的 overrides 中固定了若干存在安全公告的间接 npm 依赖版本(如 brace-expansion、ws、undici、tar 等),并通过 onlyBuiltDependencies 只允许 esbuild 与 @parcel/watcher 执行构建脚本;CI 统一使用 pnpm install --ignore-scripts 再显式 pnpm rebuild esbuild @parcel/watcher。


发布流程

推送版本 tag(v*)时触发 .github/workflows/release.yml,完成版本同步、内核获取与校验、多平台编译、签名以及产物上传。

%%{init: {'themeVariables': {'fontSize': '9px', 'nodeBorder': '1px', 'clusterBorder': '1px', 'edgeLabelHeight': '8px'}, 'flowchart': {'curve': 'basis', 'nodeSpacing': 12, 'rankSpacing': 15}}}%%
flowchart TD
    A["推送版本 Tag<br/>例如 v2.5.0"] --> V["版本同步<br/>更新 3 个文件的版本号"]
    V --> L["重新生成<br/>THIRD-PARTY-LICENSES.txt"]
    L --> M["获取最新 Mihomo Release<br/>SHA256 校验(缺少摘要即中止)"]
    M --> G["下载 geoip.dat / geosite.dat / Country.mmdb<br/>SHA256 校验"]
    G --> F["Full 构建<br/>内置内核 + Geo 数据"]
    F --> LT["Lite 构建<br/>移除内置资源后再次构建"]
    LT --> P["便携版打包<br/>复用 Full 二进制"]
    P --> W{Windows?}
    W -->|是| SP["SignPath Authenticode 签名<br/>安装包 + 便携版 exe"]
    W -->|否| MS
    SP --> MS["Minisign 签名所有产物<br/>生成 .minisig"]
    MS --> U["上传到 Draft Release"]
    U --> SB["SBOM Job<br/>附加 CycloneDX SBOM"]
    SB --> K[维护者审核后发布]
    K --> PKG["winget / Scoop / AUR<br/>自动更新"]

构建矩阵

Runner Target 架构
macos-latest aarch64-apple-darwin arm64(Apple Silicon)
macos-latest x86_64-apple-darwin x64(Intel Mac)
ubuntu-22.04 默认(x86_64-unknown-linux-gnu) x64
ubuntu-22.04-arm 默认(aarch64-unknown-linux-gnu) arm64
windows-latest 默认(x86_64-pc-windows-msvc) x64
windows-latest aarch64-pc-windows-msvc arm64

Release 流水线使用 stable Rust 工具链(CI 使用固定的 1.96.0)。为避免缓存投毒,持有签名密钥的发布 job 不使用 pnpm 缓存,每次都从干净环境安装依赖。

版本同步机制

构建前,流水线从 tag 中去掉 v 前缀得到版本号,同步写入以下 3 个文件:

# 文件 工具 位置
1 apps/desktop/src-tauri/tauri.conf.json jq .version
2 apps/desktop/src-tauri/Cargo.toml sed [package] 的 version
3 apps/desktop/package.json jq .version

因此仓库中提交的版本号(如 1.2.0)只是占位值,发布版本以 tag 为准,贡献者无需手动修改版本号。

Mihomo 内核获取与安全校验

  1. 版本发现:使用 GITHUB_TOKEN 调用 GitHub API 获取 MetaCubeX/mihomo 的最新 Release tag,失败则中止
  2. 下载与校验:下载对应平台的归档后,从 GitHub API 的 Release Assets 中读取该文件的 sha256: 摘要并与本地 sha256sum 比对;摘要不可用或不匹配都会中止构建。解压后的内核命名为 zephyr-mihomo(Windows 为 zephyr-mihomo.exe),放入 apps/desktop/src-tauri/bundled/
  3. Geo 数据:从 MetaCubeX/meta-rules-dat 最新 Release 下载以下文件,并用同名 .sha256sum 文件校验:
文件 用途
geoip.dat IP 地理位置数据库,用于路由规则匹配
geosite.dat 域名分类数据库,用于域名规则匹配
Country.mmdb MaxMind 格式 GeoIP 数据库

Full / Lite / 便携版

特性 Full Lite 便携版
Mihomo 内核 内置 不含 内置
Geo 数据 内置 不含 内置
安装方式 安装程序 安装程序 解压即用(Windows .zip / Linux .tar.gz)
数据位置 系统应用数据目录 系统应用数据目录 程序所在目录(由 .portable 标记文件启用)
开机自启 ✅ ✅ ❌
应用自更新 ✅ ✅ ❌(可单独更新内核,应用需从发布页下载)

Lite 构建在 Full 构建之后删除 bundled/ 中的资源再编译一次;便携版直接复用 Full 构建的二进制(Windows 使用已签名的 exe,Linux 使用 Full AppImage),无需第三次编译,并附带内核、Geo 数据、THIRD-PARTY-LICENSES.txt 与 .portable 标记。

代码签名与产物签名

签名 范围 说明
Authenticode(SignPath) Windows 的 NSIS 安装包、MSI(Full/Lite)以及便携版 exe 由 SignPath Foundation 提供证书,每次签名请求由维护者人工批准,详见 CODE_SIGNING_POLICY.md
Minisign(Ed25519) 所有平台的全部发布产物 每个文件生成同名 .minisig;应用内的客户端更新会校验 SHA256 与该签名

macOS 与 Linux 产物没有平台代码签名(未做 Apple 公证)。

产物命名与上传

产物在 Tauri 默认文件名的基础上追加 -full / -lite 后缀,例如:

平台 Full Lite 便携版
Windows Zephyr_{版本}_{arch}-setup-full.exe、Zephyr_{版本}_{arch}_*-full.msi ...-setup-lite.exe、...-lite.msi Zephyr_{arch}-portable.zip
macOS Zephyr_{版本}_{arch}-full.dmg、Zephyr.app 压缩包 *-full.tar.gz ...-lite.dmg、...-lite.tar.gz —
Linux *-full.deb、*-full.rpm、*-full.AppImage *-lite.deb、*-lite.rpm、*-lite.AppImage Zephyr-linux-{arch}-portable.tar.gz

({arch} 的具体写法由 Tauri 打包器决定,如 Windows 的 x64/arm64、Debian 包的 amd64/arm64。)

所有产物及其 .minisig 通过 softprops/action-gh-release 上传到 Draft Release;独立的 SBOM job 再把 CycloneDX SBOM(*.cdx.json)附加到同一 Release。维护者审核后手动发布,发布后 winget / Scoop / AUR 的更新工作流自动运行。

Source: .github/workflows/release.yml

版本号规范

Zephyr 遵循 语义化版本:

  • 主版本号 (MAJOR):不兼容的变更
  • 次版本号 (MINOR):向下兼容的功能新增
  • 修订号 (PATCH):向下兼容的问题修复

Tag 格式:v{MAJOR}.{MINOR}.{PATCH},例如 v2.5.0。


贡献者名单与 AI 代理日志

  • 贡献者名单:.all-contributorsrc 记录所有贡献者(包括参与编码、审查、安全扫描与基础设施的人和 bot),README 中的头像表格依据它手动维护(中英文 README 各一份)。新增贡献者时请同时更新 .all-contributorsrc 和两份 README 的表格
  • .jules/ 目录:存放 Google Jules 代理在本仓库工作时积累的经验日志,按角色分为 bolt.md(性能)、palette.md(UX 与无障碍)、sentinel.md(安全)。这些条目记录了项目约定(例如:自定义 .ios-switch 开关要用 aria-labelledby / aria-describedby 关联标题与说明;所有接收用户 YAML 的 IPC 命令都要经过危险键清洗;openUrl 只允许 http:/https:),人类贡献者同样可以参考
  • ImgBot:.imgbotconfig 配置每周一次的无损图片优化 PR

Issue 提交指南

提交前检查

  1. 已搜索现有 Issues,确认问题未被报告
  2. 已阅读 FAQ,确认不是已知问题
  3. 使用的是最新版本的 Zephyr
  4. 问题可以在最新版本中复现

安全漏洞请勿公开提交 Issue,请按 SECURITY.md 通过 GitHub 私密漏洞报告提交(见 Security)。

Bug 报告建议格式

markdown
## Bug 描述
[清晰简洁地描述 Bug]

## 复现步骤
1. [步骤 1]
2. [步骤 2]
3. [步骤 3]

## 预期行为
[描述你期望发生的情况]

## 实际行为
[描述实际发生的情况,包括界面上的错误提示原文]

## 环境信息
- 操作系统: [例如 Windows 11 / macOS 15 / Ubuntu 24.04]
- 架构: [x64 / arm64]
- Zephyr 版本: [例如 v2.5.0]
- 安装版本: [Full / Lite / 便携版]
- Mihomo 内核版本: [设置 → 关于 中显示]
- 代理方式: [系统代理 / TUN]

## 日志
[内核日志与应用日志的相关片段(可在日志页导出),请使用代码块并去除订阅链接、节点地址等敏感信息]

## 截图
[如有,附上截图]

功能请求建议格式

markdown
## 功能描述
[清晰描述你希望添加的功能]

## 动机
[为什么需要这个功能?它解决了什么问题?]

## 建议的实现方式
[如有想法,描述你建议的实现方式]

## 替代方案
[你考虑过的其他替代方案]

感谢你的贡献!如有任何问题,欢迎在 Issue 中讨论。本项目以 MIT 许可证 发布,提交贡献即表示你同意以 MIT 许可证授权你的贡献。

↑↓ 选择↵ 打开