From 577d2b51d7d585d65f72b079f72923c365fc49c3 Mon Sep 17 00:00:00 2001 From: tanglong <842690096@qq.com> Date: Tue, 21 Jul 2026 16:25:10 +0800 Subject: [PATCH] =?UTF-8?q?=E6=97=A5=E6=9C=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../semantic/SemanticDateRangeResolver.java | 126 ++++++++++++++++-- .../semantic/SemanticRecognitionService.java | 2 +- .../CustomerDailyReportIntentHandler.java | 3 +- ...CustomerOperationsReportIntentHandler.java | 6 +- .../SemanticDateRangeResolverTest.java | 37 +++++ 5 files changed, 158 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/company/agentfoundation/semantic/SemanticDateRangeResolver.java b/src/main/java/com/company/agentfoundation/semantic/SemanticDateRangeResolver.java index 2bd3fec..ffb68bf 100644 --- a/src/main/java/com/company/agentfoundation/semantic/SemanticDateRangeResolver.java +++ b/src/main/java/com/company/agentfoundation/semantic/SemanticDateRangeResolver.java @@ -16,8 +16,22 @@ import java.util.regex.Pattern; public class SemanticDateRangeResolver { private static final Pattern EXPLICIT_DATE = Pattern.compile( "(? parameters, LocalDate today) { + String directText = userText == null ? "" : userText; + DateRange textRange = resolveUserText(directText, today); + if (textRange != null) { + return textRange; + } + LocalDate parameterStart = parse(parameters.get("startDate")); LocalDate parameterEnd = parse(parameters.get("endDate")); if (parameterStart != null || parameterEnd != null) { @@ -26,33 +40,83 @@ public class SemanticDateRangeResolver { return ordered(start, end); } - List explicitDates = explicitDates(userText); - if (!explicitDates.isEmpty()) { - return ordered(explicitDates.get(0), explicitDates.size() > 1 ? explicitDates.get(1) : explicitDates.get(0)); - } - String preset = parameters.getOrDefault("datePreset", ""); - String text = (userText == null ? "" : userText) + " " + preset; - if (containsAny(text, "昨日", "昨天", "YESTERDAY")) { + if (containsAny(preset, "YESTERDAY")) { return new DateRange(today.minusDays(1), today.minusDays(1)); } - if (containsAny(text, "上周", "LAST_WEEK")) { + if (containsAny(preset, "LAST_WEEK")) { LocalDate start = today.minusWeeks(1).with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)); return new DateRange(start, start.plusDays(6)); } - if (containsAny(text, "本周", "这周", "THIS_WEEK")) { + if (containsAny(preset, "THIS_WEEK")) { return new DateRange(today.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)), today); } - if (containsAny(text, "上月", "上个月", "LAST_MONTH")) { + if (containsAny(preset, "LAST_MONTH")) { YearMonth month = YearMonth.from(today).minusMonths(1); return new DateRange(month.atDay(1), month.atEndOfMonth()); } - if (containsAny(text, "本月", "这个月", "THIS_MONTH")) { + if (containsAny(preset, "THIS_MONTH")) { return new DateRange(today.withDayOfMonth(1), today); } + if (containsAny(preset, "LAST_YEAR")) { + int year = today.getYear() - 1; + return new DateRange(LocalDate.of(year, 1, 1), LocalDate.of(year, 12, 31)); + } + if (containsAny(preset, "THIS_YEAR")) { + return new DateRange(today.withDayOfYear(1), today); + } return new DateRange(today, today); } + public boolean exceedsMaximumDays(String userText, DateRange range, int maximumDays) { + if (maximumDays <= 0) { + throw new IllegalArgumentException("maximumDays must be positive"); + } + boolean wholeYearRequest = containsAny(userText == null ? "" : userText, "本年", "今年", "全年", "去年", "上年") + && !(userText != null && userText.contains("月")); + return wholeYearRequest && maximumDays < 365 + || range.startDate().plusDays(maximumDays - 1L).isBefore(range.endDate()); + } + + private DateRange resolveUserText(String text, LocalDate today) { + List dates = explicitDates(text); + if (!dates.isEmpty()) { + return ordered(dates.get(0), dates.size() > 1 ? dates.get(1) : dates.get(0)); + } + YearMonth month = requestedMonth(text, today); + if (month != null) { + return new DateRange(month.atDay(1), month.atEndOfMonth()); + } + if (containsAny(text, "昨日", "昨天")) { + return new DateRange(today.minusDays(1), today.minusDays(1)); + } + if (containsAny(text, "上周")) { + LocalDate start = today.minusWeeks(1).with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)); + return new DateRange(start, start.plusDays(6)); + } + if (containsAny(text, "本周", "这周")) { + return new DateRange(today.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)), today); + } + if (containsAny(text, "上月", "上个月")) { + YearMonth lastMonth = YearMonth.from(today).minusMonths(1); + return new DateRange(lastMonth.atDay(1), lastMonth.atEndOfMonth()); + } + if (containsAny(text, "本月", "这个月")) { + return new DateRange(today.withDayOfMonth(1), today); + } + if (containsAny(text, "去年", "上年")) { + int lastYear = today.getYear() - 1; + return new DateRange(LocalDate.of(lastYear, 1, 1), LocalDate.of(lastYear, 12, 31)); + } + if (containsAny(text, "本年", "今年", "全年")) { + return new DateRange(today.withDayOfYear(1), today); + } + if (containsAny(text, "今日", "今天")) { + return new DateRange(today, today); + } + return null; + } + private List explicitDates(String text) { List dates = new ArrayList<>(); Matcher matcher = EXPLICIT_DATE.matcher(text == null ? "" : text); @@ -77,6 +141,46 @@ public class SemanticDateRangeResolver { } } + private YearMonth requestedMonth(String text, LocalDate today) { + String safeText = text == null ? "" : text; + Matcher yearMonthMatcher = EXPLICIT_YEAR_MONTH.matcher(safeText); + if (yearMonthMatcher.find()) { + try { + return YearMonth.of(Integer.parseInt(yearMonthMatcher.group(1)), + Integer.parseInt(yearMonthMatcher.group(2))); + } catch (Exception ignored) { + return null; + } + } + Matcher arabicMatcher = ARABIC_MONTH.matcher(safeText); + if (arabicMatcher.find()) { + return YearMonth.of(today.getYear(), Integer.parseInt(arabicMatcher.group(1))); + } + Matcher chineseMatcher = CHINESE_MONTH.matcher(safeText); + if (chineseMatcher.find()) { + return YearMonth.of(today.getYear(), chineseMonth(chineseMatcher.group(1))); + } + return null; + } + + private int chineseMonth(String value) { + return switch (value) { + case "一" -> 1; + case "二" -> 2; + case "三" -> 3; + case "四" -> 4; + case "五" -> 5; + case "六" -> 6; + case "七" -> 7; + case "八" -> 8; + case "九" -> 9; + case "十" -> 10; + case "十一" -> 11; + case "十二" -> 12; + default -> throw new IllegalArgumentException("invalid Chinese month: " + value); + }; + } + private DateRange ordered(LocalDate first, LocalDate second) { return first.isAfter(second) ? new DateRange(second, first) : new DateRange(first, second); } diff --git a/src/main/java/com/company/agentfoundation/semantic/SemanticRecognitionService.java b/src/main/java/com/company/agentfoundation/semantic/SemanticRecognitionService.java index 389cd75..cdaa572 100644 --- a/src/main/java/com/company/agentfoundation/semantic/SemanticRecognitionService.java +++ b/src/main/java/com/company/agentfoundation/semantic/SemanticRecognitionService.java @@ -108,7 +108,7 @@ public class SemanticRecognitionService { %s 只输出一个 JSON 对象,禁止 Markdown、解释或额外文字: - {"intent":"意图编码或UNKNOWN","confidence":0到1,"parameters":{"startDate":"yyyy-MM-dd","endDate":"yyyy-MM-dd","datePreset":"TODAY|YESTERDAY|THIS_WEEK|LAST_WEEK|THIS_MONTH|LAST_MONTH"}} + {"intent":"意图编码或UNKNOWN","confidence":0到1,"parameters":{"startDate":"yyyy-MM-dd","endDate":"yyyy-MM-dd","datePreset":"TODAY|YESTERDAY|THIS_WEEK|LAST_WEEK|THIS_MONTH|LAST_MONTH|THIS_YEAR|LAST_YEAR"}} 规则: 1. 只允许选择上面列出的意图,否则返回 UNKNOWN。 diff --git a/src/main/java/com/company/agentfoundation/semantic/handler/CustomerDailyReportIntentHandler.java b/src/main/java/com/company/agentfoundation/semantic/handler/CustomerDailyReportIntentHandler.java index 8c46541..6c734ed 100644 --- a/src/main/java/com/company/agentfoundation/semantic/handler/CustomerDailyReportIntentHandler.java +++ b/src/main/java/com/company/agentfoundation/semantic/handler/CustomerDailyReportIntentHandler.java @@ -51,8 +51,7 @@ public class CustomerDailyReportIntentHandler implements SemanticIntentHandler { @Override public String handle(SemanticRequestContext context, SemanticRecognition recognition) { - SemanticDateRangeResolver.DateRange range = dateRangeResolver.resolve( - context.userText(), recognition.parameters(), context.requestTime().toLocalDate()); + SemanticDateRangeResolver.DateRange range = dateRangeResolver.resolve(context, recognition); try { JsonNode report = objectMapper.valueToTree(reportService.generate( new CustomerDailyReportRequest(range.startDate(), range.endDate(), context.wecomUserId()))); diff --git a/src/main/java/com/company/agentfoundation/semantic/handler/WecomCustomerOperationsReportIntentHandler.java b/src/main/java/com/company/agentfoundation/semantic/handler/WecomCustomerOperationsReportIntentHandler.java index 4e60288..1a0ce4a 100644 --- a/src/main/java/com/company/agentfoundation/semantic/handler/WecomCustomerOperationsReportIntentHandler.java +++ b/src/main/java/com/company/agentfoundation/semantic/handler/WecomCustomerOperationsReportIntentHandler.java @@ -48,8 +48,10 @@ public class WecomCustomerOperationsReportIntentHandler implements SemanticInten @Override public String handle(SemanticRequestContext context, SemanticRecognition recognition) { - SemanticDateRangeResolver.DateRange range = dateRangeResolver.resolve( - context.userText(), recognition.parameters(), context.requestTime().toLocalDate()); + SemanticDateRangeResolver.DateRange range = dateRangeResolver.resolve(context, recognition); + if (dateRangeResolver.exceedsMaximumDays(context.userText(), range, 31)) { + return "企业微信客户运营日报一次只能查询一个月的数据。可输入:一月企业微信客户运营日报。"; + } try { JsonNode report = objectMapper.valueToTree(reportService.generate( new WecomCustomerOperationsReportRequest(range.startDate(), range.endDate(), context.wecomUserId()))); diff --git a/src/test/java/com/company/agentfoundation/semantic/SemanticDateRangeResolverTest.java b/src/test/java/com/company/agentfoundation/semantic/SemanticDateRangeResolverTest.java index 527d837..b42a368 100644 --- a/src/test/java/com/company/agentfoundation/semantic/SemanticDateRangeResolverTest.java +++ b/src/test/java/com/company/agentfoundation/semantic/SemanticDateRangeResolverTest.java @@ -25,6 +25,14 @@ class SemanticDateRangeResolverTest { resolver.resolve("查看本周客资日报", Map.of(), today)); } + @Test + void resolvesThisMonthFromUserTextBeforeModelDates() { + assertEquals( + new SemanticDateRangeResolver.DateRange(LocalDate.of(2026, 7, 1), today), + resolver.resolve("本月企业微信客户运营日报", + Map.of("startDate", "2025-06-01", "endDate", "2025-06-30"), today)); + } + @Test void resolvesExplicitRange() { assertEquals( @@ -38,4 +46,33 @@ class SemanticDateRangeResolverTest { new SemanticDateRangeResolver.DateRange(LocalDate.of(2026, 7, 1), LocalDate.of(2026, 7, 15)), resolver.resolve("查看客资日报", Map.of("startDate", "2026-07-01", "endDate", "2026-07-15"), today)); } + + @Test + void resolvesChineseNamedMonth() { + assertEquals( + new SemanticDateRangeResolver.DateRange(LocalDate.of(2026, 1, 1), LocalDate.of(2026, 1, 31)), + resolver.resolve("一月企业微信客户运营日报", Map.of(), today)); + } + + @Test + void resolvesExplicitYearMonth() { + assertEquals( + new SemanticDateRangeResolver.DateRange(LocalDate.of(2025, 12, 1), LocalDate.of(2025, 12, 31)), + resolver.resolve("查看2025年12月企业微信客户运营日报", Map.of(), today)); + } + + @Test + void resolvesThisYearForAllReportHandlers() { + assertEquals( + new SemanticDateRangeResolver.DateRange(LocalDate.of(2026, 1, 1), today), + resolver.resolve("本年运营日报", Map.of(), today)); + } + + @Test + void appliesReusableMaximumRangePolicy() { + SemanticDateRangeResolver.DateRange yearRange = resolver.resolve("本年运营日报", Map.of(), today); + assertEquals(true, resolver.exceedsMaximumDays("本年运营日报", yearRange, 31)); + assertEquals(false, resolver.exceedsMaximumDays("本月运营日报", + resolver.resolve("本月运营日报", Map.of(), today), 31)); + } }