const ChatManager = { isStreaming: false, messages: [], init() { const input = document.getElementById('messageInput'); if (input) { input.addEventListener('keydown', (e) => { if (e.key === 'Enter' && e.ctrlKey) { e.preventDefault(); this.sendMessage(); } }); // 自适应高度 input.addEventListener('input', () => { input.style.height = 'auto'; input.style.height = Math.min(input.scrollHeight, 120) + 'px'; }); } const sendBtn = document.getElementById('sendBtn'); if (sendBtn) { sendBtn.addEventListener('click', () => this.sendMessage()); } UploadManager.init(); }, async loadMessages(sessionId) { try { const res = await api.get('/sessions/' + sessionId + '/messages'); this.messages = res.data || []; this.renderMessages(); Storage.setCachedMessages(sessionId, this.messages); } catch (err) { // 尝试从缓存加载 const cached = Storage.getCachedMessages(sessionId); if (cached) { this.messages = cached; this.renderMessages(); } } }, renderMessages() { const container = document.getElementById('messagesContainer'); if (!container) return; if (this.messages.length === 0) { container.innerHTML = '

开始新的对话

输入消息开始聊天

'; return; } container.innerHTML = this.messages.map(msg => { if (msg.role === 'user') { return `
${this.escapeHtml(msg.content)}
`; } else { let html = `
`; if (msg.thinking_content) { html += `
思考过程 ▾
`; html += `
${MarkdownRenderer.render(msg.thinking_content)}
`; } html += `
${MarkdownRenderer.render(msg.content)}
`; html += `
`; return html; } }).join(''); container.scrollTop = container.scrollHeight; }, async sendMessage() { if (this.isStreaming) return; const input = document.getElementById('messageInput'); const content = input.value.trim(); if (!content) return; if (!SessionManager.currentSessionId) { await SessionManager.createSession(); } // 清空输入 input.value = ''; input.style.height = 'auto'; // 构建消息 const userMessage = { role: 'user', content }; if (UploadManager.getFiles().length > 0) { userMessage.file_info = UploadManager.getFiles(); } this.messages.push(userMessage); this.renderMessages(); // 保存用户消息到数据库 api.post('/sessions/' + SessionManager.currentSessionId + '/messages', userMessage).catch(console.error); // 清除文件 UploadManager.clearFiles(); // 获取当前配置 const provider = document.getElementById('providerSelect')?.value || 'newapi'; const model = document.getElementById('modelSelect')?.value || 'gpt-3.5-turbo'; const thinkingMode = document.getElementById('thinkingMode')?.checked || false; // SSE 流式请求 this.isStreaming = true; this.updateSendButton(); try { await this.streamChat(provider, model, thinkingMode); } catch (err) { this.addErrorMessage(err.message); } finally { this.isStreaming = false; this.updateSendButton(); } }, async streamChat(provider, model, thinkingMode) { const token = Storage.getToken(); // 构建消息历史(只取 role 和 content) const messages = this.messages.map(m => ({ role: m.role, content: m.content })); // 添加 AI 消息占位 const assistantEl = this.addAssistantPlaceholder(); let fullContent = ''; let thinkingContent = ''; const response = await fetch('/api/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, body: JSON.stringify({ provider, model, messages, stream: true, thinkingMode }) }); if (!response.ok) { const err = await response.json(); throw new Error(err.message || 'AI 请求失败'); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) { if (!line.startsWith('data: ')) continue; const data = line.slice(6).trim(); if (data === '[DONE]') { continue; } try { const parsed = JSON.parse(data); if (parsed.type === 'error') { throw new Error(parsed.message); } if (parsed.thinking) { thinkingContent += parsed.thinking; this.updateThinkingContent(assistantEl, thinkingContent); } if (parsed.content) { fullContent += parsed.content; this.updateAssistantContent(assistantEl, fullContent); } } catch (e) { if (e.message && !e.message.includes('JSON')) throw e; } } } // 流结束,保存 AI 消息 const assistantMessage = { role: 'assistant', content: fullContent, thinking_content: thinkingContent || null }; this.messages.push(assistantMessage); // 保存到数据库 api.post('/sessions/' + SessionManager.currentSessionId + '/messages', assistantMessage).catch(console.error); // 缓存到 localStorage Storage.setCachedMessages(SessionManager.currentSessionId, this.messages); // 移除打字机光标 this.removeTypingCursor(assistantEl); }, addAssistantPlaceholder() { const container = document.getElementById('messagesContainer'); const empty = container.querySelector('.empty-state'); if (empty) empty.remove(); const el = document.createElement('div'); el.className = 'message assistant'; el.innerHTML = ''; container.appendChild(el); container.scrollTop = container.scrollHeight; return el; }, updateAssistantContent(el, content) { const cursor = el.querySelector('.typing-cursor'); const body = el.querySelector('.markdown-body'); if (body) { body.innerHTML = MarkdownRenderer.render(content); if (cursor) body.appendChild(cursor); } else { const thinking = el.querySelector('.thinking-content, .thinking-toggle'); const md = document.createElement('div'); md.className = 'markdown-body'; md.innerHTML = MarkdownRenderer.render(content); if (cursor) md.appendChild(cursor); if (thinking) { el.appendChild(md); } else { el.innerHTML = ''; el.appendChild(md); } } el.parentElement.scrollTop = el.parentElement.scrollHeight; }, updateThinkingContent(el, content) { let toggle = el.querySelector('.thinking-toggle'); let container = el.querySelector('.thinking-content'); if (!toggle) { toggle = document.createElement('div'); toggle.className = 'thinking-toggle expanded'; toggle.textContent = '思考过程 ▾'; toggle.onclick = function() { container.classList.toggle('expanded'); this.textContent = container.classList.contains('expanded') ? '思考过程 ▴' : '思考过程 ▾'; }; el.insertBefore(toggle, el.firstChild); } if (!container) { container = document.createElement('div'); container.className = 'thinking-content expanded'; el.insertBefore(container, toggle.nextSibling); } container.innerHTML = MarkdownRenderer.render(content); el.parentElement.scrollTop = el.parentElement.scrollHeight; }, removeTypingCursor(el) { const cursor = el.querySelector('.typing-cursor'); if (cursor) cursor.remove(); }, addErrorMessage(message) { const container = document.getElementById('messagesContainer'); const el = document.createElement('div'); el.className = 'message assistant'; el.innerHTML = `
错误: ${this.escapeHtml(message)}
`; container.appendChild(el); container.scrollTop = container.scrollHeight; }, clearMessages() { const container = document.getElementById('messagesContainer'); if (container) { container.innerHTML = '

开始新的对话

输入消息开始聊天

'; } this.messages = []; }, updateSendButton() { const btn = document.getElementById('sendBtn'); if (btn) { btn.disabled = this.isStreaming; btn.textContent = this.isStreaming ? '回复中...' : '发送'; } }, escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } }; // ======================================== // 聊天页面初始化 // ======================================== // 切换侧边栏 function toggleSidebar() { document.getElementById('sidebar').classList.toggle('collapsed'); } // 供应商/模型数据缓存 let providersData = []; let personalitiesData = []; // 加载供应商配置 async function loadProviders() { try { const res = await api.get('/config'); providersData = res.data.providers || []; const select = document.getElementById('providerSelect'); select.innerHTML = ''; providersData.forEach((p, i) => { if (p.enabled) { const option = document.createElement('option'); option.value = i; option.textContent = p.name; select.appendChild(option); } }); // 如果只有一个供应商,自动选择 if (providersData.filter(p => p.enabled).length === 1) { select.value = providersData.findIndex(p => p.enabled); onProviderChange(); } } catch (err) { console.error('加载供应商配置失败:', err); } } // 供应商切换时更新模型列表 function onProviderChange() { const index = document.getElementById('providerSelect').value; const modelSelect = document.getElementById('modelSelect'); modelSelect.innerHTML = ''; if (index !== '' && providersData[index]) { const provider = providersData[index]; (provider.models || []).forEach(m => { const option = document.createElement('option'); option.value = m; option.textContent = m; modelSelect.appendChild(option); }); // 如果只有一个模型,自动选择 if (provider.models && provider.models.length === 1) { modelSelect.value = provider.models[0]; } // 如果有默认模型,自动选择 if (provider.defaultModel) { modelSelect.value = provider.defaultModel; } } } // 加载人格列表 async function loadPersonalities() { try { const res = await api.get('/personalities'); personalitiesData = res.data || []; const select = document.getElementById('personalitySelect'); select.innerHTML = ''; personalitiesData.forEach(p => { const option = document.createElement('option'); option.value = p.id; option.textContent = (p.icon ? p.icon + ' ' : '') + p.name; select.appendChild(option); }); } catch (err) { console.error('加载人格列表失败:', err); } } // 页面初始化 (async function() { // 检查认证 const token = Storage.getToken(); if (!token) { window.location.href = '/login.php'; return; } // 初始化聊天管理器 ChatManager.init(); // 加载数据 await Promise.all([ loadProviders(), loadPersonalities(), SessionManager.loadSessions() ]); // 如果有会话,自动选择第一个 if (SessionManager.sessions.length > 0) { await SessionManager.switchSession(SessionManager.sessions[0].id); } })();