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 &gt;= #{startTime} AND finish_time &lt; #{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 &gt;= #{startTime} AND finish_time &lt; #{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