diff --git a/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/service/impl/AIFileUploadServiceImpl.java b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/service/impl/AIFileUploadServiceImpl.java index ffa8c874..a1c7d21a 100644 --- a/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/service/impl/AIFileUploadServiceImpl.java +++ b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/service/impl/AIFileUploadServiceImpl.java @@ -1,5 +1,6 @@ package org.xyzh.ai.service.impl; +import org.apache.dubbo.config.annotation.DubboReference; import org.apache.dubbo.config.annotation.DubboService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -13,15 +14,18 @@ import org.xyzh.ai.client.dto.DocumentUploadResponse; import org.xyzh.api.ai.dto.TbAgent; import org.xyzh.api.ai.service.AIFileUploadService; import org.xyzh.api.ai.service.AgentService; +import org.xyzh.api.file.dto.TbSysFileDTO; +import org.xyzh.api.file.service.FileService; import org.xyzh.common.auth.utils.LoginUtil; import org.xyzh.common.core.domain.ResultDomain; import java.io.File; +import java.util.HashMap; import java.util.List; import java.util.Map; /** - * @description AI文件上传服务实现(只负责与Dify交互,不处理minio和数据库) + * @description AI文件上传服务实现(同时上传到MinIO和Dify) * @filename AIFileUploadServiceImpl.java * @author yslg * @copyright xyzh @@ -37,6 +41,9 @@ public class AIFileUploadServiceImpl implements AIFileUploadService { @Autowired private AgentService agentService; + @DubboReference(version = "1.0.0", group = "file", timeout = 30000, retries = 0) + private FileService fileService; + // ============================ 对话文件管理 ============================ @Override @@ -56,31 +63,58 @@ public class AIFileUploadServiceImpl implements AIFileUploadService { } TbAgent agent = agentResult.getData(); + // 3. 获取当前用户 + String userId = LoginUtil.getCurrentUserId(); + if (!StringUtils.hasText(userId)) { + userId = "anonymous"; + } + File tempFile = null; + String sysFileId = null; + String sysFileUrl = null; + try { - // 3. 将MultipartFile转换为临时File + // 4. 上传到MinIO(通过FileService,使用字节数组方式) + byte[] fileBytes = file.getBytes(); + String fileName = file.getOriginalFilename(); + String contentType = file.getContentType(); + ResultDomain fileResult = fileService.uploadFileBytes(fileBytes, fileName, contentType, "ai-chat", agentId); + if (fileResult.getSuccess() && fileResult.getData() != null) { + TbSysFileDTO sysFile = fileResult.getData(); + sysFileId = sysFile.getFileId(); + sysFileUrl = sysFile.getUrl(); + logger.info("上传文件到MinIO成功: fileId={}, url={}", sysFileId, sysFileUrl); + } else { + logger.warn("上传文件到MinIO失败: {}", fileResult.getMessage()); + // MinIO上传失败不阻断流程,继续上传到Dify + } + + // 5. 将MultipartFile转换为临时File用于Dify上传 tempFile = File.createTempFile("upload_", "_" + file.getOriginalFilename()); file.transferTo(tempFile); - // 4. 获取当前用户 - String userId = LoginUtil.getCurrentUserId(); - if (!StringUtils.hasText(userId)) { - userId = "anonymous"; - } - - // 5. 上传到Dify + // 6. 上传到Dify DifyFileInfo difyFile = difyApiClient.uploadFileForChat(tempFile, file.getOriginalFilename(), userId, agent.getApiKey()); if (difyFile != null && StringUtils.hasText(difyFile.getId())) { - logger.info("上传对话文件成功: agentId={}, fileId={}", agentId, difyFile.getId()); - Map result = new java.util.HashMap<>(); + logger.info("上传对话文件到Dify成功: agentId={}, difyFileId={}", agentId, difyFile.getId()); + + Map result = new HashMap<>(); + // Dify返回的信息 result.put("id", difyFile.getId()); result.put("name", difyFile.getName()); result.put("size", difyFile.getSize()); result.put("type", difyFile.getType()); + result.put("extension", difyFile.getExtension()); + result.put("mime_type", difyFile.getMimeType()); result.put("upload_file_id", difyFile.getUploadFileId()); + // 系统文件信息(用于前端展示和数据库存储) + result.put("sys_file_id", sysFileId); + result.put("preview_url", sysFileUrl); + result.put("source_url", sysFileUrl); + return ResultDomain.success("上传成功", result); } - return ResultDomain.failure("上传文件失败"); + return ResultDomain.failure("上传文件到Dify失败"); } catch (Exception e) { logger.error("上传对话文件异常: {}", e.getMessage(), e); return ResultDomain.failure("上传文件异常: " + e.getMessage()); diff --git a/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/service/impl/AgentChatServiceImpl.java b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/service/impl/AgentChatServiceImpl.java index 4fdd10ef..725942bf 100644 --- a/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/service/impl/AgentChatServiceImpl.java +++ b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/service/impl/AgentChatServiceImpl.java @@ -346,6 +346,18 @@ public class AgentChatServiceImpl implements AgentChatService { userMessage.setChatId(chatId); userMessage.setRole("user"); userMessage.setContent(query); + + // 提取系统文件ID列表保存到消息中 + if (filesData != null && !filesData.isEmpty()) { + List sysFileIds = filesData.stream() + .map(DifyFileInfo::getSysFileId) + .filter(StringUtils::hasText) + .collect(java.util.stream.Collectors.toList()); + if (!sysFileIds.isEmpty()) { + userMessage.setFiles(sysFileIds); + } + } + chatMessageMapper.insertChatMessage(userMessage); // 5. 构建Dify请求 diff --git a/urbanLifelineWeb/packages/shared/src/api/ai/aichat.ts b/urbanLifelineWeb/packages/shared/src/api/ai/aichat.ts index 104eb959..c289ce7b 100644 --- a/urbanLifelineWeb/packages/shared/src/api/ai/aichat.ts +++ b/urbanLifelineWeb/packages/shared/src/api/ai/aichat.ts @@ -119,7 +119,7 @@ export const aiChatAPI = { const formData = new FormData() formData.append('file', file) formData.append('agentId', agentId) - const response = await api.uploadPut(`${this.baseUrl}/file/upload`, formData) + const response = await api.upload(`${this.baseUrl}/file/upload`, formData) return response.data } } \ No newline at end of file diff --git a/urbanLifelineWeb/packages/shared/src/types/ai/aiChat.ts b/urbanLifelineWeb/packages/shared/src/types/ai/aiChat.ts index 3c96b0fd..8e59d3e2 100644 --- a/urbanLifelineWeb/packages/shared/src/types/ai/aiChat.ts +++ b/urbanLifelineWeb/packages/shared/src/types/ai/aiChat.ts @@ -13,27 +13,27 @@ export interface DifyFileInfo { /** 文件扩展名 */ extension?: string /** 文件MIME类型 */ - mimeType?: string + mime_type?: string /** 上传人ID */ - createdBy?: string + created_by?: string /** 上传时间(时间戳) */ - createdAt?: number + created_at?: number /** 预览URL */ - previewUrl?: string + preview_url?: string /** 源文件URL */ - sourceUrl?: string + source_url?: string /** 文件类型:image、document、audio、video、file */ type?: string /** 传输方式:remote_url、local_file */ - transferMethod?: string + transfer_method?: string /** 文件URL或ID */ url?: string /** 本地文件上传ID */ - uploadFileId?: string + upload_file_id?: string /** 系统文件ID */ - sysFileId?: string + sys_file_id?: string /** 文件路径(从系统文件表获取) */ - filePath?: string + file_path?: string } /** diff --git a/urbanLifelineWeb/packages/workcase/src/views/public/AIChat/AIChatView.scss b/urbanLifelineWeb/packages/workcase/src/views/public/AIChat/AIChatView.scss index 009b6151..eb7f57ee 100644 --- a/urbanLifelineWeb/packages/workcase/src/views/public/AIChat/AIChatView.scss +++ b/urbanLifelineWeb/packages/workcase/src/views/public/AIChat/AIChatView.scss @@ -386,15 +386,18 @@ $brand-color-hover: #004488; &.user { flex-direction: row-reverse; + .message-content { + align-items: flex-end; + } + .message-bubble { background: $brand-color; color: #fff; border-radius: 16px 16px 4px 16px; + } - .message-time { - text-align: right; - color: rgba(255, 255, 255, 0.7); - } + .message-time { + text-align: right; } } } @@ -419,8 +422,18 @@ $brand-color-hover: #004488; } } - .message-bubble { + .message-content { + display: flex; + flex-direction: column; + gap: 8px; max-width: 80%; + + &.user { + align-items: flex-end; + } + } + + .message-bubble { padding: 12px 16px; border-radius: 16px 16px 16px 4px; background: #fff; @@ -458,11 +471,17 @@ $brand-color-hover: #004488; &:nth-child(3) { animation-delay: 0s; } } } + } + .message-time { + font-size: 12px; + color: #94a3b8; + } + + // 用户消息气泡中的样式 + .message-row.user .message-bubble { .message-time { - font-size: 12px; - color: #94a3b8; - margin-top: 8px; + color: rgba(255, 255, 255, 0.7); } } } @@ -538,11 +557,14 @@ $brand-color-hover: #004488; } .input-row { + display: flex; + align-items: flex-end; + gap: 8px; padding: 12px 16px; } .chat-textarea { - width: 100%; + flex: 1; border: none; outline: none; resize: none; @@ -551,25 +573,13 @@ $brand-color-hover: #004488; background: transparent; line-height: 1.5; max-height: 120px; + min-height: 24px; &::placeholder { color: #94a3b8; } } - .toolbar-row { - padding: 12px 16px; - display: flex; - justify-content: flex-end; - border-top: 1px solid #f8fafc; - } - - .toolbar-actions { - display: flex; - align-items: center; - gap: 8px; - } - .tool-btn { padding: 8px; color: #94a3b8; @@ -578,11 +588,21 @@ $brand-color-hover: #004488; border-radius: 8px; cursor: pointer; transition: all 0.2s; + flex-shrink: 0; &:hover { color: #64748b; background: #f1f5f9; } + + &.uploading { + cursor: not-allowed; + opacity: 0.6; + } + + .spin { + animation: spin 1s linear infinite; + } } .send-btn { @@ -593,6 +613,7 @@ $brand-color-hover: #004488; border-radius: 12px; cursor: not-allowed; transition: all 0.2s; + flex-shrink: 0; &.active { background: $brand-color; @@ -717,3 +738,137 @@ $brand-color-hover: #004488; text-decoration: underline; } } + +// ==================== 文件上传相关样式 ==================== +.uploaded-files { + display: flex; + flex-wrap: wrap; + gap: 8px; + padding: 12px 16px; + border-bottom: 1px solid #f1f5f9; +} + +.uploaded-file-item { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 10px; + background: #f8fafc; + border: 1px solid #e2e8f0; + border-radius: 8px; + max-width: 200px; + + .file-preview { + width: 32px; + height: 32px; + border-radius: 4px; + overflow: hidden; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + + &.image { + img { + width: 100%; + height: 100%; + object-fit: cover; + } + } + + &.document { + background: $brand-color-light; + color: $brand-color; + } + } + + .file-name { + flex: 1; + font-size: 12px; + color: #374151; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .remove-file-btn { + padding: 4px; + color: #94a3b8; + background: transparent; + border: none; + border-radius: 4px; + cursor: pointer; + transition: all 0.2s; + flex-shrink: 0; + + &:hover { + color: #ef4444; + background: #fef2f2; + } + } +} + +// 消息气泡外的文件样式 +.message-files { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.message-file-item { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + background: #fff; + border: 1px solid #e2e8f0; + border-radius: 10px; + max-width: 220px; + text-decoration: none; + color: #374151; + transition: all 0.2s; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); + + &:hover { + border-color: $brand-color; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + } + + .file-preview { + width: 36px; + height: 36px; + border-radius: 6px; + overflow: hidden; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + + &.image { + img { + width: 100%; + height: 100%; + object-fit: cover; + } + } + + &.document { + background: $brand-color-light; + color: $brand-color; + } + } + + .file-name { + flex: 1; + font-size: 13px; + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} diff --git a/urbanLifelineWeb/packages/workcase/src/views/public/AIChat/AIChatView.vue b/urbanLifelineWeb/packages/workcase/src/views/public/AIChat/AIChatView.vue index ab8cf24a..5250cb54 100644 --- a/urbanLifelineWeb/packages/workcase/src/views/public/AIChat/AIChatView.vue +++ b/urbanLifelineWeb/packages/workcase/src/views/public/AIChat/AIChatView.vue @@ -122,17 +122,38 @@ - -
-
+ +
+ +
+
+
+ | +
+ +
- | - @@ -160,8 +181,46 @@
- + +
+
+
+ +
+
+ +
+ {{ file.name }} + +
+
+
+ + +