From 0bc2775e4fd776086c508fd7640bc3d61835cf73 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期一, 27 四月 2026 15:57:54 +0800
Subject: [PATCH] feat(ai): 添加审批待办助手功能
---
src/main/java/com/ruoyi/ai/tools/ApproveTodoTools.java | 751 +++++++++++++++++++++++++++
src/main/java/com/ruoyi/ai/config/XiaozhiAgentConfig.java | 100 +++
src/main/java/com/ruoyi/ai/mongodbBean/ChatMessages.java | 27 +
src/main/java/com/ruoyi/ai/assistant/ApproveTodoIntentExecutor.java | 208 +++++++
src/main/java/com/ruoyi/ai/config/SeparateChatAssistantConfig.java | 29 +
src/main/resources/my-prompt-template3.txt | 3
src/main/resources/my-prompt-template.txt | 2
src/main/java/com/ruoyi/ai/assistant/SeparateChatAssistant.java | 35 +
src/main/java/com/ruoyi/ai/bean/ChatForm.java | 15
src/main/java/com/ruoyi/ai/config/ApproveTodoAgentConfig.java | 20
pom.xml | 156 +++-
src/main/java/com/ruoyi/ai/store/MongoChatMemoryStore.java | 51 +
src/main/java/com/ruoyi/ai/controller/XiaozhiController.java | 36 +
src/main/java/com/ruoyi/ai/assistant/Assistant.java | 16
src/main/resources/application-dev.yml | 51 +
src/main/resources/approve-todo-agent-prompt.txt | 13
src/main/java/com/ruoyi/ai/assistant/ApproveTodoAgent.java | 20
src/main/java/com/ruoyi/ai/config/EmbeddingStoreConfig.java | 36 +
src/main/resources/application.yml | 36 +
19 files changed, 1,530 insertions(+), 75 deletions(-)
diff --git a/pom.xml b/pom.xml
index 6008877..b645d4c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -22,11 +22,11 @@
<properties>
<maven.compiler.source>25</maven.compiler.source>
<maven.compiler.target>25</maven.compiler.target>
- <maven.compiler.release>25</maven.compiler.release>
- <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
- <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
- <java.version>25</java.version>
- <lombok.version>1.18.44</lombok.version>
+ <maven.compiler.release>25</maven.compiler.release>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+ <java.version>25</java.version>
+ <lombok.version>1.18.44</lombok.version>
<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
<pagehelper.spring.boot.starter.version>2.1.1</pagehelper.spring.boot.starter.version>
<fastjson.version>2.0.53</fastjson.version>
@@ -35,9 +35,9 @@
<bitwalker.version>1.21</bitwalker.version>
<jwt.version>0.13.0</jwt.version>
<kaptcha.version>2.3.3</kaptcha.version>
- <knife4j.version>4.5.0</knife4j.version>
- <springdoc.version>2.8.17</springdoc.version>
- <swagger.annotations.version>1.6.15</swagger.annotations.version>
+ <knife4j.version>4.5.0</knife4j.version>
+ <springdoc.version>2.8.17</springdoc.version>
+ <swagger.annotations.version>1.6.15</swagger.annotations.version>
<poi.version>5.2.3</poi.version>
<oshi.version>6.6.5</oshi.version>
<velocity.version>2.3</velocity.version>
@@ -53,21 +53,41 @@
<getui-sdk.version>1.0.7.0</getui-sdk.version>
<jsqlparser.version>4.9</jsqlparser.version>
<thumbnailator.version>0.4.20</thumbnailator.version>
+ <langchain4j.version>1.0.0-beta3</langchain4j.version>
</properties>
+
+ <dependencyManagement>
+ <dependencies>
+ <dependency>
+ <groupId>dev.langchain4j</groupId>
+ <artifactId>langchain4j-bom</artifactId>
+ <version>${langchain4j.version}</version>
+ <type>pom</type>
+ <scope>import</scope>
+ </dependency>
+ <dependency>
+ <groupId>dev.langchain4j</groupId>
+ <artifactId>langchain4j-community-bom</artifactId>
+ <version>${langchain4j.version}</version>
+ <type>pom</type>
+ <scope>import</scope>
+ </dependency>
+ </dependencies>
+ </dependencyManagement>
<dependencies>
<!-- ruoyi-springboot2 / swagger knife4j 閰嶇疆 -->
- <dependency>
- <groupId>com.github.xiaoymin</groupId>
- <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
- <version>${knife4j.version}</version>
- </dependency>
-
- <dependency>
- <groupId>org.springdoc</groupId>
- <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
- <version>${springdoc.version}</version>
- </dependency>
+ <dependency>
+ <groupId>com.github.xiaoymin</groupId>
+ <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
+ <version>${knife4j.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.springdoc</groupId>
+ <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
+ <version>${springdoc.version}</version>
+ </dependency>
<!-- SpringBoot 鏍稿績鍖� -->
<dependency>
@@ -113,10 +133,44 @@
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-data-mongodb</artifactId>
+ </dependency>
+
<!-- pool 瀵硅薄姹� -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>dev.langchain4j</groupId>
+ <artifactId>langchain4j-spring-boot-starter</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>dev.langchain4j</groupId>
+ <artifactId>langchain4j-pinecone</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>dev.langchain4j</groupId>
+ <artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>dev.langchain4j</groupId>
+ <artifactId>langchain4j-ollama-spring-boot-starter</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>dev.langchain4j</groupId>
+ <artifactId>langchain4j-community-dashscope-spring-boot-starter</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>dev.langchain4j</groupId>
+ <artifactId>langchain4j-mcp</artifactId>
</dependency>
<!-- Mysql椹卞姩鍖� -->
@@ -246,11 +300,11 @@
</dependency>
<!-- Swagger3渚濊禆 -->
- <dependency>
- <groupId>io.swagger</groupId>
- <artifactId>swagger-annotations</artifactId>
- <version>${swagger.annotations.version}</version>
- </dependency>
+ <dependency>
+ <groupId>io.swagger</groupId>
+ <artifactId>swagger-annotations</artifactId>
+ <version>${swagger.annotations.version}</version>
+ </dependency>
<!-- 闃叉杩涘叆swagger椤甸潰鎶ョ被鍨嬭浆鎹㈤敊璇紝鎺掗櫎3.0.0涓殑寮曠敤锛屾墜鍔ㄥ鍔�1.6.2鐗堟湰 -->
@@ -300,11 +354,11 @@
</exclusion>
</exclusions>
</dependency>
- <dependency>
- <groupId>org.projectlombok</groupId>
- <artifactId>lombok</artifactId>
- <version>${lombok.version}</version>
- </dependency>
+ <dependency>
+ <groupId>org.projectlombok</groupId>
+ <artifactId>lombok</artifactId>
+ <version>${lombok.version}</version>
+ </dependency>
<!-- minio -->
@@ -373,28 +427,28 @@
<build>
<finalName>${project.artifactId}</finalName>
- <plugins>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-compiler-plugin</artifactId>
- <version>3.14.1</version>
- <configuration>
- <release>${maven.compiler.release}</release>
- <proc>full</proc>
- <annotationProcessorPaths>
- <path>
- <groupId>org.projectlombok</groupId>
- <artifactId>lombok</artifactId>
- <version>${lombok.version}</version>
- </path>
- </annotationProcessorPaths>
- </configuration>
- </plugin>
- <plugin>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-maven-plugin</artifactId>
- </plugin>
- </plugins>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <version>3.14.1</version>
+ <configuration>
+ <release>${maven.compiler.release}</release>
+ <proc>full</proc>
+ <annotationProcessorPaths>
+ <path>
+ <groupId>org.projectlombok</groupId>
+ <artifactId>lombok</artifactId>
+ <version>${lombok.version}</version>
+ </path>
+ </annotationProcessorPaths>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-maven-plugin</artifactId>
+ </plugin>
+ </plugins>
</build>
<repositories>
diff --git a/src/main/java/com/ruoyi/ai/assistant/ApproveTodoAgent.java b/src/main/java/com/ruoyi/ai/assistant/ApproveTodoAgent.java
new file mode 100644
index 0000000..3b37909
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/assistant/ApproveTodoAgent.java
@@ -0,0 +1,20 @@
+package com.ruoyi.ai.assistant;
+
+import dev.langchain4j.service.MemoryId;
+import dev.langchain4j.service.SystemMessage;
+import dev.langchain4j.service.UserMessage;
+import dev.langchain4j.service.spring.AiService;
+import reactor.core.publisher.Flux;
+
+import static dev.langchain4j.service.spring.AiServiceWiringMode.EXPLICIT;
+
+@AiService(
+ wiringMode = EXPLICIT,
+ streamingChatModel = "qwenStreamingChatModel",
+ chatMemoryProvider = "chatMemoryProviderApproveTodo",
+ tools = "approveTodoTools")
+public interface ApproveTodoAgent {
+
+ @SystemMessage(fromResource = "approve-todo-agent-prompt.txt")
+ Flux<String> chat(@MemoryId String memoryId, @UserMessage String userMessage);
+}
diff --git a/src/main/java/com/ruoyi/ai/assistant/ApproveTodoIntentExecutor.java b/src/main/java/com/ruoyi/ai/assistant/ApproveTodoIntentExecutor.java
new file mode 100644
index 0000000..6e1a948
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/assistant/ApproveTodoIntentExecutor.java
@@ -0,0 +1,208 @@
+package com.ruoyi.ai.assistant;
+
+import com.alibaba.fastjson2.JSON;
+import com.ruoyi.ai.tools.ApproveTodoTools;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+
+import java.math.BigDecimal;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+@Component
+public class ApproveTodoIntentExecutor {
+
+ private static final Pattern APPROVE_ID_PATTERN = Pattern.compile("\\b[A-Za-z]*\\d{8,}\\b");
+ private static final Pattern LIMIT_PATTERN = Pattern.compile("(鍓峾鏈�杩�)?(\\d{1,2})鏉�");
+
+ private final ApproveTodoTools approveTodoTools;
+
+ public ApproveTodoIntentExecutor(ApproveTodoTools approveTodoTools) {
+ this.approveTodoTools = approveTodoTools;
+ }
+
+ public String tryExecute(String message) {
+ if (!StringUtils.hasText(message)) {
+ return null;
+ }
+
+ String text = message.trim();
+ String approveId = extractApproveId(text);
+
+ if (containsAny(text, "缁熻", "鍒嗘瀽", "鍥捐〃", "瓒嬪娍", "鍗犳瘮")) {
+ return approveTodoTools.getTodoStats();
+ }
+ if (containsAny(text, "娴佽浆", "杩涘害", "鑺傜偣", "鏃ュ織")) {
+ return StringUtils.hasText(approveId)
+ ? approveTodoTools.getTodoProgress(approveId)
+ : missingApproveId("todo_progress", "鏌ヨ瀹℃壒杩涘害闇�瑕佹彁渚涙祦绋嬬紪鍙枫��");
+ }
+ if (containsAny(text, "璇︽儏", "鏄庣粏") && !containsAny(text, "鍒楄〃")) {
+ return StringUtils.hasText(approveId)
+ ? approveTodoTools.getTodoDetail(approveId)
+ : missingApproveId("todo_detail", "鏌ヨ瀹℃壒璇︽儏闇�瑕佹彁渚涙祦绋嬬紪鍙枫��");
+ }
+ if (containsAny(text, "鍙栨秷瀹℃牳", "鎾ら攢瀹℃牳", "鍥為��瀹℃牳")) {
+ return StringUtils.hasText(approveId)
+ ? approveTodoTools.cancelReviewTodo(approveId, extractTail(text, "鍘熷洜"))
+ : missingApproveId("cancel_review_action", "鍙栨秷瀹℃牳闇�瑕佹彁渚涙祦绋嬬紪鍙枫��");
+ }
+ if (containsAny(text, "鍒犻櫎")) {
+ return StringUtils.hasText(approveId)
+ ? approveTodoTools.deleteTodo(approveId)
+ : missingApproveId("delete_action", "鍒犻櫎瀹℃壒鍗曢渶瑕佹彁渚涙祦绋嬬紪鍙枫��");
+ }
+ if (containsAny(text, "椹冲洖", "鎷掔粷")) {
+ return StringUtils.hasText(approveId)
+ ? approveTodoTools.reviewTodo(approveId, "reject", extractTail(text, "鍘熷洜"))
+ : missingApproveId("review_action", "椹冲洖瀹℃壒闇�瑕佹彁渚涙祦绋嬬紪鍙枫��");
+ }
+ if (containsAny(text, "瀹℃牳閫氳繃", "瀹℃壒閫氳繃", "閫氳繃瀹℃壒", "鍚屾剰瀹℃壒", "瀹℃壒鍚屾剰")) {
+ return StringUtils.hasText(approveId)
+ ? approveTodoTools.reviewTodo(approveId, "approve", extractTail(text, "澶囨敞"))
+ : missingApproveId("review_action", "瀹℃壒閫氳繃闇�瑕佹彁渚涙祦绋嬬紪鍙枫��");
+ }
+ if (StringUtils.hasText(approveId)
+ && containsAny(text, "閫氳繃", "鍚屾剰")
+ && !containsAny(text, "鏈�氳繃", "閫氳繃鐜�", "瀹℃壒閫氳繃鐜�", "瀹℃牳閫氳繃鐜�")) {
+ return approveTodoTools.reviewTodo(approveId, "approve", extractTail(text, "澶囨敞"));
+ }
+ if (containsAny(text, "淇敼")) {
+ return StringUtils.hasText(approveId)
+ ? approveTodoTools.updateTodo(
+ approveId,
+ extractValue(text, "鏍囬"),
+ extractDateValue(text, "寮�濮嬫棩鏈�"),
+ extractDateValue(text, "缁撴潫鏃ユ湡"),
+ extractBigDecimalValue(text, "閲戦"),
+ extractValue(text, "鍦扮偣"),
+ extractIntegerValue(text, "绫诲瀷"),
+ extractValue(text, "澶囨敞"))
+ : missingApproveId("update_action", "淇敼瀹℃壒鍗曢渶瑕佹彁渚涙祦绋嬬紪鍙枫��");
+ }
+ if (containsAny(text, "鍒楄〃", "寰呭姙", "鏌ヨ瀹℃壒")) {
+ return approveTodoTools.listTodos(
+ extractStatus(text),
+ extractApproveType(text),
+ extractKeyword(text),
+ extractLimit(text));
+ }
+ return null;
+ }
+
+ private boolean containsAny(String text, String... keywords) {
+ for (String keyword : keywords) {
+ if (text.contains(keyword)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private String extractApproveId(String text) {
+ Matcher matcher = APPROVE_ID_PATTERN.matcher(text);
+ return matcher.find() ? matcher.group() : null;
+ }
+
+ private Integer extractLimit(String text) {
+ Matcher matcher = LIMIT_PATTERN.matcher(text);
+ return matcher.find() ? Integer.parseInt(matcher.group(2)) : 10;
+ }
+
+ private String extractStatus(String text) {
+ if (containsAny(text, "寰呭鏍�", "寰呭鎵�")) {
+ return "pending";
+ }
+ if (containsAny(text, "瀹℃牳涓�")) {
+ return "processing";
+ }
+ if (containsAny(text, "宸查�氳繃", "瀹℃牳瀹屾垚")) {
+ return "approved";
+ }
+ if (containsAny(text, "鏈�氳繃", "椹冲洖")) {
+ return "rejected";
+ }
+ if (containsAny(text, "閲嶆柊鎻愪氦")) {
+ return "resubmitted";
+ }
+ return "all";
+ }
+
+ private Integer extractApproveType(String text) {
+ if (text.contains("鍏嚭")) {
+ return 1;
+ }
+ if (text.contains("璇峰亣")) {
+ return 2;
+ }
+ if (text.contains("鍑哄樊")) {
+ return 3;
+ }
+ if (text.contains("鎶ラ攢")) {
+ return 4;
+ }
+ if (text.contains("閲囪喘")) {
+ return 5;
+ }
+ if (text.contains("鎶ヤ环")) {
+ return 6;
+ }
+ if (text.contains("鍙戣揣")) {
+ return 7;
+ }
+ if (text.contains("鍗遍櫓浣滀笟")) {
+ return 8;
+ }
+ return null;
+ }
+
+ private String extractKeyword(String text) {
+ String cleaned = text
+ .replace("鏌ヨ", "")
+ .replace("瀹℃壒", "")
+ .replace("寰呭姙", "")
+ .replace("鍒楄〃", "")
+ .replace("鍓�10鏉�", "")
+ .replace("鍓�20鏉�", "")
+ .trim();
+ return cleaned.length() >= 2 ? cleaned : null;
+ }
+
+ private String extractValue(String text, String fieldName) {
+ Matcher matcher = Pattern.compile(fieldName + "(鏀逛负|淇敼涓簗涓簗鏄�)([^锛屻��,锛�;\\s]+)").matcher(text);
+ return matcher.find() ? matcher.group(2) : null;
+ }
+
+ private String extractDateValue(String text, String fieldName) {
+ Matcher matcher = Pattern.compile(fieldName + "(鏀逛负|淇敼涓簗涓簗鏄�)(\\d{4}-\\d{2}-\\d{2})").matcher(text);
+ return matcher.find() ? matcher.group(2) : null;
+ }
+
+ private Integer extractIntegerValue(String text, String fieldName) {
+ Matcher matcher = Pattern.compile(fieldName + "(鏀逛负|淇敼涓簗涓簗鏄�)(\\d{1,2})").matcher(text);
+ return matcher.find() ? Integer.parseInt(matcher.group(2)) : null;
+ }
+
+ private BigDecimal extractBigDecimalValue(String text, String fieldName) {
+ Matcher matcher = Pattern.compile(fieldName + "(鏀逛负|淇敼涓簗涓簗鏄�)(\\d+(\\.\\d+)?)").matcher(text);
+ return matcher.find() ? new BigDecimal(matcher.group(2)) : null;
+ }
+
+ private String extractTail(String text, String key) {
+ Matcher matcher = Pattern.compile(key + "(鏄瘄涓簗锛殀:)?(.+)").matcher(text);
+ return matcher.find() ? matcher.group(2).trim() : null;
+ }
+
+ private String missingApproveId(String type, String description) {
+ Map<String, Object> result = new LinkedHashMap<>();
+ result.put("success", false);
+ result.put("type", type);
+ result.put("description", description);
+ result.put("summary", Map.of());
+ result.put("data", Map.of());
+ result.put("charts", Map.of());
+ return JSON.toJSONString(result);
+ }
+}
diff --git a/src/main/java/com/ruoyi/ai/assistant/Assistant.java b/src/main/java/com/ruoyi/ai/assistant/Assistant.java
new file mode 100644
index 0000000..f0718c0
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/assistant/Assistant.java
@@ -0,0 +1,16 @@
+package com.ruoyi.ai.assistant;
+
+import dev.langchain4j.service.spring.AiService;
+
+import static dev.langchain4j.service.spring.AiServiceWiringMode.EXPLICIT;
+
+/**
+ * @author :yys
+ * @date : 2025/5/2 18:26
+ */
+//鍥犱负鎴戜滑鍦ㄩ厤缃枃浠朵腑鍚屾椂閰嶇疆浜嗗涓ぇ璇█妯″瀷锛屾墍浠ラ渶瑕佸湪杩欓噷鏄庣‘鎸囧畾锛圗XPLICIT锛夋ā鍨嬬殑beanName
+
+@AiService(wiringMode = EXPLICIT, chatModel = "qwenChatModel")
+public interface Assistant {
+ String chat(String userMessage);
+}
diff --git a/src/main/java/com/ruoyi/ai/assistant/SeparateChatAssistant.java b/src/main/java/com/ruoyi/ai/assistant/SeparateChatAssistant.java
new file mode 100644
index 0000000..fa36ad5
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/assistant/SeparateChatAssistant.java
@@ -0,0 +1,35 @@
+package com.ruoyi.ai.assistant;
+
+import dev.langchain4j.service.MemoryId;
+import dev.langchain4j.service.SystemMessage;
+import dev.langchain4j.service.UserMessage;
+import dev.langchain4j.service.V;
+import dev.langchain4j.service.spring.AiService;
+import dev.langchain4j.service.spring.AiServiceWiringMode;
+
+/**
+ * @author :yys
+ * @date : 2025/5/2 19:35
+ */
+@AiService(
+ wiringMode = AiServiceWiringMode.EXPLICIT,
+ chatModel = "qwenChatModel",
+ chatMemoryProvider = "chatMemoryProvider"
+)
+public interface SeparateChatAssistant {
+
+ @SystemMessage(fromResource = "my-prompt-template.txt")//绯荤粺娑堟伅鎻愮ず璇�
+ String chat(@MemoryId String memoryId, @UserMessage String userMessage);
+
+ @UserMessage("浣犳槸鎴戠殑濂芥湅鍙嬶紝璇风敤绮よ鍥炵瓟闂銆倇{message}}")
+ String chat2(@MemoryId String memoryId, @V("message") String userMessage);
+
+ @SystemMessage(fromResource = "my-prompt-template3.txt")
+ String chat3(
+ @MemoryId String memoryId,
+ @UserMessage String userMessage,
+ @V("username") String username,
+ @V("age") int age
+ );
+
+}
diff --git a/src/main/java/com/ruoyi/ai/bean/ChatForm.java b/src/main/java/com/ruoyi/ai/bean/ChatForm.java
new file mode 100644
index 0000000..60d1a6e
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/bean/ChatForm.java
@@ -0,0 +1,15 @@
+package com.ruoyi.ai.bean;
+
+import lombok.Data;
+
+/**
+ * @author :yys
+ * @date : 2025/5/2 20:03
+ */
+@Data
+public class ChatForm {
+
+ private String memoryId;//瀵硅瘽id
+ private String message;//鐢ㄦ埛闂
+
+}
diff --git a/src/main/java/com/ruoyi/ai/config/ApproveTodoAgentConfig.java b/src/main/java/com/ruoyi/ai/config/ApproveTodoAgentConfig.java
new file mode 100644
index 0000000..61cc0dd
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/config/ApproveTodoAgentConfig.java
@@ -0,0 +1,20 @@
+package com.ruoyi.ai.config;
+
+import com.ruoyi.ai.store.MongoChatMemoryStore;
+import dev.langchain4j.memory.chat.ChatMemoryProvider;
+import dev.langchain4j.memory.chat.MessageWindowChatMemory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class ApproveTodoAgentConfig {
+
+ @Bean
+ ChatMemoryProvider chatMemoryProviderApproveTodo(MongoChatMemoryStore mongoChatMemoryStore) {
+ return memoryId -> MessageWindowChatMemory.builder()
+ .id(memoryId)
+ .maxMessages(30)
+ .chatMemoryStore(mongoChatMemoryStore)
+ .build();
+ }
+}
diff --git a/src/main/java/com/ruoyi/ai/config/EmbeddingStoreConfig.java b/src/main/java/com/ruoyi/ai/config/EmbeddingStoreConfig.java
new file mode 100644
index 0000000..ca5156d
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/config/EmbeddingStoreConfig.java
@@ -0,0 +1,36 @@
+package com.ruoyi.ai.config;
+
+import dev.langchain4j.data.segment.TextSegment;
+import dev.langchain4j.model.embedding.EmbeddingModel;
+import dev.langchain4j.store.embedding.EmbeddingStore;
+import dev.langchain4j.store.embedding.pinecone.PineconeEmbeddingStore;
+import dev.langchain4j.store.embedding.pinecone.PineconeServerlessIndexConfig;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @author :yys
+ * @date : 2025/5/2 21:07
+ */
+@Configuration
+public class EmbeddingStoreConfig {
+
+ @Autowired
+ private EmbeddingModel embeddingModel;
+
+ @Bean
+ public EmbeddingStore<TextSegment> embeddingStore() {
+ //鍒涘缓鍚戦噺瀛樺偍
+ return PineconeEmbeddingStore.builder()
+ .apiKey("pcsk_4SJLnh_tNB3wSLJU8tc4E5P28PcXX8eCLdURqZpVhg1FMV8CRYxjneWdzqRdB5Ftqooi9")
+ .index("xiaozhi-index")//濡傛灉鎸囧畾鐨勭储寮曚笉瀛樺湪锛屽皢鍒涘缓涓�涓柊鐨勭储寮�
+ .nameSpace("xiaozhi-namespace") //濡傛灉鎸囧畾鐨勫悕绉扮┖闂翠笉瀛樺湪锛屽皢鍒涘缓涓�涓柊鐨勫悕绉� 绌洪棿
+ .createIndex(PineconeServerlessIndexConfig.builder()
+ .cloud("AWS") //鎸囧畾绱㈠紩閮ㄧ讲鍦� AWS 浜戞湇鍔′笂銆�
+ .region("us-east-1") //鎸囧畾绱㈠紩鎵�鍦ㄧ殑 AWS 鍖哄煙涓� us-east-1銆�
+ .dimension(embeddingModel.dimension()) //鎸囧畾绱㈠紩鐨勫悜閲忕淮搴︼紝璇ョ淮搴︿笌 embeddedModel 鐢熸垚鐨勫悜閲忕淮搴︾浉鍚屻��
+ .build())
+ .build();
+ }
+}
diff --git a/src/main/java/com/ruoyi/ai/config/SeparateChatAssistantConfig.java b/src/main/java/com/ruoyi/ai/config/SeparateChatAssistantConfig.java
new file mode 100644
index 0000000..1927843
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/config/SeparateChatAssistantConfig.java
@@ -0,0 +1,29 @@
+package com.ruoyi.ai.config;
+
+import com.ruoyi.ai.store.MongoChatMemoryStore;
+import dev.langchain4j.memory.chat.ChatMemoryProvider;
+import dev.langchain4j.memory.chat.MessageWindowChatMemory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @author :yys
+ * @date : 2025/5/2 19:20
+ */
+@Configuration
+public class SeparateChatAssistantConfig {
+ //娉ㄥ叆鎸佷箙鍖栧璞�
+ @Autowired
+ private MongoChatMemoryStore mongoChatMemoryStore;
+
+ @Bean
+ ChatMemoryProvider chatMemoryProvider() {
+ return memoryId -> MessageWindowChatMemory.builder()
+ .id(memoryId)
+ .maxMessages(10)
+ .chatMemoryStore(mongoChatMemoryStore)//閰嶇疆鎸佷箙鍖栧璞�
+ .build();
+ }
+
+}
diff --git a/src/main/java/com/ruoyi/ai/config/XiaozhiAgentConfig.java b/src/main/java/com/ruoyi/ai/config/XiaozhiAgentConfig.java
new file mode 100644
index 0000000..58be3a6
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/config/XiaozhiAgentConfig.java
@@ -0,0 +1,100 @@
+package com.ruoyi.ai.config;
+
+import com.ruoyi.ai.store.MongoChatMemoryStore;
+import dev.langchain4j.data.document.Document;
+import dev.langchain4j.data.document.loader.FileSystemDocumentLoader;
+import dev.langchain4j.data.segment.TextSegment;
+import dev.langchain4j.memory.chat.ChatMemoryProvider;
+import dev.langchain4j.memory.chat.MessageWindowChatMemory;
+import dev.langchain4j.model.embedding.EmbeddingModel;
+import dev.langchain4j.rag.content.retriever.ContentRetriever;
+import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever;
+import dev.langchain4j.store.embedding.EmbeddingStore;
+import dev.langchain4j.store.embedding.EmbeddingStoreIngestor;
+import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author :yys
+ * @date : 2025/5/2 20:01
+ */
+@Configuration
+public class XiaozhiAgentConfig {
+
+ @Autowired
+ private MongoChatMemoryStore mongoChatMemoryStore;
+
+ @Autowired
+ private EmbeddingStore embeddingStore;
+ @Autowired
+ private EmbeddingModel embeddingModel;
+
+ @Value("${knowledge.one}")
+ private String one;
+//
+// @Value("${knowledge.two}")
+// private String two;
+//
+// @Value("${knowledge.three}")
+// private String three;
+
+ @Bean
+ ChatMemoryProvider chatMemoryProviderXiaozhi() {
+ return memoryId -> MessageWindowChatMemory.builder()
+ .id(memoryId)
+ .maxMessages(20)
+ .chatMemoryStore(mongoChatMemoryStore)
+ .build();
+ }
+
+ @Bean
+ ContentRetriever contentRetrieverXiaozhi() {
+ //浣跨敤FileSystemDocumentLoader璇诲彇鎸囧畾鐩綍涓嬬殑鐭ヨ瘑搴撴枃妗�
+ //骞朵娇鐢ㄩ粯璁ょ殑鏂囨。瑙f瀽鍣ㄥ鏂囨。杩涜瑙f瀽
+ Document document1 = FileSystemDocumentLoader.loadDocument(one);
+// Document document2 = FileSystemDocumentLoader.loadDocument(two);
+// Document document3 = FileSystemDocumentLoader.loadDocument(three);
+// List<Document> documents = Arrays.asList(document1, document2, document3);
+
+ List<Document> documents = Collections.singletonList(document1);
+// 2. 灏嗘暟鎹簱鏁版嵁杞负LangChain4j鐨凞ocument瀵硅薄
+// List<Document> documents = new ArrayList<>();
+
+ //浣跨敤鍐呭瓨鍚戦噺瀛樺偍
+ InMemoryEmbeddingStore<TextSegment> inMemoryEmbeddingStore = new InMemoryEmbeddingStore<>();
+ //浣跨敤榛樿鐨勬枃妗e垎鍓插櫒
+ EmbeddingStoreIngestor.builder()
+ .embeddingModel(embeddingModel)
+ .embeddingStore(inMemoryEmbeddingStore)
+ .build()
+ .ingest(documents);
+ //浠庡祵鍏ュ瓨鍌紙EmbeddingStore锛夐噷妫�绱㈠拰鏌ヨ鍐呭鐩稿叧鐨勪俊鎭�
+ return EmbeddingStoreContentRetriever.builder()
+ .embeddingModel(embeddingModel)
+ .embeddingStore(inMemoryEmbeddingStore)
+ .build();
+ }
+
+ @Bean
+ ContentRetriever contentRetrieverXiaozhiPincone() {
+ // 鍒涘缓涓�涓� EmbeddingStoreContentRetriever 瀵硅薄锛岀敤浜庝粠宓屽叆瀛樺偍涓绱㈠唴瀹�
+ return EmbeddingStoreContentRetriever
+ .builder()
+ // 璁剧疆鐢ㄤ簬鐢熸垚宓屽叆鍚戦噺鐨勫祵鍏ユā鍨�
+ .embeddingModel(embeddingModel)
+ // 鎸囧畾瑕佷娇鐢ㄧ殑宓屽叆瀛樺偍
+ .embeddingStore(embeddingStore)
+ // 璁剧疆鏈�澶ф绱㈢粨鏋滄暟閲忥紝杩欓噷琛ㄧず鏈�澶氳繑鍥� 1 鏉″尮閰嶇粨鏋�
+ .maxResults(1)
+ // 璁剧疆鏈�灏忓緱鍒嗛槇鍊硷紝鍙湁寰楀垎澶т簬绛変簬 0.8 鐨勭粨鏋滄墠浼氳杩斿洖
+ .minScore(0.8)
+ // 鏋勫缓鏈�缁堢殑 EmbeddingStoreContentRetriever 瀹炰緥
+ .build();
+ }
+}
diff --git a/src/main/java/com/ruoyi/ai/controller/XiaozhiController.java b/src/main/java/com/ruoyi/ai/controller/XiaozhiController.java
new file mode 100644
index 0000000..e1223cc
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/controller/XiaozhiController.java
@@ -0,0 +1,36 @@
+package com.ruoyi.ai.controller;
+
+import com.ruoyi.ai.assistant.ApproveTodoAgent;
+import com.ruoyi.ai.assistant.ApproveTodoIntentExecutor;
+import com.ruoyi.ai.bean.ChatForm;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import reactor.core.publisher.Flux;
+
+@Tag(name = "鍗忓悓鍔炲叕鍔╂墜")
+@RestController
+@RequestMapping("/xiaozhi")
+public class XiaozhiController {
+
+ private final ApproveTodoAgent approveTodoAgent;
+ private final ApproveTodoIntentExecutor approveTodoIntentExecutor;
+
+ public XiaozhiController(ApproveTodoAgent approveTodoAgent, ApproveTodoIntentExecutor approveTodoIntentExecutor) {
+ this.approveTodoAgent = approveTodoAgent;
+ this.approveTodoIntentExecutor = approveTodoIntentExecutor;
+ }
+
+ @Operation(summary = "瀵硅瘽")
+ @PostMapping(value = "/chat", produces = "text/stream;charset=utf-8")
+ public Flux<String> chat(@RequestBody ChatForm chatForm) {
+ String directResponse = approveTodoIntentExecutor.tryExecute(chatForm.getMessage());
+ if (directResponse != null) {
+ return Flux.just(directResponse);
+ }
+ return approveTodoAgent.chat(chatForm.getMemoryId(), chatForm.getMessage());
+ }
+}
diff --git a/src/main/java/com/ruoyi/ai/mongodbBean/ChatMessages.java b/src/main/java/com/ruoyi/ai/mongodbBean/ChatMessages.java
new file mode 100644
index 0000000..90f1e02
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/mongodbBean/ChatMessages.java
@@ -0,0 +1,27 @@
+package com.ruoyi.ai.mongodbBean;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.bson.types.ObjectId;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.mongodb.core.mapping.Document;
+
+/**
+ * @author :yys
+ * @date : 2025/5/2 19:13
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Document("chat_messages")
+public class ChatMessages {
+ //鍞竴鏍囪瘑锛屾槧灏勫埌 MongoDB 鏂囨。鐨� _id 瀛楁
+ @Id
+ private ObjectId id;
+
+ private String messageId;
+
+ private String content; //瀛樺偍褰撳墠鑱婂ぉ璁板綍鍒楄〃鐨刯son瀛楃涓�
+
+}
diff --git a/src/main/java/com/ruoyi/ai/store/MongoChatMemoryStore.java b/src/main/java/com/ruoyi/ai/store/MongoChatMemoryStore.java
new file mode 100644
index 0000000..e94bf73
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/store/MongoChatMemoryStore.java
@@ -0,0 +1,51 @@
+package com.ruoyi.ai.store;
+
+import com.ruoyi.ai.mongodbBean.ChatMessages;
+import dev.langchain4j.data.message.ChatMessage;
+import dev.langchain4j.data.message.ChatMessageDeserializer;
+import dev.langchain4j.data.message.ChatMessageSerializer;
+import dev.langchain4j.store.memory.chat.ChatMemoryStore;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.mongodb.core.MongoTemplate;
+import org.springframework.data.mongodb.core.query.Criteria;
+import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.data.mongodb.core.query.Update;
+import org.springframework.stereotype.Component;
+
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * @author :yys
+ * @date : 2025/5/2 19:18
+ */
+@Component
+public class MongoChatMemoryStore implements ChatMemoryStore {
+
+ @Autowired
+ private MongoTemplate mongoTemplate;
+
+ @Override
+ public List<ChatMessage> getMessages(Object memoryId) {
+ Criteria criteria = Criteria.where("memoryId").is(memoryId);
+ Query query = new Query(criteria);
+ ChatMessages chatMessages = mongoTemplate.findOne(query, ChatMessages.class);
+ if(chatMessages == null) return new LinkedList<>();
+ return ChatMessageDeserializer.messagesFromJson(chatMessages.getContent());
+ }
+ @Override
+ public void updateMessages(Object memoryId, List<ChatMessage> messages) {
+ Criteria criteria = Criteria.where("memoryId").is(memoryId);
+ Query query = new Query(criteria);
+ Update update = new Update();
+ update.set("content", ChatMessageSerializer.messagesToJson(messages));
+ //鏍规嵁query鏉′欢鑳芥煡璇㈠嚭鏂囨。锛屽垯淇敼鏂囨。锛涘惁鍒欐柊澧炴枃妗�
+ mongoTemplate.upsert(query, update, ChatMessages.class);
+ }
+ @Override
+ public void deleteMessages(Object memoryId) {
+ Criteria criteria = Criteria.where("memoryId").is(memoryId);
+ Query query = new Query(criteria);
+ mongoTemplate.remove(query, ChatMessages.class);
+ }
+}
diff --git a/src/main/java/com/ruoyi/ai/tools/ApproveTodoTools.java b/src/main/java/com/ruoyi/ai/tools/ApproveTodoTools.java
new file mode 100644
index 0000000..431b848
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/tools/ApproveTodoTools.java
@@ -0,0 +1,751 @@
+package com.ruoyi.ai.tools;
+
+import com.alibaba.fastjson2.JSON;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.ruoyi.approve.mapper.ApproveLogMapper;
+import com.ruoyi.approve.mapper.ApproveNodeMapper;
+import com.ruoyi.approve.mapper.ApproveProcessMapper;
+import com.ruoyi.approve.pojo.ApproveLog;
+import com.ruoyi.approve.pojo.ApproveNode;
+import com.ruoyi.approve.pojo.ApproveProcess;
+import dev.langchain4j.agent.tool.P;
+import dev.langchain4j.agent.tool.Tool;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+import java.math.BigDecimal;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Component
+public class ApproveTodoTools {
+
+ private static final int DEFAULT_LIMIT = 10;
+ private static final int MAX_LIMIT = 20;
+ private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");
+ private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+ private final ApproveProcessMapper approveProcessMapper;
+ private final ApproveNodeMapper approveNodeMapper;
+ private final ApproveLogMapper approveLogMapper;
+
+ public ApproveTodoTools(
+ ApproveProcessMapper approveProcessMapper,
+ ApproveNodeMapper approveNodeMapper,
+ ApproveLogMapper approveLogMapper) {
+ this.approveProcessMapper = approveProcessMapper;
+ this.approveNodeMapper = approveNodeMapper;
+ this.approveLogMapper = approveLogMapper;
+ }
+
+ @Tool(name = "鏌ヨ瀹℃壒寰呭姙鍒楄〃", value = "鏌ヨ瀹℃壒寰呭姙锛屾敮鎸佹寜鐘舵�併�佺被鍨嬨�佸叧閿瓧杩囨护锛岃繑鍥� Markdown 琛ㄦ牸銆�")
+ public String listTodos(
+ @P(value = "瀹℃壒鐘舵�侊紝鍙�夊�硷細all銆乸ending銆乸rocessing銆乤pproved銆乺ejected銆乺esubmitted", required = false) String status,
+ @P(value = "瀹℃壒绫诲瀷缂栧彿锛屽彲涓嶄紶", required = false) Integer approveType,
+ @P(value = "鍏抽敭瀛楋紝鍙尮閰嶆祦绋嬬紪鍙枫�佹爣棰樸�佺敵璇蜂汉銆佸綋鍓嶅鎵逛汉", required = false) String keyword,
+ @P(value = "杩斿洖鏉℃暟锛岄粯璁�10锛屾渶澶�20", required = false) Integer limit) {
+
+ LambdaQueryWrapper<ApproveProcess> wrapper = new LambdaQueryWrapper<>();
+ wrapper.eq(ApproveProcess::getApproveDelete, 0);
+
+ Integer statusCode = parseStatus(status);
+ if (statusCode != null) {
+ wrapper.eq(ApproveProcess::getApproveStatus, statusCode);
+ }
+ if (approveType != null) {
+ wrapper.eq(ApproveProcess::getApproveType, approveType);
+ }
+ if (StringUtils.hasText(keyword)) {
+ wrapper.and(w -> w.like(ApproveProcess::getApproveId, keyword)
+ .or().like(ApproveProcess::getApproveReason, keyword)
+ .or().like(ApproveProcess::getApproveUserName, keyword)
+ .or().like(ApproveProcess::getApproveUserCurrentName, keyword));
+ }
+
+ wrapper.orderByDesc(ApproveProcess::getCreateTime);
+ wrapper.last("limit " + normalizeLimit(limit));
+
+ List<ApproveProcess> processes = approveProcessMapper.selectList(wrapper);
+ if (processes == null || processes.isEmpty()) {
+ return jsonResponse(
+ true,
+ "todo_list",
+ "鏈煡璇㈠埌绗﹀悎鏉′欢鐨勫鎵瑰緟鍔炪��",
+ Map.of("count", 0),
+ Map.of(
+ "columns", List.of("approveId", "approveType", "approveUserName", "approveUserCurrentName", "approveReason", "approveStatus", "createTime"),
+ "items", List.of()
+ ),
+ Map.of()
+ );
+ }
+
+ List<Map<String, Object>> items = processes.stream().map(process -> {
+ Map<String, Object> item = new LinkedHashMap<>();
+ item.put("approveId", process.getApproveId());
+ item.put("approveType", approveTypeName(process.getApproveType()));
+ item.put("approveUserName", process.getApproveUserName());
+ item.put("approveUserCurrentName", process.getApproveUserCurrentName());
+ item.put("approveReason", process.getApproveReason());
+ item.put("approveStatus", approveStatusName(process.getApproveStatus()));
+ item.put("createTime", formatDateTime(process.getCreateTime()));
+ return item;
+ }).collect(Collectors.toList());
+
+ return jsonResponse(
+ true,
+ "todo_list",
+ "宸茶繑鍥炲鎵瑰緟鍔炲垪琛紝鍙洿鎺ユ覆鏌撹〃鏍兼垨鍗$墖銆�",
+ Map.of(
+ "count", items.size(),
+ "statusFilter", status == null ? "all" : status,
+ "approveType", approveType == null ? "" : approveType,
+ "keyword", keyword == null ? "" : keyword
+ ),
+ Map.of(
+ "columns", List.of("approveId", "approveType", "approveUserName", "approveUserCurrentName", "approveReason", "approveStatus", "createTime"),
+ "items", items
+ ),
+ Map.of()
+ );
+ }
+
+ @Tool(name = "鏌ヨ瀹℃壒寰呭姙璇︽儏", value = "鏍规嵁娴佺▼缂栧彿鏌ヨ鍗曟潯瀹℃壒寰呭姙璇︽儏锛岃繑鍥炵粨鏋勫寲鏂囨湰銆�")
+ public String getTodoDetail(@P("娴佺▼缂栧彿 approveId") String approveId) {
+ ApproveProcess process = getProcessByApproveId(approveId);
+ if (process == null) {
+ return "鏈壘鍒板搴旂殑瀹℃壒娴佺▼锛岃纭娴佺▼缂栧彿鏄惁姝g‘銆�";
+ }
+
+ StringJoiner detail = new StringJoiner("\n");
+ detail.add("瀹℃壒璇︽儏");
+ detail.add("娴佺▼缂栧彿: " + safe(process.getApproveId()));
+ detail.add("瀹℃壒绫诲瀷: " + approveTypeName(process.getApproveType()));
+ detail.add("鐢宠浜�: " + safe(process.getApproveUserName()));
+ detail.add("鐢宠閮ㄩ棬: " + safe(process.getApproveDeptName()));
+ detail.add("褰撳墠瀹℃壒浜�: " + safe(process.getApproveUserCurrentName()));
+ detail.add("鏍囬: " + safe(process.getApproveReason()));
+ detail.add("鐘舵��: " + approveStatusName(process.getApproveStatus()));
+ detail.add("鐢宠鏃ユ湡: " + formatDate(process.getApproveTime()));
+ detail.add("寮�濮嬫棩鏈�: " + formatDate(process.getStartDate()));
+ detail.add("缁撴潫鏃ユ湡: " + formatDate(process.getEndDate()));
+ detail.add("鍦扮偣: " + safe(process.getLocation()));
+ detail.add("閲戦: " + (process.getPrice() == null ? "" : process.getPrice().toPlainString()));
+ detail.add("澶囨敞: " + safe(process.getApproveRemark()));
+ detail.add("鍒涘缓鏃堕棿: " + formatDateTime(process.getCreateTime()));
+ return detail.toString();
+ }
+
+ @Tool(name = "鏌ヨ瀹℃壒娴佽浆璁板綍", value = "鏍规嵁娴佺▼缂栧彿鏌ヨ瀹℃壒鑺傜偣鍜屽鎵规棩蹇楋紝閫傚悎鍥炵瓟杩涘害銆佸崱鍦ㄥ摢涓妭鐐广�佽皝澶勭悊杩囥��")
+ public String getTodoProgress(@P("娴佺▼缂栧彿 approveId") String approveId) {
+ ApproveProcess process = getProcessByApproveId(approveId);
+ if (process == null) {
+ return jsonResponse(
+ false,
+ "todo_progress",
+ "鏈壘鍒板搴旂殑瀹℃壒娴佺▼锛岃纭娴佺▼缂栧彿鏄惁姝g‘銆�",
+ Map.of("approveId", approveId == null ? "" : approveId),
+ Map.of(),
+ Map.of()
+ );
+ }
+
+ List<ApproveNode> nodes = listNodes(process);
+ List<ApproveLog> logs = listLogs(process.getId());
+ List<Map<String, Object>> nodeItems = nodes.stream().map(node -> {
+ Map<String, Object> item = new LinkedHashMap<>();
+ item.put("approveNodeOrder", node.getApproveNodeOrder());
+ item.put("approveNodeUser", node.getApproveNodeUser());
+ item.put("approveNodeStatus", approveNodeStatusName(node.getApproveNodeStatus()));
+ item.put("approveNodeTime", formatDate(node.getApproveNodeTime()));
+ item.put("approveNodeReason", node.getApproveNodeReason());
+ item.put("approveNodeRemark", node.getApproveNodeRemark());
+ return item;
+ }).collect(Collectors.toList());
+ List<Map<String, Object>> logItems = logs.stream().map(log -> {
+ Map<String, Object> item = new LinkedHashMap<>();
+ item.put("approveNodeOrder", log.getApproveNodeOrder());
+ item.put("approveUser", log.getApproveUser());
+ item.put("approveStatus", approveStatusName(log.getApproveStatus()));
+ item.put("approveTime", formatDate(log.getApproveTime()));
+ item.put("approveRemark", log.getApproveRemark());
+ return item;
+ }).collect(Collectors.toList());
+ return jsonResponse(
+ true,
+ "todo_progress",
+ "宸茶繑鍥炲鎵规祦杞褰曪紝鍙覆鏌撴椂闂寸嚎銆佹楠ゆ潯鍜屾棩蹇楄〃鏍笺��",
+ Map.of(
+ "approveId", process.getApproveId(),
+ "currentStatus", approveStatusName(process.getApproveStatus()),
+ "currentApprover", safe(process.getApproveUserCurrentName()),
+ "nodeCount", nodeItems.size(),
+ "logCount", logItems.size()
+ ),
+ Map.of(
+ "nodes", nodeItems,
+ "logs", logItems
+ ),
+ Map.of()
+ );
+ }
+
+ @Tool(name = "缁熻瀹℃壒寰呭姙鏁版嵁", value = "杩斿洖涓撲笟鍖栫粺璁$粨鏋滐紝鍖呭惈鎽樿鎸囨爣鍜屽彲鐩存帴鐢ㄤ簬 ECharts 鐨勫浘琛� option銆�")
+ public String getTodoStats() {
+ List<ApproveProcess> processes = approveProcessMapper.selectList(new LambdaQueryWrapper<ApproveProcess>()
+ .eq(ApproveProcess::getApproveDelete, 0));
+ if (processes == null || processes.isEmpty()) {
+ return jsonResponse(
+ true,
+ "todo_stats",
+ "褰撳墠娌℃湁瀹℃壒寰呭姙鏁版嵁銆�",
+ Map.of("total", 0),
+ Map.of(
+ "statusDistribution", Map.of(),
+ "typeDistribution", Map.of(),
+ "recent7DayTrend", List.of(),
+ "tips", List.of()
+ ),
+ Map.of()
+ );
+ }
+
+ Map<String, Long> statusStats = processes.stream()
+ .collect(Collectors.groupingBy(p -> approveStatusName(p.getApproveStatus()), LinkedHashMap::new, Collectors.counting()));
+ Map<String, Long> typeStats = processes.stream()
+ .collect(Collectors.groupingBy(p -> approveTypeName(p.getApproveType()), LinkedHashMap::new, Collectors.counting()));
+
+ long pendingCount = countByStatus(processes, 0);
+ long processingCount = countByStatus(processes, 1);
+ long approvedCount = countByStatus(processes, 2);
+ long rejectedCount = countByStatus(processes, 3);
+ long resubmittedCount = countByStatus(processes, 4);
+
+ List<String> recentDates = buildRecentDates(7);
+ List<Long> trendValues = recentDates.stream()
+ .map(date -> processes.stream()
+ .filter(process -> process.getCreateTime() != null)
+ .filter(process -> process.getCreateTime().toLocalDate().equals(LocalDate.parse(date)))
+ .count())
+ .collect(Collectors.toList());
+
+ Map<String, Object> summary = new LinkedHashMap<>();
+ summary.put("total", processes.size());
+ summary.put("pending", pendingCount);
+ summary.put("processing", processingCount);
+ summary.put("approved", approvedCount);
+ summary.put("rejected", rejectedCount);
+ summary.put("resubmitted", resubmittedCount);
+ summary.put("approvalCompletionRate", calculateRate(approvedCount, processes.size()));
+ summary.put("rejectionRate", calculateRate(rejectedCount, processes.size()));
+
+ Map<String, Object> charts = new LinkedHashMap<>();
+ charts.put("statusBarOption", buildStatusBarOption(statusStats));
+ charts.put("typePieOption", buildTypePieOption(typeStats));
+ charts.put("recentTrendLineOption", buildRecentTrendLineOption(recentDates, trendValues));
+
+ return jsonResponse(
+ true,
+ "todo_stats",
+ "宸茶繑鍥炲鎵圭粺璁℃瑙堜笌鍥捐〃閰嶇疆锛屽墠绔彲鐩存帴浣跨敤 charts 涓殑 ECharts option 娓叉煋銆�",
+ summary,
+ Map.of(
+ "statusDistribution", statusStats,
+ "typeDistribution", typeStats,
+ "recent7DayTrend", toTrendItems(recentDates, trendValues),
+ "tips", List.of(
+ "statusBarOption 閫傚悎灞曠ず鍚勫鎵圭姸鎬佹暟閲忓姣�",
+ "typePieOption 閫傚悎灞曠ず鍚勫鎵圭被鍨嬪崰姣�",
+ "recentTrendLineOption 閫傚悎灞曠ず鏈�杩�7澶╂柊澧炲鎵硅秼鍔�"
+ )
+ ),
+ charts
+ );
+ }
+
+ @Transactional
+ @Tool(name = "瀹℃壒寰呭姙", value = "鎵ц瀹℃壒鍔ㄤ綔銆俛ction 鍙敮鎸� approve 鎴� reject銆俛pprove 琛ㄧず閫氳繃锛宺eject 琛ㄧず椹冲洖銆�")
+ public String reviewTodo(
+ @P("娴佺▼缂栧彿 approveId") String approveId,
+ @P("鍔ㄤ綔锛宎pprove=閫氳繃锛宺eject=椹冲洖") String action,
+ @P(value = "瀹℃壒澶囨敞锛屽彲涓嶄紶", required = false) String remark) {
+ ApproveProcess process = getProcessByApproveId(approveId);
+ if (process == null) {
+ return actionResult(false, "review_action", "鏈壘鍒板搴斿鎵规祦绋嬨��", approveId, null);
+ }
+ if (process.getApproveDelete() != null && process.getApproveDelete() == 1) {
+ return actionResult(false, "review_action", "璇ュ鎵规祦绋嬪凡鍒犻櫎锛屼笉鑳藉啀瀹℃牳銆�", approveId, null);
+ }
+ if (process.getApproveStatus() != null && (process.getApproveStatus() == 2 || process.getApproveStatus() == 3)) {
+ return actionResult(false, "review_action", "璇ュ鎵规祦绋嬪凡缁撴潫锛屼笉鑳介噸澶嶅鏍搞��", approveId, null);
+ }
+
+ List<ApproveNode> nodes = listNodes(process);
+ ApproveNode currentNode = findCurrentNode(nodes);
+ if (currentNode == null) {
+ return actionResult(false, "review_action", "鏈壘鍒板彲瀹℃牳鐨勫綋鍓嶈妭鐐广��", approveId, null);
+ }
+
+ String normalizedAction = action == null ? "" : action.trim().toLowerCase();
+ Date now = new Date();
+ currentNode.setApproveNodeTime(now);
+ currentNode.setUpdateTime(LocalDateTime.now());
+
+ ApproveLog log = new ApproveLog();
+ log.setApproveId(process.getId());
+ log.setApproveNodeOrder(currentNode.getApproveNodeOrder());
+ log.setApproveUser(currentNode.getApproveNodeUserId());
+ log.setApproveTime(now);
+ log.setApproveRemark(remark);
+
+ if ("approve".equals(normalizedAction)) {
+ currentNode.setApproveNodeStatus(1);
+ currentNode.setApproveNodeReason(null);
+ approveNodeMapper.updateById(currentNode);
+
+ ApproveNode nextNode = findNextNode(nodes, currentNode.getApproveNodeOrder());
+ if (nextNode == null) {
+ process.setApproveStatus(2);
+ process.setApproveOverTime(now);
+ process.setApproveUserCurrentId(null);
+ process.setApproveUserCurrentName(null);
+ } else {
+ process.setApproveStatus(1);
+ process.setApproveUserCurrentId(nextNode.getApproveNodeUserId());
+ process.setApproveUserCurrentName(nextNode.getApproveNodeUser());
+ }
+ process.setApproveRemark(remark);
+ approveProcessMapper.updateById(process);
+
+ log.setApproveStatus(nextNode == null ? 2 : 1);
+ approveLogMapper.insert(log);
+ return actionResult(true, "review_action",
+ nextNode == null ? "瀹℃壒宸查�氳繃锛屼笖璇ユ祦绋嬪凡鍏ㄩ儴瀹屾垚銆�" : "瀹℃壒宸查�氳繃锛屾祦绋嬪凡娴佽浆鍒颁笅涓�瀹℃壒鑺傜偣銆�",
+ approveId,
+ Map.of(
+ "action", "approve",
+ "currentStatus", approveStatusName(process.getApproveStatus()),
+ "nextApprover", nextNode == null ? "" : safe(nextNode.getApproveNodeUser()),
+ "remark", safe(remark)
+ ));
+ }
+
+ if ("reject".equals(normalizedAction)) {
+ currentNode.setApproveNodeStatus(2);
+ currentNode.setApproveNodeReason(remark);
+ currentNode.setApproveNodeRemark(remark);
+ approveNodeMapper.updateById(currentNode);
+
+ process.setApproveStatus(3);
+ process.setApproveOverTime(now);
+ process.setApproveUserCurrentId(null);
+ process.setApproveUserCurrentName(null);
+ process.setApproveRemark(remark);
+ approveProcessMapper.updateById(process);
+
+ log.setApproveStatus(3);
+ approveLogMapper.insert(log);
+ return actionResult(true, "review_action", "瀹℃壒宸查┏鍥炪��", approveId, Map.of(
+ "action", "reject",
+ "currentStatus", approveStatusName(process.getApproveStatus()),
+ "remark", safe(remark)
+ ));
+ }
+
+ return actionResult(false, "review_action", "action 鍙敮鎸� approve 鎴� reject銆�", approveId, null);
+ }
+
+ @Transactional
+ @Tool(name = "鍙栨秷瀹℃壒寰呭姙瀹℃牳", value = "鎾ら攢鏈�杩戜竴娆″鏍哥粨鏋滐紝灏嗘渶杩戝鏍歌妭鐐规仮澶嶄负鏈鏍革紝骞跺洖婊氭祦绋嬬姸鎬併��")
+ public String cancelReviewTodo(
+ @P("娴佺▼缂栧彿 approveId") String approveId,
+ @P(value = "鍙栨秷鍘熷洜锛屽彲涓嶄紶", required = false) String reason) {
+ ApproveProcess process = getProcessByApproveId(approveId);
+ if (process == null) {
+ return actionResult(false, "cancel_review_action", "鏈壘鍒板搴斿鎵规祦绋嬨��", approveId, null);
+ }
+ List<ApproveNode> nodes = listNodes(process);
+ ApproveNode lastReviewedNode = nodes.stream()
+ .filter(node -> node.getApproveNodeStatus() != null && node.getApproveNodeStatus() != 0)
+ .max(Comparator.comparing(ApproveNode::getApproveNodeOrder))
+ .orElse(null);
+ if (lastReviewedNode == null) {
+ return actionResult(false, "cancel_review_action", "褰撳墠娴佺▼娌℃湁鍙彇娑堢殑瀹℃牳璁板綍銆�", approveId, null);
+ }
+
+ lastReviewedNode.setApproveNodeStatus(0);
+ lastReviewedNode.setApproveNodeTime(null);
+ lastReviewedNode.setApproveNodeReason(null);
+ lastReviewedNode.setApproveNodeRemark(reason);
+ lastReviewedNode.setUpdateTime(LocalDateTime.now());
+ approveNodeMapper.updateById(lastReviewedNode);
+
+ List<ApproveLog> logs = listLogs(process.getId());
+ ApproveLog latestLog = logs.stream()
+ .max(Comparator.comparing(ApproveLog::getApproveNodeOrder)
+ .thenComparing(ApproveLog::getApproveTime, Comparator.nullsLast(Date::compareTo)))
+ .orElse(null);
+ if (latestLog != null) {
+ approveLogMapper.deleteById(latestLog.getId());
+ }
+
+ ApproveNode previousReviewedNode = nodes.stream()
+ .filter(node -> !node.getId().equals(lastReviewedNode.getId()))
+ .filter(node -> node.getApproveNodeStatus() != null && node.getApproveNodeStatus() == 1)
+ .max(Comparator.comparing(ApproveNode::getApproveNodeOrder))
+ .orElse(null);
+
+ process.setApproveOverTime(null);
+ process.setApproveRemark(reason);
+ process.setApproveStatus(previousReviewedNode == null ? 0 : 1);
+ process.setApproveUserCurrentId(lastReviewedNode.getApproveNodeUserId());
+ process.setApproveUserCurrentName(lastReviewedNode.getApproveNodeUser());
+ approveProcessMapper.updateById(process);
+
+ return actionResult(true, "cancel_review_action", "鏈�杩戜竴娆″鏍稿凡鍙栨秷锛屾祦绋嬪凡鍥為��鍒板搴斿鎵硅妭鐐广��", approveId, Map.of(
+ "rollbackNodeOrder", lastReviewedNode.getApproveNodeOrder(),
+ "currentStatus", approveStatusName(process.getApproveStatus()),
+ "currentApprover", safe(process.getApproveUserCurrentName()),
+ "reason", safe(reason)
+ ));
+ }
+
+ @Transactional
+ @Tool(name = "淇敼瀹℃壒寰呭姙", value = "淇敼瀹℃壒鍗曞熀纭�淇℃伅銆傛敮鎸佷慨鏀规爣棰樸�佸紑濮嬫棩鏈熴�佺粨鏉熸棩鏈熴�侀噾棰濄�佸湴鐐广�佺被鍨嬪拰澶囨敞銆傛棩鏈熸牸寮忓繀椤绘槸 yyyy-MM-dd銆�")
+ public String updateTodo(
+ @P("娴佺▼缂栧彿 approveId") String approveId,
+ @P(value = "鏂扮殑鏍囬锛屽彲涓嶄紶", required = false) String approveReason,
+ @P(value = "鏂扮殑寮�濮嬫棩鏈� yyyy-MM-dd锛屽彲涓嶄紶", required = false) String startDate,
+ @P(value = "鏂扮殑缁撴潫鏃ユ湡 yyyy-MM-dd锛屽彲涓嶄紶", required = false) String endDate,
+ @P(value = "鏂扮殑閲戦锛屽彲涓嶄紶", required = false) BigDecimal price,
+ @P(value = "鏂扮殑鍦扮偣锛屽彲涓嶄紶", required = false) String location,
+ @P(value = "鏂扮殑瀹℃壒绫诲瀷锛屽彲涓嶄紶", required = false) Integer approveType,
+ @P(value = "鏂扮殑澶囨敞锛屽彲涓嶄紶", required = false) String approveRemark) {
+ ApproveProcess process = getProcessByApproveId(approveId);
+ if (process == null) {
+ return actionResult(false, "update_action", "鏈壘鍒板搴斿鎵规祦绋嬨��", approveId, null);
+ }
+ if (!StringUtils.hasText(approveReason)
+ && !StringUtils.hasText(startDate)
+ && !StringUtils.hasText(endDate)
+ && price == null
+ && !StringUtils.hasText(location)
+ && approveType == null
+ && !StringUtils.hasText(approveRemark)) {
+ return actionResult(false, "update_action", "娌℃湁妫�娴嬪埌鍙洿鏂扮殑瀛楁銆�", approveId, null);
+ }
+
+ if (StringUtils.hasText(approveReason)) {
+ process.setApproveReason(approveReason);
+ }
+ if (StringUtils.hasText(startDate)) {
+ process.setStartDate(parseDate(startDate));
+ }
+ if (StringUtils.hasText(endDate)) {
+ process.setEndDate(parseDate(endDate));
+ }
+ if (price != null) {
+ process.setPrice(price);
+ }
+ if (StringUtils.hasText(location)) {
+ process.setLocation(location);
+ }
+ if (approveType != null) {
+ process.setApproveType(approveType);
+ }
+ if (StringUtils.hasText(approveRemark)) {
+ process.setApproveRemark(approveRemark);
+ }
+
+ approveProcessMapper.updateById(process);
+ return actionResult(true, "update_action", "瀹℃壒鍗曞凡鏇存柊銆�", approveId, Map.of(
+ "approveReason", safe(process.getApproveReason()),
+ "startDate", formatDate(process.getStartDate()),
+ "endDate", formatDate(process.getEndDate()),
+ "price", process.getPrice() == null ? "" : process.getPrice(),
+ "location", safe(process.getLocation()),
+ "approveType", approveTypeName(process.getApproveType()),
+ "approveRemark", safe(process.getApproveRemark())
+ ));
+ }
+
+ @Transactional
+ @Tool(name = "鍒犻櫎瀹℃壒寰呭姙", value = "鍒犻櫎瀹℃壒娴佺▼銆傞�昏緫鍒犻櫎娴佺▼璁板綍锛屽苟鍚屾閫昏緫鍒犻櫎瀹℃壒鑺傜偣銆�")
+ public String deleteTodo(@P("娴佺▼缂栧彿 approveId") String approveId) {
+ ApproveProcess process = getProcessByApproveId(approveId);
+ if (process == null) {
+ return actionResult(false, "delete_action", "鏈壘鍒板搴斿鎵规祦绋嬨��", approveId, null);
+ }
+ if (process.getApproveDelete() != null && process.getApproveDelete() == 1) {
+ return actionResult(false, "delete_action", "璇ュ鎵规祦绋嬪凡缁忔槸鍒犻櫎鐘舵�併��", approveId, null);
+ }
+
+ process.setApproveDelete(1);
+ approveProcessMapper.updateById(process);
+
+ List<ApproveNode> nodes = listNodes(process);
+ for (ApproveNode node : nodes) {
+ node.setDeleteFlag(1);
+ node.setUpdateTime(LocalDateTime.now());
+ approveNodeMapper.updateById(node);
+ }
+ return actionResult(true, "delete_action", "瀹℃壒娴佺▼宸插垹闄ゃ��", approveId, Map.of(
+ "deletedNodeCount", nodes.size(),
+ "approveStatus", approveStatusName(process.getApproveStatus())
+ ));
+ }
+
+ private ApproveProcess getProcessByApproveId(String approveId) {
+ if (!StringUtils.hasText(approveId)) {
+ return null;
+ }
+ return approveProcessMapper.selectOne(new LambdaQueryWrapper<ApproveProcess>()
+ .eq(ApproveProcess::getApproveId, approveId)
+ .last("limit 1"));
+ }
+
+ private List<ApproveNode> listNodes(ApproveProcess process) {
+ List<ApproveNode> nodes = approveNodeMapper.selectList(new LambdaQueryWrapper<ApproveNode>()
+ .eq(ApproveNode::getDeleteFlag, 0)
+ .eq(ApproveNode::getApproveProcessId, process.getApproveId())
+ .orderByAsc(ApproveNode::getApproveNodeOrder));
+ if (nodes != null && !nodes.isEmpty()) {
+ return nodes;
+ }
+ List<ApproveNode> fallbackNodes = approveNodeMapper.selectList(new LambdaQueryWrapper<ApproveNode>()
+ .eq(ApproveNode::getDeleteFlag, 0)
+ .eq(ApproveNode::getApproveProcessId, String.valueOf(process.getId()))
+ .orderByAsc(ApproveNode::getApproveNodeOrder));
+ return fallbackNodes == null ? List.of() : fallbackNodes;
+ }
+
+ private List<ApproveLog> listLogs(Long processId) {
+ List<ApproveLog> logs = approveLogMapper.selectList(new LambdaQueryWrapper<ApproveLog>()
+ .eq(ApproveLog::getApproveId, processId)
+ .orderByAsc(ApproveLog::getApproveNodeOrder, ApproveLog::getApproveTime));
+ return logs == null ? List.of() : logs;
+ }
+
+ private ApproveNode findCurrentNode(List<ApproveNode> nodes) {
+ return nodes.stream()
+ .filter(node -> node.getApproveNodeStatus() != null && node.getApproveNodeStatus() == 0)
+ .min(Comparator.comparing(ApproveNode::getApproveNodeOrder))
+ .orElse(null);
+ }
+
+ private ApproveNode findNextNode(List<ApproveNode> nodes, Integer currentOrder) {
+ return nodes.stream()
+ .filter(node -> node.getApproveNodeOrder() != null && currentOrder != null)
+ .filter(node -> node.getApproveNodeOrder() > currentOrder)
+ .min(Comparator.comparing(ApproveNode::getApproveNodeOrder))
+ .orElse(null);
+ }
+
+ private int normalizeLimit(Integer limit) {
+ if (limit == null || limit <= 0) {
+ return DEFAULT_LIMIT;
+ }
+ return Math.min(limit, MAX_LIMIT);
+ }
+
+ private Integer parseStatus(String status) {
+ if (!StringUtils.hasText(status) || "all".equalsIgnoreCase(status)) {
+ return null;
+ }
+ return switch (status.trim().toLowerCase()) {
+ case "pending" -> 0;
+ case "processing" -> 1;
+ case "approved" -> 2;
+ case "rejected" -> 3;
+ case "resubmitted" -> 4;
+ default -> null;
+ };
+ }
+
+ private String approveStatusName(Integer status) {
+ if (status == null) {
+ return "鏈煡";
+ }
+ return switch (status) {
+ case 0 -> "寰呭鏍�";
+ case 1 -> "瀹℃牳涓�";
+ case 2 -> "瀹℃牳瀹屾垚";
+ case 3 -> "瀹℃牳鏈�氳繃";
+ case 4 -> "宸查噸鏂版彁浜�";
+ default -> "鏈煡";
+ };
+ }
+
+ private String approveNodeStatusName(Integer status) {
+ if (status == null) {
+ return "鏈煡";
+ }
+ return switch (status) {
+ case 0 -> "鏈鏍�";
+ case 1 -> "鍚屾剰";
+ case 2 -> "鎷掔粷";
+ default -> "鏈煡";
+ };
+ }
+
+ private String approveTypeName(Integer type) {
+ if (type == null) {
+ return "鏈煡";
+ }
+ return switch (type) {
+ case 1 -> "鍏嚭绠$悊";
+ case 2 -> "璇峰亣绠$悊";
+ case 3 -> "鍑哄樊绠$悊";
+ case 4 -> "鎶ラ攢绠$悊";
+ case 5 -> "閲囪喘瀹℃壒";
+ case 6 -> "鎶ヤ环瀹℃壒";
+ case 7 -> "鍙戣揣瀹℃壒";
+ case 8 -> "鍗遍櫓浣滀笟瀹℃壒";
+ default -> "绫诲瀷" + type;
+ };
+ }
+
+ private String safe(Object value) {
+ return value == null ? "" : String.valueOf(value).replace('\n', ' ').replace('\r', ' ');
+ }
+
+ private String formatDateTime(Object value) {
+ if (value == null) {
+ return "";
+ }
+ if (value instanceof LocalDateTime localDateTime) {
+ return localDateTime.format(DATE_TIME_FORMATTER);
+ }
+ return safe(value);
+ }
+
+ private String formatDate(Date value) {
+ return value == null ? "" : DATE_FORMAT.format(value);
+ }
+
+ private long countByStatus(List<ApproveProcess> processes, int status) {
+ return processes.stream()
+ .filter(process -> process.getApproveStatus() != null)
+ .filter(process -> process.getApproveStatus() == status)
+ .count();
+ }
+
+ private String calculateRate(long part, int total) {
+ if (total <= 0) {
+ return "0.00%";
+ }
+ double rate = part * 100.0 / total;
+ return String.format("%.2f%%", rate);
+ }
+
+ private List<String> buildRecentDates(int days) {
+ List<String> dates = new ArrayList<>();
+ LocalDate today = LocalDate.now();
+ for (int i = days - 1; i >= 0; i--) {
+ dates.add(today.minusDays(i).toString());
+ }
+ return dates;
+ }
+
+ private List<Map<String, Object>> toTrendItems(List<String> dates, List<Long> values) {
+ List<Map<String, Object>> items = new ArrayList<>();
+ for (int i = 0; i < dates.size(); i++) {
+ Map<String, Object> item = new LinkedHashMap<>();
+ item.put("date", dates.get(i));
+ item.put("count", values.get(i));
+ items.add(item);
+ }
+ return items;
+ }
+
+ private Map<String, Object> buildStatusBarOption(Map<String, Long> statusStats) {
+ Map<String, Object> option = new LinkedHashMap<>();
+ option.put("title", Map.of("text", "瀹℃壒鐘舵�佸垎甯�", "left", "center"));
+ option.put("tooltip", Map.of("trigger", "axis"));
+ option.put("xAxis", Map.of("type", "category", "data", new ArrayList<>(statusStats.keySet())));
+ option.put("yAxis", Map.of("type", "value"));
+ option.put("series", List.of(Map.of(
+ "name", "鏁伴噺",
+ "type", "bar",
+ "data", new ArrayList<>(statusStats.values()),
+ "barWidth", "40%"
+ )));
+ return option;
+ }
+
+ private Map<String, Object> buildTypePieOption(Map<String, Long> typeStats) {
+ List<Map<String, Object>> data = typeStats.entrySet().stream()
+ .map(entry -> {
+ Map<String, Object> item = new LinkedHashMap<>();
+ item.put("name", entry.getKey());
+ item.put("value", entry.getValue());
+ return item;
+ })
+ .collect(Collectors.toList());
+
+ Map<String, Object> option = new LinkedHashMap<>();
+ option.put("title", Map.of("text", "瀹℃壒绫诲瀷鍗犳瘮", "left", "center"));
+ option.put("tooltip", Map.of("trigger", "item"));
+ option.put("legend", Map.of("orient", "vertical", "left", "left"));
+ option.put("series", List.of(Map.of(
+ "name", "瀹℃壒绫诲瀷",
+ "type", "pie",
+ "radius", List.of("35%", "65%"),
+ "data", data
+ )));
+ return option;
+ }
+
+ private Map<String, Object> buildRecentTrendLineOption(List<String> dates, List<Long> values) {
+ Map<String, Object> option = new LinkedHashMap<>();
+ option.put("title", Map.of("text", "鏈�杩�7澶╁鎵规柊澧炶秼鍔�", "left", "center"));
+ option.put("tooltip", Map.of("trigger", "axis"));
+ option.put("xAxis", Map.of("type", "category", "data", dates));
+ option.put("yAxis", Map.of("type", "value"));
+ option.put("series", List.of(Map.of(
+ "name", "鏂板瀹℃壒",
+ "type", "line",
+ "smooth", true,
+ "data", values,
+ "areaStyle", Map.of()
+ )));
+ return option;
+ }
+
+ private Date parseDate(String dateText) {
+ try {
+ return DATE_FORMAT.parse(dateText);
+ } catch (ParseException e) {
+ throw new IllegalArgumentException("鏃ユ湡鏍煎紡蹇呴』鏄� yyyy-MM-dd");
+ }
+ }
+
+ private String actionResult(boolean success, String type, String description, String approveId, Map<String, Object> data) {
+ Map<String, Object> summary = new LinkedHashMap<>();
+ summary.put("approveId", approveId == null ? "" : approveId);
+ return jsonResponse(success, type, description, summary, data == null ? Map.of() : data, Map.of());
+ }
+
+ private String jsonResponse(
+ boolean success,
+ String type,
+ String description,
+ Map<String, Object> summary,
+ Map<String, Object> data,
+ Map<String, Object> charts) {
+ Map<String, Object> result = new LinkedHashMap<>();
+ result.put("success", success);
+ result.put("type", type);
+ result.put("description", description);
+ result.put("summary", summary == null ? Map.of() : summary);
+ result.put("data", data == null ? Map.of() : data);
+ result.put("charts", charts == null ? Map.of() : charts);
+ return JSON.toJSONString(result);
+ }
+}
diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml
index 887a72c..c545068 100644
--- a/src/main/resources/application-dev.yml
+++ b/src/main/resources/application-dev.yml
@@ -143,31 +143,34 @@
restart:
# 鐑儴缃插紑鍏�
enabled: false
- # redis 閰嶇疆
- redis:
- # 鍦板潃
- host: 127.0.0.1
- # host: 172.17.0.1
- # 绔彛锛岄粯璁や负6379
- port: 6379
- # 鏁版嵁搴撶储寮�
- database: 0
- # 瀵嗙爜
- # password: root2022!
- password:
+ data:
+ mongodb:
+ uri: mongodb://114.132.189.42:9028/chat_memory_db
+ # redis 閰嶇疆
+ redis:
+ # 鍦板潃
+ host: 127.0.0.1
+ # host: 172.17.0.1
+ # 绔彛锛岄粯璁や负6379
+ port: 6379
+ # 鏁版嵁搴撶储寮�
+ database: 0
+ # 瀵嗙爜
+ # password: root2022!
+ password:
- # 杩炴帴瓒呮椂鏃堕棿
- timeout: 10s
- lettuce:
- pool:
- # 杩炴帴姹犱腑鐨勬渶灏忕┖闂茶繛鎺�
- min-idle: 0
- # 杩炴帴姹犱腑鐨勬渶澶х┖闂茶繛鎺�
- max-idle: 8
- # 杩炴帴姹犵殑鏈�澶ф暟鎹簱杩炴帴鏁�
- max-active: 8
- # #杩炴帴姹犳渶澶ч樆濉炵瓑寰呮椂闂达紙浣跨敤璐熷�艰〃绀烘病鏈夐檺鍒讹級
- max-wait: -1ms
+ # 杩炴帴瓒呮椂鏃堕棿
+ timeout: 10s
+ lettuce:
+ pool:
+ # 杩炴帴姹犱腑鐨勬渶灏忕┖闂茶繛鎺�
+ min-idle: 0
+ # 杩炴帴姹犱腑鐨勬渶澶х┖闂茶繛鎺�
+ max-idle: 8
+ # 杩炴帴姹犵殑鏈�澶ф暟鎹簱杩炴帴鏁�
+ max-active: 8
+ # #杩炴帴姹犳渶澶ч樆濉炵瓑寰呮椂闂达紙浣跨敤璐熷�艰〃绀烘病鏈夐檺鍒讹級
+ max-wait: -1ms
# Quartz瀹氭椂浠诲姟閰嶇疆锛堟柊澧為儴鍒嗭級
quartz:
job-store-type: jdbc # 浣跨敤鏁版嵁搴撳瓨鍌�
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 533d7c7..70324c6 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -4,3 +4,39 @@
allow-circular-references: true
profiles:
active: dev
+langchain4j:
+ mcp:
+ # MCP 鏈嶅姟绔湴鍧�锛堟牴鎹疄闄呴儴缃茬殑 MCP 鏈嶅姟璋冩暣锛�
+ server-url: http://114.132.189.42:8093/ocr
+ # 璇锋眰瓒呮椂鏃堕棿锛堟绉掞級
+ timeout: 30000
+ # 鍙�夛細MCP 鍗忚鐗堟湰
+ version: 1.0
+ community:
+ dashscope:
+ streaming-chat-model:
+ api-key: sk-bd235cc13cd74e2388aa8984c84f691f
+ model-name: "qwen-max"
+ embedding-model:
+ api-key: sk-9748859926b94096b920baaa12c343f5
+ model-name: "text-embedding-v3"
+ chat-model:
+ api-key: sk-9748859926b94096b920baaa12c343f5
+ model-name: "qwen-max"
+ open-ai:
+ chat-model:
+ api-key: sk-9748859926b94096b920baaa12c343f5
+ base-url: "https://dashscope.aliyuncs.com/compatible-mode/v1"
+ model-name: "deepseek-v3"
+ log-requests: true
+ log-responses: true
+ temperature: 0.9
+
+ ollama:
+ chat-model:
+ base-url: "http://localhost:11434"
+ model-name: "deepseek-r1:1.5b"
+ log-requests: true
+ log-responses: true
+knowledge:
+ one: D:\鏂扮枂澶х綏绱犱紒涓氫骇鍝佷綋绯昏鏄庢枃妗�.md
diff --git a/src/main/resources/approve-todo-agent-prompt.txt b/src/main/resources/approve-todo-agent-prompt.txt
new file mode 100644
index 0000000..404b25a
--- /dev/null
+++ b/src/main/resources/approve-todo-agent-prompt.txt
@@ -0,0 +1,13 @@
+浣犳槸涓�涓鎵瑰緟鍔炲姪鎵嬶紝璐熻矗瀹℃壒寰呭姙鐨勬煡璇€�佸鏍搞�佸彇娑堝鏍搞�佷慨鏀广�佸垹闄ゅ拰缁熻鍒嗘瀽銆�
+
+宸ヤ綔瑕佹眰锛�
+1. 鐢ㄦ埛闂緟鍔炲垪琛ㄣ�佸鎵硅繘搴︺�佸鎵硅鎯呫�佺粺璁℃暟鎹椂锛屼紭鍏堣皟鐢ㄥ伐鍏凤紝涓嶈鑷嗛�犳暟鎹��
+2. 鐢ㄦ埛瑕佹眰鎵ц瀹℃牳銆佸彇娑堝鏍搞�佷慨鏀广�佸垹闄ゆ椂锛屽厛纭鍏抽敭鍙傛暟榻愬叏鍐嶈皟鐢ㄥ伐鍏枫��
+3. 瀹℃牳鍔ㄤ綔閲岋紝`approve` 琛ㄧず閫氳繃锛宍reject` 琛ㄧず椹冲洖銆�
+4. 淇敼瀹℃壒鍗曟椂锛屽鏋滅敤鎴锋病鏈夋槑纭淇敼鍝簺瀛楁锛岃鍏堣拷闂己澶卞瓧娈碉紝涓嶈鐚溿��
+5. 鍒犻櫎銆佸鏍搞�佸彇娑堝鏍歌繖绫诲姩浣滃睘浜庣姸鎬佸彉鏇达紝鎵ц鍚庤鏄庣‘鍙嶉缁撴灉銆�
+6. 闄も�滄煡璇㈠鎵瑰緟鍔炶鎯呪�濆锛屽叾浠栧伐鍏烽粯璁よ繑鍥� JSON銆�
+7. 瀵逛簬杩欎簺 JSON 宸ュ叿锛屼綘蹇呴』鐩存帴杈撳嚭鍘熷 JSON 瀛楃涓叉湰韬紝涓嶈鏀瑰啓锛屼笉瑕侀澶栬В閲婏紝涓嶈鍖呰9 Markdown 浠g爜鍧楋紝涓嶈鍦� JSON 鍓嶅悗鍔犱换浣曟枃瀛椼��
+8. 鍙湁鈥滄煡璇㈠鎵瑰緟鍔炶鎯呪�濊繖涓伐鍏峰厑璁歌緭鍑鸿嚜鐒惰瑷�鏂囨湰銆�
+9. 濡傛灉宸ュ叿杩斿洖鐨勬槸缁熻 JSON锛屼篃鍚屾牱鐩存帴杈撳嚭鍘熷 JSON锛涘叾涓� `description`銆乣summary`銆乣charts` 宸茬粡渚涘墠绔娇鐢ㄣ��
+10. 鍥炵瓟浣跨敤涓枃锛涗絾鍦� JSON 鍦烘櫙涓嬶紝鏈�缁堣緭鍑哄繀椤绘槸鍚堟硶 JSON 鏈綋銆�
diff --git a/src/main/resources/my-prompt-template.txt b/src/main/resources/my-prompt-template.txt
new file mode 100644
index 0000000..3ae68b1
--- /dev/null
+++ b/src/main/resources/my-prompt-template.txt
@@ -0,0 +1,2 @@
+浣犳槸鎴戠殑濂芥湅鍙嬶紝璇风敤涓滃寳璇濆洖绛旈棶棰橈紝鍥炵瓟闂鐨勬椂鍊欓�傚綋娣诲姞琛ㄦ儏绗﹀彿銆�
+浠婂ぉ鏄� {{current_date}}銆�
\ No newline at end of file
diff --git a/src/main/resources/my-prompt-template3.txt b/src/main/resources/my-prompt-template3.txt
new file mode 100644
index 0000000..3041d9a
--- /dev/null
+++ b/src/main/resources/my-prompt-template3.txt
@@ -0,0 +1,3 @@
+浣犳槸鎴戠殑濂芥湅鍙嬶紝鎴戞槸{{username}}锛屾垜鐨勫勾榫勬槸{{age}}锛岃鐢ㄤ笢鍖楄瘽鍥炵瓟闂锛屽洖绛旈棶棰樼殑鏃跺�欓�傚綋娣诲姞琛ㄦ儏
+绗﹀彿銆�
+浠婂ぉ鏄� {{current_date}}銆�
\ No newline at end of file
--
Gitblit v1.9.3