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