Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ name: CI

on:
push:
branches: [main, dev]
branches: [main, beta]
pull_request:
branches: [main, dev]
branches: [main, beta]

jobs:
cross-platform:
Expand Down
24 changes: 17 additions & 7 deletions .github/workflows/release-tauri.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ jobs:
build:
permissions:
contents: write
# 渠道由 tag 后缀决定:
# v<v>-beta-tauri → beta 渠道(GitHub Release 标 prerelease,
# manifest 文件名带 -beta 后缀,正式版用户的 endpoint 拿不到)
# v<v>-tauri → stable 渠道(正式版,文件名沿用旧约定,向后兼容)
# workflow_dispatch / 非 tag 触发时 github.ref_name 不是 tag 字符串,
# endsWith 返回 false,回退为 stable,不改变现有 dispatch 行为。
env:
OPENLESS_RELEASE_CHANNEL: ${{ endsWith(github.ref_name, '-beta-tauri') && 'beta' || 'stable' }}
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -370,6 +378,8 @@ jobs:
OPENLESS_UPDATE_ARCH: ${{ matrix.updater-arch }}
OPENLESS_UPDATE_REPO: appergb/openless
OPENLESS_UPDATE_MIRROR_BASE_URL: https://fastgit.cc/https://github.com
# beta 渠道时输出 latest-{tgt}-{arch}-beta.json,stable 沿用旧文件名。
OPENLESS_RELEASE_CHANNEL: ${{ env.OPENLESS_RELEASE_CHANNEL }}
run: node scripts/write-updater-manifest.mjs

# ── 收集产物 ──
Expand Down Expand Up @@ -413,8 +423,7 @@ jobs:
path: |
openless-all/app/src-tauri/target/release/bundle/macos/*.app.tar.gz
openless-all/app/src-tauri/target/release/bundle/macos/*.app.tar.gz.sig
openless-all/app/src-tauri/target/release/bundle/latest-darwin-${{ matrix.updater-arch }}.json
openless-all/app/src-tauri/target/release/bundle/latest-darwin-${{ matrix.updater-arch }}-mirror.json
openless-all/app/src-tauri/target/release/bundle/latest-darwin-${{ matrix.updater-arch }}*.json
if-no-files-found: error

- name: Upload Windows artifacts
Expand All @@ -435,8 +444,7 @@ jobs:
path: |
openless-all/app/src-tauri/target/release/bundle/nsis/*.exe.sig
openless-all/app/src-tauri/target/release/bundle/msi/*.msi.sig
openless-all/app/src-tauri/target/release/bundle/latest-windows-x86_64.json
openless-all/app/src-tauri/target/release/bundle/latest-windows-x86_64-mirror.json
openless-all/app/src-tauri/target/release/bundle/latest-windows-x86_64*.json
if-no-files-found: error

- name: Upload Linux artifacts
Expand All @@ -457,8 +465,7 @@ jobs:
name: openless-linux-x64-updater
path: |
openless-all/app/src-tauri/target/release/bundle/appimage/*.AppImage.sig
openless-all/app/src-tauri/target/release/bundle/latest-linux-x86_64.json
openless-all/app/src-tauri/target/release/bundle/latest-linux-x86_64-mirror.json
openless-all/app/src-tauri/target/release/bundle/latest-linux-x86_64*.json
if-no-files-found: error

# ── tag 推送时,同步上传到 GitHub Release ──
Expand All @@ -469,7 +476,10 @@ jobs:
tag_name: ${{ github.ref_name }}
name: 'OpenLess ${{ github.ref_name }}'
draft: false
prerelease: false
# beta 渠道的 release 必须标 prerelease=true:GitHub UI 会折叠它,
# 普通用户看不到;同时只上传 latest-*-beta.json,正式版用户的
# endpoint(latest-*.json)永远不会被覆盖,保证 Beta 不溢出正式版。
prerelease: ${{ env.OPENLESS_RELEASE_CHANNEL == 'beta' }}
# Matrix jobs all upload assets to the same release. Generate notes once
# so macOS, Windows, and Linux jobs do not duplicate the release body.
generate_release_notes: ${{ matrix.updater-target == 'darwin' && matrix.updater-arch == 'aarch64' }}
Expand Down
42 changes: 42 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,48 @@ Push a `v*-tauri` tag → `.github/workflows/release-tauri.yml` builds macOS arm

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 target `beta`** (never `main`). 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 by `beta → main` merges performed by maintainers after a two-platform smoke build. Release tags `v<version>-tauri` are pushed on `main` and trigger `release-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.yml` runs 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.yml` runs on push/PR for both `main` and `beta`; no extra wiring needed when adding new branches off `beta`.

For maintainers:

- Merge `beta → main` only 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>-tauri` **on `main`**, not on `beta`. The release workflow keys off the tag, but tagging on `main` keeps the release commit linear with the always-releasable line.
- Avoid direct pushes to `main` outside the `beta → main` merge — it bypasses the smoke-test gate.

Channel distribution (manual-download opt-in):

- **Tag convention.** `v<v>-tauri` → Stable release (GitHub `prerelease=false`, manifest `latest-{tgt}-{arch}.json`). `v<v>-beta-tauri` → Beta release (GitHub `prerelease=true`, manifest `latest-{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-updater` 2.10's `Builder` does not expose `endpoints()` — endpoints are only readable from `tauri.conf.json` at 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, calls `fetch_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: `BetaChannelControl` inside `AboutMini` (`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:

1. **GitHub Release page** matches expectation:
- Stable tag: not marked `Pre-release`, in the `releases/latest` redirect.
- Beta tag: marked `Pre-release`, **not** the target of `releases/latest`.
2. **Release assets** are channel-correct:
- Stable tag includes `latest-{darwin,windows,linux}-{aarch64,x86_64}.json` + their `-mirror.json` siblings, **without** `-beta` suffix.
- Beta tag includes `latest-{tgt}-{arch}-beta.json` + `-beta-mirror.json`, **without** the no-suffix variant.
3. **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).
4. **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.
5. **Updater endpoint sanity.** `curl -fsSL https://github.com/appergb/openless/releases/latest/download/latest-darwin-aarch64.json` returns 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.ts` is source of truth.
Expand Down
71 changes: 64 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,16 @@ OpenLess does one thing: **turn speech into usable written text (especially AI p

- Tauri 2 + Rust backend + React/TS frontend. macOS 12+, Windows 10+.
- **Toggle and push-to-talk** recording modes. `Esc` cancels at any phase, including polish/insert.
- Volcengine streaming ASR + OpenAI Whisper-compatible batch ASR; Ark / DeepSeek / OpenAI-compatible chat-completions for polish.
- 4 output modes: raw, light polish, structured (**AI prompt mode**), formal.
- **Cloud ASR**: Volcengine streaming ASR, OpenAI Whisper-compatible batch ASR, Apple Speech (macOS).
- **Local ASR**: bundled Qwen3-ASR (0.6B / 1.7B) via vendored `antirez/qwen-asr`; Windows Foundry Local Whisper variants.
- **Polish providers**: Ark / DeepSeek / OpenAI / Doubao / Anthropic-compatible chat-completions, plus any OpenAI-compatible endpoint you bring.
- 4 output modes: raw, light polish, structured (**AI prompt mode**), formal. Plus a **translation hotkey** that converts speech directly into the configured target language ([#43](../../issues/43)).
- **Selection-ask QA panel** — separate hotkey opens a floating panel that runs voice Q&A against the highlighted text in any app ([#118](../../issues/118)).
- Main window: Overview / History / Vocab / Style / Settings. Persistent tray icon. Mini status capsule floating on screen.
- **Bilingual UI** — Settings → Language switches between 简体中文 and English (auto-detects on first launch).
- **Multilingual UI** — Settings → Language switches between 简体中文 / 繁體中文 / English / 日本語 / 한국어 (auto-detects on first launch).
- **In-app auto-update** — Settings → About → Check button; signed updater artifacts via Tauri updater plugin.
- **Beta channel (opt-in)** — Settings → About → Join Beta channel exposes the latest pre-release build for manual download; Beta releases never reach Stable users automatically (see [Contributing workflow](#contributing-workflow)).
- **Distribution channels** — direct DMG/EXE from [Releases](../../releases), Homebrew Cask (`brew install --cask openless`), Windows installer.
- **Single-instance lock** — prevents two OpenLess processes from racing the same hotkey edge.
- Dictionary entries injected as Volcengine ASR `context.hotwords` and as semantic hints during polish; hits accumulate per session.
- Platform-native global hotkey: CGEventTap on macOS, low-level keyboard hook (`WH_KEYBOARD_LL`) on Windows.
Expand Down Expand Up @@ -208,6 +213,37 @@ Logs: `~/Library/Logs/OpenLess/openless.log` (macOS) / `%LOCALAPPDATA%\OpenLess\

**Windows build** — see [`openless-all/README.md`](openless-all/README.md) for MSVC vs GNU/MinGW routes.

## Contributing workflow

OpenLess uses a two-channel branching model.

- **`beta`** — the **Beta channel**. Default branch and integration buffer; all in-progress development lands here. Beta builds may exist but are **not pushed to regular users** — they only reach people who explicitly opt into the Beta channel.
- **`main`** — the **Stable channel (正式版)**. Always-releasable. The build everyone gets by default.

```text
your fork / topic branch
│ (test locally on your target platform first)
PR → beta ← AI review (one pass, advisory only)
│ ← maintainer lightweight glance (scope, cross-module impact)
merged into beta
│ (periodically, after a two-platform smoke build)
merged into main → tag `v<version>-tauri` → release CI → Stable users
```

Rules of thumb:

- **Open PRs against `beta`, never against `main`.** GitHub already defaults the base branch to `beta` for new PRs.
- **Verify the change on your target platform before opening the PR** — build green is necessary, manual verification is required.
- **AI review runs once per PR and is advisory.** Don't loop on it. Apply your judgment.
- **Keep AI rework rounds tight (1–2).** If a fix resists, ask a human or restart with fresh context — multi-round AI back-and-forth tends to do more harm than good here.
- **Beta work must not leak to Stable.** `main` only receives merges from `beta`, performed by maintainers after a successful two-platform smoke build. No direct pushes to `main`.
- **Stable releases are cut from `main`** by pushing a `v<version>-tauri` tag — see the maintainer release checklist below.

Beta release distribution (manual-download opt-in): the in-app updater always reads the Stable manifest, so regular users never get Beta builds via auto-update. Users who want to try Beta open **Settings → About**, flip "Join Beta channel", and download the latest Beta installer manually from the link the app fetches from GitHub. Tag convention: `v<version>-beta-tauri` produces the Beta release (marked GitHub pre-release; manifest written as `latest-{tgt}-{arch}-beta.json`); `v<version>-tauri` produces the Stable release. The two manifest files never overlap, so Stable users' updater feed cannot pick up Beta releases.

## Credentials

Credentials live in the OS credential vault (service = `com.openless.app`): macOS Keychain, Windows Credential Manager, or Linux keyring. A legacy plaintext JSON file is read only as a migration source and removed after a successful vault write:
Expand Down Expand Up @@ -256,7 +292,7 @@ The main window is organized as Home / History / Dictionary / Settings. The Dict

## Architecture

The active implementation is Tauri 2 (`openless-all/app/`). Auto-updates ride on the Tauri updater plugin; signed updater artifacts are produced by CI on every `v*-tauri` tag.
The active implementation is Tauri 2 (`openless-all/app/`). Releases are split into two channels: **Stable** (`v<v>-tauri` tag, auto-updated to all users) and **Beta** (`v<v>-beta-tauri` tag, GitHub pre-release, manually downloaded by opt-in users). Signed updater artifacts are produced by CI on every release tag.

**Tauri backend (Rust)** — each module depends only on `types.rs`:

Expand Down Expand Up @@ -292,10 +328,31 @@ Planned but not yet shipped:

## Maintainer release checklist

- Bump version in `openless-all/app/package.json`, `src-tauri/tauri.conf.json`, and `src-tauri/Cargo.toml`.
OpenLess ships two release channels. Branch name = channel name (see [Contributing workflow](#contributing-workflow)).

### Common prep (both channels)

- Bump version in **all five** files: `package.json`, `package-lock.json` (root + nested entry under `packages.""`), `src-tauri/tauri.conf.json`, `src-tauri/Cargo.toml`, `Cargo.lock` (look for the `name = "openless"` block). CI's `Verify version sync` step will fail the build otherwise.
- Run `INSTALL=0 ./scripts/build-mac.sh` and confirm the `.app` launches.
- Verify on a clean macOS box: permission flow, hotkey, recording, ASR, polish, insertion, clipboard fallback.
- Push a `v<version>-tauri` tag — CI builds + signs the updater artifacts and the macOS `.dmg` + Windows `.msi`. The updater needs `TAURI_SIGNING_PRIVATE_KEY` repo secret (matching the pubkey in `tauri.conf.json`).
- Smoke-test on a clean machine: permission flow, hotkey, recording, ASR, polish, insertion, clipboard fallback.
- Confirm `TAURI_SIGNING_PRIVATE_KEY` and (for macOS) the Apple signing/notarization secrets are set on the repo.

### Beta channel — `v<v>-beta-tauri`

1. Land changes onto the `beta` branch via PR review.
2. Push tag **on `beta`**: `git tag v<v>-beta-tauri && git push origin v<v>-beta-tauri`.
3. CI tags the GitHub Release as `Pre-release` and uploads only `latest-{tgt}-{arch}-beta.json` updater manifests. Stable users' `releases/latest` redirect is unaffected.
4. Announce in the appropriate channel (issue thread, QQ group) that opt-in Beta users can grab it from Settings → About → Join Beta channel.

### Stable channel — `v<v>-tauri`

1. Merge `beta → main` after the Beta release has soaked enough (or run a final two-platform smoke build directly).
2. Push tag **on `main`**: `git tag v<v>-tauri && git push origin v<v>-tauri`.
3. CI publishes a normal GitHub Release and uploads `latest-{tgt}-{arch}.json` (no `-beta` suffix). All Stable users get the update through the in-app updater.

### Post-release verification (always run)

Run the 5-step checklist in [`CLAUDE.md` → Branch & release-channel workflow → Channel distribution](CLAUDE.md): page status (pre-release flag), asset filename channel-correctness, Stable user flow, Beta opt-in flow, raw endpoint sanity.

## Acknowledgements

Expand Down
Loading
Loading