// OpenCode 前端应用 class OpenCodeApp { constructor() { this.baseURL = window.location.origin; this.currentSessionId = null; this.sessions = []; this.streamMode = true; this.logEventSource = null; this.userId = this.generateUserId(); this.initElements(); this.bindEvents(); this.checkHealth(); this.loadSessions(); this.startLogStream(); } generateUserId() { // 生成或获取存储的用户ID let userId = localStorage.getItem('opencode_user_id'); if (!userId) { userId = 'user_' + Math.random().toString(36).substr(2, 9); localStorage.setItem('opencode_user_id', userId); } return userId; } initElements() { this.elements = { createSessionBtn: document.getElementById('createSessionBtn'), refreshSessionsBtn: document.getElementById('refreshSessionsBtn'), sessionList: document.getElementById('sessionList'), currentSessionTitle: document.getElementById('currentSessionTitle'), chatMessages: document.getElementById('chatMessages'), messageInput: document.getElementById('messageInput'), sendMessageBtn: document.getElementById('sendMessageBtn'), clearChatBtn: document.getElementById('clearChatBtn'), exportChatBtn: document.getElementById('exportChatBtn'), streamToggleBtn: document.getElementById('streamToggleBtn'), toggleLogsBtn: document.getElementById('toggleLogsBtn'), logsPanel: document.getElementById('logsPanel'), logEntries: document.getElementById('logEntries'), statusText: document.getElementById('statusText'), portInfo: document.getElementById('portInfo') }; } bindEvents() { this.elements.createSessionBtn.addEventListener('click', () => this.createSession()); this.elements.refreshSessionsBtn.addEventListener('click', () => this.loadSessions()); this.elements.sendMessageBtn.addEventListener('click', () => this.sendMessage()); this.elements.clearChatBtn.addEventListener('click', () => this.clearChat()); this.elements.exportChatBtn.addEventListener('click', () => this.exportChat()); this.elements.streamToggleBtn.addEventListener('click', () => this.toggleStreamMode()); this.elements.toggleLogsBtn.addEventListener('click', () => this.toggleLogs()); this.elements.messageInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); this.sendMessage(); } this.elements.sendMessageBtn.disabled = !this.elements.messageInput.value.trim(); }); this.elements.messageInput.addEventListener('input', () => { this.elements.sendMessageBtn.disabled = !this.elements.messageInput.value.trim(); }); } async checkHealth() { try { const response = await fetch('/api/health'); const result = await response.json(); if (result.success) { this.elements.statusText.textContent = '连接正常'; this.elements.portInfo.textContent = `端口: ${result.data.opencode_port}`; console.log('服务状态:', result.data); } else { this.elements.statusText.textContent = '服务异常'; } } catch (error) { this.elements.statusText.textContent = '连接失败'; console.error('健康检查失败:', error); } } async createSession() { const title = prompt('请输入会话标题:', '新会话 ' + new Date().toLocaleTimeString()); if (!title) return; try { const response = await fetch('/api/session/create', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title }) }); const result = await response.json(); if (result.success) { this.showNotification('会话创建成功'); this.loadSessions(); this.selectSession(result.data.id, result.data.title); } else { this.showNotification('会话创建失败: ' + result.message, 'error'); } } catch (error) { this.showNotification('请求失败: ' + error.message, 'error'); } } async loadSessions() { try { const response = await fetch('/api/session/list'); const result = await response.json(); if (result.success) { this.sessions = result.data; this.renderSessionList(); } } catch (error) { console.error('加载会话列表失败:', error); } } renderSessionList() { const list = this.elements.sessionList; list.innerHTML = ''; if (this.sessions.length === 0) { list.innerHTML = '
暂无会话
'; return; } this.sessions.forEach(session => { const item = document.createElement('li'); item.className = 'session-item'; if (this.currentSessionId === session.id) { item.classList.add('active'); } item.innerHTML = `
${this.escapeHtml(session.title)}
${session.id}
`; item.addEventListener('click', () => { this.selectSession(session.id, session.title); }); list.appendChild(item); }); } selectSession(sessionId, sessionTitle) { this.currentSessionId = sessionId; this.elements.currentSessionTitle.textContent = sessionTitle; this.renderSessionList(); this.clearChat(); this.addMessage('system', `已切换到会话: ${sessionTitle}`); this.elements.messageInput.disabled = false; this.elements.sendMessageBtn.disabled = !this.elements.messageInput.value.trim(); } async sendMessage() { const message = this.elements.messageInput.value.trim(); if (!message || !this.currentSessionId) return; // 添加用户消息 this.addMessage('user', message); this.elements.messageInput.value = ''; this.elements.sendMessageBtn.disabled = true; if (this.streamMode) { await this.sendMessageStream(message); } else { await this.sendMessageSync(message); } } async sendMessageSync(message) { try { const response = await fetch('/api/prompt/sync', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionID: this.currentSessionId, parts: [{ type: 'text', text: message }], agent: 'opencode' }) }); const result = await response.json(); if (result.success) { const assistantMessage = result.data.info; this.addMessage('assistant', assistantMessage.content || '收到响应'); } else { this.addMessage('system', '错误: ' + result.message); } } catch (error) { this.addMessage('system', '请求失败: ' + error.message); } } async sendMessageStream(message) { try { const response = await fetch('/api/prompt/stream', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionID: this.currentSessionId, parts: [{ type: 'text', text: message }], agent: 'opencode' }) }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; // 添加助理消息容器 const messageId = 'msg_' + Date.now(); const messageElement = this.addMessage('assistant', '', messageId); const contentElement = messageElement.querySelector('.message-content'); 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: ')) { const data = line.slice(6); if (data === '[DONE]') break; try { const parsed = JSON.parse(data); // 这里根据实际的流式响应格式调整 const text = parsed.text || parsed.content || parsed.data || ''; contentElement.innerHTML += this.escapeHtml(text); this.scrollToBottom(); } catch (e) { // 可能是纯文本 contentElement.innerHTML += this.escapeHtml(data); this.scrollToBottom(); } } } } if (buffer.startsWith('data: ')) { const data = buffer.slice(6); if (data !== '[DONE]') { contentElement.innerHTML += this.escapeHtml(data); } } } catch (error) { this.addMessage('system', '流式请求失败: ' + error.message); } } addMessage(role, content, id = null) { const messagesDiv = this.elements.chatMessages; const messageId = id || 'msg_' + Date.now(); const messageDiv = document.createElement('div'); messageDiv.className = `message ${role}`; messageDiv.id = messageId; const time = new Date().toLocaleTimeString(); const roleName = role === 'user' ? '您' : role === 'assistant' ? 'OpenCode' : '系统'; messageDiv.innerHTML = `
${roleName} ${time}
${this.escapeHtml(content)}
`; messagesDiv.appendChild(messageDiv); this.scrollToBottom(); return messageDiv; } clearChat() { this.elements.chatMessages.innerHTML = ''; } exportChat() { const messages = this.elements.chatMessages.querySelectorAll('.message'); let exportText = `OpenCode 对话记录\n会话: ${this.elements.currentSessionTitle.textContent}\n时间: ${new Date().toLocaleString()}\n\n`; messages.forEach(msg => { const role = msg.classList.contains('user') ? '用户' : msg.classList.contains('assistant') ? 'OpenCode' : '系统'; const time = msg.querySelector('.message-time').textContent; const content = msg.querySelector('.message-content').textContent; exportText += `[${time}] ${role}: ${content}\n\n`; }); const blob = new Blob([exportText], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `opencode-chat-${new Date().toISOString().slice(0,10)}.txt`; a.click(); URL.revokeObjectURL(url); this.showNotification('对话已导出'); } toggleStreamMode() { this.streamMode = !this.streamMode; this.elements.streamToggleBtn.textContent = `流式模式: ${this.streamMode ? '开启' : '关闭'}`; this.showNotification(`流式模式 ${this.streamMode ? '已开启' : '已关闭'}`); } toggleLogs() { this.elements.logsPanel.classList.toggle('hidden'); if (!this.elements.logsPanel.classList.contains('hidden')) { this.scrollLogsToBottom(); } } startLogStream() { if (this.logEventSource) { this.logEventSource.close(); } this.logEventSource = new EventSource('/api/logs/stream'); this.logEventSource.onmessage = (event) => { this.addLogEntry(event.data); }; this.logEventSource.onerror = (error) => { console.error('日志流错误:', error); setTimeout(() => this.startLogStream(), 5000); }; } addLogEntry(log) { const entry = document.createElement('div'); entry.className = 'log-entry'; const time = new Date().toLocaleTimeString(); let source = 'system'; let message = log; // 解析日志格式 [source] message const match = log.match(/^\[(\w+)\]\s+(.+)$/); if (match) { source = match[1]; message = match[2]; } entry.innerHTML = ` ${time} ${source} ${this.escapeHtml(message)} `; this.elements.logEntries.appendChild(entry); // 限制日志条目数量 const maxEntries = 100; const entries = this.elements.logEntries.querySelectorAll('.log-entry'); if (entries.length > maxEntries) { entries[0].remove(); } this.scrollLogsToBottom(); } scrollToBottom() { this.elements.chatMessages.scrollTop = this.elements.chatMessages.scrollHeight; } scrollLogsToBottom() { this.elements.logsPanel.scrollTop = this.elements.logsPanel.scrollHeight; } showNotification(message, type = 'success') { // 简单的通知实现 console.log(`通知 [${type}]: ${message}`); alert(message); // 实际应用中应该使用更优雅的通知方式 } escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } } // 初始化应用 window.addEventListener('DOMContentLoaded', () => { window.opencodeApp = new OpenCodeApp(); });