feat: Add GraalVM JavaScript engine to backend and update frontend dependencies.

This commit is contained in:
yyyhcyang
2026-03-18 15:13:26 +08:00
parent 807ad41f90
commit 93eb80294f
67 changed files with 2902 additions and 76 deletions
+99
View File
@@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.4</version>
<relativePath/>
</parent>
<groupId>com.cicc.enterprise</groupId>
<artifactId>ai-agent-gateway</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>ai-agent-gateway</name>
<description>Enterprise AI Agent Gateway</description>
<properties>
<java.version>21</java.version>
<graalvm.version>23.1.2</graalvm.version>
<mybatis-plus.version>3.5.5</mybatis-plus.version>
<fastjson2.version>2.0.47</fastjson2.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- GraalVM Polyglot Javascript -->
<dependency>
<groupId>org.graalvm.polyglot</groupId>
<artifactId>polyglot</artifactId>
<version>${graalvm.version}</version>
</dependency>
<dependency>
<groupId>org.graalvm.polyglot</groupId>
<artifactId>js</artifactId>
<version>${graalvm.version}</version>
<type>pom</type>
</dependency>
<!-- okhttp for proxy requests -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,13 @@
package com.cicc.enterprise.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
@EnableCaching
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
@@ -0,0 +1,19 @@
package com.cicc.enterprise.gateway.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@MapperScan("com.cicc.enterprise.gateway.mapper")
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
@@ -0,0 +1,23 @@
package com.cicc.enterprise.gateway.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
template.afterPropertiesSet();
return template;
}
}
@@ -0,0 +1,142 @@
package com.cicc.enterprise.gateway.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.cicc.enterprise.gateway.model.*;
import com.cicc.enterprise.gateway.service.*;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/v1/admin")
@RequiredArgsConstructor
@CrossOrigin(origins = "*")
public class ControlPlaneAdminController {
private final AiTenantService tenantService;
private final AiToolRegistryService toolRegistryService;
private final AiWashScriptService washScriptService;
private final AiAuditLogService auditLogService;
// ================== Tenant CRUD ==================
@GetMapping("/tenant")
public List<AiTenant> listTenants() {
return tenantService.list();
}
@PostMapping("/tenant")
public AiTenant createTenant(@RequestBody AiTenant tenant) {
tenantService.save(tenant);
return tenant;
}
@PutMapping("/tenant/{id}")
public AiTenant updateTenant(@PathVariable Long id, @RequestBody AiTenant tenant) {
tenant.setId(id);
tenantService.updateById(tenant);
return tenant;
}
@DeleteMapping("/tenant/{id}")
public void deleteTenant(@PathVariable Long id) {
tenantService.removeById(id);
}
// ================== Tool Registry CRUD ==================
@GetMapping("/tool")
public List<AiToolRegistry> listTools() {
return toolRegistryService.list();
}
@PostMapping("/tool")
public AiToolRegistry createTool(@RequestBody AiToolRegistry tool) {
toolRegistryService.save(tool);
return tool;
}
@PutMapping("/tool/{id}")
public AiToolRegistry updateTool(@PathVariable Long id, @RequestBody AiToolRegistry tool) {
tool.setId(id);
toolRegistryService.updateById(tool);
return tool;
}
@DeleteMapping("/tool/{id}")
public void deleteTool(@PathVariable Long id) {
toolRegistryService.removeById(id);
}
// ================== Wash Script ==================
@GetMapping("/wash-script/{toolId}")
public AiWashScript getWashScript(@PathVariable Long toolId) {
return washScriptService.getOne(
new LambdaQueryWrapper<AiWashScript>().eq(AiWashScript::getToolId, toolId)
);
}
@PostMapping("/wash-script")
public AiWashScript saveWashScript(@RequestBody AiWashScript script) {
AiWashScript existing = washScriptService.getOne(
new LambdaQueryWrapper<AiWashScript>().eq(AiWashScript::getToolId, script.getToolId())
);
if (existing != null) {
existing.setJsScript(script.getJsScript());
existing.setVersion(existing.getVersion() + 1);
washScriptService.updateById(existing);
return existing;
} else {
script.setVersion(1);
washScriptService.save(script);
return script;
}
}
@PostMapping("/wash-script/test")
public Map<String, String> testWashScript(@RequestBody Map<String, String> request) {
// TODO: invoke GraalVM engine to test the script
Map<String, String> result = new HashMap<>();
result.put("status", "Test endpoint placeholder — will invoke GraalVM sandbox in next phase");
result.put("input", request.getOrDefault("rawJson", ""));
return result;
}
// ================== Audit Log ==================
@GetMapping("/audit-log")
public List<AiAuditLog> listAuditLogs(
@RequestParam(required = false) String traceId,
@RequestParam(required = false) String toolName
) {
LambdaQueryWrapper<AiAuditLog> qw = new LambdaQueryWrapper<>();
if (traceId != null && !traceId.isBlank()) qw.eq(AiAuditLog::getTraceId, traceId);
if (toolName != null && !toolName.isBlank()) qw.eq(AiAuditLog::getToolName, toolName);
qw.orderByDesc(AiAuditLog::getId);
qw.last("LIMIT 200");
return auditLogService.list(qw);
}
// ================== AI Copilot Mock ==================
@PostMapping("/copilot/generate-wash-script")
public Map<String, String> generateWashScript(@RequestBody Map<String, String> request) {
String systemPrompt = """
# Role
你是一个资深的 JavaScript 数据处理专家与大模型工程架构师。
# Objective
根据用户提供的【原始庞大 JSON 样例】和【自然语言提取要求】,编写一段精简、安全、高效的 JavaScript 代码,用于在 GraalVM 环境下对 JSON 进行瘦身清洗。
""";
Map<String, String> response = new HashMap<>();
response.put("mockedSystemPrompt", systemPrompt);
response.put("status", "This endpoint should delegate to an LLM provider to generate the JS string.");
response.put("dummyOutput", "function wash(rawJsonString) { return '{}'; }");
return response;
}
}
@@ -0,0 +1,65 @@
package com.cicc.enterprise.gateway.controller;
import com.cicc.enterprise.gateway.filter.core.GatewayContext;
import com.cicc.enterprise.gateway.filter.core.GatewayFilterExecutor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
import java.util.UUID;
@Slf4j
@RestController
@RequestMapping("/api/v1/gw")
@RequiredArgsConstructor
public class GatewayEntryController {
private final GatewayFilterExecutor filterExecutor;
@RequestMapping(value = "/invoke/{toolName}", method = {RequestMethod.GET, RequestMethod.POST})
public ResponseEntity<Map<String, Object>> invokeAiTool(
@PathVariable String toolName,
@RequestHeader(value = "X-App-Key", required = true) String appKey,
@RequestHeader(value = "X-Timestamp", required = false) String timestamp,
@RequestHeader(value = "X-Signature", required = false) String signature,
@RequestBody(required = false) String requestBodyJson) {
long start = System.currentTimeMillis();
String traceId = UUID.randomUUID().toString();
log.info("Received AI Tool Invocation [{}] for tool: {}", traceId, toolName);
GatewayContext context = new GatewayContext();
context.setTraceId(traceId);
context.setAppKey(appKey);
context.setToolName(toolName);
context.setRawRequestJson(requestBodyJson);
context.setStartTimeMs(start);
try {
filterExecutor.execute(context);
if (context.getWashedResponseJson() != null) {
return ResponseEntity.ok(Map.of(
"code", 200,
"traceId", traceId,
"data", context.getWashedResponseJson()
));
} else {
return ResponseEntity.ok(Map.of(
"code", 200,
"traceId", traceId,
"data", "{}"
));
}
} catch (Exception e) {
log.error("Gateway execution error for trace [{}]", traceId, e);
return ResponseEntity.status(500).body(Map.of(
"code", 500,
"traceId", traceId,
"error", e.getMessage() != null ? e.getMessage() : "Internal Server Error"
));
}
}
}
@@ -0,0 +1,54 @@
package com.cicc.enterprise.gateway.engine;
import lombok.extern.slf4j.Slf4j;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Engine;
import org.graalvm.polyglot.HostAccess;
import org.graalvm.polyglot.Value;
import org.springframework.stereotype.Component;
import java.util.concurrent.*;
@Slf4j
@Component
public class GraalVmJsEngine {
private final Engine engine;
private final ExecutorService executorService;
public GraalVmJsEngine() {
this.engine = Engine.newBuilder("js")
.option("engine.WarnInterpreterOnly", "false")
.build();
// Fixed thread pool for script execution limits
this.executorService = Executors.newFixedThreadPool(10);
}
public String executeWash(String jsScript, String rawJsonString, long timeoutMs) throws Exception {
Future<String> future = executorService.submit(() -> {
try (Context context = Context.newBuilder("js")
.engine(engine)
.allowHostAccess(HostAccess.NONE)
.allowAllAccess(false)
.build()) {
context.eval("js", jsScript);
Value washFunction = context.getBindings("js").getMember("wash");
if (washFunction == null || !washFunction.canExecute()) {
throw new RuntimeException("Script must contain a function named 'wash'");
}
Value result = washFunction.execute(rawJsonString);
return result.isString() ? result.asString() : result.toString();
}
});
try {
return future.get(timeoutMs, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
future.cancel(true);
log.error("GraalVM JS execution timed out after {} ms", timeoutMs);
throw new RuntimeException("Script execution timeout", e);
}
}
}
@@ -0,0 +1,88 @@
package com.cicc.enterprise.gateway.engine;
import com.cicc.enterprise.gateway.model.AiTenant;
import com.cicc.enterprise.gateway.model.AiToolRegistry;
import com.cicc.enterprise.gateway.model.AiWashScript;
import com.cicc.enterprise.gateway.service.AiTenantService;
import com.cicc.enterprise.gateway.service.AiToolRegistryService;
import com.cicc.enterprise.gateway.service.AiWashScriptService;
import com.cicc.enterprise.gateway.model.AiTenantToolRel;
import com.cicc.enterprise.gateway.service.AiTenantToolRelService;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.Optional;
import jakarta.annotation.PostConstruct;
import java.util.concurrent.TimeUnit;
@Component
@RequiredArgsConstructor
public class LocalCacheManager {
private final AiTenantService tenantService;
private final AiToolRegistryService toolService;
private final AiWashScriptService scriptService;
private final AiTenantToolRelService relService;
// cache appKey -> AiTenant
private Cache<String, AiTenant> tenantCache;
// cache toolName -> AiToolRegistry
private Cache<String, AiToolRegistry> toolCache;
// cache toolId -> Optional<AiWashScript>
private Cache<Long, Optional<AiWashScript>> scriptCache;
// cache tenantId_toolId -> Boolean
private Cache<String, Boolean> relCache;
@PostConstruct
public void init() {
tenantCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();
toolCache = Caffeine.newBuilder()
.maximumSize(2000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();
scriptCache = Caffeine.newBuilder()
.maximumSize(2000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();
relCache = Caffeine.newBuilder()
.maximumSize(5000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();
}
public AiTenant getTenantByAppKey(String appKey) {
return tenantCache.get(appKey, k -> tenantService.lambdaQuery().eq(AiTenant::getAppKey, k).one());
}
public AiToolRegistry getToolByName(String toolName) {
return toolCache.get(toolName, k -> toolService.lambdaQuery().eq(AiToolRegistry::getToolName, k).one());
}
public AiWashScript getScriptByToolId(Long toolId) {
Optional<AiWashScript> opt = scriptCache.get(toolId, k -> {
AiWashScript s = scriptService.lambdaQuery().eq(AiWashScript::getToolId, k).one();
return Optional.ofNullable(s);
});
return opt != null ? opt.orElse(null) : null;
}
public boolean hasToolPermission(Long tenantId, Long toolId) {
String key = tenantId + "_" + toolId;
Boolean hasPermission = relCache.get(key, k -> {
long count = relService.lambdaQuery()
.eq(AiTenantToolRel::getTenantId, tenantId)
.eq(AiTenantToolRel::getToolId, toolId)
.count();
return count > 0;
});
return Boolean.TRUE.equals(hasPermission);
}
}
@@ -0,0 +1,49 @@
package com.cicc.enterprise.gateway.engine;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Slf4j
@Component
public class OkHttpProxyClient {
private final OkHttpClient client;
public OkHttpProxyClient() {
this.client = new OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build();
}
public String forwardProxy(String targetUrl, String httpMethod, String requestBodyJson, Map<String, String> extraHeaders) throws IOException {
Request.Builder requestBuilder = new Request.Builder()
.url(targetUrl);
if (extraHeaders != null) {
extraHeaders.forEach(requestBuilder::addHeader);
}
if ("POST".equalsIgnoreCase(httpMethod) || "PUT".equalsIgnoreCase(httpMethod)) {
RequestBody body = RequestBody.create(
requestBodyJson != null ? requestBodyJson : "", MediaType.parse("application/json; charset=utf-8"));
requestBuilder.method(httpMethod.toUpperCase(), body);
} else {
requestBuilder.method(httpMethod.toUpperCase(), null);
}
try (Response response = client.newCall(requestBuilder.build()).execute()) {
if (!response.isSuccessful()) {
log.warn("Proxy call returned non-200 code: {}", response.code());
}
if (response.body() != null) {
return response.body().string();
}
return "";
}
}
}
@@ -0,0 +1,27 @@
package com.cicc.enterprise.gateway.filter.core;
import com.cicc.enterprise.gateway.model.AiToolRegistry;
import java.util.Map;
/**
* Strategy interface for executing the underlying AI tools.
* Implementations can support HTTP proxy, Dubbo Generic Service, Spring Cloud Feign, etc.
*/
public interface ExecutionStrategy {
/**
* @return The protocol this strategy handles (e.g., "HTTP", "DUBBO")
*/
String getSupportedProtocol();
/**
* Executes the actual request against the target system.
*
* @param tool The registered tool metadata containing target endpoints
* @param requestBodyJson The raw or mapped JSON parameters from AI
* @param extraHeaders Extra tracking headers (like traceId, OBO identities)
* @return The raw JSON response from the target system
* @throws Exception If the remote call fails
*/
String execute(AiToolRegistry tool, String requestBodyJson, Map<String, String> extraHeaders) throws Exception;
}
@@ -0,0 +1,33 @@
package com.cicc.enterprise.gateway.filter.core;
import com.cicc.enterprise.gateway.model.AiTenant;
import com.cicc.enterprise.gateway.model.AiToolRegistry;
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
@Data
public class GatewayContext {
private String traceId;
private String appKey;
private String toolName;
private String rawRequestJson;
// Extracted after auth
private AiTenant tenant;
private AiToolRegistry tool;
private String jsScript;
// Execution state
private String targetUrl;
private String httpMethod;
private Map<String, String> extraHeaders = new HashMap<>();
// Result
private String rawResponseJson;
private String washedResponseJson;
private Exception error;
private long startTimeMs;
private long costTimeMs;
}
@@ -0,0 +1,8 @@
package com.cicc.enterprise.gateway.filter.core;
public interface GatewayFilter {
void doFilter(GatewayContext context, GatewayFilterChain chain) throws Exception;
// Order in the chain
int getOrder();
}
@@ -0,0 +1,19 @@
package com.cicc.enterprise.gateway.filter.core;
import java.util.List;
public class GatewayFilterChain {
private final List<GatewayFilter> filters;
private int index = 0;
public GatewayFilterChain(List<GatewayFilter> filters) {
this.filters = filters;
}
public void doFilter(GatewayContext context) throws Exception {
if (this.index < filters.size()) {
GatewayFilter filter = filters.get(this.index++);
filter.doFilter(context, this);
}
}
}
@@ -0,0 +1,29 @@
package com.cicc.enterprise.gateway.filter.core;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct;
import java.util.Comparator;
import java.util.List;
@Component
@RequiredArgsConstructor
public class GatewayFilterExecutor {
private final List<GatewayFilter> registeredFilters;
@PostConstruct
public void init() {
if (registeredFilters != null) {
registeredFilters.sort(Comparator.comparingInt(GatewayFilter::getOrder));
}
}
public void execute(GatewayContext context) throws Exception {
if (registeredFilters == null || registeredFilters.isEmpty()) {
return;
}
GatewayFilterChain chain = new GatewayFilterChain(registeredFilters);
chain.doFilter(context);
}
}
@@ -0,0 +1,64 @@
package com.cicc.enterprise.gateway.filter.impl;
import com.cicc.enterprise.gateway.filter.core.GatewayContext;
import com.cicc.enterprise.gateway.filter.core.GatewayFilter;
import com.cicc.enterprise.gateway.filter.core.GatewayFilterChain;
import com.cicc.enterprise.gateway.model.AiAuditLog;
import com.cicc.enterprise.gateway.service.AiAuditLogService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Slf4j
@Component
@RequiredArgsConstructor
public class AsyncAuditFilter implements GatewayFilter {
private final AiAuditLogService auditLogService;
private final ExecutorService auditThreadPool = Executors.newFixedThreadPool(10);
@Override
public int getOrder() {
return 1; // Highest priority, wrap the entire chain
}
@Override
public void doFilter(GatewayContext context, GatewayFilterChain chain) throws Exception {
try {
// Wait for the inner chain to complete
chain.doFilter(context);
} catch (Exception e) {
context.setError(e);
throw e;
} finally {
// Async save to audit log
auditThreadPool.submit(() -> {
try {
AiAuditLog auditLog = new AiAuditLog();
auditLog.setTraceId(context.getTraceId());
auditLog.setTenantId(context.getTenant() != null ? context.getTenant().getId() : -1L);
auditLog.setToolName(context.getToolName());
auditLog.setRawRequest(context.getRawRequestJson());
auditLog.setRawResponse(context.getRawResponseJson());
if (context.getError() != null) {
auditLog.setWashedResponse("ERROR: " + context.getError().getMessage());
} else {
auditLog.setWashedResponse(context.getWashedResponseJson());
}
auditLog.setCostTimeMs(context.getCostTimeMs() > 0 ? context.getCostTimeMs() : (System.currentTimeMillis() - context.getStartTimeMs()));
auditLog.setCreateTime(LocalDateTime.now());
auditLogService.save(auditLog);
} catch (Exception e) {
log.error("Failed to save async audit log", e);
}
});
}
}
}
@@ -0,0 +1,45 @@
package com.cicc.enterprise.gateway.filter.impl;
import com.cicc.enterprise.gateway.filter.core.GatewayContext;
import com.cicc.enterprise.gateway.filter.core.GatewayFilter;
import com.cicc.enterprise.gateway.filter.core.GatewayFilterChain;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.time.Duration;
@Component
@RequiredArgsConstructor
public class AuthRateLimitingFilter implements GatewayFilter {
private final RedisTemplate<String, Object> redisTemplate;
@Override
public int getOrder() {
return 20;
}
@Override
public void doFilter(GatewayContext context, GatewayFilterChain chain) throws Exception {
String key = "ratelimit:" + context.getTenant().getAppKey();
Integer qpsParam = context.getTenant().getRateLimitQps();
int maxQps = (qpsParam != null && qpsParam > 0) ? qpsParam : 100;
try {
// Simple atomic increment based counter for 1 second window
Long currentCount = redisTemplate.opsForValue().increment(key);
if (currentCount != null && currentCount == 1) {
redisTemplate.expire(key, Duration.ofSeconds(1));
}
if (currentCount != null && currentCount > maxQps) {
throw new RuntimeException("Rate limit exceeded for AppKey: " + context.getAppKey());
}
} catch (org.springframework.data.redis.RedisConnectionFailureException e) {
// Gracefully ignore rate limiting if Redis is down (e.g., local dev)
}
chain.doFilter(context);
}
}
@@ -0,0 +1,51 @@
package com.cicc.enterprise.gateway.filter.impl;
import com.cicc.enterprise.gateway.engine.LocalCacheManager;
import com.cicc.enterprise.gateway.filter.core.GatewayContext;
import com.cicc.enterprise.gateway.filter.core.GatewayFilter;
import com.cicc.enterprise.gateway.filter.core.GatewayFilterChain;
import com.cicc.enterprise.gateway.model.AiTenant;
import com.cicc.enterprise.gateway.model.AiToolRegistry;
import com.cicc.enterprise.gateway.model.AiWashScript;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class ConfigWatcherFilter implements GatewayFilter {
private final LocalCacheManager cacheManager;
@Override
public int getOrder() {
return 10;
}
@Override
public void doFilter(GatewayContext context, GatewayFilterChain chain) throws Exception {
AiTenant tenant = cacheManager.getTenantByAppKey(context.getAppKey());
if (tenant == null || tenant.getStatus() == 0) {
throw new RuntimeException("Invalid or disabled AppKey: " + context.getAppKey());
}
context.setTenant(tenant);
AiToolRegistry tool = cacheManager.getToolByName(context.getToolName());
if (tool == null) {
throw new RuntimeException("Target tool not found: " + context.getToolName());
}
context.setTool(tool);
// Verify if tenant has permission to use this tool
if (!cacheManager.hasToolPermission(tenant.getId(), tool.getId())) {
throw new RuntimeException("Tenant " + tenant.getTenantName() + " is not authorized to use tool: " + tool.getToolName());
}
// Optional script
AiWashScript script = cacheManager.getScriptByToolId(tool.getId());
if (script != null) {
context.setJsScript(script.getJsScript());
}
chain.doFilter(context);
}
}
@@ -0,0 +1,39 @@
package com.cicc.enterprise.gateway.filter.impl;
import com.cicc.enterprise.gateway.engine.GraalVmJsEngine;
import com.cicc.enterprise.gateway.filter.core.GatewayContext;
import com.cicc.enterprise.gateway.filter.core.GatewayFilter;
import com.cicc.enterprise.gateway.filter.core.GatewayFilterChain;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@RequiredArgsConstructor
public class GraalVmWashingFilter implements GatewayFilter {
private final GraalVmJsEngine jsEngine;
@Override
public int getOrder() {
return 60;
}
@Override
public void doFilter(GatewayContext context, GatewayFilterChain chain) throws Exception {
if (context.getJsScript() != null && !context.getJsScript().isEmpty() && context.getRawResponseJson() != null) {
log.info("Executing GraalVM washing script for tool: {}", context.getToolName());
try {
// 100ms timeout sandbox execution
String washed = jsEngine.executeWash(context.getJsScript(), context.getRawResponseJson(), 100);
context.setWashedResponseJson(washed);
} catch (Exception e) {
log.error("GraalVM Washing failed, falling back to raw response", e);
// Depending on strictness, we might throw or return raw. Let's return raw on wash failure.
}
}
chain.doFilter(context);
}
}
@@ -0,0 +1,39 @@
package com.cicc.enterprise.gateway.filter.impl;
import com.cicc.enterprise.gateway.filter.core.GatewayContext;
import com.cicc.enterprise.gateway.filter.core.GatewayFilter;
import com.cicc.enterprise.gateway.filter.core.GatewayFilterChain;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class HitlSuspenderFilter implements GatewayFilter {
@Override
public int getOrder() {
return 40;
}
@Override
public void doFilter(GatewayContext context, GatewayFilterChain chain) throws Exception {
if ("WRITE".equalsIgnoreCase(context.getTool().getActionType())) {
log.info("HITL Suspender triggered for AppKey: {} Tool: {}", context.getAppKey(), context.getToolName());
// Mock webhook call to DingTalk/WeCom
// Mock waiting for approval callback
// In a real production environment, this should be done asynchronously
// via Message Queue and a callback endpoint to resume the pipeline.
boolean approved = mockWaitForApproval();
if (!approved) {
throw new RuntimeException("HITL Approval Denied or Timeout");
}
}
chain.doFilter(context);
}
private boolean mockWaitForApproval() {
log.info("Mocking approval webhook... auto-approving.");
return true;
}
}
@@ -0,0 +1,31 @@
package com.cicc.enterprise.gateway.filter.impl;
import com.cicc.enterprise.gateway.engine.OkHttpProxyClient;
import com.cicc.enterprise.gateway.filter.core.ExecutionStrategy;
import com.cicc.enterprise.gateway.model.AiToolRegistry;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
@RequiredArgsConstructor
public class HttpExecutionStrategyImpl implements ExecutionStrategy {
private final OkHttpProxyClient proxyClient;
@Override
public String getSupportedProtocol() {
return "HTTP";
}
@Override
public String execute(AiToolRegistry tool, String requestBodyJson, Map<String, String> extraHeaders) throws Exception {
return proxyClient.forwardProxy(
tool.getTargetUrl(),
tool.getHttpMethod(),
requestBodyJson,
extraHeaders
);
}
}
@@ -0,0 +1,24 @@
package com.cicc.enterprise.gateway.filter.impl;
import com.cicc.enterprise.gateway.filter.core.GatewayContext;
import com.cicc.enterprise.gateway.filter.core.GatewayFilter;
import com.cicc.enterprise.gateway.filter.core.GatewayFilterChain;
import org.springframework.stereotype.Component;
@Component
public class IdentityOboFilter implements GatewayFilter {
@Override
public int getOrder() {
return 30;
}
@Override
public void doFilter(GatewayContext context, GatewayFilterChain chain) throws Exception {
// Assume OBO identity is passed via standard headers from AI orchestrator
context.getExtraHeaders().put("X-Forwarded-User", "Obo-User-Identity");
context.setTargetUrl(context.getTool().getTargetUrl());
context.setHttpMethod(context.getTool().getHttpMethod());
chain.doFilter(context);
}
}
@@ -0,0 +1,68 @@
package com.cicc.enterprise.gateway.filter.impl;
import com.cicc.enterprise.gateway.filter.core.ExecutionStrategy;
import com.cicc.enterprise.gateway.filter.core.GatewayContext;
import com.cicc.enterprise.gateway.filter.core.GatewayFilter;
import com.cicc.enterprise.gateway.filter.core.GatewayFilterChain;
import com.cicc.enterprise.gateway.model.AiToolRegistry;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import jakarta.annotation.PostConstruct;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@RequiredArgsConstructor
public class ProxyExecutionFilter implements GatewayFilter {
private final List<ExecutionStrategy> strategyList;
private final Map<String, ExecutionStrategy> strategyMap = new HashMap<>();
@PostConstruct
public void init() {
for (ExecutionStrategy strategy : strategyList) {
strategyMap.put(strategy.getSupportedProtocol().toUpperCase(), strategy);
}
}
@Override
public int getOrder() {
return 50;
}
@Override
public void doFilter(GatewayContext context, GatewayFilterChain chain) throws Exception {
log.info("Routing request to target: {}", context.getTargetUrl());
long start = System.currentTimeMillis();
try {
AiToolRegistry tool = context.getTool();
String protocol = tool.getExecutionProtocol() != null ? tool.getExecutionProtocol().toUpperCase() : "HTTP";
ExecutionStrategy strategy = strategyMap.get(protocol);
if (strategy == null) {
throw new UnsupportedOperationException("Unsupported Execution Protocol: " + protocol);
}
String response = strategy.execute(
tool,
context.getRawRequestJson(),
context.getExtraHeaders()
);
context.setRawResponseJson(response);
// Default washed is raw unless GraalVM changes it
context.setWashedResponseJson(response);
} catch (Exception e) {
log.error("Proxy execution failed", e);
throw new RuntimeException("Underlying service call failed", e);
} finally {
context.setCostTimeMs(System.currentTimeMillis() - start);
}
chain.doFilter(context);
}
}
@@ -0,0 +1,9 @@
package com.cicc.enterprise.gateway.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.cicc.enterprise.gateway.model.AiAuditLog;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface AiAuditLogMapper extends BaseMapper<AiAuditLog> {
}
@@ -0,0 +1,9 @@
package com.cicc.enterprise.gateway.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.cicc.enterprise.gateway.model.AiTenant;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface AiTenantMapper extends BaseMapper<AiTenant> {
}
@@ -0,0 +1,9 @@
package com.cicc.enterprise.gateway.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.cicc.enterprise.gateway.model.AiTenantToolRel;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface AiTenantToolRelMapper extends BaseMapper<AiTenantToolRel> {
}
@@ -0,0 +1,9 @@
package com.cicc.enterprise.gateway.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.cicc.enterprise.gateway.model.AiToolRegistry;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface AiToolRegistryMapper extends BaseMapper<AiToolRegistry> {
}
@@ -0,0 +1,9 @@
package com.cicc.enterprise.gateway.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.cicc.enterprise.gateway.model.AiWashScript;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface AiWashScriptMapper extends BaseMapper<AiWashScript> {
}
@@ -0,0 +1,22 @@
package com.cicc.enterprise.gateway.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("ai_audit_log")
public class AiAuditLog {
@TableId(type = IdType.AUTO)
private Long id;
private String traceId;
private Long tenantId;
private String toolName;
private String rawRequest;
private String rawResponse;
private String washedResponse;
private Long costTimeMs;
private LocalDateTime createTime;
}
@@ -0,0 +1,21 @@
package com.cicc.enterprise.gateway.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("ai_tenant")
public class AiTenant {
@TableId(type = IdType.AUTO)
private Long id;
private String tenantName;
private String appKey;
private String appSecret;
private Integer rateLimitQps;
private Integer status;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
@@ -0,0 +1,15 @@
package com.cicc.enterprise.gateway.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@Data
@TableName("ai_tenant_tool_rel")
public class AiTenantToolRel {
@TableId(type = IdType.AUTO)
private Long id;
private Long tenantId;
private Long toolId;
}
@@ -0,0 +1,23 @@
package com.cicc.enterprise.gateway.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("ai_tool_registry")
public class AiToolRegistry {
@TableId(type = IdType.AUTO)
private Long id;
private String toolName;
private String description;
private String actionType;
private String executionProtocol; // HTTP, DUBBO
private String targetUrl; // URL or Dubbo Interface Name
private String httpMethod; // GET/POST or Dubbo Method
private String requestSchema;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
@@ -0,0 +1,19 @@
package com.cicc.enterprise.gateway.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("ai_wash_script")
public class AiWashScript {
@TableId(type = IdType.AUTO)
private Long id;
private Long toolId;
private String jsScript;
private Integer version;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
@@ -0,0 +1,7 @@
package com.cicc.enterprise.gateway.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.cicc.enterprise.gateway.model.AiAuditLog;
public interface AiAuditLogService extends IService<AiAuditLog> {
}
@@ -0,0 +1,7 @@
package com.cicc.enterprise.gateway.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.cicc.enterprise.gateway.model.AiTenant;
public interface AiTenantService extends IService<AiTenant> {
}
@@ -0,0 +1,7 @@
package com.cicc.enterprise.gateway.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.cicc.enterprise.gateway.model.AiTenantToolRel;
public interface AiTenantToolRelService extends IService<AiTenantToolRel> {
}
@@ -0,0 +1,7 @@
package com.cicc.enterprise.gateway.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.cicc.enterprise.gateway.model.AiToolRegistry;
public interface AiToolRegistryService extends IService<AiToolRegistry> {
}
@@ -0,0 +1,7 @@
package com.cicc.enterprise.gateway.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.cicc.enterprise.gateway.model.AiWashScript;
public interface AiWashScriptService extends IService<AiWashScript> {
}
@@ -0,0 +1,11 @@
package com.cicc.enterprise.gateway.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.cicc.enterprise.gateway.mapper.AiAuditLogMapper;
import com.cicc.enterprise.gateway.model.AiAuditLog;
import com.cicc.enterprise.gateway.service.AiAuditLogService;
import org.springframework.stereotype.Service;
@Service
public class AiAuditLogServiceImpl extends ServiceImpl<AiAuditLogMapper, AiAuditLog> implements AiAuditLogService {
}
@@ -0,0 +1,11 @@
package com.cicc.enterprise.gateway.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.cicc.enterprise.gateway.mapper.AiTenantMapper;
import com.cicc.enterprise.gateway.model.AiTenant;
import com.cicc.enterprise.gateway.service.AiTenantService;
import org.springframework.stereotype.Service;
@Service
public class AiTenantServiceImpl extends ServiceImpl<AiTenantMapper, AiTenant> implements AiTenantService {
}
@@ -0,0 +1,11 @@
package com.cicc.enterprise.gateway.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.cicc.enterprise.gateway.mapper.AiTenantToolRelMapper;
import com.cicc.enterprise.gateway.model.AiTenantToolRel;
import com.cicc.enterprise.gateway.service.AiTenantToolRelService;
import org.springframework.stereotype.Service;
@Service
public class AiTenantToolRelServiceImpl extends ServiceImpl<AiTenantToolRelMapper, AiTenantToolRel> implements AiTenantToolRelService {
}
@@ -0,0 +1,11 @@
package com.cicc.enterprise.gateway.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.cicc.enterprise.gateway.mapper.AiToolRegistryMapper;
import com.cicc.enterprise.gateway.model.AiToolRegistry;
import com.cicc.enterprise.gateway.service.AiToolRegistryService;
import org.springframework.stereotype.Service;
@Service
public class AiToolRegistryServiceImpl extends ServiceImpl<AiToolRegistryMapper, AiToolRegistry> implements AiToolRegistryService {
}
@@ -0,0 +1,11 @@
package com.cicc.enterprise.gateway.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.cicc.enterprise.gateway.mapper.AiWashScriptMapper;
import com.cicc.enterprise.gateway.model.AiWashScript;
import com.cicc.enterprise.gateway.service.AiWashScriptService;
import org.springframework.stereotype.Service;
@Service
public class AiWashScriptServiceImpl extends ServiceImpl<AiWashScriptMapper, AiWashScript> implements AiWashScriptService {
}
@@ -0,0 +1,29 @@
server:
port: 8080
spring:
profiles:
active: dev
application:
name: ai-agent-gateway
datasource:
url: jdbc:mysql://127.0.0.1:3306/db_agent_gateway?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8&useSSL=false
username: root
password: YOUR_PASSWORD_HERE # Please configure actual password in application-dev.yml
driver-class-name: com.mysql.cj.jdbc.Driver
data:
redis:
host: localhost
port: 6379
database: 0
cache:
type: caffeine
caffeine:
spec: maximumSize=1000,expireAfterWrite=60s
main:
allow-bean-definition-overriding: true
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
+69
View File
@@ -0,0 +1,69 @@
CREATE DATABASE IF NOT EXISTS `db_agent_gateway` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE `agent_gateway`;
-- 1. ai_tenant
CREATE TABLE IF NOT EXISTS `ai_tenant` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`tenant_name` VARCHAR(128) NOT NULL COMMENT '租户/机器人名称',
`app_key` VARCHAR(64) NOT NULL COMMENT '网关调用的唯一凭证',
`app_secret` VARCHAR(128) NOT NULL COMMENT '签名与加解密密钥',
`rate_limit_qps` INT NOT NULL DEFAULT 100 COMMENT 'QPS限制阈值',
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态 (1:启用 0:禁用)',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_app_key` (`app_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户/AI 助手接入表';
-- 2. ai_tool_registry
CREATE TABLE IF NOT EXISTS `ai_tool_registry` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`tool_name` VARCHAR(64) NOT NULL COMMENT '工具标识(英文下划线)',
`description` TEXT NOT NULL COMMENT '喂给大模型的详细描述',
`action_type` VARCHAR(32) NOT NULL DEFAULT 'READ' COMMENT 'READ/WRITE',
`execution_protocol` VARCHAR(32) NOT NULL DEFAULT 'HTTP' COMMENT 'HTTP/DUBBO/FEIGN',
`target_url` VARCHAR(512) NOT NULL COMMENT '底层老系统的真实内网 URL (或 Dubbo 接口全路径)',
`http_method` VARCHAR(16) NOT NULL DEFAULT 'GET' COMMENT 'GET/POST',
`request_schema` TEXT COMMENT '请求参数的标准 JSON Schema',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tool_name` (`tool_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='API 工具注册表';
-- 3. ai_tenant_tool_rel
CREATE TABLE IF NOT EXISTS `ai_tenant_tool_rel` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`tenant_id` BIGINT NOT NULL COMMENT '关联 ai_tenant.id',
`tool_id` BIGINT NOT NULL COMMENT '关联 ai_tool_registry.id',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_tool` (`tenant_id`, `tool_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='权限控制映射表';
-- 4. ai_wash_script
CREATE TABLE IF NOT EXISTS `ai_wash_script` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`tool_id` BIGINT NOT NULL COMMENT '唯一关联 ai_tool_registry.id',
`js_script` TEXT NOT NULL COMMENT '具体的 JavaScript 源码文本',
`version` INT NOT NULL DEFAULT 1 COMMENT '版本号',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tool_id` (`tool_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='GraalVM 数据清洗脚本表';
-- 5. ai_audit_log
CREATE TABLE IF NOT EXISTS `ai_audit_log` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`trace_id` VARCHAR(64) NOT NULL COMMENT '全局请求流水号',
`tenant_id` BIGINT NOT NULL COMMENT '关联的租户 ID',
`tool_name` VARCHAR(64) NOT NULL COMMENT '命中的工具名称',
`raw_request` TEXT COMMENT 'AI 发起的原始 JSON 参数',
`raw_response` MEDIUMTEXT COMMENT '老系统返回的巨型原始 JSON',
`washed_response` TEXT COMMENT '清洗后的纯净 JSON',
`cost_time_ms` BIGINT NOT NULL DEFAULT 0 COMMENT '总耗时 (毫秒)',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_trace_id` (`trace_id`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='全链路安全审计日志表';