Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@
<artifactId>poi-ooxml</artifactId>
<version>5.4.1</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-scratchpad</artifactId>
<version>5.4.1</version>
</dependency>
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
Expand Down
Binary file modified spring-dev.log
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,35 @@
import com.aiedu.platform.ai.chat.service.AiChatService;
import com.aiedu.platform.ai.chat.vo.AiChatResponse;
import com.aiedu.platform.common.result.ApiResponse;
import com.aiedu.platform.file.service.FileService;
import com.aiedu.platform.file.vo.FileUploadVO;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
@RequestMapping("/api/ai")
public class AiChatController {

private final AiChatService aiChatService;
private final FileService fileService;

public AiChatController(AiChatService aiChatService) {
public AiChatController(AiChatService aiChatService, FileService fileService) {
this.aiChatService = aiChatService;
this.fileService = fileService;
}

@PostMapping("/chat")
public ApiResponse<AiChatResponse> chat(@Valid @RequestBody ChatRequest request) {
return ApiResponse.success(aiChatService.chat(request));
}

@PostMapping("/chat/uploads")
public ApiResponse<FileUploadVO> upload(@RequestParam("file") MultipartFile file) {
return ApiResponse.success("上传成功", fileService.upload(file, "AI_CHAT", null));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.aiedu.platform.ai.chat.dto;

public record ChatAttachmentDTO(
Long fileId,
String fileName,
String contentType,
String kind
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

public record ChatMessageDTO(
String role,
String content
String content,
java.util.List<ChatAttachmentDTO> attachments
) {
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
package com.aiedu.platform.ai.chat.dto;

import jakarta.validation.constraints.NotBlank;

import java.util.List;

public record ChatRequest(
@NotBlank(message = "消息不能为空")
String message,
List<ChatMessageDTO> history,
List<ChatAttachmentDTO> attachments,
Long modelConfigId,
String promptTemplateCode
) {
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package com.aiedu.platform.ai.chat.service.impl;

import com.aiedu.platform.ai.chat.dto.ChatAttachmentDTO;
import com.aiedu.platform.ai.review.document.service.impl.DocumentTextExtractor;
import com.aiedu.platform.common.exception.BusinessException;
import com.aiedu.platform.file.domain.entity.FileRecord;
import com.aiedu.platform.file.mapper.FileRecordMapper;
import io.minio.GetObjectArgs;
import io.minio.MinioClient;
import org.springframework.stereotype.Component;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.Set;

@Component
public class ChatAttachmentResolver {

private static final int MAX_ATTACHMENTS = 6;
private static final long MAX_IMAGE_SIZE = 10 * 1024 * 1024L;
private static final Set<String> IMAGE_EXTENSIONS = Set.of("png", "jpg", "jpeg", "gif", "webp", "bmp");
private static final Set<String> DOCUMENT_EXTENSIONS = Set.of(
"txt", "md", "markdown", "json", "csv", "log", "java", "xml", "yml", "yaml", "pdf", "doc", "docx", "xlsx", "xls"
);

private final FileRecordMapper fileRecordMapper;
private final MinioClient minioClient;
private final DocumentTextExtractor documentTextExtractor;

public ChatAttachmentResolver(FileRecordMapper fileRecordMapper,
MinioClient minioClient,
DocumentTextExtractor documentTextExtractor) {
this.fileRecordMapper = fileRecordMapper;
this.minioClient = minioClient;
this.documentTextExtractor = documentTextExtractor;
}

public List<ResolvedAttachment> resolve(List<ChatAttachmentDTO> attachments) {
if (attachments == null || attachments.isEmpty()) {
return List.of();
}
if (attachments.size() > MAX_ATTACHMENTS) {
throw new BusinessException(400, "单次对话最多上传 6 个附件");
}

List<ResolvedAttachment> resolved = new ArrayList<>();
for (ChatAttachmentDTO attachment : attachments) {
Long fileId = attachment == null ? null : attachment.fileId();
if (fileId == null) {
continue;
}
FileRecord fileRecord = getFileRecord(fileId);
String kind = resolveKind(fileRecord);
if ("image".equals(kind)) {
if (fileRecord.getFileSize() != null && fileRecord.getFileSize() > MAX_IMAGE_SIZE) {
throw new BusinessException(400, "图片附件不能超过 10MB: " + fileRecord.getOriginalName());
}
resolved.add(new ResolvedAttachment(
fileRecord.getId(),
fileRecord.getOriginalName(),
imageContentType(fileRecord),
"image",
toBase64Data(fileRecord),
null
));
continue;
}

DocumentTextExtractor.ExtractedDocument extractedDocument = documentTextExtractor.extract(fileId);
resolved.add(new ResolvedAttachment(
fileRecord.getId(),
fileRecord.getOriginalName(),
safeContentType(fileRecord.getContentType()),
"document",
null,
extractedDocument.content()
));
}
return resolved;
}

private FileRecord getFileRecord(Long fileId) {
FileRecord fileRecord = fileRecordMapper.selectById(fileId);
if (fileRecord == null || fileRecord.getDeleted() != null && fileRecord.getDeleted() == 1) {
throw new BusinessException(404, "附件不存在: " + fileId);
}
return fileRecord;
}

private String resolveKind(FileRecord fileRecord) {
String extension = Optional.ofNullable(fileRecord.getFileExt())
.orElse("")
.toLowerCase(Locale.ROOT)
.replace(".", "");
String contentType = safeContentType(fileRecord.getContentType()).toLowerCase(Locale.ROOT);

if (contentType.startsWith("image/") || IMAGE_EXTENSIONS.contains(extension)) {
return "image";
}
if (DOCUMENT_EXTENSIONS.contains(extension)) {
return "document";
}
throw new BusinessException(400, "当前聊天暂不支持该附件类型: " + fileRecord.getOriginalName());
}

private String toBase64Data(FileRecord fileRecord) {
try (InputStream inputStream = minioClient.getObject(GetObjectArgs.builder()
.bucket(fileRecord.getBucketName())
.object(fileRecord.getObjectKey())
.build())) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
inputStream.transferTo(outputStream);
return Base64.getEncoder().encodeToString(outputStream.toByteArray());
} catch (Exception exception) {
throw new BusinessException(500, "读取图片附件失败: " + exception.getMessage());
}
}

private String imageContentType(FileRecord fileRecord) {
String contentType = safeContentType(fileRecord.getContentType());
if (contentType.toLowerCase(Locale.ROOT).startsWith("image/")) {
return contentType;
}
return switch (Optional.ofNullable(fileRecord.getFileExt()).orElse("").toLowerCase(Locale.ROOT).replace(".", "")) {
case "jpg", "jpeg" -> "image/jpeg";
case "png" -> "image/png";
case "gif" -> "image/gif";
case "webp" -> "image/webp";
case "bmp" -> "image/bmp";
default -> "image/png";
};
}

private String safeContentType(String contentType) {
return contentType == null || contentType.isBlank() ? "application/octet-stream" : contentType;
}

public record ResolvedAttachment(
Long fileId,
String fileName,
String contentType,
String kind,
String base64Data,
String extractedText
) {
}
}
19 changes: 19 additions & 0 deletions src/main/java/com/aiedu/platform/ai/core/enums/AiSceneType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.aiedu.platform.ai.core.enums;

public enum AiSceneType {

CHAT("CHAT"),
SUBJECTIVE_REVIEW("SUBJECTIVE_REVIEW"),
DOCUMENT_REVIEW("DOCUMENT_REVIEW"),
RAG("RAG");

private final String code;

AiSceneType(String code) {
this.code = code;
}

public String code() {
return code;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.aiedu.platform.ai.core.gateway;

import dev.langchain4j.model.output.FinishReason;
import dev.langchain4j.model.output.TokenUsage;

public record AiGatewayChatResult(
String text,
String requestId,
String modelName,
TokenUsage tokenUsage,
FinishReason finishReason
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.aiedu.platform.ai.core.gateway;

import dev.langchain4j.model.output.FinishReason;
import dev.langchain4j.model.output.TokenUsage;

public record AiGatewayStructuredResult<T>(
T content,
String rawText,
String requestId,
String modelName,
TokenUsage tokenUsage,
FinishReason finishReason
) {
}
Loading