diff --git a/src-tauri/src/commands/ai/commands.rs b/src-tauri/src/commands/ai/commands.rs index b3d8705..1296303 100644 --- a/src-tauri/src/commands/ai/commands.rs +++ b/src-tauri/src/commands/ai/commands.rs @@ -265,10 +265,28 @@ pub async fn ai_chat_stop(state: State<'_, AppState>, app: AppHandle) -> Result< // 提供商管理 // ============================================================ +/// api_key 脱敏:IPC 不传明文给前端(FR-S1),保留首尾各 4 字符便于辨识 +fn mask_api_key(key: &str) -> String { + let chars: Vec = key.chars().collect(); + if chars.len() <= 8 { + return "•".repeat(chars.len()); + } + let prefix: String = chars[..4].iter().collect(); + let suffix: String = chars[chars.len() - 4..].iter().collect(); + format!("{}••••{}", prefix, suffix) +} + /// 列出所有已配置的 AI 提供商(is_default 真相源为 DB,重启不丢) #[tauri::command] pub async fn ai_list_providers(state: State<'_, AppState>) -> Result, String> { - state.ai_providers.list_all().await.map_err(|e| e.to_string()) + let mut providers = state.ai_providers.list_all().await.map_err(|e| e.to_string())?; + // IPC 不传明文 api_key(FR-S1):前端编辑用空 apiKey 表示不改,mask 后前端 realm 不持有明文 + for p in &mut providers { + if !p.api_key.is_empty() { + p.api_key = mask_api_key(&p.api_key); + } + } + Ok(providers) } /// 保存/更新 AI 提供商配置 @@ -300,6 +318,18 @@ pub async fn ai_save_provider( .map_err(|e| e.to_string())? .iter().any(|p| p.is_default), }; + // api_key 空(编辑不改 key,前端 list 拿到 mask 故留空)→保留原 DB 值(FR-S1) + let api_key = if api_key.is_empty() { + match &id { + Some(pid) => state.ai_providers.get_by_id(pid).await + .map_err(|e| e.to_string())? + .map(|p| p.api_key) + .unwrap_or_default(), + None => String::new(), + } + } else { + api_key + }; let record = AiProviderRecord { id: id.unwrap_or_else(new_id), name, diff --git a/src/views/Settings.vue b/src/views/Settings.vue index 1866304..d8a2ebf 100644 --- a/src/views/Settings.vue +++ b/src/views/Settings.vue @@ -443,7 +443,7 @@ function openProviderForm(p?: AiProviderConfig) { providerForm.editId = p.id providerForm.name = p.name providerForm.baseUrl = p.base_url - providerForm.apiKey = p.api_key + providerForm.apiKey = '' // 编辑不回填 api_key(list 返回 mask,留空=不改,FR-S1) providerForm.defaultModel = p.default_model providerForm.providerType = p.provider_type || 'openai_compat' } else { @@ -458,7 +458,9 @@ function openProviderForm(p?: AiProviderConfig) { } async function saveProvider() { - if (!providerForm.name || !providerForm.baseUrl || !providerForm.apiKey || !providerForm.defaultModel) { + // apiKey:新建必填;编辑可留空(不改,FR-S1 后端保留原 DB 值) + const needKey = !providerForm.editId + if (!providerForm.name || !providerForm.baseUrl || (needKey && !providerForm.apiKey) || !providerForm.defaultModel) { showToast(t('settings.toastSaveIncomplete'), 'warning') return }