Skip to content

Commit c816ad4

Browse files
committed
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.
1 parent 2fb911a commit c816ad4

18 files changed

Lines changed: 1612 additions & 94 deletions

File tree

dashboard/src/components/layout/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export const menuSections: MenuSection[] = [
3636
{ icon: LayoutGrid, label: 'sidebar.menu.configTemplate', path: '/config/pack-market' },
3737
{ icon: Sliders, label: 'sidebar.menu.pluginConfig', path: '/plugin-config' },
3838
{ icon: FileSearch, label: 'sidebar.menu.logViewer', path: '/logs', searchDescription: 'search.items.logsDesc' },
39-
{ icon: Activity, label: 'sidebar.menu.plannerMonitor', path: '/planner-monitor' },
39+
{ icon: Activity, label: 'sidebar.menu.maisakaMonitor', path: '/planner-monitor' },
4040
{ icon: MessageSquare, label: 'sidebar.menu.localChat', path: '/chat' },
4141
],
4242
},

dashboard/src/i18n/locales/en.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
"configTemplate": "Config Templates",
3939
"pluginConfig": "Plugin Config",
4040
"logViewer": "Log Viewer",
41-
"plannerMonitor": "Planner & Replier Monitor",
41+
"maisakaMonitor": "MaiSaka Chat Monitor",
4242
"localChat": "Local Chat",
4343
"settings": "Settings"
4444
}

dashboard/src/i18n/locales/ja.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
"configTemplate": "設定テンプレート",
3939
"pluginConfig": "プラグイン設定",
4040
"logViewer": "ログビューア",
41-
"plannerMonitor": "プランナー & リプライヤー監視",
41+
"maisakaMonitor": "MaiSaka チャット監視",
4242
"localChat": "ローカルチャット",
4343
"settings": "設定"
4444
}

dashboard/src/i18n/locales/ko.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
"configTemplate": "설정 템플릿",
3939
"pluginConfig": "플러그인 설정",
4040
"logViewer": "로그 뷰어",
41-
"plannerMonitor": "플래너 & 리플라이어 모니터",
41+
"maisakaMonitor": "MaiSaka 채팅 모니터",
4242
"localChat": "로컬 채팅",
4343
"settings": "설정"
4444
}

dashboard/src/i18n/locales/zh.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
"configTemplate": "配置模板市场",
3939
"pluginConfig": "插件配置",
4040
"logViewer": "日志查看器",
41-
"plannerMonitor": "计划器&回复器监控",
41+
"maisakaMonitor": "MaiSaka 聊天流监控",
4242
"localChat": "本地聊天室",
4343
"settings": "系统设置"
4444
}

dashboard/src/lib/api-base.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export async function getApiBaseUrl(): Promise<string> {
3030
/**
3131
* Get WebSocket base URL
3232
* - Electron: Convert HTTP/HTTPS URL to WS/WSS
33-
* - Browser DEV: ws://127.0.0.1:8001 (hardcoded, same as log-websocket.ts)
33+
* - Browser DEV: Use same-origin WS URL and let Vite proxy forward requests
3434
* - Browser PROD: Construct WS URL from window.location
3535
*/
3636
export async function getWsBaseUrl(): Promise<string> {
@@ -47,9 +47,10 @@ export async function getWsBaseUrl(): Promise<string> {
4747
})
4848
}
4949

50-
// Browser DEV: Use hardcoded WebSocket server
50+
// Browser DEV: Use same-origin URL so Vite proxy can forward WebSocket requests
5151
if (import.meta.env.DEV) {
52-
return 'ws://127.0.0.1:8001'
52+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
53+
return `${protocol}//${window.location.host}`
5354
}
5455

5556
// Browser PROD: Construct WS URL from current location
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
/**
2+
* MaiSaka 实时监控 WebSocket 客户端
3+
*
4+
* 订阅 maisaka_monitor 主题,接收推理引擎各阶段的实时事件。
5+
*/
6+
import type { WsEventEnvelope } from './unified-ws'
7+
8+
import { unifiedWsClient } from './unified-ws'
9+
10+
// ─── 事件数据类型 ───────────────────────────────────────────────
11+
12+
export interface MaisakaMessage {
13+
role: string
14+
content: string | null
15+
tool_call_id?: string
16+
tool_calls?: MaisakaToolCall[]
17+
}
18+
19+
export interface MaisakaToolCall {
20+
id: string
21+
name: string
22+
arguments?: Record<string, unknown>
23+
arguments_raw?: string
24+
}
25+
26+
export interface SessionStartEvent {
27+
session_id: string
28+
session_name: string
29+
timestamp: number
30+
}
31+
32+
export interface MessageIngestedEvent {
33+
session_id: string
34+
speaker_name: string
35+
content: string
36+
message_id: string
37+
timestamp: number
38+
}
39+
40+
export interface CycleStartEvent {
41+
session_id: string
42+
cycle_id: number
43+
round_index: number
44+
max_rounds: number
45+
history_count: number
46+
timestamp: number
47+
}
48+
49+
export interface TimingGateResultEvent {
50+
session_id: string
51+
cycle_id: number
52+
action: 'continue' | 'wait' | 'no_reply'
53+
content: string | null
54+
tool_calls: MaisakaToolCall[]
55+
messages: MaisakaMessage[]
56+
prompt_tokens: number
57+
selected_history_count: number
58+
duration_ms: number
59+
timestamp: number
60+
}
61+
62+
export interface PlannerRequestEvent {
63+
session_id: string
64+
cycle_id: number
65+
messages: MaisakaMessage[]
66+
tool_count: number
67+
selected_history_count: number
68+
timestamp: number
69+
}
70+
71+
export interface PlannerResponseEvent {
72+
session_id: string
73+
cycle_id: number
74+
content: string | null
75+
tool_calls: MaisakaToolCall[]
76+
prompt_tokens: number
77+
completion_tokens: number
78+
total_tokens: number
79+
duration_ms: number
80+
timestamp: number
81+
}
82+
83+
export interface ToolExecutionEvent {
84+
session_id: string
85+
cycle_id: number
86+
tool_name: string
87+
tool_args: Record<string, unknown>
88+
result_summary: string
89+
success: boolean
90+
duration_ms: number
91+
timestamp: number
92+
}
93+
94+
export interface CycleEndEvent {
95+
session_id: string
96+
cycle_id: number
97+
time_records: Record<string, number>
98+
agent_state: string
99+
timestamp: number
100+
}
101+
102+
export interface ReplierRequestEvent {
103+
session_id: string
104+
messages: MaisakaMessage[]
105+
model_name: string
106+
timestamp: number
107+
}
108+
109+
export interface ReplierResponseEvent {
110+
session_id: string
111+
content: string | null
112+
reasoning: string
113+
model_name: string
114+
prompt_tokens: number
115+
completion_tokens: number
116+
total_tokens: number
117+
duration_ms: number
118+
success: boolean
119+
timestamp: number
120+
}
121+
122+
// ─── 统一事件联合类型 ─────────────────────────────────────────
123+
124+
export type MaisakaMonitorEvent =
125+
| { type: 'session.start'; data: SessionStartEvent }
126+
| { type: 'message.ingested'; data: MessageIngestedEvent }
127+
| { type: 'cycle.start'; data: CycleStartEvent }
128+
| { type: 'timing_gate.result'; data: TimingGateResultEvent }
129+
| { type: 'planner.request'; data: PlannerRequestEvent }
130+
| { type: 'planner.response'; data: PlannerResponseEvent }
131+
| { type: 'tool.execution'; data: ToolExecutionEvent }
132+
| { type: 'cycle.end'; data: CycleEndEvent }
133+
| { type: 'replier.request'; data: ReplierRequestEvent }
134+
| { type: 'replier.response'; data: ReplierResponseEvent }
135+
136+
export type MaisakaEventListener = (event: MaisakaMonitorEvent) => void
137+
138+
// ─── 客户端 ───────────────────────────────────────────────────
139+
140+
class MaisakaMonitorClient {
141+
private initialized = false
142+
private listenerIdCounter = 0
143+
private listeners: Map<number, MaisakaEventListener> = new Map()
144+
private subscriptionActive = false
145+
private subscriptionPromise: Promise<void> | null = null
146+
private deferredUnsubTimer: ReturnType<typeof setTimeout> | null = null
147+
148+
private initialize(): void {
149+
if (this.initialized) {
150+
return
151+
}
152+
153+
unifiedWsClient.addEventListener((message: WsEventEnvelope) => {
154+
if (message.domain !== 'maisaka_monitor') {
155+
return
156+
}
157+
158+
const event: MaisakaMonitorEvent = {
159+
type: message.event as MaisakaMonitorEvent['type'],
160+
data: message.data as never,
161+
}
162+
163+
this.listeners.forEach((listener) => {
164+
try {
165+
listener(event)
166+
} catch (error) {
167+
console.error('MaiSaka 监控事件监听器执行失败:', error)
168+
}
169+
})
170+
})
171+
172+
this.initialized = true
173+
}
174+
175+
private async ensureSubscribed(): Promise<void> {
176+
if (this.subscriptionActive) {
177+
return
178+
}
179+
180+
if (this.subscriptionPromise === null) {
181+
this.subscriptionPromise = unifiedWsClient
182+
.subscribe('maisaka_monitor', 'main')
183+
.then(() => {
184+
this.subscriptionActive = true
185+
})
186+
.finally(() => {
187+
this.subscriptionPromise = null
188+
})
189+
}
190+
191+
await this.subscriptionPromise
192+
}
193+
194+
async subscribe(listener: MaisakaEventListener): Promise<() => Promise<void>> {
195+
this.initialize()
196+
const listenerId = ++this.listenerIdCounter
197+
this.listeners.set(listenerId, listener)
198+
199+
// 如果有待执行的延迟退订,取消它(React StrictMode 快速卸载/重新挂载)
200+
if (this.deferredUnsubTimer !== null) {
201+
clearTimeout(this.deferredUnsubTimer)
202+
this.deferredUnsubTimer = null
203+
}
204+
205+
await this.ensureSubscribed()
206+
207+
return async () => {
208+
this.listeners.delete(listenerId)
209+
if (this.listeners.size === 0 && this.subscriptionActive) {
210+
// 延迟退订:等待短暂时间再真正退订,避免 StrictMode 导致的竞态
211+
this.deferredUnsubTimer = setTimeout(() => {
212+
this.deferredUnsubTimer = null
213+
if (this.listeners.size === 0 && this.subscriptionActive) {
214+
this.subscriptionActive = false
215+
void unifiedWsClient.unsubscribe('maisaka_monitor', 'main')
216+
}
217+
}, 200)
218+
}
219+
}
220+
}
221+
}
222+
223+
export const maisakaMonitorClient = new MaisakaMonitorClient()

0 commit comments

Comments
 (0)