Compare commits

...
369 Commits
Author SHA1 Message Date
Jingyi-Dify 6a08d8dd6f fix(workflow): polish start selector states 2026-06-08 20:08:03 -07:00
Jingyi-Dify 7b030ff1bc fix(workflow): use scroll area for trigger actions 2026-06-08 19:38:14 -07:00
Jingyi-Dify 60d779152c feat(workflow): align start selector design 2026-06-08 19:13:11 -07:00
Jingyi-Dify ff97259011 feat(workflow): add start placeholder panel 2026-06-08 19:10:09 -07:00
Jingyi-Dify 4163b4dd81 feat(workflow): restore start placeholder drafts 2026-06-08 19:08:33 -07:00
Jingyi-Dify 02c9b6e5ce feat(workflow): add start node placeholder panel
Add a workflow-only start placeholder node that opens a right-panel start selector for the initial workflow entry choice.

Keep the placeholder frontend-local by filtering it from draft sync payloads, while preserving existing start templates for other app modes.
2026-06-08 14:19:39 -07:00
Joel 83db975374 feat: app star contract 2026-06-08 18:32:33 +08:00
Joel 18efc8ed88 chore: menu pos and menu action 2026-06-08 18:08:19 +08:00
Joel 46e1a5a6db chore: publish pipeline template copywriting and icon 2026-06-08 18:02:00 +08:00
Joel a2267276ae chore: knowledge toggle icon 2026-06-08 18:01:59 +08:00
yyh 38af34a742 fix: keep global nav account expanded 2026-06-08 17:44:02 +08:00
Joel a6084ca3c7 chore: chunking Setting move place 2026-06-08 17:27:42 +08:00
Joel d8b847dcf0 chore: i18n and split in menu 2026-06-08 17:15:03 +08:00
yyh 2509682e07 fix: align secondary sidebar help icon 2026-06-08 17:10:26 +08:00
Joel 5582b35d56 chroe: copywriting 2026-06-08 17:04:28 +08:00
Joel db83df9f9c chore: knowledge icon 2026-06-08 17:04:28 +08:00
Joel 0683c0e7a7 chore: sidbar hover toggle and no data icon 2026-06-08 17:04:28 +08:00
hjlarry 42b7bf8152 feat: gen app starred openAPI 2026-06-08 16:53:35 +08:00
hjlarry bc33ef1b97 feat: app star and its list 2026-06-08 16:47:26 +08:00
Joel 0f493a52a1 fix: annotation page highlight 2026-06-08 16:35:17 +08:00
yyh fc4f9db79a fix: creators filter styles 2026-06-08 16:31:10 +08:00
yyh e9b8a1606e fix: remove unnecessary title prop 2026-06-08 16:28:31 +08:00
Joel 453c4c4c5f chore: update annotations 2026-06-08 16:20:44 +08:00
Joel 58e25d0534 chore: toogle icon 2026-06-08 16:20:44 +08:00
yyh d7b9f2a86b Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-06-08 16:08:22 +08:00
Joel 18d2423ed1 feat: apps support sort by 2026-06-08 15:55:57 +08:00
yyh 4176e7d146 fix: font style 2026-06-08 15:43:01 +08:00
Joel 3556611c2b chore: header filter 2026-06-08 15:36:15 +08:00
yyh e37b4af0e8 feat(web): autofocus main nav search inputs 2026-06-08 15:34:43 +08:00
yyh a0eb2ba0ff feat(web): support search input autofocus 2026-06-08 15:34:33 +08:00
yyh 75f1094459 fix(web): use native button for explore app cards 2026-06-08 15:14:33 +08:00
yyh f61f9634f8 fix(web): use radio semantics for explore categories 2026-06-08 15:07:21 +08:00
yyh 11f78289d3 fix: highlight integration popover triggers 2026-06-08 14:50:18 +08:00
yyh 062341ab26 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite
# Conflicts:
#	eslint-suppressions.json
#	web/i18n/ar-TN/plugin.json
#	web/i18n/de-DE/plugin.json
#	web/i18n/es-ES/plugin.json
#	web/i18n/es-ES/workflow.json
#	web/i18n/fa-IR/plugin.json
#	web/i18n/fa-IR/workflow.json
#	web/i18n/fr-FR/plugin.json
#	web/i18n/fr-FR/workflow.json
#	web/i18n/hi-IN/plugin.json
#	web/i18n/hi-IN/workflow.json
#	web/i18n/id-ID/plugin.json
#	web/i18n/id-ID/workflow.json
#	web/i18n/it-IT/plugin.json
#	web/i18n/it-IT/workflow.json
#	web/i18n/ja-JP/plugin.json
#	web/i18n/ja-JP/workflow.json
#	web/i18n/ko-KR/plugin.json
#	web/i18n/ko-KR/workflow.json
#	web/i18n/nl-NL/app.json
#	web/i18n/nl-NL/plugin.json
#	web/i18n/nl-NL/workflow.json
#	web/i18n/pl-PL/plugin.json
#	web/i18n/pl-PL/workflow.json
#	web/i18n/pt-BR/plugin.json
#	web/i18n/ro-RO/plugin.json
#	web/i18n/ro-RO/workflow.json
#	web/i18n/ru-RU/plugin.json
#	web/i18n/ru-RU/workflow.json
#	web/i18n/sl-SI/plugin.json
#	web/i18n/sl-SI/workflow.json
#	web/i18n/th-TH/plugin.json
#	web/i18n/th-TH/workflow.json
#	web/i18n/tr-TR/plugin.json
#	web/i18n/tr-TR/workflow.json
#	web/i18n/uk-UA/plugin.json
#	web/i18n/uk-UA/workflow.json
#	web/i18n/vi-VN/plugin.json
#	web/i18n/vi-VN/workflow.json
#	web/i18n/zh-Hant/plugin.json
#	web/i18n/zh-Hant/workflow.json
2026-06-08 14:37:00 +08:00
yyh 6ea9ba5926 fix: question icon style 2026-06-08 14:20:29 +08:00
yyh 7a9054fdea fix: workspace filter style 2026-06-08 13:30:00 +08:00
yyh 508baa782c Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-06-08 13:14:55 +08:00
yyh f5ee121d2c fix: workspace card components 2026-06-08 13:02:26 +08:00
yyh 2bf60a67ad fix: style 2026-06-08 12:36:17 +08:00
hjlarry f6433097ad feat: app list add sort param 2026-06-08 11:26:18 +08:00
yyh 23b1038b99 fix: migrations 2026-06-08 11:21:56 +08:00
yyh ed10b82bb1 fix: export 2026-06-08 10:40:36 +08:00
yyh 41106ef6c9 fix: lint and type-check 2026-06-08 10:34:16 +08:00
yyh 5ee7bedb56 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite
# Conflicts:
#	.agents/skills/frontend-code-review/SKILL.md
#	.agents/skills/frontend-code-review/references/business-logic.md
#	.agents/skills/frontend-code-review/references/code-quality.md
#	.agents/skills/frontend-code-review/references/performance.md
#	api/controllers/console/explore/recommended_app.py
#	eslint-suppressions.json
#	packages/contracts/generated/api/console/workspaces/orpc.gen.ts
#	packages/iconify-collections/custom-vender/info.json
#	web/__tests__/apps/app-list-browsing-flow.test.tsx
#	web/app/(commonLayout)/role-route-guard.tsx
#	web/app/components/app-sidebar/nav-link/index.tsx
#	web/app/components/apps/__tests__/empty.spec.tsx
#	web/app/components/apps/__tests__/list.spec.tsx
#	web/app/components/apps/empty.tsx
#	web/app/components/apps/list.tsx
#	web/app/components/base/search-input/__tests__/index.spec.tsx
#	web/app/components/base/search-input/index.tsx
#	web/app/components/header/account-setting/index.tsx
#	web/app/components/header/header-wrapper.tsx
#	web/app/components/plugins/marketplace/search-box/index.tsx
#	web/app/components/tools/mcp/modal.tsx
#	web/features/tag-management/components/tag-filter.tsx
#	web/i18n/en-US/workflow.json
#	web/i18n/zh-Hans/workflow.json
2026-06-08 10:32:32 +08:00
Jingyi-Dify f0bcb77d55 fix(web): update preferences active icon 2026-06-07 00:12:29 -07:00
Jingyi-Dify 44a89bf870 fix(web): align main nav glass tokens 2026-06-07 00:11:58 -07:00
Jingyi-Dify 60ad023553 Revert "chore(web): remove explicit plugin type anys"
This reverts commit 9c6a7679ac.
2026-06-06 22:29:41 -07:00
Jingyi-Dify 9c6a7679ac chore(web): remove explicit plugin type anys 2026-06-06 22:06:10 -07:00
Jingyi-Dify 11b2ba29c1 fix(web): preserve marketplace install dialog during loading 2026-06-06 21:54:42 -07:00
Jingyi-Dify c52df73117 fix(web): align integration loading skeletons with loaded cards 2026-06-06 21:54:42 -07:00
Jingyi-Dify 9b8d81c852 fix(web): keep plugin install tasks visible while pending 2026-06-06 21:54:42 -07:00
Joel b28b2892f1 chroe: remove app create card 2026-06-06 10:42:13 +08:00
Joel 7f5349e707 chore: type selctc 2026-06-06 10:33:14 +08:00
Joel 0aaa1df1b8 chroe: tags and types copywriting 2026-06-06 10:19:55 +08:00
Joel bd41c5c3c0 chore: tags filter and pipeline conrner 2026-06-06 10:04:22 +08:00
Joel 95a2eea611 chore: knowledge align and remove create 2026-06-06 09:38:08 +08:00
yyh 19193891cf fix: remove redundant main nav link titles 2026-06-05 12:23:50 +08:00
Joel 4746dd39d3 chore: only cloud can show upgrade 2026-06-05 11:36:18 +08:00
Jingyi-Dify a5e6f58285 fix(web): resolve lint issues 2026-06-04 20:19:05 -07:00
Jingyi-Dify 4bf0398873 fix(web): use workspace last opened time 2026-06-04 20:19:05 -07:00
fatelei e6db98ef64 feat: add task detail 2026-06-05 11:13:54 +08:00
Jingyi-Dify 6b694ce829 fix(web): polish tool provider details 2026-06-04 19:58:45 -07:00
Jingyi-Dify 0ab3b5d677 feat(web): improve plugins panel filtering 2026-06-04 19:58:45 -07:00
Jingyi-Dify 346fabda27 fix(web): redirect legacy integration routes 2026-06-04 19:58:45 -07:00
Jingyi-Dify 3da0f12815 fix(web): refine plugin task popover 2026-06-04 19:58:45 -07:00
Jingyi-Dify 303cff1353 feat(web): improve data source plugin actions 2026-06-04 19:58:45 -07:00
Jingyi-Dify 10c3849887 fix(web): improve model selector empty state 2026-06-04 19:58:45 -07:00
Jingyi-Dify 29295950ea refactor(web): update main nav account settings 2026-06-04 19:58:44 -07:00
yyh c61a7ce442 fix: use next link for navigate button 2026-06-05 10:44:21 +08:00
yyh ab383969a8 style: dropdown menu pd 2026-06-05 10:16:07 +08:00
yyh 8a0aab4d81 style: remove p-1 in dropdown popup 2026-06-05 10:10:47 +08:00
hjlarry d389284813 feat: add last_opened_at for workspace API 2026-06-05 10:08:13 +08:00
yyh 530e366440 fix: use link for workspace card credits 2026-06-05 09:55:46 +08:00
yyh e4510a7d8f Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-06-05 09:35:05 +08:00
yyh 19d7a9b5d9 fix: learn dify item use dropdown menu checkbox item and no switch 2026-06-04 20:42:33 +08:00
yyh 1d063e3fd6 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-06-04 20:38:39 +08:00
yyh 850c7e311f fix(web): align focus rings in nav surfaces 2026-06-04 20:33:19 +08:00
Joel de0ccbb960 fix: question mark ui 2026-06-04 18:31:06 +08:00
Joel 407d3e28bb fix: focus template focus ring not show the full 2026-06-04 17:24:28 +08:00
Joel 8a51b3a296 chroe: remove mock logic 2026-06-04 17:02:47 +08:00
Joel 41894ad182 chore: uis 2026-06-04 16:46:41 +08:00
Joel 95ea709c91 chroe: knowledge info more info 2026-06-04 16:46:41 +08:00
Joel 3a01b91a45 chore: sidebar ui 2026-06-04 16:46:41 +08:00
Joel f50abac3f9 chore: knowledge info 2026-06-04 16:46:41 +08:00
Joel 7f44b2f601 chore: knowledge sidebar ui 2026-06-04 16:46:41 +08:00
yyh c71d2ac460 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-06-04 16:43:22 +08:00
yyh 1a7542052e style: create from template modal 2026-06-04 16:06:46 +08:00
Joel 6d26f6ea73 chore: handle tempalte and learn dify click to new 2026-06-04 14:45:02 +08:00
Joel b0d3347de9 chore: padding 2026-06-04 14:12:00 +08:00
Joel bf2a35bff8 chore: slide ui 2026-06-04 13:56:35 +08:00
yyh 63d50a61d3 style: add pointer cursor to app filter checkbox 2026-06-04 13:14:01 +08:00
yyh 101a6fcc53 fix(web): update account header home action 2026-06-04 12:56:10 +08:00
Jingyi-Dify d407c1fbf7 chore(integrations): update endpoint copy and header tests 2026-06-03 21:19:26 -07:00
Jingyi-Dify fe6ee8aa04 style(plugins): align empty states and remove actions 2026-06-03 21:19:25 -07:00
Jingyi-Dify 25228e3cde fix(integrations): align tool create actions 2026-06-03 21:19:25 -07:00
Jingyi-Dify 8049da9331 fix(integrations): align custom endpoint toolbar with design 2026-06-03 21:19:25 -07:00
Jingyi-Dify e404195c8c fix(integrations): align navigation and marketplace layout 2026-06-03 21:19:25 -07:00
Jingyi-Dify b3c7110768 fix(plugins): show correct badges and endpoint metadata 2026-06-03 21:19:25 -07:00
Jingyi-Dify 7b2b21c348 feat(integrations): use plugin category APIs for integration pages 2026-06-03 21:19:24 -07:00
Jingyi-Dify 3d3a1f4f90 style(integrations): refine provider cards and tag filter 2026-06-03 21:19:24 -07:00
yyh 274944f05e Merge branch 'feat/ui-onboarding-rewrite' of https://github.com/langgenius/dify into feat/ui-onboarding-rewrite 2026-06-04 11:57:05 +08:00
yyh a0d53b9f07 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite
# Conflicts:
#	eslint-suppressions.json
#	web/app/components/header/account-dropdown/index.tsx
2026-06-04 11:56:43 +08:00
hjlarry a24d1a0bbc feat: add PluginCategory openAPI 2026-06-04 11:52:35 +08:00
Joel fffa89a10e chore: handle tag icon and jump action 2026-06-03 18:12:38 +08:00
Joel 422461f360 chore: handle card ui 2026-06-03 17:56:47 +08:00
Joel 2ce913aaac fix: slide scroll show other slide promble 2026-06-03 17:40:23 +08:00
hjlarry 30f0a69fea feat: add builtin tools 2026-06-03 17:29:41 +08:00
hjlarry 04e8c6127f Revert "feat: PluginInstallTaskStartResponse add optional PluginInstallTask"
This reverts commit ccc9122980.
2026-06-03 17:21:07 +08:00
Joel 20e37a0457 chore: add become a partnter link 2026-06-03 17:08:00 +08:00
Joel 36c7209301 chroe: description vertical padding 2026-06-03 16:54:24 +08:00
hjlarry ccc9122980 feat: PluginInstallTaskStartResponse add optional PluginInstallTask 2026-06-03 16:51:22 +08:00
Joel 9143d44ec6 chore: new all plugin icon and request comp 2026-06-03 16:45:42 +08:00
hjlarry 1f856960f0 feat: PluginList by category 2026-06-03 16:35:33 +08:00
Joel a8218d5809 chore: market header align 2026-06-03 16:25:24 +08:00
Joel 70ec17bc34 feat: support scroll to show more plugins in group 2026-06-03 16:15:19 +08:00
Joel e4ea9d2e07 feat: marketplace page change; 2026-06-03 15:50:34 +08:00
yyh b72bfe060d fix: learn dify no skeleton 2026-06-03 14:33:31 +08:00
yyh dfc7f136ef Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-06-03 14:27:46 +08:00
yyh 15d66814ee fix: consolidate explore home initial loading 2026-06-03 14:02:12 +08:00
yyh 5f109213a5 fix: eslint 2026-06-03 13:05:19 +08:00
yyh a53828e826 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite
# Conflicts:
#	eslint-suppressions.json
#	web/app/components/app/create-app-dialog/app-list/index.tsx
#	web/app/components/app/create-app-modal/index.tsx
#	web/app/components/app/create-from-dsl-modal/index.tsx
#	web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx
#	web/hooks/use-import-dsl.ts
2026-06-03 13:04:38 +08:00
Jingyi-Dify 3a2ea826ff fix(plugins): refresh model defaults after plugin changes 2026-06-02 15:54:52 -07:00
Jingyi-Dify 4581ce2f45 style(model-provider): align model selectors 2026-06-02 15:54:15 -07:00
Jingyi-Dify 3925b2bf4f fix(model-provider): handle unavailable credentials 2026-06-02 15:53:45 -07:00
Jingyi-Dify 5744148e6d style(model-provider): align quota panel 2026-06-02 15:53:18 -07:00
Jingyi-Dify 6ba42e6d73 refactor(model-provider): split provider page body 2026-06-02 15:52:42 -07:00
Jingyi-Dify aff22cb5ed refactor integrations header layout 2026-06-02 11:19:28 -07:00
Jingyi-Dify 58c4a174ba align model provider toolbar copy 2026-06-02 11:19:27 -07:00
Jingyi-Dify c98f65cbcb refactor integrations tool provider cards 2026-06-02 11:19:27 -07:00
Joel add7c75f18 chore: stadio no app tiny 2026-06-02 17:53:12 +08:00
Joel 75e74ee8b9 chore: no knowledge some tiny 2026-06-02 17:51:10 +08:00
Joel 40a5236553 fix: learn dify no ssr to fix not same to server side if not on 2026-06-02 17:38:09 +08:00
Joel f820813e9f chore: knowledge empty page 2026-06-02 17:25:15 +08:00
Joel c4c3a2b265 chore: no apps align 2026-06-02 17:02:00 +08:00
yyh 99be8b34c8 fix: reverts agents.md to main 2026-06-02 16:56:19 +08:00
Joel 777265d898 chore: no apps page 2026-06-02 16:47:58 +08:00
Joel 7024913866 chore: siderbar tiny ui 2026-06-02 16:47:58 +08:00
yyh 3c862c3e98 fix: style 2026-06-02 16:46:04 +08:00
yyh fb497c60dd refactor(web): connect goto anything triggers to atom state 2026-06-02 16:41:41 +08:00
yyh 075af9cd44 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite
# Conflicts:
#	web/app/components/goto-anything/hooks/__tests__/use-goto-anything-modal.spec.ts
#	web/app/components/goto-anything/hooks/use-goto-anything-modal.ts
2026-06-02 16:35:10 +08:00
yyh 18e07cac9a fix(ui): use css selector 2026-06-02 16:01:58 +08:00
yyh ba06ed5f41 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-06-02 15:48:10 +08:00
Joel 3fd9d5eb14 chore: install border and nav top 2026-06-02 15:38:13 +08:00
yyh 15d2714e9d fix lint 2026-06-02 15:34:19 +08:00
yyh f36852646f Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-06-02 15:34:02 +08:00
Joel 72c3cd0d67 chore: installed app 2026-06-02 15:24:32 +08:00
Joel fd0cb47a81 chore: spacing in home page 2026-06-02 14:52:37 +08:00
Joel 87382673f3 chore: templates ui 2026-06-02 14:47:38 +08:00
Joel 3c716a6eee chore: learn dify sketlon 2026-06-02 14:32:41 +08:00
Joel 8da34fb60b chore: update banner and learn dify ui 2026-06-02 14:24:18 +08:00
Joel f0efb73fd0 chroe: recents ui and home padding x 2026-06-02 13:51:17 +08:00
Joel e2bd2355e9 chroe: app sidebar and continue work 2026-06-02 13:46:54 +08:00
Joel 73b50f5ede chore: add splits in nav 2026-06-02 13:36:46 +08:00
Joel 52cfe62d8d fix: home page auto jump to /apps 2026-06-02 11:24:10 +08:00
Joel f293253a7a feat: add logs and annotions title 2026-06-02 11:11:59 +08:00
Joel 1ac3ad4c81 chore: split logs and annotations page 2026-06-02 11:01:38 +08:00
Jingyi-Dify 2d6fd70733 feat: align integrations auto-update settings dialog 2026-06-01 16:00:52 -07:00
Jingyi-Dify 639c8d5967 chore: update frontend review component guidance 2026-06-01 16:00:15 -07:00
Jingyi-Dify 74c9b7fddd fix(web): align integrations sidebar actions 2026-06-01 13:07:31 -07:00
yyh be997384f3 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-06-01 19:40:06 +08:00
yyh acbea6701c Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-06-01 18:33:10 +08:00
yyh 86ac4dadd6 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-06-01 16:29:36 +08:00
yyh f3e11ec0ee Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-06-01 16:21:24 +08:00
yyh c9c4fca7a7 chore: merge feat/local-storage-react-hook 2026-06-01 16:05:14 +08:00
yyh d23eddc924 tweaks 2026-06-01 15:49:12 +08:00
yyh d65c7229f2 refactor: replace useEffect with useIsomorphicLayoutEffect for better SSR compatibility 2026-06-01 15:39:10 +08:00
yyh 66508326f9 test: update workflow store persistence expectations 2026-06-01 15:30:39 +08:00
yyh 310f49229e Merge remote-tracking branch 'origin/main' into feat/local-storage-react-hook 2026-06-01 15:28:05 +08:00
yyh f400be4280 fix knip 2026-06-01 15:28:02 +08:00
yyh 2855ab3a15 refactor(web): migrate local storage util to hook 2026-06-01 15:23:18 +08:00
yyh e2fd5421d2 prune 2026-06-01 15:21:45 +08:00
yyh ff37ba83b4 refactor(web): bridge workflow storage through react 2026-06-01 15:14:45 +08:00
yyh 7b97ec57ef feat(web): add use local storage hook 2026-06-01 14:45:39 +08:00
yyh 19d34b5a93 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-06-01 13:10:27 +08:00
yyh 5c315ea7fe Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite
# Conflicts:
#	web/app/components/tools/mcp/index.tsx
2026-06-01 12:29:40 +08:00
Jingyi-Dify e525880773 feat(integrations): reorganize sidebar navigation 2026-05-31 20:06:06 -07:00
Jingyi-Dify d82e30561f feat(plugin): refine install task status controls 2026-05-31 20:06:06 -07:00
yyh d2508db11f Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-05-31 22:17:12 +08:00
yyh 7f76fe68ea fix: style 2026-05-31 22:14:14 +08:00
yyh ac09702d08 Merge branch 'codex/server-prefetch-current-workspace' into feat/ui-onboarding-rewrite 2026-05-31 22:00:25 +08:00
yyh b14d1e60ec fix: save local storage 2026-05-31 21:38:54 +08:00
yyh b072136ca0 fix: add console.error for error bounadries 2026-05-31 21:38:23 +08:00
yyh c8b6ec5fb0 fix: prefetch workspace info in server 2026-05-31 21:38:10 +08:00
yyh 0b5b4271f0 fix: add console.error() in common layout error boundary 2026-05-30 23:22:33 +08:00
yyh fd60339625 fix: safe local storage 2026-05-30 23:16:48 +08:00
yyh f56f93f5c2 fix(web): defer try app preview loading 2026-05-30 23:14:26 +08:00
yyh 2921d27929 fix: use query for workspace info 2026-05-30 21:04:57 +08:00
yyh 98e3bff509 fix: skeleton 2026-05-30 20:52:23 +08:00
yyh 6c43fa459e fix(auth): reset profile query after login 2026-05-30 20:30:32 +08:00
yyh d2eae74b5e fix(web): make full screen loading fill viewport 2026-05-30 19:50:43 +08:00
yyh 27bdee85fe fix(auth): avoid leaking request origin in refresh redirects 2026-05-30 13:59:53 +08:00
Jingyi-Dify cca8295ad8 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-05-29 11:27:07 -07:00
yyh 4d1b3605b5 style: use kbd and align style 2026-05-29 22:16:51 +08:00
yyh d94be05f68 fix(web): use default profile query cache 2026-05-29 22:00:00 +08:00
yyh fea7590779 fix: remove cursor pointer 2026-05-29 21:15:52 +08:00
yyh 71ba903d4c fix: banner and template card skeleton style 2026-05-29 21:15:03 +08:00
yyh 44831839d1 fix(web): center common layout error in content area 2026-05-29 21:12:49 +08:00
yyh 88c3512471 fix: add webapp header skeleton 2026-05-29 21:06:02 +08:00
yyh 67dee6e07e fix: env badge position 2026-05-29 21:02:37 +08:00
yyh 95d7fa997b fix: hide webapp section when length===0 2026-05-29 20:59:59 +08:00
yyh 4a0b177eee Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite
# Conflicts:
#	web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx
#	web/app/(commonLayout)/layout.tsx
#	web/app/components/full-screen-loading.tsx
#	web/app/components/header/header-wrapper.tsx
2026-05-29 20:39:10 +08:00
yyh 15b2a8fdb7 Merge remote-tracking branch 'origin/refactor/suspense-boundary' into feat/ui-onboarding-rewrite 2026-05-29 20:23:54 +08:00
yyh 65098a6b4f fix: e2e 2026-05-29 20:18:16 +08:00
yyh 8055f8840c fix: remove global loading 2026-05-29 19:39:03 +08:00
yyh f033f91a68 Merge remote-tracking branch 'origin/main' into refactor/suspense-boundary 2026-05-29 18:54:09 +08:00
yyh 0b98319bd3 fix: redirect profile 401 to refresh route 2026-05-29 18:53:56 +08:00
yyh 386de25e26 add notes 2026-05-29 18:52:36 +08:00
yyh d30805353a update lock 2026-05-29 17:09:39 +08:00
yyh 21c5825508 fix(web): declare server-only dependency 2026-05-29 17:05:38 +08:00
yyh 2d324add39 fix(web): guard server profile prefetch URL 2026-05-29 16:50:29 +08:00
yyh 62beaf493e fix: knip 2026-05-29 16:46:23 +08:00
yyhandGitHub b34e5aa915 Merge branch 'main' into refactor/suspense-boundary 2026-05-29 16:41:06 +08:00
yyh 2ea19c2b1c test: fix oauth registration analytics spec 2026-05-29 16:29:47 +08:00
yyh 5712f29e8b Merge remote-tracking branch 'origin/main' into refactor/suspense-boundary 2026-05-29 16:19:58 +08:00
yyh 93f7404c6b refactor: hydrate common layout profile query 2026-05-29 16:19:26 +08:00
yyh 4f631d6f4c fix 2026-05-29 16:06:52 +08:00
yyh 2b3e15cc83 Remove app initializer and move auth boot logic to route boundaries 2026-05-29 15:12:02 +08:00
yyh 35956247ab Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-05-29 11:51:24 +08:00
yyh 343531b9dc refactor(web): migrate learn dify visibility state to jotai 2026-05-29 10:50:49 +08:00
yyh 236f389fce Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-05-29 10:24:10 +08:00
Jingyi-Dify 5a2604265c fix: align integrations search empty states 2026-05-28 12:43:58 -07:00
Jingyi-Dify 6a8aaa5a36 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-05-28 11:20:10 -07:00
yyh 0ad1e8c2d9 fix: improve Dify logo accessibility 2026-05-28 16:48:22 +08:00
yyh bf345136eb fix: style 2026-05-28 16:40:34 +08:00
yyh c9ed50c3ae fix 2026-05-28 16:36:32 +08:00
yyh c78c603a38 fix: remove workflow fullscreen 2026-05-28 16:02:18 +08:00
yyh 48b38446a3 fix: improve frontend accessibility selectors 2026-05-28 15:30:37 +08:00
yyh 8a8bec4bc6 refactor: align integrations layout 2026-05-28 15:11:27 +08:00
yyh 95936a8bac refactor(web): remove workspace context 2026-05-28 12:48:16 +08:00
yyh ac8a1107ca fix: stacking context 2026-05-28 11:20:26 +08:00
Jingyi-Dify 0c96426d91 fix: link app sidebar studio breadcrumb 2026-05-27 18:47:52 -07:00
Jingyi-Dify 67fee14770 feat: migrate installed app route 2026-05-27 17:38:33 -07:00
Jingyi-Dify d94006162d refactor: split large onboarding UI components 2026-05-27 17:04:42 -07:00
Jingyi-Dify 3d53cee8a9 fix: render home loading skeletons 2026-05-27 16:46:51 -07:00
Jingyi-Dify 1acd1b568a fix: add integration loading placeholders 2026-05-27 16:46:23 -07:00
Jingyi-Dify 68f939f3b3 fix(web): narrow home active state 2026-05-27 14:52:25 -07:00
Jingyi-Dify 1f4b76ba7e fix(web): scope common layout loading and marketplace title 2026-05-27 14:51:57 -07:00
Jingyi-Dify 4d974d8f72 fix(web): use scroll areas for integrations lists 2026-05-27 14:51:31 -07:00
Jingyi-Dify 1dc12d1661 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-05-27 12:08:53 -07:00
yyh 82345977cd fix: separate main nav web app groups 2026-05-27 20:59:59 +08:00
yyh 83c943bc21 fix: virtualize main nav web apps 2026-05-27 20:35:12 +08:00
yyh 94a376a5a7 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-05-27 20:11:47 +08:00
yyh 33d95ab23a Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-05-27 16:40:30 +08:00
yyh 7a8a92082b refactor: use segmented control 2026-05-27 16:40:20 +08:00
yyh ef00f850e4 fix: use chip for app type selector 2026-05-27 16:18:49 +08:00
hjlarry 7815228395 Merge branch 'feat/ui-onboarding-rewrite' of github.com:langgenius/dify into feat/ui-onboarding-rewrite 2026-05-27 14:45:28 +08:00
hjlarry dcd40b5004 fix install_app n+1 query 2026-05-27 14:44:00 +08:00
yyh c252006644 fix: align help menu support item 2026-05-27 14:20:34 +08:00
yyh 9e5668c233 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-05-27 13:31:01 +08:00
yyh 52ce49b3c6 fix(workspace-card): fix style and add skeleton 2026-05-27 13:18:03 +08:00
yyh e90aa76ba2 fix: use dropdown menu 2026-05-27 13:02:43 +08:00
yyh de9373e1b8 chore(contracts): type plugin auto-upgrade responses 2026-05-27 11:33:56 +08:00
yyh 58923f38e6 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-05-27 11:03:21 +08:00
yyh 8486a5b213 feat(api): add plugin auto-upgrade category contracts 2026-05-27 11:03:04 +08:00
yyh 28a8be0d5f feat(api): add learn dify app list contract 2026-05-27 11:02:45 +08:00
yyh f2d4d5b267 fix lint 2026-05-27 10:34:36 +08:00
yyh f62a59a18a fix: resolve app and dataset lint errors 2026-05-27 10:33:51 +08:00
yyh b488812714 lint 2026-05-27 10:28:42 +08:00
Jingyi-Dify 755760b97c refactor(web): migrate mcp modal controls to dify ui primitives 2026-05-26 19:06:52 -07:00
Jingyi-Dify 955c3fb797 refactor(web): migrate search inputs to dify ui primitive 2026-05-26 19:06:52 -07:00
yyh 0c9aa20047 Merge remote-tracking branch 'origin/main' into feat/ui-onboarding-rewrite 2026-05-27 10:03:36 +08:00
Jingyi-Dify 065246a9a7 Merge origin/main into feat/ui-onboarding-rewrite 2026-05-26 17:50:55 -07:00
Jingyi-Dify 0d12b5ab1c test(web): cover integrations permission-gated actions 2026-05-21 16:44:33 -07:00
Jingyi-Dify 514dcae189 style(web): align update setting time picker token 2026-05-21 15:49:39 -07:00
Jingyi-Dify 228dd84a91 refactor(web): tighten tools provider card and marketplace handling 2026-05-21 15:49:27 -07:00
Jingyi-Dify 336ddad096 fix(web): expose current nav links to assistive tech 2026-05-21 15:49:13 -07:00
Jingyi-Dify 92bb9a17b7 fix(web): scope main nav fullscreen canvas routes 2026-05-21 15:49:02 -07:00
Jingyi-Dify b8868dab90 fix(integrations): update install success action copy 2026-05-21 10:54:43 -07:00
Jingyi-Dify 94225682cd docs: update main nav and integrations follow-ups 2026-05-20 17:11:16 -07:00
Jingyi-Dify 18b6568c2a fix: refine integrations sidebar controls 2026-05-20 16:21:25 -07:00
Jingyi-Dify a3a9ded29b chore: localize main nav and integrations copy 2026-05-20 15:50:52 -07:00
Jingyi-Dify de78a26920 fix: scope dataset detail navigation routes 2026-05-20 15:50:20 -07:00
Jingyi-Dify c54d029e7c fix: restore dataset list markup 2026-05-20 15:49:49 -07:00
Jingyi-Dify ad4b9dc2c3 refactor: reuse toggle group in update settings 2026-05-20 12:42:53 -07:00
Joel cdec0c69a6 chore: learn dify try action same to template 2026-05-20 18:04:43 +08:00
Joel 53acc3726c merge 2026-05-20 17:43:06 +08:00
Joel b1d393f4d9 chore: hide select model provider in model provider page 2026-05-20 15:22:14 +08:00
Joel 62e9bdd70d chore: app permission show in app card 2026-05-20 15:00:18 +08:00
Joel d36c76c20e merge 2026-05-20 14:34:04 +08:00
Jingyi-Dify f525e1a5eb fix(web): align onboarding and integrations i18n copy 2026-05-19 16:03:01 -07:00
Joel e2f779b20d chore: load 8 contiue items 2026-05-19 14:54:23 +08:00
Joel e198d6305c merge 2026-05-19 14:14:51 +08:00
Joel 5e67514265 chore: support edcation action 2026-05-19 13:56:49 +08:00
Joel b63896de87 feat: learn dify use api 2026-05-19 13:44:14 +08:00
Jingyi-Dify e463389f2c feat: improve integration install flows 2026-05-18 20:55:05 -07:00
Jingyi-Dify cda348ca10 feat: split plugin settings by category 2026-05-18 20:54:32 -07:00
Jingyi-Dify ca48050666 feat: refine integrations page shell controls 2026-05-18 20:54:11 -07:00
Jingyi-Dify 9c0f592f34 feat: open integrations settings in account modal 2026-05-18 20:53:47 -07:00
JoelandJingyi-Dify b70241ad36 fix: app list not refresh 2026-05-18 12:11:08 -07:00
JoelandJingyi-Dify 4abe622b2e feat: continue with use the app list data 2026-05-18 12:10:58 -07:00
JoelandJingyi-Dify 16c32c82e3 feat: knowledge new sidebar 2026-05-18 12:10:48 -07:00
JoelandJingyi-Dify 46424513d1 chore: missing files 2026-05-18 12:10:37 -07:00
JoelandJingyi-Dify 2c4baa20d8 feat: app new nav 2026-05-18 12:10:25 -07:00
Jingyi-Dify b0ae553f2e fix(web): correct custom icon class names 2026-05-18 12:07:16 -07:00
Jingyi-Dify 0266a12ee5 fix(web): align rebased UI type contracts 2026-05-18 11:19:12 -07:00
Jingyi-Dify 9d7765d5fd docs: update main nav follow-up notes 2026-05-18 11:16:16 -07:00
Jingyi-Dify d4ef983f42 refactor(web): organize integrations page helpers 2026-05-18 11:16:16 -07:00
Jingyi-Dify 018f36711d fix(web): route document settings to integrations 2026-05-18 11:16:16 -07:00
Jingyi-Dify dacd333e4a chore(i18n): rename plugin-facing copy to integrations 2026-05-18 11:16:16 -07:00
Jingyi-Dify b079a26314 fix(web): gate integrations install actions 2026-05-18 11:16:15 -07:00
Jingyi-Dify 7e953ebe0b feat(web): complete update setting popover 2026-05-18 11:16:15 -07:00
Jingyi-Dify b4d28fca54 fix(web): polish integration page titles 2026-05-18 11:16:15 -07:00
JoelandJingyi-Dify 728c6b8201 chore: rename to marketplace path 2026-05-18 11:16:15 -07:00
JoelandJingyi-Dify f56e23b5fd chore: remove discover entrance 2026-05-18 11:16:15 -07:00
JoelandJingyi-Dify 5600cefa53 feat: add interation discover route 2026-05-18 11:16:15 -07:00
JoelandJingyi-Dify 561eb9cbd2 fix: trigger, agent-strategry, extension problem 2026-05-18 11:16:15 -07:00
JoelandJingyi-Dify 83766ca694 chore: new pages add to dataset route guard 2026-05-18 11:16:15 -07:00
JoelandJingyi-Dify 678be94d22 fix: custom tool copywriting 2026-05-18 11:16:15 -07:00
JoelandJingyi-Dify 9e852429be chore: split logic from accont setting and integrating setting 2026-05-18 11:16:15 -07:00
JoelandJingyi-Dify d93c5028f1 chore: rename to integration setting 2026-05-18 11:14:15 -07:00
JoelandJingyi-Dify 54f189305e chore: use new hook to handle setting 2026-05-18 11:13:33 -07:00
JoelandJingyi-Dify a610a24507 chore: filter apps and knowledges no data 2026-05-18 11:12:17 -07:00
JoelandJingyi-Dify 05e8a94bb5 fix: not configure default model tip not align 2026-05-18 11:12:17 -07:00
JoelandJingyi-Dify b2e2e7b60b chore: homepage coninue with to improve 2026-05-18 11:12:17 -07:00
JoelandJingyi-Dify e7d2e66ff5 chore: popup create hide some 2026-05-18 11:12:17 -07:00
JoelandJingyi-Dify c51069685c chore: some tiny style 2026-05-18 11:12:17 -07:00
JoelandJingyi-Dify 28c208f36a feat: knowledge items 2026-05-18 11:12:17 -07:00
JoelandJingyi-Dify 53a1386b87 feat: knowledge title 2026-05-18 11:12:17 -07:00
JoelandJingyi-Dify 0e366c7300 chore: show no empty logic 2026-05-18 11:12:17 -07:00
JoelandJingyi-Dify 939bdde373 feat: knowledge empty list 2026-05-18 11:12:17 -07:00
Jingyi-Dify 13dfa3aba4 feat(integrations): add unavailable page fallback 2026-05-18 11:12:16 -07:00
Jingyi-Dify 2705a7c1db feat(integrations): align tools and plugin category UI 2026-05-18 11:12:16 -07:00
Jingyi-Dify 258a751b8c feat(integrations): improve data source plugin management 2026-05-18 11:12:16 -07:00
Jingyi-Dify 5a35d3d9cd feat(plugin): add update settings popover 2026-05-18 11:12:16 -07:00
Jingyi-Dify c3fbafae83 chore(i18n): localize integrations updates 2026-05-18 11:12:16 -07:00
Jingyi-Dify f727c8f838 docs: update frontend agent guidance 2026-05-18 11:12:16 -07:00
JoelandJingyi-Dify 90af4c39b4 chore: some small ui 2026-05-18 11:12:16 -07:00
JoelandJingyi-Dify f7c3a4e4cb feat: empty page 2026-05-18 11:12:16 -07:00
JoelandJingyi-Dify be7d043edd chore: remove mock app data 2026-05-18 11:12:16 -07:00
JoelandJingyi-Dify cef8fe3a4b chore: remove shortcut 2026-05-18 11:12:16 -07:00
JoelandJingyi-Dify afe0e6c393 chore: missing files 2026-05-18 11:12:16 -07:00
JoelandJingyi-Dify 37309b931e feat: new head 2026-05-18 11:12:15 -07:00
JoelandJingyi-Dify 6a83c6705c temp: app hearder 2026-05-18 11:10:59 -07:00
JoelandJingyi-Dify 3e75d5e443 chore: create app card 2026-05-18 11:10:11 -07:00
JoelandJingyi-Dify 7be8a5b883 chore: app card ui 2026-05-18 11:10:11 -07:00
Jingyi-Dify 80dcb344f4 docs: record integrations install permission follow-up 2026-05-18 11:10:11 -07:00
Jingyi-Dify b029c9b1cd feat: add integrations plugin category views 2026-05-18 11:10:11 -07:00
Jingyi-Dify 6cb97e9201 fix: align tools and mcp provider behavior 2026-05-18 11:10:11 -07:00
Jingyi-Dify 4ef2e952bd feat: add integrations page shell refinements 2026-05-18 11:10:10 -07:00
Jingyi-Dify cc5545339c docs: update frontend review guidance
Document shared component reuse and component-writing checks for future frontend reviews, and refresh the MainNav follow-up notes.
2026-05-18 11:10:10 -07:00
Jingyi-Dify 0a8c46a3a7 refactor: polish integrations and main nav UI
Reuse shared base controls in MainNav and Integrations, add active integration icons, and keep compact integration content framing covered by targeted tests.
2026-05-18 11:10:10 -07:00
Jingyi-Dify 65770903d1 feat: refine integrations layout and controls
- add integrations headers, install action, permission quick settings, and update setting entry points

- centralize default vs compact content insets for integrations child pages

- cover provider, plugin, marketplace, MCP, and model provider behaviors with focused tests
2026-05-18 11:10:10 -07:00
Jingyi-Dify 5a6ba2ffb5 fix: localize integrations i18n copy 2026-05-18 11:09:15 -07:00
Jingyi-Dify aa53afe07d fix: update custom tool integration route 2026-05-18 11:09:14 -07:00
Jingyi-Dify 4740a89f4a feat: add canonical integrations routes 2026-05-18 11:09:14 -07:00
Jingyi-Dify 328db3d67a fix: align main nav interactions
Update active main nav icon positioning from the refreshed Figma assets, remove the transparent active border that caused nav item jitter, and route mobile common layout through the new MainNav instead of the legacy Header.

Also align workspace plan actions with the new UI contract by showing Upgrade for sandbox workspaces and View Plan for paid workspaces, both opening the pricing modal.
2026-05-18 11:09:14 -07:00
JoelandJingyi-Dify 88062fb247 feat: explore page to home page 2026-05-18 11:09:14 -07:00
JoelandJingyi-Dify 045da59220 chore: app card icon and palce of learn dify 2026-05-18 11:09:14 -07:00
JoelandJingyi-Dify 948b0f6bc7 chore: templates item ui and learn dify 2026-05-18 11:09:14 -07:00
JoelandJingyi-Dify 14a59f6e44 chore: tag ui 2026-05-18 11:09:14 -07:00
JoelandJingyi-Dify f9f361113e feat: add description and tag filter 2026-05-18 11:09:14 -07:00
JoelandJingyi-Dify eea6f59307 chore: remove more learning templates and templates copywrite 2026-05-18 11:09:14 -07:00
JoelandJingyi-Dify 718f69dc43 feat: hide learn dify anim effect 2026-05-18 11:09:14 -07:00
JoelandJingyi-Dify 82a2ba9264 feat: learn dify 2026-05-18 11:09:14 -07:00
JoelandJingyi-Dify 6c8e032fbb chore: fix small css 2026-05-18 11:09:14 -07:00
JoelandJingyi-Dify 28c2c3bfd3 chore: split icon to new file and enchance data struct 2026-05-18 11:09:14 -07:00
JoelandJingyi-Dify 9d463e1024 feat: continue work 2026-05-18 11:09:14 -07:00
JoelandJingyi-Dify 7f87616625 chore: no show slide logic 2026-05-18 11:09:14 -07:00
JoelandJingyi-Dify 43a04ed0c2 feat: finish slide 2026-05-18 11:09:13 -07:00
Jingyi-Dify 5083edd0ce fix: align main nav gating and account popup behavior 2026-05-18 11:09:13 -07:00
Jingyi-Dify 8306fa41b9 fix(web): align main nav defaults
Default integrations to the model provider section and route the main nav entry there.

Hide cloud-only workspace credits and upgrade actions outside cloud edition.

Add the repo-local karpathy-guidelines skill.
2026-05-18 11:09:13 -07:00
Jingyi-Dify 8f33305e90 docs: update iconify review guidance
- generalize generated icon diff review guidance for intrinsic width and height changes
2026-05-18 11:09:13 -07:00
Jingyi-Dify 7077a43c1c feat: add integrations tools page with prebuilt icons
- add the integrations page sidebar with collapsible icon-only navigation and Figma-aligned marketplace card
- move custom integration SVGs into the iconify collection and document the Tailwind i-custom workflow
- preserve source SVG collection dimensions when flattening generated icon data so existing main nav icons keep their 20x20 viewBox
- add an icon dimension guard for layout-sensitive generated icons
- update model provider routing, i18n, and focused frontend tests
2026-05-18 11:09:13 -07:00
Jingyi-Dify 884a43ae0a fix(web): preserve settings fallbacks during main nav update
- hide migrated settings tabs from the account settings sidebar

- add disabled integrations destination mapping for future migration

- keep legacy settings modal fallback until integrations sections are ready

- restore main nav active styling and add titles for truncated labels
2026-05-18 11:09:13 -07:00
Jingyi-Dify 914f89f478 refactor(web): align main nav review feedback
- move main nav active edge styling into Tailwind classes

- split account dropdown menu content into focused components

- align frontend review skill rules with i18n and styling guidance

- add missing common i18n keys across supported locales
2026-05-18 11:09:13 -07:00
Jingyi-Dify 163153db18 refactor(web): split main nav components
- Move MainNav sections into focused components under main-nav/components

- Reuse Explore AppNavItem for MainNav web app rows via a mainNav variant

- Keep WorkspaceCard expanded panel behavior and styling aligned with the pre-refactor UI
2026-05-18 11:09:13 -07:00
Jingyi-Dify 49d890d514 feat(web): refine main nav onboarding UI
- Add a reusable dimm Badge variant for workspace plan labels

- Update MainNav workspace, web apps, account, and help menu styling to match Figma

- Add MainNav-specific account dropdown with appearance, language, timezone, and logout entries

- Keep account trigger compact without plan badge while preserving the badge in the popup header

- Prevent the common layout shell from creating a page-level scrollbar
2026-05-18 11:09:13 -07:00
Jingyi-Dify 0292bc2728 feat: refine desktop main nav visuals 2026-05-18 11:09:13 -07:00
Jingyi-Dify 5c21120977 feat: add desktop main navigation 2026-05-18 11:09:13 -07:00
1000 changed files with 39138 additions and 12196 deletions
@@ -0,0 +1,33 @@
---
name: karpathy-guidelines
description: Lightweight coding guardrails for making focused, simple, and verifiable changes in this repo. Use for all coding work.
---
# Karpathy Guidelines
Use this skill whenever you touch code in this repository.
## Principles
- Keep the change small and directly tied to the user request.
- Prefer the simplest implementation that fits the existing codebase.
- Read the nearby code first, then match its patterns.
- Avoid unrelated refactors, broad rewrites, or style churn.
- Preserve existing behavior unless the user explicitly asked to change it.
- Treat regressions as a signal to narrow the change, not to add workaround layers.
## Workflow
1. Inspect the current implementation and tests around the change.
2. Make the smallest coherent edit.
3. Add or update focused tests when the behavior changes or the risk is non-trivial.
4. Run the narrowest relevant verification first.
5. Report exactly what was verified and anything left unverified.
## Review Checklist
- Does this change solve the stated problem without expanding scope?
- Did it preserve existing route/component/data-flow semantics?
- Are new abstractions justified by real complexity?
- Are tests focused on the behavior that could regress?
- Are unrelated files and generated artifacts left alone?
+2
View File
@@ -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",
+108 -1
View File
@@ -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)
+140 -45
View File
@@ -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
@@ -14,7 +15,12 @@ from werkzeug.exceptions import BadRequest
from controllers.common.fields import RedirectUrlResponse, SimpleResultResponse
from controllers.common.helpers import FileInfo
from controllers.common.schema import register_enum_models, register_response_schema_models, register_schema_models
from controllers.common.schema import (
query_params_from_model,
register_enum_models,
register_response_schema_models,
register_schema_models,
)
from controllers.console import console_ns
from controllers.console.app.wraps import get_app_model, with_session
from controllers.console.workspace.models import LoadBalancingPayload
@@ -36,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 (
@@ -68,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")
@@ -114,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]] = []
@@ -377,6 +395,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
@@ -446,12 +465,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, RedirectUrlResponse, SimpleResultResponse)
register_schema_models(
console_ns,
AppListQuery,
StarredAppListQuery,
CreateAppPayload,
UpdateAppPayload,
CopyAppPayload,
@@ -495,7 +556,7 @@ register_schema_models(
class AppListApi(Resource):
@console_ns.doc("list_apps")
@console_ns.doc(description="Get list of applications with pagination and filtering")
@console_ns.expect(console_ns.models[AppListQuery.__name__])
@console_ns.doc(params=query_params_from_model(AppListQuery))
@console_ns.response(200, "Success", console_ns.models[AppPagination.__name__])
@setup_required
@login_required
@@ -511,6 +572,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,
@@ -524,46 +586,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
@@ -599,6 +622,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/<uuid:app_id>/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/<uuid:app_id>")
class AppApi(Resource):
@console_ns.doc("get_app_detail")
@@ -155,19 +155,28 @@ class InstalledAppsListApi(Resource):
if current_user.current_tenant is None:
raise ValueError("current_user.current_tenant must not be None")
current_user.role = TenantService.get_user_role(current_user, current_user.current_tenant)
installed_app_list: list[dict[str, Any]] = [
{
"id": installed_app.id,
"app": installed_app.app,
"app_owner_tenant_id": installed_app.app_owner_tenant_id,
"is_pinned": installed_app.is_pinned,
"last_used_at": installed_app.last_used_at,
"editable": current_user.role in {"owner", "admin"},
"uninstallable": current_tenant_id == installed_app.app_owner_tenant_id,
}
for installed_app in installed_apps
if installed_app.app is not None
]
app_ids = [installed_app.app_id for installed_app in installed_apps]
apps = db.session.scalars(select(App).where(App.id.in_(app_ids))).all() if app_ids else []
apps_by_id = {app.id: app for app in apps}
installed_app_list: list[dict[str, Any]] = []
for installed_app in installed_apps:
app_model = apps_by_id.get(installed_app.app_id)
if app_model is None:
continue
installed_app_list.append(
{
"id": installed_app.id,
"app": app_model,
"app_owner_tenant_id": installed_app.app_owner_tenant_id,
"is_pinned": installed_app.is_pinned,
"last_used_at": installed_app.last_used_at,
"editable": current_user.role in {"owner", "admin"},
"uninstallable": current_tenant_id == installed_app.app_owner_tenant_id,
}
)
# filter out apps that user doesn't have access to
if FeatureService.get_system_features().webapp_auth.enabled:
@@ -65,15 +65,28 @@ class RecommendedAppListResponse(ResponseModel):
categories: list[str]
class LearnDifyAppListResponse(ResponseModel):
recommended_apps: list[RecommendedAppResponse]
register_schema_models(
console_ns,
RecommendedAppsQuery,
RecommendedAppInfoResponse,
RecommendedAppResponse,
RecommendedAppListResponse,
LearnDifyAppListResponse,
)
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))
@@ -84,13 +97,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(language_prefix),
@@ -98,6 +105,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(language_prefix),
from_attributes=True,
).model_dump(mode="json")
@console_ns.route("/explore/apps/<uuid:app_id>")
class RecommendedAppApi(Resource):
@login_required
+259 -76
View File
@@ -1,28 +1,51 @@
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
from pydantic import BaseModel, ConfigDict, Field
from werkzeug.datastructures import FileStorage
from werkzeug.exceptions import Forbidden
from configs import dify_config
from controllers.common.fields import SuccessResponse
from controllers.common.schema import register_enum_models, register_response_schema_models, register_schema_models
from controllers.common.schema import (
query_params_from_model,
register_enum_models,
register_response_schema_models,
register_schema_models,
)
from controllers.console import console_ns
from controllers.console.workspace import plugin_permission_required
from controllers.console.wraps import account_initialization_required, is_admin_or_owner_required, setup_required
from core.helper.position_helper import is_filtered
from core.plugin.entities.plugin import PluginCategory, PluginDeclaration, 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 current_account_with_tenant, login_required
from models.account import TenantPluginAutoUpgradeStrategy, TenantPluginPermission
from models.provider_ids import ToolProviderID
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):
@@ -30,6 +53,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]
@@ -88,8 +116,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):
@@ -125,13 +153,40 @@ 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 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):
@@ -145,9 +200,67 @@ 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: PluginDeclaration
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
register_schema_models(
console_ns,
ParserList,
PluginCategoryListQuery,
PluginAutoUpgradeSettingsPayload,
PluginPermissionSettingsPayload,
ParserLatest,
@@ -164,21 +277,57 @@ register_schema_models(
ParserPermissionChange,
ParserDynamicOptions,
ParserDynamicOptionsWithCredentials,
ParserPreferencesChange,
ParserAutoUpgradeChange,
ParserAutoUpgradeFetch,
ParserExcludePlugin,
ParserReadme,
)
register_response_schema_models(console_ns, PluginDebuggingKeyResponse, SuccessResponse)
register_response_schema_models(
console_ns,
PluginAutoUpgradeChangeResponse,
PluginAutoUpgradeFetchResponse,
PluginAutoUpgradeSettingsResponseModel,
PluginDebuggingKeyResponse,
PluginCategoryInstalledPluginResponse,
PluginCategoryBuiltinToolResponse,
PluginCategoryBuiltinToolProviderResponse,
PluginCategoryListResponse,
SuccessResponse,
)
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.
@@ -193,6 +342,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__])
@@ -230,6 +406,41 @@ class PluginListApi(Resource):
return jsonable_encoder({"plugins": plugins_with_total.list, "total": plugins_with_total.total})
@console_ns.route("/workspaces/current/plugin/<string:category>/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
def get(self, category: str):
_, tenant_id = current_account_with_tenant()
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__])
@@ -632,11 +843,13 @@ class PluginChangePermissionApi(Resource):
tenant_id = current_tenant_id
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")
@@ -725,9 +938,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.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
@@ -736,38 +950,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"})
@@ -775,48 +968,36 @@ class PluginChangePreferencesApi(Resource):
return jsonable_encoder({"success": True})
@console_ns.route("/workspaces/current/plugin/preferences/fetch")
class PluginFetchPreferencesApi(Resource):
@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
def get(self):
_, tenant_id = current_account_with_tenant()
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[SuccessResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@@ -826,7 +1007,9 @@ class PluginAutoUpgradeExcludePluginApi(Resource):
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")
+11 -4
View File
@@ -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
@@ -144,6 +144,7 @@ tenants_fields = {
"plan": fields.String,
"status": fields.String,
"created_at": TimestampField,
"last_opened_at": OptionalTimestampField,
"current": fields.Boolean,
}
@@ -158,7 +159,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
@@ -171,7 +177,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)
@@ -190,6 +196,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,
}
@@ -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.")
+12
View File
@@ -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,
+15
View File
@@ -23,6 +23,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,
@@ -33,6 +34,7 @@ from core.plugin.entities.plugin_daemon import (
PluginInstallTask,
PluginInstallTaskStatus,
PluginListResponse,
PluginListWithoutTotalResponse,
PluginModelProviderEntity,
PluginVerification,
)
@@ -295,6 +297,19 @@ class PluginService:
plugins = manager.list_plugins_with_total(tenant_id, page, page_size)
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 list_installations_from_ids(tenant_id: str, ids: Sequence[str]) -> Sequence[PluginInstallation]:
"""
+2
View File
@@ -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,
@@ -0,0 +1,42 @@
"""add plugin auto upgrade category
Revision ID: f6a7b8c9d012
Revises: 8d4c2a1b9f03
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 = "8d4c2a1b9f03"
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"])
@@ -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")
@@ -1,7 +1,7 @@
"""add customized snippets
Revision ID: 2b3c4d5e6f70
Revises: 8d4c2a1b9f03
Revises: f5e8a9c0d2b3
Create Date: 2026-06-03 11:00:00.000000
"""
@@ -13,7 +13,7 @@ import models.types
# revision identifiers, used by Alembic.
revision = "2b3c4d5e6f70"
down_revision = "8d4c2a1b9f03"
down_revision = "f5e8a9c0d2b3"
branch_labels = None
depends_on = None
@@ -0,0 +1,26 @@
"""add tenant account join last opened at
Revision ID: b7c2d9e8a1f4
Revises: 2b3c4d5e6f70
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 = "2b3c4d5e6f70"
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")
@@ -0,0 +1,38 @@
"""add app stars
Revision ID: c4d5e6f7a8b9
Revises: b7c2d9e8a1f4
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 = "b7c2d9e8a1f4"
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")
+2
View File
@@ -71,6 +71,7 @@ from .model import (
AppMCPServer,
AppMode,
AppModelConfig,
AppStar,
Conversation,
DatasetRetrieverResource,
DifySetup,
@@ -171,6 +172,7 @@ __all__ = [
"AppMCPServer",
"AppMode",
"AppModelConfig",
"AppStar",
"AppTrigger",
"AppTriggerStatus",
"AppTriggerType",
+17 -1
View File
@@ -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,
+26
View File
@@ -654,6 +654,29 @@ 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()
)
def __repr__(self) -> str:
return f"<AppStar app_id={self.app_id} account_id={self.account_id}>"
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 +930,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),
+88 -48
View File
@@ -6043,6 +6043,21 @@ Delete an API key for a dataset
| ---- | ----------- | ------ |
| 200 | Success | [RecommendedAppListResponse](#recommendedapplistresponse) |
### /explore/apps/learn-dify
#### GET
##### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| language | query | Language code for recommended app localization | No | string |
##### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Success | [LearnDifyAppListResponse](#learndifyapplistresponse) |
### /explore/apps/{app_id}
#### GET
@@ -10180,6 +10195,51 @@ Returns permission flags that control workspace features like member invitations
| ---- | ----------- |
| 200 | Success |
### /workspaces/current/plugin/auto-upgrade/change
#### POST
##### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| payload | body | | Yes | [ParserAutoUpgradeChange](#parserautoupgradechange) |
##### Responses
| Code | Description |
| ---- | ----------- |
| 200 | Success |
### /workspaces/current/plugin/auto-upgrade/exclude
#### POST
##### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| payload | body | | Yes | [ParserExcludePlugin](#parserexcludeplugin) |
##### Responses
| Code | Description |
| ---- | ----------- |
| 200 | Success |
### /workspaces/current/plugin/auto-upgrade/fetch
#### GET
##### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| category | query | | Yes | string |
##### Responses
| Code | Description |
| ---- | ----------- |
| 200 | Success |
### /workspaces/current/plugin/debugging-key
#### GET
@@ -10382,45 +10442,6 @@ Fetch dynamic options using credentials directly (for edit mode)
| ---- | ----------- |
| 200 | Success |
### /workspaces/current/plugin/preferences/autoupgrade/exclude
#### POST
##### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| payload | body | | Yes | [ParserExcludePlugin](#parserexcludeplugin) |
##### Responses
| Code | Description |
| ---- | ----------- |
| 200 | Success |
### /workspaces/current/plugin/preferences/change
#### POST
##### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| payload | body | | Yes | [ParserPreferencesChange](#parserpreferenceschange) |
##### Responses
| Code | Description |
| ---- | ----------- |
| 200 | Success |
### /workspaces/current/plugin/preferences/fetch
#### GET
##### Responses
| Code | Description |
| ---- | ----------- |
| 200 | Success |
### /workspaces/current/plugin/readme
#### GET
@@ -15137,6 +15158,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 |
@@ -15648,6 +15675,19 @@ Form input definition.
| 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 |
@@ -15744,6 +15784,7 @@ Form input definition.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| category | [PluginCategory](#plugincategory) | | Yes |
| plugin_id | string | | Yes |
#### ParserGetCredentials
@@ -15831,8 +15872,8 @@ Form input definition.
| 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
@@ -15862,13 +15903,6 @@ Form input definition.
| 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 |
@@ -16023,6 +16057,12 @@ Shared permission levels for resources (datasets, credentials, etc.)
| upgrade_mode | [UpgradeMode](#upgrademode) | | No |
| upgrade_time_of_day | integer | | No |
#### PluginCategory
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| PluginCategory | string | | |
#### PluginDebuggingKeyResponse
| Name | Type | Description | Required |
@@ -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
+14 -9
View File
@@ -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()
+173 -42
View File
@@ -22,7 +22,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 AgentIconType
from models.model import App, AppMode, AppModelConfig, IconType, Site
from models.tools import ApiToolProvider
@@ -35,19 +35,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
@@ -61,6 +71,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)
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,
@@ -108,61 +195,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)
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 # type: ignore[attr-defined]
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 # type: ignore[attr-defined]
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
@@ -493,6 +623,7 @@ class AppService:
"""
app_was_deleted.send(app)
db.session.execute(sa.delete(AppStar).where(AppStar.tenant_id == app.tenant_id, AppStar.app_id == app.id))
db.session.delete(app)
db.session.commit()
@@ -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
@@ -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 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] = []
+23 -10
View File
@@ -6,6 +6,7 @@ from configs import dify_config
from extensions.ext_database import db
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 = db.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(app_id)
return result
@classmethod
def get_learn_dify_apps(cls, 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(app["app_id"])
return {"recommended_apps": result["recommended_apps"]}
@classmethod
def get_recommend_app_detail(cls, 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 = db.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(app_id)
return result
@classmethod
@@ -77,3 +85,8 @@ class RecommendedAppService:
else:
db.session.add(AccountTrialAppRecord(app_id=app_id, count=1, account_id=account_id))
db.session.commit()
@staticmethod
def _can_trial_app(app_id: str) -> bool:
trial_app_model = db.session.scalar(select(TrialApp).where(TrialApp.app_id == app_id).limit(1))
return trial_app_model is not None
@@ -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:
@@ -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
@@ -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):
@@ -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
):
@@ -36,6 +36,7 @@ def current_user(tenant_id: str) -> MagicMock:
def installed_app() -> MagicMock:
app = MagicMock()
app.id = "ia1"
app.app_id = "a1"
app.app = MagicMock(id="a1")
app.app_owner_tenant_id = "t2"
app.is_pinned = False
@@ -43,6 +44,22 @@ def installed_app() -> MagicMock:
return app
def make_scalars_result(items: list[MagicMock]) -> MagicMock:
result = MagicMock()
result.all.return_value = items
return result
def make_installed_apps_session(installed_apps: list[MagicMock]) -> MagicMock:
session = MagicMock()
app_models = [installed_app.app for installed_app in installed_apps if installed_app.app is not None]
session.scalars.side_effect = [
make_scalars_result(installed_apps),
make_scalars_result(app_models),
]
return session
@pytest.fixture
def payload_patch() -> PayloadPatch:
def _patch(payload: Payload) -> AbstractContextManager[object]:
@@ -63,8 +80,7 @@ class TestInstalledAppsListApi:
api = module.InstalledAppsListApi()
method = unwrap(api.get)
session = MagicMock()
session.scalars.return_value.all.return_value = [installed_app]
session = make_installed_apps_session([installed_app])
with (
app.test_request_context("/"),
@@ -86,8 +102,7 @@ class TestInstalledAppsListApi:
api = module.InstalledAppsListApi()
method = unwrap(api.get)
session = MagicMock()
session.scalars.return_value.all.return_value = []
session = make_installed_apps_session([])
with (
app.test_request_context("/?app_id=a1"),
@@ -110,8 +125,7 @@ class TestInstalledAppsListApi:
api = module.InstalledAppsListApi()
method = unwrap(api.get)
session = MagicMock()
session.scalars.return_value.all.return_value = [installed_app]
session = make_installed_apps_session([installed_app])
mock_webapp_setting = MagicMock()
mock_webapp_setting.access_mode = "restricted"
@@ -147,8 +161,7 @@ class TestInstalledAppsListApi:
api = module.InstalledAppsListApi()
method = unwrap(api.get)
session = MagicMock()
session.scalars.return_value.all.return_value = [installed_app]
session = make_installed_apps_session([installed_app])
mock_webapp_setting = MagicMock()
mock_webapp_setting.access_mode = "restricted"
@@ -184,8 +197,7 @@ class TestInstalledAppsListApi:
api = module.InstalledAppsListApi()
method = unwrap(api.get)
session = MagicMock()
session.scalars.return_value.all.return_value = [installed_app]
session = make_installed_apps_session([installed_app])
mock_webapp_setting = MagicMock()
mock_webapp_setting.access_mode = "sso_verified"
@@ -215,10 +227,10 @@ class TestInstalledAppsListApi:
method = unwrap(api.get)
installed_app_with_null = MagicMock()
installed_app_with_null.app_id = "a1"
installed_app_with_null.app = None
session = MagicMock()
session.scalars.return_value.all.return_value = [installed_app_with_null]
session = make_installed_apps_session([installed_app_with_null])
with (
app.test_request_context("/"),
@@ -242,8 +254,7 @@ class TestInstalledAppsListApi:
current_user = MagicMock()
current_user.current_tenant = None
session = MagicMock()
session.scalars.return_value.all.return_value = [installed_app]
session = make_installed_apps_session([installed_app])
with (
app.test_request_context("/"),
@@ -79,6 +79,48 @@ 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, "current_user", MagicMock(interface_language="fr-FR")),
patch.object(
module.RecommendedAppService,
"get_learn_dify_apps",
return_value=result_data,
) as service_mock,
):
result = method(api)
service_mock.assert_called_once_with("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, "current_user", MagicMock(interface_language="fr-FR")),
patch.object(
module.RecommendedAppService,
"get_learn_dify_apps",
return_value=result_data,
) as service_mock,
):
result = method(api)
service_mock.assert_called_once_with("fr-FR")
assert result == result_data
class TestRecommendedAppApi:
def test_get_success(self, app: Flask):
api = module.RecommendedAppApi()
@@ -144,3 +186,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"]
@@ -1,4 +1,6 @@
import io
from datetime import datetime
from typing import Any
from unittest.mock import MagicMock, patch
import pytest
@@ -9,12 +11,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,
@@ -22,7 +26,6 @@ from controllers.console.workspace.plugin import (
PluginFetchManifestApi,
PluginFetchMarketplacePkgApi,
PluginFetchPermissionApi,
PluginFetchPreferencesApi,
PluginIconApi,
PluginInstallFromGithubApi,
PluginInstallFromMarketplaceApi,
@@ -42,6 +45,68 @@ from core.plugin.impl.exc import PluginDaemonClientSideError
from models.account import TenantPluginAutoUpgradeStrategy, TenantPluginPermission
def _plugin_category_list_item() -> 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"},
"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 unwrap(func):
while hasattr(func, "__wrapped__"):
func = func.__wrapped__
@@ -142,6 +207,87 @@ class TestPluginListApi:
assert result["total"] == 1
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.current_account_with_tenant", return_value=(None, "t1")),
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, "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()], has_more=False)
with (
app.test_request_context("/?page=1&page_size=10"),
patch("controllers.console.workspace.plugin.current_account_with_tenant", return_value=(None, "t1")),
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, "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"),
patch("controllers.console.workspace.plugin.current_account_with_tenant", return_value=(None, "t1")),
):
result = method(api, "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.current_account_with_tenant", return_value=(None, "t1")),
patch(
"controllers.console.workspace.plugin.PluginService.list_by_category",
side_effect=PluginDaemonClientSideError("error"),
),
):
result = method(api, "tool")
assert result == ({"code": "plugin_error", "message": "error"}, 400)
class TestPluginIconApi:
def test_plugin_icon(self, app: Flask):
api = PluginIconApi()
@@ -901,18 +1047,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 = MagicMock(is_admin_or_owner=True)
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,
@@ -925,24 +1068,53 @@ class TestPluginChangePreferencesApi:
with (
app.test_request_context("/", json=payload),
patch("controllers.console.workspace.plugin.current_account_with_tenant", return_value=(user, "t1")),
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)
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 = MagicMock(is_admin_or_owner=True)
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.current_account_with_tenant", return_value=(user, "t1")),
patch(
"controllers.console.workspace.plugin.PluginAutoUpgradeService.change_strategy", return_value=True
) as change,
):
result = method(api)
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,
@@ -955,24 +1127,20 @@ class TestPluginChangePreferencesApi:
with (
app.test_request_context("/", json=payload),
patch("controllers.console.workspace.plugin.current_account_with_tenant", return_value=(user, "t1")),
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)
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,
@@ -981,19 +1149,17 @@ class TestPluginFetchPreferencesApi:
)
with (
app.test_request_context("/"),
app.test_request_context(f"/?category={TenantPluginAutoUpgradeStrategy.PluginCategory.TOOL.value}"),
patch("controllers.console.workspace.plugin.current_account_with_tenant", return_value=(None, "t1")),
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)
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:
@@ -1001,7 +1167,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),
@@ -1016,7 +1182,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),
@@ -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),
@@ -1,5 +1,7 @@
"""Swagger JSON rendering tests for Flask-RESTX API blueprints."""
import json
import pytest
from flask import Flask
@@ -160,3 +162,36 @@ 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_swagger_path = next(path for path in written_paths if path.name == "console-swagger.json")
payload = json.loads(console_swagger_path.read_text(encoding="utf-8"))
operation = payload["paths"]["/workspaces/current/plugin/{category}/list"]["get"]
response_ref = operation["responses"]["200"]["schema"]["$ref"].removeprefix("#/definitions/")
response_schema = payload["definitions"][response_ref]
assert response_schema["properties"]["plugins"]["items"]["$ref"] == (
"#/definitions/PluginCategoryInstalledPluginResponse"
)
assert response_schema["properties"]["builtin_tools"]["items"]["$ref"] == (
"#/definitions/PluginCategoryBuiltinToolProviderResponse"
)
installed_plugin_schema = payload["definitions"]["PluginCategoryInstalledPluginResponse"]
for field in (
"plugin_unique_identifier",
"source",
"version",
"declaration",
"endpoints_active",
"endpoints_setups",
):
assert field in installed_plugin_schema["properties"]
builtin_tool_schema = payload["definitions"]["PluginCategoryBuiltinToolProviderResponse"]
for field in ("plugin_unique_identifier", "team_credentials", "type", "tools"):
assert field in builtin_tool_schema["properties"]
@@ -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
@@ -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"],
)
@@ -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):
@@ -7,6 +7,7 @@ returns None causes a TypeError / KeyError in self-hosted mode.
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
from services import recommended_app_service as service_module
from services.recommended_app_service import RecommendedAppService
@@ -44,3 +45,40 @@ class TestGetRecommendAppDetailNullCheck:
assert result is None
mock_instance.get_recommend_app_detail.assert_called_once_with("nonexistent")
class TestGetLearnDifyApps:
@patch("services.recommended_app_service.FeatureService", autospec=True)
@patch("services.recommended_app_service.DatabaseRecommendAppRetrieval", autospec=True)
def test_returns_database_learn_dify_apps_without_remote_factory(
self, mock_database_retrieval, mock_feature_service
):
expected_app = {"app_id": "app-1", "categories": ["Workflow"]}
mock_database_retrieval.fetch_learn_dify_apps_from_db.return_value = {
"recommended_apps": [expected_app],
"categories": ["Workflow"],
}
mock_feature_service.get_system_features.return_value = SimpleNamespace(enable_trial_app=False)
with patch.object(service_module.RecommendAppRetrievalFactory, "get_recommend_app_factory") as factory_mock:
result = RecommendedAppService.get_learn_dify_apps("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()
@patch("services.recommended_app_service.FeatureService", autospec=True)
@patch("services.recommended_app_service.DatabaseRecommendAppRetrieval", autospec=True)
def test_sets_can_trial_when_trial_feature_enabled(self, mock_database_retrieval, mock_feature_service):
app = {"app_id": "app-1", "categories": ["Workflow"]}
mock_database_retrieval.fetch_learn_dify_apps_from_db.return_value = {
"recommended_apps": [app],
"categories": ["Workflow"],
}
mock_feature_service.get_system_features.return_value = SimpleNamespace(enable_trial_app=True)
with patch.object(RecommendedAppService, "_can_trial_app", return_value=True) as can_trial_mock:
result = RecommendedAppService.get_learn_dify_apps("en-US")
assert result["recommended_apps"][0]["can_trial"] is True
can_trial_mock.assert_called_once_with("app-1")
@@ -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):
@@ -0,0 +1,143 @@
# Integrations Folder Structure Follow-ups
Context: the onboarding UI rewrite moved Integrations into a first-class `/integrations/...` route family, but the implementation still intentionally reuses several legacy Account Settings, Tools, and Plugins components. This document records the recommended cleanup order after this branch is merged into `main`.
## Current Structure
| Area | Current location | Status |
| --- | --- | --- |
| MainNav shell | `web/app/components/main-nav` | Healthy. Keep the shallow `components/` layout. |
| Integrations route adapter | `web/app/(commonLayout)/integrations/[[...slug]]/page.tsx` | Canonical route entry. |
| Integrations route contract | `web/app/components/tools/integration-routes.ts` | Works, but ownership belongs to Integrations long-term. |
| Integrations shell and sidebar | `web/app/components/tools/integrations-page.tsx` | Works, but ownership belongs to Integrations long-term. |
| Integrations section renderer | `web/app/components/tools/integration-section-renderer.tsx` | Reuse-first adapter for existing pages. |
| Model Provider page | `web/app/components/header/account-setting/model-provider-page` | Still active and widely imported. Do not move first. |
| Data Source page | `web/app/components/header/account-setting/data-source-page-new` | Still active and reused by Integrations. Do not move first. |
| API Extension page | `web/app/components/header/account-setting/api-based-extension-page` | Still active and reused by Integrations. Do not move first. |
| Plugin management primitives | `web/app/components/plugins/plugin-page` | Still active for `/plugins` and reused by Integrations. Do not delete. |
## Recommended Timing
Do not do a broad folder move in the current onboarding UI branch. This branch already carries UI, route, i18n, and permission behavior changes; adding large path churn would increase merge conflict risk with `main` and make review harder.
After this branch is merged into `main`, do the structure cleanup in small PRs.
## Step 1: Establish Integrations Ownership
When: first cleanup PR after the onboarding UI branch lands on `main`.
Goal: make the new feature ownership visible without moving legacy implementation internals.
Recommended target:
```text
web/app/components/integrations/
routes.ts
integrations-page.tsx
integration-section-renderer.tsx
sidebar/
sections/
hooks/
```
Move or wrap only Integrations-owned shell files first:
| Current file | Target |
| --- | --- |
| `web/app/components/tools/integration-routes.ts` | `web/app/components/integrations/routes.ts` |
| `web/app/components/tools/integrations-page.tsx` | `web/app/components/integrations/integrations-page.tsx` |
| `web/app/components/tools/integration-section-renderer.tsx` | `web/app/components/integrations/integration-section-renderer.tsx` |
| `web/app/components/tools/integration-page-header.tsx` | `web/app/components/integrations/integration-page-header.tsx` |
| `web/app/components/tools/integration-sidebar-nav-item.tsx` | `web/app/components/integrations/sidebar/nav-item.tsx` |
| `web/app/components/tools/integration-sidebar-nav-item-styles.ts` | `web/app/components/integrations/sidebar/nav-item-styles.ts` |
| `web/app/components/tools/permission-quick-panel.tsx` | `web/app/components/integrations/sidebar/permission-quick-panel.tsx` |
| `web/app/components/tools/hooks/use-integration-*` | `web/app/components/integrations/hooks/*` |
Keep compatibility re-exports from the old `components/tools` paths during this PR if the import churn becomes large.
## Step 2: Add Section Adapters
When: same PR as Step 1 if the diff stays small, otherwise a second cleanup PR.
Goal: avoid importing legacy Account Settings components directly from the shared renderer.
Add thin adapters:
```text
web/app/components/integrations/sections/model-provider-section.tsx
web/app/components/integrations/sections/data-source-section.tsx
web/app/components/integrations/sections/api-extension-section.tsx
web/app/components/integrations/sections/tools-section.tsx
web/app/components/integrations/sections/plugin-category-section.tsx
```
These adapters should keep importing the existing implementation from its current location. For example, `model-provider-section.tsx` can wrap `header/account-setting/model-provider-page` and pass Integrations-specific props such as `stickyToolbar`, `fixedWarningAlignment`, and `hideSystemModelSelectorProviderSettingsFooter`.
Do not duplicate page logic in the adapters.
## Step 3: Remove Confirmed Dead Account Settings Pages
When: after Step 1 and Step 2 are merged and the app still passes route/modal smoke tests.
Likely removal candidates:
| Candidate | Reason |
| --- | --- |
| `web/app/components/header/account-setting/Integrations-page` | Superseded by the new Integrations shell. No production reference should remain. |
| `web/app/components/header/account-setting/plugin-page` | Superseded by `web/app/components/plugins/plugin-page`. No production reference should remain. |
Before deleting, run a fresh reference check:
```bash
rg "Integrations-page|header/account-setting/plugin-page" web/app web/context web/service
```
Delete only if the remaining hits are tests for the files being removed.
## Step 4: Consider Deeper Page Moves Later
When: only after the Integrations shell ownership is stable and `main` has absorbed the onboarding rewrite.
Do not start by moving these directories:
| Directory | Why |
| --- | --- |
| `header/account-setting/model-provider-page` | Its types, hooks, model selector, model auth, and modals are imported by workflows, datasets, app debug, services, and global modal context. |
| `header/account-setting/data-source-page-new` | Its types and credential flows are used by dataset creation, Notion selectors, and Integrations. |
| `header/account-setting/api-based-extension-page` | It is still reused by feature settings and Integrations. |
| `plugins/plugin-page` | `/plugins`, Integrations install controls, plugin category pages, plugin task status, and plugin detail flows still depend on it. |
If these are moved later, split the work by domain:
1. Extract shared types/hooks into stable shared modules.
2. Move only one page family per PR.
3. Keep temporary re-export files at old paths if external imports are still broad.
4. Remove re-exports only after the branch has settled on `main`.
## Merge Conflict Strategy
Prefer additive changes first:
- New adapter files under `components/integrations`.
- Small import updates in the route adapter and renderer.
- Temporary re-exports from old paths when needed.
Avoid early broad changes:
- Large `git mv` batches.
- Renaming model-provider/data-source imports across workflow and dataset code.
- Deleting reused plugin primitives.
- Combining structure cleanup with visual changes.
## Validation Checklist
Run at least:
```bash
pnpm test app/components/tools/__tests__/integrations-page.spec.tsx
pnpm test app/components/tools/__tests__/integration-routes.spec.ts
pnpm test app/components/main-nav/__tests__/index.spec.tsx
pnpm eslint --cache --quiet app/components/tools app/components/main-nav
```
For deeper moves, also run targeted tests for the moved page family, such as model-provider, data-source, API extension, plugin-page, and plugin detail panel tests.
+140
View File
@@ -0,0 +1,140 @@
# Integrations Route Contract
This document records the current canonical routes for the Integrations navigation, the legacy routes that redirect to them, and the remaining migration gaps. The first migration only moves existing pages onto the new routes; UI redesign work is out of scope.
## Current Status
Completed:
| Area | Status |
| --- | --- |
| Canonical `/integrations/...` route adapter | Implemented in `web/app/(commonLayout)/integrations/[[...slug]]/page.tsx`. |
| Route contract utility | Implemented in `web/app/components/tools/integration-routes.ts`. |
| Existing page reuse | Implemented through `IntegrationSectionRenderer`; no duplicated UI copy. |
| Legacy `/tools?...` redirects | Implemented through `web/app/(commonLayout)/tools/page.tsx`. |
| Legacy `/plugins` installed redirects | Implemented through `web/app/(commonLayout)/plugins/page.tsx`. |
| Tools tab navigation under new URLs | Implemented; scoped tool tabs push canonical `/integrations/tools/...` URLs. |
| Singular-only canonical URLs | Implemented; plural and misplaced aliases are intentionally unsupported. |
Not completed:
| Area | Remaining work |
| --- | --- |
| Integrations overview page | Not introduced; `/integrations` currently redirects to `/integrations/model-provider`. |
| Tools overview page | Not introduced; `/integrations/tools` currently redirects to `/integrations/tools/built-in`. |
| Plugin route migration | `/plugins` still owns the old plugin management and marketplace surface; no `/integrations/plugin` route will be introduced. Non-marketplace plugin URLs should redirect to `/integrations`. |
| Marketplace route migration | `/marketplace/...` routes below are future recommendations only; they are not implemented here. |
| New onboarding UI redesign | Not started in this route migration; current pages intentionally reuse existing UI. |
| Marketplace plugin redirects | Not implemented; marketplace plugin URLs intentionally keep rendering the legacy plugin marketplace surface for now. |
## Navigation Labels
| Navigation item | Canonical label |
| --- | --- |
| Model Provider | Model Provider |
| Built-in tools | Built-in |
| Custom Tool | Swagger API as Tool |
| Workflow | Workflow as Tool |
| MCP | MCP |
| Data Source | Data Source |
| API Extension | API Extension |
| Plugins | Plugins |
| Marketplace | Marketplace |
## Canonical Integrations Routes
| Route | Destination |
| --- | --- |
| `/integrations` | Redirect to `/integrations/model-provider` unless an overview page is introduced. |
| `/integrations/model-provider` | Existing model provider management page. |
| `/integrations/tools` | Redirect to `/integrations/tools/built-in` unless a tools overview page is introduced. |
| `/integrations/tools/built-in` | Existing built-in tools list. |
| `/integrations/tool/api` | Existing custom API tool list, relabeled as Swagger API as Tool. |
| `/integrations/tools/workflow` | Existing Workflow as Tool management page. |
| `/integrations/tools/mcp` | Existing MCP tools management page. |
| `/integrations/trigger` | Existing plugin trigger list filtered from plugin management. |
| `/integrations/agent-strategy` | Existing agent strategy plugin list filtered from plugin management. |
| `/integrations/extension` | Existing extension plugin list filtered from plugin management. |
| `/integrations/data-source` | Existing data source page. |
| `/integrations/tools/api-extension` | Existing API extension page under the Tools group. |
## Integration Plugin Category Routes
These navigation items use plugin categories from the existing plugin management surface:
| Navigation item | Plugin category | Route |
| --- | --- | --- |
| Trigger | `trigger` | `/integrations/trigger` |
| Agent Strategy | `agent-strategy` | `/integrations/agent-strategy` |
| Extension | `extension` | `/integrations/extension` |
The install and filter controls in the Integrations sidebar are disabled actions, not route destinations.
These routes reuse the installed plugin management list with an initial category filter. They are not marketplace category pages.
Do not treat every plugin category as an Integrations navigation item automatically. `trigger`, `agent-strategy`, and `extension` are currently exposed under Integrations because they are explicit navigation items. Other plugin categories have different product meanings:
| Plugin category | Integrations relationship |
| --- | --- |
| `tool` | Not equal to `/integrations/tools/...`; tool plugins can expose tool providers that appear in Tools, but the Tools page is provider-based. |
| `model` | Not equal to `/integrations/model-provider`; model providers are managed through the model provider page. |
| `datasource` | Not equal to the full Data Source page; data source integrations have their own existing page. |
| `trigger` | Reused as `/integrations/trigger`, installed plugins filtered by category. |
| `agent-strategy` | Reused as `/integrations/agent-strategy`, installed plugins filtered by category. |
| `extension` | Reused as `/integrations/extension`, installed plugins filtered by category. |
## Legacy Tools Redirects
| Legacy route | New route |
| --- | --- |
| `/tools` | `/integrations/tools/built-in` |
| `/tools?section=provider` | `/integrations/model-provider` |
| `/tools?section=builtin` | `/integrations/tools/built-in` |
| `/tools?section=builtin&category=builtin` | `/integrations/tools/built-in` |
| `/tools?category=builtin` | `/integrations/tools/built-in` |
| `/tools?section=custom-tool` | `/integrations/tool/api` |
| `/tools?section=custom-tool&category=api` | `/integrations/tool/api` |
| `/tools?category=api` | `/integrations/tool/api` |
| `/tools?section=workflow-tool` | `/integrations/tools/workflow` |
| `/tools?section=workflow-tool&category=workflow` | `/integrations/tools/workflow` |
| `/tools?category=workflow` | `/integrations/tools/workflow` |
| `/tools?section=mcp` | `/integrations/tools/mcp` |
| `/tools?section=mcp&category=mcp` | `/integrations/tools/mcp` |
| `/tools?category=mcp` | `/integrations/tools/mcp` |
| `/tools?section=data-source` | `/integrations/data-source` |
| `/tools?section=api-based-extension` | `/integrations/tools/api-extension` |
| `/tools?section=trigger` | `/integrations/trigger` |
| `/tools?section=agent-strategy` | `/integrations/agent-strategy` |
| `/tools?section=extension` | `/integrations/extension` |
Preserve non-routing query parameters such as `q`, `tags`, and `sort`, but drop legacy routing parameters such as `section` and `category` during redirects.
## Non-Canonical Integrations Routes
Do not add plural or misplaced alias redirects for new Integrations URLs. Only the singular canonical routes above should resolve. For example, `/integrations/model-providers`, `/integrations/data-sources`, `/integrations/api-extensions`, `/integrations/tools/trigger`, `/integrations/tools/agent-strategy`, and `/integrations/tools/extension` should not be treated as supported URLs unless they are later confirmed to have shipped externally.
## Legacy Plugin Redirects
Plugins have two different product meanings today: installed plugin management and marketplace discovery. Only the non-marketplace plugin URLs should redirect into Integrations. There is no `/integrations/plugin` route.
| Old Plugin URL | Recommended redirect | Reason |
| --- | --- | --- |
| `/plugins` | `/integrations` | Installed plugin management entry should move into the Integrations main entry. |
| `/plugins?tab=plugins` | `/integrations` | Explicit installed plugins tab; non-marketplace semantics. |
| `/plugins?tab=discover` | Do not redirect to Integrations | Marketplace discovery. |
| `/plugins?tab=all` | Do not redirect to Integrations | Marketplace category: all. |
| `/plugins?tab=tool` | Do not redirect to Integrations | Marketplace tool category, not installed tools management. |
| `/plugins?tab=model` | Do not redirect to Integrations | Marketplace model category. |
| `/plugins?tab=trigger` | Do not redirect to Integrations | Marketplace trigger category. |
| `/plugins?tab=agent-strategy` | Do not redirect to Integrations | Marketplace agent strategy category. |
| `/plugins?tab=extension` | Do not redirect to Integrations | Marketplace extension category. |
| `/plugins?tab=datasource` | Do not redirect to Integrations | Marketplace datasource category. |
| `/plugins?tab=bundle` | Do not redirect to Integrations | Marketplace bundle category. |
## Migration Order
1. Add the canonical route map and route tests.
2. Mount the existing pages under the new Integrations routes without UI redesign.
3. Update internal links to generate canonical URLs.
4. Add legacy redirects for `/tools` and `/plugins`.
5. Keep compatibility tests for each legacy route until old links can be removed.
+97
View File
@@ -0,0 +1,97 @@
# Main Nav Gating Follow-ups
Context: the desktop MainNav rewrite moved several workspace, account, tools, and marketplace entry points out of the old header/account-setting layout. These notes track product-contract questions that should be resolved before treating the rewrite as behavior-complete.
Current status:
- Open: account-setting modal navigation API naming, Marketplace/Integrations install task status parity, account language/timezone access parity.
- Partially resolved: Apps/Datasets quick-switch/create parity.
## 1. Account-setting modal naming and moved destinations
Status: Open.
Current branch behavior:
- `setShowAccountSettingModal(PROVIDER)` routes to `/integrations/model-provider`.
- `setShowAccountSettingModal(DATA_SOURCE)` routes to `/integrations/data-source`.
- `setShowAccountSettingModal(API_BASED_EXTENSION)` routes to `/integrations/tools/api-extension`.
- Document Settings no longer directly renders the old Account Settings modal for Provider; it uses the Integrations destination helper.
Old behavior: these calls opened the account-setting modal and switched to the matching tab.
Question:
- Since Provider, Data Source, and API-based Extension are no longer inside Account Settings in the new design, should this API still be named `setShowAccountSettingModal` for those destinations?
Follow-up decision needed:
- Either keep the compatibility shim but document that these payloads are now route destinations, or introduce a clearer navigation API for integration destinations and update call sites intentionally.
- Re-check call sites launched from workflows, datasets, and app configuration when new entry points are added. The known Document Settings provider entry has been migrated.
## 2. Plugin and marketplace status parity
Status: Open.
Old header behavior:
- `PluginsNav` showed plugin install progress and error state through the installing icon and red indicator.
- Installing tasks showed the downloading icon.
- Failed or erroring install tasks showed the red status indicator.
Current MainNav behavior:
- MainNav has a Marketplace link, but it does not surface plugin installing/error state.
- Integrations has install entry points, but it does not surface the old `PluginTasks` install-task status entry near the Integrations install action.
- Marketplace is the product discovery surface for uninstalled integrations; `PluginTasks` is only the transient install-task status inbox for running/succeeded/failed installs.
Follow-up decision needed:
- Decide whether MainNav Marketplace should preserve the old plugin task status indicator.
- Decide whether Integrations should expose `PluginTasks` near the install action so users can inspect failed/running install tasks without returning to the old `/plugins` shell.
- If yes, reuse the existing `usePluginTaskStatus` behavior instead of creating a parallel status source.
## 3. Account language and timezone access
Status: Open.
Current branch behavior:
- Desktop MainNav account menu includes Language and Timezone submenus.
- The main app layout now uses MainNav across breakpoints.
- The default account dropdown does not expose a direct Language/Timezone settings entry.
- The default account dropdown still exists in non-MainNav account/header surfaces such as the account layout.
- Language and Timezone still belong to Account Settings, not Integrations.
- `UpdateSettingPopover` still links the timezone hint to `ACCOUNT_SETTING_TAB.LANGUAGE`.
- The legacy `ReferenceSettingModal` auto-update timezone hint also still links to `ACCOUNT_SETTING_TAB.LANGUAGE`.
Decision:
- Preserve the old language-access contract across breakpoints.
- The desktop MainNav path is acceptable; the remaining question is default account-dropdown parity wherever that path remains active, plus whether hidden Account Settings Language entry points should remain acceptable.
Follow-up decision needed:
- Add an equivalent language/timezone entry to the default account path, or otherwise ensure users in those surfaces can still reach language settings.
- Decide whether the Update Setting timezone hint should keep opening the hidden Account Settings Language page, or whether Account Settings should surface Language in its visible menu.
- Keep this as gate-contract parity, not a visual requirement to recreate the old Account Settings sidebar.
## 4. Apps and Datasets quick-switch/create parity
Status: Partially resolved.
Old header behavior:
- `AppNav` could show the current app, list more apps, load more results, and launch create-app flows from the header nav.
- `DatasetNav` could show the current dataset, list more datasets, load more results, and launch dataset creation from the header nav.
Current MainNav behavior:
- Apps is no longer only a static navigation link: MainNav includes a Web Apps section with installed web app search, pin, delete, and navigation behavior.
- Apps still does not preserve the old `AppNav` current-app switcher, load-more behavior, or create-app flows.
- Datasets is still a navigation link and does not preserve the old `DatasetNav` current-dataset switcher, load-more behavior, or dataset creation entry.
Follow-up decision needed:
- Decide whether the new design intentionally removes these quick-switch/create affordances.
- If not, add equivalent behavior in the MainNav flow without copying the old header UI directly.
+1 -240
View File
@@ -108,17 +108,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/tracing/__tests__/svg-attribute-error-reproduction.spec.tsx": {
"no-console": {
"count": 19
@@ -249,17 +238,9 @@
},
"web/app/components/app-sidebar/index.tsx": {
"no-restricted-globals": {
"count": 2
},
"ts/no-explicit-any": {
"count": 1
}
},
"web/app/components/app-sidebar/nav-link/index.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 3
}
},
"web/app/components/app/annotation/add-annotation-modal/edit-item/index.tsx": {
"erasable-syntax-only/enums": {
"count": 1
@@ -611,11 +592,6 @@
"count": 1
}
},
"web/app/components/app/overview/settings/index.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"web/app/components/app/overview/trigger-card.tsx": {
"ts/no-explicit-any": {
"count": 1
@@ -662,17 +638,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
@@ -2264,11 +2229,6 @@
"count": 1
}
},
"web/app/components/datasets/list/index.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"web/app/components/datasets/metadata/edit-metadata-batch/input-combined.tsx": {
"ts/no-explicit-any": {
"count": 2
@@ -2348,19 +2308,6 @@
"count": 2
}
},
"web/app/components/explore/app-list/index.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"web/app/components/explore/banner/banner-item.tsx": {
"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": {
"react-hooks-extra/no-direct-set-state-in-use-effect": {
"count": 1
@@ -2382,34 +2329,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
@@ -2425,11 +2354,6 @@
"count": 2
}
},
"web/app/components/goto-anything/command-selector.tsx": {
"react/unsupported-syntax": {
"count": 2
}
},
"web/app/components/goto-anything/components/footer.tsx": {
"react/unsupported-syntax": {
"count": 1
@@ -2453,21 +2377,11 @@
"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
}
},
"web/app/components/header/account-setting/data-source-page-new/card.tsx": {
"ts/no-explicit-any": {
"count": 2
}
},
"web/app/components/header/account-setting/data-source-page-new/hooks/index.ts": {
"no-barrel-files/no-barrel-files": {
"count": 2
@@ -2483,14 +2397,6 @@
"count": 1
}
},
"web/app/components/header/account-setting/data-source-page-new/item.tsx": {
"no-restricted-imports": {
"count": 1
},
"ts/no-explicit-any": {
"count": 1
}
},
"web/app/components/header/account-setting/data-source-page-new/types.ts": {
"ts/no-explicit-any": {
"count": 2
@@ -2636,11 +2542,6 @@
"count": 4
}
},
"web/app/components/plugins/card/index.tsx": {
"ts/no-non-null-asserted-optional-chain": {
"count": 1
}
},
"web/app/components/plugins/install-plugin/hooks.ts": {
"ts/no-explicit-any": {
"count": 4
@@ -2869,11 +2770,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/category-filter.tsx": {
"no-restricted-imports": {
"count": 1
@@ -2884,11 +2780,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/types.ts": {
"erasable-syntax-only/enums": {
"count": 2
@@ -2902,11 +2793,6 @@
"count": 25
}
},
"web/app/components/plugins/update-plugin/from-market-place.tsx": {
"erasable-syntax-only/enums": {
"count": 1
}
},
"web/app/components/rag-pipeline/components/panel/input-field/editor/form/__tests__/hidden-fields.spec.tsx": {
"react/static-components": {
"count": 4
@@ -3187,54 +3073,16 @@
"count": 1
}
},
"web/app/components/tools/mcp/create-card.tsx": {
"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": {
"ts/no-explicit-any": {
"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": {
"no-restricted-imports": {
"count": 1
},
"ts/no-explicit-any": {
"count": 1
}
},
"web/app/components/tools/provider/empty.tsx": {
"ts/no-explicit-any": {
"count": 1
}
},
"web/app/components/tools/setting/build-in/config-credentials.tsx": {
"ts/no-explicit-any": {
"count": 3
@@ -3389,11 +3237,6 @@
"count": 1
}
},
"web/app/components/workflow/header/__tests__/index.spec.tsx": {
"react/static-components": {
"count": 2
}
},
"web/app/components/workflow/header/test-run-menu.tsx": {
"erasable-syntax-only/enums": {
"count": 1
@@ -3463,16 +3306,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
@@ -4739,11 +4572,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
@@ -4990,11 +4818,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
@@ -5043,46 +4866,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
@@ -5093,14 +4876,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
@@ -5215,7 +4990,7 @@
"count": 1
},
"ts/no-explicit-any": {
"count": 29
"count": 27
}
},
"web/service/datasets.ts": {
@@ -5411,20 +5186,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
File diff suppressed because it is too large Load Diff
@@ -936,6 +936,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
@@ -2029,8 +2030,8 @@ export type GetAppsData = {
body?: never
path?: never
query?: {
creator_ids?: Array<string> | null
is_created_by_me?: boolean | null
creator_ids?: Array<string>
is_created_by_me?: boolean
limit?: number
mode?:
| 'advanced-chat'
@@ -2041,9 +2042,10 @@ export type GetAppsData = {
| 'chat'
| 'completion'
| 'workflow'
name?: string | null
name?: string
page?: number
tag_ids?: Array<string> | null
sort_by?: 'earliest_created' | 'last_modified' | 'recently_created'
tag_ids?: Array<string>
}
url: '/apps'
}
@@ -2137,6 +2139,36 @@ export type PostAppsImportsByImportIdConfirmResponses = {
export type PostAppsImportsByImportIdConfirmResponse
= PostAppsImportsByImportIdConfirmResponses[keyof PostAppsImportsByImportIdConfirmResponses]
export type GetAppsStarredData = {
body?: never
path?: never
query?: {
creator_ids?: Array<string>
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<string>
}
url: '/apps/starred'
}
export type GetAppsStarredResponses = {
200: AppPagination
}
export type GetAppsStarredResponse = GetAppsStarredResponses[keyof GetAppsStarredResponses]
export type PostAppsWorkflowsOnlineUsersData = {
body: WorkflowOnlineUsersPayload
path?: never
@@ -3750,6 +3782,55 @@ 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: {
[key: string]: unknown
}
}
export type DeleteAppsByAppIdStarError
= DeleteAppsByAppIdStarErrors[keyof DeleteAppsByAppIdStarErrors]
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: {
[key: string]: unknown
}
}
export type PostAppsByAppIdStarError = PostAppsByAppIdStarErrors[keyof PostAppsByAppIdStarErrors]
export type PostAppsByAppIdStarResponses = {
200: SimpleResultResponse
}
export type PostAppsByAppIdStarResponse
= PostAppsByAppIdStarResponses[keyof PostAppsByAppIdStarResponses]
export type GetAppsByAppIdStatisticsAverageResponseTimeData = {
body?: never
path: {
@@ -1401,6 +1401,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(),
@@ -2711,8 +2712,8 @@ export const zWorkflowCommentDetailWritable = z.object({
})
export const zGetAppsQuery = z.object({
creator_ids: z.array(z.string()).nullish(),
is_created_by_me: z.boolean().nullish(),
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([
@@ -2727,9 +2728,13 @@ export const zGetAppsQuery = z.object({
])
.optional()
.default('all'),
name: z.string().nullish(),
name: z.string().optional(),
page: z.int().gte(1).lte(99999).optional().default(1),
tag_ids: z.array(z.string()).nullish(),
sort_by: z
.enum(['earliest_created', 'last_modified', 'recently_created'])
.optional()
.default('last_modified'),
tag_ids: z.array(z.string()).optional(),
})
/**
@@ -2769,6 +2774,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
/**
@@ -3549,6 +3585,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(),
})
@@ -6,17 +6,34 @@ import * as z from 'zod'
import {
zGetExploreAppsByAppIdPath,
zGetExploreAppsByAppIdResponse,
zGetExploreAppsLearnDifyQuery,
zGetExploreAppsLearnDifyResponse,
zGetExploreAppsQuery,
zGetExploreAppsResponse,
zGetExploreBannersResponse,
} 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,
}
/**
* Generated contract types may be inaccurate because backend OpenAPI annotations are incomplete. Do not migrate callers until the generated contract is accurate.
*
* @deprecated
*/
export const get = oc
export const get2 = oc
.route({
deprecated: true,
description:
@@ -31,10 +48,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',
@@ -46,7 +63,8 @@ export const get2 = oc
.output(zGetExploreAppsResponse)
export const apps = {
get: get2,
get: get3,
learnDify,
byAppId,
}
@@ -57,7 +75,7 @@ export const apps = {
*
* @deprecated
*/
export const get3 = oc
export const get4 = oc
.route({
deprecated: true,
description:
@@ -72,7 +90,7 @@ export const get3 = oc
.output(zGetExploreBannersResponse)
export const banners = {
get: get3,
get: get4,
}
export const explore = {
@@ -9,6 +9,10 @@ export type RecommendedAppListResponse = {
recommended_apps: Array<RecommendedAppResponse>
}
export type LearnDifyAppListResponse = {
recommended_apps: Array<RecommendedAppResponse>
}
export type RecommendedAppResponse = {
app?: RecommendedAppInfoResponse
app_id: string
@@ -46,6 +50,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: {
@@ -38,6 +38,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(),
})
@@ -47,6 +54,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(),
})
File diff suppressed because it is too large Load Diff
@@ -285,6 +285,30 @@ export type WorkspacePermissionResponse = {
workspace_id: string
}
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
@@ -318,21 +342,8 @@ export type ParserDynamicOptionsWithCredentials = {
}
export type ParserPermissionChange = {
debug_permission: DebugPermission
install_permission: InstallPermission
}
export type SuccessResponse = {
success: boolean
}
export type ParserExcludePlugin = {
plugin_id: string
}
export type ParserPreferencesChange = {
auto_upgrade: PluginAutoUpgradeSettingsPayload
permission: PluginPermissionSettingsPayload
debug_permission?: DebugPermission
install_permission?: InstallPermission
}
export type ParserUninstall = {
@@ -358,6 +369,12 @@ export type ParserGithubUpload = {
version: string
}
export type PluginCategoryListResponse = {
builtin_tools: Array<PluginCategoryBuiltinToolProviderResponse>
has_more: boolean
plugins: Array<PluginCategoryInstalledPluginResponse>
}
export type ApiToolProviderAddPayload = {
credentials: {
[key: string]: unknown
@@ -650,10 +667,6 @@ export type LoadBalancingPayload = {
enabled?: boolean | null
}
export type DebugPermission = 'admins' | 'everyone' | 'noone'
export type InstallPermission = 'admins' | 'everyone' | 'noone'
export type PluginAutoUpgradeSettingsPayload = {
exclude_plugins?: Array<string>
include_plugins?: Array<string>
@@ -662,9 +675,66 @@ 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<string>
include_plugins: Array<string>
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: unknown
icon_dark: unknown
id: string
is_team_authorization: boolean
label: CoreToolsEntitiesCommonEntitiesI18nObject
labels: Array<string>
name: string
plugin_id: string | null
plugin_unique_identifier: string | null
team_credentials: {
[key: string]: unknown
}
tools: Array<PluginCategoryBuiltinToolResponse>
type: ToolProviderType
[key: string]: unknown
}
export type PluginCategoryInstalledPluginResponse = {
checksum: string
created_at: string
declaration: PluginDeclaration
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'
@@ -683,8 +753,467 @@ 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<string>
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 PluginDeclaration = {
agent_strategy?: AgentStrategyProviderEntity
author: string | null
category: PluginCategory
created_at: string
datasource?: DatasourceProviderEntity
description: CoreToolsEntitiesCommonEntitiesI18nObject
endpoint?: EndpointProviderDeclaration
icon: string
icon_dark?: string | null
label: CoreToolsEntitiesCommonEntitiesI18nObject
meta: Meta
model?: ProviderEntity
name: string
plugins: Plugins
repo?: string | null
resource: PluginResourceRequirements
tags?: Array<string>
tool?: ToolProviderEntity
trigger?: TriggerProviderEntity
verified?: boolean
version: string
}
export type PluginInstallationSource = 'github' | 'marketplace' | 'package' | 'remote'
export type ToolParameterForm = 'form' | 'llm' | 'schema'
export type AgentStrategyProviderEntity = {
identity: AgentStrategyProviderIdentity
plugin_id?: string | null
}
export type DatasourceProviderEntity = {
credentials_schema?: Array<ProviderConfig>
identity: DatasourceProviderIdentity
oauth_schema?: OAuthSchema
provider_type: DatasourceProviderType
}
export type EndpointProviderDeclaration = {
endpoints?: Array<EndpointDeclaration> | null
settings?: Array<ProviderConfig>
}
export type Meta = {
minimum_dify_version?: string | null
version?: string | null
}
export type ProviderEntity = {
background?: string | null
configurate_methods: Array<ConfigurateMethod>
description?: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject
help?: ProviderHelpEntity
icon_small?: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject
icon_small_dark?: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject
label: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject
model_credential_schema?: ModelCredentialSchema
models?: Array<AiModelEntity>
position?: {
[key: string]: Array<string>
} | null
provider: string
provider_credential_schema?: ProviderCredentialSchema
provider_name?: string
supported_model_types: Array<ModelType>
}
export type Plugins = {
datasources?: Array<string> | null
endpoints?: Array<string> | null
models?: Array<string> | null
tools?: Array<string> | null
triggers?: Array<string> | null
}
export type PluginResourceRequirements = {
memory: number
permission?: Permission
}
export type ToolProviderEntity = {
credentials_schema?: Array<ProviderConfig>
identity: ToolProviderIdentity
oauth_schema?: OAuthSchema
plugin_id?: string | null
}
export type TriggerProviderEntity = {
events?: Array<EventEntity>
identity: TriggerProviderIdentity
subscription_constructor?: SubscriptionConstructor
subscription_schema?: Array<ProviderConfig>
}
export type AgentStrategyProviderIdentity = {
author: string
description: CoreToolsEntitiesCommonEntitiesI18nObject
icon: string
icon_dark?: string | null
label: CoreToolsEntitiesCommonEntitiesI18nObject
name: string
tags?: Array<ToolLabelEnum> | null
}
export type ProviderConfig = {
default?: unknown
help?: CoreToolsEntitiesCommonEntitiesI18nObject
label?: CoreToolsEntitiesCommonEntitiesI18nObject
multiple?: boolean | null
name: string
options?: Array<Option> | null
placeholder?: CoreToolsEntitiesCommonEntitiesI18nObject
required?: boolean
scope?: unknown
type: CoreEntitiesProviderEntitiesBasicProviderConfigType
url?: string | null
}
export type DatasourceProviderIdentity = {
author: string
description: CoreToolsEntitiesCommonEntitiesI18nObject
icon: string
label: CoreToolsEntitiesCommonEntitiesI18nObject
name: string
tags?: Array<ToolLabelEnum> | null
}
export type OAuthSchema = {
client_schema?: Array<ProviderConfig>
credentials_schema?: Array<ProviderConfig>
}
export type DatasourceProviderType
= | 'local_file'
| 'online_document'
| 'online_drive'
| 'website_crawl'
export type EndpointDeclaration = {
hidden?: boolean
method: string
path: string
}
export type ConfigurateMethod = 'customizable-model' | 'predefined-model'
export type GraphonModelRuntimeEntitiesCommonEntitiesI18nObject = {
en_US: string
zh_Hans?: string | null
}
export type ProviderHelpEntity = {
title: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject
url: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject
}
export type ModelCredentialSchema = {
credential_form_schemas: Array<CredentialFormSchema>
model: FieldModelSchema
}
export type AiModelEntity = {
deprecated?: boolean
features?: Array<ModelFeature> | null
fetch_from: FetchFrom
label: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject
model: string
model_properties: {
[key: string]: unknown
}
model_type: ModelType
parameter_rules?: Array<ParameterRule>
pricing?: PriceConfig
}
export type ProviderCredentialSchema = {
credential_form_schemas: Array<CredentialFormSchema>
}
export type Permission = {
endpoint?: Endpoint
model?: Model
node?: Node
storage?: Storage
tool?: Tool
}
export type ToolProviderIdentity = {
author: string
description: CoreToolsEntitiesCommonEntitiesI18nObject
icon: string
icon_dark?: string | null
label: CoreToolsEntitiesCommonEntitiesI18nObject
name: string
tags?: Array<ToolLabelEnum> | null
}
export type EventEntity = {
description: CoreToolsEntitiesCommonEntitiesI18nObject
identity: EventIdentity
output_schema?: {
[key: string]: unknown
} | null
parameters?: Array<EventParameter>
}
export type TriggerProviderIdentity = {
author: string
description: CoreToolsEntitiesCommonEntitiesI18nObject
icon?: string | null
icon_dark?: string | null
label: CoreToolsEntitiesCommonEntitiesI18nObject
name: string
tags?: Array<string>
}
export type SubscriptionConstructor = {
credentials_schema?: Array<ProviderConfig>
oauth_schema?: OAuthSchema
parameters?: Array<EventParameter>
}
export type ToolLabelEnum
= | 'business'
| 'design'
| 'education'
| 'entertainment'
| 'finance'
| 'image'
| 'medical'
| 'news'
| 'other'
| 'productivity'
| 'rag'
| 'search'
| 'social'
| 'travel'
| 'utilities'
| 'videos'
| 'weather'
export type Option = {
label: CoreToolsEntitiesCommonEntitiesI18nObject
value: string
}
export type AppSelectorScope = 'all' | 'chat' | 'completion' | 'workflow'
export type ModelSelectorScope
= | 'llm'
| 'moderation'
| 'rerank'
| 'speech2text'
| 'text-embedding'
| 'tts'
| 'vision'
export type ToolSelectorScope = 'all' | 'builtin' | 'custom' | 'workflow'
export type CoreEntitiesProviderEntitiesBasicProviderConfigType
= | 'app-selector'
| 'array[tools]'
| 'boolean'
| 'model-selector'
| 'secret-input'
| 'select'
| 'text-input'
export type CredentialFormSchema = {
default?: string | null
label: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject
max_length?: number
options?: Array<FormOption> | null
placeholder?: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject
required?: boolean
show_on?: Array<FormShowOnObject>
type: FormType
variable: string
}
export type FieldModelSchema = {
label: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject
placeholder?: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject
}
export type ModelFeature
= | 'agent-thought'
| 'audio'
| 'document'
| 'multi-tool-call'
| 'stream-tool-call'
| 'structured-output'
| 'tool-call'
| 'video'
| 'vision'
export type FetchFrom = 'customizable-model' | 'predefined-model'
export type ModelPropertyKey
= | 'audio_type'
| 'context_size'
| 'default_voice'
| 'file_upload_limit'
| 'max_characters_per_chunk'
| 'max_chunks'
| 'max_workers'
| 'mode'
| 'supported_file_extensions'
| 'voices'
| 'word_limit'
export type ParameterRule = {
default?: unknown
help?: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject
label: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject
max?: number | null
min?: number | null
name: string
options?: Array<string>
precision?: number | null
required?: boolean
type: ParameterType
use_template?: string | null
}
export type PriceConfig = {
currency: string
input: string
output?: string | null
unit: string
}
export type Endpoint = {
enabled?: boolean | null
}
export type Model = {
enabled?: boolean | null
llm?: boolean | null
moderation?: boolean | null
rerank?: boolean | null
speech2text?: boolean | null
text_embedding?: boolean | null
tts?: boolean | null
}
export type Node = {
enabled?: boolean | null
}
export type Storage = {
enabled?: boolean | null
size?: number
}
export type Tool = {
enabled?: boolean | null
}
export type EventIdentity = {
author: string
label: CoreToolsEntitiesCommonEntitiesI18nObject
name: string
provider?: string | null
}
export type EventParameter = {
auto_generate?: PluginParameterAutoGenerate
default?: unknown
description?: CoreToolsEntitiesCommonEntitiesI18nObject
label: CoreToolsEntitiesCommonEntitiesI18nObject
max?: unknown
min?: unknown
multiple?: boolean | null
name: string
options?: Array<PluginParameterOption> | null
precision?: number | null
required?: boolean | null
scope?: string | null
template?: PluginParameterTemplate
type: EventParameterType
}
export type FormOption = {
label: GraphonModelRuntimeEntitiesCommonEntitiesI18nObject
show_on?: Array<FormShowOnObject>
value: string
}
export type FormShowOnObject = {
value: string
variable: string
}
export type FormType = 'radio' | 'secret-input' | 'select' | 'switch' | 'text-input'
export type ParameterType = 'boolean' | 'float' | 'int' | 'string' | 'text'
export type PluginParameterAutoGenerate = {
type: CorePluginEntitiesParametersPluginParameterAutoGenerateType
}
export type PluginParameterOption = {
icon?: string | null
label: CoreToolsEntitiesCommonEntitiesI18nObject
value: string
}
export type PluginParameterTemplate = {
enabled?: boolean
}
export type EventParameterType
= | 'app-selector'
| 'array'
| 'boolean'
| 'checkbox'
| 'dynamic-select'
| 'file'
| 'files'
| 'model-selector'
| 'number'
| 'object'
| 'select'
| 'string'
export type CorePluginEntitiesParametersPluginParameterAutoGenerateType = 'prompt_instruction'
export type GetWorkspacesData = {
body?: never
path?: never
@@ -1858,6 +2387,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: string
}
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
@@ -2095,54 +2668,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: {
[key: string]: unknown
}
}
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: {
[key: string]: unknown
}
}
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: {
[key: string]: unknown
}
}
export type GetWorkspacesCurrentPluginPreferencesFetchResponse
= GetWorkspacesCurrentPluginPreferencesFetchResponses[keyof GetWorkspacesCurrentPluginPreferencesFetchResponses]
export type GetWorkspacesCurrentPluginReadmeData = {
body?: never
path?: never
@@ -2340,6 +2865,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
@@ -205,6 +205,21 @@ export const zWorkspacePermissionResponse = z.object({
workspace_id: z.string(),
})
/**
* PluginAutoUpgradeChangeResponse
*/
export const zPluginAutoUpgradeChangeResponse = z.object({
message: z.string().nullish(),
success: z.boolean(),
})
/**
* SuccessResponse
*/
export const zSuccessResponse = z.object({
success: z.boolean(),
})
/**
* PluginDebuggingKeyResponse
*/
@@ -250,20 +265,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(),
})
/**
* ParserUninstall
*/
@@ -704,6 +705,26 @@ export const zParserPostModels = z.object({
model_type: zModelType,
})
/**
* 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
*/
@@ -718,14 +739,6 @@ export const zInstallPermission = z.enum(['admins', 'everyone', 'noone'])
* ParserPermissionChange
*/
export const zParserPermissionChange = z.object({
debug_permission: zDebugPermission,
install_permission: zInstallPermission,
})
/**
* PluginPermissionSettingsPayload
*/
export const zPluginPermissionSettingsPayload = z.object({
debug_permission: zDebugPermission.optional(),
install_permission: zInstallPermission.optional(),
})
@@ -860,13 +873,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,
})
/**
* PluginAutoUpgradeSettingsResponseModel
*/
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.unknown(),
icon_dark: z.unknown(),
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
*/
@@ -911,6 +1009,706 @@ export const zWorkflowToolUpdatePayload = z.object({
workflow_tool_id: z.string(),
})
/**
* Meta
*/
export const zMeta = z.object({
minimum_dify_version: z.string().nullish(),
version: z.string().nullish(),
})
/**
* Plugins
*/
export const zPlugins = z.object({
datasources: z.array(z.string()).nullish(),
endpoints: z.array(z.string()).nullish(),
models: z.array(z.string()).nullish(),
tools: z.array(z.string()).nullish(),
triggers: z.array(z.string()).nullish(),
})
/**
* DatasourceProviderType
*
* Enum class for datasource provider
*/
export const zDatasourceProviderType = z.enum([
'local_file',
'online_document',
'online_drive',
'website_crawl',
])
/**
* EndpointDeclaration
*
* declaration of an endpoint
*/
export const zEndpointDeclaration = z.object({
hidden: z.boolean().optional().default(false),
method: z.string(),
path: z.string(),
})
/**
* ConfigurateMethod
*
* Enum class for configurate method of provider model.
*/
export const zConfigurateMethod = z.enum(['customizable-model', 'predefined-model'])
/**
* 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,
})
/**
* TriggerProviderIdentity
*
* The identity of the trigger provider
*/
export const zTriggerProviderIdentity = z.object({
author: z.string(),
description: zCoreToolsEntitiesCommonEntitiesI18nObject,
icon: z.string().nullish(),
icon_dark: z.string().nullish(),
label: zCoreToolsEntitiesCommonEntitiesI18nObject,
name: z.string(),
tags: z.array(z.string()).optional(),
})
/**
* ToolLabelEnum
*/
export const zToolLabelEnum = z.enum([
'business',
'design',
'education',
'entertainment',
'finance',
'image',
'medical',
'news',
'other',
'productivity',
'rag',
'search',
'social',
'travel',
'utilities',
'videos',
'weather',
])
/**
* AgentStrategyProviderIdentity
*
* Inherits from ToolProviderIdentity, without any additional fields.
*/
export const zAgentStrategyProviderIdentity = z.object({
author: z.string(),
description: zCoreToolsEntitiesCommonEntitiesI18nObject,
icon: z.string(),
icon_dark: z.string().nullish(),
label: zCoreToolsEntitiesCommonEntitiesI18nObject,
name: z.string(),
tags: z.array(zToolLabelEnum).nullish().default([]),
})
/**
* AgentStrategyProviderEntity
*/
export const zAgentStrategyProviderEntity = z.object({
identity: zAgentStrategyProviderIdentity,
plugin_id: z.string().nullish(),
})
/**
* DatasourceProviderIdentity
*/
export const zDatasourceProviderIdentity = z.object({
author: z.string(),
description: zCoreToolsEntitiesCommonEntitiesI18nObject,
icon: z.string(),
label: zCoreToolsEntitiesCommonEntitiesI18nObject,
name: z.string(),
tags: z.array(zToolLabelEnum).nullish().default([]),
})
/**
* ToolProviderIdentity
*/
export const zToolProviderIdentity = z.object({
author: z.string(),
description: zCoreToolsEntitiesCommonEntitiesI18nObject,
icon: z.string(),
icon_dark: z.string().nullish(),
label: zCoreToolsEntitiesCommonEntitiesI18nObject,
name: z.string(),
tags: z.array(zToolLabelEnum).nullish().default([]),
})
/**
* Option
*/
export const zOption = z.object({
label: zCoreToolsEntitiesCommonEntitiesI18nObject,
value: z.string(),
})
/**
* AppSelectorScope
*/
export const zAppSelectorScope = z.enum(['all', 'chat', 'completion', 'workflow'])
/**
* ModelSelectorScope
*/
export const zModelSelectorScope = z.enum([
'llm',
'moderation',
'rerank',
'speech2text',
'text-embedding',
'tts',
'vision',
])
/**
* ToolSelectorScope
*/
export const zToolSelectorScope = z.enum(['all', 'builtin', 'custom', 'workflow'])
/**
* Type
*/
export const zCoreEntitiesProviderEntitiesBasicProviderConfigType = z.enum([
'app-selector',
'array[tools]',
'boolean',
'model-selector',
'secret-input',
'select',
'text-input',
])
/**
* ProviderConfig
*
* Model class for common provider settings like credentials
*/
export const zProviderConfig = z.object({
default: z.unknown().optional(),
help: zCoreToolsEntitiesCommonEntitiesI18nObject.optional(),
label: zCoreToolsEntitiesCommonEntitiesI18nObject.optional(),
multiple: z.boolean().nullish().default(false),
name: z.string(),
options: z.array(zOption).nullish(),
placeholder: zCoreToolsEntitiesCommonEntitiesI18nObject.optional(),
required: z.boolean().optional().default(false),
scope: z.unknown().optional(),
type: zCoreEntitiesProviderEntitiesBasicProviderConfigType,
url: z.string().nullish(),
})
/**
* EndpointProviderDeclaration
*
* declaration of an endpoint group
*/
export const zEndpointProviderDeclaration = z.object({
endpoints: z.array(zEndpointDeclaration).nullish(),
settings: z.array(zProviderConfig).optional(),
})
/**
* OAuthSchema
*
* OAuth schema
*/
export const zOAuthSchema = z.object({
client_schema: z.array(zProviderConfig).optional(),
credentials_schema: z.array(zProviderConfig).optional(),
})
/**
* DatasourceProviderEntity
*
* Datasource provider entity
*/
export const zDatasourceProviderEntity = z.object({
credentials_schema: z.array(zProviderConfig).optional(),
identity: zDatasourceProviderIdentity,
oauth_schema: zOAuthSchema.optional(),
provider_type: zDatasourceProviderType,
})
/**
* ToolProviderEntity
*/
export const zToolProviderEntity = z.object({
credentials_schema: z.array(zProviderConfig).optional(),
identity: zToolProviderIdentity,
oauth_schema: zOAuthSchema.optional(),
plugin_id: z.string().nullish(),
})
/**
* FieldModelSchema
*/
export const zFieldModelSchema = z.object({
label: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject,
placeholder: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject.optional(),
})
/**
* ModelFeature
*
* Enum class for llm feature.
*/
export const zModelFeature = z.enum([
'agent-thought',
'audio',
'document',
'multi-tool-call',
'stream-tool-call',
'structured-output',
'tool-call',
'video',
'vision',
])
/**
* FetchFrom
*
* Enum class for fetch from.
*/
export const zFetchFrom = z.enum(['customizable-model', 'predefined-model'])
/**
* ModelPropertyKey
*
* Enum class for model property key.
*/
export const zModelPropertyKey = z.enum([
'audio_type',
'context_size',
'default_voice',
'file_upload_limit',
'max_characters_per_chunk',
'max_chunks',
'max_workers',
'mode',
'supported_file_extensions',
'voices',
'word_limit',
])
/**
* PriceConfig
*
* Model class for pricing info.
*/
export const zPriceConfig = z.object({
currency: z.string(),
input: z.string().regex(/^(?![-+.]*$)[+-]?\d*(?:\.\d*)?$/),
output: z
.string()
.regex(/^(?![-+.]*$)[+-]?\d*(?:\.\d*)?$/)
.nullish(),
unit: z.string().regex(/^(?![-+.]*$)[+-]?\d*(?:\.\d*)?$/),
})
/**
* Endpoint
*/
export const zEndpoint = z.object({
enabled: z.boolean().nullish().default(false),
})
/**
* Model
*/
export const zModel = z.object({
enabled: z.boolean().nullish().default(false),
llm: z.boolean().nullish().default(false),
moderation: z.boolean().nullish().default(false),
rerank: z.boolean().nullish().default(false),
speech2text: z.boolean().nullish().default(false),
text_embedding: z.boolean().nullish().default(false),
tts: z.boolean().nullish().default(false),
})
/**
* Node
*/
export const zNode = z.object({
enabled: z.boolean().nullish().default(false),
})
/**
* Storage
*/
export const zStorage = z.object({
enabled: z.boolean().nullish().default(false),
size: z.int().gte(1024).lte(1073741824).optional().default(1048576),
})
/**
* Tool
*/
export const zTool = z.object({
enabled: z.boolean().nullish().default(false),
})
/**
* Permission
*/
export const zPermission = z.object({
endpoint: zEndpoint.optional(),
model: zModel.optional(),
node: zNode.optional(),
storage: zStorage.optional(),
tool: zTool.optional(),
})
/**
* PluginResourceRequirements
*/
export const zPluginResourceRequirements = z.object({
memory: z.int(),
permission: zPermission.optional(),
})
/**
* EventIdentity
*
* The identity of the event
*/
export const zEventIdentity = z.object({
author: z.string(),
label: zCoreToolsEntitiesCommonEntitiesI18nObject,
name: z.string(),
provider: z.string().nullish(),
})
/**
* FormShowOnObject
*
* Model class for form show on.
*/
export const zFormShowOnObject = z.object({
value: z.string(),
variable: z.string(),
})
/**
* FormOption
*
* Model class for form option.
*/
export const zFormOption = z.object({
label: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject,
show_on: z.array(zFormShowOnObject).optional().default([]),
value: z.string(),
})
/**
* FormType
*
* Enum class for form type.
*/
export const zFormType = z.enum(['radio', 'secret-input', 'select', 'switch', 'text-input'])
/**
* CredentialFormSchema
*
* Model class for credential form schema.
*/
export const zCredentialFormSchema = z.object({
default: z.string().nullish(),
label: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject,
max_length: z.int().optional().default(0),
options: z.array(zFormOption).nullish(),
placeholder: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject.optional(),
required: z.boolean().optional().default(true),
show_on: z.array(zFormShowOnObject).optional().default([]),
type: zFormType,
variable: z.string(),
})
/**
* ModelCredentialSchema
*
* Model class for model credential schema.
*/
export const zModelCredentialSchema = z.object({
credential_form_schemas: z.array(zCredentialFormSchema),
model: zFieldModelSchema,
})
/**
* ProviderCredentialSchema
*
* Model class for provider credential schema.
*/
export const zProviderCredentialSchema = z.object({
credential_form_schemas: z.array(zCredentialFormSchema),
})
/**
* ParameterType
*
* Enum class for parameter type.
*/
export const zParameterType = z.enum(['boolean', 'float', 'int', 'string', 'text'])
/**
* ParameterRule
*
* Model class for parameter rule.
*/
export const zParameterRule = z.object({
default: z.unknown().optional(),
help: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject.optional(),
label: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject,
max: z.number().nullish(),
min: z.number().nullish(),
name: z.string(),
options: z.array(z.string()).optional().default([]),
precision: z.int().nullish(),
required: z.boolean().optional().default(false),
type: zParameterType,
use_template: z.string().nullish(),
})
/**
* AIModelEntity
*
* Model class for AI model.
*/
export const zAiModelEntity = z.object({
deprecated: z.boolean().optional().default(false),
features: z.array(zModelFeature).nullish(),
fetch_from: zFetchFrom,
label: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject,
model: z.string(),
model_properties: z.record(z.string(), z.unknown()),
model_type: zModelType,
parameter_rules: z.array(zParameterRule).optional().default([]),
pricing: zPriceConfig.optional(),
})
/**
* ProviderEntity
*
* Runtime-native provider schema.
*
* `provider` is the canonical runtime identifier. `provider_name` is a
* compatibility alias for callers that still resolve providers by short name and
* is empty when no alias exists.
*/
export const zProviderEntity = z.object({
background: z.string().nullish(),
configurate_methods: z.array(zConfigurateMethod),
description: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject.optional(),
help: zProviderHelpEntity.optional(),
icon_small: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject.optional(),
icon_small_dark: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject.optional(),
label: zGraphonModelRuntimeEntitiesCommonEntitiesI18nObject,
model_credential_schema: zModelCredentialSchema.optional(),
models: z.array(zAiModelEntity).optional(),
position: z.record(z.string(), z.array(z.string())).nullish().default({}),
provider: z.string(),
provider_credential_schema: zProviderCredentialSchema.optional(),
provider_name: z.string().optional().default(''),
supported_model_types: z.array(zModelType),
})
/**
* PluginParameterOption
*/
export const zPluginParameterOption = z.object({
icon: z.string().nullish(),
label: zCoreToolsEntitiesCommonEntitiesI18nObject,
value: z.string(),
})
/**
* PluginParameterTemplate
*/
export const zPluginParameterTemplate = z.object({
enabled: z.boolean().optional().default(false),
})
/**
* EventParameterType
*
* The type of the parameter
*/
export const zEventParameterType = z.enum([
'app-selector',
'array',
'boolean',
'checkbox',
'dynamic-select',
'file',
'files',
'model-selector',
'number',
'object',
'select',
'string',
])
/**
* Type
*/
export const zCorePluginEntitiesParametersPluginParameterAutoGenerateType = z.enum([
'prompt_instruction',
])
/**
* PluginParameterAutoGenerate
*/
export const zPluginParameterAutoGenerate = z.object({
type: zCorePluginEntitiesParametersPluginParameterAutoGenerateType,
})
/**
* EventParameter
*
* The parameter of the event
*/
export const zEventParameter = z.object({
auto_generate: zPluginParameterAutoGenerate.optional(),
default: z.unknown().optional(),
description: zCoreToolsEntitiesCommonEntitiesI18nObject.optional(),
label: zCoreToolsEntitiesCommonEntitiesI18nObject,
max: z.unknown().optional(),
min: z.unknown().optional(),
multiple: z.boolean().nullish().default(false),
name: z.string(),
options: z.array(zPluginParameterOption).nullish(),
precision: z.int().nullish(),
required: z.boolean().nullish().default(false),
scope: z.string().nullish(),
template: zPluginParameterTemplate.optional(),
type: zEventParameterType,
})
/**
* EventEntity
*
* The configuration of an event
*/
export const zEventEntity = z.object({
description: zCoreToolsEntitiesCommonEntitiesI18nObject,
identity: zEventIdentity,
output_schema: z.record(z.string(), z.unknown()).nullish(),
parameters: z.array(zEventParameter).optional(),
})
/**
* SubscriptionConstructor
*
* The subscription constructor of the trigger provider
*/
export const zSubscriptionConstructor = z.object({
credentials_schema: z.array(zProviderConfig).optional(),
oauth_schema: zOAuthSchema.optional(),
parameters: z.array(zEventParameter).optional(),
})
/**
* TriggerProviderEntity
*
* The configuration of a trigger provider
*/
export const zTriggerProviderEntity = z.object({
events: z.array(zEventEntity).optional(),
identity: zTriggerProviderIdentity,
subscription_constructor: zSubscriptionConstructor.optional(),
subscription_schema: z.array(zProviderConfig).optional(),
})
/**
* PluginDeclaration
*/
export const zPluginDeclaration = z.object({
agent_strategy: zAgentStrategyProviderEntity.optional(),
author: z
.string()
.regex(/^[\w-]{1,64}$/)
.nullable(),
category: zPluginCategory,
created_at: z.iso.datetime(),
datasource: zDatasourceProviderEntity.optional(),
description: zCoreToolsEntitiesCommonEntitiesI18nObject,
endpoint: zEndpointProviderDeclaration.optional(),
icon: z.string(),
icon_dark: z.string().nullish(),
label: zCoreToolsEntitiesCommonEntitiesI18nObject,
meta: zMeta,
model: zProviderEntity.optional(),
name: z.string().regex(/^[a-z0-9_-]{1,128}$/),
plugins: zPlugins,
repo: z.string().nullish(),
resource: zPluginResourceRequirements,
tags: z.array(z.string()).optional(),
tool: zToolProviderEntity.optional(),
trigger: zTriggerProviderEntity.optional(),
verified: z.boolean().optional().default(false),
version: z.string(),
})
/**
* PluginCategoryInstalledPluginResponse
*/
export const zPluginCategoryInstalledPluginResponse = z.object({
checksum: z.string(),
created_at: z.iso.datetime(),
declaration: zPluginDeclaration,
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),
})
/**
* Success
*/
@@ -1564,6 +2362,30 @@ export const zGetWorkspacesCurrentPluginAssetQuery = z.object({
*/
export const zGetWorkspacesCurrentPluginAssetResponse = z.record(z.string(), z.unknown())
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.string(),
})
/**
* Success
*/
export const zGetWorkspacesCurrentPluginAutoUpgradeFetchResponse = zPluginAutoUpgradeFetchResponse
/**
* Success
*/
@@ -1691,31 +2513,6 @@ export const zPostWorkspacesCurrentPluginPermissionChangeResponse = zSuccessResp
*/
export const zGetWorkspacesCurrentPluginPermissionFetchResponse = z.record(z.string(), z.unknown())
export const zPostWorkspacesCurrentPluginPreferencesAutoupgradeExcludeBody = zParserExcludePlugin
/**
* Success
*/
export const zPostWorkspacesCurrentPluginPreferencesAutoupgradeExcludeResponse = z.record(
z.string(),
z.unknown(),
)
export const zPostWorkspacesCurrentPluginPreferencesChangeBody = zParserPreferencesChange
/**
* Success
*/
export const zPostWorkspacesCurrentPluginPreferencesChangeResponse = z.record(
z.string(),
z.unknown(),
)
/**
* Success
*/
export const zGetWorkspacesCurrentPluginPreferencesFetchResponse = z.record(z.string(), z.unknown())
export const zGetWorkspacesCurrentPluginReadmeQuery = z.object({
language: z.string().optional().default('en-US'),
plugin_unique_identifier: z.string(),
@@ -1810,6 +2607,20 @@ export const zPostWorkspacesCurrentPluginUploadGithubResponse = z.record(z.strin
*/
export const zPostWorkspacesCurrentPluginUploadPkgResponse = z.record(z.string(), z.unknown())
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
*/
@@ -398,6 +398,24 @@ const removeNullDefaults = (value: unknown, visited = new WeakSet<object>()) =>
Object.values(schema).forEach(item => removeNullDefaults(item, visited))
}
const normalizeRegexPatterns = (value: unknown, visited = new WeakSet<object>()) => {
if (!value || typeof value !== 'object' || visited.has(value))
return
visited.add(value)
if (Array.isArray(value)) {
value.forEach(item => normalizeRegexPatterns(item, visited))
return
}
const schema = value as SwaggerSchema
if (typeof schema.pattern === 'string')
schema.pattern = schema.pattern.replace(/^\^\(\?!\^/, '^(?!')
Object.values(schema).forEach(item => normalizeRegexPatterns(item, visited))
}
const isNullSchema = (schema: SwaggerSchema) => {
return schema.type === 'null'
}
@@ -791,6 +809,7 @@ const normalizeApiSwagger = (document: SwaggerDocument, surface: string) => {
ensureReferencedDefinitions(document)
normalizeNullableAnyOf(document)
removeNullDefaults(document)
normalizeRegexPatterns(document)
normalizeOperations(document, surface)
return document
@@ -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;
+13
View File
@@ -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);
+13
View File
@@ -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;
+13
View File
@@ -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);
+43
View File
@@ -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
<span aria-hidden className="i-custom-vender-integrations-mcp size-4" />
```
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`.
@@ -0,0 +1,3 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 15.3333 14.6667" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="Vector" d="M13.1423 4.75207L12.9779 5.12919C12.8576 5.40528 12.4757 5.40528 12.3554 5.12919L12.1911 4.75207C11.8981 4.07965 11.3703 3.54427 10.7118 3.25139L10.2053 3.02615C9.93153 2.90435 9.93153 2.50587 10.2053 2.38408L10.6835 2.17143C11.3589 1.87101 11.8961 1.31582 12.1841 0.620552L12.3529 0.213023C12.4705 -0.0710075 12.8628 -0.0710075 12.9804 0.213023L13.1492 0.620552C13.4372 1.31582 13.9744 1.87101 14.6499 2.17143L15.1279 2.38408C15.4018 2.50587 15.4018 2.90435 15.1279 3.02615L14.6215 3.25139C13.963 3.54427 13.4353 4.07965 13.1423 4.75207ZM5.33333 1.33333C8.045 1.33333 10.284 3.35708 10.6225 5.97663L12.1228 8.3358C12.2216 8.49113 12.2017 8.72313 11.9729 8.82113L10.6667 9.38067V11.3333C10.6667 12.0697 10.0697 12.6667 9.33333 12.6667H8.00067L8 14.6667H2L2.00017 12.2041C2.00022 11.4168 1.70901 10.6725 1.17033 10.0007C0.438047 9.08753 0 7.92827 0 6.66667C0 3.72115 2.38781 1.33333 5.33333 1.33333ZM13.4357 12.0683L12.3262 11.3286C12.9624 10.3761 13.3333 9.2314 13.3333 8.00007C13.3333 7.65933 13.3049 7.3252 13.2504 7L14.5457 6.66667C14.6252 7.09907 14.6667 7.54467 14.6667 8.00007C14.6667 9.50507 14.2133 10.9041 13.4357 12.0683Z" fill="var(--fill-0, #18222F)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,3 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 15.3333 14.6667" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="Vector" d="M13.1423 4.75207L12.9779 5.12919C12.8576 5.40528 12.4757 5.40528 12.3554 5.12919L12.1911 4.75207C11.8981 4.07965 11.3703 3.54427 10.7118 3.25139L10.2053 3.02615C9.93153 2.90435 9.93153 2.50587 10.2053 2.38408L10.6835 2.17143C11.3589 1.87101 11.8961 1.31582 12.1841 0.620552L12.3529 0.213023C12.4705 -0.0710075 12.8628 -0.0710075 12.9804 0.213023L13.1492 0.620552C13.4372 1.31582 13.9744 1.87101 14.6499 2.17143L15.1279 2.38408C15.4018 2.50587 15.4018 2.90435 15.1279 3.02615L14.6215 3.25139C13.963 3.54427 13.4353 4.07965 13.1423 4.75207ZM5.33333 1.33333C8.045 1.33333 10.284 3.35708 10.6225 5.97663L12.1228 8.3358C12.2216 8.49113 12.2017 8.72313 11.9729 8.82113L10.6667 9.38067V11.3333C10.6667 12.0697 10.0697 12.6667 9.33333 12.6667H8.00067L8 14.6667H2L2.00017 12.2041C2.00022 11.4168 1.70901 10.6725 1.17033 10.0007C0.438047 9.08753 0 7.92827 0 6.66667C0 3.72115 2.38781 1.33333 5.33333 1.33333ZM5.33333 2.66667C3.12419 2.66667 1.33333 4.45753 1.33333 6.66667C1.33333 7.58993 1.64545 8.46193 2.21052 9.1666C2.93977 10.076 3.33357 11.1115 3.3335 12.2042L3.33342 13.3333H6.66713L6.6678 11.3333H9.33333V8.50127L10.3665 8.05873L9.33813 6.44175L9.30007 6.14745C9.04433 4.16761 7.34953 2.66667 5.33333 2.66667ZM12.3262 11.3286L13.4357 12.0683C14.2133 10.9041 14.6667 9.50507 14.6667 8.00007C14.6667 7.54467 14.6252 7.09907 14.5457 6.66667L13.2504 7C13.3049 7.3252 13.3333 7.65933 13.3333 8.00007C13.3333 9.2314 12.9624 10.3761 12.3262 11.3286Z" fill="var(--fill-0, #495464)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1,10 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Icon">
<g id="Vector">
<path d="M5.92578 11.0095C5.92578 10.0175 5.12163 9.21267 4.12956 9.21267C3.13752 9.21271 2.33333 10.0175 2.33333 11.0095C2.33349 12.0015 3.13762 12.8057 4.12956 12.8058C5.12153 12.8058 5.92562 12.0015 5.92578 11.0095ZM13.6667 11.0095C13.6667 10.0175 12.8625 9.21271 11.8704 9.21267C10.8784 9.21267 10.0742 10.0175 10.0742 11.0095C10.0744 12.0015 10.8785 12.8058 11.8704 12.8058C12.8624 12.8057 13.6665 12.0015 13.6667 11.0095ZM9.79622 4.324C9.79619 3.33197 8.99205 2.52778 8 2.52778C7.00795 2.52778 6.20382 3.33197 6.20378 4.324C6.20378 5.31607 7.00793 6.12023 8 6.12023C8.99207 6.12023 9.79622 5.31607 9.79622 4.324ZM11.1296 4.324C11.1296 5.82362 10.0748 7.07639 8.66667 7.38194V7.9197L9.74284 8.71398C10.3012 8.19618 11.0489 7.87934 11.8704 7.87934C13.5989 7.87938 15 9.28112 15 11.0095C14.9998 12.7378 13.5988 14.1391 11.8704 14.1391C10.1421 14.1391 8.74104 12.7379 8.74089 11.0095C8.74089 10.5838 8.82585 10.1777 8.97982 9.80772L8 9.08377L7.01953 9.80772C7.17356 10.1778 7.25911 10.5837 7.25911 11.0095C7.25896 12.7379 5.85791 14.1391 4.12956 14.1391C2.40124 14.1391 1.00016 12.7378 1 11.0095C1 9.28112 2.40114 7.87938 4.12956 7.87934C4.95094 7.87934 5.69819 8.19637 6.25651 8.71398L7.33333 7.9197V7.38194C5.92523 7.07639 4.87044 5.82362 4.87044 4.324C4.87048 2.59559 6.27158 1.19444 8 1.19444C9.72842 1.19444 11.1295 2.59559 11.1296 4.324Z" fill="var(--fill-0, #18222F)"/>
<path d="M9.79622 4.324C9.79619 3.33197 8.99205 2.52778 8 2.52778C7.00795 2.52778 6.20382 3.33197 6.20378 4.324C6.20378 5.31607 7.00793 6.12023 8 6.12023C8.99207 6.12023 9.79622 5.31607 9.79622 4.324Z" fill="var(--fill-0, #18222F)"/>
<path d="M5.92578 11.0095C5.92578 10.0175 5.12163 9.21267 4.12956 9.21267C3.13752 9.21271 2.33333 10.0175 2.33333 11.0095C2.33349 12.0015 3.13762 12.8057 4.12956 12.8058C5.12153 12.8058 5.92562 12.0015 5.92578 11.0095Z" fill="var(--fill-0, #18222F)"/>
<path d="M13.6667 11.0095C13.6667 10.0175 12.8625 9.21271 11.8704 9.21267C10.8784 9.21267 10.0742 10.0175 10.0742 11.0095C10.0744 12.0015 10.8785 12.8058 11.8704 12.8058C12.8624 12.8057 13.6665 12.0015 13.6667 11.0095Z" fill="var(--fill-0, #18222F)"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

@@ -0,0 +1,3 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 14 12.9447" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="Vector" d="M4.92578 9.8151C4.92578 8.82303 4.12163 8.01823 3.12956 8.01823C2.13752 8.01827 1.33333 8.82306 1.33333 9.8151C1.33349 10.807 2.13762 11.6113 3.12956 11.6113C4.12153 11.6113 4.92563 10.807 4.92578 9.8151ZM12.6667 9.8151C12.6667 8.82306 11.8625 8.01827 10.8704 8.01823C9.87837 8.01823 9.07422 8.82303 9.07422 9.8151C9.07438 10.807 9.87847 11.6113 10.8704 11.6113C11.8624 11.6113 12.6665 10.807 12.6667 9.8151ZM8.79622 3.12956C8.79619 2.13752 7.99205 1.33333 7 1.33333C6.00795 1.33333 5.20382 2.13752 5.20378 3.12956C5.20378 4.12163 6.00793 4.92578 7 4.92578C7.99207 4.92578 8.79622 4.12163 8.79622 3.12956ZM10.1296 3.12956C10.1296 4.62918 9.07477 5.88194 7.66667 6.1875V6.72526L8.74284 7.51953C9.3012 7.00174 10.0489 6.6849 10.8704 6.6849C12.5989 6.68493 14 8.08668 14 9.8151C13.9998 11.5434 12.5988 12.9446 10.8704 12.9447C9.14209 12.9447 7.74104 11.5434 7.74089 9.8151C7.74089 9.38937 7.82585 8.98325 7.97982 8.61328L7 7.88932L6.01953 8.61328C6.17356 8.98332 6.25911 9.38929 6.25911 9.8151C6.25896 11.5434 4.85791 12.9447 3.12956 12.9447C1.40124 12.9446 0.000156326 11.5434 0 9.8151C0 8.08668 1.40114 6.68493 3.12956 6.6849C3.95094 6.6849 4.69819 7.00193 5.25651 7.51953L6.33333 6.72526V6.1875C4.92523 5.88194 3.87044 4.62918 3.87044 3.12956C3.87048 1.40114 5.27158 0 7 0C8.72843 0 10.1295 1.40114 10.1296 3.12956Z" fill="var(--fill-0, #495464)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,3 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 12.6667 14.2807" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="Vector" d="M12.0014 3.2815L6.33333 0L0.665287 3.2815L6.33333 6.563L12.0014 3.2815ZM0 4.437V11L5.66667 14.2807V7.71767L0 4.437ZM7 14.2807L12.6667 11V4.437L7 7.71767V14.2807Z" fill="var(--fill-0, #18222F)"/>
</svg>

After

Width:  |  Height:  |  Size: 403 B

@@ -0,0 +1,3 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 12.6667 14.6667" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="Vector" d="M6.33333 0L12.6667 3.66667V11L6.33333 14.6667L0 11V3.66667L6.33333 0ZM1.99592 4.0518L6.3334 6.56293L10.6708 4.05183L6.33333 1.54067L1.99592 4.0518ZM1.33333 5.20886V10.2313L5.66673 12.7401V7.71767L1.33333 5.20886ZM7.00007 12.74L11.3333 10.2313V5.20891L7.00007 7.71767V12.74Z" fill="var(--fill-0, #495464)"/>
</svg>

After

Width:  |  Height:  |  Size: 515 B

@@ -0,0 +1,3 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 12 13.3333" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="Vector" d="M3.33333 2C3.33333 0.895433 4.22877 0 5.33333 0C6.43793 0 7.33333 0.895433 7.33333 2C7.33333 2.23376 7.2932 2.45815 7.21953 2.66667H11.3333C11.7015 2.66667 12 2.96515 12 3.33333V5.41735C12 5.62345 11.9047 5.81796 11.7418 5.94423C11.5789 6.07053 11.3667 6.11433 11.1671 6.063C11.0079 6.022 10.8403 6 10.6667 6C9.56207 6 8.66667 6.8954 8.66667 8C8.66667 9.1046 9.56207 10 10.6667 10C10.8403 10 11.0079 9.978 11.1671 9.937C11.3667 9.88567 11.5789 9.92947 11.7418 10.0557C11.9047 10.1821 12 10.3765 12 10.5827V12.6667C12 13.0349 11.7015 13.3333 11.3333 13.3333H0.666667C0.29848 13.3333 0 13.0349 0 12.6667V3.33333C0 2.96515 0.29848 2.66667 0.666667 2.66667H3.44714C3.37343 2.45815 3.33333 2.23376 3.33333 2Z" fill="var(--fill-0, #18222F)"/>
</svg>

After

Width:  |  Height:  |  Size: 940 B

@@ -0,0 +1,3 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 12 13.3333" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="Vector" d="M2.66667 2.66667C2.66667 1.19391 3.86057 0 5.33333 0C6.80607 0 8 1.19391 8 2.66667H11.3333C11.7015 2.66667 12 2.96515 12 3.33333V6.1138C12 6.3302 11.8949 6.53313 11.7183 6.65813C11.5415 6.78307 11.3152 6.81447 11.1112 6.74233C10.973 6.69353 10.8237 6.66667 10.6667 6.66667C9.93027 6.66667 9.33333 7.2636 9.33333 8C9.33333 8.7364 9.93027 9.33333 10.6667 9.33333C10.8237 9.33333 10.973 9.30647 11.1112 9.25767C11.3152 9.18553 11.5415 9.21693 11.7183 9.34187C11.8949 9.46687 12 9.6698 12 9.8862V12.6667C12 13.0349 11.7015 13.3333 11.3333 13.3333H0.666667C0.29848 13.3333 0 13.0349 0 12.6667V3.33333C0 2.96515 0.29848 2.66667 0.666667 2.66667H2.66667ZM5.33333 1.33333C4.59695 1.33333 4 1.93029 4 2.66667C4 2.82369 4.02687 2.97301 4.0757 3.11117C4.14781 3.31521 4.11641 3.54157 3.99145 3.71826C3.86649 3.89495 3.66355 4 3.44714 4H1.33333V12H10.6667V10.6667C9.19393 10.6667 8 9.47273 8 8C8 6.52727 9.19393 5.33333 10.6667 5.33333V4H7.21953C7.00313 4 6.8002 3.89495 6.6752 3.71826C6.55027 3.54157 6.51887 3.31521 6.591 3.11117C6.6398 2.97301 6.66667 2.8237 6.66667 2.66667C6.66667 1.93029 6.06973 1.33333 5.33333 1.33333Z" fill="var(--fill-0, #495464)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,3 @@
<svg width="13.3333" height="13.3333" viewBox="0 0 13.3333 13.3333" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.6709 1.83301C12.754 1.83301 12.833 1.90708 12.833 2.00195V9.125L9.125 12.8311L2.00098 12.832C1.90424 12.8318 1.83301 12.7562 1.83301 12.6709V7.16699H2.16699V12.5H8.5V8.66699C8.5 8.57647 8.57647 8.5 8.66699 8.5L12 8.49902H12.5V2.16699H7.16699V1.83301H12.6709ZM11.4473 8.83301H8.83301V12.6533L9.68652 11.7998L11.8008 9.68652L12.6553 8.83203L11.4473 8.83301ZM2.83301 0.5V2.5H4.83301V2.83301H2.83301V4.83301H2.5V2.83301H0.5V2.5H2.5V0.5H2.83301Z" stroke="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 602 B

@@ -0,0 +1,3 @@
<svg width="11.6416" height="13.086" viewBox="0 0 11.6416 13.086" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.0996 0.512695C10.1337 0.518171 10.156 0.524407 10.168 0.52832C10.2148 0.543681 10.2522 0.579318 10.2705 0.625C10.5581 1.34398 10.5694 1.95842 10.4502 2.44824L10.3955 2.67578L10.5342 2.86328C10.9293 3.39873 11.1416 4.03345 11.1416 4.76172C11.1416 5.94177 10.8789 6.76774 10.4385 7.34668C9.99983 7.92308 9.33952 8.3137 8.42773 8.53711L7.91602 8.66309L8.06738 9.16699C8.1351 9.39201 8.17383 9.65236 8.17383 9.94336L8.16895 11.2803C8.16815 11.4068 8.16748 11.5418 8.16602 11.75L8.16309 12.1553L8.55762 12.2422C8.62114 12.2562 8.67222 12.3062 8.68555 12.3721C8.7036 12.4623 8.64494 12.5503 8.55469 12.5684C8.30873 12.6174 8.13226 12.5563 8.02148 12.4668C7.90812 12.3752 7.83118 12.2287 7.83105 12.043C7.83105 11.9847 7.83103 11.8999 7.83203 11.748C7.83356 11.5396 7.83507 11.4046 7.83594 11.2783C7.83901 10.8059 7.84082 10.3843 7.84082 9.94336C7.84082 9.41961 7.70678 8.93648 7.38281 8.65723C7.27271 8.56225 7.32913 8.38146 7.47363 8.36523C8.5007 8.24979 9.36433 7.98413 9.96094 7.37695C10.5669 6.76005 10.8086 5.884 10.8086 4.76172C10.8086 4.00303 10.5554 3.35601 10.0693 2.82227C10.0264 2.77514 10.0144 2.70773 10.0381 2.64844C10.1874 2.27509 10.24 1.81114 10.126 1.28125L10.0137 0.759766L9.50098 0.905273L9.49414 0.907227C9.09648 1.01979 8.63458 1.25105 8.11035 1.60742C8.06963 1.63503 8.01887 1.64314 7.97168 1.62988C7.37871 1.46312 6.74527 1.37892 6.1084 1.37891C5.4715 1.37891 4.83803 1.46315 4.24512 1.62988C4.19802 1.64313 4.14702 1.63474 4.10645 1.60742C3.57968 1.25283 3.11619 1.02297 2.71777 0.910156L2.20703 0.765625L2.09277 1.28418C1.97674 1.813 2.02979 2.27614 2.17871 2.64844C2.20225 2.70762 2.19033 2.77513 2.14746 2.82227C1.66395 3.35317 1.4082 4.00895 1.4082 4.76172C1.40822 5.88234 1.64983 6.75778 2.25391 7.375C2.84913 7.98307 3.71022 8.25023 4.7334 8.36523C4.87762 8.38143 4.93358 8.56104 4.82422 8.65625C4.66044 8.79875 4.55302 9.0215 4.48828 9.20898C4.41614 9.41793 4.36621 9.67286 4.36621 9.94336V12.043C4.36598 12.3722 4.10732 12.6499 3.64648 12.5693C3.55582 12.5535 3.49488 12.4666 3.51074 12.376C3.52276 12.3083 3.57458 12.2564 3.63965 12.2422L4.0332 12.1562V10.5586L3.49902 10.5947C2.9627 10.6308 2.58298 10.5389 2.30859 10.3555C2.16409 10.2588 2.0263 10.1279 1.83984 9.90527C1.80759 9.86673 1.71954 9.75814 1.64258 9.66211C1.60519 9.61545 1.57219 9.5735 1.55176 9.54785C1.54666 9.54146 1.54226 9.53528 1.53906 9.53125L1.53711 9.5293C1.53789 9.53032 1.54048 9.53427 1.54395 9.53906C1.54454 9.53989 1.55212 9.55023 1.56055 9.56348C1.56664 9.57332 1.58653 9.60864 1.59863 9.63477C1.62166 9.72473 1.52906 9.94237 1.35645 10.1016L1.14648 9.84082L1.53613 9.52734C1.53304 9.52351 1.52837 9.51827 1.52539 9.51465C1.52488 9.51403 1.52438 9.51285 1.52344 9.51172C1.52279 9.51094 1.51991 9.50772 1.5166 9.50391C1.51604 9.50326 1.51446 9.50199 1.5127 9.5C1.21539 9.13323 0.948869 8.85872 0.610352 8.7373C0.523946 8.70626 0.479023 8.61085 0.509766 8.52441C0.540776 8.43794 0.636154 8.39219 0.722656 8.42285C1.0883 8.55402 1.35448 8.77291 1.7793 9.29785C1.78078 9.29986 1.78248 9.30178 1.7832 9.30273C1.78551 9.30578 1.78769 9.30808 1.78809 9.30859C1.79204 9.31367 1.79973 9.32481 1.80859 9.33594C1.82821 9.36058 1.8612 9.401 1.89746 9.44629L2.0957 9.69141C2.2267 9.84782 2.35754 9.98732 2.49316 10.0781C2.78339 10.2725 3.19313 10.2925 3.58887 10.2529L4.01172 10.2109L4.03809 9.78711C4.05126 9.57333 4.09056 9.36582 4.15039 9.17578L4.31055 8.66699L3.79199 8.54004C2.88088 8.31734 2.21997 7.92594 1.78027 7.34863C1.33864 6.76857 1.07521 5.94109 1.0752 4.76172C1.0752 4.0396 1.28837 3.39917 1.68262 2.86328L1.82129 2.67578L1.76562 2.44824C1.64647 1.95845 1.65776 1.34391 1.94531 0.625C1.96354 0.57943 2.00137 0.544774 2.04785 0.529297C2.07893 0.520398 2.08893 0.517862 2.11621 0.513672C2.47591 0.45859 3.10496 0.581722 4.05176 1.1748L4.22852 1.28516L4.43164 1.2373C4.97053 1.11008 5.53743 1.04492 6.1084 1.04492C6.67867 1.04494 7.24479 1.11034 7.7832 1.2373L7.9873 1.28516L8.16504 1.17285C9.10977 0.576084 9.73863 0.454651 10.0996 0.512695ZM2.39551 9.2666C2.28256 9.36997 2.13716 9.45037 1.96484 9.45117C1.91043 9.42066 1.8462 9.37172 1.83301 9.35937C1.82701 9.35346 1.81779 9.34341 1.81445 9.33984C1.81233 9.33754 1.80911 9.3337 1.80762 9.33203C1.80466 9.3287 1.80177 9.32635 1.80078 9.3252L1.79687 9.32031L1.80078 9.32422L2.19043 9.01172C2.22028 9.0493 2.31937 9.17218 2.39551 9.2666Z" stroke="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

@@ -0,0 +1,3 @@
<svg width="12" height="13.3333" viewBox="0 0 12 13.3333" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.332 0.5C11.426 0.500094 11.5 0.580915 11.5 0.661133V12.6719C11.5 12.7601 11.4282 12.8329 11.3379 12.833H0.662109C0.578569 12.8329 0.5 12.7632 0.5 12.6621V4.20703L4.20898 0.5H11.332ZM4.83301 4.66699C4.83283 4.75878 4.75878 4.83283 4.66699 4.83301H0.833008V12.5H11.167V0.833008H4.83301V4.66699ZM3.64648 1.5332L1.53223 3.64648L0.678711 4.5H4.5V0.680664L3.64648 1.5332Z" stroke="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 517 B

@@ -0,0 +1,3 @@
<svg width="14.6667" height="13.3333" viewBox="0 0 14.6667 13.3333" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.13867 6.72949C8.67785 7.21017 8.03241 7.5 7.33301 7.5C6.63364 7.49991 5.9881 7.21021 5.52734 6.72949L5.16699 6.35254L4.80566 6.72949C4.34483 7.21027 3.69949 7.5 3 7.5C2.90569 7.5 2.81278 7.49447 2.72168 7.48438L2.16699 7.42285V12.5H12.5V7.42285L11.9453 7.48438C11.8543 7.49446 11.7612 7.49999 11.667 7.5C10.9676 7.5 10.3221 7.21017 9.86133 6.72949L9.5 6.35254L9.13867 6.72949ZM2.75977 1.08301L1.14258 3.88379C0.941179 4.21836 0.833008 4.60162 0.833008 5C0.833008 6.19661 1.80338 7.16699 3 7.16699C3.89354 7.16699 4.68499 6.62068 5.01172 5.80566C5.06765 5.66613 5.26532 5.66617 5.32129 5.80566C5.64794 6.62063 6.43956 7.16686 7.33301 7.16699C8.22659 7.16699 9.019 6.62072 9.3457 5.80566C9.4017 5.66633 9.5983 5.66633 9.6543 5.80566C9.981 6.62072 10.7734 7.16699 11.667 7.16699C12.8635 7.16682 13.833 6.1965 13.833 5C13.833 4.59984 13.725 4.21734 13.5186 3.87402H13.5176L11.9072 1.08301L11.7627 0.833008H2.90332L2.75977 1.08301ZM1.83301 7.22754L1.61133 7.0791C0.94019 6.62981 0.5 5.86624 0.5 5C0.5 4.53874 0.625339 4.09665 0.850586 3.72266L0.855469 3.71484L2.66309 0.583008C2.69288 0.531554 2.74815 0.5 2.80762 0.5H11.8594C11.9188 0.500086 11.9742 0.531575 12.0039 0.583008L13.8057 3.7041L13.8096 3.71191C14.0416 4.09734 14.167 4.53978 14.167 5C14.167 5.86614 13.7267 6.62979 13.0557 7.0791L12.833 7.22754V12.5H13.5V12.833H1.16699V12.5H1.83301V7.22754Z" stroke="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,6 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 13.4445 14.6667" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Vector">
<path d="M7.873 1.01976C8.28503 1.01976 8.68078 1.18055 8.97605 1.46791C9.12039 1.60841 9.23516 1.77638 9.31357 1.96193C9.39199 2.14747 9.43248 2.34683 9.43265 2.54827C9.43282 2.7497 9.39267 2.94913 9.31457 3.13481C9.23647 3.32048 9.12199 3.48865 8.97788 3.62939L4.53331 7.98841C4.48512 8.03528 4.44681 8.09133 4.42065 8.15326C4.39449 8.21519 4.38102 8.28173 4.38102 8.34896C4.38102 8.41619 4.39449 8.48273 4.42065 8.54466C4.44681 8.60659 4.48512 8.66264 4.53331 8.70951C4.63178 8.80538 4.76378 8.85902 4.9012 8.85902C5.03862 8.85902 5.17062 8.80538 5.26908 8.70951L5.32897 8.65024L5.33019 8.64901L9.71304 4.3505C10.0085 4.06393 10.404 3.90381 10.8155 3.90415C11.2271 3.90449 11.6224 4.06527 11.9173 4.35233L11.9479 4.38228C12.0924 4.52293 12.2072 4.69111 12.2856 4.87689C12.3641 5.06267 12.4045 5.26228 12.4045 5.46393C12.4045 5.66559 12.3641 5.8652 12.2856 6.05098C12.2072 6.23676 12.0924 6.40494 11.9479 6.54559L6.62757 11.7632C6.51503 11.8726 6.42557 12.0034 6.36448 12.1479C6.30339 12.2925 6.27191 12.4478 6.27191 12.6047C6.27191 12.7616 6.30339 12.9169 6.36448 13.0615C6.42557 13.206 6.51503 13.3368 6.62757 13.4462L7.72023 14.5175C7.81867 14.6131 7.95053 14.6667 8.08781 14.6667C8.22508 14.6667 8.35695 14.6131 8.45539 14.5175C8.50358 14.4706 8.54189 14.4145 8.56805 14.3526C8.59421 14.2907 8.60769 14.2241 8.60769 14.1569C8.60769 14.0897 8.59421 14.0231 8.56805 13.9612C8.54189 13.8993 8.50358 13.8432 8.45539 13.7964L7.36273 12.7245C7.34667 12.7089 7.33391 12.6902 7.32519 12.6696C7.31647 12.649 7.31198 12.6268 7.31198 12.6044C7.31198 12.582 7.31647 12.5598 7.32519 12.5392C7.33391 12.5186 7.34667 12.4999 7.36273 12.4843L12.683 7.2673C12.924 7.03296 13.1155 6.75268 13.2463 6.44304C13.3771 6.1334 13.4445 5.80068 13.4445 5.46454C13.4445 5.12841 13.3771 4.79569 13.2463 4.48605C13.1155 4.17641 12.924 3.89613 12.683 3.66178L12.6525 3.63123C12.3646 3.35023 12.016 3.13908 11.6336 3.01405C11.2512 2.88903 10.8453 2.85347 10.447 2.91012C10.5038 2.51705 10.4668 2.1161 10.3389 1.74009C10.211 1.36408 9.99593 1.0237 9.71121 0.746808C9.21914 0.267942 8.55962 0 7.873 0C7.18639 0 6.52687 0.267942 6.0348 0.746808L0.152297 6.51564C0.104103 6.56251 0.0657945 6.61857 0.039636 6.6805C0.0134776 6.74243 0 6.80897 0 6.8762C0 6.94342 0.0134776 7.00997 0.039636 7.0719C0.0657945 7.13382 0.104103 7.18988 0.152297 7.23675C0.250735 7.33243 0.382601 7.38595 0.519877 7.38595C0.657153 7.38595 0.789019 7.33243 0.887457 7.23675L6.76996 1.46791C7.06523 1.18055 7.46098 1.01976 7.873 1.01976Z" fill="var(--fill-0, #495464)"/>
<path d="M8.35362 2.74526C8.32747 2.80719 8.28916 2.86324 8.24096 2.91011L3.88989 7.17685C3.74539 7.3175 3.63053 7.48568 3.5521 7.67146C3.47367 7.85724 3.43327 8.05685 3.43327 8.25851C3.43327 8.46016 3.47367 8.65977 3.5521 8.84555C3.63053 9.03133 3.74539 9.19951 3.88989 9.34017C4.18516 9.62753 4.58092 9.78832 4.99294 9.78832C5.40496 9.78832 5.80072 9.62753 6.09598 9.34017L10.4464 5.07343C10.5449 4.97756 10.6769 4.92392 10.8143 4.92392C10.9518 4.92392 11.0837 4.97756 11.1822 5.07343C11.2304 5.1203 11.2687 5.17635 11.2949 5.23828C11.321 5.30021 11.3345 5.36675 11.3345 5.43398C11.3345 5.5012 11.321 5.56775 11.2949 5.62968C11.2687 5.69161 11.2304 5.74766 11.1822 5.79453L6.83114 10.0613C6.339 10.54 5.67951 10.8078 4.99294 10.8078C4.30636 10.8078 3.64688 10.54 3.15473 10.0613C2.91376 9.82692 2.72222 9.54664 2.59143 9.237C2.46064 8.92736 2.39325 8.59464 2.39325 8.25851C2.39325 7.92238 2.46064 7.58966 2.59143 7.28001C2.72222 6.97037 2.91376 6.69009 3.15473 6.45575L7.50519 2.18901C7.60366 2.09315 7.73566 2.03951 7.87308 2.03951C8.0105 2.03951 8.1425 2.09315 8.24096 2.18901C8.28916 2.23588 8.32747 2.29193 8.35362 2.35386C8.37978 2.41579 8.39326 2.48233 8.39326 2.54956C8.39326 2.61679 8.37978 2.68333 8.35362 2.74526Z" fill="var(--fill-0, #495464)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

@@ -0,0 +1,3 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 14.5 14.5" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="Vector" d="M5.08333 0.75V13.75M2.19444 0.75H12.3056C13.1033 0.75 13.75 1.3967 13.75 2.19444V12.3056C13.75 13.1033 13.1033 13.75 12.3056 13.75H2.19444C1.3967 13.75 0.75 13.1033 0.75 12.3056V2.19444C0.75 1.3967 1.3967 0.75 2.19444 0.75Z" stroke="var(--stroke-0, black)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 527 B

@@ -0,0 +1,3 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 12.3333 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="Vector" d="M9.66667 4V0H11.6667C12.0349 0 12.3333 0.29848 12.3333 0.666667V3.33333C12.3333 3.70152 12.0349 4 11.6667 4H9.66667ZM8.33333 13.3333C8.33333 13.7015 8.03487 14 7.66667 14H5C4.63181 14 4.33333 13.7015 4.33333 13.3333V4H0V2.71625C0 2.47913 0.12594 2.25987 0.330753 2.14039L4 0H8.33333V13.3333Z" fill="var(--fill-0, #18222F)"/>
</svg>

After

Width:  |  Height:  |  Size: 528 B

@@ -0,0 +1,3 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 12.3333 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="Vector" d="M11.6667 0C12.0349 0 12.3333 0.29848 12.3333 0.666667V4C12.3333 4.36819 12.0349 4.66667 11.6667 4.66667H8.33333V13.3333C8.33333 13.7015 8.03487 14 7.66667 14H5C4.63181 14 4.33333 13.7015 4.33333 13.3333V4.66667H0.666667C0.29848 4.66667 0 4.36819 0 4V2.41202C0 2.15951 0.142667 1.92867 0.368527 1.81574L4 0H11.6667ZM8.33333 1.33333H4.31476L1.33333 2.82405V3.33333H5.66667V12.6667H7V3.33333H8.33333V1.33333ZM11 1.33333H9.66667V3.33333H11V1.33333Z" fill="var(--fill-0, #495464)"/>
</svg>

After

Width:  |  Height:  |  Size: 681 B

@@ -0,0 +1,10 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 13.325 13.325" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Vector">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.81641 5.01888L5.91797 5.04883L12.8913 7.70573L12.9837 7.7487C13.3889 7.97463 13.4445 8.54581 13.0905 8.8457L13.0085 8.9056L10.4837 10.4837L8.9056 13.0085C8.62921 13.4507 7.99075 13.4178 7.7487 12.9837L7.70573 12.8913L5.04883 5.91797C4.85479 5.40863 5.31088 4.90871 5.81641 5.01888Z" fill="var(--fill-0, #18222F)"/>
<path d="M3.87891 9.06445L2.22917 10.7142L1.28646 9.77148L2.9362 8.12175L3.87891 9.06445Z" fill="var(--fill-0, #18222F)"/>
<path d="M2.33333 6.66667H0V5.33333H2.33333V6.66667Z" fill="var(--fill-0, #18222F)"/>
<path d="M3.87891 2.93555L2.9362 3.87826L1.28646 2.22852L2.22917 1.28581L3.87891 2.93555Z" fill="var(--fill-0, #18222F)"/>
<path d="M10.7142 2.22852L9.06445 3.87826L8.12175 2.93555L9.77148 1.28581L10.7142 2.22852Z" fill="var(--fill-0, #18222F)"/>
<path d="M6.66667 2.33333H5.33333V0H6.66667V2.33333Z" fill="var(--fill-0, #18222F)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,10 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 13.325 13.325" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Vector">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.81641 5.01888L5.91797 5.04883L12.8913 7.70573L12.9837 7.7487C13.3889 7.97463 13.4445 8.54581 13.0905 8.8457L13.0085 8.9056L10.4837 10.4837L8.9056 13.0085C8.62921 13.4507 7.99075 13.4178 7.7487 12.9837L7.70573 12.8913L5.04883 5.91797C4.85479 5.40863 5.31088 4.90871 5.81641 5.01888ZM8.47852 11.1751L9.43359 9.64779L9.47786 9.58529C9.52536 9.52564 9.5828 9.47422 9.64779 9.43359L11.1751 8.47852L6.81901 6.81901L8.47852 11.1751Z" fill="var(--fill-0, #495464)"/>
<path d="M3.87891 9.06445L2.22917 10.7142L1.28646 9.77148L2.9362 8.12175L3.87891 9.06445Z" fill="var(--fill-0, #495464)"/>
<path d="M2.33333 6.66667H0V5.33333H2.33333V6.66667Z" fill="var(--fill-0, #495464)"/>
<path d="M3.87891 2.93555L2.9362 3.87826L1.28646 2.22852L2.22917 1.28581L3.87891 2.93555Z" fill="var(--fill-0, #495464)"/>
<path d="M10.7142 2.22852L9.06445 3.87826L8.12175 2.93555L9.77148 1.28581L10.7142 2.22852Z" fill="var(--fill-0, #495464)"/>
<path d="M6.66667 2.33333H5.33333V0H6.66667V2.33333Z" fill="var(--fill-0, #495464)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,9 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 12.1 11.4333" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Vector">
<path d="M1.71667 0C0.768578 0 0 0.768578 0 1.71667C0 2.66476 0.768578 3.43333 1.71667 3.43333H3.71667C4.66475 3.43333 5.43333 2.66476 5.43333 1.71667C5.43333 0.768578 4.66476 0 3.71667 0H1.71667Z" fill="var(--fill-0, #18222F)"/>
<path d="M8.38333 4C7.43524 4 6.66667 4.76858 6.66667 5.71667C6.66667 6.66476 7.43524 7.43333 8.38333 7.43333H10.3833C11.3314 7.43333 12.1 6.66476 12.1 5.71667C12.1 4.76858 11.3314 4 10.3833 4H8.38333Z" fill="var(--fill-0, #18222F)"/>
<path d="M0 9.71667C0 8.76858 0.768578 8 1.71667 8H3.71667C4.66476 8 5.43333 8.76858 5.43333 9.71667C5.43333 10.6648 4.66475 11.4333 3.71667 11.4333H1.71667C0.768578 11.4333 0 10.6648 0 9.71667Z" fill="var(--fill-0, #18222F)"/>
<path d="M7.05001 2.38334H6.38334V1.05001H7.05001C8.15458 1.05001 9.05001 1.94544 9.05001 3.05001H7.71667C7.71667 2.68182 7.4182 2.38334 7.05001 2.38334Z" fill="var(--fill-0, #18222F)"/>
<path d="M9.05001 8.38334C9.05001 9.48791 8.15458 10.3833 7.05001 10.3833H6.38334V9.05001H7.05001C7.4182 9.05001 7.71667 8.75153 7.71667 8.38334H9.05001Z" fill="var(--fill-0, #18222F)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.3333 2C13.7015 2 14 2.29848 14 2.66667V6C14 6.36819 13.7015 6.66667 13.3333 6.66667H10C9.63181 6.66667 9.33333 6.36819 9.33333 6V5H6.63086C5.94959 5.00026 5.70734 5.90184 6.29688 6.24349L10.3717 8.60221C12.1411 9.62681 11.4138 12.3331 9.36914 12.3333H6.66667V13.3333C6.66667 13.7015 6.36819 14 6 14H2.66667C2.29848 14 2 13.7015 2 13.3333V10C2 9.63181 2.29848 9.33333 2.66667 9.33333H6C6.36819 9.33333 6.66667 9.63181 6.66667 10V11H9.36914C10.0504 10.9997 10.2929 10.0981 9.70378 9.75651L5.62891 7.39779C3.85933 6.37322 4.58611 3.66693 6.63086 3.66667H9.33333V2.66667C9.33333 2.29848 9.63181 2 10 2H13.3333ZM3.33333 12.6667H5.33333V10.6667H3.33333V12.6667ZM10.6667 5.33333H12.6667V3.33333H10.6667V5.33333Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 861 B

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.66667 6.17435C2.66667 5.98785 2.66667 5.89459 2.69021 5.80837C2.71107 5.73198 2.74537 5.65992 2.7915 5.59556C2.84357 5.52292 2.91595 5.46411 3.0607 5.3465L7.3274 1.87983C7.56713 1.68502 7.687 1.58762 7.82027 1.55031C7.93787 1.51741 8.06213 1.51741 8.17973 1.55031C8.313 1.58762 8.43287 1.68502 8.6726 1.87983L12.9393 5.3465C13.0841 5.46411 13.1564 5.52292 13.2085 5.59556C13.2547 5.65992 13.2889 5.73198 13.3098 5.80837C13.3333 5.89459 13.3333 5.98785 13.3333 6.17435V12.2667C13.3333 12.64 13.3333 12.8267 13.2607 12.9693C13.1967 13.0947 13.0948 13.1967 12.9693 13.2607C12.8267 13.3333 12.6401 13.3333 12.2667 13.3333H3.73333C3.35997 13.3333 3.17328 13.3333 3.03067 13.2607C2.90523 13.1967 2.80325 13.0947 2.73933 12.9693C2.66667 12.8267 2.66667 12.64 2.66667 12.2667V6.17435Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
<path d="M10 13.3333V9.66667C10 9.11438 9.55228 8.66667 9 8.66667H7C6.44772 8.66667 6 9.11438 6 9.66667V13.3333" stroke="currentColor" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(3 3) scale(1.5)">
<path d="M7 10.5C8.933 10.5 10.5 8.4853 10.5 6C10.5 3.51472 8.933 1.5 7 1.5M7 10.5C5.067 10.5 3.5 8.4853 3.5 6C3.5 3.51472 5.067 1.5 7 1.5M7 10.5H5C3.06701 10.5 1.5 8.4853 1.5 6C1.5 3.51472 3.06701 1.5 5 1.5H7" stroke="currentColor" stroke-width="1"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 410 B

@@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 15.25C12.4142 15.25 12.75 15.5858 12.75 16V16.0098C12.75 16.424 12.4142 16.7598 12 16.7598C11.5858 16.7598 11.25 16.424 11.25 16.0098V16C11.25 15.5858 11.5858 15.25 12 15.25Z" fill="currentColor"/>
<path d="M14 7.25C14.4142 7.25 14.75 7.58579 14.75 8V10.5C14.75 10.7359 14.6389 10.958 14.4502 11.0996L12.75 12.374V13C12.75 13.4142 12.4142 13.75 12 13.75C11.5858 13.75 11.25 13.4142 11.25 13V12C11.25 11.7641 11.3611 11.542 11.5498 11.4004L13.25 10.125V8.75H10.75V9C10.75 9.41421 10.4142 9.75 10 9.75C9.58579 9.75 9.25 9.41421 9.25 9V8C9.25 7.58579 9.58579 7.25 10 7.25H14Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 716 B

@@ -0,0 +1,6 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(-5.5 -6.95)">
<path d="M14.449 8.37314C15.0613 7.87562 15.9387 7.87562 16.551 8.37314L22.3843 13.1127C22.7738 13.4292 23 13.9044 23 14.4063V22.7596C23 23.6801 22.2538 24.4263 21.3333 24.4263H18.8333V17.7599C18.8333 17.2997 18.4602 16.9266 18 16.9266H13C12.5398 16.9266 12.1667 17.2997 12.1667 17.7599V24.4263H9.66667C8.74619 24.4263 8 23.6801 8 22.7596V14.4063C8 13.9044 8.22616 13.4292 8.61568 13.1127L14.449 8.37314Z" fill="currentColor"/>
<path d="M13.833 24.4263H17.1663V18.5933H13.833V24.4263Z" fill="currentColor"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 666 B

@@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.33301 7.71788C3.33301 7.48475 3.33301 7.36818 3.36243 7.2604C3.38851 7.16492 3.43138 7.07484 3.48905 6.99439C3.55414 6.90359 3.64461 6.83008 3.82555 6.68307L9.15892 2.34973C9.45859 2.10622 9.60842 1.98447 9.77501 1.93783C9.92201 1.8967 10.0773 1.8967 10.2243 1.93783C10.3909 1.98447 10.5408 2.10622 10.8404 2.34973L16.1738 6.68307C16.3548 6.83008 16.4452 6.90359 16.5103 6.99439C16.568 7.07484 16.6108 7.16492 16.6369 7.2604C16.6663 7.36818 16.6663 7.48475 16.6663 7.71788V15.3333C16.6663 15.7999 16.6663 16.0334 16.5755 16.2116C16.4956 16.3684 16.3682 16.4959 16.2113 16.5758C16.0331 16.6666 15.7998 16.6666 15.333 16.6666H4.66634C4.19963 16.6666 3.96627 16.6666 3.78802 16.5758C3.63122 16.4959 3.50373 16.3684 3.42383 16.2116C3.33301 16.0334 3.33301 15.7999 3.33301 15.3333V7.71788Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
<path d="M12.5 16.6666V11.8333C12.5 11.281 12.0523 10.8333 11.5 10.8333H8.5C7.94772 10.8333 7.5 11.281 7.5 11.8333V16.6666" stroke="currentColor" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,7 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(-6.33 -5.55)">
<path d="M9.66602 8.83333C9.66602 8.3731 10.0391 8 10.4993 8H14.666C15.1263 8 15.4993 8.3731 15.4993 8.83333V10.5H9.66602V8.83333Z" fill="currentColor"/>
<path d="M17.166 8.83333C17.166 8.3731 17.5391 8 17.9993 8H22.166C22.6263 8 22.9993 8.3731 22.9993 8.83333V10.5H17.166V8.83333Z" fill="currentColor"/>
<path d="M8 13.0001C8 12.5398 8.3731 12.1667 8.83333 12.1667H23.8333C24.2936 12.1667 24.6667 12.5398 24.6667 13.0001V21.3334C24.6667 21.7937 24.2936 22.1667 23.8333 22.1667H8.83333C8.3731 22.1667 8 21.7937 8 21.3334V13.0001Z" fill="currentColor"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 716 B

@@ -0,0 +1,5 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.5 7.50008C2.5 7.03984 2.8731 6.66675 3.33333 6.66675H16.6667C17.1269 6.66675 17.5 7.03985 17.5 7.50008V15.0001C17.5 15.4603 17.1269 15.8334 16.6667 15.8334H3.33333C2.8731 15.8334 2.5 15.4603 2.5 15.0001V7.50008Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.16699 6.66659V4.58325C4.16699 3.89289 4.72663 3.33325 5.41699 3.33325H7.08366C7.77402 3.33325 8.33366 3.89289 8.33366 4.58325V6.66659" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.667 6.66659V4.16659C11.667 3.70635 12.0401 3.33325 12.5003 3.33325H15.0003C15.4606 3.33325 15.8337 3.70635 15.8337 4.16659V6.66659" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 896 B

@@ -0,0 +1,5 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(-4.667 -6.333)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.5 8C9.11929 8 8 9.11929 8 10.5V22.1667C8 23.5474 9.11929 24.6667 10.5 24.6667H20.5C20.9602 24.6667 21.3333 24.2936 21.3333 23.8333V8.83333C21.3333 8.3731 20.9602 8 20.5 8H10.5ZM9.66667 22.1667C9.66667 22.6269 10.0398 23 10.5 23H19.6667V21.3333H10.5C10.0398 21.3333 9.66667 21.7064 9.66667 22.1667ZM12.1667 11.3333C11.7064 11.3333 11.3333 11.7064 11.3333 12.1667C11.3333 12.6269 11.7064 13 12.1667 13H17.1667C17.6269 13 18 12.6269 18 12.1667C18 11.7064 17.6269 11.3333 17.1667 11.3333H12.1667ZM11.3333 15.5C11.3333 15.0397 11.7064 14.6667 12.1667 14.6667H14.6667C15.1269 14.6667 15.5 15.0397 15.5 15.5C15.5 15.9602 15.1269 16.3333 14.6667 16.3333H12.1667C11.7064 16.3333 11.3333 15.9602 11.3333 15.5Z" fill="currentColor"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 933 B

@@ -0,0 +1,6 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.5 9.16675H10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.5 5.83325H12.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.16699 4.16667C4.16699 3.24619 4.91318 2.5 5.83366 2.5H15.417C15.6471 2.5 15.8337 2.68655 15.8337 2.91667V17.5H5.83366C4.91318 17.5 4.16699 16.7538 4.16699 15.8333V4.16667Z" stroke="currentColor" stroke-width="1.5"/>
<path d="M4.16699 15.8334C4.16699 14.9129 4.91318 14.1667 5.83366 14.1667H15.8337V17.5001H5.83366C4.91318 17.5001 4.16699 16.7539 4.16699 15.8334Z" stroke="currentColor" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 764 B

@@ -0,0 +1,5 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(-6.3 -5.5)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.25369 8.58477C9.3624 8.23688 9.6846 8 10.0491 8H22.5491C22.9136 8 23.2357 8.23688 23.3445 8.58477L24.4481 12.1162C24.8042 13.256 24.5023 14.4059 23.7991 15.2164V22.1667C23.7991 22.6269 23.426 23 22.9657 23H9.63242C9.17219 23 8.79909 22.6269 8.79909 22.1667V15.2164C8.09588 14.4059 7.79393 13.256 8.15013 12.1162L9.25369 8.58477ZM18.0271 12.7092L17.6467 9.66667H14.9514L14.5711 12.7092C14.4412 13.7486 15.2516 14.6667 16.2991 14.6667C17.3465 14.6667 18.1568 13.7485 18.0271 12.7092ZM13.2718 9.66667H10.6617L9.74093 12.6133C9.42266 13.6317 10.1835 14.6667 11.2505 14.6667C12.0482 14.6667 12.721 14.0728 12.82 13.2812L13.2718 9.66667ZM19.3264 9.66667L19.6809 12.5025L19.7782 13.2812C19.8772 14.0728 20.55 14.6667 21.3477 14.6667C22.4147 14.6667 23.1755 13.6317 22.8572 12.6133L21.9364 9.66667H19.3264Z" fill="currentColor"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.667 9.99992V16.6666H3.33366V9.99992M7.91699 3.33325H12.0837M7.91699 3.33325L7.44543 7.10578C7.25334 8.6425 8.45158 9.99992 10.0003 9.99992C11.5491 9.99992 12.7473 8.6425 12.5552 7.10578L12.0837 3.33325M7.91699 3.33325H3.75033L2.64677 6.86465C2.16081 8.41975 3.32257 9.99992 4.95179 9.99992C6.1697 9.99992 7.19703 9.093 7.34809 7.88451L7.91699 3.33325ZM12.0837 3.33325H16.2503L17.3539 6.86465C17.8398 8.41975 16.6781 9.99992 15.0489 9.99992C13.831 9.99992 12.8037 9.093 12.6526 7.88451L12.0837 3.33325Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="square" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 712 B

@@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.0004 1.875C17.0398 1.87509 21.1246 5.9602 21.1249 10.9995C21.1249 13.1138 20.4037 15.0582 19.1972 16.6055L21.7958 19.2041C22.235 19.6433 22.2348 20.3556 21.7958 20.7949C21.3565 21.2343 20.6443 21.2343 20.205 20.7949L17.6064 18.1963C16.3417 19.1831 14.8123 19.8466 13.14 20.0552C12.5235 20.132 11.9616 19.6932 11.8847 19.0767C11.8081 18.4603 12.2454 17.8981 12.8617 17.8213C16.2516 17.3983 18.8749 14.5044 18.8749 10.9995C18.8746 7.20283 15.7971 4.12509 12.0004 4.125C8.4954 4.12505 5.60139 6.74948 5.17862 10.1396C5.10154 10.7559 4.53955 11.1934 3.92325 11.1167C3.30688 11.0398 2.86963 10.4777 2.9462 9.86133C3.50765 5.35896 7.34631 1.87505 12.0004 1.875Z" fill="currentColor"/>
<path d="M3.70727 16.1747L7.91781 11.2624C8.24038 10.8861 8.85505 11.158 8.79357 11.6498L8.49979 14.0001H10.9127C11.3399 14.0001 11.5703 14.5012 11.2923 14.8255L7.08177 19.7378C6.7592 20.1141 6.14453 19.8422 6.20601 19.3504L6.49979 17.0001H4.0869C3.65972 17.0001 3.42927 16.499 3.70727 16.1747Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,8 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(-7.14 -6.38)">
<path d="M13.7969 17.1665C14.465 17.1665 15.1067 17.2803 15.7045 17.4872L14.7247 20.9165H12.9636C11.8131 20.9167 10.8803 21.8493 10.8803 22.9998C10.8803 23.2963 10.9436 23.5778 11.0552 23.8332H8.79695C8.33682 23.833 7.95953 23.4587 8.00349 23.0007C8.30296 19.8813 10.2937 17.1666 13.7969 17.1665Z" fill="currentColor"/>
<path d="M25.4632 16.3333C25.7246 16.3333 25.9715 16.4558 26.1289 16.6645C26.2668 16.8473 26.3224 17.0777 26.286 17.3008L26.2648 17.3953L24.5981 23.2286C24.4959 23.5863 24.1686 23.8333 23.7965 23.8333H12.9632C12.5031 23.8331 12.1299 23.4601 12.1299 22.9999C12.1299 22.5398 12.5031 22.1668 12.9632 22.1666H15.6675L17.1616 16.9379L17.2105 16.8093C17.3465 16.5223 17.6376 16.3333 17.9632 16.3333H25.4632Z" fill="currentColor"/>
<path d="M14.2132 9.25C16.0541 9.25 17.5465 10.7424 17.5465 12.5833C17.5465 14.4243 16.0541 15.9167 14.2132 15.9167C12.3724 15.9165 10.8799 14.4242 10.8799 12.5833C10.8799 10.7425 12.3724 9.25013 14.2132 9.25Z" fill="currentColor"/>
<path d="M22.5474 8C22.7539 8.00029 22.9276 8.15533 22.951 8.36052C23.0941 9.62402 23.8103 10.3975 25.093 10.5114C25.3029 10.53 25.4635 10.7067 25.4632 10.9175C25.4628 11.128 25.3019 11.3037 25.0921 11.3219C23.8276 11.4314 23.0613 12.1977 22.9518 13.4622C22.9335 13.672 22.758 13.833 22.5474 13.8333C22.3367 13.8336 22.1609 13.6728 22.1421 13.4631C22.0281 12.1805 21.2546 11.4635 19.9912 11.3203C19.786 11.2971 19.6303 11.124 19.6299 10.9175C19.6297 10.7108 19.7851 10.5367 19.9904 10.513C21.2719 10.3651 21.9951 9.64128 22.1429 8.3597C22.1667 8.15453 22.3408 7.99979 22.5474 8Z" fill="currentColor"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -0,0 +1,6 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.8206 2.0275C15.7973 1.82217 15.6238 1.66696 15.4171 1.66675C15.2104 1.66654 15.0365 1.82139 15.0128 2.02667C14.865 3.30836 14.1416 4.03176 12.8599 4.17959C12.6547 4.20326 12.4998 4.37719 12.5 4.58383C12.5003 4.79047 12.6554 4.96408 12.8608 4.98733C14.1243 5.13046 14.8978 5.84689 15.0117 7.12955C15.0304 7.33946 15.2064 7.50032 15.4171 7.50008C15.6278 7.49984 15.8035 7.33859 15.8217 7.12863C15.9311 5.86411 16.6973 5.09787 17.9619 4.98841C18.1718 4.97023 18.3331 4.79461 18.3333 4.58387C18.3336 4.37313 18.1728 4.19715 17.9628 4.17851C16.6802 4.06457 15.9637 3.29101 15.8206 2.0275Z" fill="currentColor"/>
<path d="M7.29167 9.16659C8.9025 9.16659 10.2083 7.86075 10.2083 6.24992C10.2083 4.63909 8.9025 3.33325 7.29167 3.33325C5.68084 3.33325 4.375 4.63909 4.375 6.24992C4.375 7.86075 5.68084 9.16659 7.29167 9.16659Z" stroke="currentColor" stroke-width="1.5"/>
<path d="M1.66699 16.6667C1.66699 13.9053 3.90557 11.6667 6.66699 11.6667H7.08366" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.16634 16.6666L10.833 10.8333H18.333L16.6663 16.6666H9.16634ZM9.16634 16.6666H5.83301" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(2 2.333)">
<path d="M1.33333 2.33333C1.33333 1.78105 1.78105 1.33333 2.33333 1.33333C2.88562 1.33333 3.33333 1.78105 3.33333 2.33333C3.33333 2.88562 2.88562 3.33333 2.33333 3.33333C1.78105 3.33333 1.33333 2.88562 1.33333 2.33333ZM2.33333 0C1.04467 0 0 1.04467 0 2.33333C0 3.622 1.04467 4.66667 2.33333 4.66667C3.622 4.66667 4.66667 3.622 4.66667 2.33333C4.66667 1.04467 3.622 0 2.33333 0ZM6 3H11.3333V1.66667H6V3ZM8.66667 9C8.66667 8.44773 9.1144 8 9.66667 8C10.2189 8 10.6667 8.44773 10.6667 9C10.6667 9.55227 10.2189 10 9.66667 10C9.1144 10 8.66667 9.55227 8.66667 9ZM9.66667 6.66667C8.378 6.66667 7.33333 7.71133 7.33333 9C7.33333 10.2887 8.378 11.3333 9.66667 11.3333C10.9553 11.3333 12 10.2887 12 9C12 7.71133 10.9553 6.66667 9.66667 6.66667ZM0.666667 8.33333V9.66667H6V8.33333H0.666667Z" fill="currentColor"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 956 B

@@ -0,0 +1,8 @@
<svg width="14" height="14" viewBox="0 0 13.3333 13.3333" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.66667 10C7.03486 10 7.33333 10.2985 7.33333 10.6667V12.6667C7.33333 13.0349 7.03486 13.3333 6.66667 13.3333C6.29848 13.3333 6 13.0349 6 12.6667V10.6667C6 10.2985 6.29848 10 6.66667 10Z" fill="currentColor"/>
<path d="M1.95247 1.95247C2.21277 1.69219 2.63483 1.69229 2.89518 1.95247L7.13867 6.19531C7.19313 6.24981 7.23528 6.3119 7.26693 6.3776C7.28146 6.40773 7.29411 6.43892 7.30404 6.47135C7.33671 6.57832 7.34196 6.69172 7.31966 6.80078C7.30858 6.85501 7.2903 6.90651 7.26693 6.95508C7.23526 7.02098 7.19325 7.08337 7.13867 7.13802L2.89518 11.3809C2.63483 11.6409 2.21272 11.641 1.95247 11.3809C1.6924 11.1206 1.6925 10.6985 1.95247 10.4382L5.05794 7.33333H0.666667C0.298477 7.33333 0 7.03486 0 6.66667C0 6.29848 0.298477 6 0.666667 6H5.05794L1.95247 2.89518C1.69236 2.63487 1.69235 2.21277 1.95247 1.95247Z" fill="currentColor"/>
<path d="M9.02409 9.02344C9.28445 8.76325 9.7065 8.76314 9.9668 9.02344L11.3809 10.4375C11.641 10.6978 11.641 11.1199 11.3809 11.3802C11.1206 11.6405 10.6985 11.6404 10.4382 11.3802L9.02409 9.96615C8.76374 9.7058 8.76374 9.28379 9.02409 9.02344Z" fill="currentColor"/>
<path d="M12.6667 6C13.0349 6 13.3333 6.29848 13.3333 6.66667C13.3333 7.03486 13.0349 7.33333 12.6667 7.33333H10.6667C10.2985 7.33333 10 7.03486 10 6.66667C10 6.29848 10.2985 6 10.6667 6H12.6667Z" fill="currentColor"/>
<path d="M10.4382 1.95247C10.6985 1.69225 11.1205 1.69226 11.3809 1.95247C11.6411 2.21281 11.6411 2.63486 11.3809 2.89518L9.9668 4.30924C9.70647 4.56956 9.28444 4.56951 9.02409 4.30924C8.76381 4.04888 8.76376 3.62686 9.02409 3.36654L10.4382 1.95247Z" fill="currentColor"/>
<path d="M6.66667 0C7.03486 0 7.33333 0.298477 7.33333 0.666667V2.66667C7.33333 3.03486 7.03486 3.33333 6.66667 3.33333C6.29848 3.33333 6 3.03486 6 2.66667V0.666667C6 0.298477 6.29848 0 6.66667 0Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1,7 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(2.5904 1.333)">
<path d="M7.07562 3C7.07562 2.07953 6.32943 1.33333 5.40895 1.33333C4.48848 1.33333 3.74229 2.07953 3.74229 3C3.74229 3.92047 4.48848 4.66667 5.40895 4.66667C6.32943 4.66667 7.07562 3.92047 7.07562 3ZM8.40895 3C8.40895 4.65685 7.06581 6 5.40895 6C3.7521 6 2.40895 4.65685 2.40895 3C2.40895 1.34315 3.7521 0 5.40895 0C7.06581 0 8.40895 1.34315 8.40895 3Z" fill="currentColor"/>
<path d="M5.40961 6.66667C5.94859 6.66667 6.46414 6.7352 6.94997 6.86523C7.30564 6.96044 7.51718 7.32597 7.42198 7.68164C7.32674 8.03718 6.96114 8.24808 6.60557 8.153C6.23516 8.05385 5.83538 8 5.40961 8C3.44272 8.00003 1.96767 9.15509 1.35687 10.8066C1.31131 10.9299 1.33437 11.0372 1.41677 11.1354C1.50836 11.2446 1.67346 11.3333 1.87445 11.3333H4.07627C4.44446 11.3333 4.74294 11.6318 4.74294 12C4.74294 12.3682 4.44446 12.6667 4.07627 12.6667H1.87445C1.28978 12.6667 0.746862 12.4112 0.395283 11.9922C0.0346179 11.5622 -0.120709 10.9573 0.106221 10.3438C0.900152 8.19736 2.85517 6.6667 5.40961 6.66667Z" fill="currentColor"/>
<path d="M8.9376 8.1952C9.56613 7.56673 10.5851 7.56673 11.2136 8.1952C11.8422 8.82374 11.8421 9.84269 11.2136 10.4712L8.9376 12.7473C8.56255 13.1223 8.05388 13.3332 7.52354 13.3332H6.74229C6.3741 13.3332 6.07562 13.0347 6.07562 12.6666V11.886C6.07562 11.3555 6.28646 10.8463 6.66156 10.4712L8.9376 8.1952ZM7.40895 11.9999H7.52354C7.70025 11.9998 7.86989 11.9296 7.99489 11.8046L10.2709 9.52854C10.3787 9.42068 10.3788 9.24575 10.2709 9.13791C10.1631 9.03013 9.98814 9.03013 9.88031 9.13791L7.60426 11.414C7.47923 11.539 7.40895 11.7091 7.40895 11.886V11.9999Z" fill="currentColor"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,7 +1,7 @@
{
"prefix": "custom-vender",
"name": "Dify Custom Vender",
"total": 281,
"total": 318,
"version": "0.0.0-private",
"author": {
"name": "LangGenius, Inc.",

Some files were not shown because too many files have changed in this diff Show More