1. 重构api请求统一处理接口返回错误,标准化错误抛出格式 2. 新增渠道类型常量与图标映射,清理冗余图标配置 3. 重构凭证状态逻辑,抽离为可复用composable工具 4. 优化WebSocket连接管理,新增断开状态标记防止重连异常 5. 新增渠道运行日志组件,支持实时查看与刷新日志 6. 新增渠道诊断报告功能,一键生成并复制渠道状态报告 7. 优化消息日志面板,支持正则搜索、多格式导出与消息重发 8. 新增统计图表对比功能,支持多渠道数据横向对比 9. 优化仪表盘统计字段映射,修正today_count与total_count字段名 10. 重构渠道启停逻辑,添加异常回滚与错误捕获 11. 新增批量注销渠道功能,优化配置保存后的重启提示 12. 优化轮询机制,新增页面可见性监听与指数退避重试策略
204 lines
6.1 KiB
JavaScript
204 lines
6.1 KiB
JavaScript
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 } |