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_BY_LABEL_PATTERN = Pattern.compile("(流程编号|流程号|流程ID|审批编号|编号)\\s*[::]?\\s*([A-Za-z0-9_-]{2,64})");
|
private static final Pattern APPROVE_ID_PATTERN = Pattern.compile("\\b[A-Za-z]*\\d{6,}[A-Za-z0-9_-]*\\b");
|
private static final Pattern LIMIT_PATTERN = Pattern.compile("(前|最近)?\\s*(\\d{1,2})\\s*条");
|
private static final Pattern DATE_PATTERN = Pattern.compile("(\\d{4}-\\d{2}-\\d{2})");
|
private static final Pattern NUMBER_PATTERN = Pattern.compile("(\\d+(?:\\.\\d+)?)");
|
private static final Pattern RECENT_RANGE_PATTERN = Pattern.compile("近\\d+(天|周|个月|月|年)");
|
private static final Pattern HALF_RANGE_PATTERN = Pattern.compile("(最近|近)?半(个)?(月|年)");
|
private static final Pattern EXPLICIT_RANGE_PATTERN = Pattern.compile(".*(到|至).*");
|
|
private final ApproveTodoTools approveTodoTools;
|
|
public ApproveTodoIntentExecutor(ApproveTodoTools approveTodoTools) {
|
this.approveTodoTools = approveTodoTools;
|
}
|
|
public String tryExecute(String memoryId, String message) {
|
if (!StringUtils.hasText(message)) {
|
return null;
|
}
|
|
String text = message.trim();
|
String quickPromptResponse = tryExecuteQuickPrompt(memoryId, text);
|
if (StringUtils.hasText(quickPromptResponse)) {
|
return quickPromptResponse;
|
}
|
|
String approveId = extractApproveId(text);
|
boolean hasApproveId = StringUtils.hasText(approveId) && !isPlaceholderApproveId(approveId);
|
String startDate = extractStartDate(text);
|
String endDate = extractEndDate(text);
|
String timeRange = extractTimeRange(text);
|
|
if (isStatsIntent(text)) {
|
return approveTodoTools.getTodoStats(
|
memoryId,
|
startDate,
|
endDate,
|
timeRange
|
);
|
}
|
if (containsAny(text, "流转", "进度", "节点", "日志", "卡在", "卡到", "当前审批人", "处理记录")) {
|
return hasApproveId
|
? approveTodoTools.getTodoProgress(memoryId, approveId)
|
: missingApproveId("todo_progress", "查询审批进度需要提供流程编号。");
|
}
|
if (containsAny(text, "详情", "明细") && !containsAny(text, "列表")) {
|
return hasApproveId
|
? approveTodoTools.getTodoDetail(memoryId, approveId)
|
: missingApproveId("todo_detail", "查询审批详情需要提供流程编号。");
|
}
|
if (containsAny(text, "取消审核", "撤销审核", "回退审核", "撤销审批", "撤回审批")
|
|| (containsAny(text, "撤销", "撤回") && containsAny(text, "审批操作", "审核操作"))) {
|
return hasApproveId
|
? approveTodoTools.cancelReviewTodo(memoryId, approveId, extractRemark(text))
|
: missingApproveId("cancel_review_action", "取消审核需要提供流程编号。");
|
}
|
if (containsAny(text, "删除", "移除")) {
|
return hasApproveId
|
? approveTodoTools.deleteTodo(memoryId, approveId)
|
: missingApproveId("delete_action", "删除审批单需要提供流程编号。");
|
}
|
if (containsAny(text, "驳回", "拒绝")) {
|
return hasApproveId
|
? approveTodoTools.reviewTodo(memoryId, approveId, "reject", extractRemark(text))
|
: missingApproveId("review_action", "驳回审批需要提供流程编号。");
|
}
|
if (containsAny(text, "审核通过", "审批通过", "通过审批", "同意审批", "审批同意")) {
|
return hasApproveId
|
? approveTodoTools.reviewTodo(memoryId, approveId, "approve", extractRemark(text))
|
: missingApproveId("review_action", "审批通过需要提供流程编号。");
|
}
|
if (hasApproveId
|
&& containsAny(text, "通过", "同意")
|
&& !containsAny(text, "未通过", "通过率", "审批通过率", "审核通过率")) {
|
return approveTodoTools.reviewTodo(memoryId, approveId, "approve", extractRemark(text));
|
}
|
if (containsAny(text, "修改", "更新", "变更")) {
|
return hasApproveId
|
? approveTodoTools.updateTodo(
|
memoryId,
|
approveId,
|
extractValue(text, "标题"),
|
extractDateValue(text, "开始日期"),
|
extractDateValue(text, "结束日期"),
|
extractBigDecimalValue(text, "金额"),
|
extractValue(text, "地点"),
|
extractIntegerValue(text, "类型"),
|
extractValue(text, "备注"))
|
: missingApproveId("update_action", "修改审批单需要提供流程编号。");
|
}
|
if (containsAny(text, "列表", "待办", "查询审批", "单据", "流程", "审批批")) {
|
return approveTodoTools.listTodos(
|
memoryId,
|
extractStatus(text),
|
extractApproveType(text),
|
extractKeyword(text),
|
extractLimit(text),
|
extractScope(text),
|
startDate,
|
endDate,
|
timeRange);
|
}
|
return null;
|
}
|
|
private String tryExecuteQuickPrompt(String memoryId, String text) {
|
String normalized = normalizeForMatch(text);
|
String approveId = extractApproveId(text);
|
boolean hasApproveId = StringUtils.hasText(approveId) && !isPlaceholderApproveId(approveId);
|
|
if ("我当前有哪些审批待办需要处理".equals(normalized)) {
|
return approveTodoTools.listTodos(memoryId, "pending", null, null, 10, "approver", null, null, null);
|
}
|
if ("帮我列出今天新增的审批待办".equals(normalized)) {
|
return approveTodoTools.listTodos(memoryId, "all", null, null, 10, "related", null, null, "今天");
|
}
|
if ("当前待我审批的单据按时间倒序列出来".equals(normalized)) {
|
return approveTodoTools.listTodos(memoryId, "pending", null, null, 10, "approver", null, null, null);
|
}
|
if ("我发起的审批里哪些还在处理中".equals(normalized)) {
|
return approveTodoTools.listTodos(memoryId, "processing", null, null, 10, "applicant", null, null, null);
|
}
|
if ("近7天我的审批待办统计情况怎么样".equals(normalized)) {
|
return approveTodoTools.getTodoStats(memoryId, null, null, "近7天");
|
}
|
if ("本月我的审批中通过驳回处理中各有多少".equals(normalized)) {
|
return approveTodoTools.getTodoStats(memoryId, null, null, "本月");
|
}
|
if ("近30天各类型审批数量分布是什么".equals(normalized)) {
|
return approveTodoTools.getTodoStats(memoryId, null, null, "近30天");
|
}
|
|
if (normalized.startsWith("查询流程编号") && normalized.contains("审批详情")) {
|
return hasApproveId
|
? approveTodoTools.getTodoDetail(memoryId, approveId)
|
: missingApproveId("todo_detail", "查询审批详情需要提供真实流程编号。");
|
}
|
if (normalized.startsWith("流程编号")
|
&& normalized.contains("卡在哪个审批节点")
|
&& normalized.contains("当前审批人是谁")) {
|
return hasApproveId
|
? approveTodoTools.getTodoProgress(memoryId, approveId)
|
: missingApproveId("todo_progress", "查询审批进度需要提供真实流程编号。");
|
}
|
if (normalized.startsWith("帮我查看流程编号") && normalized.contains("审批流转记录")) {
|
return hasApproveId
|
? approveTodoTools.getTodoProgress(memoryId, approveId)
|
: missingApproveId("todo_progress", "查询审批流转记录需要提供真实流程编号。");
|
}
|
if (normalized.startsWith("帮我审批通过流程编号")) {
|
return hasApproveId
|
? approveTodoTools.reviewTodo(memoryId, approveId, "approve", extractRemark(text))
|
: missingApproveId("review_action", "审批通过需要提供真实流程编号。");
|
}
|
if (normalized.startsWith("帮我驳回流程编号")) {
|
return hasApproveId
|
? approveTodoTools.reviewTodo(memoryId, approveId, "reject", extractRemark(text))
|
: missingApproveId("review_action", "驳回审批需要提供真实流程编号。");
|
}
|
if (normalized.startsWith("撤销我刚刚对流程编号") && normalized.contains("审批操作")) {
|
return hasApproveId
|
? approveTodoTools.cancelReviewTodo(memoryId, approveId, extractRemark(text))
|
: missingApproveId("cancel_review_action", "撤销审批操作需要提供真实流程编号。");
|
}
|
if (normalized.startsWith("帮我修改流程编号") && normalized.contains("备注为")) {
|
return hasApproveId
|
? approveTodoTools.updateTodo(memoryId, approveId, null, null, null, null, null, null, extractRemark(text))
|
: missingApproveId("update_action", "修改审批单需要提供真实流程编号。");
|
}
|
if (normalized.startsWith("删除我发起的流程编号")) {
|
return hasApproveId
|
? approveTodoTools.deleteTodo(memoryId, approveId)
|
: missingApproveId("delete_action", "删除审批单需要提供真实流程编号。");
|
}
|
return null;
|
}
|
|
private boolean isStatsIntent(String text) {
|
if (containsAny(text, "统计", "分析", "图表", "趋势", "占比", "汇总", "总量", "分布", "各有多少", "有多少")) {
|
return true;
|
}
|
boolean hasQueryWord = containsAny(text, "查询", "查看", "看下", "看看", "获取");
|
boolean hasDataWord = containsAny(text, "数据", "报表", "情况", "数量", "金额");
|
boolean hasTimeWord = containsAny(text, "今天", "昨日", "昨天", "本周", "上周", "本月", "上月", "本年", "今年", "去年")
|
|| DATE_PATTERN.matcher(text).find()
|
|| RECENT_RANGE_PATTERN.matcher(text).find()
|
|| HALF_RANGE_PATTERN.matcher(text).find()
|
|| EXPLICIT_RANGE_PATTERN.matcher(text).matches();
|
return hasQueryWord && hasDataWord && hasTimeWord;
|
}
|
|
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 keywordMatcher = APPROVE_ID_BY_LABEL_PATTERN.matcher(text);
|
if (keywordMatcher.find()) {
|
return keywordMatcher.group(2);
|
}
|
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("审批", "")
|
.replace("单据", "")
|
.replace("待办", "")
|
.replace("列表", "")
|
.replace("流程编号", "")
|
.replace("流程号", "")
|
.replace("前10条", "")
|
.replace("最近10条", "")
|
.trim();
|
return cleaned.length() >= 2 ? cleaned : null;
|
}
|
|
private String extractValue(String text, String fieldName) {
|
Pattern pattern = Pattern.compile(fieldName + "(改为|修改为|为|是)[::]?[\\s]*([^,,。;;\\s]+)");
|
Matcher matcher = pattern.matcher(text);
|
return matcher.find() ? matcher.group(2) : null;
|
}
|
|
private String extractDateValue(String text, String fieldName) {
|
int index = text.indexOf(fieldName);
|
if (index < 0) {
|
return null;
|
}
|
Matcher matcher = DATE_PATTERN.matcher(text.substring(index));
|
return matcher.find() ? matcher.group(1) : null;
|
}
|
|
private String extractStartDate(String text) {
|
Matcher matcher = DATE_PATTERN.matcher(text);
|
return matcher.find() ? matcher.group(1) : null;
|
}
|
|
private String extractEndDate(String text) {
|
Matcher matcher = DATE_PATTERN.matcher(text);
|
if (!matcher.find()) {
|
return null;
|
}
|
return matcher.find() ? matcher.group(1) : null;
|
}
|
|
private String extractTimeRange(String text) {
|
if (containsAny(text, "今天", "昨日", "昨天", "本周", "上周", "本月", "上月", "本年", "今年", "去年")) {
|
return text;
|
}
|
if (RECENT_RANGE_PATTERN.matcher(text).find()) {
|
return text;
|
}
|
if (HALF_RANGE_PATTERN.matcher(text).find()) {
|
return text;
|
}
|
if (EXPLICIT_RANGE_PATTERN.matcher(text).matches()) {
|
return text;
|
}
|
return null;
|
}
|
|
private Integer extractIntegerValue(String text, String fieldName) {
|
if (!text.contains(fieldName)) {
|
return null;
|
}
|
Matcher matcher = Pattern.compile(fieldName + "(改为|修改为|为|是)[::]?[\\s]*(\\d{1,2})").matcher(text);
|
return matcher.find() ? Integer.parseInt(matcher.group(2)) : null;
|
}
|
|
private BigDecimal extractBigDecimalValue(String text, String fieldName) {
|
int index = text.indexOf(fieldName);
|
if (index < 0) {
|
return null;
|
}
|
Matcher matcher = NUMBER_PATTERN.matcher(text.substring(index));
|
return matcher.find() ? new BigDecimal(matcher.group(1)) : null;
|
}
|
|
private String extractTail(String text, String key) {
|
Pattern quotedPattern = Pattern.compile(key + "(是|为)?[::]?[\\s]*[“\"]([^”\"]+)[”\"]");
|
Matcher quotedMatcher = quotedPattern.matcher(text);
|
if (quotedMatcher.find()) {
|
return cleanContent(quotedMatcher.group(2));
|
}
|
Pattern pattern = Pattern.compile(key + "(是|为)?[::]?[\\s]*(.+)");
|
Matcher matcher = pattern.matcher(text);
|
return matcher.find() ? cleanContent(matcher.group(2)) : null;
|
}
|
|
private String extractScope(String text) {
|
if (containsAny(text, "我发起", "我提交", "我申请", "申请人是我")) {
|
return "applicant";
|
}
|
if (containsAny(text, "待我审批", "待我审核", "我处理", "我审批", "当前待我", "需要我处理", "需要处理")) {
|
return "approver";
|
}
|
return "related";
|
}
|
|
private String extractRemark(String text) {
|
return firstNonBlank(firstNonBlank(extractTail(text, "备注"), extractTail(text, "原因")), extractQuotedContent(text));
|
}
|
|
private String extractQuotedContent(String text) {
|
Matcher matcher = Pattern.compile("[“\"]([^”\"]+)[”\"]").matcher(text);
|
return matcher.find() ? cleanContent(matcher.group(1)) : null;
|
}
|
|
private String normalizeForMatch(String text) {
|
if (!StringUtils.hasText(text)) {
|
return "";
|
}
|
return text.replace(",", "")
|
.replace(",", "")
|
.replace("。", "")
|
.replace(".", "")
|
.replace("!", "")
|
.replace("!", "")
|
.replace("?", "")
|
.replace("?", "")
|
.replace(":", "")
|
.replace(":", "")
|
.replace(";", "")
|
.replace(";", "")
|
.replace("“", "")
|
.replace("”", "")
|
.replace("\"", "")
|
.replace(" ", "")
|
.trim();
|
}
|
|
private boolean isPlaceholderApproveId(String approveId) {
|
if (!StringUtils.hasText(approveId)) {
|
return true;
|
}
|
String value = approveId.trim();
|
return "xxx".equalsIgnoreCase(value)
|
|| value.matches("[xX]{2,}")
|
|| "流程编号".equals(value)
|
|| "编号".equals(value)
|
|| value.contains("示例")
|
|| value.contains("请输入");
|
}
|
|
private String cleanContent(String text) {
|
if (!StringUtils.hasText(text)) {
|
return null;
|
}
|
return text.trim()
|
.replace("“", "")
|
.replace("”", "")
|
.replace("\"", "")
|
.replace("。", "")
|
.replace(";", "")
|
.replace(";", "")
|
.trim();
|
}
|
|
private String firstNonBlank(String first, String second) {
|
return StringUtils.hasText(first) ? first : second;
|
}
|
|
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);
|
}
|
}
|