Fix Windows local file download paths (#810)

This commit is contained in:
ekko
2026-05-17 11:09:28 +08:00
committed by GitHub
parent bcfc5053c4
commit fa035f348e
4 changed files with 76 additions and 47 deletions
+29 -46
View File
@@ -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 误解析,例如 \`<C:/Users/Administrator/Desktop/screenshot.png>\`
- 路径包含空格、中文或特殊字符时,必须使用尖括号包住链接目标,或对路径做 URL 编码
- 确保文件确实存在且路径正确
## 图片格式
使用 Markdown 图片语法,路径必须是本地绝对路径:
- Unix/macOS/WSL 路径以 \`/\` 开头,例如 \`/tmp/screenshot.png\`
- Windows 路径使用盘符绝对路径,并把反斜杠 \`\\\` 转成正斜杠 \`/\`,例如 \`C:/Users/Administrator/Desktop/screenshot.png\`
- Windows 路径必须用尖括号包住链接目标,避免盘符冒号和路径字符被 Markdown 误解析
使用 Markdown 图片语法:
\`\`\`
![图片描述](/tmp/screenshot.png)
![图片描述](<C:/Users/Administrator/Desktop/screenshot.png>)
\`\`\`
示例:
\`\`\`
![Sub2API Dashboard](/tmp/sub2api-dashboard.png)
![桌面截图](<C:/Users/Administrator/Desktop/screenshot.png>)
\`\`\`
## 视频格式
使用 Markdown 链接语法引用视频文件,路径必须是本地绝对路径,支持的格式:mp4, webm, mov
\`\`\`
[视频名称](/tmp/recording.mp4)
[视频名称](<C:/Users/Administrator/Desktop/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 录屏](<C:/Users/Administrator/Desktop/screen recording.mov>)
\`\`\`
视频会显示为可播放的视频播放器(最大 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](</Users/ekko/Desktop/录屏2026-05-08 15.19.46.mov>)
[Windows 录屏](<C:/Users/Administrator/Desktop/screen recording.mov>)
\`\`\`
错误示例:
@@ -61,33 +51,26 @@ export const AI_OUTPUT_FORMAT_GUIDELINES = `
\`\`\`
## 文件链接格式
使用 Markdown 链接语法,路径必须是本地绝对路径:
\`\`\`
[文件名](/tmp/report.pdf)
[文件名](<C:/Users/Administrator/Desktop/report.pdf>)
\`\`\`
示例:
使用 Markdown 链接语法:
\`\`\`
[下载报告](/tmp/monthly-report.pdf)
[下载报告](<C:/Users/Administrator/Desktop/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\\...\`;请改成 \`<C:/Users/...>\`
## 发送文件给用户
当用户要求"发给我"、"发送给我"、"传给我"等请求文件时,使用上述格式返回文件路径:
- 图片:\`![描述](/path/to/image.png)\`
- Windows 图片:\`![描述](<C:/Users/Administrator/Desktop/image.png>)\`
- 视频:\`[视频名](/path/to/video.mp4)\`
- Windows 视频:\`[视频名](<C:/Users/Administrator/Desktop/video.mp4>)\`
- 文件:\`[文件名](/path/to/file.pdf)\`
- Windows 文件:\`[文件名](<C:/Users/Administrator/Desktop/file.pdf>)\`
- 如果路径中有空格,优先输出编码后的路径,例如:\`[录屏](</tmp/录屏 15.19.46.mov>)\`\`[录屏](/tmp/录屏%2015.19.46.mov)\`
\`\`\`
![图片描述](/path/to/image.png)
![Windows 图片](<C:/Users/Administrator/Desktop/image.png>)
[视频名](/path/to/video.mp4)
[Windows 视频](<C:/Users/Administrator/Desktop/video.mp4>)
[文件名](/path/to/file.pdf)
[Windows 文件](<C:/Users/Administrator/Desktop/file.pdf>)
\`\`\`
`;
/**
@@ -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' })
+15
View File
@@ -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: {
+23
View File
@@ -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')
})
})