-
-
-
-
-
-
-
-
-
- {{ workflowDetail.flowName || '工作流表单' }}
-{{ workflowDetail.description || '请填写以下参数' }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 引用
- 由「{{ getSourceDisplay(field) }}」自动提供
-
-
-
-
-
-
-
- {{ uploadingFields[getFieldKey(node, field)] ? '上传中...' : '选择文件' }}
-
-
-
-
-
-
- {{ getFileRuleText(field) }}
-
- 已上传 {{ getFieldFileCountText(node, field) }}
-
-
-
-
-
- {{ f.name }}
-
- 删除
-
-
-
-
-
-
-
+
emit('form-submit', msg, payload)"
@load-more="emit('load-more', activeHistoryId)"
/>
@@ -188,11 +52,7 @@
\ No newline at end of file
+
diff --git a/src/views/home/components/SessionResults.vue b/src/views/home/components/SessionResults.vue
index e7cfa50..7d9456a 100644
--- a/src/views/home/components/SessionResults.vue
+++ b/src/views/home/components/SessionResults.vue
@@ -11,11 +11,11 @@
暂无会话结果
-
+
{{ r.type === 'workflow' ? '工作流' : '对话' }}
-
- {{ statusText(r.status) }}
+
+ {{ statusText(r) }}
@@ -73,23 +73,29 @@ const handleScroll = () => {
if (el.scrollHeight - el.scrollTop - el.clientHeight <= 4) emit('load-more');
};
-// 状态键:1-运行中, 2-成功, 3-失败
-const statusKey = (status: number): string => {
- if (status === 2) return 'success';
- if (status === 3) return 'failed';
+// 状态键:以 errorMsg 为准(部分失败记录 status 仍是 1,仅凭 status 会误判为运行中);
+// 无错误时按 status:2-成功, 其余运行中
+const statusKey = (r: VOSessionInfoResult): string => {
+ if (r.errorMsg) return 'failed';
+ if (r.status === 2) return 'success';
return 'running';
};
-const statusText = (status: number): string => {
- if (status === 2) return '成功';
- if (status === 3) return '失败';
+const statusText = (r: VOSessionInfoResult): string => {
+ if (r.errorMsg) return '失败';
+ if (r.status === 2) return '成功';
return '运行中';
};
-// 卡片问题/参数摘要:chat 取 requestParams.question,其余输出参数 JSON 摘要
+// 卡片问题/参数摘要:chat 取 requestParams.question;workflow 不再平铺 flowContent JSON,
+// 改为精简节点摘要(完整表单与结果已在消息流中,此处只需让结果卡片保持简洁)
const getQuestionText = (r: VOSessionInfoResult): string => {
const q = r.requestParams?.question;
if (q) return String(q);
+ if (r.type === 'workflow') {
+ const nodes = Array.isArray(r.requestParams?.nodes) ? r.requestParams.nodes : [];
+ return nodes.length > 0 ? `包含 ${nodes.length} 个节点` : '';
+ }
const params = r.requestParams;
if (params && Object.keys(params).length > 0) {
try {
diff --git a/src/views/home/components/WorkflowFormCard.vue b/src/views/home/components/WorkflowFormCard.vue
new file mode 100644
index 0000000..de66607
--- /dev/null
+++ b/src/views/home/components/WorkflowFormCard.vue
@@ -0,0 +1,561 @@
+
+
@@ -94,7 +93,6 @@ import type { ExecutionTreeItem } from '/@/api/settings/creation';
import {
getExecutionList,
getWorkflowDetail,
- getExecutionDetail,
deleteExecutionResult,
downloadToFile,
} from '/@/api/settings/creation';
@@ -121,6 +119,11 @@ interface ChatMessage {
// 后端记录 id / 类型(来自 session/get 的每条结果),用于删除对话记录
recordId?: string;
recordType?: string;
+ // 工作流表单卡片消息(type==='form' 时渲染 WorkflowFormCard)
+ type?: 'form';
+ form?: any;
+ formStatus?: 'editing' | 'running' | 'done' | 'failed';
+ formError?: string;
}
interface TreeNode {
@@ -358,7 +361,6 @@ const handleSessionModelSaved = async (model: { id: string; modelName: string })
};
const selectedWorkflowDetail = ref(null);
-const mainContentRef = ref(null);
const sendingSessions = reactive>({});
// ===== 会话级长连接(单活跃连接)=====
// 连接状态/helper 见 formatTime/addMessage 之后统一定义;这里声明本轮 handler 与当前连接路由
@@ -373,6 +375,8 @@ interface RoundHandler {
let activeHandler: RoundHandler | null = null;
// 当前活跃的会话级连接(单活跃:切会话时关旧开新)
let wsState: { ws: WebSocket; sid: string; sessionId: string; ready: Promise } | null = null;
+// 当前进行中的一轮类型:true=工作流执行,false=普通对话(sendCancel 协议判断用)
+let activeRunIsWorkflow = false;
const isHistoryWorkflow = ref(false);
// 生成中:有活跃会话且该会话正处于发送状态(发送按钮切换为停止按钮)
@@ -436,6 +440,8 @@ const getSessionId = () => {
const handleWorkflowSelect = async (workflowId: string | null, isTemplate?: boolean) => {
if (workflowId === null) {
+ // 取消选择:移除最新未提交(editing)的表单卡片草稿,已提交/定格的保留
+ removeDraftFormCard();
selectedWorkflowDetail.value = null;
return;
}
@@ -453,11 +459,60 @@ const handleWorkflowSelect = async (workflowId: string | null, isTemplate?: bool
try {
const res = await getWorkflowDetail(workflowId);
selectedWorkflowDetail.value = res.data || null;
+ // 对话卡片化:选中工作流即推送一张可填表单卡片进入消息流
+ if (res.data) pushFormCard(res.data);
} catch {
selectedWorkflowDetail.value = null;
}
};
+// 无活跃会话时自动创建虚拟会话(选中工作流 / 发送消息共用),返回当前 sid
+const ensureActiveSession = (): { sid: string } => {
+ if (!activeHistoryId.value) {
+ const newId = `virtual_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
+ historyList.value.unshift({
+ id: newId,
+ sessionId: newId,
+ title: '新会话 ' + (historyList.value.length + 1),
+ time: '刚刚',
+ });
+ activeHistoryId.value = newId;
+ sessionMessages.value.set(newId, []);
+ }
+ return { sid: activeHistoryId.value };
+};
+
+// 选中工作流:向当前会话消息流推送一张可填表单卡片(对话卡片化)
+const pushFormCard = (detail: any) => {
+ const { sid } = ensureActiveSession();
+ const msg: ChatMessage = {
+ id: 'form-' + Date.now() + '-' + Math.random().toString(36).slice(2, 6),
+ content: '',
+ time: formatTime(new Date()),
+ isUser: false,
+ type: 'form',
+ form: detail,
+ formStatus: 'editing',
+ };
+ const list = sessionMessages.value.get(sid);
+ if (list) list.push(msg);
+ else sessionMessages.value.set(sid, [msg]);
+};
+
+// 取消工作流选择:移除当前会话中最新一条未提交(editing)的表单卡片草稿,已提交的保留
+const removeDraftFormCard = () => {
+ const id = activeHistoryId.value;
+ if (!id) return;
+ const list = sessionMessages.value.get(id);
+ if (!list) return;
+ for (let i = list.length - 1; i >= 0; i--) {
+ if (list[i].type === 'form' && list[i].formStatus === 'editing') {
+ list.splice(i, 1);
+ return;
+ }
+ }
+};
+
// 模板补全保存完成:刷新列表并自动选中新保存的用户工作流
const handleTemplateSaved = async (newId: string) => {
templateDialogVisible.value = false;
@@ -570,7 +625,7 @@ const teardownActiveRound = () => {
const st = wsState;
if (st && st.ws.readyState === WebSocket.OPEN) {
try {
- sendCancel(st.ws, !!selectedWorkflowDetail.value);
+ sendCancel(st.ws, activeRunIsWorkflow);
} catch {
/* 连接异常时忽略,仅做本地定格 */
}
@@ -587,31 +642,9 @@ const teardownSession = () => {
const handleSend = async (message: string) => {
// 无活跃会话时自动创建
- if (!activeHistoryId.value) {
- const newId = `virtual_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
- historyList.value.unshift({
- id: newId,
- sessionId: newId,
- title: '新会话 ' + (historyList.value.length + 1),
- time: '刚刚',
- });
- activeHistoryId.value = newId;
- sessionMessages.value.set(newId, []);
- }
- const sid = activeHistoryId.value;
+ const { sid } = ensureActiveSession();
if (sendingSessions[sid]) return;
sendingSessions[sid] = true;
- const mc = mainContentRef.value;
- if (!mc) {
- delete sendingSessions[sid];
- return;
- }
-
- // 工作流模式:发送前校验必填表单字段;普通对话无需表单
- if (selectedWorkflowDetail.value && !mc.validateFormFields()) {
- delete sendingSessions[sid];
- return;
- }
// 获取当前会话的 sessionId
const curSession = historyList.value.find((h) => h.id === sid);
@@ -623,7 +656,7 @@ const handleSend = async (message: string) => {
// 添加用户消息
addMessage({
id: 'msg-' + Date.now() + '-user',
- content: message || (selectedWorkflowDetail.value ? '执行工作流' : ''),
+ content: message,
time: formatTime(new Date()),
isUser: true,
});
@@ -632,16 +665,14 @@ const handleSend = async (message: string) => {
const sessionId = curSession.sessionId || getSessionId();
- // 分支:有工作流 → 执行工作流;无工作流 → 普通对话
- if (selectedWorkflowDetail.value) {
- await runWorkflow(sid, sessionId, mc);
- } else {
- await runChat(sid, sessionId, message);
- }
+ // 工作流执行由表单卡片提交触发(handleFormSubmit);输入框发送恒走普通对话
+ await runChat(sid, sessionId, message);
};
// ===== 失败重试:重新发送同一条用户消息 =====
const handleRetry = async (msg: ChatMessage) => {
+ // 工作流结果消息不提供文本重试(重试需重新填写表单卡片提交)
+ if (msg.recordType === 'workflow') return;
let sid = '';
for (const [k, list] of sessionMessages.value) {
if (list.some((m) => m.id === msg.id)) {
@@ -719,7 +750,18 @@ const handleDeleteMessage = async (msg: ChatMessage) => {
ElMessage.success('已删除');
// 删除成功后重新拉取会话结果,消息流与结果卡片同步
const results = await loadSessionResults(sid);
- loadSessionMessagesFromResults(sid, results);
+ if (Array.isArray(results) && results.some((r) => r.type === 'workflow')) {
+ // 仍含工作流结果:完整重建消息流(带值表单卡片 + 结果消息)
+ const session = historyList.value.find((h) => h.id === sid);
+ if (session) {
+ isHistoryWorkflow.value = true;
+ await loadWorkflowSessionMessages(sid, results);
+ }
+ } else {
+ isHistoryWorkflow.value = false;
+ inputBarRef.value?.clearWorkflow?.();
+ loadSessionMessagesFromResults(sid, results);
+ }
} catch {
// 后端已提示错误,保持原状
}
@@ -728,7 +770,7 @@ const handleDeleteMessage = async (msg: ChatMessage) => {
// ===== 停止生成:按执行类型发送对应取消消息并关闭连接,保留已生成内容 =====
// 普通对话走 agent 协议(启动为 {type:'agent'},取消为 {type:'agent_cancel'});
// 工作流走 workflow 协议(取消为 {type:'workflow_cancel'})。
-// 判断依据与 handleSend 的分支一致:selectedWorkflowDetail 有值 → 工作流,无值 → 普通对话。
+// 判断依据:activeRunIsWorkflow 标记当前轮类型(runWorkflow/runChat 各自设置)。
const handleStopGenerate = () => {
const sid = activeHistoryId.value;
if (!sid || !sendingSessions[sid]) return;
@@ -738,7 +780,13 @@ const handleStopGenerate = () => {
};
// ===== 工作流执行:选中工作流 → 表单页 WS 执行 → 完成后切回对话页 =====
-const runWorkflow = async (sid: string, sessionId: string, mc: any) => {
+const runWorkflow = async (
+ sid: string,
+ sessionId: string,
+ detail: any,
+ opts: { formValues: Record; formFileNames: Record; templates?: any[] },
+ formMsg: ChatMessage
+) => {
const curSession = historyList.value.find((h) => h.id === sid);
if (!curSession) {
delete sendingSessions[sid];
@@ -749,6 +797,9 @@ const runWorkflow = async (sid: string, sessionId: string, mc: any) => {
const finishExec = (success: boolean, errorMsg?: string) => {
if (finished) return;
finished = true;
+ // 定格表单卡片:done/failed + 失败信息
+ formMsg.formStatus = success ? 'done' : 'failed';
+ formMsg.formError = success ? undefined : errorMsg || '执行失败';
curSession.status = success ? 'completed' : 'failed';
addMessage({
id: 'msg-' + Date.now() + (success ? '-done' : '-fail'),
@@ -804,9 +855,9 @@ const runWorkflow = async (sid: string, sessionId: string, mc: any) => {
};
try {
- // 1. 构建节点输入参数:深拷贝 DSL,把首页表单值写回开始节点(唯一表单源)运行字段
- const nodeInputParams = JSON.parse(JSON.stringify(selectedWorkflowDetail.value.nodeInputParams || []));
- applyHomeFormValues(nodeInputParams, mc.formValues, mc.formFileNames);
+ // 1. 构建节点输入参数:深拷贝 DSL,把表单卡片值写回开始节点(唯一表单源)运行字段
+ const nodeInputParams = JSON.parse(JSON.stringify(detail.nodeInputParams || []));
+ applyHomeFormValues(nodeInputParams, opts.formValues, opts.formFileNames);
// 开始节点为唯一表单源:执行时 model 节点不带参数结构、form 节点不带自定义字段
// (值已汇总进开始节点 outputConfig 一并提交,避免重复/冗余参数)
nodeInputParams.forEach((n: any) => {
@@ -821,7 +872,7 @@ const runWorkflow = async (sid: string, sessionId: string, mc: any) => {
// 2. 构建 flowContent
const updatedFlowContent = {
- ...selectedWorkflowDetail.value.flowContent,
+ ...detail.flowContent,
nodes: nodeInputParams,
};
@@ -857,6 +908,9 @@ const runWorkflow = async (sid: string, sessionId: string, mc: any) => {
abort: () => {
if (finished) return;
finished = true;
+ // 停止/切会话:表单卡片定格为已停止(只读),不再追加汇总消息
+ formMsg.formStatus = 'failed';
+ formMsg.formError = '执行已停止';
curSession.status = 'completed';
markStopped(sid);
delete sendingSessions[sid];
@@ -870,8 +924,9 @@ const runWorkflow = async (sid: string, sessionId: string, mc: any) => {
return;
}
if (activeHandler !== handler) return; // 等待期被终止/切走 → 不再发送启动帧
+ activeRunIsWorkflow = true;
sendWorkflowStart(ws, {
- flowId: selectedWorkflowDetail.value.id,
+ flowId: detail.id,
flowContent: updatedFlowContent,
});
} catch (e: any) {
@@ -879,8 +934,43 @@ const runWorkflow = async (sid: string, sessionId: string, mc: any) => {
}
};
+// ===== 表单卡片提交:工作流执行唯一入口(卡片内按钮触发) =====
+const handleFormSubmit = async (
+ msg: ChatMessage,
+ payload: { formValues: Record; formFileNames: Record; templates?: any[] }
+) => {
+ // 定位卡片所在会话
+ let sid = '';
+ for (const [k, list] of sessionMessages.value) {
+ if (list.some((m) => m.id === msg.id)) {
+ sid = k;
+ break;
+ }
+ }
+ if (!sid || sendingSessions[sid]) return;
+ const session = historyList.value.find((h) => h.id === sid);
+ if (!session || !msg.form) return;
+
+ // 卡片定格执行中,禁用重复提交
+ msg.formStatus = 'running';
+ msg.formError = undefined;
+
+ // 追加用户消息「执行工作流」
+ addMessage({
+ id: 'msg-' + Date.now() + '-wfuser',
+ content: '执行工作流',
+ time: formatTime(new Date()),
+ isUser: true,
+ });
+
+ session.status = 'executing';
+ const sessionId = session.sessionId || getSessionId();
+ await runWorkflow(sid, sessionId, msg.form, payload, msg);
+};
+
// ===== 普通对话:未选工作流 → 纯问答,AI 回复进消息流 =====
const runChat = async (sid: string, sessionId: string, message: string) => {
+ activeRunIsWorkflow = false;
// AI loading 占位气泡
const aiMsgId = 'msg-' + Date.now() + '-ai';
addMessage({ id: aiMsgId, content: '', time: formatTime(new Date()), isUser: false, loading: true });
@@ -1133,33 +1223,18 @@ const handleSelectHistory = async (id: string) => {
teardownSession();
activeHistoryId.value = id;
activeMenu.value = 'chat';
+ // 切换会话不继承工作流选择(回显的锁定标签由 InputBar 单独维护)
+ selectedWorkflowDetail.value = null;
const session = historyList.value.find((h) => h.id === id);
// 加载会话内结果列表(session/get),用它判断会话类型:普通会话(无 workflow 结果)只依赖这一个接口,不再调 execution/get
const results = await loadSessionResults(id);
const hasWorkflow = Array.isArray(results) && results.some((r) => r.type === 'workflow');
- let asForm = false;
- if (session && hasWorkflow) {
- // 工作流会话:查 execution/get 回显表单(此处逻辑暂保留,后续再优化为用 session/get 数据)
- try {
- const res = await getExecutionDetail(session.id);
- if (res.data) {
- selectedWorkflowDetail.value = res.data;
- isHistoryWorkflow.value = true;
- asForm = true;
- // 同步回显 InputBar 的工作流选择
- const ib = inputBarRef.value as any;
- if (ib?.commonWorkflows && res.data.flowName) {
- const match = ib.commonWorkflows.find((w: any) => !w.isTemplate && w.name === res.data.flowName);
- if (match) ib.selectedWorkflowId = match.id;
- }
- }
- } catch {
- // 无执行详情 → 按普通对话会话处理
- }
- }
- if (!asForm) {
+ // 工作流会话:回显为消息流(带值表单卡片 + 结果消息),不再跳整页表单页
+ if (hasWorkflow) {
+ isHistoryWorkflow.value = true; // InputBar 锁定(禁止更换工作流)
+ await loadWorkflowSessionMessages(id, results);
+ } else {
// 普通对话会话 / 虚拟会话:切回对话页,把 session/get 结果转换为正常对话消息流展示
- selectedWorkflowDetail.value = null;
isHistoryWorkflow.value = false;
inputBarRef.value?.clearWorkflow?.();
loadSessionMessagesFromResults(id, results);
@@ -1174,6 +1249,65 @@ const handleSelectHistory = async (id: string) => {
}
};
+// 工作流会话回显:把 session/get 结果重建为消息流(带值表单卡片 + 结果消息),
+// 表单卡片直接用结果记录的 requestParams(完整 flowContent,含 __start__ outputConfig 值快照)构造,
+// 不再依赖 execution/get;chat 结果按原逻辑转消息流
+const loadWorkflowSessionMessages = async (sid: string, results: VOSessionInfoResult[]) => {
+ // 最近一次 workflow 结果记录(results 时间倒序,第一条即最新):它的 requestParams 即回显表单值来源
+ const latestWf = Array.isArray(results) ? results.find((r) => r.type === 'workflow') : undefined;
+ // 同步回显 InputBar 的工作流选择(锁定标签展示):按 flowId 匹配(commonWorkflows 的 id 即工作流 id)
+ let flowName = '';
+ if (latestWf?.flowId) {
+ const ib = inputBarRef.value as any;
+ if (ib?.commonWorkflows) {
+ const match = ib.commonWorkflows.find((w: any) => String(w.id) === String(latestWf.flowId));
+ if (match) {
+ ib.selectedWorkflowId = match.id;
+ flowName = String(match.name || '');
+ }
+ }
+ }
+ const msgs: ChatMessage[] = [];
+ // results 时间倒序(新→旧),转正序(旧→新)重建消息流
+ [...results].reverse().forEach((r) => {
+ if (r.type === 'chat') {
+ const q = r.requestParams?.question;
+ if (q) msgs.push({ id: 'rq-' + r.id, content: String(q), time: r.createdAt || '', isUser: true, recordId: String(r.id), recordType: r.type });
+ if (r.resultContent) msgs.push({ id: 'ra-' + r.id, content: String(r.resultContent), time: r.createdAt || '', isUser: false, recordId: String(r.id), recordType: r.type });
+ } else if (r.type === 'workflow') {
+ // 失败判断:以 errorMsg 为准(部分失败记录 status 仍是 1,仅凭 status 会误判为成功)
+ const failed = !!r.errorMsg;
+ // 表单卡片:仅最近一次 workflow 结果渲染带值表单卡片(requestParams.nodes 即 nodeInputParams 结构)
+ if (r === latestWf && Array.isArray(r.requestParams?.nodes)) {
+ msgs.push({
+ id: 'form-' + r.id,
+ content: '',
+ time: r.createdAt || '',
+ isUser: false,
+ type: 'form',
+ form: {
+ flowName,
+ nodeInputParams: r.requestParams.nodes,
+ flowContent: r.requestParams,
+ },
+ formStatus: failed ? 'failed' : 'done',
+ formError: failed ? r.errorMsg || '执行失败' : '',
+ });
+ }
+ // 结果消息
+ msgs.push({
+ id: failed ? 'wfail-' + r.id : 'wok-' + r.id,
+ content: failed ? '❌ ' + (r.errorMsg || '执行失败,请重试或联系管理员') : '✅ 执行完成,可前往工作空间查看产出',
+ time: r.createdAt || '',
+ isUser: false,
+ recordId: String(r.id),
+ recordType: r.type,
+ });
+ }
+ });
+ sessionMessages.value.set(sid, msgs);
+};
+
// 加载会话内结果(session/get 第一页):workflow+chat 混排,按时间倒序
const loadSessionResults = async (sid: string) => {
if (!sid) return [];
+
+
+
+
+
+
diff --git a/src/views/home/index.vue b/src/views/home/index.vue
index 4292001..4ec7eeb 100644
--- a/src/views/home/index.vue
+++ b/src/views/home/index.vue
@@ -16,9 +16,7 @@
/>
+
+
+
+ {{ detail?.flowName || '工作流表单' }}
+{{ detail?.description || '请填写以下参数' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 引用
+ 由「{{ getSourceDisplay(field) }}」自动提供
+
+
+
+
+
+
+
+ {{ uploadingFields[getFieldKey(node, field)] ? '上传中...' : '选择文件' }}
+
+
+
+
+
+
+ {{ getFileRuleText(field) }}
+
+ 已上传 {{ getFieldFileCountText(node, field) }}
+
+
+
+
+
+ {{ f.name }}
+
+ 删除
+
+
+
+
+
+
+