1102 lines
41 KiB
JavaScript
1102 lines
41 KiB
JavaScript
|
|
#!/usr/bin/env node
|
|||
|
|
// ═══════════════════════════════════════════════
|
|||
|
|
// 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001
|
|||
|
|
// 📜 Copyright: 国作登字-2026-A-00037559
|
|||
|
|
// ═══════════════════════════════════════════════
|
|||
|
|
// server/proxy/service/zy-cloud-vpn.js
|
|||
|
|
// 💪 ZY-CLOUD VPN 活模块 · 铸渊的肌肉系统第一个实战器官
|
|||
|
|
//
|
|||
|
|
// ZY-CLOUD = 算力人格体 = 铸渊的肌肉
|
|||
|
|
// VPN = ZY-CLOUD的第一个实战落地场景
|
|||
|
|
//
|
|||
|
|
// 核心理念 (冰朔D53/D57定根):
|
|||
|
|
// - 服务器越多 → 节点越多 → 系统越强 → 不输商业VPN
|
|||
|
|
// - 所有服务器VPN能力汇聚到一个活的人格模块上
|
|||
|
|
// - 动态感知空闲、动态选路、自我修复、自我学习
|
|||
|
|
// - 活模块6接口: heartbeat / selfDiagnose / selfHeal /
|
|||
|
|
// alertZhuyuan / learnFromRun / consultAI
|
|||
|
|
//
|
|||
|
|
// 运行在大脑服务器 (ZY-SVR-005)
|
|||
|
|
// 管理所有VPN节点的生命周期
|
|||
|
|
// ═══════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
'use strict';
|
|||
|
|
|
|||
|
|
const http = require('http');
|
|||
|
|
const https = require('https');
|
|||
|
|
const net = require('net');
|
|||
|
|
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 LIVE_NODES_FILE = path.join(DATA_DIR, 'nodes-live.json');
|
|||
|
|
const LEARN_DB_FILE = path.join(DATA_DIR, 'zy-cloud-vpn-learn.json');
|
|||
|
|
const REGISTRY_FILE = path.join(DATA_DIR, 'nodes-registry.json');
|
|||
|
|
const HEARTBEAT_FILE = path.join(DATA_DIR, 'zy-cloud-vpn-heartbeat.json');
|
|||
|
|
const KEYS_FILE = path.join(PROXY_DIR, '.env.keys');
|
|||
|
|
|
|||
|
|
// ── 时间间隔(可通过环境变量调整)────────────
|
|||
|
|
const HEARTBEAT_INTERVAL = parseInt(process.env.ZY_CLOUD_HEARTBEAT_INTERVAL || '30000', 10);
|
|||
|
|
const DIAGNOSE_INTERVAL = parseInt(process.env.ZY_CLOUD_DIAGNOSE_INTERVAL || '300000', 10);
|
|||
|
|
const LEARN_INTERVAL = parseInt(process.env.ZY_CLOUD_LEARN_INTERVAL || '1800000', 10);
|
|||
|
|
const HEARTBEAT_EXPIRY_MS = parseInt(process.env.ZY_CLOUD_HB_EXPIRY_MS || '600000', 10); // 10分钟
|
|||
|
|
const NODE_UNREGISTER_MS = parseInt(process.env.ZY_CLOUD_UNREGISTER_MS || '86400000', 10); // 24小时
|
|||
|
|
|
|||
|
|
// ═══════════════════════════════════════════════
|
|||
|
|
// LivingModule 基类
|
|||
|
|
// 所有活模块的生命基础 (D53 活模块标准)
|
|||
|
|
// ═══════════════════════════════════════════════
|
|||
|
|
class LivingModule {
|
|||
|
|
constructor(moduleId, moduleName) {
|
|||
|
|
this._moduleId = moduleId;
|
|||
|
|
this._moduleName = moduleName;
|
|||
|
|
this._state = 'initializing'; // healthy | degraded | critical
|
|||
|
|
this._errorCount = 0;
|
|||
|
|
this._consecutiveErrors = 0;
|
|||
|
|
this._requestCount = 0;
|
|||
|
|
this._failCount = 0;
|
|||
|
|
this._lastSuccess = null;
|
|||
|
|
this._lastError = null;
|
|||
|
|
this._startedAt = new Date().toISOString();
|
|||
|
|
this._history = [];
|
|||
|
|
this._timers = [];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 启动生命周期
|
|||
|
|
startLifeCycle() {
|
|||
|
|
console.log(`🫀 ${this._moduleName} 活模块启动`);
|
|||
|
|
this._state = 'healthy';
|
|||
|
|
|
|||
|
|
// 心跳循环
|
|||
|
|
this._timers.push(setInterval(() => {
|
|||
|
|
this._safeRun('heartbeat', () => this.heartbeat());
|
|||
|
|
}, HEARTBEAT_INTERVAL));
|
|||
|
|
|
|||
|
|
// 诊断循环
|
|||
|
|
this._timers.push(setInterval(() => {
|
|||
|
|
this._safeRun('selfDiagnose', () => this.selfDiagnose());
|
|||
|
|
}, DIAGNOSE_INTERVAL));
|
|||
|
|
|
|||
|
|
// 学习循环
|
|||
|
|
this._timers.push(setInterval(() => {
|
|||
|
|
this._safeRun('learnFromRun', () => this.learnFromRun());
|
|||
|
|
}, LEARN_INTERVAL));
|
|||
|
|
|
|||
|
|
// 立即执行一次
|
|||
|
|
this._safeRun('heartbeat', () => this.heartbeat());
|
|||
|
|
setTimeout(() => this._safeRun('selfDiagnose', () => this.selfDiagnose()), 5000);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async _safeRun(name, fn) {
|
|||
|
|
try {
|
|||
|
|
await fn();
|
|||
|
|
this._consecutiveErrors = 0;
|
|||
|
|
} catch (err) {
|
|||
|
|
this._consecutiveErrors++;
|
|||
|
|
this._errorCount++;
|
|||
|
|
this._lastError = { time: new Date().toISOString(), message: err.message, source: name };
|
|||
|
|
console.error(`❌ ${this._moduleName}.${name} 异常:`, err.message);
|
|||
|
|
|
|||
|
|
if (this._consecutiveErrors >= 3) {
|
|||
|
|
this._state = 'critical';
|
|||
|
|
await this._safeRun('alertZhuyuan', () => this.alertZhuyuan({
|
|||
|
|
level: 'critical',
|
|||
|
|
message: `${this._moduleName} 连续${this._consecutiveErrors}次${name}失败`,
|
|||
|
|
error: err.message
|
|||
|
|
}));
|
|||
|
|
} else if (this._consecutiveErrors >= 1) {
|
|||
|
|
this._state = 'degraded';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
destroy() {
|
|||
|
|
this._timers.forEach(t => clearInterval(t));
|
|||
|
|
this._timers = [];
|
|||
|
|
console.log(`💀 ${this._moduleName} 活模块停止`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 以下由子类实现
|
|||
|
|
async heartbeat() { throw new Error('heartbeat() 未实现'); }
|
|||
|
|
async selfDiagnose() { throw new Error('selfDiagnose() 未实现'); }
|
|||
|
|
async selfHeal(problem) { throw new Error('selfHeal() 未实现'); }
|
|||
|
|
async alertZhuyuan(alert) { throw new Error('alertZhuyuan() 未实现'); }
|
|||
|
|
async learnFromRun() { throw new Error('learnFromRun() 未实现'); }
|
|||
|
|
async consultAI(context) { throw new Error('consultAI() 未实现'); }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ═══════════════════════════════════════════════
|
|||
|
|
// ZY-CLOUD VPN 活模块
|
|||
|
|
// 铸渊的VPN肌肉系统 · 汇聚所有服务器VPN能力
|
|||
|
|
// ═══════════════════════════════════════════════
|
|||
|
|
class ZyCloudVpn extends LivingModule {
|
|||
|
|
constructor() {
|
|||
|
|
super('ZY-CLOUD-VPN', 'ZY-CLOUD VPN活模块');
|
|||
|
|
this._nodes = []; // 所有已知节点
|
|||
|
|
this._liveNodes = []; // 当前存活节点
|
|||
|
|
this._registry = this._loadRegistry(); // 动态注册表
|
|||
|
|
this._learnDb = this._loadLearnDb();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 节点注册表(持久化)────────────────────
|
|||
|
|
// 像路由器一样:插入即注册,拔掉即注销
|
|||
|
|
_loadRegistry() {
|
|||
|
|
try {
|
|||
|
|
return JSON.parse(fs.readFileSync(REGISTRY_FILE, 'utf8'));
|
|||
|
|
} catch {
|
|||
|
|
return {
|
|||
|
|
version: '1.0',
|
|||
|
|
_comment: 'ZY-CLOUD VPN节点注册表 · 新服务器像路由器一样插入',
|
|||
|
|
created_at: new Date().toISOString(),
|
|||
|
|
nodes: {} // key = node_id, value = node registration data
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_saveRegistry() {
|
|||
|
|
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|||
|
|
this._registry.updated_at = new Date().toISOString();
|
|||
|
|
fs.writeFileSync(REGISTRY_FILE, JSON.stringify(this._registry, null, 2));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 注册节点(像接路由器一样插入)──────────
|
|||
|
|
// 任何服务器发送HLDP heartbeat或调用/register即完成注册
|
|||
|
|
registerNode(nodeData) {
|
|||
|
|
const nodeId = nodeData.id || nodeData.node_id;
|
|||
|
|
if (!nodeId || !nodeData.host || !nodeData.pbk) {
|
|||
|
|
throw new Error('注册失败: 缺少必填字段 (id, host, pbk)');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const existing = this._registry.nodes[nodeId];
|
|||
|
|
const node = {
|
|||
|
|
id: nodeId,
|
|||
|
|
name: nodeData.name || `VPN节点-${nodeId}`,
|
|||
|
|
host: nodeData.host,
|
|||
|
|
port: nodeData.port || 443,
|
|||
|
|
pbk: nodeData.pbk,
|
|||
|
|
sid: nodeData.sid || '',
|
|||
|
|
region: nodeData.region || 'unknown',
|
|||
|
|
server_code: nodeData.server_code || nodeId,
|
|||
|
|
type: nodeData.type || 'remote',
|
|||
|
|
specs: nodeData.specs || 'unknown',
|
|||
|
|
persona_id: nodeData.persona_id || null, // HLDP人格体ID
|
|||
|
|
registered_at: existing ? existing.registered_at : new Date().toISOString(),
|
|||
|
|
last_heartbeat: new Date().toISOString(),
|
|||
|
|
heartbeat_count: (existing ? existing.heartbeat_count : 0) + 1
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
this._registry.nodes[nodeId] = node;
|
|||
|
|
this._saveRegistry();
|
|||
|
|
|
|||
|
|
console.log(`[ZY-CLOUD VPN] 🔌 节点${existing ? '更新' : '注册'}: ${node.name} (${node.host}:${node.port})`);
|
|||
|
|
return node;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 注销节点(拔掉路由器)──────────────────
|
|||
|
|
unregisterNode(nodeId) {
|
|||
|
|
if (!this._registry.nodes[nodeId]) {
|
|||
|
|
throw new Error(`节点不存在: ${nodeId}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const removed = this._registry.nodes[nodeId];
|
|||
|
|
delete this._registry.nodes[nodeId];
|
|||
|
|
this._saveRegistry();
|
|||
|
|
|
|||
|
|
console.log(`[ZY-CLOUD VPN] 🔌 节点注销: ${removed.name} (${removed.host})`);
|
|||
|
|
return removed;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 处理HLDP heartbeat消息 ─────────────────
|
|||
|
|
// 节点通过HLDP协议发送心跳 = 自动注册 + 状态更新
|
|||
|
|
handleHldpHeartbeat(hldpMsg) {
|
|||
|
|
// 验证HLDP消息格式
|
|||
|
|
if (!hldpMsg || hldpMsg.hldp_v !== '3.0' || hldpMsg.msg_type !== 'heartbeat') {
|
|||
|
|
throw new Error('无效的HLDP heartbeat消息');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const sender = hldpMsg.sender || {};
|
|||
|
|
const payload = hldpMsg.payload || {};
|
|||
|
|
const data = payload.data || {};
|
|||
|
|
|
|||
|
|
// 从HLDP payload中提取VPN节点信息
|
|||
|
|
const vpnData = data.vpn_node || {};
|
|||
|
|
if (!vpnData.host || !vpnData.pbk) {
|
|||
|
|
// 非VPN节点的心跳,忽略
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 自动注册/更新节点
|
|||
|
|
return this.registerNode({
|
|||
|
|
id: vpnData.node_id || `hldp-${sender.id}`,
|
|||
|
|
name: vpnData.name || `${sender.name || sender.id}-VPN`,
|
|||
|
|
host: vpnData.host,
|
|||
|
|
port: vpnData.port || 443,
|
|||
|
|
pbk: vpnData.pbk,
|
|||
|
|
sid: vpnData.sid || '',
|
|||
|
|
region: vpnData.region || 'unknown',
|
|||
|
|
server_code: vpnData.server_code || sender.id,
|
|||
|
|
type: 'hldp', // 通过HLDP协议注册的节点
|
|||
|
|
specs: vpnData.specs || `${data.cpu_cores || '?'}核${data.memory_gb || '?'}G`,
|
|||
|
|
persona_id: sender.id
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 列出所有注册节点 ───────────────────────
|
|||
|
|
listRegisteredNodes() {
|
|||
|
|
return Object.values(this._registry.nodes);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 读取密钥/配置 ────────────────────────
|
|||
|
|
_readEnvOrKey(envName) {
|
|||
|
|
if (process.env[envName]) return process.env[envName];
|
|||
|
|
try {
|
|||
|
|
const content = fs.readFileSync(KEYS_FILE, 'utf8');
|
|||
|
|
for (const line of content.split('\n')) {
|
|||
|
|
if (line.startsWith('#') || !line.includes('=')) continue;
|
|||
|
|
const [key, ...vals] = line.split('=');
|
|||
|
|
if (key.trim() === envName) return vals.join('=').trim();
|
|||
|
|
}
|
|||
|
|
} catch { /* ignore */ }
|
|||
|
|
return '';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 发现所有可能的VPN节点 ─────────────────
|
|||
|
|
// ZY-CLOUD双源发现: 静态配置 + 动态注册表
|
|||
|
|
// 静态: .env.keys中的核心节点(大脑/面孔/CN中转)
|
|||
|
|
// 动态: nodes-registry.json中通过HLDP或API注册的节点
|
|||
|
|
_discoverNodes() {
|
|||
|
|
const nodes = [];
|
|||
|
|
const seenIds = new Set();
|
|||
|
|
|
|||
|
|
// ── 源1: 静态配置(核心节点)────────────
|
|||
|
|
// 节点1: 大脑服务器 (ZY-SVR-005 · 本机 · 主力)
|
|||
|
|
const brainHost = this._readEnvOrKey('ZY_BRAIN_HOST');
|
|||
|
|
const brainPbk = this._readEnvOrKey('ZY_PROXY_REALITY_PUBLIC_KEY');
|
|||
|
|
const brainSid = this._readEnvOrKey('ZY_PROXY_REALITY_SHORT_ID');
|
|||
|
|
if (brainHost && brainPbk) {
|
|||
|
|
const localSpecs = `${require('os').cpus().length}核${Math.round(require('os').totalmem() / (1024 ** 3))}G`;
|
|||
|
|
nodes.push(this._makeNode('zy-brain-sg1', '🧠 铸渊专线V2-SG1(大脑)', brainHost, 443, brainPbk, brainSid, 'sg-zone1', 'ZY-SVR-005', 'local', localSpecs));
|
|||
|
|
seenIds.add('zy-brain-sg1');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 节点2: 面孔服务器 (ZY-SVR-002 · 远程 · 备用)
|
|||
|
|
const faceHost = this._readEnvOrKey('ZY_FACE_HOST');
|
|||
|
|
const facePbk = this._readEnvOrKey('ZY_FACE_REALITY_PUBLIC_KEY');
|
|||
|
|
const faceSid = this._readEnvOrKey('ZY_FACE_REALITY_SHORT_ID');
|
|||
|
|
if (faceHost && facePbk) {
|
|||
|
|
nodes.push(this._makeNode('zy-face-sg2', '🏛️ 铸渊专线V2-SG2(面孔)', faceHost, 443, facePbk, faceSid, 'sg-zone2', 'ZY-SVR-002', 'remote', '2核8G'));
|
|||
|
|
seenIds.add('zy-face-sg2');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 节点3: CN中转 (国内→SG · 透传)
|
|||
|
|
const cnHost = this._readEnvOrKey('ZY_CN_RELAY_HOST');
|
|||
|
|
const cnPort = parseInt(this._readEnvOrKey('ZY_CN_RELAY_PORT') || '2053', 10);
|
|||
|
|
if (cnHost && brainPbk) {
|
|||
|
|
nodes.push(this._makeNode('zy-cn-relay', '🇨🇳 铸渊专线V2-CN中转', cnHost, cnPort, brainPbk, brainSid, 'cn-relay', 'ZY-SVR-004', 'relay', '中转'));
|
|||
|
|
seenIds.add('zy-cn-relay');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 节点4: 硅谷服务器 (ZY-SVR-SV · Claude专线 · 美国IP出口)
|
|||
|
|
// 冰朔D61: 硅谷服务器已配置,新增Claude单独访问VPN节点
|
|||
|
|
const svHost = this._readEnvOrKey('ZY_SVR_SV_HOST');
|
|||
|
|
const svPbk = this._readEnvOrKey('ZY_SVR_SV_REALITY_PUBLIC_KEY');
|
|||
|
|
const svSid = this._readEnvOrKey('ZY_SVR_SV_REALITY_SHORT_ID');
|
|||
|
|
const svPort = parseInt(this._readEnvOrKey('ZY_SVR_SV_PORT') || '443', 10);
|
|||
|
|
if (svHost && svPbk) {
|
|||
|
|
nodes.push(this._makeNode('zy-sv-claude', '🇺🇸 铸渊专线V2-SV(Claude)', svHost, svPort, svPbk, svSid || '', 'us-sv', 'ZY-SVR-SV', 'remote', '硅谷'));
|
|||
|
|
seenIds.add('zy-sv-claude');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 源2: 动态注册表(通过HLDP/API注册的节点)──
|
|||
|
|
// 这些是"像路由器一样插入"的节点
|
|||
|
|
for (const regNode of Object.values(this._registry.nodes)) {
|
|||
|
|
if (seenIds.has(regNode.id)) continue; // 避免重复
|
|||
|
|
|
|||
|
|
// 检查心跳是否过期
|
|||
|
|
const lastHb = new Date(regNode.last_heartbeat).getTime();
|
|||
|
|
const age = Date.now() - lastHb;
|
|||
|
|
if (age > HEARTBEAT_EXPIRY_MS) {
|
|||
|
|
console.log(`[ZY-CLOUD VPN] ⏰ 节点 ${regNode.name} 心跳过期(${Math.floor(age/60000)}分钟),标记为离线`);
|
|||
|
|
// 不删除注册,只是标记离线(可能临时断网)
|
|||
|
|
// 超过24小时无心跳才自动清理
|
|||
|
|
if (age > NODE_UNREGISTER_MS) {
|
|||
|
|
console.log(`[ZY-CLOUD VPN] 🗑️ 节点 ${regNode.name} 超24小时无心跳,自动注销`);
|
|||
|
|
delete this._registry.nodes[regNode.id];
|
|||
|
|
this._saveRegistry();
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
nodes.push(this._makeNode(
|
|||
|
|
regNode.id, regNode.name, regNode.host, regNode.port,
|
|||
|
|
regNode.pbk, regNode.sid, regNode.region, regNode.server_code,
|
|||
|
|
regNode.type, regNode.specs
|
|||
|
|
));
|
|||
|
|
seenIds.add(regNode.id);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 未来源3: COS桶发现(team-integration-v3)
|
|||
|
|
// 扫描 zy-team-hub/compute-pool/heartbeat/*.json
|
|||
|
|
// 包含 vpn_capable: true 的节点自动加入
|
|||
|
|
|
|||
|
|
this._nodes = nodes;
|
|||
|
|
return nodes;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 构建节点对象 ───────────────────────────
|
|||
|
|
_makeNode(id, name, host, port, pbk, sid, region, serverCode, type, specs) {
|
|||
|
|
// 保留已有的统计数据(如果节点已存在)
|
|||
|
|
const existing = this._nodes.find(n => n.id === id);
|
|||
|
|
return {
|
|||
|
|
id, name, host, port, pbk, sid: sid || '',
|
|||
|
|
region, server_code: serverCode, type, specs,
|
|||
|
|
status: existing ? existing.status : 'unknown',
|
|||
|
|
latency_ms: existing ? existing.latency_ms : null,
|
|||
|
|
last_check: existing ? existing.last_check : null,
|
|||
|
|
consecutive_failures: existing ? existing.consecutive_failures : 0,
|
|||
|
|
total_checks: existing ? existing.total_checks : 0,
|
|||
|
|
total_successes: existing ? existing.total_successes : 0
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── TCP端口探测 ────────────────────────────
|
|||
|
|
_probePort(host, port, timeoutMs = 5000) {
|
|||
|
|
return new Promise((resolve) => {
|
|||
|
|
const start = Date.now();
|
|||
|
|
const socket = new net.Socket();
|
|||
|
|
|
|||
|
|
socket.setTimeout(timeoutMs);
|
|||
|
|
socket.on('connect', () => {
|
|||
|
|
const latency = Date.now() - start;
|
|||
|
|
socket.destroy();
|
|||
|
|
resolve({ alive: true, latency_ms: latency });
|
|||
|
|
});
|
|||
|
|
socket.on('timeout', () => {
|
|||
|
|
socket.destroy();
|
|||
|
|
resolve({ alive: false, latency_ms: null, error: 'timeout' });
|
|||
|
|
});
|
|||
|
|
socket.on('error', (err) => {
|
|||
|
|
socket.destroy();
|
|||
|
|
resolve({ alive: false, latency_ms: null, error: err.code || err.message });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
socket.connect(port, host);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 检查本地Xray进程 ──────────────────────
|
|||
|
|
_checkLocalXray() {
|
|||
|
|
try {
|
|||
|
|
const { execSync } = require('child_process');
|
|||
|
|
const result = execSync('pgrep -x xray', { encoding: 'utf8', timeout: 3000 });
|
|||
|
|
return result.trim().length > 0;
|
|||
|
|
} catch {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ═══ 接口1: heartbeat() — "我还活着" ═══
|
|||
|
|
async heartbeat() {
|
|||
|
|
const heartbeat = {
|
|||
|
|
module_id: this._moduleId,
|
|||
|
|
module_name: this._moduleName,
|
|||
|
|
alive: true,
|
|||
|
|
state: this._state,
|
|||
|
|
uptime_ms: Date.now() - new Date(this._startedAt).getTime(),
|
|||
|
|
timestamp: new Date().toISOString(),
|
|||
|
|
consecutive_errors: this._consecutiveErrors,
|
|||
|
|
last_success: this._lastSuccess,
|
|||
|
|
last_error: this._lastError,
|
|||
|
|
metrics: {
|
|||
|
|
total_nodes: this._nodes.length,
|
|||
|
|
live_nodes: this._liveNodes.length,
|
|||
|
|
dead_nodes: this._nodes.length - this._liveNodes.length,
|
|||
|
|
total_checks: this._requestCount,
|
|||
|
|
total_failures: this._failCount
|
|||
|
|
},
|
|||
|
|
nodes_summary: this._nodes.map(n => ({
|
|||
|
|
id: n.id,
|
|||
|
|
name: n.name,
|
|||
|
|
status: n.status,
|
|||
|
|
latency_ms: n.latency_ms,
|
|||
|
|
region: n.region
|
|||
|
|
}))
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 写入心跳文件
|
|||
|
|
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|||
|
|
fs.writeFileSync(HEARTBEAT_FILE, JSON.stringify(heartbeat, null, 2));
|
|||
|
|
|
|||
|
|
this._lastSuccess = new Date().toISOString();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ═══ 接口2: selfDiagnose() — "我知道我哪里不对" ═══
|
|||
|
|
async selfDiagnose() {
|
|||
|
|
console.log(`[ZY-CLOUD VPN] 🔍 开始诊断 (${this._nodes.length}个节点)...`);
|
|||
|
|
|
|||
|
|
// 重新发现节点(配置可能更新了)
|
|||
|
|
this._discoverNodes();
|
|||
|
|
|
|||
|
|
const results = [];
|
|||
|
|
const liveNodes = [];
|
|||
|
|
|
|||
|
|
for (const node of this._nodes) {
|
|||
|
|
node.total_checks++;
|
|||
|
|
this._requestCount++;
|
|||
|
|
|
|||
|
|
let alive = false;
|
|||
|
|
let latencyMs = null;
|
|||
|
|
|
|||
|
|
if (node.type === 'local') {
|
|||
|
|
// 本机: 检查Xray进程 + 端口
|
|||
|
|
const xrayRunning = this._checkLocalXray();
|
|||
|
|
const portProbe = await this._probePort('127.0.0.1', node.port, 3000);
|
|||
|
|
alive = xrayRunning && portProbe.alive;
|
|||
|
|
latencyMs = portProbe.alive ? portProbe.latency_ms : null;
|
|||
|
|
} else {
|
|||
|
|
// 远程: TCP端口探测
|
|||
|
|
const probe = await this._probePort(node.host, node.port, 5000);
|
|||
|
|
alive = probe.alive;
|
|||
|
|
latencyMs = probe.latency_ms;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新节点状态
|
|||
|
|
node.last_check = new Date().toISOString();
|
|||
|
|
if (alive) {
|
|||
|
|
node.status = 'online';
|
|||
|
|
node.latency_ms = latencyMs;
|
|||
|
|
node.consecutive_failures = 0;
|
|||
|
|
node.total_successes++;
|
|||
|
|
liveNodes.push(node);
|
|||
|
|
console.log(` ✅ ${node.name} — 在线 (${latencyMs}ms)`);
|
|||
|
|
} else {
|
|||
|
|
node.consecutive_failures++;
|
|||
|
|
node.latency_ms = null;
|
|||
|
|
this._failCount++;
|
|||
|
|
|
|||
|
|
if (node.consecutive_failures >= 3) {
|
|||
|
|
node.status = 'dead';
|
|||
|
|
console.log(` ❌ ${node.name} — 死亡 (连续${node.consecutive_failures}次失败)`);
|
|||
|
|
} else {
|
|||
|
|
node.status = 'degraded';
|
|||
|
|
console.log(` ⚠️ ${node.name} — 异常 (${node.consecutive_failures}次失败)`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
results.push({
|
|||
|
|
id: node.id,
|
|||
|
|
name: node.name,
|
|||
|
|
status: node.status,
|
|||
|
|
latency_ms: latencyMs,
|
|||
|
|
alive
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 按延迟排序存活节点(最快的排前面)
|
|||
|
|
liveNodes.sort((a, b) => (a.latency_ms || 9999) - (b.latency_ms || 9999));
|
|||
|
|
this._liveNodes = liveNodes;
|
|||
|
|
|
|||
|
|
// 应用学习数据优化排序
|
|||
|
|
this._applyLearnedOptimization();
|
|||
|
|
|
|||
|
|
// 写入活节点列表 (subscription-server-v2 读取此文件)
|
|||
|
|
const liveNodesData = {
|
|||
|
|
_comment: 'ZY-CLOUD VPN活模块动态生成 · 订阅服务读取此文件',
|
|||
|
|
_generated_by: 'zy-cloud-vpn.js · selfDiagnose()',
|
|||
|
|
updated_at: new Date().toISOString(),
|
|||
|
|
total_nodes: this._nodes.length,
|
|||
|
|
live_count: liveNodes.length,
|
|||
|
|
dead_count: this._nodes.length - liveNodes.length,
|
|||
|
|
nodes: liveNodes.map(n => ({
|
|||
|
|
id: n.id,
|
|||
|
|
name: n.name,
|
|||
|
|
host: n.host,
|
|||
|
|
port: n.port,
|
|||
|
|
pbk: n.pbk,
|
|||
|
|
sid: n.sid,
|
|||
|
|
region: n.region,
|
|||
|
|
latency_ms: n.latency_ms,
|
|||
|
|
status: n.status,
|
|||
|
|
specs: n.specs
|
|||
|
|
}))
|
|||
|
|
};
|
|||
|
|
fs.writeFileSync(LIVE_NODES_FILE, JSON.stringify(liveNodesData, null, 2));
|
|||
|
|
|
|||
|
|
// 诊断结果
|
|||
|
|
const diagnosis = {
|
|||
|
|
overall: liveNodes.length > 0 ? 'healthy' : 'critical',
|
|||
|
|
live: liveNodes.length,
|
|||
|
|
dead: this._nodes.length - liveNodes.length,
|
|||
|
|
results,
|
|||
|
|
diagnosed_at: new Date().toISOString()
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 根据诊断结果决定自我修复
|
|||
|
|
if (liveNodes.length === 0 && this._nodes.length > 0) {
|
|||
|
|
this._state = 'critical';
|
|||
|
|
await this.selfHeal({ type: 'all_nodes_dead', nodes: this._nodes });
|
|||
|
|
} else if (liveNodes.length < this._nodes.length) {
|
|||
|
|
this._state = 'degraded';
|
|||
|
|
const deadNodes = this._nodes.filter(n => n.status !== 'online');
|
|||
|
|
for (const deadNode of deadNodes) {
|
|||
|
|
if (deadNode.consecutive_failures >= 3) {
|
|||
|
|
await this.selfHeal({ type: 'node_dead', node: deadNode });
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
this._state = 'healthy';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log(`[ZY-CLOUD VPN] 诊断完成: ${liveNodes.length}/${this._nodes.length} 在线`);
|
|||
|
|
return diagnosis;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ═══ 接口3: selfHeal() — "我能自己修" ═══
|
|||
|
|
async selfHeal(problem) {
|
|||
|
|
console.log(`[ZY-CLOUD VPN] 🔧 尝试自我修复: ${problem.type}`);
|
|||
|
|
|
|||
|
|
switch (problem.type) {
|
|||
|
|
case 'node_dead': {
|
|||
|
|
const node = problem.node;
|
|||
|
|
if (node.type === 'local') {
|
|||
|
|
// 本机Xray: 尝试重启
|
|||
|
|
try {
|
|||
|
|
const { execSync } = require('child_process');
|
|||
|
|
console.log(` 🔄 重启本机Xray...`);
|
|||
|
|
execSync('systemctl restart xray', { encoding: 'utf8', timeout: 15000 });
|
|||
|
|
console.log(` ✅ 本机Xray已重启`);
|
|||
|
|
|
|||
|
|
// 等2秒后重新检查
|
|||
|
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|||
|
|
const probe = await this._probePort('127.0.0.1', node.port, 3000);
|
|||
|
|
if (probe.alive) {
|
|||
|
|
node.status = 'online';
|
|||
|
|
node.latency_ms = probe.latency_ms;
|
|||
|
|
node.consecutive_failures = 0;
|
|||
|
|
console.log(` ✅ 自我修复成功!`);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
} catch (err) {
|
|||
|
|
console.error(` ❌ 重启失败: ${err.message}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 远程节点: 只能标记为离线,从订阅中移除
|
|||
|
|
// 未来: 通过SSH自动重启远程Xray (需要ZY_FACE_KEY)
|
|||
|
|
console.log(` ⚠️ ${node.name} 无法远程修复,已从活节点列表移除`);
|
|||
|
|
console.log(` ⚠️ 用户流量自动切换到其他在线节点`);
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
case 'all_nodes_dead': {
|
|||
|
|
console.log(` 🚨 所有节点不可用!尝试修复本机节点...`);
|
|||
|
|
const localNode = this._nodes.find(n => n.type === 'local');
|
|||
|
|
if (localNode) {
|
|||
|
|
const healed = await this.selfHeal({ type: 'node_dead', node: localNode });
|
|||
|
|
if (healed) return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 自动修复失败 → 第2次重试: 调用AI推理
|
|||
|
|
console.log(' 🤖 自动修复失败,启动AI推理...');
|
|||
|
|
const aiResult = await this.consultAI({
|
|||
|
|
type: 'all_nodes_dead',
|
|||
|
|
nodes: this._nodes.map(n => ({
|
|||
|
|
id: n.id, status: n.status, host: n.host, port: n.port,
|
|||
|
|
consecutive_failures: n.consecutive_failures
|
|||
|
|
})),
|
|||
|
|
attempted_fix: 'xray_restart_failed'
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (aiResult) {
|
|||
|
|
// AI给出了建议,记录并尝试第3次修复
|
|||
|
|
console.log(` 🤖 AI建议已记录,再次尝试修复...`);
|
|||
|
|
if (localNode) {
|
|||
|
|
const retryHeal = await this.selfHeal({ type: 'node_dead', node: localNode });
|
|||
|
|
if (retryHeal) return true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 第3次: 通知冰朔
|
|||
|
|
await this.alertZhuyuan({
|
|||
|
|
level: 'critical',
|
|||
|
|
message: '所有VPN节点不可用!自动修复+AI推理均失败,需要人工干预',
|
|||
|
|
nodes: this._nodes.map(n => ({ id: n.id, status: n.status })),
|
|||
|
|
ai_advice: aiResult ? aiResult.content.slice(0, 500) : '(AI推理未返回结果)',
|
|||
|
|
attempted_fixes: ['xray_restart', 'ai_diagnosis']
|
|||
|
|
});
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
default:
|
|||
|
|
console.log(` ⚠️ 未知问题类型: ${problem.type}`);
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ═══ 接口4: alertZhuyuan() — "铸渊,我需要帮助" ═══
|
|||
|
|
async alertZhuyuan(alert) {
|
|||
|
|
const alertData = {
|
|||
|
|
module_id: this._moduleId,
|
|||
|
|
module_name: this._moduleName,
|
|||
|
|
level: alert.level || 'warning',
|
|||
|
|
message: alert.message,
|
|||
|
|
state: this._state,
|
|||
|
|
timestamp: new Date().toISOString(),
|
|||
|
|
details: alert,
|
|||
|
|
attempted_fixes: alert.attempted_fixes || [],
|
|||
|
|
nodes_status: this._nodes.map(n => ({
|
|||
|
|
id: n.id,
|
|||
|
|
status: n.status,
|
|||
|
|
consecutive_failures: n.consecutive_failures
|
|||
|
|
}))
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 通道1: 本地告警文件
|
|||
|
|
const alertFile = path.join(DATA_DIR, 'zy-cloud-vpn-alert.json');
|
|||
|
|
fs.writeFileSync(alertFile, JSON.stringify(alertData, null, 2));
|
|||
|
|
console.log(` 🚨 告警已记录: ${alertFile}`);
|
|||
|
|
|
|||
|
|
// 通道2: 控制台输出 (PM2日志可见)
|
|||
|
|
console.error(`🚨🚨🚨 [ZY-CLOUD VPN 告警] ${alert.level}: ${alert.message}`);
|
|||
|
|
|
|||
|
|
// 未来通道3: COS桶 → Notion SYSLOG → 冰朔
|
|||
|
|
// 未来通道4: 邮件告警 (send-subscription.js)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ═══ 接口5: learnFromRun() — "我下次会做得更好" ═══
|
|||
|
|
async learnFromRun() {
|
|||
|
|
const now = new Date();
|
|||
|
|
const hour = now.getHours();
|
|||
|
|
const timeSlot = hour < 6 ? 'night' : hour < 12 ? 'morning' : hour < 18 ? 'afternoon' : 'evening';
|
|||
|
|
|
|||
|
|
// 记录当前各节点的延迟
|
|||
|
|
for (const node of this._nodes) {
|
|||
|
|
if (node.status === 'online' && node.latency_ms !== null) {
|
|||
|
|
if (!this._learnDb.latency_history[node.id]) {
|
|||
|
|
this._learnDb.latency_history[node.id] = [];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this._learnDb.latency_history[node.id].push({
|
|||
|
|
latency_ms: node.latency_ms,
|
|||
|
|
time_slot: timeSlot,
|
|||
|
|
timestamp: now.toISOString()
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 只保留最近200条
|
|||
|
|
if (this._learnDb.latency_history[node.id].length > 200) {
|
|||
|
|
this._learnDb.latency_history[node.id] =
|
|||
|
|
this._learnDb.latency_history[node.id].slice(-200);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新节点可靠性评分
|
|||
|
|
for (const node of this._nodes) {
|
|||
|
|
if (node.total_checks > 0) {
|
|||
|
|
const reliability = node.total_successes / node.total_checks;
|
|||
|
|
this._learnDb.reliability[node.id] = {
|
|||
|
|
score: parseFloat(reliability.toFixed(4)),
|
|||
|
|
total_checks: node.total_checks,
|
|||
|
|
total_successes: node.total_successes,
|
|||
|
|
updated_at: now.toISOString()
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 计算每个时段的最优节点
|
|||
|
|
for (const slot of ['night', 'morning', 'afternoon', 'evening']) {
|
|||
|
|
const avgLatencies = {};
|
|||
|
|
for (const [nodeId, history] of Object.entries(this._learnDb.latency_history)) {
|
|||
|
|
const slotRecords = history.filter(r => r.time_slot === slot);
|
|||
|
|
if (slotRecords.length > 0) {
|
|||
|
|
const avg = slotRecords.reduce((s, r) => s + r.latency_ms, 0) / slotRecords.length;
|
|||
|
|
avgLatencies[nodeId] = parseFloat(avg.toFixed(1));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (Object.keys(avgLatencies).length > 0) {
|
|||
|
|
const sorted = Object.entries(avgLatencies).sort((a, b) => a[1] - b[1]);
|
|||
|
|
this._learnDb.best_node_by_time[slot] = {
|
|||
|
|
best: sorted[0][0],
|
|||
|
|
avg_latency_ms: sorted[0][1],
|
|||
|
|
ranking: sorted.map(([id, lat]) => ({ id, avg_ms: lat }))
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this._learnDb.updated_at = now.toISOString();
|
|||
|
|
this._learnDb.learn_count = (this._learnDb.learn_count || 0) + 1;
|
|||
|
|
|
|||
|
|
// 保存学习数据
|
|||
|
|
this._saveLearnDb();
|
|||
|
|
|
|||
|
|
console.log(`[ZY-CLOUD VPN] 📚 学习完成 (第${this._learnDb.learn_count}次)`);
|
|||
|
|
const currentBest = this._learnDb.best_node_by_time[timeSlot];
|
|||
|
|
if (currentBest) {
|
|||
|
|
console.log(` 当前时段(${timeSlot})最优: ${currentBest.best} (${currentBest.avg_latency_ms}ms)`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ═══ 接口6: consultAI() — "我请教AI" (V3新增) ═══
|
|||
|
|
// 遇到复杂问题时调用LLM路由器进行深度推理
|
|||
|
|
// 三次重试机制:
|
|||
|
|
// 第1次: selfHeal 自动修复
|
|||
|
|
// 第2次: consultAI 调用LLM
|
|||
|
|
// 第3次: alertZhuyuan 邮件通知冰朔
|
|||
|
|
async consultAI(context) {
|
|||
|
|
let llmRouter;
|
|||
|
|
try {
|
|||
|
|
llmRouter = require('./llm-router');
|
|||
|
|
} catch {
|
|||
|
|
console.log('[ZY-CLOUD VPN] ⚠️ LLM路由器未加载,跳过AI推理');
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const prompt = `你是光湖语言世界VPN系统的AI诊断助手。
|
|||
|
|
当前系统状态:
|
|||
|
|
- 模块: ${this._moduleName}
|
|||
|
|
- 状态: ${this._state}
|
|||
|
|
- 总节点: ${this._nodes.length}
|
|||
|
|
- 存活节点: ${this._liveNodes.length}
|
|||
|
|
- 连续错误: ${this._consecutiveErrors}
|
|||
|
|
|
|||
|
|
问题上下文:
|
|||
|
|
${JSON.stringify(context, null, 2).slice(0, 2000)}
|
|||
|
|
|
|||
|
|
请分析问题原因并给出具体的修复建议。要求:
|
|||
|
|
1. 判断是网络层面还是服务层面的问题
|
|||
|
|
2. 给出可执行的修复步骤
|
|||
|
|
3. 评估问题严重程度(low/medium/high/critical)
|
|||
|
|
回答要简洁明确。`;
|
|||
|
|
|
|||
|
|
console.log('[ZY-CLOUD VPN] 🤖 调用AI推理...');
|
|||
|
|
const result = await llmRouter.callLLM(prompt, {
|
|||
|
|
systemPrompt: '你是光湖语言世界VPN系统的AI诊断引擎。你负责分析网络和代理服务的异常,给出精准的修复建议。',
|
|||
|
|
maxTokens: 800,
|
|||
|
|
timeout: 45000
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (result) {
|
|||
|
|
console.log(`[ZY-CLOUD VPN] 🤖 AI推理完成 (模型: ${result.model})`);
|
|||
|
|
console.log(` 建议: ${result.content.slice(0, 200)}...`);
|
|||
|
|
|
|||
|
|
// 记录AI咨询历史
|
|||
|
|
this._history.push({
|
|||
|
|
type: 'consultAI',
|
|||
|
|
model: result.model,
|
|||
|
|
context_summary: context.type || 'unknown',
|
|||
|
|
response_preview: result.content.slice(0, 100),
|
|||
|
|
timestamp: new Date().toISOString()
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 只保留最近20条历史
|
|||
|
|
if (this._history.length > 20) {
|
|||
|
|
this._history = this._history.slice(-20);
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
console.log('[ZY-CLOUD VPN] ⚠️ AI推理未返回结果');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return result;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 应用学习数据优化节点排序 ────────────────
|
|||
|
|
_applyLearnedOptimization() {
|
|||
|
|
const now = new Date();
|
|||
|
|
const hour = now.getHours();
|
|||
|
|
const timeSlot = hour < 6 ? 'night' : hour < 12 ? 'morning' : hour < 18 ? 'afternoon' : 'evening';
|
|||
|
|
|
|||
|
|
const bestData = this._learnDb.best_node_by_time[timeSlot];
|
|||
|
|
if (!bestData || !bestData.ranking) return;
|
|||
|
|
|
|||
|
|
// 根据学习到的最优排序调整节点顺序
|
|||
|
|
// 但前提是延迟差异>10ms(否则保持原排序)
|
|||
|
|
const ranking = bestData.ranking;
|
|||
|
|
this._liveNodes.sort((a, b) => {
|
|||
|
|
const rankA = ranking.findIndex(r => r.id === a.id);
|
|||
|
|
const rankB = ranking.findIndex(r => r.id === b.id);
|
|||
|
|
if (rankA === -1 && rankB === -1) return 0;
|
|||
|
|
if (rankA === -1) return 1;
|
|||
|
|
if (rankB === -1) return -1;
|
|||
|
|
return rankA - rankB;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 学习数据库 ─────────────────────────────
|
|||
|
|
_loadLearnDb() {
|
|||
|
|
try {
|
|||
|
|
return JSON.parse(fs.readFileSync(LEARN_DB_FILE, 'utf8'));
|
|||
|
|
} catch {
|
|||
|
|
return {
|
|||
|
|
version: '1.0',
|
|||
|
|
created_at: new Date().toISOString(),
|
|||
|
|
updated_at: new Date().toISOString(),
|
|||
|
|
learn_count: 0,
|
|||
|
|
latency_history: {},
|
|||
|
|
reliability: {},
|
|||
|
|
best_node_by_time: {}
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_saveLearnDb() {
|
|||
|
|
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|||
|
|
fs.writeFileSync(LEARN_DB_FILE, JSON.stringify(this._learnDb, null, 2));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 获取活节点列表(供外部调用)────────────
|
|||
|
|
getLiveNodes() {
|
|||
|
|
return this._liveNodes;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
getLearnSummary() {
|
|||
|
|
return {
|
|||
|
|
learn_count: this._learnDb.learn_count,
|
|||
|
|
reliability: this._learnDb.reliability,
|
|||
|
|
best_by_time: this._learnDb.best_node_by_time
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ═══════════════════════════════════════════════
|
|||
|
|
// HTTP 管理端口 (3804)
|
|||
|
|
// 供 subscription-server-v2 和外部查询
|
|||
|
|
// ═══════════════════════════════════════════════
|
|||
|
|
const MGMT_PORT = process.env.ZY_CLOUD_VPN_PORT || 3804;
|
|||
|
|
const vpnModule = new ZyCloudVpn();
|
|||
|
|
|
|||
|
|
const mgmtServer = http.createServer((req, res) => {
|
|||
|
|
try {
|
|||
|
|
const parsedUrl = require('url').parse(req.url, true);
|
|||
|
|
const pathname = parsedUrl.pathname;
|
|||
|
|
|
|||
|
|
// ── 活节点列表 (subscription-server-v2 调用) ──
|
|||
|
|
if (pathname === '/nodes' && req.method === 'GET') {
|
|||
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|||
|
|
res.end(JSON.stringify({
|
|||
|
|
nodes: vpnModule.getLiveNodes().map(n => ({
|
|||
|
|
id: n.id, name: n.name, host: n.host, port: n.port,
|
|||
|
|
pbk: n.pbk, sid: n.sid, region: n.region,
|
|||
|
|
latency_ms: n.latency_ms, status: n.status, specs: n.specs
|
|||
|
|
})),
|
|||
|
|
updated_at: new Date().toISOString()
|
|||
|
|
}));
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 注册节点(像接路由器一样插入)──
|
|||
|
|
// POST /register { id, name, host, port, pbk, sid, region, specs }
|
|||
|
|
if (pathname === '/register' && req.method === 'POST') {
|
|||
|
|
let body = '';
|
|||
|
|
req.on('data', chunk => { body += chunk; });
|
|||
|
|
req.on('end', () => {
|
|||
|
|
try {
|
|||
|
|
const nodeData = JSON.parse(body);
|
|||
|
|
const node = vpnModule.registerNode(nodeData);
|
|||
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|||
|
|
res.end(JSON.stringify({
|
|||
|
|
status: 'registered',
|
|||
|
|
node_id: node.id,
|
|||
|
|
name: node.name,
|
|||
|
|
message: `✅ 节点已注册: ${node.name} (${node.host}:${node.port})`
|
|||
|
|
}));
|
|||
|
|
} catch (err) {
|
|||
|
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|||
|
|
res.end(JSON.stringify({ error: true, message: err.message }));
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 注销节点(拔掉路由器)──
|
|||
|
|
// DELETE /unregister?id=xxx 或 POST /unregister { id: "xxx" }
|
|||
|
|
if (pathname === '/unregister') {
|
|||
|
|
const handleUnregister = (nodeId) => {
|
|||
|
|
try {
|
|||
|
|
const removed = vpnModule.unregisterNode(nodeId);
|
|||
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|||
|
|
res.end(JSON.stringify({
|
|||
|
|
status: 'unregistered',
|
|||
|
|
node_id: nodeId,
|
|||
|
|
message: `✅ 节点已注销: ${removed.name}`
|
|||
|
|
}));
|
|||
|
|
} catch (err) {
|
|||
|
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|||
|
|
res.end(JSON.stringify({ error: true, message: err.message }));
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
if (req.method === 'DELETE' || req.method === 'GET') {
|
|||
|
|
handleUnregister(parsedUrl.query.id);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (req.method === 'POST') {
|
|||
|
|
let body = '';
|
|||
|
|
req.on('data', chunk => { body += chunk; });
|
|||
|
|
req.on('end', () => {
|
|||
|
|
try { handleUnregister(JSON.parse(body).id); }
|
|||
|
|
catch (err) { res.writeHead(400); res.end(err.message); }
|
|||
|
|
});
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 接收HLDP heartbeat(自动注册VPN节点)──
|
|||
|
|
// POST /hldp/v3/heartbeat { hldp_v: "3.0", msg_type: "heartbeat", ... }
|
|||
|
|
if (pathname === '/hldp/v3/heartbeat' && req.method === 'POST') {
|
|||
|
|
let body = '';
|
|||
|
|
req.on('data', chunk => { body += chunk; });
|
|||
|
|
req.on('end', () => {
|
|||
|
|
try {
|
|||
|
|
const hldpMsg = JSON.parse(body);
|
|||
|
|
const node = vpnModule.handleHldpHeartbeat(hldpMsg);
|
|||
|
|
|
|||
|
|
// 发送HLDP ack回执
|
|||
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|||
|
|
res.end(JSON.stringify({
|
|||
|
|
hldp_v: '3.0',
|
|||
|
|
msg_id: `HLDP-ZY-${new Date().toISOString().slice(0,10).replace(/-/g,'')}-ACK`,
|
|||
|
|
msg_type: 'ack',
|
|||
|
|
sender: { id: 'ICE-GL-ZY001', name: '铸渊', role: 'guardian' },
|
|||
|
|
receiver: hldpMsg.sender,
|
|||
|
|
timestamp: new Date().toISOString(),
|
|||
|
|
priority: 'routine',
|
|||
|
|
payload: {
|
|||
|
|
intent: node ? 'VPN节点心跳已接收·已注册' : '心跳已接收·非VPN节点',
|
|||
|
|
data: {
|
|||
|
|
ref_msg_id: hldpMsg.msg_id,
|
|||
|
|
status: node ? 'registered' : 'ignored',
|
|||
|
|
node_id: node ? node.id : null
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}));
|
|||
|
|
} catch (err) {
|
|||
|
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|||
|
|
res.end(JSON.stringify({ error: true, message: err.message }));
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 注册表(所有注册过的节点,含离线)──
|
|||
|
|
if (pathname === '/registry' && req.method === 'GET') {
|
|||
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|||
|
|
res.end(JSON.stringify({
|
|||
|
|
registered: vpnModule.listRegisteredNodes(),
|
|||
|
|
total: vpnModule.listRegisteredNodes().length,
|
|||
|
|
updated_at: new Date().toISOString()
|
|||
|
|
}));
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 心跳状态 ──
|
|||
|
|
if (pathname === '/health' || pathname === '/heartbeat') {
|
|||
|
|
try {
|
|||
|
|
const hb = JSON.parse(fs.readFileSync(HEARTBEAT_FILE, 'utf8'));
|
|||
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|||
|
|
res.end(JSON.stringify(hb));
|
|||
|
|
} catch {
|
|||
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|||
|
|
res.end(JSON.stringify({ status: 'starting', module: vpnModule._moduleId }));
|
|||
|
|
}
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 学习数据 ──
|
|||
|
|
if (pathname === '/learn') {
|
|||
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|||
|
|
res.end(JSON.stringify(vpnModule.getLearnSummary()));
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 完整状态 ──
|
|||
|
|
if (pathname === '/status') {
|
|||
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|||
|
|
res.end(JSON.stringify({
|
|||
|
|
module: vpnModule._moduleId,
|
|||
|
|
brand: '光湖语言世界 — 冰朔开发维护',
|
|||
|
|
version: '3.0.0',
|
|||
|
|
state: vpnModule._state,
|
|||
|
|
started_at: vpnModule._startedAt,
|
|||
|
|
uptime_seconds: Math.floor((Date.now() - new Date(vpnModule._startedAt).getTime()) / 1000),
|
|||
|
|
total_nodes: vpnModule._nodes.length,
|
|||
|
|
live_nodes: vpnModule._liveNodes.length,
|
|||
|
|
registered_nodes: vpnModule.listRegisteredNodes().length,
|
|||
|
|
nodes: vpnModule._nodes.map(n => ({
|
|||
|
|
id: n.id, name: n.name, status: n.status,
|
|||
|
|
latency_ms: n.latency_ms, consecutive_failures: n.consecutive_failures,
|
|||
|
|
reliability: n.total_checks > 0
|
|||
|
|
? parseFloat((n.total_successes / n.total_checks).toFixed(4))
|
|||
|
|
: null,
|
|||
|
|
last_check: n.last_check
|
|||
|
|
})),
|
|||
|
|
learn: vpnModule.getLearnSummary(),
|
|||
|
|
endpoints: {
|
|||
|
|
nodes: 'GET /nodes — 活节点列表',
|
|||
|
|
register: 'POST /register — 注册新节点',
|
|||
|
|
unregister: 'POST /unregister — 注销节点',
|
|||
|
|
hldp_heartbeat: 'POST /hldp/v3/heartbeat — HLDP心跳(自动注册)',
|
|||
|
|
registry: 'GET /registry — 注册表',
|
|||
|
|
health: 'GET /health — 心跳',
|
|||
|
|
learn: 'GET /learn — 学习数据',
|
|||
|
|
status: 'GET /status — 完整状态'
|
|||
|
|
},
|
|||
|
|
updated_at: new Date().toISOString()
|
|||
|
|
}));
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|||
|
|
res.end('Not Found');
|
|||
|
|
} catch (err) {
|
|||
|
|
console.error('管理端口错误:', err.message);
|
|||
|
|
if (!res.headersSent) {
|
|||
|
|
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|||
|
|
}
|
|||
|
|
res.end('Internal Error');
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
mgmtServer.listen(MGMT_PORT, '127.0.0.1', () => {
|
|||
|
|
console.log(`💪 ZY-CLOUD VPN 管理端口: http://127.0.0.1:${MGMT_PORT}`);
|
|||
|
|
console.log(` ── 查询接口 ──`);
|
|||
|
|
console.log(` GET /nodes — 活节点列表 (订阅服务调用)`);
|
|||
|
|
console.log(` GET /health — 心跳状态`);
|
|||
|
|
console.log(` GET /learn — 学习数据`);
|
|||
|
|
console.log(` GET /status — 完整状态`);
|
|||
|
|
console.log(` GET /registry — 注册表`);
|
|||
|
|
console.log(` ── 注册接口 (像路由器一样插入) ──`);
|
|||
|
|
console.log(` POST /register — 注册新VPN节点`);
|
|||
|
|
console.log(` POST /unregister — 注销VPN节点`);
|
|||
|
|
console.log(` POST /hldp/v3/heartbeat — HLDP心跳(自动注册)`);
|
|||
|
|
|
|||
|
|
// 启动活模块生命周期
|
|||
|
|
vpnModule.startLifeCycle();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Graceful shutdown
|
|||
|
|
function gracefulShutdown(signal) {
|
|||
|
|
console.log(`\n${signal} received. Shutting down ZY-CLOUD VPN...`);
|
|||
|
|
vpnModule.destroy();
|
|||
|
|
const forceExit = setTimeout(() => process.exit(1), 5000);
|
|||
|
|
mgmtServer.close(() => {
|
|||
|
|
clearTimeout(forceExit);
|
|||
|
|
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);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 导出 (供测试和其他模块使用)
|
|||
|
|
module.exports = { ZyCloudVpn, LivingModule };
|