Browse Source

WIP: 修复中文路径会话图片预览

[gstack-context]
Decisions: 用浏览器标准 URL 身份匹配媒体,普通会话与群聊共用
Remaining: 全量测试、生产构建、重载 8800、真实会话复验
Skill: /investigate
[/gstack-context]
samien 2 months ago
parent
commit
a2fceb2260

+ 12 - 2
src/client/components/library/mediaSequence.ts

@@ -15,6 +15,14 @@ export function previewItemFromUrl(name: string, url: string, id = `local:${url}
   return { id, name, kind: kind || inferred, previewUrl: url, downloadUrl: url }
 }
 
+export function mediaUrlIdentity(url: string): string {
+  try {
+    return new URL(url, window.location.origin).href
+  } catch {
+    return url
+  }
+}
+
 function nameFromUrl(url: string): string {
   try { return decodeURIComponent(new URL(url, window.location.origin).pathname.split('/').at(-1) || '媒体') } catch { return '媒体' }
 }
@@ -24,8 +32,10 @@ export function mediaItemsFromMessages(messages: UiMessage[]): UiLibraryItem[] {
   const result: UiLibraryItem[] = []
   const seen = new Set<string>()
   const append = (item: UiLibraryItem) => {
-    if (!item.previewUrl || !['image', 'video'].includes(item.kind) || seen.has(item.previewUrl)) return
-    seen.add(item.previewUrl)
+    if (!item.previewUrl || !['image', 'video'].includes(item.kind)) return
+    const identity = mediaUrlIdentity(item.previewUrl)
+    if (seen.has(identity)) return
+    seen.add(identity)
     result.push(item)
   }
   for (const message of messages) {

+ 3 - 2
src/client/views/ChatView.vue

@@ -12,7 +12,7 @@ import PreviewModal from '@/components/library/PreviewModal.vue'
 import ImagePreviewLightbox from '@/components/library/ImagePreviewLightbox.vue'
 import type { PreviewMedia } from '@/components/library/ImagePreviewLightbox.vue'
 import type { UiLibraryItem } from '@/components/library/types'
-import { mediaItemsFromMessages, previewItemFromUrl } from '@/components/library/mediaSequence'
+import { mediaItemsFromMessages, mediaUrlIdentity, previewItemFromUrl } from '@/components/library/mediaSequence'
 import MessageTimeline from '@/components/messages/MessageTimeline.vue'
 import type { UiMessage } from '@/components/messages/types'
 import ResourceSidebar from '@/components/app/ResourceSidebar.vue'
@@ -156,7 +156,8 @@ function openAttachment(attachment: NonNullable<UiMessage['attachments']>[number
 }
 
 function openMedia(item: UiLibraryItem) {
-  const index = lightboxMedia.value.findIndex(media => media.url === (item.previewUrl || item.downloadUrl))
+  const target = mediaUrlIdentity(item.previewUrl || item.downloadUrl || '')
+  const index = lightboxMedia.value.findIndex(media => mediaUrlIdentity(media.url) === target)
   mediaPreviewIndex.value = index >= 0 ? index : null
 }
 

+ 3 - 2
src/client/views/GroupsView.vue

@@ -12,7 +12,7 @@ import PreviewModal from '@/components/library/PreviewModal.vue'
 import ImagePreviewLightbox from '@/components/library/ImagePreviewLightbox.vue'
 import type { PreviewMedia } from '@/components/library/ImagePreviewLightbox.vue'
 import type { UiLibraryItem } from '@/components/library/types'
-import { mediaItemsFromMessages, previewItemFromUrl } from '@/components/library/mediaSequence'
+import { mediaItemsFromMessages, mediaUrlIdentity, previewItemFromUrl } from '@/components/library/mediaSequence'
 import MessageTimeline from '@/components/messages/MessageTimeline.vue'
 import type { UiMessage } from '@/components/messages/types'
 import ResourceSidebar from '@/components/app/ResourceSidebar.vue'
@@ -110,7 +110,8 @@ function openLocalFile({ name, url }: { name: string; url: string }) {
 }
 
 function openMedia(item: UiLibraryItem) {
-  const index = lightboxMedia.value.findIndex(media => media.url === (item.previewUrl || item.downloadUrl))
+  const target = mediaUrlIdentity(item.previewUrl || item.downloadUrl || '')
+  const index = lightboxMedia.value.findIndex(media => mediaUrlIdentity(media.url) === target)
   mediaPreviewIndex.value = index >= 0 ? index : null
 }
 

+ 7 - 1
tests/client/mediaSequence.test.ts

@@ -1,5 +1,5 @@
 import { describe, expect, it } from 'vitest'
-import { mediaItemsFromMessages, previewItemFromUrl } from '@/components/library/mediaSequence'
+import { mediaItemsFromMessages, mediaUrlIdentity, previewItemFromUrl } from '@/components/library/mediaSequence'
 import type { UiMessage } from '@/components/messages/types'
 
 describe('conversation media sequence', () => {
@@ -21,4 +21,10 @@ describe('conversation media sequence', () => {
   it('infers full-view media kind from a clicked conversation link', () => {
     expect(previewItemFromUrl('片段.webm', '/media/clip.webm')).toMatchObject({ kind: 'video', previewUrl: '/media/clip.webm' })
   })
+
+  it('gives rendered and source Chinese paths the same media identity', () => {
+    expect(mediaUrlIdentity('/Users/samien/Agents/瑶儿/单视角图片.png')).toBe(
+      mediaUrlIdentity('/Users/samien/Agents/%E7%91%B6%E5%84%BF/%E5%8D%95%E8%A7%86%E8%A7%92%E5%9B%BE%E7%89%87.png'),
+    )
+  })
 })