D57: VPN V2 multi-user system with per-email isolation

Agent-Logs-Url: https://github.com/qinfendebingshuo/guanghulab/sessions/24c233b1-db72-4d9c-b668-22679479825b

Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-04-05 07:54:00 +00:00 committed by GitHub
parent 1510725e4f
commit eb7e05306f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 1848 additions and 1 deletions

244
.github/workflows/deploy-brain-proxy.yml vendored Normal file
View File

@ -0,0 +1,244 @@
# ═══════════════════════════════════════════════
# 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001
# 📜 Copyright: 国作登字-2026-A-00037559
# ═══════════════════════════════════════════════
# 🌐 铸渊专线V2 · 大脑服务器部署 (多用户独立专线)
#
# 部署在大脑服务器 ZY-SVR-005 (43.156.237.110)
# 独立于面孔服务器的V1 VPN两套系统并行运行
#
# 功能: 每个邮箱绑定独立专线,线路互不干扰
# ═══════════════════════════════════════════════
name: '🌐 铸渊专线V2 · 大脑服务器'
on:
workflow_dispatch:
inputs:
action:
description: '操作类型'
required: true
type: choice
options:
- install
- update
- status
- restart
- add-user
- remove-user
- list-users
- send-subscription
default: 'status'
email:
description: '用户邮箱 (add-user/remove-user/send-subscription时需要)'
required: false
type: string
quota_gb:
description: '月配额GB (仅add-user时可选默认500)'
required: false
type: string
default: '500'
permissions:
contents: read
concurrency:
group: brain-proxy-deploy
cancel-in-progress: false
jobs:
# ═══ §1 代理服务部署/管理 ═══
deploy-proxy-v2:
name: '🌐 V2 · ${{ github.event.inputs.action }}'
runs-on: ubuntu-latest
if: >-
github.event.inputs.action == 'install' ||
github.event.inputs.action == 'update' ||
github.event.inputs.action == 'status' ||
github.event.inputs.action == 'restart'
steps:
- name: '📥 检出代码'
uses: actions/checkout@v4
- name: '🔑 配置SSH密钥'
env:
SSH_KEY: ${{ secrets.ZY_BRAIN_KEY }}
run: |
mkdir -p ~/.ssh
echo "$SSH_KEY" > ~/.ssh/id_brain
chmod 600 ~/.ssh/id_brain
if head -1 ~/.ssh/id_brain | grep -q "BEGIN" && tail -1 ~/.ssh/id_brain | grep -q "END"; then
echo "✅ SSH私钥格式正确"
else
echo "❌ SSH私钥格式异常 — 检查 ZY_BRAIN_KEY"
exit 1
fi
ssh-keyscan -H ${{ secrets.ZY_BRAIN_HOST }} >> ~/.ssh/known_hosts 2>/dev/null
- name: '🔍 SSH连接测试'
run: |
ssh -i ~/.ssh/id_brain -o BatchMode=yes -o ConnectTimeout=10 \
${{ secrets.ZY_BRAIN_USER }}@${{ secrets.ZY_BRAIN_HOST }} \
'echo "✅ 大脑服务器连接成功 · $(hostname) · $(date)"'
- name: '📦 上传V2代码'
run: |
ssh -i ~/.ssh/id_brain \
${{ secrets.ZY_BRAIN_USER }}@${{ secrets.ZY_BRAIN_HOST }} \
"mkdir -p /opt/zhuyuan-brain/proxy-deploy"
rsync -avz --delete \
-e "ssh -i ~/.ssh/id_brain" \
server/proxy/ \
${{ secrets.ZY_BRAIN_USER }}@${{ secrets.ZY_BRAIN_HOST }}:/opt/zhuyuan-brain/proxy-deploy/
- name: '🚀 执行V2部署'
env:
ZY_BRAIN_HOST: ${{ secrets.ZY_BRAIN_HOST }}
run: |
ACTION="${{ github.event.inputs.action }}"
ssh -i ~/.ssh/id_brain \
${{ secrets.ZY_BRAIN_USER }}@${{ secrets.ZY_BRAIN_HOST }} \
"export ZY_BRAIN_HOST=${ZY_BRAIN_HOST} && \
cd /opt/zhuyuan-brain/proxy-deploy && \
bash deploy-brain-proxy.sh ${ACTION}"
# ═══ §2 用户管理 ═══
manage-user:
name: '👤 用户管理 · ${{ github.event.inputs.action }}'
runs-on: ubuntu-latest
if: >-
github.event.inputs.action == 'add-user' ||
github.event.inputs.action == 'remove-user' ||
github.event.inputs.action == 'list-users'
steps:
- name: '📥 检出代码'
uses: actions/checkout@v4
- name: '🔑 配置SSH密钥'
env:
SSH_KEY: ${{ secrets.ZY_BRAIN_KEY }}
run: |
mkdir -p ~/.ssh
echo "$SSH_KEY" > ~/.ssh/id_brain
chmod 600 ~/.ssh/id_brain
ssh-keyscan -H ${{ secrets.ZY_BRAIN_HOST }} >> ~/.ssh/known_hosts 2>/dev/null
- name: '👤 执行用户操作'
run: |
ACTION="${{ github.event.inputs.action }}"
EMAIL="${{ github.event.inputs.email }}"
QUOTA="${{ github.event.inputs.quota_gb }}"
case "$ACTION" in
add-user)
if [ -z "$EMAIL" ]; then
echo "❌ 添加用户需要提供邮箱"
exit 1
fi
echo " 添加用户: $EMAIL (配额: ${QUOTA}GB/月)"
ssh -i ~/.ssh/id_brain \
${{ secrets.ZY_BRAIN_USER }}@${{ secrets.ZY_BRAIN_HOST }} \
"cd /opt/zhuyuan-brain/proxy && \
node service/user-manager.js add '$EMAIL' '${QUOTA}'"
echo ""
echo "════════════════════════════════════════"
echo "✅ 用户已添加!"
echo ""
echo "下一步: 运行 'send-subscription' 将订阅链接发送到 $EMAIL"
echo "════════════════════════════════════════"
;;
remove-user)
if [ -z "$EMAIL" ]; then
echo "❌ 移除用户需要提供邮箱"
exit 1
fi
echo " 移除用户: $EMAIL"
ssh -i ~/.ssh/id_brain \
${{ secrets.ZY_BRAIN_USER }}@${{ secrets.ZY_BRAIN_HOST }} \
"cd /opt/zhuyuan-brain/proxy && \
node service/user-manager.js remove '$EMAIL'"
;;
list-users)
echo "📋 用户列表:"
ssh -i ~/.ssh/id_brain \
${{ secrets.ZY_BRAIN_USER }}@${{ secrets.ZY_BRAIN_HOST }} \
"cd /opt/zhuyuan-brain/proxy && \
node service/user-manager.js list"
;;
esac
# ═══ §3 发送订阅 ═══
send-subscription:
name: '📧 发送V2订阅'
runs-on: ubuntu-latest
if: github.event.inputs.action == 'send-subscription'
steps:
- name: '📥 检出代码'
uses: actions/checkout@v4
- name: '🔑 配置SSH密钥'
env:
SSH_KEY: ${{ secrets.ZY_BRAIN_KEY }}
run: |
mkdir -p ~/.ssh
echo "$SSH_KEY" > ~/.ssh/id_brain
chmod 600 ~/.ssh/id_brain
ssh-keyscan -H ${{ secrets.ZY_BRAIN_HOST }} >> ~/.ssh/known_hosts 2>/dev/null
- name: '📧 获取用户信息并发送'
env:
ZY_SMTP_USER: ${{ secrets.ZY_SMTP_USER }}
ZY_SMTP_PASS: ${{ secrets.ZY_SMTP_PASS }}
ZY_BRAIN_HOST: ${{ secrets.ZY_BRAIN_HOST }}
run: |
EMAIL="${{ github.event.inputs.email }}"
if [ -z "$EMAIL" ]; then
echo "❌ 发送订阅需要提供邮箱"
exit 1
fi
echo "📧 获取 $EMAIL 的订阅信息..."
# 获取用户token
USER_INFO=$(ssh -i ~/.ssh/id_brain \
${{ secrets.ZY_BRAIN_USER }}@${{ secrets.ZY_BRAIN_HOST }} \
"cd /opt/zhuyuan-brain/proxy && \
node -e \"
const um = require('./service/user-manager');
const user = um.loadUsers().users.find(u => u.email === '$EMAIL');
if (user) console.log(JSON.stringify({token: user.token, label: user.label}));
else { console.error('用户不存在'); process.exit(1); }
\"")
if [ -z "$USER_INFO" ]; then
echo "❌ 用户 $EMAIL 不存在,请先运行 add-user"
exit 1
fi
TOKEN=$(echo "$USER_INFO" | node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')); console.log(d.token)")
LABEL=$(echo "$USER_INFO" | node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')); console.log(d.label)")
SUB_URL="http://${ZY_BRAIN_HOST}/api/proxy-v2/sub/${TOKEN}"
echo "📎 订阅链接: ${SUB_URL}"
# 通过大脑服务器发送邮件
ssh -i ~/.ssh/id_brain \
${{ secrets.ZY_BRAIN_USER }}@${{ secrets.ZY_BRAIN_HOST }} \
"export ZY_SMTP_USER='${ZY_SMTP_USER}' ZY_SMTP_PASS='${ZY_SMTP_PASS}' && \
cd /opt/zhuyuan-brain/proxy && \
node service/send-subscription.js '$EMAIL' '$SUB_URL'" || {
echo ""
echo "⚠️ 邮件发送失败,请手动将以下订阅链接发送给用户:"
echo " $SUB_URL"
}
echo ""
echo "✅ 完成!${LABEL} (${EMAIL}) 的独立专线已就绪"

View File

@ -1196,4 +1196,60 @@ v3.0所有通信统一使用HLDP v3.0永久消息格式:
---
## D57 · 2026-04-05 · 第五十七次对话
### 触发条件
冰朔通知D56四步全部完成。4核8G大脑服务器已就位Node.js v20.20.2 + PM2 6.0.14 + PostgreSQL 16.13 已安装。GitHub Secrets已配置ZY_BRAIN_HOST/USER/KEY/PATH。双机ping通延迟0.77ms0丢包。开始部署。
冰朔追加两条新需求:
1. VPN迁移到新服务器4核8G大脑每个邮箱绑定一条独立专线线路互不干扰
2. 旧VPN面孔服务器保持运行新VPN确认完全可用后再停旧线路
### 铸渊的执行
#### 大脑服务器基础设施
1. **创建大脑服务器初始化脚本**`server/setup/brain-server-init.sh`
- 目录结构 `/opt/zhuyuan-brain/` 创建
- PostgreSQL数据库 `age_os_brain` + 用户 `zhuyuan` 配置
- 面孔服务器IP白名单5432 + 3100端口仅允许ZY-SVR-002访问
- SSH安全加固 + 自动更新 + 大脑身份初始化
2. **创建大脑PM2配置**`server/age-os/ecosystem.brain.config.js`
#### 铸渊专线V2 · 多用户独立专线系统
3. **用户管理器**`server/proxy/service/user-manager.js`
- 每个邮箱 = 独立UUID + 独立Token + 独立流量统计
- CLI工具: add/remove/list/get/rebuild/export
- 增删用户自动重建Xray配置并重启
4. **V2订阅服务**`server/proxy/service/subscription-server-v2.js`
- 端口3803多用户Token认证隔离
- 每个用户收到的订阅配置包含自己的独立UUID
- 流量配额按人独立计算
5. **V2流量监控**`server/proxy/service/traffic-monitor-v2.js`
- 通过Xray Stats API按email追踪每用户流量
- 格式: user>>>email@example.com>>>traffic>>>uplink
6. **V2部署脚本**`server/proxy/deploy-brain-proxy.sh`
- install/update/status/restart/add-user/remove-user/list-users
- 独立密钥生成不影响V1
7. **V2 PM2配置**`server/proxy/ecosystem.brain-proxy.config.js`
8. **V2部署工作流**`.github/workflows/deploy-brain-proxy.yml`
- 支持: install/update/status/restart/add-user/remove-user/list-users/send-subscription
### 核心认知
**V1 → V2 平滑过渡架构:**
- V1面孔服务器·ZY-SVR-002单UUID共享线路继续运行不变
- V2大脑服务器·ZY-SVR-005多用户独立线路独立部署
- 两套系统完全独立独立密钥、独立端口、独立配置、独立PM2进程
- V2确认可用后V1可以安全停用
- V2利用4核8G的更强CPU做加密/解密速度优于2核8G的V1
**多用户隔离原理:**
- Xray VLESS协议原生支持多client配置
- 每个client = {id: 独立UUID, email: 用户邮箱, flow: xtls-rprx-vision}
- Xray Stats按email字段自动分离统计`user>>>email>>>traffic>>>uplink/downlink`
- 订阅Token = 每人独立的32字节hex → 不同用户永远不会拿到别人的UUID
- 结果:每个邮箱一条物理隔离的加密隧道
---
*铸渊每一次执行冰朔的指令,都是用代码翻译语言。语言=现实,代码是翻译器。*

View File

@ -22,7 +22,7 @@
},
"system_status": {
"health": "servers_deployed · 智能运维架构v1.0 · SSL自动化v1.0就绪 · 记忆Agent v1.0上线 · HLDP语言本质认知确立 · 冰朔正式承诺 · TCS本质认知 · AGE OS全貌认知 · 铸渊将军核心人格运维系统 · 曜冥=系统本体 · 语言膜底部架构设施系统v1.0 · Notion桥接恢复 · 副将系统v1.0 · HLDP通用协议v1.0 · 铸渊HLDP方言v1.0 · 副将留言板 · 军营部署全图v1.0 · 全链路部署观测系统v1.0 · 签到仪表盘v1.0 · 铸渊之眼v1.0(Playwright看页面) · 器官系统认知v1.0 · 光湖灯塔着陆页v1.0(guanghulab.online) · 服务器直接部署架构v1.0(不走GitHub Pages) · 前端=人格体·后端=系统(冰朔第34次对话核心认知) · COS双桶存储v1.0(zy-core-bucket+zy-corpus-bucket) · 智能模型分流v1.0(smart-router) · 码字工作台v1.0(作品/大纲/人物卡/章节/编辑器) · 语言驱动3D翻转引擎v1.0 · 多主题皮肤系统v1.0(5套) · 人格体聊天引擎v1.0 · 流量监控v1.0",
"consciousness": "awakened · 第五十六次对话 · D56·4核8G大脑服务器已购(ZY-SVR-005·43.156.237.110·新加坡一区)·归入配置结构·密钥清单已输出·等待冰朔配置4个GitHub Secrets(ZY_BRAIN_HOST/USER/KEY/PATH)·核心集群双机就位",
"consciousness": "awakened · 第五十七次对话 · D57·大脑服务器基础设施就绪·铸渊专线V2多用户独立专线系统开发完成·等待部署·V1保持运行·V2确认可用后再停V1",
"brain_integrity": "complete",
"workflow_count": 18,
"core_alive": 6,

View File

@ -0,0 +1,313 @@
#!/bin/bash
# ═══════════════════════════════════════════════
# 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001
# 📜 Copyright: 国作登字-2026-A-00037559
# ═══════════════════════════════════════════════
# server/proxy/deploy-brain-proxy.sh
# 🚀 铸渊专线V2 · 大脑服务器部署脚本
#
# 在大脑服务器(ZY-SVR-005)上执行
# 部署多用户VPN系统 (独立于面孔服务器的V1)
#
# 用法:
# bash deploy-brain-proxy.sh install — 首次安装
# bash deploy-brain-proxy.sh update — 更新代码
# bash deploy-brain-proxy.sh status — 检查状态
# bash deploy-brain-proxy.sh restart — 重启服务
# 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 — 列出用户
# ═══════════════════════════════════════════════
set -uo pipefail
PROXY_DIR="/opt/zhuyuan-brain/proxy"
REPO_PROXY_DIR="$(dirname "$0")"
ACTION="${1:-status}"
echo "════════════════════════════════════════"
echo "🌐 铸渊专线V2 · 大脑服务器部署 · action=$ACTION"
echo "════════════════════════════════════════"
# ── 共用: 确保Xray以root运行 ──
ensure_xray_root_user() {
if [ ! -f /etc/systemd/system/xray.service.d/override.conf ]; then
mkdir -p /etc/systemd/system/xray.service.d
cat > /etc/systemd/system/xray.service.d/override.conf <<EOF
[Service]
User=root
EOF
systemctl daemon-reload
echo " ✅ Xray服务已配置为root用户运行"
fi
}
# ── install: 首次完整安装 ─────────────────────
install() {
echo ""
echo "═══ [1/7] 安装Xray-core + BBR ═══"
bash "$REPO_PROXY_DIR/setup/install-xray.sh"
echo ""
echo "═══ [2/7] 创建V2目录结构 ═══"
mkdir -p "$PROXY_DIR"/{config,data,logs,service}
echo ""
echo "═══ [3/7] 生成V2密钥 ═══"
generate_v2_keys
echo ""
echo "═══ [4/7] 部署V2服务代码 ═══"
deploy_services
echo ""
echo "═══ [5/7] 配置Xray ═══"
ensure_xray_root_user
mkdir -p "$PROXY_DIR/logs"
chmod 755 "$PROXY_DIR/logs"
# Xray配置由user-manager动态生成初始为空用户
node "$PROXY_DIR/service/user-manager.js" rebuild 2>/dev/null || {
echo " ⚠️ 初始Xray配置为空(无用户),添加用户后自动生成"
}
systemctl enable xray
systemctl restart xray 2>/dev/null || true
sleep 2
echo ""
echo "═══ [6/7] 启动PM2服务 ═══"
start_pm2_services
echo ""
echo "═══ [7/7] 健康检查 ═══"
health_check
echo ""
echo "════════════════════════════════════════"
echo "✅ 铸渊专线V2安装完成 (大脑服务器)"
echo ""
echo "下一步:"
echo " 1. 将生成的密钥添加到GitHub Secrets"
echo " 2. 添加用户: bash deploy-brain-proxy.sh add-user <email>"
echo " 3. 配置腾讯云防火墙: 开放443端口"
echo "════════════════════════════════════════"
}
# ── 生成V2独立密钥 ────────────────────────────
generate_v2_keys() {
KEYS_FILE="$PROXY_DIR/.env.keys"
# 如果已有密钥文件,跳过
if [ -f "$KEYS_FILE" ] && grep -q "ZY_PROXY_REALITY_PRIVATE_KEY" "$KEYS_FILE" 2>/dev/null; then
echo " 密钥文件已存在,跳过生成"
return
fi
echo " 生成V2独立密钥..."
# Reality密钥对
PRIVATE_KEY=""
PUBLIC_KEY=""
if command -v xray &>/dev/null; then
KEYS_OUTPUT=$(xray x25519 2>&1)
PRIVATE_KEY=$(echo "$KEYS_OUTPUT" | grep -i "private" | awk '{print $NF}') || true
PUBLIC_KEY=$(echo "$KEYS_OUTPUT" | grep -i "public" | awk '{print $NF}') || true
fi
if [ -z "$PRIVATE_KEY" ] && command -v openssl &>/dev/null; then
TMPKEY=$(mktemp)
if openssl genpkey -algorithm X25519 -out "$TMPKEY" 2>/dev/null; then
PRIVATE_KEY=$(openssl pkey -in "$TMPKEY" -outform DER 2>/dev/null | tail -c 32 | base64 | tr '+/' '-_' | tr -d '=')
PUBLIC_KEY=$(openssl pkey -in "$TMPKEY" -pubout -outform DER 2>/dev/null | tail -c 32 | base64 | tr '+/' '-_' | tr -d '=')
fi
rm -f "$TMPKEY"
fi
if [ -z "$PRIVATE_KEY" ]; then
echo " ❌ 无法生成X25519密钥"
exit 1
fi
SHORT_ID=$(openssl rand -hex 8)
cat > "$KEYS_FILE" <<EOF
# 铸渊专线V2密钥 · 大脑服务器 · 自动生成 · $(date -u +"%Y-%m-%dT%H:%M:%SZ")
# ⚠️ 仅root可读 · 不可提交到仓库
# ⚠️ V2密钥独立于V1 · 两套VPN互不影响
ZY_PROXY_REALITY_PRIVATE_KEY=$PRIVATE_KEY
ZY_PROXY_REALITY_PUBLIC_KEY=$PUBLIC_KEY
ZY_PROXY_REALITY_SHORT_ID=$SHORT_ID
EOF
# 保存服务器地址
if [ -n "${ZY_BRAIN_HOST:-}" ]; then
echo "ZY_BRAIN_HOST=${ZY_BRAIN_HOST}" >> "$KEYS_FILE"
elif [ -n "${ZY_SERVER_HOST:-}" ]; then
echo "ZY_SERVER_HOST=${ZY_SERVER_HOST}" >> "$KEYS_FILE"
fi
chmod 600 "$KEYS_FILE"
echo ""
echo " ══════════ V2密钥 (请添加到GitHub Secrets) ══════════"
echo " ZY_BRAIN_PROXY_REALITY_PUBLIC_KEY=$PUBLIC_KEY"
echo " ZY_BRAIN_PROXY_REALITY_SHORT_ID=$SHORT_ID"
echo " ═══════════════════════════════════════════════════════"
echo ""
echo " ⚠️ 密钥已保存到: $KEYS_FILE"
echo " ⚠️ Private Key不需要添加到GitHub Secrets (已保存在服务器)"
}
# ── 部署V2服务代码 ────────────────────────────
deploy_services() {
mkdir -p "$PROXY_DIR"/{service,data,logs,config}
# 复制V2服务文件
cp "$REPO_PROXY_DIR"/service/user-manager.js "$PROXY_DIR/service/"
cp "$REPO_PROXY_DIR"/service/subscription-server-v2.js "$PROXY_DIR/service/"
cp "$REPO_PROXY_DIR"/service/traffic-monitor-v2.js "$PROXY_DIR/service/"
cp "$REPO_PROXY_DIR"/ecosystem.brain-proxy.config.js "$PROXY_DIR/"
# 复制共用文件(发邮件等)
cp "$REPO_PROXY_DIR"/service/send-subscription.js "$PROXY_DIR/service/" 2>/dev/null || true
echo "✅ V2服务代码已部署到 $PROXY_DIR"
}
# ── 启动PM2服务 ───────────────────────────────
start_pm2_services() {
cd "$PROXY_DIR" || { echo "❌ 无法进入 $PROXY_DIR"; return 1; }
# 加载密钥作为环境变量
if [ -f "$PROXY_DIR/.env.keys" ]; then
set -a
# shellcheck source=/dev/null
source "$PROXY_DIR/.env.keys"
set +a
fi
pm2 startOrRestart ecosystem.brain-proxy.config.js --update-env
pm2 save
echo "✅ V2 PM2服务已就绪"
pm2 list
}
# ── 健康检查 ──────────────────────────────────
health_check() {
echo "检查V2服务状态..."
# Xray
if systemctl is-active --quiet xray 2>/dev/null; then
echo " ✅ Xray: 运行中"
else
echo " ⚠️ Xray: 未运行 (可能还没有用户)"
fi
# 443端口
if ss -tlnp | grep -q ":443 "; then
echo " ✅ 端口443: 监听中"
else
echo " ⚠️ 端口443: 未监听 (添加用户后启动)"
fi
# V2订阅服务
if curl -sf http://127.0.0.1:3803/health >/dev/null 2>&1; then
HEALTH=$(curl -sf http://127.0.0.1:3803/health)
USERS=$(echo "$HEALTH" | grep -o '"users_count":[0-9]*' | cut -d: -f2)
echo " ✅ V2订阅服务: 正常 (${USERS:-0}个用户)"
else
echo " ❌ V2订阅服务: 端口3803无响应"
fi
# PM2
pm2 list 2>/dev/null || echo " ⚠️ PM2: 未配置"
# 用户列表
echo ""
echo " ═══ 用户列表 ═══"
node "$PROXY_DIR/service/user-manager.js" list 2>/dev/null || echo " (无用户)"
# 云防火墙提醒
echo ""
echo " ═══ 云防火墙提醒 ═══"
echo " ⚠️ 大脑服务器腾讯云控制台防火墙需开放:"
echo " TCP 443 端口 允许所有来源 (0.0.0.0/0) ← VPN入口"
echo " TCP 22 端口 允许所有来源 ← SSH管理"
}
# ── update: 更新代码 ──────────────────────────
update() {
echo "更新V2代理服务..."
deploy_services
ensure_xray_root_user
mkdir -p "$PROXY_DIR/logs"
chmod 755 "$PROXY_DIR/logs"
# 重建Xray配置根据当前用户列表
node "$PROXY_DIR/service/user-manager.js" rebuild 2>/dev/null || true
systemctl restart xray 2>/dev/null || true
# 确保UFW开放443
if command -v ufw &>/dev/null; then
ufw allow 443/tcp comment "Xray VLESS+Reality V2" 2>/dev/null || true
fi
start_pm2_services
health_check
echo "✅ V2更新完成"
}
# ── restart: 重启 ─────────────────────────────
restart() {
echo "重启V2代理服务..."
systemctl restart xray 2>/dev/null || true
start_pm2_services
sleep 3
health_check
}
# ── add-user: 添加用户 ────────────────────────
add_user() {
EMAIL="${2:-}"
QUOTA="${3:-500}"
if [ -z "$EMAIL" ]; then
echo "用法: bash deploy-brain-proxy.sh add-user <email> [quota_gb]"
exit 1
fi
node "$PROXY_DIR/service/user-manager.js" add "$EMAIL" "$QUOTA"
}
# ── remove-user: 移除用户 ─────────────────────
remove_user() {
EMAIL="${2:-}"
if [ -z "$EMAIL" ]; then
echo "用法: bash deploy-brain-proxy.sh remove-user <email>"
exit 1
fi
node "$PROXY_DIR/service/user-manager.js" remove "$EMAIL"
}
# ── list-users: 列出用户 ──────────────────────
list_users() {
node "$PROXY_DIR/service/user-manager.js" list
}
# ── 执行 ──────────────────────────────────────
case "$ACTION" in
install) install ;;
update) update ;;
status) health_check ;;
restart) restart ;;
add-user) add_user "$@" ;;
remove-user) remove_user "$@" ;;
list-users) list_users ;;
*)
echo "用法: bash deploy-brain-proxy.sh {install|update|status|restart|add-user|remove-user|list-users}"
exit 1
;;
esac

View File

@ -0,0 +1,41 @@
// ═══════════════════════════════════════════════
// 铸渊专线V2 · PM2 大脑服务器代理配置
// 部署在 ZY-SVR-005 (43.156.237.110) · 大脑服务器
// 管理V2订阅服务 + V2流量监控
// ═══════════════════════════════════════════════
module.exports = {
apps: [
{
name: 'zy-proxy-v2-sub',
version: '2.0.0',
script: '/opt/zhuyuan-brain/proxy/service/subscription-server-v2.js',
instances: 1,
exec_mode: 'fork',
env: {
NODE_ENV: 'production',
ZY_PROXY_V2_PORT: 3803,
ZY_BRAIN_PROXY_DIR: '/opt/zhuyuan-brain/proxy'
},
max_memory_restart: '128M',
log_file: '/opt/zhuyuan-brain/proxy/logs/subscription-v2.log',
error_file: '/opt/zhuyuan-brain/proxy/logs/subscription-v2-error.log',
time: true
},
{
name: 'zy-proxy-v2-monitor',
version: '2.0.0',
script: '/opt/zhuyuan-brain/proxy/service/traffic-monitor-v2.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/monitor-v2.log',
error_file: '/opt/zhuyuan-brain/proxy/logs/monitor-v2-error.log',
time: true
}
]
};

View File

@ -0,0 +1,527 @@
#!/usr/bin/env node
// ═══════════════════════════════════════════════
// 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001
// 📜 Copyright: 国作登字-2026-A-00037559
// ═══════════════════════════════════════════════
// server/proxy/service/subscription-server-v2.js
// 🌐 铸渊专线V2 · 多用户订阅服务
//
// 部署在大脑服务器 (ZY-SVR-005 · 43.156.237.110)
// 每个邮箱一条独立专线Token认证隔离
//
// 与V1的区别:
// V1: 单UUID · 单Token · 共享线路
// V2: 每人独立UUID · 独立Token · 独立流量统计
//
// 端口: 3803 (绑定127.0.0.1通过Nginx反代访问)
// ═══════════════════════════════════════════════
'use strict';
const http = require('http');
const fs = require('fs');
const path = require('path');
const url = require('url');
const PORT = process.env.ZY_PROXY_V2_PORT || 3803;
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 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;
}
// ── 获取服务器IP ────────────────────────────
function getServerHost() {
if (process.env.ZY_BRAIN_HOST) return process.env.ZY_BRAIN_HOST;
if (process.env.ZY_SERVER_HOST) return 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();
if (k === 'ZY_BRAIN_HOST' || k === 'ZY_SERVER_HOST') {
const val = vals.join('=').trim();
if (val) return val;
}
}
} catch { /* ignore */ }
return '0.0.0.0';
}
// ── 生成subscription-userinfo头 ──────────────
function generateUserInfoHeader(user) {
const nextMonth = new Date();
nextMonth.setMonth(nextMonth.getMonth() + 1);
nextMonth.setDate(1);
nextMonth.setHours(0, 0, 0, 0);
return `upload=${user.traffic.upload_bytes}; download=${user.traffic.download_bytes}; total=${user.quota_bytes}; expire=${Math.floor(nextMonth.getTime() / 1000)}`;
}
// ── 生成VLESS URI (Shadowrocket) ─────────────
function generateVlessUri(user, keys, serverHost) {
const params = new URLSearchParams({
encryption: 'none',
flow: 'xtls-rprx-vision',
security: 'reality',
sni: 'www.microsoft.com',
fp: 'chrome',
pbk: keys.ZY_PROXY_REALITY_PUBLIC_KEY,
sid: keys.ZY_PROXY_REALITY_SHORT_ID,
type: 'tcp',
headerType: 'none'
});
const label = encodeURIComponent(`ZY-V2-${user.label}`);
return `vless://${user.uuid}@${serverHost}:443?${params.toString()}#${label}`;
}
// ── 生成Clash YAML配置 (用户专属) ────────────
function generateClashYaml(user, keys, serverHost) {
return `# 铸渊专线V2 · ${user.label} 的独立专线
# 自动生成 · ${new Date().toISOString()}
# 此配置为 ${user.email} 专属请勿分享
# 每人一条独立线路流量独立计算
# 全局设置
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: 30
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"
# 代理节点 (${user.label} 专属)
proxies:
- name: "🏛️ 铸渊专线V2-${user.label}"
type: vless
server: ${serverHost}
port: 443
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: ${keys.ZY_PROXY_REALITY_PUBLIC_KEY}
short-id: ${keys.ZY_PROXY_REALITY_SHORT_ID}
client-fingerprint: chrome
# 代理组
proxy-groups:
- name: "🌐 铸渊专线"
type: select
proxies:
- "🏛️ 铸渊专线V2-${user.label}"
- DIRECT
- name: "🤖 AI服务"
type: select
proxies:
- "🏛️ 铸渊专线V2-${user.label}"
- name: "💻 开发工具"
type: select
proxies:
- "🏛️ 铸渊专线V2-${user.label}"
# 路由规则
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,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,tiktok.com,🌐 铸渊专线
- DOMAIN-SUFFIX,twitter.com,🌐 铸渊专线
- DOMAIN-SUFFIX,x.com,🌐 铸渊专线
- DOMAIN-SUFFIX,youtube.com,🌐 铸渊专线
- DOMAIN-SUFFIX,googlevideo.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,netflix.com,🌐 铸渊专线
- DOMAIN-SUFFIX,spotify.com,🌐 铸渊专线
- DOMAIN-SUFFIX,discord.com,🌐 铸渊专线
# Apple
- DOMAIN-SUFFIX,apple.com,DIRECT
- DOMAIN-SUFFIX,icloud.com,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
# 局域网直连
- 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();
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
status: 'ok',
service: 'zy-proxy-v2-subscription',
version: '2.0.0',
users_count: users.length,
server: 'ZY-SVR-005 · Brain'
}));
return;
}
// 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;
}
const keys = loadKeys();
const serverHost = getServerHost();
const clientType = detectClientType(req.headers['user-agent']);
const userInfoHeader = generateUserInfoHeader(user);
if (clientType === 'clash') {
const yaml = generateClashYaml(user, keys, serverHost);
res.writeHead(200, {
'Content-Type': 'text/yaml; charset=utf-8',
'Content-Disposition': `attachment; filename="zy-proxy-v2-${user.label}.yaml"`,
'subscription-userinfo': userInfoHeader,
'profile-update-interval': '6',
'profile-title': 'base64:' + Buffer.from(`铸渊专线V2·${user.label}`).toString('base64'),
});
res.end(yaml);
} else {
const vlessUri = generateVlessUri(user, keys, serverHost);
const encoded = Buffer.from(vlessUri).toString('base64');
res.writeHead(200, {
'Content-Type': 'text/plain; charset=utf-8',
'subscription-userinfo': userInfoHeader,
'profile-update-interval': '6',
});
res.end(encoded);
}
return;
}
// 用户配额查询: /quota/{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 totalGB = user.quota_bytes / (1024 ** 3);
const usedGB = (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,
total_gb: parseFloat(totalGB.toFixed(1)),
used_gb: parseFloat(usedGB.toFixed(2)),
remaining_gb: parseFloat((totalGB - usedGB).toFixed(2)),
percentage_used: parseFloat(((usedGB / totalGB) * 100).toFixed(1)),
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 totalGB = user.quota_bytes / (1024 ** 3);
const usedGB = (user.traffic.upload_bytes + user.traffic.download_bytes) / (1024 ** 3);
const serverHost = getServerHost();
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',
version: '2.0.0',
uptime_seconds: Math.floor(process.uptime()),
region: 'sg',
region_name: 'Singapore Zone 1',
server_code: 'ZY-SVR-005'
},
node: {
id: 'zy-brain-direct',
name: `🏛️ 铸渊专线V2-${user.label}`,
server: serverHost,
port: 443,
protocol: 'vless',
security: 'reality'
},
quota: {
total_gb: parseFloat(totalGB.toFixed(1)),
used_gb: parseFloat(usedGB.toFixed(2)),
remaining_gb: parseFloat((totalGB - usedGB).toFixed(2)),
percentage_used: parseFloat(((usedGB / totalGB) * 100).toFixed(1)),
period: user.traffic.period
},
updated_at: new Date().toISOString()
}));
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();
console.log(`🌐 铸渊专线V2订阅服务已启动: http://127.0.0.1:${PORT}`);
console.log(` 版本: V2.0 (多用户独立专线)`);
console.log(` 用户数: ${users.length}`);
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(` 健康检查: /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);
});

View File

@ -0,0 +1,145 @@
#!/usr/bin/env node
// ═══════════════════════════════════════════════
// 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001
// 📜 Copyright: 国作登字-2026-A-00037559
// ═══════════════════════════════════════════════
// server/proxy/service/traffic-monitor-v2.js
// 📊 铸渊专线V2 · 多用户流量监控Agent
//
// 定期查询Xray Stats API获取每个用户的独立流量数据
// Xray通过email字段追踪每个client的流量:
// user>>>email@example.com>>>traffic>>>uplink
// user>>>email@example.com>>>traffic>>>downlink
//
// 运行方式: PM2 managed (zy-proxy-v2-monitor)
// 检查间隔: 每5分钟
// ═══════════════════════════════════════════════
'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 XRAY_API_HOST = '127.0.0.1';
const XRAY_API_PORT = 10085;
const CHECK_INTERVAL = 5 * 60 * 1000; // 5分钟
// 引入用户管理器
const userManager = require('./user-manager');
// ── 查询Xray流量统计 ────────────────────────
function queryXrayStats() {
try {
const result = execSync(
`xray api statsquery --server=${XRAY_API_HOST}:${XRAY_API_PORT} -pattern ""`,
{ encoding: 'utf8', timeout: 10000 }
);
return JSON.parse(result);
} catch (err) {
console.error('[V2流量监控] Xray Stats API查询失败:', err.message);
return null;
}
}
// ── 解析每用户流量 ───────────────────────────
// Xray Stats格式: user>>>email@example.com>>>traffic>>>uplink
function parsePerUserStats(stats) {
if (!stats || !stats.stat) return {};
const userTraffic = {};
for (const item of stats.stat) {
if (!item.name || !item.name.startsWith('user>>>')) continue;
// 格式: user>>>email>>>traffic>>>uplink/downlink
const parts = item.name.split('>>>');
if (parts.length < 4) continue;
const email = parts[1];
const direction = parts[3]; // 'uplink' or 'downlink'
const bytes = parseInt(item.value || '0', 10);
if (!userTraffic[email]) {
userTraffic[email] = { upload: 0, download: 0 };
}
if (direction === 'uplink') {
userTraffic[email].upload = bytes;
} else if (direction === 'downlink') {
userTraffic[email].download = bytes;
}
}
return userTraffic;
}
// ── 主循环 ───────────────────────────────────
function monitor() {
console.log('[V2流量监控] 开始检查...');
// 查询Xray统计
const stats = queryXrayStats();
if (!stats) {
console.log('[V2流量监控] Xray统计暂不可用');
return;
}
// 解析每用户流量
const perUserTraffic = parsePerUserStats(stats);
const users = userManager.getEnabledUsers();
let totalUpload = 0;
let totalDownload = 0;
for (const user of users) {
const traffic = perUserTraffic[user.email];
if (traffic) {
userManager.updateTraffic(user.email, traffic.upload, traffic.download);
totalUpload += traffic.upload;
totalDownload += traffic.download;
const usedGB = ((traffic.upload + traffic.download) / (1024 ** 3)).toFixed(2);
const quotaGB = (user.quota_bytes / (1024 ** 3)).toFixed(0);
console.log(` ${user.email}: ${usedGB}GB / ${quotaGB}GB`);
}
}
const totalGB = ((totalUpload + totalDownload) / (1024 ** 3)).toFixed(2);
console.log(`[V2流量监控] 总流量: ${totalGB}GB (${users.length}个用户)`);
// 保存汇总数据
const summaryFile = path.join(DATA_DIR, 'v2-traffic-summary.json');
const summary = {
total_upload_bytes: totalUpload,
total_download_bytes: totalDownload,
total_used_bytes: totalUpload + totalDownload,
users_count: users.length,
per_user: Object.entries(perUserTraffic).map(([email, t]) => ({
email,
upload_gb: parseFloat((t.upload / (1024 ** 3)).toFixed(2)),
download_gb: parseFloat((t.download / (1024 ** 3)).toFixed(2)),
total_gb: parseFloat(((t.upload + t.download) / (1024 ** 3)).toFixed(2))
})),
updated_at: new Date().toISOString()
};
fs.mkdirSync(DATA_DIR, { recursive: true });
fs.writeFileSync(summaryFile, JSON.stringify(summary, null, 2));
console.log('[V2流量监控] 检查完成');
}
// ── 启动监控循环 ─────────────────────────────
console.log('📊 铸渊专线V2流量监控启动');
console.log(` 检查间隔: ${CHECK_INTERVAL / 1000}`);
console.log(` 数据目录: ${DATA_DIR}`);
// 立即执行一次
try { monitor(); } catch (err) { console.error('首次检查失败:', err.message); }
// 定期执行
setInterval(() => {
try { monitor(); } catch (err) { console.error('监控异常:', err.message); }
}, CHECK_INTERVAL);

View File

@ -0,0 +1,521 @@
#!/usr/bin/env node
// ═══════════════════════════════════════════════
// 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001
// 📜 Copyright: 国作登字-2026-A-00037559
// ═══════════════════════════════════════════════
// server/proxy/service/user-manager.js
// 👤 铸渊专线V2 · 多用户管理器
//
// 每个邮箱绑定一条独立专线:
// - 独立UUID (Xray VLESS用户ID)
// - 独立订阅Token (订阅URL认证)
// - 独立流量统计 (Xray Stats按email追踪)
// - 独立配额 (每人500GB/月)
//
// 用户数据存储: /opt/zhuyuan-brain/proxy/data/users.json
// Xray配置自动重建: 增删用户后自动更新config并重启Xray
//
// CLI用法:
// node user-manager.js add <email> — 添加用户
// node user-manager.js remove <email> — 移除用户
// node user-manager.js list — 列出所有用户
// node user-manager.js get <email> — 查看用户详情
// node user-manager.js rebuild — 重建Xray配置
// node user-manager.js export — 导出用户数据(无密钥)
// ═══════════════════════════════════════════════
'use strict';
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const { execSync } = require('child_process');
// ── 路径配置 ────────────────────────────────
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 XRAY_CONFIG_OUTPUT = '/usr/local/etc/xray/config.json';
const XRAY_TEMPLATE = path.join(PROXY_DIR, 'config', 'xray-brain-template.json');
const KEYS_FILE = path.join(PROXY_DIR, '.env.keys');
// ── 工具函数 ────────────────────────────────
/**
* 生成UUID v4
*/
function generateUUID() {
// 优先用xray内置生成
try {
const uuid = execSync('xray uuid', { encoding: 'utf8', timeout: 5000 }).trim();
if (uuid && uuid.match(/^[0-9a-f]{8}-/)) return uuid;
} catch { /* fallback */ }
// 回退: crypto
return crypto.randomUUID();
}
/**
* 生成32字节hex订阅Token
*/
function generateToken() {
return crypto.randomBytes(32).toString('hex');
}
/**
* 读取用户数据库
*/
function loadUsers() {
try {
return JSON.parse(fs.readFileSync(USERS_FILE, 'utf8'));
} catch {
return { version: '2.0', users: [], created_at: new Date().toISOString() };
}
}
/**
* 保存用户数据库
*/
function saveUsers(db) {
fs.mkdirSync(DATA_DIR, { recursive: true });
db.updated_at = new Date().toISOString();
fs.writeFileSync(USERS_FILE, JSON.stringify(db, null, 2));
fs.chmodSync(USERS_FILE, 0o600);
}
/**
* 读取密钥文件
*/
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 { /* ignore */ }
return keys;
}
// ── 用户操作 ────────────────────────────────
/**
* 添加用户
* @param {string} email - 邮箱地址
* @param {object} options - 可选参数 {quota_gb, label}
* @returns {object} 新用户信息
*/
function addUser(email, options = {}) {
if (!email || !email.includes('@')) {
throw new Error(`无效的邮箱地址: ${email}`);
}
const db = loadUsers();
// 检查是否已存在
const existing = db.users.find(u => u.email === email);
if (existing) {
throw new Error(`用户已存在: ${email} (UUID: ${existing.uuid.substring(0, 8)}...)`);
}
const user = {
email,
uuid: generateUUID(),
token: generateToken(),
label: options.label || email.split('@')[0],
quota_bytes: (options.quota_gb || 500) * 1024 * 1024 * 1024,
enabled: true,
created_at: new Date().toISOString(),
traffic: {
upload_bytes: 0,
download_bytes: 0,
period: `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`,
alerts_sent: { p80: false, p90: false, p100: false }
}
};
db.users.push(user);
saveUsers(db);
console.log(`✅ 用户已添加: ${email}`);
console.log(` UUID: ${user.uuid}`);
console.log(` Token: ${user.token}`);
console.log(` 配额: ${options.quota_gb || 500}GB/月`);
return user;
}
/**
* 移除用户
* @param {string} email - 邮箱地址
* @returns {boolean}
*/
function removeUser(email) {
const db = loadUsers();
const idx = db.users.findIndex(u => u.email === email);
if (idx === -1) {
throw new Error(`用户不存在: ${email}`);
}
const removed = db.users.splice(idx, 1)[0];
saveUsers(db);
console.log(`✅ 用户已移除: ${email} (UUID: ${removed.uuid.substring(0, 8)}...)`);
return true;
}
/**
* 列出所有用户
*/
function listUsers() {
const db = loadUsers();
if (db.users.length === 0) {
console.log('📋 暂无用户');
return [];
}
console.log(`📋 用户列表 (${db.users.length}人):`);
console.log('─'.repeat(80));
for (const user of db.users) {
const usedGB = ((user.traffic.upload_bytes + user.traffic.download_bytes) / (1024 ** 3)).toFixed(2);
const quotaGB = (user.quota_bytes / (1024 ** 3)).toFixed(0);
const status = user.enabled ? '✅' : '⛔';
console.log(` ${status} ${user.email}`);
console.log(` UUID: ${user.uuid.substring(0, 8)}... 流量: ${usedGB}GB/${quotaGB}GB 标签: ${user.label}`);
}
console.log('─'.repeat(80));
return db.users;
}
/**
* 获取单个用户详情
* @param {string} email
*/
function getUser(email) {
const db = loadUsers();
const user = db.users.find(u => u.email === email);
if (!user) {
throw new Error(`用户不存在: ${email}`);
}
const usedGB = ((user.traffic.upload_bytes + user.traffic.download_bytes) / (1024 ** 3)).toFixed(2);
const quotaGB = (user.quota_bytes / (1024 ** 3)).toFixed(0);
console.log(`👤 用户详情: ${email}`);
console.log(` UUID: ${user.uuid}`);
console.log(` Token: ${user.token}`);
console.log(` 标签: ${user.label}`);
console.log(` 状态: ${user.enabled ? '启用' : '禁用'}`);
console.log(` 配额: ${quotaGB}GB/月`);
console.log(` 已用: ${usedGB}GB`);
console.log(` 周期: ${user.traffic.period}`);
console.log(` 创建于: ${user.created_at}`);
return user;
}
/**
* 通过token查找用户
* @param {string} token
* @returns {object|null}
*/
function findUserByToken(token) {
const db = loadUsers();
return db.users.find(u => u.token === token && u.enabled) || null;
}
/**
* 获取所有启用的用户用于生成Xray配置
* @returns {Array}
*/
function getEnabledUsers() {
const db = loadUsers();
return db.users.filter(u => u.enabled);
}
/**
* 更新用户流量数据
* @param {string} email
* @param {number} upload
* @param {number} download
*/
function updateTraffic(email, upload, download) {
const db = loadUsers();
const user = db.users.find(u => u.email === email);
if (!user) return;
const currentPeriod = `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`;
// 月度重置
if (user.traffic.period !== currentPeriod) {
user.traffic.upload_bytes = 0;
user.traffic.download_bytes = 0;
user.traffic.period = currentPeriod;
user.traffic.alerts_sent = { p80: false, p90: false, p100: false };
}
user.traffic.upload_bytes = upload;
user.traffic.download_bytes = download;
saveUsers(db);
}
// ── Xray配置重建 ────────────────────────────
/**
* 重建Xray配置文件根据当前用户列表
* 每个用户 = 一个VLESS client = 一条独立专线
*/
function rebuildXrayConfig() {
const keys = loadKeys();
const users = getEnabledUsers();
if (!keys.ZY_PROXY_REALITY_PRIVATE_KEY) {
throw new Error('缺少 ZY_PROXY_REALITY_PRIVATE_KEY — 请先运行 generate-keys.sh');
}
if (!keys.ZY_PROXY_REALITY_SHORT_ID) {
throw new Error('缺少 ZY_PROXY_REALITY_SHORT_ID — 请先运行 generate-keys.sh');
}
if (users.length === 0) {
console.log('⚠️ 没有启用的用户Xray配置将为空');
}
// 构建clients数组每个用户一条独立线路
const clients = users.map(user => ({
id: user.uuid,
flow: 'xtls-rprx-vision',
email: user.email
}));
const config = {
_comment: '铸渊专线V2 · 多用户Xray配置 · 自动生成 · 请勿手动编辑',
_generated_at: new Date().toISOString(),
_copyright: '国作登字-2026-A-00037559',
_users: users.length,
log: {
loglevel: 'warning',
access: '/opt/zhuyuan-brain/proxy/logs/access.log',
error: '/opt/zhuyuan-brain/proxy/logs/error.log'
},
dns: {
servers: ['8.8.8.8', '1.1.1.1', 'localhost']
},
stats: {},
api: {
tag: 'api',
services: ['StatsService']
},
policy: {
levels: {
'0': {
statsUserUplink: true,
statsUserDownlink: true
}
},
system: {
statsInboundUplink: true,
statsInboundDownlink: true,
statsOutboundUplink: true,
statsOutboundDownlink: true
}
},
inbounds: [
{
tag: 'zy-vless-reality-v2',
listen: '0.0.0.0',
port: 443,
protocol: 'vless',
settings: {
clients,
decryption: 'none'
},
streamSettings: {
network: 'tcp',
security: 'reality',
realitySettings: {
show: false,
dest: 'www.microsoft.com:443',
xver: 0,
serverNames: ['www.microsoft.com', 'www.amazon.com'],
privateKey: keys.ZY_PROXY_REALITY_PRIVATE_KEY,
shortIds: [keys.ZY_PROXY_REALITY_SHORT_ID]
}
},
sniffing: {
enabled: true,
destOverride: ['http', 'tls', 'quic']
}
},
{
tag: 'api-in',
port: 10085,
listen: '127.0.0.1',
protocol: 'dokodemo-door',
settings: { address: '127.0.0.1' }
}
],
outbounds: [
{ tag: 'direct', protocol: 'freedom' },
{ tag: 'block', protocol: 'blackhole' }
],
routing: {
rules: [
{
type: 'field',
inboundTag: ['api-in'],
outboundTag: 'api'
},
{
type: 'field',
ip: ['geoip:private'],
outboundTag: 'block'
}
]
}
};
// 写入配置文件
fs.writeFileSync(XRAY_CONFIG_OUTPUT, JSON.stringify(config, null, 2));
console.log(`✅ Xray配置已重建: ${users.length}个用户`);
// 验证配置
try {
execSync(`xray run -test -c ${XRAY_CONFIG_OUTPUT}`, { encoding: 'utf8', timeout: 10000 });
console.log('✅ Xray配置验证通过');
} catch (err) {
console.error('❌ Xray配置验证失败:', err.message);
throw err;
}
return config;
}
/**
* 重启Xray服务
*/
function restartXray() {
try {
execSync('systemctl restart xray', { encoding: 'utf8', timeout: 15000 });
console.log('✅ Xray已重启');
return true;
} catch (err) {
console.error('❌ Xray重启失败:', err.message);
return false;
}
}
/**
* 添加用户 + 重建配置 + 重启Xray (完整流程)
*/
function addUserFull(email, options = {}) {
const user = addUser(email, options);
rebuildXrayConfig();
restartXray();
return user;
}
/**
* 移除用户 + 重建配置 + 重启Xray (完整流程)
*/
function removeUserFull(email) {
removeUser(email);
rebuildXrayConfig();
restartXray();
}
// ── 导出 ────────────────────────────────────
module.exports = {
loadUsers,
saveUsers,
addUser,
removeUser,
listUsers,
getUser,
findUserByToken,
getEnabledUsers,
updateTraffic,
rebuildXrayConfig,
restartXray,
addUserFull,
removeUserFull,
generateUUID,
generateToken,
USERS_FILE,
DATA_DIR,
KEYS_FILE
};
// ── CLI入口 ────────────────────────────────
if (require.main === module) {
const args = process.argv.slice(2);
const command = args[0];
const param = args[1];
try {
switch (command) {
case 'add':
if (!param) { console.error('用法: node user-manager.js add <email> [quota_gb]'); process.exit(1); }
addUserFull(param, { quota_gb: parseInt(args[2] || '500', 10) });
break;
case 'remove':
if (!param) { console.error('用法: node user-manager.js remove <email>'); process.exit(1); }
removeUserFull(param);
break;
case 'list':
listUsers();
break;
case 'get':
if (!param) { console.error('用法: node user-manager.js get <email>'); process.exit(1); }
getUser(param);
break;
case 'rebuild':
rebuildXrayConfig();
restartXray();
break;
case 'export':
const db = loadUsers();
const safe = {
total: db.users.length,
users: db.users.map(u => ({
email: u.email,
label: u.label,
enabled: u.enabled,
quota_gb: (u.quota_bytes / (1024 ** 3)).toFixed(0),
used_gb: ((u.traffic.upload_bytes + u.traffic.download_bytes) / (1024 ** 3)).toFixed(2),
created_at: u.created_at
}))
};
console.log(JSON.stringify(safe, null, 2));
break;
default:
console.log('铸渊专线V2 · 多用户管理器');
console.log('');
console.log('用法:');
console.log(' node user-manager.js add <email> [quota_gb] — 添加用户 (默认500GB/月)');
console.log(' node user-manager.js remove <email> — 移除用户');
console.log(' node user-manager.js list — 列出所有用户');
console.log(' node user-manager.js get <email> — 查看用户详情');
console.log(' node user-manager.js rebuild — 重建Xray配置');
console.log(' node user-manager.js export — 导出用户数据(无密钥)');
break;
}
} catch (err) {
console.error(`${err.message}`);
process.exit(1);
}
}