feat: add MaiSaka real-time chat flow monitoring component and WebSocket event handling

- Implemented the MaiSakaMonitor component for real-time monitoring of chat flow using WebSocket.
- Created a custom hook `useMaisakaMonitor` to manage WebSocket subscriptions and event states.
- Developed a backend module for broadcasting various monitoring events through WebSocket.
- Added serialization functions for messages and tool calls to optimize data transmission.
- Included event emission functions for session start, message ingestion, cycle start, timing gate results, planner requests/responses, tool executions, and replier requests/responses.
This commit is contained in:
DrSmoothl
2026-04-05 00:23:34 +08:00
parent 2fb911a8d5
commit c816ad4179
18 changed files with 1612 additions and 94 deletions

View File

@@ -0,0 +1,223 @@
/**
* MaiSaka 实时监控 WebSocket 客户端
*
* 订阅 maisaka_monitor 主题,接收推理引擎各阶段的实时事件。
*/
import type { WsEventEnvelope } from './unified-ws'
import { unifiedWsClient } from './unified-ws'
// ─── 事件数据类型 ───────────────────────────────────────────────
export interface MaisakaMessage {
role: string
content: string | null
tool_call_id?: string
tool_calls?: MaisakaToolCall[]
}
export interface MaisakaToolCall {
id: string
name: string
arguments?: Record<string, unknown>
arguments_raw?: string
}
export interface SessionStartEvent {
session_id: string
session_name: string
timestamp: number
}
export interface MessageIngestedEvent {
session_id: string
speaker_name: string
content: string
message_id: string
timestamp: number
}
export interface CycleStartEvent {
session_id: string
cycle_id: number
round_index: number
max_rounds: number
history_count: number
timestamp: number
}
export interface TimingGateResultEvent {
session_id: string
cycle_id: number
action: 'continue' | 'wait' | 'no_reply'
content: string | null
tool_calls: MaisakaToolCall[]
messages: MaisakaMessage[]
prompt_tokens: number
selected_history_count: number
duration_ms: number
timestamp: number
}
export interface PlannerRequestEvent {
session_id: string
cycle_id: number
messages: MaisakaMessage[]
tool_count: number
selected_history_count: number
timestamp: number
}
export interface PlannerResponseEvent {
session_id: string
cycle_id: number
content: string | null
tool_calls: MaisakaToolCall[]
prompt_tokens: number
completion_tokens: number
total_tokens: number
duration_ms: number
timestamp: number
}
export interface ToolExecutionEvent {
session_id: string
cycle_id: number
tool_name: string
tool_args: Record<string, unknown>
result_summary: string
success: boolean
duration_ms: number
timestamp: number
}
export interface CycleEndEvent {
session_id: string
cycle_id: number
time_records: Record<string, number>
agent_state: string
timestamp: number
}
export interface ReplierRequestEvent {
session_id: string
messages: MaisakaMessage[]
model_name: string
timestamp: number
}
export interface ReplierResponseEvent {
session_id: string
content: string | null
reasoning: string
model_name: string
prompt_tokens: number
completion_tokens: number
total_tokens: number
duration_ms: number
success: boolean
timestamp: number
}
// ─── 统一事件联合类型 ─────────────────────────────────────────
export type MaisakaMonitorEvent =
| { type: 'session.start'; data: SessionStartEvent }
| { type: 'message.ingested'; data: MessageIngestedEvent }
| { type: 'cycle.start'; data: CycleStartEvent }
| { type: 'timing_gate.result'; data: TimingGateResultEvent }
| { type: 'planner.request'; data: PlannerRequestEvent }
| { type: 'planner.response'; data: PlannerResponseEvent }
| { type: 'tool.execution'; data: ToolExecutionEvent }
| { type: 'cycle.end'; data: CycleEndEvent }
| { type: 'replier.request'; data: ReplierRequestEvent }
| { type: 'replier.response'; data: ReplierResponseEvent }
export type MaisakaEventListener = (event: MaisakaMonitorEvent) => void
// ─── 客户端 ───────────────────────────────────────────────────
class MaisakaMonitorClient {
private initialized = false
private listenerIdCounter = 0
private listeners: Map<number, MaisakaEventListener> = new Map()
private subscriptionActive = false
private subscriptionPromise: Promise<void> | null = null
private deferredUnsubTimer: ReturnType<typeof setTimeout> | null = null
private initialize(): void {
if (this.initialized) {
return
}
unifiedWsClient.addEventListener((message: WsEventEnvelope) => {
if (message.domain !== 'maisaka_monitor') {
return
}
const event: MaisakaMonitorEvent = {
type: message.event as MaisakaMonitorEvent['type'],
data: message.data as never,
}
this.listeners.forEach((listener) => {
try {
listener(event)
} catch (error) {
console.error('MaiSaka 监控事件监听器执行失败:', error)
}
})
})
this.initialized = true
}
private async ensureSubscribed(): Promise<void> {
if (this.subscriptionActive) {
return
}
if (this.subscriptionPromise === null) {
this.subscriptionPromise = unifiedWsClient
.subscribe('maisaka_monitor', 'main')
.then(() => {
this.subscriptionActive = true
})
.finally(() => {
this.subscriptionPromise = null
})
}
await this.subscriptionPromise
}
async subscribe(listener: MaisakaEventListener): Promise<() => Promise<void>> {
this.initialize()
const listenerId = ++this.listenerIdCounter
this.listeners.set(listenerId, listener)
// 如果有待执行的延迟退订取消它React StrictMode 快速卸载/重新挂载)
if (this.deferredUnsubTimer !== null) {
clearTimeout(this.deferredUnsubTimer)
this.deferredUnsubTimer = null
}
await this.ensureSubscribed()
return async () => {
this.listeners.delete(listenerId)
if (this.listeners.size === 0 && this.subscriptionActive) {
// 延迟退订:等待短暂时间再真正退订,避免 StrictMode 导致的竞态
this.deferredUnsubTimer = setTimeout(() => {
this.deferredUnsubTimer = null
if (this.listeners.size === 0 && this.subscriptionActive) {
this.subscriptionActive = false
void unifiedWsClient.unsubscribe('maisaka_monitor', 'main')
}
}, 200)
}
}
}
}
export const maisakaMonitorClient = new MaisakaMonitorClient()