465 lines
15 KiB
JavaScript
465 lines
15 KiB
JavaScript
require('dotenv').config();
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
require('module-alias')({ base: path.resolve(__dirname, '..') });
|
|
const cors = require('cors');
|
|
const axios = require('axios');
|
|
const express = require('express');
|
|
const passport = require('passport');
|
|
const compression = require('compression');
|
|
const cookieParser = require('cookie-parser');
|
|
const { logger } = require('@librechat/data-schemas');
|
|
const mongoSanitize = require('express-mongo-sanitize');
|
|
const {
|
|
isEnabled,
|
|
ErrorController,
|
|
performStartupChecks,
|
|
handleJsonParseError,
|
|
initializeFileStorage,
|
|
GenerationJobManager,
|
|
createStreamServices,
|
|
} = require('@librechat/api');
|
|
const { connectDb, indexSync } = require('~/db');
|
|
const initializeOAuthReconnectManager = require('./services/initializeOAuthReconnectManager');
|
|
const createValidateImageRequest = require('./middleware/validateImageRequest');
|
|
const { jwtLogin, ldapLogin, passportLogin } = require('~/strategies');
|
|
const { updateInterfacePermissions } = require('~/models/interface');
|
|
const { checkMigrations } = require('./services/start/migration');
|
|
const initializeMCPs = require('./services/initializeMCPs');
|
|
const configureSocialLogins = require('./socialLogins');
|
|
const { getAppConfig } = require('./services/Config');
|
|
const staticCache = require('./utils/staticCache');
|
|
const noIndex = require('./middleware/noIndex');
|
|
const { seedDatabase } = require('~/models');
|
|
const routes = require('./routes');
|
|
|
|
const { PORT, HOST, ALLOW_SOCIAL_LOGIN, DISABLE_COMPRESSION, TRUST_PROXY } = process.env ?? {};
|
|
|
|
// Allow PORT=0 to be used for automatic free port assignment
|
|
const port = isNaN(Number(PORT)) ? 3080 : Number(PORT);
|
|
const host = HOST || 'localhost';
|
|
const trusted_proxy = Number(TRUST_PROXY) || 1; /* trust first proxy by default */
|
|
|
|
const app = express();
|
|
|
|
const startServer = async () => {
|
|
if (typeof Bun !== 'undefined') {
|
|
axios.defaults.headers.common['Accept-Encoding'] = 'gzip';
|
|
}
|
|
await connectDb();
|
|
|
|
logger.info('Connected to MongoDB');
|
|
indexSync().catch((err) => {
|
|
logger.error('[indexSync] Background sync failed:', err);
|
|
});
|
|
|
|
app.disable('x-powered-by');
|
|
app.set('trust proxy', trusted_proxy);
|
|
|
|
await seedDatabase();
|
|
const appConfig = await getAppConfig();
|
|
initializeFileStorage(appConfig);
|
|
await performStartupChecks(appConfig);
|
|
await updateInterfacePermissions(appConfig);
|
|
|
|
const indexPath = path.join(appConfig.paths.dist, 'index.html');
|
|
let indexHTML = fs.readFileSync(indexPath, 'utf8');
|
|
|
|
// In order to provide support to serving the application in a sub-directory
|
|
// We need to update the base href if the DOMAIN_CLIENT is specified and not the root path
|
|
if (process.env.DOMAIN_CLIENT) {
|
|
const clientUrl = new URL(process.env.DOMAIN_CLIENT);
|
|
const baseHref = clientUrl.pathname.endsWith('/')
|
|
? clientUrl.pathname
|
|
: `${clientUrl.pathname}/`;
|
|
if (baseHref !== '/') {
|
|
logger.info(`Setting base href to ${baseHref}`);
|
|
indexHTML = indexHTML.replace(/base href="\/"/, `base href="${baseHref}"`);
|
|
}
|
|
}
|
|
|
|
app.get('/health', (_req, res) => res.status(200).send('OK'));
|
|
|
|
/* Middleware */
|
|
app.use(noIndex);
|
|
app.use(express.json({ limit: '3mb' }));
|
|
app.use(express.urlencoded({ extended: true, limit: '3mb' }));
|
|
app.use(handleJsonParseError);
|
|
|
|
/**
|
|
* Express 5 Compatibility: Make req.query writable for mongoSanitize
|
|
* In Express 5, req.query is read-only by default, but express-mongo-sanitize needs to modify it
|
|
*/
|
|
app.use((req, _res, next) => {
|
|
Object.defineProperty(req, 'query', {
|
|
...Object.getOwnPropertyDescriptor(req, 'query'),
|
|
value: req.query,
|
|
writable: true,
|
|
});
|
|
next();
|
|
});
|
|
|
|
app.use(mongoSanitize());
|
|
app.use(cors());
|
|
app.use(cookieParser());
|
|
|
|
if (!isEnabled(DISABLE_COMPRESSION)) {
|
|
app.use(compression());
|
|
} else {
|
|
console.warn('Response compression has been disabled via DISABLE_COMPRESSION.');
|
|
}
|
|
|
|
app.use(staticCache(appConfig.paths.dist));
|
|
app.use(staticCache(appConfig.paths.fonts));
|
|
app.use(staticCache(appConfig.paths.assets));
|
|
|
|
if (!ALLOW_SOCIAL_LOGIN) {
|
|
console.warn('Social logins are disabled. Set ALLOW_SOCIAL_LOGIN=true to enable them.');
|
|
}
|
|
|
|
/* OAUTH */
|
|
app.use(passport.initialize());
|
|
passport.use(jwtLogin());
|
|
passport.use(passportLogin());
|
|
|
|
/* LDAP Auth */
|
|
if (process.env.LDAP_URL && process.env.LDAP_USER_SEARCH_BASE) {
|
|
passport.use(ldapLogin);
|
|
}
|
|
|
|
if (isEnabled(ALLOW_SOCIAL_LOGIN)) {
|
|
await configureSocialLogins(app);
|
|
}
|
|
|
|
app.use('/oauth', routes.oauth);
|
|
/* API Endpoints */
|
|
app.use('/api/auth', routes.auth);
|
|
app.use('/api/actions', routes.actions);
|
|
app.use('/api/keys', routes.keys);
|
|
app.use('/api/user', routes.user);
|
|
app.use('/api/search', routes.search);
|
|
app.use('/api/messages', routes.messages);
|
|
app.use('/api/convos', routes.convos);
|
|
app.use('/api/presets', routes.presets);
|
|
app.use('/api/prompts', routes.prompts);
|
|
app.use('/api/categories', routes.categories);
|
|
app.use('/api/endpoints', routes.endpoints);
|
|
app.use('/api/balance', routes.balance);
|
|
app.use('/api/models', routes.models);
|
|
app.use('/api/config', routes.config);
|
|
app.use('/api/assistants', routes.assistants);
|
|
app.use('/api/files', await routes.files.initialize());
|
|
app.use('/images/', createValidateImageRequest(appConfig.secureImageLinks), routes.staticRoute);
|
|
app.use('/api/share', routes.share);
|
|
app.use('/api/roles', routes.roles);
|
|
app.use('/api/agents', routes.agents);
|
|
app.use('/api/banner', routes.banner);
|
|
app.use('/api/memories', routes.memories);
|
|
app.use('/api/permissions', routes.accessPermissions);
|
|
|
|
app.use('/api/tags', routes.tags);
|
|
app.use('/api/mcp', routes.mcp);
|
|
|
|
app.use(ErrorController);
|
|
|
|
app.get('/api/healthSylar', (req, res) => {
|
|
try {
|
|
const healthStatus = {
|
|
status: 'ok',
|
|
timestamp: new Date().toISOString(),
|
|
service: 'librechat-backend',
|
|
version: process.env.LIBRECHAT_VERSION || 'unknown',
|
|
uptime: process.uptime(),
|
|
environment: process.env.NODE_ENV || 'production',
|
|
|
|
// Information sur la disponibilité du serveur GPU Sylar
|
|
gpu_server: {
|
|
status: 'online', // 'online', 'offline', 'maintenance'
|
|
// Vous pouvez ajouter ici une vérification réelle de disponibilité du serveur GPU
|
|
// en appelant votre endpoint GPU ou en utilisant une variable d'environnement
|
|
base_url: process.env.CUSTOM_API_BASE_URL || process.env.VITE_API_URL || 'unknown',
|
|
},
|
|
|
|
// Ressources système
|
|
memory: {
|
|
used: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
|
|
total: Math.round(process.memoryUsage().heapTotal / 1024 / 1024),
|
|
unit: 'MB'
|
|
}
|
|
};
|
|
|
|
res.status(200).json(healthStatus);
|
|
} catch (error) {
|
|
logger.error('[HealthSylar Check] Error:', error);
|
|
res.status(503).json({
|
|
status: 'error',
|
|
message: 'Service temporarily unavailable',
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
}
|
|
});
|
|
|
|
// Health probe for upcoming GPU1 endpoint (returns offline while upstream is down)
|
|
app.get('/api/healthGPU1', async (_req, res) => {
|
|
const started = Date.now();
|
|
const controller = new AbortController();
|
|
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
try {
|
|
const response = await fetch('https://cpu.lsbuchet.com/healthGPU1', { signal: controller.signal });
|
|
const responseTime = Date.now() - started;
|
|
|
|
if (response.ok) {
|
|
const data = await response.json().catch(() => ({}));
|
|
return res.status(200).json({
|
|
status: data.status || 'ok',
|
|
gpu_status: data.gpu_status ?? true,
|
|
responseTime,
|
|
source: 'cpu.lsbuchet.com/healthGPU1',
|
|
payload: data,
|
|
});
|
|
}
|
|
|
|
const body = await response.text().catch(() => '');
|
|
return res.status(503).json({
|
|
status: 'offline',
|
|
gpu_status: false,
|
|
responseTime,
|
|
source: 'cpu.lsbuchet.com/healthGPU1',
|
|
message: `HTTP ${response.status}`,
|
|
body,
|
|
});
|
|
} catch (error) {
|
|
const responseTime = Date.now() - started;
|
|
logger.warn('[healthGPU1] fetch failed:', error?.message || error);
|
|
return res.status(503).json({
|
|
status: 'offline',
|
|
gpu_status: false,
|
|
responseTime,
|
|
source: 'cpu.lsbuchet.com/healthGPU1',
|
|
error: error?.message || 'Unknown error',
|
|
});
|
|
} finally {
|
|
clearTimeout(timeout);
|
|
}
|
|
});
|
|
|
|
app.get('/api/credits', async (req, res) => {
|
|
const started = Date.now();
|
|
const controller = new AbortController();
|
|
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
|
|
try {
|
|
const response = await fetch('https://cpu.lsbuchet.com/v1/users/credits', {
|
|
signal: controller.signal,
|
|
});
|
|
const responseTime = Date.now() - started;
|
|
|
|
if (response.ok) {
|
|
const data = await response.json().catch(() => ({}));
|
|
return res.status(200).json({
|
|
...data,
|
|
responseTime,
|
|
source: 'cpu.lsbuchet.com/v1/users/credits',
|
|
});
|
|
}
|
|
|
|
return res.status(response.status).json({
|
|
error: 'Failed to fetch credits',
|
|
message: `HTTP ${response.status}`,
|
|
responseTime,
|
|
source: 'cpu.lsbuchet.com/v1/users/credits',
|
|
});
|
|
} catch (error) {
|
|
const responseTime = Date.now() - started;
|
|
logger.warn('[credits] fetch failed:', error?.message || error);
|
|
return res.status(503).json({
|
|
error: error?.message || 'Unknown error',
|
|
responseTime,
|
|
source: 'cpu.lsbuchet.com/v1/users/credits',
|
|
});
|
|
} finally {
|
|
clearTimeout(timeout);
|
|
}
|
|
});
|
|
|
|
// Proxy endpoint for server toggle
|
|
app.post('/api/server/toggle', async (req, res) => {
|
|
const { state } = req.body;
|
|
const started = Date.now();
|
|
const controller = new AbortController();
|
|
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
|
|
try {
|
|
const response = await fetch('https://cpu.lsbuchet.com/v1/server/toggle', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ state }),
|
|
signal: controller.signal,
|
|
});
|
|
const responseTime = Date.now() - started;
|
|
|
|
if (response.ok) {
|
|
const data = await response.json().catch(() => ({}));
|
|
return res.status(200).json({
|
|
...data,
|
|
responseTime,
|
|
source: 'cpu.lsbuchet.com/v1/server/toggle',
|
|
});
|
|
}
|
|
|
|
const errorData = await response.json().catch(() => ({}));
|
|
return res.status(response.status).json({
|
|
...errorData,
|
|
responseTime,
|
|
source: 'cpu.lsbuchet.com/v1/server/toggle',
|
|
});
|
|
} catch (error) {
|
|
const responseTime = Date.now() - started;
|
|
logger.warn('[server/toggle] fetch failed:', error?.message || error);
|
|
return res.status(503).json({
|
|
error: error?.message || 'Unknown error',
|
|
responseTime,
|
|
source: 'cpu.lsbuchet.com/v1/server/toggle',
|
|
});
|
|
} finally {
|
|
clearTimeout(timeout);
|
|
}
|
|
});
|
|
|
|
// Proxy endpoint for CPU status check (conversation-aware)
|
|
app.get('/api/statusCPU', async (req, res) => {
|
|
const conversationId = req.query.conversation_id;
|
|
const started = Date.now();
|
|
const controller = new AbortController();
|
|
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
|
|
try {
|
|
const url = conversationId
|
|
? `https://cpu.lsbuchet.com/v1/status/cpu?conversation_id=${encodeURIComponent(conversationId)}`
|
|
: 'https://cpu.lsbuchet.com/v1/status/cpu';
|
|
|
|
const response = await fetch(url, { signal: controller.signal });
|
|
const responseTime = Date.now() - started;
|
|
|
|
if (response.ok) {
|
|
const data = await response.json().catch(() => ({}));
|
|
return res.status(200).json({
|
|
...data,
|
|
responseTime,
|
|
source: 'cpu.lsbuchet.com/v1/status/cpu',
|
|
});
|
|
}
|
|
|
|
return res.status(response.status).json({
|
|
status: 'error',
|
|
message: `HTTP ${response.status}`,
|
|
responseTime,
|
|
source: 'cpu.lsbuchet.com/v1/status/cpu',
|
|
});
|
|
} catch (error) {
|
|
const responseTime = Date.now() - started;
|
|
logger.warn('[statusCPU] fetch failed:', error?.message || error);
|
|
return res.status(503).json({
|
|
status: 'error',
|
|
error: error?.message || 'Unknown error',
|
|
responseTime,
|
|
source: 'cpu.lsbuchet.com/v1/status/cpu',
|
|
});
|
|
} finally {
|
|
clearTimeout(timeout);
|
|
}
|
|
});
|
|
|
|
app.use((req, res) => {
|
|
res.set({
|
|
'Cache-Control': process.env.INDEX_CACHE_CONTROL || 'no-cache, no-store, must-revalidate',
|
|
Pragma: process.env.INDEX_PRAGMA || 'no-cache',
|
|
Expires: process.env.INDEX_EXPIRES || '0',
|
|
});
|
|
|
|
const lang = req.cookies.lang || req.headers['accept-language']?.split(',')[0] || 'en-US';
|
|
const saneLang = lang.replace(/"/g, '"');
|
|
let updatedIndexHtml = indexHTML.replace(/lang="en-US"/g, `lang="${saneLang}"`);
|
|
|
|
res.type('html');
|
|
res.send(updatedIndexHtml);
|
|
});
|
|
|
|
app.listen(port, host, async (err) => {
|
|
if (err) {
|
|
logger.error('Failed to start server:', err);
|
|
process.exit(1);
|
|
}
|
|
|
|
if (host === '0.0.0.0') {
|
|
logger.info(
|
|
`Server listening on all interfaces at port ${port}. Use http://localhost:${port} to access it`,
|
|
);
|
|
} else {
|
|
logger.info(`Server listening at http://${host == '0.0.0.0' ? 'localhost' : host}:${port}`);
|
|
}
|
|
|
|
await initializeMCPs();
|
|
await initializeOAuthReconnectManager();
|
|
await checkMigrations();
|
|
|
|
// Configure stream services (auto-detects Redis from USE_REDIS env var)
|
|
const streamServices = createStreamServices();
|
|
GenerationJobManager.configure(streamServices);
|
|
GenerationJobManager.initialize();
|
|
});
|
|
};
|
|
|
|
startServer();
|
|
|
|
let messageCount = 0;
|
|
process.on('uncaughtException', (err) => {
|
|
if (!err.message.includes('fetch failed')) {
|
|
logger.error('There was an uncaught error:', err);
|
|
}
|
|
|
|
if (err.message && err.message?.toLowerCase()?.includes('abort')) {
|
|
logger.warn('There was an uncatchable abort error.');
|
|
return;
|
|
}
|
|
|
|
if (err.message.includes('GoogleGenerativeAI')) {
|
|
logger.warn(
|
|
'\n\n`GoogleGenerativeAI` errors cannot be caught due to an upstream issue, see: https://github.com/google-gemini/generative-ai-js/issues/303',
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (err.message.includes('fetch failed')) {
|
|
if (messageCount === 0) {
|
|
logger.warn('Meilisearch error, search will be disabled');
|
|
messageCount++;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (err.message.includes('OpenAIError') || err.message.includes('ChatCompletionMessage')) {
|
|
logger.error(
|
|
'\n\nAn Uncaught `OpenAIError` error may be due to your reverse-proxy setup or stream configuration, or a bug in the `openai` node package.',
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (err.stack && err.stack.includes('@librechat/agents')) {
|
|
logger.error(
|
|
'\n\nAn error occurred in the agents system. The error has been logged and the app will continue running.',
|
|
{
|
|
message: err.message,
|
|
stack: err.stack,
|
|
},
|
|
);
|
|
return;
|
|
}
|
|
|
|
process.exit(1);
|
|
});
|
|
|
|
/** Export app for easier testing purposes */
|
|
module.exports = app;
|