merge upstream main into windows local asr

This commit is contained in:
millionart committed 2026-05-06 21:28:41 +08:00
commit f358260866
9 files changed
+472 -11

No files matched your search

+54 -5
View File
@@ -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"
+9
View File
@@ -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/
+293
View File
@@ -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 拆分和前端动画竞态修复。**
+2 -2
View File
@@ -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 -1
View File
@@ -1,7 +1,7 @@
{
"name": "openless-app",
"private": true,
"version": "1.2.20",
"version": "1.2.21",
"type": "module",
"scripts": {
"dev": "vite",
+1 -1
View File
@@ -3751,7 +3751,7 @@ dependencies = [
[[package]]
name = "openless"
version = "1.2.20"
version = "1.2.21"
dependencies = [
"anyhow",
"arboard",
+1 -1
View File
@@ -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 -1
View File
@@ -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",
+110
View File
@@ -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"