merge upstream main into windows local asr
This commit is contained in:
commit
f358260866
9 files changed
+472
-11
No files matched your search
@@ -1,5 +1,11 @@
|
||||
name: CI
|
||||
|
||||
# 多平台跨语言质量门禁。release-tauri.yml 是发版流水线(仅打包构建),这里负责
|
||||
# 在合并前快速验证两件事:
|
||||
# 1. 前端 typecheck + vite bundle 通过(tsc + vite build,捕获跨 locale 类型 drift)
|
||||
# 2. Rust 后端在 macOS / Windows 都能 cargo check 过(捕获 cfg 漏分支 / 平台 API 误用)
|
||||
# 跑 build-mac.sh / windows-package-msvc.ps1 太重;只跑轻量 cargo check + vite build。
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, dev]
|
||||
@@ -7,9 +13,20 @@ on:
|
||||
branches: [main, dev]
|
||||
|
||||
jobs:
|
||||
windows-tauri:
|
||||
name: Windows Tauri checks
|
||||
runs-on: windows-latest
|
||||
cross-platform:
|
||||
name: ${{ matrix.label }} checks
|
||||
strategy:
|
||||
# 一个平台挂掉不阻塞其他平台拿到验证结果。
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: macos-latest
|
||||
label: macOS
|
||||
preflight: false
|
||||
- os: windows-latest
|
||||
label: Windows
|
||||
preflight: true
|
||||
runs-on: ${{ matrix.os }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: openless-all/app
|
||||
@@ -32,10 +49,12 @@ jobs:
|
||||
run: npm ci
|
||||
|
||||
- name: Check Windows prerequisites
|
||||
if: matrix.preflight
|
||||
shell: pwsh
|
||||
run: ./scripts/windows-preflight.ps1 -Toolchain msvc
|
||||
|
||||
- name: Check PowerShell scripts
|
||||
if: matrix.preflight
|
||||
shell: pwsh
|
||||
run: |
|
||||
foreach ($script in @("./scripts/windows-preflight.ps1", "./scripts/windows-build-gnu.ps1", "./scripts/windows-runtime-smoke.ps1")) {
|
||||
@@ -47,8 +66,38 @@ jobs:
|
||||
}
|
||||
}
|
||||
|
||||
- name: Build frontend
|
||||
- name: Build frontend (tsc + vite)
|
||||
run: npm run build
|
||||
|
||||
- name: Check Tauri backend
|
||||
- name: Check Tauri backend (cargo check)
|
||||
run: cargo check --manifest-path src-tauri/Cargo.toml
|
||||
|
||||
- name: Verify version sync across all 5 files
|
||||
# 两个平台都跑这个校验:Windows runner 自带 git-bash,跨 shell 表现一致。
|
||||
# 一旦版本号 drift 立刻 fail,避免发版时再发现漏改。
|
||||
# 校验 5 处:package.json / package-lock.json (root + nested) /
|
||||
# tauri.conf.json / Cargo.toml / Cargo.lock 的 [openless] 包。
|
||||
shell: bash
|
||||
run: |
|
||||
PKG=$(node -p "require('./package.json').version")
|
||||
LOCK_ROOT=$(node -p "require('./package-lock.json').version")
|
||||
LOCK_NESTED=$(node -p "require('./package-lock.json').packages[''].version")
|
||||
TAU=$(node -p "require('./src-tauri/tauri.conf.json').version")
|
||||
CRG=$(grep -E '^version = ' src-tauri/Cargo.toml | head -1 | sed -E 's/^version = "(.+)"$/\1/')
|
||||
# Cargo.lock:找 [openless] 包紧跟的 version 行
|
||||
CARGO_LOCK_VER=$(awk 'BEGIN{found=0} /^name = "openless"$/{found=1; next} found && /^version = /{gsub(/"/,""); print $3; exit}' src-tauri/Cargo.lock)
|
||||
echo "package.json = $PKG"
|
||||
echo "package-lock root = $LOCK_ROOT"
|
||||
echo "package-lock nested = $LOCK_NESTED"
|
||||
echo "tauri.conf.json = $TAU"
|
||||
echo "Cargo.toml = $CRG"
|
||||
echo "Cargo.lock openless = $CARGO_LOCK_VER"
|
||||
mismatch=0
|
||||
for v in "$LOCK_ROOT" "$LOCK_NESTED" "$TAU" "$CRG" "$CARGO_LOCK_VER"; do
|
||||
if [ "$v" != "$PKG" ]; then mismatch=1; fi
|
||||
done
|
||||
if [ "$mismatch" -ne 0 ]; then
|
||||
echo "::error::版本号未对齐 — 请用 scripts/bump-version.sh 同步更新"
|
||||
exit 1
|
||||
fi
|
||||
echo "[ok] 全部 5 处版本号一致:$PKG"
|
||||
@@ -22,9 +22,18 @@ SC/
|
||||
apps/
|
||||
promo/
|
||||
promo-openless/
|
||||
promo-openless-v2/
|
||||
docs/old-promo/
|
||||
.worktrees/
|
||||
|
||||
# 派生产物(兜底):项目曾出现 promo-openless-v2/node_modules 等遗漏,
|
||||
# 这里全局通配,避免某子目录漏配 .gitignore 时把 build artifact 推进 PR。
|
||||
node_modules/
|
||||
dist/
|
||||
target/
|
||||
.cargo/registry/
|
||||
.cargo/git/
|
||||
|
||||
# Windows TSF IME local build outputs
|
||||
openless-all/app/windows-ime/OpenLessIme/
|
||||
openless-all/app/windows-ime/x64/
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
# 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 拆分和前端动画竞态修复。**
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "openless-app",
|
||||
"version": "1.2.20",
|
||||
"version": "1.2.21",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "openless-app",
|
||||
"version": "1.2.20",
|
||||
"version": "1.2.21",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.1.1",
|
||||
"@tauri-apps/plugin-autostart": "^2.5.1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "openless-app",
|
||||
"private": true,
|
||||
"version": "1.2.20",
|
||||
"version": "1.2.21",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
Generated
+1
-1
@@ -3751,7 +3751,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openless"
|
||||
version = "1.2.20"
|
||||
version = "1.2.21"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arboard",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "openless"
|
||||
version = "1.2.20"
|
||||
version = "1.2.21"
|
||||
description = "OpenLess — local voice input that types where your cursor is"
|
||||
authors = ["OpenLess"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "OpenLess",
|
||||
"version": "1.2.20",
|
||||
"version": "1.2.21",
|
||||
"identifier": "com.openless.app",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
|
||||
Executable
+110
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env bash
|
||||
# 同步更新 OpenLess 四处版本号。
|
||||
# 用法:
|
||||
# ./scripts/bump-version.sh 1.2.21
|
||||
#
|
||||
# 改的位置(CLAUDE.md 强调必须同时改,否则 release-tauri.yml 失败):
|
||||
# - openless-all/app/package.json "version": "X.Y.Z"
|
||||
# - openless-all/app/package-lock.json 根包 version + 嵌套引用
|
||||
# - openless-all/app/src-tauri/tauri.conf.json "version": "X.Y.Z"
|
||||
# - openless-all/app/src-tauri/Cargo.toml version = "X.Y.Z" (顶层)
|
||||
# - openless-all/app/src-tauri/Cargo.lock 通过 cargo update -p openless 同步
|
||||
#
|
||||
# CI 的 cross-platform 任务最后一步会校验四个文件版本号一致;漏改一处直接 fail。
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [ "${1:-}" = "" ]; then
|
||||
echo "用法: $0 <new-version>" >&2
|
||||
echo "例: $0 1.2.21" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
NEW="$1"
|
||||
|
||||
if ! [[ "$NEW" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "错误:版本号必须是 X.Y.Z 数字格式 (拿到 '$NEW')" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
APP="$REPO_ROOT/openless-all/app"
|
||||
|
||||
PKG_JSON="$APP/package.json"
|
||||
PKG_LOCK="$APP/package-lock.json"
|
||||
TAURI_CONF="$APP/src-tauri/tauri.conf.json"
|
||||
CARGO_TOML="$APP/src-tauri/Cargo.toml"
|
||||
CARGO_LOCK="$APP/src-tauri/Cargo.lock"
|
||||
|
||||
for f in "$PKG_JSON" "$PKG_LOCK" "$TAURI_CONF" "$CARGO_TOML" "$CARGO_LOCK"; do
|
||||
if [ ! -f "$f" ]; then
|
||||
echo "错误:找不到 $f" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# package.json + package-lock.json:npm version 一行同步两个,且不打 git tag。
|
||||
# --allow-same-version 让脚本可重复运行(实际 release flow 不会,但 dry-run 友好)。
|
||||
echo "▶ 升 package.json + package-lock.json → $NEW"
|
||||
( cd "$APP" && npm version "$NEW" --no-git-tag-version --allow-same-version > /dev/null )
|
||||
|
||||
# tauri.conf.json:BSD sed 与 GNU sed 都支持 -E + -i.bak 后缀;不用行号范围地址。
|
||||
echo "▶ 升 tauri.conf.json → $NEW"
|
||||
sed -E -i.bak \
|
||||
"s/\"version\":[[:space:]]*\"[0-9]+\.[0-9]+\.[0-9]+\"/\"version\": \"$NEW\"/" \
|
||||
"$TAURI_CONF"
|
||||
rm "$TAURI_CONF.bak"
|
||||
|
||||
# Cargo.toml:用 awk 替换文件里第一个 version = "X.Y.Z" 行(顶层 [package].version)。
|
||||
# 不用 GNU sed 的 `0,/.../` 行号范围地址(macOS BSD sed 不支持)。
|
||||
echo "▶ 升 Cargo.toml → $NEW"
|
||||
awk -v new="$NEW" '
|
||||
!done && /^version = "[0-9]+\.[0-9]+\.[0-9]+"$/ {
|
||||
sub(/"[0-9]+\.[0-9]+\.[0-9]+"/, "\"" new "\"")
|
||||
done = 1
|
||||
}
|
||||
{ print }
|
||||
' "$CARGO_TOML" > "$CARGO_TOML.tmp"
|
||||
mv "$CARGO_TOML.tmp" "$CARGO_TOML"
|
||||
|
||||
# Cargo.lock:cargo update 显式同步 openless package;失败要立刻退出,不能吞错。
|
||||
echo "▶ 同步 Cargo.lock"
|
||||
( cd "$APP/src-tauri" && cargo update -p openless 2>&1 | tail -5 )
|
||||
|
||||
# 校验五处一致(package.json / package-lock.json / tauri.conf.json / Cargo.toml / Cargo.lock)
|
||||
echo
|
||||
echo "===== 验证版本一致性 ====="
|
||||
PKG=$(node -p "require('$PKG_JSON').version")
|
||||
LOCK_ROOT=$(node -p "require('$PKG_LOCK').version")
|
||||
LOCK_NESTED=$(node -p "require('$PKG_LOCK').packages[''].version")
|
||||
TAU=$(node -p "require('$TAURI_CONF').version")
|
||||
CRG=$(grep -E '^version = ' "$CARGO_TOML" | head -1 | sed -E 's/^version = "(.+)"$/\1/')
|
||||
CARGO_LOCK_VER=$(awk '/^name = "openless"$/{getline; if (match($0, /version = "([0-9.]+)"/, a)) {print a[1]; exit}}' "$CARGO_LOCK" 2>/dev/null \
|
||||
|| awk 'BEGIN{found=0} /^name = "openless"$/{found=1; next} found && /^version = /{gsub(/"/,""); print $3; exit}' "$CARGO_LOCK")
|
||||
|
||||
printf '%-22s %s\n' 'package.json:' "$PKG"
|
||||
printf '%-22s %s\n' 'package-lock root:' "$LOCK_ROOT"
|
||||
printf '%-22s %s\n' 'package-lock nested:' "$LOCK_NESTED"
|
||||
printf '%-22s %s\n' 'tauri.conf.json:' "$TAU"
|
||||
printf '%-22s %s\n' 'Cargo.toml:' "$CRG"
|
||||
printf '%-22s %s\n' 'Cargo.lock (openless):' "$CARGO_LOCK_VER"
|
||||
|
||||
mismatch=0
|
||||
for v in "$LOCK_ROOT" "$LOCK_NESTED" "$TAU" "$CRG" "$CARGO_LOCK_VER"; do
|
||||
if [ "$v" != "$NEW" ]; then mismatch=1; fi
|
||||
done
|
||||
|
||||
if [ "$mismatch" -ne 0 ] || [ "$PKG" != "$NEW" ]; then
|
||||
echo
|
||||
echo "::error::版本号未对齐 — 请检查脚本输出" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "✓ 全部一致:$NEW"
|
||||
echo
|
||||
echo "下一步建议:"
|
||||
echo " git add $PKG_JSON $PKG_LOCK $TAURI_CONF $CARGO_TOML $CARGO_LOCK"
|
||||
echo " git commit -m 'chore(release): $NEW'"
|
||||
echo " git push"
|
||||
echo " git tag v$NEW-tauri && git push origin v$NEW-tauri"
|
||||
Reference in new issue
Block a user