From 866ae3d23da9f3654450f6475cee9ea6197c5873 Mon Sep 17 00:00:00 2001 From: hangox Date: Fri, 8 May 2026 19:55:33 +0800 Subject: [PATCH] fix: prevent double-wrapping of download URLs in MarkdownRenderer (#529) Co-authored-by: Hango Liang Co-authored-by: Claude Opus 4.7 --- packages/client/src/api/hermes/download.ts | 13 +++++++++++++ .../components/hermes/chat/MarkdownRenderer.vue | 16 ++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/packages/client/src/api/hermes/download.ts b/packages/client/src/api/hermes/download.ts index b53d319..20442c4 100644 --- a/packages/client/src/api/hermes/download.ts +++ b/packages/client/src/api/hermes/download.ts @@ -6,6 +6,19 @@ import { getApiKey, getBaseUrlValue } from '../client' */ export function getDownloadUrl(filePath: string, fileName?: string): string { const base = getBaseUrlValue() + + // Guard: if filePath is already a full download URL, extract the real path + // to prevent double-wrapping (/api/hermes/download?path=/api/hermes/download?path=...) + if (filePath.startsWith('/api/hermes/download?')) { + try { + const parsed = new URL(filePath, 'http://localhost') + const realPath = parsed.searchParams.get('path') + if (realPath) filePath = realPath + } catch { + // fall through with original filePath + } + } + // Decode the path first in case it's already encoded (e.g., from AI responses) // URLSearchParams will encode it again, so we need to start with decoded text const decodedPath = decodeURIComponent(filePath) diff --git a/packages/client/src/components/hermes/chat/MarkdownRenderer.vue b/packages/client/src/components/hermes/chat/MarkdownRenderer.vue index a6850e3..fc9b64c 100644 --- a/packages/client/src/components/hermes/chat/MarkdownRenderer.vue +++ b/packages/client/src/components/hermes/chat/MarkdownRenderer.vue @@ -305,6 +305,22 @@ async function handleMarkdownClick(event: MouseEvent): Promise { return } + // Full download URL: open directly (already has /api/hermes/download?path=...) + if (href.startsWith('/api/hermes/download?')) { + event.preventDefault() + event.stopPropagation() + const linkText = link.textContent || '' + const fileName = linkText.startsWith('File: ') ? linkText.slice(6).trim() : linkText.trim() + message.info(t('download.downloading')) + // Parse the real file path from the existing query param + const url = new URL(href, window.location.origin) + const realPath = url.searchParams.get('path') || href + downloadFile(realPath, fileName || undefined).catch((err: Error) => { + message.error(err.message || t('download.downloadFailed')) + }) + return + } + // File path links: intercept and download if (href.startsWith('/')) { event.preventDefault()