针对工作流管理进行组件抽离和与内容创作进行安全隔离
This commit is contained in:
@@ -109,6 +109,7 @@ These rules capture long-term repository preferences confirmed by the user and s
|
||||
5. **改动评分与总结**:每次修改完成后,对本次改动按 1-10 分打分,并附上总结和改进建议
|
||||
6. **中文沟通**:所有与用户的沟通、说明、思考展示必须使用中文
|
||||
7. **操作后清理**:每次文件操作(拆分、替换、脚本执行等)完成后,必须检查并清理产生的临时文件,不得遗留在项目目录中
|
||||
8. **内容创作与工作流管理必须隔离**:`src/views/settings/creation`(内容创作)是线上运行的旧版本,所有改动只服务于工作流管理(`src/views/settings/workflow`),绝对不修改内容创作的代码、API、组件或数据结构。工作流如需共享能力,应为自身新建独立版本(如独立的 `src/api/settings/workflow/`、本地组件),不得改动内容创作在用的东西。后端为工作流管理做的数据变化也属于工作流管理,不必适配内容创作。
|
||||
|
||||
### Pagination Rules
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import request from '/@/utils/request';
|
||||
|
||||
// ===== 节点库(V2 结构,workflow 专用;creation 继续使用 creation/index.ts 的旧结构)=====
|
||||
export interface NodeLibraryPresetOption {
|
||||
value?: unknown;
|
||||
field: string;
|
||||
label: string;
|
||||
type: string;
|
||||
required: boolean;
|
||||
options?: Array<{
|
||||
key: string;
|
||||
value: string;
|
||||
config?: NodeLibraryPresetOption[] | null;
|
||||
}> | null;
|
||||
}
|
||||
|
||||
export interface NodeLibraryItem {
|
||||
key: string;
|
||||
name: string;
|
||||
group: string;
|
||||
sort: number;
|
||||
desc?: string;
|
||||
batchExecOption: boolean;
|
||||
preToolOption: boolean;
|
||||
postToolOption: boolean;
|
||||
skillOption: boolean;
|
||||
promptOption: boolean;
|
||||
isSaveFileOption: boolean;
|
||||
formConfigOption: boolean;
|
||||
modelConfigOption: boolean;
|
||||
presetOption: NodeLibraryPresetOption[] | null;
|
||||
}
|
||||
|
||||
export interface NodeLibraryGroup {
|
||||
group: { key: string; name: string };
|
||||
nodes: NodeLibraryItem[];
|
||||
}
|
||||
|
||||
export function getNodeLibraryList() {
|
||||
return request({
|
||||
url: '/ai-agent/node/library/list',
|
||||
method: 'get',
|
||||
}) as Promise<{ code: number; message: string; data: { groups: NodeLibraryGroup[] } }>;
|
||||
}
|
||||
|
||||
// ===== 模型列表(不按类型过滤,显示全部)=====
|
||||
export interface WorkflowModelItem {
|
||||
id: string;
|
||||
modelName: string;
|
||||
modelType: number | string;
|
||||
baseUrl?: string;
|
||||
enabled?: number;
|
||||
isOwner?: number;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export function getWorkflowModelList(params?: { pageNum: number; pageSize: number; modelName?: string }) {
|
||||
return request({
|
||||
url: '/model-gateway/model/listModel',
|
||||
method: 'get',
|
||||
params,
|
||||
}) as Promise<{ code: number; message: string; data: { list: WorkflowModelItem[]; total: number } }>;
|
||||
}
|
||||
|
||||
// ===== 技能列表 =====
|
||||
export interface WorkflowSkillItem {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
category: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export function getWorkflowSkillList(params?: { pageNum: number; pageSize: number; keyword?: string }) {
|
||||
return request({
|
||||
url: '/ai-agent/skill/user/listUser',
|
||||
method: 'get',
|
||||
params,
|
||||
}) as Promise<{ code: number; message: string; data: { list: WorkflowSkillItem[]; total: number } }>;
|
||||
}
|
||||
@@ -1,279 +0,0 @@
|
||||
<template>
|
||||
<div class="input-source-manager">
|
||||
<el-divider content-position="left">上级参数引用</el-divider>
|
||||
|
||||
<!-- 已引用的参数列表 -->
|
||||
<div v-if="currentInputSource && currentInputSource.length > 0" class="input-source-list">
|
||||
<div v-for="(sourceNode, index) in currentInputSource" :key="index" class="input-source-item">
|
||||
<div class="input-source-header">
|
||||
<span class="input-source-node-name">{{ getNodeName(sourceNode.nodeId) }}</span>
|
||||
</div>
|
||||
<div v-if="sourceNode.field && sourceNode.field.length > 0" class="input-source-fields">
|
||||
<div v-for="fieldName in sourceNode.field" :key="fieldName" class="field-tag">
|
||||
<el-tag size="small">{{ fieldName }}</el-tag>
|
||||
<el-button type="danger" link size="small" @click="emit('removeField', sourceNode.nodeId, fieldName)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-source-output">
|
||||
<el-switch
|
||||
:model-value="sourceNode.quoteOutput === true"
|
||||
@change="(val: boolean) => emit('toggleOutput', sourceNode.nodeId, val)"
|
||||
size="small"
|
||||
active-text="引入输出"
|
||||
inactive-text=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 显示所有上级节点的输出引用选项 -->
|
||||
<div v-if="availableParentNodes.length > 0" class="parent-nodes-output">
|
||||
<div class="parent-nodes-title">上级节点输出</div>
|
||||
<div v-for="parentNode in availableParentNodes" :key="parentNode.id" class="parent-node-output-item">
|
||||
<span class="parent-node-name">{{ parentNode.name }}</span>
|
||||
<el-switch
|
||||
:model-value="isNodeOutputQuoted(parentNode.id)"
|
||||
@change="(val: boolean) => emit('toggleOutput', parentNode.id, val)"
|
||||
size="small"
|
||||
active-text="引入输出"
|
||||
inactive-text=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 选择参数下拉框 -->
|
||||
<el-form-item label="选择参数">
|
||||
<el-select :model-value="selectedParam" @update:model-value="handleParamSelect" placeholder="选择上级节点的参数" class="w100">
|
||||
<el-option v-for="param in availableParams" :key="param.value" :label="param.label" :value="param.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import type { Node } from '@vue-flow/core';
|
||||
|
||||
interface NodeData {
|
||||
label?: string;
|
||||
nodeCode?: string;
|
||||
inputSource?: Array<{ nodeId: string; field: string[]; quoteOutput?: boolean }> | null;
|
||||
formConfig?: any[];
|
||||
modelConfig?: any;
|
||||
skillName?: string;
|
||||
}
|
||||
|
||||
interface ParentNode {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface ParamOption {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
selectedNode: Node<NodeData, any, string> | null;
|
||||
nodes: Node<NodeData, any, string>[];
|
||||
edges: any[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'removeField', nodeId: string, fieldName: string): void;
|
||||
(e: 'toggleOutput', nodeId: string, enabled: boolean): void;
|
||||
(e: 'addParam', paramValue: string): void;
|
||||
}>();
|
||||
|
||||
const selectedParam = ref('');
|
||||
|
||||
// 当前节点的 inputSource
|
||||
const currentInputSource = computed(() => {
|
||||
if (!props.selectedNode?.data?.inputSource) return [];
|
||||
return props.selectedNode.data.inputSource.filter((item) => item.field && item.field.length > 0);
|
||||
});
|
||||
|
||||
// 获取节点名称
|
||||
const getNodeName = (nodeId: string) => {
|
||||
const node = props.nodes.find((n) => n.id === nodeId);
|
||||
return node?.data?.label || nodeId;
|
||||
};
|
||||
|
||||
// 获取所有上级节点(用于显示输出引用选项)
|
||||
const availableParentNodes = computed(() => {
|
||||
if (!props.selectedNode?.data) return [];
|
||||
|
||||
// 获取已经引用了字段的节点ID列表
|
||||
const inputSource = props.selectedNode.data.inputSource;
|
||||
const nodesWithFields = new Set<string>();
|
||||
if (Array.isArray(inputSource)) {
|
||||
inputSource.forEach((item) => {
|
||||
if (item.field && item.field.length > 0) {
|
||||
nodesWithFields.add(item.nodeId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 递归查找所有上级节点
|
||||
const findAllParentNodes = (nodeId: string, visited = new Set<string>()): string[] => {
|
||||
if (visited.has(nodeId)) return [];
|
||||
visited.add(nodeId);
|
||||
|
||||
const incomingEdges = props.edges.filter((e) => e.target === nodeId);
|
||||
const parentIds: string[] = [];
|
||||
|
||||
incomingEdges.forEach((edge) => {
|
||||
parentIds.push(edge.source);
|
||||
parentIds.push(...findAllParentNodes(edge.source, visited));
|
||||
});
|
||||
|
||||
return parentIds;
|
||||
};
|
||||
|
||||
const allParentIds = findAllParentNodes(props.selectedNode.id);
|
||||
const parentNodes = allParentIds
|
||||
.map((parentId) => {
|
||||
const parentNode = props.nodes.find((n) => n.id === parentId);
|
||||
if (!parentNode?.data) return null;
|
||||
|
||||
const nodeCode = String(parentNode.data.nodeCode || '').toLowerCase();
|
||||
const isJudge = ['判断', 'judge', 'condition', 'if', 'branch', 'gateway'].some((k) => nodeCode.includes(k));
|
||||
const isStart = nodeCode === '__start__';
|
||||
|
||||
if (isJudge || isStart || nodesWithFields.has(parentId)) return null;
|
||||
|
||||
return {
|
||||
id: parentId,
|
||||
name: parentNode.data.label || parentId,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
return parentNodes as ParentNode[];
|
||||
});
|
||||
|
||||
// 检查节点输出是否被引用
|
||||
const isNodeOutputQuoted = (nodeId: string): boolean => {
|
||||
if (!props.selectedNode?.data) return false;
|
||||
const inputSource = props.selectedNode.data.inputSource;
|
||||
if (!Array.isArray(inputSource)) return false;
|
||||
|
||||
const node = inputSource.find((item) => item.nodeId === nodeId);
|
||||
return node?.quoteOutput === true;
|
||||
};
|
||||
|
||||
// 获取可用的参数选项
|
||||
const availableParams = computed(() => {
|
||||
if (!props.selectedNode) return [];
|
||||
|
||||
const params: ParamOption[] = [];
|
||||
const visited = new Set<string>();
|
||||
|
||||
const findParents = (nodeId: string) => {
|
||||
if (visited.has(nodeId)) return;
|
||||
visited.add(nodeId);
|
||||
|
||||
props.edges
|
||||
.filter((e) => e.target === nodeId)
|
||||
.forEach((edge) => {
|
||||
const parent = props.nodes.find((n) => n.id === edge.source);
|
||||
if (parent?.data && parent.data.nodeCode !== '__start__' && parent.data.nodeCode !== 'judge') {
|
||||
params.push({
|
||||
label: `${parent.data.label}.output`,
|
||||
value: `\${${parent.id}.output}`,
|
||||
});
|
||||
}
|
||||
findParents(edge.source);
|
||||
});
|
||||
};
|
||||
|
||||
findParents(props.selectedNode.id);
|
||||
return params;
|
||||
});
|
||||
|
||||
const handleParamSelect = (value: string) => {
|
||||
if (!value) return;
|
||||
emit('addParam', value);
|
||||
selectedParam.value = '';
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.input-source-manager {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.input-source-list {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.input-source-item {
|
||||
padding: 12px;
|
||||
margin-bottom: 12px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.input-source-header {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.input-source-node-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.input-source-fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.field-tag {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.input-source-output {
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.parent-nodes-output {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.parent-nodes-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.parent-node-output-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 6px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.parent-node-name {
|
||||
font-size: 13px;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.w100 {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,268 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="选择模型" width="1000px" :close-on-click-modal="false" @close="handleClose">
|
||||
<div class="model-selector-header">
|
||||
<div class="search-bar">
|
||||
<el-input v-model="searchParams.modelName" placeholder="搜索模型名称" clearable @clear="handleSearch">
|
||||
<template #prefix
|
||||
><el-icon> <Search /> </el-icon
|
||||
></template>
|
||||
</el-input>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="model-list" v-loading="loading">
|
||||
<el-empty v-if="!loading && modelList.length === 0" description="暂无模型数据" :image-size="100" />
|
||||
<div v-else class="model-grid">
|
||||
<div
|
||||
v-for="model in modelList"
|
||||
:key="model.id"
|
||||
class="model-card"
|
||||
:class="{ selected: selectedModel?.id === model.id }"
|
||||
@click="handleSelectModel(model)"
|
||||
>
|
||||
<div class="model-card-header">
|
||||
<div class="model-type">{{ getModelTypeName(model.modelType) }}</div>
|
||||
<el-icon v-if="selectedModel?.id === model.id" class="check-icon" color="#67c23a">
|
||||
<CircleCheck />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="model-card-body">
|
||||
<h3 class="model-name">{{ model.modelName }}</h3>
|
||||
<p class="model-url">{{ model.baseUrl }}</p>
|
||||
<div class="model-status">
|
||||
<el-tag :type="model.enabled === 1 ? 'success' : 'info'" size="small">
|
||||
{{ model.enabled === 1 ? '已启用' : '已禁用' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="pagination.total > 0" class="pagination-wrap">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.pageNum"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, prev, pager, next"
|
||||
small
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleConfirm" :disabled="!selectedModel">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { Search, CircleCheck } from '@element-plus/icons-vue';
|
||||
import { getWorkflowModelList, type WorkflowModelItem } from '/@/api/settings/workflow';
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean;
|
||||
defaultModel?: WorkflowModelItem | null;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void;
|
||||
(e: 'confirm', model: WorkflowModelItem): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
defaultModel: null,
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const visible = ref(false);
|
||||
const searchParams = reactive({ modelName: '' });
|
||||
const pagination = reactive({ pageNum: 1, pageSize: 10, total: 0 });
|
||||
const modelList = ref<WorkflowModelItem[]>([]);
|
||||
const loading = ref(false);
|
||||
const selectedModel = ref<WorkflowModelItem | null>(null);
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
visible.value = val;
|
||||
if (val) {
|
||||
selectedModel.value = props.defaultModel || null;
|
||||
pagination.pageNum = 1;
|
||||
fetchModelList();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(visible, (val) => {
|
||||
if (!val) {
|
||||
emit('update:modelValue', false);
|
||||
}
|
||||
});
|
||||
|
||||
const getModelTypeName = (type: number | string) => {
|
||||
const typeMap: Record<number, string> = {
|
||||
100: '推理模型',
|
||||
200: '图片模型',
|
||||
201: '图片模型-文生图',
|
||||
202: '图片模型-图生图',
|
||||
203: '图片模型-图片编辑',
|
||||
204: '图片模型-图片变体',
|
||||
300: '音频模型',
|
||||
301: '音频模型-文生音',
|
||||
302: '音频模型-音生文',
|
||||
303: '音频模型-音生音',
|
||||
400: '向量化模型',
|
||||
401: '向量化模型-文本嵌入',
|
||||
402: '向量化模型-重排序',
|
||||
500: '全模态模型',
|
||||
501: '全模态模型-文图音',
|
||||
502: '全模态模型-视觉理解',
|
||||
600: '视频模型',
|
||||
601: '视频模型-文生视频',
|
||||
602: '视频模型-图生视频',
|
||||
603: '视频模型-图文生视频',
|
||||
604: '视频模型-视频生视频',
|
||||
605: '视频模型-视频编辑',
|
||||
};
|
||||
return typeMap[Number(type)] || '未知类型';
|
||||
};
|
||||
|
||||
const fetchModelList = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params = {
|
||||
pageNum: pagination.pageNum,
|
||||
pageSize: pagination.pageSize,
|
||||
modelName: searchParams.modelName || undefined,
|
||||
};
|
||||
const res = await getWorkflowModelList(params);
|
||||
modelList.value = res.data?.list || [];
|
||||
pagination.total = res.data?.total || 0;
|
||||
} catch {
|
||||
modelList.value = [];
|
||||
pagination.total = 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.pageNum = 1;
|
||||
fetchModelList();
|
||||
};
|
||||
|
||||
const handlePageChange = () => {
|
||||
fetchModelList();
|
||||
};
|
||||
|
||||
const handleSelectModel = (model: WorkflowModelItem) => {
|
||||
selectedModel.value = model;
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (selectedModel.value) {
|
||||
emit('confirm', selectedModel.value);
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
visible.value = false;
|
||||
selectedModel.value = null;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.model-selector-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
gap: 12px;
|
||||
}
|
||||
.search-bar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
}
|
||||
.model-list {
|
||||
min-height: 300px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.model-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
.model-card {
|
||||
background: #f8fafc;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
.model-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.model-card.selected {
|
||||
border-color: #67c23a;
|
||||
background: #f0f9ff;
|
||||
}
|
||||
.model-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.model-type {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
background: #eff6ff;
|
||||
color: #3b82f6;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.check-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
.model-card-body {
|
||||
flex: 1;
|
||||
}
|
||||
.model-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin: 0 0 8px 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.model-url {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
margin: 0 0 8px 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.model-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.pagination-wrap {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -11,7 +11,7 @@
|
||||
</el-form-item>
|
||||
|
||||
<!-- 模型选择 -->
|
||||
<el-form-item v-if="nodeConfig?.modelConfig && nodeConfig.modelConfig.length > 0" label="选择模型">
|
||||
<el-form-item v-if="nodeConfig?.modelConfigOption" label="选择模型">
|
||||
<el-button type="primary" @click="emit('openModelSelector')" style="width: 100%">选择模型</el-button>
|
||||
<div v-if="selectedNode.data?.modelConfig?.modelName" class="selected-tag">
|
||||
<el-tag type="success" size="large" closable @close="emit('removeModel')">
|
||||
@@ -31,7 +31,7 @@
|
||||
</el-form-item>
|
||||
|
||||
<!-- 贴片布局开关 -->
|
||||
<el-form-item v-if="nodeConfig?.patchLayout" label="贴片布局">
|
||||
<el-form-item v-if="isVideoModelSelected" label="贴片布局">
|
||||
<el-switch
|
||||
:model-value="selectedNode.data?.patchLayout ?? false"
|
||||
@update:model-value="emit('update:patchLayout', $event)"
|
||||
@@ -95,17 +95,6 @@
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-form>
|
||||
|
||||
<!-- 上级参数管理 -->
|
||||
<InputSourceManager
|
||||
v-if="selectedNode.data?.nodeCode !== '__start__'"
|
||||
:selected-node="selectedNode"
|
||||
:nodes="allNodes"
|
||||
:edges="allEdges"
|
||||
@remove-field="(nodeId: string, fieldName: string) => emit('removeField', nodeId, fieldName)"
|
||||
@toggle-output="(nodeId: string, enabled: boolean) => emit('toggleOutput', nodeId, enabled)"
|
||||
@add-param="(paramValue: string) => emit('addParamByValue', paramValue)"
|
||||
/>
|
||||
</div>
|
||||
<el-empty v-else description="请选择一个节点" :image-size="100" />
|
||||
|
||||
@@ -123,52 +112,45 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { ref, computed } from 'vue';
|
||||
import type { Node } from '@vue-flow/core';
|
||||
import InputSourceManager from './InputSourceManager.vue';
|
||||
import KeyValueEditor from './KeyValueEditor.vue';
|
||||
import { JsonEditor } from '/@/components/json-schema-editor';
|
||||
|
||||
interface NodeData {
|
||||
label?: string;
|
||||
nodeCode?: string;
|
||||
inputSource?: Array<{ nodeId: string; field: string[]; quoteOutput?: boolean }> | null;
|
||||
formConfig?: any[];
|
||||
modelConfig?: any;
|
||||
skillName?: string;
|
||||
patchLayout?: boolean;
|
||||
}
|
||||
|
||||
interface ParamRef {
|
||||
id: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface NodeConfig {
|
||||
formConfig: any[];
|
||||
modelConfig: any[];
|
||||
modelConfigOption: boolean;
|
||||
formConfigOption: boolean;
|
||||
skillOption: boolean;
|
||||
patchLayout: boolean;
|
||||
promptOption: boolean;
|
||||
isSaveFileOption: boolean;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
selectedNode: Node<NodeData, any, string> | null;
|
||||
availableParams: ParamRef[];
|
||||
nodeConfig: NodeConfig | null;
|
||||
allNodes: Node<NodeData, any, string>[];
|
||||
allEdges: any[];
|
||||
}>();
|
||||
|
||||
const isVideoModelSelected = computed(() => {
|
||||
const t = Number(props.selectedNode?.data?.modelConfig?.modelType);
|
||||
return t >= 600 && t <= 605;
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:selectedNode', node: Node<NodeData, any, string>): void;
|
||||
(e: 'addParam', param: ParamRef): void;
|
||||
(e: 'openModelSelector'): void;
|
||||
(e: 'removeModel'): void;
|
||||
(e: 'openSkillSelector'): void;
|
||||
(e: 'removeSkill'): void;
|
||||
(e: 'removeField', nodeId: string, fieldName: string): void;
|
||||
(e: 'toggleOutput', nodeId: string, enabled: boolean): void;
|
||||
(e: 'addParamByValue', paramValue: string): void;
|
||||
(e: 'update:patchLayout', value: boolean): void;
|
||||
}>();
|
||||
|
||||
|
||||
@@ -12,17 +12,17 @@
|
||||
<div v-if="!collapsed" class="node-library-content">
|
||||
<el-empty v-if="nodeLibraryGroups.length === 0" description="暂无节点" :image-size="40" />
|
||||
<div v-else class="node-library-groups">
|
||||
<div v-for="group in nodeLibraryGroups" :key="group.group" class="node-group">
|
||||
<div class="node-group-title">{{ group.label }}</div>
|
||||
<div v-for="group in nodeLibraryGroups" :key="group.group.key" class="node-group">
|
||||
<div class="node-group-title">{{ group.group.name }}</div>
|
||||
<div class="node-group-items">
|
||||
<el-button
|
||||
v-for="item in group.items"
|
||||
:key="item.nodeCode"
|
||||
v-for="item in group.nodes"
|
||||
:key="item.key"
|
||||
text
|
||||
class="node-item"
|
||||
@click="emit('addNode', item.nodeCode, item.nodeName)"
|
||||
@click="emit('addNode', item.key, item.name)"
|
||||
>
|
||||
{{ item.nodeName }}
|
||||
{{ item.name }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -33,7 +33,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ArrowLeftBold, ArrowRightBold } from '@element-plus/icons-vue';
|
||||
import type { NodeLibraryGroup } from '/@/api/settings/creation';
|
||||
import type { NodeLibraryGroup } from '/@/api/settings/workflow';
|
||||
|
||||
defineProps<{
|
||||
nodeLibraryGroups: NodeLibraryGroup[];
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="选择技能" width="900px" :close-on-click-modal="false" @close="handleClose">
|
||||
<div class="search-bar">
|
||||
<el-input v-model="searchParams.keyword" placeholder="搜索技能名称或描述" clearable @clear="handleSearch">
|
||||
<template #prefix
|
||||
><el-icon><Search /></el-icon
|
||||
></template>
|
||||
</el-input>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
</div>
|
||||
<div class="skill-list" v-loading="loading">
|
||||
<el-empty v-if="!loading && skillList.length === 0" description="暂无技能数据" :image-size="100" />
|
||||
<div v-else class="skill-grid">
|
||||
<div
|
||||
v-for="skill in skillList"
|
||||
:key="skill.id"
|
||||
class="skill-card"
|
||||
:class="{ selected: selectedSkill?.id === skill.id }"
|
||||
@click="handleSelectSkill(skill)"
|
||||
>
|
||||
<div class="skill-card-header">
|
||||
<div class="skill-category">{{ skill.category }}</div>
|
||||
<el-icon v-if="selectedSkill?.id === skill.id" class="check-icon" color="#67c23a"><CircleCheck /></el-icon>
|
||||
</div>
|
||||
<div class="skill-card-body">
|
||||
<h3 class="skill-name">{{ skill.name }}</h3>
|
||||
<p class="skill-desc">{{ skill.description || '暂无描述' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="pagination.total > 0" class="pagination-wrap">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.pageNum"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, prev, pager, next"
|
||||
small
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleConfirm" :disabled="!selectedSkill">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { Search, CircleCheck } from '@element-plus/icons-vue';
|
||||
import { getWorkflowSkillList, type WorkflowSkillItem } from '/@/api/settings/workflow';
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean;
|
||||
defaultSkill?: WorkflowSkillItem | null;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void;
|
||||
(e: 'confirm', skill: WorkflowSkillItem): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
defaultSkill: null,
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const visible = ref(false);
|
||||
const searchParams = reactive({ keyword: '' });
|
||||
const pagination = reactive({ pageNum: 1, pageSize: 10, total: 0 });
|
||||
const skillList = ref<WorkflowSkillItem[]>([]);
|
||||
const loading = ref(false);
|
||||
const selectedSkill = ref<WorkflowSkillItem | null>(null);
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
visible.value = val;
|
||||
if (val) {
|
||||
selectedSkill.value = props.defaultSkill || null;
|
||||
fetchSkillList();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(visible, (val) => {
|
||||
if (!val) {
|
||||
emit('update:modelValue', false);
|
||||
}
|
||||
});
|
||||
|
||||
const fetchSkillList = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params = { pageNum: pagination.pageNum, pageSize: pagination.pageSize, keyword: searchParams.keyword || undefined };
|
||||
const res = await getWorkflowSkillList(params);
|
||||
skillList.value = res.data?.list || [];
|
||||
pagination.total = res.data?.total || 0;
|
||||
} catch {
|
||||
skillList.value = [];
|
||||
pagination.total = 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.pageNum = 1;
|
||||
fetchSkillList();
|
||||
};
|
||||
|
||||
const handlePageChange = () => {
|
||||
fetchSkillList();
|
||||
};
|
||||
|
||||
const handleSelectSkill = (skill: WorkflowSkillItem) => {
|
||||
selectedSkill.value = skill;
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (selectedSkill.value) {
|
||||
emit('confirm', selectedSkill.value);
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
visible.value = false;
|
||||
selectedSkill.value = null;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.search-bar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.skill-list {
|
||||
min-height: 300px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.skill-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
.skill-card {
|
||||
background: #f8fafc;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
.skill-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.skill-card.selected {
|
||||
border-color: #67c23a;
|
||||
background: #f0f9ff;
|
||||
}
|
||||
.skill-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.skill-category {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
background: #eff6ff;
|
||||
color: #3b82f6;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.check-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
.skill-card-body {
|
||||
flex: 1;
|
||||
}
|
||||
.skill-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin: 0 0 8px 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.skill-desc {
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
min-height: 40px;
|
||||
}
|
||||
.pagination-wrap {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -18,19 +18,12 @@
|
||||
<!-- 左侧:配置面板 -->
|
||||
<NodeConfigPanel
|
||||
:selected-node="selectedNode"
|
||||
:available-params="availableParams"
|
||||
:node-config="currentNodeConfig"
|
||||
:all-nodes="nodes"
|
||||
:all-edges="edges"
|
||||
@update:selected-node="updateSelectedNode"
|
||||
@add-param="addParam"
|
||||
@open-model-selector="showModelSelector = true"
|
||||
@remove-model="handleRemoveModel"
|
||||
@open-skill-selector="showSkillSelector = true"
|
||||
@remove-skill="handleRemoveSkill"
|
||||
@remove-field="handleRemoveField"
|
||||
@toggle-output="handleToggleOutput"
|
||||
@add-param-by-value="handleAddParamByValue"
|
||||
@update:patch-layout="handleTogglePatchLayout"
|
||||
/>
|
||||
|
||||
@@ -81,7 +74,7 @@
|
||||
/>
|
||||
|
||||
<!-- 模型选择器 -->
|
||||
<ModelSelector v-model="showModelSelector" :default-model="selectedModelData" :model-type="currentNodeModelType" @confirm="handleModelConfirm" />
|
||||
<ModelSelector v-model="showModelSelector" :default-model="selectedModelData" @confirm="handleModelConfirm" />
|
||||
|
||||
<!-- 技能选择器 -->
|
||||
<SkillSelector v-model="showSkillSelector" :default-skill="selectedSkillData" @confirm="handleSkillConfirm" />
|
||||
@@ -97,37 +90,30 @@ import { MiniMap } from '@vue-flow/minimap';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import type { Node, Edge, Connection } from '@vue-flow/core';
|
||||
import {
|
||||
getNodeLibraryList,
|
||||
getWorkflowList,
|
||||
getWorkflowDetail,
|
||||
saveWorkflow,
|
||||
updateWorkflow,
|
||||
deleteWorkflow,
|
||||
type NodeLibraryGroup,
|
||||
type WorkflowItem,
|
||||
} from '/@/api/settings/creation';
|
||||
import { getNodeLibraryList, type NodeLibraryGroup } from '/@/api/settings/workflow';
|
||||
import NodeConfigPanel from './component/NodeConfigPanel.vue';
|
||||
import NodeLibraryPanel from './component/NodeLibraryPanel.vue';
|
||||
import WorkflowListPanel from './component/WorkflowListPanel.vue';
|
||||
import SaveWorkflowDialog from './component/SaveWorkflowDialog.vue';
|
||||
import ModelSelector from '/@/components/model/ModelSelector.vue';
|
||||
import SkillSelector from '/@/components/skill/SkillSelector.vue';
|
||||
import ModelSelector from './component/ModelSelector.vue';
|
||||
import SkillSelector from './component/SkillSelector.vue';
|
||||
|
||||
interface NodeData {
|
||||
label?: string;
|
||||
nodeCode?: string;
|
||||
inputSource?: Array<{ nodeId: string; field: string[]; quoteOutput?: boolean }> | null;
|
||||
formConfig?: any[];
|
||||
modelConfig?: any;
|
||||
skillName?: string;
|
||||
patchLayout?: boolean;
|
||||
}
|
||||
|
||||
interface ParamRef {
|
||||
id: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const { addNodes, addEdges, findNode, removeNodes, getNodes, updateNode } = useVueFlow();
|
||||
|
||||
// 常量定义
|
||||
@@ -144,14 +130,26 @@ const filteredNodeLibraryGroups = computed(() => {
|
||||
|
||||
// 节点配置映射:nodeCode -> 节点配置
|
||||
const nodeConfigMap = computed(() => {
|
||||
const map = new Map<string, { formConfig: any[]; modelConfig: any[]; skillOption: boolean; patchLayout: boolean }>();
|
||||
const map = new Map<
|
||||
string,
|
||||
{
|
||||
formConfig: any[];
|
||||
modelConfigOption: boolean;
|
||||
formConfigOption: boolean;
|
||||
skillOption: boolean;
|
||||
promptOption: boolean;
|
||||
isSaveFileOption: boolean;
|
||||
}
|
||||
>();
|
||||
nodeLibraryGroups.value.forEach((group) => {
|
||||
group.items.forEach((item) => {
|
||||
map.set(item.nodeCode, {
|
||||
formConfig: item.formConfig || [],
|
||||
modelConfig: item.modelConfig || [],
|
||||
group.nodes.forEach((item) => {
|
||||
map.set(item.key, {
|
||||
formConfig: item.presetOption || [],
|
||||
modelConfigOption: item.modelConfigOption || false,
|
||||
formConfigOption: item.formConfigOption || false,
|
||||
skillOption: item.skillOption || false,
|
||||
patchLayout: item.patchLayout || false,
|
||||
promptOption: item.promptOption || false,
|
||||
isSaveFileOption: item.isSaveFileOption || false,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -186,14 +184,6 @@ let nodeId = 0;
|
||||
// 模型选择器相关状态
|
||||
const showModelSelector = ref(false);
|
||||
const selectedModelData = ref<any>(null);
|
||||
const currentNodeModelType = computed(() => {
|
||||
if (!selectedNode.value || !currentNodeConfig.value) return 0;
|
||||
const modelConfig = currentNodeConfig.value.modelConfig;
|
||||
if (modelConfig && modelConfig.length > 0) {
|
||||
return modelConfig[0].modelType || 0;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
// 技能选择器相关状态
|
||||
const showSkillSelector = ref(false);
|
||||
@@ -266,74 +256,6 @@ const updateSelectedNode = (updatedNode: Node<NodeData, any, string>) => {
|
||||
}
|
||||
};
|
||||
|
||||
const availableParams = computed(() => {
|
||||
if (!selectedNode.value) return [];
|
||||
const parents: ParamRef[] = [];
|
||||
const findParents = (nodeId: string, visited = new Set<string>()) => {
|
||||
if (visited.has(nodeId)) return;
|
||||
visited.add(nodeId);
|
||||
edges.value
|
||||
.filter((e) => e.target === nodeId)
|
||||
.forEach((edge) => {
|
||||
const parent = findNode(edge.source);
|
||||
if (parent?.data && parent.data.nodeCode !== '__start__' && parent.data.nodeCode !== 'judge') {
|
||||
parents.push({ id: parent.id, label: `${parent.data.label}.output` });
|
||||
}
|
||||
findParents(edge.source, visited);
|
||||
});
|
||||
};
|
||||
findParents(selectedNode.value.id);
|
||||
return parents;
|
||||
});
|
||||
|
||||
const addParam = (param: ParamRef) => {
|
||||
if (!selectedNode.value?.data) return;
|
||||
|
||||
const updatedNode: Node<NodeData> = {
|
||||
...selectedNode.value,
|
||||
data: {
|
||||
...selectedNode.value.data,
|
||||
inputSource: selectedNode.value.data.inputSource || [],
|
||||
},
|
||||
};
|
||||
|
||||
// 查找是否已存在该节点的引用
|
||||
const existingIndex = updatedNode.data!.inputSource!.findIndex((item) => item.nodeId === param.id);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
// 已存在,添加 output 到 field 数组
|
||||
const existing = updatedNode.data!.inputSource![existingIndex];
|
||||
if (!existing.field.includes('output')) {
|
||||
existing.field.push('output');
|
||||
selectedNode.value = updatedNode;
|
||||
// 同步更新到 VueFlow 内部状态
|
||||
updateNode(updatedNode.id, updatedNode);
|
||||
const index = nodes.value.findIndex((n) => n.id === updatedNode.id);
|
||||
if (index >= 0) {
|
||||
nodes.value[index] = updatedNode;
|
||||
}
|
||||
ElMessage.success('已添加参数引用');
|
||||
} else {
|
||||
ElMessage.info('该参数已被引用');
|
||||
}
|
||||
} else {
|
||||
// 不存在,创建新的引用
|
||||
updatedNode.data!.inputSource!.push({
|
||||
nodeId: param.id,
|
||||
field: ['output'],
|
||||
quoteOutput: false,
|
||||
});
|
||||
selectedNode.value = updatedNode;
|
||||
// 同步更新到 VueFlow 内部状态
|
||||
updateNode(updatedNode.id, updatedNode);
|
||||
const index = nodes.value.findIndex((n) => n.id === updatedNode.id);
|
||||
if (index >= 0) {
|
||||
nodes.value[index] = updatedNode;
|
||||
}
|
||||
ElMessage.success('已添加参数引用');
|
||||
}
|
||||
};
|
||||
|
||||
// 模型选择确认
|
||||
const handleModelConfirm = (model: any) => {
|
||||
if (!selectedNode.value?.data) return;
|
||||
@@ -344,6 +266,7 @@ const handleModelConfirm = (model: any) => {
|
||||
...selectedNode.value.data,
|
||||
modelConfig: {
|
||||
modelName: model.modelName,
|
||||
modelType: model.modelType,
|
||||
modelApiKey: '',
|
||||
modelForm: model.modelForm || [],
|
||||
modelResponse: model.responseBody || {},
|
||||
@@ -460,146 +383,6 @@ const handleTogglePatchLayout = (value: boolean) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 删除上级参数字段
|
||||
const handleRemoveField = (nodeId: string, fieldName: string) => {
|
||||
if (!selectedNode.value?.data) return;
|
||||
|
||||
const inputSource = selectedNode.value.data.inputSource || [];
|
||||
const nodeIndex = inputSource.findIndex((item) => item.nodeId === nodeId);
|
||||
if (nodeIndex < 0) return;
|
||||
|
||||
const node = inputSource[nodeIndex];
|
||||
const newField = node.field.filter((f) => f !== fieldName);
|
||||
|
||||
let updatedInputSource;
|
||||
if (newField.length > 0) {
|
||||
updatedInputSource = [...inputSource];
|
||||
updatedInputSource[nodeIndex] = { ...node, field: newField };
|
||||
} else {
|
||||
updatedInputSource = inputSource.filter((_, idx) => idx !== nodeIndex);
|
||||
}
|
||||
|
||||
const updatedNode: Node<NodeData> = {
|
||||
...selectedNode.value,
|
||||
data: {
|
||||
...selectedNode.value.data,
|
||||
inputSource: updatedInputSource.length > 0 ? updatedInputSource : null,
|
||||
},
|
||||
};
|
||||
|
||||
selectedNode.value = updatedNode;
|
||||
// 同步更新到 VueFlow 内部状态
|
||||
updateNode(updatedNode.id, updatedNode);
|
||||
const index = nodes.value.findIndex((n) => n.id === updatedNode.id);
|
||||
if (index >= 0) {
|
||||
nodes.value[index] = updatedNode;
|
||||
}
|
||||
|
||||
ElMessage.success(`已删除参数:${fieldName}`);
|
||||
};
|
||||
|
||||
// 切换节点输出引用
|
||||
const handleToggleOutput = (nodeId: string, enabled: boolean) => {
|
||||
if (!selectedNode.value?.data) return;
|
||||
|
||||
const inputSource = selectedNode.value.data.inputSource || [];
|
||||
const nodeIndex = inputSource.findIndex((item) => item.nodeId === nodeId);
|
||||
|
||||
let updatedInputSource;
|
||||
if (nodeIndex >= 0) {
|
||||
updatedInputSource = [...inputSource];
|
||||
updatedInputSource[nodeIndex] = {
|
||||
...updatedInputSource[nodeIndex],
|
||||
quoteOutput: enabled,
|
||||
};
|
||||
} else {
|
||||
updatedInputSource = [
|
||||
...inputSource,
|
||||
{
|
||||
nodeId: nodeId,
|
||||
field: [],
|
||||
quoteOutput: enabled,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const updatedNode: Node<NodeData> = {
|
||||
...selectedNode.value,
|
||||
data: {
|
||||
...selectedNode.value.data,
|
||||
inputSource: updatedInputSource,
|
||||
},
|
||||
};
|
||||
|
||||
selectedNode.value = updatedNode;
|
||||
// 同步更新到 VueFlow 内部状态
|
||||
updateNode(updatedNode.id, updatedNode);
|
||||
const index = nodes.value.findIndex((n) => n.id === updatedNode.id);
|
||||
if (index >= 0) {
|
||||
nodes.value[index] = updatedNode;
|
||||
}
|
||||
|
||||
const parentNode = nodes.value.find((n) => n.id === nodeId);
|
||||
const nodeName = parentNode?.data?.label || '节点';
|
||||
ElMessage.success(enabled ? `已开启引入 ${nodeName} 的输出` : `已关闭引入 ${nodeName} 的输出`);
|
||||
};
|
||||
|
||||
// 通过参数值添加上级参数
|
||||
const handleAddParamByValue = (paramValue: string) => {
|
||||
if (!selectedNode.value?.data) return;
|
||||
|
||||
const match = paramValue.match(/\$\{([^.]+)\.(.+)\}/);
|
||||
if (!match) return;
|
||||
|
||||
const nodeId = match[1];
|
||||
const paramName = match[2];
|
||||
|
||||
const inputSource = selectedNode.value.data.inputSource || [];
|
||||
const existingIndex = inputSource.findIndex((item) => item.nodeId === nodeId);
|
||||
|
||||
let updatedInputSource;
|
||||
if (existingIndex >= 0) {
|
||||
const existing = inputSource[existingIndex];
|
||||
if (!existing.field.includes(paramName)) {
|
||||
updatedInputSource = [...inputSource];
|
||||
updatedInputSource[existingIndex] = {
|
||||
...existing,
|
||||
field: [...existing.field, paramName],
|
||||
};
|
||||
} else {
|
||||
ElMessage.info('该参数已被引用');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
updatedInputSource = [
|
||||
...inputSource,
|
||||
{
|
||||
nodeId: nodeId,
|
||||
field: [paramName],
|
||||
quoteOutput: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const updatedNode: Node<NodeData> = {
|
||||
...selectedNode.value,
|
||||
data: {
|
||||
...selectedNode.value.data,
|
||||
inputSource: updatedInputSource,
|
||||
},
|
||||
};
|
||||
|
||||
selectedNode.value = updatedNode;
|
||||
// 同步更新到 VueFlow 内部状态
|
||||
updateNode(updatedNode.id, updatedNode);
|
||||
const index = nodes.value.findIndex((n) => n.id === updatedNode.id);
|
||||
if (index >= 0) {
|
||||
nodes.value[index] = updatedNode;
|
||||
}
|
||||
|
||||
ElMessage.success(`已添加上级参数:${paramName}`);
|
||||
};
|
||||
|
||||
// 辅助函数:判断是否为开始节点
|
||||
const isStartNode = (node: Node<NodeData, any, string>) => {
|
||||
return node.data?.nodeCode === START_NODE_CODE;
|
||||
@@ -686,7 +469,7 @@ const addNodeFromLibrary = (nodeCode: string, nodeName: string) => {
|
||||
id: `node-${++nodeId}`,
|
||||
type: 'default',
|
||||
position: { x: spawnX, y: spawnY },
|
||||
data: { label: nodeName, nodeCode, inputSource: null, patchLayout: false },
|
||||
data: { label: nodeName, nodeCode, patchLayout: false },
|
||||
style: { background: '#fff', border: '2px solid #3b82f6', borderRadius: '8px', padding: '10px 20px' },
|
||||
},
|
||||
]);
|
||||
@@ -714,7 +497,7 @@ const addDefaultStartNode = () => {
|
||||
id: 'start-node',
|
||||
type: 'input',
|
||||
position: { x: 200, y: 200 },
|
||||
data: { label: '开始', nodeCode: '__start__', inputSource: null },
|
||||
data: { label: '开始', nodeCode: '__start__' },
|
||||
style: { background: '#10b981', color: '#fff', border: '2px solid #059669', borderRadius: '8px', padding: '10px 20px' },
|
||||
};
|
||||
addNodes([startNode]);
|
||||
@@ -757,23 +540,6 @@ const loadWorkflowFromDsl = (dsl: any) => {
|
||||
|
||||
try {
|
||||
const loadedNodes = (dsl.nodes || []).map((n: any) => {
|
||||
// 确保 inputSource 使用新结构
|
||||
let normalizedInputSource: Array<{ nodeId: string; field: string[]; quoteOutput?: boolean }> | null = null;
|
||||
|
||||
if (n.inputSource) {
|
||||
if (Array.isArray(n.inputSource)) {
|
||||
// 检查是否为新结构(对象数组)
|
||||
if (n.inputSource.length > 0 && typeof n.inputSource[0] === 'object' && n.inputSource[0].nodeId) {
|
||||
// 已经是新结构
|
||||
normalizedInputSource = n.inputSource;
|
||||
} else {
|
||||
// 旧结构(字符串数组),需要转换
|
||||
normalizedInputSource = [];
|
||||
// 旧结构不再支持,设为空
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: n.id,
|
||||
type: 'default',
|
||||
@@ -781,7 +547,6 @@ const loadWorkflowFromDsl = (dsl: any) => {
|
||||
data: {
|
||||
label: n.name || '',
|
||||
nodeCode: n.nodeCode,
|
||||
inputSource: normalizedInputSource,
|
||||
formConfig: n.formConfig || null,
|
||||
modelConfig: n.modelConfig || null,
|
||||
skillName: n.skillName || null,
|
||||
@@ -855,7 +620,6 @@ const confirmSaveWorkflow = async () => {
|
||||
x: n.position?.x || 0,
|
||||
y: n.position?.y || 0,
|
||||
},
|
||||
inputSource: n.data?.inputSource || null,
|
||||
formConfig: n.data?.formConfig || null,
|
||||
modelConfig: n.data?.modelConfig || null,
|
||||
outputResult: null,
|
||||
|
||||
Reference in New Issue
Block a user