更改ws为http
This commit is contained in:
321
Client/main.lua
321
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,26 +107,37 @@ 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)
|
||||
return (path:gsub("^computer/", ""):gsub("^computer\\", ""))
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
local responseBody = response.readAll()
|
||||
response.close()
|
||||
|
||||
local ok, result = pcall(textutils.unserialiseJSON, responseBody)
|
||||
if ok then
|
||||
return result
|
||||
else
|
||||
return nil, "无效的JSON响应"
|
||||
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 function isLikelyText(data)
|
||||
for i = 1, math.min(#data, 1024) do
|
||||
local b = data:byte(i)
|
||||
if b < 32 and not (b == 9 or b == 10 or b == 13) then
|
||||
if b < 32 and b ~= 9 and b ~= 10 and b ~= 13 then
|
||||
return false
|
||||
end
|
||||
end
|
||||
@@ -137,20 +145,15 @@ local function isLikelyText(data, maxCheck)
|
||||
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 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
|
||||
local nextPath = currentPath == "" and entry or (currentPath .. "/" .. entry)
|
||||
getFiles(nextPath, result, prefix)
|
||||
getFiles(currentPath == "" and entry or (currentPath .. "/" .. entry), result, prefix)
|
||||
end
|
||||
end
|
||||
else
|
||||
@@ -159,12 +162,9 @@ local function getFiles(currentPath, result, prefix)
|
||||
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
|
||||
@@ -172,12 +172,12 @@ end
|
||||
|
||||
local function fetchFiles()
|
||||
local files = {}
|
||||
getFiles("", files, "computer/")
|
||||
getFiles("", files, "computer_" .. computerID .. "/")
|
||||
return files
|
||||
end
|
||||
|
||||
local function saveFile(path, content)
|
||||
path = cleanPath(path)
|
||||
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")
|
||||
@@ -186,7 +186,7 @@ local function saveFile(path, content)
|
||||
end
|
||||
|
||||
local function createFile(path)
|
||||
path = cleanPath(path)
|
||||
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
|
||||
@@ -196,249 +196,192 @@ local function createFile(path)
|
||||
end
|
||||
|
||||
local function createFolder(path)
|
||||
path = cleanPath(path)
|
||||
path = cleanPath(path):gsub("^computer[" .. computerID .. "_]*/", "")
|
||||
fs.makeDir(path)
|
||||
end
|
||||
|
||||
local function renameFile(oldPath, newPath)
|
||||
oldPath = cleanPath(oldPath)
|
||||
newPath = cleanPath(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)
|
||||
path = cleanPath(path):gsub("^computer[" .. computerID .. "_]*/", "")
|
||||
if fs.exists(path) then
|
||||
fs.delete(path)
|
||||
end
|
||||
end
|
||||
|
||||
-- ========== 异步任务处理器 ==========
|
||||
local function handleFetchFiles(ws, reqId, sender)
|
||||
-- ========== 消息处理函数 ==========
|
||||
local function handleFetchFiles(reqId, sender)
|
||||
local success, result = pcall(fetchFiles)
|
||||
sendJson(ws, {
|
||||
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
|
||||
})
|
||||
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, {
|
||||
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
|
||||
})
|
||||
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, {
|
||||
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
|
||||
})
|
||||
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, {
|
||||
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
|
||||
})
|
||||
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, {
|
||||
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
|
||||
})
|
||||
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, {
|
||||
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
|
||||
})
|
||||
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 服务器创建房间")
|
||||
local result, err = httpPost("/api/room", {})
|
||||
if not result then
|
||||
error("无法创建房间: " .. tostring(err))
|
||||
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
|
||||
return result.room_id
|
||||
end
|
||||
|
||||
local function joinRoom(roomId)
|
||||
log("正在加入房间: " .. roomId)
|
||||
local function sendResponse(response)
|
||||
if response and roomId then
|
||||
httpPost("/api/client/send", {
|
||||
room_id = roomId,
|
||||
message = response
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
-- 构建 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 连接")
|
||||
local function pollMessages()
|
||||
while true do
|
||||
if not roomId then
|
||||
sleep(pollInterval)
|
||||
break
|
||||
end
|
||||
|
||||
-- 加入房间
|
||||
sendJson(ws, {
|
||||
type = "join_room",
|
||||
room_id = roomId,
|
||||
client_type = "file_client"
|
||||
local result, err = httpPost("/api/client/receive", {
|
||||
room_id = roomId
|
||||
})
|
||||
|
||||
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
|
||||
if result and result.success and result.message then
|
||||
local msg = result.message
|
||||
local msgType = msg.type
|
||||
log("收到消息: " .. tostring(msgType))
|
||||
|
||||
if msgType == "connected" then
|
||||
log("分配的客户端 ID: " .. tostring(msg.client_id))
|
||||
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
|
||||
|
||||
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
|
||||
local response
|
||||
|
||||
-- 派发到后台线程
|
||||
if op == "fetch_files" then
|
||||
mainFrame:addThread():start(function () handleFetchFiles(ws, reqId, sender) end )
|
||||
response = handleFetchFiles(reqId, sender)
|
||||
elseif op == "create_or_save_file" then
|
||||
mainFrame:addThread():start(function () handleSaveFile(ws, data, reqId, sender) end)
|
||||
response = handleSaveFile(data, reqId, sender)
|
||||
elseif op == "new_file" then
|
||||
mainFrame:addThread():start(function () handleCreateFile(ws, data, reqId, sender) end)
|
||||
response = handleCreateFile(data, reqId, sender)
|
||||
elseif op == "new_folder" then
|
||||
mainFrame:addThread():start(function () handleCreateFolder(ws, data, reqId, sender) end)
|
||||
response = handleCreateFolder(data, reqId, sender)
|
||||
elseif op == "rename" then
|
||||
mainFrame:addThread():start(function () handleRename(ws, data, reqId, sender) end)
|
||||
response = handleRename(data, reqId, sender)
|
||||
elseif op == "delete_file" then
|
||||
mainFrame:addThread():start(function () handleDelete(ws, data, reqId, sender) end)
|
||||
response = handleDelete(data, reqId, sender)
|
||||
else
|
||||
-- 同步返回未知操作错误(轻量)
|
||||
sendJson(ws, {
|
||||
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()
|
||||
if not roomId then
|
||||
roomId = createRoom()
|
||||
log("创建新房间: " .. roomId)
|
||||
else
|
||||
log("无效 JSON: " .. tostring(payload))
|
||||
log("使用现有房间: " .. roomId)
|
||||
end
|
||||
end
|
||||
end
|
||||
end,
|
||||
function ()
|
||||
|
||||
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 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
|
||||
local event, param1 = os.pullEvent()
|
||||
if event == "key" and param1 == keys.q then
|
||||
log("用户按 Q 退出")
|
||||
break
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
ws.close()
|
||||
log("客户端已停止。房间ID: " .. finalRoomId)
|
||||
end
|
||||
|
||||
-- 启动主逻辑和Basalt事件循环
|
||||
|
||||
@@ -157,8 +157,7 @@ const fileInputRef = ref<HTMLInputElement>()
|
||||
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 <T>(promise: Promise<T>, timeoutMs: number = 10000): Promise<T> => {
|
||||
const abortController = new AbortController()
|
||||
const timeoutId = setTimeout(() => abortController.abort(), timeoutMs)
|
||||
|
||||
try {
|
||||
const response = await Promise.race([
|
||||
promise,
|
||||
new Promise<T>((_, 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)
|
||||
|
||||
@@ -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
|
||||
async function httpPost(path: string, data: any): Promise<any> {
|
||||
const url = `${serverUrl}${path}`
|
||||
|
||||
try {
|
||||
// 发送离开房间消息
|
||||
sendMessage({
|
||||
type: 'leave_room',
|
||||
room_id: roomId,
|
||||
client_id: clientId,
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('发送离开消息失败:', error)
|
||||
|
||||
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<void> {
|
||||
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
|
||||
startPolling()
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
function startPolling() {
|
||||
if (isPolling) return
|
||||
isPolling = true
|
||||
pollForResponses()
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
isPolling = false
|
||||
if (pollingTimeout) {
|
||||
clearTimeout(pollingTimeout)
|
||||
pollingTimeout = null
|
||||
}
|
||||
}
|
||||
|
||||
// 创建WebSocket连接
|
||||
ws = new WebSocket(serverUrl)
|
||||
async function pollForResponses() {
|
||||
if (!isPolling || !roomId) return
|
||||
|
||||
// 连接事件
|
||||
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)
|
||||
const response = await httpPost('/api/frontend/receive', {
|
||||
room_id: roomId,
|
||||
})
|
||||
|
||||
if (response.success && response.message) {
|
||||
handleMessage(response.message)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('消息解析错误:', 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)
|
||||
})
|
||||
}
|
||||
|
||||
// 启动心跳
|
||||
function startHeartbeat() {
|
||||
stopHeartbeat() // 先停止可能存在的旧心跳
|
||||
|
||||
heartbeatInterval = setInterval(() => {
|
||||
if (isConnected && ws && ws.readyState === WebSocket.OPEN) {
|
||||
sendMessage({
|
||||
type: 'ping',
|
||||
timestamp: new Date().toISOString(),
|
||||
})
|
||||
}
|
||||
}, heartbeatIntervalMs)
|
||||
}
|
||||
|
||||
// 停止心跳
|
||||
function stopHeartbeat() {
|
||||
if (heartbeatInterval) {
|
||||
clearInterval(heartbeatInterval)
|
||||
heartbeatInterval = null
|
||||
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':
|
||||
if (messageType === '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)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理所有待处理的初始请求
|
||||
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<any> {
|
||||
function sendFileOperationInternal(operationType: string, data?: any, timeoutMs: number = 30000): Promise<any> {
|
||||
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,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
.then((response) => {
|
||||
if (!response.success) {
|
||||
pendingRequests.delete(requestId)
|
||||
clearTimeout(timeout)
|
||||
reject(new Error(`发送请求失败: ${error}`))
|
||||
reject(new Error(response.message || '发送请求失败'))
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
pendingRequests.delete(requestId)
|
||||
clearTimeout(timeout)
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 发送文件操作请求(处理连接状态)
|
||||
async function sendFileOperation(operationType: string, data?: any, timeoutMs: number = 10000): Promise<any> {
|
||||
if (!isConnected) {
|
||||
// 如果未连接,将请求加入待处理队列
|
||||
async function sendFileOperation(operationType: string, data?: any, timeoutMs: number = 30000): Promise<any> {
|
||||
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<Files> => {
|
||||
|
||||
|
||||
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}`)
|
||||
}
|
||||
}
|
||||
|
||||
export const newFile = async (path: string) => {
|
||||
|
||||
|
||||
try {
|
||||
await sendFileOperation('new_file', { path })
|
||||
} catch (error) {
|
||||
throw new Error(`创建新文件失败: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
export const newFolder = async (path: string) => {
|
||||
|
||||
|
||||
try {
|
||||
await sendFileOperation('new_folder', { path })
|
||||
} catch (error) {
|
||||
throw new Error(`创建新文件夹失败: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
export const rename = async (path: string, newPath: string) => {
|
||||
|
||||
try {
|
||||
await sendFileOperation('rename', { path, newPath })
|
||||
return true
|
||||
} catch (error) {
|
||||
throw new Error(`重命名失败: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteFile = async (path: string) => {
|
||||
|
||||
try {
|
||||
await sendFileOperation('delete_file', { path })
|
||||
return true
|
||||
} catch (error) {
|
||||
throw new Error(`删除失败: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
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
|
||||
}
|
||||
|
||||
541
PyServer/main.py
541
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 = []
|
||||
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]
|
||||
|
||||
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}")
|
||||
|
||||
# 启动清理线程
|
||||
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,11 +141,8 @@ 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
|
||||
|
||||
@@ -188,30 +157,21 @@ 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:
|
||||
# 读取文件内容
|
||||
with open(full_path, 'rb') as f:
|
||||
@@ -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端口...")
|
||||
# 可以在这里添加回退到其他端口的逻辑
|
||||
run_http_server()
|
||||
|
||||
39
README.md
39
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
|
||||
|
||||
Reference in New Issue
Block a user