diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 53d5fa08..345036d4 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,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" 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/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 拆分和前端动画竞态修复。** 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 new file mode 100755 index 00000000..45d86ed4 --- /dev/null +++ b/scripts/bump-version.sh @@ -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 " >&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"