Skip to content

Translation And Security

51hhh edited this page Sep 16, 2026 · 1 revision

翻译与安全边界

翻译架构

translation/ 包含 provider 适配、超时与重试、request-id、内容选择和密钥管理。

  • 启用的多个服务按 spawn_blocking 并行执行
  • 单个服务失败作为数据返回,不中断其它服务的结果
  • 多服务结果卡互不覆盖

语言方向

direction.rs 在文本已经是目标语言时,按备选语言换向。未知的拉丁语系源语言仍尊重用户设定的目标语言。

陈旧响应保护

react/main/translationStore.ts 管理主预览的翻译状态、多服务结果卡、单服务重试与陈旧响应保护。翻译与朗读分别管理在途任务,同一个 request-id 的旧响应不能覆盖新结果。

这类竞态在快速切换条目时必然出现,是本项目修过多次的一类缺陷:搜索、分页、新增/删除事件、预览渲染、翻译,每一条异步路径都需要代次隔离。

TTS

tts.rs 由 Rust 取回音频后以 data URL 播放,webview 不直接请求外部端点。文本长度上限 200 字符。

这样超时、大小上限与端点白名单只在 translation/http.rs 一处实现,webview 的 CSP 也不必为第三方主机放开 media-src。

安全规则

以下每条都是硬约束,不是建议。

敏感内容

  • 敏感条目在 Rust 内容选择阶段拒绝翻译
  • 朗读走同一条内容选择路径,因此同样被拒绝
  • 敏感条目从不进入翻译,因此也不会产生翻译记录

判定放在 Rust 而不是前端,因为前端可以被绕过,而内容选择是所有消费者的共同入口。

图片

图片翻译只把本地 OCR 文本发送给 provider,不上传原图。

密钥

API key 只进入系统 Secret Service,不提供明文 fallback。Secret Service 不可用时报错,不降级写文件。

私有文件权限

配置、数据库、Portal restore token 等本地私有文件:

平台 权限
Unix 文件 0600,目录 0700
Windows 禁止继承宽松权限的受保护 DACL,只保留当前用户的完整访问 ACE

配置与 restore token 通过同一个私有文件替换函数提交:

  • Unix 用 rename
  • Windows 用 MoveFileExW(REPLACE_EXISTING | WRITE_THROUGH)

替换后必须再次校正最终路径的权限。 临时文件的权限不会自动传递给目标路径。

Portal token 不进入普通配置文件。

网络请求

约束 URL 元数据 翻译响应
超时 5 s 有
大小上限 1 MiB 1 MiB
凭据 无凭据 —
拒绝 私有/保留 IP、私有 DNS 解析、重定向 —

错误正文的读取边界

非 2xx 响应只在 4xx 时读取最多 4 KiB 正文用于错误归类,5xx 正文一律不读。

理由:有些服务不返回结构化错误,「缺少或无效 key」会退化成不透明的 http_status。读 4xx 正文能把它区分出来;而读 5xx 正文只会让网关错误页被误判成凭据问题。

DOM 与执行

  • 用户文本一律通过 React 文本节点或 textContent 写入
  • 富文本仅使用严格 DOMPurify 配置后的 innerHTML
  • HTML 实体解码使用隔离的 DOMParser
  • 数学表达式不使用 eval / Function
  • 大文本在识别/解码/格式化之前执行预览预算
  • 整数进制转换使用精确整数运算(BigInt),不经过浮点

保存路径

截图保存目录与文件名模板可配置(留空即内置默认)。模板只生成文件名:路径分隔符与前导点被清洗,写不到目录之外。同名时追加序号,不覆盖已有文件。

写入采用同目录临时文件 → 完整写入 → sync_all → 原子提交,且不覆盖。

翻译记录

成功的译文与原文落在同一个 SQLite 库(translation_history,全库上限 500 条)。

条目删除、历史清空和上限清理都会一并删除它的译文;设置里另有「清空已保存的译文」入口。

这条「删条目必然删译文」由事务保证——详见剪贴板管线里关于 FTS 一致性的说明,两者是同一个机制。

错误类型化

各领域使用 thiserror:StorageError、TranslationError、PasteError、PinError、CaptureError,每个都带稳定的 code()。

顶层 ClippyError(error.rs)聚合领域错误,提供 domain() / code() / identifier(),让日志标识形如 paste.portal_start_rejected。

命令层对外仍是 String,但内部结构化:From<XxxError> for String 集中在各领域的 error.rs。

区分两种日志级别,避免把预期路径记成故障:

  • error::report(warn)— 真实故障
  • error::note(info)— 预期路径,如 Wayland 首次未授权、请求被新请求取代、快捷键连按撞上进行中的会话

Clone this wiki locally