ErpReadTool.java
6.1 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
package com.xly.tool;
import com.fasterxml.jackson.databind.JsonNode;
import com.xly.service.ErpClient;
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.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),读不会变写。
*/
@Component
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;
public ErpReadTool(ErpClient erp, JdbcTemplate jdbc) {
this.erp = erp;
this.jdbc = jdbc;
}
@Tool("读取指定 ERP 表单的真实业务数据(返回前若干行 + 总条数)。"
+ "必须先用 findForms 得到目标表单的 formId 与 moduleId,再调用本工具。")
public String readFormData(
@P("表单id(findForms 返回的 formId)") String formId,
@P("菜单id(findForms 返回的 moduleId)") String moduleId) {
if (formId == null || formId.isBlank() || moduleId == null || moduleId.isBlank()) {
return "缺少 formId 或 moduleId,请先用 findForms 检索到具体表单再调用本工具。";
}
JsonNode root;
try {
root = erp.readForm(formId.trim(), moduleId.trim(), 1, MAX_ROWS);
} 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);
StringBuilder sb = new StringBuilder();
sb.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;
}
/** 选择展示列:优先有中文名的短文本列,跳过 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;
}
}