From 4659926be83cb40733ce585b27f8479d5ca1ba5b Mon Sep 17 00:00:00 2001 From: yinjialu <2481044901@qq.com> Date: Thu, 4 Jun 2026 23:03:39 +0800 Subject: [PATCH] Fix URL preview image export capture --- apps/daemon/src/project-routes.ts | 200 +++++++++++++++++- apps/daemon/tests/project-file-range.test.ts | 28 ++- apps/web/src/components/FileViewer.tsx | 10 +- apps/web/tests/components/FileViewer.test.tsx | 16 +- .../file-viewer-image-export.test.tsx | 56 +++-- 5 files changed, 281 insertions(+), 29 deletions(-) diff --git a/apps/daemon/src/project-routes.ts b/apps/daemon/src/project-routes.ts index d095902521..8037eca5a9 100644 --- a/apps/daemon/src/project-routes.ts +++ b/apps/daemon/src/project-routes.ts @@ -533,6 +533,184 @@ const URL_PREVIEW_SELECTION_BRIDGE = ``; +const URL_PREVIEW_SNAPSHOT_BRIDGE = ``; + function previewBridgeTokens(value: unknown): string[] { if (Array.isArray(value)) return value.flatMap(previewBridgeTokens); if (typeof value !== 'string') return []; @@ -547,6 +725,10 @@ function wantsUrlPreviewSelectionBridge(value: unknown): boolean { return previewBridgeTokens(value).some((token) => token === 'selection' || token === 'comment' || token === 'comments' || token === 'annotation'); } +function wantsUrlPreviewSnapshotBridge(value: unknown): boolean { + return previewBridgeTokens(value).some((token) => token === 'snapshot' || token === 'image' || token === 'capture'); +} + function injectBeforeBodyClose(html: string, marker: string, injection: string): string { if (html.includes(marker)) return html; const bodyCloseIndex = html.search(/<\/body\s*>/i); @@ -556,10 +738,14 @@ function injectBeforeBodyClose(html: string, marker: string, injection: string): return `${html}${injection}`; } -function injectUrlPreviewBridge(html: string, bridge: 'scroll' | 'selection'): string { - return bridge === 'scroll' - ? injectBeforeBodyClose(html, 'data-od-url-scroll-bridge', URL_PREVIEW_SCROLL_BRIDGE) - : injectBeforeBodyClose(html, 'data-od-url-selection-bridge', URL_PREVIEW_SELECTION_BRIDGE); +function injectUrlPreviewBridge(html: string, bridge: 'scroll' | 'selection' | 'snapshot'): string { + if (bridge === 'scroll') { + return injectBeforeBodyClose(html, 'data-od-url-scroll-bridge', URL_PREVIEW_SCROLL_BRIDGE); + } + if (bridge === 'selection') { + return injectBeforeBodyClose(html, 'data-od-url-selection-bridge', URL_PREVIEW_SELECTION_BRIDGE); + } + return injectBeforeBodyClose(html, 'data-od-url-snapshot-bridge', URL_PREVIEW_SNAPSHOT_BRIDGE); } function normalizeChatSessionMode(value: unknown): ChatSessionMode { @@ -2040,7 +2226,8 @@ export function registerProjectFileRoutes(app: Express, ctx: RegisterProjectFile (file) => { if ( (wantsUrlPreviewScrollBridge(req.query.odPreviewBridge) || - wantsUrlPreviewSelectionBridge(req.query.odPreviewBridge)) && + wantsUrlPreviewSelectionBridge(req.query.odPreviewBridge) || + wantsUrlPreviewSnapshotBridge(req.query.odPreviewBridge)) && /^text\/html(?:;|$)/i.test(file.mime) ) { let html = file.buffer.toString('utf8'); @@ -2050,6 +2237,9 @@ export function registerProjectFileRoutes(app: Express, ctx: RegisterProjectFile if (wantsUrlPreviewSelectionBridge(req.query.odPreviewBridge)) { html = injectUrlPreviewBridge(html, 'selection'); } + if (wantsUrlPreviewSnapshotBridge(req.query.odPreviewBridge)) { + html = injectUrlPreviewBridge(html, 'snapshot'); + } return html; } return file.buffer; diff --git a/apps/daemon/tests/project-file-range.test.ts b/apps/daemon/tests/project-file-range.test.ts index eae45459a7..4b7db043fc 100644 --- a/apps/daemon/tests/project-file-range.test.ts +++ b/apps/daemon/tests/project-file-range.test.ts @@ -174,6 +174,10 @@ describe('GET /api/projects/:id/raw/* range request route', () => { path.join(dir, 'selection-bridged.html'), Buffer.from('