c3db79c00d
- core: 工具描述从文件系统加载改为编译时内联生成 - core: 添加 WASM 加载器支持嵌入式 WASM 数据 - core: bash-parser 使用动态导入 web-tree-sitter - server: 添加静态文件托管支持 (--static 参数) - server: 新增 standalone 入口点 (嵌入 Web UI + WASM) - scripts: 添加 build-standalone.ts 构建脚本 - 更新 .gitignore 忽略生成文件
321 lines
8.5 KiB
TypeScript
321 lines
8.5 KiB
TypeScript
#!/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);
|
|
});
|