@@ -86,10 +86,11 @@ const {
|
|||||||
startFileUpload,
|
startFileUpload,
|
||||||
cancelUpload,
|
cancelUpload,
|
||||||
} = useFileUploader(
|
} = useFileUploader(
|
||||||
|
computed(() => props.sessionId),
|
||||||
// 传递 manager 的 currentPath 和 fileList ref
|
// 传递 manager 的 currentPath 和 fileList ref
|
||||||
computed(() => currentSftpManager.value?.currentPath.value ?? '/'), // 提供默认值
|
computed(() => currentSftpManager.value?.currentPath.value ?? '/'),
|
||||||
computed(() => currentSftpManager.value?.fileList.value ?? []), // 提供默认值
|
computed(() => currentSftpManager.value?.fileList.value ?? []),
|
||||||
props.wsDeps
|
computed(() => props.wsDeps)
|
||||||
);
|
);
|
||||||
|
|
||||||
// 实例化其他 Stores
|
// 实例化其他 Stores
|
||||||
@@ -732,7 +733,7 @@ const {
|
|||||||
handleDragLeaveRow,
|
handleDragLeaveRow,
|
||||||
handleDropOnRow,
|
handleDropOnRow,
|
||||||
} = useFileManagerDragAndDrop({
|
} = useFileManagerDragAndDrop({
|
||||||
isConnected: props.wsDeps.isConnected,
|
isConnected: computed(() => props.wsDeps.isConnected.value),
|
||||||
// 修改:传递 manager 的 currentPath (保持 computed)
|
// 修改:传递 manager 的 currentPath (保持 computed)
|
||||||
currentPath: computed(() => currentSftpManager.value?.currentPath.value ?? '/'),
|
currentPath: computed(() => currentSftpManager.value?.currentPath.value ?? '/'),
|
||||||
fileListContainerRef: fileListContainerRef,
|
fileListContainerRef: fileListContainerRef,
|
||||||
@@ -807,7 +808,6 @@ const saveLayoutSettings = () => {
|
|||||||
|
|
||||||
// --- 生命周期钩子 ---
|
// --- 生命周期钩子 ---
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Component mounted.`);
|
|
||||||
// --- 移除 onMounted 中的加载逻辑 ---
|
// --- 移除 onMounted 中的加载逻辑 ---
|
||||||
// Initial load logic is handled by watchEffect below and the main sftp loading watchEffect
|
// Initial load logic is handled by watchEffect below and the main sftp loading watchEffect
|
||||||
});
|
});
|
||||||
@@ -960,7 +960,6 @@ watch(() => focusSwitcherStore.activateFileManagerSearchTrigger, (newValue, oldV
|
|||||||
// --- 监听 sessionId prop 的变化 ---
|
// --- 监听 sessionId prop 的变化 ---
|
||||||
watch(() => props.sessionId, (newSessionId, oldSessionId) => {
|
watch(() => props.sessionId, (newSessionId, oldSessionId) => {
|
||||||
if (newSessionId && newSessionId !== oldSessionId) {
|
if (newSessionId && newSessionId !== oldSessionId) {
|
||||||
console.log(`[FileManager ${newSessionId}-${props.instanceId}] Session ID changed from ${oldSessionId} to ${newSessionId}. Re-initializing.`);
|
|
||||||
|
|
||||||
// 1. 重新初始化 SFTP 管理器
|
// 1. 重新初始化 SFTP 管理器
|
||||||
initializeSftpManager(newSessionId, props.instanceId);
|
initializeSftpManager(newSessionId, props.instanceId);
|
||||||
|
|||||||
@@ -194,7 +194,6 @@ export function useFileManagerDragAndDrop(options: UseFileManagerDragAndDropOpti
|
|||||||
// 处理蒙版上的 Drop 事件
|
// 处理蒙版上的 Drop 事件
|
||||||
const handleOverlayDrop = (event: DragEvent) => {
|
const handleOverlayDrop = (event: DragEvent) => {
|
||||||
event.preventDefault(); // 必须阻止,以防浏览器打开文件
|
event.preventDefault(); // 必须阻止,以防浏览器打开文件
|
||||||
// console.log("[DragDrop] Drop event on overlay.");
|
|
||||||
showExternalDropOverlay.value = false; // 隐藏蒙版
|
showExternalDropOverlay.value = false; // 隐藏蒙版
|
||||||
stopAutoScroll(); // 停止滚动
|
stopAutoScroll(); // 停止滚动
|
||||||
|
|
||||||
@@ -210,7 +209,6 @@ export function useFileManagerDragAndDrop(options: UseFileManagerDragAndDropOpti
|
|||||||
if (item.kind === 'file') {
|
if (item.kind === 'file') {
|
||||||
const entry = item.webkitGetAsEntry();
|
const entry = item.webkitGetAsEntry();
|
||||||
if (entry) {
|
if (entry) {
|
||||||
// console.log(`[DragDrop] Processing entry from overlay: ${entry.name}`);
|
|
||||||
traverseFileTree(entry); // 处理文件/文件夹
|
traverseFileTree(entry); // 处理文件/文件夹
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[DragDrop] Could not get entry for item ${i} from overlay.`);
|
console.warn(`[DragDrop] Could not get entry for item ${i} from overlay.`);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ref, reactive, nextTick, onUnmounted, readonly, type Ref } from 'vue';
|
import { ref, reactive, nextTick, onUnmounted, readonly, type Ref, watchEffect } from 'vue';
|
||||||
import { createWebSocketConnectionManager } from './useWebSocketConnection';
|
import { createWebSocketConnectionManager } from './useWebSocketConnection';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import type { FileListItem } from '../types/sftp.types';
|
import type { FileListItem } from '../types/sftp.types';
|
||||||
@@ -21,12 +21,13 @@ const joinPath = (base: string, name: string): string => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function useFileUploader(
|
export function useFileUploader(
|
||||||
|
sessionIdForLog: Ref<string>,
|
||||||
currentPathRef: Ref<string>,
|
currentPathRef: Ref<string>,
|
||||||
fileListRef: Readonly<Ref<readonly FileListItem[]>>, // 使用 Readonly 类型
|
fileListRef: Readonly<Ref<readonly FileListItem[]>>, // 使用 Readonly 类型
|
||||||
wsDeps: WebSocketDependencies // 注入 WebSocket 依赖项
|
wsDeps: Ref<WebSocketDependencies>
|
||||||
) {
|
) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { sendMessage, onMessage, isConnected } = wsDeps; // 使用注入的依赖项
|
wsDeps;
|
||||||
|
|
||||||
// 对 uploads 字典使用 reactive 以获得更好的深度响应性
|
// 对 uploads 字典使用 reactive 以获得更好的深度响应性
|
||||||
const uploads = reactive<Record<string, UploadItem>>({});
|
const uploads = reactive<Record<string, UploadItem>>({});
|
||||||
@@ -36,8 +37,8 @@ export function useFileUploader(
|
|||||||
const sendFileChunks = (uploadId: string, file: File, startByte = 0) => {
|
const sendFileChunks = (uploadId: string, file: File, startByte = 0) => {
|
||||||
const upload = uploads[uploadId];
|
const upload = uploads[uploadId];
|
||||||
// 在继续之前检查连接和上传状态
|
// 在继续之前检查连接和上传状态
|
||||||
if (!isConnected.value || !upload || upload.status !== 'uploading') {
|
if (!wsDeps.value.isConnected.value || !upload || upload.status !== 'uploading') {
|
||||||
console.warn(`[文件上传模块] 无法为 ${uploadId} 发送块。连接状态: ${isConnected.value}, 上传状态: ${upload?.status}`);
|
console.warn(`[FileUploader ${sessionIdForLog.value}] Cannot send chunk for ${uploadId}. Connection: ${wsDeps.value.isConnected.value}, Upload status: ${upload?.status}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,8 +51,8 @@ export function useFileUploader(
|
|||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
const currentUpload = uploads[uploadId];
|
const currentUpload = uploads[uploadId];
|
||||||
// *发送前* 再次检查连接和状态
|
// *发送前* 再次检查连接和状态
|
||||||
if (!isConnected.value || !currentUpload || currentUpload.status !== 'uploading') {
|
if (!wsDeps.value.isConnected.value || !currentUpload || currentUpload.status !== 'uploading') {
|
||||||
console.warn(`[文件上传模块] 上传 ${uploadId} 在发送偏移量 ${offset} 的块之前状态已更改或连接已断开。`);
|
console.warn(`[FileUploader ${sessionIdForLog.value}] Upload ${uploadId} status changed or disconnected before sending chunk at offset ${offset}.`);
|
||||||
return; // 如果状态改变或断开连接,则停止发送
|
return; // 如果状态改变或断开连接,则停止发送
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +62,7 @@ export function useFileUploader(
|
|||||||
const chunkBase64 = chunkResult.split(',')[1];
|
const chunkBase64 = chunkResult.split(',')[1];
|
||||||
const isLast = offset + chunkSize >= file.size;
|
const isLast = offset + chunkSize >= file.size;
|
||||||
|
|
||||||
sendMessage({
|
wsDeps.value.sendMessage({
|
||||||
type: 'sftp:upload:chunk',
|
type: 'sftp:upload:chunk',
|
||||||
payload: { uploadId, chunkIndex: chunkIndex++, data: chunkBase64, isLast }
|
payload: { uploadId, chunkIndex: chunkIndex++, data: chunkBase64, isLast }
|
||||||
});
|
});
|
||||||
@@ -71,23 +72,20 @@ export function useFileUploader(
|
|||||||
|
|
||||||
|
|
||||||
if (!isLast) {
|
if (!isLast) {
|
||||||
|
|
||||||
|
|
||||||
nextTick(readNextChunk);
|
nextTick(readNextChunk);
|
||||||
} else {
|
} else {
|
||||||
console.log(`[文件上传模块] 已发送 ${uploadId} 的最后一个块`);
|
console.log(`[FileUploader ${sessionIdForLog.value}] Sent last chunk for ${uploadId}`);
|
||||||
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error(`[文件上传模块] FileReader 为 ${uploadId} 返回了意外结果:`, chunkResult);
|
console.error(`[FileUploader ${sessionIdForLog.value}] FileReader returned unexpected result for ${uploadId}:`, chunkResult);
|
||||||
|
|
||||||
currentUpload.status = 'error';
|
currentUpload.status = 'error';
|
||||||
currentUpload.error = t('fileManager.errors.readFileError');
|
currentUpload.error = t('fileManager.errors.readFileError');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
reader.onerror = () => {
|
reader.onerror = () => {
|
||||||
console.error(`[文件上传模块] FileReader 错误,上传 ID: ${uploadId}`);
|
console.error(`[FileUploader ${sessionIdForLog.value}] FileReader error for upload ID: ${uploadId}`);
|
||||||
const failedUpload = uploads[uploadId];
|
const failedUpload = uploads[uploadId];
|
||||||
if (failedUpload) {
|
if (failedUpload) {
|
||||||
failedUpload.status = 'error';
|
failedUpload.status = 'error';
|
||||||
@@ -109,9 +107,9 @@ export function useFileUploader(
|
|||||||
readNextChunk();
|
readNextChunk();
|
||||||
} else {
|
} else {
|
||||||
// 立即处理零字节文件
|
// 立即处理零字节文件
|
||||||
console.log(`[文件上传模块] 处理零字节文件 ${uploadId}`);
|
console.log(`[FileUploader ${sessionIdForLog.value}] Processing zero-byte file ${uploadId}`);
|
||||||
// Send chunkIndex 0 for zero-byte file
|
// Send chunkIndex 0 for zero-byte file
|
||||||
sendMessage({ type: 'sftp:upload:chunk', payload: { uploadId, chunkIndex: 0, data: '', isLast: true } });
|
wsDeps.value.sendMessage({ type: 'sftp:upload:chunk', payload: { uploadId, chunkIndex: 0, data: '', isLast: true } });
|
||||||
upload.progress = 100;
|
upload.progress = 100;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -119,8 +117,9 @@ export function useFileUploader(
|
|||||||
|
|
||||||
|
|
||||||
const startFileUpload = (file: File, relativePath?: string) => {
|
const startFileUpload = (file: File, relativePath?: string) => {
|
||||||
if (!isConnected.value) {
|
// Roo: 使用 .value 访问响应式的 sessionIdForLog
|
||||||
console.warn('[文件上传模块] 无法开始上传:WebSocket 未连接。');
|
if (!wsDeps.value.isConnected.value) {
|
||||||
|
console.warn(`[FileUploader ${sessionIdForLog.value}] Cannot start upload: WebSocket not connected.`);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -142,7 +141,7 @@ export function useFileUploader(
|
|||||||
}
|
}
|
||||||
// 规范化路径,移除多余的斜杠 e.g. /root//dir -> /root/dir
|
// 规范化路径,移除多余的斜杠 e.g. /root//dir -> /root/dir
|
||||||
finalRemotePath = finalRemotePath.replace(/\/+/g, '/');
|
finalRemotePath = finalRemotePath.replace(/\/+/g, '/');
|
||||||
console.log(`[文件上传模块] Calculated finalRemotePath: ${finalRemotePath} (current: ${currentPathRef.value}, relative: ${relativePath}, filename: ${file.name})`); // 添加日志
|
console.log(`[FileUploader ${sessionIdForLog.value}] Calculated finalRemotePath: ${finalRemotePath} (current: ${currentPathRef.value}, relative: ${relativePath}, filename: ${file.name}) // wsDeps.isSftpReady: ${wsDeps.value.isSftpReady.value}`);
|
||||||
// --- 结束修正 ---
|
// --- 结束修正 ---
|
||||||
|
|
||||||
|
|
||||||
@@ -155,10 +154,10 @@ export function useFileUploader(
|
|||||||
status: 'pending' // 初始状态
|
status: 'pending' // 初始状态
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(`[文件上传模块] 开始上传 ${uploadId} 到 ${finalRemotePath}`); // 使用 finalRemotePath
|
console.log(`[FileUploader ${sessionIdForLog.value}] Starting upload ${uploadId} to ${finalRemotePath}`);
|
||||||
sendMessage({
|
wsDeps.value.sendMessage({
|
||||||
type: 'sftp:upload:start',
|
type: 'sftp:upload:start',
|
||||||
payload: { uploadId, remotePath: finalRemotePath, size: file.size, relativePath: relativePath || undefined } // 发送修正后的 remotePath
|
payload: { uploadId, remotePath: finalRemotePath, size: file.size, relativePath: relativePath || undefined }
|
||||||
});
|
});
|
||||||
// 后端应该响应 sftp:upload:ready
|
// 后端应该响应 sftp:upload:ready
|
||||||
};
|
};
|
||||||
@@ -166,11 +165,11 @@ export function useFileUploader(
|
|||||||
const cancelUpload = (uploadId: string, notifyBackend = true) => {
|
const cancelUpload = (uploadId: string, notifyBackend = true) => {
|
||||||
const upload = uploads[uploadId];
|
const upload = uploads[uploadId];
|
||||||
if (upload && ['pending', 'uploading', 'paused'].includes(upload.status)) {
|
if (upload && ['pending', 'uploading', 'paused'].includes(upload.status)) {
|
||||||
console.log(`[文件上传模块] 取消上传 ${uploadId}`);
|
console.log(`[FileUploader ${sessionIdForLog.value}] Cancelling upload ${uploadId}`);
|
||||||
upload.status = 'cancelled'; // 立即更新状态
|
upload.status = 'cancelled'; // 立即更新状态
|
||||||
|
|
||||||
if (notifyBackend && isConnected.value) {
|
if (notifyBackend && wsDeps.value.isConnected.value) {
|
||||||
sendMessage({ type: 'sftp:upload:cancel', payload: { uploadId } });
|
wsDeps.value.sendMessage({ type: 'sftp:upload:cancel', payload: { uploadId } });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 短暂延迟后从列表中移除,以显示取消状态
|
// 短暂延迟后从列表中移除,以显示取消状态
|
||||||
@@ -190,11 +189,11 @@ export function useFileUploader(
|
|||||||
|
|
||||||
const upload = uploads[uploadId];
|
const upload = uploads[uploadId];
|
||||||
if (upload && upload.status === 'pending') {
|
if (upload && upload.status === 'pending') {
|
||||||
console.log(`[文件上传模块] 上传 ${uploadId} 已就绪,开始发送块。`);
|
console.log(`[FileUploader ${sessionIdForLog.value}] Upload ${uploadId} ready, starting chunk sending.`);
|
||||||
upload.status = 'uploading';
|
upload.status = 'uploading';
|
||||||
sendFileChunks(uploadId, upload.file); // 开始发送块
|
sendFileChunks(uploadId, upload.file); // 开始发送块
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[文件上传模块] 收到未知或非待处理状态的上传 ID 的 upload:ready 消息: ${uploadId}`);
|
console.warn(`[FileUploader ${sessionIdForLog.value}] Received upload:ready for unknown or non-pending upload ID: ${uploadId}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -204,7 +203,7 @@ export function useFileUploader(
|
|||||||
|
|
||||||
const upload = uploads[uploadId];
|
const upload = uploads[uploadId];
|
||||||
if (upload) {
|
if (upload) {
|
||||||
console.log(`[文件上传模块] 上传 ${uploadId} 成功`);
|
console.log(`[FileUploader ${sessionIdForLog.value}] Upload ${uploadId} successful.`);
|
||||||
upload.status = 'success';
|
upload.status = 'success';
|
||||||
upload.progress = 100;
|
upload.progress = 100;
|
||||||
|
|
||||||
@@ -215,7 +214,7 @@ export function useFileUploader(
|
|||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[文件上传模块] 收到未知上传 ID 的 upload:success 消息: ${uploadId}`);
|
console.warn(`[FileUploader ${sessionIdForLog.value}] Received upload:success for unknown upload ID: ${uploadId}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -223,14 +222,14 @@ export function useFileUploader(
|
|||||||
// 从 message 中获取 uploadId,因为 payload 此时是错误字符串
|
// 从 message 中获取 uploadId,因为 payload 此时是错误字符串
|
||||||
const uploadId = message.uploadId;
|
const uploadId = message.uploadId;
|
||||||
if (!uploadId) {
|
if (!uploadId) {
|
||||||
console.warn(`[文件上传模块] 收到缺少 uploadId 的 upload:error 消息:`, message);
|
console.warn(`[FileUploader ${sessionIdForLog.value}] Received upload:error with missing uploadId:`, message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const upload = uploads[uploadId];
|
const upload = uploads[uploadId];
|
||||||
if (upload) {
|
if (upload) {
|
||||||
const errorMessage = typeof payload === 'string' ? payload : t('fileManager.errors.uploadFailed');
|
const errorMessage = typeof payload === 'string' ? payload : t('fileManager.errors.uploadFailed');
|
||||||
console.error(`[文件上传模块] 上传 ${uploadId} 出错:`, errorMessage);
|
console.error(`[FileUploader ${sessionIdForLog.value}] Upload ${uploadId} error:`, errorMessage);
|
||||||
upload.status = 'error';
|
upload.status = 'error';
|
||||||
upload.error = errorMessage; // 使用 payload 作为错误消息
|
upload.error = errorMessage; // 使用 payload 作为错误消息
|
||||||
|
|
||||||
@@ -241,7 +240,7 @@ export function useFileUploader(
|
|||||||
}
|
}
|
||||||
}, 5000);
|
}, 5000);
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[文件上传模块] 收到未知上传 ID 的 upload:error 消息: ${uploadId}`);
|
console.warn(`[FileUploader ${sessionIdForLog.value}] Received upload:error for unknown upload ID: ${uploadId}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -250,7 +249,7 @@ export function useFileUploader(
|
|||||||
if (!uploadId) return;
|
if (!uploadId) return;
|
||||||
const upload = uploads[uploadId];
|
const upload = uploads[uploadId];
|
||||||
if (upload && upload.status === 'uploading') {
|
if (upload && upload.status === 'uploading') {
|
||||||
console.log(`[文件上传模块] 上传 ${uploadId} 已暂停`);
|
console.log(`[FileUploader ${sessionIdForLog.value}] Upload ${uploadId} paused.`);
|
||||||
upload.status = 'paused';
|
upload.status = 'paused';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -260,7 +259,7 @@ export function useFileUploader(
|
|||||||
if (!uploadId) return;
|
if (!uploadId) return;
|
||||||
const upload = uploads[uploadId];
|
const upload = uploads[uploadId];
|
||||||
if (upload && upload.status === 'paused') {
|
if (upload && upload.status === 'paused') {
|
||||||
console.log(`[文件上传模块] 恢复上传 ${uploadId}`);
|
console.log(`[FileUploader ${sessionIdForLog.value}] Resuming upload ${uploadId}`);
|
||||||
upload.status = 'uploading';
|
upload.status = 'uploading';
|
||||||
sendFileChunks(uploadId, upload.file);
|
sendFileChunks(uploadId, upload.file);
|
||||||
}
|
}
|
||||||
@@ -271,7 +270,6 @@ export function useFileUploader(
|
|||||||
if (!uploadId) return;
|
if (!uploadId) return;
|
||||||
const upload = uploads[uploadId];
|
const upload = uploads[uploadId];
|
||||||
if (upload) {
|
if (upload) {
|
||||||
console.log(`[文件上传模块] 后端确认上传 ${uploadId} 已取消。`);
|
|
||||||
// 状态可能已经由用户操作设置为 'cancelled'
|
// 状态可能已经由用户操作设置为 'cancelled'
|
||||||
if (upload.status !== 'cancelled') {
|
if (upload.status !== 'cancelled') {
|
||||||
upload.status = 'cancelled';
|
upload.status = 'cancelled';
|
||||||
@@ -289,7 +287,6 @@ export function useFileUploader(
|
|||||||
const onUploadProgress = (payload: MessagePayload, message: WebSocketMessage) => {
|
const onUploadProgress = (payload: MessagePayload, message: WebSocketMessage) => {
|
||||||
const uploadId = message.uploadId || payload?.uploadId; // 从顶层获取 uploadId
|
const uploadId = message.uploadId || payload?.uploadId; // 从顶层获取 uploadId
|
||||||
if (!uploadId) {
|
if (!uploadId) {
|
||||||
console.warn(`[文件上传模块] 收到缺少 uploadId 的 upload:progress 消息:`, message);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,28 +297,33 @@ export function useFileUploader(
|
|||||||
upload.progress = Math.min(100, Math.round((payload.bytesWritten / payload.totalSize) * 100));
|
upload.progress = Math.min(100, Math.round((payload.bytesWritten / payload.totalSize) * 100));
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[文件上传模块] 收到 upload:progress 消息,但 payload 格式不正确:`, payload);
|
console.warn(`[FileUploader ${sessionIdForLog.value}] Received upload:progress with incorrect payload format:`, payload);
|
||||||
}
|
}
|
||||||
} else if (upload) {
|
} else if (upload) {
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[文件上传模块] 收到未知上传 ID 的 upload:progress 消息: ${uploadId}`);
|
console.warn(`[FileUploader ${sessionIdForLog.value}] Received upload:progress for unknown upload ID: ${uploadId}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// --- 注册处理器 ---
|
// --- 动态注册和注销处理器 ---
|
||||||
const unregisterUploadReady = onMessage('sftp:upload:ready', onUploadReady);
|
watchEffect((onCleanup) => {
|
||||||
const unregisterUploadSuccess = onMessage('sftp:upload:success', onUploadSuccess);
|
// 当 wsDeps.value 变化时,此 effect 会重新运行
|
||||||
const unregisterUploadError = onMessage('sftp:upload:error', onUploadError);
|
if (!wsDeps.value || !wsDeps.value.onMessage) {
|
||||||
const unregisterUploadPause = onMessage('sftp:upload:pause', onUploadPause);
|
console.warn(`[FileUploader ${sessionIdForLog.value}] wsDeps.value or wsDeps.value.onMessage is not available for registering listeners.`);
|
||||||
const unregisterUploadResume = onMessage('sftp:upload:resume', onUploadResume);
|
return;
|
||||||
const unregisterUploadCancelled = onMessage('sftp:upload:cancelled', onUploadCancelled);
|
}
|
||||||
const unregisterUploadProgress = onMessage('sftp:upload:progress', onUploadProgress); // +++ 注册新处理器 +++
|
|
||||||
|
|
||||||
// --- 清理 ---
|
const unregisterUploadReady = wsDeps.value.onMessage('sftp:upload:ready', onUploadReady);
|
||||||
onUnmounted(() => {
|
const unregisterUploadSuccess = wsDeps.value.onMessage('sftp:upload:success', onUploadSuccess);
|
||||||
console.log('[文件上传模块] 卸载并注销处理器。');
|
const unregisterUploadError = wsDeps.value.onMessage('sftp:upload:error', onUploadError);
|
||||||
|
const unregisterUploadPause = wsDeps.value.onMessage('sftp:upload:pause', onUploadPause);
|
||||||
|
const unregisterUploadResume = wsDeps.value.onMessage('sftp:upload:resume', onUploadResume);
|
||||||
|
const unregisterUploadCancelled = wsDeps.value.onMessage('sftp:upload:cancelled', onUploadCancelled);
|
||||||
|
const unregisterUploadProgress = wsDeps.value.onMessage('sftp:upload:progress', onUploadProgress);
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
unregisterUploadReady?.();
|
unregisterUploadReady?.();
|
||||||
unregisterUploadSuccess?.();
|
unregisterUploadSuccess?.();
|
||||||
unregisterUploadError?.();
|
unregisterUploadError?.();
|
||||||
@@ -329,6 +331,13 @@ export function useFileUploader(
|
|||||||
unregisterUploadResume?.();
|
unregisterUploadResume?.();
|
||||||
unregisterUploadCancelled?.();
|
unregisterUploadCancelled?.();
|
||||||
unregisterUploadProgress?.();
|
unregisterUploadProgress?.();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- 清理 (onUnmounted 仍然用于组件生命周期结束时的清理) ---
|
||||||
|
onUnmounted(() => {
|
||||||
|
// 注意:消息监听器的注销现在主要由 watchEffect 的 onCleanup 处理。
|
||||||
|
// onUnmounted 仍然负责取消正在进行的上传。
|
||||||
|
|
||||||
// 当使用此 composable 的组件卸载时,取消任何正在进行的上传
|
// 当使用此 composable 的组件卸载时,取消任何正在进行的上传
|
||||||
Object.keys(uploads).forEach(uploadId => {
|
Object.keys(uploads).forEach(uploadId => {
|
||||||
|
|||||||
@@ -79,18 +79,18 @@ export function createWebSocketConnectionManager(
|
|||||||
* 安排 WebSocket 重连尝试
|
* 安排 WebSocket 重连尝试
|
||||||
*/
|
*/
|
||||||
const scheduleReconnect = () => {
|
const scheduleReconnect = () => {
|
||||||
if (intentionalDisconnect) return; // 如果是主动断开,则不重连
|
if (intentionalDisconnect) {
|
||||||
|
return; // 如果是主动断开,则不重连
|
||||||
|
}
|
||||||
|
|
||||||
// +++ 检查是否标记为待挂起 +++
|
// +++ 检查是否标记为待挂起 +++
|
||||||
if (getIsMarkedForSuspend && getIsMarkedForSuspend()) {
|
if (getIsMarkedForSuspend && getIsMarkedForSuspend()) {
|
||||||
console.log(`[WebSocket ${instanceSessionId}] 会话已标记为待挂起,不执行自动重连。`);
|
|
||||||
statusMessage.value = getStatusText('markedForSuspendNoReconnect'); // 可以为此添加新的i18n文本
|
statusMessage.value = getStatusText('markedForSuspendNoReconnect'); // 可以为此添加新的i18n文本
|
||||||
connectionStatus.value = 'disconnected'; // 保持断开状态或设为特定状态
|
connectionStatus.value = 'disconnected'; // 保持断开状态或设为特定状态
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reconnectAttempts >= maxReconnectAttempts) {
|
if (reconnectAttempts >= maxReconnectAttempts) {
|
||||||
console.log(`[WebSocket ${instanceSessionId}] 已达到最大重连次数 (${maxReconnectAttempts}),停止重连。`);
|
|
||||||
statusMessage.value = getStatusText('reconnectFailed');
|
statusMessage.value = getStatusText('reconnectFailed');
|
||||||
connectionStatus.value = 'error'; // 标记为错误状态
|
connectionStatus.value = 'error'; // 标记为错误状态
|
||||||
return;
|
return;
|
||||||
@@ -99,7 +99,6 @@ export function createWebSocketConnectionManager(
|
|||||||
reconnectAttempts++;
|
reconnectAttempts++;
|
||||||
// 指数退避延迟 (例如: 2s, 4s, 8s, 16s, 32s)
|
// 指数退避延迟 (例如: 2s, 4s, 8s, 16s, 32s)
|
||||||
const delay = Math.pow(2, reconnectAttempts) * 1000;
|
const delay = Math.pow(2, reconnectAttempts) * 1000;
|
||||||
console.log(`[WebSocket ${instanceSessionId}] 连接丢失,将在 ${delay / 1000} 秒后尝试第 ${reconnectAttempts} 次重连...`);
|
|
||||||
statusMessage.value = getStatusText('reconnecting', { attempt: reconnectAttempts, delay: delay / 1000 });
|
statusMessage.value = getStatusText('reconnecting', { attempt: reconnectAttempts, delay: delay / 1000 });
|
||||||
connectionStatus.value = 'connecting'; // 更新状态为正在连接
|
connectionStatus.value = 'connecting'; // 更新状态为正在连接
|
||||||
|
|
||||||
@@ -162,7 +161,6 @@ export function createWebSocketConnectionManager(
|
|||||||
}
|
}
|
||||||
// 如果 ws.value 存在且 readyState 是 CLOSED,它应该已经在 onclose 中被设为 null
|
// 如果 ws.value 存在且 readyState 是 CLOSED,它应该已经在 onclose 中被设为 null
|
||||||
|
|
||||||
console.log(`[WebSocket ${instanceSessionId}] 尝试连接到: ${url} (DB Conn ID: ${instanceDbConnectionId})`);
|
|
||||||
statusMessage.value = getStatusText('connectingWs', { url });
|
statusMessage.value = getStatusText('connectingWs', { url });
|
||||||
connectionStatus.value = 'connecting'; // 确保状态设置为 connecting
|
connectionStatus.value = 'connecting'; // 确保状态设置为 connecting
|
||||||
isSftpReady.value = false; // 重置 SFTP 状态
|
isSftpReady.value = false; // 重置 SFTP 状态
|
||||||
@@ -180,7 +178,6 @@ export function createWebSocketConnectionManager(
|
|||||||
ws.value = new WebSocket(secureUrl);
|
ws.value = new WebSocket(secureUrl);
|
||||||
|
|
||||||
ws.value.onopen = () => {
|
ws.value.onopen = () => {
|
||||||
console.log(`[WebSocket ${instanceSessionId}] 连接已打开 (${secureUrl})。`); // 在日志中包含使用的 URL
|
|
||||||
reconnectAttempts = 0; // 连接成功,重置尝试次数
|
reconnectAttempts = 0; // 连接成功,重置尝试次数
|
||||||
statusMessage.value = getStatusText('wsConnected');
|
statusMessage.value = getStatusText('wsConnected');
|
||||||
// 状态保持 'connecting' 直到收到 ssh:connected
|
// 状态保持 'connecting' 直到收到 ssh:connected
|
||||||
@@ -191,7 +188,6 @@ export function createWebSocketConnectionManager(
|
|||||||
// 对于恢复流程,WebSocket 打开即表示连接基础已建立
|
// 对于恢复流程,WebSocket 打开即表示连接基础已建立
|
||||||
// 后续的 SSH_SUSPEND_RESUME_REQUEST 会完成会话的恢复
|
// 后续的 SSH_SUSPEND_RESUME_REQUEST 会完成会话的恢复
|
||||||
connectionStatus.value = 'connected';
|
connectionStatus.value = 'connected';
|
||||||
console.log(`[WebSocket ${instanceSessionId}] 恢复流程:WebSocket 打开,状态直接设为 connected。`);
|
|
||||||
}
|
}
|
||||||
dispatchMessage('internal:opened', {}, { type: 'internal:opened' }); // 触发内部打开事件
|
dispatchMessage('internal:opened', {}, { type: 'internal:opened' }); // 触发内部打开事件
|
||||||
};
|
};
|
||||||
@@ -199,27 +195,22 @@ export function createWebSocketConnectionManager(
|
|||||||
ws.value.onmessage = (event: MessageEvent) => {
|
ws.value.onmessage = (event: MessageEvent) => {
|
||||||
try {
|
try {
|
||||||
const rawData = event.data;
|
const rawData = event.data;
|
||||||
// console.log(`[WebSocket ${instanceSessionId}] onmessage: 收到原始数据 (类型: ${typeof rawData}, 长度: ${rawData.toString().length}) 前100字符:`, rawData.toString().substring(0, 100));
|
|
||||||
const message: WebSocketMessage = JSON.parse(rawData.toString());
|
const message: WebSocketMessage = JSON.parse(rawData.toString());
|
||||||
// console.log(`[WebSocket ${instanceSessionId}] onmessage: 解析后消息类型: ${message.type}, 会话ID (消息内): ${message.sessionId || 'N/A'}, Payload keys: ${message.payload ? Object.keys(message.payload).join(', ') : 'N/A'}`);
|
|
||||||
|
|
||||||
// --- 更新此实例的连接状态 ---
|
// --- 更新此实例的连接状态 ---
|
||||||
if (message.type === 'ssh:connected') {
|
if (message.type === 'ssh:connected') {
|
||||||
if (connectionStatus.value !== 'connected') {
|
if (connectionStatus.value !== 'connected') {
|
||||||
console.log(`[WebSocket ${instanceSessionId}] SSH 会话已连接。`);
|
|
||||||
connectionStatus.value = 'connected';
|
connectionStatus.value = 'connected';
|
||||||
statusMessage.value = getStatusText('connected');
|
statusMessage.value = getStatusText('connected');
|
||||||
}
|
}
|
||||||
} else if (message.type === 'ssh:disconnected') {
|
} else if (message.type === 'ssh:disconnected') {
|
||||||
if (connectionStatus.value !== 'disconnected') {
|
if (connectionStatus.value !== 'disconnected') {
|
||||||
console.log(`[WebSocket ${instanceSessionId}] SSH 会话已断开。`);
|
|
||||||
connectionStatus.value = 'disconnected';
|
connectionStatus.value = 'disconnected';
|
||||||
statusMessage.value = getStatusText('disconnected', { reason: message.payload || '未知原因' });
|
statusMessage.value = getStatusText('disconnected', { reason: message.payload || '未知原因' });
|
||||||
isSftpReady.value = false; // SSH 断开,SFTP 也应不可用
|
isSftpReady.value = false; // SSH 断开,SFTP 也应不可用
|
||||||
}
|
}
|
||||||
} else if (message.type === 'ssh:error' || message.type === 'error') {
|
} else if (message.type === 'ssh:error' || message.type === 'error') {
|
||||||
if (connectionStatus.value !== 'disconnected' && connectionStatus.value !== 'error') {
|
if (connectionStatus.value !== 'disconnected' && connectionStatus.value !== 'error') {
|
||||||
console.error(`[WebSocket ${instanceSessionId}] 收到错误消息:`, message.payload);
|
|
||||||
connectionStatus.value = 'error';
|
connectionStatus.value = 'error';
|
||||||
let errorMsg = message.payload || '未知错误';
|
let errorMsg = message.payload || '未知错误';
|
||||||
if (typeof errorMsg === 'object' && errorMsg.message) errorMsg = errorMsg.message;
|
if (typeof errorMsg === 'object' && errorMsg.message) errorMsg = errorMsg.message;
|
||||||
@@ -242,10 +233,10 @@ export function createWebSocketConnectionManager(
|
|||||||
};
|
};
|
||||||
|
|
||||||
ws.value.onerror = (event) => {
|
ws.value.onerror = (event) => {
|
||||||
console.error(`[WebSocket ${instanceSessionId}] 连接错误:`, event);
|
if (connectionStatus.value !== 'disconnected' && connectionStatus.value !== 'error') { // Don't override if already explicitly disconnected
|
||||||
if (connectionStatus.value !== 'disconnected') {
|
|
||||||
connectionStatus.value = 'error';
|
connectionStatus.value = 'error';
|
||||||
statusMessage.value = getStatusText('wsError');
|
statusMessage.value = getStatusText('wsError');
|
||||||
|
} else {
|
||||||
}
|
}
|
||||||
dispatchMessage('internal:error', event, { type: 'internal:error' });
|
dispatchMessage('internal:error', event, { type: 'internal:error' });
|
||||||
isSftpReady.value = false;
|
isSftpReady.value = false;
|
||||||
@@ -257,12 +248,11 @@ export function createWebSocketConnectionManager(
|
|||||||
};
|
};
|
||||||
|
|
||||||
ws.value.onclose = (event) => {
|
ws.value.onclose = (event) => {
|
||||||
console.log(`[WebSocket ${instanceSessionId}] 连接已关闭: Code=${event.code}, Reason=${event.reason}`);
|
|
||||||
// 只有在非错误状态下才更新为 disconnected
|
// 只有在非错误状态下才更新为 disconnected
|
||||||
if (connectionStatus.value !== 'error') {
|
if (connectionStatus.value !== 'error' && connectionStatus.value !== 'disconnected') { // Avoid redundant sets or overriding 'error'
|
||||||
connectionStatus.value = 'disconnected';
|
connectionStatus.value = 'disconnected';
|
||||||
// 如果不是主动断开,显示尝试重连的消息
|
// 如果不是主动断开,显示尝试重连的消息
|
||||||
if (!intentionalDisconnect && event.code !== 1000) {
|
if (!intentionalDisconnect && event.code !== 1000) { // 1000 is normal closure
|
||||||
statusMessage.value = getStatusText('wsClosedWillRetry', { code: event.code });
|
statusMessage.value = getStatusText('wsClosedWillRetry', { code: event.code });
|
||||||
} else {
|
} else {
|
||||||
statusMessage.value = getStatusText('wsClosed', { code: event.code });
|
statusMessage.value = getStatusText('wsClosed', { code: event.code });
|
||||||
@@ -273,12 +263,11 @@ export function createWebSocketConnectionManager(
|
|||||||
ws.value = null; // 清理实例引用
|
ws.value = null; // 清理实例引用
|
||||||
|
|
||||||
// 如果不是主动断开 (code 1000),尝试重连
|
// 如果不是主动断开 (code 1000),尝试重连
|
||||||
if (!intentionalDisconnect && event.code !== 1000) {
|
if (!intentionalDisconnect && event.code !== 1000) { // 1000 is normal closure
|
||||||
scheduleReconnect();
|
scheduleReconnect();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`[WebSocket ${instanceSessionId}] 创建 WebSocket 实例失败:`, err);
|
|
||||||
connectionStatus.value = 'error';
|
connectionStatus.value = 'error';
|
||||||
statusMessage.value = getStatusText('wsError');
|
statusMessage.value = getStatusText('wsError');
|
||||||
isSftpReady.value = false;
|
isSftpReady.value = false;
|
||||||
@@ -296,7 +285,6 @@ export function createWebSocketConnectionManager(
|
|||||||
reconnectTimeoutId = null;
|
reconnectTimeoutId = null;
|
||||||
}
|
}
|
||||||
if (ws.value) {
|
if (ws.value) {
|
||||||
console.log(`[WebSocket ${instanceSessionId}] 手动关闭连接...`);
|
|
||||||
if (connectionStatus.value !== 'disconnected') {
|
if (connectionStatus.value !== 'disconnected') {
|
||||||
connectionStatus.value = 'disconnected';
|
connectionStatus.value = 'disconnected';
|
||||||
statusMessage.value = getStatusText('disconnected', { reason: '手动断开' });
|
statusMessage.value = getStatusText('disconnected', { reason: '手动断开' });
|
||||||
@@ -306,8 +294,6 @@ export function createWebSocketConnectionManager(
|
|||||||
isSftpReady.value = false;
|
isSftpReady.value = false;
|
||||||
// 手动断开时可以考虑清除处理器,取决于是否需要重连逻辑
|
// 手动断开时可以考虑清除处理器,取决于是否需要重连逻辑
|
||||||
// messageHandlers.clear();
|
// messageHandlers.clear();
|
||||||
} else {
|
|
||||||
console.log(`[WebSocket ${instanceSessionId}] 连接已关闭或不存在,无需断开。`);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -319,9 +305,7 @@ export function createWebSocketConnectionManager(
|
|||||||
if (ws.value && ws.value.readyState === WebSocket.OPEN) {
|
if (ws.value && ws.value.readyState === WebSocket.OPEN) {
|
||||||
try {
|
try {
|
||||||
const messageString = JSON.stringify(message);
|
const messageString = JSON.stringify(message);
|
||||||
// console.log(`[WebSocket ${instanceSessionId}] sendMessage: 准备发送消息。类型: ${message.type}, 会话ID (消息内): ${message.sessionId || 'N/A'}, Payload keys: ${message.payload ? Object.keys(message.payload).join(', ') : 'N/A'}`);
|
|
||||||
ws.value.send(messageString);
|
ws.value.send(messageString);
|
||||||
// console.log(`[WebSocket ${instanceSessionId}] sendMessage: 消息已发送。类型: ${message.type}`);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`[WebSocket ${instanceSessionId}] 序列化或发送消息失败:`, e, message);
|
console.error(`[WebSocket ${instanceSessionId}] 序列化或发送消息失败:`, e, message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -674,7 +674,6 @@ const handleFileManagerOpenRequest = (payload: { sessionId: string }) => {
|
|||||||
isConnected: session.wsManager.isConnected,
|
isConnected: session.wsManager.isConnected,
|
||||||
isSftpReady: session.wsManager.isSftpReady,
|
isSftpReady: session.wsManager.isSftpReady,
|
||||||
};
|
};
|
||||||
console.log(`[WorkspaceView] Retrieved wsDeps from session.wsManager for session ${sessionId}.`);
|
|
||||||
|
|
||||||
if (!wsDeps) {
|
if (!wsDeps) {
|
||||||
// 如果 wsDeps 仍然为 null,则无法继续
|
// 如果 wsDeps 仍然为 null,则无法继续
|
||||||
|
|||||||
Reference in New Issue
Block a user