mirror of
https://gitcode.com/ageerle/ruoyi-ai.git
synced 2026-09-16 01:44:57 +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
|
- 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
|
- Only after understanding the database schema can you construct and execute appropriate SQL queries
|
||||||
- This is mandatory and applies to all queries without exception
|
- 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("""
|
@UserMessage("""
|
||||||
Answer the following question: {{query}}
|
Answer the following question: {{query}}
|
||||||
|
|||||||
@@ -8,9 +8,14 @@ import java.util.ArrayList;
|
|||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
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 javax.sql.DataSource;
|
||||||
|
|
||||||
|
import org.ruoyi.agent.manager.TableSchemaManager;
|
||||||
import org.ruoyi.common.core.utils.SpringUtils;
|
import org.ruoyi.common.core.utils.SpringUtils;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@@ -54,6 +59,22 @@ public class ExecuteSqlQueryTool implements BuiltinToolProvider {
|
|||||||
return "Error: Only SELECT queries are allowed for security reasons";
|
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 {
|
try {
|
||||||
DataSource dataSource = getDataSource();
|
DataSource dataSource = getDataSource();
|
||||||
if (dataSource == null) {
|
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.ChatModel;
|
||||||
import dev.langchain4j.model.chat.StreamingChatModel;
|
import dev.langchain4j.model.chat.StreamingChatModel;
|
||||||
import dev.langchain4j.model.chat.response.ChatResponse;
|
import dev.langchain4j.model.chat.response.ChatResponse;
|
||||||
|
import dev.langchain4j.model.chat.response.PartialThinking;
|
||||||
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
|
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
|
||||||
import dev.langchain4j.rag.content.Content;
|
import dev.langchain4j.rag.content.Content;
|
||||||
import dev.langchain4j.rag.content.retriever.ContentRetriever;
|
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
|
@Override
|
||||||
public void onCompleteResponse(ChatResponse completeResponse) {
|
public void onCompleteResponse(ChatResponse completeResponse) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -32,10 +32,13 @@ public class OllamaServiceImpl implements AbstractChatService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) {
|
public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) {
|
||||||
|
boolean thinkingEnabled = Boolean.TRUE.equals(chatRequest.getEnableThinking());
|
||||||
return OllamaStreamingChatModel.builder()
|
return OllamaStreamingChatModel.builder()
|
||||||
.baseUrl(chatModelVo.getApiHost())
|
.baseUrl(chatModelVo.getApiHost())
|
||||||
.modelName(chatModelVo.getModelName())
|
.modelName(chatModelVo.getModelName())
|
||||||
.listeners(List.of(new MyChatModelListener()))
|
.listeners(List.of(new MyChatModelListener()))
|
||||||
|
.think(thinkingEnabled)
|
||||||
|
.returnThinking(thinkingEnabled)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user