Lumi/src/services/command-conditional.js
2026-07-19 20:34:37 +02:00

141 lines
4.8 KiB
JavaScript

const MAX_CONDITIONAL_REPLIES = 100;
function normalizeConditionalKey(value) {
return String(value || "")
.trim()
.toLowerCase()
.replace(/[^\p{L}\p{N}]+/gu, " ")
.replace(/\s+/g, " ")
.trim();
}
function splitConditionalKeywords(value) {
return String(value || "")
.split(",")
.map((keyword) => keyword.trim().slice(0, 100))
.filter(Boolean);
}
function normalizeConditionalReplies(value) {
return normalizeConditionalReplyRows(value).flatMap((entry) => splitConditionalKeywords(entry.keyword).map((keyword) => ({
keyword,
normalized: normalizeConditionalKey(keyword),
response: entry.response
})));
}
function normalizeConditionalReplyRows(value) {
let input = value;
if (typeof input === "string") {
try { input = JSON.parse(input); } catch { input = []; }
}
if (!Array.isArray(input)) return [];
const seen = new Set();
const rows = [];
let aliasCount = 0;
for (const entry of input) {
const response = String(entry?.response || "").trim().slice(0, 4000);
if (!response) continue;
const aliases = [];
for (const keyword of splitConditionalKeywords(entry?.keyword)) {
const normalized = normalizeConditionalKey(keyword);
if (!normalized || seen.has(normalized)) continue;
seen.add(normalized);
aliases.push(keyword);
aliasCount += 1;
if (aliasCount >= MAX_CONDITIONAL_REPLIES) break;
}
if (aliases.length) rows.push({ keyword: aliases.join(", "), response });
if (aliasCount >= MAX_CONDITIONAL_REPLIES) break;
}
return rows;
}
function conditionalRepliesFromBody(body = {}) {
const keywords = asArray(body.conditional_keyword);
const responses = asArray(body.conditional_response);
const replies = [];
const errors = [];
const seen = new Set();
let aliasCount = 0;
const count = Math.max(keywords.length, responses.length);
for (let index = 0; index < count; index += 1) {
const keyword = String(keywords[index] || "").trim();
const aliases = splitConditionalKeywords(keyword);
const response = String(responses[index] || "").trim();
if (!keyword && !response) continue;
if (!aliases.length || !response) {
errors.push("Each conditional reply needs both a keyword and a response.");
continue;
}
for (const alias of aliases) {
const normalized = normalizeConditionalKey(alias);
if (seen.has(normalized)) errors.push(`The keyword “${alias}” is listed more than once.`);
seen.add(normalized);
}
aliasCount += aliases.length;
replies.push({ keyword: aliases.join(", "), response: response.slice(0, 4000) });
}
if (!replies.length) errors.push("Add at least one keyword reply.");
if (aliasCount > MAX_CONDITIONAL_REPLIES) errors.push(`Use no more than ${MAX_CONDITIONAL_REPLIES} keywords.`);
return { ok: errors.length === 0, errors, replies };
}
function findConditionalReply(replies, argument, { fuzzy = true } = {}) {
const normalizedArgument = normalizeConditionalKey(argument);
if (!normalizedArgument) return { match: null, fuzzy: false };
const normalizedReplies = normalizeConditionalReplies(replies);
const exact = normalizedReplies.find((entry) => entry.normalized === normalizedArgument);
if (exact) return { match: exact, fuzzy: false };
if (!fuzzy) return { match: null, fuzzy: false };
const candidates = normalizedReplies
.map((entry) => ({ entry, score: similarity(normalizedArgument, entry.normalized) }))
.sort((left, right) => right.score - left.score);
const best = candidates[0];
const runnerUp = candidates[1];
const minimum = normalizedArgument.length <= 4 ? 0.82 : 0.68;
if (!best || best.score < minimum || (runnerUp && best.score - runnerUp.score < 0.08)) {
return { match: null, fuzzy: false };
}
return { match: best.entry, fuzzy: true };
}
function similarity(left, right) {
if (left === right) return 1;
const distance = levenshtein(left, right);
return 1 - distance / Math.max(left.length, right.length, 1);
}
function levenshtein(left, right) {
const previous = Array.from({ length: right.length + 1 }, (_, index) => index);
for (let i = 1; i <= left.length; i += 1) {
let diagonal = previous[0];
previous[0] = i;
for (let j = 1; j <= right.length; j += 1) {
const above = previous[j];
previous[j] = Math.min(
previous[j] + 1,
previous[j - 1] + 1,
diagonal + (left[i - 1] === right[j - 1] ? 0 : 1)
);
diagonal = above;
}
}
return previous[right.length];
}
function asArray(value) {
if (Array.isArray(value)) return value;
return value === undefined || value === null ? [] : [value];
}
module.exports = {
conditionalRepliesFromBody,
findConditionalReply,
normalizeConditionalKey,
normalizeConditionalReplies,
normalizeConditionalReplyRows,
splitConditionalKeywords
};