import { redactString } from "@umt/contracts"; import { load } from "cheerio"; // 网页文本工具:HTML → 纯文本(去脚本、样式、表单等非正文元素)、实体解码、截断与脱敏。 const NAMED: Record = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: " " }; /** 去掉摘要里的内联标签(Brave 用 标关键字)并解码实体 */ export function stripInlineHtml(s: string): string { return s .replace(/<[^>]*>/g, "") .replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (m, e: string) => { if (e[0] === "#") { const code = e[1] === "x" || e[1] === "X" ? Number.parseInt(e.slice(2), 16) : Number(e.slice(1)); return Number.isFinite(code) && code > 0 && code < 0x110000 ? String.fromCodePoint(code) : m; } return NAMED[e.toLowerCase()] ?? m; }) .replace(/\s+/g, " ") .trim(); } /** 按字符截断,末尾加 … */ export function truncate(s: string, max: number): { text: string; truncated: boolean } { if (s.length <= max) return { text: s, truncated: false }; return { text: `${s.slice(0, Math.max(0, max - 1))}…`, truncated: true }; } /** 交给模型前的清理:脱敏(URL 参数里的 passkey / token 等)+ 截断 */ export function sanitizeForModel(s: string, max: number): { text: string; truncated: boolean } { return truncate(redactString(s), max); } const DROP = "script,style,noscript,svg,canvas,iframe,object,embed,form,input,select,button,link,meta"; const BLOCK = "p,div,section,article,main,header,footer,aside,nav,li,ul,ol,table,tr,h1,h2,h3,h4,h5,h6,pre,blockquote,dd,dt,br,hr"; /** HTML → { title, text }:去脚本样式等,块级元素换行,合并空白 */ export function htmlToText(html: string): { title: string | null; text: string } { //