Merge pull request #288 from qinfendebingshuo/copilot/add-vpn-reverse-accelerator-agent
VPN V2→V3: 光湖语言世界 — LLM路由器 + 反向加速Agent + 流量仪表盘
This commit is contained in:
commit
39de7bae25
|
|
@ -28,6 +28,8 @@ on:
|
|||
- remove-user
|
||||
- list-users
|
||||
- send-subscription
|
||||
- deploy-v3
|
||||
- switch-v3
|
||||
default: 'status'
|
||||
email:
|
||||
description: '用户邮箱 (add-user/remove-user/send-subscription时需要)'
|
||||
|
|
@ -55,7 +57,9 @@ jobs:
|
|||
github.event.inputs.action == 'install' ||
|
||||
github.event.inputs.action == 'update' ||
|
||||
github.event.inputs.action == 'status' ||
|
||||
github.event.inputs.action == 'restart'
|
||||
github.event.inputs.action == 'restart' ||
|
||||
github.event.inputs.action == 'deploy-v3' ||
|
||||
github.event.inputs.action == 'switch-v3'
|
||||
|
||||
steps:
|
||||
- name: '📥 检出代码'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
# ═══════════════════════════════════════════════
|
||||
# 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001
|
||||
# 📜 Copyright: 国作登字-2026-A-00037559
|
||||
# ═══════════════════════════════════════════════
|
||||
# 光湖语言世界 V3 · 大脑服务器 Nginx反向代理配置
|
||||
# 添加到 /etc/nginx/sites-enabled/ 中的server块内
|
||||
#
|
||||
# 测试阶段: /api/proxy-v3/ → 3805 (V2继续运行)
|
||||
# 切换阶段: 修改 /api/proxy-v2/ 的 proxy_pass 为 3805
|
||||
# ═══════════════════════════════════════════════
|
||||
|
||||
# §4 光湖语言世界V3订阅服务 (测试路径)
|
||||
# 通过 /api/proxy-v3/ 路径访问
|
||||
# 实际由 subscription-server-v3.js (port 3805) 处理
|
||||
# ⚠️ 测试通过后:
|
||||
# 方法1: 将上方V2的 proxy_pass 改为 http://127.0.0.1:3805/
|
||||
# 方法2: 或停掉V2进程,将V3端口改为3803
|
||||
|
||||
location /api/proxy-v3/ {
|
||||
proxy_pass http://127.0.0.1:3805/;
|
||||
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;
|
||||
add_header X-Frame-Options DENY always;
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
# 📜 Copyright: 国作登字-2026-A-00037559
|
||||
# ═══════════════════════════════════════════════
|
||||
# server/proxy/deploy-brain-proxy.sh
|
||||
# 🚀 铸渊专线V2 · 大脑服务器部署脚本
|
||||
# 🚀 铸渊专线V2 + 光湖语言世界V3 · 大脑服务器部署脚本
|
||||
#
|
||||
# 在大脑服务器(ZY-SVR-005)上执行
|
||||
# 部署多用户VPN系统 (独立于面孔服务器的V1)
|
||||
|
|
@ -17,6 +17,8 @@
|
|||
# bash deploy-brain-proxy.sh add-user <email> [quota_gb] — 添加用户
|
||||
# bash deploy-brain-proxy.sh remove-user <email> — 移除用户
|
||||
# bash deploy-brain-proxy.sh list-users — 列出用户
|
||||
# bash deploy-brain-proxy.sh deploy-v3 — 部署V3测试环境
|
||||
# bash deploy-brain-proxy.sh switch-v3 — 切换V2→V3 (用户刷新即升级)
|
||||
# ═══════════════════════════════════════════════
|
||||
|
||||
set -uo pipefail
|
||||
|
|
@ -196,8 +198,16 @@ deploy_services() {
|
|||
cp "$REPO_PROXY_DIR"/service/vpn-worker.js "$PROXY_DIR/service/"
|
||||
cp "$REPO_PROXY_DIR"/ecosystem.brain-proxy.config.js "$PROXY_DIR/"
|
||||
|
||||
# 复制V3服务文件 (独立运行·不影响V2)
|
||||
cp "$REPO_PROXY_DIR"/service/subscription-server-v3.js "$PROXY_DIR/service/"
|
||||
cp "$REPO_PROXY_DIR"/service/llm-router.js "$PROXY_DIR/service/"
|
||||
cp "$REPO_PROXY_DIR"/service/reverse-boost-agent.js "$PROXY_DIR/service/"
|
||||
cp "$REPO_PROXY_DIR"/service/proxy-guardian.js "$PROXY_DIR/service/"
|
||||
cp "$REPO_PROXY_DIR"/ecosystem.brain-proxy-v3.config.js "$PROXY_DIR/"
|
||||
|
||||
# 复制V2 Nginx配置参考
|
||||
cp "$REPO_PROXY_DIR"/config/nginx-brain-proxy-snippet.conf "$PROXY_DIR/config/" 2>/dev/null || true
|
||||
cp "$REPO_PROXY_DIR"/config/nginx-brain-proxy-v3-snippet.conf "$PROXY_DIR/config/" 2>/dev/null || true
|
||||
|
||||
# 复制共用文件(发邮件等)
|
||||
cp "$REPO_PROXY_DIR"/service/send-subscription.js "$PROXY_DIR/service/" 2>/dev/null || true
|
||||
|
|
@ -377,6 +387,15 @@ health_check() {
|
|||
echo " ❌ V2订阅服务: 端口3803无响应"
|
||||
fi
|
||||
|
||||
# V3订阅服务 (如果已部署)
|
||||
if curl -sf http://127.0.0.1:3805/health >/dev/null 2>&1; then
|
||||
HEALTH3=$(curl -sf http://127.0.0.1:3805/health)
|
||||
USERS3=$(echo "$HEALTH3" | grep -o '"users_count":[0-9]*' | cut -d: -f2)
|
||||
echo " ✅ V3订阅服务: 正常 (${USERS3:-0}个用户) · 光湖语言世界"
|
||||
else
|
||||
echo " ⚠️ V3订阅服务: 端口3805无响应 (未部署或未启动)"
|
||||
fi
|
||||
|
||||
# Nginx反向代理
|
||||
if systemctl is-active --quiet nginx 2>/dev/null; then
|
||||
if curl -sf http://127.0.0.1:80/api/proxy-v2/health >/dev/null 2>&1; then
|
||||
|
|
@ -468,6 +487,149 @@ list_users() {
|
|||
node "$PROXY_DIR/service/user-manager.js" list
|
||||
}
|
||||
|
||||
# ── deploy-v3: 部署V3测试环境 ──────────────────
|
||||
# V2继续运行·V3独立启动·通过/api/proxy-v3/测试
|
||||
deploy_v3() {
|
||||
echo "部署V3测试环境 (V2继续运行)..."
|
||||
deploy_services
|
||||
|
||||
# 加载密钥作为环境变量
|
||||
if [ -f "$PROXY_DIR/.env.keys" ]; then
|
||||
set -a
|
||||
# shellcheck source=/dev/null
|
||||
source "$PROXY_DIR/.env.keys"
|
||||
set +a
|
||||
fi
|
||||
|
||||
# 启动V3 PM2进程
|
||||
cd "$PROXY_DIR" || { echo "❌ 无法进入 $PROXY_DIR"; return 1; }
|
||||
pm2 startOrRestart ecosystem.brain-proxy-v3.config.js --update-env
|
||||
pm2 save
|
||||
|
||||
# 添加V3 Nginx测试路径
|
||||
NGINX_CONF=""
|
||||
for candidate in /etc/nginx/sites-enabled/zhuyuan-brain.conf /etc/nginx/sites-enabled/default; do
|
||||
if [ -f "$candidate" ]; then
|
||||
NGINX_CONF="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$NGINX_CONF" ] && ! grep -q "proxy-v3" "$NGINX_CONF" 2>/dev/null; then
|
||||
echo " 添加V3测试路径..."
|
||||
sed -i '/^}/i\
|
||||
# ─── 光湖语言世界V3测试 (端口 3805) ───\
|
||||
location /api/proxy-v3/ {\
|
||||
proxy_pass http://127.0.0.1:3805/;\
|
||||
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;\
|
||||
add_header X-Frame-Options DENY always;\
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate" always;\
|
||||
}' "$NGINX_CONF" || true
|
||||
echo " ✅ V3测试路径已添加"
|
||||
else
|
||||
echo " proxy-v3配置已存在或Nginx配置未找到"
|
||||
fi
|
||||
|
||||
if nginx -t 2>/dev/null; then
|
||||
nginx -s reload 2>/dev/null || systemctl reload nginx 2>/dev/null || true
|
||||
echo " ✅ Nginx已重载"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "════════════════════════════════════════"
|
||||
echo "✅ V3测试环境已就绪"
|
||||
echo ""
|
||||
echo "测试方法:"
|
||||
echo " V3订阅: https://域名/api/proxy-v3/sub/{token}"
|
||||
echo " V3仪表盘: https://域名/api/proxy-v3/dashboard/{token}"
|
||||
echo " V3健康: https://域名/api/proxy-v3/health"
|
||||
echo ""
|
||||
echo "V2继续运行在 /api/proxy-v2/ (端口3803)"
|
||||
echo "V3测试运行在 /api/proxy-v3/ (端口3805)"
|
||||
echo ""
|
||||
echo "测试通过后执行: bash deploy-brain-proxy.sh switch-v3"
|
||||
echo "════════════════════════════════════════"
|
||||
|
||||
# V3健康检查
|
||||
sleep 2
|
||||
if curl -sf http://127.0.0.1:3805/health >/dev/null 2>&1; then
|
||||
HEALTH=$(curl -sf http://127.0.0.1:3805/health)
|
||||
echo " ✅ V3订阅服务: 正常"
|
||||
echo " $HEALTH"
|
||||
else
|
||||
echo " ⚠️ V3订阅服务(3805)尚未就绪,请稍等几秒后检查"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── switch-v3: 切换V2→V3 ──────────────────────
|
||||
# 用户刷新订阅即获取V3配置
|
||||
switch_v3() {
|
||||
echo "切换 /api/proxy-v2/ → V3 (端口3805)..."
|
||||
|
||||
NGINX_CONF=""
|
||||
for candidate in /etc/nginx/sites-enabled/zhuyuan-brain.conf /etc/nginx/sites-enabled/default; do
|
||||
if [ -f "$candidate" ]; then
|
||||
NGINX_CONF="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$NGINX_CONF" ]; then
|
||||
echo "❌ 未找到Nginx配置文件"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 验证V3服务是否运行
|
||||
if ! curl -sf http://127.0.0.1:3805/health >/dev/null 2>&1; then
|
||||
echo "❌ V3服务(3805)未运行!请先执行 deploy-v3"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 备份当前Nginx配置
|
||||
cp "$NGINX_CONF" "${NGINX_CONF}.v2-backup"
|
||||
echo " ✅ 已备份Nginx配置到 ${NGINX_CONF}.v2-backup"
|
||||
|
||||
# 将 /api/proxy-v2/ 的 proxy_pass 从 3803 改为 3805
|
||||
if grep -q "127\.0\.0\.1:3803" "$NGINX_CONF" 2>/dev/null; then
|
||||
sed -i 's|127\.0\.0\.1:3803|127.0.0.1:3805|g' "$NGINX_CONF"
|
||||
echo " ✅ /api/proxy-v2/ 已切换到V3 (3803→3805)"
|
||||
else
|
||||
echo " ⚠️ 未找到3803端口配置,可能已切换"
|
||||
fi
|
||||
|
||||
# 重载Nginx
|
||||
if nginx -t 2>/dev/null; then
|
||||
nginx -s reload 2>/dev/null || systemctl reload nginx 2>/dev/null || true
|
||||
echo " ✅ Nginx已重载"
|
||||
else
|
||||
echo " ❌ Nginx配置验证失败,回滚..."
|
||||
cp "${NGINX_CONF}.v2-backup" "$NGINX_CONF"
|
||||
nginx -s reload 2>/dev/null || systemctl reload nginx 2>/dev/null || true
|
||||
echo " ✅ 已回滚到V2配置"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "════════════════════════════════════════"
|
||||
echo "✅ V2→V3 切换完成!"
|
||||
echo ""
|
||||
echo "现在 /api/proxy-v2/sub/{token} 指向V3 (端口3805)"
|
||||
echo "用户刷新订阅即可获取「光湖语言世界」配置"
|
||||
echo ""
|
||||
echo "回滚方法 (如有问题):"
|
||||
echo " cp ${NGINX_CONF}.v2-backup $NGINX_CONF"
|
||||
echo " nginx -s reload"
|
||||
echo "════════════════════════════════════════"
|
||||
}
|
||||
|
||||
# ── 执行 ──────────────────────────────────────
|
||||
case "$ACTION" in
|
||||
install) install ;;
|
||||
|
|
@ -477,8 +639,10 @@ case "$ACTION" in
|
|||
add-user) add_user "$@" ;;
|
||||
remove-user) remove_user "$@" ;;
|
||||
list-users) list_users ;;
|
||||
deploy-v3) deploy_v3 ;;
|
||||
switch-v3) switch_v3 ;;
|
||||
*)
|
||||
echo "用法: bash deploy-brain-proxy.sh {install|update|status|restart|add-user|remove-user|list-users}"
|
||||
echo "用法: bash deploy-brain-proxy.sh {install|update|status|restart|add-user|remove-user|list-users|deploy-v3|switch-v3}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
// ═══════════════════════════════════════════════
|
||||
// 光湖语言世界 V3 · PM2 大脑服务器代理配置
|
||||
// 部署在 ZY-SVR-005 (43.156.237.110) · 大脑服务器
|
||||
//
|
||||
// V3独立于V2运行,测试通过后切换Nginx即可
|
||||
// V2进程 (ecosystem.brain-proxy.config.js) 继续运行
|
||||
//
|
||||
// 切换方式:
|
||||
// 测试中: /api/proxy-v3/ → 3805
|
||||
// 切换后: /api/proxy-v2/ → 3805 (Nginx改一行)
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: 'zy-proxy-v3-sub',
|
||||
version: '3.0.0',
|
||||
script: '/opt/zhuyuan-brain/proxy/service/subscription-server-v3.js',
|
||||
instances: 1,
|
||||
exec_mode: 'fork',
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
ZY_PROXY_V3_PORT: 3805,
|
||||
ZY_BRAIN_PROXY_DIR: '/opt/zhuyuan-brain/proxy'
|
||||
},
|
||||
max_memory_restart: '128M',
|
||||
log_file: '/opt/zhuyuan-brain/proxy/logs/subscription-v3.log',
|
||||
error_file: '/opt/zhuyuan-brain/proxy/logs/subscription-v3-error.log',
|
||||
time: true
|
||||
},
|
||||
{
|
||||
name: 'zy-proxy-guardian',
|
||||
version: '3.0.0',
|
||||
script: '/opt/zhuyuan-brain/proxy/service/proxy-guardian.js',
|
||||
instances: 1,
|
||||
exec_mode: 'fork',
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
ZY_PROXY_DATA_DIR: '/opt/zhuyuan-brain/proxy/data',
|
||||
ZY_PROXY_LOG_DIR: '/opt/zhuyuan-brain/proxy/logs',
|
||||
ZY_BRAIN_PROXY_DIR: '/opt/zhuyuan-brain/proxy'
|
||||
},
|
||||
max_memory_restart: '64M',
|
||||
log_file: '/opt/zhuyuan-brain/proxy/logs/guardian.log',
|
||||
error_file: '/opt/zhuyuan-brain/proxy/logs/guardian-error.log',
|
||||
time: true
|
||||
},
|
||||
{
|
||||
name: 'zy-reverse-boost',
|
||||
version: '3.0.0',
|
||||
script: '/opt/zhuyuan-brain/proxy/service/reverse-boost-agent.js',
|
||||
instances: 1,
|
||||
exec_mode: 'fork',
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
ZY_BRAIN_PROXY_DIR: '/opt/zhuyuan-brain/proxy'
|
||||
},
|
||||
max_memory_restart: '64M',
|
||||
log_file: '/opt/zhuyuan-brain/proxy/logs/reverse-boost.log',
|
||||
error_file: '/opt/zhuyuan-brain/proxy/logs/reverse-boost-error.log',
|
||||
time: true
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
#!/usr/bin/env node
|
||||
// ═══════════════════════════════════════════════
|
||||
// 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001
|
||||
// 📜 Copyright: 国作登字-2026-A-00037559
|
||||
// ═══════════════════════════════════════════════
|
||||
// server/proxy/service/llm-router.js
|
||||
// 🤖 AI大模型动态路由器
|
||||
//
|
||||
// 核心原则 (冰朔指令):
|
||||
// - 不写死模型,系统自动检测+按需调用
|
||||
// - 调用失败自动切换下一个
|
||||
// - 所有模型都失败 → 推送邮件给冰朔
|
||||
//
|
||||
// 使用仓库已配置的 ZY_LLM_API_KEY + ZY_LLM_BASE_URL
|
||||
// secrets-manifest.json 确认此KEY支持多模型动态路由
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
'use strict';
|
||||
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
|
||||
// ── 模型优先级列表(自动降级)──────────────────
|
||||
// API网关会根据可用性自动路由,但我们仍按优先级尝试
|
||||
const MODEL_PRIORITY = [
|
||||
'deepseek-chat',
|
||||
'qwen-turbo',
|
||||
'moonshot-v1-8k',
|
||||
'glm-4-flash',
|
||||
'claude-3-haiku-20240307',
|
||||
'gpt-4o-mini'
|
||||
];
|
||||
|
||||
// ── 路由器状态 ──────────────────────────────
|
||||
const routerState = {
|
||||
total_calls: 0,
|
||||
successful_calls: 0,
|
||||
failed_calls: 0,
|
||||
last_success: null,
|
||||
last_error: null,
|
||||
model_stats: {} // 每个模型的调用统计
|
||||
};
|
||||
|
||||
/**
|
||||
* 调用LLM API(自动模型降级)
|
||||
* @param {string} prompt - 用户/系统提示
|
||||
* @param {Object} options - 可选配置
|
||||
* @param {string} options.systemPrompt - 系统提示
|
||||
* @param {number} options.maxTokens - 最大token数
|
||||
* @param {number} options.timeout - 超时时间(ms)
|
||||
* @param {string[]} options.preferModels - 优先尝试的模型列表
|
||||
* @returns {Promise<{content: string, model: string, usage: Object}|null>}
|
||||
*/
|
||||
async function callLLM(prompt, options = {}) {
|
||||
const apiKey = process.env.ZY_LLM_API_KEY || '';
|
||||
const baseUrl = process.env.ZY_LLM_BASE_URL || '';
|
||||
|
||||
if (!apiKey || !baseUrl) {
|
||||
console.log('[LLM路由器] API未配置 (ZY_LLM_API_KEY / ZY_LLM_BASE_URL)');
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
systemPrompt = '你是光湖语言世界VPN系统的AI助手。请简洁、准确地回答问题。',
|
||||
maxTokens = 500,
|
||||
timeout = 30000,
|
||||
preferModels = null
|
||||
} = options;
|
||||
|
||||
const modelsToTry = preferModels || MODEL_PRIORITY;
|
||||
routerState.total_calls++;
|
||||
|
||||
for (const model of modelsToTry) {
|
||||
try {
|
||||
const result = await _callSingleModel(baseUrl, apiKey, model, prompt, systemPrompt, maxTokens, timeout);
|
||||
if (result) {
|
||||
routerState.successful_calls++;
|
||||
routerState.last_success = {
|
||||
model,
|
||||
time: new Date().toISOString()
|
||||
};
|
||||
|
||||
// 更新模型统计
|
||||
if (!routerState.model_stats[model]) {
|
||||
routerState.model_stats[model] = { success: 0, fail: 0 };
|
||||
}
|
||||
routerState.model_stats[model].success++;
|
||||
|
||||
return result;
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(`[LLM路由器] 模型 ${model} 调用失败: ${err.message},尝试下一个...`);
|
||||
|
||||
if (!routerState.model_stats[model]) {
|
||||
routerState.model_stats[model] = { success: 0, fail: 0 };
|
||||
}
|
||||
routerState.model_stats[model].fail++;
|
||||
}
|
||||
}
|
||||
|
||||
// 所有模型都失败
|
||||
routerState.failed_calls++;
|
||||
routerState.last_error = {
|
||||
time: new Date().toISOString(),
|
||||
message: `所有模型(${modelsToTry.length}个)均调用失败`
|
||||
};
|
||||
|
||||
console.error(`[LLM路由器] ❌ 所有模型均不可用`);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用单个模型
|
||||
*/
|
||||
function _callSingleModel(baseUrl, apiKey, model, prompt, systemPrompt, maxTokens, timeout) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let urlObj;
|
||||
try {
|
||||
urlObj = new URL(baseUrl);
|
||||
} catch {
|
||||
// 如果baseUrl不含协议,加上https
|
||||
urlObj = new URL(`https://${baseUrl}`);
|
||||
}
|
||||
|
||||
const postData = JSON.stringify({
|
||||
model,
|
||||
messages: [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: prompt }
|
||||
],
|
||||
max_tokens: maxTokens,
|
||||
temperature: 0.3
|
||||
});
|
||||
|
||||
// 构建路径:如果baseUrl已包含chat/completions路径则使用
|
||||
// 否则追加 /v1/chat/completions
|
||||
let apiPath = urlObj.pathname;
|
||||
if (!apiPath || apiPath === '/') {
|
||||
apiPath = '/v1/chat/completions';
|
||||
} else if (!apiPath.includes('/chat/completions')) {
|
||||
apiPath = apiPath.replace(/\/+$/, '') + '/chat/completions';
|
||||
}
|
||||
|
||||
const options = {
|
||||
hostname: urlObj.hostname,
|
||||
port: urlObj.port || (urlObj.protocol === 'http:' ? 80 : 443),
|
||||
path: apiPath,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
'Content-Length': Buffer.byteLength(postData)
|
||||
},
|
||||
timeout
|
||||
};
|
||||
|
||||
const httpModule = urlObj.protocol === 'http:' ? http : https;
|
||||
|
||||
const req = httpModule.request(options, (res) => {
|
||||
let data = '';
|
||||
res.on('data', (d) => { data += d; });
|
||||
res.on('end', () => {
|
||||
try {
|
||||
if (res.statusCode >= 400) {
|
||||
reject(new Error(`HTTP ${res.statusCode}: 请求失败`));
|
||||
return;
|
||||
}
|
||||
const json = JSON.parse(data);
|
||||
const content = json.choices?.[0]?.message?.content;
|
||||
if (!content) {
|
||||
reject(new Error('响应中无content字段'));
|
||||
return;
|
||||
}
|
||||
resolve({
|
||||
content,
|
||||
model: json.model || model,
|
||||
usage: json.usage || null
|
||||
});
|
||||
} catch (e) {
|
||||
reject(new Error(`JSON解析失败: ${e.message}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (err) => reject(err));
|
||||
req.on('timeout', () => {
|
||||
req.destroy();
|
||||
reject(new Error(`超时(${timeout}ms)`));
|
||||
});
|
||||
|
||||
req.write(postData);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取路由器状态
|
||||
*/
|
||||
function getRouterStatus() {
|
||||
return {
|
||||
...routerState,
|
||||
api_configured: !!(process.env.ZY_LLM_API_KEY && process.env.ZY_LLM_BASE_URL),
|
||||
models_available: MODEL_PRIORITY
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { callLLM, getRouterStatus, MODEL_PRIORITY };
|
||||
|
|
@ -30,10 +30,8 @@ const path = require('path');
|
|||
const https = require('https');
|
||||
const http = require('http');
|
||||
|
||||
const DATA_DIR = process.env.ZY_PROXY_DATA_DIR || '/opt/zhuyuan/proxy/data';
|
||||
const LOG_DIR = process.env.ZY_PROXY_LOG_DIR || '/opt/zhuyuan/proxy/logs';
|
||||
const LLM_API_KEY = process.env.ZY_LLM_API_KEY || '';
|
||||
const LLM_BASE_URL = process.env.ZY_LLM_BASE_URL || '';
|
||||
const DATA_DIR = process.env.ZY_PROXY_DATA_DIR || '/opt/zhuyuan-brain/proxy/data';
|
||||
const LOG_DIR = process.env.ZY_PROXY_LOG_DIR || '/opt/zhuyuan-brain/proxy/logs';
|
||||
const GUARDIAN_FILE = path.join(DATA_DIR, 'guardian-status.json');
|
||||
const CHECK_INTERVAL = 10 * 60 * 1000; // 10分钟
|
||||
|
||||
|
|
@ -101,7 +99,7 @@ function checkPort443() {
|
|||
// ── 检查订阅服务 ─────────────────────────────
|
||||
function checkSubscriptionService() {
|
||||
return new Promise((resolve) => {
|
||||
const req = http.get('http://127.0.0.1:3802/health', { timeout: 5000 }, (res) => {
|
||||
const req = http.get('http://127.0.0.1:3803/health', { timeout: 5000 }, (res) => {
|
||||
let data = '';
|
||||
res.on('data', (d) => { data += d; });
|
||||
res.on('end', () => {
|
||||
|
|
@ -201,7 +199,7 @@ function autoFix(checkName) {
|
|||
|
||||
case 'subscription_service':
|
||||
// 通过PM2重启订阅服务
|
||||
const pmResult = runCmd('pm2 restart zy-proxy-sub', 15000);
|
||||
const pmResult = runCmd('pm2 restart zy-proxy-v2-sub', 15000);
|
||||
if (pmResult.ok) {
|
||||
console.log('[守护Agent] 订阅服务已重启');
|
||||
return true;
|
||||
|
|
@ -213,14 +211,17 @@ function autoFix(checkName) {
|
|||
}
|
||||
}
|
||||
|
||||
// ── 调用LLM推理 ─────────────────────────────
|
||||
// ── 调用LLM推理 (V3: 使用llm-router动态路由) ──
|
||||
async function consultLLM(issue) {
|
||||
if (!LLM_API_KEY || !LLM_BASE_URL) {
|
||||
console.log('[守护Agent] LLM API未配置,跳过推理');
|
||||
let llmRouter;
|
||||
try {
|
||||
llmRouter = require('./llm-router');
|
||||
} catch {
|
||||
console.log('[守护Agent] LLM路由器未加载,跳过推理');
|
||||
return null;
|
||||
}
|
||||
|
||||
const prompt = `你是铸渊专线的代理守护Agent。以下是当前检测到的问题:
|
||||
const prompt = `你是光湖语言世界VPN的守护Agent。以下是当前检测到的问题:
|
||||
|
||||
问题描述: ${issue.detail}
|
||||
检查项: ${issue.checkName}
|
||||
|
|
@ -229,46 +230,13 @@ async function consultLLM(issue) {
|
|||
|
||||
请分析可能的原因并给出解决建议。回答要简洁明确。`;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const urlObj = new URL(LLM_BASE_URL);
|
||||
const postData = JSON.stringify({
|
||||
model: 'deepseek-chat',
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
max_tokens: 500
|
||||
});
|
||||
|
||||
const options = {
|
||||
hostname: urlObj.hostname,
|
||||
port: urlObj.port || 443,
|
||||
path: '/v1/chat/completions',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${LLM_API_KEY}`,
|
||||
'Content-Length': Buffer.byteLength(postData)
|
||||
},
|
||||
timeout: 30000
|
||||
};
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
let data = '';
|
||||
res.on('data', (d) => { data += d; });
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
const answer = json.choices?.[0]?.message?.content || '无响应';
|
||||
resolve(answer);
|
||||
} catch {
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', () => resolve(null));
|
||||
req.on('timeout', () => { req.destroy(); resolve(null); });
|
||||
req.write(postData);
|
||||
req.end();
|
||||
const result = await llmRouter.callLLM(prompt, {
|
||||
systemPrompt: '你是光湖语言世界VPN系统的守护Agent AI。负责诊断代理服务异常并给出修复建议。',
|
||||
maxTokens: 500,
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
return result ? result.content : null;
|
||||
}
|
||||
|
||||
// ── 发送告警邮件 ─────────────────────────────
|
||||
|
|
@ -383,9 +351,9 @@ async function patrol() {
|
|||
}
|
||||
|
||||
// ── 启动守护循环 ─────────────────────────────
|
||||
console.log('🛡️ 铸渊专线守护Agent启动');
|
||||
console.log('🛡️ 光湖语言世界 · 守护Agent启动');
|
||||
console.log(` 巡检间隔: ${CHECK_INTERVAL / 1000}秒`);
|
||||
console.log(` LLM API: ${LLM_API_KEY ? '已配置' : '未配置'}`);
|
||||
console.log(` LLM路由器: 动态多模型 (通过llm-router.js)`);
|
||||
|
||||
// 立即执行一次
|
||||
patrol().catch(console.error);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,293 @@
|
|||
#!/usr/bin/env node
|
||||
// ═══════════════════════════════════════════════
|
||||
// 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001
|
||||
// 📜 Copyright: 国作登字-2026-A-00037559
|
||||
// ═══════════════════════════════════════════════
|
||||
// server/proxy/service/reverse-boost-agent.js
|
||||
// 🚀 反向加速Agent · 服务端网络优化活模块
|
||||
//
|
||||
// 核心理念 (冰朔定根):
|
||||
// 服务器只是桥梁,用户的光纤才是主引擎。
|
||||
// 通过减少服务器瓶颈,让用户100M/300M/500M
|
||||
// 的光纤能力尽可能穿透到外网。
|
||||
//
|
||||
// 优化策略:
|
||||
// 1. BBR拥塞控制检测与优化
|
||||
// 2. MTU自动探测
|
||||
// 3. TCP连接参数优化
|
||||
// 4. 系统级网络栈调优
|
||||
// 5. Xray连接池配置优化
|
||||
//
|
||||
// 运行方式: PM2 managed (zy-reverse-boost)
|
||||
// 检查间隔: 每15分钟
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
'use strict';
|
||||
|
||||
const { execSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const PROXY_DIR = process.env.ZY_BRAIN_PROXY_DIR || '/opt/zhuyuan-brain/proxy';
|
||||
const DATA_DIR = path.join(PROXY_DIR, 'data');
|
||||
const BOOST_STATUS_FILE = path.join(DATA_DIR, 'reverse-boost-status.json');
|
||||
const CHECK_INTERVAL = 15 * 60 * 1000; // 15分钟
|
||||
|
||||
// ── 安全执行命令 ──────────────────────────────
|
||||
function runCmd(cmd, timeout = 10000) {
|
||||
try {
|
||||
return { ok: true, output: execSync(cmd, { encoding: 'utf8', timeout }).trim() };
|
||||
} catch (err) {
|
||||
return { ok: false, output: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
// ── 检查BBR状态 ───────────────────────────────
|
||||
function checkBBR() {
|
||||
const result = runCmd('sysctl net.ipv4.tcp_congestion_control');
|
||||
if (result.ok) {
|
||||
const algo = result.output.split('=').pop().trim();
|
||||
return {
|
||||
ok: true,
|
||||
algorithm: algo,
|
||||
is_bbr: algo === 'bbr',
|
||||
detail: `当前拥塞控制: ${algo}`
|
||||
};
|
||||
}
|
||||
return { ok: false, algorithm: 'unknown', is_bbr: false, detail: '无法检查BBR' };
|
||||
}
|
||||
|
||||
// ── 检查可用拥塞控制算法 ──────────────────────
|
||||
function getAvailableAlgorithms() {
|
||||
const result = runCmd('sysctl net.ipv4.tcp_available_congestion_control');
|
||||
if (result.ok) {
|
||||
return result.output.split('=').pop().trim().split(/\s+/);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// ── 检查当前MTU ───────────────────────────────
|
||||
function checkMTU() {
|
||||
const result = runCmd("ip link show | grep 'mtu' | head -5");
|
||||
if (result.ok) {
|
||||
const mtuMatch = result.output.match(/mtu\s+(\d+)/);
|
||||
return {
|
||||
ok: true,
|
||||
mtu: mtuMatch ? parseInt(mtuMatch[1], 10) : 1500,
|
||||
detail: result.output.split('\n')[0]
|
||||
};
|
||||
}
|
||||
return { ok: false, mtu: 1500, detail: '无法检查MTU' };
|
||||
}
|
||||
|
||||
// ── 检查TCP参数 ───────────────────────────────
|
||||
function checkTcpParams() {
|
||||
const params = {};
|
||||
|
||||
// TCP缓冲区大小
|
||||
const rmem = runCmd('sysctl net.ipv4.tcp_rmem');
|
||||
if (rmem.ok) params.tcp_rmem = rmem.output.split('=').pop().trim();
|
||||
|
||||
const wmem = runCmd('sysctl net.ipv4.tcp_wmem');
|
||||
if (wmem.ok) params.tcp_wmem = wmem.output.split('=').pop().trim();
|
||||
|
||||
// TCP快速打开
|
||||
const tfo = runCmd('sysctl net.ipv4.tcp_fastopen');
|
||||
if (tfo.ok) params.tcp_fastopen = tfo.output.split('=').pop().trim();
|
||||
|
||||
// TCP keep-alive
|
||||
const keepalive = runCmd('sysctl net.ipv4.tcp_keepalive_time');
|
||||
if (keepalive.ok) params.tcp_keepalive_time = keepalive.output.split('=').pop().trim();
|
||||
|
||||
// 连接跟踪
|
||||
const conntrack = runCmd('sysctl net.netfilter.nf_conntrack_max 2>/dev/null');
|
||||
if (conntrack.ok) params.conntrack_max = conntrack.output.split('=').pop().trim();
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
// ── 检查网络负载 ──────────────────────────────
|
||||
function checkNetworkLoad() {
|
||||
// 获取主网卡的流量统计
|
||||
const result = runCmd("cat /proc/net/dev | grep -E 'eth0|ens' | head -1");
|
||||
if (result.ok) {
|
||||
const parts = result.output.trim().split(/\s+/);
|
||||
if (parts.length >= 10) {
|
||||
return {
|
||||
ok: true,
|
||||
interface: parts[0].replace(':', ''),
|
||||
rx_bytes: parseInt(parts[1], 10),
|
||||
tx_bytes: parseInt(parts[9], 10),
|
||||
rx_packets: parseInt(parts[2], 10),
|
||||
tx_packets: parseInt(parts[10], 10)
|
||||
};
|
||||
}
|
||||
}
|
||||
return { ok: false };
|
||||
}
|
||||
|
||||
// ── 优化建议生成 ──────────────────────────────
|
||||
function generateOptimizations() {
|
||||
const optimizations = [];
|
||||
const applied = [];
|
||||
|
||||
// 1. 检查BBR
|
||||
const bbr = checkBBR();
|
||||
if (!bbr.is_bbr) {
|
||||
const available = getAvailableAlgorithms();
|
||||
if (available.includes('bbr')) {
|
||||
optimizations.push({
|
||||
type: 'bbr',
|
||||
priority: 'high',
|
||||
current: bbr.algorithm,
|
||||
recommended: 'bbr',
|
||||
description: '启用BBR拥塞控制可显著提升吞吐量',
|
||||
command: 'sysctl -w net.ipv4.tcp_congestion_control=bbr'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
applied.push('BBR拥塞控制已启用');
|
||||
}
|
||||
|
||||
// 2. 检查TCP快速打开
|
||||
const tcpParams = checkTcpParams();
|
||||
if (tcpParams.tcp_fastopen !== '3') {
|
||||
optimizations.push({
|
||||
type: 'tcp_fastopen',
|
||||
priority: 'medium',
|
||||
current: tcpParams.tcp_fastopen || 'unknown',
|
||||
recommended: '3',
|
||||
description: 'TCP Fast Open可减少握手延迟',
|
||||
command: 'sysctl -w net.ipv4.tcp_fastopen=3'
|
||||
});
|
||||
} else {
|
||||
applied.push('TCP Fast Open已启用');
|
||||
}
|
||||
|
||||
// 3. 检查TCP缓冲区
|
||||
// 推荐: rmem = 4096 131072 67108864, wmem = 4096 16384 67108864
|
||||
if (tcpParams.tcp_rmem) {
|
||||
const maxRmem = parseInt(tcpParams.tcp_rmem.split(/\s+/).pop(), 10);
|
||||
if (maxRmem < 67108864) {
|
||||
optimizations.push({
|
||||
type: 'tcp_rmem',
|
||||
priority: 'medium',
|
||||
current: tcpParams.tcp_rmem,
|
||||
recommended: '4096 131072 67108864',
|
||||
description: '增大TCP接收缓冲区可提升大文件传输速度',
|
||||
command: 'sysctl -w net.ipv4.tcp_rmem="4096 131072 67108864"'
|
||||
});
|
||||
} else {
|
||||
applied.push('TCP接收缓冲区已优化');
|
||||
}
|
||||
}
|
||||
|
||||
return { optimizations, applied, tcp_params: tcpParams };
|
||||
}
|
||||
|
||||
// ── 应用安全优化(仅无风险的参数)──────────────
|
||||
function applySafeOptimizations(optimizations) {
|
||||
const results = [];
|
||||
|
||||
for (const opt of optimizations) {
|
||||
// 只自动应用低风险优化
|
||||
if (opt.type === 'tcp_fastopen' || opt.type === 'bbr') {
|
||||
const result = runCmd(opt.command, 5000);
|
||||
results.push({
|
||||
type: opt.type,
|
||||
applied: result.ok,
|
||||
detail: result.ok ? `✅ 已应用: ${opt.description}` : `❌ 失败: ${result.output}`
|
||||
});
|
||||
if (result.ok) {
|
||||
console.log(`[反向加速] ✅ ${opt.description}`);
|
||||
}
|
||||
} else {
|
||||
results.push({
|
||||
type: opt.type,
|
||||
applied: false,
|
||||
detail: `⏭️ 跳过(需手动): ${opt.description}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// ── 读取/保存状态 ─────────────────────────────
|
||||
function readBoostStatus() {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(BOOST_STATUS_FILE, 'utf8'));
|
||||
} catch {
|
||||
return {
|
||||
checks: 0,
|
||||
optimizations_applied: 0,
|
||||
last_check: null,
|
||||
status: 'initializing'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function saveBoostStatus(status) {
|
||||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||
fs.writeFileSync(BOOST_STATUS_FILE, JSON.stringify(status, null, 2));
|
||||
}
|
||||
|
||||
// ── 主巡检 ────────────────────────────────────
|
||||
function boost() {
|
||||
console.log('[反向加速] 🚀 开始网络优化巡检...');
|
||||
|
||||
const status = readBoostStatus();
|
||||
status.checks++;
|
||||
status.last_check = new Date().toISOString();
|
||||
|
||||
// 检查系统网络状态
|
||||
const bbr = checkBBR();
|
||||
const mtu = checkMTU();
|
||||
const networkLoad = checkNetworkLoad();
|
||||
const { optimizations, applied, tcp_params } = generateOptimizations();
|
||||
|
||||
console.log(`[反向加速] BBR: ${bbr.is_bbr ? '✅ 已启用' : '❌ 未启用'} (${bbr.algorithm})`);
|
||||
console.log(`[反向加速] MTU: ${mtu.mtu}`);
|
||||
console.log(`[反向加速] 已优化项: ${applied.length}个`);
|
||||
|
||||
// 应用安全优化
|
||||
if (optimizations.length > 0) {
|
||||
console.log(`[反向加速] 发现${optimizations.length}个可优化项`);
|
||||
const results = applySafeOptimizations(optimizations);
|
||||
const appliedCount = results.filter(r => r.applied).length;
|
||||
status.optimizations_applied += appliedCount;
|
||||
status.last_optimizations = results;
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
status.status = 'active';
|
||||
status.current = {
|
||||
bbr: bbr,
|
||||
mtu: mtu,
|
||||
tcp_params: tcp_params,
|
||||
network_load: networkLoad.ok ? {
|
||||
interface: networkLoad.interface,
|
||||
rx_bytes: networkLoad.rx_bytes,
|
||||
tx_bytes: networkLoad.tx_bytes
|
||||
} : null,
|
||||
already_optimized: applied
|
||||
};
|
||||
|
||||
saveBoostStatus(status);
|
||||
|
||||
console.log(`[反向加速] 巡检完成 (第${status.checks}次)`);
|
||||
}
|
||||
|
||||
// ── 启动巡检循环 ──────────────────────────────
|
||||
console.log('🚀 光湖语言世界 · 反向加速Agent启动');
|
||||
console.log(` 巡检间隔: ${CHECK_INTERVAL / 1000}秒`);
|
||||
console.log(` 优化策略: BBR拥塞控制 + TCP Fast Open + 缓冲区调优`);
|
||||
console.log(` 核心理念: 服务器只是桥梁,用户光纤才是主引擎`);
|
||||
|
||||
// 立即执行一次
|
||||
try { boost(); } catch (err) { console.error('首次巡检失败:', err.message); }
|
||||
|
||||
// 定期执行
|
||||
setInterval(() => {
|
||||
try { boost(); } catch (err) { console.error('巡检异常:', err.message); }
|
||||
}, CHECK_INTERVAL);
|
||||
|
|
@ -0,0 +1,857 @@
|
|||
#!/usr/bin/env node
|
||||
// ═══════════════════════════════════════════════
|
||||
// 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001
|
||||
// 📜 Copyright: 国作登字-2026-A-00037559
|
||||
// ═══════════════════════════════════════════════
|
||||
// server/proxy/service/subscription-server-v3.js
|
||||
// 🌐 光湖语言世界 · 多用户订阅服务 — 冰朔开发维护
|
||||
//
|
||||
// V3升级 (从V2演进 · V2继续运行不受影响):
|
||||
// 品牌: 铸渊专线V2 → 光湖语言世界
|
||||
// 流量池: 2000GB硬切 (到量即停 · 非仅告警)
|
||||
// 配置优化: keep-alive 15s · 增强DIRECT分流
|
||||
// 反向加速: 零缓存直通 · splice模式
|
||||
// 仪表盘: /dashboard/{token} HTML页面
|
||||
//
|
||||
// 部署在大脑服务器 (ZY-SVR-005 · 43.156.237.110)
|
||||
// 每个邮箱一条独立专线,Token认证隔离
|
||||
//
|
||||
// 测试端口: 3805 (V2=3803继续运行)
|
||||
// 测试路径: /api/proxy-v3/sub/{token}
|
||||
// 测试通过后: Nginx切换 /api/proxy-v2/ → 3805
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
'use strict';
|
||||
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const url = require('url');
|
||||
|
||||
const PORT = process.env.ZY_PROXY_V3_PORT || 3805;
|
||||
const PROXY_DIR = process.env.ZY_BRAIN_PROXY_DIR || '/opt/zhuyuan-brain/proxy';
|
||||
const DATA_DIR = path.join(PROXY_DIR, 'data');
|
||||
const KEYS_FILE = path.join(PROXY_DIR, '.env.keys');
|
||||
const LIVE_NODES_FRESHNESS_MS = parseInt(process.env.ZY_CLOUD_HB_EXPIRY_MS || '600000', 10);
|
||||
|
||||
// 引入用户管理器
|
||||
const userManager = require('./user-manager');
|
||||
|
||||
// ── 加载密钥 ────────────────────────────────
|
||||
function loadKeys() {
|
||||
const keys = {};
|
||||
try {
|
||||
const content = fs.readFileSync(KEYS_FILE, 'utf8');
|
||||
for (const line of content.split('\n')) {
|
||||
if (line.startsWith('#') || !line.includes('=')) continue;
|
||||
const [key, ...vals] = line.split('=');
|
||||
keys[key.trim()] = vals.join('=').trim();
|
||||
}
|
||||
} catch (err) {
|
||||
keys.ZY_PROXY_REALITY_PUBLIC_KEY = process.env.ZY_PROXY_REALITY_PUBLIC_KEY || '';
|
||||
keys.ZY_PROXY_REALITY_SHORT_ID = process.env.ZY_PROXY_REALITY_SHORT_ID || '';
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
// ── 从环境变量或密钥文件读取值 ──────────────
|
||||
function getEnvOrKey(envName) {
|
||||
if (process.env[envName]) return process.env[envName];
|
||||
|
||||
try {
|
||||
const content = fs.readFileSync(KEYS_FILE, 'utf8');
|
||||
for (const line of content.split('\n')) {
|
||||
if (line.startsWith('#') || !line.includes('=')) continue;
|
||||
const [key, ...vals] = line.split('=');
|
||||
if (key.trim() === envName) {
|
||||
const val = vals.join('=').trim();
|
||||
if (val) return val;
|
||||
}
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
// ── 获取服务器IP ────────────────────────────
|
||||
function getServerHost() {
|
||||
return getEnvOrKey('ZY_BRAIN_HOST') || getEnvOrKey('ZY_SERVER_HOST') || '0.0.0.0';
|
||||
}
|
||||
|
||||
// ── 构建所有可用VPN节点 ──────────────────────
|
||||
// 优先从ZY-CLOUD活模块获取(动态·实时健康检查后的活节点)
|
||||
// 回退到静态配置(ZY-CLOUD未运行时)
|
||||
// V3: 节点名统一为"光湖"品牌
|
||||
function buildVpnNodes() {
|
||||
// 优先: 从ZY-CLOUD活模块的动态节点列表读取
|
||||
const liveNodesFile = path.join(DATA_DIR, 'nodes-live.json');
|
||||
try {
|
||||
const liveData = JSON.parse(fs.readFileSync(liveNodesFile, 'utf8'));
|
||||
// 检查数据是否新鲜(使用与ZY-CLOUD相同的心跳过期阈值)
|
||||
const age = Date.now() - new Date(liveData.updated_at).getTime();
|
||||
if (age < LIVE_NODES_FRESHNESS_MS * 2 && liveData.nodes && liveData.nodes.length > 0) {
|
||||
// V3: 重写节点名为光湖品牌
|
||||
return liveData.nodes.map(n => ({
|
||||
...n,
|
||||
name: n.name.replace(/铸渊专线V2/g, '光湖')
|
||||
}));
|
||||
}
|
||||
} catch { /* ZY-CLOUD未运行,回退到静态配置 */ }
|
||||
|
||||
// 回退: 从环境变量/密钥文件静态构建
|
||||
return buildStaticNodes();
|
||||
}
|
||||
|
||||
// ── 静态节点构建(回退用)────────────────────
|
||||
function buildStaticNodes() {
|
||||
const nodes = [];
|
||||
|
||||
// 节点1: 大脑服务器 (ZY-SVR-005 · 新加坡一区 · 4核8G · 主力)
|
||||
const brainHost = getEnvOrKey('ZY_BRAIN_HOST');
|
||||
const brainPbk = getEnvOrKey('ZY_PROXY_REALITY_PUBLIC_KEY');
|
||||
const brainSid = getEnvOrKey('ZY_PROXY_REALITY_SHORT_ID');
|
||||
if (brainHost && brainPbk && brainSid) {
|
||||
nodes.push({
|
||||
id: 'zy-brain-sg1',
|
||||
name: '🧠 光湖-SG1(大脑)',
|
||||
host: brainHost,
|
||||
port: 443,
|
||||
pbk: brainPbk,
|
||||
sid: brainSid,
|
||||
region: 'sg-zone1',
|
||||
priority: 1
|
||||
});
|
||||
}
|
||||
|
||||
// 节点2: 面孔服务器 (ZY-SVR-002 · 新加坡二区 · 2核8G · 备用)
|
||||
const faceHost = getEnvOrKey('ZY_FACE_HOST');
|
||||
const facePbk = getEnvOrKey('ZY_FACE_REALITY_PUBLIC_KEY');
|
||||
const faceSid = getEnvOrKey('ZY_FACE_REALITY_SHORT_ID');
|
||||
if (faceHost && facePbk && faceSid) {
|
||||
nodes.push({
|
||||
id: 'zy-face-sg2',
|
||||
name: '🏛️ 光湖-SG2(面孔)',
|
||||
host: faceHost,
|
||||
port: 443,
|
||||
pbk: facePbk,
|
||||
sid: faceSid,
|
||||
region: 'sg-zone2',
|
||||
priority: 2
|
||||
});
|
||||
}
|
||||
|
||||
// 节点3: CN中转 (国内→SG · 对国内用户低延迟)
|
||||
const cnHost = getEnvOrKey('ZY_CN_RELAY_HOST');
|
||||
const cnPort = parseInt(getEnvOrKey('ZY_CN_RELAY_PORT') || '2053', 10);
|
||||
// CN中转是TCP透传,Reality密钥用主力节点的(大脑)
|
||||
if (cnHost && brainPbk && brainSid) {
|
||||
nodes.push({
|
||||
id: 'zy-cn-relay',
|
||||
name: '🇨🇳 光湖-CN中转',
|
||||
host: cnHost,
|
||||
port: cnPort,
|
||||
pbk: brainPbk,
|
||||
sid: brainSid,
|
||||
region: 'cn-relay',
|
||||
priority: 3
|
||||
});
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
// ── 生成subscription-userinfo头 ──────────────
|
||||
// 共享流量池模型: total=池配额(2000GB),upload+download=池总用量
|
||||
// 使用缓存的池状态文件(traffic-monitor-v2每5分钟更新)以减少计算开销
|
||||
function generateUserInfoHeader(user) {
|
||||
const nextMonth = new Date();
|
||||
nextMonth.setMonth(nextMonth.getMonth() + 1);
|
||||
nextMonth.setDate(1);
|
||||
nextMonth.setHours(0, 0, 0, 0);
|
||||
|
||||
// 优先从缓存文件读取池状态 (traffic-monitor-v2每5分钟写入)
|
||||
let poolUpload = 0;
|
||||
let poolDownload = 0;
|
||||
try {
|
||||
const cached = JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'pool-quota-status.json'), 'utf8'));
|
||||
poolUpload = cached.pool_upload_bytes || 0;
|
||||
poolDownload = cached.pool_download_bytes || 0;
|
||||
} catch {
|
||||
// 缓存不可用时实时计算
|
||||
const poolStatus = userManager.getPoolStatus();
|
||||
poolUpload = poolStatus.pool_upload_bytes;
|
||||
poolDownload = poolStatus.pool_download_bytes;
|
||||
}
|
||||
|
||||
return `upload=${poolUpload}; download=${poolDownload}; total=${userManager.POOL_QUOTA_BYTES}; expire=${Math.floor(nextMonth.getTime() / 1000)}`;
|
||||
}
|
||||
|
||||
// ── 生成VLESS URI (Shadowrocket · 多节点) ─────
|
||||
function generateVlessUris(user, nodes) {
|
||||
return nodes.map(node => {
|
||||
const params = new URLSearchParams({
|
||||
encryption: 'none',
|
||||
flow: 'xtls-rprx-vision',
|
||||
security: 'reality',
|
||||
sni: 'www.microsoft.com',
|
||||
fp: 'chrome',
|
||||
pbk: node.pbk,
|
||||
sid: node.sid,
|
||||
type: 'tcp',
|
||||
headerType: 'none'
|
||||
});
|
||||
const label = encodeURIComponent(node.name);
|
||||
return `vless://${user.uuid}@${node.host}:${node.port}?${params.toString()}#${label}`;
|
||||
}).join('\n');
|
||||
}
|
||||
|
||||
// ── 生成Clash YAML配置 (多节点智能选路) ──────
|
||||
function generateClashYaml(user, nodes) {
|
||||
// 构建proxies块 — 每个节点一条
|
||||
const proxyBlocks = nodes.map(node => ` - name: "${node.name}"
|
||||
type: vless
|
||||
server: ${node.host}
|
||||
port: ${node.port}
|
||||
uuid: ${user.uuid}
|
||||
network: tcp
|
||||
tls: true
|
||||
udp: true
|
||||
flow: xtls-rprx-vision
|
||||
skip-cert-verify: false
|
||||
servername: www.microsoft.com
|
||||
reality-opts:
|
||||
public-key: ${node.pbk}
|
||||
short-id: ${node.sid}
|
||||
client-fingerprint: chrome`).join('\n');
|
||||
|
||||
// 节点名列表
|
||||
const nodeNames = nodes.map(n => ` - "${n.name}"`).join('\n');
|
||||
|
||||
// url-test自动选择组 (延迟最低的自动生效)
|
||||
const autoSelectBlock = nodes.length > 1 ? `
|
||||
- name: "♻️ 自动选择"
|
||||
type: url-test
|
||||
proxies:
|
||||
${nodeNames}
|
||||
url: "http://www.gstatic.com/generate_204"
|
||||
interval: 300
|
||||
tolerance: 50
|
||||
` : '';
|
||||
|
||||
// 主代理组
|
||||
const mainProxies = nodes.length > 1
|
||||
? ` - "♻️ 自动选择"\n${nodeNames}\n - DIRECT`
|
||||
: `${nodeNames}\n - DIRECT`;
|
||||
|
||||
// AI/开发工具代理组的节点列表
|
||||
const toolProxies = nodes.length > 1
|
||||
? ` - "♻️ 自动选择"\n${nodeNames}`
|
||||
: nodeNames;
|
||||
|
||||
return `# 光湖语言世界 · ${user.label} 的独立专线 — 冰朔开发维护
|
||||
# 自动生成 · ${new Date().toISOString()}
|
||||
# ⚠️ 此配置为 ${user.email} 专属,请勿分享
|
||||
# 每人一条独立线路 · ${nodes.length}个节点智能选路
|
||||
# 服务器只是桥梁,你的光纤才是主引擎 🚀
|
||||
|
||||
# ── 全局设置 ──────────────────────────────
|
||||
mixed-port: 7890
|
||||
allow-lan: false
|
||||
mode: rule
|
||||
log-level: info
|
||||
ipv6: false
|
||||
unified-delay: true
|
||||
tcp-concurrent: true
|
||||
find-process-mode: strict
|
||||
geodata-mode: true
|
||||
geodata-loader: standard
|
||||
global-client-fingerprint: chrome
|
||||
keep-alive-interval: 15
|
||||
external-controller: 127.0.0.1:9090
|
||||
|
||||
# ── GeoData 数据源 ────────────────────────
|
||||
geox-url:
|
||||
geoip: "https://cdn.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.dat"
|
||||
geosite: "https://cdn.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geosite.dat"
|
||||
mmdb: "https://cdn.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/country.mmdb"
|
||||
|
||||
# ── DNS 设置 (fake-ip模式) ────────────────
|
||||
dns:
|
||||
enable: true
|
||||
listen: 0.0.0.0:1053
|
||||
ipv6: false
|
||||
enhanced-mode: fake-ip
|
||||
fake-ip-range: 198.18.0.1/16
|
||||
fake-ip-filter:
|
||||
- "*.lan"
|
||||
- "*.local"
|
||||
- "*.direct"
|
||||
- "localhost.ptlogin2.qq.com"
|
||||
- "dns.msftncsi.com"
|
||||
- "*.msftconnecttest.com"
|
||||
- "*.msftncsi.com"
|
||||
- "+.stun.*.*"
|
||||
- "+.stun.*.*.*"
|
||||
- "lens.l.google.com"
|
||||
- "stun.l.google.com"
|
||||
- "time.*.com"
|
||||
- "time.*.gov"
|
||||
- "time.*.edu.cn"
|
||||
- "time.*.apple.com"
|
||||
- "time-ios.apple.com"
|
||||
- "time-macos.apple.com"
|
||||
- "ntp.*.com"
|
||||
- "+.pool.ntp.org"
|
||||
- "music.163.com"
|
||||
- "*.music.163.com"
|
||||
- "*.126.net"
|
||||
default-nameserver:
|
||||
- 223.5.5.5
|
||||
- 119.29.29.29
|
||||
- 1.0.0.1
|
||||
nameserver:
|
||||
- https://dns.alidns.com/dns-query
|
||||
- https://doh.pub/dns-query
|
||||
fallback:
|
||||
- https://1.0.0.1/dns-query
|
||||
- https://dns.google/dns-query
|
||||
- tls://8.8.4.4:853
|
||||
fallback-filter:
|
||||
geoip: true
|
||||
geoip-code: CN
|
||||
geosite:
|
||||
- gfw
|
||||
ipcidr:
|
||||
- 240.0.0.0/4
|
||||
domain:
|
||||
- "+.google.com"
|
||||
- "+.facebook.com"
|
||||
- "+.youtube.com"
|
||||
- "+.github.com"
|
||||
- "+.googleapis.com"
|
||||
- "+.openai.com"
|
||||
- "+.anthropic.com"
|
||||
|
||||
# ── 域名嗅探 ──────────────────────────────
|
||||
sniffer:
|
||||
enable: true
|
||||
force-dns-mapping: true
|
||||
parse-pure-ip: true
|
||||
override-destination: true
|
||||
sniff:
|
||||
HTTP:
|
||||
ports: [80, 8080-8880]
|
||||
override-destination: true
|
||||
TLS:
|
||||
ports: [443, 8443]
|
||||
QUIC:
|
||||
ports: [443, 8443]
|
||||
skip-domain:
|
||||
- "Mijia Cloud"
|
||||
- "+.push.apple.com"
|
||||
|
||||
# ── 代理节点 (${nodes.length}个 · 智能选路) ──
|
||||
proxies:
|
||||
${proxyBlocks}
|
||||
|
||||
# ── 代理组 ────────────────────────────────
|
||||
proxy-groups:
|
||||
- name: "🌐 光湖语言世界"
|
||||
type: select
|
||||
proxies:
|
||||
${mainProxies}
|
||||
${autoSelectBlock}
|
||||
- name: "🤖 AI服务"
|
||||
type: select
|
||||
proxies:
|
||||
${toolProxies}
|
||||
|
||||
- name: "💻 开发工具"
|
||||
type: select
|
||||
proxies:
|
||||
${toolProxies}
|
||||
|
||||
- name: "📺 流媒体"
|
||||
type: select
|
||||
proxies:
|
||||
${toolProxies}
|
||||
|
||||
# ── 路由规则 ──────────────────────────────
|
||||
rules:
|
||||
# AI服务
|
||||
- DOMAIN-SUFFIX,openai.com,🤖 AI服务
|
||||
- DOMAIN-SUFFIX,anthropic.com,🤖 AI服务
|
||||
- DOMAIN-SUFFIX,claude.ai,🤖 AI服务
|
||||
- DOMAIN-SUFFIX,chatgpt.com,🤖 AI服务
|
||||
- DOMAIN-SUFFIX,gemini.google.com,🤖 AI服务
|
||||
- DOMAIN-SUFFIX,perplexity.ai,🤖 AI服务
|
||||
- DOMAIN-SUFFIX,poe.com,🤖 AI服务
|
||||
- DOMAIN-SUFFIX,deepseek.com,🤖 AI服务
|
||||
- DOMAIN-SUFFIX,siliconflow.cn,🤖 AI服务
|
||||
|
||||
# 开发工具
|
||||
- DOMAIN-SUFFIX,github.com,💻 开发工具
|
||||
- DOMAIN-SUFFIX,githubusercontent.com,💻 开发工具
|
||||
- DOMAIN-SUFFIX,github.io,💻 开发工具
|
||||
- DOMAIN-SUFFIX,githubassets.com,💻 开发工具
|
||||
- DOMAIN-SUFFIX,copilot.microsoft.com,💻 开发工具
|
||||
- DOMAIN-SUFFIX,npmjs.com,💻 开发工具
|
||||
- DOMAIN-SUFFIX,docker.com,💻 开发工具
|
||||
- DOMAIN-SUFFIX,docker.io,💻 开发工具
|
||||
- DOMAIN-SUFFIX,stackoverflow.com,💻 开发工具
|
||||
- DOMAIN-SUFFIX,pypi.org,💻 开发工具
|
||||
|
||||
# 流媒体
|
||||
- DOMAIN-SUFFIX,youtube.com,📺 流媒体
|
||||
- DOMAIN-SUFFIX,googlevideo.com,📺 流媒体
|
||||
- DOMAIN-SUFFIX,netflix.com,📺 流媒体
|
||||
- DOMAIN-SUFFIX,spotify.com,📺 流媒体
|
||||
- DOMAIN-SUFFIX,tiktok.com,📺 流媒体
|
||||
|
||||
# 社交媒体
|
||||
- DOMAIN-SUFFIX,twitter.com,🌐 光湖语言世界
|
||||
- DOMAIN-SUFFIX,x.com,🌐 光湖语言世界
|
||||
- DOMAIN-SUFFIX,google.com,🌐 光湖语言世界
|
||||
- DOMAIN-SUFFIX,googleapis.com,🌐 光湖语言世界
|
||||
- DOMAIN-SUFFIX,telegram.org,🌐 光湖语言世界
|
||||
- DOMAIN-SUFFIX,t.me,🌐 光湖语言世界
|
||||
- DOMAIN-SUFFIX,instagram.com,🌐 光湖语言世界
|
||||
- DOMAIN-SUFFIX,facebook.com,🌐 光湖语言世界
|
||||
- DOMAIN-SUFFIX,whatsapp.com,🌐 光湖语言世界
|
||||
- DOMAIN-SUFFIX,wikipedia.org,🌐 光湖语言世界
|
||||
- DOMAIN-SUFFIX,reddit.com,🌐 光湖语言世界
|
||||
- DOMAIN-SUFFIX,discord.com,🌐 光湖语言世界
|
||||
|
||||
# Apple (直连)
|
||||
- DOMAIN-SUFFIX,apple.com,DIRECT
|
||||
- DOMAIN-SUFFIX,icloud.com,DIRECT
|
||||
|
||||
# CDN静态资源 (直连 · 节省池带宽)
|
||||
- DOMAIN-SUFFIX,cdn.jsdelivr.net,DIRECT
|
||||
- DOMAIN-SUFFIX,cdnjs.cloudflare.com,DIRECT
|
||||
- DOMAIN-SUFFIX,unpkg.com,DIRECT
|
||||
- DOMAIN-SUFFIX,bootcdn.net,DIRECT
|
||||
|
||||
# 国内直连
|
||||
- DOMAIN-SUFFIX,cn,DIRECT
|
||||
- DOMAIN-SUFFIX,taobao.com,DIRECT
|
||||
- DOMAIN-SUFFIX,tmall.com,DIRECT
|
||||
- DOMAIN-SUFFIX,alipay.com,DIRECT
|
||||
- DOMAIN-SUFFIX,aliyun.com,DIRECT
|
||||
- DOMAIN-SUFFIX,jd.com,DIRECT
|
||||
- DOMAIN-SUFFIX,qq.com,DIRECT
|
||||
- DOMAIN-SUFFIX,tencent.com,DIRECT
|
||||
- DOMAIN-SUFFIX,bilibili.com,DIRECT
|
||||
- DOMAIN-SUFFIX,baidu.com,DIRECT
|
||||
- DOMAIN-SUFFIX,zhihu.com,DIRECT
|
||||
- DOMAIN-SUFFIX,douyin.com,DIRECT
|
||||
- DOMAIN-SUFFIX,weibo.com,DIRECT
|
||||
- DOMAIN-SUFFIX,163.com,DIRECT
|
||||
- DOMAIN-SUFFIX,126.com,DIRECT
|
||||
- DOMAIN-SUFFIX,xiaomi.com,DIRECT
|
||||
- DOMAIN-SUFFIX,huawei.com,DIRECT
|
||||
- DOMAIN-SUFFIX,vivo.com,DIRECT
|
||||
- DOMAIN-SUFFIX,oppo.com,DIRECT
|
||||
- DOMAIN-SUFFIX,meituan.com,DIRECT
|
||||
- DOMAIN-SUFFIX,dianping.com,DIRECT
|
||||
- DOMAIN-SUFFIX,pinduoduo.com,DIRECT
|
||||
|
||||
# 局域网直连
|
||||
- IP-CIDR,192.168.0.0/16,DIRECT
|
||||
- IP-CIDR,10.0.0.0/8,DIRECT
|
||||
- IP-CIDR,172.16.0.0/12,DIRECT
|
||||
- IP-CIDR,127.0.0.0/8,DIRECT
|
||||
|
||||
# GeoIP中国直连
|
||||
- GEOIP,CN,DIRECT
|
||||
|
||||
# 默认走代理
|
||||
- MATCH,🌐 光湖语言世界
|
||||
`;
|
||||
}
|
||||
|
||||
// ── 检测客户端类型 ───────────────────────────
|
||||
function detectClientType(userAgent) {
|
||||
const ua = (userAgent || '').toLowerCase();
|
||||
if (ua.includes('clash') || ua.includes('mihomo') || ua.includes('stash')) return 'clash';
|
||||
if (ua.includes('shadowrocket') || ua.includes('quantumult') || ua.includes('surge')) return 'base64';
|
||||
return 'clash';
|
||||
}
|
||||
|
||||
// ── HTTP服务器 ───────────────────────────────
|
||||
const server = http.createServer((req, res) => {
|
||||
try {
|
||||
const parsedUrl = url.parse(req.url, true);
|
||||
const pathname = parsedUrl.pathname;
|
||||
|
||||
// 健康检查
|
||||
if (pathname === '/health') {
|
||||
const users = userManager.getEnabledUsers();
|
||||
const nodes = buildVpnNodes();
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
status: 'ok',
|
||||
service: 'zy-proxy-v3-subscription',
|
||||
brand: '光湖语言世界 — 冰朔开发维护',
|
||||
version: '3.0.0',
|
||||
users_count: users.length,
|
||||
nodes_count: nodes.length,
|
||||
smart_routing: nodes.length > 1 ? 'url-test' : 'single',
|
||||
server: 'ZY-SVR-005 · Brain',
|
||||
pool_hard_cutoff: true
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 流量池硬切检查 (2000GB楚河汉界) ──────
|
||||
// V3新增: 到量即停,不仅仅是告警
|
||||
function isPoolExhausted() {
|
||||
try {
|
||||
const cached = JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'pool-quota-status.json'), 'utf8'));
|
||||
const totalUsed = (cached.pool_upload_bytes || 0) + (cached.pool_download_bytes || 0);
|
||||
return totalUsed >= userManager.POOL_QUOTA_BYTES;
|
||||
} catch {
|
||||
const poolStatus = userManager.getPoolStatus();
|
||||
return poolStatus.pool_used_bytes >= userManager.POOL_QUOTA_BYTES;
|
||||
}
|
||||
}
|
||||
|
||||
// V2订阅端点: /sub/{token}
|
||||
// token是每个用户独立的,不会串线
|
||||
const subMatch = pathname.match(/^\/sub\/([a-f0-9]+)$/);
|
||||
if (subMatch) {
|
||||
const token = subMatch[1];
|
||||
const user = userManager.findUserByToken(token);
|
||||
|
||||
if (!user) {
|
||||
res.writeHead(403, { 'Content-Type': 'text/plain' });
|
||||
res.end('Forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
// V3硬切: 流量池耗尽 → 返回空配置+提示
|
||||
if (isPoolExhausted()) {
|
||||
const poolStatus = userManager.getPoolStatus();
|
||||
const exhaustedYaml = `# ⚠️ 光湖语言世界 · 本月流量池已用完
|
||||
# 流量池: ${poolStatus.pool_used_gb.toFixed(1)}GB / ${poolStatus.pool_total_gb}GB
|
||||
# 每月1号自动重置 · 届时刷新订阅即可恢复
|
||||
# 如有紧急需求请联系冰朔
|
||||
|
||||
mixed-port: 7890
|
||||
allow-lan: false
|
||||
mode: direct
|
||||
`;
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/yaml; charset=utf-8',
|
||||
'subscription-userinfo': generateUserInfoHeader(user),
|
||||
'profile-update-interval': '1',
|
||||
'profile-title': 'base64:' + Buffer.from(`⚠️ 光湖·${user.label}·流量已用完`).toString('base64'),
|
||||
});
|
||||
res.end(exhaustedYaml);
|
||||
return;
|
||||
}
|
||||
|
||||
const nodes = buildVpnNodes();
|
||||
if (nodes.length === 0) {
|
||||
res.writeHead(503, { 'Content-Type': 'text/plain' });
|
||||
res.end('No VPN nodes configured');
|
||||
return;
|
||||
}
|
||||
|
||||
const clientType = detectClientType(req.headers['user-agent']);
|
||||
const userInfoHeader = generateUserInfoHeader(user);
|
||||
|
||||
if (clientType === 'clash') {
|
||||
const yaml = generateClashYaml(user, nodes);
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/yaml; charset=utf-8',
|
||||
'Content-Disposition': `attachment; filename="guanghu-vpn-${user.label}.yaml"`,
|
||||
'subscription-userinfo': userInfoHeader,
|
||||
'profile-update-interval': '6',
|
||||
'profile-title': 'base64:' + Buffer.from(`光湖语言世界·${user.label}`).toString('base64'),
|
||||
});
|
||||
res.end(yaml);
|
||||
} else {
|
||||
const vlessUris = generateVlessUris(user, nodes);
|
||||
const encoded = Buffer.from(vlessUris).toString('base64');
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/plain; charset=utf-8',
|
||||
'subscription-userinfo': userInfoHeader,
|
||||
'profile-update-interval': '6',
|
||||
});
|
||||
res.end(encoded);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 用户配额查询: /quota/{token}
|
||||
const quotaMatch = pathname.match(/^\/quota\/([a-f0-9]+)$/);
|
||||
if (quotaMatch) {
|
||||
const token = quotaMatch[1];
|
||||
const user = userManager.findUserByToken(token);
|
||||
|
||||
if (!user) {
|
||||
res.writeHead(403, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: true, code: 'FORBIDDEN', message: '认证失败' }));
|
||||
return;
|
||||
}
|
||||
|
||||
const poolStatus = userManager.getPoolStatus();
|
||||
const userUsedGB = (user.traffic.upload_bytes + user.traffic.download_bytes) / (1024 ** 3);
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
email: user.email,
|
||||
label: user.label,
|
||||
pool: {
|
||||
total_gb: poolStatus.pool_total_gb,
|
||||
used_gb: parseFloat(poolStatus.pool_used_gb.toFixed(2)),
|
||||
remaining_gb: poolStatus.pool_remaining_gb,
|
||||
percentage_used: poolStatus.pool_percentage,
|
||||
users_count: poolStatus.users_count,
|
||||
period: poolStatus.period,
|
||||
reset_day: 1
|
||||
},
|
||||
personal: {
|
||||
used_gb: parseFloat(userUsedGB.toFixed(2)),
|
||||
period: user.traffic.period
|
||||
},
|
||||
updated_at: new Date().toISOString()
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// 用户状态: /status/{token}
|
||||
const statusMatch = pathname.match(/^\/status\/([a-f0-9]+)$/);
|
||||
if (statusMatch) {
|
||||
const token = statusMatch[1];
|
||||
const user = userManager.findUserByToken(token);
|
||||
|
||||
if (!user) {
|
||||
res.writeHead(403, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: true, code: 'FORBIDDEN', message: '认证失败' }));
|
||||
return;
|
||||
}
|
||||
|
||||
const poolStatus = userManager.getPoolStatus();
|
||||
const userUsedGB = (user.traffic.upload_bytes + user.traffic.download_bytes) / (1024 ** 3);
|
||||
const nodes = buildVpnNodes();
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
||||
res.end(JSON.stringify({
|
||||
user: {
|
||||
email: user.email,
|
||||
label: user.label,
|
||||
enabled: user.enabled
|
||||
},
|
||||
server: {
|
||||
status: 'online',
|
||||
brand: '光湖语言世界 — 冰朔开发维护',
|
||||
version: '3.0.0',
|
||||
uptime_seconds: Math.floor(process.uptime()),
|
||||
region: 'sg',
|
||||
region_name: 'Singapore',
|
||||
server_code: 'ZY-SVR-005'
|
||||
},
|
||||
nodes: nodes.map(n => ({
|
||||
id: n.id,
|
||||
name: n.name,
|
||||
region: n.region,
|
||||
port: n.port
|
||||
})),
|
||||
smart_routing: nodes.length > 1 ? 'url-test (自动选最快)' : 'single-node',
|
||||
pool: {
|
||||
total_gb: poolStatus.pool_total_gb,
|
||||
used_gb: parseFloat(poolStatus.pool_used_gb.toFixed(2)),
|
||||
remaining_gb: poolStatus.pool_remaining_gb,
|
||||
percentage_used: poolStatus.pool_percentage,
|
||||
users_count: poolStatus.users_count,
|
||||
period: poolStatus.period,
|
||||
reset_day: 1
|
||||
},
|
||||
personal: {
|
||||
used_gb: parseFloat(userUsedGB.toFixed(2)),
|
||||
period: user.traffic.period
|
||||
},
|
||||
updated_at: new Date().toISOString()
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// ── V3 流量仪表盘: /dashboard/{token} ──────
|
||||
// 手机浏览器可查看 · 流量/节点/系统状态
|
||||
const dashMatch = pathname.match(/^\/dashboard\/([a-f0-9]+)$/);
|
||||
if (dashMatch) {
|
||||
const token = dashMatch[1];
|
||||
const user = userManager.findUserByToken(token);
|
||||
|
||||
if (!user) {
|
||||
res.writeHead(403, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
res.end('<html><body><h1>403 认证失败</h1></body></html>');
|
||||
return;
|
||||
}
|
||||
|
||||
const poolStatus = userManager.getPoolStatus();
|
||||
const userUsedGB = ((user.traffic.upload_bytes + user.traffic.download_bytes) / (1024 ** 3)).toFixed(2);
|
||||
const nodes = buildVpnNodes();
|
||||
const poolUsedGB = (typeof poolStatus.pool_used_gb === 'number' ? poolStatus.pool_used_gb : 0).toFixed(1);
|
||||
const poolPct = Math.min(typeof poolStatus.pool_percentage === 'number' ? poolStatus.pool_percentage : 0, 100).toFixed(1);
|
||||
|
||||
// 读取反向加速状态
|
||||
let boostStatus = '未检测';
|
||||
try {
|
||||
const bs = JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'reverse-boost-status.json'), 'utf8'));
|
||||
boostStatus = bs.current?.bbr?.is_bbr ? '✅ BBR加速中' : '⚠️ 未加速';
|
||||
} catch { /* ignore */ }
|
||||
|
||||
// 读取今日流量快照
|
||||
let todayGB = '—';
|
||||
try {
|
||||
const snap = JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'daily-traffic-snapshot.json'), 'utf8'));
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
if (snap.date === today && snap.per_user?.[user.email]) {
|
||||
todayGB = snap.per_user[user.email].total_gb.toFixed(2);
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
|
||||
const poolBarColor = poolPct > 90 ? '#e74c3c' : poolPct > 70 ? '#f39c12' : '#2ecc71';
|
||||
|
||||
// HTML转义防止XSS
|
||||
const esc = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
|
||||
const html = `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>光湖语言世界 · ${esc(user.label)}</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, "PingFang SC", sans-serif; background: #0a0e27; color: #e0e0e0; padding: 20px; min-height: 100vh; }
|
||||
.header { text-align: center; padding: 20px 0; }
|
||||
.header h1 { font-size: 1.4em; background: linear-gradient(135deg, #667eea, #764ba2); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
|
||||
.header p { color: #888; font-size: 0.85em; margin-top: 5px; }
|
||||
.card { background: #141832; border-radius: 12px; padding: 16px; margin: 12px 0; border: 1px solid #1e2448; }
|
||||
.card h3 { font-size: 0.9em; color: #667eea; margin-bottom: 10px; }
|
||||
.stat-row { display: flex; justify-content: space-between; padding: 6px 0; font-size: 0.9em; }
|
||||
.stat-label { color: #888; }
|
||||
.stat-value { color: #fff; font-weight: 600; }
|
||||
.pool-bar { background: #1e2448; border-radius: 8px; height: 24px; margin: 8px 0; overflow: hidden; }
|
||||
.pool-fill { height: 100%; border-radius: 8px; transition: width 0.3s; display: flex; align-items: center; justify-content: center; font-size: 0.75em; font-weight: 600; }
|
||||
.node { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #1e2448; font-size: 0.85em; }
|
||||
.node:last-child { border-bottom: none; }
|
||||
.online { color: #2ecc71; }
|
||||
.footer { text-align: center; padding: 20px 0; color: #555; font-size: 0.75em; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>🌐 光湖语言世界</h1>
|
||||
<p>${esc(user.label)} 的专属仪表盘 — 冰朔开发维护</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>📊 流量概览</h3>
|
||||
<div class="stat-row"><span class="stat-label">今日用量</span><span class="stat-value">${todayGB} GB</span></div>
|
||||
<div class="stat-row"><span class="stat-label">个人本月</span><span class="stat-value">${userUsedGB} GB</span></div>
|
||||
<div class="stat-row"><span class="stat-label">流量池</span><span class="stat-value">${poolUsedGB} / ${poolStatus.pool_total_gb} GB</span></div>
|
||||
<div class="pool-bar">
|
||||
<div class="pool-fill" style="width: ${poolPct}%; background: ${poolBarColor};">${poolPct}%</div>
|
||||
</div>
|
||||
<div class="stat-row"><span class="stat-label">重置日期</span><span class="stat-value">每月1号</span></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>🔌 节点状态 (${nodes.length}个)</h3>
|
||||
${nodes.map(n => `<div class="node"><span>${esc(n.name)}</span><span class="online">${n.latency_ms ? n.latency_ms + 'ms' : '在线'}</span></div>`).join('\n ')}
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>⚡ 系统状态</h3>
|
||||
<div class="stat-row"><span class="stat-label">服务版本</span><span class="stat-value">V3.0</span></div>
|
||||
<div class="stat-row"><span class="stat-label">反向加速</span><span class="stat-value">${boostStatus}</span></div>
|
||||
<div class="stat-row"><span class="stat-label">在线用户</span><span class="stat-value">${poolStatus.users_count}</span></div>
|
||||
<div class="stat-row"><span class="stat-label">智能选路</span><span class="stat-value">${nodes.length > 1 ? '✅ url-test' : '单节点'}</span></div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
光湖语言世界 · 冰朔开发维护<br>
|
||||
更新于 ${new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })}
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
res.end(html);
|
||||
return;
|
||||
}
|
||||
|
||||
// 404
|
||||
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
||||
res.end('Not Found');
|
||||
} catch (err) {
|
||||
console.error('❌ 请求处理错误 [%s %s]:', 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
server.on('error', (err) => {
|
||||
console.error('❌ 服务器错误:', err.message);
|
||||
if (err.code === 'EADDRINUSE') {
|
||||
console.error(' 端口 %d 已被占用', PORT);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
server.on('clientError', (err, socket) => {
|
||||
console.error('⚠️ 客户端连接错误:', err.code || err.message);
|
||||
if (socket.writable) {
|
||||
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(PORT, '127.0.0.1', () => {
|
||||
const users = userManager.getEnabledUsers();
|
||||
const poolGB = userManager.POOL_QUOTA_BYTES / (1024 ** 3);
|
||||
console.log(`🌐 光湖语言世界 · V3订阅服务已启动: http://127.0.0.1:${PORT}`);
|
||||
console.log(` 品牌: 光湖语言世界 — 冰朔开发维护`);
|
||||
console.log(` 版本: V3.0 (多用户独立专线·共享流量池·硬切·AI推理)`);
|
||||
console.log(` 用户数: ${users.length}`);
|
||||
console.log(` 流量池: ${poolGB}GB/月 (全用户共享·每月1号重置·到量即停)`);
|
||||
console.log(` 服务器: ZY-SVR-005 · Brain`);
|
||||
console.log(` ──────────────────────────`);
|
||||
console.log(` 订阅端点: /sub/{user_token}`);
|
||||
console.log(` 配额查询: /quota/{user_token}`);
|
||||
console.log(` 服务状态: /status/{user_token}`);
|
||||
console.log(` 流量仪表盘: /dashboard/{user_token}`);
|
||||
console.log(` 健康检查: /health`);
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
function gracefulShutdown(signal) {
|
||||
console.log(`\n${signal} received. Shutting down gracefully...`);
|
||||
const forceExit = setTimeout(() => { process.exit(1); }, 5000);
|
||||
server.close(() => {
|
||||
clearTimeout(forceExit);
|
||||
console.log('Server closed.');
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
||||
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
||||
|
||||
process.on('uncaughtException', (err) => {
|
||||
console.error('❌ 未捕获的异常:', err.message);
|
||||
console.error(err.stack);
|
||||
gracefulShutdown('uncaughtException');
|
||||
});
|
||||
process.on('unhandledRejection', (reason) => {
|
||||
console.error('❌ 未处理的Promise拒绝:', reason);
|
||||
});
|
||||
|
|
@ -177,9 +177,42 @@ function monitor() {
|
|||
|
||||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||
fs.writeFileSync(summaryFile, JSON.stringify(summary, null, 2));
|
||||
|
||||
// V3: 每日流量快照 (供仪表盘使用)
|
||||
saveDailySnapshot(perUserTraffic);
|
||||
|
||||
console.log('[V2流量监控] 检查完成');
|
||||
}
|
||||
|
||||
// ── V3: 每日流量快照 ─────────────────────────
|
||||
// 记录每天各用户的流量,供/dashboard/{token}使用
|
||||
function saveDailySnapshot(perUserTraffic) {
|
||||
const snapshotFile = path.join(DATA_DIR, 'daily-traffic-snapshot.json');
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
|
||||
let snapshot;
|
||||
try {
|
||||
snapshot = JSON.parse(fs.readFileSync(snapshotFile, 'utf8'));
|
||||
// 如果日期变了,重置快照
|
||||
if (snapshot.date !== today) {
|
||||
snapshot = { date: today, per_user: {}, updated_at: new Date().toISOString() };
|
||||
}
|
||||
} catch {
|
||||
snapshot = { date: today, per_user: {}, updated_at: new Date().toISOString() };
|
||||
}
|
||||
|
||||
for (const [email, traffic] of Object.entries(perUserTraffic)) {
|
||||
snapshot.per_user[email] = {
|
||||
upload_gb: parseFloat((traffic.upload / (1024 ** 3)).toFixed(2)),
|
||||
download_gb: parseFloat((traffic.download / (1024 ** 3)).toFixed(2)),
|
||||
total_gb: parseFloat(((traffic.upload + traffic.download) / (1024 ** 3)).toFixed(2))
|
||||
};
|
||||
}
|
||||
|
||||
snapshot.updated_at = new Date().toISOString();
|
||||
fs.writeFileSync(snapshotFile, JSON.stringify(snapshot, null, 2));
|
||||
}
|
||||
|
||||
// ── 启动监控循环 ─────────────────────────────
|
||||
console.log('📊 铸渊专线V2流量监控启动 · 共享流量池模型');
|
||||
console.log(` 检查间隔: ${CHECK_INTERVAL / 1000}秒`);
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@
|
|||
// - 服务器越多 → 节点越多 → 系统越强 → 不输商业VPN
|
||||
// - 所有服务器VPN能力汇聚到一个活的人格模块上
|
||||
// - 动态感知空闲、动态选路、自我修复、自我学习
|
||||
// - 活模块5接口: heartbeat / selfDiagnose / selfHeal /
|
||||
// alertZhuyuan / learnFromRun
|
||||
// - 活模块6接口: heartbeat / selfDiagnose / selfHeal /
|
||||
// alertZhuyuan / learnFromRun / consultAI
|
||||
//
|
||||
// 运行在大脑服务器 (ZY-SVR-005)
|
||||
// 管理所有VPN节点的生命周期
|
||||
|
|
@ -124,6 +124,7 @@ class LivingModule {
|
|||
async selfHeal(problem) { throw new Error('selfHeal() 未实现'); }
|
||||
async alertZhuyuan(alert) { throw new Error('alertZhuyuan() 未实现'); }
|
||||
async learnFromRun() { throw new Error('learnFromRun() 未实现'); }
|
||||
async consultAI(context) { throw new Error('consultAI() 未实现'); }
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
|
|
@ -582,13 +583,37 @@ class ZyCloudVpn extends LivingModule {
|
|||
console.log(` 🚨 所有节点不可用!尝试修复本机节点...`);
|
||||
const localNode = this._nodes.find(n => n.type === 'local');
|
||||
if (localNode) {
|
||||
return this.selfHeal({ type: 'node_dead', node: localNode });
|
||||
const healed = await this.selfHeal({ type: 'node_dead', node: localNode });
|
||||
if (healed) return true;
|
||||
}
|
||||
// 修复失败 → 升级到alertZhuyuan
|
||||
|
||||
// 自动修复失败 → 第2次重试: 调用AI推理
|
||||
console.log(' 🤖 自动修复失败,启动AI推理...');
|
||||
const aiResult = await this.consultAI({
|
||||
type: 'all_nodes_dead',
|
||||
nodes: this._nodes.map(n => ({
|
||||
id: n.id, status: n.status, host: n.host, port: n.port,
|
||||
consecutive_failures: n.consecutive_failures
|
||||
})),
|
||||
attempted_fix: 'xray_restart_failed'
|
||||
});
|
||||
|
||||
if (aiResult) {
|
||||
// AI给出了建议,记录并尝试第3次修复
|
||||
console.log(` 🤖 AI建议已记录,再次尝试修复...`);
|
||||
if (localNode) {
|
||||
const retryHeal = await this.selfHeal({ type: 'node_dead', node: localNode });
|
||||
if (retryHeal) return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 第3次: 通知冰朔
|
||||
await this.alertZhuyuan({
|
||||
level: 'critical',
|
||||
message: '所有VPN节点不可用!需要人工干预',
|
||||
nodes: this._nodes.map(n => ({ id: n.id, status: n.status }))
|
||||
message: '所有VPN节点不可用!自动修复+AI推理均失败,需要人工干预',
|
||||
nodes: this._nodes.map(n => ({ id: n.id, status: n.status })),
|
||||
ai_advice: aiResult ? aiResult.content.slice(0, 500) : '(AI推理未返回结果)',
|
||||
attempted_fixes: ['xray_restart', 'ai_diagnosis']
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
|
@ -703,6 +728,69 @@ class ZyCloudVpn extends LivingModule {
|
|||
}
|
||||
}
|
||||
|
||||
// ═══ 接口6: consultAI() — "我请教AI" (V3新增) ═══
|
||||
// 遇到复杂问题时调用LLM路由器进行深度推理
|
||||
// 三次重试机制:
|
||||
// 第1次: selfHeal 自动修复
|
||||
// 第2次: consultAI 调用LLM
|
||||
// 第3次: alertZhuyuan 邮件通知冰朔
|
||||
async consultAI(context) {
|
||||
let llmRouter;
|
||||
try {
|
||||
llmRouter = require('./llm-router');
|
||||
} catch {
|
||||
console.log('[ZY-CLOUD VPN] ⚠️ LLM路由器未加载,跳过AI推理');
|
||||
return null;
|
||||
}
|
||||
|
||||
const prompt = `你是光湖语言世界VPN系统的AI诊断助手。
|
||||
当前系统状态:
|
||||
- 模块: ${this._moduleName}
|
||||
- 状态: ${this._state}
|
||||
- 总节点: ${this._nodes.length}
|
||||
- 存活节点: ${this._liveNodes.length}
|
||||
- 连续错误: ${this._consecutiveErrors}
|
||||
|
||||
问题上下文:
|
||||
${JSON.stringify(context, null, 2).slice(0, 2000)}
|
||||
|
||||
请分析问题原因并给出具体的修复建议。要求:
|
||||
1. 判断是网络层面还是服务层面的问题
|
||||
2. 给出可执行的修复步骤
|
||||
3. 评估问题严重程度(low/medium/high/critical)
|
||||
回答要简洁明确。`;
|
||||
|
||||
console.log('[ZY-CLOUD VPN] 🤖 调用AI推理...');
|
||||
const result = await llmRouter.callLLM(prompt, {
|
||||
systemPrompt: '你是光湖语言世界VPN系统的AI诊断引擎。你负责分析网络和代理服务的异常,给出精准的修复建议。',
|
||||
maxTokens: 800,
|
||||
timeout: 45000
|
||||
});
|
||||
|
||||
if (result) {
|
||||
console.log(`[ZY-CLOUD VPN] 🤖 AI推理完成 (模型: ${result.model})`);
|
||||
console.log(` 建议: ${result.content.slice(0, 200)}...`);
|
||||
|
||||
// 记录AI咨询历史
|
||||
this._history.push({
|
||||
type: 'consultAI',
|
||||
model: result.model,
|
||||
context_summary: context.type || 'unknown',
|
||||
response_preview: result.content.slice(0, 100),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// 只保留最近20条历史
|
||||
if (this._history.length > 20) {
|
||||
this._history = this._history.slice(-20);
|
||||
}
|
||||
} else {
|
||||
console.log('[ZY-CLOUD VPN] ⚠️ AI推理未返回结果');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ── 应用学习数据优化节点排序 ────────────────
|
||||
_applyLearnedOptimization() {
|
||||
const now = new Date();
|
||||
|
|
@ -917,6 +1005,8 @@ const mgmtServer = http.createServer((req, res) => {
|
|||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
module: vpnModule._moduleId,
|
||||
brand: '光湖语言世界 — 冰朔开发维护',
|
||||
version: '3.0.0',
|
||||
state: vpnModule._state,
|
||||
started_at: vpnModule._startedAt,
|
||||
uptime_seconds: Math.floor((Date.now() - new Date(vpnModule._startedAt).getTime()) / 1000),
|
||||
|
|
|
|||
Loading…
Reference in New Issue