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})。 * *

工具返回结构化 schema(marker {@code type=form_collect});{@code AgentChatController} 侦测到后推 * SSE {@code form_collect} 事件,前端渲染表单;用户填完提交,前端把收集到的字段拼成后续对话消息发回, * agent 再据此走 {@code proposeCreate}(人在环写入)。适合报价这类字段多的新建场景。 * *

由 {@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 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 + "」的权限。"); } String table = String.valueOf(form.get("sDataSource")); List> fields = new ArrayList<>(); // 主字段来源 = 该表的**业务录入字段**(字段字典,排除系统/审计/计算列)——比 gdsconfigformslave 的 // 界面布局字段更贴近"要填的业务参数"(报价的 客户/产品/数量/尺寸/单价,而不是 制单人/单据日期)。 List> biz = resolver.businessFields(table, MAX_FIELDS); for (Map b : biz) { String col = str(b.get("col")); if (col == null || col.isBlank()) { continue; } Map f = new LinkedHashMap<>(); f.put("name", col); f.put("label", firstNonBlank(str(b.get("label")), col)); f.put("control", "text"); f.put("required", false); Object fk = b.get("fk"); if (fk != null && !String.valueOf(fk).isBlank()) { f.put("fk", true); // 外键字段:让用户填名称,写入时解析成 id } fields.add(f); } // 兜底:字段字典没有该表映射时,退回 gdsconfigformslave 的非只读可见字段(排除系统列)。 if (fields.isEmpty()) { try { List> 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); for (Map c : cols) { String name = str(c.get("sName")); if (name == null || name.isBlank() || resolver.isSystemColumn(name)) { continue; } Map 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 opts = simpleOptions(str(c.get("sChineseDropDown"))); if (!opts.isEmpty()) { f.put("options", opts); } fields.add(f); } } catch (Exception e) { return err("读取表单结构失败:" + e.getMessage()); } } if (fields.isEmpty()) { return err("该表单没有可填写的业务字段。"); } Map 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 simpleOptions(String dropdown) { List 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 m = new LinkedHashMap<>(); m.put("error", msg); return toJson(m); } private String toJson(Map m) { try { return mapper.writeValueAsString(m); } catch (Exception e) { return "{\"error\":\"内部错误\"}"; } } }