diff --git a/_deploy/console-server/server.js b/_deploy/console-server/server.js index 0b1b778..4ca414c 100644 --- a/_deploy/console-server/server.js +++ b/_deploy/console-server/server.js @@ -1,5 +1,5 @@ // # 光湖服务器主控台 API -// # Guanghu Server Console v3.1 — with GPU & Agent API +// # Guanghu Server Console v3.2 — with Pool Registry API // # 部署: guanghulab.com/console/ // # 版权: 国作登字-2026-A-00037559 · TCS-0002∞ @@ -82,9 +82,7 @@ function checkAgentAuth(req) { 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; - } + } catch(e) { return false; } } // ========== API 路由 ========== @@ -101,18 +99,8 @@ app.post('/api/knock', (req, res) => { 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); + 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: '敲门已发送,等待冰朔确认' }); }); @@ -127,554 +115,319 @@ app.post('/api/authorize', (req, res) => { 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 - }; - + 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. 拒绝授权 +// 5-7. 拒绝/验证/执行 (保持原逻辑,已压缩) app.post('/api/reject', (req, res) => { - const { request_id } = req.body; - const entry = authQueue.find(r => r.id === request_id && r.status === 'pending'); + const entry = authQueue.find(r => r.id === req.body.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: '密钥已过期' }); - } + 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: '密钥无效或已过期' }); - } - + 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 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 }); } - }); - }); + 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(); + req.write(data); req.end(); }); res.json({ ok: true, result }); - } catch (e) { - res.status(500).json({ error: e.message }); - } + } 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' - }); + 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' }); }); -// 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 = '冰朔未确认此操作,铸渊等待新的指令。'; - } + 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 - }); + 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 + ' 上执行操作...'; - + 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 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 }); } - }); - }); + 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(); + 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 }); - } + 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 }); } }); -// 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.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) - })); + 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)); -} +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(); -// 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 }); + 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', (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 }); }); -// 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) ========== -// ========== 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 }); - } + 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: '存储失败' }); } }); -// 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 }); - } + 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 }); } }); -// 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: '存储失败' }); - } + 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未连接' }); - } + 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' }); } }); -// 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: '存储失败' }); - } +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 }); }); -// 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 }); +// 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: '未找到注册请求' }); }); -// 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: '存储失败' }); - } +// 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' }); } }); -// 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 }); - } +// 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 }); }); -// 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); +// 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 = {}; -// 27. 接收gatekeeper上报的节点状态 +// 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 } = req.body; + 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' }); - 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() - }; + 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 }); }); -// 28. 获取算力池汇总状态 +// 33. 获取算力池汇总状态(v3.2: 动态注册节点) 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 }; - + let totalSpec = { cpu: 0, mem_gb: 0 }, iceNodes = 0, teamNodes = 0, onlineNodes = 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; - } + 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++; } - - 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 } - }); + 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.1 — GPU+Agent训练台'); + console.log('光湖主控台 API v3.2 — 节点池注册'); console.log(' 监听: 127.0.0.1:' + PORT); - console.log(' 服务器: ' + SERVERS.length + ' 台 | GPU训练台: 已启用'); + console.log(' 服务器: ' + SERVERS.length + ' 台 | 节点池: 已启用'); console.log(' 数据目录: ' + DATA_DIR); }); \ No newline at end of file