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

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 }