feat: 支持 Bun standalone 单文件打包

- core: 工具描述从文件系统加载改为编译时内联生成
- core: 添加 WASM 加载器支持嵌入式 WASM 数据
- core: bash-parser 使用动态导入 web-tree-sitter
- server: 添加静态文件托管支持 (--static 参数)
- server: 新增 standalone 入口点 (嵌入 Web UI + WASM)
- scripts: 添加 build-standalone.ts 构建脚本
- 更新 .gitignore 忽略生成文件
This commit is contained in:
2025-12-30 13:57:29 +08:00
parent 5f38753f6d
commit c3db79c00d
15 changed files with 949 additions and 102 deletions
+320
View File
@@ -0,0 +1,320 @@
#!/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> Port to listen on (default: 3000)
-H, --host <host> Host to bind to (default: 127.0.0.1)
--auth Enable authentication
--no-auth Disable authentication
-t, --token <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:<port> 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);
});