shuzhiren-comfyui/任务队列后端/app.js

369 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import express, { json, urlencoded } from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import WebSocket, { WebSocketServer } from 'ws';
import { Worker } from 'worker_threads';
import { checkUsertoken } from './school/api.js';
import redis from './redis/index.js';
import initQueue from './redis/initQueue.js';
import messagePersistence from './redis/messagePersistence.js';
import code from './config/code.json' with { type: 'json' };
import fileRouter from './upload/index.js';
import recordRouter from './outside/callback.js';
import mdWebSocketServer from './utils/mdWebSocketServer.js';
dotenv.config();
const logger = {
info: (message) => {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] INFO: ${message}`);
},
error: (message, error) => {
const timestamp = new Date().toISOString();
console.error(`[${timestamp}] ERROR: ${message}`, error || '');
},
debug: (message) => {
const timestamp = new Date().toISOString();
console.debug(`[${timestamp}] DEBUG: ${message}`);
}
};
let wss = null;
const workers = [];
const socketMap = new Map();
function createWorker(scriptPath) {
const worker = new Worker(scriptPath);
worker.setMaxListeners(20);
worker.on('error', (error) => {
logger.error(`Worker ${scriptPath} 错误:`, error);
});
worker.on('exit', (code) => {
if (code !== 0) {
logger.error(`Worker ${scriptPath} 异常退出,退出码: ${code}`);
}
});
workers.push(worker);
return worker;
}
const assessment = createWorker('./worker_threads/assessment/assessment.js');
const wait = createWorker('./worker_threads/wait/waiting.js');
const polling = createWorker('./worker_threads/process/process.js');
const result = createWorker('./worker_threads/result/result.js');
const callback_timeout = createWorker('./worker_threads/callback_timeout/callbackTimeout.js');
const callback_result = createWorker('./worker_threads/callback_result/result.js');
const error = createWorker('./worker_threads/error/error.js');
async function sendMessageToClient(id, message, close = false, closeCode = 1000, closeReason = '') {
let socket;
if (typeof id === 'string' && id) {
socket = socketMap.get(id);
}
if (socket && socket.readyState === WebSocket.OPEN && message) {
try {
socket.send(message);
const messagePreview = typeof message === 'string' ? message.slice(0, 50) : JSON.stringify(message).slice(0, 50);
logger.debug(`成功发送消息到客户端id: ${id}, 消息: ${messagePreview}...`);
if (close) {
socket.close(closeCode, closeReason);
}
return true;
} catch (error) {
logger.error(`发送消息给客户端失败id: ${id}`, error);
return false;
}
} else {
if (!message) {
logger.debug(`消息为空无法发送id: ${id}`);
return false;
} else {
logger.debug(`未找到目标客户端或连接已关闭保存消息到待发送队列id: ${id}`);
try {
await messagePersistence.savePendingMessage(id, message);
logger.info(`消息已保存到待发送队列,等待重试: backendId=${id}`);
return false;
} catch (error) {
logger.error(`保存待发送消息失败: backendId=${id}`, error);
return false;
}
}
}
}
function createWebSocketServer(server) {
wss = new WebSocketServer({
server,
verifyClient: async (info, callback) => {
try {
const urlParams = new URLSearchParams(info.req.url.split('?')[1]);
const token = urlParams.get('token');
const id = urlParams.get('id');
if (!token) {
logger.info('缺少令牌');
callback(false, 401, '缺少令牌');
return;
} else if (token !== process.env.TOKEN_SECRET){
logger.info('验证后端失败');
callback(false, 401, 'Token is invalid');
return;
}
info.req.id = id;
logger.info(`用户ID: token 验证成功`);
callback(true);
} catch (error) {
logger.error('验证后端失败:', error);
callback(false, 401, 'Token is invalid');
}
}
});
logger.info(`WebSocket server is running on the same port as HTTP server`);
wss.on('error', (error) => {
logger.error('WebSocket服务器错误:', error);
});
wss.on('connection', async (socket, req) => {
const id = req.id;
logger.info(`${id}号后端 连接成功`);
socketMap.set(id, socket);
socket.send('please give me tasks');
try {
const pendingMessages = await messagePersistence.getPendingMessages(id);
if (pendingMessages.length > 0) {
logger.info(`${id}号后端 发现 ${pendingMessages.length} 条待发送消息,开始重试发送`);
for (const pendingMsg of pendingMessages) {
try {
socket.send(pendingMsg.message);
await messagePersistence.removePendingMessage(pendingMsg.key);
logger.debug(`成功重试发送消息: backendId=${id}, messageKey=${pendingMsg.key}`);
} catch (error) {
logger.error(`重试发送消息失败: backendId=${id}, messageKey=${pendingMsg.key}`, error);
await messagePersistence.incrementRetryCount(pendingMsg.key);
}
}
logger.info(`${id}号后端 待发送消息重试完成`);
}
} catch (error) {
logger.error(`获取或发送待发送消息失败: backendId=${id}`, error);
}
socket.on('message', (message) => {
const messageStr = typeof message === 'string' ? message : message.toString();
if (messageStr === 'ping') {
socket.send('pong');
return;
}
if (messageStr === 'pong') {
return;
}
if (messageStr === 'please give me tasks') {
return;
}
try {
const msg = JSON.parse(messageStr);
if (msg.type === 'JWT_UPDATE') {
logger.info(`收到 JWT_UPDATE 消息`);
return;
}
if (msg.type === 'CAPACITY_UPDATE') {
logger.debug(`收到算力状态更新: 可用容量 = ${msg.data?.summary?.availableCapacity || 0}`);
return;
}
if (msg.type === 'INSTANCE_ONLINE') {
logger.debug(`收到实例上线: ${msg.data?.instanceId}`);
return;
}
if (msg.type === 'INSTANCE_OFFLINE') {
logger.debug(`收到实例下线: ${msg.data?.instanceId}`);
return;
}
if (msg.type === 'HEARTBEAT') {
socket.send(JSON.stringify({
type: 'HEARTBEAT_ACK',
data: { timestamp: new Date().toISOString() }
}));
return;
}
const prefix = messageStr.slice(0, 50);
if (prefix.includes('"type":"generate"') || prefix.includes("'type':'generate'")) {
assessment.postMessage({
type: 'submit',
data: messageStr
});
} else {
logger.debug(`收到未知消息类型: ${prefix}`);
}
} catch (e) {
logger.error('处理消息出错:', e);
socket.send(JSON.stringify({
error: '处理消息出错',
details: e.message
}));
}
});
const heartbeatInterval = setInterval(() => {
if (socket.readyState === WebSocket.OPEN) {
socket.send('ping');
// logger.debug(`向 ${id} 号后端发送心跳`);
}
}, 30000);
socket.on('close', (code, reason) => {
clearInterval(heartbeatInterval);
logger.info(`${id}号后端 连接关闭,关闭码: ${code},原因: ${reason}`);
});
socket.on('error', (error) => {
logger.error(`${id}号后端 连接错误:`, error);
});
});
assessment.on('message', async (message) => {
logger.debug(`收到assessment worker消息: ${JSON.stringify(message)}`);
if (message.type === 'AssessmentSuccess') {
await sendMessageToClient(message.backendId, code.SUCCESS[message.type]);
} else {
await sendMessageToClient(message.backendId, code.ERROR[message.type], false, 4401, code.ERROR[message.type]);
}
});
result.on('message', async (message) => {
logger.debug(`收到result worker消息: ${JSON.stringify(message)}`);
if (message.type === 'success') {
await sendMessageToClient(message.backendId, message.message, false, 1000, 'success');
} else {
await sendMessageToClient(message.backendId, '获取结果失败,可在历史记录区刷新查看结果', false, 4401, code.ERROR[message.type]);
}
});
callback_result.on('message', async (message) => {
logger.debug(`收到callback_result worker消息: ${JSON.stringify(message)}`);
if (message.type === 'success') {
await sendMessageToClient(message.backendId, message.message, false, 1000, 'success');
} else {
await sendMessageToClient(message.backendId, '获取结果失败,可在历史记录区刷新查看结果', false, 4401);
}
});
error.on('message', async (message) => {
logger.debug(`收到error worker消息: ${JSON.stringify(message)}`);
await sendMessageToClient(message.backendId, message.message, false, 4402, 'false');
});
}
function gracefulShutdown() {
logger.info('开始优雅关闭...');
if (wss) {
wss.close(() => {
logger.info('WebSocket服务器已关闭');
});
wss.clients.forEach((client) => {
client.close(1001, '服务器正在关闭');
});
}
workers.forEach((worker, index) => {
logger.info(`终止worker线程 ${index}`);
worker.terminate();
});
if (redis.isOpen) {
redis.disconnect()
.then(() => {
logger.info('Redis连接已关闭');
process.exit(0);
})
.catch((error) => {
logger.error('关闭Redis连接失败:', error);
process.exit(1);
});
} else {
process.exit(0);
}
setTimeout(() => {
logger.info('强制关闭...');
process.exit(1);
}, 5000);
}
async function initialize() {
logger.info('***************初始化队列开始***************');
try {
if (!redis.isOpen) {
await redis.connect();
logger.info('Redis 连接成功');
}
await initQueue.init();
logger.info('***************初始化队列完成***************');
setInterval(async () => {
try {
await messagePersistence.cleanupOldMessages(2 * 24 * 60 * 60 * 1000);
} catch (error) {
logger.error('定期清理过期消息失败:', error);
}
}, 24 * 60 * 60 * 1000);
} catch (err) {
logger.error('初始化失败:', err);
process.exit(1);
}
}
const app = express();
const hostname = '0.0.0.0';
const port = process.env.WS_PORT || 8087;
app.use(cors());
app.use('/workflow/uploads', express.static('uploads'));
app.use(json());
app.use(urlencoded({ extended: true }));
app.use('/workflow/file', fileRouter);
app.use('/callback', recordRouter);
app.get('/', (req, res) => {
res.json({
name: 'ComfyUI Task Queue Backend',
version: '1.0.0',
status: 'running',
timestamp: new Date().toISOString()
});
});
process.on('SIGINT', gracefulShutdown);
process.on('SIGTERM', gracefulShutdown);
initialize().then(() => {
const server = app.listen(port, hostname, () => {
console.log(`========================================`);
console.log(`HTTP & WebSocket 服务器已启动`);
console.log(`服务地址: http://${hostname}:${port}/`);
console.log(`========================================`);
createWebSocketServer(server);
});
});