FormCollectTool.java 6.6 KB
package com.xly.tool;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.xly.agent.AgentIdentity;
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 java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * FormCollect 工具(架构 §5 #5)——在对话里渲染一张 ERP 表单,让用户一次填齐 N 个参数,
 * 而不是逐字段追问。字段/类型/必填/下拉/默认来自 ERP 表单元数据({@code gdsconfigformslave})。
 *
 * <p>工具返回结构化 schema(marker {@code type=form_collect});{@code AgentChatController} 侦测到后推
 * SSE {@code form_collect} 事件,前端渲染表单;用户填完提交,前端把收集到的字段拼成后续对话消息发回,
 * agent 再据此走 {@code proposeCreate}(人在环写入)。适合报价这类字段多的新建场景。
 *
 * <p>由 {@code AgentFactory} 按请求身份新建(非 @Component):携带 {@link AgentIdentity} 做表单级授权。
 */
public class FormCollectTool {

    private static final int MAX_FIELDS = 40;

    private final ErpClient erp;
    private final JdbcTemplate jdbc;
    private final FormResolverService resolver;
    private final AgentIdentity identity;
    private final ObjectMapper mapper;

    public FormCollectTool(ErpClient erp, JdbcTemplate jdbc, FormResolverService resolver,
                           AgentIdentity identity, ObjectMapper mapper) {
        this.erp = erp;
        this.jdbc = jdbc;
        this.resolver = resolver;
        this.identity = identity;
        this.mapper = mapper;
    }

    @Tool("在对话里弹出一张 ERP 表单让用户一次性填齐多个字段(而不是逐个追问)。用于字段较多的新建/录入场景"
            + "(如新建报价、新建客户)。入参 = 实体/单据类型(如 报价 / 客户)。用户填完提交后,你再用 proposeCreate 生成待确认的新增。")
    public String collectForm(@P("要新建的实体/单据类型,如 报价 / 客户 / 物料") String entityKeyword) {
        if (entityKeyword == null || entityKeyword.isBlank()) {
            return err("缺少实体类型。");
        }
        Map<String, Object> form = resolver.resolveMasterForm(entityKeyword.trim());
        if (form == null) {
            return err("找不到「" + entityKeyword + "」对应的可新建表单。");
        }
        String formId = String.valueOf(form.get("sFormId"));
        String moduleId = String.valueOf(form.get("sModuleId"));
        if (!identity.canAccessModule(moduleId)) {
            return err("你没有新建「" + entityKeyword + "」的权限。");
        }

        List<Map<String, Object>> cols;
        try {
            cols = jdbc.queryForList(
                    "SELECT sName, sChinese, sControlName, bNotEmpty, sDefault, sChineseDropDown " +
                            "FROM gdsconfigformslave WHERE sParentId=? AND bVisible=1 AND IFNULL(sName,'')<>'' " +
                            "AND IFNULL(bReadonly,0)=0 ORDER BY iOrder LIMIT " + MAX_FIELDS, formId);
        } catch (Exception e) {
            return err("读取表单结构失败:" + e.getMessage());
        }
        if (cols.isEmpty()) {
            return err("该表单没有可填写的字段配置。");
        }

        List<Map<String, Object>> fields = new ArrayList<>();
        for (Map<String, Object> c : cols) {
            String name = str(c.get("sName"));
            if (name == null || name.isBlank()) {
                continue;
            }
            Map<String, Object> f = new LinkedHashMap<>();
            f.put("name", name);
            f.put("label", firstNonBlank(str(c.get("sChinese")), name));
            f.put("control", firstNonBlank(str(c.get("sControlName")), "text"));
            f.put("required", truthy(c.get("bNotEmpty")));
            String def = str(c.get("sDefault"));
            if (def != null && !def.isBlank()) {
                f.put("default", def);
            }
            List<String> opts = simpleOptions(str(c.get("sChineseDropDown")));
            if (!opts.isEmpty()) {
                f.put("options", opts);
            }
            fields.add(f);
        }
        if (fields.isEmpty()) {
            return err("该表单没有可填写的字段。");
        }

        Map<String, Object> out = new LinkedHashMap<>();
        out.put("type", "form_collect");
        out.put("entity", entityKeyword.trim());
        out.put("formId", formId);
        out.put("moduleId", moduleId);
        out.put("title", "新建" + entityKeyword.trim());
        out.put("fields", fields);
        out.put("message", "请在下方表单里填写,填完点【提交】。");
        return toJson(out);
    }

    /** 只接受“简单枚举型”下拉(顿号/逗号/竖线分隔,且不含 SQL),SQL 驱动的下拉退化为自由输入。 */
    private List<String> simpleOptions(String dropdown) {
        List<String> out = new ArrayList<>();
        if (dropdown == null || dropdown.isBlank()) {
            return out;
        }
        String d = dropdown.trim();
        if (d.toLowerCase().contains("select ") || d.length() > 200) {
            return out; // SQL 或过长 -> 不当作枚举
        }
        for (String o : d.split("[、,,|]")) {
            String t = o.trim();
            if (!t.isEmpty() && out.size() < 30) {
                out.add(t);
            }
        }
        return out;
    }

    private static boolean truthy(Object o) {
        if (o == null) {
            return false;
        }
        if (o instanceof Boolean) {
            return (Boolean) o;
        }
        if (o instanceof Number) {
            return ((Number) o).intValue() != 0;
        }
        if (o instanceof byte[]) {
            byte[] b = (byte[]) o;
            return b.length > 0 && b[0] != 0;
        }
        String s = o.toString().trim();
        return s.equals("1") || s.equalsIgnoreCase("true");
    }

    private static String firstNonBlank(String a, String b) {
        return (a != null && !a.isBlank()) ? a : b;
    }

    private static String str(Object o) {
        return o == null ? null : o.toString();
    }

    private String err(String msg) {
        Map<String, Object> m = new LinkedHashMap<>();
        m.put("error", msg);
        return toJson(m);
    }

    private String toJson(Map<String, Object> m) {
        try {
            return mapper.writeValueAsString(m);
        } catch (Exception e) {
            return "{\"error\":\"内部错误\"}";
        }
    }
}