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
This commit is contained in:
lzt404 committed 2026-08-02 16:14:25 +08:00
1 parent 1a33b006e2
commit efc8811d0c
11 files changed
+265 -34

No files matched your search

+147 -13
View File
@@ -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<String, String> {
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::<Vec<_>>();
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"]);
+3 -2
View File
@@ -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': 'モデル一覧を読み込んでいます…',
+6 -4
View File
@@ -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<MessageKey, string> = {
'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<MessageKey, string> = {
'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<MessageKey, string> = {
'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…',
+26 -1
View File
@@ -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() {
<input
type="checkbox"
checked={claudeDesktopCustomMapping}
onChange={(event) => setClaudeDesktopCustomMapping(event.currentTarget.checked)}
onChange={(event) => changeClaudeDesktopCustomMapping(event.currentTarget.checked)}
disabled={busy || modelLoading}
/>
<span className="switch-track" />
+5 -4
View File
@@ -1022,10 +1022,11 @@ export function ApiAccessPage() {
<span className="provider-row-url" title={row.baseUrl || undefined}>{row.baseUrl || t('apiAccess.defaultUrl')}</span>
{row.models.length > 0 ? <span className="provider-row-models" title={row.models.map((model) => model.name).join(', ')}>{t('apiAccess.models.summary', { count: row.models.length, models: row.models.slice(0, 3).map((model) => model.name).join(', ') })}</span> : null}
</div>
<div className="provider-row-meta">
{row.priority === null ? null : <span>{t('apiAccess.priorityValue', { priority: row.priority })}</span>}
{row.authIndex ? <span title={row.authIndex}>{t('apiAccess.runtimeCredential', { index: row.authIndex.slice(0, 8) })}</span> : null}
</div>
{row.priority === null ? null : (
<div className="provider-row-meta">
<span>{t('apiAccess.priorityValue', { priority: row.priority })}</span>
</div>
)}
<div className="provider-row-actions">
<button
type="button"
+30 -8
View File
@@ -22,6 +22,7 @@ import {
} from 'lucide-react';
import { useCoreRuntime, type CoreStatus } from '../coreRuntime';
import { useI18n } from '../i18n';
import { webUiManagementUrl } from '../services/clientAccess';
import { ThinkingAliasesPage } from './ThinkingAliasesPage';
type CoreConfigSettings = {
@@ -322,6 +323,16 @@ export function ConfigPanelPage() {
}
};
const openWebUi = async () => {
try {
await invoke('open_external_url', {
url: webUiManagementUrl(settings?.port ?? 8317),
});
} catch (error) {
showNotice(t('config.webuiKey.error.openFailed', { error: String(error) }), 'error');
}
};
const changeRoutingStrategy = async (strategy: string) => {
if (strategy === settings?.routingStrategy) {
return;
@@ -573,15 +584,26 @@ export function ConfigPanelPage() {
<ShieldCheck size={18} aria-hidden="true" />
<h2>{t('config.webuiKey.title')}</h2>
</div>
{!loading && settings ? (
<span
className={`state-pill ${settings.managementSecretConfigured ? 'success' : ''}`}
<div className="config-heading-actions">
{!loading && settings ? (
<span
className={`state-pill ${settings.managementSecretConfigured ? 'success' : ''}`}
>
{settings.managementSecretConfigured
? t('config.webuiKey.configured')
: t('config.webuiKey.unconfigured')}
</span>
) : null}
<button
type="button"
className="secondary-button compact-button"
disabled={loading || settings === null}
onClick={() => void openWebUi()}
title={settings ? webUiManagementUrl(settings.port) : undefined}
>
{settings.managementSecretConfigured
? t('config.webuiKey.configured')
: t('config.webuiKey.unconfigured')}
</span>
) : null}
{t('config.webuiKey.open')}
</button>
</div>
</div>
<div className="config-management-content">
+20
View File
@@ -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;
+8 -1
View File
@@ -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;
+5
View File
@@ -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;
+5
View File
@@ -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('');
});
});
+10 -1
View File
@@ -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',
);
});
});