mirror of
https://gitcode.com/ageerle/ruoyi-ai.git
synced 2026-09-13 00:14:59 +00:00
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:
@@ -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}}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化查询结果
|
||||
* 返回清晰的表格格式,展示关键数据
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user