// js/chatbot/ui/chatbot-tooltrace-ui.js // 工具调用UI - 生成HTML嵌入到AI消息中 (function(window, document) { 'use strict'; if (window.ChatbotToolTraceUIScriptLoaded) return; var stylesInjected = false; var currentStepsHtml = []; // 存储步骤HTML字符串 var batchBuffer = null; var batchTimer = null; var isFinished = false; var isCollapsed = false; // 默认展开,避免更新时自动收起 function injectStyles() { if (stylesInjected) return; var style = document.createElement('style'); style.textContent = ` /* 工具调用思考块 - 系统日志风格 */ .tool-thinking-block { background: transparent; border-left: 3px solid #e2e8f0; margin-bottom: 16px; margin-top: 16px; padding-left: 16px; transition: all 0.3s ease; font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; } .tool-thinking-block:hover { border-left-color: #cbd5e1; } .tool-thinking-block.collapsed .tool-thinking-body { display: none; } /* 头部 */ .tool-thinking-header { display: flex; align-items: center; justify-content: space-between; padding: 8px 0; cursor: pointer; user-select: none; transition: all 0.2s; color: #64748b; } .tool-thinking-header:hover { color: #334155; } .tool-thinking-title { display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 600; } .tool-thinking-icon-wrapper { display: flex; align-items: center; justify-content: center; width: 20px; height: 20px; border-radius: 4px; background: #f1f5f9; color: #64748b; transition: all 0.3s; } .tool-thinking-icon-wrapper.running { background: #dbeafe; color: #2563eb; } .tool-thinking-icon-wrapper.done { background: #dcfce7; color: #16a34a; } .tool-thinking-icon-wrapper svg { width: 12px; height: 12px; } .tool-thinking-icon-wrapper.running svg { animation: spin 2s linear infinite; } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .tool-thinking-toggle { display: flex; align-items: center; gap: 4px; font-size: 11px; color: #94a3b8; padding: 2px 6px; border-radius: 4px; transition: all 0.2s; } .tool-thinking-header:hover .tool-thinking-toggle { background: #f1f5f9; color: #64748b; } .tool-thinking-toggle .toggle-arrow { transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); display: inline-block; font-size: 10px; } .tool-thinking-block.collapsed .tool-thinking-toggle .toggle-arrow { transform: rotate(-90deg); } /* 内容区 - 紧凑日志布局 */ .tool-thinking-body { padding: 8px 0; max-height: 400px; overflow-y: auto; position: relative; } .tool-thinking-body::-webkit-scrollbar { width: 4px; } .tool-thinking-body::-webkit-scrollbar-track { background: transparent; } .tool-thinking-body::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 2px; } .tool-thinking-body::-webkit-scrollbar-thumb:hover { background: #94a3b8; } /* 步骤项 */ .tool-step { display: flex; gap: 10px; margin-bottom: 8px; position: relative; z-index: 1; animation: slideIn 0.2s ease-out forwards; padding: 6px 8px; border-radius: 6px; transition: background 0.2s; } .tool-step:hover { background: #f8fafc; } @keyframes slideIn { from { opacity: 0; transform: translateX(-5px); } to { opacity: 1; transform: translateX(0); } } .tool-step:last-child { margin-bottom: 0; } .tool-step-indicator { flex-shrink: 0; width: 16px; height: 16px; display: flex; align-items: center; justify-content: center; color: #94a3b8; margin-top: 2px; } .tool-step.running .tool-step-indicator { color: #3b82f6; } .tool-step.done .tool-step-indicator { color: #10b981; } .tool-step.error .tool-step-indicator { color: #ef4444; } .tool-step-indicator svg { width: 12px; height: 12px; } .tool-step-content { flex: 1; min-width: 0; } .tool-step-main { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; } .tool-step-title { flex: 1; font-weight: 500; color: #475569; font-size: 12px; line-height: 1.5; font-family: 'Menlo', 'Monaco', 'Courier New', monospace; } .tool-step.running .tool-step-title { color: #2563eb; } .tool-step.error .tool-step-title { color: #dc2626; } .tool-step-detail-toggle { background: transparent; border: none; font-size: 10px; color: #94a3b8; cursor: pointer; padding: 2px 6px; border-radius: 4px; transition: all 0.2s; white-space: nowrap; } .tool-step-detail-toggle:hover { background: #e2e8f0; color: #475569; } .tool-step.detail-open .tool-step-detail-toggle { background: #e2e8f0; color: #475569; font-weight: 600; } .tool-step-detail { display: none; margin-top: 6px; padding: 8px; background: #f1f5f9; border-radius: 4px; font-size: 11px; color: #475569; white-space: pre-wrap; word-break: break-word; font-family: 'Menlo', 'Monaco', 'Courier New', monospace; line-height: 1.5; border-left: 2px solid #cbd5e1; } .tool-step.detail-open .tool-step-detail { display: block; animation: fadeIn 0.2s ease-out; } @keyframes fadeIn { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: translateY(0); } } /* 状态标签 */ .step-tag { display: inline-block; padding: 0 4px; border-radius: 3px; font-size: 10px; font-weight: normal; margin-left: 6px; vertical-align: middle; opacity: 0.8; } .step-tag.tokens { background: #e2e8f0; color: #64748b; } .step-tag.score { background: #ffedd5; color: #c2410c; } `; document.head.appendChild(style); stylesInjected = true; } /** * 开始新的工具调用会话 */ function startSession() { // console.log('[ToolTraceUI] startSession 调用'); injectStyles(); currentStepsHtml = []; isFinished = false; isCollapsed = false; // 重置为展开 batchBuffer = null; if (batchTimer) { clearTimeout(batchTimer); batchTimer = null; } // 提前插入一个初始化步骤,避免初次渲染为空 try { addStepHtml({ tool: 'preload', message: '准备检索上下文...', args: {} }, 'running'); } catch (_) { /* ignore */ } } /** * 添加步骤HTML */ function addStepHtml(stepInfo, status) { status = status || 'running'; // 动态选择tool名称(如果是带重排的向量搜索,使用特殊tool名) var toolName = stepInfo.tool; if (stepInfo.tool === 'vector_search' && stepInfo.result && Array.isArray(stepInfo.result) && stepInfo.result.length > 0 && stepInfo.result[0].rerankScore !== undefined) { toolName = 'vector_search_rerank'; } var icon = getStepIcon(toolName); var title = getStepTitle(stepInfo); var detail = formatDetail(stepInfo.args || {}); var stepHtml = `
${icon}
${escapeHtml(title)}
${escapeHtml(detail)}
`; currentStepsHtml.push(stepHtml); } /** * 更新最后一个步骤的状态 */ function updateLastStepStatus(status, result) { if (currentStepsHtml.length === 0) return; var lastIdx = currentStepsHtml.length - 1; var lastHtml = currentStepsHtml[lastIdx]; // 替换status class lastHtml = lastHtml.replace(/class="tool-step (running|done|error)"/, 'class="tool-step ' + status + '"'); // 如果有tokens信息,更新标题添加token统计 if (result && result._tokens && status === 'done') { var tokenHtml = ' ' + result._tokens + ' tok'; lastHtml = lastHtml.replace(/([\s\S]*?)(<\/span>)/, '$1' + tokenHtml + '$2'); } // 更新detail if (result) { var detail = formatDetail(result); lastHtml = lastHtml.replace(/
[\s\S]*?<\/div>\s*<\/div>\s*<\/div>/, '
' + escapeHtml(detail) + '
'); } currentStepsHtml[lastIdx] = lastHtml; } /** * 生成完整的thinking块HTML */ function generateBlockHtml() { var stepCount = currentStepsHtml.length; var iconClass = isFinished ? 'done' : 'running'; var title = isFinished ? '思考过程已完成' : '正在思考中...'; var collapsedClass = isCollapsed ? 'collapsed' : ''; // 顶部图标 var headerIcon = isFinished ? '' : ''; if (currentStepsHtml.length === 0) { return ''; } // 使用全局变量 isCollapsed 控制状态,并添加 onclick 事件来切换该变量 // 注意:这里我们使用 window.ChatbotToolTraceUI.toggleCollapse 来切换状态,而不是直接操作 DOM // 这样可以确保状态同步 // 为了支持 onclick 调用,我们需要暴露一个 toggle 方法 if (!window.ChatbotToolTraceUI.toggleCollapse) { window.ChatbotToolTraceUI.toggleCollapse = function(el) { isCollapsed = !isCollapsed; var block = el.closest('.tool-thinking-block'); if (isCollapsed) { block.classList.add('collapsed'); } else { block.classList.remove('collapsed'); } }; } var html = `
${headerIcon}
${title}
${stepCount} 步骤
${currentStepsHtml.join('')}
`; return html; } /** * 刷新批量缓冲区 */ function flushBatchBuffer() { if (!batchBuffer) return; var items = batchBuffer.items; var tool = batchBuffer.tool; if (tool === 'fetch_group') { var groupIds = items.map(function(item) { return item.groupId; }); // 优化显示:如果groupId太多,只显示前3个和总数 var displayText; if (groupIds.length > 5) { displayText = '获取意群详情: ' + groupIds.slice(0, 3).join(', ') + ' 等' + groupIds.length + '个'; } else { displayText = '获取意群详情: ' + groupIds.join(', '); } var detail = formatDetail({ tool: tool, count: items.length, groups: groupIds }); addStepHtml({ tool: tool, customTitle: displayText, args: { count: items.length, groups: groupIds } }, 'done'); } batchBuffer = null; } /** * 处理流式事件 */ function handleStreamEvent(event) { // console.log('[ToolTraceUI] handleStreamEvent:', event.type, event); switch (event.type) { case 'status': if (event.phase === 'preload' || event.phase === 'planning') { flushBatchBuffer(); addStepHtml({ tool: event.phase, message: event.message, args: {} }, 'running'); } break; case 'round_start': flushBatchBuffer(); addStepHtml({ tool: 'round', round: event.round, args: { round: event.round + 1 } }, 'running'); updateLastStepStatus('done', { message: '开始取材...' }); break; case 'plan': flushBatchBuffer(); if (currentStepsHtml.length > 0) { const planInfo = { operations: event.data.operations.length + ' 个操作', final: event.data.final }; // 如果有taskStatus,追加到显示信息中 if (event.data.taskStatus && event.data.taskStatus.current) { planInfo.task = event.data.taskStatus.current; } updateLastStepStatus('done', planInfo); } break; case 'task_status': // 展示任务追踪状态 flushBatchBuffer(); const taskStatus = event.status || {}; const taskParts = []; if (Array.isArray(taskStatus.completed) && taskStatus.completed.length > 0) { taskParts.push('已完成: ' + taskStatus.completed.join('; ')); } if (taskStatus.current) { taskParts.push('当前: ' + taskStatus.current); } if (Array.isArray(taskStatus.pending) && taskStatus.pending.length > 0) { taskParts.push('待办: ' + taskStatus.pending.join('; ')); } if (taskParts.length > 0) { addStepHtml({ tool: 'task_status', message: '任务追踪', args: { status: taskParts.join(' | ') } }, 'done'); } break; case 'tool_start': if (batchTimer) { clearTimeout(batchTimer); batchTimer = null; } // 批量合并fetch_group if (event.tool === 'fetch_group' && event.args && event.args.groupId) { if (batchBuffer && batchBuffer.tool === 'fetch_group') { batchBuffer.items.push({ groupId: event.args.groupId, granularity: event.args.granularity }); } else { flushBatchBuffer(); batchBuffer = { tool: 'fetch_group', items: [{ groupId: event.args.groupId, granularity: event.args.granularity }] }; } batchTimer = setTimeout(flushBatchBuffer, 100); } else { flushBatchBuffer(); addStepHtml({ tool: event.tool, args: event.args }, 'running'); } break; case 'tool_result': if (batchTimer) { clearTimeout(batchTimer); batchTimer = null; } flushBatchBuffer(); // 保存tokens信息供标题使用 var resultData = event.result || {}; if (event.tokens) { resultData._tokens = event.tokens; } updateLastStepStatus('done', resultData); break; case 'token_usage': // 展示规划器token使用 addStepHtml({ tool: 'planning', customTitle: `AI规划器 (输入: ${event.tokens.input}tok, 输出: ${event.tokens.output}tok, 共: ${event.tokens.total}tok)`, args: {} }, 'done'); break; case 'tool_error': flushBatchBuffer(); updateLastStepStatus('error', { error: event.error }); break; case 'tool_skip': // 静默处理,不显示 break; case 'round_end': flushBatchBuffer(); if (currentStepsHtml.length > 0) { if (event.final) { updateLastStepStatus('done', { message: '✓ 取材完成' }); } else { updateLastStepStatus('done', { message: '→ 继续下一轮' }); } } break; case 'complete': flushBatchBuffer(); isFinished = true; // 展示最终token统计 if (event.summary && event.summary.stats && event.summary.stats.finalContextTokens) { addStepHtml({ tool: 'info', customTitle: `✓ 最终上下文 (共 ${event.summary.stats.finalContextTokens} tokens)`, args: {} }, 'done'); } break; case 'info': flushBatchBuffer(); addStepHtml({ tool: 'info', message: event.message, args: {} }, 'running'); updateLastStepStatus('done', { message: event.message }); break; case 'error': case 'warning': flushBatchBuffer(); addStepHtml({ tool: event.type, message: event.message, args: {} }, 'running'); updateLastStepStatus('error', { error: event.message }); break; } } /** * 处理ReAct事件(新增) */ // 并行工具调用追踪(用于匹配 start 和 complete 事件) var parallelCallsTracker = {}; function handleReActEvent(event) { if (!event || !event.type) return; switch (event.type) { case 'context_initialized': addStepHtml({ tool: 'preload', message: '初始化上下文', args: { preview: event.context ? event.context.slice(0, 100) : '' } }, 'done'); break; case 'iteration_start': // 重置并行调用追踪 parallelCallsTracker = {}; addStepHtml({ tool: 'round', message: `第 ${event.iteration}/${event.maxIterations} 轮推理`, args: { iteration: event.iteration, maxIterations: event.maxIterations } }, 'running'); break; case 'reasoning_start': // 静默处理,不单独显示 break; case 'reasoning_complete': if (currentStepsHtml.length > 0) { const thoughtPreview = event.thought ? event.thought.slice(0, 100) : ''; updateLastStepStatus('done', { thought: thoughtPreview, action: event.action }); } break; case 'tool_call_start': // 并行工具调用:显示组标题(只在第一个工具时) if (event.parallel && event.totalCalls > 1) { const trackKey = `iter_${event.iteration}`; if (!parallelCallsTracker[trackKey]) { parallelCallsTracker[trackKey] = { totalCalls: event.totalCalls, completedCalls: 0, startIndex: currentStepsHtml.length }; addStepHtml({ tool: 'parallel', message: `🔀 并行调用 ${event.totalCalls} 个工具`, args: { totalCalls: event.totalCalls } }, 'running'); } } // 添加工具调用步骤 const toolMessage = event.parallel ? ` ├─ ${event.tool}` : `调用工具: ${event.tool}`; addStepHtml({ tool: event.tool, message: toolMessage, args: event.params || {}, parallel: event.parallel || false, toolName: event.tool // 用于匹配 complete 事件 }, 'running'); break; case 'tool_call_complete': // 查找对应的 tool_call_start 步骤并更新 const result = event.result || {}; const status = result.success === false ? 'error' : 'done'; // 从后往前找到匹配的工具步骤 for (let i = currentStepsHtml.length - 1; i >= 0; i--) { const step = currentStepsHtml[i]; if (step.toolName === event.tool && step.status === 'running') { // 更新这个步骤的状态 currentStepsHtml[i].status = status; currentStepsHtml[i].result = result; // 如果是并行调用,更新计数 if (event.parallel) { const trackKey = `iter_${event.iteration}`; if (parallelCallsTracker[trackKey]) { parallelCallsTracker[trackKey].completedCalls++; // 所有并行工具都完成了,更新组标题状态 if (parallelCallsTracker[trackKey].completedCalls === parallelCallsTracker[trackKey].totalCalls) { const groupIndex = parallelCallsTracker[trackKey].startIndex; if (currentStepsHtml[groupIndex]) { currentStepsHtml[groupIndex].status = 'done'; currentStepsHtml[groupIndex].message = `🔀 并行调用完成 (${parallelCallsTracker[trackKey].totalCalls} 个工具)`; } } } } // 重新渲染 renderSteps(); break; } } break; case 'context_updated': const parallelInfo = event.parallelCallsCount > 0 ? ` [并行${event.parallelCallsCount}个工具]` : ''; addStepHtml({ tool: 'info', message: `上下文更新 (${event.contextSize} 字符, ~${event.estimatedTokens} tokens)${parallelInfo}`, args: {} }, 'done'); break; case 'context_pruned': addStepHtml({ tool: 'warning', message: `上下文裁剪 (${event.before} → ${event.after} tokens)`, args: {} }, 'done'); break; case 'final_answer': isFinished = true; const suffix = event.fallback ? ' (降级回答)' : ''; addStepHtml({ tool: 'info', customTitle: `✓ 完成推理${suffix} (${event.iterations} 轮, ${event.toolCallCount} 次工具调用)`, args: {} }, 'done'); break; case 'max_iterations_reached': isFinished = true; addStepHtml({ tool: 'warning', message: `达到最大迭代次数 (${event.iterations} 轮, ${event.toolCallCount} 次工具调用)`, args: {} }, 'error'); break; case 'error': addStepHtml({ tool: 'error', message: event.error || '未知错误', args: {} }, 'error'); break; } } /** * 获取步骤图标(SVG) */ function getStepIcon(tool) { var icons = { 'vector_search': '', 'vector_search_rerank': '', 'keyword_search': '', 'fetch_group': '', 'fetch': '', 'map': '', 'grep': '', 'parallel': '', 'preload': '', 'planning': '', 'round': '', 'task_status': '', 'info': '', 'warning': '', 'error': '' }; return icons[tool] || ''; } /** * 获取步骤标题 */ function getStepTitle(stepInfo) { if (stepInfo.customTitle) return stepInfo.customTitle; var titles = { 'vector_search': '向量搜索', 'vector_search_rerank': '向量搜索+重排', 'keyword_search': '关键词搜索', 'fetch_group': '获取意群详情', 'fetch': '获取意群详情', 'map': '获取意群地图', 'grep': '全文短语搜索', 'parallel': '并行工具调用', 'preload': '预加载意群', 'planning': 'AI规划工具调用', 'round': '第' + ((stepInfo.round || 0) + 1) + '轮取材', 'task_status': '任务追踪', 'info': '信息', 'warning': '警告', 'error': '错误' }; var title = titles[stepInfo.tool || stepInfo.phase] || stepInfo.message || '执行中'; // 添加参数信息 if (stepInfo.tool === 'vector_search' && stepInfo.args && stepInfo.args.query) { // 检查是否使用了重排(通过result判断) if (stepInfo.result && Array.isArray(stepInfo.result) && stepInfo.result.length > 0 && stepInfo.result[0].rerankScore !== undefined) { title = '向量搜索+重排'; } var query = stepInfo.args.query.substring(0, 30); if (stepInfo.args.query.length > 30) query += '...'; title += ': ' + query; } else if (stepInfo.tool === 'grep' && stepInfo.args && stepInfo.args.query) { var gq = stepInfo.args.query.substring(0, 30); if (stepInfo.args.query.length > 30) gq += '...'; title += ': ' + gq; } else if (stepInfo.tool === 'keyword_search' && stepInfo.args && stepInfo.args.keywords) { var keywords = stepInfo.args.keywords || []; title += ': ' + (Array.isArray(keywords) ? keywords.join(', ') : keywords); } else if ((stepInfo.tool === 'fetch_group' || stepInfo.tool === 'fetch') && stepInfo.args && stepInfo.args.groupId) { title += ': ' + stepInfo.args.groupId; } return title; } /** * 格式化详情 */ function formatDetail(value) { if (value === undefined || value === null) return ''; if (typeof value === 'string') return value.trim(); // 提取并移除tokens信息(已在标题中显示) var tokens = value._tokens; var cleanValue = Object.assign({}, value); delete cleanValue._tokens; // 特殊处理:空数组 if (Array.isArray(cleanValue) && cleanValue.length === 0) { return '无结果'; } // 特殊处理:向量搜索结果 // 支持数组格式和对象格式(如 {"0": {...}, "1": {...}}) var searchResults = null; if (Array.isArray(cleanValue) && cleanValue.length > 0 && cleanValue[0].chunkId && cleanValue[0].score !== undefined) { searchResults = cleanValue; } else if (typeof cleanValue === 'object' && !Array.isArray(cleanValue) && Object.keys(cleanValue).length > 0) { // 检查是否是对象格式的搜索结果(键为数字字符串) var firstKey = Object.keys(cleanValue)[0]; var firstItem = cleanValue[firstKey]; if (firstItem && firstItem.chunkId && firstItem.score !== undefined) { // 转换为数组格式 searchResults = Object.keys(cleanValue).map(function(key) { return cleanValue[key]; }); } } if (searchResults) { // 检查是否使用了重排 var hasRerank = searchResults[0].rerankScore !== undefined; var summary = searchResults.length + ' 个结果'; if (hasRerank) { summary += ' (已重排)'; } var topGroups = {}; searchResults.forEach(function(item) { if (item.belongsToGroup) { topGroups[item.belongsToGroup] = true; } }); var groupCount = Object.keys(topGroups).length; if (groupCount > 0) { summary += ',涉及 ' + groupCount + ' 个意群'; } if (hasRerank) { summary += ',最高重排分: ' + searchResults[0].rerankScore.toFixed(3); summary += ' (原始分: ' + (searchResults[0].originalScore || searchResults[0].score).toFixed(3) + ')'; } else { summary += ',最高分: ' + searchResults[0].score.toFixed(3); } // 添加前3个结果的预览 if (searchResults.length > 0) { summary += '\n\n【前' + Math.min(3, searchResults.length) + '个结果预览】\n'; searchResults.slice(0, 3).forEach(function(item, idx) { var preview = sanitizeText(item.preview || ''); if (preview.length > 150) preview = preview.substring(0, 150) + '...'; var scoreInfo = hasRerank ? '重排分:' + item.rerankScore.toFixed(3) + ' | 原始:' + (item.originalScore || item.score).toFixed(3) : '分数:' + item.score.toFixed(3); summary += (idx + 1) + '. ' + item.belongsToGroup + ' (' + scoreInfo + ')\n ' + preview + '\n'; }); } return summary; } // 特殊处理:关键词搜索结果 var keywordResults = null; if (Array.isArray(cleanValue) && cleanValue.length > 0 && cleanValue[0].preview !== undefined && cleanValue[0].matchedKeywords) { keywordResults = cleanValue; } else if (typeof cleanValue === 'object' && !Array.isArray(cleanValue) && Object.keys(cleanValue).length > 0) { var firstKey = Object.keys(cleanValue)[0]; var firstItem = cleanValue[firstKey]; if (firstItem && firstItem.preview !== undefined && firstItem.matchedKeywords) { keywordResults = Object.keys(cleanValue).map(function(key) { return cleanValue[key]; }); } } if (keywordResults) { var summary = keywordResults.length + ' 个匹配片段'; // 统计所有匹配的关键词 var allMatched = {}; keywordResults.forEach(function(item) { if (item.matchedKeywords && Array.isArray(item.matchedKeywords)) { item.matchedKeywords.forEach(function(kw) { allMatched[kw] = (allMatched[kw] || 0) + 1; }); } }); var matchedList = Object.keys(allMatched); if (matchedList.length > 0) { summary += ',匹配: ' + matchedList.join(', '); } // 添加前3个结果的预览 if (keywordResults.length > 0) { summary += '\n\n【前' + Math.min(3, keywordResults.length) + '个结果预览】\n'; keywordResults.slice(0, 3).forEach(function(item, idx) { var preview = sanitizeText(item.preview || ''); if (preview.length > 150) preview = preview.substring(0, 150) + '...'; var matched = item.matchedKeywords ? ' [' + item.matchedKeywords.join(',') + ']' : ''; summary += (idx + 1) + '. ' + (item.belongsToGroup || '全文') + matched + '\n ' + preview + '\n'; }); } return summary; } // 特殊处理:grep搜索结果 var grepResults = null; if (Array.isArray(cleanValue) && cleanValue.length > 0 && cleanValue[0].preview !== undefined && cleanValue[0].matchedKeyword !== undefined) { grepResults = cleanValue; } else if (typeof cleanValue === 'object' && !Array.isArray(cleanValue) && Object.keys(cleanValue).length > 0) { var firstKey = Object.keys(cleanValue)[0]; var firstItem = cleanValue[firstKey]; if (firstItem && firstItem.preview !== undefined && firstItem.matchedKeyword !== undefined) { grepResults = Object.keys(cleanValue).map(function(key) { return cleanValue[key]; }); } } if (grepResults) { var summary = grepResults.length + ' 个匹配片段'; // 统计匹配的关键词 var keywordCounts = {}; grepResults.forEach(function(item) { if (item.matchedKeyword) { keywordCounts[item.matchedKeyword] = (keywordCounts[item.matchedKeyword] || 0) + 1; } }); var keywords = Object.keys(keywordCounts); if (keywords.length > 0) { var keywordList = keywords.map(function(kw) { return kw + '(' + keywordCounts[kw] + ')'; }).join(', '); summary += ',匹配: ' + keywordList; } var topGroups = {}; grepResults.forEach(function(item) { if (item.belongsToGroup) { topGroups[item.belongsToGroup] = true; } }); var groupCount = Object.keys(topGroups).length; if (groupCount > 0) { summary += ',涉及 ' + groupCount + ' 个意群'; } // 添加前3个结果的预览 if (grepResults.length > 0) { summary += '\n\n【前' + Math.min(3, grepResults.length) + '个结果预览】\n'; grepResults.slice(0, 3).forEach(function(item, idx) { var preview = sanitizeText(item.preview || ''); if (preview.length > 150) preview = preview.substring(0, 150) + '...'; var src = item.belongsToGroup ? item.belongsToGroup : '全文'; var matched = item.matchedKeyword ? ' [' + item.matchedKeyword + ']' : ''; summary += (idx + 1) + '. ' + src + matched + '\n ' + preview + '\n'; }); } return summary; } // 特殊处理:其他grep搜索结果(无matchedKeyword字段) var otherResults = null; if (Array.isArray(cleanValue) && cleanValue.length > 0 && cleanValue[0].preview !== undefined) { otherResults = cleanValue; } else if (typeof cleanValue === 'object' && !Array.isArray(cleanValue) && Object.keys(cleanValue).length > 0) { var firstKey = Object.keys(cleanValue)[0]; var firstItem = cleanValue[firstKey]; if (firstItem && firstItem.preview !== undefined) { otherResults = Object.keys(cleanValue).map(function(key) { return cleanValue[key]; }); } } if (otherResults) { var summary = otherResults.length + ' 个匹配片段'; var topGroups = {}; otherResults.forEach(function(item) { if (item.belongsToGroup) { topGroups[item.belongsToGroup] = true; } }); var groupCount = Object.keys(topGroups).length; if (groupCount > 0) { summary += ',涉及 ' + groupCount + ' 个意群'; } // 添加前3个结果的预览 if (otherResults.length > 0) { summary += '\n\n【前' + Math.min(3, otherResults.length) + '个结果预览】\n'; otherResults.slice(0, 3).forEach(function(item, idx) { var preview = sanitizeText(item.preview || ''); if (preview.length > 150) preview = preview.substring(0, 150) + '...'; var src = item.belongsToGroup ? item.belongsToGroup : '全文'; summary += (idx + 1) + '. ' + src + '\n ' + preview + '\n'; }); } return summary; } // 特殊处理:简单对象 if (typeof cleanValue === 'object' && !Array.isArray(cleanValue)) { var keys = Object.keys(cleanValue); if (keys.length === 0) return '无数据'; if (keys.length === 1 && cleanValue.message) return cleanValue.message; if (keys.length === 2 && cleanValue.operations && cleanValue.final !== undefined) { return cleanValue.operations + (cleanValue.final ? ' (最后一轮)' : ''); } // 其他对象,简化显示 var parts = []; for (var key in cleanValue) { if (cleanValue.hasOwnProperty(key)) { var val = cleanValue[key]; if (typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean') { parts.push(key + ': ' + val); } else if (Array.isArray(val)) { parts.push(key + ': ' + val.length + ' 项'); } } } return parts.length > 0 ? parts.join(', ') : JSON.stringify(cleanValue, null, 2); } try { return JSON.stringify(cleanValue, null, 2); } catch (_) { return String(cleanValue); } } /** * 清理文本内容,移除潜在的危险字符和HTML标签 * @param {string} text - 待清理的文本 * @returns {string} - 清理后的文本 */ function sanitizeText(text) { if (!text || typeof text !== 'string') return ''; // 1. 移除HTML标签(包括不完整的标签) text = text.replace(/<[^>]*>/g, ''); // 2. 移除剩余的尖括号(防止破坏DOM结构) // 注意:这会影响数学表达式如 "<0.001",但为了安全性这是必要的 text = text.replace(/[<>]/g, ''); // 3. 移除控制字符(保留常用的空白字符) text = text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/g, ''); // 4. 规范化空白字符 text = text.replace(/\s+/g, ' ').trim(); // 5. 移除不完整的Unicode代理对 text = text.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/g, ''); return text; } /** * HTML转义 */ function escapeHtml(text) { var div = document.createElement('div'); div.textContent = text; return div.innerHTML; } window.ChatbotToolTraceUI = { startSession: startSession, handleStreamEvent: handleStreamEvent, handleReActEvent: handleReActEvent, // 新增:ReAct事件处理器 generateBlockHtml: generateBlockHtml, ensureStyles: injectStyles // 导出以便外部调用 }; // 页面加载时立即注入样式,确保刷新后样式可用 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', injectStyles); } else { injectStyles(); } window.ChatbotToolTraceUIScriptLoaded = true; })(window, document);