diff --git a/Client/main.lua b/Client/main.lua index 4051f13..f28c685 100644 --- a/Client/main.lua +++ b/Client/main.lua @@ -1,18 +1,15 @@ -- simple_file_client.lua --- 支持创建房间、WebSocket 连接、异步文件操作、心跳保活 --- 使用 Basalt 实现非阻塞异步任务 - local args = {...} local httpServer = args[1] or "http://192.168.2.200:8080" -local roomId = args[2] -- 可选的房间ID参数 -local wsPort = 8081 -local heartbeatInterval = 1 -- 心跳间隔(秒) +local roomId = args[2] +local pollInterval = 1 +local computerID = tostring(os.computerID() or "unknown") -- ========== 加载 Basalt ========== local basaltUrl = "https://git.liulikeji.cn/GitHub/Basalt/releases/download/v1.7/basalt.lua" local basaltResp = http.get(basaltUrl) if not basaltResp then - error("无法下载 Basalt 框架,请检查网络或使用本地缓存") + error("无法下载 Basalt 框架") end local basalt = load(basaltResp.readAll())() basaltResp.close() @@ -110,336 +107,282 @@ function table_to_json(t, indent) end local function cleanPath(path) - if string.sub(path, 1, 9) == "computer/" then - return string.sub(path, 10) - elseif string.sub(path, 1, 9) == "computer\\" then - return string.sub(path, 10) - end - return path + return (path:gsub("^computer/", ""):gsub("^computer\\", "")) end -local function sendJson(ws, obj) - local payload = table_to_json(obj) - ws.send(payload) -end - --- ========== 文件系统操作(纯逻辑,无网络)========== -local function isLikelyText(data, maxCheck) - if not data then return false end - maxCheck = maxCheck or math.min(#data, 1024) - for i = 1, maxCheck do - local b = data:byte(i) - if b < 32 and not (b == 9 or b == 10 or b == 13) then - return false +local function httpPost(path, data) + local jsonData = table_to_json(data) + local url = httpServer .. path + + local response = http.post(url, jsonData, { + ["Content-Type"] = "application/json" + }) + + if not response then + return nil, "无法连接到服务器" end - end - return true + + local responseBody = response.readAll() + response.close() + + local ok, result = pcall(textutils.unserialiseJSON, responseBody) + if ok then + return result + else + return nil, "无效的JSON响应" + end +end + +-- ========== 文件系统操作 ========== +local function isLikelyText(data) + for i = 1, math.min(#data, 1024) do + local b = data:byte(i) + if b < 32 and b ~= 9 and b ~= 10 and b ~= 13 then + return false + end + end + return true end local function getFiles(currentPath, result, prefix) - local fullPrefix - if currentPath == "" then - fullPrefix = prefix:sub(1, -2) -- 移除末尾的斜杠,将 "computer/" 变为 "computer" - else - fullPrefix = prefix .. currentPath - end - - local absPath = "/" .. (currentPath == "" and "" or currentPath) - if fs.isDir(absPath) then - result[fullPrefix] = { isFolder = true } - for _, entry in ipairs(fs.list(absPath)) do - if entry ~= "rom" then - local nextPath = currentPath == "" and entry or (currentPath .. "/" .. entry) - getFiles(nextPath, result, prefix) - end + local computerPrefix = "computer_" .. computerID + local fullPrefix = currentPath == "" and prefix:sub(1, -2) or prefix .. currentPath + local absPath = "/" .. (currentPath == "" and "" or currentPath) + + if fs.isDir(absPath) then + result[fullPrefix] = { isFolder = true } + for _, entry in ipairs(fs.list(absPath)) do + if entry ~= "rom" then + getFiles(currentPath == "" and entry or (currentPath .. "/" .. entry), result, prefix) + end + end + else + local content = "[binary]" + local ok, handle = pcall(fs.open, absPath, "rb") + if ok and handle then + local data = handle.readAll() + handle.close() + if data and isLikelyText(data) then + content = data + end + end + result[fullPrefix] = { isFile = true, content = content } end - else - local content = "[binary]" - local ok, handle = pcall(fs.open, absPath, "rb") - if ok and handle then - local data = handle.readAll() - handle.close() - log(absPath.." File size: " .. #data) - if data and isLikelyText(data) then - content = data - log(absPath .. " is text file") - end - if content == "[binary]" then print("binary file: " .. absPath) end - end - result[fullPrefix] = { isFile = true, content = content } - end end local function fetchFiles() - local files = {} - getFiles("", files, "computer/") - return files + local files = {} + getFiles("", files, "computer_" .. computerID .. "/") + return files end local function saveFile(path, content) - path = cleanPath(path) - local dir = fs.getDir(path) - if not fs.exists(dir) then fs.makeDir(dir) end - local f = fs.open(path, "w") - f.write(content or "") - f.close() + path = cleanPath(path):gsub("^computer[" .. computerID .. "_]*/", "") + local dir = fs.getDir(path) + if not fs.exists(dir) then fs.makeDir(dir) end + local f = fs.open(path, "w") + f.write(content or "") + f.close() end local function createFile(path) - path = cleanPath(path) - local dir = fs.getDir(path) - if not fs.exists(dir) then fs.makeDir(dir) end - if not fs.exists(path) then - local f = fs.open(path, "w") - f.close() - end + path = cleanPath(path):gsub("^computer[" .. computerID .. "_]*/", "") + local dir = fs.getDir(path) + if not fs.exists(dir) then fs.makeDir(dir) end + if not fs.exists(path) then + local f = fs.open(path, "w") + f.close() + end end local function createFolder(path) - path = cleanPath(path) - fs.makeDir(path) + path = cleanPath(path):gsub("^computer[" .. computerID .. "_]*/", "") + fs.makeDir(path) end local function renameFile(oldPath, newPath) - oldPath = cleanPath(oldPath) - newPath = cleanPath(newPath) - fs.move(oldPath, newPath) + oldPath = cleanPath(oldPath):gsub("^computer[" .. computerID .. "_]*/", "") + newPath = cleanPath(newPath):gsub("^computer[" .. computerID .. "_]*/", "") + fs.move(oldPath, newPath) end local function deleteFile(path) - path = cleanPath(path) - if fs.exists(path) then - fs.delete(path) - end + path = cleanPath(path):gsub("^computer[" .. computerID .. "_]*/", "") + if fs.exists(path) then + fs.delete(path) + end end --- ========== 异步任务处理器 ========== -local function handleFetchFiles(ws, reqId, sender) - local success, result = pcall(fetchFiles) - sendJson(ws, { - type = "file_operation_response", - requestId = reqId, - success = success, - data = success and result or nil, - error = success and nil or tostring(result), - target_client_id = sender - }) - log("Async fetch_files completed: " .. (success and "OK" or "FAILED")) +-- ========== 消息处理函数 ========== +local function handleFetchFiles(reqId, sender) + local success, result = pcall(fetchFiles) + return { + type = "file_operation_response", + requestId = reqId, + success = success, + data = success and result or nil, + error = success and nil or tostring(result), + target_client_id = sender + } end -local function handleSaveFile(ws, data, reqId, sender) - local success, err = pcall(function() - assert(data.path, "Missing path") - saveFile(data.path, data.content) - end) - sendJson(ws, { - type = "file_operation_response", - requestId = reqId, - success = success, - error = success and nil or tostring(err), - target_client_id = sender - }) - log("Async save_file completed: " .. (success and "OK" or "FAILED")) +local function handleSaveFile(data, reqId, sender) + local success, err = pcall(saveFile, data.path, data.content) + return { + type = "file_operation_response", + requestId = reqId, + success = success, + error = success and nil or tostring(err), + target_client_id = sender + } end -local function handleCreateFile(ws, data, reqId, sender) - local success, err = pcall(function() - assert(data.path, "Missing path") - createFile(data.path) - end) - sendJson(ws, { - type = "file_operation_response", - requestId = reqId, - success = success, - error = success and nil or tostring(err), - target_client_id = sender - }) - log("Async create_file completed: " .. (success and "OK" or "FAILED")) +local function handleCreateFile(data, reqId, sender) + local success, err = pcall(createFile, data.path) + return { + type = "file_operation_response", + requestId = reqId, + success = success, + error = success and nil or tostring(err), + target_client_id = sender + } end -local function handleCreateFolder(ws, data, reqId, sender) - local success, err = pcall(function() - assert(data.path, "Missing path") - createFolder(data.path) - end) - sendJson(ws, { - type = "file_operation_response", - requestId = reqId, - success = success, - error = success and nil or tostring(err), - target_client_id = sender - }) - log("Async create_folder completed: " .. (success and "OK" or "FAILED")) +local function handleCreateFolder(data, reqId, sender) + local success, err = pcall(createFolder, data.path) + return { + type = "file_operation_response", + requestId = reqId, + success = success, + error = success and nil or tostring(err), + target_client_id = sender + } end -local function handleRename(ws, data, reqId, sender) - local success, err = pcall(function() - assert(data.path and data.newPath, "Missing path or newPath") - renameFile(data.path, data.newPath) - end) - sendJson(ws, { - type = "file_operation_response", - requestId = reqId, - success = success, - error = success and nil or tostring(err), - target_client_id = sender - }) - log("Async rename completed: " .. (success and "OK" or "FAILED")) +local function handleRename(data, reqId, sender) + local success, err = pcall(renameFile, data.path, data.newPath) + return { + type = "file_operation_response", + requestId = reqId, + success = success, + error = success and nil or tostring(err), + target_client_id = sender + } end -local function handleDelete(ws, data, reqId, sender) - log("delete:"..data.path) - local success, err = pcall(function() - assert(data.path, "Missing path") - deleteFile(data.path) - end) - sendJson(ws, { - type = "file_operation_response", - requestId = reqId, - success = success, - error = success and nil or tostring(err), - target_client_id = sender - }) - log("Async delete completed: " .. (success and "OK" or "FAILED")) +local function handleDelete(data, reqId, sender) + local success, err = pcall(deleteFile, data.path) + return { + type = "file_operation_response", + requestId = reqId, + success = success, + error = success and nil or tostring(err), + target_client_id = sender + } end -- ========== 房间管理函数 ========== local function createRoom() - log("正在创建房间...") - local roomUrl = httpServer .. "/api/room" - local roomResp = http.post(roomUrl, "{}") - if not roomResp then - error("无法连接到 HTTP 服务器创建房间") - end - - local body = roomResp.readAll() - roomResp.close() - - local roomData = textutils.unserialiseJSON(body) - if not roomData or not roomData.room_id or not roomData.ws_url then - error("无效的房间响应: " .. tostring(body)) - end - - log("房间创建成功,房间 ID: " .. roomData.room_id) - return roomData + local result, err = httpPost("/api/room", {}) + if not result then + error("无法创建房间: " .. tostring(err)) + end + return result.room_id end -local function joinRoom(roomId) - log("正在加入房间: " .. roomId) - - -- 构建 WebSocket URL - local wsUrl = httpServer:gsub("^http", "ws") .. "/ws?room_id=" .. roomId - - log("连接 WebSocket: " .. wsUrl) - local ws = http.websocket(wsUrl) - if not ws then - error("无法打开 WebSocket 连接") - end +local function sendResponse(response) + if response and roomId then + httpPost("/api/client/send", { + room_id = roomId, + message = response + }) + end +end - -- 加入房间 - sendJson(ws, { - type = "join_room", - room_id = roomId, - client_type = "file_client" - }) - - return ws, roomId +local function pollMessages() + while true do + if not roomId then + sleep(pollInterval) + break + end + + local result, err = httpPost("/api/client/receive", { + room_id = roomId + }) + + if result and result.success and result.message then + local msg = result.message + local msgType = msg.type + + if msgType == "file_operation" or msgType == "file_operation_request" then + local op = msg.operation_type or msg.type + local data = msg.data or {} + local reqId = msg.requestId or msg.request_id + local sender = msg.sender_id + + local response + + if op == "fetch_files" then + response = handleFetchFiles(reqId, sender) + elseif op == "create_or_save_file" then + response = handleSaveFile(data, reqId, sender) + elseif op == "new_file" then + response = handleCreateFile(data, reqId, sender) + elseif op == "new_folder" then + response = handleCreateFolder(data, reqId, sender) + elseif op == "rename" then + response = handleRename(data, reqId, sender) + elseif op == "delete_file" then + response = handleDelete(data, reqId, sender) + else + response = { + type = "file_operation_response", + requestId = reqId, + success = false, + error = "Unknown operation: " .. tostring(op), + target_client_id = sender + } + end + + sendResponse(response) + end + elseif err then + log("轮询错误: " .. tostring(err)) + end + + sleep(pollInterval) + end end -- ========== 主函数 ========== local function main() - local ws, finalRoomId - - if roomId then - -- 如果提供了房间ID,直接加入 - ws, finalRoomId = joinRoom(roomId) - else - -- 否则创建新房间 - local roomData = createRoom() - ws, finalRoomId = joinRoom(roomData.room_id) - end + if not roomId then + roomId = createRoom() + log("创建新房间: " .. roomId) + else + log("使用现有房间: " .. roomId) + end - -- 启动心跳 - local heartbeatTimer = os.startTimer(heartbeatInterval) - - mainFrame:addProgram():execute(function () - shell.run("shell") - end):setSize("parent.w","parent.h") - - -- 消息循环 - parallel.waitForAll( - function () - sleep(0.5) + mainFrame:addProgram():execute(function() + shell.run("shell") + end):setSize("parent.w","parent.h") os.queueEvent("mouse_click",1,1,1) + -- 启动消息轮询 + mainFrame:addThread():start(pollMessages) + + log("客户端已启动。房间ID: " .. roomId) + log("计算机ID: " .. computerID) + log("按 Q 退出") + + -- 主循环 while true do - local payload = ws.receive() - if payload then - local ok, msg = pcall(textutils.unserialiseJSON, payload) - if ok and type(msg) == "table" then - local msgType = msg.type - log("收到消息: " .. tostring(msgType)) - - if msgType == "connected" then - log("分配的客户端 ID: " .. tostring(msg.client_id)) - - elseif msgType == "file_operation" or msgType == "file_operation_request" then - op = msg.operation_type or msg.type - data = msg.data or {} - reqId = msg.requestId or msg.request_id - sender = msg.sender_id - - -- 派发到后台线程 - if op == "fetch_files" then - mainFrame:addThread():start(function () handleFetchFiles(ws, reqId, sender) end ) - elseif op == "create_or_save_file" then - mainFrame:addThread():start(function () handleSaveFile(ws, data, reqId, sender) end) - elseif op == "new_file" then - mainFrame:addThread():start(function () handleCreateFile(ws, data, reqId, sender) end) - elseif op == "new_folder" then - mainFrame:addThread():start(function () handleCreateFolder(ws, data, reqId, sender) end) - elseif op == "rename" then - mainFrame:addThread():start(function () handleRename(ws, data, reqId, sender) end) - elseif op == "delete_file" then - mainFrame:addThread():start(function () handleDelete(ws, data, reqId, sender) end) - else - -- 同步返回未知操作错误(轻量) - sendJson(ws, { - type = "file_operation_response", - requestId = reqId, - success = false, - error = "Unknown operation: " .. tostring(op), - target_client_id = sender - }) - end - end - else - log("无效 JSON: " .. tostring(payload)) + local event, param1 = os.pullEvent() + if event == "key" and param1 == keys.q then + log("用户按 Q 退出") + break end - end end - end, - function () - while true do - local event, param1, param2, param3 = os.pullEvent() - - if event == "timer" and param1 == heartbeatTimer then - sendJson(ws, { type = "ping" }) - heartbeatTimer = os.startTimer(heartbeatInterval) - - elseif event == "websocket_closed" and param1 == ws then - log("WebSocket 连接已关闭") - break - - elseif event == "key" and param1 == keys.q then - log("用户按 Q 退出") - break - end - end - end) - - ws.close() - log("客户端已停止。房间ID: " .. finalRoomId) end -- 启动主逻辑和Basalt事件循环 -parallel.waitForAll(basalt.autoUpdate, main) \ No newline at end of file +parallel.waitForAll(basalt.autoUpdate, main) diff --git a/Frontend1/src/App.vue b/Frontend1/src/App.vue index df86301..93f2fe3 100644 --- a/Frontend1/src/App.vue +++ b/Frontend1/src/App.vue @@ -157,8 +157,7 @@ const fileInputRef = ref() const monacoEditorRef = ref() const handleReload = (resolve: () => void, reject: (msg?: string) => void) => { - server - .fetchFiles() + withTimeout(server.fetchFiles(), 10000) .then((response) => { files.value = response nextTick(() => { @@ -168,75 +167,97 @@ const handleReload = (resolve: () => void, reject: (msg?: string) => void) => { resolve() }) .catch((e: Error) => { - reject(e.message) + const errorMessage = e.message.includes('超时') ? '文件列表请求超时,请检查网络连接' : e.message + reject(errorMessage) }) } const handleSaveFile = (path: string, content: string, resolve: () => void, reject: (msg?: string) => void) => { - server - .createOrSaveFile(path, content) + withTimeout(server.createOrSaveFile(path, content), 10000) .then((_response) => { resolve() }) .catch((e: Error) => { - reject(e.message) + const errorMessage = e.message.includes('超时') ? '保存文件请求超时,请重试' : e.message + reject(errorMessage) }) } const handleDeleteFile = (path: string, resolve: () => void, reject: (msg?: string) => void) => { - server - .deleteFile(path) + withTimeout(server.deleteFile(path), 10000) .then((_response) => { resolve() }) .catch((e: Error) => { - reject(e.message) + const errorMessage = e.message.includes('超时') ? '删除文件请求超时,请重试' : e.message + reject(errorMessage) }) } const handleDeleteFolder = (path: string, resolve: () => void, reject: (msg?: string) => void) => { - server - .deleteFile(path) + withTimeout(server.deleteFile(path), 10000) .then((_response) => { resolve() }) .catch((e: Error) => { - reject(e.message) + const errorMessage = e.message.includes('超时') ? '删除文件夹请求超时,请重试' : e.message + reject(errorMessage) }) } const handleNewFile = (path: string, resolve: Function, reject: Function) => { - server - .newFile(path) + withTimeout(server.newFile(path), 10000) .then((_response) => { resolve() }) .catch((e: Error) => { - reject(e.message) + const errorMessage = e.message.includes('超时') ? '新建文件请求超时,请重试' : e.message + reject(errorMessage) }) } const handleNewFolder = (path: string, resolve: Function, reject: Function) => { - server - .newFolder(path) + withTimeout(server.newFolder(path), 10000) .then((_response) => { resolve() }) .catch((e: Error) => { - reject(e.message) + const errorMessage = e.message.includes('超时') ? '新建文件夹请求超时,请重试' : e.message + reject(errorMessage) }) } const handleRename = (path: string, newPath: string, resolve: () => void, reject: (msg?: string) => void) => { - server - .rename(path, newPath) + withTimeout(server.rename(path, newPath), 10000) .then((_response) => { resolve() }) .catch((e: Error) => { - reject(e.message) + const errorMessage = e.message.includes('超时') ? '重命名请求超时,请重试' : e.message + reject(errorMessage) }) } +// ================ 超时处理工具函数 ================= +const withTimeout = async (promise: Promise, timeoutMs: number = 10000): Promise => { + const abortController = new AbortController() + const timeoutId = setTimeout(() => abortController.abort(), timeoutMs) + + try { + const response = await Promise.race([ + promise, + new Promise((_, reject) => { + abortController.signal.addEventListener('abort', () => { + reject(new Error('请求超时')) + }) + }), + ]) + clearTimeout(timeoutId) + return response + } catch (e) { + clearTimeout(timeoutId) + throw e + } +} // ================ 自定义菜单 ================= const fileMenu = ref([{ label: '下载文件', value: 'download' }]) @@ -262,7 +283,10 @@ const downloadFile = (path: string) => { try { const content = file.content || '' - const fileName = path.split('\\').pop() || 'file' + + // 提取纯文件名(不包含路径) + let fileName = path.split(/[/\\]/).pop() || 'file' // 同时支持 / 和 \ 分隔符 + fileName = fileName.replace(/[:*?"<>|]/g, '_') // 移除Windows非法字符 // 创建Blob对象 const blob = new Blob([content], { type: 'text/plain;charset=utf-8' }) @@ -271,7 +295,7 @@ const downloadFile = (path: string) => { // 创建下载链接 const link = document.createElement('a') link.href = url - link.download = fileName + link.download = fileName // 只使用文件名 link.style.display = 'none' document.body.appendChild(link) @@ -442,16 +466,20 @@ const extractUrlParams = () => { try { const url = new URL(currentUrl) - const host = url.host // 提取host(包含端口号) + const protocol = url.protocol // http: 或 https: + const host = url.host // 主机地址(包含端口号) const id = url.searchParams.get('id') || '未找到ID' - const ws = url.searchParams.get('ws') || '未找到WS' - // 生成CC: Tweaked命令 - const ccTweakedCommand = `wget run http://${host}/Client/cc/main.lua ${ws} ${id}` + // 构建完整的HTTP地址(包含协议) + const httpAddress = `${protocol}//${host}` + + // 生成CC: Tweaked命令 - 使用HTTP地址而不是WebSocket + const ccTweakedCommand = `wget run http://${host}/Client/cc/main.lua ${httpAddress} ${id}` // 添加CC: Tweaked命令 commandManager.add('CC: Tweaked连接命令', ccTweakedCommand) + console.log('生成的命令:', ccTweakedCommand) return true } catch (error) { console.error('URL解析错误:', error) diff --git a/Frontend1/src/mock-server.ts b/Frontend1/src/mock-server.ts index 2658d20..9105605 100644 --- a/Frontend1/src/mock-server.ts +++ b/Frontend1/src/mock-server.ts @@ -1,19 +1,11 @@ import type { Files } from 'monaco-tree-editor' -// WebSocket 连接管理 -let ws: WebSocket | null = null let roomId: string | null = null -let wsServer: string | null = null -let isConnected = false -let clientId: string | null = null -let reconnectAttempts = 0 -let heartbeatInterval: number | null = null -const maxReconnectAttempts = 5 -const reconnectDelay = 2000 -const heartbeatIntervalMs = 30000 // 30秒发送一次心跳 -let isDisconnecting = false +let serverUrl: string | null = null +let pollIntervalMs = 1000 +let isPolling = false +let pollingTimeout: number | null = null -// 请求回调映射 const pendingRequests = new Map< string, { @@ -23,7 +15,6 @@ const pendingRequests = new Map< } >() -// 待处理初始请求队列 const pendingInitialRequests: Array<{ operation: string data?: any @@ -31,245 +22,97 @@ const pendingInitialRequests: Array<{ reject: (error: Error) => void }> = [] -// 生成唯一请求ID function generateRequestId(): string { return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` } -// 从URL获取房间ID和WebSocket服务器地址 -function getParamsFromUrl(): { roomId: string | null; wsServer: string | null } { +function getParamsFromUrl(): { roomId: string | null } { const urlParams = new URLSearchParams(window.location.search) const roomId = urlParams.get('id') - const wsServer = urlParams.get('ws') || urlParams.get('server') - - return { roomId, wsServer } + return { roomId } } -// 处理页面关闭前的清理 -function handleBeforeUnload() { - if (!isDisconnecting && isConnected && ws && ws.readyState === WebSocket.OPEN) { - isDisconnecting = true - try { - // 发送离开房间消息 - sendMessage({ - type: 'leave_room', - room_id: roomId, - client_id: clientId, - }) - } catch (error) { - console.error('发送离开消息失败:', error) +async function httpPost(path: string, data: any): Promise { + const url = `${serverUrl}${path}` + + try { + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }) + + if (!response.ok) { + throw new Error(`HTTP错误: ${response.status} ${response.statusText}`) } - // 断开连接 - disconnect() + return await response.json() + } catch (error) { + console.error('HTTP请求失败:', error) + throw error } } -// 初始化WebSocket连接 export async function initWebSocketConnection(): Promise { const params = getParamsFromUrl() roomId = params.roomId - wsServer = params.wsServer if (!roomId) { - throw new Error('未找到房间ID,请通过有效的URL访问(URL应包含?id=房间ID参数)') + throw new Error('未找到房间ID') } - // 如果没有提供ws服务器地址,使用默认值 - const serverUrl = (wsServer || 'ws://localhost:8081').replace(/^http/, 'ws') + serverUrl = window.location.origin + console.log('HTTP连接已初始化,服务器:', serverUrl) + console.log('房间ID:', roomId) - return new Promise((resolve, reject) => { - if (ws && ws.readyState === WebSocket.OPEN) { - resolve() - return - } - - // 创建WebSocket连接 - ws = new WebSocket(serverUrl) - - // 连接事件 - ws.onopen = () => { - isConnected = true - reconnectAttempts = 0 - console.log('WebSocket连接已建立,服务器:', serverUrl) - - // 启动心跳 - startHeartbeat() - - // 发送加入房间消息 - sendMessage({ - type: 'join_room', - room_id: roomId, - client_type: 'frontend', - }) - - // 添加页面关闭监听 - if (typeof window !== 'undefined') { - window.addEventListener('beforeunload', handleBeforeUnload) - } - } - - ws.onmessage = (event) => { - try { - const data = JSON.parse(event.data) - handleMessage(data) - } catch (error) { - console.error('消息解析错误:', error) - } - } - - ws.onclose = (event) => { - isConnected = false - console.log('WebSocket连接已断开:', event.code, event.reason) - - // 停止心跳 - stopHeartbeat() - - // 移除事件监听 - if (typeof window !== 'undefined') { - window.removeEventListener('beforeunload', handleBeforeUnload) - } - - // 拒绝所有待处理的请求 - for (const [requestId, { reject }] of pendingRequests) { - reject(new Error('WebSocket连接已断开')) - pendingRequests.delete(requestId) - } - - // 自动重连(除非是主动断开) - if (!isDisconnecting && reconnectAttempts < maxReconnectAttempts) { - setTimeout(() => { - reconnectAttempts++ - console.log(`尝试重新连接 (${reconnectAttempts}/${maxReconnectAttempts})`) - initWebSocketConnection().catch(console.error) - }, reconnectDelay) - } - } - - ws.onerror = (error) => { - console.error('WebSocket错误:', error) - reject(new Error('WebSocket连接失败')) - } - - // 设置连接超时 - setTimeout(() => { - if (!isConnected) { - reject(new Error('WebSocket连接超时')) - } - }, 10000) - }) + startPolling() + return Promise.resolve() } -// 启动心跳 -function startHeartbeat() { - stopHeartbeat() // 先停止可能存在的旧心跳 - - heartbeatInterval = setInterval(() => { - if (isConnected && ws && ws.readyState === WebSocket.OPEN) { - sendMessage({ - type: 'ping', - timestamp: new Date().toISOString(), - }) - } - }, heartbeatIntervalMs) +function startPolling() { + if (isPolling) return + isPolling = true + pollForResponses() } -// 停止心跳 -function stopHeartbeat() { - if (heartbeatInterval) { - clearInterval(heartbeatInterval) - heartbeatInterval = null +function stopPolling() { + isPolling = false + if (pollingTimeout) { + clearTimeout(pollingTimeout) + pollingTimeout = null + } +} + +async function pollForResponses() { + if (!isPolling || !roomId) return + + try { + const response = await httpPost('/api/frontend/receive', { + room_id: roomId, + }) + + if (response.success && response.message) { + handleMessage(response.message) + } + } catch (error) { + console.error('轮询消息失败:', error) + } + + if (isPolling) { + pollingTimeout = window.setTimeout(() => pollForResponses(), pollIntervalMs) } } -// 处理接收到的消息 function handleMessage(data: any): void { const messageType = data.type - switch (messageType) { - case 'connected': - clientId = data.client_id - console.log('连接成功:', data.message) - console.log('客户端ID:', clientId) - break - - case 'joined_room': - console.log('加入房间成功:', data.message) - console.log('客户端数量:', data.client_count) - - // 连接成功后处理所有待处理的初始请求 - processPendingInitialRequests() - break - - case 'file_operation_response': - handleFileOperationResponse(data) - break - - case 'user_joined': - console.log('新用户加入:', data.client_id, data.client_type) - - break - - case 'user_left': - console.log('用户离开:', data.client_id) - break - - case 'pong': - // 心跳响应,不需要处理 - break - - case 'error': - console.error('错误:', data.message) - // 处理文件操作错误 - if (data.requestId && pendingRequests.has(data.requestId)) { - const { reject, timeout } = pendingRequests.get(data.requestId)! - clearTimeout(timeout) - reject(new Error(data.message || '请求失败')) - pendingRequests.delete(data.requestId) - } - break - - default: - console.log('未知消息类型:', messageType, data) + if (messageType === 'file_operation_response') { + handleFileOperationResponse(data) } } -// 处理所有待处理的初始请求 -function processPendingInitialRequests() { - while (pendingInitialRequests.length > 0) { - const request = pendingInitialRequests.shift()! - const { operation, data, resolve, reject } = request - - try { - // 重新发送请求 - sendFileOperationInternal(operation, data).then(resolve).catch(reject) - } catch (error) { - reject(error instanceof Error ? error : new Error(String(error))) - } - } -} - -// 发送消息 -function sendMessage(message: any): void { - if (!isConnected || !ws || ws.readyState !== WebSocket.OPEN) { - if (message.type !== 'leave_room') { - // 离开房间消息可以在关闭时发送 - throw new Error('WebSocket未连接') - } - return - } - - try { - ws.send(JSON.stringify(message)) - } catch (error) { - if (message.type !== 'leave_room') { - // 忽略离开消息的发送错误 - throw new Error(`发送消息失败: ${error}`) - } - } -} - -// 处理文件操作响应 function handleFileOperationResponse(data: any): void { const requestId = data.requestId @@ -287,12 +130,11 @@ function handleFileOperationResponse(data: any): void { } } -// 内部发送文件操作请求(不处理连接状态) -function sendFileOperationInternal(operationType: string, data?: any, timeoutMs: number = 10000): Promise { +function sendFileOperationInternal(operationType: string, data?: any, timeoutMs: number = 30000): Promise { return new Promise((resolve, reject) => { const requestId = generateRequestId() - const timeout = setTimeout(() => { + const timeout = window.setTimeout(() => { if (pendingRequests.has(requestId)) { pendingRequests.delete(requestId) reject(new Error('请求超时')) @@ -301,26 +143,33 @@ function sendFileOperationInternal(operationType: string, data?: any, timeoutMs: pendingRequests.set(requestId, { resolve, reject, timeout }) - try { - sendMessage({ + httpPost('/api/frontend/send', { + room_id: roomId, + message: { type: 'file_operation', requestId: requestId, operation_type: operationType, data: data, room_id: roomId, + }, + }) + .then((response) => { + if (!response.success) { + pendingRequests.delete(requestId) + clearTimeout(timeout) + reject(new Error(response.message || '发送请求失败')) + } + }) + .catch((error) => { + pendingRequests.delete(requestId) + clearTimeout(timeout) + reject(error) }) - } catch (error) { - pendingRequests.delete(requestId) - clearTimeout(timeout) - reject(new Error(`发送请求失败: ${error}`)) - } }) } -// 发送文件操作请求(处理连接状态) -async function sendFileOperation(operationType: string, data?: any, timeoutMs: number = 10000): Promise { - if (!isConnected) { - // 如果未连接,将请求加入待处理队列 +async function sendFileOperation(operationType: string, data?: any, timeoutMs: number = 30000): Promise { + if (!roomId) { return new Promise((resolve, reject) => { pendingInitialRequests.push({ operation: operationType, @@ -328,150 +177,66 @@ async function sendFileOperation(operationType: string, data?: any, timeoutMs: n resolve, reject, }) - - // 如果还没有连接,尝试初始化连接 - if (!ws) { - initWebSocketConnection().catch((error) => { - console.error('初始化连接失败:', error) - reject(error instanceof Error ? error : new Error(String(error))) - }) - } + initWebSocketConnection().catch(reject) }) } return sendFileOperationInternal(operationType, data, timeoutMs) } - -// 文件操作函数 export const fetchFiles = async (): Promise => { - - try { - console.log('开始获取文件列表...') const filesData = await sendFileOperation('fetch_files') - console.log('成功获取文件列表,文件数量:', Object.keys(filesData).length) - console.log(filesData) return filesData } catch (error) { - console.error('获取文件列表失败:', error) throw new Error(`获取文件列表失败: ${error}`) } } export const createOrSaveFile = async (path: string, content: string) => { - - - try { - await sendFileOperation('create_or_save_file', { path, content }) - } catch (error) { - throw new Error(`保存文件失败: ${error}`) - } + await sendFileOperation('create_or_save_file', { path, content }) } export const newFile = async (path: string) => { - - - try { - await sendFileOperation('new_file', { path }) - } catch (error) { - throw new Error(`创建新文件失败: ${error}`) - } + await sendFileOperation('new_file', { path }) } export const newFolder = async (path: string) => { - - - try { - await sendFileOperation('new_folder', { path }) - } catch (error) { - throw new Error(`创建新文件夹失败: ${error}`) - } + await sendFileOperation('new_folder', { path }) } export const rename = async (path: string, newPath: string) => { - - try { - await sendFileOperation('rename', { path, newPath }) - return true - } catch (error) { - throw new Error(`重命名失败: ${error}`) - } + await sendFileOperation('rename', { path, newPath }) + return true } export const deleteFile = async (path: string) => { - - try { - await sendFileOperation('delete_file', { path }) - return true - } catch (error) { - throw new Error(`删除失败: ${error}`) - } + await sendFileOperation('delete_file', { path }) + return true } -// 工具函数 export const getConnectionStatus = () => ({ - isConnected, + isConnected: isPolling, roomId, - clientId, - wsServer, + serverUrl, }) export const disconnect = () => { - isDisconnecting = true - - // 发送离开房间消息 - if (ws && isConnected && ws.readyState === WebSocket.OPEN) { - try { - sendMessage({ - type: 'leave_room', - room_id: roomId, - client_id: clientId, - }) - } catch (error) { - console.error('发送离开消息失败:', error) - } - } - - if (ws) { - ws.close() - ws = null - } - - isConnected = false + stopPolling() roomId = null - wsServer = null - clientId = null - isDisconnecting = false - - // 停止心跳 - stopHeartbeat() - - // 清空待处理请求 + serverUrl = null pendingInitialRequests.length = 0 - - // 移除事件监听 - if (typeof window !== 'undefined') { - window.removeEventListener('beforeunload', handleBeforeUnload) - } } -export const getShareableUrl = (includeWs: boolean = true): string => { +export const getShareableUrl = (): string => { if (!roomId) { throw new Error('未加入任何房间') } - const currentUrl = new URL(window.location.href) currentUrl.searchParams.set('id', roomId) - - if (includeWs && wsServer) { - currentUrl.searchParams.set('ws', wsServer) - } - return currentUrl.toString() } -// 设置WebSocket服务器地址 -export const setWsServer = (serverUrl: string) => { - wsServer = serverUrl +export const setPollInterval = (intervalMs: number) => { + pollIntervalMs = intervalMs } diff --git a/PyServer/main.py b/PyServer/main.py index 0e6f453..96dde49 100644 --- a/PyServer/main.py +++ b/PyServer/main.py @@ -1,14 +1,11 @@ -import asyncio import json import logging import uuid import time import os -from datetime import datetime, timedelta -from typing import Dict, Any, Set -import threading +from datetime import datetime +from typing import Dict, Any, List from http.server import HTTPServer, BaseHTTPRequestHandler -import websockets from urllib.parse import parse_qs, urlparse import mimetypes import re @@ -19,9 +16,10 @@ logger = logging.getLogger(__name__) # 存储房间信息 rooms = {} -connected_clients = {} - -ws_port = 81 # ws服务外部端口 +# 前端到客户端的消息队列 +frontend_to_client_queues = {} +# 客户端到前端的消息队列 +client_to_frontend_queues = {} # 静态文件目录 STATIC_DIR = os.path.join(os.path.dirname(__file__), 'static') @@ -33,67 +31,33 @@ class Room: def __init__(self, room_id: str, server_host: str): self.room_id = room_id self.created_at = datetime.now() - self.last_activity = datetime.now() - self.clients: Set[str] = set() # 从host中移除端口号 host_without_port = re.sub(r':\d+$', '', server_host) - # 使用80端口 - self.ws_url = f"ws://{host_without_port}:{ws_port}" - self.frontend_url = f"http://{server_host}/?id={room_id}&ws={self.ws_url}" - - def add_client(self, client_id: str): - self.clients.add(client_id) - self.last_activity = datetime.now() - logger.info(f"客户端 {client_id} 加入房间 {self.room_id}, 当前客户端数: {len(self.clients)}") - - def remove_client(self, client_id: str): - if client_id in self.clients: - self.clients.remove(client_id) - self.last_activity = datetime.now() - logger.info(f"客户端 {client_id} 离开房间 {self.room_id}, 剩余客户端数: {len(self.clients)}") - - def is_empty(self) -> bool: - return len(self.clients) == 0 - + self.frontend_url = f"http://{server_host}/?id={room_id}" + def to_dict(self) -> Dict[str, Any]: return { 'room_id': self.room_id, 'frontend_url': self.frontend_url, - 'ws_url': self.ws_url, - 'client_count': len(self.clients), - 'created_at': self.created_at.isoformat(), - 'last_activity': self.last_activity.isoformat() + 'created_at': self.created_at.isoformat() } -def cleanup_empty_rooms(): - """定期清理空房间""" - while True: - time.sleep(300) # 每5分钟检查一次 - current_time = datetime.now() - empty_rooms = [] - - for room_id, room in list(rooms.items()): - if room.is_empty() and current_time - room.last_activity > timedelta(minutes=10): - empty_rooms.append(room_id) - - for room_id in empty_rooms: - if room_id in rooms: - del rooms[room_id] - logger.info(f"清理空房间: {room_id}") +def get_frontend_to_client_queue(room_id: str) -> List[Dict[str, Any]]: + if room_id not in frontend_to_client_queues: + frontend_to_client_queues[room_id] = [] + return frontend_to_client_queues[room_id] -# 启动清理线程 -cleanup_thread = threading.Thread(target=cleanup_empty_rooms, daemon=True) -cleanup_thread.start() +def get_client_to_frontend_queue(room_id: str) -> List[Dict[str, Any]]: + if room_id not in client_to_frontend_queues: + client_to_frontend_queues[room_id] = [] + return client_to_frontend_queues[room_id] class HTTPHandler(BaseHTTPRequestHandler): def do_GET(self): """处理HTTP GET请求""" try: - logger.info(f"收到GET请求: {self.path} from {self.client_address[0]}") - logger.info(f"请求头: {dict(self.headers)}") - parsed_path = urlparse(self.path) path = parsed_path.path query_params = parse_qs(parsed_path.query) @@ -119,13 +83,22 @@ class HTTPHandler(BaseHTTPRequestHandler): def do_POST(self): """处理HTTP POST请求""" try: - logger.info(f"收到POST请求: {self.path} from {self.client_address[0]}") + content_length = int(self.headers.get('Content-Length', 0)) + post_data = self.rfile.read(content_length) if content_length > 0 else b'{}' parsed_path = urlparse(self.path) path = parsed_path.path if path == '/api/room': - self.handle_create_room() + self.handle_create_room(post_data) + elif path == '/api/frontend/send': + self.handle_frontend_send_message(post_data) + elif path == '/api/frontend/receive': + self.handle_frontend_receive_message(post_data) + elif path == '/api/client/send': + self.handle_client_send_message(post_data) + elif path == '/api/client/receive': + self.handle_client_receive_message(post_data) else: self.send_error(404, "Not Found") except Exception as e: @@ -135,7 +108,6 @@ class HTTPHandler(BaseHTTPRequestHandler): def handle_root_path(self, query_params: Dict[str, Any]): """处理根路径请求""" room_id = query_params.get('id', [None])[0] - ws_url = query_params.get('ws', [None])[0] if room_id: # 有房间ID参数,直接返回前端页面 @@ -157,8 +129,8 @@ class HTTPHandler(BaseHTTPRequestHandler): logger.info(f"通过根路径创建新房间: {room_id}") - # 重定向到带房间ID和WebSocket URL的URL - redirect_url = f'/?id={room_id}&ws=ws://{host_without_port}:{ws_port}' + # 重定向到带房间ID的URL + redirect_url = f'/?id={room_id}' self.send_response(302) self.send_header('Location', redirect_url) self.end_headers() @@ -169,14 +141,11 @@ class HTTPHandler(BaseHTTPRequestHandler): def handle_static_file(self, path: str): """处理静态文件请求""" - logger.info(f"处理静态文件请求: {path}") - # 安全检查:防止路径遍历攻击 if '..' in path: - logger.warning(f"检测到可疑路径: {path}") self.send_error(403, "Forbidden: Path traversal not allowed") return - + # 规范化路径 if path == '/': path = '/index.html' @@ -188,29 +157,20 @@ class HTTPHandler(BaseHTTPRequestHandler): # 构建完整文件路径 full_path = os.path.join(STATIC_DIR, file_path) - logger.info(f"尝试访问文件: {full_path}") # 如果是目录,尝试查找index.html if os.path.isdir(full_path): index_path = os.path.join(full_path, 'index.html') if os.path.exists(index_path): full_path = index_path - logger.info(f"重定向到目录索引文件: {index_path}") else: - logger.warning(f"目录不存在索引文件: {full_path}") self.send_error(404, "Directory index not found") return # 检查文件是否存在且是普通文件 - if not os.path.exists(full_path): - logger.warning(f"文件不存在: {full_path}") + if not os.path.exists(full_path) or not os.path.isfile(full_path): self.send_error(404, f"File not found: {path}") return - - if not os.path.isfile(full_path): - logger.warning(f"路径不是文件: {full_path}") - self.send_error(403, "Not a file") - return try: # 读取文件内容 @@ -222,21 +182,15 @@ class HTTPHandler(BaseHTTPRequestHandler): if mime_type is None: mime_type = 'application/octet-stream' - logger.info(f"成功读取文件: {full_path}, 大小: {len(content)} bytes, MIME类型: {mime_type}") - # 发送响应头 self.send_response(200) self.send_header('Content-Type', mime_type) self.send_header('Content-Length', str(len(content))) - - # 添加缓存控制头(可选) - self.send_header('Cache-Control', 'public, max-age=3600') # 缓存1小时 - + self.send_header('Cache-Control', 'public, max-age=3600') self.end_headers() # 发送文件内容 self.wfile.write(content) - logger.info(f"文件发送完成: {full_path}") except Exception as e: logger.error(f"读取或发送文件失败: {e}") @@ -246,7 +200,7 @@ class HTTPHandler(BaseHTTPRequestHandler): """服务静态文件(内部方法)""" self.handle_static_file(path) - def handle_create_room(self): + def handle_create_room(self, post_data=None): """创建新房间""" try: # 生成唯一房间ID @@ -265,8 +219,7 @@ class HTTPHandler(BaseHTTPRequestHandler): response = { 'success': True, 'room_id': room_id, - 'frontend_url': room.frontend_url, - 'ws_url': room.ws_url + 'frontend_url': room.frontend_url } self.send_response(200) @@ -279,37 +232,23 @@ class HTTPHandler(BaseHTTPRequestHandler): logger.error(f"创建房间失败: {e}") self.send_error(500, str(e)) - def handle_get_room(self, room_id: str): - """获取房间信息""" + def handle_frontend_send_message(self, post_data): + """前端发送消息到客户端""" try: - if room_id not in rooms: - self.send_error(404, '房间不存在') + data = json.loads(post_data.decode('utf-8')) + room_id = data.get('room_id') + message = data.get('message') + + if not room_id or not message: + self.send_error(400, "需要room_id和message参数") return - room = rooms[room_id] - response = {'success': True, 'data': room.to_dict()} - - self.send_response(200) - self.send_header('Content-Type', 'application/json') - self.send_header('Access-Control-Allow-Origin', '*') - self.end_headers() - self.wfile.write(json.dumps(response).encode()) - - except Exception as e: - logger.error(f"获取房间信息失败: {e}") - self.send_error(500, str(e)) - - def handle_list_rooms(self): - """列出所有活跃房间""" - try: - active_rooms = [room.to_dict() for room in rooms.values() if not room.is_empty()] + queue = get_frontend_to_client_queue(room_id) + queue.append(message) response = { 'success': True, - 'data': { - 'total_rooms': len(active_rooms), - 'rooms': active_rooms - } + 'message': '消息已发送到客户端队列' } self.send_response(200) @@ -319,7 +258,108 @@ class HTTPHandler(BaseHTTPRequestHandler): self.wfile.write(json.dumps(response).encode()) except Exception as e: - logger.error(f"获取房间列表失败: {e}") + logger.error(f"前端发送消息失败: {e}") + self.send_error(500, str(e)) + + def handle_frontend_receive_message(self, post_data): + """前端接收来自客户端的消息""" + try: + data = json.loads(post_data.decode('utf-8')) + room_id = data.get('room_id') + + if not room_id: + self.send_error(400, "需要room_id参数") + return + + queue = get_client_to_frontend_queue(room_id) + + if queue: + # 返回队列中的第一个消息 + message = queue.pop(0) + response = { + 'success': True, + 'message': message + } + else: + # 没有消息 + response = { + 'success': True, + 'message': None + } + + self.send_response(200) + self.send_header('Content-Type', 'application/json') + self.send_header('Access-Control-Allow-Origin', '*') + self.end_headers() + self.wfile.write(json.dumps(response).encode()) + + except Exception as e: + logger.error(f"前端接收消息失败: {e}") + self.send_error(500, str(e)) + + def handle_client_send_message(self, post_data): + """客户端发送消息到前端""" + try: + data = json.loads(post_data.decode('utf-8')) + room_id = data.get('room_id') + message = data.get('message') + + if not room_id or not message: + self.send_error(400, "需要room_id和message参数") + return + + queue = get_client_to_frontend_queue(room_id) + queue.append(message) + + response = { + 'success': True, + 'message': '消息已发送到前端队列' + } + + self.send_response(200) + self.send_header('Content-Type', 'application/json') + self.send_header('Access-Control-Allow-Origin', '*') + self.end_headers() + self.wfile.write(json.dumps(response).encode()) + + except Exception as e: + logger.error(f"客户端发送消息失败: {e}") + self.send_error(500, str(e)) + + def handle_client_receive_message(self, post_data): + """客户端接收来自前端的消息""" + try: + data = json.loads(post_data.decode('utf-8')) + room_id = data.get('room_id') + + if not room_id: + self.send_error(400, "需要room_id参数") + return + + queue = get_frontend_to_client_queue(room_id) + + if queue: + # 返回队列中的第一个消息 + message = queue.pop(0) + response = { + 'success': True, + 'message': message + } + else: + # 没有消息 + response = { + 'success': True, + 'message': None + } + + self.send_response(200) + self.send_header('Content-Type', 'application/json') + self.send_header('Access-Control-Allow-Origin', '*') + self.end_headers() + self.wfile.write(json.dumps(response).encode()) + + except Exception as e: + logger.error(f"客户端接收消息失败: {e}") self.send_error(500, str(e)) def log_message(self, format, *args): @@ -328,300 +368,15 @@ class HTTPHandler(BaseHTTPRequestHandler): self.log_date_time_string(), format % args)) -class WebSocketHTTPRequestHandler(HTTPHandler): - """支持WebSocket升级的HTTP请求处理器""" - - def do_GET(self): - """处理GET请求,支持WebSocket升级""" - if self.headers.get('Upgrade') == 'websocket': - self.handle_websocket_upgrade() - else: - super().do_GET() - - def handle_websocket_upgrade(self): - """处理WebSocket升级请求""" - # 这里我们只是记录日志,实际的WebSocket处理在websockets库中完成 - logger.info(f"WebSocket连接请求: {self.path}") - self.send_error(426, "WebSocket upgrade required") # 这个错误不会被触发,因为websockets库会拦截请求 - -async def handle_websocket(websocket): - """处理WebSocket连接 - 修复版本,移除了path参数""" - client_id = str(uuid.uuid4()) - room_id = None - client_type = 'unknown' - - try: - # 发送连接成功消息 - await send_message(websocket, { - 'type': 'connected', - 'message': '连接成功', - 'client_id': client_id, - 'timestamp': datetime.now().isoformat() - }) - - logger.info(f"客户端连接: {client_id}") - - # 处理消息 - async for message in websocket: - try: - data = json.loads(message) - # 处理消息并获取房间和客户端类型信息 - result = await handle_websocket_message(websocket, client_id, data) - if result: - room_id, client_type = result - except json.JSONDecodeError: - logger.error(f"消息格式错误: {message}") - except Exception as e: - logger.error(f"处理消息错误: {e}") - - except websockets.exceptions.ConnectionClosed: - logger.info(f"客户端断开连接: {client_id}") - finally: - # 清理连接 - if client_id in connected_clients: - info = connected_clients.pop(client_id) - room_id = info.get('room_id') - logger.info(f"清理客户端: {client_id}, 房间: {room_id}") - - if room_id and room_id in rooms: - room = rooms[room_id] - room.remove_client(client_id) - - # 通知房间内其他客户端 - await broadcast_to_room(room_id, { - 'type': 'user_left', - 'client_id': client_id, - 'message': '用户离开房间', - 'room_size': len(room.clients), - 'timestamp': datetime.now().isoformat() - }, exclude_client=client_id) - - # 额外检查:即使不在 connected_clients 中,如果还在房间里也要移除 - elif room_id and room_id in rooms: - room = rooms[room_id] - room.remove_client(client_id) - - # 通知房间内其他客户端 - await broadcast_to_room(room_id, { - 'type': 'user_left', - 'client_id': client_id, - 'message': '用户离开房间', - 'room_size': len(room.clients), - 'timestamp': datetime.now().isoformat() - }, exclude_client=client_id) - -async def handle_websocket_message(websocket, client_id: str, data: Dict[str, Any]): - """处理WebSocket消息""" - message_type = data.get('type') - if message_type == 'join_room': - return await handle_join_room(websocket, client_id, data) - elif message_type == 'file_operation': - await handle_file_operation(websocket, client_id, data) - elif message_type == 'file_operation_response': - await handle_file_operation_response(websocket, client_id, data) - elif message_type == 'leave_room': - await handle_leave_room(websocket, client_id, data) - elif message_type == 'ping': - await send_message(websocket, { - 'type': 'pong', - 'message': 'pong', - 'timestamp': datetime.now().isoformat() - }) - else: - logger.warning(f"未知消息类型: {message_type}") - -async def handle_join_room(websocket, client_id: str, data: Dict[str, Any]): - """处理加入房间请求""" - room_id = data.get('room_id') - client_type = data.get('client_type', 'unknown') - - if not room_id: - await send_message(websocket, { - 'type': 'error', - 'message': '房间ID不能为空' - }) - return - - if room_id not in rooms: - await send_message(websocket, { - 'type': 'error', - 'message': '房间不存在' - }) - return - - room = rooms[room_id] - room.add_client(client_id) - - # 存储客户端信息 - connected_clients[client_id] = { - 'websocket': websocket, - 'room_id': room_id, - 'client_type': client_type - } - - logger.info(f"客户端 {client_id} ({client_type}) 加入房间 {room_id}") - - # 发送加入成功消息 - await send_message(websocket, { - 'type': 'joined_room', - 'room_id': room_id, - 'message': f'成功加入房间 {room_id}', - 'client_count': len(room.clients), - 'timestamp': datetime.now().isoformat() - }) - - # 通知房间内其他客户端 - await broadcast_to_room(room_id, { - 'type': 'user_joined', - 'client_id': client_id, - 'client_type': client_type, - 'message': '新用户加入房间', - 'room_size': len(room.clients), - 'timestamp': datetime.now().isoformat() - }, exclude_client=client_id) - - # 返回房间ID和客户端类型,用于finally块清理 - return room_id, client_type - -async def handle_leave_room(websocket, client_id: str, data: Dict[str, Any]): - """处理离开房间请求""" - room_id = data.get('room_id') - - if not room_id: - room_id = connected_clients.get(client_id, {}).get('room_id') - - if room_id and room_id in rooms: - room = rooms[room_id] - room.remove_client(client_id) - - # 从连接客户端中移除 - if client_id in connected_clients: - del connected_clients[client_id] - - # 通知房间内其他客户端 - await broadcast_to_room(room_id, { - 'type': 'user_left', - 'client_id': client_id, - 'message': '用户离开房间', - 'room_size': len(room.clients), - 'timestamp': datetime.now().isoformat() - }, exclude_client=client_id) - - logger.info(f"客户端 {client_id} 主动离开房间 {room_id}") - -async def handle_file_operation(websocket, client_id: str, data: Dict[str, Any]): - """处理文件操作请求""" - room_id = data.get('room_id') - request_id = data.get('requestId') - operation_type = data.get('operation_type') - - if not room_id or room_id not in rooms: - await send_message(websocket, { - 'type': 'error', - 'message': '无效的房间ID', - 'requestId': request_id - }) - return - - # 记录操作 - logger.info(f"收到文件操作请求: {operation_type} from {client_id} in room {room_id}") - - # 查找文件客户端 - file_clients = [] - for cid, client_info in connected_clients.items(): - if client_info['room_id'] == room_id and cid != client_id: - if client_info.get('client_type') == 'file_client': - file_clients.append(client_info['websocket']) - - if file_clients: - # 转发给文件客户端 - data['sender_id'] = client_id - await send_message(file_clients[0], { - 'type': 'file_operation_request', - **data - }) - else: - # 没有文件客户端,返回错误 - await send_message(websocket, { - 'type': 'file_operation_response', - 'requestId': request_id, - 'success': False, - 'error': '房间内没有文件客户端', - 'timestamp': datetime.now().isoformat() - }) - -async def handle_file_operation_response(websocket, client_id: str, data: Dict[str, Any]): - """处理文件操作响应""" - request_id = data.get('requestId') - target_client_id = data.get('target_client_id') - - logger.info(f"收到文件操作响应: {request_id} -> {target_client_id}") - - if target_client_id and target_client_id in connected_clients: - # 发送给指定客户端 - target_websocket = connected_clients[target_client_id]['websocket'] - await send_message(target_websocket, { - 'type': 'file_operation_response', - **data - }) - else: - logger.error("文件操作响应没有指定目标客户端或目标客户端不存在") - -async def send_message(websocket, message: Dict[str, Any]): - """发送消息到WebSocket""" - try: - await websocket.send(json.dumps(message)) - except websockets.exceptions.ConnectionClosed: - logger.warning("连接已关闭,无法发送消息") - -async def broadcast_to_room(room_id: str, message: Dict[str, Any], exclude_client: str = None): - """向房间内所有客户端广播消息""" - if room_id not in rooms: - return - - room = rooms[room_id] - for client_id in room.clients: - if client_id != exclude_client and client_id in connected_clients: - try: - await send_message(connected_clients[client_id]['websocket'], message) - except Exception as e: - logger.error(f"广播消息失败: {e}") - def run_http_server(): """运行HTTP服务器""" try: - server = HTTPServer(('0.0.0.0', 80), WebSocketHTTPRequestHandler) + server = HTTPServer(('0.0.0.0', 80), HTTPHandler) logger.info("HTTP服务器启动在端口 80") server.serve_forever() except Exception as e: logger.error(f"HTTP服务器启动失败: {e}") -async def run_websocket_server(): - """运行WebSocket服务器""" - try: - # 使用新的函数签名,不传递path参数 - server = await websockets.serve(handle_websocket, '0.0.0.0', 81) - logger.info("WebSocket服务器启动在端口 81") - await server.wait_closed() - except Exception as e: - logger.error(f"WebSocket服务器启动失败: {e}") - -async def main(): - """主函数""" - # 启动HTTP服务器(在单独线程中) - http_thread = threading.Thread(target=run_http_server, daemon=True) - http_thread.start() - - # 启动WebSocket服务器 - await run_websocket_server() - if __name__ == '__main__': logger.info("启动服务器...") - - # 检查端口权限 - try: - asyncio.run(main()) - except PermissionError: - logger.error("需要管理员权限才能绑定到80端口") - logger.info("尝试使用8080端口...") - # 可以在这里添加回退到其他端口的逻辑 \ No newline at end of file + run_http_server() diff --git a/README.md b/README.md index 431ef37..3f59cb2 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ 一个为 ComputerCraft: Tweaked (CC:Tweaked) 设计的远程文件编辑器,支持实时文件管理和代码编辑。 -Demo : http://cc-web-edit.liulikeji.cn +Demo: http://cc-web-edit.liulikeji.cn 你可以直接使用 Demo,这是开放的,但你也可以部署自己的服务器 @@ -12,7 +12,7 @@ Demo : http://cc-web-edit.liulikeji.cn - **远程文件管理**:实时浏览、编辑和管理 CC:Tweaked 计算机中的文件 - **Monaco 编辑器**:基于 VS Code 的 Monaco 编辑器,提供专业的代码编辑体验 -- **WebSocket 通信**:低延迟的双向通信,确保操作的实时性 +- **HTTP 通信**:基于 HTTP 协议的可靠通信 ### 文件操作 @@ -27,7 +27,7 @@ Demo : http://cc-web-edit.liulikeji.cn - **自动命令生成**:根据 URL 参数自动生成连接命令 - **一键复制**:点击即可复制连接命令到剪贴板 - **房间管理**:支持创建和加入房间 -- **心跳保活**:自动维持连接稳定性 +- **轮询机制**:HTTP 轮询确保连接稳定性 ## 🚀 快速开始 @@ -44,7 +44,7 @@ Demo : http://cc-web-edit.liulikeji.cn ├── Frontend1/ # Vue 前端项目 │ ├── src/ │ │ ├── App.vue # 主组件 -│ │ └── mock-server.ts # WebSocket 客户端 +│ │ └── mock-server.ts # HTTP 客户端 │ └── package.json └── Client/ # 客户端文件 └── main.lua # CC:Tweaked 客户端脚本 @@ -73,8 +73,7 @@ python main.py 服务器将启动: -- HTTP 服务:端口 80(文件服务和 API) -- WebSocket 服务:端口 81(实时通信) +- HTTP 服务:端口 80(文件服务、API 和静态资源) 2. **构建前端项目** @@ -106,7 +105,7 @@ cp -r dist/* ../PyServer/static/ ```lua # 粘贴复制的命令到CC:Tweaked计算机 -# 命令格式类似:wget run http://服务器地址/Client/cc/main.lua ws://服务器ws地址 房间ID +# 命令格式类似:wget run http://服务器地址/Client/cc/main.lua http://服务器地址 房间ID ``` 3. **刷新文件列表** @@ -128,6 +127,7 @@ cp -r dist/* ../PyServer/static/ - **二进制文件**:非文本文件会显示为 `[binary]`,无法在线编辑 - **单客户端**:目前主要支持一个网页端和一个 CC 客户端的配对使用 - **文件大小**:上传文件限制为 1MB +- **轮询延迟**:HTTP 轮询机制可能有轻微延迟(默认 2 秒) ### 计划功能 @@ -136,12 +136,18 @@ cp -r dist/* ../PyServer/static/ ## ⚙️ API 接口 -### WebSocket 消息类型 +### HTTP API 接口 + +- `POST /api/room` - 创建房间 +- `POST /api/frontend/send` - 前端发送消息到客户端 +- `POST /api/frontend/receive` - 前端接收来自客户端的消息 +- `POST /api/client/send` - 客户端发送消息到前端 +- `POST /api/client/receive` - 客户端接收来自前端的消息 + +### 消息类型 -- `join_room` - 加入房间 - `file_operation` - 文件操作请求 - `file_operation_response` - 文件操作响应 -- `ping/pong` - 心跳检测 ### 文件操作类型 @@ -180,15 +186,21 @@ A: 确保 CC 客户端已成功连接,然后刷新文件列表 **Q: 文件上传失败** A: 检查文件大小是否超过 1MB 限制 +**Q: 操作响应较慢** +A: 默认轮询间隔为 1 秒,可通过调整代码中的轮询间隔改善 + ## 📄 技术说明 -- **后端**:Python + WebSocket +- **后端**:Python + HTTP Server - **前端**:Vue 3 + TypeScript + Monaco Editor -- **通信**:WebSocket 实时双向通信 -- **客户端**:CC:Tweaked Lua 脚本 +- **通信**:HTTP 轮询机制实现双向通信 +- **客户端**:CC:Tweaked + HTTP ## 🤝 开发说明 +<<<<<<< HEAD +该项目目前主要支持远程代码编辑功能,使用 HTTP 协议替代 WebSocket,提高了兼容性和部署便利性。远程控制台功能计划在后续版本中开发。 +======= 该项目目前主要支持远程代码编辑功能,远程控制台功能计划在后续版本中开发。 ## 贡献 @@ -197,3 +209,4 @@ A: 检查文件大小是否超过 1MB 限制 欢迎提交issues +>>>>>>> d3faa4b74bc0eeac9a272c4d8a348d98a48dad7e