diff --git a/src/views/home/components/ChatList.vue b/src/views/home/components/ChatList.vue index c374789..88b6fd6 100644 --- a/src/views/home/components/ChatList.vue +++ b/src/views/home/components/ChatList.vue @@ -9,9 +9,19 @@ v-for="msg in messages" :key="msg.id" class="message-row" - :class="{ 'is-user': msg.isUser, 'is-error': !msg.isUser && isErrorMsg(msg) }" + :class="{ 'is-user': msg.isUser, 'is-form': msg.type === 'form', 'is-error': !msg.isUser && isErrorMsg(msg) }" >
+ + +
{{ msg.time }}
@@ -73,6 +84,7 @@ import { ElMessage } from 'element-plus'; import { Delete, DocumentCopy, RefreshRight } from '@element-plus/icons-vue'; import 'highlight.js/styles/github-dark.css'; import { renderMarkdown } from '../utils/markdown'; +import WorkflowFormCard from './WorkflowFormCard.vue'; interface ChatMessage { id: string; @@ -86,12 +98,18 @@ interface ChatMessage { // 后端记录 id / 类型(来自 session/get 的每条结果),存在时才显示删除按钮 recordId?: string; recordType?: string; + // 工作流表单卡片消息(type==='form' 时渲染 WorkflowFormCard) + type?: 'form'; + form?: any; + formStatus?: 'editing' | 'running' | 'done' | 'failed'; + formError?: string; } interface Emits { (e: 'retry', msg: ChatMessage): void; (e: 'regenerate', msg: ChatMessage): void; (e: 'delete', msg: ChatMessage): void; + (e: 'form-submit', msg: ChatMessage, payload: any): void; (e: 'load-more'): void; } @@ -339,6 +357,16 @@ onMounted(() => { } } +/* 工作流表单卡片消息:左对齐,卡片自带背景不套气泡 */ +.message-row.is-form { + justify-content: flex-start; + .bubble-wrap { + align-items: flex-start; + width: 100%; + .time { padding-left: 2px; } + } +} + /* AI 消息操作栏(hover 显示) */ .msg-actions { display: flex; diff --git a/src/views/home/components/MainContent.vue b/src/views/home/components/MainContent.vue index 7ffc8b1..63326b9 100644 --- a/src/views/home/components/MainContent.vue +++ b/src/views/home/components/MainContent.vue @@ -1,146 +1,9 @@ \ 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) }} {{ r.totalTokens }} tokens ¥{{ r.totalFee }} @@ -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 @@ + + + + + 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 @@ />
@@ -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 [];