| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412 |
- // 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 = '<div class="session-item" style="text-align: center; color: #64748b;">暂无会话</div>';
- 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 = `
- <div class="session-title">${this.escapeHtml(session.title)}</div>
- <div class="session-id">${session.id}</div>
- `;
-
- 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 = `
- <div class="message-header">
- <span class="message-role">${roleName}</span>
- <span class="message-time">${time}</span>
- </div>
- <div class="message-content">${this.escapeHtml(content)}</div>
- `;
-
- 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 = `
- <span class="log-time">${time}</span>
- <span class="log-source">${source}</span>
- <span class="log-message">${this.escapeHtml(message)}</span>
- `;
-
- 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();
- });
|