Commit Graph
20 Commits
Author SHA1 Message Date
baiqing d8fb309feb feat(persistence): generalize providers.llm + multi-provider vault API
将 v1 schema 中 `providers.llm` 从 Ark 专用结构推广为通用
`CredentialsProviderLLMEntry`(containing displayName / temperature / extraHeaders),
为 OpenAI / DeepSeek / DashScope / Moonshot 等多 LLM provider 共用一套字段。

旧 `CredentialsProviderLLMArk` 保留为 typealias,已迁移过的 v1 文件无需再次迁移即可继续解码。

新增公开 API:
- `CredentialsVault.activeLLMProviderId` getter/setter
- `CredentialsVault.configuredLLMProviderIds`
- `CredentialsVault.llmProviderConfig(for:)`
- `CredentialsVault.setLLMProviderConfig(_:)`
- `CredentialsVault.removeLLMProvider(_:)`,禁删 active provider

`OpenLessCore` 新增 `LLMProviderRegistry`,集中维护 5 家 OpenAI 兼容供应商
预设(baseURL / model / 帮助文案 / docs URL),UI 与 vault 兜底逻辑共用。

`PolishOutputCleaner` 从 DoubaoPolishClient 内部提升为独立 public 类型,
让 `OpenAICompatibleLLMProvider` 输出统一过 boilerplate / markdown 栅栏清理。

DoubaoPolishClient.swift 已无调用方,整个文件删除。

Tests:
- LLMProviderRegistryTests:5 家预设健康检查 + 唯一性 / URL 合法性
- CredentialsVaultMultiProviderTests:active 默认 / 增删 / 往返 /
  老 `ark.*` 账号 key 路由到 active provider
2026-04-29 11:54:11 +08:00
baiqing b48d60f74c Merge B-2: Credentials schema v1 + migration
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
2026-04-29 11:33:05 +08:00
baiqing c072816beb feat(persistence): migrate CredentialsVault to versioned v1 schema
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
2026-04-29 11:25:13 +08:00
baiqing dfcdd3184b feat: introduce LLMProvider protocol + OpenAICompatibleLLMProvider
为 polish 层引入与具体供应商解耦的抽象:
- OpenLessCore: LLMProvider 协议、OpenAICompatibleConfig、LLMError
- OpenLessPolish: 单一 OpenAICompatibleLLMProvider 实现,覆盖
  Ark / OpenAI / DashScope / DeepSeek / Moonshot 等所有 OpenAI Chat
  Completions 协议端点
- DoubaoPolishClient 标记为 @available deprecated(Phase B-3 接入新 provider 后移除)
- 新增 9 项 URLProtocol 桩测试,含 apiKey 不入日志的安全断言
2026-04-29 11:23:51 +08:00
baiqing e029f0bb17 release: v1.0.07 (build B1006) 2026-04-28 17:37:18 +08:00
baiqing 7ccda2b280 fix(updater): route settings update action 2026-04-28 13:35:38 +08:00
baiqing c36ec22618 fix(help): correct GitHub URL + clean up FAQ / link layout
- HelpTab GitHub 链接 baiqing → appergb,老地址 404
- helpLink 不再把整段 URL 当 label(中间截断 + 和标题挤一行),
  改成右侧「打开 ↗」按钮,hover tooltip 显示完整 URL
- helpFAQ Q/A 间距 4 → 8、加 lineSpacing 2、行 padding 9 → 11,
  提升可读性
2026-04-28 13:28:50 +08:00
baiqing cd744d5d9b fix(dictionary): preserve hitCount when editing entry; refresh HelpTab copy
- DictionaryEditorSheet 保存时把原 hitCount 带回去,否则编辑词条会让命中数静默归零
- 帮助中心快速上手 / 常见问题里"火山 ASR"、"润色模式"指向已合并的 Tab,
  改为指向"设置"和"风格"
- 加 1.0.04 release notes(B 系第一个版本,主体是 1697→ 多文件拆分)

Codex rescue 体检发现,发版前修。
2026-04-28 13:20:35 +08:00
baiqing d94be8080d refactor(settings): split 1697-line SettingsView.swift into single-responsibility files
按用户的"800 行红线、单一职责"约束拆分。零行为变化,只是物理文件结构。

- SettingsView.swift  87 (root: 枚举 + 导航模型 + 主分发)
- SettingsComponents.swift  218 (SettingsPage / GlassSection / GlassPanel /
  MetricTile / Notification 名 / polishModeHint / isFilled)
- Sidebar.swift  270 (FixedSidebar + 状态卡 + stats + WindowCanvasBackground)
- Tabs/HomeTab.swift  161
- Tabs/HistoryTab.swift  50
- Tabs/DictionaryTab.swift  349 (含 Chip + ChipFlow + EditorSheet)
- Tabs/StyleTab.swift  123 (含 StyleCard)
- Tabs/HelpTab.swift  100
- Tabs/SettingsHubTab.swift  184
- Tabs/LegacyTabs.swift  172 (Overview/Hotkey/Modes/Privacy 旧版孤儿,原样搬)

private 改成默认 internal 让跨文件可见,封装边界仍止于 OpenLessApp 模块内部。
项目最长文件从 1697 行 → 566 行 (DictationCoordinator),全部在 800 红线下。
2026-04-28 13:13:37 +08:00
baiqing 88a5a97812 feat: 1.0.03 — 侧边栏重构 + 词汇表 chip 布局 + 风格页 + 关掉自动学词
- 侧边栏 4 → 6 项:首页 / 历史记录 / 词汇表 / 风格 / 帮助中心 / 设置
- 词典 → 词汇表,整页改 chip 流式布局:phrase + hitCount,hover ✕ 删除,
  批量输入(每行一个),重置统计 / 清除全部
- DictionaryEntry 加 hitCount + 兼容旧 dictionary.json 解码
- DictationCoordinator 在润色后扫描最终文本递增命中数;
  删除 learnTerms 自动学词路径
- 润色模式 → 风格 (StyleTab):4 张模式卡 + 启用开关;
  关闭时跳过 Ark 直接插入原文 (UserPreferences.polishEnabled)
- 帮助中心:快速上手 4 步、快捷键速查、FAQ、GitHub 外链
- 设置 Tab 合并 Volc ASR + Ark 凭据、录音、授权、隐私、关于 + 检查更新
- 胶囊:NSPanel 220×96, hasShadow=false;macOS 26 分支去掉 SwiftUI 阴影
  → 解决"红绿灯外圈黑边"
- 侧边栏视觉:strokeBorder 0.08 → 0.04、阴影 radius 16 → 6
  → 解决"灰色边栏 + 黑色卡顿"
- 首页底部新增今日概览 + 风格预览
2026-04-28 11:16:40 +08:00
TRIPandbaiqing f38643f17c feat: Sparkle 2 auto-update
* 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>
2026-04-27 18:58:37 +08:00
TRIPandbaiqing 56c3e09983 feat: hold-to-talk + UX fixes + structured prompt overhaul
* 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>
2026-04-27 18:57:41 +08:00
baiqing da14031c83 fix: use app png for status bar icon 2026-04-27 15:17:12 +08:00
baiqing 8965fb4209 feat: add custom svg status bar icon 2026-04-27 15:08:16 +08:00
baiqing 1cc6e68c70 fix: stabilize glass sidebar layout 2026-04-27 14:41:01 +08:00
baiqing cb00914423 fix: restore sidebar scrolling and glass input capsule 2026-04-27 14:34:17 +08:00
baiqing 9866773f34 fix: hide dock icon when main window closes 2026-04-27 14:24:23 +08:00
baiqing e9d4335db9 feat: add floating glass sidebar 2026-04-27 14:16:55 +08:00
baiqing 4f8e6b914e feat: add dock main window and fixed navigation 2026-04-27 14:07:12 +08:00
baiqing f5a0c810fa feat: add dictionary hotwords and insights UI 2026-04-27 13:53:18 +08:00