dynamic-interaction
Version:
Dynamic interaction 动态交互mcp,用于cursor、windsurf、trae 等 AI 智能编辑器 Agent 运行时交互使用
300 lines • 11.5 kB
JavaScript
/**
* WebSocket 通信服务
* 处理与服务器的 WebSocket 连接和消息传递
*/
import { WEBSOCKET_CONFIG } from '../config/constants.js';
import { eventBus, APP_EVENTS } from '../core/events.js';
import { notificationService } from './notification.js';
class WebSocketService {
ws;
reconnectAttempts = 0;
reconnectTimeoutId = null;
initialize() {
this.connectWebSocket();
}
sendCommand(command) {
if (this.isConnected()) {
this.ws.send(JSON.stringify({
type: 'command',
data: command
}));
return true;
}
console.warn('WebSocket 未连接,无法发送命令。');
return false;
}
sendFeedback(text, images) {
if (this.isConnected()) {
eventBus.emit(APP_EVENTS.STATUS_CHANGED, { type: 'message', status: 'sending' });
const imageData = images.map(img => img.dataUrl || img.name);
this.ws.send(JSON.stringify({
type: 'submit_feedback',
data: { text, imageData }
}));
setTimeout(() => {
eventBus.emit(APP_EVENTS.STATUS_CHANGED, { type: 'message', status: 'waiting' });
}, 200);
return true;
}
alert('WebSocket 连接已关闭,请刷新页面重试');
return false;
}
connectWebSocket() {
if (this.reconnectTimeoutId) {
clearTimeout(this.reconnectTimeoutId);
this.reconnectTimeoutId = null;
}
this.updateConnectionUI();
this.ws = new WebSocket(`ws://${window.location.host}`);
window.ws = this.ws; // 保持向后兼容
this.setupEventHandlers();
}
setupEventHandlers() {
if (!this.ws)
return;
this.ws.onopen = () => this.handleOpen();
this.ws.onmessage = (event) => this.handleMessage(event);
this.ws.onclose = () => this.handleClose();
this.ws.onerror = (err) => this.handleError(err);
}
handleOpen() {
const summaryDiv = document.getElementById('summary');
summaryDiv.textContent = 'WebSocket 连接已建立,等待 AI 响应...';
this.reconnectAttempts = 0;
eventBus.emit(APP_EVENTS.WS_CONNECTED);
// 发送客户端就绪消息
this.ws.send(JSON.stringify({ type: 'client_ready' }));
this.checkNotificationPermission();
}
handleMessage(event) {
try {
const data = JSON.parse(event.data);
eventBus.emit(APP_EVENTS.WS_MESSAGE_RECEIVED, data);
this.processMessage(data);
}
catch (e) {
console.error('解析 WebSocket 消息时出错:', e);
const summaryDiv = document.getElementById('summary');
summaryDiv.textContent = '收到无效消息';
}
}
handleClose() {
eventBus.emit(APP_EVENTS.WS_DISCONNECTED);
this.scheduleReconnect();
}
handleError(err) {
console.error('WebSocket 错误:', err);
}
processMessage(data) {
const summaryDiv = document.getElementById('summary');
switch (data.type) {
case 'summary':
this.handleSummaryMessage(data, summaryDiv);
break;
case 'server_log':
this.handleServerLogMessage(data);
break;
case 'pong':
this.handlePongMessage(data);
break;
case 'system_info':
this.handleSystemInfoMessage(data);
break;
case 'feedback_status':
this.handleFeedbackStatusMessage(data);
break;
case 'timeout':
this.handleTimeoutMessage();
break;
case 'stop_timer':
this.handleStopTimerMessage();
break;
case 'notification':
this.handleNotificationMessage(data);
break;
case 'session_request':
this.handleSessionRequestMessage(data);
break;
}
}
handleSummaryMessage(data, summaryDiv) {
if (data.data !== undefined && data.data !== null) {
summaryDiv.innerHTML = marked.parse(data.data);
eventBus.emit(APP_EVENTS.STATUS_CHANGED, { type: 'message', status: 'received' });
eventBus.emit(APP_EVENTS.FEEDBACK_SUCCESS);
}
else {
summaryDiv.textContent = 'AI Agent 正在准备摘要...';
console.warn('收到的摘要消息格式不完整,缺少 "data" 字段。', data);
}
}
handleServerLogMessage(data) {
const { level, text } = data.data;
console[level]('%c[Server] ' + text, 'color: grey');
}
handlePongMessage(data) {
if (window.statusBar) {
window.statusBar.handlePong(data.data);
}
}
handleSystemInfoMessage(data) {
if (window.statusBar && data.data) {
const sysInfo = data.data;
window.statusBar.updateSystemInfo(sysInfo);
if (sysInfo.sessionStartTime && sysInfo.leaseTimeoutSeconds && sysInfo.leaseTimeoutSeconds > 0) {
window.statusBar.startSessionTimer(sysInfo.sessionStartTime, sysInfo.leaseTimeoutSeconds);
}
}
}
handleFeedbackStatusMessage(data) {
if (window.statusBar && data.data?.status) {
window.statusBar.updateMessageStatus(data.data.status);
eventBus.emit(APP_EVENTS.FEEDBACK_SUCCESS);
}
}
handleTimeoutMessage() {
if (window.statusBar) {
window.statusBar.updateConnectionStatus('connected');
window.statusBar.stopSessionTimer();
window.statusBar.updateMessageStatus('timeout');
}
// 禁用输入控件
const feedbackInput = document.getElementById('feedback-input');
const sendButton = document.getElementById('send-button');
if (feedbackInput)
feedbackInput.disabled = true;
if (sendButton)
sendButton.disabled = true;
}
handleStopTimerMessage() {
if (window.statusBar) {
window.statusBar.stopSessionTimer();
}
}
handleNotificationMessage(data) {
const notificationData = data.data;
if (!notificationData?.summary)
return;
this.showNotificationPanel(notificationData.summary);
this.hideInteractionPanel();
if (notificationService.isPageHidden()) {
notificationService.showAINotification(notificationData.summary);
}
}
handleSessionRequestMessage(data) {
const sessionData = data.data;
if (!sessionData?.summary)
return;
this.hideNotificationPanel();
this.showInteractionPanel();
const summaryDiv = document.getElementById('summary');
summaryDiv.innerHTML = marked.parse(sessionData.summary);
if (window.statusBar && sessionData.startTime && sessionData.timeoutSeconds) {
window.statusBar.startSessionTimer(sessionData.startTime, sessionData.timeoutSeconds);
}
eventBus.emit(APP_EVENTS.FEEDBACK_SUCCESS);
if (notificationService.isPageHidden()) {
notificationService.showSessionRequestNotification(sessionData.summary);
}
}
showNotificationPanel(summary) {
const notificationPanel = document.getElementById('notification-panel');
if (notificationPanel) {
notificationPanel.style.display = 'block';
notificationPanel.innerHTML = this.createNotificationHTML(summary);
if (window.lucide) {
window.lucide.createIcons();
}
}
}
hideInteractionPanel() {
const feedbackPanel = document.getElementById('feedback-panel');
if (feedbackPanel) {
feedbackPanel.style.display = 'none';
}
}
hideNotificationPanel() {
const notificationPanel = document.getElementById('notification-panel');
if (notificationPanel) {
notificationPanel.style.display = 'none';
}
}
showInteractionPanel() {
const feedbackPanel = document.getElementById('feedback-panel');
if (feedbackPanel) {
feedbackPanel.style.display = 'block';
}
}
createNotificationHTML(summary) {
return `
<div class="notification-display">
<div class="notification-header">
<div class="notification-title">
<i data-lucide="bell" class="icon notification-icon"></i>
<span>AI 通知</span>
<span class="notification-badge">通知模式</span>
</div>
</div>
<hr class="notification-divider">
<div class="notification-content">
<div class="notification-summary">${marked.parse(summary)}</div>
<div class="notification-meta">
<div class="notification-timestamp">
<i data-lucide="clock" class="icon"></i>
<span>刚刚</span>
</div>
<div class="notification-actions">
<button class="acknowledge-btn" onclick="acknowledgeNotification()">
<i data-lucide="check" class="icon"></i>
已知晓
</button>
</div>
</div>
</div>
</div>
`;
}
scheduleReconnect() {
if (this.reconnectAttempts >= WEBSOCKET_CONFIG.MAX_RECONNECT_ATTEMPTS) {
console.error('已达到最大重连次数,停止重连。');
const summaryDiv = document.getElementById('summary');
summaryDiv.textContent = '无法连接到服务器,请检查网络并刷新页面。';
summaryDiv.style.color = 'red';
return;
}
const delay = Math.min(WEBSOCKET_CONFIG.INITIAL_RECONNECT_DELAY * Math.pow(2, this.reconnectAttempts), WEBSOCKET_CONFIG.MAX_RECONNECT_DELAY);
const summaryDiv = document.getElementById('summary');
summaryDiv.textContent = '连接已断开,正在尝试重新连接...';
summaryDiv.style.color = '#ff9500';
this.reconnectAttempts++;
this.reconnectTimeoutId = window.setTimeout(() => this.connectWebSocket(), delay);
}
updateConnectionUI() {
if (this.reconnectAttempts > 0 && window.statusBar) {
window.statusBar.updateConnectionStatus('reconnecting');
}
const summaryDiv = document.getElementById('summary');
summaryDiv.textContent = '正在连接 WebSocket...';
summaryDiv.style.color = 'inherit';
}
async checkNotificationPermission() {
if (notificationService.checkSupport()) {
const permission = notificationService.getPermissionStatus();
if (permission === 'default') {
await notificationService.requestPermission();
}
}
}
isConnected() {
return this.ws?.readyState === WebSocket.OPEN;
}
}
export const webSocketService = new WebSocketService();
// 全局函数保持向后兼容
window.acknowledgeNotification = function () {
const notificationPanel = document.getElementById('notification-panel');
if (notificationPanel) {
notificationPanel.style.display = 'none';
}
};
//# sourceMappingURL=websocket.js.map