From 49ac0601e127c5f24a63ba86ce83fd690bd59252 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Sun, 14 Jun 2026 22:18:34 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D:=20FR-S1=20api=5Fkey=20IPC?= =?UTF-8?q?=20mask=20+=20=E7=BC=96=E8=BE=91=E4=B8=8D=E5=9B=9E=E5=A1=AB?= =?UTF-8?q?=EF=BC=88=E9=98=B2=E5=89=8D=E7=AB=AF=E6=8B=BF=E6=98=8E=E6=96=87?= =?UTF-8?q?=EF=BC=8CP0=20=E5=AE=89=E5=85=A8=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ai_list_providers: 返回前 mask api_key(首尾各4+••••),IPC 不传明文给前端 realm - ai_save_provider: api_key 空(编辑不改)→保留原 DB 值 - Settings: 编辑不回填 apiKey(留空=不改) + saveProvider 校验编辑时 apiKey 可空 - 注:DB 仍明文存储(c,keyring 加密存储)留迁移单列;本轮解 IPC 传明文 + 前端 realm 持有明文 来源 fullstack-review §2 P0 安全;cargo/vue-tsc 0 err --- src-tauri/src/commands/ai/commands.rs | 32 ++++++++++++++++++++++++++- src/views/Settings.vue | 6 +++-- 2 files changed, 35 insertions(+), 3 deletions(-) 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 }