init
This commit is contained in:
281
src/views/knowledge/aiModel/batch-form.vue
Normal file
281
src/views/knowledge/aiModel/batch-form.vue
Normal file
@@ -0,0 +1,281 @@
|
||||
<template>
|
||||
<el-dialog title="批量新增AI模型" v-model="visible" :close-on-click-modal="false" draggable width="600px">
|
||||
<el-form ref="dataFormRef" :model="form" :rules="dataRules" v-loading="loading">
|
||||
<el-form-item prop="apiKey" class="relative">
|
||||
<el-input
|
||||
v-model="form.apiKey"
|
||||
placeholder="请输入API Key(格式:sk-开头,长度51位)"
|
||||
type="textarea"
|
||||
show-word-limit
|
||||
maxlength="51"
|
||||
rows="2"
|
||||
/>
|
||||
<div class="absolute -bottom-5 right-2 text-xs z-10 bg-white/90 px-2 py-1 rounded">
|
||||
<a href="https://cloud.siliconflow.cn/i/YKcJJTYP" target="_blank" class="text-blue-500 hover:text-blue-700 no-underline text-xs">获取【硅基流动】 免费 ApiKey</a>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-card class="mt-4">
|
||||
<template #header>
|
||||
<span>将要创建的模型(共{{ previewModels.length }}个)</span>
|
||||
</template>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<el-tag v-for="model in previewModels" :key="model.type" class="mb-2 mr-2" type="primary">
|
||||
{{ model.typeLabel }}: {{ model.modelName }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="onSubmit" :disabled="loading">批量创建</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="BatchAiModelDialog">
|
||||
import { useMessage } from '/@/hooks/message';
|
||||
import { addObj, details } from '/@/api/knowledge/aiModel';
|
||||
import { ref, reactive, nextTick } from 'vue';
|
||||
|
||||
const emit = defineEmits(['refresh']);
|
||||
|
||||
// 定义变量内容
|
||||
const dataFormRef = ref();
|
||||
const visible = ref(false);
|
||||
const loading = ref(false);
|
||||
|
||||
// 提交表单数据
|
||||
const form = reactive({
|
||||
apiKey: ''
|
||||
});
|
||||
|
||||
// 定义校验规则
|
||||
const dataRules = ref({
|
||||
apiKey: [
|
||||
{ required: true, message: '请输入API Key', trigger: 'blur' },
|
||||
{
|
||||
validator: (rule: any, value: any, callback: any) => {
|
||||
if (!value) {
|
||||
callback(new Error('请输入API Key'));
|
||||
return;
|
||||
}
|
||||
if (!value.startsWith('sk-')) {
|
||||
callback(new Error('API Key必须以sk-开头'));
|
||||
return;
|
||||
}
|
||||
if (value.length !== 51) {
|
||||
callback(new Error('API Key长度必须为51位'));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// 预览要创建的模型
|
||||
const previewModels = ref([
|
||||
{
|
||||
type: 'Chat',
|
||||
typeLabel: '聊天',
|
||||
modelName: 'moonshotai/Kimi-K2-Instruct',
|
||||
name: 'kimi-k2',
|
||||
provider: 'Siliconflow',
|
||||
baseUrl: 'https://api.siliconflow.cn/v1'
|
||||
},
|
||||
{
|
||||
type: 'Chat',
|
||||
typeLabel: '聊天',
|
||||
modelName: 'deepseek-ai/DeepSeek-V3',
|
||||
name: 'deepseek-chat',
|
||||
provider: 'Siliconflow',
|
||||
baseUrl: 'https://api.siliconflow.cn/v1'
|
||||
},
|
||||
{
|
||||
type: 'Embedding',
|
||||
typeLabel: '向量',
|
||||
modelName: 'Qwen/Qwen3-Embedding-8B',
|
||||
name: 'qwen-embedding',
|
||||
provider: 'Siliconflow',
|
||||
baseUrl: 'https://api.siliconflow.cn/v1'
|
||||
},
|
||||
{
|
||||
type: 'Reranker',
|
||||
typeLabel: '排序',
|
||||
modelName: 'Qwen/Qwen3-Reranker-8B',
|
||||
name: 'qwen-reranker',
|
||||
provider: 'Siliconflow',
|
||||
baseUrl: 'https://api.siliconflow.cn/v1'
|
||||
},
|
||||
{
|
||||
type: 'Image',
|
||||
typeLabel: '图片',
|
||||
modelName: 'Kwai-Kolors/Kolors',
|
||||
name: 'kolors-image',
|
||||
provider: 'Siliconflow',
|
||||
baseUrl: 'https://api.siliconflow.cn/v1'
|
||||
},
|
||||
{
|
||||
type: 'Reason',
|
||||
typeLabel: '推理',
|
||||
modelName: 'MiniMaxAI/MiniMax-M1-80k',
|
||||
name: 'minimax-reason',
|
||||
provider: 'Siliconflow',
|
||||
baseUrl: 'https://api.siliconflow.cn/v1'
|
||||
},
|
||||
{
|
||||
type: 'Vision',
|
||||
typeLabel: '视觉',
|
||||
modelName: 'Qwen/Qwen2.5-VL-72B-Instruct',
|
||||
name: 'qwen-vision',
|
||||
provider: 'Siliconflow',
|
||||
baseUrl: 'https://api.siliconflow.cn/v1'
|
||||
},
|
||||
{
|
||||
type: 'Video',
|
||||
typeLabel: '视频',
|
||||
modelName: 'Wan-AI/Wan2.1-I2V-14B-720P',
|
||||
name: 'wan-video',
|
||||
provider: 'Siliconflow',
|
||||
baseUrl: 'https://api.siliconflow.cn/v1'
|
||||
},
|
||||
{
|
||||
type: 'Voice',
|
||||
typeLabel: '音频',
|
||||
modelName: 'RVC-Boss/GPT-SoVITS',
|
||||
name: 'gpt-sovits',
|
||||
provider: 'Siliconflow',
|
||||
baseUrl: 'https://api.siliconflow.cn/v1'
|
||||
}
|
||||
]);
|
||||
|
||||
// 检查模型名称是否存在
|
||||
const checkModelExists = async (name: string): Promise<boolean> => {
|
||||
try {
|
||||
const response = await details({ name });
|
||||
return response.data !== null && response.data.length > 0;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 打开弹窗
|
||||
const openDialog = () => {
|
||||
visible.value = true;
|
||||
|
||||
// 重置表单数据
|
||||
nextTick(() => {
|
||||
dataFormRef.value?.resetFields();
|
||||
form.apiKey = '';
|
||||
});
|
||||
};
|
||||
|
||||
// 提交
|
||||
const onSubmit = async () => {
|
||||
const valid = await dataFormRef.value.validate().catch(() => {});
|
||||
if (!valid) return false;
|
||||
|
||||
try {
|
||||
loading.value = true;
|
||||
|
||||
let successCount = 0;
|
||||
let failedCount = 0;
|
||||
let skipCount = 0;
|
||||
const errors: string[] = [];
|
||||
const skipped: string[] = [];
|
||||
|
||||
// 使用单个API Key为每个模型创建
|
||||
for (const model of previewModels.value) {
|
||||
try {
|
||||
// 先检查模型名称是否存在
|
||||
const exists = await checkModelExists(model.name);
|
||||
if (exists) {
|
||||
skipCount++;
|
||||
skipped.push(`${model.typeLabel}模型(${model.name})已存在,跳过创建`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const modelData = {
|
||||
modelType: model.type,
|
||||
modelName: model.modelName,
|
||||
name: model.name,
|
||||
provider: model.provider,
|
||||
baseUrl: model.baseUrl,
|
||||
apiKey: form.apiKey.trim(),
|
||||
defaultModel: '1', // 非默认模型
|
||||
extData: '{}',
|
||||
// 为Chat类型设置默认参数
|
||||
...(model.type === 'Chat' && {
|
||||
responseLimit: 2048,
|
||||
temperature: 0.4,
|
||||
topP: 0.7
|
||||
}),
|
||||
// 为Image类型设置默认参数
|
||||
...(model.type === 'Image' && {
|
||||
imageSize: '1024x1024',
|
||||
imageQuality: 'standard',
|
||||
imageStyle: 'natural'
|
||||
})
|
||||
};
|
||||
|
||||
await addObj(modelData);
|
||||
successCount++;
|
||||
} catch (err: any) {
|
||||
failedCount++;
|
||||
errors.push(`${model.typeLabel}模型(${model.modelName})创建失败: ${err.msg || '未知错误'}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 显示结果
|
||||
const resultMessages = [];
|
||||
if (successCount > 0) {
|
||||
resultMessages.push(`成功创建${successCount}个AI模型`);
|
||||
}
|
||||
if (skipCount > 0) {
|
||||
resultMessages.push(`跳过${skipCount}个已存在的模型`);
|
||||
}
|
||||
if (failedCount > 0) {
|
||||
resultMessages.push(`失败${failedCount}个`);
|
||||
}
|
||||
|
||||
if (resultMessages.length > 0) {
|
||||
useMessage().success(resultMessages.join(','));
|
||||
}
|
||||
|
||||
// 显示跳过的模型信息
|
||||
if (skipped.length > 0 && skipped.length <= 3) {
|
||||
skipped.forEach(msg => useMessage().warning(msg));
|
||||
} else if (skipped.length > 3) {
|
||||
useMessage().warning(`共跳过${skipCount}个已存在的模型`);
|
||||
}
|
||||
|
||||
// 显示错误信息
|
||||
if (errors.length > 0 && errors.length <= 3) {
|
||||
errors.forEach(error => useMessage().error(error));
|
||||
} else if (errors.length > 3) {
|
||||
useMessage().error(`批量创建完成,有${failedCount}个模型创建失败`);
|
||||
}
|
||||
|
||||
if (successCount > 0 || skipCount > 0) {
|
||||
visible.value = false;
|
||||
emit('refresh');
|
||||
}
|
||||
} catch (err: any) {
|
||||
useMessage().error(err.msg || '批量创建失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 暴露变量
|
||||
defineExpose({
|
||||
openDialog,
|
||||
});
|
||||
</script>
|
||||
392
src/views/knowledge/aiModel/form.vue
Normal file
392
src/views/knowledge/aiModel/form.vue
Normal file
@@ -0,0 +1,392 @@
|
||||
<template>
|
||||
<el-dialog :title="form.id ? '编辑' : '新增'" v-model="visible" :close-on-click-modal="false" draggable>
|
||||
<el-form ref="dataFormRef" :model="form" :rules="dataRules" formDialogRef label-width="90px" v-loading="loading">
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="12" class="mb20">
|
||||
<el-form-item label="供应商" prop="provider">
|
||||
<el-select v-model="form.provider" placeholder="请选择供应商" @change="handleProviderChange">
|
||||
<el-option v-for="provider in providers" :key="provider.value" :label="provider.label" :value="provider.value"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12" class="mb20">
|
||||
<el-form-item label="类型" prop="modelType">
|
||||
<el-select v-model="form.modelType" placeholder="请选择类型">
|
||||
<el-option v-for="type in modelTypes" :key="type.value" :label="type.label" :value="type.value"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12" class="mb20">
|
||||
<el-form-item prop="modelName">
|
||||
<template #label>模型<tip content="模型平台的标准名称,请选择对应模型"></tip> </template>
|
||||
<el-select v-model="form.modelName" placeholder="请选择模型名称" filterable allow-create>
|
||||
<el-option v-for="model in filteredModels" :key="model.model" :label="model.model" :value="model.model"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12" class="mb20">
|
||||
<el-form-item prop="name">
|
||||
<template #label
|
||||
>别名
|
||||
<tip content="模型别名,方便本平台后续调用"></tip>
|
||||
</template>
|
||||
<el-input v-model="form.name" placeholder="请输入模型别名" :disabled="form.id !== ''" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<template v-if="form.modelType === 'Image'">
|
||||
<el-col :span="12" class="mb20">
|
||||
<el-form-item label="图片大小" prop="imageSize">
|
||||
<el-select v-model="form.imageSize" placeholder="请选择图片大小">
|
||||
<el-option v-for="size in imageSizes" :key="size" :label="size" :value="size"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12" class="mb20">
|
||||
<el-form-item label="图片质量" prop="imageQuality">
|
||||
<el-input v-model="form.imageQuality" placeholder="请入图片质量" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12" class="mb20">
|
||||
<el-form-item label="图片风格" prop="imageStyle">
|
||||
<el-input v-model="form.imageStyle" placeholder="请输入图片风格" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</template>
|
||||
|
||||
<el-col :span="12" class="mb20">
|
||||
<el-form-item label="apiKey" prop="apiKey">
|
||||
<template #label
|
||||
>apiKey
|
||||
<tip content="Olama 随意填写API KEY,但不能为空"></tip>
|
||||
</template>
|
||||
<el-input v-model="form.apiKey" placeholder="请输入 apiKey" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12" class="mb20">
|
||||
<el-form-item label="baseUrl" prop="baseUrl">
|
||||
<el-input v-model="form.baseUrl" placeholder="请输入 baseUrl" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<template v-if="form.modelType === 'Chat'">
|
||||
<el-col :span="12" class="mb20">
|
||||
<el-form-item label="响应限制" prop="responseLimit">
|
||||
<div class="w-full form-control">
|
||||
<input type="range" min="0" max="4096" v-model="form.responseLimit" class="range range-primary" />
|
||||
<div class="flex justify-between px-2 w-full text-xs">
|
||||
<span>0</span>
|
||||
<span>4096</span>
|
||||
</div>
|
||||
<div class="mt-2 text-center">
|
||||
<span class="badge">{{ form.responseLimit }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12" class="mb20">
|
||||
<el-form-item label="随机性" prop="temperature">
|
||||
<div class="w-full form-control">
|
||||
<input type="range" min="0" max="1" step="0.1" v-model="form.temperature" class="range range-primary" />
|
||||
<div class="flex justify-between px-2 w-full text-xs">
|
||||
<span>0</span>
|
||||
<span>1</span>
|
||||
</div>
|
||||
<div class="mt-2 text-center">
|
||||
<span class="badge">{{ form.temperature }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12" class="mb20">
|
||||
<el-form-item label="顶层概率" prop="topP">
|
||||
<div class="w-full form-control">
|
||||
<input type="range" min="0" max="1" step="0.1" v-model="form.topP" class="range range-primary" />
|
||||
<div class="flex justify-between px-2 w-full text-xs">
|
||||
<span>0</span>
|
||||
<span>0.5</span>
|
||||
<span>1</span>
|
||||
</div>
|
||||
<div class="mt-2 text-center">
|
||||
<span class="badge">{{ form.topP }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</template>
|
||||
|
||||
<template v-if="form.modelType === 'Embedding' && availableDimensions.length > 0">
|
||||
<el-col :span="12" class="mb20">
|
||||
<el-form-item label="维度" prop="dimensions">
|
||||
<el-select v-model="form.dimensions" placeholder="请选择维度">
|
||||
<el-option v-for="dimension in availableDimensions" :key="dimension" :label="dimension" :value="dimension"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</template>
|
||||
</el-row>
|
||||
|
||||
<el-row>
|
||||
<el-col :span="24" class="mb20">
|
||||
<el-form-item prop="extData">
|
||||
<template #label
|
||||
>特殊参数
|
||||
<tip content="部分模型参考文档配置" />
|
||||
</template>
|
||||
<json-editor ref="jsonEditorRef" v-model="form.extData" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="onSubmit" :disabled="loading">确认</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="AiModelDialog">
|
||||
import { useMessage } from '/@/hooks/message';
|
||||
import { getObj, addObj, putObj, validateExist } from '/@/api/knowledge/aiModel';
|
||||
import { ref, reactive, watch, nextTick, computed } from 'vue';
|
||||
import { rule } from '/@/utils/validate';
|
||||
import { modelTypes, providers, providerModels, providerBaseURLMap } from './model';
|
||||
// @ts-ignore
|
||||
import JsonEditor from '@axolo/json-editor-vue';
|
||||
|
||||
const emit = defineEmits(['refresh']);
|
||||
|
||||
// 定义变量内容
|
||||
const dataFormRef = ref();
|
||||
const visible = ref(false);
|
||||
const loading = ref(false);
|
||||
|
||||
// 提交表单数据
|
||||
const form = reactive({
|
||||
id: '',
|
||||
modelType: '',
|
||||
modelName: '',
|
||||
provider: '',
|
||||
name: '',
|
||||
responseLimit: 2048,
|
||||
temperature: 0.4,
|
||||
topP: 0.7,
|
||||
apiKey: '',
|
||||
baseUrl: '',
|
||||
secretKey: '',
|
||||
endpoint: '',
|
||||
azureDeploymentName: '',
|
||||
geminiProject: '',
|
||||
geminiLocation: '',
|
||||
imageSize: '',
|
||||
imageQuality: '',
|
||||
imageStyle: '',
|
||||
defaultModel: '1',
|
||||
extData: `{}`,
|
||||
dimensions: '',
|
||||
});
|
||||
|
||||
// 可用模型列表
|
||||
const availableModels = ref<{ type: string; model: string; dimensions?: number[] }[]>([]);
|
||||
|
||||
// 计算当前选择模型的可用维度
|
||||
const availableDimensions = computed(() => {
|
||||
if (form.modelType !== 'Embedding' || !form.modelName) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const currentModel = availableModels.value.find(model =>
|
||||
model.model === form.modelName && model.type === 'Embedding'
|
||||
);
|
||||
|
||||
return currentModel?.dimensions || [];
|
||||
});
|
||||
|
||||
// 处理供应商变化
|
||||
const handleProviderChange = (provider: string) => {
|
||||
availableModels.value = providerModels[provider as keyof typeof providerModels] || [];
|
||||
};
|
||||
|
||||
// 监听 provider 变化,自动更新 baseURL
|
||||
watch(
|
||||
() => form.provider,
|
||||
(newProvider) => {
|
||||
form.baseUrl = providerBaseURLMap[newProvider as keyof typeof providerBaseURLMap] || '';
|
||||
handleProviderChange(newProvider);
|
||||
}
|
||||
);
|
||||
|
||||
// 定义校验规则
|
||||
const dataRules = ref({
|
||||
modelType: [
|
||||
{ required: true, message: '请输入模型类型', trigger: 'blur' },
|
||||
{ validator: rule.overLength, trigger: 'blur' },
|
||||
],
|
||||
modelName: [
|
||||
{ required: true, message: '请输入模型名称', trigger: 'blur' },
|
||||
{ validator: rule.overLength, trigger: 'blur' },
|
||||
],
|
||||
provider: [
|
||||
{ required: true, message: '请输入供应商', trigger: 'blur' },
|
||||
{ validator: rule.overLength, trigger: 'blur' },
|
||||
],
|
||||
name: [
|
||||
{ required: true, message: '请输入模型别名', trigger: 'blur' },
|
||||
{ max: 100, message: '模型别名最多100个字符', trigger: 'blur' },
|
||||
{
|
||||
validator: (rule: any, value: any, callback: any) => validateExist(rule, value, callback, !!form.id),
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
apiKey: [
|
||||
{ required: true, message: '请输入apiKey', trigger: 'blur' },
|
||||
{ validator: rule.noChinese, trigger: 'blur' },
|
||||
{ max: 255, message: 'ApiKey最多255个字符', trigger: 'blur' },
|
||||
],
|
||||
baseUrl: [
|
||||
{ required: true, message: '请输入baseUrl', trigger: 'blur' },
|
||||
{ validator: rule.overLength, trigger: 'blur' },
|
||||
],
|
||||
imageSize: [{ required: true, message: '请选择图片大小', trigger: 'change' }],
|
||||
dimensions: [
|
||||
{
|
||||
validator: (rule: any, value: any, callback: any) => {
|
||||
// 只有当显示维度下拉框时才验证必填
|
||||
if (form.modelType === 'Embedding' && availableDimensions.value.length > 0) {
|
||||
if (!value) {
|
||||
callback(new Error('请选择维度'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
extData: [{ validator: rule.json, trigger: 'blur' }],
|
||||
// ... 其他字段的验证规则 ...
|
||||
});
|
||||
|
||||
// 打开弹窗
|
||||
const openDialog = (id?: string) => {
|
||||
visible.value = true;
|
||||
form.id = '';
|
||||
|
||||
// 重置表单数据
|
||||
nextTick(() => {
|
||||
dataFormRef.value?.resetFields();
|
||||
availableModels.value = []; // 重置可用模型列表
|
||||
});
|
||||
|
||||
// 获取aiModel信息
|
||||
if (id) {
|
||||
form.id = id;
|
||||
getAiModelData(id);
|
||||
} else {
|
||||
// Reset the name field when adding a new model
|
||||
form.name = '';
|
||||
form.provider = '';
|
||||
form.modelName = '';
|
||||
form.modelType = '';
|
||||
form.dimensions = '';
|
||||
}
|
||||
};
|
||||
|
||||
// 提交
|
||||
const onSubmit = async () => {
|
||||
const valid = await dataFormRef.value.validate().catch(() => {});
|
||||
if (!valid) return false;
|
||||
|
||||
try {
|
||||
loading.value = true;
|
||||
if (form.apiKey?.includes('***')) form.apiKey = '';
|
||||
form.id ? await putObj(form) : await addObj(form);
|
||||
useMessage().success(form.id ? '修改成功' : '添加成功');
|
||||
visible.value = false;
|
||||
emit('refresh');
|
||||
} catch (err: any) {
|
||||
useMessage().error(err.msg);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化表单数据
|
||||
const getAiModelData = (id: string) => {
|
||||
// 获取数据
|
||||
loading.value = true;
|
||||
getObj(id)
|
||||
.then((res: any) => {
|
||||
// 先设置 provider
|
||||
form.provider = res.data.provider;
|
||||
|
||||
// 设置可用模型列表
|
||||
handleProviderChange(form.provider);
|
||||
|
||||
// 设置 modelType
|
||||
form.modelType = res.data.modelType;
|
||||
|
||||
// 确保 filteredModels 已更新
|
||||
nextTick(() => {
|
||||
// 然后设置其他表单数据,包括 modelName
|
||||
Object.assign(form, res.data);
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
// 暴露变量
|
||||
defineExpose({
|
||||
openDialog,
|
||||
});
|
||||
|
||||
// Add this to your existing constants
|
||||
const imageSizes = ['1024x1024', '512x1024', '768x512', '768x1024', '1024x576', '576x1024'];
|
||||
|
||||
// Add a computed property for filtered models based on selected type
|
||||
const filteredModels = computed(() => {
|
||||
return availableModels.value.filter((model) => model.type === form.modelType);
|
||||
});
|
||||
|
||||
// Add a watcher for modelType to reset modelName when type changes
|
||||
watch(
|
||||
() => form.modelType,
|
||||
() => {
|
||||
form.modelName = '';
|
||||
form.dimensions = '';
|
||||
}
|
||||
);
|
||||
|
||||
// 监听模型名称变化,自动设置默认维度
|
||||
watch(
|
||||
() => form.modelName,
|
||||
() => {
|
||||
if (form.modelType === 'Embedding') {
|
||||
const currentModel = availableModels.value.find(model =>
|
||||
model.model === form.modelName && model.type === 'Embedding'
|
||||
);
|
||||
if (currentModel?.dimensions && currentModel.dimensions.length > 0) {
|
||||
form.dimensions = currentModel.dimensions[0].toString();
|
||||
} else {
|
||||
form.dimensions = '';
|
||||
}
|
||||
} else {
|
||||
form.dimensions = '';
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
</script>
|
||||
211
src/views/knowledge/aiModel/index.vue
Normal file
211
src/views/knowledge/aiModel/index.vue
Normal file
@@ -0,0 +1,211 @@
|
||||
<template>
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<el-row v-show="showSearch">
|
||||
<el-form :model="state.queryForm" ref="queryRef" :inline="true" @keyup.enter="getDataList">
|
||||
<el-form-item label="类型" prop="title">
|
||||
<el-select placeholder="请选择类型" v-model="state.queryForm.modelType">
|
||||
<el-option :key="index" :label="item.label" :value="item.value" v-for="(item, index) in modelTypes">
|
||||
{{ item.label }}
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="search" type="primary" @click="getDataList"> 查询</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
<el-button icon="plus" type="primary" class="ml10" @click="batchFormDialogRef.openDialog()" v-auth="'knowledge_aiModel_add'">
|
||||
批量新增
|
||||
</el-button>
|
||||
<el-button icon="folder-add" type="primary" class="ml10" @click="formDialogRef.openDialog()" v-auth="'knowledge_aiModel_add'">
|
||||
新 增
|
||||
</el-button>
|
||||
<el-button plain icon="Refresh" type="primary" @click="handleRefresh"> 刷新 </el-button>
|
||||
|
||||
<el-button plain :disabled="multiple" icon="Delete" type="primary" v-auth="'knowledge_aiModel_del'" @click="handleDelete(selectObjs)">
|
||||
删除
|
||||
</el-button>
|
||||
<right-toolbar
|
||||
v-model:showSearch="showSearch"
|
||||
:export="'knowledge_aiModel_export'"
|
||||
@exportExcel="exportExcel"
|
||||
class="ml10 mr20"
|
||||
style="float: right"
|
||||
@queryTable="getDataList"
|
||||
></right-toolbar>
|
||||
</div>
|
||||
</el-row>
|
||||
<el-table
|
||||
:data="state.dataList"
|
||||
v-loading="state.loading"
|
||||
border
|
||||
:cell-style="tableStyle.cellStyle"
|
||||
:header-cell-style="tableStyle.headerCellStyle"
|
||||
@selection-change="selectionChangHandle"
|
||||
@sort-change="sortChangeHandle"
|
||||
>
|
||||
<el-table-column type="selection" width="40" align="center" />
|
||||
<el-table-column type="index" label="#" width="60" />
|
||||
<el-table-column prop="provider" label="供应商" show-overflow-tooltip width="200">
|
||||
<template #default="scope">
|
||||
{{ getProviderLabel(scope.row.provider) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="modelType" label="类型" show-overflow-tooltip width="100">
|
||||
<template #default="scope">
|
||||
<el-tag>
|
||||
{{ getModelTypeLabel(scope.row.modelType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="name" label="名称" show-overflow-tooltip width="300" />
|
||||
<el-table-column prop="modelName" label="模型" show-overflow-tooltip />
|
||||
|
||||
<el-table-column prop="defaultModel">
|
||||
<template #header>
|
||||
默认模型
|
||||
<tip content="若调用不传递模型,则使用【默认模型】"></tip>
|
||||
</template>
|
||||
<template #default="scope">
|
||||
<el-switch v-model="scope.row.defaultModel" @change="changeSwitch(scope.row)" active-value="1" inactive-value="0"></el-switch>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150">
|
||||
<template #default="scope">
|
||||
<el-button icon="edit-pen" text type="primary" v-auth="'knowledge_aiModel_edit'" @click="formDialogRef.openDialog(scope.row.id)"
|
||||
>编辑</el-button
|
||||
>
|
||||
<el-button icon="delete" text type="primary" v-auth="'knowledge_aiModel_del'" @click="handleDelete([scope.row.id])">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
|
||||
</div>
|
||||
|
||||
<!-- 编辑、新增 -->
|
||||
<form-dialog ref="formDialogRef" @refresh="getDataList(false)" />
|
||||
|
||||
<!-- 批量新增 -->
|
||||
<batch-form-dialog ref="batchFormDialogRef" @refresh="getDataList(false)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="systemAiModel">
|
||||
import { BasicTableProps, useTable } from '/@/hooks/table';
|
||||
import { fetchList, delObjs, putObj, sync } from '/@/api/knowledge/aiModel';
|
||||
import { useMessage, useMessageBox } from '/@/hooks/message';
|
||||
import { modelTypes, providers } from './model';
|
||||
|
||||
// 定义AiModel接口
|
||||
interface AiModel {
|
||||
id: string;
|
||||
provider: string;
|
||||
modelType: string;
|
||||
name: string;
|
||||
modelName: string;
|
||||
defaultModel: string;
|
||||
}
|
||||
|
||||
// 引入组件
|
||||
const FormDialog = defineAsyncComponent(() => import('./form.vue'));
|
||||
const BatchFormDialog = defineAsyncComponent(() => import('./batch-form.vue'));
|
||||
// 定义查询字典
|
||||
|
||||
// 定义变量内容
|
||||
const formDialogRef = ref();
|
||||
const batchFormDialogRef = ref();
|
||||
// 搜索变量
|
||||
const queryRef = ref();
|
||||
const showSearch = ref(true);
|
||||
// 多选变量
|
||||
const selectObjs = ref([]) as any;
|
||||
const multiple = ref(true);
|
||||
|
||||
const state: BasicTableProps = reactive<BasicTableProps>({
|
||||
queryForm: {},
|
||||
pageList: fetchList,
|
||||
});
|
||||
|
||||
// table hook
|
||||
const { getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle, downBlobFile, tableStyle } = useTable(state);
|
||||
|
||||
// 刷新按钮的处理函数
|
||||
const handleRefresh = async () => {
|
||||
try {
|
||||
// 调用list接口获取最新数据
|
||||
await sync();
|
||||
useMessage().success('数据已同步');
|
||||
} catch (error) {
|
||||
useMessage().error('数据同步失败');
|
||||
}
|
||||
};
|
||||
|
||||
// Update the changeSwitch function
|
||||
const changeSwitch = async (row: AiModel) => {
|
||||
try {
|
||||
await putObj({
|
||||
id: row.id,
|
||||
modelType: row.modelType,
|
||||
defaultModel: row.defaultModel,
|
||||
});
|
||||
useMessage().success('更新成功');
|
||||
await getDataList();
|
||||
} catch (error) {
|
||||
useMessage().error('更新失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 多选事件
|
||||
const selectionChangHandle = (objs: { id: string }[]) => {
|
||||
selectObjs.value = objs.map(({ id }) => id);
|
||||
multiple.value = !objs.length;
|
||||
};
|
||||
|
||||
// 删除操作
|
||||
const handleDelete = async (ids: string[]) => {
|
||||
try {
|
||||
await useMessageBox().confirm('此操作将永久删除');
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await delObjs(ids);
|
||||
getDataList();
|
||||
useMessage().success('删除成功');
|
||||
} catch (err: any) {
|
||||
useMessage().error(err.msg);
|
||||
}
|
||||
};
|
||||
|
||||
// 清空搜索条件
|
||||
const resetQuery = () => {
|
||||
// 清空搜索条件
|
||||
queryRef.value?.resetFields();
|
||||
state.queryForm = {};
|
||||
// 清空多选
|
||||
selectObjs.value = [];
|
||||
getDataList();
|
||||
};
|
||||
|
||||
// 导出excel
|
||||
const exportExcel = () => {
|
||||
downBlobFile('/knowledge/aiModel/export', Object.assign(state.queryForm, { ids: selectObjs }), 'aiModel.xlsx');
|
||||
};
|
||||
|
||||
// 获取模型类型的标签
|
||||
const getModelTypeLabel = (value: string) => {
|
||||
const modelType = modelTypes.find((type) => type.value === value);
|
||||
return modelType ? modelType.label : value;
|
||||
};
|
||||
|
||||
// 获取供应商的标签
|
||||
const getProviderLabel = (value: string) => {
|
||||
const provider = providers.find((p) => p.value === value);
|
||||
return provider ? provider.label : value;
|
||||
};
|
||||
</script>
|
||||
148
src/views/knowledge/aiModel/model.ts
Normal file
148
src/views/knowledge/aiModel/model.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
// 供应商
|
||||
export const providers = [
|
||||
{ label: 'OpenAI协议', value: 'OpenAI' },
|
||||
{ label: '阿里百炼', value: 'Aliyun' },
|
||||
{ label: '火山方舟', value: 'Ark' },
|
||||
{ label: 'DeepSeek', value: 'DeepSeek' },
|
||||
{ label: '智谱清言', value: 'ChatGLM' },
|
||||
{ label: '硅基流动', value: 'Siliconflow' },
|
||||
{ label: 'Gitee', value: 'Gitee' },
|
||||
{ label: 'Ollama', value: 'Ollama' },
|
||||
{ label: 'Kimi', value: 'Kimi' },
|
||||
{ label: '搜索服务', value: 'BoCha' },
|
||||
{ label: 'Jina', value: 'Jina' },
|
||||
];
|
||||
|
||||
// 模型类型
|
||||
export const modelTypes = [
|
||||
{ label: '聊天', value: 'Chat' },
|
||||
{ label: '推理', value: 'Reason' },
|
||||
{ label: '向量', value: 'Embedding' },
|
||||
{ label: '排序', value: 'Reranker' },
|
||||
{ label: '图片', value: 'Image' },
|
||||
{ label: '视频', value: 'Video' },
|
||||
{ label: '视觉', value: 'Vision' },
|
||||
{ label: '音频', value: 'Voice' },
|
||||
{ label: '搜索', value: 'Search' },
|
||||
{ label: '解析', value: 'Parse' },
|
||||
];
|
||||
|
||||
// 各供应商的模型映射
|
||||
export const providerModels = {
|
||||
Aliyun: [
|
||||
{ type: 'Chat', model: 'qwen-max-latest', json: true },
|
||||
{ type: 'Chat', model: 'qwen-plus' },
|
||||
{ type: 'Chat', model: 'qwen3-235b-a22b' },
|
||||
{ type: 'Vision', model: 'qwen-vl-plus-latest' },
|
||||
{ type: 'Vision', model: 'qwen-vl-max-latest' },
|
||||
{ type: 'Vision', model: 'qwen-vl-ocr' },
|
||||
{ type: 'Embedding', model: 'text-embedding-v4', dimensions: [2048, 1536, 1024, 768, 512, 256, 128, 64] },
|
||||
{ type: 'Embedding', model: 'text-embedding-v3', dimensions: [1024, 768, 512, 256, 128, 64] },
|
||||
{ type: 'Reranker', model: 'gte-rerank-v2' },
|
||||
{ type: 'Image', model: 'flux-schnell' },
|
||||
{ type: 'Voice', model: 'paraformer-v2' },
|
||||
{ type: 'Voice', model: 'cosyvoice-v1' },
|
||||
],
|
||||
Ark: [
|
||||
{ type: 'Chat', model: 'deepseek-v3-250324', json: true },
|
||||
{ type: 'Chat', model: 'doubao-1-5-pro-32k-250115' },
|
||||
{ type: 'Reason', model: 'deepseek-r1-250120' },
|
||||
{ type: 'Vision', model: 'doubao-1-5-vision-pro-32k-250115' },
|
||||
{ type: 'Embedding', model: 'doubao-embedding-large-text-240915' },
|
||||
],
|
||||
DeepSeek: [
|
||||
{ type: 'Chat', model: 'deepseek-chat', json: true },
|
||||
{ type: 'Reason', model: 'deepseek-reasoner' },
|
||||
],
|
||||
ChatGLM: [
|
||||
{ type: 'Chat', model: 'glm-4-flash' },
|
||||
{ type: 'Chat', model: 'glm-4-plus' },
|
||||
{ type: 'Reason', model: 'glm-z1-air' },
|
||||
{ type: 'Reason', model: 'glm-z1-airx' },
|
||||
{ type: 'Reason', model: 'glm-z1-flash' },
|
||||
{ type: 'Vision', model: 'glm-4v-flash' },
|
||||
{ type: 'Vision', model: 'glm-4v-plus' },
|
||||
{ type: 'Embedding', model: 'embedding-3' },
|
||||
],
|
||||
OpenAI: [
|
||||
{ type: 'Chat', model: 'gpt-4.1-nano', json: true },
|
||||
{ type: 'Chat', model: 'gpt-4.1-mini', json: true },
|
||||
{ type: 'Chat', model: 'gpt-4.1', json: true },
|
||||
{ type: 'Embedding', model: 'text-embedding-3-small' },
|
||||
{ type: 'Embedding', model: 'text-embedding-3-large' },
|
||||
],
|
||||
Siliconflow: [
|
||||
{ type: 'Chat', model: 'deepseek-ai/DeepSeek-V3', json: true },
|
||||
{ type: 'Chat', model: 'moonshotai/Kimi-K2-Instruct' },
|
||||
{ type: 'Image', model: 'black-forest-labs/FLUX.1-schnell' },
|
||||
{ type: 'Image', model: 'stabilityai/stable-diffusion-3-5-large' },
|
||||
{ type: 'Image', model: 'Kwai-Kolors/Kolors' },
|
||||
{ type: 'Video', model: 'Wan-AI/Wan2.1-I2V-14B-720P' },
|
||||
{ type: 'Video', model: 'Wan-AI/Wan2.1-I2V-14B-720P-Turbo' },
|
||||
{ type: 'Voice', model: 'FunAudioLLM/CosyVoice2-0.5B' },
|
||||
{ type: 'Voice', model: 'RVC-Boss/GPT-SoVITS' },
|
||||
{ type: 'Reason', model: 'deepseek-ai/DeepSeek-R1' },
|
||||
{ type: 'Reason', model: 'MiniMaxAI/MiniMax-M1-80k' },
|
||||
{ type: 'Vision', model: 'Qwen/Qwen2.5-VL-72B-Instruct' },
|
||||
{ type: 'Vision', model: 'Qwen/Qwen2.5-VL-32B-Instruct' },
|
||||
{ type: 'Embedding', model: 'Qwen/Qwen3-Embedding-8B' },
|
||||
{ type: 'Embedding', model: 'Qwen/Qwen3-Embedding-4B' },
|
||||
{ type: 'Embedding', model: 'BAAI/bge-m3' },
|
||||
{ type: 'Embedding', model: 'BAAI/bge-large-zh-v1.5' },
|
||||
{ type: 'Reranker', model: 'Qwen/Qwen3-Reranker-8B' },
|
||||
{ type: 'Reranker', model: 'Qwen/Qwen3-Reranker-4B' },
|
||||
{ type: 'Reranker', model: 'BAAI/bge-reranker-v2-m3' },
|
||||
{ type: 'Reranker', model: 'netease-youdao/bce-reranker-base_v1' },
|
||||
],
|
||||
Ollama: [
|
||||
{ type: 'Chat', model: 'qwen3:14b' },
|
||||
{ type: 'Chat', model: 'qwen3:30b' },
|
||||
{ type: 'Chat', model: 'qwen3:32b' },
|
||||
{ type: 'Chat', model: 'qwen3:256b' },
|
||||
{ type: 'Chat', model: 'qwen2.5:14b' },
|
||||
{ type: 'Chat', model: 'qwen2.5:32b' },
|
||||
{ type: 'Chat', model: 'qwen2.5:72b' },
|
||||
{ type: 'Embedding', model: 'bge-m3:latest' },
|
||||
{ type: 'Embedding', model: 'shaw/dmeta-embedding-zh' },
|
||||
{ type: 'Vision', model: 'minicpm-v:latest' },
|
||||
{ type: 'Vision', model: 'qwen2.5vl:7b' },
|
||||
{ type: 'Vision', model: 'qwen2.5vl:32b' },
|
||||
{ type: 'Vision', model: 'qwen2.5vl:72b' },
|
||||
{ type: 'Reason', model: 'deepseek-r1:8b' },
|
||||
{ type: 'Reason', model: 'deepseek-r1:14b' },
|
||||
],
|
||||
Kimi: [
|
||||
{ type: 'Vision', model: 'kimi-latest' },
|
||||
{ type: 'Reason', model: 'kimi-thinking-preview' },
|
||||
{ type: 'Chat', model: 'kimi-k2-0711-preview', json: true },
|
||||
],
|
||||
BoCha: [
|
||||
{ type: 'Search', model: 'bocha-web-search' },
|
||||
{ type: 'Search', model: 'sear-xng' },
|
||||
],
|
||||
Jina: [
|
||||
{ type: 'Parse', model: 'jina-reader' },
|
||||
{ type: 'Reranker', model: 'jina-reranker-m0' },
|
||||
],
|
||||
Gitee: [
|
||||
{ type: 'Embedding', model: 'Qwen3-Embedding-8B' },
|
||||
{ type: 'Reranker', model: 'Qwen3-Reranker-8B' },
|
||||
],
|
||||
};
|
||||
|
||||
// 默认的 baseURL 的映射
|
||||
export const providerBaseURLMap = {
|
||||
OpenAI: 'https://api.openai-hk.com/v1',
|
||||
Ark: 'https://ark.cn-beijing.volces.com/api/v3',
|
||||
Aliyun: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
||||
DeepSeek: 'https://api.deepseek.com/v1',
|
||||
Ollama: 'http://localhost:11434/v1',
|
||||
Siliconflow: 'https://api.siliconflow.cn/v1',
|
||||
ChatGLM: 'https://open.bigmodel.cn/api/paas/v4',
|
||||
MiniMax: 'https://api.minimax.chat/v1',
|
||||
Claude: 'https://api.anthropic.com/v1',
|
||||
BoCha: 'https://api.bochaai.com/v1/',
|
||||
Jina: 'https://r.jina.ai/',
|
||||
Gitee: 'https://ai.gitee.com/v1',
|
||||
Kimi: 'https://api.moonshot.cn/v1',
|
||||
};
|
||||
Reference in New Issue
Block a user