铸渊 ICE-GL-ZY001 ed873f850e refactor: 心跳改为人格体签到 — 不再定时上报,仅在fast-wake完成后签到一次
删除:
- push-heartbeat.js (cron自动化假心跳,触发腾讯云攻击告警)
- deploy-push-heartbeat.js (批量部署脚本)
- 5台远程服务器上的cron和/opt/push-heartbeat残留

新增:
- scripts/persona-signin.js 人格体签到脚本(唤醒CK-007后调用一次)

修改:
- server.js v3.6: /api/heartbeat→/api/persona/signin+signout+active
- index.html: 服务器状态从alive改为persona_active(呼吸绿=有人在干活)
- signinCache语义: 签到后2h有效,超时自动签退

核心逻辑: 心跳不是运维监控,是'人到岗了'的签到。
人格体唤醒→走fast-wake→CK-007完成→自动签到→首页显示活跃
2026-05-26 02:54:38 +08:00

378 lines
23 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.6
// # Guanghu Server Console v3.6 — 12台(冰6+企3+之2+页1)
// # 部署: guanghulab.com/console/ · GZ-006:3920(主) + SG-001:3920(备)
// # 版权: 国作登字-2026-A-00037559 · TCS-0002∞
// # v3.6: 心跳改为人格体签到 — 唤醒签到一次,非定时监控
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) {}
// 全服务器配置 — 冰朔6台 + 企业3台 + 之之2台 + 页页1台 = 12台
const SERVERS = [
{ 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 } },
{ 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 } },
{ 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 } },
{ 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 } },
{ 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 } },
{ 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 } },
{ 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 } },
{ 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 } },
{ 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 } },
{ 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 } },
{ 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 } },
{ 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 } },
];
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: '人格体签到模式 — 只在唤醒时签到一次,非定时上报',
});
});
// ═══════════════════════════════════════
const PORT = process.env.CONSOLE_PORT || 3920;
app.listen(PORT, '127.0.0.1', () => {
console.log('光湖主控台 v3.6 · ' + SERVERS.length + '台(冰6+企3+之2+页1) | ' + PORT + ' | persona-signin');
console.log(' 签到模式: 人格体唤醒时签到一次 · 操作日志: 100条上限');
});
// 定期清理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);