// ===== 首页 WebSocket 流式消息解析工具 ===== // 服务端推送统一为 JSON 文本帧(后端 ReAct 事件协议): // {"type":"model_call","message":"模型思考","data":{"step":1,"maxStep":15}} 模型思考(step 开始) // {"type":"tool_call","message":"调用工具","data":{"tool":"web_search","arguments":{}}} 模型请求调用工具 // {"type":"tool_result","message":"工具返回","data":{"result":"..."}} 工具返回结果 // {"type":"reasoning_chunk","message":"思考中","data":{"delta":"你"}} 思考内容增量(逐 chunk) // {"type":"answer_chunk","message":"回答中","data":{"delta":"你"}} 回答内容增量(逐 chunk) // {"type":"answer","message":"作答完成","data":{"answer":"..."}} 最终回答 // {"type":"node_start","message":"开始执行(2/4): xxx","data":{"nodeCount":4,"nodeId":"..","nodeIndex":2,"nodeName":".."}} 工作流节点开始(进度推进) // {"type":"node_complete","message":"执行完成(2/4): xxx","data":同上} 工作流节点完成(进度推进) // {"type":"error","message":"...","error":"节点失败详情"} 执行失败(error 字段含详细原因) // 完成/失败判定均以 type 为准,不再用正则猜测文本。 export interface WsStreamMessage { type: string; message?: string; data?: any; payload?: any; // 失败详情:error 事件携带的详细原因(含节点名与具体失败信息),比 message 更具体 error?: string; } /** 后端 ReAct WebSocket 事件类型常量 */ export const WsEventType = { ModelCall: 'model_call', // 模型思考(step 开始) ToolCall: 'tool_call', // 模型请求调用工具 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', // 工作流节点完成(进度推进) FlowComplete: 'flow_complete', // 工作流执行完成(后端完成信号,日志末尾事件) Error: 'error', // 出错 } as const; /** * 解析服务端推送帧。 * 非 JSON、无 type 的帧返回 null(调用方忽略)。 */ export function parseWsMessage(raw: any): WsStreamMessage | null { let data: any = raw; if (typeof raw === 'string') { try { data = JSON.parse(raw); } catch { return null; } } if (!data || typeof data !== 'object' || typeof data.type !== 'string') return null; return data; } /** answer_chunk / reasoning_chunk → data.delta 流式增量 */ export function getDelta(msg: WsStreamMessage): string { return typeof msg?.data?.delta === 'string' ? msg.data.delta : ''; } /** answer → data.answer 最终答案 */ 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; if (typeof step !== 'number') return null; const maxStep = msg?.data?.maxStep; return { step, maxStep: typeof maxStep === 'number' ? maxStep : 0 }; } /** 工作流节点进度:node_start / node_complete → { current, total, nodeName },非节点事件返回 null */ export function getNodeProgress(msg: WsStreamMessage): { current: number; total: number; nodeName: string } | null { const t = msg?.type; if (t !== WsEventType.NodeStart && t !== WsEventType.NodeComplete) return null; const d = msg?.data; const nodeIndex = d?.nodeIndex; const nodeCount = d?.nodeCount; if (typeof nodeIndex !== 'number' || typeof nodeCount !== 'number') return null; return { current: nodeIndex, total: nodeCount, nodeName: typeof d?.nodeName === 'string' ? d.nodeName : '', }; } /** 工作流执行节点步骤(node_start/node_complete/error 事件驱动,渲染表单卡片执行过程区) */ export interface WorkflowNodeStep { nodeId: string; nodeName: string; nodeIndex: number; status: 'pending' | 'running' | 'done' | 'failed'; } /** 工作流执行节点进度(挂在表单卡片消息 formProgress 上) */ export interface WorkflowNodeProgress { nodes: WorkflowNodeStep[]; } /** 工作流节点事件解析结果:node_start / node_complete → { nodeId, nodeName, nodeIndex, phase } */ export interface WorkflowNodeEvent { nodeId: string; nodeName: string; nodeIndex: number; phase: 'start' | 'complete'; } /** * 解析工作流节点事件(node_start / node_complete)。 * 返回完整节点信息(nodeId/nodeName/nodeIndex/phase),供执行过程区步骤列表驱动;非节点事件返回 null。 */ export function getNodeEvent(msg: WsStreamMessage): WorkflowNodeEvent | null { const t = msg?.type; if (t !== WsEventType.NodeStart && t !== WsEventType.NodeComplete) return null; const d = msg?.data; const nodeIndex = d?.nodeIndex; if (typeof nodeIndex !== 'number') return null; return { nodeId: typeof d?.nodeId === 'string' ? d.nodeId : '', nodeName: typeof d?.nodeName === 'string' ? d.nodeName : '', nodeIndex, phase: t === WsEventType.NodeStart ? 'start' : 'complete', }; } /** tool_call → 工具名称(tool / toolName / name 依次尝试,缺失返回空串) */ export function getToolCallName(msg: WsStreamMessage): string { const d = msg?.data; if (!d || typeof d !== 'object') return ''; const name = d.tool ?? d.toolName ?? d.name; return typeof name === 'string' ? name : ''; } /** tool_result → 工具返回文本(result / output / content / message 依次尝试,对象则序列化) */ export function getToolResultText(msg: WsStreamMessage): string { const d = msg?.data; if (typeof d === 'string') return d; if (!d || typeof d !== 'object') return ''; const raw = d.result ?? d.output ?? d.content ?? d.message; if (typeof raw === 'string') return raw; if (raw != null) { try { return JSON.stringify(raw); } catch { return ''; } } return ''; } /** error → 错误文案(message 优先展示简洁业务提示;error 详细原因仅在 message 缺失时兜底) */ export function getErrorText(msg: WsStreamMessage): string { // 只显示 message 的消息(用户确认),error 详细原因不再作为界面展示内容 if (typeof msg?.message === 'string' && msg.message) return msg.message; if (typeof msg?.error === 'string' && msg.error) return msg.error; 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 '执行失败,请重试'; } /** 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 '执行失败,请重试'; }