feat: 增强模型配置与创建表单功能
- 在 NodeLibraryFormItem 和 WorkflowItem 接口中添加了新的字段,如 value、options、expand 和 outputParams,以支持更复杂的表单配置。 - 新增 getOperatorList 函数以获取服务商列表,并在 editModule.vue 中集成了运营商选择功能。 - 更新了模型创建和编辑逻辑,支持 tokenConfig 的 JSON 格式配置,确保更灵活的模型设置。 - 优化了文件上传处理,增加了上传状态管理,提升用户体验。
This commit is contained in:
@@ -12,6 +12,19 @@ export interface NodeLibraryFormItem {
|
|||||||
type: 'input' | 'number' | 'textarea' | 'switch' | string;
|
type: 'input' | 'number' | 'textarea' | 'switch' | string;
|
||||||
required: boolean;
|
required: boolean;
|
||||||
default?: string | number | boolean;
|
default?: string | number | boolean;
|
||||||
|
value?: unknown;
|
||||||
|
options?: Array<{ label: string; value: string | number }> | null;
|
||||||
|
expand?: NodeLibraryFormItem[] | Record<string, unknown> | null;
|
||||||
|
fieldConstraint?: {
|
||||||
|
fileTypes?: string;
|
||||||
|
maxFileSize?: number;
|
||||||
|
maxFileCount?: number;
|
||||||
|
minLength?: number;
|
||||||
|
maxLength?: number;
|
||||||
|
numberType?: 'integer' | 'decimal' | string;
|
||||||
|
minValue?: number;
|
||||||
|
maxValue?: number;
|
||||||
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NodeLibraryModelConfig {
|
export interface NodeLibraryModelConfig {
|
||||||
@@ -188,6 +201,11 @@ export interface WorkflowItem {
|
|||||||
description: string;
|
description: string;
|
||||||
flowContent: any;
|
flowContent: any;
|
||||||
nodeInputParams?: any[];
|
nodeInputParams?: any[];
|
||||||
|
outputParams?: Array<Record<string, string>>;
|
||||||
|
imgAddressPrefix?: string;
|
||||||
|
fileUrls?: string[];
|
||||||
|
resultUrl?: string;
|
||||||
|
sessionId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkflowListResponse {
|
export interface WorkflowListResponse {
|
||||||
@@ -313,6 +331,7 @@ export interface ExecuteFlowParams {
|
|||||||
nodeInputParams?: FlowNode[];
|
nodeInputParams?: FlowNode[];
|
||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
skillName?: string;
|
skillName?: string;
|
||||||
|
resultUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function executeFlow(data: ExecuteFlowParams | FormData, requestOptions?: RequestOptions) {
|
export function executeFlow(data: ExecuteFlowParams | FormData, requestOptions?: RequestOptions) {
|
||||||
|
|||||||
@@ -235,3 +235,13 @@ export function getIsChatModel() {
|
|||||||
method: 'get',
|
method: 'get',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取服务商列表
|
||||||
|
*/
|
||||||
|
export function getOperatorList() {
|
||||||
|
return request({
|
||||||
|
url: '/model-gateway/model/listOperator',
|
||||||
|
method: 'get',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -110,6 +110,9 @@ interface ModelItem {
|
|||||||
isPrivate?: number;
|
isPrivate?: number;
|
||||||
isChatModel?: number;
|
isChatModel?: number;
|
||||||
headMsg?: string;
|
headMsg?: string;
|
||||||
|
operatorName?: string;
|
||||||
|
responseBody?: Record<string, unknown>;
|
||||||
|
tokenConfig?: Record<string, unknown> | string;
|
||||||
form?: any;
|
form?: any;
|
||||||
requestMapping?: any;
|
requestMapping?: any;
|
||||||
responseMapping?: any;
|
responseMapping?: any;
|
||||||
@@ -150,8 +153,8 @@ const modelList = ref<ModelItem[]>([]);
|
|||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const selectedModel = ref<ModelItem | null>(null);
|
const selectedModel = ref<ModelItem | null>(null);
|
||||||
const editModuleRef = ref();
|
const editModuleRef = ref();
|
||||||
const isSuperAdmin = ref(false); // 鏄惁涓虹鐞嗗憳
|
const isSuperAdmin = ref(false);
|
||||||
const modelTypes = ref<Array<{ id: number | string; label: string }>>([]); // 妯″瀷绫诲瀷鍒楄〃
|
const modelTypes = ref<Array<{ id: number | string; label: string }>>([]);
|
||||||
|
|
||||||
// 内置模型 API Key 配置
|
// 内置模型 API Key 配置
|
||||||
const apiKeyDialogVisible = ref(false);
|
const apiKeyDialogVisible = ref(false);
|
||||||
@@ -194,6 +197,15 @@ watch(visible, (val) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelType,
|
||||||
|
() => {
|
||||||
|
if (!visible.value) return;
|
||||||
|
pagination.pageNum = 1;
|
||||||
|
fetchModelList();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const getModelTypeName = (type: number) => {
|
const getModelTypeName = (type: number) => {
|
||||||
const typeMap: Record<number, string> = {
|
const typeMap: Record<number, string> = {
|
||||||
1: '推理模型',
|
1: '推理模型',
|
||||||
@@ -210,14 +222,13 @@ const fetchModelList = async () => {
|
|||||||
pageNum: pagination.pageNum,
|
pageNum: pagination.pageNum,
|
||||||
pageSize: pagination.pageSize,
|
pageSize: pagination.pageSize,
|
||||||
modelName: searchParams.modelName || undefined,
|
modelName: searchParams.modelName || undefined,
|
||||||
modelType: props.modelType, // 使用传入的 modelType
|
modelType: props.modelType,
|
||||||
enabled: 1,
|
enabled: 1,
|
||||||
};
|
};
|
||||||
const res: any = await getModelModuleList(params);
|
const res: any = await getModelModuleList(params);
|
||||||
modelList.value = res.data?.list || [];
|
modelList.value = res.data?.list || [];
|
||||||
pagination.total = res.data?.total || 0;
|
pagination.total = res.data?.total || 0;
|
||||||
} catch {
|
} catch {
|
||||||
// 接口错误由 request 全局提示后端 message
|
|
||||||
modelList.value = [];
|
modelList.value = [];
|
||||||
pagination.total = 0;
|
pagination.total = 0;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -235,21 +246,17 @@ const handlePageChange = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectModel = (model: ModelItem) => {
|
const handleSelectModel = (model: ModelItem) => {
|
||||||
// 如果是管理员,直接选中任何模型,不需要配置 API Key
|
|
||||||
if (isSuperAdmin.value) {
|
if (isSuperAdmin.value) {
|
||||||
selectedModel.value = model;
|
selectedModel.value = model;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 非管理员:判断是否是内置模型(isOwner === 0 表示管理员创建的内置模型)
|
|
||||||
if (model.isOwner === 0) {
|
if (model.isOwner === 0) {
|
||||||
// 内置模型,需要用户配置 API Key 创建副本
|
|
||||||
builtInModelToClone.value = model;
|
builtInModelToClone.value = model;
|
||||||
apiKeyForm.modelName = model.modelName;
|
apiKeyForm.modelName = model.modelName;
|
||||||
apiKeyForm.apiKey = '';
|
apiKeyForm.apiKey = '';
|
||||||
apiKeyDialogVisible.value = true;
|
apiKeyDialogVisible.value = true;
|
||||||
} else {
|
} else {
|
||||||
// 用户模型,直接选中
|
|
||||||
selectedModel.value = model;
|
selectedModel.value = model;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -262,22 +269,22 @@ const handleCreatePrivateModel = async () => {
|
|||||||
|
|
||||||
creatingModel.value = true;
|
creatingModel.value = true;
|
||||||
|
|
||||||
// 基于内置模型创建新模型(继承原模型的所有配置,只替换 apiKey)
|
|
||||||
const builtInModel = builtInModelToClone.value;
|
const builtInModel = builtInModelToClone.value;
|
||||||
const createParams = {
|
const createParams = {
|
||||||
modelName: apiKeyForm.modelName,
|
modelName: apiKeyForm.modelName,
|
||||||
modelType: builtInModel.modelType,
|
modelType: builtInModel.modelType,
|
||||||
|
operatorName: builtInModel.operatorName || '',
|
||||||
baseUrl: builtInModel.baseUrl,
|
baseUrl: builtInModel.baseUrl,
|
||||||
httpMethod: builtInModel.httpMethod || 'POST',
|
httpMethod: builtInModel.httpMethod || 'POST',
|
||||||
headMsg: builtInModel.headMsg || '',
|
headMsg: builtInModel.headMsg || '',
|
||||||
isPrivate: builtInModel.isPrivate ?? 1, // 继承原模型的公有/私有属性
|
isPrivate: builtInModel.isPrivate ?? 1,
|
||||||
enabled: builtInModel.enabled ?? 1,
|
enabled: builtInModel.enabled ?? 1,
|
||||||
isChatModel: builtInModel.isChatModel || 0,
|
isChatModel: builtInModel.isChatModel || 0,
|
||||||
apiKey: apiKeyForm.apiKey, // 使用用户输入的新 API Key
|
apiKey: apiKeyForm.apiKey,
|
||||||
form: builtInModel.form || {},
|
form: builtInModel.form || {},
|
||||||
requestMapping: builtInModel.requestMapping || {},
|
requestMapping: builtInModel.requestMapping || {},
|
||||||
responseMapping: builtInModel.responseMapping || {},
|
responseMapping: builtInModel.responseMapping || {},
|
||||||
responseBody: builtInModel.responseBody || {}, // 杩斿洖涓讳綋瀛楁
|
responseBody: builtInModel.responseBody || {},
|
||||||
maxConcurrency: builtInModel.maxConcurrency || 10,
|
maxConcurrency: builtInModel.maxConcurrency || 10,
|
||||||
queueLimit: builtInModel.queueLimit || 100,
|
queueLimit: builtInModel.queueLimit || 100,
|
||||||
timeoutSeconds: builtInModel.timeoutSeconds || 30,
|
timeoutSeconds: builtInModel.timeoutSeconds || 30,
|
||||||
@@ -286,20 +293,15 @@ const handleCreatePrivateModel = async () => {
|
|||||||
retryQueueMaxSeconds: builtInModel.retryQueueMaxSeconds || 60,
|
retryQueueMaxSeconds: builtInModel.retryQueueMaxSeconds || 60,
|
||||||
autoCleanSeconds: builtInModel.autoCleanSeconds || 300,
|
autoCleanSeconds: builtInModel.autoCleanSeconds || 300,
|
||||||
remark: builtInModel.remark || '',
|
remark: builtInModel.remark || '',
|
||||||
tokenMapping: builtInModel.tokenMapping || '', // Token鏄犲皠瀛楁
|
tokenMapping: builtInModel.tokenMapping || '',
|
||||||
|
tokenConfig: builtInModel.tokenConfig || {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const res: any = await addModelModule(createParams);
|
const res: any = await addModelModule(createParams);
|
||||||
|
|
||||||
ElMessage.success('模型创建成功');
|
ElMessage.success('模型创建成功');
|
||||||
|
|
||||||
// 关闭对话框
|
|
||||||
apiKeyDialogVisible.value = false;
|
apiKeyDialogVisible.value = false;
|
||||||
|
|
||||||
// 刷新列表
|
|
||||||
await fetchModelList();
|
await fetchModelList();
|
||||||
|
|
||||||
// 选中新创建的模型
|
|
||||||
const newModelId = res.data?.id || res.data;
|
const newModelId = res.data?.id || res.data;
|
||||||
if (newModelId) {
|
if (newModelId) {
|
||||||
const newModel = modelList.value.find((m) => m.id === String(newModelId));
|
const newModel = modelList.value.find((m) => m.id === String(newModelId));
|
||||||
@@ -338,7 +340,6 @@ const handleClose = () => {
|
|||||||
builtInModelToClone.value = null;
|
builtInModelToClone.value = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 鍔犺浇妯″瀷绫诲瀷鍒楄〃
|
|
||||||
const loadModelTypes = async () => {
|
const loadModelTypes = async () => {
|
||||||
try {
|
try {
|
||||||
const res: any = await getModelTypeList();
|
const res: any = await getModelTypeList();
|
||||||
@@ -346,7 +347,7 @@ const loadModelTypes = async () => {
|
|||||||
modelTypes.value = normalizeModelTypeOptions(res);
|
modelTypes.value = normalizeModelTypeOptions(res);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// 鎺ュ彛閿欒鐢?request 鍏ㄥ眬鎻愮ず鍚庣 message
|
// 接口错误由 request 全局提示后端 message
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="creation-page" :class="{ 'creation-mode': isCreationMode }">
|
<div class="creation-page" :class="{ 'creation-mode': isCreationMode }">
|
||||||
<!-- 左侧面板:工作空间/当前选中元素 Tab切换 -->
|
<!-- 左侧面板:工作空间/当前选中元素 Tab切换 -->
|
||||||
<div class="panel left">
|
<div class="panel left">
|
||||||
@@ -301,7 +301,7 @@
|
|||||||
<div class="simple-form-scroll">
|
<div class="simple-form-scroll">
|
||||||
<el-form label-position="top" class="simple-creation-form creation-form-grid">
|
<el-form label-position="top" class="simple-creation-form creation-form-grid">
|
||||||
<template v-if="currentWorkflowForCreation?.nodeInputParams">
|
<template v-if="currentWorkflowForCreation?.nodeInputParams">
|
||||||
<template v-for="node in currentWorkflowForCreation.nodeInputParams" :key="node.id">
|
<template v-for="node in currentWorkflowForCreation.nodeInputParams">
|
||||||
<template v-if="node.nodeCode !== '__start__' && node.formConfig && node.formConfig.length > 0">
|
<template v-if="node.nodeCode !== '__start__' && node.formConfig && node.formConfig.length > 0">
|
||||||
<el-form-item
|
<el-form-item
|
||||||
v-for="field in getCreationVisibleFields(node)"
|
v-for="field in getCreationVisibleFields(node)"
|
||||||
@@ -352,7 +352,7 @@
|
|||||||
:accept="getCreationFileAccept(field)"
|
:accept="getCreationFileAccept(field)"
|
||||||
:on-change="(file: any) => handleCreationFieldUpload(node, field, file)"
|
:on-change="(file: any) => handleCreationFieldUpload(node, field, file)"
|
||||||
>
|
>
|
||||||
<el-button size="small" type="primary" :disabled="isFromWorkspace">选择文件</el-button>
|
<el-button size="small" type="primary" :loading="isCreationFieldUploading(node, field)" :disabled="isFromWorkspace || isCreationFieldUploading(node, field)">{{ isCreationFieldUploading(node, field) ? '上传中...' : '选择文件' }}</el-button>
|
||||||
</el-upload>
|
</el-upload>
|
||||||
</div>
|
</div>
|
||||||
<div class="creation-upload-tags">
|
<div class="creation-upload-tags">
|
||||||
@@ -703,7 +703,7 @@
|
|||||||
class="http-body-switch"
|
class="http-body-switch"
|
||||||
@change="(val: boolean) => handleHttpBodyShowInFormChange(fieldValue, val)"
|
@change="(val: boolean) => handleHttpBodyShowInFormChange(fieldValue, val)"
|
||||||
/>
|
/>
|
||||||
<el-button type="danger" :icon="Delete" circle @click="deleteHttpBodyField(fieldKey)" />
|
<el-button type="danger" :icon="Delete" circle @click="deleteHttpBodyField(String(fieldKey))" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!fieldValue.showInForm" class="http-body-value-row">
|
<div v-if="!fieldValue.showInForm" class="http-body-value-row">
|
||||||
@@ -769,7 +769,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
|
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
import { Document, Plus, Paperclip, MagicStick, Promotion, Check, Setting, Search, CircleCheck, VideoPause, Delete } from '@element-plus/icons-vue';
|
import { Plus, Paperclip, MagicStick, Promotion, Check, Setting, Search, CircleCheck, VideoPause, Delete } from '@element-plus/icons-vue';
|
||||||
import LogicFlow from '@logicflow/core';
|
import LogicFlow from '@logicflow/core';
|
||||||
import { Control, SelectionSelect } from '@logicflow/extension';
|
import { Control, SelectionSelect } from '@logicflow/extension';
|
||||||
import '@logicflow/core/dist/index.css';
|
import '@logicflow/core/dist/index.css';
|
||||||
@@ -823,7 +823,19 @@ const treeLoading = ref(false);
|
|||||||
const treeNodes = ref<TreeNode[]>([]);
|
const treeNodes = ref<TreeNode[]>([]);
|
||||||
const imgAddressPrefix = ref('');
|
const imgAddressPrefix = ref('');
|
||||||
const selectedElement = ref<SelectedState | null>(null);
|
const selectedElement = ref<SelectedState | null>(null);
|
||||||
const customFields = ref<Array<{ label: string; value: string; type: string; required: boolean }>>([]);
|
const customFields = ref<
|
||||||
|
Array<{
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
type: string;
|
||||||
|
required: boolean;
|
||||||
|
fileList?: Array<{ name: string; url: string }>;
|
||||||
|
uploadKey?: number;
|
||||||
|
fileTypes?: string;
|
||||||
|
maxFileSize?: number;
|
||||||
|
maxFileCount?: number;
|
||||||
|
}>
|
||||||
|
>([]);
|
||||||
const selectedParentParam = ref('');
|
const selectedParentParam = ref('');
|
||||||
const selectedModel = ref('');
|
const selectedModel = ref('');
|
||||||
const showSkillSelector = ref(false);
|
const showSkillSelector = ref(false);
|
||||||
@@ -984,15 +996,18 @@ const currentNodeSkillOption = computed(() => {
|
|||||||
});
|
});
|
||||||
// 获取当前节点的模型类型
|
// 获取当前节点的模型类型
|
||||||
const currentNodeModelType = computed(() => {
|
const currentNodeModelType = computed(() => {
|
||||||
let modelType = 1; // 默认为推理模型
|
const currentNodeCode = String(formState.nodeCode || '').trim();
|
||||||
nodeLibraryGroups.value.forEach((group) => {
|
if (!currentNodeCode) return 0;
|
||||||
(group.items || []).forEach((item) => {
|
|
||||||
if (item.nodeCode === formState.nodeCode) {
|
for (const group of nodeLibraryGroups.value) {
|
||||||
modelType = item.modelType || 1;
|
for (const item of group.items || []) {
|
||||||
|
if (item.nodeCode === currentNodeCode) {
|
||||||
|
const mt = Number(item.modelType ?? 0);
|
||||||
|
return Number.isNaN(mt) ? 0 : mt;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
});
|
}
|
||||||
return modelType;
|
return 0;
|
||||||
});
|
});
|
||||||
// 获取当前选中模型的表单字段
|
// 获取当前选中模型的表单字段
|
||||||
const currentModelForm = computed<NodeLibraryFormItem[]>(() => {
|
const currentModelForm = computed<NodeLibraryFormItem[]>(() => {
|
||||||
@@ -1375,7 +1390,7 @@ const fetchChatModelList = async () => {
|
|||||||
const res: any = await getModelModuleList({
|
const res: any = await getModelModuleList({
|
||||||
pageNum: chatModelPagination.pageNum,
|
pageNum: chatModelPagination.pageNum,
|
||||||
pageSize: chatModelPagination.pageSize,
|
pageSize: chatModelPagination.pageSize,
|
||||||
modelType: 1, // 传递 modelType=1 给后端,获取推理模型
|
modelType: 100, // 传递 modelType=100 固定获取对话模型
|
||||||
modelName: chatModelSearchKeyword.value || undefined,
|
modelName: chatModelSearchKeyword.value || undefined,
|
||||||
enabled: 1,
|
enabled: 1,
|
||||||
});
|
});
|
||||||
@@ -1540,7 +1555,14 @@ const useWorkflow = async (workflow: WorkflowItem) => {
|
|||||||
creationFormValues[fieldKey] = Boolean(field.value);
|
creationFormValues[fieldKey] = Boolean(field.value);
|
||||||
} else {
|
} else {
|
||||||
// 其他类型:保持原值或空字符串
|
// 其他类型:保持原值或空字符串
|
||||||
creationFormValues[fieldKey] = field.value || '';
|
creationFormValues[fieldKey] =
|
||||||
|
field.type === 'upload' || field.type === 'uploadMultiple' || field.type === 'fileUpload'
|
||||||
|
? Array.isArray(field.value)
|
||||||
|
? field.value
|
||||||
|
: field.value
|
||||||
|
? [field.value]
|
||||||
|
: []
|
||||||
|
: field.value || '';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1698,6 +1720,11 @@ const getCreationVisibleFields = (node: any) => {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
const creationFieldFiles = reactive<Record<string, Array<{ name: string; url: string }>>>({});
|
const creationFieldFiles = reactive<Record<string, Array<{ name: string; url: string }>>>({});
|
||||||
|
const creationFieldUploading = reactive<Record<string, boolean>>({});
|
||||||
|
const isCreationFieldUploading = (node: any, field: any) => {
|
||||||
|
const key = getCreationFieldKey(node, field);
|
||||||
|
return creationFieldUploading[key] === true;
|
||||||
|
};
|
||||||
const getCreationFieldFiles = (node: any, field: any) => {
|
const getCreationFieldFiles = (node: any, field: any) => {
|
||||||
const key = getCreationFieldKey(node, field);
|
const key = getCreationFieldKey(node, field);
|
||||||
return creationFieldFiles[key] || [];
|
return creationFieldFiles[key] || [];
|
||||||
@@ -1728,6 +1755,8 @@ const getCreationFileRuleText = (field: any) => {
|
|||||||
};
|
};
|
||||||
const handleCreationFieldUpload = async (node: any, field: any, file: any) => {
|
const handleCreationFieldUpload = async (node: any, field: any, file: any) => {
|
||||||
const key = getCreationFieldKey(node, field);
|
const key = getCreationFieldKey(node, field);
|
||||||
|
if (creationFieldUploading[key]) return;
|
||||||
|
creationFieldUploading[key] = true;
|
||||||
try {
|
try {
|
||||||
const fc = getCreationFieldConstraint(field);
|
const fc = getCreationFieldConstraint(field);
|
||||||
const raw = file?.raw;
|
const raw = file?.raw;
|
||||||
@@ -1770,8 +1799,10 @@ const handleCreationFieldUpload = async (node: any, field: any, file: any) => {
|
|||||||
creationFieldFiles[key].push({ name: file.name, url: fileUrl });
|
creationFieldFiles[key].push({ name: file.name, url: fileUrl });
|
||||||
creationFormValues[key] = creationFieldFiles[key].map((f) => f.url);
|
creationFormValues[key] = creationFieldFiles[key].map((f) => f.url);
|
||||||
ElMessage.success('文件上传成功');
|
ElMessage.success('文件上传成功');
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
ElMessage.error(error?.message || '文件上传失败');
|
ElMessage.error(error?.message || '文件上传失败');
|
||||||
|
} finally {
|
||||||
|
creationFieldUploading[key] = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const removeCreationFieldFile = (node: any, field: any, fileIdx: number) => {
|
const removeCreationFieldFile = (node: any, field: any, fileIdx: number) => {
|
||||||
@@ -1911,7 +1942,18 @@ const sendMessage = async () => {
|
|||||||
const userVal = creationFormValues[bodyFieldKey];
|
const userVal = creationFormValues[bodyFieldKey];
|
||||||
bodyValue[bodyKey] = {
|
bodyValue[bodyKey] = {
|
||||||
...bodyItem,
|
...bodyItem,
|
||||||
value: userVal !== undefined ? userVal : bodyItem.value,
|
value:
|
||||||
|
bodyItem.fieldType === 'fileUpload'
|
||||||
|
? Array.isArray(userVal !== undefined ? userVal : bodyItem.value)
|
||||||
|
? (userVal !== undefined ? userVal : bodyItem.value)
|
||||||
|
: userVal !== undefined
|
||||||
|
? [userVal]
|
||||||
|
: bodyItem.value
|
||||||
|
? [bodyItem.value]
|
||||||
|
: []
|
||||||
|
: userVal !== undefined
|
||||||
|
? userVal
|
||||||
|
: bodyItem.value,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
@@ -2020,7 +2062,7 @@ const handleGlobalMouseUp = () => {
|
|||||||
isDraggingMiddleSplitter.value = false;
|
isDraggingMiddleSplitter.value = false;
|
||||||
};
|
};
|
||||||
// 根据字段类型返回CSS类名
|
// 根据字段类型返回CSS类名
|
||||||
const getFieldClass = (type: string) => {
|
const _getFieldClass = (type: string) => {
|
||||||
if (type === 'textarea') return 'form-item-full';
|
if (type === 'textarea') return 'form-item-full';
|
||||||
if (type === 'number' || type === 'switch') return 'form-item-small';
|
if (type === 'number' || type === 'switch') return 'form-item-small';
|
||||||
return 'form-item-medium';
|
return 'form-item-medium';
|
||||||
@@ -2087,7 +2129,14 @@ const handleTreeNodeClick = async (data: TreeNode) => {
|
|||||||
} else if (field.type === 'switch') {
|
} else if (field.type === 'switch') {
|
||||||
creationFormValues[fieldKey] = Boolean(field.value);
|
creationFormValues[fieldKey] = Boolean(field.value);
|
||||||
} else {
|
} else {
|
||||||
creationFormValues[fieldKey] = field.value || '';
|
creationFormValues[fieldKey] =
|
||||||
|
field.type === 'upload' || field.type === 'uploadMultiple' || field.type === 'fileUpload'
|
||||||
|
? Array.isArray(field.value)
|
||||||
|
? field.value
|
||||||
|
: field.value
|
||||||
|
? [field.value]
|
||||||
|
: []
|
||||||
|
: field.value || '';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2164,7 +2213,14 @@ const handleTreeNodeClick = async (data: TreeNode) => {
|
|||||||
} else if (field.type === 'switch') {
|
} else if (field.type === 'switch') {
|
||||||
creationFormValues[fieldKey] = Boolean(field.value);
|
creationFormValues[fieldKey] = Boolean(field.value);
|
||||||
} else {
|
} else {
|
||||||
creationFormValues[fieldKey] = field.value || '';
|
creationFormValues[fieldKey] =
|
||||||
|
field.type === 'upload' || field.type === 'uploadMultiple' || field.type === 'fileUpload'
|
||||||
|
? Array.isArray(field.value)
|
||||||
|
? field.value
|
||||||
|
: field.value
|
||||||
|
? [field.value]
|
||||||
|
: []
|
||||||
|
: field.value || '';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2288,7 +2344,7 @@ const removeCustomField = (index: number) => {
|
|||||||
customFields.value.splice(index, 1);
|
customFields.value.splice(index, 1);
|
||||||
};
|
};
|
||||||
// 获取自定义字段的文件列表
|
// 获取自定义字段的文件列表
|
||||||
const getCustomFieldFileList = (index: number) => {
|
const _getCustomFieldFileList = (index: number) => {
|
||||||
const field = customFields.value[index];
|
const field = customFields.value[index];
|
||||||
if (!field || !field.fileList) return [];
|
if (!field || !field.fileList) return [];
|
||||||
return field.fileList;
|
return field.fileList;
|
||||||
@@ -2752,7 +2808,7 @@ const handleFieldUpload = async (field: string, file: any, type: string) => {
|
|||||||
ElMessage.success('文件上传成功');
|
ElMessage.success('文件上传成功');
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
ElMessage.error(error?.message || '文件上传失败');
|
ElMessage.error(error?.message || '文件上传失败');
|
||||||
console.error('Upload error:', error);
|
// 上传失败日志已省略
|
||||||
// 上传失败时,递增 uploadKey 来重置上传组件
|
// 上传失败时,递增 uploadKey 来重置上传组件
|
||||||
fieldUploadKeys[field] = (fieldUploadKeys[field] || 0) + 1;
|
fieldUploadKeys[field] = (fieldUploadKeys[field] || 0) + 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,13 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
|
||||||
|
<el-form-item label="运营商名称" prop="operatorName" required>
|
||||||
|
<el-select v-model="state.ruleForm.operatorName" placeholder="请选择运营商" clearable style="width: 100%">
|
||||||
|
<el-option v-for="item in operatorNameOptions" :key="item.value" :label="item.label" :value="item.value"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
|
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
|
||||||
<el-form-item label="模型服务地址" prop="baseUrl">
|
<el-form-item label="模型服务地址" prop="baseUrl">
|
||||||
<el-input v-model="state.ruleForm.baseUrl" placeholder="请输入模型服务地址" clearable></el-input>
|
<el-input v-model="state.ruleForm.baseUrl" placeholder="请输入模型服务地址" clearable></el-input>
|
||||||
@@ -150,6 +157,17 @@
|
|||||||
<el-input v-model="state.ruleForm.tokenMapping" placeholder="请输入Token映射" clearable></el-input>
|
<el-input v-model="state.ruleForm.tokenMapping" placeholder="请输入Token映射" clearable></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
|
||||||
|
<el-form-item label="Token计算配置" prop="tokenConfig">
|
||||||
|
<el-input
|
||||||
|
v-model="state.ruleForm.tokenConfig"
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
placeholder="请输入 JSON 对象,例如:{promptRate: 1, completionRate: 1}"
|
||||||
|
clearable
|
||||||
|
></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
</el-collapse-transition>
|
</el-collapse-transition>
|
||||||
</el-form>
|
</el-form>
|
||||||
@@ -243,11 +261,17 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts" name="systemEditModule">
|
<script setup lang="ts" name="systemEditModule">
|
||||||
import { reactive, ref, computed } from 'vue';
|
import { reactive, ref, computed, onMounted } from 'vue';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
import { ArrowUp, ArrowDown } from '@element-plus/icons-vue';
|
import { ArrowUp, ArrowDown } from '@element-plus/icons-vue';
|
||||||
import { addModelModule, updateModelModule, getModelModuleDetail, type ModelFormEntry } from '/@/api/settings/modelConfig/modelModule/index';
|
import {
|
||||||
|
addModelModule,
|
||||||
|
updateModelModule,
|
||||||
|
getModelModuleDetail,
|
||||||
|
getOperatorList,
|
||||||
|
type ModelFormEntry,
|
||||||
|
} from '/@/api/settings/modelConfig/modelModule/index';
|
||||||
|
|
||||||
export type ModelTypeOption = { id: number | string; label: string };
|
export type ModelTypeOption = { id: number | string; label: string };
|
||||||
|
|
||||||
@@ -264,6 +288,20 @@ const props = withDefaults(
|
|||||||
|
|
||||||
const modelTypeOptions = computed(() => props.modelTypes);
|
const modelTypeOptions = computed(() => props.modelTypes);
|
||||||
|
|
||||||
|
const operatorNameOptions = ref<Array<{ label: string; value: string }>>([]);
|
||||||
|
|
||||||
|
const loadOperatorOptions = async () => {
|
||||||
|
try {
|
||||||
|
const res: any = await getOperatorList();
|
||||||
|
const list = res?.data?.list;
|
||||||
|
operatorNameOptions.value = Array.isArray(list)
|
||||||
|
? list.filter((item: unknown) => typeof item === 'string' && item.trim() !== '').map((name: string) => ({ label: name, value: name }))
|
||||||
|
: [];
|
||||||
|
} catch {
|
||||||
|
operatorNameOptions.value = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const typeOptionValue = (id: number | string): number | string => {
|
const typeOptionValue = (id: number | string): number | string => {
|
||||||
const n = Number(id);
|
const n = Number(id);
|
||||||
return Number.isNaN(n) ? id : n;
|
return Number.isNaN(n) ? id : n;
|
||||||
@@ -280,6 +318,7 @@ const state = reactive({
|
|||||||
id: '',
|
id: '',
|
||||||
modelName: '',
|
modelName: '',
|
||||||
modelType: null as number | string | null,
|
modelType: null as number | string | null,
|
||||||
|
operatorName: '',
|
||||||
baseUrl: '',
|
baseUrl: '',
|
||||||
httpMethod: 'POST',
|
httpMethod: 'POST',
|
||||||
headMsg: '',
|
headMsg: '',
|
||||||
@@ -296,6 +335,7 @@ const state = reactive({
|
|||||||
autoCleanSeconds: 300,
|
autoCleanSeconds: 300,
|
||||||
remark: '',
|
remark: '',
|
||||||
tokenMapping: '',
|
tokenMapping: '',
|
||||||
|
tokenConfig: '{}',
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
modelName: [{ required: true, message: '请输入模型名称', trigger: 'blur' }],
|
modelName: [{ required: true, message: '请输入模型名称', trigger: 'blur' }],
|
||||||
@@ -313,6 +353,7 @@ const state = reactive({
|
|||||||
],
|
],
|
||||||
baseUrl: [{ required: true, message: '请输入模型服务地址', trigger: 'blur' }],
|
baseUrl: [{ required: true, message: '请输入模型服务地址', trigger: 'blur' }],
|
||||||
httpMethod: [{ required: true, message: '请选择请求方式', trigger: 'change' }],
|
httpMethod: [{ required: true, message: '请选择请求方式', trigger: 'change' }],
|
||||||
|
operatorName: [{ required: true, message: '请选择运营商名称', trigger: 'change' }],
|
||||||
apiKey: [
|
apiKey: [
|
||||||
{
|
{
|
||||||
validator: (_rule: unknown, value: unknown, callback: (e?: Error) => void) => {
|
validator: (_rule: unknown, value: unknown, callback: (e?: Error) => void) => {
|
||||||
@@ -339,6 +380,27 @@ const state = reactive({
|
|||||||
queueLimit: [{ required: true, message: '请输入排队队列上限', trigger: 'blur' }],
|
queueLimit: [{ required: true, message: '请输入排队队列上限', trigger: 'blur' }],
|
||||||
timeoutSeconds: [{ required: true, message: '请输入请求超时时间', trigger: 'blur' }],
|
timeoutSeconds: [{ required: true, message: '请输入请求超时时间', trigger: 'blur' }],
|
||||||
expectedSeconds: [{ required: true, message: '请输入预计执行时间', trigger: 'blur' }],
|
expectedSeconds: [{ required: true, message: '请输入预计执行时间', trigger: 'blur' }],
|
||||||
|
tokenConfig: [
|
||||||
|
{
|
||||||
|
validator: (_rule: unknown, value: unknown, callback: (e?: Error) => void) => {
|
||||||
|
if (value === undefined || value === null || String(value).trim() === '') {
|
||||||
|
callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(String(value));
|
||||||
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||||
|
callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback(new Error('Token计算配置必须为 JSON 对象'));
|
||||||
|
} catch {
|
||||||
|
callback(new Error('Token计算配置 JSON 格式不正确'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trigger: 'blur',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
dialog: {
|
dialog: {
|
||||||
isShowDialog: false,
|
isShowDialog: false,
|
||||||
@@ -402,12 +464,6 @@ const parseResponseMappingFields = (mapping: unknown) => {
|
|||||||
return Object.entries(mapping).map(([key, value]) => ({ key, value: String(value) }));
|
return Object.entries(mapping).map(([key, value]) => ({ key, value: String(value) }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildFormArray = (): ModelFormEntry[] => {
|
|
||||||
return state.formFields
|
|
||||||
.filter((field) => field.key?.trim() && field.value?.trim())
|
|
||||||
.map((field) => ({ key: field.key.trim(), value: field.value.trim() }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseKeyValueString = (raw: string) => {
|
const parseKeyValueString = (raw: string) => {
|
||||||
if (!raw) return [];
|
if (!raw) return [];
|
||||||
const headers: Array<{ key: string; value: string }> = [];
|
const headers: Array<{ key: string; value: string }> = [];
|
||||||
@@ -551,6 +607,7 @@ const fillFormFromDetailRow = (row: Record<string, unknown>) => {
|
|||||||
id: row.id as string,
|
id: row.id as string,
|
||||||
modelName: String(row.modelName ?? ''),
|
modelName: String(row.modelName ?? ''),
|
||||||
modelType: row.modelType !== undefined && row.modelType !== null ? typeOptionValue(row.modelType as number | string) : null,
|
modelType: row.modelType !== undefined && row.modelType !== null ? typeOptionValue(row.modelType as number | string) : null,
|
||||||
|
operatorName: String(row.operatorName ?? ''),
|
||||||
baseUrl: String(row.baseUrl ?? ''),
|
baseUrl: String(row.baseUrl ?? ''),
|
||||||
httpMethod: String(row.httpMethod || 'POST'),
|
httpMethod: String(row.httpMethod || 'POST'),
|
||||||
headMsg: String(row.headMsg || ''),
|
headMsg: String(row.headMsg || ''),
|
||||||
@@ -567,6 +624,10 @@ const fillFormFromDetailRow = (row: Record<string, unknown>) => {
|
|||||||
autoCleanSeconds: Number(row.autoCleanSeconds ?? 300),
|
autoCleanSeconds: Number(row.autoCleanSeconds ?? 300),
|
||||||
remark: String(row.remark || ''),
|
remark: String(row.remark || ''),
|
||||||
tokenMapping: String(row.tokenMapping || ''),
|
tokenMapping: String(row.tokenMapping || ''),
|
||||||
|
tokenConfig:
|
||||||
|
typeof row.tokenConfig === 'string'
|
||||||
|
? row.tokenConfig
|
||||||
|
: JSON.stringify((row.tokenConfig as Record<string, unknown>) || {}, null, 2),
|
||||||
};
|
};
|
||||||
state.headers = ensureKeyValueRows(parseHeaders(String(row.headMsg || '')));
|
state.headers = ensureKeyValueRows(parseHeaders(String(row.headMsg || '')));
|
||||||
state.formFields = ensureKeyValueRows(parseFormFields(row.form));
|
state.formFields = ensureKeyValueRows(parseFormFields(row.form));
|
||||||
@@ -628,6 +689,7 @@ const openDialog = async (type: string, row?: Record<string, unknown>) => {
|
|||||||
id: '',
|
id: '',
|
||||||
modelName: '',
|
modelName: '',
|
||||||
modelType: null,
|
modelType: null,
|
||||||
|
operatorName: '',
|
||||||
baseUrl: '',
|
baseUrl: '',
|
||||||
httpMethod: 'POST',
|
httpMethod: 'POST',
|
||||||
headMsg: '',
|
headMsg: '',
|
||||||
@@ -644,6 +706,7 @@ const openDialog = async (type: string, row?: Record<string, unknown>) => {
|
|||||||
autoCleanSeconds: 300,
|
autoCleanSeconds: 300,
|
||||||
remark: '',
|
remark: '',
|
||||||
tokenMapping: '',
|
tokenMapping: '',
|
||||||
|
tokenConfig: '{}',
|
||||||
};
|
};
|
||||||
state.headers = [{ key: '', value: '' }];
|
state.headers = [{ key: '', value: '' }];
|
||||||
state.formFields = [{ key: '', value: '' }];
|
state.formFields = [{ key: '', value: '' }];
|
||||||
@@ -682,6 +745,7 @@ const onSubmit = () => {
|
|||||||
const submitData = {
|
const submitData = {
|
||||||
modelName: state.ruleForm.modelName,
|
modelName: state.ruleForm.modelName,
|
||||||
modelType: state.ruleForm.modelType as number | string,
|
modelType: state.ruleForm.modelType as number | string,
|
||||||
|
operatorName: state.ruleForm.operatorName,
|
||||||
baseUrl: state.ruleForm.baseUrl,
|
baseUrl: state.ruleForm.baseUrl,
|
||||||
httpMethod: state.ruleForm.httpMethod || 'POST',
|
httpMethod: state.ruleForm.httpMethod || 'POST',
|
||||||
headMsg: state.ruleForm.headMsg,
|
headMsg: state.ruleForm.headMsg,
|
||||||
@@ -702,6 +766,7 @@ const onSubmit = () => {
|
|||||||
autoCleanSeconds: state.ruleForm.autoCleanSeconds,
|
autoCleanSeconds: state.ruleForm.autoCleanSeconds,
|
||||||
remark: state.ruleForm.remark || '',
|
remark: state.ruleForm.remark || '',
|
||||||
tokenMapping: state.ruleForm.tokenMapping || '',
|
tokenMapping: state.ruleForm.tokenMapping || '',
|
||||||
|
tokenConfig: parseJsonObjectField(state.ruleForm.tokenConfig || '{}', {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (state.dialog.type === 'edit') {
|
if (state.dialog.type === 'edit') {
|
||||||
@@ -725,6 +790,10 @@ const onSubmit = () => {
|
|||||||
defineExpose({
|
defineExpose({
|
||||||
openDialog,
|
openDialog,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadOperatorOptions();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@@ -773,7 +842,3 @@ defineExpose({
|
|||||||
color: #606266;
|
color: #606266;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -186,13 +186,23 @@ const checkAdminStatus = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 判断是否为推理模型(只有推理模型才能设置为会话模型)
|
// 判断是否为推理模型(只有推理模型才能设置为会话模型)
|
||||||
|
const normalizeModelTypeKey = (modelType: number | string | undefined | null) => {
|
||||||
|
if (modelType === undefined || modelType === null || modelType === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const raw = String(modelType).trim();
|
||||||
|
const n = Number(raw);
|
||||||
|
return Number.isNaN(n) ? raw : String(n);
|
||||||
|
};
|
||||||
|
|
||||||
const isInferenceModel = (modelType: number | string | undefined | null) => {
|
const isInferenceModel = (modelType: number | string | undefined | null) => {
|
||||||
if (modelType === undefined || modelType === null || modelType === '') {
|
if (modelType === undefined || modelType === null || modelType === '') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// 查找模型类型标签,判断是否为"推理模型"
|
// 查找模型类型标签,判断是否为"推理模型"
|
||||||
const typeInfo = state.modelTypes.find((t) => String(t.id) === String(modelType));
|
const modelTypeKey = normalizeModelTypeKey(modelType);
|
||||||
return typeInfo?.label === '推理模型' || String(modelType) === '1';
|
const typeInfo = state.modelTypes.find((t) => normalizeModelTypeKey(t.id) === modelTypeKey);
|
||||||
|
return typeInfo?.label === '推理模型' || modelTypeKey === '1' || modelTypeKey === '100';
|
||||||
};
|
};
|
||||||
|
|
||||||
// 设置为会话模型
|
// 设置为会话模型
|
||||||
@@ -278,7 +288,8 @@ const resolveModelTypeLabel = (modelType: number | string | undefined | null) =>
|
|||||||
if (modelType === undefined || modelType === null || modelType === '') {
|
if (modelType === undefined || modelType === null || modelType === '') {
|
||||||
return '—';
|
return '—';
|
||||||
}
|
}
|
||||||
const hit = state.modelTypes.find((t) => String(t.id) === String(modelType));
|
const modelTypeKey = normalizeModelTypeKey(modelType);
|
||||||
|
const hit = state.modelTypes.find((t) => normalizeModelTypeKey(t.id) === modelTypeKey);
|
||||||
return hit?.label ?? String(modelType);
|
return hit?.label ?? String(modelType);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user