From 61a2d2541fbe0129110e34e8359e3b96fb793bc7 Mon Sep 17 00:00:00 2001
From: zichun <26684461+reporkey@users.noreply.github.com>
Date: Tue, 21 Jul 2026 22:08:49 +0800
Subject: [PATCH] feat: lookupRecord (KgSearch) + audit log + Query (safe read-only SQL)
---
sql/ai_audit_log.sql | 16 ++++++++++++++++
src/main/java/com/xly/config/AgentConfig.java | 4 +++-
src/main/java/com/xly/service/AuditService.java | 42 ++++++++++++++++++++++++++++++++++++++++++
src/main/java/com/xly/service/FormResolverService.java | 103 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/main/java/com/xly/service/OpService.java | 5 ++++-
src/main/java/com/xly/service/SystemPromptService.java | 7 +++++--
src/main/java/com/xly/tool/ErpReadTool.java | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
src/main/java/com/xly/tool/QueryTool.java | 183 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/main/java/com/xly/web/OpController.java | 15 ++++++++++++++-
9 files changed, 442 insertions(+), 6 deletions(-)
create mode 100644 sql/ai_audit_log.sql
create mode 100644 src/main/java/com/xly/service/AuditService.java
create mode 100644 src/main/java/com/xly/service/FormResolverService.java
create mode 100644 src/main/java/com/xly/tool/QueryTool.java
diff --git a/sql/ai_audit_log.sql b/sql/ai_audit_log.sql
new file mode 100644
index 0000000..1e643fc
--- /dev/null
+++ b/sql/ai_audit_log.sql
@@ -0,0 +1,16 @@
+-- ai_audit_log:AI 操作不可变审计(金融类数据合规要求)。
+-- 记录谁、何时、什么动作、目标、结果。独立于 Langfuse(LLM tracing ≠ 业务审计)。
+-- 只追加、不更新不删除(应用层保证;生产可再加触发器/权限强制)。
+CREATE TABLE IF NOT EXISTS ai_audit_log (
+ sId bigint NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ tTime datetime NOT NULL,
+ sUserId varchar(64),
+ sConversationId varchar(96),
+ sAction varchar(24), -- propose | confirm | cancel | query
+ sTarget varchar(200), -- 目标表/记录/操作简述
+ sDetail text, -- JSON 或 SQL 明细
+ sResult varchar(16), -- ok | fail
+ sResultMsg varchar(500),
+ KEY idx_time (tTime),
+ KEY idx_user (sUserId, tTime)
+);
diff --git a/src/main/java/com/xly/config/AgentConfig.java b/src/main/java/com/xly/config/AgentConfig.java
index e1f1113..9206bcb 100644
--- a/src/main/java/com/xly/config/AgentConfig.java
+++ b/src/main/java/com/xly/config/AgentConfig.java
@@ -5,6 +5,7 @@ import com.xly.service.SystemPromptService;
import com.xly.tool.ErpReadTool;
import com.xly.tool.KgQueryTool;
import com.xly.tool.ProposeWriteTool;
+import com.xly.tool.QueryTool;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.ollama.OllamaStreamingChatModel;
import dev.langchain4j.service.AiServices;
@@ -50,11 +51,12 @@ public class AgentConfig {
KgQueryTool kgQueryTool,
ErpReadTool erpReadTool,
ProposeWriteTool proposeWriteTool,
+ QueryTool queryTool,
RedisChatMemoryStore memoryStore) {
String systemPrompt = systemPromptService.buildSystemPrompt();
return AiServices.builder(ReActAgent.class)
.streamingChatModel(agentStreamingModel())
- .tools(kgQueryTool, erpReadTool, proposeWriteTool)
+ .tools(kgQueryTool, erpReadTool, proposeWriteTool, queryTool)
.chatMemoryProvider(memoryId -> MessageWindowChatMemory.builder()
.id(memoryId)
.maxMessages(30)
diff --git a/src/main/java/com/xly/service/AuditService.java b/src/main/java/com/xly/service/AuditService.java
new file mode 100644
index 0000000..880c755
--- /dev/null
+++ b/src/main/java/com/xly/service/AuditService.java
@@ -0,0 +1,42 @@
+package com.xly.service;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Service;
+
+/**
+ * 业务审计:写操作 / SQL 的不可变留痕(谁、何时、什么动作、目标、结果)。
+ * 记账失败绝不阻断主流程(审计是旁路)。
+ */
+@Service
+public class AuditService {
+
+ private static final Logger log = LoggerFactory.getLogger(AuditService.class);
+
+ private final JdbcTemplate jdbc;
+
+ public AuditService(JdbcTemplate jdbc) {
+ this.jdbc = jdbc;
+ }
+
+ public void log(String userId, String conversationId, String action,
+ String target, String detail, boolean ok, String resultMsg) {
+ try {
+ jdbc.update(
+ "INSERT INTO ai_audit_log(tTime,sUserId,sConversationId,sAction,sTarget,sDetail,sResult,sResultMsg) " +
+ "VALUES(NOW(),?,?,?,?,?,?,?)",
+ userId, conversationId, action, trunc(target, 200), trunc(detail, 60000),
+ ok ? "ok" : "fail", trunc(resultMsg, 500));
+ } catch (Exception e) {
+ log.warn("audit write failed (action={}): {}", action, e.getMessage());
+ }
+ }
+
+ private static String trunc(String s, int max) {
+ if (s == null) {
+ return null;
+ }
+ return s.length() > max ? s.substring(0, max) : s;
+ }
+}
diff --git a/src/main/java/com/xly/service/FormResolverService.java b/src/main/java/com/xly/service/FormResolverService.java
new file mode 100644
index 0000000..8c9f789
--- /dev/null
+++ b/src/main/java/com/xly/service/FormResolverService.java
@@ -0,0 +1,103 @@
+package com.xly.service;
+
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Service;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 表单 / 字段解析(KgSearch 内核)—— 把「实体类型 / 字段中文名」解析到具体的
+ * 主表 / formId / moduleId / 技术列名,供 Read 与 Write 共用,集中一处避免各工具各猜一套。
+ *
+ *
数据全部来自已物化的知识图谱视图:{@code viw_ai_useful_forms} / {@code viw_kg_form}
+ * / {@code viw_kg_field_dict}。
+ */
+@Service
+public class FormResolverService {
+
+ private final JdbcTemplate jdbc;
+
+ public FormResolverService(JdbcTemplate jdbc) {
+ this.jdbc = jdbc;
+ }
+
+ /**
+ * 定位某实体的**可改主表**:该实体名下、table 类型、排除报表视图(viw_*)、按 AI工具/连接度/名称长度
+ * 取最常用的一张。返回含 sFormId / sModuleId / sDataSource 的行,找不到返回 null。
+ */
+ public Map resolveMasterForm(String entityKeyword) {
+ if (isBlank(entityKeyword)) {
+ return null;
+ }
+ try {
+ List