package vip.mate.tool.builtin;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
import vip.mate.datasource.model.DatasourceEntity;
import vip.mate.datasource.service.DatasourceConnectionManager;
import vip.mate.datasource.service.DatasourceService;
import java.sql.*;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
/**
* 内置工具:数据源发现
*
* 提供数据源列表查询、表列表查询、表结构查询三个动作,
* 供 Agent 在查数场景下发现可用数据源和表结构。
*
* @author MateClaw Team
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class DatasourceTool {
private final DatasourceService datasourceService;
private final DatasourceConnectionManager connectionManager;
/**
* The application ObjectMapper, which serializes every {@code Long} as a JSON
* string (see {@code JacksonConfig}). Tool output goes through it so 19-digit
* Snowflake ids reach the model as strings — exactly like the HTTP API — and
* never as JSON numbers that lose their low digits in a double/JS-number
* round-trip on the way back into a tool call.
*/
private final ObjectMapper objectMapper;
/** SQL identifier whitelist: letters, digits, underscore, dot, hyphen only */
private static final Pattern SAFE_IDENTIFIER = Pattern.compile("^[a-zA-Z0-9_][a-zA-Z0-9_.\\-]{0,127}$");
private static String sanitizeIdentifier(String name) {
if (name == null || !SAFE_IDENTIFIER.matcher(name).matches()) {
throw new IllegalArgumentException("Invalid SQL identifier: " + name);
}
return name;
}
@Tool(description = """
查询外部数据源的元数据。支持三种动作:
1. action='list_datasources' — 列出所有可用数据源(无需其他参数)
2. action='list_tables' — 列出指定数据源中的所有表(需要 datasourceId)
3. action='describe_table' — 查看指定表的列详情(需要 datasourceId 和 tableName)
""")
public String query_datasource(
@ToolParam(description = "动作:list_datasources / list_tables / describe_table") String action,
@ToolParam(description = "数据源 ID(list_tables 和 describe_table 时必填)", required = false) Long datasourceId,
@ToolParam(description = "表名(describe_table 时必填)", required = false) String tableName) {
try {
return switch (action) {
case "list_datasources" -> listDatasources();
case "list_tables" -> listTables(datasourceId);
case "describe_table" -> describeTable(datasourceId, tableName);
default -> error("未知动作: " + action + ",支持: list_datasources / list_tables / describe_table");
};
} catch (Exception e) {
log.error("数据源查询失败: {}", e.getMessage(), e);
return error(e.getMessage());
}
}
private String listDatasources() throws Exception {
List list = datasourceService.listEnabled();
List