package com.xly.tool;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
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;
public ProposeWriteTool(ErpClient erp, JdbcTemplate jdbc, OpService ops, ObjectMapper mapper) {
this.erp = erp;
this.jdbc = jdbc;
this.ops = ops;
this.mapper = mapper;
}
@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"));
// 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);
}
/** 定位实体的可修改主表:该实体名下 table 类型、最常用(AI工具/连接度)的一张。 */
private Map resolveForm(String entityKeyword) {
try {
List