hermesThinking.js 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. /**
  2. * Hermes Agent 实时思考过程的颜文字过滤。
  3. *
  4. * 背景:Hermes 的 KawaiiSpinner 会给 thinking_callback 拼接前导颜文字与尾部省略号,
  5. * 形如 `(◔_◔) pondering...`。在严肃业务场景下需要剥离这些装饰,仅保留动词本身。
  6. *
  7. * 数据来源:backend/hermes-agent/agent/display.py
  8. * - KAWAII_THINKING(15 个默认思考颜文字)
  9. * - 各皮肤(ares / poseidon / sisyphus / charizard)覆盖的思考颜文字
  10. * - KAWAII_WAITING(等待动画用的颜文字,作为兜底也一并剥离)
  11. *
  12. * 使用:在 SSE 处理 status 事件时调用 stripHermesThinkingDecoration(content)。
  13. */
  14. // 默认 16 个思考颜文字(display.py: KAWAII_THINKING)
  15. const DEFAULT_THINKING_FACES = [
  16. '(。•́︿•̀。)',
  17. '(◔_◔)',
  18. '(¬‿¬)',
  19. '( •_•)>⌐■-■)',
  20. '(⌐■_■)',
  21. '(´・_・`)',
  22. '◉_◉',
  23. '(°ロ°)',
  24. '( ˘⌣˘)♡',
  25. 'ヽ(>∀<☆)☆',
  26. '٩(๑❛ᴗ❛๑)۶',
  27. '(⊙_⊙)',
  28. '(¬_¬)',
  29. '( ͡° ͜ʖ ͡°)',
  30. 'ಠ_ಠ',
  31. '>⌐■-■',
  32. ]
  33. // 各皮肤覆盖的思考颜文字(display.py: skin_engine.py 中的 SKIN_THINKING_FACES)
  34. const SKIN_THINKING_FACES = [
  35. // ares
  36. '(⚔)', '(⛨)', '(▲)', '(⌁)', '(<>)',
  37. // poseidon
  38. '(Ψ)', '(∿)', '(≈)', '(◌)',
  39. // sisyphus
  40. '(◉)', '(◬)', '(○)', '(●)',
  41. // charizard
  42. '(✦)', '(◇)', '(🔥)',
  43. ]
  44. // 等待动画用的颜文字(display.py: KAWAII_WAITING,作为兜底)
  45. const WAITING_FACES = [
  46. '( ̄∇ ̄)', '( ̄ー ̄)', '(─‿─)', '(︶ω︶)', '( ̄︶ ̄)',
  47. '( ︶。。)', `(˘。˘)`, `(•‿•)`, `(◕‿◕)`, `(✿◡‿◡)`,
  48. ]
  49. // 合并去重,并按长度降序排列(优先匹配长串,避免前缀误判)
  50. const ALL_FACES = Array.from(new Set([
  51. ...DEFAULT_THINKING_FACES,
  52. ...SKIN_THINKING_FACES,
  53. ...WAITING_FACES,
  54. ])).sort((a, b) => b.length - a.length)
  55. /**
  56. * 剥离 Hermes 实时思考过程文本中的装饰:
  57. * 1. 反复剥离前导的已知颜文字(精确匹配)
  58. * 2. 兜底剥离前导的圆括号包裹片段(如新增皮肤未及时登记时)
  59. * 3. 去除首尾空白
  60. *
  61. * 注意:尾部省略号按 Hermes 原始输出保留,不剥离。
  62. *
  63. * @param {string} text 原始文本,如 `(◔_◔) pondering...`
  64. * @returns {string} 清洗后的文本,如 `pondering...`
  65. */
  66. export function stripHermesThinkingDecoration(text) {
  67. if (!text) return ''
  68. let s = String(text)
  69. // 1. 反复剥离前导的已知颜文字(按长度降序匹配,避免短颜文字吃掉长颜文字的前缀)
  70. let changed = true
  71. while (changed) {
  72. changed = false
  73. for (const face of ALL_FACES) {
  74. if (s.startsWith(face)) {
  75. s = s.slice(face.length)
  76. changed = true
  77. break
  78. }
  79. }
  80. // 去掉前导空白后再尝试下一轮(颜文字之间可能由空格分隔)
  81. const trimmed = s.trimStart()
  82. if (trimmed !== s) {
  83. s = trimmed
  84. changed = true
  85. }
  86. }
  87. // 2. 兜底:剥离剩余的前导圆括号片段(如 `(xxx)` 或 `(xx)(yy)`)
  88. // 覆盖未来皮肤新增颜文字未及时登记的场景
  89. // 限制:`[^)]*` 保证不会跨括号贪婪匹配,最多吃到第一个 `)` 就停
  90. s = s.replace(/^(?:\([^)]*\)\s*)+\s*/, '')
  91. return s.trim()
  92. }
  93. /**
  94. * 剥离思考过程字符串开头的所有空行(含纯空白字符组成的行)。
  95. *
  96. * 背景:Hermes LLM 首个 token 经常是 `\n`,累加到 streamingThinking 后,
  97. * 在 `<pre>` 的 `white-space: pre-wrap` 下会渲染为首行空白,影响观感。
  98. * 仅剥离开头空行,保留中间与尾部的换行(属于正常思考输出格式)。
  99. *
  100. * @param {string} text 原始文本,如 `"\n\n正在分析...\n步骤一\n"`
  101. * @returns {string} 剥离首部空行后的文本,如 `"正在分析...\n步骤一\n"`
  102. */
  103. export function trimLeadingBlankLines(text) {
  104. if (!text) return ''
  105. // [ \t]*\r?\n 匹配"可选空白 + 换行"组成的整行;+ 连续多行一次性剥离
  106. return String(text).replace(/^(?:[ \t]*\r?\n)+/, '')
  107. }
  108. // 仅用于内部测试与调试(生产可忽略)
  109. export const __HERMES_FACES__ = ALL_FACES