From bba4920fee06734f040a43bf7abb2957522a2864 Mon Sep 17 00:00:00 2001 From: ekko <152005280+EKKOLearnAI@users.noreply.github.com> Date: Fri, 8 May 2026 19:55:55 +0800 Subject: [PATCH] fix hermes markdown media and sync retry (#550) --- .../hermes/chat/MarkdownRenderer.vue | 2 +- packages/server/src/lib/llm-prompt.ts | 23 ++++++++++++++++--- .../src/services/hermes/chat-run-socket.ts | 7 ++++-- tests/client/markdown-rendering.test.ts | 20 ++++++++++++++++ 4 files changed, 46 insertions(+), 6 deletions(-) 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/image.png)\` - 视频:\`[视频名](/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: {