Repository navigation
Backend Guidelines
Rust 2021 + Tauri v2。关键 crate:rusqlite(bundled)、arboard、sha2、serde、thiserror、anyhow。
- 入口:
main.rs→lib.rs::run()→ Tauri Builder - 状态:
AppState经tauri::Builder::manage()注入,命令中用State<AppState>取 - 注释中文,格式化
cargo fmt+cargo clippy --all-targets -- -D warnings
src-tauri/src/
├── main.rs 仅调用 lib::run()
├── lib.rs 接线层:插件、manage()、invoke_handler()
├── models.rs 共享类型枢纽
├── commands.rs AppState 与稳定 re-export
├── commands/ 按功能拆分的 #[tauri::command]
└── <domain>.rs 或 <domain>/
- 一个模块负责一件事。领域有独立生命周期/状态边界时才用子目录(capture、pin、paste、translation、commands)。
-
models.rs是共享类型枢纽,跨模块的数据结构都在这里,统一#[derive(Serialize, Deserialize, Clone, Debug)]。 -
commands是 IPC 边界,命令只做转发,真正逻辑在领域模块。 -
lib.rs只接线,不放业务逻辑。
命名:文件 snake_case.rs,类型 PascalCase,函数 snake_case,常量 UPPER_SNAKE_CASE。Tauri 命令名用 snake_case(JS 侧自动转 camelCase)。
领域错误用 thiserror,内部传播用 ?,在 IPC 边界转成 String:
#[derive(Error, Debug)]
pub enum StorageError {
#[error("数据库操作失败: {0}")]
Database(#[from] rusqlite::Error),
#[error("内容哈希冲突: {0}")]
DuplicateContent(String),
}每个领域错误带稳定的 code();顶层 error.rs 聚合,提供 domain()/code()/identifier(),日志标识形如 paste.portal_start_rejected。
区分两个级别,避免把预期路径记成故障:error::report(warn)是真实故障,error::note(info)是预期路径。
监听线程遇到瞬时错误应记日志并继续:
loop {
match clipboard.get_text() {
Ok(text) => { /* process */ }
Err(e) => log::warn!("剪贴板读取失败: {}", e),
}
std::thread::sleep(poll_interval);
}-
I/O 或用户输入上
unwrap()—— 用?或显式处理 -
unwrap()仅允许用于Mutex::lock()(poisoned 即不可恢复)和编译期保证 - 向前端暴露内部错误细节
- 静默吞掉错误(至少要记日志)
SQLite + FTS5,无 ORM,裸 SQL 配参数绑定。schema 与索引见剪贴板管线。
| 规则 | 说明 |
|---|---|
始终用 ? 占位符 |
绝不把用户输入拼进 SQL |
高频查询用 prepare_cached
|
利用 rusqlite 内建 LRU,避免重复编译 |
| 去重用 UPSERT |
ON CONFLICT(content_hash) DO UPDATE,不用先查后改 |
| 插入和删除都要同步 FTS | 删除用 FTS5 'delete' 命令 |
| 多步操作用事务 | 插入 + FTS 同步 + 清理必须原子 |
| 批量删除用有界 IN 子句 | 最多 500 个 id 一块,所有块共用同一外层事务 |
PRAGMA:journal_mode = WAL、cache_size = 128、temp_store = MEMORY。
存储模式:默认持久化到应用数据目录;:memory: 用于测试。
数据库含用户敏感内容。Unix 下 clips.db 及其 -wal/-shm 必须 0600;新库必须在交给 SQLite 打开前就以私有权限创建,启动时同时修复已有库的宽松权限。配置与 token 走同一私有文件模块,目录 0700、文件 0600。权限修复失败必须返回错误或记日志,不能以宽松权限静默运行。
优先 CLI 子进程,不用 -sys crate 动态链接。
理由:跨发行版 SONAME 不兼容(例如 libtesseract.so.4 vs .so.5)会让分发的 deb/AppImage 直接起不来。唯一例外是有 bundled feature 的 crate(把 C 库静态编进二进制)。
模式:
// 运行时检测
pub fn is_available() -> bool { /* tool --version */ }
// 调用,缺失时给友好错误
.map_err(|e| if e.kind() == ErrorKind::NotFound {
"工具不可用:请安装 xxx".to_string()
} else { format!("启动工具失败: {}", e) })?约定:
-
--version是真实子进程,同一用户操作里不得由能力检查和执行路径各探测一次;成功与失败都要进程内缓存 - 应用内安装成功、缓存路径启动时报
NotFound等状态变化必须使缓存失效 - CPU/内存重任务必须有全局并发上限;同一不可变输入的并发请求用 single-flight 合并
-
不得持有同步
Mutex跨越.await或子进程执行,锁只保护短暂的缓存/在途表变更 - 可选工具不放进包依赖,由应用内提示引导安装
- Release 阶段用
readelf -d审计动态依赖
log crate facade,消息用中文。
| 级别 | 用途 |
|---|---|
error! |
影响用户功能的不可恢复失败 |
warn! |
可恢复问题、瞬时失败 |
info! |
关键生命周期事件 |
debug! |
详细操作信息 |
trace! |
轮询循环等超高频 |
不许记录:剪贴板内容(可能含密码、密钥、PII)、完整图片数据(只记大小/哈希)、info 级别的内容哈希(短字符串的哈希可被反查,降到 debug)。
log::debug!("检测到新剪贴板内容,类型: {}, 大小: {} 字节", content_type, byte_size);cd src-tauri && cargo fmt
cd src-tauri && cargo clippy --all-targets -- -D warnings
cd src-tauri && cargo test| 模式 | 原因 | 替代 |
|---|---|---|
I/O 上 unwrap()
|
panic 崩溃 |
? / match
|
| SQL 字符串插值 | 注入 | params![] |
println!() 打日志 |
release 抓不到 |
log:: 宏 |
引用够用时 clone()
|
无谓分配 | 传 &
|
一律 pub
|
破坏封装 | 默认私有 |
unsafe |
本项目不需要 | 找安全替代 |
测试与实现同文件,用 #[cfg(test)] mod tests。存储层用 :memory: 覆盖 CRUD、FTS、去重、容量清理;配置层覆盖默认值、加载保存、非法输入。
持久性回归必须真正关闭并重开磁盘数据库——把同一个内存连接重新初始化不算重开。
这些是从真实缺陷里总结的硬约束,改动相关代码前先对一遍。
- 区分「已经观察到」与「成功写入」。编码或数据库暂时失败用有上限的退避,不能在成功前更新已完成标记,否则同一内容永远漏收。
- 应用主动复制的抑制身份要持续到对应内容被处理完;读取慢图片在写入锁外执行,前后核对写入代次,避免旧快照消耗新写入的抑制记录。
- 只有可靠的系统变更通知才能跳过整图读取。信号不可用时回退轮询;同一 owner 再次复制也是变化,不能只比较窗口 owner。无新事件时仍须执行已安排的失败重试。
- X11 写入的完成屏障必须在实际写连接上;其它连接的 roundtrip 只能排空自身事件。用可控服务端阻塞复现,不能用固定
sleep代替同步。
- 配置持久化必须返回错误,写入失败不能显示 Saved。快捷键绑定失败要恢复旧配置和绑定;回滚本身失败必须明确反馈。
- Portal 仅接纳异步请求时显示 pending,不能表述为「系统已启用」。
- 正在录制与仍待恢复是两个状态,必须分开。失败保留恢复意图;显式 Save 要等恢复完成并校验后端已暂停。同一恢复协议供取消与原生关闭共用,并合并并发关闭。原生窗口关闭不依赖 JS 初始化成功。
- 开发版不能仅凭自启动项存在就禁用它,必须证明启动命令属于当前开发程序;无法证明时保留。
- 先渲染再执行复制/保存/贴图。失败保留可恢复编辑或同一产物;可能已发生的外部副作用不能盲目重试。
- 取消、窗口销毁与异步输出共用精确身份,资源只清理一次。
- 重试报忙或 IPC 结果不确定时保留恢复状态,允许的动作只能收紧。只有确定的初始渲染失败才能恢复编辑。
- GTK 对象必须在 GTK 主线程操作。异步截图路径里不得创建 GTK 样式对象或遍历修改 widget 树——发生在首屏全屏化之前的 panic 会同时造成定位偏移和后续屏幕未创建,而单屏无头环境验收不到。
- 更新安装任务归应用进程持有,多个 WebView 只提交操作并读同一权威快照。开始安装在同一锁内 claim,窗口关闭不能销毁或重复创建任务。
- 状态成功返回与 catch 都要校验发起时的 revision;迟到的查询失败不能把已完成状态降回未知。
- 快捷键保存先按原生规范化身份检查内部重复,验证失败不得持久化。
- 权限解析先全局查 denied 再按窗口匹配 allowed,因此不能用窗口限定的 deny 来隔离某个窗口;应改用独立的 scoped allow 只授予需要的窗口。
- 主事件线程不能等待包含原生窗口 getter 的 worker 锁:getter 会派发消息并等待事件线程,形成互等。生命周期用非阻塞状态,业务放 worker 执行。
- 窗口控制命令核验调用者 label 与有效句柄,只操作自身窗口;原生 getter 前后检查生命周期,不能持有会话锁等待 UI。