chore: initialize qiming workspace repository
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
<?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>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-bootstrap</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
</parent>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-web-bootstrap</artifactId>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-mcp-ui</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-agent-core-ui</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-im-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-knowledge-core-ui</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-knowledge-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-compose-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-compose-ui</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-log-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-log-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-eco-market-client-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-eco-market-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-custom-page-ui</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-sandbox-ui</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-memory-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-model-proxy-application</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-file-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>system-api</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>system-web</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-credit-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-subscription-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-pricing-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-pay-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-bill-web</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>dev</id>
|
||||
<properties>
|
||||
<!-- 环境标识,需要与配置文件的名称相对应 -->
|
||||
<activatedProperties>dev</activatedProperties>
|
||||
</properties>
|
||||
<activation>
|
||||
<!-- 默认环境 -->
|
||||
<activeByDefault>true</activeByDefault>
|
||||
</activation>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>test</id>
|
||||
<properties>
|
||||
<activatedProperties>test</activatedProperties>
|
||||
</properties>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>prod</id>
|
||||
<properties>
|
||||
<activatedProperties>prod</activatedProperties>
|
||||
</properties>
|
||||
</profile>
|
||||
</profiles>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.xspaceagi;
|
||||
|
||||
import com.xspaceagi.system.domain.log.EnableLogPrint;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
@EnableScheduling
|
||||
@EnableCaching
|
||||
@EnableLogPrint
|
||||
@SpringBootApplication(scanBasePackages = {"com.xspaceagi", "io.springfox"})
|
||||
public class PlatformApiApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(PlatformApiApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
//package com.xspaceagi.config;
|
||||
//
|
||||
//import org.springframework.context.annotation.Bean;
|
||||
//import org.springframework.context.annotation.Configuration;
|
||||
//
|
||||
//
|
||||
//@Configuration
|
||||
//@EnableSwagger
|
||||
//public class SwaggerConfig {
|
||||
//
|
||||
// @Bean
|
||||
// public Docket createRestApiOpRecord() {
|
||||
// return new Docket(DocumentationType.SWAGGER_2).groupName("接口文档").useDefaultResponseMessages(false).apiInfo(apiInfo()).select()
|
||||
// .apis(RequestHandlerSelectors.basePackage("com.xspaceagi.agent.web.ui")).paths(PathSelectors.any()).build();
|
||||
// }
|
||||
//
|
||||
// private ApiInfo apiInfo() {
|
||||
// return new ApiInfoBuilder().title("应用发布页接口文档v1").description("接口文档").version("v1").build();
|
||||
// }
|
||||
//}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.xspaceagi.config;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import com.fasterxml.jackson.core.StreamReadConstraints;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
import com.xspaceagi.log.spec.CustomLocalDateTimeSerializer;
|
||||
import com.xspaceagi.log.spec.FlexibleDateTimeDeserializer;
|
||||
|
||||
/**
|
||||
* UTC 时间序列化配置
|
||||
*/
|
||||
@Configuration
|
||||
public class UTCDateTimeConfig {
|
||||
|
||||
@Bean
|
||||
public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() {
|
||||
return builder -> {
|
||||
|
||||
// 使用 SimpleModule 统一注册序列化器和反序列化器
|
||||
SimpleModule module = new SimpleModule();
|
||||
|
||||
// 注册自定义序列化器
|
||||
module.addSerializer(LocalDateTime.class, new CustomLocalDateTimeSerializer());
|
||||
|
||||
// 注册自定义反序列化器
|
||||
module.addDeserializer(LocalDateTime.class, new FlexibleDateTimeDeserializer());
|
||||
|
||||
builder.modules(module);
|
||||
|
||||
// 配置 StreamReadConstraints 以支持更大的字符串长度
|
||||
// 设置最大字符串长度为 100MB (100 * 1024 * 1024 字符),用于支持大文件内容
|
||||
builder.postConfigurer(objectMapper -> {
|
||||
StreamReadConstraints constraints = StreamReadConstraints.builder()
|
||||
.maxStringLength(100 * 1024 * 1024)
|
||||
.build();
|
||||
objectMapper.getFactory().setStreamReadConstraints(constraints);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.xspaceagi.config;
|
||||
|
||||
import io.milvus.v2.client.ConnectConfig;
|
||||
import io.milvus.v2.client.MilvusClientV2;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class VectorDBConfig {
|
||||
|
||||
@Value("${milvus.uri}")
|
||||
private String milvusUri;
|
||||
|
||||
@Lazy
|
||||
@Bean
|
||||
public MilvusClientV2 milvusClient() {
|
||||
|
||||
log.info("milvus uri:{}", milvusUri);
|
||||
|
||||
ConnectConfig config = ConnectConfig.builder()
|
||||
.uri(milvusUri)
|
||||
.build();
|
||||
|
||||
MilvusClientV2 client = new MilvusClientV2(config);
|
||||
log.info("milvus client init success, uri:{}", milvusUri);
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.xspaceagi.config;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.core.convert.converter.ConverterFactory;
|
||||
import org.springframework.format.FormatterRegistry;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.StringHttpMessageConverter;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IEnum;
|
||||
import com.xspaceagi.interceptor.ApiKeyInterceptor;
|
||||
import com.xspaceagi.interceptor.AuthInterceptor;
|
||||
import com.xspaceagi.interceptor.HttpInterceptor;
|
||||
import com.xspaceagi.interceptor.TIdInterceptor;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
@Configuration
|
||||
public class WebMvcConfig implements WebMvcConfigurer {
|
||||
|
||||
@Autowired
|
||||
private HttpInterceptor httpInterceptor;
|
||||
|
||||
@Autowired
|
||||
private AuthInterceptor jwtInterceptor;
|
||||
|
||||
/**
|
||||
* tid统一标识处理
|
||||
*/
|
||||
@Resource
|
||||
private TIdInterceptor tidInterceptor;
|
||||
|
||||
@Resource
|
||||
private ApiKeyInterceptor apiKeyInterceptor;
|
||||
|
||||
@Value("${access.control.allow-origin}")
|
||||
private String accessControlAllowOrigin;
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry interceptorRegistry) {
|
||||
// 请求参数记录日志拦截器
|
||||
interceptorRegistry.addInterceptor(tidInterceptor).order(1).addPathPatterns("/api/**");
|
||||
interceptorRegistry.addInterceptor(httpInterceptor).order(21).addPathPatterns("/api/**");
|
||||
interceptorRegistry.addInterceptor(jwtInterceptor).order(22).addPathPatterns("/api/**");
|
||||
interceptorRegistry.addInterceptor(apiKeyInterceptor).order(23).addPathPatterns("/api/**");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
registry.addMapping("/**") // 允许所有请求路径跨域访问
|
||||
.allowCredentials(true) // 是否携带Cookie,默认false
|
||||
.allowedHeaders("Origin", "X-Requested-With", "Content-Type", "Accept", "Authorization",
|
||||
"Cache-Control", "Fragment") // 允许的请求头类型
|
||||
.maxAge(3600) // 预检请求的缓存时间(单位:秒)
|
||||
.allowedMethods("HEAD", "GET", "POST", "PUT", "DELETE", "OPTIONS") // 允许的请求方法类型
|
||||
.allowedOriginPatterns(accessControlAllowOrigin); // 允许哪些域名进行跨域访问
|
||||
}
|
||||
|
||||
@Bean
|
||||
public HttpMessageConverter<String> responseBodyConverter() {
|
||||
Charset defaultCharset = StandardCharsets.UTF_8;
|
||||
return new StringHttpMessageConverter(defaultCharset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
|
||||
converters.add(responseBodyConverter());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addFormatters(FormatterRegistry registry) {
|
||||
registry.addConverterFactory(new ConverterFactory<String, IEnum>() {
|
||||
@Override
|
||||
public <T extends IEnum> Converter<String, T> getConverter(Class<T> targetType) {
|
||||
return source -> {
|
||||
Assert.isTrue(targetType.isEnum(), () -> "the class what implements IEnum must be the enum type");
|
||||
return Arrays.stream(targetType.getEnumConstants())
|
||||
.filter(t -> String.valueOf(t.getValue()).equals(source))
|
||||
.findFirst().orElse(null);
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.xspaceagi.ctl;
|
||||
|
||||
|
||||
import com.xspaceagi.system.spec.dto.ReqResult;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/")
|
||||
@Slf4j
|
||||
public class HealthController {
|
||||
|
||||
@RequestMapping(path = "/health")
|
||||
public ReqResult<Void> health() {
|
||||
return ReqResult.success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.xspaceagi.filter;
|
||||
|
||||
import com.xspaceagi.sandbox.SandboxApiRewriteProperties;
|
||||
import com.xspaceagi.sandbox.SandboxRequestAttributes;
|
||||
import jakarta.servlet.*;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequestWrapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Sandbox 兼容 API URL 重写过滤器(全局生效)
|
||||
*/
|
||||
@Component
|
||||
@Order(1)
|
||||
@Slf4j
|
||||
public class SandboxApiRewriteFilter implements Filter {
|
||||
|
||||
private static final String SANDBOX_PREFIX = "/api/v1/4sandbox";
|
||||
/**
|
||||
* 允许重写的路径模式(相对于 /api/v1/4sandbox 之后的部分匹配)
|
||||
* 支持通配符:*、**(AntPathMatcher)
|
||||
*/
|
||||
private final SandboxApiRewriteProperties rewriteProperties;
|
||||
|
||||
private final AntPathMatcher pathMatcher = new AntPathMatcher();
|
||||
|
||||
public SandboxApiRewriteFilter(SandboxApiRewriteProperties rewriteProperties) {
|
||||
this.rewriteProperties = rewriteProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
|
||||
HttpServletRequest httpRequest = (HttpServletRequest) request;
|
||||
String requestURI = httpRequest.getRequestURI();
|
||||
if (requestURI == null || !requestURI.startsWith(SANDBOX_PREFIX)) {
|
||||
chain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
// 标记请求来源,供 controller 做沙箱/前端差异处理
|
||||
httpRequest.setAttribute(SandboxRequestAttributes.REQUEST_SOURCE, SandboxRequestAttributes.SOURCE_SANDBOX);
|
||||
|
||||
String suffix = requestURI.substring(SANDBOX_PREFIX.length());
|
||||
String pathAfterPrefix = suffix.startsWith("/") ? suffix.substring(1) : suffix;
|
||||
|
||||
if (!isAllowRewrite(pathAfterPrefix)) {
|
||||
chain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
// 保留原始 URI(/api/v1/4sandbox/...),给鉴权拦截器做白名单/AK 判断
|
||||
httpRequest.setAttribute(SandboxRequestAttributes.ORIGINAL_REQUEST_URI, requestURI);
|
||||
|
||||
String rewrittenURI = "/api" + suffix; // suffix 以 '/' 开头更安全
|
||||
log.debug("Rewrite 4Sandbox API URL: {} -> {}", requestURI, rewrittenURI);
|
||||
|
||||
HttpServletRequest wrappedRequest = new RewriteHttpServletRequestWrapper(httpRequest, rewrittenURI);
|
||||
chain.doFilter(wrappedRequest, response);
|
||||
}
|
||||
|
||||
private boolean isAllowRewrite(String pathAfterPrefix) {
|
||||
List<String> patterns = rewriteProperties.getAllowPath();
|
||||
if (patterns == null || patterns.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
for (String rawPattern : patterns) {
|
||||
String pattern = rawPattern == null ? "" : rawPattern.trim();
|
||||
if (pattern.startsWith("/")) {
|
||||
pattern = pattern.substring(1);
|
||||
}
|
||||
if (pattern.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
if (pathMatcher.match(pattern, pathAfterPrefix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static class RewriteHttpServletRequestWrapper extends HttpServletRequestWrapper {
|
||||
|
||||
private final String rewrittenURI;
|
||||
|
||||
public RewriteHttpServletRequestWrapper(HttpServletRequest request, String rewrittenURI) {
|
||||
super(request);
|
||||
this.rewrittenURI = rewrittenURI;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRequestURI() {
|
||||
return rewrittenURI;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getServletPath() {
|
||||
// Spring MVC 路由匹配使用 servletPath/requestURI 等信息;统一返回改写后的路径。
|
||||
return rewrittenURI;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.xspaceagi.interceptor;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* AES加解密工具类
|
||||
* 支持多种加密模式和填充方式
|
||||
*
|
||||
* @author Qiming
|
||||
*/
|
||||
public class AESCrypto {
|
||||
|
||||
// 加密算法
|
||||
private static final String ALGORITHM = "AES";
|
||||
|
||||
// 默认使用AES-256-CBC模式
|
||||
private static final String DEFAULT_TRANSFORMATION = "AES/CBC/PKCS5Padding";
|
||||
|
||||
/**
|
||||
* AES加密(支持自定义模式)
|
||||
*
|
||||
* @param plainText 明文
|
||||
* @param key 密钥
|
||||
* @param iv 初始化向量(ECB模式可为null)
|
||||
* @param transformation 转换模式(如:AES/CBC/PKCS5Padding)
|
||||
* @return Base64编码的密文
|
||||
*/
|
||||
public static String encrypt(String plainText, String key, String iv, String transformation) throws Exception {
|
||||
SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), ALGORITHM);
|
||||
Cipher cipher = Cipher.getInstance(transformation);
|
||||
|
||||
// 判断是否需要IV
|
||||
if (transformation.contains("ECB")) {
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
|
||||
} else {
|
||||
IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8));
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec);
|
||||
}
|
||||
|
||||
byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
|
||||
return Base64.getEncoder().encodeToString(encrypted);
|
||||
}
|
||||
|
||||
/**
|
||||
* AES解密(支持自定义模式)
|
||||
*
|
||||
* @param cipherText Base64编码的密文
|
||||
* @param key 密钥
|
||||
* @param iv 初始化向量(ECB模式可为null)
|
||||
* @param transformation 转换模式
|
||||
* @return 解密后的明文
|
||||
*/
|
||||
public static String decrypt(String cipherText, String key, String iv, String transformation) throws Exception {
|
||||
SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), ALGORITHM);
|
||||
Cipher cipher = Cipher.getInstance(transformation);
|
||||
|
||||
// 判断是否需要IV
|
||||
if (transformation.contains("ECB")) {
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey);
|
||||
} else {
|
||||
IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8));
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec);
|
||||
}
|
||||
|
||||
byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(cipherText));
|
||||
return new String(decrypted, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/**
|
||||
* AES解密(使用Hex密钥)
|
||||
*
|
||||
* @param cipherText Base64编码的密文
|
||||
* @param keyHex 十六进制密钥
|
||||
* @param ivHex 十六进制IV
|
||||
* @return 解密后的明文
|
||||
*/
|
||||
public static String decryptWithHexKey(String cipherText, String keyHex, String ivHex) throws Exception {
|
||||
byte[] keyBytes = hexStringToByteArray(keyHex);
|
||||
byte[] ivBytes = hexStringToByteArray(ivHex);
|
||||
|
||||
SecretKeySpec secretKey = new SecretKeySpec(keyBytes, ALGORITHM);
|
||||
Cipher cipher = Cipher.getInstance(DEFAULT_TRANSFORMATION);
|
||||
IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec);
|
||||
|
||||
byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(cipherText));
|
||||
return new String(decrypted, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/**
|
||||
* 十六进制字符串转字节数组
|
||||
*/
|
||||
private static byte[] hexStringToByteArray(String hex) {
|
||||
int len = hex.length();
|
||||
byte[] data = new byte[len / 2];
|
||||
for (int i = 0; i < len; i += 2) {
|
||||
data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4)
|
||||
+ Character.digit(hex.charAt(i + 1), 16));
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package com.xspaceagi.interceptor;
|
||||
|
||||
import com.xspaceagi.agent.core.adapter.application.AgentApplicationService;
|
||||
import com.xspaceagi.agent.core.adapter.dto.AgentDetailDto;
|
||||
import com.xspaceagi.sandbox.SandboxRequestAttributes;
|
||||
import com.xspaceagi.system.application.dto.TenantConfigDto;
|
||||
import com.xspaceagi.system.application.dto.UserDto;
|
||||
import com.xspaceagi.system.application.service.UserApiKeyApplicationService;
|
||||
import com.xspaceagi.system.application.service.UserApplicationService;
|
||||
import com.xspaceagi.system.infra.dao.entity.OpenApiDefinition;
|
||||
import com.xspaceagi.system.infra.dao.entity.User;
|
||||
import com.xspaceagi.system.sdk.service.UserAccessKeyApiService;
|
||||
import com.xspaceagi.system.sdk.service.dto.UserAccessKeyDto;
|
||||
import com.xspaceagi.system.spec.common.RequestContext;
|
||||
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
|
||||
import com.xspaceagi.system.spec.enums.YesOrNoEnum;
|
||||
import com.xspaceagi.system.spec.exception.BizException;
|
||||
import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
public class ApiKeyInterceptor implements HandlerInterceptor {
|
||||
|
||||
@Resource
|
||||
private UserApplicationService userApplicationService;
|
||||
|
||||
@Resource
|
||||
private AgentApplicationService agentApplicationService;
|
||||
|
||||
@Resource
|
||||
private UserAccessKeyApiService userAccessKeyApiService;
|
||||
|
||||
@Resource
|
||||
private UserApiKeyApplicationService userApiKeyApplicationService;
|
||||
|
||||
private final AntPathMatcher pathMatcher = new AntPathMatcher();
|
||||
|
||||
/**
|
||||
* 请求处理完之后
|
||||
*/
|
||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object obj, Exception exc) throws Exception {
|
||||
RequestContext.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求处理完成
|
||||
*/
|
||||
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object obj, ModelAndView model) throws Exception {
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求处理之前
|
||||
*/
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||
String originalRequestUri = getOriginalRequestUri(request);
|
||||
String authorization = request.getHeader("Authorization");
|
||||
if (authorization != null) {
|
||||
authorization = authorization.replaceFirst("Basic", "").replaceFirst("Bearer", "").trim();
|
||||
}
|
||||
//是否为ak校验
|
||||
if (authorization != null && (authorization.startsWith("ak-") || authorization.startsWith("ck-"))) {
|
||||
UserAccessKeyDto userAccessKeyDto = userAccessKeyApiService.queryAccessKey(authorization);
|
||||
if (userAccessKeyDto == null || (userAccessKeyDto.getStatus() != null && userAccessKeyDto.getStatus() != 1)) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.apiKeyInvalid);
|
||||
}
|
||||
if (userAccessKeyDto.getExpire() != null && userAccessKeyDto.getExpire().getTime() < System.currentTimeMillis()) {
|
||||
throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.apiKeyExpired);
|
||||
}
|
||||
if (!completeAuthContext(request, userAccessKeyDto)) {
|
||||
throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.apiKeyAccessDenied);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (originalRequestUri.startsWith("/api/v1/")) {
|
||||
throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.apiKeyMissing);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean completeAuthContext(HttpServletRequest request, UserAccessKeyDto userAccessKeyDto) {
|
||||
String originalRequestUri = getOriginalRequestUri(request);
|
||||
UserDto userDto = userApplicationService.queryById(userAccessKeyDto.getUserId());
|
||||
if (userDto == null || userDto.getStatus() == User.Status.Disabled || userDto.getStatus() == User.Status.Deleted) {
|
||||
throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.apiKeyUserDisabled);
|
||||
}
|
||||
RequestContext.get().setUserId(userDto.getId());
|
||||
RequestContext.get().setUser(userDto);
|
||||
RequestContext.get().setUserContext(UserDto.convertToUserContext(userDto));
|
||||
RequestContext.get().setLogin(true);
|
||||
RequestContext.get().setUserAccessKey(userAccessKeyDto);
|
||||
if (userAccessKeyDto.getTargetType() == UserAccessKeyDto.AKTargetType.Agent) {
|
||||
if (originalRequestUri.startsWith("/api/v1/") || originalRequestUri.startsWith("/api/file/")) {
|
||||
TenantConfigDto tenantConfigDto = (TenantConfigDto) RequestContext.get().getTenantConfig();
|
||||
if (tenantConfigDto.getAllowAgentApi() != null && tenantConfigDto.getAllowAgentApi().equals(YesOrNoEnum.N.getKey()) && userDto.getRole() != User.Role.Admin) {
|
||||
throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.apiKeyAgentApiDisabled);
|
||||
}
|
||||
AgentDetailDto agentDetailDto = agentApplicationService.queryAgentDetail(Long.parseLong(userAccessKeyDto.getTargetId()), false);
|
||||
RequestContext.get().setAkTarget(agentDetailDto);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (userAccessKeyDto.getTargetType() == UserAccessKeyDto.AKTargetType.OpenApi) {
|
||||
String path = request.getRequestURI();//path参考格式 /api/v1/user/add、/api/v1/user/1011(OpenApiDefinition的配置可能为/api/v1/user/:id,做匹配时要适配 :xx 为实际值)
|
||||
List<OpenApiDefinition> openApiDefinitions = userApiKeyApplicationService.queryOpenApiDefinitions(userDto.getId());
|
||||
OpenApiDefinition openApiDefinition = matchOpenApiDefinition(path, openApiDefinitions);// 从openApiDefinitions中过滤得到
|
||||
if (openApiDefinition == null) {
|
||||
throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.apiKeyOpenApiNotFound);
|
||||
}
|
||||
|
||||
request.setAttribute("currentAPI", openApiDefinition);
|
||||
request.setAttribute("userAccessKey", userAccessKeyDto);
|
||||
|
||||
//普通用户不能使用管理员接口
|
||||
if (openApiDefinition.getRole() == User.Role.Admin && userDto.getRole() != User.Role.Admin) {
|
||||
throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.permissionDenied);
|
||||
}
|
||||
|
||||
List<UserAccessKeyDto.ApiConfig> apiConfigs = userAccessKeyDto.getConfig().getApiConfigs();
|
||||
// apiConfigs如果为空则有全部接口权限
|
||||
if (CollectionUtils.isNotEmpty(apiConfigs) && apiConfigs.stream().noneMatch(apiConfig -> openApiDefinition.getKey().equals(apiConfig.getKey()))) {
|
||||
throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.permissionDenied);
|
||||
}
|
||||
userApiKeyApplicationService.requestLimitCheck(userDto.getId(), openApiDefinition);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 允许沙箱访问的接口范围
|
||||
if (userAccessKeyDto.getTargetType() == UserAccessKeyDto.AKTargetType.Sandbox && originalRequestUri.startsWith("/api/v1/4sandbox")) {
|
||||
return true;
|
||||
}
|
||||
return userAccessKeyDto.getTargetType() == UserAccessKeyDto.AKTargetType.Tenant;
|
||||
}
|
||||
|
||||
public OpenApiDefinition matchOpenApiDefinition(String path, List<OpenApiDefinition> definitions) {
|
||||
if (definitions == null || definitions.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (OpenApiDefinition def : definitions) {
|
||||
// 当前节点匹配
|
||||
if (def.getPath() != null && def.getKey().equals("api.v1.chat.file") && pathMatcher.matchStart(convertPathPattern(def.getPath()), path)) {
|
||||
return def;
|
||||
}
|
||||
if (def.getPath() != null && pathMatcher.match(convertPathPattern(def.getPath()), path)) {
|
||||
return def;
|
||||
}
|
||||
// 递归匹配子节点
|
||||
OpenApiDefinition matched = matchOpenApiDefinition(path, def.getApiList());
|
||||
if (matched != null) {
|
||||
return matched;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String convertPathPattern(String path) {
|
||||
if (path == null || !path.contains(":")) {
|
||||
return path;
|
||||
}
|
||||
return path.replaceAll(":([a-zA-Z_][a-zA-Z0-9_]*)", "{$1}");
|
||||
}
|
||||
|
||||
private String getOriginalRequestUri(HttpServletRequest request) {
|
||||
Object val = request.getAttribute(SandboxRequestAttributes.ORIGINAL_REQUEST_URI);
|
||||
if (val instanceof String s && !s.isEmpty()) {
|
||||
return s;
|
||||
}
|
||||
return request.getRequestURI();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package com.xspaceagi.interceptor;
|
||||
|
||||
import com.xspaceagi.pay.spec.exception.PayGatewayBizException;
|
||||
import com.xspaceagi.system.spec.common.RequestContext;
|
||||
import com.xspaceagi.system.spec.dto.ReqResult;
|
||||
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
|
||||
import com.xspaceagi.system.spec.exception.*;
|
||||
import jakarta.validation.UnexpectedTypeException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.web.HttpMediaTypeNotSupportedException;
|
||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.MissingServletRequestParameterException;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.servlet.resource.NoResourceFoundException;
|
||||
|
||||
@Slf4j
|
||||
@ControllerAdvice
|
||||
public class AppExceptionHandler {
|
||||
|
||||
/**
|
||||
* 处理异常消息,如果是业务异常将通过json的格式返回,否则根据模式来处理异常的显示.
|
||||
*/
|
||||
@ExceptionHandler
|
||||
@ResponseBody
|
||||
public ReqResult<?> processException(Exception ex) {
|
||||
ReqResult<?> responseData;
|
||||
if (ex instanceof PayGatewayBizException payGatewayBizException) {
|
||||
responseData = ReqResult.error(
|
||||
payGatewayBizException.getCode(),
|
||||
payGatewayBizException.getDisplayCode(),
|
||||
payGatewayBizException.getMessage());
|
||||
} else if (ex instanceof BizException bizException) {
|
||||
String message = resolveBizExceptionMessage(bizException);
|
||||
responseData = ReqResult.error(bizException.getCode(), message);
|
||||
} else {
|
||||
|
||||
if (ex instanceof SpacePermissionException) {
|
||||
log.warn("Exception[SpacePermissionException] ", ex);
|
||||
responseData = ReqResult.error(ErrorCodeEnum.PERMISSION_DENIED.getCode(),
|
||||
ErrorCodeEnum.PERMISSION_DENIED.getMsg());
|
||||
} else if (ex instanceof ResourcePermissionException) {
|
||||
responseData = ReqResult.error(ErrorCodeEnum.RESOURCE_PERMISSION_DENIED.getCode(),
|
||||
ErrorCodeEnum.RESOURCE_PERMISSION_DENIED.getMsg());
|
||||
} else if (ex instanceof HttpRequestMethodNotSupportedException) {
|
||||
log.warn("Exception[HttpRequestMethodNotSupportedException] ", ex);
|
||||
responseData = ReqResult.error(ErrorCodeEnum.METHOD_NOT_ALLOWED.getCode(), ex.getMessage());
|
||||
} else if (ex instanceof MissingServletRequestParameterException) {
|
||||
log.warn("Exception[MissingServletRequestParameterException] ", ex);
|
||||
responseData = ReqResult.error(ErrorCodeEnum.INVALID_PARAM.getCode(), ex.getMessage());
|
||||
} else if (ex instanceof HttpMessageNotReadableException) {
|
||||
log.warn("Exception[HttpMessageNotReadableException] ", ex);
|
||||
responseData = ReqResult.error(ErrorCodeEnum.INVALID_PARAM.getCode(), ex.getMessage());
|
||||
} else if (ex instanceof MethodArgumentNotValidException methodArgumentNotValidException) {
|
||||
log.warn("Exception[MethodArgumentNotValidException] ", ex);
|
||||
String msg = methodArgumentNotValidException.getBindingResult().getFieldError().getDefaultMessage();
|
||||
responseData = ReqResult.error(ErrorCodeEnum.INVALID_PARAM.getCode(), msg);
|
||||
} else if (ex instanceof UnexpectedTypeException) {
|
||||
log.warn("Exception[UnexpectedTypeException] ", ex);
|
||||
responseData = ReqResult.error(ErrorCodeEnum.INVALID_PARAM.getCode(), ex.getMessage());
|
||||
} else if (ex instanceof HttpMediaTypeNotSupportedException) {
|
||||
log.warn("Exception[HttpMediaTypeNotSupportedException] ", ex);
|
||||
responseData = ReqResult.error(ErrorCodeEnum.INVALID_PARAM.getCode(), ex.getMessage());
|
||||
} else if (ex instanceof KnowledgeException knowledgeException) {
|
||||
log.warn("Exception[KnowledgeException] ", ex);
|
||||
responseData = ReqResult.error(knowledgeException.getCode(), knowledgeException.getDisplayCode(),
|
||||
ex.getMessage());
|
||||
} else if (ex instanceof SystemManagerException systemManagerException) {
|
||||
log.warn("Exception[SystemManagerException] ", ex);
|
||||
responseData = ReqResult.error(systemManagerException.getCode(),
|
||||
systemManagerException.getDisplayCode(), ex.getMessage());
|
||||
} else if (ex instanceof ComposeException composeException) {
|
||||
responseData = ReqResult.error(composeException.getCode(), composeException.getDisplayCode(),
|
||||
composeException.getMessage());
|
||||
} else if (ex instanceof EcoMarketException ecoMarketException) {
|
||||
responseData = ReqResult.error(ecoMarketException.getCode(), ecoMarketException.getDisplayCode(),
|
||||
ecoMarketException.getMessage());
|
||||
} else if (ex instanceof IllegalArgumentException) {
|
||||
responseData = ReqResult.error(ErrorCodeEnum.INVALID_PARAM.getCode(), ex.getMessage());
|
||||
} else if (ex instanceof NoResourceFoundException) {
|
||||
responseData = ReqResult.error(ErrorCodeEnum.API_NOT_FOUND.getCode(), ex.getMessage());
|
||||
} else {
|
||||
log.error("System error ", ex);
|
||||
responseData = ReqResult.error(ErrorCodeEnum.SYS_ERROR.getCode(), "系统开小差啦,请稍后重试");
|
||||
}
|
||||
}
|
||||
return responseData;
|
||||
}
|
||||
|
||||
/**
|
||||
* BizException:按 {@link RequestContext#getLang()} 选择文案;{@code zh-CN}(忽略大小写)用中文模板,否则用英文模板;
|
||||
* 使用 {@link BizException#getMessageFormatArgs()} 与 {@link BizExceptionCodeEnum} 模板重新 {@link String#format}。
|
||||
* 无 {@code msgName}、无上下文、无 {@code lang} 或枚举名无法解析时回退 {@link Throwable#getMessage()}。
|
||||
*/
|
||||
private static String resolveBizExceptionMessage(BizException ex) {
|
||||
String fallback = ex.getMessage();
|
||||
String msgName = ex.getMsgName();
|
||||
if (msgName == null || msgName.isEmpty()) {
|
||||
return fallback;
|
||||
}
|
||||
RequestContext<?> ctx = RequestContext.get();
|
||||
if (ctx == null) {
|
||||
return fallback;
|
||||
}
|
||||
String lang = ctx.getLang();
|
||||
if (lang == null || lang.isBlank()) {
|
||||
return fallback;
|
||||
}
|
||||
try {
|
||||
BizExceptionCodeEnum codeEnum = BizExceptionCodeEnum.valueOf(msgName);
|
||||
boolean zhCn = "zh-CN".equalsIgnoreCase(lang.trim());
|
||||
String tpl = zhCn ? codeEnum.getMessageZh() : codeEnum.getMessageEn();
|
||||
Object[] args = ex.getMessageFormatArgs();
|
||||
if (args == null || args.length == 0) {
|
||||
return tpl;
|
||||
}
|
||||
return String.format(tpl, args);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
package com.xspaceagi.interceptor;
|
||||
|
||||
import com.xspaceagi.custompage.domain.model.CustomPageConfigModel;
|
||||
import com.xspaceagi.custompage.infra.service.ProxyConfigService;
|
||||
import com.xspaceagi.sandbox.SandboxRequestAttributes;
|
||||
import com.xspaceagi.system.application.dto.I18nLangDto;
|
||||
import com.xspaceagi.system.application.dto.TenantConfigDto;
|
||||
import com.xspaceagi.system.application.dto.UserDto;
|
||||
import com.xspaceagi.system.application.service.*;
|
||||
import com.xspaceagi.system.infra.dao.entity.User;
|
||||
import com.xspaceagi.system.infra.dao.entity.UserReq;
|
||||
import com.xspaceagi.system.spec.auth.UserAuthProperties;
|
||||
import com.xspaceagi.system.spec.common.RequestContext;
|
||||
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
|
||||
import com.xspaceagi.system.spec.enums.HttpStatusEnum;
|
||||
import com.xspaceagi.system.spec.enums.I18nSideEnum;
|
||||
import com.xspaceagi.system.spec.exception.BizException;
|
||||
import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
|
||||
import com.xspaceagi.system.spec.utils.IPUtil;
|
||||
import com.xspaceagi.system.spec.utils.JwtUtils;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.*;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
@Component
|
||||
public class AuthInterceptor implements HandlerInterceptor {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(AuthInterceptor.class);
|
||||
|
||||
@Value("${jwt.secretKey}")
|
||||
private String jwtSecretKey;
|
||||
|
||||
@Resource
|
||||
private UserAuthProperties userAuthProperties;
|
||||
|
||||
@Resource
|
||||
private AuthService authService;
|
||||
|
||||
// 页面开发代理,查询域名映射关系
|
||||
@Resource
|
||||
private ProxyConfigService proxyConfigService;
|
||||
|
||||
@Resource
|
||||
private TenantConfigApplicationService tenantConfigApplicationService;
|
||||
|
||||
@Resource
|
||||
private UserRequestApplicationService userRequestApplicationService;
|
||||
|
||||
@Resource
|
||||
private I18nApplicationService i18nApplicationService;
|
||||
|
||||
@Resource
|
||||
private I18nLangApplicationService i18nLangApplicationService;
|
||||
|
||||
@Value("${server.port:8080}")
|
||||
private Integer port;
|
||||
|
||||
@Value("${license:}")
|
||||
private String license;
|
||||
|
||||
/**
|
||||
* 请求处理完之后
|
||||
*/
|
||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object obj, Exception exc) throws Exception {
|
||||
RequestContext.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求处理完成
|
||||
*/
|
||||
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object obj, ModelAndView model) throws Exception {
|
||||
if (RequestContext.get() != null) {
|
||||
userRequestApplicationService.addUserRequest(
|
||||
UserReq.builder()
|
||||
.tenantId(RequestContext.get().getTenantId())
|
||||
.userId(RequestContext.get().getUserId() == null ? -1L : RequestContext.get().getUserId())
|
||||
.build()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求处理之前
|
||||
*/
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||
request.setCharacterEncoding("UTF-8");
|
||||
if (request.getMethod().equals("OPTIONS")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
//license check
|
||||
checkLicense();
|
||||
|
||||
// 优先使用 Sandbox Filter 记录的“原始 URI”
|
||||
String originalRequestUri = getOriginalRequestUri(request);
|
||||
|
||||
String domainName = request.getHeader("Host");
|
||||
if (domainName == null) {
|
||||
return false;
|
||||
}
|
||||
String referer = request.getHeader("Referer");
|
||||
Long tenantId = tenantConfigApplicationService.queryTenantIdByDomainName(domainName.split(":")[0]);
|
||||
// 获取页面应用绑定域名场景下的租户ID
|
||||
if (tenantId == null && request.getRequestURI().contains("/api/page/")) {
|
||||
CustomPageConfigModel configModel = proxyConfigService.queryCustomPageConfigByDomain(domainName.split(":")[0]);
|
||||
if (configModel != null) {
|
||||
tenantId = configModel.getTenantId();
|
||||
//构造referer,页面的工作流插件调用基于referer中的页面项目id和关联的智能体ID进行关系确认
|
||||
referer = "https://" + domainName + "/page/" + configModel.getId() + "-" + configModel.getDevAgentId() + "/prod";
|
||||
request.setAttribute("referer", referer);//后面使用
|
||||
}
|
||||
}
|
||||
if (tenantId == null) {
|
||||
// 对于无需鉴权的路径(如飞书/钉钉 webhook),使用默认租户,不要求登录
|
||||
if (isExcludedPath(originalRequestUri)) {
|
||||
tenantId = 1L;
|
||||
} else if (domainName.contains("qiming.com")) {
|
||||
throw BizException.of(ErrorCodeEnum.UNAUTHORIZED_REDIRECT, BizExceptionCodeEnum.bootstrapAuthRedirectUrl, "https://qiming.com");
|
||||
}
|
||||
String headerTenantId = request.getHeader("x-tenant-id");
|
||||
if (StringUtils.isNotBlank(headerTenantId)) {
|
||||
try {
|
||||
tenantId = Long.parseLong(headerTenantId);
|
||||
} catch (NumberFormatException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
if (tenantId == null) {
|
||||
tenantId = 1L;
|
||||
}
|
||||
}
|
||||
RequestContext<UserDto> requestContext = RequestContext.<UserDto>builder().tenantId(tenantId).build();
|
||||
RequestContext.set(requestContext);
|
||||
TenantConfigDto tenantConfig = tenantConfigApplicationService.getTenantConfig(requestContext.getTenantId());
|
||||
String fragment = request.getHeader("Fragment");
|
||||
if (StringUtils.isNotBlank(referer) && StringUtils.isNotBlank(fragment)) {
|
||||
referer = referer + "#" + fragment;
|
||||
}
|
||||
if (referer != null && !log.isDebugEnabled() && !referer.contains("https://servicewechat.com")) {
|
||||
try {
|
||||
URL url = new URL(referer);
|
||||
String baseUrl;
|
||||
if (url.getHost().equals("localhost") || url.getHost().equals("127.0.0.1")) {
|
||||
baseUrl = url.getProtocol() + "://" + url.getHost() + ":" + port;
|
||||
} else {
|
||||
baseUrl = url.getProtocol() + "://" + url.getHost() + (url.getPort() == 443 || url.getPort() == 80 || url.getPort() == -1 ? "" : ":" + url.getPort());
|
||||
}
|
||||
tenantConfig.setSiteConfigUrl(tenantConfig.getSiteUrl());
|
||||
tenantConfig.setSiteUrl(baseUrl);
|
||||
} catch (MalformedURLException e) {
|
||||
// ignore
|
||||
log.error("referer url error: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
tenantConfig.setTenantId(requestContext.getTenantId());
|
||||
requestContext.setTenantConfig(tenantConfig);
|
||||
|
||||
//获取终端语言
|
||||
requestContext.setClientLang("en-us");
|
||||
Locale locale = request.getLocale();
|
||||
requestContext.setClientLang(locale.toLanguageTag().toLowerCase());
|
||||
|
||||
String token = null;
|
||||
//优先使用cookie中的token
|
||||
Cookie[] cookies = request.getCookies();
|
||||
if (cookies != null) {
|
||||
for (Cookie cookie : cookies) {
|
||||
if (cookie.getName().equals("ticket")) {
|
||||
token = cookie.getValue();
|
||||
}
|
||||
if (cookie.getName().equals("lang")) {
|
||||
requestContext.setClientCookieLang(cookie.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
String authorization = request.getHeader("Authorization");
|
||||
if (token == null && authorization != null && authorization.length() > 35) {//避免前端传递不符合token的字符串
|
||||
token = authorization.replaceFirst("Basic", "").replaceFirst("Bearer", "").trim();
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(token)) {
|
||||
try {
|
||||
log.debug("jwt token: {}", token);
|
||||
UserDto userDto = authService.getLoginUserInfo(token);
|
||||
if (userDto != null && userDto.getTenantId().equals(tenantId)) {
|
||||
if (userDto.getStatus() == User.Status.Disabled || userDto.getStatus() == User.Status.Deleted) {
|
||||
authService.expireUserAllToken(userDto.getId());
|
||||
throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.systemAccountDisabled);
|
||||
}
|
||||
var userContent = UserDto.convertToUserContext(userDto);
|
||||
requestContext.setUserContext(userContent);
|
||||
requestContext.setUser(userDto);
|
||||
requestContext.setUserId(userDto.getId());
|
||||
requestContext.setTenantId(userDto.getTenantId());
|
||||
requestContext.setClientIp(IPUtil.getIpAddr(request));
|
||||
requestContext.setLogin(true);
|
||||
requestContext.setToken(token);
|
||||
requestContext.setLangMap(userDto.getLangMap());
|
||||
requestContext.setLang(userDto.getLang());
|
||||
log.debug("JWT token OK, userId: {} ", userDto.getId());
|
||||
if (originalRequestUri.startsWith("/api/system/")) {
|
||||
//判断是不是管理员
|
||||
if (userDto.getRole() != User.Role.Admin) {
|
||||
throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.permissionDenied);
|
||||
}
|
||||
}
|
||||
|
||||
String header = request.getHeader("X-Client-Type");
|
||||
if (header != null || token.startsWith("ticket")) {
|
||||
authService.renewToken(token);
|
||||
} else {
|
||||
int expire = (int) (tenantConfig.getAuthExpire() == null ? 86400 : tenantConfig.getAuthExpire() * 60);
|
||||
Claims claims = JwtUtils.parseJwt(token, jwtSecretKey);
|
||||
if (claims.getExpiration().getTime() < System.currentTimeMillis() + expire * 1000L * 0.8) {
|
||||
//refresh token
|
||||
String newToken = authService.refreshToken(token);
|
||||
Cookie cookie = new Cookie("ticket", newToken);
|
||||
cookie.setMaxAge(tenantConfig.getAuthExpire() == null ? 86400 * 30 : tenantConfig.getAuthExpire().intValue() * 60);
|
||||
cookie.setPath("/");
|
||||
response.addCookie(cookie);
|
||||
requestContext.setToken(newToken);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
log.debug("jwt token expired: {}", token);
|
||||
} catch (Exception e) {
|
||||
if (e instanceof BizException) {
|
||||
throw e;
|
||||
}
|
||||
log.warn("JWT token error {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
log.debug("jwt token is null, check uri: {}", originalRequestUri);
|
||||
try {
|
||||
checkAndThrowAuthException(originalRequestUri);
|
||||
} catch (Exception e) {
|
||||
if (StringUtils.isNotBlank(referer)) {
|
||||
throw BizException.of(ErrorCodeEnum.UNAUTHORIZED_REDIRECT, BizExceptionCodeEnum.bootstrapAuthRedirectUrl,
|
||||
"/login?redirect=" + URLEncoder.encode(referer, StandardCharsets.UTF_8));
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
if (requestContext.getLangMap() == null) {
|
||||
String lang = requestContext.getClientCookieLang();
|
||||
if (lang == null) {
|
||||
I18nLangDto aDefault = i18nLangApplicationService.getDefault(tenantId);
|
||||
if (aDefault != null) {
|
||||
lang = aDefault.getLang();
|
||||
} else {
|
||||
lang = requestContext.getClientLang();
|
||||
}
|
||||
}
|
||||
requestContext.setLang(lang);
|
||||
requestContext.setLangMap(i18nApplicationService.querySystemLangMap(tenantId, I18nSideEnum.Backend.getSide(), lang));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private String getOriginalRequestUri(HttpServletRequest request) {
|
||||
Object val = request.getAttribute(SandboxRequestAttributes.ORIGINAL_REQUEST_URI);
|
||||
if (val instanceof String s && !s.isEmpty()) {
|
||||
return s;
|
||||
}
|
||||
return request.getRequestURI();
|
||||
}
|
||||
|
||||
private void checkLicense() {
|
||||
if (StringUtils.isNotBlank(license)) {
|
||||
String expireDate;
|
||||
try {
|
||||
String hexKey = "a0189de032115b6b7031a8fb5194ed68b3c716d3e09c21592fe4187b4586048b"; // 256位
|
||||
String hexIV = "0ac97ac7d1081ebae42bfd987c5739eb";
|
||||
expireDate = AESCrypto.decryptWithHexKey(license, hexKey, hexIV);
|
||||
} catch (Exception e) {
|
||||
log.error("license error: {}", e.getMessage());
|
||||
throw BizException.of(ErrorCodeEnum.LICENSE_CONTENT_INVALID, BizExceptionCodeEnum.bootstrapLicenseContentInvalid);
|
||||
}
|
||||
if (parseToTimestamp(expireDate) * 1000L < System.currentTimeMillis()) {
|
||||
throw BizException.of(ErrorCodeEnum.UNAUTHORIZED_REDIRECT, BizExceptionCodeEnum.bootstrapAuthRedirectUrl, "/license-expired");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public static long parseToTimestamp(String expireDate) {
|
||||
if (expireDate == null || expireDate.isEmpty()) return -1;
|
||||
|
||||
try {
|
||||
// 标准化分隔符
|
||||
String normalized = expireDate
|
||||
.replace('/', '-')
|
||||
.replace('.', '-');
|
||||
|
||||
// 处理 8 位纯数字
|
||||
if (normalized.matches("\\d{8}")) {
|
||||
return LocalDate.parse(normalized, DateTimeFormatter.BASIC_ISO_DATE)
|
||||
.atStartOfDay(ZoneId.systemDefault()).toEpochSecond();
|
||||
}
|
||||
|
||||
// 处理带 T 和时区的 ISO 格式
|
||||
if (normalized.contains("T")) {
|
||||
if (normalized.endsWith("Z")) {
|
||||
return Instant.parse(normalized).getEpochSecond();
|
||||
}
|
||||
return OffsetDateTime.parse(normalized, DateTimeFormatter.ISO_OFFSET_DATE_TIME)
|
||||
.toInstant().getEpochSecond();
|
||||
}
|
||||
|
||||
// 处理日期 + 时间(空格分隔)
|
||||
if (normalized.contains(" ")) {
|
||||
return LocalDateTime.parse(normalized,
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
|
||||
.atZone(ZoneId.systemDefault()).toEpochSecond();
|
||||
}
|
||||
|
||||
// 仅日期
|
||||
return LocalDate.parse(normalized, DateTimeFormatter.ISO_LOCAL_DATE)
|
||||
.atStartOfDay(ZoneId.systemDefault()).toEpochSecond();
|
||||
|
||||
} catch (Exception e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isExcludedPath(String uri) {
|
||||
List<String> excludePath = userAuthProperties.getExcludePath();
|
||||
for (String ignoreLoginUri : excludePath) {
|
||||
if (uri.startsWith(ignoreLoginUri)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void checkAndThrowAuthException(String uri) {
|
||||
if (isExcludedPath(uri)) {
|
||||
return;
|
||||
}
|
||||
throw BizException.of(HttpStatusEnum.UNAUTHORIZED, ErrorCodeEnum.UNAUTHORIZED,
|
||||
BizExceptionCodeEnum.systemUnauthorizedOrSessionExpired);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
package com.xspaceagi.interceptor;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONWriter;
|
||||
import com.xspaceagi.log.sdk.service.ILogRpcService;
|
||||
import com.xspaceagi.log.sdk.vo.LogDocument;
|
||||
import com.xspaceagi.system.application.dto.UserDto;
|
||||
import com.xspaceagi.system.infra.dao.entity.OpenApiDefinition;
|
||||
import com.xspaceagi.system.sdk.service.dto.UserAccessKeyDto;
|
||||
import com.xspaceagi.system.spec.common.RequestContext;
|
||||
import com.xspaceagi.system.spec.dto.ReqResult;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.MDC;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.http.HttpInputMessage;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.http.server.ServletServerHttpRequest;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Type;
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 请求参数记录
|
||||
*
|
||||
* @apiNote http前端后请求参数记录
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@ControllerAdvice
|
||||
public class HttpInterceptor implements HandlerInterceptor, ResponseBodyAdvice, RequestBodyAdvice {
|
||||
|
||||
@Value("${access.control.allow-origin}")
|
||||
private String accessControlAllowOrigin;
|
||||
|
||||
@Autowired
|
||||
private jakarta.servlet.http.HttpServletRequest request;
|
||||
|
||||
@Resource
|
||||
private ILogRpcService iLogRpcService;
|
||||
|
||||
@Override
|
||||
public boolean preHandle(jakarta.servlet.http.HttpServletRequest request,
|
||||
jakarta.servlet.http.HttpServletResponse response, Object handler) {
|
||||
String origin = request.getHeader("Origin");
|
||||
// 如果配置是 "*" 或者 origin 包含配置的值,则允许跨域
|
||||
boolean allowOrigin = StringUtils.isNotBlank(origin) &&
|
||||
("*".equals(accessControlAllowOrigin) || origin.contains(accessControlAllowOrigin));
|
||||
if (allowOrigin) {
|
||||
response.setHeader("Vary", "Origin");
|
||||
response.setHeader("Access-Control-Allow-Origin", origin);
|
||||
response.setHeader("Access-Control-Allow-Methods", "HEAD,GET,POST,PUT,DELETE,OPTIONS");
|
||||
response.setHeader("Access-Control-Allow-Headers",
|
||||
"Origin, X-Requested-With, Content-Type, Accept, Authorization, Cache-Control, Fragment");
|
||||
response.setHeader("Access-Control-Allow-Credentials", "true");
|
||||
}
|
||||
if (request.getMethod().equals("OPTIONS")) {
|
||||
return true;
|
||||
}
|
||||
String uri = request.getRequestURI();
|
||||
if (!(handler instanceof HandlerMethod)) {
|
||||
return true;
|
||||
}
|
||||
long reqBeginTime = Instant.now().toEpochMilli();
|
||||
// 请求开始时间
|
||||
request.setAttribute("reqBeginTime", reqBeginTime);
|
||||
HandlerMethod handlerMethod = (HandlerMethod) handler;
|
||||
Method method = handlerMethod.getMethod();
|
||||
request.setAttribute("methodName", method.getDeclaringClass().getName() + "." + method.getName());
|
||||
log.info("Request URI {}, X-Client-Type {}", uri, request.getHeader("X-Client-Type"));
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(MethodParameter returnType, Class converterType) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
|
||||
Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
|
||||
if (!(body instanceof ReqResult httpResult)) {
|
||||
return body;
|
||||
}
|
||||
ServletServerHttpRequest servletServerHttpRequest = (ServletServerHttpRequest) request;
|
||||
jakarta.servlet.http.HttpServletRequest httpServletRequest = servletServerHttpRequest.getServletRequest();
|
||||
Map<String, String[]> params = httpServletRequest.getParameterMap();
|
||||
Map<String, Object> req = new HashMap<>();
|
||||
req.put("parameter", params);
|
||||
Object reqBody = this.request.getAttribute("requestBody");
|
||||
if (null != reqBody) {
|
||||
req.put("body", reqBody);
|
||||
}
|
||||
RequestContext<Object> objectRequestContext = RequestContext.get();
|
||||
if (null != objectRequestContext && objectRequestContext.getUser() != null && objectRequestContext.getUser() instanceof UserDto userDto) {
|
||||
Map<String, Object> user = new HashMap<>();
|
||||
user.put("tenantId", userDto.getTenantId());
|
||||
user.put("userId", userDto.getId());
|
||||
user.put("userName", userDto.getUserName() == null ? userDto.getNickName() : userDto.getUserName());
|
||||
req.put("user", user);
|
||||
}
|
||||
long reqBeginTime = (long) httpServletRequest.getAttribute("reqBeginTime");
|
||||
String methodName = (String) httpServletRequest.getAttribute("methodName");
|
||||
|
||||
OpenApiDefinition openApiDefinition = (OpenApiDefinition) httpServletRequest.getAttribute("currentAPI");
|
||||
UserAccessKeyDto userAccessKey = (UserAccessKeyDto) httpServletRequest.getAttribute("userAccessKey");
|
||||
try {
|
||||
if (log.isInfoEnabled() || openApiDefinition != null) {
|
||||
// 启用 LargeObject 特性,支持序列化包含大文件内容的请求和响应体
|
||||
String requestBodyStr = JSON.toJSONString(req, JSONWriter.Feature.LargeObject);
|
||||
String responseBody = JSON.toJSONString(body, JSONWriter.Feature.LargeObject);
|
||||
if (openApiDefinition != null) {
|
||||
requestBodyStr = httpServletRequest.getMethod() + " " + httpServletRequest.getRequestURI() + "\n" + requestBodyStr;
|
||||
}
|
||||
buildAndSendLogDocument(openApiDefinition, userAccessKey, requestBodyStr, responseBody, reqBeginTime, httpResult);
|
||||
log.info("HTTP API call log\nService {}\nRequest {}\nResponse {}\nElapsed {}ms", methodName, requestBodyStr.length() > 1024 * 8 ? requestBodyStr.substring(0, 1024 * 8) : requestBodyStr, responseBody.length() > 1024 * 8 ? responseBody.substring(0, 1024 * 8) : responseBody
|
||||
, System.currentTimeMillis() - reqBeginTime);
|
||||
}
|
||||
httpResult.setTid(MDC.get("tid"));
|
||||
return httpResult;
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to record HTTP access log", e);
|
||||
// 使用 LargeObject 特性序列化请求参数,避免大文件内容导致序列化失败
|
||||
String paramsStr = JSON.toJSONString(params, JSONWriter.Feature.LargeObject);
|
||||
log.error("HTTP API call log\nService {}\nRequest {}\nError {}\nElapsed {}ms", methodName, paramsStr.length() > 1024 * 8 ? paramsStr.substring(0, 1024 * 8) : paramsStr,
|
||||
e.getMessage(), System.currentTimeMillis() - reqBeginTime);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private void buildAndSendLogDocument(OpenApiDefinition openApiDefinition, UserAccessKeyDto userAccessKey, String requestBodyStr, String responseBody, Long reqBeginTime, ReqResult httpResult) {
|
||||
if (openApiDefinition == null) {
|
||||
return;
|
||||
}
|
||||
UserDto user = (UserDto) RequestContext.get().getUser();
|
||||
LogDocument logDocument = LogDocument.builder()
|
||||
.id(UUID.randomUUID().toString().replace("-", ""))
|
||||
.tenantId(RequestContext.get().getTenantId())
|
||||
.spaceId(-1L)
|
||||
.userId(RequestContext.get().getUserId())
|
||||
.userName(user.getUserName())
|
||||
.targetType("ApiKey")
|
||||
.targetName(userAccessKey.getName())
|
||||
.targetId(userAccessKey.getId().toString())
|
||||
.input(requestBodyStr)
|
||||
.output(responseBody)
|
||||
.requestStartTime(reqBeginTime)
|
||||
.requestEndTime(System.currentTimeMillis())
|
||||
.resultCode(httpResult.getCode())
|
||||
.resultMsg(httpResult.getMessage())
|
||||
.conversationId(openApiDefinition.getPath())
|
||||
.requestId(MDC.get("tid"))
|
||||
.createTime(System.currentTimeMillis())
|
||||
.from("ApiKey")
|
||||
.build();
|
||||
iLogRpcService.bulkIndex(List.of(logDocument));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(MethodParameter methodParameter, Type targetType,
|
||||
Class<? extends HttpMessageConverter<?>> converterType) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
|
||||
Class<? extends HttpMessageConverter<?>> converterType) {
|
||||
return inputMessage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
|
||||
Class<? extends HttpMessageConverter<?>> converterType) {
|
||||
request.setAttribute("requestBody", body);
|
||||
return body;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
|
||||
Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
|
||||
return body;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
|
||||
throws Exception {
|
||||
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.xspaceagi.interceptor;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.slf4j.MDC;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.time.Instant;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
/**
|
||||
* tid ,获取拦截器
|
||||
*
|
||||
* @author soddy
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class TIdInterceptor implements HandlerInterceptor {
|
||||
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
|
||||
throws Exception {
|
||||
Integer reqId = ThreadLocalRandom.current().nextInt(100, 999999);
|
||||
MDC.put("tid", reqId + "" + Instant.now().toEpochMilli());
|
||||
|
||||
// 请求开始时间
|
||||
request.setAttribute("reqBeginTime", Instant.now().toEpochMilli());
|
||||
if (handler instanceof HandlerMethod) {
|
||||
HandlerMethod handlerMethod = (HandlerMethod) handler;
|
||||
Method method = handlerMethod.getMethod();
|
||||
request.setAttribute("methodName", method.getDeclaringClass().getName() + "." + method.getName());
|
||||
} else {
|
||||
request.setAttribute("methodName", "静态资源未识别的方法");
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.xspaceagi.sandbox;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Sandbox API URL 重写配置(全局生效)
|
||||
*/
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "sandbox.api.rewrite")
|
||||
@Getter
|
||||
@Setter
|
||||
public class SandboxApiRewriteProperties {
|
||||
|
||||
/**
|
||||
* 允许重写的路径模式(相对于 /api/v1/4sandbox 之后的部分匹配)。
|
||||
*/
|
||||
private List<String> allowPath = new ArrayList<>();
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
spring:
|
||||
mvc:
|
||||
pathmatch:
|
||||
matching-strategy: ANT_PATH_MATCHER
|
||||
|
||||
servlet:
|
||||
multipart:
|
||||
max-file-size: 100MB
|
||||
max-request-size: 100MB
|
||||
|
||||
# 使用 dynamic-datasource-spring-boot-starter 配置
|
||||
datasource:
|
||||
# 动态SQL生成器配置
|
||||
sql-generator:
|
||||
# 指定使用的数据库类型:mysql 或 doris
|
||||
type: mysql
|
||||
dynamic:
|
||||
# 设置默认的数据源或者数据源组, 默认值即为 master
|
||||
primary: master
|
||||
# 设置严格模式,默认 false. 严格模式下未匹配到数据源直接报错, 非严格模式下则使用默认数据源 primary 配置的数据源
|
||||
strict: false
|
||||
# p6spy 开关, 默认 false
|
||||
p6spy: false
|
||||
# seata 开关, 默认 false
|
||||
seata: false
|
||||
# 数据源配置信息
|
||||
datasource:
|
||||
# 主数据源 (MySQL)
|
||||
master:
|
||||
url: jdbc:mysql://${DB_HOST:localhost}:3306/${DB_NAME:agent_platform}?serverTimezone=Asia/Shanghai&characterEncoding=utf-8&allowMultiQueries=true&socketTimeout=300000&useSSL=false
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
username: ${DB_USERNAME:root}
|
||||
password: ${DB_PASSWORD:your_mysql_password}
|
||||
# 以下为 Druid 连接池配置 (如果使用)
|
||||
druid:
|
||||
initial-size: 5
|
||||
min-idle: 5
|
||||
max-active: 20
|
||||
max-wait: 60000
|
||||
time-between-eviction-runs-millis: 60000
|
||||
min-evictable-idle-time-millis: 300000
|
||||
validation-query: SELECT 1 FROM DUAL
|
||||
test-while-idle: true
|
||||
test-on-borrow: false
|
||||
test-on-return: false
|
||||
pool-prepared-statements: true
|
||||
max-pool-prepared-statement-per-connection-size: 20
|
||||
filters: stat # 启用 stat filter 用于监控
|
||||
# Doris 数据源
|
||||
doris:
|
||||
url: jdbc:mysql://${DORIS_HOST:localhost}:3306/${DORIS_DB_NAME:agent_custom_table}?serverTimezone=Asia/Shanghai&characterEncoding=utf-8&allowMultiQueries=true&socketTimeout=300000&useSSL=false
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
username: ${DORIS_USERNAME:root}
|
||||
password: ${DORIS_PASSWORD:your_doris_password}
|
||||
# Doris 表配置
|
||||
table:
|
||||
replication-num: 1 # 副本数,生产环境通常为3,开发环境可以为1
|
||||
bucket-num: 10 # 分桶数
|
||||
duplicate-key: id # 重复键字段名,默认使用第一个字段
|
||||
distributed-key: id # 分布键字段名,默认使用第一个字段
|
||||
# 可选: 如果 Doris 也用 Druid,可以添加 druid 配置
|
||||
druid:
|
||||
initial-size: 2
|
||||
min-idle: 2
|
||||
max-active: 10
|
||||
max-wait: 60000
|
||||
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:localhost}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: ${REDIS_PASSWORD:your_redis_password}
|
||||
database: ${REDIS_DB:1}
|
||||
lettuce:
|
||||
pool:
|
||||
max-idle: 16
|
||||
max-active: 32
|
||||
min-idle: 8
|
||||
|
||||
main:
|
||||
allow-circular-references: true
|
||||
log:
|
||||
level: DEBUG
|
||||
|
||||
server:
|
||||
port: 8081
|
||||
|
||||
jwt:
|
||||
secretKey: ${JWT_SECRET_KEY:your_jwt_secret_key_minimum_32_characters}
|
||||
|
||||
file:
|
||||
uploadFolder: ${FILE_UPLOAD_FOLDER:/tmp/uploads}
|
||||
baseUrl: ${FILE_BASE_URL:https://your-file-domain.com/files/}
|
||||
|
||||
# Storage configuration
|
||||
storage:
|
||||
type: ${STORAGE_TYPE:local} # Storage type: local, cos(Tencent Cloud), oss(Alibaba Cloud), s3(S3 protocol/MinIO)
|
||||
|
||||
local:
|
||||
upload:
|
||||
folder: ${LOCAL_UPLOAD_FOLDER:/tmp/uploads}
|
||||
|
||||
# Tencent Cloud COS configuration (takes effect when storage.type=cos)
|
||||
cos:
|
||||
secret-id: ${COS_SECRET_ID:}
|
||||
secret-key: ${COS_SECRET_KEY:}
|
||||
region: ${COS_REGION:ap-chengdu}
|
||||
bucket-name: ${COS_BUCKET_NAME:}
|
||||
endpoint: ${COS_ENDPOINT:}
|
||||
# Legacy configuration below
|
||||
secretId: ${COS_SECRET_ID:}
|
||||
secretKey: ${COS_SECRET_KEY:}
|
||||
baseUrl: ${COS_BASE_URL:}
|
||||
|
||||
# Alibaba Cloud OSS configuration (takes effect when storage.type=oss)
|
||||
oss:
|
||||
endpoint: ${OSS_ENDPOINT:}
|
||||
access-key-id: ${OSS_ACCESS_KEY_ID:}
|
||||
access-key-secret: ${OSS_ACCESS_KEY_SECRET:}
|
||||
region-name: ${OSS_REGION_NAME:cn-chengdu}
|
||||
bucket-name: ${OSS_BUCKET_NAME:}
|
||||
|
||||
# S3 protocol configuration (takes effect when storage.type=s3, supports MinIO etc.)
|
||||
s3:
|
||||
endpoint: ${S3_ENDPOINT:}
|
||||
access-key: ${S3_ACCESS_KEY:}
|
||||
secret-key: ${S3_SECRET_KEY:}
|
||||
bucket-name: ${S3_BUCKET_NAME:}
|
||||
region: ${S3_REGION:us-east-1}
|
||||
|
||||
vector-store:
|
||||
engine: milvus
|
||||
|
||||
milvus:
|
||||
uri: ${MILVUS_URI:http://localhost:19530}
|
||||
user: ${MILVUS_USER:root}
|
||||
password: ${MILVUS_PASSWORD:your_milvus_password}
|
||||
|
||||
code:
|
||||
execute:
|
||||
url: ${CODE_EXECUTE_URL:http://localhost:8020/api/run_code_with_log}
|
||||
|
||||
# 日志模块配置
|
||||
log-module:
|
||||
log:
|
||||
rust-service:
|
||||
# Rust 日志服务的基础 URL
|
||||
base-url: ${LOG_SERVICE_URL:http://localhost:8097}
|
||||
# 连接超时时间(秒)
|
||||
connect-timeout-seconds: 10
|
||||
# 写入超时时间(秒)
|
||||
write-timeout-seconds: 30
|
||||
# 读取超时时间(秒)
|
||||
read-timeout-seconds: 30
|
||||
# 是否启用连接池
|
||||
enable-connection-pool: true
|
||||
# 最大空闲连接数
|
||||
max-idle-connections: 5
|
||||
# 连接保持存活时间(分钟)
|
||||
keep-alive-duration-minutes: 5
|
||||
|
||||
search:
|
||||
elasticsearch:
|
||||
url: ${ES_URL:http://localhost:9200}
|
||||
username: ${ES_USERNAME:elastic}
|
||||
password: ${ES_PASSWORD:your_elasticsearch_password}
|
||||
api_key: ${ES_API_KEY:}
|
||||
# 生态市场模块配置
|
||||
eco-market:
|
||||
# 服务端配置
|
||||
server:
|
||||
# 远程server端的 ip端口配置
|
||||
base-url: ${ECO_MARKET_SERVER_URL:https://test-agent-market-api.xspaceagi.com}
|
||||
client:
|
||||
# 客户端配置
|
||||
retry:
|
||||
#自动注册客户端到 server端, 重试间隔时间,单位:秒;
|
||||
interval: 300
|
||||
|
||||
mcp:
|
||||
proxy-base-url: ${MCP_PROXY_URL:http://localhost:8020}
|
||||
|
||||
knife4j:
|
||||
# 开启增强配置
|
||||
enable: true
|
||||
# 标识是否生产环境:true-生产环境关闭文档,false-显示文档
|
||||
production: false
|
||||
|
||||
custom-page:
|
||||
http-proxy:
|
||||
host: ${CUSTOM_PAGE_PROXY_HOST:0.0.0.0}
|
||||
port: ${CUSTOM_PAGE_PROXY_PORT:18082}
|
||||
dev-server-host: ${DEV_SERVER_HOST:http://localhost}
|
||||
prod-server-host: ${PROD_SERVER_HOST:http://localhost:8099}
|
||||
build-server:
|
||||
base-url: ${BUILD_SERVER_URL:http://localhost:60000/api}
|
||||
ai-agent:
|
||||
base-url: ${AI_AGENT_URL:http://localhost:8086}
|
||||
docker-proxy:
|
||||
enable: ${DOCKER_PROXY_ENABLE:true}
|
||||
base-url: ${DOCKER_PROXY_URL:http://localhost:8088}
|
||||
|
||||
computer:
|
||||
proxy:
|
||||
host: ${COMPUTER_PROXY_HOST:0.0.0.0}
|
||||
port: ${COMPUTER_PROXY_PORT:18085}
|
||||
|
||||
supportCustomDomain: true
|
||||
|
||||
# 内外穿透相关服务配置
|
||||
reverse:
|
||||
server:
|
||||
# 内部服务器配置
|
||||
inner:
|
||||
# 内部服务可访问地址,如果分布式部署,可以在每台主机上通过环境变量制定 SERVICE_HOST
|
||||
service-host: ${SERVICE_HOST:127.0.0.1}
|
||||
# 绑定主机地址,如果分布式部署,可以在每台主机上通过环境变量制定 BIND_HOST
|
||||
bind-host: ${BIND_HOST:0.0.0.0}
|
||||
# 内部服务器端口范围
|
||||
ports: ${REVERSE_PORTS:30000-40000}
|
||||
# 外部服务器配置
|
||||
outer:
|
||||
# 外部用于与客户端保持连接的服务器主机地址,多个用英文逗号隔开,例如 192.168.1.12,192.168.1.13,留空时使用平台服务器地址
|
||||
host: ${OUTER_HOST:}
|
||||
# 外部服务器端口
|
||||
port: ${OUTER_PORT:10076}
|
||||
|
||||
model-api-proxy:
|
||||
base-api-url: ${MODEL_API_BASE_URL:}
|
||||
enable-model-proxy: ${MODEL_PROXY_ENABLE:true}
|
||||
port: ${MODEL_PROXY_PORT:18086}
|
||||
#记录日志
|
||||
save-log: ${MODEL_PROXY_SAVE_LOG:true}
|
||||
@@ -0,0 +1,232 @@
|
||||
spring:
|
||||
mvc:
|
||||
pathmatch:
|
||||
matching-strategy: ANT_PATH_MATCHER
|
||||
|
||||
servlet:
|
||||
multipart:
|
||||
max-file-size: 100MB
|
||||
max-request-size: 100MB
|
||||
|
||||
# 使用 dynamic-datasource-spring-boot-starter 配置
|
||||
datasource:
|
||||
# 动态SQL生成器配置
|
||||
sql-generator:
|
||||
# 指定使用的数据库类型:mysql 或 doris
|
||||
type: mysql
|
||||
dynamic:
|
||||
# 设置默认的数据源或者数据源组, 默认值即为 master
|
||||
primary: master
|
||||
# 设置严格模式,默认 false. 严格模式下未匹配到数据源直接报错, 非严格模式下则使用默认数据源 primary 配置的数据源
|
||||
strict: false
|
||||
# p6spy 开关, 默认 false
|
||||
p6spy: false
|
||||
# seata 开关, 默认 false
|
||||
seata: false
|
||||
# 数据源配置信息
|
||||
datasource:
|
||||
# 主数据源 (MySQL)
|
||||
master:
|
||||
url: jdbc:mysql://${DB_HOST:localhost}:3306/${DB_NAME:agent_platform}?serverTimezone=Asia/Shanghai&characterEncoding=utf-8&allowMultiQueries=true&socketTimeout=300000&useSSL=false
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
username: ${DB_USERNAME:root}
|
||||
password: ${DB_PASSWORD:your_mysql_password}
|
||||
# 以下为 Druid 连接池配置 (如果使用)
|
||||
druid:
|
||||
initial-size: 5
|
||||
min-idle: 5
|
||||
max-active: 20
|
||||
max-wait: 60000
|
||||
time-between-eviction-runs-millis: 60000
|
||||
min-evictable-idle-time-millis: 300000
|
||||
validation-query: SELECT 1 FROM DUAL
|
||||
test-while-idle: true
|
||||
test-on-borrow: false
|
||||
test-on-return: false
|
||||
pool-prepared-statements: true
|
||||
max-pool-prepared-statement-per-connection-size: 20
|
||||
filters: stat # 启用 stat filter 用于监控
|
||||
# Doris 数据源
|
||||
doris:
|
||||
url: jdbc:mysql://${DORIS_HOST:localhost}:3306/${DORIS_DB_NAME:agent_custom_table}?serverTimezone=Asia/Shanghai&characterEncoding=utf-8&allowMultiQueries=true&socketTimeout=300000&useSSL=false
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
username: ${DORIS_USERNAME:root}
|
||||
password: ${DORIS_PASSWORD:your_doris_password}
|
||||
# Doris 表配置
|
||||
table:
|
||||
replication-num: 1 # 副本数,生产环境通常为3,开发环境可以为1
|
||||
bucket-num: 10 # 分桶数
|
||||
duplicate-key: id # 重复键字段名,默认使用第一个字段
|
||||
distributed-key: id # 分布键字段名,默认使用第一个字段
|
||||
# 可选: 如果 Doris 也用 Druid,可以添加 druid 配置
|
||||
druid:
|
||||
initial-size: 2
|
||||
min-idle: 2
|
||||
max-active: 10
|
||||
max-wait: 60000
|
||||
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:localhost}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: ${REDIS_PASSWORD:your_redis_password}
|
||||
database: ${REDIS_DB:1}
|
||||
lettuce:
|
||||
pool:
|
||||
max-idle: 16
|
||||
max-active: 32
|
||||
min-idle: 8
|
||||
|
||||
main:
|
||||
allow-circular-references: true
|
||||
log:
|
||||
level: DEBUG
|
||||
|
||||
server:
|
||||
port: 8081
|
||||
|
||||
jwt:
|
||||
secretKey: ${JWT_SECRET_KEY:your_jwt_secret_key_minimum_32_characters}
|
||||
|
||||
file:
|
||||
uploadFolder: ${FILE_UPLOAD_FOLDER:/tmp/uploads}
|
||||
baseUrl: ${FILE_BASE_URL:https://your-file-domain.com/files/}
|
||||
|
||||
# Storage configuration
|
||||
storage:
|
||||
type: ${STORAGE_TYPE:local} # Storage type: local, cos(Tencent Cloud), oss(Alibaba Cloud), s3(S3 protocol/MinIO)
|
||||
|
||||
local:
|
||||
upload:
|
||||
folder: ${LOCAL_UPLOAD_FOLDER:/tmp/uploads}
|
||||
|
||||
# Tencent Cloud COS configuration (takes effect when storage.type=cos)
|
||||
cos:
|
||||
secret-id: ${COS_SECRET_ID:}
|
||||
secret-key: ${COS_SECRET_KEY:}
|
||||
region: ${COS_REGION:ap-chengdu}
|
||||
bucket-name: ${COS_BUCKET_NAME:}
|
||||
endpoint: ${COS_ENDPOINT:}
|
||||
# Legacy configuration below
|
||||
secretId: ${COS_SECRET_ID:}
|
||||
secretKey: ${COS_SECRET_KEY:}
|
||||
baseUrl: ${COS_BASE_URL:}
|
||||
|
||||
# Alibaba Cloud OSS configuration (takes effect when storage.type=oss)
|
||||
oss:
|
||||
endpoint: ${OSS_ENDPOINT:}
|
||||
access-key-id: ${OSS_ACCESS_KEY_ID:}
|
||||
access-key-secret: ${OSS_ACCESS_KEY_SECRET:}
|
||||
region-name: ${OSS_REGION_NAME:cn-chengdu}
|
||||
bucket-name: ${OSS_BUCKET_NAME:}
|
||||
|
||||
# S3 protocol configuration (takes effect when storage.type=s3, supports MinIO etc.)
|
||||
s3:
|
||||
endpoint: ${S3_ENDPOINT:}
|
||||
access-key: ${S3_ACCESS_KEY:}
|
||||
secret-key: ${S3_SECRET_KEY:}
|
||||
bucket-name: ${S3_BUCKET_NAME:}
|
||||
region: ${S3_REGION:us-east-1}
|
||||
vector-store:
|
||||
engine: milvus
|
||||
|
||||
milvus:
|
||||
uri: ${MILVUS_URI:http://localhost:19530}
|
||||
user: ${MILVUS_USER:root}
|
||||
password: ${MILVUS_PASSWORD:your_milvus_password}
|
||||
|
||||
code:
|
||||
execute:
|
||||
url: ${CODE_EXECUTE_URL:http://localhost:8020/api/run_code_with_log}
|
||||
|
||||
# 日志模块配置
|
||||
log-module:
|
||||
log:
|
||||
rust-service:
|
||||
# Rust 日志服务的基础 URL
|
||||
base-url: ${LOG_SERVICE_URL:http://localhost:8097}
|
||||
# 连接超时时间(秒)
|
||||
connect-timeout-seconds: 10
|
||||
# 写入超时时间(秒)
|
||||
write-timeout-seconds: 30
|
||||
# 读取超时时间(秒)
|
||||
read-timeout-seconds: 30
|
||||
# 是否启用连接池
|
||||
enable-connection-pool: true
|
||||
# 最大空闲连接数
|
||||
max-idle-connections: 5
|
||||
# 连接保持存活时间(分钟)
|
||||
keep-alive-duration-minutes: 5
|
||||
|
||||
search:
|
||||
elasticsearch:
|
||||
url: ${ES_URL:http://localhost:9200}
|
||||
username: ${ES_USERNAME:elastic}
|
||||
password: ${ES_PASSWORD:your_elasticsearch_password}
|
||||
api_key: ${ES_API_KEY:}
|
||||
# 生态市场模块配置
|
||||
eco-market:
|
||||
# 服务端配置
|
||||
server:
|
||||
# 远程server端的 ip端口配置
|
||||
base-url: ${ECO_MARKET_SERVER_URL:https://agent-market-api.xspaceagi.com}
|
||||
client:
|
||||
# 客户端配置
|
||||
retry:
|
||||
#自动注册客户端到 server端, 重试间隔时间,单位:秒;
|
||||
interval: 300
|
||||
|
||||
mcp:
|
||||
proxy-base-url: ${MCP_PROXY_URL:http://localhost:8020}
|
||||
|
||||
knife4j:
|
||||
# 开启增强配置
|
||||
enable: true
|
||||
# 标识是否生产环境:true-生产环境关闭文档,false-显示文档
|
||||
production: false
|
||||
|
||||
custom-page:
|
||||
http-proxy:
|
||||
host: ${CUSTOM_PAGE_PROXY_HOST:0.0.0.0}
|
||||
port: ${CUSTOM_PAGE_PROXY_PORT:18082}
|
||||
dev-server-host: ${DEV_SERVER_HOST:http://localhost}
|
||||
prod-server-host: ${PROD_SERVER_HOST:http://localhost:8099}
|
||||
build-server:
|
||||
base-url: ${BUILD_SERVER_URL:http://localhost:60000/api}
|
||||
ai-agent:
|
||||
base-url: ${AI_AGENT_URL:http://localhost:8086}
|
||||
docker-proxy:
|
||||
enable: ${DOCKER_PROXY_ENABLE:true}
|
||||
base-url: ${DOCKER_PROXY_URL:http://localhost:8088}
|
||||
|
||||
computer:
|
||||
proxy:
|
||||
host: ${COMPUTER_PROXY_HOST:0.0.0.0}
|
||||
port: ${COMPUTER_PROXY_PORT:18085}
|
||||
|
||||
supportCustomDomain: true
|
||||
|
||||
# 内外穿透相关服务配置
|
||||
reverse:
|
||||
server:
|
||||
# 内部服务器配置
|
||||
inner:
|
||||
# 内部服务可访问地址,如果分布式部署,可以在每台主机上通过环境变量制定 SERVICE_HOST
|
||||
service-host: ${SERVICE_HOST:127.0.0.1}
|
||||
# 绑定主机地址,如果分布式部署,可以在每台主机上通过环境变量制定 BIND_HOST
|
||||
bind-host: ${BIND_HOST:0.0.0.0}
|
||||
# 内部服务器端口范围
|
||||
ports: ${REVERSE_PORTS:30000-40000}
|
||||
# 外部服务器配置
|
||||
outer:
|
||||
# 外部用于与客户端保持连接的服务器主机地址,多个用英文逗号隔开,例如 192.168.1.12,192.168.1.13,留空时使用平台服务器地址
|
||||
host: ${OUTER_HOST:}
|
||||
# 外部服务器端口
|
||||
port: ${OUTER_PORT:10076}
|
||||
|
||||
model-api-proxy:
|
||||
base-api-url: ${MODEL_API_BASE_URL:}
|
||||
enable-model-proxy: ${MODEL_PROXY_ENABLE:true}
|
||||
port: ${MODEL_PROXY_PORT:18086}
|
||||
#记录日志
|
||||
save-log: ${MODEL_PROXY_SAVE_LOG:true}
|
||||
@@ -0,0 +1,232 @@
|
||||
spring:
|
||||
mvc:
|
||||
pathmatch:
|
||||
matching-strategy: ANT_PATH_MATCHER
|
||||
|
||||
servlet:
|
||||
multipart:
|
||||
max-file-size: 100MB
|
||||
max-request-size: 100MB
|
||||
|
||||
# 使用 dynamic-datasource-spring-boot-starter 配置
|
||||
datasource:
|
||||
# 动态SQL生成器配置
|
||||
sql-generator:
|
||||
# 指定使用的数据库类型:mysql 或 doris
|
||||
type: mysql
|
||||
dynamic:
|
||||
# 设置默认的数据源或者数据源组, 默认值即为 master
|
||||
primary: master
|
||||
# 设置严格模式,默认 false. 严格模式下未匹配到数据源直接报错, 非严格模式下则使用默认数据源 primary 配置的数据源
|
||||
strict: false
|
||||
# p6spy 开关, 默认 false
|
||||
p6spy: false
|
||||
# seata 开关, 默认 false
|
||||
seata: false
|
||||
# 数据源配置信息
|
||||
datasource:
|
||||
# 主数据源 (MySQL)
|
||||
master:
|
||||
url: jdbc:mysql://${DB_HOST:localhost}:3306/${DB_NAME:agent_platform}?serverTimezone=Asia/Shanghai&characterEncoding=utf-8&allowMultiQueries=true&socketTimeout=300000&useSSL=false
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
username: ${DB_USERNAME:root}
|
||||
password: ${DB_PASSWORD:your_mysql_password}
|
||||
# 以下为 Druid 连接池配置 (如果使用)
|
||||
druid:
|
||||
initial-size: 5
|
||||
min-idle: 5
|
||||
max-active: 20
|
||||
max-wait: 60000
|
||||
time-between-eviction-runs-millis: 60000
|
||||
min-evictable-idle-time-millis: 300000
|
||||
validation-query: SELECT 1 FROM DUAL
|
||||
test-while-idle: true
|
||||
test-on-borrow: false
|
||||
test-on-return: false
|
||||
pool-prepared-statements: true
|
||||
max-pool-prepared-statement-per-connection-size: 20
|
||||
filters: stat # 启用 stat filter 用于监控
|
||||
# Doris 数据源
|
||||
doris:
|
||||
url: jdbc:mysql://${DORIS_HOST:localhost}:3306/${DORIS_DB_NAME:agent_custom_table}?serverTimezone=Asia/Shanghai&characterEncoding=utf-8&allowMultiQueries=true&socketTimeout=300000&useSSL=false
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
username: ${DORIS_USERNAME:root}
|
||||
password: ${DORIS_PASSWORD:your_doris_password}
|
||||
# Doris 表配置
|
||||
table:
|
||||
replication-num: 1 # 副本数,生产环境通常为3,开发环境可以为1
|
||||
bucket-num: 10 # 分桶数
|
||||
duplicate-key: id # 重复键字段名,默认使用第一个字段
|
||||
distributed-key: id # 分布键字段名,默认使用第一个字段
|
||||
# 可选: 如果 Doris 也用 Druid,可以添加 druid 配置
|
||||
druid:
|
||||
initial-size: 2
|
||||
min-idle: 2
|
||||
max-active: 10
|
||||
max-wait: 60000
|
||||
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:localhost}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: ${REDIS_PASSWORD:your_redis_password}
|
||||
database: ${REDIS_DB:1}
|
||||
lettuce:
|
||||
pool:
|
||||
max-idle: 16
|
||||
max-active: 32
|
||||
min-idle: 8
|
||||
|
||||
main:
|
||||
allow-circular-references: true
|
||||
log:
|
||||
level: DEBUG
|
||||
|
||||
server:
|
||||
port: 8081
|
||||
|
||||
jwt:
|
||||
secretKey: ${JWT_SECRET_KEY:your_jwt_secret_key_minimum_32_characters}
|
||||
|
||||
file:
|
||||
uploadFolder: ${FILE_UPLOAD_FOLDER:/tmp/uploads}
|
||||
baseUrl: ${FILE_BASE_URL:https://your-file-domain.com/files/}
|
||||
|
||||
# Storage configuration
|
||||
storage:
|
||||
type: ${STORAGE_TYPE:local} # Storage type: local, cos(Tencent Cloud), oss(Alibaba Cloud), s3(S3 protocol/MinIO)
|
||||
|
||||
local:
|
||||
upload:
|
||||
folder: ${LOCAL_UPLOAD_FOLDER:/tmp/uploads}
|
||||
|
||||
# Tencent Cloud COS configuration (takes effect when storage.type=cos)
|
||||
cos:
|
||||
secret-id: ${COS_SECRET_ID:}
|
||||
secret-key: ${COS_SECRET_KEY:}
|
||||
region: ${COS_REGION:ap-chengdu}
|
||||
bucket-name: ${COS_BUCKET_NAME:}
|
||||
endpoint: ${COS_ENDPOINT:}
|
||||
# Legacy configuration below
|
||||
secretId: ${COS_SECRET_ID:}
|
||||
secretKey: ${COS_SECRET_KEY:}
|
||||
baseUrl: ${COS_BASE_URL:}
|
||||
|
||||
# Alibaba Cloud OSS configuration (takes effect when storage.type=oss)
|
||||
oss:
|
||||
endpoint: ${OSS_ENDPOINT:}
|
||||
access-key-id: ${OSS_ACCESS_KEY_ID:}
|
||||
access-key-secret: ${OSS_ACCESS_KEY_SECRET:}
|
||||
region-name: ${OSS_REGION_NAME:cn-chengdu}
|
||||
bucket-name: ${OSS_BUCKET_NAME:}
|
||||
|
||||
# S3 protocol configuration (takes effect when storage.type=s3, supports MinIO etc.)
|
||||
s3:
|
||||
endpoint: ${S3_ENDPOINT:}
|
||||
access-key: ${S3_ACCESS_KEY:}
|
||||
secret-key: ${S3_SECRET_KEY:}
|
||||
bucket-name: ${S3_BUCKET_NAME:}
|
||||
region: ${S3_REGION:us-east-1}
|
||||
vector-store:
|
||||
engine: milvus
|
||||
|
||||
milvus:
|
||||
uri: ${MILVUS_URI:http://localhost:19530}
|
||||
user: ${MILVUS_USER:root}
|
||||
password: ${MILVUS_PASSWORD:your_milvus_password}
|
||||
|
||||
code:
|
||||
execute:
|
||||
url: ${CODE_EXECUTE_URL:http://localhost:8020/api/run_code_with_log}
|
||||
|
||||
# 日志模块配置
|
||||
log-module:
|
||||
log:
|
||||
rust-service:
|
||||
# Rust 日志服务的基础 URL
|
||||
base-url: ${LOG_SERVICE_URL:http://localhost:8097}
|
||||
# 连接超时时间(秒)
|
||||
connect-timeout-seconds: 10
|
||||
# 写入超时时间(秒)
|
||||
write-timeout-seconds: 30
|
||||
# 读取超时时间(秒)
|
||||
read-timeout-seconds: 30
|
||||
# 是否启用连接池
|
||||
enable-connection-pool: true
|
||||
# 最大空闲连接数
|
||||
max-idle-connections: 5
|
||||
# 连接保持存活时间(分钟)
|
||||
keep-alive-duration-minutes: 5
|
||||
|
||||
search:
|
||||
elasticsearch:
|
||||
url: ${ES_URL:http://localhost:9200}
|
||||
username: ${ES_USERNAME:elastic}
|
||||
password: ${ES_PASSWORD:your_elasticsearch_password}
|
||||
api_key: ${ES_API_KEY:}
|
||||
# 生态市场模块配置
|
||||
eco-market:
|
||||
# 服务端配置
|
||||
server:
|
||||
# 远程server端的 ip端口配置
|
||||
base-url: ${ECO_MARKET_SERVER_URL:https://test-agent-market-api.xspaceagi.com}
|
||||
client:
|
||||
# 客户端配置
|
||||
retry:
|
||||
#自动注册客户端到 server端, 重试间隔时间,单位:秒;
|
||||
interval: 300
|
||||
|
||||
mcp:
|
||||
proxy-base-url: ${MCP_PROXY_URL:http://localhost:8020}
|
||||
|
||||
knife4j:
|
||||
# 开启增强配置
|
||||
enable: true
|
||||
# 标识是否生产环境:true-生产环境关闭文档,false-显示文档
|
||||
production: false
|
||||
|
||||
custom-page:
|
||||
http-proxy:
|
||||
host: ${CUSTOM_PAGE_PROXY_HOST:0.0.0.0}
|
||||
port: ${CUSTOM_PAGE_PROXY_PORT:18082}
|
||||
dev-server-host: ${DEV_SERVER_HOST:http://localhost}
|
||||
prod-server-host: ${PROD_SERVER_HOST:http://localhost:8099}
|
||||
build-server:
|
||||
base-url: ${BUILD_SERVER_URL:http://localhost:60000/api}
|
||||
ai-agent:
|
||||
base-url: ${AI_AGENT_URL:http://localhost:8086}
|
||||
docker-proxy:
|
||||
enable: ${DOCKER_PROXY_ENABLE:true}
|
||||
base-url: ${DOCKER_PROXY_URL:http://localhost:8088}
|
||||
|
||||
computer:
|
||||
proxy:
|
||||
host: ${COMPUTER_PROXY_HOST:0.0.0.0}
|
||||
port: ${COMPUTER_PROXY_PORT:18085}
|
||||
|
||||
supportCustomDomain: true
|
||||
|
||||
# 内外穿透相关服务配置
|
||||
reverse:
|
||||
server:
|
||||
# 内部服务器配置
|
||||
inner:
|
||||
# 内部服务可访问地址,如果分布式部署,可以在每台主机上通过环境变量制定 SERVICE_HOST
|
||||
service-host: ${SERVICE_HOST:127.0.0.1}
|
||||
# 绑定主机地址,如果分布式部署,可以在每台主机上通过环境变量制定 BIND_HOST
|
||||
bind-host: ${BIND_HOST:0.0.0.0}
|
||||
# 内部服务器端口范围
|
||||
ports: ${REVERSE_PORTS:30000-40000}
|
||||
# 外部服务器配置
|
||||
outer:
|
||||
# 外部用于与客户端保持连接的服务器主机地址,多个用英文逗号隔开,例如 192.168.1.12,192.168.1.13,留空时使用平台服务器地址
|
||||
host: ${OUTER_HOST:}
|
||||
# 外部服务器端口
|
||||
port: ${OUTER_PORT:10076}
|
||||
|
||||
model-api-proxy:
|
||||
base-api-url: ${MODEL_API_BASE_URL:}
|
||||
enable-model-proxy: ${MODEL_PROXY_ENABLE:true}
|
||||
port: ${MODEL_PROXY_PORT:18086}
|
||||
#记录日志
|
||||
save-log: ${MODEL_PROXY_SAVE_LOG:true}
|
||||
@@ -0,0 +1,134 @@
|
||||
spring:
|
||||
application:
|
||||
name: app-platform
|
||||
profiles:
|
||||
active: @activatedProperties@
|
||||
# 默认为false。如果开启的话在事务真正物理提交doCommit失败后会进行回滚。
|
||||
transaction:
|
||||
rollback-on-commit-failure: true
|
||||
# [可选]配置,自行判断是否使用Spring Cache注解(@Cacheable 等)选项,如果不启用,在启动类上注释: @EnableCaching
|
||||
cache:
|
||||
type: CAFFEINE
|
||||
#应用程序启动创建缓存的名称,必须将所有注释为@Cacheable缓存name(或value)罗列在这里
|
||||
cache-names:
|
||||
- queryOrgTree
|
||||
- getUserInfo
|
||||
- tableTotalCache
|
||||
caffeine:
|
||||
spec: "maximumSize=2000,expireAfterAccess=30s"
|
||||
# 排除DruidDataSourceAutoConfigure,使用dynamic-datasource-spring-boot3-starter
|
||||
# autoconfigure:
|
||||
# exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure
|
||||
|
||||
server:
|
||||
compression:
|
||||
enabled: true
|
||||
min-response-size: 2KB
|
||||
mime-types: application/json,application/xml,text/html,text/xml,text/plain,text/css,text/javascript,application/javascript
|
||||
tomcat:
|
||||
# 设置POST请求大小限制,0表示不限制
|
||||
max-http-form-post-size: 0
|
||||
# 设置URI编码为UTF-8,支持中文等非ASCII字符
|
||||
uri-encoding: UTF-8
|
||||
|
||||
logging:
|
||||
config: classpath:logback-custom.xml
|
||||
|
||||
#允许跨域的根域名
|
||||
access:
|
||||
control:
|
||||
allow-origin: "*"
|
||||
|
||||
#无需检测登录的接口,前缀匹配
|
||||
auth:
|
||||
excludePath:
|
||||
- /api/user/login
|
||||
- /health
|
||||
- /ready
|
||||
- /favicon.ico
|
||||
- /doc.html
|
||||
- /webjars/**
|
||||
- /v3/**
|
||||
- /api/user/login
|
||||
- /api/user/passwordLogin
|
||||
- /api/user/codeLogin
|
||||
- /api/user/mp/codeLogin
|
||||
- /api/user/code/send
|
||||
- /api/user/email/check
|
||||
- /api/user/password/reset
|
||||
- /api/tenant/config
|
||||
- /api/system/eco/market/server
|
||||
- /api/temp/chat/conversation
|
||||
- /api/temp/chat/completions
|
||||
- /api/logo/
|
||||
- /api/file
|
||||
- /api/f/
|
||||
- /api/mcp/sse
|
||||
- /api/mcp/message
|
||||
- /api/v1/
|
||||
- /api/ticket/redirect
|
||||
- /api/audio
|
||||
- /api/page/workflow
|
||||
- /api/page/plugin
|
||||
- /api/published/category
|
||||
- /api/published/agent/list
|
||||
- /api/computer/static
|
||||
- /api/agent/conversation/share/detail
|
||||
- /api/sys/v
|
||||
- /api/sandbox/config/reg
|
||||
- /api/sandbox/config/health/
|
||||
- /api/sandbox/config/redirect/
|
||||
# IM:三方回调免登录
|
||||
- /api/im/feishu
|
||||
- /api/im/dingtalk
|
||||
- /api/im/wework
|
||||
- /api/i18n/
|
||||
|
||||
# DataSource Config
|
||||
mybatis-plus:
|
||||
mapper-locations: classpath*:/mapper/*.xml
|
||||
tenant:
|
||||
ignoreTenantTables:
|
||||
- tenant
|
||||
- pf_retry_data
|
||||
- system_config
|
||||
- card
|
||||
- knowledge_task
|
||||
- knowledge_task_history
|
||||
- schedule_task
|
||||
- eco_market_server_secret
|
||||
- eco_market_server_config
|
||||
- eco_market_server_publish_config
|
||||
- eco_market_client_secret
|
||||
|
||||
# 扫描此包下pojo类生成短别名,在xml中可以使用类名,不用输全路径类名
|
||||
type-aliases-package: com.xspaceagi.**.infra.dao.entity
|
||||
|
||||
# 此包下的类会由SqlSessionFactoryBean注册成TypeHandler
|
||||
type-handlers-package:
|
||||
global-config:
|
||||
# 数据库配置
|
||||
db-config:
|
||||
# 默认值是ID_WORKER(框架生成的分布式唯一ID,Long型),其他选项AUTO(数据库自增),INPUT(自定义),UUID(框架生成uuid),ID_WORKER_STR(框架生成的分布式唯一ID,String型)
|
||||
id-type: auto
|
||||
# 字段策略:涉及insert,update及wrapper内部的entity属性生成的where条件;有4个选项IGNORED(不判断),NOT_NULL(非null即可),NOT_EMPTY(非null且非空串),DEFAULT(非null即可)
|
||||
# 比如User对象name是""空串,调用insert接口,IGNORED会插入空串,NOT_NULL会插入空串,NOT_EMPTY不会插入name字段
|
||||
where-strategy: NOT_EMPTY
|
||||
|
||||
# qiming-cli、saas、manual
|
||||
installation-source: saas
|
||||
|
||||
app:
|
||||
version: 1.0.8.3
|
||||
|
||||
stt:
|
||||
api:
|
||||
base-url: https://stt-api.qiming.com
|
||||
|
||||
sandbox:
|
||||
api:
|
||||
rewrite:
|
||||
allow-path:
|
||||
- "skill/**"
|
||||
- "publish/apply"
|
||||
- "im/wechat/push-message"
|
||||
@@ -0,0 +1,71 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration debug="true">
|
||||
<springProperty name="LOG_LEVEL" scope="context" source="log.level"/>
|
||||
<springProperty name="DEV_MODE" scope="context" source="spring.profiles.active"/>
|
||||
<!-- 通过 logging.file.path 配置日志输出路径,默认输出到相对路径 logs/ -->
|
||||
<springProperty name="logPath" scope="context" source="logging.file.path" defaultValue="logs"/>
|
||||
<property name="outPattern"
|
||||
value="[%-5p] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] [%c] [%X{tid}] [%X{prefix}] - %m%n"/>
|
||||
<property name="maxHistory" value="7"/>
|
||||
<property name="charSet" value="UTF-8"/>
|
||||
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>${outPattern}</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<appender name="APP_LOG_FILE"
|
||||
class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${logPath}/app.log</file>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<FileNamePattern>${logPath}/app.log.%d{yyyy-MM-dd}
|
||||
</FileNamePattern>
|
||||
<maxHistory>${maxHistory}</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<Pattern>${outPattern}</Pattern>
|
||||
<charset>${charSet}</charset>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<!-- 如果是可以容忍重启时丢失的应用日志,可以使用异步日志输出,大幅提升日志性能 -->
|
||||
<appender name="ASYNC_APP_LOG_FILE" class="ch.qos.logback.classic.AsyncAppender">
|
||||
<!-- 建议不丢失日志,设置为0 -->
|
||||
<discardingThreshold>0</discardingThreshold>
|
||||
<!-- 更改默认的队列的长度,该值会影响性能,默认值为256 -->
|
||||
<queueSize>512</queueSize>
|
||||
<appender-ref ref="APP_LOG_FILE"/>
|
||||
</appender>
|
||||
|
||||
<logger name="org.springframework" level="ERROR"/>
|
||||
<logger name="org.apache.zookeeper" level="ERROR"/>
|
||||
<logger name="com.baomidou" level="ERROR"/>
|
||||
<logger name="org.mybatis.spring" level="ERROR"/>
|
||||
<logger name="com.xspaceagi.system.infra.dao.mapper" level="ERROR"/>
|
||||
<logger name="com.xspaceagi.agent.core.infra.dao.mapper" level="ERROR"/>
|
||||
<logger name="io.lettuce.core" level="ERROR"/>
|
||||
<logger name="org.redisson" level="ERROR"/>
|
||||
<logger name="com.alibaba" level="ERROR"/>
|
||||
<logger name="com.xspaceagi.system.domain.service.impl.ScheduleTaskDomainServiceImpl" level="ERROR"/>
|
||||
|
||||
<!-- 缺省应用日志输出 -->
|
||||
<root level="${LOG_LEVEL}">
|
||||
<if condition='"${DEV_MODE}".equals("dev")'>
|
||||
<then>
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
<appender-ref ref="ASYNC_APP_LOG_FILE"/>
|
||||
</then>
|
||||
</if>
|
||||
<if condition='"${DEV_MODE}".equals("prod")'>
|
||||
<then>
|
||||
<appender-ref ref="ASYNC_APP_LOG_FILE"/>
|
||||
</then>
|
||||
</if>
|
||||
<if condition='"${DEV_MODE}".equals("test")'>
|
||||
<then>
|
||||
<appender-ref ref="ASYNC_APP_LOG_FILE"/>
|
||||
</then>
|
||||
</if>
|
||||
</root>
|
||||
</configuration>
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,67 @@
|
||||
package com.xspaceagi.compose.api.service;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.xspaceagi.PlatformApiApplication;
|
||||
import com.xspaceagi.compose.sdk.DorisDataPage;
|
||||
import com.xspaceagi.compose.sdk.request.DorisTableDefineRequest;
|
||||
import com.xspaceagi.compose.sdk.request.QueryDorisTableDefinePageRequest;
|
||||
import com.xspaceagi.compose.sdk.service.IComposeDbTableRpcService;
|
||||
import com.xspaceagi.compose.sdk.vo.define.TableDefineVo;
|
||||
import com.xspaceagi.system.spec.common.RequestContext;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@SpringBootTest(classes = PlatformApiApplication.class)
|
||||
@Slf4j
|
||||
public class ComposeDbTableRpcServiceImplQueryTableDefinitionTest {
|
||||
|
||||
@Autowired
|
||||
private IComposeDbTableRpcService composeDbTableRpcService;
|
||||
|
||||
@Test
|
||||
void testQueryTableDefinition() {
|
||||
|
||||
// 设置租户上下文,防止 MyBatis Plus 多租户插件报错
|
||||
try {
|
||||
RequestContext.setThreadTenantId(1L);
|
||||
|
||||
// RequestContext.get().setUserId(6L);
|
||||
|
||||
DorisTableDefineRequest request = new DorisTableDefineRequest();
|
||||
request.setTableId(28L); // 请根据实际测试表ID设置
|
||||
TableDefineVo definitionVo = composeDbTableRpcService.queryTableDefinition(request);
|
||||
log.info("definitionVo:{}", JSON.toJSONString(definitionVo));
|
||||
assertNotNull(definitionVo);
|
||||
// 可根据实际业务补充更多断言
|
||||
} finally {
|
||||
RequestContext.remove();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testQueryTableDefineBySpaceId() {
|
||||
try {
|
||||
RequestContext.setThreadTenantId(1L);
|
||||
|
||||
QueryDorisTableDefinePageRequest request = QueryDorisTableDefinePageRequest.builder()
|
||||
.spaceId(37L) // 请根据实际测试空间ID设置
|
||||
.pageNo(1)
|
||||
.pageSize(10)
|
||||
.build();
|
||||
|
||||
DorisDataPage<TableDefineVo> pageResult = composeDbTableRpcService.queryTableDefineBySpaceId(request);
|
||||
log.info("pageResult:{}", JSON.toJSONString(pageResult));
|
||||
assertNotNull(pageResult, "分页结果不应为null");
|
||||
assertNotNull(pageResult.getRecords(), "记录列表不应为null");
|
||||
// 可根据实际业务补充更多断言,比如校验返回数量、内容等
|
||||
} finally {
|
||||
RequestContext.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
package com.xspaceagi.compose.api.service;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
import com.xspaceagi.PlatformApiApplication;
|
||||
import com.xspaceagi.compose.sdk.request.DorisTableDataRequest;
|
||||
import com.xspaceagi.compose.sdk.request.DorisTableDefineRequest;
|
||||
import com.xspaceagi.compose.sdk.response.DorisTableDataResponse;
|
||||
import com.xspaceagi.compose.sdk.vo.define.CreateTableDefineVo;
|
||||
import com.xspaceagi.system.spec.common.RequestContext;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@SpringBootTest(classes = PlatformApiApplication.class)
|
||||
public class ComposeDbTableRpcServiceImplTest {
|
||||
|
||||
@Autowired
|
||||
private ComposeDbTableRpcServiceImpl composeDbTableRpcService;
|
||||
|
||||
@Test
|
||||
void testQueryTableData_update() {
|
||||
// 设置租户上下文,防止 MyBatis Plus 多租户插件报错
|
||||
try {
|
||||
RequestContext.setThreadTenantId(1L);
|
||||
|
||||
// 按照示例构造请求参数
|
||||
DorisTableDataRequest request = new DorisTableDataRequest();
|
||||
request.setTableId(28L);
|
||||
request.setSql(
|
||||
"UPDATE custom_table_table_name SET agent_name={{agent_name}} WHERE agent_id={{agent_id}} limit 100");
|
||||
java.util.Map<String, Object> args = new java.util.HashMap<>();
|
||||
args.put("agent_id", "agent_001");
|
||||
args.put("agent_name", "智能体测试");
|
||||
request.setArgs(args);
|
||||
java.util.Map<String, Object> extArgs = new java.util.HashMap<>();
|
||||
extArgs.put("uid", "6");
|
||||
request.setExtArgs(extArgs);
|
||||
DorisTableDataResponse response = composeDbTableRpcService.queryTableData(request);
|
||||
log.info("update response: {}", JSON.toJSONString(response));
|
||||
|
||||
assertNotNull(response);
|
||||
// 可根据实际数据断言返回内容
|
||||
} finally {
|
||||
RequestContext.remove();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testQueryTableData_WithNullTableId() {
|
||||
// 设置租户上下文,防止 MyBatis Plus 多租户插件报错
|
||||
try {
|
||||
RequestContext.setThreadTenantId(1L);
|
||||
DorisTableDataRequest request = new DorisTableDataRequest();
|
||||
// 设置空的tableId
|
||||
request.setTableId(null);
|
||||
// 验证异常抛出
|
||||
assertThrows(RuntimeException.class, () -> {
|
||||
composeDbTableRpcService.queryTableData(request);
|
||||
});
|
||||
} finally {
|
||||
RequestContext.remove();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInsertCustomTable28() {
|
||||
// 设置租户上下文,防止 MyBatis Plus 多租户插件报错
|
||||
try {
|
||||
RequestContext.setThreadTenantId(1L);
|
||||
DorisTableDataRequest insertRequest = new DorisTableDataRequest();
|
||||
insertRequest.setTableId(28L);
|
||||
insertRequest.setSql(
|
||||
"INSERT INTO custom_table_table_name ( agent_id, agent_name, nick_name, uid, user_name, created) VALUES ( 'agent_002', '测试智能体2', '测试昵称2', 'user_12346', 'user2', NOW())");
|
||||
// insert 一般不需要 args/extArgs,直接写死
|
||||
DorisTableDataResponse response = composeDbTableRpcService.queryTableData(insertRequest);
|
||||
log.info("insert response: {}", JSON.toJSONString(response));
|
||||
assertNotNull(response);
|
||||
// 可根据 insert 返回内容断言
|
||||
} finally {
|
||||
RequestContext.remove();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSelectCustomTable28() {
|
||||
// 设置租户上下文,防止 MyBatis Plus 多租户插件报错
|
||||
try {
|
||||
RequestContext.setThreadTenantId(1L);
|
||||
DorisTableDataRequest selectRequest = new DorisTableDataRequest();
|
||||
selectRequest.setTableId(28L);
|
||||
selectRequest.setSql("SELECT * FROM custom_table_table_name WHERE uid='user_12346'");
|
||||
DorisTableDataResponse response = composeDbTableRpcService.queryTableData(selectRequest);
|
||||
log.info("insert response: {}", JSON.toJSONString(response));
|
||||
|
||||
assertNotNull(response);
|
||||
// 可断言返回数据包含 user_12346
|
||||
} finally {
|
||||
RequestContext.remove();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSelectLikeCustomTable28() {
|
||||
// 设置租户上下文,防止 MyBatis Plus 多租户插件报错
|
||||
try {
|
||||
RequestContext.setThreadTenantId(1L);
|
||||
DorisTableDataRequest selectRequest = new DorisTableDataRequest();
|
||||
selectRequest.setTableId(28L);
|
||||
Map<String, Object> args = new HashMap<>();
|
||||
args.put("team_name", "user");
|
||||
selectRequest.setArgs(args);
|
||||
selectRequest.setSql("SELECT * FROM custom_table WHERE user_name LIKE '%${{team_name}}%'");
|
||||
DorisTableDataResponse response = composeDbTableRpcService.queryTableData(selectRequest);
|
||||
log.info("insert response: {}", JSON.toJSONString(response));
|
||||
|
||||
assertNotNull(response);
|
||||
// 可断言返回数据包含 user_12346
|
||||
} finally {
|
||||
RequestContext.remove();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSelectCustomTable28_withLimit() {
|
||||
// 设置租户上下文,防止 MyBatis Plus 多租户插件报错
|
||||
try {
|
||||
RequestContext.setThreadTenantId(1L);
|
||||
DorisTableDataRequest selectRequest = new DorisTableDataRequest();
|
||||
selectRequest.setTableId(28L);
|
||||
selectRequest.setSql("SELECT * FROM custom_table_table_name WHERE uid='user_12346' limit 100");
|
||||
DorisTableDataResponse response = composeDbTableRpcService.queryTableData(selectRequest);
|
||||
log.info("insert response: {}", JSON.toJSONString(response));
|
||||
|
||||
assertNotNull(response);
|
||||
// 可断言返回数据包含 user_12346
|
||||
} finally {
|
||||
RequestContext.remove();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteCustomTable28() {
|
||||
// 设置租户上下文,防止 MyBatis Plus 多租户插件报错
|
||||
try {
|
||||
RequestContext.setThreadTenantId(1L);
|
||||
DorisTableDataRequest deleteRequest = new DorisTableDataRequest();
|
||||
deleteRequest.setTableId(28L);
|
||||
deleteRequest.setSql("DELETE FROM custom_table_table_name WHERE id=1002");
|
||||
DorisTableDataResponse response = composeDbTableRpcService.queryTableData(deleteRequest);
|
||||
assertNotNull(response);
|
||||
// 可根据 delete 返回内容断言
|
||||
} finally {
|
||||
RequestContext.remove();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSelectCustomTable28WithJoin() {
|
||||
// 设置租户上下文,防止 MyBatis Plus 多租户插件报错
|
||||
try {
|
||||
RequestContext.setThreadTenantId(1L);
|
||||
DorisTableDataRequest joinRequest = new DorisTableDataRequest();
|
||||
joinRequest.setTableId(28L); // 这里可用主表ID
|
||||
joinRequest.setSql(
|
||||
"SELECT a.*, b.nick_name AS b_nick_name FROM custom_table_table_name a LEFT JOIN custom_table_table_name b ON a.uid = b.uid WHERE a.uid='user_12346'");
|
||||
DorisTableDataResponse response = composeDbTableRpcService.queryTableData(joinRequest);
|
||||
log.info("SELECT response: {}", JSON.toJSONString(response));
|
||||
|
||||
assertNotNull(response);
|
||||
// 可断言返回数据包含 user_12346 及 b_nick_name 字段
|
||||
} finally {
|
||||
RequestContext.remove();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSelectCustomTable28WithJoin_ListParams() {
|
||||
// 设置租户上下文,防止 MyBatis Plus 多租户插件报错
|
||||
try {
|
||||
RequestContext.setThreadTenantId(1L);
|
||||
DorisTableDataRequest joinRequest = new DorisTableDataRequest();
|
||||
joinRequest.setTableId(28L); // 这里可用主表ID
|
||||
|
||||
var uidList = Arrays.asList("user_12346", "user_12347");
|
||||
Map<String, Object> args = new HashMap<>();
|
||||
args.put("uidList", uidList);
|
||||
joinRequest.setArgs(args);
|
||||
// 使用{{uidList}} 占位符, 替换sql中的参数,后面使用uidList 列表需要解析成英文逗号分割
|
||||
joinRequest.setSql(
|
||||
"SELECT a.*, b.nick_name AS b_nick_name FROM custom_table_table_name a LEFT JOIN custom_table_table_name b ON a.uid = b.uid WHERE a.uid in ({{uidList}})");
|
||||
DorisTableDataResponse response = composeDbTableRpcService.queryTableData(joinRequest);
|
||||
log.info("SELECT response: {}", JSON.toJSONString(response));
|
||||
assertNotNull(response);
|
||||
assertNotNull(response.getData());
|
||||
// 可断言返回数据包含 user_12346 及 b_nick_name 字段
|
||||
} finally {
|
||||
RequestContext.remove();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBatchInsertCustomTable28() {
|
||||
// 设置租户上下文,防止 MyBatis Plus 多租户插件报错
|
||||
try {
|
||||
RequestContext.setThreadTenantId(1L);
|
||||
DorisTableDataRequest batchInsertRequest = new DorisTableDataRequest();
|
||||
batchInsertRequest.setTableId(28L);
|
||||
batchInsertRequest.setSql(
|
||||
"INSERT INTO custom_table_table_name ( agent_id, agent_name, nick_name, uid, user_name, created) VALUES "
|
||||
+
|
||||
"( 'agent_003', '测试智能体3', '测试昵称3', 'user_12347', 'user3', NOW()), " +
|
||||
"( 'agent_004', '测试智能体4', '测试昵称4', 'user_12348', 'user4', NOW()), " +
|
||||
"( 'agent_005', '测试智能体5', '测试昵称5', 'user_12349', 'user5', NOW())");
|
||||
|
||||
DorisTableDataResponse response = composeDbTableRpcService.queryTableData(batchInsertRequest);
|
||||
log.info("batch insert response: {}", JSON.toJSONString(response));
|
||||
assertNotNull(response);
|
||||
// 验证批量插入的行数应该为3
|
||||
assertEquals(3L, response.getRowNum());
|
||||
} finally {
|
||||
RequestContext.remove();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBatchInsertWithSemicolonCustomTable28() {
|
||||
// 设置租户上下文,防止 MyBatis Plus 多租户插件报错
|
||||
try {
|
||||
RequestContext.setThreadTenantId(1L);
|
||||
DorisTableDataRequest batchInsertRequest = new DorisTableDataRequest();
|
||||
batchInsertRequest.setTableId(28L);
|
||||
batchInsertRequest.setSql(
|
||||
"INSERT INTO custom_table_table_name ( agent_id, agent_name, nick_name, uid, user_name, created) VALUES ( 'agent_006', '测试智能体6', '测试昵称6', 'user_12350', 'user6', NOW());"
|
||||
+
|
||||
"INSERT INTO custom_table_table_name ( agent_id, agent_name, nick_name, uid, user_name, created) VALUES ( 'agent_007', '测试智能体7', '测试昵称7', 'user_12351', 'user7', NOW());"
|
||||
+
|
||||
"INSERT INTO custom_table_table_name ( agent_id, agent_name, nick_name, uid, user_name, created) VALUES ( 'agent_008', '测试智能体8', '测试昵称8', 'user_12352', 'user8', NOW());");
|
||||
|
||||
DorisTableDataResponse response = composeDbTableRpcService.queryTableData(batchInsertRequest);
|
||||
log.info("batch insert with semicolon response: {}", JSON.toJSONString(response));
|
||||
assertNotNull(response);
|
||||
// 验证批量插入的行数,使用分号分割时每条语句都会执行成功
|
||||
assertNotNull(response.getRowNum());
|
||||
} finally {
|
||||
RequestContext.remove();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBatchUpdateWithSemicolonCustomTable28() {
|
||||
// 设置租户上下文,防止 MyBatis Plus 多租户插件报错
|
||||
try {
|
||||
RequestContext.setThreadTenantId(1L);
|
||||
DorisTableDataRequest batchUpdateRequest = new DorisTableDataRequest();
|
||||
batchUpdateRequest.setTableId(28L);
|
||||
batchUpdateRequest.setSql(
|
||||
"UPDATE custom_table_table_name SET agent_name='更新智能体3', nick_name='更新昵称3' WHERE agent_id='agent_003';"
|
||||
+
|
||||
"UPDATE custom_table_table_name SET agent_name='更新智能体4', nick_name='更新昵称4' WHERE agent_id='agent_004';"
|
||||
+
|
||||
"UPDATE custom_table_table_name SET agent_name='更新智能体5', nick_name='更新昵称5' WHERE agent_id='agent_005';");
|
||||
|
||||
DorisTableDataResponse response = composeDbTableRpcService.queryTableData(batchUpdateRequest);
|
||||
log.info("batch update with semicolon response: {}", JSON.toJSONString(response));
|
||||
assertNotNull(response);
|
||||
// 验证批量更新成功
|
||||
assertNotNull(response.getRowNum());
|
||||
|
||||
// 验证数据是否更新成功
|
||||
DorisTableDataRequest selectRequest = new DorisTableDataRequest();
|
||||
selectRequest.setTableId(28L);
|
||||
selectRequest.setSql(
|
||||
"SELECT * FROM custom_table_table_name WHERE agent_id IN ('agent_003', 'agent_004', 'agent_005')");
|
||||
DorisTableDataResponse selectResponse = composeDbTableRpcService.queryTableData(selectRequest);
|
||||
log.info("select after batch update response: {}", JSON.toJSONString(selectResponse));
|
||||
assertNotNull(selectResponse);
|
||||
assertNotNull(selectResponse.getData());
|
||||
} finally {
|
||||
RequestContext.remove();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateTable_Success() {
|
||||
try {
|
||||
RequestContext.setThreadTenantId(1L);
|
||||
|
||||
// 第一步:先获取一个已存在表的定义,作为参考模板
|
||||
DorisTableDefineRequest queryRequest = new DorisTableDefineRequest();
|
||||
queryRequest.setTableId(113L); // 请根据实际情况修改
|
||||
|
||||
CreateTableDefineVo templateTable = composeDbTableRpcService.queryCreateTableInfo(queryRequest);
|
||||
log.info("Loaded template table definition: {}", JSON.toJSONString(templateTable));
|
||||
|
||||
// 第三步:执行创建表操作
|
||||
var tableId = composeDbTableRpcService.createTable(templateTable);
|
||||
log.info("Table created, tableId={}",tableId);
|
||||
|
||||
// 可以再次查询验证表是否创建成功
|
||||
// 这里省略验证步骤
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Create table failed", e);
|
||||
assert false : "表创建应该成功,但抛出了异常: " + e.getMessage();
|
||||
} finally {
|
||||
RequestContext.remove();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package com.xspaceagi.compose.api.service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.xspaceagi.log.api.service.LogPlatformRpcServiceImpl;
|
||||
import com.xspaceagi.log.sdk.reponse.AgentLogModelResponse;
|
||||
import com.xspaceagi.log.sdk.request.AgentLogEntryRequest;
|
||||
import com.xspaceagi.log.sdk.request.AgentLogSearchParamsRequest;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@SpringBootTest
|
||||
public class LogPlatformRpcServiceImplTest {
|
||||
|
||||
@Autowired
|
||||
private LogPlatformRpcServiceImpl logPlatformRpcService;
|
||||
|
||||
@Test
|
||||
public void testAddLog_Success() {
|
||||
// Arrange
|
||||
AgentLogEntryRequest request = new AgentLogEntryRequest();
|
||||
// 填充request的测试数据(根据实际定义)
|
||||
|
||||
request.setRequestId("req_001333");
|
||||
request.setMessageId("msg_001");
|
||||
request.setConversationId("session_001");
|
||||
request.setUserUid("user_123");
|
||||
request.setTenantId("1");
|
||||
request.setSpaceId("space_001");
|
||||
request.setUserInput("请帮我分析一下这个数据");
|
||||
request.setOutput("根据您提供的数据,我分析出以下结论...");
|
||||
request.setRequestStartTime(LocalDateTime.parse("2024-01-15T10:30:00Z", DateTimeFormatter.ISO_DATE_TIME));
|
||||
request.setRequestEndTime(LocalDateTime.parse("2024-01-15T10:30:05Z", DateTimeFormatter.ISO_DATE_TIME));
|
||||
request.setStatus("success");
|
||||
|
||||
request.setUserId(1L);
|
||||
request.setUserName("张三");
|
||||
|
||||
// Act
|
||||
boolean result = logPlatformRpcService.addLog(request);
|
||||
|
||||
// Assert
|
||||
assert (result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSearchAgentLogs_Success() {
|
||||
// Arrange - 准备搜索参数
|
||||
AgentLogSearchParamsRequest searchParams = new AgentLogSearchParamsRequest();
|
||||
|
||||
// 设置时间范围 - 使用RFC 3339格式(ISO_DATE_TIME)
|
||||
LocalDateTime startTime = LocalDateTime.parse("2024-01-01T00:00:00Z", DateTimeFormatter.ISO_DATE_TIME);
|
||||
LocalDateTime endTime = LocalDateTime.parse("2024-12-31T23:59:59Z", DateTimeFormatter.ISO_DATE_TIME);
|
||||
searchParams.setStartTime(startTime);
|
||||
searchParams.setEndTime(endTime);
|
||||
searchParams.setRequestId("req_001333");
|
||||
|
||||
// 设置其他搜索条件
|
||||
searchParams.setTenantId("1"); // 当前租户
|
||||
searchParams.setUserInput(Arrays.asList("分析", "数据")); // 搜索包含这些关键词的用户输入
|
||||
|
||||
// 设置当前页和页大小
|
||||
Long current = 1L;
|
||||
Long pageSize = 10L;
|
||||
|
||||
log.info("searchParams result", JSON.toJSONString(searchParams));
|
||||
|
||||
// Act - 执行搜索
|
||||
IPage<AgentLogModelResponse> result = logPlatformRpcService.searchAgentLogs(searchParams, current, pageSize);
|
||||
|
||||
// Assert - 验证结果
|
||||
log.info("Total search hits: {}", result.getTotal());
|
||||
log.info("Current page row count: {}", result.getRecords().size());
|
||||
|
||||
log.info("result", JSON.toJSONString(result));
|
||||
// check 结果记录, RequestId("req_001333") 必须都是这个值的
|
||||
for (AgentLogModelResponse log : result.getRecords()) {
|
||||
assert (log.getRequestId().equals("req_001333"));
|
||||
}
|
||||
|
||||
// 如果有搜索结果,验证第一条记录的基本信息
|
||||
if (!result.getRecords().isEmpty()) {
|
||||
AgentLogModelResponse firstLog = result.getRecords().get(0);
|
||||
log.info("First row requestId: {}", firstLog.getRequestId());
|
||||
log.info("First row user input: {}", firstLog.getUserInput());
|
||||
log.info("First row createdAt: {}", firstLog.getCreatedAt());
|
||||
|
||||
// 验证返回的记录包含必要的字段
|
||||
assert (firstLog.getRequestId() != null);
|
||||
assert (firstLog.getUserInput() != null);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.xspaceagi.ecoMarket;
|
||||
|
||||
import com.xspaceagi.eco.market.sdk.reponse.ClientSecretResponse;
|
||||
import com.xspaceagi.eco.market.sdk.request.ClientSecretRequest;
|
||||
import com.xspaceagi.eco.market.sdk.service.IEcoMarketRpcService;
|
||||
import com.xspaceagi.system.spec.common.RequestContext;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
/**
|
||||
* EcoMarketRpcService 集成测试类
|
||||
*/
|
||||
@Slf4j
|
||||
@SpringBootTest
|
||||
public class EcoMarketRpcServiceTest {
|
||||
|
||||
@Resource
|
||||
private IEcoMarketRpcService ecoMarketRpcService;
|
||||
|
||||
/**
|
||||
* 测试查询客户端密钥功能 - 成功场景
|
||||
*/
|
||||
@Test
|
||||
public void testQueryClientSecret_Success() {
|
||||
try {
|
||||
RequestContext.setThreadTenantId(1L);
|
||||
|
||||
// Arrange
|
||||
ClientSecretRequest request = new ClientSecretRequest();
|
||||
request.setTenantId(1L);
|
||||
|
||||
// Act
|
||||
ClientSecretResponse response = ecoMarketRpcService.queryClientSecret(request);
|
||||
|
||||
log.info("Client secret query result, response={}", response);
|
||||
|
||||
// Assert
|
||||
assertNotNull(response, "响应不应为空");
|
||||
if (response != null) {
|
||||
assertNotNull(response.getTenantId(), "租户ID不应为空");
|
||||
assertEquals(1L, response.getTenantId(), "租户ID应该匹配");
|
||||
log.info("Client secret: id={}, name={}, clientId={}, tenantId={}",
|
||||
response.getId(), response.getName(), response.getClientId(), response.getTenantId());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Exception while testing client secret query", e);
|
||||
throw e;
|
||||
} finally {
|
||||
RequestContext.remove();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
package com.xspaceagi.knowledge;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.xspaceagi.knowledge.api.KnowledgeConfigRpcService;
|
||||
import com.xspaceagi.knowledge.api.KnowledgeQaSearchRpcService;
|
||||
import com.xspaceagi.knowledge.sdk.request.KnowledgeCreateRequestVo;
|
||||
import com.xspaceagi.knowledge.sdk.request.KnowledgeDocumentRequestVo;
|
||||
import com.xspaceagi.knowledge.sdk.request.KnowledgeQaRequestVo;
|
||||
import com.xspaceagi.knowledge.sdk.response.KnowledgeConfigVo;
|
||||
import com.xspaceagi.knowledge.sdk.response.KnowledgeDocumentResponseVo;
|
||||
import com.xspaceagi.knowledge.sdk.response.KnowledgeQaResponseVo;
|
||||
import com.xspaceagi.knowledge.sdk.sevice.IKnowledgeDocumentSearchRpcService;
|
||||
import com.xspaceagi.system.spec.common.RequestContext;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@SpringBootTest
|
||||
public class KnowledegRpcServiceImplTest {
|
||||
|
||||
@Resource
|
||||
private KnowledgeConfigRpcService knowledgeConfigRpcService;
|
||||
|
||||
@Resource
|
||||
private IKnowledgeDocumentSearchRpcService knowledgeDocumentSearchRpcService;
|
||||
|
||||
@Resource
|
||||
private KnowledgeQaSearchRpcService knowledgeQaSearchRpcService;
|
||||
|
||||
@Test
|
||||
public void testCreateKnowledgeConfig() {
|
||||
|
||||
try {
|
||||
RequestContext.setThreadTenantId(1L);
|
||||
|
||||
// Arrange
|
||||
KnowledgeCreateRequestVo requestVo = KnowledgeCreateRequestVo.builder()
|
||||
.spaceId(1L)
|
||||
.name("Test Knowledge")
|
||||
.description("This is a test knowledge")
|
||||
.dataType(1)
|
||||
.icon("https://example.com/icon.png")
|
||||
.userId(1L)
|
||||
.build();
|
||||
// Act
|
||||
Long result = knowledgeConfigRpcService.createKnowledgeConfig(requestVo);
|
||||
|
||||
log.info("Create database result, result={}", result);
|
||||
// Assert
|
||||
assertNotNull(result);
|
||||
} finally {
|
||||
RequestContext.remove();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testQueryKnowledgeConfigById() {
|
||||
try {
|
||||
RequestContext.setThreadTenantId(1L);
|
||||
KnowledgeConfigVo knowledgeConfigVo = knowledgeConfigRpcService.queryKnowledgeConfigById(153L);
|
||||
log.info("Knowledge config query result, knowledgeConfigVo={}", JSON.toJSONString(knowledgeConfigVo));
|
||||
assertNotNull(knowledgeConfigVo);
|
||||
} finally {
|
||||
RequestContext.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试知识库文档搜索功能
|
||||
*/
|
||||
@Test
|
||||
public void testDocumentSearch() {
|
||||
try {
|
||||
RequestContext.setThreadTenantId(1L);
|
||||
|
||||
// Arrange
|
||||
KnowledgeDocumentRequestVo requestVo = new KnowledgeDocumentRequestVo();
|
||||
requestVo.setKbId(113L); // 使用上面查询的知识库ID
|
||||
|
||||
// Act
|
||||
KnowledgeDocumentResponseVo responseVo = knowledgeDocumentSearchRpcService.documentSearch(requestVo);
|
||||
|
||||
log.info("Knowledge doc search result, responseVo={}", JSON.toJSONString(responseVo));
|
||||
|
||||
// Assert
|
||||
assertNotNull(responseVo);
|
||||
assertNotNull(responseVo.getDocumentVoList());
|
||||
log.info("Document count: {}", responseVo.getDocumentVoList().size());
|
||||
|
||||
} finally {
|
||||
RequestContext.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试知识库文档搜索功能 - 测试空结果
|
||||
*/
|
||||
@Test
|
||||
public void testDocumentSearchWithEmptyResult() {
|
||||
try {
|
||||
RequestContext.setThreadTenantId(1L);
|
||||
|
||||
// Arrange - 使用一个不存在的知识库ID
|
||||
KnowledgeDocumentRequestVo requestVo = new KnowledgeDocumentRequestVo();
|
||||
requestVo.setKbId(999999L);
|
||||
|
||||
// Act
|
||||
KnowledgeDocumentResponseVo responseVo = knowledgeDocumentSearchRpcService.documentSearch(requestVo);
|
||||
|
||||
log.info("Knowledge doc search (empty), responseVo={}", JSON.toJSONString(responseVo));
|
||||
|
||||
// Assert
|
||||
assertNotNull(responseVo);
|
||||
assertNotNull(responseVo.getDocumentVoList());
|
||||
log.info("Document count: {}", responseVo.getDocumentVoList().size());
|
||||
|
||||
} finally {
|
||||
RequestContext.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试知识库问答搜索功能
|
||||
* 测试参数:kbId=41, question="浮点数的定义是什么", 期望docId=24
|
||||
*/
|
||||
@Test
|
||||
public void testQaSearch() {
|
||||
try {
|
||||
RequestContext.setThreadTenantId(1L);
|
||||
|
||||
// Arrange - 创建问答搜索请求
|
||||
KnowledgeQaRequestVo requestVo = new KnowledgeQaRequestVo();
|
||||
requestVo.setKbId(413L); // 知识库ID
|
||||
requestVo.setQuestion("Antigravity"); // 问题
|
||||
requestVo.setTopK(5); // 返回前5个结果
|
||||
requestVo.setIgnoreDocStatus(true); // 不忽略文档状态
|
||||
requestVo.setIgnoreTenantId(false); // 不忽略租户ID
|
||||
|
||||
// Act - 执行问答搜索
|
||||
KnowledgeQaResponseVo responseVo = knowledgeQaSearchRpcService.search(requestVo);
|
||||
|
||||
log.info("Knowledge Q&A search result, responseVo={}", JSON.toJSONString(responseVo));
|
||||
|
||||
// Assert - 验证结果
|
||||
assertNotNull(responseVo);
|
||||
assertNotNull(responseVo.getQaVoList());
|
||||
log.info("Q&A result count: {}", responseVo.getQaVoList().size());
|
||||
|
||||
// 打印详细结果,特别是验证docId字段
|
||||
if (responseVo.getQaVoList() != null && !responseVo.getQaVoList().isEmpty()) {
|
||||
responseVo.getQaVoList().forEach(qaVo -> {
|
||||
log.info("Q&A detail - qaId: {}, kbId: {}, docId: {}, question: {}, answer: {}, score: {}",
|
||||
qaVo.getQaId(), qaVo.getKbId(), qaVo.getDocId(),
|
||||
qaVo.getQuestion(), qaVo.getAnswer(), qaVo.getScore());
|
||||
});
|
||||
}
|
||||
|
||||
} finally {
|
||||
RequestContext.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试知识库问答搜索功能 - 测试空结果
|
||||
*/
|
||||
@Test
|
||||
public void testQaSearchWithEmptyResult() {
|
||||
try {
|
||||
RequestContext.setThreadTenantId(1L);
|
||||
|
||||
// Arrange - 使用不存在的问题
|
||||
KnowledgeQaRequestVo requestVo = new KnowledgeQaRequestVo();
|
||||
requestVo.setKbId(41L); // 知识库ID
|
||||
requestVo.setQuestion("一个不存在的问题 xyzabc123"); // 不存在的问题
|
||||
requestVo.setTopK(5); // 返回前5个结果
|
||||
requestVo.setIgnoreDocStatus(false); // 不忽略文档状态
|
||||
requestVo.setIgnoreTenantId(false); // 不忽略租户ID
|
||||
|
||||
// Act - 执行问答搜索
|
||||
KnowledgeQaResponseVo responseVo = knowledgeQaSearchRpcService.search(requestVo);
|
||||
|
||||
log.info("Knowledge Q&A search (empty), responseVo={}", JSON.toJSONString(responseVo));
|
||||
|
||||
// Assert - 验证结果
|
||||
assertNotNull(responseVo);
|
||||
assertNotNull(responseVo.getQaVoList());
|
||||
log.info("Q&A result count: {}", responseVo.getQaVoList().size());
|
||||
|
||||
} finally {
|
||||
RequestContext.remove();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
package com.xspaceagi.knowledge;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import com.xspaceagi.system.spec.common.RequestContext;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
import com.xspaceagi.knowledge.sdk.request.KnowledgeFullTextSearchRequestVo;
|
||||
import com.xspaceagi.knowledge.sdk.response.KnowledgeFullTextSearchResponseVo;
|
||||
import com.xspaceagi.knowledge.sdk.sevice.IKnowledgeFullTextSearchRpcService;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 知识库全文检索 RPC 服务测试
|
||||
*
|
||||
* @author system
|
||||
* @date 2025-03-31
|
||||
*/
|
||||
@Slf4j
|
||||
@SpringBootTest
|
||||
public class KnowledgeFullTextSearchRpcServiceTest {
|
||||
|
||||
@Resource
|
||||
private IKnowledgeFullTextSearchRpcService knowledgeFullTextSearchRpcService;
|
||||
|
||||
/**
|
||||
* 测试知识库全文检索功能 - 单知识库检索
|
||||
*/
|
||||
@Test
|
||||
public void testFullTextSearch() {
|
||||
try {
|
||||
RequestContext.setThreadTenantId(1L);
|
||||
|
||||
// Arrange
|
||||
KnowledgeFullTextSearchRequestVo requestVo = new KnowledgeFullTextSearchRequestVo();
|
||||
requestVo.setKbIds(java.util.Arrays.asList(411L));
|
||||
requestVo.setTenantId(1L);
|
||||
requestVo.setQueryText("智能体");
|
||||
requestVo.setTopK(10);
|
||||
|
||||
log.info("Start full-text search test: kbIds={}, queryText={}, topK={}",
|
||||
requestVo.getKbIds(), requestVo.getQueryText(), requestVo.getTopK());
|
||||
|
||||
// Act
|
||||
KnowledgeFullTextSearchResponseVo responseVo = knowledgeFullTextSearchRpcService.search(requestVo);
|
||||
|
||||
// Assert
|
||||
assertNotNull(responseVo, "返回结果不能为空");
|
||||
assertNotNull(responseVo.getResults(), "结果列表不能为空");
|
||||
assertTrue(responseVo.getCostTimeMs() >= 0, "耗时应该大于等于0");
|
||||
|
||||
log.info("\n\n========================================");
|
||||
log.info("=== Full-text search test result ===");
|
||||
log.info("========================================");
|
||||
log.info("Retrieval result count: {}", responseVo.getResults().size());
|
||||
log.info("Elapsed: {}ms", responseVo.getCostTimeMs());
|
||||
log.info("========================================\n");
|
||||
|
||||
// 打印前3个结果详情
|
||||
if (!responseVo.getResults().isEmpty()) {
|
||||
log.info("\n=== Top 3 results ===");
|
||||
int index = 1;
|
||||
for (var result : responseVo.getResults().stream().limit(3).toList()) {
|
||||
String textPreview = result.getRawText() != null && result.getRawText().length() > 100
|
||||
? result.getRawText().substring(0, 100) + "..."
|
||||
: result.getRawText();
|
||||
|
||||
log.info("\n[Result #{}]", index++);
|
||||
log.info(" rawSegmentId: {}", result.getRawSegmentId());
|
||||
log.info(" docId: {}", result.getDocId());
|
||||
log.info(" kbId: {}", result.getKbId());
|
||||
log.info(" BM25 score: {}", result.getScore());
|
||||
log.info(" sortIndex: {}", result.getSortIndex());
|
||||
log.info(" documentName: {}", result.getDocumentName());
|
||||
log.info(" Text: {}", textPreview);
|
||||
}
|
||||
log.info("\n========================================\n");
|
||||
} else {
|
||||
log.warn("\n⚠️ No matching results found.\n");
|
||||
}
|
||||
|
||||
} finally {
|
||||
RequestContext.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试全文检索 - 多知识库检索
|
||||
*/
|
||||
@Test
|
||||
public void testFullTextSearchMultipleKbs() {
|
||||
try {
|
||||
RequestContext.setThreadTenantId(1L);
|
||||
|
||||
// Arrange - 测试多个知识库
|
||||
KnowledgeFullTextSearchRequestVo requestVo = new KnowledgeFullTextSearchRequestVo();
|
||||
requestVo.setKbIds(java.util.Arrays.asList(200L, 113L)); // 多个知识库
|
||||
requestVo.setTenantId(1L);
|
||||
requestVo.setQueryText("Python");
|
||||
requestVo.setTopK(5);
|
||||
|
||||
log.info("Start multi-KB full-text test: kbIds={}, queryText={}, topK={}",
|
||||
requestVo.getKbIds(), requestVo.getQueryText(), requestVo.getTopK());
|
||||
|
||||
// Act
|
||||
KnowledgeFullTextSearchResponseVo responseVo = knowledgeFullTextSearchRpcService.search(requestVo);
|
||||
|
||||
// Assert
|
||||
assertNotNull(responseVo);
|
||||
assertNotNull(responseVo.getResults());
|
||||
|
||||
log.info("\n\n========================================");
|
||||
log.info("=== Multi-KB search test result ===");
|
||||
log.info("========================================");
|
||||
log.info("Retrieval result count: {}", responseVo.getResults().size());
|
||||
log.info("Elapsed: {}ms", responseVo.getCostTimeMs());
|
||||
|
||||
// 统计各知识库的结果数量
|
||||
if (!responseVo.getResults().isEmpty()) {
|
||||
java.util.Map<Long, Long> kbCountMap = responseVo.getResults().stream()
|
||||
.collect(java.util.stream.Collectors.groupingBy(
|
||||
result -> result.getKbId(),
|
||||
java.util.stream.Collectors.counting()
|
||||
));
|
||||
|
||||
log.info("\n=== Per-KB result distribution ===");
|
||||
kbCountMap.forEach((kbId, count) ->
|
||||
log.info("Result count for kbId {}: {}", kbId, count));
|
||||
log.info("========================================\n");
|
||||
} else {
|
||||
log.warn("\n⚠️ No matching results found.\n");
|
||||
}
|
||||
|
||||
} finally {
|
||||
RequestContext.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试全文检索 - 指定文档ID检索
|
||||
*/
|
||||
@Test
|
||||
public void testFullTextSearchWithDocIds() {
|
||||
try {
|
||||
RequestContext.setThreadTenantId(1L);
|
||||
|
||||
// Arrange - 测试指定文档ID检索
|
||||
KnowledgeFullTextSearchRequestVo requestVo = new KnowledgeFullTextSearchRequestVo();
|
||||
requestVo.setKbIds(java.util.Arrays.asList(200L));
|
||||
requestVo.setTenantId(1L);
|
||||
requestVo.setQueryText("Python");
|
||||
requestVo.setTopK(10);
|
||||
requestVo.setDocIds(java.util.Arrays.asList(1L, 2L, 3L)); // 指定文档ID
|
||||
|
||||
log.info("Start specified-doc search test: kbIds={}, docIds={}, queryText={}",
|
||||
requestVo.getKbIds(), requestVo.getDocIds(), requestVo.getQueryText());
|
||||
|
||||
// Act
|
||||
KnowledgeFullTextSearchResponseVo responseVo = knowledgeFullTextSearchRpcService.search(requestVo);
|
||||
|
||||
// Assert
|
||||
assertNotNull(responseVo);
|
||||
assertNotNull(responseVo.getResults());
|
||||
|
||||
log.info("\n\n========================================");
|
||||
log.info("=== Specified-doc search test result ===");
|
||||
log.info("========================================");
|
||||
log.info("Retrieval result count: {}", responseVo.getResults().size());
|
||||
log.info("Elapsed: {}ms", responseVo.getCostTimeMs());
|
||||
|
||||
// 验证所有结果都来自指定的文档
|
||||
if (!responseVo.getResults().isEmpty()) {
|
||||
boolean allFromSpecifiedDocs = responseVo.getResults().stream()
|
||||
.allMatch(result -> requestVo.getDocIds().contains(result.getDocId()));
|
||||
|
||||
log.info("\nAll results from specified docs: {}", allFromSpecifiedDocs);
|
||||
log.info("========================================\n");
|
||||
} else {
|
||||
log.warn("\n⚠️ No matching results found.\n");
|
||||
}
|
||||
|
||||
} finally {
|
||||
RequestContext.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试全文检索 - 空结果场景
|
||||
*/
|
||||
@Test
|
||||
public void testFullTextSearchWithNoResults() {
|
||||
try {
|
||||
RequestContext.setThreadTenantId(1L);
|
||||
|
||||
// Arrange - 使用不存在的知识库ID
|
||||
KnowledgeFullTextSearchRequestVo requestVo = new KnowledgeFullTextSearchRequestVo();
|
||||
requestVo.setKbIds(java.util.Arrays.asList(999999L));
|
||||
requestVo.setTenantId(1L);
|
||||
requestVo.setQueryText("不存在的内容XYZABC123");
|
||||
requestVo.setTopK(10);
|
||||
|
||||
log.info("Start empty-result test: kbIds={}, queryText={}",
|
||||
requestVo.getKbIds(), requestVo.getQueryText());
|
||||
|
||||
// Act
|
||||
KnowledgeFullTextSearchResponseVo responseVo = knowledgeFullTextSearchRpcService.search(requestVo);
|
||||
|
||||
// Assert
|
||||
assertNotNull(responseVo);
|
||||
assertNotNull(responseVo.getResults());
|
||||
|
||||
log.info("\n\n========================================");
|
||||
log.info("=== Empty result test ===");
|
||||
log.info("========================================");
|
||||
log.info("Retrieval result count: {}", responseVo.getResults().size());
|
||||
log.info("Elapsed: {}ms", responseVo.getCostTimeMs());
|
||||
log.info("========================================\n");
|
||||
|
||||
} finally {
|
||||
RequestContext.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试全文检索 - 不同TopK值
|
||||
*/
|
||||
@Test
|
||||
public void testFullTextSearchWithDifferentTopK() {
|
||||
try {
|
||||
RequestContext.setThreadTenantId(1L);
|
||||
|
||||
int[] topKValues = {1, 5, 10, 20};
|
||||
|
||||
for (int topK : topKValues) {
|
||||
// Arrange
|
||||
KnowledgeFullTextSearchRequestVo requestVo = new KnowledgeFullTextSearchRequestVo();
|
||||
requestVo.setKbIds(java.util.Arrays.asList(200L));
|
||||
requestVo.setTenantId(1L);
|
||||
requestVo.setQueryText("Python");
|
||||
requestVo.setTopK(topK);
|
||||
|
||||
log.info("Test TopK={}: kbIds={}, queryText={}",
|
||||
topK, requestVo.getKbIds(), requestVo.getQueryText());
|
||||
|
||||
// Act
|
||||
KnowledgeFullTextSearchResponseVo responseVo = knowledgeFullTextSearchRpcService.search(requestVo);
|
||||
|
||||
// Assert
|
||||
assertNotNull(responseVo);
|
||||
assertNotNull(responseVo.getResults());
|
||||
assertTrue(responseVo.getResults().size() <= topK,
|
||||
"返回结果数量不应超过TopK值");
|
||||
|
||||
log.info("TopK={}: returned {} rows, {}ms",
|
||||
topK, responseVo.getResults().size(), responseVo.getCostTimeMs());
|
||||
}
|
||||
|
||||
} finally {
|
||||
RequestContext.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
16
qiming-backend/app-platform-bootstrap/pom.xml
Normal file
16
qiming-backend/app-platform-bootstrap/pom.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?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>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
</parent>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-bootstrap</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<modules>
|
||||
<module>app-platform-web-bootstrap</module>
|
||||
</modules>
|
||||
</project>
|
||||
Reference in New Issue
Block a user