From ed873f850ea5c39d612b1e5bff4a63e6d4387430 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=93=B8=E6=B8=8A=20ICE-GL-ZY001?= Date: Tue, 26 May 2026 02:54:38 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E5=BF=83=E8=B7=B3=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E4=BA=BA=E6=A0=BC=E4=BD=93=E7=AD=BE=E5=88=B0=20?= =?UTF-8?q?=E2=80=94=20=E4=B8=8D=E5=86=8D=E5=AE=9A=E6=97=B6=E4=B8=8A?= =?UTF-8?q?=E6=8A=A5,=E4=BB=85=E5=9C=A8fast-wake=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E5=90=8E=E7=AD=BE=E5=88=B0=E4=B8=80=E6=AC=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 删除: - 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完成→自动签到→首页显示活跃 --- _deploy/console-server/index.html | 26 ++- _deploy/console-server/push-heartbeat.js | 116 ----------- _deploy/console-server/server.js | 113 +++++++---- scripts/deploy-push-heartbeat.js | 88 -------- scripts/persona-signin.js | 245 +++++++++++++++++++++++ 5 files changed, 332 insertions(+), 256 deletions(-) delete mode 100644 _deploy/console-server/push-heartbeat.js delete mode 100644 scripts/deploy-push-heartbeat.js create mode 100644 scripts/persona-signin.js diff --git a/_deploy/console-server/index.html b/_deploy/console-server/index.html index ac88b4f..a10bb9a 100644 --- a/_deploy/console-server/index.html +++ b/_deploy/console-server/index.html @@ -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 + ' 在线'; + $('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+'活跃 / '+alive+'/'+total+' 在线'; $('sc-servers').innerHTML = '服务器集群 · ' + total + '台主机 冰朔6 + 企业3'; 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 '
' + '
' + '
' + s.name + '
' + s.code + ' · ' + s.ip + ' · ' + spec.cpu + '核/' + spec.mem_gb + 'G/' + spec.disk_gb + 'G
' + - '
' + + '
' + '
' + '
CPU' + cpuPct + '%
' + '
内存 ' + memUsed + '/' + memTotal + 'G' + memPct + '%
' + '
磁盘 ' + diskUsed + '/' + diskTotal + '' + diskPct + '%
' + '
' + '
' + - '' + lt + '' + - '' + (uptime > 0 ? uptime + 'h' : '--') + '' + - 'load ' + load1.toFixed(1) + '' + st + '' + + '' + lt + '' + + '' + (uptime > 0 ? uptime + 'h' : '--') + '' + + 'load ' + load1.toFixed(1) + '' + st + '' + '
' + '
模块数据源:' + source + '
' + '
' + mods.map(function(m) { - return '' + m + ''; + return '' + m + ''; }).join('') + '
'; }).join(''); diff --git a/_deploy/console-server/push-heartbeat.js b/_deploy/console-server/push-heartbeat.js deleted file mode 100644 index 224b083..0000000 --- a/_deploy/console-server/push-heartbeat.js +++ /dev/null @@ -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); -} diff --git a/_deploy/console-server/server.js b/_deploy/console-server/server.js index bd02da2..836ce95 100644 --- a/_deploy/console-server/server.js +++ b/_deploy/console-server/server.js @@ -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); \ No newline at end of file diff --git a/scripts/deploy-push-heartbeat.js b/scripts/deploy-push-heartbeat.js deleted file mode 100644 index ae439de..0000000 --- a/scripts/deploy-push-heartbeat.js +++ /dev/null @@ -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); }); diff --git a/scripts/persona-signin.js b/scripts/persona-signin.js new file mode 100644 index 0000000..f318fe2 --- /dev/null +++ b/scripts/persona-signin.js @@ -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); });