1
0
Эх сурвалжийг харах

review: format check precedes limits, INVALID_IMAGE/IMAGE_TYPE_MISMATCH as format copy, tile-sized retry control

creatixchu 1 сар өмнө
parent
commit
8d4c3f8a0a

+ 9 - 0
packages/client/ui-attachment/src/MessageImage.module.css

@@ -58,3 +58,12 @@
   background: var(--dsw-alias-interactive-bg-hover-danger);
   cursor: pointer;
 }
+
+/* A failed tile keeps the 64px grid cell instead of growing to its copy. */
+.error[data-variant='tile'] {
+  width: 64px;
+  height: 64px;
+  padding: 4px;
+  overflow: hidden;
+  border-radius: 16px;
+}

+ 1 - 1
packages/client/ui-attachment/src/MessageImage.tsx

@@ -79,7 +79,7 @@ export function MessageImage({ attachment, load, variant, labels }: {
   }, [attachment, load, attempt])
 
   const label = attachment.name ?? labels.image
-  if (error) return <button type="button" className={css.error} onClick={request}>{labels.loadFailed}</button>
+  if (error) return <button type="button" className={css.error} data-variant={variant} onClick={request}>{labels.loadFailed}</button>
   return (
     <>
       <button

+ 7 - 0
packages/client/ui-attachment/tests/message-image.client.spec.tsx

@@ -109,6 +109,13 @@ describe('MessageImage', () => {
     expect(frame.getAttribute('style')).toBeNull()
   })
 
+  it('keeps the tile variant on the failed-load retry control', async () => {
+    const load = vi.fn().mockRejectedValue(new Error('offline'))
+    const view = render(<MessageImage attachment={attachment} load={load} variant="tile" labels={labels} />)
+    const retry = await view.findByRole('button', { name: '图片加载失败,点击重试' })
+    expect(retry.getAttribute('data-variant')).toBe('tile')
+  })
+
   it('ignores a load settling after unmount', async () => {
     let resolve: ((url: string) => void) | undefined
     const load = vi.fn(() => new Promise<string>((r) => { resolve = r }))

+ 5 - 0
packages/client/ui-conversation/src/client/image-labels.ts

@@ -38,6 +38,11 @@ export function attachmentErrorText(
     case 'MODEL_DOES_NOT_SUPPORT_IMAGES': return t('image.modelUnsupported')
     case 'SUBAGENT_IMAGE_UNSUPPORTED': return t('image.subagentUnsupported')
     case 'IMAGE_TOO_MANY_PIXELS': return t('image.tooManyPixels')
+    // Undecodable bytes or a declared type its bytes contradict: solvable by
+    // replacing or re-exporting the file, so it reads as a format problem.
+    case 'INVALID_IMAGE':
+    case 'IMAGE_TYPE_MISMATCH':
+      return t('image.unsupportedType')
     case 'TOO_MANY_IMAGES':
       if (limits !== undefined) return t('image.tooMany', { count: limits.maxImagesPerMessage })
       break

+ 6 - 0
packages/client/ui-conversation/src/client/skeleton/InputBar.tsx

@@ -425,6 +425,12 @@ export function InputBar({
     if (addImages === undefined || files.length === 0) return
     const rejected = ((): string | null => {
       if (imageLimits !== undefined) {
+        // Format precedes limits (DeepSeek Chat's filter order): a batch with
+        // a non-image must announce the format problem, not a count or size
+        // it could never pass anyway — addImages rejects it authoritatively.
+        if (files.some(file => !(imageLimits.mediaTypes as readonly string[]).includes(file.type))) {
+          return addImages(files)
+        }
         if (attachments.length + files.length > imageLimits.maxImagesPerMessage) {
           return t('image.tooMany', { count: imageLimits.maxImagesPerMessage })
         }

+ 2 - 0
packages/client/ui-conversation/tests/image-labels.client.spec.tsx

@@ -44,6 +44,8 @@ describe('attachment rejection copy', () => {
     expect(attachmentErrorText(t, 'MODEL_DOES_NOT_SUPPORT_IMAGES')).toBe('当前模型不支持图片,请切换支持图片的模型')
     expect(attachmentErrorText(t, 'SUBAGENT_IMAGE_UNSUPPORTED')).toBe('子智能体会话暂不支持图片')
     expect(attachmentErrorText(t, 'IMAGE_TOO_MANY_PIXELS')).toBe('图片分辨率过大,请压缩后重试')
+    expect(attachmentErrorText(t, 'INVALID_IMAGE')).toBe('仅支持 PNG、JPG、WebP、GIF 格式的图片')
+    expect(attachmentErrorText(t, 'IMAGE_TYPE_MISMATCH')).toBe('仅支持 PNG、JPG、WebP、GIF 格式的图片')
     expect(attachmentErrorText(t, 'TOO_MANY_IMAGES', limits)).toBe('一条消息最多添加 20 张图片')
     expect(attachmentErrorText(t, 'IMAGE_TOO_LARGE', limits)).toBe('单张图片不能超过 10MB')
     expect(attachmentErrorText(t, 'IMAGES_TOO_LARGE', limits)).toBe('图片总大小超过 100MB,请移除部分图片')

+ 22 - 0
packages/client/ui-conversation/tests/input-bar.client.spec.tsx

@@ -297,6 +297,28 @@ describe('image draft rail', () => {
     expect(within.view.queryByRole('alert')).toBeNull()
   })
 
+  it('announces the format problem before any limit when the batch holds a non-image', () => {
+    const addImages = vi.fn(() => '仅支持 PNG、JPG、WebP、GIF 格式的图片')
+    const { view } = bench({
+      addImages,
+      imageLimits: {
+        maxImageBytes: 8,
+        maxImagesPerMessage: 1,
+        maxMessageImageBytes: 8,
+        maxImagePixels: 40_000_000,
+        mediaTypes: ['image/png'] as const,
+      },
+    })
+    // Oversized AND over-count AND wrong type: the format rejection wins.
+    const files = [
+      new File([new ArrayBuffer(64)], 'a.pdf', { type: 'application/pdf' }),
+      new File([new ArrayBuffer(64)], 'b.pdf', { type: 'application/pdf' }),
+    ]
+    fireEvent.drop(document.body, { dataTransfer: { types: ['Files'], files, dropEffect: 'none' } })
+    expect(addImages).toHaveBeenCalledWith(files)
+    expect(view.getByRole('alert').textContent).toContain('仅支持 PNG、JPG、WebP、GIF 格式的图片')
+  })
+
   it('shows the projected limits in the drop overlay desc line', () => {
     const { view } = bench({
       addImages: vi.fn(() => null),