| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155 |
- /**
- * 运行记录回放 utils:从持久化的 logs JSON 中还原运行时态数据。
- *
- * 后端 HermesBridgeClient.java 在 SSE 推送的同时,已将所有事件持久化到
- * WorkflowRunNode.logs(JSON 数组)。前端运行记录页从这里还原:
- * - streamingThinking:拼接所有 THINKING 日志的 message
- * - toolHistory:从 TOOL_CALL / TOOL_RESULT 按时间顺序配对
- * - todos:取最后一条 TODO_UPDATE 的 detail(JSON 字符串),兼容数组 / {todos: [...]}
- * - nonThinkingLogs:过滤掉 THINKING 类型后的日志列表(用于执行日志面板)
- *
- * 这样运行记录页能完整复现编辑器"运行结果"Tab 中除"实时态"外的所有内容。
- */
- /**
- * 安全解析 JSON 字符串,失败返回 null。
- */
- function parseJson(str) {
- if (!str) return null
- try { return JSON.parse(str) } catch { return null }
- }
- /**
- * 从日志数组中提取思考过程(THINKING 类型日志的 message 拼接)。
- * @param {string} logsJson 节点的 logs 字段(JSON 字符串)
- * @returns {string} 完整思考过程文本;无则返回空串
- */
- export function extractStreamingThinking(logsJson) {
- const logs = parseJson(logsJson)
- if (!Array.isArray(logs)) return ''
- return logs
- .filter(l => l.type === 'THINKING')
- .map(l => l.message || '')
- .join('')
- }
- /**
- * 从日志数组中还原工具调用历史(按时间顺序的 [{name, argsSummary, result}])。
- *
- * 配对规则:
- * - TOOL_CALL 日志:message=toolName,detail=argsSummary
- * - TOOL_RESULT 日志:message=toolName,detail=resultSummary
- * - 相邻的 TOOL_CALL → TOOL_RESULT 配对一个完整工具调用
- * - 若 TOOL_CALL 后跟另一个 TOOL_CALL(无 result),仍作为一条记录入栈,result=null
- *
- * @param {string} logsJson 节点的 logs 字段(JSON 字符串)
- * @returns {Array<{name:string, argsSummary:string, result:string|null}>}
- */
- export function extractToolHistory(logsJson) {
- const logs = parseJson(logsJson)
- if (!Array.isArray(logs)) return []
- const history = []
- let pending = null // 等待 result 的 tool_call
- for (const log of logs) {
- if (log.type === 'TOOL_CALL') {
- // 上一个 tool_call 没 result,直接入栈
- if (pending) {
- history.push(pending)
- }
- pending = {
- name: log.message || '(unknown)',
- argsSummary: log.detail || '',
- result: null,
- }
- } else if (log.type === 'TOOL_RESULT') {
- if (pending) {
- pending.result = log.detail || ''
- history.push(pending)
- pending = null
- } else {
- // 异常:tool_result 无对应 tool_call,作为孤儿记录
- history.push({
- name: log.message || '(unknown)',
- argsSummary: '',
- result: log.detail || '',
- })
- }
- }
- }
- // 兜底:最后一个未配对的 tool_call
- if (pending) {
- history.push(pending)
- }
- // 编辑器实时展示是 unshift 顺序(最新在前),保持一致
- return history.reverse()
- }
- /**
- * 从日志数组中取最后一条 TODO_UPDATE,解析出 todos 数组。
- * 兼容两种格式:
- * - 数组:[{content, status, ...}, ...]
- * - 对象:{todos: [...], summary: {...}}(Hermes 内置 todo_tool 实际返回结构)
- *
- * @param {string} logsJson 节点的 logs 字段(JSON 字符串)
- * @returns {Array} todos 数组;无则返回空数组
- */
- export function extractLatestTodos(logsJson) {
- const logs = parseJson(logsJson)
- if (!Array.isArray(logs)) return []
- // 从后往前找第一条 TODO_UPDATE
- for (let i = logs.length - 1; i >= 0; i--) {
- if (logs[i].type === 'TODO_UPDATE') {
- const detail = logs[i].detail || ''
- try {
- const parsed = JSON.parse(detail)
- const arr = Array.isArray(parsed) ? parsed : (parsed?.todos || [])
- return Array.isArray(arr) ? arr : []
- } catch { /* ignore */ }
- }
- }
- return []
- }
- /**
- * 过滤掉 THINKING 类型日志:思考过程已在 .nrc-thinking 集中展示,
- * 执行日志面板再显示会造成内容重复。
- * @param {string} logsJson 节点的 logs 字段(JSON 字符串)
- * @returns {Array} 非 THINKING 类型的日志数组
- */
- export function extractNonThinkingLogs(logsJson) {
- const logs = parseJson(logsJson)
- if (!Array.isArray(logs)) return []
- return logs.filter(l => l.type !== 'THINKING')
- }
- /**
- * 将节点(来自后端 WorkflowRunNode)转换为编辑器 nodeResults 中的节点格式。
- *
- * 用于让运行记录页直接复用 AgentThinkingPanel 等组件:
- * - status: 后端返回的是大写(SUCCESS/FAILED/...),转换为小写以匹配组件判断
- * - streamingThinking / toolHistory / todos 从 logs 还原
- * - currentStatus / currentTool 留空(节点已完成,无实时态)
- *
- * @param {Object} node 后端 WorkflowRunNode(含 logs / output / contextSnapshot)
- * @returns {Object} 编辑器节点格式
- */
- export function buildReplayNode(node) {
- const logsJson = node?.logs || ''
- return {
- nodeId: node?.nodeId,
- type: node?.nodeType,
- label: node?.label || node?.nodeId || '节点',
- status: (node?.status || '').toLowerCase(),
- output: node?.output ? parseJson(node.output) : null,
- logs: extractNonThinkingLogs(logsJson),
- contextSnapshot: node?.contextSnapshot ? parseJson(node.contextSnapshot) : null,
- error: node?.error || '',
- // 从持久化 logs 还原的运行时态
- streamingThinking: extractStreamingThinking(logsJson),
- toolHistory: extractToolHistory(logsJson),
- todos: extractLatestTodos(logsJson),
- // 已完成节点无实时态
- currentStatus: '',
- currentTool: null,
- }
- }
|