chore: initialize qiming workspace repository

This commit is contained in:
Codex
2026-05-29 14:22:48 +08:00
commit bfd67a0f2c
10750 changed files with 1885711 additions and 0 deletions

View File

@@ -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>

View File

@@ -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
}
}

View File

@@ -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 = "用户IDscope为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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}
});
}
}