diff --git a/openless-all/app/src-tauri/capabilities/default.json b/openless-all/app/src-tauri/capabilities/default.json index b04dca4a..e629bf7b 100644 --- a/openless-all/app/src-tauri/capabilities/default.json +++ b/openless-all/app/src-tauri/capabilities/default.json @@ -17,6 +17,7 @@ "core:event:default", "shell:allow-open", "updater:default", - "autostart:default" + "autostart:default", + "dialog:default" ] } diff --git a/openless-all/app/src/i18n/en.ts b/openless-all/app/src/i18n/en.ts index bf1a4631..96037628 100644 --- a/openless-all/app/src/i18n/en.ts +++ b/openless-all/app/src/i18n/en.ts @@ -384,6 +384,7 @@ export const en: typeof zhCN = { volcengineResourceIdLabel: 'Resource ID', volcengineMappingNote: 'Secret Key is not required right now. Resource ID defaults to volc.bigasr.sauc.duration.', localAsrActiveNotice: 'Local ASR ({{name}}) is currently active. Switch or disable it from the Advanced tab.', + localAsrTakeoverHint: 'Once "{{name}}" is enabled, the ASR provider will be taken over.', localAsrHint: 'Local Qwen3-ASR runs entirely on this machine. No API key needed — just download the model from HuggingFace.', foundryLocalAsrHint: 'Windows local Whisper runs on this device and does not need an ASR API key. First use downloads Foundry Local runtime components and a Whisper model; LLM polishing still uses your configured LLM provider.', localAsrPerformanceWarning: 'Local inference runs on CPU + Apple Silicon Accelerate; each transcription takes **several seconds longer than cloud ASR**, and Chinese / dialect accuracy is **typically lower** than Volcengine or Whisper turbo. Use it for offline, privacy-sensitive, or no-cloud-API scenarios.', @@ -475,9 +476,10 @@ export const en: typeof zhCN = { advanced: { localAsrTitle: 'Local ASR models (experimental)', localAsrDesc: 'Move transcription from cloud ASR to on-device inference. Offline / privacy-sensitive use only.', - localAsrWarningShort: 'Local ASR is several seconds slower than cloud, typically less accurate, and on under-spec hardware can drop words (output only part of your speech).', - qwen3Desc: 'Alibaba Qwen3-ASR, cross-platform. Models download from HuggingFace.', - foundryDesc: 'Microsoft Foundry Local Whisper. Windows only.', + localAsrWarningShort: 'Local inference is slower; under-spec hardware may drop words.', + qwen3Desc: 'Once enabled, the ASR provider will be taken over.', + foundryDesc: 'Once enabled, the ASR provider will be taken over.', + notSupportedHere: 'Not supported on this platform — no inference module bundled.', enable: 'Enable', alreadyActive: 'Active', disableLocalLabel: 'Disable local ASR', diff --git a/openless-all/app/src/i18n/ja.ts b/openless-all/app/src/i18n/ja.ts index 45c2836a..f8068a67 100644 --- a/openless-all/app/src/i18n/ja.ts +++ b/openless-all/app/src/i18n/ja.ts @@ -386,6 +386,7 @@ export const ja: typeof zhCN = { volcengineResourceIdLabel: 'Resource ID', volcengineMappingNote: 'Secret Key は現在不要。Resource ID のデフォルトは volc.bigasr.sauc.duration。', localAsrActiveNotice: '現在「{{name}}」を使用中。「詳細設定」タブから切り替えまたは無効化できます。', + localAsrTakeoverHint: '「{{name}}」を有効化すると ASR プロバイダーが引き継がれます。', localAsrHint: 'ローカル Qwen3-ASR は本機で実行されるため API Key 不要。HuggingFace からモデルをダウンロードすればすぐに利用できます。', foundryLocalAsrHint: 'Windows ローカル Whisper は本機で実行され、ASR API Key は不要です。初回使用時に Foundry Local ランタイムコンポーネントと Whisper モデルをダウンロードします。LLM 整文は引き続き設定済みの LLM プロバイダーを使用します。', localAsrPerformanceWarning: 'ローカル推論は CPU + Apple Silicon Accelerate で動作するため、1 回の転写時間は **クラウド ASR より数秒長くなります**。中国語認識精度や方言/訛り対応も **通常は** Volcengine / Whisper turbo に劣ります。ネットワーク制限下またはプライバシー重視の場合に選択してください。', @@ -477,9 +478,10 @@ export const ja: typeof zhCN = { advanced: { localAsrTitle: 'ローカル ASR モデル(実験的)', localAsrDesc: '転写をクラウドから本機推論に切り替えます。オフライン/プライバシー重視向け。', - localAsrWarningShort: 'ローカル ASR はクラウドより数秒遅く、精度も通常は低め。スペック不足のマシンでは欠字(音声の一部のみ出力)が起きる場合があります。', - qwen3Desc: 'Alibaba Qwen3-ASR、クロスプラットフォーム。モデルは HuggingFace からダウンロード。', - foundryDesc: 'Microsoft Foundry Local Whisper、Windows のみ。', + localAsrWarningShort: 'ローカル推論は遅く、スペック不足では欠字の可能性があります。', + qwen3Desc: '有効化すると ASR プロバイダーが引き継がれます。', + foundryDesc: '有効化すると ASR プロバイダーが引き継がれます。', + notSupportedHere: 'このプラットフォームでは未対応(推論モジュール未組込)。', enable: '有効化', alreadyActive: '有効', disableLocalLabel: 'ローカル ASR を無効化', diff --git a/openless-all/app/src/i18n/ko.ts b/openless-all/app/src/i18n/ko.ts index 1fbf83a5..e5c6da54 100644 --- a/openless-all/app/src/i18n/ko.ts +++ b/openless-all/app/src/i18n/ko.ts @@ -386,6 +386,7 @@ export const ko: typeof zhCN = { volcengineResourceIdLabel: 'Resource ID', volcengineMappingNote: 'Secret Key 는 현재 입력 불필요. Resource ID 기본값은 volc.bigasr.sauc.duration.', localAsrActiveNotice: '현재 "{{name}}" 사용 중. "고급" 탭에서 전환 또는 비활성화할 수 있습니다.', + localAsrTakeoverHint: '"{{name}}" 활성화 시 ASR 프로바이더가 인수됩니다.', localAsrHint: '로컬 Qwen3-ASR 은 본 기기에서 실행되며 API Key 가 필요 없습니다. HuggingFace 에서 모델을 로컬로 다운로드하면 즉시 사용 가능합니다.', foundryLocalAsrHint: 'Windows 로컬 Whisper 는 이 기기에서 실행되며 ASR API Key 가 필요 없습니다. 첫 사용 시 Foundry Local 런타임 구성 요소와 Whisper 모델을 다운로드합니다. LLM 정리는 계속 설정된 LLM 공급자를 사용합니다.', localAsrPerformanceWarning: '로컬 추론은 CPU + Apple Silicon Accelerate 에서 동작하므로, 한 번의 전사 시간이 **클라우드 ASR 보다 몇 초 더 걸립니다**. 중국어 인식 정확도와 방언/억양 대응도 **일반적으로** Volcengine / Whisper turbo 에 미치지 못합니다. 네트워크 제한 또는 프라이버시가 중요한 경우에 사용하세요.', @@ -477,9 +478,10 @@ export const ko: typeof zhCN = { advanced: { localAsrTitle: '로컬 ASR 모델 (실험적)', localAsrDesc: '전사를 클라우드에서 로컬 추론으로 전환합니다. 오프라인 / 프라이버시용에만 권장됩니다.', - localAsrWarningShort: '로컬 ASR 은 클라우드보다 몇 초 느리고 정확도도 일반적으로 낮습니다. 사양이 부족한 기기에서는 글자 누락(음성의 일부만 출력)이 발생할 수 있습니다.', - qwen3Desc: 'Alibaba Qwen3-ASR, 크로스 플랫폼. 모델은 HuggingFace 에서 다운로드.', - foundryDesc: 'Microsoft Foundry Local Whisper, Windows 전용.', + localAsrWarningShort: '로컬 추론은 느리며, 사양 부족 시 글자 누락이 발생할 수 있습니다.', + qwen3Desc: '활성화하면 ASR 프로바이더가 인수됩니다.', + foundryDesc: '활성화하면 ASR 프로바이더가 인수됩니다.', + notSupportedHere: '이 플랫폼에서는 미지원 (추론 모듈 미내장).', enable: '활성화', alreadyActive: '활성', disableLocalLabel: '로컬 ASR 비활성화', diff --git a/openless-all/app/src/i18n/zh-CN.ts b/openless-all/app/src/i18n/zh-CN.ts index d9da9665..0079272c 100644 --- a/openless-all/app/src/i18n/zh-CN.ts +++ b/openless-all/app/src/i18n/zh-CN.ts @@ -382,6 +382,7 @@ export const zhCN = { volcengineResourceIdLabel: 'Resource ID', volcengineMappingNote: 'Secret Key 当前无需填写。Resource ID 默认使用 volc.bigasr.sauc.duration。', localAsrActiveNotice: '当前已启用「{{name}}」,可在「高级」中切换或禁用。', + localAsrTakeoverHint: '启动「{{name}}」后,ASR 提供商将被接管。', localAsrHint: '本地 Qwen3-ASR 在本机运行,无需 API Key。模型从 HuggingFace 下载到本地后即可使用。', foundryLocalAsrHint: 'Windows 本地 Whisper 在本机运行,无需 ASR API Key。首次使用会下载 Foundry Local 运行组件和 Whisper 模型;LLM 润色仍按你配置的 LLM 提供商调用。', localAsrPerformanceWarning: '本地推理跑在 CPU + Apple Silicon Accelerate 上,单次转写时间会**比云端 ASR 长几秒**;中文识别准确率与方言/口音表现也**通常不如**火山引擎 / Whisper turbo。请按需取舍:网络受限或对隐私敏感时再用本地。', @@ -473,9 +474,10 @@ export const zhCN = { advanced: { localAsrTitle: '本地 ASR 模型(实验性)', localAsrDesc: '把转写从云端切到本机推理。仅推荐离线 / 隐私敏感场景。', - localAsrWarningShort: '本地 ASR 比云端慢若干秒、准确率更低;配置不足时可能吞字(仅输出部分语音)。', - qwen3Desc: 'Alibaba Qwen3-ASR,跨平台。模型从 HuggingFace 下载。', - foundryDesc: 'Microsoft Foundry Local Whisper,仅 Windows。', + localAsrWarningShort: '本地推理较慢,配置不足时可能吞字。', + qwen3Desc: '启动之后,ASR 提供商将被接管。', + foundryDesc: '启动之后,ASR 提供商将被接管。', + notSupportedHere: '本平台暂不支持,未集成推理模块。', enable: '启用', alreadyActive: '已启用', disableLocalLabel: '禁用本地 ASR', diff --git a/openless-all/app/src/i18n/zh-TW.ts b/openless-all/app/src/i18n/zh-TW.ts index d9417145..30327793 100644 --- a/openless-all/app/src/i18n/zh-TW.ts +++ b/openless-all/app/src/i18n/zh-TW.ts @@ -384,6 +384,7 @@ export const zhTW: typeof zhCN = { volcengineResourceIdLabel: 'Resource ID', volcengineMappingNote: 'Secret Key 當前無需填寫。Resource ID 默認使用 volc.bigasr.sauc.duration。', localAsrActiveNotice: '當前已啓用「{{name}}」,可在「高級」中切換或停用。', + localAsrTakeoverHint: '啓動「{{name}}」後,ASR 提供商將被接管。', localAsrHint: '本地 Qwen3-ASR 在本機運行,無需 API Key。模型從 HuggingFace 下載到本地後即可使用。', foundryLocalAsrHint: 'Windows 本地 Whisper 在本機運行,無需 ASR API Key。首次使用會下載 Foundry Local 運行組件和 Whisper 模型;LLM 潤色仍按你配置的 LLM 提供商調用。', localAsrPerformanceWarning: '本地推理跑在 CPU + Apple Silicon Accelerate 上,**首次轉寫需要加載模型(數秒)**,之後單次轉寫也會比雲端 ASR 慢若干秒;中文識別準確率與方言/口音表現通常不如火山引擎 / Whisper turbo。適用場景:離線 / 隱私敏感 / 不願付費雲 API。', @@ -475,9 +476,10 @@ export const zhTW: typeof zhCN = { advanced: { localAsrTitle: '本地 ASR 模型(實驗性)', localAsrDesc: '把轉寫從雲端切到本機推理。僅推薦離線 / 隱私敏感場景。', - localAsrWarningShort: '本地 ASR 比雲端慢若干秒、準確率更低;配置不足時可能吞字(僅輸出部分語音)。', - qwen3Desc: 'Alibaba Qwen3-ASR,跨平臺。模型從 HuggingFace 下載。', - foundryDesc: 'Microsoft Foundry Local Whisper,僅 Windows。', + localAsrWarningShort: '本地推理較慢,配置不足時可能吞字。', + qwen3Desc: '啓動之後,ASR 提供商將被接管。', + foundryDesc: '啓動之後,ASR 提供商將被接管。', + notSupportedHere: '本平臺暫不支持,未集成推理模塊。', enable: '啓用', alreadyActive: '已啓用', disableLocalLabel: '停用本地 ASR', diff --git a/openless-all/app/src/pages/LocalAsr.tsx b/openless-all/app/src/pages/LocalAsr.tsx index 135a94a4..aaf12e00 100644 --- a/openless-all/app/src/pages/LocalAsr.tsx +++ b/openless-all/app/src/pages/LocalAsr.tsx @@ -52,7 +52,13 @@ import { Btn, Card, PageHeader, Pill } from './_atoms'; // Foundry Local Whisper 后端只在 Windows 编译实体(foundry_local_sdk 仅 Windows), // 非 Windows 平台 runtime 是 stub 永远 unavailable。前端这一页对应的卡片、状态拉取、 // 事件订阅都必须按 OS 隔离,避免 macOS / Linux 用户看到 Windows 专属的 UI。 +// +// 同理 Qwen3-ASR 后端只在 macOS 编译实体(qwen_engine / cache / local_provider 全是 +// `#[cfg(target_os = "macos")]`),Qwen3 模型管理 UI 也按 IS_MAC 守严——之前用 +// `!IS_WINDOWS` 会让假设的 Linux 渲染路径暴露死 UI(pr_agent #403 'Linux regression' +// 修法)。 const IS_WINDOWS = detectOS() === 'win'; +const IS_MAC = detectOS() === 'mac'; interface RemoteSize { totalBytes: number; @@ -772,6 +778,11 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) { )} + {/* Qwen3 模型管理区——只在 macOS 渲染(后端 #[cfg(target_os = "macos")] 独占)。 + Windows / Linux 看见镜像源 / 下载 / 模型列表都是 dead UI。Foundry 块自身已经 + 被上方 IS_WINDOWS 守卫,错误 Card(共享 setError,被 Foundry handler 也写) + 保持无条件露出。 */} + {IS_MAC && (<> {!engineAvailable && (
@@ -870,6 +881,7 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) {
)} + )} {error && ( @@ -877,6 +889,7 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) { )} + {IS_MAC && (
{models.map(model => ( ))}
+ )} ); } diff --git a/openless-all/app/src/pages/Settings.tsx b/openless-all/app/src/pages/Settings.tsx index 89124acb..c1b4dde5 100644 --- a/openless-all/app/src/pages/Settings.tsx +++ b/openless-all/app/src/pages/Settings.tsx @@ -1288,63 +1288,86 @@ function ProvidersSection() { const onLlmProviderChange = async (id: LlmPresetId) => { setLlmProvider(id); const seq = ++llmSwitchSeqRef.current; - await setActiveLlmProvider(id); - if (seq !== llmSwitchSeqRef.current) return; - if (prefs) { - const next = { ...prefs, activeLlmProvider: id }; - await updatePrefs(next); + emitSaved('saving', t('common.saving')); + try { + await setActiveLlmProvider(id); if (seq !== llmSwitchSeqRef.current) return; - } - const preset = LLM_PRESETS.find(p => p.id === id); - // 修 bug:所有 LLM provider 共用 `ark.endpoint` / `ark.model_id` 一对凭据槽 - // (persistence.rs 没做 per-provider 隔离)。旧逻辑只在槽空时填默认值, - // 老用户切换 preset 时槽里早有旧值——dropdown 看着切了,polish 实际还是 - // 打老 endpoint。改成:切到任何非 custom 预设都强制覆盖 endpoint 与 model - // 到该预设的默认值,让"切换"真切到位。custom 预设没有默认值,跳过。 - if (preset && preset.id !== 'custom') { - if (preset.baseUrl) { - await setCredential('ark.endpoint', preset.baseUrl); + if (prefs) { + const next = { ...prefs, activeLlmProvider: id }; + await updatePrefs(next); if (seq !== llmSwitchSeqRef.current) return; } - if (preset.modelPlaceholder) { - await setCredential('ark.model_id', preset.modelPlaceholder); - if (seq !== llmSwitchSeqRef.current) return; + const preset = LLM_PRESETS.find(p => p.id === id); + // 修 bug:所有 LLM provider 共用 `ark.endpoint` / `ark.model_id` 一对凭据槽 + // (persistence.rs 没做 per-provider 隔离)。旧逻辑只在槽空时填默认值, + // 老用户切换 preset 时槽里早有旧值——dropdown 看着切了,polish 实际还是 + // 打老 endpoint。改成:切到任何非 custom 预设都强制覆盖 endpoint 与 model + // 到该预设的默认值,让"切换"真切到位。custom 预设没有默认值,跳过。 + if (preset && preset.id !== 'custom') { + if (preset.baseUrl) { + await setCredential('ark.endpoint', preset.baseUrl); + if (seq !== llmSwitchSeqRef.current) return; + } + if (preset.modelPlaceholder) { + await setCredential('ark.model_id', preset.modelPlaceholder); + if (seq !== llmSwitchSeqRef.current) return; + } + } + setCommittedLlmProvider(id); + emitSaved('saved', t('common.saved')); + } catch (err) { + // seq 守卫:只有当前 call 还是最新时才把 saving 翻成 failed; + // 旧 call 早被 newer call 的 emitSaved('saving') 覆盖,再叠 failed 会 + // 把 newer 正在跑的 saving 假伪成失败。 + if (seq === llmSwitchSeqRef.current) { + emitSaved('failed', t('common.operationFailed')); } + throw err; } - setCommittedLlmProvider(id); }; const onAsrProviderChange = async (id: AsrPresetId) => { setAsrProvider(id); const seq = ++asrSwitchSeqRef.current; - await setActiveAsrProvider(id); - if (seq !== asrSwitchSeqRef.current) return; - if (prefs) { - const next = { ...prefs, activeAsrProvider: id }; - await updatePrefs(next); - if (seq !== asrSwitchSeqRef.current) return; - } - // OpenAI 兼容厂商首次切换时预填 baseUrl / model 默认值,省得用户必踩 - // 「跨厂商 model 名根本不一样」的坑;但用户已自定义后就不再覆盖。 - // volcengine 走另一套凭据,跳过。 - const preset = ASR_PRESETS.find(p => p.id === id); - if (preset && preset.baseUrl) { - const existing = await readCredential('asr.endpoint'); + emitSaved('saving', t('common.saving')); + try { + await setActiveAsrProvider(id); if (seq !== asrSwitchSeqRef.current) return; - if (!existing) { - await setCredential('asr.endpoint', preset.baseUrl); + if (prefs) { + const next = { ...prefs, activeAsrProvider: id }; + await updatePrefs(next); if (seq !== asrSwitchSeqRef.current) return; } - } - if (preset && preset.model) { - const existing = await readCredential('asr.model'); - if (seq !== asrSwitchSeqRef.current) return; - if (!existing) { - await setCredential('asr.model', preset.model); + // OpenAI 兼容厂商首次切换时预填 baseUrl / model 默认值,省得用户必踩 + // 「跨厂商 model 名根本不一样」的坑;但用户已自定义后就不再覆盖。 + // volcengine 走另一套凭据,跳过。 + const preset = ASR_PRESETS.find(p => p.id === id); + if (preset && preset.baseUrl) { + const existing = await readCredential('asr.endpoint'); if (seq !== asrSwitchSeqRef.current) return; + if (!existing) { + await setCredential('asr.endpoint', preset.baseUrl); + if (seq !== asrSwitchSeqRef.current) return; + } } + if (preset && preset.model) { + const existing = await readCredential('asr.model'); + if (seq !== asrSwitchSeqRef.current) return; + if (!existing) { + await setCredential('asr.model', preset.model); + if (seq !== asrSwitchSeqRef.current) return; + } + } + setCommittedAsrProvider(id); + emitSaved('saved', t('common.saved')); + } catch (err) { + // seq 守卫同上 onLlmProviderChange:旧 call 不要把 newer call 的 saving + // 伪造成 failed。 + if (seq === asrSwitchSeqRef.current) { + emitSaved('failed', t('common.operationFailed')); + } + throw err; } - setCommittedAsrProvider(id); }; // preset 决定 placeholder 与 default —— 必须跟着 committed*Provider 走, @@ -1365,7 +1388,9 @@ function ProvidersSection() { {t('settings.providers.llmDesc')} - + {/* desc 已去掉——'选择后将自动填入 Base URL 默认值' 在 180px label 列必换行成两行, + 视觉上 label 区出现"字体单独占一行"。下拉自身已经表达了"切换"含义,desc 冗余。 */} + 强行渲染会回退到第一项制造视觉假象。 - // 直接换成纯文本 notice,告诉用户"正在用本地 ASR,去高级中切换/禁用"。 -
- {t('settings.providers.localAsrActiveNotice', { - name: t(`settings.providers.presets.${committedAsrProvider === 'local-qwen3' ? 'asrLocalQwen3' : 'asrFoundryLocalWhisper'}`), - })} -
- ) : ( - - )} + {/* desc 同 LLM 行去掉,避免 180px label 列里换行。 */} + + {(() => { + // 平台本机引擎:macOS=Qwen3,Windows=Foundry。本机项始终在下拉里露出, + // 当用户未启用本地推理时呈 disabled——用户能看到"它存在 + 提示", + // 但只能在 Advanced 启用;启用后整下拉锁住,**实际激活的** local 项被选中 + // (pr_agent #403 fix:跨机器同步导致 macOS profile = foundry / Windows + // profile = qwen3 时,UI 不能假装是平台本机引擎而要忠实显示)。 + const platformLocalAsr: AsrPresetId | null = + os === 'mac' ? 'local-qwen3' : os === 'win' ? 'foundry-local-whisper' : null; + const platformLocalNameKey = platformLocalAsr === 'local-qwen3' + ? 'asrLocalQwen3' + : platformLocalAsr === 'foundry-local-whisper' + ? 'asrFoundryLocalWhisper' + : null; + const isLocked = committedAsrProvider === 'local-qwen3' || committedAsrProvider === 'foundry-local-whisper'; + // active 是本地时,selectedValue = 实际 committed 的 provider(不强制 + // 覆盖成 platformLocalAsr);非 active 时按 asrProvider(云端选项)显示。 + const selectedValue: AsrPresetId = isLocked ? committedAsrProvider : asrProvider; + // 异常 local:当前激活的 local 不是本平台本机引擎(跨机器配置同步)。 + // 必须把它也作为 disabled + ))} + {platformLocalAsr && platformLocalNameKey && ( + + )} + {anomalousLocal && anomalousNameKey && ( + + )} + + {platformLocalAsr && platformLocalNameKey && ( +
+ {t('settings.providers.localAsrTakeoverHint', { + name: t(`settings.providers.presets.${platformLocalNameKey}`), + })} +
+ )} + + ); + })()}
{committedAsrProvider === 'volcengine' ? ( <> @@ -1477,24 +1546,25 @@ function ProvidersSection() { /// 设计: /// - 主 Providers 下拉只列云端选项;本地推理 (local-qwen3 / foundry-local-whisper) /// 完全收纳到此栏,新手不会在主流程里误开 CPU 推理。 -/// - 启用本地推理时弹出**页面顶部**的 React 浮层 popup(不再用 Tauri 系统对话框, -/// 也不再常驻一段长警告文本)——一个框、一段短话、确认/取消。 +/// - 启用本地推理时弹出**屏幕中央**的 modal(背景模糊)——一个框、一段短话、确认/取消。 /// - 旧的「模型设置」独立页 (LocalAsr.tsx 作为 nav tab) 已下线,模型管理 UI /// 通过 内嵌在此处统一管理。 /// -/// 平台可见性: -/// - macOS: 仅 Qwen3-ASR -/// - Windows: Qwen3-ASR + Foundry Local Whisper -/// - Linux: 「该平台暂未支持本地 ASR 模型集成」 +/// 平台对称(每端只露本机有后端的引擎,另一端的引擎以 disabled 行 + "本平台暂不支持"提示露出): +/// - macOS: Qwen3-ASR 主行可启用;Foundry **不显示**(macOS 不展示 Windows 端模型内容)。 +/// - Windows: Foundry 主行可启用;Qwen3 行 disabled,desc 为 notSupportedHere。 +/// - Linux: 「该平台暂未支持本地 ASR 模型集成」整段兜底。 function AdvancedSection() { const { t } = useTranslation(); const { prefs, updatePrefs } = useHotkeySettings(); const os = detectOS(); - const platformSupported = os === 'mac' || os === 'win'; + const isMac = os === 'mac'; + const isWin = os === 'win'; + const platformSupported = isMac || isWin; const switchSeqRef = useRef(0); const [busy, setBusy] = useState(false); - // 待确认的启用目标。!== null 时浮层 popup 显示在页面顶部;用户点确认 → 真切; - // 点取消 → 回到 null。一次只允许一个 popup(用户的"只一个框"硬要求)。 + // 待确认的启用目标。!== null 时中央 modal 弹出 + 背景模糊;用户点确认 → 真切; + // 点取消 → 回到 null。一次只允许一个 modal。 const [pendingTarget, setPendingTarget] = useState(null); const activeAsrProvider = (prefs?.activeAsrProvider ?? 'volcengine') as AsrPresetId; @@ -1530,60 +1600,79 @@ function AdvancedSection() { return ( <> - {/* ─── 页面顶部确认浮层 ──────────────────────────────────────────── - 用户硬要求:启用本地推理时**只**弹一个框、直接出现在页面最上方。 - 琥珀边框 + 琥珀字醒目;不阻塞滚动;点取消返回不切。 */} + {/* ─── 屏幕中央确认 modal(背景模糊) ───────────────────────────── + 点击遮罩或取消按钮关闭;切换中(busy)禁止任何关闭路径以免半切失败。 */} {pendingTarget && pendingNameKey && ( - { + if (e.target === e.currentTarget && !busy) setPendingTarget(null); }}> -
- ⚠️ {t('settings.advanced.confirmEnableLocalTitle')} -
-
- {t('settings.advanced.confirmEnableLocalBody', { - target: t(`settings.providers.presets.${pendingNameKey}`), - })} -
-
- void performSwitch(pendingTarget)}> - {t('settings.advanced.confirm')} - - setPendingTarget(null)}> - {t('common.cancel')} - -
-
+ +
+ ⚠️ {t('settings.advanced.confirmEnableLocalTitle')} +
+
+ {t('settings.advanced.confirmEnableLocalBody', { + target: t(`settings.providers.presets.${pendingNameKey}`), + })} +
+
+ setPendingTarget(null)}> + {t('common.cancel')} + + void performSwitch(pendingTarget)}> + {t('settings.advanced.confirm')} + +
+
+ )} -
-
{t('settings.advanced.localAsrTitle')}
-
- {t('settings.advanced.localAsrDesc')} + {/* 标题 + 右上角 inline 警告小字(替换原琥珀大警告条)。 */} +
+
+
{t('settings.advanced.localAsrTitle')}
+
+ {t('settings.advanced.localAsrDesc')} +
-
- - {/* 常驻短警告——一行字够了,详细解释留给点 Enable 后的页面顶部 popup。 */} -
- ⚠️ {t('settings.advanced.localAsrWarningShort')} + ⚠️ {t('settings.advanced.localAsrWarningShort')} +
{!platformSupported ? ( @@ -1592,50 +1681,62 @@ function AdvancedSection() {
) : ( <> + {/* Qwen3 行 —— macOS Toggle 可点切换;Windows 后端是 stub,Toggle 始终 off + + 不可点 + desc=notSupportedHere,跟"本平台不可用"视觉一致。跨平台 + 异常(Windows profile 同步到 local-qwen3)时 active 状态靠下方独立 + "禁用本地 ASR" 行兜底,避免 Toggle ON + desc 说不支持的自相矛盾感 + (pr_agent #403 'Stale Windows state' 修法)。 */} - requestEnable('local-qwen3')}> - {isOnLocalQwen3 ? t('settings.advanced.alreadyActive') : t('settings.advanced.enable')} - + desc={isMac ? t('settings.advanced.qwen3Desc') : t('settings.advanced.notSupportedHere')}> +
+ { + if (next) requestEnable('local-qwen3'); + else void performSwitch('volcengine'); + } : undefined} + /> +
- {os === 'win' && ( + {/* Foundry 行 —— 仅 Windows 露出(macOS 不展示 Windows 端模型内容)。 */} + {isWin && ( - requestEnable('foundry-local-whisper')}> - {isOnFoundry ? t('settings.advanced.alreadyActive') : t('settings.advanced.enable')} - +
+ { + if (next) requestEnable('foundry-local-whisper'); + else void performSwitch('volcengine'); + } : undefined} + /> +
)} )} - {/* 「禁用本地 ASR」按钮放在 platformSupported 分支之外——若用户在另一台 - 支持本地推理的机器上启用过 local-qwen3 / foundry,preferences 同步到 - 当前 Linux/不支持平台后会卡死(无 enable / disable UI 可点),polish - 走老路径必失败。这里始终在 isOnAnyLocal 时露出 disable 入口,给用户 - 退路(PR #400 pr_agent advisory 的修法)。 */} - {isOnAnyLocal && ( + {/* 「禁用本地 ASR」逃生入口——只在行内 Toggle 关不掉的场景露出: + - Linux / 不支持平台:根本没有任何引擎行 + - 跨平台异常(macOS profile 同步到 foundry / Windows profile 同步到 qwen3): + 本机引擎 Toggle 是 off,关不动异常 active 的对方引擎 + 否则平台本机 Toggle 自身就能 off → 关停,重复 disable 行徒增视觉。 */} + {isOnAnyLocal && !((isMac && isOnLocalQwen3) || (isWin && isOnFoundry)) && ( - void performSwitch('volcengine')}> - {t('settings.advanced.disable')} - +
+ void performSwitch('volcengine')}> + {t('settings.advanced.disable')} + +
)}
@@ -2226,7 +2327,7 @@ function PermissionsSection() { {desc} -
+
{microphone !== 'granted' && microphone !== 'notApplicable' && microphone !== 'loading' && ( @@ -2251,13 +2352,13 @@ function PermissionsSection() { label={t('settings.permissions.hotkeyLabel')} desc={capability ? t('settings.permissions.hotkeyDescWithAdapter', { adapter: adapterDisplayName(capability.adapter) }) : t('settings.permissions.hotkeyDescPlain')} > -
- +
{hotkey?.message && ( {hotkey.message} )} +
{windowsIme?.state !== 'notWindows' && ( @@ -2265,18 +2366,20 @@ function PermissionsSection() { label={t('settings.permissions.windowsImeLabel')} desc={t('settings.permissions.windowsImeDesc')} > -
- +
{windowsIme && ( {t(`settings.permissions.windowsIme.${windowsIme.state}`)} )} +
)} - {t('settings.permissions.networkOk')} +
+ {t('settings.permissions.networkOk')} +
);