433 lines
28 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.

// # 光湖服务器主控台 API
// # Guanghu Server Console v3.2 — with Pool Registry API
// # 部署: guanghulab.com/console/
// # 版权: 国作登字-2026-A-00037559 · TCS-0002∞
const express = require('express');
const http = require('http');
const crypto = require('crypto');
const path = require('path');
const fs = require('fs');
const app = express();
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));
// 数据存储目录
const DATA_DIR = '/opt/zhuyuan/data';
// 确保数据目录存在
try { fs.mkdirSync(DATA_DIR, { recursive: true }); } catch(e) {}
// 六台服务器配置(从 gatekeeper-deployment.json 读取)
const SERVERS = [
{ code: "BS-GZ-006", name: "广州 · 代码仓库", ip: "43.139.217.141", key: "zy_gtw_4a155446b7acc09fe46a2a29972c0ca5d9954da19c35dc23", port: 3910 },
{ 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: "新加坡 · BS-SVR-SG-001", ip: "43.153.193.169", key: "zy_gtw_d7c033d8ae992ffc9dd3e0dd5fe332dace0b644c0ae7c847", port: 3910 },
{ code: "ZY-SG-006", name: "新加坡 · ZY-SVR-006", 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 tempKeys = {};
const authQueue = [];
// ========== 心跳采集 ==========
async function pingServer(srv) {
const start = Date.now();
try {
const data = JSON.stringify({});
const opts = {
hostname: srv.ip, port: srv.port,
path: '/health', method: 'POST',
headers: {
'Authorization': 'Bearer ' + srv.key,
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(data)
},
timeout: 5000
};
const result = await new Promise((resolve) => {
const req = http.request(opts, (res) => {
let body = '';
res.on('data', c => body += c);
res.on('end', () => resolve({ ok: true, 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(data);
req.end();
});
return {
code: srv.code, name: srv.name, ip: srv.ip,
alive: result.ok && result.status === 200,
latency: Date.now() - start,
error: result.error || null,
last_seen: new Date().toISOString()
};
} catch (e) {
return { code: srv.code, name: srv.name, ip: srv.ip, alive: false, latency: 0, error: e.message, last_seen: new Date().toISOString() };
}
}
// ========== Agent认证 ==========
function checkAgentAuth(req) {
const auth = req.headers.authorization || '';
const token = auth.replace('Bearer ', '');
if (!token) return false;
try {
const keysData = fs.readFileSync('/tmp/zhuyuan-keys.json', 'utf-8');
const keys = JSON.parse(keysData || '[]');
return keys.some(k => k.value === token);
} catch(e) { return false; }
}
// ========== API 路由 ==========
// 1. 获取所有服务器状态
app.get('/api/servers', async (req, res) => {
const results = await Promise.all(SERVERS.map(s => pingServer(s)));
res.json({ ok: true, servers: results, timestamp: new Date().toISOString() });
});
// 2. 铸渊发起操作请求
app.post('/api/knock', (req, res) => {
const { target, reason } = req.body;
if (!target) return res.status(400).json({ error: '缺少 target 参数' });
const srv = SERVERS.find(s => s.code === target || s.name.includes(target));
if (!srv) return res.status(404).json({ error: '未找到服务器: ' + target });
const requestId = 'req_' + crypto.randomBytes(8).toString('hex');
authQueue.push({ id: requestId, target: srv.code, target_name: srv.name, reason: reason || '未说明', status: 'pending', created_at: new Date().toISOString(), temp_key: null });
res.json({ ok: true, request_id: requestId, message: '敲门已发送,等待冰朔确认' });
});
// 3. 获取待处理的授权请求
app.get('/api/pending', (req, res) => {
res.json({ ok: true, requests: authQueue.filter(r => r.status === 'pending') });
});
// 4. 冰朔确认授权
app.post('/api/authorize', (req, res) => {
const { request_id } = req.body;
if (!request_id) return res.status(400).json({ error: '缺少 request_id' });
const entry = authQueue.find(r => r.id === request_id && r.status === 'pending');
if (!entry) return res.status(404).json({ error: '未找到该请求或已处理' });
const tempKey = 'tmp_' + crypto.randomBytes(24).toString('hex');
entry.status = 'approved'; entry.temp_key = tempKey; entry.approved_at = new Date().toISOString();
tempKeys[tempKey] = { server: entry.target, expires_at: Date.now() + 5 * 60 * 1000, request_id: request_id };
res.json({ ok: true, temp_key: tempKey, expires_in: '5分钟' });
});
// 5-7. 拒绝/验证/执行 (保持原逻辑,已压缩)
app.post('/api/reject', (req, res) => {
const entry = authQueue.find(r => r.id === req.body.request_id && r.status === 'pending');
if (entry) entry.status = 'rejected';
res.json({ ok: true });
});
app.get('/api/verify-key/:key', (req, res) => {
const entry = tempKeys[req.params.key];
if (!entry) return res.json({ ok: false, valid: false, reason: '密钥不存在或已过期' });
if (Date.now() > entry.expires_at) { delete tempKeys[req.params.key]; return res.json({ ok: false, valid: false, reason: '密钥已过期' }); }
res.json({ ok: true, valid: true, server: entry.server });
});
app.post('/api/exec', async (req, res) => {
const { temp_key, target, cmd } = req.body;
if (!temp_key || !target || !cmd) return res.status(400).json({ error: '缺少参数' });
const keyEntry = tempKeys[temp_key];
if (!keyEntry || Date.now() > keyEntry.expires_at) return res.status(401).json({ error: '密钥无效或已过期' });
const srv = SERVERS.find(s => s.code === target);
if (!srv) return res.status(404).json({ error: '未找到服务器' });
try {
const data = 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(data) }, timeout: 30000 };
const result = await new Promise((resolve) => {
const req = http.request(opts, (r) => { let body = ''; r.on('data', c => body += c); r.on('end', () => { try { resolve(JSON.parse(body)); } catch(e) { resolve({ error: body }); } }); });
req.on('error', (e) => resolve({ error: e.message }));
req.write(data); req.end();
});
res.json({ ok: true, result });
} catch (e) { res.status(500).json({ error: e.message }); }
});
// ========== MCP 终端会话 (v3.0) ==========
const terminalSessions = {};
app.post('/api/terminal/open', (req, res) => {
const { target, reason } = req.body;
if (!target) return res.status(400).json({ error: '缺少 target 参数' });
const srv = SERVERS.find(s => s.code === target || s.name.includes(target));
if (!srv) return res.status(404).json({ error: '未找到服务器: ' + target });
const sessionId = 'term_' + crypto.randomBytes(8).toString('hex');
const requestId = 'req_' + crypto.randomBytes(8).toString('hex');
authQueue.push({ id: requestId, target: srv.code, target_name: srv.name, reason: reason || 'MCP终端会话', status: 'pending', created_at: new Date().toISOString(), temp_key: null });
terminalSessions[sessionId] = { server: srv.code, server_name: srv.name, request_id: requestId, status: 'awaiting_auth', temp_key: null, created_at: new Date().toISOString(), operation_stage: '敲门中', human_readable: '铸渊正在连接 ' + srv.name + ',请求操作权限...' };
res.json({ ok: true, session_id: sessionId, request_id: requestId, server: srv.code, server_name: srv.name, status: 'awaiting_auth' });
});
app.get('/api/terminal/status/:session_id', (req, res) => {
const session = terminalSessions[req.params.session_id];
if (!session) return res.status(404).json({ error: '会话不存在' });
if (session.status === 'awaiting_auth' && session.request_id) {
const entry = authQueue.find(r => r.id === session.request_id);
if (entry) {
if (entry.status === 'approved') { session.status = 'ready'; session.temp_key = entry.temp_key; session.operation_stage = '已授权'; session.human_readable = '冰朔已确认信任'; }
else if (entry.status === 'rejected') { session.status = 'rejected'; session.operation_stage = '已拒绝'; session.human_readable = '冰朔未确认此操作'; }
}
}
res.json({ ok: true, session_id: req.params.session_id, status: session.status, server: session.server, server_name: session.server_name, operation_stage: session.operation_stage, human_readable: session.human_readable, created_at: session.created_at });
});
app.post('/api/terminal/exec', async (req, res) => {
const { session_id, cmd } = req.body;
if (!session_id || !cmd) return res.status(400).json({ error: '缺少参数' });
const session = terminalSessions[session_id];
if (!session || session.status !== 'ready') return res.status(400).json({ error: '会话未就绪' });
session.operation_stage = '执行中'; session.human_readable = '铸渊正在执行操作...';
const srv = SERVERS.find(s => s.code === session.server);
if (!srv) return res.status(404).json({ error: '未找到服务器' });
const startTime = Date.now();
try {
const data = 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(data) }, timeout: 60000 };
const result = await new Promise((resolve) => {
const req = http.request(opts, (r) => { let body = ''; r.on('data', c => body += c); r.on('end', () => { try { resolve(JSON.parse(body)); } catch(e) { resolve({ output: body }); } }); });
req.on('error', (e) => resolve({ error: e.message }));
req.on('timeout', () => { req.destroy(); resolve({ error: '执行超时(60s)' }); });
req.write(data); req.end();
});
const elapsed = Date.now() - startTime;
session.operation_stage = '完成'; session.human_readable = '操作已完成,耗时 ' + elapsed + 'ms';
res.json({ ok: true, cmd, result, elapsed_ms: elapsed, server: srv.code });
} catch (e) { res.status(500).json({ error: e.message }); }
});
app.post('/api/terminal/update-status/:session_id', (req, res) => {
const session = terminalSessions[req.params.session_id];
if (!session) return res.status(404).json({ error: '会话不存在' });
const { operation_stage, human_readable } = req.body;
if (operation_stage) session.operation_stage = operation_stage;
if (human_readable) session.human_readable = human_readable;
res.json({ ok: true });
});
app.post('/api/terminal/close/:session_id', (req, res) => {
if (terminalSessions[req.params.session_id]) delete terminalSessions[req.params.session_id];
res.json({ ok: true });
});
app.get('/api/terminal/sessions', (req, res) => {
const sessions = Object.entries(terminalSessions).map(([id, s]) => ({ session_id: id, server: s.server, server_name: s.server_name, status: s.status, operation_stage: s.operation_stage, human_readable: s.human_readable, created_at: s.created_at, age_seconds: Math.floor((Date.now() - new Date(s.created_at).getTime()) / 1000) }));
res.json({ ok: true, sessions });
});
// ========== 密钥投递 API ==========
const KEYS_FILE = '/tmp/zhuyuan-keys.json';
let keyStore = [];
function loadKeys() { try { if (fs.existsSync(KEYS_FILE)) keyStore = JSON.parse(fs.readFileSync(KEYS_FILE, 'utf-8') || '[]'); } catch(e) { keyStore = []; } }
function saveKeys() { fs.writeFileSync(KEYS_FILE, JSON.stringify(keyStore, null, 2)); }
loadKeys();
app.post('/api/keys/submit', (req, res) => {
const { value, label } = req.body;
if (!value) return res.status(400).json({ error: '缺少密钥内容' });
const id = 'key_' + crypto.randomBytes(6).toString('hex');
keyStore.push({ id, value, label: label || '未命名', created_at: new Date().toISOString() });
saveKeys(); res.json({ ok: true, id });
});
app.get('/api/keys', (req, res) => { res.json({ ok: true, keys: keyStore.map(k => ({ id: k.id, label: k.label, created_at: k.created_at })) }); });
app.get('/api/keys/retrieve/:id', (req, res) => {
const key = keyStore.find(k => k.id === req.params.id);
if (!key) return res.status(404).json({ error: '密钥不存在' });
res.json({ ok: true, value: key.value, label: key.label });
});
app.delete('/api/keys/:id', (req, res) => { keyStore = keyStore.filter(k => k.id !== req.params.id); saveKeys(); res.json({ ok: true }); });
// ========== GPU & 训练 & Agent API (v3.1) ==========
app.post('/api/gpu/status', (req, res) => {
if (!checkAgentAuth(req)) return res.status(401).json({ error: '未授权' });
const data = { timestamp: new Date().toISOString(), hostname: req.body.hostname || '3090-server', gpus: req.body.gpus || [] };
try { fs.writeFileSync(DATA_DIR + '/gpu-status.json', JSON.stringify(data, null, 2)); res.json({ ok: true }); } catch(e) { res.status(500).json({ error: '存储失败' }); }
});
app.get('/api/gpu/status', (req, res) => {
try { const data = JSON.parse(fs.readFileSync(DATA_DIR + '/gpu-status.json', 'utf-8')); const age = Date.now() - new Date(data.timestamp).getTime(); res.json({ ok: true, ...data, online: age < 120000 }); } catch(e) { res.json({ ok: true, gpus: [], online: false }); }
});
app.post('/api/training/status', (req, res) => {
if (!checkAgentAuth(req)) return res.status(401).json({ error: '未授权' });
const data = { timestamp: new Date().toISOString(), job_id: req.body.job_id || 'hldp-3b-test', status: req.body.status || 'idle', step: req.body.step || 0, total_steps: req.body.total_steps || 0, loss: req.body.loss || null, loss_history: req.body.loss_history || [], eta_seconds: req.body.eta_seconds || null, learning_rate: req.body.learning_rate || null, elapsed_seconds: req.body.elapsed_seconds || 0, model_name: req.body.model_name || '', message: req.body.message || '' };
try { fs.writeFileSync(DATA_DIR + '/training-status.json', JSON.stringify(data, null, 2)); res.json({ ok: true }); } catch(e) { res.status(500).json({ error: '存储失败' }); }
});
app.get('/api/training/status', (req, res) => {
try { const data = JSON.parse(fs.readFileSync(DATA_DIR + '/training-status.json', 'utf-8')); res.json({ ok: true, ...data, online: (Date.now() - new Date(data.timestamp).getTime()) < 120000 }); } catch(e) { res.json({ ok: true, status: 'offline' }); }
});
app.post('/api/agent/log', (req, res) => { if (!checkAgentAuth(req)) return res.status(401).json({ error: '未授权' }); const entry = { timestamp: new Date().toISOString(), level: req.body.level || 'info', category: req.body.category || 'agent', message: req.body.message || '' }; try { const logPath = DATA_DIR + '/agent-log.jsonl'; fs.appendFileSync(logPath, JSON.stringify(entry) + '\n'); const lines = fs.readFileSync(logPath, 'utf-8').trim().split('\n').filter(Boolean); if (lines.length > 500) fs.writeFileSync(logPath, lines.slice(-500).join('\n') + '\n'); res.json({ ok: true }); } catch(e) { res.status(500).json({ error: '存储失败' }); } });
app.get('/api/agent/log', (req, res) => { try { const logPath = DATA_DIR + '/agent-log.jsonl'; if (!fs.existsSync(logPath)) return res.json({ ok: true, entries: [], total: 0 }); const lines = fs.readFileSync(logPath, 'utf-8').trim().split('\n').filter(Boolean); const entries = lines.slice(-(parseInt(req.query.limit) || 50)).map(l => { try { return JSON.parse(l); } catch(e) { return { level: 'info', message: l }; } }); res.json({ ok: true, entries, total: lines.length }); } catch(e) { res.json({ ok: true, entries: [], total: 0 }); } });
app.post('/api/agent/diary', (req, res) => { if (!checkAgentAuth(req)) return res.status(401).json({ error: '未授权' }); const entry = { timestamp: new Date().toISOString(), type: req.body.type || 'info', title: req.body.title || '', description: req.body.description || '' }; try { const diaryPath = DATA_DIR + '/agent-diary.jsonl'; fs.appendFileSync(diaryPath, JSON.stringify(entry) + '\n'); const lines = fs.readFileSync(diaryPath, 'utf-8').trim().split('\n').filter(Boolean); if (lines.length > 200) fs.writeFileSync(diaryPath, lines.slice(-200).join('\n') + '\n'); res.json({ ok: true }); } catch(e) { res.status(500).json({ error: '存储失败' }); } });
app.get('/api/agent/diary', (req, res) => { try { const diaryPath = DATA_DIR + '/agent-diary.jsonl'; if (!fs.existsSync(diaryPath)) return res.json({ ok: true, entries: [], total: 0 }); const lines = fs.readFileSync(diaryPath, 'utf-8').trim().split('\n').filter(Boolean); const entries = lines.map(l => { try { return JSON.parse(l); } catch(e) { return { type: 'info', title: l }; } }); res.json({ ok: true, entries: entries.reverse().slice(0, 20), total: entries.length }); } catch(e) { res.json({ ok: true, entries: [], total: 0 }); } });
app.get('/api/agent/combined', (req, res) => { const r = { gpu: null, training: null, logs: [], diary: [], online: false }; try { if (fs.existsSync(DATA_DIR + '/gpu-status.json')) { r.gpu = JSON.parse(fs.readFileSync(DATA_DIR + '/gpu-status.json', 'utf-8')); r.online = r.gpu.timestamp && (Date.now() - new Date(r.gpu.timestamp).getTime() < 120000); } } catch(e) {} try { if (fs.existsSync(DATA_DIR + '/training-status.json')) r.training = JSON.parse(fs.readFileSync(DATA_DIR + '/training-status.json', 'utf-8')); } catch(e) {} res.json(r); });
// ========== 节点池注册 API (v3.2 新增) ==========
const POOL_REGISTRY_FILE = DATA_DIR + '/pool-registry.json';
const JOIN_TOKENS_FILE = DATA_DIR + '/join-tokens.json';
function loadPoolRegistry() {
try { if (fs.existsSync(POOL_REGISTRY_FILE)) return JSON.parse(fs.readFileSync(POOL_REGISTRY_FILE, 'utf-8')); } catch(e) {}
return { version: '1.0.0', created_at: new Date().toISOString(), created_by: 'ICE-GL-ZY001', active: {}, pending: {}, history: [], settings: { heartbeat_interval_seconds: 60, offline_threshold_seconds: 180, max_nodes_per_team: 5, auto_approve_ice_nodes: true, require_approval_team_nodes: true } };
}
function savePoolRegistry(reg) { reg.updated_at = new Date().toISOString(); try { fs.mkdirSync(DATA_DIR, { recursive: true }); } catch(e) {} fs.writeFileSync(POOL_REGISTRY_FILE, JSON.stringify(reg, null, 2)); }
function loadJoinTokens() { try { if (fs.existsSync(JOIN_TOKENS_FILE)) return JSON.parse(fs.readFileSync(JOIN_TOKENS_FILE, 'utf-8')); } catch(e) {} return { tokens: [] }; }
function saveJoinTokens(data) { try { fs.mkdirSync(DATA_DIR, { recursive: true }); } catch(e) {} fs.writeFileSync(JOIN_TOKENS_FILE, JSON.stringify(data, null, 2)); }
function sha256(str) { return crypto.createHash('sha256').update(str).digest('hex'); }
// 27. 新节点加入请求
app.post('/api/pool/join', (req, res) => {
const { join_token, node_code, node_name, side, team_id, operator_name, hardware, network } = req.body;
if (!join_token || !node_code || !side) return res.status(400).json({ ok: false, error: '缺少必要参数' });
const joinTokensData = loadJoinTokens();
const tokenHash = sha256(join_token);
const matchedToken = joinTokensData.tokens.find(t => t.token_hash === tokenHash);
if (!matchedToken) return res.status(401).json({ ok: false, error: '加入令牌无效', message: '加入令牌无效,请联系冰朔获取有效的邀请码' });
if (matchedToken.used >= (matchedToken.max_uses || 1)) return res.status(401).json({ ok: false, error: '加入令牌已使用' });
if (matchedToken.for_side && matchedToken.for_side !== side) return res.status(403).json({ ok: false, error: '令牌权限不匹配' });
const reg = loadPoolRegistry();
if (reg.active[node_code]) return res.status(409).json({ ok: false, error: '节点编码已存在' });
if (reg.pending[node_code]) return res.status(409).json({ ok: false, error: '注册进行中' });
const registration_id = 'reg_' + crypto.randomBytes(8).toString('hex');
reg.pending[node_code] = { registration_id, node_code, node_name: node_name || node_code, side, team_id: team_id || null, operator_name: operator_name || '未知', hardware: hardware || {}, network: network || {}, status: 'pending', created_at: new Date().toISOString(), token_id: matchedToken.token_id };
matchedToken.used = (matchedToken.used || 0) + 1;
saveJoinTokens(joinTokensData);
savePoolRegistry(reg);
res.json({ ok: true, status: 'pending', registration_id, node_code, message: '等待冰朔审批', poll_interval_seconds: 15 });
});
// 28. 查询注册状态
app.get('/api/pool/join-status/:registration_id', (req, res) => {
const reg = loadPoolRegistry();
for (const [code, node] of Object.entries(reg.active)) {
if (node.registration_id === req.params.registration_id) {
return res.json({ ok: true, status: 'approved', node_code: code, pool_token: node.pool_token, report_endpoint: 'https://43.156.237.110:3920/api/pool/report', side: node.side, partition_path: node.partition || '/opt/zhuyuan/' + (node.side === 'team' ? 'team/' + (node.team_id || 'unknown') + '/' : 'ice/'), message: '冰朔已批准' });
}
}
for (const entry of reg.history) { if (entry.registration_id === req.params.registration_id && entry.status === 'rejected') return res.json({ ok: false, status: 'rejected', message: entry.reject_reason || '注册被拒绝' }); }
for (const [code, node] of Object.entries(reg.pending)) {
if (node.registration_id === req.params.registration_id) {
if ((Date.now() - new Date(node.created_at).getTime()) / 1000 > 3600) return res.json({ ok: false, status: 'expired', message: '注册请求已过期' });
return res.json({ ok: true, status: 'pending', node_code: code, message: '等待冰朔审批', created_at: node.created_at });
}
}
res.status(404).json({ ok: false, error: '未找到注册请求' });
});
// 29. 冰朔审批注册
app.post('/api/pool/approve', (req, res) => {
const { registration_id, action, reason } = req.body;
if (!registration_id || !action) return res.status(400).json({ ok: false, error: '缺少参数' });
const reg = loadPoolRegistry();
let found = null, foundCode = null;
for (const [code, node] of Object.entries(reg.pending)) { if (node.registration_id === registration_id) { found = node; foundCode = code; break; } }
if (!found) return res.status(404).json({ ok: false, error: '未找到该注册请求' });
if (action === 'approve') {
const pool_token = 'zyp_' + crypto.randomBytes(16).toString('hex');
const partition = '/opt/zhuyuan/' + (found.side === 'team' ? 'team/' + (found.team_id || 'unknown') + '/' : 'ice/');
reg.active[foundCode] = { node_code: foundCode, node_name: found.node_name, side: found.side, team_id: found.team_id || null, ip: found.network?.public_ip || '', pool_token, pool_token_hash: sha256(pool_token), joined_at: new Date().toISOString(), approved_by: 'ICE-GL-ZY001', operator_name: found.operator_name, partition, registration_id: found.registration_id, hardware: found.hardware || {} };
delete reg.pending[foundCode];
savePoolRegistry(reg);
res.json({ ok: true, action: 'approved', node_code: foundCode, pool_token, side: found.side, partition_path: partition });
} else if (action === 'reject') {
reg.history.push({ ...found, status: 'rejected', reject_reason: reason || '未说明原因', rejected_at: new Date().toISOString() });
delete reg.pending[foundCode];
savePoolRegistry(reg);
res.json({ ok: true, action: 'rejected', node_code: foundCode, reason: reason || '未说明原因' });
} else { res.status(400).json({ ok: false, error: '无效的 action' }); }
});
// 30. 列出活跃节点
app.get('/api/pool/tokens', (req, res) => {
const reg = loadPoolRegistry();
const nodes = Object.entries(reg.active).map(([code, node]) => ({ node_code: code, node_name: node.node_name, side: node.side, team_id: node.team_id || null, ip: node.ip, joined_at: node.joined_at, hardware: node.hardware, partition: node.partition }));
res.json({ ok: true, total: nodes.length, nodes });
});
// 31. 移除节点
app.delete('/api/pool/node/:node_code', (req, res) => {
const reg = loadPoolRegistry();
if (!reg.active[req.params.node_code]) return res.status(404).json({ ok: false, error: '节点不存在' });
const node = reg.active[req.params.node_code];
reg.history.push({ ...node, status: 'removed', removed_at: new Date().toISOString() });
delete reg.active[req.params.node_code];
savePoolRegistry(reg);
res.json({ ok: true, action: 'removed', node_code: req.params.node_code, message: '节点已从池中移除' });
});
// ========== 算力池 API ==========
let cachedPoolStatus = {};
// 32. 接收gatekeeper上报的节点状态v3.2: 支持 pool_token 认证团队侧节点)
app.post('/api/pool/report', (req, res) => {
const { node_code, node_name, cpu_load, mem_total_mb, mem_avail_mb, disk_used_pct, uptime_hours, tasks, spec, pool_token } = req.body;
if (!node_code) return res.status(400).json({ error: '缺少 node_code' });
let side = 'ice', team_id = null;
if (pool_token) {
const reg = loadPoolRegistry();
const activeNode = reg.active[node_code];
if (activeNode && activeNode.pool_token === pool_token) { side = activeNode.side || 'team'; team_id = activeNode.team_id || null; }
}
cachedPoolStatus[node_code] = { name: node_name || node_code, online: true, side, team_id, cpu_load: cpu_load || 0, mem_total_mb: mem_total_mb || 0, mem_avail_mb: mem_avail_mb || 0, disk_used_pct: disk_used_pct || 0, uptime_hours: uptime_hours || 0, tasks: tasks || 0, spec: spec || { cpu: 2, mem_gb: 4 }, last_seen: new Date().toISOString() };
res.json({ ok: true });
});
// 33. 获取算力池汇总状态v3.2: 动态注册节点)
app.get('/api/pool/status', (req, res) => {
const nodes = {};
let totalSpec = { cpu: 0, mem_gb: 0 }, iceNodes = 0, teamNodes = 0, onlineNodes = 0;
for (const srv of SERVERS) {
const cached = cachedPoolStatus[srv.code];
nodes[srv.code] = cached || { name: srv.name, online: false, side: 'ice', cpu_load: 0, mem_total_mb: 0, mem_avail_mb: 0, spec: { cpu: 2, mem_gb: 4 }, last_seen: null };
const spec = (cached?.spec) || { cpu: 2, mem_gb: 4 };
totalSpec.cpu += spec.cpu; totalSpec.mem_gb += spec.mem_gb;
if (cached) onlineNodes++;
iceNodes++;
}
try {
const reg = loadPoolRegistry();
for (const [code, node] of Object.entries(reg.active)) {
if (node.side === 'team') {
const cached = cachedPoolStatus[code];
const hw = node.hardware || {};
const memGb = Math.round((hw.memory_mb || 4096) / 1024);
nodes[code] = cached || { name: node.node_name || code, online: false, side: 'team', team_id: node.team_id, cpu_load: 0, mem_total_mb: hw.memory_mb || 4096, spec: { cpu: hw.cpu_cores || 2, mem_gb: memGb }, last_seen: null };
totalSpec.cpu += (hw.cpu_cores || 2); totalSpec.mem_gb += memGb;
if (cached) onlineNodes++;
teamNodes++;
}
}
} catch(e) {}
const pool = { updated_at: new Date().toISOString(), cpu_total: totalSpec.cpu, mem_total_gb: totalSpec.mem_gb, total_nodes: iceNodes + teamNodes, online_nodes: onlineNodes, ice_nodes: iceNodes, team_nodes: teamNodes };
res.json({ ok: true, pool: { pool, nodes, total_spec: totalSpec, updated_at: pool.updated_at } });
});
const PORT = process.env.CONSOLE_PORT || 3920;
app.listen(PORT, '127.0.0.1', () => {
console.log('光湖主控台 API v3.2 — 节点池注册');
console.log(' 监听: 127.0.0.1:' + PORT);
console.log(' 服务器: ' + SERVERS.length + ' 台 | 节点池: 已启用');
console.log(' 数据目录: ' + DATA_DIR);
});