diff --git a/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/controller/AppBuilderAppController.java b/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/controller/AppBuilderAppController.java index 3191333a0..da39b7e6d 100644 --- a/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/controller/AppBuilderAppController.java +++ b/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/controller/AppBuilderAppController.java @@ -41,6 +41,7 @@ import modelengine.fit.jober.aipp.dto.AppBuilderAppDto; import modelengine.fit.jober.aipp.dto.AppBuilderAppMetadataDto; import modelengine.fit.jober.aipp.dto.AppBuilderFlowGraphDto; +import modelengine.fit.jober.aipp.dto.AppBuilderNodeConfigsDto; import modelengine.fit.jober.aipp.dto.AppBuilderSaveConfigDto; import modelengine.fit.jober.aipp.dto.PublishedAppResDto; import modelengine.fit.jober.aipp.dto.check.AppCheckDto; @@ -280,6 +281,36 @@ public Rsp updateByGraph(HttpClassicServerRequest httpRequest, return this.appService.updateFlowGraph(appId, flowGraphDto, this.contextOf(httpRequest, tenantId)); } + /** + * 修改节点的配置项。 + * + * @param httpRequest 请求。 + * @param tenantId 租户Id。 + * @param nodeConfigs 节点的配置项的 dto。 + */ + @CarverSpan(value = "operation.appBuilderApp.update.node.config") + @PutMapping(value = "/node/config", description = "修改节点的配置项") + @AppValidation + public void updateNodeConfig(HttpClassicServerRequest httpRequest, @PathVariable("tenant_id") String tenantId, + @RequestBody @Validated AppBuilderNodeConfigsDto nodeConfigs) { + this.appService.updateNodeConfigs(nodeConfigs, this.contextOf(httpRequest, tenantId)); + } + + /** + * 发布最新版本。 + * + * @param httpRequest 请求。 + * @param tenantId 租户Id。 + * @param appSuiteId 应用的版本id。 + */ + @CarverSpan(value = "operation.appBuilderApp.publish.latest.version") + @PostMapping(value = "/publish/latest_version/{app_suite_id}", description = "发布最新版本") + @AppValidation + public Rsp publishLatestVersion(HttpClassicServerRequest httpRequest, + @PathVariable("tenant_id") String tenantId, @PathVariable("app_suite_id") String appSuiteId) { + return this.appService.publishLatestVersion(appSuiteId, this.contextOf(httpRequest, tenantId)); + } + /** * 更新app的基本信息。 * diff --git a/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/domains/appversion/service/AppVersionService.java b/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/domains/appversion/service/AppVersionService.java index 99869ab29..0928f90e1 100644 --- a/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/domains/appversion/service/AppVersionService.java +++ b/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/domains/appversion/service/AppVersionService.java @@ -32,197 +32,206 @@ */ public interface AppVersionService { /** - * 通过appId获取 {@link AppVersion}. + * 通过appId获取 {@link AppVersion}。 * - * @param appId app多版本中的唯一标识. - * @return {@link Optional}{@code <}{@link AppVersion}{@code >} 对象. + * @param appId app多版本中的唯一标识。 + * @return {@link Optional}{@code <}{@link AppVersion}{@code >} 对象。 */ Optional getByAppId(String appId); /** - * 通过path获取 {@link AppVersion}. + * 通过path获取 {@link AppVersion}。 * - * @param path 路径. - * @return {@link Optional}{@code <}{@link AppVersion}{@code >} 对象. + * @param path 路径。 + * @return {@link Optional}{@code <}{@link AppVersion}{@code >} 对象。 */ Optional getByPath(String path); /** - * 强制获取一个应用,若获取不到则抛出应用版本不存在的异常. + * 强制获取一个应用,若获取不到则抛出应用版本不存在的异常。 * - * @param appId 应用版本id. - * @return {@link AppVersion} 对象 - * @throws AippException 异常. + * @param appId 应用版本id。 + * @return {@link AppVersion} 对象。 + * @throws AippException 异常。 */ AppVersion retrieval(String appId); /** - * 通过appSuiteId获取所有的 {@link AppVersion} 列表. + * 通过appSuiteId获取所有的 {@link AppVersion} 列表。 * - * @param appSuiteId 应用的唯一id. - * @return {@link List}{@code <}{@link AppVersion}{@code >} 集合. + * @param appSuiteId 应用的唯一id。 + * @return {@link List}{@code <}{@link AppVersion}{@code >} 集合。 */ List getByAppSuiteId(String appSuiteId); /** - * 运行 App 的某个版本. + * 运行 App 的某个版本。 * - * @param request 运行请求. - * @param context 操作人上下文信息. - * @return {@link Choir}{@code <}{@link Object}{@code >} SSE对象. + * @param request 运行请求。 + * @param context 操作人上下文信息。 + * @return {@link Choir}{@code <}{@link Object}{@code >} SSE对象。 */ Choir run(CreateAppChatRequest request, OperationContext context); /** - * 调试 App 的某个版本. + * 调试 App 的某个版本。 * - * @param request 运行请求. - * @param context 操作人上下文信息. - * @return {@link Choir}{@code <}{@link Object}{@code >} SSE对象. + * @param request 运行请求。 + * @param context 操作人上下文信息。 + * @return {@link Choir}{@code <}{@link Object}{@code >} SSE对象。 */ Choir debug(CreateAppChatRequest request, OperationContext context); /** - * 重新启动任务实例. + * 重新启动任务实例。 * - * @param instanceId 任务实例id. - * @param params 重启参数. - * @param context 操作人上下文. - * @return {@link Choir}{@code <}{@link Object}{@code >} SSE对象. + * @param instanceId 任务实例id。 + * @param params 重启参数。 + * @param context 操作人上下文。 + * @return {@link Choir}{@code <}{@link Object}{@code >} SSE对象。 */ Choir restart(String instanceId, Map params, OperationContext context); /** - * 创建一个 {@link AppVersion} 对象. + * 创建一个 {@link AppVersion} 对象。 * - * @param templateId 模板id. - * @param dto 创建参数. - * @param context 操作人上下文信息. - * @return {@link AppVersion} 对象. + * @param templateId 模板id。 + * @param dto 创建参数。 + * @param context 操作人上下文信息。 + * @return {@link AppVersion} 对象。 */ AppVersion create(String templateId, AppBuilderAppCreateDto dto, OperationContext context); /** - * 通过模板对象创建app. + * 通过模板对象创建app。 * - * @param template 模板对象. - * @param context 操作人上下文. - * @return {@link AppVersion} 应用版本. + * @param template 模板对象。 + * @param context 操作人上下文。 + * @return {@link AppVersion} 应用版本。 */ AppVersion createByTemplate(AppTemplate template, OperationContext context); /** - * 升级并创建一个新版本. + * 升级并创建一个新版本。 * - * @param appId 待升级的appid. - * @param dto 升级参数. - * @param context 操作人上下文信息. - * @return {@link AppVersion} 对象. + * @param appId 待升级的appid。 + * @param dto 升级参数。 + * @param context 操作人上下文信息。 + * @return {@link AppVersion} 对象。 */ AppVersion upgrade(String appId, AppBuilderAppCreateDto dto, OperationContext context); /** - * 校验app名称是否符合规范. + * 校验app名称是否符合规范。 * - * @param name 名称. - * @param context 操作人上下文信息. - * @throws AippException 业务异常. + * @param name 名称。 + * @param context 操作人上下文信息。 + * @throws AippException 业务异常。 */ void validateAppName(String name, OperationContext context) throws AippException; /** - * 通过应用id获取最新创建的应用版本. + * 通过应用id获取最新创建的应用版本。 * - * @param appSuiteId 应用id. - * @return {@link Optional}{@code <}{@link AppVersion}{@code >} 对象. + * @param appSuiteId 应用id。 + * @return {@link Optional}{@code <}{@link AppVersion}{@code >} 对象。 */ Optional getLatestCreatedByAppSuiteId(String appSuiteId); /** - * 通过应用id获取最先创建的应用版本. + * 通过应用id获取最先创建的应用版本。 * - * @param appSuiteId 应用id. - * @return {@link Optional}{@code <}{@link AppVersion}{@code >} 对象. + * @param appSuiteId 应用id。 + * @return {@link Optional}{@code <}{@link AppVersion}{@code >} 对象。 */ Optional getFirstCreatedByAppSuiteId(String appSuiteId); /** - * 通过tenantId以及查询条件分页查询. + * 通过tenantId以及查询条件分页查询。 * - * @param cond 查询条件. - * @param tenantId 租户id. - * @param offset 偏移量. - * @param limit 条数限制. - * @return {@link AppVersion} 分页集合. + * @param cond 查询条件。 + * @param tenantId 租户id。 + * @param offset 偏移量。 + * @param limit 条数限制。 + * @return {@link AppVersion} 分页集合。 */ RangedResultSet pageListByTenantId(AppQueryCondition cond, String tenantId, long offset, int limit); /** - * 根据条件以及tenantId统计app数量. + * 根据条件以及tenantId统计app数量。 * - * @param cond 查询条件. - * @param tenantId 租户id. - * @return 数量. + * @param cond 查询条件。 + * @param tenantId 租户id。 + * @return 数量。 */ long countByTenantId(AppQueryCondition cond, String tenantId); /** - * 根据传入的 {@link AppBuilderAppDto} 数据进行修改. + * 根据传入的 {@link AppBuilderAppDto} 数据进行修改。 * - * @param appId 版本id. - * @param appDto 待修改数据. - * @param context 操作人上下文信息. - * @return {@link AppVersion} 对象. + * @param appId 版本id。 + * @param appDto 待修改数据。 + * @param context 操作人上下文信息。 + * @return {@link AppVersion} 对象。 */ AppVersion update(String appId, AppBuilderAppDto appDto, OperationContext context); /** - * 根据传入的 {@link AppBuilderFlowGraphDto} 数据进行修改. + * 根据传入的 {@link AppBuilderFlowGraphDto} 数据进行修改。 * - * @param appId 版本id. - * @param graphDto 待修改数据. - * @param context 操作人上下文信息. - * @return {@link AppVersion} 对象. + * @param appId 版本id。 + * @param graphDto 待修改数据。 + * @param context 操作人上下文信息。 + * @return {@link AppVersion} 对象。 */ AppVersion update(String appId, AppBuilderFlowGraphDto graphDto, OperationContext context); /** - * 根据传入的 {@link AppBuilderSaveConfigDto} 数据进行修改. + * 根据传入的 {@link AppVersion} 数据更新流程。 * - * @param appId 版本id. - * @param appBuilderSaveConfigDto 待修改数据. - * @param context 操作人上下文信息. - * @return {@link AppVersion} 对象. + * @param appVersion 应用版本。 + * @param graphDto 待修改数据。 + * @param context 操作人上下文信息。 + */ + void updateGraph(AppVersion appVersion, AppBuilderFlowGraphDto graphDto, OperationContext context); + + /** + * 根据传入的 {@link AppBuilderSaveConfigDto} 数据进行修改。 + * + * @param appId 版本id。 + * @param appBuilderSaveConfigDto 待修改数据。 + * @param context 操作人上下文信息。 + * @return {@link AppVersion} 对象。 */ AppVersion update(String appId, AppBuilderSaveConfigDto appBuilderSaveConfigDto, OperationContext context); /** - * 根据传入的 {@link AppVersion} 进行修改. + * 根据传入的 {@link AppVersion} 进行修改。 * - * @param appVersion {@link AppVersion} 对象. + * @param appVersion {@link AppVersion} 对象。 */ void update(AppVersion appVersion); /** - * 通过id批量删除. + * 通过id批量删除。 * - * @param appIds 版本id集合. + * @param appIds 版本id集合。 */ void deleteByIds(List appIds); /** - * 判断应用名称是否已经存在. + * 判断应用名称是否已经存在。 * - * @param appName 应用名称. - * @param context 操作人上下文. - * @return true/false. + * @param appName 应用名称。 + * @param context 操作人上下文。 + * @return true/false。 */ boolean isNameExists(String appName, OperationContext context); /** - * 保存. + * 保存。 * - * @param appVersion {@link AppVersion} 对象. + * @param appVersion {@link AppVersion} 对象。 */ void save(AppVersion appVersion); } diff --git a/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/domains/appversion/service/impl/AppVersionServiceImpl.java b/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/domains/appversion/service/impl/AppVersionServiceImpl.java index 52af22a12..b53c3eefc 100644 --- a/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/domains/appversion/service/impl/AppVersionServiceImpl.java +++ b/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/domains/appversion/service/impl/AppVersionServiceImpl.java @@ -372,6 +372,12 @@ public AppVersion update(String appId, AppBuilderFlowGraphDto graphDto, Operatio if (appVersion.isPublished()) { throw new AippException(AippErrCode.APP_HAS_ALREADY); } + this.updateGraph(appVersion, graphDto, context); + return appVersion; + } + + @Override + public void updateGraph(AppVersion appVersion, AppBuilderFlowGraphDto graphDto, OperationContext context) { Span.current().setAttribute("name", appVersion.getData().getName()); LocalDateTime operateTime = LocalDateTime.now(); appVersion.getFlowGraph().setUpdateAt(operateTime); @@ -389,7 +395,6 @@ public AppVersion update(String appId, AppBuilderFlowGraphDto graphDto, Operatio appVersion.getData().setUpdateBy(context.getOperator()); appVersion.putAttributes(new HashMap<>()); this.repository.update(appVersion); - return appVersion; } @Override diff --git a/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/service/AppBuilderAppService.java b/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/service/AppBuilderAppService.java index 0fedf0cca..6eb9c30b3 100644 --- a/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/service/AppBuilderAppService.java +++ b/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/service/AppBuilderAppService.java @@ -16,6 +16,7 @@ import modelengine.fit.jober.aipp.dto.AppBuilderConfigDto; import modelengine.fit.jober.aipp.dto.AppBuilderConfigFormPropertyDto; import modelengine.fit.jober.aipp.dto.AppBuilderFlowGraphDto; +import modelengine.fit.jober.aipp.dto.AppBuilderNodeConfigsDto; import modelengine.fit.jober.aipp.dto.AppBuilderSaveConfigDto; import modelengine.fit.jober.aipp.dto.PublishedAppResDto; import modelengine.fit.jober.aipp.dto.check.AppCheckDto; @@ -104,6 +105,24 @@ Rsp saveConfig(String appId, AppBuilderSaveConfigDto appBuilde @Genericable(id = "modelengine.fit.jober.aipp.service.flow.graph.update") Rsp updateFlowGraph(String appId, AppBuilderFlowGraphDto graphDto, OperationContext context); + /** + * 更新节点的配置项。 + * + * @param nodeConfigs 节点配置项的 {@link AppBuilderNodeConfigsDto}。 + * @param context 表示操作上下文的 {@link OperationContext}。 + */ + @Genericable(id = "modelengine.fit.jober.aipp.service.update.node.configs") + void updateNodeConfigs(AppBuilderNodeConfigsDto nodeConfigs, OperationContext context); + + /** + * 发布最新版本。 + * + * @param appSuiteId 应用的版本 id 的 {@link String}。 + * @param context 表示操作上下文的 {@link OperationContext}。 + */ + @Genericable(id = "modelengine.fit.jober.aipp.service.app.publish.latest.version") + Rsp publishLatestVersion(String appSuiteId, OperationContext context); + /** * 发布应用。 * diff --git a/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/service/impl/AppBuilderAppServiceImpl.java b/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/service/impl/AppBuilderAppServiceImpl.java index b10e8ae89..993ed1868 100644 --- a/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/service/impl/AppBuilderAppServiceImpl.java +++ b/app-builder/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/service/impl/AppBuilderAppServiceImpl.java @@ -38,6 +38,7 @@ import modelengine.fit.jober.aipp.dto.AppBuilderConfigDto; import modelengine.fit.jober.aipp.dto.AppBuilderConfigFormPropertyDto; import modelengine.fit.jober.aipp.dto.AppBuilderFlowGraphDto; +import modelengine.fit.jober.aipp.dto.AppBuilderNodeConfigsDto; import modelengine.fit.jober.aipp.dto.AppBuilderSaveConfigDto; import modelengine.fit.jober.aipp.dto.PublishedAppResDto; import modelengine.fit.jober.aipp.dto.check.AppCheckDto; @@ -52,6 +53,7 @@ import modelengine.fit.jober.aipp.service.Checker; import modelengine.fit.jober.aipp.service.UploadedFileManageService; import modelengine.fit.jober.aipp.util.ConvertUtils; +import modelengine.fit.jober.aipp.util.JsonUtils; import modelengine.fit.jober.aipp.util.RandomPathUtils; import modelengine.fit.jober.common.RangedResultSet; import modelengine.fitframework.annotation.Component; @@ -59,6 +61,7 @@ import modelengine.fitframework.annotation.Value; import modelengine.fitframework.log.Logger; import modelengine.fitframework.transaction.Transactional; +import modelengine.fitframework.util.ObjectUtils; import modelengine.fitframework.util.StringUtils; import modelengine.jade.knowledge.KnowledgeCenterService; @@ -189,7 +192,11 @@ public void updateFlow(String appId, OperationContext contextOf) { @Override public AppBuilderAppDto queryLatestOrchestration(String appId, OperationContext context) { AppVersion appVersion = this.appVersionService.retrieval(appId); - App app = this.appDomainFactory.create(appVersion.getData().getAppSuiteId()); + return this.queryLatestOrchestrationBySuiteId(appVersion.getData().getAppSuiteId(), context); + } + + private AppBuilderAppDto queryLatestOrchestrationBySuiteId(String appSuiteId, OperationContext context) { + App app = this.appDomainFactory.create(appSuiteId); AppVersion latestVersion = app.getLatestVersion() .orElseThrow(() -> new AippException(OBTAIN_APP_ORCHESTRATION_INFO_FAILED)); if (latestVersion.isPublished()) { @@ -309,6 +316,176 @@ public Rsp updateFlowGraph(String appId, AppBuilderFlowGraphDt return Rsp.ok(this.converterFactory.convert(appVersion, AppBuilderAppDto.class)); } + @Override + public void updateNodeConfigs(AppBuilderNodeConfigsDto nodeConfigs, OperationContext context) { + if (StringUtils.isBlank(nodeConfigs.getAppSuiteId())) { + throw new IllegalArgumentException("Id is not exist"); + } + AppVersion latestVersion = this.appVersionService.getLatestCreatedByAppSuiteId(nodeConfigs.getAppSuiteId()) + .orElseThrow(() -> new AippException(AippErrCode.APP_NOT_FOUND)); + boolean isPublished = latestVersion.isPublished(); + if (isPublished) { + // 如果是发布状态,则创建一个草稿态 + this.queryLatestOrchestrationBySuiteId(nodeConfigs.getAppSuiteId(), context); + latestVersion = this.appVersionService.getLatestCreatedByAppSuiteId(nodeConfigs.getAppSuiteId()) + .orElseThrow(() -> new AippException(AippErrCode.APP_NOT_FOUND)); + } + Map appearances = JsonUtils.parseObject(latestVersion.getFlowGraph().getAppearance()); + List shapes = this.getShapes(appearances); + for(Map.Entry nodeConfig: nodeConfigs.getNodeConfigs().entrySet()) { + Map oldShape = this.getNodeShape(shapes, ObjectUtils.cast(nodeConfig.getKey())); + Map params = this.getParams(oldShape); + List> inputParams = ObjectUtils.cast(params.get("inputParams")); + Map newConfig = ObjectUtils.cast(nodeConfig.getValue()); + this.updateValueParams(inputParams, ObjectUtils.cast(newConfig.get("valueParams"))); + } + AppBuilderFlowGraphDto flowGraphDto = this.buildAppBuilderFlowGraphDto(latestVersion, appearances); + this.appVersionService.updateGraph(latestVersion, flowGraphDto, context); + } + + @Override + public Rsp publishLatestVersion(String appSuiteId, OperationContext context) { + AppVersion latestVersion = this.appVersionService.getLatestCreatedByAppSuiteId(appSuiteId) + .orElseThrow(() -> new AippException(AippErrCode.APP_NOT_FOUND)); + if (latestVersion.isPublished()) { + throw new AippException(AippErrCode.APP_HAS_ALREADY); + } + AppBuilderAppDto appDto = this.converterFactory.convert(latestVersion, AppBuilderAppDto.class); + return this.publish(appDto, context); + } + + private AppBuilderFlowGraphDto buildAppBuilderFlowGraphDto(AppVersion appVersion, Map appearances) { + return AppBuilderFlowGraphDto.builder() + .name(appVersion.getFlowGraph().getName()) + .appearance(appearances) + .build(); + } + + private void updateValueParams(List> inputParams, Map newConfig) { + if (inputParams == null || newConfig == null || newConfig.isEmpty()) { + return; + } + for (Map.Entry entry : newConfig.entrySet()) { + this.updateInputParam(inputParams, entry); + } + } + + private void updateInputParam(List> inputParams, Map.Entry entry) { + String key = entry.getKey(); + Object value = entry.getValue(); + String[] pathParts = key.split("\\."); + if (pathParts.length == 0) { + return; + } + + // 从顶层inputParams开始查找目标节点 + List> currentLevel = inputParams; + Map targetNode = null; + for (int i = 0; i < pathParts.length; i++) { + String part = pathParts[i]; + + // 在当前层级查找name匹配的节点 + Map found = this.findConfigByName(currentLevel, part); + if (found == null) { + break; + } + + // 如果是最后一层,找到目标节点 + if (i == pathParts.length - 1) { + targetNode = found; + break; + } + + // 非最后一层,进入下一层(value字段) + Object nextLevelObj = found.get("value"); + if (!(nextLevelObj instanceof List)) { + // 下一层不是List,路径无效,跳过 + break; + } + currentLevel = ObjectUtils.cast(nextLevelObj); + } + + // 更新目标节点的value + if (targetNode != null) { + targetNode.put("value", value); + } + } + + private Map findConfigByName(List> configs, String name) { + if (configs == null) { + return null; + } + for (Map config : configs) { + Object nodeName = config.get("name"); + if (nodeName != null && StringUtils.equals(name, ObjectUtils.cast(nodeName))) { + return config; + } + } + return null; + } + + private Map getParams(Map config) { + Map flowMetaMap = + this.mapSearch(config, "flowMeta", "FlowMeta is not map", "FlowMeta is empty"); + Map joberMap = this.mapSearch(flowMetaMap, "jober", "Jober is not map", "Jober is empty"); + Map converterMap = + this.mapSearch(joberMap, "converter", "Converter is not map", "Converter is empty"); + return this.mapSearch(converterMap, "entity", "entity is not map", "Entity is empty"); + } + + private Map mapSearch(Map config, String key, String typeErrorMsg, + String emptyErrorMsg) { + Object value = config.get(key); + if (!(value instanceof Map)) { + throw new AippException(AippErrCode.NODE_CONFIG_UPDATE_FAILED, typeErrorMsg); + } + Map newMap = ObjectUtils.cast(value); + if (newMap.isEmpty()) { + throw new AippException(AippErrCode.NODE_CONFIG_UPDATE_FAILED, emptyErrorMsg); + } + return newMap; + } + + private Map getNodeShape(List shapes, String targetId) { + for (Object shape : shapes) { + if (!(shape instanceof Map)) { + continue; + } + Map shapeMap = ObjectUtils.cast(shape); + String idObj = ObjectUtils.cast(shapeMap.get("id")); + if (StringUtils.equals(idObj, targetId)) { + return ObjectUtils.cast(shape); + } + } + throw new AippException(AippErrCode.NODE_CONFIG_UPDATE_FAILED, "Target node is not found"); + } + + private List getShapes(Map appearances) { + Object pagesObj = appearances.get("pages"); + if (!(pagesObj instanceof List)) { + throw new AippException(AippErrCode.NODE_CONFIG_UPDATE_FAILED, "Pages is not list"); + } + List pages = ObjectUtils.cast(pagesObj); + if (pages.isEmpty()) { + throw new AippException(AippErrCode.NODE_CONFIG_UPDATE_FAILED, "Pages is empty"); + } + Object pageConfig = pages.get(0); + if (!(pageConfig instanceof Map)) { + throw new AippException(AippErrCode.NODE_CONFIG_UPDATE_FAILED, + "The first element of pages is not of map type"); + } + Map pageConfigMap = ObjectUtils.cast(pageConfig); + Object shapesObj = pageConfigMap.get("shapes"); + if (!(shapesObj instanceof List)) { + throw new AippException(AippErrCode.NODE_CONFIG_UPDATE_FAILED, "Shapes is not an list type"); + } + List shapes = ObjectUtils.cast(shapesObj); + if (shapes.isEmpty()) { + throw new AippException(AippErrCode.NODE_CONFIG_UPDATE_FAILED, "Shapes list is empty"); + } + return shapes; + } + @Override @Fitable(id = "default") public void delete(String appId, OperationContext context) { diff --git a/app-builder/plugins/aipp-plugin/src/main/resources/i18n/messages_en.properties b/app-builder/plugins/aipp-plugin/src/main/resources/i18n/messages_en.properties index 14ddf12a8..fef456af3 100644 --- a/app-builder/plugins/aipp-plugin/src/main/resources/i18n/messages_en.properties +++ b/app-builder/plugins/aipp-plugin/src/main/resources/i18n/messages_en.properties @@ -147,4 +147,5 @@ 90002143=No permission to operate this form. 90002144=The app is not in guest mode. 90002145=The large model call timed out. Please try changing the default model. +90002146=Node config update failed. Reason for failure: {0}. 90003000=The application template does not exist. \ No newline at end of file diff --git a/app-builder/plugins/aipp-plugin/src/main/resources/i18n/messages_zh.properties b/app-builder/plugins/aipp-plugin/src/main/resources/i18n/messages_zh.properties index db5fdf8bf..eed5044a3 100644 --- a/app-builder/plugins/aipp-plugin/src/main/resources/i18n/messages_zh.properties +++ b/app-builder/plugins/aipp-plugin/src/main/resources/i18n/messages_zh.properties @@ -148,4 +148,5 @@ 90002143=没有权限操作该表单。 90002144=应用未打开游客模式。 90002145=大模型调用超时,请尝试更换默认模型。 +90002146=节点配置更新失败,失败原因:{0}。 90003000=应用模板不存在。 \ No newline at end of file diff --git a/app-builder/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/service/AppBuilderAppServiceImplTest.java b/app-builder/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/service/AppBuilderAppServiceImplTest.java index 683453416..b3bdb4c29 100644 --- a/app-builder/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/service/AppBuilderAppServiceImplTest.java +++ b/app-builder/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/service/AppBuilderAppServiceImplTest.java @@ -6,8 +6,10 @@ package modelengine.fit.jober.aipp.service; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyString; @@ -26,9 +28,11 @@ import modelengine.fit.jane.common.entity.OperationContext; import modelengine.fit.jober.aipp.common.exception.AippErrCode; import modelengine.fit.jober.aipp.common.exception.AippException; +import modelengine.fit.jober.aipp.common.exception.AippJsonDecodeException; import modelengine.fit.jober.aipp.common.exception.AippTaskNotFoundException; import modelengine.fit.jober.aipp.condition.AppQueryCondition; import modelengine.fit.jober.aipp.converters.ConverterFactory; +import modelengine.fit.jober.aipp.domain.AppBuilderFlowGraph; import modelengine.fit.jober.aipp.domains.app.AppFactory; import modelengine.fit.jober.aipp.domains.app.service.AppDomainService; import modelengine.fit.jober.aipp.domains.appversion.AppVersion; @@ -40,6 +44,7 @@ import modelengine.fit.jober.aipp.dto.AppBuilderAppDto; import modelengine.fit.jober.aipp.dto.AppBuilderAppMetadataDto; import modelengine.fit.jober.aipp.dto.AppBuilderConfigDto; +import modelengine.fit.jober.aipp.dto.AppBuilderNodeConfigsDto; import modelengine.fit.jober.aipp.dto.AppBuilderSaveConfigDto; import modelengine.fit.jober.aipp.dto.check.AppCheckDto; import modelengine.fit.jober.aipp.dto.check.CheckResult; @@ -54,7 +59,6 @@ import modelengine.fit.jober.aipp.util.ConvertUtils; import modelengine.fit.jober.aipp.util.JsonUtils; import modelengine.fit.jober.common.RangedResultSet; - import modelengine.fitframework.util.StringUtils; import modelengine.jade.knowledge.KnowledgeCenterService; @@ -73,6 +77,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; /** @@ -138,6 +143,267 @@ void tearDown() { mockConvertUtils.close(); } + /** + * 测试 case: appSuiteId 为空时抛出 IllegalArgumentException + */ + @Test + void testUpdateNodeConfigs_WhenAppSuiteIdIsEmpty_ShouldThrowException() { + AppBuilderNodeConfigsDto nodeConfigs = new AppBuilderNodeConfigsDto("", Collections.emptyMap()); + OperationContext context = new OperationContext(); + + assertThrows(IllegalArgumentException.class, () -> + this.appBuilderAppService.updateNodeConfigs(nodeConfigs, context)); + } + + /** + * 测试 case: getLatestCreatedByAppSuiteId 返回空 Optional 应抛出 AippException + */ + @Test + void testUpdateNodeConfigs_WhenGetLatestReturnsEmpty_ShouldThrowAippException() { + String appSuiteId = "test-suite-id"; + AppBuilderNodeConfigsDto nodeConfigs = new AppBuilderNodeConfigsDto(appSuiteId, Collections.emptyMap()); + OperationContext context = new OperationContext(); + + when(appVersionService.getLatestCreatedByAppSuiteId(appSuiteId)).thenReturn(Optional.empty()); + + assertThrows(AippException.class, () -> + appBuilderAppService.updateNodeConfigs(nodeConfigs, context)); + } + + /** + * 测试 case: 更新当前版本 + */ + @Test + void testUpdateNodeConfigs_WhenNotPublished_ShouldUpdateCurrentVersionDirectly() { + String appSuiteId = "test-suite-id"; + Map nodeConfigsMap = new HashMap<>(); + nodeConfigsMap.put("nodeKey", + Collections.singletonMap("valueParams", Collections.singletonMap("key", "newValue"))); + + AppBuilderNodeConfigsDto nodeConfigs = new AppBuilderNodeConfigsDto(appSuiteId, nodeConfigsMap); + OperationContext context = new OperationContext(); + + AppVersion mockAppVersion = mock(AppVersion.class); + AppBuilderFlowGraph mockFlowGraph = mock(AppBuilderFlowGraph.class); + + when(mockAppVersion.isPublished()).thenReturn(false); + when(mockAppVersion.getFlowGraph()).thenReturn(mockFlowGraph); + when(mockFlowGraph.getAppearance()).thenReturn( + "{\"pages\":[{\"shapes\":[{\"id\":\"nodeKey\"," + + "\"flowMeta\":{\"jober\":{\"converter\":{\"entity\":{\"inputParams\":[{\"name\":\"key\"," + + "\"value\":\"oldValue\"}]}}}}}]}]}"); + + when(appVersionService.getLatestCreatedByAppSuiteId(appSuiteId)).thenReturn(Optional.of(mockAppVersion)); + doNothing().when(appVersionService).updateGraph(any(), any(), any()); + assertDoesNotThrow(() -> appBuilderAppService.updateNodeConfigs(nodeConfigs, context)); + verify(appVersionService, times(1)).updateGraph(any(), any(), any()); + } + + /** + * 测试 case: pages字段不是数组时应抛出 AippException + */ + @Test + void testUpdateNodeConfigs_WhenPagesIsNotArray_ShouldThrowAippException() { + String appSuiteId = "test-suite-id"; + Map nodeConfigsMap = new HashMap<>(); + nodeConfigsMap.put("nodeKey", + Collections.singletonMap("valueParams", Collections.singletonMap("key", "newValue"))); + + AppBuilderNodeConfigsDto nodeConfigs = new AppBuilderNodeConfigsDto(appSuiteId, nodeConfigsMap); + OperationContext context = new OperationContext(); + + AppVersion mockAppVersion = mock(AppVersion.class); + AppBuilderFlowGraph mockFlowGraph = mock(AppBuilderFlowGraph.class); + + when(mockAppVersion.isPublished()).thenReturn(false); + when(mockAppVersion.getFlowGraph()).thenReturn(mockFlowGraph); + when(mockFlowGraph.getAppearance()).thenReturn("{\"pages\":\"not-an-array\"}"); + + when(appVersionService.getLatestCreatedByAppSuiteId(appSuiteId)).thenReturn(Optional.of(mockAppVersion)); + AippException exception = assertThrows(AippException.class, () -> + appBuilderAppService.updateNodeConfigs(nodeConfigs, context)); + assertEquals(AippErrCode.NODE_CONFIG_UPDATE_FAILED.getCode(), exception.getCode()); + assertTrue(exception.getMessage().contains("节点配置更新失败,失败原因:Pages is not list")); + } + + /** + * 测试 case: page配置不是Map类型时应抛出 AippException + */ + @Test + void testUpdateNodeConfigs_WhenPageIsNotMap_ShouldThrowAippException() { + String appSuiteId = "test-suite-id"; + Map nodeConfigsMap = new HashMap<>(); + nodeConfigsMap.put("nodeKey", + Collections.singletonMap("valueParams", Collections.singletonMap("key", "newValue"))); + + AppBuilderNodeConfigsDto nodeConfigs = new AppBuilderNodeConfigsDto(appSuiteId, nodeConfigsMap); + OperationContext context = new OperationContext(); + + AppVersion mockAppVersion = mock(AppVersion.class); + AppBuilderFlowGraph mockFlowGraph = mock(AppBuilderFlowGraph.class); + + when(mockAppVersion.isPublished()).thenReturn(false); + when(mockAppVersion.getFlowGraph()).thenReturn(mockFlowGraph); + when(mockFlowGraph.getAppearance()).thenReturn( + "{\"pages\":[\"not-a-map\"]}"); + + when(appVersionService.getLatestCreatedByAppSuiteId(appSuiteId)).thenReturn(Optional.of(mockAppVersion)); + + AippException exception = assertThrows(AippException.class, () -> + appBuilderAppService.updateNodeConfigs(nodeConfigs, context)); + + assertEquals(AippErrCode.NODE_CONFIG_UPDATE_FAILED.getCode(), exception.getCode()); + assertTrue(exception.getMessage().contains("The first element of pages is not of map type")); + } + + /** + * 测试 case: shapes字段不是列表类型时应抛出 AippException + */ + @Test + void testUpdateNodeConfigs_WhenShapesIsNotList_ShouldThrowAippException() { + String appSuiteId = "test-suite-id"; + Map nodeConfigsMap = new HashMap<>(); + nodeConfigsMap.put("nodeKey", + Collections.singletonMap("valueParams", Collections.singletonMap("key", "newValue"))); + + AppBuilderNodeConfigsDto nodeConfigs = new AppBuilderNodeConfigsDto(appSuiteId, nodeConfigsMap); + OperationContext context = new OperationContext(); + + AppVersion mockAppVersion = mock(AppVersion.class); + AppBuilderFlowGraph mockFlowGraph = mock(AppBuilderFlowGraph.class); + + when(mockAppVersion.isPublished()).thenReturn(false); + when(mockAppVersion.getFlowGraph()).thenReturn(mockFlowGraph); + when(mockFlowGraph.getAppearance()).thenReturn( + "{\"pages\":[{\"shapes\":\"not-a-list\"}]}"); + + when(appVersionService.getLatestCreatedByAppSuiteId(appSuiteId)).thenReturn(Optional.of(mockAppVersion)); + + AippException exception = assertThrows(AippException.class, () -> + appBuilderAppService.updateNodeConfigs(nodeConfigs, context)); + + assertEquals(AippErrCode.NODE_CONFIG_UPDATE_FAILED.getCode(), exception.getCode()); + assertTrue(exception.getMessage().contains("Shapes is not an list type")); + } + + /** + * 测试 case: flowMeta字段不是Map类型时应抛出 AippException + */ + @Test + void testUpdateNodeConfigs_WhenFlowMetaIsNotMap_ShouldThrowAippException() { + String appSuiteId = "test-suite-id"; + Map nodeConfigsMap = new HashMap<>(); + nodeConfigsMap.put("nodeKey", + Collections.singletonMap("valueParams", Collections.singletonMap("key", "newValue"))); + + AppBuilderNodeConfigsDto nodeConfigs = new AppBuilderNodeConfigsDto(appSuiteId, nodeConfigsMap); + OperationContext context = new OperationContext(); + + AppVersion mockAppVersion = mock(AppVersion.class); + AppBuilderFlowGraph mockFlowGraph = mock(AppBuilderFlowGraph.class); + + when(mockAppVersion.isPublished()).thenReturn(false); + when(mockAppVersion.getFlowGraph()).thenReturn(mockFlowGraph); + when(mockFlowGraph.getAppearance()).thenReturn( + "{\"pages\":[{\"shapes\":[{\"id\":\"nodeKey\",\"flowMeta\":\"not-a-map\"}]}]}"); + + when(appVersionService.getLatestCreatedByAppSuiteId(appSuiteId)).thenReturn(Optional.of(mockAppVersion)); + + AippException exception = assertThrows(AippException.class, () -> + appBuilderAppService.updateNodeConfigs(nodeConfigs, context)); + + assertEquals(AippErrCode.NODE_CONFIG_UPDATE_FAILED.getCode(), exception.getCode()); + assertTrue(exception.getMessage().contains("FlowMeta is not map")); + } + + /** + * 测试 case: converter字段不是Map类型时应抛出 AippException + */ + @Test + void testUpdateNodeConfigs_WhenConverterIsNotMap_ShouldThrowAippException() { + String appSuiteId = "test-suite-id"; + Map nodeConfigsMap = new HashMap<>(); + nodeConfigsMap.put("nodeKey", + Collections.singletonMap("valueParams", Collections.singletonMap("key", "newValue"))); + + AppBuilderNodeConfigsDto nodeConfigs = new AppBuilderNodeConfigsDto(appSuiteId, nodeConfigsMap); + OperationContext context = new OperationContext(); + + AppVersion mockAppVersion = mock(AppVersion.class); + AppBuilderFlowGraph mockFlowGraph = mock(AppBuilderFlowGraph.class); + + when(mockAppVersion.isPublished()).thenReturn(false); + when(mockAppVersion.getFlowGraph()).thenReturn(mockFlowGraph); + when(mockFlowGraph.getAppearance()).thenReturn( + "{\"pages\":[{\"shapes\":[{\"id\":\"nodeKey\"," + + "\"flowMeta\":{\"jober\":{\"converter\":\"not-a-map\"}}}]}]}"); + + when(appVersionService.getLatestCreatedByAppSuiteId(appSuiteId)).thenReturn(Optional.of(mockAppVersion)); + + AippException exception = assertThrows(AippException.class, () -> + appBuilderAppService.updateNodeConfigs(nodeConfigs, context)); + + assertEquals(AippErrCode.NODE_CONFIG_UPDATE_FAILED.getCode(), exception.getCode()); + assertTrue(exception.getMessage().contains("Converter is not map")); + } + + /** + * 测试 case: entity字段不是Map类型时应抛出 AippException + */ + @Test + void testUpdateNodeConfigs_WhenEntityIsNotMap_ShouldThrowAippException() { + String appSuiteId = "test-suite-id"; + Map nodeConfigsMap = new HashMap<>(); + nodeConfigsMap.put("nodeKey", + Collections.singletonMap("valueParams", Collections.singletonMap("key", "newValue"))); + + AppBuilderNodeConfigsDto nodeConfigs = new AppBuilderNodeConfigsDto(appSuiteId, nodeConfigsMap); + OperationContext context = new OperationContext(); + + AppVersion mockAppVersion = mock(AppVersion.class); + AppBuilderFlowGraph mockFlowGraph = mock(AppBuilderFlowGraph.class); + + when(mockAppVersion.isPublished()).thenReturn(false); + when(mockAppVersion.getFlowGraph()).thenReturn(mockFlowGraph); + when(mockFlowGraph.getAppearance()).thenReturn( + "{\"pages\":[{\"shapes\":[{\"id\":\"nodeKey\"," + + "\"flowMeta\":{\"jober\":{\"converter\":{\"entity\":\"not-a-map\"}}}}]}]}"); + + when(appVersionService.getLatestCreatedByAppSuiteId(appSuiteId)).thenReturn(Optional.of(mockAppVersion)); + + AippException exception = assertThrows(AippException.class, () -> + appBuilderAppService.updateNodeConfigs(nodeConfigs, context)); + + assertEquals(AippErrCode.NODE_CONFIG_UPDATE_FAILED.getCode(), exception.getCode()); + assertTrue(exception.getMessage().contains("entity is not map")); + } + + /** + * 测试 case: JSON 解析失败应捕获并转换为 AippJsonDecodeException + */ + @Test + void testUpdateNodeConfigs_WhenJsonParseFails_ShouldThrowAippJsonDecodeException() { + String appSuiteId = "test-suite-id"; + Map nodeConfigsMap = new HashMap<>(); + nodeConfigsMap.put("nodeKey", Collections.singletonMap("valueParams", Collections.singletonList( + Collections.singletonMap("key", "newValue")))); + + AppBuilderNodeConfigsDto nodeConfigs = new AppBuilderNodeConfigsDto(appSuiteId, nodeConfigsMap); + OperationContext context = new OperationContext(); + + AppVersion mockAppVersion = mock(AppVersion.class); + AppBuilderFlowGraph mockFlowGraph = mock(AppBuilderFlowGraph.class); + + when(mockAppVersion.isPublished()).thenReturn(false); + when(mockAppVersion.getFlowGraph()).thenReturn(mockFlowGraph); + when(mockFlowGraph.getAppearance()).thenReturn("{invalid-json}"); + + when(appVersionService.getLatestCreatedByAppSuiteId(appSuiteId)).thenReturn(Optional.of(mockAppVersion)); + + assertThrows(AippJsonDecodeException.class, () -> + appBuilderAppService.updateNodeConfigs(nodeConfigs, context)); + } + /** * 为 {@link AppBuilderAppServiceImpl#updateFlow(String, OperationContext)} 提供测试 */ @@ -201,7 +467,7 @@ public void testQuerySucceed() { when(appVersion.getData()).thenReturn(AppBuilderAppPo.builder().appSuiteId("id1").build()); when(appVersionService.getFirstCreatedByAppSuiteId(anyString())).thenReturn(Optional.of(appVersion)); when(converterFactory.convert(any(), any())).thenReturn(AppBuilderAppDto.builder().aippId("id1").build()); - Assertions.assertDoesNotThrow(() -> appBuilderAppService.query("testId", new OperationContext())); + assertDoesNotThrow(() -> appBuilderAppService.query("testId", new OperationContext())); } @Test @@ -222,7 +488,7 @@ void testQueryByPathValidPath() { AppVersion appVersion = mock(AppVersion.class); when(appVersionService.getByPath(anyString())).thenReturn(Optional.of(appVersion)); when(converterFactory.convert(any(), any())).thenReturn(AppBuilderAppDto.builder().build()); - Assertions.assertDoesNotThrow(() -> appBuilderAppService.queryByPath(validPath)); + assertDoesNotThrow(() -> appBuilderAppService.queryByPath(validPath)); } @Test diff --git a/app-builder/services/aipp-service/src/main/java/modelengine/fit/jober/aipp/common/exception/AippErrCode.java b/app-builder/services/aipp-service/src/main/java/modelengine/fit/jober/aipp/common/exception/AippErrCode.java index d57b75680..296bd6343 100644 --- a/app-builder/services/aipp-service/src/main/java/modelengine/fit/jober/aipp/common/exception/AippErrCode.java +++ b/app-builder/services/aipp-service/src/main/java/modelengine/fit/jober/aipp/common/exception/AippErrCode.java @@ -786,6 +786,11 @@ public enum AippErrCode implements ErrorCode, RetCode { */ MODEL_INVOKE_TIMEOUT(90002145, "大模型调用超时,请尝试更换默认模型。"), + /** + * 节点配置更新失败 + */ + NODE_CONFIG_UPDATE_FAILED(90002146, "节点配置更新失败,失败原因:{0}。"), + /** * 应用模板不存在。 */ diff --git a/app-builder/services/aipp-service/src/main/java/modelengine/fit/jober/aipp/dto/AppBuilderNodeConfigsDto.java b/app-builder/services/aipp-service/src/main/java/modelengine/fit/jober/aipp/dto/AppBuilderNodeConfigsDto.java new file mode 100644 index 000000000..5a26702f0 --- /dev/null +++ b/app-builder/services/aipp-service/src/main/java/modelengine/fit/jober/aipp/dto/AppBuilderNodeConfigsDto.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package modelengine.fit.jober.aipp.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; + +/** + * 节点配置 dto + * + * @author 邬涨财 + * @since 2025-10-20 + */ +@Builder +@Data +@AllArgsConstructor +@NoArgsConstructor +public class AppBuilderNodeConfigsDto { + private String appSuiteId; + private Map nodeConfigs; +}