feat: 新增统一模型网关与模型管理工作区
- 新增 OpenAI 兼容统一模型调用链路、模型发布配置与批量发布能力 - 重构模型管理页面入口与统一网关工作区,更新服务商 logo 资源与模型 ID 文案 - 收口全新库初始化脚本,仅保留服务商种子并整理统一网关 migration
This commit is contained in:
@@ -1,11 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type {llmType} from '#/api';
|
||||
import type {ModelAbilityItem} from '#/views/ai/model/modelUtils/model-ability';
|
||||
import {getDefaultModelAbility} from '#/views/ai/model/modelUtils/model-ability';
|
||||
import type { llmType } from '#/api';
|
||||
import type { ModelAbilityItem } from '#/views/ai/model/modelUtils/model-ability';
|
||||
|
||||
import {computed, onMounted, reactive, ref} from 'vue';
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import {CircleCheck, CircleClose, Delete, Edit, Loading, Select,} from '@element-plus/icons-vue';
|
||||
import {
|
||||
CircleCheck,
|
||||
CircleClose,
|
||||
Delete,
|
||||
Edit,
|
||||
Loading,
|
||||
Select,
|
||||
} from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElEmpty,
|
||||
@@ -20,10 +26,11 @@ import {
|
||||
ElTag,
|
||||
} from 'element-plus';
|
||||
|
||||
import {deleteLlm, getModelList, verifyModelConfig,} from '#/api/ai/llm';
|
||||
import {$t} from '#/locales';
|
||||
import { deleteLlm, getModelList, verifyModelConfig } from '#/api/ai/llm';
|
||||
import { $t } from '#/locales';
|
||||
import ModelProviderBadge from '#/views/ai/model/ModelProviderBadge.vue';
|
||||
import {mapLlmToModelAbility} from '#/views/ai/model/modelUtils/model-ability-utils';
|
||||
import { getDefaultModelAbility } from '#/views/ai/model/modelUtils/model-ability';
|
||||
import { mapLlmToModelAbility } from '#/views/ai/model/modelUtils/model-ability-utils';
|
||||
|
||||
interface ProviderOption {
|
||||
id: string;
|
||||
@@ -480,7 +487,7 @@ defineExpose({
|
||||
>
|
||||
<ElTableColumn type="selection" width="48" />
|
||||
|
||||
<ElTableColumn label="模型名称" min-width="220">
|
||||
<ElTableColumn label="模型 ID" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<div class="active-workspace__name-with-logo">
|
||||
<ModelProviderBadge
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import type {ModelAbilityItem} from '#/views/ai/model/modelUtils/model-ability';
|
||||
import type { ModelAbilityItem } from '#/views/ai/model/modelUtils/model-ability';
|
||||
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
|
||||
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||
import { IconifyIcon } from '@easyflow/icons';
|
||||
|
||||
import { ElForm, ElFormItem, ElInput, ElMessage } from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import { $t } from '#/locales';
|
||||
import {
|
||||
getDefaultModelAbility,
|
||||
syncTagSelectedStatus as syncTagSelectedStatusUtil,
|
||||
} from '#/views/ai/model/modelUtils/model-ability';
|
||||
|
||||
import {computed, reactive, ref, watch} from 'vue';
|
||||
|
||||
import {EasyFlowFormModal} from '@easyflow/common-ui';
|
||||
|
||||
import {ArrowDown, ArrowUp} from '@element-plus/icons-vue';
|
||||
import {ElForm, ElFormItem, ElIcon, ElInput, ElMessage} from 'element-plus';
|
||||
|
||||
import {api} from '#/api/request';
|
||||
import {$t} from '#/locales';
|
||||
import {
|
||||
generateFeaturesFromModelAbility,
|
||||
resetModelAbility,
|
||||
@@ -26,6 +26,8 @@ interface FormData {
|
||||
modelName: string;
|
||||
groupName: string;
|
||||
providerId: string;
|
||||
invokeCode: string;
|
||||
publishEnabled: boolean;
|
||||
apiKey: string;
|
||||
endpoint: string;
|
||||
requestPath: string;
|
||||
@@ -63,7 +65,6 @@ const formDataRef = ref();
|
||||
const isAdd = ref(true);
|
||||
const dialogVisible = ref(false);
|
||||
const btnLoading = ref(false);
|
||||
const showAdvanced = ref(false);
|
||||
|
||||
const formData = reactive<FormData>({
|
||||
modelType: '',
|
||||
@@ -71,6 +72,8 @@ const formData = reactive<FormData>({
|
||||
modelName: '',
|
||||
groupName: '',
|
||||
providerId: '',
|
||||
invokeCode: '',
|
||||
publishEnabled: false,
|
||||
apiKey: '',
|
||||
endpoint: '',
|
||||
requestPath: '',
|
||||
@@ -99,6 +102,17 @@ const modelTypeAbilityOptions = [
|
||||
},
|
||||
] as const;
|
||||
const hasSpecialModelType = computed(() => Boolean(selectedModelType.value));
|
||||
const abilityIconMap: Record<string, string> = {
|
||||
embeddingModel: 'svg:knowledge',
|
||||
rerankModel: 'svg:data-center',
|
||||
thinking: 'svg:llm',
|
||||
tool: 'svg:wrench',
|
||||
video: 'mdi:video-outline',
|
||||
image: 'mdi:image-outline',
|
||||
audio: 'mdi:microphone-outline',
|
||||
imageB64: 'mdi:file-image-outline',
|
||||
toolMessage: 'mdi:hammer',
|
||||
};
|
||||
|
||||
const syncTagSelectedStatus = () => {
|
||||
syncTagSelectedStatusUtil(modelAbility.value, formData);
|
||||
@@ -128,11 +142,20 @@ const handleModelTypeChipClick = (
|
||||
};
|
||||
|
||||
const isAbilityChipDisabled = () => hasSpecialModelType.value;
|
||||
const getAbilityIcon = (value: string) => abilityIconMap[value] || 'svg:llm';
|
||||
|
||||
const resolveModelType = (): FormData['modelType'] => {
|
||||
return selectedModelType.value || 'chatModel';
|
||||
};
|
||||
|
||||
const normalizeSelectableModelType = (
|
||||
modelType?: string,
|
||||
): SelectableModelType => {
|
||||
return modelType === 'embeddingModel' || modelType === 'rerankModel'
|
||||
? modelType
|
||||
: '';
|
||||
};
|
||||
|
||||
const resetFormData = () => {
|
||||
Object.assign(formData, {
|
||||
id: '',
|
||||
@@ -141,6 +164,8 @@ const resetFormData = () => {
|
||||
modelName: '',
|
||||
groupName: '',
|
||||
providerId: '',
|
||||
invokeCode: '',
|
||||
publishEnabled: false,
|
||||
apiKey: '',
|
||||
endpoint: '',
|
||||
requestPath: '',
|
||||
@@ -156,13 +181,16 @@ const resetFormData = () => {
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
openAddDialog() {
|
||||
openAddDialog(modelType?: string) {
|
||||
isAdd.value = true;
|
||||
formDataRef.value?.resetFields();
|
||||
resetFormData();
|
||||
showAdvanced.value = false;
|
||||
selectedModelType.value = '';
|
||||
resetAbilitySelection();
|
||||
selectedModelType.value = normalizeSelectableModelType(modelType);
|
||||
if (selectedModelType.value) {
|
||||
resetAbilitySelection();
|
||||
} else {
|
||||
syncTagSelectedStatus();
|
||||
}
|
||||
dialogVisible.value = true;
|
||||
},
|
||||
|
||||
@@ -176,6 +204,9 @@ defineExpose({
|
||||
title: item.title || '',
|
||||
modelName: item.modelName || '',
|
||||
groupName: item.groupName || '',
|
||||
providerId: item.providerId || '',
|
||||
invokeCode: item.invokeCode || '',
|
||||
publishEnabled: item.publishEnabled || false,
|
||||
endpoint: item.endpoint || '',
|
||||
requestPath: item.requestPath || '',
|
||||
apiKey: item.apiKey || '',
|
||||
@@ -189,13 +220,7 @@ defineExpose({
|
||||
supportToolMessage:
|
||||
item.supportToolMessage === undefined ? true : item.supportToolMessage,
|
||||
});
|
||||
selectedModelType.value =
|
||||
item.modelType === 'embeddingModel' || item.modelType === 'rerankModel'
|
||||
? item.modelType
|
||||
: '';
|
||||
showAdvanced.value = Boolean(
|
||||
formData.apiKey || formData.endpoint || formData.requestPath,
|
||||
);
|
||||
selectedModelType.value = normalizeSelectableModelType(item.modelType);
|
||||
if (selectedModelType.value) {
|
||||
resetAbilitySelection();
|
||||
} else {
|
||||
@@ -281,11 +306,6 @@ const save = async () => {
|
||||
class="model-modal__form"
|
||||
>
|
||||
<div class="model-modal__section">
|
||||
<div class="model-modal__section-head">
|
||||
<h4>基础信息</h4>
|
||||
<p>这些字段决定模型在列表里的展示与组织方式。</p>
|
||||
</div>
|
||||
|
||||
<ElFormItem prop="title" :label="$t('llm.title')">
|
||||
<ElInput
|
||||
v-model.trim="formData.title"
|
||||
@@ -304,81 +324,56 @@ const save = async () => {
|
||||
placeholder="例如:默认组"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</div>
|
||||
|
||||
<div class="model-modal__section">
|
||||
<div class="model-modal__section-head">
|
||||
<h4>{{ $t('llm.ability') }}</h4>
|
||||
<p>可选嵌入或重排模型类型;选择后其余能力会自动锁定。</p>
|
||||
</div>
|
||||
<div class="model-modal__ability">
|
||||
<button
|
||||
v-for="item in modelTypeAbilityOptions"
|
||||
:key="item.value"
|
||||
type="button"
|
||||
class="model-modal__ability-chip"
|
||||
:class="{ 'is-active': selectedModelType === item.value }"
|
||||
@click="handleModelTypeChipClick(item.value)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="model-modal__ability">
|
||||
<button
|
||||
v-for="item in modelAbility"
|
||||
:key="item.value"
|
||||
type="button"
|
||||
class="model-modal__ability-chip"
|
||||
:class="{
|
||||
'is-active': item.selected,
|
||||
'is-disabled': isAbilityChipDisabled(),
|
||||
}"
|
||||
:disabled="isAbilityChipDisabled()"
|
||||
@click="handleTagClick(item)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="model-modal__section">
|
||||
<button
|
||||
type="button"
|
||||
class="model-modal__advanced-toggle"
|
||||
@click="showAdvanced = !showAdvanced"
|
||||
<ElFormItem
|
||||
:label="$t('llm.ability')"
|
||||
class="model-modal__ability-item"
|
||||
>
|
||||
<div>
|
||||
<h4>高级设置</h4>
|
||||
<p>仅在需要覆写服务商默认配置时填写。</p>
|
||||
<div class="model-modal__ability-panel">
|
||||
<div class="model-modal__ability-toolbar">
|
||||
<button
|
||||
v-for="item in modelTypeAbilityOptions"
|
||||
:key="item.value"
|
||||
type="button"
|
||||
class="model-modal__ability-chip"
|
||||
:class="[
|
||||
`is-tone-${item.value}`,
|
||||
{ 'is-active': selectedModelType === item.value },
|
||||
]"
|
||||
@click="handleModelTypeChipClick(item.value)"
|
||||
>
|
||||
<IconifyIcon
|
||||
:icon="getAbilityIcon(item.value)"
|
||||
class="model-modal__ability-icon"
|
||||
/>
|
||||
{{ item.label }}
|
||||
</button>
|
||||
<span
|
||||
class="model-modal__ability-separator"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<button
|
||||
v-for="item in modelAbility"
|
||||
:key="item.value"
|
||||
type="button"
|
||||
class="model-modal__ability-chip"
|
||||
:class="{
|
||||
'is-active': item.selected,
|
||||
'is-disabled': isAbilityChipDisabled(),
|
||||
[`is-tone-${item.value}`]: true,
|
||||
}"
|
||||
:disabled="isAbilityChipDisabled()"
|
||||
@click="handleTagClick(item)"
|
||||
>
|
||||
<IconifyIcon
|
||||
:icon="getAbilityIcon(item.value)"
|
||||
class="model-modal__ability-icon"
|
||||
/>
|
||||
{{ item.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ElIcon>
|
||||
<ArrowUp v-if="showAdvanced" />
|
||||
<ArrowDown v-else />
|
||||
</ElIcon>
|
||||
</button>
|
||||
|
||||
<div v-if="showAdvanced" class="model-modal__advanced-grid">
|
||||
<ElFormItem prop="apiKey" :label="$t('llmProvider.apiKey')">
|
||||
<ElInput
|
||||
v-model.trim="formData.apiKey"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="可选,单独覆写模型密钥"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="endpoint" :label="$t('llmProvider.endpoint')">
|
||||
<ElInput
|
||||
v-model.trim="formData.endpoint"
|
||||
placeholder="可选,单独覆写 endpoint"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="requestPath" :label="$t('llm.requestPath')">
|
||||
<ElInput
|
||||
v-model.trim="formData.requestPath"
|
||||
placeholder="可选,单独覆写 requestPath"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
</div>
|
||||
</ElForm>
|
||||
</div>
|
||||
@@ -389,114 +384,136 @@ const save = async () => {
|
||||
.model-modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.model-modal__section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
padding: 20px;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
hsl(var(--surface-panel) / 98%) 0%,
|
||||
hsl(var(--surface-contrast-soft) / 94%) 100%
|
||||
);
|
||||
border: 1px solid hsl(var(--divider-faint) / 50%);
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.model-modal__section-head h4,
|
||||
.model-modal__advanced-toggle h4 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--text-strong));
|
||||
}
|
||||
|
||||
.model-modal__section-head p,
|
||||
.model-modal__advanced-toggle p {
|
||||
margin: 6px 0 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: hsl(var(--text-muted));
|
||||
gap: 12px;
|
||||
padding: 16px 18px;
|
||||
background: hsl(var(--surface-panel) / 96%);
|
||||
border: 1px solid hsl(var(--divider-faint) / 42%);
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.model-modal__form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.model-modal__ability {
|
||||
.model-modal__ability-item {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.model-modal__ability-panel {
|
||||
overflow: hidden;
|
||||
padding: 2px;
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
.model-modal__ability-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.model-modal__ability-separator {
|
||||
width: 1px;
|
||||
align-self: stretch;
|
||||
min-height: 28px;
|
||||
background: hsl(var(--divider-faint) / 62%);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.model-modal__ability-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px 14px;
|
||||
gap: 8px;
|
||||
min-height: 40px;
|
||||
padding: 9px 18px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
color: hsl(var(--text-muted));
|
||||
cursor: pointer;
|
||||
background: hsl(var(--surface-panel));
|
||||
border: 1px solid hsl(var(--divider-faint) / 68%);
|
||||
background: hsl(var(--surface-contrast-soft) / 86%);
|
||||
border: 1px solid transparent;
|
||||
border-radius: 999px;
|
||||
transition:
|
||||
transform 0.2s ease,
|
||||
border-color 0.2s ease,
|
||||
color 0.2s ease,
|
||||
background 0.2s ease;
|
||||
background 0.2s ease,
|
||||
box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.model-modal__ability-chip:hover:not(:disabled),
|
||||
.model-modal__ability-chip:focus-visible:not(:disabled) {
|
||||
color: hsl(var(--text-strong));
|
||||
border-color: hsl(var(--divider-faint));
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 10px 18px -14px hsl(var(--foreground) / 28%);
|
||||
}
|
||||
|
||||
.model-modal__ability-chip.is-active {
|
||||
color: hsl(var(--text-strong));
|
||||
background: hsl(var(--primary) / 8%);
|
||||
border-color: hsl(var(--primary) / 38%);
|
||||
border-color: hsl(var(--primary) / 18%);
|
||||
background: hsl(var(--primary) / 10%);
|
||||
box-shadow: inset 0 0 0 1px hsl(var(--primary) / 14%);
|
||||
}
|
||||
|
||||
.model-modal__ability-chip.is-disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.56;
|
||||
box-shadow: none;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.model-modal__advanced-toggle {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
background: transparent;
|
||||
border: none;
|
||||
.model-modal__ability-icon {
|
||||
font-size: 15px;
|
||||
opacity: 0.88;
|
||||
}
|
||||
|
||||
.model-modal__advanced-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
.model-modal__ability-chip.is-active.is-tone-embeddingModel,
|
||||
.model-modal__ability-chip.is-active.is-tone-thinking,
|
||||
.model-modal__ability-chip.is-active.is-tone-toolMessage {
|
||||
color: hsl(var(--primary));
|
||||
background: hsl(var(--primary) / 10%);
|
||||
border-color: hsl(var(--primary) / 18%);
|
||||
box-shadow: inset 0 0 0 1px hsl(var(--primary) / 14%);
|
||||
}
|
||||
|
||||
.model-modal__advanced-grid :deep(.el-form-item:last-child) {
|
||||
grid-column: 1 / -1;
|
||||
.model-modal__ability-chip.is-active.is-tone-rerankModel,
|
||||
.model-modal__ability-chip.is-active.is-tone-tool {
|
||||
color: hsl(var(--warning));
|
||||
background: hsl(var(--warning) / 12%);
|
||||
border-color: hsl(var(--warning) / 18%);
|
||||
box-shadow: inset 0 0 0 1px hsl(var(--warning) / 14%);
|
||||
}
|
||||
|
||||
.model-modal__ability-chip.is-active.is-tone-image,
|
||||
.model-modal__ability-chip.is-active.is-tone-imageB64 {
|
||||
color: hsl(var(--success));
|
||||
background: hsl(var(--success) / 12%);
|
||||
border-color: hsl(var(--success) / 18%);
|
||||
box-shadow: inset 0 0 0 1px hsl(var(--success) / 14%);
|
||||
}
|
||||
|
||||
.model-modal__ability-chip.is-active.is-tone-audio,
|
||||
.model-modal__ability-chip.is-active.is-tone-video,
|
||||
.model-modal__ability-chip.is-active.is-tone-free {
|
||||
color: hsl(var(--danger));
|
||||
background: hsl(var(--danger) / 10%);
|
||||
border-color: hsl(var(--danger) / 16%);
|
||||
box-shadow: inset 0 0 0 1px hsl(var(--danger) / 12%);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.model-modal__advanced-grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.model-modal__advanced-grid :deep(.el-form-item:last-child) {
|
||||
grid-column: auto;
|
||||
.model-modal__ability-separator {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import {computed, onMounted, ref} from 'vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
import {$t} from '@easyflow/locales';
|
||||
import { $t } from '@easyflow/locales';
|
||||
|
||||
import {Delete, Edit, Plus, Select, Setting} from '@element-plus/icons-vue';
|
||||
import { Delete, Edit, Plus, Select, Setting } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElEmpty,
|
||||
@@ -15,15 +15,15 @@ import {
|
||||
ElTag,
|
||||
} from 'element-plus';
|
||||
|
||||
import {getLlmProviderList} from '#/api/ai/llm.js';
|
||||
import {api} from '#/api/request.js';
|
||||
import { getLlmProviderList } from '#/api/ai/llm.js';
|
||||
import { api } from '#/api/request.js';
|
||||
import ListPageShell from '#/components/page/ListPageShell.vue';
|
||||
import ActiveModelWorkspace from '#/views/ai/model/ActiveModelWorkspace.vue';
|
||||
import AddModelModal from '#/views/ai/model/AddModelModal.vue';
|
||||
import AddModelProviderModal from '#/views/ai/model/AddModelProviderModal.vue';
|
||||
import ModelProviderBadge from '#/views/ai/model/ModelProviderBadge.vue';
|
||||
import {getProviderPresetByValue} from '#/views/ai/model/modelUtils/defaultIcon';
|
||||
import {modelTypes} from '#/views/ai/model/modelUtils/modelTypes';
|
||||
import { getProviderPresetByValue } from '#/views/ai/model/modelUtils/defaultIcon';
|
||||
import { modelTypes } from '#/views/ai/model/modelUtils/modelTypes';
|
||||
import {
|
||||
createProviderDraft,
|
||||
getProviderConfigMetrics,
|
||||
@@ -31,8 +31,9 @@ import {
|
||||
} from '#/views/ai/model/modelUtils/providerDraft';
|
||||
import ModelVerifyConfig from '#/views/ai/model/ModelVerifyConfig.vue';
|
||||
import ModelViewItemOperation from '#/views/ai/model/ModelViewItemOperation.vue';
|
||||
import UnifiedGatewayWorkspace from '#/views/ai/model/UnifiedGatewayWorkspace.vue';
|
||||
|
||||
type ModelWorkspaceView = 'active' | 'provider';
|
||||
type ModelWorkspaceView = 'active' | 'gateway' | 'provider';
|
||||
|
||||
interface ModelGroup {
|
||||
groupName: string;
|
||||
@@ -59,6 +60,7 @@ const addLlmProviderRef = ref();
|
||||
const llmVerifyConfigRef = ref();
|
||||
const addLlmRef = ref();
|
||||
const activeWorkspaceRef = ref();
|
||||
const unifiedGatewayWorkspaceRef = ref();
|
||||
|
||||
const selectedProvider = computed(() =>
|
||||
providers.value.find((item) => item.id === selectedProviderId.value),
|
||||
@@ -194,7 +196,10 @@ const loadProviderDetail = async (
|
||||
isDetailLoading.value = true;
|
||||
|
||||
try {
|
||||
const res = await api.get(`/api/v1/model/getList?providerId=${providerId}`, {});
|
||||
const res = await api.get(
|
||||
`/api/v1/model/getList?providerId=${providerId}`,
|
||||
{},
|
||||
);
|
||||
|
||||
if (res.errorCode === 0) {
|
||||
syncGroupedModels(res.data);
|
||||
@@ -282,15 +287,30 @@ const switchWorkspaceView = async (target: ModelWorkspaceView) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await confirmDiscardProviderDraft())) {
|
||||
if (
|
||||
workspaceView.value === 'provider' &&
|
||||
!(await confirmDiscardProviderDraft())
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (workspaceView.value === 'gateway') {
|
||||
const canLeaveGateway =
|
||||
(await unifiedGatewayWorkspaceRef.value?.confirmBeforeLeave?.()) ?? true;
|
||||
if (!canLeaveGateway) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
workspaceView.value = target;
|
||||
|
||||
if (target === 'active') {
|
||||
await activeWorkspaceRef.value?.reloadData?.();
|
||||
}
|
||||
|
||||
if (target === 'gateway') {
|
||||
await unifiedGatewayWorkspaceRef.value?.reloadData?.();
|
||||
}
|
||||
};
|
||||
|
||||
const selectProvider = async (provider: any) => {
|
||||
@@ -357,6 +377,7 @@ const handleDeleteProvider = async (provider: any) => {
|
||||
const remaining = providers.value.find((item) => item.id !== provider.id);
|
||||
await loadProviders(remaining?.id, true);
|
||||
await activeWorkspaceRef.value?.reloadData?.();
|
||||
await unifiedGatewayWorkspaceRef.value?.reloadData?.();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -375,6 +396,7 @@ const openEditProviderDialog = () => {
|
||||
const handleProviderModalReload = async () => {
|
||||
await loadProviders(selectedProviderId.value, true);
|
||||
await activeWorkspaceRef.value?.reloadData?.();
|
||||
await unifiedGatewayWorkspaceRef.value?.reloadData?.();
|
||||
};
|
||||
|
||||
const handleAddLlm = (modelType = activeModelType.value) => {
|
||||
@@ -444,6 +466,7 @@ const handleModelDataReload = async () => {
|
||||
keepDraft: isProviderDirty.value,
|
||||
});
|
||||
await activeWorkspaceRef.value?.reloadData?.();
|
||||
await unifiedGatewayWorkspaceRef.value?.reloadData?.();
|
||||
await refreshProviderCounts(providers.value);
|
||||
};
|
||||
|
||||
@@ -481,6 +504,14 @@ onMounted(() => {
|
||||
>
|
||||
已配置模型
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="workspace-switch__item"
|
||||
:class="{ 'is-active': workspaceView === 'gateway' }"
|
||||
@click="switchWorkspaceView('gateway')"
|
||||
>
|
||||
统一网关
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -770,7 +801,7 @@ onMounted(() => {
|
||||
</section>
|
||||
|
||||
<ActiveModelWorkspace
|
||||
v-else
|
||||
v-else-if="workspaceView === 'active'"
|
||||
ref="activeWorkspaceRef"
|
||||
:providers="providers"
|
||||
@create-model="handleAddLlm"
|
||||
@@ -778,6 +809,12 @@ onMounted(() => {
|
||||
@refresh-provider-stats="handleActiveProviderStatsRefresh"
|
||||
/>
|
||||
|
||||
<UnifiedGatewayWorkspace
|
||||
v-else
|
||||
ref="unifiedGatewayWorkspaceRef"
|
||||
@updated="handleModelDataReload"
|
||||
/>
|
||||
|
||||
<AddModelProviderModal
|
||||
ref="addLlmProviderRef"
|
||||
@reload="handleProviderModalReload"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import {computed} from 'vue';
|
||||
import { computed } from 'vue';
|
||||
|
||||
import {ElImage} from 'element-plus';
|
||||
import { ElImage } from 'element-plus';
|
||||
|
||||
import {
|
||||
getIconByValue,
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import type {PropType} from 'vue';
|
||||
import {ref} from 'vue';
|
||||
import type { PropType } from 'vue';
|
||||
|
||||
import type {llmType} from '#/api';
|
||||
import type {ModelAbilityItem} from '#/views/ai/model/modelUtils/model-ability';
|
||||
import {getDefaultModelAbility} from '#/views/ai/model/modelUtils/model-ability';
|
||||
import type { llmType } from '#/api';
|
||||
import type { ModelAbilityItem } from '#/views/ai/model/modelUtils/model-ability';
|
||||
|
||||
import {CircleCheck, CircleClose, Delete, Edit, Loading, Select,} from '@element-plus/icons-vue';
|
||||
import {ElButton, ElIcon, ElMessage, ElTag} from 'element-plus';
|
||||
import { ref } from 'vue';
|
||||
|
||||
import {verifyModelConfig} from '#/api/ai/llm';
|
||||
import {
|
||||
CircleCheck,
|
||||
CircleClose,
|
||||
Delete,
|
||||
Edit,
|
||||
Loading,
|
||||
Select,
|
||||
} from '@element-plus/icons-vue';
|
||||
import { ElButton, ElIcon, ElMessage, ElTag } from 'element-plus';
|
||||
|
||||
import { verifyModelConfig } from '#/api/ai/llm';
|
||||
import ModelProviderBadge from '#/views/ai/model/ModelProviderBadge.vue';
|
||||
import {mapLlmToModelAbility} from '#/views/ai/model/modelUtils/model-ability-utils';
|
||||
import { getDefaultModelAbility } from '#/views/ai/model/modelUtils/model-ability';
|
||||
import { mapLlmToModelAbility } from '#/views/ai/model/modelUtils/model-ability-utils';
|
||||
|
||||
const props = defineProps({
|
||||
llmList: {
|
||||
@@ -115,14 +123,6 @@ const getSelectedAbilityTagsForLlm = (llm: llmType): ModelAbilityItem[] => {
|
||||
const allTags = mapLlmToModelAbility(llm, defaultAbility);
|
||||
return allTags.filter((tag) => tag.selected);
|
||||
};
|
||||
|
||||
const getModelMeta = (llm: llmType) => {
|
||||
const providerName =
|
||||
llm?.modelProvider?.providerName || llm?.aiLlmProvider?.providerName || '';
|
||||
const modelName = llm.llmModel || llm.title;
|
||||
|
||||
return `${providerName} · ${modelName}`;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -153,7 +153,6 @@ const getModelMeta = (llm: llmType) => {
|
||||
</ElTag>
|
||||
</div>
|
||||
</div>
|
||||
<p class="llm-item__meta">{{ getModelMeta(llm) }}</p>
|
||||
<p v-if="llm.description" class="llm-item__description">
|
||||
{{ llm.description }}
|
||||
</p>
|
||||
@@ -263,7 +262,6 @@ const getModelMeta = (llm: llmType) => {
|
||||
color: hsl(var(--text-strong));
|
||||
}
|
||||
|
||||
.llm-item__meta,
|
||||
.llm-item__description {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
|
||||
1260
easyflow-ui-admin/app/src/views/ai/model/UnifiedGatewayWorkspace.vue
Normal file
1260
easyflow-ui-admin/app/src/views/ai/model/UnifiedGatewayWorkspace.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,20 @@
|
||||
import {describe, expect, it} from 'vitest';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {getProviderBadgeText, getProviderPresetByValue} from '../defaultIcon';
|
||||
import { getProviderBadgeText, getProviderPresetByValue } from '../defaultIcon';
|
||||
|
||||
describe('defaultIcon helpers', () => {
|
||||
it('使用 lobehub 官方静态 svg 作为默认服务商 logo', () => {
|
||||
expect(getProviderPresetByValue('openai')?.icon).toBe(
|
||||
'/model-providers/lobehub/openai.svg',
|
||||
);
|
||||
expect(getProviderPresetByValue('deepseek')?.icon).toBe(
|
||||
'/model-providers/lobehub/deepseek.svg',
|
||||
);
|
||||
expect(getProviderPresetByValue('self-hosted')?.icon).toBe(
|
||||
'/model-providers/lobehub/vllm.svg',
|
||||
);
|
||||
});
|
||||
|
||||
it('可以读取新的服务商预设', () => {
|
||||
const preset = getProviderPresetByValue('self-hosted');
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import { $t } from '#/locales';
|
||||
|
||||
export type BooleanField =
|
||||
| 'supportAudio'
|
||||
| 'supportFree'
|
||||
| 'supportImage'
|
||||
| 'supportImageB64Only'
|
||||
| 'supportThinking'
|
||||
@@ -56,14 +55,6 @@ export const getDefaultModelAbility = (): ModelAbilityItem[] => [
|
||||
selected: false,
|
||||
field: 'supportImage',
|
||||
},
|
||||
{
|
||||
label: $t('llm.modelAbility.supportFree'),
|
||||
value: 'free',
|
||||
defaultType: 'info',
|
||||
activeType: 'success',
|
||||
selected: false,
|
||||
field: 'supportFree',
|
||||
},
|
||||
{
|
||||
label: $t('llm.modelAbility.supportAudio'),
|
||||
value: 'audio',
|
||||
@@ -126,7 +117,6 @@ export const syncTagSelectedStatus = (
|
||||
|
||||
/**
|
||||
* 处理标签点击事件
|
||||
* @param modelAbility 模型能力数组
|
||||
* @param item 被点击的标签项
|
||||
* @param formData 表单数据对象
|
||||
*/
|
||||
@@ -165,5 +155,4 @@ export const getAllBooleanFields = (): BooleanField[] => [
|
||||
'supportImageB64Only',
|
||||
'supportVideo',
|
||||
'supportAudio',
|
||||
'supportFree',
|
||||
];
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"icon": "/model-providers/deepseek.png"
|
||||
"icon": "/model-providers/lobehub/deepseek.svg"
|
||||
},
|
||||
{
|
||||
"label": "OpenAI",
|
||||
@@ -62,7 +62,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"icon": "/model-providers/openai.svg"
|
||||
"icon": "/model-providers/lobehub/openai.svg"
|
||||
},
|
||||
{
|
||||
"label": "阿里百炼",
|
||||
@@ -98,7 +98,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"icon": "/model-providers/aliyun.png"
|
||||
"icon": "/model-providers/lobehub/bailian.svg"
|
||||
},
|
||||
{
|
||||
"label": "智谱",
|
||||
@@ -133,7 +133,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"icon": "/model-providers/zhipu.png"
|
||||
"icon": "/model-providers/lobehub/zhipu.svg"
|
||||
},
|
||||
{
|
||||
"label": "MiniMax",
|
||||
@@ -161,7 +161,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"icon": "/model-providers/minimax.png"
|
||||
"icon": "/model-providers/lobehub/minimax.svg"
|
||||
},
|
||||
{
|
||||
"label": "Kimi",
|
||||
@@ -189,7 +189,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"icon": "/model-providers/kimi.png"
|
||||
"icon": "/model-providers/lobehub/kimi.svg"
|
||||
},
|
||||
{
|
||||
"label": "硅基流动",
|
||||
@@ -225,7 +225,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"icon": "/model-providers/siliconflow.png"
|
||||
"icon": "/model-providers/lobehub/siliconcloud.svg"
|
||||
},
|
||||
{
|
||||
"label": "Ollama",
|
||||
@@ -253,7 +253,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"icon": "/model-providers/ollama.png"
|
||||
"icon": "/model-providers/lobehub/ollama.svg"
|
||||
},
|
||||
{
|
||||
"label": "自部署",
|
||||
@@ -288,6 +288,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"icon": "/model-providers/self-hosted.svg"
|
||||
"icon": "/model-providers/lobehub/vllm.svg"
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user