同步平台业务模块能力
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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())) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 凭证解析为 minipay(JSAPI)网关下单参数。 */
|
||||
@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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)/ h5(H5支付)/ app(App 原生 SDK)")
|
||||
private PayMode payMode;
|
||||
|
||||
@Data
|
||||
@Schema(description = "订单项")
|
||||
public static class CreateOrderItem implements Serializable {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 = "WxPay:wx.login() 返回的临时 code(5 分钟有效,一次性)")
|
||||
private String wxLoginCode;
|
||||
|
||||
@Schema(description = "AliPay:my.getAuthCode() 返回的授权码")
|
||||
private String alipayAuthCode;
|
||||
}
|
||||
@@ -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 回调 code(snsapi_base,一次性)", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String wxOAuthCode;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/** 校验前端传入的结算页 URL(http/https)。 */
|
||||
private static String validateSettlementPageUrl(String url) {
|
||||
try {
|
||||
|
||||
@@ -10,9 +10,9 @@ import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* 查询doris表数据
|
||||
* 查询表数据
|
||||
*/
|
||||
@Schema(description = "查询doris表数据")
|
||||
@Schema(description = "查询表数据")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
|
||||
@@ -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 {
|
||||
|
||||
// /**
|
||||
|
||||
@@ -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 {
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 {
|
||||
|
||||
/**
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
/**
|
||||
* 获取单个文件的反向代理地址
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -202,7 +202,65 @@ public final class CustomPagePromptConstants {
|
||||
**核心记忆**:
|
||||
- 现有项目 = 先识别模版类型和框架,再用对应的框架语法编码
|
||||
- **绝不擅自转换模版/框架**:vue3-vite 项目保持 Vue 3,react-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 3,react-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:
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
// 创建请求体,包含文件、projectId、filePath和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()) {
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -13,8 +13,6 @@ public class CustomPageBuildModel {
|
||||
// 项目ID
|
||||
private Long id;
|
||||
|
||||
private Long agentId;
|
||||
|
||||
private Long projectId;
|
||||
|
||||
// 1:运行中
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
/**
|
||||
* 回滚版本
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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` | **当前项目 ID(projectId)**,`table-sql-new` / `table-sql-update` 的 `--project-id` 参数从这里取值 |
|
||||
| `DEV_SPACE_ID` | **当前项目所在空间 ID(spaceId)**,`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}]'
|
||||
```
|
||||
@@ -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()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -17,4 +17,6 @@ public interface IEcoMarketSecretRpcService {
|
||||
*/
|
||||
ClientSecretDTO registerClient(Long tenantId, String name, String description);
|
||||
|
||||
ClientSecretDTO getByTenantId(Long tenantId);
|
||||
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
//存在则使用
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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>>() {
|
||||
});
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user