#!/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(`
| Service | Port | Status |
|---|---|---|
| OpenClaw Gateway | ${OPENCLAW_PORT} | ${ocOk?'✅ Running':'❌ Not responding'} |
| CC-Switch-Web | ${CCSWITCH_PORT} | ${csOk?'✅ Running':'❌ Not responding'} |
| Router | ${LISTEN_PORT} | ✅ Running |
${sessionHtml}
${JSON.stringify(status, null, 2)}
${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`);
});