Files
openless/docs
b4d1317e77 feat(providers): LLM/ASR 供应商改成渠道卡片——同一家可存多把 key,排序即优先级 (#918)
* 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>
2026-08-07 23:09:41 +08:00
..