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
' +
- '
' +
+ '
' +
'' +
'
' +
'
内存 ' + 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); });