commit 3438971d10d34a1bcb2ccb0e8d27d9629db901a7
Author: tanglong <842690096@qq.com>
Date: Mon Jun 1 19:09:40 2026 +0800
首次提交
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..16b61c5
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+/target/
+/.idea/
+*.iml
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..a0452b2
--- /dev/null
+++ b/README.md
@@ -0,0 +1,30 @@
+# Agent Foundation
+
+Java Spring Boot base platform for multiple enterprise agents (CRM assistant, knowledge-base assistant, customer service assistant).
+
+## Core API
+- POST `/v1/chat/completions`
+- GET `/v1/healthz`
+
+## WeCom CRM Bot Integration
+- Callback URL: `/v1/integrations/wecom/callback`
+- Receive enterprise WeCom text messages and route to `crm-assistant`
+- Send the model response back to the same WeCom user
+
+### Required env vars
+- `DEEPSEEK_API_KEY`
+- `WECOM_CORP_ID`
+- `WECOM_AGENT_ID`
+- `WECOM_SECRET`
+- `WECOM_TOKEN`
+
+### WeCom backend settings
+1. Create a WeCom self-built app.
+2. Enable message receive API and set callback URL to:
+ - `https://your-domain/v1/integrations/wecom/callback`
+3. Set callback Token equal to `WECOM_TOKEN`.
+4. Make sure app has send-message permission to internal users.
+
+## Run
+1. Set env vars.
+2. Start app from IDE or `mvn spring-boot:run`.
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..a200b36
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,23 @@
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.3.0
+
+
+ com.company
+ agent-foundation
+ 0.0.1-SNAPSHOT
+ 17
+
+ org.springframework.bootspring-boot-starter-web
+ org.springframework.bootspring-boot-starter-webflux
+ org.springframework.bootspring-boot-starter-validation
+ org.springframework.bootspring-boot-starter-actuator
+ com.fasterxml.jackson.dataformatjackson-dataformat-xml
+ org.springframework.bootspring-boot-starter-testtest
+
+ org.springframework.bootspring-boot-maven-plugin
+
diff --git a/src/main/java/com/company/agentfoundation/AgentFoundationApplication.java b/src/main/java/com/company/agentfoundation/AgentFoundationApplication.java
new file mode 100644
index 0000000..e8e854d
--- /dev/null
+++ b/src/main/java/com/company/agentfoundation/AgentFoundationApplication.java
@@ -0,0 +1,7 @@
+package com.company.agentfoundation;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+@SpringBootApplication
+public class AgentFoundationApplication {
+ public static void main(String[] args) { SpringApplication.run(AgentFoundationApplication.class, args); }
+}
diff --git a/src/main/java/com/company/agentfoundation/api/DeepseekChatRequest.java b/src/main/java/com/company/agentfoundation/api/DeepseekChatRequest.java
new file mode 100644
index 0000000..5743784
--- /dev/null
+++ b/src/main/java/com/company/agentfoundation/api/DeepseekChatRequest.java
@@ -0,0 +1,5 @@
+package com.company.agentfoundation.api;
+import java.util.List;
+public record DeepseekChatRequest(String model, List messages, Double temperature, Boolean stream) {
+ public record Message(String role, String content) {}
+}
diff --git a/src/main/java/com/company/agentfoundation/api/DeepseekChatResponse.java b/src/main/java/com/company/agentfoundation/api/DeepseekChatResponse.java
new file mode 100644
index 0000000..6d1f7d6
--- /dev/null
+++ b/src/main/java/com/company/agentfoundation/api/DeepseekChatResponse.java
@@ -0,0 +1,7 @@
+package com.company.agentfoundation.api;
+import java.util.List;
+public record DeepseekChatResponse(List choices, Usage usage) {
+ public record Choice(Message message) {}
+ public record Message(String role, String content) {}
+ public record Usage(Integer prompt_tokens, Integer completion_tokens, Integer total_tokens) {}
+}
diff --git a/src/main/java/com/company/agentfoundation/api/DeepseekClient.java b/src/main/java/com/company/agentfoundation/api/DeepseekClient.java
new file mode 100644
index 0000000..88a8079
--- /dev/null
+++ b/src/main/java/com/company/agentfoundation/api/DeepseekClient.java
@@ -0,0 +1,28 @@
+package com.company.agentfoundation.api;
+import com.company.agentfoundation.config.FoundationProperties;
+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 DeepseekClient {
+ private final FoundationProperties properties;
+ private final WebClient webClient;
+ public DeepseekClient(FoundationProperties properties, WebClient.Builder builder) {
+ this.properties = properties;
+ this.webClient = builder.baseUrl(properties.getDeepseek().getBaseUrl()).defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE).build();
+ }
+ public DeepseekChatResponse chat(DeepseekChatRequest request) {
+ if (properties.getDeepseek().getApiKey() == null || properties.getDeepseek().getApiKey().isBlank()) throw new IllegalStateException("DEEPSEEK_API_KEY is missing");
+ return webClient.post()
+ .uri(properties.getDeepseek().getChatPath())
+ .header(HttpHeaders.AUTHORIZATION, "Bearer " + properties.getDeepseek().getApiKey())
+ .bodyValue(request)
+ .retrieve()
+ .bodyToMono(DeepseekChatResponse.class)
+ .retryWhen(Retry.backoff(properties.getDeepseek().getMaxRetries(), Duration.ofMillis(300)))
+ .block(Duration.ofMillis(properties.getDeepseek().getReadTimeoutMs()));
+ }
+}
diff --git a/src/main/java/com/company/agentfoundation/config/FoundationConfig.java b/src/main/java/com/company/agentfoundation/config/FoundationConfig.java
new file mode 100644
index 0000000..45861b7
--- /dev/null
+++ b/src/main/java/com/company/agentfoundation/config/FoundationConfig.java
@@ -0,0 +1,6 @@
+package com.company.agentfoundation.config;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+@Configuration
+@EnableConfigurationProperties(FoundationProperties.class)
+public class FoundationConfig {}
diff --git a/src/main/java/com/company/agentfoundation/config/FoundationProperties.java b/src/main/java/com/company/agentfoundation/config/FoundationProperties.java
new file mode 100644
index 0000000..015a50e
--- /dev/null
+++ b/src/main/java/com/company/agentfoundation/config/FoundationProperties.java
@@ -0,0 +1,78 @@
+package com.company.agentfoundation.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@ConfigurationProperties(prefix = "foundation")
+public class FoundationProperties {
+ private final Auth auth = new Auth();
+ private final ModelRouter modelRouter = new ModelRouter();
+ private final Deepseek deepseek = new Deepseek();
+ private final Wecom wecom = new Wecom();
+
+ public Auth getAuth() { return auth; }
+ public ModelRouter getModelRouter() { return modelRouter; }
+ public Deepseek getDeepseek() { return deepseek; }
+ public Wecom getWecom() { return wecom; }
+
+ public static class Auth {
+ private Map keys = new HashMap<>();
+ public Map getKeys() { return keys; }
+ public void setKeys(Map keys) { this.keys = keys; }
+ }
+
+ public static class ModelRouter {
+ private String defaultModel = "deepseek-chat";
+ private Map routeByAgent = new HashMap<>();
+ public String getDefaultModel() { return defaultModel; }
+ public void setDefaultModel(String defaultModel) { this.defaultModel = defaultModel; }
+ public Map getRouteByAgent() { return routeByAgent; }
+ public void setRouteByAgent(Map routeByAgent) { this.routeByAgent = routeByAgent; }
+ }
+
+ public static class Deepseek {
+ private String baseUrl;
+ private String chatPath;
+ private String apiKey;
+ 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 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 Wecom {
+ private boolean enabled;
+ private String corpId;
+ private Integer agentId;
+ private String secret;
+ private String token;
+ private String crmAgentId = "crm-assistant";
+ private String callbackPath = "/v1/integrations/wecom/callback";
+
+ public boolean isEnabled() { return enabled; }
+ public void setEnabled(boolean enabled) { this.enabled = enabled; }
+ public String getCorpId() { return corpId; }
+ public void setCorpId(String corpId) { this.corpId = corpId; }
+ public Integer getAgentId() { return agentId; }
+ public void setAgentId(Integer agentId) { this.agentId = agentId; }
+ public String getSecret() { return secret; }
+ public void setSecret(String secret) { this.secret = secret; }
+ public String getToken() { return token; }
+ public void setToken(String token) { this.token = token; }
+ public String getCrmAgentId() { return crmAgentId; }
+ public void setCrmAgentId(String crmAgentId) { this.crmAgentId = crmAgentId; }
+ public String getCallbackPath() { return callbackPath; }
+ public void setCallbackPath(String callbackPath) { this.callbackPath = callbackPath; }
+ }
+}
diff --git a/src/main/java/com/company/agentfoundation/controller/ChatController.java b/src/main/java/com/company/agentfoundation/controller/ChatController.java
new file mode 100644
index 0000000..d6d44f1
--- /dev/null
+++ b/src/main/java/com/company/agentfoundation/controller/ChatController.java
@@ -0,0 +1,23 @@
+package com.company.agentfoundation.controller;
+import com.company.agentfoundation.domain.ChatRequest;
+import com.company.agentfoundation.domain.ChatResponse;
+import com.company.agentfoundation.security.AgentAuthService;
+import com.company.agentfoundation.security.AgentContext;
+import com.company.agentfoundation.service.ChatService;
+import jakarta.validation.Valid;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+@RestController
+@RequestMapping("/v1")
+public class ChatController {
+ private final AgentAuthService authService;
+ private final ChatService chatService;
+ public ChatController(AgentAuthService authService, ChatService chatService) { this.authService = authService; this.chatService = chatService; }
+ @PostMapping("/chat/completions")
+ public ResponseEntity chat(@RequestHeader("X-Agent-Id") String agentId, @RequestHeader("X-Internal-Key") String internalKey, @Valid @RequestBody ChatRequest request) {
+ AgentContext context = authService.authenticate(agentId, internalKey);
+ return ResponseEntity.ok(chatService.chat(context, request));
+ }
+ @GetMapping("/healthz")
+ public ResponseEntity healthz() { return ResponseEntity.ok("ok"); }
+}
diff --git a/src/main/java/com/company/agentfoundation/controller/GlobalExceptionHandler.java b/src/main/java/com/company/agentfoundation/controller/GlobalExceptionHandler.java
new file mode 100644
index 0000000..148b1be
--- /dev/null
+++ b/src/main/java/com/company/agentfoundation/controller/GlobalExceptionHandler.java
@@ -0,0 +1,38 @@
+package com.company.agentfoundation.controller;
+
+import com.company.agentfoundation.security.UnauthorizedException;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.Map;
+
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+ @ExceptionHandler(UnauthorizedException.class)
+ public ResponseEntity