chore: initialize qiming workspace repository
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
<?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-sandbox</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
</parent>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-sandbox-application</artifactId>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-sandbox-spec</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-sandbox-infra</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>app-platform-sandbox-domain</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>system-spec</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xspaceagi</groupId>
|
||||
<artifactId>system-application</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.xspaceagi.sandbox.application.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class SandboxBindInfoDto {
|
||||
|
||||
private BindTargetType targetType;
|
||||
private Long targetId;
|
||||
private String targetName;
|
||||
|
||||
public enum BindTargetType {
|
||||
User,
|
||||
Space
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.xspaceagi.sandbox.application.dto;
|
||||
|
||||
import com.xspaceagi.sandbox.infra.dao.vo.SandboxConfigValue;
|
||||
import com.xspaceagi.sandbox.infra.dao.vo.SandboxServerInfo;
|
||||
import com.xspaceagi.sandbox.spec.enums.SandboxScopeEnum;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 沙盒配置 DTO
|
||||
*/
|
||||
@Schema(description = "沙盒配置信息")
|
||||
@Data
|
||||
public class SandboxConfigDto {
|
||||
|
||||
@Schema(description = "配置ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "配置范围:global-全局配置 user-个人配置")
|
||||
private SandboxScopeEnum scope;
|
||||
|
||||
@Schema(description = "用户ID(scope为user时必填)")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "配置名称")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "唯一标识,用户智能体电脑连接有用")
|
||||
private String configKey;
|
||||
|
||||
@Schema(description = "配置值")
|
||||
private SandboxConfigValue configValue;
|
||||
|
||||
@Schema(description = "沙盒服务端信息(个人智能体电脑专有)")
|
||||
private SandboxServerInfo serverInfo;
|
||||
|
||||
@Schema(description = "配置描述")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "是否启用:true-启用 false-禁用")
|
||||
private Boolean isActive;
|
||||
|
||||
@Schema(description = "是否在线:true-在线 false-离线")
|
||||
private boolean online;
|
||||
|
||||
@Schema(description = "正在使用的用户数")
|
||||
private int usingCount;
|
||||
|
||||
@Schema(description = "最大并发执行Agent数量")
|
||||
private Integer maxAgentCount;
|
||||
|
||||
@Schema(description = "穿透服务端地址")
|
||||
private String serverHost;
|
||||
|
||||
@Schema(description = "给智能体电脑分配的agentId")
|
||||
private Long agentId;
|
||||
|
||||
@Schema(description = "会话ID")
|
||||
private Long conversationId;
|
||||
|
||||
@Schema(description = "穿透服务端端口")
|
||||
private int serverPort;
|
||||
|
||||
@Schema(description = "会话密钥,仅首次注册时有效")
|
||||
private String token;
|
||||
|
||||
@Schema(description = "沙盒类型:智能体 Agent;应用开发 PageApp")
|
||||
private String type;
|
||||
|
||||
@Schema(description = "绑定信息")
|
||||
private List<SandboxBindInfoDto> bindItems;
|
||||
|
||||
@Schema(description = "沙盒隔离:Tenant-租户维度隔离;Space-空间维度隔离;Project-项目维度隔离")
|
||||
private String isolation;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private Date created;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
private Date modified;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.xspaceagi.sandbox.application.dto;
|
||||
|
||||
import com.xspaceagi.sandbox.spec.enums.SandboxScopeEnum;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 沙盒配置查询 DTO
|
||||
*/
|
||||
@Schema(description = "沙盒配置查询条件")
|
||||
@Data
|
||||
public class SandboxConfigQueryDto {
|
||||
|
||||
@Schema(description = "配置范围:global-全局配置 user-个人配置")
|
||||
private SandboxScopeEnum scope;
|
||||
|
||||
@Schema(description = "用户ID")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "配置名称(模糊查询)")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "是否启用")
|
||||
private Boolean isActive;
|
||||
|
||||
@Schema(description = "页码", example = "1")
|
||||
private Integer pageNum = 1;
|
||||
|
||||
@Schema(description = "每页大小", example = "10")
|
||||
private Integer pageSize = 10;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.xspaceagi.sandbox.application.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 沙盒全局配置 DTO
|
||||
*/
|
||||
@Schema(description = "沙盒全局配置")
|
||||
@Data
|
||||
public class SandboxGlobalConfigDto {
|
||||
|
||||
@Schema(description = "每个用户分配的内存大小")
|
||||
private String perUserMemoryGB;
|
||||
|
||||
@Schema(description = "每个用户分配的CPU核数")
|
||||
private String perUserCpuCores;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.xspaceagi.sandbox.application.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 临时代理 DTO
|
||||
*/
|
||||
@Schema(description = "临时代理信息")
|
||||
@Data
|
||||
public class SandboxProxyDto {
|
||||
|
||||
@Schema(description = "代理ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "租户ID")
|
||||
private Long tenantId;
|
||||
|
||||
@Schema(description = "用户ID")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "沙盒ID")
|
||||
private Long sandboxId;
|
||||
|
||||
@Schema(description = "代理键")
|
||||
private String proxyKey;
|
||||
|
||||
@Schema(description = "后端主机地址")
|
||||
private String backendHost;
|
||||
|
||||
@Schema(description = "后端端口")
|
||||
private Integer backendPort;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private Date created;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
private Date modified;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.xspaceagi.sandbox.application.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.xspaceagi.sandbox.application.dto.SandboxConfigDto;
|
||||
import com.xspaceagi.sandbox.application.dto.SandboxConfigQueryDto;
|
||||
import com.xspaceagi.sandbox.application.dto.SandboxGlobalConfigDto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 沙盒配置应用服务接口
|
||||
*/
|
||||
public interface SandboxConfigApplicationService {
|
||||
|
||||
/**
|
||||
* 分页查询配置列表
|
||||
*
|
||||
* @param queryDto 查询条件
|
||||
* @return 分页结果
|
||||
*/
|
||||
Page<SandboxConfigDto> pageQuery(SandboxConfigQueryDto queryDto);
|
||||
|
||||
/**
|
||||
* 查询配置详情
|
||||
*
|
||||
* @param id 配置ID
|
||||
* @return 配置详情
|
||||
*/
|
||||
SandboxConfigDto getById(Long id);
|
||||
|
||||
/**
|
||||
* 根据配置键查询配置详情
|
||||
*
|
||||
* @param key 配置键
|
||||
* @return 配置详情
|
||||
*/
|
||||
SandboxConfigDto getByKey(String key);
|
||||
|
||||
/**
|
||||
* 查询用户配置列表
|
||||
*
|
||||
* @param userId 用户ID(为空时查询当前用户)
|
||||
* @return 配置列表
|
||||
*/
|
||||
List<SandboxConfigDto> listUserConfigsByType(Long userId);
|
||||
|
||||
/**
|
||||
* 查询全局配置列表
|
||||
*
|
||||
* @return 配置列表
|
||||
*/
|
||||
List<SandboxConfigDto> listGlobalConfigsByType();
|
||||
|
||||
List<SandboxConfigDto> listPageDevelopmentSandboxes();
|
||||
|
||||
boolean hasGlobalConfigsForSelect();
|
||||
|
||||
/**
|
||||
* 创建配置
|
||||
*
|
||||
* @param dto 配置信息
|
||||
*/
|
||||
void create(SandboxConfigDto dto, boolean userAdd);
|
||||
|
||||
/**
|
||||
* 更新配置
|
||||
*
|
||||
* @param dto 配置信息
|
||||
*/
|
||||
void update(SandboxConfigDto dto);
|
||||
|
||||
/**
|
||||
* 删除配置
|
||||
*
|
||||
* @param id 配置ID
|
||||
*/
|
||||
void delete(Long id);
|
||||
|
||||
/**
|
||||
* 启用/禁用配置
|
||||
*
|
||||
* @param id 配置ID
|
||||
*/
|
||||
void toggle(Long id);
|
||||
|
||||
/**
|
||||
* 查询全局配置
|
||||
*
|
||||
* @return 全局配置
|
||||
*/
|
||||
SandboxGlobalConfigDto getGlobalConfig(Long tenantId);
|
||||
|
||||
void updateGlobalConfig(Long tenantId, SandboxGlobalConfigDto dto);
|
||||
|
||||
void testConnection(Long sandboxId);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.xspaceagi.sandbox.application.service;
|
||||
|
||||
import com.xspaceagi.sandbox.application.dto.SandboxProxyDto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 临时代理应用服务接口
|
||||
*/
|
||||
public interface SandboxProxyApplicationService {
|
||||
|
||||
/**
|
||||
* 创建代理配置
|
||||
*
|
||||
* @param dto 代理信息
|
||||
* @return 创建后的代理信息
|
||||
*/
|
||||
SandboxProxyDto create(SandboxProxyDto dto);
|
||||
|
||||
/**
|
||||
* 获取代理列表
|
||||
*
|
||||
* @param userId 用户id
|
||||
* @param sandboxId 沙盒id
|
||||
* @return 代理列表
|
||||
*/
|
||||
List<SandboxProxyDto> querySandboxyProxyList(Long userId, Long sandboxId);
|
||||
|
||||
/**
|
||||
* 删除代理配置
|
||||
*
|
||||
* @param proxyKey 代理key
|
||||
*/
|
||||
void deleteByProxyKey(Long userId, String proxyKey);
|
||||
|
||||
void deleteById(Long userId, Long id);
|
||||
}
|
||||
@@ -0,0 +1,451 @@
|
||||
package com.xspaceagi.sandbox.application.service.impl;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.baomidou.dynamic.datasource.annotation.DSTransactional;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.xspaceagi.agent.core.sdk.IAgentRpcService;
|
||||
import com.xspaceagi.agent.core.sdk.dto.ReqResult;
|
||||
import com.xspaceagi.sandbox.application.dto.SandboxBindInfoDto;
|
||||
import com.xspaceagi.sandbox.application.dto.SandboxConfigDto;
|
||||
import com.xspaceagi.sandbox.application.dto.SandboxConfigQueryDto;
|
||||
import com.xspaceagi.sandbox.application.dto.SandboxGlobalConfigDto;
|
||||
import com.xspaceagi.sandbox.application.service.SandboxConfigApplicationService;
|
||||
import com.xspaceagi.sandbox.infra.config.ReverseServerProperties;
|
||||
import com.xspaceagi.sandbox.infra.dao.entity.SandboxConfig;
|
||||
import com.xspaceagi.sandbox.infra.dao.service.SandboxConfigService;
|
||||
import com.xspaceagi.sandbox.infra.dao.vo.SandboxConfigValue;
|
||||
import com.xspaceagi.sandbox.infra.dao.vo.SandboxServerInfo;
|
||||
import com.xspaceagi.sandbox.infra.network.ReverseServerContainer;
|
||||
import com.xspaceagi.sandbox.spec.enums.SandboxScopeEnum;
|
||||
import com.xspaceagi.system.application.dto.TenantConfigDto;
|
||||
import com.xspaceagi.system.spec.common.RequestContext;
|
||||
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
|
||||
import com.xspaceagi.system.spec.exception.BizException;
|
||||
import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
|
||||
import com.xspaceagi.system.spec.utils.RedisUtil;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.Duration;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 沙盒配置应用服务实现
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class SandboxConfigApplicationServiceImpl implements SandboxConfigApplicationService {
|
||||
|
||||
@Resource
|
||||
private SandboxConfigService sandboxConfigService;
|
||||
|
||||
@Resource
|
||||
private RedisUtil redisUtil;
|
||||
|
||||
@Resource
|
||||
private ReverseServerProperties reverseServerProperties;
|
||||
|
||||
@Resource
|
||||
private ReverseServerContainer reverseServerContainer;
|
||||
|
||||
@Resource
|
||||
private IAgentRpcService iAgentRpcService;
|
||||
|
||||
static {
|
||||
// disable keep alive,暂不使用连接池
|
||||
System.setProperty("jdk.httpclient.keepalive.timeout", "0");
|
||||
}
|
||||
|
||||
private final HttpClient httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(2)).build();
|
||||
|
||||
@Override
|
||||
public Page<SandboxConfigDto> pageQuery(SandboxConfigQueryDto queryDto) {
|
||||
LambdaQueryWrapper<SandboxConfig> queryWrapper = buildQueryWrapper(queryDto);
|
||||
|
||||
Page<SandboxConfig> page = sandboxConfigService.page(
|
||||
new Page<>(queryDto.getPageNum(), queryDto.getPageSize()),
|
||||
queryWrapper
|
||||
);
|
||||
|
||||
return convertDtoPage(page);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SandboxConfigDto getById(Long id) {
|
||||
SandboxConfig entity = sandboxConfigService.getById(id);
|
||||
if (entity == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.configNotFound);
|
||||
}
|
||||
|
||||
return convertToDto(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SandboxConfigDto getByKey(String key) {
|
||||
Assert.hasText(key, "配置key不能为空");
|
||||
LambdaQueryWrapper<SandboxConfig> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(SandboxConfig::getConfigKey, key);
|
||||
SandboxConfig sandboxConfig = sandboxConfigService.getOne(queryWrapper);
|
||||
if (sandboxConfig == null) {
|
||||
return null;
|
||||
}
|
||||
SandboxConfigDto dto = convertToDto(sandboxConfig);
|
||||
String host = reverseServerProperties.getOuter().getHost();
|
||||
if (StringUtils.isBlank(host)) {
|
||||
TenantConfigDto tenantConfig = (TenantConfigDto) RequestContext.get().getTenantConfig();
|
||||
try {
|
||||
host = new URL(tenantConfig.getSiteUrl()).getHost();
|
||||
} catch (MalformedURLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
if (StringUtils.isBlank(host)) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.sandboxClientAddressUnavailable);
|
||||
}
|
||||
//host为多个英文逗号隔开的地址,帮我轮询选择
|
||||
String[] hosts = host.split(",");
|
||||
if (hosts.length == 1) {
|
||||
dto.setServerHost(hosts[0]);
|
||||
} else {
|
||||
Long increment = redisUtil.increment("sandbox:config:host:round:index", 1);
|
||||
dto.setServerHost(hosts[increment.intValue() % hosts.length]);
|
||||
}
|
||||
dto.setServerPort(reverseServerProperties.getOuter().getPort());
|
||||
return dto;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SandboxConfigDto> listUserConfigsByType(Long userId) {
|
||||
List<SandboxConfig> configs = sandboxConfigService.queryUserConfigsByType(userId);
|
||||
return configs.stream()
|
||||
.map(this::convertToDto)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SandboxConfigDto> listGlobalConfigsByType() {
|
||||
List<SandboxConfig> configs = sandboxConfigService.queryGlobalConfigs(null);
|
||||
List<SandboxConfigDto> collect = configs.stream()
|
||||
.map(this::convertToDto)
|
||||
.collect(Collectors.toList());
|
||||
collect.forEach(dto -> {
|
||||
try {
|
||||
int i = totalUsingCount(dto);
|
||||
dto.setUsingCount(i);
|
||||
dto.setOnline(true);
|
||||
} catch (Exception e) {
|
||||
log.warn("获取配置使用数量失败 {}", dto, e);
|
||||
}
|
||||
});
|
||||
return collect;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SandboxConfigDto> listPageDevelopmentSandboxes() {
|
||||
List<SandboxConfig> configs = sandboxConfigService.queryGlobalConfigs(true);
|
||||
configs.removeIf(config -> "Agent".equals(config.getType()));
|
||||
return configs.stream()
|
||||
.map(this::convertToDto)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasGlobalConfigsForSelect() {
|
||||
return !sandboxConfigService.queryGlobalConfigs(true).isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置使用数量(顺带用于测试连通性)
|
||||
*/
|
||||
private int totalUsingCount(SandboxConfigDto sandboxConfigDto) throws Exception {
|
||||
String url = sandboxConfigDto.getConfigValue().getHostWithScheme() + ":" + sandboxConfigDto.getConfigValue().getAgentPort() + "/computer/pod/count";
|
||||
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url))
|
||||
.header("x-api-key", sandboxConfigDto.getConfigValue().getApiKey() == null ? "" : sandboxConfigDto.getConfigValue().getApiKey())
|
||||
.timeout(java.time.Duration.ofSeconds(2))
|
||||
.GET().build();
|
||||
String serverStatusStr = httpClient.send(request, HttpResponse.BodyHandlers.ofString()).body();
|
||||
JSONObject jsonObject = JSON.parseObject(serverStatusStr);
|
||||
JSONObject data = jsonObject.getJSONObject("data");
|
||||
return data.getInteger("total_count");
|
||||
}
|
||||
|
||||
@DSTransactional(rollbackFor = Exception.class)
|
||||
@Override
|
||||
public void create(SandboxConfigDto dto, boolean userAdd) {
|
||||
validateConfigKey(dto);
|
||||
|
||||
SandboxConfig entity = convertToEntity(dto);
|
||||
entity.setCreated(new Date());
|
||||
entity.setModified(new Date());
|
||||
|
||||
// 设置默认值
|
||||
if (entity.getIsActive() == null) {
|
||||
entity.setIsActive(true);
|
||||
}
|
||||
|
||||
// 个人配置自动设置用户ID
|
||||
if (entity.getScope() == SandboxScopeEnum.USER && entity.getUserId() == null) {
|
||||
entity.setUserId(RequestContext.get().getUserId());
|
||||
}
|
||||
sandboxConfigService.save(entity);
|
||||
|
||||
// 创建用户沙盒代理
|
||||
if (entity.getScope() == SandboxScopeEnum.USER) {
|
||||
ReqResult<Long> userSandboxAgent = iAgentRpcService.createUserSandboxAgent(entity.getUserId(), entity.getId(), userAdd ? entity.getName() : null);
|
||||
if (!userSandboxAgent.isSuccess()) {
|
||||
log.error("创建用户沙盒代理失败:{}", userSandboxAgent.getMessage());
|
||||
throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.sandboxUserProxyCreateFailed);
|
||||
}
|
||||
entity.setAgentId(userSandboxAgent.getData());
|
||||
if (userAdd) {
|
||||
entity.setName(entity.getName());
|
||||
} else {
|
||||
entity.setName(entity.getName() + entity.getId());
|
||||
}
|
||||
sandboxConfigService.updateById(entity);
|
||||
}
|
||||
}
|
||||
|
||||
@DSTransactional(rollbackFor = Exception.class)
|
||||
@Override
|
||||
public void update(SandboxConfigDto dto) {
|
||||
if (dto.getId() == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "配置ID");
|
||||
}
|
||||
|
||||
SandboxConfig existingEntity = sandboxConfigService.getById(dto.getId());
|
||||
if (existingEntity == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.configNotFound);
|
||||
}
|
||||
|
||||
// 如果修改了 config_key,需要验证唯一性
|
||||
if (StringUtils.isNotBlank(dto.getConfigKey()) && !dto.getConfigKey().equals(existingEntity.getConfigKey())) {
|
||||
validateConfigKey(dto);
|
||||
}
|
||||
|
||||
SandboxConfig entity = convertToEntity(dto);
|
||||
entity.setId(dto.getId());
|
||||
entity.setModified(new Date());
|
||||
|
||||
sandboxConfigService.updateById(entity);
|
||||
|
||||
if (StringUtils.isNotBlank(entity.getName())) {
|
||||
//更新智能体名称
|
||||
iAgentRpcService.updateUserSandboxAgentName(existingEntity.getAgentId(), existingEntity.getName(), entity.getName());
|
||||
}
|
||||
}
|
||||
|
||||
@DSTransactional(rollbackFor = Exception.class)
|
||||
@Override
|
||||
public void delete(Long id) {
|
||||
SandboxConfig entity = sandboxConfigService.getById(id);
|
||||
if (entity == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.configNotFound);
|
||||
}
|
||||
|
||||
if (entity.getScope() == SandboxScopeEnum.USER) {
|
||||
iAgentRpcService.deleteUserSandboxAgent(entity.getAgentId(), entity.getId());
|
||||
}
|
||||
|
||||
sandboxConfigService.removeById(id);
|
||||
}
|
||||
|
||||
@DSTransactional(rollbackFor = Exception.class)
|
||||
@Override
|
||||
public void toggle(Long id) {
|
||||
SandboxConfig entity = sandboxConfigService.getById(id);
|
||||
if (entity == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.configNotFound);
|
||||
}
|
||||
|
||||
entity.setIsActive(!entity.getIsActive());
|
||||
entity.setModified(new Date());
|
||||
|
||||
sandboxConfigService.updateById(entity);
|
||||
if (entity.getScope() == SandboxScopeEnum.USER && !entity.getIsActive()) {
|
||||
reverseServerContainer.offlineClient(entity.getConfigKey());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public SandboxGlobalConfigDto getGlobalConfig(Long tenantId) {
|
||||
Object o = redisUtil.get("sandbox:global:config:" + tenantId);
|
||||
if (o != null) {
|
||||
try {
|
||||
return JSON.parseObject(o.toString(), SandboxGlobalConfigDto.class);
|
||||
} catch (Exception e) {
|
||||
// 忽略
|
||||
log.warn("反序列化沙盒全局配置失败", e);
|
||||
}
|
||||
}
|
||||
SandboxGlobalConfigDto dto = new SandboxGlobalConfigDto();
|
||||
dto.setPerUserMemoryGB("4");
|
||||
dto.setPerUserCpuCores("2");
|
||||
return dto;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateGlobalConfig(Long tenantId, SandboxGlobalConfigDto dto) {
|
||||
try {
|
||||
redisUtil.set("sandbox:global:config:" + tenantId, JSON.toJSONString(dto));
|
||||
} catch (Exception e) {
|
||||
// 忽略
|
||||
log.warn("序列化沙盒全局配置失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void testConnection(Long sandboxId) {
|
||||
SandboxConfigDto byId = getById(sandboxId);
|
||||
if (byId == null) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.configNotFound);
|
||||
}
|
||||
try {
|
||||
totalUsingCount(byId);
|
||||
} catch (Exception e) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.sandboxDeployFailedWithHelp,
|
||||
(e.getMessage() != null ? e.getMessage() : "") + "\n点击查看 <a target=\"_blank\" href=\"https://qiming.com/agent-computer-deploy.html#%E5%85%AD%E3%80%81%E6%95%85%E9%9A%9C%E6%8E%92%E6%9F%A5\">常见问题</a>");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建查询条件
|
||||
*/
|
||||
private LambdaQueryWrapper<SandboxConfig> buildQueryWrapper(SandboxConfigQueryDto queryDto) {
|
||||
LambdaQueryWrapper<SandboxConfig> queryWrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
if (queryDto.getScope() != null) {
|
||||
queryWrapper.eq(SandboxConfig::getScope, queryDto.getScope());
|
||||
}
|
||||
|
||||
if (queryDto.getUserId() != null) {
|
||||
queryWrapper.eq(SandboxConfig::getUserId, queryDto.getUserId());
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(queryDto.getName())) {
|
||||
queryWrapper.like(SandboxConfig::getName, queryDto.getName());
|
||||
}
|
||||
|
||||
if (queryDto.getIsActive() != null) {
|
||||
queryWrapper.eq(SandboxConfig::getIsActive, queryDto.getIsActive());
|
||||
}
|
||||
|
||||
queryWrapper.orderByAsc(SandboxConfig::getId);
|
||||
|
||||
return queryWrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证配置键唯一性
|
||||
*/
|
||||
private void validateConfigKey(SandboxConfigDto dto) {
|
||||
if (StringUtils.isBlank(dto.getConfigKey())) {
|
||||
return;
|
||||
}
|
||||
|
||||
LambdaQueryWrapper<SandboxConfig> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(SandboxConfig::getScope, dto.getScope())
|
||||
.eq(SandboxConfig::getConfigKey, dto.getConfigKey());
|
||||
|
||||
if (dto.getScope() == SandboxScopeEnum.USER) {
|
||||
Long userId = dto.getUserId() != null ? dto.getUserId() : RequestContext.get().getUserId();
|
||||
queryWrapper.eq(SandboxConfig::getUserId, userId);
|
||||
}
|
||||
|
||||
if (dto.getId() != null) {
|
||||
queryWrapper.ne(SandboxConfig::getId, dto.getId());
|
||||
}
|
||||
|
||||
long count = sandboxConfigService.count(queryWrapper);
|
||||
if (count > 0) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.sandboxConfigKeyDuplicate);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为 DTO
|
||||
*/
|
||||
private SandboxConfigDto convertToDto(SandboxConfig entity) {
|
||||
SandboxConfigDto dto = new SandboxConfigDto();
|
||||
BeanUtils.copyProperties(entity, dto);
|
||||
// 将 JSON 字符串转换为对象
|
||||
if (StringUtils.isNotBlank(entity.getConfigValue())) {
|
||||
try {
|
||||
dto.setConfigValue(JSON.parseObject(entity.getConfigValue(), SandboxConfigValue.class));
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to parse config value", e);
|
||||
}
|
||||
}
|
||||
if (StringUtils.isNotBlank(entity.getServerInfo())) {
|
||||
try {
|
||||
dto.setServerInfo(JSON.parseObject(entity.getServerInfo(), SandboxServerInfo.class));
|
||||
} catch (Exception e) {
|
||||
log.error("解析服务端信息失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (entity.getBindInfo() != null) {
|
||||
try {
|
||||
dto.setBindItems(JSON.parseArray(entity.getBindInfo(), SandboxBindInfoDto.class));
|
||||
} catch (Exception e) {
|
||||
log.warn("关系绑定数据解析失败 {}", entity.getBindInfo());
|
||||
}
|
||||
}
|
||||
|
||||
if (entity.getScope() == SandboxScopeEnum.USER) {
|
||||
dto.setOnline(reverseServerContainer.getUserSandboxAliveTime(entity.getConfigKey()) != null);
|
||||
}
|
||||
return dto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为实体
|
||||
*/
|
||||
private SandboxConfig convertToEntity(SandboxConfigDto dto) {
|
||||
SandboxConfig entity = new SandboxConfig();
|
||||
BeanUtils.copyProperties(dto, entity);
|
||||
// 将对象转换为 JSON 字符串
|
||||
if (dto.getConfigValue() != null) {
|
||||
try {
|
||||
entity.setConfigValue(JSON.toJSONString(dto.getConfigValue()));
|
||||
} catch (Exception e) {
|
||||
log.error("序列化配置值失败", e);
|
||||
}
|
||||
}
|
||||
if (dto.getBindItems() != null) {
|
||||
try {
|
||||
entity.setBindInfo(JSON.toJSONString(dto.getBindItems()));
|
||||
} catch (Exception e) {
|
||||
log.warn("关系绑定数据转换失败 {}", dto.getBindItems());
|
||||
}
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换分页结果
|
||||
*/
|
||||
private Page<SandboxConfigDto> convertDtoPage(Page<SandboxConfig> page) {
|
||||
Page<SandboxConfigDto> dtoPage = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
|
||||
List<SandboxConfigDto> dtoList = page.getRecords().stream()
|
||||
.map(this::convertToDto)
|
||||
.collect(Collectors.toList());
|
||||
dtoPage.setRecords(dtoList);
|
||||
return dtoPage;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.xspaceagi.sandbox.application.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.xspaceagi.sandbox.application.dto.SandboxProxyDto;
|
||||
import com.xspaceagi.sandbox.application.service.SandboxProxyApplicationService;
|
||||
import com.xspaceagi.sandbox.infra.dao.entity.SandboxProxy;
|
||||
import com.xspaceagi.sandbox.infra.dao.service.SandboxProxyService;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 临时代理应用服务实现
|
||||
*/
|
||||
@Service
|
||||
public class SandboxProxyApplicationServiceImpl implements SandboxProxyApplicationService {
|
||||
|
||||
@Resource
|
||||
private SandboxProxyService sandboxProxyService;
|
||||
|
||||
|
||||
@Override
|
||||
public SandboxProxyDto create(SandboxProxyDto dto) {
|
||||
Assert.notNull(dto.getUserId(), "用户ID不能为空");
|
||||
Assert.notNull(dto.getSandboxId(), "沙盒ID不能为空");
|
||||
Assert.notNull(dto.getBackendHost(), "后端主机地址不能为空");
|
||||
Assert.notNull(dto.getBackendPort(), "后端端口不能为空");
|
||||
|
||||
SandboxProxy sandboxProxy = new SandboxProxy();
|
||||
sandboxProxy.setUserId(dto.getUserId());
|
||||
sandboxProxy.setSandboxId(dto.getSandboxId());
|
||||
sandboxProxy.setProxyKey(UUID.randomUUID().toString().replace("-", ""));
|
||||
sandboxProxy.setBackendHost(dto.getBackendHost());
|
||||
sandboxProxy.setBackendPort(dto.getBackendPort());
|
||||
|
||||
sandboxProxyService.save(sandboxProxy);
|
||||
return toDto(sandboxProxy);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SandboxProxyDto> querySandboxyProxyList(Long userId, Long sandboxId) {
|
||||
return sandboxProxyService.listByUserIdAndSandboxId(userId, sandboxId).stream().map(this::toDto).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteByProxyKey(Long userId, String proxyKey) {
|
||||
sandboxProxyService.removeByProxyKey(userId, proxyKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteById(Long userId, Long id) {
|
||||
QueryWrapper<SandboxProxy> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("id", id);
|
||||
queryWrapper.eq("user_id", userId);
|
||||
sandboxProxyService.remove(queryWrapper);
|
||||
}
|
||||
|
||||
private SandboxProxyDto toDto(SandboxProxy sandboxProxy) {
|
||||
if (sandboxProxy == null) {
|
||||
return null;
|
||||
}
|
||||
SandboxProxyDto dto = new SandboxProxyDto();
|
||||
BeanUtils.copyProperties(sandboxProxy, dto);
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.xspaceagi.sandbox.application.task;
|
||||
|
||||
import com.xspaceagi.agent.core.sdk.IConversationRpcService;
|
||||
import com.xspaceagi.agent.core.sdk.dto.ConversationRpcDto;
|
||||
import com.xspaceagi.sandbox.infra.dao.entity.SandboxConfig;
|
||||
import com.xspaceagi.sandbox.infra.dao.service.SandboxConfigService;
|
||||
import com.xspaceagi.sandbox.infra.network.ReverseServerContainer;
|
||||
import com.xspaceagi.system.sdk.service.ScheduleTaskApiService;
|
||||
import com.xspaceagi.system.sdk.service.TaskExecuteService;
|
||||
import com.xspaceagi.system.sdk.service.dto.ScheduleTaskDto;
|
||||
import com.xspaceagi.system.spec.tenant.thread.TenantFunctions;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@Service("userSandboxAgentAliveCheckTask")
|
||||
public class UserSandboxAgentAliveCheckTask implements TaskExecuteService {
|
||||
|
||||
@Resource
|
||||
private ScheduleTaskApiService scheduleTaskApiService;
|
||||
|
||||
@Resource
|
||||
private SandboxConfigService sandboxConfigService;
|
||||
|
||||
@Resource
|
||||
private IConversationRpcService iConversationRpcService;
|
||||
|
||||
@Resource
|
||||
private ReverseServerContainer reverseServerContainer;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
scheduleTaskApiService.start(ScheduleTaskDto.builder()
|
||||
.taskId("userSandboxAgentAliveCheckTask")
|
||||
.beanId("userSandboxAgentAliveCheckTask")
|
||||
.maxExecTimes(Long.MAX_VALUE)
|
||||
.cron(ScheduleTaskDto.Cron.EVERY_5_MINUTE.getCron())
|
||||
.params(Map.of())
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Boolean> asyncExecute(ScheduleTaskDto scheduleTask) {
|
||||
TenantFunctions.runWithIgnoreCheck(this::execute);
|
||||
return Mono.just(false);
|
||||
}
|
||||
|
||||
private void execute() {
|
||||
List<SandboxConfig> sandboxConfigs = sandboxConfigService.queryGlobalConfigs(true);
|
||||
// 获取ID列表
|
||||
List<Long> sandboxIds = sandboxConfigs.stream().map(SandboxConfig::getId).toList();
|
||||
List<ConversationRpcDto> conversationRpcDtos = iConversationRpcService.queryLatestSandboxConversationList(sandboxIds);
|
||||
log.debug("查询最新会话 {}", conversationRpcDtos);
|
||||
//获取sandboxServerIds,并去重
|
||||
List<Long> sandboxServerIds = conversationRpcDtos.stream().map(ConversationRpcDto::getSandboxServerId).distinct().toList();
|
||||
if (CollectionUtils.isEmpty(sandboxServerIds)) {
|
||||
return;
|
||||
}
|
||||
sandboxServerIds.forEach(sandboxServerId -> {
|
||||
SandboxConfig sandboxConfig = sandboxConfigService.querySandboxConfigById(sandboxServerId);
|
||||
if (sandboxConfig.getIsActive() != null && !sandboxConfig.getIsActive()) {
|
||||
log.debug("sandboxServerId {} 已停用", sandboxServerId);
|
||||
return;
|
||||
}
|
||||
if (reverseServerContainer.getUserSandboxAliveTime(sandboxConfig.getConfigKey()) == null) {
|
||||
log.debug("sandboxServerId {} 不在线", sandboxServerId);
|
||||
return;//不在线
|
||||
}
|
||||
List<ConversationRpcDto> sandboxAliveConversations = iConversationRpcService.queryLatestConversationListBySandboxId(sandboxServerId);
|
||||
log.debug("查询 {} 的最新会话 {}", sandboxServerId, sandboxAliveConversations);
|
||||
long now = System.currentTimeMillis();
|
||||
//24小时以上没有使用的会话直接停止
|
||||
sandboxAliveConversations.forEach(conversationRpcDto -> {
|
||||
if (now - conversationRpcDto.getModified().getTime() > 1000 * 60 * 60 * 24) {
|
||||
try {
|
||||
log.debug("停止代理 {}", conversationRpcDto);
|
||||
iConversationRpcService.stopAgent(conversationRpcDto.getAgentId(), conversationRpcDto.getId());
|
||||
log.debug("停止代理成功 {}", conversationRpcDto);
|
||||
} catch (Exception e) {
|
||||
log.warn("停止代理异常 {}, {}", conversationRpcDto, e.getMessage());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
sandboxAliveConversations.removeIf(conversationRpcDto -> now - conversationRpcDto.getModified().getTime() > 1000 * 60 * 60 * 24);
|
||||
if (sandboxConfig.getMaxAgentCount() == null || sandboxAliveConversations.size() <= sandboxConfig.getMaxAgentCount()) {
|
||||
return;//没有超过最大代理数
|
||||
}
|
||||
//停止超过最大代理数的代理,按照时间最旧的停止
|
||||
sandboxAliveConversations.sort(Comparator.comparing(ConversationRpcDto::getModified).reversed());
|
||||
for (int i = sandboxConfig.getMaxAgentCount(); i < sandboxAliveConversations.size(); i++) {
|
||||
try {
|
||||
log.debug("停止超过数量的代理 {}", sandboxAliveConversations.get(i));
|
||||
iConversationRpcService.stopAgent(sandboxAliveConversations.get(i).getAgentId(), sandboxAliveConversations.get(i).getId());
|
||||
log.debug("停止超过数量的代理成功 {}", sandboxAliveConversations.get(i));
|
||||
} catch (Exception e) {
|
||||
log.warn("停止超过数量的代理异常 {}, {}", sandboxAliveConversations.get(i), e.getMessage());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user