141 lines
4.0 KiB
Lua
141 lines
4.0 KiB
Lua
-- WebSocket transport for the opencode bridge proxy.
|
|
--
|
|
-- Implements the same client surface as libhttp (getJson/postJson plus the
|
|
-- trimTrailingSlash/queryString helpers libai calls), but performs each call as
|
|
-- a blocking websocket round-trip to the mcp-bridge opencode proxy instead of a
|
|
-- direct http.get/post. ComputerCraft's http API caps requests at ~60s; a
|
|
-- websocket round-trip has no such cap, so the bridge can run a synchronous
|
|
-- opencode request server-side for as long as it needs.
|
|
|
|
local DEFAULT_RECEIVE_TIMEOUT_SECONDS = 600;
|
|
|
|
local function isBlank(s)
|
|
return type(s) ~= 'string' or string.match(s, '^%s*$') ~= nil;
|
|
end
|
|
|
|
local function trimTrailingSlash(s)
|
|
return (s:gsub('/+$', ''));
|
|
end
|
|
|
|
local function urlEncode(s)
|
|
return (tostring(s):gsub('[^%w%-_%.~]', function(c)
|
|
return string.format('%%%02X', string.byte(c));
|
|
end));
|
|
end
|
|
|
|
local function queryString(params)
|
|
local parts = {};
|
|
for _, item in ipairs(params) do
|
|
if not isBlank(item[2]) then
|
|
parts[#parts + 1] = urlEncode(item[1]) .. '=' .. urlEncode(item[2]);
|
|
end
|
|
end
|
|
if #parts == 0 then return ''; end
|
|
return '?' .. table.concat(parts, '&');
|
|
end
|
|
|
|
local function createHttpWs(opts)
|
|
opts = opts or {};
|
|
local httpLib = opts.http or http;
|
|
local textutilsLib = opts.textutils or textutils;
|
|
local bridgeUrl = opts.bridgeUrl;
|
|
local receiveTimeout = tonumber(opts.receiveTimeout) or DEFAULT_RECEIVE_TIMEOUT_SECONDS;
|
|
|
|
local api = {
|
|
trimTrailingSlash = trimTrailingSlash,
|
|
urlEncode = urlEncode,
|
|
queryString = queryString,
|
|
};
|
|
|
|
local activeWs = nil;
|
|
|
|
local function ensureSocket()
|
|
if activeWs then return activeWs, nil; end
|
|
if isBlank(bridgeUrl) then
|
|
return nil, 'missing opencc.bridge_url';
|
|
end
|
|
local ws, err = httpLib.websocket(bridgeUrl);
|
|
if not ws then
|
|
return nil, 'bridge unreachable: ' .. tostring(err);
|
|
end
|
|
activeWs = ws;
|
|
return ws, nil;
|
|
end
|
|
|
|
local function closeSocket()
|
|
if activeWs then
|
|
pcall(function() activeWs.close(); end);
|
|
activeWs = nil;
|
|
end
|
|
end
|
|
|
|
local idCounter = 0;
|
|
local function nextId()
|
|
idCounter = idCounter + 1;
|
|
local stamp = os.epoch and os.epoch('utc') or os.clock();
|
|
return 'req_' .. tostring(stamp) .. '_' .. tostring(idCounter) .. '_' .. tostring(math.random(100000, 999999));
|
|
end
|
|
|
|
local function trySend(ws, text)
|
|
return pcall(function() ws.send(text); end);
|
|
end
|
|
|
|
local function roundtrip(method, path, payload)
|
|
local ws, err = ensureSocket();
|
|
if not ws then return nil, err; end
|
|
|
|
local id = nextId();
|
|
local frame = { type = 'http', id = id, method = method, path = path };
|
|
if payload ~= nil then
|
|
frame.body = textutilsLib.serializeJSON(payload);
|
|
end
|
|
local text = textutilsLib.serializeJSON(frame);
|
|
|
|
if not trySend(ws, text) then
|
|
-- Socket went away; drop it and try one fresh reconnect.
|
|
closeSocket();
|
|
ws, err = ensureSocket();
|
|
if not ws then return nil, err; end
|
|
if not trySend(ws, text) then
|
|
closeSocket();
|
|
return nil, 'bridge send failed';
|
|
end
|
|
end
|
|
|
|
while true do
|
|
local ok, message = pcall(function() return ws.receive(receiveTimeout); end);
|
|
if not ok then
|
|
closeSocket();
|
|
return nil, 'bridge connection closed';
|
|
end
|
|
if message == nil then
|
|
return nil, 'timeout waiting for bridge response';
|
|
end
|
|
local decoded = textutilsLib.unserializeJSON(message);
|
|
if type(decoded) == 'table' and decoded.type == 'http-response' and decoded.id == id then
|
|
if decoded.status == nil or decoded.status == 0 then
|
|
return nil, decoded.error or 'bridge error';
|
|
end
|
|
return decoded.body, decoded.status;
|
|
end
|
|
-- Ignore frames that don't correlate (stale/other ids) and keep waiting.
|
|
end
|
|
end
|
|
|
|
function api.getJson(_cfg, path)
|
|
return roundtrip('GET', path, nil);
|
|
end
|
|
|
|
function api.postJson(_cfg, path, payload)
|
|
return roundtrip('POST', path, payload);
|
|
end
|
|
|
|
function api.close()
|
|
closeSocket();
|
|
end
|
|
|
|
return api;
|
|
end
|
|
|
|
return createHttpWs;
|