* 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>