From 8d285e1abcd9077ce89effa501db199683c7c3b8 Mon Sep 17 00:00:00 2001 From: Chuck1sn Date: Wed, 25 Jun 2025 15:10:02 +0800 Subject: [PATCH 01/14] add document ingestor --- backend/build.gradle.kts | 4 +- .../mjga/config/ai/ChatModelInitializer.java | 8 +- .../mjga/config/ai/EmbeddingInitializer.java | 39 ++++++++- .../config/security/WebSecurityConfig.java | 1 + .../com/zl/mjga/controller/AiController.java | 14 +++ .../controller/IdentityAccessController.java | 47 +--------- .../com/zl/mjga/service/EmbeddingService.java | 25 +++++- .../com/zl/mjga/service/UploadService.java | 85 +++++++++++++++++++ 8 files changed, 173 insertions(+), 50 deletions(-) create mode 100644 backend/src/main/java/com/zl/mjga/service/UploadService.java diff --git a/backend/build.gradle.kts b/backend/build.gradle.kts index 0047b5f..4464b35 100644 --- a/backend/build.gradle.kts +++ b/backend/build.gradle.kts @@ -32,7 +32,7 @@ sourceSets { group = "com.zl.mjga" version = "1.0.0" description = "make java great again!" -java.sourceCompatibility = JavaVersion.VERSION_17 +java.sourceCompatibility = JavaVersion.VERSION_21 configurations { compileOnly { @@ -64,6 +64,8 @@ dependencies { implementation("dev.langchain4j:langchain4j-open-ai:1.0.0") implementation("dev.langchain4j:langchain4j-pgvector:1.0.1-beta6") implementation("dev.langchain4j:langchain4j-community-zhipu-ai:1.0.1-beta6") + implementation("dev.langchain4j:langchain4j-easy-rag:1.1.0-beta7") + implementation("dev.langchain4j:langchain4j-document-loader-amazon-s3:1.1.0-beta7") implementation("io.projectreactor:reactor-core:3.7.6") testImplementation("org.testcontainers:junit-jupiter:$testcontainersVersion") testImplementation("org.testcontainers:postgresql:$testcontainersVersion") diff --git a/backend/src/main/java/com/zl/mjga/config/ai/ChatModelInitializer.java b/backend/src/main/java/com/zl/mjga/config/ai/ChatModelInitializer.java index eac37d4..ea27d5f 100644 --- a/backend/src/main/java/com/zl/mjga/config/ai/ChatModelInitializer.java +++ b/backend/src/main/java/com/zl/mjga/config/ai/ChatModelInitializer.java @@ -3,9 +3,12 @@ package com.zl.mjga.config.ai; import com.zl.mjga.component.PromptConfiguration; import com.zl.mjga.service.LlmService; import dev.langchain4j.community.model.zhipu.ZhipuAiStreamingChatModel; +import dev.langchain4j.data.segment.TextSegment; import dev.langchain4j.memory.chat.MessageWindowChatMemory; import dev.langchain4j.model.openai.OpenAiStreamingChatModel; +import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever; import dev.langchain4j.service.AiServices; +import dev.langchain4j.store.embedding.EmbeddingStore; import lombok.RequiredArgsConstructor; import org.jooq.generated.mjga.enums.LlmCodeEnum; import org.jooq.generated.mjga.tables.pojos.AiLlmConfig; @@ -54,11 +57,14 @@ public class ChatModelInitializer { @Bean @DependsOn("flywayInitializer") - public AiChatAssistant zhiPuChatAssistant(ZhipuAiStreamingChatModel zhipuChatModel) { + public AiChatAssistant zhiPuChatAssistant( + ZhipuAiStreamingChatModel zhipuChatModel, + EmbeddingStore zhiPuLibraryEmbeddingStore) { return AiServices.builder(AiChatAssistant.class) .streamingChatModel(zhipuChatModel) .systemMessageProvider(chatMemoryId -> promptConfiguration.getSystem()) .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10)) + .contentRetriever(EmbeddingStoreContentRetriever.from(zhiPuLibraryEmbeddingStore)) .build(); } diff --git a/backend/src/main/java/com/zl/mjga/config/ai/EmbeddingInitializer.java b/backend/src/main/java/com/zl/mjga/config/ai/EmbeddingInitializer.java index bc81f98..891b63d 100644 --- a/backend/src/main/java/com/zl/mjga/config/ai/EmbeddingInitializer.java +++ b/backend/src/main/java/com/zl/mjga/config/ai/EmbeddingInitializer.java @@ -1,10 +1,14 @@ package com.zl.mjga.config.ai; +import com.zl.mjga.config.minio.MinIoConfig; import com.zl.mjga.service.LlmService; import dev.langchain4j.community.model.zhipu.ZhipuAiEmbeddingModel; +import dev.langchain4j.data.document.loader.amazon.s3.AmazonS3DocumentLoader; +import dev.langchain4j.data.document.loader.amazon.s3.AwsCredentials; import dev.langchain4j.data.segment.TextSegment; import dev.langchain4j.model.embedding.EmbeddingModel; import dev.langchain4j.store.embedding.EmbeddingStore; +import dev.langchain4j.store.embedding.EmbeddingStoreIngestor; import dev.langchain4j.store.embedding.pgvector.PgVectorEmbeddingStore; import jakarta.annotation.Resource; import lombok.RequiredArgsConstructor; @@ -42,7 +46,7 @@ public class EmbeddingInitializer { } @Bean - public EmbeddingStore zhiPuEmbeddingStore(EmbeddingModel zhipuEmbeddingModel) { + public EmbeddingStore zhiPuEmbeddingStore() { String hostPort = env.getProperty("DATABASE_HOST_PORT"); String host = hostPort.split(":")[0]; return PgVectorEmbeddingStore.builder() @@ -55,4 +59,37 @@ public class EmbeddingInitializer { .dimension(2048) .build(); } + + @Bean + public EmbeddingStore zhiPuLibraryEmbeddingStore() { + String hostPort = env.getProperty("DATABASE_HOST_PORT"); + String host = hostPort.split(":")[0]; + return PgVectorEmbeddingStore.builder() + .host(host) + .port(env.getProperty("DATABASE_EXPOSE_PORT", Integer.class)) + .database(env.getProperty("DATABASE_DB")) + .user(env.getProperty("DATABASE_USER")) + .password(env.getProperty("DATABASE_PASSWORD")) + .table("mjga.zhipu_library_embedding_store") + .dimension(2048) + .build(); + } + + @Bean + public EmbeddingStoreIngestor zhipuEmbeddingStoreIngestor( + EmbeddingStore zhiPuLibraryEmbeddingStore, EmbeddingModel zhipuEmbeddingModel) { + return EmbeddingStoreIngestor.builder() + .embeddingModel(zhipuEmbeddingModel) + .embeddingStore(zhiPuLibraryEmbeddingStore) + .build(); + } + + @Bean + public AmazonS3DocumentLoader amazonS3DocumentLoader(MinIoConfig minIoConfig) { + return AmazonS3DocumentLoader.builder() + .endpointUrl(minIoConfig.getEndpoint()) + .forcePathStyle(true) + .awsCredentials(new AwsCredentials(minIoConfig.getAccessKey(), minIoConfig.getSecretKey())) + .build(); + } } diff --git a/backend/src/main/java/com/zl/mjga/config/security/WebSecurityConfig.java b/backend/src/main/java/com/zl/mjga/config/security/WebSecurityConfig.java index 6592366..d43c3cd 100644 --- a/backend/src/main/java/com/zl/mjga/config/security/WebSecurityConfig.java +++ b/backend/src/main/java/com/zl/mjga/config/security/WebSecurityConfig.java @@ -41,6 +41,7 @@ public class WebSecurityConfig { new AntPathRequestMatcher("/auth/sign-up", HttpMethod.POST.name()), new AntPathRequestMatcher("/v3/api-docs/**", HttpMethod.GET.name()), new AntPathRequestMatcher("/swagger-ui/**", HttpMethod.GET.name()), + new AntPathRequestMatcher("/ai/library/upload", HttpMethod.POST.name()), new AntPathRequestMatcher("/swagger-ui.html", HttpMethod.GET.name()), new AntPathRequestMatcher("/error")); } diff --git a/backend/src/main/java/com/zl/mjga/controller/AiController.java b/backend/src/main/java/com/zl/mjga/controller/AiController.java index 3c912b9..969a035 100644 --- a/backend/src/main/java/com/zl/mjga/controller/AiController.java +++ b/backend/src/main/java/com/zl/mjga/controller/AiController.java @@ -9,6 +9,7 @@ import com.zl.mjga.repository.*; import com.zl.mjga.service.AiChatService; import com.zl.mjga.service.EmbeddingService; import com.zl.mjga.service.LlmService; +import com.zl.mjga.service.UploadService; import dev.langchain4j.service.TokenStream; import jakarta.validation.Valid; import java.security.Principal; @@ -24,6 +25,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; import reactor.core.publisher.Flux; import reactor.core.publisher.Sinks; @@ -42,6 +44,7 @@ public class AiController { private final RoleRepository repository; private final PermissionRepository permissionRepository; private final RoleRepository roleRepository; + private final UploadService uploadService; @PostMapping(value = "/action/execute", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux actionExecute(Principal principal, @RequestBody String userMessage) { @@ -169,4 +172,15 @@ public class AiController { void createNewConversation(Principal principal) { aiChatService.evictChatMemory(principal.getName()); } + + @PostMapping( + value = "/library/upload", + consumes = MediaType.MULTIPART_FORM_DATA_VALUE, + produces = MediaType.TEXT_PLAIN_VALUE) + public String uploadLibraryFile(@RequestPart("file") MultipartFile multipartFile) + throws Exception { + String objectName = uploadService.uploadLibraryFile(multipartFile); + embeddingService.ingestDocument(objectName); + return objectName; + } } diff --git a/backend/src/main/java/com/zl/mjga/controller/IdentityAccessController.java b/backend/src/main/java/com/zl/mjga/controller/IdentityAccessController.java index bf28040..577f08a 100644 --- a/backend/src/main/java/com/zl/mjga/controller/IdentityAccessController.java +++ b/backend/src/main/java/com/zl/mjga/controller/IdentityAccessController.java @@ -1,6 +1,5 @@ package com.zl.mjga.controller; -import com.zl.mjga.config.minio.MinIoConfig; import com.zl.mjga.dto.PageRequestDto; import com.zl.mjga.dto.PageResponseDto; import com.zl.mjga.dto.department.DepartmentBindDto; @@ -13,17 +12,11 @@ import com.zl.mjga.repository.PermissionRepository; import com.zl.mjga.repository.RoleRepository; import com.zl.mjga.repository.UserRepository; import com.zl.mjga.service.IdentityAccessService; -import io.minio.MinioClient; -import io.minio.PutObjectArgs; +import com.zl.mjga.service.UploadService; import jakarta.validation.Valid; -import java.awt.image.BufferedImage; import java.security.Principal; -import java.time.Instant; import java.util.List; -import javax.imageio.ImageIO; import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.RandomStringUtils; -import org.apache.commons.lang3.StringUtils; import org.jooq.generated.mjga.tables.pojos.User; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -41,8 +34,7 @@ public class IdentityAccessController { private final UserRepository userRepository; private final RoleRepository roleRepository; private final PermissionRepository permissionRepository; - private final MinioClient minioClient; - private final MinIoConfig minIoConfig; + private final UploadService uploadService; @PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_USER_ROLE_PERMISSION)") @PostMapping( @@ -50,40 +42,7 @@ public class IdentityAccessController { consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.TEXT_PLAIN_VALUE) public String uploadAvatar(@RequestPart("file") MultipartFile multipartFile) throws Exception { - String originalFilename = multipartFile.getOriginalFilename(); - if (StringUtils.isEmpty(originalFilename)) { - throw new BusinessException("文件名不能为空"); - } - String contentType = multipartFile.getContentType(); - String extension = ""; - if ("image/jpeg".equals(contentType)) { - extension = ".jpg"; - } else if ("image/png".equals(contentType)) { - extension = ".png"; - } - String objectName = - String.format( - "/avatar/%d%s%s", - Instant.now().toEpochMilli(), - RandomStringUtils.insecure().nextAlphabetic(6), - extension); - if (multipartFile.isEmpty()) { - throw new BusinessException("上传的文件不能为空"); - } - long size = multipartFile.getSize(); - if (size > 200 * 1024) { - throw new BusinessException("头像文件大小不能超过200KB"); - } - BufferedImage img = ImageIO.read(multipartFile.getInputStream()); - if (img == null) { - throw new BusinessException("非法的上传文件"); - } - minioClient.putObject( - PutObjectArgs.builder().bucket(minIoConfig.getDefaultBucket()).object(objectName).stream( - multipartFile.getInputStream(), size, -1) - .contentType(multipartFile.getContentType()) - .build()); - return objectName; + return uploadService.uploadAvatarFile(multipartFile); } @GetMapping("/me") diff --git a/backend/src/main/java/com/zl/mjga/service/EmbeddingService.java b/backend/src/main/java/com/zl/mjga/service/EmbeddingService.java index 8967838..8c9eaaa 100644 --- a/backend/src/main/java/com/zl/mjga/service/EmbeddingService.java +++ b/backend/src/main/java/com/zl/mjga/service/EmbeddingService.java @@ -3,25 +3,30 @@ package com.zl.mjga.service; import static dev.langchain4j.store.embedding.filter.MetadataFilterBuilder.metadataKey; import com.zl.mjga.config.ai.ZhiPuEmbeddingModelConfig; +import com.zl.mjga.config.minio.MinIoConfig; import com.zl.mjga.model.urp.Actions; +import dev.langchain4j.data.document.Document; import dev.langchain4j.data.document.Metadata; +import dev.langchain4j.data.document.loader.amazon.s3.AmazonS3DocumentLoader; +import dev.langchain4j.data.document.parser.apache.tika.ApacheTikaDocumentParser; import dev.langchain4j.data.embedding.Embedding; import dev.langchain4j.data.segment.TextSegment; import dev.langchain4j.model.embedding.EmbeddingModel; -import dev.langchain4j.store.embedding.EmbeddingSearchRequest; -import dev.langchain4j.store.embedding.EmbeddingSearchResult; -import dev.langchain4j.store.embedding.EmbeddingStore; +import dev.langchain4j.store.embedding.*; import dev.langchain4j.store.embedding.filter.Filter; +import io.minio.errors.*; import jakarta.annotation.PostConstruct; import java.util.HashMap; import java.util.Map; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Service; @Configuration @RequiredArgsConstructor @Service +@Slf4j public class EmbeddingService { private final EmbeddingModel zhipuEmbeddingModel; @@ -30,6 +35,20 @@ public class EmbeddingService { private final ZhiPuEmbeddingModelConfig zhiPuEmbeddingModelConfig; + private final AmazonS3DocumentLoader amazonS3DocumentLoader; + + private final EmbeddingStoreIngestor zhiPuEmbeddingStoreIngestor; + + private final MinIoConfig minIoConfig; + + public void ingestDocument(String objectName) { + Document document = + amazonS3DocumentLoader.loadDocument( + minIoConfig.getDefaultBucket(), objectName, new ApacheTikaDocumentParser()); + IngestionResult ingest = zhiPuEmbeddingStoreIngestor.ingest(document); + log.info("Ingest document finished {}", ingest); + } + public Map searchAction(String message) { Map result = new HashMap<>(); EmbeddingSearchRequest embeddingSearchRequest = diff --git a/backend/src/main/java/com/zl/mjga/service/UploadService.java b/backend/src/main/java/com/zl/mjga/service/UploadService.java new file mode 100644 index 0000000..654dfad --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/service/UploadService.java @@ -0,0 +1,85 @@ +package com.zl.mjga.service; + +import com.zl.mjga.config.minio.MinIoConfig; +import com.zl.mjga.exception.BusinessException; +import io.minio.*; +import java.awt.image.BufferedImage; +import java.time.Instant; +import javax.imageio.ImageIO; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +@Service +@RequiredArgsConstructor +@Slf4j +public class UploadService { + + private final MinioClient minioClient; + private final MinIoConfig minIoConfig; + + public String uploadAvatarFile(MultipartFile multipartFile) throws Exception { + String originalFilename = multipartFile.getOriginalFilename(); + if (StringUtils.isEmpty(originalFilename)) { + throw new BusinessException("文件名不能为空"); + } + String contentType = multipartFile.getContentType(); + String extension = ""; + if ("image/jpeg".equals(contentType)) { + extension = ".jpg"; + } else if ("image/png".equals(contentType)) { + extension = ".png"; + } + String objectName = + String.format( + "/library/%d%s%s", + Instant.now().toEpochMilli(), + RandomStringUtils.insecure().nextAlphabetic(6), + extension); + if (multipartFile.isEmpty()) { + throw new BusinessException("上传的文件不能为空"); + } + long size = multipartFile.getSize(); + if (size > 200 * 1024) { + throw new BusinessException("头像大小不能超过200KB"); + } + BufferedImage img = ImageIO.read(multipartFile.getInputStream()); + if (img == null) { + throw new BusinessException("非法的上传文件"); + } + minioClient.putObject( + PutObjectArgs.builder().bucket(minIoConfig.getDefaultBucket()).object(objectName).stream( + multipartFile.getInputStream(), size, -1) + .contentType(multipartFile.getContentType()) + .build()); + return objectName; + } + + public String uploadLibraryFile(MultipartFile multipartFile) throws Exception { + String originalFilename = multipartFile.getOriginalFilename(); + if (StringUtils.isEmpty(originalFilename)) { + throw new BusinessException("文件名不能为空"); + } + String objectName = String.format("/library/%s", originalFilename); + if (multipartFile.isEmpty()) { + throw new BusinessException("上传的文件不能为空"); + } + long size = multipartFile.getSize(); + if (size > 1024 * 1024) { + throw new BusinessException("知识库文档大小不能超过1MB"); + } + String contentType = multipartFile.getContentType(); + if (!StringUtils.startsWith(contentType, "text/")) { + throw new BusinessException("非法的上传文件"); + } + minioClient.putObject( + PutObjectArgs.builder().bucket(minIoConfig.getDefaultBucket()).object(objectName).stream( + multipartFile.getInputStream(), size, -1) + .contentType(multipartFile.getContentType()) + .build()); + return objectName; + } +} From 5494181ae029caf7f2eeb6ecefb60f040ccd5f25 Mon Sep 17 00:00:00 2001 From: Chuck1sn Date: Thu, 26 Jun 2025 15:54:38 +0800 Subject: [PATCH 02/14] init library --- backend/build.gradle.kts | 7 +- .../mjga/config/ai/ChatModelInitializer.java | 12 +- .../mjga/config/ai/EmbeddingInitializer.java | 10 - .../com/zl/mjga/controller/AiController.java | 20 +- .../zl/mjga/controller/LibraryController.java | 66 ++++++ .../mjga/dto/library/LibraryDocUpdateDto.java | 6 + .../zl/mjga/dto/library/LibraryUpsertDto.java | 5 + .../mjga/repository/LibraryDocRepository.java | 14 ++ .../LibraryDocSegmentRepository.java | 14 ++ .../zl/mjga/repository/LibraryRepository.java | 15 ++ .../com/zl/mjga/service/EmbeddingService.java | 92 --------- .../java/com/zl/mjga/service/RagService.java | 189 ++++++++++++++++++ .../com/zl/mjga/service/UploadService.java | 2 +- .../db/migration/V1_0_0__init_table.sql | 14 +- .../db/migration/V1_0_3__init_library.sql | 28 +++ .../db/migration/test/V1_0_0__init_table.sql | 14 +- .../migration/test/V1_0_3__init_library.sql | 28 +++ 17 files changed, 394 insertions(+), 142 deletions(-) create mode 100644 backend/src/main/java/com/zl/mjga/controller/LibraryController.java create mode 100644 backend/src/main/java/com/zl/mjga/dto/library/LibraryDocUpdateDto.java create mode 100644 backend/src/main/java/com/zl/mjga/dto/library/LibraryUpsertDto.java create mode 100644 backend/src/main/java/com/zl/mjga/repository/LibraryDocRepository.java create mode 100644 backend/src/main/java/com/zl/mjga/repository/LibraryDocSegmentRepository.java create mode 100644 backend/src/main/java/com/zl/mjga/repository/LibraryRepository.java delete mode 100644 backend/src/main/java/com/zl/mjga/service/EmbeddingService.java create mode 100644 backend/src/main/java/com/zl/mjga/service/RagService.java create mode 100644 backend/src/main/resources/db/migration/V1_0_3__init_library.sql create mode 100644 backend/src/test/resources/db/migration/test/V1_0_3__init_library.sql diff --git a/backend/build.gradle.kts b/backend/build.gradle.kts index 4464b35..5b872e7 100644 --- a/backend/build.gradle.kts +++ b/backend/build.gradle.kts @@ -64,7 +64,7 @@ dependencies { implementation("dev.langchain4j:langchain4j-open-ai:1.0.0") implementation("dev.langchain4j:langchain4j-pgvector:1.0.1-beta6") implementation("dev.langchain4j:langchain4j-community-zhipu-ai:1.0.1-beta6") - implementation("dev.langchain4j:langchain4j-easy-rag:1.1.0-beta7") + implementation("dev.langchain4j:langchain4j-document-parser-apache-tika:1.1.0-beta7") implementation("dev.langchain4j:langchain4j-document-loader-amazon-s3:1.1.0-beta7") implementation("io.projectreactor:reactor-core:3.7.6") testImplementation("org.testcontainers:junit-jupiter:$testcontainersVersion") @@ -169,11 +169,6 @@ jooq { } } forcedTypes { - forcedType { - name = "varchar" - includeExpression = ".*" - includeTypes = "JSONB?" - } forcedType { name = "varchar" includeExpression = ".*" diff --git a/backend/src/main/java/com/zl/mjga/config/ai/ChatModelInitializer.java b/backend/src/main/java/com/zl/mjga/config/ai/ChatModelInitializer.java index ea27d5f..4c9727d 100644 --- a/backend/src/main/java/com/zl/mjga/config/ai/ChatModelInitializer.java +++ b/backend/src/main/java/com/zl/mjga/config/ai/ChatModelInitializer.java @@ -5,6 +5,7 @@ import com.zl.mjga.service.LlmService; import dev.langchain4j.community.model.zhipu.ZhipuAiStreamingChatModel; import dev.langchain4j.data.segment.TextSegment; import dev.langchain4j.memory.chat.MessageWindowChatMemory; +import dev.langchain4j.model.embedding.EmbeddingModel; import dev.langchain4j.model.openai.OpenAiStreamingChatModel; import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever; import dev.langchain4j.service.AiServices; @@ -59,12 +60,19 @@ public class ChatModelInitializer { @DependsOn("flywayInitializer") public AiChatAssistant zhiPuChatAssistant( ZhipuAiStreamingChatModel zhipuChatModel, - EmbeddingStore zhiPuLibraryEmbeddingStore) { + EmbeddingStore zhiPuLibraryEmbeddingStore, + EmbeddingModel zhipuEmbeddingModel) { return AiServices.builder(AiChatAssistant.class) .streamingChatModel(zhipuChatModel) .systemMessageProvider(chatMemoryId -> promptConfiguration.getSystem()) .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10)) - .contentRetriever(EmbeddingStoreContentRetriever.from(zhiPuLibraryEmbeddingStore)) + .contentRetriever( + EmbeddingStoreContentRetriever.builder() + .embeddingStore(zhiPuLibraryEmbeddingStore) + .embeddingModel(zhipuEmbeddingModel) + .minScore(0.75) + .maxResults(5) + .build()) .build(); } diff --git a/backend/src/main/java/com/zl/mjga/config/ai/EmbeddingInitializer.java b/backend/src/main/java/com/zl/mjga/config/ai/EmbeddingInitializer.java index 891b63d..79aefd0 100644 --- a/backend/src/main/java/com/zl/mjga/config/ai/EmbeddingInitializer.java +++ b/backend/src/main/java/com/zl/mjga/config/ai/EmbeddingInitializer.java @@ -8,7 +8,6 @@ import dev.langchain4j.data.document.loader.amazon.s3.AwsCredentials; import dev.langchain4j.data.segment.TextSegment; import dev.langchain4j.model.embedding.EmbeddingModel; import dev.langchain4j.store.embedding.EmbeddingStore; -import dev.langchain4j.store.embedding.EmbeddingStoreIngestor; import dev.langchain4j.store.embedding.pgvector.PgVectorEmbeddingStore; import jakarta.annotation.Resource; import lombok.RequiredArgsConstructor; @@ -75,15 +74,6 @@ public class EmbeddingInitializer { .build(); } - @Bean - public EmbeddingStoreIngestor zhipuEmbeddingStoreIngestor( - EmbeddingStore zhiPuLibraryEmbeddingStore, EmbeddingModel zhipuEmbeddingModel) { - return EmbeddingStoreIngestor.builder() - .embeddingModel(zhipuEmbeddingModel) - .embeddingStore(zhiPuLibraryEmbeddingStore) - .build(); - } - @Bean public AmazonS3DocumentLoader amazonS3DocumentLoader(MinIoConfig minIoConfig) { return AmazonS3DocumentLoader.builder() diff --git a/backend/src/main/java/com/zl/mjga/controller/AiController.java b/backend/src/main/java/com/zl/mjga/controller/AiController.java index 969a035..df74e09 100644 --- a/backend/src/main/java/com/zl/mjga/controller/AiController.java +++ b/backend/src/main/java/com/zl/mjga/controller/AiController.java @@ -7,9 +7,8 @@ import com.zl.mjga.dto.ai.LlmVm; import com.zl.mjga.exception.BusinessException; import com.zl.mjga.repository.*; import com.zl.mjga.service.AiChatService; -import com.zl.mjga.service.EmbeddingService; import com.zl.mjga.service.LlmService; -import com.zl.mjga.service.UploadService; +import com.zl.mjga.service.RagService; import dev.langchain4j.service.TokenStream; import jakarta.validation.Valid; import java.security.Principal; @@ -25,7 +24,6 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; -import org.springframework.web.multipart.MultipartFile; import reactor.core.publisher.Flux; import reactor.core.publisher.Sinks; @@ -37,14 +35,13 @@ public class AiController { private final AiChatService aiChatService; private final LlmService llmService; - private final EmbeddingService embeddingService; + private final RagService ragService; private final UserRepository userRepository; private final DepartmentRepository departmentRepository; private final PositionRepository positionRepository; private final RoleRepository repository; private final PermissionRepository permissionRepository; private final RoleRepository roleRepository; - private final UploadService uploadService; @PostMapping(value = "/action/execute", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux actionExecute(Principal principal, @RequestBody String userMessage) { @@ -112,7 +109,7 @@ public class AiController { if (!aiLlmConfig.getEnable()) { throw new BusinessException("命令模型未启用,请开启后再试。"); } - return embeddingService.searchAction(message); + return ragService.searchAction(message); } @PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_USER_ROLE_PERMISSION)") @@ -172,15 +169,4 @@ public class AiController { void createNewConversation(Principal principal) { aiChatService.evictChatMemory(principal.getName()); } - - @PostMapping( - value = "/library/upload", - consumes = MediaType.MULTIPART_FORM_DATA_VALUE, - produces = MediaType.TEXT_PLAIN_VALUE) - public String uploadLibraryFile(@RequestPart("file") MultipartFile multipartFile) - throws Exception { - String objectName = uploadService.uploadLibraryFile(multipartFile); - embeddingService.ingestDocument(objectName); - return objectName; - } } diff --git a/backend/src/main/java/com/zl/mjga/controller/LibraryController.java b/backend/src/main/java/com/zl/mjga/controller/LibraryController.java new file mode 100644 index 0000000..b8e2b4d --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/controller/LibraryController.java @@ -0,0 +1,66 @@ +package com.zl.mjga.controller; + +import com.zl.mjga.dto.library.LibraryDocUpdateDto; +import com.zl.mjga.dto.library.LibraryUpsertDto; +import com.zl.mjga.repository.LibraryDocRepository; +import com.zl.mjga.repository.LibraryRepository; +import com.zl.mjga.service.RagService; +import com.zl.mjga.service.UploadService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.jooq.generated.mjga.tables.pojos.Library; +import org.jooq.generated.mjga.tables.pojos.LibraryDoc; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +@RestController +@RequestMapping("/library") +@RequiredArgsConstructor +@Slf4j +public class LibraryController { + + private final UploadService uploadService; + private final RagService ragService; + private final LibraryRepository libraryRepository; + private final LibraryDocRepository libraryDocRepository; + + @PostMapping("/upsert") + public void upsertLibrary(@RequestBody @Valid LibraryUpsertDto libraryUpsertDto) { + Library library = new Library(); + library.setId(libraryUpsertDto.id()); + library.setName(libraryUpsertDto.name()); + libraryRepository.merge(library); + } + + @DeleteMapping + public void deleteLibrary(@RequestParam Long libraryId) { + ragService.deleteLibraryBy(libraryId); + } + + @DeleteMapping("/doc") + public void deleteLibraryDoc(@RequestParam Long libraryDocId) { + ragService.deleteDocBy(libraryDocId); + } + + @PutMapping("/doc") + public void updateLibraryDoc(@RequestBody @Valid LibraryDocUpdateDto libraryDocUpdateDto) { + LibraryDoc libraryDoc = new LibraryDoc(); + libraryDoc.setId(libraryDocUpdateDto.id()); + libraryDoc.setEnable(libraryDocUpdateDto.enable()); + libraryDocRepository.merge(libraryDoc); + } + + @PostMapping( + value = "/upload", + consumes = MediaType.MULTIPART_FORM_DATA_VALUE, + produces = MediaType.TEXT_PLAIN_VALUE) + public String uploadLibraryDoc( + @RequestPart("libraryId") Long libraryId, @RequestPart("file") MultipartFile multipartFile) + throws Exception { + String objectName = uploadService.uploadLibraryDoc(multipartFile); + ragService.ingestDocumentBy(libraryId, objectName, multipartFile.getOriginalFilename()); + return objectName; + } +} diff --git a/backend/src/main/java/com/zl/mjga/dto/library/LibraryDocUpdateDto.java b/backend/src/main/java/com/zl/mjga/dto/library/LibraryDocUpdateDto.java new file mode 100644 index 0000000..18a59cb --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/dto/library/LibraryDocUpdateDto.java @@ -0,0 +1,6 @@ +package com.zl.mjga.dto.library; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +public record LibraryDocUpdateDto(@NotNull Long id, @NotEmpty Boolean enable) {} diff --git a/backend/src/main/java/com/zl/mjga/dto/library/LibraryUpsertDto.java b/backend/src/main/java/com/zl/mjga/dto/library/LibraryUpsertDto.java new file mode 100644 index 0000000..67636dc --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/dto/library/LibraryUpsertDto.java @@ -0,0 +1,5 @@ +package com.zl.mjga.dto.library; + +import jakarta.validation.constraints.NotEmpty; + +public record LibraryUpsertDto(Long id, @NotEmpty String name) {} diff --git a/backend/src/main/java/com/zl/mjga/repository/LibraryDocRepository.java b/backend/src/main/java/com/zl/mjga/repository/LibraryDocRepository.java new file mode 100644 index 0000000..8e7deb5 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/repository/LibraryDocRepository.java @@ -0,0 +1,14 @@ +package com.zl.mjga.repository; + +import org.jooq.Configuration; +import org.jooq.generated.mjga.tables.daos.LibraryDocDao; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; + +@Repository +public class LibraryDocRepository extends LibraryDocDao { + @Autowired + public LibraryDocRepository(Configuration configuration) { + super(configuration); + } +} diff --git a/backend/src/main/java/com/zl/mjga/repository/LibraryDocSegmentRepository.java b/backend/src/main/java/com/zl/mjga/repository/LibraryDocSegmentRepository.java new file mode 100644 index 0000000..1afd329 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/repository/LibraryDocSegmentRepository.java @@ -0,0 +1,14 @@ +package com.zl.mjga.repository; + +import org.jooq.Configuration; +import org.jooq.generated.mjga.tables.daos.LibraryDocSegmentDao; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; + +@Repository +public class LibraryDocSegmentRepository extends LibraryDocSegmentDao { + @Autowired + public LibraryDocSegmentRepository(Configuration configuration) { + super(configuration); + } +} diff --git a/backend/src/main/java/com/zl/mjga/repository/LibraryRepository.java b/backend/src/main/java/com/zl/mjga/repository/LibraryRepository.java new file mode 100644 index 0000000..31e039d --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/repository/LibraryRepository.java @@ -0,0 +1,15 @@ +package com.zl.mjga.repository; + +import org.jooq.Configuration; +import org.jooq.generated.mjga.tables.daos.LibraryDao; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; + +@Repository +public class LibraryRepository extends LibraryDao { + + @Autowired + public LibraryRepository(Configuration configuration) { + super(configuration); + } +} diff --git a/backend/src/main/java/com/zl/mjga/service/EmbeddingService.java b/backend/src/main/java/com/zl/mjga/service/EmbeddingService.java deleted file mode 100644 index 8c9eaaa..0000000 --- a/backend/src/main/java/com/zl/mjga/service/EmbeddingService.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.zl.mjga.service; - -import static dev.langchain4j.store.embedding.filter.MetadataFilterBuilder.metadataKey; - -import com.zl.mjga.config.ai.ZhiPuEmbeddingModelConfig; -import com.zl.mjga.config.minio.MinIoConfig; -import com.zl.mjga.model.urp.Actions; -import dev.langchain4j.data.document.Document; -import dev.langchain4j.data.document.Metadata; -import dev.langchain4j.data.document.loader.amazon.s3.AmazonS3DocumentLoader; -import dev.langchain4j.data.document.parser.apache.tika.ApacheTikaDocumentParser; -import dev.langchain4j.data.embedding.Embedding; -import dev.langchain4j.data.segment.TextSegment; -import dev.langchain4j.model.embedding.EmbeddingModel; -import dev.langchain4j.store.embedding.*; -import dev.langchain4j.store.embedding.filter.Filter; -import io.minio.errors.*; -import jakarta.annotation.PostConstruct; -import java.util.HashMap; -import java.util.Map; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.context.annotation.Configuration; -import org.springframework.stereotype.Service; - -@Configuration -@RequiredArgsConstructor -@Service -@Slf4j -public class EmbeddingService { - - private final EmbeddingModel zhipuEmbeddingModel; - - private final EmbeddingStore zhiPuEmbeddingStore; - - private final ZhiPuEmbeddingModelConfig zhiPuEmbeddingModelConfig; - - private final AmazonS3DocumentLoader amazonS3DocumentLoader; - - private final EmbeddingStoreIngestor zhiPuEmbeddingStoreIngestor; - - private final MinIoConfig minIoConfig; - - public void ingestDocument(String objectName) { - Document document = - amazonS3DocumentLoader.loadDocument( - minIoConfig.getDefaultBucket(), objectName, new ApacheTikaDocumentParser()); - IngestionResult ingest = zhiPuEmbeddingStoreIngestor.ingest(document); - log.info("Ingest document finished {}", ingest); - } - - public Map searchAction(String message) { - Map result = new HashMap<>(); - EmbeddingSearchRequest embeddingSearchRequest = - EmbeddingSearchRequest.builder() - .queryEmbedding(zhipuEmbeddingModel.embed(message).content()) - .minScore(0.89) - .build(); - EmbeddingSearchResult embeddingSearchResult = - zhiPuEmbeddingStore.search(embeddingSearchRequest); - if (!embeddingSearchResult.matches().isEmpty()) { - Metadata metadata = embeddingSearchResult.matches().getFirst().embedded().metadata(); - result.put(Actions.INDEX_KEY, metadata.getString(Actions.INDEX_KEY)); - } - return result; - } - - @PostConstruct - public void initActionIndex() { - if (!zhiPuEmbeddingModelConfig.getEnable()) { - return; - } - for (Actions action : Actions.values()) { - Embedding queryEmbedding = zhipuEmbeddingModel.embed(action.getContent()).content(); - Filter createUserFilter = metadataKey(Actions.INDEX_KEY).isEqualTo(action.getCode()); - EmbeddingSearchRequest embeddingSearchRequest = - EmbeddingSearchRequest.builder() - .queryEmbedding(queryEmbedding) - .filter(createUserFilter) - .build(); - EmbeddingSearchResult embeddingSearchResult = - zhiPuEmbeddingStore.search(embeddingSearchRequest); - if (embeddingSearchResult.matches().isEmpty()) { - TextSegment segment = - TextSegment.from( - action.getContent(), Metadata.metadata(Actions.INDEX_KEY, action.getCode())); - Embedding embedding = zhipuEmbeddingModel.embed(segment).content(); - zhiPuEmbeddingStore.add(embedding, segment); - } - } - } -} diff --git a/backend/src/main/java/com/zl/mjga/service/RagService.java b/backend/src/main/java/com/zl/mjga/service/RagService.java new file mode 100644 index 0000000..22eff95 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/service/RagService.java @@ -0,0 +1,189 @@ +package com.zl.mjga.service; + +import static dev.langchain4j.store.embedding.filter.MetadataFilterBuilder.metadataKey; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.zl.mjga.config.ai.ZhiPuEmbeddingModelConfig; +import com.zl.mjga.config.minio.MinIoConfig; +import com.zl.mjga.model.urp.Actions; +import com.zl.mjga.repository.LibraryDocRepository; +import com.zl.mjga.repository.LibraryRepository; +import dev.langchain4j.data.document.Document; +import dev.langchain4j.data.document.Metadata; +import dev.langchain4j.data.document.loader.amazon.s3.AmazonS3DocumentLoader; +import dev.langchain4j.data.document.parser.apache.tika.ApacheTikaDocumentParser; +import dev.langchain4j.data.document.splitter.DocumentByParagraphSplitter; +import dev.langchain4j.data.embedding.Embedding; +import dev.langchain4j.data.segment.TextSegment; +import dev.langchain4j.model.embedding.EmbeddingModel; +import dev.langchain4j.model.output.Response; +import dev.langchain4j.store.embedding.*; +import dev.langchain4j.store.embedding.filter.Filter; +import jakarta.annotation.PostConstruct; +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.RandomStringUtils; +import org.jooq.JSON; +import org.jooq.generated.mjga.tables.daos.LibraryDocSegmentDao; +import org.jooq.generated.mjga.tables.pojos.LibraryDoc; +import org.jooq.generated.mjga.tables.pojos.LibraryDocSegment; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Configuration +@RequiredArgsConstructor +@Service +@Slf4j +public class RagService { + + private final EmbeddingModel zhipuEmbeddingModel; + + private final EmbeddingStore zhiPuEmbeddingStore; + + private final ZhiPuEmbeddingModelConfig zhiPuEmbeddingModelConfig; + + private final AmazonS3DocumentLoader amazonS3DocumentLoader; + + private final MinIoConfig minIoConfig; + + private final LibraryRepository libraryRepository; + + private final LibraryDocRepository libraryDocRepository; + + private final LibraryDocSegmentDao libraryDocSegmentDao; + + public void deleteLibraryBy(Long libraryId) { + List libraryDocs = libraryDocRepository.fetchByLibId(libraryId); + List docIds = libraryDocs.stream().map(LibraryDoc::getId).toList(); + for (Long docId : docIds) { + deleteDocBy(docId); + } + libraryRepository.deleteById(libraryId); + } + + public void deleteDocBy(Long docId) { + List libraryDocSegments = libraryDocSegmentDao.fetchByDocId(docId); + List embeddingIdList = + libraryDocSegments.stream().map(LibraryDocSegment::getEmbeddingId).toList(); + if (CollectionUtils.isNotEmpty(embeddingIdList)) { + zhiPuEmbeddingStore.removeAll(embeddingIdList); + } + libraryDocRepository.deleteById(docId); + } + + @Transactional(rollbackFor = Throwable.class) + public void ingestDocumentBy(Long libraryId, String objectName, String originalName) + throws Exception { + Document document = + amazonS3DocumentLoader.loadDocument( + minIoConfig.getDefaultBucket(), objectName, new ApacheTikaDocumentParser()); + ArrayList embeddingIds = new ArrayList<>(); + try { + Long libraryDocId = createLibraryDoc(objectName, originalName, document.metadata().toMap()); + DocumentByParagraphSplitter documentByParagraphSplitter = + new DocumentByParagraphSplitter(1000, 200); + documentByParagraphSplitter + .split(document) + .forEach( + textSegment -> { + Metadata metadata = textSegment.metadata(); + metadata.put("libraryId", libraryId); + Response embed = zhipuEmbeddingModel.embed(textSegment); + Integer tokenUsage = embed.tokenUsage().totalTokenCount(); + Embedding vector = embed.content(); + String embeddingId = zhiPuEmbeddingStore.add(vector, textSegment); + embeddingIds.add(embeddingId); + createLibraryDocSegment(textSegment, libraryDocId, tokenUsage, embeddingId); + }); + } catch (Exception e) { + log.error( + "文档采集失败。libraryId {} objectName {} originalName {}", + libraryId, + objectName, + originalName, + e); + if (CollectionUtils.isNotEmpty(embeddingIds)) { + zhiPuEmbeddingStore.removeAll(embeddingIds); + } + throw e; + } + } + + private void createLibraryDocSegment( + TextSegment textSegment, Long libraryDocId, Integer tokenUsage, String embeddingId) { + LibraryDocSegment libraryDocSegment = new LibraryDocSegment(); + libraryDocSegment.setDocId(libraryDocId); + libraryDocSegment.setContent(textSegment.text()); + libraryDocSegment.setTokenUsage(tokenUsage); + libraryDocSegment.setEmbeddingId(embeddingId); + libraryDocSegmentDao.insert(); + } + + private Long createLibraryDoc(String objectName, String originalName, Map meta) + throws JsonProcessingException { + String identify = + String.format( + "%d%s_%s", + Instant.now().toEpochMilli(), + RandomStringUtils.insecure().nextAlphabetic(6), + originalName); + LibraryDoc libraryDoc = new LibraryDoc(); + ObjectMapper objectMapper = new ObjectMapper(); + String metaJson = objectMapper.writeValueAsString(meta); + libraryDoc.setMeta(JSON.valueOf(metaJson)); + libraryDoc.setPath(objectName); + libraryDoc.setName(originalName); + libraryDoc.setIdentify(identify); + libraryDocRepository.insert(libraryDoc); + return libraryDocRepository.fetchOneByIdentify(identify).getId(); + } + + public Map searchAction(String message) { + Map result = new HashMap<>(); + EmbeddingSearchRequest embeddingSearchRequest = + EmbeddingSearchRequest.builder() + .queryEmbedding(zhipuEmbeddingModel.embed(message).content()) + .minScore(0.89) + .build(); + EmbeddingSearchResult embeddingSearchResult = + zhiPuEmbeddingStore.search(embeddingSearchRequest); + if (!embeddingSearchResult.matches().isEmpty()) { + Metadata metadata = embeddingSearchResult.matches().getFirst().embedded().metadata(); + result.put(Actions.INDEX_KEY, metadata.getString(Actions.INDEX_KEY)); + } + return result; + } + + @PostConstruct + public void initActionIndex() { + if (!zhiPuEmbeddingModelConfig.getEnable()) { + return; + } + for (Actions action : Actions.values()) { + Embedding queryEmbedding = zhipuEmbeddingModel.embed(action.getContent()).content(); + Filter createUserFilter = metadataKey(Actions.INDEX_KEY).isEqualTo(action.getCode()); + EmbeddingSearchRequest embeddingSearchRequest = + EmbeddingSearchRequest.builder() + .queryEmbedding(queryEmbedding) + .filter(createUserFilter) + .build(); + EmbeddingSearchResult embeddingSearchResult = + zhiPuEmbeddingStore.search(embeddingSearchRequest); + if (embeddingSearchResult.matches().isEmpty()) { + TextSegment segment = + TextSegment.from( + action.getContent(), Metadata.metadata(Actions.INDEX_KEY, action.getCode())); + Embedding embedding = zhipuEmbeddingModel.embed(segment).content(); + zhiPuEmbeddingStore.add(embedding, segment); + } + } + } +} diff --git a/backend/src/main/java/com/zl/mjga/service/UploadService.java b/backend/src/main/java/com/zl/mjga/service/UploadService.java index 654dfad..2f01f6e 100644 --- a/backend/src/main/java/com/zl/mjga/service/UploadService.java +++ b/backend/src/main/java/com/zl/mjga/service/UploadService.java @@ -58,7 +58,7 @@ public class UploadService { return objectName; } - public String uploadLibraryFile(MultipartFile multipartFile) throws Exception { + public String uploadLibraryDoc(MultipartFile multipartFile) throws Exception { String originalFilename = multipartFile.getOriginalFilename(); if (StringUtils.isEmpty(originalFilename)) { throw new BusinessException("文件名不能为空"); diff --git a/backend/src/main/resources/db/migration/V1_0_0__init_table.sql b/backend/src/main/resources/db/migration/V1_0_0__init_table.sql index e76556f..2eb34b4 100644 --- a/backend/src/main/resources/db/migration/V1_0_0__init_table.sql +++ b/backend/src/main/resources/db/migration/V1_0_0__init_table.sql @@ -4,7 +4,7 @@ CREATE TABLE mjga.user ( id BIGSERIAL PRIMARY KEY, username VARCHAR NOT NULL UNIQUE, avatar VARCHAR, - create_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + create_time TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, password VARCHAR NOT NULL, enable BOOLEAN NOT NULL DEFAULT TRUE ); @@ -39,7 +39,7 @@ CREATE TABLE mjga.user_role_map ( CREATE TABLE mjga.department ( id BIGSERIAL PRIMARY KEY, - name VARCHAR(255) NOT NULL UNIQUE, + name VARCHAR NOT NULL UNIQUE, parent_id BIGINT, FOREIGN KEY (parent_id) REFERENCES mjga.department(id) @@ -56,7 +56,7 @@ CREATE TABLE mjga.user_department_map ( CREATE TABLE mjga.position ( id BIGSERIAL PRIMARY KEY, - name VARCHAR(255) NOT NULL UNIQUE + name VARCHAR NOT NULL UNIQUE ); CREATE TABLE mjga.user_position_map ( @@ -80,12 +80,12 @@ CREATE TYPE "llm_type_enum" AS ENUM ( CREATE TABLE mjga.ai_llm_config ( id BIGSERIAL NOT NULL UNIQUE, - name VARCHAR(255) NOT NULL UNIQUE, + name VARCHAR NOT NULL UNIQUE, code mjga.llm_code_enum NOT NULL UNIQUE, - model_name VARCHAR(255) NOT NULL, + model_name VARCHAR NOT NULL, type LLM_TYPE_ENUM NOT NULL, - api_key VARCHAR(255) NOT NULL, - url VARCHAR(255) NOT NULL, + api_key VARCHAR NOT NULL, + url VARCHAR NOT NULL, enable BOOLEAN NOT NULL DEFAULT true, priority SMALLINT NOT NULL DEFAULT 0, PRIMARY KEY(id) diff --git a/backend/src/main/resources/db/migration/V1_0_3__init_library.sql b/backend/src/main/resources/db/migration/V1_0_3__init_library.sql new file mode 100644 index 0000000..e1cdde8 --- /dev/null +++ b/backend/src/main/resources/db/migration/V1_0_3__init_library.sql @@ -0,0 +1,28 @@ +CREATE TABLE mjga.library ( + id BIGSERIAL PRIMARY KEY, + name VARCHAR NOT NULL UNIQUE, + data_count INTEGER NOT NULL DEFAULT 0, + create_time TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE mjga.library_doc ( + id BIGSERIAL PRIMARY KEY, + lib_id BIGINT NOT NULL, + name VARCHAR NOT NULL, + identify VARCHAR NOT NULL UNIQUE, + path VARCHAR NOT NULL, + meta JSON NOT NULL, + enable BOOLEAN NOT NULL DEFAULT true, + create_time TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMPTZ, + FOREIGN KEY (lib_id) REFERENCES mjga.library (id) ON DELETE CASCADE +); + +CREATE TABLE mjga.library_doc_segment ( + id BIGSERIAL PRIMARY KEY, + doc_id BIGINT NOT NULL, + embedding_id VARCHAR NOT NULL UNIQUE, + content TEXT, + token_usage INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (doc_id) REFERENCES mjga.library_doc (id) ON DELETE CASCADE +); \ No newline at end of file diff --git a/backend/src/test/resources/db/migration/test/V1_0_0__init_table.sql b/backend/src/test/resources/db/migration/test/V1_0_0__init_table.sql index a77b7de..daca81a 100644 --- a/backend/src/test/resources/db/migration/test/V1_0_0__init_table.sql +++ b/backend/src/test/resources/db/migration/test/V1_0_0__init_table.sql @@ -4,7 +4,7 @@ CREATE TABLE mjga.user ( id BIGSERIAL PRIMARY KEY, username VARCHAR NOT NULL UNIQUE, avatar VARCHAR, - create_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + create_time TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, password VARCHAR NOT NULL, enable BOOLEAN NOT NULL DEFAULT TRUE ); @@ -39,7 +39,7 @@ CREATE TABLE mjga.user_role_map ( CREATE TABLE mjga.department ( id BIGSERIAL PRIMARY KEY, - name VARCHAR(255) NOT NULL UNIQUE, + name VARCHAR NOT NULL UNIQUE, parent_id BIGINT, FOREIGN KEY (parent_id) REFERENCES mjga.department(id) @@ -56,7 +56,7 @@ CREATE TABLE mjga.user_department_map ( CREATE TABLE mjga.position ( id BIGSERIAL PRIMARY KEY, - name VARCHAR(255) NOT NULL UNIQUE + name VARCHAR NOT NULL UNIQUE ); CREATE TABLE mjga.user_position_map ( @@ -80,12 +80,12 @@ CREATE TYPE "llm_type_enum" AS ENUM ( CREATE TABLE mjga.ai_llm_config ( id BIGSERIAL NOT NULL UNIQUE, - name VARCHAR(255) NOT NULL UNIQUE, + name VARCHAR NOT NULL UNIQUE, code mjga.llm_code_enum NOT NULL UNIQUE, - model_name VARCHAR(255) NOT NULL, + model_name VARCHAR NOT NULL, type LLM_TYPE_ENUM NOT NULL, - api_key VARCHAR(255) NOT NULL, - url VARCHAR(255) NOT NULL, + api_key VARCHAR NOT NULL, + url VARCHAR NOT NULL, enable BOOLEAN NOT NULL DEFAULT true, priority SMALLINT NOT NULL DEFAULT 0, PRIMARY KEY(id) diff --git a/backend/src/test/resources/db/migration/test/V1_0_3__init_library.sql b/backend/src/test/resources/db/migration/test/V1_0_3__init_library.sql new file mode 100644 index 0000000..e1cdde8 --- /dev/null +++ b/backend/src/test/resources/db/migration/test/V1_0_3__init_library.sql @@ -0,0 +1,28 @@ +CREATE TABLE mjga.library ( + id BIGSERIAL PRIMARY KEY, + name VARCHAR NOT NULL UNIQUE, + data_count INTEGER NOT NULL DEFAULT 0, + create_time TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE mjga.library_doc ( + id BIGSERIAL PRIMARY KEY, + lib_id BIGINT NOT NULL, + name VARCHAR NOT NULL, + identify VARCHAR NOT NULL UNIQUE, + path VARCHAR NOT NULL, + meta JSON NOT NULL, + enable BOOLEAN NOT NULL DEFAULT true, + create_time TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMPTZ, + FOREIGN KEY (lib_id) REFERENCES mjga.library (id) ON DELETE CASCADE +); + +CREATE TABLE mjga.library_doc_segment ( + id BIGSERIAL PRIMARY KEY, + doc_id BIGINT NOT NULL, + embedding_id VARCHAR NOT NULL UNIQUE, + content TEXT, + token_usage INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (doc_id) REFERENCES mjga.library_doc (id) ON DELETE CASCADE +); \ No newline at end of file From 09f51fa91f807442c5cc365ada9e96326d471ed6 Mon Sep 17 00:00:00 2001 From: Chuck1sn Date: Thu, 26 Jun 2025 15:58:17 +0800 Subject: [PATCH 03/14] add desc --- .../src/main/java/com/zl/mjga/controller/LibraryController.java | 1 + .../src/main/java/com/zl/mjga/dto/library/LibraryUpsertDto.java | 2 +- .../src/main/resources/db/migration/V1_0_3__init_library.sql | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/com/zl/mjga/controller/LibraryController.java b/backend/src/main/java/com/zl/mjga/controller/LibraryController.java index b8e2b4d..5bcac8b 100644 --- a/backend/src/main/java/com/zl/mjga/controller/LibraryController.java +++ b/backend/src/main/java/com/zl/mjga/controller/LibraryController.java @@ -31,6 +31,7 @@ public class LibraryController { Library library = new Library(); library.setId(libraryUpsertDto.id()); library.setName(libraryUpsertDto.name()); + library.setDesc(libraryUpsertDto.desc()); libraryRepository.merge(library); } diff --git a/backend/src/main/java/com/zl/mjga/dto/library/LibraryUpsertDto.java b/backend/src/main/java/com/zl/mjga/dto/library/LibraryUpsertDto.java index 67636dc..e5c88f6 100644 --- a/backend/src/main/java/com/zl/mjga/dto/library/LibraryUpsertDto.java +++ b/backend/src/main/java/com/zl/mjga/dto/library/LibraryUpsertDto.java @@ -2,4 +2,4 @@ package com.zl.mjga.dto.library; import jakarta.validation.constraints.NotEmpty; -public record LibraryUpsertDto(Long id, @NotEmpty String name) {} +public record LibraryUpsertDto(Long id, @NotEmpty String name, String desc) {} diff --git a/backend/src/main/resources/db/migration/V1_0_3__init_library.sql b/backend/src/main/resources/db/migration/V1_0_3__init_library.sql index e1cdde8..a56cb32 100644 --- a/backend/src/main/resources/db/migration/V1_0_3__init_library.sql +++ b/backend/src/main/resources/db/migration/V1_0_3__init_library.sql @@ -1,6 +1,7 @@ CREATE TABLE mjga.library ( id BIGSERIAL PRIMARY KEY, name VARCHAR NOT NULL UNIQUE, + desc VARCHAR, data_count INTEGER NOT NULL DEFAULT 0, create_time TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP ); From cbfbd6c5dd58773b41ba48f09b7e310e58480d66 Mon Sep 17 00:00:00 2001 From: Chuck1sn Date: Thu, 26 Jun 2025 15:59:56 +0800 Subject: [PATCH 04/14] remove data_count --- backend/src/main/resources/db/migration/V1_0_3__init_library.sql | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/main/resources/db/migration/V1_0_3__init_library.sql b/backend/src/main/resources/db/migration/V1_0_3__init_library.sql index a56cb32..fb190cb 100644 --- a/backend/src/main/resources/db/migration/V1_0_3__init_library.sql +++ b/backend/src/main/resources/db/migration/V1_0_3__init_library.sql @@ -2,7 +2,6 @@ CREATE TABLE mjga.library ( id BIGSERIAL PRIMARY KEY, name VARCHAR NOT NULL UNIQUE, desc VARCHAR, - data_count INTEGER NOT NULL DEFAULT 0, create_time TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP ); From 19090b9c94eb609ef82eeac91c0f0b5ffbc59133 Mon Sep 17 00:00:00 2001 From: Chuck1sn Date: Thu, 26 Jun 2025 18:03:00 +0800 Subject: [PATCH 05/14] add async --- .../java/com/zl/mjga/ApplicationService.java | 2 + .../zl/mjga/controller/LibraryController.java | 4 +- .../java/com/zl/mjga/service/RagService.java | 86 ++++++++----------- .../db/migration/V1_0_3__init_library.sql | 6 ++ 4 files changed, 47 insertions(+), 51 deletions(-) diff --git a/backend/src/main/java/com/zl/mjga/ApplicationService.java b/backend/src/main/java/com/zl/mjga/ApplicationService.java index 4c28dd7..7a97b28 100644 --- a/backend/src/main/java/com/zl/mjga/ApplicationService.java +++ b/backend/src/main/java/com/zl/mjga/ApplicationService.java @@ -2,7 +2,9 @@ package com.zl.mjga; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableAsync; +@EnableAsync @SpringBootApplication(scanBasePackages = {"com.zl.mjga", "org.jooq.generated"}) public class ApplicationService { diff --git a/backend/src/main/java/com/zl/mjga/controller/LibraryController.java b/backend/src/main/java/com/zl/mjga/controller/LibraryController.java index 5bcac8b..2dbc89a 100644 --- a/backend/src/main/java/com/zl/mjga/controller/LibraryController.java +++ b/backend/src/main/java/com/zl/mjga/controller/LibraryController.java @@ -61,7 +61,9 @@ public class LibraryController { @RequestPart("libraryId") Long libraryId, @RequestPart("file") MultipartFile multipartFile) throws Exception { String objectName = uploadService.uploadLibraryDoc(multipartFile); - ragService.ingestDocumentBy(libraryId, objectName, multipartFile.getOriginalFilename()); + Long libraryDocId = + ragService.createLibraryDocBy(libraryId, objectName, multipartFile.getOriginalFilename()); + ragService.embeddingAndCreateDocSegment(libraryDocId, objectName); return objectName; } } diff --git a/backend/src/main/java/com/zl/mjga/service/RagService.java b/backend/src/main/java/com/zl/mjga/service/RagService.java index 22eff95..fd0ea2b 100644 --- a/backend/src/main/java/com/zl/mjga/service/RagService.java +++ b/backend/src/main/java/com/zl/mjga/service/RagService.java @@ -31,12 +31,14 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.RandomStringUtils; import org.jooq.JSON; +import org.jooq.generated.mjga.enums.LibraryDocStatusEnum; import org.jooq.generated.mjga.tables.daos.LibraryDocSegmentDao; import org.jooq.generated.mjga.tables.pojos.LibraryDoc; import org.jooq.generated.mjga.tables.pojos.LibraryDocSegment; import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.Async; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; @Configuration @RequiredArgsConstructor @@ -79,62 +81,17 @@ public class RagService { libraryDocRepository.deleteById(docId); } - @Transactional(rollbackFor = Throwable.class) - public void ingestDocumentBy(Long libraryId, String objectName, String originalName) - throws Exception { - Document document = - amazonS3DocumentLoader.loadDocument( - minIoConfig.getDefaultBucket(), objectName, new ApacheTikaDocumentParser()); - ArrayList embeddingIds = new ArrayList<>(); - try { - Long libraryDocId = createLibraryDoc(objectName, originalName, document.metadata().toMap()); - DocumentByParagraphSplitter documentByParagraphSplitter = - new DocumentByParagraphSplitter(1000, 200); - documentByParagraphSplitter - .split(document) - .forEach( - textSegment -> { - Metadata metadata = textSegment.metadata(); - metadata.put("libraryId", libraryId); - Response embed = zhipuEmbeddingModel.embed(textSegment); - Integer tokenUsage = embed.tokenUsage().totalTokenCount(); - Embedding vector = embed.content(); - String embeddingId = zhiPuEmbeddingStore.add(vector, textSegment); - embeddingIds.add(embeddingId); - createLibraryDocSegment(textSegment, libraryDocId, tokenUsage, embeddingId); - }); - } catch (Exception e) { - log.error( - "文档采集失败。libraryId {} objectName {} originalName {}", - libraryId, - objectName, - originalName, - e); - if (CollectionUtils.isNotEmpty(embeddingIds)) { - zhiPuEmbeddingStore.removeAll(embeddingIds); - } - throw e; - } - } - - private void createLibraryDocSegment( - TextSegment textSegment, Long libraryDocId, Integer tokenUsage, String embeddingId) { - LibraryDocSegment libraryDocSegment = new LibraryDocSegment(); - libraryDocSegment.setDocId(libraryDocId); - libraryDocSegment.setContent(textSegment.text()); - libraryDocSegment.setTokenUsage(tokenUsage); - libraryDocSegment.setEmbeddingId(embeddingId); - libraryDocSegmentDao.insert(); - } - - private Long createLibraryDoc(String objectName, String originalName, Map meta) + public Long createLibraryDocBy(Long libraryId, String objectName, String originalName) throws JsonProcessingException { + String username = SecurityContextHolder.getContext().getAuthentication().getName(); String identify = String.format( "%d%s_%s", Instant.now().toEpochMilli(), RandomStringUtils.insecure().nextAlphabetic(6), originalName); + Map meta = new HashMap<>(); + meta.put("uploader", username); LibraryDoc libraryDoc = new LibraryDoc(); ObjectMapper objectMapper = new ObjectMapper(); String metaJson = objectMapper.writeValueAsString(meta); @@ -142,10 +99,39 @@ public class RagService { libraryDoc.setPath(objectName); libraryDoc.setName(originalName); libraryDoc.setIdentify(identify); + libraryDoc.setLibId(libraryId); + libraryDoc.setStatus(LibraryDocStatusEnum.INDEXING); + libraryDoc.setEnable(Boolean.TRUE); libraryDocRepository.insert(libraryDoc); return libraryDocRepository.fetchOneByIdentify(identify).getId(); } + @Async + public void embeddingAndCreateDocSegment(Long libraryDocId, String objectName) { + Document document = + amazonS3DocumentLoader.loadDocument( + minIoConfig.getDefaultBucket(), objectName, new ApacheTikaDocumentParser()); + List libraryDocSegments = new ArrayList<>(); + DocumentByParagraphSplitter documentByParagraphSplitter = + new DocumentByParagraphSplitter(1000, 200); + documentByParagraphSplitter + .split(document) + .forEach( + textSegment -> { + Response embed = zhipuEmbeddingModel.embed(textSegment); + Integer tokenUsage = embed.tokenUsage().totalTokenCount(); + Embedding vector = embed.content(); + String embeddingId = zhiPuEmbeddingStore.add(vector, textSegment); + LibraryDocSegment libraryDocSegment = new LibraryDocSegment(); + libraryDocSegment.setEmbeddingId(embeddingId); + libraryDocSegment.setContent(textSegment.text()); + libraryDocSegment.setTokenUsage(tokenUsage); + libraryDocSegment.setDocId(libraryDocId); + libraryDocSegments.add(libraryDocSegment); + }); + libraryDocSegmentDao.insert(libraryDocSegments); + } + public Map searchAction(String message) { Map result = new HashMap<>(); EmbeddingSearchRequest embeddingSearchRequest = diff --git a/backend/src/main/resources/db/migration/V1_0_3__init_library.sql b/backend/src/main/resources/db/migration/V1_0_3__init_library.sql index fb190cb..9a2ffd3 100644 --- a/backend/src/main/resources/db/migration/V1_0_3__init_library.sql +++ b/backend/src/main/resources/db/migration/V1_0_3__init_library.sql @@ -5,6 +5,11 @@ CREATE TABLE mjga.library ( create_time TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP ); +CREATE TYPE mjga.library_doc_status_enum AS ENUM ( + 'SUCCESS', + 'INDEXING' +); + CREATE TABLE mjga.library_doc ( id BIGSERIAL PRIMARY KEY, lib_id BIGINT NOT NULL, @@ -13,6 +18,7 @@ CREATE TABLE mjga.library_doc ( path VARCHAR NOT NULL, meta JSON NOT NULL, enable BOOLEAN NOT NULL DEFAULT true, + status mjga.library_doc_status_enum NOT NULL, create_time TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time TIMESTAMPTZ, FOREIGN KEY (lib_id) REFERENCES mjga.library (id) ON DELETE CASCADE From 2f7259ca9db0ec0692c8f9060ddaecb393df7b88 Mon Sep 17 00:00:00 2001 From: Chuck1sn Date: Thu, 26 Jun 2025 19:27:51 +0800 Subject: [PATCH 06/14] fix endpoint --- .../zl/mjga/controller/LibraryController.java | 37 ++++++++++++++----- .../zl/mjga/dto/knowledge/DocUpdateDto.java | 6 +++ .../LibraryUpsertDto.java | 2 +- .../mjga/dto/library/LibraryDocUpdateDto.java | 6 --- 4 files changed, 35 insertions(+), 16 deletions(-) create mode 100644 backend/src/main/java/com/zl/mjga/dto/knowledge/DocUpdateDto.java rename backend/src/main/java/com/zl/mjga/dto/{library => knowledge}/LibraryUpsertDto.java (78%) delete mode 100644 backend/src/main/java/com/zl/mjga/dto/library/LibraryDocUpdateDto.java diff --git a/backend/src/main/java/com/zl/mjga/controller/LibraryController.java b/backend/src/main/java/com/zl/mjga/controller/LibraryController.java index 2dbc89a..4cfef93 100644 --- a/backend/src/main/java/com/zl/mjga/controller/LibraryController.java +++ b/backend/src/main/java/com/zl/mjga/controller/LibraryController.java @@ -1,22 +1,25 @@ package com.zl.mjga.controller; -import com.zl.mjga.dto.library.LibraryDocUpdateDto; -import com.zl.mjga.dto.library.LibraryUpsertDto; +import com.zl.mjga.dto.knowledge.DocUpdateDto; +import com.zl.mjga.dto.knowledge.LibraryUpsertDto; import com.zl.mjga.repository.LibraryDocRepository; +import com.zl.mjga.repository.LibraryDocSegmentRepository; import com.zl.mjga.repository.LibraryRepository; import com.zl.mjga.service.RagService; import com.zl.mjga.service.UploadService; import jakarta.validation.Valid; +import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.jooq.generated.mjga.tables.pojos.Library; import org.jooq.generated.mjga.tables.pojos.LibraryDoc; +import org.jooq.generated.mjga.tables.pojos.LibraryDocSegment; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @RestController -@RequestMapping("/library") +@RequestMapping("/knowledge") @RequiredArgsConstructor @Slf4j public class LibraryController { @@ -25,8 +28,24 @@ public class LibraryController { private final RagService ragService; private final LibraryRepository libraryRepository; private final LibraryDocRepository libraryDocRepository; + private final LibraryDocSegmentRepository libraryDocSegmentRepository; - @PostMapping("/upsert") + @GetMapping("/libraries") + public List queryLibraries() { + return libraryRepository.findAll(); + } + + @GetMapping("/docs") + public List queryLibraryDocs(@RequestParam Long libraryId) { + return libraryDocRepository.fetchByLibId(libraryId); + } + + @GetMapping("/segments") + public List queryLibraryDocSegments(@RequestParam Long libraryDocId) { + return libraryDocSegmentRepository.fetchByDocId(libraryDocId); + } + + @PostMapping("/library") public void upsertLibrary(@RequestBody @Valid LibraryUpsertDto libraryUpsertDto) { Library library = new Library(); library.setId(libraryUpsertDto.id()); @@ -35,7 +54,7 @@ public class LibraryController { libraryRepository.merge(library); } - @DeleteMapping + @DeleteMapping("/library") public void deleteLibrary(@RequestParam Long libraryId) { ragService.deleteLibraryBy(libraryId); } @@ -46,15 +65,15 @@ public class LibraryController { } @PutMapping("/doc") - public void updateLibraryDoc(@RequestBody @Valid LibraryDocUpdateDto libraryDocUpdateDto) { + public void updateLibraryDoc(@RequestBody @Valid DocUpdateDto docUpdateDto) { LibraryDoc libraryDoc = new LibraryDoc(); - libraryDoc.setId(libraryDocUpdateDto.id()); - libraryDoc.setEnable(libraryDocUpdateDto.enable()); + libraryDoc.setId(docUpdateDto.id()); + libraryDoc.setEnable(docUpdateDto.enable()); libraryDocRepository.merge(libraryDoc); } @PostMapping( - value = "/upload", + value = "/doc/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.TEXT_PLAIN_VALUE) public String uploadLibraryDoc( diff --git a/backend/src/main/java/com/zl/mjga/dto/knowledge/DocUpdateDto.java b/backend/src/main/java/com/zl/mjga/dto/knowledge/DocUpdateDto.java new file mode 100644 index 0000000..f89605d --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/dto/knowledge/DocUpdateDto.java @@ -0,0 +1,6 @@ +package com.zl.mjga.dto.knowledge; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +public record DocUpdateDto(@NotNull Long id, @NotEmpty Boolean enable) {} diff --git a/backend/src/main/java/com/zl/mjga/dto/library/LibraryUpsertDto.java b/backend/src/main/java/com/zl/mjga/dto/knowledge/LibraryUpsertDto.java similarity index 78% rename from backend/src/main/java/com/zl/mjga/dto/library/LibraryUpsertDto.java rename to backend/src/main/java/com/zl/mjga/dto/knowledge/LibraryUpsertDto.java index e5c88f6..3ddfd77 100644 --- a/backend/src/main/java/com/zl/mjga/dto/library/LibraryUpsertDto.java +++ b/backend/src/main/java/com/zl/mjga/dto/knowledge/LibraryUpsertDto.java @@ -1,4 +1,4 @@ -package com.zl.mjga.dto.library; +package com.zl.mjga.dto.knowledge; import jakarta.validation.constraints.NotEmpty; diff --git a/backend/src/main/java/com/zl/mjga/dto/library/LibraryDocUpdateDto.java b/backend/src/main/java/com/zl/mjga/dto/library/LibraryDocUpdateDto.java deleted file mode 100644 index 18a59cb..0000000 --- a/backend/src/main/java/com/zl/mjga/dto/library/LibraryDocUpdateDto.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.zl.mjga.dto.library; - -import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.NotNull; - -public record LibraryDocUpdateDto(@NotNull Long id, @NotEmpty Boolean enable) {} From 2fb08968eefc9d30ae5d11448f2e9190a20e45e0 Mon Sep 17 00:00:00 2001 From: Chuck1sn Date: Thu, 26 Jun 2025 19:55:53 +0800 Subject: [PATCH 07/14] description --- .../src/main/java/com/zl/mjga/controller/LibraryController.java | 2 +- .../main/java/com/zl/mjga/dto/knowledge/LibraryUpsertDto.java | 2 +- .../src/main/resources/db/migration/V1_0_3__init_library.sql | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/com/zl/mjga/controller/LibraryController.java b/backend/src/main/java/com/zl/mjga/controller/LibraryController.java index 4cfef93..14a34b7 100644 --- a/backend/src/main/java/com/zl/mjga/controller/LibraryController.java +++ b/backend/src/main/java/com/zl/mjga/controller/LibraryController.java @@ -50,7 +50,7 @@ public class LibraryController { Library library = new Library(); library.setId(libraryUpsertDto.id()); library.setName(libraryUpsertDto.name()); - library.setDesc(libraryUpsertDto.desc()); + library.setDescription(libraryUpsertDto.description()); libraryRepository.merge(library); } diff --git a/backend/src/main/java/com/zl/mjga/dto/knowledge/LibraryUpsertDto.java b/backend/src/main/java/com/zl/mjga/dto/knowledge/LibraryUpsertDto.java index 3ddfd77..f336a03 100644 --- a/backend/src/main/java/com/zl/mjga/dto/knowledge/LibraryUpsertDto.java +++ b/backend/src/main/java/com/zl/mjga/dto/knowledge/LibraryUpsertDto.java @@ -2,4 +2,4 @@ package com.zl.mjga.dto.knowledge; import jakarta.validation.constraints.NotEmpty; -public record LibraryUpsertDto(Long id, @NotEmpty String name, String desc) {} +public record LibraryUpsertDto(Long id, @NotEmpty String name, String description) {} diff --git a/backend/src/main/resources/db/migration/V1_0_3__init_library.sql b/backend/src/main/resources/db/migration/V1_0_3__init_library.sql index 9a2ffd3..166a832 100644 --- a/backend/src/main/resources/db/migration/V1_0_3__init_library.sql +++ b/backend/src/main/resources/db/migration/V1_0_3__init_library.sql @@ -1,7 +1,7 @@ CREATE TABLE mjga.library ( id BIGSERIAL PRIMARY KEY, name VARCHAR NOT NULL UNIQUE, - desc VARCHAR, + description VARCHAR, create_time TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP ); From 8ed0b795f3e6787757da34b11bc99a1bb7ef7c50 Mon Sep 17 00:00:00 2001 From: Chuck1sn Date: Fri, 27 Jun 2025 16:51:48 +0800 Subject: [PATCH 08/14] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E7=9F=A5=E8=AF=86?= =?UTF-8?q?=E5=BA=93=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD=EF=BC=8C=E5=8C=85?= =?UTF-8?q?=E6=8B=AC=E7=9F=A5=E8=AF=86=E5=BA=93=E5=92=8C=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E7=9A=84=E5=A2=9E=E5=88=A0=E6=94=B9=E6=9F=A5=EF=BC=8C=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E6=96=87=E6=A1=A3=E4=B8=8A=E4=BC=A0=E5=92=8C=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E7=AE=A1=E7=90=86=EF=BC=8C=E6=9B=B4=E6=96=B0=E7=9B=B8?= =?UTF-8?q?=E5=85=B3API=E6=8E=A5=E5=8F=A3=E5=92=8C=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=EF=BC=8C=E6=B7=BB=E5=8A=A0=E7=9F=A5=E8=AF=86?= =?UTF-8?q?=E5=BA=93=E5=92=8C=E6=96=87=E6=A1=A3=E7=9A=84=E8=A7=86=E5=9B=BE?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/build.gradle.kts | 5 +- .../com/zl/mjga/config/JacksonConfig.java | 35 ++ .../zl/mjga/controller/LibraryController.java | 24 +- .../zl/mjga/dto/knowledge/DocUpdateDto.java | 3 +- frontend/.gitignore | 2 + frontend/src/api/schema/openapi.json | 345 ++++++++++++++++++ frontend/src/api/types/schema.d.ts | 319 ++++++++++++++++ .../src/components/common/PromotionBanner.vue | 40 +- .../src/components/icons/KnowledgeIcon.vue | 7 + frontend/src/components/icons/index.ts | 1 + frontend/src/components/layout/Sidebar.vue | 6 + .../components/modals/LibraryFormDialog.vue | 93 +++++ .../src/components/modals/UserFormDialog.vue | 3 +- .../knowledge/useKnowledgeQuery.ts | 51 +++ .../knowledge/useKnowledgeUpsert.ts | 72 ++++ frontend/src/router/constants.ts | 35 +- frontend/src/router/guards.ts | 2 +- frontend/src/router/index.ts | 4 +- frontend/src/router/modules/ai.ts | 24 ++ frontend/src/router/modules/dashboard.ts | 4 +- frontend/src/types/KnowledgeTypes.ts | 35 ++ .../src/views/KnowledgeDocManagementPage.vue | 238 ++++++++++++ .../src/views/KnowledgeManagementPage.vue | 177 +++++++++ frontend/src/views/KnowledgeSegmentsPage.vue | 97 +++++ frontend/src/views/RoleManagementPage.vue | 2 +- 25 files changed, 1578 insertions(+), 46 deletions(-) create mode 100644 backend/src/main/java/com/zl/mjga/config/JacksonConfig.java create mode 100644 frontend/src/components/icons/KnowledgeIcon.vue create mode 100644 frontend/src/components/modals/LibraryFormDialog.vue create mode 100644 frontend/src/composables/knowledge/useKnowledgeQuery.ts create mode 100644 frontend/src/composables/knowledge/useKnowledgeUpsert.ts create mode 100644 frontend/src/types/KnowledgeTypes.ts create mode 100644 frontend/src/views/KnowledgeDocManagementPage.vue create mode 100644 frontend/src/views/KnowledgeManagementPage.vue create mode 100644 frontend/src/views/KnowledgeSegmentsPage.vue diff --git a/backend/build.gradle.kts b/backend/build.gradle.kts index 5b872e7..42e4f4b 100644 --- a/backend/build.gradle.kts +++ b/backend/build.gradle.kts @@ -170,9 +170,8 @@ jooq { } forcedTypes { forcedType { - name = "varchar" - includeExpression = ".*" - includeTypes = "INET" + isJsonConverter = true + includeTypes = "(?i:JSON|JSONB)" } } } diff --git a/backend/src/main/java/com/zl/mjga/config/JacksonConfig.java b/backend/src/main/java/com/zl/mjga/config/JacksonConfig.java new file mode 100644 index 0000000..80f4c7e --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/config/JacksonConfig.java @@ -0,0 +1,35 @@ +package com.zl.mjga.config; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import java.io.IOException; +import org.jooq.JSON; +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class JacksonConfig { + + @Bean + public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() { + return builder -> + builder + .serializationInclusion(JsonInclude.Include.USE_DEFAULTS) + .serializers(new JooqJsonSerializer()); + } + + private static class JooqJsonSerializer extends StdSerializer { + public JooqJsonSerializer() { + super(JSON.class); + } + + @Override + public void serialize(JSON value, JsonGenerator gen, SerializerProvider serializers) + throws IOException { + gen.writeRawValue(value.data()); + } + } +} diff --git a/backend/src/main/java/com/zl/mjga/controller/LibraryController.java b/backend/src/main/java/com/zl/mjga/controller/LibraryController.java index 14a34b7..618b9f6 100644 --- a/backend/src/main/java/com/zl/mjga/controller/LibraryController.java +++ b/backend/src/main/java/com/zl/mjga/controller/LibraryController.java @@ -8,6 +8,8 @@ import com.zl.mjga.repository.LibraryRepository; import com.zl.mjga.service.RagService; import com.zl.mjga.service.UploadService; import jakarta.validation.Valid; + +import java.util.Comparator; import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -37,7 +39,10 @@ public class LibraryController { @GetMapping("/docs") public List queryLibraryDocs(@RequestParam Long libraryId) { - return libraryDocRepository.fetchByLibId(libraryId); + List libraryDocs = libraryDocRepository.fetchByLibId(libraryId); + return libraryDocs.stream().sorted( + Comparator.comparing(LibraryDoc::getId).reversed() + ).toList(); } @GetMapping("/segments") @@ -66,22 +71,19 @@ public class LibraryController { @PutMapping("/doc") public void updateLibraryDoc(@RequestBody @Valid DocUpdateDto docUpdateDto) { - LibraryDoc libraryDoc = new LibraryDoc(); - libraryDoc.setId(docUpdateDto.id()); - libraryDoc.setEnable(docUpdateDto.enable()); - libraryDocRepository.merge(libraryDoc); + LibraryDoc exist = libraryDocRepository.fetchOneById(docUpdateDto.id()); + exist.setEnable(docUpdateDto.enable()); + libraryDocRepository.merge(exist); } - @PostMapping( - value = "/doc/upload", - consumes = MediaType.MULTIPART_FORM_DATA_VALUE, - produces = MediaType.TEXT_PLAIN_VALUE) + @PostMapping(value = "/doc/upload", produces = MediaType.TEXT_PLAIN_VALUE) public String uploadLibraryDoc( - @RequestPart("libraryId") Long libraryId, @RequestPart("file") MultipartFile multipartFile) + @RequestPart("libraryId") String libraryId, @RequestPart("file") MultipartFile multipartFile) throws Exception { String objectName = uploadService.uploadLibraryDoc(multipartFile); Long libraryDocId = - ragService.createLibraryDocBy(libraryId, objectName, multipartFile.getOriginalFilename()); + ragService.createLibraryDocBy( + Long.valueOf(libraryId), objectName, multipartFile.getOriginalFilename()); ragService.embeddingAndCreateDocSegment(libraryDocId, objectName); return objectName; } diff --git a/backend/src/main/java/com/zl/mjga/dto/knowledge/DocUpdateDto.java b/backend/src/main/java/com/zl/mjga/dto/knowledge/DocUpdateDto.java index f89605d..244d371 100644 --- a/backend/src/main/java/com/zl/mjga/dto/knowledge/DocUpdateDto.java +++ b/backend/src/main/java/com/zl/mjga/dto/knowledge/DocUpdateDto.java @@ -1,6 +1,5 @@ package com.zl.mjga.dto.knowledge; -import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; -public record DocUpdateDto(@NotNull Long id, @NotEmpty Boolean enable) {} +public record DocUpdateDto(@NotNull Long id, @NotNull Long libId, @NotNull Boolean enable) {} diff --git a/frontend/.gitignore b/frontend/.gitignore index 59847e1..4a3e8e6 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -186,3 +186,5 @@ compose.yaml Dockerfile Caddyfile start.sh + +.cursor diff --git a/frontend/src/api/schema/openapi.json b/frontend/src/api/schema/openapi.json index 0822105..8d18abd 100644 --- a/frontend/src/api/schema/openapi.json +++ b/frontend/src/api/schema/openapi.json @@ -44,6 +44,51 @@ } } }, + "/knowledge/doc": { + "put": { + "tags": [ + "library-controller" + ], + "operationId": "updateLibraryDoc", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DocUpdateDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "delete": { + "tags": [ + "library-controller" + ], + "operationId": "deleteLibraryDoc", + "parameters": [ + { + "name": "libraryDocId", + "in": "query", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, "/ai/llm": { "put": { "tags": [ @@ -192,6 +237,93 @@ } } }, + "/knowledge/library": { + "post": { + "tags": [ + "library-controller" + ], + "operationId": "upsertLibrary", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LibraryUpsertDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "delete": { + "tags": [ + "library-controller" + ], + "operationId": "deleteLibrary", + "parameters": [ + { + "name": "libraryId", + "in": "query", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/knowledge/doc/upload": { + "post": { + "tags": [ + "library-controller" + ], + "operationId": "uploadLibraryDoc", + "requestBody": { + "content": { + "application/json": { + "schema": { + "required": [ + "file", + "libraryId" + ], + "type": "object", + "properties": { + "libraryId": { + "type": "string" + }, + "file": { + "type": "string", + "format": "binary" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, "/iam/user": { "get": { "tags": [ @@ -963,6 +1095,97 @@ } } }, + "/knowledge/segments": { + "get": { + "tags": [ + "library-controller" + ], + "operationId": "queryLibraryDocSegments", + "parameters": [ + { + "name": "libraryDocId", + "in": "query", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LibraryDocSegment" + } + } + } + } + } + } + } + }, + "/knowledge/libraries": { + "get": { + "tags": [ + "library-controller" + ], + "operationId": "queryLibraries", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Library" + } + } + } + } + } + } + } + }, + "/knowledge/docs": { + "get": { + "tags": [ + "library-controller" + ], + "operationId": "queryLibraryDocs", + "parameters": [ + { + "name": "libraryId", + "in": "query", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LibraryDoc" + } + } + } + } + } + } + } + }, "/iam/users": { "get": { "tags": [ @@ -1354,6 +1577,22 @@ } } }, + "DocUpdateDto": { + "required": [ + "enable", + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "enable": { + "type": "boolean" + } + } + }, "LlmVm": { "required": [ "apiKey", @@ -1422,6 +1661,24 @@ } } }, + "LibraryUpsertDto": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + } + } + }, "UserUpsertDto": { "required": [ "enable", @@ -1789,6 +2046,94 @@ } } }, + "LibraryDocSegment": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "docId": { + "type": "integer", + "format": "int64" + }, + "embeddingId": { + "type": "string" + }, + "content": { + "type": "string" + }, + "tokenUsage": { + "type": "integer", + "format": "int32" + } + } + }, + "Library": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "createTime": { + "type": "string", + "format": "date-time" + } + } + }, + "JSON": { + "type": "object" + }, + "LibraryDoc": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "libId": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "identify": { + "type": "string" + }, + "path": { + "type": "string" + }, + "meta": { + "$ref": "#/components/schemas/JSON" + }, + "enable": { + "type": "boolean" + }, + "status": { + "type": "string", + "enum": [ + "SUCCESS", + "INDEXING" + ] + }, + "createTime": { + "type": "string", + "format": "date-time" + }, + "updateTime": { + "type": "string", + "format": "date-time" + } + } + }, "UserQueryDto": { "type": "object", "properties": { diff --git a/frontend/src/api/types/schema.d.ts b/frontend/src/api/types/schema.d.ts index 80fea2c..9a825b0 100644 --- a/frontend/src/api/types/schema.d.ts +++ b/frontend/src/api/types/schema.d.ts @@ -20,6 +20,22 @@ export interface paths { patch?: never; trace?: never; }; + "/knowledge/doc": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put: operations["updateLibraryDoc"]; + post?: never; + delete: operations["deleteLibraryDoc"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/ai/llm": { parameters: { query?: never; @@ -100,6 +116,38 @@ export interface paths { patch?: never; trace?: never; }; + "/knowledge/library": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["upsertLibrary"]; + delete: operations["deleteLibrary"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/knowledge/doc/upload": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["uploadLibraryDoc"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/iam/user": { parameters: { query?: never; @@ -484,6 +532,54 @@ export interface paths { patch?: never; trace?: never; }; + "/knowledge/segments": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["queryLibraryDocSegments"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/knowledge/libraries": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["queryLibraries"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/knowledge/docs": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["queryLibraryDocs"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/iam/users": { parameters: { query?: never; @@ -684,6 +780,11 @@ export interface components { name: string; group: string; }; + DocUpdateDto: { + /** Format: int64 */ + id: number; + enable: boolean; + }; LlmVm: { /** Format: int64 */ id: number; @@ -705,6 +806,12 @@ export interface components { id?: number; name?: string; }; + LibraryUpsertDto: { + /** Format: int64 */ + id?: number; + name: string; + description?: string; + }; UserUpsertDto: { /** Format: int64 */ id?: number; @@ -829,6 +936,42 @@ export interface components { name: string; isBound?: boolean; }; + LibraryDocSegment: { + /** Format: int64 */ + id?: number; + /** Format: int64 */ + docId?: number; + embeddingId?: string; + content?: string; + /** Format: int32 */ + tokenUsage?: number; + }; + Library: { + /** Format: int64 */ + id?: number; + name?: string; + description?: string; + /** Format: date-time */ + createTime?: string; + }; + JSON: Record; + LibraryDoc: { + /** Format: int64 */ + id?: number; + /** Format: int64 */ + libId?: number; + name?: string; + identify?: string; + path?: string; + meta?: components["schemas"]["JSON"]; + enable?: boolean; + /** @enum {string} */ + status?: "SUCCESS" | "INDEXING"; + /** Format: date-time */ + createTime?: string; + /** Format: date-time */ + updateTime?: string; + }; UserQueryDto: { username?: string; /** Format: date-time */ @@ -973,6 +1116,48 @@ export interface operations { }; }; }; + updateLibraryDoc: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["DocUpdateDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + deleteLibraryDoc: { + parameters: { + query: { + libraryDocId: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; updateLlm: { parameters: { query?: never; @@ -1105,6 +1290,76 @@ export interface operations { }; }; }; + upsertLibrary: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["LibraryUpsertDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + deleteLibrary: { + parameters: { + query: { + libraryId: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + uploadLibraryDoc: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": { + libraryId: string; + /** Format: binary */ + file: string; + }; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "text/plain": string; + }; + }; + }; + }; queryUserWithRolePermission: { parameters: { query: { @@ -1782,6 +2037,70 @@ export interface operations { }; }; }; + queryLibraryDocSegments: { + parameters: { + query: { + libraryDocId: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["LibraryDocSegment"][]; + }; + }; + }; + }; + queryLibraries: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["Library"][]; + }; + }; + }; + }; + queryLibraryDocs: { + parameters: { + query: { + libraryId: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["LibraryDoc"][]; + }; + }; + }; + }; queryUsers: { parameters: { query: { diff --git a/frontend/src/components/common/PromotionBanner.vue b/frontend/src/components/common/PromotionBanner.vue index 31e3e0d..e94e53c 100644 --- a/frontend/src/components/common/PromotionBanner.vue +++ b/frontend/src/components/common/PromotionBanner.vue @@ -21,25 +21,25 @@ diff --git a/frontend/src/components/icons/KnowledgeIcon.vue b/frontend/src/components/icons/KnowledgeIcon.vue new file mode 100644 index 0000000..18a9607 --- /dev/null +++ b/frontend/src/components/icons/KnowledgeIcon.vue @@ -0,0 +1,7 @@ + diff --git a/frontend/src/components/icons/index.ts b/frontend/src/components/icons/index.ts index fc23c88..e20180d 100644 --- a/frontend/src/components/icons/index.ts +++ b/frontend/src/components/icons/index.ts @@ -11,3 +11,4 @@ export { default as RoleIcon } from "./RoleIcon.vue"; export { default as SettingsIcon } from "./SettingsIcon.vue"; export { default as UsersIcon } from "./UsersIcon.vue"; export { default as PermissionIcon } from "./PermissionIcon.vue"; +export { default as KnowledgeIcon } from "./KnowledgeIcon.vue"; diff --git a/frontend/src/components/layout/Sidebar.vue b/frontend/src/components/layout/Sidebar.vue index 5514a17..dae2a9d 100644 --- a/frontend/src/components/layout/Sidebar.vue +++ b/frontend/src/components/layout/Sidebar.vue @@ -35,6 +35,7 @@ import { RouterLink, useRoute } from "vue-router"; import { DepartmentIcon, + KnowledgeIcon, LlmConfigIcon, PermissionIcon, PositionIcon, @@ -113,6 +114,11 @@ const menuItems = [ path: Routes.LLMCONFIGVIEW.fullPath(), icon: LlmConfigIcon, }, + { + title: "知识库管理", + path: Routes.KNOWLEDGEVIEW.fullPath(), + icon: KnowledgeIcon, + }, ]; const route = useRoute(); diff --git a/frontend/src/components/modals/LibraryFormDialog.vue b/frontend/src/components/modals/LibraryFormDialog.vue new file mode 100644 index 0000000..76ef4e0 --- /dev/null +++ b/frontend/src/components/modals/LibraryFormDialog.vue @@ -0,0 +1,93 @@ + + + diff --git a/frontend/src/components/modals/UserFormDialog.vue b/frontend/src/components/modals/UserFormDialog.vue index 9d2d989..d6aeb16 100644 --- a/frontend/src/components/modals/UserFormDialog.vue +++ b/frontend/src/components/modals/UserFormDialog.vue @@ -116,10 +116,9 @@ const handleFileChange = (event: Event) => { throw err; }, }); - } catch (error) { + } finally { (event.target as HTMLInputElement).value = ""; uploadLoading.value = false; - throw error; } }; diff --git a/frontend/src/composables/knowledge/useKnowledgeQuery.ts b/frontend/src/composables/knowledge/useKnowledgeQuery.ts new file mode 100644 index 0000000..674ba24 --- /dev/null +++ b/frontend/src/composables/knowledge/useKnowledgeQuery.ts @@ -0,0 +1,51 @@ +import client from "@/api/client"; +import type { + DocQueryParams, + Library, + LibraryDoc, + LibraryDocSegment, + SegmentQueryParams, +} from "@/types/KnowledgeTypes"; +import { ref } from "vue"; + +export const useKnowledgeQuery = () => { + const libraries = ref([]); + const docs = ref([]); + const segments = ref([]); + + const fetchLibraries = async () => { + const { data } = await client.GET("/knowledge/libraries", {}); + libraries.value = data || []; + }; + + const fetchLibraryDocs = async (params: DocQueryParams) => { + const { data } = await client.GET("/knowledge/docs", { + params: { + query: { + libraryId: params.libraryId, + }, + }, + }); + docs.value = data || []; + }; + + const fetchDocSegments = async (params: SegmentQueryParams) => { + const { data } = await client.GET("/knowledge/segments", { + params: { + query: { + libraryDocId: params.libraryDocId, + }, + }, + }); + segments.value = data || []; + }; + + return { + libraries, + fetchLibraries, + docs, + fetchLibraryDocs, + segments, + fetchDocSegments, + }; +}; diff --git a/frontend/src/composables/knowledge/useKnowledgeUpsert.ts b/frontend/src/composables/knowledge/useKnowledgeUpsert.ts new file mode 100644 index 0000000..f30c201 --- /dev/null +++ b/frontend/src/composables/knowledge/useKnowledgeUpsert.ts @@ -0,0 +1,72 @@ +import client from "@/api/client"; +import type { + DocUpdateModel, + LibraryUpsertModel, +} from "@/types/KnowledgeTypes"; + +export const useKnowledgeUpsert = () => { + const upsertLibrary = async (library: LibraryUpsertModel) => { + await client.POST("/knowledge/library", { + body: { + id: library.id, + name: library.name, + description: library.description, + }, + }); + }; + + const deleteLibrary = async (libraryId: number) => { + await client.DELETE("/knowledge/library", { + params: { + query: { + libraryId, + }, + }, + }); + }; + + const uploadDoc = async (libraryId: number, file: File) => { + await client.POST("/knowledge/doc/upload", { + body: { + libraryId: libraryId.toString(), + file: file as unknown as string, + }, + bodySerializer: (body) => { + const formData = new FormData(); + for (const [key, value] of Object.entries(body!)) { + formData.set(key, value as unknown as string); + } + return formData; + }, + parseAs: "text", + }); + }; + + const deleteDoc = async (libraryDocId: number) => { + await client.DELETE("/knowledge/doc", { + params: { + query: { + libraryDocId, + }, + }, + }); + }; + + const updateDoc = async (doc: DocUpdateModel) => { + await client.PUT("/knowledge/doc", { + body: { + id: doc.id, + libId: doc.libId, + enable: doc.enable, + }, + }); + }; + + return { + upsertLibrary, + deleteLibrary, + uploadDoc, + deleteDoc, + updateDoc, + }; +}; diff --git a/frontend/src/router/constants.ts b/frontend/src/router/constants.ts index ab23b80..d8ed54f 100644 --- a/frontend/src/router/constants.ts +++ b/frontend/src/router/constants.ts @@ -133,9 +133,9 @@ export const UserRoutes = { export const AiRoutes = { LLMCONFIGVIEW: { path: "llm/config", - name: "llm/config", + name: "llm-config", fullPath: () => `${BaseRoutes.DASHBOARD.path}/llm/config`, - withParams: () => ({ name: "llm/config" }), + withParams: () => ({ name: "llm-config" }), }, SCHEDULERVIEW: { path: "scheduler", @@ -143,6 +143,37 @@ export const AiRoutes = { fullPath: () => `${BaseRoutes.DASHBOARD.path}/scheduler`, withParams: () => ({ name: "scheduler" }), }, + KNOWLEDGEVIEW: { + path: "knowledge", + name: "knowledge", + fullPath: () => `${BaseRoutes.DASHBOARD.path}/knowledge`, + withParams: () => ({ name: "knowledge" }), + }, + KNOWLEDGEDOCVIEW: { + path: "knowledge/:libraryId", + name: "knowledge-docs", + fullPath: () => `${BaseRoutes.DASHBOARD.path}/knowledge/:libraryId`, + withParams: (params: T) => ({ + name: "knowledge-docs", + params: { libraryId: params.libraryId.toString() }, + }), + }, + KNOWLEDGESEGMENTSVIEW: { + path: "knowledge/:libraryId/:docId", + name: "knowledge-segments", + fullPath: () => `${BaseRoutes.DASHBOARD.path}/knowledge/:libraryId/:docId`, + withParams: < + T extends { libraryId: string | number; docId: string | number }, + >( + params: T, + ) => ({ + name: "knowledge-segments", + params: { + libraryId: params.libraryId.toString(), + docId: params.docId.toString(), + }, + }), + }, } as const; export const Routes = { diff --git a/frontend/src/router/guards.ts b/frontend/src/router/guards.ts index df43b55..8b56689 100644 --- a/frontend/src/router/guards.ts +++ b/frontend/src/router/guards.ts @@ -13,7 +13,7 @@ export const authGuard: NavigationGuard = (to) => { }; } if (to.path === Routes.LOGIN.path && userStore.user) { - return { path: `${Routes.DASHBOARD.path}/${Routes.USERVIEW.path}` }; + return { path: Routes.USERVIEW.fullPath() }; } }; diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index d96e956..c88f252 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -15,7 +15,7 @@ const routes: RouteRecordRaw[] = [ path: Routes.HOME.path, name: Routes.HOME.name, redirect: { - path: `${Routes.DASHBOARD.path}/${Routes.USERVIEW.path}`, + path: Routes.USERVIEW.fullPath(), }, }, ]; @@ -27,7 +27,7 @@ const router = createRouter({ router.onError((err) => { console.error("router err:", err); - router.push(Routes.USERVIEW.name); + router.push(Routes.USERVIEW.fullPath()); return false; }); diff --git a/frontend/src/router/modules/ai.ts b/frontend/src/router/modules/ai.ts index 0bce41d..e48f45b 100644 --- a/frontend/src/router/modules/ai.ts +++ b/frontend/src/router/modules/ai.ts @@ -11,6 +11,30 @@ const aiRoutes: RouteRecordRaw[] = [ hasPermission: EPermission.READ_LLM_CONFIG_PERMISSION, }, }, + { + path: Routes.KNOWLEDGEVIEW.path, + name: Routes.KNOWLEDGEVIEW.name, + component: () => import("@/views/KnowledgeManagementPage.vue"), + meta: { + requiresAuth: true, + }, + }, + { + path: Routes.KNOWLEDGEDOCVIEW.path, + name: Routes.KNOWLEDGEDOCVIEW.name, + component: () => import("@/views/KnowledgeDocManagementPage.vue"), + meta: { + requiresAuth: true, + }, + }, + { + path: Routes.KNOWLEDGESEGMENTSVIEW.path, + name: Routes.KNOWLEDGESEGMENTSVIEW.name, + component: () => import("@/views/KnowledgeSegmentsPage.vue"), + meta: { + requiresAuth: true, + }, + }, ]; export default aiRoutes; diff --git a/frontend/src/router/modules/dashboard.ts b/frontend/src/router/modules/dashboard.ts index ecb391d..6de228e 100644 --- a/frontend/src/router/modules/dashboard.ts +++ b/frontend/src/router/modules/dashboard.ts @@ -12,6 +12,8 @@ const dashboardRoutes: RouteRecordRaw = { requiresAuth: true, }, children: [ + ...userManagementRoutes, + ...aiRoutes, { path: Routes.OVERVIEW.path, name: Routes.OVERVIEW.name, @@ -28,8 +30,6 @@ const dashboardRoutes: RouteRecordRaw = { requiresAuth: true, }, }, - ...userManagementRoutes, - ...aiRoutes, { path: Routes.NOTFOUND.path, name: Routes.NOTFOUND.name, diff --git a/frontend/src/types/KnowledgeTypes.ts b/frontend/src/types/KnowledgeTypes.ts new file mode 100644 index 0000000..c28a3e9 --- /dev/null +++ b/frontend/src/types/KnowledgeTypes.ts @@ -0,0 +1,35 @@ +import type { components } from "@/api/types/schema"; + +export type Library = components["schemas"]["Library"]; +export type LibraryDoc = components["schemas"]["LibraryDoc"]; +export type LibraryDocSegment = components["schemas"]["LibraryDocSegment"]; + +export interface LibraryUpsertModel { + id?: number; + name: string; + description?: string; +} + +export interface DocUpdateModel { + id: number; + libId: number; + enable: boolean; +} + +export interface LibraryQueryParams { + page: number; + size: number; +} + +export interface DocQueryParams { + libraryId: number; +} + +export interface SegmentQueryParams { + libraryDocId: number; +} + +export enum DocStatus { + SUCCESS = "SUCCESS", + INDEXING = "INDEXING", +} diff --git a/frontend/src/views/KnowledgeDocManagementPage.vue b/frontend/src/views/KnowledgeDocManagementPage.vue new file mode 100644 index 0000000..de84fb3 --- /dev/null +++ b/frontend/src/views/KnowledgeDocManagementPage.vue @@ -0,0 +1,238 @@ + + + diff --git a/frontend/src/views/KnowledgeManagementPage.vue b/frontend/src/views/KnowledgeManagementPage.vue new file mode 100644 index 0000000..e417393 --- /dev/null +++ b/frontend/src/views/KnowledgeManagementPage.vue @@ -0,0 +1,177 @@ + + + diff --git a/frontend/src/views/KnowledgeSegmentsPage.vue b/frontend/src/views/KnowledgeSegmentsPage.vue new file mode 100644 index 0000000..b85bbf8 --- /dev/null +++ b/frontend/src/views/KnowledgeSegmentsPage.vue @@ -0,0 +1,97 @@ + + + diff --git a/frontend/src/views/RoleManagementPage.vue b/frontend/src/views/RoleManagementPage.vue index 1fa0805..d4ffb6a 100644 --- a/frontend/src/views/RoleManagementPage.vue +++ b/frontend/src/views/RoleManagementPage.vue @@ -1,6 +1,6 @@ diff --git a/frontend/src/components/common/index.ts b/frontend/src/components/common/index.ts new file mode 100644 index 0000000..081cca7 --- /dev/null +++ b/frontend/src/components/common/index.ts @@ -0,0 +1,4 @@ +import CardBase from "./CardBase.vue"; +import PromotionBanner from "./PromotionBanner.vue"; + +export { CardBase, PromotionBanner }; diff --git a/frontend/src/components/common/knowledge/KnowledgeDocCard.vue b/frontend/src/components/common/knowledge/KnowledgeDocCard.vue index 6a776a5..2e1f2f5 100644 --- a/frontend/src/components/common/knowledge/KnowledgeDocCard.vue +++ b/frontend/src/components/common/knowledge/KnowledgeDocCard.vue @@ -1,31 +1,26 @@ diff --git a/frontend/src/components/ui/index.ts b/frontend/src/components/ui/index.ts new file mode 100644 index 0000000..4bda237 --- /dev/null +++ b/frontend/src/components/ui/index.ts @@ -0,0 +1,7 @@ +import Alert from "./Alert.vue"; +import Avatar from "./Avatar.vue"; +import Button from "./Button.vue"; +import InputButton from "./InputButton.vue"; +import SortIcon from "./SortIcon.vue"; + +export { Alert, Avatar, Button, InputButton, SortIcon }; diff --git a/frontend/src/composables/knowledge/useKnowledgeQuery.ts b/frontend/src/composables/knowledge/useKnowledgeQuery.ts index 674ba24..cdad5d2 100644 --- a/frontend/src/composables/knowledge/useKnowledgeQuery.ts +++ b/frontend/src/composables/knowledge/useKnowledgeQuery.ts @@ -12,6 +12,7 @@ export const useKnowledgeQuery = () => { const libraries = ref([]); const docs = ref([]); const segments = ref([]); + const doc = ref(null); const fetchLibraries = async () => { const { data } = await client.GET("/knowledge/libraries", {}); @@ -29,11 +30,22 @@ export const useKnowledgeQuery = () => { docs.value = data || []; }; + const fetchDocById = async (docId: number) => { + const { data } = await client.GET("/knowledge/docs", { + params: { + query: {}, + }, + }); + if (data && Array.isArray(data)) { + doc.value = data.find((item) => item.id === docId) || null; + } + }; + const fetchDocSegments = async (params: SegmentQueryParams) => { const { data } = await client.GET("/knowledge/segments", { params: { query: { - libraryDocId: params.libraryDocId, + libraryDocId: params.libraryDocId || params.docId || 0, }, }, }); @@ -45,6 +57,8 @@ export const useKnowledgeQuery = () => { fetchLibraries, docs, fetchLibraryDocs, + doc, + fetchDocById, segments, fetchDocSegments, }; diff --git a/frontend/src/types/KnowledgeTypes.ts b/frontend/src/types/KnowledgeTypes.ts index c28a3e9..7fd7116 100644 --- a/frontend/src/types/KnowledgeTypes.ts +++ b/frontend/src/types/KnowledgeTypes.ts @@ -26,8 +26,9 @@ export interface DocQueryParams { } export interface SegmentQueryParams { - libraryDocId: number; -} + libraryDocId?: number; + docId?: number; + } export enum DocStatus { SUCCESS = "SUCCESS", diff --git a/frontend/src/views/KnowledgeDocManagementPage.vue b/frontend/src/views/KnowledgeDocManagementPage.vue index 1cf017d..478c58a 100644 --- a/frontend/src/views/KnowledgeDocManagementPage.vue +++ b/frontend/src/views/KnowledgeDocManagementPage.vue @@ -1,56 +1,53 @@ @@ -39,10 +38,9 @@
暂无知识库
- +
@@ -69,6 +67,7 @@ import { KnowledgeLibraryCard } from "@/components/common/knowledge"; import Breadcrumbs from "@/components/layout/Breadcrumbs.vue"; import ConfirmationDialog from "@/components/modals/ConfirmationDialog.vue"; import LibraryFormDialog from "@/components/modals/LibraryFormDialog.vue"; +import { Button } from "@/components/ui"; import { useKnowledgeQuery } from "@/composables/knowledge/useKnowledgeQuery"; import { useKnowledgeUpsert } from "@/composables/knowledge/useKnowledgeUpsert"; diff --git a/frontend/src/views/KnowledgeSegmentsPage.vue b/frontend/src/views/KnowledgeSegmentsPage.vue index 7e43214..91cf1fc 100644 --- a/frontend/src/views/KnowledgeSegmentsPage.vue +++ b/frontend/src/views/KnowledgeSegmentsPage.vue @@ -1,76 +1,78 @@ From 3e1d7e6fee57439662ff6f748ec26e71934d0099 Mon Sep 17 00:00:00 2001 From: Chuck1sn Date: Sat, 28 Jun 2025 09:16:36 +0800 Subject: [PATCH 13/14] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E8=A1=A8=E5=8D=95?= =?UTF-8?q?=E5=92=8C=E5=AF=B9=E8=AF=9D=E6=A1=86=E7=BB=84=E4=BB=B6=EF=BC=8C?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=A4=B4=E5=83=8F=E5=A4=84=E7=90=86=E9=80=BB?= =?UTF-8?q?=E8=BE=91=EF=BC=8C=E6=9B=B4=E6=96=B0=E7=9B=B8=E5=85=B3=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E5=87=BD=E6=95=B0=EF=BC=8C=E6=8F=90=E5=8D=87=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E7=95=8C=E9=9D=A2=E5=92=8C=E4=BA=A4=E4=BA=92=E4=BD=93?= =?UTF-8?q?=E9=AA=8C=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/form/index.ts | 7 ++++ frontend/src/components/modals/BaseDialog.vue | 8 +++- .../components/modals/ConfirmationDialog.vue | 20 +++++++--- frontend/src/components/modals/index.ts | 23 +++++++++++ .../src/components/tables/TableFilterForm.vue | 17 ++++---- frontend/src/components/tables/index.ts | 15 +++++++ frontend/src/components/ui/Avatar.vue | 40 ++++++++----------- frontend/src/composables/common/index.ts | 6 +++ frontend/src/utils/avatarUtil.ts | 15 ++++++- frontend/src/utils/index.ts | 4 ++ 10 files changed, 115 insertions(+), 40 deletions(-) create mode 100644 frontend/src/components/form/index.ts create mode 100644 frontend/src/components/modals/index.ts create mode 100644 frontend/src/components/tables/index.ts create mode 100644 frontend/src/composables/common/index.ts create mode 100644 frontend/src/utils/index.ts diff --git a/frontend/src/components/form/index.ts b/frontend/src/components/form/index.ts new file mode 100644 index 0000000..87a49e3 --- /dev/null +++ b/frontend/src/components/form/index.ts @@ -0,0 +1,7 @@ +import FormInput from './FormInput.vue'; +import FormSelect from './FormSelect.vue'; + +export { + FormInput, + FormSelect +}; diff --git a/frontend/src/components/modals/BaseDialog.vue b/frontend/src/components/modals/BaseDialog.vue index 88a1d37..a9c9dd4 100644 --- a/frontend/src/components/modals/BaseDialog.vue +++ b/frontend/src/components/modals/BaseDialog.vue @@ -29,7 +29,7 @@ import { Modal, initFlowbite } from "flowbite"; import { computed, onMounted } from "vue"; -export type ModalSize = "xs" | "sm" | "md" | "lg" | "xl"; +export type ModalSize = "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl" | "5xl" | "6xl" | "7xl"; const props = defineProps<{ /** 对话框标题 */ @@ -50,6 +50,12 @@ const maxWidthClass = computed(() => { md: "max-w-md", lg: "max-w-lg", xl: "max-w-xl", + "2xl": "max-w-2xl", + "3xl": "max-w-3xl", + "4xl": "max-w-4xl", + "5xl": "max-w-5xl", + "6xl": "max-w-6xl", + "7xl": "max-w-7xl", }; return sizes[props.size || "md"]; diff --git a/frontend/src/components/modals/ConfirmationDialog.vue b/frontend/src/components/modals/ConfirmationDialog.vue index 6fc0941..cb54176 100644 --- a/frontend/src/components/modals/ConfirmationDialog.vue +++ b/frontend/src/components/modals/ConfirmationDialog.vue @@ -9,25 +9,33 @@

{{ title }}

+

{{ content }}

- - + +
diff --git a/frontend/src/components/modals/index.ts b/frontend/src/components/modals/index.ts new file mode 100644 index 0000000..4ec950a --- /dev/null +++ b/frontend/src/components/modals/index.ts @@ -0,0 +1,23 @@ +import BaseDialog from "./BaseDialog.vue"; +import ConfirmationDialog from "./ConfirmationDialog.vue"; +import DepartmentFormDialog from "./DepartmentFormDialog.vue"; +import LibraryFormDialog from "./LibraryFormDialog.vue"; +import LlmFormDialog from "./LlmFormDialog.vue"; +import PermissionFormDialog from "./PermissionFormDialog.vue"; +import PositionFormDialog from "./PositionFormDialog.vue"; +import RoleFormDialog from "./RoleFormDialog.vue"; +import SchedulerFormDialog from "./SchedulerFormDialog.vue"; +import UserFormDialog from "./UserFormDialog.vue"; + +export { + BaseDialog, + ConfirmationDialog, + DepartmentFormDialog, + LibraryFormDialog, + LlmFormDialog, + PermissionFormDialog, + PositionFormDialog, + RoleFormDialog, + SchedulerFormDialog, + UserFormDialog, +}; diff --git a/frontend/src/components/tables/TableFilterForm.vue b/frontend/src/components/tables/TableFilterForm.vue index 9011a41..df01516 100644 --- a/frontend/src/components/tables/TableFilterForm.vue +++ b/frontend/src/components/tables/TableFilterForm.vue @@ -33,15 +33,15 @@ - + @@ -53,6 +53,7 @@ diff --git a/frontend/src/composables/common/index.ts b/frontend/src/composables/common/index.ts new file mode 100644 index 0000000..70868d1 --- /dev/null +++ b/frontend/src/composables/common/index.ts @@ -0,0 +1,6 @@ +import { useErrorHandling } from "./useErrorHandling"; +import { usePagination } from "./usePagination"; +import { useSorting } from "./useSorting"; +import { useStyleSystem } from "./useStyleSystem"; + +export { useErrorHandling, usePagination, useSorting, useStyleSystem }; diff --git a/frontend/src/utils/avatarUtil.ts b/frontend/src/utils/avatarUtil.ts index 75f3ec0..7aac218 100644 --- a/frontend/src/utils/avatarUtil.ts +++ b/frontend/src/utils/avatarUtil.ts @@ -1,5 +1,16 @@ -export const getUserAvatarUrl = (avatar?: string): string | undefined => { - if (avatar?.startsWith("/")) { +/** + * 获取用户头像URL + * @param avatar 头像路径 + * @returns 完整的头像URL或默认头像 + */ +export const getUserAvatarUrl = (avatar?: string): string => { + if (!avatar) { + return "/trump.jpg"; // 默认头像 + } + + if (avatar.startsWith("/")) { return `${import.meta.env.VITE_STATIC_URL}${avatar}`; } + + return avatar; // 如果已经是完整URL则直接返回 }; diff --git a/frontend/src/utils/index.ts b/frontend/src/utils/index.ts new file mode 100644 index 0000000..6ea2412 --- /dev/null +++ b/frontend/src/utils/index.ts @@ -0,0 +1,4 @@ +import { getUserAvatarUrl } from "./avatarUtil"; +import { dayjs, formatDate, formatDateString } from "./dateUtil"; + +export { getUserAvatarUrl, dayjs, formatDate, formatDateString }; From b6ecc929b00a4faa5bc3b5ac8eda6de086edcb4f Mon Sep 17 00:00:00 2001 From: ccmjga Date: Sat, 28 Jun 2025 22:31:20 +0800 Subject: [PATCH 14/14] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20ChatDto=20=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E4=BC=A0=E8=BE=93=E5=AF=B9=E8=B1=A1=EF=BC=8C=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E8=81=8A=E5=A4=A9=E6=8E=A5=E5=8F=A3=E4=BB=A5=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E7=9F=A5=E8=AF=86=E5=BA=93=E5=8A=9F=E8=83=BD=EF=BC=8C?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=81=8A=E5=A4=A9=E6=9C=8D=E5=8A=A1=E9=80=BB?= =?UTF-8?q?=E8=BE=91=EF=BC=8C=E8=B0=83=E6=95=B4=E5=89=8D=E7=AB=AF=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E4=BB=A5=E6=8F=90=E5=8D=87=E7=94=A8=E6=88=B7=E4=BD=93?= =?UTF-8?q?=E9=AA=8C=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mjga/config/ai/ChatModelInitializer.java | 7 + .../com/zl/mjga/controller/AiController.java | 5 +- .../zl/mjga/controller/LibraryController.java | 13 +- .../main/java/com/zl/mjga/dto/ai/ChatDto.java | 7 + .../java/com/zl/mjga/model/urp/ChatMode.java | 6 + .../com/zl/mjga/service/AiChatService.java | 15 +- .../com/zl/mjga/service/UploadService.java | 4 - frontend/src/api/schema/openapi.json | 32 ++++- frontend/src/api/types/schema.d.ts | 11 +- frontend/src/components/common/Assistant.vue | 84 ++++++------ frontend/src/components/common/CardBase.vue | 8 +- frontend/src/components/common/index.ts | 2 +- .../common/knowledge/KnowledgeDocCard.vue | 10 +- .../common/knowledge/KnowledgeLibraryCard.vue | 6 +- .../common/knowledge/KnowledgeStatusBadge.vue | 34 ++--- .../common/knowledge/SegmentCard.vue | 6 +- .../src/components/common/knowledge/index.ts | 2 +- frontend/src/components/form/index.ts | 9 +- frontend/src/components/modals/BaseDialog.vue | 13 +- frontend/src/components/modals/index.ts | 2 +- frontend/src/components/tables/index.ts | 2 +- frontend/src/components/ui/Avatar.vue | 2 +- frontend/src/components/ui/Button.vue | 128 +++++++++--------- frontend/src/components/ui/index.ts | 2 +- frontend/src/composables/ai/useAiChat.ts | 16 ++- frontend/src/composables/common/index.ts | 2 +- frontend/src/types/KnowledgeTypes.ts | 6 +- frontend/src/utils/index.ts | 2 +- .../src/views/KnowledgeDocManagementPage.vue | 2 +- frontend/src/views/KnowledgeSegmentsPage.vue | 6 +- 30 files changed, 268 insertions(+), 176 deletions(-) create mode 100644 backend/src/main/java/com/zl/mjga/dto/ai/ChatDto.java create mode 100644 backend/src/main/java/com/zl/mjga/model/urp/ChatMode.java diff --git a/backend/src/main/java/com/zl/mjga/config/ai/ChatModelInitializer.java b/backend/src/main/java/com/zl/mjga/config/ai/ChatModelInitializer.java index 4c9727d..0f14e2f 100644 --- a/backend/src/main/java/com/zl/mjga/config/ai/ChatModelInitializer.java +++ b/backend/src/main/java/com/zl/mjga/config/ai/ChatModelInitializer.java @@ -1,5 +1,7 @@ package com.zl.mjga.config.ai; +import static dev.langchain4j.store.embedding.filter.MetadataFilterBuilder.metadataKey; + import com.zl.mjga.component.PromptConfiguration; import com.zl.mjga.service.LlmService; import dev.langchain4j.community.model.zhipu.ZhipuAiStreamingChatModel; @@ -72,6 +74,11 @@ public class ChatModelInitializer { .embeddingModel(zhipuEmbeddingModel) .minScore(0.75) .maxResults(5) + .dynamicFilter( + query -> { + String libraryId = (String) query.metadata().chatMemoryId(); + return metadataKey("libraryId").isEqualTo(libraryId); + }) .build()) .build(); } diff --git a/backend/src/main/java/com/zl/mjga/controller/AiController.java b/backend/src/main/java/com/zl/mjga/controller/AiController.java index df74e09..b97dff0 100644 --- a/backend/src/main/java/com/zl/mjga/controller/AiController.java +++ b/backend/src/main/java/com/zl/mjga/controller/AiController.java @@ -2,6 +2,7 @@ package com.zl.mjga.controller; import com.zl.mjga.dto.PageRequestDto; import com.zl.mjga.dto.PageResponseDto; +import com.zl.mjga.dto.ai.ChatDto; import com.zl.mjga.dto.ai.LlmQueryDto; import com.zl.mjga.dto.ai.LlmVm; import com.zl.mjga.exception.BusinessException; @@ -72,9 +73,9 @@ public class AiController { } @PostMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE) - public Flux chat(Principal principal, @RequestBody String userMessage) { + public Flux chat(Principal principal, @RequestBody ChatDto chatDto) { Sinks.Many sink = Sinks.many().unicast().onBackpressureBuffer(); - TokenStream chat = aiChatService.chatPrecedenceLlmWith(principal.getName(), userMessage); + TokenStream chat = aiChatService.chat(principal.getName(), chatDto); chat.onPartialResponse( text -> sink.tryEmitNext( diff --git a/backend/src/main/java/com/zl/mjga/controller/LibraryController.java b/backend/src/main/java/com/zl/mjga/controller/LibraryController.java index 88d4da7..6ccbc9c 100644 --- a/backend/src/main/java/com/zl/mjga/controller/LibraryController.java +++ b/backend/src/main/java/com/zl/mjga/controller/LibraryController.java @@ -8,7 +8,6 @@ import com.zl.mjga.repository.LibraryRepository; import com.zl.mjga.service.RagService; import com.zl.mjga.service.UploadService; import jakarta.validation.Valid; - import java.util.Comparator; import java.util.List; import lombok.RequiredArgsConstructor; @@ -34,16 +33,16 @@ public class LibraryController { @GetMapping("/libraries") public List queryLibraries() { - return libraryRepository.findAll().stream().sorted( - Comparator.comparing(Library::getId).reversed() - ).toList(); + return libraryRepository.findAll().stream() + .sorted(Comparator.comparing(Library::getId).reversed()) + .toList(); } @GetMapping("/docs") public List queryLibraryDocs(@RequestParam Long libraryId) { - return libraryDocRepository.fetchByLibId(libraryId).stream().sorted( - Comparator.comparing(LibraryDoc::getId).reversed() - ).toList(); + return libraryDocRepository.fetchByLibId(libraryId).stream() + .sorted(Comparator.comparing(LibraryDoc::getId).reversed()) + .toList(); } @GetMapping("/segments") diff --git a/backend/src/main/java/com/zl/mjga/dto/ai/ChatDto.java b/backend/src/main/java/com/zl/mjga/dto/ai/ChatDto.java new file mode 100644 index 0000000..e2a0e13 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/dto/ai/ChatDto.java @@ -0,0 +1,7 @@ +package com.zl.mjga.dto.ai; + +import com.zl.mjga.model.urp.ChatMode; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +public record ChatDto(@NotNull ChatMode mode, Long libraryId, @NotEmpty String message) {} diff --git a/backend/src/main/java/com/zl/mjga/model/urp/ChatMode.java b/backend/src/main/java/com/zl/mjga/model/urp/ChatMode.java new file mode 100644 index 0000000..72184bf --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/model/urp/ChatMode.java @@ -0,0 +1,6 @@ +package com.zl.mjga.model.urp; + +public enum ChatMode { + NORMAL, + WITH_LIBRARY +} diff --git a/backend/src/main/java/com/zl/mjga/service/AiChatService.java b/backend/src/main/java/com/zl/mjga/service/AiChatService.java index 9ea1a5c..d7897cd 100644 --- a/backend/src/main/java/com/zl/mjga/service/AiChatService.java +++ b/backend/src/main/java/com/zl/mjga/service/AiChatService.java @@ -2,6 +2,7 @@ package com.zl.mjga.service; import com.zl.mjga.config.ai.AiChatAssistant; import com.zl.mjga.config.ai.SystemToolAssistant; +import com.zl.mjga.dto.ai.ChatDto; import com.zl.mjga.exception.BusinessException; import dev.langchain4j.service.TokenStream; import java.util.Optional; @@ -39,8 +40,20 @@ public class AiChatService { }; } - public TokenStream chatPrecedenceLlmWith(String sessionIdentifier, String userMessage) { + public TokenStream chat(String sessionIdentifier, ChatDto chatDto) { + return switch (chatDto.mode()) { + case NORMAL -> chatWithPrecedenceLlm(sessionIdentifier, chatDto); + case WITH_LIBRARY -> chatWithLibrary(chatDto.libraryId(), chatDto); + }; + } + + public TokenStream chatWithLibrary(Long libraryId, ChatDto chatDto) { + return zhiPuChatAssistant.chat(String.valueOf(libraryId), chatDto.message()); + } + + public TokenStream chatWithPrecedenceLlm(String sessionIdentifier, ChatDto chatDto) { LlmCodeEnum code = getPrecedenceLlmCode(); + String userMessage = chatDto.message(); return switch (code) { case ZHI_PU -> zhiPuChatAssistant.chat(sessionIdentifier, userMessage); case DEEP_SEEK -> deepSeekChatAssistant.chat(sessionIdentifier, userMessage); diff --git a/backend/src/main/java/com/zl/mjga/service/UploadService.java b/backend/src/main/java/com/zl/mjga/service/UploadService.java index 2f01f6e..7aa45f5 100644 --- a/backend/src/main/java/com/zl/mjga/service/UploadService.java +++ b/backend/src/main/java/com/zl/mjga/service/UploadService.java @@ -71,10 +71,6 @@ public class UploadService { if (size > 1024 * 1024) { throw new BusinessException("知识库文档大小不能超过1MB"); } - String contentType = multipartFile.getContentType(); - if (!StringUtils.startsWith(contentType, "text/")) { - throw new BusinessException("非法的上传文件"); - } minioClient.putObject( PutObjectArgs.builder().bucket(minIoConfig.getDefaultBucket()).object(objectName).stream( multipartFile.getInputStream(), size, -1) diff --git a/frontend/src/api/schema/openapi.json b/frontend/src/api/schema/openapi.json index 8d18abd..8dbaf0b 100644 --- a/frontend/src/api/schema/openapi.json +++ b/frontend/src/api/schema/openapi.json @@ -894,7 +894,7 @@ "content": { "application/json": { "schema": { - "type": "string" + "$ref": "#/components/schemas/ChatDto" } } }, @@ -1580,7 +1580,8 @@ "DocUpdateDto": { "required": [ "enable", - "id" + "id", + "libId" ], "type": "object", "properties": { @@ -1588,6 +1589,10 @@ "type": "integer", "format": "int64" }, + "libId": { + "type": "integer", + "format": "int64" + }, "enable": { "type": "boolean" } @@ -1868,6 +1873,29 @@ } } }, + "ChatDto": { + "required": [ + "message", + "mode" + ], + "type": "object", + "properties": { + "mode": { + "type": "string", + "enum": [ + "NORMAL", + "WITH_LIBRARY" + ] + }, + "libraryId": { + "type": "integer", + "format": "int64" + }, + "message": { + "type": "string" + } + } + }, "PageRequestDto": { "type": "object", "properties": { diff --git a/frontend/src/api/types/schema.d.ts b/frontend/src/api/types/schema.d.ts index 9a825b0..13459af 100644 --- a/frontend/src/api/types/schema.d.ts +++ b/frontend/src/api/types/schema.d.ts @@ -783,6 +783,8 @@ export interface components { DocUpdateDto: { /** Format: int64 */ id: number; + /** Format: int64 */ + libId: number; enable: boolean; }; LlmVm: { @@ -867,6 +869,13 @@ export interface components { username: string; password: string; }; + ChatDto: { + /** @enum {string} */ + mode: "NORMAL" | "WITH_LIBRARY"; + /** Format: int64 */ + libraryId?: number; + message: string; + }; PageRequestDto: { /** Format: int64 */ page?: number; @@ -1888,7 +1897,7 @@ export interface operations { }; requestBody: { content: { - "application/json": string; + "application/json": components["schemas"]["ChatDto"]; }; }; responses: { diff --git a/frontend/src/components/common/Assistant.vue b/frontend/src/components/common/Assistant.vue index 387469c..ba0a6b4 100644 --- a/frontend/src/components/common/Assistant.vue +++ b/frontend/src/components/common/Assistant.vue @@ -5,14 +5,18 @@
  • -
    + :class="['flex flex-col leading-1.5 p-4 border-gray-200 max-w-[calc(100%-40px)]', chatElement.isUser ? 'bg-blue-100 rounded-tl-xl rounded-bl-xl rounded-br-xl' : 'bg-gray-100 rounded-e-xl rounded-es-xl']">
    {{ chatElement.username }} + + {{ chatElement.libraryName }} +
    - +
    + + + +
    @@ -51,7 +65,7 @@ " required>
    -