feat(faq): support rich-text image upload and RAG output

- add FAQ image upload API with MinIO path and mime/size validation

- enable FAQ editor image upload and text-or-image validation

- include FAQ image metadata in vector content and retrieval output

- enforce markdown image preservation via bot system prompt rule
This commit is contained in:
2026-02-25 19:53:39 +08:00
parent fcf1100b56
commit 01f354ede5
8 changed files with 396 additions and 26 deletions

View File

@@ -4,31 +4,56 @@ import cn.dev33.satoken.annotation.SaCheckPermission;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryWrapper;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.MediaType;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import tech.easyflow.ai.entity.DocumentCollection;
import tech.easyflow.ai.entity.FaqItem;
import tech.easyflow.ai.service.DocumentCollectionService;
import tech.easyflow.ai.service.FaqCategoryService;
import tech.easyflow.ai.service.FaqItemService;
import tech.easyflow.common.annotation.UsePermission;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.filestorage.FileStorageService;
import tech.easyflow.common.vo.UploadResVo;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.common.web.jsonbody.JsonBody;
import javax.annotation.Resource;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@RestController
@RequestMapping("/api/v1/faqItem")
@UsePermission(moduleName = "/api/v1/documentCollection")
public class FaqItemController extends BaseCurdController<FaqItemService, FaqItem> {
private static final long MAX_IMAGE_SIZE_BYTES = 5L * 1024L * 1024L;
private static final Set<String> ALLOWED_IMAGE_TYPES = new HashSet<>(Arrays.asList(
"image/jpeg",
"image/png",
"image/webp",
"image/gif"
));
private final FaqCategoryService faqCategoryService;
@Resource
private DocumentCollectionService documentCollectionService;
@Resource(name = "default")
private FileStorageService fileStorageService;
public FaqItemController(FaqItemService service, FaqCategoryService faqCategoryService) {
super(service);
this.faqCategoryService = faqCategoryService;
@@ -118,8 +143,43 @@ public class FaqItemController extends BaseCurdController<FaqItemService, FaqIte
return Result.ok(service.removeFaqItem(new java.math.BigInteger(String.valueOf(id))));
}
@PostMapping(value = "uploadImage", produces = MediaType.APPLICATION_JSON_VALUE)
@SaCheckPermission("/api/v1/documentCollection/save")
public Result<UploadResVo> uploadImage(MultipartFile file, BigInteger collectionId) {
if (collectionId == null) {
throw new BusinessException("知识库ID不能为空");
}
if (file == null || file.isEmpty()) {
throw new BusinessException("图片不能为空");
}
if (file.getSize() > MAX_IMAGE_SIZE_BYTES) {
throw new BusinessException("图片大小不能超过5MB");
}
if (!isAllowedImageType(file)) {
throw new BusinessException("仅支持 JPG/PNG/WEBP/GIF 图片");
}
DocumentCollection collection = documentCollectionService.getById(collectionId);
if (collection == null) {
throw new BusinessException("知识库不存在");
}
if (!collection.isFaqCollection()) {
throw new BusinessException("当前知识库不是FAQ类型");
}
String path = fileStorageService.save(file, "faq/" + collectionId);
UploadResVo resVo = new UploadResVo();
resVo.setPath(path);
return Result.ok(resVo);
}
@Override
protected String getDefaultOrderBy() {
return "order_no asc";
}
private boolean isAllowedImageType(MultipartFile file) {
String contentType = file.getContentType();
return StringUtils.hasText(contentType) && ALLOWED_IMAGE_TYPES.contains(contentType.toLowerCase());
}
}