From a69d1f51b9f5061f740367188735c27cb6cff090 Mon Sep 17 00:00:00 2001 From: ageerle Date: Tue, 21 Jul 2026 15:31:14 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=89=93=E9=80=9A=20thinking=20?= =?UTF-8?q?=E6=B5=81=E5=BC=8F=E9=93=BE=E8=B7=AF=20+=20SqlAgent=20=E8=A1=A8?= =?UTF-8?q?=E7=99=BD=E5=90=8D=E5=8D=95=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. OllamaServiceImpl 补 .think() + .returnThinking(),对齐其他 provider (#306) 2. ChatServiceFacade.createCombinedHandler 新增 onPartialThinking, reasoning 内容经 SseMessageUtils.sendReasoning 流式推到前端 (#303) 3. SqlAgent 系统提示词追加无表时禁止执行 SQL 的规则 (#279) 4. ExecuteSqlQueryTool 代码层硬拦截:空白名单直接拒绝, 有白名单则校验 SQL 中 FROM/JOIN 引用的表名 (#279) Closes #303 #306 #279 Co-Authored-By: Claude --- .../main/java/org/ruoyi/agent/SqlAgent.java | 3 ++ .../ruoyi/agent/tool/ExecuteSqlQueryTool.java | 36 +++++++++++++++++++ .../service/chat/impl/ChatServiceFacade.java | 12 +++++++ .../chat/impl/provider/OllamaServiceImpl.java | 3 ++ 4 files changed, 54 insertions(+) diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/SqlAgent.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/SqlAgent.java index e21b61c1..6cadc94c 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/SqlAgent.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/SqlAgent.java @@ -30,6 +30,9 @@ public interface SqlAgent { - You MUST ALWAYS use queryAllTables first to query all tables in the database before executing any SQL queries - Only after understanding the database schema can you construct and execute appropriate SQL queries - This is mandatory and applies to all queries without exception + - If queryAllTables returns NO tables or an empty list, you MUST NOT call executeSql or queryTableSchema + - When no tables are available, inform the user: "当前未配置可查询的数据库表,请联系管理员配置" + - NEVER attempt to execute any SQL query (including SELECT * FROM xxx) without first confirming available tables """) @UserMessage(""" Answer the following question: {{query}} diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/tool/ExecuteSqlQueryTool.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/tool/ExecuteSqlQueryTool.java index 1b9079f1..a17bf545 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/tool/ExecuteSqlQueryTool.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/tool/ExecuteSqlQueryTool.java @@ -8,9 +8,14 @@ import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; import javax.sql.DataSource; +import org.ruoyi.agent.manager.TableSchemaManager; import org.ruoyi.common.core.utils.SpringUtils; import org.springframework.stereotype.Component; @@ -54,6 +59,22 @@ public class ExecuteSqlQueryTool implements BuiltinToolProvider { return "Error: Only SELECT queries are allowed for security reasons"; } + // 校验表白名单:未配置表时直接拒绝,已配置则校验 SQL 中引用的表 + TableSchemaManager schemaManager = SpringUtils.getBean(TableSchemaManager.class); + List allowedTables = schemaManager.getAllowedTableNames(); + if (allowedTables.isEmpty()) { + return "Error: 当前未配置可查询的数据库表,无法执行任何SQL查询。请联系管理员配置 AGENT_ALLOWED_TABLES"; + } + Set allowedSet = allowedTables.stream() + .map(String::toLowerCase) + .collect(Collectors.toSet()); + Set referencedTables = extractTableNames(upperSql); + for (String table : referencedTables) { + if (!allowedSet.contains(table.toLowerCase())) { + return "Error: 表 " + table + " 不在允许查询的表列表中。允许查询的表: " + String.join(", ", allowedTables); + } + } + try { DataSource dataSource = getDataSource(); if (dataSource == null) { @@ -99,6 +120,21 @@ public class ExecuteSqlQueryTool implements BuiltinToolProvider { } } + /** + * 从 SQL 中提取引用的表名(FROM / JOIN 后的标识符) + * 覆盖 FROM t1, t2 / FROM t1 JOIN t2 / FROM `t1` 等常见写法 + */ + private Set extractTableNames(String upperSql) { + Set tables = new java.util.HashSet<>(); + // 匹配 FROM 或 JOIN 后面的表名(支持反引号包裹) + Pattern pattern = Pattern.compile("(?:FROM|JOIN)\\s+`?([A-Z0-9_]+)`?", Pattern.CASE_INSENSITIVE); + Matcher matcher = pattern.matcher(upperSql); + while (matcher.find()) { + tables.add(matcher.group(1)); + } + return tables; + } + /** * 格式化查询结果 * 返回清晰的表格格式,展示关键数据 diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatServiceFacade.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatServiceFacade.java index 69c85c9b..7e3dfd66 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatServiceFacade.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatServiceFacade.java @@ -17,6 +17,7 @@ import dev.langchain4j.memory.chat.MessageWindowChatMemory; import dev.langchain4j.model.chat.ChatModel; import dev.langchain4j.model.chat.StreamingChatModel; import dev.langchain4j.model.chat.response.ChatResponse; +import dev.langchain4j.model.chat.response.PartialThinking; import dev.langchain4j.model.chat.response.StreamingChatResponseHandler; import dev.langchain4j.rag.content.Content; import dev.langchain4j.rag.content.retriever.ContentRetriever; @@ -688,6 +689,17 @@ public class ChatServiceFacade implements IChatService { } } + @Override + public void onPartialThinking(PartialThinking partialThinking) { + // 发送推理内容到 SSE(前端通过 reasoning 事件监听) + SseMessageUtils.sendReasoning(userId, partialThinking.text()); + + // 转发给外部 handler + if (externalHandler != null) { + externalHandler.onPartialThinking(partialThinking); + } + } + @Override public void onCompleteResponse(ChatResponse completeResponse) { try { diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/OllamaServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/OllamaServiceImpl.java index 240ed215..feda2f30 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/OllamaServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/OllamaServiceImpl.java @@ -32,10 +32,13 @@ public class OllamaServiceImpl implements AbstractChatService { @Override public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) { + boolean thinkingEnabled = Boolean.TRUE.equals(chatRequest.getEnableThinking()); return OllamaStreamingChatModel.builder() .baseUrl(chatModelVo.getApiHost()) .modelName(chatModelVo.getModelName()) .listeners(List.of(new MyChatModelListener())) + .think(thinkingEnabled) + .returnThinking(thinkingEnabled) .build(); }