Lumi/plugins/lumi_ai_web_search/backend/settings.js
2026-06-14 05:01:13 +02:00

200 lines
8.1 KiB
JavaScript

const fs = require("fs");
const path = require("path");
const metadata = require("../tool_info.json");
const ORIGINS = new Set(["webui", "discord", "twitch", "youtube", "kick", "other"]);
function defaults() {
return Object.fromEntries(
Object.entries(metadata.settings_schema).map(([key, field]) => [key, structuredClone(field.default)])
);
}
function readSettings(dataDir) {
const fallback = defaults();
try {
const stored = JSON.parse(fs.readFileSync(settingsPath(dataDir), "utf8"));
return normalizeSettings(migrateSettings(stored, fallback));
} catch {
return normalizeSettings(fallback);
}
}
function ensureSettings(dataDir) {
if (!fs.existsSync(settingsPath(dataDir))) return writeSettings(dataDir, defaults());
return readSettings(dataDir);
}
function writeSettings(dataDir, value) {
const normalized = normalizeSettings(migrateSettings(value, defaults()));
fs.mkdirSync(dataDir, { recursive: true });
const file = settingsPath(dataDir);
const temporary = `${file}.${process.pid}.tmp`;
fs.writeFileSync(temporary, `${JSON.stringify(normalized, null, 2)}\n`, { mode: 0o600 });
try { fs.chmodSync(temporary, 0o600); } catch {}
replaceFile(temporary, file);
return normalized;
}
function migrateSettings(value = {}, fallback = defaults()) {
const migrated = { ...fallback, ...value };
if (!value.provider) migrated.provider = value.provider_endpoint ? "external_json" : "lumi_search_broker";
if (value.enabled != null && value.enable_search == null) migrated.enable_search = value.enabled === true;
if (value.safe_search && value.safe_search_level == null) migrated.safe_search_level = value.safe_search;
if (value.provider_adapter && value.external_provider_adapter == null) {
migrated.external_provider_adapter = value.provider_adapter;
}
if (value.provider_endpoint && value.external_provider_endpoint == null) {
migrated.external_provider_endpoint = value.provider_endpoint;
}
if (value.provider_api_key && value.external_provider_api_key == null) {
migrated.external_provider_api_key = value.provider_api_key;
}
if (value.provider_api_key_header && value.external_provider_api_key_header == null) {
migrated.external_provider_api_key_header = value.provider_api_key_header;
}
if (value.provider_api_key_prefix && value.external_provider_api_key_prefix == null) {
migrated.external_provider_api_key_prefix = value.provider_api_key_prefix;
}
if (value.provider_query_parameter && value.external_provider_query_parameter == null) {
migrated.external_provider_query_parameter = value.provider_query_parameter;
}
if (value.requests_per_minute && value.per_user_per_minute == null) {
migrated.per_user_per_minute = value.requests_per_minute;
}
if (typeof value.show_source_links === "boolean") {
for (const origin of ["webui", "discord", "twitch"]) {
const key = `show_source_links_${origin}`;
if (value[key] == null) migrated[key] = value.show_source_links;
}
}
return migrated;
}
function normalizeSettings(value) {
return {
provider: value.provider === "external_json" ? "external_json" : "lumi_search_broker",
enable_search: value.enable_search !== false,
enable_fetch_url: value.enable_fetch_url !== false,
enable_summarize_url: value.enable_summarize_url !== false,
enable_headless_browser_fallback: value.enable_headless_browser_fallback === true,
policy_mode: value.policy_mode === "whitelist" ? "whitelist" : "blacklist",
url_rules: stringList(value.url_rules, 200),
max_results: integer(value.max_results, 1, 10, 5),
search_timeout_ms: integer(value.search_timeout_ms, 1000, 30000, 8000),
fetch_timeout_ms: integer(value.fetch_timeout_ms, 1000, 30000, 10000),
max_fetch_bytes: integer(value.max_fetch_bytes, 65536, 4194304, 1048576),
max_extracted_chars: integer(value.max_extracted_chars, 1000, 50000, 12000),
max_redirects: integer(value.max_redirects, 0, 8, 3),
cache_ttl_seconds: integer(value.cache_ttl_seconds, 0, 86400, 900),
safe_search_level: ["off", "moderate", "strict"].includes(value.safe_search_level)
? value.safe_search_level
: "moderate",
allowed_origins: stringList(value.allowed_origins, 6).filter((origin) => ORIGINS.has(origin)),
show_source_links_webui: value.show_source_links_webui !== false,
show_source_links_discord: value.show_source_links_discord !== false,
show_source_links_twitch: value.show_source_links_twitch === true,
webui_output_chars: integer(value.webui_output_chars, 300, 12000, 2500),
discord_output_chars: integer(value.discord_output_chars, 200, 4000, 1200),
twitch_output_chars: integer(value.twitch_output_chars, 120, 1000, 450),
youtube_output_chars: integer(value.youtube_output_chars, 120, 3000, 1200),
kick_output_chars: integer(value.kick_output_chars, 120, 1000, 450),
other_output_chars: integer(value.other_output_chars, 120, 3000, 700),
per_user_per_minute: integer(value.per_user_per_minute, 1, 60, 6),
per_origin_per_minute: integer(value.per_origin_per_minute, 1, 300, 30),
per_server_per_minute: integer(value.per_server_per_minute, 1, 300, 20),
external_provider_adapter: value.external_provider_adapter === "generic_json"
? "generic_json"
: "searxng_json",
external_provider_endpoint: String(value.external_provider_endpoint || "").trim(),
external_provider_api_key: String(value.external_provider_api_key || "").trim(),
external_provider_api_key_header: value.external_provider_api_key_header === "Authorization"
? "Authorization"
: "X-API-Key",
external_provider_api_key_prefix: String(value.external_provider_api_key_prefix || "").trim().slice(0, 32),
external_provider_query_parameter: /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/.test(
String(value.external_provider_query_parameter || "")
) ? String(value.external_provider_query_parameter) : "q"
};
}
function capabilityEnabled(settings, capability) {
return {
search: settings.enable_search,
fetch_url: settings.enable_fetch_url,
summarize_url: settings.enable_summarize_url
}[capability] === true;
}
function capabilityAvailable(settings, capability) {
if (!capabilityEnabled(settings, capability)) return false;
if (capability === "search" &&
settings.provider === "external_json" &&
!settings.external_provider_endpoint) return false;
return true;
}
function sourceLinksEnabled(settings, origin) {
if (origin === "twitch") return settings.show_source_links_twitch;
if (origin === "discord") return settings.show_source_links_discord;
return settings.show_source_links_webui;
}
function settingsPath(dataDir) {
return path.join(dataDir, "settings.json");
}
function statusPath(dataDir) {
return path.join(dataDir, "status.json");
}
function readStatus(dataDir) {
try { return JSON.parse(fs.readFileSync(statusPath(dataDir), "utf8")); }
catch { return {}; }
}
function writeStatus(dataDir, patch) {
const next = { ...readStatus(dataDir), ...patch, updated_at: new Date().toISOString() };
fs.mkdirSync(dataDir, { recursive: true });
const file = statusPath(dataDir);
const temporary = `${file}.${process.pid}.tmp`;
fs.writeFileSync(temporary, `${JSON.stringify(next, null, 2)}\n`);
replaceFile(temporary, file);
return next;
}
function replaceFile(source, destination) {
try { fs.renameSync(source, destination); }
catch (error) {
if (!["EEXIST", "EPERM"].includes(error.code)) throw error;
fs.rmSync(destination, { force: true });
fs.renameSync(source, destination);
}
}
function integer(value, minimum, maximum, fallback) {
const number = Number.parseInt(value, 10);
return Number.isFinite(number) ? Math.max(minimum, Math.min(maximum, number)) : fallback;
}
function stringList(value, limit) {
const rows = Array.isArray(value) ? value : String(value || "").split(/\r?\n|,/);
return [...new Set(rows.map((entry) => String(entry).trim()).filter(Boolean))].slice(0, limit);
}
module.exports = {
capabilityAvailable,
capabilityEnabled,
defaults,
ensureSettings,
migrateSettings,
normalizeSettings,
readSettings,
readStatus,
settingsPath,
sourceLinksEnabled,
statusPath,
writeSettings,
writeStatus
};