首页工作流执行:表单默认收起可展开、节点执行过程可视化(仿豆包)、失败支持重新执行/重新编辑、错误信息去重
- 提交后表单卡片默认收起(仅显示工作流标题),头部加明确展开/收起按钮 - node_start/node_complete 事件驱动节点步骤流,执行过程区仿豆包竖向展示 - 失败卡片拆分「重新执行」(复用已填值直接重跑)与「重新编辑」两按钮 - 失败消息只显示「❌ 执行失败」,错误详情由卡片 footer 展示,避免双处重复 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,7 @@
|
||||
:form-error="msg.formError"
|
||||
:progress="msg.formProgress"
|
||||
@submit="emit('form-submit', msg, $event)"
|
||||
@retry="emit('form-retry', msg, $event)"
|
||||
@re-edit="emit('form-re-edit', msg)"
|
||||
@cancel="emit('form-cancel', msg)"
|
||||
/>
|
||||
@@ -100,6 +101,7 @@ import { Delete, DocumentCopy, RefreshRight } from '@element-plus/icons-vue';
|
||||
import 'highlight.js/styles/github-dark.css';
|
||||
import { renderMarkdown } from '../utils/markdown';
|
||||
import type { WorkflowOutput } from '../utils/flowDsl';
|
||||
import type { WorkflowNodeProgress } from '../utils/wsMessage';
|
||||
import WorkflowFormCard from './WorkflowFormCard.vue';
|
||||
import WorkflowOutputCard from './WorkflowOutputCard.vue';
|
||||
|
||||
@@ -120,8 +122,8 @@ interface ChatMessage {
|
||||
form?: any;
|
||||
formStatus?: 'editing' | 'running' | 'done' | 'failed';
|
||||
formError?: string;
|
||||
// 工作流执行节点进度(后端 node_start/node_complete 事件推进,运行中显示)
|
||||
formProgress?: { current: number; total: number; nodeName: string };
|
||||
// 工作流执行节点进度(node_start/node_complete 事件驱动的节点步骤列表,渲染执行过程区)
|
||||
formProgress?: WorkflowNodeProgress;
|
||||
// 工作流执行产出文件列表(挂在 workflow 结果消息上,渲染产出卡片)
|
||||
outputs?: WorkflowOutput[];
|
||||
}
|
||||
@@ -131,6 +133,7 @@ interface Emits {
|
||||
(e: 'regenerate', msg: ChatMessage): void;
|
||||
(e: 'delete', msg: ChatMessage): void;
|
||||
(e: 'form-submit', msg: ChatMessage, payload: any): void;
|
||||
(e: 'form-retry', msg: ChatMessage, payload: any): void;
|
||||
(e: 'form-re-edit', msg: ChatMessage): void;
|
||||
(e: 'form-cancel', msg: ChatMessage): void;
|
||||
(e: 'output-preview', msg: ChatMessage, output: WorkflowOutput): void;
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
@regenerate="emit('regenerate', $event)"
|
||||
@delete="emit('delete', $event)"
|
||||
@form-submit="(msg: any, payload: any) => emit('form-submit', msg, payload)"
|
||||
@form-retry="(msg: any, payload: any) => emit('form-retry', msg, payload)"
|
||||
@form-re-edit="(msg: any) => emit('form-re-edit', msg)"
|
||||
@form-cancel="(msg: any) => emit('form-cancel', msg)"
|
||||
@output-preview="(msg: any, output: any) => emit('output-preview', msg, output)"
|
||||
@@ -71,6 +72,7 @@ interface Emits {
|
||||
(e: 'workflow-select', id: string, isTemplate?: boolean): void;
|
||||
(e: 'delete', msg: any): void;
|
||||
(e: 'form-submit', msg: any, payload: any): void;
|
||||
(e: 'form-retry', msg: any, payload: any): void;
|
||||
(e: 'form-re-edit', msg: any): void;
|
||||
(e: 'form-cancel', msg: any): void;
|
||||
(e: 'output-preview', msg: any, output: any): void;
|
||||
|
||||
@@ -1,12 +1,37 @@
|
||||
<template>
|
||||
<div class="workflow-form-card" :class="{ readonly, submitting, failed: isFailed }">
|
||||
<div class="workflow-form-header">
|
||||
<div>
|
||||
<div class="workflow-form-header" @click="toggleCollapse">
|
||||
<div class="header-left">
|
||||
<h3>{{ detail?.flowName || '工作流表单' }}</h3>
|
||||
<div class="workflow-form-sub">{{ detail?.description || '请填写以下参数' }}</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<el-tag v-if="submitting" size="small" type="primary">执行中</el-tag>
|
||||
<el-tag v-else-if="isFailed" size="small" type="danger">执行失败</el-tag>
|
||||
<el-tag v-else-if="readonly" size="small" type="success">执行完成</el-tag>
|
||||
<!-- 折叠切换:明确按钮提示用户可展开/收起已填参数(点击标题行同样可切换) -->
|
||||
<button type="button" class="collapse-toggle" @click.stop="toggleCollapse">
|
||||
<el-icon class="collapse-arrow" :class="{ collapsed }"><ArrowDown /></el-icon>
|
||||
<span>{{ collapsed ? '展开' : '收起' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="workflow-form-scroll">
|
||||
|
||||
<!-- 节点执行过程区(仿豆包步骤流):有节点事件才显示,始终可见、不受参数收起影响 -->
|
||||
<div v-if="progress?.nodes?.length" class="node-progress-list">
|
||||
<div v-for="node in progress.nodes" :key="node.nodeId" class="node-step" :class="'is-' + node.status">
|
||||
<span class="node-step-icon">
|
||||
<span v-if="node.status === 'running'" class="node-spinner" />
|
||||
<el-icon v-else-if="node.status === 'done'" class="node-icon-done"><CircleCheckFilled /></el-icon>
|
||||
<el-icon v-else-if="node.status === 'failed'" class="node-icon-failed"><CircleCloseFilled /></el-icon>
|
||||
<span v-else class="node-dot" />
|
||||
</span>
|
||||
<span class="node-step-name">{{ node.nodeName }}</span>
|
||||
<span class="node-step-status">{{ nodeStatusText(node) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="!collapsed" class="workflow-form-scroll">
|
||||
<el-form label-position="top" class="workflow-form">
|
||||
<template v-if="detail?.nodeInputParams">
|
||||
<template v-for="node in detail.nodeInputParams" :key="node.id || node.nodeCode">
|
||||
@@ -153,10 +178,13 @@
|
||||
<el-tag :type="isFailed ? 'danger' : 'success'" size="small" effect="light">
|
||||
{{ isFailed ? '执行失败' : '执行完成' }}
|
||||
</el-tag>
|
||||
<!-- 失败卡片可「重新编辑并执行」:保留已填值切回可编辑,改完重新提交 -->
|
||||
<el-button v-if="isFailed" size="small" @click="emit('re-edit')">
|
||||
<el-icon><RefreshRight /></el-icon>重新编辑并执行
|
||||
</el-button>
|
||||
<!-- 失败卡片:可直接「重新执行」(复用当前已填值重跑),或「重新编辑」修改参数后再提交 -->
|
||||
<template v-if="isFailed">
|
||||
<el-button size="small" type="primary" @click="handleRetry">
|
||||
<el-icon><RefreshRight /></el-icon>重新执行
|
||||
</el-button>
|
||||
<el-button size="small" @click="emit('re-edit')">重新编辑</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -165,10 +193,11 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { RefreshRight } from '@element-plus/icons-vue';
|
||||
import { ArrowDown, CircleCheckFilled, CircleCloseFilled, RefreshRight } from '@element-plus/icons-vue';
|
||||
import PatchTemplateEditor from '/@/components/patchTemplate/PatchTemplateEditor.vue';
|
||||
import { uploadFile } from '/@/api/common/upload';
|
||||
import { collectHomeFormFields } from '../utils/flowDsl';
|
||||
import type { WorkflowNodeStep } from '../utils/wsMessage';
|
||||
|
||||
interface Props {
|
||||
// 工作流详情(含 nodeInputParams / flowContent)
|
||||
@@ -179,14 +208,16 @@ interface Props {
|
||||
submitting?: boolean;
|
||||
// 执行失败信息(有值即 failed 态)
|
||||
formError?: string;
|
||||
// 执行进度(后端 node_start/node_complete 事件推进):当前节点序 / 总节点数 / 节点名
|
||||
progress?: { current: number; total: number; nodeName: string };
|
||||
// 执行节点进度(node_start/node_complete 事件驱动的步骤列表,渲染执行过程区)
|
||||
progress?: { nodes: WorkflowNodeStep[] };
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'submit', payload: { formValues: Record<string, any>; formFileNames: Record<string, string | string[]>; templates: any[] }): void;
|
||||
// 失败卡片「重新编辑并执行」:由父组件把 formStatus 切回 editing,保留已填值
|
||||
(e: 're-edit'): void;
|
||||
// 失败卡片「重新执行」:复用当前已填值直接重跑(不进入编辑态)
|
||||
(e: 'retry', payload: { formValues: Record<string, any>; formFileNames: Record<string, string | string[]>; templates: any[] }): void;
|
||||
// 执行中取消:工作流取消入口(InputBar 停止按钮对工作流禁用)
|
||||
(e: 'cancel'): void;
|
||||
}
|
||||
@@ -195,7 +226,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
readonly: false,
|
||||
submitting: false,
|
||||
formError: '',
|
||||
progress: () => ({ current: 0, total: 0, nodeName: '' }),
|
||||
progress: () => ({ nodes: [] }),
|
||||
});
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
@@ -210,15 +241,52 @@ const templates = ref<any[]>([]);
|
||||
const isFailed = computed(() => !!props.formError);
|
||||
const isDisabled = computed(() => props.readonly || props.submitting);
|
||||
|
||||
// 执行进度文本:有节点进度(current/total>0)显示「正在执行 N/M:节点名」,否则兜底「执行中...」
|
||||
const progressText = computed(() => {
|
||||
const p = props.progress;
|
||||
if (p && p.total > 0 && p.current > 0) {
|
||||
const name = p.nodeName ? `:${p.nodeName}` : '';
|
||||
return `正在执行 ${p.current}/${p.total}${name}...`;
|
||||
}
|
||||
return '执行中...';
|
||||
// 表单收起/展开:editing(待提交/回显)默认展开;提交(running)自动收起;
|
||||
// done/failed 保持提交时状态(默认收起,用户可手动展开查看已填值)
|
||||
const collapsed = ref(false);
|
||||
const formStatus = computed(() => {
|
||||
if (props.submitting) return 'running';
|
||||
if (props.formError) return 'failed';
|
||||
if (props.readonly) return 'done';
|
||||
return 'editing';
|
||||
});
|
||||
watch(
|
||||
formStatus,
|
||||
(st, prev) => {
|
||||
if (st === 'editing') {
|
||||
collapsed.value = false; // 回显/重新编辑:默认展开
|
||||
} else if (prev === undefined) {
|
||||
collapsed.value = true; // 初始即非编辑态(执行中/完成/失败)→ 收起
|
||||
} else if (st === 'running') {
|
||||
collapsed.value = true; // 提交瞬间自动收起
|
||||
}
|
||||
// done/failed 到达时保持当前(提交时已收起,用户手动展开则保留)
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
const toggleCollapse = () => {
|
||||
collapsed.value = !collapsed.value;
|
||||
};
|
||||
|
||||
// 执行进度文本:有进行中节点显示「正在执行:节点名」,否则兜底「执行中...」
|
||||
const progressText = computed(() => {
|
||||
const running = (props.progress?.nodes || []).find((n) => n.status === 'running');
|
||||
return running ? `正在执行:${running.nodeName}` : '执行中...';
|
||||
});
|
||||
|
||||
// 节点步骤状态文案(执行过程区)
|
||||
const nodeStatusText = (node: WorkflowNodeStep): string => {
|
||||
switch (node.status) {
|
||||
case 'running':
|
||||
return '执行中';
|
||||
case 'done':
|
||||
return '完成';
|
||||
case 'failed':
|
||||
return '失败';
|
||||
default:
|
||||
return '待执行';
|
||||
}
|
||||
};
|
||||
|
||||
const getFieldKey = (node: any, field: any): string => {
|
||||
const id = node.id || node.nodeCode;
|
||||
@@ -481,6 +549,16 @@ const handleSubmit = () => {
|
||||
templates: JSON.parse(JSON.stringify(templates.value || [])),
|
||||
});
|
||||
};
|
||||
|
||||
// 失败后直接重跑:复用当前已填值(必填此前已校验通过),不进入编辑态原样重新提交
|
||||
const handleRetry = () => {
|
||||
if (!isFailed.value || props.submitting) return;
|
||||
emit('retry', {
|
||||
formValues: JSON.parse(JSON.stringify(formValues)),
|
||||
formFileNames: JSON.parse(JSON.stringify(formFileNames)),
|
||||
templates: JSON.parse(JSON.stringify(templates.value || [])),
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -512,9 +590,106 @@ const handleSubmit = () => {
|
||||
padding: 12px 20px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: background 0.15s;
|
||||
&:hover { background: #f8fafc; }
|
||||
h3 { margin: 0; font-size: 14px; font-weight: 600; color: #0f172a; }
|
||||
.workflow-form-sub { font-size: 12px; color: #94a3b8; margin-top: 2px; }
|
||||
}
|
||||
.header-left { min-width: 0; }
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
/* 折叠切换按钮:文字 + 箭头,明确提示可展开/收起参数区 */
|
||||
.collapse-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
padding: 3px 9px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
&:hover {
|
||||
border-color: #93c5fd;
|
||||
color: #2563eb;
|
||||
background: #eff6ff;
|
||||
}
|
||||
.collapse-arrow {
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
transition: transform 0.2s;
|
||||
&.collapsed { transform: rotate(-180deg); }
|
||||
}
|
||||
}
|
||||
|
||||
/* 节点执行过程区(豆包式步骤流) */
|
||||
.node-progress-list {
|
||||
background: #f8fafc;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
padding: 10px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.node-step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 13px;
|
||||
|
||||
.node-step-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.node-step-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: #1e293b;
|
||||
}
|
||||
.node-step-status {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
&.is-running .node-step-status { color: #2563eb; }
|
||||
&.is-done .node-step-status { color: #22c55e; }
|
||||
&.is-failed .node-step-status { color: #ef4444; }
|
||||
}
|
||||
.node-spinner {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 2px solid #dbeafe;
|
||||
border-top-color: #2563eb;
|
||||
border-radius: 50%;
|
||||
animation: node-spin 0.8s linear infinite;
|
||||
}
|
||||
.node-icon-done { color: #22c55e; font-size: 15px; }
|
||||
.node-icon-failed { color: #ef4444; font-size: 15px; }
|
||||
.node-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #cbd5e1;
|
||||
}
|
||||
@keyframes node-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.workflow-form-scroll {
|
||||
max-height: 55vh;
|
||||
|
||||
+58
-12
@@ -32,6 +32,7 @@
|
||||
@regenerate="handleRetry"
|
||||
@delete="handleDeleteMessage"
|
||||
@form-submit="handleFormSubmit"
|
||||
@form-retry="handleFormRetry"
|
||||
@form-re-edit="handleFormReEdit"
|
||||
@form-cancel="handleFormCancel"
|
||||
@output-preview="handleOutputPreview"
|
||||
@@ -100,7 +101,7 @@ import { applyHomeFormValues, buildOutputsFromUrls } from './utils/flowDsl';
|
||||
import type { WorkflowOutput } from './utils/flowDsl';
|
||||
import { getChatModel, listModelManage } from '/@/api/settings/modelConfigV2';
|
||||
import { connectSessionSocket, sendAgentStart, sendWorkflowStart, sendCancel } from './utils/wsExecute';
|
||||
import { parseWsMessage, getDelta, getAnswer, getRecordId, getErrorText, getNodeProgress, getToolCallName, getToolResultText } from './utils/wsMessage';
|
||||
import { parseWsMessage, getDelta, getAnswer, getRecordId, getErrorText, getNodeEvent, getToolCallName, getToolResultText, type WorkflowNodeProgress } from './utils/wsMessage';
|
||||
import {
|
||||
getWorkflowDetail,
|
||||
deleteExecutionResult,
|
||||
@@ -134,8 +135,8 @@ interface ChatMessage {
|
||||
form?: any;
|
||||
formStatus?: 'editing' | 'running' | 'done' | 'failed';
|
||||
formError?: string;
|
||||
// 工作流执行节点进度(后端 node_start/node_complete 事件推进,运行中显示)
|
||||
formProgress?: { current: number; total: number; nodeName: string };
|
||||
// 工作流执行节点进度(node_start/node_complete 事件驱动的节点步骤列表,渲染执行过程区)
|
||||
formProgress?: WorkflowNodeProgress;
|
||||
// 工作流执行产出文件列表(挂在结果消息上,渲染产出卡片)
|
||||
outputs?: WorkflowOutput[];
|
||||
}
|
||||
@@ -961,6 +962,24 @@ const handleStopGenerate = () => {
|
||||
};
|
||||
|
||||
// ===== 工作流执行:选中工作流 → 表单页 WS 执行 → 完成后切回对话页 =====
|
||||
|
||||
// 工作流节点进度推进:node_start → 该节点置「进行中」;node_complete → 置「完成」。
|
||||
// 节点按 nodeIndex 顺序渐进累积(执行过程区随事件实时增长),重复事件以最近一次为准(重跑场景回到进行中)。
|
||||
const updateNodeProgress = (formMsg: ChatMessage, msg: any) => {
|
||||
const ev = getNodeEvent(msg);
|
||||
if (!ev) return;
|
||||
if (!formMsg.formProgress) formMsg.formProgress = { nodes: [] };
|
||||
const list = formMsg.formProgress.nodes;
|
||||
let step = list.find((n) => n.nodeId === ev.nodeId);
|
||||
if (!step) {
|
||||
step = { nodeId: ev.nodeId, nodeName: ev.nodeName, nodeIndex: ev.nodeIndex, status: 'pending' };
|
||||
list.push(step);
|
||||
list.sort((a, b) => a.nodeIndex - b.nodeIndex);
|
||||
}
|
||||
step.nodeName = ev.nodeName || step.nodeName;
|
||||
step.status = ev.phase === 'start' ? 'running' : 'done';
|
||||
};
|
||||
|
||||
const runWorkflow = async (
|
||||
sid: string,
|
||||
sessionId: string,
|
||||
@@ -988,14 +1007,15 @@ const runWorkflow = async (
|
||||
// 定格表单卡片:done/failed + 失败信息;执行结束清进度(避免残留影响重新编辑)
|
||||
formMsg.formStatus = success ? 'done' : 'failed';
|
||||
formMsg.formError = success ? undefined : errorMsg || '执行失败';
|
||||
formMsg.formProgress = undefined;
|
||||
// 保留 formProgress:执行过程区展示最终节点状态(完成全勾 / 失败处红叉)
|
||||
curSession.status = success ? 'completed' : 'failed';
|
||||
// 结果消息:成功时携带本次执行产出(outputs),直接以产出卡片渲染在对话里。
|
||||
// 产出来源为 flow_complete 事件自带的 resultFileUrls(buildOutputsFromUrls);无产出时提示「本次未生成产出文件」
|
||||
const resultMsg: ChatMessage = {
|
||||
id: 'msg-' + Date.now() + (success ? '-done' : '-fail'),
|
||||
// 成功但无产出:明确告知「本次未生成产出文件」,避免用户以为产出丢失(产出来自 flow_complete.resultFileUrls)
|
||||
content: success ? (outputs?.length ? '✅ 执行完成' : '✅ 执行完成,本次未生成产出文件') : `❌ ${errorMsg || '执行失败,请重试或联系管理员'}`,
|
||||
// 失败消息仅展示状态;错误详情由表单卡片 footer 的 form-error-text 展示,避免双处重复
|
||||
content: success ? (outputs?.length ? '✅ 执行完成' : '✅ 执行完成,本次未生成产出文件') : '❌ 执行失败',
|
||||
time: formatTime(new Date()),
|
||||
isUser: false,
|
||||
// 本轮记录 id(round_start 首帧填充):与表单卡片共享同一记录,删除该记录即删除整组
|
||||
@@ -1058,18 +1078,29 @@ const runWorkflow = async (
|
||||
formMsg.recordType = 'workflow';
|
||||
}
|
||||
} else if (msg.type === 'error') {
|
||||
// 失败:执行中的节点标记为失败,执行过程区展示中断位置
|
||||
if (formMsg.formProgress) {
|
||||
formMsg.formProgress.nodes.forEach((n) => {
|
||||
if (n.status === 'running') n.status = 'failed';
|
||||
});
|
||||
}
|
||||
finishExec(false, getErrorText(msg));
|
||||
} else if (msg.type === 'flow_complete') {
|
||||
// flow_complete 为后端新增的工作流完成信号,事件自带产出文件 URL 列表 → 直接渲染产出卡片;
|
||||
// 以 round_start 首帧填充的 roundRecordId 作 backendId,保证实时产出可删除(与回显路径一致)
|
||||
// 全部节点补完状态(个别节点未收到 complete 事件也视为已执行完成)
|
||||
if (formMsg.formProgress) {
|
||||
formMsg.formProgress.nodes.forEach((n) => {
|
||||
if (n.status !== 'done') n.status = 'done';
|
||||
});
|
||||
}
|
||||
finishExec(true, undefined, buildOutputsFromUrls(msg?.data?.resultFileUrls, roundRecordId));
|
||||
} else if (msg.type === 'answer') {
|
||||
// answer 为最终回答完成信号(无产出文件)
|
||||
finishExec(true);
|
||||
} else {
|
||||
// 工作流节点进度推进:node_start / node_complete → 更新表单卡片运行态进度文本
|
||||
const prog = getNodeProgress(msg);
|
||||
if (prog) formMsg.formProgress = prog;
|
||||
// 工作流节点进度推进:node_start / node_complete → 更新表单卡片节点步骤列表
|
||||
updateNodeProgress(formMsg, msg);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1090,10 +1121,15 @@ const runWorkflow = async (
|
||||
abort: () => {
|
||||
if (finished) return;
|
||||
finished = true;
|
||||
// 取消/删除:表单卡片定格为已取消(只读、保留已填值),不再追加汇总消息
|
||||
// 取消/删除:表单卡片定格为已取消(只读、保留已填值),不再追加汇总消息;
|
||||
// 执行中的节点标记为失败,过程区保留展示中断位置
|
||||
formMsg.formStatus = 'failed';
|
||||
formMsg.formError = '执行已取消';
|
||||
formMsg.formProgress = undefined;
|
||||
if (formMsg.formProgress) {
|
||||
formMsg.formProgress.nodes.forEach((n) => {
|
||||
if (n.status === 'running') n.status = 'failed';
|
||||
});
|
||||
}
|
||||
curSession.status = 'completed';
|
||||
delete sendingSessions[sid];
|
||||
},
|
||||
@@ -1119,8 +1155,8 @@ const runWorkflow = async (
|
||||
}
|
||||
};
|
||||
|
||||
// ===== 表单卡片提交:工作流执行唯一入口(卡片内按钮触发) =====
|
||||
const handleFormSubmit = async (
|
||||
// ===== 表单卡片执行入口:提交 / 失败重试共用(卡片内按钮触发) =====
|
||||
const startWorkflowFromCard = async (
|
||||
msg: ChatMessage,
|
||||
payload: { formValues: Record<string, any>; formFileNames: Record<string, string | string[]>; templates?: any[] }
|
||||
) => {
|
||||
@@ -1141,17 +1177,27 @@ const handleFormSubmit = async (
|
||||
// 卡片定格执行中,禁用重复提交;表单卡片 running 态即提交意图,不追加用户消息
|
||||
msg.formStatus = 'running';
|
||||
msg.formError = undefined;
|
||||
// 新执行开始:重置节点进度,从空列表随 node_start 事件渐进累积
|
||||
msg.formProgress = undefined;
|
||||
|
||||
session.status = 'executing';
|
||||
const sessionId = resolveSessionIdForConnect(session);
|
||||
await runWorkflow(sid, sessionId, msg.form, payload, msg);
|
||||
};
|
||||
|
||||
// 表单提交:用户填写后点击「提交执行」
|
||||
const handleFormSubmit = (msg: ChatMessage, payload: any) => startWorkflowFromCard(msg, payload);
|
||||
|
||||
// 失败后「重新执行」:复用当前已填值直接重跑(卡片 handleRetry 收集当前值后触发,不进入编辑态)
|
||||
const handleFormRetry = (msg: ChatMessage, payload: any) => startWorkflowFromCard(msg, payload);
|
||||
|
||||
// 失败卡片「重新编辑并执行」:切回 editing 保留已填值(组件内 formValues 随消息保留,
|
||||
// detail 未变不触发重置),改完重新提交
|
||||
const handleFormReEdit = (msg: ChatMessage) => {
|
||||
msg.formStatus = 'editing';
|
||||
msg.formError = undefined;
|
||||
// 重新编辑:清除上次执行的节点进度,待再次提交后重新累积
|
||||
msg.formProgress = undefined;
|
||||
};
|
||||
|
||||
// 工作流执行中取消(卡片 footer「取消执行」):复用 teardownRound —— 发 cancel 帧 + 本轮 abort,
|
||||
|
||||
@@ -92,6 +92,45 @@ export function getNodeProgress(msg: WsStreamMessage): { current: number; total:
|
||||
};
|
||||
}
|
||||
|
||||
/** 工作流执行节点步骤(node_start/node_complete/error 事件驱动,渲染表单卡片执行过程区) */
|
||||
export interface WorkflowNodeStep {
|
||||
nodeId: string;
|
||||
nodeName: string;
|
||||
nodeIndex: number;
|
||||
status: 'pending' | 'running' | 'done' | 'failed';
|
||||
}
|
||||
|
||||
/** 工作流执行节点进度(挂在表单卡片消息 formProgress 上) */
|
||||
export interface WorkflowNodeProgress {
|
||||
nodes: WorkflowNodeStep[];
|
||||
}
|
||||
|
||||
/** 工作流节点事件解析结果:node_start / node_complete → { nodeId, nodeName, nodeIndex, phase } */
|
||||
export interface WorkflowNodeEvent {
|
||||
nodeId: string;
|
||||
nodeName: string;
|
||||
nodeIndex: number;
|
||||
phase: 'start' | 'complete';
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析工作流节点事件(node_start / node_complete)。
|
||||
* 返回完整节点信息(nodeId/nodeName/nodeIndex/phase),供执行过程区步骤列表驱动;非节点事件返回 null。
|
||||
*/
|
||||
export function getNodeEvent(msg: WsStreamMessage): WorkflowNodeEvent | null {
|
||||
const t = msg?.type;
|
||||
if (t !== WsEventType.NodeStart && t !== WsEventType.NodeComplete) return null;
|
||||
const d = msg?.data;
|
||||
const nodeIndex = d?.nodeIndex;
|
||||
if (typeof nodeIndex !== 'number') return null;
|
||||
return {
|
||||
nodeId: typeof d?.nodeId === 'string' ? d.nodeId : '',
|
||||
nodeName: typeof d?.nodeName === 'string' ? d.nodeName : '',
|
||||
nodeIndex,
|
||||
phase: t === WsEventType.NodeStart ? 'start' : 'complete',
|
||||
};
|
||||
}
|
||||
|
||||
/** tool_call → 工具名称(tool / toolName / name 依次尝试,缺失返回空串) */
|
||||
export function getToolCallName(msg: WsStreamMessage): string {
|
||||
const d = msg?.data;
|
||||
|
||||
Reference in New Issue
Block a user