feat: 重构为 Monorepo 架构并实现 HTTP Server
架构变更: - 采用 pnpm workspaces 实现 Monorepo 结构 - 将现有代码迁移到 packages/core - 新增 packages/server HTTP 服务层 Server 功能: - REST API: 会话管理、工具管理、配置管理 - WebSocket: 实时双向通信支持 - SSE: 服务端事件推送 - Hono + Bun 作为运行时 API 端点: - GET/POST /api/sessions - 会话 CRUD - GET/POST /api/sessions/:id/messages - 消息管理 - GET /api/sessions/:id/events - SSE 事件流 - WS /api/ws/:sessionId - WebSocket 连接 - GET/POST /api/tools - 工具管理 - GET/PUT /api/config - 配置管理
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* AI Assistant Server Entry Point
|
||||
*
|
||||
* 独立运行的服务器入口,用于开发和测试
|
||||
*
|
||||
* Usage:
|
||||
* 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
|
||||
*/
|
||||
|
||||
import { app, websocket, startServer } from '../index.js';
|
||||
|
||||
// 解析命令行参数
|
||||
function parseArgs(): { port: number; host: string } {
|
||||
const args = process.argv.slice(2);
|
||||
let port = 3000;
|
||||
let host = '127.0.0.1';
|
||||
|
||||
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++;
|
||||
}
|
||||
}
|
||||
|
||||
return { port, host };
|
||||
}
|
||||
|
||||
// 主函数
|
||||
async function main() {
|
||||
const { port, host } = parseArgs();
|
||||
|
||||
// 打印启动信息
|
||||
startServer({ port, host });
|
||||
|
||||
// 启动 Bun 服务器
|
||||
const server = Bun.serve({
|
||||
port,
|
||||
hostname: host,
|
||||
fetch: app.fetch,
|
||||
websocket,
|
||||
});
|
||||
|
||||
console.log(`Server started at http://${host}:${port}`);
|
||||
|
||||
// 优雅关闭
|
||||
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);
|
||||
});
|
||||
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* AI Assistant Server
|
||||
*
|
||||
* HTTP Server 入口,提供 REST API、WebSocket 和 SSE 支持
|
||||
*/
|
||||
|
||||
import { Hono } from 'hono';
|
||||
import { cors } from 'hono/cors';
|
||||
import { logger } from 'hono/logger';
|
||||
import { createBunWebSocket } from 'hono/bun';
|
||||
|
||||
import { sessionsRouter, toolsRouter, configRouter } from './routes/index.js';
|
||||
import {
|
||||
handleWebSocket,
|
||||
handleWebSocketMessage,
|
||||
handleWebSocketClose,
|
||||
getConnectionStats,
|
||||
} from './ws.js';
|
||||
import { handleSSE, getSSEStats } from './sse.js';
|
||||
import { getSessionManager } from './session/manager.js';
|
||||
|
||||
// 创建 Hono 应用
|
||||
const app = new Hono();
|
||||
|
||||
// WebSocket 升级 (Bun 环境)
|
||||
const { upgradeWebSocket, websocket } = createBunWebSocket();
|
||||
|
||||
// 中间件
|
||||
app.use('*', logger());
|
||||
app.use(
|
||||
'*',
|
||||
cors({
|
||||
origin: '*', // 生产环境应该限制
|
||||
allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
||||
allowHeaders: ['Content-Type', 'Authorization'],
|
||||
})
|
||||
);
|
||||
|
||||
// 健康检查
|
||||
app.get('/health', (c) => {
|
||||
const sessionManager = getSessionManager();
|
||||
const wsStats = getConnectionStats();
|
||||
const sseStats = getSSEStats();
|
||||
|
||||
return c.json({
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
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);
|
||||
|
||||
// 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);
|
||||
},
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
// 挂载 API 到 /api
|
||||
app.route('/api', api);
|
||||
|
||||
// 404 处理
|
||||
app.notFound((c) => {
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Not found',
|
||||
},
|
||||
404
|
||||
);
|
||||
});
|
||||
|
||||
// 错误处理
|
||||
app.onError((err, c) => {
|
||||
console.error('[Server Error]', err);
|
||||
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: err.message || 'Internal server error',
|
||||
},
|
||||
500
|
||||
);
|
||||
});
|
||||
|
||||
// 服务器配置
|
||||
export interface ServerOptions {
|
||||
port?: number;
|
||||
host?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建服务器实例
|
||||
*/
|
||||
export function createServer(options: ServerOptions = {}) {
|
||||
const { port = 3000, host = '127.0.0.1' } = options;
|
||||
|
||||
return {
|
||||
app,
|
||||
websocket,
|
||||
port,
|
||||
host,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动服务器 (Bun 环境)
|
||||
*/
|
||||
export function startServer(options: ServerOptions = {}): void {
|
||||
const { port = 3000, host = '127.0.0.1' } = options;
|
||||
|
||||
console.log(`
|
||||
╔════════════════════════════════════════════╗
|
||||
║ AI Assistant Server ║
|
||||
╠════════════════════════════════════════════╣
|
||||
║ REST API: http://${host}:${port}/api
|
||||
║ WebSocket: ws://${host}:${port}/api/ws/:sessionId
|
||||
║ SSE: http://${host}:${port}/api/sessions/:id/events
|
||||
║ Health: http://${host}:${port}/health
|
||||
╚════════════════════════════════════════════╝
|
||||
`);
|
||||
|
||||
// Bun.serve 需要在 CLI 包中调用
|
||||
// 这里只导出配置
|
||||
}
|
||||
|
||||
// 导出
|
||||
export { app, websocket };
|
||||
export { getSessionManager } from './session/manager.js';
|
||||
export { registerTool, getRegisteredTools } from './routes/tools.js';
|
||||
export { getConfig, setConfig } from './routes/config.js';
|
||||
export {
|
||||
emitEvent,
|
||||
broadcastEvent,
|
||||
emitStatusEvent,
|
||||
emitLogEvent,
|
||||
emitProgressEvent,
|
||||
emitFileChangeEvent,
|
||||
} from './sse.js';
|
||||
export { broadcastToSession } from './ws.js';
|
||||
export * from './types.js';
|
||||
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Config API Routes
|
||||
*
|
||||
* 配置管理相关的 REST API
|
||||
*/
|
||||
|
||||
import { Hono } from 'hono';
|
||||
|
||||
export const configRouter = new Hono();
|
||||
|
||||
// 服务器配置 (后续会从配置文件加载)
|
||||
interface ServerConfig {
|
||||
model: string;
|
||||
maxTokens: number;
|
||||
temperature: number;
|
||||
workdir: string;
|
||||
allowedPaths: string[];
|
||||
deniedPaths: string[];
|
||||
}
|
||||
|
||||
let serverConfig: ServerConfig = {
|
||||
model: 'claude-sonnet-4-20250514',
|
||||
maxTokens: 8192,
|
||||
temperature: 0.7,
|
||||
workdir: process.cwd(),
|
||||
allowedPaths: [],
|
||||
deniedPaths: [],
|
||||
};
|
||||
|
||||
/**
|
||||
* GET /config - 获取当前配置
|
||||
*/
|
||||
configRouter.get('/', (c) => {
|
||||
return c.json({
|
||||
success: true,
|
||||
data: serverConfig,
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /config - 更新配置
|
||||
*/
|
||||
configRouter.put('/', async (c) => {
|
||||
try {
|
||||
const body = await c.req.json();
|
||||
|
||||
// 合并配置 (只更新提供的字段)
|
||||
serverConfig = {
|
||||
...serverConfig,
|
||||
...body,
|
||||
};
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
data: serverConfig,
|
||||
});
|
||||
} catch (error) {
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Invalid input',
|
||||
},
|
||||
400
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PATCH /config - 部分更新配置
|
||||
*/
|
||||
configRouter.patch('/', async (c) => {
|
||||
try {
|
||||
const body = await c.req.json();
|
||||
|
||||
// 部分更新
|
||||
Object.keys(body).forEach((key) => {
|
||||
if (key in serverConfig) {
|
||||
(serverConfig as any)[key] = body[key];
|
||||
}
|
||||
});
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
data: serverConfig,
|
||||
});
|
||||
} catch (error) {
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Invalid input',
|
||||
},
|
||||
400
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取当前配置 (内部使用)
|
||||
*/
|
||||
export function getConfig(): ServerConfig {
|
||||
return { ...serverConfig };
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置配置 (内部使用)
|
||||
*/
|
||||
export function setConfig(config: Partial<ServerConfig>): void {
|
||||
serverConfig = { ...serverConfig, ...config };
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* API Routes Index
|
||||
*
|
||||
* 聚合所有 API 路由
|
||||
*/
|
||||
|
||||
export { sessionsRouter } from './sessions.js';
|
||||
export { toolsRouter, registerTool, getRegisteredTools } from './tools.js';
|
||||
export { configRouter, getConfig, setConfig } from './config.js';
|
||||
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* Sessions API Routes
|
||||
*
|
||||
* 会话管理相关的 REST API
|
||||
*/
|
||||
|
||||
import { Hono } from 'hono';
|
||||
import { getSessionManager } from '../session/manager.js';
|
||||
import { CreateSessionInputSchema, SendMessageInputSchema } from '../types.js';
|
||||
|
||||
export const sessionsRouter = new Hono();
|
||||
|
||||
const sessionManager = getSessionManager();
|
||||
|
||||
/**
|
||||
* GET /sessions - 列出所有会话
|
||||
*/
|
||||
sessionsRouter.get('/', (c) => {
|
||||
const sessions = sessionManager.list();
|
||||
return c.json({
|
||||
success: true,
|
||||
data: sessions,
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /sessions - 创建新会话
|
||||
*/
|
||||
sessionsRouter.post('/', async (c) => {
|
||||
try {
|
||||
const body = await c.req.json();
|
||||
const input = CreateSessionInputSchema.parse(body);
|
||||
const session = sessionManager.create(input);
|
||||
|
||||
return c.json(
|
||||
{
|
||||
success: true,
|
||||
data: session,
|
||||
},
|
||||
201
|
||||
);
|
||||
} catch (error) {
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Invalid input',
|
||||
},
|
||||
400
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /sessions/:id - 获取单个会话
|
||||
*/
|
||||
sessionsRouter.get('/:id', (c) => {
|
||||
const id = c.req.param('id');
|
||||
const session = sessionManager.get(id);
|
||||
|
||||
if (!session) {
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Session not found',
|
||||
},
|
||||
404
|
||||
);
|
||||
}
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
data: session,
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /sessions/:id - 删除会话
|
||||
*/
|
||||
sessionsRouter.delete('/:id', (c) => {
|
||||
const id = c.req.param('id');
|
||||
|
||||
if (!sessionManager.exists(id)) {
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Session not found',
|
||||
},
|
||||
404
|
||||
);
|
||||
}
|
||||
|
||||
sessionManager.delete(id);
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
message: 'Session deleted',
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /sessions/:id/messages - 获取会话消息
|
||||
*/
|
||||
sessionsRouter.get('/:id/messages', (c) => {
|
||||
const id = c.req.param('id');
|
||||
|
||||
if (!sessionManager.exists(id)) {
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Session not found',
|
||||
},
|
||||
404
|
||||
);
|
||||
}
|
||||
|
||||
const messages = sessionManager.getMessages(id);
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
data: messages,
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /sessions/:id/messages - 发送消息
|
||||
*
|
||||
* 注意: 这个端点仅用于添加消息记录。
|
||||
* 实际的 AI 对话应该通过 WebSocket 进行。
|
||||
*/
|
||||
sessionsRouter.post('/:id/messages', async (c) => {
|
||||
const id = c.req.param('id');
|
||||
|
||||
if (!sessionManager.exists(id)) {
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Session not found',
|
||||
},
|
||||
404
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await c.req.json();
|
||||
const input = SendMessageInputSchema.parse(body);
|
||||
|
||||
const message = sessionManager.addMessage(id, {
|
||||
role: input.role,
|
||||
content: input.content,
|
||||
});
|
||||
|
||||
if (!message) {
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Failed to add message',
|
||||
},
|
||||
500
|
||||
);
|
||||
}
|
||||
|
||||
return c.json(
|
||||
{
|
||||
success: true,
|
||||
data: message,
|
||||
},
|
||||
201
|
||||
);
|
||||
} catch (error) {
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Invalid input',
|
||||
},
|
||||
400
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Tools API Routes
|
||||
*
|
||||
* 工具管理相关的 REST API
|
||||
*/
|
||||
|
||||
import { Hono } from 'hono';
|
||||
import type { Tool } from '../types.js';
|
||||
|
||||
export const toolsRouter = new Hono();
|
||||
|
||||
// 工具注册表 (后续会从 core 模块获取)
|
||||
const toolRegistry: Map<string, Tool> = new Map();
|
||||
|
||||
/**
|
||||
* 注册工具 (内部使用)
|
||||
*/
|
||||
export function registerTool(tool: Tool): void {
|
||||
toolRegistry.set(tool.name, tool);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有已注册的工具
|
||||
*/
|
||||
export function getRegisteredTools(): Tool[] {
|
||||
return Array.from(toolRegistry.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /tools - 列出所有可用工具
|
||||
*/
|
||||
toolsRouter.get('/', (c) => {
|
||||
const tools = getRegisteredTools();
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
data: tools,
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /tools/:name - 获取单个工具详情
|
||||
*/
|
||||
toolsRouter.get('/:name', (c) => {
|
||||
const name = c.req.param('name');
|
||||
const tool = toolRegistry.get(name);
|
||||
|
||||
if (!tool) {
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Tool not found',
|
||||
},
|
||||
404
|
||||
);
|
||||
}
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
data: tool,
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /tools/:name/execute - 执行工具
|
||||
*
|
||||
* 注意: 工具执行是同步的,对于长时间运行的工具,
|
||||
* 建议通过 WebSocket 执行以获取实时反馈。
|
||||
*/
|
||||
toolsRouter.post('/:name/execute', async (c) => {
|
||||
const name = c.req.param('name');
|
||||
const tool = toolRegistry.get(name);
|
||||
|
||||
if (!tool) {
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Tool not found',
|
||||
},
|
||||
404
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await c.req.json();
|
||||
const params = body.params || {};
|
||||
|
||||
// TODO: 实际调用 core 模块的工具执行逻辑
|
||||
// const result = await executeTool(name, params);
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
data: {
|
||||
tool: name,
|
||||
params,
|
||||
result: null, // 占位,后续实现
|
||||
message: 'Tool execution not yet implemented',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Execution failed',
|
||||
},
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* Session Manager
|
||||
*
|
||||
* 管理所有活跃的会话
|
||||
*/
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import type { Session, CreateSessionInput, Message, SessionStatus } from '../types.js';
|
||||
|
||||
export class SessionManager {
|
||||
private sessions: Map<string, Session> = new Map();
|
||||
private messages: Map<string, Message[]> = new Map();
|
||||
|
||||
/**
|
||||
* 创建新会话
|
||||
*/
|
||||
create(input: CreateSessionInput = {}): Session {
|
||||
const now = new Date().toISOString();
|
||||
const session: Session = {
|
||||
id: uuidv4(),
|
||||
name: input.name,
|
||||
workdir: input.workdir || process.cwd(),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
status: 'idle',
|
||||
messageCount: 0,
|
||||
};
|
||||
|
||||
this.sessions.set(session.id, session);
|
||||
this.messages.set(session.id, []);
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有会话
|
||||
*/
|
||||
list(): Session[] {
|
||||
return Array.from(this.sessions.values()).sort(
|
||||
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个会话
|
||||
*/
|
||||
get(id: string): Session | undefined {
|
||||
return this.sessions.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除会话
|
||||
*/
|
||||
delete(id: string): boolean {
|
||||
this.messages.delete(id);
|
||||
return this.sessions.delete(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新会话状态
|
||||
*/
|
||||
updateStatus(id: string, status: SessionStatus): Session | undefined {
|
||||
const session = this.sessions.get(id);
|
||||
if (!session) return undefined;
|
||||
|
||||
session.status = status;
|
||||
session.updatedAt = new Date().toISOString();
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话消息
|
||||
*/
|
||||
getMessages(sessionId: string): Message[] {
|
||||
return this.messages.get(sessionId) || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加消息
|
||||
*/
|
||||
addMessage(sessionId: string, message: Omit<Message, 'id' | 'sessionId' | 'createdAt'>): Message | undefined {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) return undefined;
|
||||
|
||||
const fullMessage: Message = {
|
||||
...message,
|
||||
id: uuidv4(),
|
||||
sessionId,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const messages = this.messages.get(sessionId) || [];
|
||||
messages.push(fullMessage);
|
||||
this.messages.set(sessionId, messages);
|
||||
|
||||
// 更新会话
|
||||
session.messageCount = messages.length;
|
||||
session.updatedAt = new Date().toISOString();
|
||||
|
||||
return fullMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话数量
|
||||
*/
|
||||
count(): number {
|
||||
return this.sessions.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查会话是否存在
|
||||
*/
|
||||
exists(id: string): boolean {
|
||||
return this.sessions.has(id);
|
||||
}
|
||||
}
|
||||
|
||||
// 单例
|
||||
let instance: SessionManager | null = null;
|
||||
|
||||
export function getSessionManager(): SessionManager {
|
||||
if (!instance) {
|
||||
instance = new SessionManager();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
/**
|
||||
* SSE (Server-Sent Events) Handler
|
||||
*
|
||||
* 处理单向事件推送,用于状态更新、日志流、进度通知等
|
||||
*/
|
||||
|
||||
import type { Context } from 'hono';
|
||||
import { streamSSE } from 'hono/streaming';
|
||||
import { getSessionManager } from './session/manager.js';
|
||||
import type { SSEEvent } from './types.js';
|
||||
|
||||
// 存储 SSE 订阅者
|
||||
interface SSESubscriber {
|
||||
sessionId: string;
|
||||
controller: ReadableStreamDefaultController<string>;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
const subscribers: Map<string, Set<SSESubscriber>> = new Map();
|
||||
|
||||
/**
|
||||
* 向会话的所有订阅者发送 SSE 事件
|
||||
*/
|
||||
export function emitEvent(sessionId: string, event: SSEEvent): void {
|
||||
const subs = subscribers.get(sessionId);
|
||||
if (!subs) return;
|
||||
|
||||
const eventString = formatSSEMessage(event);
|
||||
|
||||
for (const sub of subs) {
|
||||
if (!sub.active) continue;
|
||||
|
||||
try {
|
||||
sub.controller.enqueue(eventString);
|
||||
} catch (error) {
|
||||
// 订阅者已断开
|
||||
sub.active = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播事件到所有会话
|
||||
*/
|
||||
export function broadcastEvent(event: SSEEvent): void {
|
||||
for (const sessionId of subscribers.keys()) {
|
||||
emitEvent(sessionId, event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化 SSE 消息
|
||||
*/
|
||||
function formatSSEMessage(event: SSEEvent): string {
|
||||
const lines: string[] = [];
|
||||
|
||||
if (event.event) {
|
||||
lines.push(`event: ${event.event}`);
|
||||
}
|
||||
|
||||
const data = JSON.stringify(event.data);
|
||||
lines.push(`data: ${data}`);
|
||||
lines.push(''); // 空行结束消息
|
||||
|
||||
return lines.join('\n') + '\n';
|
||||
}
|
||||
|
||||
/**
|
||||
* SSE 路由处理器
|
||||
*
|
||||
* GET /api/sessions/:id/events
|
||||
*/
|
||||
export async function handleSSE(c: Context): Promise<Response> {
|
||||
const sessionId = c.req.param('id');
|
||||
const sessionManager = getSessionManager();
|
||||
|
||||
// 验证会话
|
||||
if (!sessionManager.exists(sessionId)) {
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Session not found',
|
||||
},
|
||||
404
|
||||
);
|
||||
}
|
||||
|
||||
return streamSSE(c, async (stream) => {
|
||||
// 创建订阅者
|
||||
const subscriber: SSESubscriber = {
|
||||
sessionId,
|
||||
controller: null as any, // Will be set by stream internals
|
||||
active: true,
|
||||
};
|
||||
|
||||
// 注册订阅者
|
||||
if (!subscribers.has(sessionId)) {
|
||||
subscribers.set(sessionId, new Set());
|
||||
}
|
||||
subscribers.get(sessionId)!.add(subscriber);
|
||||
|
||||
console.log(`[SSE] Client subscribed to session: ${sessionId}`);
|
||||
|
||||
// 发送初始连接事件
|
||||
await stream.writeSSE({
|
||||
event: 'connected',
|
||||
data: JSON.stringify({
|
||||
timestamp: Date.now(),
|
||||
payload: { sessionId },
|
||||
}),
|
||||
});
|
||||
|
||||
// 发送心跳保持连接
|
||||
const heartbeatInterval = setInterval(async () => {
|
||||
if (!subscriber.active) {
|
||||
clearInterval(heartbeatInterval);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await stream.writeSSE({
|
||||
event: 'heartbeat',
|
||||
data: JSON.stringify({
|
||||
timestamp: Date.now(),
|
||||
payload: null,
|
||||
}),
|
||||
});
|
||||
} catch {
|
||||
subscriber.active = false;
|
||||
clearInterval(heartbeatInterval);
|
||||
}
|
||||
}, 30000); // 每 30 秒发送心跳
|
||||
|
||||
// 监听关闭
|
||||
stream.onAbort(() => {
|
||||
subscriber.active = false;
|
||||
clearInterval(heartbeatInterval);
|
||||
|
||||
const subs = subscribers.get(sessionId);
|
||||
if (subs) {
|
||||
subs.delete(subscriber);
|
||||
if (subs.size === 0) {
|
||||
subscribers.delete(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[SSE] Client unsubscribed from session: ${sessionId}`);
|
||||
});
|
||||
|
||||
// 保持连接打开
|
||||
// 事件将通过 emitEvent() 发送
|
||||
while (subscriber.active) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送状态更新事件
|
||||
*/
|
||||
export function emitStatusEvent(
|
||||
sessionId: string,
|
||||
status: string,
|
||||
details?: Record<string, any>
|
||||
): void {
|
||||
emitEvent(sessionId, {
|
||||
event: 'status',
|
||||
data: {
|
||||
timestamp: Date.now(),
|
||||
payload: { status, ...details },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送日志事件
|
||||
*/
|
||||
export function emitLogEvent(
|
||||
sessionId: string,
|
||||
level: 'info' | 'warn' | 'error',
|
||||
message: string
|
||||
): void {
|
||||
emitEvent(sessionId, {
|
||||
event: 'log',
|
||||
data: {
|
||||
timestamp: Date.now(),
|
||||
payload: { level, message },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送进度事件
|
||||
*/
|
||||
export function emitProgressEvent(
|
||||
sessionId: string,
|
||||
progress: number,
|
||||
message?: string
|
||||
): void {
|
||||
emitEvent(sessionId, {
|
||||
event: 'progress',
|
||||
data: {
|
||||
timestamp: Date.now(),
|
||||
payload: { progress, message },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送文件变更事件
|
||||
*/
|
||||
export function emitFileChangeEvent(
|
||||
sessionId: string,
|
||||
type: 'created' | 'modified' | 'deleted',
|
||||
path: string
|
||||
): void {
|
||||
emitEvent(sessionId, {
|
||||
event: 'file_change',
|
||||
data: {
|
||||
timestamp: Date.now(),
|
||||
payload: { type, path },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取订阅统计
|
||||
*/
|
||||
export function getSSEStats(): { sessions: number; subscribers: number } {
|
||||
let totalSubscribers = 0;
|
||||
for (const subs of subscribers.values()) {
|
||||
totalSubscribers += subs.size;
|
||||
}
|
||||
return {
|
||||
sessions: subscribers.size,
|
||||
subscribers: totalSubscribers,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* Server 类型定义
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
// ============ Session 相关 ============
|
||||
|
||||
export const SessionStatusSchema = z.enum(['idle', 'active', 'busy', 'running', 'paused']);
|
||||
|
||||
export const SessionSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string().optional(),
|
||||
workdir: z.string(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
status: SessionStatusSchema,
|
||||
messageCount: z.number().int().min(0),
|
||||
});
|
||||
|
||||
export type Session = z.infer<typeof SessionSchema>;
|
||||
export type SessionStatus = z.infer<typeof SessionStatusSchema>;
|
||||
|
||||
export const CreateSessionInputSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
workdir: z.string().optional(),
|
||||
});
|
||||
|
||||
export type CreateSessionInput = z.infer<typeof CreateSessionInputSchema>;
|
||||
|
||||
// ============ Message 相关 ============
|
||||
|
||||
export const MessageRoleSchema = z.enum(['user', 'assistant', 'system', 'tool']);
|
||||
|
||||
export const MessageSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
sessionId: z.string().uuid(),
|
||||
role: MessageRoleSchema,
|
||||
content: z.string(),
|
||||
createdAt: z.string(),
|
||||
toolCalls: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
arguments: z.record(z.string(), z.unknown()),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
toolResults: z
|
||||
.array(
|
||||
z.object({
|
||||
toolCallId: z.string(),
|
||||
result: z.unknown(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export type Message = z.infer<typeof MessageSchema>;
|
||||
|
||||
export const SendMessageInputSchema = z.object({
|
||||
role: MessageRoleSchema.default('user'),
|
||||
content: z.string().min(1),
|
||||
stream: z.boolean().optional().default(true),
|
||||
});
|
||||
|
||||
export type SendMessageInput = z.infer<typeof SendMessageInputSchema>;
|
||||
|
||||
// ============ Tool 相关 ============
|
||||
|
||||
export const ToolSchema = z.object({
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
category: z.string().optional(),
|
||||
parameters: z.record(
|
||||
z.string(),
|
||||
z.object({
|
||||
type: z.string(),
|
||||
description: z.string(),
|
||||
required: z.boolean().optional(),
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
export type Tool = z.infer<typeof ToolSchema>;
|
||||
|
||||
// ============ WebSocket 消息 ============
|
||||
|
||||
// 客户端发送的消息
|
||||
export interface ClientMessage {
|
||||
type: 'message' | 'cancel' | 'tool_response';
|
||||
sessionId: string;
|
||||
payload?: {
|
||||
content?: string;
|
||||
toolCallId?: string;
|
||||
approved?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
// 服务端发送的消息
|
||||
export interface ServerMessage {
|
||||
type:
|
||||
| 'connected'
|
||||
| 'message_received'
|
||||
| 'chunk'
|
||||
| 'tool_call'
|
||||
| 'tool_result'
|
||||
| 'done'
|
||||
| 'cancelled'
|
||||
| 'error';
|
||||
sessionId: string;
|
||||
payload?: unknown;
|
||||
}
|
||||
|
||||
// ============ SSE 事件 ============
|
||||
|
||||
export interface SSEEvent {
|
||||
event?: 'connected' | 'heartbeat' | 'status' | 'log' | 'progress' | 'file_change' | 'tool_execution';
|
||||
data: {
|
||||
timestamp: number;
|
||||
sessionId?: string;
|
||||
payload: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
// ============ API 响应 ============
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
error?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
items: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
hasMore: boolean;
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* WebSocket Handler
|
||||
*
|
||||
* 处理实时双向通信,主要用于 AI 对话流
|
||||
*/
|
||||
|
||||
import type { WSContext } from 'hono/ws';
|
||||
import { getSessionManager } from './session/manager.js';
|
||||
import type { ClientMessage, ServerMessage } from './types.js';
|
||||
|
||||
// 存储活跃的 WebSocket 连接
|
||||
const connections: Map<string, Set<WSContext>> = new Map();
|
||||
|
||||
/**
|
||||
* 获取会话的所有连接
|
||||
*/
|
||||
export function getSessionConnections(sessionId: string): Set<WSContext> {
|
||||
return connections.get(sessionId) || new Set();
|
||||
}
|
||||
|
||||
/**
|
||||
* 向会话的所有连接发送消息
|
||||
*/
|
||||
export function broadcastToSession(sessionId: string, message: ServerMessage): void {
|
||||
const conns = connections.get(sessionId);
|
||||
if (!conns) return;
|
||||
|
||||
const data = JSON.stringify(message);
|
||||
for (const ws of conns) {
|
||||
try {
|
||||
ws.send(data);
|
||||
} catch (error) {
|
||||
console.error('Failed to send message:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WebSocket 连接处理器
|
||||
*/
|
||||
export function handleWebSocket(ws: WSContext, sessionId: string): void {
|
||||
const sessionManager = getSessionManager();
|
||||
|
||||
// 验证会话
|
||||
if (!sessionManager.exists(sessionId)) {
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: 'error',
|
||||
sessionId,
|
||||
payload: { message: 'Session not found' },
|
||||
} as ServerMessage)
|
||||
);
|
||||
ws.close(4004, 'Session not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// 注册连接
|
||||
if (!connections.has(sessionId)) {
|
||||
connections.set(sessionId, new Set());
|
||||
}
|
||||
connections.get(sessionId)!.add(ws);
|
||||
|
||||
// 更新会话状态
|
||||
sessionManager.updateStatus(sessionId, 'active');
|
||||
|
||||
// 发送连接成功消息
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: 'connected',
|
||||
sessionId,
|
||||
payload: { message: 'Connected to session' },
|
||||
} as ServerMessage)
|
||||
);
|
||||
|
||||
console.log(`[WS] Client connected to session: ${sessionId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* WebSocket 消息处理器
|
||||
*/
|
||||
export async function handleWebSocketMessage(
|
||||
ws: WSContext,
|
||||
sessionId: string,
|
||||
data: unknown
|
||||
): Promise<void> {
|
||||
const sessionManager = getSessionManager();
|
||||
|
||||
try {
|
||||
// 处理不同类型的数据
|
||||
let text: string;
|
||||
if (typeof data === 'string') {
|
||||
text = data;
|
||||
} else if (data instanceof ArrayBuffer || data instanceof SharedArrayBuffer) {
|
||||
text = new TextDecoder().decode(data as ArrayBuffer);
|
||||
} else if (data instanceof Blob) {
|
||||
text = await data.text();
|
||||
} else {
|
||||
text = String(data);
|
||||
}
|
||||
|
||||
const message: ClientMessage = JSON.parse(text);
|
||||
|
||||
switch (message.type) {
|
||||
case 'message': {
|
||||
// 用户发送消息
|
||||
const userMessage = sessionManager.addMessage(sessionId, {
|
||||
role: 'user',
|
||||
content: message.payload?.content || '',
|
||||
});
|
||||
|
||||
if (userMessage) {
|
||||
// 广播用户消息
|
||||
broadcastToSession(sessionId, {
|
||||
type: 'message_received',
|
||||
sessionId,
|
||||
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);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'cancel': {
|
||||
// 取消当前操作
|
||||
// TODO: 实现取消逻辑
|
||||
broadcastToSession(sessionId, {
|
||||
type: 'cancelled',
|
||||
sessionId,
|
||||
payload: { message: 'Operation cancelled' },
|
||||
});
|
||||
sessionManager.updateStatus(sessionId, 'idle');
|
||||
break;
|
||||
}
|
||||
|
||||
case 'tool_response': {
|
||||
// 工具执行结果 (用于人工确认场景)
|
||||
// TODO: 处理工具响应
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: 'error',
|
||||
sessionId,
|
||||
payload: { message: `Unknown message type: ${(message as any).type}` },
|
||||
} as ServerMessage)
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: 'error',
|
||||
sessionId,
|
||||
payload: {
|
||||
message: error instanceof Error ? error.message : 'Failed to process message',
|
||||
},
|
||||
} as ServerMessage)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WebSocket 关闭处理器
|
||||
*/
|
||||
export function handleWebSocketClose(ws: WSContext, sessionId: string): void {
|
||||
const conns = connections.get(sessionId);
|
||||
if (conns) {
|
||||
conns.delete(ws);
|
||||
if (conns.size === 0) {
|
||||
connections.delete(sessionId);
|
||||
// 当没有连接时,更新会话状态
|
||||
const sessionManager = getSessionManager();
|
||||
sessionManager.updateStatus(sessionId, 'idle');
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[WS] Client disconnected from session: ${sessionId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接统计
|
||||
*/
|
||||
export function getConnectionStats(): { sessions: number; connections: number } {
|
||||
let totalConnections = 0;
|
||||
for (const conns of connections.values()) {
|
||||
totalConnections += conns.size;
|
||||
}
|
||||
return {
|
||||
sessions: connections.size,
|
||||
connections: totalConnections,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user