让凭据状态按 active provider 判断,修复错误配置提示

get_credentials 不再把 ASR 配置状态硬编码为 Volcengine,而是返回 active provider 与通用 asrConfigured/llmConfigured。前端 provider setup prompt 与 Overview 改为读取通用状态并展示 active provider 名称,避免 Whisper-compatible 用户已配置却持续被提示未配置。配置完成判定统一为 API Key 可空但 endpoint/model 必须存在:云端 ASR 需 endpoint/model,LLM 需 endpoint/model。保留旧字段做兼容过渡。

Constraint: 最小修改原则,保持现有 provider 凭据存储结构不变

Rejected: 只按 ASR API key 标记已配置 | keyless provider 会被误判,且 endpoint/model 缺失仍不可运行

Rejected: 彻底移除旧 volcengineConfigured/arkConfigured 字段 | 需要分阶段兼容前端调用

Confidence: high

Scope-risk: moderate

Reversibility: clean

Directive: Provider configured 表示具备 endpoint/model 基础配置;连接有效性仍由验证按钮负责

Tested: npm run -s build

Tested: cargo test -q (74 passed)

Related: #223
This commit is contained in:
H-Chris233 committed 2026-05-06 00:23:19 +08:00
1 parent 6d1ff0f872
commit cb60a034b6
8 files changed
+227 -22

No files matched your search

+100 -9
View File
@@ -14,8 +14,7 @@ use crate::polish::{LLMError, OpenAICompatibleConfig, OpenAICompatibleLLMProvide
use crate::types::{
ChineseScriptPreference, CredentialsStatus, DictationSession, DictionaryEntry,
HotkeyCapability, HotkeyStatus, OutputLanguagePreference, PolishMode, QaHotkeyBinding,
UserPreferences,
VocabPresetStore, WindowsImeStatus,
UserPreferences, VocabPresetStore, WindowsImeStatus,
};
type CoordinatorState<'a> = State<'a, Arc<Coordinator>>;
@@ -100,16 +99,47 @@ pub fn get_windows_ime_status() -> WindowsImeStatus {
#[tauri::command]
pub fn get_credentials() -> CredentialsStatus {
let snap = CredentialsVault::snapshot();
let active_asr_provider = CredentialsVault::get_active_asr();
let active_llm_provider = CredentialsVault::get_active_llm();
let volcengine_configured = volcengine_configured(&snap);
let asr_configured = asr_configured_for_provider(&active_asr_provider, &snap);
let llm_configured = llm_configured_for_snapshot(&snap);
CredentialsStatus {
volcengine_configured: configured(&snap.volcengine_app_key)
&& configured(&snap.volcengine_access_key)
&& configured(&snap.volcengine_resource_id),
ark_configured: configured(&snap.ark_api_key),
active_asr_provider,
active_llm_provider,
asr_configured,
llm_configured,
volcengine_configured,
ark_configured: llm_configured,
}
}
fn volcengine_configured(snap: &CredentialsSnapshot) -> bool {
configured(&snap.volcengine_app_key)
&& configured(&snap.volcengine_access_key)
&& configured(&snap.volcengine_resource_id)
}
fn asr_configured_for_provider(provider: &str, snap: &CredentialsSnapshot) -> bool {
if provider == "volcengine" {
return volcengine_configured(snap);
}
if provider == crate::asr::local::PROVIDER_ID {
// 本地 ASR 不依赖云端凭据。
return true;
}
configured(&snap.asr_endpoint) && configured(&snap.asr_model)
}
fn llm_configured_for_snapshot(snap: &CredentialsSnapshot) -> bool {
configured(&snap.ark_endpoint) && configured(&snap.ark_model_id)
}
fn configured(field: &Option<String>) -> bool {
field.as_ref().map(|s| !s.is_empty()).unwrap_or(false)
field
.as_ref()
.map(|s| !s.trim().is_empty())
.unwrap_or(false)
}
#[tauri::command]
@@ -851,9 +881,11 @@ fn _ensure_snapshot_used(_: CredentialsSnapshot) {}
#[cfg(test)]
mod tests {
use super::{
asr_transcriptions_url, fetch_provider_models, models_url, parse_model_ids,
persist_settings, ProviderConfig, SettingsWriter,
asr_configured_for_provider, asr_transcriptions_url, fetch_provider_models,
llm_configured_for_snapshot, models_url, parse_model_ids, persist_settings,
ProviderConfig, SettingsWriter,
};
use crate::persistence::CredentialsSnapshot;
use crate::types::{
HotkeyBinding, HotkeyMode, HotkeyTrigger, QaHotkeyBinding, UserPreferences,
};
@@ -869,6 +901,65 @@ mod tests {
qa_refreshes: Mutex<u32>,
}
fn snapshot() -> CredentialsSnapshot {
CredentialsSnapshot::default()
}
#[test]
fn credentials_status_follows_active_asr_provider_requirements() {
let volcengine = CredentialsSnapshot {
volcengine_app_key: Some("app".into()),
volcengine_access_key: Some("access".into()),
volcengine_resource_id: Some("resource".into()),
..snapshot()
};
assert!(asr_configured_for_provider("volcengine", &volcengine));
let whisper_key_only = CredentialsSnapshot {
asr_api_key: Some("key".into()),
..snapshot()
};
assert!(!asr_configured_for_provider("whisper", &whisper_key_only));
let whisper_keyless_ready = CredentialsSnapshot {
asr_endpoint: Some("https://api.openai.com/v1".into()),
asr_model: Some("whisper-1".into()),
..snapshot()
};
assert!(asr_configured_for_provider(
"whisper",
&whisper_keyless_ready
));
assert!(asr_configured_for_provider(
crate::asr::local::PROVIDER_ID,
&snapshot()
));
}
#[test]
fn credentials_status_accepts_keyless_llm_with_endpoint_and_model() {
let keyless_ready = CredentialsSnapshot {
ark_endpoint: Some("http://localhost:11434/v1".into()),
ark_model_id: Some("qwen".into()),
..snapshot()
};
assert!(llm_configured_for_snapshot(&keyless_ready));
let key_without_endpoint = CredentialsSnapshot {
ark_api_key: Some("key".into()),
ark_model_id: Some("qwen".into()),
..snapshot()
};
assert!(!llm_configured_for_snapshot(&key_without_endpoint));
let endpoint_without_model = CredentialsSnapshot {
ark_endpoint: Some("http://localhost:11434/v1".into()),
..snapshot()
};
assert!(!llm_configured_for_snapshot(&endpoint_without_model));
}
impl SettingsWriter for FakeSettingsWriter {
fn write_settings(&self, prefs: UserPreferences) -> Result<(), String> {
*self.saved.lock().unwrap() = Some(prefs);
@@ -674,6 +674,9 @@ pub struct CredentialsSnapshot {
pub volcengine_app_key: Option<String>,
pub volcengine_access_key: Option<String>,
pub volcengine_resource_id: Option<String>,
pub asr_api_key: Option<String>,
pub asr_endpoint: Option<String>,
pub asr_model: Option<String>,
pub ark_api_key: Option<String>,
pub ark_model_id: Option<String>,
pub ark_endpoint: Option<String>,
@@ -730,6 +733,11 @@ impl CredentialsVault {
save_credentials(&root)
}
pub fn get_active_llm() -> String {
let _guard = credentials_lock().lock();
load_credentials().active.llm
}
pub fn snapshot() -> CredentialsSnapshot {
let _guard = credentials_lock().lock();
let root = load_credentials();
@@ -737,6 +745,9 @@ impl CredentialsVault {
volcengine_app_key: lookup_account(&root, CredentialAccount::VolcengineAppKey),
volcengine_access_key: lookup_account(&root, CredentialAccount::VolcengineAccessKey),
volcengine_resource_id: lookup_account(&root, CredentialAccount::VolcengineResourceId),
asr_api_key: lookup_account(&root, CredentialAccount::AsrApiKey),
asr_endpoint: lookup_account(&root, CredentialAccount::AsrEndpoint),
asr_model: lookup_account(&root, CredentialAccount::AsrModel),
ark_api_key: lookup_account(&root, CredentialAccount::ArkApiKey),
ark_model_id: lookup_account(&root, CredentialAccount::ArkModelId),
ark_endpoint: lookup_account(&root, CredentialAccount::ArkEndpoint),
+5
View File
@@ -548,6 +548,11 @@ pub struct CapsulePayload {
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct CredentialsStatus {
pub active_asr_provider: String,
pub active_llm_provider: String,
pub asr_configured: bool,
pub llm_configured: bool,
// 兼容旧前端字段(逐步迁移中)
pub volcengine_configured: bool,
pub ark_configured: bool,
}
+4
View File
@@ -71,6 +71,10 @@ const mockHotkeyCapability: HotkeyCapability = {
};
const mockCredentialsStatus: CredentialsStatus = {
activeAsrProvider: 'volcengine',
activeLlmProvider: 'ark',
asrConfigured: true,
llmConfigured: true,
volcengineConfigured: true,
arkConfigured: true,
};
+61 -6
View File
@@ -10,26 +10,67 @@ function assertEqual(actual: boolean, expected: boolean, name: string) {
}
assertEqual(
areProvidersConfigured({ volcengineConfigured: true, arkConfigured: true }),
areProvidersConfigured({
activeAsrProvider: 'volcengine',
activeLlmProvider: 'ark',
asrConfigured: true,
llmConfigured: true,
volcengineConfigured: true,
arkConfigured: true,
}),
true,
'configured when ASR and LLM are both ready',
);
assertEqual(
areProvidersConfigured({ volcengineConfigured: false, arkConfigured: true }),
areProvidersConfigured({
activeAsrProvider: 'volcengine',
activeLlmProvider: 'ark',
asrConfigured: false,
llmConfigured: true,
volcengineConfigured: false,
arkConfigured: true,
}),
false,
'not configured when ASR provider is missing',
);
assertEqual(
areProvidersConfigured({ volcengineConfigured: true, arkConfigured: false }),
areProvidersConfigured({
activeAsrProvider: 'volcengine',
activeLlmProvider: 'ark',
asrConfigured: true,
llmConfigured: false,
volcengineConfigured: true,
arkConfigured: false,
}),
false,
'not configured when LLM provider is missing',
);
assertEqual(
areProvidersConfigured({
activeAsrProvider: 'whisper',
activeLlmProvider: 'ark',
asrConfigured: true,
llmConfigured: true,
volcengineConfigured: false,
arkConfigured: true,
}),
true,
'configured when active ASR is non-volcengine but already ready',
);
assertEqual(
shouldShowProviderSetupPrompt(
{ volcengineConfigured: false, arkConfigured: false },
{
activeAsrProvider: 'whisper',
activeLlmProvider: 'ark',
asrConfigured: false,
llmConfigured: false,
volcengineConfigured: false,
arkConfigured: false,
},
null,
),
true,
@@ -38,7 +79,14 @@ assertEqual(
assertEqual(
shouldShowProviderSetupPrompt(
{ volcengineConfigured: false, arkConfigured: false },
{
activeAsrProvider: 'whisper',
activeLlmProvider: 'ark',
asrConfigured: false,
llmConfigured: false,
volcengineConfigured: false,
arkConfigured: false,
},
'1',
),
false,
@@ -47,7 +95,14 @@ assertEqual(
assertEqual(
shouldShowProviderSetupPrompt(
{ volcengineConfigured: true, arkConfigured: true },
{
activeAsrProvider: 'whisper',
activeLlmProvider: 'ark',
asrConfigured: true,
llmConfigured: true,
volcengineConfigured: false,
arkConfigured: true,
},
null,
),
false,
+3 -1
View File
@@ -3,7 +3,9 @@ import type { CredentialsStatus } from './types';
export const PROVIDER_SETUP_PROMPT_DEFERRED_KEY = 'ol.providerSetupPromptDeferredThisSession';
export function areProvidersConfigured(credentials: CredentialsStatus): boolean {
return credentials.volcengineConfigured && credentials.arkConfigured;
const asrConfigured = credentials.asrConfigured ?? credentials.volcengineConfigured;
const llmConfigured = credentials.llmConfigured ?? credentials.arkConfigured;
return asrConfigured && llmConfigured;
}
export function shouldShowProviderSetupPrompt(
+5
View File
@@ -206,6 +206,11 @@ export interface CapsulePayload {
}
export interface CredentialsStatus {
activeAsrProvider: string;
activeLlmProvider: string;
asrConfigured: boolean;
llmConfigured: boolean;
/** 兼容旧字段(过渡期保留)。 */
volcengineConfigured: boolean;
arkConfigured: boolean;
}
+38 -6
View File
@@ -23,11 +23,32 @@ interface OverviewProps {
onOpenHistory?: () => void;
}
const ASR_NAME_KEY_BY_ID: Record<string, string> = {
volcengine: 'asrVolcengine',
siliconflow: 'asrSiliconflow',
zhipu: 'asrZhipu',
groq: 'asrGroq',
whisper: 'asrWhisper',
'local-qwen3': 'asrLocalQwen3',
};
const LLM_NAME_KEY_BY_ID: Record<string, string> = {
ark: 'ark',
deepseek: 'deepseek',
siliconflow: 'siliconflow',
openai: 'openai',
custom: 'custom',
};
export function Overview({ onOpenHistory }: OverviewProps) {
const { t } = useTranslation();
const modeLabel = useModeLabels();
const [history, setHistory] = useState<DictationSession[]>([]);
const [creds, setCreds] = useState<CredentialsStatus>({
activeAsrProvider: 'volcengine',
activeLlmProvider: 'ark',
asrConfigured: false,
llmConfigured: false,
volcengineConfigured: false,
arkConfigured: false,
});
@@ -64,6 +85,17 @@ export function Overview({ onOpenHistory }: OverviewProps) {
return buckets;
}, [history]);
const asrProviderId = creds.activeAsrProvider || 'volcengine';
const llmProviderId = creds.activeLlmProvider || 'ark';
const asrNameKey = ASR_NAME_KEY_BY_ID[asrProviderId];
const llmNameKey = LLM_NAME_KEY_BY_ID[llmProviderId];
const asrProviderName = asrNameKey
? t(`settings.providers.presets.${asrNameKey}`)
: asrProviderId;
const llmProviderName = llmNameKey
? t(`settings.providers.presets.${llmNameKey}`)
: llmProviderId;
return (
<>
<PageHeader
@@ -75,15 +107,15 @@ export function Overview({ onOpenHistory }: OverviewProps) {
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, marginBottom: 18 }}>
<ProviderCard
kind={t('overview.asrKind')}
name={t('overview.asrName')}
subname={t('overview.asrSubname')}
configured={creds.volcengineConfigured}
name={asrProviderName}
subname={asrProviderId}
configured={creds.asrConfigured}
/>
<ProviderCard
kind={t('overview.llmKind')}
name={t('overview.llmName')}
subname={creds.arkConfigured ? t('overview.llmConfigured') : t('overview.llmNotConfigured')}
configured={creds.arkConfigured}
name={llmProviderName}
subname={llmProviderId}
configured={creds.llmConfigured}
/>
</div>