Spaces:
Running
Running
| /** | |
| * 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,'<')}</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`); | |
| }); | |