perf: 模型管理界面重做
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
import {getProviderBadgeText, getProviderPresetByValue} from '../defaultIcon';
|
||||
|
||||
describe('defaultIcon helpers', () => {
|
||||
it('可以读取新的服务商预设', () => {
|
||||
const preset = getProviderPresetByValue('self-hosted');
|
||||
|
||||
expect(preset?.label).toBe('自部署');
|
||||
expect(preset?.mode).toBe('self-hosted');
|
||||
});
|
||||
|
||||
it('未知服务商类型会回退到 providerName 文本', () => {
|
||||
expect(getProviderBadgeText('历史服务商', 'legacy')).toBe('历史');
|
||||
expect(getProviderBadgeText('', 'legacy-provider')).toBe('LE');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
import {
|
||||
createProviderDraft,
|
||||
getProviderConfigMetrics,
|
||||
isProviderDraftDirty,
|
||||
} from '../providerDraft';
|
||||
|
||||
describe('providerDraft helpers', () => {
|
||||
it('相同配置不会被识别为脏数据', () => {
|
||||
const provider = {
|
||||
apiKey: 'key',
|
||||
endpoint: 'https://api.example.com',
|
||||
chatPath: '/v1/chat/completions',
|
||||
embedPath: '/v1/embeddings',
|
||||
rerankPath: '',
|
||||
};
|
||||
|
||||
expect(isProviderDraftDirty(provider, createProviderDraft(provider))).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('修改任一关键字段后会被识别为脏数据', () => {
|
||||
const provider = {
|
||||
apiKey: 'key',
|
||||
endpoint: 'https://api.example.com',
|
||||
chatPath: '/v1/chat/completions',
|
||||
embedPath: '',
|
||||
rerankPath: '',
|
||||
};
|
||||
|
||||
expect(
|
||||
isProviderDraftDirty(provider, {
|
||||
...createProviderDraft(provider),
|
||||
apiKey: 'next-key',
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('会根据预设路径计算配置完成度', () => {
|
||||
const metrics = getProviderConfigMetrics(
|
||||
{
|
||||
apiKey: 'key',
|
||||
endpoint: 'https://api.example.com',
|
||||
chatPath: '/v1/chat/completions',
|
||||
embedPath: '/v1/embeddings',
|
||||
rerankPath: '',
|
||||
},
|
||||
{
|
||||
label: '示例',
|
||||
value: 'demo',
|
||||
description: '',
|
||||
icon: '',
|
||||
mode: 'hosted',
|
||||
options: {
|
||||
chatPath: '/v1/chat/completions',
|
||||
embedPath: '/v1/embeddings',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(metrics.completed).toBe(4);
|
||||
expect(metrics.total).toBe(4);
|
||||
expect(metrics.complete).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,27 +1,66 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
import providerList from './providerList.json';
|
||||
|
||||
const providerOptions =
|
||||
ref<Array<{ icon: string; label: string; options: any; value: string }>>(
|
||||
providerList,
|
||||
);
|
||||
export interface ProviderModelPreset {
|
||||
description: string;
|
||||
label: string;
|
||||
llmModel: string;
|
||||
supportChat?: boolean;
|
||||
supportEmbed?: boolean;
|
||||
supportFunctionCalling?: boolean;
|
||||
supportRerank?: boolean;
|
||||
}
|
||||
|
||||
export interface ProviderPreset {
|
||||
description: string;
|
||||
icon: string;
|
||||
label: string;
|
||||
mode: 'hosted' | 'self-hosted';
|
||||
options: {
|
||||
chatPath?: string;
|
||||
embedPath?: string;
|
||||
llmEndpoint?: string;
|
||||
modelList?: ProviderModelPreset[];
|
||||
rerankPath?: string;
|
||||
};
|
||||
tags?: string[];
|
||||
value: string;
|
||||
}
|
||||
|
||||
const providerOptions = providerList as ProviderPreset[];
|
||||
|
||||
export const getProviderPresetByValue = (targetValue?: string) =>
|
||||
providerOptions.find((item) => item.value === targetValue);
|
||||
|
||||
/**
|
||||
* 根据传入的value,返回对应的icon属性
|
||||
* @param targetValue 要匹配的value值
|
||||
* @returns 匹配到的icon字符串,未匹配到返回空字符串
|
||||
*/
|
||||
export const getIconByValue = (targetValue: string): string => {
|
||||
const matchItem = providerOptions.value.find(
|
||||
(item) => item.value === targetValue,
|
||||
);
|
||||
|
||||
return matchItem?.icon || '';
|
||||
};
|
||||
export const getIconByValue = (targetValue: string): string =>
|
||||
getProviderPresetByValue(targetValue)?.icon || '';
|
||||
|
||||
export const isSvgString = (icon: any) => {
|
||||
if (typeof icon !== 'string') return false;
|
||||
// 简单判断:是否包含 SVG 根标签
|
||||
return icon.trim().startsWith('<svg') && icon.trim().endsWith('</svg>');
|
||||
};
|
||||
|
||||
export const getProviderBadgeText = (
|
||||
providerName?: string,
|
||||
providerType?: string,
|
||||
) => {
|
||||
const preset = getProviderPresetByValue(providerType);
|
||||
const source = providerName || preset?.label || providerType || 'AI';
|
||||
const ascii = source
|
||||
.replaceAll(/[^a-z]/gi, '')
|
||||
.slice(0, 2)
|
||||
.toUpperCase();
|
||||
|
||||
if (ascii) {
|
||||
return ascii;
|
||||
}
|
||||
|
||||
return source.replaceAll(/\s+/g, '').slice(0, 2).toUpperCase();
|
||||
};
|
||||
|
||||
export const providerPresets = providerOptions;
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import type {ProviderPreset} from './defaultIcon';
|
||||
|
||||
export const PROVIDER_EDITABLE_FIELDS = [
|
||||
'apiKey',
|
||||
'endpoint',
|
||||
'chatPath',
|
||||
'embedPath',
|
||||
'rerankPath',
|
||||
] as const;
|
||||
|
||||
export type ProviderEditableField = (typeof PROVIDER_EDITABLE_FIELDS)[number];
|
||||
|
||||
export interface ProviderDraft {
|
||||
apiKey: string;
|
||||
endpoint: string;
|
||||
chatPath: string;
|
||||
embedPath: string;
|
||||
rerankPath: string;
|
||||
}
|
||||
|
||||
type ProviderLike = Partial<Record<ProviderEditableField, string>> & {
|
||||
providerType?: string;
|
||||
};
|
||||
|
||||
export const createProviderDraft = (
|
||||
provider?: null | ProviderLike,
|
||||
): ProviderDraft => ({
|
||||
apiKey: provider?.apiKey || '',
|
||||
endpoint: provider?.endpoint || '',
|
||||
chatPath: provider?.chatPath || '',
|
||||
embedPath: provider?.embedPath || '',
|
||||
rerankPath: provider?.rerankPath || '',
|
||||
});
|
||||
|
||||
export const isProviderDraftDirty = (
|
||||
provider?: null | ProviderLike,
|
||||
draft?: null | ProviderLike,
|
||||
) =>
|
||||
PROVIDER_EDITABLE_FIELDS.some(
|
||||
(field) => (provider?.[field] || '') !== (draft?.[field] || ''),
|
||||
);
|
||||
|
||||
export const getProviderConfigMetrics = (
|
||||
provider?: null | ProviderLike,
|
||||
preset?: ProviderPreset,
|
||||
) => {
|
||||
const requiredFields: ProviderEditableField[] = [
|
||||
'apiKey',
|
||||
'endpoint',
|
||||
'chatPath',
|
||||
];
|
||||
|
||||
if (preset?.options?.embedPath || provider?.embedPath) {
|
||||
requiredFields.push('embedPath');
|
||||
}
|
||||
|
||||
if (preset?.options?.rerankPath || provider?.rerankPath) {
|
||||
requiredFields.push('rerankPath');
|
||||
}
|
||||
|
||||
const completed = requiredFields.filter((field) =>
|
||||
Boolean((provider?.[field] || '').trim()),
|
||||
).length;
|
||||
const total = requiredFields.length;
|
||||
|
||||
return {
|
||||
completed,
|
||||
total,
|
||||
ratio: total === 0 ? 0 : Math.round((completed / total) * 100),
|
||||
complete: total > 0 && completed === total,
|
||||
};
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user