贡献指南
感谢你对 Zephyr 项目的关注!Zephyr 是基于 Tauri v2 的 Mihomo GUI 代理客户端。本文档介绍贡献流程、开发环境、本地检查、测试与代码规范、翻译(i18n)贡献、CI/CD 流水线以及发布流程。
仓库根目录的 CONTRIBUTING.md 是贡献规则的权威来源;本页在其基础上补充了各项工具与流水线的实际配置。
目录
- 贡献流程
- 开发环境
- 测试
- 代码规范
- 翻译 (i18n) 贡献
- 第三方许可证文件
- CI/CD 流水线
- Semgrep 自定义规则
- Secret Detection 多层检测策略
- 许可证合规 (deny.toml)
- 依赖更新 (Renovate)
- 发布流程
- 贡献者名单与 AI 代理日志
- Issue 提交指南
贡献流程
项目采用标准的 GitHub Fork + Pull Request 流程:
- Fork 仓库到你的 GitHub 账号
- 克隆你的 Fork:
git clone https://github.com/<你的用户名>/Zephyr.git - 创建分支:
git checkout -b feature/your-feature-name - 按照下文的代码规范修改代码
- 在本地运行检查与测试(见本地检查命令速查)
- 使用清晰的提交信息提交
- 推送到你的 Fork:
git push origin feature/your-feature-name - 向
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。
常用命令(均在仓库根目录执行):
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。
// 示例: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 测试
# 运行整个 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/outerHTMLno-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 属性)。首次启动时根据系统语言自动选择,没有对应翻译时使用英文。
修改或新增文案
- 先在
en中添加或修改键(英文是源语言,检查脚本以它为基准) - 为全部 14 种语言补齐同名键——CI 要求每种语言都完整
- HTML 中通过
data-i18n/data-i18n-placeholder等属性引用的键,必须存在于en中 - 删除控件时同时删除其不再使用的键(检查脚本会报告"孤儿键")
- 运行
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 中不存在 |
"定义了但该语言复数规则永远用不到的复数形式"只作为提示信息输出,不算问题。
新增一种语言
- 在
translations中新增该语言代码的完整语言块 - 在
apps/desktop/src/index.html的语言选择器中添加该语言(自定义下拉列表中的data-lang-value按钮,以及对应的<option>,两处都要加) - 若为从右到左书写的语言,确认
isRTL()覆盖了它 - 更新
i18n.js中 JSDoc 的语言代码联合类型(如'en'|'zh'|...,位于i18n.js与ui/logs.js),否则pnpm typecheck可能报错 - 运行
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)后,请在本地重新生成并提交:
# 前置:cargo-about(CI 使用 0.9.2)、Rust 工具链、Node.js、pnpm,并已执行 pnpm install
./scripts/generate-licenses.shci.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 管理依赖审计策略:
[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 内核获取与安全校验
- 版本发现:使用
GITHUB_TOKEN调用 GitHub API 获取MetaCubeX/mihomo的最新 Release tag,失败则中止 - 下载与校验:下载对应平台的归档后,从 GitHub API 的 Release Assets 中读取该文件的
sha256:摘要并与本地sha256sum比对;摘要不可用或不匹配都会中止构建。解压后的内核命名为zephyr-mihomo(Windows 为zephyr-mihomo.exe),放入apps/desktop/src-tauri/bundled/ - 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 提交指南
提交前检查
安全漏洞请勿公开提交 Issue,请按 SECURITY.md 通过 GitHub 私密漏洞报告提交(见 Security)。
Bug 报告建议格式
## 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]
## 日志
[内核日志与应用日志的相关片段(可在日志页导出),请使用代码块并去除订阅链接、节点地址等敏感信息]
## 截图
[如有,附上截图]功能请求建议格式
## 功能描述
[清晰描述你希望添加的功能]
## 动机
[为什么需要这个功能?它解决了什么问题?]
## 建议的实现方式
[如有想法,描述你建议的实现方式]
## 替代方案
[你考虑过的其他替代方案]感谢你的贡献!如有任何问题,欢迎在 Issue 中讨论。本项目以 MIT 许可证 发布,提交贡献即表示你同意以 MIT 许可证授权你的贡献。