* feat(updater): manual-download Beta opt-in in Settings → About
接入 PR-B-2:客户端「加入 Beta 渠道」开关 + 文档完善。
为什么不走 auto-update 切 endpoint:
tauri-plugin-updater 2.10 的 Builder 不暴露 endpoints() 运行时 API,
endpoints 只能从 tauri.conf.json 编译期读,不能跑时切。继续用 plugin
自带 check 路径意味着 Beta 包会被推到所有用户,违背「Beta 不溢出正式
版」原则。fork plugin 或自实现 update flow ~500 行高风险,本轮采用
最稳路径:plugin 永远只读正式版 manifest(旧文件名),Beta 改成手动
下载——Settings 里展示最新 prerelease 的下载入口,用户点了跳浏览器。
Rust 端:
- types.rs:UpdateChannel enum (Stable | Beta) + UserPreferences::update_channel
字段;UserPreferencesWire / Default / Deserialize 全套兼容(旧 prefs 反序列化
默认 Stable,迁移无痛)。
- commands.rs:三个新命令——
- get_update_channel:读 prefs
- set_update_channel:写 prefs,复用 persist_settings + emit prefs:changed
- fetch_latest_beta_release:reqwest 调 GitHub Releases API,过滤
prerelease=true 且 tag 以 -beta-tauri 结尾的最新一条
- lib.rs:三个命令注册到 invoke_handler
前端:
- ipc.ts:UpdateChannel + LatestBetaRelease 类型 + 三个 invoke 包装
- SettingsModal.tsx:在 AboutMini 末尾加 BetaChannelControl 组件——
Toggle 切换渠道;切到 beta 时拉一次最新 prerelease 信息,展示
「最新 Beta:v1.x.x-beta-tauri」+「前往下载」按钮(openExternal 跳
GitHub release 页面)+ 重新查询按钮。切回 stable 立即清空状态。
- Settings.tsx:把 Toggle 组件 export,让 SettingsModal 复用同一开关样式。
- i18n 五个 locale(zh-CN / zh-TW / en / ja / ko)的 settings.about 都补
齐 betaChannel* 9 个 key(zh-CN 是 source of truth)。
文档:
- README.md / README.zh.md:把 PR-A 的「opt-in,尚未接入」占位换成
manual-download 路径的说明 + tag 约定。
- CLAUDE.md:把「Channel distribution (in progress)」整段重写为已接入
状态,含 tag 约定 / wiring 位置 / 新加的 release verification checklist
(5 条,发版后一定要走一遍,含 stable/beta 双向 cross-check 与 endpoint
采样验证)。
cargo check + npm run build 本地都过。本 PR base = chore/branching-workflow
(PR-A),等 PR-A merge 后会自动指向 beta;与 PR-B-1(CI 端)解耦,可独立
review / merge。
* docs(readme): refresh Status / Architecture / Maintainer release checklist
把 README 的产品/发版描述更新到当前真实状态,配合 PR-A / PR-B 系列工作流改造:
Status (v1.2) 章节:
- 把云端 ASR / 本地 ASR / 润色 provider 三类拆开列;本地 ASR 提及内置
Qwen3-ASR + Windows Foundry Local Whisper 两条线
- 加翻译热键、QA 浮窗、Beta 渠道 opt-in、Homebrew Cask 分发渠道四个要点
- 把「中英双语」改成多语言 UI(实际已支持 zh-CN / zh-TW / en / ja / ko)
Architecture 顶句:
- 由「Auto-updates ride on Tauri updater plugin」改为双渠道描述:Stable
(v<v>-tauri) 自动推送,Beta (v<v>-beta-tauri) 手动下载
Maintainer release checklist:
- 完全重写为双渠道流程:通用准备(5 处版本号 + 冒烟 + secrets)+
Beta 流程(在 beta 分支打 v<v>-beta-tauri tag)+ 正式版流程
(beta→main 后在 main 打 v<v>-tauri tag)
- 把 CI 的 Verify version sync 5-file 校验显式提一次,避免维护者
漏改 Cargo.lock / package-lock 撞 CI 红灯
- 末尾引用 CLAUDE.md 的 5 步 release verification checklist
Roadmap 章节保持原样(多个项目实际状态我无法在不引入回归的前提下
确认,避免误判)。
* docs: drop reference to non-existent bump-version.sh; describe manual bump
---------
Co-authored-by: baiqing <lbx12309@icloud.com>
13 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project
OpenLess is a menu-bar/tray voice-input layer. Hold or toggle a global hotkey, speak, and the dictated text is polished and inserted at the current cursor in any app. Product principles, state machine, and module list live in docs/openless-development.md and docs/openless-overall-logic.md — read those before changing product behavior.
The active codebase lives at openless-all/app/ and is Tauri 2 + Rust backend + React/TS frontend, targeting macOS 12+ and Windows. The legacy Swift implementation (Sources/, Tests/, Package.swift, appcast.xml, Sparkle pipeline) was removed in commit 34d2823; do not resurrect it.
UI must match openless-all/design_handoff_openless/*.jsx pixel-for-pixel; the JSX is reference-only, never imported.
Build, Run, Test
Tauri (current — start here)
cd "openless-all/app"
npm ci
# Dev: vite at :1420 + tauri shell
npm run tauri dev
# Build .app (+ DMG) — use this script, not `tauri build` directly,
# because it threads Apple signing env vars and validates Info.plist.
./scripts/build-mac.sh # build, sign, install to /Applications, reset TCC
INSTALL=0 ./scripts/build-mac.sh # build only
# Frontend-only TS check
npm run build # = tsc && vite build
# Rust type-check without full compile
cargo check --manifest-path src-tauri/Cargo.toml
Windows (cross-check only — no macOS runner in CI)
# Preflight: verify toolchain
.\scripts\windows-preflight.ps1
# Build (requires Windows host or cross-compile target)
.\scripts\windows-build-gnu.ps1
Generated artifacts:
openless-all/app/src-tauri/target/release/bundle/macos/OpenLess.appopenless-all/app/src-tauri/target/release/bundle/dmg/OpenLess_<version>_aarch64.dmg
Logs: ~/Library/Logs/OpenLess/openless.log (macOS) / %LOCALAPPDATA%\OpenLess\Logs\openless.log (Windows).
There is no test runner wired in for the frontend. src/lib/providerSetup.test.ts is a hand-rolled assertion script — run with npx tsx src/lib/providerSetup.test.ts if you need it. Rust side has no cargo test targets yet; behavior is verified by running the app.
Architecture
coordinator::Coordinator is the single owner of session state. Hotkey edges drive a small phase enum (Idle → Starting → Listening → Processing); recorder, ASR, polish, insertion, and history are wired here and nowhere else. Library/module code never calls across modules — they each depend only on shared types.
Rust (openless-all/app/src-tauri/src) Purpose
────────────────────────────────────── ────────────────────────────────
types.rs Pure value types: DictationSession, PolishMode, HotkeyBinding, errors
hotkey.rs Global hotkey monitor (modifier-key edges)
recorder.rs Mic → 16 kHz mono Int16 PCM, RMS callback
asr/{mod,frame,volcengine,whisper}.rs ASR providers: Volcengine streaming WebSocket + Whisper HTTP
polish.rs OpenAI-compatible chat completions (Ark / DeepSeek / etc.)
insertion.rs AX focused-element write → clipboard + Cmd+V → copy-only fallback
persistence.rs History/preferences/vocab JSON + platform credential vault
coordinator.rs + commands.rs + lib.rs State machine, IPC surface, tray icon, window plumbing
permissions.rs TCC checks (Accessibility / Microphone)
Frontend (openless-all/app/src)
src/components/Capsule.tsx Capsule view + state enum
src/ (React) Main window UI: Overview / History / Vocab / Style / Settings
src/i18n/ react-i18next init + zh-CN / en resources
src/pages/_atoms.tsx Recoil atoms — global frontend state
src/state/HotkeySettingsContext.tsx HotkeySettings React context (capability + binding from backend)
Dictation pipeline
hotkey edge (1st) → beginSession: Recorder.start → ASR.openSession → BufferingAudioConsumer.attach
hotkey edge (2nd) → endSession: Recorder.stop → ASR.sendLastFrame → awaitFinal → Polish → Insert → History.save
.cancelled → ASR.cancel, Recorder.stop, capsule .cancelled
Invariants:
- Polish/ASR fallbacks are silent. Missing Ark creds → insert raw transcript. Missing Volcengine creds → mock pipeline copies a placeholder. The contract is "the user's words don't get lost" — don't add hard errors here.
BufferingAudioConsumerqueues PCM until the WebSocket is ready, then drains. Recorder always pushes to it; ASR is attached afteropenSessionresolves.- Hotkey is toggle-only, not press-and-hold. The monitor yields one edge per modifier-key keydown; the coordinator interprets odd/even.
Permissions, credentials, on-disk state
- Bundle ID
com.openless.appis hard-coded inopenless-all/app/src-tauri/tauri.conf.jsonandCredentialsVault.serviceName. Changing it breaks system credential vault lookups and every existing TCC grant. - TCC: Microphone + Accessibility + AppleEvents.
NSMicrophoneUsageDescription/NSAccessibilityUsageDescription/NSAppleEventsUsageDescriptionlive inopenless-all/app/src-tauri/Info.plist. After a fresh build that resets TCC, the app must be fully quit and relaunched after granting Accessibility before the global hotkey tap installs. - Credentials live in the OS credential vault (macOS Keychain, Windows Credential Manager, Linux keyring) under service
com.openless.app. The legacy plaintext JSON (~/.openless/credentials.jsonon macOS/Linux,%APPDATA%\OpenLess\credentials.jsonon Windows) is only a migration source and is removed after a successful vault write. Never hard-code keys or include legacy credential files in logs, exports, build artifacts, or bug reports. - Per-user data:
- macOS:
~/Library/Application Support/OpenLess/{history.json, preferences.json, dictionary.json}— capped at 200 history entries. Do not renamedictionary.jsontovocab.json(drops user data). - Windows:
%APPDATA%\OpenLess\ - Linux:
$XDG_DATA_HOME/OpenLess
- macOS:
Release pipeline
Push a v*-tauri tag → .github/workflows/release-tauri.yml builds macOS arm64 .dmg and Windows x64 .msi. macOS Developer ID signing + notarization runs only when APPLE_CERTIFICATE / APPLE_CERTIFICATE_PASSWORD / APPLE_ID / APPLE_PASSWORD / APPLE_TEAM_ID secrets are set; otherwise it falls back to ad-hoc signing with a CI warning.
When bumping versions, update both version fields: openless-all/app/package.json and openless-all/app/src-tauri/tauri.conf.json (and Cargo.toml).
Branch & release-channel workflow
Two-channel branching. Branch name = release channel.
beta— Beta channel (开发版). Default branch, integration buffer. All PRs targetbeta(nevermain). Beta builds may exist but are not pushed to general users — only opt-in users on the Beta channel see them.main— Stable channel (正式版). Always-releasable. Updated only bybeta → mainmerges performed by maintainers after a two-platform smoke build. Release tagsv<version>-tauriare pushed onmainand triggerrelease-tauri.yml(tag-driven; unaffected by branch renames).
Per-PR contract:
- Run the change locally on your target platform before opening the PR (build green + manual verification of the affected feature).
pr-agent.ymlruns one AI review pass per PR — treat it as advisory, do not iterate on it.- Keep AI rework rounds tight (1–2). If a fix resists, escalate to a human or restart with fresh context.
ci.ymlruns on push/PR for bothmainandbeta; no extra wiring needed when adding new branches offbeta.
For maintainers:
- Merge
beta → mainonly after the two-platform (macOS + Windows) smoke build passes. Beta work must not leak to Stable — that gate exists for a reason. - Tag
v<version>-taurionmain, not onbeta. The release workflow keys off the tag, but tagging onmainkeeps the release commit linear with the always-releasable line. - Avoid direct pushes to
mainoutside thebeta → mainmerge — it bypasses the smoke-test gate.
Channel distribution (manual-download opt-in):
- Tag convention.
v<v>-tauri→ Stable release (GitHubprerelease=false, manifestlatest-{tgt}-{arch}.json).v<v>-beta-tauri→ Beta release (GitHubprerelease=true, manifestlatest-{tgt}-{arch}-beta.json). The two manifest filenames never overlap, so the in-app updater endpoint (which is fixed at compile time to the no-suffix file) cannot pick up Beta releases. This is the physical isolation that guarantees Beta does not leak to Stable users. - Why not auto-update for Beta.
tauri-plugin-updater2.10'sBuilderdoes not exposeendpoints()— endpoints are only readable fromtauri.conf.jsonat build time and cannot be swapped at runtime. Rather than fork the plugin or write a custom updater (~500 lines, high risk), Beta opt-in is implemented as a manual-download flow: Settings → About has a "Join Beta channel" toggle that, when on, callsfetch_latest_beta_release(GitHub Releases API), shows the latest pre-release tag, and routes the user to the GitHub release page to download manually. No installer signing/install path needs to be re-implemented. - Where the wiring lives. Pref field:
UserPreferences::update_channel(types.rs). IPC:get_update_channel/set_update_channel/fetch_latest_beta_release(commands.rs). UI:BetaChannelControlinsideAboutMini(SettingsModal.tsx). i18n:settings.about.betaChannel*keys.
Release verification checklist (run after every tag push)
Run after pushing either a v*-tauri or v*-beta-tauri tag, before announcing the release:
- GitHub Release page matches expectation:
- Stable tag: not marked
Pre-release, in thereleases/latestredirect. - Beta tag: marked
Pre-release, not the target ofreleases/latest.
- Stable tag: not marked
- Release assets are channel-correct:
- Stable tag includes
latest-{darwin,windows,linux}-{aarch64,x86_64}.json+ their-mirror.jsonsiblings, without-betasuffix. - Beta tag includes
latest-{tgt}-{arch}-beta.json+-beta-mirror.json, without the no-suffix variant.
- Stable tag includes
- Stable user flow. Install a Stable build, click
Settings → About → Check for updates. After a Stable release: should offer the new version. After a Beta release only: should report "up to date" (Beta must not appear). - Beta user flow. In the same Stable build, toggle on
Join Beta channel. The latest Beta tag should appear (or "no Beta released yet"). Clicking the download button should open the corresponding GitHub release page. - Updater endpoint sanity.
curl -fsSL https://github.com/appergb/openless/releases/latest/download/latest-darwin-aarch64.jsonreturns the Stable manifest (version field matches the latest Stable tag). It should never return a Beta version, regardless of which tag was pushed most recently.
If any step fails, do not announce the release; investigate release-tauri.yml channel detection (endsWith(github.ref_name, '-beta-tauri')) and the OPENLESS_RELEASE_CHANNEL env propagation in the run logs.
Repo conventions
- Comments, log messages, user-facing strings, and most docs are in Simplified Chinese. UI strings additionally route through
react-i18next(src/i18n/{zh-CN,en}.ts) so we ship English alongside;zh-CN.tsis source of truth. - macOS hotkey monitor must use native
CGEventTap, neverrdev.rdevsynchronously callsTSMGetInputSourcePropertyfrom non-main threads, which macOS 14+ aborts viadispatch_assert_queue_fail→ SIGTRAP. macOS uses CGEventTap;rdevis only used on Linux/Windows. - Don't
NSApp.activateon the dictation path — it steals focus and breaks insertion. Only callset_activation_policy(Regular)+activateIgnoringOtherAppsfromshow_main_window/ mic-permission prompts, never fromstart_dictation. - Rust modules wrap shared mutable state with
Arc<Mutex<...>>(parking_lot). Keep that locking discipline when adding fields. - Rust modules depend only on
types.rs. New cross-module wiring goes incoordinator.rs, not in the leaf modules.
Adding a new module
- Add a
<name>.rs(or directory) underopenless-all/app/src-tauri/src/, importing only fromtypes. - Register it in
lib.rs(mod <name>;). - Wire it into
coordinator.rsand expose any frontend-callable surface viacommands.rs+invoke_handler!. - Add the matching TS wrapper in
openless-all/app/src/lib/ipc.ts(with a mock branch for browser dev).