更改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
-- 支持创建房间、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,335 +107,281 @@ function table_to_json(t, indent)
end
local function cleanPath(path)
if string.sub(path, 1, 9) == "computer/" then
return string.sub(path, 10)
elseif string.sub(path, 1, 9) == "computer\\" then
return string.sub(path, 10)
end
return path
return (path:gsub("^computer/", ""):gsub("^computer\\", ""))
end
local function sendJson(ws, obj)
local payload = table_to_json(obj)
ws.send(payload)
end
local function httpPost(path, data)
local jsonData = table_to_json(data)
local url = httpServer .. path
-- ========== 文件系统操作(纯逻辑,无网络)==========
local function isLikelyText(data, maxCheck)
if not data then return false end
maxCheck = maxCheck or math.min(#data, 1024)
for i = 1, maxCheck do
local b = data:byte(i)
if b < 32 and not (b == 9 or b == 10 or b == 13) then
return false
local response = http.post(url, jsonData, {
["Content-Type"] = "application/json"
})
if not response then
return nil, "无法连接到服务器"
end
end
return true
local responseBody = response.readAll()
response.close()
local ok, result = pcall(textutils.unserialiseJSON, responseBody)
if ok then
return result
else
return nil, "无效的JSON响应"
end
end
-- ========== 文件系统操作 ==========
local function isLikelyText(data)
for i = 1, math.min(#data, 1024) do
local b = data:byte(i)
if b < 32 and b ~= 9 and b ~= 10 and b ~= 13 then
return false
end
end
return true
end
local function getFiles(currentPath, result, prefix)
local fullPrefix
if currentPath == "" then
fullPrefix = prefix:sub(1, -2) -- 移除末尾的斜杠,将 "computer/" 变为 "computer"
else
fullPrefix = prefix .. currentPath
end
local computerPrefix = "computer_" .. computerID
local fullPrefix = currentPath == "" and prefix:sub(1, -2) or prefix .. currentPath
local absPath = "/" .. (currentPath == "" and "" or 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)
end
if fs.isDir(absPath) then
result[fullPrefix] = { isFolder = true }
for _, entry in ipairs(fs.list(absPath)) do
if entry ~= "rom" then
getFiles(currentPath == "" and entry or (currentPath .. "/" .. entry), result, prefix)
end
end
else
local content = "[binary]"
local ok, handle = pcall(fs.open, absPath, "rb")
if ok and handle then
local data = handle.readAll()
handle.close()
if data and isLikelyText(data) then
content = data
end
end
result[fullPrefix] = { isFile = true, content = content }
end
else
local content = "[binary]"
local ok, handle = pcall(fs.open, absPath, "rb")
if ok and handle then
local data = handle.readAll()
handle.close()
log(absPath.." File size: " .. #data)
if data and isLikelyText(data) then
content = data
log(absPath .. " is text file")
end
if content == "[binary]" then print("binary file: " .. absPath) end
end
result[fullPrefix] = { isFile = true, content = content }
end
end
local function fetchFiles()
local files = {}
getFiles("", files, "computer/")
return files
local files = {}
getFiles("", files, "computer_" .. computerID .. "/")
return files
end
local function saveFile(path, content)
path = cleanPath(path)
local dir = fs.getDir(path)
if not fs.exists(dir) then fs.makeDir(dir) end
local f = fs.open(path, "w")
f.write(content or "")
f.close()
path = cleanPath(path):gsub("^computer[" .. computerID .. "_]*/", "")
local dir = fs.getDir(path)
if not fs.exists(dir) then fs.makeDir(dir) end
local f = fs.open(path, "w")
f.write(content or "")
f.close()
end
local function createFile(path)
path = cleanPath(path)
local dir = fs.getDir(path)
if not fs.exists(dir) then fs.makeDir(dir) end
if not fs.exists(path) then
local f = fs.open(path, "w")
f.close()
end
path = cleanPath(path):gsub("^computer[" .. computerID .. "_]*/", "")
local dir = fs.getDir(path)
if not fs.exists(dir) then fs.makeDir(dir) end
if not fs.exists(path) then
local f = fs.open(path, "w")
f.close()
end
end
local function createFolder(path)
path = cleanPath(path)
fs.makeDir(path)
path = cleanPath(path):gsub("^computer[" .. computerID .. "_]*/", "")
fs.makeDir(path)
end
local function renameFile(oldPath, newPath)
oldPath = cleanPath(oldPath)
newPath = cleanPath(newPath)
fs.move(oldPath, newPath)
oldPath = cleanPath(oldPath):gsub("^computer[" .. computerID .. "_]*/", "")
newPath = cleanPath(newPath):gsub("^computer[" .. computerID .. "_]*/", "")
fs.move(oldPath, newPath)
end
local function deleteFile(path)
path = cleanPath(path)
if fs.exists(path) then
fs.delete(path)
end
path = cleanPath(path):gsub("^computer[" .. computerID .. "_]*/", "")
if fs.exists(path) then
fs.delete(path)
end
end
-- ========== 异步任务处理器 ==========
local function handleFetchFiles(ws, reqId, sender)
local success, result = pcall(fetchFiles)
sendJson(ws, {
type = "file_operation_response",
requestId = reqId,
success = success,
data = success and result or nil,
error = success and nil or tostring(result),
target_client_id = sender
})
log("Async fetch_files completed: " .. (success and "OK" or "FAILED"))
-- ========== 消息处理函数 ==========
local function handleFetchFiles(reqId, sender)
local success, result = pcall(fetchFiles)
return {
type = "file_operation_response",
requestId = reqId,
success = success,
data = success and result or nil,
error = success and nil or tostring(result),
target_client_id = sender
}
end
local function handleSaveFile(ws, data, reqId, sender)
local success, err = pcall(function()
assert(data.path, "Missing path")
saveFile(data.path, data.content)
end)
sendJson(ws, {
type = "file_operation_response",
requestId = reqId,
success = success,
error = success and nil or tostring(err),
target_client_id = sender
})
log("Async save_file completed: " .. (success and "OK" or "FAILED"))
local function handleSaveFile(data, reqId, sender)
local success, err = pcall(saveFile, data.path, data.content)
return {
type = "file_operation_response",
requestId = reqId,
success = success,
error = success and nil or tostring(err),
target_client_id = sender
}
end
local function handleCreateFile(ws, data, reqId, sender)
local success, err = pcall(function()
assert(data.path, "Missing path")
createFile(data.path)
end)
sendJson(ws, {
type = "file_operation_response",
requestId = reqId,
success = success,
error = success and nil or tostring(err),
target_client_id = sender
})
log("Async create_file completed: " .. (success and "OK" or "FAILED"))
local function handleCreateFile(data, reqId, sender)
local success, err = pcall(createFile, data.path)
return {
type = "file_operation_response",
requestId = reqId,
success = success,
error = success and nil or tostring(err),
target_client_id = sender
}
end
local function handleCreateFolder(ws, data, reqId, sender)
local success, err = pcall(function()
assert(data.path, "Missing path")
createFolder(data.path)
end)
sendJson(ws, {
type = "file_operation_response",
requestId = reqId,
success = success,
error = success and nil or tostring(err),
target_client_id = sender
})
log("Async create_folder completed: " .. (success and "OK" or "FAILED"))
local function handleCreateFolder(data, reqId, sender)
local success, err = pcall(createFolder, data.path)
return {
type = "file_operation_response",
requestId = reqId,
success = success,
error = success and nil or tostring(err),
target_client_id = sender
}
end
local function handleRename(ws, data, reqId, sender)
local success, err = pcall(function()
assert(data.path and data.newPath, "Missing path or newPath")
renameFile(data.path, data.newPath)
end)
sendJson(ws, {
type = "file_operation_response",
requestId = reqId,
success = success,
error = success and nil or tostring(err),
target_client_id = sender
})
log("Async rename completed: " .. (success and "OK" or "FAILED"))
local function handleRename(data, reqId, sender)
local success, err = pcall(renameFile, data.path, data.newPath)
return {
type = "file_operation_response",
requestId = reqId,
success = success,
error = success and nil or tostring(err),
target_client_id = sender
}
end
local function handleDelete(ws, data, reqId, sender)
log("delete:"..data.path)
local success, err = pcall(function()
assert(data.path, "Missing path")
deleteFile(data.path)
end)
sendJson(ws, {
type = "file_operation_response",
requestId = reqId,
success = success,
error = success and nil or tostring(err),
target_client_id = sender
})
log("Async delete completed: " .. (success and "OK" or "FAILED"))
local function handleDelete(data, reqId, sender)
local success, err = pcall(deleteFile, data.path)
return {
type = "file_operation_response",
requestId = reqId,
success = success,
error = success and nil or tostring(err),
target_client_id = sender
}
end
-- ========== 房间管理函数 ==========
local function createRoom()
log("正在创建房间...")
local roomUrl = httpServer .. "/api/room"
local roomResp = http.post(roomUrl, "{}")
if not roomResp then
error("无法连接到 HTTP 服务器创建房间")
end
local body = roomResp.readAll()
roomResp.close()
local roomData = textutils.unserialiseJSON(body)
if not roomData or not roomData.room_id or not roomData.ws_url then
error("无效的房间响应: " .. tostring(body))
end
log("房间创建成功,房间 ID: " .. roomData.room_id)
return roomData
local result, err = httpPost("/api/room", {})
if not result then
error("无法创建房间: " .. tostring(err))
end
return result.room_id
end
local function joinRoom(roomId)
log("正在加入房间: " .. roomId)
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
local function pollMessages()
while true do
if not roomId then
sleep(pollInterval)
break
end
log("连接 WebSocket: " .. wsUrl)
local ws = http.websocket(wsUrl)
if not ws then
error("无法打开 WebSocket 连接")
end
local result, err = httpPost("/api/client/receive", {
room_id = roomId
})
-- 加入房间
sendJson(ws, {
type = "join_room",
room_id = roomId,
client_type = "file_client"
})
if result and result.success and result.message then
local msg = result.message
local msgType = msg.type
return ws, roomId
if msgType == "file_operation" or msgType == "file_operation_request" then
local op = msg.operation_type or msg.type
local data = msg.data or {}
local reqId = msg.requestId or msg.request_id
local sender = msg.sender_id
local response
if op == "fetch_files" then
response = handleFetchFiles(reqId, sender)
elseif op == "create_or_save_file" then
response = handleSaveFile(data, reqId, sender)
elseif op == "new_file" then
response = handleCreateFile(data, reqId, sender)
elseif op == "new_folder" then
response = handleCreateFolder(data, reqId, sender)
elseif op == "rename" then
response = handleRename(data, reqId, sender)
elseif op == "delete_file" then
response = handleDelete(data, reqId, sender)
else
response = {
type = "file_operation_response",
requestId = reqId,
success = false,
error = "Unknown operation: " .. tostring(op),
target_client_id = sender
}
end
sendResponse(response)
end
elseif err then
log("轮询错误: " .. tostring(err))
end
sleep(pollInterval)
end
end
-- ========== 主函数 ==========
local function main()
local ws, finalRoomId
if not roomId then
roomId = createRoom()
log("创建新房间: " .. roomId)
else
log("使用现有房间: " .. roomId)
end
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)
mainFrame:addProgram():execute(function()
shell.run("shell")
end):setSize("parent.w","parent.h")
os.queueEvent("mouse_click",1,1,1)
-- 启动消息轮询
mainFrame:addThread():start(pollMessages)
log("客户端已启动。房间ID: " .. roomId)
log("计算机ID: " .. computerID)
log("按 Q 退出")
-- 主循环
while true do
local payload = ws.receive()
if payload then
local ok, msg = pcall(textutils.unserialiseJSON, payload)
if ok and type(msg) == "table" then
local msgType = msg.type
log("收到消息: " .. tostring(msgType))
if msgType == "connected" then
log("分配的客户端 ID: " .. tostring(msg.client_id))
elseif msgType == "file_operation" or msgType == "file_operation_request" then
op = msg.operation_type or msg.type
data = msg.data or {}
reqId = msg.requestId or msg.request_id
sender = msg.sender_id
-- 派发到后台线程
if op == "fetch_files" then
mainFrame:addThread():start(function () handleFetchFiles(ws, reqId, sender) end )
elseif op == "create_or_save_file" then
mainFrame:addThread():start(function () handleSaveFile(ws, data, reqId, sender) end)
elseif op == "new_file" then
mainFrame:addThread():start(function () handleCreateFile(ws, data, reqId, sender) end)
elseif op == "new_folder" then
mainFrame:addThread():start(function () handleCreateFolder(ws, data, reqId, sender) end)
elseif op == "rename" then
mainFrame:addThread():start(function () handleRename(ws, data, reqId, sender) end)
elseif op == "delete_file" then
mainFrame:addThread():start(function () handleDelete(ws, data, reqId, sender) end)
else
-- 同步返回未知操作错误(轻量)
sendJson(ws, {
type = "file_operation_response",
requestId = reqId,
success = false,
error = "Unknown operation: " .. tostring(op),
target_client_id = sender
})
end
end
else
log("无效 JSON: " .. tostring(payload))
local event, param1 = os.pullEvent()
if event == "key" and param1 == keys.q then
log("用户按 Q 退出")
break
end
end
end
end,
function ()
while true do
local event, param1, param2, param3 = os.pullEvent()
if event == "timer" and param1 == heartbeatTimer then
sendJson(ws, { type = "ping" })
heartbeatTimer = os.startTimer(heartbeatInterval)
elseif event == "websocket_closed" and param1 == ws then
log("WebSocket 连接已关闭")
break
elseif event == "key" and param1 == keys.q then
log("用户按 Q 退出")
break
end
end
end)
ws.close()
log("客户端已停止。房间ID: " .. finalRoomId)
end
-- 启动主逻辑和Basalt事件循环

View File

@@ -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)

View File

@@ -1,19 +1,11 @@
import type { Files } from 'monaco-tree-editor'
// WebSocket 连接管理
let ws: WebSocket | null = null
let roomId: string | null = null
let wsServer: string | null = null
let isConnected = false
let clientId: string | null = null
let reconnectAttempts = 0
let heartbeatInterval: number | null = null
const maxReconnectAttempts = 5
const reconnectDelay = 2000
const heartbeatIntervalMs = 30000 // 30秒发送一次心跳
let isDisconnecting = false
let serverUrl: string | null = null
let pollIntervalMs = 1000
let isPolling = false
let pollingTimeout: number | null = null
// 请求回调映射
const pendingRequests = new Map<
string,
{
@@ -23,7 +15,6 @@ const pendingRequests = new Map<
}
>()
// 待处理初始请求队列
const pendingInitialRequests: Array<{
operation: string
data?: any
@@ -31,245 +22,97 @@ const pendingInitialRequests: Array<{
reject: (error: Error) => void
}> = []
// 生成唯一请求ID
function generateRequestId(): string {
return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
}
// 从URL获取房间ID和WebSocket服务器地址
function getParamsFromUrl(): { roomId: string | null; wsServer: string | null } {
function getParamsFromUrl(): { roomId: string | null } {
const urlParams = new URLSearchParams(window.location.search)
const roomId = urlParams.get('id')
const wsServer = urlParams.get('ws') || urlParams.get('server')
return { roomId, wsServer }
return { roomId }
}
// 处理页面关闭前的清理
function handleBeforeUnload() {
if (!isDisconnecting && isConnected && ws && ws.readyState === WebSocket.OPEN) {
isDisconnecting = true
try {
// 发送离开房间消息
sendMessage({
type: 'leave_room',
room_id: roomId,
client_id: clientId,
})
} catch (error) {
console.error('发送离开消息失败:', error)
async function httpPost(path: string, data: any): Promise<any> {
const url = `${serverUrl}${path}`
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status} ${response.statusText}`)
}
// 断开连接
disconnect()
return await response.json()
} catch (error) {
console.error('HTTP请求失败:', error)
throw error
}
}
// 初始化WebSocket连接
export async function initWebSocketConnection(): Promise<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
}
// 创建WebSocket连接
ws = new WebSocket(serverUrl)
// 连接事件
ws.onopen = () => {
isConnected = true
reconnectAttempts = 0
console.log('WebSocket连接已建立服务器:', serverUrl)
// 启动心跳
startHeartbeat()
// 发送加入房间消息
sendMessage({
type: 'join_room',
room_id: roomId,
client_type: 'frontend',
})
// 添加页面关闭监听
if (typeof window !== 'undefined') {
window.addEventListener('beforeunload', handleBeforeUnload)
}
}
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data)
handleMessage(data)
} catch (error) {
console.error('消息解析错误:', error)
}
}
ws.onclose = (event) => {
isConnected = false
console.log('WebSocket连接已断开:', event.code, event.reason)
// 停止心跳
stopHeartbeat()
// 移除事件监听
if (typeof window !== 'undefined') {
window.removeEventListener('beforeunload', handleBeforeUnload)
}
// 拒绝所有待处理的请求
for (const [requestId, { reject }] of pendingRequests) {
reject(new Error('WebSocket连接已断开'))
pendingRequests.delete(requestId)
}
// 自动重连(除非是主动断开)
if (!isDisconnecting && reconnectAttempts < maxReconnectAttempts) {
setTimeout(() => {
reconnectAttempts++
console.log(`尝试重新连接 (${reconnectAttempts}/${maxReconnectAttempts})`)
initWebSocketConnection().catch(console.error)
}, reconnectDelay)
}
}
ws.onerror = (error) => {
console.error('WebSocket错误:', error)
reject(new Error('WebSocket连接失败'))
}
// 设置连接超时
setTimeout(() => {
if (!isConnected) {
reject(new Error('WebSocket连接超时'))
}
}, 10000)
})
startPolling()
return Promise.resolve()
}
// 启动心跳
function startHeartbeat() {
stopHeartbeat() // 先停止可能存在的旧心跳
heartbeatInterval = setInterval(() => {
if (isConnected && ws && ws.readyState === WebSocket.OPEN) {
sendMessage({
type: 'ping',
timestamp: new Date().toISOString(),
})
}
}, heartbeatIntervalMs)
function startPolling() {
if (isPolling) return
isPolling = true
pollForResponses()
}
// 停止心跳
function stopHeartbeat() {
if (heartbeatInterval) {
clearInterval(heartbeatInterval)
heartbeatInterval = null
function stopPolling() {
isPolling = false
if (pollingTimeout) {
clearTimeout(pollingTimeout)
pollingTimeout = null
}
}
async function pollForResponses() {
if (!isPolling || !roomId) return
try {
const response = await httpPost('/api/frontend/receive', {
room_id: roomId,
})
if (response.success && response.message) {
handleMessage(response.message)
}
} catch (error) {
console.error('轮询消息失败:', error)
}
if (isPolling) {
pollingTimeout = window.setTimeout(() => pollForResponses(), pollIntervalMs)
}
}
// 处理接收到的消息
function handleMessage(data: any): void {
const messageType = data.type
switch (messageType) {
case 'connected':
clientId = data.client_id
console.log('连接成功:', data.message)
console.log('客户端ID:', clientId)
break
case 'joined_room':
console.log('加入房间成功:', data.message)
console.log('客户端数量:', data.client_count)
// 连接成功后处理所有待处理的初始请求
processPendingInitialRequests()
break
case 'file_operation_response':
handleFileOperationResponse(data)
break
case 'user_joined':
console.log('新用户加入:', data.client_id, data.client_type)
break
case 'user_left':
console.log('用户离开:', data.client_id)
break
case 'pong':
// 心跳响应,不需要处理
break
case 'error':
console.error('错误:', data.message)
// 处理文件操作错误
if (data.requestId && pendingRequests.has(data.requestId)) {
const { reject, timeout } = pendingRequests.get(data.requestId)!
clearTimeout(timeout)
reject(new Error(data.message || '请求失败'))
pendingRequests.delete(data.requestId)
}
break
default:
console.log('未知消息类型:', messageType, data)
if (messageType === 'file_operation_response') {
handleFileOperationResponse(data)
}
}
// 处理所有待处理的初始请求
function processPendingInitialRequests() {
while (pendingInitialRequests.length > 0) {
const request = pendingInitialRequests.shift()!
const { operation, data, resolve, reject } = request
try {
// 重新发送请求
sendFileOperationInternal(operation, data).then(resolve).catch(reject)
} catch (error) {
reject(error instanceof Error ? error : new Error(String(error)))
}
}
}
// 发送消息
function sendMessage(message: any): void {
if (!isConnected || !ws || ws.readyState !== WebSocket.OPEN) {
if (message.type !== 'leave_room') {
// 离开房间消息可以在关闭时发送
throw new Error('WebSocket未连接')
}
return
}
try {
ws.send(JSON.stringify(message))
} catch (error) {
if (message.type !== 'leave_room') {
// 忽略离开消息的发送错误
throw new Error(`发送消息失败: ${error}`)
}
}
}
// 处理文件操作响应
function handleFileOperationResponse(data: any): void {
const requestId = data.requestId
@@ -287,12 +130,11 @@ function handleFileOperationResponse(data: any): void {
}
}
// 内部发送文件操作请求(不处理连接状态)
function sendFileOperationInternal(operationType: string, data?: any, timeoutMs: number = 10000): Promise<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,
},
})
.then((response) => {
if (!response.success) {
pendingRequests.delete(requestId)
clearTimeout(timeout)
reject(new Error(response.message || '发送请求失败'))
}
})
.catch((error) => {
pendingRequests.delete(requestId)
clearTimeout(timeout)
reject(error)
})
} catch (error) {
pendingRequests.delete(requestId)
clearTimeout(timeout)
reject(new Error(`发送请求失败: ${error}`))
}
})
}
// 发送文件操作请求(处理连接状态)
async function sendFileOperation(operationType: string, data?: any, timeoutMs: number = 10000): Promise<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}`)
}
await sendFileOperation('create_or_save_file', { path, content })
}
export const newFile = async (path: string) => {
try {
await sendFileOperation('new_file', { path })
} catch (error) {
throw new Error(`创建新文件失败: ${error}`)
}
await sendFileOperation('new_file', { path })
}
export const newFolder = async (path: string) => {
try {
await sendFileOperation('new_folder', { path })
} catch (error) {
throw new Error(`创建新文件夹失败: ${error}`)
}
await sendFileOperation('new_folder', { path })
}
export const rename = async (path: string, newPath: string) => {
try {
await sendFileOperation('rename', { path, newPath })
return true
} catch (error) {
throw new Error(`重命名失败: ${error}`)
}
await sendFileOperation('rename', { path, newPath })
return true
}
export const deleteFile = async (path: string) => {
try {
await sendFileOperation('delete_file', { path })
return true
} catch (error) {
throw new Error(`删除失败: ${error}`)
}
await sendFileOperation('delete_file', { path })
return true
}
// 工具函数
export const getConnectionStatus = () => ({
isConnected,
isConnected: isPolling,
roomId,
clientId,
wsServer,
serverUrl,
})
export const disconnect = () => {
isDisconnecting = true
// 发送离开房间消息
if (ws && isConnected && ws.readyState === WebSocket.OPEN) {
try {
sendMessage({
type: 'leave_room',
room_id: roomId,
client_id: clientId,
})
} catch (error) {
console.error('发送离开消息失败:', error)
}
}
if (ws) {
ws.close()
ws = null
}
isConnected = false
stopPolling()
roomId = null
wsServer = null
clientId = null
isDisconnecting = false
// 停止心跳
stopHeartbeat()
// 清空待处理请求
serverUrl = null
pendingInitialRequests.length = 0
// 移除事件监听
if (typeof window !== 'undefined') {
window.removeEventListener('beforeunload', handleBeforeUnload)
}
}
export const getShareableUrl = (includeWs: boolean = true): string => {
export const getShareableUrl = (): string => {
if (!roomId) {
throw new Error('未加入任何房间')
}
const currentUrl = new URL(window.location.href)
currentUrl.searchParams.set('id', roomId)
if (includeWs && wsServer) {
currentUrl.searchParams.set('ws', wsServer)
}
return currentUrl.toString()
}
// 设置WebSocket服务器地址
export const setWsServer = (serverUrl: string) => {
wsServer = serverUrl
export const setPollInterval = (intervalMs: number) => {
pollIntervalMs = intervalMs
}

View File

@@ -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()

View File

@@ -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