Commit Graph
734 Commits
Author SHA1 Message Date
weikeyi 58c229dace fix(windows-ci): WiX 工具按 WixTools* 通配查找,避免 Tauri 升级后路径失效
Windows MSI Repair 之前硬编码 `C:\Users\22933\AppData\Local\tauri\WixTools314\light.exe`,Tauri 升级到新 WiX 版本(如 WixTools315)后路径不存在,Repair 步骤直接挂。

改为 WixTools* glob,按数字后缀降序回退到名称降序,取最新可用版本。同步更新 windows-package-msvc.ps1::Find-WixTool 和 release-tauri.yml 的 Repair 步骤;保留 -sice:ICE80、NSIS/MSI 两轮 build、bash shell 等 Windows CI 红线不变。

补 4 条 windows-package-msvc.test.mjs 防回归断言:禁止 WixTools314 出现在脚本/workflow,要求两边都按 WixTools* glob 查找。

Refs: #298
2026-05-11 16:50:20 +08:00
baiqing cc215fdce3 chore(release): bump version to 1.2.24-9 (Beta-9) 2026-05-11 13:19:17 +08:00
Chris233 69978a3c47 Merge pull request #404 from H-Chris233/fix/issue-402-llm-thinking-toggle
feat: 增加渠道级 LLM 思考模式开关
2026-05-11 11:48:13 +08:00
H-Chris233 8fbb965635 Let users control LLM thinking at the provider channel
Issue #402 needs a Settings toggle that applies to every LLM runtime path without changing prompt behavior. The implementation keeps the preference in user settings, threads it through polish, translate, QA, and validation, and sends only provider-channel request fields documented by each backend instead of prompt instructions or model-name heuristics.

Constraint: Thinking controls are provider-specific request fields, not a portable prompt convention

Rejected: Prompt-level thinking instructions | changes model-facing prompts and cannot guarantee provider thinking mode

Rejected: Per-model allow/deny lists | user requested channel-level behavior and provider-side handling for unsupported models

Confidence: medium

Scope-risk: moderate

Directive: Do not add prompt injection or model-name thinking adapters for #402 without a new explicit product decision

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

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

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

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

Tested: cd openless-all/app && npm run build

Tested: git diff --check

Related: #402
2026-05-11 11:20:35 +08:00
baiqing 47bda0d0ac chore(release): bump version to 1.2.24-8 (Beta-8) 2026-05-11 07:10:53 +08:00
TRIP e7a0ad5971 Merge pull request #401 from BBossss/beta
Merging Codex OAuth LLM provider integration from external contributor.
2026-05-11 07:10:15 +08:00
TRIPandbaiqing 3125ff4d20 feat(settings): cross-platform parity for local ASR (#403)
* feat(settings): cross-platform parity for local ASR

- AdvancedSection: warning collapses to inline 11px amber badge in
  the title row's top-right; the enable-confirm popup is now a
  centered fixed modal with backdrop blur(8px) and click-outside-
  to-cancel (busy-guarded).
- Symmetric rows by platform — fixes the #400 bug where the Qwen3
  row was rendered on Windows even though all qwen_engine /
  local_provider / cache modules are #[cfg(target_os = "macos")],
  so Enable on Windows would always error:
    macOS:   Qwen3 row enabled / Foundry row hidden
    Windows: Foundry row enabled / Qwen3 row disabled with
             notSupportedHere desc
- Providers ASR Card: replaces the plain-text localAsrActiveNotice
  branch with an always-rendered dropdown that includes the
  platform-native local engine (macOS=Qwen3, Windows=Foundry) as a
  disabled option, plus a takeover hint underneath. Entire
  dropdown locks when a local engine is active, so it can only be
  released via Advanced (matches "启动之后供应商将被接管" UX).
- onLlmProviderChange / onAsrProviderChange now emit saving →
  saved through emitSaved. Data was already being persisted; the
  saved-toast was missing.
- LocalAsr embedded: hide Qwen3 model management (mirror /
  engine-status / model list) on Windows — dead UI without the
  Qwen3 backend. The shared error Card is kept unconditional
  since Foundry handlers also call setError.
- i18n × 5 (zh-CN/zh-TW/en/ja/ko): shorten localAsrWarningShort;
  rewrite qwen3Desc and foundryDesc to '启动之后,ASR 提供商将被接管';
  add notSupportedHere and localAsrTakeoverHint.

* fix(settings): clear saving toast on provider-switch failure

pr_agent #403 caught: emitSaved('saving') with no try/catch — if
setActiveLlmProvider / setActiveAsrProvider / any later credential
write throws, the right-corner toast stays stuck on saving forever.

Wrap both onLlmProviderChange and onAsrProviderChange bodies in
try/catch. On error, emit 'failed' (and re-throw) only if seq still
matches — stale calls superseded by a newer switch shouldn't
overwrite the newer call's own saving state.

* fix(settings): provider-card visual cleanup + log-export permission + pr_agent #403

Fixes piled up in user feedback during local verify of the previous
commit; bundled into one commit to keep #403 small.

- capabilities/default.json: add `dialog:default` permission. The
  About → Export error log button always failed because the Tauri 2
  capabilities did not allow `tauri-plugin-dialog`'s `save()` command;
  the JS rejected before export_error_log was ever invoked.

- ProvidersSection ASR Card (pr_agent #403 'Wrong active label'):
  when committedAsrProvider is a local engine, selectedValue now
  reflects the actually-active engine instead of being forced to
  platformLocalAsr. Cross-machine config sync (macOS profile with
  foundry-local-whisper / Windows profile with local-qwen3) used to
  make the dropdown lie about which backend is running. Anomalous
  local engines are added as disabled <option>s so the controlled
  <select> still finds its value and doesn't fall back to the first
  option.

- ProvidersSection: drop verbose desc on LLM and ASR provider rows.
  '选择后将自动填入 Base URL 默认值。' is ~16 Chinese chars and
  always wraps inside the 180px label column, leaving a stranded
  second line on the left — the dropdown itself already conveys the
  semantic, the desc was redundant.

- AdvancedSection: replace the [启用] / [已启用] Btn with the
  shared Toggle component for Qwen3 and Foundry rows. Toggle off
  switches back to volcengine. The Qwen3 row's Toggle truthfully
  reflects committed state on Windows (so a cross-platform anomaly
  shows ON even though it is non-interactive there). The standalone
  '禁用本地 ASR' escape row is now only rendered when no inline
  Toggle can disable the active engine — i.e., Linux / unsupported
  platform, or anomaly where the row for the foreign engine is
  hidden / non-interactive.

- PermissionsSection: every status pill (microphone, accessibility,
  hotkey, Windows IME, network) now right-aligns inside its row via
  `justifyContent: flex-end` + `width: 100%`. Pill text moved to the
  right of inline messages so the green '已授权' / 'Authorized'
  reads as a right-edge badge — closer to macOS System Settings
  convention.

* fix(settings): keep Windows Qwen3 toggle visually OFF (pr_agent #403)

pr_agent 'Stale Windows state' caught: previous commit had `on=
{isOnLocalQwen3}` which truthfully reflected committed state, so a
profile synced from macOS with activeAsrProvider=local-qwen3 would
render Toggle ON next to a desc that says '本平台暂不支持'. The
visual contradiction was the concern.

Switch to `on={isMac && isOnLocalQwen3}` — Windows always renders
the row OFF + non-interactive, matching the unsupported desc. The
cross-platform anomaly state surfaces via the standalone '禁用本地
ASR' escape row, which is rendered exactly when no inline Toggle
can disable the active engine.

Comment updated to match the new behavior and document the
tradeoff.

* fix(localasr): scope Qwen3 mgmt UI to macOS only (pr_agent #403)

pr_agent 'Linux regression' caught: previous wrap was `!IS_WINDOWS`,
so a hypothetical Linux render path would still expose the Qwen3
mirror / model-list / engine-status UI even though Qwen3's backend
is `#[cfg(target_os = "macos")]`.

Swap the two wrappers to `IS_MAC` so the management cards only
materialize on macOS. Today's AdvancedSection guards LocalAsr
embedded with `platformSupported = isMac || isWin`, so Linux already
short-circuits before LocalAsr renders — but defense in depth here
costs nothing and keeps the comment accurate.

---------

Co-authored-by: baiqing <lbx12309@icloud.com>
2026-05-11 07:01:00 +08:00
bbossssandOmX 8391e566c6 Let OpenLess reuse Codex auth for low-latency polishing
Codex OAuth should let users spend their existing Codex subscription on short dictation polish, translation, and QA without storing another API key in OpenLess. The provider reads the local Codex auth file, keeps OpenAI-compatible providers on their existing path, uses a lighter Spark default for latency, and broadens Codex auth discovery for Windows GUI environments.

The same change keeps Volcengine on the duration-priced BigASR resource validated locally and preserves proxy behavior so Codex network paths can use system proxy settings while loopback/local provider checks avoid the proxy.

Constraint: Codex OAuth credentials must remain sourced from the Codex auth file and must not be copied into OpenLess credential storage.

Constraint: PR #401 is a fork PR from BBossss/openless:beta to appergb/openless:beta.

Rejected: Require a separate OpenAI API key | the requested path was to reuse the existing Codex subscription.

Rejected: Keep gpt-5.5 as the default | it is unnecessarily heavy for short dictation polish and was observed timing out.

Rejected: Use only HOME for Codex auth lookup | Windows GUI sessions commonly expose USERPROFILE instead.

Confidence: high

Scope-risk: moderate

Reversibility: clean

Directive: Do not persist Codex OAuth tokens into OpenLess credentials; keep using the Codex auth file as the source of truth.

Directive: Keep Codex OAuth defaults optimized for short low-latency dictation polishing unless product requirements change.

Tested: git diff --cached --check

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

Tested: npm run build

Tested: PR #401 CI passed on Windows, macOS, and Linux before this history-only squash.

Co-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-10 22:04:25 +08:00
Chris233 69081e0d42 Merge pull request #397 from appergb/docs/research-first-subagent-mechanism
docs: codify subagent-mediated API / library / platform research workflow
2026-05-10 19:08:26 +08:00
TRIPandbaiqing f3894c1f7b feat(llm): Google Gemini provider + fix LLM provider switch (#398)
* chore: ignore video-materials/ promo recordings

video-materials/ holds ~2.2 GB of promo recordings (promo /
promo-materials / promo-openless / promo-openless-v2 / SC). These
are local working assets that don't belong in the repo. Add to
.gitignore so future stash / commit operations can't accidentally
pull the binary blob in.

* feat(llm): Google Gemini provider with forced thinking-off + fix provider-switch credentials

Add Google Gemini as a native LLM provider hitting v1beta
:generateContent (single-shot) and :streamGenerateContent?alt=sse
(QA chat) with x-goog-api-key auth. Per-model thinkingConfig is
injected to keep thinking off for every supported ID — exact
matrix verified against ai.google.dev/gemini-api/docs/thinking:

  gemini-2.5-flash, gemini-2.5-flash-lite     thinkingBudget: 0
  gemini-2.5-pro                              field omitted (Google: cannot disable)
  gemini-3*-pro*                              thinkingLevel: "low"
  gemini-3*-flash*, unknown future IDs        thinkingLevel: "minimal"

Why native, not the OpenAI-compat shim: shim is beta and for 3.x
needs an extra_body workaround for thinking control; auth header
and contents shape differ enough that reusing
OpenAICompatibleLLMProvider would have been the worse compromise.

fix(settings): switching LLM preset previously only auto-filled
endpoint/model when those slots were empty. All providers share
ark.endpoint / ark.model_id (per-provider entries were never
introduced — see persistence.rs lookup_account), so for returning
users with non-empty slots, switching the dropdown updated active
provider but kept the OLD endpoint — polish silently kept calling
the previous provider. Now switching to any non-custom preset
overwrites endpoint/model to that preset's defaults. Trade-off:
user-customized endpoints within a preset are dropped on switch;
silent wrong-routing was a worse failure mode.

Backend:
- New module llm_gemini.rs (15 unit tests for thinking-config table,
  URL builder, response parser, SSE shape, history role mapping)
- polish.rs extracts compose_polish_prompts / compose_translate_prompts
  / compose_qa_system_prompt as pub(crate) helpers shared by both
  LLM paths so prompt assembly can't drift
- coordinator.rs's polish_text / translate_text / answer_chat_dispatch
  open with `if get_active_llm() == "gemini"` early-return; existing
  OpenAI-compat callsite stays untouched

Frontend:
- LLM_PRESETS gains gemini entry (baseUrl v1beta, model placeholder
  gemini-2.5-flash)
- ProviderTools hidden when committedLlmProvider === 'gemini'
  (its "Fetch models" button assumes OpenAI /models shape; native
  /v1beta/models response shape doesn't match)
- Static geminiModelHint lists the 7 available IDs so users pick
  correctly without auto-fetch
- 5 locales (zh-CN, zh-TW, en, ja, ko) gain presets.gemini and
  geminiModelHint

Test plan:
- cargo test --lib                          200/200 pass
- npm run build (tsc + vite)                green
- Manual: switch to Gemini in Settings,
  paste Google AI Studio key, dictate;
  verify polish reaches v1beta and respects
  the per-model thinking-off contract.

* docs: 2026-05-10 audit + beta logic review snapshots

Snapshot two audit artifacts produced 2026-05-10:

- docs/audit-2026-05-10-validated.md — 21 audit items run against
  the live source tree at openless-all/app/src-tauri/src/ with a
  CONFIRMED / FALSE_POSITIVE / PARTIAL verdict per item plus the
  exact file:line range that supports each verdict.

- docs/logic-review-2026-05-10.md — 5-path logic review of
  origin/beta @ 400097ad covering startup, press/release, end-of-
  session, capsule emit, and shutdown. Notable: at 400097ad the
  emit_capsule fix from PR #389 was NOT yet an ancestor, so the
  cpal audio-callback → AppKit/Win32 SIGTRAP risk was still live
  on that snapshot (subsequent merges have addressed it; this doc
  remains the dated record).

Both files are pure snapshots — no code changes here.

* fix(llm-gemini): buffer SSE bytes across chunks to handle multibyte UTF-8 split

PR #398 pr_agent flagged: reqwest::chunk() can split a multi-byte
UTF-8 codepoint (CJK / emoji) across two network chunks, but the
streaming code decoded each chunk independently with from_utf8 →
half a 你 in the tail of one chunk would surface as
"non-utf8 SSE chunk" and abort the whole stream. This breaks
Gemini QA streaming for any Chinese-language answer that doesn't
happen to align with packet boundaries.

Fix: buffer raw bytes (Vec<u8>) until we see the SSE delimiter
b"\n\n", and only then decode the complete event content as UTF-8.
The delimiter \n\n is two ASCII bytes (0x0A 0x0A) which can never
appear inside a multi-byte UTF-8 sequence, so byte-level splitting
at that boundary is always safe.

Extracted the parsing as drain_complete_sse_events(&mut Vec<u8>) so
the invariant is testable without a TcpListener. Three new unit
tests cover:
- normal split-on-delimiter + remainder buffering
- multibyte UTF-8 split across chunks (the actual regression)
- invalid-UTF-8 event content gracefully skipped (warn, don't crash)

The same byte-level pattern fix applies to polish.rs's
chat_completion_history_streaming — left untouched here to keep
this PR scoped to the Gemini diff pr_agent reviewed. polish.rs
should get the same fix in a follow-up.

* feat(llm-gemini): teach ProviderTools to fetch native Gemini /v1beta/models

Drop the verbose static geminiModelHint paragraph and restore the
normal ProviderTools "Fetch models" button when the active provider
is Gemini. Users now read live model IDs from Google instead of
staring at a wall of i18n text below their model field.

Backend fetch_provider_models now branches on the base URL:
- Google's generativelanguage.googleapis.com → x-goog-api-key auth +
  parse_gemini_model_ids() that walks `models[].name` and strips the
  `models/` prefix so the result is a plain `gemini-2.5-flash` style
  ID directly writable to ark.model_id.
- Anything else → unchanged Bearer auth + OpenAI `data[].id` shape.

Tests: parse_gemini_model_ids_strips_models_prefix_and_dedups +
is_gemini_base_url_matches_official_domain. cargo test --lib green
(32/32 in commands::tests subset).

i18n: settings.providers.geminiModelHint removed from all 5 locales.
The TS `typeof zhCN` constraint enforces the consistent removal.

* fix(llm-gemini): pr_agent #398 follow-ups — filter non-chat models + CRLF SSE delim

Two follow-up fixes for pr_agent advisories on PR #398:

1. parse_gemini_model_ids now filters by `supportedGenerationMethods`.
   Google's /v1beta/models lists every family — embeddings,
   TTS-image, predict-only — alongside generateContent-capable
   chat models. Without filtering, a "Fetch models" click would
   surface ids like `gemini-embedding-2`, and a user picking one
   would write it into ark.model_id and have polish silently fail
   on every dictation. Field is conservatively included when
   missing (some preview models don't expose it yet).

2. drain_complete_sse_events now accepts both `\n\n` and `\r\n\r\n`
   as event delimiters. Some HTTP/2 middleware and CDNs normalize
   line endings to CRLF; the previous LF-only implementation would
   never drain a complete event from those servers and report the
   stream as empty even when data was received. Tied positions
   can't happen by construction (\r\n\r\n contains no \n\n
   substring), so "earliest match wins" is unambiguous.

Tests:
- parse_gemini_model_ids_filters_out_non_generate_content_families
- drain_complete_sse_events_handles_crlf_delimiter
- drain_complete_sse_events_picks_earliest_delimiter_when_mixed

---------

Co-authored-by: baiqing <lbx12309@icloud.com>
2026-05-10 18:18:24 +08:00
baiqing a74a5472b0 docs: 2026-05-10 audit + beta logic review snapshots
Snapshot two audit artifacts produced 2026-05-10:

- docs/audit-2026-05-10-validated.md — 21 audit items run against
  the live source tree at openless-all/app/src-tauri/src/ with a
  CONFIRMED / FALSE_POSITIVE / PARTIAL verdict per item plus the
  exact file:line range that supports each verdict.

- docs/logic-review-2026-05-10.md — 5-path logic review of
  origin/beta @ 400097ad covering startup, press/release, end-of-
  session, capsule emit, and shutdown. Notable: at 400097ad the
  emit_capsule fix from PR #389 was NOT yet an ancestor, so the
  cpal audio-callback → AppKit/Win32 SIGTRAP risk was still live
  on that snapshot (subsequent merges have addressed it; this doc
  remains the dated record).

Both files are pure snapshots — no code changes here.
2026-05-10 18:13:46 +08:00
baiqing fcb13f5c23 chore: ignore video-materials/ promo recordings
video-materials/ holds ~2.2 GB of promo recordings (promo /
promo-materials / promo-openless / promo-openless-v2 / SC). These
are local working assets that don't belong in the repo. Add to
.gitignore so future stash / commit operations can't accidentally
pull the binary blob in.
2026-05-10 18:13:46 +08:00
TRIPandbaiqing ce21d99f05 feat(settings): hide local ASR behind new Advanced tab + confirm dialog (#400)
* feat(settings): hide local ASR behind new Advanced tab + system confirm dialog

Move local ASR options (local-qwen3 / foundry-local-whisper) out of
the main Providers → ASR dropdown into a new "Advanced" Settings tab,
so first-time users can't accidentally enable CPU inference and get
slower / less-accurate transcription than they expected. Enabling
local ASR now requires reading a strong yellow/amber warning and
confirming a system-level dialog (@tauri-apps/plugin-dialog ask()
with kind:'warning') — the in-page modal pattern wasn't loud enough.

Behavior changes:
- visibleAsrPresets now excludes local-qwen3 and foundry-local-whisper
  (the dropdown is cloud-only)
- useEffect lookup uses ASR_PRESETS (all) so committedAsrProvider can
  still resolve to a local provider when one is active
- When committedAsrProvider is local, the ASR Card replaces the
  dropdown with a plain notice ("Local ASR (X) is currently active —
  switch / disable in the Advanced tab"); existing
  LocalAsrProviderHint rendering for the Card body is preserved
- New AdvancedSection component:
  - Strong warning block (amber background + amber border + amber text,
    fontWeight 500, ⚠️ icon) — meets user's "yellow/red obvious" ask
  - Per-platform engine selector:
      macOS  : Qwen3-ASR only
      Windows: Qwen3-ASR + Foundry Local Whisper
      Linux  : "Local ASR model integration is not supported on this platform"
  - Enable buttons trigger Tauri ask() confirm dialog before switching
  - When already on local: shows a "Disable local ASR" button
    (switches to volcengine, no confirm needed) plus the existing
    LocalAsrProviderHint for model status / download navigation
- SettingsSectionId gains 'advanced'; SECTION_ORDER appends it after
  language so it sits last in the sidebar (matches macOS conventions
  for Advanced/Experimental panes)

i18n: 5 locales (zh-CN, zh-TW, en, ja, ko) gain
- settings.sections.advanced
- settings.providers.localAsrActiveNotice
- settings.advanced.{localAsrTitle, localAsrDesc, localAsrWarningStrong,
  qwen3Desc, foundryDesc, enable, alreadyActive, disableLocalLabel,
  disableLocalDesc, disable, platformNotSupported,
  confirmEnableLocalTitle, confirmEnableLocalBody}

Test plan:
- npm run build (tsc + vite) — green; the typeof zhCN constraint
  catches any locale that drifted out of shape
- Manual: switch Settings tab to "Advanced", verify warning is
  amber-on-amber and clearly visible, click "Enable" on Qwen3-ASR,
  verify a system dialog (not a React modal) appears with kind:warning
  iconography, cancel — verify nothing changed; confirm — verify the
  Providers tab now shows "Local ASR is currently active" instead of
  the dropdown
- Manual on macOS: confirm Foundry option does NOT appear
- Manual on Linux (if available): confirm "platform not supported"
  message renders

* feat(advanced): inline LocalAsr model UI + page-top confirm popup + concise warning

User feedback: the standalone "Models" page (LocalAsr.tsx as a main
nav tab) was redundant once Advanced settings exists, the inline
amber warning was a wall of text, and clicking Enable triggered an
OS-native dialog that didn't feel like part of the app. Refactor
the whole local-ASR surface around what users actually need:

- "Models" main-nav tab removed (FloatingShell + NAVIGATE_LOCAL_ASR_EVENT
  listener gone). Model management is now exclusively reachable via
  Settings → Advanced.
- LocalAsr.tsx gains an `embedded` prop. When embedded, the outer
  page padding/height, PageHeader, and standalone warning Card are
  skipped — the host (AdvancedSection) supplies the chrome.
- AdvancedSection no longer calls @tauri-apps/plugin-dialog ask().
  Enabling local ASR sets pendingTarget which renders a single
  amber-bordered Card at the very top of the section with
  Confirm / Cancel buttons. One box, in-app, dismissable. Disable
  is direct (no confirm needed — going back to cloud is safe).
- Persistent inline warning shrunk to one line. The longer wording
  (including the new "may drop words on under-spec hardware" risk
  the user asked for) lives in the popup body so it's only seen
  when actually consequential.
- ProvidersSection ASR Card body returns null when active is
  local — the dropdown row's localAsrActiveNotice already points
  to Advanced, no need to duplicate manage UI here.

i18n (5 locales): localAsrWarningStrong replaced by
localAsrWarningShort; confirmEnableLocalBody / Title shortened;
new `confirm` key for the popup CTA.

LocalAsrProviderHint and NAVIGATE_LOCAL_ASR_EVENT in Settings.tsx
are now unused but left in place to keep the diff focused; will
clean up in a follow-up commit if pr_agent flags them.

Test plan:
- npm run build (tsc + vite) — green
- Manual on macOS: Settings → Advanced shows one short amber line +
  Enable Qwen3-ASR button + LocalAsr model UI inline. Click Enable
  → page-top popup appears (no OS dialog), with Confirm / Cancel.
  Cancel → no change. Confirm → switches; Disable button appears,
  still single-click no confirm.
- Manual: Providers → ASR with active=local-qwen3 shows only the
  "Local ASR is currently active — switch in Advanced" notice,
  no manage UI.

* fix(advanced): pr_agent #400 follow-up — keep disable accessible on unsupported platform

If a user enables local-qwen3 (or foundry-local-whisper) on a machine
that supports it, then their preferences sync to a Linux/unsupported
machine, the previous Advanced UI rendered only the
"platformNotSupported" message and hid the disable button — they had
no in-app path to switch back to cloud and dictation would fail
silently on every press.

Move the disable SettingRow out of the platformSupported branch.
It's gated only on isOnAnyLocal now, so:
- Supported platform + on local → enable Qwen3/Foundry rows + disable row
- Supported platform + on cloud → enable rows only
- Unsupported platform + on local → "not supported" notice + disable row
- Unsupported platform + on cloud → "not supported" notice only

This is the pr_agent advisory from PR #400 review @ d0a425a1.

---------

Co-authored-by: baiqing <lbx12309@icloud.com>
2026-05-10 18:06:28 +08:00
Chris233 a47e27e2c5 Merge pull request #396 from H-Chris233/fix/issue-372-correction-rules
支持自定义听写纠正规则
2026-05-10 14:34:28 +08:00
baiqing 0cc3a2412d docs: codify subagent-mediated API research workflow
User-requested mechanism (/init args): when implementing against any
external surface — third-party HTTP APIs, unfamiliar Rust crates / npm
packages, or platform APIs (Apple Security framework / Win32 / Carbon
/ AppKit) — Claude must dispatch a dedicated subagent to fetch
authoritative documentation rather than write integration code from
training memory. The subagent acts as a network-search-and-retrieval
layer that returns distilled, version-aware usage data so the main
context stays uncluttered and the implementation always tracks the
current upstream surface.

Two surgical edits:

1. AGENTS.md §"Third-party service integrations" — heading widened to
   "Third-party service integrations & library / platform API research"
   so the existing 5-step research-first workflow explicitly covers
   library + platform APIs, not just HTTP service integrations.
   Rephrased the lead-in paragraph to enumerate the broader scope.

2. CLAUDE.md — new top-level section "调研先于编码:派子 agent 查 API
   / 库 / 平台文档" pointing back to AGENTS.md as the canonical rules
   and listing the concrete tooling Claude Code has available:
   Context7 MCP (resolve-library-id + query-docs), the
   documentation-lookup skill, and Agent dispatch with
   subagent_type=general-purpose. Specifies trigger conditions,
   non-trigger conditions (when grep is enough), the prompt fields
   every subagent dispatch must include, and the four canonical
   anti-patterns.

No code changes; pure docs.
2026-05-10 11:05:50 +08:00
H-Chris233andOmX 95237d2c49 Let users repair recurring ASR classifier mistakes
Issue #372 reports repeated classifier errors such as 2粒 being intended as 2例. The existing vocabulary hotword path is probabilistic, so this adds a separate deterministic correction-rules store and applies supported literal/{num} rules in the coordinator before polish and before insertion.

The review follow-up keeps the UI refresh path consistent, handles remove failures, rejects rules that would otherwise save as enabled but never run, and fixes the Traditional Chinese wording.

Constraint: Keep correction rules separate from dictionary.json and vocab presets.

Constraint: Only support literal replacement and one {num} wildcard; do not expose arbitrary regex.

Rejected: Add import/export or a new top-level page | outside the approved scheme-C scope.

Rejected: Change history rawTranscript schema | product/data-model decision not approved in this review round.

Confidence: high

Scope-risk: moderate

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

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

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

Tested: cd openless-all/app && npm run build

Tested: git diff --check

Related: #372

Co-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-10 10:59:07 +08:00
TRIP 225859e18d Merge pull request #395 from appergb/chore/bump-1.2.24-7
chore: bump version to 1.2.24-7 (Beta-7)
2026-05-10 10:53:02 +08:00
baiqing 049147c481 chore(release): sync package-lock.json version to 1.2.24-7
CI ci.yml "Verify version sync across all 5 files" 步骤要求 5 处版本号一致:
package.json / package-lock.json (root + nested) / tauri.conf.json /
Cargo.toml / Cargo.lock 的 [openless] 包。上一个 commit (568d30d) 漏改了
package-lock.json 两处 → Linux + macOS CI fail。

修法跟 PR #383 后续 commit 941542c6 同形:手动同步两处 'version'
字段到 1.2.24-7。下次正确流程是用 scripts/bump-version.sh 一把过。
2026-05-10 10:42:20 +08:00
baiqing 568d30d079 chore: bump version to 1.2.24-7 (Beta-7)
10 PRs landed on beta this cycle:
- #377 paste shortcut configurable (issue #360)
- #386 TS UserPreferences updateChannel alignment
- #387 focus_target leak on Processing-phase cancel
- #388 [严重] MacHotkeyAdapter::shutdown stops CFRunLoop + tap
- #389 emit_capsule window.show/hide off audio thread
- #390 QA / dictation hotkey routing race
- #391 audio-mute spawn_blocking (async hygiene)
- #392 hotkey supervisor + global dispatcher exit signal
- #393 post-audit logic-review hotfixes (QA mute .await + focus_target Processing branch)
- #394 in-process credentials cache (kills repeated Keychain prompts)

Bump 4 files: package.json, tauri.conf.json, Cargo.toml, Cargo.lock.
2026-05-10 10:34:08 +08:00
TRIP 0f7961963b Merge pull request #394 from appergb/fix/keychain-cache
fix(vault): cache CredsRoot in process to stop repeated Keychain prompts
2026-05-10 10:12:27 +08:00
baiqing 8a9fb4f4f8 fix(vault): don't cache fallback creds when keyring read fails
pr_agent on PR #394 flagged a "Stale cache on read error" issue:
both load_credentials / load_credentials_for_update were
unconditionally writing the result into CREDENTIALS_CACHE, including
the legacy-file fallback returned when load_keyring_credentials() hit
an Err (e.g. user hasn't yet clicked "Always Allow" on the first
keychain dialog, login keychain locked, DataProtection error).

Effect: after a transient keyring failure the process pinned the
fallback (or default) creds in cache for its entire lifetime — even
after the user granted access, subsequent reads would never retry
the keyring and would keep returning stale defaults / legacy content
until the app is restarted.

Fix: cache only on the Ok(Some) and Ok(None)→migrate paths. The Err
path now returns the legacy fallback without populating the cache, so
the next call retries the keyring and picks up the user's just-granted
access.

185/185 lib tests pass; cargo check clean.
2026-05-10 10:01:00 +08:00
baiqing 2dc7ac59cf ci: re-trigger pr_agent after PR description cleanup 2026-05-10 09:41:07 +08:00
baiqing b0da8e01b9 fix(vault): cache CredsRoot in process to stop repeated Keychain prompts
Without an in-process cache, every CredentialsVault::get_* /
get_active_asr / snapshot call drove load_credentials() →
load_keyring_credentials(), which reads the manifest entry plus every
chunk entry from the OS keyring. On macOS each distinct keychain entry
has its own ACL — so an ad-hoc-signed binary (or any binary whose ACL
grants haven't yet been "Always Allow"-ed) prompts the user on every
read of every entry. A single dictation cycle reads credentials 5–10
times, multiplied by (1 manifest + N chunks), produces tens of
"OpenLess wants to use the keychain" prompts per recording.

Add a process-wide CREDENTIALS_CACHE: OnceLock<Mutex<Option<CredsRoot>>>:
- load_credentials / load_credentials_for_update consult the cache
  before falling through to Keychain. First read populates the cache;
  every subsequent read in the same process is silent.
- save_credentials populates the cache with the cleaned root after
  successful Keychain write, keeping Settings → Recording credential
  edits visible immediately to dictation.
- migrate_legacy_sources{,_for_update} call save_credentials internally
  so legacy-migration paths inherit the cache update for free.

Trade-off: cross-process changes (e.g. user runs `security` CLI
manually, or a second instance of the app — single-instance is enforced
but defense in depth) are invisible until next launch. Acceptable per
the credential vault contract: the keyring is owned by this app.

Note: this does NOT fix the underlying problem that ad-hoc-signed
builds change cdhash on every rebuild and lose "Always Allow" grants.
For that the build must use a stable Apple Developer ID signature
(APPLE_CERTIFICATE / APPLE_CERTIFICATE_PASSWORD / APPLE_TEAM_ID env
vars). The cache only ensures that within a single process lifetime,
the user is prompted at most once per Keychain entry.

185/185 lib tests pass. cargo check clean.
2026-05-10 09:23:29 +08:00
TRIP 618fc7f0b1 Merge pull request #393 from appergb/fix/post-audit-logic-review
fix(coordinator): post-audit logic-review hotfixes
2026-05-10 09:11:20 +08:00
baiqing d40b08ab44 fix(coordinator): post-audit logic-review hotfixes
Two real bugs surfaced by the 2026-05-10 end-to-end logic review of
beta after the 8-PR audit-fix wave landed.

1. coordinator.rs:2313 — `acquire_recording_mute(inner, "qa");` was
   missing `.await`. PR #391 (`6171df61`) made the function `async fn`
   and updated the dictation call site at coordinator/dictation.rs:451,
   but the QA call site was missed. Effect: when a user has
   `mute_during_recording = true` and triggers QA via Right Option, the
   returned Future is dropped on the next line, `spawn_blocking` is
   never scheduled, holders never increments, and system audio is NOT
   muted (e.g. YouTube playback continues into the QA recording). Both
   the matching `release_recording_mute(inner, "qa")` calls become
   no-ops (early return at holders == 0). The compiler was emitting
   `unused_must_use` for this site (verified before this commit).
   Fix: add `.await`.

2. coordinator/dictation.rs:843-849 — PR #387 (`ce82fcd9`) was framed
   as "clear focus_target on cancel regardless of phase", but the only
   code path that gained the unconditional clear was
   `finish_cancel_session_state`. cancel_session deliberately skips
   that helper for Processing (so end_session can drive its own
   teardown), and end_session's "ASR-finished, cancelled" exit at
   dictation.rs:843-849 set phase=Idle but never touched focus_target.
   Result: cancel-during-Processing leaves a stale `usize` focus slot
   in state.focus_target until the next begin_session_state overwrites
   it. Today the leak is bounded (no documented reader between cancel
   and next begin), but it violates PR #387's stated contract and is a
   silent footgun for any future reader of focus_target on that
   interval. Fix: in the cancelled-after-ASR exit branch, take the
   state lock once and clear both phase and focus_target together.

Source: docs/logic-review-2026-05-10.md (subagent end-to-end review).
185/185 lib tests pass; cargo check clean (unused_must_use gone).
Manual verification checklist for both fixes in the review doc.
2026-05-10 08:42:23 +08:00
TRIP 892f3c4bfe Merge pull request #389 from appergb/fix/audit-emit-capsule-mainthread
fix(coordinator): marshal capsule show/hide to main thread [高]
2026-05-10 08:41:01 +08:00
baiqing e45860538d ci: re-trigger pr_agent after PR description cleanup (no code change) 2026-05-10 08:24:04 +08:00
TRIP 400097adbf Merge pull request #391 from appergb/fix/audit-async-hygiene
fix(coordinator): wrap audio-mute shellouts in spawn_blocking [中]
2026-05-10 08:22:53 +08:00
TRIP ff5e8f8ee1 Merge pull request #377 from appergb/fix/issue-360-paste-shortcut
fix(insertion): make Windows/Linux paste shortcut configurable (#360)
2026-05-10 08:10:15 +08:00
baiqing 1889b7912b fix(coordinator): log spawn_blocking panic in acquire_recording_mute
Addresses pr_agent "Possible Issue" review on PR #391. The original
`let _ = tokio::task::spawn_blocking(...).await` silently swallowed
JoinError, which means a panic inside AudioMuteGuard::activate() (or
the parking_lot lock) would leave the user with system audio leaking
during recording — and zero [audio-mute] log to explain why.

Match the JoinResult and log::error! the panic. State (holders/guard)
stays consistent under panic — activate panic means no guard stored
and holders not incremented; lock panic can't reach the mutate. The
fix is purely about observability.
2026-05-10 08:09:02 +08:00
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 26e31f2d86 Merge remote-tracking branch 'origin/beta' into fix/issue-360-paste-shortcut
# Conflicts:
#	openless-all/app/src-tauri/src/coordinator.rs
2026-05-10 08:00:08 +08:00
Chris233 033e5724ca Merge pull request #390 from appergb/fix/audit-qa-dictation-routing
fix(coordinator): block QA edge routing while dictation runs [高]
2026-05-10 07:34:32 +08:00
Chris233 01c3424353 Merge pull request #392 from appergb/fix/audit-loop-exit-signals
fix(coordinator): add shutdown flag to supervisor + dispatcher loops [中]
2026-05-10 07:33:17 +08:00
Chris233 a6f62e1d6e Merge pull request #386 from appergb/fix/audit-ts-update-channel
fix(types): align TS UserPreferences with Rust update_channel
2026-05-10 07:32:37 +08:00
Chris233 13bdf524f1 Merge pull request #387 from appergb/fix/audit-focus-target-leak
fix(coordinator): clear focus_target on cancel regardless of phase
2026-05-10 07:32:18 +08:00
Chris233 9414c30a99 Merge pull request #388 from appergb/fix/audit-mac-hotkey-shutdown
fix(hotkey): MacHotkeyAdapter::shutdown stops CFRunLoop + tap [严重]
2026-05-10 07:31:58 +08:00
baiqing 4de6164152 fix(coordinator): add shutdown flag to hotkey supervisor + dispatcher loops
The five hotkey supervisor loops in coordinator.rs (dictation / qa /
combo / translation / action) and the global-hotkey dispatcher loop in
global_hotkey_runtime.rs run as `loop { ... sleep N }` with no exit
signal. In production the OS reaps these threads at process exit, but:

- Integration tests can't drive coordinator up/down without leaking
  threads across runs.
- Future RunEvent::Exit cleanup work has nowhere to plug in.
- A panicked listener thread leaves the supervisor spinning forever
  with no way to surface the failure.

Fix:
- Inner gains an `AtomicBool shutdown` field; each supervisor loop
  reads it at the top of each iteration (before any work / sleep) and
  returns when set.
- GlobalHotkeyRuntime gains a parallel AtomicBool shutdown; dispatcher
  loop checks it before each 250 ms recv_timeout.
- Public methods Coordinator::request_shutdown and
  GlobalHotkeyRuntime::request_shutdown set the respective flags.
  Marked #[allow(dead_code)] until the RunEvent::Exit hook lands —
  infrastructure-only change for now.

Crucially this is *passive*: nothing sets the flags in the production
binary, so behavior is unchanged. Tests that want bounded teardown can
now call request_shutdown explicitly.

Audit IDs 3.1.2 + 3.4.4 (CONFIRMED 中 + 低). 3.1.3 (JoinHandle storage)
is deferred — without a supervisor consumer for is_finished()
liveness checks, storing the handle is no-op.

Test: 183/183 lib tests pass. cargo check clean.
2026-05-10 01:44:53 +08:00
baiqing 6171df6105 fix(coordinator): wrap audio-mute shellouts in spawn_blocking
AudioMuteGuard::activate() shells out to osascript on macOS (~100–300 ms
AppleScript runtime startup) and to wpctl/pactl on Linux (similar cost).
Both implementations execute synchronously through std::process::Command.
acquire_recording_mute and release_recording_mute were called from the
async begin_session path, so the tokio worker thread was blocked for the
full shellout duration on every dictation start and stop.

Fix:
- Make acquire_recording_mute async; wrap the existing
  Mutex/holders/activate body in tokio::task::spawn_blocking. The
  parking_lot Mutex never crosses an await — it lives entirely inside
  the blocking task. Caller (start_recorder_for_starting) awaits.
- release_recording_mute stays a sync entry point (some call sites are
  on plain std::thread workers, e.g. the recorder error monitor) but
  delegates to spawn_blocking when a tokio Handle::try_current is
  available. Falls back to running synchronously when there's no tokio
  context — that case is already on a non-async OS thread, so blocking
  there blocks nothing.
- start_recorder_for_starting becomes async to thread the
  acquire_recording_mute().await up. Three call sites (Bailian, generic
  Volcengine, the start_recorder_and_enter_listening helper) get
  matching .await? sequels.

Net effect: dictation-start latency on macOS no longer absorbs the
~100–300 ms osascript startup before Recorder::start can run; the work
runs on a tokio blocking pool thread instead. Same on Linux. Windows
path uses native COM (IAudioEndpointVolume::SetMute), already fast —
spawn_blocking is a small extra hop but harmless.

Audit ID 3.2.4 (CONFIRMED 中). Audit IDs 3.2.3 (sync inserter.insert)
and 3.2.5 (Linux-only 120 ms thread::sleep in permission probe) deferred
to follow-up — both are smaller-impact perf concerns and don't share
code with the audio-mute path.

Test: 183/183 lib tests pass. Manual verification (start dictation,
measure time-to-first-mic-capture before/after) requires the running
app and is expected to drop ~150 ms on macOS.
2026-05-10 01:39:58 +08:00
baiqing f46adce317 fix(coordinator): don't route hotkey edges to QA while dictation runs
handle_pressed_edge previously routed the dictation hotkey to the QA
panel whenever qa_state.panel_visible was true, regardless of whether a
main dictation session was already active. If the user opened the QA
panel mid-dictation (or while polishing/inserting), the next dictation-
hotkey edge was routed into begin_qa_session, which calls
Recorder::start a second time on the same mic device. cpal usually
rejects the second build_input_stream on macOS/Windows but the
dictation session keeps running with no UX path to stop it from the QA
panel; on Linux/PipeWire the second open can succeed and you get two
concurrent capture streams competing for the audio device.

Symmetrically, handle_released_edge swallowed Released entirely when
the panel was visible — fine when QA owned the press, but if dictation
owned the press, Hold-mode dictation could never end because the
Released edge was eaten.

Fix: read inner.state.phase alongside panel_visible. If a dictation
session is non-Idle (Starting / Listening / Processing / Inserting),
both edges go to dictation regardless of panel_visible. The QA panel
stays open, just doesn't capture this hotkey.

3.3.4 (same PR, same files): open_qa_panel always emit'd CapsuleState::Idle
to sweep stale Done residue, but if dictation was mid-flight that
clobbered the in-flight capsule (Recording bar, Polishing progress, the
brief Done toast). Guard the sweep on dictation phase == Idle.

Audit IDs 3.3.1 + 3.3.4 (CONFIRMED 高 + 中).

Test: 183/183 lib tests pass. Manual verification (open QA panel during
dictation, press dictation hotkey — should stop dictation, not begin QA;
also Hold-mode dictation while panel visible should still stop on
release) requires the running app, to be done after merge.
2026-05-10 01:35:10 +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
baiqing f4678cc2ab fix(hotkey): MacHotkeyAdapter::shutdown stops CFRunLoop + disables tap
CFRunLoopRun() in run_listen_loop has no return path; when
HotkeyMonitor::drop runs adapter.shutdown(), the macOS branch fell
through to the empty default impl from the trait. So every preference-
driven monitor swap (e.g. user changes hotkey trigger) leaked the
listener thread + CGEventTap. Long-running sessions that cycle bindings
would steadily accumulate background threads.

Fix:
- Add CFRunLoopStop FFI extern.
- Add MacShutdownHandles { tap, runloop } shared via Arc between the
  listener thread (writes refs in run_listen_loop after CGEventTapCreate
  and CFRunLoopGetCurrent) and MacHotkeyAdapter (reads them in
  shutdown).
- Implement MacHotkeyAdapter::shutdown — disable tap then stop runloop,
  in that order, so the OS stops dispatching before we tear down.
  take()s on the option locks make shutdown idempotent.
- run_listen_loop now does Box::from_raw(context) after CFRunLoopRun
  returns so the listener thread releases the callback context before
  exiting.
- Move tap re-enable in tap_callback (TAP_DISABLED_BY_TIMEOUT) to read
  through ctx.handles.tap (same lock the adapter releases on shutdown,
  consistent with the rest of the FFI thread-safety story).

Mirrors the Windows adapter's PostThreadMessageW(WM_QUIT) shutdown
pattern. Apple documents CGEventTapEnable + CFRunLoopStop as safe to
call from any thread, captured in a SAFETY comment on the
unsafe Send/Sync impl.

Audit ID 3.1.1 (CONFIRMED 严重).

Test: hotkey + coordinator + types + commands lib tests pass (31/31).
Functional verification of the leak fix requires running the app and
swapping bindings — to be done after PR ships.
2026-05-10 01:29:22 +08:00
baiqing ce82fcd9fa fix(coordinator): clear focus_target on cancel regardless of phase
finish_cancel_session_state previously only cleared state.focus_target
when the cancelled phase was not Processing — Processing-phase cancel
left the stale window-resource handle in place until the next
begin_session_state overwrote it. Between cancel and next begin, any
reader of focus_target sees a value belonging to a session that is
about to be torn down.

Move state.focus_target = None ahead of the phase check so it always
fires when the cancel was accepted (i.e. not rejected for Idle /
Inserting phases). Phase transition stays conditional on the original
non-Processing branch, since end_session must still own the
Processing → Idle collapse to avoid racing with polish/insert.

Tighten cancel_session_state_machine_is_table_driven so it asserts
focus_target gets cleared for every accepted phase (including
Processing) and stays untouched on rejected cancels.

Audit ID 3.3.5.
2026-05-10 01:25:11 +08:00
baiqing fd2d43083b fix(types): align TS UserPreferences with Rust update_channel field
Rust types.rs declares pub update_channel: UpdateChannel, but the TS
UserPreferences interface in src/lib/types.ts and the mockSettings
fixture in src/lib/ipc.ts both omitted the field. setSettings()
round-trips were lossless only because Rust's serde(default) refilled
the field, but consumers that destructured UserPreferences would
silently miss it.

- Move UpdateChannel type from ipc.ts to types.ts (avoids ipc <-> types
  circular import); ipc.ts re-exports for backward compat with
  SettingsModal etc.
- Add updateChannel: UpdateChannel to UserPreferences interface.
- Add updateChannel: 'stable' to mockSettings fixture.
- Update stylePrefs.test.ts fixture for new required field.

Audit ID 2.2.1 (docs/audit-2026-05-10-validated.md, kept local).
2026-05-10 01:23:28 +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