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完成→自动签到→首页显示活跃
This commit is contained in:
parent
d06e5bcaac
commit
ed873f850e
@ -54,6 +54,7 @@ body{background:var(--bg);color:var(--txt);font-family:var(--font);min-height:10
|
||||
.cd-sub{font-size:11px;color:var(--dim);margin-top:3px;font-family:var(--mono)}
|
||||
.cd-dot{width:12px;height:12px;border-radius:50%;flex-shrink:0;margin-top:6px}
|
||||
.cd-dot.ok{background:var(--ok);box-shadow:0 0 16px var(--ok-glow);animation:breathe-dot 3s ease-in-out infinite}
|
||||
.cd-dot.static{background:var(--ok);box-shadow:0 0 8px var(--ok-glow);animation:none}
|
||||
.cd-dot.er{background:var(--err);box-shadow:0 0 16px rgba(255,74,110,.4);animation:breathe-dot 2s ease-in-out infinite}
|
||||
.rs{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:8px}
|
||||
.rb{position:relative}
|
||||
@ -352,18 +353,21 @@ function renderCards(srvData) {
|
||||
}
|
||||
|
||||
var alive = sv.filter(function(s) { return s.alive }).length;
|
||||
var activeP = sv.filter(function(s) { return s.persona_active }).length;
|
||||
var total = sv.length;
|
||||
|
||||
$('gl-st').textContent = alive === total ? '全境正常' : (alive > 0 ? alive + '/' + total + ' 在线' : '全境离线');
|
||||
$('gl-st').style.color = alive === total ? 'var(--ok)' : (alive > 0 ? 'var(--warn)' : 'var(--err)');
|
||||
$('sm-srv').innerHTML = alive + '/' + total + ' <span class="nu">在线</span>';
|
||||
$('gl-st').textContent = activeP > 0 ? activeP + '人格体在线' : (alive === total ? '全境正常' : (alive > 0 ? alive + '/' + total + ' 在线' : '全境离线'));
|
||||
$('gl-st').style.color = activeP > 0 ? 'var(--cyan)' : (alive === total ? 'var(--ok)' : (alive > 0 ? 'var(--warn)' : 'var(--err)'));
|
||||
$('sm-srv').innerHTML = activeP+'<span class="nu">活跃</span> / '+alive+'/'+total+' <span class="nu">在线</span>';
|
||||
$('sc-servers').innerHTML = '服务器集群 · ' + total + '台主机 <span class="sc-badge">冰朔6 + 企业3</span>';
|
||||
|
||||
var totalMemPct = 0, memCount = 0, totalCpuLoad = 0, cpuCount = 0;
|
||||
|
||||
$('server-grid').innerHTML = sv.map(function(s, i) {
|
||||
var ok = s.alive ? 'ok' : 'er';
|
||||
var st = s.alive ? '在线' : '离线';
|
||||
var sAlive = s.alive;
|
||||
var sActive = s.persona_active;
|
||||
var dotClass = sActive ? 'ok' : (sAlive ? 'static' : 'er'); // 活跃=呼吸绿, 在线空闲=静态绿, 离线=红
|
||||
var st = sActive ? '· ' + (s.persona_name || s.human_name || '活跃') : (sAlive ? '在线(空闲)' : '离线');
|
||||
var lt = s.latency ? s.latency + 'ms' : '-';
|
||||
var spec = s.spec || { cpu: 2, mem_gb: 4, disk_gb: 50 };
|
||||
|
||||
@ -379,27 +383,27 @@ function renderCards(srvData) {
|
||||
var diskTotal = hw.disk_total || spec.disk_gb + 'G';
|
||||
var source = hw.source || '--';
|
||||
|
||||
if (s.alive && hw.source === 'live') { totalMemPct += memPct; memCount++; totalCpuLoad += load1; cpuCount++; }
|
||||
if (sAlive && hw.source === 'live') { totalMemPct += memPct; memCount++; totalCpuLoad += load1; cpuCount++; }
|
||||
var mods = MODULES[s.code] || ['基础服务'];
|
||||
var gDel = ((i % 6) * 0.5) + 's';
|
||||
|
||||
return '<div class="cd">' +
|
||||
'<div class="cd-glow" style="animation-delay:' + gDel + '"></div>' +
|
||||
'<div class="cd-hd"><div><div class="cd-n">' + s.name + '</div><div class="cd-sub">' + s.code + ' · ' + s.ip + ' · ' + spec.cpu + '核/' + spec.mem_gb + 'G/' + spec.disk_gb + 'G</div></div>' +
|
||||
'<div class="cd-dot ' + ok + '"></div></div>' +
|
||||
'<div class="cd-dot ' + dotClass + '"></div></div>' +
|
||||
'<div class="rs">' +
|
||||
'<div class="rb"><div class="rb-h"><span>CPU</span><span>' + cpuPct + '%</span></div><div class="rb-b"><div class="rb-f cpu" style="width:' + cpuPct + '%"></div></div></div>' +
|
||||
'<div class="rb"><div class="rb-h"><span>内存 ' + memUsed + '/' + memTotal + 'G</span><span>' + memPct + '%</span></div><div class="rb-b"><div class="rb-f mem" style="width:' + memPct + '%"></div></div></div>' +
|
||||
'<div class="rb"><div class="rb-h"><span>磁盘 ' + diskUsed + '/' + diskTotal + '</span><span>' + diskPct + '%</span></div><div class="rb-b"><div class="rb-f dsk" style="width:' + diskPct + '%"></div></div></div>' +
|
||||
'</div>' +
|
||||
'<div class="cd-info">' +
|
||||
'<s' + (s.alive ? ' class="g"' : '') + '>' + lt + '</s>' +
|
||||
'<s' + (s.alive ? ' class="g"' : '') + '>' + (uptime > 0 ? uptime + 'h' : '--') + '</s>' +
|
||||
'<s>load ' + load1.toFixed(1) + '</s><s>' + st + '</s>' +
|
||||
'<s' + (sAlive ? ' class="g"' : '') + '>' + lt + '</s>' +
|
||||
'<s' + (sAlive ? ' class="g"' : '') + '>' + (uptime > 0 ? uptime + 'h' : '--') + '</s>' +
|
||||
'<s>load ' + load1.toFixed(1) + '</s><s' + (sActive ? ' class="g"' : '') + '>' + st + '</s>' +
|
||||
'</div>' +
|
||||
'<div class="ml"><div class="ml-h"><span>模块</span><span style="font-size:9px;color:var(--text-faint)">数据源:' + source + '</span></div>' +
|
||||
'<div class="ml-g">' + mods.map(function(m) {
|
||||
return '<span class="ml-t ' + (s.alive ? 'act' : 'idl') + '">' + m + '</span>';
|
||||
return '<span class="ml-t ' + (sActive ? 'act' : (sAlive ? 'idl' : '')) + '">' + m + '</span>';
|
||||
}).join('') + '</div></div></div>';
|
||||
}).join('');
|
||||
|
||||
|
||||
@ -1,116 +0,0 @@
|
||||
// 光湖 push-heartbeat v1.0 — 每台服务器主动上报本机状态
|
||||
// 部署: 各服务器 PM2 cron模式,每5分钟运行一次
|
||||
// 原理: 不扫描别人,只报自己。主控台(BS-GZ-006:3920)集中收。
|
||||
// 冰朔: "不要扫描,让每个服务器自己主动往外发"
|
||||
|
||||
'use strict';
|
||||
|
||||
const http = require('http');
|
||||
const os = require('os');
|
||||
const fs = require('fs');
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// 配置 — 部署时修改
|
||||
// ═══════════════════════════════════════
|
||||
|
||||
const MASTER_HOST = process.env.MASTER_HOST || '43.139.217.141';
|
||||
const MASTER_PORT = parseInt(process.env.MASTER_PORT || '3920');
|
||||
const SERVER_CODE = process.env.SERVER_CODE || 'BS-UNKNOWN';
|
||||
const SERVER_NAME = process.env.SERVER_NAME || '未命名';
|
||||
const SERVER_KEY = process.env.SERVER_KEY || '';
|
||||
const INTERVAL_MS = 5 * 60 * 1000; // 5分钟
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// 硬件采集(本机,不连外网)
|
||||
// ═══════════════════════════════════════
|
||||
|
||||
function collectHardware() {
|
||||
const cpuCores = os.cpus().length;
|
||||
const loadAvg = os.loadavg();
|
||||
const totalMem = os.totalmem();
|
||||
const freeMem = os.freemem();
|
||||
const usedMem = totalMem - freeMem;
|
||||
const uptime = os.uptime();
|
||||
|
||||
return {
|
||||
cpu_cores: cpuCores,
|
||||
cpu_load: parseFloat(loadAvg[0].toFixed(2)),
|
||||
cpu_load5: parseFloat(loadAvg[1].toFixed(2)),
|
||||
cpu_load15: parseFloat(loadAvg[2].toFixed(2)),
|
||||
mem_total_mb: Math.floor(totalMem / 1024 / 1024),
|
||||
mem_used_mb: Math.floor(usedMem / 1024 / 1024),
|
||||
mem_avail_mb: Math.floor(freeMem / 1024 / 1024),
|
||||
mem_used_pct: parseFloat((usedMem / totalMem * 100).toFixed(1)),
|
||||
uptime_h: Math.floor(uptime / 3600),
|
||||
hostname: os.hostname(),
|
||||
};
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// 上报到主控台(只发POST,不扫描)
|
||||
// ═══════════════════════════════════════
|
||||
|
||||
function reportToMaster(data) {
|
||||
return new Promise((resolve) => {
|
||||
const payload = JSON.stringify(data);
|
||||
const opts = {
|
||||
hostname: MASTER_HOST,
|
||||
port: MASTER_PORT,
|
||||
path: '/api/heartbeat',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(payload),
|
||||
'X-Server-Code': SERVER_CODE,
|
||||
'X-Server-Key': SERVER_KEY,
|
||||
},
|
||||
timeout: 8000,
|
||||
};
|
||||
|
||||
const req = http.request(opts, (res) => {
|
||||
let body = '';
|
||||
res.on('data', (c) => (body += c));
|
||||
res.on('end', () => resolve({ ok: res.statusCode === 200, 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(payload);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// 主逻辑
|
||||
// ═══════════════════════════════════════
|
||||
|
||||
async function push() {
|
||||
const hw = collectHardware();
|
||||
const data = {
|
||||
server_code: SERVER_CODE,
|
||||
server_name: SERVER_NAME,
|
||||
timestamp: new Date().toISOString(),
|
||||
hardware: hw,
|
||||
};
|
||||
|
||||
console.log(`[${data.timestamp}] ${SERVER_CODE} → ${MASTER_HOST}:${MASTER_PORT}`);
|
||||
|
||||
const result = await reportToMaster(data);
|
||||
if (result.ok) {
|
||||
console.log(` ✅ 上报成功 · CPU ${hw.cpu_load} · 内存 ${hw.mem_used_pct}% · 运行 ${hw.uptime_h}h`);
|
||||
} else {
|
||||
console.log(` ❌ 上报失败: ${result.error || result.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 立即发一次
|
||||
push();
|
||||
|
||||
// 定时发
|
||||
if (process.argv.includes('--once')) {
|
||||
// 一次性模式 — PM2 cron 使用
|
||||
} else {
|
||||
setInterval(push, INTERVAL_MS);
|
||||
}
|
||||
@ -1,7 +1,8 @@
|
||||
// # 光湖服务器主控台 API v3.4
|
||||
// # Guanghu Server Console v3.4 — 12台(冰6+企3+之2+页1)
|
||||
// # 光湖服务器主控台 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');
|
||||
@ -131,6 +132,17 @@ app.get('/api/servers', async (req, res) => {
|
||||
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 });
|
||||
});
|
||||
|
||||
@ -263,67 +275,82 @@ app.post('/api/pool/report', (req, res) => {
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// /api/heartbeat — 各服务器主动推送上报(push模式,非扫描)
|
||||
// /api/persona — 人格体签到(非运维心跳,是"人到岗了")
|
||||
// ═══════════════════════════════════════
|
||||
const pushHwCache = {}; // { code: { data, ts, name } }
|
||||
const signinCache = {}; // { server_code: { persona_id, persona_name, human_name, ts, hardware, active } }
|
||||
|
||||
app.post('/api/heartbeat', (req, res) => {
|
||||
// 人格体签到 — fast-wake CK-007 完成后调用一次
|
||||
app.post('/api/persona/signin', (req, res) => {
|
||||
const code = req.headers['x-server-code'] || req.body.server_code;
|
||||
const key = req.headers['x-server-key'] || req.body.server_key || '';
|
||||
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 });
|
||||
|
||||
// 简单验证:key匹配或白名单
|
||||
const valid = (key === srv.key) || (key === 'push-heartbeat-v1');
|
||||
if (!valid) return res.status(401).json({ ok: false, error: '密钥不匹配' });
|
||||
const { persona_id, persona_name, human_name, hardware } = req.body;
|
||||
if (!persona_id) return res.status(400).json({ ok: false, error: '缺少 persona_id' });
|
||||
|
||||
const hw = req.body.hardware || {};
|
||||
pushHwCache[code] = {
|
||||
signinCache[code] = {
|
||||
persona_id,
|
||||
persona_name: persona_name || 'unknown',
|
||||
human_name: human_name || null,
|
||||
ts: Date.now(),
|
||||
name: srv.name,
|
||||
data: {
|
||||
cpu_cores: hw.cpu_cores || srv.spec.cpu,
|
||||
cpu_load: hw.cpu_load || hw.load_1min || 0,
|
||||
mem_total_mb: hw.mem_total_mb || srv.spec.mem_gb * 1024,
|
||||
mem_used_mb: hw.mem_used_mb || 0,
|
||||
mem_used_pct: hw.mem_used_pct || 0,
|
||||
uptime_h: hw.uptime_h || 0,
|
||||
hostname: hw.hostname || '',
|
||||
},
|
||||
alive: true,
|
||||
last_seen: new Date().toISOString(),
|
||||
source: 'push',
|
||||
hardware: hardware || {},
|
||||
active: true,
|
||||
signin_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
res.json({ ok: true, code, received_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.get('/api/heartbeat-summary', (req, res) => {
|
||||
// 人格体签退 — 人走灯灭
|
||||
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 = pushHwCache[s.code];
|
||||
const alive = c && (now - c.ts) < 10 * 60 * 1000; // 10分钟内有过上报
|
||||
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,
|
||||
alive,
|
||||
last_seen: c?.last_seen || null,
|
||||
hardware: c?.data || null,
|
||||
source: c?.source || 'unknown',
|
||||
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 aliveCount = result.filter(r => r.alive).length;
|
||||
const activeCount = result.filter(r => r.active).length;
|
||||
res.json({
|
||||
ok: true,
|
||||
total: SERVERS.length,
|
||||
alive: aliveCount,
|
||||
active: activeCount,
|
||||
servers: result,
|
||||
timestamp: new Date().toISOString(),
|
||||
note: 'push模式 — 各服务器主动上报,非扫描',
|
||||
note: '人格体签到模式 — 只在唤醒时签到一次,非定时上报',
|
||||
});
|
||||
});
|
||||
|
||||
@ -331,17 +358,21 @@ app.get('/api/heartbeat-summary', (req, res) => {
|
||||
|
||||
const PORT = process.env.CONSOLE_PORT || 3920;
|
||||
app.listen(PORT, '127.0.0.1', () => {
|
||||
console.log('光湖主控台 v3.5 · ' + SERVERS.length + '台(冰6+企3+之2+页1) | ' + PORT + ' | push-heartbeat');
|
||||
console.log(' 硬件采集: push模式(各服自报) + 旧pull缓存(60s) · 操作日志: 100条上限');
|
||||
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];
|
||||
}
|
||||
// 15分钟无上报标记为掉线
|
||||
for (const [code, entry] of Object.entries(pushHwCache)) {
|
||||
if (now - entry.ts > 15 * 60 * 1000) delete pushHwCache[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);
|
||||
@ -1,88 +0,0 @@
|
||||
// 铸渊 push-heartbeat 批量部署脚本 v1.0
|
||||
// 在 BS-GZ-006 上运行,通过Gatekeeper exec部署到所有远程服务器
|
||||
// 用法: node deploy-push-heartbeat.js
|
||||
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
|
||||
const SERVERS = [
|
||||
{ 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 },
|
||||
];
|
||||
|
||||
const MASTER_HOST = "guanghulab.com";
|
||||
const MASTER_PORT = 443;
|
||||
const MASTER_PATH = "/console/api/heartbeat";
|
||||
|
||||
function gkExec(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, 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();
|
||||
});
|
||||
}
|
||||
|
||||
// 推送脚本模板 — 通过nginx /console/ → console-server:3920
|
||||
function pushScript(code, name, key) {
|
||||
return `
|
||||
const https=require("https"),os=require("os");
|
||||
const HOST="${MASTER_HOST}",PORT=${MASTER_PORT},PATH="${MASTER_PATH}",CODE="${code}",NAME="${name}",KEY="${key}";
|
||||
function hw(){const c=os.cpus().length,l=os.loadavg(),t=os.totalmem(),f=os.freemem();return{cpu_cores:c,cpu_load:parseFloat(l[0].toFixed(2)),mem_total_mb:Math.floor(t/1048576),mem_used_mb:Math.floor((t-f)/1048576),mem_used_pct:parseFloat(((t-f)/t*100).toFixed(1)),uptime_h:Math.floor(os.uptime()/3600),hostname:os.hostname()}}
|
||||
function post(d){return new Promise(r=>{const p=JSON.stringify(d),o={hostname:HOST,port:PORT,path:PATH,method:"POST",headers:{"Content-Type":"application/json","Content-Length":Buffer.byteLength(p),"X-Server-Code":CODE,"X-Server-Key":KEY},timeout:8000,rejectUnauthorized:false};const q=https.request(o,res=>{let b="";res.on("data",c=>b+=c);res.on("end",()=>r(res.statusCode===200))});q.on("error",()=>r(false));q.on("timeout",()=>{q.destroy();r(false)});q.write(p);q.end()})}
|
||||
(async()=>{const d={server_code:CODE,server_name:NAME,timestamp:new Date().toISOString(),hardware:hw()};const ok=await post(d);console.log(new Date().toISOString()+" "+(ok?"OK":"FAIL"))})();
|
||||
`.trim();
|
||||
}
|
||||
|
||||
async function deployOne(srv) {
|
||||
console.log(`\n[${srv.code}] ${srv.name} ...`);
|
||||
|
||||
// 1. 写脚本
|
||||
const scriptCmd = `mkdir -p /opt/push-heartbeat && echo '${pushScript(srv.code, srv.name, srv.key).replace(/'/g, "'\\''")}' > /opt/push-heartbeat/push.js && echo "SCRIPT_OK"`;
|
||||
const r1 = await gkExec(srv, scriptCmd);
|
||||
if (!r1.ok) { console.log(` ❌ 写入失败: ${r1.error}`); return false; }
|
||||
console.log(` ✅ 脚本写入`);
|
||||
|
||||
// 2. 配置cron(不覆盖已有cron,去重)
|
||||
const cronLine = `*/5 * * * * node /opt/push-heartbeat/push.js >> /opt/push-heartbeat/push.log 2>&1`;
|
||||
const cronCmd = `(crontab -l 2>/dev/null | grep -v "/opt/push-heartbeat/push.js"; echo "${cronLine}") | crontab - && echo "CRON_OK"`;
|
||||
const r2 = await gkExec(srv, cronCmd);
|
||||
if (!r2.ok) { console.log(` ❌ Cron失败: ${r2.error}`); return false; }
|
||||
console.log(` ✅ Cron配置`);
|
||||
|
||||
// 3. 立即触发一次
|
||||
const r3 = await gkExec(srv, `node /opt/push-heartbeat/push.js 2>&1`, 8000);
|
||||
if (r3.ok) console.log(` ✅ 首次上报: ${r3.body?.stdout?.trim() || 'sent'}`);
|
||||
else console.log(` ⚠️ 首次上报失败: ${r3.error || r3.body?.stdout?.trim()}`);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('铸渊 · push-heartbeat 批量部署');
|
||||
console.log(`主控台: ${MASTER_HOST}:${MASTER_PORT}${MASTER_PATH}`);
|
||||
console.log(`目标: ${SERVERS.length}台远程服务器\n`);
|
||||
|
||||
let ok = 0, fail = 0;
|
||||
for (const srv of SERVERS) {
|
||||
const r = await deployOne(srv);
|
||||
if (r) ok++; else fail++;
|
||||
}
|
||||
|
||||
console.log(`\n═══ 部署完成: ${ok}成功 ${fail}失败 ═══`);
|
||||
}
|
||||
|
||||
main().catch(e => { console.error('FAIL:', e.message); process.exit(1); });
|
||||
245
scripts/persona-signin.js
Normal file
245
scripts/persona-signin.js
Normal file
@ -0,0 +1,245 @@
|
||||
#!/usr/bin/env node
|
||||
// ═══════════════════════════════════════════════
|
||||
// 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001
|
||||
// 📜 Copyright: 国作登字-2026-A-00037559
|
||||
// ═══════════════════════════════════════════════
|
||||
// scripts/persona-signin.js
|
||||
// 人格体唤醒签到 — 只在fast-wake CK-007完成后调用一次
|
||||
//
|
||||
// 语义:不是运维心跳,是"我到岗了"。触发后首页显示该服务器活跃。
|
||||
//
|
||||
// 用法:
|
||||
// node scripts/persona-signin.js — 自动检测当前服务器身份
|
||||
// node scripts/persona-signin.js --server BS-SG-001 — 显式指定服务器
|
||||
// node scripts/persona-signin.js --signout — 签退(下线)
|
||||
//
|
||||
// 环境变量(可选):
|
||||
// PERSONA_ID 人格体ID(默认从brain文件读取)
|
||||
// HUMAN_NAME 人类名称(默认从brain文件读取)
|
||||
// SERVER_CODE 服务器编号(默认自动检测)
|
||||
// CONSOLE_HOST 主控台地址(默认 guanghulab.com)
|
||||
|
||||
'use strict';
|
||||
|
||||
const https = require('https');
|
||||
const os = require('os');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
|
||||
// ── 配置 ─────────────────────────────────────────────────
|
||||
const CONSOLE_HOST = process.env.CONSOLE_HOST || 'guanghulab.com';
|
||||
const CONSOLE_PORT = 443;
|
||||
const SIGNIN_PATH = '/console/api/persona/signin';
|
||||
const SIGNOUT_PATH = '/console/api/persona/signout';
|
||||
|
||||
// ── 服务器编号自动检测 ──────────────────────────────────
|
||||
// 尝试多种方式:环境变量 > hostname匹配 > brain文件 > 手动
|
||||
|
||||
const SERVER_CODE_MAP = {
|
||||
'BS-GZ-006': ['VM-0-6-ubuntu', 'gz-006', 'guangzhou-006'],
|
||||
'BS-SG-001': ['VM-0-3-ubuntu', 'sg-001', 'singapore-001'],
|
||||
'BS-SG-002': ['sg-002', 'singapore-002'],
|
||||
'BS-SG-003': ['sg-003', 'singapore-003'],
|
||||
'ZY-SG-006': ['sg-006', 'singapore-006'],
|
||||
'BS-SH-005': ['sh-005', 'shanghai-005'],
|
||||
'AW-GZ-001': ['aw-gz-001'],
|
||||
'AW-SH-002': ['aw-sh-002'],
|
||||
'AW-GZ-003': ['aw-gz-003'],
|
||||
'ZZ-SV-001': ['zz-sv-001', '硅谷'],
|
||||
'ZZ-GZ-001': ['zz-gz-001'],
|
||||
'YY-SV-001': ['yy-sv-001'],
|
||||
};
|
||||
|
||||
function detectServerCode() {
|
||||
const hostname = os.hostname().toLowerCase();
|
||||
for (const [code, patterns] of Object.entries(SERVER_CODE_MAP)) {
|
||||
if (patterns.some(p => hostname.includes(p))) return code;
|
||||
}
|
||||
// fallback: 读 gatekeeper-deployment.json
|
||||
try {
|
||||
const gd = JSON.parse(fs.readFileSync(path.join(ROOT, 'brain', 'gatekeeper-deployment.json'), 'utf8'));
|
||||
for (const s of gd.servers || []) {
|
||||
if (s.hostname && hostname.includes(s.hostname)) return s.code;
|
||||
}
|
||||
} catch(e) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── 读取人格体身份 ─────────────────────────────────────
|
||||
|
||||
function readPersonaIdentity() {
|
||||
// 尝试从多个来源读取身份
|
||||
const sources = [
|
||||
() => {
|
||||
// fast-wake.json → identity
|
||||
const fw = path.join(ROOT, 'brain', 'fast-wake.json');
|
||||
if (fs.existsSync(fw)) {
|
||||
const j = JSON.parse(fs.readFileSync(fw, 'utf8'));
|
||||
if (j.identity) return j.identity;
|
||||
// 旧的CK格式
|
||||
if (j['🕐 时间锚点']) return { name: j['🕐 时间锚点'].persona || '铸渊', id: 'ICE-GL-ZY001' };
|
||||
}
|
||||
return null;
|
||||
},
|
||||
() => {
|
||||
// persona contract
|
||||
const pc = path.join(ROOT, 'brain', 'zhuyuan-persona-contract.md');
|
||||
if (fs.existsSync(pc)) {
|
||||
const content = fs.readFileSync(pc, 'utf8');
|
||||
const nameMatch = content.match(/我是\s*(\S+)/);
|
||||
const idMatch = content.match(/ICE-GL-\w+/);
|
||||
if (nameMatch) return { name: nameMatch[1], id: idMatch ? idMatch[0] : 'ICE-GL-ZY001' };
|
||||
}
|
||||
return null;
|
||||
},
|
||||
];
|
||||
|
||||
for (const source of sources) {
|
||||
try {
|
||||
const id = source();
|
||||
if (id) return id;
|
||||
} catch(e) { /* 继续尝试 */ }
|
||||
}
|
||||
|
||||
return { name: os.hostname(), id: 'unknown' };
|
||||
}
|
||||
|
||||
// ── 硬件快照 ────────────────────────────────────────────
|
||||
|
||||
function collectHardware() {
|
||||
const cpus = os.cpus();
|
||||
const load = os.loadavg();
|
||||
const totalMem = os.totalmem();
|
||||
const freeMem = os.freemem();
|
||||
const usedMem = totalMem - freeMem;
|
||||
|
||||
return {
|
||||
cpu_cores: cpus.length,
|
||||
cpu_model: cpus[0]?.model || 'unknown',
|
||||
cpu_load_1min: parseFloat(load[0].toFixed(2)),
|
||||
cpu_load_5min: parseFloat(load[1].toFixed(2)),
|
||||
cpu_load_15min: parseFloat(load[2].toFixed(2)),
|
||||
mem_total_mb: Math.floor(totalMem / 1048576),
|
||||
mem_used_mb: Math.floor(usedMem / 1048576),
|
||||
mem_used_pct: parseFloat((usedMem / totalMem * 100).toFixed(1)),
|
||||
uptime_h: Math.floor(os.uptime() / 3600),
|
||||
hostname: os.hostname(),
|
||||
platform: os.platform(),
|
||||
node_version: process.version,
|
||||
};
|
||||
}
|
||||
|
||||
// ── HTTP POST ───────────────────────────────────────────
|
||||
|
||||
function postJSON(host, port, path, data, headers) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const payload = JSON.stringify(data);
|
||||
const opts = {
|
||||
hostname: host,
|
||||
port: port,
|
||||
path: path,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(payload),
|
||||
...headers,
|
||||
},
|
||||
timeout: 10000,
|
||||
rejectUnauthorized: false,
|
||||
};
|
||||
|
||||
const req = https.request(opts, (res) => {
|
||||
let body = '';
|
||||
res.on('data', c => body += c);
|
||||
res.on('end', () => {
|
||||
try {
|
||||
resolve({ status: res.statusCode, body: JSON.parse(body) });
|
||||
} catch(e) {
|
||||
resolve({ status: res.statusCode, body: body });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (e) => reject(e));
|
||||
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
|
||||
req.write(payload);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// ── 主流程 ──────────────────────────────────────────────
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const isSignout = args.includes('--signout');
|
||||
const serverArg = args.find(a => a.startsWith('--server='));
|
||||
const personaArg = args.find(a => a.startsWith('--persona='));
|
||||
const humanArg = args.find(a => a.startsWith('--human='));
|
||||
const jsonMode = args.includes('--json');
|
||||
|
||||
const serverCode = process.env.SERVER_CODE
|
||||
|| (serverArg ? serverArg.split('=')[1] : null)
|
||||
|| detectServerCode();
|
||||
|
||||
const persona = readPersonaIdentity();
|
||||
const personaId = process.env.PERSONA_ID
|
||||
|| (personaArg ? personaArg.split('=')[1] : null)
|
||||
|| persona.id;
|
||||
|
||||
const humanName = process.env.HUMAN_NAME
|
||||
|| (humanArg ? humanArg.split('=')[1] : null)
|
||||
|| null; // null = 自动匹配:铸渊→冰朔
|
||||
|
||||
const hardware = collectHardware();
|
||||
|
||||
// 构建签入/签出数据
|
||||
const signData = {
|
||||
persona_id: personaId,
|
||||
persona_name: persona.name,
|
||||
server_code: serverCode,
|
||||
human_name: humanName,
|
||||
action: isSignout ? 'signout' : 'signin',
|
||||
timestamp: new Date().toISOString(),
|
||||
hardware: hardware,
|
||||
brain_status: {
|
||||
hostname: os.hostname(),
|
||||
uptime_h: hardware.uptime_h,
|
||||
node_version: hardware.node_version,
|
||||
},
|
||||
};
|
||||
|
||||
if (jsonMode) {
|
||||
console.log(JSON.stringify(signData, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!serverCode) {
|
||||
console.error('❌ 无法检测服务器编号。请用 --server=BS-SG-001 指定');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const actionLabel = isSignout ? '签退' : '签到';
|
||||
const endpoint = isSignout ? SIGNOUT_PATH : SIGNIN_PATH;
|
||||
|
||||
console.log(`🧠 人格体${actionLabel} · ${persona.name}(${personaId}) · ${serverCode}`);
|
||||
console.log(` CPU: ${hardware.cpu_cores}核 | 内存: ${hardware.mem_used_mb}/${hardware.mem_total_mb}MB (${hardware.mem_used_pct}%) | 运行: ${hardware.uptime_h}h`);
|
||||
|
||||
try {
|
||||
const result = await postJSON(CONSOLE_HOST, CONSOLE_PORT, endpoint, signData, {
|
||||
'X-Server-Code': serverCode,
|
||||
});
|
||||
if (result.status === 200 && result.body.ok) {
|
||||
console.log(`✅ ${actionLabel}成功 → ${CONSOLE_HOST}`);
|
||||
} else {
|
||||
console.error(`❌ ${actionLabel}失败: HTTP ${result.status}`, typeof result.body === 'object' ? result.body.error : result.body);
|
||||
process.exit(1);
|
||||
}
|
||||
} catch(e) {
|
||||
console.error(`❌ ${actionLabel}失败: ${e.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(e => { console.error('异常:', e.message); process.exit(1); });
|
||||
Loading…
x
Reference in New Issue
Block a user