From 1accecbd57e967fe3669d912f4d8d937233954de Mon Sep 17 00:00:00 2001 From: baiqing Date: Mon, 4 May 2026 08:44:37 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(settings):=20=E5=88=87=E6=8D=A2=20LLM/A?= =?UTF-8?q?SR=20preset=20=E9=A1=BA=E5=BA=8F=E9=94=99=E5=AF=BC=E8=87=B4=20U?= =?UTF-8?q?I=20=E6=98=BE=E7=A4=BA=E6=97=A7=20active=20=E5=87=AD=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 底层 providers HashMap 已是 per-provider,bug 完全在前端两个 onChange: 1) Race setLlmProvider(id) / setAsrProvider(id) 是同步 React state 改动, CredentialField 用 `key={`${provider}:api_key`}` 强制重挂载、读后端 ark.api_key —— 但此时 await setActive*Provider 还没跑,root.active.llm 还是旧值,lookup_account 落到旧 entry。把 await setActive 移到 setLlm/AsrProvider 之前修。 2) 覆盖 旧实现无条件 setCredential('ark.endpoint', preset.baseUrl),把用户在 该 provider 上之前自定义的 baseUrl 覆盖掉。改成「仅在 entry 该字段 为空」才填默认值,保留用户自定义。ASR 端的 endpoint + model 同款修。 不动 persistence schema、不动 IPC、不动 coordinator。 Closes #219 --- openless-all/app/src/pages/Settings.tsx | 28 ++++++++++++++++++------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/openless-all/app/src/pages/Settings.tsx b/openless-all/app/src/pages/Settings.tsx index a19b19fa..08828fd6 100644 --- a/openless-all/app/src/pages/Settings.tsx +++ b/openless-all/app/src/pages/Settings.tsx @@ -366,8 +366,13 @@ function ProvidersSection() { setAsrProvider(knownAsr ? knownAsr.id : 'volcengine'); }, [prefs]); + // issue #219:底层 providers HashMap 是 per-provider 的,切换 active 后 + // CredentialField 重挂载读 'ark.api_key' 会自动落到新 active 的 entry。 + // 所以**必须先 await setActive*Provider 再改前端 state**——顺序反了的话 + // CredentialField 读到的是旧 active 的 entry,用户看到「切换没生效」。 + // 默认 endpoint/model 改成「仅在新 active entry 该字段为空时」才填, + // 不再无条件覆盖用户自定义值。 const onLlmProviderChange = async (id: LlmPresetId) => { - setLlmProvider(id); await setActiveLlmProvider(id); if (prefs) { const next = { ...prefs, activeLlmProvider: id }; @@ -375,27 +380,34 @@ function ProvidersSection() { } const preset = LLM_PRESETS.find(p => p.id === id); if (preset?.baseUrl) { - await setCredential('ark.endpoint', preset.baseUrl); + const existing = await readCredential('ark.endpoint'); + if (!existing) await setCredential('ark.endpoint', preset.baseUrl); } + setLlmProvider(id); }; const onAsrProviderChange = async (id: AsrPresetId) => { - setAsrProvider(id); await setActiveAsrProvider(id); if (prefs) { const next = { ...prefs, activeAsrProvider: id }; await updatePrefs(next); } - // 切换到 OpenAI 兼容厂商时同步预填 endpoint + model:每家 base URL / 模型 ID - // 都是固定的,不预填用户必然踩坑。volcengine 走另一套凭据,跳过。 + // OpenAI 兼容厂商首次切换时预填 baseUrl / model 默认值,省得用户必踩 + // 「跨厂商 model 名根本不一样」的坑;但用户已自定义后就不再覆盖。 + // volcengine 走另一套凭据,跳过。 const preset = ASR_PRESETS.find(p => p.id === id); if (preset && preset.baseUrl) { - await setCredential('asr.endpoint', preset.baseUrl); + const existing = await readCredential('asr.endpoint'); + if (!existing) await setCredential('asr.endpoint', preset.baseUrl); } if (preset && preset.model) { - await setCredential('asr.model', preset.model); - setAsrModelRevision(v => v + 1); + const existing = await readCredential('asr.model'); + if (!existing) { + await setCredential('asr.model', preset.model); + setAsrModelRevision(v => v + 1); + } } + setAsrProvider(id); }; const preset = LLM_PRESETS.find(p => p.id === llmProvider) ?? LLM_PRESETS[LLM_PRESETS.length - 1]; From 1eec2fa21c12665917b797a3d610fd332c4fe5ae Mon Sep 17 00:00:00 2001 From: baiqing Date: Mon, 4 May 2026 09:04:41 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(settings):=20=E6=8B=86=20committed*Prov?= =?UTF-8?q?ider=20=E8=AE=A9=E5=8F=97=E6=8E=A7=20 的 value 现在依赖 IPC 完成顺序——用户连点两次时,先发的 请求晚到会用旧 id 覆盖最新的 setProvider,UI 卡在错误选项上。 修法(state 二分): - *Provider:立即跟随用户输入,驱动 立刻看到新选项(受控反馈正确) - CredentialField 等到后端 active 真切完才 remount 读 entry(issue #219 修复保留) - 100ms 内连点两次:只有最后那次的 commit 落地,UI 不会被旧请求覆盖 --- openless-all/app/src/pages/Settings.tsx | 87 +++++++++++++++++-------- 1 file changed, 61 insertions(+), 26 deletions(-) diff --git a/openless-all/app/src/pages/Settings.tsx b/openless-all/app/src/pages/Settings.tsx index 08828fd6..cb3d33d1 100644 --- a/openless-all/app/src/pages/Settings.tsx +++ b/openless-all/app/src/pages/Settings.tsx @@ -353,44 +353,71 @@ type AsrPresetId = typeof ASR_PRESETS[number]['id']; function ProvidersSection() { const { t } = useTranslation(); const { prefs, updatePrefs } = useHotkeySettings(); + // `*Provider` 立即跟随 立刻显示用户的选择(issue #220 P2:codex 指出受控选不应等 await) + // - CredentialField 不要在后端 active 切完前 remount(issue #219:避免读到旧 entry) + // `*SwitchSeq` 是 stale-write 守卫:用户 100ms 内连点两次时,先发的请求晚到不 + // 会覆盖后发的 commit。 const [llmProvider, setLlmProvider] = useState('ark'); const [asrProvider, setAsrProvider] = useState('volcengine'); + const [committedLlmProvider, setCommittedLlmProvider] = useState('ark'); + const [committedAsrProvider, setCommittedAsrProvider] = useState('volcengine'); + const llmSwitchSeqRef = useRef(0); + const asrSwitchSeqRef = useRef(0); const [llmModelRevision, setLlmModelRevision] = useState(0); const [asrModelRevision, setAsrModelRevision] = useState(0); useEffect(() => { if (!prefs) return; const knownLlm = LLM_PRESETS.find(x => x.id === prefs.activeLlmProvider); - setLlmProvider(knownLlm ? knownLlm.id : 'custom'); + const llmId = knownLlm ? knownLlm.id : 'custom'; + setLlmProvider(llmId); + setCommittedLlmProvider(llmId); const knownAsr = ASR_PRESETS.find(x => x.id === prefs.activeAsrProvider); - setAsrProvider(knownAsr ? knownAsr.id : 'volcengine'); + const asrId = knownAsr ? knownAsr.id : 'volcengine'; + setAsrProvider(asrId); + setCommittedAsrProvider(asrId); }, [prefs]); - // issue #219:底层 providers HashMap 是 per-provider 的,切换 active 后 - // CredentialField 重挂载读 'ark.api_key' 会自动落到新 active 的 entry。 - // 所以**必须先 await setActive*Provider 再改前端 state**——顺序反了的话 - // CredentialField 读到的是旧 active 的 entry,用户看到「切换没生效」。 - // 默认 endpoint/model 改成「仅在新 active entry 该字段为空时」才填, - // 不再无条件覆盖用户自定义值。 + // issue #219 / #220 P2: + // 1. 立刻 setLlmProvider —— 受控 立刻切到新厂商,但凭据字段还在显示旧 entry,placeholder + // 会先于实际数据切换、视觉上对不上。 + const preset = LLM_PRESETS.find(p => p.id === committedLlmProvider) ?? LLM_PRESETS[LLM_PRESETS.length - 1]; + const asrPreset = ASR_PRESETS.find(p => p.id === committedAsrProvider); return ( <> @@ -433,12 +468,12 @@ function ProvidersSection() { ))} - - + - - setLlmModelRevision(v => v + 1)} /> + setLlmModelRevision(v => v + 1)} /> @@ -457,24 +492,24 @@ function ProvidersSection() { ))} - {asrProvider === 'volcengine' ? ( + {committedAsrProvider === 'volcengine' ? ( <> ) : ( <> - - + - setAsrModelRevision(v => v + 1)} />