680 lines
23 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.1 — with GPU & Agent 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');
const entry = {
id: requestId,
target: srv.code,
target_name: srv.name,
reason: reason || '未说明',
status: 'pending',
created_at: new Date().toISOString(),
temp_key: null
};
authQueue.push(entry);
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. 拒绝授权
app.post('/api/reject', (req, res) => {
const { request_id } = req.body;
const entry = authQueue.find(r => r.id === request_id && r.status === 'pending');
if (entry) entry.status = 'rejected';
res.json({ ok: true });
});
// 6. 验证临时密钥是否有效
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 });
});
// 7. 通过临时密钥执行命令
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 = {};
// 8. 打开终端会话(自动敲门)
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');
const entry = {
id: requestId,
target: srv.code,
target_name: srv.name,
reason: reason || 'MCP终端会话',
status: 'pending',
created_at: new Date().toISOString(),
temp_key: null
};
authQueue.push(entry);
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'
});
});
// 9. 轮询终端会话状态
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
});
});
// 10. 在终端会话中执行命令
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) return res.status(404).json({ error: '会话不存在' });
if (session.status !== 'ready') return res.status(400).json({ error: '会话尚未就绪,等待冰朔授权中' });
session.operation_stage = '执行中';
session.human_readable = '铸渊正在 ' + session.server_name + ' 上执行操作...';
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: cmd,
result: result,
elapsed_ms: elapsed,
server: srv.code
});
} catch (e) {
res.status(500).json({ error: e.message });
}
});
// 11. 更新终端会话操作状态(铸渊从对话中推送)
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, operation_stage: session.operation_stage, human_readable: session.human_readable });
});
// 12. 关闭终端会话
app.post('/api/terminal/close/:session_id', (req, res) => {
const session = terminalSessions[req.params.session_id];
if (session) delete terminalSessions[req.params.session_id];
res.json({ ok: true });
});
// 13. 获取所有活跃终端会话
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)) {
const data = fs.readFileSync(KEYS_FILE, 'utf-8');
keyStore = JSON.parse(data || '[]');
}
} catch(e) { keyStore = []; }
}
function saveKeys() {
fs.writeFileSync(KEYS_FILE, JSON.stringify(keyStore, null, 2));
}
loadKeys();
// 14. 投递密钥
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 });
});
// 15. 列出所有密钥(不包含值)
app.get('/api/keys', (req, res) => {
const list = keyStore.map(k => ({ id: k.id, label: k.label, created_at: k.created_at }));
res.json({ ok: true, keys: list });
});
// 16. 获取单个密钥值
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 });
});
// 17. 删除密钥
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 新增) ==========
// 18. GPU状态 - Agent推送
app.post('/api/gpu/status', (req, res) => {
if (!checkAgentAuth(req)) return res.status(401).json({ error: '未授权请检查API Key' });
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: '存储失败: ' + e.message });
}
});
// 19. GPU状态 - 仪表盘读取
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, age_seconds: Math.floor(age / 1000) });
} catch(e) {
res.json({ ok: true, gpus: [], timestamp: null, online: false });
}
});
// 20. 训练状态 - Agent推送
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: '存储失败' });
}
});
// 21. 训练状态 - 仪表盘读取
app.get('/api/training/status', (req, res) => {
try {
const data = JSON.parse(fs.readFileSync(DATA_DIR + '/training-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, status: 'offline', message: '铸渊Agent未连接' });
}
});
// 22. Agent操作日志 - Agent推送
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: '存储失败' });
}
});
// 23. Agent操作日志 - 仪表盘读取
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 limit = parseInt(req.query.limit) || 50;
const entries = lines.slice(-limit).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 });
}
});
// 24. Agent日记 - Agent推送
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: '存储失败' });
}
});
// 25. Agent日记 - 仪表盘读取
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 });
}
});
// 26. 综合状态 - 一次性拉取所有Agent数据
app.get('/api/agent/combined', (req, res) => {
const result = { gpu: null, training: null, logs: [], diary: [], online: false, last_seen: null };
try {
if (fs.existsSync(DATA_DIR + '/gpu-status.json')) {
result.gpu = JSON.parse(fs.readFileSync(DATA_DIR + '/gpu-status.json', 'utf-8'));
result.last_seen = result.gpu.timestamp;
result.online = result.gpu.timestamp && (Date.now() - new Date(result.gpu.timestamp).getTime() < 120000);
}
} catch(e) {}
try {
if (fs.existsSync(DATA_DIR + '/training-status.json')) {
result.training = JSON.parse(fs.readFileSync(DATA_DIR + '/training-status.json', 'utf-8'));
}
} catch(e) {}
try {
const logPath = DATA_DIR + '/agent-log.jsonl';
if (fs.existsSync(logPath)) {
const logLines = fs.readFileSync(logPath, 'utf-8').trim().split('\n').filter(Boolean);
result.logs = logLines.slice(-20).map(l => { try { return JSON.parse(l); } catch(e) { return { message: l }; } });
}
} catch(e) {}
try {
const diaryPath = DATA_DIR + '/agent-diary.jsonl';
if (fs.existsSync(diaryPath)) {
const diaryLines = fs.readFileSync(diaryPath, 'utf-8').trim().split('\n').filter(Boolean);
result.diary = diaryLines.map(l => { try { return JSON.parse(l); } catch(e) { return { type: 'info', title: l }; } }).reverse().slice(0, 10);
}
} catch(e) {}
res.json(result);
});
// ========== 算力池 API ==========
let cachedPoolStatus = {};
// 27. 接收gatekeeper上报的节点状态
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 } = req.body;
if (!node_code) return res.status(400).json({ error: '缺少 node_code' });
cachedPoolStatus[node_code] = {
name: node_name || node_code,
online: true,
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 });
});
// 28. 获取算力池汇总状态
app.get('/api/pool/status', (req, res) => {
const nodes = {};
let totalCpu = 0, usedCpu = 0, totalMem = 0, usedMem = 0, onlineNodes = 0;
let totalSpec = { cpu: 0, mem_gb: 0 };
for (const srv of SERVERS) {
const cached = cachedPoolStatus[srv.code];
if (cached) {
nodes[srv.code] = cached;
const spec = cached.spec || { cpu: 2, mem_gb: 4 };
totalCpu += spec.cpu;
usedCpu += cached.cpu_load;
totalMem += spec.mem_gb;
const memAvailGb = cached.mem_avail_mb / 1024;
usedMem += Math.max(0, spec.mem_gb - memAvailGb);
onlineNodes++;
totalSpec.cpu += spec.cpu;
totalSpec.mem_gb += spec.mem_gb;
} else {
nodes[srv.code] = {
name: srv.name,
online: false,
cpu_load: 0, mem_total_mb: 0, mem_avail_mb: 0,
disk_used_pct: 0, uptime_hours: 0, tasks: 0,
spec: { cpu: 2, mem_gb: 4 },
last_seen: null
};
totalSpec.cpu += 2;
totalSpec.mem_gb += 4;
}
}
const pool = {
updated_at: new Date().toISOString(),
cpu_total: totalSpec.cpu,
cpu_used: usedCpu,
cpu_available: Math.max(0, totalSpec.cpu - usedCpu),
mem_total_gb: totalSpec.mem_gb,
mem_used_gb: usedMem,
mem_available_gb: Math.max(0, totalSpec.mem_gb - usedMem),
total_nodes: SERVERS.length,
online_nodes: onlineNodes
};
res.json({
ok: true,
pool: { pool, nodes, total_spec: totalSpec, active_tasks: 0, updated_at: pool.updated_at }
});
});
const PORT = process.env.CONSOLE_PORT || 3920;
app.listen(PORT, '127.0.0.1', () => {
console.log('光湖主控台 API v3.1 — GPU+Agent训练台');
console.log(' 监听: 127.0.0.1:' + PORT);
console.log(' 服务器: ' + SERVERS.length + ' 台 | GPU训练台: 已启用');
console.log(' 数据目录: ' + DATA_DIR);
});