diff --git a/src/api/settings/workflow/session.ts b/src/api/settings/workflow/session.ts index fc44074..01fb2e9 100644 --- a/src/api/settings/workflow/session.ts +++ b/src/api/settings/workflow/session.ts @@ -24,6 +24,8 @@ export interface VOSessionInfoResult { totalTokens: number; totalFee: number; errorMsg: string; + // 失败详细原因(工作流节点失败详情,如「执行工作流失败: ...节点:生成视频, 失败原因:...」),比 errorMsg 更具体 + error?: string; createdAt: string; } diff --git a/src/views/home/components/WorkflowFormCard.vue b/src/views/home/components/WorkflowFormCard.vue index ea224e3..fe80a65 100644 --- a/src/views/home/components/WorkflowFormCard.vue +++ b/src/views/home/components/WorkflowFormCard.vue @@ -171,19 +171,26 @@ @@ -268,6 +275,15 @@ const loadChargeModes = async () => { const isFailed = computed(() => !!props.formError); const isDisabled = computed(() => (props.readonly && !props.editable) || props.submitting); +// 失败原因默认收起:新失败时重置为不展示,用户点「查看失败原因」才展开 +const showError = ref(false); +watch( + () => props.formError, + () => { + showError.value = false; + } +); + // 表单收起/展开:editing(待提交/回显)默认展开;提交(running)自动收起; // done/failed 保持提交时状态(默认收起,用户可手动展开查看已填值) const collapsed = ref(false); @@ -798,19 +814,41 @@ watch( /* 底部操作区 */ .workflow-form-footer { display: flex; + flex-wrap: wrap; /* 失败原因详情换行到状态行下方 */ align-items: center; gap: 12px; padding: 12px 20px; border-top: 1px solid #f1f5f9; background: #fafbfc; flex-shrink: 0; - .form-error-text { + .form-status-row { + display: flex; + align-items: center; + gap: 8px; flex: 1; + min-width: 0; + } + .form-error-toggle { + padding: 0; + height: auto; + font-size: 12px; + color: #ef4444; + .error-arrow { + transition: transform 0.2s; + &.is-expanded { transform: rotate(180deg); } + } + } + .form-error-text { + flex-basis: 100%; /* 占整行,显示在状态行下方 */ font-size: 12px; color: #ef4444; line-height: 1.5; white-space: pre-wrap; /* 失败详情可能含换行(error 字段多行原因),允许换行展示 */ word-break: break-word; + background: #fef2f2; + border: 1px solid #fee2e2; + border-radius: 6px; + padding: 8px 12px; } .form-edit-tip { flex: 1; diff --git a/src/views/home/index.vue b/src/views/home/index.vue index 79b7743..e57bfe2 100644 --- a/src/views/home/index.vue +++ b/src/views/home/index.vue @@ -105,7 +105,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, getNodeEvent, getToolCallName, getToolResultText, type WorkflowNodeProgress } from './utils/wsMessage'; +import { parseWsMessage, getDelta, getAnswer, getRecordId, getErrorText, getErrorDetail, getNodeEvent, getToolCallName, getToolResultText, type WorkflowNodeProgress } from './utils/wsMessage'; import { getApiErrorMessage } from '/@/utils/request'; import { getWorkflowDetail, @@ -1140,13 +1140,14 @@ const runWorkflow = async ( formMsg.recordType = 'workflow'; } } else if (msg.type === 'error') { - // 失败:执行中的节点标记为失败,执行过程区展示中断位置 + // 失败:执行中的节点标记为失败,执行过程区展示中断位置; + // 失败原因绑定 error 字段(节点失败详情),供卡片「查看失败原因」手动展开 if (formMsg.formProgress) { formMsg.formProgress.nodes.forEach((n) => { if (n.status === 'running') n.status = 'failed'; }); } - finishExec(false, getErrorText(msg)); + finishExec(false, getErrorDetail(msg)); } else if (msg.type === 'flow_complete') { // flow_complete 为后端新增的工作流完成信号,事件自带产出文件 URL 列表 → 直接渲染产出卡片; // 以 round_start 首帧填充的 roundRecordId 作 backendId,保证实时产出可删除(与回显路径一致) @@ -1657,8 +1658,8 @@ const loadWorkflowSessionMessages = async (sid: string, results: VOSessionInfoRe 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; + // 失败判断:error / errorMsg 任一有值即失败(部分失败记录 status 仍是 1,仅凭 status 会误判为成功) + const failed = !!(r.errorMsg || r.error); // 每次执行都渲染带值表单卡片(requestParams.nodes 即该次提交的表单值快照); // form 补 id=flowId,供失败卡片「重新编辑并执行」时作为启动 flowId if (Array.isArray(r.requestParams?.nodes)) { @@ -1675,7 +1676,8 @@ const loadWorkflowSessionMessages = async (sid: string, results: VOSessionInfoRe flowContent: r.requestParams, }, formStatus: failed ? 'failed' : 'done', - formError: failed ? r.errorMsg || '执行失败' : '', + // 失败原因绑定 error 字段(节点失败详情);errorMsg(如「用户已终止执行」)仅作兜底 + formError: failed ? r.error || r.errorMsg || '执行失败' : '', }); } // 结果消息:成功记录直接带产出(session/get 记录自带的 resultFileUrl),不再调 execution/get diff --git a/src/views/home/utils/wsMessage.ts b/src/views/home/utils/wsMessage.ts index 02a3f7d..9c8d807 100644 --- a/src/views/home/utils/wsMessage.ts +++ b/src/views/home/utils/wsMessage.ts @@ -169,3 +169,16 @@ export function getErrorText(msg: WsStreamMessage): string { } return '执行失败,请重试'; } + +/** error 详细原因(error 字段优先展示失败详情;message 仅在 error 缺失时兜底)——供工作流失败卡片「查看失败原因」使用 */ +export function getErrorDetail(msg: WsStreamMessage): string { + if (typeof msg?.error === 'string' && msg.error) return msg.error; + if (typeof msg?.message === 'string' && msg.message) return msg.message; + const d = msg?.data; + if (typeof d === 'string') return d; + if (d && typeof d === 'object') { + const nested = d.message ?? d.error ?? d.msg; + if (typeof nested === 'string' && nested) return nested; + } + return '执行失败,请重试'; +}