package com.xly.tool; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.xly.service.AuthzService; import com.xly.service.ErpClient; import com.xly.service.OpService; 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.LinkedHashMap; import java.util.List; import java.util.Map; /** * ProposeWrite 工具(写操作,人在环)。 * *

**只提议并暂存,绝不立即执行**:把「改某条记录的某字段」解析成具体的 表/记录id/列/新值, * 写一条 draft 到 ai_op_queue,返回一个提议。真正执行发生在用户点【确认】后的确定性端点里 * (见 OpController),不经过 LLM。 */ @Component public class ProposeWriteTool { private final ErpClient erp; private final JdbcTemplate jdbc; private final OpService ops; private final ObjectMapper mapper; private final AuthzService authz; public ProposeWriteTool(ErpClient erp, JdbcTemplate jdbc, OpService ops, ObjectMapper mapper, AuthzService authz) { this.erp = erp; this.jdbc = jdbc; this.ops = ops; this.mapper = mapper; this.authz = authz; } @Tool("提议修改某条现有记录的某个字段(写操作)。本工具**只提议并暂存、绝不立即执行**——" + "必须等用户在对话内点【确认】后才真正修改。用于「把某个客户/物料的某字段改成X」这类需求。" + "直接给出实体类型(如 客户)即可,本工具会自行定位主表,无需先 findForms。") public String proposeUpdate( @P("实体/单据类型关键词,如 客户 / 物料 / 供应商") String entityKeyword, @P("要修改的那条记录的名称关键词(如某个客户名)") String recordKeyword, @P("要修改的字段中文名(如 简称 / 备注 / 联系电话)") String fieldChinese, @P("修改后的新值") String newValue) { if (isBlank(entityKeyword) || isBlank(recordKeyword) || isBlank(fieldChinese)) { return err("缺少信息:需要实体类型、记录名称关键词、字段中文名、新值。"); } // 1) 定位可修改的主表(该实体名下、table 类型、最常用的一张) Map form = resolveForm(entityKeyword.trim()); if (form == null) { return err("找不到「" + entityKeyword + "」对应的可修改主表。"); } String formId = str(form.get("sFormId")); String moduleId = str(form.get("sModuleId")); String table = str(form.get("sDataSource")); if (!authz.canAccessModule(moduleId)) { return err("你没有修改「" + entityKeyword + "」的权限。"); } // 2) 字段中文名 -> 技术列名 String field = queryOne( "SELECT sField FROM viw_kg_field_dict WHERE sTable=? AND sChinese=? ORDER BY iFormUses DESC LIMIT 1", table, fieldChinese.trim()); if (field == null) { field = queryOne( "SELECT sField FROM viw_kg_field_dict WHERE sTable=? AND sChinese LIKE ? ORDER BY iFormUses DESC LIMIT 1", table, "%" + fieldChinese.trim() + "%"); } if (field == null) { return err("在该表单里找不到叫「" + fieldChinese + "」的字段,请换个字段名或先查看该表单有哪些字段。"); } // 3) 定位唯一记录 + 旧值(用名称字段过滤) String nameField = queryOne( "SELECT sField FROM viw_kg_field_dict WHERE sTable=? AND sField LIKE '%Name' AND sField NOT LIKE '%NameId' " + "ORDER BY iFormUses DESC LIMIT 1", table); JsonNode root; try { root = erp.readForm(formId.trim(), moduleId.trim(), 1, 5, nameField, recordKeyword.trim()); } catch (Exception e) { return err("定位记录时读取失败:" + e.getMessage()); } if (root.path("code").asInt(0) < 0) { return err("定位记录失败:" + 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 err("没有找到名称含「" + 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(data.get(i).path(nameField == null ? "" : nameField).asText("")); } return err("匹配到多条记录(" + names + "),请提供更精确的名称,只改其中一条。"); } JsonNode rec = data.get(0); String billId = rec.path("sId").asText(null); if (isBlank(billId)) { return err("定位到的记录缺少主键 sId,无法安全修改。"); } String oldValue = rec.path(field).asText(""); String recordName = nameField == null ? recordKeyword : rec.path(nameField).asText(recordKeyword); // 4) 暂存 draft(不执行) String description = "将【" + recordName + "】的【" + fieldChinese + "】" + (oldValue.isBlank() ? "" : ("由「" + oldValue + "」")) + "改为「" + newValue + "」"; String opId = ops.createDraft("agent", "update", formId.trim(), moduleId.trim(), table, billId, field, fieldChinese, oldValue, newValue, description); Map out = new LinkedHashMap<>(); out.put("opId", opId); out.put("summary", description); out.put("message", "已为你生成一条待确认的修改,请在下方点【确认】执行、或【取消】。"); return toJson(out); } @Tool("提议**删除**某条现有记录(写操作)。本工具只提议并暂存、绝不立即执行——必须等用户点【确认】后才真正删除。" + "删除不可恢复,请慎用。直接给出实体类型与记录名即可。") public String proposeDelete( @P("实体类型,如 客户 / 物料 / 供应商") String entityKeyword, @P("要删除的记录的名称关键词") String recordKeyword) { if (isBlank(entityKeyword) || isBlank(recordKeyword)) { return err("缺少实体类型或记录名称。"); } Map form = resolveForm(entityKeyword.trim()); if (form == null) { return err("找不到「" + entityKeyword + "」对应的可操作主表。"); } String formId = str(form.get("sFormId")); String moduleId = str(form.get("sModuleId")); String table = str(form.get("sDataSource")); if (!authz.canAccessModule(moduleId)) { return err("你没有操作「" + entityKeyword + "」的权限。"); } String nameField = queryOne( "SELECT sField FROM viw_kg_field_dict WHERE sTable=? AND sField LIKE '%Name' AND sField NOT LIKE '%NameId' " + "ORDER BY iFormUses DESC LIMIT 1", table); JsonNode root; try { root = erp.readForm(formId, moduleId, 1, 5, nameField, recordKeyword.trim()); } catch (Exception e) { return err("定位记录失败:" + e.getMessage()); } if (root.path("code").asInt(0) < 0) { return err("定位记录失败:" + 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 err("没有找到名称含「" + 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 err("匹配到多条记录(" + names + "),请提供更精确的名称,只删其中一条。"); } JsonNode rec = data.get(0); String billId = rec.path("sId").asText(null); if (isBlank(billId)) { return err("定位到的记录缺少主键 sId,无法安全删除。"); } String recordName = nameField == null ? recordKeyword : rec.path(nameField).asText(recordKeyword); String description = "删除【" + recordName + "】(" + entityKeyword + ")"; String opId = ops.createDraft("agent", "delete", formId, moduleId, table, billId, null, null, recordName, null, description); Map out = new LinkedHashMap<>(); out.put("opId", opId); out.put("summary", description); out.put("message", "已为你生成一条待确认的删除,请在下方点【确认】执行、或【取消】。删除不可恢复,请谨慎。"); return toJson(out); } /** 定位实体的可修改主表:该实体名下 table 类型、最常用(AI工具/连接度)的一张。 */ private Map resolveForm(String entityKeyword) { try { List> r = jdbc.queryForList( "SELECT af.sFormId, af.sModuleId, af.sDataSource FROM viw_ai_useful_forms af " + "LEFT JOIN viw_kg_form f ON f.sFormId = af.sFormId " + "WHERE af.sFormTitle LIKE ? AND af.sExecType='table' " + // 排除报表视图(viw_*),只取可直接改的基础主表 "AND af.sDataSource NOT LIKE 'viw%' " + "ORDER BY COALESCE(f.bAiTool,0) DESC, " + "(COALESCE(f.iUpstream,0)+COALESCE(f.iDownstream,0)) DESC, CHAR_LENGTH(af.sFormTitle) ASC LIMIT 1", "%" + entityKeyword + "%"); return r.isEmpty() ? null : r.get(0); } catch (Exception e) { return null; } } private static String str(Object o) { return o == null ? null : o.toString(); } private String queryOne(String sql, Object... args) { try { List> r = jdbc.queryForList(sql, args); if (!r.isEmpty()) { Object v = r.get(0).values().iterator().next(); return v == null ? null : v.toString(); } } catch (Exception ignore) { } return null; } private static boolean isBlank(String s) { return s == null || s.isBlank(); } 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\":\"内部错误\"}"; } } }