xiaoxiaxia / router.js
Claude
fix: 移除 assets/ 到 CCSwitch 的路由,避免 OpenClaw 前端 JS/CSS 被拦截返回 401
534f20e
#!/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
? `<span class="ok">${sessionCount} session files</span>`
: `<span class="warn">0 session files (expected before OpenClaw starts)</span>`;
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(`<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>OpenClaw Debug</title>
<style>body{font-family:monospace;background:#1a1a2e;color:#eee;padding:20px}
h1{color:#e94560}.ok{color:#0f0}.fail{color:#f00}.warn{color:#ff0}
pre{background:#16213e;padding:10px;border-radius:5px;overflow:auto;max-height:400px}
table{border-collapse:collapse;width:100%}td,th{border:1px solid #333;padding:8px;text-align:left}
th{background:#0f3460}</style></head><body>
<h1>🔧 OpenClaw Diagnostics</h1>
<h2>Port Status</h2>
<table><tr><th>Service</th><th>Port</th><th>Status</th></tr>
<tr><td>OpenClaw Gateway</td><td>${OPENCLAW_PORT}</td><td class="${ocOk?'ok':'fail'}">${ocOk?'✅ Running':'❌ Not responding'}</td></tr>
<tr><td>CC-Switch-Web</td><td>${CCSWITCH_PORT}</td><td class="${csOk?'ok':'fail'}">${csOk?'✅ Running':'❌ Not responding'}</td></tr>
<tr><td>Router</td><td>${LISTEN_PORT}</td><td class="ok">✅ Running</td></tr>
</table>
<h2>Sessions</h2>
<p>${sessionHtml}</p>
<h2>Startup Status</h2>
<pre>${JSON.stringify(status, null, 2)}</pre>
<h2>OpenClaw Log (last 3000 chars)</h2>
<pre>${logTail.replace(/</g,'&lt;')}</pre>
<p><a href="/debug/raw/log">Full log (raw)</a> | <a href="/debug/raw/status">Status (JSON)</a> | <a href="/debug/raw/config">Config (JSON, passwords hidden)</a> | <a href="/debug/sessions">Sessions on disk</a> | <a href="/debug/info">State dir info</a></p>
</body></html>`);
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`);
});