From 172c2a098f721d792c4e88eca79442218f4ec7c7 Mon Sep 17 00:00:00 2001 From: Lee <1633292@qq.com> Date: Mon, 8 Jun 2026 23:20:38 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/l/tracecd/dto/VoiceResponse.java | 57 +- .../com/l/tracecd/service/LlmService.java | 9 +- .../com/l/tracecd/service/VoiceService.java | 90 ++- src/main/resources/application.yml | 2 +- src/main/resources/templates/browse.html | 478 +++++------ src/main/resources/templates/index.html | 753 +++++++++--------- src/main/resources/templates/login.html | 152 +++- 7 files changed, 864 insertions(+), 677 deletions(-) diff --git a/src/main/java/com/l/tracecd/dto/VoiceResponse.java b/src/main/java/com/l/tracecd/dto/VoiceResponse.java index 9beb2f4..cdd3726 100644 --- a/src/main/java/com/l/tracecd/dto/VoiceResponse.java +++ b/src/main/java/com/l/tracecd/dto/VoiceResponse.java @@ -1,5 +1,8 @@ package com.l.tracecd.dto; +import java.util.List; +import java.util.Map; + /** * 语音处理响应 DTO * 返回给前端的统一响应结构 @@ -21,6 +24,9 @@ public class VoiceResponse { /** 错误信息 */ private String error; + /** 查询结果记录列表 */ + private List> records; + public static VoiceResponse recordSuccess(String category) { VoiceResponse r = new VoiceResponse(); r.type = "RECORD"; @@ -30,11 +36,12 @@ public class VoiceResponse { return r; } - public static VoiceResponse queryResult(String message) { + public static VoiceResponse queryResult(String summary, List> records) { VoiceResponse r = new VoiceResponse(); r.type = "QUERY"; r.success = true; - r.message = message; + r.message = summary; + r.records = records; return r; } @@ -55,43 +62,21 @@ public class VoiceResponse { return r; } - public String getType() { - return type; - } + public String getType() { return type; } + public void setType(String type) { this.type = type; } - public void setType(String type) { - this.type = type; - } + public String getMessage() { return message; } + public void setMessage(String message) { this.message = message; } - public String getMessage() { - return message; - } + public String getCategory() { return category; } + public void setCategory(String category) { this.category = category; } - public void setMessage(String message) { - this.message = message; - } + public boolean isSuccess() { return success; } + public void setSuccess(boolean success) { this.success = success; } - public String getCategory() { - return category; - } + public String getError() { return error; } + public void setError(String error) { this.error = error; } - public void setCategory(String category) { - this.category = category; - } - - public boolean isSuccess() { - return success; - } - - public void setSuccess(boolean success) { - this.success = success; - } - - public String getError() { - return error; - } - - public void setError(String error) { - this.error = error; - } + public List> getRecords() { return records; } + public void setRecords(List> records) { this.records = records; } } diff --git a/src/main/java/com/l/tracecd/service/LlmService.java b/src/main/java/com/l/tracecd/service/LlmService.java index 73fdbdf..3380ea1 100644 --- a/src/main/java/com/l/tracecd/service/LlmService.java +++ b/src/main/java/com/l/tracecd/service/LlmService.java @@ -139,13 +139,8 @@ public class LlmService { ## 查询意图处理 当用户意图是查询时,**必须调用 query_daily_records 函数**执行 SQL 查询。 - - 人物、日期、内容、金额相关字段查询 - - 查询完成后根据返回数据生成回复: - * 多条数据用 markdown 表格展示,表格上方展示金额汇总 - * 一条数据直接描述 - * 无数据告知用户未找到 - * 回复要友好自然 - + - 查询结果的总结和格式化由后续系统自动处理,你只需调用函数即可 + ## 聊天意图处理 当用户输入与记账无关时,直接友好回复。 diff --git a/src/main/java/com/l/tracecd/service/VoiceService.java b/src/main/java/com/l/tracecd/service/VoiceService.java index e1c5579..c445490 100644 --- a/src/main/java/com/l/tracecd/service/VoiceService.java +++ b/src/main/java/com/l/tracecd/service/VoiceService.java @@ -12,7 +12,11 @@ import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import javax.sql.DataSource; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -129,7 +133,7 @@ public class VoiceService { } /** - * 处理查询意图:执行 function call → 返回结果给 LLM → 获取格式化回复 + * 处理查询意图:执行 function call → 返回结果给 LLM 生成总结 → 构造结构化响应 */ private VoiceResponse handleQueryIntent(DeepSeekResponse.ResponseMessage response, String originalText) throws Exception { @@ -151,15 +155,45 @@ public class VoiceService { String validatedSql = sqlValidationService.validateSelect(sql); String queryResultJson = executeSelectSql(validatedSql); - // 构建对话历史发送回 LLM + // 解析查询结果为结构化数据,并格式化日期字段 + List> records = parseQueryResult(queryResultJson); + + // 将查询结果发给 LLM,只生成一句总结 List messages = buildToolResultMessages(originalText, response, toolCall, queryResultJson); DeepSeekResponse.ResponseMessage finalResponse = llmService.continueWithToolResult(messages); String replyContent = finalResponse.getContent(); + String summary; if (replyContent == null || replyContent.isBlank()) { - replyContent = "查询完成,但未能生成回复"; + summary = "查询完成"; + } else { + // 尝试从 LLM 回复中提取 JSON + summary = extractJsonField(replyContent, "summary"); + if (summary == null) { + summary = replyContent; // 降级:直接使用原始回复 + } + } + + return VoiceResponse.queryResult(summary, records); + } + + /** + * 从 LLM 返回的 JSON 字符串中提取指定字段 + */ + private String extractJsonField(String content, String field) { + try { + // 尝试直接解析 + String trimmed = content.trim(); + // 去掉可能的 markdown 代码块包裹 + trimmed = trimmed.replaceAll("^```json\\s*", "").replaceAll("^```\\s*", "").replaceAll("```$", "").trim(); + Map map = objectMapper.readValue(trimmed, + new TypeReference>() {}); + Object value = map.get(field); + return value != null ? value.toString() : null; + } catch (Exception e) { + log.debug("解析 LLM JSON 回复失败,将使用原始文本: {}", e.getMessage()); + return null; } - return VoiceResponse.queryResult(replyContent); } /** @@ -189,6 +223,39 @@ public class VoiceService { } } + /** + * 解析查询结果 JSON,格式化日期字段为 M-d HH:mm + */ + private List> parseQueryResult(String queryResultJson) throws Exception { + List> raw = objectMapper.readValue(queryResultJson, + new TypeReference>>() {}); + DateTimeFormatter outFmt = DateTimeFormatter.ofPattern("M-d HH:mm"); + + List> result = new ArrayList<>(); + for (Map row : raw) { + LinkedHashMap formatted = new LinkedHashMap<>(); + for (Map.Entry entry : row.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + // 保留全部字段(包括 category,前端用于分类着色) + // 格式化时间字段 + if (value instanceof String && (key.toLowerCase().contains("time") || key.toLowerCase().contains("record"))) { + try { + LocalDateTime dt = LocalDateTime.parse((String) value); + value = dt.format(outFmt); + } catch (Exception ignored) {} + } + // 金额字段转为数字 + if (value instanceof BigDecimal) { + value = ((BigDecimal) value).doubleValue(); + } + formatted.put(key, value); + } + result.add(formatted); + } + return result; + } + /** * 构建 function call 的完整对话历史 */ @@ -199,8 +266,7 @@ public class VoiceService { List messages = new ArrayList<>(); // system - messages.add(new DeepSeekRequest.Message("system", - "你是一个日常记账助手。请根据查询结果友好地回答用户的问题。多条数据请用 markdown 表格展示,表格上方展示金额汇总。")); + messages.add(new DeepSeekRequest.Message("system", CONTENT_TEMPLATE)); // user original messages.add(new DeepSeekRequest.Message("user", originalText)); @@ -218,4 +284,16 @@ public class VoiceService { return messages; } + + private static final String CONTENT_TEMPLATE = """ + 你是一个日常记账助手。根据用户问题和查询结果,生成一句友好的总结回复。 + + 请严格只返回以下 JSON 格式,不要包含任何其他内容(不要 markdown 代码块): + {"summary": "你的总结,包含记录条数(如共X条)和金额汇总(合计 ¥XXX.XX),语气友好自然"} + + 示例返回: + {"summary": "共3条记录,合计 ¥156.00,"} + {"summary": "只找到1条记录,金额 ¥20.00"} + {"summary": "没有找到匹配的记录哦"} + """; } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index e168461..ccff5c0 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -63,7 +63,7 @@ logging: deepseek: api-key: ${DEEPSEEK_API_KEY:sk-732e5f5f1f07454492022401f0a2bf40} base-url: https://api.deepseek.com/chat/completions - model: deepseek-v4-pro + model: deepseek-v4-flash # MIMO ASR API mimo: diff --git a/src/main/resources/templates/browse.html b/src/main/resources/templates/browse.html index 728b14b..1ab1aee 100644 --- a/src/main/resources/templates/browse.html +++ b/src/main/resources/templates/browse.html @@ -2,159 +2,183 @@ - + + + 浏览事项 - TraceCD @@ -162,7 +186,7 @@ @@ -170,50 +194,45 @@
-
-
- - -
-
- - -
-
- - -
-
- - + +
+
人物
+
+
+ +
+
分类
+
+
+ +
+
地点
+
+
+ +
+
时间范围
+
+ +
-
-
- - -
-
- - -
+ +
+ +
+
- -
-
- 合计: + 合计
@@ -221,55 +240,60 @@
-
-

查询中...

+
+

查询中...

-
- +
+
- - - - - - +
-
点击"查询"按钮查看事项数据
+
点击「查询」查看事项数据
diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index 4a7c2c8..ee10711 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -2,578 +2,565 @@ - + + + TraceCD - 日常记录 - -
- -
-
-
- -
- -

按住右下角麦克风按钮开始语音录入或查询

+ 🎤 +
语音记账助手
+
按住下方按钮说话
录入事项或查询记录
- -
- -
- -
- -
按住说话,松开发送
+
+
+ +
+
+
按住说话,松开发送
+
+ + 处理中 +
+