From efc8811d0ce6545737f43f69c6ad144c97601029 Mon Sep 17 00:00:00 2001 From: lzt404 <2596933790@qq.com> Date: Sun, 2 Aug 2026 16:14:25 +0800 Subject: [PATCH] feat: refine WebUI access and Claude Desktop model mappings - add a shortcut to open the management WebUI - hide redundant runtime credential identifiers - simplify health check descriptions - use existing Claude model aliases directly - preserve context capabilities from original models --- src-tauri/src/main.rs | 160 ++++++++++++++++++++++++++++--- src/i18n/ja.ts | 5 +- src/i18n/resources.ts | 10 +- src/pages/AgentsPage.tsx | 27 +++++- src/pages/ApiAccessPage.tsx | 9 +- src/pages/ConfigPanel.tsx | 38 ++++++-- src/services/agentModelPicker.ts | 20 ++++ src/services/clientAccess.ts | 9 +- src/styles.css | 5 + tests/agentModelPicker.test.ts | 5 + tests/clientAccess.test.ts | 11 ++- 11 files changed, 265 insertions(+), 34 deletions(-) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index e2c428d..069ec4a 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -2158,7 +2158,7 @@ async fn apply_agent_config( claude_desktop_model_mappings, )?; if let Some(mappings) = claude_desktop_model_mappings.as_ref() { - ensure_claude_desktop_model_aliases(&config, mappings).await?; + ensure_claude_desktop_model_aliases(&config, mappings, &prepared.models).await?; } let _guard = AGENT_CONFIG_FILE_LOCK .lock() @@ -2224,7 +2224,7 @@ async fn reset_agent_config_to_default( claude_desktop_model_mappings, )?; if let Some(mappings) = claude_desktop_model_mappings.as_ref() { - ensure_claude_desktop_model_aliases(&config, mappings).await?; + ensure_claude_desktop_model_aliases(&config, mappings, &prepared.models).await?; } let _guard = AGENT_CONFIG_FILE_LOCK .lock() @@ -2285,7 +2285,7 @@ async fn set_agent_config_enabled( claude_desktop_model_mappings, )?; if let Some(mappings) = claude_desktop_model_mappings.as_ref() { - ensure_claude_desktop_model_aliases(&config, mappings).await?; + ensure_claude_desktop_model_aliases(&config, mappings, &prepared.models).await?; } let _guard = AGENT_CONFIG_FILE_LOCK .lock() @@ -2337,7 +2337,7 @@ async fn update_agent_config( claude_desktop_model_mappings, )?; if let Some(mappings) = claude_desktop_model_mappings.as_ref() { - ensure_claude_desktop_model_aliases(&config, mappings).await?; + ensure_claude_desktop_model_aliases(&config, mappings, &prepared.models).await?; } let _guard = AGENT_CONFIG_FILE_LOCK .lock() @@ -4736,11 +4736,18 @@ fn build_claude_desktop_profile( let mappings = mappings .cloned() .unwrap_or_else(|| ClaudeDesktopModelMappings::all(model)); - let inference_models = vec![ + let mut inference_models = vec![ claude_desktop_inference_model(CLAUDE_DESKTOP_OPUS_MODEL_ID, &mappings.opus, models), claude_desktop_inference_model(CLAUDE_DESKTOP_SONNET_MODEL_ID, &mappings.sonnet, models), claude_desktop_inference_model(CLAUDE_DESKTOP_HAIKU_MODEL_ID, &mappings.haiku, models), ]; + let mut seen_models = HashSet::new(); + inference_models.retain(|entry| { + entry + .get("name") + .and_then(serde_json::Value::as_str) + .is_some_and(|name| seen_models.insert(name.to_ascii_lowercase())) + }); root.insert( "inferenceModels".to_string(), serde_json::Value::Array(inference_models), @@ -4753,12 +4760,34 @@ fn claude_desktop_inference_model( source_model: &str, models: &[AgentModelOption], ) -> serde_json::Value { - let context_window = models + let selected = models .iter() - .find(|model| model.name.eq_ignore_ascii_case(source_model)) - .and_then(|model| model.context_window); + .find(|model| model.name.eq_ignore_ascii_case(source_model)); + let direct_alias = selected.is_some_and(|model| model.is_alias); + let context_model = selected.and_then(|model| { + if !model.is_alias { + return Some(model); + } + model + .alias + .as_deref() + .and_then(|source| { + models + .iter() + .find(|candidate| candidate.name.eq_ignore_ascii_case(source)) + }) + .or(Some(model)) + }); + let context_window = context_model.and_then(|model| model.context_window); let mut entry = serde_json::Map::new(); - entry.insert("name".to_string(), serde_json::json!(route_model)); + entry.insert( + "name".to_string(), + serde_json::json!(if direct_alias { + source_model + } else { + route_model + }), + ); if context_window.is_some_and(|window| window >= CLAUDE_DESKTOP_EXTENDED_CONTEXT_WINDOW) { entry.insert("supports1m".to_string(), serde_json::json!(true)); entry.insert("prefer1m".to_string(), serde_json::json!(true)); @@ -11776,9 +11805,10 @@ async fn put_management_config_yaml(config: &GuiConfigFile, content: &str) -> Re async fn ensure_claude_desktop_model_aliases( config: &GuiConfigFile, mappings: &ClaudeDesktopModelMappings, + models: &[AgentModelOption], ) -> Result<(), String> { let content = fetch_management_config_yaml(config).await?; - let updated = ensure_claude_desktop_model_aliases_in_yaml(&content, mappings)?; + let updated = ensure_claude_desktop_model_aliases_in_yaml(&content, mappings, models)?; if updated != content { put_management_config_yaml(config, &updated).await?; } @@ -11788,6 +11818,7 @@ async fn ensure_claude_desktop_model_aliases( fn ensure_claude_desktop_model_aliases_in_yaml( content: &str, mappings: &ClaudeDesktopModelMappings, + models: &[AgentModelOption], ) -> Result { let mut document = yaml_serde_edit::YamlValue::parse(content) .map_err(|error| format!("解析内核 YAML 配置失败: {error}"))?; @@ -11801,11 +11832,50 @@ fn ensure_claude_desktop_model_aliases_in_yaml( (CLAUDE_DESKTOP_SONNET_MODEL_ID, mappings.sonnet.as_str()), (CLAUDE_DESKTOP_HAIKU_MODEL_ID, mappings.haiku.as_str()), ] { - ensure_claude_desktop_model_alias(root, source_model, alias)?; + let direct_alias = models + .iter() + .any(|model| model.name.eq_ignore_ascii_case(source_model) && model.is_alias); + if direct_alias { + if !source_model.eq_ignore_ascii_case(alias) { + remove_claude_desktop_model_alias(root, alias)?; + } + } else { + ensure_claude_desktop_model_alias(root, source_model, alias)?; + } } render_updated_core_yaml(&mut document, updated) } +fn remove_claude_desktop_model_alias( + root: &mut serde_norway::Mapping, + alias: &str, +) -> Result<(), String> { + for section in MODEL_ALIAS_CONFIG_SECTIONS { + let Some(providers) = yaml_mapping_value_mut(root, section) else { + continue; + }; + let providers = providers + .as_sequence_mut() + .ok_or_else(|| format!("{section} 必须是数组"))?; + for provider in providers { + let Some(provider) = provider.as_mapping_mut() else { + continue; + }; + let Some(models) = yaml_mapping_value_mut(provider, "models") else { + continue; + }; + let models = models + .as_sequence_mut() + .ok_or_else(|| format!("{section}.models 必须是数组"))?; + models.retain(|model| { + configured_model_identity(model) + .is_none_or(|(_, client_model, _)| !client_model.eq_ignore_ascii_case(alias)) + }); + } + } + Ok(()) +} + fn ensure_claude_desktop_model_alias( root: &mut serde_norway::Mapping, source_model: &str, @@ -18842,7 +18912,10 @@ model: sonnet: "gpt-5.6-sol".to_string(), haiku: "gpt-5.6-sol".to_string(), }; - let rendered = ensure_claude_desktop_model_aliases_in_yaml(input, &mappings).unwrap(); + let available_models = test_agent_models(&["gpt-5.6-sol"]); + let rendered = + ensure_claude_desktop_model_aliases_in_yaml(input, &mappings, &available_models) + .unwrap(); let value: serde_norway::Value = serde_norway::from_str(&rendered).unwrap(); let root = value.as_mapping().unwrap(); let providers = yaml_mapping_value(root, "openai-compatibility") @@ -18866,11 +18939,72 @@ model: CLAUDE_DESKTOP_HAIKU_MODEL_ID ); assert_eq!( - ensure_claude_desktop_model_aliases_in_yaml(&rendered, &mappings).unwrap(), + ensure_claude_desktop_model_aliases_in_yaml(&rendered, &mappings, &available_models,) + .unwrap(), rendered ); } + #[test] + fn claude_desktop_uses_selected_alias_directly_with_original_context() { + let input = "openai-compatibility:\n - name: CPA\n base-url: https://example.com/v1\n models:\n - name: gpt-original\n - name: gpt-original\n alias: gpt-high\n - name: gpt-original\n alias: claude-opus-5\n"; + let mappings = ClaudeDesktopModelMappings { + opus: "gpt-high".to_string(), + sonnet: "gpt-high".to_string(), + haiku: "gpt-high".to_string(), + }; + let models = vec![ + AgentModelOption { + name: "gpt-original".to_string(), + alias: None, + is_alias: false, + context_window: Some(1_000_000), + }, + AgentModelOption { + name: "gpt-high".to_string(), + alias: Some("gpt-original".to_string()), + is_alias: true, + context_window: Some(128_000), + }, + ]; + + let rendered = + ensure_claude_desktop_model_aliases_in_yaml(input, &mappings, &models).unwrap(); + let value: serde_norway::Value = serde_norway::from_str(&rendered).unwrap(); + let root = value.as_mapping().unwrap(); + let providers = yaml_mapping_value(root, "openai-compatibility") + .and_then(serde_norway::Value::as_sequence) + .unwrap(); + let configured_models = yaml_mapping_value(providers[0].as_mapping().unwrap(), "models") + .and_then(serde_norway::Value::as_sequence) + .unwrap(); + let client_models = configured_models + .iter() + .filter_map(configured_model_identity) + .map(|(_, client_model, _)| client_model) + .collect::>(); + assert_eq!(client_models, vec!["gpt-original", "gpt-high"]); + + let profile = build_claude_desktop_profile( + None, + "http://127.0.0.1:8317", + DEFAULT_API_KEY, + "gpt-high", + &models, + Some(&mappings), + ) + .unwrap(); + let profile: serde_json::Value = serde_json::from_str(&profile).unwrap(); + assert_eq!( + profile["inferenceModels"], + serde_json::json!([{ + "name": "gpt-high", + "supports1m": true, + "prefer1m": true + }]) + ); + } + #[test] fn claude_desktop_role_mappings_can_use_different_available_models() { let models = test_agent_models(&["gpt-5.6-sol", "deepseek-chat", "gemini-3-pro"]); diff --git a/src/i18n/ja.ts b/src/i18n/ja.ts index a0a9188..20dfad6 100644 --- a/src/i18n/ja.ts +++ b/src/i18n/ja.ts @@ -293,11 +293,13 @@ export const jaOverrides = { 'config.webuiKey.confirmPlaceholder': '新しいキーをもう一度入力', 'config.webuiKey.generate': '安全なキーを生成', 'config.webuiKey.save': 'キーを保存', + 'config.webuiKey.open': 'WebUI を開く', 'config.webuiKey.error.empty': 'WebUI キーを入力してください', 'config.webuiKey.error.legacyDefault': '旧版の既定キー 123456 は使用できません', 'config.webuiKey.error.tooLong': 'WebUI キーは 512 文字以内で入力してください', 'config.webuiKey.error.invalid': 'WebUI キーに制御文字は使用できません', 'config.webuiKey.error.mismatch': '入力した WebUI キーが一致しません', + 'config.webuiKey.error.openFailed': 'WebUI を開けませんでした:{error}', 'config.webuiKey.notice.updated': 'WebUI キーを更新しました', 'config.keys.title': '認証キー', 'config.keys.count': 'キー数', @@ -622,7 +624,6 @@ export const jaOverrides = { 'apiAccess.defaultUrl': '既定のアドレスを使用', 'apiAccess.models.summary': 'モデル {count} 件 · {models}', 'apiAccess.priorityValue': '優先度 {priority}', - 'apiAccess.runtimeCredential': '実行時認証情報 {index}…', 'apiAccess.enable': '接続を有効化', 'apiAccess.disable': '接続を無効化', 'apiAccess.status.disabled': '無効', @@ -630,7 +631,7 @@ export const jaOverrides = { 'apiAccess.toggleAria': '{remark} 接続の{action}スイッチ', 'apiAccess.health.action': 'ヘルスチェック', 'apiAccess.health.title': 'モデルヘルスチェック', - 'apiAccess.health.description': '{provider} · 各モデルのエンドポイントを検証', + 'apiAccess.health.description': '{provider}', 'apiAccess.health.warning': '各モデルへ最小限のストリーミングリクエストを 1 回送信し、最初の出力文字を受信した時点で停止します。少額の料金が発生する場合があります。', 'apiAccess.health.search': 'モデル名またはエイリアスを検索', 'apiAccess.health.loadingModels': 'モデル一覧を読み込んでいます…', diff --git a/src/i18n/resources.ts b/src/i18n/resources.ts index 9d184a9..0b7fa2f 100644 --- a/src/i18n/resources.ts +++ b/src/i18n/resources.ts @@ -296,11 +296,13 @@ export const zhCN = { 'config.webuiKey.confirmPlaceholder': '再次输入新密钥', 'config.webuiKey.generate': '生成安全密钥', 'config.webuiKey.save': '保存密钥', + 'config.webuiKey.open': '打开 WebUI', 'config.webuiKey.error.empty': 'WebUI 密钥不能为空', 'config.webuiKey.error.legacyDefault': '不能继续使用旧版默认密钥 123456', 'config.webuiKey.error.tooLong': 'WebUI 密钥不能超过 512 个字符', 'config.webuiKey.error.invalid': 'WebUI 密钥不能包含控制字符', 'config.webuiKey.error.mismatch': '两次输入的 WebUI 密钥不一致', + 'config.webuiKey.error.openFailed': '无法打开 WebUI:{error}', 'config.webuiKey.notice.updated': 'WebUI 密钥已更新', 'config.keys.title': '鉴权密钥', 'config.keys.count': '密钥数量', @@ -625,7 +627,6 @@ export const zhCN = { 'apiAccess.defaultUrl': '使用默认地址', 'apiAccess.models.summary': '模型 {count} 个 · {models}', 'apiAccess.priorityValue': '优先级 {priority}', - 'apiAccess.runtimeCredential': '运行时凭据 {index}…', 'apiAccess.enable': '启用接入', 'apiAccess.disable': '停用接入', 'apiAccess.status.disabled': '已停用', @@ -633,7 +634,7 @@ export const zhCN = { 'apiAccess.toggleAria': '{remark} 接入{action}开关', 'apiAccess.health.action': '健康检测', 'apiAccess.health.title': '模型健康检测', - 'apiAccess.health.description': '{provider} · 逐个验证模型接口', + 'apiAccess.health.description': '{provider}', 'apiAccess.health.warning': '检测会对每个模型发送一次最小流式请求,收到模型首字后立即停止,并可能产生少量费用。', 'apiAccess.health.search': '搜索模型名称或别名', 'apiAccess.health.loadingModels': '正在获取模型列表…', @@ -1210,11 +1211,13 @@ export const en: Record = { 'config.webuiKey.confirmPlaceholder': 'Enter the new key again', 'config.webuiKey.generate': 'Generate secure key', 'config.webuiKey.save': 'Save key', + 'config.webuiKey.open': 'Open WebUI', 'config.webuiKey.error.empty': 'The WebUI key cannot be empty', 'config.webuiKey.error.legacyDefault': 'The legacy default key 123456 is no longer allowed', 'config.webuiKey.error.tooLong': 'The WebUI key cannot exceed 512 characters', 'config.webuiKey.error.invalid': 'The WebUI key cannot contain control characters', 'config.webuiKey.error.mismatch': 'The WebUI keys do not match', + 'config.webuiKey.error.openFailed': 'Could not open the WebUI: {error}', 'config.webuiKey.notice.updated': 'WebUI key updated', 'config.keys.title': 'Authentication Keys', 'config.keys.count': 'Key count', @@ -1539,7 +1542,6 @@ export const en: Record = { 'apiAccess.defaultUrl': 'Using default address', 'apiAccess.models.summary': '{count} models · {models}', 'apiAccess.priorityValue': 'Priority {priority}', - 'apiAccess.runtimeCredential': 'Runtime credential {index}…', 'apiAccess.enable': 'Enable connection', 'apiAccess.disable': 'Disable connection', 'apiAccess.status.disabled': 'Disabled', @@ -1547,7 +1549,7 @@ export const en: Record = { 'apiAccess.toggleAria': '{remark} connection {action} switch', 'apiAccess.health.action': 'Health Check', 'apiAccess.health.title': 'Model Health Check', - 'apiAccess.health.description': '{provider} · Validate each model endpoint', + 'apiAccess.health.description': '{provider}', 'apiAccess.health.warning': 'The check sends one minimal streaming request to each model, stops at the first output token, and may incur a small charge.', 'apiAccess.health.search': 'Search model names or aliases', 'apiAccess.health.loadingModels': 'Loading the model list…', diff --git a/src/pages/AgentsPage.tsx b/src/pages/AgentsPage.tsx index 8a81e42..6ffa1c4 100644 --- a/src/pages/AgentsPage.tsx +++ b/src/pages/AgentsPage.tsx @@ -36,6 +36,7 @@ import { filterAgentModels, filterAgentModelsByAlias, findAgentModel, + resolveAgentModelForAliasMode, resolveAgentModelSelection, } from '../services/agentModelPicker'; import type { ModelOption } from '../services/modelService'; @@ -655,6 +656,14 @@ export function AgentsPage() { useEffect(() => { if (selected !== 'claude-desktop' || !selectedModel) return; const appliedMappings = activeStatus?.claudeDesktopModelMappings; + if (!claudeDesktopModelMappingsDirtyRef.current && appliedMappings) { + const appliedModels = claudeDesktopMappingRoles + .map((role) => findAgentModel(models, appliedMappings[role.key])) + .filter((model): model is ModelOption => model !== null); + if (appliedModels.length === claudeDesktopMappingRoles.length) { + setClaudeDesktopCustomMapping(appliedModels.every((model) => Boolean(model.isAlias))); + } + } setClaudeDesktopModelMappingsDraft((current) => { const source = claudeDesktopModelMappingsDirtyRef.current ? current @@ -755,6 +764,22 @@ export function AgentsPage() { setClaudeDesktopModelMappingsDraft((current) => ({ ...current, [role]: model.name })); }; + const changeClaudeDesktopCustomMapping = (enabled: boolean) => { + setClaudeDesktopCustomMapping(enabled); + setModelSelectionError(''); + setClaudeDesktopModelMappingsDraft((current) => { + const next: ClaudeDesktopModelMappings = { + opus: resolveAgentModelForAliasMode(models, current.opus, enabled), + sonnet: resolveAgentModelForAliasMode(models, current.sonnet, enabled), + haiku: resolveAgentModelForAliasMode(models, current.haiku, enabled), + }; + if (!sameClaudeDesktopModelMappings(current, next)) { + claudeDesktopModelMappingsDirtyRef.current = true; + } + return next; + }); + }; + const requireSelectedModel = () => { if (modelLoading) { setModelSelectionError(t('agents.error.modelsLoading')); @@ -1123,7 +1148,7 @@ export function AgentsPage() { setClaudeDesktopCustomMapping(event.currentTarget.checked)} + onChange={(event) => changeClaudeDesktopCustomMapping(event.currentTarget.checked)} disabled={busy || modelLoading} /> diff --git a/src/pages/ApiAccessPage.tsx b/src/pages/ApiAccessPage.tsx index 9a13893..f479aaf 100644 --- a/src/pages/ApiAccessPage.tsx +++ b/src/pages/ApiAccessPage.tsx @@ -1022,10 +1022,11 @@ export function ApiAccessPage() { {row.baseUrl || t('apiAccess.defaultUrl')} {row.models.length > 0 ? model.name).join(', ')}>{t('apiAccess.models.summary', { count: row.models.length, models: row.models.slice(0, 3).map((model) => model.name).join(', ') })} : null} -
- {row.priority === null ? null : {t('apiAccess.priorityValue', { priority: row.priority })}} - {row.authIndex ? {t('apiAccess.runtimeCredential', { index: row.authIndex.slice(0, 8) })} : null} -
+ {row.priority === null ? null : ( +
+ {t('apiAccess.priorityValue', { priority: row.priority })} +
+ )}
- {!loading && settings ? ( - + {!loading && settings ? ( + + {settings.managementSecretConfigured + ? t('config.webuiKey.configured') + : t('config.webuiKey.unconfigured')} + + ) : null} + +
diff --git a/src/services/agentModelPicker.ts b/src/services/agentModelPicker.ts index 9f44aae..18f1e10 100644 --- a/src/services/agentModelPicker.ts +++ b/src/services/agentModelPicker.ts @@ -35,6 +35,26 @@ export function filterAgentModelsByAlias( return models.filter((model) => Boolean(model.isAlias) === aliasesOnly); } +export function resolveAgentModelForAliasMode( + models: ModelOption[], + current: string, + aliasesOnly: boolean, +): string { + const candidates = filterAgentModelsByAlias(models, aliasesOnly); + const selected = findAgentModel(models, current); + if (selected && Boolean(selected.isAlias) === aliasesOnly) return selected.name; + + if (selected && aliasesOnly) { + const alias = candidates.find((candidate) => normalized(candidate.alias ?? '') === normalized(selected.name)); + if (alias) return alias.name; + } + if (selected?.isAlias && !aliasesOnly) { + const original = candidates.find((candidate) => normalized(candidate.name) === normalized(selected.alias ?? '')); + if (original) return original.name; + } + return candidates[0]?.name ?? ''; +} + export function hasExactAgentModel(models: ModelOption[], value: string): boolean { const query = normalized(value); if (!query) return false; diff --git a/src/services/clientAccess.ts b/src/services/clientAccess.ts index 5b6be58..2973900 100644 --- a/src/services/clientAccess.ts +++ b/src/services/clientAccess.ts @@ -8,8 +8,15 @@ export type ClientApiProfile = { lanUrl: string | null; }; +const safeLocalPort = (port: number) => + Number.isInteger(port) && port >= 1 && port <= 65535 ? port : 8317; + +export function webUiManagementUrl(port: number): string { + return `http://127.0.0.1:${safeLocalPort(port)}/management.html#/login`; +} + export function clientApiProfiles(port: number, lanIpv4?: string | null): ClientApiProfile[] { - const safePort = Number.isInteger(port) && port >= 1 && port <= 65535 ? port : 8317; + const safePort = safeLocalPort(port); const origin = `http://127.0.0.1:${safePort}`; const lanOrigin = lanIpv4?.trim() ? `http://${lanIpv4.trim()}:${safePort}` : null; diff --git a/src/styles.css b/src/styles.css index d38fc13..b6c5867 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1929,6 +1929,11 @@ textarea:focus-visible { flex: none; } +.config-heading-actions .compact-button { + width: auto; + margin-top: 0; +} + .config-panel-heading .state-pill { max-width: 108px; flex: none; diff --git a/tests/agentModelPicker.test.ts b/tests/agentModelPicker.test.ts index 8fd282f..889aa7c 100644 --- a/tests/agentModelPicker.test.ts +++ b/tests/agentModelPicker.test.ts @@ -5,6 +5,7 @@ import { filterAgentModelsByAlias, findAgentModel, hasExactAgentModel, + resolveAgentModelForAliasMode, resolveAgentModelSelection, } from '../src/services/agentModelPicker'; @@ -62,5 +63,9 @@ describe('智能体模型选择器', () => { .toEqual(['gpt-original', 'claude-original']); expect(filterAgentModelsByAlias(mixedModels, true).map((model) => model.name)) .toEqual(['gpt-high']); + expect(resolveAgentModelForAliasMode(mixedModels, 'gpt-original', true)).toBe('gpt-high'); + expect(resolveAgentModelForAliasMode(mixedModels, 'gpt-high', false)).toBe('gpt-original'); + expect(resolveAgentModelForAliasMode(mixedModels, 'claude-original', true)).toBe('gpt-high'); + expect(resolveAgentModelForAliasMode([], 'gpt-original', true)).toBe(''); }); }); diff --git a/tests/clientAccess.test.ts b/tests/clientAccess.test.ts index 51b7e1a..b799bef 100644 --- a/tests/clientAccess.test.ts +++ b/tests/clientAccess.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'bun:test'; -import { clientApiProfiles } from '../src/services/clientAccess'; +import { clientApiProfiles, webUiManagementUrl } from '../src/services/clientAccess'; describe('客户端 API 接入信息', () => { it('生成 OpenAI、Claude 和 Gemini 三种正确格式', () => { @@ -20,4 +20,13 @@ describe('客户端 API 接入信息', () => { expect(openai.baseUrl).toBe('http://127.0.0.1:8317/v1'); expect(openai.lanUrl).toBeNull(); }); + + it('使用当前内核端口生成 WebUI 登录地址', () => { + expect(webUiManagementUrl(9527)).toBe( + 'http://127.0.0.1:9527/management.html#/login', + ); + expect(webUiManagementUrl(0)).toBe( + 'http://127.0.0.1:8317/management.html#/login', + ); + }); });