617 lines
18 KiB
JavaScript
617 lines
18 KiB
JavaScript
#!/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追踪)
|
||
// - 共享流量池 (2000GB/月·全用户共享·每月1号重置)
|
||
//
|
||
// 流量池模型:
|
||
// - 所有用户共享2000GB/月流量池
|
||
// - 无论多少用户,总量一致
|
||
// - 每月1号自动重置
|
||
//
|
||
// 用户数据存储: /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 — 导出用户数据(无密钥)
|
||
// node user-manager.js pool — 查看流量池状态
|
||
// ═══════════════════════════════════════════════
|
||
|
||
'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 POOL_STATUS_FILE = path.join(DATA_DIR, 'pool-quota-status.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');
|
||
|
||
// ── 共享流量池配额 ──────────────────────────
|
||
// 冰朔指令D58: 2000GB/月共享流量池·无论多少用户总量一致·每月1号重置
|
||
const POOL_QUOTA_BYTES = 2000 * 1024 * 1024 * 1024; // 2000GB = 2TB
|
||
|
||
// ── 工具函数 ────────────────────────────────
|
||
|
||
/**
|
||
* 生成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 now = new Date();
|
||
const user = {
|
||
email,
|
||
uuid: generateUUID(),
|
||
token: generateToken(),
|
||
label: options.label || email.split('@')[0],
|
||
quota_bytes: POOL_QUOTA_BYTES, // 存储池配额引用·实际配额由池级别统一控制(getPoolStatus)·非独立限额
|
||
enabled: true,
|
||
created_at: now.toISOString(),
|
||
traffic: {
|
||
upload_bytes: 0,
|
||
download_bytes: 0,
|
||
period: `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`,
|
||
alerts_sent: { p80: false, p90: false, p100: false }
|
||
}
|
||
};
|
||
|
||
db.users.push(user);
|
||
saveUsers(db);
|
||
|
||
const poolGB = (POOL_QUOTA_BYTES / (1024 ** 3)).toFixed(0);
|
||
console.log(`✅ 用户已添加: ${email}`);
|
||
console.log(` UUID: ${user.uuid}`);
|
||
console.log(` Token: ${user.token}`);
|
||
console.log(` 流量池: ${poolGB}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 [];
|
||
}
|
||
|
||
const pool = getPoolStatus();
|
||
console.log(`📋 用户列表 (${db.users.length}人) · 流量池: ${pool.pool_used_gb.toFixed(2)}GB / ${pool.pool_total_gb}GB (${pool.pool_percentage.toFixed(1)}%)`);
|
||
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 status = user.enabled ? '✅' : '⛔';
|
||
|
||
console.log(` ${status} ${user.email}`);
|
||
console.log(` UUID: ${user.uuid.substring(0, 8)}... 个人用量: ${usedGB}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 pool = getPoolStatus();
|
||
|
||
console.log(`👤 用户详情: ${email}`);
|
||
console.log(` UUID: ${user.uuid}`);
|
||
console.log(` Token: ${user.token}`);
|
||
console.log(` 标签: ${user.label}`);
|
||
console.log(` 状态: ${user.enabled ? '启用' : '禁用'}`);
|
||
console.log(` 个人用量: ${usedGB}GB`);
|
||
console.log(` 流量池: ${pool.pool_used_gb.toFixed(2)}GB / ${pool.pool_total_gb}GB (${pool.pool_percentage.toFixed(1)}%)`);
|
||
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);
|
||
}
|
||
|
||
/**
|
||
* 获取共享流量池状态
|
||
* 汇总所有启用用户的流量 · 对比2000GB池配额
|
||
* @returns {object} 流量池状态
|
||
*/
|
||
function getPoolStatus() {
|
||
const db = loadUsers();
|
||
const now = new Date();
|
||
const currentPeriod = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
|
||
|
||
let totalUpload = 0;
|
||
let totalDownload = 0;
|
||
|
||
for (const user of db.users) {
|
||
if (!user.enabled) continue;
|
||
// 只统计当前周期的流量
|
||
if (user.traffic.period === currentPeriod) {
|
||
totalUpload += user.traffic.upload_bytes;
|
||
totalDownload += user.traffic.download_bytes;
|
||
}
|
||
}
|
||
|
||
const totalUsed = totalUpload + totalDownload;
|
||
const poolTotalGB = POOL_QUOTA_BYTES / (1024 ** 3);
|
||
const poolUsedGB = totalUsed / (1024 ** 3);
|
||
const percentage = (totalUsed / POOL_QUOTA_BYTES) * 100;
|
||
|
||
return {
|
||
pool_quota_bytes: POOL_QUOTA_BYTES,
|
||
pool_total_gb: poolTotalGB,
|
||
pool_upload_bytes: totalUpload,
|
||
pool_download_bytes: totalDownload,
|
||
pool_used_bytes: totalUsed,
|
||
pool_used_gb: poolUsedGB,
|
||
pool_remaining_bytes: Math.max(0, POOL_QUOTA_BYTES - totalUsed),
|
||
pool_remaining_gb: parseFloat(Math.max(0, poolTotalGB - poolUsedGB).toFixed(2)),
|
||
pool_percentage: parseFloat(percentage.toFixed(1)),
|
||
users_count: db.users.filter(u => u.enabled).length,
|
||
period: currentPeriod,
|
||
reset_day: 1,
|
||
updated_at: now.toISOString()
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 保存流量池状态到文件 (供监控和订阅服务使用)
|
||
*/
|
||
function savePoolStatus() {
|
||
const status = getPoolStatus();
|
||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||
fs.writeFileSync(POOL_STATUS_FILE, JSON.stringify(status, null, 2));
|
||
return status;
|
||
}
|
||
|
||
// ── 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,
|
||
getPoolStatus,
|
||
savePoolStatus,
|
||
rebuildXrayConfig,
|
||
restartXray,
|
||
addUserFull,
|
||
removeUserFull,
|
||
generateUUID,
|
||
generateToken,
|
||
USERS_FILE,
|
||
DATA_DIR,
|
||
KEYS_FILE,
|
||
POOL_QUOTA_BYTES,
|
||
POOL_STATUS_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>'); process.exit(1); }
|
||
addUserFull(param);
|
||
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 'pool': {
|
||
const pool = getPoolStatus();
|
||
console.log('🏊 铸渊专线V2 · 共享流量池状态');
|
||
console.log('─'.repeat(50));
|
||
console.log(` 总配额: ${pool.pool_total_gb}GB/月`);
|
||
console.log(` 已使用: ${pool.pool_used_gb.toFixed(2)}GB`);
|
||
console.log(` 剩余: ${pool.pool_remaining_gb}GB`);
|
||
console.log(` 使用率: ${pool.pool_percentage}%`);
|
||
console.log(` 用户数: ${pool.users_count}人`);
|
||
console.log(` 周期: ${pool.period}`);
|
||
console.log(` 重置日: 每月${pool.reset_day}号`);
|
||
console.log('─'.repeat(50));
|
||
break;
|
||
}
|
||
|
||
case 'export': {
|
||
const db = loadUsers();
|
||
const pool = getPoolStatus();
|
||
const safe = {
|
||
pool: {
|
||
total_gb: pool.pool_total_gb,
|
||
used_gb: parseFloat(pool.pool_used_gb.toFixed(2)),
|
||
remaining_gb: pool.pool_remaining_gb,
|
||
percentage: pool.pool_percentage,
|
||
period: pool.period
|
||
},
|
||
total_users: db.users.length,
|
||
users: db.users.map(u => ({
|
||
email: u.email,
|
||
label: u.label,
|
||
enabled: u.enabled,
|
||
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 · 多用户管理器 · 共享流量池2000GB/月');
|
||
console.log('');
|
||
console.log('用法:');
|
||
console.log(' node user-manager.js add <email> — 添加用户');
|
||
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 pool — 查看流量池状态');
|
||
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);
|
||
}
|
||
}
|