Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf0fdfa4d0 | ||
|
|
9a68952fac | ||
|
|
b661afed4c | ||
|
|
33ff81a15d | ||
|
|
c6d9d4f093 |
154
Client/main.lua
154
Client/main.lua
@@ -4,6 +4,15 @@ local httpServer = args[1] or "http://192.168.2.200:8080"
|
|||||||
local roomId = args[2]
|
local roomId = args[2]
|
||||||
local pollInterval = 1
|
local pollInterval = 1
|
||||||
local computerID = tostring(os.computerID() or "unknown")
|
local computerID = tostring(os.computerID() or "unknown")
|
||||||
|
-- ========== 加载 JSON ==========
|
||||||
|
local JsonUrl = "https://git.liulikeji.cn/GitHub/json.lua/raw/branch/master/json.lua"
|
||||||
|
local JsonResp = http.get(JsonUrl)
|
||||||
|
if not JsonResp then
|
||||||
|
error("无法下载 Json 框架")
|
||||||
|
end
|
||||||
|
local json = load(JsonResp.readAll())()
|
||||||
|
JsonResp.close()
|
||||||
|
|
||||||
|
|
||||||
-- ========== 加载 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"
|
||||||
@@ -21,100 +30,68 @@ local function log(msg)
|
|||||||
--basalt.debug("[FileClient] " .. tostring(msg))
|
--basalt.debug("[FileClient] " .. tostring(msg))
|
||||||
end
|
end
|
||||||
|
|
||||||
function table_to_json(t, indent)
|
|
||||||
indent = indent or 0
|
|
||||||
local spaces = string.rep(" ", indent)
|
|
||||||
local result = {}
|
|
||||||
|
|
||||||
if type(t) ~= "table" then
|
local function isBinaryFile(path)
|
||||||
if type(t) == "string" then
|
local extension = string.lower(string.match(path, "%.([^%.%s]+)$") or "")
|
||||||
-- 正确转义所有特殊字符
|
local binaryExtensions = {
|
||||||
local escaped = t:gsub("[\\\"\b\f\n\r\t]", function(c)
|
["dfpwm"] = true,
|
||||||
local replacements = {
|
}
|
||||||
['\\'] = '\\\\',
|
|
||||||
['"'] = '\\"',
|
if binaryExtensions[extension] then
|
||||||
['\b'] = '\\b',
|
return true
|
||||||
['\f'] = '\\f',
|
|
||||||
['\n'] = '\\n',
|
|
||||||
['\r'] = '\\r',
|
|
||||||
['\t'] = '\\t'
|
|
||||||
}
|
|
||||||
return replacements[c]
|
|
||||||
end)
|
|
||||||
return '"' .. escaped .. '"'
|
|
||||||
elseif type(t) == "number" or type(t) == "boolean" then
|
|
||||||
return tostring(t)
|
|
||||||
else
|
|
||||||
return '"' .. tostring(t) .. '"'
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- 检查是否是数组
|
-- 对于没有扩展名的文件,检查内容
|
||||||
local is_array = true
|
local absPath = path
|
||||||
local max_index = 0
|
if not fs.exists(absPath) then
|
||||||
local count = 0
|
return false
|
||||||
for k, v in pairs(t) do
|
|
||||||
count = count + 1
|
|
||||||
if type(k) ~= "number" or k <= 0 or math.floor(k) ~= k then
|
|
||||||
is_array = false
|
|
||||||
end
|
|
||||||
if type(k) == "number" and k > max_index then
|
|
||||||
max_index = k
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- 空表当作对象处理
|
local ok, handle = pcall(fs.open, absPath, "rb")
|
||||||
if count == 0 then
|
if not ok or not handle then
|
||||||
is_array = false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
if is_array then
|
local data = handle.read(math.min(1024, fs.getSize(absPath)))
|
||||||
-- 处理数组
|
handle.close()
|
||||||
table.insert(result, "[")
|
|
||||||
local items = {}
|
if not data then
|
||||||
for i = 1, max_index do
|
return false
|
||||||
if t[i] ~= nil then
|
end
|
||||||
table.insert(items, table_to_json(t[i], indent + 2))
|
|
||||||
else
|
-- 检查是否存在控制字符(除常见的空白字符外)
|
||||||
table.insert(items, "null")
|
for i = 1, #data do
|
||||||
|
local b = data:byte(i)
|
||||||
|
-- 控制字符范围是 0-8, 11-12, 14-31, 127
|
||||||
|
if (b >= 0 and b <= 8) or (b == 11) or (b == 12) or (b >= 14 and b <= 31) or (b == 127) then
|
||||||
|
-- 如果控制字符过多(超过5%),则认为是二进制文件
|
||||||
|
local controlCount = 0
|
||||||
|
for j = 1, #data do
|
||||||
|
local byte = data:byte(j)
|
||||||
|
if (byte >= 0 and byte <= 8) or (byte == 11) or (byte == 12) or (byte >= 14 and byte <= 31) or (byte == 127) then
|
||||||
|
controlCount = controlCount + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if controlCount / #data > 0.05 then
|
||||||
|
return true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
table.insert(result, table.concat(items, ", "))
|
|
||||||
table.insert(result, "]")
|
|
||||||
else
|
|
||||||
-- 处理对象
|
|
||||||
table.insert(result, "{")
|
|
||||||
local items = {}
|
|
||||||
for k, v in pairs(t) do
|
|
||||||
local key = '"' .. tostring(k) .. '"'
|
|
||||||
local value = table_to_json(v, indent + 2)
|
|
||||||
if indent > 0 then
|
|
||||||
table.insert(items, spaces .. " " .. key .. ": " .. value)
|
|
||||||
else
|
|
||||||
table.insert(items, key .. ":" .. value)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if indent > 0 then
|
|
||||||
table.insert(result, table.concat(items, ",\n"))
|
|
||||||
table.insert(result, "\n" .. spaces .. "}")
|
|
||||||
else
|
|
||||||
table.insert(result, table.concat(items, ","))
|
|
||||||
table.insert(result, "}")
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return table.concat(result, indent > 0 and "\n" .. spaces or "")
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
local function cleanPath(path)
|
local function cleanPath(path)
|
||||||
return (path:gsub("^computer/", ""):gsub("^computer\\", ""))
|
return (path:gsub("^computer/", ""):gsub("^computer\\", ""))
|
||||||
end
|
end
|
||||||
|
|
||||||
local function httpPost(path, data)
|
local function httpPost(path, data)
|
||||||
local jsonData = table_to_json(data)
|
local jsonData = json.encode(data)
|
||||||
local url = httpServer .. path
|
local url = httpServer .. path
|
||||||
|
|
||||||
-- 使用长轮询,设置超时时间为300秒
|
-- 使用长轮询
|
||||||
local response,err = http.post({
|
local response,err = http.post({
|
||||||
url = url,
|
url = url,
|
||||||
body = jsonData,
|
body = jsonData,
|
||||||
@@ -122,7 +99,7 @@ local function httpPost(path, data)
|
|||||||
headers = {
|
headers = {
|
||||||
["Content-Type"] = "application/json"
|
["Content-Type"] = "application/json"
|
||||||
},
|
},
|
||||||
timeout = 300 -- 300秒超时
|
timeout = 60
|
||||||
})
|
})
|
||||||
|
|
||||||
if not response then
|
if not response then
|
||||||
@@ -132,7 +109,7 @@ local function httpPost(path, data)
|
|||||||
local responseBody = response.readAll()
|
local responseBody = response.readAll()
|
||||||
response.close()
|
response.close()
|
||||||
|
|
||||||
local ok, result = pcall(textutils.unserialiseJSON, responseBody)
|
local ok, result = pcall(json.decode, responseBody)
|
||||||
if ok then
|
if ok then
|
||||||
return result
|
return result
|
||||||
else
|
else
|
||||||
@@ -141,16 +118,6 @@ local function httpPost(path, data)
|
|||||||
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 function getFiles(currentPath, result, prefix)
|
||||||
local computerPrefix = "computer_" .. computerID
|
local computerPrefix = "computer_" .. computerID
|
||||||
local fullPrefix = currentPath == "" and prefix:sub(1, -2) or prefix .. currentPath
|
local fullPrefix = currentPath == "" and prefix:sub(1, -2) or prefix .. currentPath
|
||||||
@@ -165,15 +132,16 @@ local function getFiles(currentPath, result, prefix)
|
|||||||
end
|
end
|
||||||
else
|
else
|
||||||
local content = "[binary]"
|
local content = "[binary]"
|
||||||
local ok, handle = pcall(fs.open, absPath, "rb")
|
if not isBinaryFile(absPath) then
|
||||||
if ok and handle then
|
local ok, handle = pcall(fs.open, absPath, "r")
|
||||||
local data = handle.readAll()
|
if ok and handle then
|
||||||
handle.close()
|
local data = handle.readAll()
|
||||||
if data and isLikelyText(data) then
|
handle.close()
|
||||||
content = data
|
content = data or ""
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
result[fullPrefix] = { isFile = true, content = content }
|
|
||||||
|
result[fullPrefix] = { isFile = true, content = content, isBinary = isBinaryFile(absPath) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,18 @@ class Room:
|
|||||||
'created_at': self.created_at.isoformat()
|
'created_at': self.created_at.isoformat()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def safe_decode_json(raw_body: bytes) -> Dict[Any, Any]:
|
||||||
|
"""
|
||||||
|
尝试用 UTF-8 解码,失败则尝试 GB18030(兼容 GBK/GB2312)
|
||||||
|
"""
|
||||||
|
for encoding in ['utf-8', 'gb18030', 'latin1']:
|
||||||
|
try:
|
||||||
|
text = raw_body.decode(encoding)
|
||||||
|
return json.loads(text)
|
||||||
|
except (UnicodeDecodeError, json.JSONDecodeError):
|
||||||
|
continue
|
||||||
|
raise ValueError("无法解码请求体为有效 JSON")
|
||||||
|
|
||||||
def get_frontend_to_client_queue(room_id: str) -> List[Dict[str, Any]]:
|
def get_frontend_to_client_queue(room_id: str) -> List[Dict[str, Any]]:
|
||||||
if room_id not in frontend_to_client_queues:
|
if room_id not in frontend_to_client_queues:
|
||||||
frontend_to_client_queues[room_id] = []
|
frontend_to_client_queues[room_id] = []
|
||||||
@@ -88,9 +100,9 @@ class HTTPHandler:
|
|||||||
return web.json_response({'error': str(e)}, status=500)
|
return web.json_response({'error': str(e)}, status=500)
|
||||||
|
|
||||||
async def handle_frontend_send_message(self, request):
|
async def handle_frontend_send_message(self, request):
|
||||||
"""前端发送消息到客户端"""
|
|
||||||
try:
|
try:
|
||||||
data = await request.json()
|
raw_body = await request.read()
|
||||||
|
data = safe_decode_json(raw_body)
|
||||||
room_id = data.get('room_id')
|
room_id = data.get('room_id')
|
||||||
message_data = data.get('message')
|
message_data = data.get('message')
|
||||||
|
|
||||||
@@ -99,24 +111,20 @@ class HTTPHandler:
|
|||||||
|
|
||||||
queue = get_frontend_to_client_queue(room_id)
|
queue = get_frontend_to_client_queue(room_id)
|
||||||
queue.append(message_data)
|
queue.append(message_data)
|
||||||
|
|
||||||
# 检查是否有挂起的客户端请求并立即响应
|
|
||||||
if room_id in pending_requests and 'client' in pending_requests[room_id]:
|
if room_id in pending_requests and 'client' in pending_requests[room_id]:
|
||||||
client_req_id = pending_requests[room_id]['client']
|
client_req_id = pending_requests[room_id]['client']
|
||||||
if client_req_id in pending_requests:
|
if client_req_id in pending_requests:
|
||||||
pending_requests[client_req_id]['event'].set()
|
pending_requests[client_req_id]['event'].set()
|
||||||
logger.info(f"立即响应挂起的客户端请求: {client_req_id}")
|
logger.info(f"立即响应挂起的客户端请求: {client_req_id}")
|
||||||
|
|
||||||
response = {
|
return web.json_response({
|
||||||
'success': True,
|
'success': True,
|
||||||
'message': '消息已发送到客户端队列'
|
'message': '消息已发送到客户端队列'
|
||||||
}
|
})
|
||||||
|
|
||||||
return web.json_response(response)
|
except ValueError as e:
|
||||||
|
logger.error(f"前端发送消息失败: 无效的JSON或编码错误 - {e}")
|
||||||
except json.JSONDecodeError:
|
return web.json_response({'error': '无效的JSON数据或不支持的文本编码'}, status=400)
|
||||||
logger.error("JSON解析失败")
|
|
||||||
return web.json_response({'error': '无效的JSON数据'}, status=400)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"前端发送消息失败: {e}")
|
logger.error(f"前端发送消息失败: {e}")
|
||||||
return web.json_response({'error': str(e)}, status=500)
|
return web.json_response({'error': str(e)}, status=500)
|
||||||
@@ -204,9 +212,9 @@ class HTTPHandler:
|
|||||||
return web.json_response({'error': str(e)}, status=500)
|
return web.json_response({'error': str(e)}, status=500)
|
||||||
|
|
||||||
async def handle_client_send_message(self, request):
|
async def handle_client_send_message(self, request):
|
||||||
"""客户端发送消息到前端"""
|
|
||||||
try:
|
try:
|
||||||
data = await request.json()
|
raw_body = await request.read() # 获取原始字节
|
||||||
|
data = safe_decode_json(raw_body)
|
||||||
room_id = data.get('room_id')
|
room_id = data.get('room_id')
|
||||||
message_data = data.get('message')
|
message_data = data.get('message')
|
||||||
|
|
||||||
@@ -216,23 +224,20 @@ class HTTPHandler:
|
|||||||
queue = get_client_to_frontend_queue(room_id)
|
queue = get_client_to_frontend_queue(room_id)
|
||||||
queue.append(message_data)
|
queue.append(message_data)
|
||||||
|
|
||||||
# 检查是否有挂起的前端请求并立即响应
|
|
||||||
if room_id in pending_requests and 'frontend' in pending_requests[room_id]:
|
if room_id in pending_requests and 'frontend' in pending_requests[room_id]:
|
||||||
frontend_req_id = pending_requests[room_id]['frontend']
|
frontend_req_id = pending_requests[room_id]['frontend']
|
||||||
if frontend_req_id in pending_requests:
|
if frontend_req_id in pending_requests:
|
||||||
pending_requests[frontend_req_id]['event'].set()
|
pending_requests[frontend_req_id]['event'].set()
|
||||||
logger.info(f"立即响应挂起的前端请求: {frontend_req_id}")
|
logger.info(f"立即响应挂起的前端请求: {frontend_req_id}")
|
||||||
|
|
||||||
response = {
|
return web.json_response({
|
||||||
'success': True,
|
'success': True,
|
||||||
'message': '消息已发送到前端队列'
|
'message': '消息已发送到前端队列'
|
||||||
}
|
})
|
||||||
|
|
||||||
return web.json_response(response)
|
except ValueError as e:
|
||||||
|
logger.error(f"客户端发送消息失败: 无效的JSON或编码错误 - {e}")
|
||||||
except json.JSONDecodeError:
|
return web.json_response({'error': '无效的JSON数据或不支持的文本编码'}, status=400)
|
||||||
logger.error("JSON解析失败")
|
|
||||||
return web.json_response({'error': '无效的JSON数据'}, status=400)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"客户端发送消息失败: {e}")
|
logger.error(f"客户端发送消息失败: {e}")
|
||||||
return web.json_response({'error': str(e)}, status=500)
|
return web.json_response({'error': str(e)}, status=500)
|
||||||
@@ -240,7 +245,8 @@ class HTTPHandler:
|
|||||||
async def handle_client_receive_message(self, request):
|
async def handle_client_receive_message(self, request):
|
||||||
"""客户端接收来自前端的消息(长轮询)"""
|
"""客户端接收来自前端的消息(长轮询)"""
|
||||||
try:
|
try:
|
||||||
data = await request.json()
|
raw_body = await request.read()
|
||||||
|
data = safe_decode_json(raw_body) # 使用你之前添加的 safe_decode_json
|
||||||
room_id = data.get('room_id')
|
room_id = data.get('room_id')
|
||||||
|
|
||||||
if not room_id:
|
if not room_id:
|
||||||
@@ -257,7 +263,7 @@ class HTTPHandler:
|
|||||||
}
|
}
|
||||||
return web.json_response(response)
|
return web.json_response(response)
|
||||||
|
|
||||||
# 没有消息,设置长轮询
|
# 没有消息,设置长轮询(最多等待 58 秒)
|
||||||
req_id = str(uuid.uuid4())
|
req_id = str(uuid.uuid4())
|
||||||
event = asyncio.Event()
|
event = asyncio.Event()
|
||||||
|
|
||||||
@@ -274,10 +280,10 @@ class HTTPHandler:
|
|||||||
pending_requests[room_id]['client'] = req_id
|
pending_requests[room_id]['client'] = req_id
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 等待295秒或直到有消息
|
# ⏱️ 只等待 58 秒(略小于客户端或代理的 60 秒超时)
|
||||||
await asyncio.wait_for(event.wait(), timeout=295)
|
await asyncio.wait_for(event.wait(), timeout=58)
|
||||||
|
|
||||||
# 检查队列中是否有消息
|
# 被唤醒后,检查队列
|
||||||
queue = get_frontend_to_client_queue(room_id)
|
queue = get_frontend_to_client_queue(room_id)
|
||||||
if queue:
|
if queue:
|
||||||
message = queue.pop(0)
|
message = queue.pop(0)
|
||||||
@@ -286,37 +292,36 @@ class HTTPHandler:
|
|||||||
'message': message
|
'message': message
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
# 超时返回空消息
|
|
||||||
response = {
|
response = {
|
||||||
'success': True,
|
'success': True,
|
||||||
'message': None
|
'message': None
|
||||||
}
|
}
|
||||||
|
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
# 超时返回空消息
|
# 58秒超时,返回空消息
|
||||||
response = {
|
response = {
|
||||||
'success': True,
|
'success': True,
|
||||||
'message': None
|
'message': None
|
||||||
}
|
}
|
||||||
|
|
||||||
# 清理挂起的请求
|
# 清理挂起的请求
|
||||||
if req_id in pending_requests:
|
pending_requests.pop(req_id, None)
|
||||||
del pending_requests[req_id]
|
if room_id in pending_requests:
|
||||||
if room_id in pending_requests and 'client' in pending_requests[room_id]:
|
pending_requests[room_id].pop('client', None)
|
||||||
del pending_requests[room_id]['client']
|
# 如果 room_id 下已无其他引用,也可以清理整个 room 条目(可选)
|
||||||
|
|
||||||
return web.json_response(response)
|
return web.json_response(response)
|
||||||
|
|
||||||
except json.JSONDecodeError:
|
except ValueError as e:
|
||||||
logger.error("JSON解析失败")
|
logger.error(f"客户端接收消息失败: 无效JSON或编码 - {e}")
|
||||||
return web.json_response({'error': '无效的JSON数据'}, status=400)
|
return web.json_response({'error': '无效的JSON数据'}, status=400)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"客户端接收消息失败: {e}")
|
logger.error(f"客户端接收消息失败: {e}")
|
||||||
# 清理挂起的请求
|
# 清理挂起的请求
|
||||||
if 'req_id' in locals() and req_id in pending_requests:
|
if 'req_id' in locals():
|
||||||
del pending_requests[req_id]
|
pending_requests.pop(req_id, None)
|
||||||
if room_id in pending_requests and 'client' in pending_requests[room_id]:
|
if 'room_id' in locals() and room_id in pending_requests:
|
||||||
del pending_requests[room_id]['client']
|
pending_requests[room_id].pop('client', None)
|
||||||
return web.json_response({'error': str(e)}, status=500)
|
return web.json_response({'error': str(e)}, status=500)
|
||||||
|
|
||||||
async def handle_static_file(self, request):
|
async def handle_static_file(self, request):
|
||||||
|
|||||||
Reference in New Issue
Block a user