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,
|
|||
|
|
};
|