登陆加密,错误,无法进入系统。

This commit is contained in:
williamWan 2025-10-30 19:10:04 +08:00
parent 442e3a2e57
commit 6fec5781e2
56 changed files with 1379 additions and 736 deletions

8
.idea/.gitignore vendored Normal file
View File

@ -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

9
.idea/Ipsen.iml Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

18
.idea/compiler.xml Normal file
View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile name="Maven default annotation processors profile" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<module name="medical-info-system" />
</profile>
</annotationProcessing>
</component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
<module name="medical-info-system" options="-parameters" />
</option>
</component>
</project>

6
.idea/encodings.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/backend/src/main/java" charset="UTF-8" />
</component>
</project>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

20
.idea/jarRepositories.xml Normal file
View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://repo.maven.apache.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
</component>
</project>

14
.idea/misc.xml Normal file
View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/backend/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/Ipsen.iml" filepath="$PROJECT_DIR$/.idea/Ipsen.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@ -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);
}
}
};
}
}

View File

@ -4,8 +4,6 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 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.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfiguration;
@ -15,6 +13,8 @@ import org.springframework.web.reactive.function.client.WebClient;
import java.util.Arrays; import java.util.Arrays;
import com.ipsen.medical.security.PlainTextPasswordEncoder;
/** /**
* Spring Security配置 * Spring Security配置
*/ */
@ -27,14 +27,12 @@ public class SecurityConfig {
http http
.cors(cors -> cors.configurationSource(corsConfigurationSource())) .cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(csrf -> csrf.disable()) .csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authz -> authz .authorizeHttpRequests(authz -> authz
// 允许所有请求
.anyRequest().permitAll() .anyRequest().permitAll()
) )
.httpBasic(httpBasic -> httpBasic.disable()) .httpBasic(httpBasic -> httpBasic.disable())
.formLogin(formLogin -> formLogin.disable()); .formLogin(formLogin -> formLogin.disable());
return http.build(); return http.build();
} }
@ -53,7 +51,7 @@ public class SecurityConfig {
@Bean @Bean
public PasswordEncoder passwordEncoder() { public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); return new PlainTextPasswordEncoder();
} }
@Bean @Bean
@ -61,4 +59,5 @@ public class SecurityConfig {
return WebClient.builder() return WebClient.builder()
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(10 * 1024 * 1024)); // 10MB buffer .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(10 * 1024 * 1024)); // 10MB buffer
} }
} }

View File

@ -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<ApiResponse<AuthResponse>> login(@RequestBody LoginRequest request) {
Optional<User> 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;
}
}

View File

@ -73,3 +73,7 @@ public class DownloadTaskController {
} }

View File

@ -205,6 +205,17 @@ public class InquiryController {
InquiryRequestDTO result = autoSearchService.executeFullWorkflow(id); InquiryRequestDTO result = autoSearchService.executeFullWorkflow(id);
return ResponseEntity.ok(ApiResponse.success(result)); return ResponseEntity.ok(ApiResponse.success(result));
} }
/**
* 根据选中的检索结果生成下载任务
*/
@PostMapping("/{id}/downloads")
public ResponseEntity<ApiResponse<InquiryRequestDTO>> generateDownloadTasks(
@PathVariable Long id,
@RequestBody List<Long> searchResultItemIds) {
InquiryRequestDTO result = inquiryService.generateDownloadTasks(id, searchResultItemIds);
return ResponseEntity.ok(ApiResponse.success(result));
}
} }

View File

@ -103,3 +103,7 @@ public class SearchResultController {
} }

View File

@ -50,3 +50,7 @@ public class ClinicalTrialDTO {

View File

@ -29,3 +29,7 @@ public class ClinicalTrialsSearchResult {

View File

@ -14,3 +14,7 @@ public class DataSourceSelectionDTO {
} }

View File

@ -15,3 +15,7 @@ public class KeywordConfirmationDTO {
} }

View File

@ -30,3 +30,7 @@ public class ResponseGenerationDTO {
} }

View File

@ -33,3 +33,7 @@ public class SearchResultItemDTO {
} }

View File

@ -20,3 +20,7 @@ public class UserDTO {
} }

View File

@ -71,14 +71,11 @@ public class InquiryRequest {
} }
public enum RequestStatus { public enum RequestStatus {
PENDING, // 待处理 CREATED, // 已创建
KEYWORD_EXTRACTED, // 关键词已提取 SEARCHING, // 检索中提取关键词成功后进入
SEARCHING, // 检索中 SEARCHED, // 已检索有检索结果
SEARCH_COMPLETED, // 检索完成 RESPONDING, // 回复中生成回复后
UNDER_REVIEW, // 审核中 COMPLETED // 已完成邮件发送后人工标记
DOWNLOADING, // 下载文献中
COMPLETED, // 已完成
REJECTED // 已拒绝
} }
} }

View File

@ -111,3 +111,7 @@ public class SearchResultItem {
} }

View File

@ -39,3 +39,7 @@ public interface ClinicalTrialRepository extends JpaRepository<ClinicalTrial, Lo

View File

@ -44,3 +44,7 @@ public interface SearchResultItemRepository extends JpaRepository<SearchResultIt
} }

View File

@ -0,0 +1,24 @@
package com.ipsen.medical.security;
import org.springframework.security.crypto.password.PasswordEncoder;
public class PlainTextPasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence rawPassword) {
return rawPassword == null ? null : rawPassword.toString();
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
String raw = rawPassword == null ? null : rawPassword.toString();
return raw == null ? encodedPassword == null : raw.equals(encodedPassword);
}
@Override
public boolean upgradeEncoding(String encodedPassword) {
return false;
}
}

View File

@ -52,3 +52,7 @@ public interface ClinicalTrialsService {

View File

@ -66,6 +66,11 @@ public interface InquiryService {
* 完成查询请求 * 完成查询请求
*/ */
InquiryRequestDTO completeInquiry(Long id); InquiryRequestDTO completeInquiry(Long id);
/**
* 根据选中的检索结果生成下载任务并更新状态为DOWNLOADING
*/
InquiryRequestDTO generateDownloadTasks(Long id, List<Long> searchResultItemIds);
} }

View File

@ -39,3 +39,7 @@ public interface MultiSourceSearchService {
} }

View File

@ -66,3 +66,7 @@ public interface SearchResultService {
} }

View File

@ -46,3 +46,7 @@ public interface UserService {
} }

View File

@ -85,7 +85,7 @@ public class AutoSearchServiceImpl implements AutoSearchService {
// 5. 更新查询请求 // 5. 更新查询请求
inquiry.setSearchResults(searchResultsJson); inquiry.setSearchResults(searchResultsJson);
inquiry.setStatus(InquiryRequest.RequestStatus.SEARCH_COMPLETED); inquiry.setStatus(InquiryRequest.RequestStatus.SEARCHED);
inquiry.setUpdatedAt(LocalDateTime.now()); inquiry.setUpdatedAt(LocalDateTime.now());
InquiryRequest savedInquiry = inquiryRequestRepository.save(inquiry); InquiryRequest savedInquiry = inquiryRequestRepository.save(inquiry);
@ -114,7 +114,7 @@ public class AutoSearchServiceImpl implements AutoSearchService {
KeywordExtractionResult keywords = difyService.extractKeywordsStructured(inquiry.getInquiryContent()); KeywordExtractionResult keywords = difyService.extractKeywordsStructured(inquiry.getInquiryContent());
String keywordsJson = objectMapper.writeValueAsString(keywords); String keywordsJson = objectMapper.writeValueAsString(keywords);
inquiry.setKeywords(keywordsJson); inquiry.setKeywords(keywordsJson);
inquiry.setStatus(InquiryRequest.RequestStatus.KEYWORD_EXTRACTED); inquiry.setStatus(InquiryRequest.RequestStatus.SEARCHING);
inquiry.setUpdatedAt(LocalDateTime.now()); inquiry.setUpdatedAt(LocalDateTime.now());
inquiryRequestRepository.save(inquiry); inquiryRequestRepository.save(inquiry);
@ -132,7 +132,7 @@ public class AutoSearchServiceImpl implements AutoSearchService {
inquiry.getSearchResults() inquiry.getSearchResults()
); );
inquiry.setResponseContent(response); inquiry.setResponseContent(response);
inquiry.setStatus(InquiryRequest.RequestStatus.UNDER_REVIEW); inquiry.setStatus(InquiryRequest.RequestStatus.RESPONDING);
inquiry.setUpdatedAt(LocalDateTime.now()); inquiry.setUpdatedAt(LocalDateTime.now());
inquiryRequestRepository.save(inquiry); inquiryRequestRepository.save(inquiry);
} catch (Exception e) { } catch (Exception e) {

View File

@ -13,7 +13,6 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@ -31,6 +30,9 @@ public class DifyServiceImpl implements DifyService {
@Value("${app.dify.api-key}") @Value("${app.dify.api-key}")
private String difyApiKey; private String difyApiKey;
@Value("${app.dify.app-type:workflow}")
private String difyAppType;
private final WebClient webClient; private final WebClient webClient;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
@ -46,50 +48,79 @@ public class DifyServiceImpl implements DifyService {
public KeywordExtractionResult extractKeywordsStructured(String content) { public KeywordExtractionResult extractKeywordsStructured(String content) {
log.info("Extracting keywords structured from 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 { try {
String response = callDifyAPI(content, "extract_keywords"); String response = callDifyAPI(content, "extract_keywords");
// 解析响应获取结构化数据 // 解析响应获取结构化数据兼容 Workflow Chat 返回结构
JsonNode rootNode = objectMapper.readTree(response); JsonNode rootNode = objectMapper.readTree(response);
JsonNode answerNode = rootNode.path("answer"); KeywordExtractionResult result = new KeywordExtractionResult();
String answer; boolean mapped = false;
if (answerNode.isTextual()) {
answer = answerNode.asText(); // 优先Workflow 返回一般为 { data: { outputs: { ... } } } { data: { ... } }
} else { if (rootNode.has("data")) {
answer = answerNode.toString(); 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 // 兼容 Chat 返回通常 { answer: "...可能是JSON或文本...", metadata: {...} }
KeywordExtractionResult result; if (!mapped && rootNode.has("answer")) {
try { JsonNode answerNode = rootNode.get("answer");
// 如果answer是JSON格式 String answer = answerNode.isTextual() ? answerNode.asText() : answerNode.toString();
JsonNode resultNode = objectMapper.readTree(answer);
result = new KeywordExtractionResult(); try {
result.setDrugNameChinese(resultNode.path("drugNameChinese").asText(null)); JsonNode resultNode = objectMapper.readTree(answer);
result.setDrugNameEnglish(resultNode.path("drugNameEnglish").asText(null)); result.setDrugNameChinese(resultNode.path("drugNameChinese").asText(null));
result.setRequestItem(resultNode.path("requestItem").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")) { if (rootNode.has("metadata")) {
JsonNode metadata = rootNode.get("metadata"); JsonNode metadata = rootNode.get("metadata");
if (metadata.has("confidence")) { if (metadata.has("confidence")) {
result.setConfidence(metadata.get("confidence").asDouble()); 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={}", log.info("Extracted keywords: Chinese={}, English={}, Item={}",
result.getDrugNameChinese(), result.getDrugNameEnglish(), result.getRequestItem()); result.getDrugNameChinese(), result.getDrugNameEnglish(), result.getRequestItem());
@ -143,50 +174,28 @@ public class DifyServiceImpl implements DifyService {
@Override @Override
public ResponseGenerationDTO generateStructuredResponse(String inquiryContent, String selectedMaterials) { public ResponseGenerationDTO generateStructuredResponse(String inquiryContent, String selectedMaterials) {
log.info("Generating structured response"); log.info("Generating structured response");
try { try {
String prompt = String.format( String combined = String.format(
"请基于以下查询问题和检索到的资料生成标准化回复。\n\n" + "查询内容:%s\n\n选中资料%s",
"查询问题:%s\n\n" + inquiryContent, selectedMaterials
"检索到的资料:%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 response = callDifyAPI(combined, "generate_structured_response");
String response = callDifyAPI(prompt, "generate_structured_response");
// 解析响应
JsonNode rootNode = objectMapper.readTree(response); JsonNode rootNode = objectMapper.readTree(response);
JsonNode answerNode = rootNode.path("answer"); JsonNode answerNode = rootNode.path("answer");
String answerText = answerNode.isTextual() ? answerNode.asText() : answerNode.toString();
String answer;
if (answerNode.isTextual()) { // 尝试直接将 answer 反序列化为结构化对象
answer = answerNode.asText(); try {
} else { return objectMapper.readValue(answerText, ResponseGenerationDTO.class);
answer = answerNode.toString(); } 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) { } catch (Exception e) {
log.error("Error generating structured response: ", e); log.error("Error generating structured response: ", e);
throw new RuntimeException("Failed to generate structured response: " + e.getMessage(), 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) { private String callDifyAPI(String content, String taskType) {
try { try {
log.info("Calling Dify API: url={}, taskType={}", difyApiUrl, taskType); log.info("Calling Dify API: baseUrl={}, appType={}, taskType={}", difyApiUrl, difyAppType, taskType);
log.info("API Key: {}", difyApiKey != null ? difyApiKey.substring(0, Math.min(10, difyApiKey.length())) + "..." : "null");
// 构建 inputs
// 构建请求
DifyRequest request = new DifyRequest();
request.setQuery(content);
request.setResponseMode("blocking");
request.setUser("medical-info-system");
// 添加任务类型到inputs
Map<String, Object> inputs = new HashMap<>(); Map<String, Object> inputs = new HashMap<>();
inputs.put("task_type", taskType); inputs.put("task_type", taskType);
inputs.put("content", content); // Dify 工作流要求的输入参数名值来自查询详情的 inquiry_content
request.setInputs(inputs); inputs.put("Inquery_request", content);
log.debug("Dify API request: {}", objectMapper.writeValueAsString(request)); String endpoint;
Object body;
// 调用API
if ("workflow".equalsIgnoreCase(difyAppType)) {
// Workflow 应用
Map<String, Object> 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() String response = webClient.post()
.uri(difyApiUrl + "/chat-messages") .uri(difyApiUrl + endpoint)
.header(HttpHeaders.AUTHORIZATION, "Bearer " + difyApiKey) .header(HttpHeaders.AUTHORIZATION, "Bearer " + difyApiKey)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.bodyValue(request) .bodyValue(body)
.retrieve() .retrieve()
.onStatus(status -> status.is4xxClientError() || status.is5xxServerError(), .onStatus(status -> status.is4xxClientError() || status.is5xxServerError(),
clientResponse -> { clientResponse -> clientResponse.bodyToMono(String.class).map(errBody -> {
log.error("Dify API error: status={}, headers={}", String msg = String.format("Dify API error %s: %s", clientResponse.statusCode(), errBody);
clientResponse.statusCode(), clientResponse.headers().asHttpHeaders()); log.error(msg);
return clientResponse.bodyToMono(String.class) return new RuntimeException(msg);
.flatMap(body -> { }))
log.error("Dify API error body: {}", body);
return Mono.error(new RuntimeException("Dify API error: " + clientResponse.statusCode() + " - " + body));
});
})
.bodyToMono(String.class) .bodyToMono(String.class)
.block(); .block();

View File

@ -7,7 +7,9 @@ import com.ipsen.medical.dto.KeywordConfirmationDTO;
import com.ipsen.medical.dto.ResponseGenerationDTO; import com.ipsen.medical.dto.ResponseGenerationDTO;
import com.ipsen.medical.dto.SearchResultItemDTO; import com.ipsen.medical.dto.SearchResultItemDTO;
import com.ipsen.medical.entity.InquiryRequest; import com.ipsen.medical.entity.InquiryRequest;
import com.ipsen.medical.entity.AuditLog;
import com.ipsen.medical.repository.InquiryRequestRepository; import com.ipsen.medical.repository.InquiryRequestRepository;
import com.ipsen.medical.repository.AuditLogRepository;
import com.ipsen.medical.service.InquiryService; import com.ipsen.medical.service.InquiryService;
import com.ipsen.medical.service.ExcelParserService; import com.ipsen.medical.service.ExcelParserService;
import com.ipsen.medical.service.DifyService; import com.ipsen.medical.service.DifyService;
@ -44,6 +46,24 @@ public class InquiryServiceImpl implements InquiryService {
private final ObjectMapper objectMapper = new ObjectMapper(); 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 @Override
public InquiryRequestDTO uploadInquiry(MultipartFile file) { public InquiryRequestDTO uploadInquiry(MultipartFile file) {
try { try {
@ -65,7 +85,7 @@ public class InquiryServiceImpl implements InquiryService {
inquiryRequest.setCustomerEmail(inquiryRequestDTO.getCustomerEmail()); inquiryRequest.setCustomerEmail(inquiryRequestDTO.getCustomerEmail());
inquiryRequest.setCustomerTitle(inquiryRequestDTO.getCustomerTitle()); inquiryRequest.setCustomerTitle(inquiryRequestDTO.getCustomerTitle());
inquiryRequest.setInquiryContent(inquiryRequestDTO.getInquiryContent()); inquiryRequest.setInquiryContent(inquiryRequestDTO.getInquiryContent());
inquiryRequest.setStatus(InquiryRequest.RequestStatus.PENDING); inquiryRequest.setStatus(InquiryRequest.RequestStatus.CREATED);
inquiryRequest.setCreatedAt(LocalDateTime.now()); inquiryRequest.setCreatedAt(LocalDateTime.now());
InquiryRequest savedRequest = inquiryRequestRepository.save(inquiryRequest); InquiryRequest savedRequest = inquiryRequestRepository.save(inquiryRequest);
@ -102,15 +122,19 @@ public class InquiryServiceImpl implements InquiryService {
public InquiryRequestDTO extractKeywords(Long id) { public InquiryRequestDTO extractKeywords(Long id) {
InquiryRequest inquiryRequest = inquiryRequestRepository.findById(id) InquiryRequest inquiryRequest = inquiryRequestRepository.findById(id)
.orElseThrow(() -> new RuntimeException("查询请求不存在: " + id)); .orElseThrow(() -> new RuntimeException("查询请求不存在: " + id));
// 已创建或已检索后允许重新提取关键词进入检索中
assertStatus(inquiryRequest, InquiryRequest.RequestStatus.CREATED, InquiryRequest.RequestStatus.SEARCHED);
try { try {
// 使用Dify服务提取关键词 // 使用Dify服务提取关键词
String keywords = difyService.extractKeywords(inquiryRequest.getInquiryContent()); String keywords = difyService.extractKeywords(inquiryRequest.getInquiryContent());
inquiryRequest.setKeywords(keywords); inquiryRequest.setKeywords(keywords);
inquiryRequest.setStatus(InquiryRequest.RequestStatus.KEYWORD_EXTRACTED); inquiryRequest.setKeywordsConfirmed(false);
inquiryRequest.setStatus(InquiryRequest.RequestStatus.SEARCHING);
inquiryRequest.setUpdatedAt(LocalDateTime.now()); inquiryRequest.setUpdatedAt(LocalDateTime.now());
InquiryRequest savedRequest = inquiryRequestRepository.save(inquiryRequest); InquiryRequest savedRequest = inquiryRequestRepository.save(inquiryRequest);
logAudit(savedRequest, AuditLog.AuditAction.SUBMITTED, "关键词已提取");
return convertToDTO(savedRequest); return convertToDTO(savedRequest);
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException("关键词提取失败: " + e.getMessage(), e); throw new RuntimeException("关键词提取失败: " + e.getMessage(), e);
@ -121,14 +145,20 @@ public class InquiryServiceImpl implements InquiryService {
public InquiryRequestDTO performSearch(Long id) { public InquiryRequestDTO performSearch(Long id) {
InquiryRequest inquiryRequest = inquiryRequestRepository.findById(id) InquiryRequest inquiryRequest = inquiryRequestRepository.findById(id)
.orElseThrow(() -> new RuntimeException("查询请求不存在: " + id)); .orElseThrow(() -> new RuntimeException("查询请求不存在: " + id));
// 仅在检索中触发检索
assertStatus(inquiryRequest, InquiryRequest.RequestStatus.SEARCHING);
try { try {
// 执行多源检索 // 执行多源检索
inquiryRequest.setStatus(InquiryRequest.RequestStatus.SEARCHING);
inquiryRequest.setUpdatedAt(LocalDateTime.now());
inquiryRequestRepository.save(inquiryRequest);
multiSourceSearchService.performMultiSourceSearch(id); multiSourceSearchService.performMultiSourceSearch(id);
// 重新加载更新后的请求 // 重新加载更新后的请求
inquiryRequest = inquiryRequestRepository.findById(id) inquiryRequest = inquiryRequestRepository.findById(id)
.orElseThrow(() -> new RuntimeException("查询请求不存在: " + id)); .orElseThrow(() -> new RuntimeException("查询请求不存在: " + id));
logAudit(inquiryRequest, AuditLog.AuditAction.SUBMITTED, "检索完成");
return convertToDTO(inquiryRequest); return convertToDTO(inquiryRequest);
} catch (Exception e) { } catch (Exception e) {
@ -140,6 +170,8 @@ public class InquiryServiceImpl implements InquiryService {
public InquiryRequestDTO generateResponse(Long id) { public InquiryRequestDTO generateResponse(Long id) {
InquiryRequest inquiryRequest = inquiryRequestRepository.findById(id) InquiryRequest inquiryRequest = inquiryRequestRepository.findById(id)
.orElseThrow(() -> new RuntimeException("查询请求不存在: " + id)); .orElseThrow(() -> new RuntimeException("查询请求不存在: " + id));
// 仅在已检索状态生成回复
assertStatus(inquiryRequest, InquiryRequest.RequestStatus.SEARCHED);
try { try {
// 获取用户选中的检索结果includeInResponse = true // 获取用户选中的检索结果includeInResponse = true
@ -164,10 +196,11 @@ public class InquiryServiceImpl implements InquiryService {
String responseContent = objectMapper.writeValueAsString(structuredResponse); String responseContent = objectMapper.writeValueAsString(structuredResponse);
inquiryRequest.setResponseContent(responseContent); inquiryRequest.setResponseContent(responseContent);
inquiryRequest.setStatus(InquiryRequest.RequestStatus.UNDER_REVIEW); inquiryRequest.setStatus(InquiryRequest.RequestStatus.RESPONDING);
inquiryRequest.setUpdatedAt(LocalDateTime.now()); inquiryRequest.setUpdatedAt(LocalDateTime.now());
InquiryRequest savedRequest = inquiryRequestRepository.save(inquiryRequest); InquiryRequest savedRequest = inquiryRequestRepository.save(inquiryRequest);
logAudit(savedRequest, AuditLog.AuditAction.SUBMITTED, "已生成结构化回复");
return convertToDTO(savedRequest); return convertToDTO(savedRequest);
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException("回复生成失败: " + e.getMessage(), 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) { public InquiryRequestDTO reviewResponse(Long id, Boolean approved, String comments) {
InquiryRequest inquiryRequest = inquiryRequestRepository.findById(id) InquiryRequest inquiryRequest = inquiryRequestRepository.findById(id)
.orElseThrow(() -> new RuntimeException("查询请求不存在: " + id)); .orElseThrow(() -> new RuntimeException("查询请求不存在: " + id));
// 审核仅在回复中允许
assertStatus(inquiryRequest, InquiryRequest.RequestStatus.RESPONDING);
// 审核结果仅记录不改变整体生命周期完成由人工标记
if (approved) { if (approved) {
inquiryRequest.setStatus(InquiryRequest.RequestStatus.COMPLETED); logAudit(inquiryRequest, AuditLog.AuditAction.APPROVED, comments);
inquiryRequest.setCompletedAt(LocalDateTime.now());
} else { } else {
inquiryRequest.setStatus(InquiryRequest.RequestStatus.REJECTED); logAudit(inquiryRequest, AuditLog.AuditAction.REJECTED, comments);
} }
inquiryRequest.setUpdatedAt(LocalDateTime.now()); inquiryRequest.setUpdatedAt(LocalDateTime.now());
@ -195,12 +230,14 @@ public class InquiryServiceImpl implements InquiryService {
public InquiryRequestDTO completeInquiry(Long id) { public InquiryRequestDTO completeInquiry(Long id) {
InquiryRequest inquiryRequest = inquiryRequestRepository.findById(id) InquiryRequest inquiryRequest = inquiryRequestRepository.findById(id)
.orElseThrow(() -> new RuntimeException("查询请求不存在: " + id)); .orElseThrow(() -> new RuntimeException("查询请求不存在: " + id));
// 仅在回复中可标记完成
assertStatus(inquiryRequest, InquiryRequest.RequestStatus.RESPONDING);
inquiryRequest.setStatus(InquiryRequest.RequestStatus.COMPLETED); inquiryRequest.setStatus(InquiryRequest.RequestStatus.COMPLETED);
inquiryRequest.setCompletedAt(LocalDateTime.now()); inquiryRequest.setCompletedAt(LocalDateTime.now());
inquiryRequest.setUpdatedAt(LocalDateTime.now()); inquiryRequest.setUpdatedAt(LocalDateTime.now());
InquiryRequest savedRequest = inquiryRequestRepository.save(inquiryRequest); InquiryRequest savedRequest = inquiryRequestRepository.save(inquiryRequest);
logAudit(savedRequest, AuditLog.AuditAction.COMPLETED, "处理完成");
return convertToDTO(savedRequest); return convertToDTO(savedRequest);
} }
@ -208,15 +245,20 @@ public class InquiryServiceImpl implements InquiryService {
public InquiryRequestDTO confirmKeywords(Long id, KeywordConfirmationDTO keywordDTO) { public InquiryRequestDTO confirmKeywords(Long id, KeywordConfirmationDTO keywordDTO) {
InquiryRequest inquiryRequest = inquiryRequestRepository.findById(id) InquiryRequest inquiryRequest = inquiryRequestRepository.findById(id)
.orElseThrow(() -> new RuntimeException("查询请求不存在: " + id)); .orElseThrow(() -> new RuntimeException("查询请求不存在: " + id));
// 仅在检索中允许确认关键词如果保留该步骤
assertStatus(inquiryRequest, InquiryRequest.RequestStatus.SEARCHING);
try { try {
// 将用户确认的关键词保存为JSON // 将用户确认的关键词保存为JSON
String keywordsJson = objectMapper.writeValueAsString(keywordDTO); String keywordsJson = objectMapper.writeValueAsString(keywordDTO);
inquiryRequest.setKeywords(keywordsJson); inquiryRequest.setKeywords(keywordsJson);
inquiryRequest.setKeywordsConfirmed(true); inquiryRequest.setKeywordsConfirmed(true);
// 确认后仍保持检索中
inquiryRequest.setStatus(InquiryRequest.RequestStatus.SEARCHING);
inquiryRequest.setUpdatedAt(LocalDateTime.now()); inquiryRequest.setUpdatedAt(LocalDateTime.now());
InquiryRequest savedRequest = inquiryRequestRepository.save(inquiryRequest); InquiryRequest savedRequest = inquiryRequestRepository.save(inquiryRequest);
logAudit(savedRequest, AuditLog.AuditAction.SUBMITTED, "关键词已确认");
return convertToDTO(savedRequest); return convertToDTO(savedRequest);
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException("关键词确认失败: " + e.getMessage(), e); throw new RuntimeException("关键词确认失败: " + e.getMessage(), e);
@ -227,6 +269,8 @@ public class InquiryServiceImpl implements InquiryService {
public InquiryRequestDTO selectDataSources(Long id, DataSourceSelectionDTO dataSourceDTO) { public InquiryRequestDTO selectDataSources(Long id, DataSourceSelectionDTO dataSourceDTO) {
InquiryRequest inquiryRequest = inquiryRequestRepository.findById(id) InquiryRequest inquiryRequest = inquiryRequestRepository.findById(id)
.orElseThrow(() -> new RuntimeException("查询请求不存在: " + id)); .orElseThrow(() -> new RuntimeException("查询请求不存在: " + id));
// 选择数据源需在检索中
assertStatus(inquiryRequest, InquiryRequest.RequestStatus.SEARCHING);
// 保存用户选择的数据源 // 保存用户选择的数据源
inquiryRequest.setSearchInternalData(dataSourceDTO.getSearchInternalData()); inquiryRequest.setSearchInternalData(dataSourceDTO.getSearchInternalData());
@ -236,6 +280,7 @@ public class InquiryServiceImpl implements InquiryService {
inquiryRequest.setUpdatedAt(LocalDateTime.now()); inquiryRequest.setUpdatedAt(LocalDateTime.now());
InquiryRequest savedRequest = inquiryRequestRepository.save(inquiryRequest); InquiryRequest savedRequest = inquiryRequestRepository.save(inquiryRequest);
logAudit(savedRequest, AuditLog.AuditAction.SUBMITTED, "已选择数据源");
return convertToDTO(savedRequest); return convertToDTO(savedRequest);
} }
@ -272,4 +317,30 @@ public class InquiryServiceImpl implements InquiryService {
dto.setCompletedAt(inquiryRequest.getCompletedAt()); dto.setCompletedAt(inquiryRequest.getCompletedAt());
return dto; return dto;
} }
@Override
public InquiryRequestDTO generateDownloadTasks(Long id, List<Long> 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);
}
} }

View File

@ -85,8 +85,8 @@ public class MultiSourceSearchServiceImpl implements MultiSourceSearchService {
allResults.addAll(ctResults); allResults.addAll(ctResults);
} }
// 更新查询请求状态 // 更新查询请求状态为已检索
inquiry.setStatus(InquiryRequest.RequestStatus.SEARCH_COMPLETED); inquiry.setStatus(InquiryRequest.RequestStatus.SEARCHED);
inquiryRequestRepository.save(inquiry); inquiryRequestRepository.save(inquiry);
log.info("Multi-source search completed, found {} results", allResults.size()); log.info("Multi-source search completed, found {} results", allResults.size());

View File

@ -199,3 +199,7 @@ public class SearchResultServiceImpl implements SearchResultService {
} }

View File

@ -37,8 +37,10 @@ app:
# Dify配置 # Dify配置
dify: dify:
api-url: ${DIFY_API_URL:https://api.dify.ai/v1} api-url: https://api.dify.ai/v1
api-key: ${DIFY_API_KEY:your-dify-api-key-here} api-key: app-croZF0SSV5fiyXbK3neGrOT6
# 可选: workflow 或 chat
app-type: workflow
# 大模型配置 # 大模型配置
llm: llm:

View File

@ -215,3 +215,7 @@ SELECT '药物模块初始化完成!' AS message,

View File

@ -21,7 +21,7 @@
"eslint": "^8.55.0", "eslint": "^8.55.0",
"eslint-plugin-vue": "^9.19.2", "eslint-plugin-vue": "^9.19.2",
"sass": "^1.69.5", "sass": "^1.69.5",
"vite": "^5.0.0" "vite": "^6.4.1"
} }
}, },
"node_modules/@babel/helper-string-parser": { "node_modules/@babel/helper-string-parser": {
@ -89,9 +89,9 @@
} }
}, },
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5", "version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz",
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", "integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@ -102,13 +102,13 @@
"aix" "aix"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/android-arm": { "node_modules/@esbuild/android-arm": {
"version": "0.21.5", "version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.11.tgz",
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", "integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -119,13 +119,13 @@
"android" "android"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/android-arm64": { "node_modules/@esbuild/android-arm64": {
"version": "0.21.5", "version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz",
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", "integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -136,13 +136,13 @@
"android" "android"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/android-x64": { "node_modules/@esbuild/android-x64": {
"version": "0.21.5", "version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.11.tgz",
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", "integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -153,13 +153,13 @@
"android" "android"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/darwin-arm64": { "node_modules/@esbuild/darwin-arm64": {
"version": "0.21.5", "version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz",
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", "integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -170,13 +170,13 @@
"darwin" "darwin"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/darwin-x64": { "node_modules/@esbuild/darwin-x64": {
"version": "0.21.5", "version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz",
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", "integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -187,13 +187,13 @@
"darwin" "darwin"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/freebsd-arm64": { "node_modules/@esbuild/freebsd-arm64": {
"version": "0.21.5", "version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz",
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", "integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -204,13 +204,13 @@
"freebsd" "freebsd"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/freebsd-x64": { "node_modules/@esbuild/freebsd-x64": {
"version": "0.21.5", "version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz",
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", "integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -221,13 +221,13 @@
"freebsd" "freebsd"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-arm": { "node_modules/@esbuild/linux-arm": {
"version": "0.21.5", "version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz",
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", "integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -238,13 +238,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-arm64": { "node_modules/@esbuild/linux-arm64": {
"version": "0.21.5", "version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz",
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", "integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -255,13 +255,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-ia32": { "node_modules/@esbuild/linux-ia32": {
"version": "0.21.5", "version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz",
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", "integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -272,13 +272,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-loong64": { "node_modules/@esbuild/linux-loong64": {
"version": "0.21.5", "version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz",
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", "integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@ -289,13 +289,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-mips64el": { "node_modules/@esbuild/linux-mips64el": {
"version": "0.21.5", "version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz",
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", "integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==",
"cpu": [ "cpu": [
"mips64el" "mips64el"
], ],
@ -306,13 +306,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-ppc64": { "node_modules/@esbuild/linux-ppc64": {
"version": "0.21.5", "version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz",
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", "integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@ -323,13 +323,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-riscv64": { "node_modules/@esbuild/linux-riscv64": {
"version": "0.21.5", "version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz",
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", "integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@ -340,13 +340,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-s390x": { "node_modules/@esbuild/linux-s390x": {
"version": "0.21.5", "version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz",
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", "integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@ -357,13 +357,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-x64": { "node_modules/@esbuild/linux-x64": {
"version": "0.21.5", "version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz",
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", "integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -374,13 +374,30 @@
"linux" "linux"
], ],
"engines": { "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": { "node_modules/@esbuild/netbsd-x64": {
"version": "0.21.5", "version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz",
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", "integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -391,13 +408,30 @@
"netbsd" "netbsd"
], ],
"engines": { "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": { "node_modules/@esbuild/openbsd-x64": {
"version": "0.21.5", "version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz",
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", "integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -408,13 +442,30 @@
"openbsd" "openbsd"
], ],
"engines": { "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": { "node_modules/@esbuild/sunos-x64": {
"version": "0.21.5", "version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz",
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", "integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -425,13 +476,13 @@
"sunos" "sunos"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/win32-arm64": { "node_modules/@esbuild/win32-arm64": {
"version": "0.21.5", "version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz",
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", "integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -442,13 +493,13 @@
"win32" "win32"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/win32-ia32": { "node_modules/@esbuild/win32-ia32": {
"version": "0.21.5", "version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz",
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", "integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -459,13 +510,13 @@
"win32" "win32"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/win32-x64": { "node_modules/@esbuild/win32-x64": {
"version": "0.21.5", "version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz",
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", "integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -476,7 +527,7 @@
"win32" "win32"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@eslint-community/eslint-utils": { "node_modules/@eslint-community/eslint-utils": {
@ -1897,9 +1948,9 @@
} }
}, },
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.21.5", "version": "0.25.11",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz",
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
@ -1907,32 +1958,35 @@
"esbuild": "bin/esbuild" "esbuild": "bin/esbuild"
}, },
"engines": { "engines": {
"node": ">=12" "node": ">=18"
}, },
"optionalDependencies": { "optionalDependencies": {
"@esbuild/aix-ppc64": "0.21.5", "@esbuild/aix-ppc64": "0.25.11",
"@esbuild/android-arm": "0.21.5", "@esbuild/android-arm": "0.25.11",
"@esbuild/android-arm64": "0.21.5", "@esbuild/android-arm64": "0.25.11",
"@esbuild/android-x64": "0.21.5", "@esbuild/android-x64": "0.25.11",
"@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-arm64": "0.25.11",
"@esbuild/darwin-x64": "0.21.5", "@esbuild/darwin-x64": "0.25.11",
"@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-arm64": "0.25.11",
"@esbuild/freebsd-x64": "0.21.5", "@esbuild/freebsd-x64": "0.25.11",
"@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm": "0.25.11",
"@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-arm64": "0.25.11",
"@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-ia32": "0.25.11",
"@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-loong64": "0.25.11",
"@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-mips64el": "0.25.11",
"@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-ppc64": "0.25.11",
"@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-riscv64": "0.25.11",
"@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-s390x": "0.25.11",
"@esbuild/linux-x64": "0.21.5", "@esbuild/linux-x64": "0.25.11",
"@esbuild/netbsd-x64": "0.21.5", "@esbuild/netbsd-arm64": "0.25.11",
"@esbuild/openbsd-x64": "0.21.5", "@esbuild/netbsd-x64": "0.25.11",
"@esbuild/sunos-x64": "0.21.5", "@esbuild/openbsd-arm64": "0.25.11",
"@esbuild/win32-arm64": "0.21.5", "@esbuild/openbsd-x64": "0.25.11",
"@esbuild/win32-ia32": "0.21.5", "@esbuild/openharmony-arm64": "0.25.11",
"@esbuild/win32-x64": "0.21.5" "@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": { "node_modules/escape-string-regexp": {
@ -3262,6 +3316,54 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/to-regex-range": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@ -3320,21 +3422,24 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "5.4.21", "version": "6.4.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"esbuild": "^0.21.3", "esbuild": "^0.25.0",
"postcss": "^8.4.43", "fdir": "^6.4.4",
"rollup": "^4.20.0" "picomatch": "^4.0.2",
"postcss": "^8.5.3",
"rollup": "^4.34.9",
"tinyglobby": "^0.2.13"
}, },
"bin": { "bin": {
"vite": "bin/vite.js" "vite": "bin/vite.js"
}, },
"engines": { "engines": {
"node": "^18.0.0 || >=20.0.0" "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
}, },
"funding": { "funding": {
"url": "https://github.com/vitejs/vite?sponsor=1" "url": "https://github.com/vitejs/vite?sponsor=1"
@ -3343,19 +3448,25 @@
"fsevents": "~2.3.3" "fsevents": "~2.3.3"
}, },
"peerDependencies": { "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": "*", "less": "*",
"lightningcss": "^1.21.0", "lightningcss": "^1.21.0",
"sass": "*", "sass": "*",
"sass-embedded": "*", "sass-embedded": "*",
"stylus": "*", "stylus": "*",
"sugarss": "*", "sugarss": "*",
"terser": "^5.4.0" "terser": "^5.16.0",
"tsx": "^4.8.1",
"yaml": "^2.4.2"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@types/node": { "@types/node": {
"optional": true "optional": true
}, },
"jiti": {
"optional": true
},
"less": { "less": {
"optional": true "optional": true
}, },
@ -3376,9 +3487,46 @@
}, },
"terser": { "terser": {
"optional": true "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": { "node_modules/vue": {
"version": "3.5.22", "version": "3.5.22",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.22.tgz", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.22.tgz",

View File

@ -9,23 +9,19 @@
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore" "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
}, },
"dependencies": { "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", "@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": { "devDependencies": {
"@vitejs/plugin-vue": "^5.0.0", "@vitejs/plugin-vue": "^5.0.0",
"vite": "^5.0.0",
"eslint": "^8.55.0", "eslint": "^8.55.0",
"eslint-plugin-vue": "^9.19.2", "eslint-plugin-vue": "^9.19.2",
"sass": "^1.69.5" "sass": "^1.69.5",
"vite": "^6.4.1"
} }
} }

11
frontend/src/api/auth.js Normal file
View File

@ -0,0 +1,11 @@
import request from '@/utils/request'
export function login(data) {
return request({
url: '/auth/login',
method: 'post',
data
})
}

View File

@ -59,12 +59,13 @@ export function extractKeywords(id) {
} }
/** /**
* 执行检索 * 执行检索可携带查询表达式与检索范围
*/ */
export function performSearch(id) { export function performSearch(id, data) {
return request({ return request({
url: `/inquiries/${id}/search`, 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
})
}

View File

@ -107,3 +107,7 @@ export function markDownloadFailed(id, reason) {
} }

View File

@ -144,19 +144,9 @@ const router = createRouter({
routes routes
}) })
// 路由守卫 // 放开路由守卫(后端已不需要登录)
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token') next()
if (to.path === '/login') {
next()
} else {
if (!token) {
next('/login')
} else {
next()
}
}
}) })
export default router export default router

View File

@ -9,10 +9,7 @@ const service = axios.create({
// 请求拦截器 // 请求拦截器
service.interceptors.request.use( service.interceptors.request.use(
config => { config => {
const token = localStorage.getItem('token') // 取消附加 Authorization 头,后端已不需要鉴权
if (token) {
config.headers['Authorization'] = `Bearer ${token}`
}
return config return config
}, },
error => { error => {

View File

@ -45,6 +45,7 @@
import { ref, reactive } from 'vue' import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { login as apiLogin } from '@/api/auth'
const router = useRouter() const router = useRouter()
const loginFormRef = ref(null) const loginFormRef = ref(null)
@ -68,23 +69,19 @@ const rules = {
const handleLogin = async () => { const handleLogin = async () => {
if (!loginFormRef.value) return if (!loginFormRef.value) return
await loginFormRef.value.validate((valid) => { await loginFormRef.value.validate(async (valid) => {
if (valid) { if (!valid) return
loading.value = true loading.value = true
try {
// TODO: API const resp = await apiLogin({ username: loginForm.username, password: loginForm.password })
// 使 localStorage.setItem('token', resp.token)
setTimeout(() => { localStorage.setItem('username', resp.username)
if (loginForm.username === 'admin' && loginForm.password === 'admin123') { ElMessage.success('登录成功')
localStorage.setItem('token', 'mock-token-' + Date.now()) router.push('/dashboard')
localStorage.setItem('username', loginForm.username) } catch (e) {
ElMessage.success('登录成功') //
router.push('/dashboard') } finally {
} else { loading.value = false
ElMessage.error('用户名或密码错误')
}
loading.value = false
}, 1000)
} }
}) })
} }

View File

@ -435,3 +435,7 @@ const goBack = () => {
</style> </style>

View File

@ -482,3 +482,7 @@ const goBack = () => {
</style> </style>

View File

@ -43,15 +43,15 @@
style="max-width: 600px;" style="max-width: 600px;"
> >
<el-form-item label="客户姓名" prop="customerName"> <el-form-item label="客户姓名" prop="customerName">
<el-input v-model="form.customerName" placeholder="请输入客户姓名" /> <el-input v-model="form.customerName" placeholder="请输入客户姓名(可选)" />
</el-form-item> </el-form-item>
<el-form-item label="客户邮箱" prop="customerEmail"> <el-form-item label="客户邮箱" prop="customerEmail">
<el-input v-model="form.customerEmail" placeholder="请输入客户邮箱" /> <el-input v-model="form.customerEmail" placeholder="请输入客户邮箱(可选)" />
</el-form-item> </el-form-item>
<el-form-item label="客户职称" prop="customerTitle"> <el-form-item label="客户职称" prop="customerTitle">
<el-input v-model="form.customerTitle" placeholder="请输入客户职称" /> <el-input v-model="form.customerTitle" placeholder="请输入客户职称(可选)" />
</el-form-item> </el-form-item>
<el-form-item label="查询内容" prop="inquiryContent"> <el-form-item label="查询内容" prop="inquiryContent">
@ -59,12 +59,12 @@
v-model="form.inquiryContent" v-model="form.inquiryContent"
type="textarea" type="textarea"
:rows="8" :rows="8"
placeholder="请输入查询内容" placeholder="请输入查询内容(必填)"
/> />
</el-form-item> </el-form-item>
<el-form-item label="指派给" prop="assignedTo"> <el-form-item label="指派给" prop="assignedTo">
<el-input v-model="form.assignedTo" placeholder="请输入指派人员" /> <el-input v-model="form.assignedTo" placeholder="请输入指派人员(可选)" />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
@ -103,14 +103,8 @@ const form = ref({
assignedTo: '' assignedTo: ''
}) })
//
const rules = { const rules = {
customerName: [
{ required: true, message: '请输入客户姓名', trigger: 'blur' }
],
customerEmail: [
{ required: true, message: '请输入客户邮箱', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
],
inquiryContent: [ inquiryContent: [
{ required: true, message: '请输入查询内容', trigger: 'blur' } { required: true, message: '请输入查询内容', trigger: 'blur' }
] ]

File diff suppressed because it is too large Load Diff

View File

@ -15,12 +15,10 @@
<el-form-item label="状态"> <el-form-item label="状态">
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable> <el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
<el-option label="全部" value="" /> <el-option label="全部" value="" />
<el-option label="待处理" value="PENDING" /> <el-option label="已创建" value="CREATED" />
<el-option label="已提取关键词" value="KEYWORD_EXTRACTED" />
<el-option label="检索中" value="SEARCHING" /> <el-option label="检索中" value="SEARCHING" />
<el-option label="检索完成" value="SEARCH_COMPLETED" /> <el-option label="已检索" value="SEARCHED" />
<el-option label="审核中" value="UNDER_REVIEW" /> <el-option label="回复中" value="RESPONDING" />
<el-option label="下载中" value="DOWNLOADING" />
<el-option label="已完成" value="COMPLETED" /> <el-option label="已完成" value="COMPLETED" />
</el-select> </el-select>
</el-form-item> </el-form-item>
@ -144,28 +142,22 @@ const processInquiry = (id) => {
const getStatusType = (status) => { const getStatusType = (status) => {
const typeMap = { const typeMap = {
'PENDING': 'info', 'CREATED': 'info',
'KEYWORD_EXTRACTED': 'warning',
'SEARCHING': 'warning', 'SEARCHING': 'warning',
'SEARCH_COMPLETED': '', 'SEARCHED': '',
'UNDER_REVIEW': 'warning', 'RESPONDING': 'warning',
'DOWNLOADING': 'warning', 'COMPLETED': 'success'
'COMPLETED': 'success',
'REJECTED': 'danger'
} }
return typeMap[status] || 'info' return typeMap[status] || 'info'
} }
const getStatusText = (status) => { const getStatusText = (status) => {
const textMap = { const textMap = {
'PENDING': '待处理', 'CREATED': '已创建',
'KEYWORD_EXTRACTED': '已提取关键词',
'SEARCHING': '检索中', 'SEARCHING': '检索中',
'SEARCH_COMPLETED': '检索完成', 'SEARCHED': '已检索',
'UNDER_REVIEW': '审核中', 'RESPONDING': '回复中',
'DOWNLOADING': '下载中', 'COMPLETED': '已完成'
'COMPLETED': '已完成',
'REJECTED': '已拒绝'
} }
return textMap[status] || status return textMap[status] || status
} }

View File

@ -336,3 +336,7 @@ const goBack = () => {
</style> </style>

View File

@ -492,3 +492,7 @@ const goBack = () => {
</style> </style>

View File

@ -53,6 +53,13 @@
> >
批量标记下载 批量标记下载
</el-button> </el-button>
<el-button
type="primary"
@click="handleGenerateDownloadTasks"
:disabled="downloadSelectedIds.length === 0"
>
生成下载任务 ({{ downloadSelectedIds.length }})
</el-button>
<el-button <el-button
type="danger" type="danger"
@click="handleBatchDelete" @click="handleBatchDelete"
@ -215,7 +222,7 @@
<script setup> <script setup>
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { getInquiryDetail, generateResponse } from '@/api/inquiry' import { getInquiryDetail, generateResponse, generateDownloadTasks } from '@/api/inquiry'
import { import {
getSearchResults, getSearchResults,
updateSearchResult, updateSearchResult,
@ -232,6 +239,7 @@ const generating = ref(false)
const inquiry = ref({}) const inquiry = ref({})
const results = ref([]) const results = ref([])
const selectedResults = ref([]) const selectedResults = ref([])
const downloadSelectedIds = computed(() => selectedResults.value.filter(r => r.needDownload && !r.isDeleted).map(r => r.id))
const filterType = ref('active') const filterType = ref('active')
const searchText = ref('') const searchText = ref('')
const tableRef = ref(null) const tableRef = ref(null)
@ -395,6 +403,19 @@ const handleBatchDownload = async () => {
ElMessage.error('批量操作失败') ElMessage.error('批量操作失败')
} }
} }
const handleGenerateDownloadTasks = async () => {
if (downloadSelectedIds.value.length === 0) {
ElMessage.warning('请先选择并标记需要下载的条目')
return
}
try {
await generateDownloadTasks(inquiry.value.id, downloadSelectedIds.value)
ElMessage.success('下载任务已生成,状态已更新为“下载中”')
await loadDetail()
} catch (error) {
ElMessage.error('生成下载任务失败')
}
}
const handleBatchDelete = async () => { const handleBatchDelete = async () => {
ElMessageBox.confirm( ElMessageBox.confirm(

6
package-lock.json generated Normal file
View File

@ -0,0 +1,6 @@
{
"name": "Ipsen",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}