让凭据状态按 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:
1 parent
6d1ff0f872
commit
cb60a034b6
8 files changed
+227
-22
No files matched your search
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -71,6 +71,10 @@ const mockHotkeyCapability: HotkeyCapability = {
|
||||
};
|
||||
|
||||
const mockCredentialsStatus: CredentialsStatus = {
|
||||
activeAsrProvider: 'volcengine',
|
||||
activeLlmProvider: 'ark',
|
||||
asrConfigured: true,
|
||||
llmConfigured: true,
|
||||
volcengineConfigured: true,
|
||||
arkConfigured: true,
|
||||
};
|
||||
|
||||
@@ -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,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(
|
||||
|
||||
@@ -206,6 +206,11 @@ export interface CapsulePayload {
|
||||
}
|
||||
|
||||
export interface CredentialsStatus {
|
||||
activeAsrProvider: string;
|
||||
activeLlmProvider: string;
|
||||
asrConfigured: boolean;
|
||||
llmConfigured: boolean;
|
||||
/** 兼容旧字段(过渡期保留)。 */
|
||||
volcengineConfigured: boolean;
|
||||
arkConfigured: boolean;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in new issue
Block a user