Compare commits

6 Commits

Author SHA1 Message Date
nnwang
cf0fdfa4d0 删除不必要的拓展名 2025-12-12 20:45:59 +08:00
nnwang
9a68952fac 将json解析改为外部库 2025-12-12 20:42:40 +08:00
nnwang
b661afed4c 修复2进制文件dfpwm被视为文本传输的问题 2025-12-12 20:11:48 +08:00
nnwang
33ff81a15d 修复缺少safe_decode_json 2025-12-12 19:18:40 +08:00
nnwang
c6d9d4f093 修复utf8编码问题,修改客户端长轮询为60秒 2025-12-12 18:57:17 +08:00
nnwang
d862467883 更新README 2025-12-06 01:08:40 +08:00
3 changed files with 143 additions and 165 deletions

View File

@@ -4,6 +4,15 @@ local httpServer = args[1] or "http://192.168.2.200:8080"
local roomId = args[2]
local pollInterval = 1
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 ==========
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))
end
function table_to_json(t, indent)
indent = indent or 0
local spaces = string.rep(" ", indent)
local result = {}
if type(t) ~= "table" then
if type(t) == "string" then
-- 正确转义所有特殊字符
local escaped = t:gsub("[\\\"\b\f\n\r\t]", function(c)
local replacements = {
['\\'] = '\\\\',
['"'] = '\\"',
['\b'] = '\\b',
['\f'] = '\\f',
['\n'] = '\\n',
['\r'] = '\\r',
['\t'] = '\\t'
}
return replacements[c]
end)
return '"' .. escaped .. '"'
elseif type(t) == "number" or type(t) == "boolean" then
return tostring(t)
else
return '"' .. tostring(t) .. '"'
end
local function isBinaryFile(path)
local extension = string.lower(string.match(path, "%.([^%.%s]+)$") or "")
local binaryExtensions = {
["dfpwm"] = true,
}
if binaryExtensions[extension] then
return true
end
-- 检查是否是数组
local is_array = true
local max_index = 0
local count = 0
for k, v in pairs(t) do
count = count + 1
if type(k) ~= "number" or k <= 0 or math.floor(k) ~= k then
is_array = false
end
if type(k) == "number" and k > max_index then
max_index = k
end
-- 对于没有扩展名的文件,检查内容
local absPath = path
if not fs.exists(absPath) then
return false
end
-- 空表当作对象处理
if count == 0 then
is_array = false
local ok, handle = pcall(fs.open, absPath, "rb")
if not ok or not handle then
return false
end
if is_array then
-- 处理数组
table.insert(result, "[")
local items = {}
for i = 1, max_index do
if t[i] ~= nil then
table.insert(items, table_to_json(t[i], indent + 2))
else
table.insert(items, "null")
local data = handle.read(math.min(1024, fs.getSize(absPath)))
handle.close()
if not data then
return false
end
-- 检查是否存在控制字符(除常见的空白字符外)
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
table.insert(result, table.concat(items, ", "))
table.insert(result, "]")
else
-- 处理对象
table.insert(result, "{")
local items = {}
for k, v in pairs(t) do
local key = '"' .. tostring(k) .. '"'
local value = table_to_json(v, indent + 2)
if indent > 0 then
table.insert(items, spaces .. " " .. key .. ": " .. value)
else
table.insert(items, key .. ":" .. value)
end
end
if indent > 0 then
table.insert(result, table.concat(items, ",\n"))
table.insert(result, "\n" .. spaces .. "}")
else
table.insert(result, table.concat(items, ","))
table.insert(result, "}")
end
end
return table.concat(result, indent > 0 and "\n" .. spaces or "")
return false
end
local function cleanPath(path)
return (path:gsub("^computer/", ""):gsub("^computer\\", ""))
end
local function httpPost(path, data)
local jsonData = table_to_json(data)
local jsonData = json.encode(data)
local url = httpServer .. path
-- 使用长轮询设置超时时间为300秒
-- 使用长轮询
local response,err = http.post({
url = url,
body = jsonData,
@@ -122,7 +99,7 @@ local function httpPost(path, data)
headers = {
["Content-Type"] = "application/json"
},
timeout = 300 -- 300秒超时
timeout = 60
})
if not response then
@@ -132,7 +109,7 @@ local function httpPost(path, data)
local responseBody = response.readAll()
response.close()
local ok, result = pcall(textutils.unserialiseJSON, responseBody)
local ok, result = pcall(json.decode, responseBody)
if ok then
return result
else
@@ -141,16 +118,6 @@ local function httpPost(path, data)
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 computerPrefix = "computer_" .. computerID
local fullPrefix = currentPath == "" and prefix:sub(1, -2) or prefix .. currentPath
@@ -165,15 +132,16 @@ local function getFiles(currentPath, result, prefix)
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
if not isBinaryFile(absPath) then
local ok, handle = pcall(fs.open, absPath, "r")
if ok and handle then
local data = handle.readAll()
handle.close()
content = data or ""
end
end
result[fullPrefix] = { isFile = true, content = content }
result[fullPrefix] = { isFile = true, content = content, isBinary = isBinaryFile(absPath) }
end
end
@@ -399,4 +367,4 @@ local function main()
end
-- 启动主逻辑和Basalt事件循环
parallel.waitForAll(basalt.autoUpdate, main)
parallel.waitForAll(basalt.autoUpdate, main)

View File

@@ -48,6 +48,18 @@ class Room:
'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]]:
if room_id not in frontend_to_client_queues:
frontend_to_client_queues[room_id] = []
@@ -88,35 +100,31 @@ class HTTPHandler:
return web.json_response({'error': str(e)}, status=500)
async def handle_frontend_send_message(self, request):
"""前端发送消息到客户端"""
try:
data = await request.json()
raw_body = await request.read()
data = safe_decode_json(raw_body)
room_id = data.get('room_id')
message_data = data.get('message')
if not room_id or not message_data:
return web.json_response({'error': '需要room_id和message参数'}, status=400)
queue = get_frontend_to_client_queue(room_id)
queue.append(message_data)
# 检查是否有挂起的客户端请求并立即响应
if room_id in pending_requests and 'client' in pending_requests[room_id]:
client_req_id = pending_requests[room_id]['client']
if client_req_id in pending_requests:
pending_requests[client_req_id]['event'].set()
logger.info(f"立即响应挂起的客户端请求: {client_req_id}")
response = {
return web.json_response({
'success': True,
'message': '消息已发送到客户端队列'
}
return web.json_response(response)
except json.JSONDecodeError:
logger.error("JSON解析失败")
return web.json_response({'error': '无效的JSON数据'}, status=400)
})
except ValueError as e:
logger.error(f"前端发送消息失败: 无效的JSON或编码错误 - {e}")
return web.json_response({'error': '无效的JSON数据或不支持的文本编码'}, status=400)
except Exception as e:
logger.error(f"前端发送消息失败: {e}")
return web.json_response({'error': str(e)}, status=500)
@@ -204,35 +212,32 @@ class HTTPHandler:
return web.json_response({'error': str(e)}, status=500)
async def handle_client_send_message(self, request):
"""客户端发送消息到前端"""
try:
data = await request.json()
raw_body = await request.read() # 获取原始字节
data = safe_decode_json(raw_body)
room_id = data.get('room_id')
message_data = data.get('message')
if not room_id or not message_data:
return web.json_response({'error': '需要room_id和message参数'}, status=400)
queue = get_client_to_frontend_queue(room_id)
queue.append(message_data)
# 检查是否有挂起的前端请求并立即响应
if room_id in pending_requests and 'frontend' in pending_requests[room_id]:
frontend_req_id = pending_requests[room_id]['frontend']
if frontend_req_id in pending_requests:
pending_requests[frontend_req_id]['event'].set()
logger.info(f"立即响应挂起的前端请求: {frontend_req_id}")
response = {
return web.json_response({
'success': True,
'message': '消息已发送到前端队列'
}
return web.json_response(response)
except json.JSONDecodeError:
logger.error("JSON解析失败")
return web.json_response({'error': '无效的JSON数据'}, status=400)
})
except ValueError as e:
logger.error(f"客户端发送消息失败: 无效的JSON或编码错误 - {e}")
return web.json_response({'error': '无效的JSON数据或不支持的文本编码'}, status=400)
except Exception as e:
logger.error(f"客户端发送消息失败: {e}")
return web.json_response({'error': str(e)}, status=500)
@@ -240,12 +245,13 @@ class HTTPHandler:
async def handle_client_receive_message(self, request):
"""客户端接收来自前端的消息(长轮询)"""
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')
if not room_id:
return web.json_response({'error': '需要room_id参数'}, status=400)
queue = get_frontend_to_client_queue(room_id)
# 立即检查是否有消息
@@ -257,7 +263,7 @@ class HTTPHandler:
}
return web.json_response(response)
# 没有消息,设置长轮询
# 没有消息,设置长轮询(最多等待 58 秒)
req_id = str(uuid.uuid4())
event = asyncio.Event()
@@ -274,10 +280,10 @@ class HTTPHandler:
pending_requests[room_id]['client'] = req_id
try:
# 等待295秒或直到有消息
await asyncio.wait_for(event.wait(), timeout=295)
# ⏱️ 只等待 58 秒(略小于客户端或代理的 60 秒超时)
await asyncio.wait_for(event.wait(), timeout=58)
# 检查队列中是否有消息
# 被唤醒后,检查队列
queue = get_frontend_to_client_queue(room_id)
if queue:
message = queue.pop(0)
@@ -286,37 +292,36 @@ class HTTPHandler:
'message': message
}
else:
# 超时返回空消息
response = {
'success': True,
'message': None
}
except asyncio.TimeoutError:
# 超时返回空消息
# 58秒超时返回空消息
response = {
'success': True,
'message': None
}
# 清理挂起的请求
if req_id in pending_requests:
del pending_requests[req_id]
if room_id in pending_requests and 'client' in pending_requests[room_id]:
del pending_requests[room_id]['client']
pending_requests.pop(req_id, None)
if room_id in pending_requests:
pending_requests[room_id].pop('client', None)
# 如果 room_id 下已无其他引用,也可以清理整个 room 条目(可选)
return web.json_response(response)
except json.JSONDecodeError:
logger.error("JSON解析失败")
except ValueError as e:
logger.error(f"客户端接收消息失败: 无效JSON或编码 - {e}")
return web.json_response({'error': '无效的JSON数据'}, status=400)
except Exception as e:
logger.error(f"客户端接收消息失败: {e}")
# 清理挂起的请求
if 'req_id' in locals() and req_id in pending_requests:
del pending_requests[req_id]
if room_id in pending_requests and 'client' in pending_requests[room_id]:
del pending_requests[room_id]['client']
if 'req_id' in locals():
pending_requests.pop(req_id, None)
if 'room_id' in locals() and room_id in pending_requests:
pending_requests[room_id].pop('client', None)
return web.json_response({'error': str(e)}, status=500)
async def handle_static_file(self, request):

View File

@@ -12,7 +12,7 @@ Demo: http://cc-web-edit.liulikeji.cn
- **远程文件管理**:实时浏览、编辑和管理 CC:Tweaked 计算机中的文件
- **Monaco 编辑器**:基于 VS Code 的 Monaco 编辑器,提供专业的代码编辑体验
- **HTTP 通信**:基于 HTTP 协议的可靠通信
- **低延迟通信**:基于 HTTP 长轮询 + 请求挂起机制,模拟 WebSocket 的低延迟传输
### 文件操作
@@ -27,7 +27,7 @@ Demo: http://cc-web-edit.liulikeji.cn
- **自动命令生成**:根据 URL 参数自动生成连接命令
- **一键复制**:点击即可复制连接命令到剪贴板
- **房间管理**:支持创建和加入房间
- **轮询机制**HTTP 轮询确保连接稳定性
- **实时通信**:高效的请求挂起机制确保接近实时的响应
## 🚀 快速开始
@@ -127,7 +127,7 @@ cp -r dist/* ../PyServer/static/
- **二进制文件**:非文本文件会显示为 `[binary]`,无法在线编辑
- **单客户端**:目前主要支持一个网页端和一个 CC 客户端的配对使用
- **文件大小**:上传文件限制为 1MB
- **轮询延迟**HTTP 轮询机制可能有轻微延迟(默认 2 秒)
- **延迟**HTTP 轮询机制提供接近实时的响应体验
### 计划功能
@@ -140,9 +140,17 @@ cp -r dist/* ../PyServer/static/
- `POST /api/room` - 创建房间
- `POST /api/frontend/send` - 前端发送消息到客户端
- `POST /api/frontend/receive` - 前端接收来自客户端的消息
- `POST /api/frontend/receive` - 前端接收来自客户端的消息(长轮询)
- `POST /api/client/send` - 客户端发送消息到前端
- `POST /api/client/receive` - 客户端接收来自前端的消息
- `POST /api/client/receive` - 客户端接收来自前端的消息(长轮询)
### 通信机制
**长轮询 + 请求挂起**
- 客户端请求挂起最长达 295 秒,直到有消息到达
- 当有新消息时立即响应,实现低延迟传输
- 服务器端控制请求超时,避免不必要的轮询
### 消息类型
@@ -186,27 +194,24 @@ A: 确保 CC 客户端已成功连接,然后刷新文件列表
**Q: 文件上传失败**
A: 检查文件大小是否超过 1MB 限制
**Q: 操作响应较慢**
A: 默认轮询间隔为 1 秒,可通过调整代码中的轮询间隔改善
**Q: 消息传输延迟**
A: 服务器使用长轮询机制,响应通常在毫秒级别
## 📄 技术说明
- **后端**Python + HTTP Server
- **后端**Python + HTTP Server + 长轮询机制
- **前端**Vue 3 + TypeScript + Monaco Editor
- **通信**HTTP 轮询机制实现双向通信
- **通信**HTTP 轮询 + 请求挂起实现低延迟通信
- **客户端**CC:Tweaked + HTTP
## 🤝 开发说明
<<<<<<< HEAD
该项目目前主要支持远程代码编辑功能使用 HTTP 协议替代 WebSocket提高了兼容性和部署便利性远程控制台功能计划在后续版本中开发
=======
该项目目前主要支持远程代码编辑功能远程控制台功能计划在后续版本中开发
该项目使用创新的 HTTP 长轮询 + 请求挂起机制来模拟 WebSocket 的低延迟传输特性,为远程代码编辑提供了近乎实时的响应体验。
远程控制台功能计划在后续版本中开发。
## 贡献
你可以制作适配不同平台的客户端然后共享其代码
欢迎提交issues
>>>>>>> d3faa4b74bc0eeac9a272c4d8a348d98a48dad7e
欢迎提交 issues