From 2adde55e082ce505bcc7ac802957faade164c90c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=81=B0=E7=81=B0?= <654612@qq.com>
Date: Tue, 24 Mar 2026 19:41:49 +0800
Subject: [PATCH] feat: Implement the initial AI-native CRM backend application
with Spring Boot, LangChain4j, and JPA persistence.
---
.idea/.gitignore | 10 ++
.idea/compiler.xml | 19 +++
.idea/encodings.xml | 6 +
.idea/jarRepositories.xml | 20 +++
.idea/misc.xml | 12 ++
pom.xml | 78 ++++++++++++
.../example/ainative/AiNativeApplication.java | 13 ++
.../com/example/ainative/ai/CrmAgent.java | 21 ++++
.../com/example/ainative/ai/CrmTools.java | 54 ++++++++
.../ai/PersistentChatMemoryStore.java | 52 ++++++++
.../com/example/ainative/config/AiConfig.java | 20 +++
.../ainative/config/UserContextHolder.java | 17 +++
.../ainative/config/UserInterceptor.java | 25 ++++
.../example/ainative/config/WebMvcConfig.java | 18 +++
.../ainative/controller/AgentController.java | 32 +++++
.../controller/DynamicDataController.java | 116 ++++++++++++++++++
.../controller/SysEntityController.java | 33 +++++
.../ainative/entity/ChatMemoryEntity.java | 26 ++++
.../ainative/entity/SysDynamicData.java | 33 +++++
.../example/ainative/entity/SysEntity.java | 38 ++++++
.../repository/ChatMemoryRepository.java | 11 ++
.../repository/SysDynamicDataRepository.java | 11 ++
.../repository/SysEntityRepository.java | 10 ++
src/main/resources/application.yml | 28 +++++
target/classes/application.yml | 28 +++++
.../compile/default-compile/inputFiles.lst | 17 +++
26 files changed, 748 insertions(+)
create mode 100644 .idea/.gitignore
create mode 100644 .idea/compiler.xml
create mode 100644 .idea/encodings.xml
create mode 100644 .idea/jarRepositories.xml
create mode 100644 .idea/misc.xml
create mode 100644 pom.xml
create mode 100644 src/main/java/com/example/ainative/AiNativeApplication.java
create mode 100644 src/main/java/com/example/ainative/ai/CrmAgent.java
create mode 100644 src/main/java/com/example/ainative/ai/CrmTools.java
create mode 100644 src/main/java/com/example/ainative/ai/PersistentChatMemoryStore.java
create mode 100644 src/main/java/com/example/ainative/config/AiConfig.java
create mode 100644 src/main/java/com/example/ainative/config/UserContextHolder.java
create mode 100644 src/main/java/com/example/ainative/config/UserInterceptor.java
create mode 100644 src/main/java/com/example/ainative/config/WebMvcConfig.java
create mode 100644 src/main/java/com/example/ainative/controller/AgentController.java
create mode 100644 src/main/java/com/example/ainative/controller/DynamicDataController.java
create mode 100644 src/main/java/com/example/ainative/controller/SysEntityController.java
create mode 100644 src/main/java/com/example/ainative/entity/ChatMemoryEntity.java
create mode 100644 src/main/java/com/example/ainative/entity/SysDynamicData.java
create mode 100644 src/main/java/com/example/ainative/entity/SysEntity.java
create mode 100644 src/main/java/com/example/ainative/repository/ChatMemoryRepository.java
create mode 100644 src/main/java/com/example/ainative/repository/SysDynamicDataRepository.java
create mode 100644 src/main/java/com/example/ainative/repository/SysEntityRepository.java
create mode 100644 src/main/resources/application.yml
create mode 100644 target/classes/application.yml
create mode 100644 target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..b6b1ecf
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,10 @@
+# 默认忽略的文件
+/shelf/
+/workspace.xml
+# 已忽略包含查询文件的默认文件夹
+/queries/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
+# 基于编辑器的 HTTP 客户端请求
+/httpRequests/
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..ec8fe1b
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/encodings.xml b/.idea/encodings.xml
new file mode 100644
index 0000000..63e9001
--- /dev/null
+++ b/.idea/encodings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
new file mode 100644
index 0000000..abb532a
--- /dev/null
+++ b/.idea/jarRepositories.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..6546f14
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..b59ddcb
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,78 @@
+
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.2.4
+
+
+ com.example
+ ai-native-backend
+ 0.0.1-SNAPSHOT
+ ai-native-backend
+ AI-Native CRM Backend
+
+ 21
+ 0.30.0
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+ org.postgresql
+ postgresql
+ runtime
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ dev.langchain4j
+ langchain4j-spring-boot-starter
+ ${langchain4j.version}
+
+
+ dev.langchain4j
+ langchain4j-open-ai-spring-boot-starter
+ ${langchain4j.version}
+
+
+
+ io.hypersistence
+ hypersistence-utils-hibernate-63
+ 3.7.3
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+
+
+
diff --git a/src/main/java/com/example/ainative/AiNativeApplication.java b/src/main/java/com/example/ainative/AiNativeApplication.java
new file mode 100644
index 0000000..66b27a2
--- /dev/null
+++ b/src/main/java/com/example/ainative/AiNativeApplication.java
@@ -0,0 +1,13 @@
+package com.example.ainative;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class AiNativeApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(AiNativeApplication.class, args);
+ }
+
+}
diff --git a/src/main/java/com/example/ainative/ai/CrmAgent.java b/src/main/java/com/example/ainative/ai/CrmAgent.java
new file mode 100644
index 0000000..4d6089d
--- /dev/null
+++ b/src/main/java/com/example/ainative/ai/CrmAgent.java
@@ -0,0 +1,21 @@
+package com.example.ainative.ai;
+
+import dev.langchain4j.service.MemoryId;
+import dev.langchain4j.service.SystemMessage;
+import dev.langchain4j.service.UserMessage;
+import dev.langchain4j.service.V;
+import dev.langchain4j.service.spring.AiService;
+
+@AiService
+public interface CrmAgent {
+
+ @SystemMessage("""
+ You are a smart AI assistant embedded within a modern enterprise CRM system.
+ Your goal is to help the user configure business modules, dynamic forms, and insert data.
+ You have tools to define database schema structures (JSON Schema) and manipulate dynamic data.
+ Always respond concisely. If you need to manipulate the UI, explain what you are doing.
+
+ Current User/Tenant context ID: {{userContext}}
+ """)
+ String chat(@MemoryId String memoryId, @UserMessage String userMessage, @V("userContext") String userContext);
+}
diff --git a/src/main/java/com/example/ainative/ai/CrmTools.java b/src/main/java/com/example/ainative/ai/CrmTools.java
new file mode 100644
index 0000000..523a815
--- /dev/null
+++ b/src/main/java/com/example/ainative/ai/CrmTools.java
@@ -0,0 +1,54 @@
+package com.example.ainative.ai;
+
+import com.example.ainative.entity.SysEntity;
+import com.example.ainative.repository.SysEntityRepository;
+import dev.langchain4j.agent.tool.Tool;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.UUID;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class CrmTools {
+
+ private final SysEntityRepository sysEntityRepository;
+
+ /**
+ * AI 工具:用于定义新的动态业务对象(Entity)。
+ * 当用户试图创建新表单、新模块(比如“线索管理”、“合同管理”)时,大模型会自动决定调用本工具,
+ * 并将它根据系统指令生成的参数传入进来。
+ *
+ * @param entityCode 模块唯一标识符,英文,比如 crm_lead (便于后期通过接口精确操作某模块)
+ * @param entityName 模块中文名称跨,比如 销售线索
+ * @param jsonSchema 这里是大模型输出的最核心数据:一个合规的 JSON Schema (如属性定义、校验规则),由前端随后解析渲染
+ * @return 返回一段让 AI 知道是否成功的指令结果,这个结果大模型能看到并进一步回答用户。
+ */
+ @Tool("创建一个新的业务表单模块,当你确定了用户创建新功能的意图后调用。参数包括:entityCode (英文唯一标识), entityName (中文名称), jsonSchema。")
+ public String defineDynamicModule(String entityCode, String entityName, String jsonSchema) {
+ log.info("Agent 正在调用工具执行创建新模块: Code={}, Name={}", entityCode, entityName);
+
+ // 1. 业务校验:防重和容错
+ SysEntity existing = sysEntityRepository.findByEntityCode(entityCode);
+ if (existing != null) {
+ return "模块代码 [" + entityCode + "] 已存在,创建失败,请让用户考虑使用其他代码名称。";
+ }
+
+ // 2. 实体赋值与入库
+ try {
+ SysEntity entity = new SysEntity();
+ entity.setEntityCode(entityCode);
+ entity.setEntityName(entityName);
+ // 此处直接存储大模型生成的 JSON 字符串(PostgreSQL 的 Hibernate 驱动会将其封装为 JSONB)
+ entity.setSchemaDefinition(jsonSchema);
+
+ SysEntity saved = sysEntityRepository.save(entity);
+ return "核心模块 [" + entityName + "] 已成功持久化并部署到系统中。后台对应的 Entity ID (UUID) 为: " + saved.getId();
+ } catch (Exception e) {
+ log.error("保存 Schema 时出现异常", e);
+ return "数据库保存异常,请检查 JSON 结构是否合规。";
+ }
+ }
+}
diff --git a/src/main/java/com/example/ainative/ai/PersistentChatMemoryStore.java b/src/main/java/com/example/ainative/ai/PersistentChatMemoryStore.java
new file mode 100644
index 0000000..a44cfe6
--- /dev/null
+++ b/src/main/java/com/example/ainative/ai/PersistentChatMemoryStore.java
@@ -0,0 +1,52 @@
+package com.example.ainative.ai;
+
+import com.example.ainative.entity.ChatMemoryEntity;
+import com.example.ainative.repository.ChatMemoryRepository;
+import dev.langchain4j.data.message.ChatMessage;
+import dev.langchain4j.data.message.ChatMessageDeserializer;
+import dev.langchain4j.data.message.ChatMessageSerializer;
+import dev.langchain4j.store.memory.chat.ChatMemoryStore;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Component
+@RequiredArgsConstructor
+public class PersistentChatMemoryStore implements ChatMemoryStore {
+
+ private final ChatMemoryRepository repository;
+
+ @Override
+ public List getMessages(Object memoryId) {
+ String sessionId = (String) memoryId;
+ List entities = repository.findByChatSessionIdOrderByIdAsc(sessionId);
+ return entities.stream()
+ .map(entity -> ChatMessageDeserializer.messageFromJson(entity.getContent()))
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ @Transactional
+ public void updateMessages(Object memoryId, List messages) {
+ String sessionId = (String) memoryId;
+ repository.deleteByChatSessionId(sessionId);
+
+ List entities = messages.stream().map(message -> {
+ ChatMemoryEntity entity = new ChatMemoryEntity();
+ entity.setChatSessionId(sessionId);
+ entity.setContent(ChatMessageSerializer.messageToJson(message));
+ return entity;
+ }).collect(Collectors.toList());
+
+ repository.saveAll(entities);
+ }
+
+ @Override
+ @Transactional
+ public void deleteMessages(Object memoryId) {
+ repository.deleteByChatSessionId((String) memoryId);
+ }
+}
diff --git a/src/main/java/com/example/ainative/config/AiConfig.java b/src/main/java/com/example/ainative/config/AiConfig.java
new file mode 100644
index 0000000..06ac26a
--- /dev/null
+++ b/src/main/java/com/example/ainative/config/AiConfig.java
@@ -0,0 +1,20 @@
+package com.example.ainative.config;
+
+import dev.langchain4j.memory.chat.ChatMemoryProvider;
+import dev.langchain4j.memory.chat.MessageWindowChatMemory;
+import dev.langchain4j.store.memory.chat.ChatMemoryStore;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class AiConfig {
+
+ @Bean
+ public ChatMemoryProvider chatMemoryProvider(ChatMemoryStore chatMemoryStore) {
+ return memoryId -> MessageWindowChatMemory.builder()
+ .id(memoryId)
+ .maxMessages(50) // Keep last 50 messages in context
+ .chatMemoryStore(chatMemoryStore)
+ .build();
+ }
+}
diff --git a/src/main/java/com/example/ainative/config/UserContextHolder.java b/src/main/java/com/example/ainative/config/UserContextHolder.java
new file mode 100644
index 0000000..5e592bd
--- /dev/null
+++ b/src/main/java/com/example/ainative/config/UserContextHolder.java
@@ -0,0 +1,17 @@
+package com.example.ainative.config;
+
+public class UserContextHolder {
+ private static final ThreadLocal currentUser = new ThreadLocal<>();
+
+ public static void setCurrentUser(String userId) {
+ currentUser.set(userId);
+ }
+
+ public static String getCurrentUser() {
+ return currentUser.get() != null ? currentUser.get() : "anonymous";
+ }
+
+ public static void clear() {
+ currentUser.remove();
+ }
+}
diff --git a/src/main/java/com/example/ainative/config/UserInterceptor.java b/src/main/java/com/example/ainative/config/UserInterceptor.java
new file mode 100644
index 0000000..52dfa70
--- /dev/null
+++ b/src/main/java/com/example/ainative/config/UserInterceptor.java
@@ -0,0 +1,25 @@
+package com.example.ainative.config;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.stereotype.Component;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+@Component
+public class UserInterceptor implements HandlerInterceptor {
+ @Override
+ public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
+ String userId = request.getHeader("X-Creator-Id");
+ if (userId != null && !userId.isBlank()) {
+ UserContextHolder.setCurrentUser(userId);
+ } else {
+ UserContextHolder.setCurrentUser("anonymous");
+ }
+ return true;
+ }
+
+ @Override
+ public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
+ UserContextHolder.clear();
+ }
+}
diff --git a/src/main/java/com/example/ainative/config/WebMvcConfig.java b/src/main/java/com/example/ainative/config/WebMvcConfig.java
new file mode 100644
index 0000000..598d136
--- /dev/null
+++ b/src/main/java/com/example/ainative/config/WebMvcConfig.java
@@ -0,0 +1,18 @@
+package com.example.ainative.config;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+@RequiredArgsConstructor
+public class WebMvcConfig implements WebMvcConfigurer {
+
+ private final UserInterceptor userInterceptor;
+
+ @Override
+ public void addInterceptors(InterceptorRegistry registry) {
+ registry.addInterceptor(userInterceptor).addPathPatterns("/**");
+ }
+}
diff --git a/src/main/java/com/example/ainative/controller/AgentController.java b/src/main/java/com/example/ainative/controller/AgentController.java
new file mode 100644
index 0000000..81e1bc4
--- /dev/null
+++ b/src/main/java/com/example/ainative/controller/AgentController.java
@@ -0,0 +1,32 @@
+package com.example.ainative.controller;
+
+import com.example.ainative.ai.CrmAgent;
+import com.example.ainative.config.UserContextHolder;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/api/agent")
+@RequiredArgsConstructor
+public class AgentController {
+
+ private final CrmAgent crmAgent;
+
+ /**
+ * 对话交互核心入口
+ * 这里提供了一个传统的 REST 包装版本。当前端(如 CopilotKit 或测试客户端)发送自然语言时,会调用此处。
+ *
+ * @param sessionId 唯一会话识别(与底层的 ChatMemory 持久化紧密对应,确保 AI 能“记住”对话)
+ * @param message 用户的原始需求 (比如:"帮我建一个客户关系系统的线索录入表,有姓名和手机号")
+ * @return AI 所思所想或执行完 @Tool 后的结果字符串反馈
+ */
+ @PostMapping("/chat")
+ public String chat(@RequestParam String sessionId, @RequestBody String message) {
+ // 先从拦截器上下文中拿到刚才设置的调用者身份
+ String currentUser = UserContextHolder.getCurrentUser();
+
+ // 传递给底层的 Agent:这背后的复杂工作流完全交由 Langchain4J 托管!
+ // 如果用户的 message 意图命中“建表”,框架会在返回前自己拦截调去跑 CrmTools 里的 defineDynamicModule。
+ return crmAgent.chat(sessionId, message, currentUser);
+ }
+}
diff --git a/src/main/java/com/example/ainative/controller/DynamicDataController.java b/src/main/java/com/example/ainative/controller/DynamicDataController.java
new file mode 100644
index 0000000..1404997
--- /dev/null
+++ b/src/main/java/com/example/ainative/controller/DynamicDataController.java
@@ -0,0 +1,116 @@
+package com.example.ainative.controller;
+
+import com.example.ainative.config.UserContextHolder;
+import com.example.ainative.entity.SysDynamicData;
+import com.example.ainative.entity.SysEntity;
+import com.example.ainative.repository.SysDynamicDataRepository;
+import com.example.ainative.repository.SysEntityRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * 动态数据交互控制器
+ * 负责接收 AI 生成的 UI 提交过来的所有结构不固定的 JSON 数据。
+ */
+@RestController
+@RequestMapping("/api/dynamic")
+@RequiredArgsConstructor
+@CrossOrigin(origins = "*") // 允许前端 Next.js (通常是 3000 端口) 进行跨域调用
+public class DynamicDataController {
+
+ private final SysDynamicDataRepository dataRepository;
+ private final SysEntityRepository entityRepository;
+
+ /**
+ * 根据模块 UUID 获取数据列表
+ */
+ @GetMapping("/{entityId}")
+ public List getRecords(@PathVariable UUID entityId) {
+ return dataRepository.findByEntityId(entityId);
+ }
+
+ /**
+ * 根据模块 code 获取数据列表(前端动态表格主要用此接口方便查询)
+ */
+ @GetMapping("/code/{entityCode}")
+ public List getRecordsByCode(@PathVariable String entityCode) {
+ SysEntity entity = entityRepository.findByEntityCode(entityCode);
+ if (entity != null) {
+ return dataRepository.findByEntityId(entity.getId());
+ }
+ return List.of();
+ }
+
+ /**
+ * 自动注册/获取动态模块接口:解决前端渲染表单和后端实体记录未创建的时序问题
+ * 允许前端在提交实际数据前,主动将推断出的模块结构注册到数据库。
+ */
+ @PostMapping("/schema")
+ public SysEntity defineSchema(@RequestBody SysEntity requestedEntity) {
+ SysEntity entity = entityRepository.findByEntityCode(requestedEntity.getEntityCode());
+ if (entity == null) {
+ entity = new SysEntity();
+ entity.setEntityCode(requestedEntity.getEntityCode());
+ entity.setEntityName(requestedEntity.getEntityName());
+ entity.setSchemaDefinition(requestedEntity.getSchemaDefinition());
+ return entityRepository.save(entity);
+ }
+ return entity;
+ }
+
+ /**
+ * 核心接口:按 entityCode 直接入库
+ * 前端不需要知道复杂的 UUID,只需根据 AI 生成的 crm_lead 等 Code 即可提交。
+ */
+ @PostMapping("/code/{entityCode}")
+ public SysDynamicData createRecordByCode(@PathVariable String entityCode, @RequestBody String jsonData) {
+ // 1. 先通过 Code 找到模块定义的 ID
+ SysEntity entity = entityRepository.findByEntityCode(entityCode);
+ if (entity == null) {
+ throw new RuntimeException("模块 " + entityCode + " 尚未定义,无法入库。请先让 AI 创建该模块。");
+ }
+
+ // 2. 构造动态数据记录
+ SysDynamicData record = new SysDynamicData();
+ record.setEntityId(entity.getId());
+ record.setData(jsonData); // 这里的 String 在 JSONB 映射下会自动存为二进制 JSON 格式
+ record.setCreatorId(UserContextHolder.getCurrentUser());
+
+ return dataRepository.save(record);
+ }
+
+ /**
+ * 原始的 UUID 入库接口 (保留备用)
+ */
+ @PostMapping("/{entityId}")
+ public SysDynamicData createRecord(@PathVariable UUID entityId, @RequestBody String jsonData) {
+ SysDynamicData record = new SysDynamicData();
+ record.setEntityId(entityId);
+ record.setData(jsonData);
+ record.setCreatorId(UserContextHolder.getCurrentUser());
+
+ return dataRepository.save(record);
+ }
+
+ /**
+ * 修改现有记录
+ */
+ @PutMapping("/{id}")
+ public SysDynamicData updateRecord(@PathVariable UUID id, @RequestBody String jsonData) {
+ return dataRepository.findById(id).map(record -> {
+ record.setData(jsonData);
+ return dataRepository.save(record);
+ }).orElseThrow(() -> new RuntimeException("Record not found"));
+ }
+
+ /**
+ * 删除现有记录
+ */
+ @DeleteMapping("/{id}")
+ public void deleteRecord(@PathVariable UUID id) {
+ dataRepository.deleteById(id);
+ }
+}
diff --git a/src/main/java/com/example/ainative/controller/SysEntityController.java b/src/main/java/com/example/ainative/controller/SysEntityController.java
new file mode 100644
index 0000000..4ddbd11
--- /dev/null
+++ b/src/main/java/com/example/ainative/controller/SysEntityController.java
@@ -0,0 +1,33 @@
+package com.example.ainative.controller;
+
+import com.example.ainative.entity.SysEntity;
+import com.example.ainative.repository.SysEntityRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/entities")
+@RequiredArgsConstructor
+@CrossOrigin(origins = "*") // Allow frontend to fetch menus
+public class SysEntityController {
+
+ private final SysEntityRepository sysEntityRepository;
+
+ /**
+ * 获取所有已定义的模块列表,供左侧菜单动态渲染使用。
+ */
+ @GetMapping
+ public List getAllEntities() {
+ return sysEntityRepository.findAll();
+ }
+
+ /**
+ * 根据 code 获取单一实体定义,方便前台表格按需渲染列等动态行为。
+ */
+ @GetMapping("/code/{entityCode}")
+ public SysEntity getEntityByCode(@PathVariable String entityCode) {
+ return sysEntityRepository.findByEntityCode(entityCode);
+ }
+}
diff --git a/src/main/java/com/example/ainative/entity/ChatMemoryEntity.java b/src/main/java/com/example/ainative/entity/ChatMemoryEntity.java
new file mode 100644
index 0000000..8e65a3d
--- /dev/null
+++ b/src/main/java/com/example/ainative/entity/ChatMemoryEntity.java
@@ -0,0 +1,26 @@
+package com.example.ainative.entity;
+
+import jakarta.persistence.*;
+import lombok.Data;
+import org.hibernate.annotations.CreationTimestamp;
+
+import java.time.ZonedDateTime;
+
+@Data
+@Entity
+@Table(name = "chat_memory")
+public class ChatMemoryEntity {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(name = "chat_session_id", nullable = false)
+ private String chatSessionId;
+
+ @Column(name = "content", nullable = false, columnDefinition = "TEXT")
+ private String content;
+
+ @CreationTimestamp
+ @Column(name = "created_at", updatable = false)
+ private ZonedDateTime createdAt;
+}
diff --git a/src/main/java/com/example/ainative/entity/SysDynamicData.java b/src/main/java/com/example/ainative/entity/SysDynamicData.java
new file mode 100644
index 0000000..314502e
--- /dev/null
+++ b/src/main/java/com/example/ainative/entity/SysDynamicData.java
@@ -0,0 +1,33 @@
+package com.example.ainative.entity;
+
+import jakarta.persistence.*;
+import lombok.Data;
+import org.hibernate.annotations.CreationTimestamp;
+import org.hibernate.annotations.JdbcTypeCode;
+import org.hibernate.type.SqlTypes;
+
+import java.time.ZonedDateTime;
+import java.util.UUID;
+
+@Data
+@Entity
+@Table(name = "sys_dynamic_data")
+public class SysDynamicData {
+ @Id
+ @GeneratedValue(strategy = GenerationType.UUID)
+ private UUID id;
+
+ @Column(name = "entity_id")
+ private UUID entityId;
+
+ @JdbcTypeCode(SqlTypes.JSON)
+ @Column(name = "data", columnDefinition = "jsonb", nullable = false)
+ private String data;
+
+ @Column(name = "creator_id", length = 50)
+ private String creatorId;
+
+ @CreationTimestamp
+ @Column(name = "created_at", updatable = false)
+ private ZonedDateTime createdAt;
+}
diff --git a/src/main/java/com/example/ainative/entity/SysEntity.java b/src/main/java/com/example/ainative/entity/SysEntity.java
new file mode 100644
index 0000000..25264e9
--- /dev/null
+++ b/src/main/java/com/example/ainative/entity/SysEntity.java
@@ -0,0 +1,38 @@
+package com.example.ainative.entity;
+
+import jakarta.persistence.*;
+import lombok.Data;
+import org.hibernate.annotations.CreationTimestamp;
+import org.hibernate.annotations.JdbcTypeCode;
+import org.hibernate.annotations.UpdateTimestamp;
+import org.hibernate.type.SqlTypes;
+
+import java.time.ZonedDateTime;
+import java.util.UUID;
+
+@Data
+@Entity
+@Table(name = "sys_entities")
+public class SysEntity {
+ @Id
+ @GeneratedValue(strategy = GenerationType.UUID)
+ private UUID id;
+
+ @Column(name = "entity_code", unique = true, nullable = false, length = 50)
+ private String entityCode;
+
+ @Column(name = "entity_name", nullable = false, length = 100)
+ private String entityName;
+
+ @JdbcTypeCode(SqlTypes.JSON)
+ @Column(name = "schema_definition", columnDefinition = "jsonb", nullable = false)
+ private String schemaDefinition;
+
+ @CreationTimestamp
+ @Column(name = "created_at", updatable = false)
+ private ZonedDateTime createdAt;
+
+ @UpdateTimestamp
+ @Column(name = "updated_at")
+ private ZonedDateTime updatedAt;
+}
diff --git a/src/main/java/com/example/ainative/repository/ChatMemoryRepository.java b/src/main/java/com/example/ainative/repository/ChatMemoryRepository.java
new file mode 100644
index 0000000..546aa04
--- /dev/null
+++ b/src/main/java/com/example/ainative/repository/ChatMemoryRepository.java
@@ -0,0 +1,11 @@
+package com.example.ainative.repository;
+
+import com.example.ainative.entity.ChatMemoryEntity;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.List;
+
+public interface ChatMemoryRepository extends JpaRepository {
+ List findByChatSessionIdOrderByIdAsc(String chatSessionId);
+ void deleteByChatSessionId(String chatSessionId);
+}
diff --git a/src/main/java/com/example/ainative/repository/SysDynamicDataRepository.java b/src/main/java/com/example/ainative/repository/SysDynamicDataRepository.java
new file mode 100644
index 0000000..8973ea7
--- /dev/null
+++ b/src/main/java/com/example/ainative/repository/SysDynamicDataRepository.java
@@ -0,0 +1,11 @@
+package com.example.ainative.repository;
+
+import com.example.ainative.entity.SysDynamicData;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.List;
+import java.util.UUID;
+
+public interface SysDynamicDataRepository extends JpaRepository {
+ List findByEntityId(UUID entityId);
+}
diff --git a/src/main/java/com/example/ainative/repository/SysEntityRepository.java b/src/main/java/com/example/ainative/repository/SysEntityRepository.java
new file mode 100644
index 0000000..0c63d42
--- /dev/null
+++ b/src/main/java/com/example/ainative/repository/SysEntityRepository.java
@@ -0,0 +1,10 @@
+package com.example.ainative.repository;
+
+import com.example.ainative.entity.SysEntity;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.UUID;
+
+public interface SysEntityRepository extends JpaRepository {
+ SysEntity findByEntityCode(String entityCode);
+}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
new file mode 100644
index 0000000..aea9df9
--- /dev/null
+++ b/src/main/resources/application.yml
@@ -0,0 +1,28 @@
+spring:
+ application:
+ name: ai-native-backend
+ datasource:
+ url: jdbc:postgresql://localhost:5432/ai_native_db
+ username: ai_agent_user
+ password: Agent@2026
+ driver-class-name: org.postgresql.Driver
+ jpa:
+ hibernate:
+ ddl-auto: none
+ show-sql: true
+ properties:
+ hibernate:
+ format_sql: true
+ dialect: org.hibernate.dialect.PostgreSQLDialect
+
+langchain4j:
+ open-ai:
+ chat-model:
+ api-key: ${ZHIPU_API_KEY:61833436cbd642ed844d0128a99b2e89.PTbpERpysO3qSf8w}
+ base-url: https://open.bigmodel.cn/api/paas/v4/
+ model-name: glm-4-plus
+ log-requests: true
+ log-responses: true
+
+server:
+ port: 8080
diff --git a/target/classes/application.yml b/target/classes/application.yml
new file mode 100644
index 0000000..aea9df9
--- /dev/null
+++ b/target/classes/application.yml
@@ -0,0 +1,28 @@
+spring:
+ application:
+ name: ai-native-backend
+ datasource:
+ url: jdbc:postgresql://localhost:5432/ai_native_db
+ username: ai_agent_user
+ password: Agent@2026
+ driver-class-name: org.postgresql.Driver
+ jpa:
+ hibernate:
+ ddl-auto: none
+ show-sql: true
+ properties:
+ hibernate:
+ format_sql: true
+ dialect: org.hibernate.dialect.PostgreSQLDialect
+
+langchain4j:
+ open-ai:
+ chat-model:
+ api-key: ${ZHIPU_API_KEY:61833436cbd642ed844d0128a99b2e89.PTbpERpysO3qSf8w}
+ base-url: https://open.bigmodel.cn/api/paas/v4/
+ model-name: glm-4-plus
+ log-requests: true
+ log-responses: true
+
+server:
+ port: 8080
diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
new file mode 100644
index 0000000..2bbb204
--- /dev/null
+++ b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
@@ -0,0 +1,17 @@
+/Users/hui/project/ai/backend/src/main/java/com/example/ainative/entity/SysDynamicData.java
+/Users/hui/project/ai/backend/src/main/java/com/example/ainative/controller/AgentController.java
+/Users/hui/project/ai/backend/src/main/java/com/example/ainative/config/UserContextHolder.java
+/Users/hui/project/ai/backend/src/main/java/com/example/ainative/controller/DynamicDataController.java
+/Users/hui/project/ai/backend/src/main/java/com/example/ainative/ai/PersistentChatMemoryStore.java
+/Users/hui/project/ai/backend/src/main/java/com/example/ainative/entity/SysEntity.java
+/Users/hui/project/ai/backend/src/main/java/com/example/ainative/ai/CrmTools.java
+/Users/hui/project/ai/backend/src/main/java/com/example/ainative/entity/ChatMemoryEntity.java
+/Users/hui/project/ai/backend/src/main/java/com/example/ainative/controller/SysEntityController.java
+/Users/hui/project/ai/backend/src/main/java/com/example/ainative/AiNativeApplication.java
+/Users/hui/project/ai/backend/src/main/java/com/example/ainative/config/WebMvcConfig.java
+/Users/hui/project/ai/backend/src/main/java/com/example/ainative/repository/SysDynamicDataRepository.java
+/Users/hui/project/ai/backend/src/main/java/com/example/ainative/config/UserInterceptor.java
+/Users/hui/project/ai/backend/src/main/java/com/example/ainative/repository/ChatMemoryRepository.java
+/Users/hui/project/ai/backend/src/main/java/com/example/ainative/repository/SysEntityRepository.java
+/Users/hui/project/ai/backend/src/main/java/com/example/ainative/ai/CrmAgent.java
+/Users/hui/project/ai/backend/src/main/java/com/example/ainative/config/AiConfig.java