Files
openless/docs/audit-2026-05-06.md
T
baiqing 938c6a74b9 docs: 工程审计基线 (audit-2026-05-06.md)
完整工程审计报告(系统化逐项检查):
- 后端架构总评 + 5 个 P1/P2 优化点(coordinator/commands 拆分等)
- 前端 4 个 UI bug(tab 切换竞态、流式滚动打断 QA 等)
- 工程化水平评估 + 4 条改善(CI 门禁 / E2E / gitignore / 版本号)
  → 其中 3 条已在本 PR 实现(CI matrix + gitignore + bump-version 脚本)
- 多端逻辑与体验一致性表(macOS vs Windows 平台差异 vs 行为一致性)
- UI/后端 IPC 1:1 映射校验(全部命令前后端对齐 ✓)
- 改进建议汇总:P0 无;P1 5 项;P2 6 项
- 总结:项目整体健康,建议下迭代处理 coordinator 拆分 + 前端动画竞态

把这份基线纳入 git,让 main 留下 v1.2.20→v1.2.21 这一阶段的工程现状
锚点,后续审计有 baseline 对照。
2026-05-06 20:57:38 +08:00

17 KiB
Raw Blame History

OpenLess 系统化工程审计报告

审计日期:2026-05-06
项目版本:1.2.20
审计范围:openless-all/app/ 主项目(Rust 后端 + React 前端)


一、后端检查与优化

1.1 架构总评

后端整体架构清晰,遵循「单 Coordinator 状态机 + 独立叶子模块」的分层设计。模块之间只通过 types.rs 共享类型,跨模块调用全部收敛到 coordinator.rs,与 CLAUDE.md 约定的架构一致。代码质量整体较高:大量使用 thiserror / anyhow 进行错误处理,关键路径有 #[cfg(test)] 单元测试覆盖,热路径有状态竞态保护。

模块清单:asr/(火山引擎流式 + Whisper HTTP + 本地 Qwen3-ASR)、polish.rs(OpenAI-compatible LLM)、hotkey.rs(macOS CGEventTap / Windows WH_KEYBOARD_LL / Linux rdev)、recorder.rs(cpal 音频采集)、insertion.rs(跨平台文本插入)、persistence.rs(JSON 文件 + OS 凭据库)、permissions.rs(TCC 权限)、selection.rs(划词捕获)。

1.2 值得优化的后端问题

问题 A:coordinator.rs 过于臃肿(严重程度:中)

coordinator.rs 当前 3842 行,包含了 dictation 状态机、QA 状态机、双 hotkey supervisor 循环、recorder 错误监控、Windows IME 会话管理、capsule 事件发射、录音 mute 管理、本地 ASR 预加载/释放等全部胶水逻辑。单一文件内职责过多。

建议:按子状态机拆分为多个 coordinator 子模块:

  • coordinator/dictation.rs — 主听写 session 生命周期
  • coordinator/qa.rs — QA 划词追问 session 生命周期
  • coordinator/resources.rs — recorder / ASR / mute 等资源管理

问题 B:commands.rs 包含过多业务逻辑(严重程度:中)

commands.rs 中有大量本不属于「IPC 薄层」的业务逻辑,例如:

  • WAV 静音文件编码(encode_wav_16k_mono_silence)
  • ASR 端点 URL 拼接(asr_transcriptions_url)
  • 模型列表 JSON 解析(parse_model_ids)
  • LLM/ASR provider 连接验证(validate_llm_provider / validate_asr_provider)

这些应该下沉到对应的叶子模块(asr/ 或 polish.rs),commands.rs 只做参数接收和类型转换。

问题 C:平台条件编译代码分散(严重程度:低)

lib.rs、coordinator.rs、insertion.rs 中大量 #[cfg(target_os = "macos")] / #[cfg(target_os = "windows")] 块散落在主流程代码中。虽然不是编译期问题,但降低了可读性。建议将平台适配代码集中到 platform/ 子模块,用 trait 抽象。

问题 D:本地 ASR 引擎缓存释放策略依赖时间阈值(严重程度:低)

LocalAsrCache 的释放依赖 local_asr_keep_loaded_secs 定时器(默认 300 秒)。如果用户在 5 分钟内未再次使用,引擎释放。但在 Windows 上本地 ASR 引擎根本不可用(仅在 macOS 编译),相关代码却仍在 coordinator 中占据逻辑分支。建议将平台不可用的功能路径在编译期完全消除,而非运行时静默跳过。

问题 E:Volcengine 凭据与通用 Provider 凭据同时存在(严重程度:低)

系统中同时维护了 Volcengine 专用凭据字段(volcengine_app_key / volcengine_access_key / volcengine_resource_id)和通用 Provider 凭据路径(asr_api_key / asr_endpoint),导致 get_credentials 返回的 CredentialsStatus 需要同时维护 volcengineConfigured 和 asrConfigured 两个字段。历史迁移可理解,但长期维护增加复杂度。

1.3 后端与其他应用混杂检查

经检查,openless-all/app/ 是纯净的单一 Tauri 项目,未发现与其他应用混杂的代码。但仓库根目录存在以下与主项目无关的目录:

目录 内容 建议
promo-openless/ Remotion 宣传视频项目 移至独立仓库或 marketing/ 子目录
promo-openless-v2/ Remotion 宣传视频 v2 同上
SC/ 录屏素材文件(.mov / .mp4) 建议移出仓库或用 Git LFS
docs/ 开发调研文档 保留,但与主项目解耦

当前这些目录虽然在 git 仓库中,但不会参与 Tauri 构建,不会导致臃肿或冲突。与用户提到的「此前与其他软件冲突或臃肿问题」对比,当前架构没有重复该问题。


二、前端检查

2.1 UI Bug 分析

Bug 1:Tab 切换动画的双重渲染竞态(严重程度:中)

FloatingShell.tsx 的 tab 切换使用 displayTab / tabPhase 机制:旧页先播 ol-page-fadeout(180ms),之后切 displayTab 并播 ol-page-slide。但 key={displayTab} 会让 React 在 displayTab 改变时卸载旧组件树并挂载新组件树。问题:

  • 如果用户在 180ms 内快速切换两次 tab,第一次的 timeout 触发时 displayTab 已被第二次覆盖,会看到页面闪变
  • 旧页的 useEffect cleanup 和新页的 useEffect 在 180ms 内交错执行,若两者都触发了 IPC 调用,会产生竞态

修复建议:使用 useTransition 或 CSS animationend 事件代替固定 setTimeout,确保动画结束后再切 DOM。

Bug 2:Capsule 窗口 state === 'idle' 时返回空 div(严重程度:低)

Capsule.tsx 的 if (state === 'idle') { return <div style={{ width: 0, height: 0 }} />; }。问题:胶囊窗口尺寸由 Rust 端 position_capsule_bottom_center 设定(220×110),但 React 返回 0×0 的 div 时,Tauri webview 的 CSS 尺寸与窗口尺寸不一致。在 Windows 上可能导致透明区域响应鼠标事件(mouse event 穿透到下层窗口)。

Bug 3:QA 浮窗滚动容器缺少 overflow-anchor(严重程度:低)

QaPanel.tsx 的流式答案到达时,用 scrollRef.current.scrollTop = scrollRef.current.scrollHeight 手动滚到底。如果用户在流式过程中手动向上滚动查看前面的消息,新 chunk 到达时会强制跳回底部,打断阅读。应该加入「用户是否主动滚离底部」的检测(类似聊天的 scroll-to-bottom 按钮逻辑)。

Bug 4:dangerouslySetInnerHTML 的 XSS 表面(严重程度:低)

QaPanel.tsx 和 StreamingAssistantBubble 使用 dangerouslySetInnerHTML 渲染 Markdown。虽然 renderQaMarkdown 使用 marked 库且配置了 sanitize,但在流式场景下不完整的 Markdown 可能导致 HTML 结构断裂(如未闭合的 <code> 块)。当前有 fallback 到 renderQaPlainText,但错误边界不覆盖 dangerouslySetInnerHTML 渲染错误。

2.2 动效与交互流畅度

当前状态良好:

  • 胶囊波形的 AudioBars 使用 cubic-bezier(0.22, 1, 0.36, 1) 缓动曲线,过渡平滑
  • 所有 transition 使用 CSS 变量 var(--ol-motion-*) 统一缓动
  • willChange 属性在动画元素上正确设置(Capsule 的 transform, box-shadow)

可优化的点:

  1. 音频电平更新频率:LEVEL_EMIT_MIN_INTERVAL_MS = 33(~30Hz),配合 CSS 0.18s transition 效果尚可。但如果窗口失去焦点时 requestAnimationFrame 降频,可能出现电平条「冻住」的观感。建议在 coordinator 侧用 setInterval 兜底。

  2. QA 浮窗出场动画缺失:QaPanel 关闭时直接 hide(),没有退场动画。可以加一个 qa:fadeout 事件让前端先播动画,100ms 后再由 Rust 端 actual hide。

  3. SettingsModal 无入场动画:Settings 弹窗使用 animation: 'ol-prompt-pop 0.26s var(--ol-motion-spring)',但关闭时瞬间消失。


三、项目工程化与功能完善

3.1 工程化水平评估

优点

  • 模块化清晰:Rust 端严格遵循「叶子模块只依赖 types,胶水只写在 coordinator」的约定
  • 错误处理完备:关键路径全部使用 Result<T, E>,无 unwrap() 裸奔
  • 测试覆盖:commands.rs 和 polish.rs 有单元测试,persistence.rs 有集成测试
  • 类型安全 IPC:前后端类型通过 types.rs ↔ types.ts 镜像定义,序列化字段名一致
  • Mock 支持:前端 invokeOrMock 允许在浏览器中脱离 Tauri 环境开发
  • i18n 国际化:支持 zh-CN、en、ja、ko、zh-TW 五种语言
  • 自动更新:Tauri updater 插件集成完整

可改善

  1. 缺少 CI 质量门禁:当前只有 release-tauri.yml 构建流水线,没有 lint / test / typecheck 门禁(虽然有 Rust cargo check 和前端 tsc 命令,但未在 CI 强制)。

  2. 缺少 E2E 测试:没有端到端测试(如 Playwright + Tauri driver),无法验证「按热键 → 录音 → 插入」的完整链路。

  3. .gitignore 不完整:node_modules/ 出现在多个子目录(promo-openless/、promo-openless-v2/),但根 .gitignore 未统一管理。

  4. 版本号同步风险:CLAUDE.md 指出需要同时更新 package.json、tauri.conf.json、Cargo.toml 三处的版本号,容易遗漏。建议用脚本或 workspace 版本管理。

3.2 功能完整性

当前功能矩阵:

功能 macOS Windows 备注
全局热键听写 ✅ ✅ macOS CGEventTap / Windows WH_KEYBOARD_LL
火山引擎流式 ASR ✅ ✅
Whisper HTTP ASR ✅ ✅
本地 Qwen3-ASR ✅ ❌ 仅 macOS 编译
LLM 润色(四种模式) ✅ ✅
LLM 翻译模式 ✅ ✅ Shift 修饰键触发
划词 QA 问答 ✅ ✅ 双 hotkey 架构
热词词典 ✅ ✅
历史记录 ✅ ✅
开机自启 ✅ ✅
自动更新 ✅ ✅
录音时系统静音 ✅ ✅
Windows IME 直写 N/A ✅ C++ TSF 模块
系统托盘图标 ✅ ✅

四、多端逻辑与体验一致性

4.1 平台差异对比

维度 macOS Windows 一致性
Capsule 物理尺寸 220×110,visual height 96 220×84(118),visual height 52 ⚠️ 不一致
插入策略 AX 直写(通过 Accessibility API) enigo 模拟粘贴 / TSF 不同,但策略合理
窗口圆角 系统原生圆角 手动 CreateRoundRectRgn(18px) ⚠️ 视觉差异
窗口背景 NSVisualEffectView 磨砂 Mica + 自定义渐变 ⚠️ 视觉差异
默认热键 右 Option(Toggle) 右 Control(Toggle) 不一致,但符合平台惯例
QA 默认热键 Cmd+Shift+; Ctrl+Shift+; 符合平台惯例 ✅
启动权限检查 阻塞式弹窗检查 异步轮询(最多 10s) 合理差异
窗口控制按钮 系统红黄绿 自绘最小化/最大化/关闭 合理差异

4.2 逻辑完备性

  • 翻译模式:两端均通过 Shift 修饰键触发,但 macOS 用 flagsChanged 事件,Windows 用 WH_KEYBOARD_LL 的 Shift 边沿。逻辑层在 coordinator 中以 translation_modifier_seen flag 统一,完备。
  • QA 浮窗拖动:macOS 用 movableByWindowBackground,Windows 用 data-tauri-drag-region。两端都能拖,但 macOS 整窗口可拖,Windows 仅 toolbar 区域可拖。
  • 粘贴后剪贴板恢复:Windows/Linux 支持(restore_clipboard_after_paste),macOS 走 AX 直写不涉及剪贴板。完备。
  • 降级兜底:插入失败 → 文本留在剪贴板,用户可手动粘贴。两端一致。

4.3 需要关注的多端差异

  1. Windows Capsule 翻译模式高度变化:翻译模式激活时 capsule 窗口从 84→118,macOS 保持 110 不变。设计合理但视觉差异可能困扰跨平台用户。

  2. Windows WindowChrome 自绘标题栏:WindowChrome.tsx 的 WinTitleBar 绘制的关闭按钮走 getCurrentWindow().close(),但 Rust 端 RunEvent::WindowEvent::CloseRequested 只对 label == "main" 做 prevent_close + hide,其他窗口(capsule / qa)的关闭行为不一致。

  3. capsule 窗口 skipTaskbar:两端都设了,正确。


五、UI 与后端接口映射校验

5.1 前后端命令对照

经逐一核对 invoke_handler! 宏(lib.rs)与 ipc.ts 的函数导出,所有后端命令在前端都有对应的 TypeScript wrapper:

后端命令 前端函数 状态
get_settings getSettings() ✅
set_settings setSettings(prefs) ✅
get_hotkey_status getHotkeyStatus() ✅
get_hotkey_capability getHotkeyCapability() ✅
get_windows_ime_status getWindowsImeStatus() ✅
get_credentials getCredentials() ✅
set_credential setCredential(account, value) ✅
read_credential readCredential(account) ✅
set_active_asr_provider setActiveAsrProvider(provider) ✅
set_active_llm_provider setActiveLlmProvider(provider) ✅
validate_provider_credentials validateProviderCredentials(kind) ✅
list_provider_models listProviderModels(kind) ✅
list_history listHistory() ✅
delete_history_entry deleteHistoryEntry(id) ✅
clear_history clearHistory() ✅
list_vocab listVocab() ✅
add_vocab addVocab(phrase, note) ✅
remove_vocab removeVocab(id) ✅
set_vocab_enabled setVocabEnabled(id, enabled) ✅
list_vocab_presets listVocabPresets() ✅
save_vocab_presets saveVocabPresets(store) ✅
start_dictation startDictation() ✅
stop_dictation stopDictation() ✅
cancel_dictation cancelDictation() ✅
handle_window_hotkey_event handleWindowHotkeyEvent(...) ✅
inject_hotkey_click_for_dev N/A(debug only) ✅
repolish repolish(rawText, mode) ✅
set_default_polish_mode setDefaultPolishMode(mode) ✅
set_style_enabled setStyleEnabled(mode, enabled) ✅
check_accessibility_permission checkAccessibilityPermission() ✅
request_accessibility_permission requestAccessibilityPermission() ✅
check_microphone_permission checkMicrophonePermission() ✅
request_microphone_permission requestMicrophonePermission() ✅
open_system_settings openSystemSettings(pane) ✅
trigger_microphone_prompt triggerMicrophonePrompt() ✅
export_error_log exportErrorLog(name) ✅
restart_app restartApp() ✅
get_qa_hotkey_label getQaHotkeyLabel() ✅
set_qa_hotkey setQaHotkey(binding) ✅
qa_window_dismiss qaWindowDismiss() ✅
qa_window_pin qaWindowPin(pinned) ✅
local_asr_* (15 个命令) 对应的 15 个函数 ✅

结论:UI 与后端接口完全 1:1 对应,无遗漏。

5.2 事件订阅对照

事件名 发射端 订阅端 状态
capsule:state coordinator Capsule.tsx ✅
qa:state coordinator QaPanel.tsx ✅
qa:dismiss coordinator QaPanel.tsx ✅
qa:level coordinator QaPanel.tsx ✅
prefs:changed commands::set_settings QaPanel.tsx ✅
local-asr:download-progress DownloadManager LocalAsr.tsx ✅

六、改进建议汇总

立即修复(P0)

无。当前版本功能完整,无明显崩溃或数据丢失风险。

短期优化(P1 — 建议在下一版迭代中处理)

# 问题 位置 工作量
1 coordinator.rs 拆分 后端 2-3h
2 commands.rs 业务逻辑下沉 后端 1-2h
3 Tab 切换动画竞态修复 FloatingShell.tsx 1h
4 QA 流式滚动打断问题 QaPanel.tsx 1h
5 SettingsModal / QaPanel 退场动画 前端 1h

中长期改善(P2)

# 问题 建议
1 缺少 CI lint/test 门禁 添加 GitHub Actions workflow:cargo clippy + cargo test + npx tsc --noEmit
2 缺少 E2E 测试 引入 Playwright + Tauri driver 测试核心链路
3 平台代码分散 创建 src-tauri/src/platform/ 模块,用 trait 抽象平台差异
4 版本号同步 用 workspace Cargo.toml + 构建脚本自动同步三处版本号
5 仓库清理 promo-openless/、SC/ 移至独立仓库或 Git LFS
6 Windows Capsule 尺寸与 macOS 视觉差异 文档化或统一 visual height

七、总结

OpenLess 1.2.20 的代码质量在同类开源项目中属于中上水平。架构设计清晰(单 Coordinator + 叶子模块),错误处理完备,前后端类型安全,IPC 接口 1:1 映射无遗漏。

核心问题集中在:

  1. coordinator.rs 的单一文件过大(3842 行),需要拆分子状态机
  2. commands.rs 的业务逻辑应下沉到叶子模块
  3. 前端 Tab 切换的竞态可能导致动画异常
  4. QA 浮窗的流式滚动打断用户体验

没有发现「与其他应用混杂」或「耦合臃肿」的问题——代码遵循了严格的模块隔离约定。多平台覆盖完整,macOS 和 Windows 的核心行为一致,仅在视觉尺寸、窗口装饰等平台原生差异上有所不同。

项目整体健康。建议在下个迭代中优先处理 coordinator 拆分和前端动画竞态修复。