200 lines
8.1 KiB
JavaScript
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
|
|
};
|