package com.xly.tool;
import com.xly.agent.AgentIdentity;
import com.xly.service.AuditService;
import com.xly.service.FormResolverService;
import dev.langchain4j.agent.tool.P;
import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.model.chat.ChatModel;
import net.sf.jsqlparser.expression.StringValue;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
import net.sf.jsqlparser.util.TablesNamesFinder;
import org.springframework.jdbc.core.JdbcTemplate;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;
/**
* Query 工具:**只读 SQL 兜底** —— 回答没有现成表单/记录能直接答的临时统计/分析问题
* (跨表汇总、按条件计数排名等)。
*
*
安全栈(架构 §9):用 coder 模型据 KG 字段字典接地生成 SQL → jsqlparser 强制**单条 SELECT** →
* 挡 {@code INTO OUTFILE / LOAD_FILE / information_schema / SLEEP / BENCHMARK} 与多语句 →
* **表白名单**({@link #allowedTables()}:viw_* + 表单数据源 + 字段字典表,挡掉 gdslogininfo /
* sysjurisdiction / ai_op_queue 等敏感表) → **AST 注入租户谓词**({@link #applyTenant},单表查询按
* {@code sBrandsId}) → 强制 LIMIT。SQL 全部入审计。
*
*
尚未落地的加固项:多表 JOIN 只靠 prompt 提示带租户过滤(AST 注入只覆盖单表)、
* 以及独立的只读 MySQL 账号(当前与应用共用连接池)。
*/
public class QueryTool {
private final ChatModel sqlModel;
private final JdbcTemplate jdbc;
private final AuditService audit;
private final AgentIdentity identity;
private final FormResolverService resolver;
public QueryTool(ChatModel sqlModel, JdbcTemplate jdbc, AuditService audit,
AgentIdentity identity, FormResolverService resolver) {
this.sqlModel = sqlModel;
this.jdbc = jdbc;
this.audit = audit;
this.identity = identity;
this.resolver = resolver;
}
@Tool("用**只读 SQL** 回答没有现成表单/记录能直接答的临时统计或分析问题"
+ "(如跨表汇总、按条件计数、排名、分组统计)。仅在 readFormData / lookupRecord 无法回答时才用。")
public String queryData(@P("用自然语言描述要统计/分析什么") String question) {
if (question == null || question.isBlank()) {
return "请描述要查询统计的内容。";
}
String hint = schemaHint(question);
String sql = null;
String lastErr = null;
// 自修复重试:SQL 校验/执行报错就把错误喂回模型重新生成,最多 3 次
for (int attempt = 0; attempt < 3; attempt++) {
try {
sql = cleanSql(sqlModel.chat(buildPrompt(hint, question, sql, lastErr)));
} catch (Exception e) {
return "生成查询失败:" + e.getMessage();
}
String reject = validate(sql);
if (reject != null) {
lastErr = reject;
if (attempt < 2) {
continue;
}
audit.log(null, null, "query", "REJECTED", sql, false, reject);
return "无法安全执行该查询(" + reject + ")。可以换个更具体的问法。";
}
String limited = forceLimit(applyTenant(sql));
try {
List