From 67804673b39874fe4a9ede28bcba957a0428f45b Mon Sep 17 00:00:00 2001 From: april Date: Tue, 13 May 2025 22:00:13 +0800 Subject: [PATCH 1/6] =?UTF-8?q?[app-builder]=20=E4=BF=AE=E6=94=B9=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E5=8E=86=E5=8F=B2=E5=8F=91=E5=B8=83=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=EF=BC=8C=E6=96=B0=E5=A2=9E=E6=81=A2=E5=A4=8D?= =?UTF-8?q?=E5=BA=94=E7=94=A8=E5=88=B0=E6=8C=87=E5=AE=9A=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AppBuilderAppController.java | 19 +++- .../aipp/service/AppBuilderAppService.java | 15 ++- .../impl/AppBuilderAppServiceImpl.java | 91 +++++++++++++------ .../fit/jober/aipp/util/MetaUtils.java | 18 ++++ .../service/AppBuilderAppServiceImplTest.java | 64 +++++++++---- 5 files changed, 158 insertions(+), 49 deletions(-) diff --git a/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/controller/AppBuilderAppController.java b/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/controller/AppBuilderAppController.java index 3ebb73588b..04dcc16ab8 100644 --- a/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/controller/AppBuilderAppController.java +++ b/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/controller/AppBuilderAppController.java @@ -176,7 +176,7 @@ public Rsp queryLatestOrchestration(HttpClassicServerRequest h * @return 查询结果列表。 */ @GetMapping(value = "/{app_id}/recentPublished", description = "查询 app 的历史发布版本") - public Rsp> recentPublished(HttpClassicServerRequest httpRequest, + public Rsp> recentPublished(HttpClassicServerRequest httpRequest, @PathVariable("app_id") String appId, @PathVariable("tenant_id") String tenantId, @RequestParam(value = "offset", defaultValue = "0") long offset, @RequestParam(value = "limit", defaultValue = "10") int limit, @RequestBean AppQueryCondition cond) { @@ -425,6 +425,23 @@ public Rsp importApp(HttpClassicServerRequest httpRequest, } } + /** + * 恢复应用到指定历史版本。 + * + * @param httpRequest 表示 http 请求的 {@link HttpClassicServerRequest}。 + * @param tenantId 表示租户 id 的 {@link String}。 + * @param appId 表示应用 id 的 {@link String}。 + * @param resetAppId 表示指定历史版本 id 的 {@link String}。 + * @return 表示恢复后应用信息的 {@link AppBuilderAppDto}。 + */ + @CarverSpan(value = "operation.appBuilderApp.resetApp") + @PostMapping(path = "/reset/{app_id}") + public Rsp resetApp(HttpClassicServerRequest httpRequest, + @PathVariable("tenant_id") String tenantId, @PathVariable("app_id") String appId, + @RequestBody String resetAppId) { + return Rsp.ok(this.appService.resetApp(appId, resetAppId, contextOf(httpRequest, tenantId))); + } + private AppQueryCondition buildAppQueryCondition(AppQueryCondition cond, String type) { cond.setType(type); if (cond.getExcludeNames() == null) { diff --git a/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/service/AppBuilderAppService.java b/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/service/AppBuilderAppService.java index f04d65eabb..1047cbb698 100644 --- a/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/service/AppBuilderAppService.java +++ b/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/service/AppBuilderAppService.java @@ -157,10 +157,10 @@ Rsp> list(AppQueryCondition cond, Oper * @param limit 表示获取数据的最大个数的 {@code int}。 * @param appId 表示应用唯一标识的 {@link String}。 * @param context 表示操作上下文的 {@link OperationContext}。 - * @return 获取到的历史版本信息集合的 {@link List}{@code <}{@link PublishedAppResDto}{@code >}。 + * @return 获取到的历史版本信息集合的 {@link RangedResultSet}{@code <}{@link PublishedAppResDto}{@code >}。 */ @Genericable(id = "modelengine.fit.jober.aipp.service.app.recent.published") - List recentPublished(AppQueryCondition cond, long offset, int limit, String appId, + RangedResultSet recentPublished(AppQueryCondition cond, long offset, int limit, String appId, OperationContext context); /** @@ -231,4 +231,15 @@ List recentPublished(AppQueryCondition cond, long offset, in */ @Genericable(id = "modelengine.fit.jober.aipp.service.app.deleteTemplate") void deleteTemplate(String templateId, OperationContext context); + + /** + * 恢复应用到指定历史版本。 + * + * @param appId 表示应用 id 的 {@link String}。 + * @param resetId 表示指定历史版本 id 的 {@link String}。 + * @param context 表示接口操作上下文的 {@link OperationContext}。 + * @return 表示恢复应用完成后应用详情的 {@link AppBuilderAppDto}。 + */ + @Genericable(id = "modelengine.fit.jober.aipp.service.app.reset") + AppBuilderAppDto resetApp(String appId, String resetId, OperationContext context); } diff --git a/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/service/impl/AppBuilderAppServiceImpl.java b/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/service/impl/AppBuilderAppServiceImpl.java index d7e0ac9967..1efa4a033f 100644 --- a/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/service/impl/AppBuilderAppServiceImpl.java +++ b/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/service/impl/AppBuilderAppServiceImpl.java @@ -600,15 +600,7 @@ private AppBuilderAppDto createAppWithTemplate(AppBuilderAppCreateDto dto, AppBu AppBuilderFlowGraph flowGraph = templateApp.getFlowGraph(); flowGraph.setId(Entities.generateId()); Map appearance; - try { - appearance = JSONObject.parseObject(flowGraph.getAppearance(), new TypeReference>() {}); - } catch (JSONException e) { - log.error("Import config failed, cause: {}", e); - throw new AippException(AippErrCode.IMPORT_CONFIG_FIELD_ERROR, "flowGraph.appearance"); - } - appearance.computeIfPresent("id", (key, value) -> flowGraph.getId()); - // 这里在创建应用时需要保证graph中的title+version唯一,否则在发布flow时会报错 - appearance.put("title", flowGraph.getId()); + appearance = this.resetGraphId(flowGraph); // 动态修改graph中的model为可选model的第一个 flowGraph.setAppearance(JSONObject.toJSONString(appearance)); String version = this.buildVersion(templateApp, isUpgrade); @@ -1282,24 +1274,25 @@ public void deleteTemplate(String templateId, OperationContext context) { } @Override - public List recentPublished(AppQueryCondition cond, long offset, int limit, String appId, - OperationContext context) { + public RangedResultSet recentPublished(AppQueryCondition cond, long offset, int limit, + String appId, OperationContext context) { this.validateApp(appId); try { String aippId = MetaUtils.getAippIdByAppId(this.metaService, appId, context); - List allPublishedMeta = MetaUtils.getAllPublishedMeta(this.metaService, aippId, context) - .stream() - .filter(meta -> !this.isAppBelong(appId, meta)) - .collect(Collectors.toList()); + RangedResultSet metaRangedResultSet = + MetaUtils.getPublishedMetaByPage(this.metaService, aippId, offset, limit, context); + List allPublishedMeta = metaRangedResultSet.getResults(); List appIds = allPublishedMeta.stream() .map(meta -> String.valueOf(meta.getAttributes().get(AippConst.ATTR_APP_ID_KEY))) .collect(Collectors.toList()); cond.setIds(appIds); cond.setTenantId(context.getTenantId()); List allPublishedApp = this.appRepository.selectWithCondition(cond); - Map appIdKeyAppValueMap = - allPublishedApp.stream().collect(Collectors.toMap(AppBuilderApp::getId, Function.identity())); - return this.buildPublishedAppResDtos(allPublishedMeta, appIdKeyAppValueMap); + Map appIdKeyAppValueMap = allPublishedApp.stream() + .map(app -> appFactory.create(app.getId())) + .collect(Collectors.toMap(AppBuilderApp::getId, Function.identity())); + return RangedResultSet.create(this.buildPublishedAppResDtos(allPublishedMeta, appIdKeyAppValueMap), + metaRangedResultSet.getRange()); } catch (AippTaskNotFoundException exception) { throw new AippException(QUERY_PUBLICATION_HISTORY_FAILED); } @@ -1315,30 +1308,58 @@ public List checkAvailable(List appCheckDtos, Operatio return results.stream().filter(result -> !result.isValid()).collect(Collectors.toList()); } + @Override + @Transactional + public AppBuilderAppDto resetApp(String appId, String resetId, OperationContext context) { + AppBuilderApp resetApp = this.appFactory.create(resetId); + AppBuilderApp currentApp = this.appFactory.create(appId); + // 更新graph和form property,需更新form property的app id以及graph的id、title + List resetFormProperties = resetApp.getFormProperties(); + List currentFormProperties = currentApp.getFormProperties(); + Map currentPropMap = currentFormProperties.stream() + .collect(Collectors.toMap(AppBuilderFormProperty::getName, Function.identity())); + resetFormProperties.forEach(resetProp -> { + AppBuilderFormProperty currentProp = currentPropMap.get(resetProp.getName()); + if (currentProp != null) { + currentProp.setDefaultValue(resetProp.getDefaultValue()); + } + }); + currentApp.getFormPropertyRepository().updateMany(currentFormProperties); + + AppBuilderFlowGraph resetGraph = resetApp.getFlowGraph(); + AppBuilderFlowGraph currentGraph = currentApp.getFlowGraph(); + String currentGraphId = currentApp.getFlowGraphId(); + resetGraph.setId(currentGraphId); + + Map appearance; + appearance = this.resetGraphId(resetGraph); + currentGraph.setAppearance(JSONObject.toJSONString(appearance)); + currentApp.getFlowGraphRepository().updateOne(currentGraph); + + this.appFactory.update(currentApp); + return this.buildFullAppDto(currentApp); + } + private boolean isAppBelong(String appId, Meta meta) { return Objects.equals(String.valueOf(meta.getAttributes().get(AippConst.ATTR_APP_ID_KEY)), appId); } - private List buildPublishedAppResDtos(List metas, + private List buildPublishedAppResDtos(List metas, Map appIdKeyAppValueMap) { return metas.stream() .map(meta -> this.buildPublishedAppResDto(meta, appIdKeyAppValueMap)) .collect(Collectors.toList()); } - private PublishedAppResDto buildPublishedAppResDto(Meta meta, Map appIdKeyAppValueMap) { + private AppBuilderAppDto buildPublishedAppResDto(Meta meta, Map appIdKeyAppValueMap) { String appId = String.valueOf(meta.getAttributes().get(AippConst.ATTR_APP_ID_KEY)); String publishedDescription = String.valueOf(meta.getAttributes().get(AippConst.ATTR_PUBLISH_DESCRIPTION)); String publishedUpdateLog = String.valueOf(meta.getAttributes().get(AippConst.ATTR_PUBLISH_UPDATE_LOG)); AppBuilderApp app = appIdKeyAppValueMap.get(appId); - return PublishedAppResDto.builder() - .appId(appId) - .appVersion(app.getVersion()) - .publishedAt(meta.getCreationTime()) - .publishedBy(meta.getCreator()) - .publishedDescription(publishedDescription) - .publishedUpdateLog(publishedUpdateLog) - .build(); + AppBuilderAppDto dto = this.buildFullAppDto(app); + dto.setPublishedDescription(publishedDescription); + dto.setPublishedUpdateLog(publishedUpdateLog); + return dto; } private static AppBuilderConfig resetConfig(List formProperties, AppBuilderConfig config) { @@ -2036,4 +2057,18 @@ private String getAttribute(Map attributes, String name) { Object value = attributes.get(name); return value == null ? StringUtils.EMPTY : String.valueOf(value); } + + private Map resetGraphId(AppBuilderFlowGraph flowGraph) { + Map appearance; + try { + appearance = JSONObject.parseObject(flowGraph.getAppearance(), new TypeReference>() {}); + } catch (JSONException e) { + log.error("Import config failed, cause: {}", e); + throw new AippException(AippErrCode.IMPORT_CONFIG_FIELD_ERROR, "flowGraph.appearance"); + } + appearance.computeIfPresent("id", (key, value) -> flowGraph.getId()); + // 这里在创建应用时需要保证graph中的title+version唯一,否则在发布flow时会报错 + appearance.put("title", flowGraph.getId()); + return appearance; + } } diff --git a/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/util/MetaUtils.java b/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/util/MetaUtils.java index c127063efa..22017e9227 100644 --- a/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/util/MetaUtils.java +++ b/app-builder/jane/plugins/aipp-plugin/src/main/java/modelengine/fit/jober/aipp/util/MetaUtils.java @@ -128,6 +128,24 @@ public static List getAllPublishedMeta(MetaService metaService, String met return getListMetaHandle(metaService, metaFilter, context); } + /** + * 分页查询指定应用的已发布meta列表,按更新时间倒序。 + * + * @param metaService 表示使用的 {@link MetaService}。 + * @param metaId 表示指定应用id的 {@link String}。 + * @param offset 表示偏移量的 {@code long}。 + * @param limit 表示单页最大数量的 {@code int}。 + * @param context 表示操作人上下文的 {@link OperationContext}。 + * @return 表示查询到的结果集的 {@link RangedResultSet}{@code <}{@link Meta}{@code >}。 + */ + public static RangedResultSet getPublishedMetaByPage(MetaService metaService, String metaId, long offset, + int limit, OperationContext context) { + MetaFilter metaFilter = getNormalMetaFilter(metaId, NormalFilterEnum.PUBLISHED); + metaFilter.setOrderBys(Collections.singletonList(formatSorter(AippSortKeyEnum.UPDATE_AT.name(), + DirectionEnum.DESCEND.name()))); + return metaService.list(metaFilter, false, offset, limit, context); + } + /** * 查询指定aippId所有预览{@link Meta}, 按更新时间倒序 * diff --git a/app-builder/jane/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/service/AppBuilderAppServiceImplTest.java b/app-builder/jane/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/service/AppBuilderAppServiceImplTest.java index b1131592b9..184fe00f60 100644 --- a/app-builder/jane/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/service/AppBuilderAppServiceImplTest.java +++ b/app-builder/jane/plugins/aipp-plugin/src/test/java/modelengine/fit/jober/aipp/service/AppBuilderAppServiceImplTest.java @@ -199,7 +199,7 @@ public void before() { .build(), appTypeService, null, null, flowDefinitionService, aippFlowDefinitionService, "", knowledgeCenterService); } - private AppBuilderApp mockApp() { + private AppBuilderApp mockApp(String appId) { AppBuilderApp appBuilderApp = new AppBuilderApp(flowGraphRepository, configRepository, formRepository, configPropertyRepository, formPropertyRepository); appBuilderApp.setType("template"); @@ -207,7 +207,7 @@ private AppBuilderApp mockApp() { appBuilderApp.setAppCategory("chatbot"); appBuilderApp.setName("Unit Test App"); appBuilderApp.setTenantId("727d7157b3d24209aefd59eb7d1c49ff"); - appBuilderApp.setId("45698235b3d24209aefd59eb7d1c3322"); + appBuilderApp.setId(appId); appBuilderApp.setAttributes(new HashMap<>()); appBuilderApp.getAttributes() .put("icon", "/api/jober/v1/api/31f20efc7e0848deab6a6bc10fc3021e/file?filePath=/var/share/test_old" @@ -222,20 +222,20 @@ private AppBuilderApp mockApp() { appBuilderApp.setCreateAt(TIME); appBuilderApp.setVersion("1.0.0"); appBuilderApp.setPath("YGHmQFJE5ZaFW4wl"); - appBuilderApp.setConfig(this.mockConfig()); + appBuilderApp.setConfig(this.mockConfig(appId)); appBuilderApp.getConfig().setApp(appBuilderApp); appBuilderApp.setConfigId(appBuilderApp.getConfig().getId()); appBuilderApp.setFlowGraph(this.mockGraph()); appBuilderApp.setFlowGraphId(appBuilderApp.getFlowGraph().getId()); - appBuilderApp.setFormProperties(mockFormProperties()); + appBuilderApp.setFormProperties(mockFormProperties(appId)); return appBuilderApp; } - private AppBuilderConfig mockConfig() { + private AppBuilderConfig mockConfig(String appId) { AppBuilderConfig config = new AppBuilderConfig(this.formRepository, this.formPropertyRepository, this.configPropertyRepository, this.appRepository); - config.setAppId("45698235b3d24209aefd59eb7d1c3322"); + config.setAppId(appId); config.setId("24581235b3d24209aefd59eb7d1c3322"); config.setUpdateAt(TIME); @@ -247,7 +247,7 @@ private AppBuilderConfig mockConfig() { config.setForm(mockForm()); config.setFormId(config.getForm().getId()); config.setConfigProperties(mockConfigProperties()); - List formProperties = mockFormProperties(); + List formProperties = mockFormProperties(appId); for (int i = 0; i < 8; i++) { AppBuilderConfigProperty configProperty = config.getConfigProperties().get(i); configProperty.setConfig(config); @@ -270,7 +270,7 @@ private AppBuilderForm mockForm() { return form; } - private List mockFormProperties() { + private List mockFormProperties(String appId) { Object[] values = new Object[] { "null", "null", Collections.singletonList("jadewdnjbq"), Arrays.asList(Arrays.asList("jadewdnjbq", "tools"), Arrays.asList("jadewdnjbq", "workflows")), @@ -313,6 +313,7 @@ private List mockFormProperties() { formProperty.setGroup(group[i]); formProperty.setDescription(description[i]); formProperty.setFormRepository(this.formRepository); + formProperty.setAppId(appId); formProperties.add(formProperty); } return formProperties; @@ -387,7 +388,7 @@ private AppBuilderFlowGraph mockGraph() { + "\"container\": \"elsa-page:t1qrig\", \"dockAlign\": \"top\", \"fontColor\": \"#ECD0A7\", " + "\"fontStyle\": \"normal\", \"itemSpace\": 5, \"namespace\": \"jadeFlow\", \"fontWeight\": " + "\"bold\", \"itemScroll\": {\"x\": 0, \"y\": 0}, \"borderColor\": \"white\", " - + "\"focusBackColor\": \"#f2f3f5\"}], \"title\": \"jadeFlow\", \"source\": \"elsa\", " + + "\"focusBackColor\": \"#f2f3f5\"}], \"title\": \"69e9dec999384b1791e24a3032010e77\", \"source\": \"elsa\", " + "\"tenant\": \"default\", \"setting\": {\"pad\": 10, \"tag\": {}, \"code\": \"\", " + "\"pDock\": \"none\", \"hAlign\": \"center\", \"margin\": 25, \"shadow\": \"\", \"shared\":" + " false, \"vAlign\": \"top\", \"itemPad\": [5, 5, 5, 5], \"visible\": true, \"autoText\": " @@ -438,7 +439,7 @@ public void testBuildBasicApp() { appCreateDto.setName(appName); appCreateDto.setAppCategory("chatbot"); appCreateDto.setDescription(""); - AppBuilderApp appTemplate = mockApp(); + AppBuilderApp appTemplate = mockApp(appId); AippCreateDto aippCreateDto = new AippCreateDto(); aippCreateDto.setAippId("aippId1"); List knowledgeDtos = new ArrayList<>(); @@ -660,7 +661,7 @@ public void testGenerateVersion() throws NoSuchMethodException, InvocationTarget Method method = AppBuilderAppServiceImpl.class.getDeclaredMethod("buildVersion", AppBuilderApp.class, boolean.class); method.setAccessible(true); - AppBuilderApp appBuilderApp = mockApp(); + AppBuilderApp appBuilderApp = mockApp("id"); appBuilderApp.setVersion("10.99.99"); Object result = method.invoke(appBuilderAppService, appBuilderApp, true); assertThat(result).isInstanceOf(String.class); @@ -677,7 +678,7 @@ public void testQuery() { LocalDateTime mockCreateAt = LocalDateTime.of(2024, 12, 21, 12, 0, 0); mockMeta.setCreationTime(mockCreateAt); List mockMetas = Collections.singletonList(mockMeta); - when(appRepository.selectWithId(any())).thenReturn(mockApp()); + when(appRepository.selectWithId(any())).thenReturn(mockApp("test-appid")); when(metaService.list(any(), anyBoolean(), anyLong(), anyInt(), any())).thenReturn( RangedResultSet.create(mockMetas, 0, 1, 1)); AppBuilderAppDto appBuilderAppDto = appBuilderAppService.query("test-appid", new OperationContext()); @@ -691,7 +692,7 @@ public void testQuery() { @DisplayName("测试查询最新编排流程图") void testQueryByPathValidPath() { String validPath = "YGHmQFJE5ZaFW4wl"; - when(appRepository.selectWithPath(any())).thenReturn(mockApp()); + when(appRepository.selectWithPath(any())).thenReturn(mockApp("45698235b3d24209aefd59eb7d1c3322")); AppBuilderAppDto result = appBuilderAppService.queryByPath(validPath); assertEquals("45698235b3d24209aefd59eb7d1c3322", result.getId()); assertEquals("Unit Test App", result.getName()); @@ -768,7 +769,7 @@ void testUpdateAppWhenIconChanges() throws AippTaskNotFoundException { null)); doNothing().when(service).validateUpdateApp(any(), any(), any()); doNothing().when(appUpdateValidator).validate(anyString()); - when(appFactory.create(anyString())).thenReturn(mockApp()); + when(appFactory.create(anyString())).thenReturn(mockApp(appId)); doNothing().when(appFactory).update(any()); Assertions.assertDoesNotThrow(() -> service.updateApp(appId, mockAppDto(), new OperationContext())); } @@ -828,8 +829,9 @@ void testSaveNewApp() throws NoSuchMethodException { Method saveNewAppBuilderApp = AppBuilderAppServiceImpl.class.getDeclaredMethod("saveNewAppBuilderApp", AppBuilderApp.class); saveNewAppBuilderApp.setAccessible(true); - Assertions.assertDoesNotThrow(() -> saveNewAppBuilderApp.invoke(appBuilderAppService, mockApp())); - verify(uploadedFileService).updateRecord(eq("45698235b3d24209aefd59eb7d1c3322"), eq("/var/share/test_old.jpg"), + String appId = "45698235b3d24209aefd59eb7d1c3322"; + Assertions.assertDoesNotThrow(() -> saveNewAppBuilderApp.invoke(appBuilderAppService, mockApp(appId))); + verify(uploadedFileService).updateRecord(eq(appId), eq("/var/share/test_old.jpg"), eq(IRREMOVABLE)); } @@ -841,7 +843,7 @@ void testSaveConfig() { .input(Collections.singletonList(AppBuilderConfigFormPropertyDto.builder().build())) .graph("{\"graph\":\"abc\"}") .build(); - Mockito.when(this.appRepository.selectWithId(appId)).thenReturn(mockApp()); + Mockito.when(this.appRepository.selectWithId(appId)).thenReturn(mockApp(appId)); doNothing().when(this.formPropertyRepository).updateMany(any()); doNothing().when(this.flowGraphRepository).updateOne(any()); Rsp res = this.appBuilderAppService.saveConfig(appId, appBuilderSaveConfigDto, @@ -952,7 +954,7 @@ void testImportAppConfig() throws IOException { @Test @DisplayName("测试根据应用种类获取应用列表") void testListApplication() { - AppBuilderApp app = this.mockApp(); + AppBuilderApp app = this.mockApp("testId"); when(this.appRepository.selectWithLatestApp(any(), any(), anyLong(), anyInt())).thenReturn( Collections.singletonList(app)); AppQueryCondition cond = new AppQueryCondition(); @@ -962,4 +964,30 @@ void testListApplication() { AppBuilderAppMetadataDto dto = metaData.getResults().get(0); assertThat(dto).extracting(AppBuilderAppMetadataDto::getAppCategory).isEqualTo("chatbot"); } + + @Test + @DisplayName("测试恢复应用到指定版本") + void testResetApp() { + String currentAppId = "currentId"; + String resetAppId = "resetId"; + String resetTenantId = "default"; + + AppBuilderApp currentApp = this.mockApp(currentAppId); + String graphString = "{\"id\": \"graphId\", \"title\":\"graphId\"}, \"tenant\":\"tenantId\""; + String currentGraphId = "graphId"; + AppBuilderFlowGraph currentGraph = + AppBuilderFlowGraph.builder().appearance(graphString).id(currentGraphId).name("LLM template").build(); + currentApp.setFlowGraph(currentGraph); + currentApp.setFlowGraphId(currentGraphId); + + when(this.appRepository.selectWithId(eq(currentAppId))).thenReturn(currentApp); + when(this.appRepository.selectWithId(eq(resetAppId))).thenReturn(this.mockApp(resetAppId)); + AppBuilderAppDto dto = this.appBuilderAppService.resetApp(currentAppId, resetAppId, new OperationContext()); + + assertThat(dto).extracting(dto1 -> dto1.getFlowGraph().getId(), + dto1 -> dto1.getFlowGraph().getAppearance().get("id"), + dto1 -> dto1.getFlowGraph().getAppearance().get("title"), + dto1 -> dto1.getFlowGraph().getAppearance().get("tenant")) + .containsExactly(currentGraphId, currentGraphId, currentGraphId, resetTenantId); + } } From 0ecbaffd32a4c94b3737855a24b263b4a32707cf Mon Sep 17 00:00:00 2001 From: april Date: Fri, 16 May 2025 13:01:59 +0800 Subject: [PATCH 2/6] =?UTF-8?q?[frontend]=20=E5=A2=9E=E5=8A=A0=E5=BA=94?= =?UTF-8?q?=E7=94=A8=E7=BC=96=E6=8E=92=E5=9B=9E=E9=80=80=E5=88=B0=E5=8E=86?= =?UTF-8?q?=E5=8F=B2=E7=89=88=E6=9C=AC=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/timeLine/index.tsx | 224 ++++++++++++++---- app-engine/frontend/src/locale/en_US.json | 10 +- app-engine/frontend/src/locale/zh_CN.json | 10 +- .../addFlow/components/addflow-header.tsx | 9 +- .../pages/addFlow/components/elsa-stage.tsx | 17 +- .../frontend/src/pages/addFlow/index.tsx | 15 +- .../frontend/src/pages/aippIndex/context.ts | 5 + .../frontend/src/pages/aippIndex/index.tsx | 72 +++--- .../frontend/src/pages/chatPreview/index.tsx | 5 +- .../frontend/src/pages/components/header.tsx | 22 +- .../src/pages/components/publish-modal.tsx | 2 +- .../src/pages/components/styles/header.scss | 3 + .../configUi/components/component-factory.tsx | 17 +- .../components/inspiration-container.tsx | 10 +- .../configUi/components/inspiration-list.tsx | 7 +- .../configUi/components/inspiration.tsx | 4 +- .../components/knowledge-container.tsx | 9 +- .../configUi/components/knowledge.tsx | 4 +- .../configUi/components/llm-container.tsx | 8 +- .../configForm/configUi/components/llm.tsx | 5 +- .../multiConversation-container.tsx | 7 +- .../configUi/components/opening-container.tsx | 6 +- .../configUi/components/prompt-template.tsx | 6 +- .../components/recommend-container.tsx | 10 +- .../configUi/components/recommend.tsx | 6 +- .../configUi/components/skill-list.tsx | 4 +- .../configForm/configUi/components/skill.tsx | 4 +- .../configUi/components/tools-container.tsx | 11 +- .../configForm/configUi/index.scoped.scss | 3 + app-engine/frontend/src/router/route.ts | 2 +- app-engine/frontend/src/shared/http/aipp.ts | 10 +- .../frontend/src/store/common/action-types.ts | 1 + .../frontend/src/store/common/common.ts | 13 +- .../frontend/src/store/common/reducer.ts | 5 +- 34 files changed, 395 insertions(+), 151 deletions(-) diff --git a/app-engine/frontend/src/components/timeLine/index.tsx b/app-engine/frontend/src/components/timeLine/index.tsx index 1360a41811..ea85039f1b 100644 --- a/app-engine/frontend/src/components/timeLine/index.tsx +++ b/app-engine/frontend/src/components/timeLine/index.tsx @@ -4,69 +4,213 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import React, { useEffect, useState } from 'react'; -import { Drawer, Timeline, Empty } from 'antd'; +import React, { useContext, useEffect, useRef, useState } from 'react'; +import { Button, Drawer, Timeline } from 'antd'; import { useParams } from 'react-router-dom'; import { CloseOutlined } from '@ant-design/icons'; -import { getVersion } from '@/shared/http/aipp'; -import { useTranslation } from "react-i18next"; -import tagImg from '@/assets/images/ai/tag.png'; +import { Message } from '@/shared/utils/message'; +import { getAppInfo, getVersion, resetApp } from '@/shared/http/aipp'; +import { useTranslation } from 'react-i18next'; +import { useAppDispatch } from '@/store/hook'; +import { setIsReadOnly } from '@/store/common/common'; +import { RenderContext } from '@/pages/aippIndex/context'; + +const PAGE_SIZE = 10; const TimeLineFc = (props) => { - const { open, setOpen, type = '' } = props; + const { open, setOpen, type = '', updateAippCallBack } = props; const [timeList, setTimeList] = useState([]); const { tenantId, appId } = useParams(); const { t } = useTranslation(); + const [selectedAppId, setSelectedAppId] = useState(appId); + const dispatch = useAppDispatch(); + const [page, setPage] = useState(1); + const [loading, setLoading] = useState(false); + const scrollRef = useRef(); + const hasMoreRef = useRef(true); + const currentAppInfo = useRef(null); + const { renderRef, elsaReadOnlyRef } = useContext(RenderContext); - useEffect(() => { - open && getVersion(tenantId, appId, type).then(res => { + const fetchData = async (currentPage: number) => { + if (!open || loading || !hasMoreRef.current) return; + setLoading(true); + try { + const res = await getVersion(tenantId, appId, type, PAGE_SIZE * (currentPage - 1), PAGE_SIZE); if (res.code === 0) { - setTimeList(res.data); + const newItems = res.data.results || []; + setTimeList((prev) => { + const newIds = new Set(prev.map((item) => item.id)); + const filteredNewItems = newItems.filter((item) => !newIds.has(item.id)); + return [...prev, ...filteredNewItems]; + }); + if (newItems.length < PAGE_SIZE) { + hasMoreRef.current = false; + } else { + setPage(currentPage + 1); + } } - }) + } finally { + setLoading(false); + } + }; + + const getCurrentApp = async (tenantId: string, appId: string) => { + const res: any = await getAppInfo(tenantId, appId); + if (res.code === 0) { + currentAppInfo.current = res.data; + } + }; + + useEffect(() => { + console.log(open) + if (open) { + dispatch(setIsReadOnly(true)); + setTimeList([]); + setPage(1); + hasMoreRef.current = true; + window.agent?.readOnly(); + + Promise.all([ + getCurrentApp(tenantId, appId), // 刷新当前应用数据 + fetchData(1) // 加载历史版本 + ]).catch(console.error); + } }, [open]); - const descProcess = (str) => { - if (!str || str === 'null') { - return ''; + + useEffect(() => { + const handleScroll = () => { + const container = scrollRef.current; + if (!container) return; + const { scrollTop, scrollHeight, clientHeight } = container; + if (scrollTop + clientHeight >= scrollHeight - 50) { + fetchData(page); + } + }; + + const container = scrollRef.current; + container?.addEventListener('scroll', handleScroll); + return () => container?.removeEventListener('scroll', handleScroll); + }, [page, open, hasMoreRef.current]); + + + const descProcess = (str) => (!str || str === 'null' ? '' : str); + + const handleRecover = () => { + if (appId !== selectedAppId) { + resetApp(tenantId, appId, selectedAppId, { + 'Content-Type': 'application/json' + }).then(res => { + if (res.code === 0) { + Message({ type: 'success', content: t('resetSucceed') }); + currentAppInfo.current = res.data; + handleClose(); + } + }); } - return str; + }; + + const handleItemClick = (timeItem) => { + setSelectedAppId(timeItem.id); + updateAippCallBack( + { + flowGraph: timeItem.flowGraph, + configFormProperties: timeItem.configFormProperties + } + ); + renderRef.current = false; + elsaReadOnlyRef.current = true; + }; + + const handleClose = () => { + dispatch(setIsReadOnly(false)); + setSelectedAppId(appId); + setTimeList([]); + setPage(1); + hasMoreRef.current = true; + updateAippCallBack(currentAppInfo.current); + renderRef.current = false; + elsaReadOnlyRef.current = false; + setOpen(false); } - return <> + + useEffect(() => { + return () => { + // 组件卸载时自动重置 + dispatch(setIsReadOnly(false)); + }; + }, [dispatch]); + + return ( setOpen(false)} + onClose={handleClose} open={open} - footer={null} - extra={ - setOpen(false)} /> - }> -
-
- - {t('cannotRevertVersion')} + mask={false} + footer={ +
+ +
- {timeList.length > 0 ? + } + extra={} + > +
+ {timeList.length > 0 ? ( - { timeList.map(timeItem => ( - -
-
{timeItem.appVersion}
-
{descProcess(timeItem.publishedDescription)}
-
{timeItem.publishedBy}
-
{timeItem.publishedAt}
-
-
- )) } -
: -
- } + +
handleItemClick(currentAppInfo.current)} + > +
{t('currentDraft')}
+
+
+ {timeList.map(timeItem => { + const isSelected = timeItem.id === selectedAppId; + return ( + +
handleItemClick(timeItem)} + > +
{timeItem.version}
+
{descProcess(timeItem.publishedDescription)}
+
{timeItem.updateBy}
+
{timeItem.updateAt}
+
+
+ ); + })} + {loading &&
{t('loading')}...
} + {!hasMoreRef.current &&
{t('noMore')}
} + + ) : null}
- + ); }; - export default TimeLineFc; diff --git a/app-engine/frontend/src/locale/en_US.json b/app-engine/frontend/src/locale/en_US.json index 9b010a40a9..18e6f77df7 100644 --- a/app-engine/frontend/src/locale/en_US.json +++ b/app-engine/frontend/src/locale/en_US.json @@ -223,7 +223,7 @@ "gotIt": "Got It", "successReleased": "Application released", "versionTip": "Invalid version format", - "releaseTip": "The version to be released will overwrite the historical version and cannot be rolled back", + "releaseTip": "The version to be released will overwrite the historical version", "releaseApplication": "Release Application", "releaseTip2": "Release the application only after it passes the debugging", "versionName": "Version", @@ -952,5 +952,11 @@ "zoomOut": "Zoom Out", "zoomIn": "Zoom In", "formItemFieldTypeCannotBeEmpty": "Field type is required", - "addParallelTask": "Add Parallel Tasks" + "addParallelTask": "Add Parallel Tasks", + "recover": "recover", + "exit": "exit", + "resetSucceed": "Reset Succeed", + "currentDraft": "Current Draft", + "loading": "Loading", + "noMore": "You've reached the end" } diff --git a/app-engine/frontend/src/locale/zh_CN.json b/app-engine/frontend/src/locale/zh_CN.json index 8a9f0be23d..5de4d99513 100644 --- a/app-engine/frontend/src/locale/zh_CN.json +++ b/app-engine/frontend/src/locale/zh_CN.json @@ -306,7 +306,7 @@ "successReleased": "发布应用成功", "successReleased2": "发布工具流成功", "releaseApplication": "发布应用", - "releaseTip": "新版本将覆盖历史版本,并不可回退", + "releaseTip": "新版本将覆盖历史版本", "releaseTip2": "请调试应用,确认无误后发布", "releaseToolFlow": "发布工具流", "versionName": "版本名称", @@ -952,5 +952,11 @@ "zoomOut": "缩小", "zoomIn": "放大", "formItemFieldTypeCannotBeEmpty": "表单项类型不能为空", - "addParallelTask": "添加并行任务" + "addParallelTask": "添加并行任务", + "recover": "恢复", + "exit": "退出", + "resetSucceed": "恢复应用成功", + "currentDraft": "当前草稿", + "loading": "加载中", + "noMore": "已显示全部" } diff --git a/app-engine/frontend/src/pages/addFlow/components/addflow-header.tsx b/app-engine/frontend/src/pages/addFlow/components/addflow-header.tsx index b6eff9a28c..283c0b8fda 100644 --- a/app-engine/frontend/src/pages/addFlow/components/addflow-header.tsx +++ b/app-engine/frontend/src/pages/addFlow/components/addflow-header.tsx @@ -37,7 +37,7 @@ import timeImg from '@/assets/images/ai/time.png'; const AddHeader = (props) => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const { handleDebugClick, workFlow, types, saveTime } = props; + const { handleDebugClick, workFlow, types, saveTime, updateAippCallBack } = props; const { appInfo, setFlowInfo } = useContext(FlowContext); const [open, setOpen] = useState(false); const [imgPath, setImgPath] = useState(''); @@ -162,7 +162,12 @@ const AddHeader = (props) => { appInfo={appInfo} /> {/* 工具流发布历史信息弹窗 */} - +
)} }; diff --git a/app-engine/frontend/src/pages/addFlow/components/elsa-stage.tsx b/app-engine/frontend/src/pages/addFlow/components/elsa-stage.tsx index 80efe54d87..fa783f6d33 100644 --- a/app-engine/frontend/src/pages/addFlow/components/elsa-stage.tsx +++ b/app-engine/frontend/src/pages/addFlow/components/elsa-stage.tsx @@ -22,7 +22,7 @@ import { getAddFlowConfig, getEvaluateConfig } from '@/shared/http/appBuilder'; import { useAppDispatch, useAppSelector } from '@/store/hook'; import { setAppInfo, setValidateInfo } from '@/store/appInfo/appInfo'; import { setTestStatus, setTestTime } from '@/store/flowTest/flowTest'; -import { FlowContext } from '../../aippIndex/context'; +import { FlowContext, RenderContext } from '../../aippIndex/context'; import CreateTestSet from '../../appDetail/evaluate/testSet/createTestset/createTestSet'; import AddSearch from '../../configForm/configUi/components/add-search'; import { configMap } from '../config'; @@ -69,6 +69,7 @@ const Stage = (props) => { const [knowledgeConfigId, setKnowledgeConfigId] = useState(''); const { CONFIGS } = configMap[process.env.NODE_ENV]; const { type, appInfo, setFlowInfo } = useContext(FlowContext); + const { renderRef, elsaReadOnlyRef } = useContext(RenderContext); const testStatus = useAppSelector((state) => state.flowTestStore.testStatus); const appValidateInfo = useAppSelector((state) => state.appStore.validateInfo); const choseNodeId = useAppSelector((state) => state.appStore.choseNodeId); @@ -79,7 +80,6 @@ const Stage = (props) => { const pluginCallback = useRef(); const formCallback = useRef(); const currentApp = useRef(); - const render = useRef(false); const currentChange = useRef(false); const modalRef = useRef(); const openModalRef = useRef(); @@ -91,16 +91,16 @@ const Stage = (props) => { const connectKnowledgeEvent = useRef(); const dispatch = useAppDispatch(); useEffect(() => { - if (appInfo.name && !render.current) { - render.current = true; + if (appInfo.name && !renderRef.current) { + renderRef.current = true; currentApp.current = JSON.parse(JSON.stringify(appInfo)); window.agent = null; - setElsaData(); + setElsaData(elsaReadOnlyRef.current); } }, [appInfo]); useEffect(() => { return () => { - render.current = false; + renderRef.current = false; window.agent = null; dispatch(setTestStatus(null)); } @@ -113,7 +113,7 @@ const Stage = (props) => { } const realAppId = getQueryString('appId'); // 编辑工作流 - function setElsaData() { + function setElsaData(readOnly: boolean) { let graphData = appInfo.flowGraph?.appearance || {}; const stageDom = document.getElementById('stage'); let data = JSON.parse(JSON.stringify(graphData)); @@ -226,6 +226,9 @@ const Stage = (props) => { setShowTools(true); setModalTypes('parallel'); }); + if (readOnly) { + agent.readOnly(); + } }).catch(() => { setSpinning && setSpinning(false); }); diff --git a/app-engine/frontend/src/pages/addFlow/index.tsx b/app-engine/frontend/src/pages/addFlow/index.tsx index f3d07dda53..3c2ea04dd6 100644 --- a/app-engine/frontend/src/pages/addFlow/index.tsx +++ b/app-engine/frontend/src/pages/addFlow/index.tsx @@ -36,8 +36,16 @@ import './styles/index.scss'; const AddFlow = (props) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const { type, appInfo, addFlowRef, - showFlowChangeWarning, setShowFlowChangeWarning, saveTime, setSaveTime } = props; + const { + type, + appInfo, + addFlowRef, + showFlowChangeWarning, + setShowFlowChangeWarning, + saveTime, + setSaveTime, + updateAippCallBack, + } = props; const [dragData, setDragData] = useState([]); const [evaluateData, setEvaluateData] = useState([]); const [flowInfo, setFlowInfo] = useState({}); @@ -134,7 +142,7 @@ const AddFlow = (props) => {
{/* 工具流header */} - {!type && + {(!type || workFlow) && { setShowDebug={setShowDebug} workFlow={workFlow} types={evaluateType} + updateAippCallBack={updateAippCallBack} />} {/* 调试抽屉弹窗 */} {}, setFlowInfo: (params:any) => {} }); + +export const RenderContext = createContext({ + renderRef: { current: false } as React.MutableRefObject, + elsaReadOnlyRef: { current: false } as React.MutableRefObject +}) diff --git a/app-engine/frontend/src/pages/aippIndex/index.tsx b/app-engine/frontend/src/pages/aippIndex/index.tsx index e6bccae356..ce1b62245a 100644 --- a/app-engine/frontend/src/pages/aippIndex/index.tsx +++ b/app-engine/frontend/src/pages/aippIndex/index.tsx @@ -4,22 +4,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import React, { useEffect, useState, useRef, useCallback } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { Spin } from 'antd'; import { useParams } from 'react-router-dom'; import AddFlow from '../addFlow'; import ConfigForm from '../configForm'; import CommonChat from '../chatPreview/chatComminPage'; import ChoreographyHead from '../components/header'; -import { getAppInfo } from '@/shared/http/aipp'; -import { updateFormInfo } from '@/shared/http/aipp'; -import { debounce, getUiD, getCurrentTime, setSpaClassName } from '@/shared/utils/common'; +import { getAppInfo, updateFormInfo } from '@/shared/http/aipp'; +import { debounce, getCurrentTime, getUiD, setSpaClassName } from '@/shared/utils/common'; import { useAppDispatch, useAppSelector } from '@/store/hook'; import { setInspirationOpen } from '@/store/chatStore/chatStore'; -import { setAppId, setAippId, setAppInfo, setChoseNodeId, setValidateInfo } from '@/store/appInfo/appInfo'; -import { setIsDebug } from "@/store/common/common"; -import { getUser } from '../helper'; -import { setTestStatus } from "@/store/flowTest/flowTest"; +import { setAippId, setAppId, setAppInfo, setChoseNodeId, setValidateInfo } from '@/store/appInfo/appInfo'; +import { setIsDebug } from '@/store/common/common'; +import { setTestStatus } from '@/store/flowTest/flowTest'; +import { RenderContext } from '@/pages/aippIndex/context'; /** * 应用配置页面首页 @@ -33,6 +32,7 @@ const AippIndex = () => { const [spinning, setSpinning] = useState(false); const [saveTime, setSaveTime] = useState(''); const [reloadInspiration, setReloadInspiration] = useState(''); + const [workFlow, setWorkFlow] = useState(''); const [showChat, setShowChat] = useState(false); const [messageChecked, setMessageCheck] = useState(false); const [showFlowChangeWarning, setShowFlowChangeWarning] = useState(false); @@ -41,6 +41,8 @@ const AippIndex = () => { const dispatch = useAppDispatch(); const appInfo = useAppSelector((state) => state.appStore.appInfo); const addFlowRef = useRef(null); + const renderRef = useRef(false); + const elsaReadOnlyRef = useRef(false); const elsaChange = () => { setShowElsa(!showElsa); @@ -54,8 +56,10 @@ const AippIndex = () => { dispatch(setInspirationOpen(false)); // TODO: 待后端接口归一后调用 getUser() getAippDetails(); - if (window.location.href.indexOf('type=chatWorkflow') !== -1) { + // TODO: 后续归一插件和应用创建工具流入口的时候需注意type + if (window.location.href.indexOf('type=workFlow') !== -1) { setShowElsa(true); + setWorkFlow('workFlow'); }; return () => { dispatch(setChoseNodeId('')); @@ -85,9 +89,12 @@ const AippIndex = () => { } } // 修改aipp更新回调 - const updateAippCallBack = (data) => { - if (data) { - aippRef.current = data; + const updateAippCallBack = (partialData) => { + if (partialData) { + aippRef.current = { + ...aippRef.current, + ...partialData + }; dispatch(setAppInfo(aippRef.current)); } } @@ -139,19 +146,22 @@ const AippIndex = () => { <>
- -
- {showElsa ? - ( + className={`container ${showElsa ? 'layout-elsa-content' : ''} ${showChat ? 'layout-show-preview' : ''}`} + > + + {!workFlow ? ( + + ) : null} +
+ {showElsa ? ( { setSaveTime={setSaveTime} showFlowChangeWarning={showFlowChangeWarning} setShowFlowChangeWarning={setShowFlowChangeWarning} + updateAippCallBack={updateAippCallBack} /> - ) : - ( + ) : ( { showElsa={showElsa} /> )} - -
+ +
+
diff --git a/app-engine/frontend/src/pages/chatPreview/index.tsx b/app-engine/frontend/src/pages/chatPreview/index.tsx index 4d5bf8ff20..85b155525c 100644 --- a/app-engine/frontend/src/pages/chatPreview/index.tsx +++ b/app-engine/frontend/src/pages/chatPreview/index.tsx @@ -113,6 +113,7 @@ const ChatPreview = (props) => { const storageId = detailPage ? aippId : appId; const chatStatus = ['ARCHIVED', 'ERROR', 'TERMINATED']; const messageType = ['MSG', 'ERROR', 'META_MSG']; + const readOnly = useAppSelector((state) => state.commonStore.isReadOnly); useEffect(() => { currentInfo.current = appInfo; @@ -698,7 +699,7 @@ const ChatPreview = (props) => { checkedChat={checkedList} deleteChat={deleteChat} /> { /> {previewVisible && setPreviewVisible(false)} />}
- {showInspiration &&
+ {showInspiration &&
{appInfo.id && { const testStatus = useAppSelector((state) => state.flowTestStore.testStatus); const appValidateInfo = useAppSelector((state) => state.appStore.validateInfo); const readOnly = useAppSelector((state) => state.chatCommonStore.readOnly); + const preview = useAppSelector((state) => state.commonStore.isReadOnly); const [debugVisible, setDebugVisible] = useState(true); const [open, setOpen] = useState(false); const [isFormPrompt, setIsFormPrompt] = useState(true); @@ -134,7 +135,7 @@ const ChoreographyHead = (props) => { // 编辑名称 const handleEditClick = () => { - editRef.current.showModal(); + !preview && editRef.current.showModal(); }; // 未解决可用性问题点击发布按钮的提示 @@ -274,7 +275,11 @@ const ChoreographyHead = (props) => { } {appInfo?.name} - + { (appInfo.attributes?.latest_version || appInfo.state === 'active') ? ( @@ -296,7 +301,7 @@ const ChoreographyHead = (props) => {
- + { @@ -321,14 +326,14 @@ const ChoreographyHead = (props) => {
{showElsa && } @@ -338,7 +343,12 @@ const ChoreographyHead = (props) => { {/* 编辑应用基本信息弹窗 */} {/* 发布历史记录抽屉 */} - + {/* 发布未调试提示弹窗 */} {/* 应用导入错误清单提示抽屉 */} diff --git a/app-engine/frontend/src/pages/components/publish-modal.tsx b/app-engine/frontend/src/pages/components/publish-modal.tsx index 63a56f4750..961b07819c 100644 --- a/app-engine/frontend/src/pages/components/publish-modal.tsx +++ b/app-engine/frontend/src/pages/components/publish-modal.tsx @@ -48,7 +48,7 @@ const PublishModal = (props) => { app_type: publishType !== 'app' ? 'waterflow' : appInfo.attributes?.app_type }); setIsModalOpen(true); - getVersion(tenantId, appId).then(res => { + getVersion(tenantId, appId, null, 0, 1).then(res => { if (res.code === 0) { form.setFieldsValue({ description: res.data[0]?.publishedDescription || '' diff --git a/app-engine/frontend/src/pages/components/styles/header.scss b/app-engine/frontend/src/pages/components/styles/header.scss index 8b3b12c94e..065ae83bcf 100644 --- a/app-engine/frontend/src/pages/components/styles/header.scss +++ b/app-engine/frontend/src/pages/components/styles/header.scss @@ -19,6 +19,9 @@ height: 32px; border-radius: 4px; cursor: pointer; + &.not-allowed { + cursor: not-allowed; + } } .status-tag { display: flex; diff --git a/app-engine/frontend/src/pages/configForm/configUi/components/component-factory.tsx b/app-engine/frontend/src/pages/configForm/configUi/components/component-factory.tsx index 2f272c5a23..ae1aad3280 100644 --- a/app-engine/frontend/src/pages/configForm/configUi/components/component-factory.tsx +++ b/app-engine/frontend/src/pages/configForm/configUi/components/component-factory.tsx @@ -7,15 +7,26 @@ import OpeningContainer from './opening-container'; import MultiConversationContainer from './multiConversation-container'; import RecommendContainer from './recommend-container'; import InspirationContainer from './inspiration-container'; +import { useAppSelector } from '@/store/hook'; const ComponentFactory = (props) => { const { configStructure, graphOperator, updateData, eventConfigs, categoryType } = props; const [validateList, setValidateList] = useState([]); const curValidateList = useRef([]); + const readOnly = useAppSelector((state) => state.commonStore.isReadOnly); // 获取各项配置组件 const createComponent = (config) => { - const commonProps = { graphOperator, config, updateData, key: config.name, eventConfigs, validateList, categoryType }; + const commonProps = { + graphOperator, + config, + updateData, + key: config.name, + eventConfigs, + validateList, + categoryType, + readOnly + }; switch (config.name) { case 'enterWorkflow': return ; @@ -37,7 +48,7 @@ const ComponentFactory = (props) => { case 'chat': return ; default: - return '' + return ''; } }; @@ -76,4 +87,4 @@ export const ComponentContainer = ({ config, createComponent }) => { {config.children.map((item, index) =>
{createComponent(item)}
)}
); -}; \ No newline at end of file +}; diff --git a/app-engine/frontend/src/pages/configForm/configUi/components/inspiration-container.tsx b/app-engine/frontend/src/pages/configForm/configUi/components/inspiration-container.tsx index 629c569d33..ecf3a44775 100644 --- a/app-engine/frontend/src/pages/configForm/configUi/components/inspiration-container.tsx +++ b/app-engine/frontend/src/pages/configForm/configUi/components/inspiration-container.tsx @@ -16,7 +16,7 @@ import LineImg from '@/assets/images/line.svg'; const { Panel } = Collapse; const InspirationContainer = (props) => { - const { graphOperator, config, updateData, eventConfigs } = props; + const { graphOperator, config, updateData, eventConfigs, readOnly } = props; const inspirationChange = eventConfigs?.inspiration?.change; const [activePanelKey, setActivePanelKey] = useState(['']); const [inspirationValues, setInspiration] = useState(null); @@ -84,15 +84,15 @@ const InspirationContainer = (props) => { {config.description}
- + - inspirationSwitchChange(checked, event)} checked={showInspiration} /> + inspirationSwitchChange(checked, event)} checked={showInspiration} disabled={readOnly}/>
} forceRender key='inspiration' className="site-collapse-custom-panel"> - + }; -export default InspirationContainer; \ No newline at end of file +export default InspirationContainer; diff --git a/app-engine/frontend/src/pages/configForm/configUi/components/inspiration-list.tsx b/app-engine/frontend/src/pages/configForm/configUi/components/inspiration-list.tsx index 6b247d98ef..fc901989cc 100644 --- a/app-engine/frontend/src/pages/configForm/configUi/components/inspiration-list.tsx +++ b/app-engine/frontend/src/pages/configForm/configUi/components/inspiration-list.tsx @@ -10,7 +10,7 @@ import EditImg from '@/assets/images/edit_btn.svg'; import DeleteImg from '@/assets/images/delete_btn.svg'; const InspirationList = (props) => { - const { inspirationValues, clickInspiration, handleDeleteIns } = props; + const { inspirationValues, clickInspiration, handleDeleteIns, readOnly } = props; const [showOperateIndex, setShowOperateIndex] = useState(-1); const [showInspControl, setShowInspControl] = useState(true); // hover显示操作按钮 @@ -24,6 +24,9 @@ const InspirationList = (props) => { // 获取编辑删除按钮 const showOperate = (item) => { + if (readOnly) { + return; + } return ( clickInspiration(item)} className={inspirationValues?.showInspiration ? '' : 'not-allowed'} /> handleDelete(item.id)} className={inspirationValues?.showInspiration ? '' : 'not-allowed'} /> @@ -65,4 +68,4 @@ const InspirationList = (props) => { }; -export default InspirationList; \ No newline at end of file +export default InspirationList; diff --git a/app-engine/frontend/src/pages/configForm/configUi/components/inspiration.tsx b/app-engine/frontend/src/pages/configForm/configUi/components/inspiration.tsx index ce91190a9c..5f8f55be0e 100644 --- a/app-engine/frontend/src/pages/configForm/configUi/components/inspiration.tsx +++ b/app-engine/frontend/src/pages/configForm/configUi/components/inspiration.tsx @@ -33,7 +33,7 @@ import '../styles/inspiration.scss'; const Inspiration = (props) => { const { t } = useTranslation(); - const { updateData, inspirationRef } = props; + const { updateData, inspirationRef, readOnly } = props; const [inspirationValues, setInspirationValues] = useState(null); const [treeData, setTreeData] = useState(null); const [cacheTreeData, setCacheTreeData] = useState(null); @@ -379,7 +379,7 @@ const Inspiration = (props) => { {t('inspirationFunctionDescription')}
- +
{ const { t } = useTranslation(); - const { graphOperator, config, updateData, validateList } = props; + const { graphOperator, config, updateData, validateList, readOnly } = props; const [knowledge, setKnowledge] = useState([]); const [groupConfig, setGroupConfig] = useState({}); const [groupId, setGroupId] = useState(''); @@ -132,16 +132,17 @@ const KnowledgeContainer = (props) => {
{config.description} - knowledgeModalOpen(e)} /> + knowledgeModalOpen(e)} className={!readOnly ? '' : 'version-preview'}/>
- + } forceRender key='knowledge' className="site-collapse-custom-panel">
diff --git a/app-engine/frontend/src/pages/configForm/configUi/components/knowledge.tsx b/app-engine/frontend/src/pages/configForm/configUi/components/knowledge.tsx index d88cc95d94..cff7c82911 100644 --- a/app-engine/frontend/src/pages/configForm/configUi/components/knowledge.tsx +++ b/app-engine/frontend/src/pages/configForm/configUi/components/knowledge.tsx @@ -15,7 +15,7 @@ import closeImg from '@/assets/images/close_btn.svg'; const Knowledge = (props) => { const { t } = useTranslation(); - const { knowledge, groupId, updateData, knowledgeRef, knowledgeConfigId } = props; + const { knowledge, groupId, updateData, knowledgeRef, knowledgeConfigId, readOnly } = props; const [knows, setKnows] = useState([]); const [showOperateIndex, setShowOperateIndex] = useState(-1); const { tenantId } = useParams(); @@ -71,7 +71,7 @@ const Knowledge = (props) => { knows.length ? knows.map((item, index) => { return (
-
handleHoverItem(index, 'enter')} onMouseLeave={() => handleHoverItem(index, 'leave')}> +
handleHoverItem(index, 'enter') : undefined} onMouseLeave={!readOnly ? () => handleHoverItem(index, 'leave') : undefined}> {item.name} { index === showOperateIndex && ( diff --git a/app-engine/frontend/src/pages/configForm/configUi/components/llm-container.tsx b/app-engine/frontend/src/pages/configForm/configUi/components/llm-container.tsx index a7216287f1..b73bd9cfcc 100644 --- a/app-engine/frontend/src/pages/configForm/configUi/components/llm-container.tsx +++ b/app-engine/frontend/src/pages/configForm/configUi/components/llm-container.tsx @@ -15,7 +15,7 @@ import OpenImg from '@/assets/images/open_arrow.png'; const { Panel } = Collapse; const LLMContainer = (props) => { - const { graphOperator, config, updateData, validateList } = props; + const { graphOperator, config, updateData, validateList, readOnly } = props; const [validateItem, setValidateItem] = useState({}); const dispatch = useAppDispatch(); const appConfig = useAppSelector((state) => state.appConfigStore.inputConfigData); @@ -58,12 +58,12 @@ const LLMContainer = (props) => { defaultActiveKey={['model']} > -
- + +
}; -export default LLMContainer; \ No newline at end of file +export default LLMContainer; diff --git a/app-engine/frontend/src/pages/configForm/configUi/components/llm.tsx b/app-engine/frontend/src/pages/configForm/configUi/components/llm.tsx index 2733918f5a..b467dca988 100644 --- a/app-engine/frontend/src/pages/configForm/configUi/components/llm.tsx +++ b/app-engine/frontend/src/pages/configForm/configUi/components/llm.tsx @@ -17,7 +17,7 @@ import FullScreenIcon from '@/assets/images/ai/full_screen_icon.png'; const LLM = (props) => { const { t } = useTranslation(); - const { updateData, llmRef, form, validateItem } = props; + const { updateData, llmRef, form, validateItem, readOnly } = props; const [showControl, setShowControl] = useState(true); const [models, setModels] = useState([]); const [promptValue, setPromptValue] = useState(''); @@ -198,7 +198,7 @@ const LLM = (props) => { />
- openGeneratePrompt()} /> + openGeneratePrompt()} className={readOnly ? 'version-preview' : ''} />
@@ -214,6 +214,7 @@ const LLM = (props) => { promptValue={promptValue} openGeneratePrompt={openGeneratePrompt} updatePromptValue={updatePromptValue} + readOnly={readOnly} >
diff --git a/app-engine/frontend/src/pages/configForm/configUi/components/multiConversation-container.tsx b/app-engine/frontend/src/pages/configForm/configUi/components/multiConversation-container.tsx index 3cbbc661cd..0059663d5b 100644 --- a/app-engine/frontend/src/pages/configForm/configUi/components/multiConversation-container.tsx +++ b/app-engine/frontend/src/pages/configForm/configUi/components/multiConversation-container.tsx @@ -15,7 +15,7 @@ import OpenImg from '@/assets/images/open_arrow.png'; const { Panel } = Collapse; const MultiConversationContainer = (props) => { - const { graphOperator, config, updateData } = props; + const { graphOperator, config, updateData, readOnly } = props; const [memoryValues, setMemoryValues] = useState(null); const [memorySwitch, setMemorySwitch] = useState(false); const haveSetMemory = useRef(false); @@ -90,11 +90,12 @@ const MultiConversationContainer = (props) => { historySwitchChange(checked, event)} checked={memorySwitch} + disabled={readOnly} />
} forceRender key='memory' className="site-collapse-custom-panel"> { memoryValues?.type && -
+ { }; -export default MultiConversationContainer; \ No newline at end of file +export default MultiConversationContainer; diff --git a/app-engine/frontend/src/pages/configForm/configUi/components/opening-container.tsx b/app-engine/frontend/src/pages/configForm/configUi/components/opening-container.tsx index 7e1a7521f0..c5369fd06c 100644 --- a/app-engine/frontend/src/pages/configForm/configUi/components/opening-container.tsx +++ b/app-engine/frontend/src/pages/configForm/configUi/components/opening-container.tsx @@ -16,7 +16,7 @@ const { TextArea } = Input; const OpeningContainer = (props) => { const { t } = useTranslation(); - const { graphOperator, config, updateData } = props; + const { graphOperator, config, updateData, readOnly } = props; const isOpengingChange = useRef(false); const [activePanelKey, setActivePanelKey] = useState(['']); const dispatch = useAppDispatch(); @@ -65,7 +65,7 @@ const OpeningContainer = (props) => { label={t('openingRemarks')} name='opening' > -