messageDisplay.test.ts 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. import { describe, expect, it } from 'vitest'
  2. import { displayContentForMessage } from '@/utils/messageDisplay'
  3. import { normalizeChatMessage } from '@/utils/normalize'
  4. describe('displayContentForMessage', () => {
  5. it('turns persisted user @file markers into attachment cards and removes marker text', () => {
  6. const message = normalizeChatMessage({
  7. id: 'user-file', role: 'user', timestamp: 1,
  8. content: [
  9. '刘博士的技术介绍。',
  10. '',
  11. '[用户附加文件:大模型推理优化报告.docx]',
  12. '@file:\\/Users/samien/.hermes/attachments/大模型推理优化报告.docx',
  13. '',
  14. '[用户附加文件:MMLU_PRO_多模型协同评测系统_技术报告PPT_(1).pptx]',
  15. '@file:`/Users/samien/.hermes/attachments/MMLU_PRO_多模型协同评测系统_技术报告PPT_(1).pptx`',
  16. ].join('\n'),
  17. }, 'session-1', 'default')
  18. expect(message.content).toBe('刘博士的技术介绍。')
  19. expect(message.attachments).toEqual([
  20. expect.objectContaining({
  21. name: '大模型推理优化报告.docx',
  22. path: '/Users/samien/.hermes/attachments/大模型推理优化报告.docx',
  23. kind: 'file',
  24. }),
  25. expect.objectContaining({
  26. name: 'MMLU_PRO_多模型协同评测系统_技术报告PPT_(1).pptx',
  27. path: '/Users/samien/.hermes/attachments/MMLU_PRO_多模型协同评测系统_技术报告PPT_(1).pptx',
  28. kind: 'file',
  29. }),
  30. ])
  31. })
  32. it('turns iOS relative attachment markers into attachment cards', () => {
  33. const message = normalizeChatMessage({
  34. id: 'ios-user-file', role: 'user', timestamp: 1,
  35. content: [
  36. '[用户附加文件:车位 四号车库-B2307 罗益民 33600元.pdf]',
  37. '@file:`attachments/车位 四号车库-B2307 罗益民 33600元-2.pdf`',
  38. '',
  39. '--- Attached Context ---',
  40. '',
  41. '📎 @file:`attachments/车位 四号车库-B2307 罗益民 33600元-2.pdf` (application/pdf, 139.2 KB) — binary file.',
  42. ].join('\n'),
  43. }, 'session-1', 'default')
  44. expect(message.content).toBe('')
  45. expect(message.attachments).toEqual([
  46. expect.objectContaining({
  47. name: '车位 四号车库-B2307 罗益民 33600元.pdf',
  48. path: 'attachments/车位 四号车库-B2307 罗益民 33600元-2.pdf',
  49. url: '/attachments/%E8%BD%A6%E4%BD%8D%20%E5%9B%9B%E5%8F%B7%E8%BD%A6%E5%BA%93-B2307%20%E7%BD%97%E7%9B%8A%E6%B0%91%2033600%E5%85%83-2.pdf',
  50. kind: 'pdf',
  51. }),
  52. ])
  53. })
  54. it('turns iOS image markers into image attachments without keeping model screenshots', () => {
  55. const message = normalizeChatMessage({
  56. id: 'ios-user-image', role: 'user', timestamp: 1,
  57. content: [
  58. '测试',
  59. '',
  60. '[用户附加图片:照片-9270F5CF-2FE2-487F-8948-FA583F4C83B1.png]',
  61. '@image:/Users/samien/.hermes/images/upload_20260820_130901_1.png',
  62. '[screenshot]',
  63. ].join('\n'),
  64. }, 'session-1', 'default')
  65. expect(message.content).toBe('测试')
  66. expect(message.attachments).toEqual([
  67. expect.objectContaining({
  68. name: '照片-9270F5CF-2FE2-487F-8948-FA583F4C83B1.png',
  69. path: '/Users/samien/.hermes/images/upload_20260820_130901_1.png',
  70. url: '/Users/samien/.hermes/images/upload_20260820_130901_1.png',
  71. kind: 'image',
  72. }),
  73. ])
  74. })
  75. it('keeps multiple iOS image uploads together as image attachments', () => {
  76. const message = normalizeChatMessage({
  77. id: 'ios-user-images', role: 'user', timestamp: 1,
  78. content: [
  79. '[用户附加图片:照片-1.jpeg]',
  80. '',
  81. '[用户附加图片:照片-2.jpeg]',
  82. '@image:/Users/samien/.hermes/images/upload_1.jpeg',
  83. '@image:/Users/samien/.hermes/images/upload_2.jpeg',
  84. '[screenshot]',
  85. '[screenshot]',
  86. ].join('\n'),
  87. }, 'session-1', 'default')
  88. expect(message.content).toBe('')
  89. expect(message.attachments?.map(attachment => attachment.url)).toEqual([
  90. '/Users/samien/.hermes/images/upload_1.jpeg',
  91. '/Users/samien/.hermes/images/upload_2.jpeg',
  92. ])
  93. })
  94. it('restores an entire batch of iOS Profile images with their original names', () => {
  95. const names = ['照片-D2BE1E0F.png', '照片-8717030E.png', '中文 照片.heic']
  96. const paths = names.map((_, index) => `/Users/samien/.hermes/profiles/yaoer/images/upload_20260815_233834_${index + 1}.png`)
  97. const message = normalizeChatMessage({
  98. id: 'ios-profile-images', role: 'user', timestamp: 1,
  99. content: [
  100. '读取图片中的提示词',
  101. ...names.map(name => `[用户附加图片:${name}]`),
  102. ...paths.map(path => `@image:${path}`),
  103. ...paths.map(() => '[screenshot]'),
  104. ].join('\n'),
  105. }, 'session-1', 'yaoer')
  106. expect(message.content).toBe('读取图片中的提示词')
  107. expect(message.attachments?.map(({ name, path, url, kind }) => ({ name, path, url, kind })))
  108. .toEqual(names.map((name, index) => ({ name, path: paths[index], url: paths[index], kind: 'image' })))
  109. })
  110. it.each([
  111. '/Users/samien/.hermes/profiles/yaoer/config/image.png',
  112. '/Users/samien/.hermes/profiles/../images/image.png',
  113. '/Users/samien/.hermes/profiles/yaoer/images/%2e%2e/image.png',
  114. '/Users/samien/.hermes/profiles/yaoer%2Fother/images/image.png',
  115. '/Users/samien/.hermes/profiles/yaoer/images/nested%2F..%2Fimage.png',
  116. '/Users/samien/.hermes/profiles/yaoer/images/image%5C.png',
  117. '/Users/samien/.hermes/profiles/yaoer/images/image%00.png',
  118. ])('keeps unsupported or unsafe image references as text: %s', path => {
  119. const content = `[用户附加图片:照片.png]\n@image:${path}\n[screenshot]`
  120. const message = normalizeChatMessage({ id: 'unsafe', role: 'user', content }, 'session-1', 'yaoer')
  121. expect(message.content).toBe(content)
  122. expect(message.attachments).toBeUndefined()
  123. })
  124. it('hides expanded attached context while restoring a missing reference', () => {
  125. const content = [
  126. '帮我看看这个文件',
  127. '',
  128. '--- Attached Context ---',
  129. '',
  130. '📄 @file:`/Users/samien/My Notes/方案.md` (981 tokens)',
  131. '```markdown',
  132. '# 很长的附加正文',
  133. '```',
  134. ].join('\n')
  135. expect(displayContentForMessage('user', content)).toBe(
  136. '@file:`/Users/samien/My Notes/方案.md`\n\n帮我看看这个文件',
  137. )
  138. })
  139. it('keeps visible references in place and deduplicates attached references', () => {
  140. const reference = '@url:`https://example.com/a b`'
  141. const content = [
  142. reference,
  143. '总结这篇文章',
  144. '',
  145. '--- Attached Context ---',
  146. '',
  147. `🌐 ${reference} (1200 tokens)`,
  148. '正文一',
  149. `🌐 ${reference} (1200 tokens)`,
  150. '正文二',
  151. ].join('\n')
  152. expect(displayContentForMessage('user', content)).toBe(`${reference}\n总结这篇文章`)
  153. })
  154. it('supports quoted refs and removes context warnings from user display', () => {
  155. const content = [
  156. '分析附件',
  157. '',
  158. '--- Attached Context ---',
  159. '',
  160. "📁 @folder:'/Users/samien/中文 文件夹' (20 tokens)",
  161. 'folder listing',
  162. '🛠 @tool:"desktop capture" (10 tokens)',
  163. 'tool output',
  164. ].join('\n')
  165. expect(displayContentForMessage('user', content)).toBe([
  166. "@folder:'/Users/samien/中文 文件夹'",
  167. '@tool:"desktop capture"',
  168. '',
  169. '分析附件',
  170. ].join('\n'))
  171. expect(displayContentForMessage('user', '继续\n\n--- Context Warnings ---\n抓取失败')).toBe('继续')
  172. })
  173. it('never rewrites assistant or system content', () => {
  174. const content = '回答\n\n--- Attached Context ---\n\n不应隐藏'
  175. expect(displayContentForMessage('assistant', content)).toBe(content)
  176. expect(displayContentForMessage('system', content)).toBe(content)
  177. })
  178. })