// 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 = `
${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();
});