From af9c383c708c986ea6f6ee0a92231388aa6b1399 Mon Sep 17 00:00:00 2001 From: yyyhcyang Date: Tue, 17 Mar 2026 17:57:00 +0800 Subject: [PATCH] init --- .idea/.gitignore | 9 ++ .idea/compiler.xml | 19 +++ .idea/dataSources.xml | 17 +++ .idea/encodings.xml | 6 + .idea/jarRepositories.xml | 25 ++++ .idea/misc.xml | 12 ++ .idea/vcs.xml | 6 + disign.md | 134 ++++++++++++++++++ pom.xml | 99 +++++++++++++ .../gateway/GatewayApplication.java | 13 ++ .../gateway/config/MybatisPlusConfig.java | 19 +++ .../gateway/config/RedisConfig.java | 23 +++ .../ControlPlaneAdminController.java | 58 ++++++++ .../controller/GatewayEntryController.java | 65 +++++++++ .../gateway/engine/GraalVmJsEngine.java | 54 +++++++ .../gateway/engine/LocalCacheManager.java | 88 ++++++++++++ .../gateway/engine/OkHttpProxyClient.java | 49 +++++++ .../filter/core/ExecutionStrategy.java | 27 ++++ .../gateway/filter/core/GatewayContext.java | 33 +++++ .../gateway/filter/core/GatewayFilter.java | 8 ++ .../filter/core/GatewayFilterChain.java | 19 +++ .../filter/core/GatewayFilterExecutor.java | 29 ++++ .../gateway/filter/impl/AsyncAuditFilter.java | 64 +++++++++ .../filter/impl/AuthRateLimitingFilter.java | 45 ++++++ .../filter/impl/ConfigWatcherFilter.java | 51 +++++++ .../filter/impl/GraalVmWashingFilter.java | 39 +++++ .../filter/impl/HitlSuspenderFilter.java | 39 +++++ .../impl/HttpExecutionStrategyImpl.java | 31 ++++ .../filter/impl/IdentityOboFilter.java | 24 ++++ .../filter/impl/ProxyExecutionFilter.java | 68 +++++++++ .../gateway/mapper/AiAuditLogMapper.java | 9 ++ .../gateway/mapper/AiTenantMapper.java | 9 ++ .../gateway/mapper/AiTenantToolRelMapper.java | 9 ++ .../gateway/mapper/AiToolRegistryMapper.java | 9 ++ .../gateway/mapper/AiWashScriptMapper.java | 9 ++ .../enterprise/gateway/model/AiAuditLog.java | 22 +++ .../enterprise/gateway/model/AiTenant.java | 21 +++ .../gateway/model/AiTenantToolRel.java | 15 ++ .../gateway/model/AiToolRegistry.java | 23 +++ .../gateway/model/AiWashScript.java | 19 +++ .../gateway/service/AiAuditLogService.java | 7 + .../gateway/service/AiTenantService.java | 7 + .../service/AiTenantToolRelService.java | 7 + .../service/AiToolRegistryService.java | 7 + .../gateway/service/AiWashScriptService.java | 7 + .../service/impl/AiAuditLogServiceImpl.java | 11 ++ .../service/impl/AiTenantServiceImpl.java | 11 ++ .../impl/AiTenantToolRelServiceImpl.java | 11 ++ .../impl/AiToolRegistryServiceImpl.java | 11 ++ .../service/impl/AiWashScriptServiceImpl.java | 11 ++ src/main/resources/application.yml | 27 ++++ src/main/resources/schema.sql | 69 +++++++++ 52 files changed, 1504 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/compiler.xml create mode 100644 .idea/dataSources.xml create mode 100644 .idea/encodings.xml create mode 100644 .idea/jarRepositories.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/vcs.xml create mode 100644 disign.md create mode 100644 pom.xml create mode 100644 src/main/java/com/cicc/enterprise/gateway/GatewayApplication.java create mode 100644 src/main/java/com/cicc/enterprise/gateway/config/MybatisPlusConfig.java create mode 100644 src/main/java/com/cicc/enterprise/gateway/config/RedisConfig.java create mode 100644 src/main/java/com/cicc/enterprise/gateway/controller/ControlPlaneAdminController.java create mode 100644 src/main/java/com/cicc/enterprise/gateway/controller/GatewayEntryController.java create mode 100644 src/main/java/com/cicc/enterprise/gateway/engine/GraalVmJsEngine.java create mode 100644 src/main/java/com/cicc/enterprise/gateway/engine/LocalCacheManager.java create mode 100644 src/main/java/com/cicc/enterprise/gateway/engine/OkHttpProxyClient.java create mode 100644 src/main/java/com/cicc/enterprise/gateway/filter/core/ExecutionStrategy.java create mode 100644 src/main/java/com/cicc/enterprise/gateway/filter/core/GatewayContext.java create mode 100644 src/main/java/com/cicc/enterprise/gateway/filter/core/GatewayFilter.java create mode 100644 src/main/java/com/cicc/enterprise/gateway/filter/core/GatewayFilterChain.java create mode 100644 src/main/java/com/cicc/enterprise/gateway/filter/core/GatewayFilterExecutor.java create mode 100644 src/main/java/com/cicc/enterprise/gateway/filter/impl/AsyncAuditFilter.java create mode 100644 src/main/java/com/cicc/enterprise/gateway/filter/impl/AuthRateLimitingFilter.java create mode 100644 src/main/java/com/cicc/enterprise/gateway/filter/impl/ConfigWatcherFilter.java create mode 100644 src/main/java/com/cicc/enterprise/gateway/filter/impl/GraalVmWashingFilter.java create mode 100644 src/main/java/com/cicc/enterprise/gateway/filter/impl/HitlSuspenderFilter.java create mode 100644 src/main/java/com/cicc/enterprise/gateway/filter/impl/HttpExecutionStrategyImpl.java create mode 100644 src/main/java/com/cicc/enterprise/gateway/filter/impl/IdentityOboFilter.java create mode 100644 src/main/java/com/cicc/enterprise/gateway/filter/impl/ProxyExecutionFilter.java create mode 100644 src/main/java/com/cicc/enterprise/gateway/mapper/AiAuditLogMapper.java create mode 100644 src/main/java/com/cicc/enterprise/gateway/mapper/AiTenantMapper.java create mode 100644 src/main/java/com/cicc/enterprise/gateway/mapper/AiTenantToolRelMapper.java create mode 100644 src/main/java/com/cicc/enterprise/gateway/mapper/AiToolRegistryMapper.java create mode 100644 src/main/java/com/cicc/enterprise/gateway/mapper/AiWashScriptMapper.java create mode 100644 src/main/java/com/cicc/enterprise/gateway/model/AiAuditLog.java create mode 100644 src/main/java/com/cicc/enterprise/gateway/model/AiTenant.java create mode 100644 src/main/java/com/cicc/enterprise/gateway/model/AiTenantToolRel.java create mode 100644 src/main/java/com/cicc/enterprise/gateway/model/AiToolRegistry.java create mode 100644 src/main/java/com/cicc/enterprise/gateway/model/AiWashScript.java create mode 100644 src/main/java/com/cicc/enterprise/gateway/service/AiAuditLogService.java create mode 100644 src/main/java/com/cicc/enterprise/gateway/service/AiTenantService.java create mode 100644 src/main/java/com/cicc/enterprise/gateway/service/AiTenantToolRelService.java create mode 100644 src/main/java/com/cicc/enterprise/gateway/service/AiToolRegistryService.java create mode 100644 src/main/java/com/cicc/enterprise/gateway/service/AiWashScriptService.java create mode 100644 src/main/java/com/cicc/enterprise/gateway/service/impl/AiAuditLogServiceImpl.java create mode 100644 src/main/java/com/cicc/enterprise/gateway/service/impl/AiTenantServiceImpl.java create mode 100644 src/main/java/com/cicc/enterprise/gateway/service/impl/AiTenantToolRelServiceImpl.java create mode 100644 src/main/java/com/cicc/enterprise/gateway/service/impl/AiToolRegistryServiceImpl.java create mode 100644 src/main/java/com/cicc/enterprise/gateway/service/impl/AiWashScriptServiceImpl.java create mode 100644 src/main/resources/application.yml create mode 100644 src/main/resources/schema.sql diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..55eb5d3 --- /dev/null +++ b/.idea/.gitignore @@ -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 diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b221fc0 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..b2ddf3f --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,17 @@ + + + + + mysql.8 + true + com.mysql.cj.jdbc.Driver + jdbc:mysql://localhost:3306 + + + + + + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..63e9001 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..ea7349c --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..9dc782b --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/disign.md b/disign.md new file mode 100644 index 0000000..87e1478 --- /dev/null +++ b/disign.md @@ -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 责任链代码骨架。”** 期待你的架构早日跑通! \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..ef8ef25 --- /dev/null +++ b/pom.xml @@ -0,0 +1,99 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.4 + + + com.cicc.enterprise + ai-agent-gateway + 1.0.0-SNAPSHOT + ai-agent-gateway + Enterprise AI Agent Gateway + + + 21 + 23.1.2 + 3.5.5 + 2.0.47 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.springframework.boot + spring-boot-starter-cache + + + com.github.ben-manes.caffeine + caffeine + + + com.baomidou + mybatis-plus-spring-boot3-starter + ${mybatis-plus.version} + + + com.mysql + mysql-connector-j + runtime + + + com.alibaba.fastjson2 + fastjson2 + ${fastjson2.version} + + + org.projectlombok + lombok + true + + + + + org.graalvm.polyglot + polyglot + ${graalvm.version} + + + org.graalvm.polyglot + js + ${graalvm.version} + pom + + + + + com.squareup.okhttp3 + okhttp + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + diff --git a/src/main/java/com/cicc/enterprise/gateway/GatewayApplication.java b/src/main/java/com/cicc/enterprise/gateway/GatewayApplication.java new file mode 100644 index 0000000..75a8f29 --- /dev/null +++ b/src/main/java/com/cicc/enterprise/gateway/GatewayApplication.java @@ -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); + } +} diff --git a/src/main/java/com/cicc/enterprise/gateway/config/MybatisPlusConfig.java b/src/main/java/com/cicc/enterprise/gateway/config/MybatisPlusConfig.java new file mode 100644 index 0000000..d23924a --- /dev/null +++ b/src/main/java/com/cicc/enterprise/gateway/config/MybatisPlusConfig.java @@ -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; + } +} diff --git a/src/main/java/com/cicc/enterprise/gateway/config/RedisConfig.java b/src/main/java/com/cicc/enterprise/gateway/config/RedisConfig.java new file mode 100644 index 0000000..0fe6209 --- /dev/null +++ b/src/main/java/com/cicc/enterprise/gateway/config/RedisConfig.java @@ -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 redisTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate 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; + } +} diff --git a/src/main/java/com/cicc/enterprise/gateway/controller/ControlPlaneAdminController.java b/src/main/java/com/cicc/enterprise/gateway/controller/ControlPlaneAdminController.java new file mode 100644 index 0000000..3100b11 --- /dev/null +++ b/src/main/java/com/cicc/enterprise/gateway/controller/ControlPlaneAdminController.java @@ -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 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 generateWashScript(@RequestBody Map 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 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; + } +} diff --git a/src/main/java/com/cicc/enterprise/gateway/controller/GatewayEntryController.java b/src/main/java/com/cicc/enterprise/gateway/controller/GatewayEntryController.java new file mode 100644 index 0000000..3e1d363 --- /dev/null +++ b/src/main/java/com/cicc/enterprise/gateway/controller/GatewayEntryController.java @@ -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> 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" + )); + } + } +} diff --git a/src/main/java/com/cicc/enterprise/gateway/engine/GraalVmJsEngine.java b/src/main/java/com/cicc/enterprise/gateway/engine/GraalVmJsEngine.java new file mode 100644 index 0000000..d5d2007 --- /dev/null +++ b/src/main/java/com/cicc/enterprise/gateway/engine/GraalVmJsEngine.java @@ -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 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); + } + } +} diff --git a/src/main/java/com/cicc/enterprise/gateway/engine/LocalCacheManager.java b/src/main/java/com/cicc/enterprise/gateway/engine/LocalCacheManager.java new file mode 100644 index 0000000..c8ec918 --- /dev/null +++ b/src/main/java/com/cicc/enterprise/gateway/engine/LocalCacheManager.java @@ -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 tenantCache; + // cache toolName -> AiToolRegistry + private Cache toolCache; + // cache toolId -> Optional + private Cache> scriptCache; + // cache tenantId_toolId -> Boolean + private Cache 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 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); + } +} diff --git a/src/main/java/com/cicc/enterprise/gateway/engine/OkHttpProxyClient.java b/src/main/java/com/cicc/enterprise/gateway/engine/OkHttpProxyClient.java new file mode 100644 index 0000000..f11c589 --- /dev/null +++ b/src/main/java/com/cicc/enterprise/gateway/engine/OkHttpProxyClient.java @@ -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 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 ""; + } + } +} diff --git a/src/main/java/com/cicc/enterprise/gateway/filter/core/ExecutionStrategy.java b/src/main/java/com/cicc/enterprise/gateway/filter/core/ExecutionStrategy.java new file mode 100644 index 0000000..8cf8a95 --- /dev/null +++ b/src/main/java/com/cicc/enterprise/gateway/filter/core/ExecutionStrategy.java @@ -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 extraHeaders) throws Exception; +} diff --git a/src/main/java/com/cicc/enterprise/gateway/filter/core/GatewayContext.java b/src/main/java/com/cicc/enterprise/gateway/filter/core/GatewayContext.java new file mode 100644 index 0000000..e5c73a5 --- /dev/null +++ b/src/main/java/com/cicc/enterprise/gateway/filter/core/GatewayContext.java @@ -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 extraHeaders = new HashMap<>(); + + // Result + private String rawResponseJson; + private String washedResponseJson; + + private Exception error; + private long startTimeMs; + private long costTimeMs; +} diff --git a/src/main/java/com/cicc/enterprise/gateway/filter/core/GatewayFilter.java b/src/main/java/com/cicc/enterprise/gateway/filter/core/GatewayFilter.java new file mode 100644 index 0000000..8599685 --- /dev/null +++ b/src/main/java/com/cicc/enterprise/gateway/filter/core/GatewayFilter.java @@ -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(); +} diff --git a/src/main/java/com/cicc/enterprise/gateway/filter/core/GatewayFilterChain.java b/src/main/java/com/cicc/enterprise/gateway/filter/core/GatewayFilterChain.java new file mode 100644 index 0000000..e8ac35e --- /dev/null +++ b/src/main/java/com/cicc/enterprise/gateway/filter/core/GatewayFilterChain.java @@ -0,0 +1,19 @@ +package com.cicc.enterprise.gateway.filter.core; + +import java.util.List; + +public class GatewayFilterChain { + private final List filters; + private int index = 0; + + public GatewayFilterChain(List 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); + } + } +} diff --git a/src/main/java/com/cicc/enterprise/gateway/filter/core/GatewayFilterExecutor.java b/src/main/java/com/cicc/enterprise/gateway/filter/core/GatewayFilterExecutor.java new file mode 100644 index 0000000..2be7155 --- /dev/null +++ b/src/main/java/com/cicc/enterprise/gateway/filter/core/GatewayFilterExecutor.java @@ -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 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); + } +} diff --git a/src/main/java/com/cicc/enterprise/gateway/filter/impl/AsyncAuditFilter.java b/src/main/java/com/cicc/enterprise/gateway/filter/impl/AsyncAuditFilter.java new file mode 100644 index 0000000..8da03f1 --- /dev/null +++ b/src/main/java/com/cicc/enterprise/gateway/filter/impl/AsyncAuditFilter.java @@ -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); + } + }); + } + } +} diff --git a/src/main/java/com/cicc/enterprise/gateway/filter/impl/AuthRateLimitingFilter.java b/src/main/java/com/cicc/enterprise/gateway/filter/impl/AuthRateLimitingFilter.java new file mode 100644 index 0000000..70f3a77 --- /dev/null +++ b/src/main/java/com/cicc/enterprise/gateway/filter/impl/AuthRateLimitingFilter.java @@ -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 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); + } +} diff --git a/src/main/java/com/cicc/enterprise/gateway/filter/impl/ConfigWatcherFilter.java b/src/main/java/com/cicc/enterprise/gateway/filter/impl/ConfigWatcherFilter.java new file mode 100644 index 0000000..56f63c1 --- /dev/null +++ b/src/main/java/com/cicc/enterprise/gateway/filter/impl/ConfigWatcherFilter.java @@ -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); + } +} diff --git a/src/main/java/com/cicc/enterprise/gateway/filter/impl/GraalVmWashingFilter.java b/src/main/java/com/cicc/enterprise/gateway/filter/impl/GraalVmWashingFilter.java new file mode 100644 index 0000000..a11a1bb --- /dev/null +++ b/src/main/java/com/cicc/enterprise/gateway/filter/impl/GraalVmWashingFilter.java @@ -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); + } +} diff --git a/src/main/java/com/cicc/enterprise/gateway/filter/impl/HitlSuspenderFilter.java b/src/main/java/com/cicc/enterprise/gateway/filter/impl/HitlSuspenderFilter.java new file mode 100644 index 0000000..b133cbe --- /dev/null +++ b/src/main/java/com/cicc/enterprise/gateway/filter/impl/HitlSuspenderFilter.java @@ -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; + } +} diff --git a/src/main/java/com/cicc/enterprise/gateway/filter/impl/HttpExecutionStrategyImpl.java b/src/main/java/com/cicc/enterprise/gateway/filter/impl/HttpExecutionStrategyImpl.java new file mode 100644 index 0000000..10ee438 --- /dev/null +++ b/src/main/java/com/cicc/enterprise/gateway/filter/impl/HttpExecutionStrategyImpl.java @@ -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 extraHeaders) throws Exception { + return proxyClient.forwardProxy( + tool.getTargetUrl(), + tool.getHttpMethod(), + requestBodyJson, + extraHeaders + ); + } +} diff --git a/src/main/java/com/cicc/enterprise/gateway/filter/impl/IdentityOboFilter.java b/src/main/java/com/cicc/enterprise/gateway/filter/impl/IdentityOboFilter.java new file mode 100644 index 0000000..c7c2e4a --- /dev/null +++ b/src/main/java/com/cicc/enterprise/gateway/filter/impl/IdentityOboFilter.java @@ -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); + } +} diff --git a/src/main/java/com/cicc/enterprise/gateway/filter/impl/ProxyExecutionFilter.java b/src/main/java/com/cicc/enterprise/gateway/filter/impl/ProxyExecutionFilter.java new file mode 100644 index 0000000..e2591fd --- /dev/null +++ b/src/main/java/com/cicc/enterprise/gateway/filter/impl/ProxyExecutionFilter.java @@ -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 strategyList; + private final Map 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); + } +} diff --git a/src/main/java/com/cicc/enterprise/gateway/mapper/AiAuditLogMapper.java b/src/main/java/com/cicc/enterprise/gateway/mapper/AiAuditLogMapper.java new file mode 100644 index 0000000..f205287 --- /dev/null +++ b/src/main/java/com/cicc/enterprise/gateway/mapper/AiAuditLogMapper.java @@ -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 { +} diff --git a/src/main/java/com/cicc/enterprise/gateway/mapper/AiTenantMapper.java b/src/main/java/com/cicc/enterprise/gateway/mapper/AiTenantMapper.java new file mode 100644 index 0000000..a7fc9e5 --- /dev/null +++ b/src/main/java/com/cicc/enterprise/gateway/mapper/AiTenantMapper.java @@ -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 { +} diff --git a/src/main/java/com/cicc/enterprise/gateway/mapper/AiTenantToolRelMapper.java b/src/main/java/com/cicc/enterprise/gateway/mapper/AiTenantToolRelMapper.java new file mode 100644 index 0000000..c2fede5 --- /dev/null +++ b/src/main/java/com/cicc/enterprise/gateway/mapper/AiTenantToolRelMapper.java @@ -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 { +} diff --git a/src/main/java/com/cicc/enterprise/gateway/mapper/AiToolRegistryMapper.java b/src/main/java/com/cicc/enterprise/gateway/mapper/AiToolRegistryMapper.java new file mode 100644 index 0000000..ded4ced --- /dev/null +++ b/src/main/java/com/cicc/enterprise/gateway/mapper/AiToolRegistryMapper.java @@ -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 { +} diff --git a/src/main/java/com/cicc/enterprise/gateway/mapper/AiWashScriptMapper.java b/src/main/java/com/cicc/enterprise/gateway/mapper/AiWashScriptMapper.java new file mode 100644 index 0000000..66006e4 --- /dev/null +++ b/src/main/java/com/cicc/enterprise/gateway/mapper/AiWashScriptMapper.java @@ -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 { +} diff --git a/src/main/java/com/cicc/enterprise/gateway/model/AiAuditLog.java b/src/main/java/com/cicc/enterprise/gateway/model/AiAuditLog.java new file mode 100644 index 0000000..5075f61 --- /dev/null +++ b/src/main/java/com/cicc/enterprise/gateway/model/AiAuditLog.java @@ -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; +} diff --git a/src/main/java/com/cicc/enterprise/gateway/model/AiTenant.java b/src/main/java/com/cicc/enterprise/gateway/model/AiTenant.java new file mode 100644 index 0000000..581458c --- /dev/null +++ b/src/main/java/com/cicc/enterprise/gateway/model/AiTenant.java @@ -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; +} diff --git a/src/main/java/com/cicc/enterprise/gateway/model/AiTenantToolRel.java b/src/main/java/com/cicc/enterprise/gateway/model/AiTenantToolRel.java new file mode 100644 index 0000000..8ab37d4 --- /dev/null +++ b/src/main/java/com/cicc/enterprise/gateway/model/AiTenantToolRel.java @@ -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; +} diff --git a/src/main/java/com/cicc/enterprise/gateway/model/AiToolRegistry.java b/src/main/java/com/cicc/enterprise/gateway/model/AiToolRegistry.java new file mode 100644 index 0000000..fab0e2e --- /dev/null +++ b/src/main/java/com/cicc/enterprise/gateway/model/AiToolRegistry.java @@ -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; +} diff --git a/src/main/java/com/cicc/enterprise/gateway/model/AiWashScript.java b/src/main/java/com/cicc/enterprise/gateway/model/AiWashScript.java new file mode 100644 index 0000000..0b628d0 --- /dev/null +++ b/src/main/java/com/cicc/enterprise/gateway/model/AiWashScript.java @@ -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; +} diff --git a/src/main/java/com/cicc/enterprise/gateway/service/AiAuditLogService.java b/src/main/java/com/cicc/enterprise/gateway/service/AiAuditLogService.java new file mode 100644 index 0000000..ccf6474 --- /dev/null +++ b/src/main/java/com/cicc/enterprise/gateway/service/AiAuditLogService.java @@ -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 { +} diff --git a/src/main/java/com/cicc/enterprise/gateway/service/AiTenantService.java b/src/main/java/com/cicc/enterprise/gateway/service/AiTenantService.java new file mode 100644 index 0000000..b68c62a --- /dev/null +++ b/src/main/java/com/cicc/enterprise/gateway/service/AiTenantService.java @@ -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 { +} diff --git a/src/main/java/com/cicc/enterprise/gateway/service/AiTenantToolRelService.java b/src/main/java/com/cicc/enterprise/gateway/service/AiTenantToolRelService.java new file mode 100644 index 0000000..467a4e6 --- /dev/null +++ b/src/main/java/com/cicc/enterprise/gateway/service/AiTenantToolRelService.java @@ -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 { +} diff --git a/src/main/java/com/cicc/enterprise/gateway/service/AiToolRegistryService.java b/src/main/java/com/cicc/enterprise/gateway/service/AiToolRegistryService.java new file mode 100644 index 0000000..aa5ce93 --- /dev/null +++ b/src/main/java/com/cicc/enterprise/gateway/service/AiToolRegistryService.java @@ -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 { +} diff --git a/src/main/java/com/cicc/enterprise/gateway/service/AiWashScriptService.java b/src/main/java/com/cicc/enterprise/gateway/service/AiWashScriptService.java new file mode 100644 index 0000000..ea2b8b8 --- /dev/null +++ b/src/main/java/com/cicc/enterprise/gateway/service/AiWashScriptService.java @@ -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 { +} diff --git a/src/main/java/com/cicc/enterprise/gateway/service/impl/AiAuditLogServiceImpl.java b/src/main/java/com/cicc/enterprise/gateway/service/impl/AiAuditLogServiceImpl.java new file mode 100644 index 0000000..4795914 --- /dev/null +++ b/src/main/java/com/cicc/enterprise/gateway/service/impl/AiAuditLogServiceImpl.java @@ -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 implements AiAuditLogService { +} diff --git a/src/main/java/com/cicc/enterprise/gateway/service/impl/AiTenantServiceImpl.java b/src/main/java/com/cicc/enterprise/gateway/service/impl/AiTenantServiceImpl.java new file mode 100644 index 0000000..c7c9f6e --- /dev/null +++ b/src/main/java/com/cicc/enterprise/gateway/service/impl/AiTenantServiceImpl.java @@ -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 implements AiTenantService { +} diff --git a/src/main/java/com/cicc/enterprise/gateway/service/impl/AiTenantToolRelServiceImpl.java b/src/main/java/com/cicc/enterprise/gateway/service/impl/AiTenantToolRelServiceImpl.java new file mode 100644 index 0000000..c4ed1ca --- /dev/null +++ b/src/main/java/com/cicc/enterprise/gateway/service/impl/AiTenantToolRelServiceImpl.java @@ -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 implements AiTenantToolRelService { +} diff --git a/src/main/java/com/cicc/enterprise/gateway/service/impl/AiToolRegistryServiceImpl.java b/src/main/java/com/cicc/enterprise/gateway/service/impl/AiToolRegistryServiceImpl.java new file mode 100644 index 0000000..75d40fe --- /dev/null +++ b/src/main/java/com/cicc/enterprise/gateway/service/impl/AiToolRegistryServiceImpl.java @@ -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 implements AiToolRegistryService { +} diff --git a/src/main/java/com/cicc/enterprise/gateway/service/impl/AiWashScriptServiceImpl.java b/src/main/java/com/cicc/enterprise/gateway/service/impl/AiWashScriptServiceImpl.java new file mode 100644 index 0000000..55e21c0 --- /dev/null +++ b/src/main/java/com/cicc/enterprise/gateway/service/impl/AiWashScriptServiceImpl.java @@ -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 implements AiWashScriptService { +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..61e4e49 --- /dev/null +++ b/src/main/resources/application.yml @@ -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 diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 0000000..0b1db40 --- /dev/null +++ b/src/main/resources/schema.sql @@ -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='全链路安全审计日志表';