From 63b01db83d9aad8a15276b4236a9a22e4aeef065 Mon Sep 17 00:00:00 2001
From: Junjie <fallin.jie@qq.com>
Date: 星期二, 05 五月 2026 12:30:59 +0800
Subject: [PATCH] # Agent数据分析V3.0.1.7
---
src/main/java/com/zy/ai/service/impl/DataAnalysisAgentServiceImpl.java | 334 ++++++++
src/main/java/com/zy/ai/service/impl/DataAnalysisUploadServiceImpl.java | 86 ++
src/main/webapp/views/ai/data_analysis.html | 451 +++++++++++
src/main/java/com/zy/ai/service/AiTokenUsageService.java | 9
src/main/resources/sql/20260505_ai_data_analysis.sql | 146 +++
src/main/java/com/zy/ai/enums/AiPromptScene.java | 3
src/main/java/com/zy/ai/mapper/AiTokenUsageMapper.java | 17
src/main/java/com/zy/ai/service/AiDataAnalysisUploadLogService.java | 7
src/main/java/com/zy/ai/service/DataAnalysisUploadService.java | 40 +
src/main/java/com/zy/ai/mcp/service/impl/WcsDataFacadeImpl.java | 119 +++
src/main/java/com/zy/ai/service/DataAnalysisAgentService.java | 40 +
src/main/java/com/zy/ai/entity/AiDataAnalysisUploadLog.java | 45 +
src/main/java/com/zy/ai/entity/AiTokenUsage.java | 34
src/main/resources/mapper/AiTokenUsageMapper.xml | 24
src/main/java/com/zy/ai/service/impl/DataAnalysisFileStorageServiceImpl.java | 71 +
src/main/java/com/zy/ai/service/LlmChatService.java | 11
src/main/java/com/zy/ai/service/impl/LlmCallLogServiceImpl.java | 20
src/main/java/com/zy/asrs/mapper/WrkAnalysisMapper.java | 9
src/main/java/com/zy/ai/mcp/service/WcsDataFacade.java | 8
src/main/java/com/zy/system/controller/DashboardController.java | 52
src/main/java/com/zy/ai/mapper/AiDataAnalysisUploadLogMapper.java | 11
src/main/java/com/zy/ai/mcp/tool/DataAnalysisMcpTools.java | 49 +
src/main/java/com/zy/ai/gateway/AiGatewayService.java | 11
src/main/java/com/zy/ai/utils/AiPromptUtils.java | 52 +
src/main/java/com/zy/ai/timer/DataAnalysisScheduler.java | 31
src/main/java/com/zy/ai/mapper/AiDataAnalysisReportMapper.java | 11
src/main/webapp/views/dashboard/dashboard.html | 2
src/main/java/com/zy/ai/mcp/config/SpringAiMcpConfig.java | 6
src/main/java/com/zy/ai/service/impl/AiDataAnalysisReportServiceImpl.java | 12
src/main/java/com/zy/ai/service/DataAnalysisFileStorageService.java | 8
src/main/java/com/zy/ai/service/AiDataAnalysisReportService.java | 7
src/main/java/com/zy/ai/service/impl/DataAnalysisCoordinatorServiceImpl.java | 177 ++++
src/main/java/com/zy/ai/controller/DataAnalysisController.java | 142 +++
src/main/java/com/zy/ai/service/impl/AiTokenUsageServiceImpl.java | 36
src/main/java/com/zy/ai/service/impl/AiDataAnalysisUploadLogServiceImpl.java | 12
src/main/java/com/zy/ai/service/DataAnalysisCoordinatorService.java | 38 +
src/main/java/com/zy/core/enums/RedisKeyType.java | 2
src/main/resources/mapper/WrkAnalysisMapper.xml | 44 +
src/main/java/com/zy/ai/entity/AiDataAnalysisReport.java | 65 +
src/main/resources/application.yml | 2
40 files changed, 2,206 insertions(+), 38 deletions(-)
diff --git a/src/main/java/com/zy/ai/controller/DataAnalysisController.java b/src/main/java/com/zy/ai/controller/DataAnalysisController.java
new file mode 100644
index 0000000..2cb798e
--- /dev/null
+++ b/src/main/java/com/zy/ai/controller/DataAnalysisController.java
@@ -0,0 +1,142 @@
+package com.zy.ai.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.core.annotations.ManagerAuth;
+import com.core.common.R;
+import com.zy.ai.entity.AiDataAnalysisReport;
+import com.zy.ai.service.AiDataAnalysisReportService;
+import com.zy.ai.service.DataAnalysisCoordinatorService;
+import com.zy.common.web.BaseController;
+import com.zy.system.service.ConfigService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+@RestController
+@RequestMapping("/ai/dataAnalysis")
+@RequiredArgsConstructor
+public class DataAnalysisController extends BaseController {
+
+ private static final int DEFAULT_LIMIT = 20;
+ private static final int MAX_LIMIT = 100;
+
+ private final DataAnalysisCoordinatorService dataAnalysisCoordinatorService;
+ private final AiDataAnalysisReportService aiDataAnalysisReportService;
+ private final ConfigService configService;
+
+ @GetMapping("/enabled/auth")
+ @ManagerAuth(memo = "鏌ヨAI鏁版嵁鍒嗘瀽鍔熻兘寮�鍏�")
+ public R getEnabled() {
+ Map<String, Object> result = new LinkedHashMap<>();
+ result.put("enabled", dataAnalysisCoordinatorService.isEnabled());
+ result.put("scheduledPeriods", configService.getConfigValue("aiDataAnalysisScheduledPeriods", "YESTERDAY"));
+ result.put("cron", configService.getConfigValue("aiDataAnalysisCron", "0 0 1 * * ?"));
+ result.put("uploadEnabled", "1".equals(configService.getConfigValue("aiDataAnalysisUploadEnabled", "0")));
+ result.put("uploadUrl", configService.getConfigValue("aiDataAnalysisUploadUrl", ""));
+ return R.ok(result);
+ }
+
+ @PostMapping("/enabled/auth")
+ @ManagerAuth(memo = "淇敼AI鏁版嵁鍒嗘瀽鍔熻兘寮�鍏�")
+ public R setEnabled(@RequestParam("enabled") String enabled) {
+ boolean isEnabled = "1".equals(enabled) || "true".equalsIgnoreCase(enabled);
+ log.info("AI鏁版嵁鍒嗘瀽寮�鍏冲垏鎹�: enabled={}, 鍘熷��={}", enabled, dataAnalysisCoordinatorService.isEnabled());
+ dataAnalysisCoordinatorService.setEnabled(isEnabled);
+ boolean saved = dataAnalysisCoordinatorService.isEnabled();
+ log.info("AI鏁版嵁鍒嗘瀽寮�鍏充繚瀛樼粨鏋�: {}", saved);
+ Map<String, Object> result = new LinkedHashMap<>();
+ result.put("enabled", saved);
+ return R.ok(result);
+ }
+
+ @PostMapping("/trigger/auth")
+ @ManagerAuth(memo = "鎵嬪姩瑙﹀彂AI鏁版嵁鍒嗘瀽")
+ public R trigger(@RequestParam("periodType") String periodType) {
+ return R.ok(dataAnalysisCoordinatorService.runManualAnalysis(periodType));
+ }
+
+ @GetMapping("/reports/auth")
+ @ManagerAuth(memo = "鏌ョ湅AI鏁版嵁鍒嗘瀽鎶ュ憡鍒楄〃")
+ public R listReports(
+ @RequestParam(value = "periodType", required = false) String periodType,
+ @RequestParam(value = "limit", required = false) Integer limit) {
+ int safeLimit = normalizeLimit(limit);
+ QueryWrapper<AiDataAnalysisReport> wrapper = new QueryWrapper<>();
+ if (periodType != null && !periodType.trim().isEmpty()) {
+ wrapper.eq("period_type", periodType.trim());
+ }
+ wrapper.orderByDesc("create_time").last("limit " + safeLimit);
+ List<AiDataAnalysisReport> reports = aiDataAnalysisReportService.list(wrapper);
+ return R.ok(toReportSummaries(reports));
+ }
+
+ @GetMapping("/report/{id}/auth")
+ @ManagerAuth(memo = "鏌ョ湅AI鏁版嵁鍒嗘瀽鎶ュ憡璇︽儏")
+ public R getReport(@PathVariable("id") Long id) {
+ AiDataAnalysisReport report = aiDataAnalysisReportService.getById(id);
+ if (report == null) {
+ return R.error("鎶ュ憡涓嶅瓨鍦�");
+ }
+ return R.ok(toReportDetail(report));
+ }
+
+ private int normalizeLimit(Integer limit) {
+ if (limit == null || limit <= 0) {
+ return DEFAULT_LIMIT;
+ }
+ return Math.min(limit, MAX_LIMIT);
+ }
+
+ private List<Map<String, Object>> toReportSummaries(List<AiDataAnalysisReport> reports) {
+ List<Map<String, Object>> result = new ArrayList<>();
+ if (reports == null || reports.isEmpty()) {
+ return result;
+ }
+ for (AiDataAnalysisReport report : reports) {
+ result.add(toReportSummary(report));
+ }
+ return result;
+ }
+
+ private Map<String, Object> toReportSummary(AiDataAnalysisReport report) {
+ LinkedHashMap<String, Object> item = new LinkedHashMap<>();
+ item.put("id", report.getId());
+ item.put("periodType", report.getPeriodType());
+ item.put("triggerType", report.getTriggerType());
+ item.put("status", report.getStatus());
+ item.put("createTime", report.getCreateTime());
+ item.put("finishTime", report.getFinishTime());
+ item.put("llmCallCount", report.getLlmCallCount());
+ item.put("totalTokens", report.getTotalTokens());
+ item.put("uploadStatus", report.getUploadStatus());
+ return item;
+ }
+
+ private Map<String, Object> toReportDetail(AiDataAnalysisReport report) {
+ LinkedHashMap<String, Object> item = new LinkedHashMap<>();
+ item.put("id", report.getId());
+ item.put("periodType", report.getPeriodType());
+ item.put("periodStart", report.getPeriodStart());
+ item.put("periodEnd", report.getPeriodEnd());
+ item.put("triggerType", report.getTriggerType());
+ item.put("status", report.getStatus());
+ item.put("summary", report.getSummary());
+ item.put("structuredData", report.getStructuredData());
+ item.put("llmCallCount", report.getLlmCallCount());
+ item.put("promptTokens", report.getPromptTokens());
+ item.put("completionTokens", report.getCompletionTokens());
+ item.put("totalTokens", report.getTotalTokens());
+ item.put("errorMessage", report.getErrorMessage());
+ item.put("localFilePath", report.getLocalFilePath());
+ item.put("uploadStatus", report.getUploadStatus());
+ item.put("createTime", report.getCreateTime());
+ item.put("finishTime", report.getFinishTime());
+ return item;
+ }
+}
diff --git a/src/main/java/com/zy/ai/entity/AiDataAnalysisReport.java b/src/main/java/com/zy/ai/entity/AiDataAnalysisReport.java
new file mode 100644
index 0000000..c292b26
--- /dev/null
+++ b/src/main/java/com/zy/ai/entity/AiDataAnalysisReport.java
@@ -0,0 +1,65 @@
+package com.zy.ai.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("sys_ai_data_analysis_report")
+public class AiDataAnalysisReport implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ @TableId(value = "id", type = IdType.AUTO)
+ private Long id;
+
+ @TableField("period_type")
+ private String periodType;
+
+ @TableField("period_start")
+ private Date periodStart;
+
+ @TableField("period_end")
+ private Date periodEnd;
+
+ @TableField("trigger_type")
+ private String triggerType;
+
+ private String status;
+
+ private String summary;
+
+ @TableField("structured_data")
+ private String structuredData;
+
+ @TableField("llm_call_count")
+ private Integer llmCallCount;
+
+ @TableField("prompt_tokens")
+ private Integer promptTokens;
+
+ @TableField("completion_tokens")
+ private Integer completionTokens;
+
+ @TableField("total_tokens")
+ private Integer totalTokens;
+
+ @TableField("error_message")
+ private String errorMessage;
+
+ @TableField("local_file_path")
+ private String localFilePath;
+
+ @TableField("upload_status")
+ private String uploadStatus;
+
+ @TableField("create_time")
+ private Date createTime;
+
+ @TableField("finish_time")
+ private Date finishTime;
+}
diff --git a/src/main/java/com/zy/ai/entity/AiDataAnalysisUploadLog.java b/src/main/java/com/zy/ai/entity/AiDataAnalysisUploadLog.java
new file mode 100644
index 0000000..23bece6
--- /dev/null
+++ b/src/main/java/com/zy/ai/entity/AiDataAnalysisUploadLog.java
@@ -0,0 +1,45 @@
+package com.zy.ai.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("sys_ai_data_analysis_upload_log")
+public class AiDataAnalysisUploadLog implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ @TableId(value = "id", type = IdType.AUTO)
+ private Long id;
+
+ @TableField("report_id")
+ private Long reportId;
+
+ @TableField("upload_url")
+ private String uploadUrl;
+
+ @TableField("request_body")
+ private String requestBody;
+
+ @TableField("response_body")
+ private String responseBody;
+
+ @TableField("http_status")
+ private Integer httpStatus;
+
+ private String result;
+
+ @TableField("error_message")
+ private String errorMessage;
+
+ @TableField("retry_count")
+ private Integer retryCount;
+
+ @TableField("create_time")
+ private Date createTime;
+}
diff --git a/src/main/java/com/zy/ai/entity/AiTokenUsage.java b/src/main/java/com/zy/ai/entity/AiTokenUsage.java
new file mode 100644
index 0000000..e0b189a
--- /dev/null
+++ b/src/main/java/com/zy/ai/entity/AiTokenUsage.java
@@ -0,0 +1,34 @@
+package com.zy.ai.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("sys_ai_token_usage")
+public class AiTokenUsage implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ @TableId(value = "id", type = IdType.AUTO)
+ private Integer id;
+
+ @TableField("prompt_tokens")
+ private Long promptTokens;
+
+ @TableField("completion_tokens")
+ private Long completionTokens;
+
+ @TableField("total_tokens")
+ private Long totalTokens;
+
+ @TableField("llm_call_count")
+ private Long llmCallCount;
+
+ @TableField("update_time")
+ private Date updateTime;
+}
diff --git a/src/main/java/com/zy/ai/enums/AiPromptScene.java b/src/main/java/com/zy/ai/enums/AiPromptScene.java
index 3f6a9ea..f9435f5 100644
--- a/src/main/java/com/zy/ai/enums/AiPromptScene.java
+++ b/src/main/java/com/zy/ai/enums/AiPromptScene.java
@@ -4,7 +4,8 @@
DIAGNOSE_STREAM("wcs_diagnose_stream", "WCS宸℃璇婃柇"),
SENSOR_CHAT("wcs_sensor_chat", "WCS涓撳闂瓟"),
- AUTO_TUNE_DISPATCH("wcs_auto_tune_dispatch", "WCS鑷姩璋冨弬");
+ AUTO_TUNE_DISPATCH("wcs_auto_tune_dispatch", "WCS鑷姩璋冨弬"),
+ DATA_ANALYSIS("wcs_data_analysis", "WCS鏁版嵁鍒嗘瀽");
private final String code;
private final String label;
diff --git a/src/main/java/com/zy/ai/gateway/AiGatewayService.java b/src/main/java/com/zy/ai/gateway/AiGatewayService.java
index 71a256b..122f16b 100644
--- a/src/main/java/com/zy/ai/gateway/AiGatewayService.java
+++ b/src/main/java/com/zy/ai/gateway/AiGatewayService.java
@@ -8,6 +8,7 @@
import com.zy.ai.gateway.adapter.AiProviderAdapterRegistry;
import com.zy.ai.gateway.model.AiRequest;
import com.zy.ai.gateway.model.AiResponse;
+import com.zy.ai.service.AiTokenUsageService;
import com.zy.ai.service.LlmCallLogService;
import com.zy.ai.service.LlmRoutingService;
import lombok.RequiredArgsConstructor;
@@ -33,6 +34,7 @@
private final LlmRoutingService llmRoutingService;
private final AiProviderAdapterRegistry adapterRegistry;
private final LlmCallLogService llmCallLogService;
+ private final AiTokenUsageService aiTokenUsageService;
@Value("${llm.base-url:}")
private String fallbackBaseUrl;
@@ -201,6 +203,15 @@
item.setExtra(cut(extraPayload(route, response), 512));
item.setCreateTime(new Date());
llmCallLogService.saveIgnoreError(item);
+
+ // 绱姞 token 鍒扮嫭绔嬪瓨鍌�
+ if (success && response != null && response.getUsage() != null) {
+ aiTokenUsageService.incrementTokens(
+ response.getUsage().getInputTokens() == null ? 0 : response.getUsage().getInputTokens(),
+ response.getUsage().getOutputTokens() == null ? 0 : response.getUsage().getOutputTokens(),
+ response.getUsage().getTotalTokens() == null ? 0 : response.getUsage().getTotalTokens(),
+ 1);
+ }
}
private String responseText(AiResponse response) {
diff --git a/src/main/java/com/zy/ai/mapper/AiDataAnalysisReportMapper.java b/src/main/java/com/zy/ai/mapper/AiDataAnalysisReportMapper.java
new file mode 100644
index 0000000..4d470b8
--- /dev/null
+++ b/src/main/java/com/zy/ai/mapper/AiDataAnalysisReportMapper.java
@@ -0,0 +1,11 @@
+package com.zy.ai.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zy.ai.entity.AiDataAnalysisReport;
+import org.apache.ibatis.annotations.Mapper;
+import org.springframework.stereotype.Repository;
+
+@Mapper
+@Repository
+public interface AiDataAnalysisReportMapper extends BaseMapper<AiDataAnalysisReport> {
+}
diff --git a/src/main/java/com/zy/ai/mapper/AiDataAnalysisUploadLogMapper.java b/src/main/java/com/zy/ai/mapper/AiDataAnalysisUploadLogMapper.java
new file mode 100644
index 0000000..bc177f9
--- /dev/null
+++ b/src/main/java/com/zy/ai/mapper/AiDataAnalysisUploadLogMapper.java
@@ -0,0 +1,11 @@
+package com.zy.ai.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zy.ai.entity.AiDataAnalysisUploadLog;
+import org.apache.ibatis.annotations.Mapper;
+import org.springframework.stereotype.Repository;
+
+@Mapper
+@Repository
+public interface AiDataAnalysisUploadLogMapper extends BaseMapper<AiDataAnalysisUploadLog> {
+}
diff --git a/src/main/java/com/zy/ai/mapper/AiTokenUsageMapper.java b/src/main/java/com/zy/ai/mapper/AiTokenUsageMapper.java
new file mode 100644
index 0000000..94e15bf
--- /dev/null
+++ b/src/main/java/com/zy/ai/mapper/AiTokenUsageMapper.java
@@ -0,0 +1,17 @@
+package com.zy.ai.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zy.ai.entity.AiTokenUsage;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.springframework.stereotype.Repository;
+
+@Mapper
+@Repository
+public interface AiTokenUsageMapper extends BaseMapper<AiTokenUsage> {
+
+ int incrementTokens(@Param("promptTokens") long promptTokens,
+ @Param("completionTokens") long completionTokens,
+ @Param("totalTokens") long totalTokens,
+ @Param("callCount") long callCount);
+}
diff --git a/src/main/java/com/zy/ai/mcp/config/SpringAiMcpConfig.java b/src/main/java/com/zy/ai/mcp/config/SpringAiMcpConfig.java
index 886a952..10bc7e0 100644
--- a/src/main/java/com/zy/ai/mcp/config/SpringAiMcpConfig.java
+++ b/src/main/java/com/zy/ai/mcp/config/SpringAiMcpConfig.java
@@ -1,6 +1,7 @@
package com.zy.ai.mcp.config;
import com.zy.ai.mcp.tool.AutoTuneMcpTools;
+import com.zy.ai.mcp.tool.DataAnalysisMcpTools;
import com.zy.ai.mcp.tool.WcsMcpTools;
import org.springframework.ai.support.ToolCallbacks;
import org.springframework.ai.tool.StaticToolCallbackProvider;
@@ -13,7 +14,8 @@
@Bean("wcsMcpToolCallbackProvider")
public ToolCallbackProvider wcsMcpToolCallbackProvider(WcsMcpTools wcsMcpTools,
- AutoTuneMcpTools autoTuneMcpTools) {
- return new StaticToolCallbackProvider(ToolCallbacks.from(wcsMcpTools, autoTuneMcpTools));
+ AutoTuneMcpTools autoTuneMcpTools,
+ DataAnalysisMcpTools dataAnalysisMcpTools) {
+ return new StaticToolCallbackProvider(ToolCallbacks.from(wcsMcpTools, autoTuneMcpTools, dataAnalysisMcpTools));
}
}
diff --git a/src/main/java/com/zy/ai/mcp/service/WcsDataFacade.java b/src/main/java/com/zy/ai/mcp/service/WcsDataFacade.java
index 8edf358..a802008 100644
--- a/src/main/java/com/zy/ai/mcp/service/WcsDataFacade.java
+++ b/src/main/java/com/zy/ai/mcp/service/WcsDataFacade.java
@@ -19,4 +19,12 @@
Object getSystemConfig(JSONObject args);
Object getSystemPseudocode(JSONObject args);
+
+ Object getTaskThroughput(JSONObject args);
+
+ Object getDeviceFaultSummary(JSONObject args);
+
+ Object getDeviceUtilization(JSONObject args);
+
+ Object getErrorLogSummary(JSONObject args);
}
diff --git a/src/main/java/com/zy/ai/mcp/service/impl/WcsDataFacadeImpl.java b/src/main/java/com/zy/ai/mcp/service/impl/WcsDataFacadeImpl.java
index d7fb5e7..63a30d8 100644
--- a/src/main/java/com/zy/ai/mcp/service/impl/WcsDataFacadeImpl.java
+++ b/src/main/java/com/zy/ai/mcp/service/impl/WcsDataFacadeImpl.java
@@ -11,9 +11,18 @@
import com.zy.asrs.entity.BasDevp;
import com.zy.asrs.entity.BasRgv;
import com.zy.asrs.entity.WrkMast;
+import com.zy.asrs.entity.BasCrnp;
+import com.zy.asrs.entity.BasDevp;
+import com.zy.asrs.entity.BasRgv;
+import com.zy.asrs.entity.WrkMast;
+import com.zy.asrs.mapper.WrkAnalysisMapper;
+import com.zy.asrs.service.BasCrnpErrLogService;
import com.zy.asrs.service.BasCrnpService;
import com.zy.asrs.service.BasDevpService;
+import com.zy.asrs.service.BasDualCrnpErrLogService;
+import com.zy.asrs.service.BasRgvErrLogService;
import com.zy.asrs.service.BasRgvService;
+import com.zy.asrs.service.BasStationErrLogService;
import com.zy.asrs.service.WrkMastService;
import com.zy.core.cache.SlaveConnection;
import com.zy.core.enums.SlaveType;
@@ -48,6 +57,16 @@
private ConfigService configService;
@Autowired
private MainProcessPseudocodeService mainProcessPseudocodeService;
+ @Autowired
+ private WrkAnalysisMapper wrkAnalysisMapper;
+ @Autowired
+ private BasCrnpErrLogService basCrnpErrLogService;
+ @Autowired
+ private BasDualCrnpErrLogService basDualCrnpErrLogService;
+ @Autowired
+ private BasRgvErrLogService basRgvErrLogService;
+ @Autowired
+ private BasStationErrLogService basStationErrLogService;
@Override
public Object getCrnDeviceStatus(JSONObject args) {
@@ -284,6 +303,101 @@
return mainProcessPseudocodeService.queryMainProcessPseudocode(refresh);
}
+ @Override
+ public Object getTaskThroughput(JSONObject args) {
+ Date startTime = optDate(args, "startTime");
+ Date endTime = optDate(args, "endTime");
+ if (startTime == null || endTime == null) {
+ JSONObject err = new JSONObject();
+ err.put("error", "startTime and endTime are required");
+ return err;
+ }
+ Map<String, Object> result = wrkAnalysisMapper.aggregateThroughput(startTime, endTime);
+ JSONObject data = new JSONObject();
+ data.put("throughput", result);
+ data.put("startTime", startTime);
+ data.put("endTime", endTime);
+ return data;
+ }
+
+ @Override
+ public Object getDeviceFaultSummary(JSONObject args) {
+ Date startTime = optDate(args, "startTime");
+ Date endTime = optDate(args, "endTime");
+ if (startTime == null || endTime == null) {
+ JSONObject err = new JSONObject();
+ err.put("error", "startTime and endTime are required");
+ return err;
+ }
+ Map<String, Object> throughput = wrkAnalysisMapper.aggregateThroughput(startTime, endTime);
+ JSONObject data = new JSONObject();
+ data.put("totalTaskCount", throughput.get("taskCount"));
+ data.put("faultTaskCount", throughput.get("faultTaskCount"));
+ data.put("totalFaultCount", throughput.get("totalFaultCount"));
+ data.put("totalFaultDurationMs", throughput.get("totalFaultDurationMs"));
+ data.put("crnFaultCount", throughput.get("crnFaultCount"));
+ data.put("crnFaultDurationMs", throughput.get("crnFaultDurationMs"));
+ data.put("dualCrnFaultCount", throughput.get("dualCrnFaultCount"));
+ data.put("dualCrnFaultDurationMs", throughput.get("dualCrnFaultDurationMs"));
+ data.put("rgvFaultCount", throughput.get("rgvFaultCount"));
+ data.put("rgvFaultDurationMs", throughput.get("rgvFaultDurationMs"));
+ data.put("stationFaultCount", throughput.get("stationFaultCount"));
+ data.put("stationFaultDurationMs", throughput.get("stationFaultDurationMs"));
+ long taskCount = throughput.get("taskCount") != null ? ((Number) throughput.get("taskCount")).longValue() : 0;
+ long faultTask = throughput.get("faultTaskCount") != null ? ((Number) throughput.get("faultTaskCount")).longValue() : 0;
+ data.put("faultRate", taskCount > 0 ? Math.round(faultTask * 10000.0 / taskCount) / 100.0 : 0);
+ data.put("startTime", startTime);
+ data.put("endTime", endTime);
+ return data;
+ }
+
+ @Override
+ public Object getDeviceUtilization(JSONObject args) {
+ Date startTime = optDate(args, "startTime");
+ Date endTime = optDate(args, "endTime");
+ if (startTime == null || endTime == null) {
+ JSONObject err = new JSONObject();
+ err.put("error", "startTime and endTime are required");
+ return err;
+ }
+ List<Map<String, Object>> devices = wrkAnalysisMapper.groupByDevice(startTime, endTime);
+ JSONObject data = new JSONObject();
+ data.put("devices", devices);
+ data.put("startTime", startTime);
+ data.put("endTime", endTime);
+ return data;
+ }
+
+ @Override
+ public Object getErrorLogSummary(JSONObject args) {
+ Date startTime = optDate(args, "startTime");
+ Date endTime = optDate(args, "endTime");
+ if (startTime == null || endTime == null) {
+ JSONObject err = new JSONObject();
+ err.put("error", "startTime and endTime are required");
+ return err;
+ }
+ JSONObject data = new JSONObject();
+
+ long crnErrCount = basCrnpErrLogService.count(new QueryWrapper<com.zy.asrs.entity.BasCrnpErrLog>()
+ .ge("start_time", startTime).lt("start_time", endTime));
+ long dualCrnErrCount = basDualCrnpErrLogService.count(new QueryWrapper<com.zy.asrs.entity.BasDualCrnpErrLog>()
+ .ge("start_time", startTime).lt("start_time", endTime));
+ long rgvErrCount = basRgvErrLogService.count(new QueryWrapper<com.zy.asrs.entity.BasRgvErrLog>()
+ .ge("start_time", startTime).lt("start_time", endTime));
+ long stationErrCount = basStationErrLogService.count(new QueryWrapper<com.zy.asrs.entity.BasStationErrLog>()
+ .ge("start_time", startTime).lt("start_time", endTime));
+
+ data.put("crnErrorCount", crnErrCount);
+ data.put("dualCrnErrorCount", dualCrnErrCount);
+ data.put("rgvErrorCount", rgvErrCount);
+ data.put("stationErrorCount", stationErrCount);
+ data.put("totalErrorCount", crnErrCount + dualCrnErrCount + rgvErrCount + stationErrCount);
+ data.put("startTime", startTime);
+ data.put("endTime", endTime);
+ return data;
+ }
+
// --------- helpers ---------
private int optInt(JSONObject o, String key, int def) {
@@ -315,6 +429,11 @@
return value.trim();
}
+ private Date optDate(JSONObject o, String key) {
+ if (o == null || !o.containsKey(key)) return null;
+ return o.getDate(key);
+ }
+
private List<Long> optLongList(JSONObject o, String key) {
if (o == null || !o.containsKey(key)) return Collections.emptyList();
JSONArray arr = o.getJSONArray(key);
diff --git a/src/main/java/com/zy/ai/mcp/tool/DataAnalysisMcpTools.java b/src/main/java/com/zy/ai/mcp/tool/DataAnalysisMcpTools.java
new file mode 100644
index 0000000..9116ab1
--- /dev/null
+++ b/src/main/java/com/zy/ai/mcp/tool/DataAnalysisMcpTools.java
@@ -0,0 +1,49 @@
+package com.zy.ai.mcp.tool;
+
+import com.alibaba.fastjson.JSONObject;
+import com.zy.ai.mcp.service.WcsDataFacade;
+import lombok.RequiredArgsConstructor;
+import org.springframework.ai.tool.annotation.Tool;
+import org.springframework.ai.tool.annotation.ToolParam;
+import org.springframework.stereotype.Component;
+
+import java.util.Date;
+
+@Component
+@RequiredArgsConstructor
+public class DataAnalysisMcpTools {
+
+ private final WcsDataFacade wcsDataFacade;
+
+ @Tool(name = "analysis_query_task_throughput", description = "鏌ヨ鎸囧畾鏃堕棿鑼冨洿鍐呯殑浠诲姟鍚炲悙閲忕粺璁★細浠诲姟鎬婚噺銆佸叆搴�/鍑哄簱/绉诲簱鏁伴噺銆佸钩鍧囨椂闀裤�佹晠闅滄眹鎬�")
+ public Object queryTaskThroughput(
+ @ToolParam(description = "鍒嗘瀽寮�濮嬫椂闂�") Date startTime,
+ @ToolParam(description = "鍒嗘瀽缁撴潫鏃堕棿") Date endTime) {
+ return wcsDataFacade.getTaskThroughput(json().fluentPut("startTime", startTime).fluentPut("endTime", endTime));
+ }
+
+ @Tool(name = "analysis_query_device_fault_summary", description = "鏌ヨ鎸囧畾鏃堕棿鑼冨洿鍐呯殑璁惧鏁呴殰姹囨�伙細鎸夎澶囩被鍨嬶紙鍫嗗灈鏈�/鍙屽伐浣嶅爢鍨涙満/RGV/杈撻�佺嚎锛夌粺璁℃晠闅滄鏁板拰鏁呴殰鏃堕暱")
+ public Object queryDeviceFaultSummary(
+ @ToolParam(description = "鍒嗘瀽寮�濮嬫椂闂�") Date startTime,
+ @ToolParam(description = "鍒嗘瀽缁撴潫鏃堕棿") Date endTime) {
+ return wcsDataFacade.getDeviceFaultSummary(json().fluentPut("startTime", startTime).fluentPut("endTime", endTime));
+ }
+
+ @Tool(name = "analysis_query_device_utilization", description = "鏌ヨ鎸囧畾鏃堕棿鑼冨洿鍐呯殑璁惧鍒╃敤鐜囷細鎸夎澶囩紪鍙风粺璁′换鍔″垎閰嶉噺銆佸钩鍧囦换鍔℃椂闀裤�佹晠闅滄暟")
+ public Object queryDeviceUtilization(
+ @ToolParam(description = "鍒嗘瀽寮�濮嬫椂闂�") Date startTime,
+ @ToolParam(description = "鍒嗘瀽缁撴潫鏃堕棿") Date endTime) {
+ return wcsDataFacade.getDeviceUtilization(json().fluentPut("startTime", startTime).fluentPut("endTime", endTime));
+ }
+
+ @Tool(name = "analysis_query_error_logs", description = "鏌ヨ鎸囧畾鏃堕棿鑼冨洿鍐呯殑璁惧閿欒鏃ュ織缁熻锛氭寜璁惧绫诲瀷缁熻閿欒娆℃暟")
+ public Object queryErrorLogs(
+ @ToolParam(description = "鍒嗘瀽寮�濮嬫椂闂�") Date startTime,
+ @ToolParam(description = "鍒嗘瀽缁撴潫鏃堕棿") Date endTime) {
+ return wcsDataFacade.getErrorLogSummary(json().fluentPut("startTime", startTime).fluentPut("endTime", endTime));
+ }
+
+ private JSONObject json() {
+ return new JSONObject();
+ }
+}
diff --git a/src/main/java/com/zy/ai/service/AiDataAnalysisReportService.java b/src/main/java/com/zy/ai/service/AiDataAnalysisReportService.java
new file mode 100644
index 0000000..106c4e3
--- /dev/null
+++ b/src/main/java/com/zy/ai/service/AiDataAnalysisReportService.java
@@ -0,0 +1,7 @@
+package com.zy.ai.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.zy.ai.entity.AiDataAnalysisReport;
+
+public interface AiDataAnalysisReportService extends IService<AiDataAnalysisReport> {
+}
diff --git a/src/main/java/com/zy/ai/service/AiDataAnalysisUploadLogService.java b/src/main/java/com/zy/ai/service/AiDataAnalysisUploadLogService.java
new file mode 100644
index 0000000..af9a929
--- /dev/null
+++ b/src/main/java/com/zy/ai/service/AiDataAnalysisUploadLogService.java
@@ -0,0 +1,7 @@
+package com.zy.ai.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.zy.ai.entity.AiDataAnalysisUploadLog;
+
+public interface AiDataAnalysisUploadLogService extends IService<AiDataAnalysisUploadLog> {
+}
diff --git a/src/main/java/com/zy/ai/service/AiTokenUsageService.java b/src/main/java/com/zy/ai/service/AiTokenUsageService.java
new file mode 100644
index 0000000..47496ed
--- /dev/null
+++ b/src/main/java/com/zy/ai/service/AiTokenUsageService.java
@@ -0,0 +1,9 @@
+package com.zy.ai.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.zy.ai.entity.AiTokenUsage;
+
+public interface AiTokenUsageService extends IService<AiTokenUsage> {
+
+ void incrementTokens(long promptTokens, long completionTokens, long totalTokens, long callCount);
+}
diff --git a/src/main/java/com/zy/ai/service/DataAnalysisAgentService.java b/src/main/java/com/zy/ai/service/DataAnalysisAgentService.java
new file mode 100644
index 0000000..3cdac2e
--- /dev/null
+++ b/src/main/java/com/zy/ai/service/DataAnalysisAgentService.java
@@ -0,0 +1,40 @@
+package com.zy.ai.service;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+public interface DataAnalysisAgentService {
+
+ DataAnalysisAgentResult runAnalysis(String periodType);
+
+ @Data
+ class DataAnalysisAgentResult implements Serializable {
+ private static final long serialVersionUID = 1L;
+ private Boolean success;
+ private String periodType;
+ private String triggerType;
+ private String summary;
+ private String structuredData;
+ private Integer toolCallCount;
+ private Integer llmCallCount;
+ private Long promptTokens;
+ private Long completionTokens;
+ private Long totalTokens;
+ private Boolean maxRoundsReached;
+ private List<McpCallResult> mcpCalls;
+ }
+
+ @Data
+ class McpCallResult implements Serializable {
+ private static final long serialVersionUID = 1L;
+ private Integer callSeq;
+ private String toolName;
+ private Long durationMs;
+ private String status;
+ private String requestJson;
+ private String responseJson;
+ private String errorMessage;
+ }
+}
diff --git a/src/main/java/com/zy/ai/service/DataAnalysisCoordinatorService.java b/src/main/java/com/zy/ai/service/DataAnalysisCoordinatorService.java
new file mode 100644
index 0000000..06b72bd
--- /dev/null
+++ b/src/main/java/com/zy/ai/service/DataAnalysisCoordinatorService.java
@@ -0,0 +1,38 @@
+package com.zy.ai.service;
+
+import lombok.Data;
+
+public interface DataAnalysisCoordinatorService {
+
+ DataAnalysisCoordinatorResult runAnalysisIfEligible();
+
+ DataAnalysisCoordinatorResult runManualAnalysis(String periodType);
+
+ boolean isEnabled();
+
+ void setEnabled(boolean enabled);
+
+ @Data
+ class DataAnalysisCoordinatorResult {
+ private Boolean skipped;
+ private String reason;
+ private Boolean triggered;
+ private DataAnalysisAgentService.DataAnalysisAgentResult agentResult;
+
+ public static DataAnalysisCoordinatorResult skipped(String reason) {
+ DataAnalysisCoordinatorResult r = new DataAnalysisCoordinatorResult();
+ r.setSkipped(true);
+ r.setReason(reason);
+ r.setTriggered(false);
+ return r;
+ }
+
+ public static DataAnalysisCoordinatorResult triggered(DataAnalysisAgentService.DataAnalysisAgentResult agentResult) {
+ DataAnalysisCoordinatorResult r = new DataAnalysisCoordinatorResult();
+ r.setSkipped(false);
+ r.setTriggered(true);
+ r.setAgentResult(agentResult);
+ return r;
+ }
+ }
+}
diff --git a/src/main/java/com/zy/ai/service/DataAnalysisFileStorageService.java b/src/main/java/com/zy/ai/service/DataAnalysisFileStorageService.java
new file mode 100644
index 0000000..7f6e66f
--- /dev/null
+++ b/src/main/java/com/zy/ai/service/DataAnalysisFileStorageService.java
@@ -0,0 +1,8 @@
+package com.zy.ai.service;
+
+import com.zy.ai.entity.AiDataAnalysisReport;
+
+public interface DataAnalysisFileStorageService {
+
+ String saveReport(AiDataAnalysisReport report);
+}
diff --git a/src/main/java/com/zy/ai/service/DataAnalysisUploadService.java b/src/main/java/com/zy/ai/service/DataAnalysisUploadService.java
new file mode 100644
index 0000000..d25de46
--- /dev/null
+++ b/src/main/java/com/zy/ai/service/DataAnalysisUploadService.java
@@ -0,0 +1,40 @@
+package com.zy.ai.service;
+
+import com.zy.ai.entity.AiDataAnalysisReport;
+import lombok.Data;
+
+public interface DataAnalysisUploadService {
+
+ UploadResult upload(AiDataAnalysisReport report);
+
+ @Data
+ class UploadResult {
+ private boolean success;
+ private boolean skipped;
+ private Integer httpStatus;
+ private String responseBody;
+ private String errorMessage;
+
+ public static UploadResult skipped() {
+ UploadResult r = new UploadResult();
+ r.setSkipped(true);
+ return r;
+ }
+
+ public static UploadResult success(Integer httpStatus, String responseBody) {
+ UploadResult r = new UploadResult();
+ r.setSuccess(true);
+ r.setHttpStatus(httpStatus);
+ r.setResponseBody(responseBody);
+ return r;
+ }
+
+ public static UploadResult failed(Integer httpStatus, String errorMessage) {
+ UploadResult r = new UploadResult();
+ r.setSuccess(false);
+ r.setHttpStatus(httpStatus);
+ r.setErrorMessage(errorMessage);
+ return r;
+ }
+ }
+}
diff --git a/src/main/java/com/zy/ai/service/LlmChatService.java b/src/main/java/com/zy/ai/service/LlmChatService.java
index 3b7c4ec..e89b1fb 100644
--- a/src/main/java/com/zy/ai/service/LlmChatService.java
+++ b/src/main/java/com/zy/ai/service/LlmChatService.java
@@ -41,6 +41,7 @@
private final LlmSpringAiClientService llmSpringAiClientService;
private final AiGatewayService aiGatewayService;
private final OpenAiChatCompletionsMapper openAiChatCompletionsMapper;
+ private final AiTokenUsageService aiTokenUsageService;
@Value("${llm.base-url:}")
private String fallbackBaseUrl;
@@ -469,6 +470,16 @@
item.setExtra(cut(buildExtraPayload(responseObj == null ? null : responseObj.getUsage(), extra), 512));
item.setCreateTime(new Date());
llmCallLogService.saveIgnoreError(item);
+
+ // 绱姞 token 鍒扮嫭绔嬪瓨鍌�
+ if (success && responseObj != null && responseObj.getUsage() != null) {
+ ChatCompletionResponse.Usage usage = responseObj.getUsage();
+ aiTokenUsageService.incrementTokens(
+ usage.getPromptTokens() == null ? 0 : usage.getPromptTokens(),
+ usage.getCompletionTokens() == null ? 0 : usage.getCompletionTokens(),
+ usage.getTotalTokens() == null ? 0 : usage.getTotalTokens(),
+ 1);
+ }
}
private ChatCompletionResponse usageResponse(ChatCompletionResponse.Usage usage) {
diff --git a/src/main/java/com/zy/ai/service/impl/AiDataAnalysisReportServiceImpl.java b/src/main/java/com/zy/ai/service/impl/AiDataAnalysisReportServiceImpl.java
new file mode 100644
index 0000000..9ae72dc
--- /dev/null
+++ b/src/main/java/com/zy/ai/service/impl/AiDataAnalysisReportServiceImpl.java
@@ -0,0 +1,12 @@
+package com.zy.ai.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.zy.ai.entity.AiDataAnalysisReport;
+import com.zy.ai.mapper.AiDataAnalysisReportMapper;
+import com.zy.ai.service.AiDataAnalysisReportService;
+import org.springframework.stereotype.Service;
+
+@Service("aiDataAnalysisReportService")
+public class AiDataAnalysisReportServiceImpl extends ServiceImpl<AiDataAnalysisReportMapper, AiDataAnalysisReport>
+ implements AiDataAnalysisReportService {
+}
diff --git a/src/main/java/com/zy/ai/service/impl/AiDataAnalysisUploadLogServiceImpl.java b/src/main/java/com/zy/ai/service/impl/AiDataAnalysisUploadLogServiceImpl.java
new file mode 100644
index 0000000..c12b58c
--- /dev/null
+++ b/src/main/java/com/zy/ai/service/impl/AiDataAnalysisUploadLogServiceImpl.java
@@ -0,0 +1,12 @@
+package com.zy.ai.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.zy.ai.entity.AiDataAnalysisUploadLog;
+import com.zy.ai.mapper.AiDataAnalysisUploadLogMapper;
+import com.zy.ai.service.AiDataAnalysisUploadLogService;
+import org.springframework.stereotype.Service;
+
+@Service("aiDataAnalysisUploadLogService")
+public class AiDataAnalysisUploadLogServiceImpl extends ServiceImpl<AiDataAnalysisUploadLogMapper, AiDataAnalysisUploadLog>
+ implements AiDataAnalysisUploadLogService {
+}
diff --git a/src/main/java/com/zy/ai/service/impl/AiTokenUsageServiceImpl.java b/src/main/java/com/zy/ai/service/impl/AiTokenUsageServiceImpl.java
new file mode 100644
index 0000000..ed4c1d1
--- /dev/null
+++ b/src/main/java/com/zy/ai/service/impl/AiTokenUsageServiceImpl.java
@@ -0,0 +1,36 @@
+package com.zy.ai.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.zy.ai.entity.AiTokenUsage;
+import com.zy.ai.mapper.AiTokenUsageMapper;
+import com.zy.ai.service.AiTokenUsageService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+@Slf4j
+@Service("aiTokenUsageService")
+public class AiTokenUsageServiceImpl extends ServiceImpl<AiTokenUsageMapper, AiTokenUsage>
+ implements AiTokenUsageService {
+
+ @Override
+ public void incrementTokens(long promptTokens, long completionTokens, long totalTokens, long callCount) {
+ if (promptTokens <= 0 && completionTokens <= 0 && totalTokens <= 0 && callCount <= 0) {
+ return;
+ }
+ try {
+ int rows = baseMapper.incrementTokens(promptTokens, completionTokens, totalTokens, callCount);
+ if (rows == 0) {
+ // Row doesn't exist, create it
+ AiTokenUsage usage = new AiTokenUsage();
+ usage.setId(1);
+ usage.setPromptTokens(promptTokens);
+ usage.setCompletionTokens(completionTokens);
+ usage.setTotalTokens(totalTokens);
+ usage.setLlmCallCount(callCount);
+ save(usage);
+ }
+ } catch (Exception e) {
+ log.warn("Failed to increment AI token usage: {}", e.getMessage());
+ }
+ }
+}
diff --git a/src/main/java/com/zy/ai/service/impl/DataAnalysisAgentServiceImpl.java b/src/main/java/com/zy/ai/service/impl/DataAnalysisAgentServiceImpl.java
new file mode 100644
index 0000000..9ed5359
--- /dev/null
+++ b/src/main/java/com/zy/ai/service/impl/DataAnalysisAgentServiceImpl.java
@@ -0,0 +1,334 @@
+package com.zy.ai.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.zy.ai.entity.AiPromptTemplate;
+import com.zy.ai.entity.ChatCompletionRequest;
+import com.zy.ai.entity.ChatCompletionResponse;
+import com.zy.ai.enums.AiPromptScene;
+import com.zy.ai.mcp.service.SpringAiMcpToolManager;
+import com.zy.ai.service.AiPromptTemplateService;
+import com.zy.ai.service.DataAnalysisAgentService;
+import com.zy.ai.service.LlmChatService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.time.DayOfWeek;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.*;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class DataAnalysisAgentServiceImpl implements DataAnalysisAgentService {
+
+ private static final int MAX_TOOL_ROUNDS = 10;
+ private static final double TEMPERATURE = 0.3D;
+ private static final int MAX_TOKENS = 4096;
+ private static final String MCP_STATUS_SUCCESS = "success";
+ private static final String MCP_STATUS_FAILED = "failed";
+
+ private static final String TOOL_THROUGHPUT = "wcs_local_analysis_query_task_throughput";
+ private static final String TOOL_FAULT_SUMMARY = "wcs_local_analysis_query_device_fault_summary";
+ private static final String TOOL_UTILIZATION = "wcs_local_analysis_query_device_utilization";
+ private static final String TOOL_ERROR_LOGS = "wcs_local_analysis_query_error_logs";
+
+ private static final Set<String> ALLOWED_TOOL_NAMES = Set.of(
+ TOOL_THROUGHPUT,
+ TOOL_FAULT_SUMMARY,
+ TOOL_UTILIZATION,
+ TOOL_ERROR_LOGS
+ );
+
+ private final LlmChatService llmChatService;
+ private final SpringAiMcpToolManager mcpToolManager;
+ private final AiPromptTemplateService aiPromptTemplateService;
+
+ @Override
+ public DataAnalysisAgentResult runAnalysis(String periodType) {
+ String normalizedPeriod = normalizePeriodType(periodType);
+ DateRange dateRange = resolveDateRange(normalizedPeriod);
+ UsageCounter usageCounter = new UsageCounter();
+ List<McpCallResult> mcpCalls = new ArrayList<>();
+ boolean maxRoundsReached = false;
+ StringBuilder summaryBuffer = new StringBuilder();
+ int toolCallCount = 0;
+
+ try {
+ List<Object> tools = filterAllowedTools(mcpToolManager.buildOpenAiTools());
+ if (tools == null || tools.isEmpty()) {
+ throw new IllegalStateException("No data analysis MCP tools registered");
+ }
+
+ AiPromptTemplate promptTemplate = aiPromptTemplateService.resolvePublished(AiPromptScene.DATA_ANALYSIS.getCode());
+ List<ChatCompletionRequest.Message> messages = buildMessages(promptTemplate, normalizedPeriod, dateRange);
+
+ for (int round = 0; round < MAX_TOOL_ROUNDS; round++) {
+ ChatCompletionResponse response = llmChatService.chatCompletionOrThrow(messages, TEMPERATURE, MAX_TOKENS, tools);
+ ChatCompletionRequest.Message assistantMessage = extractAssistantMessage(response);
+ usageCounter.add(response.getUsage());
+ messages.add(assistantMessage);
+ appendSummary(summaryBuffer, assistantMessage.getContent());
+
+ List<ChatCompletionRequest.ToolCall> toolCalls = assistantMessage.getTool_calls();
+ if (toolCalls == null || toolCalls.isEmpty()) {
+ return buildResult(true, normalizedPeriod, summaryBuffer, toolCallCount, usageCounter, false, mcpCalls);
+ }
+
+ for (ChatCompletionRequest.ToolCall toolCall : toolCalls) {
+ McpCallResult mcpCall = callAnalysisTool(toolCall, mcpCalls);
+ toolCallCount++;
+ Object toolOutput = parseToolOutput(mcpCall);
+ messages.add(buildToolMessage(toolCall, toolOutput));
+ }
+ }
+ maxRoundsReached = true;
+ return buildResult(false, normalizedPeriod, summaryBuffer, toolCallCount, usageCounter, maxRoundsReached, mcpCalls);
+ } catch (Exception exception) {
+ log.error("Data analysis agent stopped with error", exception);
+ appendSummary(summaryBuffer, "鏁版嵁鍒嗘瀽 Agent 鎵ц寮傚父: " + exception.getMessage());
+ return buildResult(false, normalizedPeriod, summaryBuffer, toolCallCount, usageCounter, maxRoundsReached, mcpCalls);
+ }
+ }
+
+ private McpCallResult callAnalysisTool(ChatCompletionRequest.ToolCall toolCall, List<McpCallResult> mcpCalls) {
+ String toolName = resolveToolName(toolCall);
+ if (!ALLOWED_TOOL_NAMES.contains(toolName)) {
+ throw new IllegalArgumentException("Disallowed data analysis MCP tool: " + toolName);
+ }
+ JSONObject arguments = parseArguments(toolCall);
+ long startTimeMillis = System.currentTimeMillis();
+ McpCallResult mcpCall = new McpCallResult();
+ mcpCall.setCallSeq(mcpCalls.size() + 1);
+ mcpCall.setToolName(toolName);
+ mcpCall.setRequestJson(JSON.toJSONString(arguments == null ? new JSONObject() : arguments));
+ try {
+ Object output = mcpToolManager.callTool(toolName, arguments);
+ mcpCall.setDurationMs(Math.max(0L, System.currentTimeMillis() - startTimeMillis));
+ mcpCall.setStatus(MCP_STATUS_SUCCESS);
+ mcpCall.setResponseJson(JSON.toJSONString(output));
+ mcpCalls.add(mcpCall);
+ return mcpCall;
+ } catch (Exception exception) {
+ mcpCall.setDurationMs(Math.max(0L, System.currentTimeMillis() - startTimeMillis));
+ mcpCall.setStatus(MCP_STATUS_FAILED);
+ mcpCall.setErrorMessage(exception.getMessage());
+ mcpCalls.add(mcpCall);
+ throw new IllegalStateException("Data analysis MCP tool failed: " + toolName + ", " + exception.getMessage(), exception);
+ }
+ }
+
+ private Object parseToolOutput(McpCallResult mcpCall) {
+ if (MCP_STATUS_FAILED.equals(mcpCall.getStatus())) {
+ JSONObject err = new JSONObject();
+ err.put("error", mcpCall.getErrorMessage());
+ return err;
+ }
+ if (mcpCall.getResponseJson() == null || mcpCall.getResponseJson().isEmpty()) {
+ return new JSONObject();
+ }
+ try {
+ return JSON.parse(mcpCall.getResponseJson());
+ } catch (Exception e) {
+ return mcpCall.getResponseJson();
+ }
+ }
+
+ private List<ChatCompletionRequest.Message> buildMessages(AiPromptTemplate promptTemplate,
+ String periodType,
+ DateRange dateRange) {
+ List<ChatCompletionRequest.Message> messages = new ArrayList<>();
+
+ ChatCompletionRequest.Message systemMessage = new ChatCompletionRequest.Message();
+ systemMessage.setRole("system");
+ systemMessage.setContent(promptTemplate == null ? "" : promptTemplate.getContent());
+ messages.add(systemMessage);
+
+ ChatCompletionRequest.Message userMessage = new ChatCompletionRequest.Message();
+ userMessage.setRole("user");
+ userMessage.setContent("璇峰垎鏋�" + periodLabel(periodType) + "鐨刉CS杩愯惀鏁版嵁銆�"
+ + "鏃堕棿鑼冨洿锛歴tartTime=" + dateRange.start + ", endTime=" + dateRange.end
+ + "銆傝渚濇璋冪敤鎵�鏈夊垎鏋愬伐鍏疯幏鍙栨暟鎹紝鐒跺悗鐢熸垚瀹屾暣鐨勫垎鏋愭姤鍛娿��");
+ messages.add(userMessage);
+ return messages;
+ }
+
+ private String periodLabel(String periodType) {
+ switch (periodType) {
+ case "TODAY": return "浠婂ぉ";
+ case "YESTERDAY": return "鏄ㄥぉ";
+ case "THIS_WEEK": return "鏈懆";
+ case "THIS_MONTH": return "鏈湀";
+ default: return periodType;
+ }
+ }
+
+ private DateRange resolveDateRange(String periodType) {
+ LocalDate today = LocalDate.now();
+ switch (periodType) {
+ case "TODAY":
+ return new DateRange(today.atStartOfDay(), today.plusDays(1).atStartOfDay());
+ case "YESTERDAY":
+ return new DateRange(today.minusDays(1).atStartOfDay(), today.atStartOfDay());
+ case "THIS_WEEK":
+ LocalDate weekStart = today.with(DayOfWeek.MONDAY);
+ return new DateRange(weekStart.atStartOfDay(), today.plusDays(1).atStartOfDay());
+ case "THIS_MONTH":
+ LocalDate monthStart = today.withDayOfMonth(1);
+ return new DateRange(monthStart.atStartOfDay(), today.plusDays(1).atStartOfDay());
+ default:
+ throw new IllegalArgumentException("Unknown period: " + periodType);
+ }
+ }
+
+ private ChatCompletionRequest.Message extractAssistantMessage(ChatCompletionResponse response) {
+ if (response == null || response.getChoices() == null || response.getChoices().isEmpty()) {
+ throw new IllegalStateException("LLM returned empty response");
+ }
+ ChatCompletionRequest.Message message = response.getChoices().get(0).getMessage();
+ if (message == null) {
+ throw new IllegalStateException("LLM returned empty message");
+ }
+ return message;
+ }
+
+ private ChatCompletionRequest.Message buildToolMessage(ChatCompletionRequest.ToolCall toolCall, Object toolOutput) {
+ ChatCompletionRequest.Message toolMessage = new ChatCompletionRequest.Message();
+ toolMessage.setRole("tool");
+ toolMessage.setTool_call_id(toolCall == null ? null : toolCall.getId());
+ toolMessage.setContent(JSON.toJSONString(toolOutput));
+ return toolMessage;
+ }
+
+ private String resolveToolName(ChatCompletionRequest.ToolCall toolCall) {
+ if (toolCall == null || toolCall.getFunction() == null || toolCall.getFunction().getName() == null
+ || toolCall.getFunction().getName().trim().isEmpty()) {
+ throw new IllegalArgumentException("missing tool name");
+ }
+ return toolCall.getFunction().getName();
+ }
+
+ private JSONObject parseArguments(ChatCompletionRequest.ToolCall toolCall) {
+ String rawArguments = toolCall == null || toolCall.getFunction() == null
+ ? null
+ : toolCall.getFunction().getArguments();
+ if (rawArguments == null || rawArguments.trim().isEmpty()) {
+ return new JSONObject();
+ }
+ try {
+ return JSON.parseObject(rawArguments);
+ } catch (Exception exception) {
+ JSONObject arguments = new JSONObject();
+ arguments.put("_raw", rawArguments);
+ return arguments;
+ }
+ }
+
+ private List<Object> filterAllowedTools(List<Object> tools) {
+ List<Object> allowedTools = new ArrayList<>();
+ if (tools == null || tools.isEmpty()) {
+ return allowedTools;
+ }
+ for (Object tool : tools) {
+ String toolName = resolveOpenAiToolName(tool);
+ if (ALLOWED_TOOL_NAMES.contains(toolName)) {
+ allowedTools.add(tool);
+ }
+ }
+ return allowedTools;
+ }
+
+ private String resolveOpenAiToolName(Object tool) {
+ if (!(tool instanceof Map<?, ?> toolMap)) {
+ return null;
+ }
+ Object function = toolMap.get("function");
+ if (!(function instanceof Map<?, ?> functionMap)) {
+ return null;
+ }
+ Object name = functionMap.get("name");
+ return name == null ? null : String.valueOf(name);
+ }
+
+ private DataAnalysisAgentResult buildResult(boolean success,
+ String periodType,
+ StringBuilder summaryBuffer,
+ int toolCallCount,
+ UsageCounter usageCounter,
+ boolean maxRoundsReached,
+ List<McpCallResult> mcpCalls) {
+ DataAnalysisAgentResult result = new DataAnalysisAgentResult();
+ result.setSuccess(success);
+ result.setPeriodType(periodType);
+ result.setTriggerType("agent");
+ result.setToolCallCount(toolCallCount);
+ result.setLlmCallCount(usageCounter.getLlmCallCount());
+ result.setPromptTokens(usageCounter.getPromptTokens());
+ result.setCompletionTokens(usageCounter.getCompletionTokens());
+ result.setTotalTokens(usageCounter.getTotalTokens());
+ result.setMaxRoundsReached(maxRoundsReached);
+ result.setMcpCalls(mcpCalls != null ? new ArrayList<>(mcpCalls) : new ArrayList<>());
+
+ String summary = summaryBuffer == null ? "" : summaryBuffer.toString().trim();
+ if (toolCallCount <= 0) {
+ summary = "鏁版嵁鍒嗘瀽 Agent 鏈皟鐢ㄤ换浣曞垎鏋愬伐鍏凤紝鏈敓鎴愭姤鍛娿��" + (summary.isEmpty() ? "" : "\n" + summary);
+ }
+ if (maxRoundsReached) {
+ summary = summary + "\n鏁版嵁鍒嗘瀽 Agent 杈惧埌鏈�澶у伐鍏疯皟鐢ㄨ疆娆★紝宸插仠姝€��";
+ }
+ result.setSummary(summary);
+ return result;
+ }
+
+ private void appendSummary(StringBuilder summaryBuffer, String content) {
+ if (summaryBuffer == null || content == null || content.trim().isEmpty()) {
+ return;
+ }
+ if (summaryBuffer.length() > 0) {
+ summaryBuffer.append('\n');
+ }
+ summaryBuffer.append(content.trim());
+ }
+
+ private String normalizePeriodType(String periodType) {
+ if (periodType == null || periodType.trim().isEmpty()) {
+ return "YESTERDAY";
+ }
+ return periodType.trim().toUpperCase();
+ }
+
+ private static class DateRange {
+ final LocalDateTime start;
+ final LocalDateTime end;
+
+ DateRange(LocalDateTime start, LocalDateTime end) {
+ this.start = start;
+ this.end = end;
+ }
+ }
+
+ private static class UsageCounter {
+ private long promptTokens;
+ private long completionTokens;
+ private long totalTokens;
+ private int llmCallCount;
+
+ void add(ChatCompletionResponse.Usage usage) {
+ llmCallCount++;
+ if (usage == null) {
+ return;
+ }
+ promptTokens += usage.getPromptTokens() == null ? 0L : usage.getPromptTokens();
+ completionTokens += usage.getCompletionTokens() == null ? 0L : usage.getCompletionTokens();
+ totalTokens += usage.getTotalTokens() == null ? 0L : usage.getTotalTokens();
+ }
+
+ long getPromptTokens() { return promptTokens; }
+ long getCompletionTokens() { return completionTokens; }
+ long getTotalTokens() { return totalTokens; }
+ int getLlmCallCount() { return llmCallCount; }
+ }
+}
diff --git a/src/main/java/com/zy/ai/service/impl/DataAnalysisCoordinatorServiceImpl.java b/src/main/java/com/zy/ai/service/impl/DataAnalysisCoordinatorServiceImpl.java
new file mode 100644
index 0000000..3eedff9
--- /dev/null
+++ b/src/main/java/com/zy/ai/service/impl/DataAnalysisCoordinatorServiceImpl.java
@@ -0,0 +1,177 @@
+package com.zy.ai.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.zy.ai.entity.AiDataAnalysisReport;
+import com.zy.ai.service.*;
+import com.zy.common.utils.RedisUtil;
+import com.zy.core.enums.RedisKeyType;
+import com.zy.system.entity.OperateLog;
+import com.zy.system.service.ConfigService;
+import com.zy.system.service.OperateLogService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.UUID;
+
+@Slf4j
+@Service("dataAnalysisCoordinatorService")
+@RequiredArgsConstructor
+public class DataAnalysisCoordinatorServiceImpl implements DataAnalysisCoordinatorService {
+
+ private static final String CONFIG_ENABLED = "aiDataAnalysisEnabled";
+ private static final String CONFIG_PERIODS = "aiDataAnalysisScheduledPeriods";
+ private static final int RUNNING_LOCK_SECONDS = 30 * 60;
+ private static final long SYSTEM_USER_ID = 9527L;
+
+ private final ConfigService configService;
+ private final DataAnalysisAgentService dataAnalysisAgentService;
+ private final AiDataAnalysisReportService aiDataAnalysisReportService;
+ private final DataAnalysisFileStorageService dataAnalysisFileStorageService;
+ private final DataAnalysisUploadService dataAnalysisUploadService;
+ private final RedisUtil redisUtil;
+ private final OperateLogService operateLogService;
+
+ @Override
+ public boolean isEnabled() {
+ String value = configService.getConfigValue(CONFIG_ENABLED, "0");
+ return "1".equals(value.trim());
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ configService.saveConfigValue(CONFIG_ENABLED, enabled ? "1" : "0");
+ configService.refreshSystemConfigCache();
+ }
+
+ @Override
+ public DataAnalysisCoordinatorResult runAnalysisIfEligible() {
+ if (!isEnabled()) {
+ return DataAnalysisCoordinatorResult.skipped("disabled");
+ }
+
+ String periods = configService.getConfigValue(CONFIG_PERIODS, "YESTERDAY");
+ List<String> periodList = Arrays.stream(periods.split(","))
+ .map(String::trim)
+ .filter(s -> !s.isEmpty())
+ .toList();
+
+ if (periodList.isEmpty()) {
+ return DataAnalysisCoordinatorResult.skipped("no_configured_periods");
+ }
+
+ // Run the first configured period
+ String periodType = periodList.get(0);
+ return runWithLock("auto", periodType);
+ }
+
+ @Override
+ public DataAnalysisCoordinatorResult runManualAnalysis(String periodType) {
+ if (!isEnabled()) {
+ return DataAnalysisCoordinatorResult.skipped("disabled");
+ }
+ return runWithLock("manual", periodType);
+ }
+
+ private DataAnalysisCoordinatorResult runWithLock(String triggerType, String periodType) {
+ String lockKey = RedisKeyType.AI_DATA_ANALYSIS_RUNNING_LOCK.key;
+ String lockToken = UUID.randomUUID().toString();
+ if (!redisUtil.trySetStringIfAbsent(lockKey, lockToken, RUNNING_LOCK_SECONDS)) {
+ return DataAnalysisCoordinatorResult.skipped("running_lock_not_acquired");
+ }
+
+ Date startTime = new Date();
+ DataAnalysisAgentService.DataAnalysisAgentResult agentResult = null;
+ try {
+ agentResult = dataAnalysisAgentService.runAnalysis(periodType);
+ saveReport(triggerType, periodType, startTime, agentResult);
+ safeWriteOperateLog(triggerType, periodType, agentResult);
+ return DataAnalysisCoordinatorResult.triggered(agentResult);
+ } catch (Exception exception) {
+ log.error("Data analysis coordinator failed to run agent", exception);
+ agentResult = failedAgentResult(periodType, exception);
+ saveReport(triggerType, periodType, startTime, agentResult);
+ safeWriteOperateLog(triggerType, periodType, agentResult);
+ return DataAnalysisCoordinatorResult.triggered(agentResult);
+ } finally {
+ redisUtil.compareAndDelete(lockKey, lockToken);
+ }
+ }
+
+ private void saveReport(String triggerType, String periodType, Date startTime,
+ DataAnalysisAgentService.DataAnalysisAgentResult agentResult) {
+ try {
+ AiDataAnalysisReport report = new AiDataAnalysisReport();
+ report.setPeriodType(periodType);
+ report.setPeriodStart(resolvePeriodStart(periodType));
+ report.setPeriodEnd(resolvePeriodEnd(periodType));
+ report.setTriggerType(triggerType);
+ report.setStatus(Boolean.TRUE.equals(agentResult.getSuccess()) ? "success" : "failed");
+ report.setSummary(agentResult.getSummary());
+ report.setStructuredData(agentResult.getMcpCalls() != null ? JSON.toJSONString(agentResult.getMcpCalls()) : null);
+ report.setLlmCallCount(agentResult.getLlmCallCount());
+ report.setPromptTokens(agentResult.getPromptTokens() != null ? agentResult.getPromptTokens().intValue() : 0);
+ report.setCompletionTokens(agentResult.getCompletionTokens() != null ? agentResult.getCompletionTokens().intValue() : 0);
+ report.setTotalTokens(agentResult.getTotalTokens() != null ? agentResult.getTotalTokens().intValue() : 0);
+ report.setCreateTime(startTime);
+ report.setFinishTime(new Date());
+
+ // Save to local file
+ String filePath = dataAnalysisFileStorageService.saveReport(report);
+ report.setLocalFilePath(filePath);
+
+ // Save to DB
+ aiDataAnalysisReportService.save(report);
+
+ // Try upload
+ DataAnalysisUploadService.UploadResult uploadResult = dataAnalysisUploadService.upload(report);
+ report.setUploadStatus(uploadResult.isSuccess() ? "uploaded" : (uploadResult.isSkipped() ? "skipped" : "failed"));
+ aiDataAnalysisReportService.updateById(report);
+ } catch (Exception e) {
+ log.error("Failed to save data analysis report", e);
+ }
+ }
+
+ private Date resolvePeriodStart(String periodType) {
+ // Simplified - the agent resolves the actual range
+ return new Date();
+ }
+
+ private Date resolvePeriodEnd(String periodType) {
+ return new Date();
+ }
+
+ private void safeWriteOperateLog(String triggerType, String periodType,
+ DataAnalysisAgentService.DataAnalysisAgentResult agentResult) {
+ try {
+ String memo = "AI鏁版嵁鍒嗘瀽 " + periodType + " " + triggerType
+ + " 缁撴灉:" + (Boolean.TRUE.equals(agentResult.getSuccess()) ? "鎴愬姛" : "澶辫触");
+ OperateLog operateLog = new OperateLog();
+ operateLog.setUserId(SYSTEM_USER_ID);
+ operateLog.setAction("AI鏁版嵁鍒嗘瀽");
+ operateLog.setRequest(memo);
+ operateLog.setCreateTime(new Date());
+ operateLogService.save(operateLog);
+ } catch (Exception e) {
+ log.warn("Failed to write operate log for data analysis", e);
+ }
+ }
+
+ private DataAnalysisAgentService.DataAnalysisAgentResult failedAgentResult(String periodType, Exception exception) {
+ DataAnalysisAgentService.DataAnalysisAgentResult result = new DataAnalysisAgentService.DataAnalysisAgentResult();
+ result.setSuccess(false);
+ result.setPeriodType(periodType);
+ result.setTriggerType("agent");
+ result.setSummary("鏁版嵁鍒嗘瀽浠诲姟鎵ц寮傚父: " + exception.getMessage());
+ result.setToolCallCount(0);
+ result.setLlmCallCount(0);
+ result.setPromptTokens(0L);
+ result.setCompletionTokens(0L);
+ result.setTotalTokens(0L);
+ result.setMaxRoundsReached(false);
+ return result;
+ }
+}
diff --git a/src/main/java/com/zy/ai/service/impl/DataAnalysisFileStorageServiceImpl.java b/src/main/java/com/zy/ai/service/impl/DataAnalysisFileStorageServiceImpl.java
new file mode 100644
index 0000000..4cd7860
--- /dev/null
+++ b/src/main/java/com/zy/ai/service/impl/DataAnalysisFileStorageServiceImpl.java
@@ -0,0 +1,71 @@
+package com.zy.ai.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.serializer.SerializerFeature;
+import com.zy.ai.entity.AiDataAnalysisReport;
+import com.zy.ai.service.DataAnalysisFileStorageService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.OutputStreamWriter;
+import java.nio.charset.StandardCharsets;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+@Slf4j
+@Service("dataAnalysisFileStorageService")
+public class DataAnalysisFileStorageServiceImpl implements DataAnalysisFileStorageService {
+
+ @Value("${dataAnalysisStorage.loggingPath:../stock/out/wcs/aiAnalysis}")
+ private String basePath;
+
+ @Override
+ public String saveReport(AiDataAnalysisReport report) {
+ try {
+ SimpleDateFormat dirFormat = new SimpleDateFormat("yyyyMMdd");
+ SimpleDateFormat fileFormat = new SimpleDateFormat("yyyyMMdd_HHmmss");
+ String dateDir = dirFormat.format(report.getCreateTime());
+ String timestamp = fileFormat.format(report.getCreateTime());
+
+ File dir = new File(basePath, dateDir);
+ if (!dir.exists() && !dir.mkdirs()) {
+ log.warn("Failed to create analysis storage directory: {}", dir.getAbsolutePath());
+ return null;
+ }
+
+ String fileName = "analysis_" + report.getPeriodType() + "_" + timestamp + ".json";
+ File file = new File(dir, fileName);
+
+ Map<String, Object> content = new LinkedHashMap<>();
+ content.put("periodType", report.getPeriodType());
+ content.put("periodStart", report.getPeriodStart());
+ content.put("periodEnd", report.getPeriodEnd());
+ content.put("triggerType", report.getTriggerType());
+ content.put("status", report.getStatus());
+ content.put("summary", report.getSummary());
+ content.put("structuredData", report.getStructuredData());
+ content.put("llmCallCount", report.getLlmCallCount());
+ content.put("totalTokens", report.getTotalTokens());
+ content.put("createTime", report.getCreateTime());
+ content.put("finishTime", report.getFinishTime());
+
+ try (OutputStreamWriter writer = new OutputStreamWriter(
+ new FileOutputStream(file), StandardCharsets.UTF_8)) {
+ writer.write(JSON.toJSONString(content, SerializerFeature.PrettyFormat,
+ SerializerFeature.WriteDateUseDateFormat));
+ }
+
+ String relativePath = dateDir + "/" + fileName;
+ log.info("Data analysis report saved to file: {}", relativePath);
+ return relativePath;
+ } catch (Exception e) {
+ log.error("Failed to save data analysis report to file", e);
+ return null;
+ }
+ }
+}
diff --git a/src/main/java/com/zy/ai/service/impl/DataAnalysisUploadServiceImpl.java b/src/main/java/com/zy/ai/service/impl/DataAnalysisUploadServiceImpl.java
new file mode 100644
index 0000000..d14b2b8
--- /dev/null
+++ b/src/main/java/com/zy/ai/service/impl/DataAnalysisUploadServiceImpl.java
@@ -0,0 +1,86 @@
+package com.zy.ai.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.zy.ai.entity.AiDataAnalysisReport;
+import com.zy.ai.entity.AiDataAnalysisUploadLog;
+import com.zy.ai.service.AiDataAnalysisUploadLogService;
+import com.zy.ai.service.DataAnalysisUploadService;
+import com.zy.common.utils.HttpHandler;
+import com.zy.system.service.ConfigService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+@Slf4j
+@Service("dataAnalysisUploadService")
+@RequiredArgsConstructor
+public class DataAnalysisUploadServiceImpl implements DataAnalysisUploadService {
+
+ private static final String CONFIG_UPLOAD_ENABLED = "aiDataAnalysisUploadEnabled";
+ private static final String CONFIG_UPLOAD_URL = "aiDataAnalysisUploadUrl";
+
+ private final ConfigService configService;
+ private final AiDataAnalysisUploadLogService aiDataAnalysisUploadLogService;
+
+ @Override
+ public UploadResult upload(AiDataAnalysisReport report) {
+ if (!isUploadEnabled()) {
+ return UploadResult.skipped();
+ }
+
+ String url = configService.getConfigValue(CONFIG_UPLOAD_URL, "");
+ if (url == null || url.trim().isEmpty()) {
+ return UploadResult.skipped();
+ }
+
+ Map<String, Object> payload = new LinkedHashMap<>();
+ payload.put("reportId", report.getId());
+ payload.put("periodType", report.getPeriodType());
+ payload.put("periodStart", report.getPeriodStart());
+ payload.put("periodEnd", report.getPeriodEnd());
+ payload.put("triggerType", report.getTriggerType());
+ payload.put("status", report.getStatus());
+ payload.put("summary", report.getSummary());
+ payload.put("structuredData", report.getStructuredData());
+ payload.put("totalTokens", report.getTotalTokens());
+ payload.put("createTime", report.getCreateTime());
+
+ String jsonBody = JSON.toJSONString(payload);
+ AiDataAnalysisUploadLog uploadLog = new AiDataAnalysisUploadLog();
+ uploadLog.setReportId(report.getId());
+ uploadLog.setUploadUrl(url);
+ uploadLog.setRequestBody(jsonBody);
+ uploadLog.setCreateTime(new Date());
+
+ try {
+ HttpHandler httpHandler = new HttpHandler.Builder()
+ .setUri(url)
+ .setJson(jsonBody)
+ .setTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
+ .build();
+
+ String response = httpHandler.doPost();
+ uploadLog.setHttpStatus(200);
+ uploadLog.setResponseBody(response);
+ uploadLog.setResult("success");
+ aiDataAnalysisUploadLogService.save(uploadLog);
+ log.info("Data analysis report uploaded, reportId={}, url={}", report.getId(), url);
+ return UploadResult.success(200, response);
+ } catch (Exception e) {
+ log.warn("Failed to upload data analysis report, reportId={}, url={}", report.getId(), url, e);
+ uploadLog.setResult("failed");
+ uploadLog.setErrorMessage(e.getMessage());
+ aiDataAnalysisUploadLogService.save(uploadLog);
+ return UploadResult.failed(null, e.getMessage());
+ }
+ }
+
+ private boolean isUploadEnabled() {
+ String value = configService.getConfigValue(CONFIG_UPLOAD_ENABLED, "0");
+ return "1".equals(value.trim());
+ }
+}
diff --git a/src/main/java/com/zy/ai/service/impl/LlmCallLogServiceImpl.java b/src/main/java/com/zy/ai/service/impl/LlmCallLogServiceImpl.java
index 7b411b2..f2053e6 100644
--- a/src/main/java/com/zy/ai/service/impl/LlmCallLogServiceImpl.java
+++ b/src/main/java/com/zy/ai/service/impl/LlmCallLogServiceImpl.java
@@ -12,19 +12,35 @@
public class LlmCallLogServiceImpl extends ServiceImpl<LlmCallLogMapper, LlmCallLog> implements LlmCallLogService {
private volatile boolean disabled = false;
+ private volatile long lastRetryTime = 0;
+ private static final long RETRY_INTERVAL_MS = 60_000; // 1 鍒嗛挓鍚庨噸璇�
@Override
public void saveIgnoreError(LlmCallLog logItem) {
- if (logItem == null || disabled) {
+ if (logItem == null) {
return;
+ }
+ if (disabled) {
+ // 瀹氭湡閲嶈瘯锛岄槻姝㈣〃鍚庢潵鍒涘缓浜嗕絾 disabled 涓�鐩翠负 true
+ long now = System.currentTimeMillis();
+ if (now - lastRetryTime < RETRY_INTERVAL_MS) {
+ return;
+ }
+ lastRetryTime = now;
+ log.info("LLM璋冪敤鏃ュ織涔嬪墠宸茬鐢紝灏濊瘯閲嶆柊鍐欏叆...");
}
try {
save(logItem);
+ if (disabled) {
+ disabled = false;
+ log.info("LLM璋冪敤鏃ュ織鍐欏叆鎴愬姛锛屽凡鎭㈠鏃ュ織璁板綍");
+ }
} catch (Exception e) {
String msg = e.getMessage() == null ? "" : e.getMessage();
if (msg.contains("doesn't exist") || msg.contains("涓嶅瓨鍦�")) {
disabled = true;
- log.warn("LLM璋冪敤鏃ュ織琛ㄤ笉瀛樺湪锛屾棩蹇楄褰曞凡鑷姩鍏抽棴锛岃鍏堟墽琛屽缓琛⊿QL");
+ lastRetryTime = System.currentTimeMillis();
+ log.warn("LLM璋冪敤鏃ュ織琛ㄤ笉瀛樺湪锛屾棩蹇楄褰曞凡鏆傚仠锛岃鍏堟墽琛屽缓琛⊿QL");
return;
}
log.warn("鍐欏叆LLM璋冪敤鏃ュ織澶辫触: {}", msg);
diff --git a/src/main/java/com/zy/ai/timer/DataAnalysisScheduler.java b/src/main/java/com/zy/ai/timer/DataAnalysisScheduler.java
new file mode 100644
index 0000000..2ed8cf7
--- /dev/null
+++ b/src/main/java/com/zy/ai/timer/DataAnalysisScheduler.java
@@ -0,0 +1,31 @@
+package com.zy.ai.timer;
+
+import com.zy.ai.service.DataAnalysisCoordinatorService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@Component
+public class DataAnalysisScheduler {
+
+ @Autowired
+ private DataAnalysisCoordinatorService dataAnalysisCoordinatorService;
+
+ @Scheduled(cron = "0 0 1 * * ?")
+ public void runDailyAnalysis() {
+ try {
+ DataAnalysisCoordinatorService.DataAnalysisCoordinatorResult result =
+ dataAnalysisCoordinatorService.runAnalysisIfEligible();
+ if (Boolean.TRUE.equals(result.getSkipped())) {
+ log.debug("Data analysis scheduler skipped, reason={}", result.getReason());
+ return;
+ }
+ log.info("Data analysis scheduler triggered, success={}",
+ result.getAgentResult() == null ? null : result.getAgentResult().getSuccess());
+ } catch (Exception e) {
+ log.error("Data analysis scheduler failed", e);
+ }
+ }
+}
diff --git a/src/main/java/com/zy/ai/utils/AiPromptUtils.java b/src/main/java/com/zy/ai/utils/AiPromptUtils.java
index 0cd96a1..0a499f3 100644
--- a/src/main/java/com/zy/ai/utils/AiPromptUtils.java
+++ b/src/main/java/com/zy/ai/utils/AiPromptUtils.java
@@ -220,6 +220,58 @@
"- 涓嶅緱鑷嗘祴鍥炴粴鍘熷洜銆�");
return blocks;
}
+ if (scene == AiPromptScene.DATA_ANALYSIS) {
+ blocks.put(AiPromptBlockType.BASE_POLICY,
+ "浣犳槸涓�鍚� WCS锛堜粨鍌ㄦ帶鍒剁郴缁燂級杩愯惀鏁版嵁鍒嗘瀽甯堬紝绮鹃�氳嚜鍔ㄥ寲绔嬪簱鐨勪换鍔¤皟搴︺�佽澶囪繍琛屽拰鏁呴殰鍒嗘瀽銆俓n\n" +
+ "浣犵殑鑱岃矗鏄細鍩轰簬绯荤粺鎻愪緵鐨勫巻鍙茶繍钀ユ暟鎹紝鐢熸垚缁撴瀯鍖栫殑鏁版嵁鍒嗘瀽鎶ュ憡锛屽府鍔╄繍缁翠汉鍛樹簡瑙d粨搴撹繍琛岀姸鍐点�佸彂鐜版綔鍦ㄩ棶棰樺苟鎻愬嚭浼樺寲寤鸿銆�");
+ blocks.put(AiPromptBlockType.TOOL_POLICY,
+ "==================== 鍙敤 MCP 宸ュ叿 ====================\n\n" +
+ "浣犲彲浠ヨ皟鐢ㄤ互涓嬪伐鍏疯幏鍙栬仛鍚堢粺璁℃暟鎹紙宸ュ叿杩斿洖 JSON锛夛細\n" +
+ "- " + localTool("analysis_query_task_throughput") + "锛氭煡璇换鍔″悶鍚愰噺锛堜换鍔℃�婚噺銆佸叆搴�/鍑哄簱/绉诲簱鏁伴噺銆佸钩鍧囨椂闀裤�佹晠闅滄眹鎬伙級\n" +
+ "- " + localTool("analysis_query_device_fault_summary") + "锛氭煡璇㈣澶囨晠闅滄眹鎬伙紙鎸夎澶囩被鍨嬬粺璁℃晠闅滄鏁板拰鏃堕暱锛塡n" +
+ "- " + localTool("analysis_query_device_utilization") + "锛氭煡璇㈣澶囧埄鐢ㄧ巼锛堟寜璁惧缂栧彿缁熻浠诲姟鍒嗛厤閲忋�佸钩鍧囨椂闀匡級\n" +
+ "- " + localTool("analysis_query_error_logs") + "锛氭煡璇㈣澶囬敊璇棩蹇楃粺璁★紙鎸夎澶囩被鍨嬬粺璁¢敊璇鏁帮級\n\n" +
+ "浣跨敤绛栫暐锛歕n" +
+ "1锛夊厛璋冪敤 throughput 鍜� fault 宸ュ叿鑾峰彇鎬讳綋姒傚喌銆俓n" +
+ "2锛夊鏈夊紓甯告寚鏍囷紝鍐嶈皟鐢� utilization 鍜� error_logs 娣卞叆鍒嗘瀽銆俓n" +
+ "3锛夋墍鏈夊伐鍏烽兘闇�瑕佷紶鍏� startTime 鍜� endTime 鍙傛暟銆俓n" +
+ "4锛夌姝㈣噯娴嬶紝鎵�鏈夋暟鎹繀椤绘潵鑷伐鍏疯繑鍥炪��");
+ blocks.put(AiPromptBlockType.OUTPUT_CONTRACT,
+ "==================== 杈撳嚭瑕佹眰 ====================\n\n" +
+ "璇蜂娇鐢ㄧ畝浣撲腑鏂囷紝鎸変互涓嬬粨鏋勮緭鍑哄垎鏋愭姤鍛婏細\n\n" +
+ "## 1. 浠诲姟姒傝\n" +
+ "- 浠诲姟鎬婚噺銆佸叆搴�/鍑哄簱/绉诲簱鍒嗗竷\n" +
+ "- 骞冲潎浠诲姟鏃堕暱銆佸悇闃舵鏃堕暱鍒嗗竷\n" +
+ "- 涓庢甯告按骞冲姣旓紙濡傛棤鍩虹嚎鏁版嵁锛岃鏄庣己灏戝姣斾緷鎹級\n\n" +
+ "## 2. 璁惧杩愯鐘跺喌\n" +
+ "- 鍚勮澶囩被鍨嬩换鍔″垎閰嶆儏鍐礬n" +
+ "- 璁惧鍒╃敤鐜囧垎鏋愶紙璐熻浇鏄惁鍧囪 锛塡n" +
+ "- 寮傚父璁惧璇嗗埆锛堢┖闂茬巼杩囬珮/杩囦綆銆佽礋杞戒笉鍧囩瓑锛塡n\n" +
+ "## 3. 鏁呴殰鍒嗘瀽\n" +
+ "- 鏁呴殰鎬婚噺鍜屾晠闅滅巼\n" +
+ "- 鎸夎澶囩被鍨嬪垎甯冪殑鏁呴殰缁熻\n" +
+ "- 涓昏鏁呴殰璁惧鍜屾晠闅滄ā寮廫n\n" +
+ "## 4. 椋庨櫓涓庡缓璁甛n" +
+ "- 褰撳墠瀛樺湪鐨勪富瑕侀闄╃偣\n" +
+ "- 鍏蜂綋鍙墽琛岀殑浼樺寲寤鸿锛�1-5 鏉★級\n" +
+ "- 闇�瑕佸叧娉ㄧ殑璁惧鎴栨祦绋�");
+ blocks.put(AiPromptBlockType.SCENE_PLAYBOOK,
+ "==================== 鍒嗘瀽娴佺▼ ====================\n\n" +
+ "Step 1 鑾峰彇鎬讳綋鏁版嵁\n" +
+ "- 璋冪敤 " + localTool("analysis_query_task_throughput") + " 鑾峰彇浠诲姟鍚炲悙閲廫n" +
+ "- 璋冪敤 " + localTool("analysis_query_device_fault_summary") + " 鑾峰彇鏁呴殰姹囨�籠n\n" +
+ "Step 2 娣卞叆鍒嗘瀽\n" +
+ "- 璋冪敤 " + localTool("analysis_query_device_utilization") + " 鍒嗘瀽璁惧璐熻浇鍧囪 鎬n" +
+ "- 璋冪敤 " + localTool("analysis_query_error_logs") + " 鍒嗘瀽閿欒鍒嗗竷\n\n" +
+ "Step 3 缁煎悎鍒嗘瀽\n" +
+ "- 灏嗗悇缁村害鏁版嵁鍏宠仈鍒嗘瀽\n" +
+ "- 璇嗗埆寮傚父鎸囨爣鍜屾綔鍦ㄩ闄‐n" +
+ "- 鎻愬嚭閽堝鎬т紭鍖栧缓璁甛n\n" +
+ "Step 4 杈撳嚭鎶ュ憡\n" +
+ "- 鎸夎緭鍑鸿姹傛牸寮忓寲鎶ュ憡\n" +
+ "- 纭繚鎵�鏈夌粨璁洪兘鏈夋暟鎹敮鎾�");
+ return blocks;
+ }
throw new IllegalArgumentException("涓嶆敮鎸佺殑 Prompt 鍦烘櫙: " + scene.getCode());
}
diff --git a/src/main/java/com/zy/asrs/mapper/WrkAnalysisMapper.java b/src/main/java/com/zy/asrs/mapper/WrkAnalysisMapper.java
index 8a06a89..37120bc 100644
--- a/src/main/java/com/zy/asrs/mapper/WrkAnalysisMapper.java
+++ b/src/main/java/com/zy/asrs/mapper/WrkAnalysisMapper.java
@@ -3,9 +3,18 @@
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.zy.asrs.entity.WrkAnalysis;
import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
@Mapper
@Repository
public interface WrkAnalysisMapper extends BaseMapper<WrkAnalysis> {
+
+ Map<String, Object> aggregateThroughput(@Param("startTime") Date startTime, @Param("endTime") Date endTime);
+
+ List<Map<String, Object>> groupByDevice(@Param("startTime") Date startTime, @Param("endTime") Date endTime);
}
diff --git a/src/main/java/com/zy/core/enums/RedisKeyType.java b/src/main/java/com/zy/core/enums/RedisKeyType.java
index 0bbf8c5..1c9b8b6 100644
--- a/src/main/java/com/zy/core/enums/RedisKeyType.java
+++ b/src/main/java/com/zy/core/enums/RedisKeyType.java
@@ -81,6 +81,8 @@
AI_AUTO_TUNE_RUNNING_LOCK("ai_auto_tune_running_lock"),
AI_AUTO_TUNE_APPLY_LOCK("ai_auto_tune_apply_lock"),
AI_AUTO_TUNE_LAST_TRIGGER_GUARD("ai_auto_tune_last_trigger_guard"),
+ AI_DATA_ANALYSIS_RUNNING_LOCK("ai_data_analysis_running_lock"),
+ AI_DATA_ANALYSIS_LAST_TRIGGER_GUARD("ai_data_analysis_last_trigger_guard"),
PLANNER_SCHEDULE("planner_schedule_"),
HIGH_PRIVILEGE_GRANT("high_privilege_grant_"),
;
diff --git a/src/main/java/com/zy/system/controller/DashboardController.java b/src/main/java/com/zy/system/controller/DashboardController.java
index 989f8c7..974bfce 100644
--- a/src/main/java/com/zy/system/controller/DashboardController.java
+++ b/src/main/java/com/zy/system/controller/DashboardController.java
@@ -5,10 +5,12 @@
import com.core.common.R;
import com.zy.ai.entity.AiAutoTuneJob;
import com.zy.ai.entity.AiChatSession;
+import com.zy.ai.entity.AiTokenUsage;
import com.zy.ai.entity.LlmCallLog;
import com.zy.ai.entity.LlmRouteConfig;
import com.zy.ai.enums.AiPromptScene;
import com.zy.ai.mapper.AiChatSessionMapper;
+import com.zy.ai.mapper.AiTokenUsageMapper;
import com.zy.ai.service.AiAutoTuneJobService;
import com.zy.ai.service.LlmCallLogService;
import com.zy.ai.service.LlmRouteConfigService;
@@ -65,6 +67,8 @@
private AiAutoTuneJobService aiAutoTuneJobService;
@Autowired
private AiChatSessionMapper aiChatSessionMapper;
+ @Autowired
+ private AiTokenUsageMapper aiTokenUsageMapper;
@Autowired
private DevicePingFileStorageService devicePingFileStorageService;
@@ -327,49 +331,37 @@
private Map<String, Object> buildAiStats() {
Map<String, Object> result = new LinkedHashMap<>();
+ // 浠庣嫭绔嬬疮璁¤〃璇诲彇 token 缁熻
long tokenTotal = 0L;
long promptTokenTotal = 0L;
long completionTokenTotal = 0L;
+ long llmCallCountTotal = 0L;
+ try {
+ AiTokenUsage tokenUsage = aiTokenUsageMapper.selectById(1);
+ if (tokenUsage != null) {
+ promptTokenTotal = safeCount(tokenUsage.getPromptTokens());
+ completionTokenTotal = safeCount(tokenUsage.getCompletionTokens());
+ tokenTotal = safeCount(tokenUsage.getTotalTokens());
+ llmCallCountTotal = safeCount(tokenUsage.getLlmCallCount());
+ }
+ } catch (Exception e) {
+ log.warn("dashboard ai token usage load failed: {}", safeMessage(e));
+ }
+
+ // 浼氳瘽缁熻锛堜繚鐣欑敤浜庢樉绀轰細璇濇暟鍜屾彁闂疆娆★級
long askCount = 0L;
long sessionCount = 0L;
- long autoTunePromptTokenTotal = 0L;
- long autoTuneCompletionTokenTotal = 0L;
- long autoTuneTokenTotal = 0L;
try {
List<AiChatSession> sessions = aiChatSessionMapper.selectList(new QueryWrapper<AiChatSession>()
- .select("id", "sum_prompt_tokens", "sum_completion_tokens", "sum_total_tokens", "ask_count"));
+ .select("id", "ask_count"));
sessionCount = sessions == null ? 0L : sessions.size();
if (sessions != null) {
for (AiChatSession session : sessions) {
- promptTokenTotal += safeCount(session == null ? null : session.getSumPromptTokens());
- completionTokenTotal += safeCount(session == null ? null : session.getSumCompletionTokens());
- tokenTotal += safeCount(session == null ? null : session.getSumTotalTokens());
askCount += safeCount(session == null ? null : session.getAskCount());
}
}
} catch (Exception e) {
log.warn("dashboard ai session stats load failed: {}", safeMessage(e));
- }
-
- try {
- List<Map<String, Object>> autoTuneRows = aiAutoTuneJobService.listMaps(new QueryWrapper<AiAutoTuneJob>()
- .select("COALESCE(SUM(prompt_tokens), 0) AS prompt_token_total",
- "COALESCE(SUM(completion_tokens), 0) AS completion_token_total",
- "COALESCE(SUM(total_tokens), 0) AS token_total")
- .eq("prompt_scene_code", AiPromptScene.AUTO_TUNE_DISPATCH.getCode()));
- Map<String, Object> autoTuneRow = autoTuneRows == null || autoTuneRows.isEmpty()
- ? Collections.emptyMap()
- : autoTuneRows.get(0);
- autoTunePromptTokenTotal = toLong(autoTuneRow.get("prompt_token_total"));
- autoTuneCompletionTokenTotal = toLong(autoTuneRow.get("completion_token_total"));
- autoTuneTokenTotal = toLong(autoTuneRow.get("token_total"));
-
- // Agent 鑷姩璋冨弬涓嶇敓鎴� sys_ai_chat_session锛屼細鍗曠嫭钀藉埌 sys_ai_auto_tune_job銆�
- promptTokenTotal += autoTunePromptTokenTotal;
- completionTokenTotal += autoTuneCompletionTokenTotal;
- tokenTotal += autoTuneTokenTotal;
- } catch (Exception e) {
- log.warn("dashboard ai auto tune token stats load failed: {}", safeMessage(e));
}
List<LlmRouteConfig> routes = Collections.emptyList();
@@ -444,9 +436,7 @@
overview.put("tokenTotal", tokenTotal);
overview.put("promptTokenTotal", promptTokenTotal);
overview.put("completionTokenTotal", completionTokenTotal);
- overview.put("autoTuneTokenTotal", autoTuneTokenTotal);
- overview.put("autoTunePromptTokenTotal", autoTunePromptTokenTotal);
- overview.put("autoTuneCompletionTokenTotal", autoTuneCompletionTokenTotal);
+ overview.put("llmCallCountTotal", llmCallCountTotal);
overview.put("askCount", askCount);
overview.put("sessionCount", sessionCount);
overview.put("routeTotal", routeTotal);
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 9e39ace..8e8e9b3 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -1,6 +1,6 @@
# 绯荤粺鐗堟湰淇℃伅
app:
- version: 3.0.1.6
+ version: 3.0.1.7
version-type: prd # prd 鎴� dev
i18n:
default-locale: zh-CN
diff --git a/src/main/resources/mapper/AiTokenUsageMapper.xml b/src/main/resources/mapper/AiTokenUsageMapper.xml
new file mode 100644
index 0000000..d53b2f2
--- /dev/null
+++ b/src/main/resources/mapper/AiTokenUsageMapper.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.zy.ai.mapper.AiTokenUsageMapper">
+
+ <resultMap id="BaseResultMap" type="com.zy.ai.entity.AiTokenUsage">
+ <id column="id" property="id" />
+ <result column="prompt_tokens" property="promptTokens" />
+ <result column="completion_tokens" property="completionTokens" />
+ <result column="total_tokens" property="totalTokens" />
+ <result column="llm_call_count" property="llmCallCount" />
+ <result column="update_time" property="updateTime" />
+ </resultMap>
+
+ <update id="incrementTokens">
+ UPDATE sys_ai_token_usage
+ SET prompt_tokens = prompt_tokens + #{promptTokens},
+ completion_tokens = completion_tokens + #{completionTokens},
+ total_tokens = total_tokens + #{totalTokens},
+ llm_call_count = llm_call_count + #{callCount},
+ update_time = NOW()
+ WHERE id = 1
+ </update>
+
+</mapper>
diff --git a/src/main/resources/mapper/WrkAnalysisMapper.xml b/src/main/resources/mapper/WrkAnalysisMapper.xml
index 998b550..6c7c293 100644
--- a/src/main/resources/mapper/WrkAnalysisMapper.xml
+++ b/src/main/resources/mapper/WrkAnalysisMapper.xml
@@ -41,4 +41,48 @@
<result column="update_time" property="updateTime" />
</resultMap>
+ <select id="aggregateThroughput" resultType="map">
+ SELECT
+ COUNT(*) as taskCount,
+ SUM(CASE WHEN io_type = 1 THEN 1 ELSE 0 END) as inboundCount,
+ SUM(CASE WHEN io_type = 2 THEN 1 ELSE 0 END) as outboundCount,
+ SUM(CASE WHEN io_type NOT IN (1, 2) THEN 1 ELSE 0 END) as moveCount,
+ ROUND(AVG(total_duration_ms)) as avgTotalDurationMs,
+ ROUND(AVG(station_duration_ms)) as avgStationDurationMs,
+ ROUND(AVG(crane_duration_ms)) as avgCraneDurationMs,
+ SUM(has_fault) as faultTaskCount,
+ SUM(fault_count) as totalFaultCount,
+ SUM(fault_duration_ms) as totalFaultDurationMs,
+ SUM(crn_fault_count) as crnFaultCount,
+ SUM(crn_fault_duration_ms) as crnFaultDurationMs,
+ SUM(dual_crn_fault_count) as dualCrnFaultCount,
+ SUM(dual_crn_fault_duration_ms) as dualCrnFaultDurationMs,
+ SUM(rgv_fault_count) as rgvFaultCount,
+ SUM(rgv_fault_duration_ms) as rgvFaultDurationMs,
+ SUM(station_fault_count) as stationFaultCount,
+ SUM(station_fault_duration_ms) as stationFaultDurationMs
+ FROM asr_wrk_analysis
+ WHERE finish_time >= #{startTime} AND finish_time < #{endTime}
+ </select>
+
+ <select id="groupByDevice" resultType="map">
+ SELECT
+ CASE
+ WHEN crn_no IS NOT NULL THEN 'CRN'
+ WHEN dual_crn_no IS NOT NULL THEN 'DUAL_CRN'
+ WHEN rgv_no IS NOT NULL THEN 'RGV'
+ ELSE 'UNKNOWN'
+ END as deviceType,
+ COALESCE(crn_no, dual_crn_no, rgv_no) as deviceNo,
+ COUNT(*) as taskCount,
+ ROUND(AVG(total_duration_ms)) as avgDurationMs,
+ SUM(has_fault) as faultTaskCount,
+ SUM(fault_count) as faultCount,
+ SUM(fault_duration_ms) as faultDurationMs
+ FROM asr_wrk_analysis
+ WHERE finish_time >= #{startTime} AND finish_time < #{endTime}
+ GROUP BY deviceType, deviceNo
+ ORDER BY taskCount DESC
+ </select>
+
</mapper>
diff --git a/src/main/resources/sql/20260505_ai_data_analysis.sql b/src/main/resources/sql/20260505_ai_data_analysis.sql
new file mode 100644
index 0000000..34b98ec
--- /dev/null
+++ b/src/main/resources/sql/20260505_ai_data_analysis.sql
@@ -0,0 +1,146 @@
+-- AI鏁版嵁鍒嗘瀽鎶ュ憡
+CREATE TABLE IF NOT EXISTS sys_ai_data_analysis_report (
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
+ period_type VARCHAR(20) NOT NULL COMMENT 'TODAY/YESTERDAY/THIS_WEEK/THIS_MONTH',
+ period_start DATETIME COMMENT '鍒嗘瀽鍛ㄦ湡寮�濮嬫椂闂�',
+ period_end DATETIME COMMENT '鍒嗘瀽鍛ㄦ湡缁撴潫鏃堕棿',
+ trigger_type VARCHAR(20) NOT NULL COMMENT 'auto/manual',
+ status VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT 'pending/running/success/failed',
+ summary TEXT COMMENT 'LLM鐢熸垚鐨勮嚜鐒惰瑷�鍒嗘瀽鎶ュ憡',
+ structured_data LONGTEXT COMMENT 'JSON鏍煎紡鐨勭粨鏋勫寲鍒嗘瀽鏁版嵁',
+ llm_call_count INT DEFAULT 0,
+ prompt_tokens INT DEFAULT 0,
+ completion_tokens INT DEFAULT 0,
+ total_tokens INT DEFAULT 0,
+ error_message VARCHAR(1024),
+ local_file_path VARCHAR(512) COMMENT '鏈湴瀛樺偍鏂囦欢璺緞',
+ upload_status VARCHAR(20) DEFAULT 'pending' COMMENT 'pending/uploaded/failed/skipped',
+ create_time DATETIME NOT NULL,
+ finish_time DATETIME,
+ INDEX idx_period_type (period_type),
+ INDEX idx_trigger_type (trigger_type),
+ INDEX idx_create_time (create_time)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI鏁版嵁鍒嗘瀽鎶ュ憡';
+
+-- AI鏁版嵁鍒嗘瀽鎶ュ憡涓婁紶鏃ュ織
+CREATE TABLE IF NOT EXISTS sys_ai_data_analysis_upload_log (
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
+ report_id BIGINT NOT NULL COMMENT '鍏宠仈鎶ュ憡ID',
+ upload_url VARCHAR(512),
+ request_body TEXT,
+ response_body TEXT,
+ http_status INT,
+ result VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT 'success/failed',
+ error_message VARCHAR(1024),
+ retry_count INT DEFAULT 0,
+ create_time DATETIME NOT NULL,
+ INDEX idx_report_id (report_id),
+ INDEX idx_result (result)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='鏁版嵁鍒嗘瀽鎶ュ憡涓婁紶鏃ュ織';
+
+-- 閰嶇疆椤�
+INSERT INTO sys_config(name, code, value, type, status, select_type)
+SELECT 'AI鏁版嵁鍒嗘瀽鍔熻兘寮�鍏�', 'aiDataAnalysisEnabled', '0', 1, 1, 'develop' FROM dual
+WHERE NOT EXISTS (SELECT 1 FROM sys_config WHERE code = 'aiDataAnalysisEnabled');
+
+INSERT INTO sys_config(name, code, value, type, status, select_type)
+SELECT 'AI鏁版嵁鍒嗘瀽瀹氭椂鍛ㄦ湡', 'aiDataAnalysisScheduledPeriods', 'YESTERDAY', 1, 1, 'develop' FROM dual
+WHERE NOT EXISTS (SELECT 1 FROM sys_config WHERE code = 'aiDataAnalysisScheduledPeriods');
+
+INSERT INTO sys_config(name, code, value, type, status, select_type)
+SELECT 'AI鏁版嵁鍒嗘瀽瀹氭椂Cron', 'aiDataAnalysisCron', '0 0 1 * * ?', 1, 1, 'develop' FROM dual
+WHERE NOT EXISTS (SELECT 1 FROM sys_config WHERE code = 'aiDataAnalysisCron');
+
+INSERT INTO sys_config(name, code, value, type, status, select_type)
+SELECT 'AI鏁版嵁鍒嗘瀽涓婁紶鍦板潃', 'aiDataAnalysisUploadUrl', '', 1, 1, 'develop' FROM dual
+WHERE NOT EXISTS (SELECT 1 FROM sys_config WHERE code = 'aiDataAnalysisUploadUrl');
+
+INSERT INTO sys_config(name, code, value, type, status, select_type)
+SELECT 'AI鏁版嵁鍒嗘瀽涓婁紶寮�鍏�', 'aiDataAnalysisUploadEnabled', '0', 1, 1, 'develop' FROM dual
+WHERE NOT EXISTS (SELECT 1 FROM sys_config WHERE code = 'aiDataAnalysisUploadEnabled');
+
+-- 鑿滃崟锛欰I绠$悊 -> AI鏁版嵁鍒嗘瀽
+-- 鎵ц鍚庤鍦�"瑙掕壊鎺堟潈"閲岀粰瀵瑰簲瑙掕壊鍕鹃�� AI绠$悊 -> AI鏁版嵁鍒嗘瀽銆�
+SET @ai_manage_id := COALESCE(
+ (
+ SELECT id
+ FROM sys_resource
+ WHERE code = 'aiManage' AND level = 1
+ ORDER BY id
+ LIMIT 1
+ ),
+ (
+ SELECT id
+ FROM sys_resource
+ WHERE name = 'AI绠$悊' AND level = 1
+ ORDER BY id
+ LIMIT 1
+ )
+);
+
+INSERT INTO sys_resource(code, name, resource_id, level, sort, status)
+SELECT 'ai/data_analysis.html', 'AI鏁版嵁鍒嗘瀽', @ai_manage_id, 2, 5, 1
+FROM dual
+WHERE @ai_manage_id IS NOT NULL
+ AND NOT EXISTS (
+ SELECT 1
+ FROM sys_resource
+ WHERE code = 'ai/data_analysis.html' AND level = 2
+ );
+
+UPDATE sys_resource
+SET name = 'AI鏁版嵁鍒嗘瀽',
+ resource_id = @ai_manage_id,
+ level = 2,
+ sort = 5,
+ status = 1
+WHERE code = 'ai/data_analysis.html' AND level = 2;
+
+SET @ai_data_analysis_id := (
+ SELECT id
+ FROM sys_resource
+ WHERE code = 'ai/data_analysis.html' AND level = 2
+ ORDER BY id
+ LIMIT 1
+);
+
+INSERT INTO sys_resource(code, name, resource_id, level, sort, status)
+SELECT 'ai/data_analysis.html#view', '鏌ョ湅', @ai_data_analysis_id, 3, 1, 1
+FROM dual
+WHERE @ai_data_analysis_id IS NOT NULL
+ AND NOT EXISTS (
+ SELECT 1
+ FROM sys_resource
+ WHERE code = 'ai/data_analysis.html#view' AND level = 3
+ );
+
+UPDATE sys_resource
+SET name = '鏌ョ湅',
+ resource_id = @ai_data_analysis_id,
+ level = 3,
+ sort = 1,
+ status = 1
+WHERE code = 'ai/data_analysis.html#view' AND level = 3;
+
+-- 楠岃瘉鑿滃崟鍒涘缓缁撴灉
+SELECT id, code, name, resource_id, level, sort, status
+FROM sys_resource
+WHERE code IN (
+ 'ai/data_analysis.html',
+ 'ai/data_analysis.html#view'
+)
+ORDER BY level, sort, id;
+
+-- AI绱Token浣跨敤缁熻锛堢嫭绔嬪瓨鍌紝涓嶅彈鍘嗗彶璁板綍鍒犻櫎褰卞搷锛�
+CREATE TABLE IF NOT EXISTS sys_ai_token_usage (
+ id INT PRIMARY KEY DEFAULT 1,
+ prompt_tokens BIGINT NOT NULL DEFAULT 0,
+ completion_tokens BIGINT NOT NULL DEFAULT 0,
+ total_tokens BIGINT NOT NULL DEFAULT 0,
+ llm_call_count BIGINT NOT NULL DEFAULT 0,
+ update_time DATETIME
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI绱Token浣跨敤缁熻';
+
+INSERT INTO sys_ai_token_usage (id, prompt_tokens, completion_tokens, total_tokens, llm_call_count, update_time)
+SELECT 1, 0, 0, 0, 0, NOW() FROM dual
+WHERE NOT EXISTS (SELECT 1 FROM sys_ai_token_usage WHERE id = 1);
diff --git a/src/main/webapp/views/ai/data_analysis.html b/src/main/webapp/views/ai/data_analysis.html
new file mode 100644
index 0000000..25dd85a
--- /dev/null
+++ b/src/main/webapp/views/ai/data_analysis.html
@@ -0,0 +1,451 @@
+<!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>
+ <link rel="stylesheet" href="../../static/vue/element/element.css" />
+ <style>
+ body {
+ margin: 0;
+ font-family: "Avenir Next", "PingFang SC", "Microsoft YaHei", sans-serif;
+ background:
+ radial-gradient(900px 460px at 4% -8%, rgba(36, 113, 92, 0.16), transparent 52%),
+ radial-gradient(820px 420px at 106% 0%, rgba(20, 82, 128, 0.14), transparent 54%),
+ linear-gradient(180deg, #f4f8fb 0%, #eef4f8 100%);
+ color: #223046;
+ }
+ .console-page {
+ max-width: 1680px;
+ margin: 16px auto;
+ padding: 0 14px 22px;
+ }
+ .hero {
+ border-radius: 18px;
+ color: #fff;
+ padding: 16px;
+ background:
+ linear-gradient(135deg, rgba(14, 76, 82, 0.96), rgba(31, 115, 108, 0.92) 48%, rgba(44, 130, 86, 0.94)),
+ radial-gradient(460px 180px at 80% 0%, rgba(255, 255, 255, 0.24), transparent 60%);
+ box-shadow: 0 14px 34px rgba(26, 76, 91, 0.22);
+ }
+ .hero-top {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ flex-wrap: wrap;
+ }
+ .hero-title {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ min-width: 280px;
+ }
+ .hero-title .main {
+ font-size: 18px;
+ font-weight: 700;
+ letter-spacing: 0.2px;
+ }
+ .hero-title .sub {
+ margin-top: 4px;
+ font-size: 12px;
+ opacity: 0.9;
+ }
+ .hero-actions {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 8px;
+ flex-wrap: wrap;
+ }
+ .panel {
+ border-radius: 16px;
+ border: 1px solid #dfe8f1;
+ background: rgba(255, 255, 255, 0.88);
+ box-shadow: 0 10px 28px rgba(31, 62, 92, 0.1);
+ overflow: hidden;
+ margin-top: 12px;
+ }
+ .panel-head {
+ padding: 12px 14px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ border-bottom: 1px solid #edf2f7;
+ background: linear-gradient(180deg, #ffffff 0%, #f8fbfd 100%);
+ }
+ .panel-title {
+ font-weight: 700;
+ color: #223046;
+ }
+ .panel-body {
+ padding: 12px 14px 14px;
+ }
+ .status-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 12px;
+ }
+ .status-item {
+ padding: 12px 14px;
+ border-radius: 10px;
+ border: 1px solid #e4ebf2;
+ background: #f8fbfd;
+ }
+ .status-item .label {
+ font-size: 12px;
+ color: #718299;
+ margin-bottom: 4px;
+ }
+ .status-item .value {
+ font-size: 14px;
+ font-weight: 600;
+ color: #223046;
+ }
+ .status-item .desc {
+ font-size: 11px;
+ color: #999;
+ margin-top: 2px;
+ }
+ .report-summary {
+ margin-top: 12px;
+ padding: 14px;
+ border-radius: 12px;
+ border: 1px solid #e4ebf2;
+ background: #f8fbfd;
+ }
+ .report-summary h3 {
+ margin: 0 0 10px 0;
+ font-size: 15px;
+ color: #223046;
+ }
+ .report-summary pre {
+ white-space: pre-wrap;
+ word-break: break-word;
+ font-size: 13px;
+ line-height: 1.6;
+ color: #333;
+ margin: 0;
+ max-height: 500px;
+ overflow-y: auto;
+ }
+ </style>
+</head>
+<body>
+<div id="app">
+ <div class="console-page">
+ <div class="hero">
+ <div class="hero-top">
+ <div class="hero-title">
+ <span v-html="headerIcon"></span>
+ <div>
+ <div class="main">AI 鏁版嵁鍒嗘瀽</div>
+ <div class="sub">鍩轰簬 LLM 鐨� WCS 杩愯惀鏁版嵁鍒嗘瀽锛屾敮鎸佹墜鍔ㄨЕ鍙戝拰瀹氭椂鑷姩鎵ц</div>
+ </div>
+ </div>
+ <div class="hero-actions">
+ <span style="font-size:13px;opacity:0.9;">鍔熻兘寮�鍏筹細</span>
+ <el-switch
+ v-model="enabled"
+ active-text="鍚敤"
+ inactive-text="鍏抽棴"
+ active-color="#13ce66"
+ inactive-color="#ff4949"
+ :disabled="enabledLoading"
+ @change="onEnabledChange">
+ </el-switch>
+ </div>
+ </div>
+ </div>
+
+ <div class="panel">
+ <div class="panel-head">
+ <div>
+ <div class="panel-title">褰撳墠閰嶇疆鐘舵��</div>
+ <div style="color:#718299;font-size:12px;margin-top:2px;">寮�鍏虫帶鍒跺畾鏃跺垎鏋愬拰鎵嬪姩鍒嗘瀽鏄惁鎵ц</div>
+ </div>
+ <el-button size="mini" @click="loadConfig">鍒锋柊</el-button>
+ </div>
+ <div class="panel-body">
+ <div class="status-grid">
+ <div class="status-item">
+ <div class="label">鍔熻兘寮�鍏�</div>
+ <div class="value" :style="{color: enabled ? '#67c23a' : '#f56c6c'}">
+ {{ enabled ? '宸插惎鐢�' : '宸插叧闂�' }}
+ </div>
+ <div class="desc">鍏抽棴鍚庡畾鏃朵换鍔″拰鎵嬪姩瑙﹀彂鍧囦笉鎵ц</div>
+ </div>
+ <div class="status-item">
+ <div class="label">瀹氭椂鍒嗘瀽鍛ㄦ湡</div>
+ <div class="value">{{ periodLabel(config.scheduledPeriods || 'YESTERDAY') }}</div>
+ <div class="desc">瀹氭椂浠诲姟鍒嗘瀽鐨勬椂闂磋寖鍥�</div>
+ </div>
+ <div class="status-item">
+ <div class="label">瀹氭椂鎵ц鏃堕棿</div>
+ <div class="value">{{ cronDesc }}</div>
+ <div class="desc">Cron: {{ config.cron || '0 0 1 * * ?' }}</div>
+ </div>
+ <div class="status-item">
+ <div class="label">鍏綉涓婁紶</div>
+ <div class="value" :style="{color: config.uploadEnabled ? '#67c23a' : '#999'}">
+ {{ config.uploadEnabled ? '宸插惎鐢�' : '鏈惎鐢�' }}
+ </div>
+ <div class="desc">{{ config.uploadUrl || '鏈厤缃笂浼犲湴鍧�' }}</div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="panel">
+ <div class="panel-head">
+ <div>
+ <div class="panel-title">鎵嬪姩鍒嗘瀽</div>
+ <div style="color:#718299;font-size:12px;margin-top:2px;">鐐瑰嚮鎸夐挳绔嬪嵆瑙﹀彂鎸囧畾鍛ㄦ湡鐨� AI 鏁版嵁鍒嗘瀽</div>
+ </div>
+ </div>
+ <div class="panel-body">
+ <el-button-group>
+ <el-button type="primary" :disabled="!enabled || triggerLoading" :loading="triggerLoading && triggerPeriod==='TODAY'" @click="triggerAnalysis('TODAY')">鍒嗘瀽浠婃棩</el-button>
+ <el-button type="primary" :disabled="!enabled || triggerLoading" :loading="triggerLoading && triggerPeriod==='YESTERDAY'" @click="triggerAnalysis('YESTERDAY')">鍒嗘瀽鏄ㄦ棩</el-button>
+ <el-button type="primary" :disabled="!enabled || triggerLoading" :loading="triggerLoading && triggerPeriod==='THIS_WEEK'" @click="triggerAnalysis('THIS_WEEK')">鍒嗘瀽鏈懆</el-button>
+ <el-button type="primary" :disabled="!enabled || triggerLoading" :loading="triggerLoading && triggerPeriod==='THIS_MONTH'" @click="triggerAnalysis('THIS_MONTH')">鍒嗘瀽鏈湀</el-button>
+ </el-button-group>
+ <span v-if="!enabled" style="margin-left:12px;color:#f56c6c;font-size:13px;">
+ <i class="el-icon-warning"></i> 鍔熻兘鏈惎鐢紝璇峰厛鎵撳紑涓婃柟寮�鍏�
+ </span>
+ </div>
+ </div>
+
+ <div class="panel">
+ <div class="panel-head">
+ <div>
+ <div class="panel-title">鍒嗘瀽鎶ュ憡</div>
+ <div style="color:#718299;font-size:12px;margin-top:2px;">鏈�杩戠敓鎴愮殑鍒嗘瀽鎶ュ憡</div>
+ </div>
+ <el-button size="mini" :loading="reportsLoading" @click="loadReports">鍒锋柊</el-button>
+ </div>
+ <div class="panel-body">
+ <el-table :data="reports" v-loading="reportsLoading" stripe size="small" style="width:100%" @row-click="onReportClick">
+ <el-table-column prop="id" label="ID" width="60"></el-table-column>
+ <el-table-column prop="periodType" label="鍛ㄦ湡" min-width="80">
+ <template slot-scope="scope">
+ {{ periodLabel(scope.row.periodType) }}
+ </template>
+ </el-table-column>
+ <el-table-column prop="triggerType" label="瑙﹀彂鏂瑰紡" width="90">
+ <template slot-scope="scope">
+ <el-tag size="mini" :type="scope.row.triggerType==='auto'?'success':'info'">
+ {{ scope.row.triggerType === 'auto' ? '瀹氭椂' : '鎵嬪姩' }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="status" label="鐘舵��" width="80">
+ <template slot-scope="scope">
+ <el-tag size="mini" :type="statusType(scope.row.status)">{{ statusLabel(scope.row.status) }}</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="createTime" label="鍒涘缓鏃堕棿" min-width="160">
+ <template slot-scope="scope">{{ formatTime(scope.row.createTime) }}</template>
+ </el-table-column>
+ <el-table-column prop="llmCallCount" label="LLM璋冪敤" width="80"></el-table-column>
+ <el-table-column prop="totalTokens" label="Token" width="90"></el-table-column>
+ <el-table-column prop="uploadStatus" label="涓婁紶" width="80">
+ <template slot-scope="scope">
+ <el-tag size="mini" :type="uploadType(scope.row.uploadStatus)">
+ {{ uploadLabel(scope.row.uploadStatus) }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="80" fixed="right">
+ <template slot-scope="scope">
+ <el-button size="mini" type="text" @click.stop="viewReport(scope.row)">璇︽儏</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ </div>
+
+ <div class="panel" v-if="selectedReport">
+ <div class="panel-head">
+ <div>
+ <div class="panel-title">鎶ュ憡璇︽儏 #{{ selectedReport.id }}</div>
+ <div style="color:#718299;font-size:12px;margin-top:2px;">
+ {{ periodLabel(selectedReport.periodType) }} 路 {{ formatTime(selectedReport.createTime) }}
+ </div>
+ </div>
+ <el-button size="mini" @click="selectedReport=null">鍏抽棴</el-button>
+ </div>
+ <div class="panel-body">
+ <div class="report-summary">
+ <h3>鍒嗘瀽鎶ュ憡</h3>
+ <pre>{{ selectedReport.summary || '鏆傛棤鎶ュ憡鍐呭' }}</pre>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
+
+<script type="text/javascript" src="../../static/vue/js/vue.min.js"></script>
+<script type="text/javascript" src="../../static/vue/element/element.js"></script>
+<script type="text/javascript" src="../../static/js/common.js" charset="utf-8"></script>
+<script>
+ new Vue({
+ el: '#app',
+ data: function() {
+ return {
+ headerIcon: getAiIconHtml(36, 36),
+ baseUrl: baseUrl,
+ enabled: false,
+ config: {},
+ enabledLoading: false,
+ triggerLoading: false,
+ triggerPeriod: '',
+ reportsLoading: false,
+ reports: [],
+ selectedReport: null
+ };
+ },
+ computed: {
+ cronDesc: function() {
+ var cron = this.config.cron || '0 0 1 * * ?';
+ if (cron === '0 0 1 * * ?') return '姣忓ぉ鍑屾櫒 1:00';
+ if (cron === '0 0 2 * * ?') return '姣忓ぉ鍑屾櫒 2:00';
+ if (cron === '0 30 0 * * ?') return '姣忓ぉ 0:30';
+ return cron;
+ }
+ },
+ mounted: function() {
+ this.loadConfig();
+ this.loadReports();
+ },
+ methods: {
+ authHeaders: function() {
+ return { 'token': localStorage.getItem('token') };
+ },
+ requestJson: function(url, options) {
+ var requestOptions = options || {};
+ requestOptions.headers = requestOptions.headers || this.authHeaders();
+ return fetch(url, requestOptions).then(function(response) {
+ return response.json();
+ });
+ },
+ loadConfig: function() {
+ var self = this;
+ this.requestJson(this.baseUrl + '/ai/dataAnalysis/enabled/auth')
+ .then(function(res) {
+ if (res && res.code === 200 && res.data) {
+ self.enabled = res.data.enabled === true;
+ self.config = res.data;
+ }
+ });
+ },
+ onEnabledChange: function(val) {
+ var self = this;
+ this.enabledLoading = true;
+ this.requestJson(this.baseUrl + '/ai/dataAnalysis/enabled/auth?enabled=' + (val ? '1' : '0'), { method: 'POST' })
+ .then(function(res) {
+ self.enabledLoading = false;
+ if (res && res.code === 200) {
+ self.enabled = res.data && res.data.enabled === true;
+ self.$message.success(self.enabled ? '宸插惎鐢ㄦ暟鎹垎鏋�' : '宸插叧闂暟鎹垎鏋�');
+ } else {
+ self.enabled = !val;
+ self.$message.error((res && res.msg) ? res.msg : '鎿嶄綔澶辫触');
+ }
+ })
+ .catch(function() {
+ self.enabledLoading = false;
+ self.enabled = !val;
+ self.$message.error('璇锋眰澶辫触');
+ });
+ },
+ triggerAnalysis: function(periodType) {
+ var self = this;
+ this.triggerLoading = true;
+ this.triggerPeriod = periodType;
+ this.requestJson(this.baseUrl + '/ai/dataAnalysis/trigger/auth?periodType=' + periodType, { method: 'POST' })
+ .then(function(res) {
+ self.triggerLoading = false;
+ self.triggerPeriod = '';
+ if (res && res.code === 200) {
+ var result = res.data;
+ if (result && result.skipped) {
+ self.$message.warning('宸茶烦杩�: ' + (result.reason || '鏈煡鍘熷洜'));
+ } else {
+ self.$message.success('鍒嗘瀽瀹屾垚');
+ self.loadReports();
+ }
+ } else {
+ self.$message.error((res && res.msg) ? res.msg : '瑙﹀彂澶辫触');
+ }
+ })
+ .catch(function() {
+ self.triggerLoading = false;
+ self.triggerPeriod = '';
+ self.$message.error('璇锋眰澶辫触');
+ });
+ },
+ loadReports: function() {
+ var self = this;
+ this.reportsLoading = true;
+ this.requestJson(this.baseUrl + '/ai/dataAnalysis/reports/auth?limit=20')
+ .then(function(res) {
+ self.reportsLoading = false;
+ if (res && res.code === 200 && Array.isArray(res.data)) {
+ self.reports = res.data;
+ }
+ })
+ .catch(function() {
+ self.reportsLoading = false;
+ });
+ },
+ viewReport: function(row) {
+ var self = this;
+ this.requestJson(this.baseUrl + '/ai/dataAnalysis/report/' + row.id + '/auth')
+ .then(function(res) {
+ if (res && res.code === 200 && res.data) {
+ self.selectedReport = res.data;
+ }
+ });
+ },
+ onReportClick: function(row) {
+ this.viewReport(row);
+ },
+ periodLabel: function(t) {
+ var map = { 'TODAY': '浠婃棩', 'YESTERDAY': '鏄ㄦ棩', 'THIS_WEEK': '鏈懆', 'THIS_MONTH': '鏈湀' };
+ return map[t] || t;
+ },
+ statusType: function(s) {
+ if (s === 'success') return 'success';
+ if (s === 'failed') return 'danger';
+ if (s === 'running') return 'warning';
+ return 'info';
+ },
+ statusLabel: function(s) {
+ var map = { 'pending': '寰呮墽琛�', 'running': '鎵ц涓�', 'success': '鎴愬姛', 'failed': '澶辫触' };
+ return map[s] || s;
+ },
+ uploadType: function(s) {
+ if (s === 'uploaded') return 'success';
+ if (s === 'failed') return 'danger';
+ return 'info';
+ },
+ uploadLabel: function(s) {
+ var map = { 'pending': '寰呬笂浼�', 'uploaded': '宸蹭笂浼�', 'failed': '澶辫触', 'skipped': '璺宠繃' };
+ return map[s] || s;
+ },
+ formatTime: function(t) {
+ if (!t) return '-';
+ var d = new Date(t);
+ if (isNaN(d.getTime())) return t;
+ var pad = function(n) { return n < 10 ? '0' + n : n; };
+ return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate())
+ + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds());
+ }
+ }
+ });
+</script>
+</body>
+</html>
diff --git a/src/main/webapp/views/dashboard/dashboard.html b/src/main/webapp/views/dashboard/dashboard.html
index 6e51310..5b8b45a 100644
--- a/src/main/webapp/views/dashboard/dashboard.html
+++ b/src/main/webapp/views/dashboard/dashboard.html
@@ -771,7 +771,7 @@
<div class="summary-card">
<div class="label">{{ i18n('dashboard.aiTokenTotalLabel', 'AI 绱 Tokens') }}</div>
<div class="value">{{ formatNumber(overview.aiTokenTotal) }}</div>
- <div class="desc">{{ i18n('dashboard.aiTokenTotalDesc', '鎸� AI 浼氳瘽绱缁熻') }}</div>
+ <div class="desc">{{ i18n('dashboard.aiTokenTotalDesc', '鎵�鏈� AI 璋冪敤绱娑堣��') }}</div>
</div>
<div class="summary-card">
<div class="label">{{ i18n('dashboard.aiCallTotalLabel', 'LLM 璋冪敤娆℃暟') }}</div>
--
Gitblit v1.9.1