package com.xly.tool; import com.fasterxml.jackson.databind.JsonNode; import com.xly.service.ErpClient; import dev.langchain4j.agent.tool.P; import dev.langchain4j.agent.tool.Tool; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.stream.Collectors; /** * Read 工具(Phase A):读取指定 ERP 表单的真实业务数据。 * *

走 ERP 后端通用表单读接口({@link ErpClient})——即架构里"Read = 薄 HTTP 客户端、 * 业务逻辑在 ERP 后端、得到租户+行级范围"。列名用字段字典({@code viw_kg_field_dict}) * 渲染成中文,只展示前几列/前几行,避免吐爆上下文。 * *

安全:只传分页参数、不传任何写参数(如 bUpdate),读不会变写。 */ @Component public class ErpReadTool { private static final int MAX_ROWS = 10; private static final int MAX_COLS = 6; private final ErpClient erp; private final JdbcTemplate jdbc; public ErpReadTool(ErpClient erp, JdbcTemplate jdbc) { this.erp = erp; this.jdbc = jdbc; } @Tool("读取指定 ERP 表单的真实业务数据(返回前若干行 + 总条数)。" + "必须先用 findForms 得到目标表单的 formId 与 moduleId,再调用本工具。") public String readFormData( @P("表单id(findForms 返回的 formId)") String formId, @P("菜单id(findForms 返回的 moduleId)") String moduleId) { if (formId == null || formId.isBlank() || moduleId == null || moduleId.isBlank()) { return "缺少 formId 或 moduleId,请先用 findForms 检索到具体表单再调用本工具。"; } JsonNode root; try { root = erp.readForm(formId.trim(), moduleId.trim(), 1, MAX_ROWS); } catch (Exception e) { return "读取失败:" + e.getMessage(); } int code = root.path("code").asInt(0); if (code < 0) { return "读取失败:" + root.path("msg").asText("未知错误"); } JsonNode ds = root.path("dataset"); int total = ds.path("totalCount").asInt(0); JsonNode rows = ds.path("rows"); JsonNode data = (rows.isArray() && rows.size() > 0) ? rows.get(0).path("dataSet") : null; if (data == null || !data.isArray() || data.isEmpty()) { return "该表单当前没有数据(共 " + total + " 条)。"; } Map labels = loadLabels(formId.trim()); List cols = pickColumns(data.get(0), labels); StringBuilder sb = new StringBuilder(); sb.append("共 ").append(total).append(" 条,前 ").append(data.size()).append(" 条:\n\n"); sb.append("| ") .append(cols.stream().map(c -> labels.getOrDefault(c, c)).collect(Collectors.joining(" | "))) .append(" |\n"); sb.append("|").append(" --- |".repeat(cols.size())).append("\n"); for (JsonNode r : data) { List vals = new ArrayList<>(cols.size()); for (String c : cols) { vals.add(cell(r.path(c))); } sb.append("| ").append(String.join(" | ", vals)).append(" |\n"); } return sb.toString(); } /** 表单数据源表 -> 字段中文名(字段字典),用于把技术列名渲染为中文表头。 */ private Map loadLabels(String formId) { Map m = new HashMap<>(); try { List> rows = jdbc.queryForList( "SELECT fd.sField AS f, MIN(fd.sChinese) AS zh " + "FROM viw_ai_useful_forms af " + "JOIN viw_kg_field_dict fd ON fd.sTable = af.sDataSource " + "WHERE af.sFormId = ? GROUP BY fd.sField", formId); for (Map r : rows) { Object f = r.get("f"); Object zh = r.get("zh"); if (f != null && zh != null) { m.put(f.toString(), zh.toString()); } } } catch (Exception ignore) { // 无字典时退化为技术列名 } return m; } /** 选择展示列:优先有中文名的短文本列,跳过 id/token/布尔噪声,最多 MAX_COLS 列。 */ private List pickColumns(JsonNode first, Map labels) { List labeled = new ArrayList<>(); List others = new ArrayList<>(); Iterator it = first.fieldNames(); while (it.hasNext()) { String c = it.next(); if (c.equalsIgnoreCase("sToken") || c.endsWith("Id") || (c.length() > 1 && c.charAt(0) == 'b')) { continue; // 跳过 id / token / 布尔字段 } JsonNode v = first.path(c); if (v.isContainerNode()) { continue; } if (labels.containsKey(c)) { labeled.add(c); } else { others.add(c); } } List cols = new ArrayList<>(labeled); for (String o : others) { if (cols.size() >= MAX_COLS) { break; } cols.add(o); } if (cols.size() > MAX_COLS) { cols = new ArrayList<>(cols.subList(0, MAX_COLS)); } if (cols.isEmpty()) { Iterator it2 = first.fieldNames(); while (it2.hasNext() && cols.size() < 4) { String c = it2.next(); if (!first.path(c).isContainerNode()) { cols.add(c); } } } return cols; } private String cell(JsonNode v) { if (v == null || v.isNull() || v.isMissingNode()) { return ""; } String s = v.asText("").replace("|", "/").replace("\n", " ").trim(); return s.length() > 24 ? s.substring(0, 24) + "…" : s; } }