对接扣子
This commit is contained in:
parent
f057ca5053
commit
dc08bb52d5
23
README.md
23
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.
|
||||
|
||||
@ -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<String, Workflow> 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<String, Workflow> getWorkflows() { return workflows; }
|
||||
public void setWorkflows(Map<String, Workflow> 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<String, Object> 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<String, Object> getParameters() { return parameters; }
|
||||
public void setParameters(Map<String, Object> parameters) { this.parameters = parameters; }
|
||||
}
|
||||
}
|
||||
|
||||
public static class Wecom {
|
||||
private boolean enabled;
|
||||
private String corpId;
|
||||
|
||||
@ -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()));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
package com.company.agentfoundation.integration.coze.dto;
|
||||
|
||||
public record CozeMessage(
|
||||
String content_type,
|
||||
String role,
|
||||
String type,
|
||||
String content
|
||||
) {}
|
||||
@ -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<String, Object> parameters,
|
||||
List<CozeMessage> additional_messages
|
||||
) {}
|
||||
@ -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<String, Object> parameters) {
|
||||
validateBaseConfig();
|
||||
FoundationProperties.Coze.Workflow workflow = workflow(workflowKey);
|
||||
Map<String, Object> 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<String> extractReply(JsonNode response) {
|
||||
if (response == null || response.isNull()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
Optional<String> direct = firstText(response, "answer", "content", "output", "text");
|
||||
if (direct.isPresent()) {
|
||||
return direct;
|
||||
}
|
||||
|
||||
Optional<String> 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<String> 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<String> content = firstText(message, "content", "text", "answer", "output");
|
||||
if (content.isPresent()) {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (JsonNode message : messages) {
|
||||
Optional<String> content = firstText(message, "content", "text", "answer", "output");
|
||||
if (content.isPresent()) {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private Optional<String> 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) + "...";
|
||||
}
|
||||
}
|
||||
@ -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<String, Long> 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";
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user