|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import express from 'express'; |
|
|
import cors from 'cors'; |
|
|
import path from 'path'; |
|
|
import { closeRequester } from '../api/client.js'; |
|
|
import logger from '../utils/logger.js'; |
|
|
import config from '../config/config.js'; |
|
|
import memoryManager from '../utils/memoryManager.js'; |
|
|
import { getPublicDir, getRelativePath } from '../utils/paths.js'; |
|
|
import { MEMORY_CHECK_INTERVAL } from '../constants/index.js'; |
|
|
import { errorHandler } from '../utils/errors.js'; |
|
|
import { getChunkPoolSize, clearChunkPool } from './stream.js'; |
|
|
|
|
|
|
|
|
import adminRouter from '../routes/admin.js'; |
|
|
import sdRouter from '../routes/sd.js'; |
|
|
import openaiRouter from '../routes/openai.js'; |
|
|
import geminiRouter from '../routes/gemini.js'; |
|
|
import claudeRouter from '../routes/claude.js'; |
|
|
|
|
|
const publicDir = getPublicDir(); |
|
|
|
|
|
logger.info(`静态文件目录: ${getRelativePath(publicDir)}`); |
|
|
|
|
|
const app = express(); |
|
|
|
|
|
|
|
|
memoryManager.setThreshold(config.server.memoryThreshold); |
|
|
memoryManager.start(MEMORY_CHECK_INTERVAL); |
|
|
|
|
|
|
|
|
app.use(cors()); |
|
|
app.use(express.json({ limit: config.security.maxRequestSize })); |
|
|
|
|
|
|
|
|
app.use('/images', express.static(path.join(publicDir, 'images'))); |
|
|
app.use(express.static(publicDir)); |
|
|
|
|
|
|
|
|
app.use('/admin', adminRouter); |
|
|
|
|
|
|
|
|
app.use(errorHandler); |
|
|
|
|
|
|
|
|
app.use((req, res, next) => { |
|
|
const ignorePaths = [ |
|
|
'/images', '/favicon.ico', '/.well-known', |
|
|
'/sdapi/v1/options', '/sdapi/v1/samplers', '/sdapi/v1/schedulers', |
|
|
'/sdapi/v1/upscalers', '/sdapi/v1/latent-upscale-modes', |
|
|
'/sdapi/v1/sd-vae', '/sdapi/v1/sd-modules' |
|
|
]; |
|
|
|
|
|
const fullPath = req.originalUrl.split('?')[0]; |
|
|
if (!ignorePaths.some(p => fullPath.startsWith(p))) { |
|
|
const start = Date.now(); |
|
|
res.on('finish', () => { |
|
|
logger.request(req.method, fullPath, res.statusCode, Date.now() - start); |
|
|
}); |
|
|
} |
|
|
next(); |
|
|
}); |
|
|
|
|
|
|
|
|
app.use('/sdapi/v1', sdRouter); |
|
|
|
|
|
|
|
|
app.use((req, res, next) => { |
|
|
if (req.path.startsWith('/v1/')) { |
|
|
const apiKey = config.security?.apiKey; |
|
|
if (apiKey) { |
|
|
const authHeader = req.headers.authorization || req.headers['x-api-key']; |
|
|
const providedKey = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : authHeader; |
|
|
if (providedKey !== apiKey) { |
|
|
logger.warn(`API Key 验证失败: ${req.method} ${req.path} (提供的Key: ${providedKey ? providedKey.substring(0, 10) + '...' : '无'})`); |
|
|
return res.status(401).json({ error: 'Invalid API Key' }); |
|
|
} |
|
|
} |
|
|
} else if (req.path.startsWith('/v1beta/')) { |
|
|
const apiKey = config.security?.apiKey; |
|
|
if (apiKey) { |
|
|
const providedKey = req.query.key || req.headers['x-goog-api-key']; |
|
|
if (providedKey !== apiKey) { |
|
|
logger.warn(`API Key 验证失败: ${req.method} ${req.path} (提供的Key: ${providedKey ? providedKey.substring(0, 10) + '...' : '无'})`); |
|
|
return res.status(401).json({ error: 'Invalid API Key' }); |
|
|
} |
|
|
} |
|
|
} |
|
|
next(); |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app.use('/v1', openaiRouter); |
|
|
|
|
|
|
|
|
app.use('/v1beta', geminiRouter); |
|
|
|
|
|
|
|
|
app.use('/v1', claudeRouter); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app.get('/v1/memory', (req, res) => { |
|
|
const usage = process.memoryUsage(); |
|
|
res.json({ |
|
|
heapUsed: usage.heapUsed, |
|
|
heapTotal: usage.heapTotal, |
|
|
rss: usage.rss, |
|
|
external: usage.external, |
|
|
arrayBuffers: usage.arrayBuffers, |
|
|
pressure: memoryManager.getCurrentPressure(), |
|
|
poolSizes: memoryManager.getPoolSizes(), |
|
|
chunkPoolSize: getChunkPoolSize() |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
app.get('/health', (req, res) => { |
|
|
res.json({ status: 'ok', uptime: process.uptime() }); |
|
|
}); |
|
|
|
|
|
|
|
|
const server = app.listen(config.server.port, config.server.host, () => { |
|
|
logger.info(`服务器已启动: ${config.server.host}:${config.server.port}`); |
|
|
}); |
|
|
|
|
|
server.on('error', (error) => { |
|
|
if (error.code === 'EADDRINUSE') { |
|
|
logger.error(`端口 ${config.server.port} 已被占用`); |
|
|
process.exit(1); |
|
|
} else if (error.code === 'EACCES') { |
|
|
logger.error(`端口 ${config.server.port} 无权限访问`); |
|
|
process.exit(1); |
|
|
} else { |
|
|
logger.error('服务器启动失败:', error.message); |
|
|
process.exit(1); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
const shutdown = () => { |
|
|
logger.info('正在关闭服务器...'); |
|
|
|
|
|
|
|
|
memoryManager.stop(); |
|
|
logger.info('已停止内存管理器'); |
|
|
|
|
|
|
|
|
closeRequester(); |
|
|
logger.info('已关闭子进程请求器'); |
|
|
|
|
|
|
|
|
clearChunkPool(); |
|
|
logger.info('已清理对象池'); |
|
|
|
|
|
server.close(() => { |
|
|
logger.info('服务器已关闭'); |
|
|
process.exit(0); |
|
|
}); |
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
logger.warn('服务器关闭超时,强制退出'); |
|
|
process.exit(0); |
|
|
}, 5000); |
|
|
}; |
|
|
|
|
|
process.on('SIGINT', shutdown); |
|
|
process.on('SIGTERM', shutdown); |
|
|
|
|
|
|
|
|
process.on('uncaughtException', (error) => { |
|
|
logger.error('未捕获异常:', error.message); |
|
|
|
|
|
}); |
|
|
|
|
|
process.on('unhandledRejection', (reason, promise) => { |
|
|
logger.error('未处理的 Promise 拒绝:', reason); |
|
|
}); |
|
|
|