-- 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 -- 心跳间隔(秒) -- ========== 加载 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 框架,请检查网络或使用本地缓存") end local basalt = load(basaltResp.readAll())() basaltResp.close() local mainFrame = basalt.createFrame() -- ========== 工具函数 ========== local function log(msg) --basalt.debug("[FileClient] " .. tostring(msg)) end function table_to_json(t, indent) indent = indent or 0 local spaces = string.rep(" ", indent) local result = {} if type(t) ~= "table" then if type(t) == "string" then -- 正确转义所有特殊字符 local escaped = t:gsub("[\\\"\b\f\n\r\t]", function(c) local replacements = { ['\\'] = '\\\\', ['"'] = '\\"', ['\b'] = '\\b', ['\f'] = '\\f', ['\n'] = '\\n', ['\r'] = '\\r', ['\t'] = '\\t' } return replacements[c] end) return '"' .. escaped .. '"' elseif type(t) == "number" or type(t) == "boolean" then return tostring(t) else return '"' .. tostring(t) .. '"' end end -- 检查是否是数组 local is_array = true local max_index = 0 local count = 0 for k, v in pairs(t) do count = count + 1 if type(k) ~= "number" or k <= 0 or math.floor(k) ~= k then is_array = false end if type(k) == "number" and k > max_index then max_index = k end end -- 空表当作对象处理 if count == 0 then is_array = false end if is_array then -- 处理数组 table.insert(result, "[") local items = {} for i = 1, max_index do if t[i] ~= nil then table.insert(items, table_to_json(t[i], indent + 2)) else table.insert(items, "null") end end table.insert(result, table.concat(items, ", ")) table.insert(result, "]") else -- 处理对象 table.insert(result, "{") local items = {} for k, v in pairs(t) do local key = '"' .. tostring(k) .. '"' local value = table_to_json(v, indent + 2) if indent > 0 then table.insert(items, spaces .. " " .. key .. ": " .. value) else table.insert(items, key .. ":" .. value) end end if indent > 0 then table.insert(result, table.concat(items, ",\n")) table.insert(result, "\n" .. spaces .. "}") else table.insert(result, table.concat(items, ",")) table.insert(result, "}") end end return table.concat(result, indent > 0 and "\n" .. spaces or "") 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 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 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 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 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() 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 end local function createFolder(path) path = cleanPath(path) fs.makeDir(path) end local function renameFile(oldPath, newPath) oldPath = cleanPath(oldPath) newPath = cleanPath(newPath) fs.move(oldPath, newPath) end local function deleteFile(path) path = cleanPath(path) 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")) 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")) 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")) 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")) 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")) 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")) 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 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 -- 加入房间 sendJson(ws, { type = "join_room", room_id = roomId, client_type = "file_client" }) return ws, roomId 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 -- 启动心跳 local heartbeatTimer = os.startTimer(heartbeatInterval) mainFrame:addProgram():execute(function () shell.run("shell") end):setSize("parent.w","parent.h") -- 消息循环 parallel.waitForAll( function () sleep(0.5) os.queueEvent("mouse_click",1,1,1) 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)) 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)