From dc08bb52d56563dcc58c8b0646a366ae102f51b1 Mon Sep 17 00:00:00 2001 From: tanglong <842690096@qq.com> Date: Mon, 13 Jul 2026 17:17:55 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=B9=E6=8E=A5=E6=89=A3=E5=AD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 23 ++- .../config/FoundationProperties.java | 40 ++++ .../coze/client/CozeWorkflowClient.java | 36 ++++ .../integration/coze/dto/CozeMessage.java | 8 + .../coze/dto/CozeWorkflowRequest.java | 11 ++ .../coze/service/CozeWorkflowService.java | 171 ++++++++++++++++++ .../controller/WecomCallbackController.java | 19 +- src/main/resources/application.yml | 12 ++ 8 files changed, 315 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/company/agentfoundation/integration/coze/client/CozeWorkflowClient.java create mode 100644 src/main/java/com/company/agentfoundation/integration/coze/dto/CozeMessage.java create mode 100644 src/main/java/com/company/agentfoundation/integration/coze/dto/CozeWorkflowRequest.java create mode 100644 src/main/java/com/company/agentfoundation/integration/coze/service/CozeWorkflowService.java diff --git a/README.md b/README.md index 02e3f8e..d0ca66c 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,26 @@ Java Spring Boot service for Enterprise WeChat callbacks and internal APIs. - Verifies Enterprise WeChat callback signatures - Decrypts encrypted callback payloads when safe mode is enabled - Deduplicates text messages -- Sends a fixed text reply: `你好` +- Sends Enterprise WeChat text content to the configured Coze workflow +- Sends the Coze workflow reply back to the same Enterprise WeChat user -The old internal agent orchestration code has been removed. Future intelligent -responses should be connected through Coze while keeping this callback entrypoint. +The old internal agent orchestration code has been removed. Intelligent +responses now go through Coze while keeping this callback entrypoint. + +## Coze Workflows +Configure multiple Coze workflow agents under `foundation.coze.workflows`. +Business code calls them by key, for example `wecom-default`. + +```yaml +foundation: + coze: + default-workflow: wecom-default + workflows: + wecom-default: + workflow-id: "7661923520095862811" + app-id: "7661851003527954473" + parameters: {} +``` ### Required env vars - `DEEPSEEK_API_KEY` @@ -22,6 +38,7 @@ responses should be connected through Coze while keeping this callback entrypoin - `WECOM_AGENT_ID` - `WECOM_SECRET` - `WECOM_TOKEN` +- `COZE_API_KEY` ### WeCom backend settings 1. Create a WeCom self-built app. diff --git a/src/main/java/com/company/agentfoundation/config/FoundationProperties.java b/src/main/java/com/company/agentfoundation/config/FoundationProperties.java index d3019bf..eb7f40e 100644 --- a/src/main/java/com/company/agentfoundation/config/FoundationProperties.java +++ b/src/main/java/com/company/agentfoundation/config/FoundationProperties.java @@ -10,12 +10,14 @@ public class FoundationProperties { private final Auth auth = new Auth(); private final ModelRouter modelRouter = new ModelRouter(); private final Deepseek deepseek = new Deepseek(); + private final Coze coze = new Coze(); private final Wecom wecom = new Wecom(); private final MiniProgram miniProgram = new MiniProgram(); public Auth getAuth() { return auth; } public ModelRouter getModelRouter() { return modelRouter; } public Deepseek getDeepseek() { return deepseek; } + public Coze getCoze() { return coze; } public Wecom getWecom() { return wecom; } public MiniProgram getMiniProgram() { return miniProgram; } @@ -53,6 +55,44 @@ public class FoundationProperties { public void setMaxRetries(int maxRetries) { this.maxRetries = maxRetries; } } + public static class Coze { + private String baseUrl = "https://api.coze.cn"; + private String chatPath = "/v1/workflows/chat"; + private String apiKey; + private String defaultWorkflow = "wecom-default"; + private Map workflows = new HashMap<>(); + private int readTimeoutMs = 60000; + private int maxRetries = 2; + + public String getBaseUrl() { return baseUrl; } + public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; } + public String getChatPath() { return chatPath; } + public void setChatPath(String chatPath) { this.chatPath = chatPath; } + public String getApiKey() { return apiKey; } + public void setApiKey(String apiKey) { this.apiKey = apiKey; } + public String getDefaultWorkflow() { return defaultWorkflow; } + public void setDefaultWorkflow(String defaultWorkflow) { this.defaultWorkflow = defaultWorkflow; } + public Map getWorkflows() { return workflows; } + public void setWorkflows(Map workflows) { this.workflows = workflows; } + public int getReadTimeoutMs() { return readTimeoutMs; } + public void setReadTimeoutMs(int readTimeoutMs) { this.readTimeoutMs = readTimeoutMs; } + public int getMaxRetries() { return maxRetries; } + public void setMaxRetries(int maxRetries) { this.maxRetries = maxRetries; } + + public static class Workflow { + private String workflowId; + private String appId; + private Map parameters = new HashMap<>(); + + public String getWorkflowId() { return workflowId; } + public void setWorkflowId(String workflowId) { this.workflowId = workflowId; } + public String getAppId() { return appId; } + public void setAppId(String appId) { this.appId = appId; } + public Map getParameters() { return parameters; } + public void setParameters(Map parameters) { this.parameters = parameters; } + } + } + public static class Wecom { private boolean enabled; private String corpId; diff --git a/src/main/java/com/company/agentfoundation/integration/coze/client/CozeWorkflowClient.java b/src/main/java/com/company/agentfoundation/integration/coze/client/CozeWorkflowClient.java new file mode 100644 index 0000000..9f1f786 --- /dev/null +++ b/src/main/java/com/company/agentfoundation/integration/coze/client/CozeWorkflowClient.java @@ -0,0 +1,36 @@ +package com.company.agentfoundation.integration.coze.client; + +import com.company.agentfoundation.config.FoundationProperties; +import com.company.agentfoundation.integration.coze.dto.CozeWorkflowRequest; +import com.fasterxml.jackson.databind.JsonNode; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.util.retry.Retry; + +import java.time.Duration; + +@Component +public class CozeWorkflowClient { + private final FoundationProperties properties; + private final WebClient webClient; + + public CozeWorkflowClient(FoundationProperties properties, WebClient.Builder builder) { + this.properties = properties; + this.webClient = builder.baseUrl(properties.getCoze().getBaseUrl()) + .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .build(); + } + + public JsonNode chat(CozeWorkflowRequest request) { + return webClient.post() + .uri(properties.getCoze().getChatPath()) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + properties.getCoze().getApiKey()) + .bodyValue(request) + .retrieve() + .bodyToMono(JsonNode.class) + .retryWhen(Retry.backoff(properties.getCoze().getMaxRetries(), Duration.ofMillis(300))) + .block(Duration.ofMillis(properties.getCoze().getReadTimeoutMs())); + } +} diff --git a/src/main/java/com/company/agentfoundation/integration/coze/dto/CozeMessage.java b/src/main/java/com/company/agentfoundation/integration/coze/dto/CozeMessage.java new file mode 100644 index 0000000..97eb037 --- /dev/null +++ b/src/main/java/com/company/agentfoundation/integration/coze/dto/CozeMessage.java @@ -0,0 +1,8 @@ +package com.company.agentfoundation.integration.coze.dto; + +public record CozeMessage( + String content_type, + String role, + String type, + String content +) {} diff --git a/src/main/java/com/company/agentfoundation/integration/coze/dto/CozeWorkflowRequest.java b/src/main/java/com/company/agentfoundation/integration/coze/dto/CozeWorkflowRequest.java new file mode 100644 index 0000000..9943ad9 --- /dev/null +++ b/src/main/java/com/company/agentfoundation/integration/coze/dto/CozeWorkflowRequest.java @@ -0,0 +1,11 @@ +package com.company.agentfoundation.integration.coze.dto; + +import java.util.List; +import java.util.Map; + +public record CozeWorkflowRequest( + String workflow_id, + String app_id, + Map parameters, + List additional_messages +) {} diff --git a/src/main/java/com/company/agentfoundation/integration/coze/service/CozeWorkflowService.java b/src/main/java/com/company/agentfoundation/integration/coze/service/CozeWorkflowService.java new file mode 100644 index 0000000..162f05c --- /dev/null +++ b/src/main/java/com/company/agentfoundation/integration/coze/service/CozeWorkflowService.java @@ -0,0 +1,171 @@ +package com.company.agentfoundation.integration.coze.service; + +import com.company.agentfoundation.config.FoundationProperties; +import com.company.agentfoundation.integration.coze.client.CozeWorkflowClient; +import com.company.agentfoundation.integration.coze.dto.CozeMessage; +import com.company.agentfoundation.integration.coze.dto.CozeWorkflowRequest; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +@Service +public class CozeWorkflowService { + private static final Logger log = LoggerFactory.getLogger(CozeWorkflowService.class); + + private final FoundationProperties properties; + private final CozeWorkflowClient client; + private final ObjectMapper objectMapper; + + public CozeWorkflowService(FoundationProperties properties, CozeWorkflowClient client, ObjectMapper objectMapper) { + this.properties = properties; + this.client = client; + this.objectMapper = objectMapper; + } + + public String chatDefault(String userText) { + return chat(properties.getCoze().getDefaultWorkflow(), userText); + } + + public String chat(String workflowKey, String userText) { + return chat(workflowKey, userText, Map.of()); + } + + public String chat(String workflowKey, String userText, Map parameters) { + validateBaseConfig(); + FoundationProperties.Coze.Workflow workflow = workflow(workflowKey); + Map mergedParameters = new HashMap<>(); + if (workflow.getParameters() != null) { + mergedParameters.putAll(workflow.getParameters()); + } + if (parameters != null) { + mergedParameters.putAll(parameters); + } + + CozeWorkflowRequest request = new CozeWorkflowRequest( + workflow.getWorkflowId(), + workflow.getAppId(), + mergedParameters, + List.of(new CozeMessage("text", "user", "question", userText == null ? "" : userText)) + ); + + JsonNode response = client.chat(request); + String reply = extractReply(response).orElse(""); + log.info("coze workflow chat success workflowKey={} replyPreview={}", workflowKey, preview(reply)); + return reply; + } + + private FoundationProperties.Coze.Workflow workflow(String workflowKey) { + String safeWorkflowKey = isBlank(workflowKey) ? properties.getCoze().getDefaultWorkflow() : workflowKey.trim(); + FoundationProperties.Coze.Workflow workflow = properties.getCoze().getWorkflows().get(safeWorkflowKey); + if (workflow == null) { + throw new IllegalArgumentException("Coze workflow is not configured: " + safeWorkflowKey); + } + if (isBlank(workflow.getWorkflowId())) { + throw new IllegalStateException("Coze workflow_id is missing: " + safeWorkflowKey); + } + if (isBlank(workflow.getAppId())) { + throw new IllegalStateException("Coze app_id is missing: " + safeWorkflowKey); + } + return workflow; + } + + private void validateBaseConfig() { + if (isBlank(properties.getCoze().getApiKey())) { + throw new IllegalStateException("COZE_API_KEY is missing"); + } + if (properties.getCoze().getWorkflows() == null || properties.getCoze().getWorkflows().isEmpty()) { + throw new IllegalStateException("No Coze workflows configured"); + } + } + + private Optional extractReply(JsonNode response) { + if (response == null || response.isNull()) { + return Optional.empty(); + } + + Optional direct = firstText(response, "answer", "content", "output", "text"); + if (direct.isPresent()) { + return direct; + } + + Optional nested = extractFromMessages(response.get("messages")) + .or(() -> extractFromMessages(response.path("data").get("messages"))) + .or(() -> extractReply(response.get("data"))); + if (nested.isPresent()) { + return nested; + } + + if (response.isTextual()) { + String text = response.asText(); + if (isJson(text)) { + try { + return extractReply(objectMapper.readTree(text)); + } catch (Exception e) { + log.warn("coze response text json parse failed err={}", e.getMessage()); + } + } + return isBlank(text) ? Optional.empty() : Optional.of(text.trim()); + } + + return Optional.empty(); + } + + private Optional extractFromMessages(JsonNode messages) { + if (messages == null || !messages.isArray()) { + return Optional.empty(); + } + for (JsonNode message : messages) { + String role = message.path("role").asText(""); + String type = message.path("type").asText(""); + if ("assistant".equalsIgnoreCase(role) || "answer".equalsIgnoreCase(type)) { + Optional content = firstText(message, "content", "text", "answer", "output"); + if (content.isPresent()) { + return content; + } + } + } + for (JsonNode message : messages) { + Optional content = firstText(message, "content", "text", "answer", "output"); + if (content.isPresent()) { + return content; + } + } + return Optional.empty(); + } + + private Optional firstText(JsonNode node, String... fields) { + if (node == null || node.isNull()) { + return Optional.empty(); + } + for (String field : fields) { + JsonNode value = node.get(field); + if (value != null && value.isTextual() && !isBlank(value.asText())) { + return Optional.of(value.asText().trim()); + } + } + return Optional.empty(); + } + + private boolean isJson(String text) { + String trimmed = text == null ? "" : text.trim(); + return (trimmed.startsWith("{") && trimmed.endsWith("}")) || (trimmed.startsWith("[") && trimmed.endsWith("]")); + } + + private boolean isBlank(String text) { + return text == null || text.isBlank(); + } + + private String preview(String text) { + if (text == null || text.isBlank()) { + return ""; + } + return text.length() <= 80 ? text : text.substring(0, 80) + "..."; + } +} diff --git a/src/main/java/com/company/agentfoundation/integration/wecom/controller/WecomCallbackController.java b/src/main/java/com/company/agentfoundation/integration/wecom/controller/WecomCallbackController.java index b197d6c..8ab4641 100644 --- a/src/main/java/com/company/agentfoundation/integration/wecom/controller/WecomCallbackController.java +++ b/src/main/java/com/company/agentfoundation/integration/wecom/controller/WecomCallbackController.java @@ -1,5 +1,6 @@ package com.company.agentfoundation.integration.wecom.controller; +import com.company.agentfoundation.integration.coze.service.CozeWorkflowService; import com.company.agentfoundation.integration.wecom.client.WecomClient; import com.company.agentfoundation.integration.wecom.model.WecomEncryptedEnvelope; import com.company.agentfoundation.integration.wecom.model.WecomIncomingMessage; @@ -26,15 +27,18 @@ public class WecomCallbackController { private static final Logger log = LoggerFactory.getLogger(WecomCallbackController.class); private static final long MESSAGE_DEDUP_TTL_SECONDS = 600; private static final String FIXED_REPLY = "\u4f60\u597d"; + private final CozeWorkflowService cozeWorkflowService; private final WecomClient wecomClient; private final WecomCryptoService wecomCryptoService; private final XmlMapper xmlMapper = new XmlMapper(); private final Map processedMessages = new ConcurrentHashMap<>(); public WecomCallbackController( + CozeWorkflowService cozeWorkflowService, WecomClient wecomClient, WecomCryptoService wecomCryptoService ) { + this.cozeWorkflowService = cozeWorkflowService; this.wecomClient = wecomClient; this.wecomCryptoService = wecomCryptoService; } @@ -88,8 +92,19 @@ public class WecomCallbackController { return "success"; } - log.info("wecom callback sending fixed reply toUser={} replyPreview={}", msg.getFromUserName(), preview(FIXED_REPLY)); - wecomClient.sendText(msg.getFromUserName(), FIXED_REPLY); + String reply; + try { + reply = cozeWorkflowService.chatDefault(userText); + } catch (Exception e) { + log.error("wecom callback coze workflow failed fromUser={} err={}", msg.getFromUserName(), e.getMessage(), e); + reply = FIXED_REPLY; + } + if (reply == null || reply.isBlank()) { + log.warn("wecom callback coze reply blank, fallback to fixed reply"); + reply = FIXED_REPLY; + } + log.info("wecom callback sending coze reply toUser={} replyPreview={}", msg.getFromUserName(), preview(reply)); + wecomClient.sendText(msg.getFromUserName(), reply); log.info("wecom callback reply sent toUser={}", msg.getFromUserName()); return "success"; } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 77773b9..3c3b16b 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -35,6 +35,18 @@ foundation: api-key: sk-b008441047f24558bd4e4cea817206c3 read-timeout-ms: 60000 max-retries: 2 + coze: + base-url: https://api.coze.cn + chat-path: /v1/workflows/chat + api-key: ${COZE_API_KEY:cztei_hTd3jrof7F2fpRM4rVvNiK1ipxaQl4rlvQNHKBDK1RlTgnyFO9UqwShkr8IXcuVrk} + default-workflow: wecom-default + workflows: + wecom-default: + workflow-id: "7661923520095862811" + app-id: "7661851003527954473" + parameters: {} + read-timeout-ms: 60000 + max-retries: 2 # wecom: # enabled: true # corp-id: wwaec174f56094f2b5