messageDisplay.test.ts 9.3 KB

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