This commit is contained in:
tanglong 2026-07-21 16:25:10 +08:00
parent 0d813d73f0
commit 577d2b51d7
5 changed files with 158 additions and 16 deletions

View File

@ -16,8 +16,22 @@ import java.util.regex.Pattern;
public class SemanticDateRangeResolver {
private static final Pattern EXPLICIT_DATE = Pattern.compile(
"(?<!\\d)(\\d{4})[-/.年](\\d{1,2})[-/.月](\\d{1,2})日?");
private static final Pattern EXPLICIT_YEAR_MONTH = Pattern.compile(
"(?<!\\d)(\\d{4})[-/.年](\\d{1,2})月?");
private static final Pattern ARABIC_MONTH = Pattern.compile("(?<!\\d)(1[0-2]|0?[1-9])月");
private static final Pattern CHINESE_MONTH = Pattern.compile("(十二|十一|十|九|八|七|六|五|四|三|二|一)月");
public DateRange resolve(SemanticRequestContext context, SemanticRecognition recognition) {
return resolve(context.userText(), recognition.parameters(), context.requestTime().toLocalDate());
}
public DateRange resolve(String userText, Map<String, String> 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<LocalDate> 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<LocalDate> 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<LocalDate> explicitDates(String text) {
List<LocalDate> 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);
}

View File

@ -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。

View File

@ -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())));

View File

@ -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())));

View File

@ -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));
}
}