diff --git a/src/common/WebSocketManager.ts b/src/common/WebSocketManager.ts new file mode 100644 index 0000000..ef4bd9c --- /dev/null +++ b/src/common/WebSocketManager.ts @@ -0,0 +1,307 @@ +/** + * 简化的 WebSocket 管理器(Web 版) + * 专门负责 WebSocket 连接和消息传输,不包含打字机逻辑 + */ + +import { IdUtils, CallbackUtils, MessageUtils, TimerUtils } from './index' + +/* ======================= + * 类型定义 + * ======================= */ + +export interface WebSocketCallbacks { + onConnect?: (event?: Event) => void + onDisconnect?: (event?: CloseEvent) => void + onError?: (error: any) => void + onMessage?: (message: any) => void + getConversationId?: () => string + getAgentId?: () => string +} + +export interface WebSocketManagerOptions extends WebSocketCallbacks { + wsUrl?: string + protocols?: string[] + reconnectInterval?: number + maxReconnectAttempts?: number + heartbeatInterval?: number + messageId?: string + baseDelay?: number + retries?: number + maxDelay?: number + tryReconnect?: boolean +} + +export interface QueuedMessage { + [key: string]: any + retryCount?: number + timestamp?: number +} + +export interface WebSocketStats { + messagesReceived: number + messagesSent: number + messagesDropped: number + reconnectCount: number + connectionStartTime: number | null + lastMessageTime: number | null +} + +/* ======================= + * WebSocketManager + * ======================= */ + +export class WebSocketManager { + private wsUrl = '' + private protocols: string[] = [] + private reconnectInterval = 3000 + private maxReconnectAttempts = 5 + private heartbeatInterval = 30000 + + private ws: WebSocket | null = null + private reconnectAttempts = 0 + private isConnecting = false + private connectionState = false + + private heartbeatTimer: number | null = null + private reconnectTimer: number | null = null + + private callbacks: Required + + private messageQueue: QueuedMessage[] = [] + + private stats: WebSocketStats = { + messagesReceived: 0, + messagesSent: 0, + messagesDropped: 0, + reconnectCount: 0, + connectionStartTime: null, + lastMessageTime: null, + } + + constructor(options: WebSocketManagerOptions = {}) { + this.wsUrl = options.wsUrl ?? '' + this.protocols = options.protocols ?? [] + this.reconnectInterval = options.reconnectInterval ?? 3000 + this.maxReconnectAttempts = options.maxReconnectAttempts ?? 5 + this.heartbeatInterval = options.heartbeatInterval ?? 30000 + + this.callbacks = { + onConnect: options.onConnect ?? (() => { }), + onDisconnect: options.onDisconnect ?? (() => { }), + onError: options.onError ?? (() => { }), + onMessage: options.onMessage ?? (() => { }), + getConversationId: options.getConversationId ?? (() => ''), + getAgentId: options.getAgentId ?? (() => ''), + } + } + + /* ======================= + * 内部工具 + * ======================= */ + + private safeCall(name: keyof WebSocketCallbacks, ...args: any[]): void { + CallbackUtils.safeCall(this.callbacks, name as string, ...args) + } + + /* ======================= + * 连接管理 + * ======================= */ + + async init(wsUrl?: string): Promise { + if (wsUrl) this.wsUrl = wsUrl + if (!this.wsUrl) throw new Error('WebSocket URL is required') + await this.connect() + } + + async connect(): Promise { + if (this.isConnecting || this.connectionState) return + + this.isConnecting = true + + try { + this.ws = new WebSocket(this.wsUrl, this.protocols) + + this.ws.onopen = this.handleOpen + this.ws.onmessage = this.handleMessage + this.ws.onclose = this.handleClose + this.ws.onerror = this.handleError + } catch (error) { + this.isConnecting = false + this.safeCall('onError', error) + this.scheduleReconnect() + } + } + + private handleOpen = (event: Event): void => { + this.isConnecting = false + this.connectionState = true + this.reconnectAttempts = 0 + this.stats.connectionStartTime = Date.now() + + this.startHeartbeat() + this.safeCall('onConnect', event) + this.processQueue() + } + + private handleMessage = (event: MessageEvent): void => { + const raw = event.data + if (MessageUtils.isPongMessage(raw)) return + + const data = + typeof raw === 'string' + ? MessageUtils.safeParseJSON(raw) + : raw + + if (!data) return + + this.stats.messagesReceived++ + this.stats.lastMessageTime = Date.now() + + this.safeCall('onMessage', data) + } + + private handleClose = (event: CloseEvent): void => { + this.connectionState = false + this.isConnecting = false + this.stopHeartbeat() + + this.safeCall('onDisconnect', event) + + if (event.code !== 1000 && this.reconnectAttempts < this.maxReconnectAttempts) { + this.scheduleReconnect() + } + } + + private handleError = (error: Event): void => { + this.connectionState = false + this.isConnecting = false + + this.safeCall('onError', { + type: 'WEBSOCKET_ERROR', + error, + }) + + if (!this.reconnectTimer) { + this.scheduleReconnect() + } + } + + /* ======================= + * 消息发送 + * ======================= */ + + sendMessage(message: QueuedMessage): boolean { + const data = { + ...message, + timestamp: Date.now(), + retryCount: message.retryCount ?? 0, + } + + if (!this.isConnected()) { + this.messageQueue.push(data) + this.connect() + return false + } + + try { + this.ws!.send(JSON.stringify(data)) + this.stats.messagesSent++ + return true + } catch (error) { + this.messageQueue.push(data) + this.safeCall('onError', error) + return false + } + } + + private processQueue(): void { + while (this.messageQueue.length) { + const msg = this.messageQueue.shift()! + if ((msg.retryCount ?? 0) >= 3) { + this.stats.messagesDropped++ + continue + } + msg.retryCount = (msg.retryCount ?? 0) + 1 + this.sendMessage(msg) + } + } + + /* ======================= + * 心跳 & 重连 + * ======================= */ + + private startHeartbeat(): void { + this.stopHeartbeat() + this.heartbeatTimer = window.setInterval(() => { + if (!this.isConnected()) return + + this.sendMessage({ + messageType: '3', + messageContent: 'heartbeat', + messageId: IdUtils.generateMessageId(), + conversationId: this.callbacks.getConversationId(), + agentId: this.callbacks.getAgentId(), + }) + }, this.heartbeatInterval) + } + + private stopHeartbeat(): void { + this.heartbeatTimer = TimerUtils.clearTimer(this.heartbeatTimer, 'interval') + } + + private scheduleReconnect(): void { + if (this.reconnectAttempts >= this.maxReconnectAttempts) return + + this.reconnectAttempts++ + this.stats.reconnectCount++ + + const delay = Math.min( + this.reconnectInterval * Math.pow(1.5, this.reconnectAttempts - 1), + 30000 + ) + + this.reconnectTimer = window.setTimeout(() => { + this.reconnectTimer = null + this.connect() + }, delay) + } + + /* ======================= + * 对外 API + * ======================= */ + + isConnected(): boolean { + return ( + this.connectionState && + !!this.ws && + this.ws.readyState === WebSocket.OPEN + ) + } + + getStats() { + return { + ...this.stats, + queueLength: this.messageQueue.length, + isConnected: this.isConnected(), + } + } + + close(): void { + this.stopHeartbeat() + TimerUtils.clearTimer(this.reconnectTimer) + this.reconnectTimer = null + + this.ws?.close(1000) + this.ws = null + + this.connectionState = false + this.isConnecting = false + this.messageQueue = [] + } + + destroy(): void { + this.close() + } +} + +export default WebSocketManager diff --git a/src/common/index.ts b/src/common/index.ts new file mode 100644 index 0000000..bd63a11 --- /dev/null +++ b/src/common/index.ts @@ -0,0 +1,331 @@ +/** + * 工具函数集合 + * 包含打字机效果、ID生成、回调安全调用等通用工具函数 + */ + +/* ======================= + * ID 生成工具 + * ======================= */ +export class IdUtils { + /** + * 生成消息 ID + */ + static generateMessageId(): string { + const timestamp = Date.now() + const chars = 'abcdefghijklmnopqrstuvwxyz' + const randomStr = Array.from({ length: 4 }, () => + chars.charAt(Math.floor(Math.random() * chars.length)) + ).join('') + return `mid${randomStr}${timestamp}` + } +} + +/* ======================= + * 回调安全调用工具 + * ======================= */ + +export type CallbackMap = Record any> + +export interface BatchCallbackConfig { + name: string + args?: any[] +} + +export class CallbackUtils { + /** + * 安全调用回调函数 + */ + static safeCall( + callbacks: CallbackMap | null | undefined, + callbackName: string, + ...args: any[] + ): void { + const cb = callbacks?.[callbackName] + if (typeof cb === 'function') { + try { + cb(...args) + } catch (error) { + console.error(`回调函数 ${callbackName} 执行出错:`, error) + } + } else { + console.warn(`回调函数 ${callbackName} 不可用`) + } + } + + /** + * 批量安全调用回调函数 + */ + static safeBatchCall( + callbacks: CallbackMap | null | undefined, + callbackConfigs: BatchCallbackConfig[] + ): void { + callbackConfigs.forEach(({ name, args = [] }) => { + this.safeCall(callbacks, name, ...args) + }) + } +} + +/* ======================= + * 消息处理工具 + * ======================= */ + +export interface BaseMessage { + type: string + content?: any + timestamp: number + isComplete?: boolean + [key: string]: any +} + +export class MessageUtils { + /** + * 验证消息格式 + */ + static validateMessage(message: unknown): message is BaseMessage { + return ( + typeof message === 'object' && + message !== null && + 'type' in message + ) + } + + /** + * 格式化消息 + */ + static formatMessage( + type: string, + content: T, + options: Partial = {} + ): BaseMessage { + return { + type, + content, + timestamp: Date.now(), + ...options, + } + } + + /** + * 是否为完整消息 + */ + static isCompleteMessage(message: Partial | null | undefined): boolean { + return message?.isComplete === true + } + + /** + * 是否为心跳 pong + */ + static isPongMessage(messageData: unknown): boolean { + if (typeof messageData === 'string') { + return messageData === 'pong' || messageData.toLowerCase().includes('pong') + } + if (typeof messageData === 'object' && messageData !== null) { + return (messageData as any).type === 'pong' + } + return false + } + + /** + * 安全解析 JSON + */ + static safeParseJSON(messageStr: string): T | null { + try { + return JSON.parse(messageStr) as T + } catch { + console.warn('JSON 解析失败:', messageStr) + return null + } + } + + /** + * 创建打字机消息 + */ + static createTypewriterMessage( + content: string, + isComplete = false, + type = 'typewriter' + ): BaseMessage { + return { + type, + content, + isComplete, + timestamp: Date.now(), + } + } + + /** + * 创建加载消息 + */ + static createLoadingMessage(content = '加载中...'): BaseMessage { + return { + type: 'loading', + content, + timestamp: Date.now(), + } + } + + /** + * 创建错误消息 + */ + static createErrorMessage(error: unknown): BaseMessage { + return { + type: 'error', + content: + error instanceof Error ? error.message : String(error ?? '未知错误'), + timestamp: Date.now(), + } + } +} + +/* ======================= + * 定时器工具 + * ======================= */ + +export type TimerType = 'timeout' | 'interval' + +export interface CancelableTimer { + cancel(): void + isActive(): boolean +} + +export class TimerUtils { + static safeClear( + timerId: number | null, + type: TimerType = 'timeout' + ): null { + if (timerId !== null) { + type === 'interval' ? clearInterval(timerId) : clearTimeout(timerId) + } + return null + } + + static clearTimer( + timerId: number | null, + type: TimerType = 'timeout' + ): null { + return this.safeClear(timerId, type) + } + + static createCancelableTimeout( + callback: () => void, + delay: number + ): CancelableTimer { + let timerId: number | null = window.setTimeout(callback, delay) + return { + cancel() { + if (timerId !== null) { + clearTimeout(timerId) + timerId = null + } + }, + isActive() { + return timerId !== null + }, + } + } + + static createCancelableInterval( + callback: () => void, + interval: number + ): CancelableTimer { + let timerId: number | null = window.setInterval(callback, interval) + return { + cancel() { + if (timerId !== null) { + clearInterval(timerId) + timerId = null + } + }, + isActive() { + return timerId !== null + }, + } + } +} + +/* ======================= + * 防抖工具 + * ======================= */ + +export class DebounceUtils { + static createDebounce void>( + func: T, + delay: number + ): (...args: Parameters) => void { + let timerId: number | null = null + + return function (this: unknown, ...args: Parameters) { + if (timerId !== null) { + clearTimeout(timerId) + } + timerId = window.setTimeout(() => func.apply(this, args), delay) + } + } +} + +/* ======================= + * 节流工具 + * ======================= */ + +export class ThrottleUtils { + static createThrottle void>( + func: T, + delay: number + ): (...args: Parameters) => void { + let prev = Date.now() + + return function (this: unknown, ...args: Parameters) { + const now = Date.now() + if (now - prev >= delay) { + func.apply(this, args) + prev = now + } + } + } +} + +/* ======================= + * 日期工具 + * ======================= */ + +export class DateUtils { + static formatDate( + date: Date = new Date(), + format: string = 'yyyy-MM-dd' + ): string { + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + + return format + .replace('yyyy', String(year)) + .replace('MM', month) + .replace('dd', day) + } +} + +/* ======================= + * 手机号校验 + * ======================= */ + +export class PhoneUtils { + static validatePhone(phone: string): boolean { + const phoneRegex = /^1[3-9]\d{9}$/ + return phoneRegex.test(phone) + } +} + +/* ======================= + * 默认导出 + * ======================= */ + +export default { + IdUtils, + CallbackUtils, + MessageUtils, + TimerUtils, + DateUtils, + DebounceUtils, + ThrottleUtils, + PhoneUtils, +} \ No newline at end of file diff --git a/src/renderer/views/home/ChatBox.vue b/src/renderer/views/home/ChatBox.vue index c260540..5a8d122 100644 --- a/src/renderer/views/home/ChatBox.vue +++ b/src/renderer/views/home/ChatBox.vue @@ -4,32 +4,36 @@
-
- +
+ - +
-
+
ZHINIAN 20:30
-
- {{ msg.content }} +
+ {{ msg.msg }}
-
+
本回答由 AI 生成
-
+
- + @@ -39,7 +43,8 @@
- +
@@ -54,13 +59,15 @@
-