121 lines
11 KiB
JavaScript

// # 光湖服务器主控台 API
// # Guanghu Server Console v3.2 — with Pool Registry API
// # 部署: guanghulab.com/console/ · SG-001:3920
// # 版权: 国作登字-2026-A-00037559 · TCS-0002∞
const express = require('express');
const http = require('http');
const crypto = require('crypto');
const path = require('path');
const fs = require('fs');
const app = express();
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));
const DATA_DIR = '/opt/zhuyuan/data';
try { fs.mkdirSync(DATA_DIR, { recursive: true }); } catch(e) {}
// 全服务器配置 — 冰朔6台 + 企业3台
const SERVERS = [
{ code: "BS-GZ-006", name: "广州 · 代码仓库", ip: "43.139.217.141", key: "zy_gtw_4a155446b7acc09fe46a2a29972c0ca5d9954da19c35dc23", port: 3910 },
{ code: "BS-SG-001", name: "新加坡 · 铸渊大脑", ip: "43.156.237.110", key: "zy_gtw_74d30f54fa8b5581514569ee7874def57861a31ccb2be8c9", port: 3911 },
{ code: "BS-SG-002", name: "新加坡 · 面孔", ip: "43.134.16.246", key: "zy_gtw_d1f6d2b8cb4ea44292bd036e4dd03a70745ea502d4a3ae40", port: 3910 },
{ code: "BS-SG-003", name: "新加坡 · 中继", ip: "43.153.193.169", key: "zy_gtw_d7c033d8ae992ffc9dd3e0dd5fe332dace0b644c0ae7c847", port: 3910 },
{ code: "ZY-SG-006", name: "新加坡 · 语料", ip: "43.153.203.105", key: "zy_gtw_fff861a2fe40cbdc366ee21f218023be8b1c182f66c852d3", port: 3910 },
{ code: "BS-SH-005", name: "上海 · 国内节点", ip: "124.223.10.33", key: "zy_gtw_b1045de5ddfd7832e3c53349d9c896f054b85a4a9ece22f9", port: 3910 },
{ code: "AW-GZ-001", name: "企业主服务器 · 广州", ip: "43.139.251.175", key: "zy_gtw_cab20e8c1b957aaad7507bf542231521bba30665461aa540", port: 3910 },
{ code: "AW-SH-002", name: "企业灾备 · 上海", ip: "124.222.54.198", key: "zy_gtw_242f1dd3b14c1260fb01196083abde3dcb85e53532d27666", port: 3910 },
{ code: "AW-GZ-003", name: "企业灾备 · 广州", ip: "119.29.181.132", key: "zy_gtw_8c9a8b439af823690ae6ad57f52734771ff39493ab986032", port: 3910 },
];
const tempKeys = {};
const authQueue = [];
async function pingServer(srv) {
const start = Date.now();
try {
const data = JSON.stringify({});
const opts = { hostname: srv.ip, port: srv.port, path: '/health', method: 'POST', headers: { 'Authorization': 'Bearer ' + srv.key, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) }, timeout: 5000 };
const result = await new Promise((resolve) => {
const req = http.request(opts, (res) => { let body = ''; res.on('data', c => body += c); res.on('end', () => resolve({ ok: true, status: res.statusCode, body })); });
req.on('error', (e) => resolve({ ok: false, error: e.message }));
req.on('timeout', () => { req.destroy(); resolve({ ok: false, error: 'timeout' }); });
req.write(data); req.end();
});
return { code: srv.code, name: srv.name, ip: srv.ip, alive: result.ok && result.status === 200, latency: Date.now() - start, error: result.error || null, last_seen: new Date().toISOString() };
} catch (e) { return { code: srv.code, name: srv.name, ip: srv.ip, alive: false, latency: 0, error: e.message, last_seen: new Date().toISOString() }; }
}
function checkAgentAuth(req) { const a = req.headers.authorization || ''; const t = a.replace('Bearer ', ''); if (!t) return false; try { const d = fs.readFileSync('/tmp/zhuyuan-keys.json', 'utf-8'); return JSON.parse(d || '[]').some(k => k.value === t); } catch(e) { return false; } }
// ========== 基础 API ==========
app.get('/api/servers', async (req, res) => { const r = await Promise.all(SERVERS.map(s => pingServer(s))); res.json({ ok: true, servers: r, timestamp: new Date().toISOString() }); });
app.post('/api/knock', (req, res) => {
const { target, reason } = req.body; if (!target) return res.status(400).json({ error: '缺少 target' });
const s = SERVERS.find(s => s.code === target || s.name.includes(target)); if (!s) return res.status(404).json({ error: '未找到' });
const rid = 'req_' + crypto.randomBytes(8).toString('hex'); authQueue.push({ id: rid, target: s.code, target_name: s.name, reason: reason || '未说明', status: 'pending', created_at: new Date().toISOString(), temp_key: null });
res.json({ ok: true, request_id: rid, message: '等待冰朔确认' });
});
app.get('/api/pending', (req, res) => { res.json({ ok: true, requests: authQueue.filter(r => r.status === 'pending') }); });
app.post('/api/authorize', (req, res) => {
const { request_id } = req.body; const e = authQueue.find(r => r.id === request_id && r.status === 'pending'); if (!e) return res.status(404).json({ error: '未找到' });
const tk = 'tmp_' + crypto.randomBytes(24).toString('hex'); e.status = 'approved'; e.temp_key = tk; e.approved_at = new Date().toISOString();
tempKeys[tk] = { server: e.target, expires_at: Date.now() + 5*60*1000, request_id }; res.json({ ok: true, temp_key: tk, expires_in: '5分钟' });
});
app.post('/api/reject', (req, res) => { const e = authQueue.find(r => r.id === req.body.request_id && r.status === 'pending'); if (e) e.status = 'rejected'; res.json({ ok: true }); });
app.get('/api/verify-key/:key', (req, res) => { const e = tempKeys[req.params.key]; if (!e) return res.json({ ok: false, valid: false }); if (Date.now() > e.expires_at) { delete tempKeys[req.params.key]; return res.json({ ok: false, valid: false }); } res.json({ ok: true, valid: true, server: e.server }); });
app.post('/api/exec', async (req, res) => {
const { temp_key, target, cmd } = req.body; if (!temp_key || !target || !cmd) return res.status(400).json({ error: '缺少参数' });
const ke = tempKeys[temp_key]; if (!ke || Date.now() > ke.expires_at) return res.status(401).json({ error: '密钥无效' });
const s = SERVERS.find(s => s.code === target); if (!s) return res.status(404).json({ error: '未找到' });
try {
const d = JSON.stringify({ cmd }); const o = { hostname: s.ip, port: s.port, path: '/exec', method: 'POST', headers: { 'Authorization': 'Bearer ' + s.key, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(d) }, timeout: 30000 };
const r = await new Promise((resolve) => { const req = http.request(o, (r) => { let b = ''; r.on('data', c => b += c); r.on('end', () => { try { resolve(JSON.parse(b)); } catch(e) { resolve({ error: b }); } }); }); req.on('error', (e) => resolve({ error: e.message })); req.write(d); req.end(); });
res.json({ ok: true, result: r });
} catch (e) { res.status(500).json({ error: e.message }); }
});
// ========== 节点池 API (v3.2) ==========
const POOL_REGISTRY_FILE = DATA_DIR + '/pool-registry.json';
const JOIN_TOKENS_FILE = DATA_DIR + '/join-tokens.json';
function loadPoolRegistry() { try { if (fs.existsSync(POOL_REGISTRY_FILE)) return JSON.parse(fs.readFileSync(POOL_REGISTRY_FILE, 'utf-8')); } catch(e) {} return { active: {}, pending: {}, history: [] }; }
function savePoolRegistry(r) { r.updated_at = new Date().toISOString(); try { fs.mkdirSync(DATA_DIR, { recursive: true }); } catch(e) {} fs.writeFileSync(POOL_REGISTRY_FILE, JSON.stringify(r, null, 2)); }
function loadJoinTokens() { try { if (fs.existsSync(JOIN_TOKENS_FILE)) return JSON.parse(fs.readFileSync(JOIN_TOKENS_FILE, 'utf-8')); } catch(e) {} return { tokens: [] }; }
function saveJoinTokens(d) { try { fs.mkdirSync(DATA_DIR, { recursive: true }); } catch(e) {} fs.writeFileSync(JOIN_TOKENS_FILE, JSON.stringify(d, null, 2)); }
function sha256(s) { return crypto.createHash('sha256').update(s).digest('hex'); }
app.post('/api/pool/join', (req, res) => {
const { join_token, node_code, node_name, side, team_id, operator_name, hardware, network } = req.body;
if (!join_token || !node_code || !side) return res.status(400).json({ ok: false, error: '缺少参数' });
const jt = loadJoinTokens(); const th = sha256(join_token); const mt = jt.tokens.find(t => t.token_hash === th);
if (!mt) return res.status(401).json({ ok: false, error: '加入令牌无效' });
if (mt.used >= (mt.max_uses || 1)) return res.status(401).json({ ok: false, error: '令牌已用' });
const r = loadPoolRegistry(); if (r.active[node_code]) return res.status(409).json({ ok: false, error: '节点已存在' });
const rid = 'reg_' + crypto.randomBytes(8).toString('hex');
r.pending[node_code] = { registration_id: rid, node_code, node_name: node_name || node_code, side, team_id: team_id || null, operator_name: operator_name || '未知', hardware: hardware || {}, network: network || {}, status: 'pending', created_at: new Date().toISOString(), token_id: mt.token_id };
mt.used = (mt.used || 0) + 1; saveJoinTokens(jt); savePoolRegistry(r);
res.json({ ok: true, status: 'pending', registration_id: rid, node_code, poll_interval_seconds: 15 });
});
app.get('/api/pool/join-status/:rid', (req, res) => {
const r = loadPoolRegistry();
for (const [c, n] of Object.entries(r.active)) { if (n.registration_id === req.params.rid) return res.json({ ok: true, status: 'approved', node_code: c, pool_token: n.pool_token, report_endpoint: 'https://43.156.237.110:3920/api/pool/report', side: n.side }); }
for (const [c, n] of Object.entries(r.pending)) { if (n.registration_id === req.params.rid) return res.json({ ok: true, status: 'pending', node_code: c }); }
res.status(404).json({ ok: false });
});
app.post('/api/pool/approve', (req, res) => {
const { registration_id, action } = req.body; if (!registration_id || !action) return res.status(400).json({ ok: false });
const r = loadPoolRegistry(); let f = null, fc = null;
for (const [c, n] of Object.entries(r.pending)) { if (n.registration_id === registration_id) { f = n; fc = c; break; } }
if (!f) return res.status(404).json({ ok: false });
if (action === 'approve') { const pt = 'zyp_' + crypto.randomBytes(16).toString('hex'); r.active[fc] = { node_code: fc, node_name: f.node_name, side: f.side, team_id: f.team_id, ip: f.network?.public_ip || '', pool_token: pt, joined_at: new Date().toISOString(), hardware: f.hardware || {} }; delete r.pending[fc]; savePoolRegistry(r); res.json({ ok: true, node_code: fc, pool_token: pt }); }
else { r.history.push({ ...f, status: 'rejected', rejected_at: new Date().toISOString() }); delete r.pending[fc]; savePoolRegistry(r); res.json({ ok: true, action: 'rejected' }); }
});
app.get('/api/pool/tokens', (req, res) => { const r = loadPoolRegistry(); res.json({ ok: true, nodes: Object.entries(r.active).map(([c, n]) => ({ node_code: c, node_name: n.node_name, side: n.side, team_id: n.team_id, hardware: n.hardware })) }); });
app.delete('/api/pool/node/:code', (req, res) => { const r = loadPoolRegistry(); if (!r.active[req.params.code]) return res.status(404).json({ ok: false }); r.history.push({ ...r.active[req.params.code], status: 'removed', removed_at: new Date().toISOString() }); delete r.active[req.params.code]; savePoolRegistry(r); res.json({ ok: true }); });
let cachedPoolStatus = {};
const PORT = process.env.CONSOLE_PORT || 3920;
app.listen(PORT, '127.0.0.1', () => { console.log('光湖主控台 v3.2 · ' + SERVERS.length + '台 | SG-001:' + PORT); });