Przeglądaj źródła

fix: 隐藏用户消息附加上下文

samien 2 miesięcy temu
rodzic
commit
438b77d70f

+ 3 - 2
src/client/components/messages/MessageTimeline.vue

@@ -8,6 +8,7 @@ import ToolTrace from './ToolTrace.vue'
 import TurnTrace from './TurnTrace.vue'
 import type { UiInteraction, UiLocalFileLink, UiMessage } from './types'
 import { buildMessageTimelineRows } from '@/utils/turnTrace'
+import { displayContentForMessage } from '@/utils/messageDisplay'
 
 const props = withDefaults(defineProps<{
   messages: UiMessage[]
@@ -128,7 +129,7 @@ function scrollToAnchor(messageId: string, anchorId: string): boolean {
 }
 
 async function copyMessage(message: UiMessage) {
-  await navigator.clipboard.writeText(message.content)
+  await navigator.clipboard.writeText(displayContentForMessage(message.role, message.content))
 }
 
 function hasConversationActions(message: UiMessage): boolean {
@@ -237,7 +238,7 @@ defineExpose({ scrollToMessage, scrollToAnchor, scrollToBottom })
 
             <div class="message__content">
               <MarkdownContent
-                :content="message.content"
+                :content="displayContentForMessage(message.role, message.content)"
                 :streaming="message.status === 'streaming'"
                 :legacy-media="message.role === 'assistant'"
                 :plain="message.role === 'user'"

+ 32 - 0
src/client/utils/messageDisplay.ts

@@ -0,0 +1,32 @@
+const ATTACHED_CONTEXT_MARKER_RE = /(?:^|\n)--- Attached Context ---\s*\n/
+const CONTEXT_WARNINGS_MARKER_RE = /(?:^|\n)--- Context Warnings ---[\s\S]*$/
+const CONTEXT_REF_RE = /@(file|folder|url|image|tool|terminal):(?:"[^"\n]+"|'[^'\n]+'|`[^`\n]+`|\S+)/g
+
+/**
+ * Project persisted message content into the text shown to a person.
+ *
+ * Hermes deliberately keeps expanded reference payloads in the stored user
+ * message so the model can consume them. Those payloads are transport data,
+ * not part of the user's visible transcript. Keep the raw content in state and
+ * apply this projection only at display/copy boundaries.
+ */
+export function displayContentForMessage(role: string, content: string): string {
+  if (role !== 'user') return content
+
+  const marker = content.match(ATTACHED_CONTEXT_MARKER_RE)
+  if (!marker || marker.index === undefined) {
+    return content.replace(CONTEXT_WARNINGS_MARKER_RE, '').trim()
+  }
+
+  const visibleText = content
+    .slice(0, marker.index)
+    .replace(CONTEXT_WARNINGS_MARKER_RE, '')
+    .trim()
+  const attachedContext = content.slice(marker.index + marker[0].length)
+  const references = [...new Set(
+    Array.from(attachedContext.matchAll(CONTEXT_REF_RE), match => match[0]),
+  )]
+  const missingReferences = references.filter(reference => !visibleText.includes(reference))
+
+  return [missingReferences.join('\n'), visibleText].filter(Boolean).join('\n\n') || visibleText
+}

+ 66 - 0
tests/client/messageDisplay.test.ts

@@ -0,0 +1,66 @@
+import { describe, expect, it } from 'vitest'
+import { displayContentForMessage } from '@/utils/messageDisplay'
+
+describe('displayContentForMessage', () => {
+  it('hides expanded attached context while restoring a missing reference', () => {
+    const content = [
+      '帮我看看这个文件',
+      '',
+      '--- Attached Context ---',
+      '',
+      '📄 @file:`/Users/samien/My Notes/方案.md` (981 tokens)',
+      '```markdown',
+      '# 很长的附加正文',
+      '```',
+    ].join('\n')
+
+    expect(displayContentForMessage('user', content)).toBe(
+      '@file:`/Users/samien/My Notes/方案.md`\n\n帮我看看这个文件',
+    )
+  })
+
+  it('keeps visible references in place and deduplicates attached references', () => {
+    const reference = '@url:`https://example.com/a b`'
+    const content = [
+      reference,
+      '总结这篇文章',
+      '',
+      '--- Attached Context ---',
+      '',
+      `🌐 ${reference} (1200 tokens)`,
+      '正文一',
+      `🌐 ${reference} (1200 tokens)`,
+      '正文二',
+    ].join('\n')
+
+    expect(displayContentForMessage('user', content)).toBe(`${reference}\n总结这篇文章`)
+  })
+
+  it('supports quoted refs and removes context warnings from user display', () => {
+    const content = [
+      '分析附件',
+      '',
+      '--- Attached Context ---',
+      '',
+      "📁 @folder:'/Users/samien/中文 文件夹' (20 tokens)",
+      'folder listing',
+      '🛠 @tool:"desktop capture" (10 tokens)',
+      'tool output',
+    ].join('\n')
+
+    expect(displayContentForMessage('user', content)).toBe([
+      "@folder:'/Users/samien/中文 文件夹'",
+      '@tool:"desktop capture"',
+      '',
+      '分析附件',
+    ].join('\n'))
+
+    expect(displayContentForMessage('user', '继续\n\n--- Context Warnings ---\n抓取失败')).toBe('继续')
+  })
+
+  it('never rewrites assistant or system content', () => {
+    const content = '回答\n\n--- Attached Context ---\n\n不应隐藏'
+    expect(displayContentForMessage('assistant', content)).toBe(content)
+    expect(displayContentForMessage('system', content)).toBe(content)
+  })
+})