ErpReadTool.java
11.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
package com.xly.tool;
import com.fasterxml.jackson.databind.JsonNode;
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.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),读不会变写。
*/
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;
private final AgentIdentity identity;
public ErpReadTool(ErpClient erp, JdbcTemplate jdbc, FormResolverService resolver, AgentIdentity identity) {
this.erp = erp;
this.jdbc = jdbc;
this.resolver = resolver;
this.identity = identity;
}
@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"));
if (!identity.canAccessModule(moduleId)) {
return "你没有访问「" + entityKeyword + "」的权限。";
}
String nameField = resolver.resolveNameField(table);
JsonNode root;
try {
root = erp.readForm(identity.token(), 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 检索到具体表单再调用本工具。";
}
if (!identity.canAccessModule(moduleId.trim())) {
return "你没有访问该表单的权限。";
}
String kw = (keyword == null) ? "" : keyword.trim();
String nameField = kw.isEmpty() ? null : resolveNameField(formId.trim());
JsonNode root;
try {
root = erp.readForm(identity.token(), 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;
}
}