bug修复、embedding计算相关逻辑
This commit is contained in:
parent
0621a76051
commit
ca99db4b99
|
|
@ -17,9 +17,10 @@
|
|||
/start-jdk21.sh
|
||||
/PROJECT_README.md
|
||||
/Java后端开发规范.md
|
||||
/*.cmd
|
||||
/CORS_Security_Review.md
|
||||
/MCP_Agent_AI升级计划.md
|
||||
|
||||
/*.ps1
|
||||
/*方案.md
|
||||
/*设计.md
|
||||
/*.py
|
||||
|
|
@ -29,4 +30,8 @@
|
|||
/StandaloneQwenTest.java
|
||||
/QWEN_INTEGRATION_COMPLETE.md
|
||||
/dependencies.txt
|
||||
/AI_REPAIR_INTEGRATION.md
|
||||
/AI_REPAIR_INTEGRATION.md
|
||||
/AI_MANAGEMENT_UI.md
|
||||
/BATCH_API_OPTIMIZATION.md
|
||||
/cookies.txt
|
||||
/embedding-api-commands.md
|
||||
6
pom.xml
6
pom.xml
|
|
@ -64,6 +64,12 @@
|
|||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Thymeleaf Template Engine for AI Management UI -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring AI Core Dependencies for LLM Integration - TEMPORARILY COMMENTED FOR COMPATIBILITY -->
|
||||
<!-- TODO: Need to use Spring Boot 3.x compatible version or find alternative for Spring Boot 2.7.18 -->
|
||||
<!--
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ package com.chinaweal.youfool.devops;
|
|||
// import com.chinaweal.youfool.devops.ai.config.AIRepairIntegrationConfig;
|
||||
import com.chinaweal.youfool.devops.config.ErrorLogProperties;
|
||||
import com.chinaweal.youfool.devops.util.ErrorLogUtils;
|
||||
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
|
|
@ -46,6 +47,9 @@ public class DevOpsApplication extends SpringBootServletInitializer implements A
|
|||
@Autowired
|
||||
@Qualifier("youfoolDS")
|
||||
private DataSource youfoolDataSource;
|
||||
|
||||
@Autowired(required = false)
|
||||
private CircuitBreakerRegistry circuitBreakerRegistry;
|
||||
|
||||
@Override
|
||||
protected SpringApplicationBuilder configure(SpringApplicationBuilder applicationBuilder) {
|
||||
|
|
@ -96,7 +100,17 @@ public class DevOpsApplication extends SpringBootServletInitializer implements A
|
|||
}
|
||||
*/
|
||||
|
||||
log.info("====== AI功能暂时禁用 - 等待Spring Boot 2.7.18兼容性问题解决");
|
||||
// 重置熔断器以确保服务可用
|
||||
if (circuitBreakerRegistry != null) {
|
||||
try {
|
||||
circuitBreakerRegistry.circuitBreaker("qwen-embedding").reset();
|
||||
log.info("====== AI服务熔断器已重置,embedding服务就绪");
|
||||
} catch (Exception e) {
|
||||
log.warn("====== 熔断器重置失败: {}", e.getMessage());
|
||||
}
|
||||
} else {
|
||||
log.info("====== AI功能已启用 - embedding服务和向量化功能可用");
|
||||
}
|
||||
|
||||
// 打印数据库连接信息
|
||||
printDatabaseConnectionInfo();
|
||||
|
|
|
|||
|
|
@ -66,37 +66,78 @@ public class AIAnswerGenerationAspect {
|
|||
String methodName = joinPoint.getSignature().getName();
|
||||
String className = joinPoint.getTarget().getClass().getSimpleName();
|
||||
|
||||
// 获取当前活跃会话
|
||||
WorkflowTraceSession currentSession = workflowTracingService.getCurrentSession();
|
||||
// 安全获取当前活跃会话 - 增强空值检查
|
||||
WorkflowTraceSession currentSession = null;
|
||||
try {
|
||||
currentSession = workflowTracingService != null ? workflowTracingService.getCurrentSession() : null;
|
||||
} catch (Exception e) {
|
||||
log.warn("获取当前会话失败: {}", e.getMessage());
|
||||
}
|
||||
|
||||
// 创建AI回答生成追踪步骤
|
||||
WorkflowTraceStep generationStep = new WorkflowTraceStep(
|
||||
"AI_GENERATION_" + className + "." + methodName,
|
||||
WorkflowTraceStep.StepType.ANSWER_GENERATION
|
||||
);
|
||||
|
||||
if (currentSession != null) {
|
||||
generationStep.setSessionId(currentSession.getSessionId());
|
||||
// 创建AI回答生成追踪步骤 - 确保安全初始化
|
||||
WorkflowTraceStep generationStep = null;
|
||||
try {
|
||||
generationStep = new WorkflowTraceStep(
|
||||
"AI_GENERATION_" + className + "." + methodName,
|
||||
WorkflowTraceStep.StepType.ANSWER_GENERATION
|
||||
);
|
||||
|
||||
if (currentSession != null && currentSession.getSessionId() != null) {
|
||||
generationStep.setSessionId(currentSession.getSessionId());
|
||||
} else {
|
||||
// 为无会话情况设置默认值
|
||||
generationStep.setSessionId("NO_SESSION_" + System.currentTimeMillis());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("初始化AI追踪步骤失败: {}", e.getMessage(), e);
|
||||
// 创建简化的追踪步骤
|
||||
generationStep = new WorkflowTraceStep();
|
||||
generationStep.setStepName("AI_GENERATION_FALLBACK");
|
||||
generationStep.setStepType(WorkflowTraceStep.StepType.ANSWER_GENERATION);
|
||||
generationStep.setSessionId("ERROR_SESSION_" + System.currentTimeMillis());
|
||||
}
|
||||
|
||||
try {
|
||||
log.info("开始AI回答生成: {}.{}", className, methodName);
|
||||
|
||||
// 分析输入信息
|
||||
// 安全分析输入信息
|
||||
Object[] args = joinPoint.getArgs();
|
||||
AIGenerationInput inputInfo = analyzeGenerationInput(args);
|
||||
AIGenerationInput inputInfo = null;
|
||||
try {
|
||||
inputInfo = analyzeGenerationInput(args);
|
||||
} catch (Exception e) {
|
||||
log.warn("分析输入信息失败: {}", e.getMessage());
|
||||
inputInfo = new AIGenerationInput(); // 创建默认对象
|
||||
}
|
||||
|
||||
// 开始步骤追踪
|
||||
generationStep.start();
|
||||
generationStep.setInputData(sanitizeGenerationInput(args));
|
||||
generationStep.addExtendedProperty("input_length", inputInfo.inputLength);
|
||||
generationStep.addExtendedProperty("input_type", inputInfo.inputType);
|
||||
generationStep.addExtendedProperty("user_query", inputInfo.userQuery);
|
||||
// 安全开始步骤追踪
|
||||
try {
|
||||
if (generationStep != null) {
|
||||
generationStep.start();
|
||||
generationStep.setInputData(sanitizeGenerationInput(args));
|
||||
|
||||
// 安全添加扩展属性 - 避免空指针异常
|
||||
if (inputInfo != null) {
|
||||
generationStep.addExtendedProperty("input_length", inputInfo.inputLength);
|
||||
generationStep.addExtendedProperty("input_type", inputInfo.inputType);
|
||||
generationStep.addExtendedProperty("user_query", inputInfo.userQuery);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("设置追踪步骤信息失败: {}", e.getMessage(), e);
|
||||
}
|
||||
|
||||
// 记录到会话
|
||||
if (currentSession != null) {
|
||||
currentSession.addStep(generationStep);
|
||||
workflowTracingService.updateSessionStep(currentSession.getSessionId(), generationStep);
|
||||
// 安全记录到会话 - 增强空值检查
|
||||
try {
|
||||
if (currentSession != null && generationStep != null && workflowTracingService != null) {
|
||||
currentSession.addStep(generationStep);
|
||||
workflowTracingService.updateSessionStep(currentSession.getSessionId(), generationStep);
|
||||
} else {
|
||||
log.debug("无法记录追踪步骤: currentSession={}, generationStep={}, workflowTracingService={}",
|
||||
currentSession != null, generationStep != null, workflowTracingService != null);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("记录追踪步骤到会话失败: {}", e.getMessage(), e);
|
||||
}
|
||||
|
||||
// 性能监控开始
|
||||
|
|
@ -114,19 +155,41 @@ public class AIAnswerGenerationAspect {
|
|||
// 分析生成结果
|
||||
AIGenerationResult resultInfo = analyzeGenerationResult(result);
|
||||
|
||||
// 完成步骤
|
||||
generationStep.complete(sanitizeGenerationResult(result));
|
||||
// 安全完成步骤
|
||||
try {
|
||||
if (generationStep != null) {
|
||||
generationStep.complete(sanitizeGenerationResult(result));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("完成追踪步骤时发生错误: {}", e.getMessage(), e);
|
||||
}
|
||||
|
||||
// 记录性能指标
|
||||
recordGenerationPerformanceMetrics(generationStep, inputInfo, resultInfo, duration, startMemory, endMemory);
|
||||
// 安全记录性能指标
|
||||
try {
|
||||
if (generationStep != null && inputInfo != null && resultInfo != null) {
|
||||
recordGenerationPerformanceMetrics(generationStep, inputInfo, resultInfo, duration, startMemory, endMemory);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("记录性能指标时发生错误: {}", e.getMessage(), e);
|
||||
}
|
||||
|
||||
// 内容质量分析
|
||||
analyzeContentQuality(generationStep, inputInfo, resultInfo);
|
||||
// 安全分析内容质量
|
||||
try {
|
||||
if (generationStep != null && inputInfo != null && resultInfo != null) {
|
||||
analyzeContentQuality(generationStep, inputInfo, resultInfo);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("分析内容质量时发生错误: {}", e.getMessage(), e);
|
||||
}
|
||||
|
||||
// 更新会话信息
|
||||
if (currentSession != null) {
|
||||
updateSessionWithGenerationResult(currentSession, resultInfo);
|
||||
workflowTracingService.updateSessionStep(currentSession.getSessionId(), generationStep);
|
||||
// 安全更新会话信息
|
||||
try {
|
||||
if (currentSession != null && resultInfo != null && generationStep != null && workflowTracingService != null) {
|
||||
updateSessionWithGenerationResult(currentSession, resultInfo);
|
||||
workflowTracingService.updateSessionStep(currentSession.getSessionId(), generationStep);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("更新会话信息失败: {}", e.getMessage(), e);
|
||||
}
|
||||
|
||||
log.info("AI回答生成完成: {}.{}, 耗时: {}ms, 输出长度: {}",
|
||||
|
|
@ -138,17 +201,32 @@ public class AIAnswerGenerationAspect {
|
|||
log.error("AI回答生成失败: {}.{}, 错误: {}",
|
||||
className, methodName, ex.getMessage(), ex);
|
||||
|
||||
// 分析错误类型
|
||||
String errorType = analyzeGenerationError(ex);
|
||||
// 安全分析错误类型
|
||||
String errorType = "UNKNOWN_ERROR";
|
||||
try {
|
||||
errorType = analyzeGenerationError(ex);
|
||||
} catch (Exception e) {
|
||||
log.warn("分析错误类型失败: {}", e.getMessage());
|
||||
}
|
||||
|
||||
// 标记步骤失败
|
||||
generationStep.fail(errorType, ex.getMessage(), getStackTrace(ex));
|
||||
generationStep.addExtendedProperty("error_type", errorType);
|
||||
generationStep.addExtendedProperty("error_category", categorizeError(ex));
|
||||
// 安全标记步骤失败
|
||||
try {
|
||||
if (generationStep != null) {
|
||||
generationStep.fail(errorType, ex.getMessage(), getStackTrace(ex));
|
||||
generationStep.addExtendedProperty("error_type", errorType);
|
||||
generationStep.addExtendedProperty("error_category", categorizeError(ex));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("设置步骤失败信息时发生错误: {}", e.getMessage(), e);
|
||||
}
|
||||
|
||||
// 更新会话
|
||||
if (currentSession != null) {
|
||||
workflowTracingService.updateSessionStep(currentSession.getSessionId(), generationStep);
|
||||
// 安全更新会话
|
||||
try {
|
||||
if (currentSession != null && generationStep != null && workflowTracingService != null) {
|
||||
workflowTracingService.updateSessionStep(currentSession.getSessionId(), generationStep);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("更新失败会话状态时发生错误: {}", e.getMessage(), e);
|
||||
}
|
||||
|
||||
// 记录错误日志
|
||||
|
|
@ -517,15 +595,19 @@ public class AIAnswerGenerationAspect {
|
|||
* 分析生成错误类型
|
||||
*/
|
||||
private String analyzeGenerationError(Exception ex) {
|
||||
String message = ex.getMessage().toLowerCase();
|
||||
String message = ex.getMessage();
|
||||
if (message == null) {
|
||||
message = ex.getClass().getSimpleName();
|
||||
}
|
||||
String lowerMessage = message.toLowerCase();
|
||||
|
||||
if (message.contains("timeout") || message.contains("超时")) {
|
||||
if (lowerMessage.contains("timeout") || lowerMessage.contains("超时")) {
|
||||
return "GENERATION_TIMEOUT";
|
||||
} else if (message.contains("rate limit") || message.contains("限流")) {
|
||||
} else if (lowerMessage.contains("rate limit") || lowerMessage.contains("限流")) {
|
||||
return "RATE_LIMIT_ERROR";
|
||||
} else if (message.contains("content") || message.contains("内容")) {
|
||||
} else if (lowerMessage.contains("content") || lowerMessage.contains("内容")) {
|
||||
return "CONTENT_ERROR";
|
||||
} else if (message.contains("model") || message.contains("模型")) {
|
||||
} else if (lowerMessage.contains("model") || lowerMessage.contains("模型")) {
|
||||
return "MODEL_ERROR";
|
||||
} else {
|
||||
return "GENERATION_ERROR";
|
||||
|
|
@ -536,13 +618,17 @@ public class AIAnswerGenerationAspect {
|
|||
* 分析LLM错误类型
|
||||
*/
|
||||
private String analyzeLLMError(Exception ex) {
|
||||
String message = ex.getMessage().toLowerCase();
|
||||
String message = ex.getMessage();
|
||||
if (message == null) {
|
||||
message = ex.getClass().getSimpleName();
|
||||
}
|
||||
String lowerMessage = message.toLowerCase();
|
||||
|
||||
if (message.contains("api") || message.contains("接口")) {
|
||||
if (lowerMessage.contains("api") || lowerMessage.contains("接口")) {
|
||||
return "API_ERROR";
|
||||
} else if (message.contains("auth") || message.contains("认证")) {
|
||||
} else if (lowerMessage.contains("auth") || lowerMessage.contains("认证")) {
|
||||
return "AUTH_ERROR";
|
||||
} else if (message.contains("quota") || message.contains("配额")) {
|
||||
} else if (lowerMessage.contains("quota") || lowerMessage.contains("配额")) {
|
||||
return "QUOTA_ERROR";
|
||||
} else {
|
||||
return "LLM_ERROR";
|
||||
|
|
|
|||
|
|
@ -501,19 +501,23 @@ public class EmbeddingServiceAspect {
|
|||
* 分析Embedding错误类型
|
||||
*/
|
||||
private String analyzeEmbeddingError(Exception ex) {
|
||||
String message = ex.getMessage().toLowerCase();
|
||||
String message = ex.getMessage();
|
||||
if (message == null) {
|
||||
message = ex.getClass().getSimpleName();
|
||||
}
|
||||
String lowerMessage = message.toLowerCase();
|
||||
|
||||
if (message.contains("timeout") || message.contains("超时")) {
|
||||
if (lowerMessage.contains("timeout") || lowerMessage.contains("超时")) {
|
||||
return "TIMEOUT_ERROR";
|
||||
} else if (message.contains("rate limit") || message.contains("限流")) {
|
||||
} else if (lowerMessage.contains("rate limit") || lowerMessage.contains("限流")) {
|
||||
return "RATE_LIMIT_ERROR";
|
||||
} else if (message.contains("api key") || message.contains("auth") || message.contains("认证")) {
|
||||
} else if (lowerMessage.contains("api key") || lowerMessage.contains("auth") || lowerMessage.contains("认证")) {
|
||||
return "AUTHENTICATION_ERROR";
|
||||
} else if (message.contains("network") || message.contains("connection") || message.contains("网络")) {
|
||||
} else if (lowerMessage.contains("network") || lowerMessage.contains("connection") || lowerMessage.contains("网络")) {
|
||||
return "NETWORK_ERROR";
|
||||
} else if (message.contains("quota") || message.contains("配额")) {
|
||||
} else if (lowerMessage.contains("quota") || lowerMessage.contains("配额")) {
|
||||
return "QUOTA_EXCEEDED_ERROR";
|
||||
} else if (message.contains("text too long") || message.contains("文本过长")) {
|
||||
} else if (lowerMessage.contains("text too long") || lowerMessage.contains("文本过长")) {
|
||||
return "TEXT_TOO_LONG_ERROR";
|
||||
} else {
|
||||
return "GENERAL_ERROR";
|
||||
|
|
|
|||
|
|
@ -377,20 +377,26 @@ public class MCPToolCallAspect {
|
|||
* 处理异常
|
||||
*/
|
||||
private void handleException(MCPToolCallTrace mcpTrace, WorkflowTraceStep mcpStep, Exception ex) {
|
||||
if (ex instanceof java.util.concurrent.TimeoutException ||
|
||||
ex.getMessage().contains("超时") || ex.getMessage().contains("timeout")) {
|
||||
String message = ex.getMessage();
|
||||
boolean isTimeout = ex instanceof java.util.concurrent.TimeoutException ||
|
||||
(message != null && (message.contains("超时") || message.contains("timeout")));
|
||||
boolean isConnection = ex instanceof java.net.ConnectException ||
|
||||
(message != null && (message.contains("Connection") || message.contains("连接")));
|
||||
|
||||
if (isTimeout) {
|
||||
mcpTrace.callTimeout();
|
||||
mcpStep.fail("TIMEOUT", "调用超时", null);
|
||||
} else if (ex instanceof java.net.ConnectException ||
|
||||
ex.getMessage().contains("Connection") || ex.getMessage().contains("连接")) {
|
||||
mcpTrace.callFailed("CONNECTION_ERROR", "连接失败: " + ex.getMessage(), getStackTrace(ex));
|
||||
} else if (isConnection) {
|
||||
mcpTrace.callFailed("CONNECTION_ERROR", "连接失败: " + (message != null ? message : "Unknown connection error"), getStackTrace(ex));
|
||||
mcpStep.fail("CONNECTION_ERROR", "连接失败", getStackTrace(ex));
|
||||
} else if (ex instanceof SecurityException) {
|
||||
mcpTrace.callFailed("SECURITY_ERROR", "安全错误: " + ex.getMessage(), getStackTrace(ex));
|
||||
String securityMessage = message != null ? message : "Unknown security error";
|
||||
mcpTrace.callFailed("SECURITY_ERROR", "安全错误: " + securityMessage, getStackTrace(ex));
|
||||
mcpStep.fail("SECURITY_ERROR", "安全错误", getStackTrace(ex));
|
||||
} else {
|
||||
mcpTrace.callFailed("GENERAL_ERROR", ex.getMessage(), getStackTrace(ex));
|
||||
mcpStep.fail("GENERAL_ERROR", ex.getMessage(), getStackTrace(ex));
|
||||
String errorMessage = message != null ? message : ex.getClass().getSimpleName();
|
||||
mcpTrace.callFailed("GENERAL_ERROR", errorMessage, getStackTrace(ex));
|
||||
mcpStep.fail("GENERAL_ERROR", errorMessage, getStackTrace(ex));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.chinaweal.youfool.devops.ai.aspect.dto;
|
|||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
|
|
@ -18,6 +19,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Schema(description = "AI工作流追踪步骤")
|
||||
@Slf4j
|
||||
public class WorkflowTraceStep {
|
||||
|
||||
@Schema(description = "步骤ID")
|
||||
|
|
@ -229,22 +231,66 @@ public class WorkflowTraceStep {
|
|||
|
||||
/**
|
||||
* 添加性能指标
|
||||
* 增强空值检查,确保数据安全
|
||||
*/
|
||||
public void addPerformanceMetric(String key, Object value) {
|
||||
if (this.performanceMetrics == null) {
|
||||
this.performanceMetrics = new ConcurrentHashMap<>();
|
||||
// 参数验证 - 符合政府项目安全标准
|
||||
if (key == null || key.trim().isEmpty()) {
|
||||
log.warn("尝试添加空或无效的性能指标键: {}", key);
|
||||
return;
|
||||
}
|
||||
|
||||
// 双重检查锁定模式,确保线程安全
|
||||
if (this.performanceMetrics == null) {
|
||||
synchronized (this) {
|
||||
if (this.performanceMetrics == null) {
|
||||
this.performanceMetrics = new ConcurrentHashMap<>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// 安全地添加性能指标
|
||||
this.performanceMetrics.put(key, value);
|
||||
log.debug("成功添加性能指标: {} = {}", key, value);
|
||||
} catch (Exception e) {
|
||||
log.error("添加性能指标失败: key={}, value={}, error={}", key, value, e.getMessage(), e);
|
||||
// 重新初始化并重试一次
|
||||
this.performanceMetrics = new ConcurrentHashMap<>();
|
||||
this.performanceMetrics.put(key, value);
|
||||
}
|
||||
this.performanceMetrics.put(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加扩展属性
|
||||
* 增强空值检查,确保线程安全
|
||||
*/
|
||||
public void addExtendedProperty(String key, Object value) {
|
||||
if (this.extendedProperties == null) {
|
||||
this.extendedProperties = new ConcurrentHashMap<>();
|
||||
// 参数验证 - 符合政府项目安全标准
|
||||
if (key == null || key.trim().isEmpty()) {
|
||||
log.warn("尝试添加空或无效的扩展属性键: {}", key);
|
||||
return;
|
||||
}
|
||||
|
||||
// 双重检查锁定模式,确保线程安全
|
||||
if (this.extendedProperties == null) {
|
||||
synchronized (this) {
|
||||
if (this.extendedProperties == null) {
|
||||
this.extendedProperties = new ConcurrentHashMap<>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// 安全地添加属性,避免空指针异常
|
||||
this.extendedProperties.put(key, value);
|
||||
log.debug("成功添加扩展属性: {} = {}", key, value);
|
||||
} catch (Exception e) {
|
||||
log.error("添加扩展属性失败: key={}, value={}, error={}", key, value, e.getMessage(), e);
|
||||
// 重新初始化并重试一次
|
||||
this.extendedProperties = new ConcurrentHashMap<>();
|
||||
this.extendedProperties.put(key, value);
|
||||
}
|
||||
this.extendedProperties.put(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -95,6 +95,16 @@ public class EmbeddingProperties {
|
|||
*/
|
||||
private RateLimiterConfig rateLimiter = new RateLimiterConfig();
|
||||
|
||||
/**
|
||||
* 是否启用批量API
|
||||
*/
|
||||
private boolean batchEnabled = true;
|
||||
|
||||
/**
|
||||
* 使用批量API的最小文本数量阈值
|
||||
*/
|
||||
private int batchThreshold = 1000;
|
||||
|
||||
/**
|
||||
* 熔断器配置
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -54,14 +54,27 @@ public class AIAnswerController {
|
|||
HttpServletRequest httpRequest) {
|
||||
|
||||
try {
|
||||
// 参数安全验证 - 符合政府项目安全标准
|
||||
if (request == null) {
|
||||
return ResponseEntity.ok(RestResult.error(ResultCode.PARAM_IS_INVALID, "请求参数不能为空"));
|
||||
}
|
||||
if (request.getRepairId() == null || request.getRepairId().trim().isEmpty()) {
|
||||
return ResponseEntity.ok(RestResult.error(ResultCode.PARAM_IS_INVALID, "工单ID不能为空"));
|
||||
}
|
||||
|
||||
// 设置会话ID(如果没有提供)
|
||||
if (request.getSessionId() == null) {
|
||||
if (request.getSessionId() == null || request.getSessionId().trim().isEmpty()) {
|
||||
request.setSessionId("answer_" + UUID.randomUUID().toString().substring(0, 8));
|
||||
}
|
||||
|
||||
// 设置用户ID(从请求头或其他方式获取)
|
||||
if (request.getUserId() == null) {
|
||||
request.setUserId(getUserIdFromRequest(httpRequest));
|
||||
// 安全设置用户ID(从请求头或其他方式获取)
|
||||
if (request.getUserId() == null || request.getUserId().trim().isEmpty()) {
|
||||
String userId = getUserIdFromRequest(httpRequest);
|
||||
if (userId == null || userId.trim().isEmpty()) {
|
||||
log.warn("无法获取用户ID,使用匿名用户");
|
||||
userId = "anonymous_" + System.currentTimeMillis();
|
||||
}
|
||||
request.setUserId(userId);
|
||||
}
|
||||
|
||||
log.info("Generating AI answer for repair: {} by user: {} from IP: {}",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,360 @@
|
|||
package com.chinaweal.youfool.devops.ai.controller;
|
||||
|
||||
import com.chinaweal.youfool.devops.ai.migration.RepairVectorizationService;
|
||||
import com.chinaweal.youfool.devops.ai.migration.dto.VectorizationProgress;
|
||||
import com.chinaweal.youfool.devops.ai.service.QwenEmbeddingService;
|
||||
import com.chinaweal.youfool.framework.springboot.rest.RestResult;
|
||||
import com.chinaweal.youfool.framework.springboot.rest.ResultCode;
|
||||
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* AI管理控制器
|
||||
*
|
||||
* 提供AI系统的Web管理界面,包括:
|
||||
* - 登录管理页面
|
||||
* - 向量化任务控制
|
||||
* - 系统状态监控
|
||||
* - 性能指标查看
|
||||
*
|
||||
* @author AI开发团队
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Controller
|
||||
@RequestMapping("/ai-management")
|
||||
@Tag(name = "AI管理", description = "AI系统的Web管理界面")
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnProperty(prefix = "ai.management", name = "enabled", havingValue = "true", matchIfMissing = true)
|
||||
public class AIManagementController {
|
||||
|
||||
private final RepairVectorizationService vectorizationService;
|
||||
private final QwenEmbeddingService embeddingService;
|
||||
private final CircuitBreakerRegistry circuitBreakerRegistry;
|
||||
|
||||
@Value("${business.fsLoginUrl:http://121.8.152.130:9888/tzrysb/user/loginDevops}")
|
||||
private String fsLoginUrl;
|
||||
|
||||
@Value("${business.sdLoginUrl:http://121.8.152.130:9888/tzrysb/user/loginDevops}")
|
||||
private String sdLoginUrl;
|
||||
|
||||
/**
|
||||
* AI管理首页
|
||||
*/
|
||||
@GetMapping({"", "/", "/index"})
|
||||
@Operation(summary = "AI管理首页", description = "显示AI系统管理的主页面")
|
||||
public String index(Model model, HttpServletRequest request) {
|
||||
try {
|
||||
// 获取当前用户信息
|
||||
HttpSession session = request.getSession();
|
||||
String currentUser = (String) session.getAttribute("currentUser");
|
||||
String authToken = (String) session.getAttribute("authToken");
|
||||
|
||||
// 系统状态信息
|
||||
Map<String, Object> systemStatus = getSystemStatus();
|
||||
|
||||
// 向量化服务状态
|
||||
Map<String, Object> vectorizationStatus = getVectorizationStatus();
|
||||
|
||||
// 添加到模型
|
||||
model.addAttribute("currentUser", currentUser);
|
||||
model.addAttribute("isLoggedIn", authToken != null);
|
||||
model.addAttribute("systemStatus", systemStatus);
|
||||
model.addAttribute("vectorizationStatus", vectorizationStatus);
|
||||
model.addAttribute("fsLoginUrl", fsLoginUrl);
|
||||
model.addAttribute("sdLoginUrl", sdLoginUrl);
|
||||
|
||||
// 如果已登录,显示管理界面;否则显示登录页面
|
||||
return authToken != null ? "ai-management/dashboard" : "ai-management/login";
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("加载AI管理首页失败: {}", e.getMessage(), e);
|
||||
model.addAttribute("error", "系统加载失败: " + e.getMessage());
|
||||
return "ai-management/error";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录页面
|
||||
*/
|
||||
@GetMapping("/login")
|
||||
@Operation(summary = "登录页面", description = "显示用户登录页面")
|
||||
public String loginPage(Model model) {
|
||||
model.addAttribute("fsLoginUrl", fsLoginUrl);
|
||||
model.addAttribute("sdLoginUrl", sdLoginUrl);
|
||||
return "ai-management/login";
|
||||
}
|
||||
|
||||
/**
|
||||
* 仪表板页面
|
||||
*/
|
||||
@GetMapping("/dashboard")
|
||||
@Operation(summary = "管理仪表板", description = "显示AI系统管理仪表板")
|
||||
public String dashboard(Model model, HttpServletRequest request) {
|
||||
HttpSession session = request.getSession();
|
||||
String authToken = (String) session.getAttribute("authToken");
|
||||
|
||||
if (authToken == null) {
|
||||
return "redirect:/ai-management/login";
|
||||
}
|
||||
|
||||
try {
|
||||
// 系统状态
|
||||
model.addAttribute("systemStatus", getSystemStatus());
|
||||
model.addAttribute("vectorizationStatus", getVectorizationStatus());
|
||||
model.addAttribute("embeddingMetrics", getEmbeddingMetrics());
|
||||
model.addAttribute("performanceReport", getPerformanceReport());
|
||||
|
||||
return "ai-management/dashboard";
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("加载仪表板失败: {}", e.getMessage(), e);
|
||||
model.addAttribute("error", "仪表板加载失败: " + e.getMessage());
|
||||
return "ai-management/error";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajax: 触发全量向量化
|
||||
*/
|
||||
@PostMapping("/api/start-vectorization")
|
||||
@ResponseBody
|
||||
@Operation(summary = "启动向量化", description = "通过Ajax启动全量向量化任务")
|
||||
public RestResult<Map<String, Object>> startVectorization(HttpServletRequest request) {
|
||||
try {
|
||||
HttpSession session = request.getSession();
|
||||
String authToken = (String) session.getAttribute("authToken");
|
||||
|
||||
if (authToken == null) {
|
||||
return RestResult.error(ResultCode.USER_NOT_LOGGED_IN, "用户未登录");
|
||||
}
|
||||
|
||||
log.info("通过Web管理界面触发全量向量化任务");
|
||||
|
||||
CompletableFuture<VectorizationProgress> future = vectorizationService.fullVectorization();
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("status", "started");
|
||||
result.put("message", "全量向量化任务已启动");
|
||||
result.put("taskId", extractTaskId(future));
|
||||
result.put("timestamp", System.currentTimeMillis());
|
||||
result.put("startedBy", session.getAttribute("currentUser"));
|
||||
|
||||
return RestResult.ok(result);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("启动向量化任务失败: {}", e.getMessage(), e);
|
||||
return RestResult.error(ResultCode.SYSTEM_INNER_ERROR, "启动任务失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajax: 获取向量化进度
|
||||
*/
|
||||
@GetMapping("/api/vectorization-progress")
|
||||
@ResponseBody
|
||||
@Operation(summary = "获取向量化进度", description = "获取当前向量化任务的进度信息")
|
||||
public RestResult<Map<String, Object>> getVectorizationProgress() {
|
||||
try {
|
||||
Map<String, Object> progress = getVectorizationStatus();
|
||||
return RestResult.ok(progress);
|
||||
} catch (Exception e) {
|
||||
log.error("获取向量化进度失败: {}", e.getMessage(), e);
|
||||
return RestResult.error(ResultCode.SYSTEM_INNER_ERROR, "获取进度失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajax: 获取系统状态
|
||||
*/
|
||||
@GetMapping("/api/system-status")
|
||||
@ResponseBody
|
||||
@Operation(summary = "获取系统状态", description = "获取AI系统整体状态信息")
|
||||
public RestResult<Map<String, Object>> getSystemStatusAPI() {
|
||||
try {
|
||||
Map<String, Object> status = new HashMap<>();
|
||||
|
||||
// Embedding服务状态
|
||||
boolean embeddingHealthy = embeddingService.healthCheck();
|
||||
status.put("embeddingServiceHealthy", embeddingHealthy);
|
||||
|
||||
// 向量化服务状态
|
||||
status.put("vectorizationServiceEnabled", true);
|
||||
|
||||
// 性能指标
|
||||
if (embeddingHealthy) {
|
||||
status.put("embeddingMetrics", embeddingService.getBatchPerformanceMetrics());
|
||||
}
|
||||
|
||||
// 向量化统计
|
||||
status.put("vectorizationStats", vectorizationService.getPerformanceMetrics());
|
||||
|
||||
return RestResult.ok(status);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取系统状态失败: {}", e.getMessage(), e);
|
||||
return RestResult.error(ResultCode.SYSTEM_INNER_ERROR, "获取状态失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajax: 用户登录处理
|
||||
*/
|
||||
@PostMapping("/api/login")
|
||||
@ResponseBody
|
||||
@Operation(summary = "用户登录", description = "处理用户登录请求")
|
||||
public RestResult<Map<String, Object>> login(@RequestBody Map<String, String> loginData,
|
||||
HttpServletRequest request) {
|
||||
try {
|
||||
String userId = loginData.get("userId");
|
||||
String username = loginData.get("username");
|
||||
String loginType = loginData.get("loginType");
|
||||
|
||||
// 模拟登录验证 (实际应该调用业务系统API)
|
||||
if (userId != null && !userId.trim().isEmpty()) {
|
||||
HttpSession session = request.getSession();
|
||||
session.setAttribute("authToken", "mock_token_" + System.currentTimeMillis());
|
||||
session.setAttribute("currentUser", userId);
|
||||
session.setAttribute("loginType", "userId");
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("success", true);
|
||||
result.put("message", "登录成功");
|
||||
result.put("user", userId);
|
||||
result.put("loginType", "userId");
|
||||
|
||||
log.info("用户通过UserId登录成功: {}", userId);
|
||||
|
||||
return RestResult.ok(result);
|
||||
} else {
|
||||
return RestResult.error(ResultCode.PARAM_IS_INVALID, "请提供有效的UserId或用户名");
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("用户登录失败: {}", e.getMessage(), e);
|
||||
return RestResult.error(ResultCode.SYSTEM_INNER_ERROR, "登录失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置熔断器状态
|
||||
*/
|
||||
@PostMapping("/api/reset-circuit-breaker")
|
||||
@ResponseBody
|
||||
@Operation(summary = "重置熔断器", description = "重置embedding服务的熔断器状态")
|
||||
public RestResult<String> resetCircuitBreaker(HttpServletRequest request) {
|
||||
try {
|
||||
HttpSession session = request.getSession();
|
||||
String authToken = (String) session.getAttribute("authToken");
|
||||
|
||||
if (authToken == null) {
|
||||
return RestResult.error(ResultCode.USER_NOT_LOGGED_IN, "用户未登录");
|
||||
}
|
||||
|
||||
// 重置qwen-embedding熔断器
|
||||
circuitBreakerRegistry.circuitBreaker("qwen-embedding").reset();
|
||||
|
||||
log.info("用户 {} 重置了embedding服务熔断器", session.getAttribute("currentUser"));
|
||||
|
||||
return RestResult.ok("熔断器重置成功");
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("重置熔断器失败: {}", e.getMessage(), e);
|
||||
return RestResult.error(ResultCode.SYSTEM_INNER_ERROR, "重置失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登出
|
||||
*/
|
||||
@PostMapping("/api/logout")
|
||||
@ResponseBody
|
||||
@Operation(summary = "用户登出", description = "处理用户登出请求")
|
||||
public RestResult<String> logout(HttpServletRequest request) {
|
||||
try {
|
||||
HttpSession session = request.getSession();
|
||||
String currentUser = (String) session.getAttribute("currentUser");
|
||||
|
||||
session.removeAttribute("authToken");
|
||||
session.removeAttribute("currentUser");
|
||||
session.removeAttribute("loginType");
|
||||
|
||||
log.info("用户登出: {}", currentUser);
|
||||
|
||||
return RestResult.ok("登出成功");
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("用户登出失败: {}", e.getMessage(), e);
|
||||
return RestResult.error(ResultCode.SYSTEM_INNER_ERROR, "登出失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ================ 私有辅助方法 ================
|
||||
|
||||
private Map<String, Object> getSystemStatus() {
|
||||
Map<String, Object> status = new HashMap<>();
|
||||
try {
|
||||
status.put("embeddingServiceHealthy", embeddingService.healthCheck());
|
||||
status.put("vectorizationServiceEnabled", true);
|
||||
status.put("timestamp", System.currentTimeMillis());
|
||||
} catch (Exception e) {
|
||||
status.put("error", e.getMessage());
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
private Map<String, Object> getVectorizationStatus() {
|
||||
Map<String, Object> status = new HashMap<>();
|
||||
try {
|
||||
// 获取向量化服务的状态信息
|
||||
Map<String, Object> performanceMetrics = vectorizationService.getPerformanceMetrics();
|
||||
status.putAll(performanceMetrics);
|
||||
status.put("serviceEnabled", true);
|
||||
} catch (Exception e) {
|
||||
status.put("error", e.getMessage());
|
||||
status.put("serviceEnabled", false);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
private Map<String, Object> getEmbeddingMetrics() {
|
||||
try {
|
||||
return embeddingService.getBatchPerformanceMetrics();
|
||||
} catch (Exception e) {
|
||||
Map<String, Object> error = new HashMap<>();
|
||||
error.put("error", e.getMessage());
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
private String getPerformanceReport() {
|
||||
try {
|
||||
return vectorizationService.getPerformanceReport();
|
||||
} catch (Exception e) {
|
||||
return "性能报告获取失败: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
private String extractTaskId(CompletableFuture<VectorizationProgress> future) {
|
||||
try {
|
||||
return "task_" + System.currentTimeMillis() + "_" + future.hashCode();
|
||||
} catch (Exception e) {
|
||||
return "unknown_task";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|||
import com.chinaweal.youfool.devops.ai.migration.dto.VectorizationProgress;
|
||||
import com.chinaweal.youfool.devops.ai.migration.dto.SimilarRepair;
|
||||
import com.chinaweal.youfool.devops.ai.service.QwenEmbeddingService;
|
||||
import com.chinaweal.youfool.devops.ai.service.QwenBatchEmbeddingService;
|
||||
import com.chinaweal.youfool.devops.repair.entity.Repair;
|
||||
import com.chinaweal.youfool.devops.repair.mapper.RepairMapper;
|
||||
import com.chinaweal.youfool.devops.util.ErrorLogUtils;
|
||||
|
|
@ -53,6 +54,7 @@ public class RepairVectorizationService {
|
|||
|
||||
private final RepairMapper repairMapper;
|
||||
private final QwenEmbeddingService embeddingService;
|
||||
private final QwenBatchEmbeddingService batchEmbeddingService;
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
|
|
@ -71,6 +73,12 @@ public class RepairVectorizationService {
|
|||
@Value("${ai.embedding.batch-delay:100}")
|
||||
private int embeddingBatchDelay;
|
||||
|
||||
@Value("${ai.embedding.use-batch-api:true}")
|
||||
private boolean useBatchAPI;
|
||||
|
||||
@Value("${ai.embedding.batch-threshold:1000}")
|
||||
private int batchAPIThreshold;
|
||||
|
||||
// 进度追踪
|
||||
private final Map<String, VectorizationProgress> progressMap = new ConcurrentHashMap<>();
|
||||
|
||||
|
|
@ -275,9 +283,9 @@ public class RepairVectorizationService {
|
|||
.map(this::buildCombinedText)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 使用批量API获取向量
|
||||
// 智能选择批量API或实时API获取向量
|
||||
long embeddingStartTime = System.currentTimeMillis();
|
||||
List<double[]> embeddings = embeddingService.getEmbeddings(texts);
|
||||
List<double[]> embeddings = getOptimalEmbeddings(texts, progress.getTaskId());
|
||||
long embeddingEndTime = System.currentTimeMillis();
|
||||
totalEmbeddingTime.addAndGet(embeddingEndTime - embeddingStartTime);
|
||||
|
||||
|
|
@ -584,6 +592,59 @@ public class RepairVectorizationService {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 智能选择最优的向量化方法
|
||||
* <p>根据文本数量和配置,自动选择批量API或实时API:</p>
|
||||
* <ul>
|
||||
* <li>大数据量(≥1000条)且启用批量API:使用阿里云批量接口,成本降低50%</li>
|
||||
* <li>小数据量(<1000条):使用实时API,响应更快</li>
|
||||
* </ul>
|
||||
*/
|
||||
private List<double[]> getOptimalEmbeddings(List<String> texts, String taskId) {
|
||||
if (texts == null || texts.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
boolean shouldUseBatchAPI = useBatchAPI
|
||||
&& texts.size() >= batchAPIThreshold
|
||||
&& batchEmbeddingService != null
|
||||
&& batchEmbeddingService.isAvailable();
|
||||
|
||||
if (shouldUseBatchAPI) {
|
||||
log.info("使用批量API进行向量化,文本数量: {}, 预计成本节省: 50%", texts.size());
|
||||
try {
|
||||
// 使用批量API异步处理
|
||||
CompletableFuture<Map<Integer, double[]>> batchFuture =
|
||||
batchEmbeddingService.batchEmbeddingAsync(texts, taskId);
|
||||
|
||||
// 等待批量处理完成(可能需要几分钟到几小时)
|
||||
Map<Integer, double[]> batchResults = batchFuture.get();
|
||||
|
||||
// 将Map转换为List,保持原始顺序
|
||||
List<double[]> embeddings = new ArrayList<>(Collections.nCopies(texts.size(), null));
|
||||
for (Map.Entry<Integer, double[]> entry : batchResults.entrySet()) {
|
||||
embeddings.set(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
// 检查是否有缺失的向量
|
||||
int successCount = (int) embeddings.stream().filter(Objects::nonNull).count();
|
||||
log.info("批量API处理完成,成功: {}/{}, 成功率: {}%",
|
||||
successCount, texts.size(),
|
||||
String.format("%.1f", (double) successCount / texts.size() * 100));
|
||||
|
||||
return embeddings;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn("批量API处理失败,降级到实时API: {}", e.getMessage());
|
||||
// 降级到实时API
|
||||
return embeddingService.getEmbeddings(texts);
|
||||
}
|
||||
} else {
|
||||
log.info("使用实时API进行向量化,文本数量: {}", texts.size());
|
||||
return embeddingService.getEmbeddings(texts);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建组合文本
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -57,10 +57,23 @@ public class AIAnswerService {
|
|||
log.info("Generating AI answer for repair: {}, session: {}",
|
||||
request.getRepairId(), request.getSessionId());
|
||||
|
||||
// 获取工单详情
|
||||
Repair repair = getRepairDetails(request.getRepairId());
|
||||
if (repair == null) {
|
||||
throw new RuntimeException("工单不存在: " + request.getRepairId());
|
||||
// 安全获取工单详情 - 增强数据验证
|
||||
Repair repair = null;
|
||||
try {
|
||||
repair = getRepairDetails(request.getRepairId());
|
||||
if (repair == null) {
|
||||
throw new RuntimeException("工单不存在: " + request.getRepairId());
|
||||
}
|
||||
// 验证工单必要字段
|
||||
if (repair.getTitle() == null || repair.getTitle().trim().isEmpty()) {
|
||||
log.warn("工单缺少标题信息: {}", request.getRepairId());
|
||||
}
|
||||
if (repair.getFaultDescription() == null || repair.getFaultDescription().trim().isEmpty()) {
|
||||
log.warn("工单缺少故障描述: {}", request.getRepairId());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("获取工单详情失败: {}", request.getRepairId(), e);
|
||||
throw new RuntimeException("获取工单详情失败: " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
// 构建聊天请求
|
||||
|
|
|
|||
|
|
@ -0,0 +1,600 @@
|
|||
package com.chinaweal.youfool.devops.ai.service;
|
||||
|
||||
import com.chinaweal.youfool.devops.ai.config.EmbeddingProperties;
|
||||
import com.chinaweal.youfool.devops.util.ErrorLogUtils;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.*;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 阿里云模型工作室批量Embedding服务
|
||||
*
|
||||
* <p>使用阿里云模型工作室的批量接口进行大规模文本向量化。
|
||||
* 相比实时API,批量接口有以下优势:</p>
|
||||
* <ul>
|
||||
* <li>成本降低50%</li>
|
||||
* <li>支持更大规模的批量处理(最多50,000条请求)</li>
|
||||
* <li>异步处理,不占用实时API配额</li>
|
||||
* <li>更适合大数据量的向量化任务</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>批量处理流程:</p>
|
||||
* <ol>
|
||||
* <li>准备JSONL格式的输入文件</li>
|
||||
* <li>上传文件获取文件ID</li>
|
||||
* <li>创建批量任务</li>
|
||||
* <li>轮询任务状态直到完成</li>
|
||||
* <li>下载结果文件并解析</li>
|
||||
* </ol>
|
||||
*
|
||||
* @author AI开发团队
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnProperty(prefix = "ai.embedding", name = "batch-enabled", havingValue = "true", matchIfMissing = false)
|
||||
public class QwenBatchEmbeddingService {
|
||||
|
||||
private final EmbeddingProperties embeddingProperties;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
|
||||
|
||||
// 批量API的基础URL
|
||||
private static final String BATCH_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1/batch";
|
||||
private static final String FILES_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1/files";
|
||||
|
||||
// 支持的批量大小限制
|
||||
private static final int MAX_BATCH_SIZE = 50000;
|
||||
private static final long MAX_FILE_SIZE_MB = 500;
|
||||
|
||||
/**
|
||||
* 批量向量化请求
|
||||
*/
|
||||
@Data
|
||||
public static class BatchEmbeddingRequest {
|
||||
@JsonProperty("custom_id")
|
||||
private String customId;
|
||||
|
||||
private String method = "POST";
|
||||
|
||||
private String url = "/v1/embeddings";
|
||||
|
||||
private Map<String, Object> body;
|
||||
|
||||
public BatchEmbeddingRequest(String customId, String text, String model) {
|
||||
this.customId = customId;
|
||||
this.body = Map.of(
|
||||
"model", model,
|
||||
"input", text
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量任务状态
|
||||
*/
|
||||
@Data
|
||||
public static class BatchJob {
|
||||
private String id;
|
||||
private String object;
|
||||
private String status;
|
||||
@JsonProperty("created_at")
|
||||
private long createdAt;
|
||||
@JsonProperty("input_file_id")
|
||||
private String inputFileId;
|
||||
@JsonProperty("output_file_id")
|
||||
private String outputFileId;
|
||||
@JsonProperty("error_file_id")
|
||||
private String errorFileId;
|
||||
@JsonProperty("completion_window")
|
||||
private String completionWindow;
|
||||
private Map<String, Integer> requestCounts;
|
||||
private Object metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件上传响应
|
||||
*/
|
||||
@Data
|
||||
public static class FileResponse {
|
||||
private String id;
|
||||
private String object;
|
||||
private long bytes;
|
||||
@JsonProperty("created_at")
|
||||
private long createdAt;
|
||||
private String filename;
|
||||
private String purpose;
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步批量向量化 (保持原有接口)
|
||||
*
|
||||
* @param texts 要向量化的文本列表
|
||||
* @param jobId 任务标识符
|
||||
* @return CompletableFuture包装的结果Map,key为原始索引,value为向量
|
||||
*/
|
||||
public CompletableFuture<Map<Integer, double[]>> batchEmbeddingAsync(List<String> texts, String jobId) {
|
||||
return CompletableFuture.completedFuture(batchEmbeddingSync(texts, jobId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步批量向量化 - 串行处理避免请求频率过快
|
||||
*
|
||||
* @param texts 要向量化的文本列表
|
||||
* @param jobId 任务标识符
|
||||
* @return 结果Map,key为原始索引,value为向量
|
||||
*/
|
||||
public Map<Integer, double[]> batchEmbeddingSync(List<String> texts, String jobId) {
|
||||
if (texts == null || texts.isEmpty()) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
|
||||
List<String> finalTexts;
|
||||
if (texts.size() > MAX_BATCH_SIZE) {
|
||||
log.warn("批量大小 {} 超过最大限制 {},将只处理前{}条", texts.size(), MAX_BATCH_SIZE, MAX_BATCH_SIZE);
|
||||
finalTexts = texts.subList(0, MAX_BATCH_SIZE);
|
||||
} else {
|
||||
finalTexts = texts;
|
||||
}
|
||||
|
||||
try {
|
||||
log.info("开始同步批量向量化任务: {}, 文本数量: {}", jobId, finalTexts.size());
|
||||
|
||||
// 1. 创建JSONL文件
|
||||
Path inputFile = createBatchInputFile(finalTexts, jobId);
|
||||
log.info("创建输入文件: {}", inputFile);
|
||||
|
||||
// 2. 上传文件(添加延迟避免频率限制)
|
||||
String fileId = uploadFileWithRetry(inputFile);
|
||||
log.info("文件上传成功,ID: {}", fileId);
|
||||
|
||||
// 3. 创建批量任务(添加延迟)
|
||||
Thread.sleep(2000); // 2秒延迟避免请求过快
|
||||
BatchJob batchJob = createBatchJob(fileId, "24h");
|
||||
log.info("批量任务创建成功,任务ID: {}", batchJob.getId());
|
||||
|
||||
// 4. 等待任务完成(串行轮询)
|
||||
BatchJob completedJob = waitForJobCompletionSync(batchJob.getId());
|
||||
log.info("批量任务完成: {}", completedJob.getId());
|
||||
|
||||
// 5. 下载并解析结果
|
||||
Map<Integer, double[]> results = downloadAndParseResults(completedJob, finalTexts.size());
|
||||
log.info("批量向量化完成,成功处理: {}/{}", results.size(), finalTexts.size());
|
||||
|
||||
// 6. 清理临时文件
|
||||
cleanupTempFiles(inputFile);
|
||||
|
||||
return results;
|
||||
|
||||
} catch (Exception e) {
|
||||
ErrorLogUtils.saveRuntimeError("QwenBatchEmbeddingService.batchEmbeddingSync", e,
|
||||
"同步批量向量化任务失败: " + jobId);
|
||||
log.error("同步批量向量化任务失败: {}", jobId, e);
|
||||
return new HashMap<>();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建JSONL格式的批量输入文件
|
||||
*/
|
||||
private Path createBatchInputFile(List<String> texts, String jobId) throws IOException {
|
||||
Path tempDir = Paths.get(System.getProperty("java.io.tmpdir"), "qwen-batch");
|
||||
Files.createDirectories(tempDir);
|
||||
|
||||
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"));
|
||||
Path inputFile = tempDir.resolve(String.format("batch_embedding_%s_%s.jsonl", jobId, timestamp));
|
||||
|
||||
try (FileWriter writer = new FileWriter(inputFile.toFile())) {
|
||||
for (int i = 0; i < texts.size(); i++) {
|
||||
String text = texts.get(i);
|
||||
if (text == null || text.trim().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
BatchEmbeddingRequest request = new BatchEmbeddingRequest(
|
||||
String.valueOf(i),
|
||||
text.trim(),
|
||||
embeddingProperties.getModel()
|
||||
);
|
||||
|
||||
writer.write(objectMapper.writeValueAsString(request));
|
||||
writer.write("\n");
|
||||
}
|
||||
}
|
||||
|
||||
long fileSizeBytes = Files.size(inputFile);
|
||||
long fileSizeMB = fileSizeBytes / (1024 * 1024);
|
||||
|
||||
if (fileSizeMB > MAX_FILE_SIZE_MB) {
|
||||
throw new IllegalStateException(String.format(
|
||||
"文件大小 %d MB 超过限制 %d MB", fileSizeMB, MAX_FILE_SIZE_MB));
|
||||
}
|
||||
|
||||
log.info("批量输入文件创建完成: {}, 大小: {} MB", inputFile, fileSizeMB);
|
||||
return inputFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件到阿里云
|
||||
*/
|
||||
private String uploadFile(Path filePath) throws IOException {
|
||||
OkHttpClient client = new OkHttpClient.Builder()
|
||||
.connectTimeout(60, TimeUnit.SECONDS)
|
||||
.writeTimeout(300, TimeUnit.SECONDS) // 5分钟上传超时
|
||||
.readTimeout(60, TimeUnit.SECONDS)
|
||||
.build();
|
||||
|
||||
RequestBody fileBody = RequestBody.create(
|
||||
MediaType.parse("application/octet-stream"),
|
||||
filePath.toFile()
|
||||
);
|
||||
|
||||
RequestBody requestBody = new MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart("file", filePath.getFileName().toString(), fileBody)
|
||||
.addFormDataPart("purpose", "batch")
|
||||
.build();
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url(FILES_BASE_URL)
|
||||
.addHeader("Authorization", "Bearer " + embeddingProperties.getApiKey())
|
||||
.addHeader("Content-Type", "multipart/form-data")
|
||||
.post(requestBody)
|
||||
.build();
|
||||
|
||||
try (Response response = client.newCall(request).execute()) {
|
||||
if (!response.isSuccessful()) {
|
||||
String errorBody = response.body() != null ? response.body().string() : "Unknown error";
|
||||
throw new IOException("文件上传失败: " + response.code() + " " + errorBody);
|
||||
}
|
||||
|
||||
String responseBody = response.body().string();
|
||||
FileResponse fileResponse = objectMapper.readValue(responseBody, FileResponse.class);
|
||||
return fileResponse.getId();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 带重试机制的文件上传 - 避免请求频率限制
|
||||
*/
|
||||
private String uploadFileWithRetry(Path filePath) throws IOException {
|
||||
int maxRetries = 3;
|
||||
long baseDelay = 5000; // 基础延迟5秒
|
||||
|
||||
for (int attempt = 0; attempt < maxRetries; attempt++) {
|
||||
try {
|
||||
if (attempt > 0) {
|
||||
// 指数退避延迟
|
||||
long delay = baseDelay * (1L << (attempt - 1));
|
||||
log.info("文件上传重试 {}/{}, 延迟{}ms后重试", attempt + 1, maxRetries, delay);
|
||||
Thread.sleep(delay);
|
||||
}
|
||||
|
||||
return uploadFile(filePath);
|
||||
|
||||
} catch (IOException e) {
|
||||
log.warn("文件上传失败 (尝试 {}/{}): {}", attempt + 1, maxRetries, e.getMessage());
|
||||
if (attempt == maxRetries - 1) {
|
||||
throw e; // 最后一次尝试失败时抛出异常
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IOException("上传被中断", e);
|
||||
}
|
||||
}
|
||||
|
||||
throw new IOException("文件上传失败,已达到最大重试次数");
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建批量任务
|
||||
*/
|
||||
private BatchJob createBatchJob(String inputFileId, String completionWindow) throws IOException {
|
||||
OkHttpClient client = new OkHttpClient.Builder()
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.build();
|
||||
|
||||
Map<String, Object> requestBody = Map.of(
|
||||
"input_file_id", inputFileId,
|
||||
"endpoint", "/v1/embeddings",
|
||||
"completion_window", completionWindow
|
||||
);
|
||||
|
||||
RequestBody body = RequestBody.create(
|
||||
MediaType.parse("application/json"),
|
||||
objectMapper.writeValueAsString(requestBody)
|
||||
);
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url(BATCH_BASE_URL)
|
||||
.addHeader("Authorization", "Bearer " + embeddingProperties.getApiKey())
|
||||
.addHeader("Content-Type", "application/json")
|
||||
.post(body)
|
||||
.build();
|
||||
|
||||
try (Response response = client.newCall(request).execute()) {
|
||||
if (!response.isSuccessful()) {
|
||||
String errorBody = response.body() != null ? response.body().string() : "Unknown error";
|
||||
throw new IOException("批量任务创建失败: " + response.code() + " " + errorBody);
|
||||
}
|
||||
|
||||
String responseBody = response.body().string();
|
||||
return objectMapper.readValue(responseBody, BatchJob.class);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待批量任务完成
|
||||
*/
|
||||
private BatchJob waitForJobCompletion(String jobId) throws IOException, InterruptedException {
|
||||
int maxAttempts = 720; // 最多等待12小时(每分钟检查一次)
|
||||
int attempt = 0;
|
||||
|
||||
while (attempt < maxAttempts) {
|
||||
BatchJob job = getBatchJobStatus(jobId);
|
||||
|
||||
log.info("批量任务状态检查 [{}/{}]: {} - {}",
|
||||
attempt + 1, maxAttempts, job.getId(), job.getStatus());
|
||||
|
||||
switch (job.getStatus()) {
|
||||
case "completed":
|
||||
return job;
|
||||
case "failed":
|
||||
case "expired":
|
||||
case "cancelled":
|
||||
throw new RuntimeException("批量任务失败,状态: " + job.getStatus());
|
||||
case "in_progress":
|
||||
case "finalizing":
|
||||
case "validating":
|
||||
// 继续等待
|
||||
break;
|
||||
default:
|
||||
log.warn("未知的任务状态: {}", job.getStatus());
|
||||
}
|
||||
|
||||
// 等待1分钟后再次检查
|
||||
Thread.sleep(60000);
|
||||
attempt++;
|
||||
}
|
||||
|
||||
throw new RuntimeException("批量任务超时,任务ID: " + jobId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步等待批量任务完成 - 串行轮询避免请求过快
|
||||
*/
|
||||
private BatchJob waitForJobCompletionSync(String jobId) throws IOException, InterruptedException {
|
||||
int maxAttempts = 720; // 最多等待12小时(每分钟检查一次)
|
||||
int attempt = 0;
|
||||
long pollInterval = 60000; // 1分钟轮询间隔
|
||||
|
||||
while (attempt < maxAttempts) {
|
||||
try {
|
||||
// 添加延迟避免频繁请求
|
||||
if (attempt > 0) {
|
||||
Thread.sleep(pollInterval);
|
||||
}
|
||||
|
||||
BatchJob job = getBatchJobStatusWithRetry(jobId);
|
||||
|
||||
log.info("同步批量任务状态检查 [{}/{}]: {} - {}",
|
||||
attempt + 1, maxAttempts, job.getId(), job.getStatus());
|
||||
|
||||
switch (job.getStatus()) {
|
||||
case "completed":
|
||||
return job;
|
||||
case "failed":
|
||||
case "expired":
|
||||
case "cancelled":
|
||||
throw new RuntimeException("批量任务失败,状态: " + job.getStatus());
|
||||
case "in_progress":
|
||||
case "finalizing":
|
||||
case "validating":
|
||||
// 继续等待
|
||||
break;
|
||||
default:
|
||||
log.warn("未知的任务状态: {}", job.getStatus());
|
||||
}
|
||||
|
||||
attempt++;
|
||||
|
||||
} catch (IOException e) {
|
||||
log.warn("获取任务状态失败 (尝试 {}/{}): {}", attempt + 1, maxAttempts, e.getMessage());
|
||||
if (attempt >= maxAttempts - 1) {
|
||||
throw e;
|
||||
}
|
||||
// 遇到错误时延迟更长时间再重试
|
||||
Thread.sleep(pollInterval * 2);
|
||||
attempt++;
|
||||
}
|
||||
}
|
||||
|
||||
throw new RuntimeException("同步批量任务超时,任务ID: " + jobId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 带重试机制获取批量任务状态
|
||||
*/
|
||||
private BatchJob getBatchJobStatusWithRetry(String jobId) throws IOException {
|
||||
int maxRetries = 3;
|
||||
long baseDelay = 3000; // 基础延迟3秒
|
||||
|
||||
for (int attempt = 0; attempt < maxRetries; attempt++) {
|
||||
try {
|
||||
if (attempt > 0) {
|
||||
long delay = baseDelay * (1L << (attempt - 1));
|
||||
Thread.sleep(delay);
|
||||
}
|
||||
|
||||
return getBatchJobStatus(jobId);
|
||||
|
||||
} catch (IOException e) {
|
||||
log.warn("获取任务状态失败 (尝试 {}/{}): {}", attempt + 1, maxRetries, e.getMessage());
|
||||
if (attempt == maxRetries - 1) {
|
||||
throw e;
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IOException("状态查询被中断", e);
|
||||
}
|
||||
}
|
||||
|
||||
throw new IOException("获取任务状态失败,已达到最大重试次数");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取批量任务状态
|
||||
*/
|
||||
private BatchJob getBatchJobStatus(String jobId) throws IOException {
|
||||
OkHttpClient client = new OkHttpClient.Builder()
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.build();
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url(BATCH_BASE_URL + "/" + jobId)
|
||||
.addHeader("Authorization", "Bearer " + embeddingProperties.getApiKey())
|
||||
.get()
|
||||
.build();
|
||||
|
||||
try (Response response = client.newCall(request).execute()) {
|
||||
if (!response.isSuccessful()) {
|
||||
String errorBody = response.body() != null ? response.body().string() : "Unknown error";
|
||||
throw new IOException("获取任务状态失败: " + response.code() + " " + errorBody);
|
||||
}
|
||||
|
||||
String responseBody = response.body().string();
|
||||
return objectMapper.readValue(responseBody, BatchJob.class);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载并解析结果文件
|
||||
*/
|
||||
private Map<Integer, double[]> downloadAndParseResults(BatchJob completedJob, int expectedCount) throws IOException {
|
||||
if (completedJob.getOutputFileId() == null) {
|
||||
throw new RuntimeException("批量任务没有输出文件");
|
||||
}
|
||||
|
||||
// 下载结果文件
|
||||
String resultsContent = downloadFile(completedJob.getOutputFileId());
|
||||
|
||||
// 解析JSONL结果
|
||||
Map<Integer, double[]> results = new HashMap<>();
|
||||
String[] lines = resultsContent.split("\n");
|
||||
|
||||
for (String line : lines) {
|
||||
if (line.trim().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
JsonNode resultNode = objectMapper.readTree(line);
|
||||
String customId = resultNode.get("custom_id").asText();
|
||||
int index = Integer.parseInt(customId);
|
||||
|
||||
JsonNode responseNode = resultNode.get("response");
|
||||
if (responseNode != null && responseNode.get("status_code").asInt() == 200) {
|
||||
JsonNode bodyNode = responseNode.get("body");
|
||||
JsonNode dataNode = bodyNode.get("data").get(0);
|
||||
JsonNode embeddingNode = dataNode.get("embedding");
|
||||
|
||||
double[] embedding = objectMapper.convertValue(embeddingNode, double[].class);
|
||||
results.put(index, embedding);
|
||||
} else {
|
||||
log.warn("批量请求 {} 失败: {}", customId, responseNode);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("解析批量结果行失败: {}", line, e);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("批量结果解析完成: {}/{} 成功", results.size(), expectedCount);
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文件内容
|
||||
*/
|
||||
private String downloadFile(String fileId) throws IOException {
|
||||
OkHttpClient client = new OkHttpClient.Builder()
|
||||
.connectTimeout(60, TimeUnit.SECONDS)
|
||||
.readTimeout(300, TimeUnit.SECONDS) // 5分钟下载超时
|
||||
.build();
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url(FILES_BASE_URL + "/" + fileId + "/content")
|
||||
.addHeader("Authorization", "Bearer " + embeddingProperties.getApiKey())
|
||||
.get()
|
||||
.build();
|
||||
|
||||
try (Response response = client.newCall(request).execute()) {
|
||||
if (!response.isSuccessful()) {
|
||||
String errorBody = response.body() != null ? response.body().string() : "Unknown error";
|
||||
throw new IOException("文件下载失败: " + response.code() + " " + errorBody);
|
||||
}
|
||||
|
||||
return response.body().string();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理临时文件
|
||||
*/
|
||||
private void cleanupTempFiles(Path... files) {
|
||||
for (Path file : files) {
|
||||
try {
|
||||
if (Files.exists(file)) {
|
||||
Files.delete(file);
|
||||
log.debug("临时文件已删除: {}", file);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.warn("删除临时文件失败: {}", file, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取批量服务的性能指标
|
||||
*/
|
||||
public Map<String, Object> getBatchPerformanceMetrics() {
|
||||
Map<String, Object> metrics = new HashMap<>();
|
||||
metrics.put("maxBatchSize", MAX_BATCH_SIZE);
|
||||
metrics.put("maxFileSizeMB", MAX_FILE_SIZE_MB);
|
||||
metrics.put("costReduction", "50%");
|
||||
metrics.put("supportedFormats", Arrays.asList("JSONL"));
|
||||
metrics.put("completionWindow", Arrays.asList("24h", "168h", "336h"));
|
||||
return metrics;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查批量服务是否可用
|
||||
*/
|
||||
public boolean isAvailable() {
|
||||
return embeddingProperties.getApiKey() != null
|
||||
&& !embeddingProperties.getApiKey().isEmpty();
|
||||
}
|
||||
}
|
||||
|
|
@ -47,6 +47,10 @@ public class QwenEmbeddingService {
|
|||
private final OkHttpClient httpClient;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final EmbeddingCacheService cacheService;
|
||||
|
||||
// 请求频率控制
|
||||
private volatile long lastRequestTime = 0;
|
||||
private static final long MIN_REQUEST_INTERVAL = 1000; // 最小请求间隔1秒
|
||||
|
||||
@Autowired
|
||||
public QwenEmbeddingService(EmbeddingProperties embeddingProperties,
|
||||
|
|
@ -333,6 +337,9 @@ public class QwenEmbeddingService {
|
|||
* 调用DashScope Embedding API(不使用缓存)- 增强性能统计版本
|
||||
*/
|
||||
private List<double[]> getEmbeddingsFromAPI(List<String> texts) throws IOException {
|
||||
// 频率限制:确保请求间隔
|
||||
enforceRateLimit();
|
||||
|
||||
long apiStartTime = System.currentTimeMillis();
|
||||
boolean success = false;
|
||||
|
||||
|
|
@ -527,6 +534,27 @@ public class QwenEmbeddingService {
|
|||
return testResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制执行请求频率限制
|
||||
*/
|
||||
private synchronized void enforceRateLimit() {
|
||||
long currentTime = System.currentTimeMillis();
|
||||
long timeSinceLastRequest = currentTime - lastRequestTime;
|
||||
|
||||
if (timeSinceLastRequest < MIN_REQUEST_INTERVAL) {
|
||||
long sleepTime = MIN_REQUEST_INTERVAL - timeSinceLastRequest;
|
||||
try {
|
||||
log.debug("API请求频率限制,等待{}ms", sleepTime);
|
||||
Thread.sleep(sleepTime);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
log.warn("频率限制等待被中断", e);
|
||||
}
|
||||
}
|
||||
|
||||
lastRequestTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录详细的API调用统计
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -46,6 +46,8 @@ public class InterceptorConfig implements WebMvcConfigurer {
|
|||
excludes.add("/base/taskFile/downloadFile");
|
||||
// 错误日志管理接口免认证访问(用于系统监控)
|
||||
excludes.add("/api/error-logs/**");
|
||||
// AI管理界面免认证访问(内置登录机制)
|
||||
excludes.add("/ai-management/**");
|
||||
|
||||
registration.excludePathPatterns(excludes);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,14 @@ server:
|
|||
|
||||
logging:
|
||||
level:
|
||||
dao: debug
|
||||
dao: info
|
||||
youfool.dao: info
|
||||
com.chinaweal.youfool.framework.springboot.log: debug
|
||||
com.chinaweal.youfool.devops: debug
|
||||
com.chinaweal.youfool.framework.springboot.log: info
|
||||
com.chinaweal.youfool.devops: info
|
||||
# 减少MyBatis SQL输出
|
||||
com.chinaweal.youfool.devops.repair.mapper: warn
|
||||
com.chinaweal.youfool.devops.base.mapper: warn
|
||||
org.apache.ibatis: warn
|
||||
spring:
|
||||
autoconfigure:
|
||||
exclude: com.dtflys.forest.springboot.ForestAutoConfiguration
|
||||
|
|
|
|||
|
|
@ -81,15 +81,15 @@ resilience4j:
|
|||
failure-rate-threshold: 50
|
||||
slow-call-rate-threshold: 50
|
||||
slow-call-duration-threshold: 10s
|
||||
# Embedding服务熔断器配置
|
||||
# Embedding服务熔断器配置 - 优化配置以适应批量API切换
|
||||
qwen-embedding:
|
||||
register-health-indicator: true
|
||||
sliding-window-size: 10
|
||||
minimum-number-of-calls: 3
|
||||
permitted-number-of-calls-in-half-open-state: 2
|
||||
wait-duration-in-open-state: 60s
|
||||
failure-rate-threshold: 50
|
||||
slow-call-rate-threshold: 50
|
||||
sliding-window-size: 20 # 增加滑动窗口大小
|
||||
minimum-number-of-calls: 10 # 增加最小调用次数
|
||||
permitted-number-of-calls-in-half-open-state: 5 # 增加半开状态允许调用次数
|
||||
wait-duration-in-open-state: 30s # 减少开启状态等待时间
|
||||
failure-rate-threshold: 80 # 提高失败率阈值到80%
|
||||
slow-call-rate-threshold: 70 # 提高慢调用率阈值
|
||||
slow-call-duration-threshold: 15s
|
||||
retry:
|
||||
instances:
|
||||
|
|
@ -113,11 +113,11 @@ resilience4j:
|
|||
limit-for-period: 50
|
||||
limit-refresh-period: 1m
|
||||
timeout-duration: 0
|
||||
# Embedding服务限流配置
|
||||
# Embedding服务限流配置 - 降低频率避免API限制
|
||||
qwen-embedding:
|
||||
limit-for-period: 50
|
||||
limit-for-period: 20 # 降低到每分钟20次请求
|
||||
limit-refresh-period: 1m
|
||||
timeout-duration: 0
|
||||
timeout-duration: 3s # 增加超时时间,避免请求被拒绝时立即失败
|
||||
|
||||
restLog:
|
||||
ignoreServletPath: /druid,/swagger-resources,/v2/api-docs,/webjars,/websocket
|
||||
|
|
@ -198,14 +198,23 @@ ai:
|
|||
# 使用的embedding模型
|
||||
model: text-embedding-v4
|
||||
|
||||
# 批量API配置 - 阿里云模型工作室批量接口
|
||||
batch-enabled: true # 是否启用批量API
|
||||
batch-threshold: 1000 # 使用批量API的最小文本数量阈值
|
||||
|
||||
# 性能优化配置
|
||||
max-batch-size: 10 # 单次批量大小 - 阿里云API限制最大10个文本
|
||||
batch-delay: 100 # 批量间延迟(毫秒)
|
||||
timeout: 30000 # 请求超时时间(毫秒)
|
||||
|
||||
# 向量化迁移配置
|
||||
vectorization:
|
||||
# 是否启用向量化功能
|
||||
enabled: true
|
||||
# 是否启用自动同步
|
||||
auto-sync: true
|
||||
# 批处理大小
|
||||
batch-size: 100
|
||||
# 批处理大小 - 适配embedding API限制
|
||||
batch-size: 10
|
||||
# 并发线程数
|
||||
concurrent-threads: 3
|
||||
# 相似度阈值
|
||||
|
|
@ -310,8 +319,8 @@ ai:
|
|||
|
||||
# 文本相似度配置
|
||||
text-similarity:
|
||||
# 是否启用文本相似度功能
|
||||
enabled: true
|
||||
# 是否启用文本相似度功能 - 禁用启动示例避免触发限流
|
||||
enabled: false
|
||||
# 默认相似度计算方法(embedding/tfidf)
|
||||
default-method: embedding
|
||||
# 相似度阈值(0.0-1.0)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,737 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AI系统管理仪表板</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: #f8f9fa;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 1rem 2rem;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.btn-logout {
|
||||
background: rgba(255,255,255,0.2);
|
||||
color: white;
|
||||
border: 1px solid rgba(255,255,255,0.3);
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn-logout:hover {
|
||||
background: rgba(255,255,255,0.3);
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
padding: 1.5rem;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
border-left: 4px solid #667eea;
|
||||
}
|
||||
|
||||
.card h3 {
|
||||
color: #333;
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.status-healthy {
|
||||
background: #28a745;
|
||||
box-shadow: 0 0 5px rgba(40, 167, 69, 0.5);
|
||||
}
|
||||
|
||||
.status-error {
|
||||
background: #dc3545;
|
||||
box-shadow: 0 0 5px rgba(220, 53, 69, 0.5);
|
||||
}
|
||||
|
||||
.status-warning {
|
||||
background: #ffc107;
|
||||
box-shadow: 0 0 5px rgba(255, 193, 7, 0.5);
|
||||
}
|
||||
|
||||
.metric-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.metric-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-weight: bold;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #5a67d8;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #545b62;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
background: #e9ecef;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #667eea, #764ba2);
|
||||
width: 0%;
|
||||
transition: width 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 0.75rem 1rem;
|
||||
margin-bottom: 1rem;
|
||||
border-radius: 5px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background-color: #d4edda;
|
||||
border-color: #c3e6cb;
|
||||
color: #155724;
|
||||
border-left: 4px solid #28a745;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background-color: #f8d7da;
|
||||
border-color: #f5c6cb;
|
||||
color: #721c24;
|
||||
border-left: 4px solid #dc3545;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
background-color: #d1ecf1;
|
||||
border-color: #bee5eb;
|
||||
color: #0c5460;
|
||||
border-left: 4px solid #17a2b8;
|
||||
}
|
||||
|
||||
.log-container {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 5px;
|
||||
padding: 1rem;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
margin-bottom: 0.5rem;
|
||||
padding: 0.25rem;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.log-info {
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.log-success {
|
||||
background: #e8f5e8;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.log-error {
|
||||
background: #ffebee;
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 2px solid #f3f3f3;
|
||||
border-top: 2px solid #667eea;
|
||||
border-radius: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
animation: spin 1s linear infinite;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.actions-card {
|
||||
border-left-color: #28a745;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
border-left-color: #17a2b8;
|
||||
}
|
||||
|
||||
.metrics-card {
|
||||
border-left-color: #ffc107;
|
||||
}
|
||||
|
||||
.logs-card {
|
||||
border-left-color: #6f42c1;
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.logs-card {
|
||||
grid-column: span 1;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.quick-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
background: #f8f9fa;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
color: #667eea;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.875rem;
|
||||
color: #666;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<div class="header-content">
|
||||
<h1>🤖 AI系统管理仪表板</h1>
|
||||
<div class="user-info">
|
||||
<span th:if="${currentUser}">欢迎,<span th:text="${currentUser}"></span></span>
|
||||
<button class="btn-logout" onclick="logout()">退出登录</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="alert alert-success" id="successAlert"></div>
|
||||
<div class="alert alert-error" id="errorAlert"></div>
|
||||
<div class="alert alert-info" id="infoAlert"></div>
|
||||
|
||||
<div class="dashboard-grid">
|
||||
<!-- 快速操作卡片 -->
|
||||
<div class="card actions-card">
|
||||
<h3>🚀 快速操作</h3>
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<button class="btn-primary" id="startVectorizationBtn" onclick="startVectorization()">
|
||||
<span class="spinner" id="vectorizationSpinner" style="display: none;"></span>
|
||||
启动向量化任务
|
||||
</button>
|
||||
</div>
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<button class="btn-secondary" onclick="refreshStatus()">刷新状态</button>
|
||||
<button class="btn-secondary" onclick="resetCircuitBreaker()">重置熔断器</button>
|
||||
<button class="btn-secondary" onclick="viewLogs()">查看日志</button>
|
||||
</div>
|
||||
<div style="font-size: 12px; color: #666; margin-top: 1rem;">
|
||||
💡 向量化任务将处理所有历史工单数据,转换为向量格式便于AI检索
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 系统状态卡片 -->
|
||||
<div class="card status-card">
|
||||
<h3>📊 系统状态</h3>
|
||||
<div class="metric-item">
|
||||
<span>Embedding服务</span>
|
||||
<span>
|
||||
<span class="status-indicator" id="embeddingStatus"></span>
|
||||
<span id="embeddingStatusText">检查中...</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span>向量化服务</span>
|
||||
<span>
|
||||
<span class="status-indicator status-healthy"></span>
|
||||
<span>正常运行</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span>最后更新</span>
|
||||
<span class="metric-value" id="lastUpdate">-</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 向量化进度卡片 -->
|
||||
<div class="card">
|
||||
<h3>⚡ 向量化进度</h3>
|
||||
<div class="progress-container">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="progressFill"></div>
|
||||
<div class="progress-text" id="progressText">0%</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="quick-stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-value" id="processedCount">0</span>
|
||||
<span class="stat-label">已处理</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-value" id="totalCount">0</span>
|
||||
<span class="stat-label">总数量</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span>任务状态</span>
|
||||
<span class="metric-value" id="taskStatus">空闲</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 性能指标卡片 -->
|
||||
<div class="card metrics-card">
|
||||
<h3>📈 性能指标</h3>
|
||||
<div class="metric-item">
|
||||
<span>处理吞吐量</span>
|
||||
<span class="metric-value" id="throughput">0 项/秒</span>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span>平均处理时间</span>
|
||||
<span class="metric-value" id="avgProcessTime">0 ms</span>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span>连接池状态</span>
|
||||
<span class="metric-value" id="connectionPool">0/0</span>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span>缓存命中率</span>
|
||||
<span class="metric-value" id="cacheHitRate">0%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作日志卡片 -->
|
||||
<div class="card logs-card">
|
||||
<h3>📝 操作日志</h3>
|
||||
<div class="log-container" id="logContainer">
|
||||
<div class="log-entry log-info">
|
||||
<strong>[系统]</strong> AI管理系统已启动,等待操作...
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 1rem;">
|
||||
<button class="btn-secondary" onclick="clearLogs()">清空日志</button>
|
||||
<button class="btn-secondary" onclick="exportLogs()">导出日志</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let progressInterval;
|
||||
let logCount = 1;
|
||||
|
||||
// 页面加载完成后初始化
|
||||
window.addEventListener('load', function() {
|
||||
addLog('info', '仪表板加载完成');
|
||||
refreshStatus();
|
||||
startProgressMonitoring();
|
||||
});
|
||||
|
||||
// 启动向量化任务
|
||||
async function startVectorization() {
|
||||
const btn = document.getElementById('startVectorizationBtn');
|
||||
const spinner = document.getElementById('vectorizationSpinner');
|
||||
|
||||
btn.disabled = true;
|
||||
spinner.style.display = 'inline-block';
|
||||
btn.innerHTML = '<span class="spinner"></span> 启动中...';
|
||||
|
||||
addLog('info', '正在启动向量化任务...');
|
||||
|
||||
try {
|
||||
const response = await fetch('/ai-management/api/start-vectorization', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.code === 0) {
|
||||
showSuccess('向量化任务启动成功!');
|
||||
addLog('success', `向量化任务启动成功,任务ID: ${result.data.taskId}`);
|
||||
document.getElementById('taskStatus').textContent = '运行中';
|
||||
} else {
|
||||
showError(result.msg || '启动任务失败');
|
||||
addLog('error', `任务启动失败: ${result.msg}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
showError('网络错误,请重试');
|
||||
addLog('error', `网络错误: ${error.message}`);
|
||||
console.error('Start vectorization error:', error);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
spinner.style.display = 'none';
|
||||
btn.innerHTML = '启动向量化任务';
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新系统状态
|
||||
async function refreshStatus() {
|
||||
addLog('info', '正在刷新系统状态...');
|
||||
|
||||
try {
|
||||
const response = await fetch('/ai-management/api/system-status');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.code === 0) {
|
||||
const status = result.data;
|
||||
|
||||
// 更新Embedding服务状态
|
||||
const embeddingStatus = document.getElementById('embeddingStatus');
|
||||
const embeddingStatusText = document.getElementById('embeddingStatusText');
|
||||
|
||||
if (status.embeddingServiceHealthy) {
|
||||
embeddingStatus.className = 'status-indicator status-healthy';
|
||||
embeddingStatusText.textContent = '正常运行';
|
||||
} else {
|
||||
embeddingStatus.className = 'status-indicator status-error';
|
||||
embeddingStatusText.textContent = '服务异常';
|
||||
}
|
||||
|
||||
// 更新性能指标
|
||||
if (status.embeddingMetrics) {
|
||||
const metrics = status.embeddingMetrics;
|
||||
document.getElementById('connectionPool').textContent =
|
||||
`${metrics.connectionPoolIdleCount || 0}/${metrics.connectionPoolTotalCount || 0}`;
|
||||
}
|
||||
|
||||
// 更新时间
|
||||
document.getElementById('lastUpdate').textContent = new Date().toLocaleTimeString();
|
||||
|
||||
addLog('success', '系统状态刷新成功');
|
||||
} else {
|
||||
addLog('error', '状态刷新失败: ' + result.msg);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
addLog('error', `状态刷新异常: ${error.message}`);
|
||||
console.error('Refresh status error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 启动进度监控
|
||||
function startProgressMonitoring() {
|
||||
progressInterval = setInterval(updateProgress, 5000); // 每5秒更新一次
|
||||
updateProgress(); // 立即更新一次
|
||||
}
|
||||
|
||||
// 更新向量化进度
|
||||
async function updateProgress() {
|
||||
try {
|
||||
const response = await fetch('/ai-management/api/vectorization-progress');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.code === 0) {
|
||||
const progress = result.data;
|
||||
|
||||
// 更新进度条
|
||||
if (progress.processedCount && progress.totalCount) {
|
||||
const percentage = Math.round((progress.processedCount / progress.totalCount) * 100);
|
||||
document.getElementById('progressFill').style.width = percentage + '%';
|
||||
document.getElementById('progressText').textContent = percentage + '%';
|
||||
document.getElementById('processedCount').textContent = progress.processedCount;
|
||||
document.getElementById('totalCount').textContent = progress.totalCount;
|
||||
}
|
||||
|
||||
// 更新任务状态
|
||||
if (progress.status) {
|
||||
document.getElementById('taskStatus').textContent =
|
||||
progress.status === 'COMPLETED' ? '已完成' :
|
||||
progress.status === 'RUNNING' ? '运行中' :
|
||||
progress.status === 'FAILED' ? '失败' : '空闲';
|
||||
}
|
||||
|
||||
// 更新性能指标
|
||||
if (progress.averageProcessingTimeMs) {
|
||||
document.getElementById('avgProcessTime').textContent =
|
||||
Math.round(progress.averageProcessingTimeMs) + ' ms';
|
||||
}
|
||||
|
||||
if (progress.processingThroughputPerSecond) {
|
||||
document.getElementById('throughput').textContent =
|
||||
progress.processingThroughputPerSecond.toFixed(1) + ' 项/秒';
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Update progress error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 用户登出
|
||||
async function logout() {
|
||||
if (confirm('确定要退出登录吗?')) {
|
||||
try {
|
||||
await fetch('/ai-management/api/logout', { method: 'POST' });
|
||||
window.location.href = '/ai-management/login';
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error);
|
||||
window.location.href = '/ai-management/login';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 重置熔断器
|
||||
async function resetCircuitBreaker() {
|
||||
if (!confirm('确定要重置熔断器吗?这将允许embedding服务重新接受调用。')) {
|
||||
return;
|
||||
}
|
||||
|
||||
addLog('info', '正在重置熔断器...');
|
||||
|
||||
try {
|
||||
const response = await fetch('/ai-management/api/reset-circuit-breaker', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.code === 0) {
|
||||
showSuccess('熔断器重置成功!');
|
||||
addLog('success', '熔断器重置成功,embedding服务已恢复');
|
||||
// 重新刷新状态
|
||||
setTimeout(refreshStatus, 1000);
|
||||
} else {
|
||||
showError(result.msg || '重置失败');
|
||||
addLog('error', `熔断器重置失败: ${result.msg}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
showError('网络错误,请重试');
|
||||
addLog('error', `重置熔断器异常: ${error.message}`);
|
||||
console.error('Reset circuit breaker error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 查看日志
|
||||
function viewLogs() {
|
||||
window.open('/swagger-ui.html', '_blank');
|
||||
addLog('info', '已打开API文档页面');
|
||||
}
|
||||
|
||||
// 添加日志条目
|
||||
function addLog(type, message) {
|
||||
const logContainer = document.getElementById('logContainer');
|
||||
const logEntry = document.createElement('div');
|
||||
logEntry.className = `log-entry log-${type}`;
|
||||
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
logEntry.innerHTML = `<strong>[${timestamp}]</strong> ${message}`;
|
||||
|
||||
logContainer.appendChild(logEntry);
|
||||
logContainer.scrollTop = logContainer.scrollHeight;
|
||||
|
||||
// 限制日志条目数量
|
||||
if (logContainer.children.length > 100) {
|
||||
logContainer.removeChild(logContainer.firstChild);
|
||||
}
|
||||
|
||||
logCount++;
|
||||
}
|
||||
|
||||
// 清空日志
|
||||
function clearLogs() {
|
||||
document.getElementById('logContainer').innerHTML = '';
|
||||
addLog('info', '日志已清空');
|
||||
}
|
||||
|
||||
// 导出日志
|
||||
function exportLogs() {
|
||||
const logs = document.getElementById('logContainer').innerText;
|
||||
const blob = new Blob([logs], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `ai-management-logs-${new Date().toISOString().slice(0,10)}.txt`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
addLog('info', '日志已导出');
|
||||
}
|
||||
|
||||
// 显示成功消息
|
||||
function showSuccess(message) {
|
||||
hideAlerts();
|
||||
const alert = document.getElementById('successAlert');
|
||||
alert.textContent = message;
|
||||
alert.style.display = 'block';
|
||||
setTimeout(hideAlerts, 5000);
|
||||
}
|
||||
|
||||
// 显示错误消息
|
||||
function showError(message) {
|
||||
hideAlerts();
|
||||
const alert = document.getElementById('errorAlert');
|
||||
alert.textContent = message;
|
||||
alert.style.display = 'block';
|
||||
setTimeout(hideAlerts, 8000);
|
||||
}
|
||||
|
||||
// 显示信息消息
|
||||
function showInfo(message) {
|
||||
hideAlerts();
|
||||
const alert = document.getElementById('infoAlert');
|
||||
alert.textContent = message;
|
||||
alert.style.display = 'block';
|
||||
setTimeout(hideAlerts, 5000);
|
||||
}
|
||||
|
||||
// 隐藏所有提示
|
||||
function hideAlerts() {
|
||||
document.getElementById('successAlert').style.display = 'none';
|
||||
document.getElementById('errorAlert').style.display = 'none';
|
||||
document.getElementById('infoAlert').style.display = 'none';
|
||||
}
|
||||
|
||||
// 页面卸载时清理
|
||||
window.addEventListener('beforeunload', function() {
|
||||
if (progressInterval) {
|
||||
clearInterval(progressInterval);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AI系统管理 - 错误页面</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.error-container {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 15px 25px rgba(0,0,0,0.1);
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 4rem;
|
||||
color: #dc3545;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
color: #333;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #666;
|
||||
margin-bottom: 2rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 5px;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.3s;
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #5a67d8;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #545b62;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="error-container">
|
||||
<div class="error-icon">⚠️</div>
|
||||
<h1 class="error-title">系统错误</h1>
|
||||
<p class="error-message" th:text="${error} ?: '抱歉,系统遇到了一个错误。请稍后重试。'"></p>
|
||||
<div>
|
||||
<a href="/ai-management" class="btn">返回首页</a>
|
||||
<a href="/ai-management/login" class="btn btn-secondary">重新登录</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,465 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AI系统管理 - 用户登录</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 15px 25px rgba(0,0,0,0.1);
|
||||
width: 100%;
|
||||
max-width: 450px;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.login-header h1 {
|
||||
color: #333;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.login-header p {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #5a67d8;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.external-login {
|
||||
margin-top: 2rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.external-login h3 {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.external-login .btn {
|
||||
background: #28a745;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.external-login .btn:hover {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
border-radius: 5px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background-color: #d4edda;
|
||||
border-color: #c3e6cb;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background-color: #f8d7da;
|
||||
border-color: #f5c6cb;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: none;
|
||||
text-align: center;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 2px solid #f3f3f3;
|
||||
border-top: 2px solid #667eea;
|
||||
border-radius: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
animation: spin 1s linear infinite;
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: #e3f2fd;
|
||||
border: 1px solid #bbdefb;
|
||||
padding: 1rem;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 14px;
|
||||
color: #1976d2;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.info-box code {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.login-options {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.option-tabs {
|
||||
display: flex;
|
||||
background: #f8f9fa;
|
||||
border-radius: 5px;
|
||||
padding: 0.25rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.option-tab {
|
||||
flex: 1;
|
||||
padding: 0.5rem;
|
||||
text-align: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
border-radius: 3px;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.option-tab.active {
|
||||
background: white;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
color: #667eea;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.login-option {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.login-option.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.quick-select-btn {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
color: #495057;
|
||||
padding: 4px 8px;
|
||||
margin: 0 3px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.quick-select-btn:hover {
|
||||
background: #e9ecef;
|
||||
border-color: #adb5bd;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<div class="login-header">
|
||||
<h1>🤖 AI系统管理</h1>
|
||||
<p>DevOps运维管理系统 - AI功能管理界面</p>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<strong>💡 温馨提示:</strong>
|
||||
<br>• UserId登录:无需密码,输入任意用户标识即可登录
|
||||
<br>• 测试建议:可以使用 <code>admin</code>、<code>test</code>、<code>demo</code> 等
|
||||
<br>• 或者直接输入您的姓名/工号等标识
|
||||
</div>
|
||||
|
||||
<div class="alert alert-success" id="successAlert"></div>
|
||||
<div class="alert alert-error" id="errorAlert"></div>
|
||||
|
||||
<div class="login-options">
|
||||
<div class="option-tabs">
|
||||
<button class="option-tab active" onclick="switchTab('userid')">UserId登录</button>
|
||||
<button class="option-tab" onclick="switchTab('username')">用户名登录</button>
|
||||
<button class="option-tab" onclick="switchTab('external')">外部系统</button>
|
||||
</div>
|
||||
|
||||
<!-- UserId登录 -->
|
||||
<div class="login-option active" id="userid-option">
|
||||
<form class="login-form" onsubmit="loginWithUserId(event)">
|
||||
<div class="form-group">
|
||||
<label for="userId">UserId</label>
|
||||
<input type="text" id="userId" name="userId" placeholder="输入任意用户标识,如:admin、test、demo等" required>
|
||||
<div style="margin-top: 10px; text-align: center;">
|
||||
<small>快速选择:</small>
|
||||
<button type="button" class="quick-select-btn" onclick="selectUserId('admin')">admin</button>
|
||||
<button type="button" class="quick-select-btn" onclick="selectUserId('test')">test</button>
|
||||
<button type="button" class="quick-select-btn" onclick="selectUserId('demo')">demo</button>
|
||||
<button type="button" class="quick-select-btn" onclick="selectUserId('manager')">manager</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn" id="userIdBtn">立即登录</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 用户名登录 -->
|
||||
<div class="login-option" id="username-option">
|
||||
<form class="login-form" onsubmit="loginWithUsername(event)">
|
||||
<div class="form-group">
|
||||
<label for="username">用户名</label>
|
||||
<input type="text" id="username" name="username" placeholder="输入用户名" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">密码</label>
|
||||
<input type="password" id="password" name="password" placeholder="输入密码" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="vcode">验证码</label>
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<input type="text" id="vcode" name="vcode" placeholder="验证码" required style="flex: 1;">
|
||||
<button type="button" onclick="refreshVcode()" style="padding: 0.75rem 1rem; background: #6c757d; color: white; border: none; border-radius: 5px;">获取验证码</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn" id="usernameBtn">登录</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 外部系统登录 -->
|
||||
<div class="login-option" id="external-option">
|
||||
<div class="external-login">
|
||||
<h3>外部业务系统登录</h3>
|
||||
<button class="btn" onclick="redirectToExternal('fs')">
|
||||
🏢 佛山系统登录
|
||||
</button>
|
||||
<button class="btn" onclick="redirectToExternal('sd')">
|
||||
🏢 顺德系统登录
|
||||
</button>
|
||||
<div style="font-size: 12px; color: #666; text-align: center; margin-top: 1rem;">
|
||||
外部系统登录完成后将自动跳转回来
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="loading" id="loading">
|
||||
<div class="spinner"></div>
|
||||
正在登录中...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 切换登录选项卡
|
||||
function switchTab(tabName) {
|
||||
// 隐藏所有选项
|
||||
document.querySelectorAll('.login-option').forEach(option => {
|
||||
option.classList.remove('active');
|
||||
});
|
||||
document.querySelectorAll('.option-tab').forEach(tab => {
|
||||
tab.classList.remove('active');
|
||||
});
|
||||
|
||||
// 显示选中的选项
|
||||
document.getElementById(tabName + '-option').classList.add('active');
|
||||
event.target.classList.add('active');
|
||||
|
||||
hideAlerts();
|
||||
}
|
||||
|
||||
// UserId登录
|
||||
async function loginWithUserId(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const userId = document.getElementById('userId').value.trim();
|
||||
if (!userId) {
|
||||
showError('请输入UserId');
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('userIdBtn');
|
||||
const loading = document.getElementById('loading');
|
||||
|
||||
btn.disabled = true;
|
||||
btn.textContent = '登录中...';
|
||||
loading.style.display = 'block';
|
||||
|
||||
try {
|
||||
const response = await fetch('/ai-management/api/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
userId: userId,
|
||||
loginType: 'userId'
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.code === 0) {
|
||||
showSuccess('登录成功!正在跳转... <a href="/ai-management" style="color: #007bff; text-decoration: underline;">点击这里手动跳转</a>');
|
||||
// 多重跳转机制确保页面跳转
|
||||
setTimeout(() => {
|
||||
window.location.href = '/ai-management';
|
||||
}, 800);
|
||||
// 备用跳转机制
|
||||
setTimeout(() => {
|
||||
if (window.location.pathname.includes('login')) {
|
||||
window.location.replace('/ai-management');
|
||||
}
|
||||
}, 2000);
|
||||
} else {
|
||||
showError(result.msg || '登录失败');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
showError('网络错误,请重试');
|
||||
console.error('Login error:', error);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '立即登录';
|
||||
loading.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// 用户名登录
|
||||
async function loginWithUsername(event) {
|
||||
event.preventDefault();
|
||||
showError('用户名登录功能开发中,请使用UserId登录');
|
||||
}
|
||||
|
||||
// 刷新验证码
|
||||
function refreshVcode() {
|
||||
// 打开验证码获取URL
|
||||
window.open('/user/vcode', '_blank', 'width=300,height=200');
|
||||
}
|
||||
|
||||
// 外部系统登录
|
||||
function redirectToExternal(system) {
|
||||
const urls = {
|
||||
'fs': 'http://121.8.152.130:9888/tzrysb/user/loginDevops',
|
||||
'sd': 'http://121.8.152.130:9888/tzrysb/user/loginDevops'
|
||||
};
|
||||
|
||||
if (urls[system]) {
|
||||
window.open(urls[system], '_blank');
|
||||
showSuccess('已打开外部系统登录页面,登录完成后请关闭该页面并刷新本页面');
|
||||
}
|
||||
}
|
||||
|
||||
// 显示成功消息
|
||||
function showSuccess(message) {
|
||||
hideAlerts();
|
||||
const alert = document.getElementById('successAlert');
|
||||
alert.innerHTML = message;
|
||||
alert.style.display = 'block';
|
||||
}
|
||||
|
||||
// 显示错误消息
|
||||
function showError(message) {
|
||||
hideAlerts();
|
||||
const alert = document.getElementById('errorAlert');
|
||||
alert.textContent = message;
|
||||
alert.style.display = 'block';
|
||||
}
|
||||
|
||||
// 隐藏所有提示
|
||||
function hideAlerts() {
|
||||
document.getElementById('successAlert').style.display = 'none';
|
||||
document.getElementById('errorAlert').style.display = 'none';
|
||||
}
|
||||
|
||||
// 快速选择UserId
|
||||
function selectUserId(userId) {
|
||||
document.getElementById('userId').value = userId;
|
||||
document.getElementById('userId').focus();
|
||||
}
|
||||
|
||||
// 页面加载完成后检查登录状态
|
||||
window.addEventListener('load', function() {
|
||||
// 如果URL中有成功参数,显示登录成功消息
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams.get('login') === 'success') {
|
||||
showSuccess('外部系统登录成功!');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -50,7 +50,7 @@ class VectorizationPerformanceIntegrationTest {
|
|||
void testVectorizationConfiguration() {
|
||||
// 创建向量化服务实例进行配置测试
|
||||
RepairVectorizationService service = new RepairVectorizationService(
|
||||
null, null, null, null);
|
||||
null, null, null, null, null);
|
||||
|
||||
// 测试性能指标获取(默认值)
|
||||
Map<String, Object> metrics = service.getPerformanceMetrics();
|
||||
|
|
@ -75,7 +75,7 @@ class VectorizationPerformanceIntegrationTest {
|
|||
@DisplayName("测试性能报告生成")
|
||||
void testPerformanceReportGeneration() {
|
||||
RepairVectorizationService service = new RepairVectorizationService(
|
||||
null, null, null, null);
|
||||
null, null, null, null, null);
|
||||
|
||||
String report = service.getPerformanceReport();
|
||||
|
||||
|
|
@ -251,7 +251,7 @@ class VectorizationPerformanceIntegrationTest {
|
|||
};
|
||||
|
||||
RepairVectorizationService service = new RepairVectorizationService(
|
||||
null, null, null, null);
|
||||
null, null, null, null, null);
|
||||
Map<String, Object> metrics = service.getPerformanceMetrics();
|
||||
|
||||
for (String expectedMetric : expectedMetrics) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue