91 lines
3.0 KiB
JavaScript
91 lines
3.0 KiB
JavaScript
"use strict";
|
||
|
||
const SKILL_LINK_RE = /\[\$([^\]\n]+)\]\(([^)\n]*)\)/g;
|
||
const LEADING_SKILL_LINK_RE = /^\s*\[\$([^\]\n]+)\]\(([^)\n]*)\)\s*/;
|
||
const TRUNCATED_LEADING_SKILL_LINK_RE = /^\s*\[\$([^\]\n]+)\]\([^)\n]*$/;
|
||
const LEADING_SKILL_TOKEN_RE = /^\s*\$[A-Za-z][\w:.-]*(?:\s+|$)/;
|
||
|
||
function normalizeInlineText(text) {
|
||
return String(text || "")
|
||
.replace(/\s+/g, " ")
|
||
.trim();
|
||
}
|
||
|
||
function cleanSkillLinks(text) {
|
||
return String(text || "").replace(SKILL_LINK_RE, (_m, name) => `$${String(name || "").trim()}`);
|
||
}
|
||
|
||
function stripLeadingSkillMentions(text) {
|
||
let out = cleanSkillLinks(text);
|
||
let changed = true;
|
||
while (changed) {
|
||
changed = false;
|
||
const before = out;
|
||
out = out.replace(LEADING_SKILL_LINK_RE, "");
|
||
out = out.replace(TRUNCATED_LEADING_SKILL_LINK_RE, "");
|
||
out = out.replace(LEADING_SKILL_TOKEN_RE, "");
|
||
out = out.replace(/^\s*[::,,;;||\-–—]+\s*/, "");
|
||
changed = out !== before;
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function cleanUserVisibleText(text) {
|
||
return cleanSkillLinks(text);
|
||
}
|
||
|
||
// 某些中转模型会把内部思考误当成普通正文,用 XML 风格标签直接输出。
|
||
// 只清理明确的内部块,不改动普通文本中的尖括号内容。
|
||
function stripInternalThoughtBlocks(text) {
|
||
let out = String(text || "");
|
||
for (const name of ["thinking", "analysis", "reasoning"]) {
|
||
const re = new RegExp("<" + name + "\\b[^>]*>[\\s\\S]*?</" + name + "\\s*>", "gi");
|
||
out = out.replace(re, "");
|
||
}
|
||
return out.replace(/<\/?(?:thinking|analysis|reasoning)\b[^>]*>/gi, "");
|
||
}
|
||
|
||
// 流式内容可能把标签拆在多个 chunk 中:未闭合的内部块整体暂不输出。
|
||
function stripInternalThoughtStreaming(text) {
|
||
const source = String(text || "");
|
||
let out = "";
|
||
let cursor = 0;
|
||
const openRe = /<(thinking|analysis|reasoning)\b[^>]*>/gi;
|
||
while (true) {
|
||
openRe.lastIndex = cursor;
|
||
const open = openRe.exec(source);
|
||
if (!open) {
|
||
out += source.slice(cursor);
|
||
break;
|
||
}
|
||
out += source.slice(cursor, open.index);
|
||
const closeRe = new RegExp("</" + open[1] + "\\s*>", "i");
|
||
const tail = source.slice(openRe.lastIndex);
|
||
const close = closeRe.exec(tail);
|
||
if (!close) break;
|
||
cursor = openRe.lastIndex + close.index + close[0].length;
|
||
}
|
||
return stripInternalThoughtBlocks(out);
|
||
}
|
||
|
||
function cleanThreadTitle(title, fallback = "", maxLen = 80) {
|
||
const primary = normalizeInlineText(stripLeadingSkillMentions(title));
|
||
const secondary = normalizeInlineText(stripLeadingSkillMentions(fallback));
|
||
const cleaned = primary || secondary || normalizeInlineText(cleanSkillLinks(title)) || normalizeInlineText(cleanSkillLinks(fallback));
|
||
return cleaned ? cleaned.slice(0, maxLen) : "";
|
||
}
|
||
|
||
function looksLikeSkillMentionTitle(title) {
|
||
const text = normalizeInlineText(title);
|
||
return /^\[\$[^\]]+\]\(/.test(text) || /^\$[A-Za-z][\w:.-]*(?:\s|$)/.test(text);
|
||
}
|
||
|
||
module.exports = {
|
||
cleanSkillLinks,
|
||
cleanThreadTitle,
|
||
cleanUserVisibleText,
|
||
stripInternalThoughtBlocks,
|
||
stripInternalThoughtStreaming,
|
||
looksLikeSkillMentionTitle,
|
||
};
|