141 lines
4.8 KiB
JavaScript
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
|
|
};
|