ForcePilot/web/src/apis/channel_api.js

204 lines
6.1 KiB
JavaScript
Raw Normal View History

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 }