Merge pull request #69 from router-for-me/dev

Dev
This commit is contained in:
lzt404 authored and GitHub committed 2026-07-28 14:40:48 +08:00
commit b5599cd1c8
5 files changed
+247 -44

No files matched your search

+202 -17
View File
@@ -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(
@@ -10526,19 +10526,124 @@ fn merge_core_config_yaml(
template: &str,
current: Option<&str>,
config: &GuiConfigFile,
) -> Result<String, String> {
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<String, String> {
let current_value = current
.map(|current| {
let current = serde_norway::from_str::<serde_norway::Value>(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<String, String> {
let current = match serde_norway::from_str::<serde_norway::Value>(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<serde_norway::Value>,
) -> Result<String, String> {
let template_value = serde_norway::from_str::<serde_norway::Value>(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::<serde_norway::Value>(current) {
merge_yaml_values(&mut merged, current);
merge_yaml_values(&mut merged, current);
}
let rendered = render_yaml_value_changes(template, &template_value, &merged)?;
let rendered_value = serde_norway::from_str::<serde_norway::Value>(&rendered)
.map_err(|err| format!("验证迁移后的内核配置失败: {err}"))?;
if !rendered_value.is_mapping() {
return Err("迁移后的内核配置根节点必须是 YAML 映射".to_string());
}
Ok(rendered)
}
fn recover_parseable_top_level_yaml_fields(content: &str) -> 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::<Vec<_>>();
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::<serde_norway::Value>(&content[start..end]) else {
continue;
};
let Some(mapping) = value.as_mapping() else {
continue;
};
for (key, value) in mapping {
recovered.insert(key.clone(), value.clone());
}
}
let merged = render_yaml_value_changes(template, &template_value, &merged)?;
apply_gui_managed_settings(&merged, config)
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(
@@ -11628,15 +11733,46 @@ 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 = 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_tolerant(&template, &old_config)?;
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 +16508,67 @@ 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::<serde_norway::Value>(&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_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: 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: \"\"\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();
migrate_core_config_for_update(&source, &target).unwrap();
let migrated = fs::read_to_string(target.join(CORE_CONFIG_FILE)).unwrap();
let document = serde_norway::from_str::<serde_norway::Value>(&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();
}
+4 -5
View File
@@ -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': 'メモ',
+8 -10
View File
@@ -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<MessageKey, string> = {
'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<MessageKey, string> = {
'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<MessageKey, string> = {
'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<MessageKey, string> = {
'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<MessageKey, string> = {
'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',
+20 -12
View File
@@ -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<boolean> => {
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') })}
/>
<span className="switch-track" />
</span>
@@ -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({
<X size={18} />
</button>
</div>
{definition.openAi ? (
<label><span>{t('apiAccess.field.name')}</span><input autoFocus value={draft.name} onChange={(event) => updateTextField('name', event.currentTarget.value)} placeholder="openrouter" /></label>
) : null}
<label><span>{t('apiAccess.field.remark')}</span><input autoFocus={definition.openAi} value={draft.remark} maxLength={80} onChange={(event) => updateTextField('remark', event.currentTarget.value)} placeholder={t('apiAccess.remarkPlaceholder')} /></label>
<label className={definition.openAi ? 'multiline-field' : undefined}>
<span>{definition.openAi ? t('apiAccess.field.keysMany') : t('apiAccess.field.key')}</span>
{definition.openAi ? (
<textarea value={draft.apiKey} onChange={(event) => updateTextField('apiKey', event.currentTarget.value)} placeholder={'sk-...\nsk-...'} rows={3} />
) : (
<input autoFocus type="password" value={draft.apiKey} onChange={(event) => updateTextField('apiKey', event.currentTarget.value)} placeholder="sk-..." />
<input autoFocus={!definition.openAi} type="password" value={draft.apiKey} onChange={(event) => updateTextField('apiKey', event.currentTarget.value)} placeholder="sk-..." />
)}
</label>
<label><span>{t('apiAccess.field.remark')}</span><input value={draft.remark} maxLength={80} onChange={(event) => updateTextField('remark', event.currentTarget.value)} placeholder={t('apiAccess.remarkPlaceholder')} /></label>
<label><span>Base URL</span><input value={draft.baseUrl} onChange={(event) => updateTextField('baseUrl', event.currentTarget.value)} placeholder={activeSection === 'codex-api-key' || activeSection === 'openai-compatibility' ? t('apiAccess.baseRequiredPlaceholder') : t('apiAccess.baseOptionalPlaceholder')} /></label>
{activeCategory === 'deepseek' ? (
<div className="provider-preset-summary">
+13
View File
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'bun:test';
import {
allModelSelectionForDiscovery,
applyProviderRemarkIdentity,
applyProviderPreset,
buildProviderRecord,
createProviderDraft,
@@ -43,6 +44,7 @@ describe('API 接入配置合并', () => {
const result = buildProviderRecord('openai-compatibility', prepared);
expect(draft.name).toBe('DeepSeek');
expect(draft.remark).toBe('DeepSeek');
expect(draft.baseUrl).toBe(DEEPSEEK_BASE_URL);
expect(draft.models).toEqual([]);
expect(result).toMatchObject({
@@ -66,6 +68,17 @@ describe('API 接入配置合并', () => {
});
});
it('OpenAI 兼容接入使用备注自动生成内核要求的名称', () => {
const draft = applyProviderRemarkIdentity('openai-compatibility', {
...createProviderDraft('openai-compatibility'),
name: '不再使用的名称',
remark: '生产环境',
});
expect(draft.remark).toBe('生产环境');
expect(draft.name).toBe('生产环境');
});
it('DeepSeek 接入单独归类,不在 OpenAI 兼容列表重复显示', () => {
const record = {
name: 'custom-deepseek',