Ver Fonte

修复说明性链接误显示为文件卡片

Samien há 1 mês atrás
pai
commit
a2cb84e

+ 20 - 1
src/client/components/messages/MarkdownContent.vue

@@ -194,6 +194,25 @@ function decorateCopyButtons() {
   })
 }
 
+function isFilePathLabel(link: HTMLAnchorElement, url: URL): boolean {
+  const label = link.textContent?.trim() || ''
+  if (!label || /[||\r\n]/.test(label)) return false
+
+  // An explicit absolute path is unambiguous even for extensionless files.
+  if (serverFilePath(label)) return true
+
+  const targetPath = url.pathname === '/api/files/download'
+    ? serverFilePath(url.searchParams.get('path') || '')
+    : serverFilePath(url.pathname)
+  const targetName = targetPath?.split('/').at(-1)
+  if (targetName && label === targetName) return true
+
+  // A bare filename remains useful when a remote-node route hides the original
+  // path or the assistant translated the basename. Descriptive titles without
+  // a file extension deliberately stay ordinary links.
+  return /^[^/\\]+\.[A-Za-z0-9]{1,16}$/.test(label)
+}
+
 function decorateFileLinks() {
   if (props.processContent || !props.fileCards || !root.value) return
   root.value.querySelectorAll<HTMLAnchorElement>('a').forEach(link => {
@@ -204,7 +223,7 @@ function decorateFileLinks() {
     const isWorkspaceFile = /^\/Users\/[^/]+\/\.hermes\/(?:profiles\/[^/]+\/)?workspace\/.+/.test(url.pathname)
     const isRemoteNodeFile = /^\/api\/app\/files\/(?:[0-9a-f-]{36}|[0-9]+)\/(?:download|preview)$/i.test(url.pathname)
     const isServerFile = url.pathname === '/api/files/download' && Boolean(url.searchParams.get('path'))
-    if (url.origin !== window.location.origin || (!isAgentFile && !isWorkspaceFile && !isRemoteNodeFile && !isServerFile)) return
+    if (url.origin !== window.location.origin || (!isAgentFile && !isWorkspaceFile && !isRemoteNodeFile && !isServerFile) || !isFilePathLabel(link, url)) return
     link.dataset.fileCard = 'true'
     link.classList.add('file-link-card')
     link.removeAttribute('target')

+ 16 - 2
tests/client/markdownContent.test.ts

@@ -68,7 +68,7 @@ describe('Markdown file cards', () => {
     const wrapper = mount(MarkdownContent, { props: { fileCards: true, content: [
       '[外部](https://example.com/Users/samien/.hermes/workspace/report.md)',
       '[普通](https://example.com/report.md)',
-      '[配置](/Users/samien/.hermes/config.yaml)',
+      '[config.yaml](/Users/samien/.hermes/config.yaml)',
       '[未知](sandbox:/etc/passwd)',
     ].join('\n\n') } })
     expect(wrapper.findAll('.file-link-card')).toHaveLength(1)
@@ -78,6 +78,20 @@ describe('Markdown file cards', () => {
     wrapper.unmount()
   })
 
+  it('keeps descriptive file links as ordinary links instead of file cards', () => {
+    const wrapper = mount(MarkdownContent, { props: { fileCards: true, content: [
+      '[源程序鉴别材料|Word](/Users/samien/.hermes/workspace/源程序鉴别材料.docx)',
+      '[源程序鉴别材料|PDF](/Users/samien/.hermes/workspace/源程序鉴别材料.pdf)',
+      '[信息采集表|数值修订待确认](/Users/samien/.hermes/workspace/信息采集表.xlsx)',
+      '[修改说明与验证结果](/Users/samien/.hermes/workspace/修改说明.md)',
+    ].join('\n\n') } })
+
+    expect(wrapper.findAll('.file-link-card')).toHaveLength(0)
+    expect(wrapper.findAll('a')).toHaveLength(4)
+    expect(wrapper.findAll('a').every(link => link.attributes('target') === '_blank')).toBe(true)
+    wrapper.unmount()
+  })
+
   it('decorates workspace links arriving in a later message update', async () => {
     const wrapper = mount(MarkdownContent, { props: { content: '附件如下:', fileCards: true } })
     await wrapper.setProps({ content: '附件如下:\n\n[报告.md](/Users/samien/.hermes/workspace/report.md)' })
@@ -171,7 +185,7 @@ describe('streaming Markdown', () => {
 })
 
 it('renders process media as text while retaining message media previews', async () => {
-  const content = '![过程图](/tmp/process.png) [过程文件](/Users/test/Agents/process.pdf)'
+  const content = '![过程图](/tmp/process.png) [process.pdf](/Users/test/Agents/process.pdf)'
   const process = mount(MarkdownContent, { props: { content, processContent: true, fileCards: true } })
   await nextTick()
   expect(process.find('img').exists()).toBe(false)