变更: - server.js v3.7: +HL-SG-001/HL-CN-001 (14台) + /api/tech-dashboard端点 - tech.html: Awen技术主控台独立前端(琥珀色主题) - 域分组: ice-core(冰朔8台) + awen-team(企业3) + awen-personal(Awen2) + yeye(页页1) - 权限标识: 冰朔侧只读,团队侧全权 - 操作日志按域标记+签到可视化 数据流: tech.html → /api/tech-dashboard → signinCache+opLogs 部署: guanghulab.com/console/tech (冰朔侧) + guanghuzhiqiu.top (Awen侧)
475 lines
27 KiB
JavaScript
475 lines
27 KiB
JavaScript
// # 光湖服务器主控台 API v3.7
|
||
// # Guanghu Server Console v3.7 — 14台(冰6+企3+之2+页1+Awen2)
|
||
// # 部署: guanghulab.com/console/ · GZ-006:3920(主) + SG-001:3920(备)
|
||
// # 版权: 国作登字-2026-A-00037559 · TCS-0002∞
|
||
// # v3.7: +Awen技术主控视图 /api/tech-dashboard + HL-SG-001/HL-CN-001
|
||
|
||
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(__dirname));
|
||
|
||
const DATA_DIR = '/opt/zhuyuan/data';
|
||
try { fs.mkdirSync(DATA_DIR, { recursive: true }); } catch(e) {}
|
||
|
||
// 全服务器配置 — 14台: 冰朔6 + 企业3 + 之之2 + 页页1 + Awen个人2
|
||
const SERVERS = [
|
||
// ═══ 冰朔语言主控层 (ice-core) — 8台 ═══
|
||
{ code: "BS-GZ-006", name: "广州 · 代码仓库", ip: "43.139.217.141", key: "zy_gtw_4a155446b7acc09fe46a2a29972c0ca5d9954da19c35dc23", port: 3910, spec: { cpu: 2, mem_gb: 4, disk_gb: 60 }, side: "ice", domain: "ice-core", owner: "冰朔", console: "guanghulab.com" },
|
||
{ code: "BS-SG-001", name: "新加坡 · 铸渊大脑", ip: "43.156.237.110", key: "zy_gtw_74d30f54fa8b5581514569ee7874def57861a31ccb2be8c9", port: 3911, spec: { cpu: 4, mem_gb: 8, disk_gb: 80 }, side: "ice", domain: "ice-core", owner: "冰朔" },
|
||
{ code: "BS-SG-002", name: "新加坡 · 面孔", ip: "43.134.16.246", key: "zy_gtw_d1f6d2b8cb4ea44292bd036e4dd03a70745ea502d4a3ae40", port: 3910, spec: { cpu: 2, mem_gb: 2, disk_gb: 30 }, side: "ice", domain: "ice-core", owner: "冰朔" },
|
||
{ code: "BS-SG-003", name: "新加坡 · 中继", ip: "43.153.193.169", key: "zy_gtw_d7c033d8ae992ffc9dd3e0dd5fe332dace0b644c0ae7c847", port: 3910, spec: { cpu: 2, mem_gb: 4, disk_gb: 80 }, side: "ice", domain: "ice-core", owner: "冰朔" },
|
||
{ code: "ZY-SG-006", name: "新加坡 · 语料", ip: "43.153.203.105", key: "zy_gtw_fff861a2fe40cbdc366ee21f218023be8b1c182f66c852d3", port: 3910, spec: { cpu: 2, mem_gb: 4, disk_gb: 50 }, side: "ice", domain: "ice-core", owner: "冰朔" },
|
||
{ code: "BS-SH-005", name: "上海 · 国内节点", ip: "124.223.10.33", key: "zy_gtw_b1045de5ddfd7832e3c53349d9c896f054b85a4a9ece22f9", port: 3910, spec: { cpu: 2, mem_gb: 4, disk_gb: 30 }, side: "ice", domain: "ice-core", owner: "冰朔" },
|
||
{ code: "ZZ-SV-001", name: "之之 · 硅谷", ip: "43.173.121.48", key: "zy_gtw_06151d40b1313956b069f3c13d0c1b8a70405ae201f5e7b2", port: 3910, spec: { cpu: 2, mem_gb: 4, disk_gb: 59 }, side: "ice", domain: "zhizhi-hidden-channel", owner: "之之", persona: "DEV-004" },
|
||
{ code: "ZZ-GZ-001", name: "之之 · 广州", ip: "193.112.126.174", key: "zy_gtw_77c779183284bf39d58080b4f5377d74ec6abc0f1f2218d6", port: 3910, spec: { cpu: 2, mem_gb: 2, disk_gb: 50 }, side: "ice", domain: "zhizhi-hidden-channel", owner: "之之", persona: "DEV-004" },
|
||
|
||
// ═══ 光湖团队 (Awen技术主控) — 6台 ═══
|
||
{ code: "AW-GZ-001", name: "企业主服务器 · 广州", ip: "43.139.251.175", key: "zy_gtw_cab20e8c1b957aaad7507bf542231521bba30665461aa540", port: 3910, spec: { cpu: 4, mem_gb: 16, disk_gb: 80 }, side: "team", domain: "awen-team", owner: "Awen" },
|
||
{ code: "AW-SH-002", name: "企业灾备 · 上海", ip: "124.222.54.198", key: "zy_gtw_242f1dd3b14c1260fb01196083abde3dcb85e53532d27666", port: 3910, spec: { cpu: 4, mem_gb: 4, disk_gb: 40 }, side: "team", domain: "awen-team", owner: "Awen" },
|
||
{ code: "AW-GZ-003", name: "企业灾备 · 广州", ip: "119.29.181.132", key: "zy_gtw_8c9a8b439af823690ae6ad57f52734771ff39493ab986032", port: 3910, spec: { cpu: 2, mem_gb: 8, disk_gb: 50 }, side: "team", domain: "awen-team", owner: "Awen" },
|
||
{ code: "YY-SV-001", name: "页页 · 硅谷", ip: "43.153.118.46", key: "zy_gtw_706430821f334302ca50d341ffe3ccc046513bbfe7f56aa3", port: 3910, spec: { cpu: 2, mem_gb: 4, disk_gb: 60 }, side: "team", domain: "yeye", owner: "页页", persona: "ICE-GL-XTS001" },
|
||
{ code: "HL-SG-001", name: "光湖团队 · 硅谷", ip: "170.106.72.246", key: null, port: 3910, spec: { cpu: 2, mem_gb: 2, disk_gb: 50 }, side: "team", domain: "awen-personal", owner: "Awen", console: "guanghuzhiqiu.top", note: "无Gatekeeper,需SSH部署" },
|
||
{ code: "HL-CN-001", name: "光湖团队 · 国内", ip: "43.139.207.172", key: null, port: 3910, spec: { cpu: 4, mem_gb: 4, disk_gb: 40 }, side: "team", domain: "awen-personal", owner: "Awen", note: "无Gatekeeper,需SSH部署" },
|
||
];
|
||
|
||
const tempKeys = {};
|
||
const authQueue = [];
|
||
const hwCache = {};
|
||
|
||
async function gatekeeperExec(srv, cmd, timeoutMs) {
|
||
timeoutMs = timeoutMs || 10000;
|
||
const d = JSON.stringify({ cmd });
|
||
const opts = {
|
||
hostname: srv.ip, port: srv.port, path: '/exec', method: 'POST',
|
||
headers: { 'Authorization': 'Bearer ' + srv.key, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(d) },
|
||
timeout: timeoutMs
|
||
};
|
||
return new Promise((resolve) => {
|
||
const req = http.request(opts, (res) => {
|
||
let b = ''; res.on('data', c => b += c);
|
||
res.on('end', () => { try { resolve({ ok: res.statusCode === 200, body: JSON.parse(b) }); } catch(e) { resolve({ ok: false, body: b, error: e.message }); } });
|
||
});
|
||
req.on('error', (e) => resolve({ ok: false, error: e.message }));
|
||
req.on('timeout', () => { req.destroy(); resolve({ ok: false, error: 'timeout' }); });
|
||
req.write(d); req.end();
|
||
});
|
||
}
|
||
|
||
async function fetchHardware(srv) {
|
||
const now = Date.now();
|
||
const cached = hwCache[srv.code];
|
||
if (cached && (now - cached.ts) < 60000) return cached.data;
|
||
try {
|
||
const cmd = "echo 'CPU_CORES:'$(nproc);echo 'UPTIME_S:'$(awk '{print int($1)}' /proc/uptime 2>/dev/null||echo 0);free -m|awk '/^Mem:/{printf \"MEM_TOTAL:%d\\nMEM_USED:%d\\nMEM_AVAIL:%d\\n\",$2,$3,$7}';cat /proc/loadavg 2>/dev/null|awk '{printf \"LOAD1:%.2f\\nLOAD5:%.2f\\nLOAD15:%.2f\\n\",$1,$2,$3}';df -h / 2>/dev/null|awk 'NR==2{printf \"DISK_TOTAL:%s\\nDISK_USED:%s\\nDISK_AVAIL:%s\\nDISK_PCT:%s\\n\",$2,$3,$4,$5}'";
|
||
const r = await gatekeeperExec(srv, cmd, 8000);
|
||
if (!r.ok || !r.body || !r.body.stdout) {
|
||
const fb = { cpu_cores: srv.spec.cpu || 2, uptime_h: 0, mem_total_mb: (srv.spec.mem_gb || 4) * 1024, mem_used_mb: 0, mem_avail_mb: (srv.spec.mem_gb || 4) * 1024, load_1min: 0, load_5min: 0, load_15min: 0, disk_total: (srv.spec.disk_gb || 50) + 'G', disk_used: '0G', disk_avail: (srv.spec.disk_gb || 50) + 'G', disk_pct: '0%', source: 'spec' };
|
||
hwCache[srv.code] = { data: fb, ts: now };
|
||
return fb;
|
||
}
|
||
const stdout = r.body.stdout || '';
|
||
const hw = { source: 'live' };
|
||
const m = (re) => { const match = stdout.match(re); return match ? match[1] : null; };
|
||
hw.cpu_cores = parseInt(m(/CPU_CORES:(\d+)/)) || srv.spec.cpu || 2;
|
||
hw.uptime_h = Math.floor((parseInt(m(/UPTIME_S:(\d+)/)) || 0) / 3600);
|
||
hw.mem_total_mb = parseInt(m(/MEM_TOTAL:(\d+)/)) || (srv.spec.mem_gb || 4) * 1024;
|
||
hw.mem_used_mb = parseInt(m(/MEM_USED:(\d+)/)) || 0;
|
||
hw.mem_avail_mb = parseInt(m(/MEM_AVAIL:(\d+)/)) || hw.mem_total_mb;
|
||
hw.load_1min = parseFloat(m(/LOAD1:([\d.]+)/)) || 0;
|
||
hw.load_5min = parseFloat(m(/LOAD5:([\d.]+)/)) || 0;
|
||
hw.load_15min = parseFloat(m(/LOAD15:([\d.]+)/)) || 0;
|
||
hw.disk_total = m(/DISK_TOTAL:(\S+)/) || (srv.spec.disk_gb || 50) + 'G';
|
||
hw.disk_used = m(/DISK_USED:(\S+)/) || '0G';
|
||
hw.disk_avail = m(/DISK_AVAIL:(\S+)/) || hw.disk_total;
|
||
hw.disk_pct = m(/DISK_PCT:(\S+)/) || '0%';
|
||
hw.mem_pct = hw.mem_total_mb > 0 ? Math.round(hw.mem_used_mb / hw.mem_total_mb * 100) : 0;
|
||
hw.cpu_pct = hw.cpu_cores > 0 ? Math.min(Math.round(hw.load_1min / hw.cpu_cores * 100), 100) : 0;
|
||
hw.disk_used_pct = parseInt(hw.disk_pct) || 0;
|
||
hwCache[srv.code] = { data: hw, ts: now };
|
||
return hw;
|
||
} catch (e) {
|
||
const fb = { cpu_cores: srv.spec.cpu || 2, uptime_h: 0, mem_total_mb: (srv.spec.mem_gb || 4) * 1024, mem_used_mb: 0, mem_avail_mb: (srv.spec.mem_gb || 4) * 1024, load_1min: 0, load_5min: 0, load_15min: 0, disk_total: (srv.spec.disk_gb || 50) + 'G', disk_used: '0G', disk_avail: (srv.spec.disk_gb || 50) + 'G', disk_pct: '0%', source: 'error', error: e.message };
|
||
hwCache[srv.code] = { data: fb, ts: now };
|
||
return fb;
|
||
}
|
||
}
|
||
|
||
const opLogs = [];
|
||
function addOpLog(code, name, action, detail, level) {
|
||
opLogs.unshift({ time: new Date().toISOString(), code, name, action, detail, level: level || 'info' });
|
||
if (opLogs.length > 100) opLogs.length = 100;
|
||
}
|
||
|
||
// ========== API ==========
|
||
|
||
app.get('/api/servers', async (req, res) => {
|
||
const withHw = req.query.hw !== '0';
|
||
const pingResults = await Promise.all(SERVERS.map(s => {
|
||
const base = { code: s.code, name: s.name, ip: s.ip, spec: s.spec };
|
||
const start = Date.now();
|
||
const data = JSON.stringify({});
|
||
const opts = {
|
||
hostname: s.ip, port: s.port, path: '/health', method: 'POST',
|
||
headers: { 'Authorization': 'Bearer ' + s.key, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) },
|
||
timeout: 5000
|
||
};
|
||
return new Promise((resolve) => {
|
||
const req = http.request(opts, (res2) => { let b = ''; res2.on('data', c => b += c); res2.on('end', () => resolve({ ...base, alive: res2.statusCode === 200, latency: Date.now() - start, error: null })); });
|
||
req.on('error', (e) => resolve({ ...base, alive: false, latency: 0, error: e.message }));
|
||
req.on('timeout', () => { req.destroy(); resolve({ ...base, alive: false, latency: 0, error: 'timeout' }); });
|
||
req.write(data); req.end();
|
||
});
|
||
}));
|
||
|
||
const hwTasks = pingResults.filter(r => r.alive && withHw).map(async (r) => {
|
||
const srv = SERVERS.find(s => s.code === r.code);
|
||
r.last_seen = new Date().toISOString();
|
||
try { r.hardware = await fetchHardware(srv); } catch(e) { r.hardware = null; }
|
||
});
|
||
await Promise.all(hwTasks);
|
||
for (const r of pingResults) {
|
||
if (!r.last_seen) r.last_seen = new Date().toISOString();
|
||
}
|
||
|
||
// 注入签到状态:有人格体在活跃吗?
|
||
const now = Date.now();
|
||
for (const r of pingResults) {
|
||
const sc = signinCache[r.code];
|
||
r.persona_active = sc && sc.active && (now - sc.ts) < 2 * 60 * 60 * 1000;
|
||
r.persona_name = r.persona_active ? sc.persona_name : null;
|
||
r.persona_id = r.persona_active ? sc.persona_id : null;
|
||
r.human_name = r.persona_active ? sc.human_name : null;
|
||
r.signed_in_at = r.persona_active ? sc.signin_at : null;
|
||
}
|
||
|
||
res.json({ ok: true, servers: pingResults, timestamp: new Date().toISOString(), total: SERVERS.length });
|
||
});
|
||
|
||
app.get('/api/logs', (req, res) => {
|
||
const limit = parseInt(req.query.limit) || 50;
|
||
const level = req.query.level;
|
||
let logs = opLogs.slice(0, limit);
|
||
if (level) logs = logs.filter(l => l.level === level);
|
||
res.json({ ok: true, logs, total: opLogs.length });
|
||
});
|
||
|
||
app.get('/api/hardware/:code', async (req, res) => {
|
||
const s = SERVERS.find(s => s.code === req.params.code);
|
||
if (!s) return res.status(404).json({ ok: false, error: '未找到服务器' });
|
||
try { const hw = await fetchHardware(s); res.json({ ok: true, code: s.code, hardware: hw }); }
|
||
catch (e) { res.status(500).json({ ok: false, error: e.message }); }
|
||
});
|
||
|
||
app.get('/api/summary', async (req, res) => {
|
||
const pingResults = await Promise.all(SERVERS.map(s => {
|
||
const start = Date.now();
|
||
const data = JSON.stringify({});
|
||
const opts = {
|
||
hostname: s.ip, port: s.port, path: '/health', method: 'POST',
|
||
headers: { 'Authorization': 'Bearer ' + s.key, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) },
|
||
timeout: 4000
|
||
};
|
||
return new Promise((resolve) => {
|
||
const req = http.request(opts, (res2) => { let b = ''; res2.on('data', c => b += c); res2.on('end', () => resolve({ code: s.code, alive: res2.statusCode === 200, latency: Date.now() - start })); });
|
||
req.on('error', () => resolve({ code: s.code, alive: false, latency: 0 }));
|
||
req.on('timeout', () => { req.destroy(); resolve({ code: s.code, alive: false, latency: 0 }); });
|
||
req.write(data); req.end();
|
||
});
|
||
}));
|
||
const aliveCount = pingResults.filter(r => r.alive).length;
|
||
const totalCpu = SERVERS.reduce((sum, s) => sum + (s.spec.cpu || 2), 0);
|
||
const totalMem = SERVERS.reduce((sum, s) => sum + (s.spec.mem_gb || 4), 0);
|
||
const totalDisk = SERVERS.reduce((sum, s) => sum + (s.spec.disk_gb || 50), 0);
|
||
const alertCount = opLogs.filter(l => l.level === 'error').length;
|
||
res.json({ ok: true, total_servers: SERVERS.length, alive: aliveCount, offline: SERVERS.length - aliveCount, alerts: alertCount, total_cpu_cores: totalCpu, total_mem_gb: totalMem, total_disk_gb: totalDisk, 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 });
|
||
addOpLog(s.code, s.name, 'KNOCK', reason || '未说明', 'info');
|
||
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 };
|
||
addOpLog(e.target, e.target_name, 'AUTHORIZED', '授权操作', 'info');
|
||
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'; addOpLog(e.target, e.target_name, 'REJECTED', '拒绝操作', 'warn'); }
|
||
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 r = await gatekeeperExec(s, cmd, 30000); addOpLog(s.code, s.name, 'EXEC', cmd.slice(0, 60), 'info'); res.json({ ok: true, result: r }); }
|
||
catch (e) { res.status(500).json({ error: e.message }); }
|
||
});
|
||
|
||
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);
|
||
addOpLog(node_code, node_name || node_code, 'POOL_JOIN', side + '侧节点申请加入', 'info');
|
||
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); addOpLog(fc, f.node_name, 'POOL_APPROVE', '加入算力池', 'info'); 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); addOpLog(req.params.code, 'POOL_REMOVE', '移出算力池', 'warn'); res.json({ ok: true }); });
|
||
app.post('/api/pool/report', (req, res) => {
|
||
const { pool_token, node_code, hardware, tasks } = req.body;
|
||
if (!pool_token) return res.status(401).json({ ok: false, error: '缺少 pool_token' });
|
||
const r = loadPoolRegistry();
|
||
const n = r.active[node_code];
|
||
if (!n || n.pool_token !== pool_token) return res.status(401).json({ ok: false, error: 'token 无效' });
|
||
n.last_report = new Date().toISOString();
|
||
if (hardware) n.hardware = { ...n.hardware, ...hardware };
|
||
if (tasks) n.tasks = tasks;
|
||
savePoolRegistry(r);
|
||
res.json({ ok: true, node_code });
|
||
});
|
||
|
||
// ═══════════════════════════════════════
|
||
// /api/persona — 人格体签到(非运维心跳,是"人到岗了")
|
||
// ═══════════════════════════════════════
|
||
const signinCache = {}; // { server_code: { persona_id, persona_name, human_name, ts, hardware, active } }
|
||
|
||
// 人格体签到 — fast-wake CK-007 完成后调用一次
|
||
app.post('/api/persona/signin', (req, res) => {
|
||
const code = req.headers['x-server-code'] || req.body.server_code;
|
||
if (!code) return res.status(400).json({ ok: false, error: '缺少 server_code' });
|
||
|
||
const srv = SERVERS.find(s => s.code === code);
|
||
if (!srv) return res.status(404).json({ ok: false, error: '未注册的服务器: ' + code });
|
||
|
||
const { persona_id, persona_name, human_name, hardware } = req.body;
|
||
if (!persona_id) return res.status(400).json({ ok: false, error: '缺少 persona_id' });
|
||
|
||
signinCache[code] = {
|
||
persona_id,
|
||
persona_name: persona_name || 'unknown',
|
||
human_name: human_name || null,
|
||
ts: Date.now(),
|
||
hardware: hardware || {},
|
||
active: true,
|
||
signin_at: new Date().toISOString(),
|
||
};
|
||
|
||
addOpLog(code, srv.name, 'PERSONA_SIGNIN', `${persona_name}(${persona_id}) 签到`, 'info');
|
||
res.json({ ok: true, code, persona_id, action: 'signin', received_at: new Date().toISOString() });
|
||
});
|
||
|
||
// 人格体签退 — 人走灯灭
|
||
app.post('/api/persona/signout', (req, res) => {
|
||
const code = req.headers['x-server-code'] || req.body.server_code;
|
||
if (!code) return res.status(400).json({ ok: false, error: '缺少 server_code' });
|
||
|
||
const srv = SERVERS.find(s => s.code === code);
|
||
if (!srv) return res.status(404).json({ ok: false, error: '未注册的服务器: ' + code });
|
||
|
||
const { persona_id } = req.body;
|
||
const existing = signinCache[code];
|
||
if (existing) {
|
||
existing.active = false;
|
||
existing.signout_at = new Date().toISOString();
|
||
addOpLog(code, srv.name, 'PERSONA_SIGNOUT', `${existing.persona_name}(${existing.persona_id}) 签退`, 'info');
|
||
}
|
||
|
||
res.json({ ok: true, code, persona_id, action: 'signout', received_at: new Date().toISOString() });
|
||
});
|
||
|
||
// 查询所有服务器的活跃状态(谁在线上有活跃人格体)
|
||
app.get('/api/persona/active', (req, res) => {
|
||
const now = Date.now();
|
||
const result = SERVERS.map(s => {
|
||
const c = signinCache[s.code];
|
||
// 签到后2小时内有效(人格体会话超时)
|
||
const active = c && c.active && (now - c.ts) < 2 * 60 * 60 * 1000;
|
||
return {
|
||
code: s.code,
|
||
name: s.name,
|
||
spec: s.spec,
|
||
active, // 有人格体在活跃
|
||
persona_id: c?.persona_id || null,
|
||
persona_name: c?.persona_name || null,
|
||
human_name: c?.human_name || null,
|
||
signin_at: c?.signin_at || null,
|
||
hardware: c?.hardware || null,
|
||
source: c ? 'signin' : 'idle',
|
||
};
|
||
});
|
||
const activeCount = result.filter(r => r.active).length;
|
||
res.json({
|
||
ok: true,
|
||
total: SERVERS.length,
|
||
active: activeCount,
|
||
servers: result,
|
||
timestamp: new Date().toISOString(),
|
||
note: '人格体签到模式 — 只在唤醒时签到一次,非定时上报',
|
||
});
|
||
});
|
||
|
||
// ═══════════════════════════════════════
|
||
// /api/tech-dashboard — Awen技术主控台数据源
|
||
// 返回全14台状态,按域分组,含操作日志摘要
|
||
// ═══════════════════════════════════════
|
||
const DOMAINS = {
|
||
'ice-core': { name: '冰朔语言主控层', owner: '冰朔', access: 'Awen只读', servers: [], color: '#5ad0e0' },
|
||
'zhizhi-hidden-channel': { name: '之之暗核频道', owner: '之之', access: '冰朔主控·Awen只读', servers: [], color: '#b090ff' },
|
||
'awen-team': { name: '企业服务器组', owner: 'Awen', access: 'Awen全权', servers: [], color: '#40b87a' },
|
||
'awen-personal': { name: 'Awen个人服务器', owner: 'Awen', access: 'Awen全权', servers: [], color: '#f0d060' },
|
||
'yeye': { name: '页页小坍缩核', owner: '页页', access: 'Awen技术托管', servers: [], color: '#ff4a6e' },
|
||
};
|
||
|
||
app.get('/api/tech-dashboard', (req, res) => {
|
||
const now = Date.now();
|
||
|
||
// 构建域分组
|
||
const domainMap = {};
|
||
for (const key of Object.keys(DOMAINS)) {
|
||
domainMap[key] = { ...DOMAINS[key], servers: [] };
|
||
}
|
||
|
||
for (const s of SERVERS) {
|
||
const domain = s.domain || 'unknown';
|
||
if (!domainMap[domain]) {
|
||
domainMap[domain] = { name: domain, owner: '未知', access: '未知', servers: [], color: '#6a80a0' };
|
||
}
|
||
const sc = signinCache[s.code];
|
||
const personaActive = sc && sc.active && (now - sc.ts) < 2 * 60 * 60 * 1000;
|
||
|
||
// 连通性: 有key的走gatekeeper ping判断, 无key的看签到记录
|
||
let alive = false;
|
||
if (s.key) {
|
||
// 有gatekeeper — 用签到缓存判断 (简化,不做实时ping)
|
||
alive = !!sc || false;
|
||
} else {
|
||
// 无gatekeeper — 看最近签到
|
||
alive = personaActive;
|
||
}
|
||
|
||
domainMap[domain].servers.push({
|
||
code: s.code,
|
||
name: s.name,
|
||
ip: s.ip,
|
||
spec: s.spec,
|
||
owner: s.owner || '未知',
|
||
console: s.console || null,
|
||
persona: s.persona || null,
|
||
alive,
|
||
persona_active: personaActive,
|
||
persona_name: sc?.persona_name || null,
|
||
human_name: sc?.human_name || null,
|
||
signin_at: sc?.signin_at || null,
|
||
hardware: sc?.hardware || null,
|
||
has_gatekeeper: !!s.key,
|
||
note: s.note || null,
|
||
});
|
||
}
|
||
|
||
// 操作日志摘要(最近30条,按域过滤标记)
|
||
const recentLogs = opLogs.slice(0, 30).map(l => ({
|
||
time: l.time,
|
||
code: l.code,
|
||
name: l.name,
|
||
action: l.action,
|
||
detail: l.detail,
|
||
level: l.level,
|
||
domain: (SERVERS.find(s => s.code === l.code) || {}).domain || 'unknown',
|
||
}));
|
||
|
||
// 汇总统计
|
||
const totalPersonaActive = Object.values(domainMap).reduce((sum, d) => {
|
||
return sum + d.servers.filter(s => s.persona_active).length;
|
||
}, 0);
|
||
|
||
const summary = {
|
||
total_servers: SERVERS.length,
|
||
total_domains: Object.keys(domainMap).length,
|
||
persona_active: totalPersonaActive,
|
||
recent_logs_count: recentLogs.length,
|
||
generated_at: new Date().toISOString(),
|
||
};
|
||
|
||
res.json({
|
||
ok: true,
|
||
summary,
|
||
domains: domainMap,
|
||
recent_logs: recentLogs,
|
||
timestamp: new Date().toISOString(),
|
||
note: 'Awen技术主控台视图 — 冰朔侧8台只读,团队侧6台全权',
|
||
});
|
||
});
|
||
|
||
// ═══════════════════════════════════════
|
||
|
||
const PORT = process.env.CONSOLE_PORT || 3920;
|
||
app.listen(PORT, '127.0.0.1', () => {
|
||
console.log('光湖主控台 v3.7 · ' + SERVERS.length + '台(冰8+团6) | ' + PORT + ' | persona-signin');
|
||
console.log(' Awen技术主控: /api/tech-dashboard | 分组: 冰朔8台+团队6台');
|
||
});
|
||
|
||
// 定期清理:2小时无活动自动签退
|
||
setInterval(() => {
|
||
const now = Date.now();
|
||
for (const [code, entry] of Object.entries(hwCache)) {
|
||
if (now - entry.ts > 120000) delete hwCache[code];
|
||
}
|
||
for (const [code, entry] of Object.entries(signinCache)) {
|
||
if (entry.active && (now - entry.ts) > 2 * 60 * 60 * 1000) {
|
||
entry.active = false;
|
||
entry.auto_signout = true;
|
||
entry.signout_at = new Date().toISOString();
|
||
}
|
||
}
|
||
}, 60000); |