#!/usr/bin/env node /** * Router - 端口18888上的统一入口 * /ccswitch/* -> CC-Switch Web (port 3000) * /* -> OpenClaw Gateway (port 18889) * 支持 HTTP 和 WebSocket */ const http = require('http'); const net = require('net'); const fs = require('fs'); const path = require('path'); const LISTEN_PORT = 18888; const OPENCLAW_PORT = 18889; const CCSWITCH_PORT = 3000; const STATE_DIR = process.env.OPENCLAW_STATE_DIR || '/root/.openclaw'; // 工具: 检查端口是否开放 function checkPort(port, host = '127.0.0.1') { return new Promise((resolve) => { const s = net.createConnection(port, host, () => { s.destroy(); resolve(true); }); s.on('error', () => resolve(false)); s.setTimeout(2000, () => { s.destroy(); resolve(false); }); }); } // 工具: 安全读取文件 function safeRead(filePath, maxLen = 5000) { try { const content = fs.readFileSync(filePath, 'utf-8'); return content.slice(-maxLen); } catch { return '(file not found)'; } } // 工具: 读取 JSON 文件 function safeReadJSON(filePath) { try { return JSON.parse(fs.readFileSync(filePath, 'utf-8')); } catch { return null; } } // 创建 HTTP 服务器 const server = http.createServer(async (req, res) => { // === Debug/Status endpoints === if (req.url.startsWith('/debug')) { const logPath = path.join(STATE_DIR, 'logs', 'openclaw.log'); const statusPath = path.join(STATE_DIR, 'logs', 'startup_status.json'); const configPath = path.join(STATE_DIR, 'openclaw.json'); if (req.url === '/debug/raw/log') { res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' }); res.end(safeRead(logPath, 50000)); return; } if (req.url === '/debug/raw/status') { const status = safeReadJSON(statusPath) || { error: 'no status file' }; res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(status, null, 2)); return; } if (req.url === '/debug/raw/config') { const config = safeReadJSON(configPath) || {}; // 隐藏密码 if (config.gateway?.auth?.password) config.gateway.auth.password = '****'; res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(config, null, 2)); return; } if (req.url === '/debug/password') { res.writeHead(403, { 'Content-Type': 'text/plain' }); res.end('Endpoint disabled for security. Check HF Space Secrets for credentials.'); return; } // === Session diagnostic endpoint === if (req.url === '/debug/sessions') { const sessionsDir = path.join(STATE_DIR, 'agents', 'agent_1', 'sessions'); const files = []; try { const names = fs.readdirSync(sessionsDir); for (const name of names) { const fp = path.join(sessionsDir, name); const stat = fs.statSync(fp); files.push({ name, size: stat.size, mtime: stat.mtime.toISOString() }); } } catch (e) { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: `Cannot read sessions dir: ${e.message}`, dir: sessionsDir })); return; } res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ dir: sessionsDir, count: files.length, files }, null, 2)); return; } // === State directory info endpoint === if (req.url === '/debug/info') { const info = { stateDir: STATE_DIR, env: {} }; // Show env vars (mask secrets) const showKeys = ['OPENCLAW_STATE_DIR', 'HF_DATASET', 'CCSWITCH_USERNAME', 'WEIXIN_ENABLED', 'PORT']; for (const k of showKeys) { info.env[k] = process.env[k] || '(not set)'; } // Disk usage try { const items = fs.readdirSync(STATE_DIR); info.stateDirContents = items.map(name => { const fp = path.join(STATE_DIR, name); let type = 'file'; try { type = fs.statSync(fp).isDirectory() ? 'dir' : 'file'; } catch {} return { name, type }; }); } catch (e) { info.stateDirError = e.message; } res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(info, null, 2)); return; } // HTML 诊断页面 const ocOk = await checkPort(OPENCLAW_PORT); const csOk = await checkPort(CCSWITCH_PORT); const status = safeReadJSON(statusPath); const logTail = safeRead(logPath, 3000); // Count session files let sessionCount = 0; try { const sd = path.join(STATE_DIR, 'agents', 'agent_1', 'sessions'); if (fs.existsSync(sd)) { sessionCount = fs.readdirSync(sd).filter(f => f.endsWith('.jsonl')).length; } } catch {} let sessionHtml = sessionCount > 0 ? `${sessionCount} session files` : `0 session files (expected before OpenClaw starts)`; res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(` OpenClaw Debug

🔧 OpenClaw Diagnostics

Port Status

ServicePortStatus
OpenClaw Gateway${OPENCLAW_PORT}${ocOk?'✅ Running':'❌ Not responding'}
CC-Switch-Web${CCSWITCH_PORT}${csOk?'✅ Running':'❌ Not responding'}
Router${LISTEN_PORT}✅ Running

Sessions

${sessionHtml}

Startup Status

${JSON.stringify(status, null, 2)}

OpenClaw Log (last 3000 chars)

${logTail.replace(/

Full log (raw) | Status (JSON) | Config (JSON, passwords hidden) | Sessions on disk | State dir info

`); return; } // === Backup trigger endpoint (手动触发备份) === if (req.url.startsWith('/api/backup')) { const url = new URL(req.url, `http://${req.headers.host}`); const token = url.searchParams.get('token') || ''; const mode = url.searchParams.get('mode') === 'incremental' ? 'incremental' : 'full'; // 验证 token: 读取 gateway 密码文件 const pwPath = path.join(STATE_DIR, 'logs', 'gateway_password.txt'); let authorized = false; try { const expected = fs.readFileSync(pwPath, 'utf-8').trim(); authorized = (token === expected); } catch {} if (!authorized) { res.writeHead(401, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Unauthorized. Use: /api/backup?token=GATEWAY_PASSWORD[&mode=incremental]' })); return; } // 异步执行备份,输出流式返回 res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' }); res.write(`[Backup] Starting ${mode} backup...\n`); const { exec } = require('child_process'); const child = exec(`python3 ./backup-manager.py ${mode}`, { timeout: 300000 }); child.stdout.on('data', (data) => res.write(data)); child.stderr.on('data', (data) => res.write(data)); child.on('close', (code) => { res.write(`[Backup] ${mode} backup exited with code ${code}\n`); res.end(); }); return; } let targetPort, targetPath; if (req.url.startsWith('/ccswitch')) { targetPort = CCSWITCH_PORT; targetPath = req.url.replace(/^\/ccswitch/, '') || '/'; } else if (/^\/api\/(settings|system\/|providers\/|config\/)/.test(req.url)) { // CCSwitch web API endpoints - route to CC-Switch targetPort = CCSWITCH_PORT; targetPath = req.url; } else { targetPort = OPENCLAW_PORT; targetPath = req.url; } console.log(`[Router] HTTP ${req.method} ${req.url} -> port ${targetPort}`); // Collect request body const chunks = []; req.on('data', chunk => chunks.push(chunk)); req.on('end', () => { const body = Buffer.concat(chunks); // 过滤代理头,避免 OpenClaw 不信任连接 const cleanHeaders = { ...req.headers }; delete cleanHeaders['x-forwarded-for']; delete cleanHeaders['x-forwarded-proto']; delete cleanHeaders['x-forwarded-host']; delete cleanHeaders['x-real-ip']; delete cleanHeaders['forwarded']; cleanHeaders.host = `127.0.0.1:${targetPort}`; const options = { hostname: '127.0.0.1', port: targetPort, path: targetPath, method: req.method, headers: cleanHeaders }; const proxyReq = http.request(options, (proxyRes) => { res.writeHead(proxyRes.statusCode, proxyRes.headers); proxyRes.pipe(res); }); proxyReq.on('error', (err) => { console.error(`[Router] Error proxying to port ${targetPort}: ${err.message}`); if (!res.headersSent) { res.writeHead(502, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: `Service on port ${targetPort} unavailable: ${err.message}` })); } }); if (body.length > 0) proxyReq.write(body); proxyReq.end(); }); }); // 处理 WebSocket 升级 server.on('upgrade', (req, socket, head) => { let targetPort; if (req.url.startsWith('/ccswitch')) { targetPort = CCSWITCH_PORT; } else { targetPort = OPENCLAW_PORT; } console.log(`[Router] WebSocket upgrade: ${req.url} -> port ${targetPort}`); console.log(`[Router] WebSocket headers:`, JSON.stringify(req.headers)); // 创建到目标服务器的连接 const proxySocket = new net.Socket(); proxySocket.connect(targetPort, '127.0.0.1', () => { console.log(`[Router] Connected to port ${targetPort}`); // 构建升级请求 let upgradeReq = `GET ${req.url} HTTP/1.1\r\n`; upgradeReq += `Host: 127.0.0.1:${targetPort}\r\n`; upgradeReq += `Upgrade: ${req.headers.upgrade || 'websocket'}\r\n`; upgradeReq += `Connection: Upgrade\r\n`; // 复制其他重要的请求头(排除代理头,避免 OpenClaw 不信任连接) const skipHeaders = ['host', 'upgrade', 'connection', 'x-forwarded-for', 'x-forwarded-proto', 'x-forwarded-host', 'x-real-ip', 'forwarded']; for (const [key, value] of Object.entries(req.headers)) { if (!skipHeaders.includes(key.toLowerCase())) { upgradeReq += `${key}: ${value}\r\n`; } } upgradeReq += '\r\n'; console.log(`[Router] Sending upgrade request to port ${targetPort}`); proxySocket.write(upgradeReq); proxySocket.write(head); // 双向管道 socket.pipe(proxySocket); proxySocket.pipe(socket); console.log(`[Router] WebSocket piped: client <-> port ${targetPort}`); }); proxySocket.on('error', (err) => { console.error(`[Router] WebSocket proxy error to port ${targetPort}: ${err.message}`); socket.end(); }); proxySocket.on('close', () => { console.log(`[Router] WebSocket connection to port ${targetPort} closed`); }); socket.on('error', (err) => { console.error(`[Router] Client socket error: ${err.message}`); proxySocket.end(); }); socket.on('close', () => { console.log(`[Router] Client WebSocket connection closed`); proxySocket.end(); }); }); server.listen(LISTEN_PORT, '0.0.0.0', () => { console.log(`[Router] Listening on port ${LISTEN_PORT}`); console.log(`[Router] /ccswitch/* -> port ${CCSWITCH_PORT}`); console.log(`[Router] /* -> port ${OPENCLAW_PORT}`); console.log(`[Router] WebSocket support enabled`); });