feat: 打通 thinking 流式链路 + SqlAgent 表白名单校验

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 <noreply@anthropic.com>
This commit is contained in:
ageerle
2026-07-21 15:31:14 +08:00
parent 911ae6cb1f
commit a69d1f51b9
4 changed files with 54 additions and 0 deletions

View File

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

View File

@@ -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<String> allowedTables = schemaManager.getAllowedTableNames();
if (allowedTables.isEmpty()) {
return "Error: 当前未配置可查询的数据库表无法执行任何SQL查询。请联系管理员配置 AGENT_ALLOWED_TABLES";
}
Set<String> allowedSet = allowedTables.stream()
.map(String::toLowerCase)
.collect(Collectors.toSet());
Set<String> 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<String> extractTableNames(String upperSql) {
Set<String> 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;
}
/**
* 格式化查询结果
* 返回清晰的表格格式,展示关键数据

View File

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

View File

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