// SuperBizAgent 前端应用
class SuperBizAgentApp {
constructor() {
this.apiBaseUrl = 'http://localhost:9900/api';
this.currentMode = 'quick'; // 'quick' 或 'stream'
this.sessionId = this.generateSessionId();
this.isStreaming = false;
this.currentChatHistory = []; // 当前对话的消息历史
this.chatHistories = this.loadChatHistories(); // 所有历史对话
this.isCurrentChatFromHistory = false; // 标记当前对话是否是从历史记录加载的
this.lastTraceSessionId = this.loadLastTraceSessionId();
this.initializeElements();
this.bindEvents();
this.updateUI();
this.initMarkdown();
this.checkAndSetCentered();
this.renderChatHistory();
this.updateTraceWorkbenchLink();
}
// 初始化Markdown配置
initMarkdown() {
// 等待 marked 库加载完成
const checkMarked = () => {
if (typeof marked !== 'undefined') {
try {
// 配置marked选项
marked.setOptions({
breaks: true, // 支持GFM换行
gfm: true, // 启用GitHub风格的Markdown
headerIds: false,
mangle: false
});
// 配置代码高亮
if (typeof hljs !== 'undefined') {
marked.setOptions({
highlight: function(code, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return hljs.highlight(code, { language: lang }).value;
} catch (err) {
console.error('代码高亮失败:', err);
}
}
return code;
}
});
}
console.log('Markdown 渲染库初始化成功');
} catch (e) {
console.error('Markdown 配置失败:', e);
}
} else {
// 如果 marked 还没加载,等待一段时间后重试
setTimeout(checkMarked, 100);
}
};
checkMarked();
}
// 安全地渲染 Markdown
renderMarkdown(content) {
if (!content) return '';
// 检查 marked 是否可用
if (typeof marked === 'undefined') {
console.warn('marked 库未加载,使用纯文本显示');
return this.escapeHtml(content);
}
try {
const html = marked.parse(content);
return html;
} catch (e) {
console.error('Markdown 渲染失败:', e);
return this.escapeHtml(content);
}
}
// 高亮代码块
highlightCodeBlocks(container) {
if (typeof hljs !== 'undefined' && container) {
try {
container.querySelectorAll('pre code').forEach((block) => {
if (!block.classList.contains('hljs')) {
hljs.highlightElement(block);
}
});
} catch (e) {
console.error('代码高亮失败:', e);
}
}
}
// 初始化DOM元素
initializeElements() {
// 侧边栏元素
this.sidebar = document.querySelector('.sidebar');
this.newChatBtn = document.getElementById('newChatBtn');
this.aiOpsSidebarBtn = document.getElementById('aiOpsSidebarBtn');
// 输入区域元素
this.messageInput = document.getElementById('messageInput');
this.sendButton = document.getElementById('sendButton');
this.toolsBtn = document.getElementById('toolsBtn');
this.toolsMenu = document.getElementById('toolsMenu');
this.uploadFileItem = document.getElementById('uploadFileItem');
this.modeSelectorBtn = document.getElementById('modeSelectorBtn');
this.modeDropdown = document.getElementById('modeDropdown');
this.currentModeText = document.getElementById('currentModeText');
this.fileInput = document.getElementById('fileInput');
// 聊天区域元素
this.chatMessages = document.getElementById('chatMessages');
this.loadingOverlay = document.getElementById('loadingOverlay');
this.chatContainer = document.querySelector('.chat-container');
this.welcomeGreeting = document.getElementById('welcomeGreeting');
this.chatHistoryList = document.getElementById('chatHistoryList');
this.traceWorkbenchLink = document.getElementById('traceWorkbenchLink');
// 初始化时检查是否需要居中
this.checkAndSetCentered();
}
// 绑定事件监听器
bindEvents() {
// 新建对话
if (this.newChatBtn) {
this.newChatBtn.addEventListener('click', () => this.newChat());
}
// AI Ops按钮
if (this.aiOpsSidebarBtn) {
this.aiOpsSidebarBtn.addEventListener('click', () => this.triggerAIOps());
}
// 模式选择下拉菜单
if (this.modeSelectorBtn) {
this.modeSelectorBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.toggleModeDropdown();
});
}
// 下拉菜单项点击
const dropdownItems = document.querySelectorAll('.dropdown-item');
dropdownItems.forEach(item => {
item.addEventListener('click', (e) => {
const mode = item.getAttribute('data-mode');
this.selectMode(mode);
this.closeModeDropdown();
});
});
// 点击外部关闭下拉菜单
document.addEventListener('click', (e) => {
if (!this.modeSelectorBtn.contains(e.target) &&
!this.modeDropdown.contains(e.target)) {
this.closeModeDropdown();
}
});
// 发送消息
if (this.sendButton) {
this.sendButton.addEventListener('click', () => this.sendMessage());
}
if (this.messageInput) {
this.messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
this.sendMessage();
}
});
}
// 工具按钮和菜单
if (this.toolsBtn) {
this.toolsBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.toggleToolsMenu();
});
}
// 工具菜单项点击事件
if (this.uploadFileItem) {
this.uploadFileItem.addEventListener('click', () => {
if (this.fileInput) {
this.fileInput.click();
}
this.closeToolsMenu();
});
}
// 点击外部关闭工具菜单
document.addEventListener('click', (e) => {
if (this.toolsBtn && this.toolsMenu &&
!this.toolsBtn.contains(e.target) &&
!this.toolsMenu.contains(e.target)) {
this.closeToolsMenu();
}
});
if (this.fileInput) {
this.fileInput.addEventListener('change', (e) => this.handleFileSelect(e));
}
}
// 切换工具菜单显示/隐藏
toggleToolsMenu() {
if (this.toolsMenu && this.toolsBtn) {
const wrapper = this.toolsBtn.closest('.tools-btn-wrapper');
if (wrapper) {
wrapper.classList.toggle('active');
}
}
}
// 关闭工具菜单
closeToolsMenu() {
if (this.toolsMenu && this.toolsBtn) {
const wrapper = this.toolsBtn.closest('.tools-btn-wrapper');
if (wrapper) {
wrapper.classList.remove('active');
}
}
}
// 新建对话
newChat() {
if (this.isStreaming) {
this.showNotification('请等待当前对话完成后再新建对话', 'warning');
return;
}
// 如果当前有对话内容,且不是从历史记录加载的,才保存为新的历史对话
// 如果是从历史记录加载的,只需要更新该历史记录
if (this.currentChatHistory.length > 0) {
if (this.isCurrentChatFromHistory) {
// 当前对话是从历史记录加载的,更新该历史记录
this.updateCurrentChatHistory();
} else {
// 当前对话是新对话,保存为新的历史对话
this.saveCurrentChat();
}
}
// 停止所有进行中的操作
this.isStreaming = false;
// 清空输入框
if (this.messageInput) {
this.messageInput.value = '';
}
// 清空当前对话历史
this.currentChatHistory = [];
// 重置标记
this.isCurrentChatFromHistory = false;
// 清空聊天记录
if (this.chatMessages) {
this.chatMessages.innerHTML = '';
}
// 生成新的会话ID
this.sessionId = this.generateSessionId();
this.updateTraceWorkbenchLink();
// 重置模式为快速
this.currentMode = 'quick';
this.updateUI();
// 重新设置居中样式(确保对话框居中显示)
this.checkAndSetCentered();
// 确保容器有过渡动画
if (this.chatContainer) {
this.chatContainer.style.transition = 'all 0.5s ease';
}
// 更新历史对话列表
this.renderChatHistory();
}
// 保存当前对话到历史记录(新建)
saveCurrentChat() {
if (this.currentChatHistory.length === 0) {
return;
}
// 检查是否已存在相同ID的历史记录
const existingIndex = this.chatHistories.findIndex(h => h.id === this.sessionId);
if (existingIndex !== -1) {
// 如果已存在,更新而不是新建
this.updateCurrentChatHistory();
return;
}
// 获取对话标题(使用第一条用户消息的前30个字符)
const firstUserMessage = this.currentChatHistory.find(msg => msg.type === 'user');
const title = firstUserMessage ?
(firstUserMessage.content.substring(0, 30) + (firstUserMessage.content.length > 30 ? '...' : '')) :
'新对话';
const chatHistory = {
id: this.sessionId,
title: title,
messages: [...this.currentChatHistory],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
// 添加到历史记录列表的开头
this.chatHistories.unshift(chatHistory);
this.rememberTraceSessionId(this.sessionId);
// 限制历史记录数量(最多保存50条)
if (this.chatHistories.length > 50) {
this.chatHistories = this.chatHistories.slice(0, 50);
}
// 保存到localStorage
this.saveChatHistories();
}
// 更新当前对话的历史记录
updateCurrentChatHistory() {
if (this.currentChatHistory.length === 0) {
return;
}
const existingIndex = this.chatHistories.findIndex(h => h.id === this.sessionId);
if (existingIndex === -1) {
// 如果不存在,调用保存方法
this.saveCurrentChat();
return;
}
// 更新现有的历史记录
const history = this.chatHistories[existingIndex];
history.messages = [...this.currentChatHistory];
history.updatedAt = new Date().toISOString();
// 如果标题需要更新(第一条消息改变了)
const firstUserMessage = this.currentChatHistory.find(msg => msg.type === 'user');
if (firstUserMessage) {
const newTitle = firstUserMessage.content.substring(0, 30) + (firstUserMessage.content.length > 30 ? '...' : '');
if (history.title !== newTitle) {
history.title = newTitle;
}
}
// 保存到localStorage
this.saveChatHistories();
}
// 加载历史对话列表
loadChatHistories() {
try {
const stored = localStorage.getItem('chatHistories');
return stored ? JSON.parse(stored) : [];
} catch (e) {
console.error('加载历史对话失败:', e);
return [];
}
}
// 保存历史对话列表到localStorage
saveChatHistories() {
try {
localStorage.setItem('chatHistories', JSON.stringify(this.chatHistories));
} catch (e) {
console.error('保存历史对话失败:', e);
}
}
// 渲染历史对话列表
renderChatHistory() {
if (!this.chatHistoryList) {
return;
}
this.chatHistoryList.innerHTML = '';
if (this.chatHistories.length === 0) {
return;
}
this.chatHistories.forEach((history, index) => {
const historyItem = document.createElement('div');
historyItem.className = 'history-item';
historyItem.dataset.historyId = history.id;
historyItem.innerHTML = `
${this.escapeHtml(history.title)}
`;
// 点击历史项加载对话
historyItem.addEventListener('click', (e) => {
if (!e.target.closest('.history-item-delete')) {
this.loadChatHistory(history.id);
}
});
// 删除历史对话
const deleteBtn = historyItem.querySelector('.history-item-delete');
deleteBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.deleteChatHistory(history.id);
});
this.chatHistoryList.appendChild(historyItem);
});
}
// 加载历史对话
loadChatHistory(historyId) {
const history = this.chatHistories.find(h => h.id === historyId);
if (!history) {
return;
}
// 如果当前有对话内容,且不是同一个对话,先保存
if (this.currentChatHistory.length > 0 && this.sessionId !== historyId) {
if (this.isCurrentChatFromHistory) {
// 如果当前对话也是从历史记录加载的,更新它
this.updateCurrentChatHistory();
} else {
// 如果当前对话是新对话,保存为新历史
this.saveCurrentChat();
}
}
// 加载历史对话
this.sessionId = history.id;
this.rememberTraceSessionId(this.sessionId);
this.currentChatHistory = [...history.messages];
this.isCurrentChatFromHistory = true; // 标记为从历史记录加载
// 清空并重新渲染消息
if (this.chatMessages) {
this.chatMessages.innerHTML = '';
history.messages.forEach(msg => {
this.addMessage(msg.type, msg.content, false, false); // false表示不是流式,false表示不保存到历史(因为已经存在)
});
}
// 更新UI
this.checkAndSetCentered();
this.renderChatHistory();
}
// 删除历史对话
deleteChatHistory(historyId) {
this.chatHistories = this.chatHistories.filter(h => h.id !== historyId);
this.saveChatHistories();
this.renderChatHistory();
// 如果删除的是当前对话,清空当前对话
if (this.sessionId === historyId) {
this.currentChatHistory = [];
if (this.chatMessages) {
this.chatMessages.innerHTML = '';
}
this.sessionId = this.generateSessionId();
this.updateTraceWorkbenchLink();
this.checkAndSetCentered();
}
}
loadLastTraceSessionId() {
try {
return localStorage.getItem('lastTraceSessionId') || '';
} catch (e) {
return '';
}
}
rememberTraceSessionId(sessionId) {
if (!sessionId) {
return;
}
this.lastTraceSessionId = sessionId;
try {
localStorage.setItem('lastTraceSessionId', sessionId);
} catch (e) {
// localStorage may be unavailable in private or restricted contexts.
}
this.updateTraceWorkbenchLink();
}
updateTraceWorkbenchLink() {
if (!this.traceWorkbenchLink) {
return;
}
const recentHistory = this.chatHistories.find(history => history && history.id);
const sessionId = this.lastTraceSessionId
|| (this.currentChatHistory.length > 0 ? this.sessionId : '')
|| (recentHistory ? recentHistory.id : '');
this.traceWorkbenchLink.href = sessionId
? `trace.html?sessionId=${encodeURIComponent(sessionId)}`
: 'trace.html';
}
// 切换模式下拉菜单
toggleModeDropdown() {
if (this.modeSelectorBtn && this.modeDropdown) {
const wrapper = this.modeSelectorBtn.closest('.mode-selector-wrapper');
if (wrapper) {
wrapper.classList.toggle('active');
}
}
}
// 关闭模式下拉菜单
closeModeDropdown() {
if (this.modeSelectorBtn && this.modeDropdown) {
const wrapper = this.modeSelectorBtn.closest('.mode-selector-wrapper');
if (wrapper) {
wrapper.classList.remove('active');
}
}
}
// 选择模式
selectMode(mode) {
if (this.isStreaming) {
this.showNotification('请等待当前对话完成后再切换模式', 'warning');
return;
}
this.currentMode = mode;
this.updateUI();
const modeNames = {
'quick': '快速',
'stream': '流式'
};
this.showNotification(`已切换到${modeNames[mode]}模式`, 'info');
}
// 更新UI
updateUI() {
// 更新模式选择器显示
if (this.currentModeText) {
const modeNames = {
'quick': '快速',
'stream': '流式'
};
this.currentModeText.textContent = modeNames[this.currentMode] || '快速';
}
// 更新下拉菜单选中状态
const dropdownItems = document.querySelectorAll('.dropdown-item');
dropdownItems.forEach(item => {
const mode = item.getAttribute('data-mode');
if (mode === this.currentMode) {
item.classList.add('active');
} else {
item.classList.remove('active');
}
});
// 更新发送按钮状态
if (this.sendButton) {
this.sendButton.disabled = this.isStreaming;
}
// 更新输入框状态
if (this.messageInput) {
this.messageInput.disabled = this.isStreaming;
this.messageInput.placeholder = '问问智能OnCall助手';
}
}
// 生成随机会话ID
generateSessionId() {
return 'session_' + Math.random().toString(36).substr(2, 9) + '_' + Date.now();
}
// 发送消息
async sendMessage() {
let message = '';
if (this.messageInput) {
message = this.messageInput.value.trim();
}
if (!message) {
this.showNotification('请输入消息内容', 'warning');
return;
}
if (this.isStreaming) {
this.showNotification('请等待当前对话完成', 'warning');
return;
}
// 显示用户消息
this.addMessage('user', message);
// 清空输入框
if (this.messageInput) {
this.messageInput.value = '';
}
// 设置发送状态
this.isStreaming = true;
this.updateUI();
try {
if (this.currentMode === 'quick') {
await this.sendQuickMessage(message);
} else if (this.currentMode === 'stream') {
await this.sendStreamMessage(message);
}
} catch (error) {
console.error('发送消息失败:', error);
this.addMessage('assistant', '抱歉,发送消息时出现错误:' + error.message);
} finally {
this.isStreaming = false;
this.updateUI();
// 如果当前对话是从历史记录加载的,更新历史记录
if (this.isCurrentChatFromHistory && this.currentChatHistory.length > 0) {
this.updateCurrentChatHistory();
this.renderChatHistory(); // 更新历史对话列表显示
}
}
}
// 发送快速消息(普通对话)
async sendQuickMessage(message) {
// 添加等待提示消息
const loadingMessage = this.addLoadingMessage('正在思考...');
try {
const response = await fetch(`${this.apiBaseUrl}/chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
Id: this.sessionId,
Question: message
})
});
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`);
}
const data = await response.json();
console.log('[sendQuickMessage] 响应数据:', JSON.stringify(data));
// 移除等待提示消息
if (loadingMessage && loadingMessage.parentNode) {
loadingMessage.parentNode.removeChild(loadingMessage);
}
// 统一响应格式:检查 data.code 或 data.message 判断请求是否成功
if (data.code === 200 || data.message === 'success') {
// data.data 是 ChatResponse 对象
const chatResponse = data.data;
if (chatResponse && chatResponse.success) {
// 保存后端返回的 sessionId,用于 feedback 提交
if (chatResponse.sessionId) {
this.lastSessionId = chatResponse.sessionId;
this.rememberTraceSessionId(chatResponse.sessionId);
}
// 成功:添加实际响应消息(即使 answer 为空也显示)
const answer = chatResponse.answer || '(无回复内容)';
this.addMessage('assistant', answer);
} else if (chatResponse && chatResponse.errorMessage) {
// 业务错误
throw new Error(chatResponse.errorMessage);
} else {
// 兜底:尝试显示任何可用内容
const fallbackAnswer = chatResponse?.answer || chatResponse?.errorMessage || '服务返回了空内容';
this.addMessage('assistant', fallbackAnswer);
}
} else {
// HTTP 成功但业务失败
throw new Error(data.message || '请求失败');
}
} catch (error) {
// 出错时也要移除等待提示消息
if (loadingMessage && loadingMessage.parentNode) {
loadingMessage.parentNode.removeChild(loadingMessage);
}
throw error;
}
}
// 发送流式消息
async sendStreamMessage(message) {
try {
const response = await fetch(`${this.apiBaseUrl}/chat_stream`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
Id: this.sessionId,
Question: message
})
});
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`);
}
// 创建助手消息元素
const assistantMessageElement = this.addMessage('assistant', '', true);
let fullResponse = '';
// 处理流式响应
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let currentEvent = '';
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
// 流结束,使用统一的处理方法
this.handleStreamComplete(assistantMessageElement, fullResponse);
break;
}
// 解码数据并添加到缓冲区
buffer += decoder.decode(value, { stream: true });
// 按行分割处理
const lines = buffer.split('\n');
// 保留最后一行(可能不完整)
buffer = lines.pop() || '';
for (const line of lines) {
if (line.trim() === '') continue;
console.log('[SSE调试] 收到行:', line);
// 解析SSE格式
if (line.startsWith('id:')) {
console.log('[SSE调试] 解析到ID');
continue;
} else if (line.startsWith('event:')) {
// 兼容 "event:message" 和 "event: message" 两种格式
currentEvent = line.substring(6).trim();
console.log('[SSE调试] 解析到事件类型:', currentEvent);
// 注意:后端统一使用 "message" 事件名,真正的类型在 data 的 JSON 中
continue;
} else if (line.startsWith('data:')) {
// 兼容 "data:xxx" 和 "data: xxx" 两种格式
const rawData = line.substring(5).trim();
console.log('[SSE调试] 解析到数据, currentEvent:', currentEvent, ', rawData:', rawData);
// 兼容旧格式 [DONE] 标记
if (rawData === '[DONE]') {
// 流结束标记,将内容转换为Markdown渲染
this.handleStreamComplete(assistantMessageElement, fullResponse);
return;
}
// 处理 SSE 数据
try {
// 尝试解析为 SseMessage 格式的 JSON
const sseMessage = JSON.parse(rawData);
console.log('[SSE调试] 解析JSON成功:', sseMessage);
if (sseMessage && typeof sseMessage.type === 'string') {
if (sseMessage.type === 'content') {
const content = sseMessage.data || '';
fullResponse += content;
console.log('[SSE调试] 添加内容:', content);
// 实时渲染 Markdown
if (assistantMessageElement) {
const messageContent = assistantMessageElement.querySelector('.message-content');
messageContent.innerHTML = this.renderMarkdown(fullResponse);
// 高亮代码块
this.highlightCodeBlocks(messageContent);
this.scrollToBottom();
}
} else if (sseMessage.type === 'done') {
console.log('[SSE调试] 收到done标记,流结束');
this.handleStreamComplete(assistantMessageElement, fullResponse);
return;
} else if (sseMessage.type === 'error') {
console.error('[SSE调试] 收到错误:', sseMessage.data);
if (assistantMessageElement) {
const messageContent = assistantMessageElement.querySelector('.message-content');
messageContent.innerHTML = this.renderMarkdown('错误: ' + (sseMessage.data || '未知错误'));
}
return;
}
} else {
// 不是标准 SseMessage 格式,尝试兼容处理
console.log('[SSE调试] 非标准格式,尝试兼容处理');
fullResponse += rawData;
if (assistantMessageElement) {
const messageContent = assistantMessageElement.querySelector('.message-content');
messageContent.innerHTML = this.renderMarkdown(fullResponse);
this.highlightCodeBlocks(messageContent);
this.scrollToBottom();
}
}
} catch (e) {
// JSON 解析失败,尝试兼容旧格式
console.log('[SSE调试] JSON解析失败,使用兼容模式:', e.message);
if (rawData === '') {
fullResponse += '\n';
} else {
fullResponse += rawData;
}
if (assistantMessageElement) {
const messageContent = assistantMessageElement.querySelector('.message-content');
messageContent.innerHTML = this.renderMarkdown(fullResponse);
this.highlightCodeBlocks(messageContent);
this.scrollToBottom();
}
}
}
}
}
} finally {
reader.releaseLock();
}
} catch (error) {
throw error;
}
}
// 添加消息到聊天界面
addMessage(type, content, isStreaming = false, saveToHistory = true) {
// 检查是否是第一条消息,如果是则移除居中样式
const isFirstMessage = this.chatMessages && this.chatMessages.querySelectorAll('.message').length === 0;
// 保存消息到当前对话历史(如果不是流式消息且需要保存)
if (!isStreaming && saveToHistory && content) {
this.currentChatHistory.push({
type: type,
content: content,
timestamp: new Date().toISOString()
});
}
const messageDiv = document.createElement('div');
messageDiv.className = `message ${type}${isStreaming ? ' streaming' : ''}`;
// 如果是assistant消息,添加头像图标
if (type === 'assistant') {
const messageAvatar = document.createElement('div');
messageAvatar.className = 'message-avatar';
messageAvatar.innerHTML = `
`;
messageDiv.appendChild(messageAvatar);
}
// 创建消息内容包装器
const messageContentWrapper = document.createElement('div');
messageContentWrapper.className = 'message-content-wrapper';
const messageContent = document.createElement('div');
messageContent.className = 'message-content';
// 如果是assistant消息且不是流式消息,使用Markdown渲染
if (type === 'assistant' && !isStreaming) {
messageContent.innerHTML = this.renderMarkdown(content);
// 高亮代码块
this.highlightCodeBlocks(messageContent);
} else {
// 用户消息或流式消息使用纯文本
messageContent.textContent = content;
}
messageContentWrapper.appendChild(messageContent);
// assistant 消息末尾加反馈栏(流式消息完成后由 handleStreamComplete 添加)
if (type === 'assistant' && !isStreaming) {
messageContentWrapper.appendChild(this.createFeedbackBar(this.lastSessionId));
}
messageDiv.appendChild(messageContentWrapper);
if (this.chatMessages) {
this.chatMessages.appendChild(messageDiv);
// 如果是第一条消息,移除居中样式并添加动画
if (isFirstMessage && this.chatContainer) {
this.chatContainer.classList.remove('centered');
// 添加动画类
this.chatContainer.style.transition = 'all 0.5s ease';
}
this.scrollToBottom();
}
return messageDiv;
}
// 创建反馈栏,sessionId 闭包绑定,避免多轮对话时错位
createFeedbackBar(sessionId) {
const bar = document.createElement('div');
bar.className = 'feedback-bar';
const usefulBtn = document.createElement('button');
usefulBtn.className = 'feedback-btn';
usefulBtn.title = '有用';
usefulBtn.innerHTML = ``;
const notUsefulBtn = document.createElement('button');
notUsefulBtn.className = 'feedback-btn';
notUsefulBtn.title = '无用';
notUsefulBtn.innerHTML = ``;
usefulBtn.addEventListener('click', () => this.submitFeedback('useful', bar, sessionId));
notUsefulBtn.addEventListener('click', () => this.submitFeedback('not_useful', bar, sessionId));
bar.appendChild(usefulBtn);
bar.appendChild(notUsefulBtn);
return bar;
}
// 提交反馈
async submitFeedback(feedback, barElement, sessionId) {
if (!sessionId) return;
barElement.querySelectorAll('.feedback-btn').forEach(btn => btn.disabled = true);
try {
const response = await fetch(`${this.apiBaseUrl}/feedback`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionId, feedback })
});
const data = await response.json();
if (data.success) {
barElement.innerHTML = `${feedback === 'useful' ? '已标记为有用' : '已标记为无用'}`;
} else {
barElement.querySelectorAll('.feedback-btn').forEach(btn => btn.disabled = false);
this.showNotification('反馈提交失败', 'error');
}
} catch (e) {
barElement.querySelectorAll('.feedback-btn').forEach(btn => btn.disabled = false);
this.showNotification('反馈提交失败: ' + e.message, 'error');
}
}
// 添加带加载动画的消息
addLoadingMessage(content) {
const messageDiv = document.createElement('div');
messageDiv.className = 'message assistant';
// 添加头像图标
const messageAvatar = document.createElement('div');
messageAvatar.className = 'message-avatar';
messageAvatar.innerHTML = `
`;
messageDiv.appendChild(messageAvatar);
// 创建消息内容包装器
const messageContentWrapper = document.createElement('div');
messageContentWrapper.className = 'message-content-wrapper';
const messageContent = document.createElement('div');
messageContent.className = 'message-content loading-message-content';
// 创建文本和动画容器
const textSpan = document.createElement('span');
textSpan.textContent = content;
// 创建旋转动画图标
const loadingIcon = document.createElement('span');
loadingIcon.className = 'loading-spinner-icon';
loadingIcon.innerHTML = `
`;
messageContent.appendChild(textSpan);
messageContent.appendChild(loadingIcon);
messageContentWrapper.appendChild(messageContent);
messageDiv.appendChild(messageContentWrapper);
if (this.chatMessages) {
this.chatMessages.appendChild(messageDiv);
// 如果是第一条消息,移除居中样式
const isFirstMessage = this.chatMessages.querySelectorAll('.message').length === 1;
if (isFirstMessage && this.chatContainer) {
this.chatContainer.classList.remove('centered');
this.chatContainer.style.transition = 'all 0.5s ease';
}
this.scrollToBottom();
}
return messageDiv;
}
// 检查并设置居中样式
checkAndSetCentered() {
if (this.chatMessages && this.chatContainer) {
const hasMessages = this.chatMessages.querySelectorAll('.message').length > 0;
if (!hasMessages) {
this.chatContainer.classList.add('centered');
} else {
this.chatContainer.classList.remove('centered');
}
}
}
// 滚动到底部
scrollToBottom() {
if (this.chatMessages) {
this.chatMessages.scrollTop = this.chatMessages.scrollHeight;
}
}
// 处理流式传输完成
handleStreamComplete(assistantMessageElement, fullResponse) {
if (assistantMessageElement) {
assistantMessageElement.classList.remove('streaming');
const messageContentWrapper = assistantMessageElement.querySelector('.message-content-wrapper');
const messageContent = assistantMessageElement.querySelector('.message-content');
if (messageContent) {
messageContent.innerHTML = this.renderMarkdown(fullResponse);
// 高亮代码块
this.highlightCodeBlocks(messageContent);
}
// 流式完成后追加反馈栏
if (messageContentWrapper && !messageContentWrapper.querySelector('.feedback-bar')) {
messageContentWrapper.appendChild(this.createFeedbackBar(this.lastSessionId));
}
}
// 保存流式消息到历史记录
if (fullResponse) {
this.currentChatHistory.push({
type: 'assistant',
content: fullResponse,
timestamp: new Date().toISOString()
});
// 如果当前对话是从历史记录加载的,更新历史记录
if (this.isCurrentChatFromHistory) {
this.updateCurrentChatHistory();
this.renderChatHistory();
}
}
}
// 显示通知
showNotification(message, type = 'info') {
// 创建通知元素
const notification = document.createElement('div');
notification.className = `notification ${type}`;
notification.textContent = message;
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 15px 20px;
border-radius: 8px;
color: white;
font-weight: 500;
z-index: 10000;
animation: slideIn 0.3s ease;
max-width: 300px;
`;
// 根据类型设置颜色(Google Material Design配色)
const colors = {
info: '#1a73e8',
success: '#34a853',
warning: '#fbbc04',
error: '#ea4335'
};
notification.style.backgroundColor = colors[type] || colors.info;
// 添加到页面
document.body.appendChild(notification);
// 3秒后自动移除
setTimeout(() => {
notification.style.animation = 'slideOut 0.3s ease';
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 300);
}, 3000);
}
// 处理文件选择
handleFileSelect(event) {
const file = event.target.files[0];
if (file) {
// 验证文件格式
if (!this.validateFileType(file)) {
this.showNotification('只支持上传 TXT 或 Markdown (.md) 格式的文件', 'error');
this.fileInput.value = '';
return;
}
this.uploadFile(file);
}
}
// 验证文件类型
validateFileType(file) {
const fileName = file.name.toLowerCase();
const allowedExtensions = ['.txt', '.md', '.markdown'];
return allowedExtensions.some(ext => fileName.endsWith(ext));
}
// 上传文件到知识库
async uploadFile(file) {
// 再次验证文件类型(双重保险)
if (!this.validateFileType(file)) {
this.showNotification('只支持上传 TXT 或 Markdown (.md) 格式的文件', 'error');
return;
}
// 验证文件大小(限制为50MB)
const maxSize = 50 * 1024 * 1024;
if (file.size > maxSize) {
this.showNotification('文件大小不能超过50MB', 'error');
return;
}
// 锁定前端并显示上传遮罩层
this.isStreaming = true;
this.updateUI();
this.showUploadOverlay(true, file.name);
try {
// 创建 FormData
const formData = new FormData();
formData.append('file', file);
// 发送上传请求
const response = await fetch(`${this.apiBaseUrl}/upload`, {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`);
}
const data = await response.json();
if ((data.code === 200 || data.message === 'success') && data.data) {
// 在聊天界面显示上传成功消息
const successMessage = `${file.name} 上传到知识库成功`;
this.addMessage('assistant', successMessage, false, true);
} else {
throw new Error(data.message || '上传失败');
}
} catch (error) {
console.error('文件上传失败:', error);
this.showNotification('文件上传失败: ' + error.message, 'error');
} finally {
// 清空文件输入
if (this.fileInput) {
this.fileInput.value = '';
}
// 解锁前端
this.isStreaming = false;
this.showUploadOverlay(false);
this.updateUI();
}
}
// 格式化文件大小
formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
}
// 发送智能运维请求(SSE 流式模式)
async sendAIOpsRequest(loadingMessageElement) {
try {
const response = await fetch(`${this.apiBaseUrl}/ai_ops`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
});
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`);
}
let fullResponse = '';
// 处理 SSE 流式响应
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let currentEvent = 'message'; // 默认事件类型为 message
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
// 流结束,更新最终内容
if (fullResponse) {
console.log('AI Ops 流结束,更新最终内容,长度:', fullResponse.length);
this.updateAIOpsMessage(loadingMessageElement, fullResponse, []);
}
break;
}
// 解码数据并添加到缓冲区
buffer += decoder.decode(value, { stream: true });
// 按行分割处理
const lines = buffer.split('\n');
// 保留最后一行(可能不完整)
buffer = lines.pop() || '';
for (const line of lines) {
if (line.trim() === '') continue;
console.log('[AI Ops SSE] 收到行:', line);
// 解析 SSE 格式
if (line.startsWith('id:')) {
continue;
} else if (line.startsWith('event:')) {
currentEvent = line.substring(6).trim();
console.log('[AI Ops SSE] 事件类型:', currentEvent);
continue;
} else if (line.startsWith('data:')) {
const rawData = line.substring(5).trim();
console.log('[AI Ops SSE] 数据:', rawData, ', currentEvent:', currentEvent);
// 解析可能包含多个JSON对象的数据
const processJsonMessages = (data) => {
const jsonPattern = /\{"type"\s*:\s*"[^"]+"\s*,\s*"data"\s*:\s*(?:"[^"]*"|null)\}/g;
const matches = data.match(jsonPattern);
if (matches && matches.length > 0) {
console.log('[AI Ops SSE] 匹配到', matches.length, '个JSON对象');
for (const jsonStr of matches) {
try {
const sseMessage = JSON.parse(jsonStr);
if (sseMessage.type === 'session') {
this.lastSessionId = sseMessage.data;
this.rememberTraceSessionId(sseMessage.data);
} else if (sseMessage.type === 'content') {
fullResponse += sseMessage.data || '';
} else if (sseMessage.type === 'done') {
console.log('AI Ops 流完成,最终内容长度:', fullResponse.length);
this.updateAIOpsMessage(loadingMessageElement, fullResponse, []);
return true;
} else if (sseMessage.type === 'error') {
throw new Error(sseMessage.data || '智能运维分析失败');
}
} catch (e) {
if (e.message.includes('智能运维')) throw e;
console.log('[AI Ops SSE] 单个JSON解析失败:', jsonStr);
}
}
if (loadingMessageElement) {
this.updateAIOpsStreamContent(loadingMessageElement, fullResponse);
}
return false;
}
return null;
};
const result = processJsonMessages(rawData);
if (result === true) {
return; // 流结束
} else if (result === null) {
// 没有匹配到多个JSON,尝试单个JSON解析
try {
const sseMessage = JSON.parse(rawData);
if (sseMessage && sseMessage.type) {
if (sseMessage.type === 'session') {
this.lastSessionId = sseMessage.data;
this.rememberTraceSessionId(sseMessage.data);
} else if (sseMessage.type === 'content') {
fullResponse += sseMessage.data || '';
if (loadingMessageElement) {
this.updateAIOpsStreamContent(loadingMessageElement, fullResponse);
}
} else if (sseMessage.type === 'done') {
console.log('AI Ops 流完成,最终内容长度:', fullResponse.length);
this.updateAIOpsMessage(loadingMessageElement, fullResponse, []);
return;
} else if (sseMessage.type === 'error') {
throw new Error(sseMessage.data || '智能运维分析失败');
}
} else {
fullResponse += rawData;
if (loadingMessageElement) {
this.updateAIOpsStreamContent(loadingMessageElement, fullResponse);
}
}
} catch (e) {
if (e.message.includes('智能运维')) throw e;
// 非 JSON 格式,直接追加原始数据
fullResponse += rawData;
if (loadingMessageElement) {
this.updateAIOpsStreamContent(loadingMessageElement, fullResponse);
}
}
}
}
}
}
} finally {
reader.releaseLock();
}
} catch (error) {
throw error;
}
}
// 更新智能运维流式内容(实时显示)
updateAIOpsStreamContent(messageElement, content) {
if (!messageElement) return;
// 添加 aiops-message 类
messageElement.classList.add('aiops-message');
const messageContentWrapper = messageElement.querySelector('.message-content-wrapper');
if (messageContentWrapper) {
let messageContent = messageContentWrapper.querySelector('.message-content');
if (!messageContent) {
messageContent = document.createElement('div');
messageContent.className = 'message-content';
messageContentWrapper.appendChild(messageContent);
}
// 流式显示时使用纯文本
messageContent.textContent = content;
this.scrollToBottom();
}
}
// 更新智能运维消息(带折叠详情)
updateAIOpsMessage(messageElement, response, details) {
console.log('updateAIOpsMessage 被调用');
console.log('messageElement:', messageElement);
console.log('response:', response);
console.log('response length:', response ? response.length : 0);
console.log('details:', details);
if (!messageElement) {
// 如果没有传入消息元素,则创建新消息
console.log('messageElement 为空,创建新消息');
return this.addAIOpsMessage(response, details);
}
// 添加aiops-message类
messageElement.classList.add('aiops-message');
// 获取消息内容包装器
const messageContentWrapper = messageElement.querySelector('.message-content-wrapper');
if (!messageContentWrapper) {
console.error('未找到 message-content-wrapper');
return;
}
// 清空现有内容(保留消息内容容器)
const messageContent = messageContentWrapper.querySelector('.message-content');
if (!messageContent) {
console.error('未找到 message-content');
return;
}
// 移除加载动画相关的类和内容
messageContent.classList.remove('loading-message-content');
messageContent.textContent = '';
// 移除加载图标(如果存在)
const loadingIcon = messageContent.querySelector('.loading-spinner-icon');
if (loadingIcon) {
loadingIcon.remove();
}
// 详情部分(可折叠)- 先显示
if (details && details.length > 0) {
// 检查是否已存在详情容器
let detailsContainer = messageElement.querySelector('.aiops-details');
if (!detailsContainer) {
detailsContainer = document.createElement('div');
detailsContainer.className = 'aiops-details';
messageContentWrapper.insertBefore(detailsContainer, messageContent);
} else {
// 清空现有详情
detailsContainer.innerHTML = '';
}
const detailsToggle = document.createElement('div');
detailsToggle.className = 'details-toggle';
detailsToggle.innerHTML = `
查看详细步骤 (${details.length}条)
`;
const detailsContent = document.createElement('div');
detailsContent.className = 'details-content';
details.forEach((detail, index) => {
const detailItem = document.createElement('div');
detailItem.className = 'detail-item';
detailItem.innerHTML = `步骤 ${index + 1}: ${this.escapeHtml(detail)}`;
detailsContent.appendChild(detailItem);
});
// 点击切换折叠状态
detailsToggle.addEventListener('click', () => {
detailsContent.classList.toggle('expanded');
detailsToggle.classList.toggle('expanded');
});
detailsContainer.appendChild(detailsToggle);
detailsContainer.appendChild(detailsContent);
}
// 更新主要响应内容(使用Markdown渲染)
console.log('开始渲染 Markdown');
const renderedHtml = this.renderMarkdown(response);
console.log('Markdown 渲染完成,HTML 长度:', renderedHtml ? renderedHtml.length : 0);
messageContent.innerHTML = renderedHtml;
console.log('innerHTML 已设置');
// 高亮代码块
this.highlightCodeBlocks(messageContent);
console.log('代码块高亮完成');
// 保存到历史记录
this.currentChatHistory.push({
type: 'assistant',
content: response,
timestamp: new Date().toISOString()
});
this.scrollToBottom();
return messageElement;
}
// 添加智能运维消息(带折叠详情)- 保留用于兼容性
addAIOpsMessage(response, details) {
const messageDiv = document.createElement('div');
messageDiv.className = 'message assistant aiops-message';
// 添加头像图标
const messageAvatar = document.createElement('div');
messageAvatar.className = 'message-avatar';
messageAvatar.innerHTML = `
`;
messageDiv.appendChild(messageAvatar);
// 创建消息内容包装器
const messageContentWrapper = document.createElement('div');
messageContentWrapper.className = 'message-content-wrapper';
// 详情部分(可折叠)- 先显示
if (details && details.length > 0) {
const detailsContainer = document.createElement('div');
detailsContainer.className = 'aiops-details';
const detailsToggle = document.createElement('div');
detailsToggle.className = 'details-toggle';
detailsToggle.innerHTML = `
查看详细步骤 (${details.length}条)
`;
const detailsContent = document.createElement('div');
detailsContent.className = 'details-content';
details.forEach((detail, index) => {
const detailItem = document.createElement('div');
detailItem.className = 'detail-item';
detailItem.innerHTML = `步骤 ${index + 1}: ${this.escapeHtml(detail)}`;
detailsContent.appendChild(detailItem);
});
// 点击切换折叠状态
detailsToggle.addEventListener('click', () => {
detailsContent.classList.toggle('expanded');
detailsToggle.classList.toggle('expanded');
});
detailsContainer.appendChild(detailsToggle);
detailsContainer.appendChild(detailsContent);
messageContentWrapper.appendChild(detailsContainer);
}
// 主要响应内容 - 后显示(使用Markdown渲染)
const messageContent = document.createElement('div');
messageContent.className = 'message-content';
messageContent.innerHTML = this.renderMarkdown(response);
// 高亮代码块
this.highlightCodeBlocks(messageContent);
messageContentWrapper.appendChild(messageContent);
messageDiv.appendChild(messageContentWrapper);
if (this.chatMessages) {
this.chatMessages.appendChild(messageDiv);
this.scrollToBottom();
}
return messageDiv;
}
// HTML转义
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 触发智能运维(点击智能运维按钮时直接调用)
async triggerAIOps() {
if (this.isStreaming) {
this.showNotification('请等待当前操作完成', 'warning');
return;
}
// 新建对话
this.newChat();
// 添加"分析中..."的消息(带旋转动画)
const loadingMessage = this.addLoadingMessage('分析中...');
this.currentAIOpsMessage = loadingMessage; // 保存消息引用用于后续更新
// 设置发送状态
this.isStreaming = true;
this.updateUI();
try {
await this.sendAIOpsRequest(loadingMessage);
} catch (error) {
console.error('智能运维分析失败:', error);
// 更新消息为错误信息
if (loadingMessage) {
const messageContent = loadingMessage.querySelector('.message-content');
if (messageContent) {
messageContent.textContent = '抱歉,智能运维分析时出现错误:' + error.message;
}
}
} finally {
this.isStreaming = false;
this.currentAIOpsMessage = null;
this.updateUI();
}
}
// 显示/隐藏加载遮罩层
showLoadingOverlay(show) {
if (this.loadingOverlay) {
if (show) {
this.loadingOverlay.style.display = 'flex';
// 更新文字为智能运维
const loadingText = this.loadingOverlay.querySelector('.loading-text');
const loadingSubtext = this.loadingOverlay.querySelector('.loading-subtext');
if (loadingText) loadingText.textContent = '智能运维分析中,请稍候...';
if (loadingSubtext) loadingSubtext.textContent = '后端正在处理,请耐心等待';
// 防止页面滚动
document.body.style.overflow = 'hidden';
} else {
this.loadingOverlay.style.display = 'none';
// 恢复页面滚动
document.body.style.overflow = '';
}
}
}
// 显示/隐藏上传遮罩层
showUploadOverlay(show, fileName = '') {
if (this.loadingOverlay) {
if (show) {
this.loadingOverlay.style.display = 'flex';
// 更新文字为上传中
const loadingText = this.loadingOverlay.querySelector('.loading-text');
const loadingSubtext = this.loadingOverlay.querySelector('.loading-subtext');
if (loadingText) loadingText.textContent = '正在上传文件...';
if (loadingSubtext) loadingSubtext.textContent = fileName ? `上传: ${fileName}` : '请稍候';
// 防止页面滚动
document.body.style.overflow = 'hidden';
} else {
this.loadingOverlay.style.display = 'none';
// 恢复页面滚动
document.body.style.overflow = '';
}
}
}
}
// 添加CSS动画
const style = document.createElement('style');
style.textContent = `
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(100%);
opacity: 0;
}
}
`;
document.head.appendChild(style);
// 初始化应用
document.addEventListener('DOMContentLoaded', () => {
new SuperBizAgentApp();
});