ErpReadTool.java 11.2 KB
package com.xly.tool;

import com.fasterxml.jackson.databind.JsonNode;
import com.xly.service.ErpClient;
import com.xly.service.FormResolverService;
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 表单的真实业务数据。
 *
 * <p>走 ERP 后端通用表单读接口({@link ErpClient})——即架构里"Read = 薄 HTTP 客户端、
 * 业务逻辑在 ERP 后端、得到租户+行级范围"。列名用字段字典({@code viw_kg_field_dict})
 * 渲染成中文,只展示前几列/前几行,避免吐爆上下文。
 *
 * <p>安全:只传分页参数、不传任何写参数(如 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;
    private final FormResolverService resolver;

    public ErpReadTool(ErpClient erp, JdbcTemplate jdbc, FormResolverService resolver) {
        this.erp = erp;
        this.jdbc = jdbc;
        this.resolver = resolver;
    }

    @Tool("查询某个实体下某条命名记录的**完整信息**(返回该记录的所有可读字段)。"
            + "用于「某客户 / 某物料 的详细资料 / 某个具体字段(如联系电话、销售员)」这类精确查询。")
    public String lookupRecord(
            @P("实体类型,如 客户 / 物料 / 供应商") String entityKeyword,
            @P("记录名称关键词,如某个客户名") String recordKeyword) {
        if (entityKeyword == null || entityKeyword.isBlank() || recordKeyword == null || recordKeyword.isBlank()) {
            return "请提供实体类型和记录名称关键词。";
        }
        Map<String, Object> form = resolver.resolveMasterForm(entityKeyword.trim());
        if (form == null) {
            return "找不到「" + entityKeyword + "」对应的主表。";
        }
        String formId = String.valueOf(form.get("sFormId"));
        String moduleId = String.valueOf(form.get("sModuleId"));
        String table = String.valueOf(form.get("sDataSource"));
        String nameField = resolver.resolveNameField(table);

        JsonNode root;
        try {
            root = erp.readForm(formId, moduleId, 1, 3, nameField, recordKeyword.trim());
        } catch (Exception e) {
            return "读取失败:" + e.getMessage();
        }
        if (root.path("code").asInt(0) < 0) {
            return "读取失败:" + root.path("msg").asText("未知错误");
        }
        JsonNode rows = root.path("dataset").path("rows");
        JsonNode data = (rows.isArray() && rows.size() > 0) ? rows.get(0).path("dataSet") : null;
        int n = (data != null && data.isArray()) ? data.size() : 0;
        if (n == 0) {
            return "没有找到名称含「" + recordKeyword + "」的记录。";
        }
        if (n > 1) {
            StringBuilder names = new StringBuilder();
            for (int i = 0; i < data.size() && i < 5; i++) {
                if (i > 0) names.append("、");
                names.append(nameField == null ? "" : data.get(i).path(nameField).asText(""));
            }
            return "匹配到多条(" + names + "),请说得更具体一点。";
        }
        return renderRecord(data.get(0), resolver.fieldLabels(formId));
    }

    /** 把一条记录渲染成「中文名:值」清单(跳过空值、id/token/布尔噪声),最多 20 行。 */
    private String renderRecord(JsonNode rec, Map<String, String> labels) {
        StringBuilder sb = new StringBuilder();
        int count = 0;
        Iterator<String> it = rec.fieldNames();
        while (it.hasNext() && count < 20) {
            String c = it.next();
            if (c.equalsIgnoreCase("sToken") || c.endsWith("Id") || (c.length() > 1 && c.charAt(0) == 'b')) {
                continue;
            }
            JsonNode v = rec.path(c);
            if (v.isContainerNode() || v.isNull()) {
                continue;
            }
            String val = v.asText("").trim();
            String label = labels.get(c);
            if (val.isEmpty() || label == null) {
                continue;
            }
            sb.append("- ").append(label).append(":").append(val.length() > 60 ? val.substring(0, 60) + "…" : val).append("\n");
            count++;
        }
        return sb.length() == 0 ? "找到了该记录,但没有可展示的字段。" : sb.toString();
    }

    @Tool("读取指定 ERP 表单的真实业务数据(返回前若干行 + 总条数)。可选 keyword 用于按名称模糊过滤"
            + "(如查某个客户 / 物料)。必须先用 findForms 得到目标表单的 formId 与 moduleId。")
    public String readFormData(
            @P("表单id(findForms 返回的 formId)") String formId,
            @P("菜单id(findForms 返回的 moduleId)") String moduleId,
            @P(value = "可选:仅当要查找某个具体名称的记录时才填(如某个客户名/物料名);"
                    + "问数量 / 全部 / 概况时必须留空", required = false) String keyword) {

        if (formId == null || formId.isBlank() || moduleId == null || moduleId.isBlank()) {
            return "缺少 formId 或 moduleId,请先用 findForms 检索到具体表单再调用本工具。";
        }
        String kw = (keyword == null) ? "" : keyword.trim();
        String nameField = kw.isEmpty() ? null : resolveNameField(formId.trim());

        JsonNode root;
        try {
            root = erp.readForm(formId.trim(), moduleId.trim(), 1, MAX_ROWS, nameField, kw.isEmpty() ? null : kw);
        } 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<String, String> labels = loadLabels(formId.trim());
        List<String> cols = pickColumns(data.get(0), labels);

        String scope;
        if (!kw.isEmpty() && nameField != null) {
            scope = "(按名称含「" + kw + "」筛选)";
        } else if (!kw.isEmpty()) {
            scope = "(未识别到该表单的名称字段,无法按「" + kw + "」过滤,返回前几条)";
        } else {
            scope = "";
        }

        StringBuilder sb = new StringBuilder();
        sb.append(scope).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<String> 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<String, String> loadLabels(String formId) {
        Map<String, String> m = new HashMap<>();
        try {
            List<Map<String, Object>> 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<String, Object> 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;
    }

    /** 猜测该表单的"名称"字段(用于按关键词过滤):数据源表里以 Name 结尾、使用最广的字段。 */
    private String resolveNameField(String formId) {
        try {
            List<Map<String, Object>> rows = jdbc.queryForList(
                    "SELECT fd.sField AS f FROM viw_ai_useful_forms af " +
                            "JOIN viw_kg_field_dict fd ON fd.sTable = af.sDataSource " +
                            "WHERE af.sFormId = ? AND fd.sField LIKE '%Name' AND fd.sField NOT LIKE '%NameId' " +
                            "ORDER BY fd.iFormUses DESC LIMIT 1", formId);
            if (!rows.isEmpty() && rows.get(0).get("f") != null) {
                return rows.get(0).get("f").toString();
            }
        } catch (Exception ignore) {
            // 无字典时不过滤
        }
        return null;
    }

    /** 选择展示列:优先有中文名的短文本列,跳过 id/token/布尔噪声,最多 MAX_COLS 列。 */
    private List<String> pickColumns(JsonNode first, Map<String, String> labels) {
        List<String> labeled = new ArrayList<>();
        List<String> others = new ArrayList<>();
        Iterator<String> 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<String> 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<String> 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;
    }
}