diff --git a/src/views/home/components/ChatList.vue b/src/views/home/components/ChatList.vue index 33e7734..5193cf6 100644 --- a/src/views/home/components/ChatList.vue +++ b/src/views/home/components/ChatList.vue @@ -77,9 +77,12 @@ - -
- +
diff --git a/src/views/home/index.vue b/src/views/home/index.vue index e61e9f3..8ec5a14 100644 --- a/src/views/home/index.vue +++ b/src/views/home/index.vue @@ -95,7 +95,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, getErrorText, getNodeProgress, getToolCallName, getToolResultText } from './utils/wsMessage'; +import { parseWsMessage, getDelta, getAnswer, getRecordId, getErrorText, getNodeProgress, getToolCallName, getToolResultText } from './utils/wsMessage'; import type { ExecutionTreeItem } from '/@/api/settings/creation'; import { getExecutionList, @@ -785,8 +785,8 @@ const handleRetry = async (msg: ChatMessage) => { const list = sessionMessages.value.get(sid) || []; const idx = list.findIndex((m) => m.id === msg.id); if (idx < 0) return; - // 待重发的用户问题:优先失败消息上记录的原文,其次向上找最近一条用户消息 - let question = msg.retryQuestion || ''; + // 待重发的用户问题:用户消息直接用自身内容;失败消息优先记录的原文,其次向上找最近一条用户消息 + let question = msg.isUser ? msg.content : (msg.retryQuestion || ''); if (!question) { for (let i = idx - 1; i >= 0; i--) { if (list[i].isUser) { @@ -796,8 +796,16 @@ const handleRetry = async (msg: ChatMessage) => { } } if (!question) return; - // 移除失败消息,重新执行 - list.splice(idx, 1); + // 移除待重生成的消息:用户消息连同紧随其后的 AI 回复一并移除(避免旧回复残留错位);AI 回复只移除自身 + if (msg.isUser && idx + 1 < list.length && !list[idx + 1].isUser && list[idx + 1].type !== 'form') { + list.splice(idx, 2); + } else { + list.splice(idx, 1); + } + // 用户消息重新生成:runChat 只追加 AI 气泡、不会补用户消息,这里先把用户问题重新追加进消息流,避免用户气泡消失 + if (msg.isUser) { + list.push({ id: 'msg-' + Date.now() + '-user', content: question, time: formatTime(new Date()), isUser: true }); + } sendingSessions[sid] = true; session.status = 'executing'; const sessionId = resolveSessionIdForConnect(session); @@ -893,6 +901,8 @@ const runWorkflow = async ( } let finished = false; + // 本轮执行记录 id:round_start 首帧填充,绑定到表单卡片与结果消息 + let roundRecordId = ''; const finishExec = (success: boolean, errorMsg?: string, outputs?: WorkflowOutput[]) => { if (finished) return; finished = true; @@ -909,6 +919,9 @@ const runWorkflow = async ( content: success ? (outputs?.length ? '✅ 执行完成' : '✅ 执行完成,本次未生成产出文件') : `❌ ${errorMsg || '执行失败,请重试或联系管理员'}`, time: formatTime(new Date()), isUser: false, + // 本轮记录 id(round_start 首帧填充):与表单卡片共享同一记录,删除该记录即删除整组 + recordId: roundRecordId || undefined, + recordType: roundRecordId ? 'workflow' : undefined, outputs: outputs || [], }; addMessage(resultMsg); @@ -953,7 +966,14 @@ const runWorkflow = async ( // 优先按 ReAct 结构化事件判定完成/失败 const msg = parseWsMessage(raw); if (msg) { - if (msg.type === 'error') { + if (msg.type === 'round_start') { + // 本轮执行记录 id:后端收到启动帧后第一帧返回,绑定到表单卡片与结果消息 + roundRecordId = getRecordId(msg); + if (roundRecordId) { + formMsg.recordId = roundRecordId; + formMsg.recordType = 'workflow'; + } + } else if (msg.type === 'error') { finishExec(false, getErrorText(msg)); } else if (msg.type === 'flow_complete') { // flow_complete 为后端新增的工作流完成信号,事件自带产出文件 URL 列表 → 直接渲染产出卡片 @@ -1236,6 +1256,25 @@ const runChat = async (sid: string, sessionId: string, message: string) => { const aiIdx = list ? list.findIndex((m) => m.id === aiMsgId) : -1; const ai = aiIdx >= 0 && list ? list[aiIdx] : null; + // 本轮问答记录 id:后端收到提问后第一帧返回(round_start),后续流式帧都属于这条记录 + // 实时阶段即可绑定 recordId,使实时气泡支持删除/重生成,无需等 session/get 刷新 + if (msg.type === 'round_start') { + const recordId = getRecordId(msg); + if (recordId) { + // 本轮用户消息:从尾部向上找第一条未绑定记录 id 的用户消息(新发送/重生成的都在末尾) + const l = sessionMessages.value.get(sid); + if (l) { + for (let i = l.length - 1; i >= 0; i--) { + if (l[i].isUser && !l[i].recordId) { + l[i].recordId = recordId; + break; + } + } + } + if (ai && !ai.recordId) ai.recordId = recordId; + } + return; + } // 模型思考(step 开始):思考区为空时给出占位提示(虚化),并记录计时起点 if (msg.type === 'model_call') { if (ai && !ai.thinking && !ai.content) { diff --git a/src/views/home/utils/wsMessage.ts b/src/views/home/utils/wsMessage.ts index f4bcf06..ff58e0f 100644 --- a/src/views/home/utils/wsMessage.ts +++ b/src/views/home/utils/wsMessage.ts @@ -27,6 +27,7 @@ export const WsEventType = { ToolResult: 'tool_result', // 工具返回结果 Answer: 'answer', // 最终回答 AnswerChunk: 'answer_chunk', // 回答内容增量(逐 chunk) + RoundStart: 'round_start', // 本轮问答开始(后端收到提问后第一帧,data.recordId 为本次问答记录 id) ReasoningChunk: 'reasoning_chunk', // 思考内容增量(逐 chunk) NodeStart: 'node_start', // 工作流节点开始(进度推进) NodeComplete: 'node_complete', // 工作流节点完成(进度推进) @@ -61,6 +62,13 @@ export function getAnswer(msg: WsStreamMessage): string { return typeof msg?.data?.answer === 'string' ? msg.data.answer : ''; } +/** round_start → data.recordId 本次问答记录 id(19 位雪花大整数,契约要求字符串形式,兼容数字类型防御性转 string) */ +export function getRecordId(msg: WsStreamMessage): string { + const rid = msg?.data?.recordId; + if (typeof rid === 'string' || typeof rid === 'number') return String(rid); + return ''; +} + /** model_call → { step, maxStep },缺失时 maxStep 为 0 */ export function getStepInfo(msg: WsStreamMessage): { step: number; maxStep: number } | null { const step = msg?.data?.step;