修改数据库读取工具

This commit is contained in:
zhang
2026-02-24 16:07:18 +08:00
parent f25ebdf9ec
commit 26bcfbba8a
9 changed files with 214 additions and 211 deletions

View File

@@ -58,9 +58,16 @@ spring:
driverClassName: com.mysql.cj.jdbc.Driver
# jdbc 所有参数配置参考 https://lionli.blog.csdn.net/article/details/122018562
# rewriteBatchedStatements=true 批处理优化 大幅提升批量插入更新删除性能(对数据库有性能损耗 使用批量操作应考虑性能问题)
url: jdbc:mysql://127.0.0.1:3306/mysql?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
url: jdbc:mysql://127.0.0.1:3306/ruoyi_ai_agent?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
username: root
password: 123456
password: Qzhang450000
agent:
url: jdbc:mysql://127.0.0.1:3306/yunding?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
# url: jdbc:mysql://localhost:3306/agent_db
username: root
password: Qzhang450000
driverClassName: com.mysql.cj.jdbc.Driver
hikari:
# 最大连接池数量
maxPoolSize: 20
@@ -77,11 +84,7 @@ spring:
# 多久检查一次连接的活性
keepaliveTime: 30000
agent:
mysql:
url: jdbc:mysql://localhost:3306/ruoyi-ai-agent?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
username: root
password: root
--- # 上传文件地址
sys:
@@ -265,3 +268,4 @@ justauth:
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=gitea
AGENT_ALLOWED_TABLES: "abtest_rule,abtest_project,agent_ban_log,agent_ban_logs,agent_install_sub_task,agent_install_sum_task,agent_install_task"

View File

@@ -1,44 +1,44 @@
package org.ruoyi.agent.config;
// package org.ruoyi.agent.config;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import javax.sql.DataSource;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
// import com.zaxxer.hikari.HikariConfig;
// import com.zaxxer.hikari.HikariDataSource;
// import javax.sql.DataSource;
// import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
// import org.springframework.boot.context.properties.EnableConfigurationProperties;
// import org.springframework.context.annotation.Bean;
// import org.springframework.context.annotation.Configuration;
/**
* Agent MySQL 数据源配置
* 为 Agent 配置独立的 MySQL 数据库连接池HikariCP
*
* 仅在 agent.mysql.enabled=true 时启用
*/
@Configuration
@EnableConfigurationProperties(AgentMysqlProperties.class)
@ConditionalOnProperty(name = "agent.mysql.enabled", havingValue = "true")
public class AgentMysqlConfig {
// /**
// * Agent MySQL 数据源配置
// * 为 Agent 配置独立的 MySQL 数据库连接池HikariCP
// *
// * 仅在 agent.mysql.enabled=true 时启用
// */
// @Configuration
// @EnableConfigurationProperties(AgentMysqlProperties.class)
// @ConditionalOnProperty(name = "agent.mysql.enabled", havingValue = "true")
// public class AgentMysqlConfig {
/**
* 创建 Agent 专用的数据源
* 与项目主数据源隔离,独立管理
*
* @param properties Agent MySQL 配置属性
* @return HikariCP 数据源
*/
@Bean("agentDataSource")
public DataSource agentDataSource(AgentMysqlProperties properties) {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(properties.getUrl());
config.setUsername(properties.getUsername());
config.setPassword(properties.getPassword());
config.setDriverClassName("com.mysql.cj.jdbc.Driver");
config.setMaximumPoolSize(properties.getMaxPoolSize());
config.setMinimumIdle(properties.getMinIdle());
config.setConnectionTimeout(30000);
config.setIdleTimeout(600000);
config.setMaxLifetime(1800000);
// /**
// * 创建 Agent 专用的数据源
// * 与项目主数据源隔离,独立管理
// *
// * @param properties Agent MySQL 配置属性
// * @return HikariCP 数据源
// */
// @Bean("agentDataSource")
// public DataSource agentDataSource(AgentMysqlProperties properties) {
// HikariConfig config = new HikariConfig();
// config.setJdbcUrl(properties.getUrl());
// config.setUsername(properties.getUsername());
// config.setPassword(properties.getPassword());
// config.setDriverClassName("com.mysql.cj.jdbc.Driver");
// config.setMaximumPoolSize(properties.getMaxPoolSize());
// config.setMinimumIdle(properties.getMinIdle());
// config.setConnectionTimeout(30000);
// config.setIdleTimeout(600000);
// config.setMaxLifetime(1800000);
return new HikariDataSource(config);
}
}
// return new HikariDataSource(config);
// }
// }

View File

@@ -16,7 +16,8 @@ public class TableStructure {
* 表名
*/
private String tableName;
private String tableType; // 添加此字段BASE TABLE 或 VIEW
/**
* 表注释/说明
*/

View File

@@ -1,19 +1,19 @@
package org.ruoyi.agent.manager;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
/**
* 架构初始化器
* 在应用启动完成后自动初始化表结构缓存
*/
@Slf4j
@Component
@ConditionalOnProperty(name = "agent.mysql.enabled", havingValue = "true")
// @ConditionalOnProperty(name = "agent.mysql.enabled", havingValue = "true")
public class TableSchemaInitializer {
@Autowired(required = false)

View File

@@ -1,16 +1,30 @@
package org.ruoyi.agent.manager;
import lombok.extern.slf4j.Slf4j;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import javax.sql.DataSource;
import org.ruoyi.agent.domain.ColumnInfo;
import org.ruoyi.agent.domain.TableStructure;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.sql.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import com.baomidou.dynamic.datasource.annotation.DS;
import lombok.extern.slf4j.Slf4j;
/**
* 表结构管理器
@@ -24,12 +38,15 @@ import java.util.stream.Collectors;
*/
@Slf4j
@Component
@ConditionalOnProperty(name = "agent.mysql.enabled", havingValue = "true")
@DS("agent")
public class TableSchemaManager {
@Autowired(required = false)
private DataSource agentDataSource;
@Value("${AGENT_ALLOWED_TABLES}")
private String allowedTables;
/**
* 表结构缓存 (表名 -> 表结构)
* 使用 ConcurrentHashMap 支持高并发访问
@@ -55,12 +72,12 @@ public class TableSchemaManager {
if (initialized) {
return;
}
try {
log.info("Initializing database schema cache...");
loadAllowedTableSchemas();
initialized = true;
log.info("Schema cache initialized with {} tables", schemaCache.size());
} catch (Exception e) {
log.error("Failed to initialize schema cache", e);
}
@@ -103,6 +120,7 @@ public class TableSchemaManager {
try (ResultSet tableRs = metaData.getTables(conn.getCatalog(), null, tableName, new String[]{"TABLE"})) {
if (tableRs.next()) {
table.setTableComment(tableRs.getString("REMARKS"));
table.setTableType(tableRs.getString("TABLE_TYPE"));
}
}
@@ -183,7 +201,6 @@ public class TableSchemaManager {
* 获取所有允许的表名
*/
public List<String> getAllowedTableNames() {
String allowedTables = System.getenv("AGENT_ALLOWED_TABLES");
if (allowedTables == null || allowedTables.trim().isEmpty()) {
log.warn("AGENT_ALLOWED_TABLES not configured");
return new ArrayList<>();
@@ -224,7 +241,7 @@ public class TableSchemaManager {
* 检查表是否在允许列表中
*/
private boolean isTableAllowed(String tableName) {
String allowedTables = System.getenv("AGENT_ALLOWED_TABLES");
// String allowedTables = System.getenv("AGENT_ALLOWED_TABLES");
if (allowedTables == null || allowedTables.trim().isEmpty()) {
return false;
}

View File

@@ -1,12 +1,5 @@
package org.ruoyi.agent.tool;
import dev.langchain4j.agent.tool.Tool;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
@@ -16,17 +9,26 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import dev.langchain4j.agent.tool.Tool;
import lombok.extern.slf4j.Slf4j;
/**
* 执行 SQL 查询的 Tool
* 执行指定的 SELECT SQL 查询并返回结果
*/
@Slf4j
@Component
@ConditionalOnProperty(name = "agent.mysql.enabled", havingValue = "true")
public class ExecuteSqlQueryTool {
@Autowired(required = false)
private DataSource agentDataSource;
private DataSource dataSource;
/**
* 执行 SELECT SQL 查询
@@ -37,6 +39,8 @@ public class ExecuteSqlQueryTool {
*/
@Tool("Execute a SELECT SQL query and return the results. Example: SELECT * FROM sys_user")
public String executeSql(String sql) {
// 2. 手动推入数据源上下文
DynamicDataSourceContextHolder.push("agent");
if (sql == null || sql.trim().isEmpty()) {
return "Error: SQL query cannot be empty";
}
@@ -48,11 +52,11 @@ public class ExecuteSqlQueryTool {
}
try {
if (agentDataSource == null) {
if (dataSource == null) {
return "Error: Database datasource not configured";
}
try (Connection connection = agentDataSource.getConnection()) {
try (Connection connection = dataSource.getConnection()) {
try (PreparedStatement preparedStatement = connection.prepareStatement(sql);
ResultSet resultSet = preparedStatement.executeQuery()) {
@@ -82,7 +86,12 @@ public class ExecuteSqlQueryTool {
}
} catch (Exception e) {
log.error("Error executing SQL: {}", sql, e);
// 3. 必须在 finally 中清除上下文,防止污染其他请求
DynamicDataSourceContextHolder.clear();
return "Error: " + e.getMessage();
} finally {
// 3. 必须在 finally 中清除上下文,防止污染其他请求
DynamicDataSourceContextHolder.clear();
}
}

View File

@@ -1,20 +1,14 @@
package org.ruoyi.agent.tool;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import dev.langchain4j.agent.tool.Tool;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.agent.config.AgentMysqlProperties;
import java.util.List;
import org.ruoyi.agent.domain.TableStructure;
import org.ruoyi.agent.manager.TableSchemaManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;
import dev.langchain4j.agent.tool.Tool;
import lombok.extern.slf4j.Slf4j;
/**
* 查询数据库所有表的 Tool
@@ -22,12 +16,11 @@ import java.util.List;
*/
@Slf4j
@Component
@ConditionalOnProperty(name = "agent.mysql.enabled", havingValue = "true")
public class QueryAllTablesTool {
@Autowired(required = false)
private DataSource agentDataSource;
@Autowired
private TableSchemaManager tableSchemaManager; // 注入管理器
/**
* 查询数据库中所有表
* 返回数据库中存在的所有表的列表
@@ -37,44 +30,36 @@ public class QueryAllTablesTool {
@Tool("Query all tables in the database and return table names and basic information")
public String queryAllTables() {
try {
if (agentDataSource == null) {
return "Error: Database datasource not configured";
}
try (Connection connection = agentDataSource.getConnection()) {
DatabaseMetaData databaseMetaData = connection.getMetaData();
ResultSet resultSet = databaseMetaData.getTables(null, null, null, new String[]{"TABLE"});
List<String> tableNames = new ArrayList<>();
List<String> tableDetails = new ArrayList<>();
while (resultSet.next()) {
String tableName = resultSet.getString("TABLE_NAME");
String tableComment = resultSet.getString("REMARKS");
String tableType = resultSet.getString("TABLE_TYPE");
tableNames.add(tableName);
tableDetails.add(String.format("- %s (%s) - %s",
tableName, tableType, tableComment != null ? tableComment : "No comment"));
// 1. 从管理器获取所有允许的表结构信息(内部已包含初始化/缓存逻辑)
List<TableStructure> tableSchemas = tableSchemaManager.getAllowedTableSchemas();
if (tableSchemas == null || tableSchemas.isEmpty()) {
return "No tables found in database or cache is empty.";
}
resultSet.close();
if (tableNames.isEmpty()) {
return "No tables found in database";
}
// 2. 格式化结果
StringBuilder result = new StringBuilder();
result.append("Found ").append(tableNames.size()).append(" tables:\n");
for (String detail : tableDetails) {
result.append(detail).append("\n");
result.append("Found ").append(tableSchemas.size()).append(" tables in cache:\n");
for (TableStructure schema : tableSchemas) {
String tableName = schema.getTableName();
String tableType = schema.getTableType() != null ? schema.getTableType() : "TABLE";
String tableComment = schema.getTableComment();
result.append(String.format("- %s (%s) - %s\n",
tableName,
tableType,
tableComment != null ? tableComment : "No comment"));
}
log.info("Successfully queried {} tables", tableNames.size());
log.info("Successfully retrieved {} tables from schema cache", tableSchemas.size());
return result.toString();
} catch (Exception e) {
log.error("Error retrieving tables from cache", e);
return "Error: " + e.getMessage();
}
} catch (Exception e) {
log.error("Error querying all tables", e);
return "Error: " + e.getMessage();
}
}
}

View File

@@ -1,83 +1,57 @@
package org.ruoyi.agent.tool;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import dev.langchain4j.agent.tool.Tool;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
/**
* 查询表建表详情的 Tool
* 根据表名查询该表的建表 SQL 语句
*/
@Slf4j
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import dev.langchain4j.agent.tool.Tool;
import lombok.extern.slf4j.Slf4j;
@Component
@ConditionalOnProperty(name = "agent.mysql.enabled", havingValue = "true")
@Slf4j
public class QueryTableSchemaTool {
@Autowired(required = false)
private DataSource agentDataSource;
private DataSource dataSource;
/**
* 根据表名查询建表详情
* 返回指定表的 CREATE TABLE 语句
*
* @param tableName 表名
* @return 包含建表 SQL 的结果
*/
@Tool("Query the CREATE TABLE statement (DDL) for a specific table by table name")
public String queryTableSchema(String tableName) {
// 2. 手动推入数据源上下文
DynamicDataSourceContextHolder.push("agent");
if (tableName == null || tableName.trim().isEmpty()) {
return "Error: Table name cannot be empty";
}
// 验证表名有效性,防止 SQL 注入
if (!isValidIdentifier(tableName)) {
if (!tableName.matches("^[a-zA-Z0-9_]+$")) {
return "Error: Invalid table name format";
}
try {
if (agentDataSource == null) {
return "Error: Database datasource not configured";
String sql = "SHOW CREATE TABLE `" + tableName + "`";
try (Connection connection = dataSource.getConnection();
PreparedStatement ps = connection.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
return rs.getString("Create Table");
}
try (Connection connection = agentDataSource.getConnection()) {
String sql = "SHOW CREATE TABLE " + tableName;
PreparedStatement preparedStatement = connection.prepareStatement(sql);
ResultSet resultSet = preparedStatement.executeQuery();
return "Table not found: " + tableName;
if (resultSet.next()) {
String createTableSql = resultSet.getString("Create Table");
resultSet.close();
preparedStatement.close();
log.info("Successfully queried schema for table: {}", tableName);
return "CREATE TABLE DDL for " + tableName + ":\n\n" + createTableSql;
}
resultSet.close();
preparedStatement.close();
return "Error: Table not found or not accessible: " + tableName;
}
} catch (Exception e) {
log.error("Error querying table schema for table: {}", tableName, e);
// 3. 必须在 finally 中清除上下文,防止污染其他请求
DynamicDataSourceContextHolder.clear();
log.error("Error querying table schema: {}", tableName, e);
return "Error: " + e.getMessage();
} finally {
// 3. 必须在 finally 中清除上下文,防止污染其他请求
DynamicDataSourceContextHolder.clear();
}
}
/**
* 验证是否为有效的 SQL 标识符
*/
private boolean isValidIdentifier(String identifier) {
if (identifier == null || identifier.isEmpty()) {
return false;
}
return identifier.matches("^[a-zA-Z0-9_\\.]+$");
}
}

View File

@@ -3,6 +3,7 @@ package org.ruoyi.service.chat.impl;
import dev.langchain4j.agentic.AgenticServices;
import dev.langchain4j.agentic.supervisor.SupervisorAgent;
import dev.langchain4j.agentic.supervisor.SupervisorResponseStrategy;
import dev.langchain4j.community.model.dashscope.QwenChatModel;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.mcp.McpToolProvider;
@@ -325,26 +326,26 @@ public abstract class AbstractStreamingChatService implements IChatService {
protected String doAgent(String userMessage, ChatModelVo chatModelVo) {
// 步骤1: 配置MCP传输层 - 连接到bing-cn-mcp服务器
// 该服务提供两个工具: bing_search (必应搜索) 和 crawl_webpage (网页抓取)
McpTransport transport = new StdioMcpTransport.Builder()
.command(List.of("C:\\Program Files\\nodejs\\npx.cmd", "-y",
"bing-cn-mcp"
))
.logEvents(true)
.build();
// McpTransport transport = new StdioMcpTransport.Builder()
// .command(List.of("C:\\Program Files\\nodejs\\npx.cmd", "-y",
// "bing-cn-mcp"
// ))
// .logEvents(true)
// .build();
// 步骤2: 创建MCP客户端
McpClient mcpClient = new DefaultMcpClient.Builder()
.transport(transport)
.build();
// // 步骤2: 创建MCP客户端
// McpClient mcpClient = new DefaultMcpClient.Builder()
// .transport(transport)
// .build();
// 步骤3: 配置工具提供者
ToolProvider toolProvider = McpToolProvider.builder()
.mcpClients(List.of(mcpClient))
.build();
// // 步骤3: 配置工具提供者
// ToolProvider toolProvider = McpToolProvider.builder()
// .mcpClients(List.of(mcpClient))
// .build();
McpTransport transport1 = new StdioMcpTransport.Builder()
.command(List.of("C:\\Program Files\\nodejs\\npx.cmd", "-y",
.command(List.of("npx", "-y",
"mcp-echarts"
))
.logEvents(true)
@@ -361,40 +362,52 @@ public abstract class AbstractStreamingChatService implements IChatService {
.build();
// 步骤4: 配置OpenAI模型
OpenAiChatModel PLANNER_MODEL = OpenAiChatModel.builder()
.baseUrl(chatModelVo.getApiHost())
// OpenAiChatModel PLANNER_MODEL = OpenAiChatModel.builder()
// .baseUrl(chatModelVo.getApiHost())
// .apiKey(chatModelVo.getApiKey())
// .modelName(chatModelVo.getModelName())
// .build();
QwenChatModel qwenChatModel = QwenChatModel.builder()
// .baseUrl(chatModelVo.getApiHost())
.apiKey(chatModelVo.getApiKey())
.modelName(chatModelVo.getModelName())
.build();
.build();
SqlAgent sqlAgent = AgenticServices.agentBuilder(SqlAgent.class)
.chatModel(PLANNER_MODEL)
.chatModel(
qwenChatModel)
.tools(
new QueryAllTablesTool(),
new QueryTableSchemaTool(),
new ExecuteSqlQueryTool()
SpringUtils.getBean(QueryAllTablesTool.class), // 必须通过 getBean 获取
SpringUtils.getBean(QueryTableSchemaTool.class),
SpringUtils.getBean(ExecuteSqlQueryTool.class)
)
.build();
WebSearchAgent searchAgent = AgenticServices.agentBuilder(WebSearchAgent.class)
.chatModel(PLANNER_MODEL)
.toolProvider(toolProvider)
.build();
// WebSearchAgent searchAgent = AgenticServices.agentBuilder(WebSearchAgent.class)
// .chatModel(PLANNER_MODEL)
// .toolProvider(toolProvider)
// .build();
ChartGenerationAgent chartGenerationAgent = AgenticServices.agentBuilder(ChartGenerationAgent.class)
.chatModel(PLANNER_MODEL)
.chatModel(
qwenChatModel)
.toolProvider(toolProvider1)
.build();
String res = sqlAgent.getData(userMessage);
String res1 = chartGenerationAgent.generateChart(res);
System.out.println(res1);
System.out.println(res);
SupervisorAgent supervisor = AgenticServices
.supervisorBuilder()
.chatModel(PLANNER_MODEL)
.chatModel(qwenChatModel)
.subAgents(sqlAgent, chartGenerationAgent)
.responseStrategy(SupervisorResponseStrategy.LAST)
.build();
String invoke = supervisor.invoke(userMessage);
System.out.println(invoke);
return invoke;
return res1;
}
}