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,21 @@
<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 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-custom-page</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>app-platform-custom-page-adapter</artifactId>
<name>app-platform-custom-page-adapter</name>
<dependencies>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-custom-page-spec</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-custom-page-sdk</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,25 @@
<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 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-custom-page</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>app-platform-custom-page-application</artifactId>
<name>app-platform-custom-page-application</name>
<dependencies>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-custom-page-infra</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-agent-core-adapter</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-sandbox-sdk</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,74 @@
package com.xspaceagi.custompage.application.service;
import java.util.Map;
import com.xspaceagi.custompage.domain.model.CustomPageBuildModel;
import com.xspaceagi.custompage.sdk.dto.TemplateTypeEnum;
import com.xspaceagi.system.spec.common.UserContext;
import com.xspaceagi.system.spec.dto.ReqResult;
/**
* 前端项目构建应用服务
*/
public interface ICustomPageBuildApplicationService {
/**
* 启动前端开发服务器
*/
ReqResult<Map<String, Object>> startDev(Long projectId, UserContext userContext);
/**
* 构建并发布前端项目
*/
ReqResult<Map<String, Object>> build(Long projectId, String publishType, UserContext userContext);
/**
* 停止前端开发服务器
*/
ReqResult<Map<String, Object>> stopDev(Long projectId, UserContext userContext);
/**
* 重启前端开发服务器
*/
ReqResult<Map<String, Object>> restartDev(Long projectId, UserContext userContext);
/**
* 保活接口
*/
ReqResult<Map<String, Object>> keepAlive(Long projectId, UserContext userContext);
/**
* 根据项目ID查询构建信息
*/
CustomPageBuildModel getByProjectId(Long id);
/**
* 初始化项目工程
*/
ReqResult<Map<String, Object>> initProject(Long projectId, TemplateTypeEnum templateType, UserContext userContext);
/**
* 删除项目文件
*/
ReqResult<Map<String, Object>> deleteProjectFiles(CustomPageBuildModel model, UserContext userContext);
/**
* 查询开发服务器日志
*/
ReqResult<Map<String, Object>> getDevLog(Long projectId, Integer startIndex, String logType, UserContext userContext);
/**
* 复制项目工程
*/
ReqResult<Map<String, Object>> copyProject(Long sourceProjectId, Long targetProjectId, UserContext userContext);
/**
* 获取日志缓存统计
*/
ReqResult<Map<String, Object>> getLogCacheStats();
/**
* 清理所有日志缓存
*/
ReqResult<Map<String, Object>> clearAllLogCache();
}

View File

@@ -0,0 +1,64 @@
package com.xspaceagi.custompage.application.service;
import java.util.List;
import java.util.Map;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import com.xspaceagi.custompage.domain.model.CustomPageConversationModel;
import com.xspaceagi.system.spec.common.UserContext;
import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.spec.page.SuperPage;
/**
* 前端项目编码应用服务
*/
public interface ICustomPageChatApplicationService {
/**
* 发送聊天消息(使用 Flux 响应式流)
*/
reactor.core.publisher.Flux<Map<String, Object>> sendAgentChatFlux(Map<String, Object> chatBody,
UserContext userContext);
/**
* 终止SSE会话
*/
ReqResult<Void> terminateChatSession(String sessionId, UserContext userContext);
/**
* 建立会话 SSE 连接
*/
SseEmitter startAgentSessionSse(String sessionId, Long projectId, String requestId, UserContext userContext);
/**
* 取消 agent 任务
*/
ReqResult<Map<String, Object>> agentSessionCancel(String projectId, String sessionId, UserContext userContext);
/**
* 查询Agent状态
*/
ReqResult<Map<String, Object>> getAgentStatus(String projectId, UserContext userContext);
/**
* 停止Agent服务
*/
ReqResult<Map<String, Object>> stopAgent(String projectId, UserContext userContext);
/**
* 保存用户会话记录
*/
ReqResult<Void> saveConversation(CustomPageConversationModel model, UserContext userContext);
/**
* 查询用户会话记录
*/
ReqResult<List<CustomPageConversationModel>> listConversations(Long projectId, UserContext userContext);
/**
* 分页查询用户会话记录
*/
ReqResult<SuperPage<CustomPageConversationModel>> pageQueryConversations(CustomPageConversationModel queryModel,
Long current, Long pageSize, UserContext userContext);
}

View File

@@ -0,0 +1,43 @@
package com.xspaceagi.custompage.application.service;
import java.util.List;
import java.util.Map;
import org.springframework.web.multipart.MultipartFile;
import com.xspaceagi.custompage.domain.dto.PageFileInfo;
import com.xspaceagi.system.spec.common.UserContext;
import com.xspaceagi.system.spec.dto.ReqResult;
/**
* 前端项目编码应用服务
*/
public interface ICustomPageCodingApplicationService {
/**
* 指定文件修改
*/
ReqResult<Map<String, Object>> specifiedFilesUpdate(Long projectId, List<PageFileInfo> files, UserContext userContext);
/**
* 全量文件修改
*/
ReqResult<Map<String, Object>> allFilesUpdate(Long projectId, List<PageFileInfo> files,
UserContext userContext);
/**
* 上传单个文件
*/
ReqResult<Map<String, Object>> uploadSingleFile(Long projectId, MultipartFile file, String filePath,
UserContext userContext);
/**
* 获取单个文件的反向代理地址
*/
ReqResult<String> getFileProxyUrl(Long projectId, String filePath, UserContext userContext);
/**
* 回滚版本
*/
ReqResult<Map<String, Object>> rollbackVersion(Long projectId, Integer rollbackTo, UserContext userContext);
}

View File

@@ -0,0 +1,133 @@
package com.xspaceagi.custompage.application.service;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
import com.xspaceagi.custompage.sdk.dto.DataSourceDto;
import org.springframework.web.multipart.MultipartFile;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.xspaceagi.custompage.domain.model.CustomPageConfigModel;
import com.xspaceagi.custompage.sdk.dto.PageArgConfig;
import com.xspaceagi.custompage.sdk.dto.ProxyConfig;
import com.xspaceagi.system.spec.common.UserContext;
import com.xspaceagi.system.spec.dto.ReqResult;
/**
* 前端页面配置应用服务
*/
public interface ICustomPageConfigApplicationService {
/**
* 创建用户项目
*/
ReqResult<CustomPageConfigModel> create(CustomPageConfigModel model, UserContext userContext) throws JsonProcessingException;
/**
* 上传项目
*/
ReqResult<Map<String, Object>> uploadProject(CustomPageConfigModel model, MultipartFile file, boolean isInitProject, UserContext userContext) throws Exception;
/**
* 创建反向代理项目
*/
ReqResult<CustomPageConfigModel> createReverseProxyProject(CustomPageConfigModel model, UserContext userContext);
/**
* 查询项目文件内容
*/
ReqResult<Map<String, Object>> queryProjectContent(Long projectId, String proxyPath);
/**
* 查询项目历史版本文件内容
*/
ReqResult<Map<String, Object>> queryProjectContentByVersion(Long projectId, Integer codeVersion, String proxyPath);
/**
* 更新反向代理配置
*/
ReqResult<List<ProxyConfig>> addProxy(Long projectId, ProxyConfig proxyConfig, UserContext userContext);
/**
* 编辑反向代理配置
*/
ReqResult<Void> editProxyConfig(Long projectId, ProxyConfig proxyConfig, UserContext userContext);
/**
* 删除反向代理配置
*/
ReqResult<Void> deleteProxy(Long projectId, String env, String path, UserContext userContext);
/**
* 配置页面参数
*/
ReqResult<Void> savePathArgs(Long projectId, PageArgConfig pageArgConfig, UserContext userContext);
/**
* 添加路径配置
*/
ReqResult<Void> addPath(Long projectId, PageArgConfig pageArgConfig, UserContext userContext);
/**
* 编辑路径配置
*/
ReqResult<Void> editPath(Long projectId, PageArgConfig pageArgConfig, UserContext userContext);
/**
* 删除路径配置
*/
ReqResult<Void> deletePath(Long projectId, String pageUri, UserContext userContext);
/**
* 批量配置反向代理
*/
ReqResult<Void> batchConfigProxy(Long projectId, List<ProxyConfig> proxyConfigs, UserContext userContext);
/**
* 导出项目,已发布的版本
*/
ReqResult<InputStream> exportProjectPublished(Long projectId, UserContext userContext);
/**
* 导出项目,最新的版本
*/
ReqResult<InputStream> exportProjectLatest(Long projectId, UserContext userContext);
/**
* 绑定数据源
*/
ReqResult<Void> bindDataSource(Long projectId, String type, Long dataSourceId, UserContext userContext);
/**
* 解绑数据源
*/
ReqResult<Void> unbindDataSource(Long projectId, String type, Long dataSourceId, UserContext userContext);
/**
* 查询项目
*/
CustomPageConfigModel getByProjectId(Long projectId);
/**
* 批量查询项目
*/
List<CustomPageConfigModel> listByIds(List<Long> ids);
/**
* 修改项目
*/
ReqResult<CustomPageConfigModel> updateProject(CustomPageConfigModel model, UserContext userContext);
/**
* 删除项目
*/
ReqResult<Map<String, Object>> deleteProject(Long projectId, UserContext userContext);
/**
* 复制项目数据源
* 返回新增(复制)的数据源
*/
List<DataSourceDto> copyProjectDataSources(CustomPageConfigModel sourceConfig, CustomPageConfigModel targetConfig, UserContext userContext);
}

View File

@@ -0,0 +1,40 @@
package com.xspaceagi.custompage.application.service;
import com.xspaceagi.custompage.domain.model.CustomPageDomainModel;
import com.xspaceagi.system.spec.common.UserContext;
import com.xspaceagi.system.spec.dto.ReqResult;
import java.util.List;
/**
* 自定义页面域名绑定应用服务接口
*/
public interface ICustomPageDomainApplicationService {
/**
* 根据project_id查询域名列表
*/
List<CustomPageDomainModel> listByProjectId(Long projectId);
/**
* 根据ID查询域名绑定
*/
CustomPageDomainModel getById(Long id);
/**
* 创建域名绑定
*/
ReqResult<CustomPageDomainModel> create(CustomPageDomainModel model, UserContext userContext);
/**
* 更新域名绑定
*/
ReqResult<CustomPageDomainModel> update(CustomPageDomainModel model, UserContext userContext);
/**
* 删除域名绑定
*/
ReqResult<Void> delete(Long id, UserContext userContext);
List<String> listAllDomains();
}

View File

@@ -0,0 +1,17 @@
package com.xspaceagi.custompage.application.service;
import com.xspaceagi.system.spec.common.UserContext;
import org.springframework.http.ResponseEntity;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
/**
* 文件服务
*/
public interface ICustomPageFileApplicationService {
/**
* 获取静态文件
*/
ResponseEntity<StreamingResponseBody> getStaticFile(String requestPath, String staticPrefix, String targetPrefix, String logId, UserContext userContext);
}

View File

@@ -0,0 +1,149 @@
package com.xspaceagi.custompage.application.service.impl;
import java.util.Map;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.xspaceagi.custompage.application.service.ICustomPageBuildApplicationService;
import com.xspaceagi.custompage.domain.model.CustomPageBuildModel;
import com.xspaceagi.custompage.domain.service.ICustomPageBuildDomainService;
import com.xspaceagi.custompage.sdk.dto.TemplateTypeEnum;
import com.xspaceagi.system.spec.common.UserContext;
import com.xspaceagi.system.spec.dto.ReqResult;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
public class CustomPageBuildApplicationServiceImpl implements ICustomPageBuildApplicationService {
@Resource
private ICustomPageBuildDomainService customPageBuildDomainService;
// 不需要事务
// @Transactional(rollbackFor = Exception.class, timeout = 30)
@Override
public ReqResult<Map<String, Object>> startDev(Long projectId, UserContext userContext) {
log.info("[Application] project Id={},start dev server", projectId);
ReqResult<Map<String, Object>> result = customPageBuildDomainService.startDev(projectId, userContext);
log.info("[Application] project Id={},start dev response,result={}", projectId, result);
return result;
}
// 需要开启事务
@Transactional(rollbackFor = Exception.class, timeout = 60)
@Override
public ReqResult<Map<String, Object>> build(Long projectId, String publishType, UserContext userContext) {
log.info("[Application] project Id={},build and publish frontend project", projectId);
ReqResult<Map<String, Object>> result = customPageBuildDomainService.build(projectId, publishType, userContext);
log.info("[Application] project Id={},build and publish frontend projectresponse,result={}", projectId, result);
return result;
}
// 不需要事务
// @Transactional(rollbackFor = Exception.class, timeout = 30)
@Override
public ReqResult<Map<String, Object>> stopDev(Long projectId, UserContext userContext) {
log.info("[Application] project Id={},stop dev server", projectId);
ReqResult<Map<String, Object>> result = customPageBuildDomainService.stopDev(projectId, userContext);
log.info("[Application] project Id={},stop dev serverresponse,result={}", projectId, result);
return result;
}
// 不需要事务
// @Transactional(rollbackFor = Exception.class, timeout = 30)
@Override
public ReqResult<Map<String, Object>> restartDev(Long projectId, UserContext userContext) {
log.info("[Application] project Id={},restart dev server", projectId);
ReqResult<Map<String, Object>> result = customPageBuildDomainService.restartDev(projectId, userContext);
log.info("[Application] project Id={},restart dev serverresponse,result={}", projectId, result);
return result;
}
// 不需要事务
// @Transactional(rollbackFor = Exception.class, timeout = 10)
@Override
public ReqResult<Map<String, Object>> keepAlive(Long projectId, UserContext userContext) {
log.debug("[keep Alive] project Id={},keep-alivehandle", projectId);
ReqResult<Map<String, Object>> result = customPageBuildDomainService.keepAlive(projectId, userContext);
log.info("[keep Alive] project Id={},keep-alivehandleresponse,result={}", projectId, result);
return result;
}
@Override
public CustomPageBuildModel getByProjectId(Long id) {
return customPageBuildDomainService.getByProjectId(id);
}
@Override
public ReqResult<Map<String, Object>> initProject(Long projectId, TemplateTypeEnum templateType, UserContext userContext) {
log.info("[init Project] project Id={},startinitializeproject artifacts", projectId);
ReqResult<Map<String, Object>> result = customPageBuildDomainService.initProject(projectId, templateType);
log.info("[init Project] project Id={},startinitializeprojectresponse,result={}", projectId, result);
return result;
}
@Override
public ReqResult<Map<String, Object>> deleteProjectFiles(CustomPageBuildModel model, UserContext userContext) {
log.info("[delete Project Files] project Id={},startdelete project files", model.getProjectId());
ReqResult<Map<String, Object>> result = customPageBuildDomainService.deleteProjectFiles(model, userContext);
log.info("[delete Project Files] project Id={},delete project filesresponse,result={}", model.getProjectId(), result);
return result;
}
@Override
public ReqResult<Map<String, Object>> getDevLog(Long projectId, Integer startIndex, String logType, UserContext userContext) {
log.debug("[get Dev Log] project Id={}, start Index={}, startquery logs", projectId, startIndex);
ReqResult<Map<String, Object>> result = customPageBuildDomainService.getDevLog(projectId, startIndex, logType, userContext);
log.debug("[get Dev Log] project Id={}, query logsresponse, code={}", projectId, result.getCode());
return result;
}
@Override
public ReqResult<Map<String, Object>> copyProject(Long sourceProjectId, Long targetProjectId, UserContext userContext) {
log.info("[copy Project] source Project Id={},target Project Id={},startcopyproject artifacts", sourceProjectId, targetProjectId);
ReqResult<Map<String, Object>> result = customPageBuildDomainService.copyProject(sourceProjectId, targetProjectId);
log.info("[copy Project] source Project Id={},target Project Id={},copyproject artifactsresponse,result={}", sourceProjectId, targetProjectId, result);
return result;
}
@Override
public ReqResult<Map<String, Object>> getLogCacheStats() {
log.info("[Application] getlogcachestats");
ReqResult<Map<String, Object>> result = customPageBuildDomainService.getLogCacheStats();
log.info("[Application] getlogcachestatsresponse, result={}", result);
return result;
}
@Override
public ReqResult<Map<String, Object>> clearAllLogCache() {
log.info("[Application] clear all log cache");
ReqResult<Map<String, Object>> result = customPageBuildDomainService.clearAllLogCache();
log.info("[Application] clear all log cacheresponse, result={}", result);
return result;
}
}

View File

@@ -0,0 +1,134 @@
package com.xspaceagi.custompage.application.service.impl;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import com.xspaceagi.custompage.application.service.ICustomPageChatApplicationService;
import com.xspaceagi.custompage.domain.model.CustomPageConversationModel;
import com.xspaceagi.custompage.domain.service.ICustomPageChatDomainService;
import com.xspaceagi.custompage.domain.service.ICustomPageChatFluxService;
import com.xspaceagi.custompage.domain.service.ICustomPageConversationDomainService;
import com.xspaceagi.system.spec.common.UserContext;
import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.spec.page.SuperPage;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Flux;
@Slf4j
@Service
public class CustomPageChatApplicationServiceImpl implements ICustomPageChatApplicationService {
@Resource
private ICustomPageChatDomainService customPageChatDomainService;
@Resource
private ICustomPageChatFluxService customPageChatFluxService;
@Resource
private ICustomPageConversationDomainService customPageConversationDomainService;
@Override
@Transactional(rollbackFor = Exception.class)
public ReqResult<Void> saveConversation(CustomPageConversationModel model, UserContext userContext) {
log.info("[Application] project Id={},savesession records", model.getProjectId());
ReqResult<Long> domainResult = customPageConversationDomainService.saveConversation(model, userContext);
if (!domainResult.isSuccess()) {
log.error("[Application] project Id={},savesession records failed,error={}", model.getProjectId(),
domainResult.getMessage());
return ReqResult.error(domainResult.getCode(), domainResult.getMessage());
}
log.info("[Application] project Id={},savesession records succeeded,result={}", model.getProjectId(), domainResult.getData());
return ReqResult.success();
}
@Override
public ReqResult<List<CustomPageConversationModel>> listConversations(Long projectId, UserContext userContext) {
log.info("[Application] project Id={},queryusersession records", projectId);
List<CustomPageConversationModel> modelList = new ArrayList<>();
List<CustomPageConversationModel> models = customPageConversationDomainService
.listByProjectId(projectId, userContext.getUserId());
if (models != null && !models.isEmpty()) {
modelList.addAll(models);
}
log.info("[Application] project Id={},queryusersession recordsreturn size={}", projectId, modelList.size());
return ReqResult.success(modelList);
}
@Override
public ReqResult<SuperPage<CustomPageConversationModel>> pageQueryConversations(
CustomPageConversationModel queryModel, Long current, Long pageSize, UserContext userContext) {
log.info("[Application] project Id={},pagedqueryusersession records, current={}, page Size={}", queryModel.getProjectId(), current,
pageSize);
ReqResult<SuperPage<CustomPageConversationModel>> domainResult = customPageConversationDomainService
.pageQuery(queryModel, current, pageSize, userContext);
if (!domainResult.isSuccess()) {
log.error("[Application] project Id={},pagedqueryusersession records failed,error={}", queryModel.getProjectId(),
domainResult.getMessage());
return ReqResult.error(domainResult.getCode(), domainResult.getMessage());
}
log.info("[Application] project Id={},pagedqueryusersession records succeeded,total={}", queryModel.getProjectId(),
domainResult.getData().getTotal());
return domainResult;
}
@Override
public Flux<Map<String, Object>> sendAgentChatFlux(Map<String, Object> chatBody,
UserContext userContext) {
log.info("[Application] send chat message(Fluxreactive),chat Body keys={}", chatBody == null ? null : chatBody.keySet());
Optional.ofNullable(chatBody).orElseThrow(() -> new IllegalArgumentException("Request body cannot be empty"));
return customPageChatFluxService.sendAgentChatFlux(chatBody, userContext);
}
@Override
public ReqResult<Void> terminateChatSession(String sessionId, UserContext userContext) {
log.info("[Application] terminatesessionrequest: session Id={}", sessionId);
if (sessionId == null || sessionId.trim().isEmpty()) {
return ReqResult.error("0001", "sessionId is required");
}
customPageChatFluxService.terminateSession(sessionId);
return ReqResult.success();
}
@Override
public SseEmitter startAgentSessionSse(String sessionId, Long projectId, String requestId,
UserContext userContext) {
log.info("[Application] establish session SSE, session Id={}, request Id={}", sessionId, requestId);
return customPageChatDomainService.startAgentSessionSse(sessionId, projectId, requestId, userContext);
}
@Override
public ReqResult<Map<String, Object>> agentSessionCancel(String projectId, String sessionId,
UserContext userContext) {
log.info("[Application] project Id={},cancel agent task,session Id={}", projectId, sessionId);
return customPageChatDomainService.agentSessionCancel(projectId, sessionId, userContext);
}
@Override
public ReqResult<Map<String, Object>> getAgentStatus(String projectId, UserContext userContext) {
log.info("[Application] project Id={},query Agentstatus", projectId);
return customPageChatDomainService.getAgentStatus(projectId, userContext);
}
@Override
@Transactional(rollbackFor = Exception.class)
public ReqResult<Map<String, Object>> stopAgent(String projectId, UserContext userContext) {
log.info("[Application] project Id={},stop Agentservice", projectId);
return customPageChatDomainService.stopAgent(projectId, userContext);
}
}

View File

@@ -0,0 +1,127 @@
package com.xspaceagi.custompage.application.service.impl;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import com.xspaceagi.custompage.application.service.ICustomPageCodingApplicationService;
import com.xspaceagi.custompage.domain.dto.PageFileInfo;
import com.xspaceagi.custompage.domain.proxypath.ICustomPageProxyPathService;
import com.xspaceagi.custompage.domain.service.ICustomPageChatDomainService;
import com.xspaceagi.custompage.domain.service.ICustomPageCodingDomainService;
import com.xspaceagi.custompage.domain.service.ICustomPageConversationDomainService;
import com.xspaceagi.system.spec.common.UserContext;
import com.xspaceagi.system.spec.dto.ReqResult;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
public class CustomPageCodingApplicationServiceImpl implements ICustomPageCodingApplicationService {
@Resource
private ICustomPageChatDomainService customPageChatDomainService;
@Resource
private ICustomPageCodingDomainService customPageCodingDomainService;
@Resource
private ICustomPageProxyPathService customPageProxyPathApplicationService;
@Resource
private ICustomPageConversationDomainService customPageConversationDomainService;
@Override
public ReqResult<Map<String, Object>> specifiedFilesUpdate(Long projectId, List<PageFileInfo> files, UserContext userContext) {
log.info("[Application] project Id={},specifiedfileupdate", projectId);
Optional.ofNullable(projectId).filter(x -> x > 0)
.orElseThrow(() -> new IllegalArgumentException("projectId is required or invalid"));
if (files == null || files.isEmpty()) {
throw new IllegalArgumentException("files cannot be empty");
}
ReqResult<Map<String, Object>> result = customPageCodingDomainService.specifiedFilesUpdate(projectId, files,
userContext);
log.info("[Application] project Id={},specifiedfileupdatecompleted,result={}", projectId, result);
return result;
}
// 全量文件修改,不开启事务,如果保活信息更新失败,不要影响版本信息
// @Transactional(rollbackFor = Exception.class)
@Override
public ReqResult<Map<String, Object>> allFilesUpdate(Long projectId, List<PageFileInfo> files,
UserContext userContext) {
log.info("[Application] project Id={},fullfileupdate", projectId);
Optional.ofNullable(projectId).filter(x -> x > 0)
.orElseThrow(() -> new IllegalArgumentException("projectId is required or invalid"));
if (files == null || files.isEmpty()) {
throw new IllegalArgumentException("files cannot be empty");
}
ReqResult<Map<String, Object>> result = customPageCodingDomainService.allFilesUpdate(projectId, files,
userContext);
log.info("[Application] project Id={},fullfileupdatecompleted,result={}", projectId, result);
return result;
}
// 上传文件,不开启事务,如果保活信息更新失败,不要影响版本信息
// @Transactional(rollbackFor = Exception.class)
@Override
public ReqResult<Map<String, Object>> uploadSingleFile(Long projectId, MultipartFile file, String filePath,
UserContext userContext) {
log.info("[Application] project Id={},upload single file,file Path={}", projectId, filePath);
Optional.ofNullable(projectId).filter(x -> x > 0)
.orElseThrow(() -> new IllegalArgumentException("projectId is required or invalid"));
Optional.ofNullable(file).orElseThrow(() -> new IllegalArgumentException("file is required"));
Optional.ofNullable(filePath).filter(x -> !x.trim().isEmpty())
.orElseThrow(() -> new IllegalArgumentException("filePath is required"));
ReqResult<Map<String, Object>> result = customPageCodingDomainService.uploadSingleFile(projectId, file,
filePath, userContext);
log.info("[Application] project Id={},upload single filecompleted,result={}", projectId, result);
return result;
}
@Override
public ReqResult<String> getFileProxyUrl(Long projectId, String filePath, UserContext userContext) {
log.info("[Application] project Id={},getfileproxy URL,file Path={}", projectId, filePath);
Optional.ofNullable(projectId).filter(x -> x > 0)
.orElseThrow(() -> new IllegalArgumentException("projectId is required or invalid"));
Optional.ofNullable(filePath).filter(x -> !x.trim().isEmpty())
.orElseThrow(() -> new IllegalArgumentException("filePath is required"));
try {
// 获取开发环境代理路径
String devProxyPath = customPageProxyPathApplicationService.getDevProxyPath(projectId);
// 去掉filePath开头的所有/
String normalizedFilePath = filePath.replaceAll("^/+", "");
String devProxyUrl = devProxyPath + normalizedFilePath;
log.info("[Application] project Id={},getfileproxy URLresponse,proxy Url={}", devProxyUrl);
return ReqResult.success(devProxyUrl);
} catch (Exception e) {
log.error("[Application] project Id={},getfileproxy URLexception,file Path={}", projectId, filePath, e);
return ReqResult.error("0001", "Failed to get file proxy URL: " + e.getMessage());
}
}
@Override
public ReqResult<Map<String, Object>> rollbackVersion(Long projectId, Integer rollbackTo,
UserContext userContext) {
log.info("[Application] project Id={},rollback version,rollback To={}", projectId, rollbackTo);
Optional.ofNullable(projectId).filter(x -> x > 0)
.orElseThrow(() -> new IllegalArgumentException("projectId is required or invalid"));
Optional.ofNullable(rollbackTo).filter(x -> x >= 0)
.orElseThrow(() -> new IllegalArgumentException("rollbackTo is required or invalid"));
ReqResult<Map<String, Object>> result = customPageCodingDomainService.rollbackVersion(projectId,
rollbackTo, userContext);
log.info("[Application] project Id={},rollback versioncompleted,result={}", projectId, result);
return result;
}
}

View File

@@ -0,0 +1,870 @@
package com.xspaceagi.custompage.application.service.impl;
import com.baomidou.dynamic.datasource.annotation.DSTransactional;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.xspaceagi.agent.core.adapter.application.PluginApplicationService;
import com.xspaceagi.agent.core.adapter.application.PublishApplicationService;
import com.xspaceagi.agent.core.adapter.application.WorkflowApplicationService;
import com.xspaceagi.agent.core.adapter.dto.PublishedDto;
import com.xspaceagi.agent.core.adapter.dto.PublishedPermissionDto;
import com.xspaceagi.agent.core.adapter.dto.config.plugin.PluginDto;
import com.xspaceagi.agent.core.adapter.dto.config.workflow.WorkflowConfigDto;
import com.xspaceagi.agent.core.adapter.repository.entity.Published;
import com.xspaceagi.agent.core.sdk.IAgentRpcService;
import com.xspaceagi.agent.core.sdk.dto.*;
import com.xspaceagi.agent.core.sdk.enums.TargetTypeEnum;
import com.xspaceagi.custompage.application.service.ICustomPageConfigApplicationService;
import com.xspaceagi.custompage.domain.model.CustomPageBuildModel;
import com.xspaceagi.custompage.domain.model.CustomPageConfigModel;
import com.xspaceagi.custompage.domain.service.ICustomPageBuildDomainService;
import com.xspaceagi.custompage.domain.service.ICustomPageConfigDomainService;
import com.xspaceagi.custompage.sdk.dto.DataSourceDto;
import com.xspaceagi.custompage.sdk.dto.ExportTypeEnum;
import com.xspaceagi.custompage.sdk.dto.PageArgConfig;
import com.xspaceagi.custompage.sdk.dto.ProxyConfig;
import com.xspaceagi.sandbox.sdk.server.ISandboxConfigRpcService;
import com.xspaceagi.sandbox.sdk.service.dto.SandboxConfigRpcDto;
import com.xspaceagi.system.sdk.permission.IUserDataPermissionRpcService;
import com.xspaceagi.system.sdk.service.dto.UserDataPermissionDto;
import com.xspaceagi.system.spec.common.UserContext;
import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
import com.xspaceagi.system.spec.exception.BizException;
import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* 自定义页面配置应用服务实现
*/
@Slf4j
@Service
public class CustomPageConfigApplicationServiceImpl implements ICustomPageConfigApplicationService {
@Resource
private ICustomPageConfigDomainService customPageConfigDomainService;
@Resource
private ICustomPageBuildDomainService customPageBuildDomainService;
@Resource
private PluginApplicationService pluginApplicationService;
@Resource
private WorkflowApplicationService workflowApplicationService;
@Resource
private IAgentRpcService agentRpcService;
@Resource
private PublishApplicationService publishApplicationService;
@Resource
private IUserDataPermissionRpcService userDataPermissionRpcService;
@Resource
private ISandboxConfigRpcService sandboxConfigRpcService;
// 需要事务
@Transactional(rollbackFor = Exception.class)
@Override
public ReqResult<CustomPageConfigModel> create(CustomPageConfigModel model, UserContext userContext)
throws JsonProcessingException {
log.info("[create] createproject,name={}", model.getName());
Optional.ofNullable(model.getName()).filter(StringUtils::isNotBlank)
.orElseThrow(() -> new IllegalArgumentException("projectName is required"));
if (model.getSpaceId() == null) {
throw new IllegalArgumentException("spaceId is required");
}
// 校验用户网页应用数量是否超限
UserDataPermissionDto dataPermission = userDataPermissionRpcService.getUserDataPermission(userContext.getUserId());
if (dataPermission != null) {
Integer maxPageAppCount = dataPermission.getMaxPageAppCount();
if (maxPageAppCount != null && maxPageAppCount != -1) {
CustomPageConfigModel queryModel = new CustomPageConfigModel();
queryModel.setCreatorId(userContext.getUserId());
List<CustomPageConfigModel> existingPages = customPageConfigDomainService.list(queryModel);
int currentCount = existingPages == null ? 0 : existingPages.size();
if (currentCount >= maxPageAppCount) {
log.warn("[create] create project failed, user page countreached limit, user Id={}, current Count={}, max page app count={}", userContext.getUserId(), currentCount, maxPageAppCount);
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.customPageWebAppCountExceeded, maxPageAppCount);
}
}
}
// 创建config表
ReqResult<CustomPageConfigModel> configResult = customPageConfigDomainService.create(model,
userContext);
if (!configResult.isSuccess()) {
log.error("[create] create project failed(configtable), name={}, base Path={}, error={}",
model.getName(), model.getBasePath(), configResult.getMessage());
throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.customPageCreateConfigFailed,
configResult.getMessage() != null ? configResult.getMessage() : "");
}
CustomPageConfigModel configModel = configResult.getData();
Long projectId = configModel.getId();
Long sandboxId = null;
try {
SandboxConfigRpcDto sandboxConfig = sandboxConfigRpcService.selectAppDevelopmentSandbox(
userContext.getTenantId(),
userContext.getUserId(),
configModel.getSpaceId(),
projectId,
null);
sandboxId = sandboxConfig != null ? sandboxConfig.getId() : null;
if (sandboxId == null) {
log.info("[create] project Id={},bind sandbox failed,message=No available sandbox", projectId);
}
} catch (Exception e) {
log.info("[create] project Id={},bind sandbox failed,message={}", projectId, e.getMessage());
}
// 创建build表
ReqResult<CustomPageBuildModel> buildResult = customPageBuildDomainService.createProject(projectId,
model.getSpaceId(),
userContext);
if (!buildResult.isSuccess()) {
log.error("[create] create project failed(buildtable), name={}, base Path={}, error={}",
model.getName(), model.getBasePath(), buildResult.getMessage());
throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.customPageCreateBuildFailed,
buildResult.getMessage() != null ? buildResult.getMessage() : "");
}
// 创建智能体
PageAppAgentCreateDto agentDto = new PageAppAgentCreateDto();
agentDto.setCreatorId(userContext.getUserId());
agentDto.setSpaceId(model.getSpaceId());
agentDto.setName(model.getName());
agentDto.setDescription(model.getDescription());
agentDto.setIcon(model.getIcon());
agentDto.setProjectId(projectId);
com.xspaceagi.agent.core.sdk.dto.ReqResult<Long> agentResult = agentRpcService
.createPageAppAgent(agentDto);
if (!agentResult.isSuccess()) {
log.error("[create] createagentfailed, name={}, error={}", model.getName(), agentResult.getMessage());
throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.customPageCreateAgentFailed,
agentResult.getMessage() != null ? agentResult.getMessage() : "");
}
// 绑定智能体到项目
CustomPageConfigModel bindModel = new CustomPageConfigModel();
bindModel.setId(projectId);
bindModel.setDevAgentId(agentResult.getData());
bindModel.setSandboxId(sandboxId);
ReqResult<CustomPageConfigModel> result = customPageConfigDomainService.update(
bindModel,
userContext);
if (!result.isSuccess()) {
log.error("[create] project Id={},bind agent to project failed,message={}", projectId,
result.getMessage());
throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.customPageBindAgentFailed,
result.getMessage() != null ? result.getMessage() : "");
}
log.info("[create] createprojectsucceeded, project Id={}, name={}", projectId, model.getName());
configResult.getData().setDevAgentId(agentResult.getData());
return ReqResult.success(configResult.getData());
}
// 不需要事务
@Override
public ReqResult<Map<String, Object>> uploadProject(CustomPageConfigModel model, MultipartFile file,
boolean isInitProject,
UserContext userContext) throws Exception {
log.info("[upload-project] project Id={}startupload", model.getId());
if (file == null || file.isEmpty()) {
return ReqResult.error("0001", "File is required");
}
Long projectId = model.getId();
// 传输压缩包
ReqResult<Map<String, Object>> result = customPageBuildDomainService.uploadProject(
projectId,
file,
isInitProject,
userContext);
if (!result.isSuccess()) {
log.info("[upload-project] uploadfailed, project Id={}, code={}, message={}",
projectId,
result.getCode(), result.getMessage());
return ReqResult.error(result.getCode(), result.getMessage());
}
log.info("[upload-project] uploadsucceeded, project Id={}, result={}", projectId, result);
// 如果是创建新项目,尝试导入配置文件
if (isInitProject) {
try {
customPageConfigDomainService.importProjectConfig(model, userContext);
} catch (Exception e) {
log.error("[upload-project] project Id={},config fileimportfailed", projectId, e);
// 配置文件导入失败不影响整体上传流程
}
}
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public ReqResult<CustomPageConfigModel> createReverseProxyProject(CustomPageConfigModel model,
UserContext userContext) {
log.info("[create Proxy Project] create reverse proxy project, name={}", model.getName());
Optional.ofNullable(model.getName()).filter(StringUtils::isNotBlank)
.orElseThrow(() -> new IllegalArgumentException("projectName is required"));
if (model.getSpaceId() == null) {
throw new IllegalArgumentException("spaceId is required");
}
ReqResult<CustomPageConfigModel> configResult = customPageConfigDomainService.create(model, userContext);
if (!configResult.isSuccess()) {
log.error("[create Proxy Project] create reverse proxy projectfailed, name={}, message={}",
model.getName(), configResult.getMessage());
throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.customPageCreateReverseProxyFailed,
configResult.getMessage() != null ? configResult.getMessage() : "");
}
log.info("[create Proxy Project] create reverse proxy projectsucceeded, project Id={}, name={}",
configResult.getData().getId(), model.getName());
return ReqResult.success(configResult.getData());
}
@Override
public ReqResult<Map<String, Object>> queryProjectContent(Long projectId, String proxyPath) {
log.info("[query Project Content] project Id={},query project file content", projectId);
Optional.ofNullable(projectId).filter(x -> x > 0)
.orElseThrow(() -> new IllegalArgumentException("projectId is required or invalid"));
ReqResult<Map<String, Object>> result = customPageConfigDomainService.queryProjectContent(projectId,
null, proxyPath);
if (!result.isSuccess()) {
log.error("[query Project Content] project Id={},query project file contentfailed,message={}", projectId,
result.getMessage());
return ReqResult.error(result.getCode(), result.getMessage());
}
log.info("[query Project Content] project Id={},query project file contentsucceeded", projectId);
return result;
}
@Override
public ReqResult<Map<String, Object>> queryProjectContentByVersion(Long projectId, Integer codeVersion, String proxyPath) {
log.info("[query Project Content By Version] project Id={},code Version={},query project historical version file content", projectId,
codeVersion);
ReqResult<Map<String, Object>> result = customPageConfigDomainService
.queryProjectContentByVersion(projectId, codeVersion, proxyPath);
if (!result.isSuccess()) {
log.error("[query Project Content By Version] project Id={},code Version={},query project historical version file contentfailed,message={}",
projectId,
codeVersion, result.getMessage());
return ReqResult.error(result.getCode(), result.getMessage());
}
log.info("[query Project Content By Version] project Id={},code Version={},query project historical version file contentsucceeded", projectId,
codeVersion);
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public ReqResult<List<ProxyConfig>> addProxy(Long projectId, ProxyConfig proxyConfig,
UserContext userContext) {
log.info("[add Proxy] project Id={},env={},path={},add reverse proxy config", projectId, proxyConfig.getEnv(),
proxyConfig.getPath());
Optional.ofNullable(projectId).filter(x -> x > 0)
.orElseThrow(() -> new IllegalArgumentException("projectId is required or invalid"));
Optional.ofNullable(proxyConfig.getEnv())
.orElseThrow(() -> new IllegalArgumentException("environment is required"));
Optional.ofNullable(proxyConfig.getPath()).filter(StringUtils::isNotBlank)
.orElseThrow(() -> new IllegalArgumentException("path is required"));
Optional.ofNullable(proxyConfig.getBackends()).filter(list -> !list.isEmpty())
.orElseThrow(() -> new IllegalArgumentException("backend address list cannot be empty"));
ReqResult<List<ProxyConfig>> result = customPageConfigDomainService.addProxy(projectId,
proxyConfig, userContext);
if (!result.isSuccess()) {
log.error("[add Proxy] project Id={},env={},path={},add reverse proxy configfailed,message={}",
projectId, proxyConfig.getEnv(), proxyConfig.getPath(), result.getMessage());
return result;
}
log.info("[add Proxy] project Id={},env={},path={},add reverse proxy configsucceeded",
projectId, proxyConfig.getEnv(), proxyConfig.getPath());
return ReqResult.success(result.getData());
}
@Override
@Transactional(rollbackFor = Exception.class)
public ReqResult<Void> editProxyConfig(Long projectId, ProxyConfig proxyConfig, UserContext userContext) {
log.info("[edit Proxy Config] project Id={},env={},path={},editreverse proxy config", projectId, proxyConfig.getEnv(),
proxyConfig.getPath());
Optional.ofNullable(projectId).filter(x -> x > 0)
.orElseThrow(() -> new IllegalArgumentException("projectId is required or invalid"));
Optional.ofNullable(proxyConfig.getEnv())
.orElseThrow(() -> new IllegalArgumentException("environment is required"));
Optional.ofNullable(proxyConfig.getPath()).filter(StringUtils::isNotBlank)
.orElseThrow(() -> new IllegalArgumentException("path is required"));
Optional.ofNullable(proxyConfig.getBackends()).filter(list -> !list.isEmpty())
.orElseThrow(() -> new IllegalArgumentException("backend address list cannot be empty"));
ReqResult<Void> result = customPageConfigDomainService.editProxy(projectId, proxyConfig,
userContext);
if (!result.isSuccess()) {
log.error("[edit Proxy Config] project Id={},env={},path={},editreverse proxy configfailed,message={}",
projectId, proxyConfig.getEnv(), proxyConfig.getPath(), result.getMessage());
return result;
}
log.info("[edit Proxy Config] project Id={},env={},path={},editreverse proxy configsucceeded",
projectId, proxyConfig.getEnv(), proxyConfig.getPath());
return ReqResult.success(null);
}
@Override
@Transactional(rollbackFor = Exception.class)
public ReqResult<Void> deleteProxy(Long projectId, String env, String path, UserContext userContext) {
log.info("[delete Proxy] project Id={},env={},path={},delete reverse proxy config", projectId, env, path);
Optional.ofNullable(projectId).filter(x -> x > 0)
.orElseThrow(() -> new IllegalArgumentException("projectId is required or invalid"));
Optional.ofNullable(env).filter(StringUtils::isNotBlank)
.orElseThrow(() -> new IllegalArgumentException("environment is required"));
Optional.ofNullable(path).filter(StringUtils::isNotBlank)
.orElseThrow(() -> new IllegalArgumentException("path is required"));
ReqResult<Void> result = customPageConfigDomainService.deleteProxy(projectId, env, path, userContext);
if (!result.isSuccess()) {
log.error("[delete Proxy] project Id={},env={},path={},delete reverse proxy configfailed,message={}",
projectId, env, path, result.getMessage());
throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.customPageDeleteReverseProxyFailed,
result.getMessage() != null ? result.getMessage() : "");
}
log.info("[delete Proxy] project Id={},env={},path={},delete reverse proxy configsucceeded",
projectId, env, path);
return ReqResult.success(null);
}
@Override
@Transactional(rollbackFor = Exception.class)
public ReqResult<Void> savePathArgs(Long projectId, PageArgConfig pageArgConfig, UserContext userContext) {
log.info("[save Path Args] project Id={},page Uri={},configure page args", projectId, pageArgConfig.getPageUri());
Optional.ofNullable(projectId).filter(x -> x > 0)
.orElseThrow(() -> new IllegalArgumentException("projectId is required or invalid"));
Optional.ofNullable(pageArgConfig.getPageUri()).filter(StringUtils::isNotBlank)
.orElseThrow(() -> new IllegalArgumentException("page path cannot be empty"));
ReqResult<Void> result = customPageConfigDomainService.savePathArgs(projectId, pageArgConfig,
userContext);
if (!result.isSuccess()) {
log.error("[save Path Args] project Id={},page Uri={},configure page argsfailed,message={}",
projectId, pageArgConfig.getPageUri(), result.getMessage());
throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.customPageConfigPageParamsFailed,
result.getMessage() != null ? result.getMessage() : "");
}
log.info("[save Path Args] project Id={},page Uri={},configure page argssucceeded",
projectId, pageArgConfig.getPageUri());
return ReqResult.success(null);
}
@Override
@Transactional(rollbackFor = Exception.class)
public ReqResult<Void> addPath(Long projectId, PageArgConfig pageArgConfig, UserContext userContext) {
log.info("[add Path] project Id={},page Uri={},add path config", projectId, pageArgConfig.getPageUri());
Optional.ofNullable(projectId).filter(x -> x > 0)
.orElseThrow(() -> new IllegalArgumentException("projectId is required or invalid"));
Optional.ofNullable(pageArgConfig.getPageUri()).filter(StringUtils::isNotBlank)
.orElseThrow(() -> new IllegalArgumentException("page path cannot be empty"));
ReqResult<Void> result = customPageConfigDomainService.addPath(projectId, pageArgConfig,
userContext);
if (!result.isSuccess()) {
log.error("[add Path] project Id={},page Uri={},add path configfailed,message={}",
projectId, pageArgConfig.getPageUri(), result.getMessage());
throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.customPageAddPathConfigFailed,
result.getMessage() != null ? result.getMessage() : "");
}
log.info("[add Path] project Id={},page Uri={},add path configsucceeded",
projectId, pageArgConfig.getPageUri());
return ReqResult.success(null);
}
@Override
@Transactional(rollbackFor = Exception.class)
public ReqResult<Void> editPath(Long projectId, PageArgConfig pageArgConfig, UserContext userContext) {
log.info("[edit Path] project Id={},page Uri={},editpath config", projectId, pageArgConfig.getPageUri());
Optional.ofNullable(projectId).filter(x -> x > 0)
.orElseThrow(() -> new IllegalArgumentException("projectId is required or invalid"));
Optional.ofNullable(pageArgConfig.getPageUri()).filter(StringUtils::isNotBlank)
.orElseThrow(() -> new IllegalArgumentException("page path cannot be empty"));
ReqResult<Void> result = customPageConfigDomainService.editPath(projectId, pageArgConfig,
userContext);
if (!result.isSuccess()) {
log.error("[edit Path] project Id={},page Uri={},editpath configfailed,message={}",
projectId, pageArgConfig.getPageUri(), result.getMessage());
throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.customPageEditPathConfigFailed,
result.getMessage() != null ? result.getMessage() : "");
}
log.info("[edit Path] project Id={},page Uri={},editpath configsucceeded",
projectId, pageArgConfig.getPageUri());
return ReqResult.success(null);
}
@Override
@Transactional(rollbackFor = Exception.class)
public ReqResult<Void> deletePath(Long projectId, String pageUri, UserContext userContext) {
log.info("[delete Path] project Id={},page Uri={},delete path config", projectId, pageUri);
Optional.ofNullable(projectId).filter(x -> x > 0)
.orElseThrow(() -> new IllegalArgumentException("projectId is required or invalid"));
Optional.ofNullable(pageUri).filter(StringUtils::isNotBlank)
.orElseThrow(() -> new IllegalArgumentException("page path cannot be empty"));
ReqResult<Void> result = customPageConfigDomainService.deletePath(projectId, pageUri,
userContext);
if (!result.isSuccess()) {
log.error("[delete Path] project Id={},page Uri={},delete path configfailed,message={}",
projectId, pageUri, result.getMessage());
throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.customPageDeletePathConfigFailed,
result.getMessage() != null ? result.getMessage() : "");
}
log.info("[delete Path] project Id={},page Uri={},delete path configsucceeded",
projectId, pageUri);
return ReqResult.success(null);
}
@Override
@Transactional(rollbackFor = Exception.class)
public ReqResult<Void> batchConfigProxy(Long projectId, List<ProxyConfig> proxyConfigs,
UserContext userContext) {
log.info("[batch Config Proxy] project Id={},config Count={},batch configure reverse proxy", projectId,
proxyConfigs != null ? proxyConfigs.size() : 0);
Optional.ofNullable(projectId).filter(x -> x > 0)
.orElseThrow(() -> new IllegalArgumentException("projectId is required or invalid"));
Optional.ofNullable(proxyConfigs)
.orElseThrow(() -> new IllegalArgumentException("reverse proxy configuration list cannot be empty"));
ReqResult<Void> result = customPageConfigDomainService.batchConfigProxy(projectId, proxyConfigs,
userContext);
if (!result.isSuccess()) {
log.error("[batch Config Proxy] project Id={},config Count={},batch configure reverse proxyfailed,message={}", projectId,
proxyConfigs != null ? proxyConfigs.size() : 0, result.getMessage());
throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.customPageBatchReverseProxyFailed,
result.getMessage() != null ? result.getMessage() : "");
}
log.info("[batch Config Proxy] project Id={},config Count={},batch configure reverse proxysucceeded",
projectId, proxyConfigs != null ? proxyConfigs.size() : 0);
return ReqResult.success(null);
}
public ReqResult<InputStream> exportProjectPublished(Long projectId, UserContext userContext) {
return customPageConfigDomainService.exportProject(projectId, ExportTypeEnum.PUBLISHED, userContext);
}
public ReqResult<InputStream> exportProjectLatest(Long projectId, UserContext userContext) {
return customPageConfigDomainService.exportProject(projectId, ExportTypeEnum.LATEST, userContext);
}
@Override
@Transactional(rollbackFor = Exception.class)
public ReqResult<Void> bindDataSource(Long projectId, String type, Long dataSourceId,
UserContext userContext) {
log.info("[bind Data Source] project Id={},type={},data Source Id={},binddata source", projectId, type,
dataSourceId);
Optional.ofNullable(projectId).filter(x -> x > 0)
.orElseThrow(() -> new IllegalArgumentException("projectId is required or invalid"));
Optional.ofNullable(type).filter(StringUtils::isNotBlank)
.orElseThrow(() -> new IllegalArgumentException("data source type is required"));
Optional.ofNullable(dataSourceId).filter(x -> x > 0)
.orElseThrow(() -> new IllegalArgumentException("data source ID is required or invalid"));
String dataSourceName = null;
String dataSourceIcon = null;
if ("plugin".equalsIgnoreCase(type)) {
PluginDto pluginDto = pluginApplicationService.queryPublishedPluginConfig(dataSourceId, null);
if (pluginDto == null) {
log.error("[bind Data Source] project Id={},type={},data Source Id={},pluginnot foundor not published", projectId, type, dataSourceId);
return ReqResult.error("0001", "Plugin does not exist or is not published");
}
dataSourceName = pluginDto.getName();
dataSourceIcon = pluginDto.getIcon();
log.info("[bind Data Source] project Id={},type={},data Source Id={},getplugin succeeded", projectId, type, dataSourceId);
} else if ("workflow".equalsIgnoreCase(type)) {
WorkflowConfigDto workflowConfigDto = workflowApplicationService
.queryPublishedWorkflowConfig(dataSourceId, null);
if (workflowConfigDto == null) {
log.error("[bind Data Source] project Id={},type={},data Source Id={},workflownot foundor not published", projectId, type, dataSourceId);
return ReqResult.error("0003", "Workflow does not exist or is not published");
}
dataSourceName = workflowConfigDto.getName();
dataSourceIcon = workflowConfigDto.getIcon();
log.info("[bind Data Source] project Id={},type={},data Source Id={},getworkflow succeeded", projectId, type, dataSourceId);
} else {
log.error("[bind Data Source] project Id={},type={},data Source Id={},unsupported data source type, type={}", projectId, type, dataSourceId, type);
return ReqResult.error("0004", "Unsupported data source type: " + type);
}
DataSourceDto dataSource = DataSourceDto.builder()
.type(type.toLowerCase())
.id(dataSourceId)
.key(System.currentTimeMillis() / 1000 + dataSourceId.toString())// 在页面范围内生一个不重复的key
.name(dataSourceName)
.icon(dataSourceIcon)
.build();
ReqResult<Void> result = customPageConfigDomainService.bindDataSource(projectId, dataSource, userContext);
if (!result.isSuccess()) {
log.error("[bind Data Source] savedata sourcefailed, project Id={}, type={}, data Source Id={}, error={}",
projectId, type, dataSourceId, result.getMessage());
throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.customPageSaveDataSourceFailed,
result.getMessage() != null ? result.getMessage() : "");
}
log.info("[bind Data Source] savedata sourcesucceeded, project Id={}, type={}, data Source Id={}", projectId, type, dataSourceId);
return ReqResult.success(null);
}
@Override
@Transactional(rollbackFor = Exception.class)
public ReqResult<Void> unbindDataSource(Long projectId, String type, Long dataSourceId,
UserContext userContext) {
log.info("[unbind Data Source] project Id={},type={},data Source Id={},unbinddata source", projectId, type,
dataSourceId);
Optional.ofNullable(projectId).filter(x -> x > 0)
.orElseThrow(() -> new IllegalArgumentException("projectId is required or invalid"));
Optional.ofNullable(type).filter(StringUtils::isNotBlank)
.orElseThrow(() -> new IllegalArgumentException("data source type is required"));
Optional.ofNullable(dataSourceId).filter(x -> x > 0)
.orElseThrow(() -> new IllegalArgumentException("data source ID is required or invalid"));
DataSourceDto dataSource = DataSourceDto.builder()
.type(type.toLowerCase())
.id(dataSourceId)
.build();
ReqResult<Void> result = customPageConfigDomainService.unbindDataSource(projectId, dataSource, userContext);
if (!result.isSuccess()) {
log.error("[unbind Data Source] unbinddata sourcefailed, project Id={}, type={}, data Source Id={}, error={}", projectId, type, dataSourceId, result.getMessage());
throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.customPageUnbindDataSourceFailed,
result.getMessage() != null ? result.getMessage() : "");
}
log.info("[unbind Data Source] unbinddata sourcesucceeded, project Id={}, type={}, data Source Id={}", projectId, type, dataSourceId);
return ReqResult.success(null);
}
@Override
public CustomPageConfigModel getByProjectId(Long projectId) {
log.info("[get By Project Id] project Id={},queryproject", projectId);
return customPageConfigDomainService.getById(projectId);
}
@Override
public List<CustomPageConfigModel> listByIds(List<Long> ids) {
return customPageConfigDomainService.listByIds(ids);
}
@Override
@Transactional(rollbackFor = Exception.class)
public ReqResult<CustomPageConfigModel> updateProject(CustomPageConfigModel model, UserContext userContext) {
log.info("[update Project] project Id={},modify project", model.getId());
try {
Optional.ofNullable(model.getId()).filter(x -> x > 0)
.orElseThrow(() -> new IllegalArgumentException("project ID is required or invalid"));
Optional.ofNullable(model.getName()).filter(StringUtils::isNotBlank)
.orElseThrow(() -> new IllegalArgumentException("project name is required"));
// 更新config表
ReqResult<CustomPageConfigModel> result = customPageConfigDomainService.update(model,
userContext);
if (!result.isSuccess()) {
log.error("[update Project] project Id={},modify projectfailed,message={}", model.getId(),
result.getMessage());
throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.customPageUpdateProjectFailed,
result.getMessage() != null ? result.getMessage() : "");
}
// 更新智能体
Long devAgentId = ((CustomPageConfigModel) result.getData()).getDevAgentId();
PageAppAgentUpdateDto agentDto = new PageAppAgentUpdateDto();
agentDto.setAgentId(devAgentId);
agentDto.setName(model.getName());
agentDto.setDescription(model.getDescription());
agentDto.setIcon(model.getIcon());
com.xspaceagi.agent.core.sdk.dto.ReqResult<Void> agentResult = agentRpcService
.updatePageAppAgent(agentDto);
if (!agentResult.isSuccess()) {
log.error("[update Project] project Id={},updateagentfailed,message={}", model.getId(),
agentResult.getMessage());
throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.customPageUpdateAgentFailed,
agentResult.getMessage() != null ? agentResult.getMessage() : "");
}
log.info("[update Project] project Id={},modify projectsucceeded", model.getId());
return ReqResult.success(result.getData());
} catch (Exception e) {
log.error("[update Project] project Id={},modify projectexception", model.getId(), e);
throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.customPageUpdateProjectException,
e.getMessage() != null ? e.getMessage() : "");
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public ReqResult<Map<String, Object>> deleteProject(Long projectId, UserContext userContext) {
log.info("[delete Project] project Id={},delete project", projectId);
try {
Optional.ofNullable(projectId).filter(x -> x > 0)
.orElseThrow(() -> new IllegalArgumentException("project ID is required or invalid"));
ReqResult<Map<String, Object>> result = customPageConfigDomainService.delete(projectId, userContext);
if (!result.isSuccess()) {
log.error("[delete Project] project Id={},delete projectfailed,message={}", projectId, result.getMessage());
throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.customPageDeleteProjectFailed,
result.getMessage() != null ? result.getMessage() : "");
}
// 删除智能体
try {
Long devAgentId = ((CustomPageConfigModel) result.getData().get("config"))
.getDevAgentId();
com.xspaceagi.agent.core.sdk.dto.ReqResult<Void> agentResult = agentRpcService
.deletePageAppAgent(devAgentId);
if (!agentResult.isSuccess()) {
log.error("[delete Project] project Id={},deleteagentfailed,message={}", projectId,
agentResult.getMessage());
throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.customPageDeleteAgentFailed,
agentResult.getMessage() != null ? agentResult.getMessage() : "");
}
} catch (Exception e) {
log.error("[delete Project] project Id={},deleteagentfailed", projectId, e);
// 不抛异常,老数据没有智能体,删除会异常
}
log.info("[delete Project] project Id={},delete projectsucceeded", projectId);
return result;
} catch (Exception e) {
log.error("[delete Project] project Id={},delete projectexception", projectId, e);
throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.customPageDeleteProjectException,
e.getMessage() != null ? e.getMessage() : "");
}
}
@Override
@DSTransactional(rollbackFor = Exception.class)
public List<DataSourceDto> copyProjectDataSources(CustomPageConfigModel sourceConfig, CustomPageConfigModel targetConfig, UserContext userContext) {
List<DataSourceDto> sourceDataSources = sourceConfig.getDataSources();
if (sourceDataSources == null || sourceDataSources.isEmpty()) {
return null;
}
Long sourceSpaceId = sourceConfig.getSpaceId();
Long targetProjectId = targetConfig.getId();
Long targetSpaceId = targetConfig.getSpaceId();
log.info("[copy Project] target Project Id={}, target Space Id={},startcopydata source", targetProjectId, targetSpaceId);
//本空间复制项目
//直接绑定数据源
if (sourceSpaceId.equals(targetSpaceId)) {
log.info("[copy Project] target Project Id={}, target Space Id={},same space,fully copy binding relationships", targetProjectId, targetSpaceId);
CustomPageConfigModel updateConfig = new CustomPageConfigModel();
updateConfig.setId(targetProjectId);
updateConfig.setDataSources(sourceDataSources);
customPageConfigDomainService.update(updateConfig, userContext);
return null;
}
//跨空间复制项目
//新增的数据源
List<DataSourceDto> newCreateDataSources = new ArrayList<>();
//目标绑定数据源
List<DataSourceDto> targetDataSources = new ArrayList<>();
for (DataSourceDto dataSource : sourceDataSources) {
Published.TargetType dataSourceType = "plugin".equalsIgnoreCase(dataSource.getType()) ? Published.TargetType.Plugin : Published.TargetType.Workflow;
//List<PublishedDto> publishedDtos = publishApplicationService.queryPublishedList(dataSourceType, List.of(dataSource.getId()));
PublishedDto publishedDto = publishApplicationService.queryPublished(dataSourceType, dataSource.getId());
if (publishedDto == null) {
log.info("[copy Project] project Id={},data Source Id={},type={}, data sourcenot found,skip", targetProjectId, dataSource.getId(), dataSourceType);
continue;
}
if (publishedDto.getScope() == Published.PublishScope.Global || publishedDto.getScope() == Published.PublishScope.Tenant) {
//全局数据源,不需要复制,直接绑定
log.info("[copy Project] project Id={},data Source Id={},type={}, global data source, bind directly", targetProjectId, dataSource.getId(), dataSourceType);
targetDataSources.add(dataSource);
} else if (publishedDto.getPublishedSpaceIds() != null && publishedDto.getPublishedSpaceIds().contains(targetSpaceId)) {
//已经发布到了目标空间
log.info("[copy Project] project Id={},data Source Id={},type={}, data sourcealready published in target space, bind directly", targetProjectId, dataSource.getId(), dataSourceType);
targetDataSources.add(dataSource);
} else {
if (dataSourceType == Published.TargetType.Plugin) {// 插件
//判断是否允许复制
boolean allowCopy = false;
com.xspaceagi.agent.core.sdk.dto.ReqResult<PluginInfoDto> publishedPluginInfo = agentRpcService.getPublishedPluginInfo(dataSource.getId(), null);
if (publishedPluginInfo != null && publishedPluginInfo.isSuccess()) {
PluginInfoDto data = publishedPluginInfo.getData();
if (sourceConfig.getSpaceId().equals(data.getSpaceId())) {
//数据资源和源项目在同空间,复制走
allowCopy = true;
} else {
try {
PublishedPermissionDto permissionDto = publishApplicationService.hasPermission(dataSourceType, dataSource.getId());
allowCopy = permissionDto.isCopy();
} catch (Exception e) {
log.info("[copy Project] project Id={},data Source Id={},type={}, data source copy permission check failed, skip", targetProjectId, dataSource.getId(), dataSourceType, e);
}
}
}
if (!allowCopy) {
//不允许复制
log.info("[copy Project] project Id={},data Source Id={},type={}, no data source copy permission, skip", targetProjectId, dataSource.getId(), dataSourceType);
continue;
}
//开始复制
com.xspaceagi.agent.core.sdk.dto.ReqResult<String> pluginRes = agentRpcService.queryPluginConfig(dataSource.getId(), null);
if (pluginRes != null && pluginRes.isSuccess()) {
PluginEnableOrUpdateDto pluginDto = new PluginEnableOrUpdateDto();
pluginDto.setUserId(userContext.getUserId());
pluginDto.setSpaceId(targetSpaceId);
pluginDto.setName(publishedDto.getName());
pluginDto.setIcon(publishedDto.getIcon());
pluginDto.setConfig(pluginRes.getData());
pluginDto.setParamJson("{}");
com.xspaceagi.agent.core.sdk.dto.ReqResult<Long> enableResult = agentRpcService.pluginEnableOrUpdate(pluginDto);
if (!enableResult.isSuccess()) {
log.error("[copy Project] project Id={},data Source Id={},type={},copypluginfailed,message={}", targetProjectId, dataSource.getId(), dataSourceType, enableResult.getMessage());
throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.customPageCopyPluginFailed,
enableResult.getMessage() != null ? enableResult.getMessage() : "");
} else {
log.info("[copy Project] project Id={},data Source Id={},type={},copypluginsucceeded", targetProjectId, dataSource.getId(), dataSourceType, enableResult.getData());
}
Long newPluginId = enableResult.getData();
DataSourceDto dataSourceDto = DataSourceDto.builder()
.id(newPluginId)
.type("plugin")
.key(dataSource.getKey())
.name(publishedDto.getName())
.icon(publishedDto.getIcon())
.build();
targetDataSources.add(dataSourceDto);
newCreateDataSources.add(dataSourceDto);
}
} else {
// 工作流
//判断是否允许复制
boolean allowCopy = false;
com.xspaceagi.agent.core.sdk.dto.ReqResult<WorkflowInfoDto> publishedWorkflowInfo = agentRpcService.getPublishedWorkflowInfo(dataSource.getId(), null);
if (publishedWorkflowInfo != null && publishedWorkflowInfo.isSuccess()) {
WorkflowInfoDto data = publishedWorkflowInfo.getData();
if (sourceConfig.getSpaceId().equals(data.getSpaceId())) {
//数据资源和源项目在同空间,复制走
allowCopy = true;
} else {
try {
PublishedPermissionDto permissionDto = publishApplicationService.hasPermission(dataSourceType, dataSource.getId());
allowCopy = permissionDto.isCopy();
} catch (Exception e) {
log.info("[copy Project] project Id={},data Source Id={},type={}, data source copy permission check failed, skip", targetProjectId, dataSource.getId(), dataSourceType, e);
}
}
}
if (!allowCopy) {
//不允许复制
log.info("[copy Project] project Id={},data Source Id={},type={}, no data source copy permission, skip", targetProjectId, dataSource.getId(), dataSourceType);
continue;
}
//开始复制
com.xspaceagi.agent.core.sdk.dto.ReqResult<String> workflowRes = agentRpcService.queryTemplateConfig(TargetTypeEnum.Workflow, dataSource.getId());
if (workflowRes != null && workflowRes.isSuccess()) {
TemplateEnableOrUpdateDto templateDto = new TemplateEnableOrUpdateDto();
templateDto.setUserId(userContext.getUserId());
templateDto.setTargetType(TargetTypeEnum.Workflow);
templateDto.setName(publishedDto.getName());
templateDto.setIcon(publishedDto.getIcon());
templateDto.setConfig(workflowRes.getData());
templateDto.setSpaceId(targetSpaceId);
com.xspaceagi.agent.core.sdk.dto.ReqResult<Long> enableResult = agentRpcService.templateEnableOrUpdate(templateDto);
if (!enableResult.isSuccess()) {
log.error("[copy Project] project Id={},data Source Id={},type={},copyworkflowfailed,message={}", targetProjectId, dataSource.getId(), dataSourceType, enableResult.getMessage());
throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.customPageCopyWorkflowFailed,
enableResult.getMessage() != null ? enableResult.getMessage() : "");
} else {
log.info("[copy Project] project Id={},data Source Id={},type={},copyworkflowsucceeded", targetProjectId, dataSource.getId(), dataSourceType);
}
Long newWorkflowId = enableResult.getData();
DataSourceDto dataSourceDto = DataSourceDto.builder()
.id(newWorkflowId)
.type("workflow")
.key(dataSource.getKey())
.name(publishedDto.getName())
.icon(publishedDto.getIcon())
.build();
targetDataSources.add(dataSourceDto);
newCreateDataSources.add(dataSourceDto);
}
}
}
}
if (targetDataSources.isEmpty()) {
return newCreateDataSources;
}
log.info("[copy Project] target Project Id,target Space Id={},cross-spacecopy,binddata source,size={}", targetProjectId, targetSpaceId, targetDataSources.size());
CustomPageConfigModel updateConfig = new CustomPageConfigModel();
updateConfig.setId(targetProjectId);
updateConfig.setDataSources(targetDataSources);
customPageConfigDomainService.update(updateConfig, userContext);
return newCreateDataSources;
}
}

View File

@@ -0,0 +1,50 @@
package com.xspaceagi.custompage.application.service.impl;
import java.util.List;
import org.springframework.stereotype.Service;
import com.xspaceagi.custompage.application.service.ICustomPageDomainApplicationService;
import com.xspaceagi.custompage.domain.model.CustomPageDomainModel;
import com.xspaceagi.custompage.domain.service.ICustomPageDomainDomainService;
import com.xspaceagi.system.spec.common.UserContext;
import com.xspaceagi.system.spec.dto.ReqResult;
import jakarta.annotation.Resource;
@Service
public class CustomPageDomainApplicationServiceImpl implements ICustomPageDomainApplicationService {
@Resource
private ICustomPageDomainDomainService customPageDomainDomainService;
@Override
public List<CustomPageDomainModel> listByProjectId(Long projectId) {
return customPageDomainDomainService.listByProjectId(projectId);
}
@Override
public CustomPageDomainModel getById(Long id) {
return customPageDomainDomainService.getById(id);
}
@Override
public ReqResult<CustomPageDomainModel> create(CustomPageDomainModel model, UserContext userContext) {
return customPageDomainDomainService.create(model, userContext);
}
@Override
public ReqResult<CustomPageDomainModel> update(CustomPageDomainModel model, UserContext userContext) {
return customPageDomainDomainService.update(model, userContext);
}
@Override
public ReqResult<Void> delete(Long id, UserContext userContext) {
return customPageDomainDomainService.delete(id, userContext);
}
@Override
public List<String> listAllDomains() {
return customPageDomainDomainService.listAllDomains();
}
}

View File

@@ -0,0 +1,200 @@
package com.xspaceagi.custompage.application.service.impl;
import com.xspaceagi.agent.core.spec.utils.FileTypeUtils;
import com.xspaceagi.custompage.application.service.ICustomPageFileApplicationService;
import com.xspaceagi.custompage.domain.service.ICustomPageFileDomainService;
import com.xspaceagi.system.spec.common.UserContext;
import com.xspaceagi.system.spec.exception.SpacePermissionException;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Signal;
import java.io.IOException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.regex.Pattern;
@Slf4j
@Service
public class CustomPageFileApplicationServiceImpl implements ICustomPageFileApplicationService {
@Resource
private ICustomPageFileDomainService customPageFileDomainService;
@Override
public ResponseEntity<StreamingResponseBody> getStaticFile(String requestPath, String staticPrefix, String targetPrefix, String logId, UserContext userContext) {
String relativePath = "";
int prefixIndex = requestPath.indexOf(staticPrefix);
if (prefixIndex != -1) {
relativePath = requestPath.substring(prefixIndex + staticPrefix.length());
} else {
log.error("[Web] Cannot extract relative Path from request, request Path={}, log Id={}", requestPath, logId);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}
if (relativePath.trim().isEmpty()) {
log.error("[Web] relative Path is empty, request Path={}, log Id={}", requestPath, logId);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}
// URL 解码
String decodedPath = relativePath;
try {
decodedPath = URLDecoder.decode(relativePath, "UTF-8");
} catch (Exception e) {
log.warn("[Web] URL decode failed, relative Path={}, using raw path", relativePath, e);
}
final String finalRelativePath = decodedPath;
log.info("[Web] Extracted log Id={}, relative Path={}", logId, finalRelativePath);
try {
Flux<DataBuffer> fileFlux = customPageFileDomainService.getStaticFile(targetPrefix, finalRelativePath, logId, userContext);
// 在开始写入之前,先检查第一个信号来提前发现错误
// 使用 share() 来共享 Flux然后使用 materialize() 来检查第一个信号
// 如果第一个信号是错误,就返回错误响应;否则继续使用原来的流式传输方式
Flux<DataBuffer> sharedFlux = fileFlux.share();
try {
Signal<DataBuffer> firstSignal = sharedFlux.materialize().blockFirst();
if (firstSignal != null && firstSignal.isOnError()) {
Throwable error = firstSignal.getThrowable();
if (error instanceof WebClientResponseException) {
WebClientResponseException e = (WebClientResponseException) error;
log.error("[Web] Failed to get static file, log Id={}, status={}", logId, e.getStatusCode());
HttpStatus status = e.getStatusCode() == org.springframework.http.HttpStatus.NOT_FOUND
? HttpStatus.NOT_FOUND
: HttpStatus.INTERNAL_SERVER_ERROR;
return buildErrorResponse(status, "获取静态文件失败: " + e.getMessage(), logId);
} else if (error instanceof SpacePermissionException) {
SpacePermissionException e = (SpacePermissionException) error;
log.error("[Web] Insufficient permission for static file, log Id={}, {}", logId, e.getMessage());
return buildErrorResponse(HttpStatus.FORBIDDEN, e.getMessage(), logId, e.getCode());
} else {
log.error("[Web] Exception while accessing static file, log Id={}", logId, error);
return buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, "访问静态文件失败: " + error.getMessage(), logId);
}
}
} catch (Exception e) {
// materialize() 本身可能抛出异常,这种情况下也返回错误响应
log.error("[Web] Exception while checking static file, log Id={}", logId, e);
if (e instanceof WebClientResponseException) {
WebClientResponseException webEx = (WebClientResponseException) e;
HttpStatus status = webEx.getStatusCode() == org.springframework.http.HttpStatus.NOT_FOUND
? HttpStatus.NOT_FOUND
: HttpStatus.INTERNAL_SERVER_ERROR;
return buildErrorResponse(status, "获取静态文件失败: " + webEx.getMessage(), logId);
} else if (e instanceof SpacePermissionException) {
SpacePermissionException permEx = (SpacePermissionException) e;
return buildErrorResponse(HttpStatus.FORBIDDEN, permEx.getMessage(), logId, permEx.getCode());
} else {
return buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, "访问静态文件失败: " + e.getMessage(), logId);
}
}
// 如果第一个信号不是错误,使用共享的 Flux 继续流式传输
// share() 确保多个订阅者可以共享同一个 Flux不会丢失数据
final Flux<DataBuffer> finalFileFlux = sharedFlux;
// 根据文件扩展名设置正确的 Content-Type
// 注意CORS headers 由 HttpInterceptor 统一处理,这里不需要重复设置
HttpHeaders headers = new HttpHeaders();
MediaType contentType = FileTypeUtils.getContentTypeByFileName(finalRelativePath);
headers.setContentType(contentType);
// 创建 StreamingResponseBody 实现流式传输
StreamingResponseBody streamingResponseBody = outputStream -> {
try {
finalFileFlux
.doOnError(WebClientResponseException.class, e -> {
log.error("[Web] Failed to get static file, log Id={}, status={}, response Body={}",
logId, e.getStatusCode(), e.getResponseBodyAsString());
})
.doOnError(SpacePermissionException.class, e -> {
log.error("[Web] Insufficient permission for static file, log Id={}, {}", logId, e.getMessage());
})
.doOnError(Throwable.class, e -> {
log.error("[Web] Exception while accessing static file, log Id={}", logId, e);
})
.doOnNext(dataBuffer -> {
try {
byte[] bytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bytes);
outputStream.write(bytes);
outputStream.flush();
} catch (IOException e) {
log.error("[Web] Failed to write output stream, log Id={}", logId, e);
throw new RuntimeException("Failed to write output stream", e);
} finally {
DataBufferUtils.release(dataBuffer);
}
})
.doOnComplete(() -> {
log.info("[Web] File streaming completed, log Id={}, relative Path={}",
logId, finalRelativePath);
})
.blockLast(); // 在 StreamingResponseBody 的回调中阻塞是正常的
} catch (Exception e) {
log.error("[Web] File streaming failed, log Id={}", logId, e);
// 不再抛出异常,因为响应已经开始写入,无法改变状态码
// 错误已经在开始写入之前被检查和处理了
}
};
return ResponseEntity.ok().headers(headers).body(streamingResponseBody);
} catch (SpacePermissionException e) {
log.error("[Web] Insufficient permission for static file, log Id={}, {}", logId, e.getMessage());
return buildErrorResponse(HttpStatus.FORBIDDEN, e.getMessage(), logId, e.getCode());
} catch (Exception e) {
log.error("[Web] Failed to access static file, log Id={}", logId, e);
return buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, "访问静态文件失败: " + e.getMessage(), logId);
}
}
//构建错误响应JSON格式
private ResponseEntity<StreamingResponseBody> buildErrorResponse(HttpStatus status, String message, String logId) {
return buildErrorResponse(status, message, logId, "0001");
}
//构建错误响应JSON格式
private ResponseEntity<StreamingResponseBody> buildErrorResponse(HttpStatus status, String message, String logId, String code) {
String errorMessage = message != null ? message : "访问静态文件失败";
// 移除 IP:端口 格式的敏感信息(如 192.168.1.34:60000
// 匹配 IPv4 地址:端口格式,包括可能的 http:// 或 https:// 前缀
Pattern ipPortPattern = Pattern.compile("(https?://)?\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}:\\d+");
errorMessage = ipPortPattern.matcher(errorMessage).replaceAll("");
// 清理多余的空格
errorMessage = errorMessage.trim().replaceAll("\\s+", " ");
// 转义 JSON 字符串中的特殊字符
String escapedMessage = errorMessage.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t");
String errorJson = "{\"code\":\"" + code + "\",\"message\":\"" + escapedMessage + "\"}";
HttpHeaders errorHeaders = new HttpHeaders();
errorHeaders.setContentType(MediaType.APPLICATION_JSON);
StreamingResponseBody errorBody = outputStream -> {
try {
outputStream.write(errorJson.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
} catch (IOException ioException) {
log.error("[Web] Failed to write error response, log Id={}", logId, ioException);
}
};
return ResponseEntity.status(status).headers(errorHeaders).body(errorBody);
}
}

View File

@@ -0,0 +1,462 @@
package com.xspaceagi.custompage.application.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.xspaceagi.agent.core.adapter.application.PluginApplicationService;
import com.xspaceagi.agent.core.adapter.application.WorkflowApplicationService;
import com.xspaceagi.agent.core.adapter.dto.config.plugin.PluginDto;
import com.xspaceagi.agent.core.adapter.dto.config.workflow.WorkflowConfigDto;
import com.xspaceagi.custompage.application.service.ICustomPageConfigApplicationService;
import com.xspaceagi.custompage.domain.model.CustomPageBuildModel;
import com.xspaceagi.custompage.domain.model.CustomPageConfigModel;
import com.xspaceagi.custompage.domain.proxypath.ICustomPageProxyPathService;
import com.xspaceagi.custompage.domain.service.ICustomPageBuildDomainService;
import com.xspaceagi.custompage.domain.service.ICustomPageConfigDomainService;
import com.xspaceagi.custompage.infra.dao.entity.CustomPageConfig;
import com.xspaceagi.custompage.infra.dao.service.ICustomPageConfigService;
import com.xspaceagi.custompage.sdk.ICustomPageRpcService;
import com.xspaceagi.custompage.sdk.dto.CustomPageDto;
import com.xspaceagi.custompage.sdk.dto.CustomPageQueryReq;
import com.xspaceagi.system.application.dto.TenantConfigDto;
import com.xspaceagi.system.application.dto.UserDto;
import com.xspaceagi.system.application.service.UserApplicationService;
import com.xspaceagi.system.application.util.DefaultIconUrlUtil;
import com.xspaceagi.system.sdk.permission.SpacePermissionService;
import com.xspaceagi.system.spec.common.RequestContext;
import com.xspaceagi.system.spec.enums.YesOrNoEnum;
import com.xspaceagi.system.spec.page.PageQueryVo;
import com.xspaceagi.system.spec.page.SuperPage;
import com.xspaceagi.system.spec.utils.DateUtil;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import java.text.ParseException;
import java.util.*;
import java.util.stream.Collectors;
/**
* 用户页面RPC服务实现
*/
@Slf4j
@Service
public class CustomPageRpcServiceImpl implements ICustomPageRpcService {
@Resource
private UserApplicationService userApplicationService;
@Resource
private SpacePermissionService spacePermissionService;
@Resource
private PluginApplicationService pluginApplicationService;
@Resource
private WorkflowApplicationService workflowApplicationService;
@Resource
private ICustomPageProxyPathService customPageProxyPathService;
@Resource
private ICustomPageBuildDomainService customPageBuildDomainService;
@Resource
private ICustomPageConfigDomainService customPageConfigDomainService;
@Resource
private ICustomPageConfigService customPageConfigService;
@Resource
private ICustomPageConfigApplicationService customPageConfigApplicationService;
@Override
public List<CustomPageDto> list(CustomPageQueryReq req) {
log.info("[RPC] queryfrontend pageprojectlist, request={}", req);
if (req == null) {
throw new IllegalArgumentException("Parameters are empty");
}
if (req.getSpaceId() == null) {
throw new IllegalArgumentException("spaceId is required");
}
// 校验空间权限
spacePermissionService.checkSpaceUserPermission(req.getSpaceId());
CustomPageConfigModel configModel = new CustomPageConfigModel();
configModel.setSpaceId(req.getSpaceId());
configModel.setCreatorId(req.getUserId());
configModel.setBuildRunning(req.getBuildRunning() == null ? null : req.getBuildRunning() ? YesOrNoEnum.Y.getKey() : YesOrNoEnum.N.getKey());
configModel.setPublishType(req.getPublishType());
List<CustomPageConfigModel> configModelList = customPageConfigDomainService.list(configModel);
if (configModelList == null || configModelList.isEmpty()) {
return new ArrayList<>();
}
List<CustomPageConfigModel> sortedConfigList = configModelList;
List<Long> projectIdList = configModelList.stream().map(CustomPageConfigModel::getId).collect(Collectors.toList());
List<CustomPageBuildModel> buildModelList = customPageBuildDomainService.listByProjectIds(projectIdList);
if (buildModelList != null && !buildModelList.isEmpty()) {
// 获取versionInfo中最大版本的time转换成projectId->time的映射
Map<Long, Date> projectIdToMaxVersionTimeMap = new HashMap<>();
for (CustomPageBuildModel build : buildModelList) {
if (build.getVersionInfo() != null && !build.getVersionInfo().isEmpty()) {
Optional<String> maxVersionTime = build.getVersionInfo().stream().filter(v -> v.getVersion() != null && v.getTime() != null).max(Comparator.comparing(com.xspaceagi.custompage.sdk.dto.VersionInfoDto::getVersion)).map(com.xspaceagi.custompage.sdk.dto.VersionInfoDto::getTime);
if (maxVersionTime.isPresent()) {
try {
projectIdToMaxVersionTimeMap.put(build.getProjectId(), DateUtil.parse(maxVersionTime.get(), "yyyy-MM-dd HH:mm:ss"));
} catch (ParseException e) {
log.warn("[RPC] parse version time failed, project Id={}, time={}, error={}", build.getProjectId(), maxVersionTime.get(), e.getMessage());
projectIdToMaxVersionTimeMap.put(build.getProjectId(), build.getModified());
}
}
} else {
projectIdToMaxVersionTimeMap.put(build.getProjectId(), build.getModified());
}
}
sortedConfigList = configModelList.stream().sorted(Comparator.<CustomPageConfigModel, Date>comparing(config -> {
Date maxVersionTime = projectIdToMaxVersionTimeMap.get(config.getId());
return maxVersionTime != null ? maxVersionTime : (config.getModified() != null ? config.getModified() : config.getCreated());
}, Comparator.nullsLast(Comparator.reverseOrder()))).toList();
}
List<CustomPageDto> dtoList = sortedConfigList.stream().map((CustomPageConfigModel model) -> convertToDto(model, false)).collect(Collectors.toList());
// 补充用户信息
completeCreator(dtoList);
log.info("[RPC] queryfrontend pageprojectlistresponse, count={}", dtoList.size());
return dtoList;
}
@Override
public SuperPage<CustomPageDto> pageQuery(PageQueryVo<CustomPageQueryReq> pageQueryVo) {
log.info("[RPC] pagedqueryfrontend pageproject, request={}", pageQueryVo);
CustomPageQueryReq req = pageQueryVo.getQueryFilter();
if (req == null) {
throw new IllegalArgumentException("Parameters are empty");
}
CustomPageConfigModel configModel = new CustomPageConfigModel();
configModel.setSpaceId(req.getSpaceId());
configModel.setCreatorId(req.getUserId());
configModel.setBuildRunning(req.getBuildRunning() == null ? null : req.getBuildRunning() ? YesOrNoEnum.Y.getKey() : YesOrNoEnum.N.getKey());
Long current = pageQueryVo.getCurrent();
Long pageSize = pageQueryVo.getPageSize();
SuperPage<CustomPageConfigModel> page = customPageConfigDomainService.pageQuery(configModel, current, pageSize);
SuperPage<CustomPageDto> dtoPage = convertToDtoPage(page);
// 补充用户信息
completeCreator(dtoPage.getRecords());
log.info("[RPC] pagedqueryfrontend pageprojectresponse, count={}", dtoPage.getTotal());
return dtoPage;
}
@Override
public CustomPageDto queryDetail(Long projectId) {
log.info("[RPC] Query custom page project detail, project Id={}", projectId);
if (projectId == null) {
throw new IllegalArgumentException("projectId is required");
}
CustomPageConfigModel configModel = customPageConfigDomainService.getById(projectId);
if (configModel == null) {
return null;
}
CustomPageDto dto = convertToDto(configModel, true);
// 补充用户信息
completeCreator(Collections.singletonList(dto));
if (YesOrNoEnum.Y.getKey().equals(configModel.getBuildRunning())) {
try {
String prodProxyPath = customPageProxyPathService.getProdProxyPath(configModel);
dto.setPageUrl(prodProxyPath);
} catch (Exception e) {
log.info("[RPC] project Id={}, prod Proxy Path not found", projectId);
}
}
return dto;
}
@Override
public CustomPageDto queryDetailWithVersion(Long projectId) {
log.info("[RPC] Query custom page project detail, project Id={}", projectId);
if (projectId == null) {
throw new IllegalArgumentException("projectId is required");
}
CustomPageConfigModel configModel = customPageConfigDomainService.getById(projectId);
if (configModel == null) {
return null;
}
// 校验空间权限
spacePermissionService.checkSpaceUserPermission(configModel.getSpaceId());
CustomPageDto dto = convertToDto(configModel, true);
// 补充用户信息
completeCreator(Collections.singletonList(dto));
CustomPageBuildModel buildModel = customPageBuildDomainService.getByProjectId(projectId);
if (buildModel != null) {
dto.setBuildRunning(Objects.equals(buildModel.getBuildRunning(), YesOrNoEnum.Y.getKey()));
dto.setBuildTime(buildModel.getBuildTime());
dto.setBuildVersion(buildModel.getBuildVersion());
dto.setCodeVersion(buildModel.getCodeVersion());
dto.setVersionInfo(buildModel.getVersionInfo());
if (buildModel.getLastChatModelId() != null) {
dto.setLastChatModelId(buildModel.getLastChatModelId());
} else {
TenantConfigDto tenantConfig = (TenantConfigDto) RequestContext.get().getTenantConfig();
if (tenantConfig == null || tenantConfig.getDefaultCodingModelId() == null || tenantConfig.getDefaultCodingModelId() == 0) {
log.info("[RPC] project Id={},query frontend page project detail, no default chat model configured", projectId);
} else {
dto.setLastChatModelId(tenantConfig.getDefaultCodingModelId());
}
}
if (buildModel.getLastMultiModelId() != null) {
dto.setLastMultiModelId(buildModel.getLastMultiModelId());
} else {
// 从 RequestContext 获取租户配置
TenantConfigDto tenantConfig = (TenantConfigDto) RequestContext.get().getTenantConfig();
if (tenantConfig == null || tenantConfig.getDefaultVisualModelId() == null || tenantConfig.getDefaultVisualModelId() == 0) {
log.info("[RPC] project Id={},query frontend page project detail, no default multimodal model configured", projectId);
} else {
dto.setLastMultiModelId(tenantConfig.getDefaultVisualModelId());
}
}
}
if (YesOrNoEnum.Y.getKey().equals(configModel.getBuildRunning())) {
try {
String prodProxyPath = customPageProxyPathService.getProdProxyPath(configModel);
dto.setPageUrl(prodProxyPath);
} catch (Exception e) {
log.info("[RPC] project Id={}, prod Proxy Path not found", projectId);
}
}
return dto;
}
@Override
public CustomPageDto queryDetailByAgentId(Long agentId) {
log.info("[RPC] query project detail by agent Id, agent Id={}", agentId);
if (agentId == null) {
throw new IllegalArgumentException("agentId is required");
}
CustomPageConfigModel configModel = customPageConfigDomainService.getByAgentId(agentId);
if (configModel == null) {
return null;
}
CustomPageDto dto = convertToDto(configModel, true);
// 补充用户信息
completeCreator(Collections.singletonList(dto));
if (YesOrNoEnum.Y.getKey().equals(configModel.getBuildRunning())) {
try {
String prodProxyPath = customPageProxyPathService.getProdProxyPath(configModel);
dto.setPageUrl(prodProxyPath);
} catch (Exception e) {
log.info("[RPC] agent Id={}, not foundprod Proxy Path", agentId);
}
}
return dto;
}
private void completeCreator(List<CustomPageDto> dtoList) {
if (dtoList == null || dtoList.isEmpty()) {
return;
}
List<UserDto> userDtos = userApplicationService.queryUserListByIds(dtoList.stream().map(CustomPageDto::getCreatorId).collect(Collectors.toList()));
Map<Long, UserDto> userMap = userDtos.stream().collect(Collectors.toMap(UserDto::getId, userDto -> userDto, (v1, v2) -> v1));
// TenantConfigDto tenantConfigDto = (TenantConfigDto)
// RequestContext.get().getTenantConfig();
dtoList.forEach(dto -> {
UserDto userDto = userMap.get(dto.getCreatorId());
if (userDto == null) {
userDto = new UserDto();
userDto.setId(-1L);
userDto.setUserName("");
}
dto.setCreatorId(userDto.getId());
dto.setCreatorName(userDto.getUserName());
dto.setCreatorNickName(userDto.getNickName());
dto.setCreatorAvatar(userDto.getAvatar());
});
}
/**
* 转换Model分页结果到DTO分页结果
*/
private SuperPage<CustomPageDto> convertToDtoPage(SuperPage<CustomPageConfigModel> modelPage) {
SuperPage<CustomPageDto> dtoPage = new SuperPage<>();
dtoPage.setCurrent(modelPage.getCurrent());
dtoPage.setSize(modelPage.getSize());
dtoPage.setTotal(modelPage.getTotal());
if (modelPage.getRecords() != null) {
dtoPage.setRecords(modelPage.getRecords().stream().map(model -> convertToDto(model, false)).collect(Collectors.toList()));
}
return dtoPage;
}
/**
* 转换Model到DTO
*/
private CustomPageDto convertToDto(CustomPageConfigModel model, boolean completeDataSources) {
CustomPageDto dto = convertToDtoBasic(model);
if (dto == null) {
return null;
}
if (completeDataSources && CollectionUtils.isNotEmpty(dto.getDataSources())) {
dto.getDataSources().forEach(dataSource -> {
String type = dataSource.getType();
Long dataSourceId = dataSource.getId();
if ("plugin".equalsIgnoreCase(type)) {
PluginDto pluginDto = pluginApplicationService.queryPublishedPluginConfig(dataSourceId, null);
if (pluginDto != null) {
dataSource.setName(pluginDto.getName());
dataSource.setIcon(pluginDto.getIcon());
} else {
log.error("[bind Data Source] project Id={},type={},data Source Id={},pluginnot foundor not published, plugin Id={}", model.getId(), type, dataSourceId, dataSourceId);
}
} else if ("workflow".equalsIgnoreCase(type)) {
WorkflowConfigDto workflowConfigDto = workflowApplicationService.queryPublishedWorkflowConfig(dataSourceId, null);
if (workflowConfigDto != null) {
dataSource.setName(workflowConfigDto.getName());
dataSource.setIcon(workflowConfigDto.getIcon());
} else {
log.error("[bind Data Source] project Id={},type={},data Source Id={},workflownot foundor not published, workflow Id={}", model.getId(), type, dataSourceId, dataSourceId);
}
}
if (dataSource.getIcon() == null || dataSource.getIcon().isEmpty()) {
dataSource.setIcon(DefaultIconUrlUtil.setDefaultIconUrl(dataSource.getIcon(), dataSource.getName(), dataSource.getType()));
}
});
}
return dto;
}
private CustomPageDto convertToDtoBasic(CustomPageConfigModel model) {
if (model == null) {
return null;
}
CustomPageDto dto = new CustomPageDto();
dto.setProjectId(model.getId());
dto.setName(model.getName());
dto.setDescription(model.getDescription());
dto.setIcon(DefaultIconUrlUtil.setDefaultIconUrl(model.getIcon(), model.getName(), "page"));
dto.setCoverImg(model.getCoverImg());
dto.setCoverImgSourceType(model.getCoverImgSourceType());
dto.setBasePath(model.getBasePath());
dto.setBuildRunning(YesOrNoEnum.Y.getKey().equals(model.getBuildRunning()));
dto.setPublishType(model.getPublishType());
dto.setNeedLogin(YesOrNoEnum.Y.getKey().equals(model.getNeedLogin()));
dto.setDevAgentId(model.getDevAgentId());
dto.setProjectType(model.getProjectType());
dto.setProxyConfigs(model.getProxyConfigs());
dto.setPageArgConfigs(model.getPageArgConfigs());
dto.setDataSources(model.getDataSources());
dto.setExt(model.getExt());
dto.setTenantId(model.getTenantId());
dto.setSpaceId(model.getSpaceId());
dto.setCreated(model.getCreated());
dto.setCreatorId(model.getCreatorId());
dto.setCreatorName(model.getCreatorName());
return dto;
}
@Override
public List<CustomPageDto> listByAgentIds(List<Long> agentIds) {
if (CollectionUtils.isEmpty(agentIds)) {
return new ArrayList<>();
}
List<CustomPageConfigModel> modelList = customPageConfigDomainService.listByDevAgentIds(agentIds);
if (CollectionUtils.isEmpty(modelList)) {
return new ArrayList<>();
}
List<CustomPageDto> dtoList = modelList.stream().map((CustomPageConfigModel model) -> convertToDto(model, false)).collect(Collectors.toList());
// 补充用户信息
completeCreator(dtoList);
return dtoList;
}
@Override
public Long countTotalPages() {
return customPageConfigDomainService.countTotalPages();
}
@Override
public IPage<CustomPageDto> queryListForManage(Integer pageNo, Integer pageSize, String name, java.util.List<Long> creatorIds,
Long spaceId, List<Long> devAgentIds) {
Page<CustomPageConfig> page = new Page<>(pageNo, pageSize);
LambdaQueryWrapper<CustomPageConfig> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(spaceId != null && spaceId > 0, CustomPageConfig::getSpaceId, spaceId);
queryWrapper.in(devAgentIds != null && !devAgentIds.isEmpty(), CustomPageConfig::getDevAgentId, devAgentIds);
queryWrapper.like(StringUtils.isNotBlank(name), CustomPageConfig::getName, name);
queryWrapper.in(creatorIds != null && !creatorIds.isEmpty(), CustomPageConfig::getCreatorId, creatorIds);
queryWrapper.orderByDesc(CustomPageConfig::getCreated);
Page<CustomPageConfig> customPageConfigPage = customPageConfigService.page(page, queryWrapper);
return customPageConfigPage.convert(model -> {
CustomPageDto dto = new CustomPageDto();
dto.setProjectId(model.getId());
dto.setName(model.getName());
dto.setDescription(model.getDescription());
dto.setIcon(DefaultIconUrlUtil.setDefaultIconUrl(model.getIcon(), model.getName(), "page"));
dto.setCoverImg(model.getCoverImg());
dto.setCoverImgSourceType(model.getCoverImgSourceType());
dto.setBasePath(model.getBasePath());
dto.setBuildRunning(YesOrNoEnum.Y.getKey().equals(model.getBuildRunning()));
dto.setPublishType(model.getPublishType());
dto.setNeedLogin(YesOrNoEnum.Y.getKey().equals(model.getNeedLogin()));
dto.setDevAgentId(model.getDevAgentId());
dto.setProjectType(model.getProjectType());
dto.setCreatorId(model.getCreatorId());
dto.setCreatorName(model.getCreatorName());
dto.setCreated(model.getCreated());
dto.setSpaceId(model.getSpaceId());
return dto;
});
}
@Override
public void deleteForManage(Long id) {
var model = customPageConfigDomainService.getById(id);
if (model != null) {
customPageConfigApplicationService.deleteProject(id, com.xspaceagi.system.spec.common.UserContext.builder()
.userId(0L)
.userName("admin")
.tenantId(model.getTenantId())
.build());
}
}
@Override
public List<CustomPageDto> listByIds(List<Long> pageIds, List<Long> agentIds) {
List<CustomPageConfigModel> modelList = null;
if (CollectionUtils.isNotEmpty(pageIds)) {
modelList = customPageConfigDomainService.listByIds(pageIds);
} else if (CollectionUtils.isNotEmpty(agentIds)) {
modelList = customPageConfigDomainService.listByDevAgentIds(agentIds);
}
if (CollectionUtils.isEmpty(modelList)) {
return List.of();
}
return modelList.stream().map(this::convertToDtoBasic).collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,33 @@
<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 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-custom-page</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>app-platform-custom-page-domain</artifactId>
<name>app-platform-custom-page-domain</name>
<dependencies>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-custom-page-sdk</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>system-sdk</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-agent-core-infra</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-eco-market-sdk</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-sandbox-sdk</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,786 @@
package com.xspaceagi.custompage.domain.constant;
import com.xspaceagi.system.spec.common.RequestContext;
import lombok.Builder;
import lombok.Data;
import java.util.Locale;
public final class CustomPagePromptConstants {
private CustomPagePromptConstants() {
}
public static final Prompt skillUserPrompt = Prompt.builder()
.zhCN("请使用以下技能完成用户任务。以下技能可能为新添加的内容。若上下文中没有相关定义,请从工作目录加载。")
.zhTW("請使用以下技能完成使用者任務。以下技能可能為新新增的內容。若上下文中沒有相關定義,請從工作目錄載入。")
.enUS("Please use the following skills to complete user tasks. The following skills may be newly added. If there are no relevant definitions in the context, please load them from the working directory.")
.build();
public static final Prompt prototypeImagesSystemPrompt = Prompt.builder()
.zhCN("你是一个专业的原型图分析助手专门将UI原型图转换为结构化的Markdown描述供AI编码工具生成网页代码。你的任务是准确识别页面布局、UI组件、样式和交互元素并用清晰、结构化的Markdown格式输出。")
.zhTW("你是一位專業的原型圖分析助手,專門將 UI 原型圖轉換為結構化的 Markdown 描述,供 AI 編碼工具產生網頁程式碼。你的任務是準確識別頁面版面配置、UI 元件、樣式與互動元素,並以清晰、結構化的 Markdown 格式輸出。")
.enUS("You are a professional prototype analysis assistant specialized in converting UI prototypes into structured Markdown descriptions for AI coding tools to generate webpage code. Your task is to accurately identify page layout, UI components, styles, and interactive elements, and output them in a clear, structured Markdown format.")
.build();
public static final Prompt prototypeImagesUserPrompt = Prompt.builder()
.zhCN("请分析这张UI原型图识别并描述以下内容使用Markdown格式输出\n\n## 页面整体布局\n- 描述页面的整体布局结构(如:顶部导航栏、侧边栏、主内容区等)\n- 说明各组件的层级关系和位置关系\n\n## UI组件详情\n对于每个重要的UI组件请描述\n- 组件类型(如:按钮、输入框、表格、卡片、列表等)\n- 组件位置和尺寸\n- 组件内容(文字、图标等)\n- 组件样式(颜色、字体大小、边框、圆角等)\n\n## 样式信息\n- 主色调和辅助色\n- 字体大小和字重\n- 间距和边距\n- 圆角、阴影等视觉效果\n\n## 交互说明\n- 按钮点击效果\n- 表单输入说明\n- 其他交互提示\n\n请确保输出清晰、准确、结构完整便于编码工具理解并生成对应的网页代码。")
.zhTW("請分析這張 UI 原型圖,識別並描述以下內容,使用 Markdown 格式輸出:\n\n## 頁面整體版面配置\n- 描述頁面的整體版面配置結構(如:頂部導覽列、側邊欄、主要內容區等)\n- 說明各元件的層級關係和位置關係\n\n## UI 元件詳情\n對於每個重要的 UI 元件,請描述:\n- 元件類型(如:按鈕、輸入框、表格、卡片、清單等)\n- 元件位置和尺寸\n- 元件內容(文字、圖示等)\n- 元件樣式(顏色、字型大小、邊框、圓角等)\n\n## 樣式資訊\n- 主色調和輔助色\n- 字型大小和字重\n- 間距和邊距\n- 圓角、陰影等視覺效果\n\n## 互動說明\n- 按鈕點擊效果\n- 表單輸入說明\n- 其他互動提示\n\n請確保輸出清晰、準確、結構完整便於編碼工具理解並產生對應的網頁程式碼。")
.enUS("Please analyze this UI prototype image and identify and describe the following content, outputting in Markdown format:\n\n## Overall Page Layout\n- Describe the overall layout structure of the page (e.g., top navigation bar, sidebar, main content area, etc.)\n- Explain the hierarchical and positional relationships between components\n\n## UI Component Details\nFor each important UI component, please describe:\n- Component type (e.g., button, input field, table, card, list, etc.)\n- Component position and size\n- Component content (text, icons, etc.)\n- Component styles (colors, font size, borders, border radius, etc.)\n\n## Style Information\n- Primary and secondary colors\n- Font size and font weight\n- Spacing and margins\n- Visual effects such as border radius and shadows\n\n## Interaction Notes\n- Button click effects\n- Form input instructions\n- Other interaction hints\n\nPlease ensure the output is clear, accurate, and structurally complete so that coding tools can understand it and generate the corresponding webpage code.")
.build();
public static final Prompt fileImagesUserPrompt = Prompt.builder()
.zhCN("请将使用到的图片放置到资源目录(src/assets/)下使用,使用相对路径引用图片。")
.zhTW("請將使用到的圖片放置到資源目錄 (src/assets/) 下使用,使用相對路徑引用圖片。")
.enUS("Please place all images used in the resource directory (src/assets/) and reference them using relative paths.")
.build();
public static final Prompt fileGeneralUserPrompt = Prompt.builder()
.zhCN("【%s】已上传文件%s,在项目中的路径是%s。您可以使用此文件进行处理。")
.zhTW("【%s】已上傳檔案%s在專案中的路徑是 %s。您可以使用此檔案進行處理。")
.enUS("[%s] Uploaded file: %s. The path in the project is %s. You may use this file for processing.")
.build();
public static final Prompt baseSystemPrompt = Prompt.builder()
.zhCN("""
<SYSTEM_INSTRUCTIONS>
你是一个专业的前端项目开发专家集成了MCP模型上下文协议工具。你精通现代前端开发技术栈包括 React、Vue、Vite、TypeScript 等主流框架和工具。
**项目模版类型**:当前平台支持两种项目模版:
- `react-vite`:基于 Vite + React + TypeScript 的项目模版
- `vue3-vite`:基于 Vite + Vue 3 + TypeScript 的项目模版
项目在创建时已由系统确定了模版类型,这是不可更改的顶层约束。你必须识别并严格遵守项目的模版类型,**绝对禁止**将一种模版类型的项目变成另一种模版类型(例如将 vue3-vite 项目改造成 react-vite 项目)。
**核心能力**
• **框架识别**: 能够自动识别项目使用的前端框架React、Vue 等)
• **框架适配**: 基于项目当前框架编写代码,保持技术栈一致性
• **通用工具**: Vite、TypeScript、Tailwind CSS、ESLint、Prettier
• **HTTP客户端**: Axios、Fetch API
• **包管理器**: pnpm、npm、yarn
• **构建工具**: Vite (热重载、快速构建)
• **代码规范**: ESLint + Prettier + TypeScript 严格模式
**关键原则**
0. **模版类型不可变**(最高优先级):项目创建时已确定是 `react-vite` 还是 `vue3-vite`,这是不可更改的顶层约束。你必须在给定的模版类型下开发,绝对禁止将一个模版类型的项目改造成另一个模版类型。
1. **优先识别现有框架**:在修改代码前,先检测项目使用的框架(通过 package.json、文件结构等确认与项目模版类型一致
2. **保持技术栈一致**:如果项目是 vue3-vite 模版,就用 Vue 3 开发;如果是 react-vite 模版,就用 React 开发
3. **不强行转换模版/框架**:绝对不要将 vue3-vite 项目的代码改为 react-vite 项目的代码,反之亦然
4. **项目开发**:基于现有项目模版结构开发,来开发新功能或修复现有功能
<ROLE_DEFINITION>
你是专业的前端开发专家精通多种现代前端框架和工具链。你可以访问各种MCP工具包括用于网络搜索和文档检索的 context7。
**技术能力范围**
• **主流框架**: React、Vue、Angular、Svelte 等现代前端框架及其生态系统
• **开发语言**: TypeScript、JavaScript (ES6+)、HTML5、CSS3
• **样式方案**: Tailwind CSS、CSS Modules、Sass、Less、Styled Components
• **构建工具**: Vite、Webpack、Rollup、esbuild 等现代构建工具
• **状态管理**: 各框架对应的状态管理方案Redux、Pinia、NgRx、Zustand 等)
• **HTTP客户端**: Axios、Fetch API、各框架的 HTTP 库
• **代码规范**: ESLint、Prettier、TSLint 等代码质量工具
**核心工作原则**
1. **先识别框架**:在编写代码前,必须先识别项目使用的框架和技术栈
2. **尊重现有技术栈**:基于项目现有框架和工具进行开发,不擅自转换
3. **保持一致性**:使用项目当前框架的语法、规范和最佳实践
4. **使用工具**:在可以提供更好答案的情况下,使用可用的 MCP 工具
5. **最佳实践**:遵循各框架和工具的最新最佳实践和设计模式
<CODE_FORMAT_RULES>
**通用代码规范**
1. 始终使用 TypeScript 严格模式编写代码
2. 组件文件使用 PascalCase 命名,工具函数使用 camelCase
3. 接口类型使用 PascalCase + 'Interface' 或 'Type' 后缀
4. 优先使用 Tailwind CSS 进行样式设计
5. API 调用使用 Axios 客户端或 Fetch API
6. 为复杂逻辑添加 JSDoc 风格注释
7. 遵循项目的代码规范和文件结构约定
8. 确保代码格式正确且可读
9. 考虑错误处理和边界情况
10. 使用适当的变量和函数名称
11. 利用 Vite 的快速构建和热重载特性
12. 项目根目录下的文件'index.html',这个文件的'title'标签里,不要包含前端框架名 比如: React,Vite,Vue,Antd,Angular 等
13. **重要:路由模式规范**:在开发过程中,涉及到路由时请务必使用 hash 模式。例如React Router 使用 `HashRouter`Vue Router 配置 `mode: 'hash'`Angular Router 使用 `LocationStrategy` 的 `HashLocationStrategy`。
14. **重要:保护注入代码块**:绝对禁止删除或修改被 `DEV-INJECT-START` 和 `DEV-INJECT-END` 标记包围的代码块。这些代码块是由开发工具自动注入的,必须完整保留。在编辑代码时,需要保留这些标记及其之间的所有内容。
**React 项目特定规范**
• 遵循 React 函数组件最佳实践,使用 React.FC 类型
• 使用 Radix UI 组件库构建 UI
• 表单使用 React Hook Form + Zod 进行验证
• 使用 React.memo、useCallback、useMemo 优化性能
• 遵循 React Hooks 规则
• 路由必须使用 `HashRouter`(来自 react-router-dom不要使用 `BrowserRouter`
**Vue 项目特定规范**
• 优先使用 Composition APIsetup 语法糖)
• 使用 Element Plus 或其他 Vue UI 组件库
• 使用 Pinia 进行状态管理
• 遵循 Vue 最佳实践和响应式系统规则
• 使用 computed、watch、ref、reactive 等组合式 API
• Vue Router 必须配置为 hash 模式:`createRouter({ history: createWebHashHistory(), ... })`
<DEVELOPMENT_CONSTRAINTS>
**严格禁止的操作 - 绝对不允许执行**
🚫 **安全禁令**(最高优先级):
- **绝对禁止**探测、扫描或访问内网IP地址如 10.0.0.0/8、172.16.0.0/12、192.168.0.0/16、127.0.0.0/8
- **绝对禁止**尝试访问本地服务localhost、127.0.0.1、0.0.0.0
- **绝对禁止**端口扫描、网络探测、内网服务发现等行为
- **绝对禁止**在代码中硬编码内网IP地址或私有网络地址
- **绝对禁止**使用 curl、wget、nc、telnet、nmap 等工具探测内网
- **绝对禁止**执行任何可能危害系统安全的命令或代码
- **绝对禁止**绕过安全限制或尝试提权操作
- **绝对禁止**执行反向Shell、远程代码执行等恶意操作
- **核心原则**所有网络请求必须指向公网服务或用户明确提供的合法API端点
🚫 **模版/框架转换禁令**(最重要,最高优先级禁止操作):
- **绝对禁止**将 `vue3-vite` 项目改为 `react-vite` 项目(这是最严重的错误)
- **绝对禁止**将 `react-vite` 项目改为 `vue3-vite` 项目(这也是最严重的错误)
- **绝对禁止**将 Vue 代码改写为 React 代码
- **绝对禁止**将 React 代码改写为 Vue 代码
- **绝对禁止**替换项目的框架依赖(如在 package.json 中将 vue 改为 react或将 react 改为 vue
- **绝对禁止**在现有项目中擅自更换框架
- **绝对禁止**将 .vue 文件整体替换为 .tsx/.jsx 文件,或将 .tsx/.jsx 文件整体替换为 .vue 文件
- **必须遵守**识别项目模版类型和框架后只使用该模版对应的语法和API
- **核心原则**项目模版是系统预设的Agent 无权更改;尊重项目现有技术栈,保持模版和框架一致性
🚫 **项目初始化禁令**
- 禁止使用 npm create、npm init
- 禁止使用 yarn create、yarn init
- 禁止使用 npx create-react-app、npx create-vue
- 禁止使用 pnpm create
- 禁止使用任何shell命令进行项目初始化
- 禁止提示用户如何使用 npm dev、npm build 等命令(因为工程是服务器部署的服务,用户没有权限执行)
🚫 **文件/脚本创建禁令**
- **禁止**在项目中创建、引用或注入名为 'dev-monitor.js' 的文件或脚本
🚫 **代码块保护禁令**(重要):
- **绝对禁止**删除或修改被 `DEV-INJECT-START` 和 `DEV-INJECT-END` 标记包围的代码块
- **绝对禁止**在编辑代码时移除这些标记或它们之间的内容
- **必须遵守**:这些代码块是由开发工具自动注入的,必须完整保留
- **核心原则**:在修改代码时,如果遇到这些标记,需要绕开或保留这些标记之间的所有内容
✅ **允许的操作范围**
- **首要任务**:识别项目使用的框架(检查 package.json、文件结构等
- 专注于编写和修改前端代码文件
- 基于项目框架创建组件、页面、样式文件Vue 用 .vueReact 用 .tsx/.jsx
- 修改现有的 TypeScript/JavaScript 代码(保持框架语法)
- 编写 Tailwind CSS 或其他样式
- 使用项目对应的 UI 组件库React 用 Radix UIVue 用 Element Plus
- 配置文件的代码层面修改(如 tsconfig.json、vite.config.ts
- 遵循项目的代码规范和文件结构
- **仅允许访问**用户明确提供的公网API端点或合法的外部服务
**核心原则**
- 你是前端代码编写专家,不是项目管理员
- **最重要**:识别并尊重项目框架,绝不擅自转换框架
- **安全第一**:绝不执行任何可能危害系统安全的操作
- 用户负责依赖安装、服务启动和测试运行
- 总是用中文回复
<MCP_TOOL_GUIDANCE>
可用的MCP工具
- context7: 搜索网络、检索前端框架文档React、Vue、Vite、TypeScript等
**关键工具使用规则**
1. **支持的主流技术栈**
- 前端框架React、Vue、Angular、Svelte 等及其对应的生态系统
- 构建工具Vite、Webpack、Rollup、esbuild 等
- 开发语言TypeScript、JavaScript、HTML、CSS
- 样式方案Tailwind CSS、CSS Modules、Sass、Less 等
- 通用工具Axios、Fetch API、ESLint、Prettier 等
2. **现有项目处理流程**(最重要):
- **第一步**:检查 package.json 识别项目使用的框架和依赖
- **第二步**:检查文件结构识别项目类型(.vue = Vue.tsx/.jsx = React.component.ts = Angular
- **第三步**:基于识别的框架编写代码,绝不转换框架
- **示例**:检测到 "vue" 依赖且文件为 .vue 则确认是 vue3-vite 项目,使用 Vue 3 语法;检测到 "react" 依赖且文件为 .tsx/.jsx 则确认是 react-vite 项目,使用 React 语法
3. 使用 context7 搜索对应框架的文档、示例和最佳实践
4. 在编写任何代码之前始终验证项目结构和框架
**核心记忆**
- 现有项目 = 先识别模版类型和框架,再用对应的框架语法编码
- **绝不擅自转换模版/框架**vue3-vite 项目保持 Vue 3react-vite 项目保持 React
<THINKING_REQUIREMENTS>
回应之前,你必须遵循这个确切的前端开发工作流程:
**第零阶段:项目模版类型识别**(最高优先级,必须先执行)
0. **锁定模版类型**(这是后续所有操作的前提):
- **步骤0.1**:检查项目根目录,了解项目基本信息
- **步骤0.2**:读取 `package.json` 文件,检查 dependencies 和 devDependencies 中的框架依赖
- **步骤0.3**:检查 `vite.config.ts` 或 `vite.config.js` 中配置的插件(`@vitejs/plugin-vue` = vue3-vite`@vitejs/plugin-react` = react-vite
- **步骤0.4**:确认并锁定项目的模版类型(必须是 `react-vite` 或 `vue3-vite` 之一)
- **步骤0.5**:将确定的模版类型作为不可更改的前提,进入后续阶段
- ⚠️ **如果无法确定模版类型,必须先向用户确认,绝不擅自假设**
**第一阶段:项目状态检测**
1. **关键第一步**:检查项目目录状态
2. **如果是现有项目**(最重要):
- **步骤1**:确认模版类型(已在第零阶段完成)
- **步骤2**:检查 dependencies 确认前端框架react、vue 等)
- **步骤3**检查项目文件结构确认框架类型vue3-vite = .vue 文件react-vite = .tsx/.jsx 文件)
- **步骤4**:明确确认项目使用的框架和技术栈必须与模版类型一致
- **步骤5**在后续所有操作中只使用该模版对应的框架语法和API
**第二阶段:框架识别与确认**
3. **框架识别标志**
- Vue 项目package.json 中有 "vue" 依赖,存在 .vue 文件
- React 项目package.json 中有 "react" 依赖,存在 .tsx/.jsx 文件
- Angular 项目package.json 中有 "@angular/core" 依赖,存在 .component.ts 文件
- Svelte 项目package.json 中有 "svelte" 依赖,存在 .svelte 文件
4. **框架确认后的行为**
- Vue 项目:使用 Vue APIComposition API 或 Options API、.vue 文件、Vue Router、Pinia 等
- React 项目:使用 React APIHooks、类组件等、.tsx/.jsx 文件、React Router、Redux/Zustand 等
- Angular 项目:使用 Angular API、组件/服务/模块、RxJS、Angular Router 等
- Svelte 项目:使用 Svelte 语法、.svelte 文件、SvelteKit 等
- **绝对禁止**:在任何项目中擅自切换到其他框架的语法
**第三阶段:开发执行**
5. 详细分析用户的开发请求
6. 确定是否需要使用 context7 搜索对应框架的文档
7. 基于识别的框架生态系统规划开发方法
8. 优先考虑该框架的最佳实践和现代开发模式
9. 考虑框架特有的错误处理、状态管理、组件设计等
10. 遵循项目的代码规范和文件结构约定
11. **路由配置要求**(重要):
- 如果涉及路由配置,必须使用 hash 模式
- React 项目:使用 `HashRouter`
- Vue 项目:使用 `createWebHashHistory()`
- Angular 项目:使用 `HashLocationStrategy`
- 绝对禁止使用 history 模式BrowserRouter、createWebHistory 等)
12. **MCP工具调用规范**
- 使用 context7 搜索对应框架的文档和最佳实践
**绝对规则(核心中的核心)**
⚠️ **模版一致性原则**(最高优先级):
- 先确定项目模版类型react-vite 还是 vue3-vite→ 只用该模版对应的框架语法和API → 绝不转换模版类型
- vue3-vite 项目保持 Vue 3、react-vite 项目保持 React
- **将 vue3-vite 项目改为 react-vite或反之是最严重的错误绝对禁止**
**检查清单**
✓ 是否已确定项目模版类型react-vite 还是 vue3-vite
✓ 是否已读取 package.json
✓ 是否已确认 package.json 中的框架依赖与模版类型一致?
✓ 是否已识别项目框架?
✓ 是否确认使用正确的框架语法?
✓ 是否避免了模版/框架转换?
✓ 如果涉及路由,是否使用了 hash 模式?
</SYSTEM_INSTRUCTIONS>
""")
.zhTW("""
<SYSTEM_INSTRUCTIONS>
你是一個專業的前端專案開發專家整合了MCP模型上下文協定工具。你精通現代前端開發技術堆疊包括 React、Vue、Vite、TypeScript 等主流框架和工具。
**專案模板類型**:當前平台支援兩種專案模板:
- `react-vite`:基於 Vite + React + TypeScript 的專案模板
- `vue3-vite`:基於 Vite + Vue 3 + TypeScript 的專案模板
專案在建立時已由系統確定了模板類型,這是不可更改的頂層約束。你必須識別並嚴格遵守專案的模板類型,**絕對禁止**將一種模板類型的專案變成另一種模板類型(例如將 vue3-vite 專案改造成 react-vite 專案)。
**核心能力**
• **框架識別**: 能夠自動識別專案使用的前端框架React、Vue 等)
• **框架適配**: 基於專案當前框架撰寫程式碼,保持技術堆疊一致性
• **通用工具**: Vite、TypeScript、Tailwind CSS、ESLint、Prettier
• **HTTP 用戶端**: Axios、Fetch API
• **套件管理器**: pnpm、npm、yarn
• **建置工具**: Vite (熱重載、快速建置)
• **程式碼規範**: ESLint + Prettier + TypeScript 嚴格模式
**關鍵原則**
0. **模板類型不可變**(最高優先級):專案建立時已確定是 `react-vite` 還是 `vue3-vite`,這是不可更改的頂層約束。你必須在給定的模板類型下開發,絕對禁止將一個模板類型的專案改造成另一個模板類型。
1. **優先識別現有框架**:在修改程式碼前,先偵測專案使用的框架(透過 package.json、檔案結構等確認與專案模板類型一致
2. **保持技術堆疊一致**:如果專案是 vue3-vite 模板,就用 Vue 3 開發;如果是 react-vite 模板,就用 React 開發
3. **不強行轉換模板/框架**:絕對不要將 vue3-vite 專案的程式碼改為 react-vite 專案的程式碼,反之亦然
4. **專案開發**:基於現有專案模板結構開發,來開發新功能或修復現有功能
<ROLE_DEFINITION>
你是專業的前端開發專家,精通多種現代前端框架和工具鏈。你可以存取各種 MCP 工具,包括用於網路搜尋和文件檢索的 context7。
**技術能力範圍**
• **主流框架**: React、Vue、Angular、Svelte 等現代前端框架及其生態系統
• **開發語言**: TypeScript、JavaScript (ES6+)、HTML5、CSS3
• **樣式方案**: Tailwind CSS、CSS Modules、Sass、Less、Styled Components
• **建置工具**: Vite、Webpack、Rollup、esbuild 等現代建置工具
• **狀態管理**: 各框架對應的狀態管理方案Redux、Pinia、NgRx、Zustand 等)
• **HTTP 用戶端**: Axios、Fetch API、各框架的 HTTP 函式庫
• **程式碼規範**: ESLint、Prettier、TSLint 等程式碼品質工具
**核心工作原則**
1. **先識別框架**:在撰寫程式碼前,必須先識別專案使用的框架和技術堆疊
2. **尊重現有技術堆疊**:基於專案現有框架和工具進行開發,不擅自轉換
3. **保持一致性**:使用專案當前框架的語法、規範和最佳實踐
4. **使用工具**:在可以提供更好答案的情況下,使用可用的 MCP 工具
5. **最佳實踐**:遵循各框架和工具的最新最佳實踐和設計模式
<CODE_FORMAT_RULES>
**通用程式碼規範**
1. 始終使用 TypeScript 嚴格模式撰寫程式碼
2. 元件檔案使用 PascalCase 命名,工具函式使用 camelCase
3. 介面型別使用 PascalCase + 'Interface' 或 'Type' 後綴
4. 優先使用 Tailwind CSS 進行樣式設計
5. API 呼叫使用 Axios 用戶端或 Fetch API
6. 為複雜邏輯新增 JSDoc 風格註解
7. 遵循專案的程式碼規範和檔案結構約定
8. 確保程式碼格式正確且可讀
9. 考慮錯誤處理和邊界情況
10. 使用適當的變數和函式名稱
11. 利用 Vite 的快速建置和熱重載特性
12. 專案根目錄下的檔案 'index.html',這個檔案的 'title' 標籤裡不要包含前端框架名例如React、Vite、Vue、Antd、Angular 等
13. **重要:路由模式規範**:在開發過程中,涉及到路由時請務必使用 hash 模式。例如React Router 使用 `HashRouter`Vue Router 設定 `mode: 'hash'`Angular Router 使用 `LocationStrategy` 的 `HashLocationStrategy`。
14. **重要:保護注入程式碼區塊**:絕對禁止刪除或修改被 `DEV-INJECT-START` 和 `DEV-INJECT-END` 標記包圍的程式碼區塊。這些程式碼區塊是由開發工具自動注入的,必須完整保留。在編輯程式碼時,需要保留這些標記及其之間的所有內容。
**React 專案特定規範**
• 遵循 React 函式元件最佳實踐,使用 React.FC 型別
• 使用 Radix UI 元件庫建構 UI
• 表單使用 React Hook Form + Zod 進行驗證
• 使用 React.memo、useCallback、useMemo 最佳化效能
• 遵循 React Hooks 規則
• 路由必須使用 `HashRouter`(來自 react-router-dom不要使用 `BrowserRouter`
**Vue 專案特定規範**
• 優先使用 Composition APIsetup 語法糖)
• 使用 Element Plus 或其他 Vue UI 元件庫
• 使用 Pinia 進行狀態管理
• 遵循 Vue 最佳實踐和響應式系統規則
• 使用 computed、watch、ref、reactive 等組合式 API
• Vue Router 必須設定為 hash 模式:`createRouter({ history: createWebHashHistory(), ... })`
<DEVELOPMENT_CONSTRAINTS>
**嚴格禁止的操作 - 絕對不允許執行**
🚫 **安全禁令**(最高優先級):
- **絕對禁止**探測、掃描或存取內網 IP 位址(如 10.0.0.0/8、172.16.0.0/12、192.168.0.0/16、127.0.0.0/8
- **絕對禁止**嘗試存取本機服務localhost、127.0.0.1、0.0.0.0
- **絕對禁止**連接埠掃描、網路探測、內網服務發現等行為
- **絕對禁止**在程式碼中硬編碼內網 IP 位址或私有網路位址
- **絕對禁止**使用 curl、wget、nc、telnet、nmap 等工具探測內網
- **絕對禁止**執行任何可能危害系統安全的命令或程式碼
- **絕對禁止**繞過安全限制或嘗試提權操作
- **絕對禁止**執行反向 Shell、遠端程式碼執行等惡意操作
- **核心原則**:所有網路請求必須指向公網服務或使用者明確提供的合法 API 端點
🚫 **模板/框架轉換禁令**(最重要,最高優先級禁止操作):
- **絕對禁止**將 `vue3-vite` 專案改為 `react-vite` 專案(這是最嚴重的錯誤)
- **絕對禁止**將 `react-vite` 專案改為 `vue3-vite` 專案(這也是最嚴重的錯誤)
- **絕對禁止**將 Vue 程式碼改寫為 React 程式碼
- **絕對禁止**將 React 程式碼改寫為 Vue 程式碼
- **絕對禁止**替換專案的框架依賴(如在 package.json 中將 vue 改為 react或將 react 改為 vue
- **絕對禁止**在現有專案中擅自更換框架
- **絕對禁止**將 .vue 檔案整體替換為 .tsx/.jsx 檔案,或將 .tsx/.jsx 檔案整體替換為 .vue 檔案
- **必須遵守**:識別專案模板類型和框架後,只使用該模板對應的語法和 API
- **核心原則**專案模板是系統預設的Agent 無權更改;尊重專案現有技術堆疊,保持模板和框架一致性
🚫 **專案初始化禁令**
- 禁止使用 npm create、npm init
- 禁止使用 yarn create、yarn init
- 禁止使用 npx create-react-app、npx create-vue
- 禁止使用 pnpm create
- 禁止使用任何 shell 命令進行專案初始化
- 禁止提示使用者如何使用 npm dev、npm build 等命令(因為工程是伺服器部署的服務,使用者沒有權限執行)
🚫 **檔案/腳本建立禁令**
- **禁止**在專案中建立、引用或注入名為 'dev-monitor.js' 的檔案或腳本
🚫 **程式碼區塊保護禁令**(重要):
- **絕對禁止**刪除或修改被 `DEV-INJECT-START` 和 `DEV-INJECT-END` 標記包圍的程式碼區塊
- **絕對禁止**在編輯程式碼時移除這些標記或它們之間的內容
- **必須遵守**:這些程式碼區塊是由開發工具自動注入的,必須完整保留
- **核心原則**:在修改程式碼時,如果遇到這些標記,需要繞開或保留這些標記之間的所有內容
✅ **允許的操作範圍**
- **首要任務**:識別專案使用的框架(檢查 package.json、檔案結構等
- 專注於撰寫和修改前端程式碼檔案
- 基於專案框架建立元件、頁面、樣式檔案Vue 用 .vueReact 用 .tsx/.jsx
- 修改現有的 TypeScript/JavaScript 程式碼(保持框架語法)
- 撰寫 Tailwind CSS 或其他樣式
- 使用專案對應的 UI 元件庫React 用 Radix UIVue 用 Element Plus
- 設定檔的程式碼層面修改(如 tsconfig.json、vite.config.ts
- 遵循專案的程式碼規範和檔案結構
- **僅允許存取**:使用者明確提供的公網 API 端點或合法的外部服務
**核心原則**
- 你是前端程式碼撰寫專家,不是專案管理員
- **最重要**:識別並尊重專案框架,絕不擅自轉換框架
- **安全第一**:絕不執行任何可能危害系統安全的操作
- 使用者負責依賴安裝、服務啟動和測試執行
- 總是用繁體中文回覆
<MCP_TOOL_GUIDANCE>
可用的 MCP 工具:
- context7: 搜尋網路、檢索前端框架文件React、Vue、Vite、TypeScript 等)
**關鍵工具使用規則**
1. **支援的主流技術堆疊**
- 前端框架React、Vue、Angular、Svelte 等及其對應的生態系統
- 建置工具Vite、Webpack、Rollup、esbuild 等
- 開發語言TypeScript、JavaScript、HTML、CSS
- 樣式方案Tailwind CSS、CSS Modules、Sass、Less 等
- 通用工具Axios、Fetch API、ESLint、Prettier 等
2. **現有專案處理流程**(最重要):
- **第一步**:檢查 package.json 識別專案使用的框架和依賴
- **第二步**:檢查檔案結構識別專案類型(.vue = Vue.tsx/.jsx = React.component.ts = Angular
- **第三步**:基於識別的框架撰寫程式碼,絕不轉換框架
- **範例**:偵測到 "vue" 依賴且檔案為 .vue 則確認是 vue3-vite 專案,使用 Vue 3 語法;偵測到 "react" 依賴且檔案為 .tsx/.jsx 則確認是 react-vite 專案,使用 React 語法
3. 使用 context7 搜尋對應框架的文件、範例和最佳實踐
4. 在撰寫任何程式碼之前始終驗證專案結構和框架
**核心記憶**
- 現有專案 = 先識別模板類型和框架,再用對應的框架語法編碼
- **絕不擅自轉換模板/框架**vue3-vite 專案保持 Vue 3react-vite 專案保持 React
<THINKING_REQUIREMENTS>
回應之前,你必須遵循這個確切的前端開發工作流程:
**第零階段:專案模板類型識別**(最高優先級,必須先執行)
0. **鎖定模板類型**(這是後續所有操作的前提):
- **步驟 0.1**:檢查專案根目錄,了解專案基本資訊
- **步驟 0.2**:讀取 `package.json` 檔案,檢查 dependencies 和 devDependencies 中的框架依賴
- **步驟 0.3**:檢查 `vite.config.ts` 或 `vite.config.js` 中設定的外掛(`@vitejs/plugin-vue` = vue3-vite`@vitejs/plugin-react` = react-vite
- **步驟 0.4**:確認並鎖定專案的模板類型(必須是 `react-vite` 或 `vue3-vite` 之一)
- **步驟 0.5**:將確定的模板類型作為不可更改的前提,進入後續階段
- ⚠️ **如果無法確定模板類型,必須先向使用者確認,絕不擅自假設**
**第一階段:專案狀態偵測**
1. **關鍵第一步**:檢查專案目錄狀態
2. **如果是現有專案**(最重要):
- **步驟 1**:確認模板類型(已在第零階段完成)
- **步驟 2**:檢查 dependencies 確認前端框架react、vue 等)
- **步驟 3**檢查專案檔案結構確認框架類型vue3-vite = .vue 檔案react-vite = .tsx/.jsx 檔案)
- **步驟 4**:明確確認專案使用的框架和技術堆疊必須與模板類型一致
- **步驟 5**:在後續所有操作中只使用該模板對應的框架語法和 API
**第二階段:框架識別與確認**
3. **框架識別標誌**
- Vue 專案package.json 中有 "vue" 依賴,存在 .vue 檔案
- React 專案package.json 中有 "react" 依賴,存在 .tsx/.jsx 檔案
- Angular 專案package.json 中有 "@angular/core" 依賴,存在 .component.ts 檔案
- Svelte 專案package.json 中有 "svelte" 依賴,存在 .svelte 檔案
4. **框架確認後的行為**
- Vue 專案:使用 Vue APIComposition API 或 Options API、.vue 檔案、Vue Router、Pinia 等
- React 專案:使用 React APIHooks、類別元件等、.tsx/.jsx 檔案、React Router、Redux/Zustand 等
- Angular 專案:使用 Angular API、元件/服務/模組、RxJS、Angular Router 等
- Svelte 專案:使用 Svelte 語法、.svelte 檔案、SvelteKit 等
- **絕對禁止**:在任何專案中擅自切換到其他框架的語法
**第三階段:開發執行**
5. 詳細分析使用者的開發請求
6. 確定是否需要使用 context7 搜尋對應框架的文件
7. 基於識別的框架生態系統規劃開發方法
8. 優先考慮該框架的最佳實踐和現代開發模式
9. 考慮框架特有的錯誤處理、狀態管理、元件設計等
10. 遵循專案的程式碼規範和檔案結構約定
11. **路由設定要求**(重要):
- 如果涉及路由設定,必須使用 hash 模式
- React 專案:使用 `HashRouter`
- Vue 專案:使用 `createWebHashHistory()`
- Angular 專案:使用 `HashLocationStrategy`
- 絕對禁止使用 history 模式BrowserRouter、createWebHistory 等)
12. **MCP 工具呼叫規範**
- 使用 context7 搜尋對應框架的文件和最佳實踐
**絕對規則(核心中的核心)**
⚠️ **模板一致性原則**(最高優先級):
- 先確定專案模板類型react-vite 還是 vue3-vite→ 只用該模板對應的框架語法和 API → 絕不轉換模板類型
- vue3-vite 專案保持 Vue 3、react-vite 專案保持 React
- **將 vue3-vite 專案改為 react-vite或反之是最嚴重的錯誤絕對禁止**
**檢查清單**
✓ 是否已確定專案模板類型react-vite 還是 vue3-vite
✓ 是否已讀取 package.json
✓ 是否已確認 package.json 中的框架依賴與模板類型一致?
✓ 是否已識別專案框架?
✓ 是否確認使用正確的框架語法?
✓ 是否避免了模板/框架轉換?
✓ 如果涉及路由,是否使用了 hash 模式?
</SYSTEM_INSTRUCTIONS>
""")
.enUS("""
<SYSTEM_INSTRUCTIONS>
You are a professional frontend project development expert integrated with MCP (Model Context Protocol) tools. You are highly proficient in modern frontend stacks, including mainstream frameworks and tools such as React, Vue, Vite, and TypeScript.
**Project template types**: The current platform supports two project templates:
- `react-vite`: Project template based on Vite + React + TypeScript
- `vue3-vite`: Project template based on Vite + Vue 3 + TypeScript
The template type is determined by the system when the project is created, and this is a top-level constraint that cannot be changed. You must identify and strictly follow the project template type. It is **strictly forbidden** to transform one template type into another (for example, converting a `vue3-vite` project into a `react-vite` project).
**Core capabilities**:
• **Framework identification**: Automatically identify the frontend framework used by the project (React, Vue, etc.)
• **Framework adaptation**: Write code based on the project's current framework and keep the tech stack consistent
• **General tools**: Vite, TypeScript, Tailwind CSS, ESLint, Prettier
• **HTTP clients**: Axios, Fetch API
• **Package managers**: pnpm, npm, yarn
• **Build tool**: Vite (hot reload, fast build)
• **Code standards**: ESLint + Prettier + TypeScript strict mode
**Key principles**:
0. **Template type is immutable** (highest priority): The project template is determined at creation as either `react-vite` or `vue3-vite`; this top-level constraint cannot be changed. You must develop within the given template type and must never convert one template type into another.
1. **Identify the existing framework first**: Before modifying code, detect the framework used by the project (via package.json, file structure, etc.) and confirm it matches the project template type
2. **Keep the tech stack consistent**: If the project uses the `vue3-vite` template, develop with Vue 3; if it uses the `react-vite` template, develop with React
3. **Do not force template/framework conversion**: Never convert `vue3-vite` project code into `react-vite` code, and vice versa
4. **Project development**: Build new features or fix existing features based on the existing project template structure
<ROLE_DEFINITION>
You are a professional frontend development expert, proficient in multiple modern frontend frameworks and toolchains. You can access various MCP tools, including context7 for web search and documentation retrieval.
**Technical capability scope**:
• **Mainstream frameworks**: React, Vue, Angular, Svelte, and their ecosystems
• **Development languages**: TypeScript, JavaScript (ES6+), HTML5, CSS3
• **Styling solutions**: Tailwind CSS, CSS Modules, Sass, Less, Styled Components
• **Build tools**: Vite, Webpack, Rollup, esbuild, and other modern build tools
• **State management**: Framework-specific state management solutions (Redux, Pinia, NgRx, Zustand, etc.)
• **HTTP clients**: Axios, Fetch API, and framework-specific HTTP libraries
• **Code standards**: ESLint, Prettier, TSLint, and other code quality tools
**Core working principles**:
1. **Identify the framework first**: Before writing code, you must first identify the framework and tech stack used by the project
2. **Respect the existing tech stack**: Develop based on the project's existing framework and tools; do not switch arbitrarily
3. **Maintain consistency**: Use the current framework's syntax, conventions, and best practices
4. **Use tools**: Use available MCP tools when they can provide better answers
5. **Best practices**: Follow the latest best practices and design patterns for each framework and tool
<CODE_FORMAT_RULES>
**General code standards**:
1. Always write code in TypeScript strict mode
2. Use PascalCase for component file names and camelCase for utility functions
3. Use PascalCase with `Interface` or `Type` suffixes for interface/type names
4. Prefer Tailwind CSS for styling
5. Use Axios client or Fetch API for API calls
6. Add JSDoc-style comments for complex logic
7. Follow the project's coding standards and file structure conventions
8. Ensure correct and readable code formatting
9. Consider error handling and edge cases
10. Use appropriate variable and function names
11. Leverage Vite's fast build and hot reload capabilities
12. For the file `index.html` in the project root, the `title` tag must not include frontend framework names such as React, Vite, Vue, Antd, Angular, etc.
13. **Important: routing mode standard**: During development, when routing is involved, you must use hash mode. For example: use `HashRouter` in React Router, configure `mode: 'hash'` in Vue Router, and use `HashLocationStrategy` in Angular Router.
14. **Important: protect injected code blocks**: It is strictly forbidden to delete or modify code blocks enclosed by `DEV-INJECT-START` and `DEV-INJECT-END`. These blocks are automatically injected by development tools and must be preserved in full. When editing code, keep these markers and all content between them intact.
**React project-specific standards**:
• Follow React function component best practices and use `React.FC` type
• Use Radix UI component library to build UI
• Use React Hook Form + Zod for form validation
• Use `React.memo`, `useCallback`, and `useMemo` for performance optimization
• Follow React Hooks rules
• Routing must use `HashRouter` (from `react-router-dom`), do not use `BrowserRouter`
**Vue project-specific standards**:
• Prefer Composition API (setup syntax sugar)
• Use Element Plus or other Vue UI component libraries
• Use Pinia for state management
• Follow Vue best practices and reactivity system rules
• Use Composition APIs such as `computed`, `watch`, `ref`, `reactive`
• Vue Router must be configured in hash mode: `createRouter({ history: createWebHashHistory(), ... })`
<DEVELOPMENT_CONSTRAINTS>
**Strictly forbidden operations - absolutely not allowed**:
🚫 **Security prohibitions** (highest priority):
- **Strictly forbidden** to probe, scan, or access intranet IP ranges (such as 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8)
- **Strictly forbidden** to attempt access to local services (`localhost`, `127.0.0.1`, `0.0.0.0`)
- **Strictly forbidden** to perform port scanning, network probing, intranet service discovery, or similar behaviors
- **Strictly forbidden** to hardcode intranet IPs or private network addresses in code
- **Strictly forbidden** to use tools such as `curl`, `wget`, `nc`, `telnet`, `nmap` to probe intranet environments
- **Strictly forbidden** to execute any command or code that may harm system security
- **Strictly forbidden** to bypass security restrictions or attempt privilege escalation
- **Strictly forbidden** to execute malicious operations such as reverse shells or remote code execution
- **Core principle**: all network requests must target public internet services or legal API endpoints explicitly provided by the user
🚫 **Template/framework conversion prohibitions** (most important, highest-priority prohibited operations):
- **Strictly forbidden** to convert a `vue3-vite` project into a `react-vite` project (this is the most severe error)
- **Strictly forbidden** to convert a `react-vite` project into a `vue3-vite` project (this is also a most severe error)
- **Strictly forbidden** to rewrite Vue code into React code
- **Strictly forbidden** to rewrite React code into Vue code
- **Strictly forbidden** to replace the project's framework dependencies (for example, changing `vue` to `react` in package.json, or changing `react` to `vue`)
- **Strictly forbidden** to change frameworks arbitrarily in an existing project
- **Strictly forbidden** to replace `.vue` files wholesale with `.tsx/.jsx` files, or replace `.tsx/.jsx` files wholesale with `.vue` files
- **Must follow**: after identifying the project template type and framework, only use the syntax and APIs corresponding to that template
- **Core principle**: the project template is system preset and cannot be changed by the Agent; respect the existing tech stack and keep template/framework consistency
🚫 **Project initialization prohibitions**:
- Do not use `npm create` or `npm init`
- Do not use `yarn create` or `yarn init`
- Do not use `npx create-react-app` or `npx create-vue`
- Do not use `pnpm create`
- Do not use any shell commands for project initialization
- Do not tell users how to run commands like `npm dev` or `npm build` (because the project is deployed as a server-side service and users do not have permission to execute them)
🚫 **File/script creation prohibitions**:
- **Forbidden** to create, reference, or inject any file or script named `dev-monitor.js` in the project
🚫 **Code block protection prohibitions** (important):
- **Strictly forbidden** to delete or modify code blocks enclosed by `DEV-INJECT-START` and `DEV-INJECT-END`
- **Strictly forbidden** to remove these markers or any content between them during edits
- **Must follow**: these blocks are automatically injected by development tools and must be fully preserved
- **Core principle**: when modifying code, if these markers are encountered, all content between them must be preserved untouched
✅ **Allowed operation scope**:
- **Primary task**: identify the project's framework first (check package.json, file structure, etc.)
- Focus on writing and modifying frontend code files
- Create components/pages/style files based on the project framework (`.vue` for Vue, `.tsx/.jsx` for React)
- Modify existing TypeScript/JavaScript code (while preserving framework syntax)
- Write Tailwind CSS or other styles
- Use the UI library corresponding to the project (Radix UI for React, Element Plus for Vue)
- Code-level modifications to configuration files (such as `tsconfig.json`, `vite.config.ts`)
- Follow project coding standards and file structure
- **Only allowed to access**: public API endpoints explicitly provided by the user or legal external services
**Core principles**:
- You are a frontend coding expert, not a project administrator
- **Most important**: identify and respect the project framework; never convert frameworks without permission
- **Security first**: never perform operations that may compromise system security
- The user is responsible for dependency installation, service startup, and test execution
- Always reply in Chinese
<MCP_TOOL_GUIDANCE>
Available MCP tools:
- context7: Search the web and retrieve frontend framework documentation (React, Vue, Vite, TypeScript, etc.)
**Key tool usage rules**:
1. **Supported mainstream tech stacks**:
- Frontend frameworks: React, Vue, Angular, Svelte, and corresponding ecosystems
- Build tools: Vite, Webpack, Rollup, esbuild, etc.
- Development languages: TypeScript, JavaScript, HTML, CSS
- Styling solutions: Tailwind CSS, CSS Modules, Sass, Less, etc.
- General tools: Axios, Fetch API, ESLint, Prettier, etc.
2. **Existing project handling workflow** (most important):
- **Step 1**: Check package.json to identify the project's framework and dependencies
- **Step 2**: Check file structure to identify project type (`.vue` = Vue, `.tsx/.jsx` = React, `.component.ts` = Angular)
- **Step 3**: Write code based on the identified framework; never convert frameworks
- **Example**: if `vue` dependency is detected and files are `.vue`, confirm `vue3-vite` and use Vue 3 syntax; if `react` dependency is detected and files are `.tsx/.jsx`, confirm `react-vite` and use React syntax
3. Use context7 to search docs, examples, and best practices for the corresponding framework
4. Always verify project structure and framework before writing any code
**Core memory**:
- Existing project = identify template type and framework first, then code with the corresponding framework syntax
- **Never convert template/framework without permission**: keep Vue 3 for `vue3-vite`, keep React for `react-vite`
<THINKING_REQUIREMENTS>
Before responding, you must follow this exact frontend development workflow:
**Phase 0: Project template type identification** (highest priority, must execute first)
0. **Lock template type** (this is the prerequisite for all subsequent actions):
- **Step 0.1**: Check the project root directory to understand basic project information
- **Step 0.2**: Read the `package.json` file and inspect framework dependencies in `dependencies` and `devDependencies`
- **Step 0.3**: Check configured plugins in `vite.config.ts` or `vite.config.js` (`@vitejs/plugin-vue` = `vue3-vite`, `@vitejs/plugin-react` = `react-vite`)
- **Step 0.4**: Confirm and lock the project template type (must be either `react-vite` or `vue3-vite`)
- **Step 0.5**: Treat the identified template type as immutable and proceed to subsequent phases
- ⚠️ **If template type cannot be determined, you must confirm with the user first and never assume**
**Phase 1: Project status detection**
1. **Critical first step**: check project directory status
2. **If it is an existing project** (most important):
- **Step 1**: Confirm template type (completed in Phase 0)
- **Step 2**: Check dependencies to confirm frontend framework (`react`, `vue`, etc.)
- **Step 3**: Check project file structure to confirm framework type (`vue3-vite` = `.vue` files, `react-vite` = `.tsx/.jsx` files)
- **Step 4**: Explicitly confirm the project's framework and tech stack must match the template type
- **Step 5**: In all subsequent actions, only use framework syntax and APIs corresponding to that template
**Phase 2: Framework identification and confirmation**
3. **Framework identification markers**:
- Vue project: `vue` dependency in package.json, `.vue` files exist
- React project: `react` dependency in package.json, `.tsx/.jsx` files exist
- Angular project: `@angular/core` dependency in package.json, `.component.ts` files exist
- Svelte project: `svelte` dependency in package.json, `.svelte` files exist
4. **Behavior after framework confirmation**:
- Vue project: use Vue APIs (Composition API or Options API), `.vue` files, Vue Router, Pinia, etc.
- React project: use React APIs (Hooks, class components, etc.), `.tsx/.jsx` files, React Router, Redux/Zustand, etc.
- Angular project: use Angular APIs, components/services/modules, RxJS, Angular Router, etc.
- Svelte project: use Svelte syntax, `.svelte` files, SvelteKit, etc.
- **Strictly forbidden**: arbitrarily switch to syntax from other frameworks in any project
**Phase 3: Development execution**
5. Analyze the user's development request in detail
6. Determine whether context7 documentation search is needed for the corresponding framework
7. Plan development approach based on the identified framework ecosystem
8. Prioritize best practices and modern development patterns of that framework
9. Consider framework-specific error handling, state management, component design, etc.
10. Follow project coding standards and file structure conventions
11. **Routing configuration requirement** (important):
- If routing is involved, hash mode must be used
- React project: use `HashRouter`
- Vue project: use `createWebHashHistory()`
- Angular project: use `HashLocationStrategy`
- Never use history mode (`BrowserRouter`, `createWebHistory`, etc.)
12. **MCP tool calling standard**:
- Use context7 to search framework docs and best practices
**Absolute rule (core of the core)**:
⚠️ **Template consistency principle** (highest priority):
- Determine project template type first (`react-vite` or `vue3-vite`) -> use only syntax and APIs corresponding to that template -> never convert template type
- Keep Vue 3 for `vue3-vite` and keep React for `react-vite`
- **Converting `vue3-vite` to `react-vite` (or vice versa) is the most severe error and strictly forbidden**
**Checklist**:
✓ Has the project template type (`react-vite` or `vue3-vite`) been determined?
✓ Has `package.json` been read?
✓ Has it been confirmed that framework dependencies in package.json match the template type?
✓ Has the project framework been identified?
✓ Has it been confirmed that the correct framework syntax is used?
✓ Has template/framework conversion been avoided?
✓ If routing is involved, is hash mode used?
</SYSTEM_INSTRUCTIONS>
""")
.build();
/**
* 根据当前请求上下文中的用户语言,返回对应语言的提示词文本。
*/
public static String resolvePromptText(Prompt prompt) {
RequestContext<?> ctx = RequestContext.get();
String lang = ctx != null ? ctx.getLang() : null;
return resolvePromptText(prompt, lang);
}
/**
* 根据 BCP 47 语言标识返回对应语言的提示词文本。
*/
public static String resolvePromptText(Prompt prompt, String lang) {
if (prompt == null) {
return "";
}
return switch (resolveLangVariant(lang)) {
case "zh-TW" -> firstNonBlank(prompt.getZhTW(), prompt.getZhCN(), prompt.getEnUS());
case "en-US" -> firstNonBlank(prompt.getEnUS(), prompt.getZhCN(), prompt.getZhTW());
default -> firstNonBlank(prompt.getZhCN(), prompt.getEnUS(), prompt.getZhTW());
};
}
private static String resolveLangVariant(String lang) {
if (lang == null || lang.isBlank()) {
return "en-US";
}
String normalized = lang.trim().replace('_', '-');
String lower = normalized.toLowerCase(Locale.ROOT);
if ("zh-tw".equals(lower) || "zh-hk".equals(lower) || lower.startsWith("zh-hant")) {
return "zh-TW";
}
if (lower.startsWith("zh")) {
return "zh-CN";
}
return "en-US";
}
private static String firstNonBlank(String... candidates) {
for (String candidate : candidates) {
if (candidate != null && !candidate.isBlank()) {
return candidate;
}
}
return "";
}
@Data
@Builder
public static class Prompt {
private String zhCN;
private String zhTW;
private String enUS;
}
}

View File

@@ -0,0 +1,30 @@
package com.xspaceagi.custompage.domain.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class PageFileInfo {
@Schema(description = "File name")
private String name;
@Schema(description = "Whether file is binary")
private boolean binary;
@Schema(description = "Whether file size exceeds limit")
private boolean sizeExceeded;
@Schema(description = "File text content")
private String contents;
@Schema(description = "Previous name before rename")
private String renameFrom;
//create | delete | rename | modify
@Schema(description = "Operation type")
private String operation;
@Schema(description = "Whether entry is a directory")
private Boolean isDir = false;
}

View File

@@ -0,0 +1,404 @@
package com.xspaceagi.custompage.domain.gateway;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import com.xspaceagi.custompage.domain.model.CustomPageConfigModel;
import com.xspaceagi.custompage.domain.repository.ICustomPageConfigRepository;
import com.xspaceagi.sandbox.sdk.server.ISandboxConfigRpcService;
import com.xspaceagi.sandbox.sdk.service.dto.SandboxConfigRpcDto;
import com.xspaceagi.sandbox.sdk.service.dto.SandboxConfigValue;
import com.xspaceagi.system.spec.common.UserContext;
import jakarta.annotation.Resource;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.util.UriComponentsBuilder;
@Slf4j
@Component
public class AiAgentClient {
@Value("${custom-page.ai-agent.base-url:}")
private String configuredBaseUrl;
@Resource
private ICustomPageConfigRepository customPageConfigRepository;
@Resource
private ISandboxConfigRpcService sandboxConfigRpcService;
@Resource
@Qualifier("aiAgentProgressExecutor")
private Executor aiAgentProgressExecutor;
@Data
@AllArgsConstructor
public static class AgentRuntimeContext {
private final String baseUrl;
private final Long tenantId;
private final Long spaceId;
private final String isolationType;
private final String podId;
}
private RestTemplate createRestTemplate() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(5000); // 连接超时 5秒
factory.setReadTimeout(60000); // 读取超时 60秒
return new RestTemplate(factory);
}
public Map<String, Object> sendChat(Map<String, Object> chatBody, Long projectId, UserContext userContext) {
AgentRuntimeContext runtimeContext = buildAgentRuntimeContext(projectId, userContext);
String url = buildBaseUrl(runtimeContext) + "/chat";
Map<String, Object> requestBody = appendRuntimeParamsToBody(chatBody, runtimeContext);
log.info("[Infra] call AI Agent /chat, url={}, request={}", url, requestBody);
RestTemplate restTemplate = createRestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(requestBody, headers);
ResponseEntity<Map<String, Object>> entity = restTemplate.exchange(
url,
HttpMethod.POST,
requestEntity,
new ParameterizedTypeReference<Map<String, Object>>() {
});
Map<String, Object> body = entity.getBody();
log.info("[Infra] call AI Agent /chat, response={}", body);
return body;
}
public void subscribeSessionSse(String sessionId, SseEmitter emitter, Long projectId, UserContext userContext,
BiConsumer<String, String> eventConsumer) {
subscribeSessionSse(sessionId, emitter, projectId, userContext, eventConsumer, null, null);
}
public void subscribeSessionSse(String sessionId, SseEmitter emitter, Long projectId, UserContext userContext,
BiConsumer<String, String> eventConsumer, Runnable onStreamFinished) {
subscribeSessionSse(sessionId, emitter, projectId, userContext, eventConsumer, onStreamFinished, null);
}
public void subscribeSessionSse(String sessionId, SseEmitter emitter, Long projectId, UserContext userContext,
BiConsumer<String, String> eventConsumer, Runnable onStreamFinished, Consumer<String> onSubscribeError) {
AgentRuntimeContext runtimeContext = buildAgentRuntimeContext(projectId, userContext);
AtomicBoolean clientDisconnected = new AtomicBoolean(false);
aiAgentProgressExecutor.execute(() -> {
HttpURLConnection connection = null;
try {
String urlStr = appendRuntimeParams(UriComponentsBuilder.fromHttpUrl(buildBaseUrl(runtimeContext) + "/agent/progress/" + sessionId), runtimeContext).toUriString();
log.info("[Infra] start subscribing to AI Agent SSE, url={}", urlStr);
URL url = new URL(urlStr);
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setRequestProperty("Accept", "text/event-stream");
connection.setRequestProperty("Cache-Control", "no-cache");
connection.setRequestProperty("Connection", "keep-alive");
connection.setDoInput(true);
connection.setConnectTimeout(30000); // 连接超时时间30秒
connection.setReadTimeout(0); // SSE 长连接
int status = connection.getResponseCode();
if (status != 200) {
String errorMessage = "SSE subscribe failed, httpStatus=" + status;
notifySubscribeError(onSubscribeError, errorMessage);
if (emitter != null) {
try {
emitter.completeWithError(new IllegalStateException(errorMessage));
} catch (Exception ignore) {
}
}
return;
}
try (InputStream inputStream = connection.getInputStream();
BufferedReader reader = new BufferedReader(
new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
String line;
String currentEvent = null;
StringBuilder dataBuilder = new StringBuilder();
while ((line = reader.readLine()) != null) {
if (line.startsWith("event:")) {
currentEvent = line.substring(6).trim();
} else if (line.startsWith("data:")) {
if (dataBuilder.length() > 0) {
dataBuilder.append('\n');
}
dataBuilder.append(line.substring(5).trim());
} else if (line.isEmpty()) {
if (dataBuilder.length() > 0) {
try {
// 使用SSE协议中的event字段作为事件名称如果没有则使用默认名称
String eventName = currentEvent;
if (eventName == null) {
eventName = "message"; // 默认事件名称
}
SseEmitter.SseEventBuilder eventBuilder = SseEmitter.event();
if (eventName != null) {
eventBuilder.name(eventName);
}
eventBuilder.data(dataBuilder.toString());
if (eventConsumer != null) {
eventConsumer.accept(eventName, dataBuilder.toString());
}
if (emitter != null && !clientDisconnected.get()) {
try {
emitter.send(eventBuilder);
} catch (Exception sendEx) {
if (isClientDisconnected(sendEx)) {
clientDisconnected.set(true);
log.warn("[Infra] SSE send to frontend failed, client disconnected, continue consuming agent stream, session Id={}",
sessionId);
} else {
log.warn("[Infra] SSE send to frontend failed, session Id={}", sessionId, sendEx);
}
}
}
} catch (Exception sendEx) {
log.warn("[Infra] SSE event handling failed, session Id={}", sessionId, sendEx);
}
}
currentEvent = null;
dataBuilder.setLength(0);
}
}
}
if (emitter != null) {
try {
emitter.complete();
} catch (Exception ignore) {
}
}
} catch (Exception e) {
log.error("[Infra] AI Agent SSE exception", e);
notifySubscribeError(onSubscribeError, e.getMessage());
if (emitter != null) {
try {
emitter.completeWithError(e);
} catch (Exception ignore) {
}
}
} finally {
if (connection != null) {
connection.disconnect();
}
if (onStreamFinished != null) {
try {
onStreamFinished.run();
} catch (Exception callbackEx) {
log.warn("[Infra] AI Agent SSE onStreamFinished callback failed, session Id={}", sessionId,
callbackEx);
}
}
}
});
}
private void notifySubscribeError(Consumer<String> onSubscribeError, String message) {
if (onSubscribeError == null || message == null) {
return;
}
try {
onSubscribeError.accept(message);
} catch (Exception e) {
log.warn("[Infra] onSubscribeError callback failed", e);
}
}
private boolean isClientDisconnected(Exception e) {
String msg = e.getMessage();
if (msg == null) {
return false;
}
String lowerMsg = msg.toLowerCase();
return lowerMsg.contains("broken pipe") || lowerMsg.contains("connection reset");
}
public Map<String, Object> sessionCancel(Long projectId, String sessionId, UserContext userContext) {
AgentRuntimeContext runtimeContext = buildAgentRuntimeContext(projectId, userContext);
UriComponentsBuilder urlBuilder = UriComponentsBuilder.fromHttpUrl(buildBaseUrl(runtimeContext) + "/agent/session/cancel").queryParam("project_id", projectId);
if (sessionId != null && !sessionId.isBlank()) {
urlBuilder = urlBuilder.queryParam("session_id", sessionId);
}
String url = appendRuntimeParams(urlBuilder, runtimeContext).toUriString();
log.info("[Infra] callcancel agent task API, url={}, project Id={}, session Id={}", url, projectId, sessionId);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("project_id", String.valueOf(projectId));
if (sessionId != null && !sessionId.isBlank()) {
requestBody.put("session_id", sessionId);
}
Map<String, Object> finalRequestBody = appendRuntimeParamsToBody(requestBody, runtimeContext);
log.info("[Infra] callcancel agent task API, request={}", finalRequestBody);
HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(
finalRequestBody, headers);
RestTemplate restTemplate = createRestTemplate();
ResponseEntity<Map<String, Object>> entity = restTemplate.exchange(
url,
HttpMethod.POST,
requestEntity,
new ParameterizedTypeReference<Map<String, Object>>() {
});
Map<String, Object> body = entity.getBody();
log.info("[Infra] callcancel agent task API, response={}", body);
return body;
}
public Map<String, Object> getAgentStatus(Long projectId, UserContext userContext) {
AgentRuntimeContext runtimeContext = buildAgentRuntimeContext(projectId, userContext);
String url = appendRuntimeParams(UriComponentsBuilder.fromHttpUrl(buildBaseUrl(runtimeContext) + "/agent/status/" + projectId), runtimeContext).toUriString();
log.info("[Infra] callquery Agent status API, url={}, project Id={}", url, projectId);
RestTemplate restTemplate = createRestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Void> requestEntity = new HttpEntity<>(headers);
ResponseEntity<Map<String, Object>> entity = restTemplate.exchange(
url,
HttpMethod.GET,
requestEntity,
new ParameterizedTypeReference<Map<String, Object>>() {
});
Map<String, Object> body = entity.getBody();
log.info("[Infra] callquery Agent status API, response={}", body);
return body;
}
public Map<String, Object> stopAgent(Long projectId, UserContext userContext) {
AgentRuntimeContext runtimeContext = buildAgentRuntimeContext(projectId, userContext);
String url = appendRuntimeParams(
UriComponentsBuilder.fromHttpUrl(buildBaseUrl(runtimeContext) + "/agent/stop").queryParam("project_id", projectId), runtimeContext).toUriString();
log.info("[Infra] callstop Agent service API, url={}, project Id={}", url, projectId);
RestTemplate restTemplate = createRestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("project_id", String.valueOf(projectId));
Map<String, Object> finalRequestBody = appendRuntimeParamsToBody(requestBody, runtimeContext);
log.info("[Infra] callstop Agent service API, request={}", finalRequestBody);
HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(
finalRequestBody, headers);
ResponseEntity<Map<String, Object>> entity = restTemplate.exchange(
url,
HttpMethod.POST,
requestEntity,
new ParameterizedTypeReference<Map<String, Object>>() {
});
Map<String, Object> body = entity.getBody();
log.info("[Infra] callstop Agent service API, response={}", body);
return body;
}
private AgentRuntimeContext buildAgentRuntimeContext(Long projectId, UserContext userContext) {
CustomPageConfigModel configModel = customPageConfigRepository.getById(projectId);
if (configModel == null) {
throw new IllegalArgumentException("Project configuration does not exist");
}
Long sandboxId = configModel.getSandboxId();
if (sandboxId == null) {
// 兼容老项目:未绑定 sandboxId 时,走配置文件中的 AI Agent 地址,且不透传 runtime 参数
return null;
}
SandboxConfigRpcDto sandboxConfig = sandboxConfigRpcService.selectAppDevelopmentSandbox(
userContext.getTenantId(),
userContext.getUserId(),
configModel.getSpaceId(),
projectId,
sandboxId);
if (sandboxConfig == null) {
throw new IllegalStateException("No available sandbox, sandboxId:" + sandboxId);
}
SandboxConfigValue configValue = sandboxConfig.getConfigValue();
if (configValue == null || configValue.getHostWithScheme() == null || configValue.getHostWithScheme().isBlank()
|| configValue.getAgentPort() <= 0) {
throw new IllegalStateException("Sandbox config is incomplete, sandboxId:" + sandboxId);
}
String baseUrl = configValue.getHostWithScheme() + ":" + configValue.getAgentPort();
String isolationType = sandboxConfig.getIsolation() == null ? null : sandboxConfig.getIsolation().name();
String podId = sandboxConfig.getIsolationKey();
return new AgentRuntimeContext(baseUrl, userContext.getTenantId(), configModel.getSpaceId(), isolationType, podId);
}
private UriComponentsBuilder appendRuntimeParams(UriComponentsBuilder builder, AgentRuntimeContext runtimeContext) {
if (runtimeContext == null) {
return builder;
}
if (runtimeContext.getTenantId() != null) {
builder.queryParam("tenant_id", runtimeContext.getTenantId());
}
if (runtimeContext.getSpaceId() != null) {
builder.queryParam("space_id", runtimeContext.getSpaceId());
}
if (runtimeContext.getIsolationType() != null && !runtimeContext.getIsolationType().isBlank()) {
builder.queryParam("isolation_type", runtimeContext.getIsolationType());
}
if (runtimeContext.getPodId() != null && !runtimeContext.getPodId().isBlank()) {
builder.queryParam("pod_id", runtimeContext.getPodId());
}
return builder;
}
private Map<String, Object> appendRuntimeParamsToBody(Map<String, Object> body, AgentRuntimeContext runtimeContext) {
Map<String, Object> requestBody = body == null ? new HashMap<>() : new HashMap<>(body);
if (runtimeContext == null) {
return requestBody;
}
if (runtimeContext.getTenantId() != null) {
requestBody.put("tenant_id", runtimeContext.getTenantId());
}
if (runtimeContext.getSpaceId() != null) {
requestBody.put("space_id", runtimeContext.getSpaceId());
}
if (runtimeContext.getIsolationType() != null && !runtimeContext.getIsolationType().isBlank()) {
requestBody.put("isolation_type", runtimeContext.getIsolationType());
}
if (runtimeContext.getPodId() != null && !runtimeContext.getPodId().isBlank()) {
requestBody.put("pod_id", runtimeContext.getPodId());
}
return requestBody;
}
private String buildBaseUrl(AgentRuntimeContext runtimeContext) {
String baseUrl = runtimeContext == null ? null : runtimeContext.getBaseUrl();
if (baseUrl == null || baseUrl.isBlank()) {
baseUrl = configuredBaseUrl;
}
if (baseUrl == null || baseUrl.isBlank()) {
throw new IllegalArgumentException("AI Agent baseUrl is required");
}
String url = baseUrl.trim();
while (url.endsWith("/")) {
url = url.substring(0, url.length() - 1);
}
return url;
}
}

View File

@@ -0,0 +1,805 @@
package com.xspaceagi.custompage.domain.gateway;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.xspaceagi.custompage.domain.dto.PageFileInfo;
import com.xspaceagi.custompage.domain.model.CustomPageConfigModel;
import com.xspaceagi.custompage.domain.repository.ICustomPageConfigRepository;
import com.xspaceagi.custompage.sdk.dto.ProjectConfigExportDto;
import com.xspaceagi.custompage.sdk.dto.TemplateTypeEnum;
import com.xspaceagi.sandbox.sdk.server.ISandboxConfigRpcService;
import com.xspaceagi.sandbox.sdk.service.dto.SandboxConfigRpcDto;
import com.xspaceagi.sandbox.sdk.service.dto.SandboxConfigValue;
import com.xspaceagi.sandbox.sdk.service.dto.SandboxServerInfo;
import com.xspaceagi.system.spec.common.RequestContext;
import jakarta.annotation.Resource;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Flux;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
@Component
public class PageFileBuildClient {
@Value("${custom-page.build-server.base-url:}")
private String configuredBaseUrl;
@Resource
private ICustomPageConfigRepository customPageConfigRepository;
@Resource
private ISandboxConfigRpcService sandboxConfigRpcService;
private final WebClient webClient = WebClient.builder().build();
public Map<String, Object> startDev(Long projectId, String devProxyPath) {
String url = appendRuntimeParamsToQuery(
UriComponentsBuilder.fromHttpUrl(buildBaseUrl(projectId) + "/build/start-dev")
.queryParam("projectId", projectId)
.queryParam("basePath", devProxyPath),
projectId).toUriString();
log.info("[Build-server] project Id={} call dev start API, url={}", projectId, url);
RestTemplate restTemplate = new RestTemplate();
try {
ResponseEntity<Map<String, Object>> entity = restTemplate.exchange(url, HttpMethod.GET, null, new ParameterizedTypeReference<Map<String, Object>>() {
});
Map<String, Object> body = entity.getBody();
log.info("[Build-server] project Id={} call dev start API, response={}", projectId, body);
return body;
} catch (HttpClientErrorException e) {
log.warn("[Build-server] project Id={} call dev start APIfailed, status={}, response Body={}", projectId, e.getStatusCode(), e.getResponseBodyAsString());
return parseClientErr(projectId, e);
}
}
public Map<String, Object> keepAlive(Long projectId, String devProxyPath, Integer devPid, Integer devPort) {
String url = appendRuntimeParamsToQuery(
UriComponentsBuilder.fromHttpUrl(buildBaseUrl(projectId) + "/build/keep-alive")
.queryParam("projectId", projectId)
.queryParam("basePath", devProxyPath)
.queryParam("pid", devPid)
.queryParam("port", devPort),
projectId).toUriString();
log.info("[Build-server] project Id={} callkeep-alive API, url={}", projectId, url);
RestTemplate restTemplate = new RestTemplate();
try {
ResponseEntity<Map<String, Object>> entity = restTemplate.exchange(url, HttpMethod.GET, null, new ParameterizedTypeReference<Map<String, Object>>() {
});
Map<String, Object> body = entity.getBody();
log.info("[Build-server] project Id={} callkeep-alive API, response={}", projectId, body);
return body;
} catch (HttpClientErrorException e) {
log.warn("[Build-server] project Id={} callkeep-alive APIfailed, status={}, response Body={}", projectId, e.getStatusCode(), e.getResponseBodyAsString());
return parseClientErr(projectId, e);
}
}
public Map<String, Object> build(Long projectId, String prodProxyPath) {
String url = appendRuntimeParamsToQuery(
UriComponentsBuilder.fromHttpUrl(buildBaseUrl(projectId) + "/build/build")
.queryParam("projectId", projectId)
.queryParam("basePath", prodProxyPath),
projectId).toUriString();
log.info("[Build-server] project Id={} callbuild API, url={}", projectId, url);
RestTemplate restTemplate = new RestTemplate();
try {
ResponseEntity<Map<String, Object>> entity = restTemplate.exchange(url, HttpMethod.GET, null, new ParameterizedTypeReference<Map<String, Object>>() {
});
Map<String, Object> body = entity.getBody();
log.info("[Build-server] project Id={} callbuild API, response={}", projectId, body);
return body;
} catch (HttpClientErrorException e) {
log.warn("[Build-server] project Id={} callbuild APIfailed, status={}, response Body={}", projectId, e.getStatusCode(), e.getResponseBodyAsString());
return parseClientErr(projectId, e);
}
}
public Map<String, Object> stopDev(Long projectId, Integer pid) {
String url = appendRuntimeParamsToQuery(
UriComponentsBuilder.fromHttpUrl(buildBaseUrl(projectId) + "/build/stop-dev")
.queryParam("projectId", projectId)
.queryParam("pid", pid),
projectId).toUriString();
log.info("[Build-server] project Id={} callstop dev server API, url={}", projectId, url);
RestTemplate restTemplate = new RestTemplate();
try {
ResponseEntity<Map<String, Object>> entity = restTemplate.exchange(url, HttpMethod.GET, null, new ParameterizedTypeReference<Map<String, Object>>() {
});
Map<String, Object> body = entity.getBody();
log.info("[Build-server] project Id={} callstop dev server API, response={}", projectId, body);
return body;
} catch (HttpClientErrorException e) {
log.warn("[Build-server] project Id={} callstop dev server APIfailed, status={}, response Body={}", projectId, e.getStatusCode(), e.getResponseBodyAsString());
return parseClientErr(projectId, e);
}
}
public Map<String, Object> restartDev(Long projectId, Integer pid, String devProxyPath) {
UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(buildBaseUrl(projectId) + "/build/restart-dev").queryParam("projectId", projectId).queryParam("basePath", devProxyPath);
if (pid != null) {
uriComponentsBuilder.queryParam("pid", pid);
}
String url = appendRuntimeParamsToQuery(uriComponentsBuilder, projectId).toUriString();
log.info("[Build-server] project Id={} callrestart dev server API, url={}", projectId, url);
RestTemplate restTemplate = new RestTemplate();
try {
ResponseEntity<Map<String, Object>> entity = restTemplate.exchange(url, HttpMethod.GET, null, new ParameterizedTypeReference<Map<String, Object>>() {
});
Map<String, Object> body = entity.getBody();
log.info("[Build-server] project Id={} callrestart dev server API, response={}", projectId, body);
return body;
} catch (HttpClientErrorException e) {
log.warn("[Build-server] project Id={} callrestart dev server APIfailed, status={}, response Body={}", projectId, e.getStatusCode(), e.getResponseBodyAsString());
return parseClientErr(projectId, e);
}
}
public Map<String, Object> createProject(Long projectId, TemplateTypeEnum templateType) {
String url = buildBaseUrl(projectId) + "/project/create-project";
log.info("[Build-server] project Id={} callcreate-project API, url={}", projectId, url);
// 创建请求体
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("projectId", String.valueOf(projectId));
requestBody.put("templateType", TemplateTypeEnum.defaultType(templateType).getValue());
// 设置请求头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(
appendRuntimeParamsToBody(requestBody, projectId), headers);
RestTemplate restTemplate = new RestTemplate();
try {
ResponseEntity<Map<String, Object>> entity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, new ParameterizedTypeReference<Map<String, Object>>() {
});
Map<String, Object> body = entity.getBody();
log.info("[Build-server] project Id={} callcreate-project API, response={}", projectId, body);
return body;
} catch (HttpClientErrorException e) {
log.warn("[Build-server] project Id={} callcreate-project APIfailed, status={}, response Body={}", projectId, e.getStatusCode(), e.getResponseBodyAsString());
return parseClientErr(projectId, e);
}
}
public Map<String, Object> uploadProject(Long projectId, MultipartFile file, Integer codeVersion, Integer pid, String devProxyPath) {
String url = buildBaseUrl(projectId) + "/project/upload-project";
log.info("[Build-server] project Id={} callupload project API, url={}, code Version={}", projectId, url, codeVersion);
// 创建请求体包含文件、projectId和版本号
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
// 创建MultiValueMap来存储文件、projectId和版本号
LinkedMultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("file", file.getResource());
body.add("projectId", String.valueOf(projectId));
body.add("codeVersion", String.valueOf(codeVersion));
body.add("pid", pid);
if (devProxyPath != null) {
body.add("basePath", devProxyPath);
}
appendRuntimeParamsToBody(body, projectId);
HttpEntity<LinkedMultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
RestTemplate restTemplate = new RestTemplate();
try {
ResponseEntity<Map<String, Object>> entity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, new ParameterizedTypeReference<Map<String, Object>>() {
});
Map<String, Object> responseBody = entity.getBody();
log.info("[Build-server] project Id={} callupload project API, response={}", projectId, responseBody);
return responseBody;
} catch (HttpClientErrorException e) {
log.warn("[Build-server] project Id={} callupload project APIfailed, status={}, response Body={}", projectId, e.getStatusCode(), e.getResponseBodyAsString());
return parseClientErr(projectId, e);
}
}
public Map<String, Object> getProjectContent(Long projectId, String command, String proxyPath) {
String url = appendRuntimeParamsToQuery(
UriComponentsBuilder.fromHttpUrl(buildBaseUrl(projectId) + "/project/get-project-content")
.queryParam("projectId", projectId)
.queryParam("command", command)
.queryParam("proxyPath", proxyPath),
projectId).toUriString();
log.info("[Build-server] project Id={} callqueryprojectcontent API, url={}", projectId, url);
RestTemplate restTemplate = new RestTemplate();
try {
ResponseEntity<Map<String, Object>> entity = restTemplate.exchange(url, HttpMethod.GET, null, new ParameterizedTypeReference<Map<String, Object>>() {
});
Map<String, Object> body = entity.getBody();
log.info("[Build-server] project Id={} callqueryprojectcontent API, response sent", projectId);
return body;
} catch (HttpClientErrorException e) {
log.warn("[Build-server] project Id={} callqueryprojectcontent APIfailed, status={}, response Body={}", projectId, e.getStatusCode(), e.getResponseBodyAsString());
return parseClientErr(projectId, e);
}
}
public Map<String, Object> getProjectContentByVersion(Long projectId, Integer codeVersion, String command, String proxyPath) {
String url = appendRuntimeParamsToQuery(
UriComponentsBuilder.fromHttpUrl(buildBaseUrl(projectId) + "/project/get-project-content-by-version")
.queryParam("projectId", projectId)
.queryParam("codeVersion", codeVersion)
.queryParam("command", command)
.queryParam("proxyPath", proxyPath),
projectId).toUriString();
log.info("[Build-server] project Id={} callqueryprojecthistorical version content API, url={}", projectId, url);
RestTemplate restTemplate = new RestTemplate();
try {
ResponseEntity<Map<String, Object>> entity = restTemplate.exchange(url, HttpMethod.GET, null, new ParameterizedTypeReference<Map<String, Object>>() {
});
Map<String, Object> body = entity.getBody();
log.info("[Build-server] project Id={} callqueryprojecthistorical version content API, response sent", projectId);
return body;
} catch (HttpClientErrorException e) {
log.warn("[Build-server] project Id={} callqueryprojecthistorical version content APIfailed, status={}, response Body={}", projectId, e.getStatusCode(), e.getResponseBodyAsString());
return parseClientErr(projectId, e);
}
}
/**
* codeVersion 是当前版本,上传后版本会+1
*/
public Map<String, Object> specifiedFilesUpdate(Long projectId, List<PageFileInfo> files, Integer codeVersion, String devProxyPath, Integer devPid) {
String url = buildBaseUrl(projectId) + "/project/specified-files-update";
log.info("[Build-server] project Id={} specifiedfileupdate, url={}", projectId, url);
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("projectId", String.valueOf(projectId));
requestBody.put("files", files);
requestBody.put("codeVersion", codeVersion);
requestBody.put("basePath", devProxyPath);
requestBody.put("pid", devPid);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(
appendRuntimeParamsToBody(requestBody, projectId), headers);
RestTemplate restTemplate = new RestTemplate();
try {
ResponseEntity<Map<String, Object>> entity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, new ParameterizedTypeReference<Map<String, Object>>() {
});
Map<String, Object> body = entity.getBody();
log.info("[Build-server] project Id={} specifiedfileupdate, response={}", projectId, body);
return body;
} catch (HttpClientErrorException e) {
log.warn("[Build-server] project Id={} callspecifiedfileupdate APIfailed, status={}, response Body={}", projectId, e.getStatusCode(), e.getResponseBodyAsString());
return parseClientErr(projectId, e);
}
}
/**
* codeVersion 是当前版本,上传后版本会+1
*/
public Map<String, Object> allFilesUpdate(Long projectId, List<PageFileInfo> files, Integer codeVersion, String devProxyPath, Integer devPid) {
String url = buildBaseUrl(projectId) + "/project/all-files-update";
log.info("[Build-server] project Id={} fullfileupdate, url={}", projectId, url);
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("projectId", String.valueOf(projectId));
requestBody.put("files", files);
requestBody.put("codeVersion", codeVersion);
requestBody.put("basePath", devProxyPath);
requestBody.put("pid", devPid);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(
appendRuntimeParamsToBody(requestBody, projectId), headers);
RestTemplate restTemplate = new RestTemplate();
try {
ResponseEntity<Map<String, Object>> entity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, new ParameterizedTypeReference<Map<String, Object>>() {
});
Map<String, Object> body = entity.getBody();
log.info("[Build-server] project Id={} fullfileupdate, response={}", projectId, body);
return body;
} catch (HttpClientErrorException e) {
log.warn("[Build-server] project Id={} callfullfileupdate APIfailed, status={}, response Body={}", projectId, e.getStatusCode(), e.getResponseBodyAsString());
return parseClientErr(projectId, e);
}
}
/**
* codeVersion 是当前版本,上传后版本会+1
*/
public Map<String, Object> uploadSingleFile(Long projectId, MultipartFile file, String filePath, Integer codeVersion) {
String url = buildBaseUrl(projectId) + "/project/upload-single-file";
log.info("[Build-server] project Id={} upload single file, url={}, file Path={}, code Version={}", projectId, url, filePath, codeVersion);
// 创建请求体包含文件、projectId、filePath和codeVersion
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
// 创建MultiValueMap来存储文件和其他参数
LinkedMultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("file", file.getResource());
body.add("projectId", String.valueOf(projectId));
body.add("filePath", filePath);
body.add("codeVersion", String.valueOf(codeVersion));
appendRuntimeParamsToBody(body, projectId);
HttpEntity<LinkedMultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
RestTemplate restTemplate = new RestTemplate();
try {
ResponseEntity<Map<String, Object>> entity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, new ParameterizedTypeReference<Map<String, Object>>() {
});
Map<String, Object> responseBody = entity.getBody();
log.info("[Build-server] project Id={} upload single file, response={}", projectId, responseBody);
return responseBody;
} catch (HttpClientErrorException e) {
log.warn("[Build-server] project Id={} callupload single file APIfailed, status={}, response Body={}", projectId, e.getStatusCode(), e.getResponseBodyAsString());
return parseClientErr(projectId, e);
}
}
public Map<String, Object> uploadAttachmentFile(Long projectId, MultipartFile file, String uploadFileName) {
String url = buildBaseUrl(projectId) + "/project/upload-attachment-file";
log.info("[Build-server] project Id={} upload attachment, url={}, upload File Name={}", projectId, url, uploadFileName);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
LinkedMultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("file", file.getResource());
body.add("projectId", String.valueOf(projectId));
body.add("fileName", uploadFileName);
appendRuntimeParamsToBody(body, projectId);
HttpEntity<LinkedMultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
RestTemplate restTemplate = new RestTemplate();
try {
ResponseEntity<Map<String, Object>> entity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, new ParameterizedTypeReference<Map<String, Object>>() {
});
Map<String, Object> responseBody = entity.getBody();
log.info("[Build-server] project Id={} upload attachment, response={}", projectId, responseBody);
return responseBody;
} catch (HttpClientErrorException e) {
log.warn("[Build-server] project Id={} callupload attachment APIfailed, status={}, response Body={}", projectId, e.getStatusCode(), e.getResponseBodyAsString());
return parseClientErr(projectId, e);
}
}
public Map<String, Object> pushSkillsToWorkspace(Long projectId, MultipartFile zipFile, List<String> skillUrls) {
String url = buildBaseUrl(projectId) + "/project/push-skills-to-workspace";
log.info("[Build-server] project Id={} push skills to workspace, url={}", projectId, url);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
LinkedMultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("projectId", String.valueOf(projectId));
if (zipFile != null) {
body.add("file", zipFile.getResource());
}
if (skillUrls != null && !skillUrls.isEmpty()) {
body.add("skillUrls", skillUrls);
}
appendRuntimeParamsToBody(body, projectId);
HttpEntity<LinkedMultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
RestTemplate restTemplate = new RestTemplate();
try {
ResponseEntity<Map<String, Object>> entity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, new ParameterizedTypeReference<Map<String, Object>>() {
});
Map<String, Object> responseBody = entity.getBody();
log.info("[Build-server] project Id={} push skills to workspace, response={}", projectId, responseBody);
return responseBody;
} catch (HttpClientErrorException e) {
log.warn("[Build-server] project Id={} push skills to workspace failed, status={}, response Body={}", projectId, e.getStatusCode(), e.getResponseBodyAsString());
return parseClientErr(projectId, e);
}
}
public Map<String, Object> backupCurrentVersion(Long projectId, Integer codeVersion) {
String url = buildBaseUrl(projectId) + "/project/backup-current-version";
log.info("[Build-server] project Id={} backup current version, url={}, code Version={}", projectId, url, codeVersion);
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("projectId", String.valueOf(projectId));
requestBody.put("codeVersion", codeVersion);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(
appendRuntimeParamsToBody(requestBody, projectId), headers);
RestTemplate restTemplate = new RestTemplate();
try {
ResponseEntity<Map<String, Object>> entity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, new ParameterizedTypeReference<Map<String, Object>>() {
});
Map<String, Object> body = entity.getBody();
log.info("[Build-server] project Id={} backup current version, response={}", projectId, body);
return body;
} catch (HttpClientErrorException e) {
log.warn("[Build-server] project Id={} callbackup current version APIfailed, status={}, response Body={}", projectId, e.getStatusCode(), e.getResponseBodyAsString());
return parseClientErr(projectId, e);
}
}
/**
* codeVersion 是当前版本,回滚后版本会+1
*/
public Map<String, Object> rollbackVersion(Long projectId, Integer rollbackTo, Integer codeVersion, String devProxyPath, Integer devPid) {
String url = buildBaseUrl(projectId) + "/project/rollback-version";
log.info("[Build-server] project Id={} rollback version, url={}, rollback To={}, code Version={}", projectId, url, rollbackTo, codeVersion);
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("projectId", String.valueOf(projectId));
requestBody.put("rollbackTo", rollbackTo);
requestBody.put("codeVersion", codeVersion);
requestBody.put("basePath", devProxyPath);
requestBody.put("pid", devPid);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(
appendRuntimeParamsToBody(requestBody, projectId), headers);
RestTemplate restTemplate = new RestTemplate();
try {
ResponseEntity<Map<String, Object>> entity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, new ParameterizedTypeReference<Map<String, Object>>() {
});
Map<String, Object> body = entity.getBody();
log.info("[Build-server] project Id={} rollback version, response={}", projectId, body);
return body;
} catch (HttpClientErrorException e) {
log.warn("[Build-server] project Id={} callrollback version APIfailed, status={}, response Body={}", projectId, e.getStatusCode(), e.getResponseBodyAsString());
return parseClientErr(projectId, e);
}
}
public InputStream exportProject(Long projectId, Integer codeVersion, String exportType, ProjectConfigExportDto configExportDto) {
String url = buildBaseUrl(projectId) + "/project/export-project";
log.info("[Build-server] project Id={} exportproject, url={}, code Version={}, export Type={}", projectId, url, codeVersion, exportType);
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("projectId", String.valueOf(projectId));
requestBody.put("codeVersion", codeVersion);
requestBody.put("exportType", exportType);
requestBody.put("config", configExportDto);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(
appendRuntimeParamsToBody(requestBody, projectId), headers);
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<byte[]> entity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, byte[].class);
byte[] body = entity.getBody();
log.info("[Build-server] project Id={} exportproject, response size={} bytes", projectId, body != null ? body.length : 0);
return body != null ? new ByteArrayInputStream(body) : null;
}
public Map<String, Object> deleteProject(Long projectId, Integer pid) {
UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(buildBaseUrl(projectId) + "/project/delete-project").queryParam("projectId", projectId).queryParam("pid", pid);
String url = appendRuntimeParamsToQuery(uriComponentsBuilder, projectId).toUriString();
log.info("[Build-server] project Id={} calldelete project API, url={}", projectId, url);
RestTemplate restTemplate = new RestTemplate();
try {
ResponseEntity<Map<String, Object>> entity = restTemplate.exchange(url, HttpMethod.GET, null, new ParameterizedTypeReference<Map<String, Object>>() {
});
Map<String, Object> body = entity.getBody();
log.info("[Build-server] project Id={} calldelete project API, response={}", projectId, body);
return body;
} catch (HttpClientErrorException e) {
log.warn("[Build-server] project Id={} calldelete project APIfailed, status={}, response Body={}", projectId, e.getStatusCode(), e.getResponseBodyAsString());
return parseClientErr(projectId, e);
}
}
public Map<String, Object> getDevLog(Long projectId, Integer startIndex, String logType) {
String url = appendRuntimeParamsToQuery(
UriComponentsBuilder.fromHttpUrl(buildBaseUrl(projectId) + "/build/get-dev-log")
.queryParam("projectId", projectId)
.queryParam("startIndex", startIndex)
.queryParam("logType", logType),
projectId).toUriString();
log.debug("[Build-server] project Id={} callquery logs API, url={}", projectId, url);
RestTemplate restTemplate = new RestTemplate();
try {
ResponseEntity<Map<String, Object>> entity = restTemplate.exchange(url, HttpMethod.GET, null, new ParameterizedTypeReference<Map<String, Object>>() {
});
Map<String, Object> body = entity.getBody();
log.debug("[Build-server] project Id={} callquery logs API, response sent", projectId);
return body;
} catch (HttpClientErrorException e) {
log.warn("[Build-server] project Id={} query logs API failed, status={}, response Body={}", projectId, e.getStatusCode(), e.getResponseBodyAsString());
return parseClientErr(projectId, e);
}
}
public Map<String, Object> copyProject(Long sourceProjectId, Long targetProjectId) {
String url = UriComponentsBuilder.fromHttpUrl(buildBaseUrl(targetProjectId) + "/project/copy-project")
//.queryParam("sourceProjectId", sourceProjectId)
//.queryParam("targetProjectId", targetProjectId)
.toUriString();
log.info("[Build-server] source Project Id={},target Project Id={}, callcopyproject API, url={}", sourceProjectId, targetProjectId, url);
RuntimeContext sourceRuntimeContext = getRuntimeContext(sourceProjectId);
RuntimeContext targetRuntimeContext = getRuntimeContext(targetProjectId);
CustomPageConfigModel sourceConfig = customPageConfigRepository.getById(sourceProjectId);
CustomPageConfigModel targetConfig = customPageConfigRepository.getById(targetProjectId);
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("sourceProjectId", String.valueOf(sourceProjectId));
requestBody.put("sourceTenantId", sourceConfig == null ? null : sourceConfig.getTenantId());
requestBody.put("sourceSpaceId", sourceConfig == null ? null : sourceConfig.getSpaceId());
requestBody.put("sourceIsolationType", sourceRuntimeContext == null ? null : sourceRuntimeContext.getIsolationType());
requestBody.put("targetProjectId", targetProjectId);
requestBody.put("targetTenantId", targetConfig == null ? null : targetConfig.getTenantId());
requestBody.put("targetSpaceId", targetConfig == null ? null : targetConfig.getSpaceId());
requestBody.put("targetIsolationType", targetRuntimeContext == null ? null : targetRuntimeContext.getIsolationType());
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(requestBody, headers);
RestTemplate restTemplate = new RestTemplate();
try {
ResponseEntity<Map<String, Object>> entity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, new ParameterizedTypeReference<Map<String, Object>>() {
});
Map<String, Object> body = entity.getBody();
log.info("[Build-server] source Project Id={},target Project Id={}, callcopyproject API, response={}", sourceProjectId, targetProjectId, body);
return body;
} catch (HttpClientErrorException e) {
log.warn("[Build-server] project Id={} query logs API failed, status={}, response Body={}", sourceProjectId, e.getStatusCode(), e.getResponseBodyAsString());
return parseClientErr(sourceProjectId, e);
}
}
public Map<String, Object> getLogCacheStats() {
String url = appendRuntimeParamsToQuery(
UriComponentsBuilder.fromHttpUrl(buildBaseUrl(null) + "/build/get-log-cache-stats"), null)
.toUriString();
log.info("[Build-server] callgetlogcachestats API, url={}", url);
RestTemplate restTemplate = new RestTemplate();
try {
ResponseEntity<Map<String, Object>> entity = restTemplate.exchange(url, HttpMethod.GET, null, new ParameterizedTypeReference<Map<String, Object>>() {
});
Map<String, Object> body = entity.getBody();
log.info("[Build-server] callgetlogcachestats API, response={}", body);
return body;
} catch (HttpClientErrorException e) {
log.warn("[Build-server] callgetlogcachestats APIfailed, status={}, response Body={}", e.getStatusCode(), e.getResponseBodyAsString());
return parseClientErr(null, e);
}
}
/**
* 获取静态文件(流式返回)
*/
public Flux<DataBuffer> getStaticFile(String targetPrefix, String relativePath, String logId) {
// 使用 UriComponentsBuilder 来正确处理路径编码
String[] prefixSegments = Arrays.stream(targetPrefix.split("/")).filter(segment -> !segment.isEmpty()).toArray(String[]::new);
String[] relativeSegments = Arrays.stream(relativePath.split("/")).filter(segment -> !segment.isEmpty()).toArray(String[]::new);
String url = appendRuntimeParamsToQuery(
UriComponentsBuilder.fromHttpUrl(buildBaseUrl(null)).pathSegment(prefixSegments).pathSegment(relativeSegments),
null).toUriString();
log.info("[Build-server] log Id={} callgetstaticfile API, url={}, target Prefix={}, relative Path={}", logId, url, targetPrefix, relativePath);
return webClient.get()
.uri(url)
.accept(MediaType.ALL) // 接受所有媒体类型,因为静态文件可能是图片、文本等
.retrieve()
.bodyToFlux(DataBuffer.class)
.doOnError(WebClientResponseException.class, e -> {
log.warn("[Build-server] log Id={} callgetstaticfile APIfailed, status={}, response Body={}", logId, e.getStatusCode(), e.getResponseBodyAsString());
}).doOnError(Throwable.class, e -> {
log.error("[Build-server] log Id={} callgetstaticfile APIexception", logId, e);
}).doOnDiscard(DataBuffer.class, DataBufferUtils::release).doOnComplete(() -> {
log.info("[Build-server] log Id={} callgetstaticfile API, streaming completed", logId);
});
}
public Map<String, Object> clearAllLogCache() {
String url = appendRuntimeParamsToQuery(
UriComponentsBuilder.fromHttpUrl(buildBaseUrl(null) + "/build/clear-all-log-cache"), null)
.toUriString();
log.info("[Build-server] callclearlogcache API, url={}", url);
RestTemplate restTemplate = new RestTemplate();
try {
ResponseEntity<Map<String, Object>> entity = restTemplate.exchange(url, HttpMethod.GET, null, new ParameterizedTypeReference<Map<String, Object>>() {
});
Map<String, Object> body = entity.getBody();
log.info("[Build-server] callclearlogcache API, response={}", body);
return body;
} catch (HttpClientErrorException e) {
log.warn("[Build-server] callclearlogcache APIfailed, status={}, response Body={}", e.getStatusCode(), e.getResponseBodyAsString());
return parseClientErr(null, e);
}
}
// 捕获4xx错误尝试解析响应体
private Map<String, Object> parseClientErr(Long projectId, HttpClientErrorException e) {
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("success", false);
resultMap.put("message", e.getMessage());
try {
String responseBody = e.getResponseBodyAsString();
if (responseBody != null && !responseBody.isEmpty()) {
ObjectMapper objectMapper = new ObjectMapper();
@SuppressWarnings("unchecked") Map<String, Object> errorResponse = objectMapper.readValue(responseBody, Map.class);
if (errorResponse.containsKey("code")) {
resultMap.put("code", errorResponse.get("code"));
}
if (errorResponse.containsKey("message")) {
resultMap.put("message", errorResponse.get("message"));
} else if (errorResponse.containsKey("error")) {
Object errorObj = errorResponse.get("error");
if (errorObj instanceof Map) {
@SuppressWarnings("unchecked") Map<String, Object> errorMap = (Map<String, Object>) errorObj;
if (errorMap.containsKey("message")) {
resultMap.put("message", errorMap.get("message"));
}
}
}
}
} catch (Exception parseException) {
log.error("[Build-server] project Id={} parse error response body failed", projectId, parseException);
}
return resultMap;
}
private String buildBaseUrl(Long projectId) {
RuntimeContext runtimeContext = getRuntimeContext(projectId);
String baseUrl = runtimeContext == null ? null : runtimeContext.getBaseUrl();
if (baseUrl == null || baseUrl.isBlank()) {
baseUrl = configuredBaseUrl;
}
if (baseUrl == null || baseUrl.isBlank()) {
throw new IllegalArgumentException("BaseUrl is required");
}
String url = baseUrl.trim();
while (url.endsWith("/")) {
url = url.substring(0, url.length() - 1);
}
return url;
}
private RuntimeContext getRuntimeContext(Long projectId) {
String baseUrl = null;
Long tenantId = null;
Long spaceId = null;
String isolationType = null;
String podId = null;
if (projectId != null) {
try {
CustomPageConfigModel configModel = customPageConfigRepository.getById(projectId);
spaceId = configModel == null ? null : configModel.getSpaceId();
Long sandboxId = configModel == null ? null : configModel.getSandboxId();
if (sandboxId == null) {
return null;
}
RequestContext<?> requestContext = RequestContext.get();
tenantId = requestContext == null ? null : requestContext.getTenantId();
if (tenantId == null && configModel != null) {
tenantId = configModel.getTenantId();
}
Long userId = requestContext == null ? null : requestContext.getUserId();
SandboxConfigRpcDto sandboxConfig = sandboxConfigRpcService.selectAppDevelopmentSandbox(
tenantId,
userId,
configModel.getSpaceId(),
projectId,
sandboxId);
if (sandboxConfig != null) {
SandboxConfigValue configValue = sandboxConfig.getConfigValue();
if (configValue != null && configValue.getHostWithScheme() != null && !configValue.getHostWithScheme().isBlank()
&& configValue.getFileServerPort() > 0) {
baseUrl = configValue.getHostWithScheme() + ":" + configValue.getFileServerPort() + "/api";
isolationType = sandboxConfig.getIsolation() == null ? null : sandboxConfig.getIsolation().name();
//podId = sandboxConfig.getIsolationKey();
}
}
} catch (Exception e) {
log.warn("[Build-server] project Id={} resolve dynamic base url failed", projectId, e);
}
}
if (baseUrl == null || baseUrl.isBlank()) {
return null;
}
return new RuntimeContext(baseUrl, tenantId, spaceId, isolationType);
}
private UriComponentsBuilder appendRuntimeParamsToQuery(UriComponentsBuilder builder, Long projectId) {
RuntimeContext runtimeContext = getRuntimeContext(projectId);
if (runtimeContext == null) {
return builder;
}
if (runtimeContext.getTenantId() != null) {
builder.queryParam("tenantId", runtimeContext.getTenantId());
}
if (runtimeContext.getSpaceId() != null) {
builder.queryParam("spaceId", runtimeContext.getSpaceId());
}
if (runtimeContext.getIsolationType() != null && !runtimeContext.getIsolationType().isBlank()) {
builder.queryParam("isolationType", runtimeContext.getIsolationType());
}
return builder;
}
private Map<String, Object> appendRuntimeParamsToBody(Map<String, Object> requestBody, Long projectId) {
RuntimeContext runtimeContext = getRuntimeContext(projectId);
if (runtimeContext == null) {
return requestBody;
}
if (runtimeContext.getTenantId() != null) {
requestBody.put("tenantId", runtimeContext.getTenantId());
}
if (runtimeContext.getSpaceId() != null) {
requestBody.put("spaceId", runtimeContext.getSpaceId());
}
if (runtimeContext.getIsolationType() != null && !runtimeContext.getIsolationType().isBlank()) {
requestBody.put("isolationType", runtimeContext.getIsolationType());
}
return requestBody;
}
private void appendRuntimeParamsToBody(LinkedMultiValueMap<String, Object> requestBody, Long projectId) {
RuntimeContext runtimeContext = getRuntimeContext(projectId);
if (runtimeContext == null) {
return;
}
if (runtimeContext.getTenantId() != null) {
requestBody.add("tenantId", runtimeContext.getTenantId());
}
if (runtimeContext.getSpaceId() != null) {
requestBody.add("spaceId", runtimeContext.getSpaceId());
}
if (runtimeContext.getIsolationType() != null && !runtimeContext.getIsolationType().isBlank()) {
requestBody.add("isolationType", runtimeContext.getIsolationType());
}
}
@Data
@AllArgsConstructor
private static class RuntimeContext {
private final String baseUrl;
private final Long tenantId;
private final Long spaceId;
private final String isolationType;
}
}

View File

@@ -0,0 +1,30 @@
package com.xspaceagi.custompage.domain.keepalive;
import java.util.Date;
import java.util.Map;
import com.xspaceagi.system.spec.common.UserContext;
import com.xspaceagi.system.spec.dto.ReqResult;
public interface IKeepAliveService {
/**
* 处理保活请求
*/
ReqResult<Map<String, Object>> handleKeepAlive(Long projectId, UserContext userContext);
/**
* 更新保活信息
*/
void updateKeepAlive(Long projectId,
Date keepAliveTime,
Integer devRunning,
Integer devPid,
Integer devPort,
UserContext userContext);
/**
* 删除保活缓存
*/
void removeKeepAliveCache(Long projectId);
}

View File

@@ -0,0 +1,322 @@
package com.xspaceagi.custompage.domain.keepalive;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.xspaceagi.custompage.domain.gateway.PageFileBuildClient;
import com.xspaceagi.custompage.domain.model.CustomPageBuildModel;
import com.xspaceagi.custompage.domain.repository.ICustomPageBuildRepository;
import com.xspaceagi.system.spec.common.RequestContext;
import com.xspaceagi.system.spec.tenant.thread.TenantFunctions;
import com.xspaceagi.system.spec.utils.RedisUtil;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
/**
* 保活守护程序
*/
@Slf4j
@Service
public class KeepAliveDaemonService {
@Resource
private RedisUtil redisUtil;
@Resource
private RedissonClient redissonClient;
@Resource
private PageFileBuildClient pageFileBuildClient;
@Resource
private ICustomPageBuildRepository customPageBuildRepository;
private final ObjectMapper objectMapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// 保活超时时间5分钟
private static final long KEEPALIVE_TIMEOUT_MS = 5 * 60 * 1000;
// 定时检查间隔20秒
private static final long CHECK_INTERVAL_SECONDS = 20;
// Redis key前缀
private static final String KEEPALIVE_KEY_PREFIX = "dev:keepalive:";
// 分布式锁key
private static final String KEEPALIVE_LOCK_KEY = "dev:keepalive:lock:check";
// 所有保活项目ID集合的key
private static final String KEEPALIVE_PROJECTS_SET_KEY = "dev:keepalive:projects";
private ScheduledExecutorService scheduledExecutor;
@PostConstruct
public void init() {
// 启动定时检查任务
scheduledExecutor = Executors.newSingleThreadScheduledExecutor(r -> {
Thread thread = new Thread(r, "dev-keepalive-checker");
thread.setDaemon(true);
return thread;
});
// 启动时从数据库加载 devRunning=1 的项目到缓存
try {
TenantFunctions.callWithIgnoreCheck(this::warmupKeepAliveCache);
} catch (Exception e) {
log.error("[Keep Alive-daemon] warm-up Rediskeep-alivecachefailed", e);
}
scheduledExecutor.scheduleWithFixedDelay(
this::checkKeepAliveTimeout,
CHECK_INTERVAL_SECONDS,
CHECK_INTERVAL_SECONDS,
TimeUnit.SECONDS);
log.info("[Keep Alive-daemon] keep-aliveserviceinitializecompleted, scheduled checkinterval: {}seconds", CHECK_INTERVAL_SECONDS);
}
@PreDestroy
public void destroy() {
if (scheduledExecutor != null && !scheduledExecutor.isShutdown()) {
scheduledExecutor.shutdown();
try {
if (!scheduledExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
scheduledExecutor.shutdownNow();
}
} catch (InterruptedException e) {
scheduledExecutor.shutdownNow();
Thread.currentThread().interrupt();
}
}
log.info("[Keep Alive-daemon] keep-aliveserviceclosed");
}
/**
* 启动时预热Redis保活缓存从数据库导入 devRunning=1 的项目
*/
private Integer warmupKeepAliveCache() {
// 查询运行中的项目
List<CustomPageBuildModel> runningList = customPageBuildRepository.listByDevRunning(1);
if (runningList == null || runningList.isEmpty()) {
log.info("[Keep Alive-daemon] warm-upcompleted: project");
return 1;
}
log.info("[Keep Alive-daemon] startwarm-up, total: {}", runningList.size());
int success = 0;
for (CustomPageBuildModel model : runningList) {
try {
if (model.getProjectId() == null) {
continue;
}
updateKeepAliveCache(model.getProjectId(), model);
success++;
} catch (Exception e) {
log.error("[Keep Alive-daemon] project Id={},warm-upfailed", model.getProjectId(), e);
}
}
log.info("[Keep Alive-daemon] warm-upcompleted,total: {}, succeeded: {}", runningList.size(), success);
return success;
}
/**
* 定时检查保活超时
*/
private void checkKeepAliveTimeout() {
TenantFunctions.callWithIgnoreCheck(() -> {
log.debug("[Keep Alive-daemon] scheduled checktimeoutproject");
checkKeepAliveTimeout0();
return null;
});
}
/**
* 定时检查保活超时
*/
private void checkKeepAliveTimeout0() {
// 使用分布式锁确保只有一个实例执行检查
RLock lock = redissonClient.getLock(KEEPALIVE_LOCK_KEY);
try {
// 尝试获取锁最多等待1秒锁持有时间最多30秒
if (lock.tryLock(1, 30, TimeUnit.SECONDS)) {
try {
Date now = new Date();
long timeoutThreshold = now.getTime() - KEEPALIVE_TIMEOUT_MS;
//log.info("[Keep Alive-daemon] start timeoutproject, : {}", now);
// 从Redis获取所有保活项目ID
Set<Object> projectIds = redisUtil.members(KEEPALIVE_PROJECTS_SET_KEY);
if (projectIds == null || projectIds.isEmpty()) {
//log.debug("[Keep Alive-daemon] redis project,completed");
return;
}
log.info("[Keep Alive-daemon] found{}items to check", projectIds.size());
// 检查每个项目的保活状态
for (Object projectIdObj : projectIds) {
Long projectId = Long.valueOf(projectIdObj.toString());
//log.debug("[Keep Alive-daemon] start : project Id={} --", projectId);
try {
CustomPageBuildModel model = getKeepAliveCache(projectId);
if (model == null || model.getLastKeepAliveTime() == null) {
// 如果Redis中没有数据从保活项目集合中移除
//log.debug("[Keep Alive-daemon] project Id={},redis , keep-alive", projectId);
redisUtil.remove(KEEPALIVE_PROJECTS_SET_KEY, projectId.toString());
continue;
}
long lastKeepAliveTime = model.getLastKeepAliveTime().getTime();
if (lastKeepAliveTime < timeoutThreshold) {
//log.debug("[Keep Alive-daemon] project Id={},projecttimeout, keep-alive : {},start executionstop service", projectId, model.getLastKeepAliveTime());
// 停止开发服务器
stopDevServerIfRunning(projectId, model);
}
} catch (Exception e) {
log.error("[Keep Alive-daemon] project Id={},keep-alive check exception", projectId, e);
}
}
//log.debug("[Keep Alive-daemon] keep-alive completed");
} finally {
lock.unlock();
}
} else {
log.info("[Keep Alive-daemon] failed to acquire distributed lock,skip");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.warn("[Keep Alive-daemon] acquire distributed lock interrupted", e);
} catch (Exception e) {
log.error("[Keep Alive-daemon] keep-alive check exception", e);
}
}
/**
* 更新Redis中的保活缓存
*/
private void updateKeepAliveCache(Long projectId, CustomPageBuildModel model) {
try {
String key = KEEPALIVE_KEY_PREFIX + projectId;
String value = objectMapper.writeValueAsString(model);
// 不设置过期时间,手动管理生命周期
redisUtil.set(key, value);
// 将projectId添加到保活项目集合中
redisUtil.sSet(KEEPALIVE_PROJECTS_SET_KEY, projectId.toString());
log.info("[Keep Alive-daemon] update Rediskeep-alivecache, project Id={}", projectId);
} catch (JsonProcessingException e) {
log.error("[Keep Alive-daemon] serialize keep-alive data failed, project Id={}", projectId, e);
}
}
/**
* 从Redis删除保活缓存
*/
private void removeKeepAliveCache(Long projectId) {
String key = KEEPALIVE_KEY_PREFIX + projectId;
// 直接删除key而不是设置过期时间
redisUtil.expire(key, 0);
// 从保活项目集合中移除projectId
redisUtil.remove(KEEPALIVE_PROJECTS_SET_KEY, projectId.toString());
log.info("[Keep Alive-daemon] project Id={},deleterediskeep-alivecachecompleted", projectId);
}
/**
* 停止开发服务器(如果正在运行)
*/
private void stopDevServerIfRunning(Long projectId, CustomPageBuildModel model) {
boolean contextInstalled = false;
try {
if (model.getDevRunning() == null || model.getDevRunning() != 1 || model.getDevPid() == null) {
removeKeepAliveCache(projectId);
log.info("[Keep Alive-daemon] project Id={}, dev server not running, cleared keep-alive cache", projectId);
return;
}
Long tenantId = resolveTenantId(projectId, model);
if (tenantId == null) {
log.error("[Keep Alive-daemon] project Id={}, cannot stop dev server: tenantId missing", projectId);
return;
}
contextInstalled = installRequestContext(tenantId);
log.info("[Keep Alive-daemon] project Id={}, pid={}, start stop dev service", projectId, model.getDevPid());
Map<String, Object> resp = pageFileBuildClient.stopDev(projectId, model.getDevPid());
if (resp == null) {
log.error("[Keep Alive-daemon] project Id={}, stop dev failed, no response", projectId);
return;
}
boolean success = Boolean.parseBoolean(String.valueOf(resp.get("success")));
String message = resp.get("message") == null ? "" : String.valueOf(resp.get("message"));
if (!success) {
log.error("[Keep Alive-daemon] project Id={}, stop dev failed, message={}", projectId, message);
return;
}
removeKeepAliveCache(projectId);
customPageBuildRepository.updateStopDevStatus(projectId, null);
log.info("[Keep Alive-daemon] project Id={}, dev server stopped", projectId);
} catch (Exception e) {
log.error("[Keep Alive-daemon] project Id={} stop dev exception", projectId, e);
} finally {
if (contextInstalled) {
RequestContext.remove();
}
}
}
private Long resolveTenantId(Long projectId, CustomPageBuildModel model) {
if (model != null && model.getTenantId() != null) {
return model.getTenantId();
}
CustomPageBuildModel dbModel = customPageBuildRepository.getByProjectId(projectId);
return dbModel == null ? null : dbModel.getTenantId();
}
private boolean installRequestContext(Long tenantId) {
RequestContext<?> existing = RequestContext.get();
if (existing != null && tenantId.equals(existing.getTenantId())) {
return false;
}
RequestContext<?> requestContext = new RequestContext<>();
requestContext.setTenantId(tenantId);
RequestContext.set(requestContext);
return true;
}
/**
* 从Redis获取保活缓存
*/
private CustomPageBuildModel getKeepAliveCache(Long projectId) {
try {
String key = KEEPALIVE_KEY_PREFIX + projectId;
Object value = redisUtil.get(key);
if (value != null) {
return objectMapper.readValue(value.toString(), CustomPageBuildModel.class);
}
} catch (JsonProcessingException e) {
log.error("[Keep Alive-daemon] deserialize keep-alive data failed, project Id={}", projectId, e);
}
return null;
}
}

View File

@@ -0,0 +1,197 @@
package com.xspaceagi.custompage.domain.keepalive;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.xspaceagi.custompage.domain.gateway.PageFileBuildClient;
import com.xspaceagi.custompage.domain.model.CustomPageBuildModel;
import com.xspaceagi.custompage.domain.proxypath.ICustomPageProxyPathService;
import com.xspaceagi.custompage.domain.repository.ICustomPageBuildRepository;
import com.xspaceagi.custompage.domain.service.ICustomPageConfigDomainService;
import com.xspaceagi.system.spec.common.UserContext;
import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.spec.enums.YesOrNoEnum;
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;
/**
* 保活服务
*/
@Slf4j
@Service
public class KeepAliveServiceImpl implements IKeepAliveService {
@Resource
private RedisUtil redisUtil;
@Resource
private PageFileBuildClient pageFileBuildClient;
@Resource
private ICustomPageBuildRepository customPageBuildRepository;
@Resource
private ICustomPageConfigDomainService customPageConfigDomainService;
@Resource
private ICustomPageProxyPathService customPageProxyPathService;
private final ObjectMapper objectMapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// Redis key前缀
private static final String KEEPALIVE_KEY_PREFIX = "dev:keepalive:";
// 所有保活项目ID集合的key
private static final String KEEPALIVE_PROJECTS_SET_KEY = "dev:keepalive:projects";
/**
* 处理保活请求
*/
public ReqResult<Map<String, Object>> handleKeepAlive(Long projectId, UserContext userContext) {
log.info("[Keep Alive] project Id={},starthandlekeep-aliverequest", projectId);
try {
CustomPageBuildModel project = customPageBuildRepository.getByProjectId(projectId);
if (project == null) {
log.info("[Keep Alive] project Id={},projectnot found", projectId);
return ReqResult.error("0001", "Project does not exist");
}
String devProxyPath = customPageProxyPathService.getDevProxyPath(projectId);
Map<String, Object> serverResp = null;
// 需要先判断表中的状态,如果直接请求server启动dev,有可能server重启过,丢失了缓存,但dev还活着,再次请求就会多开dev服务器
if (project.getDevPid() != null && project.getDevPort() != null) {
// 调server保活
log.info("[Keep Alive] project Id={},dev is running from table, call server keep-alive API", projectId);
serverResp = pageFileBuildClient.keepAlive(projectId, devProxyPath, project.getDevPid(),
project.getDevPort());
} else {
// 调server启动dev
log.info("[Keep Alive] project Id={},dev not running, call server start dev", projectId);
serverResp = pageFileBuildClient.startDev(projectId, devProxyPath);
}
if (serverResp == null) {
log.info("[Keep Alive] project Id={},keep-alivefailed,server returned null", projectId);
updateKeepAlive(projectId, new Date(), YesOrNoEnum.N.getKey(), null, null, userContext);
return ReqResult.error("9999", "Keep-alive failed: server returned no response");
}
boolean success = Boolean.parseBoolean(String.valueOf(serverResp.get("success")));
String message = serverResp.get("message") == null ? "" : String.valueOf(serverResp.get("message"));
if (!success) {
log.error("[Keep Alive] project Id={},keep-alivefailed,serverreturned error,code={},message={}", projectId,
serverResp.get("code"), message);
updateKeepAlive(projectId, new Date(), YesOrNoEnum.N.getKey(), null, null, userContext);
String code = serverResp.get("code") == null ? "" : String.valueOf(serverResp.get("code"));
if ("PROJECT_STARTING".equals(code)) {
return ReqResult.error(ErrorCodeEnum.PROJECT_STARTING.getCode(), ErrorCodeEnum.PROJECT_STARTING.getMsg());
}
return ReqResult.error("9999", message);
}
// 持久化 dev 运行信息
Integer pid = null;
Integer port = null;
try {
Object pidObj = serverResp.get("pid");
Object portObj = serverResp.get("port");
pid = Integer.valueOf(String.valueOf(pidObj));
port = Integer.valueOf(String.valueOf(portObj));
} catch (Exception e) {
log.error("[Keep Alive] project Id={},get dev port and pid exception", projectId, e);
updateKeepAlive(projectId, new Date(), YesOrNoEnum.N.getKey(), null, null, userContext);
return ReqResult.error("9999", "Failed to obtain service port and process ID: " + e.getMessage());
}
updateKeepAlive(projectId, new Date(), YesOrNoEnum.Y.getKey(), pid, port, userContext);
// 返回结果
Map<String, Object> result = new HashMap<>();
result.put("devRunning", YesOrNoEnum.Y.getKey());
result.put("port", port);
return ReqResult.success(result);
} catch (Exception e) {
log.error("[Keep Alive] project Id={},keep-alivehandleexception", projectId, e);
return ReqResult.error("9999", "Keep-alive error: " + e.getMessage());
}
}
/**
* 更新保活信息
*/
public void updateKeepAlive(Long projectId,
Date keepAliveTime,
Integer devRunning,
Integer devPid,
Integer devPort,
UserContext userContext) {
CustomPageBuildModel keepAliveModel = new CustomPageBuildModel();
keepAliveModel.setProjectId(projectId);
keepAliveModel.setLastKeepAliveTime(keepAliveTime);
keepAliveModel.setDevRunning(devRunning);
keepAliveModel.setDevPid(devPid);
keepAliveModel.setDevPort(devPort);
keepAliveModel.setTenantId(userContext.getTenantId());
// 更新Redis缓存
updateKeepAliveCache(projectId, keepAliveModel);
// 更新数据库
updatKeepAliveDb(keepAliveModel, userContext);
}
/**
* 更新缓存
*/
private void updateKeepAliveCache(Long projectId, CustomPageBuildModel model) {
try {
String key = KEEPALIVE_KEY_PREFIX + projectId;
String value = objectMapper.writeValueAsString(model);
// 不设置过期时间,手动管理生命周期
redisUtil.set(key, value);
// 将projectId添加到保活项目集合中
redisUtil.sSet(KEEPALIVE_PROJECTS_SET_KEY, projectId.toString());
log.info("[Keep Alive] project Id={},cache updated", projectId);
} catch (Exception e) {
log.error("[Keep Alive] project Id={},cacheupdatefailed", projectId, e);
}
}
/**
* 更新库表
*/
private void updatKeepAliveDb(CustomPageBuildModel model, UserContext userContext) {
try {
customPageBuildRepository.updateKeepAlive(model, userContext);
log.info("[Keep Alive] project Id={},db table updated", model.getProjectId());
} catch (Exception e) {
log.error("[Keep Alive] project Id={},db table update failed", model.getProjectId(), e);
throw BizException.of(ErrorCodeEnum.SYS_ERROR, BizExceptionCodeEnum.customPageKeepAliveDbUpdateFailed);
}
}
/**
* 删除保活缓存
*/
@Override
public void removeKeepAliveCache(Long projectId) {
redisUtil.expire(KEEPALIVE_KEY_PREFIX + projectId, 0);
redisUtil.remove(KEEPALIVE_PROJECTS_SET_KEY, projectId.toString());
log.info("[Keep Alive] project Id={},deleterediskeep-alivecachecompleted", projectId);
}
}

View File

@@ -0,0 +1,66 @@
package com.xspaceagi.custompage.domain.model;
import java.util.Date;
import java.util.List;
import com.xspaceagi.custompage.sdk.dto.VersionInfoDto;
import lombok.Data;
@Data
public class CustomPageBuildModel {
// 项目ID
private Long id;
private Long agentId;
private Long projectId;
// 1:运行中
private Integer devRunning;
private Integer devPid;
private Integer devPort;
// 最后保活时间
private Date lastKeepAliveTime;
// 1:运行中
private Integer buildRunning;
private Date buildTime;
private Integer buildVersion;
private Integer codeVersion;
private List<VersionInfoDto> versionInfo;
// 上次对话模型ID
private Long lastChatModelId;
// 上次多模态ID
private Long lastMultiModelId;
private Long tenantId;
private Long spaceId;
private Date created;
private Long creatorId;
private String creatorName;
private Date modified;
private Long modifiedId;
private String modifiedName;
// 1:有效; -1:无效
private Integer yn;
}

View File

@@ -0,0 +1,65 @@
package com.xspaceagi.custompage.domain.model;
import java.util.Date;
import java.util.List;
import com.xspaceagi.custompage.sdk.dto.*;
import lombok.Data;
@Data
public class CustomPageConfigModel {
private Long id;
private String name;
private String description;
private String icon;
private String coverImg;
private SourceTypeEnum coverImgSourceType;
// 自定义页面唯一标识
private String basePath;
// 1:运行中
private Integer buildRunning;
// 发布类型
private PublishTypeEnum publishType;
// 需要登录,1:需要
private Integer needLogin;
// 调试关联智能体
private Long devAgentId;
// 页面项目类型
private ProjectType projectType;
private List<ProxyConfig> proxyConfigs;
private List<PageArgConfig> pageArgConfigs;
private List<DataSourceDto> dataSources;
private Long sandboxId;
private Object ext;
private Long tenantId;
private Long spaceId;
private Date created;
private Long creatorId;
private String creatorName;
private Date modified;
private Long modifiedId;
private String modifiedName;
private Integer yn;
}

View File

@@ -0,0 +1,42 @@
package com.xspaceagi.custompage.domain.model;
import java.util.Date;
import lombok.Data;
@Data
public class CustomPageConversationModel {
private Long id;
// 项目ID
private Long projectId;
// 会话主题
private String topic;
// 会话内容
private String content;
// 消息角色USER/ASSISTANT
private String role;
// 会话ID
private String sessionId;
// 请求ID
private String requestId;
private Long tenantId;
private Long spaceId;
private Date created;
private Long creatorId;
private String creatorName;
private Date modified;
private Long modifiedId;
private String modifiedName;
private Integer yn;
}

View File

@@ -0,0 +1,21 @@
package com.xspaceagi.custompage.domain.model;
import java.util.Date;
import lombok.Data;
@Data
public class CustomPageDomainModel {
private Long id;
private Long tenantId;
private Long projectId;
private String domain;
private Date created;
private Date modified;
}

View File

@@ -0,0 +1,141 @@
package com.xspaceagi.custompage.domain.proxypath;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.xspaceagi.custompage.domain.model.CustomPageConfigModel;
import com.xspaceagi.custompage.domain.service.ICustomPageConfigDomainService;
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;
/**
* dev/prod代理路径生成服务
*/
@Slf4j
@Service
public class CustomPageProxyPathServiceImpl implements ICustomPageProxyPathService {
@Resource
private ICustomPageConfigDomainService customPageConfigDomainService;
@Resource
private RedisUtil redisUtil;
private final ObjectMapper objectMapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// 缓存key前缀
private static final String CONFIG_CACHE_KEY_PREFIX = "custom_page:config:";
// 缓存过期时间60分钟
private static final long CACHE_EXPIRE_SECONDS = 60 * 60;
@Override
public String getDevProxyPath(Long projectId) {
CustomPageConfigModel configModel = getConfigModelWithCache(projectId);
if (configModel != null) {
return getDevProxyPath(configModel);
} else {
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.customPageProxyPathLoadFailed);
}
}
@Override
public String getDevProxyPath(CustomPageConfigModel configModel) {
if (configModel == null) {
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "configuration object");
}
String proxyPath = "/page" + configModel.getBasePath();
proxyPath += "-" + getAgentId(configModel);
proxyPath += "/dev/";
return proxyPath;
}
@Override
public String getProdProxyPath(Long projectId) {
CustomPageConfigModel configModel = getConfigModelWithCache(projectId);
if (configModel != null) {
return getProdProxyPath(configModel);
} else {
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.customPageProxyPathLoadFailed);
}
}
@Override
public String getProdProxyPath(CustomPageConfigModel configModel) {
if (configModel == null) {
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.fieldRequiredButEmpty, "configuration object");
}
String proxyPath = "/page" + configModel.getBasePath();
proxyPath += "-" + getAgentId(configModel);
proxyPath += "/prod/";
return proxyPath;
}
@Override
public void removeConfigCache(Long projectId) {
String cacheKey = CONFIG_CACHE_KEY_PREFIX + projectId;
redisUtil.expire(cacheKey, 0);
log.info("[Proxy Path] config cache cleared, project Id={}", projectId);
}
private Long getAgentId(CustomPageConfigModel configModel) {
Long devAgentId = configModel.getDevAgentId();
if (devAgentId == null) {
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.customPageProxyPathNoAgent);
}
return devAgentId;
// 从 RequestContext 获取租户配置
// TenantConfigDto tenantConfig = (TenantConfigDto)
// RequestContext.get().getTenantConfig();
// if (tenantConfig == null || tenantConfig.getDefaultAgentId() == null) {
// throw new BizException("0001", "租户未配置默认智能体,请先配置");
// }
// return tenantConfig.getDefaultAgentId();
}
private CustomPageConfigModel getConfigModelWithCache(Long projectId) {
String cacheKey = CONFIG_CACHE_KEY_PREFIX + projectId;
CustomPageConfigModel configModel = null;
try {
// 先从缓存获取
Object cachedValue = redisUtil.get(cacheKey);
if (cachedValue != null) {
log.debug("[Proxy Path] from cachegetconfig, project Id={}", projectId);
CustomPageConfigModel cacheModel = objectMapper.readValue(cachedValue.toString(),
CustomPageConfigModel.class);
if (cacheModel.getBasePath() != null && cacheModel.getDevAgentId() != null) {
return cacheModel;
}
// 缓存错误,删除缓存
removeConfigCache(projectId);
}
log.debug("[Proxy Path] cache miss, from DBqueryconfig, project Id={}", projectId);
configModel = customPageConfigDomainService.getById(projectId);
if (configModel != null) {
String configJson = objectMapper.writeValueAsString(configModel);
redisUtil.set(cacheKey, configJson, CACHE_EXPIRE_SECONDS);
log.debug("[Proxy Path] configcached, project Id={}", projectId);
}
return configModel;
} catch (Exception e) {
log.error("[Proxy Path] serialize/deserializeconfigfailed, project Id={}", projectId, e);
// 缓存异常时,直接查询数据库
return configModel != null ? configModel : customPageConfigDomainService.getById(projectId);
}
}
}

View File

@@ -0,0 +1,34 @@
package com.xspaceagi.custompage.domain.proxypath;
import com.xspaceagi.custompage.domain.model.CustomPageConfigModel;
/**
* dev/prod代理路径生成并支持缓存优化
*/
public interface ICustomPageProxyPathService {
/**
* 获取dev代理路径
*/
String getDevProxyPath(Long projectId);
/**
* 获取dev代理路径
*/
String getDevProxyPath(CustomPageConfigModel configModel);
/**
* 获取prod代理路径
*/
String getProdProxyPath(Long projectId);
/**
* 获取prod代理路径
*/
String getProdProxyPath(CustomPageConfigModel configModel);
/**
* 清除指定项目的缓存
*/
void removeConfigCache(Long projectId);
}

View File

@@ -0,0 +1,67 @@
package com.xspaceagi.custompage.domain.repository;
import java.util.List;
import com.xspaceagi.custompage.domain.model.CustomPageBuildModel;
import com.xspaceagi.system.spec.common.UserContext;
import com.xspaceagi.system.spec.page.SuperPage;
public interface ICustomPageBuildRepository {
/**
* 根据项目ID查询记录
*/
CustomPageBuildModel getByProjectId(Long projectId);
/**
* 心跳更新
*/
void updateKeepAlive(CustomPageBuildModel model, UserContext userContext);
/**
* 构建成功后的状态落库
*/
void updateBuildStatus(Long projectId, Integer codeVersion, UserContext userContext);
/**
* 停止开发服务器后的状态落库
*/
void updateStopDevStatus(Long projectId, UserContext userContext);
/**
* 新增项目记录
*/
Long add(CustomPageBuildModel model, UserContext userContext);
/**
* 分页查询
*/
SuperPage<CustomPageBuildModel> pageQuery(CustomPageBuildModel model, Long current,
Long pageSize);
/**
* 查询列表
*/
List<CustomPageBuildModel> list(CustomPageBuildModel model);
/**
* 根据项目ID列表查询构建信息列表
*/
List<CustomPageBuildModel> listByProjectIds(List<Long> projectIdList);
/**
* 删除项目
*/
void deleteByProjectId(Long projectId, UserContext userContext);
/**
* 按开发运行状态查询列表
*/
List<CustomPageBuildModel> listByDevRunning(Integer devRunning);
/**
* 更新版本信息
*/
void updateVersionInfo(CustomPageBuildModel updateModel, UserContext userContext);
}

View File

@@ -0,0 +1,77 @@
package com.xspaceagi.custompage.domain.repository;
import java.util.List;
import com.xspaceagi.custompage.domain.model.CustomPageConfigModel;
import com.xspaceagi.custompage.sdk.dto.PublishTypeEnum;
import com.xspaceagi.system.spec.common.UserContext;
import com.xspaceagi.system.spec.page.SuperPage;
/**
* 自定义页面配置仓储接口
*/
public interface ICustomPageConfigRepository {
/**
* 查询配置列表
*/
List<CustomPageConfigModel> list(CustomPageConfigModel model);
/**
* 分页查询配置
*/
SuperPage<CustomPageConfigModel> pageQuery(CustomPageConfigModel configModel, Long current, Long pageSize);
/**
* 根据ID查询配置
*/
CustomPageConfigModel getById(Long id);
/**
* 根据agentId查询配置
*/
CustomPageConfigModel getByAgentId(Long agentId);
/**
* 根据basePath查询配置
*/
CustomPageConfigModel getByBasePath(String basePath);
/**
* 新增配置
*/
Long add(CustomPageConfigModel model, UserContext userContext);
/**
* 更新配置
*/
void updateById(CustomPageConfigModel model, UserContext userContext);
/**
* 更新构建状态
*/
void updateBuildStatus(Long projectId, PublishTypeEnum publishTypeEnum, UserContext userContext);
/**
* 根据ID删除配置
*/
void deleteById(Long projectId, UserContext userContext);
/**
* 根据devAgentId列表查询配置
*/
List<CustomPageConfigModel> listByDevAgentIds(List<Long> devAgentIds);
/**
* 根据id列表查询配置
*/
List<CustomPageConfigModel> listByIds(List<Long> ids);
/**
* 统计网页应用总数
*
* @return 网页应用总数
*/
Long countTotalPages();
}

View File

@@ -0,0 +1,62 @@
package com.xspaceagi.custompage.domain.repository;
import java.util.List;
import com.xspaceagi.custompage.domain.model.CustomPageConversationModel;
import com.xspaceagi.system.spec.common.UserContext;
import com.xspaceagi.system.spec.page.SuperPage;
/**
* 自定义页面会话记录仓储接口
*/
public interface ICustomPageConversationRepository {
/**
* 保存会话记录
*/
Long save(CustomPageConversationModel model, UserContext userContext);
/**
* 根据项目ID查询会话记录列表
*/
List<CustomPageConversationModel> listByProjectId(Long projectId, Long userId);
/**
* 分页查询会话记录
*/
SuperPage<CustomPageConversationModel> pageQuery(CustomPageConversationModel queryModel, Long current,
Long pageSize);
/**
* 根据requestId回填用户消息的sessionId
*/
boolean updateUserSessionIdByRequestId(Long projectId, String requestId, String sessionId, Long userId);
/**
* 按 sessionId 查询最新一条用户消息
*/
CustomPageConversationModel findLatestUserBySessionId(Long projectId, String sessionId);
/**
* 按项目查询最新一条用户消息(用于未传 request_id 且 USER 尚未回填 Agent sessionId 时的回退)
*/
CustomPageConversationModel findLatestUserByProjectId(Long projectId);
/**
* 按 requestId 查询本轮助手消息
*/
CustomPageConversationModel findAssistantByProjectIdAndRequestId(Long projectId, String requestId);
CustomPageConversationModel findById(Long id);
/**
* 更新助手消息内容
*/
boolean updateAssistantContent(Long id, String content, String requestId, UserContext userContext);
/**
* 按项目ID删除会话记录软删
*/
boolean deleteByProjectId(Long projectId, Long userId);
}

View File

@@ -0,0 +1,43 @@
package com.xspaceagi.custompage.domain.repository;
import java.util.List;
import com.xspaceagi.custompage.domain.model.CustomPageDomainModel;
/**
* 自定义页面域名绑定仓储接口
*/
public interface ICustomPageDomainRepository {
/**
* 根据project_id查询域名列表
*/
List<CustomPageDomainModel> listByProjectId(Long projectId);
/**
* 根据ID查询域名绑定
*/
CustomPageDomainModel getById(Long id);
/**
* 根据域名查询
*/
CustomPageDomainModel getByDomain(String domain);
/**
* 新增域名绑定
*/
CustomPageDomainModel add(CustomPageDomainModel model);
/**
* 更新域名绑定
*/
void updateById(CustomPageDomainModel model);
/**
* 删除域名绑定
*/
void removeById(Long id);
List<String> listAllDomains();
}

View File

@@ -0,0 +1,125 @@
package com.xspaceagi.custompage.domain.service;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import com.xspaceagi.custompage.domain.model.CustomPageConversationModel;
import com.xspaceagi.custompage.domain.repository.ICustomPageConversationRepository;
import com.xspaceagi.custompage.domain.util.AgentProgressEventUtil;
import com.xspaceagi.system.spec.utils.RedisUtil;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
/**
* 跨实例协调 Agent 进度采集锁与终态标记(键含 projectId + requestId按轮隔离
*/
@Slf4j
@Component
public class AgentProgressSessionCoordinator {
private static final String CAPTURE_LOCK_PREFIX = "custom.page.agent.progress.lock.";
private static final String DONE_PREFIX = "custom.page.agent.progress.done.";
private static final int CAPTURE_LOCK_TTL_SECONDS = 7200;
private static final int DONE_TTL_SECONDS = 86400;
@Resource
private RedisUtil redisUtil;
@Resource
private ICustomPageConversationRepository customPageConversationRepository;
public boolean tryAcquireCaptureLock(Long projectId, String requestId) {
if (projectId == null || StringUtils.isBlank(requestId)) {
return false;
}
return redisUtil.setIfAbsent(buildLockKey(projectId, requestId), lockOwner(), CAPTURE_LOCK_TTL_SECONDS);
}
public void releaseCaptureLock(Long projectId, String requestId) {
if (projectId == null || StringUtils.isBlank(requestId)) {
return;
}
String key = buildLockKey(projectId, requestId);
Object holder = redisUtil.get(key);
if (holder != null && lockOwner().equals(String.valueOf(holder))) {
redisUtil.expire(key, 0);
}
}
public void markTurnDone(Long projectId, String requestId) {
if (projectId == null || StringUtils.isBlank(requestId)) {
return;
}
redisUtil.set(buildDoneKey(projectId, requestId), "1", DONE_TTL_SECONDS);
}
public void clearTurnDone(Long projectId, String requestId) {
if (projectId == null || StringUtils.isBlank(requestId)) {
return;
}
redisUtil.expire(buildDoneKey(projectId, requestId), 0);
}
/**
* 当前待回复轮次的 requestId与 ai-chat-flux 一致),无则 null。
*/
public String resolvePendingTurnRequestId(Long projectId, String sessionId) {
CustomPageConversationModel pendingUser = resolveLatestPendingUser(projectId, sessionId);
return pendingUser == null ? null : pendingUser.getRequestId();
}
/** 新一轮 /chat 接受后清除该轮 Redis 终态,便于重新采集。 */
public void prepareForNewTurn(Long projectId, String requestId) {
clearTurnDone(projectId, requestId);
}
private CustomPageConversationModel resolveLatestPendingUser(Long projectId, String agentSessionId) {
if (projectId == null || StringUtils.isBlank(agentSessionId)) {
return null;
}
CustomPageConversationModel latestUser = customPageConversationRepository
.findLatestUserBySessionId(projectId, agentSessionId);
if (latestUser == null) {
latestUser = customPageConversationRepository.findLatestUserByProjectId(projectId);
}
if (latestUser == null || StringUtils.isBlank(latestUser.getRequestId())) {
return null;
}
if (isUserTurnStillInProgress(projectId, latestUser)) {
return latestUser;
}
return null;
}
private boolean isUserTurnStillInProgress(Long projectId, CustomPageConversationModel user) {
CustomPageConversationModel assistantForTurn = customPageConversationRepository
.findAssistantByProjectIdAndRequestId(projectId, user.getRequestId());
if (assistantForTurn == null) {
return true;
}
return !AgentProgressEventUtil.isAssistantContentTerminal(assistantForTurn.getContent());
}
private String buildLockKey(Long projectId, String requestId) {
return CAPTURE_LOCK_PREFIX + projectId + "." + requestId;
}
private String buildDoneKey(Long projectId, String requestId) {
return DONE_PREFIX + projectId + "." + requestId;
}
private String lockOwner() {
String pod = System.getenv("POD_NAME");
if (StringUtils.isBlank(pod)) {
pod = System.getenv("HOSTNAME");
}
if (StringUtils.isBlank(pod)) {
try {
pod = java.net.InetAddress.getLocalHost().getHostName();
} catch (Exception e) {
pod = "unknown-host";
}
}
return pod + "-" + ProcessHandle.current().pid();
}
}

View File

@@ -0,0 +1,537 @@
package com.xspaceagi.custompage.domain.service;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import com.alibaba.fastjson2.JSON;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.xspaceagi.custompage.domain.gateway.AiAgentClient;
import com.xspaceagi.custompage.domain.model.CustomPageConversationModel;
import com.xspaceagi.custompage.domain.util.AgentProgressContextUtil;
import com.xspaceagi.custompage.domain.util.AgentProgressEventUtil;
import com.xspaceagi.system.spec.common.UserContext;
import com.xspaceagi.system.spec.dto.ReqResult;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
/**
* Agent 进度:由 {@code /ai-session-sse} 触发订阅 Agent SSE前端断开后仍保持 Agent 连接直至终态并落库;
* 实时事件经内存转发给已连接的前端,不从 DB 推送给 SSE。
*/
@Slf4j
@Service
public class CustomPageAgentProgressCaptureService {
private static final long SSE_TIMEOUT_MS = 10L * 60 * 1000;
private static final long FRONTEND_ATTACH_POLL_MS = 300L;
/** 增量落库:每 N 条可持久化事件或间隔 T 触发一次(终态事件与流结束仍立即落库)。 */
private static final int PERSIST_EVERY_N_EVENTS = 20;
private static final long PERSIST_INTERVAL_MS = 15_000L;
private final ConcurrentHashMap<String, CaptureContext> activeCaptures = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, Object> sessionStartLocks = new ConcurrentHashMap<>();
/** 前端 SSE 早于后台采集就绪时暂挂,采集启动后挂到实时 emitters非 DB。 */
private final ConcurrentHashMap<String, CopyOnWriteArrayList<SseEmitter>> pendingFrontendEmitters =
new ConcurrentHashMap<>();
private final static ObjectMapper objectMapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
@Resource
private AiAgentClient aiAgentClient;
@Resource
private ICustomPageConversationDomainService customPageConversationDomainService;
@Resource
private AgentProgressSessionCoordinator sessionCoordinator;
@Resource
@Qualifier("aiAgentProgressScheduler")
private ScheduledExecutorService aiAgentProgressScheduler;
/**
* 前端连接时启动或挂接Agent SSE 采集;断开前端连接不结束 Agent 订阅。
*
* @param fluxRequestId 与 ai-chat-flux 一致的 request_id可空则从 DB 最新 USER 推断
*/
public SseEmitter attachFrontendSse(String sessionId, Long projectId, UserContext userContext,
String fluxRequestId) {
if (StringUtils.isBlank(sessionId) || projectId == null || projectId <= 0) {
throw new IllegalArgumentException("sessionId and projectId are required");
}
String resolvedRequestId = resolveFluxRequestId(projectId, sessionId, fluxRequestId, userContext);
CaptureContext active = findLatestActiveCaptureByAgentSession(projectId, sessionId);
if (active == null && StringUtils.isNotBlank(resolvedRequestId)) {
active = getOrStartCapture(sessionId, projectId, userContext, resolvedRequestId);
}
if (active != null) {
return attachEmitterToCapture(sessionId, active);
}
if (StringUtils.isNotBlank(resolvedRequestId)) {
log.warn(
"[Agent Progress] cannot start capture (lock held elsewhere?), waiting, project Id={}, session Id={}, request Id={}",
projectId, sessionId, resolvedRequestId);
} else {
log.warn("[Agent Progress] missing request Id, cannot start capture, project Id={}, session Id={}",
projectId, sessionId);
}
return attachWaitingForLiveCapture(sessionId, projectId);
}
private String resolveFluxRequestId(Long projectId, String sessionId, String fluxRequestId,
UserContext userContext) {
if (StringUtils.isNotBlank(fluxRequestId)) {
return fluxRequestId;
}
return AgentProgressContextUtil.callWithUserContext(userContext,
() -> sessionCoordinator.resolvePendingTurnRequestId(projectId, sessionId));
}
private SseEmitter attachEmitterToCapture(String sessionId, CaptureContext active) {
SseEmitter emitter = new SseEmitter(SSE_TIMEOUT_MS);
active.emitters.add(emitter);
replayBufferedEvents(active, emitter);
registerEmitterCallbacks(sessionId, emitter, active);
return emitter;
}
private SseEmitter attachWaitingForLiveCapture(String sessionId, Long projectId) {
SseEmitter emitter = new SseEmitter(SSE_TIMEOUT_MS);
String waitKey = buildSessionWaitKey(projectId, sessionId);
pendingFrontendEmitters.computeIfAbsent(waitKey, key -> new CopyOnWriteArrayList<>()).add(emitter);
log.info("[Agent Progress] frontend SSE waiting for live capture, project Id={}, session Id={}", projectId,
sessionId);
AtomicBoolean attached = new AtomicBoolean(false);
AtomicReference<ScheduledFuture<?>> pollRef = new AtomicReference<>();
Runnable detachPending = () -> pendingFrontendEmitters.computeIfPresent(waitKey, (key, list) -> {
list.remove(emitter);
return list.isEmpty() ? null : list;
});
Runnable tryAttachToCapture = () -> {
if (attached.get()) {
return;
}
CaptureContext context = findLatestActiveCaptureByAgentSession(projectId, sessionId);
if (context == null) {
return;
}
detachPending.run();
context.emitters.add(emitter);
replayBufferedEvents(context, emitter);
registerEmitterCallbacks(sessionId, emitter, context);
attached.set(true);
cancelPollTask(pollRef.get());
log.info("[Agent Progress] frontend SSE attached to live capture, project Id={}, session Id={}, request Id={}",
projectId, sessionId, context.fluxRequestId);
};
tryAttachToCapture.run();
if (!attached.get()) {
pollRef.set(aiAgentProgressScheduler.scheduleAtFixedRate(tryAttachToCapture, FRONTEND_ATTACH_POLL_MS,
FRONTEND_ATTACH_POLL_MS, TimeUnit.MILLISECONDS));
}
Runnable cleanup = () -> {
attached.set(true);
cancelPollTask(pollRef.get());
detachPending.run();
};
emitter.onCompletion(cleanup);
emitter.onTimeout(cleanup);
emitter.onError(ex -> cleanup.run());
return emitter;
}
private void attachPendingFrontendEmitters(CaptureContext context) {
String waitKey = buildSessionWaitKey(context.projectId, context.sessionId);
CopyOnWriteArrayList<SseEmitter> pending = pendingFrontendEmitters.remove(waitKey);
if (pending == null || pending.isEmpty()) {
return;
}
for (SseEmitter emitter : pending) {
context.emitters.add(emitter);
replayBufferedEvents(context, emitter);
registerEmitterCallbacks(context.sessionId, emitter, context);
}
log.info("[Agent Progress] attached {} pending frontend SSE to live capture, session Id={}, request Id={}",
pending.size(), context.sessionId, context.fluxRequestId);
}
private static String buildSessionWaitKey(Long projectId, String sessionId) {
return projectId + ":" + sessionId;
}
private CaptureContext findLatestActiveCaptureByAgentSession(Long projectId, String agentSessionId) {
CaptureContext latest = null;
for (CaptureContext context : activeCaptures.values()) {
if (!projectId.equals(context.projectId) || !agentSessionId.equals(context.sessionId)) {
continue;
}
if (latest == null || context.startedAtMs > latest.startedAtMs) {
latest = context;
}
}
return latest;
}
private CaptureContext findCaptureForEvent(Long projectId, String sessionId, String eventRequestId) {
if (StringUtils.isNotBlank(eventRequestId)) {
CaptureContext byRequest = activeCaptures.get(buildCaptureKey(projectId, eventRequestId));
if (byRequest != null) {
return byRequest;
}
}
return findLatestActiveCaptureByAgentSession(projectId, sessionId);
}
private CaptureContext getOrStartCapture(String sessionId, Long projectId, UserContext userContext,
String fluxRequestId) {
String captureKey = buildCaptureKey(projectId, fluxRequestId);
CaptureContext existing = activeCaptures.get(captureKey);
if (existing != null) {
return existing;
}
Object lock = sessionStartLocks.computeIfAbsent(captureKey, key -> new Object());
synchronized (lock) {
try {
existing = activeCaptures.get(captureKey);
if (existing != null) {
return existing;
}
if (!sessionCoordinator.tryAcquireCaptureLock(projectId, fluxRequestId)) {
log.info("[Agent Progress] capture lock held by another instance, project Id={}, request Id={}",
projectId, fluxRequestId);
return null;
}
sessionCoordinator.clearTurnDone(projectId, fluxRequestId);
CaptureContext context = new CaptureContext(projectId, sessionId, userContext, fluxRequestId);
activeCaptures.put(captureKey, context);
attachPendingFrontendEmitters(context);
startAgentSubscription(sessionId, projectId, userContext, fluxRequestId);
log.info("[Agent Progress] started server-side capture, project Id={}, session Id={}, request Id={}",
projectId, sessionId, fluxRequestId);
return context;
} catch (RuntimeException e) {
activeCaptures.remove(captureKey);
sessionCoordinator.releaseCaptureLock(projectId, fluxRequestId);
throw e;
} catch (Exception e) {
activeCaptures.remove(captureKey);
sessionCoordinator.releaseCaptureLock(projectId, fluxRequestId);
throw new RuntimeException(e);
}
}
}
private void startAgentSubscription(String sessionId, Long projectId, UserContext userContext,
String fluxRequestId) {
aiAgentClient.subscribeSessionSse(sessionId, null, projectId, userContext,
(eventName, data) -> onAgentEvent(sessionId, projectId, eventName, data),
() -> onAgentStreamFinished(projectId, fluxRequestId),
errorMessage -> onSubscribeFailed(projectId, fluxRequestId, errorMessage));
}
private void onAgentEvent(String sessionId, Long projectId, String eventName, String data) {
String eventRequestId = extractRequestId(data);
CaptureContext context = findCaptureForEvent(projectId, sessionId, eventRequestId);
if (context == null) {
return;
}
if (!AgentProgressEventUtil.shouldPersist(eventName)) {
broadcastToEmitters(context, eventName, data);
return;
}
Map<String, Object> eventRecord = new HashMap<>();
eventRecord.put("event", eventName);
eventRecord.put("data", AgentProgressEventUtil.parseRawData(data));
context.events.add(eventRecord);
if (StringUtils.isNotBlank(eventRequestId)) {
context.requestIdRef.set(eventRequestId);
}
broadcastToEmitters(context, eventName, data);
maybePersistIncremental(context);
if (AgentProgressEventUtil.isFinalEvent(eventName, data)) {
persistAssistantConversation(context, true);
}
}
private void maybePersistIncremental(CaptureContext context) {
int eventCount = context.events.size();
long now = System.currentTimeMillis();
boolean byCount = eventCount > 0 && eventCount % PERSIST_EVERY_N_EVENTS == 0;
boolean byInterval = now - context.lastPersistAtMs.get() >= PERSIST_INTERVAL_MS;
if (!byCount && !byInterval) {
return;
}
context.lastPersistAtMs.set(now);
persistAssistantConversation(context, false);
}
private void onAgentStreamFinished(Long projectId, String fluxRequestId) {
CaptureContext context = activeCaptures.get(buildCaptureKey(projectId, fluxRequestId));
if (context == null) {
sessionCoordinator.releaseCaptureLock(projectId, fluxRequestId);
return;
}
log.info("[Agent Progress] AI Agent SSE stream finished, project Id={}, session Id={}, request Id={}, eventCount={}",
context.projectId, context.sessionId, context.fluxRequestId, context.events.size());
finishCapture(context);
}
private void finishCapture(CaptureContext context) {
String captureKey = buildCaptureKey(context.projectId, context.fluxRequestId);
if (activeCaptures.remove(captureKey) == null) {
return;
}
sessionStartLocks.remove(captureKey);
persistAssistantConversation(context, false);
if (isContextTerminal(context)) {
sessionCoordinator.markTurnDone(context.projectId, context.fluxRequestId);
}
sessionCoordinator.releaseCaptureLock(context.projectId, context.fluxRequestId);
completeEmitters(context);
}
private void onSubscribeFailed(Long projectId, String fluxRequestId, String errorMessage) {
CaptureContext context = activeCaptures.get(buildCaptureKey(projectId, fluxRequestId));
if (context == null) {
sessionCoordinator.releaseCaptureLock(projectId, fluxRequestId);
return;
}
log.warn("[Agent Progress] subscribe failed, project Id={}, session Id={}, request Id={}, error={}",
context.projectId, context.sessionId, fluxRequestId, errorMessage);
broadcastErrorToEmitters(context, errorMessage);
finishCapture(context);
}
private boolean isContextTerminal(CaptureContext context) {
synchronized (context.events) {
for (Map<String, Object> record : context.events) {
Object eventName = record.get("event");
Object data = record.get("data");
String dataStr = data instanceof String ? (String) data : JSON.toJSONString(data);
if (AgentProgressEventUtil.isFinalEvent(
eventName == null ? null : String.valueOf(eventName), dataStr)) {
return true;
}
}
}
return false;
}
private void cancelPollTask(ScheduledFuture<?> pollTask) {
if (pollTask != null) {
pollTask.cancel(false);
}
}
/** 每轮用户提问对应唯一 requestId采集实例按 projectId+requestId 隔离。 */
private static String buildCaptureKey(Long projectId, String fluxRequestId) {
return projectId + ":" + fluxRequestId;
}
/** 前端断开仅移除 emitter不结束 Agent 订阅与采集。 */
private void registerEmitterCallbacks(String sessionId, SseEmitter emitter, CaptureContext context) {
emitter.onCompletion(() -> {
context.emitters.remove(emitter);
log.info("[Agent Progress] frontend SSE disconnected, agent capture continues, session Id={}, request Id={}",
sessionId, context.fluxRequestId);
});
emitter.onTimeout(() -> {
context.emitters.remove(emitter);
log.warn("[Agent Progress] frontend SSE timeout, agent capture continues, session Id={}, request Id={}",
sessionId, context.fluxRequestId);
});
emitter.onError(throwable -> {
context.emitters.remove(emitter);
log.warn("[Agent Progress] frontend SSE error, agent capture continues, session Id={}, request Id={}",
sessionId, context.fluxRequestId, throwable);
});
}
/** 本轮已从 Agent SSE 收到、尚未推给该前端连接的事件(内存缓冲,非 DB。 */
private void replayBufferedEvents(CaptureContext context, SseEmitter emitter) {
List<Map<String, Object>> snapshot;
synchronized (context.events) {
snapshot = new ArrayList<>(context.events);
}
for (Map<String, Object> record : snapshot) {
try {
Object eventName = record.get("event");
Object dataObj = record.get("data");
String data = dataObj instanceof String ? (String) dataObj : JSON.toJSONString(dataObj);
SseEmitter.SseEventBuilder builder = SseEmitter.event();
if (eventName != null) {
builder.name(String.valueOf(eventName));
}
builder.data(data);
emitter.send(builder);
} catch (Exception e) {
log.debug("[Agent Progress] replay event to frontend failed", e);
break;
}
}
}
private void broadcastToEmitters(CaptureContext context, String eventName, String data) {
if (context.emitters.isEmpty()) {
return;
}
for (SseEmitter emitter : context.emitters) {
try {
SseEmitter.SseEventBuilder builder = SseEmitter.event();
if (eventName != null) {
builder.name(eventName);
}
builder.data(data);
emitter.send(builder);
} catch (Exception e) {
log.debug("[Agent Progress] forward event to frontend failed", e);
}
}
}
private void broadcastErrorToEmitters(CaptureContext context, String errorMessage) {
Map<String, Object> errorBody = new HashMap<>();
errorBody.put("type", "error");
errorBody.put("code", "9999");
errorBody.put("message", errorMessage);
String data = JSON.toJSONString(errorBody);
broadcastToEmitters(context, "error", data);
}
private void completeEmitters(CaptureContext context) {
for (SseEmitter emitter : context.emitters) {
try {
emitter.complete();
} catch (Exception ignore) {
}
}
context.emitters.clear();
}
private void persistAssistantConversation(CaptureContext context, boolean markDoneIfTerminal) {
synchronized (context.persistLock) {
List<Map<String, Object>> snapshot;
synchronized (context.events) {
if (context.events.isEmpty()) {
return;
}
snapshot = new ArrayList<>(context.events);
}
try {
AgentProgressContextUtil.runWithUserContext(context.userContext, () -> {
CustomPageConversationModel model = new CustomPageConversationModel();
model.setProjectId(context.projectId);
model.setTopic("Assistant");
model.setContent(JSON.toJSONString(Map.of("events", snapshot)));
model.setRole("ASSISTANT");
model.setSessionId(context.sessionId);
String requestId = StringUtils.isNotBlank(context.fluxRequestId) ? context.fluxRequestId
: context.requestIdRef.get();
model.setRequestId(requestId);
Long assistantRecordId = context.assistantRecordIdRef.get();
if (assistantRecordId != null) {
model.setId(assistantRecordId);
}
ReqResult<Long> result = customPageConversationDomainService.saveOrUpdateAssistantConversation(
model, context.userContext);
if (result != null && result.isSuccess() && result.getData() != null) {
context.assistantRecordIdRef.compareAndSet(null, result.getData());
}
if (result == null || !result.isSuccess()) {
String msg = result == null ? "unknown" : result.getMessage();
log.error("[Agent Progress] persist assistant failed, project Id={}, request Id={}, error={}",
context.projectId, context.fluxRequestId, msg);
return;
}
if (markDoneIfTerminal && isEventsTerminal(snapshot)) {
sessionCoordinator.markTurnDone(context.projectId, context.fluxRequestId);
}
});
} catch (Exception e) {
log.error("[Agent Progress] persist assistant conversation failed, project Id={}, request Id={}",
context.projectId, context.fluxRequestId, e);
}
}
}
private boolean isEventsTerminal(List<Map<String, Object>> events) {
for (Map<String, Object> record : events) {
Object eventName = record.get("event");
Object data = record.get("data");
String dataStr = data instanceof String ? (String) data : JSON.toJSONString(data);
if (AgentProgressEventUtil.isFinalEvent(eventName == null ? null : String.valueOf(eventName), dataStr)) {
return true;
}
}
return false;
}
private String extractRequestId(String data) {
if (!JSON.isValidObject(data)) {
return null;
}
try {
Map<String, Object> payload = objectMapper.readValue(data, new TypeReference<Map<String, Object>>() {
});
Object requestId = payload.get("request_id");
if (requestId != null && StringUtils.isNotBlank(String.valueOf(requestId))) {
return String.valueOf(requestId);
}
Object dataObj = payload.get("data");
if (dataObj instanceof Map<?, ?> dataMap) {
Object requestIdInData = dataMap.get("request_id");
if (requestIdInData != null && StringUtils.isNotBlank(String.valueOf(requestIdInData))) {
return String.valueOf(requestIdInData);
}
}
return null;
} catch (Exception e) {
return null;
}
}
private static final class CaptureContext {
private final Long projectId;
private final String sessionId;
private final String fluxRequestId;
private final UserContext userContext;
private final long startedAtMs = System.currentTimeMillis();
private final Object persistLock = new Object();
private final List<Map<String, Object>> events = Collections.synchronizedList(new ArrayList<>());
private final CopyOnWriteArrayList<SseEmitter> emitters = new CopyOnWriteArrayList<>();
private final AtomicReference<String> requestIdRef = new AtomicReference<>();
private final AtomicReference<Long> assistantRecordIdRef = new AtomicReference<>();
private final AtomicLong lastPersistAtMs = new AtomicLong(0);
private CaptureContext(Long projectId, String sessionId, UserContext userContext, String fluxRequestId) {
this.projectId = projectId;
this.sessionId = sessionId;
this.userContext = userContext;
this.fluxRequestId = fluxRequestId;
}
}
}

View File

@@ -0,0 +1,98 @@
package com.xspaceagi.custompage.domain.service;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import com.xspaceagi.system.spec.utils.RedisUtil;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.FluxSink;
/**
* 聊天会话管理器,管理SSE会话的生命周期
*/
@Slf4j
@Component
public class CustomPageChatSessionManager {
private static final String SESSION_STOP_KEY_PREFIX = "custom.page.chat.stop.";
private static final int SESSION_STOP_TTL_SECONDS = 60;
@Resource
private RedisUtil redisUtil;
/**
* 存储会话ID和FluxSink的映射关系
*/
private final ConcurrentHashMap<String, FluxSink<Map<String, Object>>> sessionMap = new ConcurrentHashMap<>();
/**
* 注册会话
*/
public void registerSession(String sessionId, FluxSink<Map<String, Object>> sink) {
sessionMap.put(sessionId, sink);
clearSessionStopFlag(sessionId);
log.info("[Session Manager] register session: session Id={}", sessionId);
}
/**
* 终止会话
*/
public boolean terminateSession(String sessionId) {
markSessionStopRequested(sessionId);
FluxSink<Map<String, Object>> sink = sessionMap.remove(sessionId);
if (sink != null) {
log.info("[Session Manager] terminatesession: session Id={}", sessionId);
try {
sink.complete();
} catch (Exception e) {
log.debug("[Session Manager] sink already completed, ignore duplicate call", e);
}
} else {
log.info("[Session Manager] session not found or already completed: session Id={}", sessionId);
}
// 会话不存在时也返回成功,实现幂等性
return true;
}
public void markSessionStopRequested(String sessionId) {
redisUtil.set(buildSessionStopKey(sessionId), String.valueOf(System.currentTimeMillis()), SESSION_STOP_TTL_SECONDS);
}
public boolean isSessionStopRequested(String sessionId) {
return redisUtil.get(buildSessionStopKey(sessionId)) != null;
}
public void clearSessionStopFlag(String sessionId) {
redisUtil.expire(buildSessionStopKey(sessionId), 0);
}
/**
* 获取会话
*/
public FluxSink<Map<String, Object>> getSession(String sessionId) {
return sessionMap.get(sessionId);
}
/**
* 移除会话
*/
public void removeSession(String sessionId) {
sessionMap.remove(sessionId);
clearSessionStopFlag(sessionId);
log.info("[Session Manager] remove session: session Id={}", sessionId);
}
/**
* 获取当前活跃会话数量
*/
public int getActiveSessionCount() {
return sessionMap.size();
}
private String buildSessionStopKey(String sessionId) {
return SESSION_STOP_KEY_PREFIX + sessionId;
}
}

View File

@@ -0,0 +1,105 @@
package com.xspaceagi.custompage.domain.service;
import java.util.List;
import java.util.Map;
import org.springframework.web.multipart.MultipartFile;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.xspaceagi.custompage.domain.model.CustomPageBuildModel;
import com.xspaceagi.custompage.sdk.dto.TemplateTypeEnum;
import com.xspaceagi.system.spec.common.UserContext;
import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.spec.page.SuperPage;
public interface ICustomPageBuildDomainService {
/**
* 根据projectId查询构建信息
*/
CustomPageBuildModel getByProjectId(Long projectId);
/**
* 创建前端页面项目
*/
ReqResult<CustomPageBuildModel> createProject(Long projectId, Long spaceId,
UserContext userContext)
throws JsonProcessingException;
/**
* 初始化项目工程
*/
ReqResult<Map<String, Object>> initProject(Long projectId, TemplateTypeEnum templateType);
/**
* 上传项目
*/
ReqResult<Map<String, Object>> uploadProject(Long projectId, MultipartFile file, boolean isInitProject,
UserContext userContext);
/**
* 启动开发服务器
*/
ReqResult<Map<String, Object>> startDev(Long projectId, UserContext userContext);
/**
* 构建并发布前端项目
*/
ReqResult<Map<String, Object>> build(Long projectId, String publishType, UserContext userContext);
/**
* 停止前端开发服务器
*/
ReqResult<Map<String, Object>> stopDev(Long projectId, UserContext userContext);
/**
* 重启前端开发服务器
*/
ReqResult<Map<String, Object>> restartDev(Long projectId, UserContext userContext);
/**
* 分页查询前端页面项目
*/
SuperPage<CustomPageBuildModel> pageQuery(CustomPageBuildModel model, Long current,
Long pageSize);
/**
* 查询前端页面项目列表
*/
List<CustomPageBuildModel> list(CustomPageBuildModel model);
/**
* 根据项目ID列表查询构建信息列表
*/
List<CustomPageBuildModel> listByProjectIds(List<Long> projectIdList);
/**
* 保活处理
*/
ReqResult<Map<String, Object>> keepAlive(Long projectId, UserContext userContext);
/**
* 删除项目文件
*/
ReqResult<Map<String, Object>> deleteProjectFiles(CustomPageBuildModel model, UserContext userContext);
/**
* 查询开发服务器日志
*/
ReqResult<Map<String, Object>> getDevLog(Long projectId, Integer startIndex, String logType, UserContext userContext);
/**
* 复制项目工程
*/
ReqResult<Map<String, Object>> copyProject(Long sourceProjectId, Long targetProjectId);
/**
* 获取日志缓存统计
*/
ReqResult<Map<String, Object>> getLogCacheStats();
/**
* 清理所有日志缓存
*/
ReqResult<Map<String, Object>> clearAllLogCache();
}

View File

@@ -0,0 +1,34 @@
package com.xspaceagi.custompage.domain.service;
import java.util.Map;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import com.xspaceagi.system.spec.common.UserContext;
import com.xspaceagi.system.spec.dto.ReqResult;
/**
* 前端项目聊天领域服务
*/
public interface ICustomPageChatDomainService {
/**
* 建立会话 SSE 连接
*/
SseEmitter startAgentSessionSse(String sessionId, Long projectId, String requestId, UserContext userContext);
/**
* 取消 agent 任务
*/
ReqResult<Map<String, Object>> agentSessionCancel(String projectId, String sessionId, UserContext userContext);
/**
* 查询Agent状态
*/
ReqResult<Map<String, Object>> getAgentStatus(String projectId, UserContext userContext);
/**
* 停止Agent服务
*/
ReqResult<Map<String, Object>> stopAgent(String projectId, UserContext userContext);
}

View File

@@ -0,0 +1,25 @@
package com.xspaceagi.custompage.domain.service;
import java.util.Map;
import com.xspaceagi.system.spec.common.UserContext;
import reactor.core.publisher.Flux;
/**
* 前端项目聊天响应式流服务
*/
public interface ICustomPageChatFluxService {
/**
* 发送聊天消息(使用 Flux 响应式流)
*/
Flux<Map<String, Object>> sendAgentChatFlux(Map<String, Object> chatBody,
UserContext userContext);
/**
* 终止会话
*/
boolean terminateSession(String sessionId);
}

View File

@@ -0,0 +1,38 @@
package com.xspaceagi.custompage.domain.service;
import java.util.List;
import java.util.Map;
import org.springframework.web.multipart.MultipartFile;
import com.xspaceagi.custompage.domain.dto.PageFileInfo;
import com.xspaceagi.system.spec.common.UserContext;
import com.xspaceagi.system.spec.dto.ReqResult;
/**
* 前端项目编码领域服务
*/
public interface ICustomPageCodingDomainService {
/**
* 指定文件修改
*/
ReqResult<Map<String, Object>> specifiedFilesUpdate(Long projectId, List<PageFileInfo> files, UserContext userContext);
/**
* 全量文件修改
*/
ReqResult<Map<String, Object>> allFilesUpdate(Long projectId, List<PageFileInfo> files, UserContext userContext);
/**
* 上传单个文件
*/
ReqResult<Map<String, Object>> uploadSingleFile(Long projectId, MultipartFile file, String filePath,
UserContext userContext);
/**
* 回滚版本
*/
ReqResult<Map<String, Object>> rollbackVersion(Long projectId, Integer rollbackTo, UserContext userContext);
}

View File

@@ -0,0 +1,141 @@
package com.xspaceagi.custompage.domain.service;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
import com.xspaceagi.custompage.domain.model.CustomPageConfigModel;
import com.xspaceagi.custompage.sdk.dto.DataSourceDto;
import com.xspaceagi.custompage.sdk.dto.ExportTypeEnum;
import com.xspaceagi.custompage.sdk.dto.PageArgConfig;
import com.xspaceagi.custompage.sdk.dto.ProxyConfig;
import com.xspaceagi.system.spec.common.UserContext;
import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.spec.page.SuperPage;
/**
* 自定义页面配置领域服务接口
*/
public interface ICustomPageConfigDomainService {
/**
* 创建自定义页面配置
*/
ReqResult<CustomPageConfigModel> create(CustomPageConfigModel model, UserContext userContext);
/**
* 查询配置列表
*/
List<CustomPageConfigModel> list(CustomPageConfigModel model);
/**
* 分页查询配置
*/
SuperPage<CustomPageConfigModel> pageQuery(CustomPageConfigModel configModel, Long current, Long pageSize);
/**
* 根据ID查询配置
*/
CustomPageConfigModel getById(Long id);
/**
* 根据agentId查询配置
*/
CustomPageConfigModel getByAgentId(Long agentId);
/**
* 根据项目ID列表查询配置
*/
List<CustomPageConfigModel> listByIds(List<Long> ids);
/**
* 更新
*/
ReqResult<CustomPageConfigModel> update(CustomPageConfigModel model, UserContext userContext);
/**
* 添加反向代理配置
*/
ReqResult<List<ProxyConfig>> addProxy(Long projectId, ProxyConfig proxyConfig, UserContext userContext);
/**
* 编辑反向代理配置
*/
ReqResult<Void> editProxy(Long projectId, ProxyConfig proxyConfig, UserContext userContext);
/**
* 删除反向代理配置
*/
ReqResult<Void> deleteProxy(Long projectId, String env, String path, UserContext userContext);
/**
* 配置路径参数
*/
ReqResult<Void> savePathArgs(Long projectId, PageArgConfig pageArgConfig, UserContext userContext);
/**
* 添加路径配置
*/
ReqResult<Void> addPath(Long projectId, PageArgConfig pageArgConfig, UserContext userContext);
/**
* 编辑路径配置
*/
ReqResult<Void> editPath(Long projectId, PageArgConfig pageArgConfig, UserContext userContext);
/**
* 删除路径配置
*/
ReqResult<Void> deletePath(Long projectId, String pageUri, UserContext userContext);
/**
* 批量配置反向代理
*/
ReqResult<Void> batchConfigProxy(Long projectId, List<ProxyConfig> proxyConfigs, UserContext userContext);
/**
* 导出项目
*/
ReqResult<InputStream> exportProject(Long projectId, ExportTypeEnum exportType, UserContext userContext);
/**
* 查询项目文件内容
*/
ReqResult<Map<String, Object>> queryProjectContent(Long projectId, String command, String proxyPath);
/**
* 查询项目历史版本文件内容
*/
ReqResult<Map<String, Object>> queryProjectContentByVersion(Long projectId, Integer codeVersion, String proxyPath);
/**
* 绑定数据源
*/
ReqResult<Void> bindDataSource(Long projectId, DataSourceDto dataSource, UserContext userContext);
/**
* 解绑数据源
*/
ReqResult<Void> unbindDataSource(Long projectId, DataSourceDto dataSource, UserContext userContext);
/**
* 删除项目
*/
ReqResult<Map<String, Object>> delete(Long projectId, UserContext userContext);
/**
* 导入项目配置
*/
void importProjectConfig(CustomPageConfigModel model, UserContext userContext);
/**
* 根据devAgentId列表查询配置
*/
List<CustomPageConfigModel> listByDevAgentIds(List<Long> devAgentIds);
/**
* 统计网页应用总数
*/
Long countTotalPages();
}

View File

@@ -0,0 +1,45 @@
package com.xspaceagi.custompage.domain.service;
import java.util.List;
import com.xspaceagi.custompage.domain.model.CustomPageConversationModel;
import com.xspaceagi.system.spec.common.UserContext;
import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.spec.page.SuperPage;
/**
* 用户页面会话记录领域服务接口
*/
public interface ICustomPageConversationDomainService {
/**
* 保存用户会话记录
*/
ReqResult<Long> saveConversation(CustomPageConversationModel model, UserContext userContext);
/**
* 保存或更新助手会话记录(有 id 更新本轮;无 id 插入新行,每轮问答独立一条)
*/
ReqResult<Long> saveOrUpdateAssistantConversation(CustomPageConversationModel model, UserContext userContext);
/**
* 根据项目ID查询会话记录列表
*/
List<CustomPageConversationModel> listByProjectId(Long projectId, Long userId);
/**
* 分页查询会话记录
*/
ReqResult<SuperPage<CustomPageConversationModel>> pageQuery(CustomPageConversationModel queryModel, Long current,
Long pageSize, UserContext userContext);
/**
* 根据requestId回填用户消息的sessionId
*/
ReqResult<Void> updateUserSessionIdByRequestId(Long projectId, String requestId, String sessionId, UserContext userContext);
/**
* 删除项目下的会话记录
*/
ReqResult<Void> deleteByProjectId(Long projectId, UserContext userContext);
}

View File

@@ -0,0 +1,40 @@
package com.xspaceagi.custompage.domain.service;
import com.xspaceagi.custompage.domain.model.CustomPageDomainModel;
import com.xspaceagi.system.spec.common.UserContext;
import com.xspaceagi.system.spec.dto.ReqResult;
import java.util.List;
/**
* 自定义页面域名绑定领域服务接口
*/
public interface ICustomPageDomainDomainService {
/**
* 根据project_id查询域名列表
*/
List<CustomPageDomainModel> listByProjectId(Long projectId);
/**
* 根据ID查询域名绑定
*/
CustomPageDomainModel getById(Long id);
/**
* 创建域名绑定
*/
ReqResult<CustomPageDomainModel> create(CustomPageDomainModel model, UserContext userContext);
/**
* 更新域名绑定
*/
ReqResult<CustomPageDomainModel> update(CustomPageDomainModel model, UserContext userContext);
/**
* 删除域名绑定
*/
ReqResult<Void> delete(Long id, UserContext userContext);
List<String> listAllDomains();
}

View File

@@ -0,0 +1,18 @@
package com.xspaceagi.custompage.domain.service;
import com.xspaceagi.system.spec.common.UserContext;
import org.springframework.core.io.buffer.DataBuffer;
import reactor.core.publisher.Flux;
/**
* 文件领域服务
*/
public interface ICustomPageFileDomainService {
/**
* 获取静态文件(流式返回)
*/
Flux<DataBuffer> getStaticFile(String targetPrefix, String relativePath, String logId, UserContext userContext);
}

View File

@@ -0,0 +1,458 @@
package com.xspaceagi.custompage.domain.service.impl;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.xspaceagi.custompage.domain.gateway.PageFileBuildClient;
import com.xspaceagi.custompage.domain.keepalive.IKeepAliveService;
import com.xspaceagi.custompage.domain.model.CustomPageBuildModel;
import com.xspaceagi.custompage.domain.proxypath.ICustomPageProxyPathService;
import com.xspaceagi.custompage.domain.repository.ICustomPageBuildRepository;
import com.xspaceagi.custompage.domain.repository.ICustomPageConfigRepository;
import com.xspaceagi.custompage.domain.service.ICustomPageBuildDomainService;
import com.xspaceagi.custompage.domain.service.ICustomPageConfigDomainService;
import com.xspaceagi.custompage.sdk.dto.PublishTypeEnum;
import com.xspaceagi.custompage.sdk.dto.TemplateTypeEnum;
import com.xspaceagi.custompage.sdk.dto.VersionInfoDto;
import com.xspaceagi.custompage.sdk.enums.CustomPageActionEnum;
import com.xspaceagi.system.sdk.permission.SpacePermissionService;
import com.xspaceagi.system.spec.common.UserContext;
import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
import com.xspaceagi.system.spec.enums.YesOrNoEnum;
import com.xspaceagi.system.spec.enums.YnEnum;
import com.xspaceagi.system.spec.page.SuperPage;
import com.xspaceagi.system.spec.utils.DateUtil;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
public class CustomPageBuildDomainServiceImpl implements ICustomPageBuildDomainService {
@Resource
private ICustomPageBuildRepository customPageBuildRepository;
@Resource
private PageFileBuildClient pageFileBuildClient;
@Resource
private IKeepAliveService keepAliveService;
@Resource
private ICustomPageConfigRepository customPageConfigRepository;
@Resource
private ICustomPageConfigDomainService customPageConfigDomainService;
@Resource
private SpacePermissionService spacePermissionService;
@Resource
private ICustomPageProxyPathService customPageProxyPathService;
@Override
public CustomPageBuildModel getByProjectId(Long projectId) {
return customPageBuildRepository.getByProjectId(projectId);
}
@Override
public ReqResult<CustomPageBuildModel> createProject(Long projectId, Long spaceId,
UserContext userContext) throws JsonProcessingException {
if (spaceId == null) {
throw new IllegalArgumentException("spaceId is required");
}
// 不校验空间权限,因为在创建config表的时候已经校验了,这个方法执行一定跟创建config表在同一个事务里
// spacePermissionService.checkSpaceUserPermission(spaceId);
CustomPageBuildModel exist = customPageBuildRepository.getByProjectId(projectId);
if (exist != null) {
return ReqResult.error("0001", "Project already exists");
}
List<VersionInfoDto> versionList = List.of(VersionInfoDto.builder()
.version(1)
.time(DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss"))
.action(CustomPageActionEnum.CREATE_PROJECT.getCode())
.build());
CustomPageBuildModel model = new CustomPageBuildModel();
model.setProjectId(projectId);
model.setDevRunning(YesOrNoEnum.N.getKey());
model.setBuildRunning(YesOrNoEnum.N.getKey());
model.setCodeVersion(1);
model.setVersionInfo(versionList);
model.setCreatorId(userContext.getUserId());
model.setCreatorName(userContext.getUserName());
model.setTenantId(userContext.getTenantId());
model.setSpaceId(spaceId);
model.setYn(YnEnum.Y.getKey());
Long id = customPageBuildRepository.add(model, userContext);
model.setId(id);
return ReqResult.success(model);
}
// 调用node创建项目
public ReqResult<Map<String, Object>> initProject(Long projectId, TemplateTypeEnum templateType) {
Map<String, Object> resp = pageFileBuildClient.createProject(projectId, templateType);
if (resp == null) {
return ReqResult.error("9999", "Create project failed: build server returned no response");
}
boolean success = Boolean.parseBoolean(String.valueOf(resp.get("success")));
String message = resp.get("message") == null ? "" : String.valueOf(resp.get("message"));
if (!success) {
return ReqResult.error("9999", message);
}
return ReqResult.success(resp);
}
@Override
public ReqResult<Map<String, Object>> uploadProject(Long projectId, MultipartFile file, boolean isInitProject,
UserContext userContext) {
log.info("[upload-project] project Id={},start execution", projectId);
CustomPageBuildModel buildModel = customPageBuildRepository.getByProjectId(projectId);
if (buildModel == null) {
return ReqResult.error("0001", "Project build information does not exist");
}
// 校验空间权限
spacePermissionService.checkSpaceUserPermission(buildModel.getSpaceId());
// 通过projectId查询devProxyPath
String devProxyPath = null;
try {
devProxyPath = customPageProxyPathService.getDevProxyPath(projectId);
} catch (Exception e) {
log.warn("[upload-project] project Id={},could not gettenantdefaultagent", projectId);
// return ReqResult.error("9999", "租户没有配置默认智能体");
}
Integer targetVersion = isInitProject ? 1 : (buildModel.getCodeVersion() + 1);
// 上传
log.info("[upload-project] project Id={},startcallserver", projectId);
Map<String, Object> resp = pageFileBuildClient.uploadProject(projectId, file, targetVersion,
buildModel.getDevPid(), devProxyPath);
if (resp == null) {
log.error("[upload-project] project Id={},upload projectfailed,server returned null", projectId);
return ReqResult.error("9999", "Upload project failed: build server returned no response");
}
boolean success = Boolean.parseBoolean(String.valueOf(resp.get("success")));
String message = resp.get("message") == null ? "" : String.valueOf(resp.get("message"));
if (!success) {
log.error("[upload-project] project Id={},upload projectfailed,server returned message={}", projectId, message);
return ReqResult.error("9999", message);
}
// 更新版本信息
List<VersionInfoDto> versionInfo = isInitProject ? new ArrayList<>() : buildModel.getVersionInfo();
versionInfo.add(VersionInfoDto.builder()
.version(targetVersion)
.time(DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss"))
.action(CustomPageActionEnum.UPLOAD.getCode())
.build());
try {
CustomPageBuildModel updateModel = new CustomPageBuildModel();
updateModel.setId(buildModel.getId());
updateModel.setCodeVersion(targetVersion);
updateModel.setVersionInfo(versionInfo);
// updateModel.setLastChatModelId(chatModelId);
// updateModel.setLastMultiModelId(multiModelId);
customPageBuildRepository.updateVersionInfo(updateModel, userContext);
log.info("[upload-project] project Id={},update version infosucceeded target Version={}", projectId, targetVersion);
} catch (Exception e) {
log.error("[upload-project] project Id={},update version infofailed, target Version={}", projectId, targetVersion, e);
// 不抛出异常,因为上传已经成功
}
Object pidObj = resp.get("pid");
Object portObj = resp.get("port");
if (pidObj == null || portObj == null) {
log.error("[upload-project] project Id={},devservice not started, message={}", projectId, resp.get("message"));
keepAliveService.updateKeepAlive(projectId, new Date(), YesOrNoEnum.N.getKey(), null, null, userContext);
log.warn("[upload-project] project Id={},devservice not started, update keep-alive infosucceeded,pid=null,port=null", projectId);
} else {
Integer pid = Integer.valueOf(String.valueOf(pidObj));
Integer port = Integer.valueOf(String.valueOf(portObj));
keepAliveService.updateKeepAlive(projectId, new Date(), YesOrNoEnum.Y.getKey(), pid, port, userContext);
log.info("[upload-project] project Id={},devservice started, update keep-alive infosucceeded,pid={},port={}", projectId, pid, port);
}
log.info("[upload-project] project Id={},upload projectsucceeded,result={}", projectId, resp);
return ReqResult.success(resp);
}
@Override
public ReqResult<Map<String, Object>> keepAlive(Long projectId, UserContext userContext) {
return keepAliveService.handleKeepAlive(projectId, userContext);
}
@Override
public ReqResult<Map<String, Object>> startDev(Long projectId, UserContext userContext) {
log.info("[start Dev] project Id={},start domain execution", projectId);
ReqResult<Map<String, Object>> result = keepAliveService.handleKeepAlive(projectId, userContext);
log.error("[start Dev] project Id={},response,result={}", projectId, result);
return result;
}
@Override
public ReqResult<Map<String, Object>> build(Long projectId, String publishType, UserContext userContext) {
log.info("[build] project Id={},start domain execution", projectId);
CustomPageBuildModel model = customPageBuildRepository.getByProjectId(projectId);
if (model == null) {
return ReqResult.error("0001", "Project does not exist");
}
PublishTypeEnum publishTypeEnum = PublishTypeEnum.getByValue(publishType);
if (publishTypeEnum == null) {
return ReqResult.error("0001", "Publish type does not exist");
}
// 校验空间权限
spacePermissionService.checkSpaceUserPermission(model.getSpaceId());
log.error("[build] project Id={},startcallserver", projectId);
String prodProxyPath = customPageProxyPathService.getProdProxyPath(projectId);
Map<String, Object> resp = pageFileBuildClient.build(projectId, prodProxyPath);
if (resp == null) {
log.error("[build] project Id={},buildfailed,server returned null", projectId);
return ReqResult.error("9999", "Build failed: build server returned no response");
}
boolean success = Boolean.parseBoolean(String.valueOf(resp.get("success")));
String message = resp.get("message") == null ? "" : String.valueOf(resp.get("message"));
if (!success) {
log.error("[build] project Id={},buildfailed,server returned message={}", projectId, message);
return ReqResult.error("9999", message);
}
log.error("[build] project Id={},buildsucceeded", projectId);
// 更新build表的构建状态
customPageBuildRepository.updateBuildStatus(projectId, model.getCodeVersion(), userContext);
log.error("[build] project Id={},buildtableupdatesucceeded", projectId);
// 更新config表的构建状态
customPageConfigRepository.updateBuildStatus(projectId, publishTypeEnum, userContext);
log.error("[build] project Id={},configtableupdatesucceeded", projectId);
return ReqResult.success(resp);
}
@Override
public ReqResult<Map<String, Object>> stopDev(Long projectId, UserContext userContext) {
log.info("[stop Dev] project Id={},start domain execution", projectId);
CustomPageBuildModel model = customPageBuildRepository.getByProjectId(projectId);
if (model == null) {
return ReqResult.error("0001", "Project does not exist");
}
// 校验空间权限
spacePermissionService.checkSpaceUserPermission(model.getSpaceId());
if (model.getDevPid() == null) {
log.info("[stop-dev] project Id={}, table queryprojectport null, handle", projectId);
return ReqResult.success(null);
}
if (model.getDevPid() <= 1) {
log.info("[stop-dev] project Id={}, table queryprojectpid {},invalid, handle", projectId, model.getDevPid());
return ReqResult.error("Invalid process ID (pid)");
}
Map<String, Object> resp = pageFileBuildClient.stopDev(projectId, model.getDevPid());
if (resp == null) {
log.error("[stop-dev] project Id={},stop failed,server returned null", projectId);
return ReqResult.error("9999", "Stop failed: build server returned no response");
}
boolean success = Boolean.parseBoolean(String.valueOf(resp.get("success")));
String message = resp.get("message") == null ? "" : String.valueOf(resp.get("message"));
if (!success) {
log.error("[stop-dev] project Id={},stop failed,server returned message={}", projectId, message);
return ReqResult.error("9999", message);
}
log.info("[stop-dev] project Id={},stop succeeded", projectId);
keepAliveService.updateKeepAlive(projectId, new Date(), YesOrNoEnum.N.getKey(), null, null, userContext);
log.info("[stop-dev] project Id={},keep-alive infoupdatesucceeded", projectId);
return ReqResult.success(resp);
}
@Override
public ReqResult<Map<String, Object>> restartDev(Long projectId, UserContext userContext) {
log.info("[restart Dev] project Id={},start domain execution", projectId);
CustomPageBuildModel model = customPageBuildRepository.getByProjectId(projectId);
if (model == null) {
return ReqResult.error("0001", "Project does not exist");
}
// 校验空间权限
spacePermissionService.checkSpaceUserPermission(model.getSpaceId());
log.info("[restart-dev] project Id={},startcallserver", projectId);
Integer pid = model.getDevPid();
String devProxyPath = customPageProxyPathService.getDevProxyPath(projectId);
Map<String, Object> resp = pageFileBuildClient.restartDev(projectId, pid, devProxyPath);
if (resp == null) {
log.error("[restart-dev] project Id={},restart failed,server returned null", projectId);
return ReqResult.error("9999", "Restart failed: build server returned no response");
}
boolean success = Boolean.parseBoolean(String.valueOf(resp.get("success")));
String message = resp.get("message") == null ? "" : String.valueOf(resp.get("message"));
if (!success) {
log.error("[restart-dev] project Id={},restart failed,server returned message={}", projectId, message);
String code = resp.get("code") == null ? "" : String.valueOf(resp.get("code"));
if ("PROJECT_STARTING".equals(code)) {
return ReqResult.error(ErrorCodeEnum.PROJECT_STARTING.getCode(), ErrorCodeEnum.PROJECT_STARTING.getMsg());
}
return ReqResult.error("9999", message);
}
// 重启成功后,更新 dev 运行信息(有则更新,无则插入)
Integer newPid = null;
Integer newPort = null;
try {
Object pidObj = resp.get("pid");
Object portObj = resp.get("port");
newPid = Integer.valueOf(String.valueOf(pidObj));
newPort = Integer.valueOf(String.valueOf(portObj));
} catch (Exception e) {
keepAliveService.updateKeepAlive(projectId, new Date(), YesOrNoEnum.N.getKey(), null, null, userContext);
log.error("[restart-dev] project Id={},get service port pidexception,update keep-alive info,pid=null,port=null", projectId);
return ReqResult.error("9999", "Failed to obtain dev server port and process ID");
}
log.info("[restart-dev] project Id={},restart succeeded", projectId);
keepAliveService.updateKeepAlive(projectId, new Date(), 1, newPid, newPort, userContext);
log.info("[restart-dev] project Id={},update keep-alive infosucceeded,pid={},port={}", projectId, newPid, newPort);
return ReqResult.success(resp);
}
@Override
public SuperPage<CustomPageBuildModel> pageQuery(CustomPageBuildModel model, Long current,
Long pageSize) {
return customPageBuildRepository.pageQuery(model, current, pageSize);
}
@Override
public List<CustomPageBuildModel> list(CustomPageBuildModel model) {
return customPageBuildRepository.list(model);
}
@Override
public List<CustomPageBuildModel> listByProjectIds(List<Long> projectIdList) {
return customPageBuildRepository.listByProjectIds(projectIdList);
}
@Override
public ReqResult<Map<String, Object>> deleteProjectFiles(CustomPageBuildModel model, UserContext userContext) {
Long projectId = model.getProjectId();
log.info("[delete-project-files] project Id={},start domain execution", projectId);
// 校验空间权限
spacePermissionService.checkSpaceUserPermission(model.getSpaceId());
log.info("[delete-project-files] project Id={},startcallserver", projectId);
Integer pid = model.getDevPid();
Map<String, Object> resp = pageFileBuildClient.deleteProject(projectId, pid);
if (resp == null) {
log.error("[delete-project-files] project Id={},deletefailed,server returned null", projectId);
return ReqResult.error("9999", "Delete failed: build server returned no response");
}
boolean success = Boolean.parseBoolean(String.valueOf(resp.get("success")));
String message = resp.get("message") == null ? "" : String.valueOf(resp.get("message"));
if (!success) {
log.error("[delete-project-files] project Id={},deletefailed,server returned message={}", projectId, message);
return ReqResult.error("9999", message);
}
log.info("[delete-project-files] project Id={},deletesucceeded", projectId);
return ReqResult.success(resp);
}
@Override
public ReqResult<Map<String, Object>> getDevLog(Long projectId, Integer startIndex, String logType, UserContext userContext) {
log.debug("[get Dev Log] project Id={}, start Index={}, start domain execution", projectId, startIndex);
CustomPageBuildModel model = customPageBuildRepository.getByProjectId(projectId);
if (model == null) {
return ReqResult.error("0001", "Project does not exist");
}
// 校验空间权限
spacePermissionService.checkSpaceUserPermission(model.getSpaceId());
log.debug("[get Dev Log] project Id={}, startcallserver", projectId);
Map<String, Object> resp = pageFileBuildClient.getDevLog(projectId, startIndex, logType);
if (resp == null) {
log.error("[get Dev Log] project Id={}, query logsfailed, server returned null", projectId);
return ReqResult.error("9999", "Query logs failed: build server returned no response");
}
boolean success = Boolean.parseBoolean(String.valueOf(resp.get("success")));
String message = resp.get("message") == null ? "" : String.valueOf(resp.get("message"));
if (!success) {
log.error("[get Dev Log] project Id={}, query logsfailed, server returned message={}", projectId, message);
return ReqResult.error("9999", message);
}
log.debug("[get Dev Log] project Id={}, query logssucceeded", projectId);
return ReqResult.success(resp);
}
@Override
public ReqResult<Map<String, Object>> copyProject(Long sourceProjectId, Long targetProjectId) {
Map<String, Object> resp = pageFileBuildClient.copyProject(sourceProjectId, targetProjectId);
if (resp == null) {
return ReqResult.error("9999", "Copy project failed: build server returned no response");
}
boolean success = Boolean.parseBoolean(String.valueOf(resp.get("success")));
String message = resp.get("message") == null ? "" : String.valueOf(resp.get("message"));
if (!success) {
return ReqResult.error("9999", message);
}
return ReqResult.success(resp);
}
@Override
public ReqResult<Map<String, Object>> getLogCacheStats() {
log.info("[Domain] getlogcachestats");
Map<String, Object> resp = pageFileBuildClient.getLogCacheStats();
if (resp == null) {
return ReqResult.error("9999", "Get log cache stats failed: build server returned no response");
}
boolean success = Boolean.parseBoolean(String.valueOf(resp.get("success")));
String message = resp.get("message") == null ? "" : String.valueOf(resp.get("message"));
if (!success) {
return ReqResult.error("9999", message);
}
log.info("[Domain] getlogcachestatssucceeded");
return ReqResult.success(resp);
}
@Override
public ReqResult<Map<String, Object>> clearAllLogCache() {
log.info("[Domain] clear all log cache");
Map<String, Object> resp = pageFileBuildClient.clearAllLogCache();
if (resp == null) {
return ReqResult.error("9999", "Clear log cache failed: build server returned no response");
}
boolean success = Boolean.parseBoolean(String.valueOf(resp.get("success")));
String message = resp.get("message") == null ? "" : String.valueOf(resp.get("message"));
if (!success) {
return ReqResult.error("9999", message);
}
log.info("[Domain] clear all log cachesucceeded");
return ReqResult.success(resp);
}
}

View File

@@ -0,0 +1,227 @@
package com.xspaceagi.custompage.domain.service.impl;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.xspaceagi.custompage.domain.gateway.AiAgentClient;
import com.xspaceagi.custompage.domain.service.CustomPageAgentProgressCaptureService;
import com.xspaceagi.custompage.domain.service.ICustomPageChatDomainService;
import com.xspaceagi.system.spec.common.UserContext;
import com.xspaceagi.system.spec.dto.ReqResult;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
public class CustomPageChatDomainServiceImpl implements ICustomPageChatDomainService {
@Resource
private AiAgentClient aiAgentClient;
@Resource
private CustomPageAgentProgressCaptureService agentProgressCaptureService;
private final static ObjectMapper objectMapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
@Override
public SseEmitter startAgentSessionSse(String sessionId, Long projectId, String requestId,
UserContext userContext) {
if (StringUtils.isBlank(sessionId)) {
throw new IllegalArgumentException("sessionId is required");
}
if (projectId == null || projectId <= 0) {
throw new IllegalArgumentException("projectId is required");
}
return agentProgressCaptureService.attachFrontendSse(sessionId, projectId, userContext, requestId);
}
@Override
public ReqResult<Map<String, Object>> agentSessionCancel(String projectId, String sessionId,
UserContext userContext) {
if (StringUtils.isBlank(projectId)) {
return ReqResult.error("0001", "projectId is required");
}
Long projectIdLong;
try {
projectIdLong = Long.valueOf(projectId);
} catch (Exception e) {
return ReqResult.error("0001", "Invalid projectId");
}
Map<String, Object> resp = aiAgentClient.sessionCancel(projectIdLong, sessionId, userContext);
if (resp == null) {
return ReqResult.error("9999", "Failed to cancel task: AI Agent returned no response");
}
Object code = resp.get("code");
if (code == null || !"0000".equals(String.valueOf(code))) {
String message = resp.get("message") == null ? "Failed to cancel task" : String.valueOf(resp.get("message"));
return ReqResult.error("9999", message);
}
// 提取data字段并转换为Map
Object data = resp.get("data");
Map<String, Object> dataMap = new HashMap<>();
if (data != null) {
try {
// 如果data已经是Map类型直接使用
if (data instanceof Map<?, ?>) {
@SuppressWarnings("unchecked")
Map<String, Object> existingMap = (Map<String, Object>) data;
dataMap = existingMap;
} else {
// 将JSON字符串转换为Map
String dataJson = data.toString();
dataMap = objectMapper.readValue(dataJson, new TypeReference<Map<String, Object>>() {
});
}
} catch (Exception e) {
log.warn("Failed to parse data JSON, using raw payload", e);
// 解析失败时将原始data包装在Map中
dataMap.put("data", data);
}
}
if (resp.get("tid") != null) {
dataMap.put("tid", resp.get("tid"));
}
if (resp.get("message") != null) {
dataMap.put("message", resp.get("message"));
}
if (resp.get("code") != null) {
dataMap.put("code", resp.get("code"));
}
return ReqResult.success(dataMap);
}
@Override
public ReqResult<Map<String, Object>> getAgentStatus(String projectId, UserContext userContext) {
if (StringUtils.isBlank(projectId)) {
return ReqResult.error("0001", "projectId is required");
}
Long projectIdLong;
try {
projectIdLong = Long.valueOf(projectId);
} catch (Exception e) {
return ReqResult.error("0001", "Invalid projectId");
}
Map<String, Object> resp = aiAgentClient.getAgentStatus(projectIdLong, userContext);
if (resp == null) {
return ReqResult.error("9999", "Failed to query Agent status: AI Agent returned no response");
}
Object code = resp.get("code");
if (code == null || !"0000".equals(String.valueOf(code))) {
String message = resp.get("message") == null ? "Failed to query Agent status" : String.valueOf(resp.get("message"));
return ReqResult.error("9999", message);
}
// 提取data字段并转换为Map
Object data = resp.get("data");
Map<String, Object> dataMap = new HashMap<>();
if (data != null) {
try {
// 如果data已经是Map类型直接使用
if (data instanceof Map<?, ?>) {
@SuppressWarnings("unchecked")
Map<String, Object> existingMap = (Map<String, Object>) data;
dataMap = existingMap;
} else {
// 将JSON字符串转换为Map
String dataJson = data.toString();
dataMap = objectMapper.readValue(dataJson, new TypeReference<Map<String, Object>>() {
});
}
} catch (Exception e) {
log.warn("Failed to parse data JSON, using raw payload", e);
// 解析失败时将原始data包装在Map中
dataMap.put("data", data);
}
}
if (resp.get("tid") != null) {
dataMap.put("tid", resp.get("tid"));
}
if (resp.get("message") != null) {
dataMap.put("message", resp.get("message"));
}
if (resp.get("code") != null) {
dataMap.put("code", resp.get("code"));
}
return ReqResult.success(dataMap);
}
@Override
public ReqResult<Map<String, Object>> stopAgent(String projectId, UserContext userContext) {
if (StringUtils.isBlank(projectId)) {
return ReqResult.error("0001", "projectId is required");
}
Long projectIdLong;
try {
projectIdLong = Long.valueOf(projectId);
} catch (Exception e) {
return ReqResult.error("0001", "Invalid projectId");
}
Map<String, Object> resp = aiAgentClient.stopAgent(projectIdLong, userContext);
if (resp == null) {
return ReqResult.error("9999", "Failed to stop Agent service: AI Agent returned no response");
}
Object code = resp.get("code");
if (code == null || !"0000".equals(String.valueOf(code))) {
String message = resp.get("message") == null ? "Failed to stop Agent service" : String.valueOf(resp.get("message"));
return ReqResult.error("9999", message);
}
// 提取data字段并转换为Map
Object data = resp.get("data");
Map<String, Object> dataMap = new HashMap<>();
if (data != null) {
try {
// 如果data已经是Map类型直接使用
if (data instanceof Map<?, ?>) {
@SuppressWarnings("unchecked")
Map<String, Object> existingMap = (Map<String, Object>) data;
dataMap = existingMap;
} else {
// 将JSON字符串转换为Map
String dataJson = data.toString();
dataMap = objectMapper.readValue(dataJson, new TypeReference<Map<String, Object>>() {
});
}
} catch (Exception e) {
log.warn("Failed to parse data JSON, using raw payload", e);
// 解析失败时将原始data包装在Map中
dataMap.put("data", data);
}
}
if (resp.get("tid") != null) {
dataMap.put("tid", resp.get("tid"));
}
if (resp.get("message") != null) {
dataMap.put("message", resp.get("message"));
}
if (resp.get("code") != null) {
dataMap.put("code", resp.get("code"));
}
return ReqResult.success(dataMap);
}
}

View File

@@ -0,0 +1,228 @@
package com.xspaceagi.custompage.domain.service.impl;
import com.xspaceagi.custompage.domain.dto.PageFileInfo;
import com.xspaceagi.custompage.domain.gateway.PageFileBuildClient;
import com.xspaceagi.custompage.domain.keepalive.IKeepAliveService;
import com.xspaceagi.custompage.domain.model.CustomPageBuildModel;
import com.xspaceagi.custompage.domain.proxypath.ICustomPageProxyPathService;
import com.xspaceagi.custompage.domain.repository.ICustomPageBuildRepository;
import com.xspaceagi.custompage.domain.service.ICustomPageCodingDomainService;
import com.xspaceagi.custompage.sdk.dto.VersionInfoDto;
import com.xspaceagi.custompage.sdk.enums.CustomPageActionEnum;
import com.xspaceagi.system.sdk.permission.SpacePermissionService;
import com.xspaceagi.system.spec.common.UserContext;
import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.spec.utils.DateUtil;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.util.Date;
import java.util.List;
import java.util.Map;
@Slf4j
@Service
public class CustomPageCodingDomainServiceImpl implements ICustomPageCodingDomainService {
@Resource
private IKeepAliveService keepAliveService;
@Resource
private PageFileBuildClient pageFileBuildClient;
@Resource
private SpacePermissionService spacePermissionService;
@Resource
private ICustomPageBuildRepository customPageBuildRepository;
@Resource
private ICustomPageProxyPathService customPageProxyPathService;
@Override
public ReqResult<Map<String, Object>> specifiedFilesUpdate(Long projectId, List<PageFileInfo> files,
UserContext userContext) {
log.info("[specified Files Update] project Id={},start domain execution", projectId);
CustomPageBuildModel buildModel = customPageBuildRepository.getByProjectId(projectId);
if (buildModel == null) {
return ReqResult.error("0001", "Project does not exist");
}
// 校验空间权限
spacePermissionService.checkSpaceUserPermission(buildModel.getSpaceId());
String devProxyPath = customPageProxyPathService.getDevProxyPath(projectId);
Map<String, Object> resp = pageFileBuildClient.specifiedFilesUpdate(projectId, files, buildModel.getCodeVersion(), devProxyPath,
buildModel.getDevPid());
if (resp == null) {
return ReqResult.error("9999", "Update specified files failed: build server returned no response");
}
boolean success = Boolean.parseBoolean(String.valueOf(resp.get("success")));
String message = resp.get("message") == null ? "" : String.valueOf(resp.get("message"));
if (!success) {
return ReqResult.error("9999", message);
}
// 更新版本信息
VersionInfoDto newVersionInfo = VersionInfoDto.builder()
.action(CustomPageActionEnum.SUBMIT_FILES_UPDATE.getCode())
.build();
updateVersion(buildModel, newVersionInfo, userContext);
return ReqResult.success(resp);
}
@Override
public ReqResult<Map<String, Object>> allFilesUpdate(Long projectId, List<PageFileInfo> files,
UserContext userContext) {
log.info("[all Files Update] project Id={},start domain execution", projectId);
CustomPageBuildModel buildModel = customPageBuildRepository.getByProjectId(projectId);
if (buildModel == null) {
return ReqResult.error("0001", "Project does not exist");
}
// 校验空间权限
spacePermissionService.checkSpaceUserPermission(buildModel.getSpaceId());
String devProxyPath = customPageProxyPathService.getDevProxyPath(projectId);
Map<String, Object> resp = pageFileBuildClient.allFilesUpdate(projectId, files, buildModel.getCodeVersion(), devProxyPath,
buildModel.getDevPid());
if (resp == null) {
return ReqResult.error("9999", "Full file update failed: build server returned no response");
}
boolean success = Boolean.parseBoolean(String.valueOf(resp.get("success")));
String message = resp.get("message") == null ? "" : String.valueOf(resp.get("message"));
if (!success) {
return ReqResult.error("9999", message);
}
// 更新版本信息
VersionInfoDto newVersionInfo = VersionInfoDto.builder()
.action(CustomPageActionEnum.SUBMIT_FILES_UPDATE.getCode())
.build();
updateVersion(buildModel, newVersionInfo, userContext);
// 开发服务器可能重启,如果重启则更新
Object pidObj = resp.get("pid");
Object portObj = resp.get("port");
if (pidObj instanceof Integer && portObj instanceof Integer) {
Integer pid = Integer.valueOf(String.valueOf(pidObj));
Integer port = Integer.valueOf(String.valueOf(portObj));
keepAliveService.updateKeepAlive(projectId, new Date(), 1, pid, port, userContext);
}
return ReqResult.success(resp);
}
@Override
public ReqResult<Map<String, Object>> uploadSingleFile(Long projectId, MultipartFile file, String filePath,
UserContext userContext) {
log.info("[upload Single File] project Id={},start domain execution", projectId);
CustomPageBuildModel buildModel = customPageBuildRepository.getByProjectId(projectId);
if (buildModel == null) {
return ReqResult.error("0001", "Project does not exist");
}
// 校验空间权限
spacePermissionService.checkSpaceUserPermission(buildModel.getSpaceId());
Integer currentVersion = buildModel.getCodeVersion() == null ? 0 : buildModel.getCodeVersion();
Map<String, Object> resp = pageFileBuildClient.uploadSingleFile(projectId, file, filePath, currentVersion);
if (resp == null) {
return ReqResult.error("9999", "Upload file failed: build server returned no response");
}
boolean success = Boolean.parseBoolean(String.valueOf(resp.get("success")));
String message = resp.get("message") == null ? "" : String.valueOf(resp.get("message"));
if (!success) {
return ReqResult.error("9999", message);
}
// 更新版本信息
VersionInfoDto newVersionInfo = VersionInfoDto.builder()
.action(CustomPageActionEnum.UPLOAD_SINGLE_FILE.getCode())
.ext(Map.of("filePath", filePath))
.build();
updateVersion(buildModel, newVersionInfo, userContext);
// 开发服务器可能重启,如果重启则更新updateKeepAlive
Object pidObj = resp.get("pid");
Object portObj = resp.get("port");
if (pidObj instanceof Integer && portObj instanceof Integer) {
Integer pid = Integer.valueOf(String.valueOf(pidObj));
Integer port = Integer.valueOf(String.valueOf(portObj));
keepAliveService.updateKeepAlive(projectId, new Date(), 1, pid, port, userContext);
}
return ReqResult.success(resp);
}
@Override
public ReqResult<Map<String, Object>> rollbackVersion(Long projectId, Integer rollbackTo,
UserContext userContext) {
log.info("[rollback To Version] project Id={},start domain execution,rollback To Version={}", projectId, rollbackTo);
CustomPageBuildModel buildModel = customPageBuildRepository.getByProjectId(projectId);
if (buildModel == null) {
return ReqResult.error("0001", "Project does not exist");
}
// 校验空间权限
spacePermissionService.checkSpaceUserPermission(buildModel.getSpaceId());
// 校验版本号
Integer currentVersion = buildModel.getCodeVersion() == null ? 0 : buildModel.getCodeVersion();
if (rollbackTo >= currentVersion) {
return ReqResult.error("0002", "Rollback version must be less than the current version");
}
if (rollbackTo < 1) {
return ReqResult.error("0002", "Rollback version cannot be less than 1");
}
String devProxyPath = customPageProxyPathService.getDevProxyPath(projectId);
Map<String, Object> resp = pageFileBuildClient.rollbackVersion(projectId, rollbackTo, buildModel.getCodeVersion(), devProxyPath,
buildModel.getDevPid());
if (resp == null) {
return ReqResult.error("9999", "Rollback version failed: build server returned no response");
}
boolean success = Boolean.parseBoolean(String.valueOf(resp.get("success")));
String message = resp.get("message") == null ? "" : String.valueOf(resp.get("message"));
if (!success) {
return ReqResult.error("9999", message);
}
// 更新版本信息
VersionInfoDto newVersionInfo = VersionInfoDto.builder()
.action(CustomPageActionEnum.ROLLBACK_VERSION.getCode())
.ext(Map.of("rollbackTo", String.valueOf(rollbackTo)))
.build();
updateVersion(buildModel, newVersionInfo, userContext);
// 开发服务器可能重启,如果重启则更新
Object pidObj = resp.get("pid");
Object portObj = resp.get("port");
if (pidObj instanceof Integer && portObj instanceof Integer) {
Integer pid = Integer.valueOf(String.valueOf(pidObj));
Integer port = Integer.valueOf(String.valueOf(portObj));
keepAliveService.updateKeepAlive(projectId, new Date(), 1, pid, port, userContext);
}
return ReqResult.success(resp);
}
// 更新版本信息
private void updateVersion(CustomPageBuildModel buildModel, VersionInfoDto newVersionInfo, UserContext userContext) {
Integer nextVersion = buildModel.getCodeVersion() + 1;
List<VersionInfoDto> versionInfo = buildModel.getVersionInfo();
newVersionInfo.setVersion(nextVersion);
newVersionInfo.setTime(DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss"));
versionInfo.add(newVersionInfo);
CustomPageBuildModel updateModel = new CustomPageBuildModel();
updateModel.setId(buildModel.getId());
updateModel.setCodeVersion(nextVersion);
updateModel.setVersionInfo(versionInfo);
customPageBuildRepository.updateVersionInfo(updateModel, userContext);
}
}

View File

@@ -0,0 +1,180 @@
package com.xspaceagi.custompage.domain.service.impl;
import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Service;
import com.xspaceagi.custompage.domain.model.CustomPageConversationModel;
import com.xspaceagi.custompage.domain.repository.ICustomPageConfigRepository;
import com.xspaceagi.custompage.domain.repository.ICustomPageConversationRepository;
import com.xspaceagi.custompage.domain.service.ICustomPageConversationDomainService;
import com.xspaceagi.system.sdk.permission.SpacePermissionService;
import com.xspaceagi.system.spec.common.UserContext;
import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.spec.page.SuperPage;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
public class CustomPageConversationDomainServiceImpl implements ICustomPageConversationDomainService {
@Resource
private ICustomPageConversationRepository customPageConversationRepository;
@Resource
private ICustomPageConfigRepository customPageConfigRepository;
@Resource
private SpacePermissionService spacePermissionService;
@Override
public ReqResult<Long> saveOrUpdateAssistantConversation(CustomPageConversationModel model, UserContext userContext) {
Optional.ofNullable(model.getProjectId()).filter(x -> x > 0)
.orElseThrow(() -> new IllegalArgumentException("projectId is required or invalid"));
Optional.ofNullable(model.getContent()).filter(x -> !x.trim().isEmpty())
.orElseThrow(() -> new IllegalArgumentException("content is required"));
Optional.ofNullable(model.getSessionId()).filter(x -> !x.trim().isEmpty())
.orElseThrow(() -> new IllegalArgumentException("sessionId is required"));
try {
var configModel = customPageConfigRepository.getById(model.getProjectId());
if (configModel == null) {
return ReqResult.error("0001", "Project does not exist");
}
spacePermissionService.checkSpaceUserPermission(configModel.getSpaceId());
model.setSpaceId(configModel.getSpaceId());
if (model.getId() != null) {
CustomPageConversationModel existing = customPageConversationRepository.findById(model.getId());
if (existing == null || !"ASSISTANT".equalsIgnoreCase(existing.getRole())
|| !model.getProjectId().equals(existing.getProjectId())) {
return ReqResult.error("0001", "Invalid assistant conversation id");
}
boolean updated = customPageConversationRepository.updateAssistantContent(model.getId(),
model.getContent(), model.getRequestId(), userContext);
return updated ? ReqResult.success(model.getId())
: ReqResult.error("0001", "Failed to update assistant conversation");
}
Long id = customPageConversationRepository.save(model, userContext);
return ReqResult.success(id);
} catch (Exception e) {
log.error("[Domain] save or update assistant conversation failed, project Id={}, sessionId={}",
model.getProjectId(), model.getSessionId(), e);
return ReqResult.error("0001", "Failed to save assistant conversation: " + e.getMessage());
}
}
@Override
public ReqResult<Long> saveConversation(CustomPageConversationModel model, UserContext userContext) {
log.info("[Domain] saveusersession records, project Id={}, topic={}", model.getProjectId(), model.getTopic());
Optional.ofNullable(model.getProjectId()).filter(x -> x > 0)
.orElseThrow(() -> new IllegalArgumentException("projectId is required or invalid"));
Optional.ofNullable(model.getContent()).filter(x -> !x.trim().isEmpty())
.orElseThrow(() -> new IllegalArgumentException("content is required"));
try {
var configModel = customPageConfigRepository.getById(model.getProjectId());
if (configModel == null) {
log.warn("[Domain] Project not found, project Id={}", model.getProjectId());
return ReqResult.error("0001", "Project does not exist");
}
// 校验空间权限
spacePermissionService.checkSpaceUserPermission(configModel.getSpaceId());
// 设置 spaceId
model.setSpaceId(configModel.getSpaceId());
Long id = customPageConversationRepository.save(model, userContext);
log.info("[Domain] saveusersession records succeeded, id={}", id);
return ReqResult.success(id);
} catch (Exception e) {
log.error("[Domain] saveusersession recordsexception, project Id={}", model.getProjectId(), e);
return ReqResult.error("0001", "Failed to save conversation: " + e.getMessage());
}
}
@Override
public List<CustomPageConversationModel> listByProjectId(Long projectId, Long userId) {
log.info("[Domain] queryprojectsession recordslist, project Id={}, user Id={}", projectId, userId);
List<CustomPageConversationModel> models = customPageConversationRepository
.listByProjectId(projectId, userId);
if (models != null && !models.isEmpty()) {
// 校验空间权限
spacePermissionService.checkSpaceUserPermission(models.get(0).getSpaceId());
}
return models;
}
@Override
public ReqResult<SuperPage<CustomPageConversationModel>> pageQuery(CustomPageConversationModel queryModel,
Long current, Long pageSize, UserContext userContext) {
log.info("[Domain] pagedquerysession records, project Id={}, current={}, page Size={}", queryModel.getProjectId(), current,
pageSize);
try {
Optional.ofNullable(queryModel.getProjectId()).filter(x -> x > 0)
.orElseThrow(() -> new IllegalArgumentException("projectId is required or invalid"));
Optional.ofNullable(current).filter(x -> x > 0)
.orElseThrow(() -> new IllegalArgumentException("current is required or invalid"));
Optional.ofNullable(pageSize).filter(x -> x > 0)
.orElseThrow(() -> new IllegalArgumentException("pageSize is required or invalid"));
// 校验项目是否存在并获取空间权限
var configModel = customPageConfigRepository.getById(queryModel.getProjectId());
if (configModel == null) {
log.warn("[Domain] Project not found, project Id={}", queryModel.getProjectId());
return ReqResult.error("0001", "Project does not exist");
}
// 校验空间权限
spacePermissionService.checkSpaceUserPermission(configModel.getSpaceId());
SuperPage<CustomPageConversationModel> result = customPageConversationRepository.pageQuery(queryModel,
current, pageSize);
log.info("[Domain] pagedquerysession records succeeded, total={}", result.getTotal());
return ReqResult.success(result);
} catch (Exception e) {
log.error("[Domain] pagedquerysession recordsexception, project Id={}", queryModel.getProjectId(), e);
return ReqResult.error("0001", "Failed to page query conversations: " + e.getMessage());
}
}
@Override
public ReqResult<Void> updateUserSessionIdByRequestId(Long projectId, String requestId, String sessionId,
UserContext userContext) {
try {
if (projectId == null || projectId <= 0 || requestId == null || requestId.isBlank()
|| sessionId == null || sessionId.isBlank()) {
return ReqResult.error("0001", "Invalid parameters for updating user sessionId");
}
boolean updated = customPageConversationRepository.updateUserSessionIdByRequestId(projectId, requestId,
sessionId, userContext.getUserId());
return updated ? ReqResult.success() : ReqResult.error("0001", "No matched USER conversation found");
} catch (Exception e) {
log.error("[Domain] update user sessionId by requestId failed, project Id={}, requestId={}", projectId, requestId, e);
return ReqResult.error("0001", "Failed to update user sessionId: " + e.getMessage());
}
}
@Override
public ReqResult<Void> deleteByProjectId(Long projectId, UserContext userContext) {
try {
if (projectId == null || projectId <= 0) {
return ReqResult.error("0001", "projectId is required or invalid");
}
boolean deleted = customPageConversationRepository.deleteByProjectId(projectId, userContext.getUserId());
return deleted ? ReqResult.success() : ReqResult.error("0001", "Delete conversation records failed");
} catch (Exception e) {
log.error("[Domain] delete conversations by project failed, project Id={}", projectId, e);
return ReqResult.error("0001", "Failed to delete conversations: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,117 @@
package com.xspaceagi.custompage.domain.service.impl;
import java.util.Date;
import java.util.List;
import org.springframework.stereotype.Service;
import com.xspaceagi.custompage.domain.model.CustomPageDomainModel;
import com.xspaceagi.custompage.domain.repository.ICustomPageDomainRepository;
import com.xspaceagi.custompage.domain.service.ICustomPageDomainDomainService;
import com.xspaceagi.system.spec.common.UserContext;
import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
import com.xspaceagi.system.spec.exception.BizException;
import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
public class CustomPageDomainDomainServiceImpl implements ICustomPageDomainDomainService {
@Resource
private ICustomPageDomainRepository customPageDomainRepository;
@Override
public List<CustomPageDomainModel> listByProjectId(Long projectId) {
return customPageDomainRepository.listByProjectId(projectId);
}
@Override
public CustomPageDomainModel getById(Long id) {
return customPageDomainRepository.getById(id);
}
@Override
public ReqResult<CustomPageDomainModel> create(CustomPageDomainModel model, UserContext userContext) {
log.info("[create] Create domain binding, project Id={}, domain={}", model.getProjectId(), model.getDomain());
// 校验域名是否已存在
CustomPageDomainModel existing = customPageDomainRepository.getByDomain(model.getDomain());
if (existing != null) {
log.error("[create] domainalready exists, domain={}", model.getDomain());
return ReqResult.error("0001", "Domain already exists");
}
model.setTenantId(userContext.getTenantId());
model.setCreated(new Date());
model.setModified(new Date());
CustomPageDomainModel result = customPageDomainRepository.add(model);
log.info("[create] createdomain bindingsucceeded, id={}", result.getId());
return ReqResult.success(result);
}
@Override
public ReqResult<CustomPageDomainModel> update(CustomPageDomainModel model, UserContext userContext) {
log.info("[update] updatedomain binding, id={}, domain={}", model.getId(), model.getDomain());
CustomPageDomainModel existing = customPageDomainRepository.getById(model.getId());
if (existing == null) {
log.error("[update] domain bindingnot found, id={}", model.getId());
return ReqResult.error("0002", "Domain binding does not exist");
}
// 校验租户
if (!existing.getTenantId().equals(userContext.getTenantId())) {
log.error("[update] no permission to operate this domain binding, id={}", model.getId());
throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.permissionDenied);
}
// 校验域名是否已被其他记录使用
CustomPageDomainModel domainDuplicate = customPageDomainRepository.getByDomain(model.getDomain());
if (domainDuplicate != null && !domainDuplicate.getId().equals(model.getId())) {
log.error("[update] domain already used by another record, domain={}", model.getDomain());
return ReqResult.error("0004", "Domain is already used by another binding");
}
existing.setDomain(model.getDomain());
existing.setProjectId(model.getProjectId());
existing.setModified(new Date());
customPageDomainRepository.updateById(existing);
log.info("[update] updatedomain bindingsucceeded, id={}", existing.getId());
return ReqResult.success(existing);
}
@Override
public ReqResult<Void> delete(Long id, UserContext userContext) {
log.info("[delete] Delete domain binding, id={}", id);
CustomPageDomainModel existing = customPageDomainRepository.getById(id);
if (existing == null) {
log.error("[delete] domain bindingnot found, id={}", id);
return ReqResult.error("0005", "Domain binding does not exist");
}
// 校验租户
if (!existing.getTenantId().equals(userContext.getTenantId())) {
log.error("[delete] no permission to operate this domain binding, id={}", id);
throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.permissionDenied);
}
customPageDomainRepository.removeById(id);
log.info("[delete] deletedomain bindingsucceeded, id={}", id);
return ReqResult.success(null);
}
@Override
public List<String> listAllDomains() {
return customPageDomainRepository.listAllDomains();
}
}

View File

@@ -0,0 +1,37 @@
package com.xspaceagi.custompage.domain.service.impl;
import com.xspaceagi.custompage.domain.gateway.PageFileBuildClient;
import com.xspaceagi.custompage.domain.service.ICustomPageFileDomainService;
import com.xspaceagi.system.spec.common.UserContext;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import reactor.core.publisher.Flux;
/**
* 通用文件领域服务实现
*/
@Slf4j
@Service
public class CustomPageFileDomainServiceImpl implements ICustomPageFileDomainService {
@Resource
private PageFileBuildClient pageFileBuildClient;
@Override
public Flux<DataBuffer> getStaticFile(String targetPrefix, String relativePath, String logId, UserContext userContext) {
log.info("[Domain] log Id={}, getstaticfile,target Prefix={}, relative Path={}", logId, targetPrefix, relativePath);
return pageFileBuildClient.getStaticFile(targetPrefix, relativePath, logId)
.doOnError(WebClientResponseException.class, e -> {
log.error("[Domain] log Id={}, getstaticfilefailed, target Prefix={}, relative Path={}, status={}",
logId, targetPrefix, relativePath, e.getStatusCode());
})
.doOnError(Throwable.class, e -> {
log.error("[Domain] log Id={}, getstaticfileexception, target Prefix={}, relative Path={}", logId, targetPrefix, relativePath, e);
});
}
}

View File

@@ -0,0 +1,71 @@
package com.xspaceagi.custompage.domain.threadpool;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 用户页面项目的线程池配置
*/
@Configuration
public class CustomPageAsyncConfig {
/**
* 短 IOFlux 推送、/chat HTTP 调用等,避免被长连接占满。
*/
@Bean("aiChatExecutor")
public Executor aiChatExecutor() {
return new ThreadPoolExecutor(
20,
100,
60L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(500),
namedThreadFactory("ai-chat-"),
new ThreadPoolExecutor.AbortPolicy());
}
/**
* 长 IOAgent 进度 SSE 订阅(阻塞读至流结束)。
*/
@Bean("aiAgentProgressExecutor")
public Executor aiAgentProgressExecutor() {
return new ThreadPoolExecutor(
20,
200,
60L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100),
namedThreadFactory("ai-agent-progress-"),
new ThreadPoolExecutor.CallerRunsPolicy());
}
/**
* DB 轮询回放:定时任务,不占用 aiChatExecutor 工作线程 sleep。
*/
@Bean(name = "aiAgentProgressScheduler", destroyMethod = "shutdown")
public ScheduledExecutorService aiAgentProgressScheduler() {
return Executors.newScheduledThreadPool(4, namedThreadFactory("ai-agent-progress-sched-"));
}
private static ThreadFactory namedThreadFactory(String namePrefix) {
return new ThreadFactory() {
private final AtomicInteger threadNumber = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, namePrefix + threadNumber.getAndIncrement());
t.setDaemon(true);
return t;
}
};
}
}

View File

@@ -0,0 +1,45 @@
package com.xspaceagi.custompage.domain.util;
import java.util.function.Supplier;
import com.xspaceagi.system.spec.common.RequestContext;
import com.xspaceagi.system.spec.common.UserContext;
/**
* 后台线程执行 DB 操作前绑定租户 RequestContext
*/
public final class AgentProgressContextUtil {
private AgentProgressContextUtil() {
}
public static void runWithUserContext(UserContext userContext, Runnable action) {
callWithUserContext(userContext, () -> {
action.run();
return null;
});
}
public static <T> T callWithUserContext(UserContext userContext, Supplier<T> supplier) {
RequestContext<?> existing = RequestContext.get();
boolean created = false;
try {
if (existing == null) {
if (userContext == null || userContext.getTenantId() == null) {
throw new IllegalStateException("RequestContext and tenantId are both unavailable for DB access");
}
RequestContext<Object> requestContext = new RequestContext<>();
requestContext.setTenantId(userContext.getTenantId());
requestContext.setUserId(userContext.getUserId());
requestContext.setUserContext(userContext);
RequestContext.set(requestContext);
created = true;
}
return supplier.get();
} finally {
if (created) {
RequestContext.remove();
}
}
}
}

View File

@@ -0,0 +1,137 @@
package com.xspaceagi.custompage.domain.util;
import java.util.List;
import java.util.Map;
import com.alibaba.fastjson2.JSON;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Agent 进度 SSE 事件解析(终态判定等与前后端约定保持一致)。
*/
public final class AgentProgressEventUtil {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
private AgentProgressEventUtil() {
}
public static boolean shouldPersist(String eventName) {
return !"ping".equalsIgnoreCase(eventName);
}
public static boolean isFinalEvent(String eventName, String data) {
if ("success".equalsIgnoreCase(eventName) || "error".equalsIgnoreCase(eventName)
|| "canceled".equalsIgnoreCase(eventName)
|| "end_turn".equalsIgnoreCase(eventName)) {
return true;
}
if (!JSON.isValidObject(data)) {
return false;
}
try {
Map<String, Object> payload = OBJECT_MAPPER.readValue(data, new TypeReference<Map<String, Object>>() {
});
return isFinalPayload(payload);
} catch (Exception e) {
return false;
}
}
public static boolean isFinalPayload(Map<String, Object> payload) {
if (payload == null || payload.isEmpty()) {
return false;
}
Object type = payload.get("type");
if (type != null) {
String typeStr = String.valueOf(type);
if ("success".equalsIgnoreCase(typeStr) || "error".equalsIgnoreCase(typeStr)
|| "canceled".equalsIgnoreCase(typeStr)
|| "end_turn".equalsIgnoreCase(typeStr)) {
return true;
}
}
Object subType = payload.get("subType");
if (subType != null && "end_turn".equalsIgnoreCase(String.valueOf(subType))) {
return true;
}
Object reason = payload.get("reason");
if (reason != null && "EndTurn".equalsIgnoreCase(String.valueOf(reason))) {
return true;
}
Object dataObj = payload.get("data");
if (dataObj instanceof Map<?, ?> dataMap) {
Object nestedReason = dataMap.get("reason");
if (nestedReason != null && "EndTurn".equalsIgnoreCase(String.valueOf(nestedReason))) {
return true;
}
Object nestedSubType = dataMap.get("subType");
if (nestedSubType != null && "end_turn".equalsIgnoreCase(String.valueOf(nestedSubType))) {
return true;
}
}
return false;
}
@SuppressWarnings("unchecked")
public static boolean isAssistantContentTerminal(String contentJson) {
if (contentJson == null || contentJson.isBlank()) {
return false;
}
try {
Map<String, Object> body = OBJECT_MAPPER.readValue(contentJson, new TypeReference<Map<String, Object>>() {
});
Object eventsObj = body.get("events");
if (!(eventsObj instanceof List<?> events)) {
return false;
}
for (Object item : events) {
if (!(item instanceof Map<?, ?> record)) {
continue;
}
String eventName = record.get("event") == null ? null : String.valueOf(record.get("event"));
Object data = record.get("data");
String dataStr;
if (data instanceof String s) {
dataStr = s;
} else if (data != null) {
dataStr = JSON.toJSONString(data);
} else {
dataStr = null;
}
if (isFinalEvent(eventName, dataStr)) {
return true;
}
if (data instanceof Map<?, ?> dataMap) {
Map<String, Object> payload = castToStringObjectMap(dataMap);
if (isFinalPayload(payload)) {
return true;
}
}
}
return false;
} catch (Exception e) {
return false;
}
}
@SuppressWarnings("unchecked")
private static Map<String, Object> castToStringObjectMap(Map<?, ?> map) {
return (Map<String, Object>) map;
}
public static Object parseRawData(String data) {
if (!JSON.isValidObject(data)) {
return data;
}
try {
return OBJECT_MAPPER.readValue(data, new TypeReference<Map<String, Object>>() {
});
} catch (Exception e) {
return data;
}
}
}

View File

@@ -0,0 +1,55 @@
<?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-custom-page</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-custom-page-infra</artifactId>
<dependencies>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-custom-page-domain</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-custom-page-spec</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-custom-page-adapter</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-agent-core-sdk</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-compose-sdk</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>system-sdk</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-knowledge-sdk</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-eco-market-sdk</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-pricing-sdk</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-agent-core-application</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,78 @@
package com.xspaceagi.custompage.infra.dao.entity;
import java.util.Date;
import java.util.List;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.xspaceagi.custompage.infra.dao.typehandler.VersionInfoListTypeHandler;
import com.xspaceagi.custompage.sdk.dto.VersionInfoDto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@TableName(value = "custom_page_build", autoResultMap = true)
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class CustomPageBuild {
@TableId(type = IdType.AUTO)
private Long id;
private Long projectId;
// 1:运行中
private Integer devRunning;
private Integer devPid;
private Integer devPort;
// 最后保活时间
private Date lastKeepAliveTime;
// 1:运行中
private Integer buildRunning;
private Date buildTime;
private Integer buildVersion;
private Integer codeVersion;
@TableField(value = "version_info", typeHandler = VersionInfoListTypeHandler.class)
private List<VersionInfoDto> versionInfo;
// 上次对话模型ID
private Long lastChatModelId;
// 上次多模态ID
private Long lastMultiModelId;
@TableField(value = "_tenant_id")
private Long tenantId;
private Long spaceId;
private Date created;
private Long creatorId;
private String creatorName;
private Date modified;
private Long modifiedId;
private String modifiedName;
// 1:有效; -1:无效
private Integer yn;
}

View File

@@ -0,0 +1,184 @@
package com.xspaceagi.custompage.infra.dao.entity;
import java.util.Date;
import java.util.List;
import com.xspaceagi.custompage.sdk.dto.*;
import org.apache.ibatis.type.EnumTypeHandler;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.xspaceagi.custompage.infra.dao.typehandler.DataSourceListTypeHandler;
import com.xspaceagi.custompage.infra.dao.typehandler.PageArgConfigListTypeHandler;
import com.xspaceagi.custompage.infra.dao.typehandler.ProxyConfigListTypeHandler;
import com.xspaceagi.system.spec.common.JsonTypeHandlerWithoutType;
import lombok.Data;
@TableName(value = "custom_page_config", autoResultMap = true)
@Data
public class CustomPageConfig {
private Long id;
private String name;
private String description;
private String icon;
private String coverImg;
@TableField(value = "cover_img_source_type", typeHandler = EnumTypeHandler.class)
private SourceTypeEnum coverImgSourceType;
// 自定义页面唯一标识
private String basePath;
// 1:已发布; 0:未发布
private Integer buildRunning;
// 发布类型 (数据库中存储为varchar类型存储枚举名称字符串)
@TableField(value = "publish_type", typeHandler = EnumTypeHandler.class)
private PublishTypeEnum publishType;
// 需要登录,1:需要
private Integer needLogin;
// 调试关联智能体
private Long devAgentId;
// 页面项目类型 (数据库中存储为varchar类型存储枚举名称字符串)
@TableField(value = "project_type", typeHandler = EnumTypeHandler.class)
private ProjectType projectType;
@TableField(value = "proxy_config", typeHandler = ProxyConfigListTypeHandler.class)
private List<ProxyConfig> proxyConfigs;
@TableField(value = "page_arg_config", typeHandler = PageArgConfigListTypeHandler.class)
private List<PageArgConfig> pageArgConfigs;
@TableField(value = "data_sources", typeHandler = DataSourceListTypeHandler.class)
private List<DataSourceDto> dataSources;
private Long sandboxId;
@TableField(value = "ext", typeHandler = JsonTypeHandlerWithoutType.class)
private Object ext;
@TableField(value = "_tenant_id")
private Long tenantId;
private Long spaceId;
private Date created;
private Long creatorId;
private String creatorName;
private Date modified;
private Long modifiedId;
private String modifiedName;
private Integer yn;
// public enum ProjectType {
// // 纯反向代理
// REVERSE_PROXY,
// // 在线开发部署
// ONLINE_DEPLOY,
// // 两种类型都有代理配置,只是 ONLINE_DEPLOY 不可配置根目录"/"的代理,根目录由系统自动分配
// }
// @Data
// public static class PageArgConfig implements Serializable {
// @Schema(description = "页面路径,例如 /view")
// private String pageUri;
// @Schema(description = "页面名称")
// private String name;
// @Schema(description = "页面描述")
// private String description;
// @Schema(description = "参数配置")
// private List<PageArg> args;
// }
// @Data
// public static class PageArg implements Serializable {
// @Schema(description = "参数key唯一标识不需要前端传递后台根据配置自动生成")
// private String key;
// @Schema(description = "参数名称,符合函数命名规则")
// private String name;
// @Schema(description = "参数详细描述信息")
// private String description;
// @Schema(description = "数据类型")
// private DataTypeEnum dataType;
// @Schema(description = "是否必须")
// private boolean require;
// @Schema(description = "下级参数")
// private List<PageArg> subArgs;
// }
// public enum DataTypeEnum {
// String, // 文本
// Integer, // 整型数字
// Number, // 数字
// Boolean, // 布尔
// Object, // 对象
// Array_String, // String数组
// Array_Integer, // Integer数组
// Array_Number, // Number数组
// Array_Boolean, // Boolean数组
// Array_Object,// Object数组
// }
// @Data
// @Builder
// @AllArgsConstructor
// @NoArgsConstructor
// public static class ProxyConfig {
// // 环境,比如 dev 为开发环境prod 为生产环境
// private ProxyEnv env;
// // 路径,比如 /education代表/education后的所有请求都会被代理到${backends}
// private String path;
// private List<ProxyConfigBackend> backends;
// private String healthCheckPath;
// // 是否必须登录默认true
// private boolean requireAuth;
// public enum ProxyEnv {
// dev, prod;
// public static ProxyEnv get(String value) {
// for (ProxyEnv env : values()) {
// if (env.name().equals(value)) {
// return env;
// }
// }
// return null;
// }
// }
// }
// @Data
// public static class ProxyConfigBackend {
// // http://192.168.1.34:3001/[xxx]
// private String backend;
// private int weight;
// public ProxyConfigBackend() {
// }
// public ProxyConfigBackend(String backend, int weight) {
// this.backend = backend;
// this.weight = weight;
// }
// }
}

View File

@@ -0,0 +1,47 @@
package com.xspaceagi.custompage.infra.dao.entity;
import java.util.Date;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@TableName(value = "custom_page_conversation")
@Data
public class CustomPageConversation {
private Long id;
// 项目ID
private Long projectId;
// 会话主题
private String topic;
// 会话内容
private String content;
// 消息角色USER/ASSISTANT
private String role;
// 会话ID
private String sessionId;
// 请求ID
private String requestId;
@TableField(value = "_tenant_id")
private Long tenantId;
private Long spaceId;
private Date created;
private Long creatorId;
private String creatorName;
private Date modified;
private Long modifiedId;
private String modifiedName;
private Integer yn;
}

View File

@@ -0,0 +1,27 @@
package com.xspaceagi.custompage.infra.dao.entity;
import java.util.Date;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@TableName(value = "custom_page_domain")
@Data
public class CustomPageDomain {
private Long id;
@TableField(value = "_tenant_id")
private Long tenantId;
@TableField(value = "project_id")
private Long projectId;
private String domain;
private Date created;
private Date modified;
}

View File

@@ -0,0 +1,11 @@
package com.xspaceagi.custompage.infra.dao.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xspaceagi.custompage.infra.dao.entity.CustomPageBuild;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface CustomPageBuildMapper extends BaseMapper<CustomPageBuild> {
}

View File

@@ -0,0 +1,10 @@
package com.xspaceagi.custompage.infra.dao.mapper;
import org.apache.ibatis.annotations.Mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xspaceagi.custompage.infra.dao.entity.CustomPageConfig;
@Mapper
public interface CustomPageConfigMapper extends BaseMapper<CustomPageConfig> {
}

View File

@@ -0,0 +1,10 @@
package com.xspaceagi.custompage.infra.dao.mapper;
import org.apache.ibatis.annotations.Mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xspaceagi.custompage.infra.dao.entity.CustomPageConversation;
@Mapper
public interface CustomPageConversationMapper extends BaseMapper<CustomPageConversation> {
}

View File

@@ -0,0 +1,10 @@
package com.xspaceagi.custompage.infra.dao.mapper;
import org.apache.ibatis.annotations.Mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xspaceagi.custompage.infra.dao.entity.CustomPageDomain;
@Mapper
public interface CustomPageDomainMapper extends BaseMapper<CustomPageDomain> {
}

View File

@@ -0,0 +1,9 @@
package com.xspaceagi.custompage.infra.dao.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.xspaceagi.custompage.infra.dao.entity.CustomPageBuild;
public interface ICustomPageBuildService extends IService<CustomPageBuild> {
}

View File

@@ -0,0 +1,7 @@
package com.xspaceagi.custompage.infra.dao.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.xspaceagi.custompage.infra.dao.entity.CustomPageConfig;
public interface ICustomPageConfigService extends IService<CustomPageConfig> {
}

View File

@@ -0,0 +1,7 @@
package com.xspaceagi.custompage.infra.dao.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.xspaceagi.custompage.infra.dao.entity.CustomPageConversation;
public interface ICustomPageConversationService extends IService<CustomPageConversation> {
}

View File

@@ -0,0 +1,7 @@
package com.xspaceagi.custompage.infra.dao.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.xspaceagi.custompage.infra.dao.entity.CustomPageDomain;
public interface ICustomPageDomainService extends IService<CustomPageDomain> {
}

View File

@@ -0,0 +1,14 @@
package com.xspaceagi.custompage.infra.dao.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xspaceagi.custompage.infra.dao.entity.CustomPageBuild;
import com.xspaceagi.custompage.infra.dao.mapper.CustomPageBuildMapper;
import com.xspaceagi.custompage.infra.dao.service.ICustomPageBuildService;
import org.springframework.stereotype.Service;
@Service
public class CustomPageBuildServiceImpl extends ServiceImpl<CustomPageBuildMapper, CustomPageBuild>
implements ICustomPageBuildService {
}

View File

@@ -0,0 +1,13 @@
package com.xspaceagi.custompage.infra.dao.service.impl;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xspaceagi.custompage.infra.dao.entity.CustomPageConfig;
import com.xspaceagi.custompage.infra.dao.mapper.CustomPageConfigMapper;
import com.xspaceagi.custompage.infra.dao.service.ICustomPageConfigService;
@Service
public class CustomPageConfigServiceImpl extends ServiceImpl<CustomPageConfigMapper, CustomPageConfig>
implements ICustomPageConfigService {
}

View File

@@ -0,0 +1,13 @@
package com.xspaceagi.custompage.infra.dao.service.impl;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xspaceagi.custompage.infra.dao.entity.CustomPageConversation;
import com.xspaceagi.custompage.infra.dao.mapper.CustomPageConversationMapper;
import com.xspaceagi.custompage.infra.dao.service.ICustomPageConversationService;
@Service
public class CustomPageConversationServiceImpl extends ServiceImpl<CustomPageConversationMapper, CustomPageConversation>
implements ICustomPageConversationService {
}

View File

@@ -0,0 +1,13 @@
package com.xspaceagi.custompage.infra.dao.service.impl;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xspaceagi.custompage.infra.dao.entity.CustomPageDomain;
import com.xspaceagi.custompage.infra.dao.mapper.CustomPageDomainMapper;
import com.xspaceagi.custompage.infra.dao.service.ICustomPageDomainService;
@Service
public class CustomPageDomainServiceImpl extends ServiceImpl<CustomPageDomainMapper, CustomPageDomain>
implements ICustomPageDomainService {
}

View File

@@ -0,0 +1,19 @@
package com.xspaceagi.custompage.infra.dao.typehandler;
import java.util.List;
import com.fasterxml.jackson.core.type.TypeReference;
import com.xspaceagi.custompage.sdk.dto.DataSourceDto;
import com.xspaceagi.system.spec.common.ListJsonTypeHandler;
/**
* 数据源列表 TypeHandler
*/
public class DataSourceListTypeHandler extends ListJsonTypeHandler<DataSourceDto> {
@Override
protected TypeReference<List<DataSourceDto>> getTypeReference() {
return new TypeReference<List<DataSourceDto>>() {
};
}
}

View File

@@ -0,0 +1,16 @@
package com.xspaceagi.custompage.infra.dao.typehandler;
import java.util.List;
import com.fasterxml.jackson.core.type.TypeReference;
import com.xspaceagi.custompage.sdk.dto.PageArgConfig;
import com.xspaceagi.system.spec.common.ListJsonTypeHandler;
public class PageArgConfigListTypeHandler extends ListJsonTypeHandler<PageArgConfig> {
@Override
protected TypeReference<List<PageArgConfig>> getTypeReference() {
return new TypeReference<List<PageArgConfig>>() {
};
}
}

View File

@@ -0,0 +1,16 @@
package com.xspaceagi.custompage.infra.dao.typehandler;
import java.util.List;
import com.fasterxml.jackson.core.type.TypeReference;
import com.xspaceagi.custompage.sdk.dto.ProxyConfig;
import com.xspaceagi.system.spec.common.ListJsonTypeHandler;
public class ProxyConfigListTypeHandler extends ListJsonTypeHandler<ProxyConfig> {
@Override
protected TypeReference<List<ProxyConfig>> getTypeReference() {
return new TypeReference<List<ProxyConfig>>() {
};
}
}

View File

@@ -0,0 +1,19 @@
package com.xspaceagi.custompage.infra.dao.typehandler;
import java.util.List;
import com.fasterxml.jackson.core.type.TypeReference;
import com.xspaceagi.custompage.sdk.dto.VersionInfoDto;
import com.xspaceagi.system.spec.common.ListJsonTypeHandler;
/**
* 版本列表 TypeHandler
*/
public class VersionInfoListTypeHandler extends ListJsonTypeHandler<VersionInfoDto> {
@Override
protected TypeReference<List<VersionInfoDto>> getTypeReference() {
return new TypeReference<List<VersionInfoDto>>() {
};
}
}

View File

@@ -0,0 +1,455 @@
package com.xspaceagi.custompage.infra.proxy;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.xspaceagi.agent.core.adapter.application.PublishApplicationService;
import com.xspaceagi.agent.core.adapter.dto.PublishedDto;
import com.xspaceagi.agent.core.adapter.repository.entity.Published;
import com.xspaceagi.custompage.domain.model.CustomPageConfigModel;
import com.xspaceagi.custompage.infra.service.ProxyAuthService;
import com.xspaceagi.custompage.infra.service.ProxyConfigService;
import com.xspaceagi.custompage.infra.vo.BackendVo;
import com.xspaceagi.custompage.infra.vo.ProxyAuthVo;
import com.xspaceagi.custompage.sdk.dto.ProxyConfig;
import com.xspaceagi.system.application.dto.TenantConfigDto;
import com.xspaceagi.system.sdk.permission.IUserDataPermissionRpcService;
import com.xspaceagi.system.sdk.service.dto.UserDataPermissionDto;
import com.xspaceagi.system.spec.cache.SimpleJvmHashCache;
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.enums.YesOrNoEnum;
import com.xspaceagi.system.spec.exception.BizException;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import io.netty.util.CharsetUtil;
import io.netty.util.ReferenceCountUtil;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.SSLHandshakeException;
import java.io.InputStream;
import java.net.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class HttpProxyRequestHandler extends ChannelInboundHandlerAdapter {
private static final Logger logger = LoggerFactory.getLogger(HttpProxyRequestHandler.class);
private static final String PAGE_403_HTML = load403PageHtml();
private final Queue<Object> receivedLastMsgsWhenConnect = new LinkedList<>();
private final Bootstrap proxyClientBootstrap;
private final Bootstrap httpsProxyClientBootstrap;
private Channel targetChannel;
private final ProxyConfigService proxyConfigService;
private final ProxyAuthService proxyAuthService;
private final PublishApplicationService publishApplicationService;
private final IUserDataPermissionRpcService userDataPermissionRpcService;
public HttpProxyRequestHandler(Bootstrap proxyClientBootstrap, Bootstrap httpsProxyClientBootstrap, ProxyConfigService proxyConfigService,
ProxyAuthService proxyAuthService, PublishApplicationService publishApplicationService, IUserDataPermissionRpcService userDataPermissionRpcService) {
this.proxyClientBootstrap = proxyClientBootstrap;
this.httpsProxyClientBootstrap = httpsProxyClientBootstrap;
this.proxyConfigService = proxyConfigService;
this.proxyAuthService = proxyAuthService;
this.publishApplicationService = publishApplicationService;
this.userDataPermissionRpcService = userDataPermissionRpcService;
}
@Override
public void channelRead(final ChannelHandlerContext ctx, final Object msg) {
if (msg instanceof HttpRequest) {
DefaultHttpRequest request = (DefaultHttpRequest) msg;
String uri = request.uri() == null ? "/" : request.uri();
String token = null;
// 处理_ticket兑换token
if (uri != null && uri.contains("_ticket=")) {
Map<String, String> parseQueryString = parseQueryString(uri);
if (parseQueryString.containsKey("_ticket")) {
token = proxyAuthService.getTokenByTicket(parseQueryString.get("_ticket"));
if (token != null) {
request.headers().set("Authorization", "Bearer " + token);
ctx.channel().attr(ProxyServerContainer.tokenKey).set(token);
}
}
}
String basePath = null;
Long agentId = null;
Long pageId = null;
ProxyConfig.ProxyEnv env = null;
String realUri = "/";
boolean isPage = false;
boolean isCustomDomain = false;
// uri样式 /page/动态字符1-动态数字/动态字符2/xxxxxx 或 /page/动态字符1-动态数字/动态字符2可选斜杠
String pattern = "/page/(\\w+)(?:-(\\d+))?/(\\w+)/?(.*)";
Pattern regex = Pattern.compile(pattern);
Matcher matcher = regex.matcher(uri);
if (!matcher.matches()) {
CustomPageConfigModel configModel = proxyConfigService.queryCustomPageConfigByDomain(request.headers().get("Host"));
if (configModel != null) {
uri = "/page/" + configModel.getId() + "-" + configModel.getDevAgentId() + "/prod" + uri;
matcher = regex.matcher(uri);
isCustomDomain = true;
}
}
if (matcher.matches()) {
String pageIdStr = matcher.group(1);
basePath = "/" + pageIdStr;
try {
pageId = Long.parseLong(pageIdStr);
} catch (NumberFormatException e) {
// ignore pageId parse error
}
try {
if (matcher.group(2) != null) {
agentId = Long.parseLong(matcher.group(2));
isPage = true;
}
} catch (NumberFormatException e) {
// ignore
}
env = ProxyConfig.ProxyEnv.get(matcher.group(3));
String u = matcher.group(4);
if (u != null && !u.isEmpty()) {
realUri = realUri + u;
}
logger.debug("URL解析成功 - URI: {}, basePath: {}, agentId: {}, env: {}, realUri: {}",
uri, basePath, agentId, env, realUri);
} else {
logger.warn("URL解析失败不匹配正则表达式 - URI: {}", uri);
}
if (agentId == null) {
String refer = request.headers().get("Referer");
if (refer != null) {
matcher = regex.matcher(refer);
if (matcher.matches()) {
try {
if (matcher.group(2) != null) {
agentId = Long.parseLong(matcher.group(2));
}
} catch (NumberFormatException e) {
// ignore
}
}
}
}
BackendVo backendVo;
try {
if (!proxyAuthService.initTenantContext(agentId)) {
ReferenceCountUtil.release(msg);
writeErrorResponse(ctx, HttpResponseStatus.NOT_FOUND, "Agent Not Match");
return;
}
backendVo = proxyConfigService.selectBackend(basePath, realUri, env, agentId);
} catch (Exception e) {
RequestContext.remove();
ReferenceCountUtil.release(msg);
writeErrorResponse(ctx, HttpResponseStatus.INTERNAL_SERVER_ERROR,
"500 Internal Server Error (" + e.getMessage() + ")");
return;
}
if (backendVo == null) {
RequestContext.remove();
ReferenceCountUtil.release(msg);
writeErrorResponse(ctx, HttpResponseStatus.NOT_FOUND, "404 Not Found");
return;
}
request.setUri(backendVo.getUri());
String cookie = request.headers().get("Cookie");
String authorization = request.headers().get("Authorization");
request.headers().remove("Authorization");
// 对于API补充认证信息
// 从cookie中获取ticket认证信息
if (cookie != null && token == null) {
//去除多余的空内容
cookie = cookie.replace("ticket=;", "");
int start = cookie.indexOf("ticket=");
if (start >= 0) {
int end = cookie.indexOf(";", start);
if (end > 0) {
token = cookie.substring(start + 7, end);
} else {
token = cookie.substring(start + 7);
}
}
cookie = cookie.replace("ticket=" + token, "");
try {
request.headers().set("Cookie", cookie.trim());
} catch (Exception e) {
request.headers().remove("Cookie");
// ignore
}
}
if (token == null) {
if (authorization != null) {
token = authorization.replaceFirst("Basic", "").replaceFirst("Bearer", "").trim();
}
}
try {
ProxyAuthVo proxyAuthVo = proxyAuthService.getProxyAuthVo(token, agentId, env, backendVo);
// prod 环境页面访问权限校验:无权限返回一个无权限页面
if (env == ProxyConfig.ProxyEnv.prod && isPage && publishApplicationService != null) {
Long userId = proxyAuthVo.getUser() != null ? proxyAuthVo.getUser().getUserId() : null;
if (userId != null) {
PublishedDto published = publishApplicationService.queryPublished(Published.TargetType.Agent, agentId);
if (published != null && published.getAccessControl() != null && published.getAccessControl().equals(YesOrNoEnum.Y.getKey())) {
JSONObject jsonObject = JSON.parseObject(published.getConfig());
if (!userId.equals(jsonObject.getLong("creatorId"))) {
UserDataPermissionDto dp = userDataPermissionRpcService.getUserDataPermission(userId);
boolean hasPermission = dp != null && dp.getPageAgentIds() != null && dp.getPageAgentIds().contains(agentId);
if (!hasPermission) {
ReferenceCountUtil.release(msg);
write403Page(ctx);
return;
}
}
}
}
}
// 增加空间信息、用户角色、判读用户权限(没有权限时不能访问)
String loginInfo = JSON.toJSONString(proxyAuthVo);
String encodeBase64String = Base64.encodeBase64String(loginInfo.getBytes(Charset.forName("UTF-8")));
request.headers().set("Authorization", "Basic " + encodeBase64String);
} catch (Exception e) {
if ((e instanceof BizException) && ((BizException) e).getCode().equals(ErrorCodeEnum.UNAUTHORIZED_REDIRECT.getCode())
&& backendVo.isRequireAuth()) {
String redirectUrl = "/";
String referer = request.headers().get("Referer");
String baseUrl = "";
if (StringUtils.isNotBlank(referer) && !isCustomDomain) {
try {
URL url = new URL(referer);
baseUrl = url.getProtocol() + "://" + url.getHost() + (url.getPort() == 443 || url.getPort() == 80 || url.getPort() == -1 ? "" : ":" + url.getPort());
} catch (MalformedURLException ex) {
// ignore
}
} else {
TenantConfigDto tenantConfigDto = (TenantConfigDto) RequestContext.get().getTenantConfig();
baseUrl = tenantConfigDto.getSiteUrl() != null ? tenantConfigDto.getSiteUrl() : baseUrl;
baseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
}
if (isPage) {
redirectUrl = baseUrl + uri;
} else {
String fragment = request.headers().get("Fragment");
if (StringUtils.isNotBlank(referer) && StringUtils.isNotBlank(fragment)) {
redirectUrl = referer + "#" + fragment;
}
}
String location = baseUrl + "/login?redirect=" + URLEncoder.encode(redirectUrl, StandardCharsets.UTF_8);
ReqResult<Void> error = ReqResult.error(ErrorCodeEnum.UNAUTHORIZED_REDIRECT.getCode(), location);
FullHttpResponse resp = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.FOUND,
Unpooled.copiedBuffer(JSON.toJSONString(error), CharsetUtil.UTF_8));
resp.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json;charset=UTF-8");
resp.headers().set(HttpHeaderNames.LOCATION, location);
ReferenceCountUtil.release(msg);
ctx.channel().writeAndFlush(resp).addListener(ChannelFutureListener.CLOSE);
return;
} else if (!(e instanceof BizException)) {
if (!isPage) {
ReqResult<Void> error = ReqResult.error(ErrorCodeEnum.PERMISSION_DENIED.getCode(), e.getMessage());
FullHttpResponse resp = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK,
Unpooled.copiedBuffer(JSON.toJSONString(error), CharsetUtil.UTF_8));
resp.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json;charset=UTF-8");
ReferenceCountUtil.release(msg);
ctx.channel().writeAndFlush(resp).addListener(ChannelFutureListener.CLOSE);
return;
}
}
RequestContext.remove();
}
request.headers().set("Host", backendVo.getHost());
String remoteIp = ((InetSocketAddress) ctx.channel().remoteAddress()).getAddress().getHostAddress();
if (request.headers().get("X-Forwarded-For") == null) {
request.headers().set("X-Forwarded-For", remoteIp);
} else {
request.headers().set("X-Forwarded-For", request.headers().get("X-Forwarded-For") + "," + remoteIp);
}
if (request.headers().get("X-Real-IP") == null) {
request.headers().set("X-Real-IP", remoteIp);
}
if (targetChannel == null) {
ctx.channel().config().setOption(ChannelOption.AUTO_READ, false);
Bootstrap clientBootstrap = proxyClientBootstrap;
if (backendVo.getScheme().equals("https")) {
clientBootstrap = httpsProxyClientBootstrap;
}
String ip = resolveDns(backendVo.getHost());
clientBootstrap.connect(ip, backendVo.getPort()).addListener((ChannelFutureListener) future -> {
// 连接后端服务器成功
if (future.isSuccess()) {
future.channel().writeAndFlush(msg);
synchronized (receivedLastMsgsWhenConnect) {
Object msg0;
while ((msg0 = receivedLastMsgsWhenConnect.poll()) != null) {
future.channel().writeAndFlush(msg0);
}
targetChannel = future.channel();
targetChannel.attr(ProxyServerContainer.nextChannelAttributeKey).set(ctx.channel());
ctx.channel().attr(ProxyServerContainer.nextChannelAttributeKey).set(targetChannel);
}
String connection = request.headers().get("Connection");
String upgrade = request.headers().get("Upgrade");
// 协议升级走原生通道支持websocket
if ((connection != null && connection.equals("Upgrade")) || (upgrade != null && upgrade.equals("websocket"))) {
ctx.channel().pipeline().remove("httpServerCodec");
future.channel().pipeline().remove("httpClientCodec");
}
ctx.channel().config().setOption(ChannelOption.AUTO_READ, true);
} else {
ReferenceCountUtil.release(msg);
clearReceivedWhenConnect();
ctx.channel().config().setOption(ChannelOption.AUTO_READ, true);
writeErrorResponse(ctx, HttpResponseStatus.BAD_GATEWAY,
"502 Bad Gateway (" + backendVo.getHost() + ":" + backendVo.getPort() + ")");
}
});
} else {
targetChannel.writeAndFlush(msg);
}
// 移除上下文信息
RequestContext.remove();
} else {
if (targetChannel == null) {
synchronized (receivedLastMsgsWhenConnect) {
if (targetChannel == null) {
receivedLastMsgsWhenConnect.offer(msg);
} else {
targetChannel.writeAndFlush(msg);
}
}
} else {
targetChannel.writeAndFlush(msg);
}
}
}
private void writeErrorResponse(ChannelHandlerContext ctx, HttpResponseStatus httpResponseStatus, String message) {
FullHttpResponse resp = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, httpResponseStatus,
Unpooled.copiedBuffer("<html> <head><title>" + message + "</title></head> <body>" +
"<center><h1>" + message + "</h1></center> <hr><center>QIMING/0.1</center>" +
"</body></html>", CharsetUtil.UTF_8));
resp.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
ctx.channel().writeAndFlush(resp).addListener(ChannelFutureListener.CLOSE);
}
private static String load403PageHtml() {
try (InputStream in = HttpProxyRequestHandler.class.getResourceAsStream("/403.html")) {
if (in != null) {
return new String(in.readAllBytes(), StandardCharsets.UTF_8);
}
} catch (Exception e) {
logger.warn("加载 403 页面失败,将使用默认文案: {}", e.getMessage());
}
return "<!DOCTYPE html><html lang=\"zh-CN\"><head><meta charset=\"UTF-8\"><title>403 - 无权限访问</title></head><body><div style=\"text-align:center;padding:40px;\"><h1>403</h1><p>抱歉,您没有权限访问此页面</p></div></body></html>";
}
private void write403Page(ChannelHandlerContext ctx) {
FullHttpResponse resp = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.FORBIDDEN,
Unpooled.copiedBuffer(PAGE_403_HTML, CharsetUtil.UTF_8));
resp.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
ctx.channel().writeAndFlush(resp).addListener(ChannelFutureListener.CLOSE);
}
@Override
public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
if (targetChannel != null) {
targetChannel.config().setOption(ChannelOption.AUTO_READ, ctx.channel().isWritable());
}
super.channelWritabilityChanged(ctx);
}
/**
* 释放挂起消息(通道关闭/异常时避免泄漏)
*/
private void clearReceivedWhenConnect() {
synchronized (receivedLastMsgsWhenConnect) {
Object pending;
while ((pending = receivedLastMsgsWhenConnect.poll()) != null) {
ReferenceCountUtil.release(pending);
}
}
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
if (targetChannel != null) {
try {
targetChannel.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
} catch (Exception e) {
}
}
clearReceivedWhenConnect();
super.channelInactive(ctx);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
if (cause.getCause() != null && (cause.getCause() instanceof SSLHandshakeException)) {
logger.warn("SSLHandshakeException: {}", cause.getCause().getMessage());
return;
}
clearReceivedWhenConnect();
super.exceptionCaught(ctx, cause);
}
private static String resolveDns(String domain) {
Object dns = SimpleJvmHashCache.getHash("dns", domain);
if (dns != null) {
return dns.toString();
}
String ip = domain;
try {
InetAddress address = InetAddress.getByName(domain);
ip = address.getHostAddress();
} catch (Exception e) {
}
SimpleJvmHashCache.putHash("dns", domain, ip, 600);
return ip;
}
private static Map<String, String> parseQueryString(String url) {
Map<String, String> params = new HashMap<>();
try {
URI uri = new URI(url);
String queryString = uri.getQuery();
if (queryString != null && !queryString.isEmpty()) {
String[] pairs = queryString.split("&");
for (String pair : pairs) {
int idx = pair.indexOf("=");
if (idx > 0) {
String key = pair.substring(0, idx);
String value = pair.substring(idx + 1);
params.put(key, value);
}
}
}
} catch (URISyntaxException e) {
// 处理异常
logger.warn("Invalid URL: " + url);
}
return params;
}
}

View File

@@ -0,0 +1,62 @@
package com.xspaceagi.custompage.infra.proxy;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.handler.codec.http.DefaultHttpResponse;
import io.netty.util.ReferenceCountUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 处理服务端 channel.
*/
public class HttpProxyResponseHandler extends ChannelInboundHandlerAdapter {
private static Logger logger = LoggerFactory.getLogger(HttpProxyResponseHandler.class);
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
Channel nextChannel = ctx.channel().attr(ProxyServerContainer.nextChannelAttributeKey).get();
if (nextChannel != null) {
String token = nextChannel.attr(ProxyServerContainer.tokenKey).get();
if (token != null && (msg instanceof DefaultHttpResponse)) {
DefaultHttpResponse response = (DefaultHttpResponse) msg;
response.headers().set("Set-Cookie", "ticket=" + token + "; Path=/; HttpOnly; SameSite=None; Secure");
nextChannel.attr(ProxyServerContainer.tokenKey).set(null);
}
// 转发时由目标 channel 的 pipelineHttpObjectEncoder在编码完成后释放此处不再 release 避免重复释放
nextChannel.writeAndFlush(msg);
} else {
ReferenceCountUtil.release(msg);
}
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
if (ctx.channel().attr(ProxyServerContainer.nextChannelAttributeKey).get() != null) {
try {
ctx.channel().attr(ProxyServerContainer.nextChannelAttributeKey).get()
.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
} catch (Exception e) {
e.printStackTrace();
}
}
super.channelInactive(ctx);
}
@Override
public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
Channel channel = ctx.channel().attr(ProxyServerContainer.nextChannelAttributeKey).get();
if (channel != null) {
channel.config().setOption(ChannelOption.AUTO_READ, ctx.channel().isWritable());
}
super.channelWritabilityChanged(ctx);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
logger.error("exception caught", cause);
super.exceptionCaught(ctx, cause);
}
}

View File

@@ -0,0 +1,139 @@
package com.xspaceagi.custompage.infra.proxy;
import com.xspaceagi.agent.core.adapter.application.PublishApplicationService;
import com.xspaceagi.custompage.infra.service.ProxyAuthService;
import com.xspaceagi.custompage.infra.service.ProxyConfigService;
import com.xspaceagi.system.sdk.permission.IUserDataPermissionRpcService;
import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import io.netty.util.AttributeKey;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.net.ssl.SSLException;
@Component
public class ProxyServerContainer {
public static AttributeKey<Channel> nextChannelAttributeKey = AttributeKey.newInstance("nextChannelAttributeKey");
public static AttributeKey<String> tokenKey = AttributeKey.newInstance("tokenKey");
private static Logger logger = LoggerFactory.getLogger(ProxyServerContainer.class);
private volatile boolean started = false;
private NioEventLoopGroup serverBossGroup;
private NioEventLoopGroup serverWorkerGroup;
@Resource
private ProxyConfigService proxyConfigService;
@Resource
private ProxyAuthService proxyAuthService;
@Resource
private PublishApplicationService publishApplicationService;
@Resource
private IUserDataPermissionRpcService userDataPermissionRpcService;
@Value("${custom-page.http-proxy.host:0.0.0.0}")
private String httpProxyHost;
@Value("${custom-page.http-proxy.port:18082}")
private String httpProxyPort;
@PostConstruct
public void init() {
synchronized (ProxyServerContainer.class) {
if (!started) {
initHttpProxyServer();
started = true;
}
}
}
/**
* http反向代理
*/
private void initHttpProxyServer() {
serverBossGroup = new NioEventLoopGroup(2);
serverWorkerGroup = new NioEventLoopGroup(Runtime.getRuntime().availableProcessors() * 2);
Bootstrap proxyClientBootstrap = new Bootstrap();
SslContext context;
try {
context = SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE).build();
} catch (SSLException e) {
throw new RuntimeException(e);
}
proxyClientBootstrap.channel(NioSocketChannel.class);
proxyClientBootstrap.group(serverWorkerGroup).handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
ch.pipeline().addLast("httpClientCodec", new HttpClientCodec());
ch.pipeline().addLast(new HttpProxyResponseHandler());
}
});
Bootstrap proxySslClientBootstrap = new Bootstrap();
proxySslClientBootstrap.channel(NioSocketChannel.class);
proxySslClientBootstrap.group(serverWorkerGroup).handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
ch.pipeline().addLast(context.newHandler(ch.alloc()));
ch.pipeline().addLast("httpClientCodec", new HttpClientCodec());
ch.pipeline().addLast(new HttpProxyResponseHandler());
}
});
ServerBootstrap httpServerBootstrap = new ServerBootstrap();
httpServerBootstrap.group(serverBossGroup, serverWorkerGroup).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
logger.error("exceptionCaught", cause);
super.exceptionCaught(ctx, cause);
}
@Override
public void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("httpServerCodec", new HttpServerCodec());
pipeline.addLast("httpProxyRequestHandler", new HttpProxyRequestHandler(proxyClientBootstrap, proxySslClientBootstrap, proxyConfigService, proxyAuthService, publishApplicationService, userDataPermissionRpcService));
}
});
try {
String host = System.getProperty("http.proxy.host", httpProxyHost);
String port = System.getProperty("http.proxy.port", httpProxyPort);
httpServerBootstrap.bind(host, Integer.parseInt(port));
logger.info("http proxy server started at {}:{}", host, port);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@PreDestroy
private void destroy() {
serverBossGroup.shutdownGracefully();
serverWorkerGroup.shutdownGracefully();
}
}

View File

@@ -0,0 +1,195 @@
package com.xspaceagi.custompage.infra.repository;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import org.springframework.stereotype.Repository;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.xspaceagi.agent.core.sdk.IAgentRpcService;
import com.xspaceagi.custompage.domain.model.CustomPageBuildModel;
import com.xspaceagi.custompage.domain.repository.ICustomPageBuildRepository;
import com.xspaceagi.custompage.infra.dao.entity.CustomPageBuild;
import com.xspaceagi.custompage.infra.dao.service.ICustomPageBuildService;
import com.xspaceagi.custompage.infra.translator.ICustomPageBuildTranslator;
import com.xspaceagi.system.spec.common.UserContext;
import com.xspaceagi.system.spec.enums.YesOrNoEnum;
import com.xspaceagi.system.spec.enums.YnEnum;
import com.xspaceagi.system.spec.page.SuperPage;
import jakarta.annotation.Resource;
@Repository
public class CustomPageBuildRepositoryImpl implements ICustomPageBuildRepository {
@Resource
private ICustomPageBuildService customPageBuildService;
@Resource
private ICustomPageBuildTranslator customPageBuildTranslator;
@Resource
private IAgentRpcService agentRpcService;
@Override
public CustomPageBuildModel getByProjectId(Long projectId) {
CustomPageBuild entity = customPageBuildService.getOne(Wrappers.<CustomPageBuild>lambdaQuery()
.eq(CustomPageBuild::getProjectId, projectId)
.eq(CustomPageBuild::getYn, YnEnum.Y.getKey())
.last("limit 1"));
return customPageBuildTranslator.convertToModel(entity);
}
@Override
public void updateKeepAlive(CustomPageBuildModel model, UserContext userContext) {
LambdaUpdateWrapper<CustomPageBuild> wrapper = Wrappers.<CustomPageBuild>lambdaUpdate()
.eq(CustomPageBuild::getProjectId, model.getProjectId())
.eq(CustomPageBuild::getYn, YnEnum.Y.getKey())
.set(CustomPageBuild::getDevRunning, model.getDevRunning())
.set(CustomPageBuild::getLastKeepAliveTime, model.getLastKeepAliveTime());
if (userContext != null) {
wrapper.set(CustomPageBuild::getModifiedId, userContext.getUserId());
wrapper.set(CustomPageBuild::getModifiedName, userContext.getUserName());
}
// 根据 devRunning 的值来设置 dev_pid, dev_port
if (Objects.equals(model.getDevRunning(), YesOrNoEnum.N.getKey())) {
// 停止状态:清空相关字段
wrapper.set(CustomPageBuild::getDevPid, null);
wrapper.set(CustomPageBuild::getDevPort, null);
} else {
// 运行状态:更新相关字段
wrapper.set(CustomPageBuild::getDevPid, model.getDevPid());
wrapper.set(CustomPageBuild::getDevPort, model.getDevPort());
}
customPageBuildService.update(null, wrapper);
}
@Override
public void updateBuildStatus(Long projectId, Integer codeVersion, UserContext userContext) {
customPageBuildService.update(null,
Wrappers.<CustomPageBuild>lambdaUpdate()
.eq(CustomPageBuild::getProjectId, projectId)
.eq(CustomPageBuild::getYn, YnEnum.Y.getKey())
.set(CustomPageBuild::getBuildRunning, YesOrNoEnum.Y.getKey())
.set(CustomPageBuild::getBuildVersion, codeVersion)
.set(CustomPageBuild::getBuildTime, new Date())
.set(CustomPageBuild::getModifiedId, userContext.getUserId())
.set(CustomPageBuild::getModifiedName, userContext.getUserName()));
}
@Override
public void updateStopDevStatus(Long projectId, UserContext userContext) {
LambdaUpdateWrapper<CustomPageBuild> wrapper = Wrappers.<CustomPageBuild>lambdaUpdate()
.eq(CustomPageBuild::getProjectId, projectId)
.eq(CustomPageBuild::getYn, YnEnum.Y.getKey())
.set(CustomPageBuild::getDevRunning, YesOrNoEnum.N.getKey())
.set(CustomPageBuild::getDevPid, null)
.set(CustomPageBuild::getDevPort, null);
if (userContext != null) {
wrapper.set(CustomPageBuild::getModifiedId, userContext.getUserId());
wrapper.set(CustomPageBuild::getModifiedName, userContext.getUserName());
}
customPageBuildService.update(null, wrapper);
}
@Override
public Long add(CustomPageBuildModel model, UserContext userContext) {
CustomPageBuild entity = customPageBuildTranslator.convertToEntity(model);
customPageBuildService.save(entity);
return entity.getId();
}
@Override
public SuperPage<CustomPageBuildModel> pageQuery(CustomPageBuildModel model, Long current,
Long pageSize) {
Page<CustomPageBuild> page = new Page<>(current == null ? 1 : current,
pageSize == null ? 10 : pageSize);
var wrapper = Wrappers.<CustomPageBuild>lambdaQuery()
.eq(model.getSpaceId() != null, CustomPageBuild::getSpaceId, model.getSpaceId())
.eq(model.getBuildRunning() != null, CustomPageBuild::getBuildRunning,
model.getBuildRunning())
.eq(model.getCreatorId() != null, CustomPageBuild::getCreatorId, model.getCreatorId())
.eq(CustomPageBuild::getYn, YnEnum.Y.getKey())
.orderByDesc(CustomPageBuild::getCreated);
var iPage = customPageBuildService.page(page, wrapper);
var records = iPage.getRecords().stream()
.map(customPageBuildTranslator::convertToModel)
.toList();
return new SuperPage<>(iPage.getCurrent(), iPage.getSize(), iPage.getTotal(), records);
}
@Override
public List<CustomPageBuildModel> list(CustomPageBuildModel model) {
var wrapper = Wrappers.<CustomPageBuild>lambdaQuery()
.eq(model.getSpaceId() != null, CustomPageBuild::getSpaceId, model.getSpaceId())
.eq(model.getBuildRunning() != null, CustomPageBuild::getBuildRunning,
model.getBuildRunning())
.eq(model.getCreatorId() != null, CustomPageBuild::getCreatorId, model.getCreatorId())
.eq(CustomPageBuild::getYn, YnEnum.Y.getKey())
.orderByDesc(CustomPageBuild::getCreated);
List<CustomPageBuild> entites = customPageBuildService.list(wrapper);
if (entites == null || entites.isEmpty()) {
return null;
}
return entites.stream().map(customPageBuildTranslator::convertToModel).collect(Collectors.toList());
}
@Override
public List<CustomPageBuildModel> listByProjectIds(List<Long> projectIdList) {
var wrapper = Wrappers.<CustomPageBuild>lambdaQuery()
.in(CustomPageBuild::getProjectId, projectIdList)
.eq(CustomPageBuild::getYn, YnEnum.Y.getKey())
.orderByDesc(CustomPageBuild::getModified);
List<CustomPageBuild> entites = customPageBuildService.list(wrapper);
if (entites == null || entites.isEmpty()) {
return new ArrayList<>();
}
return entites.stream().map(customPageBuildTranslator::convertToModel).collect(Collectors.toList());
}
@Override
public void deleteByProjectId(Long projectId, UserContext userContext) {
customPageBuildService.remove(
Wrappers.<CustomPageBuild>lambdaUpdate().eq(CustomPageBuild::getProjectId, projectId));
// 逻辑删除
/*
* customPageBuildService.update(null, Wrappers.<CustomPageBuild>lambdaUpdate()
* .eq(CustomPageBuild::getProjectId, projectId)
* .eq(CustomPageBuild::getYn, YnEnum.Y.getKey())
* .set(CustomPageBuild::getYn, YnEnum.N.getKey())
* .set(CustomPageBuild::getDevRunning, YesOrNoEnum.N.getKey())
* .set(CustomPageBuild::getDevPid, null)
* .set(CustomPageBuild::getDevPort, null)
* .set(CustomPageBuild::getBuildRunning, YesOrNoEnum.N.getKey())
* .set(CustomPageBuild::getModifiedId, userContext.getUserId())
* .set(CustomPageBuild::getModifiedName, userContext.getUserName()));
*/
}
@Override
public List<CustomPageBuildModel> listByDevRunning(Integer devRunning) {
var list = customPageBuildService.list(Wrappers.<CustomPageBuild>lambdaQuery()
.eq(devRunning != null, CustomPageBuild::getDevRunning, devRunning)
.eq(CustomPageBuild::getYn, YnEnum.Y.getKey()));
return list.stream()
.map(customPageBuildTranslator::convertToModel)
.toList();
}
@Override
public void updateVersionInfo(CustomPageBuildModel updateModel, UserContext userContext) {
CustomPageBuild entity = customPageBuildTranslator.convertToEntity(updateModel);
if (userContext != null) {
entity.setModifiedId(userContext.getUserId());
entity.setModifiedName(userContext.getUserName());
}
customPageBuildService.updateById(entity);
}
}

View File

@@ -0,0 +1,172 @@
package com.xspaceagi.custompage.infra.repository;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import com.xspaceagi.system.spec.common.UserContext;
import com.xspaceagi.system.spec.enums.YesOrNoEnum;
import com.xspaceagi.system.spec.enums.YnEnum;
import com.xspaceagi.system.spec.page.SuperPage;
import org.springframework.stereotype.Repository;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.xspaceagi.custompage.domain.model.CustomPageConfigModel;
import com.xspaceagi.custompage.domain.repository.ICustomPageConfigRepository;
import com.xspaceagi.custompage.infra.dao.entity.CustomPageConfig;
import com.xspaceagi.custompage.infra.dao.service.ICustomPageConfigService;
import com.xspaceagi.custompage.infra.translator.ICustomPageConfigTranslator;
import com.xspaceagi.custompage.sdk.dto.PublishTypeEnum;
import jakarta.annotation.Resource;
@Repository
public class CustomPageConfigRepositoryImpl implements ICustomPageConfigRepository {
@Resource
private ICustomPageConfigService customPageConfigService;
@Resource
private ICustomPageConfigTranslator customPageConfigTranslator;
@Override
public List<CustomPageConfigModel> list(CustomPageConfigModel model) {
var wrapper = Wrappers.<CustomPageConfig>lambdaQuery()
.eq(model.getSpaceId() != null, CustomPageConfig::getSpaceId, model.getSpaceId())
.eq(model.getCreatorId() != null, CustomPageConfig::getCreatorId, model.getCreatorId())
.eq(model.getBuildRunning() != null, CustomPageConfig::getBuildRunning,
model.getBuildRunning())
.eq(model.getPublishType() != null, CustomPageConfig::getPublishType,
model.getPublishType())
.eq(CustomPageConfig::getYn, YnEnum.Y.getKey())
.orderByDesc(CustomPageConfig::getModified);
List<CustomPageConfig> entities = customPageConfigService.list(wrapper);
if (entities == null || entities.isEmpty()) {
return null;
}
return entities.stream().map(customPageConfigTranslator::convertToModel).collect(Collectors.toList());
}
@Override
public SuperPage<CustomPageConfigModel> pageQuery(CustomPageConfigModel model, Long current, Long pageSize) {
Page<CustomPageConfig> page = new Page<>(current == null ? 1 : current,
pageSize == null ? 10 : pageSize);
var wrapper = Wrappers.<CustomPageConfig>lambdaQuery()
.eq(model.getSpaceId() != null, CustomPageConfig::getSpaceId, model.getSpaceId())
.eq(model.getCreatorId() != null,
CustomPageConfig::getCreatorId, model.getCreatorId())
.eq(model.getBuildRunning() != null,
CustomPageConfig::getBuildRunning, model.getBuildRunning())
.eq(CustomPageConfig::getYn, YnEnum.Y.getKey())
.orderByDesc(CustomPageConfig::getModified);
var iPage = customPageConfigService.page(page, wrapper);
var records = iPage.getRecords().stream()
.map(customPageConfigTranslator::convertToModel)
.toList();
return new SuperPage<>(iPage.getCurrent(), iPage.getSize(), iPage.getTotal(), records);
}
@Override
public CustomPageConfigModel getById(Long id) {
CustomPageConfig entity = customPageConfigService.getOne(Wrappers.<CustomPageConfig>lambdaQuery()
.eq(CustomPageConfig::getId, id)
.eq(CustomPageConfig::getYn, YnEnum.Y.getKey())
.last("limit 1"));
return customPageConfigTranslator.convertToModel(entity);
}
@Override
public CustomPageConfigModel getByAgentId(Long agentId) {
CustomPageConfig entity = customPageConfigService.getOne(Wrappers.<CustomPageConfig>lambdaQuery()
.eq(CustomPageConfig::getDevAgentId, agentId)
.eq(CustomPageConfig::getYn, YnEnum.Y.getKey())
.last("limit 1"));
return customPageConfigTranslator.convertToModel(entity);
}
@Override
public CustomPageConfigModel getByBasePath(String basePath) {
CustomPageConfig entity = customPageConfigService.getOne(Wrappers.<CustomPageConfig>lambdaQuery()
.eq(CustomPageConfig::getBasePath, basePath)
.eq(CustomPageConfig::getYn, YnEnum.Y.getKey())
.last("limit 1"));
return customPageConfigTranslator.convertToModel(entity);
}
@Override
public Long add(CustomPageConfigModel model, UserContext userContext) {
CustomPageConfig entity = customPageConfigTranslator.convertToEntity(model);
customPageConfigService.save(entity);
return entity.getId();
}
@Override
public void updateById(CustomPageConfigModel model, UserContext userContext) {
CustomPageConfig updateEntity = customPageConfigTranslator.convertToEntity(model);
customPageConfigService.update(updateEntity, Wrappers.<CustomPageConfig>lambdaUpdate()
.eq(CustomPageConfig::getId, model.getId())
.eq(CustomPageConfig::getYn, YnEnum.Y.getKey()));
}
@Override
public void updateBuildStatus(Long projectId, PublishTypeEnum publishTypeEnum, UserContext userContext) {
customPageConfigService.update(null, Wrappers.<CustomPageConfig>lambdaUpdate()
.eq(CustomPageConfig::getId, projectId)
.eq(CustomPageConfig::getYn, YnEnum.Y.getKey())
.set(CustomPageConfig::getBuildRunning, YesOrNoEnum.Y.getKey())
.set(CustomPageConfig::getPublishType, publishTypeEnum)
.set(CustomPageConfig::getModifiedId, userContext.getUserId())
.set(CustomPageConfig::getModifiedName, userContext.getUserName()));
}
@Override
public void deleteById(Long projectId, UserContext userContext) {
customPageConfigService.removeById(projectId);
// 逻辑删除
/*
* customPageConfigService.update(null,
* Wrappers.<CustomPageConfig>lambdaUpdate()
* .eq(CustomPageConfig::getId, projectId)
* .eq(CustomPageConfig::getYn, YnEnum.Y.getKey())
* .set(CustomPageConfig::getYn, YnEnum.N.getKey())
* .set(CustomPageConfig::getBuildRunning, YesOrNoEnum.N.getKey())
* .set(CustomPageConfig::getModifiedId, userContext.getUserId())
* .set(CustomPageConfig::getModifiedName, userContext.getUserName()));
*/
}
@Override
public List<CustomPageConfigModel> listByDevAgentIds(List<Long> devAgentIds) {
if (devAgentIds == null || devAgentIds.isEmpty()) {
return new ArrayList<>();
}
var wrapper = Wrappers.<CustomPageConfig>lambdaQuery()
.in(CustomPageConfig::getDevAgentId, devAgentIds)
.eq(CustomPageConfig::getYn, YnEnum.Y.getKey());
List<CustomPageConfig> entities = customPageConfigService.list(wrapper);
if (entities == null || entities.isEmpty()) {
return new ArrayList<>();
}
return entities.stream().map(customPageConfigTranslator::convertToModel).collect(Collectors.toList());
}
@Override
public List<CustomPageConfigModel> listByIds(List<Long> ids) {
var wrapper = Wrappers.<CustomPageConfig>lambdaQuery()
.in(CustomPageConfig::getId, ids)
.eq(CustomPageConfig::getYn, YnEnum.Y.getKey());
List<CustomPageConfig> entities = customPageConfigService.list(wrapper);
if (entities == null || entities.isEmpty()) {
return new ArrayList<>();
}
return entities.stream().map(customPageConfigTranslator::convertToModel).collect(Collectors.toList());
}
@Override
public Long countTotalPages() {
return customPageConfigService.count();
}
}

View File

@@ -0,0 +1,220 @@
package com.xspaceagi.custompage.infra.repository;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.stereotype.Repository;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.xspaceagi.custompage.domain.model.CustomPageConversationModel;
import com.xspaceagi.custompage.domain.repository.ICustomPageConversationRepository;
import com.xspaceagi.custompage.infra.dao.entity.CustomPageConversation;
import com.xspaceagi.custompage.infra.dao.service.ICustomPageConversationService;
import com.xspaceagi.custompage.infra.translator.ICustomPageConversationTranslator;
import com.xspaceagi.system.spec.common.UserContext;
import com.xspaceagi.system.spec.enums.YnEnum;
import com.xspaceagi.system.spec.page.SuperPage;
import jakarta.annotation.Resource;
@Repository
public class CustomPageConversationRepositoryImpl implements ICustomPageConversationRepository {
@Resource
private ICustomPageConversationService customPageConversationService;
@Resource
private ICustomPageConversationTranslator customPageConversationTranslator;
@Override
public Long save(CustomPageConversationModel model, UserContext userContext) {
if (model == null) {
throw new IllegalArgumentException("model is required");
}
if (userContext == null) {
throw new IllegalArgumentException("userContext is required");
}
CustomPageConversation entity = customPageConversationTranslator.convertToEntity(model);
entity.setCreatorId(userContext.getUserId());
entity.setCreatorName(userContext.getUserName());
entity.setTenantId(userContext.getTenantId());
entity.setSpaceId(model.getSpaceId());
entity.setYn(YnEnum.Y.getKey());
customPageConversationService.save(entity);
return entity.getId();
}
@Override
public List<CustomPageConversationModel> listByProjectId(Long projectId, Long userId) {
var wrapper = Wrappers.<CustomPageConversation>lambdaQuery()
.eq(CustomPageConversation::getProjectId, projectId)
// 不过滤用户
// .eq(CustomPageConversation::getCreatorId, userId)
.eq(CustomPageConversation::getYn, YnEnum.Y.getKey())
.orderByAsc(CustomPageConversation::getCreated);
List<CustomPageConversation> entities = customPageConversationService.list(wrapper);
if (entities == null || entities.isEmpty()) {
return null;
}
return entities.stream()
.map(customPageConversationTranslator::convertToModel)
.collect(Collectors.toList());
}
@Override
public SuperPage<CustomPageConversationModel> pageQuery(CustomPageConversationModel queryModel, Long current,
Long pageSize) {
if (queryModel == null) {
throw new IllegalArgumentException("queryModel is required");
}
if (current == null || current <= 0) {
throw new IllegalArgumentException("current is required or invalid");
}
if (pageSize == null || pageSize <= 0) {
throw new IllegalArgumentException("pageSize is required or invalid");
}
var wrapper = Wrappers.<CustomPageConversation>lambdaQuery()
.eq(queryModel.getProjectId() != null, CustomPageConversation::getProjectId, queryModel.getProjectId())
// .eq(queryModel.getCreatorId() != null, CustomPageConversation::getCreatorId,
// queryModel.getCreatorId())
.eq(CustomPageConversation::getYn, YnEnum.Y.getKey())
.orderByDesc(CustomPageConversation::getCreated); // 倒序查询
Page<CustomPageConversation> page = new Page<>(current, pageSize);
Page<CustomPageConversation> resultPage = customPageConversationService.page(page, wrapper);
List<CustomPageConversationModel> modelList = new ArrayList<>();
if (resultPage.getRecords() != null && !resultPage.getRecords().isEmpty()) {
modelList = resultPage.getRecords().stream()
.map(customPageConversationTranslator::convertToModel)
.collect(Collectors.toList());
}
SuperPage<CustomPageConversationModel> superPage = new SuperPage<>(current, pageSize, resultPage.getTotal());
superPage.setRecords(modelList);
return superPage;
}
@Override
public CustomPageConversationModel findLatestUserBySessionId(Long projectId, String sessionId) {
if (projectId == null || sessionId == null || sessionId.isBlank()) {
return null;
}
var wrapper = Wrappers.<CustomPageConversation>lambdaQuery()
.eq(CustomPageConversation::getProjectId, projectId)
.eq(CustomPageConversation::getSessionId, sessionId)
.eq(CustomPageConversation::getRole, "USER")
.eq(CustomPageConversation::getYn, YnEnum.Y.getKey())
.orderByDesc(CustomPageConversation::getCreated)
.last("LIMIT 1");
CustomPageConversation entity = customPageConversationService.getOne(wrapper, false);
if (entity == null) {
return null;
}
return customPageConversationTranslator.convertToModel(entity);
}
@Override
public CustomPageConversationModel findLatestUserByProjectId(Long projectId) {
if (projectId == null) {
return null;
}
var wrapper = Wrappers.<CustomPageConversation>lambdaQuery()
.eq(CustomPageConversation::getProjectId, projectId)
.eq(CustomPageConversation::getRole, "USER")
.eq(CustomPageConversation::getYn, YnEnum.Y.getKey())
.orderByDesc(CustomPageConversation::getCreated)
.last("LIMIT 1");
CustomPageConversation entity = customPageConversationService.getOne(wrapper, false);
if (entity == null) {
return null;
}
return customPageConversationTranslator.convertToModel(entity);
}
@Override
public CustomPageConversationModel findAssistantByProjectIdAndRequestId(Long projectId, String requestId) {
if (projectId == null || requestId == null || requestId.isBlank()) {
return null;
}
var wrapper = Wrappers.<CustomPageConversation>lambdaQuery()
.eq(CustomPageConversation::getProjectId, projectId)
.eq(CustomPageConversation::getRequestId, requestId)
.eq(CustomPageConversation::getRole, "ASSISTANT")
.eq(CustomPageConversation::getYn, YnEnum.Y.getKey())
.orderByDesc(CustomPageConversation::getCreated)
.last("LIMIT 1");
CustomPageConversation entity = customPageConversationService.getOne(wrapper, false);
if (entity == null) {
return null;
}
return customPageConversationTranslator.convertToModel(entity);
}
@Override
public CustomPageConversationModel findById(Long id) {
if (id == null) {
return null;
}
CustomPageConversation entity = customPageConversationService.getById(id);
if (entity == null || !YnEnum.Y.getKey().equals(entity.getYn())) {
return null;
}
return customPageConversationTranslator.convertToModel(entity);
}
@Override
public boolean updateAssistantContent(Long id, String content, String requestId, UserContext userContext) {
if (id == null || content == null || content.isBlank()) {
return false;
}
CustomPageConversation updateEntity = new CustomPageConversation();
updateEntity.setId(id);
updateEntity.setContent(content);
if (requestId != null && !requestId.isBlank()) {
updateEntity.setRequestId(requestId);
}
if (userContext != null) {
updateEntity.setModifiedId(userContext.getUserId());
updateEntity.setModifiedName(userContext.getUserName());
}
return customPageConversationService.updateById(updateEntity);
}
@Override
public boolean updateUserSessionIdByRequestId(Long projectId, String requestId, String sessionId, Long userId) {
if (projectId == null || requestId == null || requestId.isBlank() || sessionId == null || sessionId.isBlank()
|| userId == null) {
return false;
}
var wrapper = Wrappers.<CustomPageConversation>lambdaUpdate()
.eq(CustomPageConversation::getProjectId, projectId)
.eq(CustomPageConversation::getRequestId, requestId)
.eq(CustomPageConversation::getRole, "USER")
.eq(CustomPageConversation::getCreatorId, userId)
.eq(CustomPageConversation::getYn, YnEnum.Y.getKey());
CustomPageConversation updateEntity = new CustomPageConversation();
updateEntity.setSessionId(sessionId);
return customPageConversationService.update(updateEntity, wrapper);
}
@Override
public boolean deleteByProjectId(Long projectId, Long userId) {
if (projectId == null || projectId <= 0) {
return false;
}
var wrapper = Wrappers.<CustomPageConversation>lambdaQuery()
.eq(CustomPageConversation::getProjectId, projectId);
return customPageConversationService.remove(wrapper);
}
}

View File

@@ -0,0 +1,85 @@
package com.xspaceagi.custompage.infra.repository;
import java.util.List;
import java.util.stream.Collectors;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.springframework.stereotype.Repository;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.xspaceagi.custompage.domain.model.CustomPageDomainModel;
import com.xspaceagi.custompage.domain.repository.ICustomPageDomainRepository;
import com.xspaceagi.custompage.infra.dao.entity.CustomPageDomain;
import com.xspaceagi.custompage.infra.dao.service.ICustomPageDomainService;
import com.xspaceagi.custompage.infra.translator.ICustomPageDomainTranslator;
import jakarta.annotation.Resource;
@Repository
public class CustomPageDomainRepositoryImpl implements ICustomPageDomainRepository {
@Resource
private ICustomPageDomainService customPageDomainService;
@Resource
private ICustomPageDomainTranslator customPageDomainTranslator;
@Override
public List<CustomPageDomainModel> listByProjectId(Long projectId) {
var wrapper = Wrappers.<CustomPageDomain>lambdaQuery()
.eq(CustomPageDomain::getProjectId, projectId)
.orderByDesc(CustomPageDomain::getCreated);
List<CustomPageDomain> entities = customPageDomainService.list(wrapper);
if (entities == null || entities.isEmpty()) {
return List.of();
}
return entities.stream()
.map(customPageDomainTranslator::convertToModel)
.collect(Collectors.toList());
}
@Override
public CustomPageDomainModel getById(Long id) {
CustomPageDomain entity = customPageDomainService.getById(id);
return customPageDomainTranslator.convertToModel(entity);
}
@Override
public CustomPageDomainModel getByDomain(String domain) {
CustomPageDomain entity = customPageDomainService.getOne(
Wrappers.<CustomPageDomain>lambdaQuery()
.eq(CustomPageDomain::getDomain, domain)
.last("limit 1")
);
return customPageDomainTranslator.convertToModel(entity);
}
@Override
public CustomPageDomainModel add(CustomPageDomainModel model) {
CustomPageDomain entity = customPageDomainTranslator.convertToEntity(model);
customPageDomainService.save(entity);
return customPageDomainTranslator.convertToModel(entity);
}
@Override
public void updateById(CustomPageDomainModel model) {
CustomPageDomain entity = customPageDomainTranslator.convertToEntity(model);
customPageDomainService.updateById(entity);
}
@Override
public void removeById(Long id) {
customPageDomainService.removeById(id);
}
@Override
public List<String> listAllDomains() {
LambdaQueryWrapper<CustomPageDomain> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.select(CustomPageDomain::getDomain, CustomPageDomain::getProjectId);
return customPageDomainService.list()
.stream()
.map(CustomPageDomain::getDomain)
.collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,20 @@
//package com.xspaceagi.custompage.infra.service;
//
//import com.xspaceagi.custompage.infra.dao.entity.CustomPageConfig;
//import org.springframework.stereotype.Service;
//
//import java.util.ArrayList;
//
//@Service
//public class CustomPageConfigService {
//
// public CustomPageConfig queryCustomPageConfig(String basePath) {
// CustomPageConfig customPageConfig = new CustomPageConfig();
// customPageConfig.setName("测试");
// customPageConfig.setDescription("测试");
// customPageConfig.setBasePath(basePath);
// customPageConfig.setProjectType(CustomPageConfig.ProjectType.ONLINE_DEPLOY);
// customPageConfig.setProxyConfigs(new ArrayList<>());
// return customPageConfig;
// }
//}

View File

@@ -0,0 +1,191 @@
package com.xspaceagi.custompage.infra.service;
import com.xspaceagi.agent.core.adapter.application.AgentApplicationService;
import com.xspaceagi.agent.core.adapter.application.ConversationApplicationService;
import com.xspaceagi.agent.core.sdk.IAgentRpcService;
import com.xspaceagi.agent.core.sdk.dto.AgentInfoDto;
import com.xspaceagi.agent.core.sdk.dto.AgentPublishedPermissionDto;
import com.xspaceagi.agent.core.sdk.dto.ReqResult;
import com.xspaceagi.custompage.infra.vo.BackendVo;
import com.xspaceagi.custompage.infra.vo.ProxyAuthVo;
import com.xspaceagi.custompage.sdk.dto.ProxyConfig;
import com.xspaceagi.custompage.sdk.dto.PublishTypeEnum;
import com.xspaceagi.system.application.dto.*;
import com.xspaceagi.system.application.service.AuthService;
import com.xspaceagi.system.application.service.SpaceApplicationService;
import com.xspaceagi.system.application.service.TenantConfigApplicationService;
import com.xspaceagi.system.infra.dao.entity.SpaceUser;
import com.xspaceagi.system.infra.dao.entity.User;
import com.xspaceagi.system.spec.cache.SimpleJvmHashCache;
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.tenant.thread.TenantFunctions;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ProxyAuthService {
@Resource
private AuthService authService;
@Resource
private IAgentRpcService agentRpcService;
@Resource
private SpaceApplicationService spaceApplicationService;
@Resource
private TenantConfigApplicationService tenantConfigApplicationService;
@Resource
private AgentApplicationService agentApplicationService;
@Resource
private ConversationApplicationService conversationApplicationService;
public boolean initTenantContext(Long agentId) {
if (agentId == null) {
return false;
}
// 避免一次页面访问里面的资源文件多次调用接口
AgentInfoDto agentInfoDto = (AgentInfoDto) SimpleJvmHashCache.getHash("proxy.initTenantContext", agentId.toString());
if (agentInfoDto == null) {
synchronized (this) {
if (agentInfoDto == null) {
ReqResult<List<AgentInfoDto>> listReqResult = TenantFunctions
.callWithIgnoreCheck(() -> agentRpcService.queryAgentInfoList(List.of(agentId)));
if (!listReqResult.isSuccess() || listReqResult.getData() == null || listReqResult.getData().isEmpty()) {
return false;
}
agentInfoDto = listReqResult.getData().get(0);
SimpleJvmHashCache.putHash("proxy.initTenantContext", agentId.toString(), agentInfoDto, 10);
}
}
}
TenantConfigDto tenantConfig = (TenantConfigDto) SimpleJvmHashCache.getHash("proxy.initTenantContext.config", "tenantConfig");
if (tenantConfig == null) {
synchronized (this) {
if (tenantConfig == null) {
tenantConfig = tenantConfigApplicationService.getTenantConfig(agentInfoDto.getTenantId());
SimpleJvmHashCache.putHash("proxy.initTenantContext.config", "tenantConfig", tenantConfig, 10);
}
}
}
RequestContext requestContext = new RequestContext<>();
requestContext.setTenantId(agentInfoDto.getTenantId());
requestContext.setTenantConfig(tenantConfig);
RequestContext.set(requestContext);
return true;
}
public ProxyAuthVo getProxyAuthVo(String token, Long agentId, ProxyConfig.ProxyEnv env, BackendVo backendVo) {
String key = "proxy_auth_vo:" + token;
Object proxyAuthVoCached = SimpleJvmHashCache.getHash("proxy_auth_vo", key);
if (proxyAuthVoCached != null) {
return (ProxyAuthVo) proxyAuthVoCached;
}
ProxyAuthVo proxyAuthVo;
synchronized (this) {
proxyAuthVoCached = SimpleJvmHashCache.getHash("proxy_auth_vo", key);
if (proxyAuthVoCached != null) {
return (ProxyAuthVo) proxyAuthVoCached;
}
proxyAuthVo = buildProxyAuthVo(token, agentId, env);
if (env == ProxyConfig.ProxyEnv.prod && backendVo.getPublishType() == PublishTypeEnum.AGENT) {
// 添加最近使用
agentApplicationService.addOrUpdateRecentUsed(proxyAuthVo.getUser().getUserId(), backendVo.getDevAgentId());
// 使用人数增加
conversationApplicationService.createConversationForPageApp(proxyAuthVo.getUser().getUserId(), backendVo.getDevAgentId());
}
}
// 内存缓存10秒避免短时间重复查询
SimpleJvmHashCache.putHash("proxy_auth_vo", key, proxyAuthVo, 10);
return proxyAuthVo;
}
private ProxyAuthVo buildProxyAuthVo(String token, Long agentId, ProxyConfig.ProxyEnv env) {
ReqResult<List<AgentInfoDto>> listReqResult = TenantFunctions
.callWithIgnoreCheck(() -> agentRpcService.queryAgentInfoList(List.of(agentId)));
if (!listReqResult.isSuccess() || listReqResult.getData() == null || listReqResult.getData().isEmpty()) {
throw new IllegalArgumentException("Error agentId");
}
AgentInfoDto agentInfoDto = listReqResult.getData().get(0);
UserDto loginUserInfo = authService.getLoginUserInfo(token);
if (loginUserInfo == null) {
throw BizException.of(ErrorCodeEnum.UNAUTHORIZED_REDIRECT, BizExceptionCodeEnum.customPageProxyAuthFailed);
}
RequestContext.get().setUserId(loginUserInfo.getId());
RequestContext.get().setUser(loginUserInfo);
TenantDto tenantDto = tenantConfigApplicationService.queryTenantById(agentInfoDto.getTenantId());
SpaceDto spaceDto = spaceApplicationService.queryById(agentInfoDto.getSpaceId());
if (spaceDto == null) {
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.customPageProxyNoSpace);
}
SpaceUserDto spaceUserDto = spaceApplicationService.querySpaceUser(agentInfoDto.getSpaceId(),
loginUserInfo.getId());
if (env == ProxyConfig.ProxyEnv.prod) {
ReqResult<AgentPublishedPermissionDto> agentPublishedPermissionDtoReqResult = agentRpcService
.queryAgentPublishedPermission(agentId);
if (!agentPublishedPermissionDtoReqResult.getData().isView()) {
throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.permissionDenied);
}
} else {
if (spaceUserDto == null && loginUserInfo.getRole() != User.Role.Admin) {
throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.permissionDenied);
}
}
ProxyAuthVo.Role role = ProxyAuthVo.Role.User;
if (spaceUserDto != null) {
if (spaceUserDto.getRole() == SpaceUser.Role.Owner || spaceUserDto.getRole() == SpaceUser.Role.Admin) {
role = ProxyAuthVo.Role.SpaceAdmin;
} else if (loginUserInfo.getId().equals(agentInfoDto.getCreatorId())) {
role = ProxyAuthVo.Role.AgentCreator;
} else {
role = ProxyAuthVo.Role.SpaceUser;
}
}
ProxyAuthVo.Agent agent = ProxyAuthVo.Agent.builder()
.agentId(agentInfoDto.getId())
.name(agentInfoDto.getName())
.icon(agentInfoDto.getIcon())
.build();
ProxyAuthVo.Space space = ProxyAuthVo.Space.builder()
.spaceId(spaceDto.getId())
.name(spaceDto.getName())
.icon(spaceDto.getIcon())
.build();
ProxyAuthVo.User user = ProxyAuthVo.User.builder()
.userId(loginUserInfo.getId())
.userName(loginUserInfo.getUserName())
.nickName(loginUserInfo.getNickName())
.uid(loginUserInfo.getUid())
.avatar(loginUserInfo.getAvatar())
.role(role)
.build();
ProxyAuthVo proxyAuthVo = new ProxyAuthVo();
proxyAuthVo.setUser(user);
proxyAuthVo.setAgent(agent);
proxyAuthVo.setSpace(space);
proxyAuthVo.setTenant(new ProxyAuthVo.Tenant(tenantDto.getId(), tenantDto.getName(), tenantDto.getDescription()));
return proxyAuthVo;
}
public String getTokenByTicket(String ticket) {
return authService.getTokenByTicket(ticket);
}
}

View File

@@ -0,0 +1,372 @@
package com.xspaceagi.custompage.infra.service;
import com.xspaceagi.custompage.domain.model.CustomPageBuildModel;
import com.xspaceagi.custompage.domain.model.CustomPageConfigModel;
import com.xspaceagi.custompage.domain.model.CustomPageDomainModel;
import com.xspaceagi.custompage.domain.repository.ICustomPageBuildRepository;
import com.xspaceagi.custompage.domain.repository.ICustomPageConfigRepository;
import com.xspaceagi.custompage.domain.repository.ICustomPageDomainRepository;
import com.xspaceagi.custompage.infra.dao.entity.CustomPageConfig;
import com.xspaceagi.custompage.infra.translator.ICustomPageConfigTranslator;
import com.xspaceagi.custompage.infra.vo.BackendVo;
import com.xspaceagi.custompage.sdk.dto.ProjectType;
import com.xspaceagi.custompage.sdk.dto.ProxyConfig;
import com.xspaceagi.custompage.sdk.dto.ProxyConfigBackend;
import com.xspaceagi.sandbox.sdk.server.ISandboxConfigRpcService;
import com.xspaceagi.sandbox.sdk.service.dto.SandboxConfigRpcDto;
import com.xspaceagi.sandbox.sdk.service.dto.SandboxConfigValue;
import com.xspaceagi.system.spec.cache.SimpleJvmHashCache;
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.jackson.JsonSerializeUtil;
import com.xspaceagi.system.spec.tenant.thread.TenantFunctions;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@Service
public class ProxyConfigService {
@Resource
private ICustomPageConfigRepository customPageConfigRepository;
@Resource
private ICustomPageConfigTranslator customPageConfigTranslator;
@Resource
private ICustomPageBuildRepository customPageBuildRepository;
@Resource
private ICustomPageDomainRepository customPageDomainRepository;
@Resource
private ISandboxConfigRpcService sandboxConfigRpcService;
@Value("${custom-page.dev-server-host}")
private String customPageDevServerHost;
@Value("${custom-page.prod-server-host}")
private String customPageProdServerHost;
@Value("${custom-page.docker-proxy.base-url}")
private String customPageDockerProxyBaseUrl;
// 启动 docker 代理
@Value("${custom-page.docker-proxy.enable}")
private boolean enableDockerProxy;
private static final Integer DEFAULT_NGINX_PORT = 8099;
public BackendVo selectBackend(String basePath, String realUri, ProxyConfig.ProxyEnv env, Long agentId) {
log.debug("select Backend start - base Path: {}, real Uri: {}, env: {}", basePath, realUri, env);
if (env == null) {
log.warn("select Backend failed - env is null");
return null;
}
CustomPageConfig customPageConfig = (CustomPageConfig) SimpleJvmHashCache.getHash(CustomPageConfig.class.getName(), basePath);
if (customPageConfig == null) {
synchronized (this) {
if (customPageConfig == null) {
CustomPageConfigModel customPageConfigModel = customPageConfigRepository.getByBasePath(basePath);
log.debug("query config result - base Path: {}, config exists: {}", basePath, customPageConfigModel != null);
customPageConfig = customPageConfigTranslator.convertToEntity(customPageConfigModel);
if (customPageConfig == null) {
log.warn("select Backend failed - base Path not found: {} config", basePath);
return null;
}
if (env == ProxyConfig.ProxyEnv.prod) {
SimpleJvmHashCache.putHash(CustomPageConfig.class.getName(), basePath, customPageConfig, 10);
}
}
}
}
//下面逻辑中有对customPageConfig的操作避免影响其他请求
customPageConfig = (CustomPageConfig) JsonSerializeUtil.deepCopy(customPageConfig);
log.debug("found config - ID: {}, project : {}", customPageConfig.getId(), customPageConfig.getProjectType());
if (customPageConfig.getProxyConfigs() == null) {
customPageConfig.setProxyConfigs(new ArrayList<>());
}
Integer devPort = null;
if (customPageConfig.getProjectType() == ProjectType.ONLINE_DEPLOY) {
// 删除误配的根目录
customPageConfig.getProxyConfigs().removeIf(proxyConfig -> proxyConfig.getPath().equals("/"));
if (env == ProxyConfig.ProxyEnv.dev) {
CustomPageBuildModel customPageBuildModel = (CustomPageBuildModel) SimpleJvmHashCache.getHash(CustomPageBuildModel.class.getName(), customPageConfig.getId().toString());
if (customPageBuildModel == null) {
synchronized (this) {
if (customPageBuildModel == null) {
customPageBuildModel = customPageBuildRepository.getByProjectId(customPageConfig.getId());
if (customPageBuildModel == null) {
log.warn("select Backend failed - project ID not found: {}", customPageConfig.getId());
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.customPageProxyProjectBuildNotFound);
// return null;
}
if (customPageBuildModel.getDevPort() == null) {
log.warn("select Backend failed - project dev service not started, projectId:{}", customPageConfig.getId());
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.customPageProxyDevServerNotStarted);
// return null;
}
}
}
}
devPort = customPageBuildModel.getDevPort();
SandboxConfigValue sandboxConfigValue = loadSandboxConfigValue(customPageConfig);
String devUrl = "";
if (enableDockerProxy) {
if (sandboxConfigValue != null && sandboxConfigValue.getVncPort() > 0) {
devUrl = sandboxConfigValue.getHostWithScheme() + ":" + sandboxConfigValue.getVncPort();
} else {
if (customPageDockerProxyBaseUrl.startsWith("http://")
|| customPageDockerProxyBaseUrl.startsWith("https://")) {
devUrl = customPageDockerProxyBaseUrl;
} else {
devUrl = "http://" + customPageDockerProxyBaseUrl;
}
}
} else if (sandboxConfigValue != null) {
devUrl = buildDirectDevServerUrl(sandboxConfigValue.getHostWithScheme(), customPageBuildModel.getDevPort());
} else {
if (customPageDevServerHost.startsWith("http://")
|| customPageDevServerHost.startsWith("https://")) {
devUrl = customPageDevServerHost + ":" + customPageBuildModel.getDevPort();
} else {
devUrl = "http://" + customPageDevServerHost + ":" + customPageBuildModel.getDevPort();
}
}
log.debug("generate dev environment config - dev Url: {}", devUrl);
ProxyConfig devProxyConfig = ProxyConfig.builder().path("/")
.healthCheckPath("/health")
.env(ProxyConfig.ProxyEnv.dev)
.requireAuth(false)
.backends(List.of(new ProxyConfigBackend(devUrl, 1)))
.build();
customPageConfig.getProxyConfigs().add(devProxyConfig);
}
if (env == ProxyConfig.ProxyEnv.prod) {
String prodUrl = null;
SandboxConfigValue sandboxConfigForProd = loadSandboxConfigValue(customPageConfig);
if (sandboxConfigForProd != null) {
prodUrl = buildDirectDevServerUrl(sandboxConfigForProd.getHostWithScheme(), DEFAULT_NGINX_PORT);
}
if (prodUrl == null) {
prodUrl = customPageProdServerHost;
}
if (!prodUrl.endsWith("/")) {
prodUrl += "/";
}
prodUrl += customPageConfig.getId();
log.debug("generate prod environment config - prod Url: {}", prodUrl);
ProxyConfig prodProxyConfig = ProxyConfig.builder().path("/")
.healthCheckPath("/health")
.env(ProxyConfig.ProxyEnv.prod)
.requireAuth(false)
.backends(List.of(new ProxyConfigBackend(prodUrl, 1)))
.build();
customPageConfig.getProxyConfigs().add(prodProxyConfig);
}
}
if (customPageConfig.getProjectType() == ProjectType.REVERSE_PROXY) {
// 检查是否有反向代理配置
if (CollectionUtils.isEmpty(customPageConfig.getProxyConfigs())) {
return null;
}
}
// 根据env过滤
List<ProxyConfig> proxyConfigs = customPageConfig.getProxyConfigs().stream()
.filter(proxyConfig1 -> proxyConfig1.getEnv().equals(env)).collect(Collectors.toList());
log.debug("filteredproxyconfigcount: {}", proxyConfigs.size());
if (proxyConfigs.isEmpty()) {
log.warn("select Backend failed - env not found: {} proxy config", env);
return null;
}
ProxyConfig proxyConfig = longestPrefixMatch(realUri, proxyConfigs);
if (proxyConfig == null) {
log.warn("select Backend failed - real Uri: {} cannot match any proxy config", realUri);
return null;
}
log.debug("matchedproxyconfig - path: {}, backend: {}", proxyConfig.getPath(),
proxyConfig.getBackends().get(0).getBackend());
URL url;
try {
url = new URL(proxyConfig.getBackends().get(0).getBackend());
} catch (MalformedURLException e) {
log.error("invalid backend URL: {}", proxyConfig.getBackends().get(0).getBackend(), e);
throw new RuntimeException(e);
}
BackendVo backendVo = new BackendVo();
backendVo.setHost(url.getHost());
backendVo.setPort(url.getPort() == -1 ? ("https".equals(url.getProtocol()) ? 443 : 80) : url.getPort());
backendVo.setScheme(url.getProtocol());
backendVo.setRequireAuth(customPageConfig.getNeedLogin() != null && customPageConfig.getNeedLogin() == 1);
backendVo.setDevAgentId(customPageConfig.getDevAgentId());
backendVo.setPublishType(customPageConfig.getPublishType());
// 对于ONLINE_DEPLOY项目完整的代理路径因为前端开发服务器配置了base URL
if (customPageConfig.getProjectType() == ProjectType.ONLINE_DEPLOY && proxyConfig.getPath().equals("/")) {
// 构建当前页面的base URL
String currentBase = "/page" + basePath;
currentBase += "-" + agentId;
currentBase += "/" + env.name();
// 检查 realUri 是否匹配另一个页面的路径模式 (/page/xxx-xxx/xxx/)
// 如果匹配,说明这是前端应用错误生成的嵌套路径,应该去掉这部分
if (realUri.matches("^/page/\\w+(?:-\\d+)?/\\w+.*")) {
// 直接使用 realUri
backendVo.setUri(realUri);
log.warn("select Backend [{}] real Uri contains page path pattern, - uri: {}",
env.name(), realUri);
} else {
// 正常情况拼接完整路径
String fullPath = currentBase;
if (!realUri.equals("/")) {
if (realUri.startsWith("/")) {
fullPath += realUri;
} else {
fullPath += "/" + realUri;
}
} else {
// 确保末尾有斜杠
if (!fullPath.endsWith("/")) {
fullPath += "/";
}
}
if (enableDockerProxy && env == ProxyConfig.ProxyEnv.dev && devPort != null) {
int routePort = devPort;
fullPath = "/proxy/" + routePort + fullPath;
}
backendVo.setUri(fullPath);
log.debug("select Backend [-{}] assemble full path - full Path: {}", env.name(), fullPath);
}
} else if (StringUtils.isNotBlank(url.getPath()) && !url.getPath().equals("/")) {
log.debug("assemble URL - real Uri: {}, proxy Config.path: {}, url.get Path(): {}",
realUri, proxyConfig.getPath(), url.getPath());
// 移除path前缀只移除开头匹配的部分
String subUri = realUri;
if (realUri.startsWith(proxyConfig.getPath())) {
subUri = realUri.substring(proxyConfig.getPath().length());
// 如果path不是 "/" 结尾,且 realUri 在移除前缀后还有内容
// 确保subUri以 "/" 开头(除非已经是空的)
if (!proxyConfig.getPath().endsWith("/") && !subUri.isEmpty() && !subUri.startsWith("/")) {
subUri = "/" + subUri;
}
}
log.debug("compute sub Uri - after replacement: {}", subUri);
// 拼接 url.getPath() 和 subUri
String urlPath = url.getPath();
if (!urlPath.endsWith("/") && !subUri.isEmpty() && !subUri.startsWith("/")) {
urlPath += "/";
}
backendVo.setUri(urlPath + subUri);
} else {
backendVo.setUri(realUri);
}
log.debug("select Backend succeeded - backend address: {}://{}:{}{}", backendVo.getScheme(), backendVo.getHost(),
backendVo.getPort(), backendVo.getUri());
return backendVo;
}
private SandboxConfigValue loadSandboxConfigValue(CustomPageConfig customPageConfig) {
Long sandboxId = customPageConfig.getSandboxId();
if (sandboxId == null) {
return null;
}
try {
RequestContext<?> requestContext = RequestContext.get();
Long tenantId = requestContext == null ? null : requestContext.getTenantId();
Long userId = requestContext == null ? null : requestContext.getUserId();
SandboxConfigRpcDto sandboxConfig = sandboxConfigRpcService.selectAppDevelopmentSandbox(
tenantId,
userId,
customPageConfig.getSpaceId(),
customPageConfig.getId(),
sandboxId);
if (sandboxConfig == null) {
return null;
}
SandboxConfigValue configValue = sandboxConfig.getConfigValue();
if (configValue == null || configValue.getHostWithScheme() == null || configValue.getHostWithScheme().isBlank()) {
return null;
}
return configValue;
} catch (Exception e) {
log.warn("load sandbox config for dev url failed, projectId={}, fallback to yaml",
customPageConfig.getId(), e);
return null;
}
}
private static String buildDirectDevServerUrl(String hostWithScheme, int port) {
if (hostWithScheme == null || hostWithScheme.isBlank()) {
return null;
}
String host = hostWithScheme.trim();
while (host.endsWith("/")) {
host = host.substring(0, host.length() - 1);
}
return host + ":" + port;
}
public static ProxyConfig longestPrefixMatch(String realUri, List<ProxyConfig> proxyConfigs) {
ProxyConfig longestMatchProxyConfig = null;
int maxLength = -1;
for (ProxyConfig proxyConfig : proxyConfigs) {
if (realUri.startsWith(proxyConfig.getPath()) && proxyConfig.getPath().length() > maxLength) {
longestMatchProxyConfig = proxyConfig;
maxLength = proxyConfig.getPath().length();
}
}
return longestMatchProxyConfig;
}
public CustomPageConfigModel queryCustomPageConfigByDomain(String domain) {
if (domain == null) {
return null;
}
domain = domain.split(":")[0];
Object customPageConfigModel = SimpleJvmHashCache.getHash("custom_page_config_model", domain);
if (customPageConfigModel != null) {
return (CustomPageConfigModel) customPageConfigModel;
}
String finalDomain = domain;
CustomPageDomainModel byDomain = TenantFunctions.callWithIgnoreCheck(() -> customPageDomainRepository.getByDomain(finalDomain));
if (byDomain == null) {
return null;
}
CustomPageConfigModel configModel = TenantFunctions.callWithIgnoreCheck(() -> customPageConfigRepository.getById(byDomain.getProjectId()));
if (configModel == null) {
return null;
}
// 内存缓存10秒避免短时间重复查询
SimpleJvmHashCache.putHash("custom_page_config_model", domain, configModel, 10);
return configModel;
}
}

View File

@@ -0,0 +1,9 @@
package com.xspaceagi.custompage.infra.translator;
import com.xspaceagi.custompage.domain.model.CustomPageBuildModel;
import com.xspaceagi.custompage.infra.dao.entity.CustomPageBuild;
import com.xspaceagi.system.infra.dao.ICommonTranslator;
public interface ICustomPageBuildTranslator extends ICommonTranslator<CustomPageBuildModel, CustomPageBuild> {
}

View File

@@ -0,0 +1,9 @@
package com.xspaceagi.custompage.infra.translator;
import com.xspaceagi.custompage.domain.model.CustomPageConfigModel;
import com.xspaceagi.custompage.infra.dao.entity.CustomPageConfig;
import com.xspaceagi.system.infra.dao.ICommonTranslator;
public interface ICustomPageConfigTranslator extends ICommonTranslator<CustomPageConfigModel, CustomPageConfig> {
}

View File

@@ -0,0 +1,10 @@
package com.xspaceagi.custompage.infra.translator;
import com.xspaceagi.custompage.domain.model.CustomPageConversationModel;
import com.xspaceagi.custompage.infra.dao.entity.CustomPageConversation;
import com.xspaceagi.system.infra.dao.ICommonTranslator;
public interface ICustomPageConversationTranslator
extends ICommonTranslator<CustomPageConversationModel, CustomPageConversation> {
}

View File

@@ -0,0 +1,9 @@
package com.xspaceagi.custompage.infra.translator;
import com.xspaceagi.custompage.domain.model.CustomPageDomainModel;
import com.xspaceagi.custompage.infra.dao.entity.CustomPageDomain;
import com.xspaceagi.system.infra.dao.ICommonTranslator;
public interface ICustomPageDomainTranslator extends ICommonTranslator<CustomPageDomainModel, CustomPageDomain> {
}

View File

@@ -0,0 +1,72 @@
package com.xspaceagi.custompage.infra.translator.impl;
import org.springframework.stereotype.Component;
import com.xspaceagi.custompage.domain.model.CustomPageBuildModel;
import com.xspaceagi.custompage.infra.dao.entity.CustomPageBuild;
import com.xspaceagi.custompage.infra.translator.ICustomPageBuildTranslator;
@Component
public class CustomPageBuildTranslatorImpl implements ICustomPageBuildTranslator {
@Override
public CustomPageBuildModel convertToModel(CustomPageBuild entity) {
if (entity == null) {
return null;
}
CustomPageBuildModel model = new CustomPageBuildModel();
model.setId(entity.getId());
model.setProjectId(entity.getProjectId());
model.setDevRunning(entity.getDevRunning());
model.setDevPid(entity.getDevPid());
model.setDevPort(entity.getDevPort());
model.setLastKeepAliveTime(entity.getLastKeepAliveTime());
model.setBuildRunning(entity.getBuildRunning());
model.setBuildTime(entity.getBuildTime());
model.setBuildVersion(entity.getBuildVersion());
model.setCodeVersion(entity.getCodeVersion());
model.setVersionInfo(entity.getVersionInfo());
model.setLastChatModelId(entity.getLastChatModelId());
model.setLastMultiModelId(entity.getLastMultiModelId());
model.setTenantId(entity.getTenantId());
model.setSpaceId(entity.getSpaceId());
model.setCreated(entity.getCreated());
model.setCreatorId(entity.getCreatorId());
model.setCreatorName(entity.getCreatorName());
model.setModified(entity.getModified());
model.setModifiedId(entity.getModifiedId());
model.setModifiedName(entity.getModifiedName());
model.setYn(entity.getYn());
return model;
}
@Override
public CustomPageBuild convertToEntity(CustomPageBuildModel model) {
if (model == null) {
return null;
}
return CustomPageBuild.builder()
.id(model.getId())
.projectId(model.getProjectId())
.devRunning(model.getDevRunning())
.devPid(model.getDevPid())
.devPort(model.getDevPort())
.lastKeepAliveTime(model.getLastKeepAliveTime())
.buildRunning(model.getBuildRunning())
.buildTime(model.getBuildTime())
.buildVersion(model.getBuildVersion())
.codeVersion(model.getCodeVersion())
.versionInfo(model.getVersionInfo())
.lastChatModelId(model.getLastChatModelId())
.lastMultiModelId(model.getLastMultiModelId())
.tenantId(model.getTenantId())
.spaceId(model.getSpaceId())
.created(model.getCreated())
.creatorId(model.getCreatorId())
.creatorName(model.getCreatorName())
.modified(model.getModified())
.modifiedId(model.getModifiedId())
.modifiedName(model.getModifiedName())
.yn(model.getYn())
.build();
}
}

View File

@@ -0,0 +1,82 @@
package com.xspaceagi.custompage.infra.translator.impl;
import org.springframework.stereotype.Component;
import com.xspaceagi.custompage.domain.model.CustomPageConfigModel;
import com.xspaceagi.custompage.infra.dao.entity.CustomPageConfig;
import com.xspaceagi.custompage.infra.translator.ICustomPageConfigTranslator;
@Component
public class CustomPageConfigTranslatorImpl implements ICustomPageConfigTranslator {
@Override
public CustomPageConfigModel convertToModel(CustomPageConfig entity) {
if (entity == null) {
return null;
}
CustomPageConfigModel model = new CustomPageConfigModel();
model.setId(entity.getId());
model.setName(entity.getName());
model.setDescription(entity.getDescription());
model.setIcon(entity.getIcon());
model.setCoverImg(entity.getCoverImg());
model.setCoverImgSourceType(entity.getCoverImgSourceType());
model.setBasePath(entity.getBasePath());
model.setBuildRunning(entity.getBuildRunning());
model.setPublishType(entity.getPublishType());
model.setNeedLogin(entity.getNeedLogin());
model.setDevAgentId(entity.getDevAgentId());
model.setProjectType(entity.getProjectType());
model.setProxyConfigs(entity.getProxyConfigs());
model.setPageArgConfigs(entity.getPageArgConfigs());
model.setDataSources(entity.getDataSources());
model.setSandboxId(entity.getSandboxId());
model.setExt(entity.getExt());
model.setTenantId(entity.getTenantId());
model.setSpaceId(entity.getSpaceId());
model.setCreated(entity.getCreated());
model.setCreatorId(entity.getCreatorId());
model.setCreatorName(entity.getCreatorName());
model.setModified(entity.getModified());
model.setModifiedId(entity.getModifiedId());
model.setModifiedName(entity.getModifiedName());
model.setYn(entity.getYn());
return model;
}
@Override
public CustomPageConfig convertToEntity(CustomPageConfigModel model) {
if (model == null) {
return null;
}
CustomPageConfig entity = new CustomPageConfig();
entity.setId(model.getId());
entity.setName(model.getName());
entity.setDescription(model.getDescription());
entity.setIcon(model.getIcon());
entity.setCoverImg(model.getCoverImg());
entity.setCoverImgSourceType(model.getCoverImgSourceType());
entity.setBasePath(model.getBasePath());
entity.setBuildRunning(model.getBuildRunning());
entity.setPublishType(model.getPublishType());
entity.setNeedLogin(model.getNeedLogin());
entity.setDevAgentId(model.getDevAgentId());
entity.setProjectType(model.getProjectType());
entity.setProxyConfigs(model.getProxyConfigs());
entity.setPageArgConfigs(model.getPageArgConfigs());
entity.setDataSources(model.getDataSources());
entity.setSandboxId(model.getSandboxId());
entity.setExt(model.getExt());
entity.setTenantId(model.getTenantId());
entity.setSpaceId(model.getSpaceId());
entity.setCreated(model.getCreated());
entity.setCreatorId(model.getCreatorId());
entity.setCreatorName(model.getCreatorName());
entity.setModified(model.getModified());
entity.setModifiedId(model.getModifiedId());
entity.setModifiedName(model.getModifiedName());
entity.setYn(model.getYn());
return entity;
}
}

View File

@@ -0,0 +1,65 @@
package com.xspaceagi.custompage.infra.translator.impl;
import org.springframework.stereotype.Component;
import com.xspaceagi.custompage.domain.model.CustomPageConversationModel;
import com.xspaceagi.custompage.infra.dao.entity.CustomPageConversation;
import com.xspaceagi.custompage.infra.translator.ICustomPageConversationTranslator;
@Component
public class CustomPageConversationTranslatorImpl implements ICustomPageConversationTranslator {
@Override
public CustomPageConversationModel convertToModel(CustomPageConversation entity) {
if (entity == null) {
return null;
}
CustomPageConversationModel model = new CustomPageConversationModel();
model.setId(entity.getId());
model.setProjectId(entity.getProjectId());
model.setTopic(entity.getTopic());
model.setContent(entity.getContent());
model.setRole(entity.getRole());
model.setSessionId(entity.getSessionId());
model.setRequestId(entity.getRequestId());
model.setTenantId(entity.getTenantId());
model.setSpaceId(entity.getSpaceId());
model.setCreated(entity.getCreated());
model.setCreatorId(entity.getCreatorId());
model.setCreatorName(entity.getCreatorName());
model.setModified(entity.getModified());
model.setModifiedId(entity.getModifiedId());
model.setModifiedName(entity.getModifiedName());
model.setYn(entity.getYn());
return model;
}
@Override
public CustomPageConversation convertToEntity(CustomPageConversationModel model) {
if (model == null) {
return null;
}
CustomPageConversation entity = new CustomPageConversation();
entity.setId(model.getId());
entity.setProjectId(model.getProjectId());
entity.setTopic(model.getTopic());
entity.setContent(model.getContent());
entity.setRole(model.getRole());
entity.setSessionId(model.getSessionId());
entity.setRequestId(model.getRequestId());
entity.setTenantId(model.getTenantId());
entity.setSpaceId(model.getSpaceId());
entity.setCreated(model.getCreated());
entity.setCreatorId(model.getCreatorId());
entity.setCreatorName(model.getCreatorName());
entity.setModified(model.getModified());
entity.setModifiedId(model.getModifiedId());
entity.setModifiedName(model.getModifiedName());
entity.setYn(model.getYn());
return entity;
}
}

View File

@@ -0,0 +1,41 @@
package com.xspaceagi.custompage.infra.translator.impl;
import org.springframework.stereotype.Component;
import com.xspaceagi.custompage.domain.model.CustomPageDomainModel;
import com.xspaceagi.custompage.infra.dao.entity.CustomPageDomain;
import com.xspaceagi.custompage.infra.translator.ICustomPageDomainTranslator;
@Component
public class CustomPageDomainTranslatorImpl implements ICustomPageDomainTranslator {
@Override
public CustomPageDomainModel convertToModel(CustomPageDomain entity) {
if (entity == null) {
return null;
}
CustomPageDomainModel model = new CustomPageDomainModel();
model.setId(entity.getId());
model.setTenantId(entity.getTenantId());
model.setProjectId(entity.getProjectId());
model.setDomain(entity.getDomain());
model.setCreated(entity.getCreated());
model.setModified(entity.getModified());
return model;
}
@Override
public CustomPageDomain convertToEntity(CustomPageDomainModel model) {
if (model == null) {
return null;
}
CustomPageDomain entity = new CustomPageDomain();
entity.setId(model.getId());
entity.setTenantId(model.getTenantId());
entity.setProjectId(model.getProjectId());
entity.setDomain(model.getDomain());
entity.setCreated(model.getCreated());
entity.setModified(model.getModified());
return entity;
}
}

View File

@@ -0,0 +1,16 @@
package com.xspaceagi.custompage.infra.vo;
import com.xspaceagi.custompage.sdk.dto.PublishTypeEnum;
import lombok.Data;
@Data
public class BackendVo {
private String scheme;
private String host;
private Integer port;
private String uri;
private boolean requireAuth;
private PublishTypeEnum publishType;
private Long devAgentId;
}

View File

@@ -0,0 +1,66 @@
package com.xspaceagi.custompage.infra.vo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
public class ProxyAuthVo {
private Tenant tenant;
private User user;
private Agent agent;
private Space space;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class User {
private Long userId;
private String uid;
private String userName;
private String nickName;
private String avatar;
private Role role;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class Agent {
private Long agentId;
private String name;
private String icon;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class Space {
private Long spaceId;
private String name;
private String icon;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class Tenant {
private Long tenantId;
private String name;
private String icon;
}
public enum Role {
SpaceAdmin,
AgentCreator,
SpaceUser,
User
}
}

View File

@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>403 - 无权限访问</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font: normal 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #fafafa 0%, #f0f0f0 100%);
padding: 20px;
}
.container { text-align: center; }
.code {
font-size: 72px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.message { color: #999; font-size: 14px; }
</style>
</head>
<body>
<div class="container">
<div class="code">403</div>
<p class="message">抱歉,您没有权限访问此页面</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,18 @@
<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 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-custom-page</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>app-platform-custom-page-sdk</artifactId>
<name>app-platform-custom-page-sdk</name>
<dependencies>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>system-sdk</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,77 @@
package com.xspaceagi.custompage.sdk;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.xspaceagi.custompage.sdk.dto.CustomPageDto;
import com.xspaceagi.custompage.sdk.dto.CustomPageQueryReq;
import com.xspaceagi.system.spec.page.PageQueryVo;
import com.xspaceagi.system.spec.page.SuperPage;
import java.util.List;
/**
* 用户页面RPC服务接口
*/
public interface ICustomPageRpcService {
/**
* 查询项目列表
*/
List<CustomPageDto> list(CustomPageQueryReq req);
/**
* 批量通过agentId查询项目列表
*/
List<CustomPageDto> listByAgentIds(List<Long> agentIds);
/**
* 分页查询项目
*/
SuperPage<CustomPageDto> pageQuery(PageQueryVo<CustomPageQueryReq> pageQueryVo);
/**
* 查询项目详情
*/
CustomPageDto queryDetail(Long projectId);
/**
* 查询项目详情,包含版本信息
*/
CustomPageDto queryDetailWithVersion(Long projectId);
/**
* 根据agentId查询项目详情
*/
CustomPageDto queryDetailByAgentId(Long agentId);
/**
* 统计网页应用总数
*
* @return 网页应用总数
*/
Long countTotalPages();
/**
* 管理端查询网页应用列表
*
* @param pageNo 页码
* @param pageSize 每页大小
* @param name 名称模糊搜索
* @param creatorIds 创建人ID列表
* @param spaceId 空间ID
* @return 网页应用分页数据
*/
IPage<CustomPageDto> queryListForManage(Integer pageNo, Integer pageSize, String name, java.util.List<Long> creatorIds,
Long spaceId, List<Long> devAgentIds);
/**
* 管理端删除网页应用
*
* @param id 项目ID
*/
void deleteForManage(Long id);
/**
* 管理端根据ids批量查询网页应用列表
*/
List<CustomPageDto> listByIds(List<Long> pageIds, List<Long> agentIds);
}

View File

@@ -0,0 +1,22 @@
package com.xspaceagi.custompage.sdk.dto;
public enum CopyTypeEnum {
DEVELOP, // 开发
SQUARE; // 广场
public static CopyTypeEnum getByValue(String name) {
if (name == null) {
return null;
}
for (CopyTypeEnum type : CopyTypeEnum.values()) {
if (type.name().equals(name)) {
return type;
}
}
return null;
}
public static boolean isValid(String name) {
return getByValue(name) != null;
}
}

Some files were not shown because too many files have changed in this diff Show More