From c903968ab0b05c36c35b6c937745b319ca2c19f1 Mon Sep 17 00:00:00 2001 From: bingshuo <565183519@qq.com> Date: Thu, 21 May 2026 15:50:04 +0800 Subject: [PATCH] deploy: console-server v2.0 with MCP terminal --- _deploy/console-server/server.js | 339 +++++++++++++++++++++++++++++++ 1 file changed, 339 insertions(+) create mode 100644 _deploy/console-server/server.js diff --git a/_deploy/console-server/server.js b/_deploy/console-server/server.js new file mode 100644 index 0000000..c84979e --- /dev/null +++ b/_deploy/console-server/server.js @@ -0,0 +1,339 @@ +// # 光湖服务器主控台 API +// # Guanghu Server Console v2.0 — with MCP Terminal +// # 部署: 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'))); + +// 六台服务器配置(从 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() }; + } +} + +// ========== 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 终端会话 (v2.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() + }; + + 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; + } else if (entry.status === 'rejected') { + session.status = 'rejected'; + } + } + } + + res.json({ + ok: true, + session_id: req.params.session_id, + status: session.status, + server: session.server, + server_name: session.server_name, + 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: '会话尚未就绪,等待冰朔授权中' }); + + 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; + 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/close/:session_id', (req, res) => { + const session = terminalSessions[req.params.session_id]; + if (session) delete terminalSessions[req.params.session_id]; + res.json({ ok: true }); +}); + +// 12. 获取所有活跃终端会话 +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, + created_at: s.created_at, + age_seconds: Math.floor((Date.now() - new Date(s.created_at).getTime()) / 1000) + })); + res.json({ ok: true, sessions }); +}); + +const PORT = process.env.CONSOLE_PORT || 3920; +app.listen(PORT, '127.0.0.1', () => { + console.log(`光湖服务器主控台 API v2.0`); + console.log(` 监听: 127.0.0.1:${PORT}`); + console.log(` 服务器: ${SERVERS.length} 台`); + console.log(` MCP终端: 已启用`); +}); \ No newline at end of file