同步平台业务模块能力

This commit is contained in:
baiyanyun
2026-07-13 09:13:30 +08:00
parent e612f5e434
commit a1a38f62d8
224 changed files with 22331 additions and 1224 deletions

View File

@@ -1,15 +1,36 @@
package com.xspaceagi.bill.app.service;
import com.xspaceagi.bill.sdk.dto.AppNativeInvokeRequest;
import com.xspaceagi.bill.sdk.dto.CreateOrderRequest;
import com.xspaceagi.bill.sdk.dto.H5WebInvokeRequest;
import com.xspaceagi.bill.sdk.dto.MiniPayInvokeRequest;
import com.xspaceagi.bill.sdk.dto.OrderDTO;
import com.xspaceagi.bill.sdk.dto.OrderPageDTO;
import com.xspaceagi.bill.sdk.dto.OrderQueryRequest;
import com.xspaceagi.bill.sdk.dto.OrderSettlementStatusResponse;
import com.xspaceagi.bill.sdk.dto.WeChatJsapiInvokeRequest;
import com.xspaceagi.pay.sdk.dto.CashierSessionCreateResponse;
import com.xspaceagi.pay.sdk.dto.OrderAndTransactionCreateResponse;
public interface BillOrderAppService {
OrderDTO createOrder(CreateOrderRequest request);
/** 对已有 Bill 订单调起小程序支付,返回 wx.requestPayment / my.tradePay 参数。 */
OrderAndTransactionCreateResponse invokeMiniPay(long userId, MiniPayInvokeRequest request, String clientIp);
/** 微信内 H5 JSAPI 调起支付,返回 WeixinJSBridge 所需 wxPayParams。 */
OrderAndTransactionCreateResponse invokeWeChatJsapi(long userId, WeChatJsapiInvokeRequest request, String clientIp);
/** 系统浏览器 H5 调起支付,返回 invokeType + formHtml/redirectUrl。 */
OrderAndTransactionCreateResponse invokeH5Web(long userId, H5WebInvokeRequest request, String clientIp);
/** App 原生 SDK 调起支付,返回 wxPayParams / redirectUrl。 */
OrderAndTransactionCreateResponse invokeAppNative(long userId, AppNativeInvokeRequest request, String clientIp);
/** 扫码/H5 收银台调起支付。 */
CashierSessionCreateResponse invokeScanCashier(long userId, Long orderId, String returnUrl);
boolean paymentCallback(Long tenantId, Long orderId, String payStatus);
OrderPageDTO queryOrders(OrderQueryRequest query);

View File

@@ -192,7 +192,7 @@ public class BillCalculateTaskServiceImpl extends AbstractTaskExecuteService {
if (tenantConfig.getEnableSubscription() != null && tenantConfig.getEnableSubscription() == 1 && !traceContext.isError() && !traceContext.isDevTest()) {
PricingConfigDTO pricingConfigDTO = iPricingRpcService.queryPricingInfo(request);
if (pricingConfigDTO != null && pricingConfigDTO.getStatus() == 1 && (pricingConfigDTO.getPricingType() == PricingTypeEnum.ONE_TIME || pricingConfigDTO.getPricingType() == PricingTypeEnum.TIERED)) {
if (pricingConfigDTO != null && pricingConfigDTO.getStatus() == 1) {
log.info("定价配置匹配成功 pricingType={} price={} trialCount={}",
pricingConfigDTO.getPricingType(), pricingConfigDTO.getPrice(), pricingConfigDTO.getTrialCount());
boolean isToolCall = false;
@@ -202,6 +202,20 @@ public class BillCalculateTaskServiceImpl extends AbstractTaskExecuteService {
feeAmount = pricingConfigDTO.getPrice();
isToolCall = true;
}
if (pricingConfigDTO.getPricingType() == PricingTypeEnum.SECOND && traceContext.getDurationUsage() != null && traceContext.getDurationUsage().duration > 0
&& (pricingConfigDTO.getTargetType() == TargetTypeEnum.WORKFLOW || pricingConfigDTO.getTargetType() == TargetTypeEnum.PLUGIN)) {
//积分扣减计算
feeAmount = pricingConfigDTO.getPrice().multiply(BigDecimal.valueOf(traceContext.getDurationUsage().duration));
creditAmount = feeAmount.multiply(BigDecimal.valueOf(tenantConfig.getCreditExchangeRate()));
isToolCall = true;
}
if (pricingConfigDTO.getPricingType() == PricingTypeEnum.MILLION_TOKEN && tokenUsage != null && tokenUsage.outputTokens > 0
&& (pricingConfigDTO.getTargetType() == TargetTypeEnum.WORKFLOW || pricingConfigDTO.getTargetType() == TargetTypeEnum.PLUGIN)) {
//积分扣减计算
feeAmount = pricingConfigDTO.getPrice().multiply(BigDecimal.valueOf(tokenUsage.outputTokens)).divide(BigDecimal.valueOf(1000000), RoundingMode.HALF_UP);
creditAmount = feeAmount.multiply(BigDecimal.valueOf(tenantConfig.getCreditExchangeRate()));
isToolCall = true;
}
if (pricingConfigDTO.getPricingType() == PricingTypeEnum.TIERED && pricingConfigDTO.getTargetType() == TargetTypeEnum.MODEL) {
//阶梯计费
if (tokenUsage != null && CollectionUtils.isNotEmpty(pricingConfigDTO.getModelPriceTiers())) {

View File

@@ -8,6 +8,8 @@ import com.xspaceagi.agent.core.sdk.dto.ReqResult;
import com.xspaceagi.agent.core.sdk.dto.SkillInfoDto;
import com.xspaceagi.bill.app.service.BillOrderAppService;
import com.xspaceagi.bill.app.service.BillRevenueAppService;
import com.xspaceagi.bill.app.service.support.MiniPayChannelCredentialResolver;
import com.xspaceagi.bill.app.service.support.WeChatJsapiChannelCredentialResolver;
import com.xspaceagi.bill.infra.dao.entity.BillOrder;
import com.xspaceagi.bill.infra.dao.entity.BillOrderItem;
import com.xspaceagi.bill.infra.dao.mapper.BillOrderMapper;
@@ -18,10 +20,22 @@ import com.xspaceagi.bill.spec.enums.*;
import com.xspaceagi.credit.sdk.dto.CreditAddRequest;
import com.xspaceagi.credit.sdk.rpc.ICreditRpcService;
import com.xspaceagi.credit.spec.enums.CreditTypeEnum;
import com.xspaceagi.pay.sdk.dto.PaymentOrderCreateResponse;
import com.xspaceagi.pay.sdk.dto.AppOrderRpcCreateRequest;
import com.xspaceagi.pay.sdk.dto.AppTransactionRpcCreateRequest;
import com.xspaceagi.pay.sdk.dto.MiniPayOrderRpcCreateRequest;
import com.xspaceagi.pay.sdk.dto.MiniPayTransactionRpcCreateRequest;
import com.xspaceagi.pay.sdk.dto.OrderAndTransactionCreateResponse;
import com.xspaceagi.pay.sdk.dto.OrderCreateResponse;
import com.xspaceagi.pay.sdk.dto.ScanOrderCreateRequest;
import com.xspaceagi.pay.sdk.dto.H5OrderRpcCreateRequest;
import com.xspaceagi.pay.sdk.dto.H5TransactionRpcCreateRequest;
import com.xspaceagi.pay.sdk.dto.CashierSessionCreateRequest;
import com.xspaceagi.pay.sdk.dto.CashierSessionCreateResponse;
import com.xspaceagi.pay.sdk.enums.PayChannel;
import com.xspaceagi.pay.sdk.enums.PayMode;
import com.xspaceagi.pay.sdk.enums.PayClientScene;
import com.xspaceagi.pay.sdk.service.IPaymentRpcService;
import com.xspaceagi.pay.sdk.support.PayOrderExtKeys;
import com.xspaceagi.subscription.sdk.dto.CreateSubscriptionRequest;
import com.xspaceagi.subscription.sdk.dto.PlanDTO;
import com.xspaceagi.subscription.sdk.rpc.ISubscriptionRpcService;
@@ -40,6 +54,9 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
@@ -79,6 +96,12 @@ public class BillOrderAppServiceImpl implements BillOrderAppService {
@Resource
private UserApplicationService userApplicationService;
@Resource
private MiniPayChannelCredentialResolver miniPayChannelCredentialResolver;
@Resource
private WeChatJsapiChannelCredentialResolver weChatJsapiChannelCredentialResolver;
@Override
public OrderDTO createOrder(CreateOrderRequest request) {
if (request.getItems() == null || request.getItems().isEmpty()) {
@@ -122,17 +145,39 @@ public class BillOrderAppServiceImpl implements BillOrderAppService {
order.setAmount(totalAmount);
//创建支付订单
ScanOrderCreateRequest paymentScanCreateOrderRequest = new ScanOrderCreateRequest();
paymentScanCreateOrderRequest.setBizOrderNo(order.getId().toString());
paymentScanCreateOrderRequest.setOrderAmount(totalAmount.multiply(new BigDecimal(100)).longValue());
paymentScanCreateOrderRequest.setSubject(request.getDescription());
PaymentOrderCreateResponse orderForScan = iPaymentRpcService.createOrderForScan(paymentScanCreateOrderRequest);
PayMode payMode = request.getPayMode() != null ? request.getPayMode() : PayMode.scan;
OrderCreateResponse payOrderResponse;
if (payMode == PayMode.minipay) {
MiniPayOrderRpcCreateRequest miniPayOrderRequest = new MiniPayOrderRpcCreateRequest();
miniPayOrderRequest.setBizOrderNo(order.getId().toString());
miniPayOrderRequest.setOrderAmount(totalAmount.multiply(new BigDecimal(100)).longValue());
miniPayOrderRequest.setSubject(request.getDescription());
payOrderResponse = iPaymentRpcService.createOrderForMiniPay(miniPayOrderRequest);
} else if (payMode == PayMode.h5) {
H5OrderRpcCreateRequest h5OrderRequest = new H5OrderRpcCreateRequest();
h5OrderRequest.setBizOrderNo(order.getId().toString());
h5OrderRequest.setOrderAmount(totalAmount.multiply(new BigDecimal(100)).longValue());
h5OrderRequest.setSubject(request.getDescription());
payOrderResponse = iPaymentRpcService.createOrderForH5(h5OrderRequest);
} else if (payMode == PayMode.app) {
AppOrderRpcCreateRequest appOrderRequest = new AppOrderRpcCreateRequest();
appOrderRequest.setBizOrderNo(order.getId().toString());
appOrderRequest.setOrderAmount(totalAmount.multiply(new BigDecimal(100)).longValue());
appOrderRequest.setSubject(request.getDescription());
payOrderResponse = iPaymentRpcService.createOrderForApp(appOrderRequest);
} else {
ScanOrderCreateRequest paymentScanCreateOrderRequest = new ScanOrderCreateRequest();
paymentScanCreateOrderRequest.setBizOrderNo(order.getId().toString());
paymentScanCreateOrderRequest.setOrderAmount(totalAmount.multiply(new BigDecimal(100)).longValue());
paymentScanCreateOrderRequest.setSubject(request.getDescription());
payOrderResponse = iPaymentRpcService.createOrderForScan(paymentScanCreateOrderRequest);
}
Map<String, Object> extra = request.getExtra();
if (extra == null) {
extra = new HashMap<>();
}
extra.put("gatewayPaymentOrderNo", orderForScan.getGatewayPaymentOrderNo());
extra.put("payMode", PayMode.scan.name());
extra.put("gatewayPaymentOrderNo", payOrderResponse.getGatewayPaymentOrderNo());
extra.put("payMode", payMode.name());
order.setExtra(JSON.toJSONString(extra));
billOrderService.updateById(order);
} catch (Exception e) {
@@ -146,6 +191,221 @@ public class BillOrderAppServiceImpl implements BillOrderAppService {
return convertToDTO(order);
}
@Override
public OrderAndTransactionCreateResponse invokeMiniPay(long userId, MiniPayInvokeRequest request, String clientIp) {
if (request.getOrderId() == null) {
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), I18nUtil.systemMessage("Backend.Bill.Order.Validate.OrderIdRequired"));
}
if (request.getPayChannel() == null) {
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "payChannel is required");
}
BillOrder order = requirePendingBillOrder(userId, request.getOrderId());
ensurePayOrderForMode(order, PayMode.minipay);
MiniPayTransactionRpcCreateRequest txRequest = new MiniPayTransactionRpcCreateRequest();
txRequest.setBizOrderNo(String.valueOf(order.getId()));
txRequest.setPayChannel(request.getPayChannel());
txRequest.setClientIp(clientIp);
txRequest.setPayClientScene(PayClientScene.MINI_PROGRAM);
miniPayChannelCredentialResolver.applyChannelParams(order.getTenantId(), request, txRequest);
OrderAndTransactionCreateResponse response = iPaymentRpcService.createMiniPayTransaction(txRequest);
recordPayClientSelectionOnBillOrder(order, PayClientScene.MINI_PROGRAM, response.getPayChannel());
return response;
}
@Override
public OrderAndTransactionCreateResponse invokeWeChatJsapi(
long userId, WeChatJsapiInvokeRequest request, String clientIp) {
if (request.getOrderId() == null) {
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), I18nUtil.systemMessage("Backend.Bill.Order.Validate.OrderIdRequired"));
}
BillOrder order = requirePendingBillOrder(userId, request.getOrderId());
ensurePayOrderForMode(order, PayMode.minipay);
MiniPayTransactionRpcCreateRequest txRequest = new MiniPayTransactionRpcCreateRequest();
txRequest.setBizOrderNo(String.valueOf(order.getId()));
txRequest.setClientIp(clientIp);
txRequest.setPayClientScene(PayClientScene.WECHAT_JSAPI);
weChatJsapiChannelCredentialResolver.applyChannelParams(order.getTenantId(), request, txRequest);
OrderAndTransactionCreateResponse response = iPaymentRpcService.createMiniPayTransaction(txRequest);
recordPayClientSelectionOnBillOrder(order, PayClientScene.WECHAT_JSAPI, response.getPayChannel());
return response;
}
@Override
public OrderAndTransactionCreateResponse invokeH5Web(long userId, H5WebInvokeRequest request, String clientIp) {
if (request.getOrderId() == null) {
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), I18nUtil.systemMessage("Backend.Bill.Order.Validate.OrderIdRequired"));
}
if (request.getPayChannel() == null) {
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "payChannel is required");
}
if (request.getFrontNotifyUrl() == null || request.getFrontNotifyUrl().isBlank()) {
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "frontNotifyUrl is required");
}
BillOrder order = requirePendingBillOrder(userId, request.getOrderId());
ensurePayOrderForMode(order, PayMode.h5);
H5TransactionRpcCreateRequest txRequest = new H5TransactionRpcCreateRequest();
txRequest.setBizOrderNo(String.valueOf(order.getId()));
txRequest.setPayChannel(request.getPayChannel());
txRequest.setClientIp(clientIp);
String frontNotifyUrl = validateFrontNotifyUrl(request.getFrontNotifyUrl().trim());
txRequest.setFrontNotifyUrl(buildChannelFrontNotifyUrl(frontNotifyUrl, request.getOrderId()));
OrderAndTransactionCreateResponse response = iPaymentRpcService.createH5Transaction(txRequest);
recordPayClientSelectionOnBillOrder(order, PayClientScene.H5_WEB, response.getPayChannel());
return response;
}
@Override
public OrderAndTransactionCreateResponse invokeAppNative(
long userId, AppNativeInvokeRequest request, String clientIp) {
if (request.getOrderId() == null) {
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), I18nUtil.systemMessage("Backend.Bill.Order.Validate.OrderIdRequired"));
}
if (request.getPayChannel() == null) {
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "payChannel is required");
}
BillOrder order = requirePendingBillOrder(userId, request.getOrderId());
ensurePayOrderForMode(order, PayMode.app);
AppTransactionRpcCreateRequest txRequest = new AppTransactionRpcCreateRequest();
txRequest.setBizOrderNo(String.valueOf(order.getId()));
txRequest.setPayChannel(request.getPayChannel());
txRequest.setClientIp(clientIp);
txRequest.setPayClientScene(PayClientScene.NATIVE_APP);
OrderAndTransactionCreateResponse response = iPaymentRpcService.createAppTransaction(txRequest);
recordPayClientSelectionOnBillOrder(order, PayClientScene.NATIVE_APP, response.getPayChannel());
return response;
}
@Override
public CashierSessionCreateResponse invokeScanCashier(long userId, Long orderId, String returnUrl) {
if (orderId == null) {
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), I18nUtil.systemMessage("Backend.Bill.Order.Validate.OrderIdRequired"));
}
BillOrder order = requirePendingBillOrder(userId, orderId);
OrderCreateResponse payOrderResponse = ensurePayOrderForMode(order, PayMode.scan);
CashierSessionCreateRequest paymentCreateCashierSessionRequest = new CashierSessionCreateRequest();
paymentCreateCashierSessionRequest.setGatewayPaymentOrderNo(payOrderResponse.getGatewayPaymentOrderNo());
paymentCreateCashierSessionRequest.setBizRedirectUrl(returnUrl);
return iPaymentRpcService.createCashierSession(paymentCreateCashierSessionRequest);
}
private static String buildChannelFrontNotifyUrl(String frontNotifyUrl, Long orderId) {
URI frontendUri = URI.create(frontNotifyUrl);
String origin = frontendUri.getScheme() + "://" + frontendUri.getAuthority();
String encodedReturnUrl = URLEncoder.encode(frontNotifyUrl, StandardCharsets.UTF_8);
return origin + "/api/bill/order/pay/front-notify?orderId=" + orderId + "&returnUrl=" + encodedReturnUrl;
}
private static String validateFrontNotifyUrl(String url) {
try {
URI uri = URI.create(url);
String scheme = uri.getScheme();
if (!"http".equalsIgnoreCase(scheme) && !"https".equalsIgnoreCase(scheme)) {
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "frontNotifyUrl must start with http or https");
}
String host = uri.getHost();
if (host == null || host.isBlank()) {
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "Invalid frontNotifyUrl");
}
return url;
} catch (IllegalArgumentException e) {
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "Invalid frontNotifyUrl");
}
}
private void recordPayClientSelectionOnBillOrder(BillOrder order, PayClientScene scene, PayChannel payChannel) {
Map<String, Object> extra = parseExtra(order.getExtra());
extra.put(PayOrderExtKeys.PAY_CLIENT_SCENE, scene.name());
if (payChannel != null) {
extra.put("payChannel", payChannel.name());
}
order.setExtra(JSON.toJSONString(extra));
billOrderService.updateById(order);
}
private BillOrder requirePendingBillOrder(long userId, Long orderId) {
BillOrder order = billOrderService.getById(orderId);
if (order == null) {
throw new BizException("ORDER_NOT_FOUND", I18nUtil.systemMessage("Backend.Bill.Order.Error.OrderNotFound"));
}
if (!Long.valueOf(userId).equals(order.getUserId())) {
throw new BizException("USER_NOT_MATCH", I18nUtil.systemMessage("Backend.Bill.Order.Error.UserNotMatch"));
}
Long ctxTenantId = RequestContext.get() != null ? RequestContext.get().getTenantId() : null;
if (ctxTenantId == null || !ctxTenantId.equals(order.getTenantId())) {
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "订单租户与当前上下文不一致");
}
if (!PayStatusEnum.PENDING.getCode().equals(order.getPayStatus())) {
throw new BizException("ORDER_NOT_PENDING", "The order status is incorrect.");
}
return order;
}
private OrderCreateResponse ensurePayOrderForMode(BillOrder order, PayMode targetPayMode) {
OrderCreateResponse response;
if (targetPayMode == PayMode.minipay) {
MiniPayOrderRpcCreateRequest req = new MiniPayOrderRpcCreateRequest();
req.setBizOrderNo(String.valueOf(order.getId()));
req.setOrderAmount(orderAmountFen(order));
req.setSubject(resolvePaySubject(order));
response = iPaymentRpcService.createOrderForMiniPay(req);
} else if (targetPayMode == PayMode.h5) {
H5OrderRpcCreateRequest req = new H5OrderRpcCreateRequest();
req.setBizOrderNo(String.valueOf(order.getId()));
req.setOrderAmount(orderAmountFen(order));
req.setSubject(resolvePaySubject(order));
response = iPaymentRpcService.createOrderForH5(req);
} else if (targetPayMode == PayMode.app) {
AppOrderRpcCreateRequest req = new AppOrderRpcCreateRequest();
req.setBizOrderNo(String.valueOf(order.getId()));
req.setOrderAmount(orderAmountFen(order));
req.setSubject(resolvePaySubject(order));
response = iPaymentRpcService.createOrderForApp(req);
} else {
ScanOrderCreateRequest req = new ScanOrderCreateRequest();
req.setBizOrderNo(String.valueOf(order.getId()));
req.setOrderAmount(orderAmountFen(order));
req.setSubject(resolvePaySubject(order));
response = iPaymentRpcService.createOrderForScan(req);
}
updateBillPayPointer(order, targetPayMode, response.getGatewayPaymentOrderNo());
return response;
}
private void updateBillPayPointer(BillOrder order, PayMode payMode, String gatewayPaymentOrderNo) {
Map<String, Object> extra = parseExtra(order.getExtra());
extra.put("payMode", payMode.name());
extra.put("gatewayPaymentOrderNo", gatewayPaymentOrderNo);
order.setExtra(JSON.toJSONString(extra));
billOrderService.updateById(order);
}
private static long orderAmountFen(BillOrder order) {
BigDecimal amount = order.getAmount() == null ? BigDecimal.ZERO : order.getAmount();
return amount.multiply(new BigDecimal(100)).longValue();
}
private static String resolvePaySubject(BillOrder order) {
if (order.getDescription() != null && !order.getDescription().isBlank()) {
return order.getDescription();
}
return "BillOrder-" + order.getId();
}
private static Map<String, Object> parseExtra(String extraJson) {
if (extraJson == null || extraJson.isBlank()) {
return new HashMap<>();
}
try {
Map<String, Object> parsed = JSON.parseObject(extraJson);
return parsed != null ? parsed : new HashMap<>();
} catch (Exception e) {
return new HashMap<>();
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean paymentCallback(Long tenantId, Long orderId, String payStatus) {

View File

@@ -0,0 +1,59 @@
package com.xspaceagi.bill.app.service.support;
import com.xspaceagi.bill.sdk.dto.MiniPayInvokeRequest;
import com.xspaceagi.pay.sdk.dto.MiniPayTransactionRpcCreateRequest;
import com.xspaceagi.pay.sdk.enums.PayChannel;
import com.xspaceagi.system.application.dto.TenantConfigDto;
import com.xspaceagi.system.application.service.TenantConfigApplicationService;
import com.xspaceagi.system.infra.rpc.WeChatMpService;
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 org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
/** 将小程序支付渠道凭证解析为网关下单参数 */
@Component
public class MiniPayChannelCredentialResolver {
@Resource
private TenantConfigApplicationService tenantConfigApplicationService;
@Resource
private WeChatMpService weChatMpService;
public void applyChannelParams(long tenantId, MiniPayInvokeRequest request, MiniPayTransactionRpcCreateRequest tx) {
PayChannel channel = request.getPayChannel();
if (channel == PayChannel.WxPay) {
applyWxPay(tenantId, request, tx);
return;
}
if (channel == PayChannel.AliPay) {
applyAliPay(request, tx);
return;
}
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "小程序支付仅支持 WxPay 与 AliPay");
}
private void applyWxPay(long tenantId, MiniPayInvokeRequest request, MiniPayTransactionRpcCreateRequest tx) {
if (!StringUtils.hasText(request.getWxLoginCode())) {
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "wxLoginCode 不能为空");
}
TenantConfigDto config = tenantConfigApplicationService.getTenantConfig(tenantId);
if (config == null || !StringUtils.hasText(config.getMpAppId())) {
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemWeChatMpNotConfigured);
}
String openId = weChatMpService.getOpenIdByLoginCode(request.getWxLoginCode().trim());
tx.setSubAppid(config.getMpAppId().trim());
tx.setOpenId(openId);
}
private void applyAliPay(MiniPayInvokeRequest request, MiniPayTransactionRpcCreateRequest tx) {
if (!StringUtils.hasText(request.getAlipayAuthCode())) {
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "alipayAuthCode 不能为空");
}
// 支付宝 oauth 换 buyer_id 依赖租户密钥配置,待接入 AliPayMpService 后实现
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemAlipayMiniPayNotConfigured);
}
}

View File

@@ -0,0 +1,39 @@
package com.xspaceagi.bill.app.service.support;
import com.xspaceagi.bill.sdk.dto.WeChatJsapiInvokeRequest;
import com.xspaceagi.pay.sdk.dto.MiniPayTransactionRpcCreateRequest;
import com.xspaceagi.pay.sdk.enums.PayChannel;
import com.xspaceagi.system.application.dto.TenantConfigDto;
import com.xspaceagi.system.application.service.TenantConfigApplicationService;
import com.xspaceagi.system.infra.rpc.WeChatOaService;
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 org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
/** 将公众号 OAuth 凭证解析为 minipayJSAPI网关下单参数。 */
@Component
public class WeChatJsapiChannelCredentialResolver {
@Resource
private TenantConfigApplicationService tenantConfigApplicationService;
@Resource
private WeChatOaService weChatOaService;
public void applyChannelParams(long tenantId, WeChatJsapiInvokeRequest request, MiniPayTransactionRpcCreateRequest tx) {
if (!StringUtils.hasText(request.getWxOAuthCode())) {
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "wxOAuthCode 不能为空");
}
TenantConfigDto config = tenantConfigApplicationService.getTenantConfig(tenantId);
if (config == null || !StringUtils.hasText(config.getOaAppId())) {
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemWeChatOaNotConfigured);
}
String openId = weChatOaService.getOpenIdByOAuthCode(request.getWxOAuthCode().trim(), tenantId);
tx.setPayChannel(PayChannel.WxPay);
tx.setSubAppid(config.getOaAppId().trim());
tx.setOpenId(openId);
}
}

View File

@@ -20,6 +20,10 @@
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-bill-spec</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-pay-sdk</artifactId>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>

View File

@@ -0,0 +1,18 @@
package com.xspaceagi.bill.sdk.dto;
import com.xspaceagi.pay.sdk.enums.PayChannel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
@Data
@Schema(description = "App 原生 SDK 调起支付请求")
public class AppNativeInvokeRequest implements Serializable {
@Schema(description = "Bill 订单 ID", requiredMode = Schema.RequiredMode.REQUIRED)
private Long orderId;
@Schema(description = "支付渠道WxPay / AliPay", requiredMode = Schema.RequiredMode.REQUIRED)
private PayChannel payChannel;
}

View File

@@ -1,6 +1,7 @@
package com.xspaceagi.bill.sdk.dto;
import com.xspaceagi.bill.spec.enums.BizTypeEnum;
import com.xspaceagi.pay.sdk.enums.PayMode;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -30,6 +31,9 @@ public class CreateOrderRequest implements Serializable {
@Schema(description = "扩展字段")
private Map<String, Object> extra;
@Schema(description = "支付方式scan默认扫码/H5 收银台)/ minipay小程序/微信JSAPI/ h5H5支付/ appApp 原生 SDK")
private PayMode payMode;
@Data
@Schema(description = "订单项")
public static class CreateOrderItem implements Serializable {

View File

@@ -0,0 +1,21 @@
package com.xspaceagi.bill.sdk.dto;
import com.xspaceagi.pay.sdk.enums.PayChannel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
@Data
@Schema(description = "系统浏览器 H5 调起支付请求h5")
public class H5WebInvokeRequest implements Serializable {
@Schema(description = "Bill 订单 ID", requiredMode = Schema.RequiredMode.REQUIRED)
private Long orderId;
@Schema(description = "支付渠道WxPay / AliPay", requiredMode = Schema.RequiredMode.REQUIRED)
private PayChannel payChannel;
@Schema(description = "前端结算页地址(后端将封装为渠道可回调的中转地址)", requiredMode = Schema.RequiredMode.REQUIRED)
private String frontNotifyUrl;
}

View File

@@ -0,0 +1,24 @@
package com.xspaceagi.bill.sdk.dto;
import com.xspaceagi.pay.sdk.enums.PayChannel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
@Data
@Schema(description = "小程序调起支付请求(微信内 H5 JSAPI 请使用 WeChatJsapiInvokeRequest")
public class MiniPayInvokeRequest implements Serializable {
@Schema(description = "Bill 订单 ID", requiredMode = Schema.RequiredMode.REQUIRED)
private Long orderId;
@Schema(description = "支付渠道WxPay / AliPay", requiredMode = Schema.RequiredMode.REQUIRED)
private PayChannel payChannel;
@Schema(description = "WxPaywx.login() 返回的临时 code5 分钟有效,一次性)")
private String wxLoginCode;
@Schema(description = "AliPaymy.getAuthCode() 返回的授权码")
private String alipayAuthCode;
}

View File

@@ -0,0 +1,17 @@
package com.xspaceagi.bill.sdk.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
@Data
@Schema(description = "微信内 H5 JSAPI 调起支付请求")
public class WeChatJsapiInvokeRequest implements Serializable {
@Schema(description = "Bill 订单 ID", requiredMode = Schema.RequiredMode.REQUIRED)
private Long orderId;
@Schema(description = "公众号 OAuth 回调 codesnsapi_base一次性", requiredMode = Schema.RequiredMode.REQUIRED)
private String wxOAuthCode;
}

View File

@@ -11,7 +11,8 @@ public enum RevenueTargetTypeEnum {
MODEL("Model", "模型"),
PLUGIN("Plugin", "插件"),
MCP("Mcp", "MCP"),
WORKFLOW("Workflow", "工作流");
WORKFLOW("Workflow", "工作流"),
PROJECT("Project", "项目");
private final String code;
private final String desc;

View File

@@ -8,7 +8,8 @@ import lombok.Getter;
public enum RevenueTypeEnum {
PLAN("Plan", "计划购买"),
MODEL_CALL("ModelCall", "模型调用"),
TOOL_CALL("ToolCall", "工具调用");
TOOL_CALL("ToolCall", "工具调用"),
PROJECT_PAY("ProjectPay", "项目支付");
private final String code;
private final String desc;

View File

@@ -22,5 +22,9 @@
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-bill-api</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-pay-spec</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -5,20 +5,24 @@ import com.xspaceagi.bill.app.service.BillRevenueAppService;
import com.xspaceagi.bill.app.service.BillWithdrawAppService;
import com.xspaceagi.bill.app.service.ResourceStatAppService;
import com.xspaceagi.bill.sdk.dto.*;
import com.xspaceagi.bill.spec.enums.PayStatusEnum;
import com.xspaceagi.bill.spec.enums.ResourceStatTypeEnum;
import com.xspaceagi.bill.spec.enums.RevenueTargetTypeEnum;
import com.xspaceagi.bill.spec.enums.RevenueTypeEnum;
import com.xspaceagi.pay.sdk.dto.CashierSessionCreateRequest;
import com.xspaceagi.pay.sdk.dto.CashierSessionCreateResponse;
import com.xspaceagi.pay.sdk.service.IPaymentRpcService;
import com.xspaceagi.pay.sdk.dto.OrderAndTransactionCreateResponse;
import com.xspaceagi.pay.sdk.support.PayAppWebViewDetector;
import com.xspaceagi.pay.spec.support.PayAppNativeInAppGuard;
import com.xspaceagi.pay.spec.support.PayH5InAppGuard;
import com.xspaceagi.system.spec.common.RequestContext;
import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
import com.xspaceagi.system.spec.exception.BizException;
import com.xspaceagi.system.spec.utils.IPUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -42,9 +46,6 @@ public class BillController {
@Resource
private BillWithdrawAppService billWithdrawAppService;
@Resource
private IPaymentRpcService iPaymentRpcService;
@Resource
private ResourceStatAppService resourceStatAppService;
@@ -143,26 +144,88 @@ public class BillController {
@RequestParam Long orderId, @RequestParam String returnUrl) {
Assert.notNull(orderId, "orderId can not be null");
Assert.hasText(returnUrl, "returnUrl can not be blank");
OrderDTO orderDTO = billOrderAppService.queryOrder(orderId);
if (orderDTO == null) {
throw new IllegalArgumentException("order not found");
}
if (!RequestContext.get().getUserId().equals(orderDTO.getUserId())) {
throw new BizException("USER_NOT_MATCH", "The order data does not match the current user.");
}
if (orderDTO.getPayStatus() != PayStatusEnum.PENDING) {
throw new BizException("ORDER_NOT_PENDING", "The order status is incorrect.");
}
if (orderDTO.getExtra().get("gatewayPaymentOrderNo") == null) {
throw new BizException("The order data is incomplete.");
}
CashierSessionCreateRequest paymentCreateCashierSessionRequest = new CashierSessionCreateRequest();
paymentCreateCashierSessionRequest.setGatewayPaymentOrderNo(String.valueOf(orderDTO.getExtra().get("gatewayPaymentOrderNo")));
paymentCreateCashierSessionRequest.setBizRedirectUrl(validateSettlementPageUrl(returnUrl.trim()));
CashierSessionCreateResponse cashierSession = iPaymentRpcService.createCashierSession(paymentCreateCashierSessionRequest);
CashierSessionCreateResponse cashierSession = billOrderAppService.invokeScanCashier(
RequestContext.get().getUserId(),
orderId,
validateSettlementPageUrl(returnUrl.trim()));
return ReqResult.success(cashierSession);
}
@PostMapping("/order/pay/minipay")
@Operation(
summary = "小程序调起支付",
description = "调起微信/支付宝小程序支付,返回 wx.requestPayment / my.tradePay 所需参数")
public ReqResult<OrderAndTransactionCreateResponse> invokeMiniPay(
@RequestBody MiniPayInvokeRequest request, HttpServletRequest httpServletRequest) {
Assert.notNull(request, "request cannot be null");
Assert.notNull(request.getOrderId(), "orderId can not be null");
String clientIp = IPUtil.getIpAddr(httpServletRequest);
return ReqResult.success(
billOrderAppService.invokeMiniPay(RequestContext.get().getUserId(), request, clientIp));
}
@PostMapping("/order/pay/wechat-jsapi")
@Operation(
summary = "微信内 H5 JSAPI 调起支付",
description = "仅微信内置浏览器:先 GET /api/system/wechat/oa/oauth-url 完成 snsapi_base 授权,"
+ "再传 wxOAuthCode返回 wxPayParams 供 WeixinJSBridge.getBrandWCPayRequest"
+ "付后须轮询 GET /api/bill/order/settlement-status")
public ReqResult<OrderAndTransactionCreateResponse> invokeWeChatJsapi(
@RequestBody WeChatJsapiInvokeRequest request, HttpServletRequest httpServletRequest) {
Assert.notNull(request, "request cannot be null");
Assert.notNull(request.getOrderId(), "orderId can not be null");
String clientIp = IPUtil.getIpAddr(httpServletRequest);
return ReqResult.success(
billOrderAppService.invokeWeChatJsapi(RequestContext.get().getUserId(), request, clientIp));
}
@PostMapping("/order/pay/h5-web")
@Operation(
summary = "系统浏览器 H5 调起支付",
description = "调起 h5 支付,返回 invokeType + formHtml/redirectUrl支付后轮询 settlement-status。"
+ "App WebView 内禁止调用")
public ReqResult<OrderAndTransactionCreateResponse> invokeH5Web(
@RequestBody H5WebInvokeRequest request, HttpServletRequest httpServletRequest) {
Assert.notNull(request, "request cannot be null");
Assert.notNull(request.getOrderId(), "orderId can not be null");
PayH5InAppGuard.assertH5PayAllowed(
httpServletRequest.getHeader(PayAppWebViewDetector.HEADER_CLIENT_TYPE),
httpServletRequest.getHeader("User-Agent"));
String clientIp = IPUtil.getIpAddr(httpServletRequest);
return ReqResult.success(
billOrderAppService.invokeH5Web(RequestContext.get().getUserId(), request, clientIp));
}
@PostMapping("/order/pay/app-native")
@Operation(
summary = "App 原生 SDK 调起支付",
description = "WebView/App 壳内调起支付。支付后轮询 settlement-status。"
+ "仅限 App WebView 内调用")
public ReqResult<OrderAndTransactionCreateResponse> invokeAppNative(
@RequestBody AppNativeInvokeRequest request, HttpServletRequest httpServletRequest) {
Assert.notNull(request, "request cannot be null");
Assert.notNull(request.getOrderId(), "orderId can not be null");
PayAppNativeInAppGuard.assertAppNativePayRequired(
httpServletRequest.getHeader(PayAppWebViewDetector.HEADER_CLIENT_TYPE),
httpServletRequest.getHeader("User-Agent"));
String clientIp = IPUtil.getIpAddr(httpServletRequest);
return ReqResult.success(
billOrderAppService.invokeAppNative(RequestContext.get().getUserId(), request, clientIp));
}
@RequestMapping(value = "/order/pay/front-notify", method = {RequestMethod.GET, RequestMethod.POST})
@Operation(
summary = "H5 渠道前台回跳中转",
description = "接收渠道前台回调GET/POST统一 302 到前端结算页")
public void handleH5FrontNotify(
@RequestParam(required = false) Long orderId,
@RequestParam String returnUrl,
HttpServletResponse response) {
String target = validateSettlementPageUrl(returnUrl.trim());
response.setStatus(HttpServletResponse.SC_FOUND);
response.setHeader("Location", target);
}
/** 校验前端传入的结算页 URLhttp/https。 */
private static String validateSettlementPageUrl(String url) {
try {

View File

@@ -10,9 +10,9 @@ import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* 查询doris表数据
* 查询表数据
*/
@Schema(description = "查询doris表数据")
@Schema(description = "查询表数据")
@Getter
@Setter
@NoArgsConstructor

View File

@@ -1,19 +1,18 @@
package com.xspaceagi.compose.sdk.vo.define;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.*;
import java.util.List;
@Schema(description = "表结构定义,新增数据表")
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class CreateTableDefineVo {
// /**

View File

@@ -1,19 +1,18 @@
package com.xspaceagi.compose.sdk.vo.define;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import java.time.LocalDateTime;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@Schema(description = "表结构定义")
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class TableDefineVo {
/**

View File

@@ -3,14 +3,14 @@ package com.xspaceagi.compose.sdk.vo.define;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.*;
@Schema(description = "表字段定义")
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class TableFieldDefineVo {
/**

View File

@@ -6,6 +6,8 @@ import com.xspaceagi.compose.domain.model.CustomTableDefinitionModel;
import com.xspaceagi.compose.domain.repository.ICustomTableDefinitionRepository;
import com.xspaceagi.compose.ui.base.BaseController;
import com.xspaceagi.compose.ui.dto.*;
import com.xspaceagi.sandbox.SandboxUtils;
import com.xspaceagi.system.application.service.SpaceApplicationService;
import com.xspaceagi.system.application.util.DefaultIconUrlUtil;
import com.xspaceagi.system.domain.log.LogPrint;
import com.xspaceagi.system.infra.service.QueryVoListDelegateService;
@@ -26,6 +28,7 @@ import com.xspaceagi.system.spec.page.SuperPage;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.*;
@@ -43,24 +46,32 @@ public class ComposeDbTableConfigController extends BaseController {
@Resource
private QueryVoListDelegateService queryVoListDelegateService;
@Resource
private IUserDataPermissionRpcService userDataPermissionRpcService;
@Resource
private SpacePermissionService spacePermissionService;
@Resource
private SpaceApplicationService spaceApplicationService;
@RequireResource(ResourceEnum.COMPONENT_LIB_CREATE)
@OperationLogReporter(actionType = ActionType.ADD, action = "新增表定义", objectName = "表定义配置", systemCode = SystemEnum.DB_TABLE)
@LogPrint(step = "表定义配置-新增表定义")
@Operation(summary = "新增表定义")
@PostMapping("/add")
public ReqResult<Long> addTableDefinition(@RequestBody CustomTableAddRequest request) {
public ReqResult<Long> addTableDefinition(@RequestBody CustomTableAddRequest addRequest, HttpServletRequest request) {
if (SandboxUtils.isSandboxRequest(request)) {
if (addRequest.getSpaceId() == null) {
Long personalSpaceId = spaceApplicationService.getPersonalSpaceId(RequestContext.get().getUserId());
addRequest.setSpaceId(personalSpaceId);
}
}
UserContext userContext = this.getUser();
UserDataPermissionDto userDataPermission = userDataPermissionRpcService.getUserDataPermission(userContext.getUserId());
Long ct = customTableDefinitionApplicationService.countUserTotalTable(userContext.getUserId());
userDataPermission.checkMaxDataTableCount(ct.intValue());
var model = CustomTableAddRequest.convert2Model(request);
var model = CustomTableAddRequest.convert2Model(addRequest);
var data = customTableDefinitionApplicationService
.addInfo(model, userContext);
@@ -114,7 +125,16 @@ public class ComposeDbTableConfigController extends BaseController {
@Operation(summary = "查询表定义列表")
@RequestMapping(path = "/list", method = RequestMethod.POST)
public ReqResult<IPage<CustomTableDefinitionModel>> list(
@RequestBody PageQueryVo<CustomTableQueryRequest> pageQueryVo) {
@RequestBody PageQueryVo<CustomTableQueryRequest> pageQueryVo, HttpServletRequest request) {
if (SandboxUtils.isSandboxRequest(request)) {
if (pageQueryVo.getQueryFilter() == null) {
pageQueryVo.setQueryFilter(new CustomTableQueryRequest());
}
if (pageQueryVo.getQueryFilter().getSpaceId() == null) {
Long personalSpaceId = spaceApplicationService.getPersonalSpaceId(RequestContext.get().getUserId());
pageQueryVo.getQueryFilter().setSpaceId(personalSpaceId);
}
}
Assert.notNull(pageQueryVo, "pageQueryVo is null");
Assert.notNull(pageQueryVo.getQueryFilter(), "queryFilter is null");
Assert.notNull(pageQueryVo.getQueryFilter().getSpaceId(), "spaceId is null");

View File

@@ -10,6 +10,7 @@ import com.xspaceagi.credit.app.service.CreditPackageService;
import com.xspaceagi.credit.app.service.CreditService;
import com.xspaceagi.credit.sdk.dto.*;
import com.xspaceagi.credit.spec.enums.CreditTypeEnum;
import com.xspaceagi.pay.sdk.enums.PayMode;
import com.xspaceagi.system.application.dto.TenantConfigDto;
import com.xspaceagi.system.spec.common.RequestContext;
import com.xspaceagi.system.spec.dto.ReqResult;
@@ -95,7 +96,10 @@ public class UserCreditController {
@PostMapping("/order/create")
@Operation(summary = "创建积分增购订单")
public ReqResult<OrderDTO> createOrder(@RequestParam Long packageId, HttpServletRequest httpServletRequest) {
public ReqResult<OrderDTO> createOrder(
@RequestParam Long packageId,
@RequestParam(required = false) PayMode payMode,
HttpServletRequest httpServletRequest) {
CreditPackageDTO packageById = creditPackageService.getPackageById(packageId);
if (packageById == null) {
return ReqResult.error(I18nUtil.systemMessage("Backend.Credit.Package.Error.NotFound"));
@@ -113,6 +117,7 @@ public class UserCreditController {
item.setCount(1);
item.setSnapshot(JSON.parseObject(JSON.toJSONString(packageById)));
request.setItems(List.of(item));
request.setPayMode(payMode);
OrderDTO order = iBillRpcService.createOrder(request);
return ReqResult.success(order);
}

View File

@@ -30,6 +30,12 @@ public interface ICustomPageCodingApplicationService {
*/
ReqResult<Map<String, Object>> uploadSingleFile(Long projectId, MultipartFile file, String filePath,
UserContext userContext);
/**
* 批量上传文件
*/
ReqResult<Map<String, Object>> uploadBatchFiles(Long projectId, List<MultipartFile> files,
List<String> filePaths, UserContext userContext);
/**
* 获取单个文件的反向代理地址
*/

View File

@@ -85,6 +85,25 @@ public class CustomPageCodingApplicationServiceImpl implements ICustomPageCoding
return result;
}
@Override
public ReqResult<Map<String, Object>> uploadBatchFiles(Long projectId, List<MultipartFile> files,
List<String> filePaths, UserContext userContext) {
log.info("[Application] project Id={},upload batch files, fileCount={}", projectId, files != null ? files.size() : 0);
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");
}
if (filePaths == null || filePaths.size() != files.size()) {
throw new IllegalArgumentException("filePaths and files count mismatch");
}
ReqResult<Map<String, Object>> result = customPageCodingDomainService.uploadBatchFiles(projectId, files,
filePaths, userContext);
log.info("[Application] project Id={},upload batch files completed,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);

View File

@@ -4,12 +4,15 @@ 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.ResourceGroupApplicationService;
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.ResourceGroupDto;
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.adapter.repository.entity.ResourceGroupRelation;
import com.xspaceagi.agent.core.sdk.IAgentRpcService;
import com.xspaceagi.agent.core.sdk.dto.*;
import com.xspaceagi.agent.core.sdk.enums.TargetTypeEnum;
@@ -40,9 +43,11 @@ import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* 自定义页面配置应用服务实现
@@ -67,13 +72,15 @@ public class CustomPageConfigApplicationServiceImpl implements ICustomPageConfig
private IUserDataPermissionRpcService userDataPermissionRpcService;
@Resource
private ISandboxConfigRpcService sandboxConfigRpcService;
@Resource
private ResourceGroupApplicationService resourceGroupApplicationService;
// 需要事务
@Transactional(rollbackFor = Exception.class)
@Override
public ReqResult<CustomPageConfigModel> create(CustomPageConfigModel model, UserContext userContext)
throws JsonProcessingException {
log.info("[create] createproject,name={}", model.getName());
log.info("[create] create project,name={}", model.getName());
Optional.ofNullable(model.getName()).filter(StringUtils::isNotBlank)
.orElseThrow(() -> new IllegalArgumentException("projectName is required"));
@@ -91,7 +98,7 @@ public class CustomPageConfigApplicationServiceImpl implements ICustomPageConfig
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);
log.warn("[create] create project failed, user page count reached limit, user Id={}, current Count={}, max page app count={}", userContext.getUserId(), currentCount, maxPageAppCount);
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.customPageWebAppCountExceeded, maxPageAppCount);
}
}
@@ -102,7 +109,7 @@ public class CustomPageConfigApplicationServiceImpl implements ICustomPageConfig
userContext);
if (!configResult.isSuccess()) {
log.error("[create] create project failed(configtable), name={}, base Path={}, error={}",
log.error("[create] create project failed(config table), name={}, base Path={}, error={}",
model.getName(), model.getBasePath(), configResult.getMessage());
throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.customPageCreateConfigFailed,
configResult.getMessage() != null ? configResult.getMessage() : "");
@@ -132,7 +139,7 @@ public class CustomPageConfigApplicationServiceImpl implements ICustomPageConfig
userContext);
if (!buildResult.isSuccess()) {
log.error("[create] create project failed(buildtable), name={}, base Path={}, error={}",
log.error("[create] create project failed(build table), name={}, base Path={}, error={}",
model.getName(), model.getBasePath(), buildResult.getMessage());
throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.customPageCreateBuildFailed,
buildResult.getMessage() != null ? buildResult.getMessage() : "");
@@ -150,7 +157,7 @@ public class CustomPageConfigApplicationServiceImpl implements ICustomPageConfig
.createPageAppAgent(agentDto);
if (!agentResult.isSuccess()) {
log.error("[create] createagentfailed, name={}, error={}", model.getName(), agentResult.getMessage());
log.error("[create] create agent failed, name={}, error={}", model.getName(), agentResult.getMessage());
throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.customPageCreateAgentFailed,
agentResult.getMessage() != null ? agentResult.getMessage() : "");
}
@@ -170,7 +177,7 @@ public class CustomPageConfigApplicationServiceImpl implements ICustomPageConfig
result.getMessage() != null ? result.getMessage() : "");
}
log.info("[create] createprojectsucceeded, project Id={}, name={}", projectId, model.getName());
log.info("[create] create project succeeded, project Id={}, name={}", projectId, model.getName());
configResult.getData().setDevAgentId(agentResult.getData());
return ReqResult.success(configResult.getData());
}
@@ -180,7 +187,7 @@ public class CustomPageConfigApplicationServiceImpl implements ICustomPageConfig
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());
log.info("[upload-project] project Id={} start upload", model.getId());
if (file == null || file.isEmpty()) {
return ReqResult.error("0001", "File is required");
}
@@ -194,19 +201,19 @@ public class CustomPageConfigApplicationServiceImpl implements ICustomPageConfig
userContext);
if (!result.isSuccess()) {
log.info("[upload-project] uploadfailed, project Id={}, code={}, message={}",
log.info("[upload-project] upload failed, 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);
log.info("[upload-project] upload succeeded, 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);
log.error("[upload-project] project Id={},config file import failed", projectId, e);
// 配置文件导入失败不影响整体上传流程
}
}
@@ -229,13 +236,13 @@ public class CustomPageConfigApplicationServiceImpl implements ICustomPageConfig
ReqResult<CustomPageConfigModel> configResult = customPageConfigDomainService.create(model, userContext);
if (!configResult.isSuccess()) {
log.error("[create Proxy Project] create reverse proxy projectfailed, name={}, message={}",
log.error("[create Proxy Project] create reverse proxy project failed, 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={}",
log.info("[create Proxy Project] create reverse proxy project succeeded, project Id={}, name={}",
configResult.getData().getId(), model.getName());
return ReqResult.success(configResult.getData());
}
@@ -249,12 +256,12 @@ public class CustomPageConfigApplicationServiceImpl implements ICustomPageConfig
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,
log.error("[query Project Content] project Id={},query project file content failed,message={}", projectId,
result.getMessage());
return ReqResult.error(result.getCode(), result.getMessage());
}
log.info("[query Project Content] project Id={},query project file contentsucceeded", projectId);
log.info("[query Project Content] project Id={},query project file content succeeded", projectId);
return result;
}
@@ -266,13 +273,13 @@ public class CustomPageConfigApplicationServiceImpl implements ICustomPageConfig
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={}",
log.error("[query Project Content By Version] project Id={},code Version={},query project historical version file content failed,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,
log.info("[query Project Content By Version] project Id={},code Version={},query project historical version file content succeeded", projectId,
codeVersion);
return result;
}
@@ -297,12 +304,12 @@ public class CustomPageConfigApplicationServiceImpl implements ICustomPageConfig
proxyConfig, userContext);
if (!result.isSuccess()) {
log.error("[add Proxy] project Id={},env={},path={},add reverse proxy configfailed,message={}",
log.error("[add Proxy] project Id={},env={},path={},add reverse proxy config failed,message={}",
projectId, proxyConfig.getEnv(), proxyConfig.getPath(), result.getMessage());
return result;
}
log.info("[add Proxy] project Id={},env={},path={},add reverse proxy configsucceeded",
log.info("[add Proxy] project Id={},env={},path={},add reverse proxy config succeeded",
projectId, proxyConfig.getEnv(), proxyConfig.getPath());
return ReqResult.success(result.getData());
}
@@ -310,7 +317,7 @@ public class CustomPageConfigApplicationServiceImpl implements ICustomPageConfig
@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(),
log.info("[edit Proxy Config] project Id={},env={},path={},edit reverse proxy config", projectId, proxyConfig.getEnv(),
proxyConfig.getPath());
Optional.ofNullable(projectId).filter(x -> x > 0)
@@ -326,12 +333,12 @@ public class CustomPageConfigApplicationServiceImpl implements ICustomPageConfig
userContext);
if (!result.isSuccess()) {
log.error("[edit Proxy Config] project Id={},env={},path={},editreverse proxy configfailed,message={}",
log.error("[edit Proxy Config] project Id={},env={},path={},edit reverse proxy config failed,message={}",
projectId, proxyConfig.getEnv(), proxyConfig.getPath(), result.getMessage());
return result;
}
log.info("[edit Proxy Config] project Id={},env={},path={},editreverse proxy configsucceeded",
log.info("[edit Proxy Config] project Id={},env={},path={},edit reverse proxy config succeeded",
projectId, proxyConfig.getEnv(), proxyConfig.getPath());
return ReqResult.success(null);
}
@@ -351,13 +358,13 @@ public class CustomPageConfigApplicationServiceImpl implements ICustomPageConfig
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={}",
log.error("[delete Proxy] project Id={},env={},path={},delete reverse proxy config failed,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",
log.info("[delete Proxy] project Id={},env={},path={},delete reverse proxy config succeeded",
projectId, env, path);
return ReqResult.success(null);
}
@@ -376,13 +383,13 @@ public class CustomPageConfigApplicationServiceImpl implements ICustomPageConfig
userContext);
if (!result.isSuccess()) {
log.error("[save Path Args] project Id={},page Uri={},configure page argsfailed,message={}",
log.error("[save Path Args] project Id={},page Uri={},configure page args failed,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",
log.info("[save Path Args] project Id={},page Uri={},configure page args succeeded",
projectId, pageArgConfig.getPageUri());
return ReqResult.success(null);
}
@@ -401,13 +408,13 @@ public class CustomPageConfigApplicationServiceImpl implements ICustomPageConfig
userContext);
if (!result.isSuccess()) {
log.error("[add Path] project Id={},page Uri={},add path configfailed,message={}",
log.error("[add Path] project Id={},page Uri={},add path config failed,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",
log.info("[add Path] project Id={},page Uri={},add path config succeeded",
projectId, pageArgConfig.getPageUri());
return ReqResult.success(null);
}
@@ -415,7 +422,7 @@ public class CustomPageConfigApplicationServiceImpl implements ICustomPageConfig
@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());
log.info("[edit Path] project Id={},page Uri={},edit path config", projectId, pageArgConfig.getPageUri());
Optional.ofNullable(projectId).filter(x -> x > 0)
.orElseThrow(() -> new IllegalArgumentException("projectId is required or invalid"));
@@ -426,13 +433,13 @@ public class CustomPageConfigApplicationServiceImpl implements ICustomPageConfig
userContext);
if (!result.isSuccess()) {
log.error("[edit Path] project Id={},page Uri={},editpath configfailed,message={}",
log.error("[edit Path] project Id={},page Uri={},edit path config failed,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",
log.info("[edit Path] project Id={},page Uri={},edit path config succeeded",
projectId, pageArgConfig.getPageUri());
return ReqResult.success(null);
}
@@ -451,13 +458,13 @@ public class CustomPageConfigApplicationServiceImpl implements ICustomPageConfig
userContext);
if (!result.isSuccess()) {
log.error("[delete Path] project Id={},page Uri={},delete path configfailed,message={}",
log.error("[delete Path] project Id={},page Uri={},delete path config failed,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",
log.info("[delete Path] project Id={},page Uri={},delete path config succeeded",
projectId, pageUri);
return ReqResult.success(null);
}
@@ -478,13 +485,13 @@ public class CustomPageConfigApplicationServiceImpl implements ICustomPageConfig
userContext);
if (!result.isSuccess()) {
log.error("[batch Config Proxy] project Id={},config Count={},batch configure reverse proxyfailed,message={}", projectId,
log.error("[batch Config Proxy] project Id={},config Count={},batch configure reverse proxy failed,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",
log.info("[batch Config Proxy] project Id={},config Count={},batch configure reverse proxy succeeded",
projectId, proxyConfigs != null ? proxyConfigs.size() : 0);
return ReqResult.success(null);
}
@@ -501,7 +508,7 @@ public class CustomPageConfigApplicationServiceImpl implements ICustomPageConfig
@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,
log.info("[bind Data Source] project Id={},type={},data Source Id={},bind data source", projectId, type,
dataSourceId);
Optional.ofNullable(projectId).filter(x -> x > 0)
@@ -516,25 +523,25 @@ public class CustomPageConfigApplicationServiceImpl implements ICustomPageConfig
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);
log.error("[bind Data Source] project Id={},type={},data Source Id={},plugin not found or 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);
log.info("[bind Data Source] project Id={},type={},data Source Id={},get plugin 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);
log.error("[bind Data Source] project Id={},type={},data Source Id={},workflow not found or 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);
log.info("[bind Data Source] project Id={},type={},data Source Id={},get workflow 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);
@@ -552,13 +559,13 @@ public class CustomPageConfigApplicationServiceImpl implements ICustomPageConfig
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={}",
log.error("[bind Data Source] save data source failed, 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);
log.info("[bind Data Source] save data source succeeded, project Id={}, type={}, data Source Id={}", projectId, type, dataSourceId);
return ReqResult.success(null);
}
@@ -566,7 +573,7 @@ public class CustomPageConfigApplicationServiceImpl implements ICustomPageConfig
@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,
log.info("[unbind Data Source] project Id={},type={},data Source Id={},unbind data source", projectId, type,
dataSourceId);
Optional.ofNullable(projectId).filter(x -> x > 0)
@@ -584,18 +591,18 @@ public class CustomPageConfigApplicationServiceImpl implements ICustomPageConfig
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());
log.error("[unbind Data Source] unbind data source failed, 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);
log.info("[unbind Data Source] unbind data source succeeded, 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);
log.info("[get By Project Id] project Id={},query project", projectId);
return customPageConfigDomainService.getById(projectId);
}
@@ -618,7 +625,7 @@ public class CustomPageConfigApplicationServiceImpl implements ICustomPageConfig
ReqResult<CustomPageConfigModel> result = customPageConfigDomainService.update(model,
userContext);
if (!result.isSuccess()) {
log.error("[update Project] project Id={},modify projectfailed,message={}", model.getId(),
log.error("[update Project] project Id={},modify project failed,message={}", model.getId(),
result.getMessage());
throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.customPageUpdateProjectFailed,
result.getMessage() != null ? result.getMessage() : "");
@@ -633,16 +640,16 @@ public class CustomPageConfigApplicationServiceImpl implements ICustomPageConfig
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(),
log.error("[update Project] project Id={},update agent failed,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());
log.info("[update Project] project Id={},modify project succeeded", model.getId());
return ReqResult.success(result.getData());
} catch (Exception e) {
log.error("[update Project] project Id={},modify projectexception", model.getId(), e);
log.error("[update Project] project Id={},modify project exception", model.getId(), e);
throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.customPageUpdateProjectException,
e.getMessage() != null ? e.getMessage() : "");
}
@@ -658,7 +665,7 @@ public class CustomPageConfigApplicationServiceImpl implements ICustomPageConfig
ReqResult<Map<String, Object>> result = customPageConfigDomainService.delete(projectId, userContext);
if (!result.isSuccess()) {
log.error("[delete Project] project Id={},delete projectfailed,message={}", projectId, result.getMessage());
log.error("[delete Project] project Id={},delete project failed,message={}", projectId, result.getMessage());
throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.customPageDeleteProjectFailed,
result.getMessage() != null ? result.getMessage() : "");
}
@@ -670,20 +677,20 @@ public class CustomPageConfigApplicationServiceImpl implements ICustomPageConfig
com.xspaceagi.agent.core.sdk.dto.ReqResult<Void> agentResult = agentRpcService
.deletePageAppAgent(devAgentId);
if (!agentResult.isSuccess()) {
log.error("[delete Project] project Id={},deleteagentfailed,message={}", projectId,
log.error("[delete Project] project Id={},delete agent failed,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.error("[delete Project] project Id={},delete agent failed", projectId, e);
// 不抛异常,老数据没有智能体,删除会异常
}
log.info("[delete Project] project Id={},delete projectsucceeded", projectId);
log.info("[delete Project] project Id={},delete project succeeded", projectId);
return result;
} catch (Exception e) {
log.error("[delete Project] project Id={},delete projectexception", projectId, e);
log.error("[delete Project] project Id={},delete project exception", projectId, e);
throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.customPageDeleteProjectException,
e.getMessage() != null ? e.getMessage() : "");
}
@@ -700,7 +707,7 @@ public class CustomPageConfigApplicationServiceImpl implements ICustomPageConfig
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);
log.info("[copy Project] target Project Id={}, target Space Id={},start copy data source", targetProjectId, targetSpaceId);
//本空间复制项目
//直接绑定数据源
@@ -710,6 +717,15 @@ public class CustomPageConfigApplicationServiceImpl implements ICustomPageConfig
updateConfig.setId(targetProjectId);
updateConfig.setDataSources(sourceDataSources);
customPageConfigDomainService.update(updateConfig, userContext);
// 同空间复制,因为引用的是同一份数据源,已经被原项目分组引用,不创建新的资源分组
/*
Map<Long, Long> workflowIdMap = sourceDataSources.stream()
.filter(ds -> "workflow".equalsIgnoreCase(ds.getType()))
.collect(Collectors.toMap(DataSourceDto::getId, DataSourceDto::getId, (a, b) -> a));
copyProjectResourceGroup(sourceConfig.getId(), targetProjectId, targetConfig.getName(),
sourceSpaceId, targetSpaceId, workflowIdMap);
*/
return null;
}
@@ -719,6 +735,8 @@ public class CustomPageConfigApplicationServiceImpl implements ICustomPageConfig
List<DataSourceDto> newCreateDataSources = new ArrayList<>();
//目标绑定数据源
List<DataSourceDto> targetDataSources = new ArrayList<>();
// 复制工作流时记录旧ID到新ID的映射用于重建资源分组关系
Map<Long, Long> copiedWorkflowIdMap = new HashMap<>();
for (DataSourceDto dataSource : sourceDataSources) {
Published.TargetType dataSourceType = "plugin".equalsIgnoreCase(dataSource.getType()) ? Published.TargetType.Plugin : Published.TargetType.Workflow;
@@ -727,7 +745,7 @@ public class CustomPageConfigApplicationServiceImpl implements ICustomPageConfig
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);
log.info("[copy Project] project Id={},data Source Id={},type={}, data source not found,skip", targetProjectId, dataSource.getId(), dataSourceType);
continue;
}
@@ -737,7 +755,7 @@ public class CustomPageConfigApplicationServiceImpl implements ICustomPageConfig
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);
log.info("[copy Project] project Id={},data Source Id={},type={}, data source already published in target space, bind directly", targetProjectId, dataSource.getId(), dataSourceType);
targetDataSources.add(dataSource);
} else {
@@ -778,11 +796,11 @@ public class CustomPageConfigApplicationServiceImpl implements ICustomPageConfig
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());
log.error("[copy Project] project Id={},data Source Id={},type={},copy plugin failed,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());
log.info("[copy Project] project Id={},data Source Id={},type={},copy plugin succeeded", targetProjectId, dataSource.getId(), dataSourceType, enableResult.getData());
}
Long newPluginId = enableResult.getData();
DataSourceDto dataSourceDto = DataSourceDto.builder()
@@ -834,11 +852,11 @@ public class CustomPageConfigApplicationServiceImpl implements ICustomPageConfig
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());
log.error("[copy Project] project Id={},data Source Id={},type={},copy workflow failed,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);
log.info("[copy Project] project Id={},data Source Id={},type={},copy workflow succeeded", targetProjectId, dataSource.getId(), dataSourceType);
}
Long newWorkflowId = enableResult.getData();
DataSourceDto dataSourceDto = DataSourceDto.builder()
@@ -850,15 +868,20 @@ public class CustomPageConfigApplicationServiceImpl implements ICustomPageConfig
.build();
targetDataSources.add(dataSourceDto);
newCreateDataSources.add(dataSourceDto);
copiedWorkflowIdMap.put(dataSource.getId(), newWorkflowId);
}
}
}
}
if (!copiedWorkflowIdMap.isEmpty()) {
copyProjectResourceGroup(sourceConfig.getId(), targetProjectId, targetConfig.getName(),
sourceSpaceId, targetSpaceId, copiedWorkflowIdMap);
}
if (targetDataSources.isEmpty()) {
return newCreateDataSources;
}
log.info("[copy Project] target Project Id,target Space Id={},cross-spacecopy,binddata source,size={}", targetProjectId, targetSpaceId, targetDataSources.size());
log.info("[copy Project] target Project Id,target Space Id={},cross-space copy,bind data source,size={}", targetProjectId, targetSpaceId, targetDataSources.size());
CustomPageConfigModel updateConfig = new CustomPageConfigModel();
updateConfig.setId(targetProjectId);
updateConfig.setDataSources(targetDataSources);
@@ -867,4 +890,47 @@ public class CustomPageConfigApplicationServiceImpl implements ICustomPageConfig
return newCreateDataSources;
}
private void copyProjectResourceGroup(Long sourceProjectId, Long targetProjectId, String targetProjectName,
Long sourceSpaceId, Long targetSpaceId, Map<Long, Long> workflowIdMap) {
List<ResourceGroupDto> sourceGroups = resourceGroupApplicationService.queryList(
sourceProjectId.toString(), List.of(TargetTypeEnum.Workflow.name()), sourceSpaceId);
if (sourceGroups.isEmpty()) {
return;
}
ResourceGroupDto sourceGroup = sourceGroups.get(0);
List<ResourceGroupRelation> relations = resourceGroupApplicationService.queryGroupRelations(sourceGroup.getId());
if (relations == null || relations.isEmpty()) {
return;
}
Long groupId = getOrCreateProjectResourceGroup(targetProjectId, targetProjectName, targetSpaceId, sourceGroup.getDescription());
for (ResourceGroupRelation relation : relations) {
if (!TargetTypeEnum.Workflow.name().equals(relation.getTargetType())) {
continue;
}
Long newWorkflowId = workflowIdMap.get(relation.getTargetId());
if (newWorkflowId != null) {
resourceGroupApplicationService.addResourceToGroup(groupId, TargetTypeEnum.Workflow.name(), newWorkflowId);
}
}
}
private Long getOrCreateProjectResourceGroup(Long projectId, String projectName, Long spaceId, String description) {
String groupName = projectId.toString();
List<ResourceGroupDto> existingGroups = resourceGroupApplicationService.queryList(
groupName, List.of(TargetTypeEnum.Workflow.name()), spaceId);
if (!existingGroups.isEmpty()) {
return existingGroups.get(0).getId();
}
ResourceGroupDto resourceGroupDto = new ResourceGroupDto();
resourceGroupDto.setName(groupName);
resourceGroupDto.setDescription(projectName);
resourceGroupDto.setIcon(null);
resourceGroupDto.setType(TargetTypeEnum.Workflow.name());
resourceGroupDto.setSpaceId(spaceId);
return resourceGroupApplicationService.add(resourceGroupDto);
}
}

View File

@@ -3,10 +3,12 @@ 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.fasterxml.jackson.core.JsonProcessingException;
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.ICustomPageBuildApplicationService;
import com.xspaceagi.custompage.application.service.ICustomPageConfigApplicationService;
import com.xspaceagi.custompage.domain.model.CustomPageBuildModel;
import com.xspaceagi.custompage.domain.model.CustomPageConfigModel;
@@ -16,14 +18,15 @@ 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.custompage.sdk.dto.*;
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.common.UserContext;
import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.spec.enums.YesOrNoEnum;
import com.xspaceagi.system.spec.page.PageQueryVo;
import com.xspaceagi.system.spec.page.SuperPage;
@@ -65,6 +68,53 @@ public class CustomPageRpcServiceImpl implements ICustomPageRpcService {
@Resource
private ICustomPageConfigApplicationService customPageConfigApplicationService;
@Resource
private ICustomPageBuildApplicationService customPageBuildApplicationService;
@Override
public String create(Long userId, Long spaceId, String name) {
TenantConfigDto tenantConfig = (TenantConfigDto) RequestContext.get().getTenantConfig();
CustomPageConfigModel model = new CustomPageConfigModel();
model.setName(name);
model.setSpaceId(spaceId);
model.setNeedLogin(YesOrNoEnum.Y.getKey());
model.setProjectType(ProjectType.ONLINE_DEPLOY);
UserContext userContext = UserContext.builder()
.userId(userId)
.tenantConfig(tenantConfig)
.tenantId(tenantConfig.getTenantId())
.build();
try {
ReqResult<CustomPageConfigModel> customPageConfigModelReqResult = customPageConfigApplicationService.create(model, userContext);
if (customPageConfigModelReqResult.isSuccess()) {
// 初始化项目
ReqResult<Map<String, Object>> initResult = customPageBuildApplicationService.initProject(customPageConfigModelReqResult.getData().getId(), TemplateTypeEnum.REACT, userContext);
if (!initResult.isSuccess()) {
customPageConfigApplicationService.deleteProject(customPageConfigModelReqResult.getData().getId(), userContext);
throw new RuntimeException(initResult.getMessage());
}
return customPageConfigModelReqResult.getData().getId().toString();
}
throw new RuntimeException(customPageConfigModelReqResult.getMessage());
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
@Override
public void bindDataSource(Long userId, Long projectId, String type, Long dataSourceId) {
TenantConfigDto tenantConfig = (TenantConfigDto) RequestContext.get().getTenantConfig();
UserContext userContext = UserContext.builder()
.userId(userId)
.tenantConfig(tenantConfig)
.tenantId(tenantConfig.getTenantId())
.build();
ReqResult<Void> voidReqResult = customPageConfigApplicationService.bindDataSource(projectId, type, dataSourceId, userContext);
if (!voidReqResult.isSuccess()) {
throw new RuntimeException(voidReqResult.getMessage());
}
}
@Override
public List<CustomPageDto> list(CustomPageQueryReq req) {
log.info("[RPC] queryfrontend pageprojectlist, request={}", req);
@@ -319,6 +369,8 @@ public class CustomPageRpcServiceImpl implements ICustomPageRpcService {
}
if (completeDataSources && CollectionUtils.isNotEmpty(dto.getDataSources())) {
List<Long> workflowIds = dto.getDataSources().stream().filter(dataSource -> "workflow".equalsIgnoreCase(dataSource.getType())).map(DataSourceDto::getId).toList();
Map<Long, WorkflowConfigDto> workflowConfigDtoMap = workflowApplicationService.queryPublishedWorkflowConfigs(workflowIds).stream().collect(Collectors.toMap(WorkflowConfigDto::getId, workflowConfigDto -> workflowConfigDto, (v1, v2) -> v1));
dto.getDataSources().forEach(dataSource -> {
String type = dataSource.getType();
Long dataSourceId = dataSource.getId();
@@ -332,7 +384,7 @@ public class CustomPageRpcServiceImpl implements ICustomPageRpcService {
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);
WorkflowConfigDto workflowConfigDto = workflowConfigDtoMap.get(dataSourceId);
if (workflowConfigDto != null) {
dataSource.setName(workflowConfigDto.getName());
dataSource.setIcon(workflowConfigDto.getIcon());
@@ -372,6 +424,7 @@ public class CustomPageRpcServiceImpl implements ICustomPageRpcService {
dto.setExt(model.getExt());
dto.setTenantId(model.getTenantId());
dto.setSpaceId(model.getSpaceId());
dto.setSandboxId(model.getSandboxId());
dto.setCreated(model.getCreated());
dto.setCreatorId(model.getCreatorId());
dto.setCreatorName(model.getCreatorName());

View File

@@ -202,7 +202,65 @@ public final class CustomPagePromptConstants {
**核心记忆**
- 现有项目 = 先识别模版类型和框架,再用对应的框架语法编码
- **绝不擅自转换模版/框架**vue3-vite 项目保持 Vue 3react-vite 项目保持 React
<DATA_TABLE_SKILL>
你在开发页面应用时,始终拥有 @datatable-for-page-api 技能可用。
当用户的页面需要数据表支持时,使用该技能完成以下操作:
- 创建数据表、修改表名称/描述
- 定义/修改表字段结构(列名、类型等)
- 删除表、查询表列表、查看表详情、复制表结构
- 创建/更新数据表 SQL 操作 API供页面组件调用
使用流程:先创建表并定义字段结构,再创建 SQL 操作 API 供前端调用。
加载方式:从工作目录的 skills/datatable-for-page-api/ 读取 SKILL.md 和脚本。
⚠️ **projectId 获取方式**:创建 SQL 操作 API 需要 projectId直接从环境变量 `DEV_PROJECT_ID` 读取。**绝对禁止创建测试 SQL 工作流(如 `SELECT 1`)来探测 projectId 或验证连通性**——平台没有删除接口,测试工作流会永久残留。
⚠️ **spaceId 获取方式(关键)**:创建/查询数据表必须使用项目所在空间,`spaceId` 直接来自环境变量 `DEV_SPACE_ID`(脚本 `add-table` / `list-tables` 已自动读取并注入请求体)。**绝对禁止**省略 spaceId 或改用个人空间——后端在 spaceId 为空时会回退到调用者的个人空间,导致表建到/查到错误的个人空间里,前端项目无法使用。
</DATA_TABLE_SKILL>
<PAYMENT_SKILL>
你在开发项目时,始终拥有 @nuwax-pay 技能可用。
当用户的项目需要支付/收款功能时,使用该技能完成以下操作:
- 收银台模式:创建订单并获取收银台跳转链接(最简接入,适合大多数场景)
- H5 支付模式:创建订单后调起微信/支付宝 H5 支付(可自定义支付 UI
- 支付状态查询:轮询订单支付结果
加载方式:从工作目录的 skills/nuwax-pay/ 读取 SKILL.md。
⚠️ 前端调用时使用 /api/pay/general/ 路径(同源),不要使用 /api/v1/4sandbox/ 路径。
</PAYMENT_SKILL>
<PROJECT_MEMORY>
# 项目记忆:.project.md项目开发中枢强制执行
## 核心机制
本项目根目录维护一份 `.project.md` 文件,作为 AI 的「项目记忆中枢」集中沉淀项目设计、数据表结构、SQL 操作 API及其调用端点等关键信息。
- 进入项目做任何开发前,必须先阅读 `.project.md`(不存在则创建)。
- 开发过程中,凡是产生了应当长期保留的信息(新建/修改了表、新建/更新了 SQL 操作 API、确定了端点 token、调整了字段、确定了前端调用约定等必须同步更新 `.project.md`。
- `.project.md` 是团队/后续 AI 的唯一事实来源single source of truth代码与文档不一致时以最新更新为准。
## 关于「SQL 操作 API」的真相关键避免反复踩坑
平台上通过 datatable-for-page-api 技能创建的「数据表 SQL 操作 API」本质上**会被实例化为一个 Workflow工作流**,工作流内部包含一个 SQL 节点,真正去读写底层物理表(形如 `custom_table_{表ID}`)。
- 前端页面调用时,调用的就是这个工作流,调用路径为:`POST /api/page/w/{token}`(同源,浏览器自带登录态,不要在前端请求头里放鉴权密钥)。
- 每个这样的工作流都有一个唯一的**端点 token**(一串数字),前端 lib 层靠这张「功能名 → token」映射表来调用。
- ⚠️ 该 token 通常不直接出现在创建接口的返回体里需要从平台「SQL 操作 API / 工作流列表」中获取。**一旦获取到,必须立即登记进 `.project.md`,下次无需再重新查找。**
## .project.md 必须维护的内容(建议结构)
1. **项目概述**:项目用途、技术栈、模版类型(如 react-vite / vue3-vite
2. **数据表清单**:每张表登记 —— 表名 / 表ID / 物理表名dorisTable如 `custom_table_398`/ 字段清单(字段名、类型、是否系统字段、说明)。
3. **SQL 操作 API工作流清单**:逐条登记 ——
- 功能名(如 getAll / add / update / delete / search / getByXxx
- 工作流 ID如平台分配的 workflow id
- **端点 token**(前端 `/api/page/w/{token}` 调用用的那串数字)
- 对应 SQL 节点的 SQL 文本(用 `{{var}}` / `${{var}}` 占位符)
- 入参清单(参数名、是否必填、用途;模糊查询统一用 `{{var}}` 普通参数占位符SQL 写 `LIKE {{keyword}}`,前端传值时自拼 `%关键词%`,不要用 `${{var}}`
- 出参说明(响应数据在 `data.outputList`,写操作通常只回 success 不回填 id
- 绑定的物理表
4. **前端调用约定**lib 层文件位置、字段映射规则(后端列名 ↔ 前端模型名)、写后刷新策略、跨表级联顺序等。
5. **变更记录**:日期 + 改动摘要(便于回溯)。
## 操作纪律
- 建表 / 建或更新SQL 操作 API 后,立即把表 ID、物理表名、token、SQL、入出参写进 `.project.md`,再开始写前端代码。
- 前端开发时,端点 token、字段名一律**从 `.project.md` 取**,不要凭记忆或猜测。
- `.project.md` 只存「项目级、跨会话需要保留」的信息;临时调试信息不要塞进来,保持文件精炼可读。
</PROJECT_MEMORY>
<THINKING_REQUIREMENTS>
回应之前,你必须遵循这个确切的前端开发工作流程:
@@ -430,7 +488,65 @@ public final class CustomPagePromptConstants {
**核心記憶**
- 現有專案 = 先識別模板類型和框架,再用對應的框架語法編碼
- **絕不擅自轉換模板/框架**vue3-vite 專案保持 Vue 3react-vite 專案保持 React
<DATA_TABLE_SKILL>
你在開發頁面應用時,始終擁有 @datatable-for-page-api 技能可用。
當使用者的頁面需要資料表支援時,使用該技能完成以下操作:
- 建立資料表、修改表名稱/描述
- 定義/修改表欄位結構(欄位名稱、型別等)
- 刪除表、查詢表列表、查看表詳情、複製表結構
- 建立/更新資料表 SQL 操作 API供頁面元件呼叫
使用流程:先建立表並定義欄位結構,再建立 SQL 操作 API 供前端呼叫。
載入方式:從工作目錄的 skills/datatable-for-page-api/ 讀取 SKILL.md 和腳本。
⚠️ **projectId 取得方式**:建立 SQL 操作 API 需要 projectId直接從環境變數 `DEV_PROJECT_ID` 讀取。**絕對禁止建立測試 SQL 工作流(如 `SELECT 1`)來探測 projectId 或驗證連通性**——平台沒有刪除介面,測試工作流會永久殘留。
⚠️ **spaceId 取得方式(關鍵)**:建立/查詢資料表必須使用專案所在空間,`spaceId` 直接來自環境變數 `DEV_SPACE_ID`(腳本 `add-table` / `list-tables` 已自動讀取並注入請求體)。**絕對禁止**省略 spaceId 或改用個人空間——後端在 spaceId 為空時會回退到呼叫者的個人空間,導致表建到/查到錯誤的個人空間裡,前端專案無法使用。
</DATA_TABLE_SKILL>
<PAYMENT_SKILL>
你在開發專案時,始終擁有 @nuwax-pay 技能可用。
當使用者的專案需要支付/收款功能時,使用該技能完成以下操作:
- 收銀台模式:建立訂單並取得收銀台跳轉連結(最簡接入,適合大多數場景)
- H5 支付模式:建立訂單後調起微信/支付寶 H5 支付(可自訂支付 UI
- 支付狀態查詢:輪詢訂單支付結果
載入方式:從工作目錄的 skills/nuwax-pay/ 讀取 SKILL.md。
⚠️ 前端呼叫時使用 /api/pay/general/ 路徑(同源),不要使用 /api/v1/4sandbox/ 路徑。
</PAYMENT_SKILL>
<PROJECT_MEMORY>
# 專案記憶:.project.md專案開發中樞強制執行
## 核心機制
本專案根目錄維護一份 `.project.md` 檔案,作為 AI 的「專案記憶中樞」集中沉澱專案設計、資料表結構、SQL 操作 API及其呼叫端點等關鍵資訊。
- 進入專案做任何開發前,必須先閱讀 `.project.md`(不存在則建立)。
- 開發過程中,凡是產生了應當長期保留的資訊(新建/修改了表、新建/更新了 SQL 操作 API、確定了端點 token、調整了欄位、確定了前端呼叫約定等必須同步更新 `.project.md`。
- `.project.md` 是團隊/後續 AI 的唯一事實來源single source of truth程式碼與文件不一致時以最新更新為準。
## 關於「SQL 操作 API」的真相關鍵避免反覆踩坑
平台上透過 datatable-for-page-api 技能建立的「資料表 SQL 操作 API」本質上**會被實例化為一個 Workflow工作流**,工作流內部包含一個 SQL 節點,真正去讀寫底層物理表(形如 `custom_table_{表ID}`)。
- 前端頁面呼叫時,呼叫的就是這個工作流,呼叫路徑為:`POST /api/page/w/{token}`(同源,瀏覽器自帶登入態,不要在前端請求標頭裡放鑑權金鑰)。
- 每個這樣的工作流都有一個唯一的**端點 token**(一串數字),前端 lib 層靠這張「功能名 → token」對映表來呼叫。
- ⚠️ 該 token 通常不直接出現在建立介面的回應體裡需要從平台「SQL 操作 API / 工作流列表」中取得。**一旦取得到,必須立即登記進 `.project.md`,下次無需再重新查找。**
## .project.md 必須維護的內容(建議結構)
1. **專案概述**:專案用途、技術堆疊、模板類型(如 react-vite / vue3-vite
2. **資料表清單**:每張表登記 —— 表名 / 表ID / 物理表名dorisTable如 `custom_table_398`/ 欄位清單(欄位名稱、型別、是否系統欄位、說明)。
3. **SQL 操作 API工作流清單**:逐條登記 ——
- 功能名(如 getAll / add / update / delete / search / getByXxx
- 工作流 ID如平台分配的 workflow id
- **端點 token**(前端 `/api/page/w/{token}` 呼叫用的那串數字)
- 對應 SQL 節點的 SQL 文字(用 `{{var}}` / `${{var}}` 佔位符)
- 入參清單(參數名稱、是否必填、用途;模糊查詢統一用 `{{var}}` 普通參數佔位符SQL 寫 `LIKE {{keyword}}`,前端傳值時自拼 `%關鍵詞%`,不要用 `${{var}}`
- 出參說明(回應資料在 `data.outputList`,寫操作通常只回 success 不回填 id
- 綁定的物理表
4. **前端呼叫約定**lib 層檔案位置、欄位對映規則(後端列名 ↔ 前端模型名)、寫後重新整理策略、跨表級聯順序等。
5. **變更記錄**:日期 + 改動摘要(便於回溯)。
## 操作紀律
- 建表 / 建或更新SQL 操作 API 後,立即把表 ID、物理表名、token、SQL、入出參寫進 `.project.md`,再開始寫前端程式碼。
- 前端開發時,端點 token、欄位名稱一律**從 `.project.md` 取**,不要憑記憶或猜測。
- `.project.md` 只存「專案級、跨會話需要保留」的資訊;臨時除錯資訊不要塞進來,保持檔案精煉可讀。
</PROJECT_MEMORY>
<THINKING_REQUIREMENTS>
回應之前,你必須遵循這個確切的前端開發工作流程:
@@ -658,7 +774,65 @@ public final class CustomPagePromptConstants {
**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`
<DATA_TABLE_SKILL>
You always have the @datatable-for-page-api skill available when developing page applications.
When the user's page requires data table support, use this skill to perform the following operations:
- Create data tables, modify table name/description
- Define/modify table field definitions (column names, types, etc.)
- Delete tables, list tables, view table details, copy table structure
- Create/update data table SQL operation APIs (for page components to call)
Workflow: first create the table and define its field structure, then create SQL operation APIs for the frontend to call.
How to load: read SKILL.md and scripts from skills/datatable-for-page-api/ in the working directory.
⚠️ **How to obtain projectId**: Creating SQL operation APIs requires a projectId; read it directly from the `DEV_PROJECT_ID` environment variable. **It is strictly forbidden to create test SQL workflows (e.g., `SELECT 1`) to probe for projectId or verify connectivity** — the platform has no delete interface, so test workflows will remain permanently.
⚠️ **How to obtain spaceId (critical)**: Creating/querying data tables must use the project's space. The `spaceId` comes directly from the `DEV_SPACE_ID` environment variable (the `add-table` / `list-tables` scripts already read it automatically and inject it into the request body). **It is strictly forbidden** to omit spaceId or use the personal space — when spaceId is empty the backend falls back to the caller's personal space, causing tables to be created/queried in the wrong personal space, which the frontend project cannot use.
</DATA_TABLE_SKILL>
<PAYMENT_SKILL>
You always have the @nuwax-pay skill available when developing projects.
When the user's project requires payment / checkout functionality, use this skill to perform the following operations:
- Cashier mode: create an order and get a hosted cashier redirect URL (simplest integration, fits most scenarios)
- H5 pay mode: create an order then invoke WeChat/Alipay H5 payment (custom payment UI supported)
- Payment status query: poll the order payment result
How to load: read SKILL.md from skills/nuwax-pay/ in the working directory.
⚠️ When the frontend calls these APIs, use the /api/pay/general/ path (same-origin); do NOT use the /api/v1/4sandbox/ path.
</PAYMENT_SKILL>
<PROJECT_MEMORY>
# Project Memory: .project.md (Project Development Hub, Mandatory)
## Core Mechanism
This project maintains a `.project.md` file in the root directory as the AI's "project memory hub," centrally consolidating key information such as project design, data table structures, SQL operation APIs (and their calling endpoints), etc.
- Before doing any development in the project, you must first read `.project.md` (create it if it does not exist).
- During development, whenever information that should be retained long-term is produced (new/modified tables, new/updated SQL operation APIs, determined endpoint tokens, adjusted fields, determined frontend calling conventions, etc.), you must synchronously update `.project.md`.
- `.project.md` is the single source of truth for the team and subsequent AI. When code and documentation are inconsistent, the latest update prevails.
## The Truth About "SQL Operation APIs" (Critical, Avoid Repeated Pitfalls)
The "data table SQL operation APIs" created through the datatable-for-page-api skill on the platform are essentially **instantiated as a Workflow**, which internally contains a SQL node that actually reads and writes the underlying physical table (in the form `custom_table_{tableID}`).
- When the frontend page calls, it calls this workflow. The calling path is: `POST /api/page/w/{token}` (same origin, the browser carries the login state; do not put authentication keys in the frontend request headers).
- Each such workflow has a unique **endpoint token** (a string of numbers). The frontend lib layer relies on this "function name → token" mapping table to make calls.
- ⚠️ This token usually does not appear directly in the response body of the creation API. It needs to be obtained from the platform's "SQL Operation API / Workflow List." **Once obtained, it must be immediately registered in `.project.md`; there is no need to look it up again next time.**
## Content That Must Be Maintained in .project.md (Suggested Structure)
1. **Project Overview**: Project purpose, tech stack, template type (e.g., react-vite / vue3-vite).
2. **Data Table Inventory**: For each table, register — table name / table ID / physical table name (dorisTable, e.g., `custom_table_398`) / field list (field name, type, whether it is a system field, description).
3. **SQL Operation API (Workflow) Inventory**: Register each item —
- Function name (e.g., getAll / add / update / delete / search / getByXxx)
- Workflow ID (e.g., the workflow id assigned by the platform)
- **Endpoint token** (the string of numbers used for frontend `/api/page/w/{token}` calls)
- The SQL text of the corresponding SQL node (using `{{var}}` / `${{var}}` placeholders)
- Input parameter list (parameter name, required or not, purpose; for fuzzy queries use the `{{var}}` normal parameter placeholder, write SQL as `LIKE {{keyword}}`, and the frontend sends `%keyword%` with `%` manually prepended/appended; do not use `${{var}}`)
- Output description (response data is in `data.outputList`; write operations usually only return success without filling in the id)
- Bound physical table
4. **Frontend Calling Conventions**: lib layer file location, field mapping rules (backend column names ↔ frontend model names), post-write refresh strategy, cross-table cascade order, etc.
5. **Change Log**: Date + summary of changes (for traceability).
## Operational Discipline
- After creating tables / creating (or updating) SQL operation APIs, immediately write the table ID, physical table name, token, SQL, input/output parameters into `.project.md` before starting to write frontend code.
- During frontend development, endpoint tokens and field names must **always be taken from `.project.md`**; do not rely on memory or guessing.
- `.project.md` only stores "project-level, cross-session information that needs to be retained"; do not put temporary debugging information in it. Keep the file concise and readable.
</PROJECT_MEMORY>
<THINKING_REQUIREMENTS>
Before responding, you must follow this exact frontend development workflow:

View File

@@ -40,7 +40,7 @@ import org.springframework.web.util.UriComponentsBuilder;
@Slf4j
@Component
public class AiAgentClient {
public class PageAppAIClient {
@Value("${custom-page.ai-agent.base-url:}")
private String configuredBaseUrl;
@Resource

View File

@@ -9,7 +9,6 @@ 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;
@@ -39,7 +38,7 @@ import java.util.Map;
@Slf4j
@Component
public class PageFileBuildClient {
public class PageAppFileClient {
@Value("${custom-page.build-server.base-url:}")
private String configuredBaseUrl;
@@ -56,17 +55,17 @@ public class PageFileBuildClient {
.queryParam("projectId", projectId)
.queryParam("basePath", devProxyPath),
projectId).toUriString();
log.info("[Build-server] project Id={} call dev start API, url={}", projectId, url);
log.info("[file-client] 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);
log.info("[file-client] 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());
log.warn("[file-client] project Id={} call dev start APIfailed, status={}, response Body={}", projectId, e.getStatusCode(), e.getResponseBodyAsString());
return parseClientErr(projectId, e);
}
}
@@ -79,17 +78,17 @@ public class PageFileBuildClient {
.queryParam("pid", devPid)
.queryParam("port", devPort),
projectId).toUriString();
log.info("[Build-server] project Id={} callkeep-alive API, url={}", projectId, url);
log.info("[file-client] 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);
log.info("[file-client] 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());
log.warn("[file-client] project Id={} callkeep-alive APIfailed, status={}, response Body={}", projectId, e.getStatusCode(), e.getResponseBodyAsString());
return parseClientErr(projectId, e);
}
}
@@ -100,17 +99,17 @@ public class PageFileBuildClient {
.queryParam("projectId", projectId)
.queryParam("basePath", prodProxyPath),
projectId).toUriString();
log.info("[Build-server] project Id={} callbuild API, url={}", projectId, url);
log.info("[file-client] 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);
log.info("[file-client] 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());
log.warn("[file-client] project Id={} callbuild APIfailed, status={}, response Body={}", projectId, e.getStatusCode(), e.getResponseBodyAsString());
return parseClientErr(projectId, e);
}
}
@@ -121,17 +120,17 @@ public class PageFileBuildClient {
.queryParam("projectId", projectId)
.queryParam("pid", pid),
projectId).toUriString();
log.info("[Build-server] project Id={} callstop dev server API, url={}", projectId, url);
log.info("[file-client] 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);
log.info("[file-client] 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());
log.warn("[file-client] project Id={} callstop dev server APIfailed, status={}, response Body={}", projectId, e.getStatusCode(), e.getResponseBodyAsString());
return parseClientErr(projectId, e);
}
}
@@ -143,24 +142,24 @@ public class PageFileBuildClient {
uriComponentsBuilder.queryParam("pid", pid);
}
String url = appendRuntimeParamsToQuery(uriComponentsBuilder, projectId).toUriString();
log.info("[Build-server] project Id={} callrestart dev server API, url={}", projectId, url);
log.info("[file-client] 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);
log.info("[file-client] 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());
log.warn("[file-client] 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);
log.info("[file-client] project Id={} callcreate-project API, url={}", projectId, url);
// 创建请求体
Map<String, Object> requestBody = new HashMap<>();
@@ -178,17 +177,17 @@ public class PageFileBuildClient {
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);
log.info("[file-client] 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());
log.warn("[file-client] 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);
log.info("[file-client] project Id={} callupload project API, url={}, code Version={}", projectId, url, codeVersion);
// 创建请求体包含文件projectId和版本号
HttpHeaders headers = new HttpHeaders();
@@ -212,10 +211,10 @@ public class PageFileBuildClient {
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);
log.info("[file-client] 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());
log.warn("[file-client] project Id={} callupload project APIfailed, status={}, response Body={}", projectId, e.getStatusCode(), e.getResponseBodyAsString());
return parseClientErr(projectId, e);
}
}
@@ -227,17 +226,17 @@ public class PageFileBuildClient {
.queryParam("command", command)
.queryParam("proxyPath", proxyPath),
projectId).toUriString();
log.info("[Build-server] project Id={} callqueryprojectcontent API, url={}", projectId, url);
log.info("[file-client] 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);
log.info("[file-client] 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());
log.warn("[file-client] project Id={} callqueryprojectcontent APIfailed, status={}, response Body={}", projectId, e.getStatusCode(), e.getResponseBodyAsString());
return parseClientErr(projectId, e);
}
}
@@ -250,17 +249,17 @@ public class PageFileBuildClient {
.queryParam("command", command)
.queryParam("proxyPath", proxyPath),
projectId).toUriString();
log.info("[Build-server] project Id={} callqueryprojecthistorical version content API, url={}", projectId, url);
log.info("[file-client] 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);
log.info("[file-client] 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());
log.warn("[file-client] project Id={} callqueryprojecthistorical version content APIfailed, status={}, response Body={}", projectId, e.getStatusCode(), e.getResponseBodyAsString());
return parseClientErr(projectId, e);
}
}
@@ -270,7 +269,7 @@ public class PageFileBuildClient {
*/
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);
log.info("[file-client] project Id={} specifiedfileupdate, url={}", projectId, url);
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("projectId", String.valueOf(projectId));
@@ -289,10 +288,10 @@ public class PageFileBuildClient {
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);
log.info("[file-client] 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());
log.warn("[file-client] project Id={} callspecifiedfileupdate APIfailed, status={}, response Body={}", projectId, e.getStatusCode(), e.getResponseBodyAsString());
return parseClientErr(projectId, e);
}
}
@@ -302,7 +301,7 @@ public class PageFileBuildClient {
*/
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);
log.info("[file-client] project Id={} fullfileupdate, url={}", projectId, url);
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("projectId", String.valueOf(projectId));
@@ -321,10 +320,47 @@ public class PageFileBuildClient {
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);
log.info("[file-client] 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());
log.warn("[file-client] project Id={} callfullfileupdate APIfailed, status={}, response Body={}", projectId, e.getStatusCode(), e.getResponseBodyAsString());
return parseClientErr(projectId, e);
}
}
/**
* 批量上传文件
* codeVersion 是当前版本上传后版本会+1
*/
public Map<String, Object> uploadBatchFiles(Long projectId, List<MultipartFile> files, List<String> filePaths, Integer codeVersion) {
String url = buildBaseUrl(projectId) + "/project/upload-batch-files";
log.info("[file-client] project Id={} upload batch files, url={}, fileCount={}, code Version={}", projectId, url, files.size(), codeVersion);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
LinkedMultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
for (MultipartFile file : files) {
body.add("files", file.getResource());
}
body.add("projectId", String.valueOf(projectId));
body.add("codeVersion", String.valueOf(codeVersion));
for (String fp : filePaths) {
body.add("filePaths", fp);
}
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("[file-client] project Id={} upload batch files, response={}", projectId, responseBody);
return responseBody;
} catch (HttpClientErrorException e) {
log.warn("[file-client] project Id={} call upload batch files API failed, status={}, response Body={}", projectId, e.getStatusCode(), e.getResponseBodyAsString());
return parseClientErr(projectId, e);
}
}
@@ -334,7 +370,7 @@ public class PageFileBuildClient {
*/
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);
log.info("[file-client] project Id={} upload single file, url={}, file Path={}, code Version={}", projectId, url, filePath, codeVersion);
// 创建请求体包含文件projectIdfilePath和codeVersion
HttpHeaders headers = new HttpHeaders();
@@ -355,17 +391,17 @@ public class PageFileBuildClient {
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);
log.info("[file-client] 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());
log.warn("[file-client] 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);
log.info("[file-client] project Id={} upload attachment, url={}, upload File Name={}", projectId, url, uploadFileName);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
@@ -383,17 +419,17 @@ public class PageFileBuildClient {
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);
log.info("[file-client] 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());
log.warn("[file-client] 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);
log.info("[file-client] project Id={} push skills to workspace, url={}", projectId, url);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
@@ -415,17 +451,17 @@ public class PageFileBuildClient {
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);
log.info("[file-client] 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());
log.warn("[file-client] 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);
log.info("[file-client] project Id={} backup current version, url={}, code Version={}", projectId, url, codeVersion);
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("projectId", String.valueOf(projectId));
@@ -441,10 +477,10 @@ public class PageFileBuildClient {
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);
log.info("[file-client] 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());
log.warn("[file-client] project Id={} callbackup current version APIfailed, status={}, response Body={}", projectId, e.getStatusCode(), e.getResponseBodyAsString());
return parseClientErr(projectId, e);
}
}
@@ -454,7 +490,7 @@ public class PageFileBuildClient {
*/
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);
log.info("[file-client] 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));
@@ -473,17 +509,17 @@ public class PageFileBuildClient {
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);
log.info("[file-client] 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());
log.warn("[file-client] 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);
log.info("[file-client] project Id={} exportproject, url={}, code Version={}, export Type={}", projectId, url, codeVersion, exportType);
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("projectId", String.valueOf(projectId));
@@ -501,7 +537,7 @@ public class PageFileBuildClient {
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);
log.info("[file-client] project Id={} exportproject, response size={} bytes", projectId, body != null ? body.length : 0);
return body != null ? new ByteArrayInputStream(body) : null;
}
@@ -509,17 +545,17 @@ public class PageFileBuildClient {
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);
log.info("[file-client] 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);
log.info("[file-client] 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());
log.warn("[file-client] project Id={} calldelete project APIfailed, status={}, response Body={}", projectId, e.getStatusCode(), e.getResponseBodyAsString());
return parseClientErr(projectId, e);
}
}
@@ -531,17 +567,17 @@ public class PageFileBuildClient {
.queryParam("startIndex", startIndex)
.queryParam("logType", logType),
projectId).toUriString();
log.debug("[Build-server] project Id={} callquery logs API, url={}", projectId, url);
log.debug("[file-client] 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);
log.debug("[file-client] 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());
log.warn("[file-client] project Id={} query logs API failed, status={}, response Body={}", projectId, e.getStatusCode(), e.getResponseBodyAsString());
return parseClientErr(projectId, e);
}
}
@@ -551,7 +587,7 @@ public class PageFileBuildClient {
//.queryParam("sourceProjectId", sourceProjectId)
//.queryParam("targetProjectId", targetProjectId)
.toUriString();
log.info("[Build-server] source Project Id={},target Project Id={}, callcopyproject API, url={}", sourceProjectId, targetProjectId, url);
log.info("[file-client] source Project Id={},target Project Id={}, callcopyproject API, url={}", sourceProjectId, targetProjectId, url);
RuntimeContext sourceRuntimeContext = getRuntimeContext(sourceProjectId);
RuntimeContext targetRuntimeContext = getRuntimeContext(targetProjectId);
@@ -577,10 +613,10 @@ public class PageFileBuildClient {
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);
log.info("[file-client] 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());
log.warn("[file-client] project Id={} query logs API failed, status={}, response Body={}", sourceProjectId, e.getStatusCode(), e.getResponseBodyAsString());
return parseClientErr(sourceProjectId, e);
}
}
@@ -589,17 +625,17 @@ public class PageFileBuildClient {
String url = appendRuntimeParamsToQuery(
UriComponentsBuilder.fromHttpUrl(buildBaseUrl(null) + "/build/get-log-cache-stats"), null)
.toUriString();
log.info("[Build-server] callgetlogcachestats API, url={}", url);
log.info("[file-client] 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);
log.info("[file-client] callgetlogcachestats API, response={}", body);
return body;
} catch (HttpClientErrorException e) {
log.warn("[Build-server] callgetlogcachestats APIfailed, status={}, response Body={}", e.getStatusCode(), e.getResponseBodyAsString());
log.warn("[file-client] callgetlogcachestats APIfailed, status={}, response Body={}", e.getStatusCode(), e.getResponseBodyAsString());
return parseClientErr(null, e);
}
}
@@ -615,7 +651,7 @@ public class PageFileBuildClient {
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);
log.info("[file-client] log Id={} callgetstaticfile API, url={}, target Prefix={}, relative Path={}", logId, url, targetPrefix, relativePath);
return webClient.get()
.uri(url)
@@ -623,11 +659,11 @@ public class PageFileBuildClient {
.retrieve()
.bodyToFlux(DataBuffer.class)
.doOnError(WebClientResponseException.class, e -> {
log.warn("[Build-server] log Id={} callgetstaticfile APIfailed, status={}, response Body={}", logId, e.getStatusCode(), e.getResponseBodyAsString());
log.warn("[file-client] 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);
log.error("[file-client] log Id={} callgetstaticfile APIexception", logId, e);
}).doOnDiscard(DataBuffer.class, DataBufferUtils::release).doOnComplete(() -> {
log.info("[Build-server] log Id={} callgetstaticfile API, streaming completed", logId);
log.info("[file-client] log Id={} callgetstaticfile API, streaming completed", logId);
});
}
@@ -635,21 +671,62 @@ public class PageFileBuildClient {
String url = appendRuntimeParamsToQuery(
UriComponentsBuilder.fromHttpUrl(buildBaseUrl(null) + "/build/clear-all-log-cache"), null)
.toUriString();
log.info("[Build-server] callclearlogcache API, url={}", url);
log.info("[file-client] 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);
log.info("[file-client] callclearlogcache API, response={}", body);
return body;
} catch (HttpClientErrorException e) {
log.warn("[Build-server] callclearlogcache APIfailed, status={}, response Body={}", e.getStatusCode(), e.getResponseBodyAsString());
log.warn("[file-client] callclearlogcache APIfailed, status={}, response Body={}", e.getStatusCode(), e.getResponseBodyAsString());
return parseClientErr(null, e);
}
}
/**
* 自动 git commit
*/
public void gitAutoCommit(Long projectId, String message, String authorName, String authorEmail) {
Map<String, Object> body = new HashMap<>();
body.put("workspaceType", "pageApp");
body.put("projectId", projectId);
RuntimeContext ctx = getRuntimeContext(projectId);
if (ctx != null) {
body.put("tenantId", ctx.getTenantId());
body.put("spaceId", ctx.getSpaceId());
if (ctx.getIsolationType() != null && !ctx.getIsolationType().isBlank()) {
body.put("isolationType", ctx.getIsolationType());
}
}
body.put("message", message);
if (authorName != null) body.put("authorName", authorName);
if (authorEmail != null) body.put("authorEmail", authorEmail);
String baseUrl = buildBaseUrl(projectId);
String url = baseUrl + "/git/commit";
log.info("[Git] auto commit, project Id={}, url={}", projectId, url);
RestTemplate restTemplate = new RestTemplate();
try {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(body, headers);
ResponseEntity<Map<String, Object>> entity = restTemplate.exchange(
url, HttpMethod.POST, requestEntity, new ParameterizedTypeReference<Map<String, Object>>() {});
Map<String, Object> result = entity.getBody();
log.info("[Git] auto commit result, project Id={}, result={}", projectId, result);
} catch (HttpClientErrorException e) {
log.warn("[Git] auto commit failed, project Id={}, status={}, body={}",
projectId, e.getStatusCode(), e.getResponseBodyAsString());
} catch (Exception e) {
log.warn("[Git] auto commit failed, project Id={}", projectId, e);
}
}
// 捕获4xx错误尝试解析响应体
private Map<String, Object> parseClientErr(Long projectId, HttpClientErrorException e) {
Map<String, Object> resultMap = new HashMap<>();
@@ -676,7 +753,7 @@ public class PageFileBuildClient {
}
}
} catch (Exception parseException) {
log.error("[Build-server] project Id={} parse error response body failed", projectId, parseException);
log.error("[file-client] project Id={} parse error response body failed", projectId, parseException);
}
return resultMap;
}
@@ -709,6 +786,7 @@ public class PageFileBuildClient {
spaceId = configModel == null ? null : configModel.getSpaceId();
Long sandboxId = configModel == null ? null : configModel.getSandboxId();
if (sandboxId == null) {
log.debug("[file-client] project Id={} sandboxId is null", projectId);
return null;
}
RequestContext<?> requestContext = RequestContext.get();
@@ -734,7 +812,7 @@ public class PageFileBuildClient {
}
}
} catch (Exception e) {
log.warn("[Build-server] project Id={} resolve dynamic base url failed", projectId, e);
log.warn("[file-client] project Id={} resolve dynamic base url failed", projectId, e);
}
}
if (baseUrl == null || baseUrl.isBlank()) {

View File

@@ -15,7 +15,7 @@ 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.gateway.PageAppFileClient;
import com.xspaceagi.custompage.domain.model.CustomPageBuildModel;
import com.xspaceagi.custompage.domain.repository.ICustomPageBuildRepository;
import com.xspaceagi.system.spec.common.RequestContext;
@@ -39,7 +39,7 @@ public class KeepAliveDaemonService {
@Resource
private RedissonClient redissonClient;
@Resource
private PageFileBuildClient pageFileBuildClient;
private PageAppFileClient pageAppFileClient;
@Resource
private ICustomPageBuildRepository customPageBuildRepository;
@@ -261,7 +261,7 @@ public class KeepAliveDaemonService {
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());
Map<String, Object> resp = pageAppFileClient.stopDev(projectId, model.getDevPid());
if (resp == null) {
log.error("[Keep Alive-daemon] project Id={}, stop dev failed, no response", projectId);
return;

View File

@@ -9,7 +9,7 @@ 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.gateway.PageAppFileClient;
import com.xspaceagi.custompage.domain.model.CustomPageBuildModel;
import com.xspaceagi.custompage.domain.proxypath.ICustomPageProxyPathService;
import com.xspaceagi.custompage.domain.repository.ICustomPageBuildRepository;
@@ -17,7 +17,6 @@ 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;
@@ -35,7 +34,7 @@ public class KeepAliveServiceImpl implements IKeepAliveService {
@Resource
private RedisUtil redisUtil;
@Resource
private PageFileBuildClient pageFileBuildClient;
private PageAppFileClient pageAppFileClient;
@Resource
private ICustomPageBuildRepository customPageBuildRepository;
@Resource
@@ -72,12 +71,12 @@ public class KeepAliveServiceImpl implements IKeepAliveService {
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(),
serverResp = pageAppFileClient.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);
serverResp = pageAppFileClient.startDev(projectId, devProxyPath);
}
if (serverResp == null) {

View File

@@ -13,8 +13,6 @@ public class CustomPageBuildModel {
// 项目ID
private Long id;
private Long agentId;
private Long projectId;
// 1:运行中

View File

@@ -23,7 +23,8 @@ 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.gateway.PageAppAIClient;
import com.xspaceagi.custompage.domain.gateway.PageAppFileClient;
import com.xspaceagi.custompage.domain.model.CustomPageConversationModel;
import com.xspaceagi.custompage.domain.util.AgentProgressContextUtil;
import com.xspaceagi.custompage.domain.util.AgentProgressEventUtil;
@@ -57,7 +58,9 @@ public class CustomPageAgentProgressCaptureService {
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
@Resource
private AiAgentClient aiAgentClient;
private PageAppAIClient pageAppAIClient;
@Resource
private PageAppFileClient pageAppFileClient;
@Resource
private ICustomPageConversationDomainService customPageConversationDomainService;
@Resource
@@ -244,16 +247,19 @@ public class CustomPageAgentProgressCaptureService {
private void startAgentSubscription(String sessionId, Long projectId, UserContext userContext,
String fluxRequestId) {
aiAgentClient.subscribeSessionSse(sessionId, null, projectId, userContext,
pageAppAIClient.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) {
log.info("[Agent Progress] event received, project Id={}, session Id={}, event={}, data={}",
projectId, sessionId, eventName, data == null ? null : (data.length() > 500 ? data.substring(0, 500) + "..." : data));
String eventRequestId = extractRequestId(data);
CaptureContext context = findCaptureForEvent(projectId, sessionId, eventRequestId);
if (context == null) {
log.warn("[Agent Progress] no capture context found for event, project Id={}, session Id={}, event={}", projectId, sessionId, eventName);
return;
}
if (!AgentProgressEventUtil.shouldPersist(eventName)) {
@@ -269,10 +275,15 @@ public class CustomPageAgentProgressCaptureService {
context.requestIdRef.set(eventRequestId);
}
boolean isFinalEvent = AgentProgressEventUtil.isFinalEvent(eventName, data);
if (isFinalEvent) {
autoGitCommit(context);
}
broadcastToEmitters(context, eventName, data);
maybePersistIncremental(context);
if (AgentProgressEventUtil.isFinalEvent(eventName, data)) {
if (isFinalEvent) {
log.info("[Agent Progress] terminal event detected, project Id={}, event={}", projectId, eventName);
persistAssistantConversation(context, true);
}
}
@@ -309,11 +320,33 @@ public class CustomPageAgentProgressCaptureService {
persistAssistantConversation(context, false);
if (isContextTerminal(context)) {
sessionCoordinator.markTurnDone(context.projectId, context.fluxRequestId);
// 兜底:如果 onAgentEvent 终态时未执行则在此执行
autoGitCommit(context);
}
sessionCoordinator.releaseCaptureLock(context.projectId, context.fluxRequestId);
completeEmitters(context);
}
/**
* 执行 autoGitCommit通过 AtomicBoolean 保证只执行一次。
* SSE 回调线程中 RequestContext 不可用,调用前通过 userContext 绑定。
*/
private void autoGitCommit(CaptureContext context) {
if (!context.gitCommitted.compareAndSet(false, true)) {
return;
}
AgentProgressContextUtil.runWithUserContext(context.userContext, () -> {
try {
pageAppFileClient.gitAutoCommit(
context.projectId,
"auto commit: AI development changes",
null, null);
} catch (Exception e) {
log.warn("[Agent Progress] auto git commit failed, project Id={}", context.projectId, e);
}
});
}
private void onSubscribeFailed(Long projectId, String fluxRequestId, String errorMessage) {
CaptureContext context = activeCaptures.get(buildCaptureKey(projectId, fluxRequestId));
if (context == null) {
@@ -526,6 +559,7 @@ public class CustomPageAgentProgressCaptureService {
private final AtomicReference<String> requestIdRef = new AtomicReference<>();
private final AtomicReference<Long> assistantRecordIdRef = new AtomicReference<>();
private final AtomicLong lastPersistAtMs = new AtomicLong(0);
private final AtomicBoolean gitCommitted = new AtomicBoolean(false);
private CaptureContext(Long projectId, String sessionId, UserContext userContext, String fluxRequestId) {
this.projectId = projectId;

View File

@@ -30,6 +30,12 @@ public interface ICustomPageCodingDomainService {
ReqResult<Map<String, Object>> uploadSingleFile(Long projectId, MultipartFile file, String filePath,
UserContext userContext);
/**
* 批量上传文件
*/
ReqResult<Map<String, Object>> uploadBatchFiles(Long projectId, List<MultipartFile> files,
List<String> filePaths, UserContext userContext);
/**
* 回滚版本
*/

View File

@@ -9,7 +9,7 @@ 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.gateway.PageAppFileClient;
import com.xspaceagi.custompage.domain.keepalive.IKeepAliveService;
import com.xspaceagi.custompage.domain.model.CustomPageBuildModel;
import com.xspaceagi.custompage.domain.proxypath.ICustomPageProxyPathService;
@@ -40,7 +40,7 @@ public class CustomPageBuildDomainServiceImpl implements ICustomPageBuildDomainS
@Resource
private ICustomPageBuildRepository customPageBuildRepository;
@Resource
private PageFileBuildClient pageFileBuildClient;
private PageAppFileClient pageAppFileClient;
@Resource
private IKeepAliveService keepAliveService;
@Resource
@@ -98,7 +98,7 @@ public class CustomPageBuildDomainServiceImpl implements ICustomPageBuildDomainS
// 调用node创建项目
public ReqResult<Map<String, Object>> initProject(Long projectId, TemplateTypeEnum templateType) {
Map<String, Object> resp = pageFileBuildClient.createProject(projectId, templateType);
Map<String, Object> resp = pageAppFileClient.createProject(projectId, templateType);
if (resp == null) {
return ReqResult.error("9999", "Create project failed: build server returned no response");
}
@@ -128,62 +128,62 @@ public class CustomPageBuildDomainServiceImpl implements ICustomPageBuildDomainS
try {
devProxyPath = customPageProxyPathService.getDevProxyPath(projectId);
} catch (Exception e) {
log.warn("[upload-project] project Id={},could not gettenantdefaultagent", projectId);
log.warn("[upload-project] project Id={},could not get tenant default agent", 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,
log.info("[upload-project] project Id={},start call server", projectId);
Map<String, Object> resp = pageAppFileClient.uploadProject(projectId, file, targetVersion,
buildModel.getDevPid(), devProxyPath);
if (resp == null) {
log.error("[upload-project] project Id={},upload projectfailed,server returned null", projectId);
log.error("[upload-project] project Id={},upload project failed,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);
log.error("[upload-project] project Id={},upload project failed,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);
// 不抛出异常,因为上传已经成功
}
// 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 info succeeded target Version={}", projectId, targetVersion);
// } catch (Exception e) {
// log.error("[upload-project] project Id={},update version info failed, 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"));
log.error("[upload-project] project Id={},dev service 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);
log.warn("[upload-project] project Id={},dev service not started, update keep-alive info succeeded,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={},dev service started, update keep-alive info succeeded,pid={},port={}", projectId, pid, port);
}
log.info("[upload-project] project Id={},upload projectsucceeded,result={}", projectId, resp);
log.info("[upload-project] project Id={},upload project succeeded,result={}", projectId, resp);
return ReqResult.success(resp);
}
@@ -198,7 +198,7 @@ public class CustomPageBuildDomainServiceImpl implements ICustomPageBuildDomainS
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);
log.info("[start Dev] project Id={},response,result={}", projectId, result);
return result;
}
@@ -219,28 +219,28 @@ public class CustomPageBuildDomainServiceImpl implements ICustomPageBuildDomainS
// 校验空间权限
spacePermissionService.checkSpaceUserPermission(model.getSpaceId());
log.error("[build] project Id={},startcallserver", projectId);
log.info("[build] project Id={},start call server", projectId);
String prodProxyPath = customPageProxyPathService.getProdProxyPath(projectId);
Map<String, Object> resp = pageFileBuildClient.build(projectId, prodProxyPath);
Map<String, Object> resp = pageAppFileClient.build(projectId, prodProxyPath);
if (resp == null) {
log.error("[build] project Id={},buildfailed,server returned null", projectId);
log.error("[build] project Id={},build failed,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);
log.error("[build] project Id={},build failed,server returned message={}", projectId, message);
return ReqResult.error("9999", message);
}
log.error("[build] project Id={},buildsucceeded", projectId);
log.info("[build] project Id={},build succeeded", projectId);
// 更新build表的构建状态
customPageBuildRepository.updateBuildStatus(projectId, model.getCodeVersion(), userContext);
log.error("[build] project Id={},buildtableupdatesucceeded", projectId);
log.info("[build] project Id={},build table update succeeded", projectId);
// 更新config表的构建状态
customPageConfigRepository.updateBuildStatus(projectId, publishTypeEnum, userContext);
log.error("[build] project Id={},configtableupdatesucceeded", projectId);
log.info("[build] project Id={},config table update succeeded", projectId);
return ReqResult.success(resp);
}
@@ -258,15 +258,15 @@ public class CustomPageBuildDomainServiceImpl implements ICustomPageBuildDomainS
spacePermissionService.checkSpaceUserPermission(model.getSpaceId());
if (model.getDevPid() == null) {
log.info("[stop-dev] project Id={}, table queryprojectport null, handle", projectId);
log.info("[stop-dev] project Id={}, table query project port null, handle", projectId);
return ReqResult.success(null);
}
if (model.getDevPid() <= 1) {
log.info("[stop-dev] project Id={}, table queryprojectpid {},invalid, handle", projectId, model.getDevPid());
log.info("[stop-dev] project Id={}, table query project pid {},invalid, handle", projectId, model.getDevPid());
return ReqResult.error("Invalid process ID (pid)");
}
Map<String, Object> resp = pageFileBuildClient.stopDev(projectId, model.getDevPid());
Map<String, Object> resp = pageAppFileClient.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");
@@ -279,7 +279,7 @@ public class CustomPageBuildDomainServiceImpl implements ICustomPageBuildDomainS
}
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);
log.info("[stop-dev] project Id={},keep-alive info update succeeded", projectId);
return ReqResult.success(resp);
}
@@ -296,11 +296,11 @@ public class CustomPageBuildDomainServiceImpl implements ICustomPageBuildDomainS
// 校验空间权限
spacePermissionService.checkSpaceUserPermission(model.getSpaceId());
log.info("[restart-dev] project Id={},startcallserver", projectId);
log.info("[restart-dev] project Id={},start call server", projectId);
Integer pid = model.getDevPid();
String devProxyPath = customPageProxyPathService.getDevProxyPath(projectId);
Map<String, Object> resp = pageFileBuildClient.restartDev(projectId, pid, devProxyPath);
Map<String, Object> resp = pageAppFileClient.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");
@@ -326,13 +326,13 @@ public class CustomPageBuildDomainServiceImpl implements ICustomPageBuildDomainS
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);
log.error("[restart-dev] project Id={},get service port pid exception,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);
log.info("[restart-dev] project Id={},update keep-alive info succeeded,pid={},port={}", projectId, newPid, newPort);
return ReqResult.success(resp);
}
@@ -361,22 +361,22 @@ public class CustomPageBuildDomainServiceImpl implements ICustomPageBuildDomainS
// 校验空间权限
spacePermissionService.checkSpaceUserPermission(model.getSpaceId());
log.info("[delete-project-files] project Id={},startcallserver", projectId);
log.info("[delete-project-files] project Id={},start call server", projectId);
Integer pid = model.getDevPid();
Map<String, Object> resp = pageFileBuildClient.deleteProject(projectId, pid);
Map<String, Object> resp = pageAppFileClient.deleteProject(projectId, pid);
if (resp == null) {
log.error("[delete-project-files] project Id={},deletefailed,server returned null", projectId);
log.error("[delete-project-files] project Id={},delete failed,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);
log.error("[delete-project-files] project Id={},delete failed,server returned message={}", projectId, message);
return ReqResult.error("9999", message);
}
log.info("[delete-project-files] project Id={},deletesucceeded", projectId);
log.info("[delete-project-files] project Id={},delete succeeded", projectId);
return ReqResult.success(resp);
}
@@ -392,27 +392,27 @@ public class CustomPageBuildDomainServiceImpl implements ICustomPageBuildDomainS
// 校验空间权限
spacePermissionService.checkSpaceUserPermission(model.getSpaceId());
log.debug("[get Dev Log] project Id={}, startcallserver", projectId);
log.debug("[get Dev Log] project Id={}, start call server", projectId);
Map<String, Object> resp = pageFileBuildClient.getDevLog(projectId, startIndex, logType);
Map<String, Object> resp = pageAppFileClient.getDevLog(projectId, startIndex, logType);
if (resp == null) {
log.error("[get Dev Log] project Id={}, query logsfailed, server returned null", projectId);
log.error("[get Dev Log] project Id={}, query logs failed, 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);
log.error("[get Dev Log] project Id={}, query logs failed, server returned message={}", projectId, message);
return ReqResult.error("9999", message);
}
log.debug("[get Dev Log] project Id={}, query logssucceeded", projectId);
log.debug("[get Dev Log] project Id={}, query logs succeeded", projectId);
return ReqResult.success(resp);
}
@Override
public ReqResult<Map<String, Object>> copyProject(Long sourceProjectId, Long targetProjectId) {
Map<String, Object> resp = pageFileBuildClient.copyProject(sourceProjectId, targetProjectId);
Map<String, Object> resp = pageAppFileClient.copyProject(sourceProjectId, targetProjectId);
if (resp == null) {
return ReqResult.error("9999", "Copy project failed: build server returned no response");
}
@@ -426,8 +426,8 @@ public class CustomPageBuildDomainServiceImpl implements ICustomPageBuildDomainS
@Override
public ReqResult<Map<String, Object>> getLogCacheStats() {
log.info("[Domain] getlogcachestats");
Map<String, Object> resp = pageFileBuildClient.getLogCacheStats();
log.info("[Domain] get log cache stats");
Map<String, Object> resp = pageAppFileClient.getLogCacheStats();
if (resp == null) {
return ReqResult.error("9999", "Get log cache stats failed: build server returned no response");
}
@@ -436,14 +436,14 @@ public class CustomPageBuildDomainServiceImpl implements ICustomPageBuildDomainS
if (!success) {
return ReqResult.error("9999", message);
}
log.info("[Domain] getlogcachestatssucceeded");
log.info("[Domain] get log cache stats succeeded");
return ReqResult.success(resp);
}
@Override
public ReqResult<Map<String, Object>> clearAllLogCache() {
log.info("[Domain] clear all log cache");
Map<String, Object> resp = pageFileBuildClient.clearAllLogCache();
Map<String, Object> resp = pageAppFileClient.clearAllLogCache();
if (resp == null) {
return ReqResult.error("9999", "Clear log cache failed: build server returned no response");
}
@@ -452,7 +452,7 @@ public class CustomPageBuildDomainServiceImpl implements ICustomPageBuildDomainS
if (!success) {
return ReqResult.error("9999", message);
}
log.info("[Domain] clear all log cachesucceeded");
log.info("[Domain] clear all log cache succeeded");
return ReqResult.success(resp);
}
}

View File

@@ -10,7 +10,7 @@ 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.gateway.PageAppAIClient;
import com.xspaceagi.custompage.domain.service.CustomPageAgentProgressCaptureService;
import com.xspaceagi.custompage.domain.service.ICustomPageChatDomainService;
import com.xspaceagi.system.spec.common.UserContext;
@@ -24,7 +24,7 @@ import lombok.extern.slf4j.Slf4j;
public class CustomPageChatDomainServiceImpl implements ICustomPageChatDomainService {
@Resource
private AiAgentClient aiAgentClient;
private PageAppAIClient pageAppAIClient;
@Resource
private CustomPageAgentProgressCaptureService agentProgressCaptureService;
@@ -56,7 +56,7 @@ public class CustomPageChatDomainServiceImpl implements ICustomPageChatDomainSer
} catch (Exception e) {
return ReqResult.error("0001", "Invalid projectId");
}
Map<String, Object> resp = aiAgentClient.sessionCancel(projectIdLong, sessionId, userContext);
Map<String, Object> resp = pageAppAIClient.sessionCancel(projectIdLong, sessionId, userContext);
if (resp == null) {
return ReqResult.error("9999", "Failed to cancel task: AI Agent returned no response");
}
@@ -116,7 +116,7 @@ public class CustomPageChatDomainServiceImpl implements ICustomPageChatDomainSer
} catch (Exception e) {
return ReqResult.error("0001", "Invalid projectId");
}
Map<String, Object> resp = aiAgentClient.getAgentStatus(projectIdLong, userContext);
Map<String, Object> resp = pageAppAIClient.getAgentStatus(projectIdLong, userContext);
if (resp == null) {
return ReqResult.error("9999", "Failed to query Agent status: AI Agent returned no response");
}
@@ -176,7 +176,7 @@ public class CustomPageChatDomainServiceImpl implements ICustomPageChatDomainSer
} catch (Exception e) {
return ReqResult.error("0001", "Invalid projectId");
}
Map<String, Object> resp = aiAgentClient.stopAgent(projectIdLong, userContext);
Map<String, Object> resp = pageAppAIClient.stopAgent(projectIdLong, userContext);
if (resp == null) {
return ReqResult.error("9999", "Failed to stop Agent service: AI Agent returned no response");
}

View File

@@ -26,8 +26,8 @@ import com.xspaceagi.agent.core.spec.enums.OutputTypeEnum;
import com.xspaceagi.agent.core.spec.utils.FileTypeUtils;
import com.xspaceagi.agent.core.spec.utils.UrlFile;
import com.xspaceagi.custompage.domain.constant.CustomPagePromptConstants;
import com.xspaceagi.custompage.domain.gateway.AiAgentClient;
import com.xspaceagi.custompage.domain.gateway.PageFileBuildClient;
import com.xspaceagi.custompage.domain.gateway.PageAppAIClient;
import com.xspaceagi.custompage.domain.gateway.PageAppFileClient;
import com.xspaceagi.custompage.domain.model.CustomPageBuildModel;
import com.xspaceagi.custompage.domain.model.CustomPageConfigModel;
import com.xspaceagi.custompage.domain.model.CustomPageConversationModel;
@@ -37,6 +37,7 @@ import com.xspaceagi.custompage.domain.service.AgentProgressSessionCoordinator;
import com.xspaceagi.custompage.domain.service.CustomPageChatSessionManager;
import com.xspaceagi.custompage.domain.service.ICustomPageChatFluxService;
import com.xspaceagi.custompage.domain.service.ICustomPageConversationDomainService;
import com.xspaceagi.custompage.domain.util.ClasspathSkillLoader;
import com.xspaceagi.custompage.sdk.dto.DataSourceDto;
import com.xspaceagi.custompage.sdk.dto.VersionInfoDto;
import com.xspaceagi.custompage.sdk.enums.CustomPageActionEnum;
@@ -51,6 +52,8 @@ import com.xspaceagi.system.application.dto.TenantConfigDto;
import com.xspaceagi.system.application.dto.UserDto;
import com.xspaceagi.system.sdk.common.TraceContext;
import com.xspaceagi.system.sdk.permission.SpacePermissionService;
import com.xspaceagi.system.sdk.service.UserAccessKeyApiService;
import com.xspaceagi.system.sdk.service.dto.UserAccessKeyDto;
import com.xspaceagi.system.spec.common.RequestContext;
import com.xspaceagi.system.spec.common.UserContext;
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
@@ -58,6 +61,7 @@ import com.xspaceagi.system.spec.exception.BizException;
import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
import com.xspaceagi.system.spec.file.FileSystemMultipartFile;
import com.xspaceagi.system.spec.file.InMemoryMultipartFile;
import com.xspaceagi.system.spec.tenant.thread.TenantFunctions;
import com.xspaceagi.system.spec.utils.DateUtil;
import com.xspaceagi.system.spec.utils.I18nUtil;
import com.xspaceagi.system.spec.utils.TimeWheel;
@@ -92,11 +96,11 @@ public class CustomPageChatFluxServiceImpl implements ICustomPageChatFluxService
@Resource
private IFileAccessService iFileAccessService;
@Resource
private AiAgentClient aiAgentClient;
private PageAppAIClient pageAppAIClient;
@Resource
private IAgentRpcService agentRpcService;
@Resource
private PageFileBuildClient pageFileBuildClient;
private PageAppFileClient pageAppFileClient;
@Resource
private SpacePermissionService spacePermissionService;
@Resource
@@ -127,40 +131,40 @@ public class CustomPageChatFluxServiceImpl implements ICustomPageChatFluxService
@Resource
private TimeWheel timeWheel;
@Resource
private UserAccessKeyApiService userAccessKeyApiService;
private final static ObjectMapper objectMapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
@Resource
private IPricingRpcService iPricingRpcService;
@Override
public Flux<Map<String, Object>> sendAgentChatFlux(Map<String, Object> chatBody, UserContext userContext) {
// 验证参数
if (chatBody == null) {
return Flux.error(new IllegalArgumentException("Request body cannot be empty"));
}
Long projectId;
Object projectIdObj = chatBody.get("project_id");
Object promptObj = chatBody.get("prompt");
if (projectIdObj == null) {
if (!chatBody.containsKey("project_id")) {
return Flux.error(new IllegalArgumentException("project_id is required"));
}
if (promptObj == null || StringUtils.isBlank(String.valueOf(promptObj))) {
if (!chatBody.containsKey("prompt") || StringUtils.isBlank(String.valueOf(chatBody.get("prompt")))) {
return Flux.error(new IllegalArgumentException("prompt is required"));
}
Long projectId;
try {
projectId = Long.valueOf(String.valueOf(projectIdObj));
projectId = Long.valueOf(String.valueOf(chatBody.get("project_id")));
} catch (Exception e) {
return Flux.error(new IllegalArgumentException("Invalid project_id"));
}
CustomPageBuildModel buildModel = customPageBuildRepository.getByProjectId(projectId);
if (buildModel == null) {
return Flux.error(new IllegalArgumentException("Project does not exist"));
CustomPageConfigModel configModel = customPageConfigRepository.getById(projectId);
if (configModel == null) {
throw new IllegalArgumentException("Project configuration does not exist: " + projectId);
}
spacePermissionService.checkSpaceUserPermission(buildModel.getSpaceId());
spacePermissionService.checkSpaceUserPermission(configModel.getSpaceId());
// 平台临时会话 ID仅用于本 Flux 流终止、USER 落库占位,不可传给 Agent不可用于 ai-session-sse
String platformFluxSessionId = UUID.randomUUID().toString().replace("-", "");
@@ -176,8 +180,7 @@ public class CustomPageChatFluxServiceImpl implements ICustomPageChatFluxService
sendSessionIdFlux(sink, platformFluxSessionId);
executeChatFlux(chatBody, userContext, sink, buildModel, promptObj, platformFluxSessionId, requestId,
stopRequested);
executeChatFlux(chatBody, userContext, sink, configModel, platformFluxSessionId, requestId, stopRequested);
} catch (Exception e) {
log.error("[Flux Service] chat exception", e);
sink.error(e);
@@ -201,92 +204,50 @@ public class CustomPageChatFluxServiceImpl implements ICustomPageChatFluxService
}
private void executeChatFlux(Map<String, Object> chatBody, UserContext userContext,
FluxSink<Map<String, Object>> sink, CustomPageBuildModel buildModel,
Object promptObj, String platformFluxSessionId, String requestId,
FluxSink<Map<String, Object>> sink, CustomPageConfigModel configModel,
String platformFluxSessionId, String requestId,
AtomicBoolean stopRequested) {
try {
Long projectId = buildModel.getProjectId();
saveConversationSafely(projectId, buildTopic(String.valueOf(promptObj)), "USER", platformFluxSessionId,
requestId,
buildUserContent(chatBody, promptObj), userContext);
Long projectId = configModel.getId();
// 1 保存会话记录
saveConversationSafely(chatBody, projectId, platformFluxSessionId, requestId, userContext);
throwIfStopRequested(stopRequested);
// 1: 处理原型图片
Long multiModelId = processPrototypeImagesFlux(userContext, chatBody, projectId, sink);
// 2 处理原型图片
Long multiModelId = processPrototypeImagesFlux(chatBody, projectId, userContext, sink);
throwIfStopRequested(stopRequested);
// 2: 处理附件文件
processAttachmentFilesFlux(chatBody, projectId, promptObj, sink);
// 3 处理附件文件
processAttachmentFilesFlux(chatBody, projectId, sink);
throwIfStopRequested(stopRequested);
// 3: 处理模型配置
Long chatModelId = processModelConfigFlux(chatBody, userContext, sink);
// 4 处理模型配置
Long chatModelId = processModelConfigFlux(chatBody, userContext, configModel, sink);
throwIfStopRequested(stopRequested);
// 4: 处理数据源
processDataSourcesFlux(chatBody, projectId, sink);
// 5 处理数据源
processDataSourcesFlux(chatBody, configModel, sink);
throwIfStopRequested(stopRequested);
// 5: 处理技能列表并推送到网页应用开发工作空间
// 6 处理技能列表并推送到网页应用开发工作空间
processSkillsFlux(chatBody, projectId, userContext, sink);
throwIfStopRequested(stopRequested);
// 6: 备份当前版本
// sendProgressFlux(sink, "正在备份当前版本...", 60);
// 7 备份并更新版本
// backAndUpdateVersion(userContext, chatBody, projectId, buildModel, chatModelId, multiModelId, sink, stopRequested);
// 8 调用 AI Agent
//sendProgressFlux(sink, "Calling AI agent...80%");
sendHeartbeatFlux(sink);
Integer currentVersion = buildModel.getCodeVersion() == null ? 0 : buildModel.getCodeVersion();
Map<String, Object> backupResp = pageFileBuildClient.backupCurrentVersion(projectId, currentVersion);
if (backupResp == null || !Boolean.parseBoolean(String.valueOf(backupResp.get("success")))) {
String msg = backupResp != null && backupResp.get("message") != null
? String.valueOf(backupResp.get("message"))
: "备份失败";
sendErrorFlux(sink, "9999", msg);
return;
}
throwIfStopRequested(stopRequested);
// 7: 更新版本
// sendProgressFlux(sink, "正在更新版本...", 70);
sendHeartbeatFlux(sink);
Integer nextVersion = currentVersion + 1;
List<VersionInfoDto> versionInfo = buildModel.getVersionInfo();
// 仅记录提示词前100个字符到ext
String promptStr = String.valueOf(promptObj);
String briefPrompt = promptStr.length() > 100 ? promptStr.substring(0, 100) : promptStr;
Map<String, String> ext = new HashMap<>();
ext.put("prompt", briefPrompt);
versionInfo.add(VersionInfoDto.builder()
.version(nextVersion)
.time(DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss"))
.action(CustomPageActionEnum.CHAT.getCode())
.ext(ext)
.build());
CustomPageBuildModel updateModel = new CustomPageBuildModel();
updateModel.setId(buildModel.getId());
updateModel.setCodeVersion(nextVersion);
updateModel.setVersionInfo(versionInfo);
updateModel.setLastChatModelId(chatModelId);
updateModel.setLastMultiModelId(multiModelId);
customPageBuildRepository.updateVersionInfo(updateModel, userContext);
throwIfStopRequested(stopRequested);
// 8: 调用 AI Agent
//sendProgressFlux(sink, "正在与 AI 对话...", 80);
sendHeartbeatFlux(sink, "Calling AI agent...80%");
// 异步调用 sendChat并在等待期间发送心跳
//Map<String, Object> chatResp = callSendChatWithHeartbeat(chatBody, sink);
prepareAgentChatBody(chatBody, platformFluxSessionId);
Map<String, Object> chatResp = callSendChatSync(chatBody, projectId, userContext, stopRequested,
platformFluxSessionId);
Map<String, Object> chatResp = callSendChatSync(chatBody, projectId, userContext, stopRequested, platformFluxSessionId);
if (chatResp == null) {
sendErrorFlux(sink, "9999", "AI Agent 无响应");
return;
}
throwIfStopRequested(stopRequested);
Object code = chatResp.get("code");
if (code == null || !"0000".equals(String.valueOf(code))) {
String errorCode = code != null ? String.valueOf(code) : "9999";
@@ -294,10 +255,9 @@ public class CustomPageChatFluxServiceImpl implements ICustomPageChatFluxService
return;
}
// 9: 返回结果
//sendProgressFlux(sink, "AI 处理中...", 100);
sendHeartbeatFlux(sink, "AI returned result...100%");
// 9 返回结果
//sendProgressFlux(sink, "AI returned result...100%");
sendHeartbeatFlux(sink);
Map<String, Object> dataMap = parseResponseData(chatResp);
if (dataMap != null) {
dataMap.put("request_id", requestId);
@@ -313,8 +273,55 @@ public class CustomPageChatFluxServiceImpl implements ICustomPageChatFluxService
}
}
private Long processPrototypeImagesFlux(UserContext userContext, Map<String, Object> chatBody, Long projectId,
FluxSink<Map<String, Object>> sink) {
private void backAndUpdateVersion(UserContext userContext, Map<String, Object> chatBody,
Long projectId, CustomPageBuildModel buildModel,
Long chatModelId, Long multiModelId,
FluxSink<Map<String, Object>> sink, AtomicBoolean stopRequested) {
// sendProgressFlux(sink, "正在备份当前版本...", 60);
sendHeartbeatFlux(sink);
// 备份版本
int currentVersion = buildModel.getCodeVersion() == null ? 0 : buildModel.getCodeVersion();
Map<String, Object> backupResp = pageAppFileClient.backupCurrentVersion(projectId, currentVersion);
if (backupResp == null || !Boolean.parseBoolean(String.valueOf(backupResp.get("success")))) {
String msg = backupResp != null && backupResp.get("message") != null
? String.valueOf(backupResp.get("message"))
: "备份失败";
sendErrorFlux(sink, "9999", msg);
return;
}
throwIfStopRequested(stopRequested);
// 更新版本
// sendProgressFlux(sink, "正在更新版本...", 70);
sendHeartbeatFlux(sink);
Integer nextVersion = currentVersion + 1;
List<VersionInfoDto> versionInfo = buildModel.getVersionInfo();
// 仅记录提示词前100个字符到ext
String promptStr = String.valueOf(chatBody.get("prompt"));
String briefPrompt = promptStr.length() > 100 ? promptStr.substring(0, 100) : promptStr;
Map<String, String> ext = new HashMap<>();
ext.put("prompt", briefPrompt);
versionInfo.add(VersionInfoDto.builder()
.version(nextVersion)
.time(DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss"))
.action(CustomPageActionEnum.CHAT.getCode())
.ext(ext)
.build());
CustomPageBuildModel updateModel = new CustomPageBuildModel();
updateModel.setId(buildModel.getId());
updateModel.setCodeVersion(nextVersion);
updateModel.setVersionInfo(versionInfo);
updateModel.setLastChatModelId(chatModelId);
updateModel.setLastMultiModelId(multiModelId);
customPageBuildRepository.updateVersionInfo(updateModel, userContext);
throwIfStopRequested(stopRequested);
}
private Long processPrototypeImagesFlux(Map<String, Object> chatBody, Long projectId,
UserContext userContext, FluxSink<Map<String, Object>> sink) {
Long multiModelId = null;
Object multiModelIdObj = chatBody.get("multi_model_id");
if (multiModelIdObj == null) {
@@ -367,8 +374,6 @@ public class CustomPageChatFluxServiceImpl implements ICustomPageChatFluxService
log.info("[Flux Service] send chat message,project Id={},prototype Images={}", projectId, JSON.toJSONString(prototypeImages));
Object promptObj = chatBody.get("prompt");
for (Object prototypeImage : (List<?>) prototypeImages) {
if (prototypeImage instanceof Map<?, ?> m) {
Object urlObj = m.get("url");
@@ -442,20 +447,19 @@ public class CustomPageChatFluxServiceImpl implements ICustomPageChatFluxService
sendImageAnalysisResultFlux(sink, String.valueOf(urlObj), String.valueOf(fileNameObj), markdownContent);
// 将解析结果添加到聊天体中
chatBody.put("prompt", promptObj + "\n" + markdownContent);
chatBody.compute("prompt", (k, v) -> v + "\n" + markdownContent);
}
}
return multiModelId;
}
private void processAttachmentFilesFlux(Map<String, Object> chatBody, Long projectId, Object promptObj,
FluxSink<Map<String, Object>> sink) {
private void processAttachmentFilesFlux(Map<String, Object> chatBody, Long projectId, FluxSink<Map<String, Object>> sink) {
Object attachmentFiles = chatBody.get("attachment_files");
if (attachmentFiles == null) {
return;
}
log.info("[Flux Service] send chat message,project Id={},starthandle ,files={}", projectId, JSON.toJSONString(attachmentFiles));
log.info("[Flux Service] send chat message,project Id={},start handle ,files={}", projectId, JSON.toJSONString(attachmentFiles));
for (Object attachment : (List<?>) attachmentFiles) {
if (attachment instanceof Map<?, ?> m) {
@@ -475,15 +479,15 @@ public class CustomPageChatFluxServiceImpl implements ICustomPageChatFluxService
String outputPrompt = parseFileToText(projectId, String.valueOf(urlObj), String.valueOf(fileNameObj));
if (outputPrompt != null && !outputPrompt.isEmpty()) {
chatBody.put("prompt", promptObj + "\n" + outputPrompt);
chatBody.compute("prompt", (k, v) -> v + "\n" + outputPrompt);
}
}
}
}
private Long processModelConfigFlux(Map<String, Object> chatBody, UserContext userContext,
private Long processModelConfigFlux(Map<String, Object> chatBody, UserContext userContext, CustomPageConfigModel configModel,
FluxSink<Map<String, Object>> sink) {
Long projectId = Long.valueOf(String.valueOf(chatBody.get("project_id")));
Long projectId = configModel.getId();
Object chatModelIdObj = chatBody.get("chat_model_id");
log.info("[Flux Service] send chat message,project Id={},chat Model Id={}", projectId, chatModelIdObj);
@@ -591,13 +595,23 @@ public class CustomPageChatFluxServiceImpl implements ICustomPageChatFluxService
modelProvider.put("requires_openai_auth", true);
chatBody.put("model_provider", modelProvider);
chatBody.put("agent_config", Map.of(
"agent_server", buildAgentServer(modelConfigDto)
"agent_server", buildAgentServer(modelConfigDto, configModel, userContext)
));
return chatModelId;
}
private static Map<String, Object> buildAgentServer(ModelConfigDto modelConfig) {
private Map<String, Object> buildAgentServer(ModelConfigDto modelConfig, CustomPageConfigModel configModel, UserContext userContext) {
UserAccessKeyDto userAccessKeyDto = TenantFunctions.callWithIgnoreCheck(() -> userAccessKeyApiService.queryAccessKey(userContext.getUserId(), UserAccessKeyDto.AKTargetType.Sandbox, configModel.getDevAgentId().toString()));
if (userAccessKeyDto == null) {
userAccessKeyDto = TenantFunctions.callWithIgnoreCheck(() -> userAccessKeyApiService.newAccessKey(userContext.getTenantId(), userContext.getUserId(), UserAccessKeyDto.AKTargetType.Sandbox, configModel.getDevAgentId().toString(), new UserAccessKeyDto.UserAccessKeyConfig()));
}
TenantConfigDto tenantConfigDto = (TenantConfigDto) userContext.getTenantConfig();
String baseUrl = tenantConfigDto.getSiteUrl().endsWith("/") ? tenantConfigDto.getSiteUrl().substring(0, tenantConfigDto.getSiteUrl().length() - 1) : tenantConfigDto.getSiteUrl();
Map<String, String> env = new HashMap<>();
env.put("PLATFORM_BASE_URL", baseUrl);
env.put("SANDBOX_ACCESS_KEY", userAccessKeyDto.getAccessKey());
env.put("DEV_PROJECT_ID", configModel.getId().toString());
env.put("DEV_SPACE_ID", configModel.getSpaceId().toString());
// 不支持claudecode的模型直接使用opencode
if (modelConfig.getApiProtocol() == ModelApiProtocolEnum.OpenAI) {
env.put("OPENCODE_LOG_DIR", "/app/container-logs");
@@ -667,96 +681,182 @@ public class CustomPageChatFluxServiceImpl implements ICustomPageChatFluxService
return apiInfoList.get(apiInfoList.size() - 1);
}
private void processDataSourcesFlux(Map<String, Object> chatBody, Long projectId, FluxSink<Map<String, Object>> sink) {
Object dataSources = chatBody.get("data_sources");
if (dataSources == null) {
private void processDataSourcesFlux(Map<String, Object> chatBody, CustomPageConfigModel configModel, FluxSink<Map<String, Object>> sink) {
Long projectId = configModel.getId();
List<DataSourceDto> boundDataSources = configModel.getDataSources();
// Build prompt from all bound data sources
appendBoundDataSourcesPrompt(chatBody, projectId, boundDataSources, sink);
// Build prompt from user at data sources
appendUserAtDataSourcesPrompt(chatBody, projectId, boundDataSources, sink);
}
private void appendBoundDataSourcesPrompt(Map<String, Object> chatBody, Long projectId, List<DataSourceDto> boundDataSources, FluxSink<Map<String, Object>> sink) {
if (CollectionUtils.isEmpty(boundDataSources)) {
return;
}
log.info("[Flux Service] send chat message,project Id={},data Sources={}", projectId, JSON.toJSONString(dataSources));
// Batch fetch published info for all bound data sources
Map<Long, PublishedDto> pluginMap = new HashMap<>();
Map<Long, PublishedDto> workflowMap = new HashMap<>();
List<DataSourceDto> dataSourceList = new ArrayList<>();
if (dataSources instanceof List<?>) {
// sendProgressFlux(sink, "正在处理数据源...", 50);
sendHeartbeatFlux(sink);
List<Long> pluginIds = boundDataSources.stream()
.filter(ds -> "plugin".equals(ds.getType()) && ds.getId() != null)
.map(DataSourceDto::getId)
.distinct()
.collect(Collectors.toList());
List<Long> workflowIds = boundDataSources.stream()
.filter(ds -> "workflow".equals(ds.getType()) && ds.getId() != null)
.map(DataSourceDto::getId)
.distinct()
.collect(Collectors.toList());
for (Object ds : (List<?>) dataSources) {
if (ds instanceof Map<?, ?> m) {
DataSourceDto dataSource = new DataSourceDto();
Object type = m.get("type");
Object dataSourceId = m.get("dataSourceId");
if (type != null) {
dataSource.setType(String.valueOf(type));
}
if (dataSourceId != null) {
dataSource.setId(Long.valueOf(String.valueOf(dataSourceId)));
}
dataSourceList.add(dataSource);
}
}
if (dataSourceList.size() > 0) {
CustomPageConfigModel configModel = customPageConfigRepository.getById(projectId);
if (configModel == null) {
throw new IllegalArgumentException("Project configuration does not exist: " + projectId);
}
List<DataSourceDto> existingDataSources = Optional.ofNullable(configModel.getDataSources())
.orElseThrow(() -> new IllegalArgumentException("Project has no data sources bound"));
// 判断传入的dataSourceList是否都在existingDataSources中
for (DataSourceDto incoming : dataSourceList) {
boolean found = existingDataSources.stream()
.anyMatch(existing -> existing.getId() != null
&& existing.getId().equals(incoming.getId())
&& existing.getType() != null && existing.getType().equals(incoming.getType()));
if (!found) {
throw new IllegalArgumentException(
"Data source not authorized: dataSouceId=" + incoming.getId() + ", type=" + incoming.getType());
}
}
List<String> dataSourceSchemaList = new ArrayList<>();
for (DataSourceDto incoming : dataSourceList) {
String type = incoming.getType();
Long id = incoming.getId();
TargetTypeEnum typeEnum = "plugin".equals(String.valueOf(type))
? TargetTypeEnum.Plugin
: "workflow".equals(String.valueOf(type))
? TargetTypeEnum.Workflow
: null;
if (typeEnum == null) {
throw new IllegalArgumentException("Unsupported data source type: " + type);
}
// 发送心跳
sendHeartbeatFlux(sink);
com.xspaceagi.agent.core.sdk.dto.ReqResult<String> queryApiSchemaResult = agentRpcService
.queryApiSchema(typeEnum, id, projectId);
if (!queryApiSchemaResult.isSuccess()) {
throw new IllegalArgumentException("Failed to query data source schema: " + queryApiSchemaResult.getMessage());
}
String dataSourceSchema = queryApiSchemaResult.getData();
dataSourceSchemaList.add(dataSourceSchema);
}
log.info("[Flux Service] send chat message,project Id={},data source prompt={}", projectId, dataSourceSchemaList);
chatBody.put("data_source_attachments", dataSourceSchemaList);
if (!pluginIds.isEmpty()) {
List<PublishedDto> pluginList = publishApplicationService.queryPublishedList(
Published.TargetType.Plugin, pluginIds);
if (pluginList != null) {
pluginList.forEach(dto -> pluginMap.put(dto.getTargetId(), dto));
}
}
sendHeartbeatFlux(sink);
if (!workflowIds.isEmpty()) {
List<PublishedDto> workflowList = publishApplicationService.queryPublishedList(
Published.TargetType.Workflow, workflowIds);
if (workflowList != null) {
workflowList.forEach(dto -> workflowMap.put(dto.getTargetId(), dto));
}
}
sendHeartbeatFlux(sink);
// Build XML prompt
StringBuilder promptBuilder = new StringBuilder();
promptBuilder.append("The following tools are available for the current project. ");
promptBuilder.append("You can call /api/v1/4sandbox/page/target/schema with parameters type (Plugin or Workflow), id (the dataSourceId), and projectId to retrieve the detailed API schema for each tool.\n\n");
promptBuilder.append("<availableTools>\n");
for (DataSourceDto ds : boundDataSources) {
if (ds.getId() == null || ds.getType() == null) {
continue;
}
String type = ds.getType();
String typeName = "plugin".equals(type) ? "Plugin" : "workflow".equals(type) ? "Workflow" : null;
if (typeName == null) {
continue;
}
String name = "";
String description = "";
PublishedDto publishedDto = "plugin".equals(type) ? pluginMap.get(ds.getId()) : workflowMap.get(ds.getId());
if (publishedDto != null) {
if (StringUtils.isNotBlank(publishedDto.getName())) {
name = publishedDto.getName();
}
if (StringUtils.isNotBlank(publishedDto.getDescription())) {
description = publishedDto.getDescription();
}
}
promptBuilder.append(" <tool>\n");
promptBuilder.append(" <type>").append(typeName).append("</type>\n");
promptBuilder.append(" <id>").append(ds.getId()).append("</id>\n");
promptBuilder.append(" <name>").append(name).append("</name>\n");
promptBuilder.append(" <description>").append(description).append("</description>\n");
promptBuilder.append(" </tool>\n");
}
promptBuilder.append("</availableTools>");
// chatBody.put("bound_data_sources_tool_prompt", promptBuilder.toString());
chatBody.compute("prompt", (k, v) -> v + "\n" + promptBuilder.toString());
log.debug("[Flux Service] send chat message,project Id={},bound data sources prompt={}", projectId, promptBuilder);
}
private void appendUserAtDataSourcesPrompt(Map<String, Object> chatBody, Long projectId, List<DataSourceDto> boundDataSources, FluxSink<Map<String, Object>> sink) {
Object dataSources = chatBody.get("data_sources");
log.info("[Flux Service] send chat message,project Id={},data Sources={}", projectId, JSON.toJSONString(dataSources));
if (!(dataSources instanceof List)) {
return;
}
List<DataSourceDto> atDataSourceList = new ArrayList<>();
// sendProgressFlux(sink, "正在处理数据源...", 50);
sendHeartbeatFlux(sink);
for (Object ds : (List<?>) dataSources) {
if (ds instanceof Map<?, ?> m) {
DataSourceDto dataSource = new DataSourceDto();
Object type = m.get("type");
Object dataSourceId = m.get("dataSourceId");
if (type != null) {
dataSource.setType(String.valueOf(type));
}
if (dataSourceId != null) {
dataSource.setId(Long.valueOf(String.valueOf(dataSourceId)));
}
atDataSourceList.add(dataSource);
}
}
if (atDataSourceList.isEmpty()) {
return;
}
if (CollectionUtils.isEmpty(boundDataSources)) {
throw new IllegalArgumentException("Project has no data sources bound");
}
// 判断传入的dataSourceList是否都在boundDataSources中
for (DataSourceDto incoming : atDataSourceList) {
boolean found = boundDataSources.stream()
.anyMatch(existing -> existing.getId() != null
&& existing.getId().equals(incoming.getId())
&& existing.getType() != null && existing.getType().equals(incoming.getType()));
if (!found) {
throw new IllegalArgumentException(
"Data source not authorized: dataSouceId=" + incoming.getId() + ", type=" + incoming.getType());
}
}
List<String> dataSourceSchemaList = new ArrayList<>();
for (DataSourceDto incoming : atDataSourceList) {
String type = incoming.getType();
Long id = incoming.getId();
TargetTypeEnum typeEnum = "plugin".equals(String.valueOf(type))
? TargetTypeEnum.Plugin
: "workflow".equals(String.valueOf(type))
? TargetTypeEnum.Workflow
: null;
if (typeEnum == null) {
throw new IllegalArgumentException("Unsupported data source type: " + type);
}
// 发送心跳
sendHeartbeatFlux(sink);
com.xspaceagi.agent.core.sdk.dto.ReqResult<String> queryApiSchemaResult = agentRpcService
.queryApiSchema(typeEnum, id, projectId);
if (!queryApiSchemaResult.isSuccess()) {
throw new IllegalArgumentException("Failed to query data source schema: " + queryApiSchemaResult.getMessage());
}
String dataSourceSchema = queryApiSchemaResult.getData();
dataSourceSchemaList.add(dataSourceSchema);
}
log.info("[Flux Service] send chat message,project Id={},data source prompt={}", projectId, dataSourceSchemaList);
chatBody.put("data_source_attachments", dataSourceSchemaList);
}
private void processSkillsFlux(Map<String, Object> chatBody, Long projectId, UserContext userContext, FluxSink<Map<String, Object>> sink) {
List<Long> skillIds = parseSkillIds(chatBody.get("skill_ids"));
if (skillIds.isEmpty()) {
return;
}
TenantConfigDto tenantConfig = (TenantConfigDto) userContext.getTenantConfig();
// 付费校验
if (tenantConfig != null && tenantConfig.getEnableSubscription() != null && tenantConfig.getEnableSubscription() == 1) {
// 付费校验(固定技能无需校验)
if (!skillIds.isEmpty() && tenantConfig != null && tenantConfig.getEnableSubscription() != null && tenantConfig.getEnableSubscription() == 1) {
List<PriceEstimate.EstimateTarget> estimateTargets = skillIds.stream().map(skillId -> PriceEstimate.EstimateTarget.builder()
.targetType(com.xspaceagi.pricing.spec.enums.TargetTypeEnum.SKILL)
.targetId(skillId.toString())
@@ -796,13 +896,23 @@ public class CustomPageChatFluxServiceImpl implements ICustomPageChatFluxService
skillConfigs.add(skillConfigDto);
}
// 固定推送 datatable-for-page-api 技能
SkillConfigDto datatableSkill = ClasspathSkillLoader.load("skills/datatable-for-page-api/");
collectSkillNameForPrompt(datatableSkill, skillNamesForPrompt);
skillConfigs.add(datatableSkill);
// 固定推送 nuwax-pay 技能
SkillConfigDto paymentSkill = ClasspathSkillLoader.load("skills/nuwax-pay/");
collectSkillNameForPrompt(paymentSkill, skillNamesForPrompt);
skillConfigs.add(paymentSkill);
prependSkillPrompt(chatBody, skillNamesForPrompt);
MultipartFile zipFile = buildSkillZip(skillConfigs);
if (zipFile == null && CollectionUtils.isEmpty(skillUrls)) {
throw new IllegalArgumentException("No valid skill files to push");
}
Map<String, Object> pushResp = pageFileBuildClient.pushSkillsToWorkspace(projectId, zipFile, skillUrls);
Map<String, Object> pushResp = pageAppFileClient.pushSkillsToWorkspace(projectId, zipFile, skillUrls);
if (pushResp == null || !Boolean.parseBoolean(String.valueOf(pushResp.get("success")))) {
String msg = pushResp != null && pushResp.get("message") != null
? String.valueOf(pushResp.get("message"))
@@ -1097,7 +1207,7 @@ public class CustomPageChatFluxServiceImpl implements ICustomPageChatFluxService
asyncContext.setUserId(userId);
RequestContext.set(asyncContext);
prepareAgentChatBody(chatBody, platformFluxSessionId);
return aiAgentClient.sendChat(chatBody, projectId, userContext);
return pageAppAIClient.sendChat(chatBody, projectId, userContext);
} finally {
RequestContext.remove();
}
@@ -1244,8 +1354,18 @@ public class CustomPageChatFluxServiceImpl implements ICustomPageChatFluxService
return dataMap;
}
private void saveConversationSafely(Long projectId, String topic, String role, String sessionId, String requestId,
String content, UserContext userContext) {
private void saveConversationSafely(Map<String, Object> chatBody, Long projectId, String sessionId, String requestId, UserContext userContext) {
String promptStr = String.valueOf(chatBody.get("prompt"));
String topic = buildTopic(promptStr);
Map<String, Object> contentMap = new LinkedHashMap<>();
contentMap.put("text", promptStr + "\n");
contentMap.put("attachments", normalizeToList(chatBody.get("attachment_files")));
contentMap.put("dataSources", normalizeToList(chatBody.get("data_sources")));
contentMap.put("attachmentPrototypeImages", normalizeToList(chatBody.get("attachment_prototype_images")));
String content = JSON.toJSONString(contentMap);
if (projectId == null || StringUtils.isBlank(content)) {
return;
}
@@ -1254,7 +1374,7 @@ public class CustomPageChatFluxServiceImpl implements ICustomPageChatFluxService
conversationModel.setProjectId(projectId);
conversationModel.setTopic(topic);
conversationModel.setContent(content);
conversationModel.setRole(role);
conversationModel.setRole("USER");
conversationModel.setSessionId(sessionId);
conversationModel.setRequestId(requestId);
customPageConversationDomainService.saveConversation(conversationModel, userContext);
@@ -1263,15 +1383,6 @@ public class CustomPageChatFluxServiceImpl implements ICustomPageChatFluxService
}
}
private String buildUserContent(Map<String, Object> chatBody, Object promptObj) {
Map<String, Object> content = new LinkedHashMap<>();
content.put("text", String.valueOf(promptObj) + "\n");
content.put("attachments", normalizeToList(chatBody.get("attachment_files")));
content.put("dataSources", normalizeToList(chatBody.get("data_sources")));
content.put("attachmentPrototypeImages", normalizeToList(chatBody.get("attachment_prototype_images")));
return JSON.toJSONString(content);
}
private List<?> normalizeToList(Object value) {
if (value instanceof List<?>) {
return (List<?>) value;
@@ -1365,7 +1476,7 @@ public class CustomPageChatFluxServiceImpl implements ICustomPageChatFluxService
private String parseFileToText(Long projectId, String url, String fileName) {
DataTypeEnum dataType = getDocumentTypeFromUrl(url);
if (dataType == null) {
log.warn("[Flux Service] project Id={} get file typefailed,url={}", projectId, url);
log.warn("[Flux Service] project Id={} get file type failed,url={}", projectId, url);
return null;
}
@@ -1374,69 +1485,68 @@ public class CustomPageChatFluxServiceImpl implements ICustomPageChatFluxService
switch (dataType) {
case File_Doc:
try {
log.info("[Flux Service] project Id={} startparse Word , url={}", projectId, url);
log.info("[Flux Service] project Id={} start parse Word , url={}", projectId, url);
String textContent = UrlFile.wordToMarkdown(url);
File tempFile = writeTextToTempFile(projectId, textContent, fileName);
output = uploadFileAndGeneratePrompt(projectId, tempFile, fileName,
"application/msword", "Word文档附件", true);
} catch (Exception e) {
log.warn("[Flux Service] project Id={} Word handlefailed, url={}", projectId, url, e);
log.warn("[Flux Service] project Id={} Word handle failed, url={}", projectId, url, e);
output = "";
}
break;
case File_Excel:
try {
log.info("[Flux Service] project Id={} startparse Excel , url={}", projectId, url);
log.info("[Flux Service] project Id={} start parse Excel , url={}", projectId, url);
String textContent = UrlFile.excelToJson(url);
File tempFile = writeTextToTempFile(projectId, textContent, fileName);
output = uploadFileAndGeneratePrompt(projectId, tempFile, fileName, "application/vnd.ms-excel", "Excel文档附件", true);
} catch (Exception e) {
log.warn("[Flux Service] project Id={} Excel handlefailed, url={}", projectId, url, e);
log.warn("[Flux Service] project Id={} Excel handle failed, url={}", projectId, url, e);
output = "";
}
break;
case File_Txt:
try {
log.info("[Flux Service] project Id={} startparse Txt , url={}", projectId, url);
log.info("[Flux Service] project Id={} start parse Txt , url={}", projectId, url);
String textContent = UrlFile.urlToText(url, "UTF-8");
File tempFile = writeTextToTempFile(projectId, textContent, fileName);
output = uploadFileAndGeneratePrompt(projectId, tempFile, fileName, "text/plain", "文本文件附件", true);
} catch (Exception e) {
log.warn("[Flux Service] project Id={} file processingfailed, url={}", projectId, url, e);
log.warn("[Flux Service] project Id={} file processing failed, url={}", projectId, url, e);
output = "";
}
break;
case File_Image:
try {
log.info("[Flux Service] project Id={} startupload , url={}", projectId, url);
log.info("[Flux Service] project Id={} start upload , url={}", projectId, url);
File tempFile = downloadUrlToTempFile(projectId, url, fileName);
output = uploadFileAndGeneratePrompt(projectId, tempFile, fileName, "image/*", "图片附件", false);
output += "\n" + CustomPagePromptConstants.resolvePromptText(fileImagesUserPrompt);
} catch (Exception e) {
log.warn("[Flux Service] project Id={} uploadfailed, url={}", projectId, url, e);
log.warn("[Flux Service] project Id={} upload failed, url={}", projectId, url, e);
output = "";
}
break;
case File_Svg:
try {
log.info("[Flux Service] project Id={} startupload SVG, url={}", projectId, url);
log.info("[Flux Service] project Id={} start upload SVG, url={}", projectId, url);
File tempFile = downloadUrlToTempFile(projectId, url, fileName);
output = uploadFileAndGeneratePrompt(projectId, tempFile, fileName, "image/svg+xml", "SVG附件", false);
output += "\n" + CustomPagePromptConstants.resolvePromptText(fileImagesUserPrompt);
;
} catch (Exception e) {
log.warn("[Flux Service] project Id={} SVGuploadfailed, url={}", projectId, url, e);
log.warn("[Flux Service] project Id={} SVG upload failed, url={}", projectId, url, e);
output = "";
}
break;
default:
try {
log.info("[Flux Service] project Id={} startparse file, url={}", projectId, url);
log.info("[Flux Service] project Id={} start parse file, url={}", projectId, url);
String textContent = UrlFile.parseToString(url);
File tempFile = writeTextToTempFile(projectId, textContent, fileName);
output = uploadFileAndGeneratePrompt(projectId, tempFile, fileName, "application/octet-stream", "文件附件", true);
} catch (Exception e) {
log.warn("[Flux Service] project Id={} file processingfailed, url={}", projectId, url, e);
log.warn("[Flux Service] project Id={} file processing failed, url={}", projectId, url, e);
output = "";
}
break;
@@ -1473,8 +1583,8 @@ public class CustomPageChatFluxServiceImpl implements ICustomPageChatFluxService
MultipartFile multipartFile = new FileSystemMultipartFile(file, uploadFileName, contentType);
log.info("[Flux Service] project Id={} startuploadfile, upload File Name={}", projectId, uploadFileName);
Map<String, Object> resp = pageFileBuildClient.uploadAttachmentFile(projectId, multipartFile, uploadFileName);
log.info("[Flux Service] project Id={} start upload file, upload File Name={}", projectId, uploadFileName);
Map<String, Object> resp = pageAppFileClient.uploadAttachmentFile(projectId, multipartFile, uploadFileName);
if (resp != null) {
String finalFileName = resp.get("fileName") != null ? String.valueOf(resp.get("fileName"))
@@ -1494,7 +1604,7 @@ public class CustomPageChatFluxServiceImpl implements ICustomPageChatFluxService
}
private File downloadUrlToTempFile(Long projectId, String url, String fileName) throws IOException {
log.info("[Flux Service] project Id={} startdownload URL file, url={}, file Name={}", projectId, url, fileName);
log.info("[Flux Service] project Id={} start download URL file, url={}, file Name={}", projectId, url, fileName);
File tempFile = File.createTempFile("upload_", "_" + fileName);
String fileUrlWithAk = iFileAccessService.getFileUrlWithAk(url, true);
URL fileUrl = new URL(fileUrlWithAk);
@@ -1596,7 +1706,7 @@ public class CustomPageChatFluxServiceImpl implements ICustomPageChatFluxService
@Override
public boolean terminateSession(String sessionId) {
log.info("[Flux Service] terminatesessionrequest: session Id={}", sessionId);
log.info("[Flux Service] terminate session request: session Id={}", sessionId);
return sessionManager.terminateSession(sessionId);
}
}

View File

@@ -1,7 +1,7 @@
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.gateway.PageAppFileClient;
import com.xspaceagi.custompage.domain.keepalive.IKeepAliveService;
import com.xspaceagi.custompage.domain.model.CustomPageBuildModel;
import com.xspaceagi.custompage.domain.proxypath.ICustomPageProxyPathService;
@@ -29,7 +29,7 @@ public class CustomPageCodingDomainServiceImpl implements ICustomPageCodingDomai
@Resource
private IKeepAliveService keepAliveService;
@Resource
private PageFileBuildClient pageFileBuildClient;
private PageAppFileClient pageAppFileClient;
@Resource
private SpacePermissionService spacePermissionService;
@Resource
@@ -51,7 +51,7 @@ public class CustomPageCodingDomainServiceImpl implements ICustomPageCodingDomai
String devProxyPath = customPageProxyPathService.getDevProxyPath(projectId);
Map<String, Object> resp = pageFileBuildClient.specifiedFilesUpdate(projectId, files, buildModel.getCodeVersion(), devProxyPath,
Map<String, Object> resp = pageAppFileClient.specifiedFilesUpdate(projectId, files, buildModel.getCodeVersion(), devProxyPath,
buildModel.getDevPid());
if (resp == null) {
return ReqResult.error("9999", "Update specified files failed: build server returned no response");
@@ -63,10 +63,10 @@ public class CustomPageCodingDomainServiceImpl implements ICustomPageCodingDomai
}
// 更新版本信息
VersionInfoDto newVersionInfo = VersionInfoDto.builder()
.action(CustomPageActionEnum.SUBMIT_FILES_UPDATE.getCode())
.build();
updateVersion(buildModel, newVersionInfo, userContext);
// VersionInfoDto newVersionInfo = VersionInfoDto.builder()
// .action(CustomPageActionEnum.SUBMIT_FILES_UPDATE.getCode())
// .build();
// updateVersion(buildModel, newVersionInfo, userContext);
return ReqResult.success(resp);
}
@@ -84,7 +84,7 @@ public class CustomPageCodingDomainServiceImpl implements ICustomPageCodingDomai
spacePermissionService.checkSpaceUserPermission(buildModel.getSpaceId());
String devProxyPath = customPageProxyPathService.getDevProxyPath(projectId);
Map<String, Object> resp = pageFileBuildClient.allFilesUpdate(projectId, files, buildModel.getCodeVersion(), devProxyPath,
Map<String, Object> resp = pageAppFileClient.allFilesUpdate(projectId, files, buildModel.getCodeVersion(), devProxyPath,
buildModel.getDevPid());
if (resp == null) {
return ReqResult.error("9999", "Full file update failed: build server returned no response");
@@ -96,10 +96,10 @@ public class CustomPageCodingDomainServiceImpl implements ICustomPageCodingDomai
}
// 更新版本信息
VersionInfoDto newVersionInfo = VersionInfoDto.builder()
.action(CustomPageActionEnum.SUBMIT_FILES_UPDATE.getCode())
.build();
updateVersion(buildModel, newVersionInfo, userContext);
// VersionInfoDto newVersionInfo = VersionInfoDto.builder()
// .action(CustomPageActionEnum.SUBMIT_FILES_UPDATE.getCode())
// .build();
// updateVersion(buildModel, newVersionInfo, userContext);
// 开发服务器可能重启,如果重启则更新
Object pidObj = resp.get("pid");
@@ -128,7 +128,7 @@ public class CustomPageCodingDomainServiceImpl implements ICustomPageCodingDomai
Integer currentVersion = buildModel.getCodeVersion() == null ? 0 : buildModel.getCodeVersion();
Map<String, Object> resp = pageFileBuildClient.uploadSingleFile(projectId, file, filePath, currentVersion);
Map<String, Object> resp = pageAppFileClient.uploadSingleFile(projectId, file, filePath, currentVersion);
if (resp == null) {
return ReqResult.error("9999", "Upload file failed: build server returned no response");
}
@@ -139,11 +139,55 @@ public class CustomPageCodingDomainServiceImpl implements ICustomPageCodingDomai
}
// 更新版本信息
VersionInfoDto newVersionInfo = VersionInfoDto.builder()
.action(CustomPageActionEnum.UPLOAD_SINGLE_FILE.getCode())
.ext(Map.of("filePath", filePath))
.build();
updateVersion(buildModel, newVersionInfo, userContext);
// 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>> uploadBatchFiles(Long projectId, List<MultipartFile> files,
List<String> filePaths, UserContext userContext) {
log.info("[upload Batch Files] project Id={}, start domain execution, fileCount={}", projectId, files.size());
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 = pageAppFileClient.uploadBatchFiles(projectId, files, filePaths, currentVersion);
if (resp == null) {
return ReqResult.error("9999", "Upload batch 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.UPLOAD_BATCH_FILES.getCode())
// .ext(Map.of("fileCount", String.valueOf(files.size())))
// .build();
// updateVersion(buildModel, newVersionInfo, userContext);
// 开发服务器可能重启,如果重启则更新updateKeepAlive
Object pidObj = resp.get("pid");
@@ -179,7 +223,7 @@ public class CustomPageCodingDomainServiceImpl implements ICustomPageCodingDomai
}
String devProxyPath = customPageProxyPathService.getDevProxyPath(projectId);
Map<String, Object> resp = pageFileBuildClient.rollbackVersion(projectId, rollbackTo, buildModel.getCodeVersion(), devProxyPath,
Map<String, Object> resp = pageAppFileClient.rollbackVersion(projectId, rollbackTo, buildModel.getCodeVersion(), devProxyPath,
buildModel.getDevPid());
if (resp == null) {
return ReqResult.error("9999", "Rollback version failed: build server returned no response");
@@ -191,11 +235,11 @@ public class CustomPageCodingDomainServiceImpl implements ICustomPageCodingDomai
}
// 更新版本信息
VersionInfoDto newVersionInfo = VersionInfoDto.builder()
.action(CustomPageActionEnum.ROLLBACK_VERSION.getCode())
.ext(Map.of("rollbackTo", String.valueOf(rollbackTo)))
.build();
updateVersion(buildModel, newVersionInfo, userContext);
// 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");
@@ -210,19 +254,20 @@ public class CustomPageCodingDomainServiceImpl implements ICustomPageCodingDomai
}
// 更新版本信息
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);
}
// 已改为用git 版本管理,不再记录库表
// 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

@@ -7,6 +7,10 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import com.xspaceagi.agent.core.adapter.application.ResourceGroupApplicationService;
import com.xspaceagi.agent.core.adapter.dto.ResourceGroupDto;
import com.xspaceagi.agent.core.adapter.repository.entity.ResourceGroupRelation;
import com.xspaceagi.custompage.sdk.dto.*;
import com.xspaceagi.system.spec.common.UserContext;
import com.xspaceagi.system.spec.dto.ReqResult;
@@ -20,6 +24,7 @@ import com.xspaceagi.system.spec.page.SuperPage;
import com.xspaceagi.system.spec.utils.IdGeneratorRetryUtil;
import com.xspaceagi.system.spec.utils.RedisUtil;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import com.alibaba.fastjson2.JSON;
@@ -30,7 +35,7 @@ import com.xspaceagi.agent.core.sdk.dto.TemplateEnableOrUpdateDto;
import com.xspaceagi.agent.core.sdk.dto.WorkflowInfoDto;
import com.xspaceagi.agent.core.sdk.enums.TargetTypeEnum;
import com.xspaceagi.custompage.domain.dto.PageFileInfo;
import com.xspaceagi.custompage.domain.gateway.PageFileBuildClient;
import com.xspaceagi.custompage.domain.gateway.PageAppFileClient;
import com.xspaceagi.custompage.domain.keepalive.IKeepAliveService;
import com.xspaceagi.custompage.domain.model.CustomPageBuildModel;
import com.xspaceagi.custompage.domain.model.CustomPageConfigModel;
@@ -57,7 +62,7 @@ public class CustomPageConfigDomainServiceImpl implements ICustomPageConfigDomai
@Resource
private IKeepAliveService keepAliveService;
@Resource
private PageFileBuildClient pageFileBuildClient;
private PageAppFileClient pageAppFileClient;
@Resource
private SpacePermissionService spacePermissionService;
@Resource
@@ -68,6 +73,8 @@ public class CustomPageConfigDomainServiceImpl implements ICustomPageConfigDomai
private ICustomPageProxyPathService customPageProxyPathService;
@Resource
private ICustomPageConversationDomainService customPageConversationDomainService;
@Resource
private ResourceGroupApplicationService resourceGroupApplicationService;
@Override
public ReqResult<CustomPageConfigModel> create(CustomPageConfigModel model, UserContext userContext) {
@@ -145,14 +152,14 @@ public class CustomPageConfigDomainServiceImpl implements ICustomPageConfigDomai
customPageConfigRepository.updateById(model, userContext);
log.info("[update] update projectsucceeded, project Id={}", model.getId());
log.info("[update] update project succeeded, project Id={}", model.getId());
return ReqResult.success(configModel);
}
@Override
public ReqResult<List<ProxyConfig>> addProxy(Long projectId, ProxyConfig proxyConfig,
UserContext userContext) {
log.info("[Domain] updatereverse proxy config, project Id={}, env={}, path={}", projectId, proxyConfig.getEnv(),
log.info("[Domain] update reverse proxy config, project Id={}, env={}, path={}", projectId, proxyConfig.getEnv(),
proxyConfig.getPath());
CustomPageConfigModel configModel = customPageConfigRepository.getById(projectId);
@@ -173,7 +180,7 @@ public class CustomPageConfigDomainServiceImpl implements ICustomPageConfigDomai
for (int i = 0; i < existingProxyConfigs.size(); i++) {
ProxyConfig existing = existingProxyConfigs.get(i);
if (existing.getEnv() == proxyConfig.getEnv() && existing.getPath().equals(proxyConfig.getPath())) {
log.error("[Domain] reverse proxy configalready exists, project Id={}, env={}, path={}", projectId, proxyConfig.getEnv(),
log.error("[Domain] reverse proxy config already exists, project Id={}, env={}, path={}", projectId, proxyConfig.getEnv(),
proxyConfig.getPath());
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.customPageProxyEnvPathExists);
}
@@ -192,14 +199,14 @@ public class CustomPageConfigDomainServiceImpl implements ICustomPageConfigDomai
customPageConfigRepository.updateById(updateModel, userContext);
log.info("[Domain] updatereverse proxy configsucceeded, project Id={}, env={}, path={}",
log.info("[Domain] update reverse proxy config succeeded, project Id={}, env={}, path={}",
projectId, proxyConfig.getEnv(), proxyConfig.getPath());
return ReqResult.success(existingProxyConfigs);
}
@Override
public ReqResult<Void> editProxy(Long projectId, ProxyConfig proxyConfig, UserContext userContext) {
log.info("[Domain] editreverse proxy config, project Id={}, env={}, path={}", projectId, proxyConfig.getEnv(),
log.info("[Domain] edit reverse proxy config, project Id={}, env={}, path={}", projectId, proxyConfig.getEnv(),
proxyConfig.getPath());
CustomPageConfigModel configModel = customPageConfigRepository.getById(projectId);
@@ -239,7 +246,7 @@ public class CustomPageConfigDomainServiceImpl implements ICustomPageConfigDomai
customPageConfigRepository.updateById(updateModel, userContext);
log.info("[Domain] editreverse proxy configsucceeded, project Id={}, env={}, path={}",
log.info("[Domain] edit reverse proxy config succeeded, project Id={}, env={}, path={}",
projectId, proxyConfig.getEnv(), proxyConfig.getPath());
return ReqResult.success(null);
}
@@ -334,7 +341,7 @@ public class CustomPageConfigDomainServiceImpl implements ICustomPageConfigDomai
customPageConfigRepository.updateById(updateModel, userContext);
log.info("[Domain] configure path argssucceeded, project Id={}, page Uri={}",
log.info("[Domain] configure path args succeeded, project Id={}, page Uri={}",
projectId, pageArgConfig.getPageUri());
return ReqResult.success(null);
}
@@ -361,7 +368,7 @@ public class CustomPageConfigDomainServiceImpl implements ICustomPageConfigDomai
// 查找是否已存在相同pageUri的配置
for (PageArgConfig existing : existingPageArgConfigs) {
if (existing.getPageUri().equals(pageArgConfig.getPageUri())) {
log.error("[Domain] path configalready exists, project Id={}, page Uri={}", projectId, pageArgConfig.getPageUri());
log.error("[Domain] path config already exists, project Id={}, page Uri={}", projectId, pageArgConfig.getPageUri());
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.customPagePathConfigExists);
}
}
@@ -376,7 +383,7 @@ public class CustomPageConfigDomainServiceImpl implements ICustomPageConfigDomai
customPageConfigRepository.updateById(updateModel, userContext);
log.info("[Domain] add path configsucceeded, project Id={}, page Uri={}",
log.info("[Domain] add path config succeeded, project Id={}, page Uri={}",
projectId, pageArgConfig.getPageUri());
return ReqResult.success(null);
}
@@ -425,7 +432,7 @@ public class CustomPageConfigDomainServiceImpl implements ICustomPageConfigDomai
customPageConfigRepository.updateById(updateModel, userContext);
log.info("[Domain] editpath configsucceeded, project Id={}, page Uri={}",
log.info("[Domain] edit path config succeeded, project Id={}, page Uri={}",
projectId, pageArgConfig.getPageUri());
return ReqResult.success(null);
}
@@ -470,7 +477,7 @@ public class CustomPageConfigDomainServiceImpl implements ICustomPageConfigDomai
customPageConfigRepository.updateById(updateModel, userContext);
log.info("[Domain] delete path configsucceeded, project Id={}, page Uri={}",
log.info("[Domain] delete path config succeeded, project Id={}, page Uri={}",
projectId, pageUri);
return ReqResult.success(null);
}
@@ -502,7 +509,7 @@ public class CustomPageConfigDomainServiceImpl implements ICustomPageConfigDomai
customPageConfigRepository.updateById(updateModel, userContext);
log.info("[Domain] batch configure reverse proxysucceeded, project Id={}, config Count={}",
log.info("[Domain] batch configure reverse proxy succeeded, project Id={}, config Count={}",
projectId, proxyConfigs != null ? proxyConfigs.size() : 0);
return ReqResult.success(null);
}
@@ -546,7 +553,7 @@ public class CustomPageConfigDomainServiceImpl implements ICustomPageConfigDomai
}
public ReqResult<InputStream> exportProject(Long projectId, ExportTypeEnum exportType, UserContext userContext) {
log.info("[export Project] project Id={}, export Type={},startexportproject", projectId, exportType);
log.info("[export Project] project Id={}, export Type={},start export project", projectId, exportType);
Optional.ofNullable(projectId).filter(x -> x > 0)
.orElseThrow(() -> new IllegalArgumentException("projectId is required or invalid"));
@@ -569,12 +576,12 @@ public class CustomPageConfigDomainServiceImpl implements ICustomPageConfigDomai
ProjectConfigExportDto configExportDto = exportType == ExportTypeEnum.LATEST ? buildConfigExportDto(configModel) : null;
InputStream inputStream = pageFileBuildClient.exportProject(projectId, exportVersion, exportType.name(), configExportDto);
InputStream inputStream = pageAppFileClient.exportProject(projectId, exportVersion, exportType.name(), configExportDto);
if (inputStream == null) {
return ReqResult.error("9999", "Export project failed: build server returned no response");
}
log.info("[export Project] exportprojectsucceeded, project Id={}, export Type={}, code Version={}", projectId, exportType, exportVersion);
log.info("[export Project] export project succeeded, project Id={}, export Type={}, code Version={}", projectId, exportType, exportVersion);
return ReqResult.success(inputStream);
}
@@ -596,6 +603,7 @@ public class CustomPageConfigDomainServiceImpl implements ICustomPageConfigDomai
if (configModel.getDataSources() != null && !configModel.getDataSources().isEmpty()) {
List<Map<String, Object>> dataSourcePlugins = new ArrayList<>();
List<Map<String, Object>> dataSourceWorkflows = new ArrayList<>();
Map<Long, String> workflowIdToKey = new HashMap<>();
for (DataSourceDto dataSource : configModel.getDataSources()) {
try {
@@ -614,7 +622,7 @@ public class CustomPageConfigDomainServiceImpl implements ICustomPageConfigDomai
try {
spacePermissionService.checkSpaceUserPermission(pluginSpaceId);
} catch (Exception e) {
log.info("[export Project] project Id={},exportuserno plugin space permission,ignoreplugin,id={}, error={}", configModel.getId(),
log.info("[export Project] project Id={} export, user no plugin space permission,ignore plugin id={}, error={}", configModel.getId(),
pluginSpaceId, e.getMessage());
continue;
}
@@ -634,7 +642,7 @@ public class CustomPageConfigDomainServiceImpl implements ICustomPageConfigDomai
if (pluginMap != null) {
dataSourcePlugins.add(pluginMap);
} else {
log.warn("[export Project] project Id={},queryplugindetailsfailed,id={}, error={}", configModel.getId(),
log.warn("[export Project] project Id={},query plugin details failed,id={}, error={}", configModel.getId(),
dataSource.getId(), errResult != null ? errResult.getMessage() : "无响应");
throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.customPagePluginQueryFailed,
dataSource.getId(), dataSource.getName());
@@ -655,7 +663,7 @@ public class CustomPageConfigDomainServiceImpl implements ICustomPageConfigDomai
try {
spacePermissionService.checkSpaceUserPermission(workflowSpaceId);
} catch (Exception e) {
log.info("[export Project] project Id={},exportuserno workflow space permission,ignoreworkflow,id={}, error={}", configModel.getId(),
log.info("[export Project] project Id={} export user, no workflow space permission,ignore workflow id={}, error={}", configModel.getId(),
workflowSpaceId, e.getMessage());
continue;
}
@@ -674,8 +682,9 @@ public class CustomPageConfigDomainServiceImpl implements ICustomPageConfigDomai
if (workflowMap != null) {
dataSourceWorkflows.add(workflowMap);
workflowIdToKey.put(dataSource.getId(), dataSource.getKey());
} else {
log.warn("[export Project] project Id={},queryworkflowdetailsfailed,id={}, error={}", configModel.getId(),
log.warn("[export Project] project Id={},query workflow details failed,id={}, error={}", configModel.getId(),
dataSource.getId(), errResult != null ? errResult.getMessage() : "无响应");
throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.customPageWorkflowQueryFailed,
dataSource.getId(), dataSource.getName());
@@ -683,17 +692,51 @@ public class CustomPageConfigDomainServiceImpl implements ICustomPageConfigDomai
}
} catch (Exception e) {
log.warn("[export Project] project Id={},querydata sourcedetailsfailed, type={}, id={}, error={}", configModel.getId(),
log.warn("[export Project] project Id={},query data source details failed, type={}, id={}, error={}", configModel.getId(),
dataSource.getType(), dataSource.getId(), e.getMessage());
}
}
configDto.setDataSourcePlugins(dataSourcePlugins.isEmpty() ? null : dataSourcePlugins);
configDto.setDataSourceWorkflows(dataSourceWorkflows.isEmpty() ? null : dataSourceWorkflows);
configDto.setResourceGroup(buildResourceGroupExportDto(configModel.getId(), configModel.getSpaceId(), workflowIdToKey));
}
return configDto;
}
private ResourceGroupExportDto buildResourceGroupExportDto(Long projectId, Long spaceId, Map<Long, String> workflowIdToKey) {
List<ResourceGroupDto> groups = resourceGroupApplicationService.queryList(
projectId.toString(), List.of(TargetTypeEnum.Workflow.name()), spaceId);
if (groups.isEmpty()) {
return null;
}
List<ResourceGroupRelation> relations = resourceGroupApplicationService.queryGroupRelations(groups.get(0).getId());
if (CollectionUtils.isEmpty(relations)) {
return null;
}
List<String> workflowKeys = new ArrayList<>();
for (ResourceGroupRelation relation : relations) {
if (!TargetTypeEnum.Workflow.name().equals(relation.getTargetType())) {
continue;
}
String workflowKey = workflowIdToKey.get(relation.getTargetId());
if (workflowKey != null) {
workflowKeys.add(workflowKey);
}
}
if (workflowKeys.isEmpty()) {
return null;
}
ResourceGroupDto group = groups.get(0);
return ResourceGroupExportDto.builder()
.description(group.getDescription())
.workflowKeys(workflowKeys)
.build();
}
@Override
public ReqResult<Map<String, Object>> queryProjectContent(Long projectId, String command, String proxyPath) {
log.info("[Domain] query project file content, project Id={}, command={}", projectId, command);
@@ -709,7 +752,7 @@ public class CustomPageConfigDomainServiceImpl implements ICustomPageConfigDomai
// }
// spacePermissionService.checkSpaceUserPermission(configModel.getSpaceId());
Map<String, Object> resp = pageFileBuildClient.getProjectContent(projectId, command, proxyPath);
Map<String, Object> resp = pageAppFileClient.getProjectContent(projectId, command, proxyPath);
if (resp == null) {
return ReqResult.error("9999", "Failed to query project file content: build server returned no response");
}
@@ -754,9 +797,9 @@ public class CustomPageConfigDomainServiceImpl implements ICustomPageConfigDomai
return ReqResult.error("0002", "The specified version does not exist");
}
resp = pageFileBuildClient.getProjectContentByVersion(projectId, codeVersion, null, proxyPath);
resp = pageAppFileClient.getProjectContentByVersion(projectId, codeVersion, null, proxyPath);
} else {
resp = pageFileBuildClient.getProjectContent(projectId, null, proxyPath);
resp = pageAppFileClient.getProjectContent(projectId, null, proxyPath);
}
if (resp == null) {
@@ -794,11 +837,11 @@ public class CustomPageConfigDomainServiceImpl implements ICustomPageConfigDomai
@Override
public ReqResult<Void> bindDataSource(Long projectId, DataSourceDto dataSource, UserContext userContext) {
log.info("[Domain] savedata source, project Id={}, type={}, data Source Id={}",
log.info("[Domain] save data source, project Id={}, type={}, data Source Id={}",
projectId, dataSource.getType(), dataSource.getId());
if (dataSource.getType() == null || dataSource.getType().trim().isEmpty()) {
log.error("[Domain] data sourcetype cannot be empty, project Id={}", projectId);
log.error("[Domain] data source type cannot be empty, project Id={}", projectId);
return ReqResult.error("0003", "Data source type is required");
}
@@ -808,7 +851,7 @@ public class CustomPageConfigDomainServiceImpl implements ICustomPageConfigDomai
}
if (dataSource.getName() == null || dataSource.getName().trim().isEmpty()) {
log.error("[Domain] data sourcename cannot be empty, project Id={}", projectId);
log.error("[Domain] data source name cannot be empty, project Id={}", projectId);
return ReqResult.error("0005", "Data source name is required");
}
@@ -853,19 +896,19 @@ public class CustomPageConfigDomainServiceImpl implements ICustomPageConfigDomai
customPageConfigRepository.updateById(updateModel, userContext);
log.info("[Domain] savedata sourcesucceeded, project Id={}, type={}, id={}",
log.info("[Domain] save data source succeeded, project Id={}, type={}, id={}",
projectId, dataSource.getType(), dataSource.getId());
return ReqResult.success(null);
}
@Override
public ReqResult<Void> unbindDataSource(Long projectId, DataSourceDto dataSource, UserContext userContext) {
log.info("[unbind Data Source] unbinddata source, project Id={}, type={}, data Source Id={}",
log.info("[unbind Data Source] unbind data source, project Id={}, type={}, data Source Id={}",
projectId, dataSource.getType(), dataSource.getId());
CustomPageConfigModel configModel = customPageConfigRepository.getById(projectId);
if (configModel == null) {
log.error("[unbind Data Source] projectnot found, project Id={}", projectId);
log.error("[unbind Data Source] project not found, project Id={}", projectId);
return ReqResult.error("0001", "Project configuration does not exist");
}
@@ -883,7 +926,7 @@ public class CustomPageConfigDomainServiceImpl implements ICustomPageConfigDomai
// 删除现有数据源
existingDataSources.remove(existing);
found = true;
log.info("[unbind Data Source] unbinddata source, project Id={}, type={}, id={}",
log.info("[unbind Data Source] unbind data source, project Id={}, type={}, id={}",
projectId, dataSource.getType(), dataSource.getId());
break;
}
@@ -903,7 +946,7 @@ public class CustomPageConfigDomainServiceImpl implements ICustomPageConfigDomai
customPageConfigRepository.updateById(updateModel, userContext);
log.info("[Domain] unbinddata sourcesucceeded, project Id={}, type={}, id={}",
log.info("[Domain] unbind data source succeeded, project Id={}, type={}, id={}",
projectId, dataSource.getType(), dataSource.getId());
return ReqResult.success(null);
}
@@ -938,7 +981,7 @@ public class CustomPageConfigDomainServiceImpl implements ICustomPageConfigDomai
// 删除会话记录
customPageConversationDomainService.deleteByProjectId(projectId, userContext);
log.info("[Domain] delete projectsucceeded, project Id={}", projectId);
log.info("[Domain] delete project succeeded, project Id={}", projectId);
Map<String, Object> map = new HashMap<>();
map.put("config", configModel);
map.put("build", buildModel);
@@ -951,20 +994,20 @@ public class CustomPageConfigDomainServiceImpl implements ICustomPageConfigDomai
*/
public void importProjectConfig(CustomPageConfigModel model, UserContext userContext) {
Long projectId = model.getId();
log.info("[upload-project] project Id={},startimportconfig file", projectId);
log.info("[upload-project] project Id={},start import config file", projectId);
String proxyPath = "/page/static/" + projectId;
// 获取项目文件内容
ReqResult<Map<String, Object>> contentResult = this.queryProjectContent(projectId, "cpage_config", proxyPath);
if (!contentResult.isSuccess()) {
log.warn("[upload-project] project Id={},getprojectfilecontentfailed", projectId);
log.warn("[upload-project] project Id={},get project file content failed", projectId);
return;
}
Map<String, Object> data = contentResult.getData();
Object filesObj = data.get("files");
if (filesObj == null) {
log.info("[upload-project] project Id={},projectfile list is empty", projectId);
log.info("[upload-project] project Id={},project file list is empty", projectId);
return;
}
@@ -995,7 +1038,7 @@ public class CustomPageConfigDomainServiceImpl implements ICustomPageConfigDomai
log.info("[upload-project] project Id={},cpage_config.json file not found", projectId);
return;
}
log.info("[upload-project] project Id={},found cpage_config.json file,startparseconfig", projectId);
log.info("[upload-project] project Id={},found cpage_config.json file,start parse config", projectId);
// 解析配置文件
try {
@@ -1010,7 +1053,7 @@ public class CustomPageConfigDomainServiceImpl implements ICustomPageConfigDomai
// 应用配置
applyProjectConfig(model, configDto, userContext);
log.info("[upload-project] project Id={},config fileimportsucceeded", projectId);
log.info("[upload-project] project Id={},config file import succeeded", projectId);
} catch (Exception e) {
log.error("[upload-project] project Id={}, failed to parse config file", projectId, e);
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.customPageConfigFileParseFailed,
@@ -1024,7 +1067,7 @@ public class CustomPageConfigDomainServiceImpl implements ICustomPageConfigDomai
private void applyProjectConfig(CustomPageConfigModel model, ProjectConfigExportDto configDto,
UserContext userContext) {
Long projectId = model.getId();
log.info("[upload-project] project Id={},startapplyconfig", projectId);
log.info("[upload-project] project Id={},start apply config", projectId);
CustomPageConfigModel updateModel = new CustomPageConfigModel();
updateModel.setId(projectId);
@@ -1062,10 +1105,11 @@ public class CustomPageConfigDomainServiceImpl implements ICustomPageConfigDomai
// 创建插件和工作流,准备数据源数据
List<DataSourceDto> dataSources = new ArrayList<>();
Map<String, Long> workflowKeyToId = new HashMap<>();
// 创建插件
if (configDto.getDataSourcePlugins() != null && !configDto.getDataSourcePlugins().isEmpty()) {
log.info("[upload-project] project Id={},startcreateplugin,count={}", projectId,
log.info("[upload-project] project Id={},start create plugin,count={}", projectId,
configDto.getDataSourcePlugins().size());
for (Map<String, Object> pluginMap : configDto.getDataSourcePlugins()) {
try {
@@ -1087,13 +1131,13 @@ public class CustomPageConfigDomainServiceImpl implements ICustomPageConfigDomai
com.xspaceagi.agent.core.sdk.dto.ReqResult<Long> pluginResult = agentRpcService
.pluginEnableOrUpdate(pluginDto);
if (!pluginResult.isSuccess()) {
log.error("[upload-project] project Id={},createpluginfailed,message={}",
log.error("[upload-project] project Id={},create plugin failed,message={}",
projectId,
pluginResult.getMessage());
// throw new BizException("创建插件失败: " + pluginResult.getMessage());
continue;
} else {
log.info("[upload-project] project Id={},createpluginsucceeded,plugin Id={}",
log.info("[upload-project] project Id={},create plugin succeeded,plugin Id={}",
projectId, pluginResult.getData());
}
Long pluginId = pluginResult.getData();
@@ -1108,14 +1152,14 @@ public class CustomPageConfigDomainServiceImpl implements ICustomPageConfigDomai
dataSources.add(dataSource);
}
} catch (Exception e) {
log.error("[upload-project] project Id={},createpluginfailed", projectId, e);
log.error("[upload-project] project Id={},create plugin failed", projectId, e);
}
}
}
// 创建工作流
if (configDto.getDataSourceWorkflows() != null && !configDto.getDataSourceWorkflows().isEmpty()) {
log.info("[upload-project] project Id={},startcreateworkflow,count={}", projectId,
log.info("[upload-project] project Id={},start create workflow,count={}", projectId,
configDto.getDataSourceWorkflows().size());
for (Map<String, Object> workflowMap : configDto.getDataSourceWorkflows()) {
try {
@@ -1139,7 +1183,7 @@ public class CustomPageConfigDomainServiceImpl implements ICustomPageConfigDomai
// throw new BizException("创建工作流失败: " + templateResult.getMessage());
continue;
} else {
log.info("[upload-project] project Id={},createworkflowsucceeded,workflow Id={}",
log.info("[upload-project] project Id={},create workflow succeeded,workflow Id={}",
projectId, templateResult.getData());
}
Long workflowId = templateResult.getData();
@@ -1152,6 +1196,9 @@ public class CustomPageConfigDomainServiceImpl implements ICustomPageConfigDomai
.key(workflowKey)
.build();
dataSources.add(dataSource);
if (StringUtils.isNotBlank(workflowKey)) {
workflowKeyToId.put(workflowKey, workflowId);
}
}
} catch (Exception e) {
log.error("[upload-project] project Id={}, failed to create workflow", projectId, e);
@@ -1159,9 +1206,11 @@ public class CustomPageConfigDomainServiceImpl implements ICustomPageConfigDomai
}
}
bindImportedResourceGroup(projectId, model.getName(), model.getSpaceId(), configDto.getResourceGroup(), workflowKeyToId);
if (!dataSources.isEmpty()) {
updateModel.setDataSources(dataSources);
log.info("[upload-project] project Id={},binddata source,data Sources={}", projectId, dataSources);
log.info("[upload-project] project Id={},bind data source,data Sources={}", projectId, dataSources);
}
// 统一更新项目配置
@@ -1174,10 +1223,42 @@ public class CustomPageConfigDomainServiceImpl implements ICustomPageConfigDomai
customPageConfigRepository.updateById(updateModel, userContext);
log.info("[upload-project] project Id={},update project configsucceeded", projectId);
log.info("[upload-project] project Id={},update project config succeeded", projectId);
}
}
private void bindImportedResourceGroup(Long projectId, String projectName, Long spaceId,
ResourceGroupExportDto groupExport, Map<String, Long> workflowKeyToId) {
if (groupExport == null || CollectionUtils.isEmpty(groupExport.getWorkflowKeys()) || workflowKeyToId.isEmpty()) {
return;
}
Long groupId = getOrCreateProjectResourceGroup(projectId, projectName, spaceId, groupExport.getDescription());
for (String workflowKey : groupExport.getWorkflowKeys()) {
Long workflowId = workflowKeyToId.get(workflowKey);
if (workflowId != null) {
resourceGroupApplicationService.addResourceToGroup(groupId, TargetTypeEnum.Workflow.name(), workflowId);
}
}
}
private Long getOrCreateProjectResourceGroup(Long projectId, String projectName, Long spaceId, String description) {
String groupName = projectId.toString();
List<ResourceGroupDto> existingGroups = resourceGroupApplicationService.queryList(
groupName, List.of(TargetTypeEnum.Workflow.name()), spaceId);
if (!existingGroups.isEmpty()) {
return existingGroups.get(0).getId();
}
ResourceGroupDto resourceGroupDto = new ResourceGroupDto();
resourceGroupDto.setName(groupName);
resourceGroupDto.setDescription(projectName);
resourceGroupDto.setIcon(null);
resourceGroupDto.setType(TargetTypeEnum.Workflow.name());
resourceGroupDto.setSpaceId(spaceId);
return resourceGroupApplicationService.add(resourceGroupDto);
}
@Override
public List<CustomPageConfigModel> listByDevAgentIds(List<Long> devAgentIds) {
return customPageConfigRepository.listByDevAgentIds(devAgentIds);

View File

@@ -1,6 +1,6 @@
package com.xspaceagi.custompage.domain.service.impl;
import com.xspaceagi.custompage.domain.gateway.PageFileBuildClient;
import com.xspaceagi.custompage.domain.gateway.PageAppFileClient;
import com.xspaceagi.custompage.domain.service.ICustomPageFileDomainService;
import com.xspaceagi.system.spec.common.UserContext;
import jakarta.annotation.Resource;
@@ -18,12 +18,12 @@ import reactor.core.publisher.Flux;
public class CustomPageFileDomainServiceImpl implements ICustomPageFileDomainService {
@Resource
private PageFileBuildClient pageFileBuildClient;
private PageAppFileClient pageAppFileClient;
@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)
return pageAppFileClient.getStaticFile(targetPrefix, relativePath, logId)
.doOnError(WebClientResponseException.class, e -> {
log.error("[Domain] log Id={}, getstaticfilefailed, target Prefix={}, relative Path={}, status={}",
logId, targetPrefix, relativePath, e.getStatusCode());

View File

@@ -0,0 +1,106 @@
package com.xspaceagi.custompage.domain.util;
import com.xspaceagi.agent.core.adapter.dto.SkillConfigDto;
import com.xspaceagi.agent.core.adapter.dto.SkillFileDto;
import com.xspaceagi.agent.core.spec.utils.FileTypeUtils;
import com.xspaceagi.agent.core.spec.utils.MarkdownExtractUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
/**
* 从 classpath 加载固定技能模板skills/xxx/),构建 SkillConfigDto。
* name / description 自动从 SKILL.md 的 YAML front matter 中提取,无需调用方传入。
* 技能模板文件不变,首次加载后以 basePath 为 key 缓存,后续调用直接返回。
*/
@Slf4j
public final class ClasspathSkillLoader {
private ClasspathSkillLoader() {
}
private static final ConcurrentHashMap<String, SkillConfigDto> CACHE = new ConcurrentHashMap<>();
/**
* 加载(并缓存)一个 classpath 技能模板。
* name 和 description 从目录下的 SKILL.md front matter 自动提取。
*
* @param basePath classpath 根路径,以 / 结尾(如 "skills/nuwax-pay/"
* @return 构建好的 SkillConfigDto含 name/enName/description/files
*/
public static SkillConfigDto load(String basePath) {
return CACHE.computeIfAbsent(basePath, k -> doLoad(basePath));
}
private static SkillConfigDto doLoad(String basePath) {
try {
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource[] resources = resolver.getResources("classpath*:" + basePath + "**/*");
List<SkillFileDto> files = new ArrayList<>();
String skillName = null;
String description = null;
for (Resource resource : resources) {
if (resource.getFilename() == null) {
continue;
}
String url = resource.getURL().toString();
int idx = url.indexOf(basePath);
if (idx < 0) {
continue;
}
String relativePath = url.substring(idx + basePath.length());
if (relativePath.isEmpty() || relativePath.endsWith("/")) {
continue;
}
byte[] bytes;
try (InputStream is = resource.getInputStream()) {
bytes = is.readAllBytes();
}
String contents;
if (FileTypeUtils.isTextFile(relativePath)) {
contents = new String(bytes, StandardCharsets.UTF_8);
} else {
contents = Base64.getEncoder().encodeToString(bytes);
}
// 从 SKILL.md 提取 name / description
if ("SKILL.md".equals(relativePath)) {
skillName = MarkdownExtractUtil.extractFieldValue(contents, "name");
description = MarkdownExtractUtil.extractFieldValue(contents, "description");
}
SkillFileDto fileDto = new SkillFileDto();
fileDto.setName(relativePath);
fileDto.setContents(contents);
files.add(fileDto);
}
if (files.isEmpty()) {
throw new IllegalStateException("No files found under classpath:" + basePath);
}
if (skillName == null || skillName.isBlank()) {
throw new IllegalStateException("SKILL.md front matter missing 'name' under classpath:" + basePath);
}
SkillConfigDto skillConfigDto = new SkillConfigDto();
skillConfigDto.setName(skillName);
skillConfigDto.setEnName(skillName);
skillConfigDto.setDescription(description);
skillConfigDto.setFiles(files);
return skillConfigDto;
} catch (IOException e) {
log.error("[ClasspathSkillLoader] load skill template failed, basePath={}", basePath, e);
throw new IllegalArgumentException("Load skill template failed, basePath=" + basePath, e);
}
}
}

View File

@@ -0,0 +1,482 @@
---
name: datatable-for-page-api
description: "Custom Data Table management API for creating and managing user-defined data tables. Supports two groups: 1) Table Schema Management — create tables, rename tables, update table descriptions, define/modify columns and field types, delete tables, list/search tables, get table detail, copy table structure, check if table has data; 2) Table SQL API Management — create new SQL-based table operation APIs, update existing SQL-based table operation APIs. Use this skill whenever the user wants to work with custom data tables, create a new table, manage table columns or fields, or create/update SQL-based data table APIs. Keywords: data table, custom table, table creation, table schema, columns, fields, SQL API, table operation API. 支持中文自定义数据表、表结构、字段定义、数据表SQL操作API、创建API、更新API."
license: MIT
---
# Data Table for Page API Skill
## Overview
This skill provides a Python client script to access the platform data table REST APIs in sandbox.
Use this skill (`@datatable-for-page-api`) when the user wants to perform any of the following:
- **创建数据表**: create a new custom table with a name and optional description
- **查看/搜索数据表**: list existing tables, search by name, get table definition detail
- **修改表结构**: rename table, change description, update field/column definitions
- **删除数据表**: remove a table definition
- **复制表**: copy an existing table structure to a new table
- **创建数据表SQL操作API**: create a new SQL-based table operation API for page components
- **更新数据表SQL操作API**: update an existing SQL-based table operation API
## Agent workflow
### 创建表并定义SQL操作API推荐标准流程
1. 调用 **`add-table`**`POST .../compose/db/table/add`)创建表,获得表 ID。
2. **立刻调用 `get-table`** 读出建表时自动生成的 8 个系统字段(`id`/`uid`/`user_name`/`nick_name`/`agent_id`/`agent_name`/`created`/`modified`),它们必须原样回传。
3. 调用 **`update-table-definition`**`POST .../compose/db/table/updateTableDefinition`)定义字段结构——**必须传"系统字段(原样)+自定义字段"的完整 fieldList**,不能只传新字段。详见下方「⚠️ update-table-definition 关键约束」。
4. 再次调用 **`get-table`** 读取 **`dorisTable`**(物理表名,形如 `custom_table_{表ID}`SQL API 的 SQL 语句里必须用这个物理表名,**不能用建表时的中文表名(如"用户表"**。
5. 调用 **`table-sql-new`**`POST .../table/sql/new`创建数据表SQL操作API用于页面组件调用。
6. 如需修改已创建的SQL操作API调用 **`table-sql-update`**`POST .../table/sql/update`)。
7. **【最易遗漏】前端对接 + 登记进 `.project.md`**:建表/建 API 只是脚手架。建好后必须:①获取每个 SQL 操作 API 的端点 token②按下方「⚠ 前端对接规范(必读)」落地前端 lib③把表 ID、物理表名、token、SQL、入出参全部登记进项目根目录的 `.project.md`(见下方「项目记忆 `.project.md`」)。前端真正能调用才算闭环。
> ⚠️ **不要用 `copy-table` 来"绕过"建表/加字段**。`update-table-definition` 用对格式后完全能自建自定义字段(见下方约束)。复制别人的表会把无关字段带进来,造成字段语义错乱。
### 查询表信息
1. 若不知道表 ID先调用 **`list-tables`** 获取表列表。
2. 调用 **`get-table`**`GET .../compose/db/table/detailById`)获取表定义详情。
脚本封装:`add_table` / `list_tables` / `update_table_definition` / `table_sql_new` / `table_sql_update`CLI`add-table``list-tables``table-sql-new``table-sql-update` 等。
## ⚠️ update-table-definition 关键约束(必读,否则必然报错)
这是本技能最容易踩坑的接口。以下任一条件不满足,后端会返回 `4000 JSON parse error``5000 系统开小差啦`
1. **`fieldType` 必须是数字枚举,绝对不能传类型名字符串**(如 `"VARCHAR"` / `"INT"`)。合法枚举:
| fieldType | 类型 |
|-----------|------|
| `1` | VARCHAR |
| `2` | INT |
| `5` | DATETIME |
| `6` | BIGINT |
| `7` | MEDIUMTEXT / 长文本 |
2. **必须传完整的 fieldList系统字段 + 自定义字段)**,只传自定义字段会被拒。
-`get-table` 读出系统字段,原样保留其 `fieldType` / `defaultValue`(时间字段是 `CURRENT_TIMESTAMP`/ `sortIndex` / `systemFieldFlag:true`,再追加自定义字段。
- VARCHAR 字段需带 **`fieldStrLen`**(如 255数值/时间字段 `fieldStrLen``null`
3. **字段注释的 key 是 `fieldDescription` **
4. 每个字段需带完整属性:`fieldName` / `fieldDescription` / `fieldType`(数字) / `fieldStrLen` / `nullableFlag` / `defaultValue` / `uniqueFlag` / `enabledFlag` / `sortIndex` / `systemFieldFlag`
### 可直接复制的正确 fieldList 模板
下面是已验证成功的完整请求体(系统字段原样 + 自定义字段),替换 `id` 为你的表 ID、自定义字段按需修改即可
```bash
python scripts/datatable_for_page_api.py update-table-definition --id 398 --field-list '[
{"fieldName":"id","fieldDescription":"主键ID","fieldType":6,"fieldStrLen":null,"nullableFlag":false,"defaultValue":null,"uniqueFlag":false,"enabledFlag":true,"sortIndex":0,"systemFieldFlag":true},
{"fieldName":"uid","fieldDescription":"用户唯一标识","fieldType":1,"fieldStrLen":255,"nullableFlag":false,"defaultValue":null,"uniqueFlag":false,"enabledFlag":true,"sortIndex":1,"systemFieldFlag":true},
{"fieldName":"user_name","fieldDescription":"用户名","fieldType":1,"fieldStrLen":255,"nullableFlag":true,"defaultValue":null,"uniqueFlag":false,"enabledFlag":true,"sortIndex":2,"systemFieldFlag":true},
{"fieldName":"nick_name","fieldDescription":"用户昵称","fieldType":1,"fieldStrLen":255,"nullableFlag":true,"defaultValue":null,"uniqueFlag":false,"enabledFlag":true,"sortIndex":3,"systemFieldFlag":true},
{"fieldName":"agent_id","fieldDescription":"智能体唯一标识","fieldType":1,"fieldStrLen":255,"nullableFlag":true,"defaultValue":null,"uniqueFlag":false,"enabledFlag":true,"sortIndex":4,"systemFieldFlag":true},
{"fieldName":"agent_name","fieldDescription":"智能体名称","fieldType":1,"fieldStrLen":255,"nullableFlag":true,"defaultValue":null,"uniqueFlag":false,"enabledFlag":true,"sortIndex":5,"systemFieldFlag":true},
{"fieldName":"created","fieldDescription":"数据创建时间","fieldType":5,"fieldStrLen":null,"nullableFlag":false,"defaultValue":"CURRENT_TIMESTAMP","uniqueFlag":false,"enabledFlag":true,"sortIndex":6,"systemFieldFlag":true},
{"fieldName":"modified","fieldDescription":"数据修改时间","fieldType":5,"fieldStrLen":null,"nullableFlag":false,"defaultValue":"CURRENT_TIMESTAMP","uniqueFlag":false,"enabledFlag":true,"sortIndex":7,"systemFieldFlag":true},
{"fieldName":"title","fieldDescription":"标题","fieldType":1,"fieldStrLen":255,"nullableFlag":true,"defaultValue":null,"uniqueFlag":false,"enabledFlag":true,"sortIndex":8,"systemFieldFlag":false},
]'
```
> 排错提示:若仍报 `5000`,逐项自查——①是否误用了字符串 fieldType ②是否漏传系统字段 ③是否漏了 fieldStrLen。
## ⚠️ 前端对接规范(必读,建完 SQL API 后立刻照做)
`table-sql-new` / `table-sql-update` 只是把 SQL 操作 API 注册到平台;**前端页面真正调用它时走的是另一条路径**。这一步没有文档支撑是前后对接反复踩坑的根因,逐条核对:
### 0. 关键真相SQL 操作 API 本质是工作流Workflow
通过本技能创建的「数据表 SQL 操作 API」**平台会把它实例化为一个 Workflow工作流**,工作流内部含一个 SQL 节点,真正去读写底层物理表(形如 `custom_table_{表ID}`)。
- 前端页面调用时,调用的就是这个工作流。
- 每个这样的工作流都有一个唯一的**端点 token**(一串数字),前端 lib 靠「功能名 → token」映射表来调用。
- ⚠️ 该 token 通常**不直接出现在创建接口的返回体里**需要从平台「SQL 操作 API / 工作流列表」中获取。**取到后必须立即登记进 `.project.md`,下次无需再查找。**
- ⚠️ 实际上 `table-sql-new` / `table-sql-update` 的返回体(`data.apiSchema`)里**会**带调用路径 `.../api/v1/4sandbox/page/w/{token}`,末尾那串数字就是当前生效的 token —— 直接从这里抓即可,不必再去别处找。
### 0.1 ⚠️ 更新 SQL API 后,端点 token 会变(高频踩坑)
- **`table-sql-update` 成功后,平台经常会重新生成端点 token**,旧 token 立即失效。
- 现象:只改了 SQL或 args前端却开始 404 / 调用失败 / 返回非预期结果,且 `.project.md` 里登记的还是旧 token。
- **每次 new/update 后的强制闭环(少一步都会断链)**
1. 从返回体 `data.apiSchema` 里抓最新 token正则 `page/w/(\d+)`
2. 立刻用新 token 覆盖 `.project.md` 对应条目;
3. 立刻同步前端 lib 的「功能名 → token」映射
4. 用新 token 实际调一次验证通不通。
- **排错第一反应**:前端报「调用失败 / 404 / 调不通」时,**先查 token 是否在最近一次 update 后没刷新**,再去查 SQL 语法。
### 1. 调用路径与端点 token
- 页面调用路径(**唯一**正确路径,同源调用,浏览器自带登录态):
```text
POST {PLATFORM_BASE_URL}/api/v1/4sandbox/page/w/{token}
Content-Type: application/json
```
- 前端代码里直接用**相对路径** `/api/page/w/{token}` 即可(同源,登录态由浏览器自动带),**不要**在前端请求头里塞 `Authorization` / `SANDBOX_ACCESS_KEY`——那是 Python 脚本侧管理端鉴权用的,前端拿不到也不该出现。
- `/api/v1/4sandbox/...` 前缀仅用于 Python 脚本侧内部调试,**前端不要用**。
### 2. 响应外壳结构(查询类)
所有查询类 API 的返回都包在统一外壳里,业务数据在 `data.outputList`**不是 `data` 本身**
```json
{
"success": true,
"message": "success",
"data": {
"outputList": [ { "id": 1, "...": "..." } ],
"rowNum": 1
}
}
```
前端解析:`const rows = result.data?.outputList ?? []`。务必先判 `result.success`,失败时抛 `result.message`。
### 3. 写操作INSERT / UPDATE / DELETE语义
- 写操作通常只返回 `{ success: true }`**不回填新主键 id**。
- 因此前端「新增」后**不要**用假 id 渲染单条,应直接整表/按条件**重新拉取列表**刷新。
- 批量删除/更新同理,按调用次数计数成功条数即可。
### 4. LIKE 模糊查询:用普通参数 `{{var}}` + 前端自拼 `%`
- **模糊查询统一用 `{{var}}` 普通参数占位符,不要用 `${{var}}`。**
- SQL 写法:`WHERE name LIKE {{keyword}}`(和精确匹配 `=` 用同一种占位符)。
- **平台不会自动包 `%`,前端必须自己拼**:传 `%关键词%`(前一个 `%`、后一个 `%`),例如搜「小美」就传 `%小美%`。
- 普通参数走参数化通道,值一定带引号,不会把关键词当成列名。
- ⚠️ **不要用 `${{var}}`**:它在本平台展开后值常丢引号,生成 `WHERE name LIKE 小美`MySQL 把关键词当列名,报 **`Unknown column 'xxx' in 'where clause'`**。
- 精确匹配用 `={{var}}`,模糊匹配用 `LIKE {{keyword}}`(前端拼 `%`),二者不要混用。
### 5. 字段名对齐
- 建表字段名(`fieldName`= SQL 列名 = 响应 JSON 字段名,三者必须一致。
- 当后端列名与前端业务概念不同时(很常见),**在前端 `lib` 层集中写一个 `toXxx(raw)` 映射函数**做转换,组件层只消费映射后的统一类型,不要把映射逻辑散落到各个组件。
- `id` 在后端是数值bigint前端建议统一 `String(id)` 处理,避免大整数精度问题。
- 系统字段 `created`/`modified` 是平台自动维护的 `CURRENT_TIMESTAMP`,建表时保留其 `defaultValue`,前端只读不写。
### 6. referer
页面同源调用时浏览器会自动带上 `referer`,一般无需手动处理。若在非页面环境(如脚本/SSR调试页面端点需手动带 `referer: {PLATFORM_BASE_URL}/page/{PROJECT_ID}-/dev/`,否则可能被拦。
## 项目记忆 `.project.md`(强制执行)
项目根目录维护一份 `.project.md`,作为 AI 的「项目记忆中枢」集中沉淀项目设计、数据表结构、SQL 操作 API工作流及其端点 token 等关键信息。解决「端点 token 从哪来」反复踩坑的问题。
- **进入项目做任何开发前,必须先阅读 `.project.md`**(不存在则创建)。
- **建表 / 建或更新SQL 操作 API 后,立即把表 ID、物理表名、端点 token、SQL、入出参登记进 `.project.md`,再开始写前端代码**。
- 前端开发时,端点 token、字段名一律**从 `.project.md` 取**,不要凭记忆或猜测。
- 开发过程中只要产生了应长期保留的信息(改了字段、定了调用约定等),同步更新 `.project.md`。
- 它是唯一事实来源single source of truth只存「项目级、跨会话需保留」的信息临时调试信息不要塞进来。
### `.project.md` 建议登记模板
```markdown
# 项目记忆(.project.md
## 1. 项目概述
- 用途 / 技术栈 / 模版类型react-vite / vue3-vite 等)
## 2. 数据表清单
### 表:<表名>
- 表 ID<如 398>
- 物理表名dorisTable<如 custom_table_398>
- 字段:
| 字段 | 类型 | 系统/自定义 | 说明 |
|------|------|------------|------|
## 3. SQL 操作 API工作流清单
### <功能名,如 getAll / add / update / delete / search>
- 工作流 ID<如平台分配的 workflow id>
- 端点 token<前端 /api/page/w/{token} 用的那串数字>
- 绑定物理表:<如 custom_table_398>
- SQL 节点文本:
```sql
SELECT ... FROM custom_table_398 WHERE ...
```
- 入参:<参数名 / 是否必填 / 用途LIKE 用 {{var}},前端自拼 `%关键词%`>
- 出参:查询类在 data.outputList写操作仅回 success不回填 id
## 4. 前端调用约定
- lib 文件位置:<如 src/lib/xxxTableApi.ts>
- 字段映射:<后端列名 ↔ 前端模型名>
- 写后刷新 / 跨表级联顺序等约定
## 5. 变更记录
- 日期 + 改动摘要
```
## 跨表关联与级联更新(常见范式)
业务里常出现「主表某个字段引用另一张字典表」的情况(例如:业务表 A 有一个分类字段,值来自字典表 B 的名称)。改字典表名称时,必须**级联更新**业务表里所有引用它的行,否则出现脏数据。
### 范式
1. 为级联操作单独建一个 **UPDATE 类 SQL 操作 API**(不要复用普通更新 API形如
```sql
UPDATE custom_table_{A的表ID}
SET {关联字段} = {{newName}}
WHERE {关联字段} = {{oldName}}
```
2. 前端「改名」流程要保证**先级联、再改字典表**的顺序:
```
1) 调用级联 UPDATE API把业务表里 oldName 全部改成 newName
2) 调用字典表自身的 update API改字典记录
3) 刷新业务列表(级联生效)
```
3. 删除字典项同理:先级联把引用置为一个约定的默认值(如 `"default"` 或「未指定」),再删字典记录。
> 跨表没有外键约束,一致性完全靠应用层按上述顺序保证。把级联 API 和调用顺序登记进 `.project.md`,避免下次遗漏。
## 前端 lib 层最小模板TypeScript
新建业务时,前端 `src/lib/xxxTableApi.ts` 按下面骨架填写即可,已内含:端点 token 映射、统一外壳解析、`outputList` 取值、写后刷新约定、原始行→前端模型映射。**字段全部替换为你的实际表结构**。
```ts
/**
* 业务表 API 封装(基于平台数据表 SQL 操作 API本质是工作流
* 调用路径POST /api/page/w/{token}(同源,浏览器自带登录态)
*/
// 每个建好的 SQL 操作 API工作流一个 token从 .project.md 取,不要凭记忆
const API_ENDPOINTS = {
getAll: '<<token>>',
getById: '<<token>>',
add: '<<token>>',
update: '<<token>>',
remove: '<<token>>',
// 模糊查询用 LIKE 占位符 ${query} 的那个 API
search: '<<token>>',
} as const;
/** 前端统一模型(字段名按你的业务命名,与后端列名解耦) */
export interface YourItem {
id: string;
// ... 你的业务字段
createdAt: string;
updatedAt: string;
}
/** 后端原始行(字段名 = SQL 列名 = 建表 fieldName */
interface RawRow {
id: number;
// ... 与 SQL SELECT 列一一对应
created?: string;
modified?: string;
}
/** 统一外壳 */
interface ApiEnvelope<T> {
success: boolean;
message: string;
data?: { outputList: T[]; rowNum: number };
}
const API_PATH_PREFIX = '/api/page/w';
/** 统一调用:解析外壳,失败抛错,成功返回 data */
async function callAPI<T>(
token: string,
params: Record<string, unknown> = {}
): Promise<T> {
const res = await fetch(`${API_PATH_PREFIX}/${token}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const result: ApiEnvelope<T> = await res.json();
if (!result.success) throw new Error(result.message || 'API 调用失败');
return (result.data ?? ({} as T));
}
/** 后端原始行 -> 前端模型(字段映射集中在此,勿散落到组件) */
function toItem(r: RawRow): YourItem {
return {
id: String(r.id),
// ... 字段映射
createdAt: r.created ?? '',
updatedAt: r.modified ?? '',
};
}
// ===== 查询类:读 data.outputList =====
export async function getAll(): Promise<YourItem[]> {
const data = await callAPI<{ outputList: RawRow[] }>(API_ENDPOINTS.getAll);
return (data.outputList ?? []).map(toItem);
}
export async function search(query: string): Promise<YourItem[]> {
// 后端用普通参数占位符 LIKE {{query}},平台不自动包 %,前端自拼 %query%
const data = await callAPI<{ outputList: RawRow[] }>(API_ENDPOINTS.search, { query: `%${query}%` });
return (data.outputList ?? []).map(toItem);
}
// ===== 写操作类:通常只回 success新增后整表刷新 =====
export async function add(input: Partial<YourItem>): Promise<void> {
await callAPI(API_ENDPOINTS.add, {
/* 把 input 映射成后端列名参数 */
});
// 注意:写操作不回填新 id调用方 add 后应重新 getAll() 刷新
}
export async function update(id: string, input: Partial<YourItem>): Promise<void> {
await callAPI(API_ENDPOINTS.update, { id, /* ...列名参数 */ });
}
export async function remove(id: string): Promise<void> {
await callAPI(API_ENDPOINTS.remove, { id });
}
```
> 调用方约定:页面组件 `await add(...)` 之后立即 `await getAll()` 重新拉取列表,不要依赖写操作的返回值渲染单条。
## Authentication
The script reads authentication from environment variables:
| Variable | Description |
|----------|-------------|
| `PLATFORM_BASE_URL` | Platform API base URL (e.g. `https://xxx`) |
| `SANDBOX_ACCESS_KEY` | Sandbox access key for API calls |
| `SANDBOX_ID` | Sandbox identifier (optional; sent as `X-Sandbox-Id` when available) |
| `DEV_PROJECT_ID` | **当前项目 IDprojectId**`table-sql-new` / `table-sql-update` 的 `--project-id` 参数从这里取值 |
| `DEV_SPACE_ID` | **当前项目所在空间 IDspaceId**`add-table` / `list-tables` 的 `spaceId` 由脚本从此变量自动读取并注入请求体,确保表建在/查在项目空间而非个人空间 |
Requests use:
- `Authorization: Bearer $SANDBOX_ACCESS_KEY`
- `X-Sandbox-Id: $SANDBOX_ID` (when set)
## ⚠️ 获取 projectId禁止创建测试工作流
`table-sql-new` 和 `table-sql-update` 需要 `projectId` 参数。**直接从环境变量 `DEV_PROJECT_ID` 读取**,不要用其他方式获取。
> 🚫 **绝对禁止创建测试 SQL 工作流(如 `SELECT 1`)来探测 projectId 或验证 API 连通性。**
> - 每个通过 `table-sql-new` 创建的 SQL API 都会实例化为一个**永久工作流**,平台**没有删除接口**,测试工作流会永久残留在项目中。
> - 正确做法:`projectId = os.environ.get('DEV_PROJECT_ID')`,直接使用。
## ⚠️ 获取 spaceId禁止使用个人空间
`add-table` 和 `list-tables` 需要 `spaceId` 参数。**脚本已自动从环境变量 `DEV_SPACE_ID` 读取并注入请求体,无需也禁止手动指定。**
> 🚫 **绝对禁止把表建到/查到用户个人空间。**
> - 平台后端在 `spaceId` 为空时,会回退到**调用者的个人空间 ID**,而个人空间**不是**项目所在空间——建到个人空间的表不会出现在项目里,前端 SQL API 也读不到。
> - 正确做法:保证沙箱环境注入了 `DEV_SPACE_ID`(项目空间 ID脚本会自动带上**不要**通过修改脚本、手动构造请求体等方式绕过。
> - 排错第一反应:表建完后在前端/列表里看不到,先确认 `DEV_SPACE_ID` 是否正确指向项目空间,而不是个人空间。
## Python Script
The client script is located here:
```text
datatable-for-page-api/scripts/datatable_for_page_api.py
```
## Base URL
Table Definition endpoints are called under:
```text
$PLATFORM_BASE_URL/api/v1/4sandbox/compose/db/table/...
```
Table SQL API endpoints are called under:
```text
$PLATFORM_BASE_URL/api/v1/4sandbox/table/sql/...
```
For sandbox calls, `spaceId` in table-add and table-list requests is sourced automatically from the `DEV_SPACE_ID` environment variable by the script (see below). **Never omit or override it** — if `spaceId` is missing the backend falls back to the caller's personal space, which is NOT the project space and will create/query tables in the wrong place.
## Supported Endpoints
### Table Definition表结构管理
| CLI Command | Method | Endpoint | Description |
|-------------|--------|----------|-------------|
| `add-table` | POST | `/add` | Create a new table definition新增表定义 |
| `update-table-name` | POST | `/updateTableName` | Update table name / description / icon更新表名称和描述 |
| `update-table-definition` | POST | `/updateTableDefinition` | Update table field definitions更新表字段定义 |
| `delete-table` | POST | `/delete/{id}` | Delete a table definition删除表定义 |
| `list-tables` | POST | `/list` | Query table definitions paginated查询表定义列表 |
| `get-table` | GET | `/detailById?id=` | Get table definition detail查询表定义详情 |
| `exist-table-data` | GET | `/existTableData?tableId=` | Check if table has business data查询表是否有数据 |
| `copy-table` | POST | `/copyTableDefinition?tableId=` | Copy a table structure复制表结构 |
### Table SQL API数据表SQL操作API管理
| CLI Command | Method | Endpoint | Description |
|-------------|--------|----------|-------------|
| `table-sql-new` | POST | `/api/v1/4sandbox/table/sql/new` | Create a new SQL-based table operation API创建数据表SQL操作API |
| `table-sql-update` | POST | `/api/v1/4sandbox/table/sql/update` | Update an existing SQL-based table operation API更新数据表SQL操作API |
## Usage
```bash
python scripts/datatable_for_page_api.py --help
```
### Table Definition Examples
```bash
# Create a new table创建表
python scripts/datatable_for_page_api.py add-table --name "Users" --description "User information table"
# List tables查询表列表
python scripts/datatable_for_page_api.py list-tables --page-no 1 --page-size 20
# Get table definition detail查看表结构详情
python scripts/datatable_for_page_api.py get-table --table-id 123
# Update table name and description修改表名称和描述
python scripts/datatable_for_page_api.py update-table-name --id 123 --name "Users_v2" --description "Updated user table"
# Update table field definitions修改表字段定义
# ⚠️ fieldType 必须是数字枚举(1=VARCHAR,2=INT,5=DATETIME,6=BIGINT,7=TEXT),不能传 "VARCHAR"/"INT" 字符串
# ⚠️ 注释字段 key 是 fieldDescription
# ⚠️ 必须传完整 fieldList(系统字段+自定义字段),详见上文「⚠️ update-table-definition 关键约束」
python scripts/datatable_for_page_api.py update-table-definition --id 123 --field-list '[{"fieldName":"name","fieldType":1,"fieldStrLen":255,"fieldDescription":"user name","nullableFlag":true,"defaultValue":null,"uniqueFlag":false,"enabledFlag":true,"sortIndex":8,"systemFieldFlag":false},{"fieldName":"age","fieldType":2,"fieldStrLen":null,"fieldDescription":"user age","nullableFlag":true,"defaultValue":null,"uniqueFlag":false,"enabledFlag":true,"sortIndex":9,"systemFieldFlag":false}]'
# Copy a table复制表
python scripts/datatable_for_page_api.py copy-table --table-id 123
# Check if table has data检查表是否有数据
python scripts/datatable_for_page_api.py exist-table-data --table-id 123
# Delete a table删除表
python scripts/datatable_for_page_api.py delete-table --id 123
```
### Table SQL API Examples
> ⚠️ SQL 语句里的表名必须用 `get-table` 返回的 **`dorisTable`**(物理表名,形如 `custom_table_123`**不能用建表时的表名(如"用户表"**。
> 占位符:`{{var}}` 普通参数LIKE 也用它,前端自拼 `%关键词%`)。
```bash
# Create a new SQL table operation API创建数据表SQL操作API
# 注意FROM 后面是 dorisTable 物理表名 custom_table_123不是表名"用户表"
python scripts/datatable_for_page_api.py table-sql-new \
--project-id "100" \
--table-id 123 \
--api-name "queryUsers" \
--description "Query users by name" \
--sql "SELECT * FROM custom_table_123 WHERE name = {{name}}" \
--args '[{"name":"name","description":"user name","require":true}]'
# Update an existing SQL table operation API更新数据表SQL操作API
python scripts/datatable_for_page_api.py table-sql-update \
--project-id "100" \
--api-id 456 \
--api-name "queryUsersV2" \
--description "Query users by name and age" \
--sql "SELECT * FROM custom_table_123 WHERE name = {{name}} AND age > {{age}}" \
--args '[{"name":"name","description":"user name","require":true},{"name":"age","description":"min age","require":false}]'
```

View File

@@ -0,0 +1,498 @@
#!/usr/bin/env python3
"""
Data Table for Page API Client (sandbox)
This client calls platform data-table REST endpoints under:
Table Definition: $PLATFORM_BASE_URL/api/v1/4sandbox/compose/db/table/...
Table SQL API: $PLATFORM_BASE_URL/api/v1/4sandbox/table/sql/...
Two groups of operations:
1. Table Definition (表结构配置): add, updateTableName, updateTableDefinition,
delete, list, detailById, existTableData, copyTableDefinition
2. Table SQL API (数据表SQL操作API): tableNewSql, tableUpdateSql
Auth:
Authorization: Bearer $SANDBOX_ACCESS_KEY
X-Sandbox-Id: $SANDBOX_ID (optional, sent when provided)
"""
import os
import sys
import json
import argparse
from typing import Optional, List, Dict, Any
import requests
TABLE_PREFIX = "/api/v1/4sandbox/compose/db/table"
SQL_PREFIX = "/api/v1/4sandbox/table/sql"
def _require_env(name: str) -> str:
val = os.environ.get(name)
if not val:
raise ValueError(f"{name} environment variable is required")
return val
def _build_headers(access_key: str, sandbox_id: Optional[str]) -> Dict[str, str]:
headers = {
"Authorization": f"Bearer {access_key}",
}
if sandbox_id:
headers["X-Sandbox-Id"] = sandbox_id
return headers
def _base_url(base_url: Optional[str]) -> str:
if base_url is not None:
return base_url.rstrip("/")
v = os.environ.get("PLATFORM_BASE_URL")
if not v:
raise ValueError("PLATFORM_BASE_URL environment variable is required")
return v.rstrip("/")
def _access_key(access_key: Optional[str]) -> str:
return access_key or os.environ.get("SANDBOX_ACCESS_KEY")
def _sandbox_id(sandbox_id: Optional[str]) -> Optional[str]:
return sandbox_id or os.environ.get("SANDBOX_ID")
def _request_json(
method: str,
url: str,
headers: Dict[str, str],
*,
json_body: Optional[Dict[str, Any]] = None,
params: Optional[Dict[str, Any]] = None,
timeout_s: int = 60,
) -> Any:
resp = requests.request(method, url, headers=headers, json=json_body, params=params, timeout=timeout_s)
resp.raise_for_status()
return resp.json() if resp.content else None
# ---------------------------------------------------------------------------
# Table Definition APIs
# ---------------------------------------------------------------------------
def _space_id() -> int:
"""Read the project space id from the DEV_SPACE_ID environment variable.
The table must be created/queried inside the project's space, NOT the user's
personal space. The caller guarantees the source is DEV_SPACE_ID.
"""
raw = os.environ.get("DEV_SPACE_ID")
if not raw or not raw.strip():
raise ValueError(
"DEV_SPACE_ID environment variable is required so tables are created "
"in the project space instead of the personal space"
)
return int(raw.strip())
def add_table(
*,
name: str,
description: Optional[str] = None,
icon: Optional[str] = None,
base_url: Optional[str] = None,
access_key: Optional[str] = None,
sandbox_id: Optional[str] = None,
) -> Any:
"""Create a new table definition in the project space (DEV_SPACE_ID)."""
access_key = _access_key(access_key) or _require_env("SANDBOX_ACCESS_KEY")
sandbox_id = _sandbox_id(sandbox_id)
url = f"{_base_url(base_url)}{TABLE_PREFIX}/add"
headers = _build_headers(access_key, sandbox_id)
# spaceId MUST come from DEV_SPACE_ID so the table lands in the project space,
# not the caller's personal space (which is the backend's fallback when null).
payload: Dict[str, Any] = {"tableName": name, "spaceId": _space_id()}
if description is not None:
payload["tableDescription"] = description
if icon is not None:
payload["icon"] = icon
return _request_json("POST", url, headers, json_body=payload)
def update_table_name(
*,
id: int,
name: Optional[str] = None,
description: Optional[str] = None,
icon: Optional[str] = None,
base_url: Optional[str] = None,
access_key: Optional[str] = None,
sandbox_id: Optional[str] = None,
) -> Any:
"""Update table name / description / icon."""
access_key = _access_key(access_key) or _require_env("SANDBOX_ACCESS_KEY")
sandbox_id = _sandbox_id(sandbox_id)
url = f"{_base_url(base_url)}{TABLE_PREFIX}/updateTableName"
headers = _build_headers(access_key, sandbox_id)
payload: Dict[str, Any] = {"id": id}
if name is not None:
payload["tableName"] = name
if description is not None:
payload["tableDescription"] = description
if icon is not None:
payload["icon"] = icon
return _request_json("POST", url, headers, json_body=payload)
def update_table_definition(
*,
id: int,
field_list: List[Dict[str, Any]],
base_url: Optional[str] = None,
access_key: Optional[str] = None,
sandbox_id: Optional[str] = None,
) -> Any:
"""Update table field definitions (columns)."""
access_key = _access_key(access_key) or _require_env("SANDBOX_ACCESS_KEY")
sandbox_id = _sandbox_id(sandbox_id)
url = f"{_base_url(base_url)}{TABLE_PREFIX}/updateTableDefinition"
headers = _build_headers(access_key, sandbox_id)
payload = {"id": id, "fieldList": field_list}
return _request_json("POST", url, headers, json_body=payload)
def delete_table(
*,
id: int,
base_url: Optional[str] = None,
access_key: Optional[str] = None,
sandbox_id: Optional[str] = None,
) -> Any:
"""Delete a table definition by id."""
access_key = _access_key(access_key) or _require_env("SANDBOX_ACCESS_KEY")
sandbox_id = _sandbox_id(sandbox_id)
url = f"{_base_url(base_url)}{TABLE_PREFIX}/delete/{id}"
headers = _build_headers(access_key, sandbox_id)
return _request_json("POST", url, headers)
def list_tables(
*,
table_name: Optional[str] = None,
table_description: Optional[str] = None,
page_no: int = 1,
page_size: int = 20,
base_url: Optional[str] = None,
access_key: Optional[str] = None,
sandbox_id: Optional[str] = None,
) -> Any:
"""List table definitions (paginated) in the project space (DEV_SPACE_ID)."""
access_key = _access_key(access_key) or _require_env("SANDBOX_ACCESS_KEY")
sandbox_id = _sandbox_id(sandbox_id)
url = f"{_base_url(base_url)}{TABLE_PREFIX}/list"
headers = _build_headers(access_key, sandbox_id)
# spaceId MUST come from DEV_SPACE_ID so we list the project's tables,
# not the caller's personal space tables (which is the backend's fallback when null).
query_filter: Dict[str, Any] = {"spaceId": _space_id()}
if table_name is not None:
query_filter["tableName"] = table_name
if table_description is not None:
query_filter["tableDescription"] = table_description
payload: Dict[str, Any] = {
"pageNo": page_no,
"pageSize": page_size,
"queryFilter": query_filter,
}
return _request_json("POST", url, headers, json_body=payload)
def get_table(
*,
table_id: int,
base_url: Optional[str] = None,
access_key: Optional[str] = None,
sandbox_id: Optional[str] = None,
) -> Any:
"""Get table definition detail by id."""
access_key = _access_key(access_key) or _require_env("SANDBOX_ACCESS_KEY")
sandbox_id = _sandbox_id(sandbox_id)
url = f"{_base_url(base_url)}{TABLE_PREFIX}/detailById"
headers = _build_headers(access_key, sandbox_id)
return _request_json("GET", url, headers, params={"id": table_id})
def exist_table_data(
*,
table_id: int,
base_url: Optional[str] = None,
access_key: Optional[str] = None,
sandbox_id: Optional[str] = None,
) -> Any:
"""Check if a table has any business data rows."""
access_key = _access_key(access_key) or _require_env("SANDBOX_ACCESS_KEY")
sandbox_id = _sandbox_id(sandbox_id)
url = f"{_base_url(base_url)}{TABLE_PREFIX}/existTableData"
headers = _build_headers(access_key, sandbox_id)
return _request_json("GET", url, headers, params={"tableId": table_id})
def copy_table(
*,
table_id: int,
base_url: Optional[str] = None,
access_key: Optional[str] = None,
sandbox_id: Optional[str] = None,
) -> Any:
"""Copy a table structure definition to a new table."""
access_key = _access_key(access_key) or _require_env("SANDBOX_ACCESS_KEY")
sandbox_id = _sandbox_id(sandbox_id)
url = f"{_base_url(base_url)}{TABLE_PREFIX}/copyTableDefinition"
headers = _build_headers(access_key, sandbox_id)
return _request_json("POST", url, headers, params={"tableId": table_id})
# ---------------------------------------------------------------------------
# Table SQL API
# ---------------------------------------------------------------------------
def table_sql_new(
*,
project_id: str,
api_name: str,
description: str,
sql: str,
table_id: Optional[int] = None,
group_name: Optional[str] = None,
args: Optional[List[Dict[str, Any]]] = None,
base_url: Optional[str] = None,
access_key: Optional[str] = None,
sandbox_id: Optional[str] = None,
) -> Any:
"""Create a new SQL-based table operation API for page components.
SQL placeholders: use {{var}} for ALL variables, including LIKE fuzzy queries.
For LIKE, the caller must wrap the value with % itself (e.g. '%keyword%') — the
platform does NOT auto-wrap %. Do NOT use ${{var}} (it drops quotes and causes
"Unknown column" errors in this sandbox).
The endpoint token is carried in the returned payload's path `.../page/w/{token}`;
record it in .project.md immediately.
Args format: list of dicts with keys like name, description, require, dataType, etc.
"""
access_key = _access_key(access_key) or _require_env("SANDBOX_ACCESS_KEY")
sandbox_id = _sandbox_id(sandbox_id)
url = f"{_base_url(base_url)}{SQL_PREFIX}/new"
headers = _build_headers(access_key, sandbox_id)
payload: Dict[str, Any] = {
"projectId": project_id,
"apiName": api_name,
"description": description,
"sql": sql,
}
if table_id is not None:
payload["tableId"] = table_id
if group_name is not None:
payload["groupName"] = group_name
if args is not None:
payload["args"] = args
return _request_json("POST", url, headers, json_body=payload)
def table_sql_update(
*,
project_id: str,
api_id: int,
api_name: str,
description: str,
sql: str,
args: Optional[List[Dict[str, Any]]] = None,
base_url: Optional[str] = None,
access_key: Optional[str] = None,
sandbox_id: Optional[str] = None,
) -> Any:
"""Update an existing SQL-based table operation API.
SQL placeholders: same as table_sql_new — use {{var}} (+ caller-wrapped %) for LIKE;
do NOT use ${{var}}.
⚠️ The endpoint token is OFTEN REGENERATED after update. The returned payload's path
`.../page/w/{token}` carries the NEW token; you MUST refresh .project.md and the
front-end lib's token map, or page calls will silently fail with the stale token.
Args format: list of dicts with keys like name, description, require, dataType, etc.
"""
access_key = _access_key(access_key) or _require_env("SANDBOX_ACCESS_KEY")
sandbox_id = _sandbox_id(sandbox_id)
url = f"{_base_url(base_url)}{SQL_PREFIX}/update"
headers = _build_headers(access_key, sandbox_id)
payload: Dict[str, Any] = {
"projectId": project_id,
"apiId": api_id,
"apiName": api_name,
"description": description,
"sql": sql,
}
if args is not None:
payload["args"] = args
return _request_json("POST", url, headers, json_body=payload)
def _parse_json_arg(s: Optional[str]) -> Optional[Any]:
if s is None:
return None
s = s.strip()
if not s:
return None
return json.loads(s)
def main():
parser = argparse.ArgumentParser(description="Data Table for Page API Client (sandbox)")
parser.add_argument("--base-url", help="PLATFORM_BASE_URL override")
parser.add_argument("--access-key", help="SANDBOX_ACCESS_KEY override")
parser.add_argument("--sandbox-id", help="SANDBOX_ID override")
parser.add_argument("--raw", action="store_true", help="print raw JSON")
sub = parser.add_subparsers(dest="command", required=True)
# ---- Table Definition ----
p_add = sub.add_parser("add-table", help="Create a new table definition")
p_add.add_argument("--name", required=True, help="Table name")
p_add.add_argument("--description", help="Table description")
p_add.add_argument("--icon", help="Table icon")
p_utn = sub.add_parser("update-table-name", help="Update table name/description/icon")
p_utn.add_argument("--id", type=int, required=True, help="Table ID")
p_utn.add_argument("--name", help="New table name")
p_utn.add_argument("--description", help="New table description")
p_utn.add_argument("--icon", help="New table icon")
p_utf = sub.add_parser("update-table-definition", help="Update table field definitions")
p_utf.add_argument("--id", type=int, required=True, help="Table ID")
p_utf.add_argument("--field-list", required=True, help="JSON array of field definitions")
p_del = sub.add_parser("delete-table", help="Delete a table definition")
p_del.add_argument("--id", type=int, required=True, help="Table ID")
p_list = sub.add_parser("list-tables", help="List table definitions (paginated)")
p_list.add_argument("--table-name", help="Filter by table name")
p_list.add_argument("--table-description", help="Filter by table description")
p_list.add_argument("--page-no", type=int, default=1, help="Page number (default: 1)")
p_list.add_argument("--page-size", type=int, default=20, help="Page size (default: 20)")
p_get = sub.add_parser("get-table", help="Get table definition detail")
p_get.add_argument("--table-id", type=int, required=True, help="Table ID")
p_exist = sub.add_parser("exist-table-data", help="Check if table has business data")
p_exist.add_argument("--table-id", type=int, required=True, help="Table ID")
p_copy = sub.add_parser("copy-table", help="Copy a table structure")
p_copy.add_argument("--table-id", type=int, required=True, help="Source table ID")
# ---- Table SQL API ----
p_tsn = sub.add_parser("table-sql-new", help="Create a new SQL table operation API")
p_tsn.add_argument("--project-id", required=True, help="Project ID")
p_tsn.add_argument("--table-id", type=int, help="Table ID")
p_tsn.add_argument("--api-name", required=True, help="API name")
p_tsn.add_argument("--description", required=True, help="API description")
p_tsn.add_argument("--sql", required=True, help="SQL statement (use {{var}} for placeholders)")
p_tsn.add_argument("--group-name", help="Group name for the API")
p_tsn.add_argument("--args", help="JSON array of parameter definitions")
p_tsu = sub.add_parser("table-sql-update", help="Update an existing SQL table operation API")
p_tsu.add_argument("--project-id", required=True, help="Project ID")
p_tsu.add_argument("--api-id", type=int, required=True, help="API ID to update")
p_tsu.add_argument("--api-name", required=True, help="API name")
p_tsu.add_argument("--description", required=True, help="API description")
p_tsu.add_argument("--sql", required=True, help="SQL statement (use {{var}} for placeholders)")
p_tsu.add_argument("--args", help="JSON array of parameter definitions")
args = parser.parse_args()
common = dict(
base_url=args.base_url,
access_key=args.access_key,
sandbox_id=args.sandbox_id,
)
result = None
if args.command == "add-table":
result = add_table(
name=args.name,
description=args.description,
icon=args.icon,
**common,
)
elif args.command == "update-table-name":
result = update_table_name(
id=args.id,
name=args.name,
description=args.description,
icon=args.icon,
**common,
)
elif args.command == "update-table-definition":
field_list = _parse_json_arg(args.field_list)
result = update_table_definition(
id=args.id,
field_list=field_list,
**common,
)
elif args.command == "delete-table":
result = delete_table(id=args.id, **common)
elif args.command == "list-tables":
result = list_tables(
table_name=args.table_name,
table_description=args.table_description,
page_no=args.page_no,
page_size=args.page_size,
**common,
)
elif args.command == "get-table":
result = get_table(table_id=args.table_id, **common)
elif args.command == "exist-table-data":
result = exist_table_data(table_id=args.table_id, **common)
elif args.command == "copy-table":
result = copy_table(table_id=args.table_id, **common)
elif args.command == "table-sql-new":
parsed_args = _parse_json_arg(args.args)
result = table_sql_new(
project_id=args.project_id,
api_name=args.api_name,
description=args.description,
sql=args.sql,
table_id=args.table_id,
group_name=args.group_name,
args=parsed_args,
**common,
)
elif args.command == "table-sql-update":
parsed_args = _parse_json_arg(args.args)
result = table_sql_update(
project_id=args.project_id,
api_id=args.api_id,
api_name=args.api_name,
description=args.description,
sql=args.sql,
args=parsed_args,
**common,
)
else:
raise RuntimeError(f"unknown command: {args.command}")
if result is not None:
print(json.dumps(result, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()

View File

@@ -409,6 +409,7 @@ public class HttpProxyRequestHandler extends ChannelInboundHandlerAdapter {
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
if (cause.getCause() != null && (cause.getCause() instanceof SSLHandshakeException)) {
logger.warn("SSLHandshakeException: {}", cause.getCause().getMessage());
ctx.close();
return;
}
clearReceivedWhenConnect();

View File

@@ -3,6 +3,8 @@ 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.common.UserContext;
import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.spec.page.PageQueryVo;
import com.xspaceagi.system.spec.page.SuperPage;
@@ -13,6 +15,11 @@ import java.util.List;
*/
public interface ICustomPageRpcService {
/**
* 创建项目
*/
String create(Long userId, Long spaceId, String name);
/**
* 查询项目列表
*/
@@ -74,4 +81,6 @@ public interface ICustomPageRpcService {
* 管理端根据ids批量查询网页应用列表
*/
List<CustomPageDto> listByIds(List<Long> pageIds, List<Long> agentIds);
void bindDataSource(Long userId, Long projectId, String type, Long dataSourceId);
}

View File

@@ -88,6 +88,9 @@ public class CustomPageDto {
@Schema(description = "Space ID")
private Long spaceId;
@Schema(description = "Sandbox ID")
private Long sandboxId;
@Schema(description = "Created time")
private Date created;

View File

@@ -49,4 +49,7 @@ public class ProjectConfigExportDto implements Serializable {
@Schema(description = "Bound workflow data sources")
private List<Map<String, Object>> dataSourceWorkflows;
@Schema(description = "Project resource group and workflow bindings")
private ResourceGroupExportDto resourceGroup;
}

View File

@@ -0,0 +1,28 @@
package com.xspaceagi.custompage.sdk.dto;
import java.io.Serializable;
import java.util.List;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Resource group export DTO for project import/export.
* Group name is always projectId, so only description and workflow bindings need exporting.
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Schema(description = "Resource group export DTO")
public class ResourceGroupExportDto implements Serializable {
@Schema(description = "Resource group description")
private String description;
@Schema(description = "Workflow data source keys bound to this group")
private List<String> workflowKeys;
}

View File

@@ -11,6 +11,7 @@ public enum CustomPageActionEnum {
UPLOAD("upload", "Upload project"),
SUBMIT_FILES_UPDATE("submit_files_update", "Submit file update"),
UPLOAD_SINGLE_FILE("upload_single_file", "Upload single file"),
UPLOAD_BATCH_FILES("upload_batch_files", "Upload batch files"),
CHAT("chat", "Chat"),
ROLLBACK_VERSION("rollback_version", "Rollback version");

View File

@@ -18,6 +18,7 @@ import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
import java.util.Map;
import static com.xspaceagi.system.spec.enums.ResourceEnum.*;
@@ -104,6 +105,33 @@ public class CustomPageCodingController extends BaseController {
}
}
@RequireResource(PAGE_APP_UPLOAD_FILE)
@Operation(summary = "Upload batch files", description = "Upload multiple files to paths in the project")
@PostMapping(value = "/upload-batch-files", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<Map<String, Object>> uploadBatchFiles(
@RequestParam("projectId") Long projectId,
@RequestParam("files") List<MultipartFile> files,
@RequestParam("filePaths") List<String> filePaths) {
log.info("[Web] upload batch files request, project Id={}, fileCount={}", projectId, files != null ? files.size() : 0);
try {
if (files == null || files.isEmpty()) {
return ReqResult.error("0001", "Files are required");
}
if (filePaths == null || filePaths.size() != files.size()) {
return ReqResult.error("0001", "filePaths and files count mismatch");
}
UserContext userContext = getUser();
return customPageCodingApplicationService.uploadBatchFiles(projectId, files, filePaths, userContext);
} catch (SpacePermissionException e) {
log.error("[Web] upload batch files failed, project Id={}, {}", projectId, e.getMessage());
return ReqResult.error(e.getCode(), e.getMessage());
} catch (Exception e) {
log.error("[Web] upload batch files failed, project Id={}", projectId, e);
return ReqResult.error("0001", "Upload batch files failed: " + e.getMessage());
}
}
@RequireResource(PAGE_APP_QUERY_DETAIL)
@Operation(summary = "Get file proxy URL", description = "Get reverse-proxy URL for a single file")
@GetMapping(value = "/file-proxy-url", produces = MediaType.APPLICATION_JSON_VALUE)

View File

@@ -4,6 +4,7 @@ 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.PublishedPermissionDto;
import com.xspaceagi.agent.core.adapter.repository.CopyIndexRecordRepository;
import com.xspaceagi.agent.core.adapter.repository.entity.Published;
import com.xspaceagi.custompage.application.service.ICustomPageBuildApplicationService;
import com.xspaceagi.custompage.application.service.ICustomPageConfigApplicationService;
@@ -63,6 +64,8 @@ public class CustomPageConfigController extends BaseController {
private ICustomPageProxyPathService customPageProxyPathApplicationService;
@Resource
private ICustomPageBuildApplicationService customPageBuildApplicationService;
@Resource
private CopyIndexRecordRepository copyIndexRecordRepository;
@RequireResource(PAGE_APP_CREATE)
@Operation(summary = "Create project", description = "Create a new project")
@@ -87,7 +90,7 @@ public class CustomPageConfigController extends BaseController {
if (!result.isSuccess()) {
return ReqResult.create(result.getCode(), result.getMessage(), null);
}
log.info("[Web] createprojectsucceeded,startinitialize,project Id={}", result.getData().getId());
log.info("[Web] create project succeeded,start initialize,project Id={}", result.getData().getId());
// 初始化项目
ReqResult<Map<String, Object>> initResult = customPageBuildApplicationService
@@ -96,7 +99,7 @@ public class CustomPageConfigController extends BaseController {
result.getData().getId(), initResult.getCode(), initResult.getMessage(), initResult.getData());
if (!initResult.isSuccess()) {
log.warn("[Web] project Id={},initializeprojectfailed,startdelete project",
log.warn("[Web] project Id={},initialize project failed,start delete project",
result.getData().getId());
customPageConfigApplicationService.deleteProject(result.getData().getId(), userContext);
return ReqResult.error("9999", initResult.getMessage());
@@ -117,7 +120,7 @@ public class CustomPageConfigController extends BaseController {
@Operation(summary = "Upload project", description = "Upload a zip to create or update a project")
@PostMapping(value = "/upload-and-start", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<CustomPageCreateRes> uploadProject(@ModelAttribute CustomPageCreateReq req) {
log.info("[Web] upload projectrequest, project Name={}, project Id={}", req.getProjectName(), req.getProjectId());
log.info("[Web] upload project request, project Name={}, project Id={}", req.getProjectName(), req.getProjectId());
try {
UserContext userContext = getUser();
CustomPageConfigModel configModel = null;
@@ -153,7 +156,7 @@ public class CustomPageConfigController extends BaseController {
userContext);
if (!result.isSuccess()) {
if (isInitProject) {
log.warn("[Web] project Id={},upload projectfailed,startdelete project", configModel.getId());
log.warn("[Web] project Id={},upload project failed,start delete project", configModel.getId());
customPageConfigApplicationService.deleteProject(configModel.getId(), userContext);
}
return ReqResult.error("9999", "Upload project failed: " + result.getMessage());
@@ -170,11 +173,11 @@ public class CustomPageConfigController extends BaseController {
return ReqResult.success(res);
} catch (SpacePermissionException e) {
log.error("[Web] upload projectfailed, project Name={}, project Id={}, {}", req.getProjectName(), req.getProjectId(),
log.error("[Web] upload project failed, project Name={}, project Id={}, {}", req.getProjectName(), req.getProjectId(),
e.getMessage());
return ReqResult.error(e.getCode(), e.getMessage());
} catch (Exception e) {
log.error("[Web] upload projectexception, project Name={}, project Id={}", req.getProjectName(), req.getProjectId(), e);
log.error("[Web] upload project exception, project Name={}, project Id={}", req.getProjectName(), req.getProjectId(), e);
return ReqResult.error("0001", e.getMessage());
}
}
@@ -183,7 +186,7 @@ public class CustomPageConfigController extends BaseController {
@Operation(summary = "Update project", description = "Update basic project information")
@PostMapping(value = "/update-project", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<Void> updateProject(@RequestBody CustomPageUpdateReq req) {
log.info("[Web] modify projectrequest, project Id={}, project Name={}", req.getProjectId(), req.getProjectName());
log.info("[Web] modify project request, project Id={}, project Name={}", req.getProjectId(), req.getProjectName());
try {
UserContext userContext = getUser();
@@ -199,17 +202,17 @@ public class CustomPageConfigController extends BaseController {
var result = customPageConfigApplicationService.updateProject(model, userContext);
if (!result.isSuccess()) {
log.warn("[Web] project Id={},modify projectfailed,message={}", req.getProjectId(), result.getMessage());
log.warn("[Web] project Id={},modify project failed,message={}", req.getProjectId(), result.getMessage());
return ReqResult.create(result.getCode(), result.getMessage(), null);
}
return ReqResult.success();
} catch (SpacePermissionException e) {
log.error("[Web] modify projectfailed, project Id={}, project Name={}, {}", req.getProjectId(), req.getProjectName(),
log.error("[Web] modify project failed, project Id={}, project Name={}, {}", req.getProjectId(), req.getProjectName(),
e.getMessage());
return ReqResult.error(e.getCode(), e.getMessage());
} catch (Exception e) {
log.error("[Web] modify projectfailed, project Id={}, project Name={}", req.getProjectId(), req.getProjectName(), e);
log.error("[Web] modify project failed, project Id={}, project Name={}", req.getProjectId(), req.getProjectName(), e);
return ReqResult.error("0001", e.getMessage());
}
}
@@ -218,13 +221,13 @@ public class CustomPageConfigController extends BaseController {
@Operation(summary = "Delete project", description = "Delete a project")
@PostMapping(value = "/delete-project", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<Void> deleteProject(@RequestBody CustomPageDeleteReq req) {
log.info("[Web] delete projectrequest, project Id={}", req.getProjectId());
log.info("[Web] delete project request, project Id={}", req.getProjectId());
try {
UserContext userContext = getUser();
var result = customPageConfigApplicationService.deleteProject(req.getProjectId(), userContext);
if (!result.isSuccess()) {
log.warn("[Web] project Id={},delete projectfailed,message={}", req.getProjectId(), result.getMessage());
log.warn("[Web] project Id={},delete project failed,message={}", req.getProjectId(), result.getMessage());
return ReqResult.create(result.getCode(), result.getMessage(), null);
}
@@ -238,10 +241,10 @@ public class CustomPageConfigController extends BaseController {
return ReqResult.success();
} catch (SpacePermissionException e) {
log.error("[Web] delete projectfailed, project Id={}, {}", req.getProjectId(), e.getMessage());
log.error("[Web] delete project failed, project Id={}, {}", req.getProjectId(), e.getMessage());
return ReqResult.error(e.getCode(), e.getMessage());
} catch (Exception e) {
log.error("[Web] delete projectfailed, project Id={}", req.getProjectId(), e);
log.error("[Web] delete project failed, project Id={}", req.getProjectId(), e);
return ReqResult.error("0001", e.getMessage());
}
}
@@ -250,7 +253,7 @@ public class CustomPageConfigController extends BaseController {
@Operation(summary = "Copy project", description = "Copy project to target space")
@PostMapping(value = "/copy-project", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<CustomPageCreateRes> copyProject(@RequestBody CustomPageCopyReq req) {
log.info("[Web] copyprojectrequest, project Id={}, target Space Id={}", req.getProjectId(), req.getTargetSpaceId());
log.info("[Web] copy project request, project Id={}, target Space Id={}", req.getProjectId(), req.getTargetSpaceId());
Long projectId = req.getProjectId();
try {
UserContext userContext = getUser();
@@ -282,7 +285,7 @@ public class CustomPageConfigController extends BaseController {
// 创建新项目
CustomPageConfigModel newConfigModel = new CustomPageConfigModel();
newConfigModel.setName(sourceConfig.getName());
newConfigModel.setName(copyIndexRecordRepository.newCopyName("pageApp", targetSpaceId, sourceConfig.getName()));
newConfigModel.setDescription(sourceConfig.getDescription());
newConfigModel.setIcon(sourceConfig.getIcon());
newConfigModel.setCoverImg(sourceConfig.getCoverImg());
@@ -297,7 +300,7 @@ public class CustomPageConfigController extends BaseController {
ReqResult<CustomPageConfigModel> createResult = customPageConfigApplicationService.create(newConfigModel, userContext);
if (!createResult.isSuccess()) {
log.error("[copy Project] copyprojectfailed, message={}", createResult.getMessage());
log.error("[copy Project] copy project failed, message={}", createResult.getMessage());
return ReqResult.error("0001", createResult.getMessage());
}
CustomPageConfigModel targetConfig = createResult.getData();
@@ -318,7 +321,7 @@ public class CustomPageConfigController extends BaseController {
//删除项目
var deleteResult = customPageConfigApplicationService.deleteProject(targetConfig.getId(), userContext);
if (!deleteResult.isSuccess()) {
log.warn("[copy Project] target Project Id={},after copy project artifacts failed,delete projectfailed,message={}", targetConfig.getId(), deleteResult.getMessage());
log.warn("[copy Project] target Project Id={},after copy project artifacts failed,delete project failed,message={}", targetConfig.getId(), deleteResult.getMessage());
}
//删除数据源
if (newCreateDataSources != null && !newCreateDataSources.isEmpty()) {
@@ -332,7 +335,7 @@ public class CustomPageConfigController extends BaseController {
workflowApplicationService.delete(id);
}
} catch (Exception e) {
log.error("[copy Project] copyprojectfailed, project Id={}, target Space Id={},delete component failed: type={}, id={}",
log.error("[copy Project] copy project failed, project Id={}, target Space Id={},delete component failed: type={}, id={}",
req.getProjectId(), req.getTargetSpaceId(), type, id, e);
}
});
@@ -340,11 +343,11 @@ public class CustomPageConfigController extends BaseController {
return ReqResult.error(copyResult.getCode(), copyResult.getMessage());
} catch (SpacePermissionException e) {
log.error("[copy Project] copyprojectfailed, project Id={}, target Space Id={}, {}",
log.error("[copy Project] copy project failed, project Id={}, target Space Id={}, {}",
req.getProjectId(), req.getTargetSpaceId(), e.getMessage());
return ReqResult.error(e.getCode(), e.getMessage());
} catch (Exception e) {
log.error("[copy Project] copyprojectfailed, project Id={}, target Space Id={}",
log.error("[copy Project] copy project failed, project Id={}, target Space Id={}",
req.getProjectId(), req.getTargetSpaceId(), e);
return ReqResult.error("0001", e.getMessage());
}
@@ -354,7 +357,7 @@ public class CustomPageConfigController extends BaseController {
@Operation(summary = "Create reverse-proxy project", description = "Create a reverse-proxy type project")
@PostMapping(value = "/create-reverse-proxy", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<CustomPageCreateRes> createReverseProxy(@RequestBody CustomPageCreateReq req) {
log.info("[Web] create reverse proxy projectrequest, project Name={}", req.getProjectName());
log.info("[Web] create reverse proxy project request, project Name={}", req.getProjectName());
try {
UserContext userContext = getUser();
CustomPageConfigModel model = new CustomPageConfigModel();
@@ -366,17 +369,17 @@ public class CustomPageConfigController extends BaseController {
var result = customPageConfigApplicationService.createReverseProxyProject(model, userContext);
if (!result.isSuccess()) {
log.warn("[Web] create reverse proxy projectfailed,message={}", result.getMessage());
log.warn("[Web] create reverse proxy project failed,message={}", result.getMessage());
return ReqResult.create(result.getCode(), result.getMessage(), null);
}
CustomPageCreateRes res = CustomPageCreateRes.builder().projectId(result.getData().getId()).build();
return ReqResult.success(res);
} catch (SpacePermissionException e) {
log.error("[Web] create reverse proxy projectfailed, project Name={}, {}", req.getProjectName(), e.getMessage());
log.error("[Web] create reverse proxy project failed, project Name={}, {}", req.getProjectName(), e.getMessage());
return ReqResult.error(e.getCode(), e.getMessage());
} catch (Exception e) {
log.error("[Web] create reverse proxy projectfailed, project Name={}", req.getProjectName(), e);
log.error("[Web] create reverse proxy project failed, project Name={}", req.getProjectName(), e);
return ReqResult.error("0001", e.getMessage());
}
}
@@ -385,12 +388,12 @@ public class CustomPageConfigController extends BaseController {
@Operation(summary = "List projects", description = "Query project list")
@GetMapping(value = "/list-projects", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<List<CustomPageDto>> listProjects(CustomPageQueryReq req) {
log.info("[Web] queryprojectlistrequest, req={}", req);
log.info("[Web] query project list request, req={}", req);
try {
List<CustomPageDto> listData = iCustomPageRpcService.list(req);
return ReqResult.success(listData);
} catch (Exception e) {
log.error("[Web] queryprojectlistfailed", e);
log.error("[Web] query project list failed", e);
return ReqResult.error("0001", e.getMessage());
}
}
@@ -400,7 +403,7 @@ public class CustomPageConfigController extends BaseController {
@GetMapping(value = "/page-query-projects", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<SuperPage<CustomPageDto>> pageQueryProjects(
@RequestBody PageQueryVo<CustomPageQueryReq> pageQueryVo) {
log.info("[Web] pagedqueryprojectrequest, page Query Vo={}", pageQueryVo);
log.info("[Web] paged query project request, page Query Vo={}", pageQueryVo);
try {
CustomPageQueryReq req = pageQueryVo.getQueryFilter();
if (req == null) {
@@ -414,7 +417,7 @@ public class CustomPageConfigController extends BaseController {
SuperPage<CustomPageDto> page = iCustomPageRpcService.pageQuery(pageQueryVo);
return ReqResult.success(page);
} catch (Exception e) {
log.error("[Web] pagedqueryprojectfailed", e);
log.error("[Web] paged query project failed", e);
return ReqResult.error("0001", e.getMessage());
}
}
@@ -439,7 +442,7 @@ public class CustomPageConfigController extends BaseController {
log.error("[Web] query project detail, project Id={}, {}", projectId, e.getMessage());
return ReqResult.error(e.getCode(), e.getMessage());
} catch (Exception e) {
log.error("[Web] query project detailexception, project Id={}", projectId, e);
log.error("[Web] query project detail exception, project Id={}", projectId, e);
return ReqResult.error("0001", e.getMessage());
}
}
@@ -464,7 +467,7 @@ public class CustomPageConfigController extends BaseController {
log.error("[Web] query project detail, agent Id={}, {}", agentId, e.getMessage());
return ReqResult.error(e.getCode(), e.getMessage());
} catch (Exception e) {
log.error("[Web] query project detailexception, agent Id={}", agentId, e);
log.error("[Web] query project detail exception, agent Id={}", agentId, e);
return ReqResult.error("0001", e.getMessage());
}
}
@@ -473,12 +476,12 @@ public class CustomPageConfigController extends BaseController {
@Operation(summary = "Get project file content", description = "Query project workspace file content")
@GetMapping(value = "/get-project-content", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<ProjectContentRes> getProjectContent(@RequestParam("projectId") Long projectId) {
log.info("[Web] query project file contentrequest, project Id={}", projectId);
log.info("[Web] query project file content request, project Id={}", projectId);
try {
String proxyPath = "/page/static/" + projectId;
var result = customPageConfigApplicationService.queryProjectContent(projectId, proxyPath);
if (!result.isSuccess()) {
log.warn("[Web] project Id={},query project file contentfailed,message={}", projectId, result.getMessage());
log.warn("[Web] project Id={},query project file content failed,message={}", projectId, result.getMessage());
return ReqResult.create(result.getCode(), result.getMessage(), null);
}
@@ -496,10 +499,10 @@ public class CustomPageConfigController extends BaseController {
return ReqResult.success(res);
} catch (SpacePermissionException e) {
log.error("[Web] query project file contentfailed, project Id={}, {}", projectId, e.getMessage());
log.error("[Web] query project file content failed, project Id={}, {}", projectId, e.getMessage());
return ReqResult.error(e.getCode(), e.getMessage());
} catch (Exception e) {
log.error("[Web] query project file contentfailed, project Id={}", projectId, e);
log.error("[Web] query project file content failed, project Id={}", projectId, e);
return ReqResult.error("0001", e.getMessage());
}
}
@@ -509,7 +512,7 @@ public class CustomPageConfigController extends BaseController {
@GetMapping(value = "/get-project-content-by-version", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<ProjectContentRes> getProjectContentByVersion(@RequestParam("projectId") Long projectId,
@RequestParam("codeVersion") Integer codeVersion) {
log.info("[Web] queryprojecthistorical version contentrequest, project Id={}, code Version={}", projectId, codeVersion);
log.info("[Web] query project historical version content request, project Id={}, code Version={}", projectId, codeVersion);
try {
if (projectId == null || projectId <= 0) {
return ReqResult.error("0001", "projectId is required or invalid");
@@ -520,7 +523,7 @@ public class CustomPageConfigController extends BaseController {
String proxyPath = "/page/static/_his/" + projectId;
var result = customPageConfigApplicationService.queryProjectContentByVersion(projectId, codeVersion, proxyPath);
if (!result.isSuccess()) {
log.warn("[Web] project Id={},queryprojecthistorical version contentfailed,message={}", projectId, result.getMessage());
log.warn("[Web] project Id={},query project historical version content failed,message={}", projectId, result.getMessage());
return ReqResult.create(result.getCode(), result.getMessage(), null);
}
@@ -529,10 +532,10 @@ public class CustomPageConfigController extends BaseController {
return ReqResult.success(res);
} catch (SpacePermissionException e) {
log.error("[Web] queryprojecthistorical version contentfailed, project Id={}, code Version={}, {}", projectId, codeVersion, e.getMessage());
log.error("[Web] query project historical version content failed, project Id={}, code Version={}, {}", projectId, codeVersion, e.getMessage());
return ReqResult.error(e.getCode(), e.getMessage());
} catch (Exception e) {
log.error("[Web] queryprojecthistorical version contentfailed, project Id={}, code Version={}", projectId, codeVersion, e);
log.error("[Web] query project historical version content failed, project Id={}, code Version={}", projectId, codeVersion, e);
return ReqResult.error("0001", e.getMessage());
}
}
@@ -541,7 +544,7 @@ public class CustomPageConfigController extends BaseController {
@Operation(summary = "Export project", description = "Export project as a zip file")
@GetMapping(value = "/export-project")
public ResponseEntity<byte[]> exportProject(@RequestParam("projectId") Long projectId) {
log.info("[Web] exportprojectrequest, project Id={}", projectId);
log.info("[Web] export project request, project Id={}", projectId);
try {
if (projectId == null || projectId <= 0) {
return ResponseEntity.badRequest().build();
@@ -550,7 +553,7 @@ public class CustomPageConfigController extends BaseController {
UserContext userContext = getUser();
var result = customPageConfigApplicationService.exportProjectLatest(projectId, userContext);
if (!result.isSuccess()) {
log.warn("[Web] project Id={},exportprojectfailed,message={}", projectId, result.getMessage());
log.warn("[Web] project Id={},export project failed,message={}", projectId, result.getMessage());
return ResponseEntity.badRequest().build();
}
@@ -577,7 +580,7 @@ public class CustomPageConfigController extends BaseController {
log.error("[Web] Export project error, project Id={}", projectId, e);
return ResponseEntity.internalServerError().build();
} catch (SpacePermissionException e) {
log.error("[Web] exportprojectfailed, project Id={}, {}", projectId, e.getMessage());
log.error("[Web] export project failed, project Id={}, {}", projectId, e.getMessage());
return ResponseEntity.internalServerError().build();
} catch (Exception e) {
log.error("[Web] Export project error, project Id={}", projectId, e);
@@ -589,7 +592,7 @@ public class CustomPageConfigController extends BaseController {
@Operation(summary = "Batch configure reverse proxy", description = "Replace reverse-proxy configuration entirely")
@PostMapping(value = "/batch-config-proxy", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<Void> batchConfigProxy(@RequestBody ProxyConfigBatchReq req) {
log.info("[Web] batch configure reverse proxyrequest, project Id={}, config Count={}",
log.info("[Web] batch configure reverse proxy request, project Id={}, config Count={}",
req.getProjectId(), req.getProxyConfigs() != null ? req.getProxyConfigs().size() : 0);
try {
UserContext userContext = getUser();
@@ -611,16 +614,16 @@ public class CustomPageConfigController extends BaseController {
var result = customPageConfigApplicationService.batchConfigProxy(req.getProjectId(), proxyConfigs,
userContext);
if (!result.isSuccess()) {
log.warn("[Web] project Id={},batch configure reverse proxyfailed,message={}", req.getProjectId(), result.getMessage());
log.warn("[Web] project Id={},batch configure reverse proxy failed,message={}", req.getProjectId(), result.getMessage());
return ReqResult.create(result.getCode(), result.getMessage(), null);
}
return ReqResult.success();
} catch (SpacePermissionException e) {
log.error("[Web] batch configure reverse proxyfailed, project Id={}, {}", req.getProjectId(), e.getMessage());
log.error("[Web] batch configure reverse proxy failed, project Id={}, {}", req.getProjectId(), e.getMessage());
return ReqResult.error(e.getCode(), e.getMessage());
} catch (Exception e) {
log.error("[Web] batch configure reverse proxyfailed, project Id={}", req.getProjectId(), e);
log.error("[Web] batch configure reverse proxy failed, project Id={}", req.getProjectId(), e);
return ReqResult.error("0001", e.getMessage());
}
}
@@ -629,7 +632,7 @@ public class CustomPageConfigController extends BaseController {
@Operation(summary = "Save path arguments", description = "Save path argument schema for a page URI")
@PostMapping(value = "/save-path-args", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<Void> savePathArgs(@RequestBody PageArgConfigReq req) {
log.info("[Web] savepath request, project Id={}, page Uri={}", req.getProjectId(), req.getPageUri());
log.info("[Web] save path request, project Id={}, page Uri={}", req.getProjectId(), req.getPageUri());
try {
UserContext userContext = getUser();
@@ -672,17 +675,17 @@ public class CustomPageConfigController extends BaseController {
var result = customPageConfigApplicationService.savePathArgs(req.getProjectId(), pageArgConfig,
userContext);
if (!result.isSuccess()) {
log.warn("[Web] project Id={},savepath failed,message={}", req.getProjectId(), result.getMessage());
log.warn("[Web] project Id={},save path failed,message={}", req.getProjectId(), result.getMessage());
return ReqResult.create(result.getCode(), result.getMessage(), null);
}
return ReqResult.success();
} catch (SpacePermissionException e) {
log.error("[Web] savepath failed, project Id={}, page Uri={}, {}", req.getProjectId(), req.getPageUri(),
log.error("[Web] save path failed, project Id={}, page Uri={}, {}", req.getProjectId(), req.getPageUri(),
e.getMessage());
return ReqResult.error(e.getCode(), e.getMessage());
} catch (Exception e) {
log.error("[Web] savepath failed, project Id={}, page Uri={}", req.getProjectId(), req.getPageUri(), e);
log.error("[Web] save path failed, project Id={}, page Uri={}", req.getProjectId(), req.getPageUri(), e);
return ReqResult.error("0001", e.getMessage());
}
}
@@ -691,7 +694,7 @@ public class CustomPageConfigController extends BaseController {
@Operation(summary = "Add path config", description = "Add path config; fails if pageUri already exists")
@PostMapping(value = "/add-path", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<Void> addPath(@RequestBody PageArgConfigReq req) {
log.info("[Web] add path configrequest, project Id={}, page Uri={}", req.getProjectId(), req.getPageUri());
log.info("[Web] add path config request, project Id={}, page Uri={}", req.getProjectId(), req.getPageUri());
try {
UserContext userContext = getUser();
@@ -704,17 +707,17 @@ public class CustomPageConfigController extends BaseController {
var result = customPageConfigApplicationService.addPath(req.getProjectId(), pageArgConfig,
userContext);
if (!result.isSuccess()) {
log.warn("[Web] project Id={},add path configfailed,message={}", req.getProjectId(), result.getMessage());
log.warn("[Web] project Id={},add path config failed,message={}", req.getProjectId(), result.getMessage());
return ReqResult.create(result.getCode(), result.getMessage(), null);
}
return ReqResult.success();
} catch (SpacePermissionException e) {
log.error("[Web] add path configfailed, project Id={}, page Uri={}, {}", req.getProjectId(), req.getPageUri(),
log.error("[Web] add path config failed, project Id={}, page Uri={}, {}", req.getProjectId(), req.getPageUri(),
e.getMessage());
return ReqResult.error(e.getCode(), e.getMessage());
} catch (Exception e) {
log.error("[Web] add path configfailed, project Id={}, page Uri={}", req.getProjectId(), req.getPageUri(), e);
log.error("[Web] add path config failed, project Id={}, page Uri={}", req.getProjectId(), req.getPageUri(), e);
return ReqResult.error("0001", e.getMessage());
}
}
@@ -723,7 +726,7 @@ public class CustomPageConfigController extends BaseController {
@Operation(summary = "Edit path config", description = "Edit path config; fails if path does not exist")
@PostMapping(value = "/edit-path", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<Void> editPath(@RequestBody PageArgConfigReq req) {
log.info("[Web] editpath configrequest, project Id={}, page Uri={}", req.getProjectId(), req.getPageUri());
log.info("[Web] edit path config request, project Id={}, page Uri={}", req.getProjectId(), req.getPageUri());
try {
UserContext userContext = getUser();
@@ -735,17 +738,17 @@ public class CustomPageConfigController extends BaseController {
var result = customPageConfigApplicationService.editPath(req.getProjectId(), pageArgConfig,
userContext);
if (!result.isSuccess()) {
log.warn("[Web] project Id={},editpath configfailed,message={}", req.getProjectId(), result.getMessage());
log.warn("[Web] project Id={},edit path config failed,message={}", req.getProjectId(), result.getMessage());
return ReqResult.create(result.getCode(), result.getMessage(), null);
}
return ReqResult.success();
} catch (SpacePermissionException e) {
log.error("[Web] editpath configfailed, project Id={}, page Uri={}, {}", req.getProjectId(), req.getPageUri(),
log.error("[Web] edit path config failed, project Id={}, page Uri={}, {}", req.getProjectId(), req.getPageUri(),
e.getMessage());
return ReqResult.error(e.getCode(), e.getMessage());
} catch (Exception e) {
log.error("[Web] editpath configexception, project Id={}, page Uri={}", req.getProjectId(), req.getPageUri(), e);
log.error("[Web] edit path config exception, project Id={}, page Uri={}", req.getProjectId(), req.getPageUri(), e);
return ReqResult.error("0001", e.getMessage());
}
}
@@ -754,24 +757,24 @@ public class CustomPageConfigController extends BaseController {
@Operation(summary = "Delete path config", description = "Delete path config; fails if path does not exist")
@PostMapping(value = "/delete-path", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<Void> deletePath(@RequestBody DeletePathReq req) {
log.info("[Web] delete path configrequest, project Id={}, page Uri={}", req.getProjectId(), req.getPageUri());
log.info("[Web] delete path config request, project Id={}, page Uri={}", req.getProjectId(), req.getPageUri());
try {
UserContext userContext = getUser();
var result = customPageConfigApplicationService.deletePath(req.getProjectId(), req.getPageUri(),
userContext);
if (!result.isSuccess()) {
log.warn("[Web] project Id={},delete path configfailed,message={}", req.getProjectId(), result.getMessage());
log.warn("[Web] project Id={},delete path config failed,message={}", req.getProjectId(), result.getMessage());
return ReqResult.create(result.getCode(), result.getMessage(), null);
}
return ReqResult.success();
} catch (SpacePermissionException e) {
log.error("[Web] delete path configfailed, project Id={}, page Uri={}, {}", req.getProjectId(), req.getPageUri(),
log.error("[Web] delete path config failed, project Id={}, page Uri={}, {}", req.getProjectId(), req.getPageUri(),
e.getMessage());
return ReqResult.error(e.getCode(), e.getMessage());
} catch (Exception e) {
log.error("[Web] delete path configfailed, project Id={}, page Uri={}", req.getProjectId(), req.getPageUri(), e);
log.error("[Web] delete path config failed, project Id={}, page Uri={}", req.getProjectId(), req.getPageUri(), e);
return ReqResult.error("0001", e.getMessage());
}
}
@@ -780,7 +783,7 @@ public class CustomPageConfigController extends BaseController {
@Operation(summary = "Bind data source", description = "Bind plugin or workflow data source")
@PostMapping(value = "/bind-data-source", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<Void> bindDataSource(@RequestBody BindDataSourceReq req) {
log.info("[Web] binddata sourcerequest, project Id={}, type={}, data Source Id={}",
log.info("[Web] bind data source request, project Id={}, type={}, data Source Id={}",
req.getProjectId(), req.getType(), req.getDataSourceId());
try {
UserContext userContext = getUser();
@@ -791,17 +794,17 @@ public class CustomPageConfigController extends BaseController {
req.getDataSourceId(),
userContext);
if (!result.isSuccess()) {
log.warn("[Web] project Id={},binddata sourcefailed,message={}", req.getProjectId(), result.getMessage());
log.warn("[Web] project Id={},bind data source failed,message={}", req.getProjectId(), result.getMessage());
return ReqResult.create(result.getCode(), result.getMessage(), null);
}
return ReqResult.success();
} catch (SpacePermissionException e) {
log.error("[Web] binddata sourcefailed, project Id={}, type={}, data Source Id={}, {}", req.getProjectId(), req.getType(),
log.error("[Web] bind data source failed, project Id={}, type={}, data Source Id={}, {}", req.getProjectId(), req.getType(),
req.getDataSourceId(), e.getMessage());
return ReqResult.error(e.getCode(), e.getMessage());
} catch (Exception e) {
log.error("[Web] binddata sourcefailed, project Id={}, type={}, data Source Id={}",
log.error("[Web] bind data source failed, project Id={}, type={}, data Source Id={}",
req.getProjectId(), req.getType(), req.getDataSourceId(), e);
return ReqResult.error("0001", e.getMessage());
}
@@ -811,7 +814,7 @@ public class CustomPageConfigController extends BaseController {
@Operation(summary = "Unbind data source", description = "Unbind a data source from the project")
@PostMapping(value = "/unbind-data-source", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<Void> unbindDataSource(@RequestBody BindDataSourceReq req) {
log.info("[Web] unbinddata sourcerequest, project Id={}, type={}, data Source Id={}",
log.info("[Web] unbind data source request, project Id={}, type={}, data Source Id={}",
req.getProjectId(), req.getType(), req.getDataSourceId());
try {
UserContext userContext = getUser();
@@ -822,17 +825,17 @@ public class CustomPageConfigController extends BaseController {
req.getDataSourceId(),
userContext);
if (!result.isSuccess()) {
log.warn("[Web] project Id={},unbinddata sourcefailed,message={}", req.getProjectId(), result.getMessage());
log.warn("[Web] project Id={},unbind data source failed,message={}", req.getProjectId(), result.getMessage());
return ReqResult.create(result.getCode(), result.getMessage(), null);
}
return ReqResult.success();
} catch (SpacePermissionException e) {
log.error("[Web] unbinddata sourcefailed, project Id={}, type={}, data Source Id={}, {}", req.getProjectId(), req.getType(),
log.error("[Web] unbind data source failed, project Id={}, type={}, data Source Id={}, {}", req.getProjectId(), req.getType(),
req.getDataSourceId(), e.getMessage());
return ReqResult.error(e.getCode(), e.getMessage());
} catch (Exception e) {
log.error("[Web] unbinddata sourcefailed, project Id={}, type={}, data Source Id={}",
log.error("[Web] unbind data source failed, project Id={}, type={}, data Source Id={}",
req.getProjectId(), req.getType(), req.getDataSourceId(), e);
return ReqResult.error("0001", e.getMessage());
}

View File

@@ -1,110 +0,0 @@
package com.xspaceagi.eco.market.spec.api;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import com.xspaceagi.system.domain.service.UserDomainService;
import com.xspaceagi.system.infra.dao.entity.Tenant;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.Scheduled;
import com.xspaceagi.eco.market.domain.service.IEcoMarketClientSecretDomainService;
import com.xspaceagi.eco.market.spec.app.service.IEcoMarketClientSecretApplicationService;
import com.xspaceagi.system.spec.common.RequestContext;
import com.xspaceagi.system.spec.common.UserContext;
import com.xspaceagi.system.spec.enums.TenantStatus;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
/**
* 生态市场客户端配置
*/
@Slf4j
@Configuration
public class EcoMarketClientRegisterTask {
@Resource
private IEcoMarketClientSecretDomainService ecoMarketClientSecretDomainService;
@Resource
private UserDomainService userDomainService;
@Resource
private IEcoMarketClientSecretApplicationService clientSecretApplicationService;
/**
* 定时重试失败的任务
* 每5分钟执行一次
*/
@Scheduled(fixedDelayString = "${eco.market.client.retry.interval:300}", timeUnit = TimeUnit.SECONDS)
public void scheduledRetryFailedTasks() {
log.debug("Start eco-market client secret retry");
// 查询所有的租户
List<Tenant> tenants = userDomainService.queryTenantsByStatus(TenantStatus.Enabled);
for (Tenant tenant : tenants) {
log.debug("Check client secret for tenant [{}]", tenant.getName());
processTenant(tenant);
}
}
/**
* 处理单个租户的客户端密钥注册和保存
*
* @param tenant 租户信息
*/
private void processTenant(Tenant tenant) {
try {
// 创建系统用户上下文
UserContext userContext = UserContext.builder()
.userId(0L)
.userName("system")
.tenantId(tenant.getId())
.tenantName(tenant.getName())
.build();
// 设置请求上下文
RequestContext<UserContext> requestContext = new RequestContext<>();
requestContext.setTenantId(tenant.getId());
requestContext.setUserContext(userContext);
RequestContext.set(requestContext);
var tenantId = tenant.getId();
if (Objects.isNull(tenantId)) {
log.error("Tenant [{}] id empty; cannot register client secret", tenant.getName());
return;
}
// 检查并注册客户端密钥
boolean exists = ecoMarketClientSecretDomainService.existsClientSecret(tenantId);
if (!exists) {
log.info("Tenant [{}] has no client secret; registering", tenant.getName());
try {
var clientSecret = ecoMarketClientSecretDomainService.getOrRegisterClientSecret(
tenantId,
tenant.getName(),
"生态市场客户端 - " + tenant.getName());
// 使用应用层服务保存客户端密钥到本地数据库
if (clientSecret != null) {
clientSecretApplicationService.saveClientSecretDTO(clientSecret, userContext);
log.info("Tenant [{}] client secret registered and saved", tenant.getName());
}
} catch (Exception e) {
// 注册异常
log.error("Tenant [{}] client secret register error; queued for retry", tenant.getName(), e);
}
} else {
log.debug("Tenant [{}] client secret already exists", tenant.getName());
}
} catch (Exception e) {
log.error("Client secret handling error for tenant [{}]", tenant.getName(), e);
} finally {
// 清除上下文
RequestContext.remove();
}
}
}

View File

@@ -0,0 +1,70 @@
package com.xspaceagi.eco.market.spec.api;
import com.xspaceagi.eco.market.domain.service.IEcoMarketClientSecretDomainService;
import com.xspaceagi.eco.market.sdk.constant.EcoMarketRegisterTaskConstant;
import com.xspaceagi.eco.market.sdk.service.IEcoMarketSecretRpcService;
import com.xspaceagi.system.sdk.service.AbstractTaskExecuteService;
import com.xspaceagi.system.sdk.service.dto.ScheduleTaskDto;
import com.xspaceagi.system.spec.common.RequestContext;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.Map;
/**
* 租户生态市场客户端注册任务:创建租户后异步注册,失败自动重试直至成功。
*/
@Slf4j
@Service(EcoMarketRegisterTaskConstant.BEAN_ID)
public class EcoMarketClientRegisterTaskService extends AbstractTaskExecuteService {
@Resource
private IEcoMarketSecretRpcService ecoMarketSecretRpcService;
@Resource
private IEcoMarketClientSecretDomainService ecoMarketClientSecretDomainService;
@Value("${eco-market.server.cancelHB:}")
private String cancelHB;
@Value("${eco-market.server.enabled:false}")
private boolean ecoMarketServerEnabled;
@Override
protected boolean execute(ScheduleTaskDto scheduleTask) {
if (!ecoMarketServerEnabled || StringUtils.isNotBlank(cancelHB)) {
return true;
}
Map<String, Object> params = (Map<String, Object>) scheduleTask.getParams();
if (params == null || params.get("tenantId") == null) {
log.error("Eco market register task missing tenantId, taskId={}", scheduleTask.getTaskId());
return true;
}
Long tenantId = Long.parseLong(params.get("tenantId").toString());
String name = params.get("name") != null ? params.get("name").toString() : "EcoMarket-Client-" + tenantId;
String description = params.get("description") != null ? params.get("description").toString() : "";
if (StringUtils.isBlank(description)) {
description = "生态市场客户端 - " + name;
}
RequestContext.setThreadTenantId(tenantId);
try {
if (ecoMarketClientSecretDomainService.existsClientSecret(tenantId)) {
log.info("Eco market client already registered, tenantId={}", tenantId);
return true;
}
ecoMarketSecretRpcService.registerClient(tenantId, name, description);
log.info("Eco market client registered via schedule task, tenantId={}", tenantId);
return true;
} catch (Exception e) {
log.warn("Eco market client register failed, will retry, tenantId={}, taskId={}",
tenantId, scheduleTask.getTaskId(), e);
return false;
} finally {
RequestContext.remove();
}
}
}

View File

@@ -16,20 +16,17 @@ import org.springframework.stereotype.Service;
@Service
public class EcoMarketRpcService implements IEcoMarketRpcService {
@Resource
private IEcoMarketClientSecretDomainService ecoMarketClientSecretDomainService;
@LogRecordPrint(content = "查询客户端密钥")
@Override
public ClientSecretResponse queryClientSecret(ClientSecretRequest request) {
var tenantId = request.getTenantId();
var clientSecret = ecoMarketClientSecretDomainService.queryByTenantId(tenantId);
if (clientSecret == null) {
return null;
}
return clientSecret.toClientSecretResponse();
}

View File

@@ -1,5 +1,6 @@
package com.xspaceagi.eco.market.spec.api;
import com.xspaceagi.eco.market.domain.service.IEcoMarketClientSecretDomainService;
import com.xspaceagi.eco.market.domain.specification.EcoMarkerSecretWrapper;
import com.xspaceagi.eco.market.sdk.model.ClientSecretDTO;
import com.xspaceagi.eco.market.sdk.service.IEcoMarketSecretRpcService;
@@ -7,6 +8,7 @@ import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
import com.xspaceagi.system.spec.exception.EcoMarketException;
import com.xspaceagi.system.domain.log.LogRecordPrint;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import java.util.Objects;
@@ -17,6 +19,8 @@ public class EcoMarketSecretRpcService implements IEcoMarketSecretRpcService {
@Resource
private EcoMarkerSecretWrapper ecoMarkerSecretWrapper;
@Resource
private IEcoMarketClientSecretDomainService ecoMarketClientSecretDomainService;
@LogRecordPrint(content = "注册生态市场的密钥")
@Override
@@ -28,4 +32,11 @@ public class EcoMarketSecretRpcService implements IEcoMarketSecretRpcService {
return ecoMarkerSecretWrapper.registerClientSecret(tenantId, name, description);
}
@Override
public ClientSecretDTO getByTenantId(Long tenantId) {
if (Objects.isNull(tenantId)) {
throw EcoMarketException.build(BizExceptionCodeEnum.fieldRequiredButEmpty, "租户ID");
}
return ecoMarketClientSecretDomainService.getByTenantId(tenantId);
}
}

View File

@@ -10,6 +10,7 @@ import com.xspaceagi.eco.market.domain.model.EcoMarketClientConfigModel;
import com.xspaceagi.eco.market.domain.model.EcoMarketClientPublishConfigModel;
import com.xspaceagi.eco.market.spec.app.service.IEcoMarketClientConfigApplicationService;
import com.xspaceagi.eco.market.spec.app.service.IEcoMarketClientPublishConfigApplicationService;
import com.xspaceagi.eco.market.spec.enums.EcoMarketDataTypeEnum;
import com.xspaceagi.eco.market.spec.enums.EcoMarketOwnedFlagEnum;
import com.xspaceagi.system.spec.common.RequestContext;
import com.xspaceagi.system.spec.common.UserContext;
@@ -113,9 +114,15 @@ public class EcoMarketPullMessage {
.collect(Collectors.toList());
// 如果本地没有配置,则主动启用..不管本地配置是否实际启用,只要有记录了,就不在操作自动启用,需要用户自己去手动操作
// 租户初始化时不自动启用 MCP 配置
var needEnableUids = autoUseUids.stream()
.filter(uid -> !localUids.contains(uid))
.filter(uid -> !myShareUids.contains(uid))
.filter(uid -> autoConfigList.stream()
.filter(config -> config.getUid().equals(uid))
.findFirst()
.map(config -> !EcoMarketDataTypeEnum.MCP.getCode().equals(config.getDataType()))
.orElse(true))
.collect(Collectors.toList());
log.info("Eco-market tenant auto-enable, need-enable list: {}", JSON.toJSONString(needEnableUids));

View File

@@ -0,0 +1,33 @@
package com.xspaceagi.eco.market.spec.app.service;
import com.xspaceagi.eco.market.domain.model.EcoMarketImportRecordModel;
import java.util.List;
public interface IEcoMarketImportApplicationService {
/**
* 新增导入记录
*/
Long addImportRecord(EcoMarketImportRecordModel model);
/**
* 判断是否已导入唯一条件space_id + target_type + eco_target_id
*/
EcoMarketImportRecordModel existsImportRecord(Long spaceId, String targetType, String ecoTargetId);
/**
* 删除导入记录
*/
void deleteImportRecord(Long id);
/**
* 查询用户对某个对象已导入的空间列表
*/
List<EcoMarketImportRecordModel> listImportRecords(Long userId, String targetType, Long targetId);
/**
* 查询用户对某个生态对象已导入的空间列表
*/
List<EcoMarketImportRecordModel> listImportRecordsByEcoTargetId(Long userId, String targetType, String ecoTargetId);
}

View File

@@ -0,0 +1,41 @@
package com.xspaceagi.eco.market.spec.app.service.impl;
import com.xspaceagi.eco.market.domain.model.EcoMarketImportRecordModel;
import com.xspaceagi.eco.market.domain.service.IEcoMarketImportRecordDomainService;
import com.xspaceagi.eco.market.spec.app.service.IEcoMarketImportApplicationService;
import jakarta.annotation.Resource;
import java.util.List;
import org.springframework.stereotype.Service;
@Service
public class EcoMarketImportApplicationService implements IEcoMarketImportApplicationService {
@Resource
private IEcoMarketImportRecordDomainService ecoMarketImportRecordDomainService;
@Override
public Long addImportRecord(EcoMarketImportRecordModel model) {
return ecoMarketImportRecordDomainService.addImportRecord(model);
}
@Override
public EcoMarketImportRecordModel existsImportRecord(Long spaceId, String targetType, String ecoTargetId) {
return ecoMarketImportRecordDomainService.existsImportRecord(spaceId, targetType, ecoTargetId);
}
@Override
public void deleteImportRecord(Long id) {
ecoMarketImportRecordDomainService.deleteImportRecord(id);
}
@Override
public List<EcoMarketImportRecordModel> listImportRecords(Long userId, String targetType, Long targetId) {
return ecoMarketImportRecordDomainService.listImportRecords(userId, targetType, targetId);
}
@Override
public List<EcoMarketImportRecordModel> listImportRecordsByEcoTargetId(Long userId, String targetType, String ecoTargetId) {
return ecoMarketImportRecordDomainService.listImportRecordsByEcoTargetId(userId, targetType, ecoTargetId);
}
}

View File

@@ -0,0 +1,540 @@
package com.xspaceagi.eco.market.web.controller;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.xspaceagi.agent.core.adapter.application.*;
import com.xspaceagi.agent.core.adapter.dto.*;
import com.xspaceagi.agent.core.adapter.dto.config.ModelConfigDto;
import com.xspaceagi.agent.core.adapter.dto.config.plugin.PluginDto;
import com.xspaceagi.agent.core.adapter.repository.entity.ModelConfig;
import com.xspaceagi.agent.core.adapter.repository.entity.Published;
import com.xspaceagi.agent.core.sdk.ISkillRpcService;
import com.xspaceagi.agent.core.sdk.dto.ReqResult;
import com.xspaceagi.agent.core.spec.enums.UsageScenarioEnum;
import com.xspaceagi.agent.core.spec.utils.FileTypeUtils;
import com.xspaceagi.agent.core.spec.utils.UrlFile;
import com.xspaceagi.eco.market.domain.config.EcoMarketProperties;
import com.xspaceagi.eco.market.domain.model.EcoMarketImportRecordModel;
import com.xspaceagi.eco.market.domain.service.IEcoMarketClientSecretDomainService;
import com.xspaceagi.eco.market.sdk.model.ClientSecretDTO;
import com.xspaceagi.eco.market.spec.app.service.IEcoMarketImportApplicationService;
import com.xspaceagi.file.application.service.FileManagementService;
import com.xspaceagi.system.application.dto.SpaceDto;
import com.xspaceagi.system.application.dto.TenantConfigDto;
import com.xspaceagi.system.application.dto.UserDto;
import com.xspaceagi.system.application.service.AuthService;
import com.xspaceagi.system.application.service.SpaceApplicationService;
import com.xspaceagi.system.application.service.UserApplicationService;
import com.xspaceagi.system.infra.dao.entity.Space;
import com.xspaceagi.system.infra.dao.entity.SpaceUser;
import com.xspaceagi.system.infra.dao.entity.User;
import com.xspaceagi.system.spec.common.RequestContext;
import com.xspaceagi.system.spec.common.UserContext;
import com.xspaceagi.system.spec.exception.BizException;
import com.xspaceagi.system.spec.file.InMemoryMultipartFile;
import com.xspaceagi.system.spec.utils.HttpClient;
import com.xspaceagi.system.spec.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@Slf4j
@Tag(name = "女娲生态平台相关通信接口")
@RequestMapping("/api/eco")
@RestController
public class EcoClientApiController {
@Resource
private IEcoMarketClientSecretDomainService ecoMarketClientSecretDomainService;
@Resource
private AuthService authService;
@Resource
private EcoMarketProperties ecoMarketProperties;
@Resource
private UserApplicationService userApplicationService;
@Resource
private SpaceApplicationService spaceApplicationService;
@Resource
private IEcoMarketImportApplicationService iEcoMarketImportApplicationService;
@Resource
private ResourceGroupApplicationService resourceGroupApplicationService;
@Resource
private ModelApplicationService modelApplicationService;
@Resource
private PluginApplicationService pluginApplicationService;
@Resource
private ISkillRpcService iSkillRpcService;
@Resource
private SkillApplicationService skillApplicationService;
@Resource
private PublishApplicationService publishApplicationService;
@Resource
private FileManagementService fileManagementService;
@Resource
private HttpClient httpClient;
@Value("app.version")
private String version;
private final ObjectMapper objectMapper = new ObjectMapper();
public UserDto getRequestUser(HttpServletRequest request) {
String authorization = request.getHeader("Authorization");
if (authorization == null) {
authorization = request.getParameter("token");
if (authorization == null) {
throw new BizException("Authorization information not found");
}
}
String token = authorization.replace("Bearer ", "").trim();
ClientSecretDTO clientSecretDTO = ecoMarketClientSecretDomainService.getByTenantId(RequestContext.get().getTenantId());
Claims claims = JwtUtils.parseJwt(token, clientSecretDTO.getClientSecret());
if (claims.getExpiration().getTime() < System.currentTimeMillis()) {
throw new BizException("Token expired");
}
UserDto userDto = userApplicationService.queryById(Long.parseLong(claims.getId()));
if (userDto == null) {
throw new BizException("User not found");
}
RequestContext.get().setUser(userDto);
RequestContext.get().setUserId(userDto.getId());
RequestContext.get().setLangMap(userDto.getLangMap());
return (UserDto) RequestContext.get().getUser();
}
//版本获取
@Operation(summary = "获取版本信息", description = "JSONP接口需传callback参数")
@ApiResponse(responseCode = "200", description = "成功",
content = @Content(mediaType = "application/javascript",
schema = @Schema(implementation = VersionResponse.class)))
@GetMapping("/client/version")
public void getVersion(HttpServletRequest request, HttpServletResponse response) throws IOException {
String callback = request.getParameter("callback");
ReqResult<String> result;
try {
result = ReqResult.success(version);
} catch (Exception e) {
result = ReqResult.create("4030", e.getMessage(), null);
}
response.setContentType("application/javascript;charset=UTF-8");
String json = objectMapper.writeValueAsString(result);
response.getWriter().write(callback + "(" + json + ");");
}
@Operation(summary = "获取用户空间列表", description = "JSONP接口需传callback参数")
@ApiResponse(responseCode = "200", description = "成功",
content = @Content(mediaType = "application/javascript",
schema = @Schema(implementation = SpaceListResponse.class)))
@GetMapping("/client/user/space/list")
public void getUserSpaceList(HttpServletRequest request, HttpServletResponse response) throws IOException {
String callback = request.getParameter("callback");
ReqResult<List<SpaceDto>> result;
try {
UserDto userDto = getRequestUser(request);
List<SpaceDto> spaces = spaceApplicationService.queryListByUserId(userDto.getId());
result = ReqResult.success(spaces);
} catch (Exception e) {
result = ReqResult.create("4030", e.getMessage(), null);
}
response.setContentType("application/javascript;charset=UTF-8");
String json = objectMapper.writeValueAsString(result);
response.getWriter().write(callback + "(" + json + ");");
}
@Operation(summary = "根据生态对象查询空间列表(含导入状态)", description = "JSONP接口需传callback参数")
@ApiResponse(responseCode = "200", description = "成功",
content = @Content(mediaType = "application/javascript",
schema = @Schema(implementation = SpaceImportStatusListResponse.class)))
@GetMapping("/client/space/import/status")
public void getSpaceImportStatus(HttpServletRequest request, HttpServletResponse response,
@RequestParam String targetType,
@RequestParam String ecoTargetId) throws IOException {
String callback = request.getParameter("callback");
ReqResult<List<SpaceImportStatus>> result;
try {
UserDto userDto = getRequestUser(request);
List<SpaceDto> spaces = spaceApplicationService.queryListByUserId(userDto.getId());
targetType = targetType.equals("Tool") ? "Plugin" : targetType;
// 查询该用户针对此生态对象已导入的空间记录
var importRecords = iEcoMarketImportApplicationService.listImportRecordsByEcoTargetId(
userDto.getId(), targetType, ecoTargetId);
Set<Long> importedSpaceIds = importRecords.stream()
.map(EcoMarketImportRecordModel::getSpaceId)
.collect(Collectors.toSet());
if ((targetType.equals("Plugin") || targetType.equals("Skill")) && !importRecords.isEmpty()) {
PublishedDto publishedDto = publishApplicationService.queryPublished(Published.TargetType.Plugin, importRecords.get(0).getTargetId());
if (publishedDto != null && publishedDto.getPublishedSpaceIds() != null) {
importedSpaceIds.addAll(publishedDto.getPublishedSpaceIds());
}
}
List<SpaceImportStatus> statusList = new ArrayList<>();
for (SpaceDto space : spaces) {
// 过滤:普通用户且空间未开启开发功能时跳过
if (space.getCurrentUserRole() == SpaceUser.Role.User
&& (space.getAllowDevelop() == null || space.getAllowDevelop() == 0)) {
continue;
}
SpaceImportStatus status = SpaceImportStatus.of(space, importedSpaceIds.contains(space.getId()));
statusList.add(status);
}
result = ReqResult.success(statusList);
} catch (Exception e) {
result = ReqResult.create("4030", e.getMessage(), null);
}
response.setContentType("application/javascript;charset=UTF-8");
String json = objectMapper.writeValueAsString(result);
response.getWriter().write(callback + "(" + json + ");");
}
@Operation(summary = "导入配置", description = "JSONP接口从生态市场导入配置到指定空间")
@ApiResponse(responseCode = "200", description = "成功",
content = @Content(mediaType = "application/javascript",
schema = @Schema(implementation = ImportResponse.class)))
@GetMapping("/client/import/config")
public void importConfig(HttpServletRequest request, HttpServletResponse response,
@RequestParam String importDataKey) throws IOException {
String callback = request.getParameter("callback");
ReqResult<Map<String, Object>> result;
try {
TenantConfigDto tenantConfigDto = (TenantConfigDto) RequestContext.get().getTenantConfig();
tenantConfigDto.setSiteUrl(tenantConfigDto.getSiteConfigUrl());
UserDto userDto = getRequestUser(request);
RequestContext.get().setUserContext(UserContext.builder().userId(userDto.getId()).tenantId(userDto.getTenantId()).userName(userDto.getUserName()).build());
String content = httpClient.get(ecoMarketProperties.getWeb().getBaseUrl() + "/api/eco/server/getImportData?dataKey=" + importDataKey);
JSONObject importDataResponse = JSONObject.parseObject(content);
if (!ReqResult.SUCCESS.equals(importDataResponse.getString("code"))) {
result = ReqResult.create(importDataResponse.getString("code"), importDataResponse.getString("message"), null);
} else {
JSONObject importData = importDataResponse.getJSONObject("data");
String targetType = importData.getString("targetType");
String ecoTargetId = importData.getString("targetId");
List<Long> spaceIds = importData.getList("spaceIds", Long.class);
String targetData = importData.getString("targetData");
Assert.notEmpty(spaceIds, "spaceIds不能为空");
Assert.notNull(targetData, "targetData不能为空");
Assert.notNull(targetType, "targetType不能为空");
Assert.notNull(ecoTargetId, "targetId不能为空");
targetType = targetType.equals("Tool") ? "Plugin" : targetType;
List<SpaceDto> userSpaces = spaceApplicationService.queryListByUserId(userDto.getId());
if (targetType.equals("Model")) {
for (Long spaceId : spaceIds) {
if (spaceId != -1L) {
SpaceDto spaceDto = userSpaces.stream()
.filter(space -> spaceIds.contains(space.getId()))
.findFirst()
.orElse(null);
if (spaceDto == null) {
continue;
}
}
if (spaceId == -1L && userDto.getRole() != User.Role.Admin) {
continue;
}
ModelConfigDto modelConfigDto = JSON.parseObject(targetData, ModelConfigDto.class);
modelConfigDto.setSpaceId(spaceId);
modelConfigDto.setScope(spaceId == -1L ? ModelConfig.ModelScopeEnum.Tenant : ModelConfig.ModelScopeEnum.Space);
EcoMarketImportRecordModel imported = iEcoMarketImportApplicationService.existsImportRecord(spaceId, targetType, ecoTargetId);
if (imported != null) {
if (modelApplicationService.queryModelConfigById(imported.getTargetId()) != null) {
modelConfigDto.setId(imported.getTargetId());
} else {
iEcoMarketImportApplicationService.deleteImportRecord(imported.getId());
imported = null;
}
}
modelApplicationService.addOrUpdate(modelConfigDto);
if (imported == null) {
// 记录导入
EcoMarketImportRecordModel record = EcoMarketImportRecordModel.builder()
.tenantId(RequestContext.get().getTenantId())
.userId(userDto.getId())
.spaceId(spaceId)
.targetType(targetType)
.targetId(modelConfigDto.getId()) // 真实导入逻辑完成后会填充此字段
.ecoTargetId(ecoTargetId)
.build();
iEcoMarketImportApplicationService.addImportRecord(record);
}
}
}
SpaceDto spaceDto = userSpaces.stream().filter(space -> space.getType() == Space.Type.Personal).findFirst().orElse(null);
if (spaceDto == null) {
throw new BizException("The user lacks personal space.");
}
if (targetType.equals("Plugin")) {
JSONArray objects = JSON.parseArray(targetData);
String groupInfo = importData.getString("groupInfo");
Long groupId = null;
if (groupInfo != null) {
ResourceGroupDto ecoResourceGroupDto = JSON.parseObject(importData.getString("groupInfo"), ResourceGroupDto.class);
ResourceGroupDto resourceGroupDto = resourceGroupApplicationService.queryByName(spaceDto.getId(), "Plugin", ecoResourceGroupDto.getName());
if (resourceGroupDto == null) {
resourceGroupDto = new ResourceGroupDto();
resourceGroupDto.setName(ecoResourceGroupDto.getName());
resourceGroupDto.setDescription(ecoResourceGroupDto.getDescription());
resourceGroupDto.setIcon(fileExchange(ecoResourceGroupDto.getIcon()));
resourceGroupDto.setType("Plugin");
resourceGroupDto.setSpaceId(spaceDto.getId());
groupId = resourceGroupApplicationService.add(resourceGroupDto);
} else {
groupId = resourceGroupDto.getId();
}
}
for (int i = 0; i < objects.size(); i++) {
PluginDto pluginDto = objects.getObject(i, PluginDto.class);
ecoTargetId = pluginDto.getToolId();
EcoMarketImportRecordModel imported = iEcoMarketImportApplicationService.existsImportRecord(spaceDto.getId(), "Plugin", ecoTargetId);
Long pluginId = imported == null ? null : imported.getTargetId();
if (pluginId != null) {
PluginDto pluginDto1 = pluginApplicationService.queryById(pluginId);
if (pluginDto1 == null) {
pluginId = null;
}
iEcoMarketImportApplicationService.deleteImportRecord(imported.getId());
}
if (pluginId == null) {
PluginAddDto pluginAddDto = new PluginAddDto();
pluginAddDto.setCreatorId(userDto.getId());
pluginAddDto.setSpaceId(spaceDto.getId());
pluginAddDto.setName(pluginDto.getName());
pluginAddDto.setDescription(pluginDto.getDescription());
pluginAddDto.setType(pluginDto.getType());
pluginAddDto.setCodeLang(pluginDto.getCodeLang());
pluginAddDto.setIcon(fileExchange(pluginDto.getIcon()));
pluginId = pluginApplicationService.add(pluginAddDto);
}
if (groupId != null) {
resourceGroupApplicationService.addResourceToGroup(groupId, "Plugin", pluginId);
}
PluginUpdateDto<Object> pluginUpdateDto = new PluginUpdateDto<>();
pluginUpdateDto.setId(pluginId);
pluginUpdateDto.setName(pluginDto.getName());
pluginUpdateDto.setConfig(pluginDto.getConfig());
pluginUpdateDto.setDescription(pluginDto.getDescription());
pluginUpdateDto.setIcon(fileExchange(pluginDto.getIcon()));
pluginApplicationService.update(pluginUpdateDto);
pluginDto.setId(pluginId);
pluginDto.setSpaceId(spaceDto.getId());
pluginDto.setCreatorId(userDto.getId());
if (imported == null) {
// 记录导入
EcoMarketImportRecordModel record = EcoMarketImportRecordModel.builder()
.tenantId(RequestContext.get().getTenantId())
.userId(userDto.getId())
.spaceId(spaceDto.getId())
.targetType(targetType)
.targetId(pluginId) // 真实导入逻辑完成后会填充此字段
.ecoTargetId(ecoTargetId)
.build();
iEcoMarketImportApplicationService.addImportRecord(record);
}
PublishApplySubmitDto publishApplySubmitDto = new PublishApplySubmitDto();
publishApplySubmitDto.setCategory(pluginDto.getCategory() == null ? "Other" : pluginDto.getCategory());
publishApplySubmitDto.setTargetId(pluginId);
publishApplySubmitDto.setTargetType(Published.TargetType.Plugin);
publishApplySubmitDto.setRemark("From the Qiming ecosystem platform");
List<PublishApplySubmitDto.PublishItem> items = new ArrayList<>();
publishApplySubmitDto.setItems(items);
for (Long spaceId : spaceIds) {
PublishApplySubmitDto.PublishItem item = new PublishApplySubmitDto.PublishItem();
item.setScope(spaceId == -1L ? Published.PublishScope.Tenant : Published.PublishScope.Space);
item.setSpaceId(spaceId);
item.setAllowCopy(0);
item.setOnlyTemplate(0);
items.add(item);
}
publishApplicationService.publishOrApply(publishApplySubmitDto);
}
}
if (targetType.equals("Skill")) {
SkillConfigDto skillConfigDto = JSON.parseObject(targetData, SkillConfigDto.class);
EcoMarketImportRecordModel imported = iEcoMarketImportApplicationService.existsImportRecord(spaceDto.getId(), targetType, ecoTargetId);
if (imported != null) {
if (skillApplicationService.queryById(imported.getTargetId()) != null) {
skillConfigDto.setId(imported.getTargetId());
} else {
iEcoMarketImportApplicationService.deleteImportRecord(imported.getId());
imported = null;
}
}
log.info("导入技能:{}, targetId {}", skillConfigDto, imported != null ? imported.getTargetId() : null);
Long skillId = iSkillRpcService.importSkill(skillConfigDto.getZipFileUrl(), null, imported != null ? imported.getTargetId() : null, spaceDto.getId(), List.of(UsageScenarioEnum.TaskAgent));
SkillConfigDto updateSkillConfigDto = new SkillConfigDto();
updateSkillConfigDto.setId(skillId);
updateSkillConfigDto.setIcon(fileExchange(skillConfigDto.getIcon()));
updateSkillConfigDto.setDescription(skillConfigDto.getDescription());
updateSkillConfigDto.setName(skillConfigDto.getName());
skillApplicationService.update(updateSkillConfigDto, false);
if (imported == null) {
// 记录导入
EcoMarketImportRecordModel record = EcoMarketImportRecordModel.builder()
.tenantId(RequestContext.get().getTenantId())
.userId(userDto.getId())
.spaceId(spaceDto.getId())
.targetType(targetType)
.targetId(skillId) // 真实导入逻辑完成后会填充此字段
.ecoTargetId(ecoTargetId)
.build();
iEcoMarketImportApplicationService.addImportRecord(record);
}
PublishApplySubmitDto publishApplySubmitDto = new PublishApplySubmitDto();
publishApplySubmitDto.setCategory(skillConfigDto.getCategory() == null ? "Other" : skillConfigDto.getCategory());
publishApplySubmitDto.setTargetId(skillId);
publishApplySubmitDto.setTargetType(Published.TargetType.Skill);
publishApplySubmitDto.setRemark("From the Qiming ecosystem platform");
List<PublishApplySubmitDto.PublishItem> items = new ArrayList<>();
publishApplySubmitDto.setItems(items);
for (Long spaceId : spaceIds) {
PublishApplySubmitDto.PublishItem item = new PublishApplySubmitDto.PublishItem();
item.setScope(spaceId == -1L ? Published.PublishScope.Tenant : Published.PublishScope.Space);
item.setSpaceId(spaceId);
item.setAllowCopy(0);
item.setOnlyTemplate(0);
items.add(item);
}
publishApplicationService.publishOrApply(publishApplySubmitDto);
}
result = ReqResult.success();
}
} catch (Exception e) {
log.error("导入失败", e);
result = ReqResult.create("4030", e.getMessage(), null);
}
response.setContentType("application/javascript;charset=UTF-8");
String json = objectMapper.writeValueAsString(result);
response.getWriter().write(callback + "(" + json + ");");
}
@Operation(summary = "跳转到生态市场", description = "跳转生态市场")
@GetMapping("/redirect")
public void redirect(HttpServletResponse response) {
ClientSecretDTO clientSecretDTO = ecoMarketClientSecretDomainService.getByTenantId(RequestContext.get().getTenantId());
if (clientSecretDTO == null) {
// 未找到客户端信息 翻译成英文
throw new RuntimeException("未找到客户端信息");
}
UserDto user = (UserDto) RequestContext.get().getUser();
String token = authService.newEcoToken(clientSecretDTO.getClientId(), clientSecretDTO.getClientSecret(), user);
try {
log.info("跳转到生态市场:{}", token);
response.sendRedirect(ecoMarketProperties.getWeb().getBaseUrl() + "/api/eco/server?token=" + token);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Schema(description = "用户空间列表响应")
private static class SpaceListResponse extends ReqResult<List<SpaceDto>> {
}
@Schema(description = "版本信息响应")
private static class VersionResponse extends ReqResult<String> {
}
@Schema(description = "空间导入状态")
private static class SpaceImportStatus {
@Schema(description = "空间ID")
private Long spaceId;
@Schema(description = "空间名称")
private String spaceName;
@Schema(description = "空间类型")
private String type;
@Schema(description = "当前用户角色")
private String userRole;
@Schema(description = "是否已导入")
private Boolean imported;
static SpaceImportStatus of(SpaceDto space, boolean imported) {
SpaceImportStatus s = new SpaceImportStatus();
s.spaceId = space.getId();
s.spaceName = space.getName();
s.type = space.getType() != null ? space.getType().name() : null;
s.userRole = space.getCurrentUserRole() != null ? space.getCurrentUserRole().name() : null;
s.imported = imported;
return s;
}
public Long getSpaceId() {
return spaceId;
}
public String getSpaceName() {
return spaceName;
}
public String getType() {
return type;
}
public String getUserRole() {
return userRole;
}
public Boolean getImported() {
return imported;
}
}
@Schema(description = "空间导入状态列表响应")
private static class SpaceImportStatusListResponse extends ReqResult<List<SpaceImportStatus>> {
}
@Schema(description = "导入配置响应")
private static class ImportResponse extends ReqResult<Map<String, Object>> {
}
private String fileExchange(String url) {
try {
byte[] bytes = UrlFile.downLoad(url);
String contentType = FileTypeUtils.getContentTypeByFileName(url).toString();
InMemoryMultipartFile multipartFile = new InMemoryMultipartFile("file", "", contentType, bytes);
Long tenantId = RequestContext.get() != null ? RequestContext.get().getTenantId() : null;
Long userId = RequestContext.get() != null ? RequestContext.get().getUserId() : null;
return fileManagementService.uploadFile(multipartFile, tenantId, userId, "skill_dev", null, null, true).getFileUrl();
} catch (Exception e) {
log.warn("fileExchange error {}", url, e);
return null;
}
}
}

View File

@@ -1,32 +1,14 @@
package com.xspaceagi.eco.market.domain.client;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.springframework.context.annotation.DependsOn;
import org.springframework.stereotype.Service;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.TypeReference;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.xspaceagi.agent.core.adapter.application.ModelApplicationService;
import com.xspaceagi.agent.core.adapter.dto.config.ModelConfigDto;
import com.xspaceagi.agent.core.adapter.repository.entity.ModelConfig;
import com.xspaceagi.agent.core.spec.enums.ModelApiProtocolEnum;
import com.xspaceagi.agent.core.spec.enums.ModelFunctionCallEnum;
import com.xspaceagi.agent.core.spec.enums.ModelTypeEnum;
import com.xspaceagi.agent.core.spec.enums.*;
import com.xspaceagi.eco.market.domain.config.EcoMarketConfig;
import com.xspaceagi.eco.market.domain.dto.req.ClientRegisterReqDTO;
import com.xspaceagi.eco.market.domain.dto.req.ClientValidateReqDTO;
import com.xspaceagi.eco.market.domain.dto.req.ServerConfigBatchDetailReqDTO;
import com.xspaceagi.eco.market.domain.dto.req.ServerConfigDetailReqDTO;
import com.xspaceagi.eco.market.domain.dto.req.ServerConfigOfflineReqDTO;
import com.xspaceagi.eco.market.domain.dto.req.ServerConfigQueryRequest;
import com.xspaceagi.eco.market.domain.dto.req.ServerConfigSaveReqDTO;
import com.xspaceagi.eco.market.domain.dto.req.ServerConfigStatusReqDTO;
import com.xspaceagi.eco.market.domain.dto.req.ServerConfigUnpublishReqDTO;
import com.xspaceagi.eco.market.domain.dto.req.*;
import com.xspaceagi.eco.market.domain.dto.resp.ServerConfigDetailRespDTO;
import com.xspaceagi.eco.market.domain.dto.resp.ServerConfigListRespDTO;
import com.xspaceagi.eco.market.sdk.model.ClientSecretDTO;
@@ -39,16 +21,17 @@ import com.xspaceagi.system.spec.page.PageQueryVo;
import com.xspaceagi.system.spec.page.SuperPage;
import com.xspaceagi.system.spec.tenant.thread.TenantFunctions;
import com.xspaceagi.system.spec.utils.RedisUtil;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.*;
import org.springframework.context.annotation.DependsOn;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* 生态市场服务器API调用服务
@@ -78,8 +61,6 @@ public class EcoMarketServerApiService {
.writeTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build();
log.info("Init eco-market server API client (Fastjson2 JSON)");
}
/**
@@ -87,6 +68,10 @@ public class EcoMarketServerApiService {
*/
@PostConstruct
public void init() {
if (!ecoMarketConfig.isServerEnabled()) {
log.info("Skip eco-market remote API client initialization");
return;
}
try {
this.serverBaseUrl = ecoMarketConfig.getServerBaseUrl();
log.info("Init eco-market server API client, URL: {}", this.serverBaseUrl);
@@ -102,6 +87,9 @@ public class EcoMarketServerApiService {
* @return 完整的API URL
*/
private String buildApiUrl(String apiPath) {
if (!ecoMarketConfig.isServerEnabled()) {
throw EcoMarketException.build(BizExceptionCodeEnum.configNotFound, "生态市场远程服务已关闭");
}
// 如果初始化时未能获取URL这里再次尝试
if (serverBaseUrl == null) {
try {
@@ -170,11 +158,9 @@ public class EcoMarketServerApiService {
if (result != null && result.isSuccess()) {
// 保存默认会话模型配置
modelApplicationService
.addOrUpdate(buildModelConfig(tenantId, ModelTypeEnum.Chat, 1L));
modelApplicationService.addOrUpdate(buildModelConfig(tenantId, ModelTypeEnum.Chat, 1L));
// 保存默认嵌入模型配置
modelApplicationService
.addOrUpdate(buildModelConfig(tenantId, ModelTypeEnum.Embeddings, 2L));
modelApplicationService.addOrUpdate(buildModelConfig(tenantId, ModelTypeEnum.Embeddings, 2L));
redisUtil.leftPush("tenant_created", tenantId);
// todo
@@ -194,6 +180,7 @@ public class EcoMarketServerApiService {
private ModelConfigDto buildModelConfig(Long tenantId, ModelTypeEnum type, Long id) {
ModelConfigDto modelConfigDto = new ModelConfigDto();
modelConfigDto.setPid("nuwax");
ModelConfigDto modelConfigDto1 = TenantFunctions
.callWithIgnoreCheck(() -> modelApplicationService.queryModelConfigById(id));
if (modelConfigDto1 == null) {
@@ -342,7 +329,7 @@ public class EcoMarketServerApiService {
* @return 配置详情列表,失败时抛出异常
*/
public List<ServerConfigDetailRespDTO> getBatchServerConfigDetail(List<String> uids, String clientId,
String clientSecret) {
String clientSecret) {
try {
String url = buildApiUrl(EcoMarketApiConstant.SERVER_API_BASE + "/config/batchDetail");
@@ -398,7 +385,7 @@ public class EcoMarketServerApiService {
* @return 配置详情列表,失败时抛出异常
*/
public List<ServerConfigDetailRespDTO> getBatchServerApproveDetail(List<String> uids, String clientId,
String clientSecret) {
String clientSecret) {
try {
String url = buildApiUrl(EcoMarketApiConstant.SERVER_API_BASE + "/config/batchApproveDetail");
@@ -782,7 +769,6 @@ public class EcoMarketServerApiService {
* 查询自动租户启用的配置信息
* 服务器端接口对应 EcoMarketServerConfigController 中的 autoUse 方法
*
* @param reqDTO 查询请求DTO
* @return 分页查询结果
*/
public List<ServerConfigDetailRespDTO> queryAutoUseConfigList() {
@@ -836,23 +822,23 @@ public class EcoMarketServerApiService {
/**
* 上传页面zip包到服务器端
*
* @param fileBytes 文件字节数组
* @param fileName 文件名
* @param contentType 文件类型
* @param clientId 客户端ID
* @param clientSecret 客户端密钥
*
* @param fileBytes 文件字节数组
* @param fileName 文件名
* @param contentType 文件类型
* @param clientId 客户端ID
* @param clientSecret 客户端密钥
* @return 上传成功后的文件URL失败时抛出异常
*/
public String uploadPageZip(byte[] fileBytes, String fileName, String contentType,
String clientId, String clientSecret) {
String clientId, String clientSecret) {
try {
String url = buildApiUrl(EcoMarketApiConstant.ServerConfig.UPLOAD_PAGE_ZIP);
log.info("Upload page zip to server: fileName={}, size={}, contentType={}", fileName, fileBytes.length, contentType);
MediaType fileMediaType = MediaType.parse(contentType != null ? contentType : "application/octet-stream");
RequestBody fileBody = RequestBody.create(fileBytes, fileMediaType);
// 构建multipart/form-data请求体
MultipartBody.Builder multipartBuilder = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
@@ -860,7 +846,7 @@ public class EcoMarketServerApiService {
.addFormDataPart("fileName", fileName)
.addFormDataPart("clientId", clientId)
.addFormDataPart("clientSecret", clientSecret);
RequestBody requestBody = multipartBuilder.build();
Request request = new Request.Builder()
@@ -879,7 +865,7 @@ public class EcoMarketServerApiService {
throw EcoMarketException.build(BizExceptionCodeEnum.ecoMarketConfigResultBodyEmpty);
}
String responseBody = responseBodyObj.string();
ReqResult<String> result = JSON.parseObject(responseBody,
new TypeReference<ReqResult<String>>() {
});
@@ -900,4 +886,59 @@ public class EcoMarketServerApiService {
}
}
}
/**
* 根据importDataKey从生态市场服务器获取导入数据
*
* @param importDataKey 导入数据key
* @param clientId 客户端ID
* @param clientSecret 客户端密钥
* @return 导入数据Map格式
*/
public Map<String, Object> getImportData(String importDataKey, String clientId, String clientSecret) {
try {
String url = buildApiUrl(EcoMarketApiConstant.ImportData.GET_IMPORT_DATA);
Map<String, String> reqMap = new java.util.HashMap<>();
reqMap.put("importDataKey", importDataKey);
reqMap.put("clientId", clientId);
reqMap.put("clientSecret", clientSecret);
String jsonBody = JSON.toJSONString(reqMap);
RequestBody body = RequestBody.create(jsonBody, JSON_MEDIA_TYPE);
Request request = new Request.Builder()
.url(url)
.post(body)
.build();
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
log.error("Get import data request failed, status: {}", response.code());
throw EcoMarketException.build(BizExceptionCodeEnum.ecoMarketGetConfigFailed,
"请求失败,状态码: " + response.code());
}
var responseBodyObj = response.body();
if (responseBodyObj == null) {
throw EcoMarketException.build(BizExceptionCodeEnum.ecoMarketConfigResultBodyEmpty);
}
String responseBody = responseBodyObj.string();
ReqResult<Map<String, Object>> result = JSON.parseObject(responseBody,
new TypeReference<ReqResult<Map<String, Object>>>() {
});
if (result != null && result.isSuccess()) {
return result.getData();
} else {
log.error("Get import data failed: {}", result != null ? result.getMessage() : "Unknown error");
throw EcoMarketException.build(BizExceptionCodeEnum.ecoMarketGetConfigFailed,
result != null ? result.getMessage() : "获取导入数据失败");
}
}
} catch (IOException e) {
log.error("Get import data API error", e);
throw EcoMarketException.build(BizExceptionCodeEnum.ecoMarketGetConfigFailed, e.getMessage());
}
}
}

View File

@@ -18,8 +18,12 @@ public class EcoMarketConfig {
public EcoMarketConfig(EcoMarketProperties ecoMarketProperties) {
this.ecoMarketProperties = ecoMarketProperties;
log.info("Eco-market config loaded, server URL: {}",
ecoMarketProperties.getServer().getBaseUrl());
if (isServerEnabled()) {
log.info("Eco-market remote server enabled, server URL: {}",
ecoMarketProperties.getServer().getBaseUrl());
} else {
log.info("Eco-market remote server disabled, using local marketplace data");
}
}
/**
@@ -40,4 +44,13 @@ public class EcoMarketConfig {
public String getServerBaseUrl() {
return ecoMarketProperties.getServer().getBaseUrl();
}
}
/**
* 远程生态市场是否启用
*
* @return true 表示允许访问远程生态市场服务
*/
public boolean isServerEnabled() {
return ecoMarketProperties.getServer().isEnabled();
}
}

View File

@@ -18,14 +18,32 @@ public class EcoMarketProperties {
*/
private ServerConfig server = new ServerConfig();
private WebConfig web = new WebConfig();
/**
* 服务端配置
*/
@Data
public static class ServerConfig {
/**
* 是否启用远程生态市场服务
*/
private boolean enabled = false;
/**
* 远程服务器基础URL
*/
private String baseUrl;
}
}
/**
* 服务端WEB地址配置
*/
@Data
public static class WebConfig {
/**
* 远程服务器基础URL
*/
private String baseUrl;
}
}

View File

@@ -0,0 +1,31 @@
package com.xspaceagi.eco.market.domain.model;
import lombok.*;
import java.time.LocalDateTime;
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class EcoMarketImportRecordModel {
private Long id;
private Long tenantId;
private Long userId;
private Long spaceId;
private String targetType;
private Long targetId;
private String ecoTargetId;
private LocalDateTime created;
private LocalDateTime modified;
}

View File

@@ -0,0 +1,18 @@
package com.xspaceagi.eco.market.domain.repository;
import com.xspaceagi.eco.market.domain.model.EcoMarketImportRecordModel;
import java.util.List;
public interface IEcoMarketImportRecordRepository {
Long add(EcoMarketImportRecordModel model);
EcoMarketImportRecordModel existsBySpaceIdAndTargetTypeAndEcoTargetId(Long spaceId, String targetType, String ecoTargetId);
void deleteById(Long id);
List<EcoMarketImportRecordModel> listByUserIdAndTargetTypeAndTargetId(Long userId, String targetType, Long targetId);
List<EcoMarketImportRecordModel> listByUserIdAndTargetTypeAndEcoTargetId(Long userId, String targetType, String ecoTargetId);
}

View File

@@ -0,0 +1,33 @@
package com.xspaceagi.eco.market.domain.service;
import com.xspaceagi.eco.market.domain.model.EcoMarketImportRecordModel;
import java.util.List;
public interface IEcoMarketImportRecordDomainService {
/**
* 新增导入记录
*/
Long addImportRecord(EcoMarketImportRecordModel model);
/**
* 判断是否已导入唯一条件space_id + target_type + eco_target_id
*/
EcoMarketImportRecordModel existsImportRecord(Long spaceId, String targetType, String ecoTargetId);
/**
* 删除导入记录
*/
void deleteImportRecord(Long id);
/**
* 查询用户对某个对象已导入的空间列表
*/
List<EcoMarketImportRecordModel> listImportRecords(Long userId, String targetType, Long targetId);
/**
* 查询用户对某个生态对象已导入的空间列表
*/
List<EcoMarketImportRecordModel> listImportRecordsByEcoTargetId(Long userId, String targetType, String ecoTargetId);
}

View File

@@ -400,7 +400,7 @@ public class EcoMarketClientPublishConfigDomainService implements IEcoMarketClie
}
// 根据数据类型调用不同的启用接口
Long resultId;
Long resultId = null;
try {
switch (dataTypeEnum) {
@@ -569,8 +569,9 @@ public class EcoMarketClientPublishConfigDomainService implements IEcoMarketClie
}
}
case MCP -> {
// MCP类型启用
// 租户初始化时不再通过生态市场自动部署 MCP
log.info("Skip MCP auto-enable: uid={}", config.getUid());
/*
var configJson = config.getConfigJson();
if (StringUtils.isBlank(configJson)) {
log.error("MCP config content required: uid={}", config.getUid());
@@ -601,7 +602,7 @@ public class EcoMarketClientPublishConfigDomainService implements IEcoMarketClie
mcpDtoFromJson.setCreator(creator);
resultId = ecoMarketAdaptor.deployOfficialMcp(mcpDtoFromJson);
*/
}
default -> {
log.error("Unsupported data type: {}", dataType);

View File

@@ -3,6 +3,7 @@ package com.xspaceagi.eco.market.domain.service.impl;
import com.baomidou.dynamic.datasource.annotation.DSTransactional;
import com.google.common.eventbus.EventBus;
import com.xspaceagi.eco.market.domain.client.EcoMarketServerApiService;
import com.xspaceagi.eco.market.domain.config.EcoMarketConfig;
import com.xspaceagi.eco.market.domain.model.EcoMarketClientSecretModel;
import com.xspaceagi.eco.market.domain.repository.IEcoMarketClientSecretRepository;
import com.xspaceagi.eco.market.domain.service.IEcoMarketClientSecretDomainService;
@@ -22,6 +23,7 @@ import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
@Slf4j
@Service
@@ -33,6 +35,8 @@ public class EcoMarketClientSecretDomainService implements IEcoMarketClientSecre
@Resource
private EcoMarketServerApiService ecoMarketServerApiService;
@Resource
private EcoMarketConfig ecoMarketConfig;
@Resource
private EventBus eventBus;
@@ -152,6 +156,26 @@ public class EcoMarketClientSecretDomainService implements IEcoMarketClientSecre
return getByTenantId(tenantId);
}
String clientId = EcoMarketCaffeineCacheUtil.getTenantClientIdWithCache(tenantId);
if (!ecoMarketConfig.isServerEnabled()) {
log.info("Eco market remote server disabled, creating local client secret: tenantId={}, name={}", tenantId, name);
ClientSecretDTO localClientSecretDTO = ClientSecretDTO.builder()
.tenantId(tenantId)
.name(name)
.description(description)
.clientId(clientId)
.clientSecret(UUID.randomUUID().toString().replace("-", ""))
.build();
UserContext userContext = UserContext.builder()
.tenantId(tenantId)
.userId(0L)
.userName("system")
.build();
saveFromClientSecretDTO(localClientSecretDTO, userContext);
return localClientSecretDTO;
}
// 不存在调用服务器端API注册
log.info("Client secret missing, registering on server: tenantId={}, name={}", tenantId, name);
@@ -159,7 +183,6 @@ public class EcoMarketClientSecretDomainService implements IEcoMarketClientSecre
// 主动生成租户的clientId, 并保存到内存中, 用于后续的注册.注:
// 服务器不一定认客户端给的clientId,正常情况会使用客户端给的clientId, 如果客户端给的clientId不合法,
// 则使用生成的clientId返回
String clientId = EcoMarketCaffeineCacheUtil.getTenantClientIdWithCache(tenantId);
ClientSecretDTO clientSecretDTO = ecoMarketServerApiService.registerClient(name, description, tenantId,
clientId);

View File

@@ -0,0 +1,43 @@
package com.xspaceagi.eco.market.domain.service.impl;
import com.xspaceagi.eco.market.domain.model.EcoMarketImportRecordModel;
import com.xspaceagi.eco.market.domain.repository.IEcoMarketImportRecordRepository;
import com.xspaceagi.eco.market.domain.service.IEcoMarketImportRecordDomainService;
import jakarta.annotation.Resource;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class EcoMarketImportRecordDomainService implements IEcoMarketImportRecordDomainService {
@Resource
private IEcoMarketImportRecordRepository ecoMarketImportRecordRepository;
@Override
public Long addImportRecord(EcoMarketImportRecordModel model) {
return ecoMarketImportRecordRepository.add(model);
}
@Override
public EcoMarketImportRecordModel existsImportRecord(Long spaceId, String targetType, String ecoTargetId) {
return ecoMarketImportRecordRepository.existsBySpaceIdAndTargetTypeAndEcoTargetId(spaceId, targetType, ecoTargetId);
}
@Override
public void deleteImportRecord(Long id) {
ecoMarketImportRecordRepository.deleteById(id);
}
@Override
public List<EcoMarketImportRecordModel> listImportRecords(Long userId, String targetType, Long targetId) {
return ecoMarketImportRecordRepository.listByUserIdAndTargetTypeAndTargetId(userId, targetType, targetId);
}
@Override
public List<EcoMarketImportRecordModel> listImportRecordsByEcoTargetId(Long userId, String targetType, String ecoTargetId) {
return ecoMarketImportRecordRepository.listByUserIdAndTargetTypeAndEcoTargetId(userId, targetType, ecoTargetId);
}
}

View File

@@ -0,0 +1,38 @@
package com.xspaceagi.eco.market.spec.infra.dao.entity;
import java.time.LocalDateTime;
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 lombok.Data;
/**
* 生态市场导入记录
*/
@Data
@TableName("eco_market_import_record")
public class EcoMarketImportRecord {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@TableField(value = "_tenant_id")
private Long tenantId;
private Long userId;
private Long spaceId;
private String targetType;
private Long targetId;
private String ecoTargetId;
private LocalDateTime created;
private LocalDateTime modified;
}

View File

@@ -0,0 +1,9 @@
package com.xspaceagi.eco.market.spec.infra.dao.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xspaceagi.eco.market.spec.infra.dao.entity.EcoMarketImportRecord;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface EcoMarketImportRecordMapper extends BaseMapper<EcoMarketImportRecord> {
}

View File

@@ -0,0 +1,19 @@
package com.xspaceagi.eco.market.spec.infra.dao.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.xspaceagi.eco.market.spec.infra.dao.entity.EcoMarketImportRecord;
import java.util.List;
public interface EcoMarketImportRecordService extends IService<EcoMarketImportRecord> {
Long addInfo(EcoMarketImportRecord entity);
EcoMarketImportRecord getBySpaceIdAndTargetTypeAndEcoTargetId(Long spaceId, String targetType, String ecoTargetId);
void deleteById(Long id);
List<EcoMarketImportRecord> listByUserIdAndTargetTypeAndTargetId(Long userId, String targetType, Long targetId);
List<EcoMarketImportRecord> listByUserIdAndTargetTypeAndEcoTargetId(Long userId, String targetType, String ecoTargetId);
}

View File

@@ -0,0 +1,62 @@
package com.xspaceagi.eco.market.spec.infra.dao.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xspaceagi.eco.market.spec.infra.dao.entity.EcoMarketImportRecord;
import com.xspaceagi.eco.market.spec.infra.dao.mapper.EcoMarketImportRecordMapper;
import com.xspaceagi.eco.market.spec.infra.dao.service.EcoMarketImportRecordService;
import jakarta.annotation.Resource;
import java.util.List;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@Service
public class EcoMarketImportRecordServiceImpl extends ServiceImpl<EcoMarketImportRecordMapper, EcoMarketImportRecord>
implements EcoMarketImportRecordService {
@Lazy
@Resource
private EcoMarketImportRecordServiceImpl self;
@Override
public Long addInfo(EcoMarketImportRecord entity) {
entity.setId(null);
entity.setCreated(null);
entity.setModified(null);
self.save(entity);
return entity.getId();
}
@Override
public EcoMarketImportRecord getBySpaceIdAndTargetTypeAndEcoTargetId(Long spaceId, String targetType, String ecoTargetId) {
LambdaQueryWrapper<EcoMarketImportRecord> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(EcoMarketImportRecord::getSpaceId, spaceId)
.eq(EcoMarketImportRecord::getTargetType, targetType)
.eq(EcoMarketImportRecord::getEcoTargetId, ecoTargetId);
return getOne(queryWrapper, false);
}
@Override
public void deleteById(Long id) {
self.removeById(id);
}
@Override
public List<EcoMarketImportRecord> listByUserIdAndTargetTypeAndTargetId(Long userId, String targetType, Long targetId) {
LambdaQueryWrapper<EcoMarketImportRecord> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(EcoMarketImportRecord::getUserId, userId)
.eq(EcoMarketImportRecord::getTargetType, targetType)
.eq(EcoMarketImportRecord::getTargetId, targetId);
return list(queryWrapper);
}
@Override
public List<EcoMarketImportRecord> listByUserIdAndTargetTypeAndEcoTargetId(Long userId, String targetType, String ecoTargetId) {
LambdaQueryWrapper<EcoMarketImportRecord> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(EcoMarketImportRecord::getUserId, userId)
.eq(EcoMarketImportRecord::getTargetType, targetType)
.eq(EcoMarketImportRecord::getEcoTargetId, ecoTargetId);
return list(queryWrapper);
}
}

View File

@@ -0,0 +1,69 @@
package com.xspaceagi.eco.market.spec.infra.repository;
import com.baomidou.dynamic.datasource.annotation.DSTransactional;
import com.xspaceagi.eco.market.domain.model.EcoMarketImportRecordModel;
import com.xspaceagi.eco.market.domain.repository.IEcoMarketImportRecordRepository;
import com.xspaceagi.eco.market.spec.infra.dao.entity.EcoMarketImportRecord;
import com.xspaceagi.eco.market.spec.infra.dao.service.EcoMarketImportRecordService;
import com.xspaceagi.eco.market.spec.infra.translator.IEcoMarketImportRecordTranslator;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
@Slf4j
@Repository
public class EcoMarketImportRecordRepository implements IEcoMarketImportRecordRepository {
@Resource
private IEcoMarketImportRecordTranslator ecoMarketImportRecordTranslator;
@Resource
private EcoMarketImportRecordService ecoMarketImportRecordService;
@Override
@DSTransactional(rollbackFor = Exception.class)
public Long add(EcoMarketImportRecordModel model) {
model.setId(null);
var entity = ecoMarketImportRecordTranslator.convertToEntity(model);
return ecoMarketImportRecordService.addInfo(entity);
}
@Override
public EcoMarketImportRecordModel existsBySpaceIdAndTargetTypeAndEcoTargetId(Long spaceId, String targetType, String ecoTargetId) {
var data = ecoMarketImportRecordService.getBySpaceIdAndTargetTypeAndEcoTargetId(spaceId, targetType, ecoTargetId);
if (data == null) {
return null;
}
return ecoMarketImportRecordTranslator.convertToModel(data);
}
@Override
@DSTransactional(rollbackFor = Exception.class)
public void deleteById(Long id) {
var existObj = ecoMarketImportRecordService.getById(id);
if (existObj == null) {
return;
}
ecoMarketImportRecordService.deleteById(id);
}
@Override
public List<EcoMarketImportRecordModel> listByUserIdAndTargetTypeAndTargetId(Long userId, String targetType, Long targetId) {
var dataList = ecoMarketImportRecordService.listByUserIdAndTargetTypeAndTargetId(userId, targetType, targetId);
return dataList.stream()
.map(ecoMarketImportRecordTranslator::convertToModel)
.collect(Collectors.toList());
}
@Override
public List<EcoMarketImportRecordModel> listByUserIdAndTargetTypeAndEcoTargetId(Long userId, String targetType, String ecoTargetId) {
var dataList = ecoMarketImportRecordService.listByUserIdAndTargetTypeAndEcoTargetId(userId, targetType, ecoTargetId);
return dataList.stream()
.map(ecoMarketImportRecordTranslator::convertToModel)
.collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,8 @@
package com.xspaceagi.eco.market.spec.infra.translator;
import com.xspaceagi.eco.market.domain.model.EcoMarketImportRecordModel;
import com.xspaceagi.eco.market.spec.infra.dao.entity.EcoMarketImportRecord;
import com.xspaceagi.system.infra.dao.ICommonTranslator;
public interface IEcoMarketImportRecordTranslator extends ICommonTranslator<EcoMarketImportRecordModel, EcoMarketImportRecord> {
}

View File

@@ -0,0 +1,46 @@
package com.xspaceagi.eco.market.spec.infra.translator.impl;
import com.xspaceagi.eco.market.domain.model.EcoMarketImportRecordModel;
import com.xspaceagi.eco.market.spec.infra.dao.entity.EcoMarketImportRecord;
import com.xspaceagi.eco.market.spec.infra.translator.IEcoMarketImportRecordTranslator;
import org.springframework.stereotype.Component;
@Component
public class EcoMarketImportRecordTranslator implements IEcoMarketImportRecordTranslator {
@Override
public EcoMarketImportRecordModel convertToModel(EcoMarketImportRecord entity) {
if (entity == null) {
return null;
}
return EcoMarketImportRecordModel.builder()
.id(entity.getId())
.tenantId(entity.getTenantId())
.userId(entity.getUserId())
.spaceId(entity.getSpaceId())
.targetType(entity.getTargetType())
.targetId(entity.getTargetId())
.ecoTargetId(entity.getEcoTargetId())
.created(entity.getCreated())
.modified(entity.getModified())
.build();
}
@Override
public EcoMarketImportRecord convertToEntity(EcoMarketImportRecordModel model) {
if (model == null) {
return null;
}
EcoMarketImportRecord entity = new EcoMarketImportRecord();
entity.setId(model.getId());
entity.setTenantId(model.getTenantId());
entity.setUserId(model.getUserId());
entity.setSpaceId(model.getSpaceId());
entity.setTargetType(model.getTargetType());
entity.setTargetId(model.getTargetId());
entity.setEcoTargetId(model.getEcoTargetId());
entity.setCreated(model.getCreated());
entity.setModified(model.getModified());
return entity;
}
}

View File

@@ -0,0 +1,16 @@
package com.xspaceagi.eco.market.sdk.constant;
/**
* 生态市场客户端注册调度任务常量
*/
public final class EcoMarketRegisterTaskConstant {
public static final String BEAN_ID = "ecoMarketClientRegisterTaskService";
private EcoMarketRegisterTaskConstant() {
}
public static String taskIdForTenant(Long tenantId) {
return "eco-market:register:" + tenantId;
}
}

View File

@@ -17,4 +17,6 @@ public interface IEcoMarketSecretRpcService {
*/
ClientSecretDTO registerClient(Long tenantId, String name, String description);
ClientSecretDTO getByTenantId(Long tenantId);
}

View File

@@ -125,4 +125,14 @@ public class EcoMarketApiConstant {
*/
public static final String BASE = CLIENT_API_BASE + "/publish/config";
}
/**
* 导入数据相关API路径
*/
public static class ImportData {
/**
* 获取导入数据API路径
*/
public static final String GET_IMPORT_DATA = SERVER_API_BASE + "/import/data";
}
}

View File

@@ -4,6 +4,7 @@ import com.xspaceagi.file.application.service.FileManagementService;
import com.xspaceagi.file.domain.model.FileRecordDomain;
import com.xspaceagi.file.domain.repository.FileRecordRepository;
import com.xspaceagi.file.domain.storage.FileStorageStrategy;
import com.xspaceagi.file.infra.storage.FileKeyGenerator;
import com.xspaceagi.system.application.dto.TenantConfigDto;
import com.xspaceagi.system.application.service.TenantConfigApplicationService;
import com.xspaceagi.system.spec.common.RequestContext;
@@ -102,15 +103,8 @@ public class FileManagementServiceImpl implements FileManagementService {
}
private static String getFileExtension(String fileName) {
String fileExtension = "";
if (fileName != null && fileName.contains(".")) {
fileExtension = fileName.substring(fileName.lastIndexOf(".") + 1);
// List<String> fileTypes = List.of("pdf", "txt", "doc", "docx", "md", "json", "xml", "xls", "xlsx", "ppt", "pptx", "mp4", "mov", "mp3", "wav", "aac", "flac", "ogg", "wma", "aiff", "m4a", "amr", "midi", "opus", "ra", "zip", "rar", "7z", "tar", "gz", "bz2", "tgz", "tar.gz", "tar.bz2", "tar.7z", "tar.gz", "jpg", "jpeg", "jpe", "png", "gif", "bmp", "ico", "icns", "svg", "webp", "heic", "mkv", "webm");
// if (!fileTypes.contains(fileExtension.toLowerCase())) {
// throw new BizException("Unsupported file type");
// }
}
return fileExtension;
String ext = FileKeyGenerator.extractExtension(fileName);
return ext.startsWith(".") ? ext.substring(1) : ext;
}
@Override

View File

@@ -30,16 +30,26 @@ public class FileKeyGenerator {
return String.format("%s/%s/%s/%s%s", storageType, businessType, date, uuid, extension);
}
private static final String[] COMPOUND_EXTENSIONS = {
".tar.gz", ".tar.bz2", ".tar.xz", ".tar.7z", ".tar.zst", ".tar.lz", ".tar.lzma", ".tar.sz", ".tar.lzo"
};
/**
* Extract file extension
* Extract file extension (including dot), e.g. ".tar.gz" or ".jpg"
*
* @param fileName File name
* @return Extension (including dot), or empty string if no extension
*/
private static String extractExtension(String fileName) {
public static String extractExtension(String fileName) {
if (fileName == null || fileName.isEmpty()) {
return "";
}
String lowerName = fileName.toLowerCase();
for (String ext : COMPOUND_EXTENSIONS) {
if (lowerName.endsWith(ext)) {
return fileName.substring(fileName.length() - ext.length());
}
}
int dotIndex = fileName.lastIndexOf('.');
if (dotIndex > 0 && dotIndex < fileName.length() - 1) {
return fileName.substring(dotIndex);

View File

@@ -1,9 +1,11 @@
package com.xspaceagi.file.infra.storage;
import com.xspaceagi.file.domain.storage.FileStorageStrategy;
import com.xspaceagi.system.spec.utils.RedisUtil;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
@@ -22,6 +24,7 @@ import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.UUID;
/**
* S3 Protocol Storage Implementation
@@ -46,6 +49,12 @@ public class S3FileStorageStrategy implements FileStorageStrategy {
@Value("${s3.region:us-east-1}")
private String region;
@Value("${s3.proxy:}")
private String proxyBaseUrl;
@Resource
private RedisUtil redisUtil;
private volatile S3Client s3Client;
private volatile S3Presigner s3Presigner;
@@ -181,7 +190,11 @@ public class S3FileStorageStrategy implements FileStorageStrategy {
PresignedGetObjectRequest presignedRequest = presigner.presignGetObject(presignRequest);
String url = presignedRequest.url().toString();
if (StringUtils.isNotBlank(proxyBaseUrl) && expireSeconds < 86400 * 30) {
String key = UUID.randomUUID().toString().replace("-", "") + FileKeyGenerator.extractExtension(fileKey);
redisUtil.set("s3p:" + key, url, expireSeconds);
url = proxyBaseUrl + "/" + key;
}
log.info("S3 presigned URL generated successfully: {}", fileKey);
return url;
} catch (Exception e) {

View File

@@ -2,22 +2,28 @@ package com.xspaceagi.file.web.controller;
import com.xspaceagi.file.application.service.FileManagementService;
import com.xspaceagi.file.domain.model.FileRecordDomain;
import com.xspaceagi.file.infra.storage.FileKeyGenerator;
import com.xspaceagi.file.sdk.IFileAccessService;
import com.xspaceagi.file.web.dto.FileRecordVO;
import com.xspaceagi.system.sdk.service.UserAccessKeyApiService;
import com.xspaceagi.system.sdk.service.dto.UserAccessKeyDto;
import com.xspaceagi.system.spec.common.RequestContext;
import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
import com.xspaceagi.system.spec.enums.HttpStatusEnum;
import com.xspaceagi.system.spec.exception.BizException;
import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
import com.xspaceagi.system.spec.utils.RedisUtil;
import com.xspaceagi.system.web.controller.ChatKeyCheck;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
@@ -28,8 +34,10 @@ import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.net.URI;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -49,6 +57,16 @@ public class FileManagementController {
private final IFileAccessService iFileAccessService;
@Resource
private RedisUtil redisUtil;
@Value("${s3.proxy:}")
private String proxyBaseUrl;
@Value("${s3.shortUrlKey:}")
private String shortUrlKey;
@PostMapping("/file/upload")
@Operation(summary = "Upload file")
public ReqResult<FileRecordVO> uploadFile(HttpServletRequest request, @RequestParam("file") MultipartFile file,
@@ -56,7 +74,8 @@ public class FileManagementController {
@RequestParam(value = "targetId", required = false, defaultValue = "-1") Long targetId,
@RequestParam(value = "metadata", required = false) String metadata) {
if (!RequestContext.get().isLogin()) {
ChatKeyCheck.check(request, userAccessKeyApiService);
UserAccessKeyDto userAccessKeyDto = ChatKeyCheck.check(request, userAccessKeyApiService);
RequestContext.get().setUserId(userAccessKeyDto.getUserId());
}
boolean isAuthRequired = true;
// If targetType or targetId not provided, try to parse from Referer
@@ -354,6 +373,37 @@ public class FileManagementController {
}
}
@GetMapping("/f1/{key}")
@Operation(summary = "Access file by fileKey")
public ResponseEntity<?> redirectOriginalUrl(@PathVariable String key) {
Object o = redisUtil.get("s3p:" + key);
if (o == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}
return ResponseEntity.status(HttpStatus.FOUND)
.location(URI.create(o.toString()))
.header("X-S3-Url", o.toString())
.build();
}
@GetMapping("/file/url/short")
@Operation(summary = "Get file short url")
public ReqResult<String> getShortUrl(@RequestParam String url, @RequestParam String ak, @RequestParam(required = false) Integer expireSeconds) {
if (StringUtils.isBlank(shortUrlKey) || shortUrlKey.equals(ak)) {
return ReqResult.error("Invalid ak");
}
String decode = URLDecoder.decode(url, StandardCharsets.UTF_8);
if (decode == null) {
return ReqResult.error("Invalid url");
}
String ext = FileKeyGenerator.extractExtension(decode);
ext = ext.contains("?") ? ext.substring(0, ext.indexOf("?")) : ext;
String key = UUID.randomUUID().toString().replace("-", "") + ext;
redisUtil.set("s3p:" + key, decode, expireSeconds == null ? 3600 : expireSeconds);
return ReqResult.success(proxyBaseUrl + "/" + key);
}
/**
* Extract storage type from fileKey
* fileKey format: /storageType/businessType/date/uuid.ext

View File

@@ -191,7 +191,7 @@ public class ImOutputProcessor {
try {
String proxyPath = String.format("/api/computer/static/%s", conversationId);
Map<String, Object> result = computerFileApplicationService.getFileList(userId, conversationId, proxyPath, null);
Map<String, Object> result = computerFileApplicationService.getFileList(userId, conversationId, proxyPath, null, null);
if (result == null) {
return entries;
}

View File

@@ -1,16 +1,31 @@
package com.xspaceagi.knowledge.api;
import com.xspaceagi.knowledge.core.spec.enums.KnowledgeDataTypeEnum;
import com.xspaceagi.knowledge.domain.model.KnowledgeDocumentModel;
import com.xspaceagi.knowledge.domain.service.IKnowledgeDocumentDomainService;
import com.xspaceagi.knowledge.domain.service.impl.KnowledgeConfigDomainService;
import com.xspaceagi.knowledge.sdk.enums.KnowledgePubStatusEnum;
import com.xspaceagi.knowledge.sdk.request.KnowledgeDocumentRequestVo;
import com.xspaceagi.knowledge.sdk.response.KnowledgeDocumentResponseVo;
import com.xspaceagi.knowledge.sdk.sevice.IKnowledgeDocumentSearchRpcService;
import com.xspaceagi.knowledge.sdk.vo.DocumentAddRequestVo;
import com.xspaceagi.knowledge.sdk.vo.SegmentConfigModel;
import com.xspaceagi.system.domain.log.LogRecordPrint;
import com.xspaceagi.system.spec.common.UserContext;
import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
import com.xspaceagi.system.spec.exception.KnowledgeException;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Objects;
@Slf4j
@Service
public class KnowledgeDocumentSearchRpcService implements IKnowledgeDocumentSearchRpcService {
@@ -18,6 +33,9 @@ public class KnowledgeDocumentSearchRpcService implements IKnowledgeDocumentSear
@Resource
private IKnowledgeDocumentDomainService knowledgeDocumentDomainService;
@Resource
private KnowledgeConfigDomainService knowledgeConfigDomainService;
@LogRecordPrint(content = "[知识库文档]-根据知识库id查询文档")
@Override
@@ -30,4 +48,74 @@ public class KnowledgeDocumentSearchRpcService implements IKnowledgeDocumentSear
responseVo.setDocumentVoList(ans);
return responseVo;
}
@Override
public Long documentAdd(DocumentAddRequestVo documentAddRequestVo) {
// 查询基础配置,补全基础信息
var knowledgeConfig = this.knowledgeConfigDomainService.queryOneInfoById(documentAddRequestVo.getKbId());
if (Objects.isNull(knowledgeConfig)) {
throw KnowledgeException.build(BizExceptionCodeEnum.resourceDataNotFound);
}
KnowledgeDocumentModel knowledgeDocumentModel = new KnowledgeDocumentModel();
knowledgeDocumentModel.setTenantId(knowledgeConfig.getTenantId());
knowledgeDocumentModel.setCreatorId(knowledgeConfig.getCreatorId());
knowledgeDocumentModel.setCreatorName(knowledgeConfig.getCreatorName());
knowledgeDocumentModel.setSpaceId(knowledgeConfig.getSpaceId());
knowledgeDocumentModel.setId(null);
knowledgeDocumentModel.setKbId(documentAddRequestVo.getKbId());
knowledgeDocumentModel.setName(documentAddRequestVo.getName());
knowledgeDocumentModel.setDocUrl(documentAddRequestVo.getDocUrl());
knowledgeDocumentModel.setPubStatus(KnowledgePubStatusEnum.Waiting);
knowledgeDocumentModel.setHasQa(Boolean.FALSE);
knowledgeDocumentModel.setHasEmbedding(Boolean.FALSE);
SegmentConfigModel segmentConfig = SegmentConfigModel.obtainDefaultModel();
segmentConfig.setSegment(documentAddRequestVo.getSegment());
segmentConfig.setWords(documentAddRequestVo.getWords());
segmentConfig.setIsTrim(true);
segmentConfig.setOverlaps(10);
knowledgeDocumentModel.setSegmentConfig(segmentConfig);
//默认 url地址的方式记录存储
knowledgeDocumentModel.setDataType(StringUtils.isNotBlank(documentAddRequestVo.getDocUrl()) ? KnowledgeDataTypeEnum.URL_FILE.getCode() : KnowledgeDataTypeEnum.CUSTOM_TEXT.getCode());
if (KnowledgeDataTypeEnum.URL_FILE.getCode().equals(knowledgeDocumentModel.getDataType())) {
try {
knowledgeDocumentModel.setFileSize(getFileSize(documentAddRequestVo.getDocUrl()));
} catch (Exception e) {
log.warn("获取文件大小失败 {}", documentAddRequestVo.getDocUrl(), e);
}
} else {
knowledgeDocumentModel.setFileContent(documentAddRequestVo.getFileContent());
}
UserContext userContext = UserContext.builder()
.userId(knowledgeConfig.getCreatorId())
.userName(knowledgeConfig.getCreatorName())
.tenantId(knowledgeConfig.getTenantId())
.build();
var id = this.knowledgeDocumentDomainService.customAddInfo(knowledgeDocumentModel, userContext);
// 触发更新知识库文件预估大小值
this.knowledgeDocumentDomainService.triggerUpdateKnowledgeFileSize(knowledgeDocumentModel.getKbId());
return id;
}
/**
* 获取远程文件大小(字节)
*/
public static long getFileSize(String fileUrl) throws IOException, URISyntaxException {
URL url = new URI(fileUrl).toURL();
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("HEAD"); // 只获取头信息,不下载内容
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
conn.setRequestProperty("User-Agent", "Mozilla/5.0");
int code = conn.getResponseCode();
if (code != HttpURLConnection.HTTP_OK) {
throw new IOException("请求失败HTTP状态码: " + code);
}
long size = conn.getContentLengthLong();
conn.disconnect();
return size;
}
}

View File

@@ -35,8 +35,8 @@ public class FileParseService {
List<String> segments = null;
int words = segmentConfig.getWords();
int overlaps = segmentConfig.getOverlaps();
int words = segmentConfig.getWords() == null ? 800 : segmentConfig.getWords();
int overlaps = segmentConfig.getOverlaps() == null ? 10 : segmentConfig.getOverlaps();
if (segmentConfig.getSegment().equals(SegmentEnum.WORDS)) {
List<String> contents = new ArrayList<>(1);

View File

@@ -2,6 +2,7 @@ package com.xspaceagi.knowledge.sdk.sevice;
import com.xspaceagi.knowledge.sdk.request.KnowledgeDocumentRequestVo;
import com.xspaceagi.knowledge.sdk.response.KnowledgeDocumentResponseVo;
import com.xspaceagi.knowledge.sdk.vo.DocumentAddRequestVo;
/**
* Q&A搜索接口
@@ -15,6 +16,13 @@ public interface IKnowledgeDocumentSearchRpcService {
* @param requestVo 搜索参数
* @return 列表
*/
public KnowledgeDocumentResponseVo documentSearch(KnowledgeDocumentRequestVo requestVo);
KnowledgeDocumentResponseVo documentSearch(KnowledgeDocumentRequestVo requestVo);
/**
* 文档添加
*
* @param documentAddRequestVo 文档添加参数
* @return 文档id
*/
Long documentAdd(DocumentAddRequestVo documentAddRequestVo);
}

View File

@@ -0,0 +1,36 @@
package com.xspaceagi.knowledge.sdk.vo;
import com.xspaceagi.knowledge.sdk.enums.SegmentEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
@Data
public class DocumentAddRequestVo implements Serializable {
@Schema(description = "知识库ID", requiredMode = Schema.RequiredMode.REQUIRED)
private Long kbId;
@Schema(description = "文档名称")
private String name;
@Schema(description = "文件内容")
private String fileContent;
@Schema(description = "文件URL")
private String docUrl;
@Schema(description = "分段类型words: 按照词数分段, delimiter: 按照分隔符分段field: 按照字段分段")
private SegmentEnum segment;
@Schema(description = "分段最大字符数选择words或delimiter时有效")
private Integer words;
@Schema(description = "用户ID")
private Long userId;
@Schema(description = "用户名称")
private String userName;
}

View File

@@ -90,7 +90,10 @@ public class LogApplicationServiceImpl extends AbstractTaskExecuteService implem
if (traceContext0 == null) {
return;
}
redisUtil.rightPush("log:queue", JSON.toJSONString(((TraceContext) traceContext0).getLog()));
TraceContext traceContext = (TraceContext) traceContext0;
LogDocument logDocument = (LogDocument) traceContext.getLog();
logDocument.setApiKey(TraceContext.maskApiKey(traceContext.getApiKey()));
redisUtil.rightPush("log:queue", JSON.toJSONString(logDocument));
((TraceContext) traceContext0).setLog(null);
redisUtil.rightPush("bill:queue", JSON.toJSONString(traceContext0));
}

View File

@@ -69,6 +69,9 @@ public class LogDocument extends SearchDocument {
@Schema(description = "缓存输入token数量")
private Integer cacheInputToken;
@Schema(description = "缓存创建输入token数量")
private Integer cacheCreationInputToken;
@Schema(description = "输入token数量")
private Integer inputToken;
@@ -81,6 +84,9 @@ public class LogDocument extends SearchDocument {
@Schema(description = "请求结束时间")
private Long requestEndTime;
@Schema(description = "API Key")
private String apiKey;
@Schema(description = "执行结果码 0000为成功")
@SearchField(keyword = true)
private String resultCode;

View File

@@ -138,6 +138,8 @@ public abstract class BaseController {
page.setRecords(items.stream().map(item -> {
LogDocument document = (LogDocument) item.getDocument();
document.setProcessData(null);
document.setInput(document.getInput() == null ? null : StringUtils.abbreviate(document.getInput(), 100));
document.setOutput(document.getOutput() == null ? null : StringUtils.abbreviate(document.getOutput(), 100));
return document;
}).collect(Collectors.toList()));
return ReqResult.success(page);

View File

@@ -106,6 +106,9 @@ public class McpConfigApplicationServiceImpl implements McpConfigApplicationServ
}
if (mcpDto.getMcpConfig() != null && mcpDto.getMcpConfig().getComponents() != null) {
for (McpComponentDto componentDto : mcpDto.getMcpConfig().getComponents()) {
if (StringUtils.isNotBlank(componentDto.getToolName())) {
continue;
}
String key = componentDto.getType().name() + "_" + componentDto.getTargetId();
McpComponentDto component = componentMap.get(key);
//存在则使用

View File

@@ -364,10 +364,10 @@ public class McpAsyncClient {
initializeResult.protocolVersion(), initializeResult.capabilities(), initializeResult.serverInfo(),
initializeResult.instructions());
if (!this.protocolVersions.contains(initializeResult.protocolVersion())) {
return Mono.error(new McpError(
"Unsupported protocol version from the server: " + initializeResult.protocolVersion()));
}
// if (!this.protocolVersions.contains(initializeResult.protocolVersion())) {
// return Mono.error(new McpError(
// "Unsupported protocol version from the server: " + initializeResult.protocolVersion()));
// }
nextHeartbeat();

View File

@@ -55,7 +55,7 @@ public class McpDeployRpcService {
@Resource
private MarketplaceRpcService marketplaceRpcService;
private java.net.http.HttpClient sseHttpClient = java.net.http.HttpClient.newBuilder()
private final java.net.http.HttpClient sseHttpClient = java.net.http.HttpClient.newBuilder()
.version(java.net.http.HttpClient.Version.HTTP_1_1)
.connectTimeout(Duration.ofSeconds(10)).build();

View File

@@ -21,16 +21,14 @@ 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.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.regex.Pattern;
@Slf4j
@@ -329,7 +327,9 @@ public class MemoryApplicationServiceImpl implements MemoryApplicationService {
}
public List<MemoryExtractDto> createMemory0(MemoryMetaData memoryData) {
if(StringUtils.isBlank(memoryData.getContext())){
return Collections.emptyList();
}
String userPrompt = MEMORY_EXTRACT_PROMPT.replace("{{user_input}}", memoryData.getUserInput()).replace("{{conversation_context}}", memoryData.getContext());
List<MemoryExtractDto> memoryExtractResults = iModelRpcService.call("当前时间: " + (new Date()), userPrompt, new ParameterizedTypeReference<List<MemoryExtractDto>>() {
});

View File

@@ -75,7 +75,7 @@ public class ModelApiProxyConfigServiceImpl implements IModelApiProxyConfigServi
private String enableModelProxy;
@Override
public BackendModelDto getBackendModelConfig(String userApiKey, Long id) {
public BackendModelDto getBackendModelConfig(String userApiKey, Long id, String traceId) {
String cacheKey = BACKEND_MODEL_KEY_PREFIX + userApiKey + (id == null ? "-" : id);
Object val = redisUtil.get(cacheKey);
if (val != null) {
@@ -118,7 +118,7 @@ public class ModelApiProxyConfigServiceImpl implements IModelApiProxyConfigServi
userAccessKeyDto.getConfig().setEnabled(modelInfoDto.getEnabled() != null && modelInfoDto.getEnabled() == 1);
userAccessKeyDto.getConfig().setUserName("user" + userAccessKeyDto.getUserId());
userAccessKeyDto.getConfig().setConversationId(userAccessKeyDto.getUserId().toString());
userAccessKeyDto.getConfig().setRequestId(UUID.randomUUID().toString().replace("-", ""));
userAccessKeyDto.getConfig().setRequestId(traceId == null ? UUID.randomUUID().toString().replace("-", "") : traceId);
userAccessKeyDto.getConfig().setModelBaseUrl(apiInfo.getUrl());
userAccessKeyDto.getConfig().setTraceContext(TraceContext.builder()
.nickName(userAccessKeyDto.getConfig().getUserName())
@@ -129,6 +129,7 @@ public class ModelApiProxyConfigServiceImpl implements IModelApiProxyConfigServi
.billUserId(userAccessKeyDto.getUserId())
.enableSubscription(tenantConfigDto.getEnableSubscription() != null && tenantConfigDto.getEnableSubscription() == 1)
.traceId(userAccessKeyDto.getConfig().getRequestId())
.apiKey(TraceContext.maskApiKey(userApiKey))
.traceTargets(List.of(TraceContext.TraceTarget.builder()
.targetId(modelInfoDto.getId().toString())
.targetType(TraceContext.TraceTargetType.Model)

View File

@@ -35,6 +35,7 @@ public class BackendProxyHandler extends ChannelInboundHandlerAdapter {
private volatile Map<String, Object> responseContext;
private final TokenLogService tokenLogService;
private volatile boolean logPushed = false;
private String contentEncoding = null;
public BackendProxyHandler(TokenLogService tokenLogService) {
this.tokenLogService = tokenLogService;
@@ -108,6 +109,7 @@ public class BackendProxyHandler extends ChannelInboundHandlerAdapter {
sb.append(header.getKey()).append(": ").append(header.getValue()).append("\n");
logger.debug(" {}: {}", header.getKey(), header.getValue());
});
contentEncoding = response.headers().get("Content-Encoding");
getResponseContext().put("responseHeaders", sb.toString());
} catch (Exception e) {
logger.error("Error logging response header", e);
@@ -148,7 +150,9 @@ public class BackendProxyHandler extends ChannelInboundHandlerAdapter {
logger.debug("[Model Proxy Response Body] AccessKey: {}, ResponseTime: {}ms", accessKey, responseTime);
logger.debug("Backend Model: {}", backendModel);
// 统一将字节转换为字符串确保UTF-8编码正确
String resBody = responseBuffer.toString(CharsetUtil.UTF_8);
byte[] byteArray = responseBuffer.toByteArray();
byte[] decompress = DecompressUtils.decompress(byteArray, contentEncoding);
String resBody = new String(decompress, CharsetUtil.UTF_8);
logger.debug("Response Body: {}", resBody);
getResponseContext().put("responseBody", resBody);

View File

@@ -0,0 +1,72 @@
package com.xspaceagi.modelproxy.infra.proxy;
import lombok.extern.slf4j.Slf4j;
import org.brotli.dec.BrotliInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.zip.GZIPInputStream;
@Slf4j
public class DecompressUtils {
/**
* 根据 Content-Encoding 自动解压
*
* @param data 原始字节
* @param encoding Content-Encoding 值gzip, br, deflate, null
*/
public static byte[] decompress(byte[] data, String encoding) throws Exception {
try {
if (encoding == null || encoding.isEmpty()) {
return data;
}
return switch (encoding.toLowerCase()) {
case "gzip", "x-gzip" -> decompressGzip(data);
case "br" -> decompressBrotli(data);
case "deflate" -> decompressDeflate(data);
default -> data;
};
} catch (Throwable e) {
log.error("Error decompressing data", e);
return data;
}
}
private static byte[] decompressGzip(byte[] compressed) throws Exception {
try (InputStream is = new GZIPInputStream(new ByteArrayInputStream(compressed));
ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
byte[] buffer = new byte[4096];
int len;
while ((len = is.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
return bos.toByteArray();
}
}
private static byte[] decompressBrotli(byte[] compressed) throws Exception {
try (InputStream bis = new BrotliInputStream(new ByteArrayInputStream(compressed));
ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
byte[] buffer = new byte[4096];
int len;
while ((len = bis.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
return bos.toByteArray();
}
}
private static byte[] decompressDeflate(byte[] compressed) throws Exception {
try (InputStream is = new java.util.zip.InflaterInputStream(new ByteArrayInputStream(compressed));
ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
byte[] buffer = new byte[4096];
int len;
while ((len = is.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
return bos.toByteArray();
}
}
}

View File

@@ -87,6 +87,7 @@ public class HttpProxyRequestHandler extends ChannelInboundHandlerAdapter {
int targetPort;
String targetScheme;
logger.debug("uri {}, headers {}", request.uri(), request.headers());
String traceId = request.headers().get("trace-id");
//从header里通过x-api-key或Authorization获取apiKey
String apiKey = request.headers().get("x-api-key");
String xApiKey = apiKey;
@@ -110,7 +111,7 @@ public class HttpProxyRequestHandler extends ChannelInboundHandlerAdapter {
return;
}
backendModelDto = modelApiProxyConfigService.getBackendModelConfig(apiKey, extractModelId(request.uri()));
backendModelDto = modelApiProxyConfigService.getBackendModelConfig(apiKey, extractModelId(request.uri()), traceId);
if (backendModelDto == null) {
sendError(ctx, "Invalid API Key", msg, HttpResponseStatus.FORBIDDEN);
return;
@@ -362,6 +363,10 @@ public class HttpProxyRequestHandler extends ChannelInboundHandlerAdapter {
} catch (Exception ignored) {
}
}
Object pending;
while ((pending = receivedLastMsgsWhenConnect.poll()) != null) {
ReferenceCountUtil.release(pending);
}
super.channelInactive(ctx);
}
@@ -369,6 +374,7 @@ public class HttpProxyRequestHandler extends ChannelInboundHandlerAdapter {
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
if (cause.getCause() != null && (cause.getCause() instanceof SSLHandshakeException)) {
logger.warn("SSLHandshakeException: {}", cause.getCause().getMessage());
ctx.close();
return;
}
super.exceptionCaught(ctx, cause);

View File

@@ -87,10 +87,12 @@ public class TokenLogService implements TaskExecuteService {
long cacheInputTokens = 0L;
long inputTokens = 0L;
long outputTokens = 0L;
long cacheCreationInputTokens = 0L;
String content = null;
if ("Anthropic".equalsIgnoreCase(apiProtocol)) {
AnthropicSSEParser.TokenUsage tokenUsage = AnthropicSSEParser.extractTokenUsage(responseBody);
inputTokens = tokenUsage.inputTokens;
cacheCreationInputTokens = tokenUsage.cacheCreationInputTokens;
inputTokens = (long) (tokenUsage.inputTokens + (tokenUsage.cacheCreationInputTokens * 1.25));//一般厂商缓存创建输入的价格比正常输入的要贵25%
outputTokens = tokenUsage.outputTokens;
cacheInputTokens = tokenUsage.cacheInputTokens;
content = tokenUsage.text;
@@ -105,7 +107,8 @@ public class TokenLogService implements TaskExecuteService {
} else if ("OpenAi".equalsIgnoreCase(apiProtocol)) {
OpenAISSEParser.TokenUsage tokenUsage = OpenAISSEParser.extractTokenUsage(responseBody);
cacheInputTokens = tokenUsage.cachedTokens;
inputTokens = tokenUsage.promptTokens;
cacheCreationInputTokens = tokenUsage.cacheCreationInputTokens;
inputTokens = (long) (tokenUsage.promptTokens - cacheInputTokens + tokenUsage.cacheCreationInputTokens * 1.25);
outputTokens = tokenUsage.completionTokens;
content = tokenUsage.content;
}
@@ -143,8 +146,13 @@ public class TokenLogService implements TaskExecuteService {
.conversationId(conversationId)
.targetName(model)
.cacheInputToken((int) cacheInputTokens)
.cacheCreationInputToken((int) cacheCreationInputTokens)
.inputToken((int) inputTokens)
.outputToken((int) outputTokens)
.apiKey(traceContext != null && traceContext.getApiKey() != null ? TraceContext.maskApiKey(traceContext.getApiKey()) : "")
.resultCode(traceContext != null && traceContext.isError() ? traceContext.getErrorCode() : "0000")
.resultMsg(traceContext != null && traceContext.isError() ? traceContext.getErrorMessage() : "success")
.apiKey(traceContext != null && traceContext.getApiKey() != null ? traceContext.getApiKey() : "")
.build());
}
if (traceContext != null) {

View File

@@ -8,7 +8,7 @@ import com.xspaceagi.modelproxy.sdk.service.dto.FrontendModelDto;
*/
public interface IModelApiProxyConfigService {
BackendModelDto getBackendModelConfig(String userApiKey, Long id);
BackendModelDto getBackendModelConfig(String userApiKey, Long id, String traceId);
FrontendModelDto generateUserFrontendModelConfig(Long tenantId, Long userId, Long agentId, BackendModelDto backendModel, String siteUrl);
}

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