From e32a120571b24a931824aa8593525330938670bd Mon Sep 17 00:00:00 2001 From: zichun <26684461+reporkey@users.noreply.github.com> Date: Tue, 21 Jul 2026 19:40:43 +0800 Subject: [PATCH] feat(A): real ERP data reads (Read tool via form API) --- src/main/java/com/xly/config/AgentConfig.java | 7 +++++-- src/main/java/com/xly/service/ErpClient.java | 117 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main/java/com/xly/service/SystemPromptService.java | 10 ++++++---- src/main/java/com/xly/tool/ErpReadTool.java | 160 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main/resources/application-saaslocal.yml | 13 +++++++++++++ 5 files changed, 301 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/xly/service/ErpClient.java create mode 100644 src/main/java/com/xly/tool/ErpReadTool.java diff --git a/src/main/java/com/xly/config/AgentConfig.java b/src/main/java/com/xly/config/AgentConfig.java index 51479fd..ca6449b 100644 --- a/src/main/java/com/xly/config/AgentConfig.java +++ b/src/main/java/com/xly/config/AgentConfig.java @@ -2,6 +2,7 @@ package com.xly.config; import com.xly.agent.ReActAgent; import com.xly.service.SystemPromptService; +import com.xly.tool.ErpReadTool; import com.xly.tool.KgQueryTool; import dev.langchain4j.memory.chat.MessageWindowChatMemory; import dev.langchain4j.model.ollama.OllamaStreamingChatModel; @@ -41,11 +42,13 @@ public class AgentConfig { } @Bean - public ReActAgent reActAgent(SystemPromptService systemPromptService, KgQueryTool kgQueryTool) { + public ReActAgent reActAgent(SystemPromptService systemPromptService, + KgQueryTool kgQueryTool, + ErpReadTool erpReadTool) { String systemPrompt = systemPromptService.buildSystemPrompt(); return AiServices.builder(ReActAgent.class) .streamingChatModel(agentStreamingModel()) - .tools(kgQueryTool) + .tools(kgQueryTool, erpReadTool) .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(20)) .systemMessageProvider(memoryId -> systemPrompt) .build(); diff --git a/src/main/java/com/xly/service/ErpClient.java b/src/main/java/com/xly/service/ErpClient.java new file mode 100644 index 0000000..1fe942f --- /dev/null +++ b/src/main/java/com/xly/service/ErpClient.java @@ -0,0 +1,117 @@ +package com.xly.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.List; +import java.util.Map; + +/** + * ERP 后端(xlyEntry)薄 HTTP 客户端。 + * + *
xlyAi 的读写业务逻辑在 ERP 后端,xlyAi 只是像前端一样发同样的 API 请求。本类负责: + *
走 ERP 后端通用表单读接口({@link ErpClient})——即架构里"Read = 薄 HTTP 客户端、 + * 业务逻辑在 ERP 后端、得到租户+行级范围"。列名用字段字典({@code viw_kg_field_dict}) + * 渲染成中文,只展示前几列/前几行,避免吐爆上下文。 + * + *
安全:只传分页参数、不传任何写参数(如 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