#!/usr/bin/env bun /** * AI Assistant Standalone Server * * 包含嵌入式 Web UI 的独立服务器 * 由 scripts/build-standalone.ts 构建 */ import { Hono } from 'hono'; import { cors } from 'hono/cors'; import { logger } from 'hono/logger'; import { createBunWebSocket } from 'hono/bun'; import { sessionsRouter, toolsRouter, configRouter, filesRouter, commandsRouter, mcpRouter, hooksRouter, agentsRouter, checkpointsRouter, providersRouter, servicesRouter, contextRouter, lspRouter, systemCommandsRouter, statsRouter, } from '../routes/index.js'; import { handleWebSocket, handleWebSocketMessage, handleWebSocketClose, getConnectionStats, } from '../ws.js'; import { handleSSE, getSSEStats } from '../sse.js'; import { getSessionManager } from '../session/manager.js'; import { initCore, isCoreAvailable } from '../agent/index.js'; import { authMiddleware, initAuth, getAuthConfig, generateToken, addToken, } from '../auth/index.js'; // 嵌入的 Web 资源(构建时生成) import { EMBEDDED_ASSETS, decodeAsset } from '../embedded-assets.generated.js'; // 嵌入的 WASM 文件(构建时生成) import { EMBEDDED_WASM } from '../embedded-wasm.generated.js'; // 创建 Hono 应用 const app = new Hono(); // WebSocket 升级 const { upgradeWebSocket, websocket } = createBunWebSocket(); // 中间件 app.use('*', logger()); app.use( '*', cors({ origin: '*', allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], allowHeaders: ['Content-Type', 'Authorization'], }) ); // 认证中间件 app.use('*', authMiddleware); // 健康检查 app.get('/health', (c) => { const sessionManager = getSessionManager(); const wsStats = getConnectionStats(); const sseStats = getSSEStats(); const authConfig = getAuthConfig(); return c.json({ status: 'ok', timestamp: new Date().toISOString(), agent: { coreAvailable: isCoreAvailable(), }, auth: { enabled: authConfig.enabled, tokenCount: authConfig.tokens.length, }, stats: { sessions: sessionManager.count(), websocket: wsStats, sse: sseStats, }, }); }); // API 路由 const api = new Hono(); api.route('/sessions', sessionsRouter); api.route('/tools', toolsRouter); api.route('/config', configRouter); api.route('/files', filesRouter); api.route('/commands', commandsRouter); api.route('/mcp', mcpRouter); api.route('/hooks', hooksRouter); api.route('/agents', agentsRouter); api.route('/checkpoints', checkpointsRouter); api.route('/providers', providersRouter); api.route('/services', servicesRouter); api.route('/lsp', lspRouter); api.route('/system-commands', systemCommandsRouter); api.route('/stats', statsRouter); api.route('/', contextRouter); // SSE 事件流 api.get('/sessions/:id/events', handleSSE); // WebSocket 端点 api.get( '/ws/:sessionId', upgradeWebSocket((c) => { const sessionId = c.req.param('sessionId'); return { onOpen(_event, ws) { handleWebSocket(ws, sessionId); }, onMessage(event, ws) { handleWebSocketMessage(ws, sessionId, event.data); }, onClose(_event, ws) { handleWebSocketClose(ws, sessionId); }, onError(event, ws) { console.error('[WS] Error:', event); handleWebSocketClose(ws, sessionId); }, }; }) ); app.route('/api', api); // 嵌入式 Web UI 静态文件服务 app.get('*', (c) => { let reqPath = c.req.path; // 尝试查找精确匹配 let asset = EMBEDDED_ASSETS.get(reqPath); // 如果没找到且不是文件路径,尝试 index.html (SPA 路由) if (!asset && !reqPath.includes('.')) { asset = EMBEDDED_ASSETS.get('/index.html'); } if (asset) { const body = decodeAsset(asset); return new Response(body, { headers: { 'Content-Type': asset.mimeType, 'Cache-Control': reqPath.includes('/assets/') ? 'public, max-age=31536000' : 'no-cache', }, }); } return c.json({ success: false, error: 'Not found' }, 404); }); // 解析命令行参数 function parseArgs(): { port: number; host: string; auth?: boolean; token?: string } { const args = process.argv.slice(2); let port = 3000; let host = '127.0.0.1'; let auth: boolean | undefined; let token: string | undefined; for (let i = 0; i < args.length; i++) { if (args[i] === '--port' || args[i] === '-p') { port = parseInt(args[i + 1], 10) || 3000; i++; } else if (args[i] === '--host' || args[i] === '-H') { host = args[i + 1] || '127.0.0.1'; i++; } else if (args[i] === '--auth') { auth = true; } else if (args[i] === '--no-auth') { auth = false; } else if (args[i] === '--token' || args[i] === '-t') { token = args[i + 1]; i++; } else if (args[i] === '--help' || args[i] === '-h') { console.log(` AI Assistant (Standalone) A terminal-based AI coding assistant with embedded Web UI. Usage: ai-assistant [options] Options: -p, --port Port to listen on (default: 3000) -H, --host Host to bind to (default: 127.0.0.1) --auth Enable authentication --no-auth Disable authentication -t, --token Set authentication token -h, --help Show this help message Examples: # Local development ai-assistant # Listen on all interfaces with auth ai-assistant --host 0.0.0.0 --auth # Custom port and token ai-assistant --port 8080 --token mysecrettoken After starting, open http://localhost: in your browser. `); process.exit(0); } } return { port, host, auth, token }; } // 主函数 async function main() { const { port, host, auth, token } = parseArgs(); // 初始化嵌入的 WASM(在 Core 初始化之前) if (EMBEDDED_WASM.size > 0) { try { // 动态导入 core 模块来设置 WASM const core = await import('@ai-assistant/core'); if (typeof core.setEmbeddedWasm === 'function') { core.setEmbeddedWasm(EMBEDDED_WASM); console.log(`[Server] Embedded WASM initialized (${EMBEDDED_WASM.size} files)`); } } catch (error) { console.warn('[Server] Failed to initialize embedded WASM:', error); } } // 初始化 SessionManager const sessionManager = getSessionManager(); await sessionManager.init(); // 初始化 Core const coreLoaded = await initCore(); if (coreLoaded) { console.log('[Server] Core module initialized'); } else { console.warn('[Server] Core module not available, running in limited mode'); } // 初始化认证 const isRemote = host !== '127.0.0.1' && host !== 'localhost'; const authEnabled = auth !== undefined ? auth : isRemote; initAuth({ enabled: authEnabled, tokens: [], skipPaths: ['/health', '/api/health'], }); if (authEnabled) { const serverToken = token || generateToken(); addToken(serverToken); console.log(`[Auth] Authentication enabled`); console.log(`[Auth] Token: ${serverToken}`); } const coreStatus = isCoreAvailable() ? '✅ Core loaded' : '⚠️ Core not available'; const authConfig = getAuthConfig(); const authStatus = authConfig.enabled ? '🔐 Enabled' : '🔓 Disabled'; console.log(` ╔════════════════════════════════════════════╗ ║ AI Assistant (Standalone) ║ ╠════════════════════════════════════════════╣ ║ Web UI: http://${host}:${port} ║ REST API: http://${host}:${port}/api ║ WebSocket: ws://${host}:${port}/api/ws/:sessionId ║ Health: http://${host}:${port}/health ║ Agent: ${coreStatus} ║ Auth: ${authStatus} ║ Web UI: 📦 Embedded ╚════════════════════════════════════════════╝ `); // 启动服务器 const server = Bun.serve({ port, hostname: host, fetch: app.fetch, websocket, }); console.log(`Server running at http://${host}:${port}`); console.log(`Open http://${host === '0.0.0.0' ? 'localhost' : host}:${port} in your browser\n`); // 优雅关闭 process.on('SIGINT', () => { console.log('\nShutting down server...'); server.stop(); process.exit(0); }); process.on('SIGTERM', () => { console.log('\nShutting down server...'); server.stop(); process.exit(0); }); } main().catch((error) => { console.error('Failed to start server:', error); process.exit(1); });