| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119 |
- /**
- * Hermes Agent 实时思考过程的颜文字过滤。
- *
- * 背景:Hermes 的 KawaiiSpinner 会给 thinking_callback 拼接前导颜文字与尾部省略号,
- * 形如 `(◔_◔) pondering...`。在严肃业务场景下需要剥离这些装饰,仅保留动词本身。
- *
- * 数据来源:backend/hermes-agent/agent/display.py
- * - KAWAII_THINKING(15 个默认思考颜文字)
- * - 各皮肤(ares / poseidon / sisyphus / charizard)覆盖的思考颜文字
- * - KAWAII_WAITING(等待动画用的颜文字,作为兜底也一并剥离)
- *
- * 使用:在 SSE 处理 status 事件时调用 stripHermesThinkingDecoration(content)。
- */
- // 默认 16 个思考颜文字(display.py: KAWAII_THINKING)
- const DEFAULT_THINKING_FACES = [
- '(。•́︿•̀。)',
- '(◔_◔)',
- '(¬‿¬)',
- '( •_•)>⌐■-■)',
- '(⌐■_■)',
- '(´・_・`)',
- '◉_◉',
- '(°ロ°)',
- '( ˘⌣˘)♡',
- 'ヽ(>∀<☆)☆',
- '٩(๑❛ᴗ❛๑)۶',
- '(⊙_⊙)',
- '(¬_¬)',
- '( ͡° ͜ʖ ͡°)',
- 'ಠ_ಠ',
- '>⌐■-■',
- ]
- // 各皮肤覆盖的思考颜文字(display.py: skin_engine.py 中的 SKIN_THINKING_FACES)
- const SKIN_THINKING_FACES = [
- // ares
- '(⚔)', '(⛨)', '(▲)', '(⌁)', '(<>)',
- // poseidon
- '(Ψ)', '(∿)', '(≈)', '(◌)',
- // sisyphus
- '(◉)', '(◬)', '(○)', '(●)',
- // charizard
- '(✦)', '(◇)', '(🔥)',
- ]
- // 等待动画用的颜文字(display.py: KAWAII_WAITING,作为兜底)
- const WAITING_FACES = [
- '( ̄∇ ̄)', '( ̄ー ̄)', '(─‿─)', '(︶ω︶)', '( ̄︶ ̄)',
- '( ︶。。)', `(˘。˘)`, `(•‿•)`, `(◕‿◕)`, `(✿◡‿◡)`,
- ]
- // 合并去重,并按长度降序排列(优先匹配长串,避免前缀误判)
- const ALL_FACES = Array.from(new Set([
- ...DEFAULT_THINKING_FACES,
- ...SKIN_THINKING_FACES,
- ...WAITING_FACES,
- ])).sort((a, b) => b.length - a.length)
- /**
- * 剥离 Hermes 实时思考过程文本中的装饰:
- * 1. 反复剥离前导的已知颜文字(精确匹配)
- * 2. 兜底剥离前导的圆括号包裹片段(如新增皮肤未及时登记时)
- * 3. 去除首尾空白
- *
- * 注意:尾部省略号按 Hermes 原始输出保留,不剥离。
- *
- * @param {string} text 原始文本,如 `(◔_◔) pondering...`
- * @returns {string} 清洗后的文本,如 `pondering...`
- */
- export function stripHermesThinkingDecoration(text) {
- if (!text) return ''
- let s = String(text)
- // 1. 反复剥离前导的已知颜文字(按长度降序匹配,避免短颜文字吃掉长颜文字的前缀)
- let changed = true
- while (changed) {
- changed = false
- for (const face of ALL_FACES) {
- if (s.startsWith(face)) {
- s = s.slice(face.length)
- changed = true
- break
- }
- }
- // 去掉前导空白后再尝试下一轮(颜文字之间可能由空格分隔)
- const trimmed = s.trimStart()
- if (trimmed !== s) {
- s = trimmed
- changed = true
- }
- }
- // 2. 兜底:剥离剩余的前导圆括号片段(如 `(xxx)` 或 `(xx)(yy)`)
- // 覆盖未来皮肤新增颜文字未及时登记的场景
- // 限制:`[^)]*` 保证不会跨括号贪婪匹配,最多吃到第一个 `)` 就停
- s = s.replace(/^(?:\([^)]*\)\s*)+\s*/, '')
- return s.trim()
- }
- /**
- * 剥离思考过程字符串开头的所有空行(含纯空白字符组成的行)。
- *
- * 背景:Hermes LLM 首个 token 经常是 `\n`,累加到 streamingThinking 后,
- * 在 `<pre>` 的 `white-space: pre-wrap` 下会渲染为首行空白,影响观感。
- * 仅剥离开头空行,保留中间与尾部的换行(属于正常思考输出格式)。
- *
- * @param {string} text 原始文本,如 `"\n\n正在分析...\n步骤一\n"`
- * @returns {string} 剥离首部空行后的文本,如 `"正在分析...\n步骤一\n"`
- */
- export function trimLeadingBlankLines(text) {
- if (!text) return ''
- // [ \t]*\r?\n 匹配"可选空白 + 换行"组成的整行;+ 连续多行一次性剥离
- return String(text).replace(/^(?:[ \t]*\r?\n)+/, '')
- }
- // 仅用于内部测试与调试(生产可忽略)
- export const __HERMES_FACES__ = ALL_FACES
|