@songm_d/standalone-toolbar-service
Version:
独立的Stagewise工具栏服务 - 支持SRPC通信和WebSocket广播,可与MCP反馈收集器集成
409 lines (408 loc) • 16.6 kB
JavaScript
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'
};
}
}