init
This commit is contained in:
Generated
+9
@@ -0,0 +1,9 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
*.class
|
||||
Generated
+19
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<annotationProcessing>
|
||||
<profile default="true" name="Default" enabled="true" />
|
||||
<profile name="Maven default annotation processors profile" enabled="true">
|
||||
<sourceOutputDir name="target/generated-sources/annotations" />
|
||||
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
|
||||
<outputRelativeToContentRoot value="true" />
|
||||
<module name="ai-agent-gateway" />
|
||||
</profile>
|
||||
</annotationProcessing>
|
||||
</component>
|
||||
<component name="JavacSettings">
|
||||
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
|
||||
<module name="ai-agent-gateway" options="-parameters" />
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+17
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||
<data-source source="LOCAL" name="@localhost" uuid="a9d2bc30-f428-4724-9870-f12c4d800a7f">
|
||||
<driver-ref>mysql.8</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
|
||||
<jdbc-url>jdbc:mysql://localhost:3306</jdbc-url>
|
||||
<jdbc-additional-properties>
|
||||
<property name="com.intellij.clouds.kubernetes.db.host.port" />
|
||||
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
|
||||
<property name="com.intellij.clouds.kubernetes.db.container.port" />
|
||||
</jdbc-additional-properties>
|
||||
<working-dir>$ProjectFileDir$</working-dir>
|
||||
</data-source>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding">
|
||||
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+25
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RemoteRepositoriesConfiguration">
|
||||
<remote-repository>
|
||||
<option name="id" value="ciccwm-maven-public" />
|
||||
<option name="name" value="CICC Maven Public" />
|
||||
<option name="url" value="https://af.ciccwm.com/artifactory/ciccwm-maven-public" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="central" />
|
||||
<option name="name" value="Central Repository" />
|
||||
<option name="url" value="https://repo.maven.apache.org/maven2" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="central" />
|
||||
<option name="name" value="Maven Central repository" />
|
||||
<option name="url" value="https://repo1.maven.org/maven2" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="jboss.community" />
|
||||
<option name="name" value="JBoss Community repository" />
|
||||
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
|
||||
</remote-repository>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+12
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="MavenProjectsManager">
|
||||
<option name="originalFiles">
|
||||
<list>
|
||||
<option value="$PROJECT_DIR$/pom.xml" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK" />
|
||||
</project>
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,134 @@
|
||||
# enterprise-ai-agent-gateway
|
||||
企业级 AI 智能体网关
|
||||
|
||||
这份终稿设计书已经为你全面整合完毕。它不仅包含了底层的**数据库模型**和**网关责任链**,还把你刚刚关心的**“前端如何降低脚本编写成本”**的设计思路,以及核心的**系统提示词(System Prompt)**全部固化了进去。
|
||||
|
||||
你可以直接将以下全部内容复制,喂给 Antigravity,让它开始生成骨架代码。
|
||||
|
||||
***
|
||||
|
||||
# 🚀 Enterprise AI Agent Gateway (企业级 AI 智能体网关) - 核心架构与开发设计书
|
||||
|
||||
## 1. 架构定位与核心设计理念
|
||||
本系统是一个定位于大模型(LLM/AI Agent)与企业老旧业务系统(Legacy Systems)之间的**安全中间件与协议转换枢纽**。
|
||||
* **零侵入架构**:老系统无需修改任何代码,通过本网关即可将现有 API 暴露为标准的大模型 Tool Schema。
|
||||
* **控制面与数据面分离**:
|
||||
* **控制面 (Control Plane)**:前端可视化配置,业务人员通过“表单填报”或“AI 辅助生成”完成 API 注册与清洗规则设定。
|
||||
* **数据面 (Data Plane)**:纯粹的 Java 高并发执行引擎,负责鉴权、状态挂起(人工审批)、代理调用与底层 GraalVM 极速数据洗涤。
|
||||
|
||||
## 2. 技术栈选型
|
||||
* **数据面网关核心**:Spring Boot 3.x (WebFlux/纯 WebMVC) + JDK 17/21。
|
||||
* **清洗执行引擎**:GraalVM Polyglot (内嵌高安全级别的 JavaScript 运行时沙箱)。
|
||||
* **缓存与风控**:Redis (分布式限流、状态机挂起) + Caffeine (本地极速配置缓存)。
|
||||
* **控制面后端**:Spring Boot / FastAPI (提供标准的 CRUD 接口)。
|
||||
* **前端管理台**:Vue 3 + Element Plus / Ant Design Vue + Monaco Editor。
|
||||
* **数据存储**:MySQL 8.0。
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心数据库实体模型 (MySQL 8.0 DDL 需求)
|
||||
|
||||
> **To Antigravity**: 请基于以下实体关系,优先生成 DDL 建表语句以及对应的底层 CRUD 代码(Entity/Mapper/Service)。
|
||||
|
||||
### 3.1 `ai_tenant` (租户/AI 助手接入表)
|
||||
* `id`: 主键
|
||||
* `tenant_name`: 租户/机器人名称(例:“财务客服 Agent”)
|
||||
* `app_key`: 网关调用的唯一凭证 (唯一索引)
|
||||
* `app_secret`: 签名与加解密密钥
|
||||
* `rate_limit_qps`: 该租户的全局 QPS 限制阈值
|
||||
* `status`: 状态 (启用/禁用)
|
||||
|
||||
### 3.2 `ai_tool_registry` (API 工具注册表)
|
||||
* `id`: 主键
|
||||
* `tool_name`: 工具标识(英文下划线,符合 LLM 命名规范)
|
||||
* `description`: 喂给大模型的详细描述(决定 LLM 是否调用该工具的核心依据)
|
||||
* `action_type`: `READ` (默认放行) / `WRITE` (高危,强制触发人工审批)
|
||||
* `target_url`: 底层老系统的真实内网 URL
|
||||
* `http_method`: GET / POST
|
||||
* `request_schema`: 请求参数的标准 JSON Schema (用于 LLM 识别)
|
||||
|
||||
### 3.3 `ai_tenant_tool_rel` (权限控制映射表)
|
||||
* `tenant_id`: 关联 `ai_tenant.id`
|
||||
* `tool_id`: 关联 `ai_tool_registry.id`
|
||||
|
||||
### 3.4 `ai_wash_script` (GraalVM 数据清洗脚本表)
|
||||
* `tool_id`: 唯一关联 `ai_tool_registry.id`
|
||||
* `js_script`: 具体的 JavaScript 源码文本(由控制面生成,供数据面 GraalVM 执行)
|
||||
* `version`: 版本号,用于并发控制和缓存刷新
|
||||
|
||||
### 3.5 `ai_audit_log` (全链路安全审计日志表)
|
||||
* `trace_id`: 全局请求流水号 (UUID)
|
||||
* `tenant_id`: 关联的租户 ID
|
||||
* `tool_name`: 命中的工具名称
|
||||
* `raw_request`: AI 发起的原始 JSON 参数
|
||||
* `raw_response`: 老系统返回的巨型原始 JSON (MediumText)
|
||||
* `washed_response`: 经过 GraalVM JS 脚本洗涤后的纯净 JSON
|
||||
* `cost_time_ms`: 总耗时 (毫秒)
|
||||
|
||||
---
|
||||
|
||||
## 4. 控制面前端 (Vue 3) 交互设计规范
|
||||
|
||||
前端的核心目标是**隐藏 JavaScript 的编写复杂度**。对于清洗规则 (`ai_wash_script`) 的配置页面,需实现以下三层交互降级方案:
|
||||
|
||||
1. **AI Copilot 生成(主推)**:页面提供左侧输入“原始 JSON 样例”,上方输入“自然语言提取需求”,点击按钮后,后台调用大模型生成 JS 代码并填充。
|
||||
2. **可视化勾选表单(中阶)**:提供“保留字段白名单”、“移除 Null 值”、“拍平数组”等勾选项,前端根据勾选自动拼接为 JS 字符串。
|
||||
3. **Monaco Editor 兜底(高阶)**:提供类似 VS Code 的代码编辑框,允许懂代码的开发人员直接修正在线生成的 JavaScript 规则。
|
||||
|
||||
---
|
||||
|
||||
## 5. 数据面网关 Pipeline (责任链/Filter Chain 设计)
|
||||
|
||||
> **To Antigravity**: 在生成网关核心转发逻辑时,请严格按照以下 Filter 顺序构建职责链模式。
|
||||
|
||||
1. **Config Watcher (配置加载)**:定时从 MySQL 拉取 `ai_tool_registry`、权限与 `js_script`,加载至 Caffeine 内存。
|
||||
2. **Auth & Rate Limiting (鉴权限流)**:校验 `app_key`,拦截非法调用;利用 Redis 执行令牌桶限流。
|
||||
3. **Identity OBO (权限穿透)**:若包含终端用户 Token,将该身份附加至 Downstream Header 中。
|
||||
4. **HITL Suspender (审批挂起)**:若 `action_type == 'WRITE'`,拦截当前线程,向外部(如企微/钉钉)发送包含审批链接的 Webhook,当前请求挂起等待回调。
|
||||
5. **Proxy Execution (代理转发)**:组装参数,向 `target_url` 发起真实的 HTTP 内部请求。
|
||||
6. **GraalVM Washing (洗涤瘦身)**:拿到原始大 JSON,在限制了内存与执行时间(如 max 100ms)的 GraalVM 沙箱中执行对应的 `js_script`,返回瘦身结果。
|
||||
7. **Async Audit (异步审计)**:无论成功失败,将完整上下文抛入线程池,落库至 `ai_audit_log`。
|
||||
|
||||
---
|
||||
|
||||
## 6. 核心资产:AI 辅助生成清洗脚本的 System Prompt
|
||||
|
||||
> 针对控制面的“AI 自动生成 JS 规则”功能,后端需要内置以下 System Prompt。
|
||||
|
||||
```text
|
||||
# Role
|
||||
你是一个资深的 JavaScript 数据处理专家与大模型工程架构师。
|
||||
|
||||
# Objective
|
||||
你的任务是根据用户提供的【原始庞大 JSON 样例】和【自然语言提取要求】,编写一段精简、安全、高效的 JavaScript 代码,用于在 GraalVM 环境下对 JSON 进行瘦身清洗。这段清洗后的数据将被直接喂给大模型(LLM)作为上下文。
|
||||
|
||||
# Constraints & Rules
|
||||
1. 你的输出必须且只能包含一个名为 `wash` 的 JavaScript 函数。
|
||||
2. 该函数的入参必须是 `rawJsonString` (字符串类型)。
|
||||
3. 该函数的返回值必须是清洗并序列化后的 JSON 字符串,以确保后续直接使用。
|
||||
4. 代码必须健壮,需处理常见的空指针(undefined/null)和数组越界异常。
|
||||
5. 必须剔除所有无业务价值的字段(如 HTML 标签、内部 traceId、全 null 字段等),最大限度节省 LLM 的 Token。
|
||||
6. 不要使用任何外部依赖或 npm 包,只能使用 ES6+ 标准原生语法。
|
||||
|
||||
# Output Format Example
|
||||
```javascript
|
||||
function wash(rawJsonString) {
|
||||
try {
|
||||
const data = JSON.parse(rawJsonString);
|
||||
if (!data || !data.list) return "[]";
|
||||
|
||||
const result = data.list.map(item => ({
|
||||
eventName: item.event_name || '未知',
|
||||
date: item.timestamp ? new Date(item.timestamp).toISOString().split('T')[0] : '',
|
||||
coreProfit: item.financial_data?.net_profit ?? 0
|
||||
}));
|
||||
|
||||
return JSON.stringify(result);
|
||||
} catch (e) {
|
||||
return JSON.stringify({ error: "数据清洗失败" });
|
||||
}
|
||||
}
|
||||
```
|
||||
***
|
||||
|
||||
把这份文档丢给 Antigravity 后,你可以直接下达指令:**“请先根据第 3 节生成所有 MySQL 建表语句,然后生成第 5 节的 Filter 责任链代码骨架。”** 期待你的架构早日跑通!
|
||||
@@ -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,58 @@
|
||||
package com.cicc.enterprise.gateway.controller;
|
||||
|
||||
import com.cicc.enterprise.gateway.model.AiToolRegistry;
|
||||
import com.cicc.enterprise.gateway.service.AiToolRegistryService;
|
||||
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
|
||||
public class ControlPlaneAdminController {
|
||||
|
||||
private final AiToolRegistryService toolRegistryService;
|
||||
|
||||
// Standard CRUD
|
||||
@GetMapping("/tools")
|
||||
public List<AiToolRegistry> listTools() {
|
||||
return toolRegistryService.list();
|
||||
}
|
||||
|
||||
@PostMapping("/tools")
|
||||
public AiToolRegistry createTool(@RequestBody AiToolRegistry tool) {
|
||||
toolRegistryService.save(tool);
|
||||
return tool;
|
||||
}
|
||||
|
||||
// AI Copilot Mock API for Wash Script Generation
|
||||
@PostMapping("/copilot/generate-wash-script")
|
||||
public Map<String, String> generateWashScript(@RequestBody Map<String, String> request) {
|
||||
// Extract parameters from request if needed for dynamic prompt building
|
||||
// String rawJsonSample = request.get("rawJsonSample");
|
||||
// String requirement = request.get("requirement");
|
||||
String systemPrompt = """
|
||||
# Role
|
||||
你是一个资深的 JavaScript 数据处理专家与大模型工程架构师。
|
||||
|
||||
# Objective
|
||||
根据用户提供的【原始庞大 JSON 样例】和【自然语言提取要求】,编写一段精简、安全、高效的 JavaScript 代码,用于在 GraalVM 环境下对 JSON 进行瘦身清洗。这段清洗后的数据将被直接喂给大模型(LLM)作为上下文。
|
||||
|
||||
# Constraints & Rules
|
||||
1. 你的输出必须且只能包含一个名为 `wash` 的 JavaScript 函数。
|
||||
2. 该函数的入参必须是 `rawJsonString` (字符串类型)。
|
||||
3. 该函数的返回值必须是清洗并序列化后的 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 {
|
||||
}
|
||||
+11
@@ -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,27 @@
|
||||
server:
|
||||
port: 8080
|
||||
spring:
|
||||
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&allowPublicKeyRetrieval=true
|
||||
username: root
|
||||
password: Gmcc@123
|
||||
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
|
||||
@@ -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='全链路安全审计日志表';
|
||||
Reference in New Issue
Block a user