diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 76b49b8..46373cd 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -67,7 +67,7 @@ const MIN_MAIN_WINDOW_HEIGHT: u32 = 600; const MAX_SAVED_WINDOW_DIMENSION: u32 = 16_384; const OAUTH_DIR_NAME: &str = "oauth"; const DEFAULT_API_KEY: &str = "123456"; -const DEFAULT_API_KEY_REMARK: &str = "内置密钥"; +const DEFAULT_API_KEY_INITIAL_REMARK: &str = "默认密钥"; const DEFAULT_MANAGEMENT_SECRET_KEY: &str = "123456"; const MANAGED_AGENT_PROVIDER_ID: &str = "cpa-gui"; const CODEX_MODEL_CATALOG_FILE: &str = "cpa-gui-model-catalog.json"; @@ -465,7 +465,7 @@ impl Default for GuiConfigFile { .and_then(|path| path.parent().map(|parent| parent.join(OAUTH_DIR_NAME))) .map(|path| path_to_string(&path)) .unwrap_or_else(|| OAUTH_DIR_NAME.to_string()), - api_keys: vec![built_in_api_key_entry()], + api_keys: vec![default_api_key_entry()], // Keep plaintext here for management API auth. Core hashes the // value written into config.yaml on startup. management_secret_key: DEFAULT_MANAGEMENT_SECRET_KEY.to_string(), @@ -734,7 +734,6 @@ struct CoreConfigSettings { struct CoreApiKeyView { api_key: String, remark: String, - built_in: bool, } #[derive(Clone, Serialize)] @@ -1166,7 +1165,6 @@ impl From<&GuiConfigFile> for CoreConfigView { .map(|entry| CoreApiKeyView { api_key: entry.key.clone(), remark: entry.remark.clone(), - built_in: entry.key == DEFAULT_API_KEY, }) .collect(), management_secret_configured: !config.management_secret_key.is_empty(), @@ -4753,17 +4751,13 @@ fn add_core_api_key( validate_core_api_key(&api_key)?; validate_api_key_remark(&remark)?; let mut settings = current_core_config_settings(gui_config_state.inner())?; - if api_key == DEFAULT_API_KEY - || settings - .api_keys - .iter() - .any(|existing| existing == &api_key) + if settings + .api_keys + .iter() + .any(|existing| existing == &api_key) { return Err("该鉴权密钥已经存在".to_string()); } - if !settings.api_keys.iter().any(|key| key == DEFAULT_API_KEY) { - settings.api_keys.insert(0, DEFAULT_API_KEY.to_string()); - } settings.api_keys.push(api_key); patch_core_api_keys(&settings.api_keys)?; let added_api_key = settings.api_keys.last().map(|key| GuiApiKeyEntry { @@ -4774,6 +4768,64 @@ fn add_core_api_key( Ok(CoreConfigView::from(&config)) } +fn replace_core_api_key_value( + api_keys: &mut [String], + original_api_key: &str, + replacement_api_key: String, +) -> Result<(), String> { + let index = api_keys + .iter() + .position(|existing| existing == original_api_key) + .ok_or_else(|| "要编辑的鉴权密钥不存在,请刷新后重试".to_string())?; + if replacement_api_key != original_api_key + && api_keys + .iter() + .any(|existing| existing == &replacement_api_key) + { + return Err("该鉴权密钥已经存在".to_string()); + } + api_keys[index] = replacement_api_key; + Ok(()) +} + +fn remove_core_api_key_value(api_keys: &mut Vec, api_key: &str) -> Result<(), String> { + let index = api_keys + .iter() + .position(|existing| existing == api_key) + .ok_or_else(|| "要删除的鉴权密钥不存在,请刷新后重试".to_string())?; + api_keys.remove(index); + Ok(()) +} + +#[tauri::command] +fn update_core_api_key( + gui_config_state: tauri::State<'_, GuiConfigState>, + original_api_key: String, + api_key: String, + remark: String, +) -> Result { + let original_api_key = original_api_key.trim(); + let api_key = api_key.trim().to_string(); + let remark = remark.trim().to_string(); + if original_api_key.is_empty() { + return Err("要编辑的鉴权密钥不能为空".to_string()); + } + validate_core_api_key(&api_key)?; + validate_api_key_remark(&remark)?; + + let mut settings = current_core_config_settings(gui_config_state.inner())?; + replace_core_api_key_value(&mut settings.api_keys, original_api_key, api_key.clone())?; + patch_core_api_keys(&settings.api_keys)?; + let config = gui_config_state.sync_core_settings_with_api_key( + &settings, + Some(GuiApiKeyEntry { + key: api_key, + remark, + }), + )?; + Ok(CoreConfigView::from(&config)) +} + #[tauri::command] fn delete_core_api_key( gui_config_state: tauri::State<'_, GuiConfigState>, @@ -4783,16 +4835,8 @@ fn delete_core_api_key( if api_key.is_empty() { return Err("要删除的鉴权密钥不能为空".to_string()); } - if api_key == DEFAULT_API_KEY { - return Err("内置密钥不能删除".to_string()); - } let mut settings = current_core_config_settings(gui_config_state.inner())?; - let index = settings - .api_keys - .iter() - .position(|existing| existing == api_key) - .ok_or_else(|| "要删除的鉴权密钥不存在,请刷新后重试".to_string())?; - settings.api_keys.remove(index); + remove_core_api_key_value(&mut settings.api_keys, api_key)?; patch_core_api_keys(&settings.api_keys)?; let config = gui_config_state.sync_core_settings(&settings)?; Ok(CoreConfigView::from(&config)) @@ -8625,12 +8669,19 @@ fn ensure_fixed_oauth_dir() -> Result { Ok(directory) } +fn should_import_core_api_keys(had_existing_gui_config: bool, core_api_keys: &[String]) -> bool { + had_existing_gui_config || !core_api_keys.is_empty() +} + fn load_or_create_gui_config() -> Result { ensure_fixed_oauth_dir()?; let config_path = gui_config_path()?; let legacy_config_path = legacy_gui_config_path()?; + let gui_config_exists = config_path.is_file(); + let legacy_config_exists = legacy_config_path.is_file(); + let had_existing_gui_config = gui_config_exists || legacy_config_exists; - let (mut config, presence, mut changed) = if config_path.is_file() { + let (mut config, presence, mut changed) = if gui_config_exists { let content = fs::read_to_string(&config_path) .map_err(|err| format!("读取 GUI 配置失败 {}: {err}", path_to_string(&config_path)))?; let config = toml::from_str::(&content) @@ -8638,7 +8689,7 @@ fn load_or_create_gui_config() -> Result { let presence = toml::from_str::(&content) .map_err(|err| format!("解析 GUI 配置字段失败: {err}"))?; (config, presence, false) - } else if legacy_config_path.is_file() { + } else if legacy_config_exists { let content = fs::read_to_string(&legacy_config_path).map_err(|err| { format!( "读取旧 GUI 配置失败 {}: {err}", @@ -8660,7 +8711,9 @@ fn load_or_create_gui_config() -> Result { || presence.routing_strategy.is_none(); if missing_core_settings { if let Ok(core_settings) = read_installed_core_config_settings() { - if presence.api_keys.is_none() { + if presence.api_keys.is_none() + && should_import_core_api_keys(had_existing_gui_config, &core_settings.api_keys) + { config.api_keys = merge_core_api_keys_with_gui_metadata( &config.api_keys, &core_settings.api_keys, @@ -8704,10 +8757,10 @@ fn apply_core_settings_to_gui_config( config.routing_strategy = core_settings.routing_strategy.clone(); } -fn built_in_api_key_entry() -> GuiApiKeyEntry { +fn default_api_key_entry() -> GuiApiKeyEntry { GuiApiKeyEntry { key: DEFAULT_API_KEY.to_string(), - remark: DEFAULT_API_KEY_REMARK.to_string(), + remark: DEFAULT_API_KEY_INITIAL_REMARK.to_string(), } } @@ -8720,11 +8773,11 @@ fn merge_core_api_keys_with_gui_metadata( core_api_keys: &[String], added_api_key: Option<&GuiApiKeyEntry>, ) -> Vec { - let mut merged = vec![built_in_api_key_entry()]; + let mut merged: Vec = Vec::new(); for api_key in core_api_keys { let api_key = api_key.trim(); - if api_key.is_empty() || api_key == DEFAULT_API_KEY || is_example_core_api_key(api_key) { + if api_key.is_empty() || is_example_core_api_key(api_key) { continue; } if merged.iter().any(|entry| entry.key == api_key) { @@ -8885,9 +8938,6 @@ fn sanitize_gui_config(config: &mut GuiConfigFile) -> Result { for entry in &mut config.api_keys { entry.key = entry.key.trim().to_string(); entry.remark = entry.remark.trim().to_string(); - if entry.key == DEFAULT_API_KEY { - entry.remark = DEFAULT_API_KEY_REMARK.to_string(); - } } if config.api_keys != original_api_keys { changed = true; @@ -10828,6 +10878,7 @@ fn main() { save_gui_settings, get_core_config_settings, add_core_api_key, + update_core_api_key, delete_core_api_key, set_core_management_secret_key, clear_core_management_secret_key, @@ -10948,7 +10999,7 @@ mod tests { assert!(content.contains("auth-dir = ")); assert!(content.contains("[[api-keys]]")); assert!(content.contains("key = \"123456\"")); - assert!(content.contains("remark = \"内置密钥\"")); + assert!(content.contains("remark = \"默认密钥\"")); assert!(content.contains("management-secret-key = \"123456\"")); assert!(content.contains("plugins-enabled = false")); assert!(content.contains("routing-strategy = \"round-robin\"")); @@ -11998,7 +12049,7 @@ mod tests { } #[test] - fn legacy_string_api_keys_gain_remarks_and_keep_custom_keys() { + fn legacy_string_api_keys_keep_custom_keys_without_special_protection() { let legacy = "port = 8317\nallow-lan = false\nrun-on-startup = false\nauth-dir = \"/tmp/oauth\"\napi-keys = [\"123456\", \"custom-key\"]\nmanagement-secret-key = \"123456\"\nplugins-enabled = false\nrouting-strategy = \"round-robin\"\n"; let mut config = toml::from_str::(legacy).unwrap(); @@ -12007,12 +12058,11 @@ mod tests { gui_api_key_values(&config.api_keys), vec!["123456", "custom-key"] ); - assert_eq!(config.api_keys[0].remark, DEFAULT_API_KEY_REMARK); + assert!(config.api_keys[0].remark.is_empty()); assert!(config.api_keys[1].remark.is_empty()); let serialized = toml::to_string_pretty(&config).unwrap(); assert!(serialized.contains("[[api-keys]]")); - assert!(serialized.contains("remark = \"内置密钥\"")); let reparsed = toml::from_str::(&serialized).unwrap(); assert_eq!(reparsed.api_keys, config.api_keys); } @@ -12020,7 +12070,7 @@ mod tests { #[test] fn api_key_remarks_follow_matching_core_keys() { let existing = vec![ - built_in_api_key_entry(), + default_api_key_entry(), GuiApiKeyEntry { key: "custom-key".to_string(), remark: "开发环境".to_string(), @@ -12030,12 +12080,48 @@ mod tests { let merged = merge_core_api_keys_with_gui_metadata(&existing, &core_keys, None); - assert_eq!( - gui_api_key_values(&merged), - vec!["123456", "custom-key", "new-key"] - ); - assert_eq!(merged[1].remark, "开发环境"); - assert!(merged[2].remark.is_empty()); + assert_eq!(gui_api_key_values(&merged), vec!["custom-key", "new-key"]); + assert_eq!(merged[0].remark, "开发环境"); + assert!(merged[1].remark.is_empty()); + } + + #[test] + fn explicit_empty_api_key_list_stays_empty() { + let existing = vec![default_api_key_entry()]; + assert!(merge_core_api_keys_with_gui_metadata(&existing, &[], None).is_empty()); + + let mut config = GuiConfigFile { + api_keys: Vec::new(), + ..GuiConfigFile::default() + }; + sanitize_gui_config(&mut config).unwrap(); + assert!(config.api_keys.is_empty()); + + let content = toml::to_string_pretty(&config).unwrap(); + let restored = toml::from_str::(&content).unwrap(); + assert!(restored.api_keys.is_empty()); + } + + #[test] + fn fresh_config_keeps_initial_default_unless_core_has_real_keys() { + assert!(!should_import_core_api_keys(false, &[])); + assert!(should_import_core_api_keys( + false, + &["existing-key".to_string()] + )); + assert!(should_import_core_api_keys(true, &[])); + } + + #[test] + fn initial_default_api_key_can_be_edited_and_deleted() { + let mut api_keys = vec![DEFAULT_API_KEY.to_string()]; + + replace_core_api_key_value(&mut api_keys, DEFAULT_API_KEY, "custom-key".to_string()) + .unwrap(); + assert_eq!(api_keys, vec!["custom-key"]); + + remove_core_api_key_value(&mut api_keys, "custom-key").unwrap(); + assert!(api_keys.is_empty()); } #[test] @@ -12043,8 +12129,8 @@ mod tests { let view = serde_json::to_value(CoreConfigView::from(&GuiConfigFile::default())).unwrap(); assert_eq!(view["apiKeys"][0]["apiKey"], DEFAULT_API_KEY); - assert_eq!(view["apiKeys"][0]["remark"], DEFAULT_API_KEY_REMARK); - assert_eq!(view["apiKeys"][0]["builtIn"], true); + assert_eq!(view["apiKeys"][0]["remark"], DEFAULT_API_KEY_INITIAL_REMARK); + assert!(view["apiKeys"][0].get("builtIn").is_none()); } #[test] @@ -12101,10 +12187,7 @@ mod tests { assert!(presence.routing_strategy.is_none()); apply_core_settings_to_gui_config(&mut config, &core_settings); - assert_eq!( - gui_api_key_values(&config.api_keys), - vec!["123456", "existing-key"] - ); + assert_eq!(gui_api_key_values(&config.api_keys), vec!["existing-key"]); assert_eq!(config.management_secret_key, DEFAULT_MANAGEMENT_SECRET_KEY); assert!(config.plugins_enabled); assert_eq!(config.routing_strategy, "fill-first"); @@ -12121,10 +12204,7 @@ mod tests { apply_core_settings_to_gui_config(&mut config, &core_settings); assert_eq!(core_settings.api_keys, vec!["real-key"]); - assert_eq!( - gui_api_key_values(&config.api_keys), - vec!["123456", "real-key"] - ); + assert_eq!(gui_api_key_values(&config.api_keys), vec!["real-key"]); assert_eq!( core_settings.management_secret_key.as_deref(), Some("plain-management-secret") @@ -12472,7 +12552,7 @@ mod tests { window_height: None, auth_dir: path_to_string(&fixed_oauth_dir().unwrap()), api_keys: vec![ - built_in_api_key_entry(), + default_api_key_entry(), GuiApiKeyEntry { key: "gui-key".to_string(), remark: "测试密钥".to_string(), diff --git a/src/i18n/ja.ts b/src/i18n/ja.ts index 5632097..9d5a52b 100644 --- a/src/i18n/ja.ts +++ b/src/i18n/ja.ts @@ -221,6 +221,7 @@ export const jaOverrides = { 'config.error.duplicateKey': '同じ認証キーがすでに存在します', 'config.error.remarkTooLong': 'メモは 80 文字以内で入力してください', 'config.notice.keyAdded': '認証キーを追加しました', + 'config.notice.keyUpdated': '認証キーを更新しました', 'config.notice.keyDeleted': '認証キーを削除しました', 'config.notice.keyCopied': '認証キーをコピーしました', 'config.notice.keyCopyFailed': '認証キーをコピーできませんでした', @@ -235,8 +236,8 @@ export const jaOverrides = { 'config.keys.noRemark': 'メモなし', 'config.keys.copy': '認証キーをコピー', 'config.keys.copyNth': '{number} 番目の認証キーをコピー', - 'config.keys.builtInProtected': '組み込みキーは削除できません', - 'config.keys.builtIn': '組み込み', + 'config.keys.edit': '認証キーを編集', + 'config.keys.editNth': '{number} 番目の認証キーを編集', 'config.keys.delete': '認証キーを削除', 'config.keys.deleteNth': '{number} 番目の認証キーを削除', 'config.keys.empty': '認証キーがありません', @@ -248,6 +249,7 @@ export const jaOverrides = { 'config.plugins.enable': 'プラグインシステムを有効にする', 'config.routing.title': 'ルーティング方式', 'config.keys.addTitle': '認証キーを追加', + 'config.keys.editTitle': '認証キーを編集', 'common.close': '閉じる', 'config.keys.label': '認証キー', 'config.keys.hide': 'キーを隠す', @@ -258,6 +260,7 @@ export const jaOverrides = { 'config.keys.adding': '追加中', 'common.add': '追加', 'config.keys.deleteTitle': '認証キーを削除', + 'config.keys.deleteAllWarning': 'すべてのキーを削除すると、デフォルトですべてのキーが許可されます', 'common.cancel': 'キャンセル', 'common.deleting': '削除中', 'common.delete': '削除', @@ -633,6 +636,7 @@ export const jaOverrides = { 'kernel.versions.noBundled': 'このリリースには組み込みコアが含まれていません', 'kernel.versions.offlineInstall': 'オフラインインストール', 'kernel.access.defaultKey': '既定のキー:{key}', + 'kernel.access.noConfiguredKey': 'キーが設定されていないため、現在はすべてのキーがデフォルトで許可されます', 'kernel.access.connectable': '接続可能', 'kernel.access.waiting': 'コアの起動を待っています', 'kernel.access.localUrl': 'ローカル URL', diff --git a/src/i18n/resources.ts b/src/i18n/resources.ts index 4d44a3a..f736461 100644 --- a/src/i18n/resources.ts +++ b/src/i18n/resources.ts @@ -224,6 +224,7 @@ export const zhCN = { 'config.error.duplicateKey': '该鉴权密钥已经存在', 'config.error.remarkTooLong': '备注不能超过 80 个字符', 'config.notice.keyAdded': '鉴权密钥已新增', + 'config.notice.keyUpdated': '鉴权密钥已更新', 'config.notice.keyDeleted': '鉴权密钥已删除', 'config.notice.keyCopied': '鉴权密钥已复制', 'config.notice.keyCopyFailed': '复制鉴权密钥失败', @@ -238,8 +239,8 @@ export const zhCN = { 'config.keys.noRemark': '未填写备注', 'config.keys.copy': '复制鉴权密钥', 'config.keys.copyNth': '复制第 {number} 个鉴权密钥', - 'config.keys.builtInProtected': '内置密钥不能删除', - 'config.keys.builtIn': '内置', + 'config.keys.edit': '编辑鉴权密钥', + 'config.keys.editNth': '编辑第 {number} 个鉴权密钥', 'config.keys.delete': '删除鉴权密钥', 'config.keys.deleteNth': '删除第 {number} 个鉴权密钥', 'config.keys.empty': '暂无鉴权密钥', @@ -251,6 +252,7 @@ export const zhCN = { 'config.plugins.enable': '启用插件系统', 'config.routing.title': '路由策略', 'config.keys.addTitle': '新增鉴权密钥', + 'config.keys.editTitle': '编辑鉴权密钥', 'common.close': '关闭', 'config.keys.label': '鉴权密钥', 'config.keys.hide': '隐藏密钥', @@ -261,6 +263,7 @@ export const zhCN = { 'config.keys.adding': '正在新增', 'common.add': '新增', 'config.keys.deleteTitle': '删除鉴权密钥', + 'config.keys.deleteAllWarning': '删除所有密钥将会导致默认放行所有密钥', 'common.cancel': '取消', 'common.deleting': '正在删除', 'common.delete': '删除', @@ -636,6 +639,7 @@ export const zhCN = { 'kernel.versions.noBundled': '当前发行包未包含内置内核', 'kernel.versions.offlineInstall': '离线安装', 'kernel.access.defaultKey': '默认密钥:{key}', + 'kernel.access.noConfiguredKey': '未设置密钥,当前默认放行所有密钥', 'kernel.access.connectable': '可连接', 'kernel.access.waiting': '等待内核启动', 'kernel.access.localUrl': '本机 URL', @@ -927,6 +931,7 @@ export const en: Record = { 'config.error.duplicateKey': 'This authentication key already exists', 'config.error.remarkTooLong': 'The remark cannot exceed 80 characters', 'config.notice.keyAdded': 'Authentication key added', + 'config.notice.keyUpdated': 'Authentication key updated', 'config.notice.keyDeleted': 'Authentication key deleted', 'config.notice.keyCopied': 'Authentication key copied', 'config.notice.keyCopyFailed': 'Failed to copy the authentication key', @@ -941,8 +946,8 @@ export const en: Record = { 'config.keys.noRemark': 'No remark', 'config.keys.copy': 'Copy authentication key', 'config.keys.copyNth': 'Copy authentication key {number}', - 'config.keys.builtInProtected': 'The built-in key cannot be deleted', - 'config.keys.builtIn': 'Built-in', + 'config.keys.edit': 'Edit authentication key', + 'config.keys.editNth': 'Edit authentication key {number}', 'config.keys.delete': 'Delete authentication key', 'config.keys.deleteNth': 'Delete authentication key {number}', 'config.keys.empty': 'No authentication keys', @@ -954,6 +959,7 @@ export const en: Record = { 'config.plugins.enable': 'Enable plugin system', 'config.routing.title': 'Routing Strategy', 'config.keys.addTitle': 'Add Authentication Key', + 'config.keys.editTitle': 'Edit Authentication Key', 'common.close': 'Close', 'config.keys.label': 'Authentication Key', 'config.keys.hide': 'Hide key', @@ -964,6 +970,7 @@ export const en: Record = { 'config.keys.adding': 'Adding', 'common.add': 'Add', 'config.keys.deleteTitle': 'Delete Authentication Key', + 'config.keys.deleteAllWarning': 'Deleting all keys will allow all keys by default', 'common.cancel': 'Cancel', 'common.deleting': 'Deleting', 'common.delete': 'Delete', @@ -1339,6 +1346,7 @@ export const en: Record = { 'kernel.versions.noBundled': 'This release does not include a bundled core', 'kernel.versions.offlineInstall': 'Offline Install', 'kernel.access.defaultKey': 'Default key: {key}', + 'kernel.access.noConfiguredKey': 'No key is configured; all keys are currently allowed by default', 'kernel.access.connectable': 'Connectable', 'kernel.access.waiting': 'Waiting for core startup', 'kernel.access.localUrl': 'Local URL', diff --git a/src/pages/ConfigPanel.tsx b/src/pages/ConfigPanel.tsx index 01ad6cd..16c22c7 100644 --- a/src/pages/ConfigPanel.tsx +++ b/src/pages/ConfigPanel.tsx @@ -7,6 +7,7 @@ import { Eye, EyeOff, KeyRound, + Pencil, Plus, Plug, RefreshCw, @@ -26,10 +27,9 @@ type CoreConfigSettings = { type CoreApiKey = { apiKey: string; remark: string; - builtIn: boolean; }; -type ConfigAction = 'add-key' | 'delete-key' | 'plugins' | 'routing' | null; +type ConfigAction = 'add-key' | 'update-key' | 'delete-key' | 'plugins' | 'routing' | null; type NoticeTone = 'success' | 'error'; const ROUTING_OPTIONS = [ @@ -44,6 +44,7 @@ export function ConfigPanelPage() { const [loadError, setLoadError] = useState(''); const [busyAction, setBusyAction] = useState(null); const [addDialogOpen, setAddDialogOpen] = useState(false); + const [editingApiKey, setEditingApiKey] = useState(null); const [deleteIndex, setDeleteIndex] = useState(null); const [newApiKey, setNewApiKey] = useState(''); const [newApiKeyRemark, setNewApiKeyRemark] = useState(''); @@ -114,6 +115,7 @@ export function ConfigPanelPage() { }; const openAddDialog = () => { + setEditingApiKey(null); setNewApiKey(''); setNewApiKeyRemark(''); setShowApiKey(false); @@ -121,11 +123,21 @@ export function ConfigPanelPage() { setAddDialogOpen(true); }; + const openEditDialog = (entry: CoreApiKey) => { + setEditingApiKey(entry.apiKey); + setNewApiKey(entry.apiKey); + setNewApiKeyRemark(entry.remark); + setShowApiKey(false); + setFormError(''); + setAddDialogOpen(true); + }; + const closeAddDialog = () => { - if (busyAction === 'add-key') { + if (busyAction === 'add-key' || busyAction === 'update-key') { return; } setAddDialogOpen(false); + setEditingApiKey(null); setFormError(''); }; @@ -149,7 +161,7 @@ export function ConfigPanelPage() { setFormError(t('config.error.invalidKey')); return; } - if (settings?.apiKeys.some((entry) => entry.apiKey === apiKey)) { + if (settings?.apiKeys.some((entry) => entry.apiKey === apiKey && entry.apiKey !== editingApiKey)) { setFormError(t('config.error.duplicateKey')); return; } @@ -159,14 +171,16 @@ export function ConfigPanelPage() { return; } + const editing = editingApiKey !== null; const saved = await runMutation( - 'add-key', - 'add_core_api_key', - { apiKey, remark }, - t('config.notice.keyAdded'), + editing ? 'update-key' : 'add-key', + editing ? 'update_core_api_key' : 'add_core_api_key', + editing ? { originalApiKey: editingApiKey, apiKey, remark } : { apiKey, remark }, + editing ? t('config.notice.keyUpdated') : t('config.notice.keyAdded'), ); if (saved) { setAddDialogOpen(false); + setEditingApiKey(null); setNewApiKey(''); setNewApiKeyRemark(''); } @@ -228,6 +242,8 @@ export function ConfigPanelPage() { const controlsDisabled = loading || settings === null || busyAction !== null; const selectedDeleteKey = deleteIndex === null ? '' : settings?.apiKeys[deleteIndex]?.apiKey || ''; + const deletingLastKey = deleteIndex !== null && settings?.apiKeys.length === 1; + const keyMutationBusy = busyAction === 'add-key' || busyAction === 'update-key'; return (
@@ -302,22 +318,26 @@ export function ConfigPanelPage() {