From 7dc1600c4c649cdfee74f795e3d78bcfa1e8e4c2 Mon Sep 17 00:00:00 2001 From: baiqing Date: Fri, 22 May 2026 21:09:11 +0800 Subject: [PATCH] =?UTF-8?q?fix(settings):=20=E5=A4=84=E7=90=86=20PR=20?= =?UTF-8?q?=E5=AE=A1=E6=A0=B8=E6=84=8F=E8=A7=81=20=E2=80=94=20=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=E5=A4=B1=E8=B4=A5=E6=96=87=E6=A1=88=20/=20=E5=88=87?= =?UTF-8?q?=E6=8D=A2=E5=9B=9E=E6=BB=9A=20/=20=E8=AE=A1=E6=97=B6=E5=99=A8?= =?UTF-8?q?=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ProvidersSection: 验证失败时显示 validateFailed 文案,不再对 result.ok=false 误报「连接检查通过」 - ProvidersSection: LLM/ASR 切换失败时下拉框回滚到 committed provider,避免 UI 停在新选项而后端仍是旧 provider;去掉 handler 末尾的 rethrow —— 作为 SelectLite onChange 是即发即忘调用,rethrow 会变成未处理的 promise rejection - CheckUpdateButton: 自动收起计时器只依赖 status,不再因 useAutoUpdate 每次渲染 返回新 updater 对象而被反复 clear/reset - 新增 i18n key settings.providers.validateFailed(zh-CN/zh-TW/en/ja/ko) --- openless-all/app/src/i18n/en.ts | 1 + openless-all/app/src/i18n/ja.ts | 1 + openless-all/app/src/i18n/ko.ts | 1 + openless-all/app/src/i18n/zh-CN.ts | 1 + openless-all/app/src/i18n/zh-TW.ts | 1 + .../src/pages/settings/CheckUpdateButton.tsx | 5 +++- .../src/pages/settings/ProvidersSection.tsx | 23 ++++++++++++------- 7 files changed, 24 insertions(+), 9 deletions(-) diff --git a/openless-all/app/src/i18n/en.ts b/openless-all/app/src/i18n/en.ts index 1e360d9c..29724430 100644 --- a/openless-all/app/src/i18n/en.ts +++ b/openless-all/app/src/i18n/en.ts @@ -660,6 +660,7 @@ export const en: typeof zhCN = { selectModel: 'Select a model to fill the field above', modelSaved: 'Saved model {{model}}.', validateSuccess: 'Connection check passed.', + validateFailed: 'Connection check failed.', providerHttpStatus: 'Provider returned HTTP {{status}}. Check the API key permissions or endpoint.', endpointMustUseHttps: 'Endpoint must use HTTPS (localhost/127.0.0.1 are allowed for local testing).', endpointInvalid: 'Endpoint format is invalid.', diff --git a/openless-all/app/src/i18n/ja.ts b/openless-all/app/src/i18n/ja.ts index 3aeef9a2..f4b96286 100644 --- a/openless-all/app/src/i18n/ja.ts +++ b/openless-all/app/src/i18n/ja.ts @@ -662,6 +662,7 @@ export const ja: typeof zhCN = { selectModel: 'モデルを選んで上記欄に入力', modelSaved: 'モデル {{model}} を保存しました。', validateSuccess: '接続チェックに合格しました。', + validateFailed: '接続チェックに失敗しました。', providerHttpStatus: 'サプライヤーが {{status}} を返しました。API Key 権限またはエンドポイントを確認してください。', endpointMustUseHttps: 'Endpoint は HTTPS を使用する必要があります(localhost/127.0.0.1 を除く)。', endpointInvalid: 'Endpoint の形式が無効です。', diff --git a/openless-all/app/src/i18n/ko.ts b/openless-all/app/src/i18n/ko.ts index fa026f47..a14300c5 100644 --- a/openless-all/app/src/i18n/ko.ts +++ b/openless-all/app/src/i18n/ko.ts @@ -662,6 +662,7 @@ export const ko: typeof zhCN = { selectModel: '모델을 선택해 위 필드에 입력', modelSaved: '모델 {{model}} 을(를) 저장했습니다.', validateSuccess: '연결 확인을 통과했습니다.', + validateFailed: '연결 확인에 실패했습니다.', providerHttpStatus: '공급자가 {{status}} 를 반환했습니다. API Key 권한 또는 Endpoint 를 확인해 주세요.', endpointMustUseHttps: 'Endpoint 는 HTTPS 를 사용해야 합니다(localhost/127.0.0.1 제외).', endpointInvalid: 'Endpoint 형식이 올바르지 않습니다.', diff --git a/openless-all/app/src/i18n/zh-CN.ts b/openless-all/app/src/i18n/zh-CN.ts index e984cdba..36b18b45 100644 --- a/openless-all/app/src/i18n/zh-CN.ts +++ b/openless-all/app/src/i18n/zh-CN.ts @@ -658,6 +658,7 @@ export const zhCN = { selectModel: '选择一个模型写入上方字段', modelSaved: '已保存模型 {{model}}。', validateSuccess: '连接检查通过。', + validateFailed: '连接检查未通过。', providerHttpStatus: '供应商接口返回 {{status}},请检查 API Key 权限或 Endpoint。', endpointMustUseHttps: 'Endpoint 必须使用 HTTPS(本地 localhost/127.0.0.1 测试除外)。', endpointInvalid: 'Endpoint 格式不合法。', diff --git a/openless-all/app/src/i18n/zh-TW.ts b/openless-all/app/src/i18n/zh-TW.ts index e26b7dfe..0accec1d 100644 --- a/openless-all/app/src/i18n/zh-TW.ts +++ b/openless-all/app/src/i18n/zh-TW.ts @@ -660,6 +660,7 @@ export const zhTW: typeof zhCN = { selectModel: '選擇一個模型寫入上方字段', modelSaved: '已保存模型 {{model}}。', validateSuccess: '連接檢查通過。', + validateFailed: '連接檢查未通過。', providerHttpStatus: '供應商接口返回 {{status}},請檢查 API Key 權限或 Endpoint。', endpointMustUseHttps: 'Endpoint 必須使用 HTTPS(本地 localhost/127.0.0.1 測試除外)。', endpointInvalid: 'Endpoint 格式不合法。', diff --git a/openless-all/app/src/pages/settings/CheckUpdateButton.tsx b/openless-all/app/src/pages/settings/CheckUpdateButton.tsx index d6c70397..23ab8ec5 100644 --- a/openless-all/app/src/pages/settings/CheckUpdateButton.tsx +++ b/openless-all/app/src/pages/settings/CheckUpdateButton.tsx @@ -22,7 +22,10 @@ export function CheckUpdateButton({ channel }: { channel: UpdateChannel }) { return () => window.clearTimeout(id); } return undefined; - }, [status, updater]); + // 只按 status 触发:useAutoUpdate 每次渲染都返回新 updater 对象,把它放进 + // 依赖会让父组件每次重渲染都把 2.5s 自动收起计时器清掉重置。 + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [status]); const upToDate = status === 'none'; const failed = status === 'error'; diff --git a/openless-all/app/src/pages/settings/ProvidersSection.tsx b/openless-all/app/src/pages/settings/ProvidersSection.tsx index 8f3ae3aa..37002626 100644 --- a/openless-all/app/src/pages/settings/ProvidersSection.tsx +++ b/openless-all/app/src/pages/settings/ProvidersSection.tsx @@ -224,13 +224,17 @@ export function ProvidersSection() { setCommittedLlmProvider(id); emitSaved('saved', t('common.saved')); } catch (err) { - // seq 守卫:只有当前 call 还是最新时才把 saving 翻成 failed; - // 旧 call 早被 newer call 的 emitSaved('saving') 覆盖,再叠 failed 会 - // 把 newer 正在跑的 saving 假伪成失败。 + // seq 守卫:只有当前 call 还是最新时才翻 failed + 回滚下拉框;旧 call 早被 + // newer call 的 emitSaved('saving') 覆盖,不要插手。 if (seq === llmSwitchSeqRef.current) { emitSaved('failed', t('common.operationFailed')); + // 后端切换失败 → 下拉框退回真实生效的 committed provider,免得 UI 停在 + // 新选项、实际 provider 仍是旧的,造成显示与后端不一致。 + setLlmProvider(committedLlmProvider); } - throw err; + // 不再 rethrow:本 handler 作为 SelectLite onChange 是即发即忘调用, + // rethrow 会变成未处理的 promise rejection。错误已 emitSaved + 记日志。 + console.error('[settings] switch LLM provider failed', err); } }; @@ -277,12 +281,12 @@ export function ProvidersSection() { setCommittedAsrProvider(id); emitSaved('saved', t('common.saved')); } catch (err) { - // seq 守卫同上 onLlmProviderChange:旧 call 不要把 newer call 的 saving - // 伪造成 failed。 + // seq 守卫 + 回滚 + 不 rethrow,同 onLlmProviderChange。 if (seq === asrSwitchSeqRef.current) { emitSaved('failed', t('common.operationFailed')); + setAsrProvider(committedAsrProvider); } - throw err; + console.error('[settings] switch ASR provider failed', err); } }; @@ -473,7 +477,10 @@ function ProviderTools({ kind, modelAccount, onModelSelected }: { kind: 'llm' | setResult('loading', t('settings.providers.validating')); try { const result = await validateProviderCredentials(kind); - setResult(result.ok ? 'success' : 'error', t('settings.providers.validateSuccess')); + setResult( + result.ok ? 'success' : 'error', + t(result.ok ? 'settings.providers.validateSuccess' : 'settings.providers.validateFailed'), + ); } catch (error) { const message = error instanceof Error ? error.message : String(error); if ((kind === 'llm' && message === 'llmModelMissing') || (kind === 'asr' && message === 'asrModelMissing')) {