- 修复 emitEvent 无限递归(调用自身→调用 eventBus.publishSessionEvent) - 修复 prompt() 用户消息缺失 uuid/content 字段 - session.ready 事件映射为 session.updated(非重复 session.created) - 新增用户消息事件(message.updated role:user + message.part.updated) - 新增 session.status busy/idle、step-start/step-finish、session.idle/diff - 清理 heartbeat properties 为空对象 - 仅 assistant 类型消息作为 session.message 转发,过滤 system/其他杂消息 - 权限响应映射 once/always/reject → allow/deny - 会话创建支持 permission ruleset 推导 permission mode - prompt 路由支持 model 字段切换模型 - 初始化消息支持 hooks 配置 - 新增 HookCallback/ControlCancelRequest 处理 - 权限响应支持 updatedPermissions 和 interrupt - 新增 --resume-session-at、--permission-prompt-tool stdio 参数 - 追踪 lastMessageUuid 用于消息级回退 - EventBus dead client 清理
114 lines
2.7 KiB
TypeScript
114 lines
2.7 KiB
TypeScript
export type SSEWriter = {
|
|
writeSSE: (opts: { event: string; data: string }) => Promise<void>
|
|
}
|
|
|
|
type SSEClient = {
|
|
id: string
|
|
writer: SSEWriter | null
|
|
rawWrite: ((event: string, data: unknown) => void) | null
|
|
sessionIdFilter?: string
|
|
}
|
|
|
|
type BusEvent = {
|
|
event: string
|
|
data: unknown
|
|
}
|
|
|
|
export class EventBus {
|
|
private clients = new Map<string, SSEClient>()
|
|
private buffer: BusEvent[] = []
|
|
private bufferSize = 100
|
|
private heartbeatInterval: ReturnType<typeof setInterval> | null = null
|
|
|
|
addClient(writer: SSEWriter, sessionIdFilter?: string): string {
|
|
const id = crypto.randomUUID()
|
|
const client: SSEClient = { id, writer, rawWrite: null, sessionIdFilter }
|
|
this.clients.set(id, client)
|
|
|
|
const sendAndCleanup = (opts: { event: string; data: string }) =>
|
|
writer.writeSSE(opts).catch(() => {
|
|
this.clients.delete(id)
|
|
})
|
|
|
|
void sendAndCleanup({
|
|
event: 'connected',
|
|
data: JSON.stringify({ type: 'server.connected' }),
|
|
})
|
|
|
|
for (const buffered of this.buffer) {
|
|
void sendAndCleanup({
|
|
event: buffered.event,
|
|
data: JSON.stringify(buffered.data),
|
|
})
|
|
}
|
|
|
|
return id
|
|
}
|
|
|
|
removeClient(id: string): void {
|
|
this.clients.delete(id)
|
|
}
|
|
|
|
publish(event: string, data: unknown): void {
|
|
const entry: BusEvent = { event, data }
|
|
this.buffer.push(entry)
|
|
if (this.buffer.length > this.bufferSize) {
|
|
this.buffer.shift()
|
|
}
|
|
const payload = JSON.stringify(data)
|
|
const deadIds: string[] = []
|
|
for (const client of this.clients.values()) {
|
|
if (client.writer) {
|
|
if (client.sessionIdFilter) {
|
|
const dataObj = data as Record<string, unknown> | undefined
|
|
if (dataObj?.session_id && dataObj.session_id !== client.sessionIdFilter) {
|
|
continue
|
|
}
|
|
}
|
|
client.writer.writeSSE({ event, data: payload }).catch(() => {
|
|
deadIds.push(client.id)
|
|
})
|
|
}
|
|
}
|
|
if (deadIds.length > 0) {
|
|
for (const id of deadIds) {
|
|
this.clients.delete(id)
|
|
}
|
|
}
|
|
}
|
|
|
|
publishSessionEvent(
|
|
sessionId: string,
|
|
event: string,
|
|
data: Record<string, unknown>,
|
|
): void {
|
|
this.publish(`session.${event}`, { session_id: sessionId, ...data })
|
|
}
|
|
|
|
startHeartbeat(intervalMs = 10000): void {
|
|
this.heartbeatInterval = setInterval(() => {
|
|
this.publish('heartbeat', {
|
|
type: 'server.heartbeat',
|
|
ts: Date.now(),
|
|
})
|
|
}, intervalMs)
|
|
}
|
|
|
|
stopHeartbeat(): void {
|
|
if (this.heartbeatInterval) {
|
|
clearInterval(this.heartbeatInterval)
|
|
this.heartbeatInterval = null
|
|
}
|
|
}
|
|
|
|
clientCount(): number {
|
|
return this.clients.size
|
|
}
|
|
|
|
destroy(): void {
|
|
this.stopHeartbeat()
|
|
this.clients.clear()
|
|
this.buffer.length = 0
|
|
}
|
|
}
|