完整工程审计报告(系统化逐项检查): - 后端架构总评 + 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 对照。
17 KiB
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已被第二次覆盖,会看到页面闪变 - 旧页的
useEffectcleanup 和新页的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)
可优化的点:
-
音频电平更新频率:
LEVEL_EMIT_MIN_INTERVAL_MS = 33(~30Hz),配合 CSS 0.18s transition 效果尚可。但如果窗口失去焦点时requestAnimationFrame降频,可能出现电平条「冻住」的观感。建议在 coordinator 侧用setInterval兜底。 -
QA 浮窗出场动画缺失:
QaPanel关闭时直接hide(),没有退场动画。可以加一个qa:fadeout事件让前端先播动画,100ms 后再由 Rust 端 actual hide。 -
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 插件集成完整
可改善
-
缺少 CI 质量门禁:当前只有
release-tauri.yml构建流水线,没有 lint / test / typecheck 门禁(虽然有 Rustcargo check和前端tsc命令,但未在 CI 强制)。 -
缺少 E2E 测试:没有端到端测试(如 Playwright + Tauri driver),无法验证「按热键 → 录音 → 插入」的完整链路。
-
.gitignore不完整:node_modules/出现在多个子目录(promo-openless/、promo-openless-v2/),但根.gitignore未统一管理。 -
版本号同步风险: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_seenflag 统一,完备。 - QA 浮窗拖动:macOS 用
movableByWindowBackground,Windows 用data-tauri-drag-region。两端都能拖,但 macOS 整窗口可拖,Windows 仅 toolbar 区域可拖。 - 粘贴后剪贴板恢复:Windows/Linux 支持(
restore_clipboard_after_paste),macOS 走 AX 直写不涉及剪贴板。完备。 - 降级兜底:插入失败 → 文本留在剪贴板,用户可手动粘贴。两端一致。
4.3 需要关注的多端差异
-
Windows Capsule 翻译模式高度变化:翻译模式激活时 capsule 窗口从 84→118,macOS 保持 110 不变。设计合理但视觉差异可能困扰跨平台用户。
-
Windows WindowChrome 自绘标题栏:
WindowChrome.tsx的WinTitleBar绘制的关闭按钮走getCurrentWindow().close(),但 Rust 端RunEvent::WindowEvent::CloseRequested只对label == "main"做prevent_close+hide,其他窗口(capsule / qa)的关闭行为不一致。 -
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 映射无遗漏。
核心问题集中在:
- coordinator.rs 的单一文件过大(3842 行),需要拆分子状态机
- commands.rs 的业务逻辑应下沉到叶子模块
- 前端 Tab 切换的竞态可能导致动画异常
- QA 浮窗的流式滚动打断用户体验
没有发现「与其他应用混杂」或「耦合臃肿」的问题——代码遵循了严格的模块隔离约定。多平台覆盖完整,macOS 和 Windows 的核心行为一致,仅在视觉尺寸、窗口装饰等平台原生差异上有所不同。
项目整体健康。建议在下个迭代中优先处理 coordinator 拆分和前端动画竞态修复。