fix(web): download markdown file-preview links as attachments (#38030)

This commit is contained in:
Dx. 2026-06-26 21:46:44 +08:00 committed by GitHub
parent 8d09b32cd5
commit 449b46b863
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 68 additions and 3 deletions

View File

@ -170,6 +170,57 @@ describe('Link component', () => {
expect(link).toHaveAttribute('rel', 'noopener noreferrer')
})
it('adds attachment mode to file-preview links by default', () => {
mockIsValidUrl.mockReturnValue(true)
const node = {
properties: {
href: 'http://localhost:5001/files/123/file-preview?timestamp=1&nonce=2&sign=3',
},
}
render(<Link node={node}>doc.pdf</Link>)
expect(screen.getByText('doc.pdf')).toHaveAttribute(
'href',
'http://localhost:5001/files/123/file-preview?timestamp=1&nonce=2&sign=3&as_attachment=true',
)
})
it('does not duplicate attachment mode for file-preview links', () => {
mockIsValidUrl.mockReturnValue(true)
const node = {
properties: {
href: 'http://localhost:5001/files/123/file-preview?timestamp=1&nonce=2&sign=3&as_attachment=true',
},
}
render(<Link node={node}>doc.pdf</Link>)
expect(screen.getByText('doc.pdf')).toHaveAttribute(
'href',
'http://localhost:5001/files/123/file-preview?timestamp=1&nonce=2&sign=3&as_attachment=true',
)
})
it('keeps protocol-relative file-preview links protocol-relative', () => {
mockIsValidUrl.mockReturnValue(true)
const node = {
properties: {
href: '//localhost:5001/files/123/file-preview?timestamp=1#page',
},
}
render(<Link node={node}>doc.pdf</Link>)
expect(screen.getByText('doc.pdf')).toHaveAttribute(
'href',
'//localhost:5001/files/123/file-preview?timestamp=1&as_attachment=true#page',
)
})
// --------------------------
// NO HREF
// --------------------------

View File

@ -7,6 +7,19 @@ import * as React from 'react'
import { useChatContext } from '@/app/components/base/chat/chat/context'
import { isValidUrl } from './utils'
const filePreviewPathPattern = /\/files\/[^/?#]+\/file-preview(?:[?#]|$)/
const asAttachmentParamPattern = /[?&]as_attachment=/
const withFilePreviewAttachment = (href: string) => {
if (!filePreviewPathPattern.test(href) || asAttachmentParamPattern.test(href))
return href
const url = new URL(href, 'http://dify.local')
url.searchParams.set('as_attachment', 'true')
return href.startsWith('//') ? url.href.replace(url.protocol, '') : url.href
}
const Link = ({ node, children, ...props }: any) => {
const { onSend } = useChatContext()
const commonClassName = 'cursor-pointer underline decoration-primary-700! decoration-dashed'
@ -16,8 +29,9 @@ const Link = ({ node, children, ...props }: any) => {
return <abbr className={commonClassName} onClick={() => onSend?.(hidden_text)} title={node.children[0]?.value || ''}>{node.children[0]?.value || ''}</abbr>
}
else {
const href = props.href || node.properties?.href
if (href && /^#[\w-]+$/.test(href.toString())) {
const rawHref = props.href || node.properties?.href
const href = rawHref?.toString()
if (href && /^#[\w-]+$/.test(href)) {
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
e.preventDefault()
// scroll to target element if exists within the answer container
@ -36,7 +50,7 @@ const Link = ({ node, children, ...props }: any) => {
if (!href || !isValidUrl(href))
return <span>{children}</span>
return <a href={href} target="_blank" rel="noopener noreferrer" className={commonClassName}>{children || 'Download'}</a>
return <a href={withFilePreviewAttachment(href)} target="_blank" rel="noopener noreferrer" className={commonClassName}>{children || 'Download'}</a>
}
}