D112: server.js v3.2 final — 9台服务器全配置(冰朔6台+企业3台),池API完整
This commit is contained in:
parent
1e1d62ec5c
commit
da37f23ae4
@ -1,6 +1,6 @@
|
||||
// # 光湖服务器主控台 API
|
||||
// # Guanghu Server Console v3.2 — with Pool Registry API
|
||||
// # 部署: guanghulab.com/console/
|
||||
// # 部署: guanghulab.com/console/ · SG-001:3920
|
||||
// # 版权: 国作登字-2026-A-00037559 · TCS-0002∞
|
||||
|
||||
const express = require('express');
|
||||
@ -13,421 +13,109 @@ 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 读取)
|
||||
// 全服务器配置 — 冰朔6台 + 企业3台
|
||||
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 },
|
||||
{ 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 },
|
||||
{ code: "AW-GZ-001", name: "企业主服务器 · 广州", ip: "43.139.251.175", key: "zy_gtw_cab20e8c1b957aaad7507bf542231521bba30665461aa540", port: 3910 },
|
||||
{ code: "AW-SH-002", name: "企业灾备 · 上海", ip: "124.222.54.198", key: "zy_gtw_242f1dd3b14c1260fb01196083abde3dcb85e53532d27666", port: 3910 },
|
||||
{ code: "AW-GZ-003", name: "企业灾备 · 广州", ip: "119.29.181.132", key: "zy_gtw_8c9a8b439af823690ae6ad57f52734771ff39493ab986032", 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 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 }));
|
||||
});
|
||||
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();
|
||||
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() };
|
||||
}
|
||||
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 a = req.headers.authorization || ''; const t = a.replace('Bearer ', ''); if (!t) return false; try { const d = fs.readFileSync('/tmp/zhuyuan-keys.json', 'utf-8'); return JSON.parse(d || '[]').some(k => k.value === t); } catch(e) { return false; } }
|
||||
|
||||
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 ==========
|
||||
app.get('/api/servers', async (req, res) => { const r = await Promise.all(SERVERS.map(s => pingServer(s))); res.json({ ok: true, servers: r, timestamp: new Date().toISOString() }); });
|
||||
|
||||
// ========== 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: '敲门已发送,等待冰朔确认' });
|
||||
const { target, reason } = req.body; if (!target) return res.status(400).json({ error: '缺少 target' });
|
||||
const s = SERVERS.find(s => s.code === target || s.name.includes(target)); if (!s) return res.status(404).json({ error: '未找到' });
|
||||
const rid = 'req_' + crypto.randomBytes(8).toString('hex'); authQueue.push({ id: rid, target: s.code, target_name: s.name, reason: reason || '未说明', status: 'pending', created_at: new Date().toISOString(), temp_key: null });
|
||||
res.json({ ok: true, request_id: rid, message: '等待冰朔确认' });
|
||||
});
|
||||
|
||||
// 3. 获取待处理的授权请求
|
||||
app.get('/api/pending', (req, res) => {
|
||||
res.json({ ok: true, requests: authQueue.filter(r => r.status === 'pending') });
|
||||
});
|
||||
|
||||
// 4. 冰朔确认授权
|
||||
app.get('/api/pending', (req, res) => { res.json({ ok: true, requests: authQueue.filter(r => r.status === 'pending') }); });
|
||||
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分钟' });
|
||||
const { request_id } = req.body; const e = authQueue.find(r => r.id === request_id && r.status === 'pending'); if (!e) return res.status(404).json({ error: '未找到' });
|
||||
const tk = 'tmp_' + crypto.randomBytes(24).toString('hex'); e.status = 'approved'; e.temp_key = tk; e.approved_at = new Date().toISOString();
|
||||
tempKeys[tk] = { server: e.target, expires_at: Date.now() + 5*60*1000, request_id }; res.json({ ok: true, temp_key: tk, 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/reject', (req, res) => { const e = authQueue.find(r => r.id === req.body.request_id && r.status === 'pending'); if (e) e.status = 'rejected'; res.json({ ok: true }); });
|
||||
app.get('/api/verify-key/:key', (req, res) => { const e = tempKeys[req.params.key]; if (!e) return res.json({ ok: false, valid: false }); if (Date.now() > e.expires_at) { delete tempKeys[req.params.key]; return res.json({ ok: false, valid: false }); } res.json({ ok: true, valid: true, server: e.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: '未找到服务器' });
|
||||
const { temp_key, target, cmd } = req.body; if (!temp_key || !target || !cmd) return res.status(400).json({ error: '缺少参数' });
|
||||
const ke = tempKeys[temp_key]; if (!ke || Date.now() > ke.expires_at) return res.status(401).json({ error: '密钥无效' });
|
||||
const s = SERVERS.find(s => s.code === target); if (!s) 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 });
|
||||
const d = JSON.stringify({ cmd }); const o = { hostname: s.ip, port: s.port, path: '/exec', method: 'POST', headers: { 'Authorization': 'Bearer ' + s.key, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(d) }, timeout: 30000 };
|
||||
const r = await new Promise((resolve) => { const req = http.request(o, (r) => { let b = ''; r.on('data', c => b += c); r.on('end', () => { try { resolve(JSON.parse(b)); } catch(e) { resolve({ error: b }); } }); }); req.on('error', (e) => resolve({ error: e.message })); req.write(d); req.end(); });
|
||||
res.json({ ok: true, result: r });
|
||||
} 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 新增) ==========
|
||||
|
||||
// ========== 节点池 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 loadPoolRegistry() { try { if (fs.existsSync(POOL_REGISTRY_FILE)) return JSON.parse(fs.readFileSync(POOL_REGISTRY_FILE, 'utf-8')); } catch(e) {} return { active: {}, pending: {}, history: [] }; }
|
||||
function savePoolRegistry(r) { r.updated_at = new Date().toISOString(); try { fs.mkdirSync(DATA_DIR, { recursive: true }); } catch(e) {} fs.writeFileSync(POOL_REGISTRY_FILE, JSON.stringify(r, 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(d) { try { fs.mkdirSync(DATA_DIR, { recursive: true }); } catch(e) {} fs.writeFileSync(JOIN_TOKENS_FILE, JSON.stringify(d, null, 2)); }
|
||||
function sha256(s) { return crypto.createHash('sha256').update(s).digest('hex'); }
|
||||
|
||||
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 });
|
||||
if (!join_token || !node_code || !side) return res.status(400).json({ ok: false, error: '缺少参数' });
|
||||
const jt = loadJoinTokens(); const th = sha256(join_token); const mt = jt.tokens.find(t => t.token_hash === th);
|
||||
if (!mt) return res.status(401).json({ ok: false, error: '加入令牌无效' });
|
||||
if (mt.used >= (mt.max_uses || 1)) return res.status(401).json({ ok: false, error: '令牌已用' });
|
||||
const r = loadPoolRegistry(); if (r.active[node_code]) return res.status(409).json({ ok: false, error: '节点已存在' });
|
||||
const rid = 'reg_' + crypto.randomBytes(8).toString('hex');
|
||||
r.pending[node_code] = { registration_id: rid, 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: mt.token_id };
|
||||
mt.used = (mt.used || 0) + 1; saveJoinTokens(jt); savePoolRegistry(r);
|
||||
res.json({ ok: true, status: 'pending', registration_id: rid, node_code, 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: '未找到注册请求' });
|
||||
app.get('/api/pool/join-status/:rid', (req, res) => {
|
||||
const r = loadPoolRegistry();
|
||||
for (const [c, n] of Object.entries(r.active)) { if (n.registration_id === req.params.rid) return res.json({ ok: true, status: 'approved', node_code: c, pool_token: n.pool_token, report_endpoint: 'https://43.156.237.110:3920/api/pool/report', side: n.side }); }
|
||||
for (const [c, n] of Object.entries(r.pending)) { if (n.registration_id === req.params.rid) return res.json({ ok: true, status: 'pending', node_code: c }); }
|
||||
res.status(404).json({ ok: false });
|
||||
});
|
||||
|
||||
// 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' }); }
|
||||
const { registration_id, action } = req.body; if (!registration_id || !action) return res.status(400).json({ ok: false });
|
||||
const r = loadPoolRegistry(); let f = null, fc = null;
|
||||
for (const [c, n] of Object.entries(r.pending)) { if (n.registration_id === registration_id) { f = n; fc = c; break; } }
|
||||
if (!f) return res.status(404).json({ ok: false });
|
||||
if (action === 'approve') { const pt = 'zyp_' + crypto.randomBytes(16).toString('hex'); r.active[fc] = { node_code: fc, node_name: f.node_name, side: f.side, team_id: f.team_id, ip: f.network?.public_ip || '', pool_token: pt, joined_at: new Date().toISOString(), hardware: f.hardware || {} }; delete r.pending[fc]; savePoolRegistry(r); res.json({ ok: true, node_code: fc, pool_token: pt }); }
|
||||
else { r.history.push({ ...f, status: 'rejected', rejected_at: new Date().toISOString() }); delete r.pending[fc]; savePoolRegistry(r); res.json({ ok: true, action: 'rejected' }); }
|
||||
});
|
||||
|
||||
// 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 ==========
|
||||
app.get('/api/pool/tokens', (req, res) => { const r = loadPoolRegistry(); res.json({ ok: true, nodes: Object.entries(r.active).map(([c, n]) => ({ node_code: c, node_name: n.node_name, side: n.side, team_id: n.team_id, hardware: n.hardware })) }); });
|
||||
app.delete('/api/pool/node/:code', (req, res) => { const r = loadPoolRegistry(); if (!r.active[req.params.code]) return res.status(404).json({ ok: false }); r.history.push({ ...r.active[req.params.code], status: 'removed', removed_at: new Date().toISOString() }); delete r.active[req.params.code]; savePoolRegistry(r); res.json({ ok: true }); });
|
||||
|
||||
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);
|
||||
});
|
||||
app.listen(PORT, '127.0.0.1', () => { console.log('光湖主控台 v3.2 · ' + SERVERS.length + '台 | SG-001:' + PORT); });
|
||||
Loading…
x
Reference in New Issue
Block a user