import { apiGet, apiPut, apiAdminPost, apiAdminDelete } from './base' import { useUserStore } from '@/stores/user' class ChannelWSClient { constructor({ url, token, maxReconnectAttempts = 10, heartbeatInterval = 30000 }) { this.url = url this.token = token this.ws = null this.reconnectAttempts = 0 this.maxReconnectAttempts = maxReconnectAttempts this.heartbeatInterval = heartbeatInterval this.heartbeatTimer = null this.pendingQueue = [] this._disconnecting = false this.eventHandlers = { onChunk: null, onDone: null, onError: null, onTyping: null, onStateChange: null, onChannelStatus: null } } connect() { this._disconnecting = false return new Promise((resolve, reject) => { this.ws = new WebSocket(`${this.url}?token=${this.token}`) this.ws.onopen = () => { this.reconnectAttempts = 0 this._startHeartbeat() this._flushPendingQueue() this._emitStateChange('connected') resolve() } this.ws.onmessage = (event) => { const msg = JSON.parse(event.data) this._dispatch(msg) } this.ws.onclose = (event) => { this._stopHeartbeat() this._emitStateChange('disconnected') if (this._disconnecting) return if (!event.wasClean && this.reconnectAttempts < this.maxReconnectAttempts) { this._scheduleReconnect() } } this.ws.onerror = () => { this._emitStateChange('error') reject(new Error('WebSocket connection failed')) } }) } send(content, { agentConfigId, threadId } = {}) { const requestId = crypto.randomUUID() const message = { type: 'message', payload: { request_id: requestId, content, agent_config_id: agentConfigId, thread_id: threadId }, timestamp: new Date().toISOString() } if (this.ws?.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify(message)) } else { this.pendingQueue.push(JSON.stringify(message)) } return requestId } disconnect() { this._disconnecting = true this.maxReconnectAttempts = 0 this._stopHeartbeat() this.ws?.close(1000, 'Client disconnect') } _dispatch(msg) { switch (msg.type) { case 'chunk': this.eventHandlers.onChunk?.(msg.payload) break case 'done': this.eventHandlers.onDone?.(msg.payload) break case 'error': this.eventHandlers.onError?.(msg.payload) break case 'typing': this.eventHandlers.onTyping?.(msg.payload) break case 'channel_status': this.eventHandlers.onChannelStatus?.(msg.payload) break case 'pong': break } } _scheduleReconnect() { const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000) this.reconnectAttempts++ setTimeout(() => this.connect().catch(() => {}), delay) } _startHeartbeat() { this.heartbeatTimer = setInterval(() => { if (this.ws?.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify({ type: 'ping', payload: {} })) } }, this.heartbeatInterval) } _stopHeartbeat() { clearInterval(this.heartbeatTimer) } _flushPendingQueue() { while (this.pendingQueue.length > 0 && this.ws?.readyState === WebSocket.OPEN) { this.ws.send(this.pendingQueue.shift()) } } _emitStateChange(state) { this.eventHandlers.onStateChange?.(state) } } export const channelApi = { getStatusList: () => apiGet('/api/channels/status'), getChannelDetail: (channelId) => apiGet(`/api/channels/${channelId}/status`), startChannel: (channelId) => apiAdminPost(`/api/channels/${channelId}/start`), stopChannel: (channelId) => apiAdminPost(`/api/channels/${channelId}/stop`), restartChannel: (channelId) => apiAdminPost(`/api/channels/${channelId}/restart`), testChannel: (channelId) => apiAdminPost(`/api/channels/${channelId}/test`), updateConfig: (channelId, config) => apiPut(`/api/channels/${channelId}/config`, config), getConfigSchema: (channelId) => apiGet(`/api/channels/${channelId}/config-schema`), getRouting: (channelId) => apiGet(`/api/channels/${channelId}/routing`), updateRouting: (channelId, routing) => apiPut(`/api/channels/${channelId}/routing`, routing), getPolicy: (channelId) => apiGet(`/api/channels/${channelId}/policy`), updatePolicy: (channelId, policy) => apiPut(`/api/channels/${channelId}/policy`, policy), registerChannel: (channelType, config = {}) => apiAdminPost('/api/channels/register', { channel_type: channelType, config }), unregisterChannel: (channelId) => apiAdminDelete(`/api/channels/${channelId}`), getAllChannelActions: (action) => { const query = action ? `?action=${action}` : '' return apiGet(`/api/channels/actions${query}`) }, getChannelActions: (channelId) => apiGet(`/api/channels/${channelId}/actions`), getStats: (channelId, params = {}) => { const query = new URLSearchParams(params).toString() return apiGet(`/api/channels/${channelId}/stats?${query}`) }, getMessageLog: (channelId, params = {}) => { const query = new URLSearchParams(params).toString() return apiGet(`/api/channels/${channelId}/messages?${query}`) }, exportMessageLog: (channelId, params = {}) => { const query = new URLSearchParams(params).toString() const userStore = useUserStore() return fetch(`/api/channels/${channelId}/messages/export?${query}`, { headers: { ...userStore.getAuthHeaders() } }) }, getUserMapping: (channelId, userId) => apiGet(`/api/channels/${channelId}/users/${userId}`), getChatMapping: (channelId, chatId, internalUserId) => { const params = internalUserId ? { internal_user_id: internalUserId } : {} return apiGet(`/api/channels/${channelId}/chats/${chatId}`, { params }) }, getCredentialStatus: (channelId) => apiGet(`/api/channels/${channelId}/credential-status`), refreshCredential: (channelId) => apiAdminPost(`/api/channels/${channelId}/refresh-credential`), getDashboardChannelStats: () => apiGet('/api/channels/status') } export { ChannelWSClient }