首页调试工作流管理
This commit is contained in:
@@ -22,7 +22,11 @@
|
||||
@click="handleSelectModel(model)"
|
||||
>
|
||||
<div class="model-card-header">
|
||||
<div class="model-type">{{ getModelTypeName(model.modelType) }}</div>
|
||||
<div class="model-type">
|
||||
{{ getModelTypeName(model.modelType) }}
|
||||
<el-tag v-if="model.systemModel" size="small" type="warning" class="model-owner-tag">内置</el-tag>
|
||||
<el-tag v-else size="small" type="success" class="model-owner-tag">我的</el-tag>
|
||||
</div>
|
||||
<el-icon v-if="selectedModel?.id === model.id" class="check-icon" color="#67c23a">
|
||||
<CircleCheck />
|
||||
</el-icon>
|
||||
@@ -56,16 +60,56 @@
|
||||
<el-button type="primary" @click="handleConfirm" :disabled="!selectedModel">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 系统内置模型:填写 API Key,经修改接口转换为用户模型后自动绑定 -->
|
||||
<el-dialog
|
||||
v-model="apiKeyDialogVisible"
|
||||
title="填写 API Key"
|
||||
width="480px"
|
||||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
@close="handleApiKeyClose"
|
||||
>
|
||||
<el-alert
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="该模型为系统内置模型,填写你的 API Key 后将创建一条用户模型并自动绑定到当前节点。"
|
||||
class="api-key-alert"
|
||||
/>
|
||||
<el-form label-position="top" class="api-key-form">
|
||||
<el-form-item label="API Key" required>
|
||||
<el-input
|
||||
v-model="apiKeyForm.apiKey"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="请输入你的 API Key"
|
||||
@keyup.enter="handleApiKeyConfirm"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleApiKeyClose">取消</el-button>
|
||||
<el-button type="primary" :loading="converting" @click="handleApiKeyConfirm">确认并转换</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { Search, CircleCheck } from '@element-plus/icons-vue';
|
||||
import { getWorkflowModelList, type WorkflowModelItem } from '/@/api/settings/workflow';
|
||||
import { updateModelManage } from '/@/api/settings/modelConfigV2';
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean;
|
||||
defaultModel?: WorkflowModelItem | null;
|
||||
// 非空时仅列出同类型模型(首页「重新选择模型」场景);工作流管理不传,行为不变
|
||||
sameTypeModelType?: string | number | null;
|
||||
// 系统模型是否必须填写 API Key(默认 true)。
|
||||
// 超级管理员绘制/编辑模板工作流时传 false:选系统模型直接选中,不再弹 API Key。
|
||||
systemModelRequireKey?: boolean;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
@@ -76,6 +120,8 @@ interface Emits {
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
defaultModel: null,
|
||||
sameTypeModelType: null,
|
||||
systemModelRequireKey: true,
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
@@ -86,6 +132,11 @@ const pagination = reactive({ pageNum: 1, pageSize: 10, total: 0 });
|
||||
const modelList = ref<WorkflowModelItem[]>([]);
|
||||
const loading = ref(false);
|
||||
const selectedModel = ref<WorkflowModelItem | null>(null);
|
||||
// 系统内置模型(systemModel === true)转用户模型:填 API Key → 调修改接口,后端自动克隆出新用户模型
|
||||
const apiKeyDialogVisible = ref(false);
|
||||
const converting = ref(false);
|
||||
const pendingCloneModel = ref<WorkflowModelItem | null>(null);
|
||||
const apiKeyForm = reactive({ apiKey: '' });
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
@@ -143,6 +194,9 @@ const fetchModelList = async () => {
|
||||
pageNum: pagination.pageNum,
|
||||
pageSize: pagination.pageSize,
|
||||
modelName: searchParams.modelName || undefined,
|
||||
...(props.sameTypeModelType !== undefined && props.sameTypeModelType !== null && props.sameTypeModelType !== ''
|
||||
? { isSameType: true, modelType: props.sameTypeModelType }
|
||||
: {}),
|
||||
};
|
||||
const res = await getWorkflowModelList(params);
|
||||
modelList.value = res.data?.list || [];
|
||||
@@ -170,6 +224,14 @@ const handlePageChange = () => {
|
||||
};
|
||||
|
||||
const handleSelectModel = (model: WorkflowModelItem) => {
|
||||
// 系统内置模型:默认需填写 API Key,经修改接口转成用户模型后再绑定;
|
||||
// 超级管理员绘制模板(systemModelRequireKey=false)时直接选中,模板保留系统模型供用户使用时再补 Key
|
||||
if (model.systemModel && props.systemModelRequireKey) {
|
||||
pendingCloneModel.value = model;
|
||||
apiKeyForm.apiKey = '';
|
||||
apiKeyDialogVisible.value = true;
|
||||
return;
|
||||
}
|
||||
selectedModel.value = model;
|
||||
};
|
||||
|
||||
@@ -180,6 +242,68 @@ const handleConfirm = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 确认 API Key:调用修改接口,后端克隆出用户模型并直接返回,前端立即绑定
|
||||
const handleApiKeyConfirm = async () => {
|
||||
if (!apiKeyForm.apiKey.trim()) {
|
||||
ElMessage.warning('请输入 API Key');
|
||||
return;
|
||||
}
|
||||
if (!pendingCloneModel.value?.id) return;
|
||||
converting.value = true;
|
||||
try {
|
||||
const src = pendingCloneModel.value;
|
||||
const res = await updateModelManage({
|
||||
id: src.id,
|
||||
// 传递系统模型全部配置,供后端克隆用户模型时继承
|
||||
modelName: src.modelName,
|
||||
modelType: src.modelType,
|
||||
modelSupplier: src.modelSupplier,
|
||||
baseUrl: src.baseUrl,
|
||||
responseType: src.responseType ?? src.invokeType,
|
||||
apiKey: apiKeyForm.apiKey.trim(),
|
||||
enabled: src.enabled,
|
||||
chatModel: src.ChatModel ?? src.chatModel,
|
||||
maxConcurrency: src.maxConcurrency,
|
||||
maxTokens: src.maxTokens,
|
||||
tokenPredictPrice: src.tokenPredictPrice,
|
||||
requestHeadMapping: src.requestHeadMapping ?? src.requestMapping,
|
||||
requestBodyMapping: src.requestBodyMapping,
|
||||
responseMapping: src.responseMapping,
|
||||
responseBodyMapping: src.responseBodyMapping,
|
||||
...(src.tokenMapping ? { tokenMapping: src.tokenMapping } : {}),
|
||||
...(src.asyncTaskMapping ? { asyncTaskMapping: src.asyncTaskMapping } : {}),
|
||||
...(src.lastFrame ? { lastFrame: src.lastFrame } : {}),
|
||||
...(src.maxDuration ? { maxDuration: src.maxDuration } : {}),
|
||||
...(src.tokenPredictPriceUnit ? { tokenPredictPriceUnit: src.tokenPredictPriceUnit } : {}),
|
||||
} as any);
|
||||
// 返回结构为 { data: { modelManage: {...} } },新用户模型 id 在 modelManage 中
|
||||
const newModelManage = res?.data?.modelManage || res?.data;
|
||||
if (!newModelManage?.id) throw new Error('接口未返回新的用户模型');
|
||||
// 后端返回的 modelManage 仅带新 id/apiKey,modelName/requestBodyMapping 等为空。
|
||||
// 用原系统模型完整配置 + 新 id 合并,保证绑定后模型名、参数表单、下游引用、isSameType 过滤都正常。
|
||||
const newModel: WorkflowModelItem = {
|
||||
...(pendingCloneModel.value || {}),
|
||||
id: newModelManage.id,
|
||||
systemModel: false,
|
||||
apiKey: apiKeyForm.apiKey.trim(),
|
||||
};
|
||||
apiKeyDialogVisible.value = false;
|
||||
pendingCloneModel.value = null;
|
||||
emit('confirm', newModel);
|
||||
handleClose();
|
||||
ElMessage.success('已创建用户模型并绑定');
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message || '模型转换失败,请重试');
|
||||
} finally {
|
||||
converting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleApiKeyClose = () => {
|
||||
apiKeyDialogVisible.value = false;
|
||||
pendingCloneModel.value = null;
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
visible.value = false;
|
||||
selectedModel.value = null;
|
||||
@@ -232,13 +356,19 @@ const handleClose = () => {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.model-type {
|
||||
display: inline-block;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 2px 8px;
|
||||
background: #eff6ff;
|
||||
color: #3b82f6;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
|
||||
.model-owner-tag {
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
.check-icon {
|
||||
font-size: 20px;
|
||||
@@ -272,4 +402,12 @@ const handleClose = () => {
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.api-key-alert {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.api-key-form {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -74,8 +74,13 @@
|
||||
@confirm="confirmSaveWorkflow"
|
||||
/>
|
||||
|
||||
<!-- 模型选择器 -->
|
||||
<ModelSelector v-model="showModelSelector" :default-model="selectedModelData" @confirm="handleModelConfirm" />
|
||||
<!-- 模型选择器(管理员绘制模板时选系统模型无需填 API Key,直接选中) -->
|
||||
<ModelSelector
|
||||
v-model="showModelSelector"
|
||||
:default-model="selectedModelData"
|
||||
:system-model-require-key="!isSuperAdmin"
|
||||
@confirm="handleModelConfirm"
|
||||
/>
|
||||
|
||||
<!-- 技能选择器 -->
|
||||
<SkillSelector v-model="showSkillSelector" :default-skill="selectedSkillData" @confirm="handleSkillConfirm" />
|
||||
@@ -97,6 +102,7 @@ import {
|
||||
deleteWorkflow,
|
||||
type WorkflowItem,
|
||||
} from '/@/api/settings/creation';
|
||||
import { checkIsSuperAdmin } from '/@/api/system/user';
|
||||
import { getNodeLibraryList, type NodeLibraryGroup } from '/@/api/settings/workflow';
|
||||
import NodeConfigPanel from './component/NodeConfigPanel.vue';
|
||||
import {
|
||||
@@ -226,6 +232,8 @@ let nodeId = 0;
|
||||
// 模型选择器相关状态
|
||||
const showModelSelector = ref(false);
|
||||
const selectedModelData = ref<any>(null);
|
||||
// 当前用户是否为超级管理员(管理员绘制/编辑模板工作流时选系统模型无需填 API Key)
|
||||
const isSuperAdmin = ref(false);
|
||||
|
||||
// 技能选择器相关状态
|
||||
const showSkillSelector = ref(false);
|
||||
@@ -270,9 +278,10 @@ const onConnect = (connection: Connection) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// id 含 source/target/handle,同一对节点间不同 handle 的连线也互不冲突
|
||||
addEdges([
|
||||
{
|
||||
id: `edge-${connection.source}-${connection.target}`,
|
||||
id: `edge-${connection.source}-${connection.sourceHandle || 'source-bottom'}-${connection.target}-${connection.targetHandle || 'target-top'}`,
|
||||
source: connection.source,
|
||||
target: connection.target,
|
||||
sourceHandle: connection.sourceHandle,
|
||||
@@ -695,28 +704,33 @@ const fetchWorkflowList = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 添加默认开始节点
|
||||
// 构造默认开始节点(固定 id,供新建/清空画布后恢复开始节点)
|
||||
const createStartNode = () => ({
|
||||
id: 'start-node',
|
||||
type: 'input',
|
||||
position: { x: 200, y: 200 },
|
||||
data: { label: '开始', nodeCode: '__start__', runFormFields: [] },
|
||||
});
|
||||
|
||||
// 添加默认开始节点(onMounted 首次初始化用 addNodes,此时画布为空、无 v-model watch 竞争)
|
||||
const addDefaultStartNode = () => {
|
||||
const startNode = {
|
||||
id: 'start-node',
|
||||
type: 'input',
|
||||
position: { x: 200, y: 200 },
|
||||
data: { label: '开始', nodeCode: '__start__', runFormFields: [] },
|
||||
};
|
||||
addNodes([startNode]);
|
||||
nodes.value.push(startNode);
|
||||
addNodes([createStartNode()]);
|
||||
};
|
||||
|
||||
// 新建工作流
|
||||
const createNewWorkflow = () => {
|
||||
currentEditingWorkflowId.value = null;
|
||||
nodes.value = [];
|
||||
edges.value = [];
|
||||
selectedNode.value = null;
|
||||
nodeId = 0;
|
||||
|
||||
// 添加默认开始节点
|
||||
addDefaultStartNode();
|
||||
// 重置保存表单,避免新建后点保存还带着上次编辑/未保存的名称描述
|
||||
saveForm.value.flowName = '';
|
||||
saveForm.value.description = '';
|
||||
|
||||
// 直接整体赋值 v-model(nodes/edges),不再混用 removeNodes/addNodes 直接改 store,
|
||||
// 避免 VueFlow 双向 watch(外部↔store)竞争导致开始节点丢失
|
||||
nodes.value = [createStartNode()];
|
||||
edges.value = [];
|
||||
|
||||
ElMessage.success('已清空画布,可以开始创建新工作流');
|
||||
};
|
||||
@@ -773,7 +787,7 @@ const buildNodeFormConfigFromDsl = (n: any) => {
|
||||
});
|
||||
}
|
||||
// 新格式:HTTP 节点 outputConfig 为 {type,field,label,value} 数组
|
||||
if (Array.isArray(n.outputConfig) && n.outputConfig.length > 0) {
|
||||
if (Array.isArray(n.outputConfig) && n.outputConfig.length > 0 && n.nodeCode !== '__start__') {
|
||||
const defs = nodeConfigMap.value.get(n.nodeCode)?.formConfig || [];
|
||||
// 无 presetOption 定义(如 form 节点为命名值数组)时直接透传
|
||||
if (defs.length === 0) return n.outputConfig;
|
||||
@@ -799,7 +813,7 @@ const buildNodeFormConfigFromDsl = (n: any) => {
|
||||
return null;
|
||||
};
|
||||
|
||||
// 构建保存用的 outputConfig(HTTP/form 节点),其余返回 null
|
||||
// 构建保存用的 outputConfig(HTTP/form 节点),其余返回 null(开始节点的运行表单字段由保存处单独写入)
|
||||
const buildOutputConfig = (node: Node<NodeData>) => {
|
||||
const nodeCode = node.data?.nodeCode || '';
|
||||
if (nodeCode === 'http') {
|
||||
@@ -855,14 +869,26 @@ const buildModelConfigFromDsl = (n: any) => {
|
||||
}
|
||||
return {
|
||||
modelId: n.modelConfig?.modelId || '',
|
||||
modelName: '',
|
||||
modelType: undefined,
|
||||
// 回读 DSL 中保存的模型名称/类型(旧 DSL 无这些字段时为空,兼容)
|
||||
modelName: n.modelConfig?.modelName || '',
|
||||
modelType: n.modelConfig?.modelType ?? undefined,
|
||||
modelRequestParams,
|
||||
modelFormFields,
|
||||
modelResponseBodyMapping: n.modelConfig?.modelResponseBodyMapping ?? null,
|
||||
};
|
||||
};
|
||||
|
||||
// 从 edge id 反向解析 handle:edge id 形如 edge-{source}-{sourceHandle}-{target}-{targetHandle},
|
||||
// 后端保存时可能丢弃 sourceHandle/targetHandle,用 id 兜底还原,保证连线回显位置正确。
|
||||
const parseEdgeHandleFromId = (id: string) => {
|
||||
const sourceMatch = id.match(/^edge-.+-source-(right|bottom)-.+$/);
|
||||
const targetMatch = id.match(/-target-(top|left)$/);
|
||||
return {
|
||||
sourceHandle: sourceMatch ? `source-${sourceMatch[1]}` : undefined,
|
||||
targetHandle: targetMatch ? `target-${targetMatch[1]}` : undefined,
|
||||
};
|
||||
};
|
||||
|
||||
// 从 DSL 加载工作流
|
||||
const loadWorkflowFromDsl = (dsl: any) => {
|
||||
if (!dsl) return;
|
||||
@@ -886,21 +912,33 @@ const loadWorkflowFromDsl = (dsl: any) => {
|
||||
patchLayout: n.patchLayout || false,
|
||||
isSaveFile: Boolean(n.isSaveFile),
|
||||
preTool: n.preTool ?? null,
|
||||
...(isStart ? { runFormFields: Array.isArray(n.runFormFields) ? n.runFormFields : [] } : {}),
|
||||
// 开始节点运行表单字段:新格式存 outputConfig;旧 DSL 顶层 runFormFields 兜底兼容
|
||||
...(isStart
|
||||
? {
|
||||
runFormFields: Array.isArray(n.outputConfig)
|
||||
? n.outputConfig
|
||||
: Array.isArray(n.runFormFields)
|
||||
? n.runFormFields
|
||||
: [],
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const loadedEdges = (dsl.edges || []).map((e: any) => ({
|
||||
id: e.id,
|
||||
source: e.from,
|
||||
target: e.to,
|
||||
sourceHandle: e.sourceHandle || 'source-bottom',
|
||||
targetHandle: e.targetHandle || 'target-top',
|
||||
type: 'smoothstep',
|
||||
animated: true,
|
||||
style: { stroke: '#3b82f6', strokeWidth: 2 },
|
||||
}));
|
||||
const loadedEdges = (dsl.edges || []).map((e: any) => {
|
||||
const fromId = parseEdgeHandleFromId(e.id || '');
|
||||
return {
|
||||
id: e.id,
|
||||
source: e.from,
|
||||
target: e.to,
|
||||
sourceHandle: e.sourceHandle || fromId.sourceHandle || 'source-bottom',
|
||||
targetHandle: e.targetHandle || fromId.targetHandle || 'target-top',
|
||||
type: 'smoothstep',
|
||||
animated: true,
|
||||
style: { stroke: '#3b82f6', strokeWidth: 2 },
|
||||
};
|
||||
});
|
||||
|
||||
nodes.value = loadedNodes;
|
||||
edges.value = loadedEdges;
|
||||
@@ -965,6 +1003,11 @@ const confirmSaveWorkflow = async () => {
|
||||
subConfig: null,
|
||||
modelConfig: {
|
||||
modelId: n.data?.modelConfig?.modelId || '',
|
||||
// 模型名称/类型随工作流保存,供首页补全弹窗展示模型名与「同类型模型」过滤
|
||||
...(n.data?.modelConfig?.modelName ? { modelName: n.data.modelConfig.modelName } : {}),
|
||||
...(n.data?.modelConfig?.modelType !== undefined && n.data.modelConfig.modelType !== null && n.data.modelConfig.modelType !== ''
|
||||
? { modelType: n.data.modelConfig.modelType }
|
||||
: {}),
|
||||
modelRequestParams: n.data?.modelConfig?.modelRequestParams ?? null,
|
||||
// 保存时实时收集勾选的「表单展示」字段(路径带实例索引)
|
||||
...(n.data?.modelConfig?.modelRequestParams
|
||||
@@ -973,12 +1016,12 @@ const confirmSaveWorkflow = async () => {
|
||||
// 模型返回参数随工作流保存,保证重开后仍可被下游引用
|
||||
modelResponseBodyMapping: n.data?.modelConfig?.modelResponseBodyMapping ?? null,
|
||||
},
|
||||
outputConfig: buildOutputConfig(n),
|
||||
// 开始节点:运行表单字段存入 outputConfig(不再单独存顶层 runFormFields 字段)
|
||||
outputConfig: isStartNode(n) ? (n.data?.runFormFields ?? []) : buildOutputConfig(n),
|
||||
...(n.data?.skillName ? { skillName: n.data.skillName } : {}),
|
||||
...(n.data?.prompt ? { prompt: n.data.prompt } : {}),
|
||||
...(n.data?.negativePrompt ? { negativePrompt: n.data.negativePrompt } : {}),
|
||||
...(n.data?.patchLayout ? { patchLayout: n.data.patchLayout } : {}),
|
||||
...(isStartNode(n) ? { runFormFields: n.data?.runFormFields ?? [] } : {}),
|
||||
outputResult: null,
|
||||
};
|
||||
}),
|
||||
@@ -1037,6 +1080,12 @@ const deleteWorkflowAction = async (workflow: WorkflowItem) => {
|
||||
currentEditingWorkflowId.value = null;
|
||||
saveForm.value.flowName = '';
|
||||
saveForm.value.description = '';
|
||||
// 删除的是当前编辑中的工作流时,恢复画布到默认开始节点,避免旧节点/连线残留。
|
||||
// 整体赋值 v-model,与 createNewWorkflow 一致,避免直接改 store 触发 watch 竞争
|
||||
selectedNode.value = null;
|
||||
nodeId = 0;
|
||||
nodes.value = [createStartNode()];
|
||||
edges.value = [];
|
||||
}
|
||||
|
||||
await fetchWorkflowList();
|
||||
@@ -1052,6 +1101,14 @@ onMounted(async () => {
|
||||
await getNodeLibrary();
|
||||
await fetchWorkflowList();
|
||||
|
||||
// 获取当前用户是否为超级管理员(管理员绘制模板时选系统模型无需填 API Key)
|
||||
try {
|
||||
const res: any = await checkIsSuperAdmin();
|
||||
isSuperAdmin.value = res.data?.isSuperAdmin || false;
|
||||
} catch {
|
||||
isSuperAdmin.value = false;
|
||||
}
|
||||
|
||||
// 添加默认开始节点(固定在页面中间偏左)
|
||||
addDefaultStartNode();
|
||||
nodeId = 0;
|
||||
|
||||
Reference in New Issue
Block a user