ErpReadTool.java
10.2 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
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 java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* Read 工具(架构 §5 #1):读取 ERP 表单的真实业务数据。两个能力:
* {@link #readFormData} 列表/计数(前若干行 + 总条数),{@link #lookupRecord} 单条命名记录的全字段。
*
* <p>走 ERP 后端通用表单读接口({@link ErpClient})——即架构里"Read = 薄 HTTP 客户端、
* 业务逻辑在 ERP 后端、得到租户+行级范围"。列名用字段字典({@code viw_kg_field_dict})
* 渲染成中文,只展示前几列/前几行,避免吐爆上下文。
*
* <p>安全:按请求身份({@link AgentIdentity})做表单级授权;只传分页/过滤参数、
* 不传任何写参数(如 bUpdate),读不会变写。
*/
public class ErpReadTool {
private static final int MAX_ROWS = 10;
private static final int MAX_COLS = 6;
private final ErpClient erp;
private final FormResolverService resolver;
private final AgentIdentity identity;
public ErpReadTool(ErpClient erp, FormResolverService resolver, AgentIdentity identity) {
this.erp = erp;
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.searchField(table, recordKeyword.trim());
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 用于按名称模糊过滤"
+ "(如查某个客户 / 物料);可选 page 翻页(从 1 开始,用户要看「下一页/更多」时递增)。"
+ "必须先用 findForms 得到目标表单的 formId 与 moduleId。")
public String readFormData(
@P("表单id(findForms 返回的 formId)") String formId,
@P("菜单id(findForms 返回的 moduleId)") String moduleId,
@P(value = "可选:仅当要查找某个具体名称的记录时才填(如某个客户名/物料名);"
+ "问数量 / 全部 / 概况时必须留空", required = false) String keyword,
@P(value = "可选:页码,从 1 开始;默认 1", required = false) Integer page) {
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 : resolver.resolveNameFieldByFormId(formId.trim());
int pageNo = page == null || page < 1 ? 1 : page;
JsonNode root;
try {
root = erp.readForm(identity.token(), formId.trim(), moduleId.trim(), pageNo, 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 = resolver.fieldLabels(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(pageNo)
.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();
}
/** 选择展示列:优先有中文名的短文本列,跳过 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;
}
}