Merge pull request #296 from qinfendebingshuo/copilot/auto-update-vpn-system
∞ version: self-evolving VPN with protocol mirroring, auto-update orchestration, and email hub
This commit is contained in:
commit
2354bc35c8
|
|
@ -31,6 +31,7 @@ on:
|
|||
- send-subscription-v3
|
||||
- deploy-v3
|
||||
- switch-v3
|
||||
- deploy-infinity
|
||||
default: 'status'
|
||||
email:
|
||||
description: '用户邮箱 (add-user/remove-user/send-subscription/send-subscription-v3时需要)'
|
||||
|
|
@ -60,7 +61,8 @@ jobs:
|
|||
github.event.inputs.action == 'status' ||
|
||||
github.event.inputs.action == 'restart' ||
|
||||
github.event.inputs.action == 'deploy-v3' ||
|
||||
github.event.inputs.action == 'switch-v3'
|
||||
github.event.inputs.action == 'switch-v3' ||
|
||||
github.event.inputs.action == 'deploy-infinity'
|
||||
|
||||
steps:
|
||||
- name: '📥 检出代码'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,169 @@
|
|||
# ═══════════════════════════════════════════════
|
||||
# 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001
|
||||
# 📜 Copyright: 国作登字-2026-A-00037559
|
||||
# ═══════════════════════════════════════════════
|
||||
# ♾️ 光湖语言世界 · 自主进化定时工作流
|
||||
#
|
||||
# 定时触发∞版本的关键任务:
|
||||
# - 每月1号: 月度进化 (LLM分析 + 流量重置邮件)
|
||||
# - 每周五: 用户反馈处理 (Phase 2)
|
||||
# - 手动触发: 任意∞版本操作
|
||||
#
|
||||
# 注: 大部分定时任务由服务器上的PM2进程 (auto-evolution.js)
|
||||
# 自动执行。此工作流作为补充/备份触发机制。
|
||||
# ═══════════════════════════════════════════════
|
||||
|
||||
name: '♾️ 光湖∞ · 自主进化'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# 每月1号 UTC 16:00 = CST 00:00 (月度进化)
|
||||
- cron: '0 16 1 * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
action:
|
||||
description: '操作类型'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- monthly-evolution
|
||||
- deploy-infinity
|
||||
- status
|
||||
- send-monthly-reset
|
||||
- send-update-notify
|
||||
- send-traffic-warn
|
||||
default: 'status'
|
||||
message:
|
||||
description: '附加消息 (update-notify描述 / traffic-warn百分比)'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: infinity-evolution
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
# ═══ §1 ∞版本部署/管理 ═══
|
||||
infinity-action:
|
||||
name: '♾️ ${{ github.event.inputs.action || ''monthly-evolution'' }}'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- name: '📥 检出代码'
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: '🔑 配置SSH'
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.ZY_BRAIN_KEY }}" > ~/.ssh/brain_key
|
||||
chmod 600 ~/.ssh/brain_key
|
||||
ssh-keyscan -H ${{ secrets.ZY_BRAIN_HOST }} >> ~/.ssh/known_hosts 2>/dev/null || true
|
||||
|
||||
- name: '♾️ 执行∞操作'
|
||||
env:
|
||||
ACTION: ${{ github.event.inputs.action || 'monthly-evolution' }}
|
||||
MESSAGE: ${{ github.event.inputs.message || '' }}
|
||||
ZY_SMTP_USER: ${{ secrets.ZY_SMTP_USER }}
|
||||
ZY_SMTP_PASS: ${{ secrets.ZY_SMTP_PASS }}
|
||||
run: |
|
||||
SSH_CMD="ssh -i ~/.ssh/brain_key -o StrictHostKeyChecking=no ${{ secrets.ZY_BRAIN_USER }}@${{ secrets.ZY_BRAIN_HOST }}"
|
||||
PROXY_DIR="/opt/zhuyuan-brain/proxy"
|
||||
|
||||
echo "════════════════════════════════════════"
|
||||
echo "♾️ 光湖∞ · 自主进化 · action=$ACTION"
|
||||
echo "════════════════════════════════════════"
|
||||
|
||||
case "$ACTION" in
|
||||
deploy-infinity)
|
||||
echo "部署∞版本..."
|
||||
# 先同步最新代码到服务器
|
||||
$SSH_CMD "mkdir -p $PROXY_DIR/.deploy-tmp"
|
||||
|
||||
scp -i ~/.ssh/brain_key -o StrictHostKeyChecking=no \
|
||||
server/proxy/service/auto-evolution.js \
|
||||
server/proxy/service/email-hub.js \
|
||||
server/proxy/service/protocol-mirror.js \
|
||||
server/proxy/ecosystem.brain-proxy-v3.config.js \
|
||||
${{ secrets.ZY_BRAIN_USER }}@${{ secrets.ZY_BRAIN_HOST }}:$PROXY_DIR/.deploy-tmp/
|
||||
|
||||
$SSH_CMD "
|
||||
cp $PROXY_DIR/.deploy-tmp/auto-evolution.js $PROXY_DIR/service/
|
||||
cp $PROXY_DIR/.deploy-tmp/email-hub.js $PROXY_DIR/service/
|
||||
cp $PROXY_DIR/.deploy-tmp/protocol-mirror.js $PROXY_DIR/service/
|
||||
cp $PROXY_DIR/.deploy-tmp/ecosystem.brain-proxy-v3.config.js $PROXY_DIR/
|
||||
rm -rf $PROXY_DIR/.deploy-tmp
|
||||
mkdir -p $PROXY_DIR/data/evolution-reports
|
||||
cd $PROXY_DIR && pm2 startOrRestart ecosystem.brain-proxy-v3.config.js --update-env && pm2 save
|
||||
echo '✅ ∞版本已部署'
|
||||
pm2 list
|
||||
"
|
||||
;;
|
||||
|
||||
monthly-evolution)
|
||||
echo "触发月度进化..."
|
||||
$SSH_CMD "
|
||||
cd $PROXY_DIR
|
||||
export ZY_SMTP_USER='$ZY_SMTP_USER'
|
||||
export ZY_SMTP_PASS='$ZY_SMTP_PASS'
|
||||
node service/email-hub.js monthly-reset 2>&1 || echo '⚠️ 月度邮件发送遇到问题'
|
||||
echo '✅ 月度进化已触发'
|
||||
"
|
||||
;;
|
||||
|
||||
status)
|
||||
echo "查询∞版本状态..."
|
||||
$SSH_CMD "
|
||||
cd $PROXY_DIR
|
||||
echo '── PM2进程 ──'
|
||||
pm2 list 2>/dev/null || echo '⚠️ PM2不可用'
|
||||
echo ''
|
||||
echo '── 进化状态 ──'
|
||||
cat data/auto-evolution-status.json 2>/dev/null || echo '(未初始化)'
|
||||
echo ''
|
||||
echo '── 镜像状态 ──'
|
||||
cat data/protocol-mirror-status.json 2>/dev/null || echo '(未初始化)'
|
||||
echo ''
|
||||
echo '── 流量池 ──'
|
||||
cat data/pool-quota-status.json 2>/dev/null || echo '(未初始化)'
|
||||
"
|
||||
;;
|
||||
|
||||
send-monthly-reset)
|
||||
echo "发送月初重置邮件..."
|
||||
$SSH_CMD "
|
||||
cd $PROXY_DIR
|
||||
export ZY_SMTP_USER='$ZY_SMTP_USER'
|
||||
export ZY_SMTP_PASS='$ZY_SMTP_PASS'
|
||||
node service/email-hub.js monthly-reset
|
||||
"
|
||||
;;
|
||||
|
||||
send-update-notify)
|
||||
echo "发送更新通知..."
|
||||
$SSH_CMD "
|
||||
cd $PROXY_DIR
|
||||
export ZY_SMTP_USER='$ZY_SMTP_USER'
|
||||
export ZY_SMTP_PASS='$ZY_SMTP_PASS'
|
||||
node service/email-hub.js update-notify '${MESSAGE:-系统已更新}'
|
||||
"
|
||||
;;
|
||||
|
||||
send-traffic-warn)
|
||||
echo "发送流量预警..."
|
||||
$SSH_CMD "
|
||||
cd $PROXY_DIR
|
||||
export ZY_SMTP_USER='$ZY_SMTP_USER'
|
||||
export ZY_SMTP_PASS='$ZY_SMTP_PASS'
|
||||
node service/email-hub.js traffic-warn '${MESSAGE:-70}'
|
||||
"
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "❌ 未知操作: $ACTION"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
|
@ -645,6 +645,7 @@ deploy_v3() {
|
|||
echo "V3运行在 /api/proxy-v3/ (端口3805)"
|
||||
echo ""
|
||||
echo "切换V2用户到V3: bash deploy-brain-proxy.sh switch-v3"
|
||||
echo "升级到∞版本: bash deploy-brain-proxy.sh deploy-infinity"
|
||||
echo "════════════════════════════════════════"
|
||||
|
||||
# V3健康检查
|
||||
|
|
@ -753,6 +754,78 @@ switch_v3() {
|
|||
echo "════════════════════════════════════════"
|
||||
}
|
||||
|
||||
# ── deploy-infinity: 部署∞版本 ─────────────────
|
||||
# 在V3基础上启动自主进化引擎和协议镜像引擎
|
||||
deploy_infinity() {
|
||||
echo "部署 ∞ 版本 (在V3基础上添加自主进化能力)..."
|
||||
deploy_services
|
||||
|
||||
# 确保V3进程已在运行
|
||||
if ! pm2 list 2>/dev/null | grep -q "zy-proxy-v3-sub"; then
|
||||
echo " ⚠️ V3进程未运行,先部署V3..."
|
||||
deploy_v3
|
||||
fi
|
||||
|
||||
# 加载密钥作为环境变量
|
||||
if [ -f "$PROXY_DIR/.env.keys" ]; then
|
||||
set -a
|
||||
# shellcheck source=/dev/null
|
||||
source "$PROXY_DIR/.env.keys"
|
||||
set +a
|
||||
fi
|
||||
|
||||
# 复制 ∞ 版本新增文件
|
||||
echo " 复制∞版本模块..."
|
||||
for f in auto-evolution.js email-hub.js protocol-mirror.js; do
|
||||
if [ -f "$REPO_PROXY_DIR/service/$f" ]; then
|
||||
cp "$REPO_PROXY_DIR/service/$f" "$PROXY_DIR/service/$f"
|
||||
echo " ✅ $f"
|
||||
else
|
||||
echo " ⚠️ $f 不存在于仓库"
|
||||
fi
|
||||
done
|
||||
|
||||
# 复制更新后的PM2配置
|
||||
cp "$REPO_PROXY_DIR/ecosystem.brain-proxy-v3.config.js" "$PROXY_DIR/ecosystem.brain-proxy-v3.config.js"
|
||||
|
||||
# 创建进化报告目录
|
||||
mkdir -p "$PROXY_DIR/data/evolution-reports"
|
||||
|
||||
# 重启所有V3+∞进程 (PM2会根据新配置启动新增进程)
|
||||
cd "$PROXY_DIR" || { echo "❌ 无法进入 $PROXY_DIR"; return 1; }
|
||||
pm2 startOrRestart ecosystem.brain-proxy-v3.config.js --update-env
|
||||
pm2 save
|
||||
|
||||
echo ""
|
||||
echo "════════════════════════════════════════"
|
||||
echo "♾️ ∞ 版本部署完成!"
|
||||
echo ""
|
||||
echo "新增进程:"
|
||||
echo " zy-auto-evolution — 自主进化引擎 (调度中枢)"
|
||||
echo " zy-protocol-mirror — 协议镜像引擎 (Xray自动更新)"
|
||||
echo ""
|
||||
echo "定时任务:"
|
||||
echo " 每30分钟 — 协议版本检查"
|
||||
echo " 每月1号 — 月度进化 + 流量重置邮件"
|
||||
echo " 流量70% — 全用户预警邮件"
|
||||
echo ""
|
||||
echo "PM2进程列表:"
|
||||
pm2 list 2>/dev/null || true
|
||||
echo "════════════════════════════════════════"
|
||||
|
||||
# 健康检查
|
||||
sleep 2
|
||||
echo ""
|
||||
echo "∞ 版本健康检查:"
|
||||
for proc in zy-proxy-v3-sub zy-proxy-guardian zy-reverse-boost zy-auto-evolution zy-protocol-mirror; do
|
||||
if pm2 show "$proc" 2>/dev/null | grep -q "online"; then
|
||||
echo " ✅ $proc: 在线"
|
||||
else
|
||||
echo " ⚠️ $proc: 未就绪 (等待启动)"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# ── 执行 ──────────────────────────────────────
|
||||
case "$ACTION" in
|
||||
install) install ;;
|
||||
|
|
@ -764,8 +837,9 @@ case "$ACTION" in
|
|||
list-users) list_users ;;
|
||||
deploy-v3) deploy_v3 ;;
|
||||
switch-v3) switch_v3 ;;
|
||||
deploy-infinity) deploy_infinity ;;
|
||||
*)
|
||||
echo "用法: bash deploy-brain-proxy.sh {install|update|status|restart|add-user|remove-user|list-users|deploy-v3|switch-v3}"
|
||||
echo "用法: bash deploy-brain-proxy.sh {install|update|status|restart|add-user|remove-user|list-users|deploy-v3|switch-v3|deploy-infinity}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
|
|
|||
|
|
@ -1,20 +1,19 @@
|
|||
// ═══════════════════════════════════════════════
|
||||
// 光湖语言世界 V3 · PM2 大脑服务器代理配置
|
||||
// 光湖语言世界 ∞ · PM2 大脑服务器代理配置
|
||||
// 部署在 ZY-SVR-005 (43.156.237.110) · 大脑服务器
|
||||
//
|
||||
// V3独立于V2运行,测试通过后切换Nginx即可
|
||||
// V2进程 (ecosystem.brain-proxy.config.js) 继续运行
|
||||
// ∞ 版本在V3基础上新增:
|
||||
// - zy-auto-evolution: 自主进化引擎 (调度所有定时任务)
|
||||
// - zy-protocol-mirror: 协议镜像引擎 (Xray自动更新)
|
||||
//
|
||||
// 切换方式:
|
||||
// 测试中: /api/proxy-v3/ → 3805
|
||||
// 切换后: /api/proxy-v2/ → 3805 (Nginx改一行)
|
||||
// V2进程 (ecosystem.brain-proxy.config.js) 继续运行
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: 'zy-proxy-v3-sub',
|
||||
version: '3.0.0',
|
||||
version: '∞',
|
||||
script: '/opt/zhuyuan-brain/proxy/service/subscription-server-v3.js',
|
||||
instances: 1,
|
||||
exec_mode: 'fork',
|
||||
|
|
@ -30,7 +29,7 @@ module.exports = {
|
|||
},
|
||||
{
|
||||
name: 'zy-proxy-guardian',
|
||||
version: '3.0.0',
|
||||
version: '∞',
|
||||
script: '/opt/zhuyuan-brain/proxy/service/proxy-guardian.js',
|
||||
instances: 1,
|
||||
exec_mode: 'fork',
|
||||
|
|
@ -47,7 +46,7 @@ module.exports = {
|
|||
},
|
||||
{
|
||||
name: 'zy-reverse-boost',
|
||||
version: '3.0.0',
|
||||
version: '∞',
|
||||
script: '/opt/zhuyuan-brain/proxy/service/reverse-boost-agent.js',
|
||||
instances: 1,
|
||||
exec_mode: 'fork',
|
||||
|
|
@ -59,6 +58,40 @@ module.exports = {
|
|||
log_file: '/opt/zhuyuan-brain/proxy/logs/reverse-boost.log',
|
||||
error_file: '/opt/zhuyuan-brain/proxy/logs/reverse-boost-error.log',
|
||||
time: true
|
||||
},
|
||||
{
|
||||
name: 'zy-auto-evolution',
|
||||
version: '∞',
|
||||
script: '/opt/zhuyuan-brain/proxy/service/auto-evolution.js',
|
||||
instances: 1,
|
||||
exec_mode: 'fork',
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
ZY_BRAIN_PROXY_DIR: '/opt/zhuyuan-brain/proxy',
|
||||
ZY_PROXY_DATA_DIR: '/opt/zhuyuan-brain/proxy/data',
|
||||
ZY_PROXY_LOG_DIR: '/opt/zhuyuan-brain/proxy/logs'
|
||||
},
|
||||
max_memory_restart: '64M',
|
||||
log_file: '/opt/zhuyuan-brain/proxy/logs/auto-evolution.log',
|
||||
error_file: '/opt/zhuyuan-brain/proxy/logs/auto-evolution-error.log',
|
||||
time: true
|
||||
},
|
||||
{
|
||||
name: 'zy-protocol-mirror',
|
||||
version: '∞',
|
||||
script: '/opt/zhuyuan-brain/proxy/service/protocol-mirror.js',
|
||||
instances: 1,
|
||||
exec_mode: 'fork',
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
ZY_BRAIN_PROXY_DIR: '/opt/zhuyuan-brain/proxy',
|
||||
ZY_PROXY_DATA_DIR: '/opt/zhuyuan-brain/proxy/data',
|
||||
ZY_PROXY_LOG_DIR: '/opt/zhuyuan-brain/proxy/logs'
|
||||
},
|
||||
max_memory_restart: '64M',
|
||||
log_file: '/opt/zhuyuan-brain/proxy/logs/protocol-mirror.log',
|
||||
error_file: '/opt/zhuyuan-brain/proxy/logs/protocol-mirror-error.log',
|
||||
time: true
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,508 @@
|
|||
#!/usr/bin/env node
|
||||
// ═══════════════════════════════════════════════
|
||||
// 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001
|
||||
// 📜 Copyright: 国作登字-2026-A-00037559
|
||||
// ═══════════════════════════════════════════════
|
||||
// server/proxy/service/auto-evolution.js
|
||||
// ♾️ 光湖语言世界 · 自主进化引擎
|
||||
//
|
||||
// ∞版本核心调度中枢 — 管理所有定时任务和自我进化生命周期
|
||||
//
|
||||
// 调度表 (Asia/Shanghai时区):
|
||||
// - 每30分钟: 协议版本检查 (protocol-mirror.js)
|
||||
// - 每月1号 00:00: 月度进化周期 (LLM分析 + 流量重置邮件)
|
||||
// - 每周五 20:00: 处理用户反馈 (Phase 2)
|
||||
// - 每周一 09:00: 推送反馈结果 (Phase 2)
|
||||
// - 流量池70%: 触发全用户预警邮件
|
||||
//
|
||||
// 更新编排流水线:
|
||||
// 备份 → 应用 → 验证 → 通知用户 → 失败3次升级告警
|
||||
//
|
||||
// 运行方式: PM2 managed (zy-auto-evolution)
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
'use strict';
|
||||
|
||||
const { execFileSync } = 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 EVOLUTION_FILE = path.join(DATA_DIR, 'auto-evolution-status.json');
|
||||
const REPORTS_DIR = path.join(DATA_DIR, 'evolution-reports');
|
||||
const POOL_STATUS_FILE = path.join(DATA_DIR, 'pool-quota-status.json');
|
||||
const GUARDIAN_FILE = path.join(DATA_DIR, 'guardian-status.json');
|
||||
const MIRROR_FILE = path.join(DATA_DIR, 'protocol-mirror-status.json');
|
||||
const USERS_FILE = path.join(DATA_DIR, 'users.json');
|
||||
|
||||
const SCHEDULE_CHECK_INTERVAL = 5 * 60 * 1000; // 每5分钟检查调度
|
||||
const PROTOCOL_CHECK_INTERVAL = 30 * 60 * 1000; // 每30分钟协议检查
|
||||
const MAX_UPDATE_RETRIES = 3;
|
||||
|
||||
// ── 获取中国时间 ─────────────────────────────
|
||||
function getChinaTime() {
|
||||
// 返回中国标准时间
|
||||
const now = new Date();
|
||||
const utc = now.getTime() + now.getTimezoneOffset() * 60000;
|
||||
return new Date(utc + 8 * 3600000); // UTC+8
|
||||
}
|
||||
|
||||
function getChinaTimeStr() {
|
||||
return getChinaTime().toISOString().replace('T', ' ').slice(0, 19) + ' CST';
|
||||
}
|
||||
|
||||
// ── 读取进化状态 ─────────────────────────────
|
||||
function readEvolutionStatus() {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(EVOLUTION_FILE, 'utf8'));
|
||||
} catch {
|
||||
return {
|
||||
version: '∞',
|
||||
started_at: new Date().toISOString(),
|
||||
last_schedule_check: null,
|
||||
schedules: {
|
||||
protocol_check: { interval_min: 30, last_run: null },
|
||||
monthly_evolution: { schedule: '每月1号 00:00', last_run: null },
|
||||
weekly_feedback: { schedule: '每周五 20:00', last_run: null },
|
||||
weekly_response: { schedule: '每周一 09:00', last_run: null },
|
||||
traffic_alert_70: { last_run: null }
|
||||
},
|
||||
evolution_count: 0,
|
||||
updates_applied: 0,
|
||||
updates_failed: 0,
|
||||
last_monthly_report: null,
|
||||
status: 'running'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ── 保存进化状态 ─────────────────────────────
|
||||
function saveEvolutionStatus(status) {
|
||||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||
fs.writeFileSync(EVOLUTION_FILE, JSON.stringify(status, null, 2));
|
||||
}
|
||||
|
||||
// ── 安全读取JSON ─────────────────────────────
|
||||
function safeReadJSON(filePath) {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 安全加载模块 ─────────────────────────────
|
||||
function safeRequire(modulePath) {
|
||||
try {
|
||||
return require(modulePath);
|
||||
} catch (err) {
|
||||
console.error(`[自主进化] 模块加载失败: ${modulePath} — ${err.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// §1 协议版本检查 (每30分钟)
|
||||
// ═══════════════════════════════════════════════
|
||||
async function runProtocolCheck() {
|
||||
console.log('[自主进化] ── 协议版本检查 ──');
|
||||
|
||||
const protocolMirror = safeRequire('./protocol-mirror');
|
||||
if (!protocolMirror) {
|
||||
console.log('[自主进化] 协议镜像模块不可用,跳过');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await protocolMirror.checkForUpdates();
|
||||
if (result && result.update_available) {
|
||||
console.log(`[自主进化] 🆕 检测到Xray更新: ${result.installed_version} → ${result.latest_version}`);
|
||||
await orchestrateUpdate({
|
||||
type: 'xray-core',
|
||||
from_version: result.installed_version,
|
||||
to_version: result.latest_version,
|
||||
source: 'protocol-mirror'
|
||||
});
|
||||
} else {
|
||||
console.log('[自主进化] ✅ 协议版本已是最新');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[自主进化] 协议检查异常:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// §2 月度进化周期 (每月1号 00:00)
|
||||
// ═══════════════════════════════════════════════
|
||||
async function monthlyEvolution() {
|
||||
console.log('[自主进化] ════ 月度进化周期开始 ════');
|
||||
const status = readEvolutionStatus();
|
||||
|
||||
// 1. 收集系统状态
|
||||
const guardian = safeReadJSON(GUARDIAN_FILE) || {};
|
||||
const mirror = safeReadJSON(MIRROR_FILE) || {};
|
||||
const pool = safeReadJSON(POOL_STATUS_FILE) || {};
|
||||
const users = safeReadJSON(USERS_FILE) || { users: [] };
|
||||
const userCount = (users.users || []).filter(u => u.enabled !== false).length;
|
||||
|
||||
console.log(`[自主进化] 系统状态: ${userCount}活跃用户, 守护=${guardian.status || '未知'}, 镜像=${mirror.mirror_status || '未知'}`);
|
||||
|
||||
// 2. LLM 深度推理 — 系统健康分析
|
||||
const llmRouter = safeRequire('./llm-router');
|
||||
let llmAnalysis = null;
|
||||
|
||||
if (llmRouter) {
|
||||
try {
|
||||
const prompt = `你是光湖语言世界VPN系统的月度分析引擎。现在是每月1号,请分析系统状态并给出建议。
|
||||
|
||||
当前状态:
|
||||
- 活跃用户: ${userCount}人
|
||||
- 守护Agent状态: ${guardian.status || '未知'},自动修复${guardian.auto_fixes || 0}次,LLM咨询${guardian.llm_consultations || 0}次
|
||||
- 协议镜像: ${mirror.mirror_status || '未知'},已安装版本=${mirror.installed_version || '未知'},最新=${mirror.latest_version || '未知'}
|
||||
- 上月流量池: 已用${pool.pool_used_gb ? pool.pool_used_gb.toFixed(1) : '?'}GB / ${pool.pool_total_gb || 2000}GB (${pool.pool_percentage || 0}%)
|
||||
- 累计进化次数: ${status.evolution_count}
|
||||
- 累计更新: 成功${status.updates_applied}次,失败${status.updates_failed}次
|
||||
|
||||
请回答:
|
||||
1. 当前系统是否健康?(一句话)
|
||||
2. 是否需要升级Xray或调整配置?(是/否+理由)
|
||||
3. 流量使用是否正常?(一句话)
|
||||
4. 本月优化建议 (最多3条)
|
||||
|
||||
请用JSON格式回复: {"healthy":bool,"need_upgrade":bool,"analysis":"...","suggestions":["..."]}`;
|
||||
|
||||
const result = await llmRouter.callLLM(prompt, {
|
||||
systemPrompt: '你是光湖语言世界VPN系统的AI分析引擎。请以JSON格式简洁回复。',
|
||||
maxTokens: 800,
|
||||
timeout: 60000
|
||||
});
|
||||
|
||||
if (result) {
|
||||
llmAnalysis = result.content;
|
||||
console.log(`[自主进化] LLM分析完成 (模型: ${result.model})`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[自主进化] LLM月度分析失败:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 发送月初重置邮件给所有用户
|
||||
console.log('[自主进化] 发送月初重置邮件...');
|
||||
try {
|
||||
const emailHub = safeRequire('./email-hub');
|
||||
if (emailHub) {
|
||||
const result = await emailHub.sendMonthlyResetEmail();
|
||||
console.log(`[自主进化] 月初邮件: ${result.sent}成功 / ${result.failed}失败`);
|
||||
} else {
|
||||
// 回退: 用send-subscription.js的alert命令
|
||||
const sendScript = path.join(__dirname, 'send-subscription.js');
|
||||
execFileSync('node', [sendScript, 'alert', '每月1号流量池已重置 · 本月可用2000GB'], {
|
||||
encoding: 'utf8', timeout: 30000
|
||||
});
|
||||
console.log('[自主进化] 月初邮件: 已通过alert方式发送给管理员');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[自主进化] 月初邮件发送失败:', err.message);
|
||||
}
|
||||
|
||||
// 4. 生成月度进化报告
|
||||
const report = {
|
||||
period: getChinaTime().toISOString().slice(0, 7),
|
||||
generated_at: new Date().toISOString(),
|
||||
user_count: userCount,
|
||||
guardian_status: guardian.status || 'unknown',
|
||||
mirror_status: mirror.mirror_status || 'unknown',
|
||||
pool_last_month: {
|
||||
used_gb: pool.pool_used_gb || 0,
|
||||
total_gb: pool.pool_total_gb || 2000,
|
||||
percentage: pool.pool_percentage || 0
|
||||
},
|
||||
llm_analysis: llmAnalysis,
|
||||
evolution_count: status.evolution_count,
|
||||
updates: { applied: status.updates_applied, failed: status.updates_failed }
|
||||
};
|
||||
|
||||
fs.mkdirSync(REPORTS_DIR, { recursive: true });
|
||||
const reportFile = path.join(REPORTS_DIR, `${report.period}.json`);
|
||||
fs.writeFileSync(reportFile, JSON.stringify(report, null, 2));
|
||||
console.log(`[自主进化] 月度报告已保存: ${reportFile}`);
|
||||
|
||||
// 5. 更新状态
|
||||
status.evolution_count++;
|
||||
status.last_monthly_report = report.period;
|
||||
status.schedules.monthly_evolution.last_run = new Date().toISOString();
|
||||
saveEvolutionStatus(status);
|
||||
|
||||
console.log('[自主进化] ════ 月度进化周期完成 ════');
|
||||
return report;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// §3 更新编排 (检测到更新时触发)
|
||||
// ═══════════════════════════════════════════════
|
||||
async function orchestrateUpdate(updateInfo) {
|
||||
console.log(`[自主进化] ═══ 开始更新编排: ${updateInfo.type} ═══`);
|
||||
const status = readEvolutionStatus();
|
||||
|
||||
let retries = 0;
|
||||
let success = false;
|
||||
|
||||
while (retries < MAX_UPDATE_RETRIES && !success) {
|
||||
retries++;
|
||||
console.log(`[自主进化] 尝试 ${retries}/${MAX_UPDATE_RETRIES}...`);
|
||||
|
||||
try {
|
||||
// Phase 1: 备份
|
||||
console.log('[自主进化] Phase 1: 备份当前状态...');
|
||||
// protocol-mirror.js 内部会处理备份
|
||||
|
||||
// Phase 2: 应用更新
|
||||
console.log('[自主进化] Phase 2: 应用更新...');
|
||||
const protocolMirror = safeRequire('./protocol-mirror');
|
||||
if (!protocolMirror) {
|
||||
throw new Error('协议镜像模块不可用');
|
||||
}
|
||||
await protocolMirror.performUpdate();
|
||||
|
||||
// Phase 3: 验证
|
||||
console.log('[自主进化] Phase 3: 验证更新...');
|
||||
const mirrorStatus = safeReadJSON(MIRROR_FILE);
|
||||
if (mirrorStatus && mirrorStatus.mirror_status === 'error') {
|
||||
throw new Error('更新后镜像状态异常');
|
||||
}
|
||||
|
||||
success = true;
|
||||
status.updates_applied++;
|
||||
console.log('[自主进化] ✅ 更新成功');
|
||||
|
||||
} catch (err) {
|
||||
console.error(`[自主进化] ❌ 更新失败 (尝试${retries}): ${err.message}`);
|
||||
if (retries < MAX_UPDATE_RETRIES) {
|
||||
console.log('[自主进化] 等待30秒后重试...');
|
||||
await new Promise(r => setTimeout(r, 30000));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (success) {
|
||||
// Phase 4: 通知用户
|
||||
console.log('[自主进化] Phase 4: 通知所有用户...');
|
||||
try {
|
||||
const emailHub = safeRequire('./email-hub');
|
||||
if (emailHub) {
|
||||
const description = `Xray核心已从 ${updateInfo.from_version} 升级到 ${updateInfo.to_version}。\n安全性和性能已优化,无需手动操作。\n刷新订阅即可获取最新配置。`;
|
||||
await emailHub.sendUpdateNotifyEmail(description);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[自主进化] 更新通知邮件发送失败:', err.message);
|
||||
}
|
||||
} else {
|
||||
// Phase 5: 3次失败 → 告警管理员
|
||||
status.updates_failed++;
|
||||
console.error('[自主进化] ⚠️ 更新3次均失败,告警管理员...');
|
||||
try {
|
||||
const sendScript = path.join(__dirname, 'send-subscription.js');
|
||||
const msg = `Xray更新失败(${MAX_UPDATE_RETRIES}次)\n类型: ${updateInfo.type}\n目标版本: ${updateInfo.to_version}\n请手动检查`;
|
||||
execFileSync('node', [sendScript, 'alert', msg], {
|
||||
encoding: 'utf8', timeout: 30000
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[自主进化] 告警邮件也发送失败:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
saveEvolutionStatus(status);
|
||||
console.log(`[自主进化] ═══ 更新编排完成 (${success ? '成功' : '失败'}) ═══`);
|
||||
return success;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// §4 流量预警处理
|
||||
// ═══════════════════════════════════════════════
|
||||
async function handleTrafficAlert(percentage) {
|
||||
console.log(`[自主进化] 流量预警触发: ${percentage}%`);
|
||||
|
||||
try {
|
||||
const emailHub = safeRequire('./email-hub');
|
||||
if (emailHub) {
|
||||
await emailHub.sendTrafficWarnEmail(percentage);
|
||||
} else {
|
||||
const sendScript = path.join(__dirname, 'send-subscription.js');
|
||||
execFileSync('node', [sendScript, 'alert', `流量池已使用${percentage}%,请注意用量`], {
|
||||
encoding: 'utf8', timeout: 30000
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[自主进化] 流量预警邮件发送失败:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// §5 用户反馈处理 (Phase 2 预留)
|
||||
// ═══════════════════════════════════════════════
|
||||
async function processWeeklyFeedback() {
|
||||
console.log('[自主进化] 📥 用户反馈处理 (Phase 2 — 待实现)');
|
||||
// Phase 2: 从COS桶读取反馈 → LLM评估 → 生成处理结果
|
||||
const status = readEvolutionStatus();
|
||||
status.schedules.weekly_feedback.last_run = new Date().toISOString();
|
||||
saveEvolutionStatus(status);
|
||||
}
|
||||
|
||||
async function sendWeeklyResponse() {
|
||||
console.log('[自主进化] 📤 反馈结果推送 (Phase 2 — 待实现)');
|
||||
// Phase 2: 将处理结果推送给对应用户
|
||||
const status = readEvolutionStatus();
|
||||
status.schedules.weekly_response.last_run = new Date().toISOString();
|
||||
saveEvolutionStatus(status);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// §6 流量池监测 (检查是否需要触发70%预警)
|
||||
// ═══════════════════════════════════════════════
|
||||
async function checkTrafficPoolAlert() {
|
||||
const pool = safeReadJSON(POOL_STATUS_FILE);
|
||||
if (!pool) return;
|
||||
|
||||
const status = readEvolutionStatus();
|
||||
const pct = pool.pool_percentage || 0;
|
||||
|
||||
// 70%预警 (每月只触发一次)
|
||||
if (pct >= 70) {
|
||||
const lastRun = status.schedules.traffic_alert_70.last_run;
|
||||
const currentPeriod = pool.period || new Date().toISOString().slice(0, 7);
|
||||
|
||||
// 检查本月是否已发过 (比较YYYY-MM)
|
||||
const lastRunMonth = lastRun ? lastRun.slice(0, 7) : '';
|
||||
if (lastRunMonth !== currentPeriod) {
|
||||
await handleTrafficAlert(Math.round(pct));
|
||||
status.schedules.traffic_alert_70.last_run = new Date().toISOString();
|
||||
saveEvolutionStatus(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// §7 调度引擎 (每5分钟检查)
|
||||
// ═══════════════════════════════════════════════
|
||||
async function checkSchedule() {
|
||||
const now = getChinaTime();
|
||||
const status = readEvolutionStatus();
|
||||
status.last_schedule_check = new Date().toISOString();
|
||||
|
||||
const hour = now.getHours();
|
||||
const minute = now.getMinutes();
|
||||
const day = now.getDate();
|
||||
const dayOfWeek = now.getDay(); // 0=周日, 5=周五, 1=周一
|
||||
|
||||
// ── 协议版本检查 (每30分钟) ──
|
||||
const lastProtocol = status.schedules.protocol_check.last_run;
|
||||
if (!lastProtocol || (Date.now() - new Date(lastProtocol).getTime()) >= PROTOCOL_CHECK_INTERVAL) {
|
||||
try {
|
||||
await runProtocolCheck();
|
||||
status.schedules.protocol_check.last_run = new Date().toISOString();
|
||||
} catch (err) {
|
||||
console.error('[自主进化] 协议检查调度异常:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 月度进化 (每月1号 00:00-00:04) ──
|
||||
if (day === 1 && hour === 0 && minute < 5) {
|
||||
const lastMonthly = status.schedules.monthly_evolution.last_run;
|
||||
// 比较YYYY-MM确保本月未执行过
|
||||
const lastRunDate = lastMonthly ? new Date(lastMonthly) : null;
|
||||
const lastRunMonth = lastRunDate ? `${lastRunDate.getFullYear()}-${String(lastRunDate.getMonth() + 1).padStart(2, '0')}` : '';
|
||||
const currentMonth = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
|
||||
|
||||
if (lastRunMonth !== currentMonth) {
|
||||
try {
|
||||
await monthlyEvolution();
|
||||
} catch (err) {
|
||||
console.error('[自主进化] 月度进化调度异常:', err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 周五20:00 用户反馈处理 ──
|
||||
if (dayOfWeek === 5 && hour === 20 && minute < 5) {
|
||||
const lastFeedback = status.schedules.weekly_feedback.last_run;
|
||||
const today = now.toISOString().slice(0, 10);
|
||||
if (!lastFeedback || !lastFeedback.startsWith(today)) {
|
||||
try {
|
||||
await processWeeklyFeedback();
|
||||
} catch (err) {
|
||||
console.error('[自主进化] 反馈处理调度异常:', err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 周一09:00 推送反馈结果 ──
|
||||
if (dayOfWeek === 1 && hour === 9 && minute < 5) {
|
||||
const lastResponse = status.schedules.weekly_response.last_run;
|
||||
const today = now.toISOString().slice(0, 10);
|
||||
if (!lastResponse || !lastResponse.startsWith(today)) {
|
||||
try {
|
||||
await sendWeeklyResponse();
|
||||
} catch (err) {
|
||||
console.error('[自主进化] 反馈推送调度异常:', err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 流量池预警检测 ──
|
||||
try {
|
||||
await checkTrafficPoolAlert();
|
||||
} catch (err) {
|
||||
console.error('[自主进化] 流量预警检测异常:', err.message);
|
||||
}
|
||||
|
||||
saveEvolutionStatus(status);
|
||||
}
|
||||
|
||||
// ── 获取进化状态 (供外部查询) ─────────────────
|
||||
function getEvolutionStatus() {
|
||||
return readEvolutionStatus();
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 导出
|
||||
// ═══════════════════════════════════════════════
|
||||
module.exports = {
|
||||
monthlyEvolution,
|
||||
orchestrateUpdate,
|
||||
handleTrafficAlert,
|
||||
getEvolutionStatus,
|
||||
runProtocolCheck
|
||||
};
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 启动自主进化引擎
|
||||
// ═══════════════════════════════════════════════
|
||||
if (require.main === module) {
|
||||
console.log('♾️ 光湖语言世界 · 自主进化引擎启动 (∞ 版本)');
|
||||
console.log(` 调度间隔: ${SCHEDULE_CHECK_INTERVAL / 1000}秒`);
|
||||
console.log(` 协议检查: 每${PROTOCOL_CHECK_INTERVAL / 60000}分钟`);
|
||||
console.log(` 月度进化: 每月1号 00:00 CST`);
|
||||
console.log(` 反馈处理: 每周五 20:00 CST (Phase 2)`);
|
||||
console.log(` 反馈推送: 每周一 09:00 CST (Phase 2)`);
|
||||
console.log(` 当前时间: ${getChinaTimeStr()}`);
|
||||
console.log(` 数据目录: ${DATA_DIR}`);
|
||||
|
||||
// 初始化状态文件
|
||||
const status = readEvolutionStatus();
|
||||
status.started_at = new Date().toISOString();
|
||||
status.status = 'running';
|
||||
saveEvolutionStatus(status);
|
||||
|
||||
// 立即执行一次协议检查
|
||||
runProtocolCheck().catch(err => {
|
||||
console.error('[自主进化] 初始协议检查失败:', err.message);
|
||||
});
|
||||
|
||||
// 定期调度检查
|
||||
setInterval(() => {
|
||||
checkSchedule().catch(err => {
|
||||
console.error('[自主进化] 调度检查异常:', err.message);
|
||||
});
|
||||
}, SCHEDULE_CHECK_INTERVAL);
|
||||
}
|
||||
|
|
@ -0,0 +1,608 @@
|
|||
#!/usr/bin/env node
|
||||
// ═══════════════════════════════════════════════
|
||||
// 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001
|
||||
// 📜 Copyright: 国作登字-2026-A-00037559
|
||||
// ═══════════════════════════════════════════════
|
||||
// server/proxy/service/email-hub.js
|
||||
// 📧 光湖语言世界 · 邮件通信中枢
|
||||
//
|
||||
// ∞版本邮件中枢 — 统一管理所有用户邮件推送
|
||||
//
|
||||
// 邮件类型:
|
||||
// 1. 月初重置通知 — 每月1号通知流量池已重置
|
||||
// 2. 更新升级通知 — 系统更新后告知用户
|
||||
// 3. 流量预警通知 — 70%/80%/90%/100% 阶梯告警
|
||||
// 4. 安全风险提醒 — 单用户异常行为告知
|
||||
// 5. 反馈确认回复 — 收到用户反馈后的自动确认
|
||||
//
|
||||
// 所有邮件底部附「意见反馈」链接
|
||||
//
|
||||
// 用法:
|
||||
// node email-hub.js monthly-reset — 发送月初重置邮件给所有用户
|
||||
// node email-hub.js update-notify <desc> — 发送更新通知给所有用户
|
||||
// node email-hub.js traffic-warn <pct> — 发送流量预警给所有用户
|
||||
// node email-hub.js security-warn <email> <msg> — 发送安全提醒给单用户
|
||||
// node email-hub.js feedback-ack <email> — 发送反馈确认给单用户
|
||||
//
|
||||
// 运行方式: CLI调用 (由auto-evolution.js调度)
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
'use strict';
|
||||
|
||||
const tls = require('tls');
|
||||
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 USERS_FILE = path.join(DATA_DIR, 'users.json');
|
||||
const KEYS_FILE = process.env.ZY_PROXY_KEYS_FILE || path.join(PROXY_DIR, '.env.keys');
|
||||
const POOL_STATUS_FILE = path.join(DATA_DIR, 'pool-quota-status.json');
|
||||
const EMAIL_LOG_FILE = path.join(DATA_DIR, 'email-hub-log.json');
|
||||
|
||||
// ── 加载配置 ─────────────────────────────────
|
||||
function loadConfig() {
|
||||
const config = {
|
||||
smtp_user: process.env.ZY_SMTP_USER || '',
|
||||
smtp_pass: process.env.ZY_SMTP_PASS || '',
|
||||
server_host: process.env.ZY_SERVER_HOST || ''
|
||||
};
|
||||
|
||||
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('=');
|
||||
const k = key.trim();
|
||||
const v = vals.join('=').trim();
|
||||
if (!v) continue;
|
||||
if (k === 'ZY_SERVER_HOST' && !config.server_host) config.server_host = v;
|
||||
if (k === 'ZY_SMTP_USER' && !config.smtp_user) config.smtp_user = v;
|
||||
if (k === 'ZY_SMTP_PASS' && !config.smtp_pass) config.smtp_pass = v;
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
// ── SMTP发送邮件 ─────────────────────────────
|
||||
function detectSmtpHost(email) {
|
||||
if (email.includes('@qq.com')) return 'smtp.qq.com';
|
||||
if (email.includes('@163.com')) return 'smtp.163.com';
|
||||
if (email.includes('@126.com')) return 'smtp.126.com';
|
||||
if (email.includes('@gmail.com')) return 'smtp.gmail.com';
|
||||
if (email.includes('@outlook.com') || email.includes('@hotmail.com')) return 'smtp.office365.com';
|
||||
if (email.includes('@yeah.net')) return 'smtp.yeah.net';
|
||||
return 'smtp.qq.com';
|
||||
}
|
||||
|
||||
async function sendEmail(to, subject, htmlBody) {
|
||||
const config = loadConfig();
|
||||
if (!config.smtp_user || !config.smtp_pass) {
|
||||
throw new Error('SMTP未配置 (需要ZY_SMTP_USER和ZY_SMTP_PASS)');
|
||||
}
|
||||
|
||||
const smtpHost = detectSmtpHost(config.smtp_user);
|
||||
const smtpPort = 465;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let settled = false;
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
try { socket.destroy(); } catch { /* ignore */ }
|
||||
reject(new Error('SMTP超时(30s)'));
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
const socket = tls.connect(smtpPort, smtpHost, {}, () => {
|
||||
let step = 0;
|
||||
const from = config.smtp_user;
|
||||
|
||||
const commands = [
|
||||
`EHLO zy-proxy\r\n`,
|
||||
`AUTH LOGIN\r\n`,
|
||||
`${Buffer.from(from).toString('base64')}\r\n`,
|
||||
`${Buffer.from(config.smtp_pass).toString('base64')}\r\n`,
|
||||
`MAIL FROM:<${from}>\r\n`,
|
||||
`RCPT TO:<${to}>\r\n`,
|
||||
`DATA\r\n`,
|
||||
`From: =?UTF-8?B?${Buffer.from('光湖语言世界').toString('base64')}?= <${from}>\r\nTo: <${to}>\r\nSubject: =?UTF-8?B?${Buffer.from(subject).toString('base64')}?=\r\nContent-Type: text/html; charset=utf-8\r\nMIME-Version: 1.0\r\n\r\n${htmlBody}\r\n.\r\n`,
|
||||
`QUIT\r\n`
|
||||
];
|
||||
|
||||
socket.on('data', () => {
|
||||
if (step < commands.length) {
|
||||
socket.write(commands[step]);
|
||||
step++;
|
||||
}
|
||||
if (step >= commands.length && !settled) {
|
||||
settled = true;
|
||||
clearTimeout(timeoutId);
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('error', (err) => {
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
clearTimeout(timeoutId);
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('error', (err) => {
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
clearTimeout(timeoutId);
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── 加载所有启用用户 ─────────────────────────
|
||||
function getEnabledUsers() {
|
||||
try {
|
||||
const data = JSON.parse(fs.readFileSync(USERS_FILE, 'utf8'));
|
||||
return (data.users || []).filter(u => u.enabled !== false);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// ── 加载流量池状态 ────────────────────────────
|
||||
function getPoolStatus() {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(POOL_STATUS_FILE, 'utf8'));
|
||||
} catch {
|
||||
return { pool_total_gb: 2000, pool_used_gb: 0, pool_percentage: 0, period: '' };
|
||||
}
|
||||
}
|
||||
|
||||
// ── 记录邮件发送日志 ─────────────────────────
|
||||
function logEmail(type, recipients, success, error) {
|
||||
let log;
|
||||
try {
|
||||
log = JSON.parse(fs.readFileSync(EMAIL_LOG_FILE, 'utf8'));
|
||||
} catch {
|
||||
log = { entries: [] };
|
||||
}
|
||||
|
||||
log.entries.push({
|
||||
type,
|
||||
recipients: Array.isArray(recipients) ? recipients.length : 1,
|
||||
success,
|
||||
error: error || null,
|
||||
time: new Date().toISOString()
|
||||
});
|
||||
|
||||
// 只保留最近100条
|
||||
if (log.entries.length > 100) {
|
||||
log.entries = log.entries.slice(-100);
|
||||
}
|
||||
|
||||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||
fs.writeFileSync(EMAIL_LOG_FILE, JSON.stringify(log, null, 2));
|
||||
}
|
||||
|
||||
// ── 反馈链接 (所有邮件底部附带) ───────────────
|
||||
function getFeedbackFooter(config) {
|
||||
const host = config.server_host || 'guanghulab.com';
|
||||
return `
|
||||
<hr style="border: none; border-top: 1px solid #eee; margin: 25px 0 15px;">
|
||||
<div style="text-align: center; padding: 10px;">
|
||||
<p style="color: #888; font-size: 12px; margin: 0 0 8px;">
|
||||
有建议或问题?欢迎反馈 👇
|
||||
</p>
|
||||
<a href="https://${host}/api/proxy-v3/feedback"
|
||||
style="display: inline-block; background: #4a90d9; color: white; padding: 8px 20px; border-radius: 6px; text-decoration: none; font-size: 13px;">
|
||||
📝 提交意见反馈
|
||||
</a>
|
||||
<p style="color: #aaa; font-size: 11px; margin: 10px 0 0;">
|
||||
每周五 20:00 铸渊集中处理 · 每周一推送处理结果
|
||||
</p>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── 通用邮件模板包装 ─────────────────────────
|
||||
function wrapEmailTemplate(title, content, config) {
|
||||
const now = new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' });
|
||||
const feedbackFooter = getFeedbackFooter(config);
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><meta charset="utf-8"></head>
|
||||
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; background: #f8f9fa;">
|
||||
<div style="background: white; border-radius: 12px; padding: 30px; box-shadow: 0 2px 10px rgba(0,0,0,0.1);">
|
||||
<h1 style="color: #1a1a2e; margin-bottom: 5px;">🌐 光湖语言世界</h1>
|
||||
<p style="color: #666; margin-top: 0; font-size: 13px;">${title}</p>
|
||||
|
||||
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
|
||||
|
||||
${content}
|
||||
|
||||
${feedbackFooter}
|
||||
|
||||
<hr style="border: none; border-top: 1px solid #eee; margin: 15px 0;">
|
||||
<p style="color: #aaa; font-size: 11px; text-align: center;">
|
||||
光湖语言世界 · ∞版本 · ${now}<br>
|
||||
国作登字-2026-A-00037559
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 📧 邮件类型 1: 月初重置通知
|
||||
// ═══════════════════════════════════════════════
|
||||
function generateMonthlyResetEmail(config) {
|
||||
const now = new Date();
|
||||
const month = `${now.getFullYear()}年${now.getMonth() + 1}月`;
|
||||
|
||||
const content = `
|
||||
<div style="background: #d4edda; border: 1px solid #c3e6cb; border-radius: 8px; padding: 15px; margin: 15px 0;">
|
||||
<strong style="color: #155724;">✅ ${month} 流量节点已重置</strong>
|
||||
</div>
|
||||
|
||||
<h3 style="color: #333;">📊 本月配额</h3>
|
||||
<table style="width: 100%; border-collapse: collapse;">
|
||||
<tr><td style="padding: 8px; color: #666;">流量池</td><td style="padding: 8px; font-weight: bold;">2000 GB</td></tr>
|
||||
<tr><td style="padding: 8px; color: #666;">已使用</td><td style="padding: 8px; color: #28a745; font-weight: bold;">0 GB (已重置)</td></tr>
|
||||
<tr><td style="padding: 8px; color: #666;">重置日期</td><td style="padding: 8px;">每月1日</td></tr>
|
||||
<tr><td style="padding: 8px; color: #666;">协议</td><td style="padding: 8px;">VLESS + Reality</td></tr>
|
||||
</table>
|
||||
|
||||
<p style="color: #666; font-size: 13px; margin-top: 15px;">
|
||||
💡 您无需任何操作,刷新订阅即可继续使用。<br>
|
||||
流量池为所有用户共享,请合理使用。
|
||||
</p>`;
|
||||
|
||||
return wrapEmailTemplate(`${month} · 流量重置通知`, content, config);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 📧 邮件类型 2: 更新升级通知
|
||||
// ═══════════════════════════════════════════════
|
||||
function generateUpdateNotifyEmail(description, config) {
|
||||
const now = new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' });
|
||||
|
||||
const content = `
|
||||
<div style="background: #cce5ff; border: 1px solid #b8daff; border-radius: 8px; padding: 15px; margin: 15px 0;">
|
||||
<strong style="color: #004085;">🔄 系统已完成升级</strong>
|
||||
</div>
|
||||
|
||||
<h3 style="color: #333;">📋 本次更新内容</h3>
|
||||
<div style="background: #f8f9fa; border-radius: 8px; padding: 15px; color: #333; line-height: 1.8;">
|
||||
${escapeHtml(description).replace(/\n/g, '<br>')}
|
||||
</div>
|
||||
|
||||
<p style="color: #666; font-size: 13px; margin-top: 15px;">
|
||||
⏰ 更新时间: ${now}<br>
|
||||
💡 大部分更新只需刷新订阅即可生效。如需重新下载订阅链接,会另行通知。
|
||||
</p>`;
|
||||
|
||||
return wrapEmailTemplate('系统升级通知', content, config);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 📧 邮件类型 3: 流量预警通知
|
||||
// ═══════════════════════════════════════════════
|
||||
function generateTrafficWarnEmail(percentage, poolStatus, config) {
|
||||
const usedGB = poolStatus.pool_used_gb ? poolStatus.pool_used_gb.toFixed(1) : '?';
|
||||
const totalGB = poolStatus.pool_total_gb || 2000;
|
||||
const remainGB = (totalGB - parseFloat(usedGB)).toFixed(1);
|
||||
|
||||
let urgencyColor, urgencyBg, urgencyBorder, urgencyText;
|
||||
if (percentage >= 100) {
|
||||
urgencyColor = '#721c24'; urgencyBg = '#f8d7da'; urgencyBorder = '#f5c6cb';
|
||||
urgencyText = '⛔ 流量池已耗尽!所有连接已暂停。';
|
||||
} else if (percentage >= 90) {
|
||||
urgencyColor = '#856404'; urgencyBg = '#fff3cd'; urgencyBorder = '#ffeaa7';
|
||||
urgencyText = `⚠️ 流量池仅剩 ${remainGB}GB,请节约使用!`;
|
||||
} else {
|
||||
urgencyColor = '#0c5460'; urgencyBg = '#d1ecf1'; urgencyBorder = '#bee5eb';
|
||||
urgencyText = `📊 流量池已使用 ${percentage}%,剩余 ${remainGB}GB`;
|
||||
}
|
||||
|
||||
const content = `
|
||||
<div style="background: ${urgencyBg}; border: 1px solid ${urgencyBorder}; border-radius: 8px; padding: 15px; margin: 15px 0;">
|
||||
<strong style="color: ${urgencyColor};">${urgencyText}</strong>
|
||||
</div>
|
||||
|
||||
<h3 style="color: #333;">📊 流量池状态</h3>
|
||||
<table style="width: 100%; border-collapse: collapse;">
|
||||
<tr><td style="padding: 8px; color: #666;">已使用</td><td style="padding: 8px; font-weight: bold;">${usedGB} GB</td></tr>
|
||||
<tr><td style="padding: 8px; color: #666;">总配额</td><td style="padding: 8px;">${totalGB} GB</td></tr>
|
||||
<tr><td style="padding: 8px; color: #666;">使用率</td><td style="padding: 8px; font-weight: bold; color: ${urgencyColor};">${percentage}%</td></tr>
|
||||
<tr><td style="padding: 8px; color: #666;">剩余</td><td style="padding: 8px;">${remainGB} GB</td></tr>
|
||||
</table>
|
||||
|
||||
<div style="background: #f0f0f0; border-radius: 8px; height: 20px; margin: 15px 0; overflow: hidden;">
|
||||
<div style="background: ${percentage >= 90 ? '#dc3545' : percentage >= 70 ? '#ffc107' : '#28a745'}; height: 100%; width: ${Math.min(percentage, 100)}%; border-radius: 8px;"></div>
|
||||
</div>
|
||||
|
||||
<p style="color: #888; font-size: 12px;">
|
||||
流量池每月1日重置。所有用户共享 ${totalGB}GB 月配额。
|
||||
</p>`;
|
||||
|
||||
return wrapEmailTemplate('流量预警通知', content, config);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 📧 邮件类型 4: 安全风险提醒 (单用户)
|
||||
// ═══════════════════════════════════════════════
|
||||
function generateSecurityWarnEmail(message, config) {
|
||||
const content = `
|
||||
<div style="background: #fff3cd; border: 1px solid #ffc107; border-radius: 8px; padding: 15px; margin: 15px 0;">
|
||||
<strong style="color: #856404;">🛡️ 安全提醒</strong>
|
||||
</div>
|
||||
|
||||
<div style="background: #f8f9fa; border-radius: 8px; padding: 15px; color: #333; line-height: 1.8;">
|
||||
${escapeHtml(message).replace(/\n/g, '<br>')}
|
||||
</div>
|
||||
|
||||
<h3 style="color: #333;">💡 安全建议</h3>
|
||||
<ul style="color: #666; line-height: 2;">
|
||||
<li>避免同时使用多个VPN客户端</li>
|
||||
<li>关闭不使用的VPN连接</li>
|
||||
<li>确保订阅链接仅个人使用</li>
|
||||
<li>如有异常,请及时反馈</li>
|
||||
</ul>`;
|
||||
|
||||
return wrapEmailTemplate('安全使用提醒', content, config);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 📧 邮件类型 5: 反馈确认回复
|
||||
// ═══════════════════════════════════════════════
|
||||
function generateFeedbackAckEmail(config) {
|
||||
const content = `
|
||||
<div style="background: #d4edda; border: 1px solid #c3e6cb; border-radius: 8px; padding: 15px; margin: 15px 0;">
|
||||
<strong style="color: #155724;">✅ 您的反馈已收到</strong>
|
||||
</div>
|
||||
|
||||
<p style="color: #666; line-height: 1.8;">
|
||||
感谢您的宝贵意见!铸渊将在以下时间处理:
|
||||
</p>
|
||||
|
||||
<table style="width: 100%; border-collapse: collapse;">
|
||||
<tr><td style="padding: 8px; color: #666;">📥 收集截止</td><td style="padding: 8px;">每周五 20:00</td></tr>
|
||||
<tr><td style="padding: 8px; color: #666;">🔍 深度分析</td><td style="padding: 8px;">周五晚间 (AI辅助评估)</td></tr>
|
||||
<tr><td style="padding: 8px; color: #666;">📤 结果推送</td><td style="padding: 8px;">每周一 09:00</td></tr>
|
||||
</table>
|
||||
|
||||
<p style="color: #888; font-size: 12px; margin-top: 15px;">
|
||||
注: 并非所有需求都会被采纳。铸渊会基于系统安全性、架构完整性和整体规划进行评估。
|
||||
</p>`;
|
||||
|
||||
return wrapEmailTemplate('反馈已收到', content, config);
|
||||
}
|
||||
|
||||
// ── HTML转义 ─────────────────────────────────
|
||||
function escapeHtml(str) {
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 📧 批量发送函数
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* 给所有启用用户发送邮件
|
||||
* @param {string} subject 邮件主题
|
||||
* @param {Function} htmlGenerator 生成HTML的函数(接收config参数)
|
||||
* @returns {Promise<{sent: number, failed: number, errors: string[]}>}
|
||||
*/
|
||||
async function sendToAllUsers(subject, htmlGenerator) {
|
||||
const users = getEnabledUsers();
|
||||
const config = loadConfig();
|
||||
|
||||
if (users.length === 0) {
|
||||
console.log('[邮件中枢] 无启用用户,跳过发送');
|
||||
return { sent: 0, failed: 0, errors: [] };
|
||||
}
|
||||
|
||||
const html = htmlGenerator(config);
|
||||
let sent = 0;
|
||||
let failed = 0;
|
||||
const errors = [];
|
||||
|
||||
console.log(`[邮件中枢] 开始批量发送 (${users.length}位用户): ${subject}`);
|
||||
|
||||
for (const user of users) {
|
||||
try {
|
||||
await sendEmail(user.email, subject, html);
|
||||
sent++;
|
||||
console.log(` ✅ ${user.email}`);
|
||||
// 间隔500ms避免SMTP限流
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
} catch (err) {
|
||||
failed++;
|
||||
errors.push(`${user.email}: ${err.message}`);
|
||||
console.error(` ❌ ${user.email}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[邮件中枢] 发送完成: ${sent}成功 / ${failed}失败`);
|
||||
return { sent, failed, errors };
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 📧 公开API (供auto-evolution.js调用)
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* 发送月初重置通知给所有用户
|
||||
*/
|
||||
async function sendMonthlyResetEmail() {
|
||||
const result = await sendToAllUsers(
|
||||
'🌐 光湖语言世界 · 本月流量已重置',
|
||||
(config) => generateMonthlyResetEmail(config)
|
||||
);
|
||||
logEmail('monthly-reset', result.sent + result.failed, result.sent, result.errors.join('; '));
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送更新通知给所有用户
|
||||
* @param {string} description 更新说明
|
||||
*/
|
||||
async function sendUpdateNotifyEmail(description) {
|
||||
const result = await sendToAllUsers(
|
||||
'🌐 光湖语言世界 · 系统已升级',
|
||||
(config) => generateUpdateNotifyEmail(description, config)
|
||||
);
|
||||
logEmail('update-notify', result.sent + result.failed, result.sent, result.errors.join('; '));
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送流量预警给所有用户
|
||||
* @param {number} percentage 流量使用百分比
|
||||
*/
|
||||
async function sendTrafficWarnEmail(percentage) {
|
||||
const poolStatus = getPoolStatus();
|
||||
const result = await sendToAllUsers(
|
||||
`🌐 光湖语言世界 · 流量预警 (${percentage}%)`,
|
||||
(config) => generateTrafficWarnEmail(percentage, poolStatus, config)
|
||||
);
|
||||
logEmail('traffic-warn', result.sent + result.failed, result.sent, result.errors.join('; '));
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送安全提醒给单个用户
|
||||
* @param {string} email 目标邮箱
|
||||
* @param {string} message 安全提醒内容
|
||||
*/
|
||||
async function sendSecurityWarnEmail(email, message) {
|
||||
const config = loadConfig();
|
||||
const html = generateSecurityWarnEmail(message, config);
|
||||
|
||||
try {
|
||||
await sendEmail(email, '🛡️ 光湖语言世界 · 安全提醒', html);
|
||||
console.log(`[邮件中枢] ✅ 安全提醒已发送: ${email}`);
|
||||
logEmail('security-warn', email, true, null);
|
||||
return { sent: 1, failed: 0 };
|
||||
} catch (err) {
|
||||
console.error(`[邮件中枢] ❌ 安全提醒发送失败: ${err.message}`);
|
||||
logEmail('security-warn', email, false, err.message);
|
||||
return { sent: 0, failed: 1 };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送反馈确认给单个用户
|
||||
* @param {string} email 目标邮箱
|
||||
*/
|
||||
async function sendFeedbackAckEmail(email) {
|
||||
const config = loadConfig();
|
||||
const html = generateFeedbackAckEmail(config);
|
||||
|
||||
try {
|
||||
await sendEmail(email, '🌐 光湖语言世界 · 反馈已收到', html);
|
||||
console.log(`[邮件中枢] ✅ 反馈确认已发送: ${email}`);
|
||||
logEmail('feedback-ack', email, true, null);
|
||||
return { sent: 1, failed: 0 };
|
||||
} catch (err) {
|
||||
console.error(`[邮件中枢] ❌ 反馈确认发送失败: ${err.message}`);
|
||||
logEmail('feedback-ack', email, false, err.message);
|
||||
return { sent: 0, failed: 1 };
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// CLI 主入口
|
||||
// ═══════════════════════════════════════════════
|
||||
async function main() {
|
||||
const [,, action, arg1, arg2] = process.argv;
|
||||
|
||||
if (!action) {
|
||||
console.log('📧 光湖语言世界 · 邮件通信中枢');
|
||||
console.log('用法:');
|
||||
console.log(' node email-hub.js monthly-reset — 月初重置通知');
|
||||
console.log(' node email-hub.js update-notify <描述> — 更新升级通知');
|
||||
console.log(' node email-hub.js traffic-warn <百分比> — 流量预警通知');
|
||||
console.log(' node email-hub.js security-warn <邮箱> <消息> — 安全风险提醒');
|
||||
console.log(' node email-hub.js feedback-ack <邮箱> — 反馈确认回复');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case 'monthly-reset': {
|
||||
const result = await sendMonthlyResetEmail();
|
||||
console.log(`📧 月初重置通知: ${result.sent}成功 / ${result.failed}失败`);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'update-notify': {
|
||||
if (!arg1) {
|
||||
console.error('❌ 请提供更新描述');
|
||||
process.exit(1);
|
||||
}
|
||||
const result = await sendUpdateNotifyEmail(arg1);
|
||||
console.log(`📧 更新通知: ${result.sent}成功 / ${result.failed}失败`);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'traffic-warn': {
|
||||
const pct = parseInt(arg1, 10);
|
||||
if (isNaN(pct)) {
|
||||
console.error('❌ 请提供流量使用百分比 (数字)');
|
||||
process.exit(1);
|
||||
}
|
||||
const result = await sendTrafficWarnEmail(pct);
|
||||
console.log(`📧 流量预警: ${result.sent}成功 / ${result.failed}失败`);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'security-warn': {
|
||||
if (!arg1 || !arg2) {
|
||||
console.error('❌ 请提供邮箱和消息');
|
||||
process.exit(1);
|
||||
}
|
||||
await sendSecurityWarnEmail(arg1, arg2);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'feedback-ack': {
|
||||
if (!arg1) {
|
||||
console.error('❌ 请提供邮箱');
|
||||
process.exit(1);
|
||||
}
|
||||
await sendFeedbackAckEmail(arg1);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
console.error(`❌ 未知操作: ${action}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 导出API (供其他模块调用)
|
||||
module.exports = {
|
||||
sendMonthlyResetEmail,
|
||||
sendUpdateNotifyEmail,
|
||||
sendTrafficWarnEmail,
|
||||
sendSecurityWarnEmail,
|
||||
sendFeedbackAckEmail,
|
||||
getEnabledUsers,
|
||||
sendEmail
|
||||
};
|
||||
|
||||
// CLI直接运行
|
||||
if (require.main === module) {
|
||||
main().catch(err => {
|
||||
console.error('❌ 邮件中枢异常:', err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,657 @@
|
|||
#!/usr/bin/env node
|
||||
// ═══════════════════════════════════════════════
|
||||
// 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001
|
||||
// 📜 Copyright: 国作登字-2026-A-00037559
|
||||
// ═══════════════════════════════════════════════
|
||||
// server/proxy/service/protocol-mirror.js
|
||||
// 🪞 铸渊专线 · 协议镜像引擎
|
||||
//
|
||||
// 核心概念: 镜像同步
|
||||
// - 上游协议 (Xray-core) 更新时自动检测
|
||||
// - 自动下载/备份/升级/验证/重启
|
||||
// - TLS/Reality 指纹一致性校验
|
||||
// - 对外呈现标准 Xray 行为 (伪装)
|
||||
//
|
||||
// 安全机制:
|
||||
// - LLM分析更新风险 (安全补丁/破坏性变更)
|
||||
// - 失败3次自动告警管理员
|
||||
// - 升级前备份,失败可回滚
|
||||
//
|
||||
// 运行方式: PM2 managed (zy-protocol-mirror)
|
||||
// 检查间隔: 每30分钟 (GitHub API每6小时)
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
'use strict';
|
||||
|
||||
const { execSync, execFileSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const https = require('https');
|
||||
|
||||
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 MIRROR_FILE = path.join(DATA_DIR, 'protocol-mirror-status.json');
|
||||
const XRAY_BIN = process.env.ZY_XRAY_BIN || '/usr/local/bin/xray';
|
||||
const XRAY_BACKUP_DIR = path.join(DATA_DIR, 'xray-backups');
|
||||
|
||||
const CHECK_INTERVAL = 30 * 60 * 1000; // 30分钟
|
||||
const GITHUB_CHECK_INTERVAL = 6 * 60 * 60 * 1000; // 6小时
|
||||
const MAX_FAILED_UPDATES = 3;
|
||||
const MAX_UPDATE_HISTORY = 20;
|
||||
|
||||
// ── 读取镜像状态 ─────────────────────────────
|
||||
function readMirrorStatus() {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(MIRROR_FILE, 'utf8'));
|
||||
} catch {
|
||||
return {
|
||||
installed_version: null,
|
||||
latest_version: null,
|
||||
update_available: false,
|
||||
last_github_check: null,
|
||||
last_fingerprint_check: null,
|
||||
update_history: [],
|
||||
failed_updates: 0,
|
||||
mirror_status: 'initializing',
|
||||
fingerprint: {
|
||||
tls_match: true,
|
||||
reality_match: true,
|
||||
protocol_version: 'unknown'
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ── 保存镜像状态 ─────────────────────────────
|
||||
function saveMirrorStatus(status) {
|
||||
if (!fs.existsSync(DATA_DIR)) {
|
||||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(MIRROR_FILE, JSON.stringify(status, null, 2));
|
||||
}
|
||||
|
||||
// ── 执行命令 ─────────────────────────────────
|
||||
function runCmd(cmd, timeout = 10000) {
|
||||
try {
|
||||
return { ok: true, output: execSync(cmd, { encoding: 'utf8', timeout }).trim() };
|
||||
} catch (err) {
|
||||
return { ok: false, output: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
// ── HTTPS GET 请求 ───────────────────────────
|
||||
function httpsGet(url, headers = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const urlObj = new URL(url);
|
||||
const options = {
|
||||
hostname: urlObj.hostname,
|
||||
path: urlObj.pathname + urlObj.search,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'User-Agent': 'ZY-Protocol-Mirror/1.0',
|
||||
...headers
|
||||
},
|
||||
timeout: 15000
|
||||
};
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
// 处理重定向
|
||||
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
||||
httpsGet(res.headers.location, headers).then(resolve).catch(reject);
|
||||
return;
|
||||
}
|
||||
|
||||
let data = '';
|
||||
res.on('data', (d) => { data += d; });
|
||||
res.on('end', () => {
|
||||
resolve({ statusCode: res.statusCode, body: data, headers: res.headers });
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (err) => {
|
||||
reject(new Error(`HTTPS请求失败: ${err.message}`));
|
||||
});
|
||||
req.on('timeout', () => {
|
||||
req.destroy();
|
||||
reject(new Error('HTTPS请求超时'));
|
||||
});
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// ── 获取已安装的Xray版本 ────────────────────
|
||||
function getInstalledVersion() {
|
||||
try {
|
||||
const output = execFileSync(XRAY_BIN, ['version'], {
|
||||
encoding: 'utf8',
|
||||
timeout: 5000
|
||||
}).trim();
|
||||
|
||||
// Xray version output: "Xray 1.8.x (Xray, Penetrates Everything.) ..."
|
||||
const match = output.match(/Xray\s+(\d+\.\d+\.\d+)/i);
|
||||
if (match) {
|
||||
return match[1];
|
||||
}
|
||||
console.error('[协议镜像] 无法解析Xray版本号:', output.slice(0, 100));
|
||||
return null;
|
||||
} catch (err) {
|
||||
console.error('[协议镜像] 获取Xray版本失败:', err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 查询GitHub最新版本 ──────────────────────
|
||||
async function fetchLatestRelease() {
|
||||
try {
|
||||
const resp = await httpsGet('https://api.github.com/repos/XTLS/Xray-core/releases/latest', {
|
||||
'Accept': 'application/vnd.github.v3+json'
|
||||
});
|
||||
|
||||
if (resp.statusCode !== 200) {
|
||||
console.error(`[协议镜像] GitHub API返回 ${resp.statusCode}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const release = JSON.parse(resp.body);
|
||||
const tagName = release.tag_name || '';
|
||||
// tag格式: "v1.8.x"
|
||||
const version = tagName.replace(/^v/, '');
|
||||
const body = release.body || '';
|
||||
const publishedAt = release.published_at || null;
|
||||
|
||||
// 查找Linux AMD64二进制下载链接
|
||||
let downloadUrl = null;
|
||||
if (release.assets && Array.isArray(release.assets)) {
|
||||
const linuxAsset = release.assets.find(a =>
|
||||
a.name && a.name.includes('linux') && a.name.includes('64') && a.name.endsWith('.zip')
|
||||
);
|
||||
if (linuxAsset) {
|
||||
downloadUrl = linuxAsset.browser_download_url;
|
||||
}
|
||||
}
|
||||
|
||||
return { version, body, publishedAt, downloadUrl, tagName };
|
||||
} catch (err) {
|
||||
console.error('[协议镜像] 获取GitHub release失败:', err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 版本比较 ─────────────────────────────────
|
||||
function isNewer(latest, installed) {
|
||||
if (!latest || !installed) return false;
|
||||
const latestParts = latest.split('.').map(Number);
|
||||
const installedParts = installed.split('.').map(Number);
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const l = latestParts[i] || 0;
|
||||
const c = installedParts[i] || 0;
|
||||
if (l > c) return true;
|
||||
if (l < c) return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── 协议指纹检查 ─────────────────────────────
|
||||
function checkFingerprint() {
|
||||
console.log('[协议镜像] 检查协议指纹一致性...');
|
||||
const fingerprint = {
|
||||
tls_match: true,
|
||||
reality_match: true,
|
||||
protocol_version: 'unknown'
|
||||
};
|
||||
|
||||
// 检查TLS配置是否符合标准Xray行为
|
||||
try {
|
||||
const configPath = process.env.ZY_XRAY_CONFIG || '/usr/local/etc/xray/config.json';
|
||||
if (fs.existsSync(configPath)) {
|
||||
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||
|
||||
// 验证Reality配置存在且结构完整
|
||||
const inbounds = config.inbounds || [];
|
||||
const realityInbound = inbounds.find(ib =>
|
||||
ib.streamSettings &&
|
||||
ib.streamSettings.realitySettings
|
||||
);
|
||||
|
||||
if (realityInbound) {
|
||||
const reality = realityInbound.streamSettings.realitySettings;
|
||||
fingerprint.reality_match = !!(reality.privateKey && reality.shortIds);
|
||||
if (!fingerprint.reality_match) {
|
||||
console.log('[协议镜像] ⚠️ Reality配置不完整');
|
||||
}
|
||||
}
|
||||
|
||||
// 验证TLS相关配置
|
||||
const tlsInbound = inbounds.find(ib =>
|
||||
ib.streamSettings &&
|
||||
ib.streamSettings.security === 'tls'
|
||||
);
|
||||
|
||||
if (tlsInbound) {
|
||||
const tls = tlsInbound.streamSettings.tlsSettings || {};
|
||||
fingerprint.tls_match = !!(tls.certificates && tls.certificates.length > 0);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[协议镜像] 指纹检查异常:', err.message);
|
||||
fingerprint.tls_match = false;
|
||||
}
|
||||
|
||||
// 确认Xray进程运行中的协议版本
|
||||
const installed = getInstalledVersion();
|
||||
if (installed) {
|
||||
fingerprint.protocol_version = 'matching';
|
||||
} else {
|
||||
fingerprint.protocol_version = 'unavailable';
|
||||
}
|
||||
|
||||
return fingerprint;
|
||||
}
|
||||
|
||||
// ── LLM更新风险分析 ─────────────────────────
|
||||
async function consultLLMForUpdate(releaseInfo, installedVersion) {
|
||||
let llmRouter;
|
||||
try {
|
||||
llmRouter = require('./llm-router');
|
||||
} catch {
|
||||
console.log('[协议镜像] LLM路由器未加载,跳过风险分析');
|
||||
return null;
|
||||
}
|
||||
|
||||
const prompt = `你是光湖语言世界VPN系统的协议镜像引擎。Xray-core 有新版本发布,请分析更新风险。
|
||||
|
||||
当前版本: v${installedVersion}
|
||||
最新版本: v${releaseInfo.version}
|
||||
发布时间: ${releaseInfo.publishedAt || '未知'}
|
||||
|
||||
更新日志 (前1500字):
|
||||
${(releaseInfo.body || '无更新日志').slice(0, 1500)}
|
||||
|
||||
请回答以下问题 (JSON格式):
|
||||
1. is_critical: 是否是安全修复 (true/false)
|
||||
2. changes_summary: 主要变更摘要 (一句话)
|
||||
3. risk_level: 更新风险等级 (low/medium/high)
|
||||
4. recommend_auto_update: 是否推荐自动更新 (true/false)
|
||||
5. reason: 推荐理由 (一句话)`;
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = await llmRouter.callLLM(prompt, {
|
||||
systemPrompt: '你是光湖语言世界VPN系统的协议分析AI。负责评估Xray-core更新的风险和必要性。请用JSON格式回答。',
|
||||
maxTokens: 500,
|
||||
timeout: 30000
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[协议镜像] LLM调用异常:', err.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!result || !result.content) return null;
|
||||
|
||||
// 尝试解析LLM返回的JSON
|
||||
try {
|
||||
const jsonMatch = result.content.match(/\{[\s\S]*\}/);
|
||||
if (jsonMatch) {
|
||||
return JSON.parse(jsonMatch[0]);
|
||||
}
|
||||
} catch {
|
||||
console.log('[协议镜像] LLM返回内容无法解析为JSON');
|
||||
}
|
||||
|
||||
return { raw_analysis: result.content, model: result.model };
|
||||
}
|
||||
|
||||
// ── 下载文件 ─────────────────────────────────
|
||||
function downloadFile(url, destPath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const file = fs.createWriteStream(destPath);
|
||||
|
||||
const cleanup = () => {
|
||||
file.close();
|
||||
if (fs.existsSync(destPath)) fs.unlinkSync(destPath);
|
||||
};
|
||||
|
||||
const doRequest = (reqUrl) => {
|
||||
const urlObj = new URL(reqUrl);
|
||||
const options = {
|
||||
hostname: urlObj.hostname,
|
||||
path: urlObj.pathname + urlObj.search,
|
||||
method: 'GET',
|
||||
headers: { 'User-Agent': 'ZY-Protocol-Mirror/1.0' },
|
||||
timeout: 120000
|
||||
};
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
||||
doRequest(res.headers.location);
|
||||
return;
|
||||
}
|
||||
if (res.statusCode !== 200) {
|
||||
cleanup();
|
||||
reject(new Error(`下载失败: HTTP ${res.statusCode}`));
|
||||
return;
|
||||
}
|
||||
res.pipe(file);
|
||||
file.on('finish', () => {
|
||||
file.close();
|
||||
resolve(destPath);
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (err) => {
|
||||
cleanup();
|
||||
reject(new Error(`下载错误: ${err.message}`));
|
||||
});
|
||||
req.on('timeout', () => {
|
||||
req.destroy();
|
||||
cleanup();
|
||||
reject(new Error('下载超时'));
|
||||
});
|
||||
req.end();
|
||||
};
|
||||
|
||||
doRequest(url);
|
||||
});
|
||||
}
|
||||
|
||||
// ── 执行更新 ─────────────────────────────────
|
||||
async function performUpdate() {
|
||||
const status = readMirrorStatus();
|
||||
|
||||
if (!status.update_available || !status.latest_version) {
|
||||
console.log('[协议镜像] 无可用更新');
|
||||
return { ok: false, detail: '无可用更新' };
|
||||
}
|
||||
|
||||
if (status.failed_updates >= MAX_FAILED_UPDATES) {
|
||||
console.error('[协议镜像] 已连续失败3次,需人工介入');
|
||||
sendAlertEmail(
|
||||
'🪞 协议镜像更新失败 - 需人工介入',
|
||||
`Xray-core 更新已连续失败 ${status.failed_updates} 次。\n` +
|
||||
`目标版本: v${status.latest_version}\n` +
|
||||
`当前版本: v${status.installed_version}\n` +
|
||||
`请手动检查并更新。`
|
||||
);
|
||||
return { ok: false, detail: `连续失败${status.failed_updates}次,已通知管理员` };
|
||||
}
|
||||
|
||||
console.log(`[协议镜像] 开始更新: v${status.installed_version} → v${status.latest_version}`);
|
||||
status.mirror_status = 'updating';
|
||||
saveMirrorStatus(status);
|
||||
|
||||
const updateRecord = {
|
||||
from_version: status.installed_version,
|
||||
to_version: status.latest_version,
|
||||
started_at: new Date().toISOString(),
|
||||
completed_at: null,
|
||||
success: false,
|
||||
detail: ''
|
||||
};
|
||||
|
||||
try {
|
||||
// 1. 获取下载链接
|
||||
const release = await fetchLatestRelease();
|
||||
if (!release || !release.downloadUrl) {
|
||||
throw new Error('无法获取下载链接');
|
||||
}
|
||||
|
||||
// 2. 下载新版本
|
||||
console.log('[协议镜像] 下载新版本...');
|
||||
const downloadPath = path.join(DATA_DIR, `xray-${release.version}.zip`);
|
||||
await downloadFile(release.downloadUrl, downloadPath);
|
||||
|
||||
// 3. 备份当前版本
|
||||
console.log('[协议镜像] 备份当前版本...');
|
||||
if (!fs.existsSync(XRAY_BACKUP_DIR)) {
|
||||
fs.mkdirSync(XRAY_BACKUP_DIR, { recursive: true });
|
||||
}
|
||||
const backupPath = path.join(XRAY_BACKUP_DIR, `xray-${status.installed_version}-${Date.now()}`);
|
||||
if (fs.existsSync(XRAY_BIN)) {
|
||||
fs.copyFileSync(XRAY_BIN, backupPath);
|
||||
console.log(`[协议镜像] 备份完成: ${backupPath}`);
|
||||
}
|
||||
|
||||
// 4. 解压并安装
|
||||
console.log('[协议镜像] 解压并安装...');
|
||||
const extractDir = path.join(DATA_DIR, 'xray-extract');
|
||||
if (fs.existsSync(extractDir)) {
|
||||
execFileSync('rm', ['-rf', extractDir], { timeout: 10000 });
|
||||
}
|
||||
fs.mkdirSync(extractDir, { recursive: true });
|
||||
execFileSync('unzip', ['-o', downloadPath, '-d', extractDir], {
|
||||
encoding: 'utf8',
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
const newBin = path.join(extractDir, 'xray');
|
||||
if (!fs.existsSync(newBin)) {
|
||||
throw new Error('解压后未找到xray二进制文件');
|
||||
}
|
||||
|
||||
// 设置执行权限并安装
|
||||
fs.chmodSync(newBin, 0o755);
|
||||
execFileSync('cp', [newBin, XRAY_BIN], { timeout: 10000 });
|
||||
|
||||
// 5. 验证安装
|
||||
console.log('[协议镜像] 验证安装...');
|
||||
const newVersion = getInstalledVersion();
|
||||
if (!newVersion) {
|
||||
throw new Error('安装后无法获取版本号');
|
||||
}
|
||||
console.log(`[协议镜像] 新版本已安装: v${newVersion}`);
|
||||
|
||||
// 6. 重启Xray服务
|
||||
console.log('[协议镜像] 重启Xray服务...');
|
||||
try {
|
||||
execFileSync('systemctl', ['restart', 'xray'], { encoding: 'utf8', timeout: 30000 });
|
||||
} catch (err) {
|
||||
throw new Error(`重启失败: ${err.message}`);
|
||||
}
|
||||
|
||||
// 等待2秒后验证进程
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
try {
|
||||
execFileSync('pgrep', ['-x', 'xray'], { encoding: 'utf8', timeout: 5000 });
|
||||
} catch {
|
||||
throw new Error('Xray重启后进程未运行');
|
||||
}
|
||||
|
||||
// 7. 清理下载文件
|
||||
if (fs.existsSync(downloadPath)) fs.unlinkSync(downloadPath);
|
||||
if (fs.existsSync(extractDir)) {
|
||||
execFileSync('rm', ['-rf', extractDir], { timeout: 10000 });
|
||||
}
|
||||
|
||||
// 更新成功
|
||||
updateRecord.success = true;
|
||||
updateRecord.completed_at = new Date().toISOString();
|
||||
updateRecord.detail = `成功从 v${status.installed_version} 更新到 v${newVersion}`;
|
||||
|
||||
status.installed_version = newVersion;
|
||||
status.update_available = false;
|
||||
status.failed_updates = 0;
|
||||
status.mirror_status = 'synced';
|
||||
status.update_history.push(updateRecord);
|
||||
|
||||
if (status.update_history.length > MAX_UPDATE_HISTORY) {
|
||||
status.update_history = status.update_history.slice(-MAX_UPDATE_HISTORY);
|
||||
}
|
||||
|
||||
saveMirrorStatus(status);
|
||||
console.log(`[协议镜像] ✅ 更新完成: v${newVersion}`);
|
||||
|
||||
// 发送更新成功通知
|
||||
sendAlertEmail(
|
||||
`🪞 Xray-core 已更新至 v${newVersion}`,
|
||||
getUpdateSummary()
|
||||
);
|
||||
|
||||
return { ok: true, detail: updateRecord.detail };
|
||||
} catch (err) {
|
||||
console.error('[协议镜像] ❌ 更新失败:', err.message);
|
||||
|
||||
updateRecord.completed_at = new Date().toISOString();
|
||||
updateRecord.detail = `更新失败: ${err.message}`;
|
||||
|
||||
status.failed_updates++;
|
||||
status.mirror_status = 'error';
|
||||
status.update_history.push(updateRecord);
|
||||
|
||||
if (status.update_history.length > MAX_UPDATE_HISTORY) {
|
||||
status.update_history = status.update_history.slice(-MAX_UPDATE_HISTORY);
|
||||
}
|
||||
|
||||
saveMirrorStatus(status);
|
||||
|
||||
// 连续失败达到上限时告警
|
||||
if (status.failed_updates >= MAX_FAILED_UPDATES) {
|
||||
sendAlertEmail(
|
||||
'🪞 协议镜像更新失败 - 需人工介入',
|
||||
`Xray-core 更新已连续失败 ${status.failed_updates} 次。\n` +
|
||||
`目标版本: v${status.latest_version}\n` +
|
||||
`当前版本: v${status.installed_version}\n` +
|
||||
`最后错误: ${err.message}\n` +
|
||||
`请手动检查并更新。`
|
||||
);
|
||||
}
|
||||
|
||||
return { ok: false, detail: `更新失败: ${err.message}` };
|
||||
}
|
||||
}
|
||||
|
||||
// ── 发送告警邮件 ─────────────────────────────
|
||||
function sendAlertEmail(subject, body) {
|
||||
try {
|
||||
const sendScript = path.join(__dirname, 'send-subscription.js');
|
||||
execFileSync('node', [sendScript, 'alert', `${subject}\n\n${body}`], {
|
||||
encoding: 'utf8',
|
||||
timeout: 30000
|
||||
});
|
||||
console.log('[协议镜像] 通知邮件已发送');
|
||||
} catch (err) {
|
||||
console.error('[协议镜像] 邮件发送失败:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 获取更新摘要 ─────────────────────────────
|
||||
function getUpdateSummary() {
|
||||
const status = readMirrorStatus();
|
||||
const lastUpdate = status.update_history.length > 0
|
||||
? status.update_history[status.update_history.length - 1]
|
||||
: null;
|
||||
|
||||
const lines = [
|
||||
'🪞 协议镜像引擎 · 状态报告',
|
||||
'─'.repeat(30),
|
||||
`当前版本: v${status.installed_version || '未知'}`,
|
||||
`最新版本: v${status.latest_version || '未知'}`,
|
||||
`镜像状态: ${status.mirror_status}`,
|
||||
`更新可用: ${status.update_available ? '是' : '否'}`,
|
||||
`失败次数: ${status.failed_updates}`,
|
||||
''
|
||||
];
|
||||
|
||||
if (lastUpdate) {
|
||||
lines.push('最近更新:');
|
||||
lines.push(` ${lastUpdate.from_version} → ${lastUpdate.to_version}`);
|
||||
lines.push(` 时间: ${lastUpdate.completed_at}`);
|
||||
lines.push(` 结果: ${lastUpdate.success ? '成功' : '失败'}`);
|
||||
lines.push(` 详情: ${lastUpdate.detail}`);
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
lines.push('指纹检查:');
|
||||
lines.push(` TLS匹配: ${status.fingerprint.tls_match ? '✅' : '❌'}`);
|
||||
lines.push(` Reality匹配: ${status.fingerprint.reality_match ? '✅' : '❌'}`);
|
||||
lines.push(` 协议版本: ${status.fingerprint.protocol_version}`);
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
// ── 获取镜像状态 ─────────────────────────────
|
||||
function getMirrorStatus() {
|
||||
return readMirrorStatus();
|
||||
}
|
||||
|
||||
// ── 主检查流程 ───────────────────────────────
|
||||
async function checkForUpdates() {
|
||||
console.log('[协议镜像] 开始检查...');
|
||||
const status = readMirrorStatus();
|
||||
|
||||
// 1. 获取已安装版本
|
||||
const installedVersion = getInstalledVersion();
|
||||
if (installedVersion) {
|
||||
status.installed_version = installedVersion;
|
||||
}
|
||||
|
||||
// 2. 指纹检查 (每次都执行)
|
||||
status.fingerprint = checkFingerprint();
|
||||
status.last_fingerprint_check = new Date().toISOString();
|
||||
|
||||
if (!status.fingerprint.tls_match || !status.fingerprint.reality_match) {
|
||||
console.log('[协议镜像] ⚠️ 指纹不一致,可能被探测');
|
||||
}
|
||||
|
||||
// 3. 检查GitHub最新版本 (受频率限制)
|
||||
const now = Date.now();
|
||||
const lastCheck = status.last_github_check ? new Date(status.last_github_check).getTime() : 0;
|
||||
const shouldCheckGitHub = (now - lastCheck) >= GITHUB_CHECK_INTERVAL;
|
||||
|
||||
if (shouldCheckGitHub) {
|
||||
console.log('[协议镜像] 查询GitHub最新版本...');
|
||||
const release = await fetchLatestRelease();
|
||||
|
||||
if (release && release.version) {
|
||||
status.latest_version = release.version;
|
||||
status.last_github_check = new Date().toISOString();
|
||||
|
||||
if (installedVersion && isNewer(release.version, installedVersion)) {
|
||||
console.log(`[协议镜像] 🆕 发现新版本: v${release.version} (当前: v${installedVersion})`);
|
||||
status.update_available = true;
|
||||
status.mirror_status = 'outdated';
|
||||
|
||||
// LLM风险分析
|
||||
const analysis = await consultLLMForUpdate(release, installedVersion);
|
||||
if (analysis) {
|
||||
console.log('[协议镜像] LLM分析结果:', JSON.stringify(analysis).slice(0, 200));
|
||||
}
|
||||
} else {
|
||||
status.update_available = false;
|
||||
if (status.mirror_status !== 'error') {
|
||||
status.mirror_status = 'synced';
|
||||
}
|
||||
console.log(`[协议镜像] ✅ 版本已同步: v${installedVersion || '未知'}`);
|
||||
}
|
||||
} else {
|
||||
console.log('[协议镜像] GitHub查询跳过 (API不可达或已限流)');
|
||||
}
|
||||
} else {
|
||||
const nextCheckMin = Math.round((GITHUB_CHECK_INTERVAL - (now - lastCheck)) / 60000);
|
||||
console.log(`[协议镜像] 使用缓存的GitHub结果 (下次查询: ${nextCheckMin}分钟后)`);
|
||||
}
|
||||
|
||||
saveMirrorStatus(status);
|
||||
|
||||
// 打印摘要
|
||||
console.log(`[协议镜像] 检查完成 | 版本: v${status.installed_version || '?'} | 最新: v${status.latest_version || '?'} | 状态: ${status.mirror_status}`);
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
// ── 模块导出 ─────────────────────────────────
|
||||
module.exports = { checkForUpdates, performUpdate, getMirrorStatus, getUpdateSummary };
|
||||
|
||||
// ── 启动协议镜像引擎 ────────────────────────
|
||||
console.log('🪞 光湖语言世界 · 协议镜像引擎启动');
|
||||
console.log(` 检查间隔: ${CHECK_INTERVAL / 1000}秒`);
|
||||
console.log(` GitHub API间隔: ${GITHUB_CHECK_INTERVAL / 3600000}小时`);
|
||||
console.log(` LLM路由器: 动态多模型 (通过llm-router.js)`);
|
||||
|
||||
// 立即执行一次
|
||||
checkForUpdates().catch(console.error);
|
||||
|
||||
// 定期执行
|
||||
setInterval(() => {
|
||||
checkForUpdates().catch(console.error);
|
||||
}, CHECK_INTERVAL);
|
||||
|
|
@ -424,26 +424,36 @@ rules:
|
|||
# Apple (直连)
|
||||
- DOMAIN-SUFFIX,apple.com,DIRECT
|
||||
- DOMAIN-SUFFIX,icloud.com,DIRECT
|
||||
- DOMAIN-SUFFIX,mzstatic.com,DIRECT
|
||||
- DOMAIN-SUFFIX,apple-cloudkit.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,staticfile.org,DIRECT
|
||||
|
||||
# 国内直连
|
||||
# 国内直连 (∞ 智能分流 · 用户切回国内网时VPN休眠)
|
||||
- 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,aliyuncs.com,DIRECT
|
||||
- DOMAIN-SUFFIX,jd.com,DIRECT
|
||||
- DOMAIN-SUFFIX,qq.com,DIRECT
|
||||
- DOMAIN-SUFFIX,tencent.com,DIRECT
|
||||
- DOMAIN-SUFFIX,weixin.qq.com,DIRECT
|
||||
- DOMAIN-SUFFIX,wechat.com,DIRECT
|
||||
- DOMAIN-SUFFIX,bilibili.com,DIRECT
|
||||
- DOMAIN-SUFFIX,bilivideo.com,DIRECT
|
||||
- DOMAIN-SUFFIX,baidu.com,DIRECT
|
||||
- DOMAIN-SUFFIX,bdstatic.com,DIRECT
|
||||
- DOMAIN-SUFFIX,zhihu.com,DIRECT
|
||||
- DOMAIN-SUFFIX,douyin.com,DIRECT
|
||||
- DOMAIN-SUFFIX,bytedance.com,DIRECT
|
||||
- DOMAIN-SUFFIX,toutiao.com,DIRECT
|
||||
- DOMAIN-SUFFIX,weibo.com,DIRECT
|
||||
- DOMAIN-SUFFIX,163.com,DIRECT
|
||||
- DOMAIN-SUFFIX,126.com,DIRECT
|
||||
|
|
@ -454,6 +464,20 @@ rules:
|
|||
- DOMAIN-SUFFIX,meituan.com,DIRECT
|
||||
- DOMAIN-SUFFIX,dianping.com,DIRECT
|
||||
- DOMAIN-SUFFIX,pinduoduo.com,DIRECT
|
||||
- DOMAIN-SUFFIX,suning.com,DIRECT
|
||||
- DOMAIN-SUFFIX,csdn.net,DIRECT
|
||||
- DOMAIN-SUFFIX,cnblogs.com,DIRECT
|
||||
- DOMAIN-SUFFIX,gitee.com,DIRECT
|
||||
- DOMAIN-SUFFIX,jianshu.com,DIRECT
|
||||
- DOMAIN-SUFFIX,douban.com,DIRECT
|
||||
- DOMAIN-SUFFIX,kuaishou.com,DIRECT
|
||||
- DOMAIN-SUFFIX,ctrip.com,DIRECT
|
||||
- DOMAIN-SUFFIX,ele.me,DIRECT
|
||||
- DOMAIN-SUFFIX,netease.com,DIRECT
|
||||
- DOMAIN-SUFFIX,iqiyi.com,DIRECT
|
||||
- DOMAIN-SUFFIX,youku.com,DIRECT
|
||||
- DOMAIN-SUFFIX,dingtalk.com,DIRECT
|
||||
- DOMAIN-SUFFIX,feishu.cn,DIRECT
|
||||
|
||||
# 局域网直连
|
||||
- IP-CIDR,192.168.0.0/16,DIRECT
|
||||
|
|
|
|||
Loading…
Reference in New Issue