UNPKG

@songm_d/standalone-toolbar-service

Version:

独立的Stagewise工具栏服务 - 支持SRPC通信和WebSocket广播,可与MCP反馈收集器集成

409 lines (408 loc) 16.6 kB
import cors from 'cors'; import express from 'express'; import { createServer } from 'http'; import { WebSocketServer } from 'ws'; import { createSRPCBridge, ToolbarRPCHandler } from '../toolbar/index.js'; import { logger } from '../utils/logger.js'; import { SimplePortManager } from '../utils/port-manager.js'; export class ToolbarServer { constructor() { this.port = 5748; this.isServerRunning = false; this.srpcBridge = null; this.toolbarRPCHandler = null; this.broadcastPort = 15749; this.broadcastWss = null; this.clients = new Map(); this.latestPrompt = null; this.portManager = new SimplePortManager(); this.app = express(); this.server = createServer(this.app); this.broadcastApp = express(); this.broadcastServer = createServer(this.broadcastApp); this.setupMiddleware(); this.setupRoutes(); this.setupBroadcastRoutes(); } setupMiddleware() { this.app.use(cors({ origin: '*', methods: ['GET', 'POST'], allowedHeaders: ['Content-Type'], })); this.app.use(express.json()); this.app.use((req, res, next) => { const start = Date.now(); res.on('finish', () => { const duration = Date.now() - start; logger.request(req.method, req.url, res.statusCode, duration); }); next(); }); } setupRoutes() { this.app.get('/ping/stagewise', (req, res) => { logger.info('[Toolbar] Ping request received'); res.send('stagewise'); }); this.app.get('/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString(), service: 'standalone-toolbar-service', port: this.port, srpcConnected: this.srpcBridge?.isConnected() || false, broadcastClients: this.clients.size, version: '1.0.0' }); }); this.app.get('/api/toolbar/status', (req, res) => { const toolbarStatus = { enabled: true, srpcConnected: this.srpcBridge?.isConnected() || false, registeredMethods: this.srpcBridge?.getRegisteredMethods() || [], service: 'standalone-toolbar-service', timestamp: new Date().toISOString(), port: this.port, uptime: process.uptime(), broadcastClients: this.clients.size, latestPromptTime: this.latestPrompt?.timestamp }; logger.info('[Toolbar] Status requested:', toolbarStatus); res.json(toolbarStatus); }); this.app.get('/api/latest-prompt', (req, res) => { if (this.latestPrompt) { res.json({ success: true, data: this.latestPrompt }); } else { res.json({ success: false, message: 'No prompt available' }); } }); this.app.get('/api/clients', (req, res) => { const clientList = Array.from(this.clients.values()).map(client => ({ id: client.id, connected: client.connected, lastActivity: client.lastActivity })); res.json({ success: true, data: { clients: clientList, total: clientList.length } }); }); this.app.use((req, res) => { res.status(404).json({ error: 'Not found' }); }); this.app.use((err, req, res, next) => { logger.error('[Toolbar] Server error:', err); res.status(500).json({ error: 'Internal server error' }); }); } setupBroadcastRoutes() { this.broadcastApp.use(cors({ origin: '*', methods: ['GET', 'POST'], allowedHeaders: ['Content-Type'], })); this.broadcastApp.use(express.json()); this.broadcastApp.get('/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString(), service: 'standalone-toolbar-broadcast-service', port: this.broadcastPort, clients: this.clients.size, version: '1.0.0' }); }); this.broadcastApp.get('/api/broadcast/status', (req, res) => { const broadcastStatus = { enabled: true, clients: this.clients.size, service: 'standalone-toolbar-broadcast-service', timestamp: new Date().toISOString(), port: this.broadcastPort, uptime: process.uptime(), latestPromptTime: this.latestPrompt?.timestamp }; logger.info('[Toolbar Broadcast] Status requested:', broadcastStatus); res.json(broadcastStatus); }); this.broadcastApp.use((req, res) => { res.status(404).json({ error: 'Not found' }); }); this.broadcastApp.use((err, req, res, next) => { logger.error('[Toolbar Broadcast] Server error:', err); res.status(500).json({ error: 'Internal server error' }); }); } setupWebSocketBroadcast() { this.broadcastWss = new WebSocketServer({ server: this.broadcastServer, path: '/broadcast' }); this.broadcastWss.on('connection', (ws, req) => { const clientId = `client_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const client = { id: clientId, ws: ws, connected: true, lastActivity: new Date() }; this.clients.set(clientId, client); logger.info(`[Toolbar Broadcast] WebSocket client connected: ${clientId}, total clients: ${this.clients.size}`); if (this.latestPrompt) { this.sendToClient(client, 'prompt_received', this.latestPrompt); } this.sendToClient(client, 'welcome', { clientId, service: 'standalone-toolbar-broadcast-service', version: '1.0.0', timestamp: Date.now() }); ws.on('message', (data) => { try { const message = JSON.parse(data.toString()); logger.socket('message_received', clientId, message); client.lastActivity = new Date(); if (message.type === 'ping') { this.sendToClient(client, 'pong', { timestamp: Date.now() }); } } catch (error) { logger.error(`[Toolbar Broadcast] Error parsing message from client ${clientId}:`, error); } }); ws.on('close', () => { client.connected = false; this.clients.delete(clientId); logger.info(`[Toolbar Broadcast] WebSocket client disconnected: ${clientId}, remaining clients: ${this.clients.size}`); }); ws.on('error', (error) => { logger.error(`[Toolbar Broadcast] WebSocket error for client ${clientId}:`, error); client.connected = false; this.clients.delete(clientId); }); }); logger.info('[Toolbar Broadcast] WebSocket broadcast server initialized'); } sendToClient(client, event, data) { if (client.connected && client.ws.readyState === client.ws.OPEN) { try { const message = JSON.stringify({ event, data, timestamp: Date.now() }); client.ws.send(message); logger.socket('message_sent', client.id, { event, dataSize: JSON.stringify(data).length }); } catch (error) { logger.error(`[Toolbar] Error sending message to client ${client.id}:`, error); client.connected = false; this.clients.delete(client.id); } } } broadcastToAllClients(event, data) { const connectedClients = Array.from(this.clients.values()).filter(client => client.connected); logger.info(`[Toolbar] Broadcasting ${event} to ${connectedClients.length} clients`); connectedClients.forEach(client => { this.sendToClient(client, event, data); }); this.cleanupDisconnectedClients(); } cleanupDisconnectedClients() { const disconnectedClients = []; this.clients.forEach((client, clientId) => { if (!client.connected || client.ws.readyState !== client.ws.OPEN) { disconnectedClients.push(clientId); } }); disconnectedClients.forEach(clientId => { this.clients.delete(clientId); logger.debug(`[Toolbar] Cleaned up disconnected client: ${clientId}`); }); } async broadcastPromptToClients(promptData) { this.latestPrompt = { ...promptData, timestamp: Date.now() }; this.broadcastToAllClients('prompt_received', this.latestPrompt); logger.toolbar('prompt_broadcasted', { sessionId: promptData.sessionId, clientCount: this.clients.size, promptLength: promptData.prompt.length }); return { success: true, clientCount: this.clients.size, timestamp: this.latestPrompt.timestamp, sessionId: promptData.sessionId }; } setupSRPCHandlers() { try { this.srpcBridge = createSRPCBridge(this.server); if (this.srpcBridge) { this.toolbarRPCHandler = new ToolbarRPCHandler(this.srpcBridge, this.broadcastPromptToClients.bind(this)); logger.debug('[Toolbar] ✅ SRPC WebSocket 桥接器初始化成功'); logger.debug('[Toolbar] ✅ Toolbar RPC 处理器初始化成功'); const registeredMethods = this.srpcBridge.getRegisteredMethods(); logger.debug(`[Toolbar] 📋 已注册的 RPC 方法: ${registeredMethods.join(', ')}`); } } catch (error) { logger.error('[Toolbar] ❌ SRPC 初始化失败:', error); } } async start() { if (this.isServerRunning) { logger.warn('[Toolbar] 服务器已在运行中'); return; } try { logger.debug('[Toolbar] 检查所有端口可用性...'); const portStatus = await this.portManager.checkAllPorts(); if (!portStatus.allAvailable) { const errors = []; if (!portStatus.toolbarPort) { errors.push(`SRPC端口 ${this.port} 不可用`); } if (!portStatus.broadcastPort) { errors.push(`广播端口 ${this.broadcastPort} 不可用`); } throw new Error(`端口检查失败: ${errors.join(', ')}`); } this.port = await this.portManager.getToolbarPort(); logger.debug(`[Toolbar] 准备在端口 ${this.port} 启动SRPC服务器...`); await new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('SRPC server start timeout')); }, 10000); this.server.listen(this.port, (error) => { clearTimeout(timeout); if (error) { reject(error); } else { resolve(); } }); }); this.setupSRPCHandlers(); logger.info(`[Toolbar] ✅ SRPC服务器启动成功: http://localhost:${this.port}`); logger.info(`[Toolbar] 📡 SRPC WebSocket端点: ws://localhost:${this.port}`); this.broadcastPort = await this.portManager.getBroadcastPort(); logger.debug(`[Toolbar] 准备在端口 ${this.broadcastPort} 启动广播服务器...`); await new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('Broadcast server start timeout')); }, 10000); this.broadcastServer.listen(this.broadcastPort, (error) => { clearTimeout(timeout); if (error) { reject(error); } else { resolve(); } }); }); this.setupWebSocketBroadcast(); this.isServerRunning = true; logger.info(`[Toolbar] ✅ 广播服务器启动成功: http://localhost:${this.broadcastPort}`); logger.info(`[Toolbar] 🔄 广播WebSocket端点: ws://localhost:${this.broadcastPort}/broadcast`); logger.info(`[Toolbar] 🔍 Ping端点: http://localhost:${this.port}/ping/stagewise`); logger.info(`[Toolbar] ❤️ 健康检查: http://localhost:${this.port}/health`); logger.info(`[Toolbar] ❤️ 广播健康检查: http://localhost:${this.broadcastPort}/health`); } catch (error) { logger.error('[Toolbar] 服务器启动失败:', error); throw new Error(`Failed to start toolbar server: ${error}`); } } async stop() { if (!this.isServerRunning) { return; } const currentPort = this.port; const currentBroadcastPort = this.broadcastPort; logger.debug(`[Toolbar] 正在停止服务器 (SRPC端口: ${currentPort}, 广播端口: ${currentBroadcastPort})...`); try { if (this.broadcastWss) { this.broadcastWss.close(); this.broadcastWss = null; } this.clients.forEach(client => { if (client.connected) { client.ws.close(); } }); this.clients.clear(); if (this.srpcBridge) { this.srpcBridge.close(); this.srpcBridge = null; } await new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('Broadcast server close timeout')); }, 5000); this.broadcastServer.close((error) => { clearTimeout(timeout); if (error) { reject(error); } else { resolve(); } }); }); await new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('SRPC server close timeout')); }, 5000); this.server.close((error) => { clearTimeout(timeout); if (error) { reject(error); } else { resolve(); } }); }); this.isServerRunning = false; logger.info(`[Toolbar] ✅ 独立Toolbar服务器已停止 (SRPC端口: ${currentPort}, 广播端口: ${currentBroadcastPort})`); } catch (error) { logger.error('[Toolbar] 停止服务器时出错:', error); throw error; } } isRunning() { return this.isServerRunning; } getPort() { return this.port; } getToolbarStatus() { return { enabled: this.srpcBridge !== null, connected: this.srpcBridge?.isConnected() || false, registeredMethods: this.srpcBridge?.getRegisteredMethods() || [], rpcHandlerActive: this.toolbarRPCHandler !== null, port: this.port, running: this.isServerRunning, broadcastClients: this.clients.size, service: 'standalone-toolbar-service', version: '1.0.0' }; } }