更改ws为http

This commit is contained in:
nnwang
2025-12-05 19:02:19 +08:00
parent d3faa4b74b
commit 258bc8915a
5 changed files with 554 additions and 1050 deletions

View File

@@ -1,18 +1,15 @@
-- simple_file_client.lua -- simple_file_client.lua
-- 支持创建房间、WebSocket 连接、异步文件操作、心跳保活
-- 使用 Basalt 实现非阻塞异步任务
local args = {...} local args = {...}
local httpServer = args[1] or "http://192.168.2.200:8080" local httpServer = args[1] or "http://192.168.2.200:8080"
local roomId = args[2] -- 可选的房间ID参数 local roomId = args[2]
local wsPort = 8081 local pollInterval = 1
local heartbeatInterval = 1 -- 心跳间隔(秒) local computerID = tostring(os.computerID() or "unknown")
-- ========== 加载 Basalt ========== -- ========== 加载 Basalt ==========
local basaltUrl = "https://git.liulikeji.cn/GitHub/Basalt/releases/download/v1.7/basalt.lua" local basaltUrl = "https://git.liulikeji.cn/GitHub/Basalt/releases/download/v1.7/basalt.lua"
local basaltResp = http.get(basaltUrl) local basaltResp = http.get(basaltUrl)
if not basaltResp then if not basaltResp then
error("无法下载 Basalt 框架,请检查网络或使用本地缓存") error("无法下载 Basalt 框架")
end end
local basalt = load(basaltResp.readAll())() local basalt = load(basaltResp.readAll())()
basaltResp.close() basaltResp.close()
@@ -110,26 +107,37 @@ function table_to_json(t, indent)
end end
local function cleanPath(path) local function cleanPath(path)
if string.sub(path, 1, 9) == "computer/" then return (path:gsub("^computer/", ""):gsub("^computer\\", ""))
return string.sub(path, 10)
elseif string.sub(path, 1, 9) == "computer\\" then
return string.sub(path, 10)
end
return path
end end
local function sendJson(ws, obj) local function httpPost(path, data)
local payload = table_to_json(obj) local jsonData = table_to_json(data)
ws.send(payload) local url = httpServer .. path
local response = http.post(url, jsonData, {
["Content-Type"] = "application/json"
})
if not response then
return nil, "无法连接到服务器"
end end
-- ========== 文件系统操作(纯逻辑,无网络)========== local responseBody = response.readAll()
local function isLikelyText(data, maxCheck) response.close()
if not data then return false end
maxCheck = maxCheck or math.min(#data, 1024) local ok, result = pcall(textutils.unserialiseJSON, responseBody)
for i = 1, maxCheck do 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) 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 return false
end end
end end
@@ -137,20 +145,15 @@ local function isLikelyText(data, maxCheck)
end end
local function getFiles(currentPath, result, prefix) local function getFiles(currentPath, result, prefix)
local fullPrefix local computerPrefix = "computer_" .. computerID
if currentPath == "" then local fullPrefix = currentPath == "" and prefix:sub(1, -2) or prefix .. currentPath
fullPrefix = prefix:sub(1, -2) -- 移除末尾的斜杠,将 "computer/" 变为 "computer"
else
fullPrefix = prefix .. currentPath
end
local absPath = "/" .. (currentPath == "" and "" or currentPath) local absPath = "/" .. (currentPath == "" and "" or currentPath)
if fs.isDir(absPath) then if fs.isDir(absPath) then
result[fullPrefix] = { isFolder = true } result[fullPrefix] = { isFolder = true }
for _, entry in ipairs(fs.list(absPath)) do for _, entry in ipairs(fs.list(absPath)) do
if entry ~= "rom" then if entry ~= "rom" then
local nextPath = currentPath == "" and entry or (currentPath .. "/" .. entry) getFiles(currentPath == "" and entry or (currentPath .. "/" .. entry), result, prefix)
getFiles(nextPath, result, prefix)
end end
end end
else else
@@ -159,12 +162,9 @@ local function getFiles(currentPath, result, prefix)
if ok and handle then if ok and handle then
local data = handle.readAll() local data = handle.readAll()
handle.close() handle.close()
log(absPath.." File size: " .. #data)
if data and isLikelyText(data) then if data and isLikelyText(data) then
content = data content = data
log(absPath .. " is text file")
end end
if content == "[binary]" then print("binary file: " .. absPath) end
end end
result[fullPrefix] = { isFile = true, content = content } result[fullPrefix] = { isFile = true, content = content }
end end
@@ -172,12 +172,12 @@ end
local function fetchFiles() local function fetchFiles()
local files = {} local files = {}
getFiles("", files, "computer/") getFiles("", files, "computer_" .. computerID .. "/")
return files return files
end end
local function saveFile(path, content) local function saveFile(path, content)
path = cleanPath(path) path = cleanPath(path):gsub("^computer[" .. computerID .. "_]*/", "")
local dir = fs.getDir(path) local dir = fs.getDir(path)
if not fs.exists(dir) then fs.makeDir(dir) end if not fs.exists(dir) then fs.makeDir(dir) end
local f = fs.open(path, "w") local f = fs.open(path, "w")
@@ -186,7 +186,7 @@ local function saveFile(path, content)
end end
local function createFile(path) local function createFile(path)
path = cleanPath(path) path = cleanPath(path):gsub("^computer[" .. computerID .. "_]*/", "")
local dir = fs.getDir(path) local dir = fs.getDir(path)
if not fs.exists(dir) then fs.makeDir(dir) end if not fs.exists(dir) then fs.makeDir(dir) end
if not fs.exists(path) then if not fs.exists(path) then
@@ -196,249 +196,192 @@ local function createFile(path)
end end
local function createFolder(path) local function createFolder(path)
path = cleanPath(path) path = cleanPath(path):gsub("^computer[" .. computerID .. "_]*/", "")
fs.makeDir(path) fs.makeDir(path)
end end
local function renameFile(oldPath, newPath) local function renameFile(oldPath, newPath)
oldPath = cleanPath(oldPath) oldPath = cleanPath(oldPath):gsub("^computer[" .. computerID .. "_]*/", "")
newPath = cleanPath(newPath) newPath = cleanPath(newPath):gsub("^computer[" .. computerID .. "_]*/", "")
fs.move(oldPath, newPath) fs.move(oldPath, newPath)
end end
local function deleteFile(path) local function deleteFile(path)
path = cleanPath(path) path = cleanPath(path):gsub("^computer[" .. computerID .. "_]*/", "")
if fs.exists(path) then if fs.exists(path) then
fs.delete(path) fs.delete(path)
end end
end end
-- ========== 异步任务处理器 ========== -- ========== 消息处理函数 ==========
local function handleFetchFiles(ws, reqId, sender) local function handleFetchFiles(reqId, sender)
local success, result = pcall(fetchFiles) local success, result = pcall(fetchFiles)
sendJson(ws, { return {
type = "file_operation_response", type = "file_operation_response",
requestId = reqId, requestId = reqId,
success = success, success = success,
data = success and result or nil, data = success and result or nil,
error = success and nil or tostring(result), error = success and nil or tostring(result),
target_client_id = sender target_client_id = sender
}) }
log("Async fetch_files completed: " .. (success and "OK" or "FAILED"))
end end
local function handleSaveFile(ws, data, reqId, sender) local function handleSaveFile(data, reqId, sender)
local success, err = pcall(function() local success, err = pcall(saveFile, data.path, data.content)
assert(data.path, "Missing path") return {
saveFile(data.path, data.content)
end)
sendJson(ws, {
type = "file_operation_response", type = "file_operation_response",
requestId = reqId, requestId = reqId,
success = success, success = success,
error = success and nil or tostring(err), error = success and nil or tostring(err),
target_client_id = sender target_client_id = sender
}) }
log("Async save_file completed: " .. (success and "OK" or "FAILED"))
end end
local function handleCreateFile(ws, data, reqId, sender) local function handleCreateFile(data, reqId, sender)
local success, err = pcall(function() local success, err = pcall(createFile, data.path)
assert(data.path, "Missing path") return {
createFile(data.path)
end)
sendJson(ws, {
type = "file_operation_response", type = "file_operation_response",
requestId = reqId, requestId = reqId,
success = success, success = success,
error = success and nil or tostring(err), error = success and nil or tostring(err),
target_client_id = sender target_client_id = sender
}) }
log("Async create_file completed: " .. (success and "OK" or "FAILED"))
end end
local function handleCreateFolder(ws, data, reqId, sender) local function handleCreateFolder(data, reqId, sender)
local success, err = pcall(function() local success, err = pcall(createFolder, data.path)
assert(data.path, "Missing path") return {
createFolder(data.path)
end)
sendJson(ws, {
type = "file_operation_response", type = "file_operation_response",
requestId = reqId, requestId = reqId,
success = success, success = success,
error = success and nil or tostring(err), error = success and nil or tostring(err),
target_client_id = sender target_client_id = sender
}) }
log("Async create_folder completed: " .. (success and "OK" or "FAILED"))
end end
local function handleRename(ws, data, reqId, sender) local function handleRename(data, reqId, sender)
local success, err = pcall(function() local success, err = pcall(renameFile, data.path, data.newPath)
assert(data.path and data.newPath, "Missing path or newPath") return {
renameFile(data.path, data.newPath)
end)
sendJson(ws, {
type = "file_operation_response", type = "file_operation_response",
requestId = reqId, requestId = reqId,
success = success, success = success,
error = success and nil or tostring(err), error = success and nil or tostring(err),
target_client_id = sender target_client_id = sender
}) }
log("Async rename completed: " .. (success and "OK" or "FAILED"))
end end
local function handleDelete(ws, data, reqId, sender) local function handleDelete(data, reqId, sender)
log("delete:"..data.path) local success, err = pcall(deleteFile, data.path)
local success, err = pcall(function() return {
assert(data.path, "Missing path")
deleteFile(data.path)
end)
sendJson(ws, {
type = "file_operation_response", type = "file_operation_response",
requestId = reqId, requestId = reqId,
success = success, success = success,
error = success and nil or tostring(err), error = success and nil or tostring(err),
target_client_id = sender target_client_id = sender
}) }
log("Async delete completed: " .. (success and "OK" or "FAILED"))
end end
-- ========== 房间管理函数 ========== -- ========== 房间管理函数 ==========
local function createRoom() local function createRoom()
log("正在创建房间...") local result, err = httpPost("/api/room", {})
local roomUrl = httpServer .. "/api/room" if not result then
local roomResp = http.post(roomUrl, "{}") error("无法创建房间: " .. tostring(err))
if not roomResp then end
error("无法连接到 HTTP 服务器创建房间") return result.room_id
end end
local body = roomResp.readAll() local function sendResponse(response)
roomResp.close() if response and roomId then
httpPost("/api/client/send", {
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, room_id = roomId,
client_type = "file_client" message = response
})
end
end
local function pollMessages()
while true do
if not roomId then
sleep(pollInterval)
break
end
local result, err = httpPost("/api/client/receive", {
room_id = roomId
}) })
return ws, roomId if result and result.success and result.message then
end local msg = result.message
-- ========== 主函数 ==========
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 local msgType = msg.type
log("收到消息: " .. tostring(msgType))
if msgType == "connected" then if msgType == "file_operation" or msgType == "file_operation_request" then
log("分配的客户端 ID: " .. tostring(msg.client_id)) 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 local response
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 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 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 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 elseif op == "new_folder" then
mainFrame:addThread():start(function () handleCreateFolder(ws, data, reqId, sender) end) response = handleCreateFolder(data, reqId, sender)
elseif op == "rename" then elseif op == "rename" then
mainFrame:addThread():start(function () handleRename(ws, data, reqId, sender) end) response = handleRename(data, reqId, sender)
elseif op == "delete_file" then elseif op == "delete_file" then
mainFrame:addThread():start(function () handleDelete(ws, data, reqId, sender) end) response = handleDelete(data, reqId, sender)
else else
-- 同步返回未知操作错误(轻量) response = {
sendJson(ws, {
type = "file_operation_response", type = "file_operation_response",
requestId = reqId, requestId = reqId,
success = false, success = false,
error = "Unknown operation: " .. tostring(op), error = "Unknown operation: " .. tostring(op),
target_client_id = sender target_client_id = sender
}) }
end
sendResponse(response)
end
elseif err then
log("轮询错误: " .. tostring(err))
end
sleep(pollInterval)
end end
end end
-- ========== 主函数 ==========
local function main()
if not roomId then
roomId = createRoom()
log("创建新房间: " .. roomId)
else else
log("无效 JSON: " .. tostring(payload)) log("使用现有房间: " .. roomId)
end end
end
end mainFrame:addProgram():execute(function()
end, shell.run("shell")
function () 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 while true do
local event, param1, param2, param3 = os.pullEvent() local event, param1 = os.pullEvent()
if event == "key" and param1 == keys.q then
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 退出") log("用户按 Q 退出")
break break
end end
end end
end)
ws.close()
log("客户端已停止。房间ID: " .. finalRoomId)
end end
-- 启动主逻辑和Basalt事件循环 -- 启动主逻辑和Basalt事件循环

View File

@@ -157,8 +157,7 @@ const fileInputRef = ref<HTMLInputElement>()
const monacoEditorRef = ref() const monacoEditorRef = ref()
const handleReload = (resolve: () => void, reject: (msg?: string) => void) => { const handleReload = (resolve: () => void, reject: (msg?: string) => void) => {
server withTimeout(server.fetchFiles(), 10000)
.fetchFiles()
.then((response) => { .then((response) => {
files.value = response files.value = response
nextTick(() => { nextTick(() => {
@@ -168,75 +167,97 @@ const handleReload = (resolve: () => void, reject: (msg?: string) => void) => {
resolve() resolve()
}) })
.catch((e: Error) => { .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) => { const handleSaveFile = (path: string, content: string, resolve: () => void, reject: (msg?: string) => void) => {
server withTimeout(server.createOrSaveFile(path, content), 10000)
.createOrSaveFile(path, content)
.then((_response) => { .then((_response) => {
resolve() resolve()
}) })
.catch((e: Error) => { .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) => { const handleDeleteFile = (path: string, resolve: () => void, reject: (msg?: string) => void) => {
server withTimeout(server.deleteFile(path), 10000)
.deleteFile(path)
.then((_response) => { .then((_response) => {
resolve() resolve()
}) })
.catch((e: Error) => { .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) => { const handleDeleteFolder = (path: string, resolve: () => void, reject: (msg?: string) => void) => {
server withTimeout(server.deleteFile(path), 10000)
.deleteFile(path)
.then((_response) => { .then((_response) => {
resolve() resolve()
}) })
.catch((e: Error) => { .catch((e: Error) => {
reject(e.message) const errorMessage = e.message.includes('超时') ? '删除文件夹请求超时,请重试' : e.message
reject(errorMessage)
}) })
} }
const handleNewFile = (path: string, resolve: Function, reject: Function) => { const handleNewFile = (path: string, resolve: Function, reject: Function) => {
server withTimeout(server.newFile(path), 10000)
.newFile(path)
.then((_response) => { .then((_response) => {
resolve() resolve()
}) })
.catch((e: Error) => { .catch((e: Error) => {
reject(e.message) const errorMessage = e.message.includes('超时') ? '新建文件请求超时,请重试' : e.message
reject(errorMessage)
}) })
} }
const handleNewFolder = (path: string, resolve: Function, reject: Function) => { const handleNewFolder = (path: string, resolve: Function, reject: Function) => {
server withTimeout(server.newFolder(path), 10000)
.newFolder(path)
.then((_response) => { .then((_response) => {
resolve() resolve()
}) })
.catch((e: Error) => { .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) => { const handleRename = (path: string, newPath: string, resolve: () => void, reject: (msg?: string) => void) => {
server withTimeout(server.rename(path, newPath), 10000)
.rename(path, newPath)
.then((_response) => { .then((_response) => {
resolve() resolve()
}) })
.catch((e: Error) => { .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' }]) const fileMenu = ref([{ label: '下载文件', value: 'download' }])
@@ -262,7 +283,10 @@ const downloadFile = (path: string) => {
try { try {
const content = file.content || '' const content = file.content || ''
const fileName = path.split('\\').pop() || 'file'
// 提取纯文件名(不包含路径)
let fileName = path.split(/[/\\]/).pop() || 'file' // 同时支持 / 和 \ 分隔符
fileName = fileName.replace(/[:*?"<>|]/g, '_') // 移除Windows非法字符
// 创建Blob对象 // 创建Blob对象
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' }) const blob = new Blob([content], { type: 'text/plain;charset=utf-8' })
@@ -271,7 +295,7 @@ const downloadFile = (path: string) => {
// 创建下载链接 // 创建下载链接
const link = document.createElement('a') const link = document.createElement('a')
link.href = url link.href = url
link.download = fileName link.download = fileName // 只使用文件名
link.style.display = 'none' link.style.display = 'none'
document.body.appendChild(link) document.body.appendChild(link)
@@ -442,16 +466,20 @@ const extractUrlParams = () => {
try { try {
const url = new URL(currentUrl) 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 id = url.searchParams.get('id') || '未找到ID'
const ws = url.searchParams.get('ws') || '未找到WS'
// 生成CC: Tweaked命令 // 构建完整的HTTP地址包含协议
const ccTweakedCommand = `wget run http://${host}/Client/cc/main.lua ${ws} ${id}` const httpAddress = `${protocol}//${host}`
// 生成CC: Tweaked命令 - 使用HTTP地址而不是WebSocket
const ccTweakedCommand = `wget run http://${host}/Client/cc/main.lua ${httpAddress} ${id}`
// 添加CC: Tweaked命令 // 添加CC: Tweaked命令
commandManager.add('CC: Tweaked连接命令', ccTweakedCommand) commandManager.add('CC: Tweaked连接命令', ccTweakedCommand)
console.log('生成的命令:', ccTweakedCommand)
return true return true
} catch (error) { } catch (error) {
console.error('URL解析错误:', error) console.error('URL解析错误:', error)

View File

@@ -1,19 +1,11 @@
import type { Files } from 'monaco-tree-editor' import type { Files } from 'monaco-tree-editor'
// WebSocket 连接管理
let ws: WebSocket | null = null
let roomId: string | null = null let roomId: string | null = null
let wsServer: string | null = null let serverUrl: string | null = null
let isConnected = false let pollIntervalMs = 1000
let clientId: string | null = null let isPolling = false
let reconnectAttempts = 0 let pollingTimeout: number | null = null
let heartbeatInterval: number | null = null
const maxReconnectAttempts = 5
const reconnectDelay = 2000
const heartbeatIntervalMs = 30000 // 30秒发送一次心跳
let isDisconnecting = false
// 请求回调映射
const pendingRequests = new Map< const pendingRequests = new Map<
string, string,
{ {
@@ -23,7 +15,6 @@ const pendingRequests = new Map<
} }
>() >()
// 待处理初始请求队列
const pendingInitialRequests: Array<{ const pendingInitialRequests: Array<{
operation: string operation: string
data?: any data?: any
@@ -31,245 +22,97 @@ const pendingInitialRequests: Array<{
reject: (error: Error) => void reject: (error: Error) => void
}> = [] }> = []
// 生成唯一请求ID
function generateRequestId(): string { function generateRequestId(): string {
return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
} }
// 从URL获取房间ID和WebSocket服务器地址 function getParamsFromUrl(): { roomId: string | null } {
function getParamsFromUrl(): { roomId: string | null; wsServer: string | null } {
const urlParams = new URLSearchParams(window.location.search) const urlParams = new URLSearchParams(window.location.search)
const roomId = urlParams.get('id') const roomId = urlParams.get('id')
const wsServer = urlParams.get('ws') || urlParams.get('server') return { roomId }
return { roomId, wsServer }
} }
// 处理页面关闭前的清理 async function httpPost(path: string, data: any): Promise<any> {
function handleBeforeUnload() { const url = `${serverUrl}${path}`
if (!isDisconnecting && isConnected && ws && ws.readyState === WebSocket.OPEN) {
isDisconnecting = true
try { try {
// 发送离开房间消息 const response = await fetch(url, {
sendMessage({ method: 'POST',
type: 'leave_room', headers: {
room_id: roomId, 'Content-Type': 'application/json',
client_id: clientId, },
body: JSON.stringify(data),
}) })
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status} ${response.statusText}`)
}
return await response.json()
} catch (error) { } catch (error) {
console.error('发送离开消息失败:', error) console.error('HTTP请求失败:', error)
} throw error
// 断开连接
disconnect()
} }
} }
// 初始化WebSocket连接
export async function initWebSocketConnection(): Promise<void> { export async function initWebSocketConnection(): Promise<void> {
const params = getParamsFromUrl() const params = getParamsFromUrl()
roomId = params.roomId roomId = params.roomId
wsServer = params.wsServer
if (!roomId) { if (!roomId) {
throw new Error('未找到房间ID请通过有效的URL访问URL应包含?id=房间ID参数') throw new Error('未找到房间ID')
} }
// 如果没有提供ws服务器地址使用默认值 serverUrl = window.location.origin
const serverUrl = (wsServer || 'ws://localhost:8081').replace(/^http/, 'ws') console.log('HTTP连接已初始化服务器:', serverUrl)
console.log('房间ID:', roomId)
return new Promise((resolve, reject) => { startPolling()
if (ws && ws.readyState === WebSocket.OPEN) { return Promise.resolve()
resolve()
return
} }
// 创建WebSocket连接 function startPolling() {
ws = new WebSocket(serverUrl) if (isPolling) return
isPolling = true
pollForResponses()
}
// 连接事件 function stopPolling() {
ws.onopen = () => { isPolling = false
isConnected = true if (pollingTimeout) {
reconnectAttempts = 0 clearTimeout(pollingTimeout)
console.log('WebSocket连接已建立服务器:', serverUrl) pollingTimeout = null
// 启动心跳
startHeartbeat()
// 发送加入房间消息
sendMessage({
type: 'join_room',
room_id: roomId,
client_type: 'frontend',
})
// 添加页面关闭监听
if (typeof window !== 'undefined') {
window.addEventListener('beforeunload', handleBeforeUnload)
} }
} }
ws.onmessage = (event) => { async function pollForResponses() {
if (!isPolling || !roomId) return
try { try {
const data = JSON.parse(event.data) const response = await httpPost('/api/frontend/receive', {
handleMessage(data) room_id: roomId,
})
if (response.success && response.message) {
handleMessage(response.message)
}
} catch (error) { } catch (error) {
console.error('消息解析错误:', error) console.error('轮询消息失败:', error)
}
if (isPolling) {
pollingTimeout = window.setTimeout(() => pollForResponses(), pollIntervalMs)
} }
} }
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
}
}
// 处理接收到的消息
function handleMessage(data: any): void { function handleMessage(data: any): void {
const messageType = data.type const messageType = data.type
switch (messageType) { if (messageType === 'file_operation_response') {
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) 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 { function handleFileOperationResponse(data: any): void {
const requestId = data.requestId const requestId = data.requestId
@@ -287,12 +130,11 @@ function handleFileOperationResponse(data: any): void {
} }
} }
// 内部发送文件操作请求(不处理连接状态) function sendFileOperationInternal(operationType: string, data?: any, timeoutMs: number = 30000): Promise<any> {
function sendFileOperationInternal(operationType: string, data?: any, timeoutMs: number = 10000): Promise<any> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const requestId = generateRequestId() const requestId = generateRequestId()
const timeout = setTimeout(() => { const timeout = window.setTimeout(() => {
if (pendingRequests.has(requestId)) { if (pendingRequests.has(requestId)) {
pendingRequests.delete(requestId) pendingRequests.delete(requestId)
reject(new Error('请求超时')) reject(new Error('请求超时'))
@@ -301,26 +143,33 @@ function sendFileOperationInternal(operationType: string, data?: any, timeoutMs:
pendingRequests.set(requestId, { resolve, reject, timeout }) pendingRequests.set(requestId, { resolve, reject, timeout })
try { httpPost('/api/frontend/send', {
sendMessage({ room_id: roomId,
message: {
type: 'file_operation', type: 'file_operation',
requestId: requestId, requestId: requestId,
operation_type: operationType, operation_type: operationType,
data: data, data: data,
room_id: roomId, room_id: roomId,
},
}) })
} catch (error) { .then((response) => {
if (!response.success) {
pendingRequests.delete(requestId) pendingRequests.delete(requestId)
clearTimeout(timeout) 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 = 30000): Promise<any> {
async function sendFileOperation(operationType: string, data?: any, timeoutMs: number = 10000): Promise<any> { if (!roomId) {
if (!isConnected) {
// 如果未连接,将请求加入待处理队列
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
pendingInitialRequests.push({ pendingInitialRequests.push({
operation: operationType, operation: operationType,
@@ -328,150 +177,66 @@ async function sendFileOperation(operationType: string, data?: any, timeoutMs: n
resolve, resolve,
reject, reject,
}) })
initWebSocketConnection().catch(reject)
// 如果还没有连接,尝试初始化连接
if (!ws) {
initWebSocketConnection().catch((error) => {
console.error('初始化连接失败:', error)
reject(error instanceof Error ? error : new Error(String(error)))
})
}
}) })
} }
return sendFileOperationInternal(operationType, data, timeoutMs) return sendFileOperationInternal(operationType, data, timeoutMs)
} }
// 文件操作函数
export const fetchFiles = async (): Promise<Files> => { export const fetchFiles = async (): Promise<Files> => {
try { try {
console.log('开始获取文件列表...')
const filesData = await sendFileOperation('fetch_files') const filesData = await sendFileOperation('fetch_files')
console.log('成功获取文件列表,文件数量:', Object.keys(filesData).length)
console.log(filesData)
return filesData return filesData
} catch (error) { } catch (error) {
console.error('获取文件列表失败:', error)
throw new Error(`获取文件列表失败: ${error}`) throw new Error(`获取文件列表失败: ${error}`)
} }
} }
export const createOrSaveFile = async (path: string, content: string) => { export const createOrSaveFile = async (path: string, content: string) => {
try {
await sendFileOperation('create_or_save_file', { path, content }) await sendFileOperation('create_or_save_file', { path, content })
} catch (error) {
throw new Error(`保存文件失败: ${error}`)
}
} }
export const newFile = async (path: string) => { export const newFile = async (path: string) => {
try {
await sendFileOperation('new_file', { path }) await sendFileOperation('new_file', { path })
} catch (error) {
throw new Error(`创建新文件失败: ${error}`)
}
} }
export const newFolder = async (path: string) => { export const newFolder = async (path: string) => {
try {
await sendFileOperation('new_folder', { path }) await sendFileOperation('new_folder', { path })
} catch (error) {
throw new Error(`创建新文件夹失败: ${error}`)
}
} }
export const rename = async (path: string, newPath: string) => { export const rename = async (path: string, newPath: string) => {
try {
await sendFileOperation('rename', { path, newPath }) await sendFileOperation('rename', { path, newPath })
return true return true
} catch (error) {
throw new Error(`重命名失败: ${error}`)
}
} }
export const deleteFile = async (path: string) => { export const deleteFile = async (path: string) => {
try {
await sendFileOperation('delete_file', { path }) await sendFileOperation('delete_file', { path })
return true return true
} catch (error) {
throw new Error(`删除失败: ${error}`)
}
} }
// 工具函数
export const getConnectionStatus = () => ({ export const getConnectionStatus = () => ({
isConnected, isConnected: isPolling,
roomId, roomId,
clientId, serverUrl,
wsServer,
}) })
export const disconnect = () => { export const disconnect = () => {
isDisconnecting = true stopPolling()
// 发送离开房间消息
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
roomId = null roomId = null
wsServer = null serverUrl = null
clientId = null
isDisconnecting = false
// 停止心跳
stopHeartbeat()
// 清空待处理请求
pendingInitialRequests.length = 0 pendingInitialRequests.length = 0
// 移除事件监听
if (typeof window !== 'undefined') {
window.removeEventListener('beforeunload', handleBeforeUnload)
}
} }
export const getShareableUrl = (includeWs: boolean = true): string => { export const getShareableUrl = (): string => {
if (!roomId) { if (!roomId) {
throw new Error('未加入任何房间') throw new Error('未加入任何房间')
} }
const currentUrl = new URL(window.location.href) const currentUrl = new URL(window.location.href)
currentUrl.searchParams.set('id', roomId) currentUrl.searchParams.set('id', roomId)
if (includeWs && wsServer) {
currentUrl.searchParams.set('ws', wsServer)
}
return currentUrl.toString() return currentUrl.toString()
} }
// 设置WebSocket服务器地址 export const setPollInterval = (intervalMs: number) => {
export const setWsServer = (serverUrl: string) => { pollIntervalMs = intervalMs
wsServer = serverUrl
} }

View File

@@ -1,14 +1,11 @@
import asyncio
import json import json
import logging import logging
import uuid import uuid
import time import time
import os import os
from datetime import datetime, timedelta from datetime import datetime
from typing import Dict, Any, Set from typing import Dict, Any, List
import threading
from http.server import HTTPServer, BaseHTTPRequestHandler from http.server import HTTPServer, BaseHTTPRequestHandler
import websockets
from urllib.parse import parse_qs, urlparse from urllib.parse import parse_qs, urlparse
import mimetypes import mimetypes
import re import re
@@ -19,9 +16,10 @@ logger = logging.getLogger(__name__)
# 存储房间信息 # 存储房间信息
rooms = {} rooms = {}
connected_clients = {} # 前端到客户端的消息队列
frontend_to_client_queues = {}
ws_port = 81 # ws服务外部端口 # 客户端到前端的消息队列
client_to_frontend_queues = {}
# 静态文件目录 # 静态文件目录
STATIC_DIR = os.path.join(os.path.dirname(__file__), 'static') 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): def __init__(self, room_id: str, server_host: str):
self.room_id = room_id self.room_id = room_id
self.created_at = datetime.now() self.created_at = datetime.now()
self.last_activity = datetime.now()
self.clients: Set[str] = set()
# 从host中移除端口号 # 从host中移除端口号
host_without_port = re.sub(r':\d+$', '', server_host) host_without_port = re.sub(r':\d+$', '', server_host)
# 使用80端口 self.frontend_url = f"http://{server_host}/?id={room_id}"
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
def to_dict(self) -> Dict[str, Any]: def to_dict(self) -> Dict[str, Any]:
return { return {
'room_id': self.room_id, 'room_id': self.room_id,
'frontend_url': self.frontend_url, 'frontend_url': self.frontend_url,
'ws_url': self.ws_url, 'created_at': self.created_at.isoformat()
'client_count': len(self.clients),
'created_at': self.created_at.isoformat(),
'last_activity': self.last_activity.isoformat()
} }
def cleanup_empty_rooms(): def get_frontend_to_client_queue(room_id: str) -> List[Dict[str, Any]]:
"""定期清理空房间""" if room_id not in frontend_to_client_queues:
while True: frontend_to_client_queues[room_id] = []
time.sleep(300) # 每5分钟检查一次 return frontend_to_client_queues[room_id]
current_time = datetime.now()
empty_rooms = []
for room_id, room in list(rooms.items()): def get_client_to_frontend_queue(room_id: str) -> List[Dict[str, Any]]:
if room.is_empty() and current_time - room.last_activity > timedelta(minutes=10): if room_id not in client_to_frontend_queues:
empty_rooms.append(room_id) client_to_frontend_queues[room_id] = []
return client_to_frontend_queues[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()
class HTTPHandler(BaseHTTPRequestHandler): class HTTPHandler(BaseHTTPRequestHandler):
def do_GET(self): def do_GET(self):
"""处理HTTP GET请求""" """处理HTTP GET请求"""
try: try:
logger.info(f"收到GET请求: {self.path} from {self.client_address[0]}")
logger.info(f"请求头: {dict(self.headers)}")
parsed_path = urlparse(self.path) parsed_path = urlparse(self.path)
path = parsed_path.path path = parsed_path.path
query_params = parse_qs(parsed_path.query) query_params = parse_qs(parsed_path.query)
@@ -119,13 +83,22 @@ class HTTPHandler(BaseHTTPRequestHandler):
def do_POST(self): def do_POST(self):
"""处理HTTP POST请求""" """处理HTTP POST请求"""
try: 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) parsed_path = urlparse(self.path)
path = parsed_path.path path = parsed_path.path
if path == '/api/room': 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: else:
self.send_error(404, "Not Found") self.send_error(404, "Not Found")
except Exception as e: except Exception as e:
@@ -135,7 +108,6 @@ class HTTPHandler(BaseHTTPRequestHandler):
def handle_root_path(self, query_params: Dict[str, Any]): def handle_root_path(self, query_params: Dict[str, Any]):
"""处理根路径请求""" """处理根路径请求"""
room_id = query_params.get('id', [None])[0] room_id = query_params.get('id', [None])[0]
ws_url = query_params.get('ws', [None])[0]
if room_id: if room_id:
# 有房间ID参数直接返回前端页面 # 有房间ID参数直接返回前端页面
@@ -157,8 +129,8 @@ class HTTPHandler(BaseHTTPRequestHandler):
logger.info(f"通过根路径创建新房间: {room_id}") logger.info(f"通过根路径创建新房间: {room_id}")
# 重定向到带房间ID和WebSocket URL的URL # 重定向到带房间ID的URL
redirect_url = f'/?id={room_id}&ws=ws://{host_without_port}:{ws_port}' redirect_url = f'/?id={room_id}'
self.send_response(302) self.send_response(302)
self.send_header('Location', redirect_url) self.send_header('Location', redirect_url)
self.end_headers() self.end_headers()
@@ -169,11 +141,8 @@ class HTTPHandler(BaseHTTPRequestHandler):
def handle_static_file(self, path: str): def handle_static_file(self, path: str):
"""处理静态文件请求""" """处理静态文件请求"""
logger.info(f"处理静态文件请求: {path}")
# 安全检查:防止路径遍历攻击 # 安全检查:防止路径遍历攻击
if '..' in path: if '..' in path:
logger.warning(f"检测到可疑路径: {path}")
self.send_error(403, "Forbidden: Path traversal not allowed") self.send_error(403, "Forbidden: Path traversal not allowed")
return return
@@ -188,30 +157,21 @@ class HTTPHandler(BaseHTTPRequestHandler):
# 构建完整文件路径 # 构建完整文件路径
full_path = os.path.join(STATIC_DIR, file_path) full_path = os.path.join(STATIC_DIR, file_path)
logger.info(f"尝试访问文件: {full_path}")
# 如果是目录尝试查找index.html # 如果是目录尝试查找index.html
if os.path.isdir(full_path): if os.path.isdir(full_path):
index_path = os.path.join(full_path, 'index.html') index_path = os.path.join(full_path, 'index.html')
if os.path.exists(index_path): if os.path.exists(index_path):
full_path = index_path full_path = index_path
logger.info(f"重定向到目录索引文件: {index_path}")
else: else:
logger.warning(f"目录不存在索引文件: {full_path}")
self.send_error(404, "Directory index not found") self.send_error(404, "Directory index not found")
return return
# 检查文件是否存在且是普通文件 # 检查文件是否存在且是普通文件
if not os.path.exists(full_path): if not os.path.exists(full_path) or not os.path.isfile(full_path):
logger.warning(f"文件不存在: {full_path}")
self.send_error(404, f"File not found: {path}") self.send_error(404, f"File not found: {path}")
return return
if not os.path.isfile(full_path):
logger.warning(f"路径不是文件: {full_path}")
self.send_error(403, "Not a file")
return
try: try:
# 读取文件内容 # 读取文件内容
with open(full_path, 'rb') as f: with open(full_path, 'rb') as f:
@@ -222,21 +182,15 @@ class HTTPHandler(BaseHTTPRequestHandler):
if mime_type is None: if mime_type is None:
mime_type = 'application/octet-stream' mime_type = 'application/octet-stream'
logger.info(f"成功读取文件: {full_path}, 大小: {len(content)} bytes, MIME类型: {mime_type}")
# 发送响应头 # 发送响应头
self.send_response(200) self.send_response(200)
self.send_header('Content-Type', mime_type) self.send_header('Content-Type', mime_type)
self.send_header('Content-Length', str(len(content))) self.send_header('Content-Length', str(len(content)))
self.send_header('Cache-Control', 'public, max-age=3600')
# 添加缓存控制头(可选)
self.send_header('Cache-Control', 'public, max-age=3600') # 缓存1小时
self.end_headers() self.end_headers()
# 发送文件内容 # 发送文件内容
self.wfile.write(content) self.wfile.write(content)
logger.info(f"文件发送完成: {full_path}")
except Exception as e: except Exception as e:
logger.error(f"读取或发送文件失败: {e}") logger.error(f"读取或发送文件失败: {e}")
@@ -246,7 +200,7 @@ class HTTPHandler(BaseHTTPRequestHandler):
"""服务静态文件(内部方法)""" """服务静态文件(内部方法)"""
self.handle_static_file(path) self.handle_static_file(path)
def handle_create_room(self): def handle_create_room(self, post_data=None):
"""创建新房间""" """创建新房间"""
try: try:
# 生成唯一房间ID # 生成唯一房间ID
@@ -265,8 +219,7 @@ class HTTPHandler(BaseHTTPRequestHandler):
response = { response = {
'success': True, 'success': True,
'room_id': room_id, 'room_id': room_id,
'frontend_url': room.frontend_url, 'frontend_url': room.frontend_url
'ws_url': room.ws_url
} }
self.send_response(200) self.send_response(200)
@@ -279,37 +232,23 @@ class HTTPHandler(BaseHTTPRequestHandler):
logger.error(f"创建房间失败: {e}") logger.error(f"创建房间失败: {e}")
self.send_error(500, str(e)) self.send_error(500, str(e))
def handle_get_room(self, room_id: str): def handle_frontend_send_message(self, post_data):
"""获取房间信息""" """前端发送消息到客户端"""
try: try:
if room_id not in rooms: data = json.loads(post_data.decode('utf-8'))
self.send_error(404, '房间不存在') 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 return
room = rooms[room_id] queue = get_frontend_to_client_queue(room_id)
response = {'success': True, 'data': room.to_dict()} queue.append(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_list_rooms(self):
"""列出所有活跃房间"""
try:
active_rooms = [room.to_dict() for room in rooms.values() if not room.is_empty()]
response = { response = {
'success': True, 'success': True,
'data': { 'message': '消息已发送到客户端队列'
'total_rooms': len(active_rooms),
'rooms': active_rooms
}
} }
self.send_response(200) self.send_response(200)
@@ -319,7 +258,108 @@ class HTTPHandler(BaseHTTPRequestHandler):
self.wfile.write(json.dumps(response).encode()) self.wfile.write(json.dumps(response).encode())
except Exception as e: 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)) self.send_error(500, str(e))
def log_message(self, format, *args): def log_message(self, format, *args):
@@ -328,300 +368,15 @@ class HTTPHandler(BaseHTTPRequestHandler):
self.log_date_time_string(), self.log_date_time_string(),
format % args)) 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(): def run_http_server():
"""运行HTTP服务器""" """运行HTTP服务器"""
try: try:
server = HTTPServer(('0.0.0.0', 80), WebSocketHTTPRequestHandler) server = HTTPServer(('0.0.0.0', 80), HTTPHandler)
logger.info("HTTP服务器启动在端口 80") logger.info("HTTP服务器启动在端口 80")
server.serve_forever() server.serve_forever()
except Exception as e: except Exception as e:
logger.error(f"HTTP服务器启动失败: {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__': if __name__ == '__main__':
logger.info("启动服务器...") logger.info("启动服务器...")
run_http_server()
# 检查端口权限
try:
asyncio.run(main())
except PermissionError:
logger.error("需要管理员权限才能绑定到80端口")
logger.info("尝试使用8080端口...")
# 可以在这里添加回退到其他端口的逻辑

View File

@@ -12,7 +12,7 @@ Demo : http://cc-web-edit.liulikeji.cn
- **远程文件管理**:实时浏览、编辑和管理 CC:Tweaked 计算机中的文件 - **远程文件管理**:实时浏览、编辑和管理 CC:Tweaked 计算机中的文件
- **Monaco 编辑器**:基于 VS Code 的 Monaco 编辑器,提供专业的代码编辑体验 - **Monaco 编辑器**:基于 VS Code 的 Monaco 编辑器,提供专业的代码编辑体验
- **WebSocket 通信**:低延迟的双向通信,确保操作的实时性 - **HTTP 通信**:基于 HTTP 协议的可靠通信
### 文件操作 ### 文件操作
@@ -27,7 +27,7 @@ Demo : http://cc-web-edit.liulikeji.cn
- **自动命令生成**:根据 URL 参数自动生成连接命令 - **自动命令生成**:根据 URL 参数自动生成连接命令
- **一键复制**:点击即可复制连接命令到剪贴板 - **一键复制**:点击即可复制连接命令到剪贴板
- **房间管理**:支持创建和加入房间 - **房间管理**:支持创建和加入房间
- **心跳保活**:自动维持连接稳定性 - **轮询机制**HTTP 轮询确保连接稳定性
## 🚀 快速开始 ## 🚀 快速开始
@@ -44,7 +44,7 @@ Demo : http://cc-web-edit.liulikeji.cn
├── Frontend1/ # Vue 前端项目 ├── Frontend1/ # Vue 前端项目
│ ├── src/ │ ├── src/
│ │ ├── App.vue # 主组件 │ │ ├── App.vue # 主组件
│ │ └── mock-server.ts # WebSocket 客户端 │ │ └── mock-server.ts # HTTP 客户端
│ └── package.json │ └── package.json
└── Client/ # 客户端文件 └── Client/ # 客户端文件
└── main.lua # CC:Tweaked 客户端脚本 └── main.lua # CC:Tweaked 客户端脚本
@@ -73,8 +73,7 @@ python main.py
服务器将启动: 服务器将启动:
- HTTP 服务:端口 80文件服务API - HTTP 服务:端口 80文件服务API 和静态资源
- WebSocket 服务:端口 81实时通信
2. **构建前端项目** 2. **构建前端项目**
@@ -106,7 +105,7 @@ cp -r dist/* ../PyServer/static/
```lua ```lua
# 粘贴复制的命令到CC:Tweaked计算机 # 粘贴复制的命令到CC:Tweaked计算机
# 命令格式类似:wget run http://服务器地址/Client/cc/main.lua ws://服务器ws地址 房间ID # 命令格式类似:wget run http://服务器地址/Client/cc/main.lua http://服务器地址 房间ID
``` ```
3. **刷新文件列表** 3. **刷新文件列表**
@@ -128,6 +127,7 @@ cp -r dist/* ../PyServer/static/
- **二进制文件**:非文本文件会显示为 `[binary]`,无法在线编辑 - **二进制文件**:非文本文件会显示为 `[binary]`,无法在线编辑
- **单客户端**:目前主要支持一个网页端和一个 CC 客户端的配对使用 - **单客户端**:目前主要支持一个网页端和一个 CC 客户端的配对使用
- **文件大小**:上传文件限制为 1MB - **文件大小**:上传文件限制为 1MB
- **轮询延迟**HTTP 轮询机制可能有轻微延迟(默认 2 秒)
### 计划功能 ### 计划功能
@@ -136,12 +136,18 @@ cp -r dist/* ../PyServer/static/
## ⚙️ API 接口 ## ⚙️ 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` - 文件操作请求
- `file_operation_response` - 文件操作响应 - `file_operation_response` - 文件操作响应
- `ping/pong` - 心跳检测
### 文件操作类型 ### 文件操作类型
@@ -180,15 +186,21 @@ A: 确保 CC 客户端已成功连接,然后刷新文件列表
**Q: 文件上传失败** **Q: 文件上传失败**
A: 检查文件大小是否超过 1MB 限制 A: 检查文件大小是否超过 1MB 限制
**Q: 操作响应较慢**
A: 默认轮询间隔为 1 秒,可通过调整代码中的轮询间隔改善
## 📄 技术说明 ## 📄 技术说明
- **后端**Python + WebSocket - **后端**Python + HTTP Server
- **前端**Vue 3 + TypeScript + Monaco Editor - **前端**Vue 3 + TypeScript + Monaco Editor
- **通信**WebSocket 实时双向通信 - **通信**HTTP 轮询机制实现双向通信
- **客户端**CC:Tweaked Lua 脚本 - **客户端**CC:Tweaked + HTTP
## 🤝 开发说明 ## 🤝 开发说明
<<<<<<< HEAD
该项目目前主要支持远程代码编辑功能使用 HTTP 协议替代 WebSocket提高了兼容性和部署便利性远程控制台功能计划在后续版本中开发
=======
该项目目前主要支持远程代码编辑功能远程控制台功能计划在后续版本中开发 该项目目前主要支持远程代码编辑功能远程控制台功能计划在后续版本中开发
## 贡献 ## 贡献
@@ -197,3 +209,4 @@ A: 检查文件大小是否超过 1MB 限制
欢迎提交issues 欢迎提交issues
>>>>>>> d3faa4b74bc0eeac9a272c4d8a348d98a48dad7e