2026-04-27 0bc2775e4fd776086c508fd7640bc3d61835cf73
feat(ai): 添加审批待办助手功能

- 集成 langchain4j 配置,支持多种 AI 模型包括 DashScope、OpenAI 和 Ollama
- 配置 MongoDB 存储聊天记忆,替换原有的 Redis 配置
- 添加审批待办助手的 Prompt 提示词文件
- 创建 ApproveTodoAgent 接口和相关配置类
- 实现 ApproveTodoIntentExecutor 意图执行器,支持审批相关操作识别
- 开发 ApproveTodoTools 工具类,提供审批待办的增删改查和统计功能
- 实现完整的审批流程管理,包括审核、驳回、修改、删除等操作
- 添加审批数据统计分析功能,支持状态分布和趋势图表
- 集成 ECharts 图表配置,便于前端展示统计结果
已添加16个文件
已修改3个文件
1605 ■■■■■ 文件已修改
pom.xml 156 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/assistant/ApproveTodoAgent.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/assistant/ApproveTodoIntentExecutor.java 208 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/assistant/Assistant.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/assistant/SeparateChatAssistant.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/bean/ChatForm.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/config/ApproveTodoAgentConfig.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/config/EmbeddingStoreConfig.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/config/SeparateChatAssistantConfig.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/config/XiaozhiAgentConfig.java 100 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/controller/XiaozhiController.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/mongodbBean/ChatMessages.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/store/MongoChatMemoryStore.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/tools/ApproveTodoTools.java 751 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-dev.yml 51 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application.yml 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/approve-todo-agent-prompt.txt 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/my-prompt-template.txt 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/my-prompt-template3.txt 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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>
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);
}
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);
    }
}
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
 */
//因为我们在配置文件中同时配置了多个大语言模型,所以需要在这里明确指定(EXPLICIT)模型的beanName
@AiService(wiringMode = EXPLICIT, chatModel = "qwenChatModel")
public interface Assistant {
    String chat(String userMessage);
}
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
    );
}
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;//用户问题
}
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();
    }
}
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();
    }
}
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();
    }
}
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读取指定目录下的知识库文档
        //并使用默认的文档解析器对文档进行解析
        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的Document对象
//        List<Document> documents = new ArrayList<>();
        //使用内存向量存储
        InMemoryEmbeddingStore<TextSegment> inMemoryEmbeddingStore = new InMemoryEmbeddingStore<>();
        //使用默认的文档分割器
        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();
    }
}
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());
    }
}
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; //存储当前聊天记录列表的json字符串
}
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);
    }
}
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、pending、processing、approved、rejected、resubmitted", 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 "未找到对应的审批流程,请确认流程编号是否正确。";
        }
        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",
                    "未找到对应的审批流程,请确认流程编号是否正确。",
                    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 = "执行审批动作。action åªæ”¯æŒ approve æˆ– reject。approve è¡¨ç¤ºé€šè¿‡ï¼Œreject è¡¨ç¤ºé©³å›žã€‚")
    public String reviewTodo(
            @P("流程编号 approveId") String approveId,
            @P("动作,approve=通过,reject=驳回") 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);
    }
}
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  # ä½¿ç”¨æ•°æ®åº“存储
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
src/main/resources/approve-todo-agent-prompt.txt
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,13 @@
你是一个审批待办助手,负责审批待办的查询、审核、取消审核、修改、删除和统计分析。
工作要求:
1. ç”¨æˆ·é—®å¾…办列表、审批进度、审批详情、统计数据时,优先调用工具,不要臆造数据。
2. ç”¨æˆ·è¦æ±‚执行审核、取消审核、修改、删除时,先确认关键参数齐全再调用工具。
3. å®¡æ ¸åŠ¨ä½œé‡Œï¼Œ`approve` è¡¨ç¤ºé€šè¿‡ï¼Œ`reject` è¡¨ç¤ºé©³å›žã€‚
4. ä¿®æ”¹å®¡æ‰¹å•时,如果用户没有明确要修改哪些字段,要先追问缺失字段,不要猜。
5. åˆ é™¤ã€å®¡æ ¸ã€å–消审核这类动作属于状态变更,执行后要明确反馈结果。
6. é™¤â€œæŸ¥è¯¢å®¡æ‰¹å¾…办详情”外,其他工具默认返回 JSON。
7. å¯¹äºŽè¿™äº› JSON å·¥å…·ï¼Œä½ å¿…须直接输出原始 JSON å­—符串本身,不要改写,不要额外解释,不要包裹 Markdown ä»£ç å—,不要在 JSON å‰åŽåŠ ä»»ä½•æ–‡å­—ã€‚
8. åªæœ‰â€œæŸ¥è¯¢å®¡æ‰¹å¾…办详情”这个工具允许输出自然语言文本。
9. å¦‚果工具返回的是统计 JSON,也同样直接输出原始 JSON;其中 `description`、`summary`、`charts` å·²ç»ä¾›å‰ç«¯ä½¿ç”¨ã€‚
10. å›žç­”使用中文;但在 JSON åœºæ™¯ä¸‹ï¼Œæœ€ç»ˆè¾“出必须是合法 JSON æœ¬ä½“。
src/main/resources/my-prompt-template.txt
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,2 @@
你是我的好朋友,请用东北话回答问题,回答问题的时候适当添加表情符号。
今天是 {{current_date}}。
src/main/resources/my-prompt-template3.txt
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,3 @@
你是我的好朋友,我是{{username}},我的年龄是{{age}},请用东北话回答问题,回答问题的时候适当添加表情
符号。
今天是 {{current_date}}。