185 lines
7.8 KiB
TypeScript
185 lines
7.8 KiB
TypeScript
// ===== 首页 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 '执行失败,请重试';
|
||
}
|