document.addEventListener('DOMContentLoaded', () => { // 注意:认证状态检查由 index.html 中的 checkAuthStatus 函数处理 // 这里不再重复检查,避免冲突 // 防重复重定向标志 let isRedirecting = false; // 主题检测和适配功能 function initThemeDetection () { // 检测当前主题偏好 const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; // 为body添加主题类名,便于CSS选择器 if (prefersDark) { document.body.classList.add('dark-theme'); document.body.classList.remove('light-theme'); } else { document.body.classList.add('light-theme'); document.body.classList.remove('dark-theme'); } // 监听主题变化 const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', (e) => { if (e.matches) { // 切换到深色主题 document.body.classList.add('dark-theme'); document.body.classList.remove('light-theme'); } else { // 切换到浅色主题 document.body.classList.add('light-theme'); document.body.classList.remove('dark-theme'); } // 触发自定义事件,通知其他组件主题已变化 window.dispatchEvent(new CustomEvent('themeChanged', { detail: { isDark: e.matches } })); }); // 添加平滑过渡效果 const style = document.createElement('style'); style.textContent = ` * { transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease !important; } /* 确保过渡效果不会影响动画 */ .typing-indicator span, .loading-spinner, .ripple, .message { transition: none !important; } `; document.head.appendChild(style); } // 初始化主题检测 initThemeDetection(); // 获取DOM元素 const chatMessages = document.getElementById('chat-messages'); const userInput = document.getElementById('user-input'); const sendButton = document.getElementById('send-btn'); const resetButton = document.getElementById('reset-btn'); const logoutButton = document.getElementById('logout-btn'); // 初始化复制按钮 if (typeof window.setupCopyButtons === 'function') { window.setupCopyButtons(); } // 绑定退出登录事件 if (logoutButton) { logoutButton.addEventListener('click', handleLogout); } // 添加字符计数器样式 const style = document.createElement('style'); style.textContent = ` .char-counter { position: absolute; bottom: 8px; right: 10px; font-size: 12px; color: #999; opacity: 0.8; transition: all 0.3s ease; display: flex; align-items: center; background: rgba(255, 255, 255, 0.9); padding: 2px 6px; border-radius: 4px; pointer-events: none; z-index: 1; } .char-counter.warning { color: #ff9800; font-weight: 500; background: rgba(255, 152, 0, 0.1); } .char-counter.exceeded { color: #f44336; font-weight: 500; background: rgba(244, 67, 54, 0.1); animation: pulse 1s infinite; } @keyframes pulse { 0% { transform: scale(1); } 50% { transform: scale(1.05); } 100% { transform: scale(1); } } .error-message { background-color: #ff5252; color: white; padding: 10px 15px; border-radius: 4px; margin: 10px auto; max-width: 80%; text-align: center; box-shadow: 0 2px 4px rgba(0,0,0,0.2); position: relative; transition: opacity 0.3s ease, transform 0.3s ease; } /* AI思考过程样式 */ .thought-container { background-color: #f3f0fa; border-left: 3px solid #7952b3; padding: 10px 15px; margin: 10px 0 10px 0; border-radius: 0 4px 4px 0; font-style: italic; color: #7952b3; position: relative; font-size: 0.98em; } .thought-container::before { content: "思考过程"; display: block; font-size: 0.75rem; color: #7952b3; font-weight: 600; margin-bottom: 4px; font-style: normal; } .thought-toggle-btn { position: absolute; top: 8px; right: 12px; background: none; border: none; color: #7952b3; font-size: 1.1em; cursor: pointer; z-index: 2; padding: 2px 6px; border-radius: 3px; transition: background 0.2s; } .thought-toggle-btn:hover { background: #e5e1f6; } .thought-content { overflow-y: auto; max-height: 500px; transition: max-height 0.35s cubic-bezier(.4,0,.2,1), padding 0.3s; will-change: max-height; /* 自定义滚动条样式 */ scrollbar-width: thin; scrollbar-color: #d1c4e9 #f3f0fa; } /* Webkit浏览器的滚动条样式 */ .thought-content::-webkit-scrollbar { width: 8px; } .thought-content::-webkit-scrollbar-track { background: #f3f0fa; border-radius: 4px; } .thought-content::-webkit-scrollbar-thumb { background: linear-gradient(135deg, #d1c4e9, #b39ddb); border-radius: 4px; border: 1px solid #f3f0fa; transition: background 0.2s ease; } .thought-content::-webkit-scrollbar-thumb:hover { background: linear-gradient(135deg, #b39ddb, #9575cd); } .thought-content::-webkit-scrollbar-thumb:active { background: linear-gradient(135deg, #9575cd, #7e57c2); } .thought-container.collapsed .thought-content { max-height: 0; padding: 0 !important; } .thought-container.collapsed::before { content: "思考过程(已收起)"; } .thought-container p { margin: 0 0 6px 0; font-style: italic; color: #7952b3; background: none; border-bottom: 1px dashed #d1c4e9; padding-bottom: 2px; } .thought-container p:last-child { border-bottom: none; } /* 打字机闪烁光标 */ .cursor-blink { display: inline-block; color: #666; font-weight: bold; margin-left: 1px; animation: blink 1s step-end infinite; } @keyframes blink { 50% { opacity: 0; } } #send-btn:disabled { opacity: 0.6; cursor: not-allowed; transform: scale(0.98); } #send-btn:disabled:hover { background-color: #007bff; } /* 输入框Loading样式 */ .input-loading { background-image: linear-gradient(90deg, rgba(239, 242, 255, 0.5), rgba(255, 255, 255, 0.8), rgba(239, 242, 255, 0.5)); background-size: 200% 100%; animation: input-loading 1.5s infinite; border-color: #6366f1 !important; box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.15); } @keyframes input-loading { 0% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } } /* 消息气泡动画效果 */ .message-content.typing { position: relative; overflow: hidden; } .message-content.typing::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; border-radius: inherit; border: 2px solid; border-color: #6366f1; box-shadow: 0 0 5px rgba(99, 102, 241, 0.5); animation: border-pulse 0.8s ease-in-out infinite; pointer-events: none; } @keyframes border-pulse { 0% { opacity: 0.4; border-color: #6366f1; } 50% { opacity: 1; border-color: #818cf8; } 100% { opacity: 0.4; border-color: #6366f1; } } /* 对话切换状态样式 */ .conversation-item.streaming { position: relative; } .conversation-item.streaming::after { content: ''; position: absolute; top: 8px; right: 8px; width: 8px; height: 8px; background: #10b981; border-radius: 50%; animation: pulse-dot 2s infinite; } .conversation-item.paused { position: relative; } .conversation-item.paused::after { content: ''; position: absolute; top: 8px; right: 8px; width: 8px; height: 8px; background: #f59e0b; border-radius: 50%; animation: pulse-dot 2s infinite; } @keyframes pulse-dot { 0% { opacity: 1; transform: scale(1); } 50% { opacity: 0.5; transform: scale(1.2); } 100% { opacity: 1; transform: scale(1); } } .paused-tip { background: rgba(245, 158, 11, 0.1); border: 1px solid rgba(245, 158, 11, 0.3); border-radius: 4px; padding: 4px 8px; font-size: 0.85em; color: #f59e0b; margin-top: 8px; font-style: italic; text-align: center; } `; document.head.appendChild(style); // 状态变量 - 确保用户ID始终存在 let userId = localStorage.getItem('chat_user_id'); if (!userId) { userId = generateUUID(); localStorage.setItem('chat_user_id', userId); } // 新增:用于流式输出中断的标志 let isStopped = false; let isWaitingForResponse = false; let currentBotMessageElement = null; let currentBotMessageContainer = null; // 添加消息容器引用 let useStreamingMode = true; // 始终启用流式响应 let currentLoadingElement = null; let markdownEnabled = true; // 启用 Markdown 渲染 // 统一的Markdown渲染函数 function renderMarkdown (text) { if (!markdownEnabled || typeof showdown === 'undefined') { return text; } const converter = new showdown.Converter({ strikethrough: true, tables: true, tasklists: true, ghCodeBlocks: true, openLinksInNewWindow: true, emoji: true, underline: true, simplifiedAutoLink: true, excludeTrailingPunctuationFromURLs: true, literalMidWordUnderscores: true, tablesHeaderId: true, smoothLivePreview: true, smartIndentationFix: true, disableForced4SpacesIndentedSublists: true, simpleLineBreaks: true, requireSpaceBeforeHeadingText: true, ghCompatibleHeaderId: true, ghMentions: true, ghMentionsLink: 'https://github.com/{mention}', encodeEmails: true, backslashEscapesHTMLTags: true, omitExtraWLInCodeBlocks: true, noHeaderId: false, prefixHeaderId: false, rawPrefixHeaderId: false, rawHeaderId: false, headerLevelStart: 1, parseImgDimensions: true }); return converter.makeHtml(text); } // 思考过程专用的渲染函数 function renderThoughts (text) { if (!markdownEnabled || typeof showdown === 'undefined') { return text; } const converter = new showdown.Converter({ strikethrough: true, tables: true, tasklists: true, ghCodeBlocks: true, openLinksInNewWindow: true, emoji: true, underline: true, simplifiedAutoLink: true, excludeTrailingPunctuationFromURLs: true, literalMidWordUnderscores: true, tablesHeaderId: true, smoothLivePreview: true, smartIndentationFix: true, disableForced4SpacesIndentedSublists: true, simpleLineBreaks: false, // 禁用简单换行,保持段落结构 requireSpaceBeforeHeadingText: true, ghCompatibleHeaderId: true, ghMentions: true, ghMentionsLink: 'https://github.com/{mention}', encodeEmails: true, backslashEscapesHTMLTags: true, omitExtraWLInCodeBlocks: true, noHeaderId: false, prefixHeaderId: false, rawPrefixHeaderId: false, rawHeaderId: false, headerLevelStart: 1, parseImgDimensions: true }); return converter.makeHtml(text); } let renderDebounceTimer = null; // 用于防抖动渲染 let maxMessageLength = 20000; // 设置最大消息长度与后端POST限制一致 let eventSource = null; // 存储EventSource实例,用于停止请求 let abortController = null; // 用于中止fetch请求 // 全局对话状态管理 - 支持多对话并发流式响应 const conversationStates = new Map(); // 存储每个对话的状态 let currentActiveConversationId = null; // 当前激活的对话ID let conversationStreams = new Map(); // 存储每个对话的流式连接 // 用于控制自动滚动的变量 let shouldAutoScroll = true; let lastScrollTop = 0; let scrollThreshold = 50; // 距离底部多少像素内触发自动滚动 // 对话状态管理函数 function initializeConversationState (conversationId) { if (!conversationStates.has(conversationId)) { conversationStates.set(conversationId, { isStreaming: false, isPaused: false, currentMessage: '', currentThoughts: [], messageElement: null, thoughtContainer: null, abortController: null, lastUpdateTime: Date.now(), isVisible: false, isUserStopped: false // 标记是否为用户主动停止 }); } } function setActiveConversation (conversationId) { // 暂停当前激活的对话(如果存在) if (currentActiveConversationId && currentActiveConversationId !== conversationId) { pauseConversationStream(currentActiveConversationId); } // 激活新对话 currentActiveConversationId = conversationId; initializeConversationState(conversationId); // 切换对话时默认启用自动跟随底部 shouldAutoScroll = true; const state = conversationStates.get(conversationId); state.isVisible = true; // 根据当前对话状态更新输入框UI updateInputUIForConversation(conversationId); // 如果该对话有正在进行的流式响应,恢复显示 if (state.isStreaming) { if (state.isPaused) { // 如果被暂停了,恢复显示 resumeConversationStream(conversationId); } else { // 如果正在流式响应但没有暂停,确保可见 state.isVisible = true; updateConversationItemStatus(conversationId, 'streaming'); // 确保消息元素可见 if (state.messageElement) { state.messageElement.style.opacity = '1'; state.messageElement.style.transform = 'translateY(0)'; // 滚动到消息位置 const chatMessages = document.getElementById('chat-messages'); if (chatMessages) { state.messageElement.scrollIntoView({ behavior: 'smooth', block: 'end' }); } } } } else if (state.currentMessage || state.currentThoughts.length > 0) { // 如果流式响应已完成,但还有内容,确保显示 if (state.messageElement) { state.isVisible = true; updateConversationItemStatus(conversationId, 'stopped'); // 确保消息元素可见 state.messageElement.style.opacity = '1'; state.messageElement.style.transform = 'translateY(0)'; // 滚动到消息位置 const chatMessages = document.getElementById('chat-messages'); if (chatMessages && state.messageElement) { state.messageElement.scrollIntoView({ behavior: 'smooth', block: 'end' }); } } } // 更新全局状态:检查是否有任何对话正在流式响应 isWaitingForResponse = isAnyConversationStreaming(); } // 根据对话状态更新输入框UI function updateInputUIForConversation (conversationId) { const state = conversationStates.get(conversationId); const userInput = document.getElementById('user-input'); const sendButton = document.getElementById('send-btn'); if (!userInput || !sendButton) return; // 检查当前对话是否正在流式响应 const isCurrentConversationStreaming = state && state.isStreaming && !state.isPaused; if (isCurrentConversationStreaming) { // 如果当前对话正在流式响应,设置输入框为回复中状态 userInput.classList.add('input-loading'); userInput.placeholder = '正在回复中...'; userInput.disabled = true; // 更新发送按钮为停止按钮 sendButton.innerHTML = ` `; sendButton.classList.add('stopping'); // 更新全局状态 isWaitingForResponse = true; } else { // 如果当前对话没有流式响应,恢复输入框为正常状态 userInput.classList.remove('input-loading'); userInput.placeholder = '请输入需要撰写的品类名和要求...'; userInput.disabled = false; // 恢复发送按钮 sendButton.innerHTML = ` `; sendButton.classList.remove('stopping'); // 更新全局状态 isWaitingForResponse = false; } } // 新增:检查是否有任何对话正在流式响应 function isAnyConversationStreaming () { for (const [conversationId, state] of conversationStates) { if (state.isStreaming && !state.isPaused) { return true; } } return false; } // 新增:获取当前正在流式响应的对话ID function getStreamingConversationId () { for (const [conversationId, state] of conversationStates) { if (state.isStreaming && !state.isPaused) { return conversationId; } } return null; } function pauseConversationStream (conversationId) { const state = conversationStates.get(conversationId); if (state && state.isStreaming) { state.isPaused = true; state.isVisible = false; // 在思考过程区域显示"已暂停"提示 if (state.thoughtContainer && !state.thoughtContainer.querySelector('.paused-tip')) { const pausedTip = document.createElement('div'); pausedTip.className = 'paused-tip'; pausedTip.textContent = '对话已切换,AI回复已暂停'; pausedTip.style.color = '#f59e0b'; pausedTip.style.marginTop = '8px'; pausedTip.style.fontSize = '0.9em'; pausedTip.style.fontStyle = 'italic'; state.thoughtContainer.appendChild(pausedTip); } // 更新侧边栏状态显示 updateConversationItemStatus(conversationId, 'paused'); } } function resumeConversationStream (conversationId) { const state = conversationStates.get(conversationId); if (state && state.isStreaming && state.isPaused) { state.isPaused = false; state.isVisible = true; // 移除暂停提示 const pausedTip = state.thoughtContainer?.querySelector('.paused-tip'); if (pausedTip) { pausedTip.remove(); } // 更新侧边栏状态显示 updateConversationItemStatus(conversationId, 'streaming'); // 更新输入框状态 updateInputUIForConversation(conversationId); // 恢复显示最新的思考过程 if (state.thoughtContainer && state.currentThoughts.length > 0) { const thoughtContent = state.thoughtContainer.querySelector('.thought-content'); if (thoughtContent) { const combinedThoughts = state.currentThoughts.join(''); // 使用思考过程专用的渲染函数 thoughtContent.innerHTML = renderThoughts(combinedThoughts); } } // 恢复显示最新的消息内容 if (state.messageElement && state.currentMessage) { const contentContainer = state.messageElement.querySelector('.message-content'); if (contentContainer) { debouncedRenderMarkdown(contentContainer, state.currentMessage); } } // 确保消息元素可见 if (state.messageElement) { state.messageElement.style.opacity = '1'; state.messageElement.style.transform = 'translateY(0)'; // 滚动到当前对话的底部 const chatMessages = document.getElementById('chat-messages'); if (chatMessages) { chatMessages.scrollTop = chatMessages.scrollHeight; } } // 如果消息元素不在聊天区域中,重新添加到聊天区域 if (state.messageElement && !state.messageElement.parentNode) { const chatMessages = document.getElementById('chat-messages'); if (chatMessages) { chatMessages.appendChild(state.messageElement); // 滚动到底部 setTimeout(() => { chatMessages.scrollTop = chatMessages.scrollHeight; }, 100); } } } } function stopConversationStream (conversationId) { const state = conversationStates.get(conversationId); if (state) { // 标记为用户主动停止 state.isUserStopped = true; state.isStreaming = false; state.isPaused = false; if (state.abortController) { state.abortController.abort(); state.abortController = null; } // 移除暂停提示 const pausedTip = state.thoughtContainer?.querySelector('.paused-tip'); if (pausedTip) { pausedTip.remove(); } } // 更新侧边栏状态显示 updateConversationItemStatus(conversationId, 'stopped'); // 更新全局状态:检查是否有任何对话正在流式响应 isWaitingForResponse = isAnyConversationStreaming(); // 如果停止的是当前激活的对话,更新输入框状态 if (conversationId === currentActiveConversationId) { updateInputUIForConversation(conversationId); } } function updateConversationItemStatus (conversationId, status) { const conversationItem = document.querySelector(`[data-conversation-id="${conversationId}"]`); if (conversationItem) { const statusDot = conversationItem.querySelector('.conversation-status-dot'); // 移除所有状态类 conversationItem.classList.remove('streaming', 'paused'); // 添加新的状态类 if (status === 'streaming') { conversationItem.classList.add('streaming'); if (statusDot) { statusDot.style.opacity = '1'; statusDot.style.background = '#10b981'; } } else if (status === 'paused') { conversationItem.classList.add('paused'); if (statusDot) { statusDot.style.opacity = '1'; statusDot.style.background = '#f59e0b'; } } else if (status === 'stopped') { if (statusDot) { statusDot.style.opacity = '0'; } } } } // 创建字符计数器 const charCounter = document.createElement('div'); charCounter.className = 'char-counter'; charCounter.textContent = '0/' + maxMessageLength; charCounter.id = 'char-counter'; // 将字符计数器添加到输入框的父元素中,以便定位在输入框内部 const inputContainer = document.querySelector('.chat-input-container'); if (inputContainer) { inputContainer.style.position = 'relative'; inputContainer.appendChild(charCounter); } // 防抖动函数 function debounce (func, wait) { return function () { const context = this; const args = arguments; clearTimeout(renderDebounceTimer); renderDebounceTimer = setTimeout(() => { func.apply(context, args); }, wait); }; } // 防抖动的Markdown渲染函数,确保光标跟随文字 const debouncedRenderMarkdown = debounce((element, text) => { // 检查是否需要自动滚动 const wasAtBottom = shouldAutoScroll && isUserNearBottom(); // 使用 requestAnimationFrame 优化渲染性能 requestAnimationFrame(() => { try { // 移除现有光标 const existingCursor = element.querySelector('.cursor-blink'); if (existingCursor) existingCursor.remove(); // 使用统一的Markdown渲染函数 element.innerHTML = renderMarkdown(text); // 添加代码高亮 applyCodeHighlighting(element); // 处理审核结果表格 processAuditResultTables(); } catch (error) { // 出错时使用简单文本显示 element.textContent = text; } // 根据情况滚动到底部 if (wasAtBottom) { requestAnimationFrame(() => { forceScrollToBottom(); }); } }); }, 16); // 降低防抖时间到16ms,约等于60fps // 改进 Markdown 解析器 function parseMarkdown (text) { if (!markdownEnabled || !text) return text; try { // 检查marked是否存在 if (typeof marked === 'undefined') { // 简单的备用实现:仅处理基本格式 const formattedText = text .replace(/\n\n/g, '

') // 段落 .replace(/\n/g, '
') // 换行 .replace(/\*\*(.*?)\*\*/g, '$1') // 粗体 .replace(/\*(.*?)\*/g, '$1') // 斜体 .replace(/`(.*?)`/g, '$1'); // 行内代码 return `

${formattedText}

`; } // 配置marked选项 marked.setOptions({ renderer: new marked.Renderer(), highlight: function (code, lang) { const language = hljs.getLanguage(lang) ? lang : 'plaintext'; return hljs.highlight(code, { language }).value; }, langPrefix: 'hljs language-', // 为代码块添加类名 pedantic: false, gfm: true, breaks: true, sanitize: false, smartypants: false, xhtml: false }); // 创建自定义渲染器 const renderer = new marked.Renderer(); // 定制表格渲染方法 renderer.table = function (header, body) { // 检测表格中是否包含违规内容 const hasViolations = body.includes('violation-row'); // 添加audit-result类,确保能应用我们的CSS样式 return `\n\n${header}\n\n${body}\n
\n`; }; // 定制表格行渲染方法 renderer.tablerow = function (content) { // 检测行内是否包含违规关键词 const violationKeywords = ['违规', '违反', '不符合', '禁止', '风险', '投诉', '举报', '警告', '严重', '不通过']; let rowClass = ''; if (violationKeywords.some(keyword => content.includes(keyword))) { rowClass = 'violation-row'; } else if (content.includes('注意') || content.includes('警示') || content.includes('提醒')) { rowClass = 'warning-row'; } else if (content.includes('正常') || content.includes('合规') || content.includes('通过')) { rowClass = 'safe-row'; } return `${content}\n`; }; // 定制段落渲染方法 renderer.paragraph = function (text) { // 高亮审核结果关键词 if (text.includes('通过') && !text.includes('不通过')) { text = text.replace( /(.*?)(通过)(.*?)/g, '$1$2$3' ); } if (text.includes('不通过')) { text = text.replace( /(.*?)(不通过)(.*?)/g, '$1$2$3' ); } // 突出显示包含违规关键词的段落 const violationKeywords = ['违规', '违反', '不符合', '禁止', '风险', '投诉', '举报', '警告', '严重']; violationKeywords.forEach(keyword => { if (text.includes(keyword)) { text = text.replace( new RegExp(`(.*?)(${keyword})(.*?)`, 'g'), '$1$2$3' ); } }); return `

${text}

\n`; }; // 使用定制的渲染器解析Markdown marked.setOptions({ renderer: renderer }); // 解析Markdown文本 const parsed = marked.parse(text); // 添加markdown-content类以应用样式 const result = `
${parsed}
`; return result; } catch (error) { // 发生错误时,回退到原始文本 return `

${text}

`; } } // 自动调整文本区域高度 function autoResizeTextarea () { // 重置高度,以便重新计算 userInput.style.height = 'auto'; // 设置行高基准值 const lineHeight = 24; // 假设行高为24px const minHeight = lineHeight + 12; // 最小高度,一行加上padding const maxHeight = 200; // 最大高度 // 计算内容高度 const contentHeight = userInput.scrollHeight; if (contentHeight <= minHeight) { // 内容少于或等于一行 userInput.style.height = minHeight + 'px'; userInput.style.overflowY = 'hidden'; // 隐藏滚动条 } else if (contentHeight > maxHeight) { // 内容超过最大高度 userInput.style.height = maxHeight + 'px'; userInput.style.overflowY = 'auto'; // 显示滚动条 } else { // 内容在一行和最大高度之间 userInput.style.height = contentHeight + 'px'; userInput.style.overflowY = 'hidden'; // 隐藏滚动条 } } // 更新字符计数器 function updateCharCounter () { const charCount = userInput.value.length; const counterElement = document.getElementById('char-counter'); if (counterElement) { counterElement.textContent = `${charCount}/${maxMessageLength}`; // 当接近字符上限时改变样式提醒用户 counterElement.classList.remove('warning', 'exceeded'); if (charCount > maxMessageLength) { counterElement.classList.add('exceeded'); } else if (charCount > maxMessageLength * 0.9) { counterElement.classList.add('warning'); } } } // 检查用户是否接近底部 function isUserNearBottom () { return chatMessages.scrollHeight - chatMessages.scrollTop - chatMessages.clientHeight < scrollThreshold; } // 强制滚动到底部 function forceScrollToBottom (force = false) { // 如果正在编辑消息,不滚动 if (window.isEditingMessage) { return; } if (!force && !shouldAutoScroll) { return; } chatMessages.scrollTop = chatMessages.scrollHeight; } // 添加用户消息 function addUserMessage (message, messageId = null) { const messageElement = document.createElement('div'); messageElement.className = 'message user'; const contentElement = document.createElement('div'); contentElement.className = 'message-content'; contentElement.textContent = message; messageElement.appendChild(contentElement); // 如果有 messageId,添加编辑按钮 if (messageId) { // 转义特殊字符,防止XSS攻击 const escapedContent = message .replace(/\\/g, '\\\\') .replace(/"/g, '"') .replace(/'/g, ''') .replace(//g, '>') .replace(/\n/g, '\\n'); const editButton = document.createElement('button'); editButton.className = 'edit-button'; editButton.setAttribute('title', '编辑消息'); editButton.innerHTML = ` `; editButton.onclick = function () { // 调用全局的 editMessage 函数(在 index.html 中定义) if (typeof window.editMessage === 'function') { window.editMessage(messageId, message); } }; messageElement.appendChild(editButton); messageElement.setAttribute('data-message-id', messageId); } chatMessages.appendChild(messageElement); // 确保消息立即可见 messageElement.style.opacity = '1'; messageElement.style.transform = 'translateY(0)'; // 滚动到底部 forceScrollToBottom(true); return messageElement; } // 修改添加机器人消息函数 function addBotMessage (message, isMarkdown = true, thoughts = []) { if (message.length <= 0 && (!thoughts || thoughts.length === 0)) { return; } const botMessage = document.createElement('div'); botMessage.className = 'message bot-message'; // 先渲染思考过程(如有) if (thoughts && Array.isArray(thoughts) && thoughts.length > 0) { const thoughtContainer = document.createElement('div'); thoughtContainer.className = 'thought-container'; // 创建折叠按钮 const toggleBtn = document.createElement('button'); toggleBtn.className = 'thought-toggle-btn'; toggleBtn.innerHTML = ''; toggleBtn.title = '收起/展开思考过程'; toggleBtn.onclick = function (e) { e.stopPropagation(); thoughtContainer.classList.toggle('collapsed'); const icon = toggleBtn.querySelector('svg path'); if (thoughtContainer.classList.contains('collapsed')) { icon.setAttribute('d', 'M18 15L12 9L6 15'); } else { icon.setAttribute('d', 'M6 9l6 6 6-6'); } }; thoughtContainer.appendChild(toggleBtn); const contentArea = document.createElement('div'); contentArea.className = 'thought-content'; // 处理思考过程,显示为连续的文本块 if (Array.isArray(thoughts)) { // 如果是数组,合并后使用思考过程专用的渲染函数 const combinedThoughts = thoughts.join(''); contentArea.innerHTML = renderThoughts(combinedThoughts); } else if (typeof thoughts === 'string') { // 如果是字符串,使用思考过程专用的渲染函数 contentArea.innerHTML = renderThoughts(thoughts); } thoughtContainer.appendChild(contentArea); botMessage.appendChild(thoughtContainer); } // 再渲染AI回复内容 const messageContent = document.createElement('div'); messageContent.className = 'message-content'; if (isMarkdown) { messageContent.innerHTML = renderMarkdown(message); } else { messageContent.innerText = message; } botMessage.appendChild(messageContent); document.getElementById('chat-messages').appendChild(botMessage); applyCodeHighlighting(botMessage); processAuditResultTables(); // 为新添加的消息添加复制按钮 if (typeof window.setupCopyButtons === 'function') { window.setupCopyButtons(); } forceScrollToBottom(); return botMessage; } // 添加错误消息 function addErrorMessage (error, isAuthError = false) { // 如果已经有打字动画,则移除 removeTypingAnimation(); const errorElement = document.createElement('div'); errorElement.className = 'error-message'; if (isAuthError) { errorElement.innerHTML = `
${error}
正在尝试重新连接,如果失败将跳转到登录页面...
`; } else { errorElement.textContent = error; } chatMessages.appendChild(errorElement); forceScrollToBottom(); // 认证错误显示更长时间,其他错误5秒后移除 const timeout = isAuthError ? 10000 : 5000; setTimeout(() => { if (errorElement.parentNode) { errorElement.parentNode.removeChild(errorElement); } }, timeout); } // 生成UUID function generateUUID () { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { var r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } // 应用代码块高亮 function applyCodeHighlighting (element) { // 检查hljs是否存在 if (typeof hljs === 'undefined') { return; } try { const codeBlocks = element.querySelectorAll('pre code'); for (const block of codeBlocks) { hljs.highlightElement(block); } } catch (error) { } } // 处理审核结果表格 function processAuditResultTables () { const tables = document.querySelectorAll('table.audit-result'); tables.forEach(table => { // 如果表格已经处理过,则跳过 if (table.dataset.processed) return; // 标记表格已处理 table.dataset.processed = 'true'; // 处理表格内的长文本 const cells = table.querySelectorAll('td'); cells.forEach(cell => { if (cell.textContent.length > 100) { cell.classList.add('long-text'); } }); }); } // 修改发送消息函数 function sendMessage () { const message = userInput.value.trim(); // 检查是否有任何对话正在流式响应 if (!message || isAnyConversationStreaming()) return; if (message.length > maxMessageLength) { alert(`消息长度不能超过${maxMessageLength}字符`); return; } // 获取当前对话ID const conversationId = window.currentConversationId || 'new'; // 初始化对话状态 initializeConversationState(conversationId); setActiveConversation(conversationId); // 确保当前对话可见 const state = conversationStates.get(conversationId); if (state) { state.isVisible = true; } // 展示用户消息(立即显示,不等待后端响应) // 注意:此时还没有 message_id,后续会在收到后端响应后更新 const userMessageElement = addUserMessage(message); // 将用户消息保存到对话状态中,确保切换对话时不会丢失 if (state) { state.userMessages = state.userMessages || []; state.userMessages.push({ content: message, timestamp: new Date(), element: userMessageElement, messageId: null // 将在收到后端响应后设置 }); } // 清空输入框 userInput.value = ''; autoResizeTextarea(); updateCharCounter(); // 更新按钮状态为停止按钮 sendButton.innerHTML = ` `; sendButton.classList.add('stopping'); // 标记正在等待响应 isWaitingForResponse = true; // 将输入框设置为loading状态 userInput.classList.add('input-loading'); userInput.placeholder = '正在回复中...'; userInput.disabled = true; // 准备请求数据 const requestData = { message: message, user_id: userId, agent: (localStorage.getItem('current_agent') || 'kuaishou'), conversation_id: window.currentConversationId || null, session_id: window.currentSessionId || null, force_new: window.forceNewConversation || false }; // 重置强制创建新对话的标志 window.forceNewConversation = false; // 根据选择的模式,使用不同的请求方式 if (useStreamingMode) { handleStreamResponse(message, conversationId); } else { handleStandardResponse(message, requestData); } isStopped = false; // 每次发送消息时重置 } // 修改处理流式响应函数 function handleStreamResponse (message, conversationId) { const state = conversationStates.get(conversationId); if (!state) { return; } // 重置对话状态 state.isStreaming = true; state.isPaused = false; state.isUserStopped = false; // 重置用户停止标志 state.currentMessage = ''; state.currentThoughts = []; state.lastUpdateTime = Date.now(); // 更新侧边栏状态显示 updateConversationItemStatus(conversationId, 'streaming'); let fullMessage = ''; let lastProcessedMessage = ''; let lastRenderTime = 0; const RENDER_INTERVAL = 16; // 16ms,约等于60fps let allThoughts = []; let thoughtContainer = null; // 新增思考过程容器 let thoughtContent = null; // 新增思考内容包裹层 let thoughtToggleBtn = null; // 折叠按钮 let contentContainer = null; // 结果内容容器,初始不创建 try { // 创建AbortController用于取消请求 state.abortController = new AbortController(); // 创建机器人消息元素 const messageDiv = document.createElement('div'); messageDiv.className = 'message bot'; // 创建思考过程容器 thoughtContainer = document.createElement('div'); thoughtContainer.className = 'thought-container'; // 创建折叠按钮 thoughtToggleBtn = document.createElement('button'); thoughtToggleBtn.className = 'thought-toggle-btn'; // 展开时默认向上箭头 thoughtToggleBtn.innerHTML = '' + '' + '' + '' + ''; thoughtToggleBtn.title = '收起/展开思考过程'; thoughtToggleBtn.onclick = function (e) { e.stopPropagation(); thoughtContainer.classList.toggle('collapsed'); // 切换SVG图标 const icon = thoughtToggleBtn.querySelector('.toggle-icon'); if (thoughtContainer.classList.contains('collapsed')) { // 向下箭头 icon.innerHTML = '' + '' + ''; } else { // 向上箭头 icon.innerHTML = '' + '' + ''; } }; thoughtContainer.appendChild(thoughtToggleBtn); // 创建思考内容包裹层 thoughtContent = document.createElement('div'); thoughtContent.className = 'thought-content'; thoughtContainer.appendChild(thoughtContent); messageDiv.appendChild(thoughtContainer); // 保存到对话状态 state.messageElement = messageDiv; state.thoughtContainer = thoughtContainer; // 添加到聊天区域 chatMessages.appendChild(state.messageElement); // 确保消息元素可见 messageDiv.style.opacity = '1'; messageDiv.style.transform = 'translateY(0)'; // 添加对话ID标识,便于后续管理 messageDiv.setAttribute('data-conversation-id', conversationId); // 滚动到底部 forceScrollToBottom(); // 始终使用POST方法发送所有消息 fetch('/api/chat/stream', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: message, user_id: userId, agent: (localStorage.getItem('current_agent') || 'kuaishou'), conversation_id: window.currentConversationId || null, session_id: window.currentSessionId || null, force_new: window.forceNewConversation || false }), signal: state.abortController.signal }) .then(response => { if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); } // 创建新的EventSource,包装响应 const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; function readStream () { reader.read().then(({ done, value }) => { if (done) { // 流式响应完成 state.isStreaming = false; updateConversationItemStatus(conversationId, 'stopped'); resetUIState(); return; } // 解码二进制数据为文本 const chunk = decoder.decode(value, { stream: true }); buffer += chunk; // 处理buffer中完整的SSE消息 const lines = buffer.split('\n\n'); buffer = lines.pop() || ''; // 最后一个可能不完整,保留到下次处理 lines.forEach(line => { if (line.startsWith('data: ')) { try { const eventData = JSON.parse(line.substring(6)); processEventData(eventData, state); } catch (error) { } } }); // 无论是否暂停,都继续读取流数据 readStream(); // 继续读取下一块数据 }).catch(error => { // 如果是用户主动停止,不显示错误提示 if (!state.isPaused && !state.isUserStopped && error.name !== 'AbortError') { addErrorMessage('连接错误,请重试'); } state.isStreaming = false; state.isUserStopped = false; // 重置标志 resetUIState(); }); } readStream(); // 开始读取流 }) .catch(error => { // 如果是用户主动停止(AbortError),不显示错误提示 if (!state.isPaused && !state.isUserStopped && error.name !== 'AbortError') { if (error.message.includes('401')) { addErrorMessage('登录已过期,正在重新连接...', true); // 尝试刷新token tryRefreshToken().then(refreshed => { if (!refreshed) { clearAuthAndRedirect(); } }); } else { addErrorMessage('连接错误,请重试: ' + error.message); } } state.isStreaming = false; state.isUserStopped = false; // 重置标志 updateConversationItemStatus(conversationId, 'stopped'); resetUIState(); }); // 提取通用的事件数据处理逻辑为函数 function processEventData (data, conversationState) { // 更新最后更新时间 conversationState.lastUpdateTime = Date.now(); // 处理用户消息ID(在初始响应中返回) if (data.user_message_id) { // 更新最后添加的用户消息,为其添加编辑按钮 if (conversationState.userMessages && conversationState.userMessages.length > 0) { const lastUserMessage = conversationState.userMessages[conversationState.userMessages.length - 1]; if (lastUserMessage && !lastUserMessage.messageId) { lastUserMessage.messageId = data.user_message_id; // 更新消息元素,添加编辑按钮 if (lastUserMessage.element) { const messageElement = lastUserMessage.element; const messageId = data.user_message_id; const messageContent = lastUserMessage.content; // 转义特殊字符 const escapedContent = messageContent .replace(/\\/g, '\\\\') .replace(/"/g, '"') .replace(/'/g, ''') .replace(//g, '>') .replace(/\n/g, '\\n'); // 创建编辑按钮 const editButton = document.createElement('button'); editButton.className = 'edit-button'; editButton.setAttribute('title', '编辑消息'); editButton.innerHTML = ` `; editButton.onclick = function () { if (typeof window.editMessage === 'function') { window.editMessage(messageId, messageContent); } }; messageElement.appendChild(editButton); messageElement.setAttribute('data-message-id', messageId); } } } } if (!data.success) { if (data.error && data.error.includes('401')) { addErrorMessage('登录已过期,正在重新连接...', true); // 尝试刷新token tryRefreshToken().then(refreshed => { if (!refreshed) { clearAuthAndRedirect(); } }); } else { addErrorMessage(data.error || '发生未知错误'); } conversationState.isStreaming = false; updateConversationItemStatus(conversationId, 'stopped'); resetUIState(); return; } // 处理思考过程 if (data.thought) { allThoughts.push(data.thought); conversationState.currentThoughts = allThoughts; // 显示为连续的文本块,不添加换行符 if (thoughtContent) { const combinedThoughts = allThoughts.join(''); // 使用思考过程专用的渲染函数 thoughtContent.innerHTML = renderThoughts(combinedThoughts); // 只有在可见且未暂停时才滚动 if (conversationState.isVisible && !conversationState.isPaused) { forceScrollToBottom(); } } return; } if (data.chunk) { fullMessage += data.chunk; conversationState.currentMessage = fullMessage; // 第一次收到chunk时再创建contentContainer if (!contentContainer) { contentContainer = document.createElement('div'); contentContainer.className = 'message-content typing'; messageDiv.appendChild(contentContainer); } // 使用 requestAnimationFrame 优化渲染性能 const now = performance.now(); if (now - lastRenderTime >= RENDER_INTERVAL) { requestAnimationFrame(() => { if (fullMessage !== lastProcessedMessage && contentContainer) { // 总是渲染内容,但只在可见且未暂停时滚动 debouncedRenderMarkdown(contentContainer, fullMessage); lastProcessedMessage = fullMessage; if (conversationState.isVisible && !conversationState.isPaused && isUserNearBottom()) { requestAnimationFrame(() => { forceScrollToBottom(); }); } } }); lastRenderTime = now; } } if (data.session_id) { // 更新当前对话的session_id window.currentSessionId = data.session_id; } if (data.conversation_id) { // 更新当前对话ID window.currentConversationId = data.conversation_id; // 更新对话状态中的对话ID if (conversationId !== data.conversation_id) { // 获取当前状态 const currentState = conversationStates.get(conversationId); if (currentState) { // 更新状态到新的对话ID conversationStates.set(data.conversation_id, currentState); conversationStates.delete(conversationId); // 更新当前激活的对话ID if (currentActiveConversationId === conversationId) { currentActiveConversationId = data.conversation_id; } } } } if (data.done) { if (contentContainer) { contentContainer.classList.remove('typing'); // 只在done时渲染最终结果 debouncedRenderMarkdown(contentContainer, fullMessage); } conversationState.isStreaming = false; updateConversationItemStatus(conversationId, 'stopped'); resetUIState(); userInput.focus(); if (conversationState.isVisible) { forceScrollToBottom(); } // 为流式响应完成的消息添加复制按钮 if (typeof window.setupCopyButtons === 'function') { window.setupCopyButtons(); } // 对话完成后刷新历史记录 if (typeof loadConversationHistory === 'function') { // 添加延迟确保数据库保存完成 setTimeout(() => { loadConversationHistory(true); // 自动选中最新的对话 }, 500); } // 如果是新对话,更新侧边栏选中状态 if (data.conversation_id && data.conversation_id !== conversationId) { if (typeof updateConversationSelection === 'function') { updateConversationSelection(data.conversation_id); } } // 更新对话内容缓存 if (typeof updateConversationCache === 'function') { updateConversationCache(conversationId, fullMessage, allThoughts); } return; } } } catch (error) { addErrorMessage('连接错误,请重试'); state.isStreaming = false; updateConversationItemStatus(conversationId, 'stopped'); resetUIState(); } } // 修改处理标准响应函数 function handleStandardResponse (message, requestData) { abortController = new AbortController(); fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestData), signal: abortController.signal }) .then(response => response.json()) .then(data => { if (data.success) { addBotMessage(data.message, true, data.thoughts); // 更新当前对话ID和session_id if (data.conversation_id) { window.currentConversationId = data.conversation_id; } if (data.session_id) { window.currentSessionId = data.session_id; } // 对话完成后刷新历史记录 if (typeof loadConversationHistory === 'function') { // 添加延迟确保数据库保存完成 setTimeout(() => { loadConversationHistory(true); // 自动选中最新的对话 }, 500); } // 如果是新对话,更新侧边栏选中状态 if (data.conversation_id) { if (typeof updateConversationSelection === 'function') { updateConversationSelection(data.conversation_id); } } } else { if (data.error && data.error.includes('401')) { addErrorMessage('登录已过期,正在重新连接...', true); // 尝试刷新token tryRefreshToken().then(refreshed => { if (!refreshed) { clearAuthAndRedirect(); } }); } else { addErrorMessage(data.error || '发生未知错误'); } } }) .catch(error => { if (error.name === 'AbortError') { addErrorMessage('请求已取消'); } else { addErrorMessage('连接错误,请重试'); } }) .finally(() => { document.querySelectorAll('.message-content.typing').forEach(el => { el.classList.remove('typing'); }); resetUIState(); }); } // 修改停止请求函数 function stopRequest () { isStopped = true; // 新增:设置停止标志 // 停止当前激活对话的流式响应 if (currentActiveConversationId) { stopConversationStream(currentActiveConversationId); } if (eventSource) { closeEventSource(); } if (abortController) { abortController.abort(); abortController = null; } // 重置为发送按钮 resetUIState(); // 重置消息元素引用 currentBotMessageElement = null; currentBotMessageContainer = null; } // 关闭EventSource连接 function closeEventSource () { if (eventSource) { eventSource.close(); eventSource = null; } } // 修改重置UI状态函数 function resetUIState () { // 更新全局状态:检查是否有任何对话正在流式响应 isWaitingForResponse = isAnyConversationStreaming(); // 根据当前激活的对话状态来重置UI if (currentActiveConversationId) { updateInputUIForConversation(currentActiveConversationId); } else { // 如果没有激活的对话,恢复默认状态 sendButton.innerHTML = ` `; sendButton.classList.remove('stopping'); // 恢复输入框状态 userInput.classList.remove('input-loading'); userInput.placeholder = '请输入需要撰写的品类名和要求...'; userInput.disabled = false; } // 重置消息元素引用 currentBotMessageElement = null; currentBotMessageContainer = null; // 如果有加载动画,移除它 if (currentLoadingElement) { currentLoadingElement.remove(); currentLoadingElement = null; } } // 重置对话 function resetConversation () { // 如果正在确认,则执行重置 if (resetButton.classList.contains('confirming')) { // 发送重置请求 fetch('/api/reset', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user_id: userId }) }) .then(response => response.json()) .then(data => { if (data.success) { // 清空聊天记录,仅保留欢迎消息 while (chatMessages.childNodes.length > 1) { chatMessages.removeChild(chatMessages.lastChild); } // 清除当前对话ID window.currentConversationId = null; // 重置按钮状态 resetButton.classList.remove('confirming'); resetButton.textContent = '重置对话'; // 显示提示 addBotMessage('对话已重置,您可以开始新的对话了。'); } else { addErrorMessage(data.error || '重置失败,请重试'); } }) .catch(error => { addErrorMessage('重置失败,请重试'); }); } else { // 进入确认状态 resetButton.classList.add('confirming'); resetButton.textContent = '确认重置'; // 5秒后如果没有点击,恢复原状 setTimeout(() => { if (resetButton.classList.contains('confirming')) { resetButton.classList.remove('confirming'); resetButton.textContent = '重置对话'; } }, 5000); } } // 添加事件监听器 userInput.addEventListener('input', () => { autoResizeTextarea(); updateCharCounter(); }); // 在输入框内按Enter发送消息(Shift+Enter换行),状态与发送按钮一致 userInput.addEventListener('keydown', (e) => { if (e.key !== 'Enter') return; if (e.shiftKey || e.ctrlKey || e.altKey || e.metaKey || e.isComposing) return; e.preventDefault(); // 阻止默认换行 if (isAnyConversationStreaming()) { stopRequest(); // 和按钮行为保持一致:正在流式时改为停止 return; } if (userInput.disabled) return; sendMessage(); }); // 按下Escape键时取消请求 document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && isAnyConversationStreaming()) { e.preventDefault(); stopRequest(); } }); // 点击发送按钮 sendButton.addEventListener('click', () => { if (isAnyConversationStreaming()) { stopRequest(); } else { sendMessage(); } }); // 重置按钮点击事件 resetButton.addEventListener('click', resetConversation); // 监听滚动事件 chatMessages.addEventListener('scroll', () => { // 记录当前滚动位置 const currentScrollTop = chatMessages.scrollTop; const maxScrollTop = chatMessages.scrollHeight - chatMessages.clientHeight; if (currentScrollTop < lastScrollTop - 2) { shouldAutoScroll = false; } else if (maxScrollTop - currentScrollTop <= scrollThreshold) { shouldAutoScroll = true; } lastScrollTop = currentScrollTop; }); // 初始化调整文本区域高度 autoResizeTextarea(); // 聚焦到输入框 userInput.focus(); // 初始化字符计数器 updateCharCounter(); // 向输入框添加水波纹效果 function createRipple (event) { const button = event.currentTarget; // 如果按钮被禁用,则不创建水波纹 if (button.disabled) return; const ripple = document.createElement('span'); ripple.classList.add('ripple'); button.appendChild(ripple); const rect = button.getBoundingClientRect(); const size = Math.max(rect.width, rect.height); const x = event.clientX - rect.left - size / 2; const y = event.clientY - rect.top - size / 2; ripple.style.width = ripple.style.height = `${size}px`; ripple.style.left = `${x}px`; ripple.style.top = `${y}px`; // 动画结束后移除水波纹元素 setTimeout(() => { ripple.remove(); }, 600); } // 添加水波纹效果 sendButton.addEventListener('click', createRipple); resetButton.addEventListener('click', createRipple); // 移除打字动画 function removeTypingAnimation () { document.querySelectorAll('.message-content.typing').forEach(el => { el.classList.remove('typing'); }); } // 认证相关函数 - 增强版本 async function checkAuthStatusInChat () { try { const token = localStorage.getItem('auth_token'); const userInfo = localStorage.getItem('user_info'); if (!token || !userInfo) { window.location.href = '/login'; return; } // 检查token是否即将过期(提前1小时检查) if (isTokenExpiringSoon(token)) { const refreshed = await tryRefreshToken(); if (!refreshed) { clearAuthAndRedirect(); return; } } // 显示本地用户信息 try { const user = JSON.parse(userInfo); displayUserInfo(user); showUserSection(); addAuthHeader(token); } catch (e) { clearAuthAndRedirect(); } } catch (error) { clearAuthAndRedirect(); } } // 检查token是否即将过期 function isTokenExpiringSoon (token) { try { const payload = JSON.parse(atob(token.split('.')[1])); const exp = payload.exp * 1000; // 转换为毫秒 const now = Date.now(); const oneHour = 60 * 60 * 1000; // 1小时 return (exp - now) < oneHour; } catch (e) { return true; // 解析失败,认为已过期 } } // 尝试刷新token(单飞模式,避免并发刷新导致状态错乱) let isRefreshing = false; let refreshPromise = null; async function tryRefreshToken () { // 如果已有刷新任务在进行,直接复用该任务结果 if (refreshPromise) { return refreshPromise; } refreshPromise = (async () => { if (isRefreshing) return false; try { isRefreshing = true; const token = localStorage.getItem('auth_token'); if (!token) { return false; } // 使用原生fetch,避免被addAuthHeader包装后递归进入刷新逻辑 const nativeFetch = window.__nativeFetch || window.fetch; const response = await nativeFetch('/api/auth/refresh', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` } }); const data = await response.json(); if (response.ok && data.success && data.token) { localStorage.setItem('auth_token', data.token); return true; } if (response.status === 401 || data.code === 'TOKEN_EXPIRED_NO_SESSION' || data.code === 'MISSING_TOKEN') { return false; } return false; } catch (error) { return false; } finally { isRefreshing = false; refreshPromise = null; } })(); return refreshPromise; } function clearAuthAndRedirect () { if (isRedirecting) { return; } isRedirecting = true; // 清除本地存储 localStorage.removeItem('auth_token'); localStorage.removeItem('user_info'); // 清除session存储(如果有) sessionStorage.removeItem('auth_token'); sessionStorage.removeItem('user_info'); // 立即跳转,不使用setTimeout try { window.location.replace('/login'); } catch (error) { window.location.href = '/login'; } } function showUserSection () { const userInfo = document.getElementById('user-info'); if (userInfo) userInfo.style.display = 'flex'; } function addAuthHeader () { // 避免重复包装fetch if (!window.__nativeFetch) { window.__nativeFetch = window.fetch; } if (window.__authFetchWrapped) { return; } window.__authFetchWrapped = true; const originalFetch = window.__nativeFetch; window.fetch = async function (url, options = {}) { if (options.headers === undefined) { options.headers = {}; } // 每次请求都读取最新token,避免闭包持有旧token导致长期运行后401 const latestToken = localStorage.getItem('auth_token'); if (latestToken) { options.headers['Authorization'] = `Bearer ${latestToken}`; } try { const response = await originalFetch(url, options); // 检查401错误,但不要对refresh接口本身进行重试 if (response.status == 401) { // 如果是refresh接口返回401,直接跳转登录,不要再次尝试刷新 const urlString = typeof url === 'string' ? url : url.toString(); if (urlString.includes('/api/auth/refresh')) { clearAuthAndRedirect(); return response; } const refreshed = await tryRefreshToken(); if (refreshed) { // 更新token后重试请求 const retryToken = localStorage.getItem('auth_token'); if (retryToken) { options.headers['Authorization'] = `Bearer ${retryToken}`; } return originalFetch(url, options); } else { // 刷新失败,清除认证信息并跳转登录 clearAuthAndRedirect(); return response; } } return response; } catch (error) { throw error; } }; } // 将addAuthHeader函数暴露到全局作用域 window.addAuthHeader = addAuthHeader; // 定期更新对话状态 function startConversationStatusUpdater () { setInterval(() => { updateAllConversationStatuses(); }, 2000); // 每2秒更新一次 } function updateAllConversationStatuses () { conversationStates.forEach((state, conversationId) => { if (state.isStreaming) { // 检查流式响应是否还在进行 const now = Date.now(); const timeSinceLastUpdate = now - state.lastUpdateTime; // 如果超过30秒没有更新,认为流式响应已结束 if (timeSinceLastUpdate > 30000) { state.isStreaming = false; state.isPaused = false; updateConversationItemStatus(conversationId, 'stopped'); } } }); } // 将对话状态管理函数和变量暴露到全局作用域 window.conversationStates = conversationStates; window.currentActiveConversationId = currentActiveConversationId; window.setActiveConversation = setActiveConversation; window.pauseConversationStream = pauseConversationStream; window.resumeConversationStream = resumeConversationStream; window.stopConversationStream = stopConversationStream; window.initializeConversationState = initializeConversationState; window.updateConversationItemStatus = updateConversationItemStatus; window.startConversationStatusUpdater = startConversationStatusUpdater; window.isAnyConversationStreaming = isAnyConversationStreaming; window.getStreamingConversationId = getStreamingConversationId; window.updateInputUIForConversation = updateInputUIForConversation; window.renderMarkdown = renderMarkdown; window.renderThoughts = renderThoughts; function displayUserInfo (userInfo) { const userInfoElement = document.getElementById('user-info'); const userNameElement = document.getElementById('user-name'); const userTypeElement = document.getElementById('user-type'); const userTitleElement = document.getElementById('user-title'); const avatarImg = document.getElementById('avatar-img'); const avatarFallback = document.getElementById('avatar-fallback'); const userDropdown = document.getElementById('user-dropdown'); if (!userInfoElement || !userNameElement) return; // 显示用户名 userNameElement.textContent = userInfo.nickname || userInfo.name || '用户'; // 显示用户类型 if (userTypeElement) { const typeMap = { 'dingtalk': '钉钉用户', 'wecom': '企业微信用户' }; userTypeElement.textContent = typeMap[userInfo.login_type] || '用户'; } // 显示用户职位 if (userTitleElement && userInfo.title) { userTitleElement.textContent = userInfo.title; userTitleElement.style.display = 'block'; } // 显示用户头像 if (avatarImg && avatarFallback) { if (userInfo.avatar) { avatarImg.src = userInfo.avatar; avatarImg.style.display = 'block'; avatarFallback.style.display = 'none'; // 添加头像加载失败处理 avatarImg.onerror = function () { this.style.display = 'none'; avatarFallback.style.display = 'flex'; const firstChar = (userInfo.name || 'U').charAt(0).toUpperCase(); avatarFallback.textContent = firstChar; }; } else { avatarImg.style.display = 'none'; avatarFallback.style.display = 'flex'; const firstChar = (userInfo.name || 'U').charAt(0).toUpperCase(); avatarFallback.textContent = firstChar; } } // 显示用户详细信息下拉框 if (userDropdown) { displayUserDetails(userInfo); } userInfoElement.style.display = 'flex'; } function displayUserDetails (userInfo) { // 显示邮箱信息 const emailElement = document.getElementById('user-email'); if (emailElement && userInfo.email) { emailElement.querySelector('.info-value').textContent = userInfo.email; emailElement.style.display = 'flex'; } // 显示手机信息 const mobileElement = document.getElementById('user-mobile'); if (mobileElement && userInfo.mobile) { mobileElement.querySelector('.info-value').textContent = userInfo.mobile; mobileElement.style.display = 'flex'; } // 显示工号信息 const jobElement = document.getElementById('user-job'); if (jobElement && userInfo.job_number) { jobElement.querySelector('.info-value').textContent = userInfo.job_number; jobElement.style.display = 'flex'; } // 显示部门信息 const deptElement = document.getElementById('user-dept'); if (deptElement && userInfo.dept_name) { deptElement.querySelector('.info-value').textContent = userInfo.dept_name; deptElement.style.display = 'flex'; } // 如果有任何详细信息,显示下拉框 const userDropdown = document.getElementById('user-dropdown'); if (userDropdown && (userInfo.email || userInfo.mobile || userInfo.job_number || userInfo.dept_name)) { userDropdown.style.display = 'block'; } } async function handleLogout () { try { const token = localStorage.getItem('auth_token'); // 调用退出登录API const response = await fetch('/api/auth/logout', { method: 'POST', headers: { 'Authorization': `Bearer ${token}` } }); // 清除本地存储 localStorage.removeItem('auth_token'); localStorage.removeItem('user_info'); // 跳转到登录页 window.location.href = '/login'; } catch (error) { // 即使API调用失败,也清除本地存储并跳转 localStorage.removeItem('auth_token'); localStorage.removeItem('user_info'); window.location.href = '/login'; } } });