246 lines
8.7 KiB
JavaScript
246 lines
8.7 KiB
JavaScript
|
|
#!/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); });
|