diff --git a/packages/client/src/components/hermes/chat/MarkdownRenderer.vue b/packages/client/src/components/hermes/chat/MarkdownRenderer.vue
index fc9b64c..4c85cda 100644
--- a/packages/client/src/components/hermes/chat/MarkdownRenderer.vue
+++ b/packages/client/src/components/hermes/chat/MarkdownRenderer.vue
@@ -83,7 +83,7 @@ const renderedHtml = computed(() => {
const ext = path.split('.').pop()?.toLowerCase()
// Video files: render as video player
- if (ext === 'mp4' || ext === 'webm') {
+ if (ext === 'mp4' || ext === 'webm' || ext === 'mov') {
const downloadUrl = getDownloadUrl(path)
return `
diff --git a/packages/server/src/lib/llm-prompt.ts b/packages/server/src/lib/llm-prompt.ts
index c45c5bb..0bbd4c7 100644
--- a/packages/server/src/lib/llm-prompt.ts
+++ b/packages/server/src/lib/llm-prompt.ts
@@ -25,7 +25,7 @@ export const AI_OUTPUT_FORMAT_GUIDELINES = `
\`\`\`
## 视频格式
-使用 Markdown 链接语法引用视频文件,路径必须是本地绝对路径(以 / 开头),支持的格式:mp4, webm
+使用 Markdown 链接语法引用视频文件,路径必须是本地绝对路径(以 / 开头),支持的格式:mp4, webm, mov
\`\`\`
[视频名称](/tmp/recording.mp4)
\`\`\`
@@ -33,9 +33,25 @@ export const AI_OUTPUT_FORMAT_GUIDELINES = `
\`\`\`
[屏幕录制](/tmp/screen-recording.mp4)
[操作演示](/tmp/demo.webm)
+[录屏2026-05-08 15.19.46](/Users/ekko/Desktop/录屏2026-05-08%2015.19.46.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]()
+\`\`\`
+
+错误示例:
+\`\`\`
+[录屏2026-05-08 15.19.46](/Users/ekko/Desktop/录屏2026-05-08 15.19.46.mov)
+\`\`\`
+
## 文件链接格式
使用 Markdown 链接语法,路径必须是本地绝对路径(以 / 开头):
\`\`\`
@@ -49,13 +65,15 @@ export const AI_OUTPUT_FORMAT_GUIDELINES = `
## 注意事项
1. 图片、视频、文件路径必须使用本地绝对路径(以 / 开头)
2. 确保文件确实存在且路径正确
-3. 视频支持格式:.mp4, .webm
+3. 视频支持格式:.mp4, .webm, .mov
+4. 路径中如果有空格或特殊字符,必须编码或使用尖括号包裹链接目标
## 发送文件给用户
当用户要求"发给我"、"发送给我"、"传给我"等请求文件时,使用上述格式返回文件路径:
- 图片:\`\`
- 视频:\`[视频名](/path/to/video.mp4)\`
- 文件:\`[文件名](/path/to/file.pdf)\`
+- 如果路径中有空格,优先输出编码后的路径,例如:\`[录屏]()\` 或 \`[录屏](/tmp/录屏%2015.19.46.mov)\`
`;
/**
@@ -74,4 +92,3 @@ export function getSystemPrompt(customPrompt?: string): string {
return parts.join('\n\n');
}
-
diff --git a/packages/server/src/services/hermes/chat-run-socket.ts b/packages/server/src/services/hermes/chat-run-socket.ts
index e35b014..505598e 100644
--- a/packages/server/src/services/hermes/chat-run-socket.ts
+++ b/packages/server/src/services/hermes/chat-run-socket.ts
@@ -1304,7 +1304,10 @@ export class ChatRunSocket {
const prof = state.profile
this.hermesSessionIds.delete(sessionId)
state.profile = undefined
- await this.syncFromHermes(socket, sessionId, hermesId, prof)
+ await this.syncFromHermes(socket, sessionId, hermesId, prof, {
+ maxAttempts: 4,
+ delayMs: 1000,
+ })
}
}
@@ -1341,7 +1344,7 @@ export class ChatRunSocket {
this.hermesSessionIds.delete(sessionId)
logger.info({ sessionId, hermesId, profile: profile || 'default' }, '[chat-run-socket][abort] syncing stopped run from Hermes')
synced = await this.syncFromHermes(socket, sessionId, hermesId, profile, {
- maxAttempts: 10,
+ maxAttempts: 4,
delayMs: 1000,
})
}
diff --git a/tests/client/markdown-rendering.test.ts b/tests/client/markdown-rendering.test.ts
index c6b1e2a..bf61ef8 100644
--- a/tests/client/markdown-rendering.test.ts
+++ b/tests/client/markdown-rendering.test.ts
@@ -36,6 +36,11 @@ vi.mock('naive-ui', () => ({
}),
}))
+vi.mock('@/api/hermes/download', () => ({
+ downloadFile: vi.fn(),
+ getDownloadUrl: (path: string) => `http://test.local/api/hermes/download?path=${encodeURIComponent(path)}`,
+}))
+
import MarkdownRenderer from '@/components/hermes/chat/MarkdownRenderer.vue'
describe('MarkdownRenderer', () => {
@@ -218,6 +223,21 @@ describe('MarkdownRenderer', () => {
expect(wrapper.find('.markdown-body').text()).toContain('Done outside.')
})
+ it('renders local mov links as inline video players', () => {
+ const wrapper = mount(MarkdownRenderer, {
+ props: {
+ content: '[录屏2026-05-08 15.19.46.mov](/Users/ekko/Desktop/录屏2026-05-08%2015.19.46.mov)',
+ },
+ })
+
+ const video = wrapper.find('video.markdown-video')
+ expect(video.exists()).toBe(true)
+ expect(video.attributes('src')).toContain('/api/hermes/download?path=')
+ const src = new URL(video.attributes('src'))
+ expect(decodeURIComponent(src.searchParams.get('path') || '')).toBe('/Users/ekko/Desktop/录屏2026-05-08 15.19.46.mov')
+ expect(wrapper.find('.markdown-video-footer .att-name').text()).toBe('录屏2026-05-08 15.19.46.mov')
+ })
+
it('keeps tilde-fenced markdown examples with nested tilde fences intact', () => {
const wrapper = mount(MarkdownRenderer, {
props: {