guanghulab/scripts/persona-signin.js
铸渊 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

246 lines
8.7 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.

#!/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); });