From b997f5fca28f7ceb620313a081f83b06108da549 Mon Sep 17 00:00:00 2001 From: baiqing Date: Wed, 6 May 2026 20:55:03 +0800 Subject: [PATCH 1/3] =?UTF-8?q?chore(devex):=20CI=20=E5=8A=A0=20mac=20?= =?UTF-8?q?=E7=9F=A9=E9=98=B5=20+=20version-triplet=20=E6=A0=A1=E9=AA=8C?= =?UTF-8?q?=20+=20bump-version.sh?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 审查反馈: 1. CI 缺质量门禁 — 仅 windows-latest 1 job,mac 端 cargo check 路径 无 PR-time 验证,每次 release-tauri.yml 才暴露平台问题 2. .gitignore 不全 — promo-openless-v2/ 反复出现在 git status; node_modules / dist / target 没全局兜底 3. 版本号同步风险 — package.json + tauri.conf.json + Cargo.toml 三处 人工改,每次发版都担心漏一处 改动: A. .github/workflows/ci.yml - 单一 windows-tauri job → matrix(macos-latest + windows-latest) - mac job:cargo check + npm run build 验证跨平台编译 - Windows job:保留原 preflight + ps1 tokenize - 新加 step:Verify version triplet stays in sync(两端都跑)— 一旦 package.json / tauri.conf.json / Cargo.toml 三处 drift 立刻 fail,给发版工程加 long-running guard B. .gitignore - 加 promo-openless-v2/(已实测出现在 untracked) - 加 node_modules/ + dist/ + target/ 全局兜底(避免某子目录漏配 时 build artifact 进 PR) - 加 .cargo/registry / .cargo/git 兜底 C. scripts/bump-version.sh(新文件) - 1 个参数(X.Y.Z),sed 同步改 3 个文件 + cargo update -p openless 同步 Cargo.lock - 验证三处一致后输出后续 git 操作建议 - 跨 macOS sed / GNU sed 兼容(-i.bak 后 rm) 下次发版:./scripts/bump-version.sh 1.2.21 一行搞定。 未来 PR 漏改版本号 CI 立刻 fail,不需要靠人记忆。 --- .github/workflows/ci.yml | 46 ++++++++++++++++--- .gitignore | 9 ++++ scripts/bump-version.sh | 97 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 147 insertions(+), 5 deletions(-) create mode 100755 scripts/bump-version.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 53d5fa08..67283231 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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,25 @@ 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 triplet stays in sync + # 两个平台都跑这个校验:Windows runner 自带 git-bash,跨 shell 表现一致。 + # 一旦三处版本号 drift 立刻 fail,避免发版时再发现漏改。 + shell: bash + run: | + PKG=$(node -p "require('./package.json').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/') + echo "package.json = $PKG" + echo "tauri.conf.json = $TAU" + echo "Cargo.toml = $CRG" + if [ "$PKG" != "$TAU" ] || [ "$PKG" != "$CRG" ]; then + echo "::error::version triplet 不一致 — 请用 scripts/bump-version.sh 同步更新" + exit 1 + fi + echo "[ok] 三处版本号一致:$PKG" diff --git a/.gitignore b/.gitignore index a2d5ad09..772b2eda 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh new file mode 100755 index 00000000..2acb360e --- /dev/null +++ b/scripts/bump-version.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# 同步更新 OpenLess 三处版本号 + Cargo.lock。 +# 用法: +# ./scripts/bump-version.sh 1.2.21 +# +# 改的位置(CLAUDE.md 强调必须三处一起改): +# - openless-all/app/package.json "version": "X.Y.Z" +# - 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 " >&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" +TAURI_CONF="$APP/src-tauri/tauri.conf.json" +CARGO_TOML="$APP/src-tauri/Cargo.toml" + +for f in "$PKG_JSON" "$TAURI_CONF" "$CARGO_TOML"; do + if [ ! -f "$f" ]; then + echo "错误:找不到 $f" >&2 + exit 1 + fi +done + +# macOS sed 跟 GNU sed 行为不同(-i 后缀必填空字符串)。统一用 -i.bak 然后 rm。 +update_json_version() { + local file="$1" + sed -E -i.bak \ + "s/\"version\":[[:space:]]*\"[0-9]+\.[0-9]+\.[0-9]+\"/\"version\": \"$NEW\"/" \ + "$file" + rm "$file.bak" +} + +update_cargo_toml_version() { + local file="$1" + # 仅匹配文件顶层 [package] 段下的 version = "X.Y.Z",避免误改 dependencies 里的版本。 + # OpenLess 项目 Cargo.toml 第一个出现的 version = 一定是 [package] 自己的。 + sed -E -i.bak \ + "0,/^version = \"[0-9]+\.[0-9]+\.[0-9]+\"$/s//version = \"$NEW\"/" \ + "$file" + rm "$file.bak" +} + +echo "▶ 升 package.json → $NEW" +update_json_version "$PKG_JSON" + +echo "▶ 升 tauri.conf.json → $NEW" +update_json_version "$TAURI_CONF" + +echo "▶ 升 Cargo.toml → $NEW" +update_cargo_toml_version "$CARGO_TOML" + +echo "▶ 同步 Cargo.lock" +( cd "$APP/src-tauri" && cargo update -p openless 2>&1 | grep -E 'Updating|Locking|^error' || true ) + +echo +echo "===== 验证三处版本一致 =====" +PKG=$(node -p "require('$PKG_JSON').version") +TAU=$(node -p "require('$TAURI_CONF').version") +CRG=$(grep -E '^version = ' "$CARGO_TOML" | head -1 | sed -E 's/^version = "(.+)"$/\1/') + +printf '%-20s %s\n' 'package.json:' "$PKG" +printf '%-20s %s\n' 'tauri.conf.json:' "$TAU" +printf '%-20s %s\n' 'Cargo.toml:' "$CRG" + +if [ "$PKG" != "$NEW" ] || [ "$TAU" != "$NEW" ] || [ "$CRG" != "$NEW" ]; then + echo "::error::三处版本号未对齐 — 请检查 sed 是否成功" >&2 + exit 1 +fi + +echo +echo "✓ 三处版本号一致:$NEW" +echo +echo "下一步建议:" +echo " git diff --stat $PKG_JSON $TAURI_CONF $CARGO_TOML \"$APP/src-tauri/Cargo.lock\"" +echo " git add $PKG_JSON $TAURI_CONF $CARGO_TOML \"$APP/src-tauri/Cargo.lock\"" +echo " git commit -m 'chore(release): $NEW'" +echo " git push" +echo " git tag v$NEW-tauri && git push origin v$NEW-tauri" From 938c6a74b9517ee98ffa9365a17facb18983ff42 Mon Sep 17 00:00:00 2001 From: baiqing Date: Wed, 6 May 2026 20:57:38 +0800 Subject: [PATCH 2/3] =?UTF-8?q?docs:=20=E5=B7=A5=E7=A8=8B=E5=AE=A1?= =?UTF-8?q?=E8=AE=A1=E5=9F=BA=E7=BA=BF=20(audit-2026-05-06.md)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 完整工程审计报告(系统化逐项检查): - 后端架构总评 + 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 对照。 --- docs/audit-2026-05-06.md | 293 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 293 insertions(+) create mode 100644 docs/audit-2026-05-06.md diff --git a/docs/audit-2026-05-06.md b/docs/audit-2026-05-06.md new file mode 100644 index 00000000..d791d879 --- /dev/null +++ b/docs/audit-2026-05-06.md @@ -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
; }`。问题:胶囊窗口尺寸由 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 结构断裂(如未闭合的 `` 块)。当前有 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`,无 `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 拆分和前端动画竞态修复。** From 97fca4abab8e8481fab4be114a200062fa5ae049 Mon Sep 17 00:00:00 2001 From: baiqing Date: Wed, 6 May 2026 21:04:28 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix(devex):=20bump-version=20=E4=BF=AE=20BS?= =?UTF-8?q?D=20sed=20/=20lock=20=E5=A4=B1=E8=B4=A5=E5=90=9E=E9=94=99=20/?= =?UTF-8?q?=20package-lock=20=E6=9C=AA=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex 在 PR #294 上提了 3 个 P2,全部采纳: P2-A: BSD sed 不支持 GNU 行号范围 0,/.../ 原 sed -E -i.bak "0,/^version = .../s//.../" 在 macOS 默认 BSD sed 上 会 fail。换成 awk first-match 替换(mac BSD awk + GNU awk 都支持)。 P2-B: Cargo.lock 同步失败被 `|| true` 吞了 原 cargo update -p openless ... || true 会让脚本继续,validate 又只 校验 Cargo.toml 不校验 Cargo.lock。删 || true,让 set -euo pipefail 在 cargo update fail 时立刻退出;同时 validate 加 Cargo.lock 校验。 P2-C: package-lock.json 未跟着同步 npm 把 root version 也存在 package-lock.json 里(root + packages[''] 两处)。原脚本只 sed package.json 不更 lock,会让 npm ci 在 CI 出现 EUSAGE。换成 npm version --no-git-tag-version --allow-same-version 一次更两个文件。 ci.yml 的 verify step 也升级到校验 5 处一致: package.json / package-lock.json root+nested / tauri.conf.json / Cargo.toml / Cargo.lock 的 [openless] 包。 dry-run 已验证(用当前版本 1.2.20 跑一遍输出全部一致)。 --- .github/workflows/ci.yml | 29 +++++++--- openless-all/app/package-lock.json | 4 +- scripts/bump-version.sh | 93 +++++++++++++++++------------- 3 files changed, 76 insertions(+), 50 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 67283231..345036d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,19 +72,32 @@ jobs: - name: Check Tauri backend (cargo check) run: cargo check --manifest-path src-tauri/Cargo.toml - - name: Verify version triplet stays in sync + - name: Verify version sync across all 5 files # 两个平台都跑这个校验:Windows runner 自带 git-bash,跨 shell 表现一致。 - # 一旦三处版本号 drift 立刻 fail,避免发版时再发现漏改。 + # 一旦版本号 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/') - echo "package.json = $PKG" - echo "tauri.conf.json = $TAU" - echo "Cargo.toml = $CRG" - if [ "$PKG" != "$TAU" ] || [ "$PKG" != "$CRG" ]; then - echo "::error::version triplet 不一致 — 请用 scripts/bump-version.sh 同步更新" + # 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] 三处版本号一致:$PKG" + echo "[ok] 全部 5 处版本号一致:$PKG" diff --git a/openless-all/app/package-lock.json b/openless-all/app/package-lock.json index dc916113..2fb5f8bf 100644 --- a/openless-all/app/package-lock.json +++ b/openless-all/app/package-lock.json @@ -1,12 +1,12 @@ { "name": "openless-app", - "version": "1.2.19", + "version": "1.2.20", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openless-app", - "version": "1.2.19", + "version": "1.2.20", "dependencies": { "@tauri-apps/api": "^2.1.1", "@tauri-apps/plugin-autostart": "^2.5.1", diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh index 2acb360e..45d86ed4 100755 --- a/scripts/bump-version.sh +++ b/scripts/bump-version.sh @@ -1,15 +1,16 @@ #!/usr/bin/env bash -# 同步更新 OpenLess 三处版本号 + Cargo.lock。 +# 同步更新 OpenLess 四处版本号。 # 用法: # ./scripts/bump-version.sh 1.2.21 # -# 改的位置(CLAUDE.md 强调必须三处一起改): +# 改的位置(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.toml version = "X.Y.Z" (顶层) # - openless-all/app/src-tauri/Cargo.lock 通过 cargo update -p openless 同步 # -# CI 的 cross-platform 任务最后一步会校验三个文件版本号一致;漏改一处直接 fail。 +# CI 的 cross-platform 任务最后一步会校验四个文件版本号一致;漏改一处直接 fail。 set -euo pipefail @@ -30,68 +31,80 @@ 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" "$TAURI_CONF" "$CARGO_TOML"; do +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 -# macOS sed 跟 GNU sed 行为不同(-i 后缀必填空字符串)。统一用 -i.bak 然后 rm。 -update_json_version() { - local file="$1" - sed -E -i.bak \ - "s/\"version\":[[:space:]]*\"[0-9]+\.[0-9]+\.[0-9]+\"/\"version\": \"$NEW\"/" \ - "$file" - rm "$file.bak" -} - -update_cargo_toml_version() { - local file="$1" - # 仅匹配文件顶层 [package] 段下的 version = "X.Y.Z",避免误改 dependencies 里的版本。 - # OpenLess 项目 Cargo.toml 第一个出现的 version = 一定是 [package] 自己的。 - sed -E -i.bak \ - "0,/^version = \"[0-9]+\.[0-9]+\.[0-9]+\"$/s//version = \"$NEW\"/" \ - "$file" - rm "$file.bak" -} - -echo "▶ 升 package.json → $NEW" -update_json_version "$PKG_JSON" +# 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" -update_json_version "$TAURI_CONF" +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" -update_cargo_toml_version "$CARGO_TOML" - +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 | grep -E 'Updating|Locking|^error' || true ) +( 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 "===== 验证三处版本一致 =====" +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 -printf '%-20s %s\n' 'package.json:' "$PKG" -printf '%-20s %s\n' 'tauri.conf.json:' "$TAU" -printf '%-20s %s\n' 'Cargo.toml:' "$CRG" - -if [ "$PKG" != "$NEW" ] || [ "$TAU" != "$NEW" ] || [ "$CRG" != "$NEW" ]; then - echo "::error::三处版本号未对齐 — 请检查 sed 是否成功" >&2 +if [ "$mismatch" -ne 0 ] || [ "$PKG" != "$NEW" ]; then + echo + echo "::error::版本号未对齐 — 请检查脚本输出" >&2 exit 1 fi echo -echo "✓ 三处版本号一致:$NEW" +echo "✓ 全部一致:$NEW" echo echo "下一步建议:" -echo " git diff --stat $PKG_JSON $TAURI_CONF $CARGO_TOML \"$APP/src-tauri/Cargo.lock\"" -echo " git add $PKG_JSON $TAURI_CONF $CARGO_TOML \"$APP/src-tauri/Cargo.lock\"" +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"