文章目录7 节
TieZ 是一个 Tauri 2 + React + Rust 的跨平台剪贴板管理器,3.5 万行 Rust 里包含采集管线、SQLite 仓储、主题系统、WebDAV/MQTT 同步,以及一条刻意独立于云同步的加密中继通道。本文讲它的分层边界、主题令牌体系和中继的密码学设计。
TieZ 的代码量在个人项目里算偏大的:src-tauri/src 下约 35,800 行 Rust,前端 114 个 TS/TSX 文件。大不是重点,重点在于它把"剪贴板采集""本地仓储""主题渲染""跨设备传输"这几件事分得比较清楚,尤其是把剪贴板中继(Clipboard Relay)和云历史同步做成了两条互不干扰的通道。这在实际使用里很关键:临时把一段文本丢到另一台机器,不应该意味着把整份历史记录上传。
一、分层与模块地图
1 | src-tauri/src/ |
技术栈上几个刻意的选择:
| 关注点 | 选型 |
|---|---|
| 剪贴板读写 | arboard(启用 wayland-data-control)+ clipboard-rs,兼顾 X11/XWayland 与实现 data-control 的 Wayland 合成器 |
| 本地存储 | rusqlite(bundled)+ 自管迁移,不依赖外部服务 |
| 凭据存储 | keyring 4.1.5(Windows Credential Manager / macOS Keychain / Linux Secret Service) |
| 加密 | chacha20poly1305(XChaCha20-Poly1305) |
| 局域网传输 | axum 0.8.8(multipart/ws) |
| MQTT | rumqttc + rustls |
| 二维码识别 | rqrr;图片处理 image |
发布构型也做了取舍:lto = true、codegen-units = 1、opt-level = "s"、strip、panic = "abort" —— 牺牲编译时间换包体与启动表现,同时用 portable / devtools 两个 feature 区分便携模式与调试模式。
窗口是"贴边小面板"而不是普通窗口:主窗口 352×380(最小 250×300),transparent、decorations: false、skipTaskbar: true、dragDropEnabled: false,配合 window-vibrancy 做 mica/acrylic 背景。CSP 写得比较收敛:default-src 'self',脚本/样式只额外放开 'unsafe-inline',connect-src 限定 'self' ws: http: https:;资源协议只允许 $HOME/**、$APPDATA/**、$APPLOCALDATA/**、$PROFILE/**;opener:allow-open-url 只允许打开 https://github.com/**。
二、主题系统:令牌 + 单一注册表
docs/THEME-SYSTEM.md 把主题拆成两层,规则是"共享 CSS 只负责结构与中性默认值,每个主题文件拥有自己的视觉语言":
- 注册表唯一:
src/shared/config/themes.ts是 id、多语言标签(zh/en/tw)和功能开关的唯一来源; - 自动装载:
src/styles/themes/load.ts自动 importthemes/*.css,所以"纯 CSS 主题"新增时不需要改任何加载器代码; - 运行时归一:未知主题 id 回落到
DEFAULT_THEME,并在html与body上同时挂theme-<id>类,主窗口与 compact-preview 窗口用同一个 id; - 令牌规模:约 100+ 个语义变量,按
--shell-*、--toolbar-*、--title-*、--input-*、--button-*、--card-*、--panel-*、--switch-*、--segmented-*、--modal-*、--tags-*、--menu-*、--queue-*分组; - 原生层单独出口:mica/acrylic 这类需要调系统 API 的效果只走
app/commands/ui_cmd.rs的set_theme,不把平台分支渗透进前端。
内置主题是 retro / mica / acrylic / sticky-note。文档里也诚实列了"主题债":Announcement.css、compact-mode.css、emoji.css、file-transfer.css 还没完全令牌化。
三、中继(Relay):把"发一段文本"和"同步全量历史"分开
docs/CLIPBOARD-RELAY.md 定义得很清楚:中继是显式的一次性传输,与云历史同步完全独立。
数据路径与语义:
- 收发双方配置同一 WebDAV 账号 + 同一个 relay 共享密钥,各自绑定
Send clipboard relay/Fetch clipboard relay两个全局快捷键(默认为空,不会偷偷占用按键); - Send 只上传当前
text/plain剪贴板值:不上传历史、标签、置顶状态、使用元数据、设置或本地路径; - Fetch 只把最新一条合格消息写回系统剪贴板,并标记这次写入是"应用发起",因此不会进入本地历史;
- 消息默认 10 分钟过期,协议校验收到的 TTL 不允许超过 24 小时,也会拒绝过大的时钟偏移;
- 远端队列上限 2000 个对象,过期与超量对象在正常收发流程中做尽力而为的清理。
密码学与凭据:
- 流量走独立的
<base-path>/relay/v1WebDAV 命名空间; - 32 字节共享密钥 + XChaCha20-Poly1305 加密消息正文,附加认证数据绑定消息 ID、发送者、目标列表、媒体类型、时间戳、算法标识和 WebDAV 文件名——也就是说,元数据不能在不被发现的情况下被改名或重放;
- ACK 文件同样加密并认证,未认证的 ACK 直接拒绝;
- 信封里没有明文的正文哈希或其他指纹,避免"看不到内容但能比对内容"的泄漏;
- 共享密钥存进操作系统凭据库,不通过通用设置 API 返回,也永不作为普通设置项落盘;生成密钥时只在原生写入成功后显示一次;便携模式下直接禁用中继,而不是退化成明文存 key;
- 因为应用的端到端加密保护不了 WebDAV 凭据、对象路径和流量特征,所以中继强制要求
https://的 WebDAV 地址。
投递语义写得很克制:从本地视角是 at-most-once。如果剪贴板写入成功但 ACK 上传失败,收据停在 copied_pending_ack,之后的 fetch 只重试 ACK、不会再次覆盖剪贴板;损坏、被篡改、密钥不符或未认证的对象会报错,而不是静默显示成"空收件箱"。Linux 上需要可用的用户态 D-Bus Secret Service(GNOME Keyring / KWallet),否则中继 fail closed,其他功能不受影响。
四、数据与迁移
- SQLite 仓储由
database.rs与infrastructure/repository/migrations.rs(855 行)共同管理,单是剪贴板仓储clipboard_repo.rs就有 2271 行,标签等实体拆成独立 repo,避免堆成一个巨型 DAO; - 迁移是版本化的(含 schema 版本与一次性迁移逻辑),采集侧有
pipeline.rs明确划分"读取 → 规范化 → 去重/掩码 → 入库 → 事件分发"的阶段; sensitive_align.rs+ 设置项负责敏感预览的自动掩码——在本地就掩掉,而不是依赖 UI 层不显示。
五、工程化程度
- 主校验命令:
npm test(前端)、npm run build、npm run test:rust; - 发布构型 + CI 组合(Rust 静态检查、clippy、测试、前端构建)意味着"能跑"不是验收标准,"能复现地跑"才是;
- 上游为社区 fork,README 明确要求发布二进制前先审查旧更新器端点、签名凭据与分发基础设施——这类清单化的提醒比"自行注意安全"有用得多。
六、限制
- 自动粘贴在 Linux 依赖
xdotool(X11) /wtype(Wayland),两者都不可用时只把内容留在剪贴板(不假装粘贴成功); - macOS 需要辅助功能与剪贴板权限;当前构建未做 Apple 签名/公证,首次启动需手动放行;
- 系统 OCR 依赖 Windows Runtime,仅在 Windows 可用(二维码识别是跨平台的);
- 中继仅支持 UTF-8 文本,单条上限 64 KiB。
小结
TieZ 最值得借鉴的两点:主题是带注册表的数据层,而不是散落的样式覆盖;临时传输与历史同步必须是两条通道,并且这条临时通道从密钥存储、加密模式到失败策略都按"最坏情况可见"来设计——包括明文指纹都不留。
仓库:sakuradairong/tiez-clipboard(GPL-3.0,社区维护 fork)