* feat(providers): LLM/ASR 供应商改成渠道卡片——同一家可存多把 key,排序即优先级 原来一个供应商只能存一份配置,换 key 只能把旧的覆盖掉;想在主号/备号之间来回切, 每次都要重新粘贴。这个 PR 把「服务 → AI 提供商」从「下拉选厂商 + 一组字段」改成 一张张可命名、可排序、可开关的渠道卡片。 心智只有一条:**排序即优先级,列表里第一个启用的就是当前生效的渠道**。关掉的渠道 自动沉到末尾;后端不另存「当前选中」,避免「列表第一张是 A、实际请求打的是 B」 这种两处真相。 本 PR 只做多渠道的存储、编辑与切换,**不含失败重试与故障转移**(那是下一个 PR, 需要先把听写/润色链路里几十处隐式读 active 凭据的地方显式化)。 存储与迁移 - ChannelMeta(providerType / order / enabled / lastTest)以 serde(flatten) 嵌进 ASR、LLM 两个 entry;v1 老 payload 照常反序列化。 - providerType 与渠道 id 解耦:同一家厂商的多张卡片各有自己的 id,但 providerType 都指向同一个厂商实现。coordinator::resolve_effective_asr_provider 和 commands 里 几十处 `== PROVIDER_ID` 的比较依赖它,拿成 id 会让整个 ASR 路由失效。 - v1→v2 迁移幂等:迁移出的渠道 **id 沿用原 preset id**,不生成 uuid,老用户的 map key 一个字节都不变,重复执行结果一致。 - 迁移只在内存里做、不主动落盘:启动时写 keyring 会在 macOS 触发钥匙串 ACL 弹窗, 留给下一次真实写入顺带固化。 - clean_credentials 原本会 retain(!is_empty) 删掉空 entry —— 刚点「添加渠道」、名字 取好了还没填 key 的卡片会被静默删掉,改为「渠道卡片只能由用户显式删除」。 - ChannelMeta 手写 Default(derive 会让 enabled=false,而 write_account 用 entry().or_default() 建 entry,新写入的渠道会一出生就被禁用)。 - Windows 全新安装预置一张 Foundry 本地 ASR 卡片,保住开箱即用。 IPC - 新增 list/create/rename/delete/set_enabled/reorder/record_test 七个命令。 - set_credential/read_credential 的 provider 作用域原本硬性拒绝 LLM 账户,补上 get/set_for_llm_provider —— 编辑列表里第 3 张 LLM 卡片必须能按 id 定位。 - validate_provider_credentials / list_provider_models 新增可选 channel_id: 卡片上的「测试连通」要测用户点的那一张,而不是当前生效的那张。 前端 - ChannelList:卡片列表、拖拽排序、开关沉底、生效中/失败标红/延迟展示。 - 添加与编辑弹窗;本地引擎与 Codex OAuth 不做预置固定卡片,和云端厂商一样从 「添加渠道」里选,只是编辑时没有 key/地址字段。 - 新手引导(Onboarding)列表为空时直接摊开添加表单,不让新用户对着空列表发呆。 - 五种语言文案齐备。 验证:cargo test --lib persistence::credentials(31 passed)、commands::(104 passed)、 npm test(含 tsc + vite build,退出码 0)、浏览器 mock 预览逐项走查四种卡片状态与两个弹窗。 设计与分期见 docs/provider-channels-plan.md。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(channels): 添加改成一步、拖拽在真机能用、卡片状态不再假装"健康" 三处都是装机自用后暴露的问题。 **添加渠道少一步。** 原来是「先建卡片、再填凭据」两步 —— 那只是实现上需要先有渠道 id 才能写凭据(凭据按 id 作用域存),不该变成用户多点一次。改成点+ 直接开一个完整弹窗, 供应商、名字、密钥、地址、模型、连接检查都在一屏里;草稿卡片在后台先建出来,用户什么 都没填就关掉时由 delete_channel_if_blank 回收,不会在列表里留空卡片。换供应商也不再 需要重建卡片,走新增的 set_channel_provider_type。 **拖拽在打包后的 app 里根本不动。** Tauri 的 webview 默认开着 dragDropEnabled,会把 HTML5 的 dragstart/drop 当成文件拖放吞掉 —— 浏览器预览里是好的,真机里是坏的,只验 前者就会漏。改用 pointer 事件手写,顺带让 Windows / Android 行为一致。 改的过程中炸出第二个:最初用 setPointerCapture,它把后续事件重定向到手柄,浏览器补发 的 click 于是落到设置弹窗的遮罩上(遮罩挂着 onClick={onClose}),**拖一下卡片整个设置 面板就关了**。改成 window 级监听不动事件目标,并在捕获阶段吞掉拖拽后紧跟的那一次 click。 **卡片状态不再假装"健康"。** 原来当前那张有绿点 + 「生效中」文字,两者都被读成"这张 能用",可它只代表排在最前面 —— 一张 key 已经失效的卡片照样排第一。优先级和健康度是两 个正交的维度,被压成了一个视觉。现在: - 绿点与「生效中」全部去掉,当前那张只用左侧一条竖条表达位置,不带健康暗示; - 验证按钮移到卡片上(开关左侧),**按钮自己就是结果容器**:未验过显示「验证」、 通过显示延迟数字(`284ms`,数字本身既说明通了又能比快慢)、失败显示能指导行动的短 标签(`✗ 401` 改 key / `✗ 429` 等会儿 / `✗ 超时` 查网络); - 副行显示上次验证是多久以前,超过一天的结果褪色 —— 让"这条结论会过期"可见; - 按钮宽度固定,避免文字变化把开关和箭头挤来挤去; - **不做自动验证**:验证是真实 API 调用(LLM 走一次真润色、ASR 传一段静音音频), 打开设置就全部验一遍等于按卡片数烧额度,还容易撞进限流。 顺带把 mock 的 reorderChannels 改成真重排:原来是空操作,浏览器预览里松手后顺序被 listChannels 拉回原样,看着像"拖拽坏了",会误导下一个人。 另:补上此前遗漏的 rustfmt(自己编辑范围内的行)。 验证:cargo test --lib persistence::credentials(31 passed)、commands::(101 passed)、 tsc 0;浏览器侧用精确的 pointer 事件序列验了拖动跟手、松手持久化、设置面板不被误关。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(channels): 迁移时优先把「填过凭据」的渠道排第一,别让空卡片顶到最前 `active` 指向一个已不存在的 entry 是真实会发生的:前端 prefs 里的 activeAsrProvider 与凭据库里的 active.asr 是两份数据,历史上可能不同步。原来遇到这种情况纯按字母序挑一张 排第一,完全不看那张有没有填过 key —— 结果很容易把一张空卡排到最前,用户升级后打开就 看到「未配置」,而他真正配好的那张其实还在列表下面躺着。 排序优先级改为:原 active → 填过凭据的 → 字母序(兜底,保证幂等)。 同时补一个边界测试钉死底线:即使 active 指向缺失 entry,迁移也**绝不碰任何凭据** —— 所有 key 原样留在各自 entry 里,用户把想用的那张拖回第一位就能恢复。 验证:cargo test --lib persistence::credentials(33 passed,跑前已备份 preferences.json 且跑后 md5 未变)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(channels): 写明重试尚未实现,并区分代码里已有的连接层重连与渠道故障转移 排查时在代码里搜到 retry 容易误以为渠道重试做了。补两条既有行为的说明: net.rs::send_with_retry 只对连接层失败重连同一个 endpoint(拿到任何 HTTP 响应即返回、 超时不重试),永远不会换卡片;润色失败已经会回落插入 ASR 原文,所以「全渠道失败 → 出原文」这条决策天然满足,P2 要做的是在回落前多试几张卡片。 同时写明 P0 的定位:多渠道现在的价值是存档与手动切换,排第二的卡片不会被自动用上。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(channels): 修复渠道化 Spec 缺陷——禁用语义/order 冲突/默认值回填/LocalAsr 激活 - sync_active_channels 全部禁用时清空 active,运行时不再使用已禁用渠道凭据 - create_channel 落盘前 compact_orders,消除新卡与禁用项 order 冲突 - 换供应商时仅空槽写入 preset 默认 endpoint/model(不覆盖用户已填值) - LocalAsr 激活前确保本地引擎渠道卡存在、启用并置顶 - 更新 provider-channels-plan 状态与待确认项 * fix(channels): 修复 Omni 路由与草稿生命周期 --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Chris233 <h-chris233@outlook.com>
OpenLess All-Platform
This is the current cross-platform OpenLess workspace.
App Directory
The runnable Tauri app lives in app/. The macOS build links a vendored C ASR engine (Open-Less/qwen-asr, forked from antirez/qwen-asr) tracked as a git submodule under app/src-tauri/vendor/qwen-asr/, so initialize submodules on first clone.
# First clone only — pull in vendored submodules
git submodule update --init --recursive
cd app
npm ci
npm run tauri dev
macOS Build
Use the project build script instead of calling tauri build directly:
cd app
INSTALL=0 ./scripts/build-mac.sh
Generated macOS artifacts:
app/src-tauri/target/release/bundle/macos/OpenLess.appapp/src-tauri/target/release/bundle/dmg/OpenLess_1.1.0_aarch64.dmg
For local install during development:
cd app
./scripts/build-mac.sh
Windows Build
The runnable Tauri app is still app/. Windows contributors should run a
preflight before building so missing MSVC, Windows SDK, or MinGW tools fail
with actionable messages.
cd app
powershell -ExecutionPolicy Bypass -File .\scripts\windows-preflight.ps1
MSVC Route
Use this route when Visual Studio Build Tools and the Windows SDK are installed.
Open a Developer PowerShell, or call vcvars64.bat, then run:
cd app
npm ci
npm run tauri -- build
Required Visual Studio Installer components:
Microsoft.VisualStudio.Workload.VCTools- MSVC v143 x64/x86 build tools
- Windows 10/11 SDK that provides
kernel32.lib
If link.exe or kernel32.lib is missing, rerun:
powershell -ExecutionPolicy Bypass -File .\scripts\windows-preflight.ps1 -Toolchain msvc
GNU / MinGW Route
Use this route when MSVC/Windows SDK is unavailable. The app now lives under
the no-space openless-all directory to avoid GNU/MinGW path quoting issues
while generating import libraries. Use the helper script to keep the GNU build
environment and target setup consistent.
cd app
scoop install rustup mingw
rustup toolchain install stable-x86_64-pc-windows-gnu
rustup target add x86_64-pc-windows-gnu
powershell -ExecutionPolicy Bypass -File .\scripts\windows-preflight.ps1 -Toolchain gnu
powershell -ExecutionPolicy Bypass -File .\scripts\windows-build-gnu.ps1
Generated GNU artifacts:
%TEMP%\openless-windows-gnu\src-tauri\target\x86_64-pc-windows-gnu\release\openless.exe%TEMP%\openless-windows-gnu\src-tauri\target\x86_64-pc-windows-gnu\release\bundle\msi\OpenLess_*_x64_en-US.msi%TEMP%\openless-windows-gnu\src-tauri\target\x86_64-pc-windows-gnu\release\bundle\nsis\OpenLess_*_x64-setup.exe
Hotkey Injection Gate
Use this gate before/after Windows hotkey changes when a physical keyboard
regression is unavailable. It injects a dev/test-only hotkey click through the
coordinator handle_pressed / handle_released path, asserts the log contains
[coord] hotkey pressed, and cancels the dry-run session automatically.
cd app
npm run check:hotkey-injection
Windows Runtime Notes
- Windows does not need the macOS Accessibility permission. Use Settings -> Permissions -> Global hotkey to inspect listener status.
- Microphone permission is checked by opening a short-lived input stream, so a device-format query alone is not treated as permission granted.
- Text insertion through
Ctrl+Vis treated as copy fallback unless the app can confirm insertion.
Release Signing
Tagged releases (v*-tauri) must be Developer ID signed and notarized so users can download and open the macOS app without manually removing quarantine attributes.
Required GitHub secrets:
APPLE_CERTIFICATEAPPLE_CERTIFICATE_PASSWORDAPPLE_IDAPPLE_PASSWORDAPPLE_TEAM_ID
Optional:
APPLE_PROVIDER_SHORT_NAMEKEYCHAIN_PASSWORD
Manual workflow runs can still produce ad-hoc signed test builds, but tagged macOS releases fail if signing/notarization secrets are missing.
Ignored Local Output
The following are intentionally local-only:
app/node_modules/app/dist/app/src-tauri/target/app/src-tauri/gen/.DS_Store