Files
MoviePilot-Frontend/public/service.js
2025-07-05 23:56:51 +00:00

178 lines
4.8 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.
const path = require('node:path')
const express = require('express')
const proxy = require('express-http-proxy')
const { createProxyMiddleware } = require('http-proxy-middleware')
const app = express()
const port = process.env.NGINX_PORT || 3000
// 后端 API 地址
const proxyConfig = {
URL: '127.0.0.1',
PORT: process.env.PORT || 3001
}
// 静态文件服务目录
app.use(express.static(__dirname))
// 创建专门的SSE代理中间件
const sseProxyMiddleware = createProxyMiddleware({
target: `http://${proxyConfig.URL}:${proxyConfig.PORT}`,
changeOrigin: true,
ws: false,
timeout: 0, // 无超时
proxyTimeout: 0, // 无超时
headers: {
'Connection': 'keep-alive',
'Cache-Control': 'no-cache'
},
onProxyRes: (proxyRes, req, res) => {
// 检测SSE响应
const isSSE = proxyRes.headers['content-type'] &&
proxyRes.headers['content-type'].includes('text/event-stream');
if (isSSE) {
// 设置SSE响应头
res.writeHead(proxyRes.statusCode, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Cache-Control, Content-Type, Authorization'
});
// 直接将代理响应流式传输到客户端
proxyRes.pipe(res);
// 处理客户端断开连接
req.on('close', () => {
console.log('Client disconnected from SSE stream');
if (proxyRes.destroy) {
proxyRes.destroy();
}
});
// 处理代理响应结束
proxyRes.on('end', () => {
console.log('SSE stream ended');
if (!res.headersSent) {
res.end();
}
});
// 处理代理响应错误
proxyRes.on('error', (err) => {
console.error('SSE proxy response error:', err);
if (!res.headersSent) {
res.status(500).end();
}
});
}
},
onError: (err, req, res) => {
console.error('SSE proxy error:', err);
if (!res.headersSent) {
res.status(500).json({ error: 'Proxy error' });
}
}
});
// 创建普通API代理中间件
const apiProxyMiddleware = proxy(`${proxyConfig.URL}:${proxyConfig.PORT}`, {
// 路径加上 /api 前缀
proxyReqPathResolver: (req) => {
return `/api${req.url}`
},
proxyReqOptDecorator: (proxyReqOpts, srcReq) => {
proxyReqOpts.headers = proxyReqOpts.headers || {};
// 检测是否为SSE请求
const isSSE = srcReq.headers.accept && srcReq.headers.accept.includes('text/event-stream');
if (!isSSE) {
// 普通请求设置超时
proxyReqOpts.timeout = 600000; // 600秒超时
}
return proxyReqOpts;
},
userResDecorator: (proxyRes, proxyResData, userReq, userRes) => {
// 只处理非SSE响应
const isSSEResponse = proxyRes.headers['content-type'] &&
proxyRes.headers['content-type'].includes('text/event-stream');
if (!isSSEResponse) {
// 普通响应:正常处理
return proxyResData;
}
// SSE响应不在这里处理已经由专门的中间件处理
return proxyResData;
},
// 错误处理
proxyErrorHandler: (err, res, next) => {
// 客户端断开连接的正常情况
if (err.code === 'ECONNRESET' || err.code === 'EPIPE') {
console.log('Client disconnected:', err.code);
if (!res.headersSent) {
res.end();
}
return;
}
// 超时错误处理
if (err.code === 'ETIMEDOUT') {
console.log('Proxy request timed out:', err.code);
if (!res.headersSent) {
res.status(504).send('Gateway Timeout');
}
return;
}
// 其他错误
console.error('Proxy error:', err);
if (!res.headersSent) {
res.status(500).send('Internal Server Error');
}
}
});
// 配置API代理路由
app.use('/api', (req, res, next) => {
// 检测是否为SSE请求
const isSSE = req.headers.accept && req.headers.accept.includes('text/event-stream');
if (isSSE) {
// 使用专门的SSE代理中间件
sseProxyMiddleware(req, res, next);
} else {
// 使用普通API代理中间件
apiProxyMiddleware(req, res, next);
}
});
// 配置代理中间件将CookieCloud请求转发给后端API
app.use(
'/cookiecloud',
proxy(`${proxyConfig.URL}:${proxyConfig.PORT}`, {
// 路径加上 /cookiecloud 前缀
proxyReqPathResolver: (req) => {
return `/cookiecloud${req.url}`
}
})
);
// 处理根路径的请求
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'index.html'))
})
// 处理所有其他请求,重定向到前端入口文件
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'index.html'))
})
app.listen(port, () => {
console.log(`Server is running on port ${port}`)
})