update
This commit is contained in:
@@ -1,215 +1,223 @@
|
||||
import { ref, shallowRef, onUnmounted, computed, type Ref, readonly } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
// 从类型文件导入 WebSocket 相关类型
|
||||
import { ref, shallowRef, computed, readonly } from 'vue';
|
||||
import type { ConnectionStatus, MessagePayload, WebSocketMessage, MessageHandler } from '../types/websocket.types';
|
||||
|
||||
// --- 类型定义 (已移至 websocket.types.ts) ---
|
||||
// export type ConnectionStatus = ...;
|
||||
// export type MessagePayload = ...;
|
||||
// export interface WebSocketMessage { ... }
|
||||
// export type MessageHandler = ...;
|
||||
/**
|
||||
* 创建并管理单个 WebSocket 连接实例。
|
||||
* 每个实例对应一个会话 (Session)。
|
||||
*
|
||||
* @param {string} sessionId - 此 WebSocket 连接关联的会话 ID (用于日志记录)。
|
||||
* @param {string} dbConnectionId - 此 WebSocket 连接关联的数据库连接 ID (用于后端识别)。
|
||||
* @param {Function} t - i18n 翻译函数,从父组件传入
|
||||
* @returns 一个包含状态和方法的 WebSocket 连接管理器对象。
|
||||
*/
|
||||
export function createWebSocketConnectionManager(sessionId: string, dbConnectionId: string, t: Function) {
|
||||
// --- Instance State ---
|
||||
// 每个实例拥有独立的 WebSocket 对象、状态和消息处理器
|
||||
const ws = shallowRef<WebSocket | null>(null); // WebSocket 实例
|
||||
const connectionStatus = ref<ConnectionStatus>('disconnected'); // 连接状态
|
||||
const statusMessage = ref<string>(''); // 状态描述文本
|
||||
const isSftpReady = ref<boolean>(false); // SFTP 是否就绪
|
||||
const messageHandlers = new Map<string, Set<MessageHandler>>(); // 此实例的消息处理器注册表
|
||||
const instanceSessionId = sessionId; // 保存会话 ID 用于日志
|
||||
const instanceDbConnectionId = dbConnectionId; // 保存数据库连接 ID
|
||||
// --- End Instance State ---
|
||||
|
||||
// --- Singleton State within the module scope ---
|
||||
// This ensures only one WebSocket connection and state is managed across the app.
|
||||
const ws = shallowRef<WebSocket | null>(null); // Use shallowRef for the WebSocket object itself
|
||||
const connectionStatus = ref<ConnectionStatus>('disconnected');
|
||||
const statusMessage = ref<string>('');
|
||||
const connectionIdForSession = ref<string | null>(null); // Store the connectionId used for the current session
|
||||
const isSftpReady = ref<boolean>(false); // Track SFTP readiness
|
||||
|
||||
// Registry for message handlers
|
||||
const messageHandlers = new Map<string, Set<MessageHandler>>();
|
||||
// --- End Singleton State ---
|
||||
|
||||
|
||||
export function useWebSocketConnection() {
|
||||
const { t } = useI18n(); // Get t function for status messages
|
||||
|
||||
// Helper to get status text safely
|
||||
/**
|
||||
* 安全地获取状态文本的辅助函数
|
||||
* @param {string} statusKey - i18n 键名 (例如 'connectingWs')
|
||||
* @param {Record<string, unknown>} [params] - i18n 插值参数
|
||||
* @returns {string} 翻译后的文本或键名本身 (如果翻译失败)
|
||||
*/
|
||||
const getStatusText = (statusKey: string, params?: Record<string, unknown>): string => {
|
||||
try {
|
||||
// Use a fallback key or message if translation is missing
|
||||
const translated = t(`workspace.status.${statusKey}`, params || {});
|
||||
// Check if the key itself was returned (indicating missing translation)
|
||||
return translated === `workspace.status.${statusKey}` ? statusKey : translated;
|
||||
} catch (e) {
|
||||
console.warn(`[i18n] Error getting translation for workspace.status.${statusKey}:`, e);
|
||||
return statusKey; // Fallback to the key itself
|
||||
console.warn(`[WebSocket ${instanceSessionId}] i18n 错误 (键: workspace.status.${statusKey}):`, e);
|
||||
return statusKey;
|
||||
}
|
||||
};
|
||||
|
||||
// Function to dispatch a message to all registered handlers for its type
|
||||
/**
|
||||
* 将收到的消息分发给已注册的处理器
|
||||
* @param {string} type - 消息类型
|
||||
* @param {MessagePayload} payload - 消息负载
|
||||
* @param {WebSocketMessage} fullMessage - 完整的消息对象
|
||||
*/
|
||||
const dispatchMessage = (type: string, payload: MessagePayload, fullMessage: WebSocketMessage) => {
|
||||
if (messageHandlers.has(type)) {
|
||||
messageHandlers.get(type)?.forEach(handler => {
|
||||
try {
|
||||
handler(payload, fullMessage);
|
||||
} catch (e) {
|
||||
console.error(`[WebSocket] Error in message handler for type "${type}":`, e);
|
||||
console.error(`[WebSocket ${instanceSessionId}] 消息处理器错误 (类型: "${type}"):`, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const connect = (url: string, connId: string) => {
|
||||
// Prevent multiple connections or connection attempts
|
||||
/**
|
||||
* 建立 WebSocket 连接
|
||||
* @param {string} url - WebSocket 服务器 URL
|
||||
*/
|
||||
const connect = (url: string) => {
|
||||
// 防止重复连接同一实例
|
||||
if (ws.value && (ws.value.readyState === WebSocket.OPEN || ws.value.readyState === WebSocket.CONNECTING)) {
|
||||
// If it's the same connection ID and already open/connecting, do nothing
|
||||
if (connectionIdForSession.value === connId) {
|
||||
console.warn(`[WebSocket] Connection for ${connId} already open or connecting.`);
|
||||
return;
|
||||
}
|
||||
// If different connection ID, close the old one first
|
||||
console.log(`[WebSocket] Closing existing connection for ${connectionIdForSession.value} before connecting to ${connId}`);
|
||||
disconnect(); // Ensure cleanup before new connection
|
||||
console.warn(`[WebSocket ${instanceSessionId}] 连接已打开或正在连接中。`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[WebSocket] Attempting to connect to: ${url} for connection ${connId}`);
|
||||
connectionIdForSession.value = connId;
|
||||
console.log(`[WebSocket ${instanceSessionId}] 尝试连接到: ${url} (DB Conn ID: ${instanceDbConnectionId})`);
|
||||
statusMessage.value = getStatusText('connectingWs', { url });
|
||||
connectionStatus.value = 'connecting';
|
||||
isSftpReady.value = false; // 重置 SFTP 状态
|
||||
|
||||
try {
|
||||
ws.value = new WebSocket(url);
|
||||
|
||||
ws.value.onopen = () => {
|
||||
console.log('[WebSocket] Connection opened.');
|
||||
console.log(`[WebSocket ${instanceSessionId}] 连接已打开。`);
|
||||
statusMessage.value = getStatusText('wsConnected');
|
||||
// Status remains 'connecting' until ssh:connected is received
|
||||
// Send the initial connection message required by the backend
|
||||
sendMessage({ type: 'ssh:connect', payload: { connectionId: connId } });
|
||||
// Dispatch an internal event if needed
|
||||
// dispatchMessage('internal:opened', {}, { type: 'internal:opened' });
|
||||
// 状态保持 'connecting' 直到收到 ssh:connected
|
||||
// 发送后端所需的初始连接消息,包含数据库连接 ID
|
||||
sendMessage({ type: 'ssh:connect', payload: { connectionId: instanceDbConnectionId } });
|
||||
dispatchMessage('internal:opened', {}, { type: 'internal:opened' }); // 触发内部打开事件
|
||||
};
|
||||
|
||||
ws.value.onmessage = (event: MessageEvent) => {
|
||||
try {
|
||||
const message: WebSocketMessage = JSON.parse(event.data);
|
||||
// console.debug('[WebSocket] Received:', message.type); // Less verbose logging
|
||||
// console.debug(`[WebSocket ${instanceSessionId}] 收到:`, message.type);
|
||||
|
||||
// --- Update Global Connection Status based on specific messages ---
|
||||
// --- 更新此实例的连接状态 ---
|
||||
if (message.type === 'ssh:connected') {
|
||||
if (connectionStatus.value !== 'connected') {
|
||||
console.log('[WebSocket] SSH session connected.');
|
||||
console.log(`[WebSocket ${instanceSessionId}] SSH 会话已连接。`);
|
||||
connectionStatus.value = 'connected';
|
||||
statusMessage.value = getStatusText('connected');
|
||||
}
|
||||
} else if (message.type === 'ssh:disconnected') {
|
||||
if (connectionStatus.value !== 'disconnected') {
|
||||
console.log('[WebSocket] SSH session disconnected.');
|
||||
if (connectionStatus.value !== 'disconnected') {
|
||||
console.log(`[WebSocket ${instanceSessionId}] SSH 会话已断开。`);
|
||||
connectionStatus.value = 'disconnected';
|
||||
statusMessage.value = getStatusText('disconnected', { reason: message.payload || 'Unknown reason' });
|
||||
}
|
||||
} else if (message.type === 'ssh:error' || message.type === 'error') { // Handle generic backend errors too
|
||||
statusMessage.value = getStatusText('disconnected', { reason: message.payload || '未知原因' });
|
||||
isSftpReady.value = false; // SSH 断开,SFTP 也应不可用
|
||||
}
|
||||
} else if (message.type === 'ssh:error' || message.type === 'error') {
|
||||
if (connectionStatus.value !== 'disconnected' && connectionStatus.value !== 'error') {
|
||||
console.error('[WebSocket] Received error message:', message.payload);
|
||||
console.error(`[WebSocket ${instanceSessionId}] 收到错误消息:`, message.payload);
|
||||
connectionStatus.value = 'error';
|
||||
let errorMsg = message.payload || 'Unknown error';
|
||||
let errorMsg = message.payload || '未知错误';
|
||||
if (typeof errorMsg === 'object' && errorMsg.message) errorMsg = errorMsg.message;
|
||||
statusMessage.value = getStatusText('error', { message: errorMsg });
|
||||
isSftpReady.value = false; // Reset SFTP status on error
|
||||
isSftpReady.value = false;
|
||||
}
|
||||
} else if (message.type === 'sftp_ready') {
|
||||
console.log('[WebSocket] SFTP session ready.');
|
||||
console.log(`[WebSocket ${instanceSessionId}] SFTP 会话已就绪。`);
|
||||
isSftpReady.value = true;
|
||||
}
|
||||
// --- End Status Update ---
|
||||
// --- 状态更新结束 ---
|
||||
|
||||
// Dispatch message to specific handlers
|
||||
// 分发消息给此实例的处理器
|
||||
dispatchMessage(message.type, message.payload, message);
|
||||
|
||||
} catch (e) {
|
||||
console.error('[WebSocket] Error processing message:', e, 'Raw data:', event.data);
|
||||
// Optionally dispatch raw data if needed by some handler
|
||||
// dispatchMessage('internal:raw', event.data, { type: 'internal:raw' });
|
||||
console.error(`[WebSocket ${instanceSessionId}] 处理消息时出错:`, e, '原始数据:', event.data);
|
||||
dispatchMessage('internal:raw', event.data, { type: 'internal:raw' });
|
||||
}
|
||||
};
|
||||
|
||||
ws.value.onerror = (event) => {
|
||||
console.error('[WebSocket] Connection error:', event);
|
||||
if (connectionStatus.value !== 'disconnected') { // Avoid overwriting disconnect status
|
||||
console.error(`[WebSocket ${instanceSessionId}] 连接错误:`, event);
|
||||
if (connectionStatus.value !== 'disconnected') {
|
||||
connectionStatus.value = 'error';
|
||||
statusMessage.value = getStatusText('wsError');
|
||||
}
|
||||
dispatchMessage('internal:error', event, { type: 'internal:error' });
|
||||
isSftpReady.value = false; // Reset SFTP status on WS error
|
||||
ws.value = null; // Clean up on error
|
||||
connectionIdForSession.value = null;
|
||||
isSftpReady.value = false;
|
||||
ws.value = null; // 清理实例
|
||||
};
|
||||
|
||||
ws.value.onclose = (event) => {
|
||||
console.log(`[WebSocket] Connection closed: Code=${event.code}, Reason=${event.reason}`);
|
||||
// Update status only if not already handled by ssh:disconnected or error
|
||||
console.log(`[WebSocket ${instanceSessionId}] 连接已关闭: Code=${event.code}, Reason=${event.reason}`);
|
||||
if (connectionStatus.value !== 'disconnected' && connectionStatus.value !== 'error') {
|
||||
connectionStatus.value = 'disconnected';
|
||||
statusMessage.value = getStatusText('wsClosed', { code: event.code });
|
||||
}
|
||||
dispatchMessage('internal:closed', { code: event.code, reason: event.reason }, { type: 'internal:closed' });
|
||||
isSftpReady.value = false; // Reset SFTP status on close
|
||||
ws.value = null; // Clean up reference
|
||||
connectionIdForSession.value = null;
|
||||
// Optionally clear handlers on close? Depends on desired behavior.
|
||||
// messageHandlers.clear();
|
||||
isSftpReady.value = false;
|
||||
ws.value = null; // 清理实例引用
|
||||
// 不自动清除处理器,以便在重连时可能复用
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('[WebSocket] Failed to create WebSocket instance:', err);
|
||||
console.error(`[WebSocket ${instanceSessionId}] 创建 WebSocket 实例失败:`, err);
|
||||
connectionStatus.value = 'error';
|
||||
statusMessage.value = getStatusText('wsError'); // Or a more specific creation error
|
||||
isSftpReady.value = false; // Reset SFTP status on creation error
|
||||
statusMessage.value = getStatusText('wsError');
|
||||
isSftpReady.value = false;
|
||||
ws.value = null;
|
||||
connectionIdForSession.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 手动断开此 WebSocket 连接
|
||||
*/
|
||||
const disconnect = () => {
|
||||
if (ws.value) {
|
||||
console.log('[WebSocket] Closing connection manually...');
|
||||
// Set status immediately to prevent race conditions with onclose
|
||||
console.log(`[WebSocket ${instanceSessionId}] 手动关闭连接...`);
|
||||
if (connectionStatus.value !== 'disconnected') {
|
||||
connectionStatus.value = 'disconnected';
|
||||
statusMessage.value = getStatusText('disconnected', { reason: 'Manual disconnect' });
|
||||
statusMessage.value = getStatusText('disconnected', { reason: '手动断开' });
|
||||
}
|
||||
ws.value.close(1000, 'Client initiated disconnect'); // Use standard code and reason
|
||||
ws.value.close(1000, '客户端主动断开'); // 使用标准代码和原因
|
||||
ws.value = null;
|
||||
connectionIdForSession.value = null;
|
||||
isSftpReady.value = false; // Reset SFTP status on manual disconnect
|
||||
// messageHandlers.clear(); // Clear handlers on manual disconnect
|
||||
isSftpReady.value = false;
|
||||
// 手动断开时可以考虑清除处理器,取决于是否需要重连逻辑
|
||||
// messageHandlers.clear();
|
||||
} else {
|
||||
console.log(`[WebSocket ${instanceSessionId}] 连接已关闭或不存在,无需断开。`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 发送 WebSocket 消息
|
||||
* @param {WebSocketMessage} message - 要发送的消息对象
|
||||
*/
|
||||
const sendMessage = (message: WebSocketMessage) => {
|
||||
if (ws.value && ws.value.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
const messageString = JSON.stringify(message);
|
||||
// console.debug('[WebSocket] Sending:', message.type); // Less verbose
|
||||
// console.debug(`[WebSocket ${instanceSessionId}] 发送:`, message.type);
|
||||
ws.value.send(messageString);
|
||||
} catch (e) {
|
||||
console.error('[WebSocket] Failed to stringify or send message:', e, message);
|
||||
console.error(`[WebSocket ${instanceSessionId}] 序列化或发送消息失败:`, e, message);
|
||||
}
|
||||
} else {
|
||||
console.warn(`[WebSocket] Cannot send message, connection not open. State: ${connectionStatus.value}, ReadyState: ${ws.value?.readyState}`);
|
||||
console.warn(`[WebSocket ${instanceSessionId}] 无法发送消息,连接未打开。状态: ${connectionStatus.value}, ReadyState: ${ws.value?.readyState}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Register a handler for a specific message type
|
||||
const onMessage = (type: string, handler: MessageHandler) => {
|
||||
/**
|
||||
* 注册一个消息处理器
|
||||
* @param {string} type - 要监听的消息类型
|
||||
* @param {MessageHandler} handler - 处理函数
|
||||
* @returns {Function} 一个用于注销此处理器的函数
|
||||
*/
|
||||
const onMessage = (type: string, handler: MessageHandler): (() => void) => {
|
||||
if (!messageHandlers.has(type)) {
|
||||
messageHandlers.set(type, new Set());
|
||||
}
|
||||
const handlersSet = messageHandlers.get(type);
|
||||
if (handlersSet) {
|
||||
handlersSet.add(handler);
|
||||
console.debug(`[WebSocket] Handler registered for type: ${type}`);
|
||||
// console.debug(`[WebSocket ${instanceSessionId}] 已注册处理器: ${type}`);
|
||||
}
|
||||
|
||||
|
||||
// Return an unregister function
|
||||
// 返回注销函数
|
||||
return () => {
|
||||
const currentSet = messageHandlers.get(type);
|
||||
if (currentSet) {
|
||||
currentSet.delete(handler);
|
||||
console.debug(`[WebSocket] Handler unregistered for type: ${type}`);
|
||||
// console.debug(`[WebSocket ${instanceSessionId}] 已注销处理器: ${type}`);
|
||||
if (currentSet.size === 0) {
|
||||
messageHandlers.delete(type);
|
||||
}
|
||||
@@ -217,20 +225,18 @@ export function useWebSocketConnection() {
|
||||
};
|
||||
};
|
||||
|
||||
// Cleanup logic: The singleton nature means disconnect should be called explicitly
|
||||
// when the connection is no longer needed (e.g., when WorkspaceView unmounts).
|
||||
// onUnmounted is generally tied to the component instance using the composable.
|
||||
// If useWebSocketConnection is called in WorkspaceView's setup, its onUnmounted
|
||||
// will trigger disconnect, which is the desired behavior.
|
||||
// 注意:没有在此处使用 onUnmounted。
|
||||
// disconnect 方法需要由外部调用者 (例如 WorkspaceView) 在会话关闭时显式调用。
|
||||
|
||||
// 返回此实例的状态和方法
|
||||
return {
|
||||
// State (Exported as readonly refs where appropriate)
|
||||
// 状态 (只读引用)
|
||||
isConnected: computed(() => connectionStatus.value === 'connected'),
|
||||
isSftpReady: readonly(isSftpReady), // Expose SFTP readiness state
|
||||
isSftpReady: readonly(isSftpReady),
|
||||
connectionStatus: readonly(connectionStatus),
|
||||
statusMessage: readonly(statusMessage),
|
||||
|
||||
// Methods
|
||||
// 方法
|
||||
connect,
|
||||
disconnect,
|
||||
sendMessage,
|
||||
|
||||
Reference in New Issue
Block a user