273 lines
6.9 KiB
JavaScript
273 lines
6.9 KiB
JavaScript
import WebSocket from 'ws';
|
|
import dotenv from 'dotenv';
|
|
import bridgeManager from '../bridge-manager/index.js';
|
|
import jwt from 'jsonwebtoken';
|
|
|
|
dotenv.config();
|
|
|
|
class MDWebSocketClient {
|
|
constructor() {
|
|
this.ws = null;
|
|
this.connected = false;
|
|
this.reconnectAttempts = 0;
|
|
this.tokenPushInterval = null;
|
|
this.heartbeatInterval = null;
|
|
this.serverUrl = process.env.TASK_QUEUE_WS_URL || 'ws://localhost:8087';
|
|
this.jwtSecret = process.env.JWT_SECRET || 'comfyui-cluster-bridge-secret-key-2024';
|
|
this.taskQueueToken = process.env.TASK_QUEUE_TOKEN || '1Ag9BJJn0rXDnidCyXqu';
|
|
}
|
|
|
|
async init() {
|
|
console.log('[MDWebSocketClient] 初始化 WebSocket 客户端');
|
|
bridgeManager.onBridgeChange(() => {
|
|
if (this.connected) {
|
|
this.pushCapacityState();
|
|
}
|
|
});
|
|
await this.connect();
|
|
}
|
|
|
|
async connect() {
|
|
return new Promise((resolve, reject) => {
|
|
const urlWithParams = `${this.serverUrl}?token=${this.taskQueueToken}&id=message-dispatcher`;
|
|
console.log(`[MDWebSocketClient] 正在连接到 ${urlWithParams}`);
|
|
|
|
this.ws = new WebSocket(urlWithParams);
|
|
|
|
this.ws.on('open', () => {
|
|
console.log('[MDWebSocketClient] WebSocket 连接已建立');
|
|
this.connected = true;
|
|
this.reconnectAttempts = 0;
|
|
|
|
this.pushJwtToken();
|
|
this.pushCapacityState();
|
|
|
|
this.startHeartbeat();
|
|
this.startTokenPushTimer();
|
|
|
|
resolve();
|
|
});
|
|
|
|
this.ws.on('message', (data) => {
|
|
this.handleMessage(data);
|
|
});
|
|
|
|
this.ws.on('close', (code, reason) => {
|
|
console.log(`[MDWebSocketClient] WebSocket 连接已关闭 (code: ${code})`);
|
|
this.connected = false;
|
|
this.clearIntervals();
|
|
this.scheduleReconnect();
|
|
});
|
|
|
|
this.ws.on('error', (error) => {
|
|
// console.error('[MDWebSocketClient] WebSocket 连接错误:', error);
|
|
this.connected = false;
|
|
});
|
|
});
|
|
}
|
|
|
|
disconnect() {
|
|
console.log('[MDWebSocketClient] 断开 WebSocket 连接');
|
|
this.clearIntervals();
|
|
if (this.ws) {
|
|
this.ws.close();
|
|
this.ws = null;
|
|
}
|
|
this.connected = false;
|
|
}
|
|
|
|
clearIntervals() {
|
|
if (this.tokenPushInterval) {
|
|
clearInterval(this.tokenPushInterval);
|
|
this.tokenPushInterval = null;
|
|
}
|
|
if (this.heartbeatInterval) {
|
|
clearInterval(this.heartbeatInterval);
|
|
this.heartbeatInterval = null;
|
|
}
|
|
}
|
|
|
|
scheduleReconnect() {
|
|
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
|
|
this.reconnectAttempts++;
|
|
console.log(`[MDWebSocketClient] ${delay}ms 后尝试重连 (第 ${this.reconnectAttempts} 次)`);
|
|
|
|
setTimeout(() => {
|
|
this.connect().catch(err => {
|
|
console.error('[MDWebSocketClient] 重连失败:', err);
|
|
});
|
|
}, delay);
|
|
}
|
|
|
|
pushJwtToken() {
|
|
const token = this.generateJwtToken();
|
|
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
|
|
|
|
const message = {
|
|
type: 'JWT_UPDATE',
|
|
data: {
|
|
token,
|
|
expiresAt,
|
|
timestamp: new Date().toISOString()
|
|
}
|
|
};
|
|
|
|
this.send(message);
|
|
console.log('[MDWebSocketClient] 已推送 JWT Token');
|
|
}
|
|
|
|
generateJwtToken() {
|
|
return jwt.sign(
|
|
{
|
|
sub: 'message-dispatcher',
|
|
iss: 'comfyui-bridge',
|
|
iat: Math.floor(Date.now() / 1000)
|
|
},
|
|
this.jwtSecret,
|
|
{ expiresIn: '24h' }
|
|
);
|
|
}
|
|
|
|
pushCapacityState(pendingCount = 0) {
|
|
const bridges = bridgeManager.getAllBridges();
|
|
const summary = this.calculateCapacitySummary(bridges, pendingCount);
|
|
|
|
const message = {
|
|
type: 'CAPACITY_UPDATE',
|
|
data: {
|
|
timestamp: new Date().toISOString(),
|
|
bridges: bridges.map(b => ({
|
|
bridgeId: b.id,
|
|
info: b.info
|
|
})),
|
|
summary
|
|
}
|
|
};
|
|
|
|
this.send(message);
|
|
console.log('[MDWebSocketClient] 已推送算力状态:', summary);
|
|
}
|
|
|
|
calculateCapacitySummary(bridges, pendingCount = 0) {
|
|
let totalInstances = 0;
|
|
let onlineInstances = 0;
|
|
let lockedInstances = 0;
|
|
let offlineInstances = 0;
|
|
|
|
for (const bridge of bridges) {
|
|
if (bridge.info?.instances) {
|
|
totalInstances += bridge.info.instances.length;
|
|
for (const instance of bridge.info.instances) {
|
|
if (instance.status === 'online') {
|
|
onlineInstances++;
|
|
if (bridgeManager.isInstanceLocked(instance.id)) {
|
|
lockedInstances++;
|
|
}
|
|
} else {
|
|
offlineInstances++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const busyInstances = lockedInstances;
|
|
const availableCapacity = Math.max(0, onlineInstances - busyInstances - pendingCount);
|
|
|
|
return {
|
|
totalBridges: bridges.length,
|
|
totalInstances,
|
|
onlineInstances,
|
|
busyInstances,
|
|
offlineInstances,
|
|
lockedInstances,
|
|
availableCapacity
|
|
};
|
|
}
|
|
|
|
pushInstanceOnline(instanceId, bridgeId) {
|
|
const message = {
|
|
type: 'INSTANCE_ONLINE',
|
|
data: {
|
|
instanceId,
|
|
bridgeId,
|
|
timestamp: new Date().toISOString()
|
|
}
|
|
};
|
|
this.send(message);
|
|
console.log(`[MDWebSocketClient] 已推送实例上线: ${instanceId}`);
|
|
}
|
|
|
|
pushInstanceOffline(instanceId, bridgeId) {
|
|
const message = {
|
|
type: 'INSTANCE_OFFLINE',
|
|
data: {
|
|
instanceId,
|
|
bridgeId,
|
|
timestamp: new Date().toISOString()
|
|
}
|
|
};
|
|
this.send(message);
|
|
console.log(`[MDWebSocketClient] 已推送实例下线: ${instanceId}`);
|
|
}
|
|
|
|
send(message) {
|
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
this.ws.send(JSON.stringify(message));
|
|
} else {
|
|
console.warn('[MDWebSocketClient] WebSocket 未连接,无法发送消息');
|
|
}
|
|
}
|
|
|
|
handleMessage(data) {
|
|
const messageStr = data.toString();
|
|
|
|
if (messageStr === 'please give me tasks') {
|
|
console.log('[MDWebSocketClient] 收到任务请求');
|
|
return;
|
|
}
|
|
|
|
if (messageStr === 'ping') {
|
|
this.ws.send('pong');
|
|
return;
|
|
}
|
|
|
|
if (messageStr === 'pong') {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const message = JSON.parse(messageStr);
|
|
// console.log(`[MDWebSocketClient] 收到消息: ${message.type}`);
|
|
|
|
switch (message.type) {
|
|
case 'HEARTBEAT_ACK':
|
|
break;
|
|
default:
|
|
console.log('[MDWebSocketClient] 未知消息类型:', message.type);
|
|
}
|
|
} catch (error) {
|
|
console.error('[MDWebSocketClient] 解析消息失败:', error);
|
|
}
|
|
}
|
|
|
|
startHeartbeat() {
|
|
this.heartbeatInterval = setInterval(() => {
|
|
const message = {
|
|
type: 'HEARTBEAT',
|
|
data: {
|
|
timestamp: new Date().toISOString()
|
|
}
|
|
};
|
|
this.send(message);
|
|
}, 30000);
|
|
}
|
|
|
|
startTokenPushTimer() {
|
|
this.tokenPushInterval = setInterval(() => {
|
|
this.pushJwtToken();
|
|
}, 20 * 60 * 60 * 1000);
|
|
}
|
|
}
|
|
|
|
export default new MDWebSocketClient();
|