diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/Ipsen.iml b/.idea/Ipsen.iml new file mode 100644 index 0000000..d6ebd48 --- /dev/null +++ b/.idea/Ipsen.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..e6ffd3e --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..dfc8d74 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..03d9549 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..712ab9d --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..1749431 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..aa7b182 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/backend/src/main/java/com/ipsen/medical/config/DataInitializer.java b/backend/src/main/java/com/ipsen/medical/config/DataInitializer.java new file mode 100644 index 0000000..b414185 --- /dev/null +++ b/backend/src/main/java/com/ipsen/medical/config/DataInitializer.java @@ -0,0 +1,52 @@ +package com.ipsen.medical.config; + +import com.ipsen.medical.entity.User; +import com.ipsen.medical.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.time.LocalDateTime; + +@Configuration +@RequiredArgsConstructor +public class DataInitializer { + + private final UserRepository userRepository; + + @Value("${APP_ADMIN_USERNAME:admin}") + private String defaultAdminUsername; + + @Value("${APP_ADMIN_PASSWORD:admin123}") + private String defaultAdminPassword; + + @Bean + public CommandLineRunner ensureDefaultAdminUser() { + return args -> { + User admin = userRepository.findByUsername(defaultAdminUsername).orElse(null); + if (admin == null) { + admin = new User(); + admin.setUsername(defaultAdminUsername); + admin.setFullName("系统管理员"); + admin.setEmail("admin@ipsen.com"); + admin.setRole(User.UserRole.ADMIN); + admin.setEnabled(true); + admin.setCreatedAt(LocalDateTime.now()); + admin.setPassword(defaultAdminPassword); + userRepository.save(admin); + } else { + // If password looks like a placeholder bcrypt string, reset to default plain text + String existing = admin.getPassword() == null ? "" : admin.getPassword(); + boolean looksBcrypt = existing.startsWith("$2a$") || existing.startsWith("$2b$") || existing.startsWith("$2y$"); + if (looksBcrypt) { + admin.setPassword(defaultAdminPassword); + userRepository.save(admin); + } + } + }; + } +} + + diff --git a/backend/src/main/java/com/ipsen/medical/config/SecurityConfig.java b/backend/src/main/java/com/ipsen/medical/config/SecurityConfig.java index 3c7ecc0..72f8a00 100644 --- a/backend/src/main/java/com/ipsen/medical/config/SecurityConfig.java +++ b/backend/src/main/java/com/ipsen/medical/config/SecurityConfig.java @@ -4,8 +4,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.cors.CorsConfiguration; @@ -15,6 +13,8 @@ import org.springframework.web.reactive.function.client.WebClient; import java.util.Arrays; +import com.ipsen.medical.security.PlainTextPasswordEncoder; + /** * Spring Security配置 */ @@ -27,14 +27,12 @@ public class SecurityConfig { http .cors(cors -> cors.configurationSource(corsConfigurationSource())) .csrf(csrf -> csrf.disable()) - .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(authz -> authz - // 允许所有请求 .anyRequest().permitAll() ) .httpBasic(httpBasic -> httpBasic.disable()) .formLogin(formLogin -> formLogin.disable()); - + return http.build(); } @@ -53,7 +51,7 @@ public class SecurityConfig { @Bean public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); + return new PlainTextPasswordEncoder(); } @Bean @@ -61,4 +59,5 @@ public class SecurityConfig { return WebClient.builder() .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(10 * 1024 * 1024)); // 10MB buffer } + } diff --git a/backend/src/main/java/com/ipsen/medical/controller/AuthController.java b/backend/src/main/java/com/ipsen/medical/controller/AuthController.java new file mode 100644 index 0000000..15f2108 --- /dev/null +++ b/backend/src/main/java/com/ipsen/medical/controller/AuthController.java @@ -0,0 +1,63 @@ +package com.ipsen.medical.controller; + +import com.ipsen.medical.dto.ApiResponse; +import com.ipsen.medical.entity.User; +import com.ipsen.medical.repository.UserRepository; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.Optional; + +@RestController +@RequestMapping("/auth") +@RequiredArgsConstructor +@CrossOrigin(origins = "*") +public class AuthController { + + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + + @PostMapping("/login") + public ResponseEntity> login(@RequestBody LoginRequest request) { + Optional optional = userRepository.findByUsername(request.getUsername()); + if (!optional.isPresent()) { + return ResponseEntity.status(401).body(ApiResponse.error("用户不存在")); + } + User user = optional.get(); + if (!Boolean.TRUE.equals(user.getEnabled())) { + return ResponseEntity.status(403).body(ApiResponse.error("用户已禁用")); + } + if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) { + return ResponseEntity.status(401).body(ApiResponse.error("用户名或密码错误")); + } + user.setLastLoginAt(LocalDateTime.now()); + userRepository.save(user); + + AuthResponse resp = new AuthResponse(); + resp.setToken(""); + resp.setUsername(user.getUsername()); + resp.setFullName(user.getFullName()); + resp.setRole(user.getRole().name()); + return ResponseEntity.ok(ApiResponse.success(resp)); + } + + @Data + public static class LoginRequest { + private String username; + private String password; + } + + @Data + public static class AuthResponse { + private String token; + private String username; + private String fullName; + private String role; + } +} + + diff --git a/backend/src/main/java/com/ipsen/medical/controller/DownloadTaskController.java b/backend/src/main/java/com/ipsen/medical/controller/DownloadTaskController.java index 3dc61a1..1c5dbfa 100644 --- a/backend/src/main/java/com/ipsen/medical/controller/DownloadTaskController.java +++ b/backend/src/main/java/com/ipsen/medical/controller/DownloadTaskController.java @@ -73,3 +73,7 @@ public class DownloadTaskController { } + + + + diff --git a/backend/src/main/java/com/ipsen/medical/controller/InquiryController.java b/backend/src/main/java/com/ipsen/medical/controller/InquiryController.java index 6af3ff1..8da99b9 100644 --- a/backend/src/main/java/com/ipsen/medical/controller/InquiryController.java +++ b/backend/src/main/java/com/ipsen/medical/controller/InquiryController.java @@ -205,6 +205,17 @@ public class InquiryController { InquiryRequestDTO result = autoSearchService.executeFullWorkflow(id); return ResponseEntity.ok(ApiResponse.success(result)); } + + /** + * 根据选中的检索结果生成下载任务 + */ + @PostMapping("/{id}/downloads") + public ResponseEntity> generateDownloadTasks( + @PathVariable Long id, + @RequestBody List searchResultItemIds) { + InquiryRequestDTO result = inquiryService.generateDownloadTasks(id, searchResultItemIds); + return ResponseEntity.ok(ApiResponse.success(result)); + } } diff --git a/backend/src/main/java/com/ipsen/medical/controller/SearchResultController.java b/backend/src/main/java/com/ipsen/medical/controller/SearchResultController.java index 63d3996..31d3aac 100644 --- a/backend/src/main/java/com/ipsen/medical/controller/SearchResultController.java +++ b/backend/src/main/java/com/ipsen/medical/controller/SearchResultController.java @@ -103,3 +103,7 @@ public class SearchResultController { } + + + + diff --git a/backend/src/main/java/com/ipsen/medical/dto/ClinicalTrialDTO.java b/backend/src/main/java/com/ipsen/medical/dto/ClinicalTrialDTO.java index 587da0d..a5da489 100644 --- a/backend/src/main/java/com/ipsen/medical/dto/ClinicalTrialDTO.java +++ b/backend/src/main/java/com/ipsen/medical/dto/ClinicalTrialDTO.java @@ -50,3 +50,7 @@ public class ClinicalTrialDTO { + + + + diff --git a/backend/src/main/java/com/ipsen/medical/dto/ClinicalTrialsSearchResult.java b/backend/src/main/java/com/ipsen/medical/dto/ClinicalTrialsSearchResult.java index db27f02..100a60f 100644 --- a/backend/src/main/java/com/ipsen/medical/dto/ClinicalTrialsSearchResult.java +++ b/backend/src/main/java/com/ipsen/medical/dto/ClinicalTrialsSearchResult.java @@ -29,3 +29,7 @@ public class ClinicalTrialsSearchResult { + + + + diff --git a/backend/src/main/java/com/ipsen/medical/dto/DataSourceSelectionDTO.java b/backend/src/main/java/com/ipsen/medical/dto/DataSourceSelectionDTO.java index 18a44ce..c67ac7f 100644 --- a/backend/src/main/java/com/ipsen/medical/dto/DataSourceSelectionDTO.java +++ b/backend/src/main/java/com/ipsen/medical/dto/DataSourceSelectionDTO.java @@ -14,3 +14,7 @@ public class DataSourceSelectionDTO { } + + + + diff --git a/backend/src/main/java/com/ipsen/medical/dto/KeywordConfirmationDTO.java b/backend/src/main/java/com/ipsen/medical/dto/KeywordConfirmationDTO.java index fa97466..8be6d91 100644 --- a/backend/src/main/java/com/ipsen/medical/dto/KeywordConfirmationDTO.java +++ b/backend/src/main/java/com/ipsen/medical/dto/KeywordConfirmationDTO.java @@ -15,3 +15,7 @@ public class KeywordConfirmationDTO { } + + + + diff --git a/backend/src/main/java/com/ipsen/medical/dto/ResponseGenerationDTO.java b/backend/src/main/java/com/ipsen/medical/dto/ResponseGenerationDTO.java index 093cbad..8581843 100644 --- a/backend/src/main/java/com/ipsen/medical/dto/ResponseGenerationDTO.java +++ b/backend/src/main/java/com/ipsen/medical/dto/ResponseGenerationDTO.java @@ -30,3 +30,7 @@ public class ResponseGenerationDTO { } + + + + diff --git a/backend/src/main/java/com/ipsen/medical/dto/SearchResultItemDTO.java b/backend/src/main/java/com/ipsen/medical/dto/SearchResultItemDTO.java index 7f9fa8a..dabe54d 100644 --- a/backend/src/main/java/com/ipsen/medical/dto/SearchResultItemDTO.java +++ b/backend/src/main/java/com/ipsen/medical/dto/SearchResultItemDTO.java @@ -33,3 +33,7 @@ public class SearchResultItemDTO { } + + + + diff --git a/backend/src/main/java/com/ipsen/medical/dto/UserDTO.java b/backend/src/main/java/com/ipsen/medical/dto/UserDTO.java index 5d5873d..21c9dcf 100644 --- a/backend/src/main/java/com/ipsen/medical/dto/UserDTO.java +++ b/backend/src/main/java/com/ipsen/medical/dto/UserDTO.java @@ -20,3 +20,7 @@ public class UserDTO { } + + + + diff --git a/backend/src/main/java/com/ipsen/medical/entity/InquiryRequest.java b/backend/src/main/java/com/ipsen/medical/entity/InquiryRequest.java index d4f6179..b3d6666 100644 --- a/backend/src/main/java/com/ipsen/medical/entity/InquiryRequest.java +++ b/backend/src/main/java/com/ipsen/medical/entity/InquiryRequest.java @@ -71,14 +71,11 @@ public class InquiryRequest { } public enum RequestStatus { - PENDING, // 待处理 - KEYWORD_EXTRACTED, // 关键词已提取 - SEARCHING, // 检索中 - SEARCH_COMPLETED, // 检索完成 - UNDER_REVIEW, // 审核中 - DOWNLOADING, // 下载文献中 - COMPLETED, // 已完成 - REJECTED // 已拒绝 + CREATED, // 已创建 + SEARCHING, // 检索中(提取关键词成功后进入) + SEARCHED, // 已检索(有检索结果) + RESPONDING, // 回复中(生成回复后) + COMPLETED // 已完成(邮件发送后人工标记) } } diff --git a/backend/src/main/java/com/ipsen/medical/entity/SearchResultItem.java b/backend/src/main/java/com/ipsen/medical/entity/SearchResultItem.java index 3cf30e9..e71b4d7 100644 --- a/backend/src/main/java/com/ipsen/medical/entity/SearchResultItem.java +++ b/backend/src/main/java/com/ipsen/medical/entity/SearchResultItem.java @@ -111,3 +111,7 @@ public class SearchResultItem { } + + + + diff --git a/backend/src/main/java/com/ipsen/medical/repository/ClinicalTrialRepository.java b/backend/src/main/java/com/ipsen/medical/repository/ClinicalTrialRepository.java index d4d36b3..fe5080d 100644 --- a/backend/src/main/java/com/ipsen/medical/repository/ClinicalTrialRepository.java +++ b/backend/src/main/java/com/ipsen/medical/repository/ClinicalTrialRepository.java @@ -39,3 +39,7 @@ public interface ClinicalTrialRepository extends JpaRepository searchResultItemIds); } diff --git a/backend/src/main/java/com/ipsen/medical/service/MultiSourceSearchService.java b/backend/src/main/java/com/ipsen/medical/service/MultiSourceSearchService.java index c6bc18e..3cc2f4f 100644 --- a/backend/src/main/java/com/ipsen/medical/service/MultiSourceSearchService.java +++ b/backend/src/main/java/com/ipsen/medical/service/MultiSourceSearchService.java @@ -39,3 +39,7 @@ public interface MultiSourceSearchService { } + + + + diff --git a/backend/src/main/java/com/ipsen/medical/service/SearchResultService.java b/backend/src/main/java/com/ipsen/medical/service/SearchResultService.java index fa4d293..0c12c5f 100644 --- a/backend/src/main/java/com/ipsen/medical/service/SearchResultService.java +++ b/backend/src/main/java/com/ipsen/medical/service/SearchResultService.java @@ -66,3 +66,7 @@ public interface SearchResultService { } + + + + diff --git a/backend/src/main/java/com/ipsen/medical/service/UserService.java b/backend/src/main/java/com/ipsen/medical/service/UserService.java index 603ebf1..7e8cd10 100644 --- a/backend/src/main/java/com/ipsen/medical/service/UserService.java +++ b/backend/src/main/java/com/ipsen/medical/service/UserService.java @@ -46,3 +46,7 @@ public interface UserService { } + + + + diff --git a/backend/src/main/java/com/ipsen/medical/service/impl/AutoSearchServiceImpl.java b/backend/src/main/java/com/ipsen/medical/service/impl/AutoSearchServiceImpl.java index 7085c59..296666e 100644 --- a/backend/src/main/java/com/ipsen/medical/service/impl/AutoSearchServiceImpl.java +++ b/backend/src/main/java/com/ipsen/medical/service/impl/AutoSearchServiceImpl.java @@ -85,7 +85,7 @@ public class AutoSearchServiceImpl implements AutoSearchService { // 5. 更新查询请求 inquiry.setSearchResults(searchResultsJson); - inquiry.setStatus(InquiryRequest.RequestStatus.SEARCH_COMPLETED); + inquiry.setStatus(InquiryRequest.RequestStatus.SEARCHED); inquiry.setUpdatedAt(LocalDateTime.now()); InquiryRequest savedInquiry = inquiryRequestRepository.save(inquiry); @@ -114,7 +114,7 @@ public class AutoSearchServiceImpl implements AutoSearchService { KeywordExtractionResult keywords = difyService.extractKeywordsStructured(inquiry.getInquiryContent()); String keywordsJson = objectMapper.writeValueAsString(keywords); inquiry.setKeywords(keywordsJson); - inquiry.setStatus(InquiryRequest.RequestStatus.KEYWORD_EXTRACTED); + inquiry.setStatus(InquiryRequest.RequestStatus.SEARCHING); inquiry.setUpdatedAt(LocalDateTime.now()); inquiryRequestRepository.save(inquiry); @@ -132,7 +132,7 @@ public class AutoSearchServiceImpl implements AutoSearchService { inquiry.getSearchResults() ); inquiry.setResponseContent(response); - inquiry.setStatus(InquiryRequest.RequestStatus.UNDER_REVIEW); + inquiry.setStatus(InquiryRequest.RequestStatus.RESPONDING); inquiry.setUpdatedAt(LocalDateTime.now()); inquiryRequestRepository.save(inquiry); } catch (Exception e) { diff --git a/backend/src/main/java/com/ipsen/medical/service/impl/DifyServiceImpl.java b/backend/src/main/java/com/ipsen/medical/service/impl/DifyServiceImpl.java index 459ae76..d6f0334 100644 --- a/backend/src/main/java/com/ipsen/medical/service/impl/DifyServiceImpl.java +++ b/backend/src/main/java/com/ipsen/medical/service/impl/DifyServiceImpl.java @@ -13,7 +13,6 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.web.reactive.function.client.WebClient; -import reactor.core.publisher.Mono; import java.util.HashMap; import java.util.Map; @@ -31,6 +30,9 @@ public class DifyServiceImpl implements DifyService { @Value("${app.dify.api-key}") private String difyApiKey; + @Value("${app.dify.app-type:workflow}") + private String difyAppType; + private final WebClient webClient; private final ObjectMapper objectMapper; @@ -46,50 +48,79 @@ public class DifyServiceImpl implements DifyService { public KeywordExtractionResult extractKeywordsStructured(String content) { log.info("Extracting keywords structured from content"); - // 检查API Key配置 - if (difyApiKey == null || difyApiKey.equals("your-dify-api-key-here") || difyApiKey.trim().isEmpty()) { - log.error("Dify API Key not configured properly. Current value: {}", - difyApiKey != null ? difyApiKey.substring(0, Math.min(20, difyApiKey.length())) + "..." : "null"); - throw new RuntimeException("Dify API Key未配置。请设置环境变量DIFY_API_KEY或修改application.yml中的配置。"); - } - try { String response = callDifyAPI(content, "extract_keywords"); - // 解析响应获取结构化数据 + // 解析响应获取结构化数据(兼容 Workflow 与 Chat 返回结构) JsonNode rootNode = objectMapper.readTree(response); - JsonNode answerNode = rootNode.path("answer"); - - String answer; - if (answerNode.isTextual()) { - answer = answerNode.asText(); - } else { - answer = answerNode.toString(); + KeywordExtractionResult result = new KeywordExtractionResult(); + + boolean mapped = false; + + // 优先:Workflow 返回,一般为 { data: { outputs: { ... } } } 或 { data: { ... } } + if (rootNode.has("data")) { + JsonNode dataNode = rootNode.get("data"); + JsonNode outputsNode = dataNode.has("outputs") ? dataNode.get("outputs") : dataNode; + + // 直接包含目标字段 + if (outputsNode.has("drugNameChinese") || outputsNode.has("drugNameEnglish") || outputsNode.has("requestItem")) { + result.setDrugNameChinese(outputsNode.path("drugNameChinese").asText(null)); + result.setDrugNameEnglish(outputsNode.path("drugNameEnglish").asText(null)); + result.setRequestItem(outputsNode.path("requestItem").asText(null)); + mapped = true; + } + + // 某些工作流可能将结构化结果放在 result 字段 + if (!mapped && outputsNode.has("result")) { + JsonNode inner = outputsNode.get("result"); + if (inner.isTextual()) { + try { + JsonNode parsed = objectMapper.readTree(inner.asText()); + result.setDrugNameChinese(parsed.path("drugNameChinese").asText(null)); + result.setDrugNameEnglish(parsed.path("drugNameEnglish").asText(null)); + result.setRequestItem(parsed.path("requestItem").asText(null)); + mapped = true; + } catch (Exception ignore) { + // 非JSON文本则尝试从文本解析 + result = parseKeywordsFromText(inner.asText()); + mapped = true; + } + } else if (inner.isObject()) { + result.setDrugNameChinese(inner.path("drugNameChinese").asText(null)); + result.setDrugNameEnglish(inner.path("drugNameEnglish").asText(null)); + result.setRequestItem(inner.path("requestItem").asText(null)); + mapped = true; + } + } } - - // 尝试解析answer字段中的JSON - KeywordExtractionResult result; - try { - // 如果answer是JSON格式 - JsonNode resultNode = objectMapper.readTree(answer); - result = new KeywordExtractionResult(); - result.setDrugNameChinese(resultNode.path("drugNameChinese").asText(null)); - result.setDrugNameEnglish(resultNode.path("drugNameEnglish").asText(null)); - result.setRequestItem(resultNode.path("requestItem").asText(null)); - - // 提取置信度 + + // 兼容 Chat 返回:通常 { answer: "...可能是JSON或文本...", metadata: {...} } + if (!mapped && rootNode.has("answer")) { + JsonNode answerNode = rootNode.get("answer"); + String answer = answerNode.isTextual() ? answerNode.asText() : answerNode.toString(); + + try { + JsonNode resultNode = objectMapper.readTree(answer); + result.setDrugNameChinese(resultNode.path("drugNameChinese").asText(null)); + result.setDrugNameEnglish(resultNode.path("drugNameEnglish").asText(null)); + result.setRequestItem(resultNode.path("requestItem").asText(null)); + mapped = true; + } catch (Exception e) { + log.warn("Answer is not JSON format, trying text parsing"); + result = parseKeywordsFromText(answer); + mapped = true; + } + + // 提取置信度(如有) if (rootNode.has("metadata")) { JsonNode metadata = rootNode.get("metadata"); if (metadata.has("confidence")) { result.setConfidence(metadata.get("confidence").asDouble()); } } - } catch (Exception e) { - // 如果answer不是JSON格式,尝试文本解析 - log.warn("Answer is not JSON format, trying text parsing"); - result = parseKeywordsFromText(answer); } - + + // 若仍未映射,兜底为空对象 log.info("Extracted keywords: Chinese={}, English={}, Item={}", result.getDrugNameChinese(), result.getDrugNameEnglish(), result.getRequestItem()); @@ -143,50 +174,28 @@ public class DifyServiceImpl implements DifyService { @Override public ResponseGenerationDTO generateStructuredResponse(String inquiryContent, String selectedMaterials) { log.info("Generating structured response"); - try { - String prompt = String.format( - "请基于以下查询问题和检索到的资料生成标准化回复。\n\n" + - "查询问题:%s\n\n" + - "检索到的资料:%s\n\n" + - "请按照以下JSON格式返回:\n" + - "{\n" + - " \"question\": \"原始查询问题\",\n" + - " \"queriedMaterials\": \"查询到的资料简要说明\",\n" + - " \"summary\": \"基于资料的核心观点总结\",\n" + - " \"materialList\": [\n" + - " {\n" + - " \"title\": \"标题\",\n" + - " \"authors\": \"作者\",\n" + - " \"source\": \"来源\",\n" + - " \"publicationDate\": \"发表日期\",\n" + - " \"url\": \"链接\",\n" + - " \"summary\": \"摘要\",\n" + - " \"relevance\": \"相关性说明\"\n" + - " }\n" + - " ]\n" + - "}", - inquiryContent, selectedMaterials + String combined = String.format( + "查询内容:%s\n\n选中资料:%s", + inquiryContent, selectedMaterials ); - - String response = callDifyAPI(prompt, "generate_structured_response"); - - // 解析响应 + String response = callDifyAPI(combined, "generate_structured_response"); + JsonNode rootNode = objectMapper.readTree(response); JsonNode answerNode = rootNode.path("answer"); - - String answer; - if (answerNode.isTextual()) { - answer = answerNode.asText(); - } else { - answer = answerNode.toString(); + String answerText = answerNode.isTextual() ? answerNode.asText() : answerNode.toString(); + + // 尝试直接将 answer 反序列化为结构化对象 + try { + return objectMapper.readValue(answerText, ResponseGenerationDTO.class); + } catch (Exception ignore) { + // 如果不是严格JSON,尝试构造最小可用结构 + ResponseGenerationDTO dto = new ResponseGenerationDTO(); + dto.setQuestion(inquiryContent); + dto.setSummary(answerText); + dto.setQueriedMaterials("已选资料共计,详见前端展示"); + return dto; } - - // 解析结构化回复 - ResponseGenerationDTO result = objectMapper.readValue(answer, ResponseGenerationDTO.class); - - return result; - } catch (Exception e) { log.error("Error generating structured response: ", e); throw new RuntimeException("Failed to generate structured response: " + e.getMessage(), e); @@ -198,40 +207,48 @@ public class DifyServiceImpl implements DifyService { */ private String callDifyAPI(String content, String taskType) { try { - log.info("Calling Dify API: url={}, taskType={}", difyApiUrl, taskType); - log.info("API Key: {}", difyApiKey != null ? difyApiKey.substring(0, Math.min(10, difyApiKey.length())) + "..." : "null"); - - // 构建请求 - DifyRequest request = new DifyRequest(); - request.setQuery(content); - request.setResponseMode("blocking"); - request.setUser("medical-info-system"); - - // 添加任务类型到inputs + log.info("Calling Dify API: baseUrl={}, appType={}, taskType={}", difyApiUrl, difyAppType, taskType); + + // 构建 inputs Map inputs = new HashMap<>(); inputs.put("task_type", taskType); - inputs.put("content", content); - request.setInputs(inputs); - - log.debug("Dify API request: {}", objectMapper.writeValueAsString(request)); - - // 调用API + // Dify 工作流要求的输入参数名,值来自查询详情的 inquiry_content + inputs.put("Inquery_request", content); + + String endpoint; + Object body; + + if ("workflow".equalsIgnoreCase(difyAppType)) { + // Workflow 应用 + Map workflowBody = new HashMap<>(); + workflowBody.put("inputs", inputs); + workflowBody.put("response_mode", "blocking"); + workflowBody.put("user", "medical-info-system"); + endpoint = "/workflows/run"; + body = workflowBody; + } else { + // Chat 应用 + DifyRequest request = new DifyRequest(); + request.setInputs(inputs); + request.setQuery(content); + request.setResponseMode("blocking"); + request.setUser("medical-info-system"); + endpoint = "/chat-messages"; + body = request; + } + String response = webClient.post() - .uri(difyApiUrl + "/chat-messages") + .uri(difyApiUrl + endpoint) .header(HttpHeaders.AUTHORIZATION, "Bearer " + difyApiKey) .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) - .bodyValue(request) + .bodyValue(body) .retrieve() - .onStatus(status -> status.is4xxClientError() || status.is5xxServerError(), - clientResponse -> { - log.error("Dify API error: status={}, headers={}", - clientResponse.statusCode(), clientResponse.headers().asHttpHeaders()); - return clientResponse.bodyToMono(String.class) - .flatMap(body -> { - log.error("Dify API error body: {}", body); - return Mono.error(new RuntimeException("Dify API error: " + clientResponse.statusCode() + " - " + body)); - }); - }) + .onStatus(status -> status.is4xxClientError() || status.is5xxServerError(), + clientResponse -> clientResponse.bodyToMono(String.class).map(errBody -> { + String msg = String.format("Dify API error %s: %s", clientResponse.statusCode(), errBody); + log.error(msg); + return new RuntimeException(msg); + })) .bodyToMono(String.class) .block(); diff --git a/backend/src/main/java/com/ipsen/medical/service/impl/InquiryServiceImpl.java b/backend/src/main/java/com/ipsen/medical/service/impl/InquiryServiceImpl.java index 1122f6c..47760be 100644 --- a/backend/src/main/java/com/ipsen/medical/service/impl/InquiryServiceImpl.java +++ b/backend/src/main/java/com/ipsen/medical/service/impl/InquiryServiceImpl.java @@ -7,7 +7,9 @@ import com.ipsen.medical.dto.KeywordConfirmationDTO; import com.ipsen.medical.dto.ResponseGenerationDTO; import com.ipsen.medical.dto.SearchResultItemDTO; import com.ipsen.medical.entity.InquiryRequest; +import com.ipsen.medical.entity.AuditLog; import com.ipsen.medical.repository.InquiryRequestRepository; +import com.ipsen.medical.repository.AuditLogRepository; import com.ipsen.medical.service.InquiryService; import com.ipsen.medical.service.ExcelParserService; import com.ipsen.medical.service.DifyService; @@ -44,6 +46,24 @@ public class InquiryServiceImpl implements InquiryService { private final ObjectMapper objectMapper = new ObjectMapper(); + @Autowired + private AuditLogRepository auditLogRepository; + + private void assertStatus(InquiryRequest inquiryRequest, InquiryRequest.RequestStatus... allowed) { + for (InquiryRequest.RequestStatus s : allowed) { + if (inquiryRequest.getStatus() == s) return; + } + throw new RuntimeException("当前状态不允许此操作,当前状态: " + inquiryRequest.getStatus()); + } + + private void logAudit(InquiryRequest inquiryRequest, AuditLog.AuditAction action, String comments) { + AuditLog log = new AuditLog(); + log.setInquiryRequest(inquiryRequest); + log.setAction(action); + log.setComments(comments); + auditLogRepository.save(log); + } + @Override public InquiryRequestDTO uploadInquiry(MultipartFile file) { try { @@ -65,7 +85,7 @@ public class InquiryServiceImpl implements InquiryService { inquiryRequest.setCustomerEmail(inquiryRequestDTO.getCustomerEmail()); inquiryRequest.setCustomerTitle(inquiryRequestDTO.getCustomerTitle()); inquiryRequest.setInquiryContent(inquiryRequestDTO.getInquiryContent()); - inquiryRequest.setStatus(InquiryRequest.RequestStatus.PENDING); + inquiryRequest.setStatus(InquiryRequest.RequestStatus.CREATED); inquiryRequest.setCreatedAt(LocalDateTime.now()); InquiryRequest savedRequest = inquiryRequestRepository.save(inquiryRequest); @@ -102,15 +122,19 @@ public class InquiryServiceImpl implements InquiryService { public InquiryRequestDTO extractKeywords(Long id) { InquiryRequest inquiryRequest = inquiryRequestRepository.findById(id) .orElseThrow(() -> new RuntimeException("查询请求不存在: " + id)); + // 已创建或已检索后允许重新提取关键词,进入检索中 + assertStatus(inquiryRequest, InquiryRequest.RequestStatus.CREATED, InquiryRequest.RequestStatus.SEARCHED); try { // 使用Dify服务提取关键词 String keywords = difyService.extractKeywords(inquiryRequest.getInquiryContent()); inquiryRequest.setKeywords(keywords); - inquiryRequest.setStatus(InquiryRequest.RequestStatus.KEYWORD_EXTRACTED); + inquiryRequest.setKeywordsConfirmed(false); + inquiryRequest.setStatus(InquiryRequest.RequestStatus.SEARCHING); inquiryRequest.setUpdatedAt(LocalDateTime.now()); InquiryRequest savedRequest = inquiryRequestRepository.save(inquiryRequest); + logAudit(savedRequest, AuditLog.AuditAction.SUBMITTED, "关键词已提取"); return convertToDTO(savedRequest); } catch (Exception e) { throw new RuntimeException("关键词提取失败: " + e.getMessage(), e); @@ -121,14 +145,20 @@ public class InquiryServiceImpl implements InquiryService { public InquiryRequestDTO performSearch(Long id) { InquiryRequest inquiryRequest = inquiryRequestRepository.findById(id) .orElseThrow(() -> new RuntimeException("查询请求不存在: " + id)); + // 仅在检索中触发检索 + assertStatus(inquiryRequest, InquiryRequest.RequestStatus.SEARCHING); try { // 执行多源检索 + inquiryRequest.setStatus(InquiryRequest.RequestStatus.SEARCHING); + inquiryRequest.setUpdatedAt(LocalDateTime.now()); + inquiryRequestRepository.save(inquiryRequest); multiSourceSearchService.performMultiSourceSearch(id); // 重新加载更新后的请求 inquiryRequest = inquiryRequestRepository.findById(id) .orElseThrow(() -> new RuntimeException("查询请求不存在: " + id)); + logAudit(inquiryRequest, AuditLog.AuditAction.SUBMITTED, "检索完成"); return convertToDTO(inquiryRequest); } catch (Exception e) { @@ -140,6 +170,8 @@ public class InquiryServiceImpl implements InquiryService { public InquiryRequestDTO generateResponse(Long id) { InquiryRequest inquiryRequest = inquiryRequestRepository.findById(id) .orElseThrow(() -> new RuntimeException("查询请求不存在: " + id)); + // 仅在已检索状态生成回复 + assertStatus(inquiryRequest, InquiryRequest.RequestStatus.SEARCHED); try { // 获取用户选中的检索结果(includeInResponse = true) @@ -164,10 +196,11 @@ public class InquiryServiceImpl implements InquiryService { String responseContent = objectMapper.writeValueAsString(structuredResponse); inquiryRequest.setResponseContent(responseContent); - inquiryRequest.setStatus(InquiryRequest.RequestStatus.UNDER_REVIEW); + inquiryRequest.setStatus(InquiryRequest.RequestStatus.RESPONDING); inquiryRequest.setUpdatedAt(LocalDateTime.now()); InquiryRequest savedRequest = inquiryRequestRepository.save(inquiryRequest); + logAudit(savedRequest, AuditLog.AuditAction.SUBMITTED, "已生成结构化回复"); return convertToDTO(savedRequest); } catch (Exception e) { throw new RuntimeException("回复生成失败: " + e.getMessage(), e); @@ -178,12 +211,14 @@ public class InquiryServiceImpl implements InquiryService { public InquiryRequestDTO reviewResponse(Long id, Boolean approved, String comments) { InquiryRequest inquiryRequest = inquiryRequestRepository.findById(id) .orElseThrow(() -> new RuntimeException("查询请求不存在: " + id)); + // 审核仅在回复中允许 + assertStatus(inquiryRequest, InquiryRequest.RequestStatus.RESPONDING); + // 审核结果仅记录,不改变整体生命周期(完成由人工标记) if (approved) { - inquiryRequest.setStatus(InquiryRequest.RequestStatus.COMPLETED); - inquiryRequest.setCompletedAt(LocalDateTime.now()); + logAudit(inquiryRequest, AuditLog.AuditAction.APPROVED, comments); } else { - inquiryRequest.setStatus(InquiryRequest.RequestStatus.REJECTED); + logAudit(inquiryRequest, AuditLog.AuditAction.REJECTED, comments); } inquiryRequest.setUpdatedAt(LocalDateTime.now()); @@ -195,12 +230,14 @@ public class InquiryServiceImpl implements InquiryService { public InquiryRequestDTO completeInquiry(Long id) { InquiryRequest inquiryRequest = inquiryRequestRepository.findById(id) .orElseThrow(() -> new RuntimeException("查询请求不存在: " + id)); - + // 仅在回复中可标记完成 + assertStatus(inquiryRequest, InquiryRequest.RequestStatus.RESPONDING); inquiryRequest.setStatus(InquiryRequest.RequestStatus.COMPLETED); inquiryRequest.setCompletedAt(LocalDateTime.now()); inquiryRequest.setUpdatedAt(LocalDateTime.now()); InquiryRequest savedRequest = inquiryRequestRepository.save(inquiryRequest); + logAudit(savedRequest, AuditLog.AuditAction.COMPLETED, "处理完成"); return convertToDTO(savedRequest); } @@ -208,15 +245,20 @@ public class InquiryServiceImpl implements InquiryService { public InquiryRequestDTO confirmKeywords(Long id, KeywordConfirmationDTO keywordDTO) { InquiryRequest inquiryRequest = inquiryRequestRepository.findById(id) .orElseThrow(() -> new RuntimeException("查询请求不存在: " + id)); + // 仅在检索中允许确认关键词(如果保留该步骤) + assertStatus(inquiryRequest, InquiryRequest.RequestStatus.SEARCHING); try { // 将用户确认的关键词保存为JSON String keywordsJson = objectMapper.writeValueAsString(keywordDTO); inquiryRequest.setKeywords(keywordsJson); inquiryRequest.setKeywordsConfirmed(true); + // 确认后仍保持检索中 + inquiryRequest.setStatus(InquiryRequest.RequestStatus.SEARCHING); inquiryRequest.setUpdatedAt(LocalDateTime.now()); InquiryRequest savedRequest = inquiryRequestRepository.save(inquiryRequest); + logAudit(savedRequest, AuditLog.AuditAction.SUBMITTED, "关键词已确认"); return convertToDTO(savedRequest); } catch (Exception e) { throw new RuntimeException("关键词确认失败: " + e.getMessage(), e); @@ -227,6 +269,8 @@ public class InquiryServiceImpl implements InquiryService { public InquiryRequestDTO selectDataSources(Long id, DataSourceSelectionDTO dataSourceDTO) { InquiryRequest inquiryRequest = inquiryRequestRepository.findById(id) .orElseThrow(() -> new RuntimeException("查询请求不存在: " + id)); + // 选择数据源需在检索中 + assertStatus(inquiryRequest, InquiryRequest.RequestStatus.SEARCHING); // 保存用户选择的数据源 inquiryRequest.setSearchInternalData(dataSourceDTO.getSearchInternalData()); @@ -236,6 +280,7 @@ public class InquiryServiceImpl implements InquiryService { inquiryRequest.setUpdatedAt(LocalDateTime.now()); InquiryRequest savedRequest = inquiryRequestRepository.save(inquiryRequest); + logAudit(savedRequest, AuditLog.AuditAction.SUBMITTED, "已选择数据源"); return convertToDTO(savedRequest); } @@ -272,4 +317,30 @@ public class InquiryServiceImpl implements InquiryService { dto.setCompletedAt(inquiryRequest.getCompletedAt()); return dto; } + + @Override + public InquiryRequestDTO generateDownloadTasks(Long id, List searchResultItemIds) { + InquiryRequest inquiryRequest = inquiryRequestRepository.findById(id) + .orElseThrow(() -> new RuntimeException("查询请求不存在: " + id)); + assertStatus(inquiryRequest, InquiryRequest.RequestStatus.SEARCHED); + + if (searchResultItemIds == null || searchResultItemIds.isEmpty()) { + throw new RuntimeException("未选中任何需要下载的条目"); + } + + // 标记选中的条目为需要下载 + for (Long itemId : searchResultItemIds) { + com.ipsen.medical.dto.SearchResultItemDTO dto = new com.ipsen.medical.dto.SearchResultItemDTO(); + dto.setId(itemId); + dto.setNeedDownload(true); + searchResultService.updateSearchResult(itemId, dto); + } + + // 生成下载任务不改变主流程状态 + inquiryRequest.setUpdatedAt(LocalDateTime.now()); + InquiryRequest saved = inquiryRequestRepository.save(inquiryRequest); + + logAudit(saved, AuditLog.AuditAction.SUBMITTED, "已生成下载任务"); + return convertToDTO(saved); + } } diff --git a/backend/src/main/java/com/ipsen/medical/service/impl/MultiSourceSearchServiceImpl.java b/backend/src/main/java/com/ipsen/medical/service/impl/MultiSourceSearchServiceImpl.java index cff5ef2..a56bd5c 100644 --- a/backend/src/main/java/com/ipsen/medical/service/impl/MultiSourceSearchServiceImpl.java +++ b/backend/src/main/java/com/ipsen/medical/service/impl/MultiSourceSearchServiceImpl.java @@ -85,8 +85,8 @@ public class MultiSourceSearchServiceImpl implements MultiSourceSearchService { allResults.addAll(ctResults); } - // 更新查询请求状态 - inquiry.setStatus(InquiryRequest.RequestStatus.SEARCH_COMPLETED); + // 更新查询请求状态为已检索 + inquiry.setStatus(InquiryRequest.RequestStatus.SEARCHED); inquiryRequestRepository.save(inquiry); log.info("Multi-source search completed, found {} results", allResults.size()); diff --git a/backend/src/main/java/com/ipsen/medical/service/impl/SearchResultServiceImpl.java b/backend/src/main/java/com/ipsen/medical/service/impl/SearchResultServiceImpl.java index 3f8b6d4..3ba8516 100644 --- a/backend/src/main/java/com/ipsen/medical/service/impl/SearchResultServiceImpl.java +++ b/backend/src/main/java/com/ipsen/medical/service/impl/SearchResultServiceImpl.java @@ -199,3 +199,7 @@ public class SearchResultServiceImpl implements SearchResultService { } + + + + diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 1202907..a63b055 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -37,8 +37,10 @@ app: # Dify配置 dify: - api-url: ${DIFY_API_URL:https://api.dify.ai/v1} - api-key: ${DIFY_API_KEY:your-dify-api-key-here} + api-url: https://api.dify.ai/v1 + api-key: app-croZF0SSV5fiyXbK3neGrOT6 + # 可选: workflow 或 chat + app-type: workflow # 大模型配置 llm: diff --git a/database/init_drug_module.sql b/database/init_drug_module.sql index 6a010a5..f508602 100644 --- a/database/init_drug_module.sql +++ b/database/init_drug_module.sql @@ -215,3 +215,7 @@ SELECT '药物模块初始化完成!' AS message, + + + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a10e79c..f0e74cf 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -21,7 +21,7 @@ "eslint": "^8.55.0", "eslint-plugin-vue": "^9.19.2", "sass": "^1.69.5", - "vite": "^5.0.0" + "vite": "^6.4.1" } }, "node_modules/@babel/helper-string-parser": { @@ -89,9 +89,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz", + "integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==", "cpu": [ "ppc64" ], @@ -102,13 +102,13 @@ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.11.tgz", + "integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==", "cpu": [ "arm" ], @@ -119,13 +119,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz", + "integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==", "cpu": [ "arm64" ], @@ -136,13 +136,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.11.tgz", + "integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==", "cpu": [ "x64" ], @@ -153,13 +153,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz", + "integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==", "cpu": [ "arm64" ], @@ -170,13 +170,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz", + "integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==", "cpu": [ "x64" ], @@ -187,13 +187,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz", + "integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==", "cpu": [ "arm64" ], @@ -204,13 +204,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz", + "integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==", "cpu": [ "x64" ], @@ -221,13 +221,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz", + "integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==", "cpu": [ "arm" ], @@ -238,13 +238,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz", + "integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==", "cpu": [ "arm64" ], @@ -255,13 +255,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz", + "integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==", "cpu": [ "ia32" ], @@ -272,13 +272,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz", + "integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==", "cpu": [ "loong64" ], @@ -289,13 +289,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz", + "integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==", "cpu": [ "mips64el" ], @@ -306,13 +306,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz", + "integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==", "cpu": [ "ppc64" ], @@ -323,13 +323,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz", + "integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==", "cpu": [ "riscv64" ], @@ -340,13 +340,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz", + "integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==", "cpu": [ "s390x" ], @@ -357,13 +357,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz", + "integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==", "cpu": [ "x64" ], @@ -374,13 +374,30 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz", + "integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz", + "integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==", "cpu": [ "x64" ], @@ -391,13 +408,30 @@ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz", + "integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz", + "integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==", "cpu": [ "x64" ], @@ -408,13 +442,30 @@ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz", + "integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz", + "integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==", "cpu": [ "x64" ], @@ -425,13 +476,13 @@ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz", + "integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==", "cpu": [ "arm64" ], @@ -442,13 +493,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz", + "integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==", "cpu": [ "ia32" ], @@ -459,13 +510,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz", + "integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==", "cpu": [ "x64" ], @@ -476,7 +527,7 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { @@ -1897,9 +1948,9 @@ } }, "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz", + "integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1907,32 +1958,35 @@ "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "@esbuild/aix-ppc64": "0.25.11", + "@esbuild/android-arm": "0.25.11", + "@esbuild/android-arm64": "0.25.11", + "@esbuild/android-x64": "0.25.11", + "@esbuild/darwin-arm64": "0.25.11", + "@esbuild/darwin-x64": "0.25.11", + "@esbuild/freebsd-arm64": "0.25.11", + "@esbuild/freebsd-x64": "0.25.11", + "@esbuild/linux-arm": "0.25.11", + "@esbuild/linux-arm64": "0.25.11", + "@esbuild/linux-ia32": "0.25.11", + "@esbuild/linux-loong64": "0.25.11", + "@esbuild/linux-mips64el": "0.25.11", + "@esbuild/linux-ppc64": "0.25.11", + "@esbuild/linux-riscv64": "0.25.11", + "@esbuild/linux-s390x": "0.25.11", + "@esbuild/linux-x64": "0.25.11", + "@esbuild/netbsd-arm64": "0.25.11", + "@esbuild/netbsd-x64": "0.25.11", + "@esbuild/openbsd-arm64": "0.25.11", + "@esbuild/openbsd-x64": "0.25.11", + "@esbuild/openharmony-arm64": "0.25.11", + "@esbuild/sunos-x64": "0.25.11", + "@esbuild/win32-arm64": "0.25.11", + "@esbuild/win32-ia32": "0.25.11", + "@esbuild/win32-x64": "0.25.11" } }, "node_modules/escape-string-regexp": { @@ -3262,6 +3316,54 @@ "dev": true, "license": "MIT" }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3320,21 +3422,24 @@ "license": "MIT" }, "node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -3343,19 +3448,25 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", - "terser": "^5.4.0" + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, + "jiti": { + "optional": true + }, "less": { "optional": true }, @@ -3376,9 +3487,46 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/vue": { "version": "3.5.22", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.22.tgz", diff --git a/frontend/package.json b/frontend/package.json index 8be44d5..2445cca 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,23 +9,19 @@ "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore" }, "dependencies": { - "vue": "^3.4.0", - "vue-router": "^4.2.5", - "pinia": "^2.1.7", - "axios": "^1.6.2", - "element-plus": "^2.5.0", "@element-plus/icons-vue": "^2.3.1", - "dayjs": "^1.11.10" + "axios": "^1.6.2", + "dayjs": "^1.11.10", + "element-plus": "^2.5.0", + "pinia": "^2.1.7", + "vue": "^3.4.0", + "vue-router": "^4.2.5" }, "devDependencies": { "@vitejs/plugin-vue": "^5.0.0", - "vite": "^5.0.0", "eslint": "^8.55.0", "eslint-plugin-vue": "^9.19.2", - "sass": "^1.69.5" + "sass": "^1.69.5", + "vite": "^6.4.1" } } - - - - diff --git a/frontend/src/api/auth.js b/frontend/src/api/auth.js new file mode 100644 index 0000000..fd885ce --- /dev/null +++ b/frontend/src/api/auth.js @@ -0,0 +1,11 @@ +import request from '@/utils/request' + +export function login(data) { + return request({ + url: '/auth/login', + method: 'post', + data + }) +} + + diff --git a/frontend/src/api/inquiry.js b/frontend/src/api/inquiry.js index 6ce632d..91056b6 100644 --- a/frontend/src/api/inquiry.js +++ b/frontend/src/api/inquiry.js @@ -59,12 +59,13 @@ export function extractKeywords(id) { } /** - * 执行检索 + * 执行检索(可携带查询表达式与检索范围) */ -export function performSearch(id) { +export function performSearch(id, data) { return request({ url: `/inquiries/${id}/search`, - method: 'post' + method: 'post', + data }) } @@ -182,6 +183,17 @@ export function selectDataSources(id, data) { }) } +/** + * 生成下载任务(根据选中的检索结果ID) + */ +export function generateDownloadTasks(id, ids) { + return request({ + url: `/inquiries/${id}/downloads`, + method: 'post', + data: ids + }) +} + diff --git a/frontend/src/api/searchResult.js b/frontend/src/api/searchResult.js index 5c2e360..1cf5957 100644 --- a/frontend/src/api/searchResult.js +++ b/frontend/src/api/searchResult.js @@ -107,3 +107,7 @@ export function markDownloadFailed(id, reason) { } + + + + diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index f9612ee..e886f19 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -144,19 +144,9 @@ const router = createRouter({ routes }) -// 路由守卫 +// 放开路由守卫(后端已不需要登录) router.beforeEach((to, from, next) => { - const token = localStorage.getItem('token') - - if (to.path === '/login') { - next() - } else { - if (!token) { - next('/login') - } else { - next() - } - } + next() }) export default router diff --git a/frontend/src/utils/request.js b/frontend/src/utils/request.js index fd116df..12fd0b4 100644 --- a/frontend/src/utils/request.js +++ b/frontend/src/utils/request.js @@ -9,10 +9,7 @@ const service = axios.create({ // 请求拦截器 service.interceptors.request.use( config => { - const token = localStorage.getItem('token') - if (token) { - config.headers['Authorization'] = `Bearer ${token}` - } + // 取消附加 Authorization 头,后端已不需要鉴权 return config }, error => { diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue index f242e81..61b0ded 100644 --- a/frontend/src/views/Login.vue +++ b/frontend/src/views/Login.vue @@ -45,6 +45,7 @@ import { ref, reactive } from 'vue' import { useRouter } from 'vue-router' import { ElMessage } from 'element-plus' +import { login as apiLogin } from '@/api/auth' const router = useRouter() const loginFormRef = ref(null) @@ -68,23 +69,19 @@ const rules = { const handleLogin = async () => { if (!loginFormRef.value) return - await loginFormRef.value.validate((valid) => { - if (valid) { - loading.value = true - - // TODO: 实际项目中应该调用登录API - // 这里使用模拟登录 - setTimeout(() => { - if (loginForm.username === 'admin' && loginForm.password === 'admin123') { - localStorage.setItem('token', 'mock-token-' + Date.now()) - localStorage.setItem('username', loginForm.username) - ElMessage.success('登录成功') - router.push('/dashboard') - } else { - ElMessage.error('用户名或密码错误') - } - loading.value = false - }, 1000) + await loginFormRef.value.validate(async (valid) => { + if (!valid) return + loading.value = true + try { + const resp = await apiLogin({ username: loginForm.username, password: loginForm.password }) + localStorage.setItem('token', resp.token) + localStorage.setItem('username', resp.username) + ElMessage.success('登录成功') + router.push('/dashboard') + } catch (e) { + // 错误提示由拦截器处理 + } finally { + loading.value = false } }) } diff --git a/frontend/src/views/inquiry/DataSourceSelection.vue b/frontend/src/views/inquiry/DataSourceSelection.vue index c770164..e5fac46 100644 --- a/frontend/src/views/inquiry/DataSourceSelection.vue +++ b/frontend/src/views/inquiry/DataSourceSelection.vue @@ -435,3 +435,7 @@ const goBack = () => { + + + + diff --git a/frontend/src/views/inquiry/DownloadTasks.vue b/frontend/src/views/inquiry/DownloadTasks.vue index 076c294..771593d 100644 --- a/frontend/src/views/inquiry/DownloadTasks.vue +++ b/frontend/src/views/inquiry/DownloadTasks.vue @@ -482,3 +482,7 @@ const goBack = () => { + + + + diff --git a/frontend/src/views/inquiry/InquiryCreate.vue b/frontend/src/views/inquiry/InquiryCreate.vue index 47c5d55..9dc0c04 100644 --- a/frontend/src/views/inquiry/InquiryCreate.vue +++ b/frontend/src/views/inquiry/InquiryCreate.vue @@ -43,15 +43,15 @@ style="max-width: 600px;" > - + - + - + @@ -59,12 +59,12 @@ v-model="form.inquiryContent" type="textarea" :rows="8" - placeholder="请输入查询内容" + placeholder="请输入查询内容(必填)" /> - + @@ -103,14 +103,8 @@ const form = ref({ assignedTo: '' }) +// 仅保留“查询内容”为必填,其余为可选 const rules = { - customerName: [ - { required: true, message: '请输入客户姓名', trigger: 'blur' } - ], - customerEmail: [ - { required: true, message: '请输入客户邮箱', trigger: 'blur' }, - { type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' } - ], inquiryContent: [ { required: true, message: '请输入查询内容', trigger: 'blur' } ] diff --git a/frontend/src/views/inquiry/InquiryDetail.vue b/frontend/src/views/inquiry/InquiryDetail.vue index c8b5b68..353e50d 100644 --- a/frontend/src/views/inquiry/InquiryDetail.vue +++ b/frontend/src/views/inquiry/InquiryDetail.vue @@ -14,14 +14,7 @@ {{ inquiry.requestNumber }} - - - - - - - - + {{ inquiry.customerName }} @@ -43,139 +36,34 @@ - - + - - 开始处理(关键词提取) - - - - 确认关键词 - - - - 选择数据源并检索 - - - - 查看检索结果 - - - - 查看生成的回复 - - - - - 一键执行AI流程 - - - - 提取关键词 - - + :loading="extractLoading" + :disabled="extractLoading || searchLoading || !['CREATED','SEARCHED'].includes(inquiry.status)" + >提取关键词 - 智能自动检索 - - - - 执行检索 - - + @click="handleSearchWithParams" + :loading="searchLoading" + :disabled="extractLoading || searchLoading || !standardQuery?.trim()" + >一键检索 + 查看检索结果 - 生成回复 - - - - - 批准回复 - - - 要求修改 - - - - - 下载文献 - - - - 完成处理 - + :disabled="processing || inquiry.status !== 'SEARCHED'" + >生成回复 + 查看回复 - - - + + AI识别结果 - + {{ getKeywordField(inquiry.keywords, 'drugNameChinese') }} @@ -194,168 +82,199 @@ - - - 检索结果 - {{ formatJSON(inquiry.searchResults) }} - - - - 回复内容 - - {{ inquiry.responseContent }} + + + + 检索范围与标准检索词 + + + + {{ row.label }} + + + + + + + + 删除 + + + 添加检索式 + + + + + + + + {{ item.summary }} + — + + + + + + + 未选中的检索范围禁用编辑;当该条有“结果概述”表示已完成,不可修改。 - + - - - - 临床试验信息 (ClinicalTrials.gov) - - - - 搜索临床试验 - - - 导出CSV - - - 刷新 - + + + + + + + + 临床试验信息 (ClinicalTrials.gov) + + + 导出CSV + + + 刷新 + + + + + + + + + + + + + + {{ row.nctId }} + + + + + {{ row.overallStatus }} + + + + {{ row.studyType }} + + + {{ row.phase || 'N/A' }} + + + {{ row.startDate || 'N/A' }} + + + {{ row.completionDate || 'N/A' }} + + + {{ row.enrollment || 'N/A' }} + + + {{ row.sponsor }} + + + + {{ condition }} + + + + + {{ intervention }} + + + + + {{ row.briefSummary || 'N/A' }} + + + + + + {{ location.facility }} - {{ location.city }}, {{ location.country }} + + + ... 和其他 {{ row.locations.length - 5 }} 个地点 + + + N/A + + + + + + + + + {{ row.nctId }} + + + + + + + + {{ row.overallStatus }} + + + + + + + + + + - + - - - - - - - - - - - {{ row.nctId }} - - - - - {{ row.overallStatus }} - - - - {{ row.studyType }} - - - {{ row.phase || 'N/A' }} - - - {{ row.startDate || 'N/A' }} - - - {{ row.completionDate || 'N/A' }} - - - {{ row.enrollment || 'N/A' }} - - - {{ row.sponsor }} - - - - {{ condition }} - - - - - {{ intervention }} - - - - - {{ row.briefSummary || 'N/A' }} - - - - - - {{ location.facility }} - {{ location.city }}, {{ location.country }} - - - ... 和其他 {{ row.locations.length - 5 }} 个地点 - - - N/A - - - - - - - - - {{ row.nctId }} - - - - - - - - {{ row.overallStatus }} - - - - - - - - - - - + + + + + + + + + + 返回列表 @@ -365,12 +284,12 @@
{{ formatJSON(inquiry.searchResults) }}