From 9b74df21d02b218aa59f4ec7e2600a40f4964192 Mon Sep 17 00:00:00 2001 From: Jingyi Date: Mon, 15 Jun 2026 01:47:15 -0700 Subject: [PATCH] feat(web): refine onboarding UI (#37433) Signed-off-by: dependabot[bot] Co-authored-by: yyh Co-authored-by: Joel Co-authored-by: hjlarry Co-authored-by: fatelei Co-authored-by: Asuka Minato Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Xiyuan Chen <52963600+GareArc@users.noreply.github.com> Co-authored-by: gigglewang Co-authored-by: Yunlu Wen Co-authored-by: chariri Co-authored-by: Evan <2869018789@qq.com> Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com> --- api/commands/__init__.py | 2 + api/commands/plugin.py | 109 +- api/controllers/console/app/app.py | 178 +++- .../console/explore/recommended_app.py | 38 +- api/controllers/console/workspace/plugin.py | 356 +++++-- .../console/workspace/workspace.py | 15 +- api/core/plugin/entities/plugin_daemon.py | 6 + api/core/plugin/impl/plugin.py | 12 + api/core/plugin/plugin_service.py | 15 + api/extensions/ext_commands.py | 2 + ...-b7c2d9e8a1f4_add_tenant_last_opened_at.py | 26 + ...c9d012_add_plugin_auto_upgrade_category.py | 42 + ...add_learn_dify_flag_to_recommended_apps.py | 26 + ...6_06_15_1400-c4d5e6f7a8b9_add_app_stars.py | 38 + api/models/__init__.py | 2 + api/models/account.py | 18 +- api/models/model.py | 31 + api/openapi/markdown/console-openapi.md | 409 ++++++-- api/schedule/check_upgradable_plugin_task.py | 1 + api/services/account_service.py | 23 +- api/services/app_service.py | 214 +++- .../entities/model_provider_entities.py | 21 + .../plugin/plugin_auto_upgrade_service.py | 401 ++++++-- .../database/database_retrieval.py | 48 +- api/services/recommended_app_service.py | 33 +- ...ss_tenant_plugin_autoupgrade_check_task.py | 37 +- api/tasks/remove_app_and_related_data_task.py | 14 + .../services/plugin/test_plugin_lifecycle.py | 35 +- .../recommend_app/test_database_retrieval.py | 61 ++ .../services/test_app_service.py | 232 +++++ .../services/test_recommended_app_service.py | 48 + .../console/explore/test_recommended_app.py | 66 ++ .../console/workspace/test_plugin.py | 226 ++++- .../console/workspace/test_workspace.py | 34 +- .../unit_tests/controllers/test_swagger.py | 37 + .../core/plugin/test_plugin_manager.py | 21 + .../test_plugin_auto_upgrade_service.py | 121 ++- .../services/test_account_service.py | 13 +- ...ss_tenant_plugin_autoupgrade_check_task.py | 32 +- .../test_remove_app_and_related_data_task.py | 22 + .../smoke/authenticated-entry.feature | 1 - e2e/features/smoke/install.feature | 1 - .../step-definitions/apps/create-app.steps.ts | 6 +- .../step-definitions/apps/delete-app.steps.ts | 2 +- .../apps/duplicate-app.steps.ts | 8 +- .../step-definitions/auth/sign-in.steps.ts | 2 +- .../step-definitions/common/app.steps.ts | 7 +- .../common/navigation.steps.ts | 3 +- e2e/fixtures/auth.ts | 176 ++-- e2e/support/apps.ts | 24 + eslint-suppressions.json | 297 +----- .../generated/api/console/apps/orpc.gen.ts | 554 +++++----- .../generated/api/console/apps/types.gen.ts | 72 ++ .../generated/api/console/apps/zod.gen.ts | 54 + .../generated/api/console/explore/orpc.gen.ts | 28 +- .../api/console/explore/types.gen.ts | 20 + .../generated/api/console/explore/zod.gen.ts | 16 + .../api/console/workspaces/orpc.gen.ts | 338 ++++--- .../api/console/workspaces/types.gen.ts | 357 +++++-- .../api/console/workspaces/zod.gen.ts | 346 +++++-- packages/dify-ui/src/styles/utilities.css | 6 + packages/dify-ui/src/themes/dark.css | 13 + packages/dify-ui/src/themes/light.css | 13 + packages/dify-ui/src/themes/theme.css | 13 + packages/iconify-collections/README.md | 43 + .../integrations/agent-strategy-active.svg | 3 + .../vender/integrations/agent-strategy.svg | 3 + .../integrations/api-extension-active.svg | 10 + .../vender/integrations/api-extension.svg | 3 + .../integrations/custom-tool-active.svg | 3 + .../vender/integrations/custom-tool.svg | 3 + .../vender/integrations/extension-active.svg | 3 + .../assets/vender/integrations/extension.svg | 3 + .../vender/integrations/install-drop.svg | 3 + .../vender/integrations/install-github.svg | 3 + .../integrations/install-local-package.svg | 3 + .../integrations/install-marketplace.svg | 3 + .../assets/vender/integrations/mcp.svg | 6 + .../assets/vender/integrations/panel-left.svg | 3 + .../vender/integrations/tools-active.svg | 3 + .../assets/vender/integrations/tools.svg | 3 + .../vender/integrations/trigger-active.svg | 10 + .../assets/vender/integrations/trigger.svg | 10 + .../integrations/workflow-as-tool-active.svg | 9 + .../vender/integrations/workflow-as-tool.svg | 3 + .../assets/vender/main-nav/app-home.svg | 4 + .../assets/vender/main-nav/credits.svg | 5 + .../assets/vender/main-nav/help.svg | 4 + .../assets/vender/main-nav/home-active.svg | 6 + .../assets/vender/main-nav/home.svg | 4 + .../vender/main-nav/integrations-active.svg | 7 + .../assets/vender/main-nav/integrations.svg | 5 + .../vender/main-nav/knowledge-active.svg | 5 + .../assets/vender/main-nav/knowledge.svg | 6 + .../vender/main-nav/marketplace-active.svg | 5 + .../assets/vender/main-nav/marketplace.svg | 3 + .../assets/vender/main-nav/quick-search.svg | 4 + .../assets/vender/main-nav/studio-active.svg | 8 + .../assets/vender/main-nav/studio.svg | 6 + .../vender/main-nav/workspace-settings.svg | 5 + .../custom-public/icons.json | 270 +++-- .../custom-vender/icons.json | 522 ++++++++-- .../custom-vender/info.json | 2 +- packages/iconify-collections/package.json | 1 + .../scripts/check-icon-dimensions.ts | 94 ++ .../scripts/generate-collections.ts | 12 +- pnpm-lock.yaml | 137 ++- pnpm-workspace.yaml | 2 + web/__mocks__/provider-context.ts | 1 + .../app-sidebar/sidebar-shell-flow.test.tsx | 9 +- web/__tests__/app-star-i18n.test.ts | 47 + .../apps/app-card-operations-flow.test.tsx | 16 +- .../apps/app-list-browsing-flow.test.tsx | 62 +- web/__tests__/apps/create-app-flow.test.tsx | 58 +- .../base/notion-page-selector-flow.test.tsx | 3 + .../billing/billing-integration.test.tsx | 8 + .../education-verification-flow.test.tsx | 8 + .../custom/custom-page-flow.test.tsx | 8 + .../explore/explore-app-list-flow.test.tsx | 109 +- .../explore/installed-app-flow.test.tsx | 4 +- .../explore/sidebar-lifecycle-flow.test.tsx | 1 + .../plugins/plugin-page-shell-flow.test.tsx | 19 + .../tools/provider-list-shell-flow.test.tsx | 2 +- .../tool-browsing-and-filtering.test.tsx | 34 +- .../__tests__/hydration-boundary.spec.tsx | 10 + .../[appId]/__tests__/layout-main.spec.tsx | 117 +++ .../(appDetailLayout)/[appId]/layout-main.tsx | 151 +-- .../__tests__/layout-main.spec.tsx | 33 +- .../[datasetId]/layout-main.tsx | 111 +- web/app/(commonLayout)/error.tsx | 2 +- web/app/(commonLayout)/explore/apps/page.tsx | 25 +- .../installed/[appId]/__tests__/page.spec.tsx | 19 + .../explore/installed/[appId]/page.tsx | 8 +- web/app/(commonLayout)/hydration-boundary.tsx | 2 +- .../installed/[appId]/__tests__/page.spec.tsx | 23 + .../(commonLayout)/installed/[appId]/page.tsx | 18 + .../integrations/[[...slug]]/not-found.tsx | 7 + .../integrations/[[...slug]]/page.tsx | 29 + .../(commonLayout)/integrations/layout.tsx | 12 + web/app/(commonLayout)/layout.tsx | 14 +- .../marketplace/__tests__/page.spec.tsx | 23 + web/app/(commonLayout)/marketplace/layout.tsx | 12 + web/app/(commonLayout)/marketplace/page.tsx | 21 + web/app/(commonLayout)/page.tsx | 15 + web/app/(commonLayout)/plugins/page.tsx | 16 +- web/app/(commonLayout)/role-route-guard.tsx | 2 +- web/app/(commonLayout)/tools/page.tsx | 27 +- web/app/account/(commonLayout)/header.tsx | 13 +- web/app/auth/refresh/__tests__/route.spec.ts | 15 +- web/app/auth/refresh/route.ts | 2 +- .../__tests__/app-detail-section.spec.tsx | 154 +++ .../__tests__/app-detail-top.spec.tsx | 73 ++ .../__tests__/dataset-detail-section.spec.tsx | 90 ++ .../__tests__/dataset-detail-top.spec.tsx | 77 ++ .../app-sidebar/__tests__/index.spec.tsx | 84 -- .../app-sidebar/app-detail-section.tsx | 162 +++ .../components/app-sidebar/app-detail-top.tsx | 105 ++ .../__tests__/app-info-trigger.spec.tsx | 10 +- .../app-info/__tests__/index.spec.tsx | 26 +- .../app-info/app-info-detail-drawer.tsx | 48 +- .../app-sidebar/app-info/app-info-trigger.tsx | 46 +- .../components/app-sidebar/app-info/index.tsx | 11 +- .../app-sidebar/dataset-detail-section.tsx | 145 +++ .../app-sidebar/dataset-detail-top.tsx | 113 +++ .../dataset-info/__tests__/index.spec.tsx | 5 +- .../app-sidebar/dataset-info/dropdown.tsx | 5 +- .../app-sidebar/dataset-info/index.tsx | 98 +- web/app/components/app-sidebar/index.tsx | 38 +- .../nav-link/__tests__/index.spec.tsx | 115 ++- .../components/app-sidebar/nav-link/index.tsx | 41 +- .../components/app-sidebar/toggle-button.tsx | 12 +- .../app/annotation/__tests__/index.spec.tsx | 7 + web/app/components/app/annotation/index.tsx | 12 +- .../app-publisher/__tests__/index.spec.tsx | 8 +- .../components/app/app-publisher/index.tsx | 3 +- .../dataset-config/settings-modal/index.tsx | 6 +- .../configuration/hooks/use-configuration.ts | 8 +- .../app-list/__tests__/index.spec.tsx | 5 + .../app-list/__tests__/sidebar.spec.tsx | 1 - .../app/create-app-dialog/app-list/index.tsx | 3 + .../create-app-dialog/app-list/sidebar.tsx | 7 +- .../create-app-modal/__tests__/index.spec.tsx | 5 + .../components/app/create-app-modal/index.tsx | 5 +- .../__tests__/index.spec.tsx | 143 ++- .../app/create-from-dsl-modal/index.tsx | 4 + .../log-annotation/__tests__/index.spec.tsx | 47 +- .../components/app/log-annotation/index.tsx | 28 +- .../app/log-annotation/page-title.tsx | 41 + .../app/log/__tests__/index.spec.tsx | 7 + web/app/components/app/log/index.tsx | 12 +- .../__tests__/cloud.spec.tsx | 9 +- .../__tests__/index.spec.tsx | 9 +- .../apikey-info-panel.test-utils.tsx | 24 +- .../app/overview/apikey-info-panel/index.tsx | 6 +- .../settings/__tests__/index.spec.tsx | 8 + .../app/overview/settings/index.tsx | 6 +- .../apps/__tests__/app-card.spec.tsx | 129 ++- .../apps/__tests__/creators-filter.spec.tsx | 4 +- .../components/apps/__tests__/empty.spec.tsx | 13 +- .../components/apps/__tests__/footer.spec.tsx | 93 -- .../components/apps/__tests__/list.spec.tsx | 457 +++++++-- .../apps/__tests__/new-app-card.spec.tsx | 6 +- web/app/components/apps/app-card-skeleton.tsx | 8 +- web/app/components/apps/app-card.tsx | 772 +++++++++++--- .../apps/app-list-creation-modals.tsx | 90 ++ .../apps/app-list-header-filters.tsx | 140 +++ .../apps/app-list-tag-management-modal.tsx | 26 + web/app/components/apps/app-sort-filter.tsx | 65 ++ web/app/components/apps/app-type-filter.tsx | 5 +- web/app/components/apps/constants.ts | 1 + web/app/components/apps/creators-filter.tsx | 11 +- web/app/components/apps/empty.tsx | 28 +- .../apps/first-empty-state/action-card.tsx | 157 +++ .../apps/first-empty-state/index.tsx | 116 +++ web/app/components/apps/footer.tsx | 49 - web/app/components/apps/list.tsx | 241 +++-- web/app/components/apps/new-app-card.tsx | 85 +- web/app/components/apps/starred-app-card.tsx | 62 ++ web/app/components/apps/starred-app-list.tsx | 50 + .../components/apps/studio-list-header.tsx | 20 + .../components/base/__tests__/badge.spec.tsx | 7 + .../base/__tests__/page-unavailable.spec.tsx | 17 + .../base/audio-btn/__tests__/index.spec.tsx | 4 +- web/app/components/base/audio-btn/index.tsx | 3 +- web/app/components/base/badge.tsx | 8 +- web/app/components/base/carousel/index.tsx | 2 + .../base/chat/chat/__tests__/hooks.spec.tsx | 2 +- web/app/components/base/chat/chat/hooks.ts | 3 +- .../header/__tests__/index.spec.tsx | 4 +- .../base/chip/__tests__/index.spec.tsx | 17 + web/app/components/base/chip/index.tsx | 9 +- .../components/base/corner-label/index.tsx | 8 +- .../components/base/create-resource-card.tsx | 43 + .../moderation-setting-modal.spec.tsx | 4 +- .../moderation/moderation-setting-modal.tsx | 11 +- .../base/filter-empty-state/index.tsx | 37 + .../base/icons/src/vender/Annotations.json | 26 + .../base/icons/src/vender/Annotations.tsx | 17 + .../src/vender/SidebarLeftArrowIcon.json | 35 + .../icons/src/vender/SidebarLeftArrowIcon.tsx | 17 + .../base/icons/src/vender/Star.json | 22 + .../components/base/icons/src/vender/Star.tsx | 17 + .../vender/line/alertsAndFeedback/index.ts | 1 - .../vender/line/financeAndECommerce/index.ts | 1 - .../src/vender/line/general/HelpQuestion.json | 51 + .../src/vender/line/general/HelpQuestion.tsx | 20 + .../base/icons/src/vender/plugin/Plugin.json | 65 ++ .../base/icons/src/vender/plugin/Plugin.tsx | 20 + .../base/logo/__tests__/dify-logo.spec.tsx | 16 +- web/app/components/base/logo/dify-logo.tsx | 2 +- .../new-audio-button/__tests__/index.spec.tsx | 2 +- .../base/new-audio-button/index.tsx | 3 +- .../__tests__/base.spec.tsx | 6 +- .../base/notion-page-selector/base.tsx | 8 +- web/app/components/base/page-unavailable.tsx | 28 + .../__tests__/search-state.spec.ts | 43 + .../base/search-input/search-state.ts | 23 + .../base/voice-input/__tests__/index.spec.tsx | 6 +- web/app/components/base/voice-input/index.tsx | 3 +- .../__tests__/index.spec.tsx | 8 + .../__tests__/index.spec.tsx | 8 + .../billing/plan/__tests__/index.spec.tsx | 25 + web/app/components/billing/plan/index.tsx | 7 +- .../__tests__/index.spec.tsx | 8 + .../upgrade-btn/__tests__/index.spec.tsx | 18 + .../components/billing/upgrade-btn/index.tsx | 5 + .../custom-page/__tests__/index.spec.tsx | 8 + .../components/custom/custom-page/index.tsx | 3 +- .../__tests__/powered-by-brand.spec.tsx | 2 +- .../datasets/create/__tests__/index.spec.tsx | 3 + web/app/components/datasets/create/index.tsx | 8 +- .../create/step-one/__tests__/index.spec.tsx | 1 + .../step-one/__tests__/upgrade-card.spec.tsx | 8 + .../datasets/create/step-one/upgrade-card.tsx | 4 + .../create/website/__tests__/index.spec.tsx | 18 +- .../firecrawl/__tests__/index.spec.tsx | 22 +- .../create/website/firecrawl/index.tsx | 8 +- .../datasets/create/website/index.tsx | 8 +- .../jina-reader/__tests__/index.spec.tsx | 19 +- .../create/website/jina-reader/index.tsx | 8 +- .../watercrawl/__tests__/index.spec.tsx | 19 +- .../create/website/watercrawl/index.tsx | 8 +- .../components/__tests__/operations.spec.tsx | 41 +- .../documents/components/operations.tsx | 59 +- .../online-documents/__tests__/index.spec.tsx | 3 + .../data-source/online-documents/index.tsx | 8 +- .../online-drive/__tests__/index.spec.tsx | 3 + .../data-source/online-drive/index.tsx | 8 +- .../website-crawl/__tests__/index.spec.tsx | 3 + .../data-source/website-crawl/index.tsx | 8 +- .../steps/__tests__/step-one-content.spec.tsx | 8 + .../__tests__/document-settings.spec.tsx | 24 +- .../detail/settings/document-settings.tsx | 26 +- .../extra-info/__tests__/statistics.spec.tsx | 8 + .../api-access/__tests__/index.spec.tsx | 7 + .../datasets/extra-info/api-access/index.tsx | 4 +- .../service-api/__tests__/index.spec.tsx | 8 +- .../datasets/extra-info/service-api/index.tsx | 6 +- .../datasets/extra-info/statistics.tsx | 19 +- .../datasets/list/__tests__/datasets.spec.tsx | 416 ++------ .../datasets/list/__tests__/header.spec.tsx | 64 ++ .../datasets/list/__tests__/index.spec.tsx | 126 ++- .../dataset-card/__tests__/index.spec.tsx | 4 +- .../__tests__/corner-labels.spec.tsx | 11 +- .../__tests__/dataset-card-header.spec.tsx | 23 +- .../dataset-card/components/corner-labels.tsx | 10 - .../components/dataset-card-header.tsx | 35 +- .../datasets/list/dataset-card/index.tsx | 2 +- .../dataset-footer/__tests__/index.spec.tsx | 52 - .../datasets/list/dataset-footer/index.tsx | 25 - web/app/components/datasets/list/datasets.tsx | 52 +- .../__tests__/index.spec.tsx | 19 + .../datasets/list/first-empty-state/index.tsx | 99 ++ web/app/components/datasets/list/header.tsx | 146 +++ web/app/components/datasets/list/index.tsx | 118 ++- .../new-dataset-card/__tests__/index.spec.tsx | 47 +- .../__tests__/option.spec.tsx | 7 +- .../datasets/list/new-dataset-card/index.tsx | 45 +- .../datasets/list/new-dataset-card/option.tsx | 15 +- .../explore/__tests__/category.spec.tsx | 14 +- .../explore/__tests__/index.spec.tsx | 29 +- .../explore/app-card/__tests__/index.spec.tsx | 69 +- web/app/components/explore/app-card/index.tsx | 68 +- .../explore/app-list/__tests__/index.spec.tsx | 437 +++++++- .../app-list/explore-app-list-header.tsx | 59 ++ .../app-list/explore-recommendations.tsx | 33 + web/app/components/explore/app-list/index.tsx | 418 ++++---- .../explore/app-list/loading-skeletons.tsx | 152 +++ .../explore/app-list/style.module.css | 10 +- .../banner/__tests__/banner-item.spec.tsx | 55 +- .../explore/banner/__tests__/banner.spec.tsx | 197 ++-- .../components/explore/banner/banner-item.tsx | 104 +- web/app/components/explore/banner/banner.tsx | 111 +- web/app/components/explore/category.tsx | 62 +- .../explore/continue-work/index.tsx | 47 + .../components/explore/continue-work/item.tsx | 56 + .../explore/create-app-modal/index.tsx | 12 +- web/app/components/explore/index.tsx | 6 +- .../installed-app/__tests__/index.spec.tsx | 4 +- .../installed-app/__tests__/routes.spec.ts | 23 + .../explore/installed-app/index.tsx | 88 +- .../explore/installed-app/routes.ts | 18 + .../item-operation/__tests__/index.spec.tsx | 6 +- .../explore/item-operation/index.tsx | 12 +- .../explore/item-operation/style.module.css | 23 - .../learn-dify/__tests__/item.spec.tsx | 148 +++ .../components/explore/learn-dify/atoms.ts | 23 + .../components/explore/learn-dify/index.tsx | 151 +++ .../components/explore/learn-dify/item.tsx | 88 ++ .../explore/sidebar/__tests__/index.spec.tsx | 1 + .../app-nav-item/__tests__/index.spec.tsx | 32 +- .../explore/sidebar/app-nav-item/index.tsx | 51 +- web/app/components/explore/sidebar/index.tsx | 7 +- web/app/components/full-screen-loading.tsx | 2 +- .../__tests__/command-selector.spec.tsx | 14 +- .../goto-anything/__tests__/index.spec.tsx | 2 +- .../actions/__tests__/index.spec.ts | 2 +- .../actions/commands/__tests__/slash.spec.tsx | 7 +- .../actions/commands/__tests__/zen.spec.ts | 80 -- .../goto-anything/actions/commands/index.ts | 7 - .../actions/commands/slash-provider.tsx | 59 ++ .../goto-anything/actions/commands/slash.tsx | 61 -- .../goto-anything/actions/commands/zen.tsx | 59 -- .../components/goto-anything/actions/index.ts | 5 +- .../goto-anything/command-selector.tsx | 54 +- .../components/goto-anything/hooks/index.ts | 7 - web/app/components/goto-anything/index.tsx | 12 +- .../header/__tests__/header-wrapper.spec.tsx | 59 +- .../header/__tests__/index.spec.tsx | 4 - .../account-dropdown/__tests__/index.spec.tsx | 44 + .../__tests__/support.spec.tsx | 212 ---- .../header/account-dropdown/compliance.tsx | 5 +- .../account-dropdown/default-menu-content.tsx | 231 +++++ .../header/account-dropdown/index.tsx | 278 ++--- .../main-nav-menu-content.tsx | 155 +++ .../account-dropdown/menu-item-content.tsx | 4 +- .../header/account-dropdown/support.tsx | 86 -- .../__tests__/index.spec.tsx | 88 +- .../workplace-selector/index.tsx | 12 +- .../__tests__/constants.spec.ts | 31 +- .../account-setting/__tests__/index.spec.tsx | 74 +- .../use-integrations-setting.spec.ts | 43 + .../__tests__/index.spec.tsx | 99 +- .../api-based-extension-page/empty.tsx | 32 +- .../api-based-extension-page/index.tsx | 109 +- .../api-based-extension-page/selector.tsx | 128 ++- .../header/account-setting/constants.ts | 53 +- .../__tests__/card.spec.tsx | 10 +- .../__tests__/index.spec.tsx | 183 +++- .../install-from-marketplace.spec.tsx | 15 +- .../__tests__/item.spec.tsx | 20 +- .../__tests__/plugin-actions.spec.tsx | 154 +++ .../data-source-page-new/card.tsx | 83 +- .../data-source-page-new/configure.tsx | 1 + .../data-source-page-new/index.tsx | 181 +++- .../install-from-marketplace.tsx | 14 +- .../data-source-page-new/item.tsx | 59 +- .../data-source-page-new/plugin-actions.tsx | 162 +++ .../header/account-setting/destinations.ts | 31 + .../header/account-setting/index.tsx | 167 +-- .../account-setting/language-page/index.tsx | 54 +- .../account-setting/members-page/index.tsx | 8 +- .../header/account-setting/menu-dialog.tsx | 4 +- .../__tests__/index.non-cloud.spec.tsx | 48 + .../__tests__/index.spec.tsx | 294 +++++- .../install-from-marketplace.spec.tsx | 15 +- .../model-provider-page/index.tsx | 184 ++-- .../install-from-marketplace.tsx | 19 +- .../__tests__/credential-item.spec.tsx | 15 +- .../model-auth/authorized/credential-item.tsx | 22 +- .../model-auth/hooks/use-credential-status.ts | 7 +- .../model-name/__tests__/index.spec.tsx | 8 + .../model-provider-page/model-name/index.tsx | 4 +- .../model-provider-page-body.tsx | 175 ++++ .../model-selector/__tests__/index.spec.tsx | 21 +- .../__tests__/model-selector-trigger.spec.tsx | 12 + .../__tests__/popup-item.spec.tsx | 14 +- .../model-selector/__tests__/popup.spec.tsx | 46 +- .../model-selector/index.tsx | 14 + .../model-selector/marketplace-section.tsx | 5 +- .../model-selector/model-selector-trigger.tsx | 9 +- .../model-selector/popup-item.tsx | 1 + .../model-selector/popup.tsx | 19 +- .../__tests__/model-list-item.spec.tsx | 3 +- .../__tests__/provider-card-actions.spec.tsx | 11 +- .../use-credential-panel-state.spec.ts | 41 +- .../__tests__/use-trial-credits.spec.ts | 61 +- .../provider-added-card/credential-panel.tsx | 62 +- .../provider-added-card/index.tsx | 120 ++- .../credits-exhausted-alert.spec.tsx | 8 + .../__tests__/index.spec.tsx | 4 +- .../model-auth-dropdown/button-config.ts | 24 + .../credits-exhausted-alert.tsx | 26 +- .../model-auth-dropdown/index.tsx | 29 +- .../provider-added-card/model-list-item.tsx | 1 + .../model-load-balancing-configs.tsx | 6 +- .../provider-card-actions.tsx | 1 + .../provider-added-card/quota-panel.tsx | 84 +- .../provider-added-card/system-quota-card.tsx | 4 +- .../provider-added-card/use-trial-credits.ts | 29 +- .../__tests__/index.spec.tsx | 53 +- .../system-model-selector/index.tsx | 37 +- .../update-setting-dialog-form.tsx | 192 ++++ .../account-setting/update-setting-dialog.tsx | 283 ++++++ .../update-setting-option-card.tsx | 44 + .../use-integrations-setting.ts | 25 + .../explore-nav/__tests__/index.spec.tsx | 9 +- .../components/header/explore-nav/index.tsx | 7 +- web/app/components/header/header-wrapper.tsx | 1 - web/app/components/header/index.tsx | 9 +- .../plan-badge/__tests__/index.spec.tsx | 24 + .../components/header/plan-badge/index.tsx | 3 +- .../__tests__/downloading-icon.spec.tsx | 11 +- .../plugins-nav/downloading-icon.module.css | 44 - .../header/plugins-nav/downloading-icon.tsx | 24 +- .../header/tools-nav/__tests__/index.spec.tsx | 11 +- web/app/components/header/tools-nav/index.tsx | 5 +- .../integrations/__tests__/page.spec.tsx | 735 ++++++++++++++ .../__tests__/plugin-category-page.spec.tsx | 137 +++ .../integrations/__tests__/routes.spec.ts | 118 +++ .../__tests__/tool-provider-card.spec.tsx | 101 ++ .../__tests__/tool-provider-list.spec.tsx | 888 ++++++++++++++++ .../integrations/hooks/use-integration-nav.ts | 154 +++ .../hooks/use-integration-permissions.ts | 45 + .../hooks/use-integration-section.ts | 17 + .../hooks/use-tool-marketplace-panel.ts | 54 + .../hooks/use-tool-provider-category.ts | 34 + .../components/integrations/page-header.tsx | 51 + web/app/components/integrations/page.tsx | 315 ++++++ .../integrations/permission-quick-panel.tsx | 92 ++ .../integrations/plugin-category-page.tsx | 115 +++ web/app/components/integrations/routes.ts | 196 ++++ .../integrations/section-layout.tsx | 32 + .../integrations/section-renderer.tsx | 105 ++ .../integrations/sidebar-actions.tsx | 193 ++++ .../integrations/sidebar-nav-item-styles.ts | 4 + .../integrations/sidebar-nav-item.tsx | 107 ++ .../integrations/tool-provider-card.tsx | 143 +++ .../tool-provider-create-action.tsx | 32 + .../integrations/tool-provider-list.tsx | 274 +++++ .../integrations/tool-provider-toolbar.tsx | 81 ++ .../main-nav/__tests__/index.spec.tsx | 957 ++++++++++++++++++ .../main-nav/__tests__/layout.spec.tsx | 37 + .../__tests__/support-menu.spec.tsx | 38 + .../__tests__/workspace-card.spec.tsx | 335 ++++++ .../main-nav/components/account-section.tsx | 39 + .../main-nav/components/help-menu.tsx | 171 ++++ .../main-nav/components/nav-link.tsx | 59 ++ .../main-nav/components/search-button.tsx | 29 + .../main-nav/components/support-menu.tsx | 36 + .../main-nav/components/web-apps-section.tsx | 300 ++++++ .../main-nav/components/workspace-card.tsx | 323 ++++++ .../components/workspace-menu-content.tsx | 38 + .../components/workspace-plan-badge.tsx | 22 + .../components/workspace-switcher.tsx | 179 ++++ web/app/components/main-nav/index.tsx | 337 ++++++ web/app/components/main-nav/layout.tsx | 23 + web/app/components/main-nav/types.ts | 11 + web/app/components/main-nav/utils.ts | 5 + .../plugins/__tests__/plugin-routes.spec.ts | 39 + .../card/base/__tests__/corner-mark.spec.tsx | 8 + .../plugins/card/base/corner-mark.tsx | 7 +- .../plugins/card/base/description.tsx | 2 +- .../components/plugins/card/base/title.tsx | 2 +- .../plugins/card/card-more-info.tsx | 26 +- web/app/components/plugins/card/index.tsx | 78 +- .../base/__tests__/check-task-status.spec.ts | 24 + .../base/__tests__/installed.spec.tsx | 34 + .../install-plugin/base/check-task-status.ts | 3 +- .../plugins/install-plugin/base/installed.tsx | 75 +- .../hooks/__tests__/use-hide-logic.spec.ts | 11 + .../use-install-plugin-limit.spec.ts | 13 + .../__tests__/use-refresh-plugin-list.spec.ts | 21 +- .../install-plugin/hooks/use-hide-logic.ts | 1 + .../hooks/use-install-plugin-limit.tsx | 25 +- .../hooks/use-refresh-plugin-list.tsx | 22 +- .../install-bundle/__tests__/index.spec.tsx | 10 +- .../__tests__/ready-to-install.spec.tsx | 15 +- .../install-plugin/install-bundle/index.tsx | 7 +- .../install-bundle/ready-to-install.tsx | 7 +- .../steps/__tests__/install.spec.tsx | 10 + .../steps/__tests__/installed.spec.tsx | 7 +- .../install-bundle/steps/install.tsx | 19 +- .../install-bundle/steps/installed.tsx | 17 +- .../install-from-github/index.tsx | 10 +- .../steps/__tests__/loaded.spec.tsx | 6 +- .../install-from-github/steps/loaded.tsx | 22 +- .../__tests__/index.spec.tsx | 6 +- .../install-from-local-package/index.tsx | 12 +- .../ready-to-install.tsx | 5 +- .../steps/__tests__/install.spec.tsx | 2 +- .../steps/install.tsx | 7 +- .../__tests__/index.spec.tsx | 8 +- .../install-from-marketplace/index.tsx | 14 +- .../steps/__tests__/install.spec.tsx | 48 +- .../steps/install.tsx | 37 +- .../marketplace/__tests__/index.spec.tsx | 27 +- .../__tests__/plugin-type-switch.spec.tsx | 34 + .../description/__tests__/index.spec.tsx | 67 +- .../plugins/marketplace/description/index.tsx | 366 +++++-- .../components/plugins/marketplace/index.tsx | 17 +- .../list/__tests__/card-wrapper.spec.tsx | 19 +- .../marketplace/list/__tests__/index.spec.tsx | 25 +- .../__tests__/list-with-collection.spec.tsx | 107 ++ .../plugins/marketplace/list/card-wrapper.tsx | 38 +- .../plugins/marketplace/list/carousel.tsx | 189 ++++ .../marketplace/list/collection-constants.ts | 16 + .../plugins/marketplace/list/index.tsx | 5 +- .../marketplace/list/list-with-collection.tsx | 196 +++- .../plugins/marketplace/list/list-wrapper.tsx | 47 +- .../marketplace/plugin-type-switch.tsx | 93 +- .../plugins/marketplace/search-box/index.tsx | 44 +- .../search-box/search-box-wrapper.tsx | 31 +- .../components/plugins/marketplace/utils.ts | 13 + .../authorize/add-oauth-button.tsx | 1 - .../plugins/plugin-auth/authorized/index.tsx | 2 +- .../__tests__/detail-header.spec.tsx | 70 +- .../__tests__/endpoint-modal.spec.tsx | 11 +- .../__tests__/index.spec.tsx | 11 +- .../__tests__/operation-dropdown.spec.tsx | 66 +- .../__tests__/strategy-detail.spec.tsx | 11 +- .../app-selector/app-picker.tsx | 2 +- .../app-selector/app-trigger.tsx | 2 +- .../components/header-modals.tsx | 4 +- .../__tests__/use-plugin-operations.spec.ts | 24 +- .../hooks/use-detail-header-state.ts | 5 +- .../hooks/use-plugin-operations.ts | 18 +- .../detail-header/index.tsx | 16 +- .../plugin-detail-panel/endpoint-card.tsx | 8 +- .../plugin-detail-panel/endpoint-list.tsx | 27 +- .../plugin-detail-panel/endpoint-modal.tsx | 2 +- .../plugins/plugin-detail-panel/index.tsx | 2 +- .../operation-dropdown.tsx | 47 +- .../plugin-detail-panel/strategy-detail.tsx | 2 +- .../plugin-detail-panel/strategy-item.tsx | 2 +- .../components/reasoning-config-form.tsx | 2 +- .../__tests__/event-detail-drawer.spec.tsx | 11 +- .../trigger/event-detail-drawer.tsx | 2 +- .../trigger/event-list.tsx | 2 +- .../plugin-item/__tests__/action.spec.tsx | 20 + .../plugin-item/__tests__/index.spec.tsx | 65 +- .../components/plugins/plugin-item/action.tsx | 5 +- .../components/plugins/plugin-item/index.tsx | 22 +- .../plugin-page/__tests__/debug-info.spec.tsx | 17 +- .../plugin-page/__tests__/index.spec.tsx | 74 ++ .../install-plugin-dropdown.spec.tsx | 87 +- .../__tests__/plugins-panel.spec.tsx | 385 ++++++- .../__tests__/use-reference-setting.spec.ts | 200 ++-- .../plugins/plugin-page/content-inset.ts | 11 + .../plugins/plugin-page/context-provider.tsx | 4 +- .../plugins/plugin-page/debug-info.tsx | 97 +- .../empty/__tests__/index.spec.tsx | 98 ++ .../plugins/plugin-page/empty/index.tsx | 240 +++-- .../__tests__/index.spec.tsx | 32 +- .../__tests__/tag-filter.spec.tsx | 6 +- .../plugin-page/filter-management/index.tsx | 40 +- .../filter-management/tag-filter.tsx | 20 +- .../components/plugins/plugin-page/index.tsx | 28 +- .../plugin-page/install-plugin-dropdown.tsx | 81 +- .../plugin-page/install-source-icons.tsx | 19 + .../plugin-page/list/__tests__/index.spec.tsx | 6 +- .../plugins/plugin-page/list/index.tsx | 8 +- .../plugins/plugin-page/nav-operations.tsx | 123 +++ .../plugin-page/plugin-list-skeleton.tsx | 56 + .../plugin-page/plugin-sidecar-panel.tsx | 46 + .../plugin-tasks/__tests__/hooks.spec.ts | 7 +- .../plugin-tasks/__tests__/index.spec.tsx | 175 ++-- .../__tests__/plugin-section.spec.tsx | 2 +- .../__tests__/plugin-task-list.spec.tsx | 131 +-- .../__tests__/task-status-indicator.spec.tsx | 128 ++- .../plugin-tasks/components/plugin-item.tsx | 2 + .../components/plugin-section.tsx | 22 +- .../components/plugin-task-list.tsx | 171 ++-- .../components/task-status-indicator.tsx | 111 +- .../plugins/plugin-page/plugin-tasks/hooks.ts | 4 +- .../plugin-page/plugin-tasks/index.tsx | 36 +- .../plugin-page/plugins-panel-results.tsx | 166 +++ .../plugin-page/plugins-panel-utils.ts | 28 + .../plugins/plugin-page/plugins-panel.tsx | 230 ++++- .../plugin-page/use-reference-setting.ts | 76 +- web/app/components/plugins/plugin-routes.ts | 71 ++ .../__tests__/index.spec.tsx | 35 - .../__tests__/config.spec.ts | 15 - .../__tests__/index.spec.tsx | 126 +-- .../__tests__/plugins-picker.spec.tsx | 62 +- .../__tests__/strategy-picker.spec.tsx | 47 +- .../__tests__/tool-picker.spec.tsx | 31 +- .../auto-update-setting/config.ts | 10 - .../auto-update-setting/index.tsx | 45 +- .../no-plugin-selected.tsx | 2 +- .../auto-update-setting/plugins-picker.tsx | 58 +- .../auto-update-setting/plugins-selected.tsx | 10 +- .../auto-update-setting/strategy-picker.tsx | 71 +- .../auto-update-setting/tool-item.tsx | 28 +- .../auto-update-setting/tool-picker.tsx | 70 +- .../plugins/reference-setting-modal/index.tsx | 10 +- web/app/components/plugins/types.ts | 21 +- .../__tests__/from-market-place.spec.tsx | 12 +- .../update-plugin/__tests__/index.spec.tsx | 16 +- .../__tests__/plugin-version-picker.spec.tsx | 23 + .../update-plugin/from-market-place.tsx | 28 +- .../update-plugin/plugin-version-picker.tsx | 12 +- .../__tests__/index.spec.tsx | 8 + .../publisher/__tests__/index.spec.tsx | 8 + .../publisher/__tests__/popup.spec.tsx | 7 +- .../rag-pipeline-header/publisher/popup.tsx | 10 +- .../snippet-list/__tests__/index.spec.tsx | 12 +- web/app/components/snippet-list/index.tsx | 88 +- .../tools/__tests__/provider-list.spec.tsx | 507 ---------- web/app/components/tools/content-inset.ts | 13 + .../tools/integrations-setting-modal.tsx | 52 + .../tools/labels/__tests__/filter.spec.tsx | 34 +- .../tools/labels/__tests__/selector.spec.tsx | 33 +- web/app/components/tools/labels/filter.tsx | 25 +- web/app/components/tools/labels/selector.tsx | 4 +- .../marketplace/__tests__/index.spec.tsx | 59 ++ .../components/tools/marketplace/index.tsx | 45 +- .../tools/mcp/__tests__/create-card.spec.tsx | 46 +- .../tools/mcp/__tests__/index.spec.tsx | 60 +- .../tools/mcp/__tests__/modal.spec.tsx | 12 - .../mcp/__tests__/provider-card.spec.tsx | 26 +- web/app/components/tools/mcp/create-card.tsx | 102 +- .../mcp/detail/__tests__/content.spec.tsx | 13 +- .../detail/__tests__/provider-detail.spec.tsx | 11 +- .../components/tools/mcp/detail/content.tsx | 12 +- .../tools/mcp/detail/provider-detail.tsx | 2 +- .../components/tools/mcp/detail/tool-item.tsx | 2 +- .../components/tools/mcp/headers-input.tsx | 2 +- web/app/components/tools/mcp/index.tsx | 55 +- web/app/components/tools/mcp/modal.tsx | 29 +- .../components/tools/mcp/provider-card.tsx | 46 +- .../__tests__/configurations-section.spec.tsx | 33 +- .../mcp/sections/authentication-section.tsx | 2 +- .../mcp/sections/configurations-section.tsx | 28 +- web/app/components/tools/provider-list.tsx | 238 ----- .../__tests__/custom-create-card.spec.tsx | 64 +- .../tools/provider/__tests__/detail.spec.tsx | 105 ++ .../tools/provider/__tests__/empty.spec.tsx | 21 +- .../tools/provider/create-entry-card.tsx | 60 ++ .../tools/provider/custom-create-card.tsx | 82 +- web/app/components/tools/provider/detail.tsx | 41 +- web/app/components/tools/provider/empty.tsx | 103 +- .../tools/provider/tool-card-skeleton.tsx | 92 +- .../components/tools/tool-provider-grid.tsx | 163 +++ web/app/components/tools/types.ts | 1 + .../__tests__/configure-button.spec.tsx | 2 +- .../tools/workflow-tool/configure-button.tsx | 3 +- .../components/tools/workflow-tool/index.tsx | 4 +- .../__tests__/use-workflow-run-utils.spec.ts | 4 + .../hooks/use-workflow-run-utils.ts | 3 +- .../__tests__/all-start-blocks.spec.tsx | 10 +- .../__tests__/all-tools.spec.tsx | 2 +- .../__tests__/featured-tools.spec.tsx | 2 +- .../__tests__/featured-triggers.spec.tsx | 4 +- .../block-selector/__tests__/tools.spec.tsx | 1 + .../block-selector/all-start-blocks.tsx | 4 +- .../workflow/block-selector/all-tools.tsx | 4 +- .../block-selector/featured-tools.tsx | 5 +- .../block-selector/featured-triggers.tsx | 5 +- .../__tests__/item-list.spec.tsx | 10 +- .../market-place-plugin/list.tsx | 6 +- web/app/components/workflow/constants.ts | 6 - .../header/__tests__/editing-title.spec.tsx | 4 - .../workflow/header/__tests__/index.spec.tsx | 69 +- .../workflow/header/editing-title.tsx | 3 +- web/app/components/workflow/header/index.tsx | 7 - .../hooks/__tests__/use-shortcuts.spec.ts | 25 +- .../use-workflow-canvas-maximize.spec.ts | 59 -- .../use-workflow-interactions.spec.tsx | 27 +- .../hooks/use-workflow-canvas-maximize.ts | 28 - .../hooks/use-workflow-interactions.ts | 2 +- .../_base/components/workflow-panel/index.tsx | 8 +- .../__tests__/upgrade-modal.spec.tsx | 8 + .../delivery-method/upgrade-modal.tsx | 29 +- .../__tests__/input-var-list.spec.tsx | 2 +- .../operator/__tests__/control.spec.tsx | 16 +- .../operator/__tests__/more-actions.spec.tsx | 37 +- .../components/workflow/operator/control.tsx | 21 - .../workflow/operator/more-actions.tsx | 20 +- .../components/workflow/shortcuts/commands.ts | 19 - .../workflow/shortcuts/definitions.ts | 7 - .../shortcuts/use-workflow-hotkeys.ts | 18 - .../store/__tests__/workflow-store.spec.ts | 7 - .../workflow/__tests__/layout-slice.spec.ts | 4 +- .../workflow/store/workflow/layout-slice.ts | 17 - .../education-apply/education-apply-page.tsx | 24 +- .../education-apply/expire-notice-modal.tsx | 3 +- web/app/page.tsx | 23 - web/context/app-context-provider.tsx | 14 +- web/context/modal-context-provider.tsx | 42 +- web/context/modal-context.test.tsx | 37 +- web/context/modal-context.ts | 4 +- web/context/provider-context-provider.tsx | 8 +- web/context/provider-context.ts | 2 + web/context/workspace-context-provider.tsx | 24 - web/context/workspace-context.ts | 14 - web/contract/console/apps.ts | 42 +- web/contract/console/explore.ts | 8 + web/contract/console/workspaces.ts | 56 + web/contract/router.ts | 22 +- .../__tests__/tag-filter.spec.tsx | 5 + .../components/app-card-tags.tsx | 2 +- .../tag-management/components/tag-filter.tsx | 19 +- .../components/tag-item-editor.tsx | 4 +- .../components/tag-search-content.tsx | 2 +- .../components/tag-selector.tsx | 2 +- web/hooks/use-import-dsl.ts | 8 +- web/hooks/use-query-params.spec.tsx | 31 + web/hooks/use-query-params.ts | 12 +- web/i18n/ar-TN/app-log.json | 2 +- web/i18n/ar-TN/app.json | 23 +- web/i18n/ar-TN/common.json | 73 +- web/i18n/ar-TN/dataset.json | 10 + web/i18n/ar-TN/explore.json | 4 +- web/i18n/ar-TN/pipeline.json | 2 +- web/i18n/ar-TN/tools.json | 12 +- web/i18n/ar-TN/workflow.json | 6 +- web/i18n/de-DE/app-log.json | 2 +- web/i18n/de-DE/app.json | 31 +- web/i18n/de-DE/billing.json | 4 +- web/i18n/de-DE/common.json | 79 +- web/i18n/de-DE/dataset.json | 10 + web/i18n/de-DE/explore.json | 4 +- web/i18n/de-DE/pipeline.json | 4 +- web/i18n/de-DE/plugin-trigger.json | 2 +- web/i18n/de-DE/tools.json | 12 +- web/i18n/de-DE/workflow.json | 32 +- web/i18n/en-US/app-debug.json | 2 +- web/i18n/en-US/app-log.json | 2 +- web/i18n/en-US/app.json | 31 +- web/i18n/en-US/billing.json | 2 +- web/i18n/en-US/common.json | 115 ++- web/i18n/en-US/dataset.json | 15 + web/i18n/en-US/explore.json | 13 +- web/i18n/en-US/pipeline.json | 4 +- web/i18n/en-US/plugin-trigger.json | 2 +- web/i18n/en-US/plugin.json | 174 ++-- web/i18n/en-US/tools.json | 22 +- web/i18n/en-US/workflow.json | 40 +- web/i18n/es-ES/app.json | 25 +- web/i18n/es-ES/billing.json | 2 +- web/i18n/es-ES/common.json | 79 +- web/i18n/es-ES/dataset.json | 10 + web/i18n/es-ES/explore.json | 4 +- web/i18n/es-ES/pipeline.json | 2 +- web/i18n/es-ES/tools.json | 12 +- web/i18n/es-ES/workflow.json | 28 +- web/i18n/fa-IR/app-log.json | 2 +- web/i18n/fa-IR/app.json | 31 +- web/i18n/fa-IR/billing.json | 2 +- web/i18n/fa-IR/common.json | 79 +- web/i18n/fa-IR/dataset.json | 10 + web/i18n/fa-IR/explore.json | 4 +- web/i18n/fa-IR/pipeline.json | 4 +- web/i18n/fa-IR/plugin-trigger.json | 2 +- web/i18n/fa-IR/tools.json | 12 +- web/i18n/fa-IR/workflow.json | 34 +- web/i18n/fr-FR/app-debug.json | 2 +- web/i18n/fr-FR/app-log.json | 2 +- web/i18n/fr-FR/app.json | 29 +- web/i18n/fr-FR/billing.json | 2 +- web/i18n/fr-FR/common.json | 79 +- web/i18n/fr-FR/dataset.json | 10 + web/i18n/fr-FR/explore.json | 4 +- web/i18n/fr-FR/pipeline.json | 2 +- web/i18n/fr-FR/plugin-trigger.json | 4 +- web/i18n/fr-FR/tools.json | 14 +- web/i18n/fr-FR/workflow.json | 36 +- web/i18n/hi-IN/app-log.json | 2 +- web/i18n/hi-IN/app.json | 31 +- web/i18n/hi-IN/billing.json | 2 +- web/i18n/hi-IN/common.json | 79 +- web/i18n/hi-IN/dataset.json | 10 + web/i18n/hi-IN/explore.json | 4 +- web/i18n/hi-IN/pipeline.json | 2 +- web/i18n/hi-IN/plugin-trigger.json | 2 +- web/i18n/hi-IN/tools.json | 12 +- web/i18n/hi-IN/workflow.json | 34 +- web/i18n/id-ID/app-log.json | 2 +- web/i18n/id-ID/app.json | 31 +- web/i18n/id-ID/billing.json | 2 +- web/i18n/id-ID/common.json | 79 +- web/i18n/id-ID/dataset.json | 10 + web/i18n/id-ID/explore.json | 4 +- web/i18n/id-ID/pipeline.json | 2 +- web/i18n/id-ID/plugin-trigger.json | 2 +- web/i18n/id-ID/tools.json | 12 +- web/i18n/id-ID/workflow.json | 34 +- web/i18n/it-IT/app-log.json | 2 +- web/i18n/it-IT/app.json | 31 +- web/i18n/it-IT/billing.json | 2 +- web/i18n/it-IT/common.json | 79 +- web/i18n/it-IT/dataset.json | 10 + web/i18n/it-IT/explore.json | 4 +- web/i18n/it-IT/pipeline.json | 4 +- web/i18n/it-IT/plugin-trigger.json | 2 +- web/i18n/it-IT/tools.json | 12 +- web/i18n/it-IT/workflow.json | 28 +- web/i18n/ja-JP/app-log.json | 2 +- web/i18n/ja-JP/app.json | 31 +- web/i18n/ja-JP/billing.json | 4 +- web/i18n/ja-JP/common.json | 149 ++- web/i18n/ja-JP/dataset.json | 13 + web/i18n/ja-JP/explore.json | 13 +- web/i18n/ja-JP/pipeline.json | 2 +- web/i18n/ja-JP/plugin-trigger.json | 2 +- web/i18n/ja-JP/plugin.json | 25 +- web/i18n/ja-JP/tools.json | 26 +- web/i18n/ja-JP/workflow.json | 36 +- web/i18n/ko-KR/app-log.json | 2 +- web/i18n/ko-KR/app.json | 31 +- web/i18n/ko-KR/billing.json | 2 +- web/i18n/ko-KR/common.json | 79 +- web/i18n/ko-KR/dataset.json | 10 + web/i18n/ko-KR/explore.json | 4 +- web/i18n/ko-KR/pipeline.json | 4 +- web/i18n/ko-KR/plugin-trigger.json | 2 +- web/i18n/ko-KR/tools.json | 12 +- web/i18n/ko-KR/workflow.json | 34 +- web/i18n/nl-NL/app-log.json | 2 +- web/i18n/nl-NL/app.json | 113 ++- web/i18n/nl-NL/billing.json | 2 +- web/i18n/nl-NL/common.json | 79 +- web/i18n/nl-NL/dataset.json | 10 + web/i18n/nl-NL/explore.json | 4 +- web/i18n/nl-NL/pipeline.json | 4 +- web/i18n/nl-NL/plugin-trigger.json | 2 +- web/i18n/nl-NL/tools.json | 12 +- web/i18n/nl-NL/workflow.json | 40 +- web/i18n/pl-PL/app-log.json | 2 +- web/i18n/pl-PL/app.json | 25 +- web/i18n/pl-PL/billing.json | 2 +- web/i18n/pl-PL/common.json | 79 +- web/i18n/pl-PL/dataset.json | 10 + web/i18n/pl-PL/explore.json | 4 +- web/i18n/pl-PL/pipeline.json | 2 +- web/i18n/pl-PL/plugin-trigger.json | 2 +- web/i18n/pl-PL/tools.json | 12 +- web/i18n/pl-PL/workflow.json | 28 +- web/i18n/pt-BR/app-log.json | 2 +- web/i18n/pt-BR/app.json | 29 +- web/i18n/pt-BR/billing.json | 2 +- web/i18n/pt-BR/common.json | 81 +- web/i18n/pt-BR/dataset.json | 10 + web/i18n/pt-BR/explore.json | 4 +- web/i18n/pt-BR/pipeline.json | 4 +- web/i18n/pt-BR/plugin-trigger.json | 2 +- web/i18n/pt-BR/tools.json | 12 +- web/i18n/pt-BR/workflow.json | 20 +- web/i18n/ro-RO/app-log.json | 2 +- web/i18n/ro-RO/app.json | 31 +- web/i18n/ro-RO/billing.json | 2 +- web/i18n/ro-RO/common.json | 79 +- web/i18n/ro-RO/dataset.json | 10 + web/i18n/ro-RO/explore.json | 4 +- web/i18n/ro-RO/pipeline.json | 4 +- web/i18n/ro-RO/plugin-trigger.json | 2 +- web/i18n/ro-RO/tools.json | 12 +- web/i18n/ro-RO/workflow.json | 34 +- web/i18n/ru-RU/app-log.json | 2 +- web/i18n/ru-RU/app.json | 31 +- web/i18n/ru-RU/billing.json | 2 +- web/i18n/ru-RU/common.json | 81 +- web/i18n/ru-RU/dataset.json | 10 + web/i18n/ru-RU/explore.json | 4 +- web/i18n/ru-RU/pipeline.json | 4 +- web/i18n/ru-RU/plugin-trigger.json | 2 +- web/i18n/ru-RU/tools.json | 12 +- web/i18n/ru-RU/workflow.json | 34 +- web/i18n/sl-SI/app-log.json | 2 +- web/i18n/sl-SI/app.json | 31 +- web/i18n/sl-SI/billing.json | 2 +- web/i18n/sl-SI/common.json | 79 +- web/i18n/sl-SI/dataset.json | 10 + web/i18n/sl-SI/explore.json | 4 +- web/i18n/sl-SI/pipeline.json | 4 +- web/i18n/sl-SI/plugin-trigger.json | 2 +- web/i18n/sl-SI/tools.json | 12 +- web/i18n/sl-SI/workflow.json | 34 +- web/i18n/th-TH/app-log.json | 2 +- web/i18n/th-TH/app.json | 31 +- web/i18n/th-TH/billing.json | 2 +- web/i18n/th-TH/common.json | 79 +- web/i18n/th-TH/dataset.json | 10 + web/i18n/th-TH/explore.json | 4 +- web/i18n/th-TH/pipeline.json | 2 +- web/i18n/th-TH/plugin-trigger.json | 2 +- web/i18n/th-TH/tools.json | 12 +- web/i18n/th-TH/workflow.json | 34 +- web/i18n/tr-TR/app-log.json | 2 +- web/i18n/tr-TR/app.json | 31 +- web/i18n/tr-TR/billing.json | 2 +- web/i18n/tr-TR/common.json | 79 +- web/i18n/tr-TR/dataset.json | 10 + web/i18n/tr-TR/explore.json | 4 +- web/i18n/tr-TR/pipeline.json | 4 +- web/i18n/tr-TR/plugin-trigger.json | 2 +- web/i18n/tr-TR/tools.json | 12 +- web/i18n/tr-TR/workflow.json | 34 +- web/i18n/uk-UA/app-debug.json | 2 +- web/i18n/uk-UA/app-log.json | 2 +- web/i18n/uk-UA/app.json | 31 +- web/i18n/uk-UA/billing.json | 2 +- web/i18n/uk-UA/common.json | 79 +- web/i18n/uk-UA/dataset.json | 10 + web/i18n/uk-UA/explore.json | 4 +- web/i18n/uk-UA/pipeline.json | 4 +- web/i18n/uk-UA/plugin-trigger.json | 2 +- web/i18n/uk-UA/tools.json | 12 +- web/i18n/uk-UA/workflow.json | 34 +- web/i18n/vi-VN/app.json | 31 +- web/i18n/vi-VN/billing.json | 2 +- web/i18n/vi-VN/common.json | 79 +- web/i18n/vi-VN/dataset.json | 10 + web/i18n/vi-VN/explore.json | 4 +- web/i18n/vi-VN/pipeline.json | 4 +- web/i18n/vi-VN/plugin-trigger.json | 2 +- web/i18n/vi-VN/tools.json | 12 +- web/i18n/vi-VN/workflow.json | 34 +- web/i18n/zh-Hans/app-log.json | 2 +- web/i18n/zh-Hans/app.json | 31 +- web/i18n/zh-Hans/billing.json | 2 +- web/i18n/zh-Hans/common.json | 117 ++- web/i18n/zh-Hans/dataset.json | 15 + web/i18n/zh-Hans/explore.json | 13 +- web/i18n/zh-Hans/pipeline.json | 4 +- web/i18n/zh-Hans/plugin-trigger.json | 2 +- web/i18n/zh-Hans/plugin.json | 180 ++-- web/i18n/zh-Hans/tools.json | 14 +- web/i18n/zh-Hans/workflow.json | 40 +- web/i18n/zh-Hant/app-log.json | 2 +- web/i18n/zh-Hant/app.json | 31 +- web/i18n/zh-Hant/billing.json | 4 +- web/i18n/zh-Hant/common.json | 101 +- web/i18n/zh-Hant/dataset.json | 10 + web/i18n/zh-Hant/explore.json | 4 +- web/i18n/zh-Hant/pipeline.json | 4 +- web/i18n/zh-Hant/plugin-trigger.json | 2 +- web/i18n/zh-Hant/tools.json | 18 +- web/i18n/zh-Hant/workflow.json | 42 +- web/models/common.ts | 41 +- web/next.config.ts | 4 +- web/next/navigation.ts | 1 + web/package.json | 2 + web/public/marketplace/hero-bg.jpg | Bin 0 -> 206100 bytes .../marketplace/hero-gradient-noise.svg | 27 + web/service/__tests__/use-plugins.spec.tsx | 701 +++++++++++++ web/service/common.ts | 9 - web/service/explore.ts | 9 + web/service/use-apps.ts | 29 + web/service/use-common.ts | 9 - web/service/use-explore.ts | 41 +- web/service/use-plugins.spec.tsx | 100 -- web/service/use-plugins.ts | 423 +++++++- web/types/app.ts | 2 + 995 files changed, 35090 insertions(+), 10333 deletions(-) create mode 100644 api/migrations/versions/2026_06_15_1100-b7c2d9e8a1f4_add_tenant_last_opened_at.py create mode 100644 api/migrations/versions/2026_06_15_1200-f6a7b8c9d012_add_plugin_auto_upgrade_category.py create mode 100644 api/migrations/versions/2026_06_15_1300-f5e8a9c0d2b3_add_learn_dify_flag_to_recommended_apps.py create mode 100644 api/migrations/versions/2026_06_15_1400-c4d5e6f7a8b9_add_app_stars.py create mode 100644 e2e/support/apps.ts create mode 100644 packages/iconify-collections/README.md create mode 100644 packages/iconify-collections/assets/vender/integrations/agent-strategy-active.svg create mode 100644 packages/iconify-collections/assets/vender/integrations/agent-strategy.svg create mode 100644 packages/iconify-collections/assets/vender/integrations/api-extension-active.svg create mode 100644 packages/iconify-collections/assets/vender/integrations/api-extension.svg create mode 100644 packages/iconify-collections/assets/vender/integrations/custom-tool-active.svg create mode 100644 packages/iconify-collections/assets/vender/integrations/custom-tool.svg create mode 100644 packages/iconify-collections/assets/vender/integrations/extension-active.svg create mode 100644 packages/iconify-collections/assets/vender/integrations/extension.svg create mode 100644 packages/iconify-collections/assets/vender/integrations/install-drop.svg create mode 100644 packages/iconify-collections/assets/vender/integrations/install-github.svg create mode 100644 packages/iconify-collections/assets/vender/integrations/install-local-package.svg create mode 100644 packages/iconify-collections/assets/vender/integrations/install-marketplace.svg create mode 100644 packages/iconify-collections/assets/vender/integrations/mcp.svg create mode 100644 packages/iconify-collections/assets/vender/integrations/panel-left.svg create mode 100644 packages/iconify-collections/assets/vender/integrations/tools-active.svg create mode 100644 packages/iconify-collections/assets/vender/integrations/tools.svg create mode 100644 packages/iconify-collections/assets/vender/integrations/trigger-active.svg create mode 100644 packages/iconify-collections/assets/vender/integrations/trigger.svg create mode 100644 packages/iconify-collections/assets/vender/integrations/workflow-as-tool-active.svg create mode 100644 packages/iconify-collections/assets/vender/integrations/workflow-as-tool.svg create mode 100644 packages/iconify-collections/assets/vender/main-nav/app-home.svg create mode 100644 packages/iconify-collections/assets/vender/main-nav/credits.svg create mode 100644 packages/iconify-collections/assets/vender/main-nav/help.svg create mode 100644 packages/iconify-collections/assets/vender/main-nav/home-active.svg create mode 100644 packages/iconify-collections/assets/vender/main-nav/home.svg create mode 100644 packages/iconify-collections/assets/vender/main-nav/integrations-active.svg create mode 100644 packages/iconify-collections/assets/vender/main-nav/integrations.svg create mode 100644 packages/iconify-collections/assets/vender/main-nav/knowledge-active.svg create mode 100644 packages/iconify-collections/assets/vender/main-nav/knowledge.svg create mode 100644 packages/iconify-collections/assets/vender/main-nav/marketplace-active.svg create mode 100644 packages/iconify-collections/assets/vender/main-nav/marketplace.svg create mode 100644 packages/iconify-collections/assets/vender/main-nav/quick-search.svg create mode 100644 packages/iconify-collections/assets/vender/main-nav/studio-active.svg create mode 100644 packages/iconify-collections/assets/vender/main-nav/studio.svg create mode 100644 packages/iconify-collections/assets/vender/main-nav/workspace-settings.svg create mode 100644 packages/iconify-collections/scripts/check-icon-dimensions.ts create mode 100644 web/__tests__/app-star-i18n.test.ts create mode 100644 web/app/(commonLayout)/app/(appDetailLayout)/[appId]/__tests__/layout-main.spec.tsx create mode 100644 web/app/(commonLayout)/explore/installed/[appId]/__tests__/page.spec.tsx create mode 100644 web/app/(commonLayout)/installed/[appId]/__tests__/page.spec.tsx create mode 100644 web/app/(commonLayout)/installed/[appId]/page.tsx create mode 100644 web/app/(commonLayout)/integrations/[[...slug]]/not-found.tsx create mode 100644 web/app/(commonLayout)/integrations/[[...slug]]/page.tsx create mode 100644 web/app/(commonLayout)/integrations/layout.tsx create mode 100644 web/app/(commonLayout)/marketplace/__tests__/page.spec.tsx create mode 100644 web/app/(commonLayout)/marketplace/layout.tsx create mode 100644 web/app/(commonLayout)/marketplace/page.tsx create mode 100644 web/app/(commonLayout)/page.tsx create mode 100644 web/app/components/app-sidebar/__tests__/app-detail-section.spec.tsx create mode 100644 web/app/components/app-sidebar/__tests__/app-detail-top.spec.tsx create mode 100644 web/app/components/app-sidebar/__tests__/dataset-detail-section.spec.tsx create mode 100644 web/app/components/app-sidebar/__tests__/dataset-detail-top.spec.tsx create mode 100644 web/app/components/app-sidebar/app-detail-section.tsx create mode 100644 web/app/components/app-sidebar/app-detail-top.tsx create mode 100644 web/app/components/app-sidebar/dataset-detail-section.tsx create mode 100644 web/app/components/app-sidebar/dataset-detail-top.tsx create mode 100644 web/app/components/app/log-annotation/page-title.tsx delete mode 100644 web/app/components/apps/__tests__/footer.spec.tsx create mode 100644 web/app/components/apps/app-list-creation-modals.tsx create mode 100644 web/app/components/apps/app-list-header-filters.tsx create mode 100644 web/app/components/apps/app-list-tag-management-modal.tsx create mode 100644 web/app/components/apps/app-sort-filter.tsx create mode 100644 web/app/components/apps/first-empty-state/action-card.tsx create mode 100644 web/app/components/apps/first-empty-state/index.tsx delete mode 100644 web/app/components/apps/footer.tsx create mode 100644 web/app/components/apps/starred-app-card.tsx create mode 100644 web/app/components/apps/starred-app-list.tsx create mode 100644 web/app/components/apps/studio-list-header.tsx create mode 100644 web/app/components/base/__tests__/page-unavailable.spec.tsx create mode 100644 web/app/components/base/create-resource-card.tsx create mode 100644 web/app/components/base/filter-empty-state/index.tsx create mode 100644 web/app/components/base/icons/src/vender/Annotations.json create mode 100644 web/app/components/base/icons/src/vender/Annotations.tsx create mode 100644 web/app/components/base/icons/src/vender/SidebarLeftArrowIcon.json create mode 100644 web/app/components/base/icons/src/vender/SidebarLeftArrowIcon.tsx create mode 100644 web/app/components/base/icons/src/vender/Star.json create mode 100644 web/app/components/base/icons/src/vender/Star.tsx create mode 100644 web/app/components/base/icons/src/vender/line/general/HelpQuestion.json create mode 100644 web/app/components/base/icons/src/vender/line/general/HelpQuestion.tsx create mode 100644 web/app/components/base/icons/src/vender/plugin/Plugin.json create mode 100644 web/app/components/base/icons/src/vender/plugin/Plugin.tsx create mode 100644 web/app/components/base/page-unavailable.tsx create mode 100644 web/app/components/base/search-input/__tests__/search-state.spec.ts create mode 100644 web/app/components/base/search-input/search-state.ts create mode 100644 web/app/components/datasets/list/__tests__/header.spec.tsx delete mode 100644 web/app/components/datasets/list/dataset-footer/__tests__/index.spec.tsx delete mode 100644 web/app/components/datasets/list/dataset-footer/index.tsx create mode 100644 web/app/components/datasets/list/first-empty-state/__tests__/index.spec.tsx create mode 100644 web/app/components/datasets/list/first-empty-state/index.tsx create mode 100644 web/app/components/datasets/list/header.tsx create mode 100644 web/app/components/explore/app-list/explore-app-list-header.tsx create mode 100644 web/app/components/explore/app-list/explore-recommendations.tsx create mode 100644 web/app/components/explore/app-list/loading-skeletons.tsx create mode 100644 web/app/components/explore/continue-work/index.tsx create mode 100644 web/app/components/explore/continue-work/item.tsx create mode 100644 web/app/components/explore/installed-app/__tests__/routes.spec.ts create mode 100644 web/app/components/explore/installed-app/routes.ts create mode 100644 web/app/components/explore/learn-dify/__tests__/item.spec.tsx create mode 100644 web/app/components/explore/learn-dify/atoms.ts create mode 100644 web/app/components/explore/learn-dify/index.tsx create mode 100644 web/app/components/explore/learn-dify/item.tsx delete mode 100644 web/app/components/goto-anything/actions/commands/__tests__/zen.spec.ts delete mode 100644 web/app/components/goto-anything/actions/commands/index.ts create mode 100644 web/app/components/goto-anything/actions/commands/slash-provider.tsx delete mode 100644 web/app/components/goto-anything/actions/commands/zen.tsx delete mode 100644 web/app/components/goto-anything/hooks/index.ts delete mode 100644 web/app/components/header/account-dropdown/__tests__/support.spec.tsx create mode 100644 web/app/components/header/account-dropdown/default-menu-content.tsx create mode 100644 web/app/components/header/account-dropdown/main-nav-menu-content.tsx delete mode 100644 web/app/components/header/account-dropdown/support.tsx create mode 100644 web/app/components/header/account-setting/__tests__/use-integrations-setting.spec.ts create mode 100644 web/app/components/header/account-setting/data-source-page-new/__tests__/plugin-actions.spec.tsx create mode 100644 web/app/components/header/account-setting/data-source-page-new/plugin-actions.tsx create mode 100644 web/app/components/header/account-setting/destinations.ts create mode 100644 web/app/components/header/account-setting/model-provider-page/model-provider-page-body.tsx create mode 100644 web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/button-config.ts create mode 100644 web/app/components/header/account-setting/update-setting-dialog-form.tsx create mode 100644 web/app/components/header/account-setting/update-setting-dialog.tsx create mode 100644 web/app/components/header/account-setting/update-setting-option-card.tsx create mode 100644 web/app/components/header/account-setting/use-integrations-setting.ts delete mode 100644 web/app/components/header/plugins-nav/downloading-icon.module.css create mode 100644 web/app/components/integrations/__tests__/page.spec.tsx create mode 100644 web/app/components/integrations/__tests__/plugin-category-page.spec.tsx create mode 100644 web/app/components/integrations/__tests__/routes.spec.ts create mode 100644 web/app/components/integrations/__tests__/tool-provider-card.spec.tsx create mode 100644 web/app/components/integrations/__tests__/tool-provider-list.spec.tsx create mode 100644 web/app/components/integrations/hooks/use-integration-nav.ts create mode 100644 web/app/components/integrations/hooks/use-integration-permissions.ts create mode 100644 web/app/components/integrations/hooks/use-integration-section.ts create mode 100644 web/app/components/integrations/hooks/use-tool-marketplace-panel.ts create mode 100644 web/app/components/integrations/hooks/use-tool-provider-category.ts create mode 100644 web/app/components/integrations/page-header.tsx create mode 100644 web/app/components/integrations/page.tsx create mode 100644 web/app/components/integrations/permission-quick-panel.tsx create mode 100644 web/app/components/integrations/plugin-category-page.tsx create mode 100644 web/app/components/integrations/routes.ts create mode 100644 web/app/components/integrations/section-layout.tsx create mode 100644 web/app/components/integrations/section-renderer.tsx create mode 100644 web/app/components/integrations/sidebar-actions.tsx create mode 100644 web/app/components/integrations/sidebar-nav-item-styles.ts create mode 100644 web/app/components/integrations/sidebar-nav-item.tsx create mode 100644 web/app/components/integrations/tool-provider-card.tsx create mode 100644 web/app/components/integrations/tool-provider-create-action.tsx create mode 100644 web/app/components/integrations/tool-provider-list.tsx create mode 100644 web/app/components/integrations/tool-provider-toolbar.tsx create mode 100644 web/app/components/main-nav/__tests__/index.spec.tsx create mode 100644 web/app/components/main-nav/__tests__/layout.spec.tsx create mode 100644 web/app/components/main-nav/components/__tests__/support-menu.spec.tsx create mode 100644 web/app/components/main-nav/components/__tests__/workspace-card.spec.tsx create mode 100644 web/app/components/main-nav/components/account-section.tsx create mode 100644 web/app/components/main-nav/components/help-menu.tsx create mode 100644 web/app/components/main-nav/components/nav-link.tsx create mode 100644 web/app/components/main-nav/components/search-button.tsx create mode 100644 web/app/components/main-nav/components/support-menu.tsx create mode 100644 web/app/components/main-nav/components/web-apps-section.tsx create mode 100644 web/app/components/main-nav/components/workspace-card.tsx create mode 100644 web/app/components/main-nav/components/workspace-menu-content.tsx create mode 100644 web/app/components/main-nav/components/workspace-plan-badge.tsx create mode 100644 web/app/components/main-nav/components/workspace-switcher.tsx create mode 100644 web/app/components/main-nav/index.tsx create mode 100644 web/app/components/main-nav/layout.tsx create mode 100644 web/app/components/main-nav/types.ts create mode 100644 web/app/components/main-nav/utils.ts create mode 100644 web/app/components/plugins/__tests__/plugin-routes.spec.ts create mode 100644 web/app/components/plugins/marketplace/list/carousel.tsx create mode 100644 web/app/components/plugins/marketplace/list/collection-constants.ts create mode 100644 web/app/components/plugins/plugin-page/content-inset.ts create mode 100644 web/app/components/plugins/plugin-page/install-source-icons.tsx create mode 100644 web/app/components/plugins/plugin-page/nav-operations.tsx create mode 100644 web/app/components/plugins/plugin-page/plugin-list-skeleton.tsx create mode 100644 web/app/components/plugins/plugin-page/plugin-sidecar-panel.tsx create mode 100644 web/app/components/plugins/plugin-page/plugins-panel-results.tsx create mode 100644 web/app/components/plugins/plugin-page/plugins-panel-utils.ts create mode 100644 web/app/components/plugins/plugin-routes.ts delete mode 100644 web/app/components/plugins/reference-setting-modal/auto-update-setting/__tests__/config.spec.ts delete mode 100644 web/app/components/plugins/reference-setting-modal/auto-update-setting/config.ts delete mode 100644 web/app/components/tools/__tests__/provider-list.spec.tsx create mode 100644 web/app/components/tools/content-inset.ts create mode 100644 web/app/components/tools/integrations-setting-modal.tsx delete mode 100644 web/app/components/tools/provider-list.tsx create mode 100644 web/app/components/tools/provider/create-entry-card.tsx create mode 100644 web/app/components/tools/tool-provider-grid.tsx delete mode 100644 web/app/components/workflow/hooks/__tests__/use-workflow-canvas-maximize.spec.ts delete mode 100644 web/app/components/workflow/hooks/use-workflow-canvas-maximize.ts delete mode 100644 web/app/components/workflow/shortcuts/commands.ts delete mode 100644 web/app/page.tsx delete mode 100644 web/context/workspace-context-provider.tsx delete mode 100644 web/context/workspace-context.ts create mode 100644 web/contract/console/workspaces.ts create mode 100644 web/public/marketplace/hero-bg.jpg create mode 100644 web/public/marketplace/hero-gradient-noise.svg create mode 100644 web/service/__tests__/use-plugins.spec.tsx delete mode 100644 web/service/use-plugins.spec.tsx diff --git a/api/commands/__init__.py b/api/commands/__init__.py index 86f6faa78c..ea4c5aaa2a 100644 --- a/api/commands/__init__.py +++ b/api/commands/__init__.py @@ -11,6 +11,7 @@ from .data_migration import ( migration_data_wizard, ) from .plugin import ( + backfill_plugin_auto_upgrade, extract_plugins, extract_unique_plugins, install_plugins, @@ -49,6 +50,7 @@ from .vector import ( __all__ = [ "add_qdrant_index", "archive_workflow_runs", + "backfill_plugin_auto_upgrade", "clean_expired_messages", "clean_workflow_runs", "cleanup_orphaned_draft_variables", diff --git a/api/commands/plugin.py b/api/commands/plugin.py index e1b3cf0fa1..71c19f842f 100644 --- a/api/commands/plugin.py +++ b/api/commands/plugin.py @@ -1,10 +1,11 @@ import json import logging +import time from typing import Any, cast import click from pydantic import TypeAdapter -from sqlalchemy import delete, select +from sqlalchemy import delete, func, select from sqlalchemy.engine import CursorResult from configs import dify_config @@ -15,11 +16,13 @@ from core.plugin.plugin_service import PluginService from core.tools.utils.system_encryption import encrypt_system_params from extensions.ext_database import db from models import Tenant +from models.account import TenantPluginAutoUpgradeStrategy from models.oauth import DatasourceOauthParamConfig, DatasourceProvider from models.provider_ids import DatasourceProviderID, ToolProviderID from models.source import DataSourceApiKeyAuthBinding, DataSourceOauthBinding from models.tools import ToolOAuthSystemClient from services.plugin.data_migration import PluginDataMigration +from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService from services.plugin.plugin_migration import PluginMigration logger = logging.getLogger(__name__) @@ -402,6 +405,110 @@ def migrate_data_for_plugin(): click.echo(click.style("Migrate data for plugin completed.", fg="green")) +def _candidate_auto_upgrade_strategy_tenant_ids_stmt(limit: int | None = None): + category_count = len(TenantPluginAutoUpgradeStrategy.PluginCategory) + stmt = ( + select(TenantPluginAutoUpgradeStrategy.tenant_id) + .group_by(TenantPluginAutoUpgradeStrategy.tenant_id) + .having(func.count(func.distinct(TenantPluginAutoUpgradeStrategy.category)) < category_count) + .order_by(TenantPluginAutoUpgradeStrategy.tenant_id) + ) + + if limit is not None: + stmt = stmt.limit(limit) + + return stmt + + +def _count_auto_upgrade_strategy_tenant_ids(limit: int | None) -> int: + candidate_stmt = _candidate_auto_upgrade_strategy_tenant_ids_stmt(limit).subquery() + return db.session.scalar(select(func.count()).select_from(candidate_stmt)) or 0 + + +def _iter_auto_upgrade_strategy_tenant_ids(limit: int | None): + stmt = _candidate_auto_upgrade_strategy_tenant_ids_stmt(limit).execution_options(yield_per=1000) + yield from db.session.scalars(stmt) + + +@click.command( + "backfill-plugin-auto-upgrade", + help="Backfill category-scoped plugin auto-upgrade strategies and normalize plugin lists.", +) +@click.option("--tenant-id", multiple=True, help="Tenant ID to backfill. Can be passed multiple times.") +@click.option("--limit", type=int, default=None, help="Maximum number of candidate tenants to process.") +@click.option("--batch-size", type=int, default=500, show_default=True, help="Progress reporting batch size.") +@click.option("--dry-run", is_flag=True, help="Only print candidate tenant count.") +def backfill_plugin_auto_upgrade( + tenant_id: tuple[str, ...], + limit: int | None, + batch_size: int, + dry_run: bool, +): + """ + Backfill historical auto-upgrade strategies after the category column exists. + + Missing category rows are created from the tenant's tool/default row. Pure default + strategies become latest for model plugins and fix-only for all other categories. + Tenants with include/exclude plugin IDs are split + by installed plugin category using plugin daemon metadata. + """ + start_at = time.perf_counter() + candidate_count = len(tenant_id) if tenant_id else _count_auto_upgrade_strategy_tenant_ids(limit) + click.echo(click.style(f"Found {candidate_count} candidate tenants.", fg="yellow")) + + if dry_run: + elapsed = time.perf_counter() - start_at + click.echo(click.style(f"Dry run completed. elapsed={elapsed:.2f}s", fg="green")) + return + + tenant_ids = list(tenant_id) if tenant_id else _iter_auto_upgrade_strategy_tenant_ids(limit) + + backfilled_count = 0 + created_count = 0 + normalized_count = 0 + skipped_count = 0 + failed_count = 0 + for index, current_tenant_id in enumerate(tenant_ids, start=1): + try: + result = PluginAutoUpgradeService.backfill_strategy_categories( + current_tenant_id, + ) + except Exception as e: + failed_count += 1 + click.echo(click.style(f"Failed tenant {current_tenant_id}: {str(e)}", fg="red")) + continue + + if result.created_count > 0: + backfilled_count += 1 + created_count += result.created_count + elif not result.normalized: + skipped_count += 1 + if result.normalized: + normalized_count += 1 + + if batch_size > 0 and index % batch_size == 0: + click.echo( + click.style( + f"Processed {index}/{candidate_count} tenants. " + f"backfilled={backfilled_count}, created_rows={created_count}, " + f"normalized={normalized_count}, skipped={skipped_count}, failed={failed_count}, " + f"elapsed={time.perf_counter() - start_at:.2f}s", + fg="yellow", + ) + ) + + elapsed = time.perf_counter() - start_at + click.echo( + click.style( + f"Backfill plugin auto-upgrade strategy categories completed. " + f"backfilled={backfilled_count}, created_rows={created_count}, " + f"normalized={normalized_count}, skipped={skipped_count}, failed={failed_count}, " + f"elapsed={elapsed:.2f}s", + fg="green", + ) + ) + + @click.command("extract-plugins", help="Extract plugins.") @click.option("--output_file", prompt=True, help="The file to store the extracted plugins.", default="plugins.jsonl") @click.option("--workers", prompt=True, help="The number of workers to extract plugins.", default=10) diff --git a/api/controllers/console/app/app.py b/api/controllers/console/app/app.py index f6bba27d56..7d0656e862 100644 --- a/api/controllers/console/app/app.py +++ b/api/controllers/console/app/app.py @@ -1,6 +1,7 @@ import logging import re import uuid +from collections.abc import Sequence from datetime import datetime from typing import Any, Literal, cast @@ -41,12 +42,12 @@ from core.trigger.constants import TRIGGER_NODE_TYPES from extensions.ext_database import db from fields.base import ResponseModel from graphon.enums import WorkflowExecutionStatus -from libs.helper import build_icon_url, to_timestamp +from libs.helper import build_icon_url, dump_response, to_timestamp from libs.login import login_required from models import Account, App, DatasetPermissionEnum, Workflow from models.model import IconType from services.app_dsl_service import AppDslService -from services.app_service import AppListParams, AppService, CreateAppParams +from services.app_service import AppListParams, AppListSortBy, AppService, CreateAppParams, StarredAppListParams from services.enterprise.enterprise_service import EnterpriseService from services.entities.dsl_entities import ImportMode, ImportStatus from services.entities.knowledge_entities.knowledge_entities import ( @@ -73,10 +74,14 @@ _CREATOR_IDS_BRACKET_PATTERN = re.compile(r"^creator_ids\[(\d+)\]$") AppListMode = Literal["completion", "chat", "advanced-chat", "workflow", "agent-chat", "agent", "channel", "all"] -class AppListQuery(BaseModel): +class AppListBaseQuery(BaseModel): page: int = Field(default=1, ge=1, le=99999, description="Page number (1-99999)") limit: int = Field(default=20, ge=1, le=100, description="Page size (1-100)") mode: AppListMode = Field(default=cast(AppListMode, "all"), description="App mode filter") + sort_by: AppListSortBy = Field( + default="last_modified", + description="Sort apps by last modified, recently created, or earliest created", + ) name: str | None = Field(default=None, description="Filter by app name") tag_ids: list[str] | None = Field(default=None, description="Filter by tag IDs") creator_ids: list[str] | None = Field(default=None, description="Filter by creator account IDs") @@ -119,6 +124,14 @@ class AppListQuery(BaseModel): raise ValueError("Invalid UUID format in creator_ids.") from exc +class AppListQuery(AppListBaseQuery): + pass + + +class StarredAppListQuery(AppListBaseQuery): + pass + + def _normalize_app_list_query_args(query_args: MultiDict[str, str]) -> dict[str, str | list[str]]: normalized: dict[str, str | list[str]] = {} indexed_tag_ids: list[tuple[int, str]] = [] @@ -387,6 +400,7 @@ class AppPartial(ResponseModel): create_user_name: str | None = None author_name: str | None = None has_draft_trigger: bool | None = None + is_starred: bool = False @computed_field(return_type=str | None) # type: ignore @property @@ -456,12 +470,54 @@ class AppExportResponse(ResponseModel): data: str +def _enrich_app_list_items(session: Session, *, apps: Sequence[App], tenant_id: str) -> None: + if FeatureService.get_system_features().webapp_auth.enabled: + app_ids = [str(app.id) for app in apps] + res = EnterpriseService.WebAppAuth.batch_get_app_access_mode_by_id(app_ids=app_ids) + if len(res) != len(app_ids): + raise BadRequest("Invalid app id in webapp auth") + + for app in apps: + if str(app.id) in res: + app.access_mode = res[str(app.id)].access_mode + + workflow_capable_app_ids = [str(app.id) for app in apps if app.mode in {"workflow", "advanced-chat"}] + draft_trigger_app_ids: set[str] = set() + if workflow_capable_app_ids: + draft_workflows = ( + session.execute( + select(Workflow).where( + Workflow.version == Workflow.VERSION_DRAFT, + Workflow.app_id.in_(workflow_capable_app_ids), + Workflow.tenant_id == tenant_id, + ) + ) + .scalars() + .all() + ) + trigger_node_types = TRIGGER_NODE_TYPES + for workflow in draft_workflows: + node_id = None + try: + for node_id, node_data in workflow.walk_nodes(): + if node_data.get("type") in trigger_node_types: + draft_trigger_app_ids.add(str(workflow.app_id)) + break + except Exception: + _logger.exception("error while walking nodes, workflow_id=%s, node_id=%s", workflow.id, node_id) + continue + + for app in apps: + app.has_draft_trigger = str(app.id) in draft_trigger_app_ids + + register_enum_models(console_ns, RetrievalMethod, WorkflowExecutionStatus, DatasetPermissionEnum) register_response_schema_models(console_ns, AppTraceResponse, RedirectUrlResponse, SimpleResultResponse) register_schema_models( console_ns, AppListQuery, + StarredAppListQuery, CreateAppPayload, UpdateAppPayload, CopyAppPayload, @@ -521,6 +577,7 @@ class AppListApi(Resource): page=args.page, limit=args.limit, mode=args.mode, + sort_by=args.sort_by, name=args.name, tag_ids=args.tag_ids, creator_ids=args.creator_ids, @@ -534,46 +591,7 @@ class AppListApi(Resource): empty = AppPagination(page=args.page, limit=args.limit, total=0, has_more=False, data=[]) return empty.model_dump(mode="json"), 200 - if FeatureService.get_system_features().webapp_auth.enabled: - app_ids = [str(app.id) for app in app_pagination.items] - res = EnterpriseService.WebAppAuth.batch_get_app_access_mode_by_id(app_ids=app_ids) - if len(res) != len(app_ids): - raise BadRequest("Invalid app id in webapp auth") - - for app in app_pagination.items: - if str(app.id) in res: - app.access_mode = res[str(app.id)].access_mode - - workflow_capable_app_ids = [ - str(app.id) for app in app_pagination.items if app.mode in {"workflow", "advanced-chat"} - ] - draft_trigger_app_ids: set[str] = set() - if workflow_capable_app_ids: - draft_workflows = ( - session.execute( - select(Workflow).where( - Workflow.version == Workflow.VERSION_DRAFT, - Workflow.app_id.in_(workflow_capable_app_ids), - Workflow.tenant_id == current_tenant_id, - ) - ) - .scalars() - .all() - ) - trigger_node_types = TRIGGER_NODE_TYPES - for workflow in draft_workflows: - node_id = None - try: - for node_id, node_data in workflow.walk_nodes(): - if node_data.get("type") in trigger_node_types: - draft_trigger_app_ids.add(str(workflow.app_id)) - break - except Exception: - _logger.exception("error while walking nodes, workflow_id=%s, node_id=%s", workflow.id, node_id) - continue - - for app in app_pagination.items: - app.has_draft_trigger = str(app.id) in draft_trigger_app_ids + _enrich_app_list_items(session, apps=app_pagination.items, tenant_id=current_tenant_id) pagination_model = AppPagination.model_validate(app_pagination, from_attributes=True) return pagination_model.model_dump(mode="json"), 200 @@ -609,6 +627,78 @@ class AppListApi(Resource): return app_detail.model_dump(mode="json"), 201 +@console_ns.route("/apps/starred") +class StarredAppListApi(Resource): + @console_ns.doc("list_starred_apps") + @console_ns.doc(description="Get applications starred by the current account") + @console_ns.doc(params=query_params_from_model(StarredAppListQuery)) + @console_ns.response(200, "Success", console_ns.models[AppPagination.__name__]) + @setup_required + @login_required + @account_initialization_required + @enterprise_license_required + @with_session(write=False) + @with_current_user_id + @with_current_tenant_id + def get(self, current_tenant_id: str, current_user_id: str, session: Session): + args = StarredAppListQuery.model_validate(_normalize_app_list_query_args(request.args)) + params = StarredAppListParams( + page=args.page, + limit=args.limit, + mode=args.mode, + sort_by=args.sort_by, + name=args.name, + tag_ids=args.tag_ids, + creator_ids=args.creator_ids, + is_created_by_me=args.is_created_by_me, + ) + + app_pagination = AppService().get_paginate_starred_apps(current_user_id, current_tenant_id, params) + if not app_pagination: + empty = AppPagination(page=args.page, limit=args.limit, total=0, has_more=False, data=[]) + return empty.model_dump(mode="json"), 200 + + _enrich_app_list_items(session, apps=app_pagination.items, tenant_id=current_tenant_id) + + pagination_model = AppPagination.model_validate(app_pagination, from_attributes=True) + return pagination_model.model_dump(mode="json"), 200 + + +@console_ns.route("/apps//star") +class AppStarApi(Resource): + @console_ns.doc("star_app") + @console_ns.doc(description="Star an application for the current account") + @console_ns.doc(params={"app_id": "Application ID"}) + @console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__]) + @console_ns.response(404, "App not found") + @setup_required + @login_required + @account_initialization_required + @enterprise_license_required + @with_current_user_id + @with_session + @get_app_model(mode=None) + def post(self, session: Session, current_user_id: str, app_model: App): + AppService.star_app(session, app=app_model, account_id=current_user_id) + return dump_response(SimpleResultResponse, {"result": "success"}) + + @console_ns.doc("unstar_app") + @console_ns.doc(description="Remove the current account's star from an application") + @console_ns.doc(params={"app_id": "Application ID"}) + @console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__]) + @console_ns.response(404, "App not found") + @setup_required + @login_required + @account_initialization_required + @enterprise_license_required + @with_current_user_id + @with_session + @get_app_model(mode=None) + def delete(self, session: Session, current_user_id: str, app_model: App): + AppService.unstar_app(session, app=app_model, account_id=current_user_id) + return dump_response(SimpleResultResponse, {"result": "success"}) + + @console_ns.route("/apps/") class AppApi(Resource): @console_ns.doc("get_app_detail") @@ -628,7 +718,7 @@ class AppApi(Resource): if FeatureService.get_system_features().webapp_auth.enabled: app_setting = EnterpriseService.WebAppAuth.get_app_access_mode_by_id(app_id=str(app_model.id)) - app_model.access_mode = app_setting.access_mode # type: ignore[attr-defined] + app_model.access_mode = app_setting.access_mode response_model = AppDetailWithSite.model_validate(app_model, from_attributes=True) return response_model.model_dump(mode="json") diff --git a/api/controllers/console/explore/recommended_app.py b/api/controllers/console/explore/recommended_app.py index 1b53226440..72f797eeb3 100644 --- a/api/controllers/console/explore/recommended_app.py +++ b/api/controllers/console/explore/recommended_app.py @@ -66,6 +66,10 @@ class RecommendedAppListResponse(ResponseModel): categories: list[str] +class LearnDifyAppListResponse(ResponseModel): + recommended_apps: list[RecommendedAppResponse] + + class RecommendedAppDetailResponse(RootModel[dict[str, Any]]): root: dict[str, Any] @@ -76,10 +80,19 @@ register_schema_models( RecommendedAppInfoResponse, RecommendedAppResponse, RecommendedAppListResponse, + LearnDifyAppListResponse, ) register_response_schema_models(console_ns, RecommendedAppDetailResponse) +def _resolve_language(language: str | None, user: Account) -> str: + if language and language in languages: + return language + if user.interface_language: + return user.interface_language + return languages[0] + + @console_ns.route("/explore/apps") class RecommendedAppListApi(Resource): @console_ns.doc(params=query_params_from_model(RecommendedAppsQuery)) @@ -90,13 +103,7 @@ class RecommendedAppListApi(Resource): def get(self, current_user: Account): # language args args = RecommendedAppsQuery.model_validate(request.args.to_dict(flat=True)) - language = args.language - if language and language in languages: - language_prefix = language - elif current_user.interface_language: - language_prefix = current_user.interface_language - else: - language_prefix = languages[0] + language_prefix = _resolve_language(args.language, current_user) return RecommendedAppListResponse.model_validate( RecommendedAppService.get_recommended_apps_and_categories(db.session, language_prefix), @@ -104,6 +111,23 @@ class RecommendedAppListApi(Resource): ).model_dump(mode="json") +@console_ns.route("/explore/apps/learn-dify") +class LearnDifyAppListApi(Resource): + @console_ns.doc(params=query_params_from_model(RecommendedAppsQuery)) + @console_ns.response(200, "Success", console_ns.models[LearnDifyAppListResponse.__name__]) + @login_required + @account_initialization_required + @with_current_user + def get(self, current_user: Account): + args = RecommendedAppsQuery.model_validate(request.args.to_dict(flat=True)) + language_prefix = _resolve_language(args.language, current_user) + + return LearnDifyAppListResponse.model_validate( + RecommendedAppService.get_learn_dify_apps(db.session, language_prefix), + from_attributes=True, + ).model_dump(mode="json") + + @console_ns.route("/explore/apps/") class RecommendedAppApi(Resource): @console_ns.response(200, "Success", console_ns.models[RecommendedAppDetailResponse.__name__]) diff --git a/api/controllers/console/workspace/plugin.py b/api/controllers/console/workspace/plugin.py index be5bef0efa..94979e25b3 100644 --- a/api/controllers/console/workspace/plugin.py +++ b/api/controllers/console/workspace/plugin.py @@ -1,10 +1,11 @@ import io from collections.abc import Mapping -from typing import Any, Literal +from datetime import datetime +from typing import Any, Literal, TypedDict from flask import request, send_file from flask_restx import Resource -from pydantic import BaseModel, Field, RootModel +from pydantic import BaseModel, ConfigDict, Field, RootModel from werkzeug.datastructures import FileStorage from werkzeug.exceptions import Forbidden @@ -26,15 +27,33 @@ from controllers.console.wraps import ( with_current_user, with_current_user_id, ) +from core.helper.position_helper import is_filtered +from core.plugin.entities.plugin import PluginCategory, PluginInstallationSource from core.plugin.impl.exc import PluginDaemonClientSideError from core.plugin.plugin_service import PluginService +from core.tools.builtin_tool.providers._positions import BuiltinToolProviderSort +from core.tools.entities.common_entities import I18nObject +from core.tools.entities.tool_entities import ToolProviderType +from core.tools.tool_manager import ToolManager from fields.base import ResponseModel from graphon.model_runtime.utils.encoders import jsonable_encoder +from libs.helper import dump_response from libs.login import login_required from models.account import Account, TenantPluginAutoUpgradeStrategy, TenantPluginPermission +from models.provider_ids import ToolProviderID +from services.entities.model_provider_entities import ProviderEntityResponse from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService from services.plugin.plugin_parameter_service import PluginParameterService from services.plugin.plugin_permission_service import PluginPermissionService +from services.tools.tools_transform_service import ToolTransformService + + +class AutoUpgradeSettingsResponse(TypedDict): + strategy_setting: TenantPluginAutoUpgradeStrategy.StrategySetting + upgrade_time_of_day: int + upgrade_mode: TenantPluginAutoUpgradeStrategy.UpgradeMode + exclude_plugins: list[str] + include_plugins: list[str] class ParserList(BaseModel): @@ -42,6 +61,11 @@ class ParserList(BaseModel): page_size: int = Field(default=256, ge=1, le=256, description="Page size (1-256)") +class PluginCategoryListQuery(BaseModel): + page: int = Field(default=1, ge=1, description="Page number") + page_size: int = Field(default=256, ge=1, le=256, description="Page size (1-256)") + + class ParserLatest(BaseModel): plugin_ids: list[str] @@ -100,8 +124,8 @@ class ParserUninstall(BaseModel): class ParserPermissionChange(BaseModel): - install_permission: TenantPluginPermission.InstallPermission - debug_permission: TenantPluginPermission.DebugPermission + install_permission: TenantPluginPermission.InstallPermission = TenantPluginPermission.InstallPermission.EVERYONE + debug_permission: TenantPluginPermission.DebugPermission = TenantPluginPermission.DebugPermission.EVERYONE class ParserDynamicOptions(BaseModel): @@ -137,13 +161,64 @@ class PluginAutoUpgradeSettingsPayload(BaseModel): include_plugins: list[str] = Field(default_factory=list) -class ParserPreferencesChange(BaseModel): - permission: PluginPermissionSettingsPayload +class PluginAutoUpgradeChangeResponse(ResponseModel): + success: bool + message: str | None = None + + +class PluginAutoUpgradeSettingsResponseModel(ResponseModel): + strategy_setting: TenantPluginAutoUpgradeStrategy.StrategySetting + upgrade_time_of_day: int + upgrade_mode: TenantPluginAutoUpgradeStrategy.UpgradeMode + exclude_plugins: list[str] + include_plugins: list[str] + + +class PluginAutoUpgradeFetchResponse(ResponseModel): + category: TenantPluginAutoUpgradeStrategy.PluginCategory + auto_upgrade: PluginAutoUpgradeSettingsResponseModel + + +class PluginDeclarationResponse(ResponseModel): + version: str + author: str | None + name: str + description: I18nObject + icon: str + icon_dark: str | None = None + label: I18nObject + category: PluginCategory + created_at: datetime + resource: Mapping[str, Any] + plugins: Mapping[str, list[str] | None] + tags: list[str] = Field(default_factory=list) + repo: str | None = None + verified: bool = False + tool: Mapping[str, Any] | None = None + model: ProviderEntityResponse | None = None + endpoint: Mapping[str, Any] | None = None + agent_strategy: Mapping[str, Any] | None = None + datasource: Mapping[str, Any] | None = None + trigger: Mapping[str, Any] | None = None + meta: Mapping[str, Any] + + +class ParserAutoUpgradeChange(BaseModel): + model_config = ConfigDict(extra="forbid") + + category: TenantPluginAutoUpgradeStrategy.PluginCategory auto_upgrade: PluginAutoUpgradeSettingsPayload +class ParserAutoUpgradeFetch(BaseModel): + category: TenantPluginAutoUpgradeStrategy.PluginCategory + + class ParserExcludePlugin(BaseModel): + model_config = ConfigDict(extra="forbid") + plugin_id: str + category: TenantPluginAutoUpgradeStrategy.PluginCategory class ParserReadme(BaseModel): @@ -157,6 +232,63 @@ class PluginDebuggingKeyResponse(ResponseModel): port: int +class PluginCategoryInstalledPluginResponse(ResponseModel): + id: str + name: str + tenant_id: str + plugin_id: str + plugin_unique_identifier: str + endpoints_active: int + endpoints_setups: int + installation_id: str + declaration: PluginDeclarationResponse + runtime_type: str + version: str + created_at: datetime + updated_at: datetime + source: PluginInstallationSource + checksum: str + meta: Mapping[str, Any] + + +class PluginCategoryBuiltinToolResponse(ResponseModel): + model_config = ConfigDict(extra="allow") + + author: str + name: str + label: I18nObject + description: I18nObject + parameters: list[Mapping[str, Any]] | None = None + labels: list[str] + output_schema: Mapping[str, object] + + +class PluginCategoryBuiltinToolProviderResponse(ResponseModel): + model_config = ConfigDict(extra="allow") + + id: str + author: str + name: str + plugin_id: str | None + plugin_unique_identifier: str | None + description: I18nObject + icon: str | Mapping[str, str] + icon_dark: str | Mapping[str, str] | None + label: I18nObject + type: ToolProviderType + team_credentials: Mapping[str, object] + is_team_authorization: bool + allow_delete: bool + tools: list[PluginCategoryBuiltinToolResponse] + labels: list[str] + + +class PluginCategoryListResponse(ResponseModel): + plugins: list[PluginCategoryInstalledPluginResponse] + builtin_tools: list[PluginCategoryBuiltinToolProviderResponse] + has_more: bool + + class PluginDaemonOperationResponse(RootModel[Any]): root: Any @@ -200,11 +332,6 @@ class PluginOperationSuccessResponse(ResponseModel): message: str | None = None -class PluginPreferencesResponse(ResponseModel): - permission: PluginPermissionSettingsPayload - auto_upgrade: PluginAutoUpgradeSettingsPayload - - class PluginReadmeResponse(ResponseModel): readme: str @@ -212,6 +339,7 @@ class PluginReadmeResponse(ResponseModel): register_schema_models( console_ns, ParserList, + PluginCategoryListQuery, PluginAutoUpgradeSettingsPayload, PluginPermissionSettingsPayload, ParserLatest, @@ -228,13 +356,21 @@ register_schema_models( ParserPermissionChange, ParserDynamicOptions, ParserDynamicOptionsWithCredentials, - ParserPreferencesChange, + ParserAutoUpgradeChange, + ParserAutoUpgradeFetch, ParserExcludePlugin, ParserReadme, ) register_response_schema_models( console_ns, + PluginAutoUpgradeChangeResponse, + PluginAutoUpgradeFetchResponse, + PluginAutoUpgradeSettingsResponseModel, BinaryFileResponse, + PluginCategoryBuiltinToolProviderResponse, + PluginCategoryBuiltinToolResponse, + PluginCategoryInstalledPluginResponse, + PluginCategoryListResponse, PluginDaemonOperationResponse, PluginDebuggingKeyResponse, PluginDynamicOptionsResponse, @@ -243,7 +379,6 @@ register_response_schema_models( PluginManifestResponse, PluginOperationSuccessResponse, PluginPermissionResponse, - PluginPreferencesResponse, PluginReadmeResponse, PluginTaskResponse, PluginTasksResponse, @@ -254,12 +389,36 @@ register_response_schema_models( register_enum_models( console_ns, TenantPluginPermission.DebugPermission, + TenantPluginAutoUpgradeStrategy.PluginCategory, TenantPluginAutoUpgradeStrategy.UpgradeMode, TenantPluginAutoUpgradeStrategy.StrategySetting, TenantPluginPermission.InstallPermission, ) +def _default_auto_upgrade_settings( + tenant_id: str, + category: TenantPluginAutoUpgradeStrategy.PluginCategory, +) -> AutoUpgradeSettingsResponse: + return { + "strategy_setting": PluginAutoUpgradeService.default_strategy_setting_for_category(category), + "upgrade_time_of_day": PluginAutoUpgradeService.default_upgrade_time_of_day(tenant_id), + "upgrade_mode": TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE, + "exclude_plugins": [], + "include_plugins": [], + } + + +def _auto_upgrade_settings_to_dict(strategy: TenantPluginAutoUpgradeStrategy) -> AutoUpgradeSettingsResponse: + return { + "strategy_setting": strategy.strategy_setting, + "upgrade_time_of_day": strategy.upgrade_time_of_day, + "upgrade_mode": strategy.upgrade_mode, + "exclude_plugins": strategy.exclude_plugins, + "include_plugins": strategy.include_plugins, + } + + def _read_upload_content(file: FileStorage, max_size: int) -> bytes: """ Read the uploaded file and validate its actual size before delegating to the plugin service. @@ -274,6 +433,33 @@ def _read_upload_content(file: FileStorage, max_size: int) -> bytes: return content +def _list_hardcoded_builtin_tool_providers(tenant_id: str) -> list[dict[str, Any]]: + db_builtin_providers = { + str(ToolProviderID(provider.provider)): provider + for provider in ToolManager.list_default_builtin_providers(tenant_id) + } + builtin_providers = [] + + for provider in ToolManager.list_hardcoded_providers(): + if is_filtered( + include_set=dify_config.POSITION_TOOL_INCLUDES_SET, + exclude_set=dify_config.POSITION_TOOL_EXCLUDES_SET, + data=provider, + name_func=lambda provider_controller: provider_controller.entity.identity.name, + ): + continue + + user_provider = ToolTransformService.builtin_provider_to_user_provider( + provider_controller=provider, + db_provider=db_builtin_providers.get(provider.entity.identity.name), + decrypt_credentials=False, + ) + ToolTransformService.repack_provider(tenant_id=tenant_id, provider=user_provider) + builtin_providers.append(user_provider) + + return [provider.to_dict() for provider in BuiltinToolProviderSort.sort(builtin_providers)] + + @console_ns.route("/workspaces/current/plugin/debugging-key") class PluginDebuggingKeyApi(Resource): @console_ns.response(200, "Success", console_ns.models[PluginDebuggingKeyResponse.__name__]) @@ -312,6 +498,41 @@ class PluginListApi(Resource): return jsonable_encoder({"plugins": plugins_with_total.list, "total": plugins_with_total.total}) +@console_ns.route("/workspaces/current/plugin//list") +class PluginCategoryListApi(Resource): + @console_ns.doc(params=query_params_from_model(PluginCategoryListQuery)) + @console_ns.response(200, "Success", console_ns.models[PluginCategoryListResponse.__name__]) + @setup_required + @login_required + @account_initialization_required + @with_current_tenant_id + def get(self, tenant_id: str, category: str): + args = PluginCategoryListQuery.model_validate(request.args.to_dict(flat=True)) + + try: + plugin_category = PluginCategory(category) + except ValueError: + return {"code": "invalid_param", "message": "invalid plugin category"}, 400 + + try: + plugins = PluginService.list_by_category(tenant_id, plugin_category, args.page, args.page_size) + except PluginDaemonClientSideError as e: + return {"code": "plugin_error", "message": e.description}, 400 + + builtin_tools = [] + if plugin_category == PluginCategory.Tool: + builtin_tools = _list_hardcoded_builtin_tool_providers(tenant_id) + + return dump_response( + PluginCategoryListResponse, + { + "plugins": jsonable_encoder(plugins.list), + "builtin_tools": builtin_tools, + "has_more": plugins.has_more, + }, + ) + + @console_ns.route("/workspaces/current/plugin/list/latest-versions") class PluginListLatestVersionsApi(Resource): @console_ns.expect(console_ns.models[ParserLatest.__name__]) @@ -713,11 +934,13 @@ class PluginChangePermissionApi(Resource): args = ParserPermissionChange.model_validate(console_ns.payload) - return { - "success": PluginPermissionService.change_permission( - tenant_id, args.install_permission, args.debug_permission - ) - } + set_permission_result = PluginPermissionService.change_permission( + tenant_id, args.install_permission, args.debug_permission + ) + if not set_permission_result: + return jsonable_encoder({"success": False, "message": "Failed to set permission"}) + + return jsonable_encoder({"success": True}) @console_ns.route("/workspaces/current/plugin/permission/fetch") @@ -806,10 +1029,10 @@ class PluginFetchDynamicSelectOptionsWithCredentialsApi(Resource): return jsonable_encoder({"options": options}) -@console_ns.route("/workspaces/current/plugin/preferences/change") -class PluginChangePreferencesApi(Resource): - @console_ns.expect(console_ns.models[ParserPreferencesChange.__name__]) - @console_ns.response(200, "Success", console_ns.models[PluginOperationSuccessResponse.__name__]) +@console_ns.route("/workspaces/current/plugin/auto-upgrade/change") +class PluginChangeAutoUpgradeApi(Resource): + @console_ns.expect(console_ns.models[ParserAutoUpgradeChange.__name__]) + @console_ns.response(200, "Success", console_ns.models[PluginAutoUpgradeChangeResponse.__name__]) @setup_required @login_required @account_initialization_required @@ -819,38 +1042,17 @@ class PluginChangePreferencesApi(Resource): if not user.is_admin_or_owner: raise Forbidden() - args = ParserPreferencesChange.model_validate(console_ns.payload) - - permission = args.permission - - install_permission = permission.install_permission - debug_permission = permission.debug_permission + args = ParserAutoUpgradeChange.model_validate(console_ns.payload) auto_upgrade = args.auto_upgrade - - strategy_setting = auto_upgrade.strategy_setting - upgrade_time_of_day = auto_upgrade.upgrade_time_of_day - upgrade_mode = auto_upgrade.upgrade_mode - exclude_plugins = auto_upgrade.exclude_plugins - include_plugins = auto_upgrade.include_plugins - - # set permission - set_permission_result = PluginPermissionService.change_permission( - tenant_id, - install_permission, - debug_permission, - ) - if not set_permission_result: - return jsonable_encoder({"success": False, "message": "Failed to set permission"}) - - # set auto upgrade strategy set_auto_upgrade_strategy_result = PluginAutoUpgradeService.change_strategy( tenant_id, - strategy_setting, - upgrade_time_of_day, - upgrade_mode, - exclude_plugins, - include_plugins, + auto_upgrade.strategy_setting, + auto_upgrade.upgrade_time_of_day, + auto_upgrade.upgrade_mode, + auto_upgrade.exclude_plugins, + auto_upgrade.include_plugins, + category=args.category, ) if not set_auto_upgrade_strategy_result: return jsonable_encoder({"success": False, "message": "Failed to set auto upgrade strategy"}) @@ -858,49 +1060,35 @@ class PluginChangePreferencesApi(Resource): return jsonable_encoder({"success": True}) -@console_ns.route("/workspaces/current/plugin/preferences/fetch") -class PluginFetchPreferencesApi(Resource): - @console_ns.response(200, "Success", console_ns.models[PluginPreferencesResponse.__name__]) +@console_ns.route("/workspaces/current/plugin/auto-upgrade/fetch") +class PluginFetchAutoUpgradeApi(Resource): + @console_ns.doc(params=query_params_from_model(ParserAutoUpgradeFetch)) + @console_ns.response(200, "Success", console_ns.models[PluginAutoUpgradeFetchResponse.__name__]) @setup_required @login_required @account_initialization_required @with_current_tenant_id def get(self, tenant_id: str): - permission = PluginPermissionService.get_permission(tenant_id) - permission_dict = { - "install_permission": TenantPluginPermission.InstallPermission.EVERYONE, - "debug_permission": TenantPluginPermission.DebugPermission.EVERYONE, - } + args = ParserAutoUpgradeFetch.model_validate(request.args.to_dict(flat=True)) + auto_upgrade = PluginAutoUpgradeService.get_strategy(tenant_id, args.category) + auto_upgrade_dict = ( + _auto_upgrade_settings_to_dict(auto_upgrade) + if auto_upgrade + else _default_auto_upgrade_settings(tenant_id, args.category) + ) - if permission: - permission_dict["install_permission"] = permission.install_permission - permission_dict["debug_permission"] = permission.debug_permission - - auto_upgrade = PluginAutoUpgradeService.get_strategy(tenant_id) - auto_upgrade_dict = { - "strategy_setting": TenantPluginAutoUpgradeStrategy.StrategySetting.DISABLED, - "upgrade_time_of_day": 0, - "upgrade_mode": TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE, - "exclude_plugins": [], - "include_plugins": [], - } - - if auto_upgrade: - auto_upgrade_dict = { - "strategy_setting": auto_upgrade.strategy_setting, - "upgrade_time_of_day": auto_upgrade.upgrade_time_of_day, - "upgrade_mode": auto_upgrade.upgrade_mode, - "exclude_plugins": auto_upgrade.exclude_plugins, - "include_plugins": auto_upgrade.include_plugins, + return jsonable_encoder( + { + "category": args.category, + "auto_upgrade": auto_upgrade_dict, } - - return jsonable_encoder({"permission": permission_dict, "auto_upgrade": auto_upgrade_dict}) + ) -@console_ns.route("/workspaces/current/plugin/preferences/autoupgrade/exclude") +@console_ns.route("/workspaces/current/plugin/auto-upgrade/exclude") class PluginAutoUpgradeExcludePluginApi(Resource): @console_ns.expect(console_ns.models[ParserExcludePlugin.__name__]) - @console_ns.response(200, "Success", console_ns.models[PluginOperationSuccessResponse.__name__]) + @console_ns.response(200, "Success", console_ns.models[SuccessResponse.__name__]) @setup_required @login_required @account_initialization_required @@ -909,7 +1097,9 @@ class PluginAutoUpgradeExcludePluginApi(Resource): # exclude one single plugin args = ParserExcludePlugin.model_validate(console_ns.payload) - return jsonable_encoder({"success": PluginAutoUpgradeService.exclude_plugin(tenant_id, args.plugin_id)}) + return jsonable_encoder( + {"success": PluginAutoUpgradeService.exclude_plugin(tenant_id, args.plugin_id, args.category)} + ) @console_ns.route("/workspaces/current/plugin/readme") diff --git a/api/controllers/console/workspace/workspace.py b/api/controllers/console/workspace/workspace.py index 7cf88e4453..59a33fe038 100644 --- a/api/controllers/console/workspace/workspace.py +++ b/api/controllers/console/workspace/workspace.py @@ -31,9 +31,9 @@ from controllers.console.wraps import ( from enums.cloud_plan import CloudPlan from extensions.ext_database import db from fields.base import ResponseModel -from libs.helper import TimestampField, dump_response, to_timestamp +from libs.helper import OptionalTimestampField, TimestampField, dump_response, to_timestamp from libs.login import login_required -from models.account import Account, Tenant, TenantCustomConfigDict, TenantStatus +from models.account import Account, Tenant, TenantAccountJoin, TenantCustomConfigDict, TenantStatus from services.account_service import TenantService from services.billing_service import BillingService, SubscriptionPlan from services.enterprise.enterprise_service import EnterpriseService @@ -219,6 +219,7 @@ tenants_fields = { "plan": fields.String, "status": fields.String, "created_at": TimestampField, + "last_opened_at": OptionalTimestampField, "current": fields.Boolean, } @@ -234,7 +235,12 @@ class TenantListApi(Resource): @with_current_user @with_current_tenant_id def get(self, current_tenant_id: str, current_user: Account): - tenants = TenantService.get_join_tenants(current_user) + tenant_rows: list[tuple[Tenant, TenantAccountJoin]] = [ + (tenant, membership) + for tenant, membership in TenantService.get_workspaces_for_account(db.session, current_user.id) + if tenant.status == TenantStatus.NORMAL + ] + tenants = [tenant for tenant, _ in tenant_rows] tenant_dicts = [] is_enterprise_only = dify_config.ENTERPRISE_ENABLED and not dify_config.BILLING_ENABLED is_saas = dify_config.EDITION == "CLOUD" and dify_config.BILLING_ENABLED @@ -247,7 +253,7 @@ class TenantListApi(Resource): if not tenant_plans: logger.warning("get_plan_bulk returned empty result, falling back to legacy feature path") - for tenant in tenants: + for tenant, membership in tenant_rows: plan: str = CloudPlan.SANDBOX if is_saas: tenant_plan = tenant_plans.get(tenant.id) @@ -266,6 +272,7 @@ class TenantListApi(Resource): "name": tenant.name, "status": tenant.status, "created_at": tenant.created_at, + "last_opened_at": membership.last_opened_at, "plan": plan, "current": tenant.id == current_tenant_id if current_tenant_id else False, } diff --git a/api/core/plugin/entities/plugin_daemon.py b/api/core/plugin/entities/plugin_daemon.py index 257638ad77..507a6ea5cd 100644 --- a/api/core/plugin/entities/plugin_daemon.py +++ b/api/core/plugin/entities/plugin_daemon.py @@ -168,6 +168,7 @@ class PluginInstallTask(BasePluginEntity): class PluginInstallTaskStartResponse(BaseModel): all_installed: bool = Field(description="Whether all plugins are installed.") task_id: str = Field(description="The ID of the install task.") + task: PluginInstallTask | None = Field(default=None, description="The install task.") class PluginVerification(BaseModel): @@ -206,6 +207,11 @@ class PluginListResponse(BaseModel): total: int +class PluginListWithoutTotalResponse(BaseModel): + list: list[PluginEntity] + has_more: bool + + class PluginDynamicSelectOptionsResponse(BaseModel): options: Sequence[PluginParameterOption] = Field(description="The options of the dynamic select.") diff --git a/api/core/plugin/impl/plugin.py b/api/core/plugin/impl/plugin.py index 8a7175bb51..34e8d315d8 100644 --- a/api/core/plugin/impl/plugin.py +++ b/api/core/plugin/impl/plugin.py @@ -6,6 +6,7 @@ from requests import HTTPError from core.plugin.entities.bundle import PluginBundleDependency from core.plugin.entities.plugin import ( MissingPluginDependency, + PluginCategory, PluginDeclaration, PluginEntity, PluginInstallation, @@ -16,6 +17,7 @@ from core.plugin.entities.plugin_daemon import ( PluginInstallTask, PluginInstallTaskStartResponse, PluginListResponse, + PluginListWithoutTotalResponse, PluginReadmeResponse, ) from core.plugin.impl.base import BasePluginClient @@ -74,6 +76,16 @@ class PluginInstaller(BasePluginClient): params={"page": page, "page_size": page_size, "response_type": "paged"}, ) + def list_plugins_by_category( + self, tenant_id: str, category: PluginCategory, page: int, page_size: int + ) -> PluginListWithoutTotalResponse: + return self._request_with_plugin_daemon_response( + "GET", + f"plugin/{tenant_id}/management/{category.value}/list", + PluginListWithoutTotalResponse, + params={"page": page, "page_size": page_size, "response_type": "paged"}, + ) + def upload_pkg( self, tenant_id: str, diff --git a/api/core/plugin/plugin_service.py b/api/core/plugin/plugin_service.py index 79c372690e..2ab3f87db7 100644 --- a/api/core/plugin/plugin_service.py +++ b/api/core/plugin/plugin_service.py @@ -31,6 +31,7 @@ from core.helper.marketplace import download_plugin_pkg from core.helper.model_provider_cache import ProviderCredentialsCache, ProviderCredentialsCacheType from core.plugin.entities.bundle import PluginBundleDependency from core.plugin.entities.plugin import ( + PluginCategory, PluginDeclaration, PluginEntity, PluginInstallation, @@ -41,6 +42,7 @@ from core.plugin.entities.plugin_daemon import ( PluginInstallTask, PluginInstallTaskStatus, PluginListResponse, + PluginListWithoutTotalResponse, PluginModelProviderEntity, PluginVerification, ) @@ -437,6 +439,19 @@ class PluginService: PluginService._reconcile_endpoint_counts(tenant_id, user_id, plugins.list) return plugins + @staticmethod + def list_by_category( + tenant_id: str, category: PluginCategory, page: int, page_size: int + ) -> PluginListWithoutTotalResponse: + """ + List plugins in one category with a has-more cursor signal and without calculating total. + + The daemon scans tenant installations in the existing list order and stops once it finds one extra match. + This keeps pagination usable before category is persisted on installation rows. + """ + manager = PluginInstaller() + return manager.list_plugins_by_category(tenant_id, category, page, page_size) + @staticmethod def _normalize_endpoint_count(value: object) -> int: """Convert daemon endpoint counters to safe non-negative integers. diff --git a/api/extensions/ext_commands.py b/api/extensions/ext_commands.py index 5d91d79f26..4d60bdb5f6 100644 --- a/api/extensions/ext_commands.py +++ b/api/extensions/ext_commands.py @@ -5,6 +5,7 @@ def init_app(app: DifyApp): from commands import ( add_qdrant_index, archive_workflow_runs, + backfill_plugin_auto_upgrade, clean_expired_messages, clean_workflow_runs, cleanup_orphaned_draft_variables, @@ -53,6 +54,7 @@ def init_app(app: DifyApp): upgrade_db, fix_app_site_missing, migrate_data_for_plugin, + backfill_plugin_auto_upgrade, extract_plugins, extract_unique_plugins, install_plugins, diff --git a/api/migrations/versions/2026_06_15_1100-b7c2d9e8a1f4_add_tenant_last_opened_at.py b/api/migrations/versions/2026_06_15_1100-b7c2d9e8a1f4_add_tenant_last_opened_at.py new file mode 100644 index 0000000000..ce2fd2b79c --- /dev/null +++ b/api/migrations/versions/2026_06_15_1100-b7c2d9e8a1f4_add_tenant_last_opened_at.py @@ -0,0 +1,26 @@ +"""add tenant account join last opened at + +Revision ID: b7c2d9e8a1f4 +Revises: 9f4b7c2d1a80 +Create Date: 2026-06-05 11:00:00.000000 + +""" + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "b7c2d9e8a1f4" +down_revision = "9f4b7c2d1a80" +branch_labels = None +depends_on = None + + +def upgrade(): + with op.batch_alter_table("tenant_account_joins", schema=None) as batch_op: + batch_op.add_column(sa.Column("last_opened_at", sa.DateTime(), nullable=True)) + + +def downgrade(): + with op.batch_alter_table("tenant_account_joins", schema=None) as batch_op: + batch_op.drop_column("last_opened_at") diff --git a/api/migrations/versions/2026_06_15_1200-f6a7b8c9d012_add_plugin_auto_upgrade_category.py b/api/migrations/versions/2026_06_15_1200-f6a7b8c9d012_add_plugin_auto_upgrade_category.py new file mode 100644 index 0000000000..491d8c201f --- /dev/null +++ b/api/migrations/versions/2026_06_15_1200-f6a7b8c9d012_add_plugin_auto_upgrade_category.py @@ -0,0 +1,42 @@ +"""add plugin auto upgrade category + +Revision ID: f6a7b8c9d012 +Revises: b7c2d9e8a1f4 +Create Date: 2026-05-15 12:00:00.000000 + +""" + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "f6a7b8c9d012" +down_revision = "b7c2d9e8a1f4" +branch_labels = None +depends_on = None + + +LEGACY_CATEGORY = "tool" +UNIQUE_CONSTRAINT_NAME = "unique_tenant_plugin_auto_upgrade_strategy" +UPGRADE_TIME_INDEX_NAME = "idx_tenant_plugin_auto_upgrade_strategy_time" +STRATEGY_TABLE_NAME = "tenant_plugin_auto_upgrade_strategies" + + +def upgrade(): + with op.batch_alter_table(STRATEGY_TABLE_NAME, schema=None) as batch_op: + batch_op.add_column( + sa.Column("category", sa.String(length=32), server_default=LEGACY_CATEGORY, nullable=False) + ) + batch_op.drop_constraint(UNIQUE_CONSTRAINT_NAME, type_="unique") + batch_op.create_unique_constraint(UNIQUE_CONSTRAINT_NAME, ["tenant_id", "category"]) + batch_op.create_index(UPGRADE_TIME_INDEX_NAME, ["upgrade_time_of_day"]) + + +def downgrade(): + op.execute(sa.text(f"DELETE FROM {STRATEGY_TABLE_NAME} WHERE category != '{LEGACY_CATEGORY}'")) + + with op.batch_alter_table(STRATEGY_TABLE_NAME, schema=None) as batch_op: + batch_op.drop_index(UPGRADE_TIME_INDEX_NAME) + batch_op.drop_constraint(UNIQUE_CONSTRAINT_NAME, type_="unique") + batch_op.drop_column("category") + batch_op.create_unique_constraint(UNIQUE_CONSTRAINT_NAME, ["tenant_id"]) diff --git a/api/migrations/versions/2026_06_15_1300-f5e8a9c0d2b3_add_learn_dify_flag_to_recommended_apps.py b/api/migrations/versions/2026_06_15_1300-f5e8a9c0d2b3_add_learn_dify_flag_to_recommended_apps.py new file mode 100644 index 0000000000..f0af2ddcaa --- /dev/null +++ b/api/migrations/versions/2026_06_15_1300-f5e8a9c0d2b3_add_learn_dify_flag_to_recommended_apps.py @@ -0,0 +1,26 @@ +"""add learn dify flag to recommended apps + +Revision ID: f5e8a9c0d2b3 +Revises: f6a7b8c9d012 +Create Date: 2026-05-18 15:00:00.000000 + +""" + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "f5e8a9c0d2b3" +down_revision = "f6a7b8c9d012" +branch_labels = None +depends_on = None + + +def upgrade(): + with op.batch_alter_table("recommended_apps", schema=None) as batch_op: + batch_op.add_column(sa.Column("is_learn_dify", sa.Boolean(), server_default=sa.text("false"), nullable=False)) + + +def downgrade(): + with op.batch_alter_table("recommended_apps", schema=None) as batch_op: + batch_op.drop_column("is_learn_dify") diff --git a/api/migrations/versions/2026_06_15_1400-c4d5e6f7a8b9_add_app_stars.py b/api/migrations/versions/2026_06_15_1400-c4d5e6f7a8b9_add_app_stars.py new file mode 100644 index 0000000000..780d94be9a --- /dev/null +++ b/api/migrations/versions/2026_06_15_1400-c4d5e6f7a8b9_add_app_stars.py @@ -0,0 +1,38 @@ +"""add app stars + +Revision ID: c4d5e6f7a8b9 +Revises: f5e8a9c0d2b3 +Create Date: 2026-06-08 12:00:00.000000 + +""" + +import sqlalchemy as sa +from alembic import op + +import models.types + +# revision identifiers, used by Alembic. +revision = "c4d5e6f7a8b9" +down_revision = "f5e8a9c0d2b3" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_table( + "app_stars", + sa.Column("id", models.types.StringUUID(), nullable=False), + sa.Column("tenant_id", models.types.StringUUID(), nullable=False), + sa.Column("app_id", models.types.StringUUID(), nullable=False), + sa.Column("account_id", models.types.StringUUID(), nullable=False), + sa.Column("created_at", sa.DateTime(), server_default=sa.func.current_timestamp(), nullable=False), + sa.PrimaryKeyConstraint("id", name="app_star_pkey"), + sa.UniqueConstraint("tenant_id", "account_id", "app_id", name="app_star_tenant_account_app_unique"), + ) + with op.batch_alter_table("app_stars", schema=None) as batch_op: + batch_op.create_index("app_star_tenant_account_idx", ["tenant_id", "account_id"], unique=False) + batch_op.create_index("app_star_app_idx", ["app_id"], unique=False) + + +def downgrade() -> None: + op.drop_table("app_stars") diff --git a/api/models/__init__.py b/api/models/__init__.py index 55bc642566..78ca43fa37 100644 --- a/api/models/__init__.py +++ b/api/models/__init__.py @@ -73,6 +73,7 @@ from .model import ( AppMCPServer, AppMode, AppModelConfig, + AppStar, Conversation, DatasetRetrieverResource, DifySetup, @@ -175,6 +176,7 @@ __all__ = [ "AppMCPServer", "AppMode", "AppModelConfig", + "AppStar", "AppTrigger", "AppTriggerStatus", "AppTriggerType", diff --git a/api/models/account.py b/api/models/account.py index a3074c6f63..66766693a5 100644 --- a/api/models/account.py +++ b/api/models/account.py @@ -301,6 +301,7 @@ class TenantAccountJoin(TypeBase): updated_at: Mapped[datetime] = mapped_column( DateTime, server_default=func.current_timestamp(), nullable=False, init=False, onupdate=func.current_timestamp() ) + last_opened_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, default=None) class AccountIntegrate(TypeBase): @@ -389,6 +390,14 @@ class TenantPluginPermission(TypeBase): class TenantPluginAutoUpgradeStrategy(TypeBase): + class PluginCategory(enum.StrEnum): + TOOL = "tool" + MODEL = "model" + EXTENSION = "extension" + AGENT_STRATEGY = "agent-strategy" + DATASOURCE = "datasource" + TRIGGER = "trigger" + class StrategySetting(enum.StrEnum): DISABLED = "disabled" FIX_ONLY = "fix_only" @@ -402,13 +411,20 @@ class TenantPluginAutoUpgradeStrategy(TypeBase): __tablename__ = "tenant_plugin_auto_upgrade_strategies" __table_args__ = ( sa.PrimaryKeyConstraint("id", name="tenant_plugin_auto_upgrade_strategy_pkey"), - sa.UniqueConstraint("tenant_id", name="unique_tenant_plugin_auto_upgrade_strategy"), + sa.UniqueConstraint("tenant_id", "category", name="unique_tenant_plugin_auto_upgrade_strategy"), + sa.Index("idx_tenant_plugin_auto_upgrade_strategy_time", "upgrade_time_of_day"), ) id: Mapped[str] = mapped_column( StringUUID, insert_default=lambda: str(uuid4()), default_factory=lambda: str(uuid4()), init=False ) tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) + category: Mapped[PluginCategory] = mapped_column( + EnumText(PluginCategory, length=32), + nullable=False, + server_default="tool", + default=PluginCategory.TOOL, + ) strategy_setting: Mapped[StrategySetting] = mapped_column( EnumText(StrategySetting, length=16), nullable=False, diff --git a/api/models/model.py b/api/models/model.py index 540cf0eeed..09809b85f6 100644 --- a/api/models/model.py +++ b/api/models/model.py @@ -397,6 +397,12 @@ class App(Base): __tablename__ = "apps" __table_args__ = (sa.PrimaryKeyConstraint("id", name="app_pkey"), sa.Index("app_tenant_id_idx", "tenant_id")) + if TYPE_CHECKING: + # Response-only attributes attached by app list/detail enrichers. + access_mode: str | None + has_draft_trigger: bool + is_starred: bool + id: Mapped[str] = mapped_column(StringUUID, default=lambda: str(uuid4())) tenant_id: Mapped[str] = mapped_column(StringUUID) name: Mapped[str] = mapped_column(String(255)) @@ -654,6 +660,28 @@ class App(Base): return None +class AppStar(Base): + """Account-scoped star marker for apps in a workspace.""" + + __tablename__ = "app_stars" + __table_args__ = ( + sa.PrimaryKeyConstraint("id", name="app_star_pkey"), + sa.UniqueConstraint("tenant_id", "account_id", "app_id", name="app_star_tenant_account_app_unique"), + sa.Index("app_star_tenant_account_idx", "tenant_id", "account_id"), + sa.Index("app_star_app_idx", "app_id"), + ) + + id: Mapped[str] = mapped_column(StringUUID, default=lambda: str(uuidv7())) + tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) + app_id: Mapped[str] = mapped_column(StringUUID, nullable=False) + account_id: Mapped[str] = mapped_column(StringUUID, nullable=False) + created_at: Mapped[datetime] = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) + + @override + def __repr__(self) -> str: + return f"" + + class AppModelConfig(TypeBase): __tablename__ = "app_model_configs" __table_args__ = (sa.PrimaryKeyConstraint("id", name="app_model_config_pkey"), sa.Index("app_app_id_idx", "app_id")) @@ -907,6 +935,9 @@ class RecommendedApp(TypeBase): custom_disclaimer: Mapped[str] = mapped_column(LongText, default="") position: Mapped[int] = mapped_column(sa.Integer, nullable=False, default=0) is_listed: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, default=True) + is_learn_dify: Mapped[bool] = mapped_column( + sa.Boolean, nullable=False, server_default=sa.text("false"), default=False + ) install_count: Mapped[int] = mapped_column(sa.Integer, nullable=False, default=0) language: Mapped[str] = mapped_column( String(255), diff --git a/api/openapi/markdown/console-openapi.md b/api/openapi/markdown/console-openapi.md index cf63b9de26..682822a6e4 100644 --- a/api/openapi/markdown/console-openapi.md +++ b/api/openapi/markdown/console-openapi.md @@ -531,6 +531,7 @@ Get list of applications with pagination and filtering | mode | query | App mode filter | No | string,
**Available values:** "advanced-chat", "agent", "agent-chat", "all", "channel", "chat", "completion", "workflow",
**Default:** all | | name | query | Filter by app name | No | string | | page | query | Page number (1-99999) | No | integer,
**Default:** 1 | +| sort_by | query | Sort apps by last modified, recently created, or earliest created | No | string,
**Available values:** "earliest_created", "last_modified", "recently_created",
**Default:** last_modified | | tag_ids | query | Filter by tag IDs | No | [ string ] | #### Responses @@ -600,6 +601,28 @@ Create a new application | 200 | Import confirmed | **application/json**: [Import](#import)
| | 400 | Import failed | **application/json**: [Import](#import)
| +### [GET] /apps/starred +Get applications starred by the current account + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| creator_ids | query | Filter by creator account IDs | No | [ string ] | +| is_created_by_me | query | Filter by creator | No | boolean | +| limit | query | Page size (1-100) | No | integer,
**Default:** 20 | +| mode | query | App mode filter | No | string,
**Available values:** "advanced-chat", "agent", "agent-chat", "all", "channel", "chat", "completion", "workflow",
**Default:** all | +| name | query | Filter by app name | No | string | +| page | query | Page number (1-99999) | No | integer,
**Default:** 1 | +| sort_by | query | Sort apps by last modified, recently created, or earliest created | No | string,
**Available values:** "earliest_created", "last_modified", "recently_created",
**Default:** last_modified | +| tag_ids | query | Filter by tag IDs | No | [ string ] | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Success | **application/json**: [AppPagination](#apppagination)
| + ### [POST] /apps/workflows/online-users Get workflow online users @@ -2045,6 +2068,38 @@ Reset access token for application site | 403 | Insufficient permissions (admin/owner required) | | | 404 | App or site not found | | +### [DELETE] /apps/{app_id}/star +Remove the current account's star from an application + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| app_id | path | Application ID | Yes | string | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| +| 404 | App not found | | + +### [POST] /apps/{app_id}/star +Star an application for the current account + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| app_id | path | Application ID | Yes | string | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| +| 404 | App not found | | + ### [GET] /apps/{app_id}/statistics/average-response-time Get average response time statistics for an application @@ -5573,6 +5628,19 @@ Check if dataset is in use | ---- | ----------- | ------ | | 200 | Success | **application/json**: [RecommendedAppListResponse](#recommendedapplistresponse)
| +### [GET] /explore/apps/learn-dify +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| language | query | Language code for recommended app localization | No | string | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Success | **application/json**: [LearnDifyAppListResponse](#learndifyapplistresponse)
| + ### [GET] /explore/apps/{app_id} #### Parameters @@ -9391,6 +9459,45 @@ Returns permission flags that control workspace features like member invitations | ---- | ----------- | ------ | | 200 | Success | **application/json**: [BinaryFileResponse](#binaryfileresponse)
| +### [POST] /workspaces/current/plugin/auto-upgrade/change +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ParserAutoUpgradeChange](#parserautoupgradechange)
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Success | **application/json**: [PluginAutoUpgradeChangeResponse](#pluginautoupgradechangeresponse)
| + +### [POST] /workspaces/current/plugin/auto-upgrade/exclude +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ParserExcludePlugin](#parserexcludeplugin)
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Success | **application/json**: [SuccessResponse](#successresponse)
| + +### [GET] /workspaces/current/plugin/auto-upgrade/fetch +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| category | query | | Yes | string,
**Available values:** "agent-strategy", "datasource", "extension", "model", "tool", "trigger" | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Success | **application/json**: [PluginAutoUpgradeFetchResponse](#pluginautoupgradefetchresponse)
| + ### [GET] /workspaces/current/plugin/debugging-key #### Responses @@ -9570,39 +9677,6 @@ Returns permission flags that control workspace features like member invitations | ---- | ----------- | ------ | | 200 | Success | **application/json**: [PluginPermissionResponse](#pluginpermissionresponse)
| -### [POST] /workspaces/current/plugin/preferences/autoupgrade/exclude -#### Request Body - -| Required | Schema | -| -------- | ------ | -| Yes | **application/json**: [ParserExcludePlugin](#parserexcludeplugin)
| - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Success | **application/json**: [PluginOperationSuccessResponse](#pluginoperationsuccessresponse)
| - -### [POST] /workspaces/current/plugin/preferences/change -#### Request Body - -| Required | Schema | -| -------- | ------ | -| Yes | **application/json**: [ParserPreferencesChange](#parserpreferenceschange)
| - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Success | **application/json**: [PluginOperationSuccessResponse](#pluginoperationsuccessresponse)
| - -### [GET] /workspaces/current/plugin/preferences/fetch -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Success | **application/json**: [PluginPreferencesResponse](#pluginpreferencesresponse)
| - ### [GET] /workspaces/current/plugin/readme #### Parameters @@ -9744,6 +9818,21 @@ Returns permission flags that control workspace features like member invitations | ---- | ----------- | ------ | | 200 | Success | **application/json**: [PluginDaemonOperationResponse](#plugindaemonoperationresponse)
| +### [GET] /workspaces/current/plugin/{category}/list +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| page | query | Page number | No | integer,
**Default:** 1 | +| page_size | query | Page size (1-256) | No | integer,
**Default:** 256 | +| category | path | | Yes | string | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Success | **application/json**: [PluginCategoryListResponse](#plugincategorylistresponse)
| + ### [GET] /workspaces/current/tool-labels #### Responses @@ -10641,7 +10730,7 @@ Default namespace | deprecated | boolean | | No | | features | [ [ModelFeature](#modelfeature) ] | | No | | fetch_from | [FetchFrom](#fetchfrom) | | Yes | -| label | [I18nObject](#i18nobject) | | Yes | +| label | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | Yes | | model | string | | Yes | | model_properties | object | | Yes | | model_type | [ModelType](#modeltype) | | Yes | @@ -12167,6 +12256,7 @@ Enum class for api provider schema type. | mode | string,
**Available values:** "advanced-chat", "agent", "agent-chat", "all", "channel", "chat", "completion", "workflow",
**Default:** all | App mode filter
*Enum:* `"advanced-chat"`, `"agent"`, `"agent-chat"`, `"all"`, `"channel"`, `"chat"`, `"completion"`, `"workflow"` | No | | name | string | Filter by app name | No | | page | integer,
**Default:** 1 | Page number (1-99999) | No | +| sort_by | string,
**Available values:** "earliest_created", "last_modified", "recently_created",
**Default:** last_modified | Sort apps by last modified, recently created, or earliest created
*Enum:* `"earliest_created"`, `"last_modified"`, `"recently_created"` | No | | tag_ids | [ string ] | Filter by tag IDs | No | #### AppMCPServerResponse @@ -12222,6 +12312,7 @@ AppMCPServer Status Enum | icon_background | string | | No | | icon_type | string | | No | | id | string | | Yes | +| is_starred | boolean | | No | | max_active_requests | integer | | No | | mode_compatible_with_agent | string | | Yes | | name | string | | Yes | @@ -13050,10 +13141,10 @@ Model class for credential form schema. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | default | string | | No | -| label | [I18nObject](#i18nobject) | | Yes | +| label | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | Yes | | max_length | integer | | No | | options | [ [FormOption](#formoption) ] | | No | -| placeholder | [I18nObject](#i18nobject) | | No | +| placeholder | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | No | | required | boolean,
**Default:** true | | No | | show_on | [ [FormShowOnObject](#formshowonobject) ],
**Default:** | | No | | type | [FormType](#formtype) | | Yes | @@ -14473,8 +14564,8 @@ Enum class for fetch from. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| label | [I18nObject](#i18nobject) | | Yes | -| placeholder | [I18nObject](#i18nobject) | | No | +| label | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | Yes | +| placeholder | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | No | #### FileInfo @@ -14605,7 +14696,7 @@ Model class for form option. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| label | [I18nObject](#i18nobject) | | Yes | +| label | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | Yes | | show_on | [ [FormShowOnObject](#formshowonobject) ],
**Default:** | | No | | value | string | | Yes | @@ -14837,6 +14928,8 @@ Model class for i18n object. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | en_US | string | | Yes | +| ja_JP | string | | No | +| pt_BR | string | | No | | zh_Hans | string | | No | #### IconInfo @@ -15098,6 +15191,12 @@ Enum class for large language model mode. | ---- | ---- | ----------- | -------- | | LLMMode | string | Enum class for large language model mode. | | +#### LearnDifyAppListResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| recommended_apps | [ [RecommendedAppResponse](#recommendedappresponse) ] | | Yes | + #### LegacyEndpointUpdatePayload | Name | Type | Description | Required | @@ -15932,8 +16031,8 @@ Model class for parameter rule. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | default | | | No | -| help | [I18nObject](#i18nobject) | | No | -| label | [I18nObject](#i18nobject) | | Yes | +| help | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | No | +| label | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | Yes | | max | number | | No | | min | number | | No | | name | string | | Yes | @@ -15983,6 +16082,19 @@ Enum class for parameter type. | file_name | string | | Yes | | plugin_unique_identifier | string | | Yes | +#### ParserAutoUpgradeChange + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| auto_upgrade | [PluginAutoUpgradeSettingsPayload](#pluginautoupgradesettingspayload) | | Yes | +| category | [PluginCategory](#plugincategory) | | Yes | + +#### ParserAutoUpgradeFetch + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| category | [PluginCategory](#plugincategory) | | Yes | + #### ParserCreateCredential | Name | Type | Description | Required | @@ -16079,6 +16191,7 @@ Enum class for parameter type. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | +| category | [PluginCategory](#plugincategory) | | Yes | | plugin_id | string | | Yes | #### ParserGetCredentials @@ -16166,8 +16279,8 @@ Enum class for parameter type. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| debug_permission | [DebugPermission](#debugpermission) | | Yes | -| install_permission | [InstallPermission](#installpermission) | | Yes | +| debug_permission | [DebugPermission](#debugpermission) | | No | +| install_permission | [InstallPermission](#installpermission) | | No | #### ParserPluginIdentifierQuery @@ -16197,13 +16310,6 @@ Enum class for parameter type. | model | string | | Yes | | model_type | [ModelType](#modeltype) | | Yes | -#### ParserPreferencesChange - -| Name | Type | Description | Required | -| ---- | ---- | ----------- | -------- | -| auto_upgrade | [PluginAutoUpgradeSettingsPayload](#pluginautoupgradesettingspayload) | | Yes | -| permission | [PluginPermissionSettingsPayload](#pluginpermissionsettingspayload) | | Yes | - #### ParserPreferredProviderType | Name | Type | Description | Required | @@ -16348,6 +16454,20 @@ Shared permission levels for resources (datasets, credentials, etc.) | unit | string | | No | | variable | string | | Yes | +#### PluginAutoUpgradeChangeResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| message | string | | No | +| success | boolean | | Yes | + +#### PluginAutoUpgradeFetchResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| auto_upgrade | [PluginAutoUpgradeSettingsResponseModel](#pluginautoupgradesettingsresponsemodel) | | Yes | +| category | [PluginCategory](#plugincategory) | | Yes | + #### PluginAutoUpgradeSettingsPayload | Name | Type | Description | Required | @@ -16358,6 +16478,90 @@ Shared permission levels for resources (datasets, credentials, etc.) | upgrade_mode | [UpgradeMode](#upgrademode) | | No | | upgrade_time_of_day | integer | | No | +#### PluginAutoUpgradeSettingsResponseModel + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| exclude_plugins | [ string ] | | Yes | +| include_plugins | [ string ] | | Yes | +| strategy_setting | [StrategySetting](#strategysetting) | | Yes | +| upgrade_mode | [UpgradeMode](#upgrademode) | | Yes | +| upgrade_time_of_day | integer | | Yes | + +#### PluginCategory + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| PluginCategory | string | | | + +#### PluginCategoryBuiltinToolProviderResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| allow_delete | boolean | | Yes | +| author | string | | Yes | +| description | [core__tools__entities__common_entities__I18nObject](#core__tools__entities__common_entities__i18nobject) | | Yes | +| icon | string
object | | Yes | +| icon_dark | string
object | | Yes | +| id | string | | Yes | +| is_team_authorization | boolean | | Yes | +| label | [core__tools__entities__common_entities__I18nObject](#core__tools__entities__common_entities__i18nobject) | | Yes | +| labels | [ string ] | | Yes | +| name | string | | Yes | +| plugin_id | string | | Yes | +| plugin_unique_identifier | string | | Yes | +| team_credentials | object | | Yes | +| tools | [ [PluginCategoryBuiltinToolResponse](#plugincategorybuiltintoolresponse) ] | | Yes | +| type | [ToolProviderType](#toolprovidertype) | | Yes | + +#### PluginCategoryBuiltinToolResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| author | string | | Yes | +| description | [core__tools__entities__common_entities__I18nObject](#core__tools__entities__common_entities__i18nobject) | | Yes | +| label | [core__tools__entities__common_entities__I18nObject](#core__tools__entities__common_entities__i18nobject) | | Yes | +| labels | [ string ] | | Yes | +| name | string | | Yes | +| output_schema | object | | Yes | +| parameters | [ object ] | | No | + +#### PluginCategoryInstalledPluginResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| checksum | string | | Yes | +| created_at | dateTime | | Yes | +| declaration | [PluginDeclarationResponse](#plugindeclarationresponse) | | Yes | +| endpoints_active | integer | | Yes | +| endpoints_setups | integer | | Yes | +| id | string | | Yes | +| installation_id | string | | Yes | +| meta | object | | Yes | +| name | string | | Yes | +| plugin_id | string | | Yes | +| plugin_unique_identifier | string | | Yes | +| runtime_type | string | | Yes | +| source | [PluginInstallationSource](#plugininstallationsource) | | Yes | +| tenant_id | string | | Yes | +| updated_at | dateTime | | Yes | +| version | string | | Yes | + +#### PluginCategoryListQuery + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| page | integer,
**Default:** 1 | Page number | No | +| page_size | integer,
**Default:** 256 | Page size (1-256) | No | + +#### PluginCategoryListResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| builtin_tools | [ [PluginCategoryBuiltinToolProviderResponse](#plugincategorybuiltintoolproviderresponse) ] | | Yes | +| has_more | boolean | | Yes | +| plugins | [ [PluginCategoryInstalledPluginResponse](#plugincategoryinstalledpluginresponse) ] | | Yes | + #### PluginDaemonOperationResponse | Name | Type | Description | Required | @@ -16372,6 +16576,32 @@ Shared permission levels for resources (datasets, credentials, etc.) | key | string | | Yes | | port | integer | | Yes | +#### PluginDeclarationResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| agent_strategy | object | | No | +| author | string | | Yes | +| category | [PluginCategory](#plugincategory) | | Yes | +| created_at | dateTime | | Yes | +| datasource | object | | No | +| description | [core__tools__entities__common_entities__I18nObject](#core__tools__entities__common_entities__i18nobject) | | Yes | +| endpoint | object | | No | +| icon | string | | Yes | +| icon_dark | string | | No | +| label | [core__tools__entities__common_entities__I18nObject](#core__tools__entities__common_entities__i18nobject) | | Yes | +| meta | object | | Yes | +| model | [ProviderEntityResponse](#providerentityresponse) | | No | +| name | string | | Yes | +| plugins | object | | Yes | +| repo | string | | No | +| resource | object | | Yes | +| tags | [ string ] | | No | +| tool | object | | No | +| trigger | object | | No | +| verified | boolean | | No | +| version | string | | Yes | + #### PluginDependency | Name | Type | Description | Required | @@ -16405,6 +16635,12 @@ Shared permission levels for resources (datasets, credentials, etc.) | ---- | ---- | ----------- | -------- | | PluginInstallationScope | string | | | +#### PluginInstallationSource + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| PluginInstallationSource | string | | | + #### PluginInstallationsResponse | Name | Type | Description | Required | @@ -16457,13 +16693,6 @@ Shared permission levels for resources (datasets, credentials, etc.) | debug_permission | [DebugPermission](#debugpermission) | | No | | install_permission | [InstallPermission](#installpermission) | | No | -#### PluginPreferencesResponse - -| Name | Type | Description | Required | -| ---- | ---- | ----------- | -------- | -| auto_upgrade | [PluginAutoUpgradeSettingsPayload](#pluginautoupgradesettingspayload) | | Yes | -| permission | [PluginPermissionSettingsPayload](#pluginpermissionsettingspayload) | | Yes | - #### PluginReadmeResponse | Name | Type | Description | Required | @@ -16550,14 +16779,35 @@ Model class for provider credential schema. | error | string | | No | | result | string,
**Available values:** "error", "success" | *Enum:* `"error"`, `"success"` | Yes | +#### ProviderEntityResponse + +Runtime provider response with codegen-safe model pricing schemas. + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| background | string | | No | +| configurate_methods | [ [ConfigurateMethod](#configuratemethod) ] | | Yes | +| description | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | No | +| help | [ProviderHelpEntity](#providerhelpentity) | | No | +| icon_small | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | No | +| icon_small_dark | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | No | +| label | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | Yes | +| model_credential_schema | [ModelCredentialSchema](#modelcredentialschema) | | No | +| models | [ [AIModelEntityResponse](#aimodelentityresponse) ],
**Default:** | | No | +| position | object | | No | +| provider | string | | Yes | +| provider_credential_schema | [ProviderCredentialSchema](#providercredentialschema) | | No | +| provider_name | string | | No | +| supported_model_types | [ [ModelType](#modeltype) ] | | Yes | + #### ProviderHelpEntity Model class for provider help. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| title | [I18nObject](#i18nobject) | | Yes | -| url | [I18nObject](#i18nobject) | | Yes | +| title | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | Yes | +| url | [graphon__model_runtime__entities__common_entities__I18nObject](#graphon__model_runtime__entities__common_entities__i18nobject) | | Yes | #### ProviderModelWithStatusEntity @@ -17473,6 +17723,19 @@ Query parameters for listing snippet published workflows. | updated_by | [SimpleAccount](#simpleaccount) | | No | | version | string | | Yes | +#### StarredAppListQuery + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| creator_ids | [ string ] | Filter by creator account IDs | No | +| is_created_by_me | boolean | Filter by creator | No | +| limit | integer,
**Default:** 20 | Page size (1-100) | No | +| mode | string,
**Available values:** "advanced-chat", "agent", "agent-chat", "all", "channel", "chat", "completion", "workflow",
**Default:** all | App mode filter
*Enum:* `"advanced-chat"`, `"agent"`, `"agent-chat"`, `"all"`, `"channel"`, `"chat"`, `"completion"`, `"workflow"` | No | +| name | string | Filter by app name | No | +| page | integer,
**Default:** 1 | Page number (1-99999) | No | +| sort_by | string,
**Available values:** "earliest_created", "last_modified", "recently_created",
**Default:** last_modified | Sort apps by last modified, recently created, or earliest created
*Enum:* `"earliest_created"`, `"last_modified"`, `"recently_created"` | No | +| tag_ids | [ string ] | Filter by tag IDs | No | + #### StatisticTimeRangeQuery | Name | Type | Description | Required | @@ -17817,6 +18080,14 @@ Tag type | ---- | ---- | ----------- | -------- | | ToolProviderOpaqueResponse | | | | +#### ToolProviderType + +Enum class for tool provider + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| ToolProviderType | string | Enum class for tool provider | | + #### TraceAppConfigResponse | Name | Type | Description | Required | @@ -19236,6 +19507,26 @@ Workflow tool configuration | use_count | integer | | No | | version | integer | | No | +#### core__tools__entities__common_entities__I18nObject + +Model class for i18n object. + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| en_US | string | | Yes | +| ja_JP | string | | No | +| pt_BR | string | | No | +| zh_Hans | string | | No | + +#### graphon__model_runtime__entities__common_entities__I18nObject + +Model class for i18n object. + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| en_US | string | | Yes | +| zh_Hans | string | | No | + ## FastOpenAPI Preview (OpenAPI 3.1) ### Dify API (FastOpenAPI PoC) diff --git a/api/schedule/check_upgradable_plugin_task.py b/api/schedule/check_upgradable_plugin_task.py index cf223f6e9e..f19a2bf18d 100644 --- a/api/schedule/check_upgradable_plugin_task.py +++ b/api/schedule/check_upgradable_plugin_task.py @@ -73,6 +73,7 @@ def check_upgradable_plugin_task(): strategy.upgrade_mode, strategy.exclude_plugins, strategy.include_plugins, + strategy.category, ) # Only sleep if batch_interval_time > 0.0001 AND current batch is not the last one diff --git a/api/services/account_service.py b/api/services/account_service.py index 1ab5fbd450..e39d13a392 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -70,6 +70,7 @@ from services.errors.account import ( ) from services.errors.workspace import WorkSpaceNotAllowedCreateError, WorkspacesLimitExceededError from services.feature_service import FeatureService +from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService from tasks.delete_account_task import delete_account_task from tasks.mail_account_deletion_task import send_account_deletion_verification_code from tasks.mail_change_mail_task import ( @@ -263,6 +264,7 @@ class AccountService: account.set_tenant_id(available_ta.tenant_id) available_ta.current = True + available_ta.last_opened_at = naive_utc_now() db.session.commit() AccountService._refresh_account_last_active(account) @@ -1167,15 +1169,17 @@ class TenantService: db.session.add(tenant) db.session.commit() - plugin_upgrade_strategy = TenantPluginAutoUpgradeStrategy( - tenant_id=tenant.id, - strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY, - upgrade_time_of_day=0, - upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE, - exclude_plugins=[], - include_plugins=[], - ) - db.session.add(plugin_upgrade_strategy) + for category in TenantPluginAutoUpgradeStrategy.PluginCategory: + plugin_upgrade_strategy = TenantPluginAutoUpgradeStrategy( + tenant_id=tenant.id, + category=category, + strategy_setting=PluginAutoUpgradeService.default_strategy_setting_for_category(category), + upgrade_time_of_day=PluginAutoUpgradeService.default_upgrade_time_of_day(tenant.id), + upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE, + exclude_plugins=[], + include_plugins=[], + ) + db.session.add(plugin_upgrade_strategy) db.session.commit() tenant.encrypt_public_key = generate_key_pair(tenant.id) @@ -1447,6 +1451,7 @@ class TenantService: .values(current=False) ) tenant_account_join.current = True + tenant_account_join.last_opened_at = naive_utc_now() # Set the current tenant for the account account.set_tenant_id(tenant_account_join.tenant_id) db.session.commit() diff --git a/api/services/app_service.py b/api/services/app_service.py index 837e1a1449..d3437a4575 100644 --- a/api/services/app_service.py +++ b/api/services/app_service.py @@ -23,7 +23,7 @@ from graphon.model_runtime.entities.model_entities import ModelPropertyKey, Mode from graphon.model_runtime.model_providers.base.large_language_model import LargeLanguageModel from libs.datetime_utils import naive_utc_now from libs.login import current_user -from models import Account +from models import Account, AppStar from models.agent import Agent, AgentIconType, AgentScope, AgentSource, AgentStatus from models.model import App, AppMode, AppModelConfig, IconType, Site from models.tools import ApiToolProvider @@ -36,19 +36,29 @@ from tasks.remove_app_and_related_data_task import remove_app_and_related_data_t logger = logging.getLogger(__name__) +AppListSortBy = Literal["last_modified", "recently_created", "earliest_created"] -class AppListParams(BaseModel): + +class AppListBaseParams(BaseModel): page: int = Field(default=1, ge=1) limit: int = Field(default=20, ge=1, le=100) mode: Literal["completion", "chat", "advanced-chat", "workflow", "agent-chat", "agent", "channel", "all"] = "all" + sort_by: AppListSortBy = "last_modified" name: str | None = None tag_ids: list[str] | None = None creator_ids: list[str] | None = None is_created_by_me: bool | None = None + + +class AppListParams(AppListBaseParams): status: str | None = None openapi_visible: bool = False +class StarredAppListParams(AppListBaseParams): + pass + + class CreateAppParams(BaseModel): name: str = Field(min_length=1) description: str | None = None @@ -62,6 +72,83 @@ class CreateAppParams(BaseModel): class AppService: + @staticmethod + def _build_app_list_filters( + user_id: str, tenant_id: str, params: AppListBaseParams + ) -> list[sa.ColumnElement[bool]]: + filters = [App.tenant_id == tenant_id, App.is_universal == False] + + if params.mode == "workflow": + filters.append(App.mode == AppMode.WORKFLOW) + elif params.mode == "completion": + filters.append(App.mode == AppMode.COMPLETION) + elif params.mode == "chat": + filters.append(App.mode == AppMode.CHAT) + elif params.mode == "advanced-chat": + filters.append(App.mode == AppMode.ADVANCED_CHAT) + elif params.mode == "agent-chat": + filters.append(App.mode == AppMode.AGENT_CHAT) + elif params.mode == "agent": + filters.append(App.mode == AppMode.AGENT) + + if isinstance(params, AppListParams): + if params.status: + filters.append(App.status == params.status) + # OpenAPI surface visibility gate. Pushed into the query so + # `pagination.total` reflects only apps the openapi caller can + # actually reach; post-filtering by enable_api after the page + # arrives would make `total` page-dependent. + if params.openapi_visible: + filters.append(App.enable_api.is_(True)) + + if params.is_created_by_me: + filters.append(App.created_by == user_id) + if params.creator_ids: + filters.append(App.created_by.in_(params.creator_ids)) + if params.name: + from libs.helper import escape_like_pattern + + name = params.name[:30] + escaped_name = escape_like_pattern(name) + filters.append(App.name.ilike(f"%{escaped_name}%", escape="\\")) + if params.tag_ids and len(params.tag_ids) > 0: + target_ids = TagService.get_target_ids_by_tag_ids("app", tenant_id, params.tag_ids, match_all=True) + if target_ids and len(target_ids) > 0: + filters.append(App.id.in_(target_ids)) + else: + return [] + + return filters + + @staticmethod + def _build_app_list_order_by(sort_by: AppListSortBy) -> sa.ColumnElement[Any]: + return { + "last_modified": App.updated_at.desc(), + "recently_created": App.created_at.desc(), + "earliest_created": App.created_at.asc(), + }[sort_by] + + @staticmethod + def get_starred_app_ids( + session: Session | scoped_session, + *, + tenant_id: str, + account_id: str, + app_ids: Sequence[str], + ) -> set[str]: + """Return app IDs starred by this account within the tenant.""" + if not app_ids: + return set() + + starred_app_ids = session.scalars( + select(AppStar.app_id).where( + AppStar.tenant_id == tenant_id, + AppStar.account_id == account_id, + AppStar.app_id.in_(list(app_ids)), + ) + ).all() + return set(starred_app_ids) + @staticmethod def get_app_by_id( session: Session | scoped_session, @@ -109,61 +196,104 @@ class AppService: def get_paginate_apps(self, user_id: str, tenant_id: str, params: AppListParams) -> Pagination | None: """ - Get app list with pagination + Get app list with pagination, filters, and explicit sort order. :param user_id: user id :param tenant_id: tenant id :param params: query parameters :return: """ - filters = [App.tenant_id == tenant_id, App.is_universal == False] + filters = self._build_app_list_filters(user_id, tenant_id, params) + if not filters: + return None - if params.mode == "workflow": - filters.append(App.mode == AppMode.WORKFLOW) - elif params.mode == "completion": - filters.append(App.mode == AppMode.COMPLETION) - elif params.mode == "chat": - filters.append(App.mode == AppMode.CHAT) - elif params.mode == "advanced-chat": - filters.append(App.mode == AppMode.ADVANCED_CHAT) - elif params.mode == "agent-chat": - filters.append(App.mode == AppMode.AGENT_CHAT) - elif params.mode == "agent": - filters.append(App.mode == AppMode.AGENT) - - if params.status: - filters.append(App.status == params.status) - # OpenAPI surface visibility gate. Pushed into the query so - # `pagination.total` reflects only apps the openapi caller can - # actually reach — post-filtering by enable_api after the page - # arrives would make `total` page-dependent. - if params.openapi_visible: - filters.append(App.enable_api.is_(True)) - if params.is_created_by_me: - filters.append(App.created_by == user_id) - if params.creator_ids: - filters.append(App.created_by.in_(params.creator_ids)) - if params.name: - from libs.helper import escape_like_pattern - - name = params.name[:30] - escaped_name = escape_like_pattern(name) - filters.append(App.name.ilike(f"%{escaped_name}%", escape="\\")) - if params.tag_ids and len(params.tag_ids) > 0: - target_ids = TagService.get_target_ids_by_tag_ids("app", tenant_id, params.tag_ids, match_all=True) - if target_ids and len(target_ids) > 0: - filters.append(App.id.in_(target_ids)) - else: - return None + order_by = self._build_app_list_order_by(params.sort_by) app_models = db.paginate( - sa.select(App).where(*filters).order_by(App.created_at.desc()), + sa.select(App).where(*filters).order_by(order_by), page=params.page, per_page=params.limit, error_out=False, ) + app_ids = [str(app.id) for app in app_models.items] + starred_app_ids = self.get_starred_app_ids( + db.session, + tenant_id=tenant_id, + account_id=user_id, + app_ids=app_ids, + ) + for app in app_models.items: + app.is_starred = str(app.id) in starred_app_ids + return app_models + def get_paginate_starred_apps( + self, user_id: str, tenant_id: str, params: StarredAppListParams + ) -> Pagination | None: + """ + Get apps starred by the current account with pagination, filters, and explicit sort order. + """ + filters = self._build_app_list_filters(user_id, tenant_id, params) + if not filters: + return None + + order_by = self._build_app_list_order_by(params.sort_by) + app_models = db.paginate( + sa.select(App) + .join( + AppStar, + sa.and_( + AppStar.tenant_id == App.tenant_id, + AppStar.app_id == App.id, + AppStar.account_id == user_id, + ), + ) + .where(AppStar.tenant_id == tenant_id, *filters) + .order_by(order_by), + page=params.page, + per_page=params.limit, + error_out=False, + ) + + for app in app_models.items: + app.is_starred = True + + return app_models + + @staticmethod + def star_app(session: Session, *, app: App, account_id: str) -> None: + """Create the account's app star if it does not already exist.""" + existing_star = session.scalar( + select(AppStar) + .where( + AppStar.tenant_id == app.tenant_id, + AppStar.app_id == app.id, + AppStar.account_id == account_id, + ) + .limit(1) + ) + if existing_star: + return + + session.add(AppStar(tenant_id=app.tenant_id, app_id=app.id, account_id=account_id)) + + @staticmethod + def unstar_app(session: Session, *, app: App, account_id: str) -> None: + """Remove the account's app star if present.""" + existing_star = session.scalar( + select(AppStar) + .where( + AppStar.tenant_id == app.tenant_id, + AppStar.app_id == app.id, + AppStar.account_id == account_id, + ) + .limit(1) + ) + if not existing_star: + return + + session.delete(existing_star) + def create_app(self, tenant_id: str, params: CreateAppParams, account: Account) -> App: """ Create app diff --git a/api/services/entities/model_provider_entities.py b/api/services/entities/model_provider_entities.py index 7499ebbc18..020dc4a2ea 100644 --- a/api/services/entities/model_provider_entities.py +++ b/api/services/entities/model_provider_entities.py @@ -192,6 +192,27 @@ class SimpleProviderEntityResponse(BaseModel): return self +class ProviderEntityResponse(BaseModel): + """Runtime provider response with codegen-safe model pricing schemas.""" + + provider: str + provider_name: str = "" + label: I18nObject + description: I18nObject | None = None + icon_small: I18nObject | None = None + icon_small_dark: I18nObject | None = None + background: str | None = None + help: ProviderHelpEntity | None = None + supported_model_types: Sequence[ModelType] + configurate_methods: list[ConfigurateMethod] + models: list[AIModelEntityResponse] = [] + provider_credential_schema: ProviderCredentialSchema | None = None + model_credential_schema: ModelCredentialSchema | None = None + position: dict[str, list[str]] | None = {} + + model_config = ConfigDict(from_attributes=True, protected_namespaces=()) + + class DefaultModelResponse(BaseModel): """ Default model entity. diff --git a/api/services/plugin/plugin_auto_upgrade_service.py b/api/services/plugin/plugin_auto_upgrade_service.py index b96b8140ac..0e13214ee7 100644 --- a/api/services/plugin/plugin_auto_upgrade_service.py +++ b/api/services/plugin/plugin_auto_upgrade_service.py @@ -1,18 +1,295 @@ +"""Manage tenant plugin auto-upgrade strategies. + +The storage is category-scoped: each tenant can have one strategy per plugin +category. Public mutation helpers require an explicit category so callers do +not accidentally overwrite every plugin type with one workspace-level policy. +""" + +import logging +from dataclasses import dataclass +from hashlib import sha256 + from sqlalchemy import select +from sqlalchemy.orm import Session from core.db.session_factory import session_factory +from core.plugin.impl.plugin import PluginInstaller from models.account import TenantPluginAutoUpgradeStrategy +logger = logging.getLogger(__name__) + +PluginCategory = TenantPluginAutoUpgradeStrategy.PluginCategory +PLUGIN_CATEGORIES = tuple(PluginCategory) +SECONDS_PER_DAY = 24 * 60 * 60 +AUTO_UPGRADE_CHECK_SLOT_SECONDS = 15 * 60 +AUTO_UPGRADE_CHECK_SLOT_COUNT = SECONDS_PER_DAY // AUTO_UPGRADE_CHECK_SLOT_SECONDS + + +@dataclass(frozen=True) +class PluginAutoUpgradeBackfillResult: + created_count: int + normalized: bool + class PluginAutoUpgradeService: @staticmethod - def get_strategy(tenant_id: str) -> TenantPluginAutoUpgradeStrategy | None: - with session_factory.create_session() as session: - return session.scalar( - select(TenantPluginAutoUpgradeStrategy) - .where(TenantPluginAutoUpgradeStrategy.tenant_id == tenant_id) - .limit(1) + def default_strategy_setting_for_category( + category: PluginCategory, + ) -> TenantPluginAutoUpgradeStrategy.StrategySetting: + if category == PluginCategory.MODEL: + return TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST + return TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY + + @staticmethod + def default_upgrade_time_of_day(tenant_id: str) -> int: + """Spread default checks across 15-minute aligned slots by tenant.""" + hash_input = tenant_id.encode() + slot = int.from_bytes(sha256(hash_input).digest()[:8], "big") % AUTO_UPGRADE_CHECK_SLOT_COUNT + return slot * AUTO_UPGRADE_CHECK_SLOT_SECONDS + + @staticmethod + def _coerce_category(category: object) -> PluginCategory | None: + """Accept daemon enum/string categories and ignore unknown values.""" + category_value = getattr(category, "value", category) + if category_value is None: + return None + + try: + return PluginCategory(str(category_value)) + except ValueError: + return None + + @staticmethod + def _get_installed_plugin_categories(tenant_id: str) -> dict[str, PluginCategory]: + """Build a plugin_id -> category map for splitting legacy include/exclude lists.""" + installed_plugins = PluginInstaller().list_plugins(tenant_id) + plugin_categories: dict[str, PluginCategory] = {} + + for plugin in installed_plugins: + plugin_category = PluginAutoUpgradeService._coerce_category(plugin.declaration.category) + if plugin_category is not None: + plugin_categories[plugin.plugin_id] = plugin_category + + return plugin_categories + + @staticmethod + def _filter_plugin_ids_for_category( + plugin_ids: list[str], + category: PluginCategory, + plugin_categories: dict[str, PluginCategory], + ) -> list[str]: + return [plugin_id for plugin_id in plugin_ids if plugin_categories.get(plugin_id) == category] + + @staticmethod + def _log_unknown_plugin_ids( + tenant_id: str, + field_name: str, + plugin_ids: list[str], + plugin_categories: dict[str, PluginCategory], + ) -> None: + unknown_plugin_ids = [plugin_id for plugin_id in plugin_ids if plugin_id not in plugin_categories] + if not unknown_plugin_ids: + return + + logger.warning( + "Skipped unknown plugin IDs while backfilling plugin auto-upgrade strategies: " + "tenant_id=%s, field=%s, plugin_ids=%s", + tenant_id, + field_name, + unknown_plugin_ids, + ) + + @staticmethod + def _has_default_strategy(strategy: TenantPluginAutoUpgradeStrategy) -> bool: + return ( + strategy.strategy_setting == TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY + and strategy.upgrade_time_of_day == 0 + and strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE + and not strategy.exclude_plugins + and not strategy.include_plugins + ) + + @staticmethod + def _strategy_setting_for_category( + source_strategy: TenantPluginAutoUpgradeStrategy, + category: PluginCategory, + source_has_default_strategy: bool, + ) -> TenantPluginAutoUpgradeStrategy.StrategySetting: + # Only pure legacy defaults adopt the new model=latest default. User-edited + # strategies keep their original setting across all categories. + if source_has_default_strategy: + return PluginAutoUpgradeService.default_strategy_setting_for_category(category) + return source_strategy.strategy_setting + + @staticmethod + def _upgrade_time_of_day_for_category( + tenant_id: str, + source_strategy: TenantPluginAutoUpgradeStrategy, + source_has_default_strategy: bool, + ) -> int: + # Pure legacy defaults are spread by tenant so all default rows do not + # concentrate in the same scheduler window. User-edited schedules keep their time. + if source_has_default_strategy: + return PluginAutoUpgradeService.default_upgrade_time_of_day(tenant_id) + return source_strategy.upgrade_time_of_day + + @staticmethod + def backfill_strategy_categories( + tenant_id: str, + ) -> PluginAutoUpgradeBackfillResult: + """Create missing category strategies and split include/exclude lists when needed. + + The historical row is treated as the workspace-level source strategy. + New category rows copy it first, then plugin lists are narrowed by real + plugin category when the source strategy contains include/exclude IDs. + """ + with session_factory.create_session() as session, session.begin(): + strategies = list( + session.scalars( + select(TenantPluginAutoUpgradeStrategy).where( + TenantPluginAutoUpgradeStrategy.tenant_id == tenant_id + ) + ).all() ) + if not strategies: + return PluginAutoUpgradeBackfillResult(created_count=0, normalized=False) + + # Schema migration marks the historical workspace-level row as tool. + source_strategy = next( + (strategy for strategy in strategies if strategy.category == PluginCategory.TOOL), + strategies[0], + ) + source_has_default_strategy = PluginAutoUpgradeService._has_default_strategy(source_strategy) + strategies_by_category = {strategy.category: strategy for strategy in strategies} + exclude_plugins = source_strategy.exclude_plugins + include_plugins = source_strategy.include_plugins + should_split_plugin_lists = bool(exclude_plugins or include_plugins) + # Query daemon only for tenants that actually customized plugin lists. + plugin_categories = ( + PluginAutoUpgradeService._get_installed_plugin_categories(tenant_id) + if should_split_plugin_lists + else {} + ) + if should_split_plugin_lists: + PluginAutoUpgradeService._log_unknown_plugin_ids( + tenant_id, + "exclude_plugins", + exclude_plugins, + plugin_categories, + ) + PluginAutoUpgradeService._log_unknown_plugin_ids( + tenant_id, + "include_plugins", + include_plugins, + plugin_categories, + ) + + created_count = 0 + for category in PLUGIN_CATEGORIES: + strategy = strategies_by_category.get(category) + if strategy is None: + # Start from the legacy workspace-level behavior before narrowing lists. + strategy = TenantPluginAutoUpgradeStrategy( + tenant_id=tenant_id, + category=category, + strategy_setting=PluginAutoUpgradeService._strategy_setting_for_category( + source_strategy, category, source_has_default_strategy + ), + upgrade_time_of_day=PluginAutoUpgradeService._upgrade_time_of_day_for_category( + tenant_id, source_strategy, source_has_default_strategy + ), + upgrade_mode=source_strategy.upgrade_mode, + exclude_plugins=source_strategy.exclude_plugins.copy(), + include_plugins=source_strategy.include_plugins.copy(), + ) + session.add(strategy) + created_count += 1 + elif source_has_default_strategy: + strategy.strategy_setting = PluginAutoUpgradeService.default_strategy_setting_for_category( + strategy.category + ) + strategy.upgrade_time_of_day = PluginAutoUpgradeService.default_upgrade_time_of_day(tenant_id) + + if not should_split_plugin_lists: + continue + + # Narrow include/exclude lists to the current category after all rows exist. + strategy.exclude_plugins = PluginAutoUpgradeService._filter_plugin_ids_for_category( + exclude_plugins, + strategy.category, + plugin_categories, + ) + strategy.include_plugins = PluginAutoUpgradeService._filter_plugin_ids_for_category( + include_plugins, + strategy.category, + plugin_categories, + ) + + return PluginAutoUpgradeBackfillResult(created_count=created_count, normalized=should_split_plugin_lists) + + @staticmethod + def _get_strategy( + session: Session, + tenant_id: str, + category: PluginCategory, + ) -> TenantPluginAutoUpgradeStrategy | None: + return session.scalar( + select(TenantPluginAutoUpgradeStrategy) + .where( + TenantPluginAutoUpgradeStrategy.tenant_id == tenant_id, + TenantPluginAutoUpgradeStrategy.category == category, + ) + .limit(1) + ) + + @staticmethod + def get_strategy( + tenant_id: str, + category: PluginCategory, + ) -> TenantPluginAutoUpgradeStrategy | None: + with session_factory.create_session() as session: + return PluginAutoUpgradeService._get_strategy(session, tenant_id, category) + + @staticmethod + def get_strategies(tenant_id: str) -> list[TenantPluginAutoUpgradeStrategy]: + with session_factory.create_session() as session: + return list( + session.scalars( + select(TenantPluginAutoUpgradeStrategy).where( + TenantPluginAutoUpgradeStrategy.tenant_id == tenant_id + ) + ).all() + ) + + @staticmethod + def _change_strategy( + session: Session, + tenant_id: str, + category: PluginCategory, + strategy_setting: TenantPluginAutoUpgradeStrategy.StrategySetting, + upgrade_time_of_day: int, + upgrade_mode: TenantPluginAutoUpgradeStrategy.UpgradeMode, + exclude_plugins: list[str], + include_plugins: list[str], + ) -> None: + exist_strategy = PluginAutoUpgradeService._get_strategy(session, tenant_id, category) + if not exist_strategy: + strategy = TenantPluginAutoUpgradeStrategy( + tenant_id=tenant_id, + category=category, + strategy_setting=strategy_setting, + upgrade_time_of_day=upgrade_time_of_day, + upgrade_mode=upgrade_mode, + exclude_plugins=exclude_plugins, + include_plugins=include_plugins, + ) + session.add(strategy) + else: + exist_strategy.strategy_setting = strategy_setting + exist_strategy.upgrade_time_of_day = upgrade_time_of_day + exist_strategy.upgrade_mode = upgrade_mode + exist_strategy.exclude_plugins = exclude_plugins + exist_strategy.include_plugins = include_plugins @staticmethod def change_strategy( @@ -22,64 +299,72 @@ class PluginAutoUpgradeService: upgrade_mode: TenantPluginAutoUpgradeStrategy.UpgradeMode, exclude_plugins: list[str], include_plugins: list[str], + category: PluginCategory, ) -> bool: with session_factory.create_session() as session, session.begin(): - exist_strategy = session.scalar( - select(TenantPluginAutoUpgradeStrategy) - .where(TenantPluginAutoUpgradeStrategy.tenant_id == tenant_id) - .limit(1) + PluginAutoUpgradeService._change_strategy( + session, + tenant_id=tenant_id, + category=category, + strategy_setting=strategy_setting, + upgrade_time_of_day=upgrade_time_of_day, + upgrade_mode=upgrade_mode, + exclude_plugins=exclude_plugins, + include_plugins=include_plugins, ) - if not exist_strategy: - strategy = TenantPluginAutoUpgradeStrategy( - tenant_id=tenant_id, - strategy_setting=strategy_setting, - upgrade_time_of_day=upgrade_time_of_day, - upgrade_mode=upgrade_mode, - exclude_plugins=exclude_plugins, - include_plugins=include_plugins, - ) - session.add(strategy) - else: - exist_strategy.strategy_setting = strategy_setting - exist_strategy.upgrade_time_of_day = upgrade_time_of_day - exist_strategy.upgrade_mode = upgrade_mode - exist_strategy.exclude_plugins = exclude_plugins - exist_strategy.include_plugins = include_plugins return True @staticmethod - def exclude_plugin(tenant_id: str, plugin_id: str) -> bool: - with session_factory.create_session() as session, session.begin(): - exist_strategy = session.scalar( - select(TenantPluginAutoUpgradeStrategy) - .where(TenantPluginAutoUpgradeStrategy.tenant_id == tenant_id) - .limit(1) + def _exclude_plugin( + session: Session, + tenant_id: str, + category: PluginCategory, + plugin_id: str, + ) -> None: + """Remove one plugin from automatic updates for a single category strategy.""" + exist_strategy = PluginAutoUpgradeService._get_strategy(session, tenant_id, category) + if not exist_strategy: + PluginAutoUpgradeService._change_strategy( + session, + tenant_id, + category, + TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY, + 0, + TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE, + [plugin_id], + [], ) - if not exist_strategy: - # create for this tenant - PluginAutoUpgradeService.change_strategy( - tenant_id, - TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY, - 0, - TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE, - [plugin_id], - [], - ) - return True - else: - if exist_strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE: - if plugin_id not in exist_strategy.exclude_plugins: - new_exclude_plugins = exist_strategy.exclude_plugins.copy() - new_exclude_plugins.append(plugin_id) - exist_strategy.exclude_plugins = new_exclude_plugins - elif exist_strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.PARTIAL: - if plugin_id in exist_strategy.include_plugins: - new_include_plugins = exist_strategy.include_plugins.copy() - new_include_plugins.remove(plugin_id) - exist_strategy.include_plugins = new_include_plugins - elif exist_strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL: - exist_strategy.upgrade_mode = TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE - exist_strategy.exclude_plugins = [plugin_id] + else: + if exist_strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE: + # In exclude mode, disabling one plugin means adding it to exclude_plugins. + if plugin_id not in exist_strategy.exclude_plugins: + new_exclude_plugins = exist_strategy.exclude_plugins.copy() + new_exclude_plugins.append(plugin_id) + exist_strategy.exclude_plugins = new_exclude_plugins + elif exist_strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.PARTIAL: + # In partial mode, disabling one plugin means removing it from include_plugins. + if plugin_id in exist_strategy.include_plugins: + new_include_plugins = exist_strategy.include_plugins.copy() + new_include_plugins.remove(plugin_id) + exist_strategy.include_plugins = new_include_plugins + elif exist_strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL: + # In all mode, switch to exclude mode so only this plugin is skipped. + exist_strategy.upgrade_mode = TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE + exist_strategy.exclude_plugins = [plugin_id] - return True + @staticmethod + def exclude_plugin( + tenant_id: str, + plugin_id: str, + category: PluginCategory, + ) -> bool: + with session_factory.create_session() as session, session.begin(): + PluginAutoUpgradeService._exclude_plugin( + session, + tenant_id, + category, + plugin_id, + ) + + return True diff --git a/api/services/recommend_app/database/database_retrieval.py b/api/services/recommend_app/database/database_retrieval.py index d420b33930..9d6c28c211 100644 --- a/api/services/recommend_app/database/database_retrieval.py +++ b/api/services/recommend_app/database/database_retrieval.py @@ -1,4 +1,4 @@ -from typing import Any, TypedDict, override +from typing import Any, NotRequired, TypedDict, override from sqlalchemy import select @@ -22,6 +22,7 @@ class RecommendedAppItemDict(TypedDict): categories: list[str] position: int is_listed: bool + can_trial: NotRequired[bool] class RecommendedAppsResultDict(TypedDict): @@ -64,14 +65,47 @@ class DatabaseRecommendAppRetrieval(RecommendAppRetrievalBase): :param language: language :return: """ - recommended_apps = db.session.scalars( - select(RecommendedApp).where(RecommendedApp.is_listed == True, RecommendedApp.language == language) - ).all() + recommended_apps = cls._fetch_listed_recommended_apps(language) if len(recommended_apps) == 0: - recommended_apps = db.session.scalars( - select(RecommendedApp).where(RecommendedApp.is_listed == True, RecommendedApp.language == languages[0]) - ).all() + recommended_apps = cls._fetch_listed_recommended_apps(languages[0]) + + return cls._format_recommended_apps(recommended_apps, language) + + @classmethod + def fetch_learn_dify_apps_from_db(cls, language: str) -> RecommendedAppsResultDict: + """ + Fetch listed recommended apps explicitly marked for the Learn Dify section. + :param language: language + :return: + """ + recommended_apps = cls._fetch_listed_recommended_apps(language, is_learn_dify=True) + + if len(recommended_apps) == 0 and language != languages[0]: + recommended_apps = cls._fetch_listed_recommended_apps(languages[0], is_learn_dify=True) + + return cls._format_recommended_apps(recommended_apps, language) + + @classmethod + def _fetch_listed_recommended_apps( + cls, language: str, *, is_learn_dify: bool | None = None + ) -> list[RecommendedApp]: + filters = [RecommendedApp.is_listed.is_(True), RecommendedApp.language == language] + if is_learn_dify is not None: + filters.append(RecommendedApp.is_learn_dify.is_(is_learn_dify)) + + return list(db.session.scalars(select(RecommendedApp).where(*filters)).all()) + + @classmethod + def _format_recommended_apps( + cls, recommended_apps: list[RecommendedApp], language: str + ) -> RecommendedAppsResultDict: + """ + Serialize DB recommended app rows into the Explore list response shape. + :param recommended_apps: recommended app rows + :param language: language used for category ordering + :return: + """ categories = set() recommended_apps_result: list[RecommendedAppItemDict] = [] diff --git a/api/services/recommended_app_service.py b/api/services/recommended_app_service.py index 00eb5ee2d1..bc8bb58acb 100644 --- a/api/services/recommended_app_service.py +++ b/api/services/recommended_app_service.py @@ -6,6 +6,7 @@ from sqlalchemy.orm import scoped_session from configs import dify_config from models.model import AccountTrialAppRecord, TrialApp from services.feature_service import FeatureService +from services.recommend_app.database.database_retrieval import DatabaseRecommendAppRetrieval from services.recommend_app.recommend_app_factory import RecommendAppRetrievalFactory @@ -31,13 +32,24 @@ class RecommendedAppService: apps = result["recommended_apps"] for app in apps: app_id = app["app_id"] - trial_app_model = session.scalar(select(TrialApp).where(TrialApp.app_id == app_id).limit(1)) - if trial_app_model: - app["can_trial"] = True - else: - app["can_trial"] = False + app["can_trial"] = cls._can_trial_app(session, app_id) return result + @classmethod + def get_learn_dify_apps(cls, session: scoped_session, language: str) -> dict[str, Any]: + """ + Get database-backed recommended apps marked as Learn Dify. + :param language: language + :return: + """ + result = DatabaseRecommendAppRetrieval.fetch_learn_dify_apps_from_db(language) + + if FeatureService.get_system_features().enable_trial_app: + for app in result["recommended_apps"]: + app["can_trial"] = cls._can_trial_app(session, app["app_id"]) + + return {"recommended_apps": result["recommended_apps"]} + @classmethod def get_recommend_app_detail(cls, session: scoped_session, app_id: str) -> dict[str, Any] | None: """ @@ -52,11 +64,7 @@ class RecommendedAppService: return None if FeatureService.get_system_features().enable_trial_app: app_id = result["id"] - trial_app_model = session.scalar(select(TrialApp).where(TrialApp.app_id == app_id).limit(1)) - if trial_app_model: - result["can_trial"] = True - else: - result["can_trial"] = False + result["can_trial"] = cls._can_trial_app(session, app_id) return result @classmethod @@ -77,3 +85,8 @@ class RecommendedAppService: else: session.add(AccountTrialAppRecord(app_id=app_id, count=1, account_id=account_id)) session.commit() + + @staticmethod + def _can_trial_app(session: scoped_session, app_id: str) -> bool: + trial_app_model = session.scalar(select(TrialApp).where(TrialApp.app_id == app_id).limit(1)) + return trial_app_model is not None diff --git a/api/tasks/process_tenant_plugin_autoupgrade_check_task.py b/api/tasks/process_tenant_plugin_autoupgrade_check_task.py index ab54b9e72e..0840a595b7 100644 --- a/api/tasks/process_tenant_plugin_autoupgrade_check_task.py +++ b/api/tasks/process_tenant_plugin_autoupgrade_check_task.py @@ -7,7 +7,7 @@ import click from celery import shared_task from core.plugin.entities.marketplace import MarketplacePluginSnapshot -from core.plugin.entities.plugin import PluginInstallationSource +from core.plugin.entities.plugin import PluginInstallation, PluginInstallationSource from core.plugin.impl.plugin import PluginInstaller from core.plugin.plugin_service import PluginService from extensions.ext_redis import redis_client @@ -15,6 +15,7 @@ from models.account import TenantPluginAutoUpgradeStrategy logger = logging.getLogger(__name__) +PluginCategory = TenantPluginAutoUpgradeStrategy.PluginCategory RETRY_TIMES_OF_ONE_PLUGIN_IN_ONE_TENANT = 3 CACHE_REDIS_KEY_PREFIX = "plugin_autoupgrade_check_task:cached_plugin_snapshot:" CACHE_REDIS_TTL = 60 * 60 # 1 hour @@ -72,6 +73,25 @@ def marketplace_batch_fetch_plugin_manifests( return result +def _normalize_category(category: PluginCategory | str | None) -> str | None: + if category is None: + return None + if isinstance(category, PluginCategory): + return category.value + return str(category) + + +def _plugin_matches_category(plugin: PluginInstallation, category: str | None) -> bool: + """Return whether an installed plugin should be checked by a category strategy.""" + if category is None: + return True + + declaration = getattr(plugin, "declaration", None) + plugin_category = getattr(declaration, "category", None) + plugin_category_value = getattr(plugin_category, "value", plugin_category) + return plugin_category_value == category + + @shared_task(queue="plugin") def process_tenant_plugin_autoupgrade_check_task( tenant_id: str, @@ -80,13 +100,15 @@ def process_tenant_plugin_autoupgrade_check_task( upgrade_mode: TenantPluginAutoUpgradeStrategy.UpgradeMode, exclude_plugins: list[str], include_plugins: list[str], + category: PluginCategory | str | None = None, ): try: manager = PluginInstaller() + category_value = _normalize_category(category) click.echo( click.style( - f"Checking upgradable plugin for tenant: {tenant_id}", + f"Checking upgradable plugin for tenant: {tenant_id}, category: {category_value or 'all'}", fg="green", ) ) @@ -102,7 +124,11 @@ def process_tenant_plugin_autoupgrade_check_task( all_plugins = manager.list_plugins(tenant_id) for plugin in all_plugins: - if plugin.source == PluginInstallationSource.Marketplace and plugin.plugin_id in include_plugins: + if ( + plugin.source == PluginInstallationSource.Marketplace + and plugin.plugin_id in include_plugins + and _plugin_matches_category(plugin, category_value) + ): plugin_ids.append( ( plugin.plugin_id, @@ -117,7 +143,9 @@ def process_tenant_plugin_autoupgrade_check_task( plugin_ids = [ (plugin.plugin_id, plugin.version, plugin.plugin_unique_identifier) for plugin in all_plugins - if plugin.source == PluginInstallationSource.Marketplace and plugin.plugin_id not in exclude_plugins + if plugin.source == PluginInstallationSource.Marketplace + and plugin.plugin_id not in exclude_plugins + and _plugin_matches_category(plugin, category_value) ] elif upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL: all_plugins = manager.list_plugins(tenant_id) @@ -125,6 +153,7 @@ def process_tenant_plugin_autoupgrade_check_task( (plugin.plugin_id, plugin.version, plugin.plugin_unique_identifier) for plugin in all_plugins if plugin.source == PluginInstallationSource.Marketplace + and _plugin_matches_category(plugin, category_value) ] if not plugin_ids: diff --git a/api/tasks/remove_app_and_related_data_task.py b/api/tasks/remove_app_and_related_data_task.py index 5f1f0952af..d0763a7a1a 100644 --- a/api/tasks/remove_app_and_related_data_task.py +++ b/api/tasks/remove_app_and_related_data_task.py @@ -22,6 +22,7 @@ from models import ( AppDatasetJoin, AppMCPServer, AppModelConfig, + AppStar, AppTrigger, Conversation, EndUser, @@ -64,6 +65,7 @@ def remove_app_and_related_data_task(self, tenant_id: str, app_id: str): _delete_app_mcp_servers(tenant_id, app_id) _delete_app_api_tokens(tenant_id, app_id) _delete_installed_apps(tenant_id, app_id) + _delete_app_stars(tenant_id, app_id) _delete_recommended_apps(tenant_id, app_id) _delete_app_annotation_data(tenant_id, app_id) _delete_app_dataset_joins(tenant_id, app_id) @@ -173,6 +175,18 @@ def _delete_installed_apps(tenant_id: str, app_id: str): ) +def _delete_app_stars(tenant_id: str, app_id: str): + def del_app_star(session, app_star_id: str): + session.execute(delete(AppStar).where(AppStar.id == app_star_id).execution_options(synchronize_session=False)) + + _delete_records( + """select id from app_stars where tenant_id=:tenant_id and app_id=:app_id limit 1000""", + {"tenant_id": tenant_id, "app_id": app_id}, + del_app_star, + "app star", + ) + + def _delete_recommended_apps(tenant_id: str, app_id: str): def del_recommended_app(session, recommended_app_id: str): session.execute( diff --git a/api/tests/integration_tests/services/plugin/test_plugin_lifecycle.py b/api/tests/integration_tests/services/plugin/test_plugin_lifecycle.py index 0a19debc39..fc76129e3c 100644 --- a/api/tests/integration_tests/services/plugin/test_plugin_lifecycle.py +++ b/api/tests/integration_tests/services/plugin/test_plugin_lifecycle.py @@ -7,6 +7,8 @@ from models.account import TenantPluginAutoUpgradeStrategy, TenantPluginPermissi from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService from services.plugin.plugin_permission_service import PluginPermissionService +PLUGIN_CATEGORY = TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL + @pytest.fixture def tenant(flask_req_ctx): @@ -71,7 +73,7 @@ class TestPluginPermissionLifecycle: class TestPluginAutoUpgradeLifecycle: def test_get_returns_none_for_new_tenant(self, tenant): - assert PluginAutoUpgradeService.get_strategy(tenant) is None + assert PluginAutoUpgradeService.get_strategy(tenant, PLUGIN_CATEGORY) is None def test_change_creates_row(self, tenant): result = PluginAutoUpgradeService.change_strategy( @@ -81,10 +83,11 @@ class TestPluginAutoUpgradeLifecycle: upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL, exclude_plugins=[], include_plugins=[], + category=PLUGIN_CATEGORY, ) assert result is True - strategy = PluginAutoUpgradeService.get_strategy(tenant) + strategy = PluginAutoUpgradeService.get_strategy(tenant, PLUGIN_CATEGORY) assert strategy is not None assert strategy.strategy_setting == TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST assert strategy.upgrade_time_of_day == 3 @@ -97,6 +100,7 @@ class TestPluginAutoUpgradeLifecycle: upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL, exclude_plugins=[], include_plugins=[], + category=PLUGIN_CATEGORY, ) PluginAutoUpgradeService.change_strategy( tenant, @@ -105,9 +109,10 @@ class TestPluginAutoUpgradeLifecycle: upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.PARTIAL, exclude_plugins=[], include_plugins=["plugin-a"], + category=PLUGIN_CATEGORY, ) - strategy = PluginAutoUpgradeService.get_strategy(tenant) + strategy = PluginAutoUpgradeService.get_strategy(tenant, PLUGIN_CATEGORY) assert strategy is not None assert strategy.strategy_setting == TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST assert strategy.upgrade_time_of_day == 12 @@ -115,9 +120,9 @@ class TestPluginAutoUpgradeLifecycle: assert strategy.include_plugins == ["plugin-a"] def test_exclude_plugin_creates_strategy_when_none_exists(self, tenant): - PluginAutoUpgradeService.exclude_plugin(tenant, "my-plugin") + PluginAutoUpgradeService.exclude_plugin(tenant, "my-plugin", PLUGIN_CATEGORY) - strategy = PluginAutoUpgradeService.get_strategy(tenant) + strategy = PluginAutoUpgradeService.get_strategy(tenant, PLUGIN_CATEGORY) assert strategy is not None assert strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE assert "my-plugin" in strategy.exclude_plugins @@ -130,10 +135,11 @@ class TestPluginAutoUpgradeLifecycle: upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE, exclude_plugins=["existing"], include_plugins=[], + category=PLUGIN_CATEGORY, ) - PluginAutoUpgradeService.exclude_plugin(tenant, "new-plugin") + PluginAutoUpgradeService.exclude_plugin(tenant, "new-plugin", PLUGIN_CATEGORY) - strategy = PluginAutoUpgradeService.get_strategy(tenant) + strategy = PluginAutoUpgradeService.get_strategy(tenant, PLUGIN_CATEGORY) assert strategy is not None assert "existing" in strategy.exclude_plugins assert "new-plugin" in strategy.exclude_plugins @@ -146,10 +152,11 @@ class TestPluginAutoUpgradeLifecycle: upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE, exclude_plugins=["same-plugin"], include_plugins=[], + category=PLUGIN_CATEGORY, ) - PluginAutoUpgradeService.exclude_plugin(tenant, "same-plugin") + PluginAutoUpgradeService.exclude_plugin(tenant, "same-plugin", PLUGIN_CATEGORY) - strategy = PluginAutoUpgradeService.get_strategy(tenant) + strategy = PluginAutoUpgradeService.get_strategy(tenant, PLUGIN_CATEGORY) assert strategy is not None assert strategy.exclude_plugins.count("same-plugin") == 1 @@ -161,10 +168,11 @@ class TestPluginAutoUpgradeLifecycle: upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.PARTIAL, exclude_plugins=[], include_plugins=["p1", "p2"], + category=PLUGIN_CATEGORY, ) - PluginAutoUpgradeService.exclude_plugin(tenant, "p1") + PluginAutoUpgradeService.exclude_plugin(tenant, "p1", PLUGIN_CATEGORY) - strategy = PluginAutoUpgradeService.get_strategy(tenant) + strategy = PluginAutoUpgradeService.get_strategy(tenant, PLUGIN_CATEGORY) assert strategy is not None assert "p1" not in strategy.include_plugins assert "p2" in strategy.include_plugins @@ -177,10 +185,11 @@ class TestPluginAutoUpgradeLifecycle: upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL, exclude_plugins=[], include_plugins=[], + category=PLUGIN_CATEGORY, ) - PluginAutoUpgradeService.exclude_plugin(tenant, "excluded-plugin") + PluginAutoUpgradeService.exclude_plugin(tenant, "excluded-plugin", PLUGIN_CATEGORY) - strategy = PluginAutoUpgradeService.get_strategy(tenant) + strategy = PluginAutoUpgradeService.get_strategy(tenant, PLUGIN_CATEGORY) assert strategy is not None assert strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE assert "excluded-plugin" in strategy.exclude_plugins diff --git a/api/tests/test_containers_integration_tests/services/recommend_app/test_database_retrieval.py b/api/tests/test_containers_integration_tests/services/recommend_app/test_database_retrieval.py index e4a106694b..0f7c790ba1 100644 --- a/api/tests/test_containers_integration_tests/services/recommend_app/test_database_retrieval.py +++ b/api/tests/test_containers_integration_tests/services/recommend_app/test_database_retrieval.py @@ -51,6 +51,7 @@ def _create_recommended_app( categories: list[str] | None = None, language: str = "en-US", is_listed: bool = True, + is_learn_dify: bool = False, position: int = 1, ) -> RecommendedApp: rec = RecommendedApp( @@ -62,6 +63,7 @@ def _create_recommended_app( categories=[category] if categories is None else categories, language=language, is_listed=is_listed, + is_learn_dify=is_learn_dify, position=position, ) rec.id = str(uuid4()) @@ -205,6 +207,65 @@ class TestFetchRecommendedAppsFromDb: app_ids = {r["app_id"] for r in result["recommended_apps"]} assert app1.id not in app_ids + def test_fetch_learn_dify_apps_uses_flag_not_categories( + self, + flask_app_with_containers, + db_session_with_containers: Session, + ): + tenant_id = str(uuid4()) + learn_dify_app = _create_app(db_session_with_containers, tenant_id=tenant_id) + _create_site(db_session_with_containers, app_id=learn_dify_app.id) + _create_recommended_app( + db_session_with_containers, + app_id=learn_dify_app.id, + category="workflow", + categories=["Workflow"], + is_learn_dify=True, + ) + + category_only_app = _create_app(db_session_with_containers, tenant_id=tenant_id) + _create_site(db_session_with_containers, app_id=category_only_app.id) + _create_recommended_app( + db_session_with_containers, + app_id=category_only_app.id, + category="Learn Dify", + categories=["Learn Dify"], + is_learn_dify=False, + ) + + db_session_with_containers.expire_all() + + result = DatabaseRecommendAppRetrieval.fetch_learn_dify_apps_from_db("en-US") + + app_ids = {r["app_id"] for r in result["recommended_apps"]} + assert learn_dify_app.id in app_ids + assert category_only_app.id not in app_ids + recommended_app = next(item for item in result["recommended_apps"] if item["app_id"] == learn_dify_app.id) + assert recommended_app["categories"] == ["Workflow"] + + def test_fetch_learn_dify_apps_falls_back_to_default_language( + self, + flask_app_with_containers, + db_session_with_containers: Session, + ): + tenant_id = str(uuid4()) + learn_dify_app = _create_app(db_session_with_containers, tenant_id=tenant_id) + _create_site(db_session_with_containers, app_id=learn_dify_app.id) + _create_recommended_app( + db_session_with_containers, + app_id=learn_dify_app.id, + categories=["Workflow"], + is_learn_dify=True, + language="en-US", + ) + + db_session_with_containers.expire_all() + + result = DatabaseRecommendAppRetrieval.fetch_learn_dify_apps_from_db("fr-FR") + + app_ids = {r["app_id"] for r in result["recommended_apps"]} + assert learn_dify_app.id in app_ids + class TestFetchRecommendedAppDetailFromDb: def test_returns_none_when_not_listed(self, flask_app_with_containers: Flask, db_session_with_containers: Session): diff --git a/api/tests/test_containers_integration_tests/services/test_app_service.py b/api/tests/test_containers_integration_tests/services/test_app_service.py index 56d643e4c1..384f83fce3 100644 --- a/api/tests/test_containers_integration_tests/services/test_app_service.py +++ b/api/tests/test_containers_integration_tests/services/test_app_service.py @@ -1,6 +1,8 @@ +from datetime import datetime from unittest.mock import create_autospec, patch import pytest +import sqlalchemy as sa from faker import Faker from pydantic import ValidationError from sqlalchemy.orm import Session @@ -245,6 +247,236 @@ class TestAppService: assert app.tenant_id == tenant.id assert app.mode == "chat" + def test_get_paginate_apps_sorts_by_modified_and_created_times( + self, db_session_with_containers: Session, mock_external_service_dependencies + ): + """ + Test app list sort options for modified time and creation time. + """ + fake = Faker() + + account = AccountService.create_account( + email=fake.email(), + name=fake.name(), + interface_language="en-US", + password=generate_valid_password(fake), + ) + TenantService.create_owner_tenant_if_not_exist(account, name=fake.company()) + tenant = account.current_tenant + + from services.app_service import AppListParams, AppService, CreateAppParams + + app_service = AppService() + oldest_created = app_service.create_app( + tenant.id, + CreateAppParams(name="Oldest Created", mode="chat", icon_type="emoji", icon="1"), + account, + ) + newest_modified = app_service.create_app( + tenant.id, + CreateAppParams(name="Newest Modified", mode="chat", icon_type="emoji", icon="2"), + account, + ) + newest_created = app_service.create_app( + tenant.id, + CreateAppParams(name="Newest Created", mode="chat", icon_type="emoji", icon="3"), + account, + ) + + timestamp_by_app_id = { + oldest_created.id: (datetime(2026, 1, 1, 10, 0, 0), datetime(2026, 1, 1, 10, 0, 0)), + newest_modified.id: (datetime(2026, 1, 2, 10, 0, 0), datetime(2026, 1, 4, 10, 0, 0)), + newest_created.id: (datetime(2026, 1, 3, 10, 0, 0), datetime(2026, 1, 3, 10, 0, 0)), + } + for app_id, (created_at, updated_at) in timestamp_by_app_id.items(): + db_session_with_containers.execute( + sa.update(App).where(App.id == app_id).values(created_at=created_at, updated_at=updated_at) + ) + db_session_with_containers.commit() + + last_modified_apps = app_service.get_paginate_apps( + account.id, tenant.id, AppListParams(page=1, limit=10, mode="chat") + ) + assert last_modified_apps is not None + assert [app.name for app in last_modified_apps.items] == [ + "Newest Modified", + "Newest Created", + "Oldest Created", + ] + + recently_created_apps = app_service.get_paginate_apps( + account.id, tenant.id, AppListParams(page=1, limit=10, mode="chat", sort_by="recently_created") + ) + assert recently_created_apps is not None + assert [app.name for app in recently_created_apps.items] == [ + "Newest Created", + "Newest Modified", + "Oldest Created", + ] + + earliest_created_apps = app_service.get_paginate_apps( + account.id, tenant.id, AppListParams(page=1, limit=10, mode="chat", sort_by="earliest_created") + ) + assert earliest_created_apps is not None + assert [app.name for app in earliest_created_apps.items] == [ + "Oldest Created", + "Newest Modified", + "Newest Created", + ] + + def test_get_paginate_apps_marks_starred_apps( + self, db_session_with_containers: Session, mock_external_service_dependencies + ): + """ + Test app list marks apps starred by the current account. + """ + fake = Faker() + + account = AccountService.create_account( + email=fake.email(), + name=fake.name(), + interface_language="en-US", + password=generate_valid_password(fake), + ) + TenantService.create_owner_tenant_if_not_exist(account, name=fake.company()) + tenant = account.current_tenant + + from models import AppStar + from services.app_service import AppListParams, AppService, CreateAppParams + + app_service = AppService() + starred_app = app_service.create_app( + tenant.id, + CreateAppParams(name="Starred App", mode="chat", icon_type="emoji", icon="1"), + account, + ) + unstarred_app = app_service.create_app( + tenant.id, + CreateAppParams(name="Unstarred App", mode="chat", icon_type="emoji", icon="2"), + account, + ) + + app_service.star_app(db_session_with_containers, app=starred_app, account_id=account.id) + app_service.star_app(db_session_with_containers, app=starred_app, account_id=account.id) + db_session_with_containers.commit() + + star_count = db_session_with_containers.scalar( + sa.select(sa.func.count()).select_from(AppStar).where(AppStar.app_id == starred_app.id) + ) + assert star_count == 1 + + paginated_apps = app_service.get_paginate_apps( + account.id, tenant.id, AppListParams(page=1, limit=10, mode="chat") + ) + assert paginated_apps is not None + starred_by_app_id = {app.id: app.is_starred for app in paginated_apps.items} + assert starred_by_app_id[starred_app.id] is True + assert starred_by_app_id[unstarred_app.id] is False + + app_service.unstar_app(db_session_with_containers, app=starred_app, account_id=account.id) + db_session_with_containers.commit() + + paginated_apps = app_service.get_paginate_apps( + account.id, tenant.id, AppListParams(page=1, limit=10, mode="chat") + ) + assert paginated_apps is not None + starred_by_app_id = {app.id: app.is_starred for app in paginated_apps.items} + assert starred_by_app_id[starred_app.id] is False + assert starred_by_app_id[unstarred_app.id] is False + + def test_get_paginate_starred_apps_returns_only_starred_apps_with_requested_sort( + self, db_session_with_containers: Session, mock_external_service_dependencies + ): + """ + Test starred app list returns only starred apps ordered by requested app sort. + """ + fake = Faker() + + account = AccountService.create_account( + email=fake.email(), + name=fake.name(), + interface_language="en-US", + password=generate_valid_password(fake), + ) + TenantService.create_owner_tenant_if_not_exist(account, name=fake.company()) + tenant = account.current_tenant + + from services.app_service import AppService, CreateAppParams, StarredAppListParams + + app_service = AppService() + oldest_created_app = app_service.create_app( + tenant.id, + CreateAppParams(name="Oldest Created Starred App", mode="chat", icon_type="emoji", icon="1"), + account, + ) + newest_modified_app = app_service.create_app( + tenant.id, + CreateAppParams(name="Newest Modified Starred App", mode="chat", icon_type="emoji", icon="2"), + account, + ) + newest_created_app = app_service.create_app( + tenant.id, + CreateAppParams(name="Newest Created Starred App", mode="chat", icon_type="emoji", icon="3"), + account, + ) + unstarred_app = app_service.create_app( + tenant.id, + CreateAppParams(name="Unstarred App", mode="chat", icon_type="emoji", icon="4"), + account, + ) + + app_service.star_app(db_session_with_containers, app=oldest_created_app, account_id=account.id) + app_service.star_app(db_session_with_containers, app=newest_modified_app, account_id=account.id) + app_service.star_app(db_session_with_containers, app=newest_created_app, account_id=account.id) + + timestamp_by_app_id = { + oldest_created_app.id: (datetime(2026, 1, 1, 10, 0, 0), datetime(2026, 1, 1, 10, 0, 0)), + newest_modified_app.id: (datetime(2026, 1, 2, 10, 0, 0), datetime(2026, 1, 4, 10, 0, 0)), + newest_created_app.id: (datetime(2026, 1, 3, 10, 0, 0), datetime(2026, 1, 3, 10, 0, 0)), + unstarred_app.id: (datetime(2026, 1, 5, 10, 0, 0), datetime(2026, 1, 5, 10, 0, 0)), + } + for app_id, (created_at, updated_at) in timestamp_by_app_id.items(): + db_session_with_containers.execute( + sa.update(App).where(App.id == app_id).values(created_at=created_at, updated_at=updated_at) + ) + db_session_with_containers.commit() + + last_modified_apps = app_service.get_paginate_starred_apps( + account.id, tenant.id, StarredAppListParams(page=1, limit=10, mode="chat") + ) + assert last_modified_apps is not None + assert [app.name for app in last_modified_apps.items] == [ + "Newest Modified Starred App", + "Newest Created Starred App", + "Oldest Created Starred App", + ] + assert all(app.is_starred for app in last_modified_apps.items) + assert unstarred_app.id not in {app.id for app in last_modified_apps.items} + + recently_created_apps = app_service.get_paginate_starred_apps( + account.id, + tenant.id, + StarredAppListParams(page=1, limit=10, mode="chat", sort_by="recently_created"), + ) + assert recently_created_apps is not None + assert [app.name for app in recently_created_apps.items] == [ + "Newest Created Starred App", + "Newest Modified Starred App", + "Oldest Created Starred App", + ] + + earliest_created_apps = app_service.get_paginate_starred_apps( + account.id, + tenant.id, + StarredAppListParams(page=1, limit=10, mode="chat", sort_by="earliest_created"), + ) + assert earliest_created_apps is not None + assert [app.name for app in earliest_created_apps.items] == [ + "Oldest Created Starred App", + "Newest Modified Starred App", + "Newest Created Starred App", + ] + def test_get_paginate_apps_with_filters( self, db_session_with_containers: Session, mock_external_service_dependencies ): diff --git a/api/tests/test_containers_integration_tests/services/test_recommended_app_service.py b/api/tests/test_containers_integration_tests/services/test_recommended_app_service.py index c4c4f0ac1f..750e35843b 100644 --- a/api/tests/test_containers_integration_tests/services/test_recommended_app_service.py +++ b/api/tests/test_containers_integration_tests/services/test_recommended_app_service.py @@ -263,6 +263,54 @@ class TestRecommendedAppServiceGetDetail: mock_factory_class.get_recommend_app_factory.assert_called_with(mode) +# ── Pure logic tests: get_learn_dify_apps ────────────────────────────── + + +class TestRecommendedAppServiceGetLearnDifyApps: + def test_returns_database_learn_dify_apps_without_remote_factory(self, monkeypatch: pytest.MonkeyPatch) -> None: + expected_app = RecommendedAppPayload(app_id="app-1", category="Workflow") + mock_database_retrieval = MagicMock() + mock_database_retrieval.fetch_learn_dify_apps_from_db.return_value = { + "recommended_apps": [expected_app], + "categories": ["Workflow"], + } + monkeypatch.setattr(service_module, "DatabaseRecommendAppRetrieval", mock_database_retrieval) + monkeypatch.setattr( + service_module.FeatureService, + "get_system_features", + MagicMock(return_value=SimpleNamespace(enable_trial_app=False)), + ) + factory_mock = MagicMock() + monkeypatch.setattr(service_module.RecommendAppRetrievalFactory, "get_recommend_app_factory", factory_mock) + + result = RecommendedAppService.get_learn_dify_apps(db.session, "en-US") + + assert result == {"recommended_apps": [expected_app]} + mock_database_retrieval.fetch_learn_dify_apps_from_db.assert_called_once_with("en-US") + factory_mock.assert_not_called() + + def test_sets_can_trial_when_trial_feature_enabled(self, monkeypatch: pytest.MonkeyPatch) -> None: + app = RecommendedAppPayload(app_id="app-1", category="Workflow") + mock_database_retrieval = MagicMock() + mock_database_retrieval.fetch_learn_dify_apps_from_db.return_value = { + "recommended_apps": [app], + "categories": ["Workflow"], + } + monkeypatch.setattr(service_module, "DatabaseRecommendAppRetrieval", mock_database_retrieval) + monkeypatch.setattr( + service_module.FeatureService, + "get_system_features", + MagicMock(return_value=SimpleNamespace(enable_trial_app=True)), + ) + can_trial_mock = MagicMock(return_value=True) + monkeypatch.setattr(RecommendedAppService, "_can_trial_app", can_trial_mock) + + result = RecommendedAppService.get_learn_dify_apps(db.session, "en-US") + + assert result["recommended_apps"][0]["can_trial"] is True + can_trial_mock.assert_called_once_with(db.session, "app-1") + + # ── Integration tests: trial app features (real DB) ──────────────────── diff --git a/api/tests/unit_tests/controllers/console/explore/test_recommended_app.py b/api/tests/unit_tests/controllers/console/explore/test_recommended_app.py index e0eab9a4d3..ef08aa1b36 100644 --- a/api/tests/unit_tests/controllers/console/explore/test_recommended_app.py +++ b/api/tests/unit_tests/controllers/console/explore/test_recommended_app.py @@ -79,6 +79,46 @@ class TestRecommendedAppListApi: assert result == result_data +class TestLearnDifyAppListApi: + def test_get_with_language_param(self, app: Flask): + api = module.LearnDifyAppListApi() + method = unwrap(api.get) + + result_data = {"recommended_apps": []} + + with ( + app.test_request_context("/", query_string={"language": "en-US"}), + patch.object( + module.RecommendedAppService, + "get_learn_dify_apps", + return_value=result_data, + ) as service_mock, + ): + result = method(api, make_account("fr-FR")) + + service_mock.assert_called_once_with(ANY, "en-US") + assert result == result_data + + def test_get_fallback_to_user_language(self, app: Flask): + api = module.LearnDifyAppListApi() + method = unwrap(api.get) + + result_data = {"recommended_apps": []} + + with ( + app.test_request_context("/", query_string={"language": "invalid"}), + patch.object( + module.RecommendedAppService, + "get_learn_dify_apps", + return_value=result_data, + ) as service_mock, + ): + result = method(api, make_account("fr-FR")) + + service_mock.assert_called_once_with(ANY, "fr-FR") + assert result == result_data + + class TestRecommendedAppApi: def test_get_success(self, app: Flask): api = module.RecommendedAppApi() @@ -144,3 +184,29 @@ class TestRecommendedAppResponseModels: assert response["recommended_apps"][0]["app_id"] == "app-1" assert response["recommended_apps"][0]["categories"] == ["cat", "other"] assert response["categories"] == ["cat"] + + def test_learn_dify_app_list_response_serialization(self): + response = module.LearnDifyAppListResponse.model_validate( + { + "recommended_apps": [ + { + "app": { + "id": "app-1", + "name": "App", + "mode": "chat", + "icon": "icon.png", + "icon_type": "emoji", + "icon_background": "#fff", + }, + "app_id": "app-1", + "description": "desc", + "categories": ["Workflow"], + "position": 1, + "is_listed": True, + } + ], + } + ).model_dump(mode="json") + + assert response["recommended_apps"][0]["app_id"] == "app-1" + assert response["recommended_apps"][0]["categories"] == ["Workflow"] diff --git a/api/tests/unit_tests/controllers/console/workspace/test_plugin.py b/api/tests/unit_tests/controllers/console/workspace/test_plugin.py index 2f9c7d4fd6..bc76560dca 100644 --- a/api/tests/unit_tests/controllers/console/workspace/test_plugin.py +++ b/api/tests/unit_tests/controllers/console/workspace/test_plugin.py @@ -1,5 +1,7 @@ import io +from datetime import datetime from inspect import unwrap +from typing import Any from unittest.mock import MagicMock, patch import pytest @@ -10,12 +12,14 @@ from werkzeug.exceptions import Forbidden from controllers.console.workspace.plugin import ( PluginAssetApi, PluginAutoUpgradeExcludePluginApi, + PluginCategoryListApi, + PluginChangeAutoUpgradeApi, PluginChangePermissionApi, - PluginChangePreferencesApi, PluginDebuggingKeyApi, PluginDeleteAllInstallTaskItemsApi, PluginDeleteInstallTaskApi, PluginDeleteInstallTaskItemApi, + PluginFetchAutoUpgradeApi, PluginFetchDynamicSelectOptionsApi, PluginFetchDynamicSelectOptionsWithCredentialsApi, PluginFetchInstallTaskApi, @@ -23,7 +27,6 @@ from controllers.console.workspace.plugin import ( PluginFetchManifestApi, PluginFetchMarketplacePkgApi, PluginFetchPermissionApi, - PluginFetchPreferencesApi, PluginIconApi, PluginInstallFromGithubApi, PluginInstallFromMarketplaceApi, @@ -43,6 +46,69 @@ from core.plugin.impl.exc import PluginDaemonClientSideError from models.account import Account, TenantAccountRole, TenantPluginAutoUpgradeStrategy, TenantPluginPermission +def _plugin_category_list_item(category: str = "tool") -> dict[str, Any]: + now = datetime(2023, 1, 1, 0, 0, 0) + return { + "id": "entity-1", + "created_at": now, + "updated_at": now, + "tenant_id": "t1", + "endpoints_setups": 0, + "endpoints_active": 0, + "runtime_type": "remote", + "source": "marketplace", + "meta": {}, + "plugin_id": "test-author/test-plugin", + "plugin_unique_identifier": "test-author/test-plugin:1.0.0@checksum", + "version": "1.0.0", + "checksum": "checksum", + "name": "test-plugin", + "installation_id": "entity-1", + "declaration": { + "version": "1.0.0", + "author": "test-author", + "name": "test-plugin", + "description": {"en_US": "Test plugin"}, + "icon": "icon.svg", + "label": {"en_US": "Test Plugin"}, + "category": category, + "created_at": now, + "resource": {"memory": 268435456, "permission": None}, + "plugins": {"tools": ["provider/test.yaml"]}, + "meta": {"version": "1.0.0"}, + "tool": { + "identity": { + "author": "test-author", + "name": "test-plugin", + "description": {"en_US": "Test plugin"}, + "icon": "icon.svg", + "label": {"en_US": "Test Plugin"}, + } + }, + }, + } + + +def _builtin_tool_provider_item() -> dict[str, Any]: + return { + "id": "builtin", + "author": "dify", + "name": "builtin", + "plugin_id": "", + "plugin_unique_identifier": "", + "description": {"en_US": "Builtin tool provider"}, + "icon": "icon.svg", + "icon_dark": "", + "label": {"en_US": "Builtin"}, + "type": "builtin", + "team_credentials": {}, + "is_team_authorization": False, + "allow_delete": True, + "tools": [], + "labels": [], + } + + def _account(role: TenantAccountRole = TenantAccountRole.OWNER) -> Account: account = Account(name="Test User", email="u1@example.com") account.id = "u1" @@ -142,6 +208,83 @@ class TestPluginListApi: mock_list_with_total.assert_called_once_with("t1", "u1", 1, 10) +class TestPluginCategoryListApi: + def test_plugin_category_list(self, app: Flask): + api = PluginCategoryListApi() + method = unwrap(api.get) + plugin_item = _plugin_category_list_item() + builtin_item = _builtin_tool_provider_item() + mock_list = MagicMock(list=[plugin_item], has_more=True) + + with ( + app.test_request_context("/?page=2&page_size=10"), + patch( + "controllers.console.workspace.plugin.PluginService.list_by_category", return_value=mock_list + ) as list_mock, + patch( + "controllers.console.workspace.plugin._list_hardcoded_builtin_tool_providers", + return_value=[builtin_item], + ) as builtin_mock, + ): + result = method(api, "t1", "tool") + + list_mock.assert_called_once() + assert list_mock.call_args.args[0] == "t1" + assert list_mock.call_args.args[1] == "tool" + assert list_mock.call_args.args[2] == 2 + assert list_mock.call_args.args[3] == 10 + assert result["plugins"][0]["id"] == "entity-1" + assert result["plugins"][0]["plugin_unique_identifier"] == "test-author/test-plugin:1.0.0@checksum" + assert result["builtin_tools"][0]["id"] == "builtin" + assert result["builtin_tools"][0]["type"] == "builtin" + assert result["has_more"] is True + assert "total" not in result + builtin_mock.assert_called_once_with("t1") + + def test_non_tool_category_does_not_include_builtin_tools(self, app: Flask): + api = PluginCategoryListApi() + method = unwrap(api.get) + mock_list = MagicMock(list=[_plugin_category_list_item(category="datasource")], has_more=False) + + with ( + app.test_request_context("/?page=1&page_size=10"), + patch("controllers.console.workspace.plugin.PluginService.list_by_category", return_value=mock_list), + patch("controllers.console.workspace.plugin._list_hardcoded_builtin_tool_providers") as builtin_mock, + ): + result = method(api, "t1", "datasource") + + assert result["plugins"][0]["id"] == "entity-1" + assert result["builtin_tools"] == [] + assert result["has_more"] is False + builtin_mock.assert_not_called() + + def test_invalid_category(self, app: Flask): + api = PluginCategoryListApi() + method = unwrap(api.get) + + with ( + app.test_request_context("/?page=1&page_size=10"), + ): + result = method(api, "t1", "unknown") + + assert result == ({"code": "invalid_param", "message": "invalid plugin category"}, 400) + + def test_daemon_error(self, app: Flask): + api = PluginCategoryListApi() + method = unwrap(api.get) + + with ( + app.test_request_context("/?page=1&page_size=10"), + patch( + "controllers.console.workspace.plugin.PluginService.list_by_category", + side_effect=PluginDaemonClientSideError("error"), + ), + ): + result = method(api, "t1", "tool") + + assert result == ({"code": "plugin_error", "message": "error"}, 400) + + class TestPluginIconApi: def test_plugin_icon(self, app: Flask): api = PluginIconApi() @@ -857,18 +1000,15 @@ class TestPluginFetchDynamicSelectOptionsWithCredentialsApi: assert result == ({"code": "plugin_error", "message": "error"}, 400) -class TestPluginChangePreferencesApi: +class TestPluginChangeAutoUpgradeApi: def test_success(self, app: Flask): - api = PluginChangePreferencesApi() + api = PluginChangeAutoUpgradeApi() method = unwrap(api.post) user = _account() payload = { - "permission": { - "install_permission": TenantPluginPermission.InstallPermission.EVERYONE, - "debug_permission": TenantPluginPermission.DebugPermission.EVERYONE, - }, + "category": TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL.value, "auto_upgrade": { "strategy_setting": TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY, "upgrade_time_of_day": 0, @@ -880,24 +1020,52 @@ class TestPluginChangePreferencesApi: with ( app.test_request_context("/", json=payload), - patch("controllers.console.workspace.plugin.PluginPermissionService.change_permission", return_value=True), - patch("controllers.console.workspace.plugin.PluginAutoUpgradeService.change_strategy", return_value=True), + patch( + "controllers.console.workspace.plugin.PluginAutoUpgradeService.change_strategy", return_value=True + ) as change, ): result = method(api, "t1", user) assert result["success"] is True + change.assert_called_once() - def test_permission_fail(self, app: Flask): - api = PluginChangePreferencesApi() + def test_success_with_model_category_auto_upgrade(self, app: Flask): + api = PluginChangeAutoUpgradeApi() method = unwrap(api.post) user = _account() payload = { - "permission": { - "install_permission": TenantPluginPermission.InstallPermission.EVERYONE, - "debug_permission": TenantPluginPermission.DebugPermission.EVERYONE, + "category": TenantPluginAutoUpgradeStrategy.PluginCategory.MODEL.value, + "auto_upgrade": { + "strategy_setting": TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST, + "upgrade_time_of_day": 3600, + "upgrade_mode": TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL, + "exclude_plugins": [], + "include_plugins": [], }, + } + + with ( + app.test_request_context("/", json=payload), + patch( + "controllers.console.workspace.plugin.PluginAutoUpgradeService.change_strategy", return_value=True + ) as change, + ): + result = method(api, "t1", user) + + assert result["success"] is True + change.assert_called_once() + assert change.call_args.kwargs["category"] == TenantPluginAutoUpgradeStrategy.PluginCategory.MODEL + + def test_auto_upgrade_fail(self, app: Flask): + api = PluginChangeAutoUpgradeApi() + method = unwrap(api.post) + + user = MagicMock(is_admin_or_owner=True) + + payload = { + "category": TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL.value, "auto_upgrade": { "strategy_setting": TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY, "upgrade_time_of_day": 0, @@ -909,24 +1077,20 @@ class TestPluginChangePreferencesApi: with ( app.test_request_context("/", json=payload), - patch("controllers.console.workspace.plugin.PluginPermissionService.change_permission", return_value=False), + patch("controllers.console.workspace.plugin.PluginAutoUpgradeService.change_strategy", return_value=False), ): result = method(api, "t1", user) assert result["success"] is False -class TestPluginFetchPreferencesApi: +class TestPluginFetchAutoUpgradeApi: def test_success(self, app: Flask): - api = PluginFetchPreferencesApi() + api = PluginFetchAutoUpgradeApi() method = unwrap(api.get) - permission = MagicMock( - install_permission=TenantPluginPermission.InstallPermission.EVERYONE, - debug_permission=TenantPluginPermission.DebugPermission.EVERYONE, - ) - auto_upgrade = MagicMock( + category=TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL, strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY, upgrade_time_of_day=1, upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE, @@ -935,18 +1099,16 @@ class TestPluginFetchPreferencesApi: ) with ( - app.test_request_context("/"), + app.test_request_context(f"/?category={TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL.value}"), patch( - "controllers.console.workspace.plugin.PluginPermissionService.get_permission", return_value=permission - ), - patch( - "controllers.console.workspace.plugin.PluginAutoUpgradeService.get_strategy", return_value=auto_upgrade + "controllers.console.workspace.plugin.PluginAutoUpgradeService.get_strategy", + return_value=auto_upgrade, ), ): result = method(api, "t1") - assert "permission" in result - assert "auto_upgrade" in result + assert result["category"] == TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL + assert result["auto_upgrade"]["upgrade_time_of_day"] == 1 class TestPluginAutoUpgradeExcludePluginApi: @@ -954,7 +1116,7 @@ class TestPluginAutoUpgradeExcludePluginApi: api = PluginAutoUpgradeExcludePluginApi() method = unwrap(api.post) - payload = {"plugin_id": "p"} + payload = {"plugin_id": "p", "category": TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL.value} with ( app.test_request_context("/", json=payload), @@ -968,7 +1130,7 @@ class TestPluginAutoUpgradeExcludePluginApi: api = PluginAutoUpgradeExcludePluginApi() method = unwrap(api.post) - payload = {"plugin_id": "p"} + payload = {"plugin_id": "p", "category": TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL.value} with ( app.test_request_context("/", json=payload), diff --git a/api/tests/unit_tests/controllers/console/workspace/test_workspace.py b/api/tests/unit_tests/controllers/console/workspace/test_workspace.py index 68d5a879e4..4663d503e3 100644 --- a/api/tests/unit_tests/controllers/console/workspace/test_workspace.py +++ b/api/tests/unit_tests/controllers/console/workspace/test_workspace.py @@ -53,6 +53,12 @@ def make_tenant( return tenant +def make_membership(*, last_opened_at=None) -> MagicMock: + membership = MagicMock() + membership.last_opened_at = last_opened_at + return membership + + def make_account_with_tenant(tenant: Tenant) -> Account: account = make_account() account._current_tenant = tenant @@ -66,13 +72,17 @@ class TestTenantListApi: tenant1 = make_tenant("t1", name="Tenant 1") tenant2 = make_tenant("t2", name="Tenant 2") + last_opened_at = naive_utc_now() user = make_account() with ( app.test_request_context("/workspaces"), patch( - "controllers.console.workspace.workspace.TenantService.get_join_tenants", - return_value=[tenant1, tenant2], + "controllers.console.workspace.workspace.TenantService.get_workspaces_for_account", + return_value=[ + (tenant1, make_membership(last_opened_at=last_opened_at)), + (tenant2, make_membership()), + ], ), patch("controllers.console.workspace.workspace.dify_config.ENTERPRISE_ENABLED", False), patch("controllers.console.workspace.workspace.dify_config.BILLING_ENABLED", True), @@ -92,7 +102,9 @@ class TestTenantListApi: assert len(result["workspaces"]) == 2 assert result["workspaces"][0]["current"] is True assert result["workspaces"][0]["plan"] == CloudPlan.TEAM + assert result["workspaces"][0]["last_opened_at"] == int(last_opened_at.timestamp()) assert result["workspaces"][1]["plan"] == CloudPlan.PROFESSIONAL + assert result["workspaces"][1]["last_opened_at"] is None get_plan_bulk_mock.assert_called_once_with(["t1", "t2"]) get_features_mock.assert_not_called() @@ -116,8 +128,8 @@ class TestTenantListApi: with ( app.test_request_context("/workspaces"), patch( - "controllers.console.workspace.workspace.TenantService.get_join_tenants", - return_value=[tenant1, tenant2], + "controllers.console.workspace.workspace.TenantService.get_workspaces_for_account", + return_value=[(tenant1, make_membership()), (tenant2, make_membership())], ), patch("controllers.console.workspace.workspace.dify_config.ENTERPRISE_ENABLED", False), patch("controllers.console.workspace.workspace.dify_config.BILLING_ENABLED", True), @@ -159,8 +171,8 @@ class TestTenantListApi: with ( app.test_request_context("/workspaces"), patch( - "controllers.console.workspace.workspace.TenantService.get_join_tenants", - return_value=[tenant1, tenant2], + "controllers.console.workspace.workspace.TenantService.get_workspaces_for_account", + return_value=[(tenant1, make_membership()), (tenant2, make_membership())], ), patch("controllers.console.workspace.workspace.dify_config.ENTERPRISE_ENABLED", False), patch("controllers.console.workspace.workspace.dify_config.BILLING_ENABLED", True), @@ -198,8 +210,8 @@ class TestTenantListApi: with ( app.test_request_context("/workspaces"), patch( - "controllers.console.workspace.workspace.TenantService.get_join_tenants", - return_value=[tenant], + "controllers.console.workspace.workspace.TenantService.get_workspaces_for_account", + return_value=[(tenant, make_membership())], ), patch("controllers.console.workspace.workspace.dify_config.ENTERPRISE_ENABLED", False), patch("controllers.console.workspace.workspace.dify_config.BILLING_ENABLED", False), @@ -226,8 +238,8 @@ class TestTenantListApi: with ( app.test_request_context("/workspaces"), patch( - "controllers.console.workspace.workspace.TenantService.get_join_tenants", - return_value=[tenant1, tenant2], + "controllers.console.workspace.workspace.TenantService.get_workspaces_for_account", + return_value=[(tenant1, make_membership()), (tenant2, make_membership())], ), patch("controllers.console.workspace.workspace.dify_config.ENTERPRISE_ENABLED", True), patch("controllers.console.workspace.workspace.dify_config.BILLING_ENABLED", False), @@ -251,7 +263,7 @@ class TestTenantListApi: with ( app.test_request_context("/workspaces"), patch( - "controllers.console.workspace.workspace.TenantService.get_join_tenants", + "controllers.console.workspace.workspace.TenantService.get_workspaces_for_account", return_value=[], ), patch("controllers.console.workspace.workspace.dify_config.ENTERPRISE_ENABLED", True), diff --git a/api/tests/unit_tests/controllers/test_swagger.py b/api/tests/unit_tests/controllers/test_swagger.py index 8ad590c4dd..898eb2e86b 100644 --- a/api/tests/unit_tests/controllers/test_swagger.py +++ b/api/tests/unit_tests/controllers/test_swagger.py @@ -1,5 +1,6 @@ """OpenAPI JSON rendering tests for Flask-RESTX API blueprints.""" +import json from collections.abc import Iterator import pytest @@ -187,3 +188,39 @@ def test_console_account_avatar_query_param_renders_as_query(monkeypatch: pytest assert "payload" not in params assert params["avatar"]["in"] == "query" assert params["avatar"]["required"] is True + + +def test_console_plugin_category_list_exported_schema_uses_typed_items(tmp_path): + from dev.generate_swagger_specs import generate_specs + + written_paths = generate_specs(tmp_path) + console_openapi_path = next(path for path in written_paths if path.name == "console-openapi.json") + payload = json.loads(console_openapi_path.read_text(encoding="utf-8")) + operation = payload["paths"]["/workspaces/current/plugin/{category}/list"]["get"] + response_ref = operation["responses"]["200"]["content"]["application/json"]["schema"]["$ref"].removeprefix( + "#/components/schemas/" + ) + schemas = payload["components"]["schemas"] + response_schema = schemas[response_ref] + + assert response_schema["properties"]["plugins"]["items"]["$ref"] == ( + "#/components/schemas/PluginCategoryInstalledPluginResponse" + ) + assert response_schema["properties"]["builtin_tools"]["items"]["$ref"] == ( + "#/components/schemas/PluginCategoryBuiltinToolProviderResponse" + ) + + installed_plugin_schema = schemas["PluginCategoryInstalledPluginResponse"] + for field in ( + "plugin_unique_identifier", + "source", + "version", + "declaration", + "endpoints_active", + "endpoints_setups", + ): + assert field in installed_plugin_schema["properties"] + + builtin_tool_schema = schemas["PluginCategoryBuiltinToolProviderResponse"] + for field in ("plugin_unique_identifier", "team_credentials", "type", "tools"): + assert field in builtin_tool_schema["properties"] diff --git a/api/tests/unit_tests/core/plugin/test_plugin_manager.py b/api/tests/unit_tests/core/plugin/test_plugin_manager.py index 510aedd551..1c5e1b9c0c 100644 --- a/api/tests/unit_tests/core/plugin/test_plugin_manager.py +++ b/api/tests/unit_tests/core/plugin/test_plugin_manager.py @@ -33,6 +33,7 @@ from core.plugin.entities.plugin_daemon import ( PluginInstallTaskStartResponse, PluginInstallTaskStatus, PluginListResponse, + PluginListWithoutTotalResponse, PluginReadmeResponse, PluginVerification, ) @@ -123,6 +124,26 @@ class TestPluginDiscovery: assert call_args[1]["params"]["page_size"] == 5 assert result.total == 10 + def test_list_plugins_by_category(self, plugin_installer, mock_plugin_entity): + """Test category plugin listing without total.""" + mock_response = PluginListWithoutTotalResponse(list=[mock_plugin_entity], has_more=True) + + with patch.object( + plugin_installer, "_request_with_plugin_daemon_response", return_value=mock_response + ) as mock_request: + result = plugin_installer.list_plugins_by_category( + "test-tenant", category=PluginCategory.Tool, page=2, page_size=10 + ) + + mock_request.assert_called_once() + call_args = mock_request.call_args + assert call_args.args[1] == "plugin/test-tenant/management/tool/list" + assert call_args.args[2] is PluginListWithoutTotalResponse + assert call_args.kwargs["params"]["page"] == 2 + assert call_args.kwargs["params"]["page_size"] == 10 + assert result.list == [mock_plugin_entity] + assert result.has_more is True + def test_list_plugins_empty_result(self, plugin_installer): """Test plugin listing when no plugins are installed.""" # Arrange: Mock empty response diff --git a/api/tests/unit_tests/services/plugin/test_plugin_auto_upgrade_service.py b/api/tests/unit_tests/services/plugin/test_plugin_auto_upgrade_service.py index 021bebceff..88d5853a78 100644 --- a/api/tests/unit_tests/services/plugin/test_plugin_auto_upgrade_service.py +++ b/api/tests/unit_tests/services/plugin/test_plugin_auto_upgrade_service.py @@ -1,8 +1,10 @@ +from types import SimpleNamespace from unittest.mock import MagicMock, patch from models.account import TenantPluginAutoUpgradeStrategy MODULE = "services.plugin.plugin_auto_upgrade_service" +PLUGIN_CATEGORY = TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL def _patched_session(): @@ -25,7 +27,7 @@ class TestGetStrategy: with p1: from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService - result = PluginAutoUpgradeService.get_strategy("t1") + result = PluginAutoUpgradeService.get_strategy("t1", PLUGIN_CATEGORY) assert result is strategy @@ -36,7 +38,7 @@ class TestGetStrategy: with p1: from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService - result = PluginAutoUpgradeService.get_strategy("t1") + result = PluginAutoUpgradeService.get_strategy("t1", PLUGIN_CATEGORY) assert result is None @@ -57,6 +59,7 @@ class TestChangeStrategy: TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL, [], [], + category=PLUGIN_CATEGORY, ) assert result is True @@ -77,6 +80,7 @@ class TestChangeStrategy: TenantPluginAutoUpgradeStrategy.UpgradeMode.PARTIAL, ["p1"], ["p2"], + category=PLUGIN_CATEGORY, ) assert result is True @@ -96,17 +100,19 @@ class TestExcludePlugin: p1, patch(f"{MODULE}.select"), patch(f"{MODULE}.TenantPluginAutoUpgradeStrategy") as strat_cls, - patch(f"{MODULE}.PluginAutoUpgradeService.change_strategy") as cs, ): strat_cls.StrategySetting.FIX_ONLY = "fix_only" strat_cls.UpgradeMode.EXCLUDE = "exclude" - cs.return_value = True from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService - result = PluginAutoUpgradeService.exclude_plugin("t1", "plugin-1") + result = PluginAutoUpgradeService.exclude_plugin( + "t1", + "plugin-1", + PLUGIN_CATEGORY, + ) assert result is True - cs.assert_called_once() + session.add.assert_called_once() def test_appends_to_exclude_list_in_exclude_mode(self): p1, session = _patched_session() @@ -121,7 +127,7 @@ class TestExcludePlugin: strat_cls.UpgradeMode.ALL = "all" from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService - result = PluginAutoUpgradeService.exclude_plugin("t1", "p-new") + result = PluginAutoUpgradeService.exclude_plugin("t1", "p-new", PLUGIN_CATEGORY) assert result is True assert existing.exclude_plugins == ["p-existing", "p-new"] @@ -139,7 +145,7 @@ class TestExcludePlugin: strat_cls.UpgradeMode.ALL = "all" from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService - result = PluginAutoUpgradeService.exclude_plugin("t1", "p1") + result = PluginAutoUpgradeService.exclude_plugin("t1", "p1", PLUGIN_CATEGORY) assert result is True assert existing.include_plugins == ["p2"] @@ -156,7 +162,7 @@ class TestExcludePlugin: strat_cls.UpgradeMode.ALL = "all" from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService - result = PluginAutoUpgradeService.exclude_plugin("t1", "p1") + result = PluginAutoUpgradeService.exclude_plugin("t1", "p1", PLUGIN_CATEGORY) assert result is True assert existing.upgrade_mode == "exclude" @@ -175,6 +181,101 @@ class TestExcludePlugin: strat_cls.UpgradeMode.ALL = "all" from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService - PluginAutoUpgradeService.exclude_plugin("t1", "p1") + PluginAutoUpgradeService.exclude_plugin("t1", "p1", PLUGIN_CATEGORY) assert existing.exclude_plugins == ["p1"] + + +class TestBackfillStrategyCategories: + def test_creates_default_missing_categories_without_fetching_daemon(self): + p1, session = _patched_session() + tool_strategy = SimpleNamespace( + category=TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL, + strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY, + upgrade_time_of_day=0, + upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE, + exclude_plugins=[], + include_plugins=[], + ) + session.scalars.return_value.all.return_value = [tool_strategy] + installer = MagicMock() + + with p1, patch(f"{MODULE}.PluginInstaller", return_value=installer): + from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService + + result = PluginAutoUpgradeService.backfill_strategy_categories("t1") + expected_time = PluginAutoUpgradeService.default_upgrade_time_of_day("t1") + + assert result.created_count == len(TenantPluginAutoUpgradeStrategy.PluginCategory) - 1 + assert result.normalized is False + installer.list_plugins.assert_not_called() + assert tool_strategy.upgrade_time_of_day == expected_time + created_strategies = [call.args[0] for call in session.add.call_args_list] + model_strategy = next( + strategy + for strategy in created_strategies + if strategy.category == TenantPluginAutoUpgradeStrategy.PluginCategory.MODEL + ) + assert model_strategy.strategy_setting == TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST + assert model_strategy.upgrade_time_of_day == expected_time + + def test_default_upgrade_time_is_aligned_to_fifteen_minutes(self): + from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService + + default_time = PluginAutoUpgradeService.default_upgrade_time_of_day("t1") + + assert default_time % (15 * 60) == 0 + assert 0 <= default_time < 24 * 60 * 60 + + def test_creates_missing_categories_and_splits_known_plugins(self): + p1, session = _patched_session() + tool_strategy = SimpleNamespace( + category=TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL, + strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY, + upgrade_time_of_day=0, + upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE, + exclude_plugins=["tool-plugin", "model-plugin", "unknown-plugin"], + include_plugins=["model-plugin", "tool-plugin"], + ) + model_strategy = SimpleNamespace( + category=TenantPluginAutoUpgradeStrategy.PluginCategory.MODEL, + strategy_setting=TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY, + upgrade_time_of_day=0, + upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE, + exclude_plugins=["tool-plugin", "model-plugin", "unknown-plugin"], + include_plugins=["model-plugin", "tool-plugin"], + ) + session.scalars.return_value.all.return_value = [tool_strategy, model_strategy] + + installed_plugins = [ + SimpleNamespace( + plugin_id="tool-plugin", + declaration=SimpleNamespace(category=TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL), + ), + SimpleNamespace( + plugin_id="model-plugin", + declaration=SimpleNamespace(category=TenantPluginAutoUpgradeStrategy.PluginCategory.MODEL), + ), + ] + installer = MagicMock() + installer.list_plugins.return_value = installed_plugins + + with p1, patch(f"{MODULE}.PluginInstaller", return_value=installer), patch(f"{MODULE}.logger") as logger: + from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService + + result = PluginAutoUpgradeService.backfill_strategy_categories("t1") + + assert result.created_count == len(TenantPluginAutoUpgradeStrategy.PluginCategory) - 2 + assert result.normalized is True + assert session.add.call_count == len(TenantPluginAutoUpgradeStrategy.PluginCategory) - 2 + assert tool_strategy.exclude_plugins == ["tool-plugin"] + assert tool_strategy.include_plugins == ["tool-plugin"] + assert model_strategy.exclude_plugins == ["model-plugin"] + assert model_strategy.include_plugins == ["model-plugin"] + logger.warning.assert_called_once_with( + "Skipped unknown plugin IDs while backfilling plugin auto-upgrade strategies: " + "tenant_id=%s, field=%s, plugin_ids=%s", + "t1", + "exclude_plugins", + ["unknown-plugin"], + ) diff --git a/api/tests/unit_tests/services/test_account_service.py b/api/tests/unit_tests/services/test_account_service.py index 626c4b9706..db79cf4cb5 100644 --- a/api/tests/unit_tests/services/test_account_service.py +++ b/api/tests/unit_tests/services/test_account_service.py @@ -65,6 +65,7 @@ class TestAccountAssociatedDataFactory: tenant_join.account_id = account_id tenant_join.current = current tenant_join.role = role + tenant_join.last_opened_at = kwargs.pop("last_opened_at", None) for key, value in kwargs.items(): setattr(tenant_join, key, value) return tenant_join @@ -489,11 +490,13 @@ class TestAccountService: # Mock datetime with ( patch("services.account_service.datetime") as mock_datetime, + patch("services.account_service.naive_utc_now") as mock_naive_utc_now, patch.object(AccountService, "_refresh_account_last_active") as mock_refresh_last_active, ): mock_now = datetime.now() mock_datetime.now.return_value = mock_now mock_datetime.UTC = "UTC" + mock_naive_utc_now.return_value = mock_now # Execute test result = AccountService.load_user("user-123") @@ -501,6 +504,7 @@ class TestAccountService: # Verify results assert result == mock_account assert mock_available_tenant.current is True + assert mock_available_tenant.last_opened_at == mock_now self._assert_database_operations_called(mock_db_dependencies["db"]) mock_refresh_last_active.assert_called_once_with(mock_account) @@ -922,11 +926,16 @@ class TestTenantService: # Mock scalar for the join query mock_db.session.scalar.return_value = mock_tenant_join - # Execute test - TenantService.switch_tenant(mock_account, "tenant-456") + with patch("services.account_service.naive_utc_now") as mock_naive_utc_now: + mock_now = datetime(2026, 6, 5, 11, 0, 0) + mock_naive_utc_now.return_value = mock_now + + # Execute test + TenantService.switch_tenant(mock_account, "tenant-456") # Verify tenant was switched assert mock_tenant_join.current is True + assert mock_tenant_join.last_opened_at == mock_now self._assert_database_operations_called(mock_db) def test_switch_tenant_no_tenant_id(self): diff --git a/api/tests/unit_tests/tasks/test_process_tenant_plugin_autoupgrade_check_task.py b/api/tests/unit_tests/tasks/test_process_tenant_plugin_autoupgrade_check_task.py index 75d8b92044..a4412c1ee9 100644 --- a/api/tests/unit_tests/tasks/test_process_tenant_plugin_autoupgrade_check_task.py +++ b/api/tests/unit_tests/tasks/test_process_tenant_plugin_autoupgrade_check_task.py @@ -4,19 +4,25 @@ from types import SimpleNamespace from unittest.mock import MagicMock, patch from core.plugin.entities.marketplace import MarketplacePluginSnapshot -from core.plugin.entities.plugin import PluginInstallationSource +from core.plugin.entities.plugin import PluginCategory, PluginInstallationSource from models.account import TenantPluginAutoUpgradeStrategy MODULE = "tasks.process_tenant_plugin_autoupgrade_check_task" -def _make_plugin(plugin_id: str, version: str, source=PluginInstallationSource.Marketplace): +def _make_plugin( + plugin_id: str, + version: str, + source=PluginInstallationSource.Marketplace, + category: PluginCategory = PluginCategory.Tool, +): """Build a minimal stand-in for a PluginInstallation entry returned by manager.list_plugins.""" return SimpleNamespace( plugin_id=plugin_id, version=version, plugin_unique_identifier=f"{plugin_id}:{version}@deadbeef", source=source, + declaration=SimpleNamespace(category=category), ) @@ -39,6 +45,7 @@ def _run_task( upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL, exclude_plugins=None, include_plugins=None, + category=None, ): """ Execute the celery task synchronously with mocks for the plugin manager, @@ -72,6 +79,7 @@ def _run_task( upgrade_mode, exclude_plugins or [], include_plugins or [], + category, ) return upgrade_mock, upgrade_calls @@ -246,6 +254,26 @@ class TestUpgradeMode: assert upgrade_mock.call_count == 1 assert calls[0][1] == plugins[0].plugin_unique_identifier + def test_category_strategy_only_upgrades_matching_category(self): + plugins = [ + _make_plugin("acme/model-provider", "1.0.0", category=PluginCategory.Model), + _make_plugin("acme/tool-provider", "1.0.0", category=PluginCategory.Tool), + ] + manifests = [ + _make_manifest("acme/model-provider", "1.0.1"), + _make_manifest("acme/tool-provider", "1.0.1"), + ] + + upgrade_mock, calls = _run_task( + plugins=plugins, + manifests=manifests, + upgrade_mode=TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL, + category=TenantPluginAutoUpgradeStrategy.PluginCategory.MODEL, + ) + + upgrade_mock.assert_called_once() + assert calls[0][1] == plugins[0].plugin_unique_identifier + class TestErrorIsolation: def test_one_plugin_failure_does_not_block_others(self): diff --git a/api/tests/unit_tests/tasks/test_remove_app_and_related_data_task.py b/api/tests/unit_tests/tasks/test_remove_app_and_related_data_task.py index 626d1ee0a8..50d03670c5 100644 --- a/api/tests/unit_tests/tasks/test_remove_app_and_related_data_task.py +++ b/api/tests/unit_tests/tasks/test_remove_app_and_related_data_task.py @@ -4,6 +4,7 @@ import pytest from libs.archive_storage import ArchiveStorageNotConfiguredError from tasks.remove_app_and_related_data_task import ( + _delete_app_stars, _delete_app_workflow_archive_logs, _delete_archived_workflow_run_files, _delete_draft_variable_offload_data, @@ -89,6 +90,27 @@ class TestDeleteWorkflowArchiveLogs: mock_session.execute.assert_called_once() +class TestDeleteAppStars: + @patch("tasks.remove_app_and_related_data_task._delete_records") + def test_delete_app_stars_calls_delete_records(self, mock_delete_records): + tenant_id = "tenant-1" + app_id = "app-1" + + _delete_app_stars(tenant_id, app_id) + + mock_delete_records.assert_called_once() + query_sql, params, delete_func, name = mock_delete_records.call_args[0] + assert "app_stars" in query_sql + assert params == {"tenant_id": tenant_id, "app_id": app_id} + assert name == "app star" + + mock_session = MagicMock() + + delete_func(mock_session, "star-1") + + mock_session.execute.assert_called_once() + + class TestDeleteArchivedWorkflowRunFiles: @patch("tasks.remove_app_and_related_data_task.get_archive_storage") @patch("tasks.remove_app_and_related_data_task.logger") diff --git a/e2e/features/smoke/authenticated-entry.feature b/e2e/features/smoke/authenticated-entry.feature index 3c1191a330..53d72bd667 100644 --- a/e2e/features/smoke/authenticated-entry.feature +++ b/e2e/features/smoke/authenticated-entry.feature @@ -4,5 +4,4 @@ Feature: Authenticated app console Given I am signed in as the default E2E admin When I open the apps console Then I should stay on the apps console - And I should see the "Create from Blank" button And I should not see the "Sign in" button diff --git a/e2e/features/smoke/install.feature b/e2e/features/smoke/install.feature index 39fc1f996b..5685d87486 100644 --- a/e2e/features/smoke/install.feature +++ b/e2e/features/smoke/install.feature @@ -4,4 +4,3 @@ Feature: Fresh installation bootstrap Given the last authentication bootstrap came from a fresh install When I open the apps console Then I should stay on the apps console - And I should see the "Create from Blank" button diff --git a/e2e/features/step-definitions/apps/create-app.steps.ts b/e2e/features/step-definitions/apps/create-app.steps.ts index 931d4662a2..d6a5eb21d5 100644 --- a/e2e/features/step-definitions/apps/create-app.steps.ts +++ b/e2e/features/step-definitions/apps/create-app.steps.ts @@ -1,12 +1,10 @@ import type { DifyWorld } from '../../support/world' import { Then, When } from '@cucumber/cucumber' import { expect } from '@playwright/test' +import { openBlankAppCreation } from '../../../support/apps' When('I start creating a blank app', async function (this: DifyWorld) { - const page = this.getPage() - - await expect(page.getByRole('button', { name: 'Create from Blank' })).toBeVisible() - await page.getByRole('button', { name: 'Create from Blank' }).click() + await openBlankAppCreation(this.getPage()) }) When('I enter a unique E2E app name', async function (this: DifyWorld) { diff --git a/e2e/features/step-definitions/apps/delete-app.steps.ts b/e2e/features/step-definitions/apps/delete-app.steps.ts index e5da626645..e5a5da424a 100644 --- a/e2e/features/step-definitions/apps/delete-app.steps.ts +++ b/e2e/features/step-definitions/apps/delete-app.steps.ts @@ -29,7 +29,7 @@ Then('the app should no longer appear in the apps console', async function (this ) } - await expect(this.getPage().getByTitle(appName)).not.toBeVisible({ + await expect(this.getPage().getByRole('link', { name: appName, exact: true })).not.toBeVisible({ timeout: 10_000, }) }) diff --git a/e2e/features/step-definitions/apps/duplicate-app.steps.ts b/e2e/features/step-definitions/apps/duplicate-app.steps.ts index e5e3694e4d..5e998b3603 100644 --- a/e2e/features/step-definitions/apps/duplicate-app.steps.ts +++ b/e2e/features/step-definitions/apps/duplicate-app.steps.ts @@ -1,5 +1,6 @@ import type { DifyWorld } from '../../support/world' import { Given, When } from '@cucumber/cucumber' +import { expect } from '@playwright/test' import { createTestApp } from '../../../support/api' Given('there is an existing E2E app available for testing', async function (this: DifyWorld) { @@ -15,14 +16,13 @@ When('I open the options menu for the last created E2E app', async function (thi throw new Error('No app name stored. Run "I enter a unique E2E app name" first.') const page = this.getPage() - // Scope to the specific card: the card root is the innermost div that contains - // both the unique app name text and a More button (they are in separate branches, - // so no child div satisfies both). .last() picks the deepest match in DOM order. + const appLink = page.getByRole('link', { name: appName, exact: true }) const appCard = page .locator('div') - .filter({ has: page.getByText(appName, { exact: true }) }) + .filter({ has: appLink }) .filter({ has: page.getByRole('button', { name: 'More' }) }) .last() + await expect(appLink).toBeVisible() await appCard.hover() await appCard.getByRole('button', { name: 'More' }).click() }) diff --git a/e2e/features/step-definitions/auth/sign-in.steps.ts b/e2e/features/step-definitions/auth/sign-in.steps.ts index 095f816407..469203d8bf 100644 --- a/e2e/features/step-definitions/auth/sign-in.steps.ts +++ b/e2e/features/step-definitions/auth/sign-in.steps.ts @@ -4,7 +4,7 @@ import { expect } from '@playwright/test' import { adminCredentials } from '../../../fixtures/auth' When('I open the sign-in page', async function (this: DifyWorld) { - await this.getPage().goto('/signin') + await this.getPage().goto('/signin?redirect_url=%2Fapps') }) When('I sign in as the default E2E admin', async function (this: DifyWorld) { diff --git a/e2e/features/step-definitions/common/app.steps.ts b/e2e/features/step-definitions/common/app.steps.ts index 93e808e3c5..6deca22c60 100644 --- a/e2e/features/step-definitions/common/app.steps.ts +++ b/e2e/features/step-definitions/common/app.steps.ts @@ -2,6 +2,7 @@ import type { DifyWorld } from '../../support/world' import { Given, When } from '@cucumber/cucumber' import { expect } from '@playwright/test' import { createTestApp, syncMinimalWorkflowDraft } from '../../../support/api' +import { waitForAppsConsole } from '../../../support/apps' Given('a {string} app has been created via API', async function (this: DifyWorld, mode: string) { const app = await createTestApp(`E2E ${Date.now()}`, mode) @@ -17,6 +18,8 @@ Given('a minimal workflow draft has been synced', async function (this: DifyWorl When('I open the app from the app list', async function (this: DifyWorld) { const page = this.getPage() await page.goto('/apps') - await expect(page.getByRole('button', { name: 'Create from Blank' })).toBeVisible() - await page.getByText(this.lastCreatedAppName!).click() + await waitForAppsConsole(page) + const appLink = page.getByRole('link', { name: this.lastCreatedAppName!, exact: true }) + await expect(appLink).toBeVisible() + await appLink.click() }) diff --git a/e2e/features/step-definitions/common/navigation.steps.ts b/e2e/features/step-definitions/common/navigation.steps.ts index 9bec34c224..02b860b372 100644 --- a/e2e/features/step-definitions/common/navigation.steps.ts +++ b/e2e/features/step-definitions/common/navigation.steps.ts @@ -1,13 +1,14 @@ import type { DifyWorld } from '../../support/world' import { Then, When } from '@cucumber/cucumber' import { expect } from '@playwright/test' +import { waitForAppsConsole } from '../../../support/apps' When('I open the apps console', async function (this: DifyWorld) { await this.getPage().goto('/apps') }) Then('I should stay on the apps console', async function (this: DifyWorld) { - await expect(this.getPage()).toHaveURL(/\/apps(?:\?.*)?$/) + await waitForAppsConsole(this.getPage()) }) Then('I should be redirected to the signin page', async function (this: DifyWorld) { diff --git a/e2e/fixtures/auth.ts b/e2e/fixtures/auth.ts index cc54a6d47b..9039f97483 100644 --- a/e2e/fixtures/auth.ts +++ b/e2e/fixtures/auth.ts @@ -1,9 +1,10 @@ -import type { Browser, Page } from '@playwright/test' +import type { APIResponse, Browser, BrowserContext } from '@playwright/test' +import { Buffer } from 'node:buffer' import { mkdir, readFile, writeFile } from 'node:fs/promises' import path from 'node:path' import { fileURLToPath } from 'node:url' -import { expect } from '@playwright/test' -import { defaultBaseURL, defaultLocale } from '../test-env' +import { waitForAppsConsole } from '../support/apps' +import { apiURL, defaultBaseURL, defaultLocale } from '../test-env' export type AuthSessionMetadata = { adminEmail: string @@ -12,7 +13,8 @@ export type AuthSessionMetadata = { usedInitPassword: boolean } -export const AUTH_BOOTSTRAP_TIMEOUT_MS = 120_000 +export const AUTH_BOOTSTRAP_TIMEOUT_MS = 180_000 +const AUTH_FLOW_TIMEOUT_MS = AUTH_BOOTSTRAP_TIMEOUT_MS - 30_000 const e2eRoot = fileURLToPath(new URL('..', import.meta.url)) export const authDir = path.join(e2eRoot, '.auth') @@ -35,89 +37,106 @@ export const readAuthSessionMetadata = async () => { return JSON.parse(content) as AuthSessionMetadata } -const escapeRegex = (value: string) => value.replaceAll(/[.*+?^${}()|[\]\\]/g, '\\$&') - const appURL = (baseURL: string, pathname: string) => new URL(pathname, baseURL).toString() +const apiEndpoint = (pathname: string) => new URL(pathname, apiURL).toString() -type AuthPageState = 'install' | 'login' | 'init' +type SetupStatusResponse = { + step: 'not_started' | 'finished' +} + +type InitStatusResponse = { + status: 'not_started' | 'finished' +} + +type AuthBootstrapResult = { + mode: AuthSessionMetadata['mode'] + usedInitPassword: boolean +} const getRemainingTimeout = (deadline: number) => Math.max(deadline - Date.now(), 1) -const waitForPageState = async (page: Page, deadline: number): Promise => { - const installHeading = page.getByRole('heading', { name: 'Setting up an admin account' }) - const signInButton = page.getByRole('button', { name: 'Sign in' }) - const initPasswordField = page.getByLabel('Admin initialization password') +const encodeField = (value: string) => Buffer.from(value, 'utf8').toString('base64') - try { - return await Promise.any([ - installHeading - .waitFor({ state: 'visible', timeout: getRemainingTimeout(deadline) }) - .then(() => 'install'), - signInButton - .waitFor({ state: 'visible', timeout: getRemainingTimeout(deadline) }) - .then(() => 'login'), - initPasswordField - .waitFor({ state: 'visible', timeout: getRemainingTimeout(deadline) }) - .then(() => 'init'), - ]) - } - catch { - throw new Error(`Unable to determine auth page state for ${page.url()}`) - } +const assertAPIResponse = async (response: APIResponse, action: string) => { + if (response.ok()) + return + + const body = await response.text().catch(() => '') + throw new Error( + `${action} failed with ${response.status()} ${response.statusText()}${body ? `: ${body}` : ''}`, + ) } -const completeInitPasswordIfNeeded = async (page: Page, deadline: number) => { - const initPasswordField = page.getByLabel('Admin initialization password') - - const needsInitPassword = await initPasswordField - .waitFor({ state: 'visible', timeout: Math.min(getRemainingTimeout(deadline), 3_000) }) - .then(() => true) - .catch(() => false) - - if (!needsInitPassword) - return false - - await initPasswordField.fill(initPassword) - await page.getByRole('button', { name: 'Validate' }).click() - await expect(page.getByRole('heading', { name: 'Setting up an admin account' })).toBeVisible({ +const getConsoleAPI = async (context: BrowserContext, pathname: string, deadline: number) => { + const response = await context.request.get(apiEndpoint(pathname), { timeout: getRemainingTimeout(deadline), }) + await assertAPIResponse(response, `GET ${pathname}`) + return response.json() as Promise +} +const postConsoleAPI = async ( + context: BrowserContext, + pathname: string, + deadline: number, + data: Record, +) => { + const response = await context.request.post(apiEndpoint(pathname), { + data, + timeout: getRemainingTimeout(deadline), + }) + await assertAPIResponse(response, `POST ${pathname}`) +} + +const validateInitPasswordIfNeeded = async (context: BrowserContext, deadline: number) => { + const initStatus = await getConsoleAPI(context, '/console/api/init', deadline) + if (initStatus.status === 'finished') + return false + + console.warn('[e2e] auth bootstrap: validating init password') + await postConsoleAPI(context, '/console/api/init', deadline, { password: initPassword }) return true } -const completeInstall = async (page: Page, baseURL: string, deadline: number) => { - await expect(page.getByRole('heading', { name: 'Setting up an admin account' })).toBeVisible({ - timeout: getRemainingTimeout(deadline), - }) +const ensureAdminAccount = async ( + context: BrowserContext, + deadline: number, +): Promise => { + const setupStatus = await getConsoleAPI( + context, + '/console/api/setup', + deadline, + ) + let usedInitPassword = false - await page.getByLabel('Email address').fill(adminCredentials.email) - await page.getByLabel('Username').fill(adminCredentials.name) - await page.getByLabel('Password').fill(adminCredentials.password) - await page.getByRole('button', { name: 'Set up' }).click() + if (setupStatus.step === 'not_started') { + usedInitPassword = await validateInitPasswordIfNeeded(context, deadline) + console.warn('[e2e] auth bootstrap: creating admin account') + await postConsoleAPI(context, '/console/api/setup', deadline, { + email: adminCredentials.email, + name: adminCredentials.name, + password: adminCredentials.password, + language: defaultLocale, + }) - await expect(page).toHaveURL(new RegExp(`^${escapeRegex(baseURL)}/apps(?:\\?.*)?$`), { - timeout: getRemainingTimeout(deadline), - }) + return { mode: 'install', usedInitPassword } + } + + return { mode: 'login', usedInitPassword } } -const completeLogin = async (page: Page, baseURL: string, deadline: number) => { - await expect(page.getByRole('button', { name: 'Sign in' })).toBeVisible({ - timeout: getRemainingTimeout(deadline), - }) - - await page.getByLabel('Email address').fill(adminCredentials.email) - await page.getByLabel('Password').fill(adminCredentials.password) - await page.getByRole('button', { name: 'Sign in' }).click() - - await expect(page).toHaveURL(new RegExp(`^${escapeRegex(baseURL)}/apps(?:\\?.*)?$`), { - timeout: getRemainingTimeout(deadline), +const loginAdmin = async (context: BrowserContext, deadline: number) => { + console.warn('[e2e] auth bootstrap: logging in admin') + await postConsoleAPI(context, '/console/api/login', deadline, { + email: adminCredentials.email, + password: encodeField(adminCredentials.password), + remember_me: true, }) } export const ensureAuthenticatedState = async (browser: Browser, configuredBaseURL?: string) => { const baseURL = resolveBaseURL(configuredBaseURL) - const deadline = Date.now() + AUTH_BOOTSTRAP_TIMEOUT_MS + const deadline = Date.now() + AUTH_FLOW_TIMEOUT_MS await mkdir(authDir, { recursive: true }) @@ -128,37 +147,22 @@ export const ensureAuthenticatedState = async (browser: Browser, configuredBaseU const page = await context.newPage() try { - await page.goto(appURL(baseURL, '/install'), { + const { mode, usedInitPassword } = await ensureAdminAccount(context, deadline) + await loginAdmin(context, deadline) + + console.warn('[e2e] auth bootstrap: verifying apps console') + await page.goto(appURL(baseURL, '/apps'), { timeout: getRemainingTimeout(deadline), waitUntil: 'domcontentloaded', }) - - let usedInitPassword = await completeInitPasswordIfNeeded(page, deadline) - let pageState = await waitForPageState(page, deadline) - - while (pageState === 'init') { - const completedInitPassword = await completeInitPasswordIfNeeded(page, deadline) - if (!completedInitPassword) - throw new Error(`Unable to validate initialization password for ${page.url()}`) - - usedInitPassword = true - pageState = await waitForPageState(page, deadline) - } - - if (pageState === 'install') - await completeInstall(page, baseURL, deadline) - else await completeLogin(page, baseURL, deadline) - - await expect(page.getByRole('button', { name: 'Create from Blank' })).toBeVisible({ - timeout: getRemainingTimeout(deadline), - }) + await waitForAppsConsole(page, getRemainingTimeout(deadline)) await context.storageState({ path: authStatePath }) const metadata: AuthSessionMetadata = { adminEmail: adminCredentials.email, baseURL, - mode: pageState, + mode, usedInitPassword, } diff --git a/e2e/support/apps.ts b/e2e/support/apps.ts new file mode 100644 index 0000000000..f035b5f4a1 --- /dev/null +++ b/e2e/support/apps.ts @@ -0,0 +1,24 @@ +import type { Page } from '@playwright/test' +import { expect } from '@playwright/test' + +export const waitForAppsConsole = async (page: Page, timeout?: number) => { + await expect(page).toHaveURL(/\/apps(?:\?.*)?$/, timeout === undefined ? undefined : { timeout }) + await expect(page.getByRole('heading', { name: 'Studio' })).toBeVisible( + timeout === undefined ? undefined : { timeout }, + ) +} + +export const openBlankAppCreation = async (page: Page) => { + const createFromBlankButton = page.getByRole('button', { name: 'Create from Blank' }).first() + const isDirectCreateVisible = await createFromBlankButton + .isVisible({ timeout: 3_000 }) + .catch(() => false) + + if (isDirectCreateVisible) { + await createFromBlankButton.click() + return + } + + await page.getByRole('button', { name: 'Create' }).click() + await page.getByRole('menuitem', { name: 'Create from Blank' }).click() +} diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 0f5754a813..9cb80eb758 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -152,17 +152,6 @@ "count": 1 } }, - "web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx": { - "no-restricted-globals": { - "count": 1 - }, - "react/set-state-in-effect": { - "count": 2 - }, - "ts/no-explicit-any": { - "count": 1 - } - }, "web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/date-picker.tsx": { "jsx-a11y/click-events-have-key-events": { "count": 1 @@ -396,9 +385,6 @@ }, "web/app/components/app-sidebar/index.tsx": { "no-restricted-globals": { - "count": 2 - }, - "ts/no-explicit-any": { "count": 1 } }, @@ -990,9 +976,6 @@ }, "jsx-a11y/no-static-element-interactions": { "count": 1 - }, - "no-restricted-imports": { - "count": 1 } }, "web/app/components/app/overview/trigger-card.tsx": { @@ -1051,10 +1034,10 @@ }, "web/app/components/apps/app-card.tsx": { "jsx-a11y/click-events-have-key-events": { - "count": 2 + "count": 1 }, "jsx-a11y/no-static-element-interactions": { - "count": 2 + "count": 1 } }, "web/app/components/apps/import-from-marketplace-template-modal.tsx": { @@ -1065,17 +1048,6 @@ "count": 1 } }, - "web/app/components/apps/new-app-card.tsx": { - "react-hooks-extra/no-direct-set-state-in-use-effect": { - "count": 1 - }, - "react/set-state-in-effect": { - "count": 1 - }, - "ts/no-explicit-any": { - "count": 1 - } - }, "web/app/components/base/action-button/index.tsx": { "erasable-syntax-only/enums": { "count": 1 @@ -1886,7 +1858,7 @@ }, "web/app/components/base/icons/src/vender/line/alertsAndFeedback/index.ts": { "no-barrel-files/no-barrel-files": { - "count": 2 + "count": 1 } }, "web/app/components/base/icons/src/vender/line/arrows/index.ts": { @@ -1921,7 +1893,7 @@ }, "web/app/components/base/icons/src/vender/line/financeAndECommerce/index.ts": { "no-barrel-files/no-barrel-files": { - "count": 4 + "count": 3 } }, "web/app/components/base/icons/src/vender/line/general/index.ts": { @@ -3474,6 +3446,11 @@ "count": 1 } }, + "web/app/components/datasets/list/__tests__/header.spec.tsx": { + "jsx-a11y/label-has-associated-control": { + "count": 1 + } + }, "web/app/components/datasets/list/dataset-card/__tests__/index.spec.tsx": { "jsx-a11y/click-events-have-key-events": { "count": 1 @@ -3628,11 +3605,6 @@ "count": 2 } }, - "web/app/components/explore/app-list/index.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/explore/banner/__tests__/indicator-button.spec.tsx": { "jsx-a11y/click-events-have-key-events": { "count": 1 @@ -3647,12 +3619,6 @@ }, "jsx-a11y/no-static-element-interactions": { "count": 1 - }, - "react-hooks-extra/no-direct-set-state-in-use-effect": { - "count": 1 - }, - "react/set-state-in-effect": { - "count": 1 } }, "web/app/components/explore/banner/indicator-button.tsx": { @@ -3663,14 +3629,6 @@ "count": 2 } }, - "web/app/components/explore/category.tsx": { - "jsx-a11y/click-events-have-key-events": { - "count": 2 - }, - "jsx-a11y/no-static-element-interactions": { - "count": 2 - } - }, "web/app/components/explore/item-operation/__tests__/index.spec.tsx": { "jsx-a11y/click-events-have-key-events": { "count": 1 @@ -3679,12 +3637,17 @@ "count": 1 } }, + "web/app/components/explore/learn-dify/item.tsx": { + "jsx-a11y/no-noninteractive-element-interactions": { + "count": 1 + } + }, "web/app/components/explore/sidebar/app-nav-item/index.tsx": { "jsx-a11y/click-events-have-key-events": { - "count": 2 + "count": 1 }, "jsx-a11y/no-static-element-interactions": { - "count": 2 + "count": 1 } }, "web/app/components/explore/try-app/app/text-generation.tsx": { @@ -3700,34 +3663,16 @@ "count": 2 } }, - "web/app/components/goto-anything/actions/commands/index.ts": { - "no-barrel-files/no-barrel-files": { - "count": 2 - } - }, "web/app/components/goto-anything/actions/commands/registry.ts": { "ts/no-explicit-any": { "count": 3 } }, - "web/app/components/goto-anything/actions/commands/slash.tsx": { - "react-refresh/only-export-components": { - "count": 1 - }, - "ts/no-explicit-any": { - "count": 1 - } - }, "web/app/components/goto-anything/actions/commands/types.ts": { "ts/no-explicit-any": { "count": 1 } }, - "web/app/components/goto-anything/actions/index.ts": { - "no-barrel-files/no-barrel-files": { - "count": 2 - } - }, "web/app/components/goto-anything/actions/plugin.tsx": { "no-restricted-imports": { "count": 1 @@ -3743,11 +3688,6 @@ "count": 2 } }, - "web/app/components/goto-anything/command-selector.tsx": { - "react/unsupported-syntax": { - "count": 2 - } - }, "web/app/components/goto-anything/components/__tests__/search-input.spec.tsx": { "jsx-a11y/no-autofocus": { "count": 1 @@ -3779,11 +3719,6 @@ "count": 4 } }, - "web/app/components/goto-anything/hooks/index.ts": { - "no-barrel-files/no-barrel-files": { - "count": 4 - } - }, "web/app/components/goto-anything/hooks/use-goto-anything-results.ts": { "@tanstack/query/exhaustive-deps": { "count": 1 @@ -3798,10 +3733,10 @@ } }, "web/app/components/header/account-setting/data-source-page-new/card.tsx": { - "jsx-a11y/alt-text": { - "count": 1 + "jsx-a11y/click-events-have-key-events": { + "count": 2 }, - "ts/no-explicit-any": { + "jsx-a11y/no-static-element-interactions": { "count": 2 } }, @@ -3815,16 +3750,11 @@ "count": 1 } }, - "web/app/components/header/account-setting/data-source-page-new/install-from-marketplace.tsx": { - "ts/no-explicit-any": { - "count": 1 - } - }, - "web/app/components/header/account-setting/data-source-page-new/item.tsx": { - "no-restricted-imports": { + "web/app/components/header/account-setting/data-source-page-new/plugin-actions.tsx": { + "jsx-a11y/click-events-have-key-events": { "count": 1 }, - "ts/no-explicit-any": { + "jsx-a11y/no-static-element-interactions": { "count": 1 } }, @@ -4027,6 +3957,11 @@ "count": 2 } }, + "web/app/components/header/account-setting/model-provider-page/model-provider-page-body.tsx": { + "jsx-a11y/anchor-has-content": { + "count": 1 + } + }, "web/app/components/header/account-setting/model-provider-page/provider-added-card/cooldown-timer.tsx": { "react/set-state-in-effect": { "count": 1 @@ -4096,8 +4031,13 @@ "count": 1 } }, - "web/app/components/plugins/card/index.tsx": { - "ts/no-non-null-asserted-optional-chain": { + "web/app/components/main-nav/components/web-apps-section.tsx": { + "jsx-a11y/no-autofocus": { + "count": 1 + } + }, + "web/app/components/main-nav/components/workspace-switcher.tsx": { + "jsx-a11y/no-autofocus": { "count": 1 } }, @@ -4490,11 +4430,6 @@ "count": 1 } }, - "web/app/components/plugins/plugin-page/empty/index.tsx": { - "react/set-state-in-effect": { - "count": 2 - } - }, "web/app/components/plugins/plugin-page/filter-management/__tests__/index.spec.tsx": { "jsx-a11y/click-events-have-key-events": { "count": 1 @@ -4513,11 +4448,6 @@ "count": 1 } }, - "web/app/components/plugins/plugin-page/filter-management/tag-filter.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/plugins/reference-setting-modal/auto-update-setting/__tests__/index.spec.tsx": { "jsx-a11y/click-events-have-key-events": { "count": 1 @@ -4555,11 +4485,6 @@ "count": 25 } }, - "web/app/components/plugins/update-plugin/from-market-place.tsx": { - "erasable-syntax-only/enums": { - "count": 1 - } - }, "web/app/components/plugins/update-plugin/plugin-version-picker.tsx": { "jsx-a11y/click-events-have-key-events": { "count": 1 @@ -4940,32 +4865,11 @@ "count": 2 } }, - "web/app/components/tools/mcp/create-card.tsx": { - "jsx-a11y/click-events-have-key-events": { - "count": 1 - }, - "jsx-a11y/no-static-element-interactions": { - "count": 1 - }, - "ts/no-explicit-any": { - "count": 1 - } - }, - "web/app/components/tools/mcp/headers-input.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/tools/mcp/mcp-server-param-item.tsx": { "ts/no-explicit-any": { "count": 1 } }, - "web/app/components/tools/mcp/modal.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/tools/mcp/provider-card.tsx": { "jsx-a11y/click-events-have-key-events": { "count": 2 @@ -4977,40 +4881,8 @@ "count": 3 } }, - "web/app/components/tools/mcp/sections/authentication-section.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "web/app/components/tools/mcp/sections/configurations-section.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "web/app/components/tools/provider-list.tsx": { - "jsx-a11y/click-events-have-key-events": { - "count": 1 - }, - "jsx-a11y/no-static-element-interactions": { - "count": 1 - }, - "no-restricted-imports": { - "count": 1 - }, - "ts/no-explicit-any": { - "count": 1 - } - }, - "web/app/components/tools/provider/custom-create-card.tsx": { - "jsx-a11y/click-events-have-key-events": { - "count": 1 - }, - "jsx-a11y/no-static-element-interactions": { - "count": 1 - } - }, - "web/app/components/tools/provider/empty.tsx": { - "ts/no-explicit-any": { + "web/app/components/tools/provider/detail.tsx": { + "jsx-a11y/anchor-has-content": { "count": 1 } }, @@ -5027,6 +4899,14 @@ "count": 3 } }, + "web/app/components/tools/tool-provider-grid.tsx": { + "jsx-a11y/click-events-have-key-events": { + "count": 1 + }, + "jsx-a11y/no-static-element-interactions": { + "count": 1 + } + }, "web/app/components/tools/types.ts": { "erasable-syntax-only/enums": { "count": 4 @@ -5334,11 +5214,6 @@ "count": 1 } }, - "web/app/components/workflow/header/__tests__/index.spec.tsx": { - "react/static-components": { - "count": 2 - } - }, "web/app/components/workflow/header/online-users.tsx": { "jsx-a11y/click-events-have-key-events": { "count": 2 @@ -5448,16 +5323,6 @@ "count": 1 } }, - "web/app/components/workflow/hooks/use-workflow-canvas-maximize.ts": { - "no-restricted-globals": { - "count": 1 - } - }, - "web/app/components/workflow/hooks/use-workflow-interactions.ts": { - "no-barrel-files/no-barrel-files": { - "count": 5 - } - }, "web/app/components/workflow/hooks/use-workflow-run-event/index.ts": { "no-barrel-files/no-barrel-files": { "count": 19 @@ -7406,11 +7271,6 @@ "count": 2 } }, - "web/app/components/workflow/store/workflow/layout-slice.ts": { - "no-restricted-properties": { - "count": 1 - } - }, "web/app/components/workflow/store/workflow/workflow-draft-slice.ts": { "ts/no-explicit-any": { "count": 1 @@ -7728,11 +7588,6 @@ "count": 3 } }, - "web/context/provider-context-provider.tsx": { - "ts/no-explicit-any": { - "count": 1 - } - }, "web/context/web-app-context.tsx": { "react-refresh/only-export-components": { "count": 1 @@ -7799,46 +7654,6 @@ "count": 1 } }, - "web/i18n/de-DE/billing.json": { - "no-irregular-whitespace": { - "count": 1 - } - }, - "web/i18n/en-US/app-debug.json": { - "no-irregular-whitespace": { - "count": 1 - } - }, - "web/i18n/fr-FR/app-debug.json": { - "no-irregular-whitespace": { - "count": 1 - } - }, - "web/i18n/fr-FR/plugin-trigger.json": { - "no-irregular-whitespace": { - "count": 1 - } - }, - "web/i18n/fr-FR/tools.json": { - "no-irregular-whitespace": { - "count": 1 - } - }, - "web/i18n/pt-BR/common.json": { - "no-irregular-whitespace": { - "count": 1 - } - }, - "web/i18n/ru-RU/common.json": { - "no-irregular-whitespace": { - "count": 2 - } - }, - "web/i18n/uk-UA/app-debug.json": { - "no-irregular-whitespace": { - "count": 1 - } - }, "web/models/access-control.ts": { "erasable-syntax-only/enums": { "count": 2 @@ -7849,14 +7664,6 @@ "count": 2 } }, - "web/models/common.ts": { - "erasable-syntax-only/enums": { - "count": 2 - }, - "ts/no-explicit-any": { - "count": 3 - } - }, "web/models/datasets.ts": { "erasable-syntax-only/enums": { "count": 7 @@ -7971,7 +7778,7 @@ "count": 1 }, "ts/no-explicit-any": { - "count": 29 + "count": 27 } }, "web/service/datasets.ts": { @@ -8167,20 +7974,6 @@ "count": 4 } }, - "web/service/use-plugins.ts": { - "no-restricted-imports": { - "count": 1 - }, - "react/set-state-in-effect": { - "count": 1 - }, - "regexp/no-unused-capturing-group": { - "count": 1 - }, - "ts/no-explicit-any": { - "count": 3 - } - }, "web/service/use-snippet-workflows.ts": { "no-restricted-imports": { "count": 1 diff --git a/packages/contracts/generated/api/console/apps/orpc.gen.ts b/packages/contracts/generated/api/console/apps/orpc.gen.ts index 0d4d0ab775..bd97b260ee 100644 --- a/packages/contracts/generated/api/console/apps/orpc.gen.ts +++ b/packages/contracts/generated/api/console/apps/orpc.gen.ts @@ -20,6 +20,8 @@ import { zDeleteAppsByAppIdCompletionConversationsByConversationIdResponse, zDeleteAppsByAppIdPath, zDeleteAppsByAppIdResponse, + zDeleteAppsByAppIdStarPath, + zDeleteAppsByAppIdStarResponse, zDeleteAppsByAppIdTraceConfigPath, zDeleteAppsByAppIdTraceConfigQuery, zDeleteAppsByAppIdTraceConfigResponse, @@ -247,6 +249,8 @@ import { zGetAppsImportsByAppIdCheckDependenciesResponse, zGetAppsQuery, zGetAppsResponse, + zGetAppsStarredQuery, + zGetAppsStarredResponse, zPatchAppsByAppIdTraceConfigBody, zPatchAppsByAppIdTraceConfigPath, zPatchAppsByAppIdTraceConfigResponse, @@ -350,6 +354,8 @@ import { zPostAppsByAppIdSiteEnableResponse, zPostAppsByAppIdSitePath, zPostAppsByAppIdSiteResponse, + zPostAppsByAppIdStarPath, + zPostAppsByAppIdStarResponse, zPostAppsByAppIdTextToAudioBody, zPostAppsByAppIdTextToAudioPath, zPostAppsByAppIdTextToAudioResponse, @@ -516,6 +522,25 @@ export const imports = { byImportId, } +/** + * Get applications starred by the current account + */ +export const get2 = oc + .route({ + description: 'Get applications starred by the current account', + inputStructure: 'detailed', + method: 'GET', + operationId: 'getAppsStarred', + path: '/apps/starred', + tags: ['console'], + }) + .input(z.object({ query: zGetAppsStarredQuery.optional() })) + .output(zGetAppsStarredResponse) + +export const starred = { + get: get2, +} + /** * Get workflow online users */ @@ -544,7 +569,7 @@ export const workflows = { * * Get advanced chat workflow runs count statistics */ -export const get2 = oc +export const get3 = oc .route({ description: 'Get advanced chat workflow runs count statistics', inputStructure: 'detailed', @@ -563,7 +588,7 @@ export const get2 = oc .output(zGetAppsByAppIdAdvancedChatWorkflowRunsCountResponse) export const count = { - get: get2, + get: get3, } /** @@ -571,7 +596,7 @@ export const count = { * * Get advanced chat workflow run list */ -export const get3 = oc +export const get4 = oc .route({ description: 'Get advanced chat workflow run list', inputStructure: 'detailed', @@ -590,7 +615,7 @@ export const get3 = oc .output(zGetAppsByAppIdAdvancedChatWorkflowRunsResponse) export const workflowRuns = { - get: get3, + get: get4, count, } @@ -786,7 +811,7 @@ export const advancedChat = { workflows: workflows2, } -export const get4 = oc +export const get5 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -798,7 +823,7 @@ export const get4 = oc .output(zGetAppsByAppIdAgentComposerCandidatesResponse) export const candidates = { - get: get4, + get: get5, } export const post9 = oc @@ -821,7 +846,7 @@ export const validate = { post: post9, } -export const get5 = oc +export const get6 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -846,7 +871,7 @@ export const put = oc .output(zPutAppsByAppIdAgentComposerResponse) export const agentComposer = { - get: get5, + get: get6, put, candidates, validate, @@ -879,7 +904,7 @@ export const agentFeatures = { /** * List workflow apps that reference this Agent App's bound Agent (read-only) */ -export const get6 = oc +export const get7 = oc .route({ description: 'List workflow apps that reference this Agent App\'s bound Agent (read-only)', inputStructure: 'detailed', @@ -892,13 +917,13 @@ export const get6 = oc .output(zGetAppsByAppIdAgentReferencingWorkflowsResponse) export const agentReferencingWorkflows = { - get: get6, + get: get7, } /** * Read a text/binary preview file in an Agent App conversation sandbox */ -export const get7 = oc +export const get8 = oc .route({ description: 'Read a text/binary preview file in an Agent App conversation sandbox', inputStructure: 'detailed', @@ -916,7 +941,7 @@ export const get7 = oc .output(zGetAppsByAppIdAgentSandboxFilesReadResponse) export const read = { - get: get7, + get: get8, } /** @@ -946,7 +971,7 @@ export const upload = { /** * List a directory in an Agent App conversation sandbox */ -export const get8 = oc +export const get9 = oc .route({ description: 'List a directory in an Agent App conversation sandbox', inputStructure: 'detailed', @@ -964,7 +989,7 @@ export const get8 = oc .output(zGetAppsByAppIdAgentSandboxFilesResponse) export const files = { - get: get8, + get: get9, read, upload, } @@ -976,7 +1001,7 @@ export const agentSandbox = { /** * Time-limited external signed URL for one drive value (no streaming proxy) */ -export const get9 = oc +export const get10 = oc .route({ description: 'Time-limited external signed URL for one drive value (no streaming proxy)', inputStructure: 'detailed', @@ -994,13 +1019,13 @@ export const get9 = oc .output(zGetAppsByAppIdAgentDriveFilesDownloadResponse) export const download = { - get: get9, + get: get10, } /** * Truncated text preview of one drive value (binary-safe; SKILL.md is the main case) */ -export const get10 = oc +export const get11 = oc .route({ description: 'Truncated text preview of one drive value (binary-safe; SKILL.md is the main case)', @@ -1019,13 +1044,13 @@ export const get10 = oc .output(zGetAppsByAppIdAgentDriveFilesPreviewResponse) export const preview2 = { - get: get10, + get: get11, } /** * List agent drive entries (read-only inspector; one endpoint for both tabs) */ -export const get11 = oc +export const get12 = oc .route({ description: 'List agent drive entries (read-only inspector; one endpoint for both tabs)', inputStructure: 'detailed', @@ -1043,7 +1068,7 @@ export const get11 = oc .output(zGetAppsByAppIdAgentDriveFilesResponse) export const files2 = { - get: get11, + get: get12, download, preview: preview2, } @@ -1107,7 +1132,7 @@ export const files3 = { * * Get agent execution logs for an application */ -export const get12 = oc +export const get13 = oc .route({ description: 'Get agent execution logs for an application', inputStructure: 'detailed', @@ -1121,7 +1146,7 @@ export const get12 = oc .output(zGetAppsByAppIdAgentLogsResponse) export const logs = { - get: get12, + get: get13, } /** @@ -1249,7 +1274,7 @@ export const agent = { /** * Get status of annotation reply action job */ -export const get13 = oc +export const get14 = oc .route({ description: 'Get status of annotation reply action job', inputStructure: 'detailed', @@ -1262,7 +1287,7 @@ export const get13 = oc .output(zGetAppsByAppIdAnnotationReplyByActionStatusByJobIdResponse) export const byJobId = { - get: get13, + get: get14, } export const status = { @@ -1301,7 +1326,7 @@ export const annotationReply = { /** * Get annotation settings for an app */ -export const get14 = oc +export const get15 = oc .route({ description: 'Get annotation settings for an app', inputStructure: 'detailed', @@ -1314,7 +1339,7 @@ export const get14 = oc .output(zGetAppsByAppIdAnnotationSettingResponse) export const annotationSetting = { - get: get14, + get: get15, } /** @@ -1367,7 +1392,7 @@ export const batchImport = { /** * Get status of batch import job */ -export const get15 = oc +export const get16 = oc .route({ description: 'Get status of batch import job', inputStructure: 'detailed', @@ -1380,7 +1405,7 @@ export const get15 = oc .output(zGetAppsByAppIdAnnotationsBatchImportStatusByJobIdResponse) export const byJobId2 = { - get: get15, + get: get16, } export const batchImportStatus = { @@ -1390,7 +1415,7 @@ export const batchImportStatus = { /** * Get count of message annotations for the app */ -export const get16 = oc +export const get17 = oc .route({ description: 'Get count of message annotations for the app', inputStructure: 'detailed', @@ -1403,13 +1428,13 @@ export const get16 = oc .output(zGetAppsByAppIdAnnotationsCountResponse) export const count2 = { - get: get16, + get: get17, } /** * Export all annotations for an app with CSV injection protection */ -export const get17 = oc +export const get18 = oc .route({ description: 'Export all annotations for an app with CSV injection protection', inputStructure: 'detailed', @@ -1422,13 +1447,13 @@ export const get17 = oc .output(zGetAppsByAppIdAnnotationsExportResponse) export const export_ = { - get: get17, + get: get18, } /** * Get hit histories for an annotation */ -export const get18 = oc +export const get19 = oc .route({ description: 'Get hit histories for an annotation', inputStructure: 'detailed', @@ -1446,7 +1471,7 @@ export const get18 = oc .output(zGetAppsByAppIdAnnotationsByAnnotationIdHitHistoriesResponse) export const hitHistories = { - get: get18, + get: get19, } export const delete3 = oc @@ -1502,7 +1527,7 @@ export const delete4 = oc /** * Get annotations for an app with pagination */ -export const get19 = oc +export const get20 = oc .route({ description: 'Get annotations for an app with pagination', inputStructure: 'detailed', @@ -1539,7 +1564,7 @@ export const post20 = oc export const annotations = { delete: delete4, - get: get19, + get: get20, post: post20, batchImport, batchImportStatus, @@ -1605,7 +1630,7 @@ export const delete5 = oc /** * Get chat conversation details */ -export const get20 = oc +export const get21 = oc .route({ description: 'Get chat conversation details', inputStructure: 'detailed', @@ -1619,13 +1644,13 @@ export const get20 = oc export const byConversationId = { delete: delete5, - get: get20, + get: get21, } /** * Get chat conversations with pagination, filtering and summary */ -export const get21 = oc +export const get22 = oc .route({ description: 'Get chat conversations with pagination, filtering and summary', inputStructure: 'detailed', @@ -1643,14 +1668,14 @@ export const get21 = oc .output(zGetAppsByAppIdChatConversationsResponse) export const chatConversations = { - get: get21, + get: get22, byConversationId, } /** * Get suggested questions for a message */ -export const get22 = oc +export const get23 = oc .route({ description: 'Get suggested questions for a message', inputStructure: 'detailed', @@ -1663,7 +1688,7 @@ export const get22 = oc .output(zGetAppsByAppIdChatMessagesByMessageIdSuggestedQuestionsResponse) export const suggestedQuestions = { - get: get22, + get: get23, } export const byMessageId = { @@ -1696,7 +1721,7 @@ export const byTaskId = { /** * Get chat messages for a conversation with pagination */ -export const get23 = oc +export const get24 = oc .route({ description: 'Get chat messages for a conversation with pagination', inputStructure: 'detailed', @@ -1711,7 +1736,7 @@ export const get23 = oc .output(zGetAppsByAppIdChatMessagesResponse) export const chatMessages = { - get: get23, + get: get24, byMessageId, byTaskId, } @@ -1735,7 +1760,7 @@ export const delete6 = oc /** * Get completion conversation details with messages */ -export const get24 = oc +export const get25 = oc .route({ description: 'Get completion conversation details with messages', inputStructure: 'detailed', @@ -1749,13 +1774,13 @@ export const get24 = oc export const byConversationId2 = { delete: delete6, - get: get24, + get: get25, } /** * Get completion conversations with pagination and filtering */ -export const get25 = oc +export const get26 = oc .route({ description: 'Get completion conversations with pagination and filtering', inputStructure: 'detailed', @@ -1773,7 +1798,7 @@ export const get25 = oc .output(zGetAppsByAppIdCompletionConversationsResponse) export const completionConversations = { - get: get25, + get: get26, byConversationId: byConversationId2, } @@ -1828,7 +1853,7 @@ export const completionMessages = { /** * Get conversation variables for an application */ -export const get26 = oc +export const get27 = oc .route({ description: 'Get conversation variables for an application', inputStructure: 'detailed', @@ -1846,7 +1871,7 @@ export const get26 = oc .output(zGetAppsByAppIdConversationVariablesResponse) export const conversationVariables = { - get: get26, + get: get27, } /** @@ -1907,7 +1932,7 @@ export const copy = { * * Export application configuration as DSL */ -export const get27 = oc +export const get28 = oc .route({ description: 'Export application configuration as DSL', inputStructure: 'detailed', @@ -1923,13 +1948,13 @@ export const get27 = oc .output(zGetAppsByAppIdExportResponse) export const export2 = { - get: get27, + get: get28, } /** * Export user feedback data for Google Sheets */ -export const get28 = oc +export const get29 = oc .route({ description: 'Export user feedback data for Google Sheets', inputStructure: 'detailed', @@ -1947,7 +1972,7 @@ export const get28 = oc .output(zGetAppsByAppIdFeedbacksExportResponse) export const export3 = { - get: get28, + get: get29, } /** @@ -1992,7 +2017,7 @@ export const icon = { /** * Get message details by ID */ -export const get29 = oc +export const get30 = oc .route({ description: 'Get message details by ID', inputStructure: 'detailed', @@ -2005,7 +2030,7 @@ export const get29 = oc .output(zGetAppsByAppIdMessagesByMessageIdResponse) export const byMessageId2 = { - get: get29, + get: get30, } export const messages = { @@ -2077,7 +2102,7 @@ export const publishToCreatorsPlatform = { /** * Get MCP server configuration for an application */ -export const get30 = oc +export const get31 = oc .route({ description: 'Get MCP server configuration for an application', inputStructure: 'detailed', @@ -2121,7 +2146,7 @@ export const put2 = oc .output(zPutAppsByAppIdServerResponse) export const server = { - get: get30, + get: get31, post: post33, put: put2, } @@ -2184,10 +2209,45 @@ export const siteEnable = { post: post36, } +/** + * Remove the current account's star from an application + */ +export const delete7 = oc + .route({ + description: 'Remove the current account\'s star from an application', + inputStructure: 'detailed', + method: 'DELETE', + operationId: 'deleteAppsByAppIdStar', + path: '/apps/{app_id}/star', + tags: ['console'], + }) + .input(z.object({ params: zDeleteAppsByAppIdStarPath })) + .output(zDeleteAppsByAppIdStarResponse) + +/** + * Star an application for the current account + */ +export const post37 = oc + .route({ + description: 'Star an application for the current account', + inputStructure: 'detailed', + method: 'POST', + operationId: 'postAppsByAppIdStar', + path: '/apps/{app_id}/star', + tags: ['console'], + }) + .input(z.object({ params: zPostAppsByAppIdStarPath })) + .output(zPostAppsByAppIdStarResponse) + +export const star = { + delete: delete7, + post: post37, +} + /** * Get average response time statistics for an application */ -export const get31 = oc +export const get32 = oc .route({ description: 'Get average response time statistics for an application', inputStructure: 'detailed', @@ -2205,13 +2265,13 @@ export const get31 = oc .output(zGetAppsByAppIdStatisticsAverageResponseTimeResponse) export const averageResponseTime = { - get: get31, + get: get32, } /** * Get average session interaction statistics for an application */ -export const get32 = oc +export const get33 = oc .route({ description: 'Get average session interaction statistics for an application', inputStructure: 'detailed', @@ -2229,13 +2289,13 @@ export const get32 = oc .output(zGetAppsByAppIdStatisticsAverageSessionInteractionsResponse) export const averageSessionInteractions = { - get: get32, + get: get33, } /** * Get daily conversation statistics for an application */ -export const get33 = oc +export const get34 = oc .route({ description: 'Get daily conversation statistics for an application', inputStructure: 'detailed', @@ -2253,13 +2313,13 @@ export const get33 = oc .output(zGetAppsByAppIdStatisticsDailyConversationsResponse) export const dailyConversations = { - get: get33, + get: get34, } /** * Get daily terminal/end-user statistics for an application */ -export const get34 = oc +export const get35 = oc .route({ description: 'Get daily terminal/end-user statistics for an application', inputStructure: 'detailed', @@ -2277,13 +2337,13 @@ export const get34 = oc .output(zGetAppsByAppIdStatisticsDailyEndUsersResponse) export const dailyEndUsers = { - get: get34, + get: get35, } /** * Get daily message statistics for an application */ -export const get35 = oc +export const get36 = oc .route({ description: 'Get daily message statistics for an application', inputStructure: 'detailed', @@ -2301,13 +2361,13 @@ export const get35 = oc .output(zGetAppsByAppIdStatisticsDailyMessagesResponse) export const dailyMessages = { - get: get35, + get: get36, } /** * Get daily token cost statistics for an application */ -export const get36 = oc +export const get37 = oc .route({ description: 'Get daily token cost statistics for an application', inputStructure: 'detailed', @@ -2325,13 +2385,13 @@ export const get36 = oc .output(zGetAppsByAppIdStatisticsTokenCostsResponse) export const tokenCosts = { - get: get36, + get: get37, } /** * Get tokens per second statistics for an application */ -export const get37 = oc +export const get38 = oc .route({ description: 'Get tokens per second statistics for an application', inputStructure: 'detailed', @@ -2349,13 +2409,13 @@ export const get37 = oc .output(zGetAppsByAppIdStatisticsTokensPerSecondResponse) export const tokensPerSecond = { - get: get37, + get: get38, } /** * Get user satisfaction rate statistics for an application */ -export const get38 = oc +export const get39 = oc .route({ description: 'Get user satisfaction rate statistics for an application', inputStructure: 'detailed', @@ -2373,7 +2433,7 @@ export const get38 = oc .output(zGetAppsByAppIdStatisticsUserSatisfactionRateResponse) export const userSatisfactionRate = { - get: get38, + get: get39, } export const statistics = { @@ -2390,7 +2450,7 @@ export const statistics = { /** * Get available TTS voices for a specific language */ -export const get39 = oc +export const get40 = oc .route({ description: 'Get available TTS voices for a specific language', inputStructure: 'detailed', @@ -2408,13 +2468,13 @@ export const get39 = oc .output(zGetAppsByAppIdTextToAudioVoicesResponse) export const voices = { - get: get39, + get: get40, } /** * Convert text to speech for chat messages */ -export const post37 = oc +export const post38 = oc .route({ description: 'Convert text to speech for chat messages', inputStructure: 'detailed', @@ -2429,7 +2489,7 @@ export const post37 = oc .output(zPostAppsByAppIdTextToAudioResponse) export const textToAudio = { - post: post37, + post: post38, voices, } @@ -2438,7 +2498,7 @@ export const textToAudio = { * * Get app tracing configuration */ -export const get40 = oc +export const get41 = oc .route({ description: 'Get app tracing configuration', inputStructure: 'detailed', @@ -2454,7 +2514,7 @@ export const get40 = oc /** * Update app tracing configuration */ -export const post38 = oc +export const post39 = oc .route({ description: 'Update app tracing configuration', inputStructure: 'detailed', @@ -2467,8 +2527,8 @@ export const post38 = oc .output(zPostAppsByAppIdTraceResponse) export const trace = { - get: get40, - post: post38, + get: get41, + post: post39, } /** @@ -2476,7 +2536,7 @@ export const trace = { * * Delete an existing tracing configuration for an application */ -export const delete7 = oc +export const delete8 = oc .route({ description: 'Delete an existing tracing configuration for an application', inputStructure: 'detailed', @@ -2498,7 +2558,7 @@ export const delete7 = oc /** * Get tracing configuration for an application */ -export const get41 = oc +export const get42 = oc .route({ description: 'Get tracing configuration for an application', inputStructure: 'detailed', @@ -2537,7 +2597,7 @@ export const patch = oc * * Create a new tracing configuration for an application */ -export const post39 = oc +export const post40 = oc .route({ description: 'Create a new tracing configuration for an application', inputStructure: 'detailed', @@ -2554,16 +2614,16 @@ export const post39 = oc .output(zPostAppsByAppIdTraceConfigResponse) export const traceConfig = { - delete: delete7, - get: get41, + delete: delete8, + get: get42, patch, - post: post39, + post: post40, } /** * Update app trigger (enable/disable) */ -export const post40 = oc +export const post41 = oc .route({ inputStructure: 'detailed', method: 'POST', @@ -2581,13 +2641,13 @@ export const post40 = oc .output(zPostAppsByAppIdTriggerEnableResponse) export const triggerEnable = { - post: post40, + post: post41, } /** * Get app triggers list */ -export const get42 = oc +export const get43 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2600,7 +2660,7 @@ export const get42 = oc .output(zGetAppsByAppIdTriggersResponse) export const triggers = { - get: get42, + get: get43, } /** @@ -2608,7 +2668,7 @@ export const triggers = { * * Get workflow application execution logs */ -export const get43 = oc +export const get44 = oc .route({ description: 'Get workflow application execution logs', inputStructure: 'detailed', @@ -2627,7 +2687,7 @@ export const get43 = oc .output(zGetAppsByAppIdWorkflowAppLogsResponse) export const workflowAppLogs = { - get: get43, + get: get44, } /** @@ -2635,7 +2695,7 @@ export const workflowAppLogs = { * * Get workflow archived execution logs */ -export const get44 = oc +export const get45 = oc .route({ description: 'Get workflow archived execution logs', inputStructure: 'detailed', @@ -2654,7 +2714,7 @@ export const get44 = oc .output(zGetAppsByAppIdWorkflowArchivedLogsResponse) export const workflowArchivedLogs = { - get: get44, + get: get45, } /** @@ -2662,7 +2722,7 @@ export const workflowArchivedLogs = { * * Get workflow runs count statistics */ -export const get45 = oc +export const get46 = oc .route({ description: 'Get workflow runs count statistics', inputStructure: 'detailed', @@ -2681,7 +2741,7 @@ export const get45 = oc .output(zGetAppsByAppIdWorkflowRunsCountResponse) export const count3 = { - get: get45, + get: get46, } /** @@ -2689,7 +2749,7 @@ export const count3 = { * * Stop running workflow task */ -export const post41 = oc +export const post42 = oc .route({ description: 'Stop running workflow task', inputStructure: 'detailed', @@ -2703,7 +2763,7 @@ export const post41 = oc .output(zPostAppsByAppIdWorkflowRunsTasksByTaskIdStopResponse) export const stop3 = { - post: post41, + post: post42, } export const byTaskId3 = { @@ -2717,7 +2777,7 @@ export const tasks = { /** * Generate a download URL for an archived workflow run. */ -export const get46 = oc +export const get47 = oc .route({ description: 'Generate a download URL for an archived workflow run.', inputStructure: 'detailed', @@ -2730,7 +2790,7 @@ export const get46 = oc .output(zGetAppsByAppIdWorkflowRunsByRunIdExportResponse) export const export4 = { - get: get46, + get: get47, } /** @@ -2738,7 +2798,7 @@ export const export4 = { * * Get workflow run node execution list */ -export const get47 = oc +export const get48 = oc .route({ description: 'Get workflow run node execution list', inputStructure: 'detailed', @@ -2752,7 +2812,7 @@ export const get47 = oc .output(zGetAppsByAppIdWorkflowRunsByRunIdNodeExecutionsResponse) export const nodeExecutions = { - get: get47, + get: get48, } /** @@ -2760,7 +2820,7 @@ export const nodeExecutions = { * * Get workflow run detail */ -export const get48 = oc +export const get49 = oc .route({ description: 'Get workflow run detail', inputStructure: 'detailed', @@ -2774,7 +2834,7 @@ export const get48 = oc .output(zGetAppsByAppIdWorkflowRunsByRunIdResponse) export const byRunId = { - get: get48, + get: get49, export: export4, nodeExecutions, } @@ -2782,7 +2842,7 @@ export const byRunId = { /** * Read a text/binary preview file in a workflow Agent node sandbox */ -export const get49 = oc +export const get50 = oc .route({ description: 'Read a text/binary preview file in a workflow Agent node sandbox', inputStructure: 'detailed', @@ -2800,13 +2860,13 @@ export const get49 = oc .output(zGetAppsByAppIdWorkflowRunsByWorkflowRunIdAgentNodesByNodeIdSandboxFilesReadResponse) export const read2 = { - get: get49, + get: get50, } /** * Upload one workflow Agent sandbox file as a Dify ToolFile mapping */ -export const post42 = oc +export const post43 = oc .route({ description: 'Upload one workflow Agent sandbox file as a Dify ToolFile mapping', inputStructure: 'detailed', @@ -2824,13 +2884,13 @@ export const post42 = oc .output(zPostAppsByAppIdWorkflowRunsByWorkflowRunIdAgentNodesByNodeIdSandboxFilesUploadResponse) export const upload3 = { - post: post42, + post: post43, } /** * List a directory in a workflow Agent node sandbox */ -export const get50 = oc +export const get51 = oc .route({ description: 'List a directory in a workflow Agent node sandbox', inputStructure: 'detailed', @@ -2849,7 +2909,7 @@ export const get50 = oc .output(zGetAppsByAppIdWorkflowRunsByWorkflowRunIdAgentNodesByNodeIdSandboxFilesResponse) export const files4 = { - get: get50, + get: get51, read: read2, upload: upload3, } @@ -2875,7 +2935,7 @@ export const byWorkflowRunId = { * * Get workflow run list */ -export const get51 = oc +export const get52 = oc .route({ description: 'Get workflow run list', inputStructure: 'detailed', @@ -2894,7 +2954,7 @@ export const get51 = oc .output(zGetAppsByAppIdWorkflowRunsResponse) export const workflowRuns2 = { - get: get51, + get: get52, count: count3, tasks, byRunId, @@ -2906,7 +2966,7 @@ export const workflowRuns2 = { * * Get all users in current tenant for mentions */ -export const get52 = oc +export const get53 = oc .route({ description: 'Get all users in current tenant for mentions', inputStructure: 'detailed', @@ -2920,7 +2980,7 @@ export const get52 = oc .output(zGetAppsByAppIdWorkflowCommentsMentionUsersResponse) export const mentionUsers = { - get: get52, + get: get53, } /** @@ -2928,7 +2988,7 @@ export const mentionUsers = { * * Delete a comment reply */ -export const delete8 = oc +export const delete9 = oc .route({ description: 'Delete a comment reply', inputStructure: 'detailed', @@ -2966,7 +3026,7 @@ export const put3 = oc .output(zPutAppsByAppIdWorkflowCommentsByCommentIdRepliesByReplyIdResponse) export const byReplyId = { - delete: delete8, + delete: delete9, put: put3, } @@ -2975,7 +3035,7 @@ export const byReplyId = { * * Add a reply to a workflow comment */ -export const post43 = oc +export const post44 = oc .route({ description: 'Add a reply to a workflow comment', inputStructure: 'detailed', @@ -2995,7 +3055,7 @@ export const post43 = oc .output(zPostAppsByAppIdWorkflowCommentsByCommentIdRepliesResponse) export const replies = { - post: post43, + post: post44, byReplyId, } @@ -3004,7 +3064,7 @@ export const replies = { * * Resolve a workflow comment */ -export const post44 = oc +export const post45 = oc .route({ description: 'Resolve a workflow comment', inputStructure: 'detailed', @@ -3018,7 +3078,7 @@ export const post44 = oc .output(zPostAppsByAppIdWorkflowCommentsByCommentIdResolveResponse) export const resolve = { - post: post44, + post: post45, } /** @@ -3026,7 +3086,7 @@ export const resolve = { * * Delete a workflow comment */ -export const delete9 = oc +export const delete10 = oc .route({ description: 'Delete a workflow comment', inputStructure: 'detailed', @@ -3045,7 +3105,7 @@ export const delete9 = oc * * Get a specific workflow comment */ -export const get53 = oc +export const get54 = oc .route({ description: 'Get a specific workflow comment', inputStructure: 'detailed', @@ -3082,8 +3142,8 @@ export const put4 = oc .output(zPutAppsByAppIdWorkflowCommentsByCommentIdResponse) export const byCommentId = { - delete: delete9, - get: get53, + delete: delete10, + get: get54, put: put4, replies, resolve, @@ -3094,7 +3154,7 @@ export const byCommentId = { * * Get all comments for a workflow */ -export const get54 = oc +export const get55 = oc .route({ description: 'Get all comments for a workflow', inputStructure: 'detailed', @@ -3112,7 +3172,7 @@ export const get54 = oc * * Create a new workflow comment */ -export const post45 = oc +export const post46 = oc .route({ description: 'Create a new workflow comment', inputStructure: 'detailed', @@ -3132,8 +3192,8 @@ export const post45 = oc .output(zPostAppsByAppIdWorkflowCommentsResponse) export const comments = { - get: get54, - post: post45, + get: get55, + post: post46, mentionUsers, byCommentId, } @@ -3141,7 +3201,7 @@ export const comments = { /** * Get workflow average app interaction statistics */ -export const get55 = oc +export const get56 = oc .route({ description: 'Get workflow average app interaction statistics', inputStructure: 'detailed', @@ -3159,13 +3219,13 @@ export const get55 = oc .output(zGetAppsByAppIdWorkflowStatisticsAverageAppInteractionsResponse) export const averageAppInteractions = { - get: get55, + get: get56, } /** * Get workflow daily runs statistics */ -export const get56 = oc +export const get57 = oc .route({ description: 'Get workflow daily runs statistics', inputStructure: 'detailed', @@ -3183,13 +3243,13 @@ export const get56 = oc .output(zGetAppsByAppIdWorkflowStatisticsDailyConversationsResponse) export const dailyConversations2 = { - get: get56, + get: get57, } /** * Get workflow daily terminals statistics */ -export const get57 = oc +export const get58 = oc .route({ description: 'Get workflow daily terminals statistics', inputStructure: 'detailed', @@ -3207,13 +3267,13 @@ export const get57 = oc .output(zGetAppsByAppIdWorkflowStatisticsDailyTerminalsResponse) export const dailyTerminals = { - get: get57, + get: get58, } /** * Get workflow daily token cost statistics */ -export const get58 = oc +export const get59 = oc .route({ description: 'Get workflow daily token cost statistics', inputStructure: 'detailed', @@ -3231,7 +3291,7 @@ export const get58 = oc .output(zGetAppsByAppIdWorkflowStatisticsTokenCostsResponse) export const tokenCosts2 = { - get: get58, + get: get59, } export const statistics2 = { @@ -3251,7 +3311,7 @@ export const workflow = { * * Get default block configuration by type */ -export const get59 = oc +export const get60 = oc .route({ description: 'Get default block configuration by type', inputStructure: 'detailed', @@ -3270,7 +3330,7 @@ export const get59 = oc .output(zGetAppsByAppIdWorkflowsDefaultWorkflowBlockConfigsByBlockTypeResponse) export const byBlockType = { - get: get59, + get: get60, } /** @@ -3278,7 +3338,7 @@ export const byBlockType = { * * Get default block configurations for workflow */ -export const get60 = oc +export const get61 = oc .route({ description: 'Get default block configurations for workflow', inputStructure: 'detailed', @@ -3292,14 +3352,14 @@ export const get60 = oc .output(zGetAppsByAppIdWorkflowsDefaultWorkflowBlockConfigsResponse) export const defaultWorkflowBlockConfigs = { - get: get60, + get: get61, byBlockType, } /** * Get conversation variables for workflow */ -export const get61 = oc +export const get62 = oc .route({ description: 'Get conversation variables for workflow', inputStructure: 'detailed', @@ -3314,7 +3374,7 @@ export const get61 = oc /** * Update conversation variables for workflow draft */ -export const post46 = oc +export const post47 = oc .route({ description: 'Update conversation variables for workflow draft', inputStructure: 'detailed', @@ -3332,8 +3392,8 @@ export const post46 = oc .output(zPostAppsByAppIdWorkflowsDraftConversationVariablesResponse) export const conversationVariables2 = { - get: get61, - post: post46, + get: get62, + post: post47, } /** @@ -3341,7 +3401,7 @@ export const conversationVariables2 = { * * Get environment variables for workflow */ -export const get62 = oc +export const get63 = oc .route({ description: 'Get environment variables for workflow', inputStructure: 'detailed', @@ -3357,7 +3417,7 @@ export const get62 = oc /** * Update environment variables for workflow draft */ -export const post47 = oc +export const post48 = oc .route({ description: 'Update environment variables for workflow draft', inputStructure: 'detailed', @@ -3375,14 +3435,14 @@ export const post47 = oc .output(zPostAppsByAppIdWorkflowsDraftEnvironmentVariablesResponse) export const environmentVariables = { - get: get62, - post: post47, + get: get63, + post: post48, } /** * Update draft workflow features */ -export const post48 = oc +export const post49 = oc .route({ description: 'Update draft workflow features', inputStructure: 'detailed', @@ -3400,7 +3460,7 @@ export const post48 = oc .output(zPostAppsByAppIdWorkflowsDraftFeaturesResponse) export const features = { - post: post48, + post: post49, } /** @@ -3408,7 +3468,7 @@ export const features = { * * Test human input delivery for workflow */ -export const post49 = oc +export const post50 = oc .route({ description: 'Test human input delivery for workflow', inputStructure: 'detailed', @@ -3427,7 +3487,7 @@ export const post49 = oc .output(zPostAppsByAppIdWorkflowsDraftHumanInputNodesByNodeIdDeliveryTestResponse) export const deliveryTest = { - post: post49, + post: post50, } /** @@ -3435,7 +3495,7 @@ export const deliveryTest = { * * Get human input form preview for workflow */ -export const post50 = oc +export const post51 = oc .route({ description: 'Get human input form preview for workflow', inputStructure: 'detailed', @@ -3454,7 +3514,7 @@ export const post50 = oc .output(zPostAppsByAppIdWorkflowsDraftHumanInputNodesByNodeIdFormPreviewResponse) export const preview3 = { - post: post50, + post: post51, } /** @@ -3462,7 +3522,7 @@ export const preview3 = { * * Submit human input form preview for workflow */ -export const post51 = oc +export const post52 = oc .route({ description: 'Submit human input form preview for workflow', inputStructure: 'detailed', @@ -3481,7 +3541,7 @@ export const post51 = oc .output(zPostAppsByAppIdWorkflowsDraftHumanInputNodesByNodeIdFormRunResponse) export const run5 = { - post: post51, + post: post52, } export const form2 = { @@ -3507,7 +3567,7 @@ export const humanInput2 = { * * Run draft workflow iteration node */ -export const post52 = oc +export const post53 = oc .route({ description: 'Run draft workflow iteration node', inputStructure: 'detailed', @@ -3526,7 +3586,7 @@ export const post52 = oc .output(zPostAppsByAppIdWorkflowsDraftIterationNodesByNodeIdRunResponse) export const run6 = { - post: post52, + post: post53, } export const byNodeId6 = { @@ -3546,7 +3606,7 @@ export const iteration2 = { * * Run draft workflow loop node */ -export const post53 = oc +export const post54 = oc .route({ description: 'Run draft workflow loop node', inputStructure: 'detailed', @@ -3565,7 +3625,7 @@ export const post53 = oc .output(zPostAppsByAppIdWorkflowsDraftLoopNodesByNodeIdRunResponse) export const run7 = { - post: post53, + post: post54, } export const byNodeId7 = { @@ -3580,7 +3640,7 @@ export const loop2 = { nodes: nodes6, } -export const get63 = oc +export const get64 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3594,10 +3654,10 @@ export const get63 = oc .output(zGetAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerCandidatesResponse) export const candidates2 = { - get: get63, + get: get64, } -export const post54 = oc +export const post55 = oc .route({ inputStructure: 'detailed', method: 'POST', @@ -3614,10 +3674,10 @@ export const post54 = oc .output(zPostAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerImpactResponse) export const impact = { - post: post54, + post: post55, } -export const post55 = oc +export const post56 = oc .route({ inputStructure: 'detailed', method: 'POST', @@ -3634,10 +3694,10 @@ export const post55 = oc .output(zPostAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerSaveToRosterResponse) export const saveToRoster = { - post: post55, + post: post56, } -export const post56 = oc +export const post57 = oc .route({ inputStructure: 'detailed', method: 'POST', @@ -3654,10 +3714,10 @@ export const post56 = oc .output(zPostAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerValidateResponse) export const validate2 = { - post: post56, + post: post57, } -export const get64 = oc +export const get65 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3685,7 +3745,7 @@ export const put5 = oc .output(zPutAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerResponse) export const agentComposer2 = { - get: get64, + get: get65, put: put5, candidates: candidates2, impact, @@ -3696,7 +3756,7 @@ export const agentComposer2 = { /** * Get last run result for draft workflow node */ -export const get65 = oc +export const get66 = oc .route({ description: 'Get last run result for draft workflow node', inputStructure: 'detailed', @@ -3709,7 +3769,7 @@ export const get65 = oc .output(zGetAppsByAppIdWorkflowsDraftNodesByNodeIdLastRunResponse) export const lastRun = { - get: get65, + get: get66, } /** @@ -3717,7 +3777,7 @@ export const lastRun = { * * Run draft workflow node */ -export const post57 = oc +export const post58 = oc .route({ description: 'Run draft workflow node', inputStructure: 'detailed', @@ -3736,7 +3796,7 @@ export const post57 = oc .output(zPostAppsByAppIdWorkflowsDraftNodesByNodeIdRunResponse) export const run8 = { - post: post57, + post: post58, } /** @@ -3744,7 +3804,7 @@ export const run8 = { * * Poll for trigger events and execute single node when event arrives */ -export const post58 = oc +export const post59 = oc .route({ description: 'Poll for trigger events and execute single node when event arrives', inputStructure: 'detailed', @@ -3758,7 +3818,7 @@ export const post58 = oc .output(zPostAppsByAppIdWorkflowsDraftNodesByNodeIdTriggerRunResponse) export const run9 = { - post: post58, + post: post59, } export const trigger = { @@ -3768,7 +3828,7 @@ export const trigger = { /** * Delete all variables for a specific node */ -export const delete10 = oc +export const delete11 = oc .route({ description: 'Delete all variables for a specific node', inputStructure: 'detailed', @@ -3784,7 +3844,7 @@ export const delete10 = oc /** * Get variables for a specific node */ -export const get66 = oc +export const get67 = oc .route({ description: 'Get variables for a specific node', inputStructure: 'detailed', @@ -3797,8 +3857,8 @@ export const get66 = oc .output(zGetAppsByAppIdWorkflowsDraftNodesByNodeIdVariablesResponse) export const variables = { - delete: delete10, - get: get66, + delete: delete11, + get: get67, } export const byNodeId8 = { @@ -3818,7 +3878,7 @@ export const nodes7 = { * * Run draft workflow */ -export const post59 = oc +export const post60 = oc .route({ description: 'Run draft workflow', inputStructure: 'detailed', @@ -3837,13 +3897,13 @@ export const post59 = oc .output(zPostAppsByAppIdWorkflowsDraftRunResponse) export const run10 = { - post: post59, + post: post60, } /** * Server-Sent Events stream of inspector deltas for a draft workflow run. */ -export const get67 = oc +export const get68 = oc .route({ description: 'Server-Sent Events stream of inspector deltas for a draft workflow run.', inputStructure: 'detailed', @@ -3856,13 +3916,13 @@ export const get67 = oc .output(zGetAppsByAppIdWorkflowsDraftRunsByRunIdNodeOutputsEventsResponse) export const events = { - get: get67, + get: get68, } /** * Full value for one declared output, including signed download URL for files. */ -export const get68 = oc +export const get69 = oc .route({ description: 'Full value for one declared output, including signed download URL for files.', inputStructure: 'detailed', @@ -3879,7 +3939,7 @@ export const get68 = oc .output(zGetAppsByAppIdWorkflowsDraftRunsByRunIdNodeOutputsByNodeIdByOutputNamePreviewResponse) export const preview4 = { - get: get68, + get: get69, } export const byOutputName = { @@ -3889,7 +3949,7 @@ export const byOutputName = { /** * One node's declared outputs for a draft workflow run. */ -export const get69 = oc +export const get70 = oc .route({ description: 'One node\'s declared outputs for a draft workflow run.', inputStructure: 'detailed', @@ -3902,14 +3962,14 @@ export const get69 = oc .output(zGetAppsByAppIdWorkflowsDraftRunsByRunIdNodeOutputsByNodeIdResponse) export const byNodeId9 = { - get: get69, + get: get70, byOutputName, } /** * Snapshot of every node's declared outputs for a draft workflow run. */ -export const get70 = oc +export const get71 = oc .route({ description: 'Snapshot of every node\'s declared outputs for a draft workflow run.', inputStructure: 'detailed', @@ -3922,7 +3982,7 @@ export const get70 = oc .output(zGetAppsByAppIdWorkflowsDraftRunsByRunIdNodeOutputsResponse) export const nodeOutputs = { - get: get70, + get: get71, events, byNodeId: byNodeId9, } @@ -3938,7 +3998,7 @@ export const runs = { /** * Get system variables for workflow */ -export const get71 = oc +export const get72 = oc .route({ description: 'Get system variables for workflow', inputStructure: 'detailed', @@ -3951,7 +4011,7 @@ export const get71 = oc .output(zGetAppsByAppIdWorkflowsDraftSystemVariablesResponse) export const systemVariables = { - get: get71, + get: get72, } /** @@ -3959,7 +4019,7 @@ export const systemVariables = { * * Poll for trigger events and execute full workflow when event arrives */ -export const post60 = oc +export const post61 = oc .route({ description: 'Poll for trigger events and execute full workflow when event arrives', inputStructure: 'detailed', @@ -3978,7 +4038,7 @@ export const post60 = oc .output(zPostAppsByAppIdWorkflowsDraftTriggerRunResponse) export const run11 = { - post: post60, + post: post61, } /** @@ -3986,7 +4046,7 @@ export const run11 = { * * Full workflow debug when the start node is a trigger */ -export const post61 = oc +export const post62 = oc .route({ description: 'Full workflow debug when the start node is a trigger', inputStructure: 'detailed', @@ -4005,7 +4065,7 @@ export const post61 = oc .output(zPostAppsByAppIdWorkflowsDraftTriggerRunAllResponse) export const runAll = { - post: post61, + post: post62, } export const trigger2 = { @@ -4035,7 +4095,7 @@ export const reset = { /** * Delete a workflow variable */ -export const delete11 = oc +export const delete12 = oc .route({ description: 'Delete a workflow variable', inputStructure: 'detailed', @@ -4051,7 +4111,7 @@ export const delete11 = oc /** * Get a specific workflow variable */ -export const get72 = oc +export const get73 = oc .route({ description: 'Get a specific workflow variable', inputStructure: 'detailed', @@ -4084,8 +4144,8 @@ export const patch2 = oc .output(zPatchAppsByAppIdWorkflowsDraftVariablesByVariableIdResponse) export const byVariableId = { - delete: delete11, - get: get72, + delete: delete12, + get: get73, patch: patch2, reset, } @@ -4093,7 +4153,7 @@ export const byVariableId = { /** * Delete all draft workflow variables */ -export const delete12 = oc +export const delete13 = oc .route({ description: 'Delete all draft workflow variables', inputStructure: 'detailed', @@ -4111,7 +4171,7 @@ export const delete12 = oc * * Get draft workflow variables */ -export const get73 = oc +export const get74 = oc .route({ description: 'Get draft workflow variables', inputStructure: 'detailed', @@ -4130,8 +4190,8 @@ export const get73 = oc .output(zGetAppsByAppIdWorkflowsDraftVariablesResponse) export const variables2 = { - delete: delete12, - get: get73, + delete: delete13, + get: get74, byVariableId, } @@ -4140,7 +4200,7 @@ export const variables2 = { * * Get draft workflow for an application */ -export const get74 = oc +export const get75 = oc .route({ description: 'Get draft workflow for an application', inputStructure: 'detailed', @@ -4158,7 +4218,7 @@ export const get74 = oc * * Sync draft workflow configuration */ -export const post62 = oc +export const post63 = oc .route({ description: 'Sync draft workflow configuration', inputStructure: 'detailed', @@ -4177,8 +4237,8 @@ export const post62 = oc .output(zPostAppsByAppIdWorkflowsDraftResponse) export const draft2 = { - get: get74, - post: post62, + get: get75, + post: post63, conversationVariables: conversationVariables2, environmentVariables, features, @@ -4198,7 +4258,7 @@ export const draft2 = { * * Get published workflow for an application */ -export const get75 = oc +export const get76 = oc .route({ description: 'Get published workflow for an application', inputStructure: 'detailed', @@ -4214,7 +4274,7 @@ export const get75 = oc /** * Publish workflow */ -export const post63 = oc +export const post64 = oc .route({ inputStructure: 'detailed', method: 'POST', @@ -4232,14 +4292,14 @@ export const post63 = oc .output(zPostAppsByAppIdWorkflowsPublishResponse) export const publish = { - get: get75, - post: post63, + get: get76, + post: post64, } /** * Server-Sent Events stream of inspector deltas for a published workflow run. */ -export const get76 = oc +export const get77 = oc .route({ description: 'Server-Sent Events stream of inspector deltas for a published workflow run.', inputStructure: 'detailed', @@ -4252,13 +4312,13 @@ export const get76 = oc .output(zGetAppsByAppIdWorkflowsPublishedRunsByRunIdNodeOutputsEventsResponse) export const events2 = { - get: get76, + get: get77, } /** * Full value for one declared output of a published run. */ -export const get77 = oc +export const get78 = oc .route({ description: 'Full value for one declared output of a published run.', inputStructure: 'detailed', @@ -4279,7 +4339,7 @@ export const get77 = oc ) export const preview5 = { - get: get77, + get: get78, } export const byOutputName2 = { @@ -4289,7 +4349,7 @@ export const byOutputName2 = { /** * One node's declared outputs for a published workflow run. */ -export const get78 = oc +export const get79 = oc .route({ description: 'One node\'s declared outputs for a published workflow run.', inputStructure: 'detailed', @@ -4302,14 +4362,14 @@ export const get78 = oc .output(zGetAppsByAppIdWorkflowsPublishedRunsByRunIdNodeOutputsByNodeIdResponse) export const byNodeId10 = { - get: get78, + get: get79, byOutputName: byOutputName2, } /** * Snapshot of every node's declared outputs for a published workflow run. */ -export const get79 = oc +export const get80 = oc .route({ description: 'Snapshot of every node\'s declared outputs for a published workflow run.', inputStructure: 'detailed', @@ -4322,7 +4382,7 @@ export const get79 = oc .output(zGetAppsByAppIdWorkflowsPublishedRunsByRunIdNodeOutputsResponse) export const nodeOutputs2 = { - get: get79, + get: get80, events: events2, byNodeId: byNodeId10, } @@ -4342,7 +4402,7 @@ export const published = { /** * Get webhook trigger for a node */ -export const get80 = oc +export const get81 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -4360,7 +4420,7 @@ export const get80 = oc .output(zGetAppsByAppIdWorkflowsTriggersWebhookResponse) export const webhook = { - get: get80, + get: get81, } export const triggers2 = { @@ -4370,7 +4430,7 @@ export const triggers2 = { /** * Restore a published workflow version into the draft workflow */ -export const post64 = oc +export const post65 = oc .route({ description: 'Restore a published workflow version into the draft workflow', inputStructure: 'detailed', @@ -4383,13 +4443,13 @@ export const post64 = oc .output(zPostAppsByAppIdWorkflowsByWorkflowIdRestoreResponse) export const restore = { - post: post64, + post: post65, } /** * Delete workflow */ -export const delete13 = oc +export const delete14 = oc .route({ inputStructure: 'detailed', method: 'DELETE', @@ -4426,7 +4486,7 @@ export const patch3 = oc .output(zPatchAppsByAppIdWorkflowsByWorkflowIdResponse) export const byWorkflowId = { - delete: delete13, + delete: delete14, patch: patch3, restore, } @@ -4436,7 +4496,7 @@ export const byWorkflowId = { * * Get all published workflows for an application */ -export const get81 = oc +export const get82 = oc .route({ description: 'Get all published workflows for an application', inputStructure: 'detailed', @@ -4455,7 +4515,7 @@ export const get81 = oc .output(zGetAppsByAppIdWorkflowsResponse) export const workflows3 = { - get: get81, + get: get82, defaultWorkflowBlockConfigs, draft: draft2, publish, @@ -4469,7 +4529,7 @@ export const workflows3 = { * * Delete application */ -export const delete14 = oc +export const delete15 = oc .route({ description: 'Delete application', inputStructure: 'detailed', @@ -4488,7 +4548,7 @@ export const delete14 = oc * * Get application details */ -export const get82 = oc +export const get83 = oc .route({ description: 'Get application details', inputStructure: 'detailed', @@ -4520,8 +4580,8 @@ export const put7 = oc .output(zPutAppsByAppIdResponse) export const byAppId2 = { - delete: delete14, - get: get82, + delete: delete15, + get: get83, put: put7, advancedChat, agentComposer, @@ -4552,6 +4612,7 @@ export const byAppId2 = { server, site, siteEnable, + star, statistics, textToAudio, trace, @@ -4570,7 +4631,7 @@ export const byAppId2 = { * * Delete an API key for an app */ -export const delete15 = oc +export const delete16 = oc .route({ description: 'Delete an API key for an app', inputStructure: 'detailed', @@ -4585,7 +4646,7 @@ export const delete15 = oc .output(zDeleteAppsByResourceIdApiKeysByApiKeyIdResponse) export const byApiKeyId = { - delete: delete15, + delete: delete16, } /** @@ -4593,7 +4654,7 @@ export const byApiKeyId = { * * Get all API keys for an app */ -export const get83 = oc +export const get84 = oc .route({ description: 'Get all API keys for an app', inputStructure: 'detailed', @@ -4611,7 +4672,7 @@ export const get83 = oc * * Create a new API key for an app */ -export const post65 = oc +export const post66 = oc .route({ description: 'Create a new API key for an app', inputStructure: 'detailed', @@ -4626,8 +4687,8 @@ export const post65 = oc .output(zPostAppsByResourceIdApiKeysResponse) export const apiKeys = { - get: get83, - post: post65, + get: get84, + post: post66, byApiKeyId, } @@ -4638,7 +4699,7 @@ export const byResourceId = { /** * Refresh MCP server configuration and regenerate server code */ -export const get84 = oc +export const get85 = oc .route({ description: 'Refresh MCP server configuration and regenerate server code', inputStructure: 'detailed', @@ -4651,7 +4712,7 @@ export const get84 = oc .output(zGetAppsByServerIdServerRefreshResponse) export const refresh = { - get: get84, + get: get85, } export const server2 = { @@ -4667,7 +4728,7 @@ export const byServerId = { * * Get list of applications with pagination and filtering */ -export const get85 = oc +export const get86 = oc .route({ description: 'Get list of applications with pagination and filtering', inputStructure: 'detailed', @@ -4685,7 +4746,7 @@ export const get85 = oc * * Create a new application */ -export const post66 = oc +export const post67 = oc .route({ description: 'Create a new application', inputStructure: 'detailed', @@ -4700,9 +4761,10 @@ export const post66 = oc .output(zPostAppsResponse) export const apps = { - get: get85, - post: post66, + get: get86, + post: post67, imports, + starred, workflows, byAppId: byAppId2, byResourceId, diff --git a/packages/contracts/generated/api/console/apps/types.gen.ts b/packages/contracts/generated/api/console/apps/types.gen.ts index 5ba8675ee3..4265c4843f 100644 --- a/packages/contracts/generated/api/console/apps/types.gen.ts +++ b/packages/contracts/generated/api/console/apps/types.gen.ts @@ -1191,6 +1191,7 @@ export type AppPartial = { icon_background?: string | null icon_type?: string | null id: string + is_starred?: boolean max_active_requests?: number | null mode_compatible_with_agent: string name: string @@ -2683,6 +2684,7 @@ export type GetAppsData = { | 'workflow' name?: string page?: number + sort_by?: 'earliest_created' | 'last_modified' | 'recently_created' tag_ids?: Array } url: '/apps' @@ -2771,6 +2773,36 @@ export type PostAppsImportsByImportIdConfirmResponses = { export type PostAppsImportsByImportIdConfirmResponse = PostAppsImportsByImportIdConfirmResponses[keyof PostAppsImportsByImportIdConfirmResponses] +export type GetAppsStarredData = { + body?: never + path?: never + query?: { + creator_ids?: Array + is_created_by_me?: boolean + limit?: number + mode?: + | 'advanced-chat' + | 'agent' + | 'agent-chat' + | 'all' + | 'channel' + | 'chat' + | 'completion' + | 'workflow' + name?: string + page?: number + sort_by?: 'earliest_created' | 'last_modified' | 'recently_created' + tag_ids?: Array + } + url: '/apps/starred' +} + +export type GetAppsStarredResponses = { + 200: AppPagination +} + +export type GetAppsStarredResponse = GetAppsStarredResponses[keyof GetAppsStarredResponses] + export type PostAppsWorkflowsOnlineUsersData = { body: WorkflowOnlineUsersPayload path?: never @@ -4250,6 +4282,46 @@ export type PostAppsByAppIdSiteAccessTokenResetResponses = { export type PostAppsByAppIdSiteAccessTokenResetResponse = PostAppsByAppIdSiteAccessTokenResetResponses[keyof PostAppsByAppIdSiteAccessTokenResetResponses] +export type DeleteAppsByAppIdStarData = { + body?: never + path: { + app_id: string + } + query?: never + url: '/apps/{app_id}/star' +} + +export type DeleteAppsByAppIdStarErrors = { + 404: unknown +} + +export type DeleteAppsByAppIdStarResponses = { + 200: SimpleResultResponse +} + +export type DeleteAppsByAppIdStarResponse + = DeleteAppsByAppIdStarResponses[keyof DeleteAppsByAppIdStarResponses] + +export type PostAppsByAppIdStarData = { + body?: never + path: { + app_id: string + } + query?: never + url: '/apps/{app_id}/star' +} + +export type PostAppsByAppIdStarErrors = { + 404: unknown +} + +export type PostAppsByAppIdStarResponses = { + 200: SimpleResultResponse +} + +export type PostAppsByAppIdStarResponse + = PostAppsByAppIdStarResponses[keyof PostAppsByAppIdStarResponses] + export type GetAppsByAppIdStatisticsAverageResponseTimeData = { body?: never path: { diff --git a/packages/contracts/generated/api/console/apps/zod.gen.ts b/packages/contracts/generated/api/console/apps/zod.gen.ts index 397fcd5cce..ebc87ce2c7 100644 --- a/packages/contracts/generated/api/console/apps/zod.gen.ts +++ b/packages/contracts/generated/api/console/apps/zod.gen.ts @@ -2000,6 +2000,7 @@ export const zAppPartial = z.object({ icon_background: z.string().nullish(), icon_type: z.string().nullish(), id: z.string(), + is_starred: z.boolean().optional().default(false), max_active_requests: z.int().nullish(), mode_compatible_with_agent: z.string(), name: z.string(), @@ -3625,6 +3626,10 @@ export const zGetAppsQuery = z.object({ .default('all'), name: z.string().optional(), page: z.int().gte(1).lte(99999).optional().default(1), + sort_by: z + .enum(['earliest_created', 'last_modified', 'recently_created']) + .optional() + .default('last_modified'), tag_ids: z.array(z.string()).optional(), }) @@ -3665,6 +3670,37 @@ export const zPostAppsImportsByImportIdConfirmPath = z.object({ */ export const zPostAppsImportsByImportIdConfirmResponse = zImport +export const zGetAppsStarredQuery = z.object({ + creator_ids: z.array(z.string()).optional(), + is_created_by_me: z.boolean().optional(), + limit: z.int().gte(1).lte(100).optional().default(20), + mode: z + .enum([ + 'advanced-chat', + 'agent', + 'agent-chat', + 'all', + 'channel', + 'chat', + 'completion', + 'workflow', + ]) + .optional() + .default('all'), + name: z.string().optional(), + page: z.int().gte(1).lte(99999).optional().default(1), + sort_by: z + .enum(['earliest_created', 'last_modified', 'recently_created']) + .optional() + .default('last_modified'), + tag_ids: z.array(z.string()).optional(), +}) + +/** + * Success + */ +export const zGetAppsStarredResponse = zAppPagination + export const zPostAppsWorkflowsOnlineUsersBody = zWorkflowOnlineUsersPayload /** @@ -4541,6 +4577,24 @@ export const zPostAppsByAppIdSiteAccessTokenResetPath = z.object({ */ export const zPostAppsByAppIdSiteAccessTokenResetResponse = zAppSiteResponse +export const zDeleteAppsByAppIdStarPath = z.object({ + app_id: z.string(), +}) + +/** + * Success + */ +export const zDeleteAppsByAppIdStarResponse = zSimpleResultResponse + +export const zPostAppsByAppIdStarPath = z.object({ + app_id: z.string(), +}) + +/** + * Success + */ +export const zPostAppsByAppIdStarResponse = zSimpleResultResponse + export const zGetAppsByAppIdStatisticsAverageResponseTimePath = z.object({ app_id: z.string(), }) diff --git a/packages/contracts/generated/api/console/explore/orpc.gen.ts b/packages/contracts/generated/api/console/explore/orpc.gen.ts index db163ae92a..23a7ef8bc4 100644 --- a/packages/contracts/generated/api/console/explore/orpc.gen.ts +++ b/packages/contracts/generated/api/console/explore/orpc.gen.ts @@ -6,6 +6,8 @@ import * as z from 'zod' import { zGetExploreAppsByAppIdPath, zGetExploreAppsByAppIdResponse, + zGetExploreAppsLearnDifyQuery, + zGetExploreAppsLearnDifyResponse, zGetExploreAppsQuery, zGetExploreAppsResponse, zGetExploreBannersQuery, @@ -13,6 +15,21 @@ import { } from './zod.gen' export const get = oc + .route({ + inputStructure: 'detailed', + method: 'GET', + operationId: 'getExploreAppsLearnDify', + path: '/explore/apps/learn-dify', + tags: ['console'], + }) + .input(z.object({ query: zGetExploreAppsLearnDifyQuery.optional() })) + .output(zGetExploreAppsLearnDifyResponse) + +export const learnDify = { + get, +} + +export const get2 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -24,10 +41,10 @@ export const get = oc .output(zGetExploreAppsByAppIdResponse) export const byAppId = { - get, + get: get2, } -export const get2 = oc +export const get3 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -39,14 +56,15 @@ export const get2 = oc .output(zGetExploreAppsResponse) export const apps = { - get: get2, + get: get3, + learnDify, byAppId, } /** * Get banner list */ -export const get3 = oc +export const get4 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -59,7 +77,7 @@ export const get3 = oc .output(zGetExploreBannersResponse) export const banners = { - get: get3, + get: get4, } export const explore = { diff --git a/packages/contracts/generated/api/console/explore/types.gen.ts b/packages/contracts/generated/api/console/explore/types.gen.ts index 72920964ad..e5980e3c54 100644 --- a/packages/contracts/generated/api/console/explore/types.gen.ts +++ b/packages/contracts/generated/api/console/explore/types.gen.ts @@ -9,6 +9,10 @@ export type RecommendedAppListResponse = { recommended_apps: Array } +export type LearnDifyAppListResponse = { + recommended_apps: Array +} + export type RecommendedAppDetailResponse = { [key: string]: unknown } @@ -61,6 +65,22 @@ export type GetExploreAppsResponses = { export type GetExploreAppsResponse = GetExploreAppsResponses[keyof GetExploreAppsResponses] +export type GetExploreAppsLearnDifyData = { + body?: never + path?: never + query?: { + language?: string + } + url: '/explore/apps/learn-dify' +} + +export type GetExploreAppsLearnDifyResponses = { + 200: LearnDifyAppListResponse +} + +export type GetExploreAppsLearnDifyResponse + = GetExploreAppsLearnDifyResponses[keyof GetExploreAppsLearnDifyResponses] + export type GetExploreAppsByAppIdData = { body?: never path: { diff --git a/packages/contracts/generated/api/console/explore/zod.gen.ts b/packages/contracts/generated/api/console/explore/zod.gen.ts index 913338afdf..9346796a86 100644 --- a/packages/contracts/generated/api/console/explore/zod.gen.ts +++ b/packages/contracts/generated/api/console/explore/zod.gen.ts @@ -60,6 +60,13 @@ export const zRecommendedAppListResponse = z.object({ recommended_apps: z.array(zRecommendedAppResponse), }) +/** + * LearnDifyAppListResponse + */ +export const zLearnDifyAppListResponse = z.object({ + recommended_apps: z.array(zRecommendedAppResponse), +}) + export const zGetExploreAppsQuery = z.object({ language: z.string().optional(), }) @@ -69,6 +76,15 @@ export const zGetExploreAppsQuery = z.object({ */ export const zGetExploreAppsResponse = zRecommendedAppListResponse +export const zGetExploreAppsLearnDifyQuery = z.object({ + language: z.string().optional(), +}) + +/** + * Success + */ +export const zGetExploreAppsLearnDifyResponse = zLearnDifyAppListResponse + export const zGetExploreAppsByAppIdPath = z.object({ app_id: z.string(), }) diff --git a/packages/contracts/generated/api/console/workspaces/orpc.gen.ts b/packages/contracts/generated/api/console/workspaces/orpc.gen.ts index ecfc3d44ef..630e8f6b35 100644 --- a/packages/contracts/generated/api/console/workspaces/orpc.gen.ts +++ b/packages/contracts/generated/api/console/workspaces/orpc.gen.ts @@ -67,6 +67,11 @@ import { zGetWorkspacesCurrentPermissionResponse, zGetWorkspacesCurrentPluginAssetQuery, zGetWorkspacesCurrentPluginAssetResponse, + zGetWorkspacesCurrentPluginAutoUpgradeFetchQuery, + zGetWorkspacesCurrentPluginAutoUpgradeFetchResponse, + zGetWorkspacesCurrentPluginByCategoryListPath, + zGetWorkspacesCurrentPluginByCategoryListQuery, + zGetWorkspacesCurrentPluginByCategoryListResponse, zGetWorkspacesCurrentPluginDebuggingKeyResponse, zGetWorkspacesCurrentPluginFetchManifestQuery, zGetWorkspacesCurrentPluginFetchManifestResponse, @@ -79,7 +84,6 @@ import { zGetWorkspacesCurrentPluginParametersDynamicOptionsQuery, zGetWorkspacesCurrentPluginParametersDynamicOptionsResponse, zGetWorkspacesCurrentPluginPermissionFetchResponse, - zGetWorkspacesCurrentPluginPreferencesFetchResponse, zGetWorkspacesCurrentPluginReadmeQuery, zGetWorkspacesCurrentPluginReadmeResponse, zGetWorkspacesCurrentPluginTasksByTaskIdPath, @@ -214,6 +218,10 @@ import { zPostWorkspacesCurrentModelProvidersByProviderPreferredProviderTypeBody, zPostWorkspacesCurrentModelProvidersByProviderPreferredProviderTypePath, zPostWorkspacesCurrentModelProvidersByProviderPreferredProviderTypeResponse, + zPostWorkspacesCurrentPluginAutoUpgradeChangeBody, + zPostWorkspacesCurrentPluginAutoUpgradeChangeResponse, + zPostWorkspacesCurrentPluginAutoUpgradeExcludeBody, + zPostWorkspacesCurrentPluginAutoUpgradeExcludeResponse, zPostWorkspacesCurrentPluginInstallGithubBody, zPostWorkspacesCurrentPluginInstallGithubResponse, zPostWorkspacesCurrentPluginInstallMarketplaceBody, @@ -228,10 +236,6 @@ import { zPostWorkspacesCurrentPluginParametersDynamicOptionsWithCredentialsResponse, zPostWorkspacesCurrentPluginPermissionChangeBody, zPostWorkspacesCurrentPluginPermissionChangeResponse, - zPostWorkspacesCurrentPluginPreferencesAutoupgradeExcludeBody, - zPostWorkspacesCurrentPluginPreferencesAutoupgradeExcludeResponse, - zPostWorkspacesCurrentPluginPreferencesChangeBody, - zPostWorkspacesCurrentPluginPreferencesChangeResponse, zPostWorkspacesCurrentPluginTasksByTaskIdDeleteByIdentifierPath, zPostWorkspacesCurrentPluginTasksByTaskIdDeleteByIdentifierResponse, zPostWorkspacesCurrentPluginTasksByTaskIdDeletePath, @@ -1487,7 +1491,58 @@ export const asset = { get: get20, } +export const post26 = oc + .route({ + inputStructure: 'detailed', + method: 'POST', + operationId: 'postWorkspacesCurrentPluginAutoUpgradeChange', + path: '/workspaces/current/plugin/auto-upgrade/change', + tags: ['console'], + }) + .input(z.object({ body: zPostWorkspacesCurrentPluginAutoUpgradeChangeBody })) + .output(zPostWorkspacesCurrentPluginAutoUpgradeChangeResponse) + +export const change = { + post: post26, +} + +export const post27 = oc + .route({ + inputStructure: 'detailed', + method: 'POST', + operationId: 'postWorkspacesCurrentPluginAutoUpgradeExclude', + path: '/workspaces/current/plugin/auto-upgrade/exclude', + tags: ['console'], + }) + .input(z.object({ body: zPostWorkspacesCurrentPluginAutoUpgradeExcludeBody })) + .output(zPostWorkspacesCurrentPluginAutoUpgradeExcludeResponse) + +export const exclude = { + post: post27, +} + export const get21 = oc + .route({ + inputStructure: 'detailed', + method: 'GET', + operationId: 'getWorkspacesCurrentPluginAutoUpgradeFetch', + path: '/workspaces/current/plugin/auto-upgrade/fetch', + tags: ['console'], + }) + .input(z.object({ query: zGetWorkspacesCurrentPluginAutoUpgradeFetchQuery })) + .output(zGetWorkspacesCurrentPluginAutoUpgradeFetchResponse) + +export const fetch_ = { + get: get21, +} + +export const autoUpgrade = { + change, + exclude, + fetch: fetch_, +} + +export const get22 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -1498,10 +1553,10 @@ export const get21 = oc .output(zGetWorkspacesCurrentPluginDebuggingKeyResponse) export const debuggingKey = { - get: get21, + get: get22, } -export const get22 = oc +export const get23 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -1513,10 +1568,10 @@ export const get22 = oc .output(zGetWorkspacesCurrentPluginFetchManifestResponse) export const fetchManifest = { - get: get22, + get: get23, } -export const get23 = oc +export const get24 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -1528,10 +1583,10 @@ export const get23 = oc .output(zGetWorkspacesCurrentPluginIconResponse) export const icon = { - get: get23, + get: get24, } -export const post26 = oc +export const post28 = oc .route({ inputStructure: 'detailed', method: 'POST', @@ -1543,10 +1598,10 @@ export const post26 = oc .output(zPostWorkspacesCurrentPluginInstallGithubResponse) export const github = { - post: post26, + post: post28, } -export const post27 = oc +export const post29 = oc .route({ inputStructure: 'detailed', method: 'POST', @@ -1558,10 +1613,10 @@ export const post27 = oc .output(zPostWorkspacesCurrentPluginInstallMarketplaceResponse) export const marketplace = { - post: post27, + post: post29, } -export const post28 = oc +export const post30 = oc .route({ inputStructure: 'detailed', method: 'POST', @@ -1573,7 +1628,7 @@ export const post28 = oc .output(zPostWorkspacesCurrentPluginInstallPkgResponse) export const pkg = { - post: post28, + post: post30, } export const install = { @@ -1582,7 +1637,7 @@ export const install = { pkg, } -export const post29 = oc +export const post31 = oc .route({ inputStructure: 'detailed', method: 'POST', @@ -1594,14 +1649,14 @@ export const post29 = oc .output(zPostWorkspacesCurrentPluginListInstallationsIdsResponse) export const ids = { - post: post29, + post: post31, } export const installations = { ids, } -export const post30 = oc +export const post32 = oc .route({ inputStructure: 'detailed', method: 'POST', @@ -1613,10 +1668,10 @@ export const post30 = oc .output(zPostWorkspacesCurrentPluginListLatestVersionsResponse) export const latestVersions = { - post: post30, + post: post32, } -export const get24 = oc +export const get25 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -1628,12 +1683,12 @@ export const get24 = oc .output(zGetWorkspacesCurrentPluginListResponse) export const list2 = { - get: get24, + get: get25, installations, latestVersions, } -export const get25 = oc +export const get26 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -1645,14 +1700,14 @@ export const get25 = oc .output(zGetWorkspacesCurrentPluginMarketplacePkgResponse) export const pkg2 = { - get: get25, + get: get26, } export const marketplace2 = { pkg: pkg2, } -export const get26 = oc +export const get27 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -1664,13 +1719,13 @@ export const get26 = oc .output(zGetWorkspacesCurrentPluginParametersDynamicOptionsResponse) export const dynamicOptions = { - get: get26, + get: get27, } /** * Fetch dynamic options using credentials directly (for edit mode) */ -export const post31 = oc +export const post33 = oc .route({ inputStructure: 'detailed', method: 'POST', @@ -1685,7 +1740,7 @@ export const post31 = oc .output(zPostWorkspacesCurrentPluginParametersDynamicOptionsWithCredentialsResponse) export const dynamicOptionsWithCredentials = { - post: post31, + post: post33, } export const parameters = { @@ -1693,7 +1748,7 @@ export const parameters = { dynamicOptionsWithCredentials, } -export const post32 = oc +export const post34 = oc .route({ inputStructure: 'detailed', method: 'POST', @@ -1704,11 +1759,11 @@ export const post32 = oc .input(z.object({ body: zPostWorkspacesCurrentPluginPermissionChangeBody })) .output(zPostWorkspacesCurrentPluginPermissionChangeResponse) -export const change = { - post: post32, +export const change2 = { + post: post34, } -export const get27 = oc +export const get28 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -1718,65 +1773,11 @@ export const get27 = oc }) .output(zGetWorkspacesCurrentPluginPermissionFetchResponse) -export const fetch_ = { - get: get27, -} - -export const permission2 = { - change, - fetch: fetch_, -} - -export const post33 = oc - .route({ - inputStructure: 'detailed', - method: 'POST', - operationId: 'postWorkspacesCurrentPluginPreferencesAutoupgradeExclude', - path: '/workspaces/current/plugin/preferences/autoupgrade/exclude', - tags: ['console'], - }) - .input(z.object({ body: zPostWorkspacesCurrentPluginPreferencesAutoupgradeExcludeBody })) - .output(zPostWorkspacesCurrentPluginPreferencesAutoupgradeExcludeResponse) - -export const exclude = { - post: post33, -} - -export const autoupgrade = { - exclude, -} - -export const post34 = oc - .route({ - inputStructure: 'detailed', - method: 'POST', - operationId: 'postWorkspacesCurrentPluginPreferencesChange', - path: '/workspaces/current/plugin/preferences/change', - tags: ['console'], - }) - .input(z.object({ body: zPostWorkspacesCurrentPluginPreferencesChangeBody })) - .output(zPostWorkspacesCurrentPluginPreferencesChangeResponse) - -export const change2 = { - post: post34, -} - -export const get28 = oc - .route({ - inputStructure: 'detailed', - method: 'GET', - operationId: 'getWorkspacesCurrentPluginPreferencesFetch', - path: '/workspaces/current/plugin/preferences/fetch', - tags: ['console'], - }) - .output(zGetWorkspacesCurrentPluginPreferencesFetchResponse) - export const fetch2 = { get: get28, } -export const preferences = { - autoupgrade, +export const permission2 = { change: change2, fetch: fetch2, } @@ -1973,8 +1974,33 @@ export const upload = { pkg: pkg3, } +export const get32 = oc + .route({ + inputStructure: 'detailed', + method: 'GET', + operationId: 'getWorkspacesCurrentPluginByCategoryList', + path: '/workspaces/current/plugin/{category}/list', + tags: ['console'], + }) + .input( + z.object({ + params: zGetWorkspacesCurrentPluginByCategoryListPath, + query: zGetWorkspacesCurrentPluginByCategoryListQuery.optional(), + }), + ) + .output(zGetWorkspacesCurrentPluginByCategoryListResponse) + +export const list3 = { + get: get32, +} + +export const byCategory = { + list: list3, +} + export const plugin2 = { asset, + autoUpgrade, debuggingKey, fetchManifest, icon, @@ -1983,15 +2009,15 @@ export const plugin2 = { marketplace: marketplace2, parameters, permission: permission2, - preferences, readme, tasks, uninstall, upgrade, upload, + byCategory, } -export const get32 = oc +export const get33 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2002,7 +2028,7 @@ export const get32 = oc .output(zGetWorkspacesCurrentToolLabelsResponse) export const toolLabels = { - get: get32, + get: get33, } export const post44 = oc @@ -2035,7 +2061,7 @@ export const delete9 = { post: post45, } -export const get33 = oc +export const get34 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2046,11 +2072,11 @@ export const get33 = oc .input(z.object({ query: zGetWorkspacesCurrentToolProviderApiGetQuery })) .output(zGetWorkspacesCurrentToolProviderApiGetResponse) -export const get34 = { - get: get33, +export const get35 = { + get: get34, } -export const get35 = oc +export const get36 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2062,7 +2088,7 @@ export const get35 = oc .output(zGetWorkspacesCurrentToolProviderApiRemoteResponse) export const remote = { - get: get35, + get: get36, } export const post46 = oc @@ -2099,7 +2125,7 @@ export const test = { pre, } -export const get36 = oc +export const get37 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2111,7 +2137,7 @@ export const get36 = oc .output(zGetWorkspacesCurrentToolProviderApiToolsResponse) export const tools = { - get: get36, + get: get37, } export const post48 = oc @@ -2132,7 +2158,7 @@ export const update2 = { export const api = { add, delete: delete9, - get: get34, + get: get35, remote, schema, test, @@ -2160,7 +2186,7 @@ export const add2 = { post: post49, } -export const get37 = oc +export const get38 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2177,10 +2203,10 @@ export const get37 = oc .output(zGetWorkspacesCurrentToolProviderBuiltinByProviderCredentialInfoResponse) export const info = { - get: get37, + get: get38, } -export const get38 = oc +export const get39 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2200,7 +2226,7 @@ export const get38 = oc ) export const byCredentialType = { - get: get38, + get: get39, } export const schema2 = { @@ -2212,7 +2238,7 @@ export const credential = { schema: schema2, } -export const get39 = oc +export const get40 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2229,7 +2255,7 @@ export const get39 = oc .output(zGetWorkspacesCurrentToolProviderBuiltinByProviderCredentialsResponse) export const credentials3 = { - get: get39, + get: get40, } export const post50 = oc @@ -2272,7 +2298,7 @@ export const delete10 = { post: post51, } -export const get40 = oc +export const get41 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2284,10 +2310,10 @@ export const get40 = oc .output(zGetWorkspacesCurrentToolProviderBuiltinByProviderIconResponse) export const icon2 = { - get: get40, + get: get41, } -export const get41 = oc +export const get42 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2299,10 +2325,10 @@ export const get41 = oc .output(zGetWorkspacesCurrentToolProviderBuiltinByProviderInfoResponse) export const info2 = { - get: get41, + get: get42, } -export const get42 = oc +export const get43 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2316,7 +2342,7 @@ export const get42 = oc .output(zGetWorkspacesCurrentToolProviderBuiltinByProviderOauthClientSchemaResponse) export const clientSchema = { - get: get42, + get: get43, } export const delete11 = oc @@ -2334,7 +2360,7 @@ export const delete11 = oc ) .output(zDeleteWorkspacesCurrentToolProviderBuiltinByProviderOauthCustomClientResponse) -export const get43 = oc +export const get44 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2365,7 +2391,7 @@ export const post52 = oc export const customClient = { delete: delete11, - get: get43, + get: get44, post: post52, } @@ -2374,7 +2400,7 @@ export const oauth = { customClient, } -export const get44 = oc +export const get45 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2386,7 +2412,7 @@ export const get44 = oc .output(zGetWorkspacesCurrentToolProviderBuiltinByProviderToolsResponse) export const tools2 = { - get: get44, + get: get45, } export const post53 = oc @@ -2441,7 +2467,7 @@ export const auth = { post: post54, } -export const get45 = oc +export const get46 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2453,14 +2479,14 @@ export const get45 = oc .output(zGetWorkspacesCurrentToolProviderMcpToolsByProviderIdResponse) export const byProviderId = { - get: get45, + get: get46, } export const tools3 = { byProviderId, } -export const get46 = oc +export const get47 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2472,7 +2498,7 @@ export const get46 = oc .output(zGetWorkspacesCurrentToolProviderMcpUpdateByProviderIdResponse) export const byProviderId2 = { - get: get46, + get: get47, } export const update4 = { @@ -2551,7 +2577,7 @@ export const delete13 = { post: post57, } -export const get47 = oc +export const get48 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2562,11 +2588,11 @@ export const get47 = oc .input(z.object({ query: zGetWorkspacesCurrentToolProviderWorkflowGetQuery.optional() })) .output(zGetWorkspacesCurrentToolProviderWorkflowGetResponse) -export const get48 = { - get: get47, +export const get49 = { + get: get48, } -export const get49 = oc +export const get50 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2578,7 +2604,7 @@ export const get49 = oc .output(zGetWorkspacesCurrentToolProviderWorkflowToolsResponse) export const tools4 = { - get: get49, + get: get50, } export const post58 = oc @@ -2599,7 +2625,7 @@ export const update5 = { export const workflow = { create: create2, delete: delete13, - get: get48, + get: get49, tools: tools4, update: update5, } @@ -2611,7 +2637,7 @@ export const toolProvider = { workflow, } -export const get50 = oc +export const get51 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2623,10 +2649,10 @@ export const get50 = oc .output(zGetWorkspacesCurrentToolProvidersResponse) export const toolProviders = { - get: get50, + get: get51, } -export const get51 = oc +export const get52 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2637,10 +2663,10 @@ export const get51 = oc .output(zGetWorkspacesCurrentToolsApiResponse) export const api2 = { - get: get51, + get: get52, } -export const get52 = oc +export const get53 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2651,10 +2677,10 @@ export const get52 = oc .output(zGetWorkspacesCurrentToolsBuiltinResponse) export const builtin2 = { - get: get52, + get: get53, } -export const get53 = oc +export const get54 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2665,10 +2691,10 @@ export const get53 = oc .output(zGetWorkspacesCurrentToolsMcpResponse) export const mcp2 = { - get: get53, + get: get54, } -export const get54 = oc +export const get55 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2679,7 +2705,7 @@ export const get54 = oc .output(zGetWorkspacesCurrentToolsWorkflowResponse) export const workflow2 = { - get: get54, + get: get55, } export const tools5 = { @@ -2689,7 +2715,7 @@ export const tools5 = { workflow: workflow2, } -export const get55 = oc +export const get56 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2701,13 +2727,13 @@ export const get55 = oc .output(zGetWorkspacesCurrentTriggerProviderByProviderIconResponse) export const icon3 = { - get: get55, + get: get56, } /** * Get info for a trigger provider */ -export const get56 = oc +export const get57 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2720,7 +2746,7 @@ export const get56 = oc .output(zGetWorkspacesCurrentTriggerProviderByProviderInfoResponse) export const info3 = { - get: get56, + get: get57, } /** @@ -2741,7 +2767,7 @@ export const delete14 = oc /** * Get OAuth client configuration for a provider */ -export const get57 = oc +export const get58 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2775,7 +2801,7 @@ export const post59 = oc export const client = { delete: delete14, - get: get57, + get: get58, post: post59, } @@ -2842,7 +2868,7 @@ export const create3 = { /** * Get the request logs for a subscription instance for a trigger provider */ -export const get58 = oc +export const get59 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2863,7 +2889,7 @@ export const get58 = oc ) export const bySubscriptionBuilderId2 = { - get: get58, + get: get59, } export const logs = { @@ -2937,7 +2963,7 @@ export const verifyAndUpdate = { /** * Get a subscription instance for a trigger provider */ -export const get59 = oc +export const get60 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2958,7 +2984,7 @@ export const get59 = oc ) export const bySubscriptionBuilderId5 = { - get: get59, + get: get60, } export const builder = { @@ -2973,7 +2999,7 @@ export const builder = { /** * List all trigger subscriptions for the current tenant's provider */ -export const get60 = oc +export const get61 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -2985,14 +3011,14 @@ export const get60 = oc .input(z.object({ params: zGetWorkspacesCurrentTriggerProviderByProviderSubscriptionsListPath })) .output(zGetWorkspacesCurrentTriggerProviderByProviderSubscriptionsListResponse) -export const list3 = { - get: get60, +export const list4 = { + get: get61, } /** * Initiate OAuth authorization flow for a trigger provider */ -export const get61 = oc +export const get62 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3009,7 +3035,7 @@ export const get61 = oc .output(zGetWorkspacesCurrentTriggerProviderByProviderSubscriptionsOauthAuthorizeResponse) export const authorize = { - get: get61, + get: get62, } export const oauth3 = { @@ -3050,7 +3076,7 @@ export const verify = { export const subscriptions = { builder, - list: list3, + list: list4, oauth: oauth3, verify, } @@ -3126,7 +3152,7 @@ export const triggerProvider = { /** * List all trigger providers for the current tenant */ -export const get62 = oc +export const get63 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3138,7 +3164,7 @@ export const get62 = oc .output(zGetWorkspacesCurrentTriggersResponse) export const triggers = { - get: get62, + get: get63, } export const post67 = oc @@ -3237,7 +3263,7 @@ export const switch3 = { post: post71, } -export const get63 = oc +export const get64 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3249,7 +3275,7 @@ export const get63 = oc .output(zGetWorkspacesByTenantIdModelProvidersByProviderByIconTypeByLangResponse) export const byLang = { - get: get63, + get: get64, } export const byIconType = { @@ -3268,7 +3294,7 @@ export const byTenantId = { modelProviders: modelProviders2, } -export const get64 = oc +export const get65 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -3279,7 +3305,7 @@ export const get64 = oc .output(zGetWorkspacesResponse) export const workspaces = { - get: get64, + get: get65, current, customConfig, info: info4, diff --git a/packages/contracts/generated/api/console/workspaces/types.gen.ts b/packages/contracts/generated/api/console/workspaces/types.gen.ts index a3c207dd22..8b6f34b18f 100644 --- a/packages/contracts/generated/api/console/workspaces/types.gen.ts +++ b/packages/contracts/generated/api/console/workspaces/types.gen.ts @@ -376,6 +376,30 @@ export type WorkspacePermissionResponse = { export type BinaryFileResponse = Blob | File +export type ParserAutoUpgradeChange = { + auto_upgrade: PluginAutoUpgradeSettingsPayload + category: PluginCategory +} + +export type PluginAutoUpgradeChangeResponse = { + message?: string | null + success: boolean +} + +export type ParserExcludePlugin = { + category: PluginCategory + plugin_id: string +} + +export type SuccessResponse = { + success: boolean +} + +export type PluginAutoUpgradeFetchResponse = { + auto_upgrade: PluginAutoUpgradeSettingsResponseModel + category: PluginCategory +} + export type PluginDebuggingKeyResponse = { host: string key: string @@ -432,12 +456,8 @@ export type ParserDynamicOptionsWithCredentials = { } export type ParserPermissionChange = { - debug_permission: DebugPermission - install_permission: InstallPermission -} - -export type SuccessResponse = { - success: boolean + debug_permission?: DebugPermission + install_permission?: InstallPermission } export type PluginPermissionResponse = { @@ -445,25 +465,6 @@ export type PluginPermissionResponse = { install_permission: InstallPermission } -export type ParserExcludePlugin = { - plugin_id: string -} - -export type PluginOperationSuccessResponse = { - message?: string | null - success: boolean -} - -export type ParserPreferencesChange = { - auto_upgrade: PluginAutoUpgradeSettingsPayload - permission: PluginPermissionSettingsPayload -} - -export type PluginPreferencesResponse = { - auto_upgrade: PluginAutoUpgradeSettingsPayload - permission: PluginPermissionSettingsPayload -} - export type PluginReadmeResponse = { readme: string } @@ -499,6 +500,12 @@ export type ParserGithubUpload = { version: string } +export type PluginCategoryListResponse = { + builtin_tools: Array + has_more: boolean + plugins: Array +} + export type ToolProviderOpaqueResponse = unknown export type ApiToolProviderAddPayload = { @@ -901,8 +908,8 @@ export type ModelCredentialLoadBalancingResponse = { export type ParameterRule = { default?: unknown | null - help?: I18nObject | null - label: I18nObject + help?: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject | null + label: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject max?: number | null min?: number | null name: string @@ -923,10 +930,6 @@ export type ProviderWithModelsResponse = { tenant_id: string } -export type DebugPermission = 'admins' | 'everyone' | 'noone' - -export type InstallPermission = 'admins' | 'everyone' | 'noone' - export type PluginAutoUpgradeSettingsPayload = { exclude_plugins?: Array include_plugins?: Array @@ -935,9 +938,75 @@ export type PluginAutoUpgradeSettingsPayload = { upgrade_time_of_day?: number } -export type PluginPermissionSettingsPayload = { - debug_permission?: DebugPermission - install_permission?: InstallPermission +export type PluginCategory + = | 'agent-strategy' + | 'datasource' + | 'extension' + | 'model' + | 'tool' + | 'trigger' + +export type PluginAutoUpgradeSettingsResponseModel = { + exclude_plugins: Array + include_plugins: Array + strategy_setting: StrategySetting + upgrade_mode: UpgradeMode + upgrade_time_of_day: number +} + +export type DebugPermission = 'admins' | 'everyone' | 'noone' + +export type InstallPermission = 'admins' | 'everyone' | 'noone' + +export type PluginCategoryBuiltinToolProviderResponse = { + allow_delete: boolean + author: string + description: CoreToolsEntitiesCommonEntitiesI18nObject + icon: + | string + | { + [key: string]: string + } + icon_dark: + | string + | { + [key: string]: string + } + | null + id: string + is_team_authorization: boolean + label: CoreToolsEntitiesCommonEntitiesI18nObject + labels: Array + name: string + plugin_id: string | null + plugin_unique_identifier: string | null + team_credentials: { + [key: string]: unknown + } + tools: Array + type: ToolProviderType + [key: string]: unknown +} + +export type PluginCategoryInstalledPluginResponse = { + checksum: string + created_at: string + declaration: PluginDeclarationResponse + endpoints_active: number + endpoints_setups: number + id: string + installation_id: string + meta: { + [key: string]: unknown + } + name: string + plugin_id: string + plugin_unique_identifier: string + runtime_type: string + source: PluginInstallationSource + tenant_id: string + updated_at: string + version: string } export type ApiProviderSchemaType = 'openai_actions' | 'openai_plugin' | 'openapi' | 'swagger' @@ -976,12 +1045,14 @@ export type CustomConfigurationResponse = { export type I18nObject = { en_US: string + ja_JP?: string | null + pt_BR?: string | null zh_Hans?: string | null } export type ProviderHelpEntity = { - title: I18nObject - url: I18nObject + title: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject + url: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject } export type ModelCredentialSchema = { @@ -1036,6 +1107,11 @@ export type ModelStatus | 'no-permission' | 'quota-exceeded' +export type GraphonModelRuntimeEntitiesCommonEntitiesI18nObject = { + en_US: string + zh_Hans?: string | null +} + export type ParameterType = 'boolean' | 'float' | 'int' | 'string' | 'text' export type ProviderModelWithStatusEntity = { @@ -1059,13 +1135,86 @@ export type StrategySetting = 'disabled' | 'fix_only' | 'latest' export type UpgradeMode = 'all' | 'exclude' | 'partial' +export type CoreToolsEntitiesCommonEntitiesI18nObject = { + en_US: string + ja_JP?: string | null + pt_BR?: string | null + zh_Hans?: string | null +} + +export type PluginCategoryBuiltinToolResponse = { + author: string + description: CoreToolsEntitiesCommonEntitiesI18nObject + label: CoreToolsEntitiesCommonEntitiesI18nObject + labels: Array + name: string + output_schema: { + [key: string]: unknown + } + parameters?: Array<{ + [key: string]: unknown + }> | null + [key: string]: unknown +} + +export type ToolProviderType + = | 'api' + | 'app' + | 'builtin' + | 'dataset-retrieval' + | 'mcp' + | 'plugin' + | 'workflow' + +export type PluginDeclarationResponse = { + agent_strategy?: { + [key: string]: unknown + } | null + author: string | null + category: PluginCategory + created_at: string + datasource?: { + [key: string]: unknown + } | null + description: CoreToolsEntitiesCommonEntitiesI18nObject + endpoint?: { + [key: string]: unknown + } | null + icon: string + icon_dark?: string | null + label: CoreToolsEntitiesCommonEntitiesI18nObject + meta: { + [key: string]: unknown + } + model?: ProviderEntityResponse | null + name: string + plugins: { + [key: string]: Array | null + } + repo?: string | null + resource: { + [key: string]: unknown + } + tags?: Array + tool?: { + [key: string]: unknown + } | null + trigger?: { + [key: string]: unknown + } | null + verified?: boolean + version: string +} + +export type PluginInstallationSource = 'github' | 'marketplace' | 'package' | 'remote' + export type ToolParameterForm = 'form' | 'llm' | 'schema' export type AiModelEntityResponse = { deprecated?: boolean features?: Array | null fetch_from: FetchFrom - label: I18nObject + label: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject model: string model_properties: { [key in ModelPropertyKey]?: unknown @@ -1094,10 +1243,10 @@ export type CustomModelConfiguration = { export type CredentialFormSchema = { default?: string | null - label: I18nObject + label: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject max_length?: number options?: Array | null - placeholder?: I18nObject | null + placeholder?: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject | null required?: boolean show_on?: Array type: FormType @@ -1105,8 +1254,8 @@ export type CredentialFormSchema = { } export type FieldModelSchema = { - label: I18nObject - placeholder?: I18nObject | null + label: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject + placeholder?: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject | null } export type ProviderQuotaType = 'free' | 'paid' | 'trial' @@ -1120,6 +1269,25 @@ export type QuotaConfiguration = { restrict_models?: Array } +export type ProviderEntityResponse = { + background?: string | null + configurate_methods: Array + description?: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject | null + help?: ProviderHelpEntity | null + icon_small?: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject | null + icon_small_dark?: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject | null + label: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject + model_credential_schema?: ModelCredentialSchema | null + models?: Array + position?: { + [key: string]: Array + } | null + provider: string + provider_credential_schema?: ProviderCredentialSchema | null + provider_name?: string + supported_model_types: Array +} + export type PriceConfigResponse = { currency: string input: string @@ -1128,7 +1296,7 @@ export type PriceConfigResponse = { } export type FormOption = { - label: I18nObject + label: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject show_on?: Array value: string } @@ -2166,6 +2334,50 @@ export type GetWorkspacesCurrentPluginAssetResponses = { export type GetWorkspacesCurrentPluginAssetResponse = GetWorkspacesCurrentPluginAssetResponses[keyof GetWorkspacesCurrentPluginAssetResponses] +export type PostWorkspacesCurrentPluginAutoUpgradeChangeData = { + body: ParserAutoUpgradeChange + path?: never + query?: never + url: '/workspaces/current/plugin/auto-upgrade/change' +} + +export type PostWorkspacesCurrentPluginAutoUpgradeChangeResponses = { + 200: PluginAutoUpgradeChangeResponse +} + +export type PostWorkspacesCurrentPluginAutoUpgradeChangeResponse + = PostWorkspacesCurrentPluginAutoUpgradeChangeResponses[keyof PostWorkspacesCurrentPluginAutoUpgradeChangeResponses] + +export type PostWorkspacesCurrentPluginAutoUpgradeExcludeData = { + body: ParserExcludePlugin + path?: never + query?: never + url: '/workspaces/current/plugin/auto-upgrade/exclude' +} + +export type PostWorkspacesCurrentPluginAutoUpgradeExcludeResponses = { + 200: SuccessResponse +} + +export type PostWorkspacesCurrentPluginAutoUpgradeExcludeResponse + = PostWorkspacesCurrentPluginAutoUpgradeExcludeResponses[keyof PostWorkspacesCurrentPluginAutoUpgradeExcludeResponses] + +export type GetWorkspacesCurrentPluginAutoUpgradeFetchData = { + body?: never + path?: never + query: { + category: 'agent-strategy' | 'datasource' | 'extension' | 'model' | 'tool' | 'trigger' + } + url: '/workspaces/current/plugin/auto-upgrade/fetch' +} + +export type GetWorkspacesCurrentPluginAutoUpgradeFetchResponses = { + 200: PluginAutoUpgradeFetchResponse +} + +export type GetWorkspacesCurrentPluginAutoUpgradeFetchResponse + = GetWorkspacesCurrentPluginAutoUpgradeFetchResponses[keyof GetWorkspacesCurrentPluginAutoUpgradeFetchResponses] + export type GetWorkspacesCurrentPluginDebuggingKeyData = { body?: never path?: never @@ -2379,48 +2591,6 @@ export type GetWorkspacesCurrentPluginPermissionFetchResponses = { export type GetWorkspacesCurrentPluginPermissionFetchResponse = GetWorkspacesCurrentPluginPermissionFetchResponses[keyof GetWorkspacesCurrentPluginPermissionFetchResponses] -export type PostWorkspacesCurrentPluginPreferencesAutoupgradeExcludeData = { - body: ParserExcludePlugin - path?: never - query?: never - url: '/workspaces/current/plugin/preferences/autoupgrade/exclude' -} - -export type PostWorkspacesCurrentPluginPreferencesAutoupgradeExcludeResponses = { - 200: PluginOperationSuccessResponse -} - -export type PostWorkspacesCurrentPluginPreferencesAutoupgradeExcludeResponse - = PostWorkspacesCurrentPluginPreferencesAutoupgradeExcludeResponses[keyof PostWorkspacesCurrentPluginPreferencesAutoupgradeExcludeResponses] - -export type PostWorkspacesCurrentPluginPreferencesChangeData = { - body: ParserPreferencesChange - path?: never - query?: never - url: '/workspaces/current/plugin/preferences/change' -} - -export type PostWorkspacesCurrentPluginPreferencesChangeResponses = { - 200: PluginOperationSuccessResponse -} - -export type PostWorkspacesCurrentPluginPreferencesChangeResponse - = PostWorkspacesCurrentPluginPreferencesChangeResponses[keyof PostWorkspacesCurrentPluginPreferencesChangeResponses] - -export type GetWorkspacesCurrentPluginPreferencesFetchData = { - body?: never - path?: never - query?: never - url: '/workspaces/current/plugin/preferences/fetch' -} - -export type GetWorkspacesCurrentPluginPreferencesFetchResponses = { - 200: PluginPreferencesResponse -} - -export type GetWorkspacesCurrentPluginPreferencesFetchResponse - = GetWorkspacesCurrentPluginPreferencesFetchResponses[keyof GetWorkspacesCurrentPluginPreferencesFetchResponses] - export type GetWorkspacesCurrentPluginReadmeData = { body?: never path?: never @@ -2602,6 +2772,25 @@ export type PostWorkspacesCurrentPluginUploadPkgResponses = { export type PostWorkspacesCurrentPluginUploadPkgResponse = PostWorkspacesCurrentPluginUploadPkgResponses[keyof PostWorkspacesCurrentPluginUploadPkgResponses] +export type GetWorkspacesCurrentPluginByCategoryListData = { + body?: never + path: { + category: string + } + query?: { + page?: number + page_size?: number + } + url: '/workspaces/current/plugin/{category}/list' +} + +export type GetWorkspacesCurrentPluginByCategoryListResponses = { + 200: PluginCategoryListResponse +} + +export type GetWorkspacesCurrentPluginByCategoryListResponse + = GetWorkspacesCurrentPluginByCategoryListResponses[keyof GetWorkspacesCurrentPluginByCategoryListResponses] + export type GetWorkspacesCurrentToolLabelsData = { body?: never path?: never diff --git a/packages/contracts/generated/api/console/workspaces/zod.gen.ts b/packages/contracts/generated/api/console/workspaces/zod.gen.ts index f35dd93991..b342c58659 100644 --- a/packages/contracts/generated/api/console/workspaces/zod.gen.ts +++ b/packages/contracts/generated/api/console/workspaces/zod.gen.ts @@ -289,6 +289,21 @@ export const zWorkspacePermissionResponse = z.object({ */ export const zBinaryFileResponse = z.custom() +/** + * PluginAutoUpgradeChangeResponse + */ +export const zPluginAutoUpgradeChangeResponse = z.object({ + message: z.string().nullish(), + success: z.boolean(), +}) + +/** + * SuccessResponse + */ +export const zSuccessResponse = z.object({ + success: z.boolean(), +}) + /** * PluginDebuggingKeyResponse */ @@ -375,28 +390,6 @@ export const zParserDynamicOptionsWithCredentials = z.object({ provider: z.string(), }) -/** - * SuccessResponse - */ -export const zSuccessResponse = z.object({ - success: z.boolean(), -}) - -/** - * ParserExcludePlugin - */ -export const zParserExcludePlugin = z.object({ - plugin_id: z.string(), -}) - -/** - * PluginOperationSuccessResponse - */ -export const zPluginOperationSuccessResponse = z.object({ - message: z.string().nullish(), - success: z.boolean(), -}) - /** * PluginReadmeResponse */ @@ -1022,6 +1015,26 @@ export const zModelCredentialResponse = z.object({ load_balancing: zModelCredentialLoadBalancingResponse, }) +/** + * PluginCategory + */ +export const zPluginCategory = z.enum([ + 'agent-strategy', + 'datasource', + 'extension', + 'model', + 'tool', + 'trigger', +]) + +/** + * ParserExcludePlugin + */ +export const zParserExcludePlugin = z.object({ + category: zPluginCategory, + plugin_id: z.string(), +}) + /** * DebugPermission */ @@ -1036,8 +1049,8 @@ export const zInstallPermission = z.enum(['admins', 'everyone', 'noone']) * ParserPermissionChange */ export const zParserPermissionChange = z.object({ - debug_permission: zDebugPermission, - install_permission: zInstallPermission, + debug_permission: zDebugPermission.optional().default('everyone'), + install_permission: zInstallPermission.optional().default('everyone'), }) /** @@ -1048,14 +1061,6 @@ export const zPluginPermissionResponse = z.object({ install_permission: zInstallPermission, }) -/** - * PluginPermissionSettingsPayload - */ -export const zPluginPermissionSettingsPayload = z.object({ - debug_permission: zDebugPermission.optional().default('everyone'), - install_permission: zInstallPermission.optional().default('everyone'), -}) - /** * ApiProviderSchemaType * @@ -1178,19 +1183,11 @@ export const zConfigurateMethod = z.enum(['customizable-model', 'predefined-mode */ export const zI18nObject = z.object({ en_US: z.string(), + ja_JP: z.string().nullish(), + pt_BR: z.string().nullish(), zh_Hans: z.string().nullish(), }) -/** - * ProviderHelpEntity - * - * Model class for provider help. - */ -export const zProviderHelpEntity = z.object({ - title: zI18nObject, - url: zI18nObject, -}) - /** * ProviderType */ @@ -1254,6 +1251,26 @@ export const zModelStatus = z.enum([ 'quota-exceeded', ]) +/** + * I18nObject + * + * Model class for i18n object. + */ +export const zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject = z.object({ + en_US: z.string(), + zh_Hans: z.string().nullish(), +}) + +/** + * ProviderHelpEntity + * + * Model class for provider help. + */ +export const zProviderHelpEntity = z.object({ + title: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject, + url: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject, +}) + /** * ParameterType * @@ -1268,8 +1285,8 @@ export const zParameterType = z.enum(['boolean', 'float', 'int', 'string', 'text */ export const zParameterRule = z.object({ default: z.unknown().nullish(), - help: zI18nObject.nullish(), - label: zI18nObject, + help: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject.nullish(), + label: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject, max: z.number().nullish(), min: z.number().nullish(), name: z.string(), @@ -1356,21 +1373,98 @@ export const zPluginAutoUpgradeSettingsPayload = z.object({ }) /** - * ParserPreferencesChange + * ParserAutoUpgradeChange */ -export const zParserPreferencesChange = z.object({ +export const zParserAutoUpgradeChange = z.object({ auto_upgrade: zPluginAutoUpgradeSettingsPayload, - permission: zPluginPermissionSettingsPayload, + category: zPluginCategory, }) /** - * PluginPreferencesResponse + * PluginAutoUpgradeSettingsResponseModel */ -export const zPluginPreferencesResponse = z.object({ - auto_upgrade: zPluginAutoUpgradeSettingsPayload, - permission: zPluginPermissionSettingsPayload, +export const zPluginAutoUpgradeSettingsResponseModel = z.object({ + exclude_plugins: z.array(z.string()), + include_plugins: z.array(z.string()), + strategy_setting: zStrategySetting, + upgrade_mode: zUpgradeMode, + upgrade_time_of_day: z.int(), }) +/** + * PluginAutoUpgradeFetchResponse + */ +export const zPluginAutoUpgradeFetchResponse = z.object({ + auto_upgrade: zPluginAutoUpgradeSettingsResponseModel, + category: zPluginCategory, +}) + +/** + * I18nObject + * + * Model class for i18n object. + */ +export const zCoreToolsEntitiesCommonEntitiesI18nObject = z.object({ + en_US: z.string(), + ja_JP: z.string().nullish(), + pt_BR: z.string().nullish(), + zh_Hans: z.string().nullish(), +}) + +/** + * PluginCategoryBuiltinToolResponse + */ +export const zPluginCategoryBuiltinToolResponse = z.object({ + author: z.string(), + description: zCoreToolsEntitiesCommonEntitiesI18nObject, + label: zCoreToolsEntitiesCommonEntitiesI18nObject, + labels: z.array(z.string()), + name: z.string(), + output_schema: z.record(z.string(), z.unknown()), + parameters: z.array(z.record(z.string(), z.unknown())).nullish(), +}) + +/** + * ToolProviderType + * + * Enum class for tool provider + */ +export const zToolProviderType = z.enum([ + 'api', + 'app', + 'builtin', + 'dataset-retrieval', + 'mcp', + 'plugin', + 'workflow', +]) + +/** + * PluginCategoryBuiltinToolProviderResponse + */ +export const zPluginCategoryBuiltinToolProviderResponse = z.object({ + allow_delete: z.boolean(), + author: z.string(), + description: zCoreToolsEntitiesCommonEntitiesI18nObject, + icon: z.union([z.string(), z.record(z.string(), z.string())]), + icon_dark: z.union([z.string(), z.record(z.string(), z.string())]).nullable(), + id: z.string(), + is_team_authorization: z.boolean(), + label: zCoreToolsEntitiesCommonEntitiesI18nObject, + labels: z.array(z.string()), + name: z.string(), + plugin_id: z.string().nullable(), + plugin_unique_identifier: z.string().nullable(), + team_credentials: z.record(z.string(), z.unknown()), + tools: z.array(zPluginCategoryBuiltinToolResponse), + type: zToolProviderType, +}) + +/** + * PluginInstallationSource + */ +export const zPluginInstallationSource = z.enum(['github', 'marketplace', 'package', 'remote']) + /** * ToolParameterForm */ @@ -1458,8 +1552,8 @@ export const zCustomConfigurationResponse = z.object({ * FieldModelSchema */ export const zFieldModelSchema = z.object({ - label: zI18nObject, - placeholder: zI18nObject.nullish(), + label: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject, + placeholder: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject.nullish(), }) /** @@ -1489,7 +1583,7 @@ export const zAiModelEntityResponse = z.object({ deprecated: z.boolean().optional().default(false), features: z.array(zModelFeature).nullish(), fetch_from: zFetchFrom, - label: zI18nObject, + label: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject, model: z.string(), model_properties: z.record(z.string(), z.unknown()), model_type: zModelType, @@ -1573,7 +1667,7 @@ export const zFormShowOnObject = z.object({ * Model class for form option. */ export const zFormOption = z.object({ - label: zI18nObject, + label: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject, show_on: z.array(zFormShowOnObject).optional().default([]), value: z.string(), }) @@ -1592,10 +1686,10 @@ export const zFormType = z.enum(['radio', 'secret-input', 'select', 'switch', 't */ export const zCredentialFormSchema = z.object({ default: z.string().nullish(), - label: zI18nObject, + label: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject, max_length: z.int().optional().default(0), options: z.array(zFormOption).nullish(), - placeholder: zI18nObject.nullish(), + placeholder: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject.nullish(), required: z.boolean().optional().default(true), show_on: z.array(zFormShowOnObject).optional().default([]), type: zFormType, @@ -1621,6 +1715,86 @@ export const zProviderCredentialSchema = z.object({ credential_form_schemas: z.array(zCredentialFormSchema), }) +/** + * ProviderEntityResponse + * + * Runtime provider response with codegen-safe model pricing schemas. + */ +export const zProviderEntityResponse = z.object({ + background: z.string().nullish(), + configurate_methods: z.array(zConfigurateMethod), + description: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject.nullish(), + help: zProviderHelpEntity.nullish(), + icon_small: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject.nullish(), + icon_small_dark: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject.nullish(), + label: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject, + model_credential_schema: zModelCredentialSchema.nullish(), + models: z.array(zAiModelEntityResponse).optional().default([]), + position: z.record(z.string(), z.array(z.string())).nullish().default({}), + provider: z.string(), + provider_credential_schema: zProviderCredentialSchema.nullish(), + provider_name: z.string().optional().default(''), + supported_model_types: z.array(zModelType), +}) + +/** + * PluginDeclarationResponse + */ +export const zPluginDeclarationResponse = z.object({ + agent_strategy: z.record(z.string(), z.unknown()).nullish(), + author: z.string().nullable(), + category: zPluginCategory, + created_at: z.iso.datetime(), + datasource: z.record(z.string(), z.unknown()).nullish(), + description: zCoreToolsEntitiesCommonEntitiesI18nObject, + endpoint: z.record(z.string(), z.unknown()).nullish(), + icon: z.string(), + icon_dark: z.string().nullish(), + label: zCoreToolsEntitiesCommonEntitiesI18nObject, + meta: z.record(z.string(), z.unknown()), + model: zProviderEntityResponse.nullish(), + name: z.string(), + plugins: z.record(z.string(), z.array(z.string()).nullable()), + repo: z.string().nullish(), + resource: z.record(z.string(), z.unknown()), + tags: z.array(z.string()).optional(), + tool: z.record(z.string(), z.unknown()).nullish(), + trigger: z.record(z.string(), z.unknown()).nullish(), + verified: z.boolean().optional().default(false), + version: z.string(), +}) + +/** + * PluginCategoryInstalledPluginResponse + */ +export const zPluginCategoryInstalledPluginResponse = z.object({ + checksum: z.string(), + created_at: z.iso.datetime(), + declaration: zPluginDeclarationResponse, + endpoints_active: z.int(), + endpoints_setups: z.int(), + id: z.string(), + installation_id: z.string(), + meta: z.record(z.string(), z.unknown()), + name: z.string(), + plugin_id: z.string(), + plugin_unique_identifier: z.string(), + runtime_type: z.string(), + source: zPluginInstallationSource, + tenant_id: z.string(), + updated_at: z.iso.datetime(), + version: z.string(), +}) + +/** + * PluginCategoryListResponse + */ +export const zPluginCategoryListResponse = z.object({ + builtin_tools: z.array(zPluginCategoryBuiltinToolProviderResponse), + has_more: z.boolean(), + plugins: z.array(zPluginCategoryInstalledPluginResponse), +}) + /** * QuotaUnit */ @@ -2294,6 +2468,30 @@ export const zGetWorkspacesCurrentPluginAssetQuery = z.object({ */ export const zGetWorkspacesCurrentPluginAssetResponse = zBinaryFileResponse +export const zPostWorkspacesCurrentPluginAutoUpgradeChangeBody = zParserAutoUpgradeChange + +/** + * Success + */ +export const zPostWorkspacesCurrentPluginAutoUpgradeChangeResponse + = zPluginAutoUpgradeChangeResponse + +export const zPostWorkspacesCurrentPluginAutoUpgradeExcludeBody = zParserExcludePlugin + +/** + * Success + */ +export const zPostWorkspacesCurrentPluginAutoUpgradeExcludeResponse = zSuccessResponse + +export const zGetWorkspacesCurrentPluginAutoUpgradeFetchQuery = z.object({ + category: z.enum(['agent-strategy', 'datasource', 'extension', 'model', 'tool', 'trigger']), +}) + +/** + * Success + */ +export const zGetWorkspacesCurrentPluginAutoUpgradeFetchResponse = zPluginAutoUpgradeFetchResponse + /** * Success */ @@ -2408,26 +2606,6 @@ export const zPostWorkspacesCurrentPluginPermissionChangeResponse = zSuccessResp */ export const zGetWorkspacesCurrentPluginPermissionFetchResponse = zPluginPermissionResponse -export const zPostWorkspacesCurrentPluginPreferencesAutoupgradeExcludeBody = zParserExcludePlugin - -/** - * Success - */ -export const zPostWorkspacesCurrentPluginPreferencesAutoupgradeExcludeResponse - = zPluginOperationSuccessResponse - -export const zPostWorkspacesCurrentPluginPreferencesChangeBody = zParserPreferencesChange - -/** - * Success - */ -export const zPostWorkspacesCurrentPluginPreferencesChangeResponse = zPluginOperationSuccessResponse - -/** - * Success - */ -export const zGetWorkspacesCurrentPluginPreferencesFetchResponse = zPluginPreferencesResponse - export const zGetWorkspacesCurrentPluginReadmeQuery = z.object({ language: z.string().optional().default('en-US'), plugin_unique_identifier: z.string(), @@ -2519,6 +2697,20 @@ export const zPostWorkspacesCurrentPluginUploadGithubResponse = zPluginDaemonOpe */ export const zPostWorkspacesCurrentPluginUploadPkgResponse = zPluginDaemonOperationResponse +export const zGetWorkspacesCurrentPluginByCategoryListPath = z.object({ + category: z.string(), +}) + +export const zGetWorkspacesCurrentPluginByCategoryListQuery = z.object({ + page: z.int().gte(1).optional().default(1), + page_size: z.int().gte(1).lte(256).optional().default(256), +}) + +/** + * Success + */ +export const zGetWorkspacesCurrentPluginByCategoryListResponse = zPluginCategoryListResponse + /** * Success */ diff --git a/packages/dify-ui/src/styles/utilities.css b/packages/dify-ui/src/styles/utilities.css index 69b15d4c10..312d6052bd 100644 --- a/packages/dify-ui/src/styles/utilities.css +++ b/packages/dify-ui/src/styles/utilities.css @@ -265,6 +265,12 @@ line-height: 1.2; } +@utility title-5xl-semi-bold { + font-size: 30px; + font-weight: 600; + line-height: 1.2; +} + @utility title-5xl-bold { font-size: 30px; font-weight: 700; diff --git a/packages/dify-ui/src/themes/dark.css b/packages/dify-ui/src/themes/dark.css index 4c005983ef..1b24e8fb48 100644 --- a/packages/dify-ui/src/themes/dark.css +++ b/packages/dify-ui/src/themes/dark.css @@ -156,6 +156,19 @@ html[data-theme="dark"] { --color-components-main-nav-nav-button-bg-active: rgb(200 206 218 / 0.14); --color-components-main-nav-nav-button-border: rgb(255 255 255 / 0.08); --color-components-main-nav-nav-button-bg-hover: rgb(200 206 218 / 0.04); + --color-components-main-nav-glass-text-glow: #3146ff2e; + --color-components-main-nav-glass-surface-first: #0033ff14; + --color-components-main-nav-glass-surface-middle-1: #0033ff1f; + --color-components-main-nav-glass-surface-middle-2: #0033ff1a; + --color-components-main-nav-glass-surface-end: #0033ff14; + --color-components-main-nav-glass-edge-highlight-first: #fffffffa; + --color-components-main-nav-glass-edge-highlight-end: #ffffff6b; + --color-components-main-nav-glass-edge-reflection-first: #0033ff00; + --color-components-main-nav-glass-edge-reflection-middle: #0033ff99; + --color-components-main-nav-glass-edge-reflection-end: #0033ff00; + --color-components-main-nav-glass-inner-glow: #ffffff4d; + --color-components-main-nav-glass-shadow-reflection: #0033ff0a; + --color-components-main-nav-glass-shadow-reflection-glow: #ffffff00; --color-components-main-nav-nav-user-border: rgb(255 255 255 / 0.05); diff --git a/packages/dify-ui/src/themes/light.css b/packages/dify-ui/src/themes/light.css index 16e5e898ee..3feb4afb47 100644 --- a/packages/dify-ui/src/themes/light.css +++ b/packages/dify-ui/src/themes/light.css @@ -156,6 +156,19 @@ html[data-theme="light"] { --color-components-main-nav-nav-button-bg-active: #fcfcfd; --color-components-main-nav-nav-button-border: rgb(255 255 255 / 0.95); --color-components-main-nav-nav-button-bg-hover: rgb(16 24 40 / 0.04); + --color-components-main-nav-glass-text-glow: #3146ff2e; + --color-components-main-nav-glass-surface-first: #0033ff14; + --color-components-main-nav-glass-surface-middle-1: #0033ff1f; + --color-components-main-nav-glass-surface-middle-2: #0033ff1a; + --color-components-main-nav-glass-surface-end: #0033ff14; + --color-components-main-nav-glass-edge-highlight-first: #fffffffa; + --color-components-main-nav-glass-edge-highlight-end: #ffffff6b; + --color-components-main-nav-glass-edge-reflection-first: #0033ff00; + --color-components-main-nav-glass-edge-reflection-middle: #0033ff99; + --color-components-main-nav-glass-edge-reflection-end: #0033ff00; + --color-components-main-nav-glass-inner-glow: #ffffff4d; + --color-components-main-nav-glass-shadow-reflection: #0033ff0a; + --color-components-main-nav-glass-shadow-reflection-glow: #ffffff00; --color-components-main-nav-nav-user-border: #ffffff; diff --git a/packages/dify-ui/src/themes/theme.css b/packages/dify-ui/src/themes/theme.css index 04e02d852c..3e35feb8eb 100644 --- a/packages/dify-ui/src/themes/theme.css +++ b/packages/dify-ui/src/themes/theme.css @@ -163,6 +163,19 @@ --color-components-main-nav-nav-button-bg-active: var(--color-components-main-nav-nav-button-bg-active); --color-components-main-nav-nav-button-border: var(--color-components-main-nav-nav-button-border); --color-components-main-nav-nav-button-bg-hover: var(--color-components-main-nav-nav-button-bg-hover); + --color-components-main-nav-glass-text-glow: var(--color-components-main-nav-glass-text-glow); + --color-components-main-nav-glass-surface-first: var(--color-components-main-nav-glass-surface-first); + --color-components-main-nav-glass-surface-middle-1: var(--color-components-main-nav-glass-surface-middle-1); + --color-components-main-nav-glass-surface-middle-2: var(--color-components-main-nav-glass-surface-middle-2); + --color-components-main-nav-glass-surface-end: var(--color-components-main-nav-glass-surface-end); + --color-components-main-nav-glass-edge-highlight-first: var(--color-components-main-nav-glass-edge-highlight-first); + --color-components-main-nav-glass-edge-highlight-end: var(--color-components-main-nav-glass-edge-highlight-end); + --color-components-main-nav-glass-edge-reflection-first: var(--color-components-main-nav-glass-edge-reflection-first); + --color-components-main-nav-glass-edge-reflection-middle: var(--color-components-main-nav-glass-edge-reflection-middle); + --color-components-main-nav-glass-edge-reflection-end: var(--color-components-main-nav-glass-edge-reflection-end); + --color-components-main-nav-glass-inner-glow: var(--color-components-main-nav-glass-inner-glow); + --color-components-main-nav-glass-shadow-reflection: var(--color-components-main-nav-glass-shadow-reflection); + --color-components-main-nav-glass-shadow-reflection-glow: var(--color-components-main-nav-glass-shadow-reflection-glow); --color-components-main-nav-nav-user-border: var(--color-components-main-nav-nav-user-border); diff --git a/packages/iconify-collections/README.md b/packages/iconify-collections/README.md new file mode 100644 index 0000000000..f36b63d03b --- /dev/null +++ b/packages/iconify-collections/README.md @@ -0,0 +1,43 @@ +# @dify/iconify-collections + +Pre-generated Iconify collections for Dify custom SVG icons. The web app imports these collections from this package so Tailwind does not need to scan and build custom SVG icon data from the old `web/app/components/base/icons/src` tree during dev startup. + +## Adding Custom SVG Icons + +Add new SVG source files under one of these directories: + +- `assets/public/...` for multi-color or public brand-like icons. +- `assets/vender/...` for UI vendor icons that should render with `currentColor`. + +After adding or changing SVG files, regenerate the packaged collections: + +```bash +pnpm --filter @dify/iconify-collections generate +``` + +Then run the dimension guard: + +```bash +pnpm --filter @dify/iconify-collections check:dimensions +``` + +This protects existing icon groups with layout-sensitive intrinsic sizes, such as the `main-nav-*` icons that must remain `20x20` after collection flattening. + +Commit both the SVG source files and the generated package files under `custom-public/` or `custom-vender/`. +Restart the web dev server after regenerating icons. Tailwind loads this plugin collection at startup, so an already-running dev server may not render newly-added `i-custom-*` classes until it restarts. + +Use the generated icons through Tailwind icon classes in frontend code. For example: + +```text +assets/vender/integrations/mcp.svg +``` + +becomes: + +```tsx + +``` + +Do not add new generated React icon components or JSON files under `web/app/components/base/icons/src/...` for new custom SVG icons. That path is legacy; new custom icons should flow through this package and be consumed as `i-custom-*` classes. + +When reviewing generated `icons.json` diffs, check that unrelated existing icon groups did not lose or change their intrinsic `width` and `height`. If a group is layout-sensitive, add it to `scripts/check-icon-dimensions.ts`. diff --git a/packages/iconify-collections/assets/vender/integrations/agent-strategy-active.svg b/packages/iconify-collections/assets/vender/integrations/agent-strategy-active.svg new file mode 100644 index 0000000000..85b5cede98 --- /dev/null +++ b/packages/iconify-collections/assets/vender/integrations/agent-strategy-active.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/iconify-collections/assets/vender/integrations/agent-strategy.svg b/packages/iconify-collections/assets/vender/integrations/agent-strategy.svg new file mode 100644 index 0000000000..8dd5385fde --- /dev/null +++ b/packages/iconify-collections/assets/vender/integrations/agent-strategy.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/iconify-collections/assets/vender/integrations/api-extension-active.svg b/packages/iconify-collections/assets/vender/integrations/api-extension-active.svg new file mode 100644 index 0000000000..631a2c6adc --- /dev/null +++ b/packages/iconify-collections/assets/vender/integrations/api-extension-active.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/iconify-collections/assets/vender/integrations/api-extension.svg b/packages/iconify-collections/assets/vender/integrations/api-extension.svg new file mode 100644 index 0000000000..97dd93a77b --- /dev/null +++ b/packages/iconify-collections/assets/vender/integrations/api-extension.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/iconify-collections/assets/vender/integrations/custom-tool-active.svg b/packages/iconify-collections/assets/vender/integrations/custom-tool-active.svg new file mode 100644 index 0000000000..a4dd7d6f84 --- /dev/null +++ b/packages/iconify-collections/assets/vender/integrations/custom-tool-active.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/iconify-collections/assets/vender/integrations/custom-tool.svg b/packages/iconify-collections/assets/vender/integrations/custom-tool.svg new file mode 100644 index 0000000000..15c6324fbf --- /dev/null +++ b/packages/iconify-collections/assets/vender/integrations/custom-tool.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/iconify-collections/assets/vender/integrations/extension-active.svg b/packages/iconify-collections/assets/vender/integrations/extension-active.svg new file mode 100644 index 0000000000..41793c99e1 --- /dev/null +++ b/packages/iconify-collections/assets/vender/integrations/extension-active.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/iconify-collections/assets/vender/integrations/extension.svg b/packages/iconify-collections/assets/vender/integrations/extension.svg new file mode 100644 index 0000000000..ea3735aff6 --- /dev/null +++ b/packages/iconify-collections/assets/vender/integrations/extension.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/iconify-collections/assets/vender/integrations/install-drop.svg b/packages/iconify-collections/assets/vender/integrations/install-drop.svg new file mode 100644 index 0000000000..c82a9fc481 --- /dev/null +++ b/packages/iconify-collections/assets/vender/integrations/install-drop.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/iconify-collections/assets/vender/integrations/install-github.svg b/packages/iconify-collections/assets/vender/integrations/install-github.svg new file mode 100644 index 0000000000..b11c3255c3 --- /dev/null +++ b/packages/iconify-collections/assets/vender/integrations/install-github.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/iconify-collections/assets/vender/integrations/install-local-package.svg b/packages/iconify-collections/assets/vender/integrations/install-local-package.svg new file mode 100644 index 0000000000..5167a2fe03 --- /dev/null +++ b/packages/iconify-collections/assets/vender/integrations/install-local-package.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/iconify-collections/assets/vender/integrations/install-marketplace.svg b/packages/iconify-collections/assets/vender/integrations/install-marketplace.svg new file mode 100644 index 0000000000..a1649a4a09 --- /dev/null +++ b/packages/iconify-collections/assets/vender/integrations/install-marketplace.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/iconify-collections/assets/vender/integrations/mcp.svg b/packages/iconify-collections/assets/vender/integrations/mcp.svg new file mode 100644 index 0000000000..a4a15f99f7 --- /dev/null +++ b/packages/iconify-collections/assets/vender/integrations/mcp.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/iconify-collections/assets/vender/integrations/panel-left.svg b/packages/iconify-collections/assets/vender/integrations/panel-left.svg new file mode 100644 index 0000000000..fb0378e557 --- /dev/null +++ b/packages/iconify-collections/assets/vender/integrations/panel-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/iconify-collections/assets/vender/integrations/tools-active.svg b/packages/iconify-collections/assets/vender/integrations/tools-active.svg new file mode 100644 index 0000000000..3961797744 --- /dev/null +++ b/packages/iconify-collections/assets/vender/integrations/tools-active.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/iconify-collections/assets/vender/integrations/tools.svg b/packages/iconify-collections/assets/vender/integrations/tools.svg new file mode 100644 index 0000000000..d88b285bce --- /dev/null +++ b/packages/iconify-collections/assets/vender/integrations/tools.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/iconify-collections/assets/vender/integrations/trigger-active.svg b/packages/iconify-collections/assets/vender/integrations/trigger-active.svg new file mode 100644 index 0000000000..018048390e --- /dev/null +++ b/packages/iconify-collections/assets/vender/integrations/trigger-active.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/iconify-collections/assets/vender/integrations/trigger.svg b/packages/iconify-collections/assets/vender/integrations/trigger.svg new file mode 100644 index 0000000000..80b9bba35f --- /dev/null +++ b/packages/iconify-collections/assets/vender/integrations/trigger.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/iconify-collections/assets/vender/integrations/workflow-as-tool-active.svg b/packages/iconify-collections/assets/vender/integrations/workflow-as-tool-active.svg new file mode 100644 index 0000000000..110a9cb7a0 --- /dev/null +++ b/packages/iconify-collections/assets/vender/integrations/workflow-as-tool-active.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/packages/iconify-collections/assets/vender/integrations/workflow-as-tool.svg b/packages/iconify-collections/assets/vender/integrations/workflow-as-tool.svg new file mode 100644 index 0000000000..6a25ba0411 --- /dev/null +++ b/packages/iconify-collections/assets/vender/integrations/workflow-as-tool.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/iconify-collections/assets/vender/main-nav/app-home.svg b/packages/iconify-collections/assets/vender/main-nav/app-home.svg new file mode 100644 index 0000000000..157d8dcd52 --- /dev/null +++ b/packages/iconify-collections/assets/vender/main-nav/app-home.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/iconify-collections/assets/vender/main-nav/credits.svg b/packages/iconify-collections/assets/vender/main-nav/credits.svg new file mode 100644 index 0000000000..e956861d72 --- /dev/null +++ b/packages/iconify-collections/assets/vender/main-nav/credits.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/iconify-collections/assets/vender/main-nav/help.svg b/packages/iconify-collections/assets/vender/main-nav/help.svg new file mode 100644 index 0000000000..58d0a92dd6 --- /dev/null +++ b/packages/iconify-collections/assets/vender/main-nav/help.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/iconify-collections/assets/vender/main-nav/home-active.svg b/packages/iconify-collections/assets/vender/main-nav/home-active.svg new file mode 100644 index 0000000000..13dc13e094 --- /dev/null +++ b/packages/iconify-collections/assets/vender/main-nav/home-active.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/iconify-collections/assets/vender/main-nav/home.svg b/packages/iconify-collections/assets/vender/main-nav/home.svg new file mode 100644 index 0000000000..cf685e7f23 --- /dev/null +++ b/packages/iconify-collections/assets/vender/main-nav/home.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/iconify-collections/assets/vender/main-nav/integrations-active.svg b/packages/iconify-collections/assets/vender/main-nav/integrations-active.svg new file mode 100644 index 0000000000..351802502f --- /dev/null +++ b/packages/iconify-collections/assets/vender/main-nav/integrations-active.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/iconify-collections/assets/vender/main-nav/integrations.svg b/packages/iconify-collections/assets/vender/main-nav/integrations.svg new file mode 100644 index 0000000000..af030453f9 --- /dev/null +++ b/packages/iconify-collections/assets/vender/main-nav/integrations.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/iconify-collections/assets/vender/main-nav/knowledge-active.svg b/packages/iconify-collections/assets/vender/main-nav/knowledge-active.svg new file mode 100644 index 0000000000..869982f117 --- /dev/null +++ b/packages/iconify-collections/assets/vender/main-nav/knowledge-active.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/iconify-collections/assets/vender/main-nav/knowledge.svg b/packages/iconify-collections/assets/vender/main-nav/knowledge.svg new file mode 100644 index 0000000000..c54812ffc2 --- /dev/null +++ b/packages/iconify-collections/assets/vender/main-nav/knowledge.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/iconify-collections/assets/vender/main-nav/marketplace-active.svg b/packages/iconify-collections/assets/vender/main-nav/marketplace-active.svg new file mode 100644 index 0000000000..8a2d7a7911 --- /dev/null +++ b/packages/iconify-collections/assets/vender/main-nav/marketplace-active.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/iconify-collections/assets/vender/main-nav/marketplace.svg b/packages/iconify-collections/assets/vender/main-nav/marketplace.svg new file mode 100644 index 0000000000..cbec531fe9 --- /dev/null +++ b/packages/iconify-collections/assets/vender/main-nav/marketplace.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/iconify-collections/assets/vender/main-nav/quick-search.svg b/packages/iconify-collections/assets/vender/main-nav/quick-search.svg new file mode 100644 index 0000000000..f96f09d4e4 --- /dev/null +++ b/packages/iconify-collections/assets/vender/main-nav/quick-search.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/iconify-collections/assets/vender/main-nav/studio-active.svg b/packages/iconify-collections/assets/vender/main-nav/studio-active.svg new file mode 100644 index 0000000000..2441d4760c --- /dev/null +++ b/packages/iconify-collections/assets/vender/main-nav/studio-active.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/iconify-collections/assets/vender/main-nav/studio.svg b/packages/iconify-collections/assets/vender/main-nav/studio.svg new file mode 100644 index 0000000000..ac64f1100a --- /dev/null +++ b/packages/iconify-collections/assets/vender/main-nav/studio.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/iconify-collections/assets/vender/main-nav/workspace-settings.svg b/packages/iconify-collections/assets/vender/main-nav/workspace-settings.svg new file mode 100644 index 0000000000..0eba74b874 --- /dev/null +++ b/packages/iconify-collections/assets/vender/main-nav/workspace-settings.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/iconify-collections/custom-public/icons.json b/packages/iconify-collections/custom-public/icons.json index 13d8400b89..7a4ea8bdaf 100644 --- a/packages/iconify-collections/custom-public/icons.json +++ b/packages/iconify-collections/custom-public/icons.json @@ -1,6 +1,6 @@ { "prefix": "custom-public", - "lastModified": 1776670621, + "lastModified": 1781246368, "icons": { "avatar-user": { "body": "", @@ -9,11 +9,13 @@ }, "billing-ar-cube-1": { "body": "", - "width": 28 + "width": 28, + "height": 28 }, "billing-asterisk": { "body": "", - "width": 28 + "width": 28, + "height": 28 }, "billing-aws-marketplace-dark": { "body": "", @@ -31,10 +33,14 @@ "height": 20 }, "billing-buildings": { - "body": "" + "body": "", + "width": 29, + "height": 28 }, "billing-diamond": { - "body": "" + "body": "", + "width": 29, + "height": 28 }, "billing-google-cloud": { "body": "", @@ -42,10 +48,14 @@ "height": 18 }, "billing-group-2": { - "body": "" + "body": "", + "width": 29, + "height": 28 }, "billing-keyframe": { - "body": "" + "body": "", + "width": 29, + "height": 28 }, "billing-sparkles-soft": { "body": "", @@ -132,31 +142,49 @@ "height": 22 }, "files-csv": { - "body": "" + "body": "", + "width": 32, + "height": 34 }, "files-doc": { - "body": "" + "body": "", + "width": 32, + "height": 34 }, "files-docx": { - "body": "" + "body": "", + "width": 32, + "height": 34 }, "files-html": { - "body": "" + "body": "", + "width": 32, + "height": 34 }, "files-json": { - "body": "" + "body": "", + "width": 32, + "height": 34 }, "files-md": { - "body": "" + "body": "", + "width": 32, + "height": 34 }, "files-pdf": { - "body": "" + "body": "", + "width": 32, + "height": 34 }, "files-txt": { - "body": "" + "body": "", + "width": 32, + "height": 34 }, "files-unknown": { - "body": "" + "body": "", + "width": 32, + "height": 34 }, "files-xlsx": { "body": "", @@ -184,10 +212,14 @@ "height": 74 }, "knowledge-option-card-effect-orange": { - "body": "" + "body": "", + "width": 220, + "height": 220 }, "knowledge-option-card-effect-purple": { - "body": "" + "body": "", + "width": 220, + "height": 220 }, "knowledge-option-card-effect-teal": { "body": "", @@ -205,33 +237,49 @@ "height": 500 }, "knowledge-dataset-card-external-knowledge-base": { - "body": "" + "body": "", + "width": 18, + "height": 18 }, "knowledge-dataset-card-general": { - "body": "" + "body": "", + "width": 18, + "height": 18 }, "knowledge-dataset-card-graph": { - "body": "" + "body": "", + "width": 18, + "height": 18 }, "knowledge-dataset-card-parent-child": { - "body": "" + "body": "", + "width": 18, + "height": 18 }, "knowledge-dataset-card-qa": { - "body": "" + "body": "", + "width": 18, + "height": 18 }, "knowledge-online-drive-buckets-blue": { "body": "", - "height": 21 + "height": 21, + "width": 20 }, "knowledge-online-drive-buckets-gray": { "body": "", - "width": 18 + "width": 18, + "height": 19 }, "knowledge-online-drive-folder": { - "body": "" + "body": "", + "width": 20, + "height": 19 }, "llm-anthropic": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "llm-anthropic-dark": { "body": "", @@ -255,32 +303,43 @@ }, "llm-azure-openai-service": { "body": "", - "width": 56 + "width": 56, + "height": 24 }, "llm-azure-openai-service-text": { "body": "", - "width": 212 + "width": 212, + "height": 24 }, "llm-azureai": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "llm-azureai-text": { "body": "", - "width": 92 + "width": 92, + "height": 24 }, "llm-baichuan": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "llm-baichuan-text": { "body": "", - "width": 130 + "width": 130, + "height": 24 }, "llm-chatglm": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "llm-chatglm-text": { "body": "", - "width": 100 + "width": 100, + "height": 24 }, "llm-cohere": { "body": "", @@ -289,7 +348,8 @@ }, "llm-cohere-text": { "body": "", - "width": 120 + "width": 120, + "height": 24 }, "llm-deepseek": { "body": "", @@ -302,10 +362,14 @@ "height": 40 }, "llm-gpt-3": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "llm-gpt-4": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "llm-grok": { "body": "", @@ -313,33 +377,44 @@ "height": 40 }, "llm-huggingface": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "llm-huggingface-text": { "body": "", - "width": 120 + "width": 120, + "height": 24 }, "llm-huggingface-text-hub": { "body": "", - "width": 151 + "width": 151, + "height": 24 }, "llm-iflytek-spark": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "llm-iflytek-spark-text": { "body": "", - "width": 150 + "width": 150, + "height": 24 }, "llm-iflytek-spark-text-cn": { "body": "", - "width": 84 + "width": 84, + "height": 24 }, "llm-jina": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "llm-jina-text": { "body": "", - "width": 58 + "width": 58, + "height": 24 }, "llm-microsoft": { "body": "", @@ -347,16 +422,24 @@ "height": 22 }, "llm-openai-black": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "llm-openai-blue": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "llm-openai-green": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "llm-openai-teal": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "llm-openai-text": { "body": "", @@ -364,16 +447,24 @@ "height": 20 }, "llm-openai-transparent": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "llm-openai-violet": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "llm-openai-yellow": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "llm-openllm": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "llm-openllm-text": { "body": "", @@ -381,21 +472,29 @@ "height": 25 }, "llm-replicate": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "llm-replicate-text": { "body": "", - "width": 92 + "width": 92, + "height": 24 }, "llm-xorbits-inference": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "llm-xorbits-inference-text": { "body": "", - "width": 152 + "width": 152, + "height": 24 }, "llm-zhipuai": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "llm-zhipuai-text": { "body": "", @@ -416,7 +515,9 @@ "height": 12 }, "other-default-tool-icon": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "other-icon-3-dots": { "body": "", @@ -424,7 +525,9 @@ "height": 16 }, "other-message-3-fill": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "other-row-struct": { "body": "", @@ -447,16 +550,24 @@ "height": 24 }, "plugins-partner-dark": { - "body": "" + "body": "", + "width": 20, + "height": 20 }, "plugins-partner-light": { - "body": "" + "body": "", + "width": 20, + "height": 20 }, "plugins-verified-dark": { - "body": "" + "body": "", + "width": 20, + "height": 20 }, "plugins-verified-light": { - "body": "" + "body": "", + "width": 20, + "height": 20 }, "plugins-web-reader": { "body": "", @@ -469,19 +580,29 @@ "height": 24 }, "thought-data-set": { - "body": "" + "body": "", + "width": 12, + "height": 12 }, "thought-loading": { - "body": "" + "body": "", + "width": 12, + "height": 12 }, "thought-search": { - "body": "" + "body": "", + "width": 12, + "height": 12 }, "thought-thought-list": { - "body": "" + "body": "", + "width": 12, + "height": 12 }, "thought-web-reader": { - "body": "" + "body": "", + "width": 12, + "height": 12 }, "tracing-aliyun-icon": { "body": "", @@ -493,7 +614,8 @@ "height": 24 }, "tracing-arize-icon": { - "body": "" + "body": "", + "width": 74 }, "tracing-arize-icon-big": { "body": "", @@ -510,7 +632,8 @@ "height": 24 }, "tracing-langfuse-icon": { - "body": "" + "body": "", + "width": 74 }, "tracing-langfuse-icon-big": { "body": "", @@ -546,7 +669,8 @@ "height": 24 }, "tracing-phoenix-icon": { - "body": "" + "body": "", + "width": 74 }, "tracing-phoenix-icon-big": { "body": "", diff --git a/packages/iconify-collections/custom-vender/icons.json b/packages/iconify-collections/custom-vender/icons.json index f85c44d912..bf08a79505 100644 --- a/packages/iconify-collections/custom-vender/icons.json +++ b/packages/iconify-collections/custom-vender/icons.json @@ -1,36 +1,156 @@ { "prefix": "custom-vender", - "lastModified": 1781036531, + "lastModified": 1781246368, "icons": { "features-citations": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "features-content-moderation": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "features-document": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "features-folder-upload": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "features-love-message": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "features-message-fast": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "features-microphone-01": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "features-text-to-audio": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "features-virtual-assistant": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "features-vision": { - "body": "" + "body": "", + "width": 24, + "height": 24 + }, + "integrations-agent-strategy": { + "body": "", + "width": 15.3333, + "height": 14.6667 + }, + "integrations-agent-strategy-active": { + "body": "", + "width": 15.3333, + "height": 14.6667 + }, + "integrations-api-extension": { + "body": "", + "width": 14, + "height": 12.9447 + }, + "integrations-api-extension-active": { + "body": "", + "width": 16, + "height": 16 + }, + "integrations-custom-tool": { + "body": "", + "width": 12.6667, + "height": 14.6667 + }, + "integrations-custom-tool-active": { + "body": "", + "width": 12.6667, + "height": 14.2807 + }, + "integrations-extension": { + "body": "", + "width": 12, + "height": 13.3333 + }, + "integrations-extension-active": { + "body": "", + "width": 12, + "height": 13.3333 + }, + "integrations-install-drop": { + "body": "", + "width": 13.3333, + "height": 13.3333 + }, + "integrations-install-github": { + "body": "", + "width": 11.6416, + "height": 13.086 + }, + "integrations-install-local-package": { + "body": "", + "width": 12, + "height": 13.3333 + }, + "integrations-install-marketplace": { + "body": "", + "width": 14.6667, + "height": 13.3333 + }, + "integrations-mcp": { + "body": "", + "width": 13.4445, + "height": 14.6667 + }, + "integrations-panel-left": { + "body": "", + "width": 14.5, + "height": 14.5 + }, + "integrations-tools": { + "body": "", + "width": 12.3333, + "height": 14 + }, + "integrations-tools-active": { + "body": "", + "width": 12.3333, + "height": 14 + }, + "integrations-trigger": { + "body": "", + "width": 13.325, + "height": 13.325 + }, + "integrations-trigger-active": { + "body": "", + "width": 13.325, + "height": 13.325 + }, + "integrations-workflow-as-tool": { + "body": "", + "width": 16, + "height": 16 + }, + "integrations-workflow-as-tool-active": { + "body": "", + "width": 12.1, + "height": 11.4333 }, "knowledge-add-chunks": { "body": "", @@ -62,7 +182,8 @@ }, "knowledge-economic": { "body": "", - "height": 18 + "height": 18, + "width": 18 }, "knowledge-full-text-search": { "body": "", @@ -70,11 +191,13 @@ }, "knowledge-general-chunk": { "body": "", - "height": 18 + "height": 18, + "width": 18 }, "knowledge-high-quality": { "body": "", - "height": 18 + "height": 18, + "width": 18 }, "knowledge-hybrid-search": { "body": "", @@ -82,11 +205,13 @@ }, "knowledge-parent-child-chunk": { "body": "", - "height": 18 + "height": 18, + "width": 18 }, "knowledge-question-and-answer": { "body": "", - "height": 18 + "height": 18, + "width": 18 }, "knowledge-search-lines-sparkle": { "body": "", @@ -121,7 +246,9 @@ "height": 16 }, "line-arrows-arrow-up-right": { - "body": "" + "body": "", + "width": 14, + "height": 14 }, "line-arrows-chevron-down-double": { "body": "", @@ -129,7 +256,9 @@ "height": 13 }, "line-arrows-chevron-right": { - "body": "" + "body": "", + "width": 14, + "height": 14 }, "line-arrows-chevron-selector-vertical": { "body": "", @@ -137,7 +266,9 @@ "height": 24 }, "line-arrows-iconr": { - "body": "" + "body": "", + "width": 14, + "height": 14 }, "line-arrows-refresh-ccw-01": { "body": "", @@ -155,10 +286,14 @@ "height": 16 }, "line-communication-ai-text": { - "body": "" + "body": "", + "width": 14, + "height": 14 }, "line-communication-chat-bot": { - "body": "" + "body": "", + "width": 14, + "height": 14 }, "line-communication-chat-bot-slim": { "body": "", @@ -166,7 +301,9 @@ "height": 48 }, "line-communication-cute-robot": { - "body": "" + "body": "", + "width": 14, + "height": 14 }, "line-communication-message-check-remove": { "body": "", @@ -230,7 +367,9 @@ "body": "" }, "line-editor-align-left": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "line-editor-bezier-curve-03": { "body": "", @@ -243,16 +382,24 @@ "height": 16 }, "line-editor-colors": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "line-editor-image-indent-left": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "line-editor-left-indent-02": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "line-editor-letter-spacing-01": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "line-editor-type-square": { "body": "", @@ -306,10 +453,14 @@ "height": 14 }, "line-financeAndECommerce-balance": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "line-financeAndECommerce-coins-stacked-01": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "line-financeAndECommerce-credits-coin": { "body": "", @@ -322,7 +473,9 @@ "height": 16 }, "line-financeAndECommerce-receipt-list": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "line-financeAndECommerce-tag-01": { "body": "", @@ -469,10 +622,14 @@ "body": "" }, "line-layout-align-left-01": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "line-layout-align-right-01": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "line-layout-grid-01": { "body": "", @@ -480,7 +637,9 @@ "height": 16 }, "line-layout-layout-grid-02": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "line-mediaAndDevices-microphone-01": { "body": "" @@ -514,7 +673,7 @@ "height": 14 }, "line-others-dhs": { - "body": "", + "body": "", "width": 18, "height": 18 }, @@ -522,7 +681,7 @@ "body": "" }, "line-others-dvs": { - "body": "", + "body": "", "width": 18, "height": 18 }, @@ -530,7 +689,7 @@ "body": "" }, "line-others-evaluation": { - "body": "", + "body": "", "width": 18, "height": 18 }, @@ -592,21 +751,100 @@ "width": 24, "height": 24 }, + "main-nav-app-home": { + "body": "", + "width": 16, + "height": 16 + }, + "main-nav-credits": { + "body": "", + "width": 24, + "height": 24 + }, + "main-nav-help": { + "body": "", + "width": 24, + "height": 24 + }, + "main-nav-home": { + "body": "", + "width": 20, + "height": 20 + }, + "main-nav-home-active": { + "body": "", + "width": 20, + "height": 20 + }, + "main-nav-integrations": { + "body": "", + "width": 20, + "height": 20 + }, + "main-nav-integrations-active": { + "body": "", + "width": 20, + "height": 20 + }, + "main-nav-knowledge": { + "body": "", + "width": 20, + "height": 20 + }, + "main-nav-knowledge-active": { + "body": "", + "width": 20, + "height": 20 + }, + "main-nav-marketplace": { + "body": "", + "width": 20, + "height": 20 + }, + "main-nav-marketplace-active": { + "body": "", + "width": 20, + "height": 20 + }, + "main-nav-quick-search": { + "body": "", + "width": 24, + "height": 24 + }, + "main-nav-studio": { + "body": "", + "width": 20, + "height": 20 + }, + "main-nav-studio-active": { + "body": "", + "width": 20, + "height": 20 + }, + "main-nav-workspace-settings": { + "body": "", + "width": 16, + "height": 16 + }, "other-anthropic-text": { "body": "", "width": 90, "height": 20 }, "other-generator": { - "body": "" + "body": "", + "width": 14, + "height": 14 }, "other-group": { "body": "", - "height": 16 + "height": 16, + "width": 14 }, "other-hourglass-shape": { "body": "", - "width": 8 + "width": 8, + "height": 14 }, "other-mcp": { "body": "", @@ -639,10 +877,14 @@ "height": 16 }, "pipeline-pipeline-fill": { - "body": "" + "body": "", + "width": 18, + "height": 18 }, "pipeline-pipeline-line": { - "body": "" + "body": "", + "width": 18, + "height": 18 }, "plugin-box-sparkle-fill": { "body": "", @@ -658,10 +900,14 @@ "body": "" }, "solid-FinanceAndECommerce-gold-coin": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "solid-FinanceAndECommerce-scales-02": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "solid-alertsAndFeedback-alert-triangle": { "body": "", @@ -688,10 +934,14 @@ "height": 24 }, "solid-communication-ai-text": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "solid-communication-bubble-text-mod": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "solid-communication-chat-bot": { "body": "", @@ -699,22 +949,34 @@ "height": 12 }, "solid-communication-cute-robot": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "solid-communication-edit-list": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "solid-communication-list-sparkle": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "solid-communication-logic": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "solid-communication-message-dots-circle": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "solid-communication-message-fast": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "solid-communication-message-heart-circle": { "body": "", @@ -784,7 +1046,9 @@ "height": 24 }, "solid-editor-brush-01": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "solid-editor-citations": { "body": "", @@ -792,13 +1056,19 @@ "height": 16 }, "solid-editor-colors": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "solid-editor-paragraph": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "solid-editor-type-square": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "solid-education-beaker-02": { "body": "", @@ -806,13 +1076,19 @@ "height": 12 }, "solid-education-bubble-text": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "solid-education-heart-02": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "solid-education-unblur": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "solid-files-file-05": { "body": "" @@ -842,10 +1118,14 @@ "height": 16 }, "solid-general-check-done-01": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "solid-general-download-02": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "solid-general-edit-03": { "body": "", @@ -853,10 +1133,14 @@ "height": 12 }, "solid-general-edit-04": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "solid-general-eye": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "solid-general-github": { "body": "", @@ -869,7 +1153,9 @@ "height": 16 }, "solid-general-plus-circle": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "solid-general-question-triangle": { "body": "", @@ -877,10 +1163,14 @@ "height": 12 }, "solid-general-search-md": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "solid-general-target-04": { - "body": "" + "body": "", + "width": 24, + "height": 24 }, "solid-general-tool-03": { "body": "", @@ -906,19 +1196,29 @@ "body": "" }, "solid-mediaAndDevices-audio-support-icon": { - "body": "" + "body": "", + "width": 12, + "height": 12 }, "solid-mediaAndDevices-document-support-icon": { - "body": "" + "body": "", + "width": 12, + "height": 12 }, "solid-mediaAndDevices-magic-box": { - "body": "" + "body": "", + "width": 12, + "height": 12 }, "solid-mediaAndDevices-magic-eyes": { - "body": "" + "body": "", + "width": 12, + "height": 12 }, "solid-mediaAndDevices-magic-wand": { - "body": "" + "body": "", + "width": 12, + "height": 12 }, "solid-mediaAndDevices-microphone-01": { "body": "", @@ -926,10 +1226,14 @@ "height": 16 }, "solid-mediaAndDevices-play": { - "body": "" + "body": "", + "width": 12, + "height": 12 }, "solid-mediaAndDevices-robot": { - "body": "" + "body": "", + "width": 12, + "height": 12 }, "solid-mediaAndDevices-sliders-02": { "body": "", @@ -947,7 +1251,9 @@ "height": 20 }, "solid-mediaAndDevices-video-support-icon": { - "body": "" + "body": "", + "width": 12, + "height": 12 }, "solid-security-lock-01": { "body": "", @@ -994,7 +1300,9 @@ "height": 16 }, "workflow-answer": { - "body": "" + "body": "", + "width": 14, + "height": 14 }, "workflow-api-aggregate": { "body": "", @@ -1007,16 +1315,24 @@ "height": 16 }, "workflow-asterisk": { - "body": "" + "body": "", + "width": 14, + "height": 14 }, "workflow-calendar-check-line": { - "body": "" + "body": "", + "width": 14, + "height": 14 }, "workflow-code": { - "body": "" + "body": "", + "width": 14, + "height": 14 }, "workflow-datasource": { - "body": "" + "body": "", + "width": 14, + "height": 14 }, "workflow-docs-extractor": { "body": "", @@ -1024,13 +1340,19 @@ "height": 16 }, "workflow-end": { - "body": "" + "body": "", + "width": 14, + "height": 14 }, "workflow-home": { - "body": "" + "body": "", + "width": 14, + "height": 14 }, "workflow-http": { - "body": "" + "body": "", + "width": 14, + "height": 14 }, "workflow-human-in-loop": { "body": "", @@ -1038,15 +1360,19 @@ "height": 16 }, "workflow-if-else": { - "body": "" + "body": "", + "width": 14, + "height": 14 }, "workflow-input-field": { - "body": "", + "body": "", "width": 16, "height": 16 }, "workflow-iteration": { - "body": "" + "body": "", + "width": 14, + "height": 14 }, "workflow-iteration-start": { "body": "", @@ -1059,7 +1385,9 @@ "height": 12 }, "workflow-knowledge-base": { - "body": "" + "body": "", + "width": 14, + "height": 14 }, "workflow-knowledge-retrieval": { "body": "", @@ -1072,7 +1400,9 @@ "height": 16 }, "workflow-llm": { - "body": "" + "body": "", + "width": 14, + "height": 14 }, "workflow-loop": { "body": "", @@ -1090,10 +1420,14 @@ "height": 12 }, "workflow-parameter-extractor": { - "body": "" + "body": "", + "width": 14, + "height": 14 }, "workflow-question-classifier": { - "body": "" + "body": "", + "width": 14, + "height": 14 }, "workflow-schedule": { "body": "", @@ -1106,10 +1440,14 @@ "height": 13.3333 }, "workflow-templating-transform": { - "body": "" + "body": "", + "width": 14, + "height": 14 }, "workflow-trigger-all": { - "body": "" + "body": "", + "width": 14, + "height": 14 }, "workflow-user-input": { "body": "", @@ -1117,7 +1455,9 @@ "height": 16 }, "workflow-variable-x": { - "body": "" + "body": "", + "width": 14, + "height": 14 }, "workflow-webhook-line": { "body": "", diff --git a/packages/iconify-collections/custom-vender/info.json b/packages/iconify-collections/custom-vender/info.json index f08b18fcad..cef352ac54 100644 --- a/packages/iconify-collections/custom-vender/info.json +++ b/packages/iconify-collections/custom-vender/info.json @@ -1,7 +1,7 @@ { "prefix": "custom-vender", "name": "Dify Custom Vender", - "total": 284, + "total": 319, "version": "0.0.0-private", "author": { "name": "LangGenius, Inc.", diff --git a/packages/iconify-collections/package.json b/packages/iconify-collections/package.json index 752b7ce437..eeec58e1fa 100644 --- a/packages/iconify-collections/package.json +++ b/packages/iconify-collections/package.json @@ -23,6 +23,7 @@ "./custom-vender/chars.json": "./custom-vender/chars.json" }, "scripts": { + "check:dimensions": "tsx ./scripts/check-icon-dimensions.ts", "generate": "tsx ./scripts/generate-collections.ts" }, "devDependencies": { diff --git a/packages/iconify-collections/scripts/check-icon-dimensions.ts b/packages/iconify-collections/scripts/check-icon-dimensions.ts new file mode 100644 index 0000000000..ab933fdf29 --- /dev/null +++ b/packages/iconify-collections/scripts/check-icon-dimensions.ts @@ -0,0 +1,94 @@ +import { readFile } from 'node:fs/promises' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +type IconData = { + width?: number + height?: number +} + +type IconCollection = { + icons: Record + width?: number + height?: number +} + +type DimensionRule = { + collection: 'custom-vender' + icons: string[] + width: number + height: number +} + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const packageDir = path.resolve(__dirname, '..') + +const dimensionRules: DimensionRule[] = [ + { + collection: 'custom-vender', + icons: [ + 'main-nav-home', + 'main-nav-home-active', + 'main-nav-integrations', + 'main-nav-integrations-active', + 'main-nav-knowledge', + 'main-nav-knowledge-active', + 'main-nav-marketplace', + 'main-nav-marketplace-active', + 'main-nav-studio', + 'main-nav-studio-active', + ], + width: 20, + height: 20, + }, +] + +const readCollection = async (collection: DimensionRule['collection']): Promise => { + return JSON.parse( + await readFile(path.resolve(packageDir, collection, 'icons.json'), 'utf8'), + ) as IconCollection +} + +const main = async () => { + const collections = new Map() + const failures: string[] = [] + + for (const rule of dimensionRules) { + if (!collections.has(rule.collection)) + collections.set(rule.collection, await readCollection(rule.collection)) + + const collection = collections.get(rule.collection)! + + for (const iconName of rule.icons) { + const icon = collection.icons[iconName] + const width = icon?.width ?? collection.width ?? 16 + const height = icon?.height ?? collection.height ?? 16 + + if (!icon) { + failures.push(`${rule.collection}:${iconName} is missing`) + continue + } + + if (width !== rule.width || height !== rule.height) { + failures.push( + `${rule.collection}:${iconName} expected ${rule.width}x${rule.height}, got ${width}x${height}`, + ) + } + } + } + + if (failures.length) { + console.error('Icon dimension check failed:') + for (const failure of failures) + console.error(`- ${failure}`) + process.exitCode = 1 + return + } + + console.log('Icon dimension check passed.') +} + +main().catch((error: unknown) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/packages/iconify-collections/scripts/generate-collections.ts b/packages/iconify-collections/scripts/generate-collections.ts index 5cc67dd588..0a4d969fff 100644 --- a/packages/iconify-collections/scripts/generate-collections.ts +++ b/packages/iconify-collections/scripts/generate-collections.ts @@ -21,6 +21,8 @@ type AliasData = Omit & { type ImportedCollection = { icons?: Record aliases?: Record + width?: number + height?: number lastModified?: number } @@ -60,11 +62,17 @@ const flattenCollections = (collections: ImportedCollections, prefix: string) => const segment = collectionKey.slice(prefix.length + 1) const namePrefix = segment ? `${segment}-` : '' + const applyCollectionSize = (iconData: T): T => ({ + ...iconData, + ...(iconData.width === undefined && collection.width !== undefined ? { width: collection.width } : {}), + ...(iconData.height === undefined && collection.height !== undefined ? { height: collection.height } : {}), + }) + for (const [iconName, iconData] of Object.entries(collection.icons ?? {})) - icons[`${namePrefix}${iconName}`] = iconData + icons[`${namePrefix}${iconName}`] = applyCollectionSize(iconData) for (const [aliasName, aliasData] of Object.entries(collection.aliases ?? {})) - aliases[`${namePrefix}${aliasName}`] = aliasData + aliases[`${namePrefix}${aliasName}`] = applyCollectionSize(aliasData) if (typeof collection.lastModified === 'number') lastModified = Math.max(lastModified, collection.lastModified) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 044c3cb391..9577b53f99 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -303,6 +303,9 @@ catalogs: embla-carousel-autoplay: specifier: 8.6.0 version: 8.6.0 + embla-carousel-fade: + specifier: 8.6.0 + version: 8.6.0 embla-carousel-react: specifier: 8.6.0 version: 8.6.0 @@ -426,6 +429,9 @@ catalogs: mitt: specifier: 3.0.1 version: 3.0.1 + motion: + specifier: 12.40.0 + version: 12.40.0 negotiator: specifier: 1.0.0 version: 1.0.0 @@ -1124,6 +1130,9 @@ importers: embla-carousel-autoplay: specifier: 'catalog:' version: 8.6.0(embla-carousel@8.6.0) + embla-carousel-fade: + specifier: 'catalog:' + version: 8.6.0(embla-carousel@8.6.0) embla-carousel-react: specifier: 'catalog:' version: 8.6.0(react@19.2.7) @@ -1199,6 +1208,9 @@ importers: mitt: specifier: 'catalog:' version: 3.0.1 + motion: + specifier: 'catalog:' + version: 12.40.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) negotiator: specifier: 'catalog:' version: 1.0.0 @@ -1728,7 +1740,6 @@ packages: '@babel/parser@7.29.2': resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} engines: {node: '>=6.0.0'} - hasBin: true '@babel/runtime@7.29.2': resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} @@ -1843,7 +1854,6 @@ packages: '@cucumber/gherkin-utils@11.0.0': resolution: {integrity: sha512-LJ+s4+TepHTgdKWDR4zbPyT7rQjmYIcukTwNbwNwgqr6i8Gjcmzf6NmtbYDA19m1ZFg6kWbFsmHnj37ZuX+kZA==} - hasBin: true '@cucumber/gherkin@38.0.0': resolution: {integrity: sha512-duEXK+KDfQUzu3vsSzXjkxQ2tirF5PRsc1Xrts6THKHJO6mjw4RjM8RV+vliuDasmhhrmdLcOcM7d9nurNTJKw==} @@ -3784,7 +3794,6 @@ packages: '@playwright/test@1.60.0': resolution: {integrity: sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==} engines: {node: '>=18'} - hasBin: true '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} @@ -4119,7 +4128,6 @@ packages: '@shuding/opentype.js@1.4.0-beta.0': resolution: {integrity: sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==} engines: {node: '>= 8.0.0'} - hasBin: true '@sindresorhus/base62@1.0.0': resolution: {integrity: sha512-TeheYy0ILzBEI/CO55CP6zJCSdSWeRtGnHy8U8dWSUH4I68iqTsy7HkMktR4xakThc9jotkPQUXT4ITdbV7cHA==} @@ -4412,7 +4420,6 @@ packages: '@tanstack/devtools-event-client@0.4.3': resolution: {integrity: sha512-OZI6QyULw0FI0wjgmeYzCIfbgPsOEzwJtCpa69XrfLMtNXLGnz3d/dIabk7frg0TmHo+Ah49w5I4KC7Tufwsvw==} engines: {node: '>=18'} - hasBin: true '@tanstack/eslint-plugin-query@5.101.0': resolution: {integrity: sha512-wsfg821y4yw21J7nKI2oM5yyGSz3vASXqgWbmWCXZpnyY9ObLrBCcXivwZKj4YHF2fUWiqoOIRX2pbE79cf6gQ==} @@ -5167,7 +5174,6 @@ packages: acorn@8.16.0: resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} engines: {node: '>=0.4.0'} - hasBin: true agentation@3.0.2: resolution: {integrity: sha512-iGzBxFVTuZEIKzLY6AExSLAQH6i6SwxV4pAu7v7m3X6bInZ7qlZXAwrEqyc4+EfP4gM7z2RXBF6SF4DeH0f2lA==} @@ -5274,7 +5280,6 @@ packages: astring@1.9.0: resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} - hasBin: true async-function@1.0.0: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} @@ -5319,7 +5324,6 @@ packages: baseline-browser-mapping@2.10.12: resolution: {integrity: sha512-qyq26DxfY4awP2gIRXhhLWfwzwI+N5Nxk6iQi8EFizIaWIjqicQTE4sLnZZVdeKPRcVNoJOkkpfzoIYuvCKaIQ==} engines: {node: '>=6.0.0'} - hasBin: true birecord@0.1.1: resolution: {integrity: sha512-VUpsf/qykW0heRlC8LooCq28Kxn3mAqKohhDG/49rrsQ1dT1CXyj/pgXS+5BSRzFTR/3DyIBOqQOrGyZOh71Aw==} @@ -5347,7 +5351,6 @@ packages: browserslist@4.28.1: resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} @@ -5567,7 +5570,6 @@ packages: color-support@1.1.3: resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} - hasBin: true comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} @@ -5678,7 +5680,6 @@ packages: cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} - hasBin: true cssfontparser@1.2.1: resolution: {integrity: sha512-6tun4LoZnj7VN6YeegOVb67KBX/7JJsqvj+pv3ZA7F878/eN33AbGa5b/S/wXxS/tcp8nc40xRUrsPlxIyNUPg==} @@ -5746,7 +5747,6 @@ packages: d3-dsv@3.0.1: resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} engines: {node: '>=12'} - hasBin: true d3-ease@3.0.1: resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} @@ -6011,6 +6011,11 @@ packages: peerDependencies: embla-carousel: 8.6.0 + embla-carousel-fade@8.6.0: + resolution: {integrity: sha512-qaYsx5mwCz72ZrjlsXgs1nKejSrW+UhkbOMwLgfRT7w2LtdEB03nPRI06GHuHv5ac2USvbEiX2/nAHctcDwvpg==} + peerDependencies: + embla-carousel: 8.6.0 + embla-carousel-react@8.6.0: resolution: {integrity: sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==} peerDependencies: @@ -6123,7 +6128,6 @@ packages: esbuild@0.28.1: resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} engines: {node: '>=18'} - hasBin: true escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} @@ -6494,7 +6498,6 @@ packages: extract-zip@2.0.1: resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} engines: {node: '>= 10.17.0'} - hasBin: true fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -6597,6 +6600,20 @@ packages: react-dom: optional: true + framer-motion@12.40.0: + resolution: {integrity: sha512-uaBd3qC1v3KQqBEjwTUd183K6PbS+j0yR9w9VmEOLWA/tnUcSn8Xa3uck7t4dgpDoUss8xQTcj8W2L07lrnLFg==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} @@ -6668,7 +6685,6 @@ packages: giget@3.2.0: resolution: {integrity: sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A==} - hasBin: true github-from-package@0.0.0: resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} @@ -6878,7 +6894,6 @@ packages: image-size@2.0.2: resolution: {integrity: sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==} engines: {node: '>=16.x'} - hasBin: true immer@11.1.8: resolution: {integrity: sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA==} @@ -6975,7 +6990,6 @@ packages: is-docker@3.0.0: resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - hasBin: true is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} @@ -7003,7 +7017,6 @@ packages: is-inside-container@1.0.0: resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} engines: {node: '>=14.16'} - hasBin: true is-installed-globally@1.0.0: resolution: {integrity: sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==} @@ -7145,11 +7158,9 @@ packages: js-yaml@4.1.1: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} - hasBin: true js-yaml@4.2.0: resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==} - hasBin: true jsdoc-type-pratt-parser@7.1.1: resolution: {integrity: sha512-/2uqY7x6bsrpi3i9LVU6J89352C0rpMk0as8trXxCtvd4kPk1ke/Eyif6wqfSLvoNJqcDG9Vk4UsXgygzCt2xA==} @@ -7162,7 +7173,6 @@ packages: jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} - hasBin: true json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -7198,11 +7208,9 @@ packages: katex@0.16.47: resolution: {integrity: sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==} - hasBin: true katex@0.17.0: resolution: {integrity: sha512-Vdw0ATsQ9V+LuegM/BTwQqV/6cTl5lbGcIrU+BCgLxyf6bo38ybOr372tuSIxir3CN720flu1meYR6XzNMwQnw==} - hasBin: true keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -7371,7 +7379,6 @@ packages: loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} - hasBin: true loro-crdt@1.13.2: resolution: {integrity: sha512-Br9tZZk9x/HP83By9RvOCqzWh8v8tnOhVlR6/ibYNtLSmysO7ZgwzjNpqsCABqaSOcGC7TBkx5sG8tfosdJMQA==} @@ -7392,7 +7399,6 @@ packages: lz-string@1.5.0: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} - hasBin: true magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -7414,12 +7420,10 @@ packages: marked@16.4.2: resolution: {integrity: sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==} engines: {node: '>= 20'} - hasBin: true marked@17.0.5: resolution: {integrity: sha512-6hLvc0/JEbRjRgzI6wnT2P1XuM1/RrrDEX0kPt0N7jGm1133g6X7DlxFasUIx+72aKAr904GTxhSLDrd5DIlZg==} engines: {node: '>= 20'} - hasBin: true math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} @@ -7622,12 +7626,10 @@ packages: mime@3.0.0: resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} engines: {node: '>=10.0.0'} - hasBin: true mime@4.1.0: resolution: {integrity: sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==} engines: {node: '>=16'} - hasBin: true mimic-function@5.0.1: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} @@ -7668,7 +7670,6 @@ packages: mkdirp@3.0.1: resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} engines: {node: '>=10'} - hasBin: true mlly@1.8.2: resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} @@ -7682,6 +7683,26 @@ packages: moo-color@1.0.3: resolution: {integrity: sha512-i/+ZKXMDf6aqYtBhuOcej71YSlbjT3wCO/4H1j8rPvxDJEifdwgg5MaFyu6iYAT8GBZJg2z0dkgK4YMzvURALQ==} + motion-dom@12.40.0: + resolution: {integrity: sha512-HxU3ZaBwNPVQUBQf1xxgq+7JrPNZvjLVxgbpEZL7RrWJnsxOf0/OM+yrHG9ogLQ31Do/r57Oz2gQWPK+6q62mg==} + + motion-utils@12.39.0: + resolution: {integrity: sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==} + + motion@12.40.0: + resolution: {integrity: sha512-yjrHUrBFW6kQvjJwRsoiPSAhC5tRwRqNGJWmiJ4CrGnbKp0V88AdzkhBmDoqIsIPfarOe0Uddd37Xq43/gIocA==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} @@ -7692,7 +7713,6 @@ packages: nanoid@3.3.12: resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true napi-build-utils@2.0.0: resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} @@ -7888,7 +7908,6 @@ packages: os: linux libc: musl version: 22.22.3 - hasBin: true normalize-package-data@8.0.0: resolution: {integrity: sha512-RWk+PI433eESQ7ounYxIp67CYuVsS1uYSonX3kA6ps/3LWfjVQa/ptEg6Y3T6uAMq1mWpX9PQ+qx+QaHpsc7gQ==} @@ -8023,7 +8042,6 @@ packages: oxlint-tsgolint@0.23.0: resolution: {integrity: sha512-3mBv3CoPbh8dFbzfDGIWa2ytZjn2v+3EX4aKRXjIhsoGFzG8GCjfRirz3rwZf1wYbZzsNLTSgpw8VjQuWdp/jA==} - hasBin: true oxlint@1.67.0: resolution: {integrity: sha512-blwwaHPdoH8piQ5/z0KHeoHFR7FZgl12WluKJfu4qFLPkZl6mK04PkLE45Fw1NxfBRSlh40Gu7MkxHUw++ociQ==} @@ -8160,12 +8178,10 @@ packages: playwright-core@1.60.0: resolution: {integrity: sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==} engines: {node: '>=18'} - hasBin: true playwright@1.60.0: resolution: {integrity: sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==} engines: {node: '>=18'} - hasBin: true pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} @@ -8215,7 +8231,6 @@ packages: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. - hasBin: true prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} @@ -8265,7 +8280,6 @@ packages: rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} - hasBin: true re-resizable@6.11.2: resolution: {integrity: sha512-2xI2P3OHs5qw7K0Ud1aLILK6MQxW50TcO+DetD9eIV58j84TqYeHoZcL9H4GXFXXIh7afhH8mv5iUCXII7OW7A==} @@ -8480,7 +8494,6 @@ packages: regexp-tree@0.1.27: resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} - hasBin: true regexp.prototype.flags@1.5.4: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} @@ -8488,7 +8501,6 @@ packages: regjsparser@0.13.0: resolution: {integrity: sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==} - hasBin: true rehype-harden@1.1.8: resolution: {integrity: sha512-Qn7vR1xrf6fZCrkm9TDWi/AB4ylrHy+jqsNm1EHOAmbARYA6gsnVJBq/sdBh6kmT4NEZxH5vgIjrscefJAOXcw==} @@ -8756,7 +8768,6 @@ packages: srvx@0.11.15: resolution: {integrity: sha512-iXsux0UcOjdvs0LCMa2Ws3WwcDUozA3JN3BquNXkaFPP7TpRqgunKdEgoZ/uwb1J6xaYHfxtz9Twlh6yzwM6Tg==} engines: {node: '>=20.16.0'} - hasBin: true stackframe@1.3.4: resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} @@ -8900,7 +8911,6 @@ packages: svgo@3.3.3: resolution: {integrity: sha512-+wn7I4p7YgJhHs38k2TNjy1vCfPIfLIJWR5MnCStsN8WuuTcBnRKcMHQLMM2ijxGZmDoZwNv8ipl5aTTen62ng==} engines: {node: '>=14.0.0'} - hasBin: true synckit@0.11.12: resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==} @@ -8987,7 +8997,6 @@ packages: tldts@7.4.2: resolution: {integrity: sha512-kCwffuaH8ntKtygnWe1b4BJKWiCUH30n5KfoTr6IchcXOwR7chAOFJxFrH3vjANafUYrIA4a7SDL+nn7SiR4Sw==} - hasBin: true to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} @@ -9010,7 +9019,6 @@ packages: tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} - hasBin: true trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -9064,7 +9072,6 @@ packages: tsx@4.22.4: resolution: {integrity: sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==} engines: {node: '>=18.0.0'} - hasBin: true tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} @@ -9107,7 +9114,6 @@ packages: typescript@6.0.3: resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} engines: {node: '>=14.17'} - hasBin: true ufo@1.6.3: resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} @@ -9115,7 +9121,6 @@ packages: uglify-js@3.19.3: resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} engines: {node: '>=0.8.0'} - hasBin: true unbash@3.0.0: resolution: {integrity: sha512-FeFPZ/WFT0mbRCuydiZzpPFlrYN8ZUpphQKoq4EeElVIYjYyGzPMxQR/simUwCOJIyVhpFk4RbtyO7RuMpMnHA==} @@ -9128,6 +9133,10 @@ packages: undici-types@7.24.6: resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + undici@7.26.0: + resolution: {integrity: sha512-3O9Tf67pGhgOv9jM35AbhkXAKi13f3oy3aE4CSgr+TckGeY+/iu97ZXN+J7DpHPzLbVApFd1IFhcnBjREYXYcg==} + engines: {node: '>=20.18.1'} + undici@7.27.2: resolution: {integrity: sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA==} engines: {node: '>=20.18.1'} @@ -9263,7 +9272,6 @@ packages: uuid@14.0.0: resolution: {integrity: sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==} - hasBin: true valibot@1.4.1: resolution: {integrity: sha512-klCmFTz2jeDluy9RwX+F884TCiogtdBJ/YaxSx1EOBYXa3NXNWj8kR1jjN8rzluwojJVWWaHJ4r1U5LfICnM3g==} @@ -9331,7 +9339,6 @@ packages: vite-plus@0.1.24: resolution: {integrity: sha512-b3fr6WtCiEhetjuzW/4KcEMOAMuZxoxZATWaXKmPzOLf1upG+pzKJOFZTb94D6wiPBlwcjxoaUtF7C3uAN+VjQ==} engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true vite-tsconfig-paths@5.1.4: resolution: {integrity: sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==} @@ -9438,7 +9445,6 @@ packages: which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} - hasBin: true word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} @@ -13489,7 +13495,7 @@ snapshots: parse5: 7.3.0 parse5-htmlparser2-tree-adapter: 7.1.0 parse5-parser-stream: 7.1.2 - undici: 7.27.2 + undici: 7.26.0 whatwg-mimetype: 4.0.0 chokidar@5.0.0: @@ -14032,6 +14038,10 @@ snapshots: dependencies: embla-carousel: 8.6.0 + embla-carousel-fade@8.6.0(embla-carousel@8.6.0): + dependencies: + embla-carousel: 8.6.0 + embla-carousel-react@8.6.0(react@19.2.7): dependencies: embla-carousel: 8.6.0 @@ -14861,6 +14871,15 @@ snapshots: react: 19.2.7 react-dom: 19.2.7(react@19.2.7) + framer-motion@12.40.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + motion-dom: 12.40.0 + motion-utils: 12.39.0 + tslib: 2.8.1 + optionalDependencies: + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + fs-constants@1.0.0: optional: true @@ -16267,6 +16286,20 @@ snapshots: dependencies: color-name: 1.1.4 + motion-dom@12.40.0: + dependencies: + motion-utils: 12.39.0 + + motion-utils@12.39.0: {} + + motion@12.40.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + framer-motion: 12.40.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + tslib: 2.8.1 + optionalDependencies: + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + mrmime@2.0.1: {} ms@2.1.3: {} @@ -17931,6 +17964,8 @@ snapshots: undici-types@7.24.6: {} + undici@7.26.0: {} + undici@7.27.2: {} unicode-trie@2.0.0: @@ -18585,6 +18620,7 @@ time: echarts@6.1.0: '2026-05-19T17:52:11.076Z' elkjs@0.11.1: '2026-03-03T12:21:48.463Z' embla-carousel-autoplay@8.6.0: '2025-04-04T17:37:46.303Z' + embla-carousel-fade@8.6.0: '2025-04-04T17:37:50.278Z' embla-carousel-react@8.6.0: '2025-04-04T17:37:53.976Z' emoji-mart@5.6.0: '2024-04-25T14:22:21.440Z' es-toolkit@1.47.1: '2026-06-12T07:38:48.983Z' @@ -18627,6 +18663,7 @@ time: mermaid@11.15.0: '2026-05-11T11:15:09.824Z' mime@4.1.0: '2025-09-12T17:53:01.376Z' mitt@3.0.1: '2023-07-04T17:31:47.638Z' + motion@12.40.0: '2026-05-21T12:00:11.274Z' negotiator@1.0.0: '2024-08-31T15:42:18.280Z' next-themes@0.4.6: '2025-03-11T21:02:05.882Z' next@16.2.9: '2026-06-09T23:02:22.464Z' diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 2e40972bfb..e442b5b504 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -146,6 +146,7 @@ catalog: echarts-for-react: 3.0.6 elkjs: 0.11.1 embla-carousel-autoplay: 8.6.0 + embla-carousel-fade: 8.6.0 embla-carousel-react: 8.6.0 emoji-mart: 5.6.0 es-toolkit: 1.47.1 @@ -187,6 +188,7 @@ catalog: mermaid: 11.15.0 mime: 4.1.0 mitt: 3.0.1 + motion: 12.40.0 negotiator: 1.0.0 next: 16.2.9 next-themes: 0.4.6 diff --git a/web/__mocks__/provider-context.ts b/web/__mocks__/provider-context.ts index 10fac8d8b6..38afc2ff0e 100644 --- a/web/__mocks__/provider-context.ts +++ b/web/__mocks__/provider-context.ts @@ -8,6 +8,7 @@ import { defaultPlan } from '@/app/components/billing/config' export const baseProviderContextValue: ProviderContextState = { modelProviders: [], refreshModelProviders: noop, + isLoadingModelProviders: false, textGenerationModelList: [], supportRetrievalMethods: [], isAPIKeySet: true, diff --git a/web/__tests__/app-sidebar/sidebar-shell-flow.test.tsx b/web/__tests__/app-sidebar/sidebar-shell-flow.test.tsx index 053edb8288..0e51c650b2 100644 --- a/web/__tests__/app-sidebar/sidebar-shell-flow.test.tsx +++ b/web/__tests__/app-sidebar/sidebar-shell-flow.test.tsx @@ -154,18 +154,13 @@ describe('App Sidebar Shell Flow', () => { expect(mockSetAppSidebarExpand).toHaveBeenCalledWith('collapse') }) - it('switches to the workflow fullscreen dropdown shell and opens its navigation menu', async () => { + it('keeps the normal sidebar on workflow routes', () => { mockPathname = '/app/app-1/workflow' mockSelectedSegment = 'workflow' - localStorage.setItem('workflow-canvas-maximize', 'true') render() - expect(screen.queryByTestId('app-info')).not.toBeInTheDocument() - - fireEvent.click(screen.getByRole('button', { name: 'operation.more' })) - - expect(await screen.findByText('Demo App')).toBeInTheDocument() + expect(screen.getByTestId('app-info')).toBeInTheDocument() expect(screen.getByRole('link', { name: /Overview/i })).toBeInTheDocument() expect(screen.getByRole('link', { name: /Logs/i })).toBeInTheDocument() }) diff --git a/web/__tests__/app-star-i18n.test.ts b/web/__tests__/app-star-i18n.test.ts new file mode 100644 index 0000000000..c785d7d8c8 --- /dev/null +++ b/web/__tests__/app-star-i18n.test.ts @@ -0,0 +1,47 @@ +import fs from 'node:fs' +import path from 'node:path' + +const I18N_DIR = path.join(__dirname, '../i18n') + +const REQUIRED_APP_STAR_KEYS = [ + 'studio.allApps', + 'studio.starApp', + 'studio.starFailed', + 'studio.starred', + 'studio.unstarApp', +] as const + +type AppTranslations = Record + +const getSupportedLocales = () => fs.readdirSync(I18N_DIR) + .filter(item => fs.statSync(path.join(I18N_DIR, item)).isDirectory()) + .sort() + +const loadAppTranslations = (locale: string): AppTranslations => { + const filePath = path.join(I18N_DIR, locale, 'app.json') + + if (!fs.existsSync(filePath)) + throw new Error(`Translation file not found: ${filePath}`) + + return JSON.parse(fs.readFileSync(filePath, 'utf-8')) as AppTranslations +} + +describe('App star i18n translations', () => { + it('should define star-related app list labels for every locale', () => { + const supportedLocales = getSupportedLocales() + + const missingKeys = supportedLocales.flatMap((locale) => { + const translations = loadAppTranslations(locale) + + return REQUIRED_APP_STAR_KEYS + .filter((key) => { + const value = translations[key] + return typeof value !== 'string' || value.trim() === '' + }) + .map(key => `${locale}:${key}`) + }) + + expect(supportedLocales.length).toBeGreaterThan(0) + expect(missingKeys).toEqual([]) + }) +}) diff --git a/web/__tests__/apps/app-card-operations-flow.test.tsx b/web/__tests__/apps/app-card-operations-flow.test.tsx index 8162f12dad..5702755a5c 100644 --- a/web/__tests__/apps/app-card-operations-flow.test.tsx +++ b/web/__tests__/apps/app-card-operations-flow.test.tsx @@ -100,6 +100,10 @@ vi.mock('@/service/use-apps', () => ({ mutateAsync: mockDeleteAppMutation, isPending: mockDeleteMutationPending, }), + useToggleAppStarMutation: () => ({ + mutateAsync: vi.fn(), + isPending: false, + }), })) vi.mock('@/service/apps', () => ({ @@ -277,21 +281,13 @@ describe('App Card Operations Flow', () => { it('should navigate to app config page when card is clicked', () => { renderAppCard({ id: 'app-123', mode: AppModeEnum.CHAT }) - const card = screen.getByText('Test Chat App').closest('[class*="cursor-pointer"]') - if (card) - fireEvent.click(card) - - expect(mockRouterPush).toHaveBeenCalledWith('/app/app-123/configuration') + expect(screen.getByRole('link', { name: 'Test Chat App' })).toHaveAttribute('href', '/app/app-123/configuration') }) it('should navigate to workflow page for workflow apps', () => { renderAppCard({ id: 'app-wf', mode: AppModeEnum.WORKFLOW, name: 'WF App' }) - const card = screen.getByText('WF App').closest('[class*="cursor-pointer"]') - if (card) - fireEvent.click(card) - - expect(mockRouterPush).toHaveBeenCalledWith('/app/app-wf/workflow') + expect(screen.getByRole('link', { name: 'WF App' })).toHaveAttribute('href', '/app/app-wf/workflow') }) }) diff --git a/web/__tests__/apps/app-list-browsing-flow.test.tsx b/web/__tests__/apps/app-list-browsing-flow.test.tsx index 0c43472020..39d9219be6 100644 --- a/web/__tests__/apps/app-list-browsing-flow.test.tsx +++ b/web/__tests__/apps/app-list-browsing-flow.test.tsx @@ -64,6 +64,7 @@ vi.mock('@/context/app-context', () => ({ isCurrentWorkspaceEditor: mockIsCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator: mockIsCurrentWorkspaceDatasetOperator, isLoadingCurrentWorkspace: mockIsLoadingCurrentWorkspace, + userProfile: { id: 'member-1' }, }), })) @@ -89,6 +90,17 @@ vi.mock('@/service/tag', () => ({ fetchTagList: vi.fn().mockResolvedValue([]), })) +vi.mock('@/service/use-common', () => ({ + useMembers: () => ({ + data: { + accounts: [ + { id: 'member-1', name: 'Alice', avatar_url: null, status: 'active' }, + { id: 'member-2', name: 'Bob', avatar_url: null, status: 'active' }, + ], + }, + }), +})) + vi.mock('@tanstack/react-query', async (importOriginal) => { const actual = await importOriginal() return { @@ -114,6 +126,10 @@ vi.mock('@/service/use-apps', () => ({ mutateAsync: vi.fn(), isPending: false, }), + useToggleAppStarMutation: () => ({ + mutateAsync: vi.fn(), + isPending: false, + }), })) vi.mock('@/app/components/apps/hooks/use-workflow-online-users', () => ({ @@ -229,7 +245,7 @@ describe('App List Browsing Flow', () => { mockPages = [createPage([])] renderList() - expect(screen.getByText('app.newApp.noAppsFound')).toBeInTheDocument() + expect(screen.getByText('app.firstEmpty.title')).toBeInTheDocument() }) it('should transition from loading to content when data loads', () => { @@ -277,17 +293,17 @@ describe('App List Browsing Flow', () => { expect(screen.getByText('A powerful AI assistant')).toBeInTheDocument() }) - it('should show the NewAppCard for workspace editors', () => { + it('should show the create menu for workspace editors', () => { mockPages = [createPage([ createMockApp({ name: 'Test App' }), ])] renderList() - expect(screen.getByText('app.createApp')).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'common.operation.create' })).toBeInTheDocument() }) - it('should hide NewAppCard when user is not a workspace editor', () => { + it('should hide the create menu when user is not a workspace editor', () => { mockIsCurrentWorkspaceEditor = false mockPages = [createPage([ createMockApp({ name: 'Test App' }), @@ -295,20 +311,20 @@ describe('App List Browsing Flow', () => { renderList() - expect(screen.queryByText('app.createApp')).not.toBeInTheDocument() + expect(screen.queryByRole('button', { name: 'common.operation.create' })).not.toBeInTheDocument() }) }) - // -- Footer visibility -- - describe('Footer Visibility', () => { - it('should show footer when branding is disabled', () => { + // -- Legacy footer removal -- + describe('Legacy Footer', () => { + it('should not show the legacy footer when branding is disabled', () => { mockSystemFeatures = { ...mockSystemFeatures, branding: { enabled: false } } mockPages = [createPage([createMockApp()])] renderList() - expect(screen.getByText('app.join')).toBeInTheDocument() - expect(screen.getByText('app.communityIntro')).toBeInTheDocument() + expect(screen.queryByText('app.join')).not.toBeInTheDocument() + expect(screen.queryByText('app.communityIntro')).not.toBeInTheDocument() }) it('should hide footer when branding is enabled', () => { @@ -341,11 +357,18 @@ describe('App List Browsing Flow', () => { // -- Tab navigation -- describe('Tab Navigation', () => { - it('should render the app type dropdown trigger', () => { + it('should render all category options', async () => { mockPages = [createPage([createMockApp()])] renderList() - expect(screen.getByText('app.studio.filters.types')).toBeInTheDocument() + fireEvent.click(screen.getByRole('button', { name: 'app.studio.filters.types' })) + + expect(await screen.findByRole('menuitemradio', { name: 'app.types.all' })).toBeInTheDocument() + expect(await screen.findByRole('menuitemradio', { name: 'app.types.workflow' })).toBeInTheDocument() + expect(await screen.findByRole('menuitemradio', { name: 'app.types.advanced' })).toBeInTheDocument() + expect(await screen.findByRole('menuitemradio', { name: 'app.types.chatbot' })).toBeInTheDocument() + expect(await screen.findByRole('menuitemradio', { name: 'app.types.agent' })).toBeInTheDocument() + expect(await screen.findByRole('menuitemradio', { name: 'app.newApp.completeApp' })).toBeInTheDocument() }) }) @@ -374,21 +397,22 @@ describe('App List Browsing Flow', () => { }) }) - // -- "Created by me" filter -- - describe('Created By Me Filter', () => { - it('should not render a standalone "created by me" checkbox in the current header layout', () => { + // -- Creators filter -- + describe('Creators Filter', () => { + it('should render the creators filter', () => { mockPages = [createPage([createMockApp()])] renderList() - expect(screen.queryByText('app.showMyCreatedAppsOnly')).not.toBeInTheDocument() + expect(screen.getByText('app.studio.filters.creators')).toBeInTheDocument() }) - it('should keep the current layout stable without a "created by me" control', () => { + it('should open the creators filter menu', () => { mockPages = [createPage([createMockApp()])] renderList() - expect(screen.getByText('app.studio.filters.types')).toBeInTheDocument() - expect(screen.queryByText('app.showMyCreatedAppsOnly')).not.toBeInTheDocument() + fireEvent.click(screen.getByRole('button', { name: 'app.studio.filters.creators' })) + + expect(screen.getByRole('button', { name: /Bob/ })).toBeInTheDocument() }) }) diff --git a/web/__tests__/apps/create-app-flow.test.tsx b/web/__tests__/apps/create-app-flow.test.tsx index ba3ab166de..1fcc22c4d2 100644 --- a/web/__tests__/apps/create-app-flow.test.tsx +++ b/web/__tests__/apps/create-app-flow.test.tsx @@ -101,6 +101,10 @@ vi.mock('@/service/use-apps', () => ({ mutateAsync: vi.fn(), isPending: false, }), + useToggleAppStarMutation: () => ({ + mutateAsync: vi.fn(), + isPending: false, + }), })) vi.mock('@/app/components/apps/hooks/use-workflow-online-users', () => ({ @@ -242,6 +246,15 @@ const renderList = () => { return { ...render(, { wrapper: Wrapper }), onUrlUpdate } } +const openCreateMenu = () => { + fireEvent.click(screen.getByRole('button', { name: 'common.operation.create' })) +} + +const clickCreateMenuItem = (label: string) => { + openCreateMenu() + fireEvent.click(screen.getByText(label)) +} + describe('Create App Flow', () => { beforeEach(() => { vi.clearAllMocks() @@ -259,28 +272,28 @@ describe('Create App Flow', () => { }) describe('NewAppCard Rendering', () => { - it('should render the "Create App" card with all options', () => { + it('should render the create menu with all options', () => { renderList() - expect(screen.getByText('app.createApp')).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'common.operation.create' })).toBeInTheDocument() + openCreateMenu() expect(screen.getByText('app.newApp.startFromBlank')).toBeInTheDocument() expect(screen.getByText('app.newApp.startFromTemplate')).toBeInTheDocument() expect(screen.getByText('app.importDSL')).toBeInTheDocument() }) - it('should not render NewAppCard when user is not an editor', () => { + it('should not render the create menu when user is not an editor', () => { mockIsCurrentWorkspaceEditor = false renderList() - expect(screen.queryByText('app.createApp')).not.toBeInTheDocument() + expect(screen.queryByRole('button', { name: 'common.operation.create' })).not.toBeInTheDocument() }) - it('should show loading state when workspace is loading', () => { + it('should keep the create menu available while workspace state is loading', () => { mockIsLoadingCurrentWorkspace = true renderList() - // NewAppCard renders but with loading style (pointer-events-none opacity-50) - expect(screen.getByText('app.createApp')).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'common.operation.create' })).toBeInTheDocument() }) }) @@ -289,7 +302,7 @@ describe('Create App Flow', () => { it('should open the create app modal when "Start from Blank" is clicked', async () => { renderList() - fireEvent.click(screen.getByText('app.newApp.startFromBlank')) + clickCreateMenuItem('app.newApp.startFromBlank') await waitFor(() => { expect(screen.getByTestId('create-app-modal')).toBeInTheDocument() @@ -299,7 +312,7 @@ describe('Create App Flow', () => { it('should close the create app modal on cancel', async () => { renderList() - fireEvent.click(screen.getByText('app.newApp.startFromBlank')) + clickCreateMenuItem('app.newApp.startFromBlank') await waitFor(() => { expect(screen.getByTestId('create-app-modal')).toBeInTheDocument() }) @@ -313,7 +326,7 @@ describe('Create App Flow', () => { it('should call onPlanInfoChanged and refetch on successful creation', async () => { renderList() - fireEvent.click(screen.getByText('app.newApp.startFromBlank')) + clickCreateMenuItem('app.newApp.startFromBlank') await waitFor(() => { expect(screen.getByTestId('create-app-modal')).toBeInTheDocument() }) @@ -331,7 +344,7 @@ describe('Create App Flow', () => { it('should open template dialog when "Start from Template" is clicked', async () => { renderList() - fireEvent.click(screen.getByText('app.newApp.startFromTemplate')) + clickCreateMenuItem('app.newApp.startFromTemplate') await waitFor(() => { expect(screen.getByTestId('template-dialog')).toBeInTheDocument() @@ -341,7 +354,7 @@ describe('Create App Flow', () => { it('should allow switching from template to blank modal', async () => { renderList() - fireEvent.click(screen.getByText('app.newApp.startFromTemplate')) + clickCreateMenuItem('app.newApp.startFromTemplate') await waitFor(() => { expect(screen.getByTestId('template-dialog')).toBeInTheDocument() }) @@ -356,7 +369,7 @@ describe('Create App Flow', () => { it('should allow switching from blank to template dialog', async () => { renderList() - fireEvent.click(screen.getByText('app.newApp.startFromBlank')) + clickCreateMenuItem('app.newApp.startFromBlank') await waitFor(() => { expect(screen.getByTestId('create-app-modal')).toBeInTheDocument() }) @@ -374,7 +387,7 @@ describe('Create App Flow', () => { it('should open DSL import modal when "Import DSL" is clicked', async () => { renderList() - fireEvent.click(screen.getByText('app.importDSL')) + clickCreateMenuItem('app.importDSL') await waitFor(() => { expect(screen.getByTestId('create-from-dsl-modal')).toBeInTheDocument() @@ -384,7 +397,7 @@ describe('Create App Flow', () => { it('should close DSL import modal on cancel', async () => { renderList() - fireEvent.click(screen.getByText('app.importDSL')) + clickCreateMenuItem('app.importDSL') await waitFor(() => { expect(screen.getByTestId('create-from-dsl-modal')).toBeInTheDocument() }) @@ -398,7 +411,7 @@ describe('Create App Flow', () => { it('should call onPlanInfoChanged and refetch on successful DSL import', async () => { renderList() - fireEvent.click(screen.getByText('app.importDSL')) + clickCreateMenuItem('app.importDSL') await waitFor(() => { expect(screen.getByTestId('create-from-dsl-modal')).toBeInTheDocument() }) @@ -461,17 +474,18 @@ describe('Create App Flow', () => { mockPages = [createPage([])] renderList() - // NewAppCard should still be visible even with no apps - expect(screen.getByText('app.createApp')).toBeInTheDocument() + expect(screen.getByText('app.firstEmpty.title')).toBeInTheDocument() + expect(screen.getByText('app.newApp.startFromBlank')).toBeInTheDocument() + expect(screen.getByText('app.newApp.startFromTemplate')).toBeInTheDocument() + expect(screen.getByText('app.importDSL')).toBeInTheDocument() }) it('should handle multiple rapid clicks on create buttons without crashing', async () => { renderList() - // Rapidly click different create options - fireEvent.click(screen.getByText('app.newApp.startFromBlank')) - fireEvent.click(screen.getByText('app.newApp.startFromTemplate')) - fireEvent.click(screen.getByText('app.importDSL')) + clickCreateMenuItem('app.newApp.startFromBlank') + clickCreateMenuItem('app.newApp.startFromTemplate') + clickCreateMenuItem('app.importDSL') // Should not crash, and some modal should be present await waitFor(() => { diff --git a/web/__tests__/base/notion-page-selector-flow.test.tsx b/web/__tests__/base/notion-page-selector-flow.test.tsx index 34f4c988e1..3943507d7e 100644 --- a/web/__tests__/base/notion-page-selector-flow.test.tsx +++ b/web/__tests__/base/notion-page-selector-flow.test.tsx @@ -28,6 +28,9 @@ vi.mock('@/service/knowledge/use-import', () => ({ })) vi.mock('@/context/modal-context', () => ({ + useModalContext: () => ({ + setShowAccountSettingModal: mockSetShowAccountSettingModal, + }), useModalContextSelector: (selector: (state: { setShowAccountSettingModal: typeof mockSetShowAccountSettingModal }) => unknown) => selector({ setShowAccountSettingModal: mockSetShowAccountSettingModal }), })) diff --git a/web/__tests__/billing/billing-integration.test.tsx b/web/__tests__/billing/billing-integration.test.tsx index 0f3a0708e7..57f6faddcc 100644 --- a/web/__tests__/billing/billing-integration.test.tsx +++ b/web/__tests__/billing/billing-integration.test.tsx @@ -21,6 +21,14 @@ let mockAppCtx: Record = {} const mockSetShowPricingModal = vi.fn() const mockSetShowAccountSettingModal = vi.fn() +vi.mock('@/config', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + IS_CLOUD_EDITION: true, + } +}) + vi.mock('@/context/provider-context', () => ({ useProviderContext: () => mockProviderCtx, })) diff --git a/web/__tests__/billing/education-verification-flow.test.tsx b/web/__tests__/billing/education-verification-flow.test.tsx index 83885b9f9f..6098d44634 100644 --- a/web/__tests__/billing/education-verification-flow.test.tsx +++ b/web/__tests__/billing/education-verification-flow.test.tsx @@ -24,6 +24,14 @@ const mockRouterPush = vi.fn() const mockMutateAsync = vi.fn() const mockSetEducationVerifying = vi.hoisted(() => vi.fn()) +vi.mock('@/config', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + IS_CLOUD_EDITION: true, + } +}) + // ─── Context mocks ─────────────────────────────────────────────────────────── vi.mock('@/context/provider-context', () => ({ useProviderContext: () => mockProviderCtx, diff --git a/web/__tests__/custom/custom-page-flow.test.tsx b/web/__tests__/custom/custom-page-flow.test.tsx index 6eb5ccadb9..66158ec506 100644 --- a/web/__tests__/custom/custom-page-flow.test.tsx +++ b/web/__tests__/custom/custom-page-flow.test.tsx @@ -8,6 +8,14 @@ import useWebAppBrand from '@/app/components/custom/custom-web-app-brand/hooks/u const mockSetShowPricingModal = vi.fn() +vi.mock('@/config', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + IS_CLOUD_EDITION: true, + } +}) + vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string, options?: { ns?: string }) => options?.ns ? `${options.ns}.${key}` : key, diff --git a/web/__tests__/explore/explore-app-list-flow.test.tsx b/web/__tests__/explore/explore-app-list-flow.test.tsx index dbbbbee456..f93353443c 100644 --- a/web/__tests__/explore/explore-app-list-flow.test.tsx +++ b/web/__tests__/explore/explore-app-list-flow.test.tsx @@ -8,10 +8,10 @@ import type { Mock } from 'vitest' import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal' import type { App } from '@/models/explore' import { fireEvent, screen, waitFor } from '@testing-library/react' -import { renderWithSystemFeatures as render } from '@/__tests__/utils/mock-system-features' +import { createTestQueryClient, renderWithSystemFeatures as render } from '@/__tests__/utils/mock-system-features' import AppList from '@/app/components/explore/app-list' import { useAppContext } from '@/context/app-context' -import { fetchAppDetail } from '@/service/explore' +import { fetchAppDetail, fetchAppList, fetchBanners } from '@/service/explore' import { useMembers } from '@/service/use-common' import { AppModeEnum } from '@/types/app' @@ -47,9 +47,9 @@ vi.mock('ahooks', async () => { }) vi.mock('@/service/use-explore', () => ({ - useExploreAppList: () => ({ - data: mockExploreData, - isLoading: mockIsLoading, + useLearnDifyAppList: () => ({ + data: [], + isLoading: false, isError: false, }), })) @@ -57,6 +57,55 @@ vi.mock('@/service/use-explore', () => ({ vi.mock('@/service/explore', () => ({ fetchAppDetail: vi.fn(), fetchAppList: vi.fn(), + fetchBanners: vi.fn(), +})) + +vi.mock('@/service/client', () => ({ + consoleClient: {}, + consoleQuery: { + systemFeatures: { + get: { + queryKey: () => ['console', 'systemFeatures'], + }, + }, + apps: { + list: { + queryOptions: (options: { + input?: { query?: { limit?: number } } + select?: (response: { + data: [] + has_more: boolean + limit: number + page: number + total: number + }) => unknown + }) => { + const limit = options.input?.query?.limit ?? 0 + const response = { + data: [], + has_more: false, + limit, + page: 1, + total: 0, + } + return { + queryKey: ['console', 'apps', 'list', options.input], + queryFn: () => Promise.resolve(response), + initialData: response, + select: options.select, + } + }, + }, + }, + explore: { + apps: { + queryKey: ({ input }: { input?: unknown } = {}) => ['console', 'explore', 'apps', input], + }, + banners: { + queryKey: ({ input }: { input?: unknown } = {}) => ['console', 'explore', 'banners', input], + }, + }, + }, })) vi.mock('@/context/app-context', () => ({ @@ -147,14 +196,37 @@ const mockMemberRole = (hasEditPermission: boolean) => { }) } -const renderAppList = (hasEditPermission = true, onSuccess?: () => void) => { - mockMemberRole(hasEditPermission) - return render() +const localeInput = { query: { language: 'en' } } +const exploreAppListQueryKey = ['console', 'explore', 'apps', localeInput, 'en'] +const homeContinueWorkAppsInput = { + query: { + page: 1, + limit: 8, + name: '', + }, } -const appListElement = (hasEditPermission = true, onSuccess?: () => void) => { +const createHomeQueryClient = () => { + const queryClient = createTestQueryClient() + queryClient.setQueryData(['console', 'apps', 'list', homeContinueWorkAppsInput], { + data: [], + has_more: false, + limit: 8, + page: 1, + total: 0, + }) + + if (!mockIsLoading && mockExploreData) + queryClient.setQueryData(exploreAppListQueryKey, mockExploreData) + + return queryClient +} + +const renderAppList = (hasEditPermission = true, onSuccess?: () => void) => { mockMemberRole(hasEditPermission) - return + return render(, { + queryClient: createHomeQueryClient(), + }) } describe('Explore App List Flow', () => { @@ -170,6 +242,8 @@ describe('Explore App List Flow', () => { createApp({ app_id: 'app-3', app: { ...createApp().app, id: 'app-id-3', name: 'Code Helper' }, categories: ['Programming'] }), ], } + ;(fetchAppList as unknown as Mock).mockImplementation(() => new Promise(() => {})) + ;(fetchBanners as unknown as Mock).mockResolvedValue([]) }) describe('Browse and Filter Flow', () => { @@ -242,8 +316,8 @@ describe('Explore App List Flow', () => { renderAppList(true, onSuccess) - // Step 2: Click add to workspace button - opens create modal - fireEvent.click(screen.getAllByText('explore.appCard.addToWorkspace')[0]!) + // Step 2: Click the app card - opens create modal in self-hosted/non-cloud mode + fireEvent.click(screen.getByRole('button', { name: 'Writer Bot' })) // Step 3: Confirm creation in modal fireEvent.click(await screen.findByTestId('confirm-create')) @@ -256,7 +330,6 @@ describe('Explore App List Flow', () => { // Step 5: DSL import triggers pending confirmation expect(mockHandleImportDSL).toHaveBeenCalledTimes(1) - // Step 6: DSL confirm modal appears and user confirms // Step 6: DSL confirm modal appears and user confirms expect(await screen.findByTestId('dsl-confirm-modal'))!.toBeInTheDocument() fireEvent.click(screen.getByTestId('dsl-confirm')) @@ -274,7 +347,7 @@ describe('Explore App List Flow', () => { // Step 1: Loading state mockIsLoading = true mockExploreData = undefined - const { unmount } = render(appListElement()) + const { unmount } = renderAppList() expect(screen.getByRole('status'))!.toBeInTheDocument() @@ -293,16 +366,16 @@ describe('Explore App List Flow', () => { }) describe('Permission-Based Behavior', () => { - it('should hide add-to-workspace button when user has no edit permission', () => { + it('should not make app cards clickable when user has no edit permission', () => { renderAppList(false) - expect(screen.queryByText('explore.appCard.addToWorkspace')).not.toBeInTheDocument() + expect(screen.queryByRole('button', { name: 'Writer Bot' })).not.toBeInTheDocument() }) - it('should show add-to-workspace button when user has edit permission', () => { + it('should make app cards clickable when user has edit permission', () => { renderAppList(true) - expect(screen.getAllByText('explore.appCard.addToWorkspace').length).toBeGreaterThan(0) + expect(screen.getByRole('button', { name: 'Writer Bot' })).toBeInTheDocument() }) }) }) diff --git a/web/__tests__/explore/installed-app-flow.test.tsx b/web/__tests__/explore/installed-app-flow.test.tsx index 34bfac5cd6..66d88cdf92 100644 --- a/web/__tests__/explore/installed-app-flow.test.tsx +++ b/web/__tests__/explore/installed-app-flow.test.tsx @@ -144,13 +144,13 @@ describe('Installed App Flow', () => { [AppModeEnum.CHAT, 'chat-with-history'], [AppModeEnum.ADVANCED_CHAT, 'chat-with-history'], [AppModeEnum.AGENT_CHAT, 'chat-with-history'], - ])('should render ChatWithHistory for %s mode', (mode, testId) => { + ])('should render ChatWithHistory for %s mode', async (mode, testId) => { const app = createInstalledApp(mode) setupDefaultMocks(app) render() - expect(screen.getByTestId(testId)).toBeInTheDocument() + expect(await screen.findByTestId(testId)).toBeInTheDocument() expect(screen.getByText(/Integration Test App/)).toBeInTheDocument() }) diff --git a/web/__tests__/explore/sidebar-lifecycle-flow.test.tsx b/web/__tests__/explore/sidebar-lifecycle-flow.test.tsx index 35c8175a36..11c3426f48 100644 --- a/web/__tests__/explore/sidebar-lifecycle-flow.test.tsx +++ b/web/__tests__/explore/sidebar-lifecycle-flow.test.tsx @@ -24,6 +24,7 @@ let mockInstalledApps: InstalledApp[] = [] let mockIsUninstallPending = false vi.mock('@/next/navigation', () => ({ + usePathname: () => '/explore', useSelectedLayoutSegments: () => mockSegments, useRouter: () => ({ push: mockPush, diff --git a/web/__tests__/plugins/plugin-page-shell-flow.test.tsx b/web/__tests__/plugins/plugin-page-shell-flow.test.tsx index bd089d325c..421d805d07 100644 --- a/web/__tests__/plugins/plugin-page-shell-flow.test.tsx +++ b/web/__tests__/plugins/plugin-page-shell-flow.test.tsx @@ -38,6 +38,7 @@ vi.mock('@/context/app-context', () => ({ })) vi.mock('@/service/use-plugins', () => ({ + hasPluginPermission: () => true, useReferenceSettings: () => ({ data: { permission: { @@ -51,6 +52,24 @@ vi.mock('@/service/use-plugins', () => ({ isPending: false, }), useInvalidateReferenceSettings: () => vi.fn(), + usePluginPermissionSettings: () => ({ + data: { + install_permission: 'everyone', + debug_permission: 'noOne', + }, + }), + useMutationPluginPermissionSettings: () => ({ + mutate: vi.fn(), + isPending: false, + }), + usePluginAutoUpgradeSettings: () => ({ + data: { + auto_upgrade: false, + strategy_setting: {}, + exclude_plugins: [], + include_plugins: [], + }, + }), useInstalledPluginList: () => ({ data: { total: 2, diff --git a/web/__tests__/tools/provider-list-shell-flow.test.tsx b/web/__tests__/tools/provider-list-shell-flow.test.tsx index afa3f45e9f..346834c642 100644 --- a/web/__tests__/tools/provider-list-shell-flow.test.tsx +++ b/web/__tests__/tools/provider-list-shell-flow.test.tsx @@ -2,7 +2,7 @@ import type { ReactNode } from 'react' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' import { createSystemFeaturesWrapper } from '@/__tests__/utils/mock-system-features' -import ProviderList from '@/app/components/tools/provider-list' +import ProviderList from '@/app/components/integrations/tool-provider-list' import { CollectionType } from '@/app/components/tools/types' import { createNuqsTestWrapper } from '@/test/nuqs-testing' diff --git a/web/__tests__/tools/tool-browsing-and-filtering.test.tsx b/web/__tests__/tools/tool-browsing-and-filtering.test.tsx index b1cc0c1312..d470326805 100644 --- a/web/__tests__/tools/tool-browsing-and-filtering.test.tsx +++ b/web/__tests__/tools/tool-browsing-and-filtering.test.tsx @@ -129,30 +129,6 @@ vi.mock('@/app/components/base/tab-slider-new', () => ({ ), })) -vi.mock('@/app/components/base/input', () => ({ - default: ({ value, onChange, onClear, showLeftIcon, showClearIcon, wrapperClassName }: { - value: string - onChange: (e: { target: { value: string } }) => void - onClear: () => void - showLeftIcon?: boolean - showClearIcon?: boolean - wrapperClassName?: string - }) => ( -
- - {showClearIcon && value && ( - - )} -
- ), -})) - vi.mock('@/app/components/plugins/card', () => ({ default: ({ payload, className }: { payload: { brief: Record | string, name: string }, className?: string }) => { const briefText = typeof payload.brief === 'object' ? payload.brief?.en_US || '' : payload.brief @@ -230,7 +206,7 @@ vi.mock('@/app/components/workflow/block-selector/types', () => ({ ToolTypeEnum: { BuiltIn: 'builtin', Custom: 'api', Workflow: 'workflow', MCP: 'mcp' }, })) -const { default: ProviderList } = await import('@/app/components/tools/provider-list') +const { default: ProviderList } = await import('@/app/components/integrations/tool-provider-list') const createWrapper = () => { const { wrapper } = createSystemFeaturesWrapper({ @@ -263,7 +239,7 @@ describe('Tool Browsing & Filtering Integration', () => { it('filters tools by keyword search', async () => { render(, { wrapper: createWrapper() }) - const searchInput = screen.getByTestId('search-input') + const searchInput = screen.getByPlaceholderText('operation.search') fireEvent.change(searchInput, { target: { value: 'Google' } }) await waitFor(() => { @@ -275,7 +251,7 @@ describe('Tool Browsing & Filtering Integration', () => { it('clears search keyword and shows all tools again', async () => { render(, { wrapper: createWrapper() }) - const searchInput = screen.getByTestId('search-input') + const searchInput = screen.getByPlaceholderText('operation.search') fireEvent.change(searchInput, { target: { value: 'Google' } }) await waitFor(() => { expect(screen.queryByTestId('card-weather_api')).not.toBeInTheDocument() @@ -323,7 +299,7 @@ describe('Tool Browsing & Filtering Integration', () => { expect(screen.getByTestId('card-google_search')).toBeInTheDocument() }) - const searchInput = screen.getByTestId('search-input') + const searchInput = screen.getByPlaceholderText('operation.search') fireEvent.change(searchInput, { target: { value: 'Weather' } }) await waitFor(() => { expect(screen.queryByTestId('card-google_search')).not.toBeInTheDocument() @@ -368,6 +344,6 @@ describe('Tool Browsing & Filtering Integration', () => { it('shows search input on all tabs', () => { render(, { wrapper: createWrapper() }) - expect(screen.getByTestId('search-input')).toBeInTheDocument() + expect(screen.getByPlaceholderText('operation.search')).toBeInTheDocument() }) }) diff --git a/web/app/(commonLayout)/__tests__/hydration-boundary.spec.tsx b/web/app/(commonLayout)/__tests__/hydration-boundary.spec.tsx index 7546e95d69..45bbf24a25 100644 --- a/web/app/(commonLayout)/__tests__/hydration-boundary.spec.tsx +++ b/web/app/(commonLayout)/__tests__/hydration-boundary.spec.tsx @@ -130,6 +130,16 @@ describe('CommonLayoutHydrationBoundary', () => { expect(mocks.redirect).toHaveBeenCalledWith('/auth/refresh?redirect_url=%2Fapps%3Ftag%3Dworkflow') }) + it('should default unauthorized refresh redirects to the home path when the pathname header is missing', async () => { + mocks.headers.mockResolvedValue(new Headers()) + mocks.profileQueryFn.mockRejectedValue(new Response(JSON.stringify({ code: 'unauthorized' }), { status: 401 })) + const { CommonLayoutHydrationBoundary } = await import('../hydration-boundary') + + await expect(CommonLayoutHydrationBoundary({ children: null })).rejects.toThrow('NEXT_REDIRECT') + + expect(mocks.redirect).toHaveBeenCalledWith('/auth/refresh?redirect_url=%2F') + }) + it('should redirect setup errors to install', async () => { mocks.profileQueryFn.mockRejectedValue(new Response(JSON.stringify({ code: 'not_setup' }), { status: 401 })) const { CommonLayoutHydrationBoundary } = await import('../hydration-boundary') diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/__tests__/layout-main.spec.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/__tests__/layout-main.spec.tsx new file mode 100644 index 0000000000..f1e12b34c7 --- /dev/null +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/__tests__/layout-main.spec.tsx @@ -0,0 +1,117 @@ +import type { App } from '@/types/app' +import { render, screen, waitFor } from '@testing-library/react' +import { useStore } from '@/app/components/app/store' +import { usePathname, useRouter } from '@/next/navigation' +import { fetchAppDetailDirect } from '@/service/apps' +import { AppModeEnum } from '@/types/app' +import AppDetailLayout from '../layout-main' + +const mockReplace = vi.fn() +let mockPathname = '/app/app-1/workflow' +let mockIsCurrentWorkspaceEditor = true + +vi.mock('@/next/navigation', () => ({ + usePathname: vi.fn(), + useRouter: vi.fn(), +})) + +vi.mock('@/service/apps', () => ({ + fetchAppDetailDirect: vi.fn(), +})) + +vi.mock('@/context/app-context', () => ({ + useAppContext: () => ({ + currentWorkspace: { id: 'workspace-1' }, + isCurrentWorkspaceEditor: mockIsCurrentWorkspaceEditor, + isLoadingCurrentWorkspace: false, + }), +})) + +vi.mock('@/hooks/use-document-title', () => ({ + default: vi.fn(), +})) + +const mockUsePathname = vi.mocked(usePathname) +const mockUseRouter = vi.mocked(useRouter) +const mockFetchAppDetailDirect = vi.mocked(fetchAppDetailDirect) + +const createAppDetail = (overrides: Partial = {}) => ({ + id: 'app-1', + name: 'Demo App', + mode: AppModeEnum.WORKFLOW, + ...overrides, +}) as App + +const waitForAppContent = async () => { + await waitFor(() => { + expect(screen.getByText('App page content')).toBeInTheDocument() + }) +} + +describe('AppDetailLayout', () => { + beforeEach(() => { + vi.clearAllMocks() + mockPathname = '/app/app-1/workflow' + mockIsCurrentWorkspaceEditor = true + mockUsePathname.mockImplementation(() => mockPathname) + mockUseRouter.mockReturnValue({ + back: vi.fn(), + forward: vi.fn(), + refresh: vi.fn(), + push: vi.fn(), + replace: mockReplace, + prefetch: vi.fn(), + }) + mockFetchAppDetailDirect.mockResolvedValue(createAppDetail()) + useStore.getState().setAppDetail() + }) + + it('should keep app detail data when navigating between pages in the same app', async () => { + const { rerender, unmount } = render( + +
App page content
+
, + ) + await waitForAppContent() + expect(mockFetchAppDetailDirect).toHaveBeenCalledTimes(1) + + mockPathname = '/app/app-1/logs' + rerender( + +
App page content
+
, + ) + + await waitForAppContent() + expect(mockFetchAppDetailDirect).toHaveBeenCalledTimes(1) + expect(useStore.getState().appDetail?.id).toBe('app-1') + + unmount() + render( + +
App page content
+
, + ) + + await waitForAppContent() + expect(mockFetchAppDetailDirect).toHaveBeenCalledTimes(1) + expect(useStore.getState().appDetail?.id).toBe('app-1') + }) + + it('should redirect restricted app pages before exposing app detail content', async () => { + mockIsCurrentWorkspaceEditor = false + mockPathname = '/app/app-1/logs' + + render( + +
App page content
+
, + ) + + await waitFor(() => { + expect(mockReplace).toHaveBeenCalledWith('/app/app-1/overview') + }) + expect(screen.queryByText('App page content')).not.toBeInTheDocument() + expect(useStore.getState().appDetail).toBeUndefined() + }) +}) diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx index 1e001a5ca4..d0187b67fc 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx @@ -1,30 +1,14 @@ 'use client' import type { FC } from 'react' -import type { NavIcon } from '@/app/components/app-sidebar/nav-link' import type { App } from '@/types/app' import { cn } from '@langgenius/dify-ui/cn' -import { - RiDashboard2Fill, - RiDashboard2Line, - RiFileList3Fill, - RiFileList3Line, - RiTerminalBoxFill, - RiTerminalBoxLine, - RiTerminalWindowFill, - RiTerminalWindowLine, -} from '@remixicon/react' -import { useUnmount } from 'ahooks' import * as React from 'react' -import { useCallback, useEffect, useState } from 'react' +import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { useShallow } from 'zustand/react/shallow' -import AppSideBar from '@/app/components/app-sidebar' -import { AppInfoDetailLayer } from '@/app/components/app-sidebar/app-info' -import { useAppInfoActions } from '@/app/components/app-sidebar/app-info/use-app-info-actions' import { useStore } from '@/app/components/app/store' import Loading from '@/app/components/base/loading' import { useAppContext } from '@/context/app-context' -import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import useDocumentTitle from '@/hooks/use-document-title' import { usePathname, useRouter } from '@/next/navigation' import { fetchAppDetailDirect } from '@/service/apps' @@ -36,6 +20,13 @@ type IAppDetailLayoutProps = { appId: string } +const isNotFoundError = (error: unknown) => ( + typeof error === 'object' + && error !== null + && 'status' in error + && error.status === 404 +) + const AppDetailLayout: FC = (props) => { const { children, @@ -44,113 +35,78 @@ const AppDetailLayout: FC = (props) => { const { t } = useTranslation() const router = useRouter() const pathname = usePathname() - const media = useBreakpoints() - const isMobile = media === MediaType.mobile const { isCurrentWorkspaceEditor, isLoadingCurrentWorkspace, currentWorkspace } = useAppContext() - const appInfoActions = useAppInfoActions({ resetKey: appId }) - const { appDetail, setAppDetail, setAppSidebarExpand } = useStore(useShallow(state => ({ + const { appDetail, setAppDetail } = useStore(useShallow(state => ({ appDetail: state.appDetail, setAppDetail: state.setAppDetail, - setAppSidebarExpand: state.setAppSidebarExpand, }))) const [isLoadingAppDetail, setIsLoadingAppDetail] = useState(false) const [appDetailRes, setAppDetailRes] = useState(null) - const [navigation, setNavigation] = useState>([]) - - const getNavigationConfig = useCallback((appId: string, isCurrentWorkspaceEditor: boolean, mode: AppModeEnum) => { - const navConfig = [ - ...(isCurrentWorkspaceEditor - ? [{ - name: t('appMenus.promptEng', { ns: 'common' }), - href: `/app/${appId}/${(mode === AppModeEnum.WORKFLOW || mode === AppModeEnum.ADVANCED_CHAT) ? 'workflow' : 'configuration'}`, - icon: RiTerminalWindowLine, - selectedIcon: RiTerminalWindowFill, - }] - : [] - ), - { - name: t('appMenus.apiAccess', { ns: 'common' }), - href: `/app/${appId}/develop`, - icon: RiTerminalBoxLine, - selectedIcon: RiTerminalBoxFill, - }, - ...(isCurrentWorkspaceEditor - ? [{ - name: mode !== AppModeEnum.WORKFLOW - ? t('appMenus.logAndAnn', { ns: 'common' }) - : t('appMenus.logs', { ns: 'common' }), - href: `/app/${appId}/logs`, - icon: RiFileList3Line, - selectedIcon: RiFileList3Fill, - }] - : [] - ), - { - name: t('appMenus.overview', { ns: 'common' }), - href: `/app/${appId}/overview`, - icon: RiDashboard2Line, - selectedIcon: RiDashboard2Fill, - }, - ] - return navConfig - }, [t]) + const routeAppDetail = appDetailRes ?? (appDetail?.id === appId ? appDetail : null) useDocumentTitle(appDetail?.name || t('menus.appDetail', { ns: 'common' })) useEffect(() => { - if (appDetail) { - const localeMode = localStorage.getItem('app-detail-collapse-or-expand') || 'expand' - const mode = isMobile ? 'collapse' : 'expand' - setAppSidebarExpand(isMobile ? mode : localeMode) - // TODO: consider screen size and mode - // if ((appDetail.mode === AppModeEnum.ADVANCED_CHAT || appDetail.mode === 'workflow') && (pathname).endsWith('workflow')) - // setAppSidebarExpand('collapse') - } - }, [appDetail, isMobile]) + let ignore = false + + const currentAppDetail = useStore.getState().appDetail + if (currentAppDetail?.id === appId) { + return () => { + ignore = true + } + } - useEffect(() => { setAppDetail() - setIsLoadingAppDetail(true) + void Promise.resolve().then(() => { + if (!ignore) + setIsLoadingAppDetail(true) + }) fetchAppDetailDirect({ url: '/apps', id: appId }).then((res: App) => { + if (ignore) + return + setAppDetailRes(res) - }).catch((e: any) => { - if (e.status === 404) + }).catch((error: unknown) => { + if (ignore) + return + + if (isNotFoundError(error)) router.replace('/apps') }).finally(() => { + if (ignore) + return + setIsLoadingAppDetail(false) }) - }, [appId, pathname]) + + return () => { + ignore = true + } + }, [appId, router, setAppDetail]) useEffect(() => { - if (!appDetailRes || !currentWorkspace.id || isLoadingCurrentWorkspace || isLoadingAppDetail) + if (!routeAppDetail || !currentWorkspace.id || isLoadingCurrentWorkspace || isLoadingAppDetail) return - const res = appDetailRes + if (routeAppDetail.id !== appId) + return + // redirection const canIEditApp = isCurrentWorkspaceEditor - if (!canIEditApp && (pathname.endsWith('configuration') || pathname.endsWith('workflow') || pathname.endsWith('logs'))) { + if (!canIEditApp && (pathname.endsWith('configuration') || pathname.endsWith('workflow') || pathname.endsWith('logs') || pathname.endsWith('annotations'))) { router.replace(`/app/${appId}/overview`) return } - if ((res.mode === AppModeEnum.WORKFLOW || res.mode === AppModeEnum.ADVANCED_CHAT) && (pathname).endsWith('configuration')) { + if ((routeAppDetail.mode === AppModeEnum.WORKFLOW || routeAppDetail.mode === AppModeEnum.ADVANCED_CHAT) && (pathname).endsWith('configuration')) { router.replace(`/app/${appId}/workflow`) } - else if ((res.mode !== AppModeEnum.WORKFLOW && res.mode !== AppModeEnum.ADVANCED_CHAT) && (pathname).endsWith('workflow')) { + else if ((routeAppDetail.mode !== AppModeEnum.WORKFLOW && routeAppDetail.mode !== AppModeEnum.ADVANCED_CHAT) && (pathname).endsWith('workflow')) { router.replace(`/app/${appId}/configuration`) + return } - else { - setAppDetail({ ...res, enable_sso: false }) - setNavigation(getNavigationConfig(appId, isCurrentWorkspaceEditor, res.mode)) - } - }, [appDetailRes, isCurrentWorkspaceEditor, isLoadingAppDetail, isLoadingCurrentWorkspace]) - useUnmount(() => { - setAppDetail() - }) + if (appDetailRes && appDetail?.id !== appDetailRes.id) + setAppDetail({ ...appDetailRes, enable_sso: false }) + }, [appDetail?.id, appDetailRes, appId, currentWorkspace.id, isCurrentWorkspaceEditor, isLoadingAppDetail, isLoadingCurrentWorkspace, pathname, routeAppDetail, router, setAppDetail]) if (!appDetail) { return ( @@ -161,17 +117,10 @@ const AppDetailLayout: FC = (props) => { } return ( -
- {appDetail && ( - - )} +
{children}
-
) } diff --git a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/__tests__/layout-main.spec.tsx b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/__tests__/layout-main.spec.tsx index 0d7f01a210..11ddc89b4c 100644 --- a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/__tests__/layout-main.spec.tsx +++ b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/__tests__/layout-main.spec.tsx @@ -1,10 +1,9 @@ import { render, screen, waitFor } from '@testing-library/react' -import { usePathname, useRouter } from '@/next/navigation' -import { useDatasetDetail, useDatasetRelatedApps } from '@/service/knowledge/use-dataset' +import { useRouter } from '@/next/navigation' +import { useDatasetDetail } from '@/service/knowledge/use-dataset' import DatasetDetailLayout from '../layout-main' const mockReplace = vi.fn() -const mockSetAppSidebarExpand = vi.fn() vi.mock('@/next/navigation', () => ({ usePathname: vi.fn(), @@ -13,13 +12,6 @@ vi.mock('@/next/navigation', () => ({ vi.mock('@/service/knowledge/use-dataset', () => ({ useDatasetDetail: vi.fn(), - useDatasetRelatedApps: vi.fn(), -})) - -vi.mock('@/app/components/app/store', () => ({ - useStore: (selector: (state: { setAppSidebarExpand: typeof mockSetAppSidebarExpand }) => unknown) => selector({ - setAppSidebarExpand: mockSetAppSidebarExpand, - }), })) vi.mock('@/context/app-context', () => ({ @@ -34,34 +26,16 @@ vi.mock('@/context/event-emitter', () => ({ }), })) -vi.mock('@/hooks/use-breakpoints', () => ({ - default: () => 'desktop', - MediaType: { - mobile: 'mobile', - }, -})) - vi.mock('@/hooks/use-document-title', () => ({ default: vi.fn(), })) -vi.mock('@/app/components/app-sidebar', () => ({ - default: () =>