Lumi/plugins/lumi_ai/backend/response_formatter.js
2026-06-12 11:54:46 +02:00

103 lines
3.7 KiB
JavaScript

const {
normalizeLink,
absoluteInternalUrl,
routeLabel,
stripTags
} = require("./link_normalizer");
const METHODS = /\b(?:GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\s+(\/[^\s<>"')\]]+)/gi;
const MARKDOWN_LINK = /\[([^\]]+)\]\(([^)]+)\)/g;
const HTML_LINK = /<a\b[^>]*\bhref\s*=\s*(["'])(.*?)\1[^>]*>(.*?)<\/a>/gi;
const REPO_PATH = /\b(?:src|plugins|scripts|data)\/[a-z0-9_./-]+\.(?:js|json|ejs|md|css|txt)\b/gi;
const SOURCE_DETAIL = /\s*\((?:src|plugins|scripts)\/[^)]+\)/gi;
function formatAssistantResponse({
text,
links = [],
baseUrl = "",
verifiedRoutes = [],
role = "user",
allowModeratorCodeHelp = false,
maxLength = 4000
}) {
const routeSet = new Set(verifiedRoutes);
const collected = [];
let output = normalizeCodeFences(String(text || ""));
output = output.replace(HTML_LINK, (_match, _quote, href, label) => {
const normalized = collectLink(collected, { href, label: stripTags(label), external: true }, baseUrl, routeSet);
return normalized ? markdownLink(normalized) : stripTags(label);
});
output = output.replace(MARKDOWN_LINK, (_match, label, href) => {
const normalized = collectLink(collected, { href, label, external: true }, baseUrl, routeSet);
return normalized ? markdownLink(normalized) : label;
});
output = output.replace(METHODS, (_match, route) => {
const punctuation = route.match(/[.,;:!?]+$/)?.[0] || "";
const cleanRoute = punctuation ? route.slice(0, -punctuation.length) : route;
const normalized = collectLink(collected, { href: cleanRoute, label: routeLabel(cleanRoute) }, baseUrl, routeSet);
return normalized ? `${markdownLink(normalized)}${punctuation}` : route;
});
for (const link of Array.isArray(links) ? links : []) {
collectLink(collected, link, baseUrl, routeSet);
}
output = output
.replace(/<br\s*\/?>/gi, "\n")
.replace(/<\/p\s*>/gi, "\n\n")
.replace(/<[^>]*>/g, "");
if (role !== "admin") {
output = output.replace(REPO_PATH, "internal Lumi component").replace(SOURCE_DETAIL, "");
if (role === "user" || !allowModeratorCodeHelp) {
output = output.replace(/```[\s\S]*?```/g, "Technical implementation details are available to administrators.");
}
}
output = output.replace(/[ \t]+\n/g, "\n").replace(/\n{4,}/g, "\n\n\n").trim();
const originalLength = output.length;
const delivered = truncateFinal(output, maxLength);
return {
text: delivered,
links: uniqueLinks(collected),
original_final_length: originalLength,
delivered_length: delivered.length,
truncated: delivered.length < originalLength
};
}
function collectLink(output, link, baseUrl, routeSet) {
const normalized = normalizeLink(link, baseUrl, routeSet);
if (normalized) output.push(normalized);
return normalized;
}
function truncateFinal(value, maxLength) {
const limit = Math.max(100, Number.parseInt(maxLength, 10) || 4000);
if (value.length <= limit) return value;
const suffix = " [reply truncated]";
return `${value.slice(0, Math.max(1, limit - suffix.length)).trimEnd()}${suffix}`;
}
function markdownLink(link) {
const label = String(link.label || "").replace(/[[\]]/g, "");
const href = String(link.href || "").replace(/[<>\s]/g, (character) => encodeURIComponent(character));
return `[${label}](${href})`;
}
function uniqueLinks(links) {
return [...new Map(links.map((link) => [link.href, link])).values()];
}
function normalizeCodeFences(value) {
return value.replace(/```(javascript|python)\s+([^\n][\s\S]*?)```/gi, (_match, language, code) =>
`\`\`\`${language.toLowerCase()}\n${code.trim()}\n\`\`\``);
}
module.exports = {
formatAssistantResponse,
normalizeLink,
truncateFinal,
absoluteInternalUrl,
normalizeCodeFences
};