diff --git a/api/controllers/console/feature.py b/api/controllers/console/feature.py index 86ef961948..ae4b457fa5 100644 --- a/api/controllers/console/feature.py +++ b/api/controllers/console/feature.py @@ -2,13 +2,25 @@ from flask_restx import Resource from werkzeug.exceptions import Unauthorized from controllers.common.schema import register_response_schema_models +from fields.base import ResponseModel +from libs.helper import dump_response from libs.login import current_user, login_required -from services.feature_service import FeatureModel, FeatureService, LimitationModel, SystemFeatureModel +from services.feature_service import ( + FeatureModel, + FeatureService, + LimitationModel, + SystemFeatureModel, +) from . import console_ns from .wraps import account_initialization_required, cloud_utm_record, setup_required, with_current_tenant_id -register_response_schema_models(console_ns, FeatureModel, LimitationModel, SystemFeatureModel) + +class AppDslVersionResponse(ResponseModel): + app_dsl_version: str + + +register_response_schema_models(console_ns, AppDslVersionResponse, FeatureModel, LimitationModel, SystemFeatureModel) @console_ns.route("/features") @@ -54,6 +66,23 @@ class FeatureVectorSpaceApi(Resource): return FeatureService.get_vector_space(current_tenant_id).model_dump() +@console_ns.route("/app-dsl-version") +class AppDslVersionApi(Resource): + @console_ns.doc("get_app_dsl_version") + @console_ns.doc(description="Get current app DSL version") + @console_ns.response( + 200, + "Success", + console_ns.models[AppDslVersionResponse.__name__], + ) + def get(self): + """Get current app DSL version for workflow clipboard compatibility.""" + return dump_response( + AppDslVersionResponse, + {"app_dsl_version": FeatureService.get_app_dsl_version()}, + ) + + @console_ns.route("/system-features") class SystemFeatureApi(Resource): @console_ns.doc("get_system_features") diff --git a/api/openapi/markdown/console-swagger.md b/api/openapi/markdown/console-swagger.md index abd5cb37f8..0131e61eb4 100644 --- a/api/openapi/markdown/console-swagger.md +++ b/api/openapi/markdown/console-swagger.md @@ -599,6 +599,23 @@ Update API-based extension | ---- | ----------- | | 204 | Binding deleted successfully | +### /app-dsl-version + +#### GET +##### Summary + +Get current app DSL version for workflow clipboard compatibility + +##### Description + +Get current app DSL version + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Success | [AppDslVersionResponse](#appdslversionresponse) | + ### /app/prompt-templates #### GET @@ -11379,6 +11396,12 @@ Enum class for api provider schema type. | use_icon_as_answer_icon | boolean | | No | | workflow | [WorkflowPartial](#workflowpartial) | | No | +#### AppDslVersionResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| app_dsl_version | string | | Yes | + #### AppExportQuery | Name | Type | Description | Required | @@ -15250,7 +15273,6 @@ Default configuration for form inputs. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| app_dsl_version | string | | Yes | | branding | [BrandingModel](#brandingmodel) | | Yes | | enable_change_email | boolean | | Yes | | enable_collaboration_mode | boolean | | Yes | diff --git a/api/openapi/markdown/web-swagger.md b/api/openapi/markdown/web-swagger.md index 3676169d59..34d245e09a 100644 --- a/api/openapi/markdown/web-swagger.md +++ b/api/openapi/markdown/web-swagger.md @@ -1323,7 +1323,6 @@ Returns Server-Sent Events stream. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| app_dsl_version | string | | Yes | | branding | [BrandingModel](#brandingmodel) | | Yes | | enable_change_email | boolean | | Yes | | enable_collaboration_mode | boolean | | Yes | diff --git a/api/services/feature_service.py b/api/services/feature_service.py index 7137a9c8c9..4486e5f94a 100644 --- a/api/services/feature_service.py +++ b/api/services/feature_service.py @@ -160,7 +160,6 @@ class PluginManagerModel(FeatureResponseModel): class SystemFeatureModel(FeatureResponseModel): - app_dsl_version: str = "" sso_enforced_for_signin: bool = False sso_enforced_for_signin_protocol: str = "" enable_marketplace: bool = False @@ -248,7 +247,6 @@ class FeatureService: @classmethod def get_system_features(cls, is_authenticated: bool = False) -> SystemFeatureModel: system_features = SystemFeatureModel() - system_features.app_dsl_version = CURRENT_APP_DSL_VERSION cls._fulfill_system_params_from_env(system_features) @@ -267,6 +265,10 @@ class FeatureService: return system_features + @classmethod + def get_app_dsl_version(cls) -> str: + return CURRENT_APP_DSL_VERSION + @classmethod def _fulfill_system_params_from_env(cls, system_features: SystemFeatureModel): system_features.enable_email_code_login = dify_config.ENABLE_EMAIL_CODE_LOGIN diff --git a/api/tests/unit_tests/controllers/console/test_feature.py b/api/tests/unit_tests/controllers/console/test_feature.py index 58ba3f08cb..9d314920fb 100644 --- a/api/tests/unit_tests/controllers/console/test_feature.py +++ b/api/tests/unit_tests/controllers/console/test_feature.py @@ -46,6 +46,21 @@ class TestFeatureVectorSpaceApi: get_vector_space.assert_called_once_with("tenant_123") +class TestAppDslVersionApi: + def test_get_app_dsl_version_success(self, mocker: MockerFixture): + from controllers.console.feature import AppDslVersionApi + + get_app_dsl_version = mocker.patch("controllers.console.feature.FeatureService.get_app_dsl_version") + get_app_dsl_version.return_value = "0.6.0" + + api = AppDslVersionApi() + + result = api.get() + + assert result == {"app_dsl_version": "0.6.0"} + get_app_dsl_version.assert_called_once_with() + + class TestSystemFeatureApi: def test_get_system_features_authenticated(self, mocker: MockerFixture): """ diff --git a/api/tests/unit_tests/services/test_feature_service_app_dsl_version.py b/api/tests/unit_tests/services/test_feature_service_app_dsl_version.py new file mode 100644 index 0000000000..b2ba664394 --- /dev/null +++ b/api/tests/unit_tests/services/test_feature_service_app_dsl_version.py @@ -0,0 +1,14 @@ +from constants.dsl_version import CURRENT_APP_DSL_VERSION +from services.feature_service import FeatureService + + +def test_get_system_features_excludes_app_dsl_version(): + result = FeatureService.get_system_features().model_dump() + + assert "app_dsl_version" not in result + + +def test_get_app_dsl_version_returns_current_version(): + result = FeatureService.get_app_dsl_version() + + assert result == CURRENT_APP_DSL_VERSION diff --git a/packages/contracts/generated/api/console/app-dsl-version/orpc.gen.ts b/packages/contracts/generated/api/console/app-dsl-version/orpc.gen.ts new file mode 100644 index 0000000000..c384179ab5 --- /dev/null +++ b/packages/contracts/generated/api/console/app-dsl-version/orpc.gen.ts @@ -0,0 +1,30 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { oc } from '@orpc/contract' + +import { zGetAppDslVersionResponse } from './zod.gen' + +/** + * Get current app DSL version for workflow clipboard compatibility + * + * Get current app DSL version + */ +export const get = oc + .route({ + description: 'Get current app DSL version', + inputStructure: 'detailed', + method: 'GET', + operationId: 'getAppDslVersion', + path: '/app-dsl-version', + summary: 'Get current app DSL version for workflow clipboard compatibility', + tags: ['console'], + }) + .output(zGetAppDslVersionResponse) + +export const appDslVersion = { + get, +} + +export const contract = { + appDslVersion, +} diff --git a/packages/contracts/generated/api/console/app-dsl-version/types.gen.ts b/packages/contracts/generated/api/console/app-dsl-version/types.gen.ts new file mode 100644 index 0000000000..901fa796c1 --- /dev/null +++ b/packages/contracts/generated/api/console/app-dsl-version/types.gen.ts @@ -0,0 +1,22 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type ClientOptions = { + baseUrl: `${string}://${string}/console/api` | (string & {}) +} + +export type AppDslVersionResponse = { + app_dsl_version: string +} + +export type GetAppDslVersionData = { + body?: never + path?: never + query?: never + url: '/app-dsl-version' +} + +export type GetAppDslVersionResponses = { + 200: AppDslVersionResponse +} + +export type GetAppDslVersionResponse = GetAppDslVersionResponses[keyof GetAppDslVersionResponses] diff --git a/packages/contracts/generated/api/console/app-dsl-version/zod.gen.ts b/packages/contracts/generated/api/console/app-dsl-version/zod.gen.ts new file mode 100644 index 0000000000..66c8db405a --- /dev/null +++ b/packages/contracts/generated/api/console/app-dsl-version/zod.gen.ts @@ -0,0 +1,15 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import * as z from 'zod' + +/** + * AppDslVersionResponse + */ +export const zAppDslVersionResponse = z.object({ + app_dsl_version: z.string(), +}) + +/** + * Success + */ +export const zGetAppDslVersionResponse = zAppDslVersionResponse diff --git a/packages/contracts/generated/api/console/orpc.gen.ts b/packages/contracts/generated/api/console/orpc.gen.ts index 48f6d4df3b..18317a65f9 100644 --- a/packages/contracts/generated/api/console/orpc.gen.ts +++ b/packages/contracts/generated/api/console/orpc.gen.ts @@ -6,6 +6,7 @@ import { agents } from './agents/orpc.gen' import { allWorkspaces } from './all-workspaces/orpc.gen' import { apiBasedExtension } from './api-based-extension/orpc.gen' import { apiKeyAuth } from './api-key-auth/orpc.gen' +import { appDslVersion } from './app-dsl-version/orpc.gen' import { app } from './app/orpc.gen' import { apps } from './apps/orpc.gen' import { auth } from './auth/orpc.gen' @@ -55,6 +56,7 @@ export const contract = { apiBasedExtension, apiKeyAuth, app, + appDslVersion, apps, auth, billing, diff --git a/packages/contracts/generated/api/console/system-features/types.gen.ts b/packages/contracts/generated/api/console/system-features/types.gen.ts index 3ef9c6e30e..ec917bf12f 100644 --- a/packages/contracts/generated/api/console/system-features/types.gen.ts +++ b/packages/contracts/generated/api/console/system-features/types.gen.ts @@ -5,7 +5,6 @@ export type ClientOptions = { } export type SystemFeatureModel = { - app_dsl_version: string branding: BrandingModel enable_change_email: boolean enable_collaboration_mode: boolean diff --git a/packages/contracts/generated/api/console/system-features/zod.gen.ts b/packages/contracts/generated/api/console/system-features/zod.gen.ts index b3bba61c3f..10e09e6b8e 100644 --- a/packages/contracts/generated/api/console/system-features/zod.gen.ts +++ b/packages/contracts/generated/api/console/system-features/zod.gen.ts @@ -87,7 +87,6 @@ export const zWebAppAuthModel = z.object({ * SystemFeatureModel */ export const zSystemFeatureModel = z.object({ - app_dsl_version: z.string().default(''), branding: zBrandingModel, enable_change_email: z.boolean().default(true), enable_collaboration_mode: z.boolean().default(true), diff --git a/packages/contracts/generated/api/web/types.gen.ts b/packages/contracts/generated/api/web/types.gen.ts index b08f372208..56f8164e91 100644 --- a/packages/contracts/generated/api/web/types.gen.ts +++ b/packages/contracts/generated/api/web/types.gen.ts @@ -218,7 +218,6 @@ export type SuggestedQuestionsResponse = { } export type SystemFeatureModel = { - app_dsl_version: string branding: BrandingModel enable_change_email: boolean enable_collaboration_mode: boolean diff --git a/packages/contracts/generated/api/web/zod.gen.ts b/packages/contracts/generated/api/web/zod.gen.ts index d1e8ab7600..9ca727267d 100644 --- a/packages/contracts/generated/api/web/zod.gen.ts +++ b/packages/contracts/generated/api/web/zod.gen.ts @@ -364,7 +364,6 @@ export const zWebAppAuthModel = z.object({ * SystemFeatureModel */ export const zSystemFeatureModel = z.object({ - app_dsl_version: z.string().default(''), branding: zBrandingModel, enable_change_email: z.boolean().default(true), enable_collaboration_mode: z.boolean().default(true), diff --git a/web/__tests__/utils/mock-system-features.tsx b/web/__tests__/utils/mock-system-features.tsx index 6884e68237..ed4daedd29 100644 --- a/web/__tests__/utils/mock-system-features.tsx +++ b/web/__tests__/utils/mock-system-features.tsx @@ -12,6 +12,22 @@ type DeepPartial = T extends Array ? { [K in keyof T]?: DeepPartial } : T +type QueryKeyProvider = { + queryKey: () => readonly unknown[] +} + +type AppDslVersionQueryProvider = { + get?: QueryKeyProvider +} + +const fallbackAppDslVersionQueryKey = ['console', 'appDslVersion', 'get'] as const + +const getAppDslVersionQueryKey = () => { + const appDslVersionQuery = (consoleQuery as { appDslVersion?: AppDslVersionQueryProvider }).appDslVersion + + return appDslVersionQuery?.get?.queryKey() ?? fallbackAppDslVersionQueryKey +} + const buildSystemFeatures = ( overrides: DeepPartial = {}, ): SystemFeatures => { @@ -70,6 +86,13 @@ export const seedSystemFeatures = ( return data } +export const seedAppDslVersion = ( + queryClient: QueryClient, + appDslVersion = '0.6.0', +) => { + queryClient.setQueryData(getAppDslVersionQueryKey(), { app_dsl_version: appDslVersion }) +} + type SystemFeaturesTestOptions = { /** * Partial overrides for the systemFeatures payload. When omitted, the cache @@ -78,6 +101,11 @@ type SystemFeaturesTestOptions = { * keep the systemFeatures query in the pending state. */ systemFeatures?: DeepPartial | null + /** + * Seed the workflow clipboard DSL version query only for tests that need it. + * Omit or pass `null` to leave it unseeded. + */ + appDslVersion?: string | null queryClient?: QueryClient } @@ -94,6 +122,8 @@ export const createSystemFeaturesWrapper = ( const systemFeatures = options.systemFeatures === null ? null : seedSystemFeatures(queryClient, options.systemFeatures) + if (options.appDslVersion !== undefined && options.appDslVersion !== null) + seedAppDslVersion(queryClient, options.appDslVersion) const wrapper = ({ children }: { children: ReactNode }) => ( {children} ) @@ -104,9 +134,10 @@ export const renderWithSystemFeatures = ( ui: ReactElement, options: SystemFeaturesTestOptions & Omit = {}, ): RenderResult & { queryClient: QueryClient, systemFeatures: SystemFeatures | null } => { - const { systemFeatures: sf, queryClient: qc, ...renderOptions } = options + const { systemFeatures: sf, appDslVersion, queryClient: qc, ...renderOptions } = options const { wrapper, queryClient, systemFeatures } = createSystemFeaturesWrapper({ systemFeatures: sf, + appDslVersion, queryClient: qc, }) const rendered = render(ui, { wrapper, ...renderOptions }) @@ -117,9 +148,10 @@ export const renderHookWithSystemFeatures = ( callback: (props: Props) => Result, options: SystemFeaturesTestOptions & Omit, 'wrapper'> = {}, ): RenderHookResult & { queryClient: QueryClient, systemFeatures: SystemFeatures | null } => { - const { systemFeatures: sf, queryClient: qc, ...hookOptions } = options + const { systemFeatures: sf, appDslVersion, queryClient: qc, ...hookOptions } = options const { wrapper, queryClient, systemFeatures } = createSystemFeaturesWrapper({ systemFeatures: sf, + appDslVersion, queryClient: qc, }) const rendered = renderHook(callback, { wrapper, ...hookOptions }) diff --git a/web/app/components/workflow/__tests__/workflow-test-env.tsx b/web/app/components/workflow/__tests__/workflow-test-env.tsx index b88600331a..ff18b304e5 100644 --- a/web/app/components/workflow/__tests__/workflow-test-env.tsx +++ b/web/app/components/workflow/__tests__/workflow-test-env.tsx @@ -69,7 +69,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { render, renderHook } from '@testing-library/react' import * as React from 'react' import ReactFlow, { ReactFlowProvider } from 'reactflow' -import { seedSystemFeatures } from '@/__tests__/utils/mock-system-features' +import { seedAppDslVersion, seedSystemFeatures } from '@/__tests__/utils/mock-system-features' import { WorkflowContext } from '../context' import { HooksStoreContext } from '../hooks-store/provider' import { createHooksStore } from '../hooks-store/store' @@ -168,6 +168,8 @@ function createWorkflowWrapper( }) if (!externalQueryClient) seedSystemFeatures(queryClient) + if (!externalQueryClient) + seedAppDslVersion(queryClient) return ({ children }: { children: React.ReactNode }) => { let inner: React.ReactNode = children diff --git a/web/app/components/workflow/hooks/use-nodes-interactions.ts b/web/app/components/workflow/hooks/use-nodes-interactions.ts index 0b436c9266..5b8dc1a14e 100644 --- a/web/app/components/workflow/hooks/use-nodes-interactions.ts +++ b/web/app/components/workflow/hooks/use-nodes-interactions.ts @@ -14,7 +14,7 @@ import type { VariableAssignerNodeType } from '../nodes/variable-assigner/types' import type { Edge, Node, OnNodeAdd } from '../types' import type { RAGPipelineVariables } from '@/models/pipeline' import { toast } from '@langgenius/dify-ui/toast' -import { useSuspenseQuery } from '@tanstack/react-query' +import { useQuery } from '@tanstack/react-query' import { produce } from 'immer' import { useCallback, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -23,7 +23,7 @@ import { getOutgoers, useReactFlow, } from 'reactflow' -import { systemFeaturesQueryOptions } from '@/service/system-features' +import { consoleQuery } from '@/service/client' import { collaborationManager } from '../collaboration/core/collaboration-manager' import { CUSTOM_EDGE, @@ -145,10 +145,10 @@ const isNoteLinkClickTarget = (target: EventTarget | null, node: Node) => { export const useNodesInteractions = () => { const { t } = useTranslation() - const { data: appDslVersion } = useSuspenseQuery({ - ...systemFeaturesQueryOptions(), - select: s => s.app_dsl_version, - }) + const { data: appDslVersion = '' } = useQuery(consoleQuery.appDslVersion.get.queryOptions({ + staleTime: Infinity, + select: data => data.app_dsl_version, + })) const collaborativeWorkflow = useCollaborativeWorkflow() const workflowStore = useWorkflowStore() const reactflow = useReactFlow() diff --git a/web/app/components/workflow/hooks/use-panel-interactions.ts b/web/app/components/workflow/hooks/use-panel-interactions.ts index c8a2ddf633..05f47518ec 100644 --- a/web/app/components/workflow/hooks/use-panel-interactions.ts +++ b/web/app/components/workflow/hooks/use-panel-interactions.ts @@ -1,16 +1,16 @@ import type { MouseEvent } from 'react' -import { useSuspenseQuery } from '@tanstack/react-query' +import { useQuery } from '@tanstack/react-query' import { useCallback } from 'react' -import { systemFeaturesQueryOptions } from '@/service/system-features' +import { consoleQuery } from '@/service/client' import { useWorkflowStore } from '../store' import { readWorkflowClipboard } from '../utils' export const usePanelInteractions = () => { const workflowStore = useWorkflowStore() - const { data: appDslVersion } = useSuspenseQuery({ - ...systemFeaturesQueryOptions(), - select: s => s.app_dsl_version, - }) + const { data: appDslVersion = '' } = useQuery(consoleQuery.appDslVersion.get.queryOptions({ + staleTime: Infinity, + select: data => data.app_dsl_version, + })) const handlePaneContextMenu = useCallback((e: MouseEvent) => { e.preventDefault() diff --git a/web/types/feature.ts b/web/types/feature.ts index 77d4045318..8e5ae417c7 100644 --- a/web/types/feature.ts +++ b/web/types/feature.ts @@ -28,7 +28,6 @@ type License = { } export type SystemFeatures = { - app_dsl_version: string trial_models: ModelProviderQuotaGetPaid[] plugin_installation_permission: { plugin_installation_scope: InstallationScope @@ -70,7 +69,6 @@ export type SystemFeatures = { } export const defaultSystemFeatures: SystemFeatures = { - app_dsl_version: '', trial_models: [], plugin_installation_permission: { plugin_installation_scope: InstallationScope.ALL,