From 0b764b79dea8b2f5100d2ad229eed53f484cfe20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=AD=90=E9=BB=98?= <925456043@qq.com> Date: Fri, 31 Jul 2026 09:19:11 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E8=81=8A=E5=A4=A9?= =?UTF-8?q?=E9=99=84=E4=BB=B6=E6=8B=96=E6=8B=BD=E4=B8=8A=E4=BC=A0=E6=8F=90?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 不支持格式改用轻量提示并展示支持范围 - 扩展拖拽区域并增加全区上传覆盖层 - 补充文档和图片格式校验测试 --- .../ai-chat/useChatDocumentUploads.test.ts | 15 ++ .../ai-chat/useChatImageUploads.test.ts | 15 ++ .../components/ai-chat/useChatImageUploads.ts | 30 +++- .../app/src/views/ai/agent-chat/index.vue | 169 ++++++++++++++---- .../ai/agents/components/AgentTryoutPanel.vue | 48 +++-- .../src/components/chat-timeline/index.ts | 2 + .../chat-timeline/useChatDocumentUploads.ts | 30 +++- 7 files changed, 242 insertions(+), 67 deletions(-) diff --git a/easyflow-ui-admin/app/src/components/ai-chat/useChatDocumentUploads.test.ts b/easyflow-ui-admin/app/src/components/ai-chat/useChatDocumentUploads.test.ts index b22e0508..d13d35c1 100644 --- a/easyflow-ui-admin/app/src/components/ai-chat/useChatDocumentUploads.test.ts +++ b/easyflow-ui-admin/app/src/components/ai-chat/useChatDocumentUploads.test.ts @@ -107,6 +107,21 @@ describe('useChatDocumentUploads', () => { }); }); + it('rejects unsupported files without adding an error attachment', async () => { + const uploads = useChatDocumentUploads(); + const result = await uploads.addFiles( + [new File(['archive'], 'unsupported.zip', { type: 'application/zip' })], + { agentId: 'agent-1', mode: 'FORMAL', sessionId: 'session-1' }, + ); + + expect(result).toEqual({ + rejectedCount: 0, + unsupportedCount: 1, + }); + expect(uploads.items.value).toEqual([]); + expect(mediaApi.uploadAgentChatDocument).not.toHaveBeenCalled(); + }); + it('exposes a server read failure without continuing to poll', async () => { mediaApi.uploadAgentChatDocument.mockResolvedValue({ data: { diff --git a/easyflow-ui-admin/app/src/components/ai-chat/useChatImageUploads.test.ts b/easyflow-ui-admin/app/src/components/ai-chat/useChatImageUploads.test.ts index 1d6e2360..390ebea6 100644 --- a/easyflow-ui-admin/app/src/components/ai-chat/useChatImageUploads.test.ts +++ b/easyflow-ui-admin/app/src/components/ai-chat/useChatImageUploads.test.ts @@ -59,4 +59,19 @@ describe('useChatImageUploads', () => { expect(observedStatuses.at(-1)).toBe('ready'); expect(uploads.uploadIds.value).toEqual(['upload-1']); }); + + it('rejects unsupported images without adding an error attachment', async () => { + const uploads = useChatImageUploads(); + const result = await uploads.addFiles( + [new File(['vector'], 'unsupported.svg', { type: 'image/svg+xml' })], + { agentId: 'agent-1', mode: 'FORMAL', sessionId: 'session-1' }, + ); + + expect(result).toEqual({ + rejectedCount: 0, + unsupportedCount: 1, + }); + expect(uploads.items.value).toEqual([]); + expect(mediaApi.uploadAgentChatImage).not.toHaveBeenCalled(); + }); }); diff --git a/easyflow-ui-admin/app/src/components/ai-chat/useChatImageUploads.ts b/easyflow-ui-admin/app/src/components/ai-chat/useChatImageUploads.ts index 61e084fb..d03f1231 100644 --- a/easyflow-ui-admin/app/src/components/ai-chat/useChatImageUploads.ts +++ b/easyflow-ui-admin/app/src/components/ai-chat/useChatImageUploads.ts @@ -7,6 +7,7 @@ import type { AgentComposerMode, AgentMediaUpload } from './mediaApi'; const MAX_IMAGES = 5; const MAX_IMAGE_BYTES = 10 * 1024 * 1024; +export const CHAT_IMAGE_SUPPORTED_FORMATS = 'PNG、JPG、JPEG、WebP、GIF、BMP'; const ACCEPTED_EXTENSIONS = new Set([ 'bmp', 'gif', @@ -26,6 +27,11 @@ interface LocalAttachment extends ChatImageAttachment { file?: File; } +interface ChatImageAddFilesResult { + rejectedCount: number; + unsupportedCount: number; +} + function createLocalId() { return `agent-image-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`; } @@ -69,12 +75,20 @@ export function useChatImageUploads() { ); async function addFiles(files: File[], context: UploadContext) { + const supportedFiles: File[] = []; + let unsupportedCount = 0; + for (const file of files) { + if (ACCEPTED_EXTENSIONS.has(extensionOf(file))) { + supportedFiles.push(file); + } else { + unsupportedCount++; + } + } const available = Math.max(0, MAX_IMAGES - items.value.length); - const accepted = files.slice(0, available); - const rejectedCount = Math.max(0, files.length - accepted.length); + const accepted = supportedFiles.slice(0, available); + const rejectedCount = Math.max(0, supportedFiles.length - accepted.length); const uploads: Promise[] = []; for (const file of accepted) { - const extension = extensionOf(file); const local: LocalAttachment = { file, localId: createLocalId(), @@ -85,11 +99,6 @@ export function useChatImageUploads() { status: 'uploading', }; items.value.push(local); - if (!ACCEPTED_EXTENSIONS.has(extension)) { - local.status = 'error'; - local.error = '仅支持 PNG、JPG、JPEG、WebP、GIF、BMP'; - continue; - } if (file.size > MAX_IMAGE_BYTES) { local.status = 'error'; local.error = '单张图片不能超过 10 MiB'; @@ -98,7 +107,10 @@ export function useChatImageUploads() { uploads.push(upload(local, context)); } await Promise.all(uploads); - return rejectedCount; + return { + rejectedCount, + unsupportedCount, + } satisfies ChatImageAddFilesResult; } async function upload(item: LocalAttachment, context: UploadContext) { diff --git a/easyflow-ui-admin/app/src/views/ai/agent-chat/index.vue b/easyflow-ui-admin/app/src/views/ai/agent-chat/index.vue index 7bee3a8d..77c39649 100644 --- a/easyflow-ui-admin/app/src/views/ai/agent-chat/index.vue +++ b/easyflow-ui-admin/app/src/views/ai/agent-chat/index.vue @@ -19,6 +19,7 @@ import { computed, onBeforeUnmount, onMounted, ref } from 'vue'; import { useRoute, useRouter } from 'vue-router'; import { + CHAT_DOCUMENT_SUPPORTED_FORMATS, ChatDocumentAttachments, ChatImageAttachments, ChatTimeline, @@ -33,6 +34,7 @@ import { Paperclip, Plus, Promotion, + UploadFilled, } from '@element-plus/icons-vue'; import { ElButton, @@ -52,6 +54,7 @@ import { loadAgentChatImage, } from '#/components/ai-chat/mediaApi'; import { useAgentComposerDraft } from '#/components/ai-chat/useAgentComposerDraft'; +import { CHAT_IMAGE_SUPPORTED_FORMATS } from '#/components/ai-chat/useChatImageUploads'; import ChatCapabilityMenu from '#/components/chat-workspace/ChatCapabilityMenu.vue'; import ChatInputTriggerPanel from '#/components/chat-workspace/ChatInputTriggerPanel.vue'; import { useChatInputTrigger } from '#/components/chat-workspace/input-triggers/useChatInputTrigger'; @@ -114,6 +117,11 @@ const selectedAgentImageSupport = computed(() => selectedAgent.value?.publishedSnapshotJson?.modelSummary?.supportImage, ), ); +const supportedAttachmentFormats = computed(() => + selectedAgentImageSupport.value === false + ? CHAT_DOCUMENT_SUPPORTED_FORMATS + : `${CHAT_DOCUMENT_SUPPORTED_FORMATS};图片支持 ${CHAT_IMAGE_SUPPORTED_FORMATS}`, +); const interactionDisplay = computed(() => resolveInteractionDisplay(selectedAgent.value), ); @@ -747,17 +755,29 @@ async function addImageFiles(files: File[]) { return; } if (selectedAgentImageSupport.value === false) { - ElMessage.warning('当前智能体不支持图片'); + ElMessage.warning({ + grouping: true, + message: `当前智能体不支持图片,支持:${CHAT_DOCUMENT_SUPPORTED_FORMATS}`, + }); return; } await composer.ensureSession(); - const rejected = await composer.images.addFiles(files, { - agentId: selectedAgentId.value, - mode: 'FORMAL', - sessionId: composer.sessionId.value, - }); + const { rejectedCount, unsupportedCount } = await composer.images.addFiles( + files, + { + agentId: selectedAgentId.value, + mode: 'FORMAL', + sessionId: composer.sessionId.value, + }, + ); composer.scheduleSave(); - if (rejected > 0) { + if (unsupportedCount > 0) { + ElMessage.warning({ + grouping: true, + message: `不支持该图片格式,支持:${CHAT_IMAGE_SUPPORTED_FORMATS}`, + }); + } + if (rejectedCount > 0) { ElMessage.warning('每次最多添加 5 张图片'); } } @@ -768,22 +788,38 @@ async function addDocumentFiles(files: File[]) { return; } await composer.ensureSession(); - const rejected = await composer.documents.addFiles(files, { - agentId: selectedAgentId.value, - mode: 'FORMAL', - sessionId: composer.sessionId.value, - }); + const { rejectedCount, unsupportedCount } = await composer.documents.addFiles( + files, + { + agentId: selectedAgentId.value, + mode: 'FORMAL', + sessionId: composer.sessionId.value, + }, + ); composer.scheduleSave(); - if (rejected > 0) { + if (unsupportedCount > 0) { + ElMessage.warning({ + grouping: true, + message: `不支持该文件格式,支持:${supportedAttachmentFormats.value}`, + }); + } + if (rejectedCount > 0) { ElMessage.warning('每轮最多添加 3 份文档'); } } +function isImageAttachmentFile(file: File) { + return ( + file.type.startsWith('image/') || + /\.(?:bmp|gif|jpe?g|png|webp)$/i.test(file.name) + ); +} + function handleAttachmentFiles(event: Event) { const target = event.target as HTMLInputElement; const files = [...(target.files || [])]; - const imageFiles = files.filter((file) => file.type.startsWith('image/')); - const documentFiles = files.filter((file) => !file.type.startsWith('image/')); + const imageFiles = files.filter((file) => isImageAttachmentFile(file)); + const documentFiles = files.filter((file) => !isImageAttachmentFile(file)); if (imageFiles.length > 0) void addImageFiles(imageFiles); if (documentFiles.length > 0) void addDocumentFiles(documentFiles); target.value = ''; @@ -810,6 +846,13 @@ function handleAttachmentDragEnter(event: DragEvent) { } } +function handleAttachmentDragOver(event: DragEvent) { + handleAttachmentDragEnter(event); + if (composerDragActive.value && event.dataTransfer) { + event.dataTransfer.dropEffect = 'copy'; + } +} + function handleAttachmentDragLeave(event: DragEvent) { const container = event.currentTarget as HTMLElement; if ( @@ -824,17 +867,8 @@ function handleAttachmentDrop(event: DragEvent) { composerDragActive.value = false; if (capabilityDisabled.value) return; const files = [...(event.dataTransfer?.files || [])]; - const imageFiles = files.filter( - (file) => - file.type.startsWith('image/') && - selectedAgentImageSupport.value !== false && - composer.images.items.value.length < 5, - ); - const documentFiles = files.filter( - (file) => - !file.type.startsWith('image/') && - composer.documents.items.value.length < 3, - ); + const imageFiles = files.filter((file) => isImageAttachmentFile(file)); + const documentFiles = files.filter((file) => !isImageAttachmentFile(file)); if (imageFiles.length > 0) void addImageFiles(imageFiles); if (documentFiles.length > 0) void addDocumentFiles(documentFiles); } @@ -1151,7 +1185,13 @@ onBeforeUnmount(() => { -
+
@@ -1214,14 +1254,7 @@ onBeforeUnmount(() => { />
-
+
{
+ +
+
+ + + +
松手上传文件
+
+ 支持 {{ supportedAttachmentFormats }} +
+
+
+
@@ -1526,9 +1577,53 @@ onBeforeUnmount(() => { box-shadow: var(--el-box-shadow-light); } -.agent-chat__composer.is-dragging { +.agent-chat__drop-overlay { + position: absolute; + inset: 16px; + z-index: 20; + display: grid; + pointer-events: none; background: var(--el-color-primary-light-9); - border-color: var(--el-color-primary-light-5); + border: 2px dashed var(--el-color-primary-light-5); + border-radius: 16px; + place-items: center; +} + +.agent-chat__drop-overlay-content { + display: flex; + flex-direction: column; + gap: 8px; + align-items: center; + max-width: min(560px, calc(100% - 48px)); + padding: 24px; + text-align: center; +} + +.agent-chat__drop-overlay-icon { + font-size: 40px; + color: var(--el-color-primary); +} + +.agent-chat__drop-overlay-title { + font-size: 18px; + font-weight: 600; + color: var(--el-text-color-primary); +} + +.agent-chat__drop-overlay-formats { + font-size: 13px; + line-height: 20px; + color: var(--el-text-color-secondary); +} + +.agent-chat-drag-fade-enter-active, +.agent-chat-drag-fade-leave-active { + transition: opacity 160ms ease; +} + +.agent-chat-drag-fade-enter-from, +.agent-chat-drag-fade-leave-to { + opacity: 0; } .agent-chat__trigger-panel { diff --git a/easyflow-ui-admin/app/src/views/ai/agents/components/AgentTryoutPanel.vue b/easyflow-ui-admin/app/src/views/ai/agents/components/AgentTryoutPanel.vue index 86c872c3..716816e8 100644 --- a/easyflow-ui-admin/app/src/views/ai/agents/components/AgentTryoutPanel.vue +++ b/easyflow-ui-admin/app/src/views/ai/agents/components/AgentTryoutPanel.vue @@ -14,7 +14,10 @@ import type { import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'; -import { ChatTimeline } from '@easyflow/common-ui'; +import { + CHAT_DOCUMENT_SUPPORTED_FORMATS, + ChatTimeline, +} from '@easyflow/common-ui'; import { BrushCleaning } from '@easyflow/icons'; import { copyTextToClipboard } from '@easyflow/utils'; @@ -26,6 +29,7 @@ import { loadAgentChatImage, } from '#/components/ai-chat/mediaApi'; import { useAgentComposerDraft } from '#/components/ai-chat/useAgentComposerDraft'; +import { CHAT_IMAGE_SUPPORTED_FORMATS } from '#/components/ai-chat/useChatImageUploads'; import { approveAgentRun, rejectAgentRun } from '../api'; import { useAgentTryoutStream } from '../composables/useAgentTryoutStream'; @@ -214,12 +218,21 @@ async function handleAddImageFiles(files: File[]) { return; } await composer.ensureSession(); - const rejected = await composer.images.addFiles(files, { - agentId: String(props.agent.id), - mode: 'DRAFT', - sessionId: composer.sessionId.value, - }); - if (rejected > 0) { + const { rejectedCount, unsupportedCount } = await composer.images.addFiles( + files, + { + agentId: String(props.agent.id), + mode: 'DRAFT', + sessionId: composer.sessionId.value, + }, + ); + if (unsupportedCount > 0) { + ElMessage.warning({ + grouping: true, + message: `不支持该图片格式,支持:${CHAT_IMAGE_SUPPORTED_FORMATS}`, + }); + } + if (rejectedCount > 0) { ElMessage.warning('每次最多添加 5 张图片'); } composer.scheduleSave(); @@ -231,12 +244,21 @@ async function handleAddDocumentFiles(files: File[]) { return; } await composer.ensureSession(); - const rejected = await composer.documents.addFiles(files, { - agentId: String(props.agent.id), - mode: 'DRAFT', - sessionId: composer.sessionId.value, - }); - if (rejected > 0) { + const { rejectedCount, unsupportedCount } = await composer.documents.addFiles( + files, + { + agentId: String(props.agent.id), + mode: 'DRAFT', + sessionId: composer.sessionId.value, + }, + ); + if (unsupportedCount > 0) { + ElMessage.warning({ + grouping: true, + message: `不支持该文件格式,支持:${CHAT_DOCUMENT_SUPPORTED_FORMATS}`, + }); + } + if (rejectedCount > 0) { ElMessage.warning('每轮最多添加 3 份文档'); } composer.scheduleSave(); diff --git a/easyflow-ui-admin/packages/effects/common-ui/src/components/chat-timeline/index.ts b/easyflow-ui-admin/packages/effects/common-ui/src/components/chat-timeline/index.ts index fde63d51..8a3cd72a 100644 --- a/easyflow-ui-admin/packages/effects/common-ui/src/components/chat-timeline/index.ts +++ b/easyflow-ui-admin/packages/effects/common-ui/src/components/chat-timeline/index.ts @@ -34,6 +34,8 @@ export type { ChatTimelineToolStatus, } from './types'; export { + CHAT_DOCUMENT_SUPPORTED_FORMATS, + type ChatDocumentAddFilesResult, type ChatDocumentUploadApi, type ChatDocumentUploadContext, type ChatDocumentUploadView, diff --git a/easyflow-ui-admin/packages/effects/common-ui/src/components/chat-timeline/useChatDocumentUploads.ts b/easyflow-ui-admin/packages/effects/common-ui/src/components/chat-timeline/useChatDocumentUploads.ts index 99f2bd58..e29bb8e9 100644 --- a/easyflow-ui-admin/packages/effects/common-ui/src/components/chat-timeline/useChatDocumentUploads.ts +++ b/easyflow-ui-admin/packages/effects/common-ui/src/components/chat-timeline/useChatDocumentUploads.ts @@ -11,6 +11,8 @@ const MAX_EXCEL_BYTES = 10 * 1024 * 1024; const MAX_TEXT_BYTES = 5 * 1024 * 1024; const POLL_INTERVAL_MS = 750; const MAX_POLL_ATTEMPTS = 80; +export const CHAT_DOCUMENT_SUPPORTED_FORMATS = + 'PDF、Word、PPT、Excel、TXT、Markdown'; const SUPPORTED_EXTENSIONS = new Set([ 'doc', 'docx', @@ -54,6 +56,11 @@ export interface ChatDocumentUploadApi { ) => Promise>; } +export interface ChatDocumentAddFilesResult { + rejectedCount: number; + unsupportedCount: number; +} + interface LocalDocument extends ChatDocumentAttachment { file?: File; } @@ -198,9 +205,18 @@ export function createChatDocumentUploads(api: ChatDocumentUploadApi) { } async function addFiles(files: File[], context: ChatDocumentUploadContext) { + const supportedFiles: File[] = []; + let unsupportedCount = 0; + for (const file of files) { + if (SUPPORTED_EXTENSIONS.has(extensionOf(file))) { + supportedFiles.push(file); + } else { + unsupportedCount++; + } + } const available = Math.max(0, MAX_DOCUMENTS - items.value.length); - const accepted = files.slice(0, available); - const rejectedCount = Math.max(0, files.length - accepted.length); + const accepted = supportedFiles.slice(0, available); + const rejectedCount = Math.max(0, supportedFiles.length - accepted.length); const currentTotal = items.value.reduce( (total, item) => total + Number(item.size || 0), 0, @@ -220,11 +236,6 @@ export function createChatDocumentUploads(api: ChatDocumentUploadApi) { items.value.push(item); const trackedItem = items.value[items.value.length - 1]; if (!trackedItem) continue; - if (!SUPPORTED_EXTENSIONS.has(extension)) { - trackedItem.status = 'error'; - trackedItem.error = '仅支持 PDF、Word、PPT、Excel、TXT、Markdown'; - continue; - } if (file.size > maxBytes(extension)) { trackedItem.status = 'error'; trackedItem.error = sizeLimitMessage(extension); @@ -239,7 +250,10 @@ export function createChatDocumentUploads(api: ChatDocumentUploadApi) { uploads.push(upload(trackedItem, context)); } await Promise.all(uploads); - return rejectedCount; + return { + rejectedCount, + unsupportedCount, + } satisfies ChatDocumentAddFilesResult; } async function remove(item: ChatDocumentAttachment) {