feat: 完善 Server 层并添加 CLI 和 Web 前端

Server 层增强:
- 添加 Agent 适配层,支持动态加载 core 模块
- 实现 Token 认证机制,支持本地/远程模式
- WebSocket 集成 Agent 实时对话

CLI 模块 (packages/cli):
- serve 命令启动 HTTP Server
- attach 命令连接远程 Server
- API Client 封装

Web 前端 (packages/web):
- React 18 + Vite + Tailwind CSS
- 会话管理侧边栏
- WebSocket 实时聊天界面
- 流式消息显示
This commit is contained in:
2025-12-12 11:22:25 +08:00
parent 5e32375f0e
commit 168996a475
35 changed files with 4028 additions and 52 deletions
+249
View File
@@ -0,0 +1,249 @@
/**
* Agent Adapter
*
* 将 core 模块的 Agent 适配到 Server 环境
* 处理流式输出、事件推送等
*
* 使用接口定义避免直接依赖 @ai-assistant/core 类型
*/
import type { SessionStatus } from '../types.js';
import { getSessionManager } from '../session/manager.js';
import { broadcastToSession } from '../ws.js';
import { emitStatusEvent, emitLogEvent } from '../sse.js';
// ============================================================================
// Core 模块接口定义(避免直接依赖 @ai-assistant/core 类型)
// ============================================================================
/**
* Agent 实例接口
*/
interface AgentInstance {
setRegistry(registry: unknown): void;
chat(message: string, onStream?: (chunk: string) => void): Promise<string>;
getToolCount(): { core: number; discovered: number; total: number };
getContextUsageFormatted(): string;
}
/**
* Agent 构造函数接口
*/
interface AgentConstructor {
new (config: unknown): AgentInstance;
}
/**
* Tool Registry 接口
*/
interface ToolRegistry {
getCoreTools(): unknown[];
getAllTools(): unknown[];
}
/**
* Core 模块接口
*/
interface CoreModule {
Agent: AgentConstructor;
toolRegistry: ToolRegistry;
loadConfig: () => unknown;
}
// ============================================================================
// 模块状态
// ============================================================================
// Core 模块引用
let coreModule: CoreModule | null = null;
// Agent 实例缓存(每个 session 一个)
const agentCache: Map<string, AgentInstance> = new Map();
// ============================================================================
// 公共 API
// ============================================================================
/**
* 初始化 core 模块
*/
export async function initCore(): Promise<boolean> {
try {
// 使用变量避免 TypeScript 静态分析 import 路径
const corePath = '@ai-assistant/core';
// eslint-disable-next-line @typescript-eslint/no-require-imports
const core = (await import(/* webpackIgnore: true */ corePath)) as unknown as CoreModule;
// 验证模块结构
if (!core.Agent || !core.toolRegistry || !core.loadConfig) {
console.warn('[Agent] Core module missing required exports');
return false;
}
coreModule = core;
console.log('[Agent] Core module loaded');
return true;
} catch (error) {
console.warn('[Agent] Core module not available:', error);
return false;
}
}
/**
* 检查 core 模块是否可用
*/
export function isCoreAvailable(): boolean {
return coreModule !== null;
}
/**
* 获取或创建 Agent 实例
*/
export function getOrCreateAgent(sessionId: string): AgentInstance | null {
if (!coreModule) {
return null;
}
// 检查缓存
if (agentCache.has(sessionId)) {
return agentCache.get(sessionId)!;
}
// 创建新 Agent
const config = coreModule.loadConfig();
const agent = new coreModule.Agent(config);
agent.setRegistry(coreModule.toolRegistry);
agentCache.set(sessionId, agent);
return agent;
}
/**
* 销毁 Agent 实例
*/
export function destroyAgent(sessionId: string): void {
agentCache.delete(sessionId);
}
/**
* 处理用户消息并流式返回响应
*/
export async function processMessage(sessionId: string, content: string): Promise<void> {
const sessionManager = getSessionManager();
// 更新状态
sessionManager.updateStatus(sessionId, 'busy' as SessionStatus);
emitStatusEvent(sessionId, 'processing', { message: '正在处理...' });
// 获取 Agent
const agent = getOrCreateAgent(sessionId);
if (!agent) {
// Core 模块不可用,返回占位响应
broadcastToSession(sessionId, {
type: 'chunk',
sessionId,
payload: {
content: 'Agent core module not available. Please build @ai-assistant/core first.',
},
});
const assistantMessage = sessionManager.addMessage(sessionId, {
role: 'assistant',
content: 'Agent core module not available. Please build @ai-assistant/core first.',
});
broadcastToSession(sessionId, {
type: 'done',
sessionId,
payload: assistantMessage,
});
sessionManager.updateStatus(sessionId, 'idle' as SessionStatus);
emitStatusEvent(sessionId, 'idle');
return;
}
try {
// 调用 Agent 的 chat 方法,使用流式回调
const response = await agent.chat(content, (chunk: string) => {
// 推送流式内容
broadcastToSession(sessionId, {
type: 'chunk',
sessionId,
payload: { content: chunk },
});
// 检测工具调用
if (chunk.includes('[调用工具:')) {
const match = chunk.match(/\[调用工具: (.+?)\]/);
if (match) {
emitLogEvent(sessionId, 'info', `调用工具: ${match[1]}`);
}
}
});
// 保存助手消息
const assistantMessage = sessionManager.addMessage(sessionId, {
role: 'assistant',
content: response,
});
// 发送完成消息
broadcastToSession(sessionId, {
type: 'done',
sessionId,
payload: assistantMessage,
});
emitStatusEvent(sessionId, 'idle');
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
// 发送错误
broadcastToSession(sessionId, {
type: 'error',
sessionId,
payload: { message: errorMessage },
});
emitLogEvent(sessionId, 'error', errorMessage);
} finally {
sessionManager.updateStatus(sessionId, 'idle' as SessionStatus);
}
}
/**
* 取消正在进行的处理
*/
export function cancelProcessing(sessionId: string): void {
// TODO: 实现取消逻辑
// 目前 AI SDK 的 streamText 不支持取消
const sessionManager = getSessionManager();
sessionManager.updateStatus(sessionId, 'idle' as SessionStatus);
emitStatusEvent(sessionId, 'cancelled');
}
/**
* 获取 Agent 统计信息
*/
export function getAgentStats(sessionId: string): {
available: boolean;
toolCount?: { core: number; discovered: number; total: number };
contextUsage?: string;
} {
if (!coreModule) {
return { available: false };
}
const agent = agentCache.get(sessionId);
if (!agent) {
return { available: true };
}
return {
available: true,
toolCount: agent.getToolCount(),
contextUsage: agent.getContextUsageFormatted(),
};
}
+15
View File
@@ -0,0 +1,15 @@
/**
* Agent Module
*
* 导出 Agent 适配器
*/
export {
initCore,
isCoreAvailable,
getOrCreateAgent,
destroyAgent,
processMessage,
cancelProcessing,
getAgentStats,
} from './adapter.js';
+25
View File
@@ -0,0 +1,25 @@
/**
* Auth Module
*
* 导出认证相关功能
*/
export {
// 类型
type AuthConfig,
type AuthContext,
// Token 操作
generateToken,
maskToken,
validateToken,
extractToken,
// 配置
initAuth,
getAuthConfig,
addToken,
removeToken,
setAuthEnabled,
// 中间件
authMiddleware,
getAuthContext,
} from './token.js';
+263
View File
@@ -0,0 +1,263 @@
/**
* Token Authentication
*
* 简单的 Token 认证机制
* - 本地模式 (127.0.0.1): 无需认证
* - 远程模式: 需要 Bearer Token
*/
import { createMiddleware } from 'hono/factory';
import type { Context, Next } from 'hono';
// ============================================================================
// 类型定义
// ============================================================================
export interface AuthConfig {
/** 是否启用认证 (远程模式下自动启用) */
enabled: boolean;
/** 有效的 token 列表 */
tokens: string[];
/** 跳过认证的路径 */
skipPaths: string[];
}
export interface AuthContext {
/** 是否已认证 */
authenticated: boolean;
/** 使用的 token (脱敏) */
tokenHint?: string;
}
// ============================================================================
// 默认配置
// ============================================================================
const defaultConfig: AuthConfig = {
enabled: false,
tokens: [],
skipPaths: ['/health', '/api/health'],
};
// 当前配置
let authConfig: AuthConfig = { ...defaultConfig };
// ============================================================================
// Token 生成
// ============================================================================
/**
* 生成随机 token
*/
export function generateToken(length: number = 32): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let token = '';
const randomValues = new Uint8Array(length);
crypto.getRandomValues(randomValues);
for (let i = 0; i < length; i++) {
token += chars[randomValues[i] % chars.length];
}
return token;
}
/**
* 脱敏 token (只显示前4位和后4位)
*/
export function maskToken(token: string): string {
if (token.length <= 8) {
return '****';
}
return `${token.slice(0, 4)}...${token.slice(-4)}`;
}
// ============================================================================
// 配置管理
// ============================================================================
/**
* 初始化认证配置
*/
export function initAuth(config: Partial<AuthConfig> = {}): AuthConfig {
authConfig = {
...defaultConfig,
...config,
};
return authConfig;
}
/**
* 获取认证配置
*/
export function getAuthConfig(): AuthConfig {
return { ...authConfig };
}
/**
* 添加 token
*/
export function addToken(token: string): void {
if (!authConfig.tokens.includes(token)) {
authConfig.tokens.push(token);
}
}
/**
* 移除 token
*/
export function removeToken(token: string): void {
const index = authConfig.tokens.indexOf(token);
if (index !== -1) {
authConfig.tokens.splice(index, 1);
}
}
/**
* 启用/禁用认证
*/
export function setAuthEnabled(enabled: boolean): void {
authConfig.enabled = enabled;
}
/**
* 检查是否为本地请求
*/
function isLocalRequest(c: Context): boolean {
// 获取客户端 IP
const forwarded = c.req.header('x-forwarded-for');
const realIp = c.req.header('x-real-ip');
// 如果有代理头,检查原始 IP
if (forwarded) {
const clientIp = forwarded.split(',')[0].trim();
return isLocalIP(clientIp);
}
if (realIp) {
return isLocalIP(realIp);
}
// Bun/Hono 环境下获取连接 IP
// 这需要 server 层传递,目前假设本地
return true;
}
/**
* 检查是否为本地 IP
*/
function isLocalIP(ip: string): boolean {
return (
ip === '127.0.0.1' ||
ip === '::1' ||
ip === 'localhost' ||
ip.startsWith('192.168.') ||
ip.startsWith('10.') ||
ip.startsWith('172.16.') ||
ip.startsWith('172.17.') ||
ip.startsWith('172.18.') ||
ip.startsWith('172.19.') ||
ip.startsWith('172.2') ||
ip.startsWith('172.30.') ||
ip.startsWith('172.31.')
);
}
// ============================================================================
// 认证验证
// ============================================================================
/**
* 验证 token
*/
export function validateToken(token: string): boolean {
return authConfig.tokens.includes(token);
}
/**
* 从请求中提取 token
*/
export function extractToken(c: Context): string | null {
// 1. 从 Authorization header 提取
const authHeader = c.req.header('Authorization');
if (authHeader?.startsWith('Bearer ')) {
return authHeader.slice(7);
}
// 2. 从 query parameter 提取 (用于 WebSocket)
const queryToken = c.req.query('token');
if (queryToken) {
return queryToken;
}
return null;
}
// ============================================================================
// Hono 中间件
// ============================================================================
/**
* 认证中间件
*/
export const authMiddleware = createMiddleware(async (c: Context, next: Next) => {
// 检查是否跳过认证
const path = c.req.path;
if (authConfig.skipPaths.some((p) => path.startsWith(p))) {
await next();
return;
}
// 如果认证未启用,跳过
if (!authConfig.enabled) {
c.set('auth', { authenticated: true } as AuthContext);
await next();
return;
}
// 本地请求跳过认证
if (isLocalRequest(c)) {
c.set('auth', { authenticated: true } as AuthContext);
await next();
return;
}
// 提取 token
const token = extractToken(c);
if (!token) {
return c.json(
{
success: false,
error: 'Authentication required',
message: 'Please provide a valid token in the Authorization header',
},
401
);
}
// 验证 token
if (!validateToken(token)) {
return c.json(
{
success: false,
error: 'Invalid token',
message: 'The provided token is not valid',
},
401
);
}
// 设置认证上下文
c.set('auth', {
authenticated: true,
tokenHint: maskToken(token),
} as AuthContext);
await next();
});
/**
* 获取当前请求的认证上下文
*/
export function getAuthContext(c: Context): AuthContext {
return c.get('auth') || { authenticated: false };
}
+43 -7
View File
@@ -8,35 +8,71 @@
* bun run packages/server/src/bin/server.ts
* bun run packages/server/src/bin/server.ts --port 8080
* bun run packages/server/src/bin/server.ts --host 0.0.0.0 --port 3000
* bun run packages/server/src/bin/server.ts --auth --token mytoken
*/
import { app, websocket, startServer } from '../index.js';
// 解析命令行参数
function parseArgs(): { port: number; host: string } {
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') {
} 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 Server
Usage:
bun run server.ts [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 (no auth)
bun run server.ts
# Remote server with auth
bun run server.ts --host 0.0.0.0 --auth
# Custom token
bun run server.ts --host 0.0.0.0 --token mysecrettoken
`);
process.exit(0);
}
}
return { port, host };
return { port, host, auth, token };
}
// 主函数
async function main() {
const { port, host } = parseArgs();
const { port, host, auth, token } = parseArgs();
// 打印启动信息
startServer({ port, host });
// 初始化并打印启动信息
await startServer({ port, host, auth, token });
// 启动 Bun 服务器
const server = Bun.serve({
@@ -46,7 +82,7 @@ async function main() {
websocket,
});
console.log(`Server started at http://${host}:${port}`);
console.log(`Server running at http://${host}:${port}`);
// 优雅关闭
process.on('SIGINT', () => {
+85 -2
View File
@@ -18,6 +18,16 @@ import {
} from './ws.js';
import { handleSSE, getSSEStats } from './sse.js';
import { getSessionManager } from './session/manager.js';
import { initCore, isCoreAvailable, getAgentStats } from './agent/index.js';
import {
authMiddleware,
initAuth,
getAuthConfig,
generateToken,
addToken,
setAuthEnabled,
maskToken,
} from './auth/index.js';
// 创建 Hono 应用
const app = new Hono();
@@ -36,15 +46,26 @@ app.use(
})
);
// 健康检查
// 认证中间件 (在 CORS 之后)
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,
@@ -119,6 +140,10 @@ app.onError((err, c) => {
export interface ServerOptions {
port?: number;
host?: string;
/** 是否启用认证 (远程模式自动启用) */
auth?: boolean;
/** 预设的 token */
token?: string;
}
/**
@@ -135,12 +160,51 @@ export function createServer(options: ServerOptions = {}) {
};
}
/**
* 初始化服务器(加载 core 模块等)
*/
export async function initServer(options: ServerOptions = {}): Promise<void> {
// 尝试加载 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 { host = '127.0.0.1', auth, token } = options;
const isRemote = host !== '127.0.0.1' && host !== 'localhost';
const authEnabled = auth !== undefined ? auth : isRemote;
initAuth({
enabled: authEnabled,
tokens: [],
skipPaths: ['/health', '/api/health'],
});
// 如果启用认证,生成或使用提供的 token
if (authEnabled) {
const serverToken = token || generateToken();
addToken(serverToken);
console.log(`[Auth] Authentication enabled`);
console.log(`[Auth] Token: ${serverToken}`);
}
}
/**
* 启动服务器 (Bun 环境)
*/
export function startServer(options: ServerOptions = {}): void {
export async function startServer(options: ServerOptions = {}): Promise<void> {
const { port = 3000, host = '127.0.0.1' } = options;
// 初始化
await initServer(options);
const coreStatus = isCoreAvailable() ? '✅ Core loaded' : '⚠️ Core not available';
const authConfig = getAuthConfig();
const authStatus = authConfig.enabled ? '🔐 Enabled' : '🔓 Disabled';
console.log(`
╔════════════════════════════════════════════╗
║ AI Assistant Server ║
@@ -149,6 +213,8 @@ export function startServer(options: ServerOptions = {}): void {
║ WebSocket: ws://${host}:${port}/api/ws/:sessionId
║ SSE: http://${host}:${port}/api/sessions/:id/events
║ Health: http://${host}:${port}/health
║ Agent: ${coreStatus}
║ Auth: ${authStatus}
╚════════════════════════════════════════════╝
`);
@@ -170,4 +236,21 @@ export {
emitFileChangeEvent,
} from './sse.js';
export { broadcastToSession } from './ws.js';
export {
initCore,
isCoreAvailable,
getAgentStats,
processMessage,
cancelProcessing,
} from './agent/index.js';
export {
initAuth,
getAuthConfig,
generateToken,
addToken,
removeToken,
setAuthEnabled,
validateToken,
maskToken,
} from './auth/index.js';
export * from './types.js';
+8 -32
View File
@@ -6,6 +6,7 @@
import type { WSContext } from 'hono/ws';
import { getSessionManager } from './session/manager.js';
import { processMessage, cancelProcessing } from './agent/index.js';
import type { ClientMessage, ServerMessage } from './types.js';
// 存储活跃的 WebSocket 连接
@@ -103,9 +104,10 @@ export async function handleWebSocketMessage(
switch (message.type) {
case 'message': {
// 用户发送消息
const content = message.payload?.content || '';
const userMessage = sessionManager.addMessage(sessionId, {
role: 'user',
content: message.payload?.content || '',
content,
});
if (userMessage) {
@@ -116,48 +118,22 @@ export async function handleWebSocketMessage(
payload: userMessage,
});
// 更新状态为处理中
sessionManager.updateStatus(sessionId, 'busy');
// TODO: 调用 Agent 处理消息并流式返回
// 这里需要集成 core 模块的 Agent
// const agent = createAgent();
// for await (const chunk of agent.stream(message.payload.content)) {
// broadcastToSession(sessionId, {
// type: 'chunk',
// sessionId,
// payload: { content: chunk },
// });
// }
// 模拟响应 (后续替换为真实 Agent 调用)
setTimeout(() => {
const assistantMessage = sessionManager.addMessage(sessionId, {
role: 'assistant',
content: 'This is a placeholder response. Agent integration coming soon.',
});
broadcastToSession(sessionId, {
type: 'done',
sessionId,
payload: assistantMessage,
});
sessionManager.updateStatus(sessionId, 'idle');
}, 1000);
// 调用 Agent 处理消息(异步,不阻塞)
processMessage(sessionId, content).catch((error) => {
console.error('[WS] Agent processing error:', error);
});
}
break;
}
case 'cancel': {
// 取消当前操作
// TODO: 实现取消逻辑
cancelProcessing(sessionId);
broadcastToSession(sessionId, {
type: 'cancelled',
sessionId,
payload: { message: 'Operation cancelled' },
});
sessionManager.updateStatus(sessionId, 'idle');
break;
}