ai-query对接新版freesun-agent接口的分支
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

221 lines
6.6 KiB

// mermaidSanitizer.js
export function validateAndFixMermaid(raw) {
const warnings = [];
if (!raw || !String(raw).trim()) {
return { ok: false, fixedSource: "", warnings };
}
let text = String(raw)
// HTML 实体
.replace(/&/g, "&")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
// 零宽字符
.replace(/[\u200b-\u200f\u2028\u202f\u205f\u2060\ufeff]/g, "")
// 全角符号
.replace(/[“”]/g, '"')
.replace(/[‘’]/g, "'")
.replace(/\u00a0/g, " ")
// HTML 换行
.replace(/<br\s*\/?>/gi, "\n")
// CRLF
.replace(/\r\n?/g, "\n")
.trim();
// 清理残留的 HTML 标签,但保留 mermaid 语法可能用到的字符
// 仅移除常见的块级和内联标签,避免误伤 mermaid 节点文本(如 A[<Label>])
text = text
.replace(
/<\/?(div|p|span|br|pre|code|table|tr|td|th|thead|tbody|ul|ol|li|h[1-6]|img)\b[^>]*>/gi,
""
)
.trim();
// 移除可能混入的 Markdown 图片语法
text = text
.split("\n")
.filter((line) => !/^\s*!\[[^\]]*]\([^)]+\)\s*$/.test(line))
.join("\n")
.trim();
// 修复被压成一行的语句,避免多个语句连在一起导致语法错误
const injectLineBreaks = (value) => {
let out = String(value || "");
out = out.replace(/\s+(?=section\b)/gi, "\n");
out = out.replace(
/([^\n])\s+(?=[A-Za-z0-9_]+\s*(-->|--|==>|==|<--|<-->)) /g,
"$1\n"
);
out = out.replace(
/([^\n])\s+(?=[A-Za-z0-9_]+\s*(-->|--|==>|==|<--|<-->))/g,
"$1\n"
);
return out;
};
text = injectLineBreaks(text);
// 去掉代码围栏
text = text
.replace(/^\s*```mermaid\s*/i, "")
.replace(/^\s*```/gm, "")
.replace(/^\s*~~~mermaid\s*/i, "")
.replace(/^\s*~~~/gm, "")
.trim();
const lines = text.split("\n").filter(Boolean);
if (!lines.length) {
return { ok: false, fixedSource: "", warnings };
}
// 自动为包含特殊字符(如 |)的节点文本添加引号
// 处理 [] [[ ]]
text = text.replace(
/([A-Za-z0-9_\u4e00-\u9fa5]+)\s*(\[+)([^"\]\n]*\|[^"\]\n]*)(\]+)/g,
(match, id, open, content, close) => {
warnings.push(`自动为包含特殊字符的节点添加引号: ${id}`);
return `${id}${open}"${content}"${close}`;
}
);
// 处理 () (( ))
text = text.replace(
/([A-Za-z0-9_\u4e00-\u9fa5]+)\s*(\(+)([^"\)\n]*\|[^"\)\n]*)(\)+)/g,
(match, id, open, content, close) => {
warnings.push(`自动为包含特殊字符的节点添加引号: ${id}`);
return `${id}${open}"${content}"${close}`;
}
);
// 处理 {} {{ }}
text = text.replace(
/([A-Za-z0-9_\u4e00-\u9fa5]+)\s*(\{+)([^"\}\n]*\|[^"\}\n]*)(\}+)/g,
(match, id, open, content, close) => {
warnings.push(`自动为包含特殊字符的节点添加引号: ${id}`);
return `${id}${open}"${content}"${close}`;
}
);
// 修复非法边标签
text = text.replace(
/(\S+)\s*--\s*([^|\n-]+?)\s*-->\s*(\S+)/g,
(_, from, label, to) => {
warnings.push(`修复非法边标签: "${label.trim()}"`);
return `${from} --|${label.trim()}|--> ${to}`;
}
);
// subgraph 中文名加引号
text = text.replace(/^\s*subgraph\s+(.+)$/gim, (_, name) => {
const n = name.trim();
if (/^["'].*["']$/.test(n)) return `subgraph ${n}`;
if (/[\u4e00-\u9fa5\s]/.test(n)) {
warnings.push(`subgraph 名称加引号: ${n}`);
return `subgraph "${n}"`;
}
return `subgraph ${n}`;
});
return {
ok: true,
fixedSource: text.trim(),
warnings,
};
}
const normalizeEscapedNewlines = (raw) => {
const source = String(raw || "");
let result = "";
let bracketDepth = 0;
let parenthesisDepth = 0;
let braceDepth = 0;
let pipeLabel = false;
for (let index = 0; index < source.length; index += 1) {
const current = source[index];
const next = source[index + 1];
const inLabel =
bracketDepth > 0 ||
parenthesisDepth > 0 ||
braceDepth > 0 ||
pipeLabel;
if (current === "\\" && next === "n") {
result += inLabel ? "<br/>" : "\n";
index += 1;
continue;
}
if (current === "\n") {
result += inLabel ? "<br/>" : "\n";
continue;
}
if (current === "[") bracketDepth += 1;
if (current === "]" && bracketDepth > 0) bracketDepth -= 1;
if (current === "(") parenthesisDepth += 1;
if (current === ")" && parenthesisDepth > 0) parenthesisDepth -= 1;
if (current === "{") braceDepth += 1;
if (current === "}" && braceDepth > 0) braceDepth -= 1;
if (
current === "|" &&
bracketDepth === 0 &&
parenthesisDepth === 0 &&
braceDepth === 0
) {
pipeLabel = !pipeLabel;
}
result += current;
}
return result;
};
const normalizeMermaidRenderSource = (raw) => {
let text = String(raw || "")
.replace(/\r\n?/g, "\n")
.replace(/<br\s*\/?>/gi, "\n")
.replace(/[“”]/g, '"')
.replace(/[‘’]/g, "'")
.replace(/[:]/g, ":")
.replace(/\u00a0/g, " ");
text = normalizeEscapedNewlines(text)
.replace(/^\s*>\s?/gm, "")
.replace(/^\s*code\b[:\s]*/gim, "")
.replace(/^\s*```(?:mermaid)?\s*/gim, "")
.replace(/^\s*~~~(?:mermaid)?\s*/gim, "")
.replace(/^\s*subgraph\s+(.+)$/gim, (_, name) => {
const trimmedName = String(name || "").trim();
if (!trimmedName || /^["'].*["']$/.test(trimmedName)) {
return `subgraph ${trimmedName}`.trim();
}
if (/[\s\u4e00-\u9fa5]/.test(trimmedName)) {
return `subgraph "${trimmedName}"`;
}
return `subgraph ${trimmedName}`;
})
.trim();
return text;
};
/**
* 功能:生成 Mermaid 节点视图可依次尝试的渲染源码。
* 使用场景:兼容接口返回的转义换行、中文 subgraph 和旧 Mermaid 源码。
*
* 入参:原始 Mermaid 源码。
*
* 返回:去重后的候选源码数组,首项为常规清洗结果。
*
* 注意:调用方应依次尝试候选源码,不能在首个候选失败后直接清空预览。
*/
export function getMermaidRenderCandidates(raw) {
const { fixedSource } = validateAndFixMermaid(raw);
const candidates = [fixedSource, normalizeMermaidRenderSource(fixedSource)];
return [...new Set(candidates.map((item) => String(item || "").trim()))].filter(
Boolean
);
}