Commit Graph
690 Commits
Author SHA1 Message Date
baiqing 84ee3d96d5 fix(coordinator): read show_capsule pref inside main-thread closure
Addresses pr_agent "Stale State" review on PR #389. show_capsule was
read on the audio callback thread and snapshotted into the
run_on_main_thread closure. If the user toggled the preference between
the audio-thread emit and the main-thread closure firing (one or two
frames later), the queued window show/hide could apply the old
preference and briefly flash the wrong UI state.

Move that read inside the closure so the latest user pref always wins.

Why visible / translation stay snapshotted: they're the payload of
THIS specific capsule:state event — the whole point is to tell the JS
frontend "this is the state at the moment emit_capsule was called".
Re-reading them inside the closure would mean the OS-level window state
and the JS-side payload disagree (the closure lands one frame later
than the emit_to that already shipped).

No behavior change in the steady state; eliminates a 16–33 ms stale-
pref window when the user changes show_capsule mid-dictation.
2026-05-10 08:08:07 +08:00
baiqing faf02ad47e fix(coordinator): marshal capsule window.show/hide to main thread
emit_capsule does two things — (a) push a CapsulePayload event to the
JS frontend via app.emit_to, and (b) flip the OS-level window visibility
via window.show/hide and reposition it via maybe_position_capsule_*.
Path (a) is safe from any thread (Tauri event bus marshals internally).
Path (b) calls NSWindow / HWND APIs that expect the main thread —
but emit_capsule is invoked at ~30 Hz from the cpal process_callback
(audio thread) via the level_handler in coordinator/dictation.rs.

On macOS this risks dispatch_assert_queue_fail SIGTRAP if the AppKit
runloop assertion fires; on Windows SendMessage can deadlock against
the GUI thread; in either case the audio callback can miss its deadline
and trigger kAudioUnitErr_TooManyFramesToProcess on macOS. The 30 Hz
throttle limits frequency but does not change per-call risk.

Fix: wrap window.show / window.hide / maybe_position_capsule_bottom_center
/ show_capsule_window_no_activate / restore_main_window_key_if_active
in app.run_on_main_thread. The closure captures an Arc<Inner> clone +
AppHandle clone + Copy primitives (state, translation, show_capsule,
visible) — no payload clone needed. emit_to stays on the calling thread
since it is internally thread-safe, preserving the existing happens-
before relationship between event payload and window state on the JS
frontend (frontend processes events asynchronously regardless).

This pattern is already established in the codebase — see
coordinator.rs::stop_qa_hotkey_listener (line 326) for the same
reasoning around Carbon hotkey drop on the wrong thread (issue #169).

Audit ID 3.2.2 (CONFIRMED 高).

Test: 31 lib tests pass. Functional verification (no SIGTRAP on long
recordings, capsule still appears at start of dictation) requires
running the app — to be done after PR ships.
2026-05-10 01:32:15 +08:00
Chris233 b1c387265b Merge pull request #385 from H-Chris233/fix/issue-384-bailian-asr
接入百炼 DashScope 实时 ASR
2026-05-10 00:29:14 +08:00
H-Chris233 0a2b6bd14c Avoid missed Bailian task-start notifications
Bailian uses Notify::notify_waiters when the remote task starts. send_last_frame must register the Notified future before checking the started flag so a task-started event cannot land between the check and waiter registration.\n\nConstraint: DashScope stop should not falsely time out when task-started races with user stop.\nRejected: Replace Notify with polling | broader and less direct than the Tokio enable pattern.\nConfidence: high\nScope-risk: narrow\nTested: cargo check --manifest-path openless-all/app/src-tauri/Cargo.toml\nTested: cargo test --manifest-path openless-all/app/src-tauri/Cargo.toml --lib asr::bailian::tests\nCo-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-09 23:30:39 +08:00
H-Chris233 00f150a091 Prevent stale Bailian startup resources after connect failure
Bailian startup stores the ASR instance before opening the remote session so recorder startup audio can be buffered. If the websocket setup then fails in the active startup path, the provider must be cancelled before the coordinator returns to idle. The partial-result fallback remains intentional and is now documented as matching the existing Volcengine no-loss behavior.\n\nConstraint: OpenLess ASR paths prefer not losing already recognized user speech.\nRejected: Treat every websocket close with partial text as failure | would diverge from Volcengine fallback semantics.\nConfidence: high\nScope-risk: narrow\nTested: cargo check --manifest-path openless-all/app/src-tauri/Cargo.toml\nTested: cargo test --manifest-path openless-all/app/src-tauri/Cargo.toml --lib\nTested: cargo test --manifest-path openless-all/app/src-tauri/backend-tests/Cargo.toml\nCo-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-09 21:59:55 +08:00
H-Chris233andOmX 9551786cae Enable DashScope realtime ASR without changing recorder flow
百炼传统实时识别协议可以直接消费 OpenLess 已有的 16 kHz mono PCM,所以新增独立 ASR provider 并复用现有 DeferredAsrBridge / coordinator 会话状态机,而不是引入另一条录音链路。Settings 侧增加百炼选项、默认北京 endpoint、默认 fun-asr-realtime 模型,并允许填写可选 vocabulary_id 下发已创建的百炼热词表。

Constraint: issue #384 要求接入百炼 DashScope 实时 ASR,并保持北京/新加坡 endpoint 可配置。
Constraint: 凭据仍必须走系统凭据库,新增 asr.vocabulary_id 只作为 active ASR provider 的可选 provider 设置。
Rejected: 同时实现 qwen3-asr-flash OpenAI Realtime 协议 | 协议和结果事件不同,超出最小更改范围。
Rejected: 自动从本地词汇表创建百炼热词表 | 需要远端资源生命周期和用户确认,不能静默替用户管理供应商资源。
Confidence: high
Scope-risk: moderate
Directive: 百炼 provider 当前走传统 /api-ws/v1/inference 协议;不要把 qwen realtime 事件格式混进同一实现。
Tested: cargo check --manifest-path openless-all/app/src-tauri/Cargo.toml
Tested: cargo test --manifest-path openless-all/app/src-tauri/Cargo.toml --lib
Tested: cargo test --manifest-path openless-all/app/src-tauri/backend-tests/Cargo.toml
Tested: cd openless-all/app && npm run build
Not-tested: 需要真实 DashScope API Key 和麦克风环境才能完成线上 macOS dictation 验证。
Related: #384
Co-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-09 21:23:53 +08:00
Chris233 4475e026c4 Merge pull request #383 from H-Chris233/fix/issue-312-style-master-toggle
修复 Style 总开关关闭后的兜底风格
2026-05-09 21:01:07 +08:00
H-Chris233 6341f19dec Keep style master off state stable across default changes
Review noted that changing defaultMode after turning Style master off made the switch look on again because enabledModes still reflected the old fallback pair.\n\nWhen master is already off, default-mode changes now persist the whole style preference set through setSettings so enabledModes is recomputed to raw plus the new default. Normal enabled master behavior still uses the narrower setDefaultPolishMode IPC.\n\nConstraint: Issue #312 selected raw/default fallback semantics for master-off state.\nRejected: Add a separate persisted masterEnabled flag | broader schema and migration change for a narrow UI consistency bug.\nConfidence: high\nScope-risk: narrow\nTested: cd openless-all/app && npx tsx src/lib/stylePrefs.test.ts\nTested: cd openless-all/app && npm run build\nNot-tested: Manual Style page click flow in packaged Tauri app.\nRelated: https://github.com/appergb/openless/pull/383\nCo-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-09 20:31:40 +08:00
H-Chris233 91769e0e4c Keep style master toggle from clearing fallback modes
Style 页总开关关闭时沿用现有注释语义:保留 raw 和当前 default mode,避免把 enabledModes 持久化为空集合。\n\n将 master toggle 的 off 判定抽到 stylePrefs,确保仅剩 raw/default 时 UI 仍显示为关闭,并用现有手写测试覆盖最终保存值。\n\nConstraint: Issue #312 要求总开关实现与注释一致,并覆盖关闭后的 enabledModes。\nRejected: 保持 enabledModes=[] 仅改注释 | 会让切换风格热键面对空集合,偏离 raw/default 兜底语义。\nConfidence: high\nScope-risk: narrow\nTested: cd openless-all/app && npx tsx src/lib/stylePrefs.test.ts\nTested: cd openless-all/app && npm run build\nNot-tested: Real Tauri UI click path on macOS/Windows.\nRelated: https://github.com/appergb/openless/issues/312\nCo-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-09 19:27:52 +08:00
Chris233 9ccaf0571c Merge pull request #382 from weikeyi/beta
fix(windows): 修正 LeftOption 映射到左 Alt
2026-05-09 17:34:09 +08:00
weikeyi 941542c6e7 chore(release): 同步 package-lock 版本到 1.2.24-6 2026-05-09 17:22:21 +08:00
wei keyi 9cdb58d3ae Merge branch 'appergb:beta' into beta 2026-05-09 17:03:47 +08:00
baiqing 7b39fd57aa chore: bump version to 1.2.24-6 (Beta-6)
Beta-6 包含的合并:
- #379 fix(recorder): watchdog 在 sleep 醒来后重检 stop_flag,消除停采误报
- #380 fix(commands): wrap tray refresh in run_on_main_thread 修主线程死锁
- #381 feat(ui): consolidate footer/nav, sliding indicator, hover cues, top-right saved toast
- #378 拆分 coordinator 子状态机模块(间接合入)

Tag: v1.2.24-6-beta-tauri 推到 main 后触发 release-tauri.yml Beta 流水线。
2026-05-09 16:31:32 +08:00
TRIPandbaiqing fa6038f92d feat(ui): consolidate footer/nav, sliding indicator, hover cues, top-right saved toast (#381)
整体 UI 整理 + 跨页面"已保存"提示统一到右上角小 pill。

## Footer 简化
- 删 账号 icon(= openSettings('providers') 跟齿轮重复入口)
- 删 帮助 popover(version + 2 条外链跟 Settings → 关于 重复)
- 删 检查更新 按钮(Settings.about 已有同等控件)
- BETA 角标从 sidebar 移到右下角紧贴版本号
- 设置 icon 内嵌 12px 不再贴右边角

## Sidebar / Settings 三处 nav 统一
- 滑动指示器:用一个 absolute pill,currentTab 切换时 top/height 平滑过渡
  到目标 button(cubic-bezier(0.16,1,0.3,1) 360ms,无 overshoot 的 Apple
  ease-out-quint),不是各按钮瞬切背景。
- Hover 三段视觉层次:
    基础态 ink-3 中灰文字 + 透明底
    Hover  ink 全黑文字 + 浅灰底(图标通过 currentColor 同步加深)
    选中  ink 全黑文字 + 白色滑动 pill 底
  用 .ol-nav-btn class + :hover 实现,inline color 让位给 CSS(CSS 不能盖
  inline style 是关键)。

## "已保存" toast 改右上角小 pill
- 新增 components/SavedToast.tsx + lib/savedEvent.ts(emitSaved 事件总线)
- Translation / SelectionAsk 内联 banner 改为锚到 ol-thinscroll 控制台卡
  右上角的 absolute pill;scroll wrapper 加 position:relative
- Settings → ASR 配置:CredentialField API key 内联"已保存"挪到 SettingsModal
  右上角统一 pill(offsetStyle: right:54 避开关闭按钮);只保留持续性的
  readError 在原位标识字段不可用

## Translation / SelectionAsk 简化
- 删跟 Settings.shortcuts 重复的快捷键卡(用户改键统一在 Settings 完成)
- SelectionAsk "历史保存"从全宽 Card 改成左对齐的小模块(标签 + toggle 紧贴)
- PageHeader 砍 kicker+desc → 单行 title
- 使用方法卡去 inset 提示块(保留 5 步 ol)

## Overview 收头
- PageHeader 单行 title(去 kicker / desc / 右侧"按 X 开始"pill)

Co-authored-by: baiqing <lbx12309@icloud.com>
2026-05-09 16:29:52 +08:00
TRIPandbaiqing 14bfa5f749 fix(commands): wrap tray refresh in run_on_main_thread to avoid macOS UI freeze (#380)
set_settings 和 set_default_polish_mode 是同步 Tauri commands,跑在 IPC handler
线程。它们直接调 refresh_tray_microphone_menu,里面 tray.set_menu(...) 改 macOS
NSStatusItem 必须在主线程上做。从 IPC 线程直调会触发 macOS dispatch queue
死锁,导致用户改任何偏好开关后整个 UI 永久卡死、所有按键无响应。

修复:把两处 tray 刷新都包到 app.run_on_main_thread(move || ...) dispatch 到主线程,
IPC 线程立即返回不阻塞。这跟 lib.rs:558 里 start_tray_microphone_watcher 已经
在用的 pattern 完全一致(issue #169 stop_qa_hotkey_listener 同样用法)。

防御性扫描结论:整个项目里 tray.set_menu() 只在 refresh_tray_microphone_menu
(lib.rs:518)一处出现,所有 5 个调用点已确认安全:
  - lib.rs:207 tray hover event:tray 事件本身就在主线程
  - lib.rs:559 麦克风设备 watcher:已有 run_on_main_thread
  - lib.rs:605 tray 菜单事件:本身在主线程
  - commands.rs:160 set_settings:本提交修复
  - commands.rs:944 set_default_polish_mode:本提交修复

其它 sync IPC commands(set_dictation/qa/translation/switch_style/open_app_hotkey)
通过 coord.refresh_*_hotkey() 间接刷新,那些函数内部已包了 run_on_main_thread;
不碰 AppKit 的 commands 安全。

Co-authored-by: baiqing <lbx12309@icloud.com>
2026-05-09 16:29:45 +08:00
Chris233 feadbfa70b Merge pull request #378 from H-Chris233/fix/issue-296-coordinator-modules
拆分 coordinator 子状态机模块
2026-05-09 16:16:21 +08:00
weikeyi af2048e5ca fix(windows): map LeftOption to left Alt 2026-05-09 16:15:47 +08:00
Chris233 2198f0cbf8 Merge pull request #379 from appergb/fix/recorder-watchdog-stop-race
fix(recorder): watchdog 在 sleep 醒来后重检 stop_flag,消除停采误报
2026-05-09 16:09:31 +08:00
baiqing 1bb38ef0f7 fix(recorder): watchdog 在 sleep 醒来后重检 stop_flag,消除停采误报
当用户松开 hotkey 进入 Listening → Processing 转移时,coordinator 调
rec.stop() 设置 stop_flag 并 pause cpal Stream。但 watchdog 线程此时
可能正卡在 1 秒的 sleep 里:sleep 结束后旧逻辑直接读 last_callback_time,
看到"已 >3 秒未更新"就触发 EngineFailed("录音回调静默停止 N 秒"),
让 coordinator 把正常停采当成引擎崩溃,胶囊弹出 "audio engine failed"。

复现日志(~/Library/Logs/OpenLess/openless.log):

  27.011  [recorder] cb#4250 ...                <- 最后一次回调
  27.054  [asr] server JSON ...                 <- ASR 仍在收尾
  ~27.5   end_session 调 rec.stop() 设置 stop_flag + pause cpal
  31.667  [ERROR] [recorder] watchdog: 录音回调已停止 4 秒,触发错误恢复
  31.667  [ERROR] [coord] recorder runtime error: audio engine failed

修复:watchdog sleep 醒来后立即再 load 一次 stop_flag,flag 已置位则
直接 break 退出,不再做 elapsed 判定。这样:

- 主动停采路径(hotkey-release / cancel):watchdog 静默退出,无误报
- 真故障路径(CoreAudio 设备掉线、OS 音频复位):stop_flag 仍为 false,
  watchdog 照常按 3 秒静默阈值触发 EngineFailed,行为不变

附 18 行注释解释 race,避免后续重构时被无意识回退。
2026-05-09 14:06:20 +08:00
H-Chris233andOmX 14813322dd Reduce coordinator refactor risk with focused modules
coordinator.rs had accumulated dictation lifecycle, QA panel state, and shared recording resources in one file. This split keeps behavior unchanged while moving the dictation edge/session helpers, QA panel state helpers, and resource ownership helpers into child modules under coordinator/.

Constraint: Issue #296 asks for module boundaries after Rust state-machine coverage is in place

Rejected: Split every helper into separate public modules | larger churn with no behavior benefit

Confidence: high

Scope-risk: moderate

Directive: Keep cross-module wiring in coordinator; leaf modules should remain coordinator-private

Tested: cargo check --manifest-path openless-all/app/src-tauri/Cargo.toml

Tested: cargo test --manifest-path openless-all/app/src-tauri/Cargo.toml --lib

Tested: cargo test --manifest-path openless-all/app/src-tauri/backend-tests/Cargo.toml

Co-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-09 13:19:22 +08:00
Chris233 15442b5aad Merge pull request #374 from H-Chris233/fix/backend-rust-unit-tests
让 Windows CI 实际运行 Rust-only 后端单测
2026-05-09 12:42:29 +08:00
Chris233 9db6de34dc Merge pull request #375 from katanumahotori/feat/whisper-prompt-vocabulary
feat(asr): forward dictionary entries to Whisper prompt
2026-05-09 12:06:15 +08:00
H-Chris233 c63c69fb2f Lock recorder resampling and RMS coverage
The backend-only test PR needs to cover the concrete recorder paths named in the ticket, not just empty input handling. This adds focused Rust unit coverage for non-target sample-rate resampling, emitted PCM shape, scaled RMS levels, and peak tracking while keeping the production recorder unchanged.\n\nConstraint: Scope is Rust-only backend tests; do not launch or link extra Tauri runtime paths for Windows CI.\nConfidence: high\nScope-risk: narrow\nTested: cargo test --manifest-path openless-all/app/src-tauri/backend-tests/Cargo.toml recorder:: -- --nocapture\nTested: cargo test --manifest-path openless-all/app/src-tauri/backend-tests/Cargo.toml\nTested: cargo test --manifest-path openless-all/app/src-tauri/Cargo.toml --lib\nCo-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-09 11:40:39 +08:00
H-Chris233 044799e705 Cover backend edge tests in Rust-only harness
The Rust-only backend harness already ran existing hotkey, recorder, and insertion unit tests, but the review could not see target edge coverage directly in this diff. Add focused edge tests for modifier trigger filtering/re-emission, recorder callback no-op inputs, and insertion fallback status so the coverage is explicit in the changed files.\n\nConstraint: Keep scope to backend Rust unit tests and avoid app/runtime integration.\nRejected: Add UI or Tauri runtime tests | conflicts with the Rust-only backend boundary.\nConfidence: high\nScope-risk: narrow\nTested: cargo test --manifest-path openless-all/app/src-tauri/backend-tests/Cargo.toml\nTested: cargo test --manifest-path openless-all/app/src-tauri/Cargo.toml --lib\nTested: git diff --check\nNot-tested: GitHub Windows runner result after push.\n\nCo-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-09 10:07:38 +08:00
lightnovel0 403985208d feat(asr): forward dictionary entries to Whisper prompt parameter
The Vocabulary screen advertises that entries are passed to "both ASR
hotword and post-processing model context", and that contract holds for
Volcengine ASR (which receives them as hotwords) and the Polish LLM
(which receives them as a system-prompt block). Whisper-compatible
providers (whisper / siliconflow / zhipu / groq) were the gap: their
client never set the OpenAI `prompt` field, so the dictionary had no
effect on the STT stage at all.

That gap shows up as inconsistent transliteration of registered proper
nouns. A user who registers "梁山泊" still gets "両山泊" / "良山泊" /
"りょうざんぱく" from Whisper, and the Polish LLM cannot always recover
once the script choice has already drifted.

This change:

- Adds an optional `prompt: Option<String>` to `WhisperBatchASR` and
  attaches it as a `prompt` multipart field when non-blank. Empty/blank
  prompts are skipped defensively so they cannot trigger
  provider-specific 4xx responses.
- Adds `build_prompt_from_phrases`, a pure helper that joins enabled
  vocabulary phrases with `, ` and a trailing period, capped at 240
  characters (Whisper's published limit is 244 tokens; CJK is worst-case
  ~1 token/char, so 240 chars is the safe cap). Over-budget entries are
  skipped individually rather than truncating mid-list, which preserves
  registration order and lets short later entries still ride along when
  one earlier entry is unusually long.
- Wires `enabled_phrases(inner)` through the helper at the existing
  Whisper-compatible branch in `coordinator.rs`, mirroring how the
  Volcengine branch already builds its hotword list.

Existing call sites unchanged in behavior when the dictionary is empty
(`None` prompt → no `prompt` field on the request). No new dependencies.

Tests in `asr::whisper::tests` cover: empty / all-blank input returns
None; single and multi phrase joining; per-entry trimming; mid-list
blanks are skipped without breaking the join; an over-long single entry
is dropped while shorter entries that follow it are kept; the resulting
string never exceeds the budget; truncation prefers earlier entries.
2026-05-09 10:00:18 +09:00
H-Chris233 02ccac2dd5 Make Rust-only backend test target explicit
The backend-tests crate is intentionally an integration-test harness, but declaring the target explicitly makes Cargo discovery and review intent unambiguous. The harness comment also records that path-included modules run their own cfg(test) unit tests for hotkey, recorder, and insertion without linking the full Tauri library.\n\nConstraint: Stay within Rust-only backend tests; do not add a Tauri lib/runtime target.\nRejected: Add src/lib.rs just to satisfy target discovery | would introduce an unused crate surface instead of the actual test harness.\nConfidence: high\nScope-risk: narrow\nTested: cargo test --manifest-path openless-all/app/src-tauri/backend-tests/Cargo.toml -- --list | grep -E '^(hotkey|recorder|insertion)::'\nTested: cargo test --manifest-path openless-all/app/src-tauri/backend-tests/Cargo.toml\nTested: git diff --check\nNot-tested: GitHub Windows runner rerun result pending after push.\n\nCo-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-09 08:30:23 +08:00
H-Chris233 f339209394 Align shortcut tests with platform modifier semantics
Windows intentionally normalizes cmd/command shortcut modifiers to ctrl, while macOS and Linux keep the super bit. The Rust-only backend test crate now executes these source tests on Windows, so the assertion must follow the same platform contract as production parsing instead of assuming the non-Windows result.\n\nConstraint: Keep the fix inside Rust backend tests and avoid changing shortcut behavior.\nRejected: Change normalize_modifier_tag to keep cmd as super on Windows | would alter user-facing hotkey semantics.\nConfidence: high\nScope-risk: narrow\nTested: cargo test --manifest-path openless-all/app/src-tauri/backend-tests/Cargo.toml\nTested: cargo test --manifest-path openless-all/app/src-tauri/Cargo.toml --lib\nNot-tested: GitHub Windows runner rerun result pending after push.\n\nCo-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-09 08:00:21 +08:00
H-Chris233andOmX 029232a269 Make backend state tests runnable without Tauri
Windows cannot reliably execute the full Tauri lib test harness in CI because the process exits before Rust tests start in the desktop runtime loader path. Split the coordinator session state transitions into a Tauri-free Rust module used by coordinator.rs, and add a small Rust-only backend test crate that path-includes coordinator/hotkey/recorder/insertion logic for real Windows execution.

Constraint: User scoped this follow-up to backend-only Rust-to-Rust tests, not app/runtime smoke tests.
Constraint: Existing Windows CI still keeps full lib --no-run cfg/link coverage.
Rejected: Keep trying to execute cargo test --lib on Windows | it starts the full Tauri-linked test binary and fails before unit tests run.
Rejected: Rework release/runtime DLL handling | outside the backend unit-test scope.
Confidence: high
Scope-risk: moderate
Directive: Keep Windows actual unit execution on the Rust-only backend-tests manifest unless the full Tauri lib harness is proven runnable on clean Windows CI.
Tested: cargo test --manifest-path openless-all/app/src-tauri/backend-tests/Cargo.toml
Tested: cargo test --manifest-path openless-all/app/src-tauri/Cargo.toml --lib
Tested: cargo check --manifest-path openless-all/app/src-tauri/Cargo.toml
Tested: python yaml parse for .github/workflows/ci.yml; git diff --check
Not-tested: Windows runner execution; intended to be verified by GitHub Actions.
Related: #295
Co-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-09 07:44:57 +08:00
Chris233 1aa4f9b494 Merge pull request #371 from millionart/codex/windows-tray-style-sync
fix: sync Windows tray style selection
2026-05-08 21:14:10 +08:00
millionart 19a87a4410 fix: sync Windows tray style selection 2026-05-08 20:30:23 +08:00
Chris233 7d77044b97 Merge pull request #361 from H-Chris233/fix/issue-295-rust-unit-tests
为 Rust 后端补充单元测试门禁
2026-05-08 20:19:58 +08:00
TRIPandbaiqing b44cb998af fix(installer): unregister TSF IME before replacing OpenLessIme.dll on upgrade (#321) (#369)
Auto-update was failing on Windows because OpenLessIme.dll is a TSF input-method
DLL: once registered, Windows maps it into every text-input client process
(browsers, IDEs, chat apps...). The installer's PREINSTALL hook tried to
overwrite the file directly while it was still mapped, hitting a sharing
violation and aborting the update.

NSIS hook (openless-ime-hooks.nsh):
- PREINSTALL now calls OPENLESS_IME_UNREGISTER_X86/X64 first to release the
  COM/TSF registration. On a fresh install this is a harmless no-op (existing
  unregister macros log the exit code without aborting).
- DLL bytes are written to OpenLessIme.dll.new and atomically renamed into
  place. If the old DLL is still locked by a TSF client process, Delete fails
  and we fall back to Rename /REBOOTOK + SetRebootFlag — Windows queues the
  replacement via PendingFileRenameOperations and the new bytes activate after
  the next reboot. Same TSF registration path is preserved, so re-registration
  in POSTINSTALL stays valid across the reboot.

WiX (openless-ime.wxs):
- UnregisterOpenLessImeX86/X64 now also fire Before InstallFiles when a
  previous version is installed (Installed AND NOT REMOVE). Original condition
  (REMOVE="ALL", Before RemoveFiles) only covered the dedicated uninstall
  sequence; auto-update / minor-upgrade skipped it entirely and ran straight
  into the same sharing violation as NSIS.

Bumps to 1.2.24-5 (Beta-5).

Co-authored-by: baiqing <lbx12309@icloud.com>
2026-05-08 19:58:05 +08:00
H-Chris233 ffeaa243d9 Keep backend test CI executable on Windows
The #295 unit suite now runs in CI, but the Windows clean runner exits before the lib test harness starts with STATUS_ENTRYPOINT_NOT_FOUND when optional native runtime entrypoints are absent. Windows still needs cfg/link coverage, while macOS and Linux can execute the shared Rust unit suite.

Constraint: Preserve the new Rust test gate without making optional Windows native runtime installation a CI prerequisite.
Rejected: Install Foundry/Windows App Runtime into every CI run | broader, slower, and unrelated to backend unit-test coverage.
Confidence: high
Scope-risk: narrow
Directive: Do not switch Windows back to executing the lib test binary until the optional native runtime dependency is present on the runner.
Tested: cargo test --manifest-path openless-all/app/src-tauri/Cargo.toml --lib
Not-tested: Windows CI rerun before push.
2026-05-08 19:15:54 +08:00
H-Chris233 b082805158 Close backend test blind spots from review
The first #295 pass made cargo tests visible but still left the reviewer-requested state-machine, modifier-edge, and insertion fallback contracts too implicit. This adds narrow unit coverage around those contracts without refactoring runtime flow.\n\nConstraint: Address review compliance with minimal production changes.\nRejected: Split coordinator or hotkey adapters for testability | broader than the issue and riskier for the active PR.\nConfidence: high\nScope-risk: narrow\nTested: cargo test --manifest-path openless-all/app/src-tauri/Cargo.toml --lib; cargo check --manifest-path src-tauri/Cargo.toml; npm run build\nNot-tested: Live macOS/Windows native hotkey hooks and clipboard/AX behavior.
2026-05-08 15:35:34 +08:00
H-Chris233 88e8418982 Make backend regressions visible in CI
Issue #295 calls out that core Rust paths have been too dependent on manual app runs. Keep the change narrow by covering existing pure state/audio/clipboard helpers instead of refactoring coordinator or touching hardware-facing code, then wire the existing lib test suite into the lightweight CI lane.

Constraint: Global hotkey, recorder, and insertion integration still require real OS permissions and devices.

Rejected: Split coordinator before adding tests | broader refactor would raise review risk and is tracked separately.

Confidence: high

Scope-risk: narrow

Directive: Keep Rust CI on --lib unless integration tests get explicit OS fixtures.

Tested: npm run build

Tested: cargo test --manifest-path src-tauri/Cargo.toml --lib

Tested: cargo check --manifest-path src-tauri/Cargo.toml

Not-tested: Live microphone, global hotkey, clipboard, and insertion device paths.

Related: #295
2026-05-08 15:18:40 +08:00
Chris233 d51f6ee59e Merge pull request #359 from H-Chris233/fix/issue-226-tauri-csp
security: 为 Tauri WebView 启用最小 CSP
2026-05-08 15:01:06 +08:00
H-Chris233 717e694370 Constrain WebView loading with a minimal CSP
The desktop WebView no longer runs with an unexplained disabled CSP. The policy keeps scripts self-hosted, permits Tauri IPC and local Vite development connections, keeps provider/update/model networking on the Rust side, and documents the only unsafe allowance for React inline styles used by the existing pixel-aligned UI.

Constraint: Issue #226 requires csp:null to be replaced or explicitly bounded

Constraint: Provider validation, updater checks, and model downloads are Rust-side paths, not frontend fetch allowlist entries

Rejected: Keep csp:null with documentation only | weaker defense-in-depth and leaves the WebView effectively unconstrained

Confidence: high

Scope-risk: narrow

Directive: Do not add external provider domains to WebView connect-src unless a frontend fetch path is introduced

Tested: npm run build

Tested: npm run tauri -- info

Tested: cargo check --manifest-path src-tauri/Cargo.toml
2026-05-08 12:00:09 +08:00
TRIPandbaiqing 29d3a79eb6 chore: bump version to 1.2.24-4 (Beta-4) (#358)
Co-authored-by: baiqing <lbx12309@icloud.com>
2026-05-08 11:57:36 +08:00
TRIPandbaiqing aee0d82a59 feat(startup): add "start minimized" toggle (Windows background autostart) (#357)
回应用户「Windows 端开机启动时不弹软件界面、自动后台启动」需求。

新 pref start_minimized: bool,默认 false 不破坏现有行为。打开后所有
启动路径都不弹主窗口(包括开机自启 + 手动启动),App 仅在菜单栏 / 托盘
运行;用户从托盘进入主窗口。Windows 开机自启场景用得多——本来想要
后台 + 托盘,不想被主窗口打扰。

设计选择:不区分"自启动 vs 手动启动"——开关一开则所有启动统一静默。
理由:避免 argv parsing + 重新注册 autostart 带来的复杂性(tauri-plugin-
autostart 的 args 是 init() 时静态设的、动态切需要重注册)。开关明确
打开 = 接受所有启动都从托盘进。

实现:
- types.rs: UserPreferences 加 start_minimized 字段(含 UserPreferencesWire +
  Default + Deserialize 全套兼容)
- lib.rs setup hook: 读 coordinator.prefs().get().start_minimized;为 true
  时跳过 main.show()。OPENLESS_SHOW_MAIN_ON_START=1 仍保留老的强制 show
  路径(dev 用),优先级高于 prefs
- 前端 types.ts / ipc.ts mockSettings / stylePrefs.test.ts: 加默认 false
- Settings.tsx Recording 区在 AutostartRow 下加 Toggle
- i18n 5 locale (zh-CN / zh-TW / en / ja / ko) 加 startMinimizedLabel +
  startMinimizedDesc 两个 key

顺手修预存 bug(跟 PR #355 副 commit 同源——本 branch 没含那个修复):
HotkeyBinding default keys=None,避免旧 prefs 反序列化时 trigger / keys
不一致。types::tests 8/8 pass。

不在本 PR:Windows IME 切换还原偶发不还原 bug——需要 Windows 日志
+ 复现条件才能定位,盲改可能引入新 bug。已请求用户提供
%LOCALAPPDATA%\OpenLess\Logs\openless.log + 系统版本 + IME 信息。

cargo test --lib: 135/135 pass。npm run build: tsc + vite 干净。

Co-authored-by: baiqing <lbx12309@icloud.com>
2026-05-08 11:49:13 +08:00
TRIPandbaiqing 375456f834 fix(icons): pad bundle icon to Apple HIG ~10% margin (#356)
* feat(history+polish): time-based retention + conversation-aware polish

回应用户「历史记录功能 + 对话上下文 + 提示词调优」需求。

新增 2 个用户偏好(types.rs + UserPreferencesWire + Default + Deserialize 全套兼容):
- history_retention_days: u32(默认 7;0=不按时间清理仅受 200 上限)
- polish_context_window_minutes: u32(默认 5;0=关闭走原有单轮 polish)

后端:
- persistence.rs:
  - HistoryStore::append_with_retention(session, retention_days):写入新条目时
    顺手把超过 N 天的会话裁掉,不需要后台轮询
  - HistoryStore::recent_within_minutes(minutes):返回最近 N 分钟内的会话(newest-
    first,take_while 利用 newest-first 顺序提前终止)
  - 老 append() 保留,等价 append_with_retention(s, 0)
- polish.rs:
  - polish() 多一个 prior_turns 参数;不为空时走 chat_completion_with_polish_history
    构造 system + (user/assistant)... + 当前 user 多轮 chat completion
  - 把 chat_completion 的 send/parse 抽成 send_chat_request 复用
  - prompts::polish_context_instruction():明确告诉 LLM 历史 turns 仅作语义上下文,
    不要复读已经被插入到用户文档里的 assistant 输出
- coordinator.rs:
  - 主流水线在 Polishing 阶段拉 recent_within_minutes(prefs.window) 作为 prior_turns,
    过滤掉失败的 session(error_code/empty final_text)避免喂噪声
  - 翻译路径 / Raw 模式 / repolish 主动跳过上下文(语义不合适)
  - 3 个 history.append 改成 append_with_retention(session, prefs.history_retention_days)
- commands.rs / coordinator.rs:repolish / polish.rs::tests / commands.rs::testProvider:
  原 polish() 调用全部加 &[] 空 prior_turns 参数

前端:
- types.ts:UserPreferences 加 historyRetentionDays / polishContextWindowMinutes
- ipc.ts mockSettings + stylePrefs.test.ts mockPrefs:补默认值
- Settings.tsx Recording 区加 2 个 number input(裸 input + clamp,retention 0-365 天,
  context window 0-60 分钟)
- i18n 5 locale(zh-CN / zh-TW / en / ja / ko)补 4 个 key:
  historyRetentionLabel / Desc + polishContextWindowLabel / Desc

性能注意:retention 在 append 时 O(n) 一次扫描裁切;recent_within_minutes 使用
take_while 利用 newest-first 顺序提前终止,平均 O(window 内条目数) 不是 O(总历史)。

cargo check + commands::tests (30/30) + polish::tests (10/10) + npm run build 全 pass。
预先存在的 types::tests::legacy_hotkey_trigger 失败跟本 PR 无关(macOS 平台特有)。

不包含:连接 / 厂商 / 提示词排布优化(用户也提了)——那些需要先 measure 再动,
不能盲改,留给独立 PR。

* fix(types): HotkeyBinding default keys = None (legacy trigger / keys mismatch)

跑测试时发现 types::tests::legacy_hotkey_trigger_still_produces_effective_key_codes
fail,原因跟本 PR 无关,但用户要求中途发现 bug 顺手修。

Bug:HotkeyBinding 用 `#[serde(default)]` 结构级 default。反序列化时整个 struct
先按 Default 填充,JSON 字段覆盖。如果 Default 里 keys 预填具体 codes(macOS
Default ["AltRight"] / Windows ["ControlRight"]),那么旧 prefs 写
`{"trigger":"rightControl","mode":"toggle"}`(无 keys)反序列化后会变成:
  trigger = RightControl  ← JSON 覆盖
  keys = Some([HotkeyKey { code: "AltRight" }])  ← Default 残留,跟 trigger 不一致

effective_codes() 在 keys 是 Some 时直接信任 keys、不再 fallback 到
legacy_trigger_code(trigger),于是用户选的 trigger 跟实际生效的 keys 对不上。

修法:Default::keys = None。这样 keys 缺省时 effective_codes() 走 legacy_trigger_code
路径,跟 trigger 自动同步;JSON 显式写 keys 时仍可以覆盖。

types::tests 8/8 pass,全 lib 测试 135/135 pass。

* test(polish): lock down "no echo prior context" with 4 explicit unit tests

用户的核心顾虑:上下文感知 polish 不能让 LLM 把上文吐出来。PR #355 设计了
两层防御(chat structure + system prompt 显式指令),但**没写过 explicit 测试**
盯死这条不变量——回归就听天由命。这次把它锁住。

把 chat_completion_with_polish_history 里 inline 的 messages 构造抽成纯函数
build_polish_history_messages(system_prompt, prior_turns, user_prompt) → Vec<Value>,
方便可测,行为零变化。

新增 4 个 polish::tests:

1. build_polish_history_messages_empty_prior_falls_back_to_two_messages
   prior 空时只剩 [system, user],跟单轮 chat_completion 等价

2. build_polish_history_messages_orders_prior_oldest_to_newest_then_current
   入参 prior_turns 是 newest-first(match HistoryStore::recent_within_minutes
   返回顺序),chat 需要 oldest-first 时间序——验证 reverse 没漏。
   顺序错了 LLM 会看到「未来→过去→当前」错乱时间轴,对话就废了。

3. build_polish_history_messages_keeps_polished_text_at_assistant_role
   关键不变量:历史 polish 必须挂 role=assistant,**不能**误挂 user。
   typo 把它放进 user role 会让 LLM 当成新输入再润色 → 输出复读上文,
   直接违反"不复读"目标。这个 test 把这条锁死,重构时一改就 fail。

4. polish_context_instruction_explicitly_forbids_repeating_prior_assistant_output
   第二层防御:仅靠 chat structure 不够(一些模型在长上下文里仍可能 echo prior turns)。
   验证 system prompt 末尾追加的指令含「不要 / 复读 / assistant-or-已经整理 /
   当前最新」四组关键词,文案可以改但语义不能丢。

cargo test --lib:139/139 pass(135 之前 + 4 新增),完全无回归。

* fix(icons): pad bundle icon to Apple HIG ~10% transparent margin (#354)

issue #354:在 macOS 26 以下系统,Dock 里的 OpenLess 图标比其他 app 大约 25%。

根因:原 icon.png 内容 100% 填满 1024×1024 画布,没留 Apple HIG 推荐的
~10% 透明 padding。macOS 26 自带 Liquid Glass squircle 容器会自动套圆角 +
裁切,掩盖了这个问题;但 macOS 11-15 把 icon PNG 原样渲染到 Dock,
所以视觉尺寸明显比标准 app 大一圈。

修法:
- 用 PIL 把 1024×1024 master 缩到 820×820 居中、四周各 102px 透明 padding
  (内容占比 ≈ 80%,符合 Apple HIG 约定)
- 重新生成 Tauri bundle 的 5 个 PNG:icon.png / 32×32 / 64×64 / 128×128 / 128×128@2x
- 用 iconutil 重新打包 icon.icns(含 16/32/128/256/512 + @2x 共 10 张)
- 不动 frontend public/AppIcon.png——那个是 React UI 内部用的小图(14-56px +
  CSS borderRadius),padding 反而会让它在固定容器里显得偏小

校验:python PIL 读新 icon.png,bbox 显示画布 512×512、内容 416×416 居中、
四周 padding 各 48px,内容占比 81.2%——接近 80% 推荐值。

不影响 macOS 26 渲染:Liquid Glass 看到的视觉内容仍是同一张图,
只是周围多了透明像素,Apple 自动 squircle 会无视它。

---------

Co-authored-by: baiqing <lbx12309@icloud.com>
2026-05-08 11:43:33 +08:00
TRIPandbaiqing b86c8d7966 feat(history+polish): time-based retention + conversation-aware polish (#355)
* feat(history+polish): time-based retention + conversation-aware polish

回应用户「历史记录功能 + 对话上下文 + 提示词调优」需求。

新增 2 个用户偏好(types.rs + UserPreferencesWire + Default + Deserialize 全套兼容):
- history_retention_days: u32(默认 7;0=不按时间清理仅受 200 上限)
- polish_context_window_minutes: u32(默认 5;0=关闭走原有单轮 polish)

后端:
- persistence.rs:
  - HistoryStore::append_with_retention(session, retention_days):写入新条目时
    顺手把超过 N 天的会话裁掉,不需要后台轮询
  - HistoryStore::recent_within_minutes(minutes):返回最近 N 分钟内的会话(newest-
    first,take_while 利用 newest-first 顺序提前终止)
  - 老 append() 保留,等价 append_with_retention(s, 0)
- polish.rs:
  - polish() 多一个 prior_turns 参数;不为空时走 chat_completion_with_polish_history
    构造 system + (user/assistant)... + 当前 user 多轮 chat completion
  - 把 chat_completion 的 send/parse 抽成 send_chat_request 复用
  - prompts::polish_context_instruction():明确告诉 LLM 历史 turns 仅作语义上下文,
    不要复读已经被插入到用户文档里的 assistant 输出
- coordinator.rs:
  - 主流水线在 Polishing 阶段拉 recent_within_minutes(prefs.window) 作为 prior_turns,
    过滤掉失败的 session(error_code/empty final_text)避免喂噪声
  - 翻译路径 / Raw 模式 / repolish 主动跳过上下文(语义不合适)
  - 3 个 history.append 改成 append_with_retention(session, prefs.history_retention_days)
- commands.rs / coordinator.rs:repolish / polish.rs::tests / commands.rs::testProvider:
  原 polish() 调用全部加 &[] 空 prior_turns 参数

前端:
- types.ts:UserPreferences 加 historyRetentionDays / polishContextWindowMinutes
- ipc.ts mockSettings + stylePrefs.test.ts mockPrefs:补默认值
- Settings.tsx Recording 区加 2 个 number input(裸 input + clamp,retention 0-365 天,
  context window 0-60 分钟)
- i18n 5 locale(zh-CN / zh-TW / en / ja / ko)补 4 个 key:
  historyRetentionLabel / Desc + polishContextWindowLabel / Desc

性能注意:retention 在 append 时 O(n) 一次扫描裁切;recent_within_minutes 使用
take_while 利用 newest-first 顺序提前终止,平均 O(window 内条目数) 不是 O(总历史)。

cargo check + commands::tests (30/30) + polish::tests (10/10) + npm run build 全 pass。
预先存在的 types::tests::legacy_hotkey_trigger 失败跟本 PR 无关(macOS 平台特有)。

不包含:连接 / 厂商 / 提示词排布优化(用户也提了)——那些需要先 measure 再动,
不能盲改,留给独立 PR。

* fix(types): HotkeyBinding default keys = None (legacy trigger / keys mismatch)

跑测试时发现 types::tests::legacy_hotkey_trigger_still_produces_effective_key_codes
fail,原因跟本 PR 无关,但用户要求中途发现 bug 顺手修。

Bug:HotkeyBinding 用 `#[serde(default)]` 结构级 default。反序列化时整个 struct
先按 Default 填充,JSON 字段覆盖。如果 Default 里 keys 预填具体 codes(macOS
Default ["AltRight"] / Windows ["ControlRight"]),那么旧 prefs 写
`{"trigger":"rightControl","mode":"toggle"}`(无 keys)反序列化后会变成:
  trigger = RightControl  ← JSON 覆盖
  keys = Some([HotkeyKey { code: "AltRight" }])  ← Default 残留,跟 trigger 不一致

effective_codes() 在 keys 是 Some 时直接信任 keys、不再 fallback 到
legacy_trigger_code(trigger),于是用户选的 trigger 跟实际生效的 keys 对不上。

修法:Default::keys = None。这样 keys 缺省时 effective_codes() 走 legacy_trigger_code
路径,跟 trigger 自动同步;JSON 显式写 keys 时仍可以覆盖。

types::tests 8/8 pass,全 lib 测试 135/135 pass。

* test(polish): lock down "no echo prior context" with 4 explicit unit tests

用户的核心顾虑:上下文感知 polish 不能让 LLM 把上文吐出来。PR #355 设计了
两层防御(chat structure + system prompt 显式指令),但**没写过 explicit 测试**
盯死这条不变量——回归就听天由命。这次把它锁住。

把 chat_completion_with_polish_history 里 inline 的 messages 构造抽成纯函数
build_polish_history_messages(system_prompt, prior_turns, user_prompt) → Vec<Value>,
方便可测,行为零变化。

新增 4 个 polish::tests:

1. build_polish_history_messages_empty_prior_falls_back_to_two_messages
   prior 空时只剩 [system, user],跟单轮 chat_completion 等价

2. build_polish_history_messages_orders_prior_oldest_to_newest_then_current
   入参 prior_turns 是 newest-first(match HistoryStore::recent_within_minutes
   返回顺序),chat 需要 oldest-first 时间序——验证 reverse 没漏。
   顺序错了 LLM 会看到「未来→过去→当前」错乱时间轴,对话就废了。

3. build_polish_history_messages_keeps_polished_text_at_assistant_role
   关键不变量:历史 polish 必须挂 role=assistant,**不能**误挂 user。
   typo 把它放进 user role 会让 LLM 当成新输入再润色 → 输出复读上文,
   直接违反"不复读"目标。这个 test 把这条锁死,重构时一改就 fail。

4. polish_context_instruction_explicitly_forbids_repeating_prior_assistant_output
   第二层防御:仅靠 chat structure 不够(一些模型在长上下文里仍可能 echo prior turns)。
   验证 system prompt 末尾追加的指令含「不要 / 复读 / assistant-or-已经整理 /
   当前最新」四组关键词,文案可以改但语义不能丢。

cargo test --lib:139/139 pass(135 之前 + 4 新增),完全无回归。

---------

Co-authored-by: baiqing <lbx12309@icloud.com>
2026-05-08 11:42:47 +08:00
millionart aa5add1188 [codex] fix Foundry Windows App Runtime prompt (#340) 2026-05-08 11:42:07 +08:00
Chris233 4aa3f6c58d Merge pull request #352 from H-Chris233/fix/issue-297-session-uuid
修复 session_id 回绕导致的迟到事件误匹配
2026-05-08 11:08:56 +08:00
H-Chris233 e95519a6b4 Prevent stale recorder events from matching wrapped session ids
The dictation coordinator used a wrapping u64 counter to guard delayed recorder and startup events. Replacing that guard with UUID-backed session ids removes the theoretical wraparound collision while keeping the existing stale-event comparison model.

Constraint: Issue #297 requests the UUID-based fix rather than checked arithmetic.\nRejected: checked_add with warning | still preserves a numeric boundary condition instead of eliminating the collision class.\nConfidence: high\nScope-risk: narrow\nTested: cargo check --manifest-path openless-all/app/src-tauri/Cargo.toml\nTested: cargo test --manifest-path openless-all/app/src-tauri/Cargo.toml coordinator::tests:: --lib\nNot-tested: Full cargo test --lib still has unrelated existing failure in types::tests::legacy_hotkey_trigger_still_produces_effective_key_codes.\nRelated: #297
2026-05-08 10:39:33 +08:00
Chris233 6c5eee6b78 Merge pull request #351 from H-Chris233/fix/issue-316-hotkey-settings-error
修复热键设置加载失败后永久 loading
2026-05-08 10:31:37 +08:00
H-Chris233 98f25bd9bb Keep hotkey settings load failures recoverable
Hotkey settings are a shared context for multiple pages, so one rejected IPC call must not leave the provider in a permanent loading state. The refresh path now records a localized recoverable error, keeps any successfully loaded half of the state, and always releases loading so consumers can render retry UI instead of staying blank.

Constraint: Issue #316 requires local load failures to stay visible and non-fatal.
Rejected: Keep Promise.all with one catch | it would still discard partial success from the other IPC call.
Confidence: high
Scope-risk: narrow
Tested: git diff --check; npm run build; cargo check --manifest-path src-tauri/Cargo.toml
Not-tested: Manual Tauri UI session with a deliberately corrupted preferences.json.
Related: https://github.com/appergb/openless/issues/316
2026-05-08 10:05:17 +08:00
Chris233 29c9160b9f Merge pull request #346 from H-Chris233/fix/issue-300-linux-ci-gate
为 PR CI 增加 Linux cargo check 门禁
2026-05-08 09:48:47 +08:00
TRIPandbaiqing 10440c7f72 chore: bump version to 1.2.24-3 (Beta-3) (#349)
Co-authored-by: baiqing <lbx12309@icloud.com>
2026-05-08 09:47:30 +08:00
TRIPandbaiqing e358c20b20 fix(updater): use GitHub atom feed for Beta lookup, dodge API rate limit (#350)
用户报「获取 Beta 版本信息失败」根因:fetch_latest_beta_release 调
api.github.com/repos/.../releases,未认证 60 req/h/IP。多次切 Beta toggle
+ 同 IP 共享配额(公司/家庭网络后),轻易撞 403 rate limit exceeded。
现场实测 IP 已耗尽:x-ratelimit-remaining: 0。

修法:换成 GitHub releases.atom(公开 RSS feed,CDN cache,无 API rate limit)。

具体改动:
- fetch_latest_beta_release 端点从 api.github.com 改成 github.com/.../releases.atom
- 提取一个纯函数 parse_latest_beta_from_atom 做字符串解析(不引 XML 库——
  feed 格式稳定,找 /releases/tag/ 锚点抓 tag 即可),方便单测
- atom feed 不显式标 prerelease,但项目约定 tag 后缀 `-beta-tauri` 就是 Beta,
  过滤后缀已足够
- timeout 从 10s 提到 15s,给跨境网络更多余地
- 加 2 个 unit test:(1) 混合 stable+beta 时返回 Beta entry;(2) 全 stable
  时返回 None。两个 test 都已 pass

不改前端:JS 端调用签名 / 错误展示路径不变,下次 Beta-3 release 出来后用户
装上即生效。

Co-authored-by: baiqing <lbx12309@icloud.com>
2026-05-08 09:47:18 +08:00
TRIPandbaiqing 8b55469d8e feat(ui): drop sidebar version chip; show BETA tag only on beta builds (#348)
用户反馈:左上角 OpenLess logo 旁边的 v1.2.24-2 chip 在窄侧栏下被截断换行
(视觉上读作 "v1.2.24-/2"),太丑且没有信息价值——版本号在底部 footer
和「设置 → 关于」里都已经展示,不需要这第三处。

同时 BETA 标签当前是无条件渲染的,正式版用户也会看到,违反"Beta 不
泄漏到正式版"原则。

改动:
- appVersion.ts:新增 IS_BETA_BUILD 常量。约定:semver prerelease 段
  (含 `-`)= Beta(如 1.2.24-1 / 1.2.24-2);不含 `-` = 正式版
  (如 1.2.23 / 1.2.24)。
- FloatingShell.tsx:
  - 删左上 brand 行里的 version chip <span>(保留 logo + "OpenLess" 文字)
  - BETA 区域用 {IS_BETA_BUILD && (...)} 包条件渲染。正式版 build 直接
    不渲染整块,避免普通用户误以为正式版是 Beta

footer 里的 "版本 v1.2.24-2 检查" 和 设置 → 关于的 AboutUpdateControl
保留不动——那两处是用户主动想看的版本信息,本次只删左上的冗余 chip。

npm run build 干净通过。

Co-authored-by: baiqing <lbx12309@icloud.com>
2026-05-08 09:39:14 +08:00