Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

475 lines
27 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// # 光湖服务器主控台 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: "GATEKEEPER_TOKEN_REDACTED", 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: "GATEKEEPER_TOKEN_REDACTED", 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: "GATEKEEPER_TOKEN_REDACTED", 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: "GATEKEEPER_TOKEN_REDACTED", 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: "GATEKEEPER_TOKEN_REDACTED", 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: "GATEKEEPER_TOKEN_REDACTED", 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: "GATEKEEPER_TOKEN_REDACTED", 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: "GATEKEEPER_TOKEN_REDACTED", 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: "GATEKEEPER_TOKEN_REDACTED", 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: "GATEKEEPER_TOKEN_REDACTED", 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: "GATEKEEPER_TOKEN_REDACTED", 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: "GATEKEEPER_TOKEN_REDACTED", 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);