The unified-config-tab branch landed first and used UserDefaults as a
temporary bypass because the asr-aliyun-and-custom-whisper branch (which
extended the schema and added vault.asrProviderConfig helpers) hadn't
merged yet. Now both are merged, so route drafts through
vault.asrProviderConfig(for:) / setASRProviderConfig(_:for:) /
removeASRProvider(_:). DictationCoordinator's factory reads from vault,
so this closes the storage mismatch — saving an Aliyun apiKey in
the UI now actually reaches the provider at runtime.
Adds AliyunParaformerASRProvider (DashScope WebSocket streaming via
URLSessionWebSocketTask, paraformer-realtime-v2 model) and
CustomOpenAIWhisperASRProvider (HTTP batch via multipart upload, 25MB
guard, WAV header wrapping, BCP-47 → ISO-639-1 language mapping).
CredentialsSchemaV1 generalizes providers.asr to per-providerId
[String: CredentialsProviderASREntry] with optional apiKey/baseURL/model
alongside legacy appKey/accessKey/resourceId. Fully backward compat
(encodeIfPresent + typealias for old name).
Vault gains asrProviderConfig(for:) / setASRProviderConfig(_:for:) /
removeASRProvider(_:) helpers.
DictationCoordinator factory adds cases for aliyun-paraformer (fully
wired streaming, auto-falls-back to LLM aliyun-dashscope apiKey when ASR
side is empty) and custom-openai-whisper (factory case in place but
coordinator still only calls openStreamingSession; batch dispatch
TODO).
Merges LLM tab and ASR tab into a single 配置 tab placed above 设置 in
sidebar. Volcengine 3-field form moves out of SettingsHubTab into the
new tab's ASR section. Single prominent 保存 button at bottom commits
all pending changes (LLM provider switch + LLM provider configs + ASR
provider switch + Volcengine creds + new ASR provider creds).
ASRProviderRegistry adds aliyun-paraformer and custom-openai-whisper
presets. New ASR provider classes are added in a separate parallel
branch.
Adds AppleSpeechASRProvider (SFSpeechRecognizer wrapper, on-device when
available, hotwords via contextualStrings, async TCC auth). New ASR tab
in Settings with chip-row picker (Volcengine + Apple Speech), prominent
borderedProminent .large Save button gated on dirty state. Vault gains
activeASRProviderId getter/setter + configuredASRProviderIds.
DictationCoordinator routes makeASRProvider() through the active id.
Info.plist gains NSSpeechRecognitionUsageDescription via build-app.sh
heredoc — single targeted addition.
Branch: feature/apple-speech-asr-and-switcher
Phase C-1 of provider abstraction. Adds ASRProvider / ASRStreamingSession
protocols + ASRError / ASRMode / ASRProviderInfo in OpenLessCore.
VolcengineASRProvider wraps existing VolcengineStreamingASR via adapter
without modifying the Volcengine class itself (zero edits to
VolcengineStreamingASR/Frame/Credentials). DictationCoordinator now
routes ASR through the protocol via a hardcoded makeASRProvider()
factory; vault.activeASRProviderId plumbing comes in Wave 3c.
AudioConsumer bridged via default-impl forwarder so existing
Recorder→Buffering→session.consume(pcmChunk:) flow is unchanged.
partialResults stream exposed but always finishes immediately —
VolcengineStreamingASR doesn't expose partial callbacks and we promised
zero edits to it. Confirmed no UI surface used partial transcripts, so
this is a documentation-only difference, not a regression.
Branch: feature/asr-provider-abstraction
Phase B-3 of provider abstraction. Adds LLMProvidersTab Settings UI
with 5 OpenAI-compat presets (豆包/OpenAI/阿里通义/DeepSeek/Moonshot) +
custom OpenAI-compatible entry. Generalizes providers.llm to
[String: CredentialsProviderLLMEntry] (backward compat via typealias).
Rewires DictationCoordinator to use OpenAICompatibleLLMProvider against
the active LLM provider config, preserving silent fallback to raw
transcript when creds missing. PolishOutputCleaner extracted from old
DoubaoPolishClient (now deleted) so OpenAICompatibleLLMProvider keeps
the boilerplate-stripping behavior.
21 new tests across LLMProviderRegistryTests + CredentialsVaultMultiProviderTests.
Branch: feature/settings-ui-llm
Phase B-2 of provider abstraction. CredentialsVault migrates flat v0
schema (volcengine.* / ark.* dot-style keys) to versioned v1 with
providers.{asr,llm}.* sections. Original v0 file backed up to
credentials.v0.bak.<timestamp>.json before any write. Atomic write
preserves 0600 mode. 14 unit tests covering fresh/full/partial
v0/already-v1/futureVersion/damaged/backup-conflict cases.
Existing CredentialsVault public API unchanged — no caller (Settings,
DictationCoordinator) needed modification.
Branch: feature/credentials-schema-v1
Phase B-1 of provider abstraction. Adds LLMProvider protocol and
OpenAICompatibleLLMProvider single implementation in OpenLessPolish.
DoubaoPolishClient retained, marked deprecated. 9 unit tests added.
Branch: feature/llm-provider-abstraction
Split flat v0 credentials.json into version=1 schema with provider
sections (asr/llm), each keyed by providerId. Add automatic migration
on first load with safe backup of the original v0 file.
- New types: CredentialsSchemaV1, CredentialsProviderASRVolcengine,
CredentialsProviderLLMArk, CredentialsActiveSelection
- New CredentialsError enum: unparseable / futureVersion / ioError /
backupFailed / writeFailed
- Pure migration function (CredentialsMigration.parseAndMigrate) for
unit testability — no disk I/O
- Vault: atomic write via tmp + replaceItemAt, mode 0600 preserved
- Backup format: credentials.v0.bak.<timestamp>[-N].json
- Existing public API (get/set/remove/snapshot, CredentialAccount
constants) is unchanged so SettingsHubTab / Sidebar continue to work
- New OpenLessPersistenceTests target with 13 migration tests covering
fresh install, full v0, partial v0 (asr-only / llm-only), empty
ark.endpoint default, future-version safety, damaged-JSON safety,
backup-name conflict suffix, idempotent save, atomic write, and
0600 permissions
The first release.sh run wrote a Chinese-locale pubDate ("一, 27 4月 2026 …")
into appcast.xml, which Sparkle's RFC 822 date parser rejects. Force
LC_ALL=C around the date call so future releases produce the standard
"Mon, 27 Apr 2026 11:02:01 +0000" format. Manually fix the v1.0.02
entry's pubDate and move <language> back above <item> for a tidy feed.
* feat: integrate Sparkle 2 for auto-update
Adds the standard pieces for Sparkle-based silent updates:
- Package.swift: add Sparkle 2 SPM dep, wired into OpenLessApp.
- Sources/OpenLessApp/Updater/UpdaterController.swift: thin wrapper
around SPUStandardUpdaterController. Owned by AppDelegate so the
updater starts on app launch.
- AppDelegate: instantiate UpdaterController before installing the
main menu; pass it to ApplicationMenu so the menu item can target
it.
- ApplicationMenu: add "检查更新…" as the first item in the OpenLess
menu when an UpdaterController target is present.
- scripts/build-app.sh: copy Sparkle.framework (with Updater.app +
XPC services + Autoupdate) from the SPM artifacts dir into
Contents/Frameworks/, inject SUFeedURL / SUPublicEDKey /
SUEnableAutomaticChecks / SUEnableInstallerLauncherService /
SUScheduledCheckInterval into Info.plist, and ad-hoc sign the
embedded helpers explicitly (deep sign alone is unreliable for
Sparkle's nested XPC bundles).
- scripts/release.sh: one-shot release pipeline.
./scripts/release.sh <version> [<build>] [<notes>]
Bumps version in build-app.sh, builds, zips, signs the zip with the
Keychain-stored EdDSA private key (auto-downloads sign_update from
the Sparkle 2.9.1 release on first run), prepends a new <item> to
appcast.xml, commits + tags + pushes main, and creates the GitHub
release with the zip uploaded.
- appcast.xml: empty initial feed file at repo root, served via the
raw.githubusercontent.com URL referenced by SUFeedURL. Items are
appended automatically by release.sh.
Public EdDSA key embedded in the build:
iT00+eUw/55obn1suEnWqI7za2pc8mHIFIdRbOWXW1Q=
Private key lives only in the local Keychain on the release machine
(account: macOS login user; service: https://sparkle-project.org).
First-time install still requires the user to remove the quarantine
xattr (no Apple Developer ID); subsequent updates flow through
Sparkle's installer helper and bypass quarantine.
* fix(sparkle): add @executable_path/../Frameworks rpath to OpenLessApp
Without this, the SPM-built executable links Sparkle's symbols but
ships with no rpath that points at the embedded framework, so dyld
fails at launch with:
Library not loaded: @rpath/Sparkle.framework/Versions/B/Sparkle
…and the user sees the macOS Gatekeeper-styled "请与开发者联系,以确保
OpenLess 可以配合此 macOS 版本使用" error. Inject the rpath via
linkerSettings.unsafeFlags so the binary itself knows where to look.
---------
Co-authored-by: baiqing <lbx12309@icloud.com>
* feat: add hold-to-talk hotkey mode (closes#1)
OpenLessHotkey now emits explicit .pressed / .released edge events on
the modifier-key trigger; the coordinator interprets them per the new
HotkeyMode preference (toggle stays the default, hold = press-and-hold).
Hold mode rules: .pressed on idle starts the session; .released stops
it from .listening; releasing during .starting cancels (no audio
useful was captured yet). Esc-cancel sets a one-shot suppress flag so
the trailing .released after a cancel does not re-trigger end.
Settings hub gets a "录音方式" segmented picker and a one-line hint
explaining the tradeoff. Bump version to 1.1.0; update README roadmap
and USAGE walkthrough to document the new mode.
* test: update HotkeyEventTests for pressed/released split
* chore: tag hold-to-talk build as 1.0.01 / A1003
* fix: stop window-background drag eating text selection; add eye toggle on secret fields
Settings window had isMovableByWindowBackground = true, so any drag
inside the content view dragged the whole window — making it
impossible to select words inside a TextField. Disable it; the native
title-bar strip (with the traffic lights) still drags the window.
PasteableCredentialField now exposes an eye / eye.slash toggle when
secure: true, so users can verify pasted API keys before saving.
* fix: stop pretending polish ran when it was skipped or errored
When the user removes the Ark API key (or the Ark call errors out),
DictationCoordinator was still showing the green "已插入" capsule and
recording the session in history under the originally-selected polish
mode. From the user's side it looked like polish had succeeded, even
though only the raw transcript reached the cursor.
Track a PolishOutcome (ok / skippedNoCredentials / failed) through
insertText. Capsule now uses a new CapsuleState.warning (orange) state
to surface honest messages — "已插入原文 · 未润色" / "润色失败 · 已用原文" —
and history saves the session as PolishMode.raw whenever polish
didn't actually run. The error capsule + 1.5s artificial sleep that
preceded a fake "success" insert is gone; warning state stays for the
normal 2.5s.
* feat(polish): make Structured mode prompt force a 1./1)/a. hierarchy
Some weaker chat models would emit a single flat list when the user
asked for Structured polish, even when the dictation clearly covered
multiple topics. The mode is meant to produce something pasteable as
an AI prompt or a working doc, not a wall of bullets.
Rewrite the .structured system prompt to be prescriptive: when the
input contains ≥2 themes, emit three explicit levels — top "1./2./3."
sections, "1)/2)/3)" sub-points indented 3 spaces, and "a./b./c."
detail items indented another 3 — and to fall back to a plain
paragraph when the input is short / single-topic. Add a concrete
format example so weaker models have a pattern to copy.
---------
Co-authored-by: baiqing <lbx12309@icloud.com>
Promote the English README to README.md and move the Chinese version to
README.zh.md so the default GitHub view ships in English. Replace the
incorrect "Talk (Talktastic)" reference in the comparison and intro
with Typeless (typeless.com), which is the actual product OpenLess is
positioned against. Update the cross-language switch in both files.
Add a centered icon header pulling Resources/AppIcon.png, language switch,
and shield badges for release / license / macOS / Swift / stars. Lead with
a tangible "say this, get this prompt" SQL example so the AI-prompt
positioning is obvious within the first screenful. Mirror in English.
Reframe README around the AI-prompt use case (dictate loose speech →
get a usable AI prompt) and mark OpenLess as the open-source
counterpart to Wispr Flow, Talk (Talktastic), Lazy, and Superwhisper.
Add a comparison table, a "why open source" section, and use cases.
Add an English mirror at README.en.md and cross-link both languages.
Bump APP_VERSION to 1.0.0 / BUILD_NUMBER to 1; refresh README to clarify
project direction and add quarantine-removal guidance for the unsigned
release artifact; add USAGE.md as the end-user walkthrough; add MIT LICENSE.