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')) {