From 71eda1389fc4bdbd8bf34e4fa0f66017c863a796 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 4 Apr 2026 09:45:36 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=E9=93=B8=E6=B8=8A=E4=B8=93=E7=BA=BF?= =?UTF-8?q?=E8=AE=A2=E9=98=85=E6=9C=8D=E5=8A=A1=E4=BF=AE=E5=A4=8D=20?= =?UTF-8?q?=E2=80=94=20PM2=20fork=E6=A8=A1=E5=BC=8F=E3=80=81=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E5=A4=84=E7=90=86=E3=80=81Nginx=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E3=80=81=E5=81=A5=E5=BA=B7=E6=A3=80=E6=9F=A5?= =?UTF-8?q?=E5=A2=9E=E5=BC=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因分析: - PM2使用cluster模式可能干扰server.listen('127.0.0.1')绑定 - subscription-server.js缺少try-catch和进程级错误处理,异常导致静默崩溃 - deploy-proxy.sh的configure_nginx()目标文件错误(/etc/nginx/sites-enabled/default → zhuyuan.conf) - update()不调用configure_nginx(),主部署覆盖Nginx配置后proxy-sub location丢失 - 健康检查仅测直连3802,未验证Nginx反代路径 - Nginx反代缺少proxy_connect_timeout/proxy_read_timeout 修复: 1. ecosystem.proxy.config.js: 添加exec_mode:'fork'防止cluster模式 2. subscription-server.js: 请求级try-catch + server error/clientError事件 + uncaughtException保护 3. deploy-proxy.sh: configure_nginx()自动查找正确配置文件 + update()调用configure_nginx + 双路健康检查 4. zhuyuan-sovereign.conf: 添加proxy timeout配置 5. nginx-proxy-snippet.conf: 同步更新proxy_http_version和timeout Agent-Logs-Url: https://github.com/qinfendebingshuo/guanghulab/sessions/88ed5687-7596-4d04-b181-57c89c032004 Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com> --- server/nginx/zhuyuan-sovereign.conf | 3 + server/proxy/config/nginx-proxy-snippet.conf | 4 + server/proxy/deploy-proxy.sh | 40 ++++- server/proxy/ecosystem.proxy.config.js | 3 + server/proxy/service/subscription-server.js | 172 ++++++++++++------- 5 files changed, 148 insertions(+), 74 deletions(-) diff --git a/server/nginx/zhuyuan-sovereign.conf b/server/nginx/zhuyuan-sovereign.conf index 8f7c98c0..3a12cb39 100644 --- a/server/nginx/zhuyuan-sovereign.conf +++ b/server/nginx/zhuyuan-sovereign.conf @@ -100,6 +100,9 @@ server { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; + proxy_connect_timeout 10s; + proxy_read_timeout 30s; + proxy_send_timeout 30s; add_header X-Content-Type-Options nosniff always; add_header Cache-Control "no-store, no-cache, must-revalidate" always; } diff --git a/server/proxy/config/nginx-proxy-snippet.conf b/server/proxy/config/nginx-proxy-snippet.conf index 3c1a33f5..978560f0 100644 --- a/server/proxy/config/nginx-proxy-snippet.conf +++ b/server/proxy/config/nginx-proxy-snippet.conf @@ -9,10 +9,14 @@ location /api/proxy-sub/ { proxy_pass http://127.0.0.1:3802/; + proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; + proxy_connect_timeout 10s; + proxy_read_timeout 30s; + proxy_send_timeout 30s; # 订阅服务安全头 add_header X-Content-Type-Options nosniff always; diff --git a/server/proxy/deploy-proxy.sh b/server/proxy/deploy-proxy.sh index 12052bf5..bf4b0573 100644 --- a/server/proxy/deploy-proxy.sh +++ b/server/proxy/deploy-proxy.sh @@ -224,24 +224,40 @@ deploy_services() { # ── 配置Nginx ───────────────────────────────── configure_nginx() { - # 检查主Nginx配置是否已有proxy-sub - NGINX_CONF="/etc/nginx/sites-enabled/default" + # 查找正确的Nginx配置文件 (zhuyuan.conf 优先于 default) + NGINX_CONF="" + for candidate in /etc/nginx/sites-enabled/zhuyuan.conf /etc/nginx/sites-enabled/default; do + if [ -f "$candidate" ]; then + NGINX_CONF="$candidate" + break + fi + done - if [ -f "$NGINX_CONF" ] && ! grep -q "proxy-sub" "$NGINX_CONF" 2>/dev/null; then + if [ -z "$NGINX_CONF" ]; then + echo " ⚠️ 未找到Nginx站点配置文件" + return 0 + fi + + echo " 使用Nginx配置: $NGINX_CONF" + + if ! grep -q "proxy-sub" "$NGINX_CONF" 2>/dev/null; then echo " 添加Nginx代理订阅反向代理配置..." # 在第一个 location = /health 之前插入 proxy-sub location sed -i '/# ─── 健康探针 ───/{ # 只在第一次匹配时插入 - i\ # ─── 铸渊专线订阅服务 (端口 3802) ───\n location /api/proxy-sub/ {\n proxy_pass http://127.0.0.1:3802/;\n proxy_http_version 1.1;\n proxy_set_header Host $host;\n proxy_set_header X-Real-IP $remote_addr;\n proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n proxy_set_header X-Forwarded-Proto $scheme;\n add_header X-Content-Type-Options nosniff always;\n add_header Cache-Control "no-store, no-cache, must-revalidate" always;\n }\n + i\ # ─── 铸渊专线订阅服务 (端口 3802) ───\n location /api/proxy-sub/ {\n proxy_pass http://127.0.0.1:3802/;\n proxy_http_version 1.1;\n proxy_set_header Host $host;\n proxy_set_header X-Real-IP $remote_addr;\n proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n proxy_set_header X-Forwarded-Proto $scheme;\n proxy_connect_timeout 10s;\n proxy_read_timeout 30s;\n add_header X-Content-Type-Options nosniff always;\n add_header Cache-Control "no-store, no-cache, must-revalidate" always;\n }\n }' "$NGINX_CONF" || true echo " ✅ Nginx proxy-sub配置已注入" else - echo " Nginx代理配置已存在 (或主配置不存在)" + echo " Nginx proxy-sub配置已存在" fi if nginx -t 2>/dev/null; then nginx -s reload || true echo " ✅ Nginx配置验证通过并已重载" + else + echo " ⚠️ Nginx配置验证失败:" + nginx -t 2>&1 || true fi } @@ -291,11 +307,18 @@ health_check() { echo " ❌ 端口443: 未监听" fi - # 订阅服务 + # 订阅服务 (直接访问) if curl -sf http://127.0.0.1:3802/health >/dev/null 2>&1; then - echo " ✅ 订阅服务: 正常" + echo " ✅ 订阅服务: 正常 (直连3802)" else - echo " ⏳ 订阅服务: 启动中..." + echo " ❌ 订阅服务: 端口3802无响应" + fi + + # 订阅服务 (通过Nginx反代) + if curl -sf http://127.0.0.1/api/proxy-sub/health >/dev/null 2>&1; then + echo " ✅ Nginx反代: 正常 (/api/proxy-sub/ → 3802)" + else + echo " ⚠️ Nginx反代: /api/proxy-sub/ 未响应 (Nginx配置可能缺失)" fi # PM2 @@ -308,6 +331,7 @@ update() { deploy_services save_server_host configure_xray + configure_nginx ensure_xray_root_user ensure_log_permissions diff --git a/server/proxy/ecosystem.proxy.config.js b/server/proxy/ecosystem.proxy.config.js index ee805d36..3e4f8e1e 100644 --- a/server/proxy/ecosystem.proxy.config.js +++ b/server/proxy/ecosystem.proxy.config.js @@ -10,6 +10,7 @@ module.exports = { version: '1.0.0', script: '/opt/zhuyuan/proxy/service/subscription-server.js', instances: 1, + exec_mode: 'fork', env: { NODE_ENV: 'production', ZY_PROXY_SUB_PORT: 3802, @@ -26,6 +27,7 @@ module.exports = { version: '1.0.0', script: '/opt/zhuyuan/proxy/service/traffic-monitor.js', instances: 1, + exec_mode: 'fork', env: { NODE_ENV: 'production', ZY_PROXY_DATA_DIR: '/opt/zhuyuan/proxy/data', @@ -41,6 +43,7 @@ module.exports = { version: '1.0.0', script: '/opt/zhuyuan/proxy/service/proxy-guardian.js', instances: 1, + exec_mode: 'fork', env: { NODE_ENV: 'production', ZY_PROXY_DATA_DIR: '/opt/zhuyuan/proxy/data', diff --git a/server/proxy/service/subscription-server.js b/server/proxy/service/subscription-server.js index ecaba074..b6726bbc 100644 --- a/server/proxy/service/subscription-server.js +++ b/server/proxy/service/subscription-server.js @@ -482,82 +482,112 @@ function detectClientType(userAgent) { // ── HTTP服务器 ─────────────────────────────── const server = http.createServer((req, res) => { - const parsedUrl = url.parse(req.url, true); - const pathname = parsedUrl.pathname; + try { + const parsedUrl = url.parse(req.url, true); + const pathname = parsedUrl.pathname; - // 健康检查 - if (pathname === '/health') { - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ status: 'ok', service: 'zy-proxy-subscription' })); - return; - } - - // 订阅端点: /sub/{token} - const subMatch = pathname.match(/^\/sub\/([a-f0-9]+)$/); - if (subMatch) { - const token = subMatch[1]; - const keys = loadKeys(); - - // 验证Token - if (token !== keys.ZY_PROXY_SUB_TOKEN) { - res.writeHead(403, { 'Content-Type': 'text/plain' }); - res.end('Forbidden'); + // 健康检查 + if (pathname === '/health') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ status: 'ok', service: 'zy-proxy-subscription' })); return; } - const serverHost = getServerHost(); - const quota = getQuotaInfo(); - const clientType = detectClientType(req.headers['user-agent']); - const userInfoHeader = generateUserInfoHeader(quota); + // 订阅端点: /sub/{token} + const subMatch = pathname.match(/^\/sub\/([a-f0-9]+)$/); + if (subMatch) { + const token = subMatch[1]; + const keys = loadKeys(); - if (clientType === 'clash') { - // Clash YAML格式 - const yaml = generateClashYaml(keys, serverHost); - res.writeHead(200, { - 'Content-Type': 'text/yaml; charset=utf-8', - 'Content-Disposition': 'attachment; filename="zy-proxy.yaml"', - 'subscription-userinfo': userInfoHeader, - 'profile-update-interval': '6', - 'profile-title': 'base64:6ZO45ria5LiT57q/', // "铸渊专线" in base64 - }); - res.end(yaml); - } else { - // Base64 URI格式 (Shadowrocket) - const vlessUri = generateVlessUri(keys, serverHost); - const encoded = Buffer.from(vlessUri).toString('base64'); - res.writeHead(200, { - 'Content-Type': 'text/plain; charset=utf-8', - 'subscription-userinfo': userInfoHeader, - 'profile-update-interval': '6', - }); - res.end(encoded); + // 验证Token + if (token !== keys.ZY_PROXY_SUB_TOKEN) { + res.writeHead(403, { 'Content-Type': 'text/plain' }); + res.end('Forbidden'); + return; + } + + const serverHost = getServerHost(); + const quota = getQuotaInfo(); + const clientType = detectClientType(req.headers['user-agent']); + const userInfoHeader = generateUserInfoHeader(quota); + + if (clientType === 'clash') { + // Clash YAML格式 + const yaml = generateClashYaml(keys, serverHost); + res.writeHead(200, { + 'Content-Type': 'text/yaml; charset=utf-8', + 'Content-Disposition': 'attachment; filename="zy-proxy.yaml"', + 'subscription-userinfo': userInfoHeader, + 'profile-update-interval': '6', + 'profile-title': 'base64:6ZO45ria5LiT57q/', // "铸渊专线" in base64 + }); + res.end(yaml); + } else { + // Base64 URI格式 (Shadowrocket) + const vlessUri = generateVlessUri(keys, serverHost); + const encoded = Buffer.from(vlessUri).toString('base64'); + res.writeHead(200, { + 'Content-Type': 'text/plain; charset=utf-8', + 'subscription-userinfo': userInfoHeader, + 'profile-update-interval': '6', + }); + res.end(encoded); + } + return; + } + + // 配额查询端点: /quota (公开安全 - 仅数字) + if (pathname === '/quota') { + const quota = getQuotaInfo(); + const totalGB = (quota.total_bytes / (1024 ** 3)).toFixed(1); + const usedGB = ((quota.upload_bytes + quota.download_bytes) / (1024 ** 3)).toFixed(1); + const remainGB = (totalGB - usedGB).toFixed(1); + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + total_gb: parseFloat(totalGB), + used_gb: parseFloat(usedGB), + remaining_gb: parseFloat(remainGB), + percentage_used: parseFloat(((usedGB / totalGB) * 100).toFixed(1)), + period: quota.period, + reset_day: quota.reset_day, + updated_at: quota.updated_at || new Date().toISOString() + })); + return; + } + + // 404 + res.writeHead(404, { 'Content-Type': 'text/plain' }); + res.end('Not Found'); + } catch (err) { + console.error(`❌ 请求处理错误 [${req.method} ${req.url}]:`, err.message || err); + try { + if (!res.headersSent) { + res.writeHead(500, { 'Content-Type': 'text/plain' }); + } + res.end('Internal Server Error'); + } catch (writeErr) { + console.error(' ⚠️ 响应写入失败:', writeErr.message); } - return; } +}); - // 配额查询端点: /quota (公开安全 - 仅数字) - if (pathname === '/quota') { - const quota = getQuotaInfo(); - const totalGB = (quota.total_bytes / (1024 ** 3)).toFixed(1); - const usedGB = ((quota.upload_bytes + quota.download_bytes) / (1024 ** 3)).toFixed(1); - const remainGB = (totalGB - usedGB).toFixed(1); - - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ - total_gb: parseFloat(totalGB), - used_gb: parseFloat(usedGB), - remaining_gb: parseFloat(remainGB), - percentage_used: parseFloat(((usedGB / totalGB) * 100).toFixed(1)), - period: quota.period, - reset_day: quota.reset_day, - updated_at: quota.updated_at || new Date().toISOString() - })); - return; +// 处理连接级别错误 (防止未捕获的socket错误导致进程崩溃) +server.on('error', (err) => { + console.error('❌ 服务器错误:', err.message); + if (err.code === 'EADDRINUSE') { + console.error(` 端口 ${PORT} 已被占用,10秒后重试...`); + setTimeout(() => { + server.close(); + server.listen(PORT, '127.0.0.1'); + }, 10000); } +}); - // 404 - res.writeHead(404, { 'Content-Type': 'text/plain' }); - res.end('Not Found'); +server.on('clientError', (err, socket) => { + if (socket.writable) { + socket.end('HTTP/1.1 400 Bad Request\r\n\r\n'); + } }); server.listen(PORT, '127.0.0.1', () => { @@ -579,3 +609,13 @@ function gracefulShutdown(signal) { } process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); process.on('SIGINT', () => gracefulShutdown('SIGINT')); + +// 进程级错误保护 (防止未捕获的异常导致静默崩溃) +process.on('uncaughtException', (err) => { + console.error('❌ 未捕获的异常:', err.message); + console.error(err.stack); + // 记录但不退出 — PM2会重启,但频繁重启意味着间歇性不可用 +}); +process.on('unhandledRejection', (reason) => { + console.error('❌ 未处理的Promise拒绝:', reason); +});