import { Hono } from 'hono' import type { MiddlewareHandler } from 'hono/types' import type { UpgradeWebSocket } from 'hono/ws' import { dirname, resolve } from 'node:path' import { existsSync, appendFileSync } from 'node:fs' import { fileURLToPath } from 'node:url' import { createAcpApp } from './routes/acp' import { createSessionIngressApp } from './routes/v1/session-ingress' import { csrfCheck } from './auth/csrf' // Routes import v1Environments from './routes/v1/environments' import v1EnvironmentsWork from './routes/v1/environments.work' import v1Sessions from './routes/v1/sessions' import v2CodeSessions from './routes/v2/code-sessions' import v2Worker from './routes/v2/worker' import v2WorkerEventsStream from './routes/v2/worker-events-stream' import v2WorkerEvents from './routes/v2/worker-events' import webAuth from './routes/web/auth' import webAuthRoutes from './routes/web/auth-routes' import webSessions from './routes/web/sessions' import webControl from './routes/web/control' import webEnvironments from './routes/web/environments' import webShares from './routes/web/shares' import webTeams from './routes/web/teams' type ServeStaticFn = (opts: Record) => MiddlewareHandler /** * Register all RCS routes on the given Hono app. * Accepts `serveStatic` and `upgradeWebSocket` factories so both Bun and * Node.js can share the same route table with their own adapters. * * This module has NO side effects — it only exports the registerRoutes function. */ export function registerRoutes( app: Hono, ss: ServeStaticFn, uws: UpgradeWebSocket, ): void { const __dirname = dirname(fileURLToPath(import.meta.url)) // Dev: packages/.../src → ../web/dist. Prod: dist/ → ./rcs-web (post-build copies here). const distDir = resolve(__dirname, '../web/dist') const rcsWebDir = resolve(__dirname, './rcs-web') const webDir = existsSync(resolve(distDir, 'index.html')) ? distDir : existsSync(resolve(rcsWebDir, 'index.html')) ? rcsWebDir : resolve(__dirname, '../web') const stripCodePrefix = (p: string) => p.replace(/^\/code/, '') // Serve all static files under /code/* from web/ directory app.use('/code/*', ss({ root: webDir, rewriteRequestPath: stripCodePrefix })) // /code, /code/, and /code/:sessionId — SPA fallback app.get('/code', ss({ root: webDir, path: 'index.html' })) app.get('/code/', ss({ root: webDir, path: 'index.html' })) app.get('/code/:sessionId', ss({ root: webDir, path: 'index.html' })) // /code/s/:shareToken — share landing page (two-segment path, not matched by /code/:sessionId) app.get('/code/s/:shareToken', ss({ root: webDir, path: 'index.html' })) // /code/claim/:claimToken — environment claim landing page app.get('/code/claim/:claimToken', ss({ root: webDir, path: 'index.html' })) // v1 Environment routes app.route('/v1/environments', v1Environments) app.route('/v1/environments', v1EnvironmentsWork) // v1 Session routes app.route('/v1/sessions', v1Sessions) // Session Ingress (WebSocket) — created via factory with the appropriate // upgradeWebSocket adapter. Mounted at both /v1 and /v2. const sessionIngressApp = createSessionIngressApp(uws) app.route('/v1/session_ingress', sessionIngressApp) app.route('/v2/session_ingress', sessionIngressApp) // v2 Code Sessions routes app.route('/v1/code/sessions', v2CodeSessions) app.route('/v1/code/sessions', v2Worker) app.route('/v1/code/sessions', v2WorkerEventsStream) app.route('/v1/code/sessions', v2WorkerEvents) // Web control panel routes — CSRF protection applied before route handlers app.use('/web/*', csrfCheck) app.route('/web', webAuth) app.route('/web/auth', webAuthRoutes) app.route('/web', webShares) app.route('/web', webTeams) app.route('/web', webSessions) app.route('/web', webControl) app.route('/web', webEnvironments) // ACP protocol routes — created via factory with upgradeWebSocket console.log('[RCS] ACP support enabled') app.route('/acp', createAcpApp(uws)) // Debug log endpoint — Web UI 前端把 console.log POST 到这里,追加写入 /tmp/rcs-web-debug.log // 用于 REPL 模式下排查前端问题(浏览器 Console 不可见) const DEBUG_LOG = '/tmp/rcs-web-debug.log' app.post('/code/__debug__', async c => { try { const body = await c.req.json() const ts = new Date().toISOString() const line = `[${ts}] ${typeof body === 'string' ? body : JSON.stringify(body)}\n` appendFileSync(DEBUG_LOG, line) return c.json({ ok: true }) } catch (err) { return c.json({ ok: false, error: String(err) }, 500) } }) // SPA fallback — serve index.html for frontend routes handled by React Router. // Must be registered AFTER all API routes to avoid intercepting /web/*, /v1/*, /acp/* etc. const spaPaths = [ '/login', '/register', '/setup', '/join', '/settings', '/teams', '/admin', '/share', ] for (const p of spaPaths) { app.get(p, ss({ root: webDir, path: 'index.html' })) app.get(`${p}/*`, ss({ root: webDir, path: 'index.html' })) } // Root redirect → /code/ (main dashboard entry point) app.get('/', c => c.redirect('/code/')) }