From fa035f348e823b368d45adf6e45c1ff6aa9e017c Mon Sep 17 00:00:00 2001 From: ekko <152005280+EKKOLearnAI@users.noreply.github.com> Date: Sun, 17 May 2026 11:09:28 +0800 Subject: [PATCH] Fix Windows local file download paths (#810) --- packages/server/src/lib/llm-prompt.ts | 75 +++++++------------ .../src/services/hermes/file-provider.ts | 10 ++- tests/client/markdown-rendering.test.ts | 15 ++++ tests/server/file-provider-paths.test.ts | 23 ++++++ 4 files changed, 76 insertions(+), 47 deletions(-) create mode 100644 tests/server/file-provider-paths.test.ts diff --git a/packages/server/src/lib/llm-prompt.ts b/packages/server/src/lib/llm-prompt.ts index e6c4e51..405f785 100644 --- a/packages/server/src/lib/llm-prompt.ts +++ b/packages/server/src/lib/llm-prompt.ts @@ -12,46 +12,36 @@ export const AI_OUTPUT_FORMAT_GUIDELINES = ` # 输出格式规范 -当你的回复中包含图片、视频或文件引用时,请遵循以下格式规范: +当你的回复中包含图片、视频或文件引用时,必须使用 Markdown,并引用本地绝对路径。 + +## 路径规则 + +- Unix/macOS/WSL:使用 \`/path/to/file\`,例如 \`/tmp/screenshot.png\` +- Windows:使用盘符绝对路径,并把反斜杠 \`\\\` 转成正斜杠 \`/\`,例如 \`C:/Users/Administrator/Desktop/screenshot.png\` +- Windows 路径必须用尖括号包住链接目标,避免盘符冒号或特殊字符被 Markdown 误解析,例如 \`\` +- 路径包含空格、中文或特殊字符时,必须使用尖括号包住链接目标,或对路径做 URL 编码 +- 确保文件确实存在且路径正确 ## 图片格式 -使用 Markdown 图片语法,路径必须是本地绝对路径: -- Unix/macOS/WSL 路径以 \`/\` 开头,例如 \`/tmp/screenshot.png\` -- Windows 路径使用盘符绝对路径,并把反斜杠 \`\\\` 转成正斜杠 \`/\`,例如 \`C:/Users/Administrator/Desktop/screenshot.png\` -- Windows 路径必须用尖括号包住链接目标,避免盘符冒号和路径字符被 Markdown 误解析 + +使用 Markdown 图片语法: + \`\`\` ![图片描述](/tmp/screenshot.png) -![图片描述]() -\`\`\` -示例: -\`\`\` ![Sub2API Dashboard](/tmp/sub2api-dashboard.png) ![桌面截图]() \`\`\` ## 视频格式 -使用 Markdown 链接语法引用视频文件,路径必须是本地绝对路径,支持的格式:mp4, webm, mov -\`\`\` -[视频名称](/tmp/recording.mp4) -[视频名称]() -\`\`\` -示例: + +使用 Markdown 链接语法引用视频文件,支持格式:.mp4、.webm、.mov。视频会显示为可播放的视频播放器(最大 640x480),支持原生播放控件。 + \`\`\` [屏幕录制](/tmp/screen-recording.mp4) [操作演示](/tmp/demo.webm) [录屏2026-05-08 15.19.46](/Users/ekko/Desktop/录屏2026-05-08%2015.19.46.mov) -[Windows 录屏]() -\`\`\` -视频会显示为可播放的视频播放器(最大 640x480),支持原生播放控件。 - -如果路径包含空格、中文或其他特殊字符,必须使用以下两种写法之一: -1. 对路径做 URL 编码,至少把空格写成 \`%20\` -2. 用尖括号包住链接目标 - -示例: -\`\`\` -[录屏2026-05-08 15.19.46](/Users/ekko/Desktop/录屏2026-05-08%2015.19.46.mov) [录屏2026-05-08 15.19.46]() +[Windows 录屏]() \`\`\` 错误示例: @@ -61,33 +51,26 @@ export const AI_OUTPUT_FORMAT_GUIDELINES = ` \`\`\` ## 文件链接格式 -使用 Markdown 链接语法,路径必须是本地绝对路径: -\`\`\` -[文件名](/tmp/report.pdf) -[文件名]() -\`\`\` -示例: + +使用 Markdown 链接语法: + \`\`\` [下载报告](/tmp/monthly-report.pdf) [下载报告]() \`\`\` -## 注意事项 -1. 图片、视频、文件路径必须使用本地绝对路径:Unix/macOS/WSL 使用 \`/path/to/file\`,Windows 使用 \`C:/path/to/file\` -2. 确保文件确实存在且路径正确 -3. 视频支持格式:.mp4, .webm, .mov -4. 路径中如果有空格或特殊字符,必须编码或使用尖括号包裹链接目标 -5. Windows 路径不要输出反斜杠形式,例如不要输出 \`C:\\Users\\...\`;请改成 \`\` - ## 发送文件给用户 + 当用户要求"发给我"、"发送给我"、"传给我"等请求文件时,使用上述格式返回文件路径: -- 图片:\`![描述](/path/to/image.png)\` -- Windows 图片:\`![描述]()\` -- 视频:\`[视频名](/path/to/video.mp4)\` -- Windows 视频:\`[视频名]()\` -- 文件:\`[文件名](/path/to/file.pdf)\` -- Windows 文件:\`[文件名]()\` -- 如果路径中有空格,优先输出编码后的路径,例如:\`[录屏]()\` 或 \`[录屏](/tmp/录屏%2015.19.46.mov)\` + +\`\`\` +![图片描述](/path/to/image.png) +![Windows 图片]() +[视频名](/path/to/video.mp4) +[Windows 视频]() +[文件名](/path/to/file.pdf) +[Windows 文件]() +\`\`\` `; /** diff --git a/packages/server/src/services/hermes/file-provider.ts b/packages/server/src/services/hermes/file-provider.ts index 8b72e34..7617430 100644 --- a/packages/server/src/services/hermes/file-provider.ts +++ b/packages/server/src/services/hermes/file-provider.ts @@ -65,9 +65,17 @@ export interface TerminalConfig { /** * Validate a file path: must be absolute and not contain '..' traversal. */ +export function normalizePlatformPath(filePath: string, platform = process.platform): string { + if (platform !== 'win32') return filePath + const msysDrivePath = filePath.match(/^\/([a-zA-Z])(?:\/(.*))?$/) + if (!msysDrivePath) return filePath + const [, drive, rest = ''] = msysDrivePath + return `${drive.toUpperCase()}:\\${rest.replace(/\//g, '\\')}` +} + export function validatePath(filePath: string): string { if (!filePath) throw Object.assign(new Error('Missing file path'), { code: 'missing_path' }) - const resolved = resolve(filePath) + const resolved = resolve(normalizePlatformPath(filePath)) const normalized = normalize(resolved) if (normalized.includes('..')) { throw Object.assign(new Error('Invalid file path'), { code: 'invalid_path' }) diff --git a/tests/client/markdown-rendering.test.ts b/tests/client/markdown-rendering.test.ts index bf61ef8..b00deb2 100644 --- a/tests/client/markdown-rendering.test.ts +++ b/tests/client/markdown-rendering.test.ts @@ -238,6 +238,21 @@ describe('MarkdownRenderer', () => { expect(wrapper.find('.markdown-video-footer .att-name').text()).toBe('录屏2026-05-08 15.19.46.mov') }) + it('renders MSYS-style Windows image paths through the download endpoint', () => { + const wrapper = mount(MarkdownRenderer, { + props: { + content: '![桌面截图](/c/Users/Administrator/Desktop/screenshot.png)', + }, + }) + + const img = wrapper.find('img') + expect(img.exists()).toBe(true) + expect(img.attributes('src')).toContain('/api/hermes/download?path=') + const src = new URL(img.attributes('src')) + expect(decodeURIComponent(src.searchParams.get('path') || '')).toBe('/c/Users/Administrator/Desktop/screenshot.png') + expect(img.attributes('alt')).toBe('桌面截图') + }) + it('keeps tilde-fenced markdown examples with nested tilde fences intact', () => { const wrapper = mount(MarkdownRenderer, { props: { diff --git a/tests/server/file-provider-paths.test.ts b/tests/server/file-provider-paths.test.ts new file mode 100644 index 0000000..92093dd --- /dev/null +++ b/tests/server/file-provider-paths.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from 'vitest' +import { normalizePlatformPath } from '../../packages/server/src/services/hermes/file-provider' + +describe('file provider platform path normalization', () => { + it('converts MSYS drive paths to Windows absolute paths on Windows', () => { + expect(normalizePlatformPath('/c/Users/Administrator/Desktop/screenshot.png', 'win32')) + .toBe('C:\\Users\\Administrator\\Desktop\\screenshot.png') + expect(normalizePlatformPath('/d/tmp/report.txt', 'win32')) + .toBe('D:\\tmp\\report.txt') + }) + + it('leaves MSYS-style paths unchanged on non-Windows platforms', () => { + expect(normalizePlatformPath('/c/Users/Administrator/Desktop/screenshot.png', 'darwin')) + .toBe('/c/Users/Administrator/Desktop/screenshot.png') + expect(normalizePlatformPath('/c/Users/Administrator/Desktop/screenshot.png', 'linux')) + .toBe('/c/Users/Administrator/Desktop/screenshot.png') + }) + + it('leaves normal Windows paths unchanged', () => { + expect(normalizePlatformPath('C:\\Users\\Administrator\\Desktop\\screenshot.png', 'win32')) + .toBe('C:\\Users\\Administrator\\Desktop\\screenshot.png') + }) +})