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,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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user