105 lines
3.7 KiB
JavaScript
105 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 = null
|
|
}) {
|
|
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 = Number.isFinite(Number(maxLength)) && Number(maxLength) > 0
|
|
? truncateFinal(output, maxLength)
|
|
: output;
|
|
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
|
|
};
|