新增:首页工作流失败卡片提供「查看失败原因」按钮(默认收起、点击展开,绑定 error 字段);实时与历史回显均展示详细失败原因
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,8 @@ export interface VOSessionInfoResult {
|
||||
totalTokens: number;
|
||||
totalFee: number;
|
||||
errorMsg: string;
|
||||
// 失败详细原因(工作流节点失败详情,如「执行工作流失败: ...节点:生成视频, 失败原因:...」),比 errorMsg 更具体
|
||||
error?: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -171,19 +171,26 @@
|
||||
|
||||
<!-- 底部操作区:editing 提示 / running 执行中 / done·failed 状态标签(执行统一由 InputBar 发送按钮触发) -->
|
||||
<div class="workflow-form-footer">
|
||||
<div v-if="isFailed" class="form-error-text">{{ formError }}</div>
|
||||
<!-- 执行中:节点文本进度推进(后端 node_start/node_complete 事件),无进度时兜底「执行中...」;
|
||||
停止统一由 InputBar 发送按钮(变停止)触发,卡片内不再提供取消入口 -->
|
||||
<div v-if="submitting" class="executing-progress">
|
||||
<span class="exec-spinner" />
|
||||
<span class="exec-text">{{ progressText }}</span>
|
||||
</div>
|
||||
<!-- 已执行卡片(done/failed,含可编辑的最后一张):显示状态标签,参数是否可编辑由 editable 决定 -->
|
||||
<el-tag v-else-if="formStatus === 'done' || formStatus === 'failed'" :type="isFailed ? 'danger' : 'success'" size="small" effect="light">
|
||||
{{ isFailed ? '执行失败' : '执行完成' }}
|
||||
</el-tag>
|
||||
<!-- 已执行卡片(done/failed,含可编辑的最后一张):状态标签 + 失败原因按钮(默认收起,点击手动展开) -->
|
||||
<div v-else-if="formStatus === 'done' || formStatus === 'failed'" class="form-status-row">
|
||||
<el-tag :type="isFailed ? 'danger' : 'success'" size="small" effect="light">
|
||||
{{ isFailed ? '执行失败' : '执行完成' }}
|
||||
</el-tag>
|
||||
<el-button v-if="isFailed && formError" text size="small" class="form-error-toggle" @click="showError = !showError">
|
||||
{{ showError ? '收起失败原因' : '查看失败原因' }}
|
||||
<el-icon class="error-arrow" :class="{ 'is-expanded': showError }"><ArrowDown /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<!-- 编辑态:无执行按钮,提示通过下方发送按钮执行 -->
|
||||
<span v-else class="form-edit-tip">填写参数后,点击下方发送按钮执行工作流</span>
|
||||
<!-- 失败原因详情:默认收起,点「查看失败原因」展开(error 字段可能含多行详细原因) -->
|
||||
<div v-if="isFailed && showError && formError" class="form-error-text">{{ formError }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 '执行失败,请重试';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user