222 lines
6.3 KiB
Lua
222 lines
6.3 KiB
Lua
local _VERSION = '0.4.0';
|
|
|
|
local PING_PROMPT = 'reply with exactly: pong';
|
|
|
|
local DEFAULT_TIMEOUT_SECONDS = 1200;
|
|
|
|
local B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
|
|
local function base64encode(s)
|
|
local pad = (3 - #s % 3) % 3;
|
|
s = s .. string.rep('\0', pad);
|
|
local r = {};
|
|
for i = 1, #s, 3 do
|
|
local a, b, c = s:byte(i), s:byte(i + 1), s:byte(i + 2);
|
|
local n = a * 65536 + b * 256 + c;
|
|
r[#r + 1] = B64:sub(math.floor(n / 262144) % 64 + 1, math.floor(n / 262144) % 64 + 1)
|
|
.. B64:sub(math.floor(n / 4096) % 64 + 1, math.floor(n / 4096) % 64 + 1)
|
|
.. B64:sub(math.floor(n / 64) % 64 + 1, math.floor(n / 64) % 64 + 1)
|
|
.. B64:sub(n % 64 + 1, n % 64 + 1);
|
|
end
|
|
local result = table.concat(r);
|
|
if pad > 0 then
|
|
result = result:sub(1, #result - pad) .. string.rep('=', pad);
|
|
end
|
|
return result;
|
|
end
|
|
|
|
local function trimTrailingSlash(s)
|
|
return (s:gsub('/+$', ''));
|
|
end
|
|
|
|
local function isBlank(s)
|
|
return type(s) ~= 'string' or string.match(s, '^%s*$') ~= nil;
|
|
end
|
|
|
|
local function readAllAndClose(response)
|
|
local body = response.readAll();
|
|
response.close();
|
|
return body;
|
|
end
|
|
|
|
local function statusCode(response)
|
|
if response.getResponseCode then
|
|
return response.getResponseCode();
|
|
end
|
|
return nil;
|
|
end
|
|
|
|
local function extractTextParts(parts)
|
|
local texts = {};
|
|
for _, part in ipairs(parts) do
|
|
if part.type == 'text' and type(part.text) == 'string' then
|
|
texts[#texts + 1] = part.text;
|
|
end
|
|
end
|
|
return table.concat(texts, '');
|
|
end
|
|
|
|
local function createAi(opts)
|
|
opts = opts or {};
|
|
|
|
local httpLib = opts.http or http;
|
|
local settingsLib = opts.settings or settings;
|
|
|
|
local api = {};
|
|
|
|
function api.version()
|
|
return _VERSION;
|
|
end
|
|
|
|
local function resolveTimeout(options)
|
|
local raw = options.timeoutSeconds;
|
|
if raw == nil then raw = settingsLib.get('opencc.timeout_seconds'); end
|
|
local n = tonumber(raw);
|
|
if n and n > 0 then return n; end
|
|
return DEFAULT_TIMEOUT_SECONDS;
|
|
end
|
|
|
|
local function resolveConfig(options)
|
|
local url = options.serverUrl or settingsLib.get('opencc.server_url');
|
|
if not url or url == '' then
|
|
return nil, 'missing opencc.server_url; run: set opencc.server_url <url>';
|
|
end
|
|
local username = options.username or settingsLib.get('opencc.username') or 'opencode';
|
|
local password = options.password or settingsLib.get('opencc.password') or '';
|
|
return {
|
|
url = trimTrailingSlash(url),
|
|
username = username,
|
|
password = password,
|
|
timeoutSeconds = resolveTimeout(options),
|
|
};
|
|
end
|
|
|
|
local function buildHeaders(cfg)
|
|
local headers = {
|
|
['Content-Type'] = 'application/json',
|
|
['Accept'] = 'application/json',
|
|
};
|
|
if cfg.password and cfg.password ~= '' then
|
|
headers['Authorization'] = 'Basic ' .. base64encode(cfg.username .. ':' .. cfg.password);
|
|
end
|
|
return headers;
|
|
end
|
|
|
|
local function doGet(cfg, path)
|
|
local response, _, errorResponse = httpLib.get({
|
|
url = cfg.url .. path,
|
|
headers = buildHeaders(cfg),
|
|
timeout = cfg.timeoutSeconds,
|
|
});
|
|
response = response or errorResponse;
|
|
if not response then
|
|
return nil, 'serveur injoignable';
|
|
end
|
|
local code = statusCode(response);
|
|
local body = readAllAndClose(response);
|
|
return body, code;
|
|
end
|
|
|
|
local function doPost(cfg, path, payload)
|
|
local response, _, errorResponse = httpLib.post({
|
|
url = cfg.url .. path,
|
|
body = textutils.serializeJSON(payload),
|
|
headers = buildHeaders(cfg),
|
|
timeout = cfg.timeoutSeconds,
|
|
});
|
|
response = response or errorResponse;
|
|
if not response then
|
|
return nil, 'serveur injoignable';
|
|
end
|
|
local code = statusCode(response);
|
|
local body = readAllAndClose(response);
|
|
return body, code;
|
|
end
|
|
|
|
function api.clearSession()
|
|
settingsLib.unset('opencc.session_id');
|
|
if settingsLib.save then settingsLib.save(); end
|
|
end
|
|
|
|
function api.listSessions(options)
|
|
options = options or {};
|
|
local cfg, err = resolveConfig(options);
|
|
if not cfg then return false, err; end
|
|
|
|
local body, code = doGet(cfg, '/session');
|
|
if not body then return false, code; end
|
|
if code and code ~= 200 then
|
|
return false, 'erreur serveur: HTTP ' .. tostring(code);
|
|
end
|
|
|
|
local decoded = textutils.unserializeJSON(body);
|
|
if type(decoded) ~= 'table' then
|
|
return false, 'reponse invalide';
|
|
end
|
|
return true, decoded;
|
|
end
|
|
|
|
function api.ask(prompt, options)
|
|
options = options or {};
|
|
if isBlank(prompt) then
|
|
return false, 'missing prompt; usage: ai <prompt>';
|
|
end
|
|
|
|
local cfg, err = resolveConfig(options);
|
|
if not cfg then return false, err; end
|
|
|
|
local sessionId = options.sessionId;
|
|
if sessionId == nil then
|
|
sessionId = settingsLib.get('opencc.session_id');
|
|
end
|
|
|
|
if not sessionId or sessionId == '' then
|
|
local body, code = doPost(cfg, '/session', { title = 'cc-ai' });
|
|
if not body then return false, code; end
|
|
if code and code ~= 200 then
|
|
return false, 'impossible de creer une session: HTTP ' .. tostring(code);
|
|
end
|
|
local decoded = textutils.unserializeJSON(body);
|
|
if type(decoded) ~= 'table' or type(decoded.id) ~= 'string' then
|
|
return false, 'reponse session invalide';
|
|
end
|
|
sessionId = decoded.id;
|
|
settingsLib.set('opencc.session_id', sessionId);
|
|
if settingsLib.save then settingsLib.save(); end
|
|
end
|
|
|
|
local body, code = doPost(cfg, '/session/' .. sessionId .. '/message', {
|
|
parts = { { type = 'text', text = prompt } },
|
|
});
|
|
if not body then return false, code; end
|
|
if code == 404 then
|
|
settingsLib.unset('opencc.session_id');
|
|
if settingsLib.save then settingsLib.save(); end
|
|
return false, 'session introuvable; lance: ai new <prompt>';
|
|
end
|
|
if code and code ~= 200 then
|
|
return false, 'erreur message: HTTP ' .. tostring(code);
|
|
end
|
|
|
|
local decoded = textutils.unserializeJSON(body);
|
|
if type(decoded) ~= 'table' or type(decoded.parts) ~= 'table' then
|
|
return false, 'reponse message invalide';
|
|
end
|
|
|
|
local reply = extractTextParts(decoded.parts);
|
|
if reply == '' then
|
|
return false, 'reponse vide';
|
|
end
|
|
|
|
return true, { reply = reply, sessionId = sessionId };
|
|
end
|
|
|
|
function api.ping(options)
|
|
return api.ask(PING_PROMPT, options);
|
|
end
|
|
|
|
return api;
|
|
end
|
|
|
|
return createAi;
|