feat: 添加执行列表功能以支持工作流执行管理
- 新增执行列表相关接口和数据结构,支持获取执行流和执行项 - 更新创作页面以展示执行流和预览功能,提升用户交互体验 - 优化树形结构展示,确保根据执行流动态生成节点 - 引入文件上传功能,支持用户上传文件并获取文件URL
This commit is contained in:
@@ -67,6 +67,23 @@ export interface CreationTreeItem {
|
|||||||
contentTypes: CreationContentTypeItem[];
|
contentTypes: CreationContentTypeItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 新的执行列表数据结构
|
||||||
|
export interface ExecutionItem {
|
||||||
|
timestamp: string;
|
||||||
|
content: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExecutionFlowItem {
|
||||||
|
flowName: string;
|
||||||
|
items: ExecutionItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExecutionTreeItem {
|
||||||
|
createDate: string;
|
||||||
|
flows: ExecutionFlowItem[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface CreationListData {
|
export interface CreationListData {
|
||||||
list: unknown[] | null;
|
list: unknown[] | null;
|
||||||
total: number;
|
total: number;
|
||||||
@@ -74,12 +91,23 @@ export interface CreationListData {
|
|||||||
imgAddressPrefix: string;
|
imgAddressPrefix: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ExecutionListData {
|
||||||
|
tree: ExecutionTreeItem[];
|
||||||
|
imgAddressPrefix: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CreationListResponse {
|
export interface CreationListResponse {
|
||||||
code: number;
|
code: number;
|
||||||
message: string;
|
message: string;
|
||||||
data: CreationListData;
|
data: CreationListData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ExecutionListResponse {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
data: ExecutionListData;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CreationSubmitParams {
|
export interface CreationSubmitParams {
|
||||||
mode: string;
|
mode: string;
|
||||||
content_type: string;
|
content_type: string;
|
||||||
@@ -105,6 +133,14 @@ export function getCreationList(params: CreationListParams, requestOptions?: Req
|
|||||||
}) as Promise<CreationListResponse>;
|
}) as Promise<CreationListResponse>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getExecutionList(requestOptions?: RequestOptions) {
|
||||||
|
return request({
|
||||||
|
url: '/ai-agent/flow/execution/list',
|
||||||
|
method: 'get',
|
||||||
|
requestOptions,
|
||||||
|
}) as Promise<ExecutionListResponse>;
|
||||||
|
}
|
||||||
|
|
||||||
export function getNodeLibraryList(requestOptions?: RequestOptions) {
|
export function getNodeLibraryList(requestOptions?: RequestOptions) {
|
||||||
return request({
|
return request({
|
||||||
url: '/ai-agent/node/library/list',
|
url: '/ai-agent/node/library/list',
|
||||||
@@ -260,7 +296,8 @@ export interface ExecuteFlowParams {
|
|||||||
desc?: string;
|
desc?: string;
|
||||||
fileUrl?: string[];
|
fileUrl?: string[];
|
||||||
flowContent?: FlowInfo;
|
flowContent?: FlowInfo;
|
||||||
flowId?: number;
|
flowId?: string;
|
||||||
|
flowName?: string;
|
||||||
nodeInputParams?: FlowNode[];
|
nodeInputParams?: FlowNode[];
|
||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
skillName?: string;
|
skillName?: string;
|
||||||
|
|||||||
@@ -15,19 +15,28 @@
|
|||||||
default-expand-all
|
default-expand-all
|
||||||
:highlight-current="true"
|
:highlight-current="true"
|
||||||
:expand-on-click-node="false"
|
:expand-on-click-node="false"
|
||||||
@node-click="handleNodeClick"
|
|
||||||
>
|
>
|
||||||
<template #default="{ data }">
|
<template #default="{ data }">
|
||||||
<div class="tree-node">
|
<div class="tree-node">
|
||||||
<span class="ellipsis">{{ data.label }}</span>
|
<span class="ellipsis">{{ data.label }}</span>
|
||||||
<el-button
|
<div v-if="data.nodeType === 'html' || data.nodeType === 'image'" class="tree-node-actions">
|
||||||
v-if="data.nodeType === 'html' || data.nodeType === 'image'"
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
link
|
link
|
||||||
class="tree-download"
|
size="small"
|
||||||
@click.stop="downloadNode(data)"
|
@click.stop="previewNode(data)"
|
||||||
>下载</el-button
|
>
|
||||||
>
|
预览
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
link
|
||||||
|
size="small"
|
||||||
|
@click.stop="downloadNode(data)"
|
||||||
|
>
|
||||||
|
下载
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-tree>
|
</el-tree>
|
||||||
@@ -249,7 +258,7 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 其他配置字段(使用 fieldMetadata 中的中文名称) -->
|
<!-- 其他配置字段(排除已在 formConfig 中的字段) -->
|
||||||
<template v-if="node.config">
|
<template v-if="node.config">
|
||||||
<el-form-item
|
<el-form-item
|
||||||
v-for="(value, key) in node.config"
|
v-for="(value, key) in node.config"
|
||||||
@@ -257,7 +266,7 @@
|
|||||||
v-show="
|
v-show="
|
||||||
!['nodeCode', 'width', 'height', 'x', 'y', 'formConfig', 'inputSource', 'fieldMetadata', 'selectedModel'].includes(
|
!['nodeCode', 'width', 'height', 'x', 'y', 'formConfig', 'inputSource', 'fieldMetadata', 'selectedModel'].includes(
|
||||||
String(key)
|
String(key)
|
||||||
)
|
) && !(node.formConfig || []).some((f: any) => f.label === key || f.label === node.config.fieldMetadata?.[key]?.label)
|
||||||
"
|
"
|
||||||
:label="node.config.fieldMetadata?.[key]?.label || String(key)"
|
:label="node.config.fieldMetadata?.[key]?.label || String(key)"
|
||||||
:required="node.config.fieldMetadata?.[key]?.required || false"
|
:required="node.config.fieldMetadata?.[key]?.required || false"
|
||||||
@@ -482,6 +491,14 @@
|
|||||||
|
|
||||||
<!-- 创作技能选择器 -->
|
<!-- 创作技能选择器 -->
|
||||||
<SkillSelector v-model="showCreationSkillSelector" :default-skill="selectedCreationSkill" @confirm="handleCreationSkillConfirm" />
|
<SkillSelector v-model="showCreationSkillSelector" :default-skill="selectedCreationSkill" @confirm="handleCreationSkillConfirm" />
|
||||||
|
|
||||||
|
<!-- 预览弹窗 -->
|
||||||
|
<el-dialog v-model="previewDialogVisible" title="预览" width="90%" :close-on-click-modal="false" destroy-on-close>
|
||||||
|
<div class="preview-container">
|
||||||
|
<iframe v-if="previewUrl" :src="previewUrl" class="preview-iframe" frameborder="0"></iframe>
|
||||||
|
<el-empty v-else description="无法加载预览内容" />
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -497,7 +514,7 @@ import SkillSelector from '/@/components/skill/NodeSkillSelector.vue';
|
|||||||
import type { SkillItem } from '/@/api/digitalHuman/skill';
|
import type { SkillItem } from '/@/api/digitalHuman/skill';
|
||||||
import {
|
import {
|
||||||
downloadToFile,
|
downloadToFile,
|
||||||
getCreationList,
|
getExecutionList,
|
||||||
getNodeLibraryList,
|
getNodeLibraryList,
|
||||||
getWorkflowList,
|
getWorkflowList,
|
||||||
getWorkflowDetail,
|
getWorkflowDetail,
|
||||||
@@ -505,13 +522,13 @@ import {
|
|||||||
deleteWorkflow,
|
deleteWorkflow,
|
||||||
saveWorkflow,
|
saveWorkflow,
|
||||||
executeFlow,
|
executeFlow,
|
||||||
type CreationListParams,
|
type ExecutionTreeItem,
|
||||||
type CreationTreeItem,
|
|
||||||
type NodeLibraryFormItem,
|
type NodeLibraryFormItem,
|
||||||
type NodeLibraryGroup,
|
type NodeLibraryGroup,
|
||||||
type WorkflowItem,
|
type WorkflowItem,
|
||||||
type ExecuteFlowParams,
|
type ExecuteFlowParams,
|
||||||
} from '/@/api/digitalHuman/creation';
|
} from '/@/api/digitalHuman/creation';
|
||||||
|
import { uploadFile } from '/@/api/common/upload';
|
||||||
|
|
||||||
type NodeType = 'date' | 'contentType' | 'theme' | 'title' | 'html' | 'image';
|
type NodeType = 'date' | 'contentType' | 'theme' | 'title' | 'html' | 'image';
|
||||||
type Item = Record<string, any>;
|
type Item = Record<string, any>;
|
||||||
@@ -573,6 +590,9 @@ const selectedFiles = ref<File[]>([]);
|
|||||||
const selectedCreationSkill = ref<SkillItem | null>(null);
|
const selectedCreationSkill = ref<SkillItem | null>(null);
|
||||||
const showCreationSkillSelector = ref(false);
|
const showCreationSkillSelector = ref(false);
|
||||||
const isCreating = ref(false);
|
const isCreating = ref(false);
|
||||||
|
// 预览相关状态
|
||||||
|
const previewDialogVisible = ref(false);
|
||||||
|
const previewUrl = ref('');
|
||||||
// 会话ID管理(存储在 sessionStorage 中)
|
// 会话ID管理(存储在 sessionStorage 中)
|
||||||
const getSessionId = () => {
|
const getSessionId = () => {
|
||||||
let sessionId = sessionStorage.getItem('ai_creation_session_id');
|
let sessionId = sessionStorage.getItem('ai_creation_session_id');
|
||||||
@@ -718,7 +738,6 @@ const availableParentParams = computed(() => {
|
|||||||
return params;
|
return params;
|
||||||
});
|
});
|
||||||
const treeProps = { children: 'children', label: 'label' };
|
const treeProps = { children: 'children', label: 'label' };
|
||||||
const queryParams: CreationListParams = { keyword: '', pageNum: 1, pageSize: 10 };
|
|
||||||
const apiBaseUrl = (import.meta.env.VITE_API_URL || '').replace(/\/$/, '');
|
const apiBaseUrl = (import.meta.env.VITE_API_URL || '').replace(/\/$/, '');
|
||||||
const nodeLibraryGroups = ref<NodeLibraryGroup[]>([]);
|
const nodeLibraryGroups = ref<NodeLibraryGroup[]>([]);
|
||||||
const workflowDsl = computed(() => ({
|
const workflowDsl = computed(() => ({
|
||||||
@@ -761,44 +780,36 @@ const buildAssetUrl = (p?: string) =>
|
|||||||
: imgAddressPrefix.value
|
: imgAddressPrefix.value
|
||||||
? joinUrl(joinUrl(apiBaseUrl, imgAddressPrefix.value), p)
|
? joinUrl(joinUrl(apiBaseUrl, imgAddressPrefix.value), p)
|
||||||
: joinUrl(apiBaseUrl, p);
|
: joinUrl(apiBaseUrl, p);
|
||||||
const buildTreeNodes = (tree: CreationTreeItem[]): TreeNode[] =>
|
const buildTreeNodes = (tree: ExecutionTreeItem[]): TreeNode[] =>
|
||||||
tree.map((d, di) => ({
|
tree.map((d, di) => ({
|
||||||
id: `date-${di}`,
|
id: `date-${di}`,
|
||||||
label: d.createdDate,
|
label: d.createDate,
|
||||||
nodeType: 'date',
|
nodeType: 'date',
|
||||||
children: (d.contentTypes || []).map((c, ci) => ({
|
children: (d.flows || []).map((f, fi) => ({
|
||||||
id: `content-${di}-${ci}`,
|
id: `flow-${di}-${fi}`,
|
||||||
label: c.contentType,
|
label: f.flowName || '未命名工作流',
|
||||||
nodeType: 'contentType',
|
nodeType: 'contentType',
|
||||||
children: (c.themes || []).map((t, ti) => ({
|
children: (f.items || []).map((item, ii) => ({
|
||||||
id: `theme-${di}-${ci}-${ti}`,
|
id: `item-${di}-${fi}-${ii}`,
|
||||||
label: t.theme,
|
label: item.label || `作品${ii + 1}`,
|
||||||
nodeType: 'theme',
|
nodeType: 'title',
|
||||||
children: (t.titles || []).map((title, i) => ({
|
children: [
|
||||||
id: `title-${di}-${ci}-${ti}-${i}`,
|
{
|
||||||
label: title.title || `作品${i + 1}`,
|
id: `html-${di}-${fi}-${ii}`,
|
||||||
nodeType: 'title',
|
label: 'HTML',
|
||||||
children: [
|
nodeType: 'html' as const,
|
||||||
...(title.htmlFileUrl
|
fileUrl: item.content,
|
||||||
? [{ id: `html-${di}-${ci}-${ti}-${i}`, label: 'HTML', nodeType: 'html' as const, fileUrl: title.htmlFileUrl }]
|
},
|
||||||
: []),
|
],
|
||||||
...(title.imageUrls || []).map((img, ii) => ({
|
|
||||||
id: `img-${di}-${ci}-${ti}-${i}-${ii}`,
|
|
||||||
label: img.name || `图片 ${ii + 1}`,
|
|
||||||
nodeType: 'image' as const,
|
|
||||||
fileUrl: img.url,
|
|
||||||
})),
|
|
||||||
],
|
|
||||||
})),
|
|
||||||
})),
|
})),
|
||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
const getList = async () => {
|
const getList = async () => {
|
||||||
treeLoading.value = true;
|
treeLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const res = await getCreationList({ ...queryParams, keyword: queryParams.keyword || undefined }, { errorMode: 'page' });
|
const res = await getExecutionList({ errorMode: 'page' });
|
||||||
imgAddressPrefix.value = res.data?.imgAddressPrefix || '';
|
imgAddressPrefix.value = res.data?.imgAddressPrefix || '';
|
||||||
treeNodes.value = buildTreeNodes(res.data?.Tree || []);
|
treeNodes.value = buildTreeNodes(res.data?.tree || []);
|
||||||
} catch {
|
} catch {
|
||||||
treeNodes.value = [];
|
treeNodes.value = [];
|
||||||
imgAddressPrefix.value = '';
|
imgAddressPrefix.value = '';
|
||||||
@@ -1048,7 +1059,25 @@ const sendMessage = async () => {
|
|||||||
isCreating.value = true;
|
isCreating.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. 构建节点输入参数
|
// 1. 先上传文件到 OSS,获取文件 URL
|
||||||
|
const fileUrls: string[] = [];
|
||||||
|
if (selectedFiles.value.length > 0) {
|
||||||
|
for (const file of selectedFiles.value) {
|
||||||
|
try {
|
||||||
|
const uploadRes = await uploadFile(file, { errorMode: 'page' });
|
||||||
|
// 拼接完整的文件地址
|
||||||
|
const fullUrl = uploadRes.data.fileAddressPrefix
|
||||||
|
? `${uploadRes.data.fileAddressPrefix}${uploadRes.data.fileURL}`
|
||||||
|
: uploadRes.data.fileURL;
|
||||||
|
fileUrls.push(fullUrl);
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(`文件 ${file.name} 上传失败`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 构建节点输入参数
|
||||||
const nodeInputParams = currentWorkflowForCreation.value.nodeInputParams?.map((node: any) => {
|
const nodeInputParams = currentWorkflowForCreation.value.nodeInputParams?.map((node: any) => {
|
||||||
const nodeParam: any = {
|
const nodeParam: any = {
|
||||||
id: node.id,
|
id: node.id,
|
||||||
@@ -1087,44 +1116,26 @@ const sendMessage = async () => {
|
|||||||
return nodeParam;
|
return nodeParam;
|
||||||
}) || [];
|
}) || [];
|
||||||
|
|
||||||
// 2. 同步更新 flowContent.nodes,使其与 nodeInputParams 一致
|
// 3. 同步更新 flowContent.nodes,使其与 nodeInputParams 一致
|
||||||
const updatedFlowContent = {
|
const updatedFlowContent = {
|
||||||
...currentWorkflowForCreation.value.flowContent,
|
...currentWorkflowForCreation.value.flowContent,
|
||||||
nodes: nodeInputParams, // 使用更新后的节点参数
|
nodes: nodeInputParams, // 使用更新后的节点参数
|
||||||
};
|
};
|
||||||
|
|
||||||
// 3. 构建请求参数
|
// 4. 构建请求参数
|
||||||
const params: ExecuteFlowParams = {
|
const params: ExecuteFlowParams = {
|
||||||
flowId: parseInt(currentWorkflowForCreation.value.id),
|
flowId: currentWorkflowForCreation.value.id, // ID 是字符串
|
||||||
flowContent: updatedFlowContent,
|
flowContent: updatedFlowContent,
|
||||||
nodeInputParams: nodeInputParams,
|
nodeInputParams: nodeInputParams,
|
||||||
sessionId: getSessionId(),
|
sessionId: getSessionId(),
|
||||||
desc: userInput.value,
|
desc: userInput.value,
|
||||||
skillName: selectedCreationSkill.value?.name,
|
skillName: selectedCreationSkill.value?.name,
|
||||||
|
flowName: currentWorkflowForCreation.value.flowName || currentWorkflowForCreation.value.flowTemplateName, // 工作流名称
|
||||||
|
fileUrl: fileUrls, // 添加文件 URL 数组
|
||||||
};
|
};
|
||||||
|
|
||||||
// 4. 使用 FormData 传递文件流
|
// 5. 调用执行接口(不再使用 FormData,直接传 JSON)
|
||||||
const formData = new FormData();
|
await executeFlow(params, { errorMode: 'page' });
|
||||||
|
|
||||||
// 添加文件
|
|
||||||
if (selectedFiles.value.length > 0) {
|
|
||||||
selectedFiles.value.forEach((file) => {
|
|
||||||
formData.append('files', file);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加其他参数(转为 JSON 字符串)
|
|
||||||
formData.append('flowId', params.flowId!.toString());
|
|
||||||
formData.append('flowContent', JSON.stringify(params.flowContent));
|
|
||||||
formData.append('nodeInputParams', JSON.stringify(params.nodeInputParams));
|
|
||||||
formData.append('sessionId', params.sessionId!);
|
|
||||||
formData.append('desc', params.desc!);
|
|
||||||
if (params.skillName) {
|
|
||||||
formData.append('skillName', params.skillName);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. 调用执行接口
|
|
||||||
await executeFlow(formData, { errorMode: 'page' });
|
|
||||||
|
|
||||||
ElMessage.success('创作完成!');
|
ElMessage.success('创作完成!');
|
||||||
|
|
||||||
@@ -1151,12 +1162,15 @@ const getFieldClass = (type: string) => {
|
|||||||
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';
|
||||||
};
|
};
|
||||||
const handleNodeClick = (d: TreeNode) => {
|
// 预览节点
|
||||||
|
const previewNode = (d: TreeNode) => {
|
||||||
if (d.nodeType !== 'html' && d.nodeType !== 'image') return;
|
if (d.nodeType !== 'html' && d.nodeType !== 'image') return;
|
||||||
const url = buildAssetUrl(d.fileUrl);
|
const url = buildAssetUrl(d.fileUrl);
|
||||||
if (!url) return ElMessage.warning('当前节点没有可用预览地址');
|
if (!url) return ElMessage.warning('当前节点没有可用预览地址');
|
||||||
window.open(url, '_blank');
|
previewUrl.value = url;
|
||||||
|
previewDialogVisible.value = true;
|
||||||
};
|
};
|
||||||
|
// 下载节点
|
||||||
const downloadNode = async (d: TreeNode) => {
|
const downloadNode = async (d: TreeNode) => {
|
||||||
if (d.nodeType !== 'html' && d.nodeType !== 'image') return;
|
if (d.nodeType !== 'html' && d.nodeType !== 'image') return;
|
||||||
if (!d.fileUrl) return ElMessage.warning('当前节点没有可下载地址');
|
if (!d.fileUrl) return ElMessage.warning('当前节点没有可下载地址');
|
||||||
@@ -3101,4 +3115,27 @@ onBeforeUnmount(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 预览弹窗样式 */
|
||||||
|
.preview-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 70vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.preview-iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 树节点操作按钮样式 */
|
||||||
|
.tree-node-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user