From b3e8d33af0fcedf36b1bf2fe4be80a95f2a4f045 Mon Sep 17 00:00:00 2001 From: lzt404 <2596933790@qq.com> Date: Tue, 28 Jul 2026 13:30:50 +0800 Subject: [PATCH 1/3] feat: migrate core configuration during updates to prevent data loss --- src-tauri/src/main.rs | 120 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 103 insertions(+), 17 deletions(-) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index f88639b..a05ea87 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -7726,7 +7726,7 @@ async fn install_core_version_inner( .strip_prefix(&staging_dir) .map_err(|err| format!("计算内核二进制相对路径失败: {err}"))? .to_path_buf(); - preserve_core_runtime_files(&install_dir, &staging_dir)?; + migrate_core_config_for_update(&install_dir, &staging_dir)?; preserve_bundled_core_assets(&install_dir, &staging_dir)?; write_core_metadata( &staging_dir, @@ -7789,7 +7789,7 @@ fn install_bundled_core_inner( .strip_prefix(&staging_dir) .map_err(|error| format!("计算内置内核二进制路径失败: {error}"))? .to_path_buf(); - preserve_core_runtime_files(&install_dir, &staging_dir)?; + migrate_core_config_for_update(&install_dir, &staging_dir)?; preserve_bundled_core_assets(&install_dir, &staging_dir)?; preserve_selected_bundled_core_asset(archive_path, &staging_dir)?; write_core_metadata( @@ -10527,18 +10527,34 @@ fn merge_core_config_yaml( current: Option<&str>, config: &GuiConfigFile, ) -> Result { + let merged = merge_core_config_fields(template, current)?; + apply_gui_managed_settings(&merged, config) +} + +fn merge_core_config_fields(template: &str, current: Option<&str>) -> Result { let template_value = serde_norway::from_str::(template) .map_err(|err| format!("解析内核配置模板失败: {err}"))?; + if !template_value.is_mapping() { + return Err("内核配置模板根节点必须是 YAML 映射".to_string()); + } let mut merged = template_value.clone(); if let Some(current) = current { - if let Ok(current) = serde_norway::from_str::(current) { - merge_yaml_values(&mut merged, current); + let current = serde_norway::from_str::(current) + .map_err(|err| format!("解析现有内核配置失败,为避免配置丢失已停止写入: {err}"))?; + if !current.is_mapping() { + return Err("现有内核配置根节点必须是 YAML 映射,已停止写入".to_string()); } + merge_yaml_values(&mut merged, current); } - let merged = render_yaml_value_changes(template, &template_value, &merged)?; - apply_gui_managed_settings(&merged, config) + let rendered = render_yaml_value_changes(template, &template_value, &merged)?; + let rendered_value = serde_norway::from_str::(&rendered) + .map_err(|err| format!("验证迁移后的内核配置失败: {err}"))?; + if !rendered_value.is_mapping() { + return Err("迁移后的内核配置根节点必须是 YAML 映射".to_string()); + } + Ok(rendered) } fn patch_core_network_yaml( @@ -11628,15 +11644,43 @@ fn preserve_selected_bundled_core_asset( Ok(()) } -fn preserve_core_runtime_files(source_dir: &Path, target_dir: &Path) -> Result<(), String> { +fn migrate_core_config_for_update(source_dir: &Path, target_dir: &Path) -> Result<(), String> { if !source_dir.is_dir() { return Ok(()); } - let source = source_dir.join(CORE_CONFIG_FILE); - if source.is_file() { - fs::copy(&source, target_dir.join(CORE_CONFIG_FILE)) - .map_err(|error| format!("保留内核配置文件失败: {error}"))?; + let old_config_path = source_dir.join(CORE_CONFIG_FILE); + if !old_config_path.is_file() { + return Ok(()); } + + let template_path = target_dir.join(CORE_EXAMPLE_CONFIG_FILE); + if !template_path.is_file() { + return Err(format!( + "新版内核缺少配置模板,已取消更新: {}", + path_to_string(&template_path) + )); + } + let old_config = fs::read_to_string(&old_config_path).map_err(|error| { + format!( + "读取旧版内核配置失败 {}: {error}", + path_to_string(&old_config_path) + ) + })?; + let template = fs::read_to_string(&template_path).map_err(|error| { + format!( + "读取新版内核配置模板失败 {}: {error}", + path_to_string(&template_path) + ) + })?; + let migrated = merge_core_config_fields(&template, Some(&old_config)) + .map_err(|error| format!("迁移旧版内核配置失败,已取消更新以防止配置丢失: {error}"))?; + let config_path = target_dir.join(CORE_CONFIG_FILE); + fs::write(&config_path, migrated).map_err(|error| { + format!( + "写入迁移后的新版内核配置失败 {}: {error}", + path_to_string(&config_path) + ) + })?; Ok(()) } @@ -16372,18 +16416,60 @@ custom_option = "keep-original" } #[test] - fn replacing_a_core_preserves_the_existing_configuration_bytes() { - let root = agent_test_home("core-config-preserve"); + fn replacing_a_core_migrates_old_fields_into_the_new_template() { + let root = agent_test_home("core-config-migrate"); let source = root.join("source"); let target = root.join("target"); fs::create_dir_all(&source).unwrap(); fs::create_dir_all(&target).unwrap(); - let original = b"host: 127.0.0.1\n# keep this comment\ndebug: true\n"; - fs::write(source.join(CORE_CONFIG_FILE), original).unwrap(); + let old_config = "# Old comment\nhost: 127.0.0.1\nport: 9527\nnested:\n keep: old\n old-only: retained\nlist:\n - old-a\n - old-b\nextra: true\n"; + let new_template = "# New template\nhost: \"\"\nport: 8317\nnested:\n keep: new-default\n added: new-field\nlist:\n - new-default\nnew-option: true\n"; + fs::write(source.join(CORE_CONFIG_FILE), old_config).unwrap(); + fs::write(target.join(CORE_EXAMPLE_CONFIG_FILE), new_template).unwrap(); - preserve_core_runtime_files(&source, &target).unwrap(); + migrate_core_config_for_update(&source, &target).unwrap(); - assert_eq!(fs::read(target.join(CORE_CONFIG_FILE)).unwrap(), original); + let migrated = fs::read_to_string(target.join(CORE_CONFIG_FILE)).unwrap(); + let document = serde_norway::from_str::(&migrated).unwrap(); + assert!(migrated.contains("# New template")); + assert_eq!(document["host"], "127.0.0.1"); + assert_eq!(document["port"], 9527); + assert_eq!(document["nested"]["keep"], "old"); + assert_eq!(document["nested"]["added"], "new-field"); + assert_eq!(document["nested"]["old-only"], "retained"); + assert_eq!(document["list"][0], "old-a"); + assert_eq!(document["list"][1], "old-b"); + assert_eq!(document["new-option"], true); + assert_eq!(document["extra"], true); + assert_eq!( + fs::read_to_string(target.join(CORE_EXAMPLE_CONFIG_FILE)).unwrap(), + new_template + ); + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn replacing_a_core_rejects_invalid_old_config_without_overwriting_staging() { + let root = agent_test_home("core-config-migrate-invalid"); + let source = root.join("source"); + let target = root.join("target"); + fs::create_dir_all(&source).unwrap(); + fs::create_dir_all(&target).unwrap(); + fs::write(source.join(CORE_CONFIG_FILE), "host: [invalid\n").unwrap(); + fs::write( + target.join(CORE_EXAMPLE_CONFIG_FILE), + "host: \"\"\nport: 8317\n", + ) + .unwrap(); + fs::write(target.join(CORE_CONFIG_FILE), "staged: untouched\n").unwrap(); + + let error = migrate_core_config_for_update(&source, &target).unwrap_err(); + + assert!(error.contains("已取消更新以防止配置丢失")); + assert_eq!( + fs::read_to_string(target.join(CORE_CONFIG_FILE)).unwrap(), + "staged: untouched\n" + ); fs::remove_dir_all(root).unwrap(); } From 70eb90fe06d99100b894ec83e013722376ac69a5 Mon Sep 17 00:00:00 2001 From: lzt404 <2596933790@qq.com> Date: Tue, 28 Jul 2026 14:18:53 +0800 Subject: [PATCH 2/3] feat: enhance core configuration migration to tolerate invalid fields and prevent data loss --- src-tauri/src/main.rs | 143 +++++++++++++++++++++++++++++++++++------- 1 file changed, 121 insertions(+), 22 deletions(-) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index a05ea87..269ad95 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -10532,6 +10532,31 @@ fn merge_core_config_yaml( } fn merge_core_config_fields(template: &str, current: Option<&str>) -> Result { + let current_value = current + .map(|current| { + let current = serde_norway::from_str::(current) + .map_err(|err| format!("解析现有内核配置失败,为避免配置丢失已停止写入: {err}"))?; + if !current.is_mapping() { + return Err("现有内核配置根节点必须是 YAML 映射,已停止写入".to_string()); + } + Ok(current) + }) + .transpose()?; + merge_core_config_value(template, current_value) +} + +fn merge_core_config_fields_tolerant(template: &str, current: &str) -> Result { + let current = match serde_norway::from_str::(current) { + Ok(current) if current.is_mapping() => current, + _ => recover_parseable_top_level_yaml_fields(current), + }; + merge_core_config_value(template, Some(current)) +} + +fn merge_core_config_value( + template: &str, + current: Option, +) -> Result { let template_value = serde_norway::from_str::(template) .map_err(|err| format!("解析内核配置模板失败: {err}"))?; if !template_value.is_mapping() { @@ -10540,11 +10565,6 @@ fn merge_core_config_fields(template: &str, current: Option<&str>) -> Result(current) - .map_err(|err| format!("解析现有内核配置失败,为避免配置丢失已停止写入: {err}"))?; - if !current.is_mapping() { - return Err("现有内核配置根节点必须是 YAML 映射,已停止写入".to_string()); - } merge_yaml_values(&mut merged, current); } @@ -10557,6 +10577,75 @@ fn merge_core_config_fields(template: &str, current: Option<&str>) -> Result serde_norway::Value { + let lines = yaml_line_ranges(content); + let boundaries = lines + .iter() + .copied() + .filter(|range| is_top_level_yaml_boundary(yaml_line_content(content, *range))) + .collect::>(); + let mut recovered = serde_norway::Mapping::new(); + + for (index, (start, _)) in boundaries.iter().copied().enumerate() { + let line = yaml_line_content(content, boundaries[index]); + if !is_top_level_yaml_mapping_field(line) { + continue; + } + let end = boundaries + .get(index + 1) + .map(|(next_start, _)| *next_start) + .unwrap_or(content.len()); + let Ok(value) = serde_norway::from_str::(&content[start..end]) else { + continue; + }; + let Some(mapping) = value.as_mapping() else { + continue; + }; + for (key, value) in mapping { + recovered.insert(key.clone(), value.clone()); + } + } + + serde_norway::Value::Mapping(recovered) +} + +fn is_top_level_yaml_boundary(line: &str) -> bool { + if line.is_empty() + || line.chars().next().is_some_and(char::is_whitespace) + || line.starts_with('#') + || is_indentationless_yaml_sequence_item(line) + { + return false; + } + !matches!(line.trim(), "---" | "...") && !line.starts_with('%') +} + +fn is_top_level_yaml_mapping_field(line: &str) -> bool { + let mut single_quoted = false; + let mut double_quoted = false; + let mut escaped = false; + for (index, character) in line.char_indices() { + if escaped { + escaped = false; + continue; + } + if double_quoted && character == '\\' { + escaped = true; + continue; + } + match character { + '\'' if !double_quoted => single_quoted = !single_quoted, + '"' if !single_quoted => double_quoted = !double_quoted, + ':' if !single_quoted && !double_quoted => { + let rest = &line[index + character.len_utf8()..]; + return rest.is_empty() || rest.chars().next().is_some_and(char::is_whitespace); + } + _ => {} + } + } + false +} + fn patch_core_network_yaml( content: &str, config: &GuiConfigFile, @@ -11660,20 +11749,23 @@ fn migrate_core_config_for_update(source_dir: &Path, target_dir: &Path) -> Resul path_to_string(&template_path) )); } - let old_config = fs::read_to_string(&old_config_path).map_err(|error| { - format!( - "读取旧版内核配置失败 {}: {error}", - path_to_string(&old_config_path) - ) - })?; + let old_config = match fs::read(&old_config_path) { + Ok(content) => String::from_utf8_lossy(&content).into_owned(), + Err(error) => { + eprintln!( + "读取旧版内核配置失败,将使用新版默认配置继续更新 {}: {error}", + path_to_string(&old_config_path) + ); + String::new() + } + }; let template = fs::read_to_string(&template_path).map_err(|error| { format!( "读取新版内核配置模板失败 {}: {error}", path_to_string(&template_path) ) })?; - let migrated = merge_core_config_fields(&template, Some(&old_config)) - .map_err(|error| format!("迁移旧版内核配置失败,已取消更新以防止配置丢失: {error}"))?; + let migrated = merge_core_config_fields_tolerant(&template, &old_config)?; let config_path = target_dir.join(CORE_CONFIG_FILE); fs::write(&config_path, migrated).map_err(|error| { format!( @@ -16449,27 +16541,34 @@ custom_option = "keep-original" } #[test] - fn replacing_a_core_rejects_invalid_old_config_without_overwriting_staging() { + fn replacing_a_core_discards_invalid_fields_and_continues_migration() { let root = agent_test_home("core-config-migrate-invalid"); let source = root.join("source"); let target = root.join("target"); fs::create_dir_all(&source).unwrap(); fs::create_dir_all(&target).unwrap(); - fs::write(source.join(CORE_CONFIG_FILE), "host: [invalid\n").unwrap(); + fs::write( + source.join(CORE_CONFIG_FILE), + "host: 127.0.0.1\nbroken: [invalid\nport: 9527\napi-keys:\n- old-a\n- old-b\n", + ) + .unwrap(); fs::write( target.join(CORE_EXAMPLE_CONFIG_FILE), - "host: \"\"\nport: 8317\n", + "host: \"\"\nbroken: new-default\nport: 8317\napi-keys:\n - new-default\nnew-option: true\n", ) .unwrap(); fs::write(target.join(CORE_CONFIG_FILE), "staged: untouched\n").unwrap(); - let error = migrate_core_config_for_update(&source, &target).unwrap_err(); + migrate_core_config_for_update(&source, &target).unwrap(); - assert!(error.contains("已取消更新以防止配置丢失")); - assert_eq!( - fs::read_to_string(target.join(CORE_CONFIG_FILE)).unwrap(), - "staged: untouched\n" - ); + let migrated = fs::read_to_string(target.join(CORE_CONFIG_FILE)).unwrap(); + let document = serde_norway::from_str::(&migrated).unwrap(); + assert_eq!(document["host"], "127.0.0.1"); + assert_eq!(document["broken"], "new-default"); + assert_eq!(document["port"], 9527); + assert_eq!(document["api-keys"][0], "old-a"); + assert_eq!(document["api-keys"][1], "old-b"); + assert_eq!(document["new-option"], true); fs::remove_dir_all(root).unwrap(); } From a912dd473a6282e54d67d3334e3df3fa6ce9cec2 Mon Sep 17 00:00:00 2001 From: lzt404 <2596933790@qq.com> Date: Tue, 28 Jul 2026 14:39:45 +0800 Subject: [PATCH 3/3] feat: update API access error messages and remarks handling for improved clarity --- src/i18n/ja.ts | 9 ++++----- src/i18n/resources.ts | 18 ++++++++---------- src/pages/ApiAccessPage.tsx | 32 ++++++++++++++++++++------------ tests/providerConfig.test.ts | 13 +++++++++++++ 4 files changed, 45 insertions(+), 27 deletions(-) diff --git a/src/i18n/ja.ts b/src/i18n/ja.ts index f528ce5..9aefe1e 100644 --- a/src/i18n/ja.ts +++ b/src/i18n/ja.ts @@ -522,7 +522,7 @@ export const jaOverrides = { 'apiAccess.error.headerMissingColon': 'カスタムヘッダーの {number} 行目にコロンがありません', 'apiAccess.error.headerInvalid': 'カスタムヘッダーの {number} 行目の形式が無効です', 'apiAccess.error.partialLoad': '一部の接続を読み込めませんでした:{errors}', - 'apiAccess.error.requiredAll': '名前、Base URL、キーを入力してください', + 'apiAccess.error.requiredAll': 'メモ、Base URL、キーを入力してください', 'apiAccess.error.requiredBaseKey': 'Base URL と API キーを入力してください', 'apiAccess.error.requiredKey': 'API キーを入力してください', 'apiAccess.error.baseRequired': '{provider} 接続には Base URL が必要です', @@ -532,7 +532,7 @@ export const jaOverrides = { 'apiAccess.error.remarkInvalid': 'メモは 80 文字以内で、改行や制御文字を含めないでください', 'apiAccess.notice.updated': '接続を更新しました', 'apiAccess.notice.added': '接続を追加しました', - 'apiAccess.deleteConfirm': '「{name}」を削除しますか?', + 'apiAccess.deleteConfirm': '「{remark}」を削除しますか?', 'apiAccess.notice.deleted': '接続を削除しました', 'apiAccess.notice.enabled': '接続を有効にしました', 'apiAccess.notice.disabled': '接続を無効にしました', @@ -540,7 +540,7 @@ export const jaOverrides = { 'apiAccess.count': '接続 {count} 件', 'apiAccess.add': '追加', 'apiAccess.matches': '一致する接続 {count} 件', - 'apiAccess.search': '名前、キー、アドレスを検索', + 'apiAccess.search': 'メモ、キー、アドレスを検索', 'apiAccess.loading': '設定を読み込んでいます', 'apiAccess.empty.filtered': '一致する接続がありません', 'apiAccess.empty.none': '接続設定がありません', @@ -557,7 +557,7 @@ export const jaOverrides = { 'apiAccess.disable': '接続を無効化', 'apiAccess.status.disabled': '無効', 'apiAccess.status.enabled': '有効', - 'apiAccess.toggleAria': '{name} 接続の{action}スイッチ', + 'apiAccess.toggleAria': '{remark} 接続の{action}スイッチ', 'common.edit': '編集', 'apiAccess.error.enterBaseUrl': '先に Base URL を入力してください', 'apiAccess.error.noAvailableModels': '利用可能なモデルが見つかりません', @@ -571,7 +571,6 @@ export const jaOverrides = { 'apiAccess.models.allHint': '現在、上流のすべてのモデルを公開しています', 'apiAccess.dialog.edit': 'API 接続を編集', 'apiAccess.dialog.add': 'API 接続を追加', - 'apiAccess.field.name': '名前', 'apiAccess.field.keysMany': 'API キー(1 行に 1 件)', 'apiAccess.field.key': 'API キー', 'apiAccess.field.remark': 'メモ', diff --git a/src/i18n/resources.ts b/src/i18n/resources.ts index e510e89..a67ce26 100644 --- a/src/i18n/resources.ts +++ b/src/i18n/resources.ts @@ -525,7 +525,7 @@ export const zhCN = { 'apiAccess.error.headerMissingColon': '自定义请求头第 {number} 行缺少冒号', 'apiAccess.error.headerInvalid': '自定义请求头第 {number} 行格式无效', 'apiAccess.error.partialLoad': '部分接入读取失败:{errors}', - 'apiAccess.error.requiredAll': '名称、Base URL 和密钥不能为空', + 'apiAccess.error.requiredAll': '备注、Base URL 和密钥不能为空', 'apiAccess.error.requiredBaseKey': 'Base URL 和 API 密钥不能为空', 'apiAccess.error.requiredKey': 'API 密钥不能为空', 'apiAccess.error.baseRequired': '{provider} 接入必须填写 Base URL', @@ -535,7 +535,7 @@ export const zhCN = { 'apiAccess.error.remarkInvalid': '备注不能超过 80 个字符,且不能包含换行或控制字符', 'apiAccess.notice.updated': '接入已更新', 'apiAccess.notice.added': '接入已新增', - 'apiAccess.deleteConfirm': '确定删除“{name}”吗?', + 'apiAccess.deleteConfirm': '确定删除“{remark}”吗?', 'apiAccess.notice.deleted': '接入已删除', 'apiAccess.notice.enabled': '接入已启用', 'apiAccess.notice.disabled': '接入已停用', @@ -543,7 +543,7 @@ export const zhCN = { 'apiAccess.count': '{count} 个接入', 'apiAccess.add': '新增', 'apiAccess.matches': '{count} 个匹配接入', - 'apiAccess.search': '搜索名称、密钥或地址', + 'apiAccess.search': '搜索备注、密钥或地址', 'apiAccess.loading': '读取配置中', 'apiAccess.empty.filtered': '没有匹配的接入', 'apiAccess.empty.none': '暂无接入配置', @@ -560,7 +560,7 @@ export const zhCN = { 'apiAccess.disable': '停用接入', 'apiAccess.status.disabled': '已停用', 'apiAccess.status.enabled': '已启用', - 'apiAccess.toggleAria': '{name} 接入{action}开关', + 'apiAccess.toggleAria': '{remark} 接入{action}开关', 'common.edit': '编辑', 'apiAccess.error.enterBaseUrl': '请先填写 Base URL', 'apiAccess.error.noAvailableModels': '未发现可用模型', @@ -574,7 +574,6 @@ export const zhCN = { 'apiAccess.models.allHint': '当前开放上游全部模型', 'apiAccess.dialog.edit': '编辑 API 接入', 'apiAccess.dialog.add': '新增 API 接入', - 'apiAccess.field.name': '名称', 'apiAccess.field.keysMany': 'API 密钥(每行一个)', 'apiAccess.field.key': 'API 密钥', 'apiAccess.field.remark': '备注', @@ -1341,7 +1340,7 @@ export const en: Record = { 'apiAccess.error.headerMissingColon': 'Custom header line {number} is missing a colon', 'apiAccess.error.headerInvalid': 'Custom header line {number} is invalid', 'apiAccess.error.partialLoad': 'Some connections could not be loaded: {errors}', - 'apiAccess.error.requiredAll': 'Name, Base URL, and key are required', + 'apiAccess.error.requiredAll': 'Remark, Base URL, and key are required', 'apiAccess.error.requiredBaseKey': 'Base URL and API key are required', 'apiAccess.error.requiredKey': 'API key is required', 'apiAccess.error.baseRequired': 'A Base URL is required for {provider}', @@ -1351,7 +1350,7 @@ export const en: Record = { 'apiAccess.error.remarkInvalid': 'The remark cannot exceed 80 characters or contain line breaks or control characters', 'apiAccess.notice.updated': 'Connection updated', 'apiAccess.notice.added': 'Connection added', - 'apiAccess.deleteConfirm': 'Delete “{name}”?', + 'apiAccess.deleteConfirm': 'Delete “{remark}”?', 'apiAccess.notice.deleted': 'Connection deleted', 'apiAccess.notice.enabled': 'Connection enabled', 'apiAccess.notice.disabled': 'Connection disabled', @@ -1359,7 +1358,7 @@ export const en: Record = { 'apiAccess.count': '{count} connections', 'apiAccess.add': 'Add', 'apiAccess.matches': '{count} matching connections', - 'apiAccess.search': 'Search names, keys, or addresses', + 'apiAccess.search': 'Search remarks, keys, or addresses', 'apiAccess.loading': 'Loading configuration', 'apiAccess.empty.filtered': 'No matching connections', 'apiAccess.empty.none': 'No API connections', @@ -1376,7 +1375,7 @@ export const en: Record = { 'apiAccess.disable': 'Disable connection', 'apiAccess.status.disabled': 'Disabled', 'apiAccess.status.enabled': 'Enabled', - 'apiAccess.toggleAria': '{name} connection {action} switch', + 'apiAccess.toggleAria': '{remark} connection {action} switch', 'common.edit': 'Edit', 'apiAccess.error.enterBaseUrl': 'Enter a Base URL first', 'apiAccess.error.noAvailableModels': 'No available models found', @@ -1390,7 +1389,6 @@ export const en: Record = { 'apiAccess.models.allHint': 'All upstream models are currently open', 'apiAccess.dialog.edit': 'Edit API Connection', 'apiAccess.dialog.add': 'Add API Connection', - 'apiAccess.field.name': 'Name', 'apiAccess.field.keysMany': 'API Keys (one per line)', 'apiAccess.field.key': 'API Key', 'apiAccess.field.remark': 'Remark', diff --git a/src/pages/ApiAccessPage.tsx b/src/pages/ApiAccessPage.tsx index f0b532c..835d154 100644 --- a/src/pages/ApiAccessPage.tsx +++ b/src/pages/ApiAccessPage.tsx @@ -323,7 +323,7 @@ const thinkingLevelsFromModels = (models: ModelOption[]): string[] => { const draftFromRow = (row: ProviderRow): ProviderDraft => ({ name: row.name, apiKey: definitionFor(row.section).openAi ? row.apiKeys.join('\n') : row.apiKey, - remark: row.remark, + remark: row.remark || (definitionFor(row.section).openAi ? row.name : ''), baseUrl: row.baseUrl, priority: row.priority === null ? '' : String(row.priority), models: row.models, @@ -383,11 +383,19 @@ export const createProviderDraft = (category: ProviderCategory): ProviderDraft = return { ...draft, name: 'DeepSeek', + remark: 'DeepSeek', baseUrl: DEEPSEEK_BASE_URL, thinkingLevels: [...DEEPSEEK_THINKING_LEVELS], }; }; +export const applyProviderRemarkIdentity = ( + category: ProviderCategory, + draft: ProviderDraft, +): ProviderDraft => definitionFor(category).openAi + ? { ...draft, name: draft.remark.trim() } + : draft; + export const applyProviderPreset = ( category: ProviderCategory, draft: ProviderDraft, @@ -696,7 +704,7 @@ export function ApiAccessPage() { .filter((row) => { const query = filter.trim().toLowerCase(); if (!query) return true; - return [row.name, row.remark, row.apiKey, row.baseUrl, row.models.map((model) => model.name).join(' ')] + return [row.remark || row.name, row.apiKey, row.baseUrl, row.models.map((model) => model.name).join(' ')] .join(' ') .toLowerCase() .includes(query); @@ -721,7 +729,10 @@ export function ApiAccessPage() { const saveProvider = async (nextDraft: ProviderDraft): Promise => { const definition = activeDefinition; - const preparedDraft = applyProviderPreset(activeCategory, nextDraft); + const preparedDraft = applyProviderRemarkIdentity( + activeCategory, + applyProviderPreset(activeCategory, nextDraft), + ); const baseUrlRequired = definition.openAi || definition.section === 'codex-api-key'; const parsedApiKeys = preparedDraft.apiKey .split(/\r?\n/) @@ -729,7 +740,7 @@ export function ApiAccessPage() { .filter(Boolean); if ( parsedApiKeys.length === 0 - || (definition.openAi && !preparedDraft.name.trim()) + || (definition.openAi && !preparedDraft.remark.trim()) || (baseUrlRequired && !preparedDraft.baseUrl.trim()) ) { setError( @@ -827,7 +838,7 @@ export function ApiAccessPage() { }; const deleteRow = async (row: ProviderRow) => { - if (!window.confirm(t('apiAccess.deleteConfirm', { name: row.name }))) return; + if (!window.confirm(t('apiAccess.deleteConfirm', { remark: row.remark || row.name }))) return; setBusy(true); setError(''); try { @@ -992,7 +1003,7 @@ export function ApiAccessPage() { checked={!row.disabled} onChange={() => void toggleProvider(row)} disabled={busy} - aria-label={t('apiAccess.toggleAria', { name: row.name, action: row.disabled ? t('common.enable') : t('common.disable') })} + aria-label={t('apiAccess.toggleAria', { remark: row.remark || row.name, action: row.disabled ? t('common.enable') : t('common.disable') })} /> @@ -1077,7 +1088,7 @@ function ApiProviderDialog({ && visibleModelOptions.every((model) => selectedModelNames.has(model.name.toLowerCase())); const updateTextField = ( - field: 'name' | 'apiKey' | 'remark' | 'baseUrl' | 'priority' | 'prefix' | 'headersText' | 'excludedModelsText' | 'testModel' | 'cloakMode' | 'cloakSensitiveWordsText', + field: 'apiKey' | 'remark' | 'baseUrl' | 'priority' | 'prefix' | 'headersText' | 'excludedModelsText' | 'testModel' | 'cloakMode' | 'cloakSensitiveWordsText', value: string, ) => { setDraft((current) => ({ ...current, [field]: value })); @@ -1239,18 +1250,15 @@ function ApiProviderDialog({ - {definition.openAi ? ( - - ) : null} +