Compare commits

...
Author SHA1 Message Date
zxhlyh 685f611f86 Merge branch 'skill' into deploy/dev 2026-08-11 18:29:46 +08:00
zxhlyh 01552e03d6 fix(skills): align markdown placeholder with caret 2026-08-11 18:26:28 +08:00
fatelei e1bacf098a Merge branch 'skill' into deploy/dev 2026-08-11 17:56:57 +08:00
fatelei 37510f544f fix: fix agent pull skill name wrong 2026-08-11 17:56:17 +08:00
fatelei 33e5b84074 Merge branch 'skill' into deploy/dev 2026-08-11 15:24:09 +08:00
fatelei 953b264c07 fix: fix skill name not refresh 2026-08-11 15:19:57 +08:00
zxhlyh c440fa1beb fix(skills): reload latest draft after conflicts 2026-08-11 15:01:28 +08:00
fatelei 330288ba3d Merge branch 'skill' into deploy/dev 2026-08-11 14:30:20 +08:00
fatelei d95049cec8 fix: fix permission 2026-08-11 14:30:04 +08:00
zxhlyh 615628e4f0 fix(skills): avoid duplicate publish errors 2026-08-11 13:43:28 +08:00
fatelei 7d2a377374 Merge branch 'skill' into deploy/dev 2026-08-11 12:15:33 +08:00
fatelei 080a09b0f5 chore: add miss file 2026-08-11 12:15:11 +08:00
fatelei f3df36aec1 fix: fix scroll 2026-08-11 11:57:55 +08:00
fatelei a87e396b02 feat: add permission ui check 2026-08-11 11:57:50 +08:00
fatelei 6e4893f37f fix: fix scroll 2026-08-11 11:57:09 +08:00
fatelei c7ae155a07 feat: add permission ui check 2026-08-11 11:53:43 +08:00
fatelei a5d8af27d0 Merge branch 'skill' into deploy/dev 2026-08-11 11:32:39 +08:00
fatelei 179968303a fix: fix agent read wrong skill name and description 2026-08-11 11:31:49 +08:00
zxhlyh 202f650b7c fix(skills): localize published status 2026-08-11 11:09:32 +08:00
zxhlyh f23739272f fix(skills): align tag selector interactions 2026-08-11 11:06:39 +08:00
fatelei 0958b120e2 Merge branch 'skill' into deploy/dev 2026-08-11 10:55:54 +08:00
fatelei e6bf7d333c fix: fix agent delete not update skill ref 2026-08-11 10:55:23 +08:00
zxhlyh 98ff51d570 fix(skills): preserve markdown editor position 2026-08-11 10:49:17 +08:00
fatelei b1249dfc79 Merge branch 'skill' into deploy/dev 2026-08-11 07:25:15 +08:00
fatelei eef27dfc8e fix: fix publish 2026-08-11 07:24:54 +08:00
fatelei 85dd61de3a fix: fix ci failed 2026-08-11 07:20:09 +08:00
fatelei fee28ca253 fix: fix ci failed 2026-08-10 22:16:26 +08:00
fatelei 1cbea4a95f Merge branch 'skill' into deploy/dev 2026-08-10 21:23:25 +08:00
fatelei b1f11384b5 feat: version conflict dialog 2026-08-10 21:20:28 +08:00
autofix-ci[bot]andGitHub 890c062c01 [autofix.ci] apply automated fixes 2026-08-10 09:44:14 +00:00
fatelei ed4dd78210 Merge branch 'skill' into deploy/dev 2026-08-10 17:40:56 +08:00
fatelei 8bafbf7238 fix: fix list tag and duplicate skill 2026-08-10 17:40:02 +08:00
zxhlyh f6522e6a34 fix(skills): hide empty builder draft marker 2026-08-10 17:17:40 +08:00
zxhlyh a5336da117 fix(agent): enable skill tag filtering 2026-08-10 17:05:48 +08:00
zxhlyh a0d2d0302e fix(skills): render markdown file previews 2026-08-10 16:43:29 +08:00
zxhlyh f5ea9bbc02 fix(skills): commit custom metadata on blur 2026-08-10 16:11:34 +08:00
zxhlyh 3d7c403580 fix(skills): discard stale uploads after restart 2026-08-10 15:28:07 +08:00
zxhlyh c3554ad525 fix(skills): preserve attachments when retrying 2026-08-10 14:29:58 +08:00
fatelei f805038d16 fix: fix conflict 2026-08-10 14:01:55 +08:00
fatelei 53542cf84e fix: fix draft skill time display 2026-08-10 13:47:13 +08:00
zxhlyh 6609ac0b1e fix(skills): guard deletion during reference refresh 2026-08-10 13:43:20 +08:00
autofix-ci[bot]andGitHub 963410e562 [autofix.ci] apply automated fixes 2026-08-08 03:13:10 +00:00
fatelei 5e0d54d4ce fix: fix test issue 2026-08-08 11:08:50 +08:00
fatelei 94cf4c2957 fix: fix frontend test 2026-08-08 10:36:43 +08:00
autofix-ci[bot]andGitHub aa43aee224 [autofix.ci] apply automated fixes 2026-08-08 01:11:20 +00:00
fatelei 9d1a3c5a00 Merge remote-tracking branch 'upstream/main' into skill 2026-08-08 09:06:56 +08:00
fatelei ebe0a269ea fix: fix backend test 2026-08-08 09:06:36 +08:00
fatelei edff7a4471 Merge remote-tracking branch 'upstream/main' into skill 2026-08-07 22:21:52 +08:00
autofix-ci[bot]andGitHub 9009960b35 [autofix.ci] apply automated fixes 2026-08-07 12:57:53 +00:00
fatelei 9969c9f845 fix: fix conflict 2026-08-07 20:53:07 +08:00
fatelei 2dace438a3 Merge branch 'skill' into deploy/dev 2026-08-07 19:37:09 +08:00
fatelei 19a8683bca fix: fix skill name is untitled-skill and optimize ai logic 2026-08-07 19:35:44 +08:00
fatelei 154926d4ac Merge branch 'skill' into deploy/dev 2026-08-07 17:12:46 +08:00
fatelei e740e6c423 fix: fix llm context 2026-08-07 17:10:36 +08:00
fatelei 703289fb0b Merge branch 'skill' into deploy/dev 2026-08-07 15:55:06 +08:00
fatelei 89219c479c fix: fix version restore 2026-08-07 15:54:24 +08:00
zxhlyh 3bbbdd478f Merge branch 'skill' into deploy/dev 2026-08-07 15:42:02 +08:00
zxhlyh 8bf624401a fix(skill): prevent builder send icon distortion 2026-08-07 15:37:24 +08:00
fatelei 9d5f87f0b4 Merge branch 'skill' into deploy/dev 2026-08-07 15:29:18 +08:00
fatelei f8ca584a81 fix: fix some ui issue 2026-08-07 15:24:15 +08:00
zxhlyh 520999691f fix(skills): require display name to confirm deletion 2026-08-07 15:09:43 +08:00
fatelei 4b1f4f3223 Merge branch 'skill' into deploy/dev 2026-08-07 14:29:20 +08:00
fatelei 5cc0f16bae fix: fix publish date 2026-08-07 14:27:15 +08:00
fatelei e9caaa43a3 Merge branch 'skill' into deploy/dev 2026-08-07 12:56:27 +08:00
fatelei 793300066b feat: add image upload limit 2026-08-07 12:53:19 +08:00
fatelei 8b456fb4df Merge branch 'skill' into deploy/dev 2026-08-07 11:49:02 +08:00
fatelei 6c052bb9b5 fix: fix some issue 2026-08-07 11:47:32 +08:00
zxhlyh bc3ef4de71 fix(skills): align publish bar expansion 2026-08-07 11:22:34 +08:00
fatelei a14b9be0be Merge branch 'skill' into deploy/dev 2026-08-06 17:38:45 +08:00
fatelei f39e55caf6 chore: increase limit 2026-08-06 17:37:29 +08:00
fatelei d5964397b2 Merge branch 'skill' into deploy/dev 2026-08-06 16:27:12 +08:00
fatelei 0065d0e0f1 chore: remove single file limit 2026-08-06 16:26:27 +08:00
zxhlyh 5053828fad feat(skills): align file editor previews 2026-08-06 14:25:57 +08:00
fatelei fd38ac854c Merge branch 'skill' into deploy/dev 2026-08-05 17:41:40 +08:00
fatelei 3b033f9eb5 chore: upload zip skill use UPLOAD_SKILL_FILE_SIZE_LIMIT 2026-08-05 17:41:10 +08:00
zxhlyh 0451a321cb fix(skills): refresh list after version changes 2026-08-05 17:00:44 +08:00
zxhlyh cfe627e894 fix(skills): refresh tag cache after creation 2026-08-05 15:33:44 +08:00
fatelei 47788f1617 Merge branch 'skill' into deploy/dev 2026-08-04 19:02:57 +08:00
fatelei dcdf6715a3 feat: add rbac 2026-08-04 19:02:34 +08:00
zxhlyh ea94659e7c feat(skills): add detail sidebar actions 2026-08-04 18:03:50 +08:00
zxhlyh b3e0f6c5b7 fix(skills): pluralize upload failure copy 2026-08-04 16:54:10 +08:00
fatelei 4bdc28959d Merge branch 'skill' into deploy/dev 2026-08-04 16:34:01 +08:00
fatelei 3482166f78 fix: fix skill builder upload icon 2026-08-04 16:31:56 +08:00
fatelei ff1c935761 Merge branch 'skill' into deploy/dev 2026-08-04 16:28:29 +08:00
fatelei 1a9c03c0af fix: fix some ui issue 2026-08-04 16:28:11 +08:00
zxhlyh a43f2ccc0a fix(skills): correct dark theme surfaces 2026-08-04 15:50:31 +08:00
zxhlyh 4e00f708f7 fix(agent): allow removing embedded skills in build drafts 2026-08-04 14:54:37 +08:00
fatelei c9a0aff2f9 Merge branch 'skill' into deploy/dev 2026-08-04 14:43:48 +08:00
fatelei 5afbf25a59 fix: fix ui issue 2026-08-04 14:43:25 +08:00
zxhlyh b29abeafb5 feat(skills): align tag management behavior 2026-08-04 14:26:30 +08:00
fatelei 3a8676371c fix: fix conflict 2026-08-04 13:44:24 +08:00
fatelei 5a13489fdd fix: fix some bugs 2026-08-04 13:39:32 +08:00
zxhlyh 2f5fee324a fix(skills): refine empty state layout 2026-08-04 13:06:35 +08:00
fatelei c9eb56c4cb fix: fix some ui issue 2026-08-04 11:45:25 +08:00
fatelei 6aa5804503 Merge branch 'skill' into deploy/dev 2026-08-04 11:27:39 +08:00
fatelei bbba892849 fix: fix some ui issue 2026-08-04 11:27:24 +08:00
zxhlyh 62d95441d1 feat(skills): add file upload review workflow 2026-08-03 17:41:56 +08:00
zxhlyh 09479cafa4 fix(skills): refine detail loading skeleton 2026-08-03 17:41:56 +08:00
fatelei 26a5465c5b fix: fix ci failed 2026-08-03 17:09:43 +08:00
fatelei 64558c418c fix: fix ci failed 2026-08-03 16:56:55 +08:00
autofix-ci[bot]andGitHub 785622f7f3 [autofix.ci] apply automated fixes 2026-08-03 08:16:10 +00:00
zxhlyh 43440c6edb fix(skills): link detail header navigation 2026-08-03 16:08:51 +08:00
fatelei 7a6c9934d7 Merge branch 'skill' into deploy/dev 2026-08-03 16:07:23 +08:00
fatelei 21a4e6fbcc feat: add file check api 2026-08-03 16:06:11 +08:00
zxhlyh a4621041b6 feat(agent-v2): split skill add sources 2026-08-03 15:20:04 +08:00
autofix-ci[bot]andGitHub 7c7d30fe1f [autofix.ci] apply automated fixes 2026-08-03 06:59:05 +00:00
fatelei 4ce6b1f620 Merge branch 'skill' into deploy/dev 2026-08-03 14:56:17 +08:00
fatelei f0fb69f4c7 fix: fix some ui issue 2026-08-03 14:55:04 +08:00
zxhlyh 3ac6e1b115 feat(skills): refine versions and account navigation 2026-08-03 14:39:52 +08:00
zxhlyh 8767767794 fix(skills): match sidebar resize states 2026-08-03 11:56:58 +08:00
zxhlyh a7ca830fad feat(skills): refine editor and builder parity 2026-08-03 10:49:26 +08:00
fatelei acf56bf154 Merge branch 'skill' into deploy/dev 2026-08-03 09:54:13 +08:00
fatelei 63615b2fab fix: fix delete dialog 2026-08-03 09:52:24 +08:00
fatelei 7196a370a7 chore: increase test coverage 2026-08-02 22:23:32 +08:00
fatelei f1f9e34683 chore: increase test coverage 2026-08-02 22:09:08 +08:00
fatelei fa7a349a2a chore: increase test coverage 2026-08-02 21:43:24 +08:00
fatelei ac902eb2e9 fix: fix test failed 2026-08-02 20:57:47 +08:00
autofix-ci[bot]andGitHub bbef12212f [autofix.ci] apply automated fixes 2026-08-02 12:08:38 +00:00
fatelei b252c0834d fix: fix conflict 2026-08-02 20:04:29 +08:00
zxhlyhandfatelei 8b908cbc43 fix(skills): complete metadata editor parity 2026-08-02 17:54:51 +08:00
zxhlyhandfatelei 0aea96233d feat(skills): restore editor design parity 2026-08-02 17:51:29 +08:00
fatelei 977b2c4c93 fix: fix delete skill ref 2026-08-02 17:46:30 +08:00
fatelei b441e3e838 feat: support agent skill 2026-08-02 17:28:21 +08:00
b91a3b8c27 fix(api): bound JinaReader and adaptive-crawl HTTP timeouts (#39859) (#39860)
Co-authored-by: Harsh Kashyap <[email protected]>
2026-08-02 17:21:57 +08:00
Taranum WasufateleiTaranum01autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
49695a7575 fix(api): make SSRF-blocked error message actionable (#39867)
Co-authored-by: Taranum01 <[email protected]>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-02 17:21:57 +08:00
654904fce0 fix(matrixone): apply score threshold to vector search (#39784)
Co-authored-by: caosurong <[email protected]>
2026-08-02 17:21:57 +08:00
kenwoodjwandfatelei 2f1276864d fix(api): prevent cookies from shadowing admin API keys (#39872)
Signed-off-by: kenwoodjw <[email protected]>
2026-08-02 17:21:57 +08:00
yyhfateleiautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
5ff641a49c refactor(web): align Base UI state consumers (#39864)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-02 17:21:57 +08:00
75bb174719 fix(api): bound billing service HTTP timeouts (#39874) (#39875)
Co-authored-by: Harsh Kashyap <[email protected]>
2026-08-02 17:19:28 +08:00
fe6375e39c chore(api): fix typos in config description and comment (#39870)
Co-authored-by: Jony <[email protected]>
2026-08-02 17:19:28 +08:00
WH-2099andfatelei 9939df18f5 fix(api): scope segment batch deletes (#38217) 2026-08-02 17:19:28 +08:00
yyhandfatelei cac9be9d98 fix(web): align secondary sidebar help trigger (#39862) 2026-08-02 17:19:27 +08:00
Chesterandfatelei e3350f7ada fix(auth): add bounded timeout to WaterCrawl credential validation (#39824) 2026-08-02 17:19:27 +08:00
Chesterandfatelei 7526ab3163 fix(rag): add bounded timeout to Notion extractor requests (#39833) 2026-08-02 17:19:27 +08:00
Chesterandfatelei 893fe3bb01 fix(rag): add bounded timeout to Firecrawl extractor requests (#39831) 2026-08-02 17:19:27 +08:00
Asuka Minatoandfatelei 851b01578e test: use SQLite sessions in services workflow (#39087) 2026-08-02 17:19:27 +08:00
Asuka Minatoandfatelei 765f1172d5 test: use SQLite sessions in core workflow (#39086) 2026-08-02 17:19:27 +08:00
Asuka Minatoandfatelei 6137c60651 test: use SQLite sessions in core callback_handler (#39081) 2026-08-02 17:19:27 +08:00
Asuka Minatoandfatelei 975b1a6303 test: use SQLite sessions in core repositories (#39078) 2026-08-02 17:19:27 +08:00
Asuka Minatoandfatelei 045b39c6d6 test: use SQLite sessions in core llm_generator (#39070) 2026-08-02 17:19:27 +08:00
Asuka Minatoandfatelei 385f2196a9 test: use SQLite sessions in core tools (#39068) 2026-08-02 17:19:27 +08:00
Asuka Minatofateleiautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
9f73e16373 test: use SQLite sessions in core ops (#39062)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-02 17:19:27 +08:00
Asuka Minatoandfatelei cd48673c20 test: use SQLite sessions in core rag (#39055) 2026-08-02 17:19:27 +08:00
Asuka Minatoandfatelei 33a8d28d0d test: use SQLite sessions in services agent (#39054) 2026-08-02 17:19:26 +08:00
yyhandfatelei 38a6567bad refactor(ui): simplify scroll area API (#39857) 2026-08-02 17:19:26 +08:00
yyhandfatelei 1434758d42 fix(web): hide prompt log action in agent v2 logs (#39858) 2026-08-02 16:41:18 +08:00
fatelei 2941960545 Merge branch 'skill' into deploy/dev 2026-07-31 21:06:09 +08:00
zxhlyhandfatelei c50ec44bd8 feat(skills): restore editor design parity 2026-07-31 21:02:02 +08:00
非法操作andfatelei 3a6b69bfe7 fix(web): skip auth validation for local file datasource (#39856) 2026-07-31 20:56:38 +08:00
Joelandfatelei 78d43c6712 feat: prioritize configured tools in the agent prompt picker (#39850) 2026-07-31 20:56:38 +08:00
autofix-ci[bot]andGitHub fcf05b4b63 [autofix.ci] apply automated fixes 2026-07-31 12:52:10 +00:00
fatelei 58abd13450 fix: fix some bugs 2026-07-31 20:47:57 +08:00
fatelei 38b1a406bb fix: fix publish update button ui issue 2026-07-31 20:47:56 +08:00
zxhlyh dd6a28ca1f Merge branch 'skill' into deploy/dev 2026-07-31 17:50:41 +08:00
zxhlyh f2b0aaeb8f fix(skills): complete metadata editor parity 2026-07-31 17:49:28 +08:00
zxhlyh 977c6ca61f fix(skills): match metadata entry design 2026-07-31 17:49:28 +08:00
zxhlyh a8879e7360 fix(skills): match add metadata button design 2026-07-31 17:49:28 +08:00
fatelei 80365aec68 fix: fix conflict 2026-07-31 17:46:43 +08:00
fatelei b1b01630e5 fix: fix some ui issue 2026-07-31 17:44:16 +08:00
zxhlyh ec2e511d0d Merge branch 'codex/skills-editor-design-parity' into skill 2026-07-31 17:24:13 +08:00
zxhlyh 003c7b4fa9 feat(skills): restore editor design parity 2026-07-31 17:15:51 +08:00
zxhlyh 5caa2eea03 feat(skills): refine publish workflow UI 2026-07-31 17:05:33 +08:00
fatelei d326d38a0a fix: fix failed test 2026-07-31 15:58:46 +08:00
KVOJJJinandfatelei 4275c24dc5 fix(workflow): remap snippet references (#39843) 2026-07-31 15:58:46 +08:00
Bond Zhuandfatelei 542544d8eb fix(api): silence opentelemetry.context error reports to Sentry on cloud (#39841) 2026-07-31 15:58:45 +08:00
iridescentWenandfatelei f835f0c59c docs: fix typos in api comments and prompt example (#39838) 2026-07-31 15:58:45 +08:00
Xin Zhangfateleiautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
d6fd31c756 fix: refresh mcp server code by app id (#39828)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-07-31 15:58:45 +08:00
zxhlyh 0ef7bf929e fix(skills): refine detail interactions and rename flow 2026-07-31 15:53:11 +08:00
zxhlyh 64cb61f902 merge: skills detail layout fix 2026-07-31 15:02:32 +08:00
zxhlyh 779f57902e fix(skills): hide main navigation on detail pages 2026-07-31 15:02:22 +08:00
zxhlyh 4bfd7fd17c feat(skills): match file drag and upload interactions 2026-07-31 15:00:10 +08:00
林玮 (Jade Lin) ce775fd3f9 feat(plugin): isolate from local packages
(cherry picked from commit e6b371c9daea6892cf3068a3966363be6bcf61f8)
2026-07-31 14:25:48 +08:00
JzoNg a0a8ed7594 Merge branch 'fix/snippet-variable-remap' into deploy/dev 2026-07-31 13:57:47 +08:00
JzoNg 111260fa37 fix(workflow): remap snippet references 2026-07-31 13:55:53 +08:00
zxhlyh f3853f48df refactor(skills): split skill detail page modules 2026-07-31 12:16:01 +08:00
zxhlyh 7fde7b7ca4 fix(skills): align detail page interactions 2026-07-31 11:53:47 +08:00
Escape0707andfatelei 68c59ad047 test: exercise credential visibility with SQLite (#39827) 2026-07-31 11:37:04 +08:00
Parman Mohammadalizadehandfatelei 7e3514adae chore(api): consolidate chained .where() calls into single calls (#39819) 2026-07-31 11:37:04 +08:00
起岚andfatelei ffbde43b2c docs: remove trailing whitespace in docker/README.md (#39808) 2026-07-31 11:37:04 +08:00
fatelei 64b7451003 Merge branch 'skill' into deploy/dev 2026-07-31 11:18:33 +08:00
fatelei 3ec3242a66 fix: fix skill tag 2026-07-31 11:18:14 +08:00
fatelei 238887036b fix: fix conflict 2026-07-31 09:11:04 +08:00
autofix-ci[bot]andGitHub 4ec9fb0a58 [autofix.ci] apply automated fixes 2026-07-30 15:01:47 +00:00
fatelei 51b668a2a6 feat: update publish button logic 2026-07-30 22:57:22 +08:00
zxhlyhandfatelei ce38c21253 fix(skills): align file tree interactions 2026-07-30 22:57:22 +08:00
zxhlyhandfatelei f5ba4a8e51 fix: block skill file editing before load 2026-07-30 22:57:22 +08:00
fatelei 52100bbb29 fix: fix create skill not reload 2026-07-30 22:57:22 +08:00
fatelei 65e6d72254 fix: fix test failed 2026-07-30 22:57:22 +08:00
fatelei 48b5dca16a fix: fix style check failed 2026-07-30 22:57:22 +08:00
zxhlyhandfatelei fbe4e586f6 fix(web): support library skills in agent prompts 2026-07-30 22:57:22 +08:00
zxhlyhandfatelei ca99dda275 fix(web): isolate refreshed main navigation icons 2026-07-30 22:57:22 +08:00
zxhlyhandfatelei b28d45bb2d fix(web): align agent skills interactions with design 2026-07-30 22:57:22 +08:00
autofix-ci[bot]andfatelei 404f79f370 [autofix.ci] apply automated fixes 2026-07-30 22:57:22 +08:00
fatelei 5a002d8f6a fix: fix style check failed 2026-07-30 22:57:21 +08:00
fatelei 606d1d7926 chore: increase test coverage and fix skill duplicate 2026-07-30 22:57:21 +08:00
fatelei 277c1ddf9c fix: fix test failed 2026-07-30 22:57:21 +08:00
autofix-ci[bot]andfatelei d6fc092795 [autofix.ci] apply automated fixes 2026-07-30 22:57:21 +08:00
fatelei 3a422d4dde fix: fix test failed 2026-07-30 22:57:21 +08:00
zxhlyhandfatelei eb8cda6355 fix(web): block skill builder sends during upload 2026-07-30 22:57:21 +08:00
fatelei c81c774b49 fix: fix file conflict 2026-07-30 22:57:21 +08:00
fatelei b6431e82cd feat: agent can op file and fix version check 2026-07-30 22:57:21 +08:00
zxhlyhandfatelei 9d3b0ddf58 fix(web): preserve IME composition in skill builder 2026-07-30 22:57:21 +08:00
zxhlyhandfatelei a6948b57e6 fix(web): protect skill bindings in version view 2026-07-30 22:57:21 +08:00
zxhlyhandfatelei 4cc89a054b fix(web): paginate workspace skills list 2026-07-30 22:57:21 +08:00
autofix-ci[bot]andfatelei 03b18bd4e3 [autofix.ci] apply automated fixes 2026-07-30 22:57:21 +08:00
fatelei 2907adb12e fix: fix db version 2026-07-30 22:57:20 +08:00
fatelei d08ca0e069 fix: fix model not change 2026-07-30 22:57:20 +08:00
zxhlyhandfatelei ca22915bf1 fix(web): prevent duplicate skill autosaves 2026-07-30 22:57:20 +08:00
fatelei a4430fd373 fix: fix model not update 2026-07-30 22:57:20 +08:00
fatelei 90e802d5e4 fix: fix failed test 2026-07-30 22:57:20 +08:00
fatelei 6113893a53 fix: fix skill not in agent 2026-07-30 22:57:20 +08:00
fatelei 5aea0b1489 fix: fix frontend issue and let draft check relax 2026-07-30 22:57:20 +08:00
fatelei 6d59f60f88 fix: fix ci failed 2026-07-30 22:57:20 +08:00
fatelei a5a08455b2 fix: fix ci failed 2026-07-30 22:57:20 +08:00
fatelei bc296aa440 fix: fix scroll and live mode can not enter 2026-07-30 22:57:20 +08:00
fatelei d01d63275e fix: fix some bugs 2026-07-30 22:57:19 +08:00
fatelei 00e67c1db1 chore: adjust skill entrance 2026-07-30 22:57:19 +08:00
fatelei 946603c513 fix: fix delete skill ref 2026-07-30 22:57:19 +08:00
fatelei 6414d0a020 feat: support agent skill 2026-07-30 22:57:18 +08:00
fatelei 22cab8f4bd fix: fix conflict 2026-07-30 13:12:21 +08:00
fatelei f08d5343d0 fix: fix create skill not reload 2026-07-30 13:00:08 +08:00
CodingOnStar 8c0fa26938 Merge branch 'feat/marketplace-homepage-redesign' into deploy/dev 2026-07-30 12:10:24 +08:00
fatelei e07c1be932 fix: fix test failed 2026-07-30 11:52:08 +08:00
CodingOnStar 1e4ecbfe0a chore: remove detail dialog readme 2026-07-30 11:49:04 +08:00
CodingOnStar fd242a37de feat: open marketplace details in dialog 2026-07-30 11:45:50 +08:00
fatelei 2d8440309d fix: fix style check failed 2026-07-30 11:38:27 +08:00
zxhlyhandfatelei 4bdc2a3d71 fix(web): support library skills in agent prompts 2026-07-30 11:38:27 +08:00
zxhlyhandfatelei 668b99fab7 fix(web): isolate refreshed main navigation icons 2026-07-30 11:38:27 +08:00
zxhlyhandfatelei 14942e7f1c fix(web): align agent skills interactions with design 2026-07-30 11:38:27 +08:00
autofix-ci[bot]andfatelei c1ace7800b [autofix.ci] apply automated fixes 2026-07-30 11:38:27 +08:00
fatelei 9a602ab6d6 fix: fix style check failed 2026-07-30 11:38:27 +08:00
fatelei 36eddb6a6b chore: increase test coverage and fix skill duplicate 2026-07-30 11:38:27 +08:00
fatelei 340cf3a5e8 fix: fix test failed 2026-07-30 11:38:27 +08:00
autofix-ci[bot]andfatelei 1e20db0758 [autofix.ci] apply automated fixes 2026-07-30 11:38:26 +08:00
fatelei 2c8f39c217 fix: fix test failed 2026-07-30 11:38:26 +08:00
zxhlyhandfatelei a87d0bed3a fix(web): block skill builder sends during upload 2026-07-30 11:38:26 +08:00
fatelei c01167b1f4 fix: fix file conflict 2026-07-30 11:38:26 +08:00
fatelei 23b06d5710 feat: agent can op file and fix version check 2026-07-30 11:38:26 +08:00
zxhlyhandfatelei 4731f0fd08 fix(web): preserve IME composition in skill builder 2026-07-30 11:38:26 +08:00
zxhlyhandfatelei cf791ff9f4 fix(web): protect skill bindings in version view 2026-07-30 11:38:26 +08:00
zxhlyhandfatelei 2f2889f957 fix(web): paginate workspace skills list 2026-07-30 11:38:26 +08:00
autofix-ci[bot]andfatelei 22ba24b6a4 [autofix.ci] apply automated fixes 2026-07-30 11:38:26 +08:00
fatelei bbfbe3f94c fix: fix db version 2026-07-30 11:38:26 +08:00
fatelei a8a081486b fix: fix model not change 2026-07-30 11:38:26 +08:00
zxhlyhandfatelei f966ada97b fix(web): prevent duplicate skill autosaves 2026-07-30 11:38:26 +08:00
fatelei ba865c7c9c fix: fix model not update 2026-07-30 11:38:26 +08:00
fatelei f72b30f5a2 fix: fix failed test 2026-07-30 11:38:26 +08:00
fatelei 30888bfcf9 fix: fix skill not in agent 2026-07-30 11:38:25 +08:00
fatelei 067e95435f fix: fix frontend issue and let draft check relax 2026-07-30 11:38:25 +08:00
fatelei c0c0b22415 fix: fix ci failed 2026-07-30 11:38:25 +08:00
fatelei b95dd9594e fix: fix ci failed 2026-07-30 11:38:25 +08:00
fatelei fda3de1ec0 fix: fix scroll and live mode can not enter 2026-07-30 11:38:25 +08:00
fatelei 1c1fd3517c fix: fix some bugs 2026-07-30 11:38:25 +08:00
fatelei 7f162a924c chore: adjust skill entrance 2026-07-30 11:38:25 +08:00
fatelei d3fcb6e045 fix: fix delete skill ref 2026-07-30 11:38:25 +08:00
fatelei a3d17b5e86 feat: support agent skill 2026-07-30 11:38:25 +08:00
zxhlyh e1595f4b55 Merge branch 'skill' into deploy/dev
# Conflicts:
#	api/services/skill_management_service.py
#	api/services/tag_service.py
#	api/tests/unit_tests/controllers/console/workspace/test_skills.py
#	api/tests/unit_tests/services/test_skill_management_service.py
#	packages/contracts/generated/api/console/tags/types.gen.ts
#	packages/contracts/generated/api/console/tags/zod.gen.ts
#	packages/contracts/generated/api/console/workspaces/types.gen.ts
#	packages/contracts/generated/api/console/workspaces/zod.gen.ts
#	web/app/components/header/account-setting/model-provider-page/model-parameter-modal/__tests__/index.spec.tsx
#	web/app/components/header/account-setting/model-provider-page/model-parameter-modal/index.tsx
#	web/app/components/header/account-setting/model-provider-page/model-selector/index.tsx
#	web/app/components/header/account-setting/model-provider-page/model-selector/popup.tsx
#	web/app/components/main-nav/__tests__/index.spec.tsx
#	web/app/components/main-nav/routes.ts
#	web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/__tests__/index.spec.tsx
#	web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/index.tsx
#	web/features/skills/__tests__/detail-page.spec.tsx
#	web/features/skills/detail-page.tsx
2026-07-29 19:31:15 +08:00
zxhlyh ae20b3b189 fix(web): support library skills in agent prompts 2026-07-29 19:21:46 +08:00
zxhlyh cd05b1d3e3 fix(web): isolate refreshed main navigation icons 2026-07-29 18:16:02 +08:00
zxhlyh e61d4ff85d fix(web): align agent skills interactions with design 2026-07-29 17:16:53 +08:00
autofix-ci[bot]andGitHub 8b72e93067 [autofix.ci] apply automated fixes 2026-07-29 09:07:07 +00:00
fatelei ceea0b0222 fix: fix style check failed 2026-07-29 17:01:10 +08:00
fatelei 137354df91 chore: increase test coverage and fix skill duplicate 2026-07-29 15:39:28 +08:00
fatelei 039ca228fa fix: fix test failed 2026-07-29 15:39:28 +08:00
autofix-ci[bot]andfatelei 44f7a814c3 [autofix.ci] apply automated fixes 2026-07-29 15:39:28 +08:00
fatelei d5e702894a fix: fix test failed 2026-07-29 15:39:27 +08:00
zxhlyhandfatelei 33f8999c60 fix(web): block skill builder sends during upload 2026-07-29 15:39:27 +08:00
fatelei f5f3c59d33 fix: fix file conflict 2026-07-29 15:39:27 +08:00
fatelei a046f66b76 feat: agent can op file and fix version check 2026-07-29 15:39:27 +08:00
zxhlyhandfatelei 05259faaf9 fix(web): preserve IME composition in skill builder 2026-07-29 15:39:27 +08:00
zxhlyhandfatelei 6d815cb16c fix(web): protect skill bindings in version view 2026-07-29 15:39:27 +08:00
zxhlyhandfatelei 7aa44930a7 fix(web): paginate workspace skills list 2026-07-29 15:39:27 +08:00
autofix-ci[bot]andfatelei 4763a02d55 [autofix.ci] apply automated fixes 2026-07-29 15:39:27 +08:00
fatelei d82c30da65 fix: fix db version 2026-07-29 15:39:26 +08:00
fatelei fff55e65ea fix: fix model not change 2026-07-29 15:39:26 +08:00
zxhlyhandfatelei 53fe349fa8 fix(web): prevent duplicate skill autosaves 2026-07-29 15:39:26 +08:00
fatelei a05db83783 fix: fix model not update 2026-07-29 15:39:26 +08:00
fatelei ac5cde8d1e fix: fix failed test 2026-07-29 15:39:26 +08:00
fatelei 2c40f2d3cd fix: fix skill not in agent 2026-07-29 15:39:26 +08:00
fatelei a006cd91a2 fix: fix frontend issue and let draft check relax 2026-07-29 15:39:26 +08:00
fatelei 136eef8c70 fix: fix ci failed 2026-07-29 15:39:26 +08:00
fatelei b510cdcc48 fix: fix ci failed 2026-07-29 15:39:25 +08:00
fatelei 64745080f7 fix: fix scroll and live mode can not enter 2026-07-29 15:39:25 +08:00
fatelei b991a91b80 fix: fix some bugs 2026-07-29 15:39:25 +08:00
fatelei 5a042854bd chore: adjust skill entrance 2026-07-29 15:39:25 +08:00
fatelei 61cec2e1be fix: fix delete skill ref 2026-07-29 15:39:25 +08:00
fatelei 3589fb3efc feat: support agent skill 2026-07-29 15:39:24 +08:00
fatelei 59295c05bf fix: fix conflict 2026-07-29 15:25:26 +08:00
fatelei 3fd75f6bd5 chore: increase test coverage and fix skill duplicate 2026-07-29 14:49:54 +08:00
fatelei da94a3e334 fix: fix test failed 2026-07-29 12:05:21 +08:00
autofix-ci[bot]andGitHub 9c96d3d417 [autofix.ci] apply automated fixes 2026-07-29 03:40:27 +00:00
fatelei f7dc60bf90 fix: fix test failed 2026-07-29 11:35:33 +08:00
zxhlyh 58f17aa5a8 fix(web): block skill builder sends during upload 2026-07-29 11:15:04 +08:00
fatelei 95f76e12ef fix: fix file conflict 2026-07-29 10:58:21 +08:00
fatelei 3933334a4d feat: agent can op file and fix version check 2026-07-29 10:58:21 +08:00
zxhlyhandfatelei 59e68de46b fix(web): preserve IME composition in skill builder 2026-07-29 10:58:21 +08:00
zxhlyhandfatelei e1e20c9981 fix(web): protect skill bindings in version view 2026-07-29 10:58:21 +08:00
zxhlyhandfatelei 1495ea9a8f fix(web): paginate workspace skills list 2026-07-29 10:58:21 +08:00
autofix-ci[bot]andfatelei 1af973bd79 [autofix.ci] apply automated fixes 2026-07-29 10:58:21 +08:00
fatelei 15ac8b8954 fix: fix db version 2026-07-29 10:58:21 +08:00
fatelei 1cd7699660 fix: fix model not change 2026-07-29 10:58:21 +08:00
zxhlyhandfatelei d264aee057 fix(web): prevent duplicate skill autosaves 2026-07-29 10:58:20 +08:00
fatelei 8f24e8af40 fix: fix model not update 2026-07-29 10:58:20 +08:00
fatelei f92e7339f4 fix: fix failed test 2026-07-29 10:58:20 +08:00
fatelei 7908dd69be fix: fix skill not in agent 2026-07-29 10:58:20 +08:00
fatelei a5ff29eea5 fix: fix frontend issue and let draft check relax 2026-07-29 10:58:20 +08:00
fatelei 2fe4790722 fix: fix ci failed 2026-07-29 10:58:20 +08:00
fatelei 35ddd2b6f4 fix: fix ci failed 2026-07-29 10:58:20 +08:00
fatelei 94dfaae3d0 fix: fix scroll and live mode can not enter 2026-07-29 10:58:20 +08:00
fatelei 78154faa62 fix: fix some bugs 2026-07-29 10:58:20 +08:00
fatelei 1ff2a7c05e chore: adjust skill entrance 2026-07-29 10:58:19 +08:00
fatelei bb22386e30 fix: fix delete skill ref 2026-07-29 10:58:19 +08:00
fatelei 18fa5392fe feat: support agent skill 2026-07-29 10:58:19 +08:00
fatelei 33af2faf83 Merge branch 'skill' into deploy/dev 2026-07-29 10:36:54 +08:00
fatelei e9e2b6e2a9 fix: fix file conflict 2026-07-29 10:33:18 +08:00
CodingOnStar 61c43c95e5 chore: include marketplace app styles 2026-07-28 20:14:17 +08:00
CodingOnStar 9316880ecb fix: add hybrid SSR for embedded marketplace 2026-07-28 19:16:17 +08:00
CodingOnStar e39a7b10de fix: isolate embedded marketplace client 2026-07-28 19:10:05 +08:00
fatelei 2bb01984ed fix: fix conflict 2026-07-28 19:03:11 +08:00
fatelei 77e41a37ee feat: agent can op file and fix version check 2026-07-28 18:36:06 +08:00
zxhlyh 79229b7ede fix(web): preserve IME composition in skill builder 2026-07-28 17:46:12 +08:00
CodingOnStar fd20d4f536 fix: simplify marketplace home tabs 2026-07-28 16:57:20 +08:00
CodingOnStar 796f32baa2 fix: localize marketplace homepage copy 2026-07-28 16:52:40 +08:00
CodingOnStar a605790a84 feat: add themed marketplace logo 2026-07-28 16:40:33 +08:00
zxhlyh 36ae648a63 fix(web): protect skill bindings in version view 2026-07-28 16:21:43 +08:00
CodingOnStar 59bf9019c9 feat: share marketplace home shell with templates 2026-07-28 16:05:30 +08:00
zxhlyh c409d531da fix(web): paginate workspace skills list 2026-07-28 15:49:44 +08:00
CodingOnStar 5fef0cf07c fix: stabilize marketplace home sticky navigation 2026-07-28 15:40:05 +08:00
autofix-ci[bot]andGitHub 511ca69e4e [autofix.ci] apply automated fixes 2026-07-28 07:17:11 +00:00
fatelei 4c7e60d7e7 fix: fix db version 2026-07-28 15:12:58 +08:00
CodingOnStar 068aced02e feat: complete marketplace trending carousel 2026-07-28 15:07:38 +08:00
fatelei a9e163403d fix: fix model not change 2026-07-28 14:52:16 +08:00
zxhlyhandfatelei bc136b89f9 fix(web): prevent duplicate skill autosaves 2026-07-28 14:52:16 +08:00
fatelei b1bd4f9a8b fix: fix model not update 2026-07-28 14:52:16 +08:00
fatelei 06d3927f05 fix: fix failed test 2026-07-28 14:52:16 +08:00
fatelei 5c3516cae8 fix: fix skill not in agent 2026-07-28 14:52:16 +08:00
fatelei 1c18d8ddbd fix: fix frontend issue and let draft check relax 2026-07-28 14:52:16 +08:00
fatelei 253dfe9351 fix: fix ci failed 2026-07-28 14:52:15 +08:00
fatelei 2d3999e984 fix: fix ci failed 2026-07-28 14:52:15 +08:00
fatelei a431cc726d fix: fix scroll and live mode can not enter 2026-07-28 14:52:15 +08:00
fatelei 08493d2429 fix: fix some bugs 2026-07-28 14:52:15 +08:00
fatelei 8dd0969006 chore: adjust skill entrance 2026-07-28 14:52:15 +08:00
fatelei 2d80d3c35c fix: fix delete skill ref 2026-07-28 14:52:14 +08:00
fatelei 4da764904c feat: support agent skill 2026-07-28 14:46:34 +08:00
fatelei 5c4f4fd1ef Merge branch 'skill' into deploy/dev 2026-07-28 14:44:57 +08:00
fatelei 54843971ac fix: fix model not change 2026-07-28 14:43:05 +08:00
zxhlyh 5ec5d3aeb9 fix(web): prevent duplicate skill autosaves 2026-07-28 14:30:17 +08:00
fatelei d661b53e49 fix: fix model not update 2026-07-28 14:10:49 +08:00
zxhlyh fd1e777f85 Merge branch 'codex/fix-skill-autosave-conflict' into deploy/dev 2026-07-28 14:04:25 +08:00
zxhlyh e5f77ce185 fix(web): prevent duplicate skill autosaves 2026-07-28 14:02:18 +08:00
CodingOnStar 60bc4eaf3c refactor: preserve marketplace home server rendering 2026-07-28 11:55:01 +08:00
CodingOnStar 208073e228 feat: align marketplace hero with redesign 2026-07-28 11:28:29 +08:00
fatelei 99929d1c16 fix: fix conflict 2026-07-27 08:12:58 +08:00
fatelei c699aa11db fix: fix failed test 2026-07-26 23:42:50 +08:00
fatelei 06cd0b56ac fix: fix skill not in agent 2026-07-26 21:00:38 +08:00
fatelei 241a9e1fec fix: fix frontend issue and let draft check relax 2026-07-26 21:00:38 +08:00
fatelei 3d79689fb5 fix: fix ci failed 2026-07-26 20:59:56 +08:00
fatelei 9f050f7957 fix: fix ci failed 2026-07-26 20:59:56 +08:00
fatelei cb691d47d3 fix: fix scroll and live mode can not enter 2026-07-26 20:57:29 +08:00
fatelei 04f5ee58d0 fix: fix some bugs 2026-07-26 20:57:29 +08:00
fatelei f5cff724f4 chore: adjust skill entrance 2026-07-26 20:57:29 +08:00
fatelei 1f4ccafdea fix: fix delete skill ref 2026-07-26 20:57:29 +08:00
fatelei 163049f6ca feat: support agent skill 2026-07-26 20:54:32 +08:00
CodingOnStar 895af9c1d1 fix: restore marketplace category icons 2026-07-24 18:31:00 +08:00
CodingOnStar 2bec40d7be feat: add sticky marketplace home navigation 2026-07-24 18:23:05 +08:00
CodingOnStar 6d18502041 fix: restore marketplace home filter styles 2026-07-24 17:48:05 +08:00
CodingOnStar e1432889cb fix: align marketplace home filters to left 2026-07-24 17:34:46 +08:00
CodingOnStar 0d65a338ef feat: add marketplace home catalog filters 2026-07-24 17:10:12 +08:00
CodingOnStar 3f2fa4d7de fix: align marketplace brand typography 2026-07-24 16:06:48 +08:00
CodingOnStar 0a23c0c386 fix: align embedded marketplace header 2026-07-24 15:53:21 +08:00
CodingOnStar 49bd13248f fix: enable redesigned marketplace route 2026-07-24 15:47:59 +08:00
fatelei 65e7507ca3 Merge branch 'skill' into deploy/dev 2026-07-24 13:38:15 +08:00
fatelei ba157f9604 fix: fix skill not in agent 2026-07-24 13:37:19 +08:00
CodingOnStar c989c3c6ac fix: align marketplace homepage with standalone design 2026-07-24 13:26:41 +08:00
fatelei 0dc913630e fix: fix conflict 2026-07-24 13:18:31 +08:00
fatelei 875cd30b1f fix: fix frontend issue and let draft check relax 2026-07-24 13:11:18 +08:00
fatelei aa4a32ae84 fix: fix ci failed 2026-07-24 11:13:44 +08:00
fatelei 8573e14777 fix: fix ci failed 2026-07-24 11:13:44 +08:00
fatelei 1855be234c fix: fix scroll and live mode can not enter 2026-07-24 11:13:44 +08:00
fatelei 9bb960ff12 fix: fix some bugs 2026-07-24 11:13:44 +08:00
fatelei 1c14c7d467 chore: adjust skill entrance 2026-07-24 11:13:44 +08:00
fatelei 61faec16ca fix: fix delete skill ref 2026-07-24 11:13:44 +08:00
fatelei 57c836e692 feat: support agent skill 2026-07-24 11:13:43 +08:00
CodingOnStar 7d4718d8c5 Merge remote-tracking branch 'origin/main' into feat/marketplace-homepage-redesign
# Conflicts:
#	web/app/components/plugins/marketplace/index.tsx
#	web/app/components/plugins/marketplace/list/card-wrapper.tsx
#	web/app/components/plugins/marketplace/utils.ts
2026-07-24 10:26:43 +08:00
CodingOnStar 3b9176f538 feat: redesign marketplace homepage 2026-07-23 18:57:28 +08:00
samzong 626cc282b1 fix(web): echo OAuth state on authorize redirect (#39459)
Signed-off-by: samzong <[email protected]>
2026-07-23 06:36:58 -04:00
fatelei 52624d54e3 fix: fix db migration 2026-07-23 16:28:27 +08:00
fatelei 5ce038ef92 fix: fix db migration 2026-07-23 16:25:33 +08:00
fatelei 30f4d4c0c6 fix: fix db migration 2026-07-23 16:04:09 +08:00
fatelei 510679a7d1 fix: fix db migration 2026-07-23 15:38:05 +08:00
fatelei 9237f2a14a fix: fix conflict 2026-07-23 15:23:09 +08:00
fatelei bd178c7b29 Merge branch 'skill' into deploy/dev 2026-07-23 14:38:04 +08:00
fatelei d80947aa72 fix: fix scroll and live mode can not enter 2026-07-23 14:36:44 +08:00
fatelei 1618c37d26 Merge branch 'skill' into deploy/dev 2026-07-23 14:17:52 +08:00
fatelei 701ab64462 fix: fix some bugs 2026-07-23 14:14:28 +08:00
fatelei b3298800e9 chore: adjust skill entrance 2026-07-22 16:30:38 +08:00
fatelei 0f1c6b3f78 chore: remove agent v2 feature 2026-07-22 16:26:00 +08:00
林玮 (Jade Lin) 9b4b246aad refactor(api): simplify batch cleanup refresh state
(cherry picked from commit 1ded32d949)
2026-07-22 14:49:11 +08:00
林玮 (Jade Lin) 1bd654a289 refactor(api): simplify vector cache refresh metric
(cherry picked from commit fa585cf134)
2026-07-22 14:49:11 +08:00
林玮 (Jade Lin) fc70329bdb refactor(api): derive cleanup tenant from dataset
(cherry picked from commit b124ec86fc)
2026-07-22 14:49:11 +08:00
林玮 (Jade Lin) cd8a82fbd4 fix(api): invalidate vector space cache after cleanup
(cherry picked from commit a07e5c6eb4)
2026-07-22 14:49:11 +08:00
fatelei 550cb7eff5 fix: fix db migration 2026-07-22 14:42:11 +08:00
fatelei 2e748c16e9 Merge branch 'skill' into deploy/dev 2026-07-22 14:17:08 +08:00
fatelei 577012b66d fix: fix delete skill ref 2026-07-22 14:11:55 +08:00
fatelei 251c324180 Merge branch 'skill' into deploy/dev 2026-07-22 12:59:22 +08:00
fatelei 865a618fd5 Revert "feat: support agent skill"
This reverts commit 991116990a.
2026-07-22 12:48:29 +08:00
fatelei 991116990a feat: support agent skill 2026-07-22 11:45:50 +08:00
fatelei eb5d1da0e8 feat: support agent skill 2026-07-22 10:25:25 +08:00
Joel dcc660daa2 fix: not support jump to detail in marketplace 2026-07-21 13:49:18 +08:00
fatelei dce3b7a7fc feat: use celery task 2026-07-10 15:20:06 +08:00
fatelei be386aba3b chore: add switch 2026-07-10 15:19:56 +08:00
JzoNg 02e51e7d7c Merge branch 'main' into deploy/dev 2026-07-10 10:28:07 +08:00
Xiyuan ChenandGitHub 96b6d4f2c0 Merge branch 'fix/socketio-user-connect-app-context' into deploy/dev 2026-07-08 22:02:43 -07:00
GareArc 4c84c5957d fix: run user_connect authorization inside Flask app context
Socket.IO event handlers run in plain gevent greenlets without a Flask
application context, so the db.session() call added in #38227 raises
RuntimeError: Working outside of application context, and every
user_connect is rejected as unauthorized. Wrap the authorization call in
sio.app.app_context(), matching the existing connect handler.
2026-07-08 21:57:07 -07:00
CodingOnStar 34613ecdc5 fix(app): update app creation logic to use database session and ensure RBAC access initialization 2026-07-09 12:00:16 +08:00
CodingOnStar 6a14245401 Merge remote-tracking branch 'origin/main' into deploy/dev 2026-07-09 11:49:16 +08:00
CodingOnStar a758ca2aef fix(amplitude): update API key for amplitude initialization 2026-07-09 11:37:28 +08:00
CodingOnStar 9e60d4e213 Merge branch 'fix/report-slug' into deploy/dev 2026-07-09 11:29:04 +08:00
CodingOnStar ef29c8442c refactor(amplitude): comment out amplitude initialization logic for future implementation 2026-07-09 11:26:59 +08:00
CodingOnStar a1b45415ac fix(web): preserve attribution from auth redirect 2026-07-09 11:00:37 +08:00
fatelei 7fc46d75bd chore: add switch 2026-07-08 18:08:02 +08:00
fatelei 953a4ef0ca feat: create app sync scope and workspace user 2026-07-08 16:12:40 +08:00
283 changed files with 45194 additions and 1178 deletions
+2
View File
@@ -144,6 +144,7 @@ from .workspace import (
models,
plugin,
rbac,
skills,
snippets,
tool_providers,
trigger_providers,
@@ -225,6 +226,7 @@ __all__ = [
"saved_message",
"setup",
"site",
"skills",
"snippet_workflow",
"snippet_workflow_draft_variable",
"snippets",
+1 -1
View File
@@ -60,7 +60,7 @@ class TagBindingRemovePayload(BaseModel):
class TagListQueryParam(BaseModel):
type: Literal["knowledge", "app", "snippet", ""] = Field("", description="Tag type filter")
type: TagType | Literal[""] = Field("", description="Tag type filter")
keyword: str | None = Field(None, description="Search keyword")
+899
View File
@@ -0,0 +1,899 @@
"""Console API for workspace-level Skill Management."""
from __future__ import annotations
import io
from flask import request, send_file
from flask_restx import Resource
from pydantic import BaseModel, ConfigDict, Field, ValidationError
from controllers.common.fields import BinaryFileResponse
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
from controllers.console import console_ns
from controllers.console.wraps import (
RBACPermission,
RBACResourceScope,
account_initialization_required,
edit_permission_required,
rbac_permission_required,
setup_required,
with_current_tenant_id,
with_current_user,
)
from fields.base import ResponseModel
from libs import helper
from libs.helper import dump_response
from libs.login import login_required
from models.account import Account
from services.skill_management_service import (
SkillAssistMessagePayload,
SkillCreatePayload,
SkillDraftFileCheckPayload,
SkillDraftFileOperationPayload,
SkillDraftTreePayload,
SkillImportPayload,
SkillManagementService,
SkillManagementServiceError,
SkillMetadataPayload,
SkillPublishPayload,
SkillRestorePayload,
SkillVersionUpdatePayload,
)
_FILE_UPLOAD_PARAMS = {
"file": {
"description": "Skill draft file payload",
"in": "formData",
"type": "file",
"required": True,
},
}
class WorkspaceSkillsQuery(BaseModel):
keyword: str | None = Field(default=None, description="Search keyword matching skill name or description.")
page: int = Field(default=1, ge=1, le=99999, description="Page number.")
limit: int = Field(default=20, ge=1, le=100, description="Number of items per page.")
tag: list[str] = Field(
default_factory=list,
description="Skill tag filters. Repeat the parameter for multiple tags.",
)
class SkillDeletePayload(BaseModel):
model_config = ConfigDict(extra="forbid")
confirmation_name: str | None = Field(
default=None,
description="Required when deleting a referenced Skill. Must match the Skill display name.",
)
class AgentSkillBindingsPayload(BaseModel):
model_config = ConfigDict(extra="forbid")
skill_ids: list[str] = Field(default_factory=list, description="Ordered Skill IDs bound to the Agent.")
class SkillFileQuery(BaseModel):
path: str = Field(description="Skill file path relative to the Skill root.")
version_id: str | None = Field(default=None, description="Optional published version ID. Omit for current draft.")
class SkillResponse(ResponseModel):
id: str
name: str
display_name: str
icon: str
description: str
tags: list[str] = Field(default_factory=list)
name_manually_edited: bool = False
visibility: str
latest_published_version_id: str | None = None
latest_published_version_number: int | None = None
latest_published_at: int | None = None
reference_count: int = 0
created_by: str | None = None
created_by_name: str | None = None
updated_by: str | None = None
updated_by_name: str | None = None
created_at: int
updated_at: int
class SkillFileResponse(ResponseModel):
id: str | None = None
path: str
kind: str
storage: str | None = None
mime_type: str | None = None
content: str | None = None
tool_file_id: str | None = None
size: int | None = None
hash: str | None = None
class SkillFilePreviewResponse(ResponseModel):
path: str
mime_type: str
content: str
size: int
hash: str
class SkillFileUploadResponse(ResponseModel):
id: str
name: str
mime_type: str
size: int
hash: str
class SkillFileCheckErrorResponse(ResponseModel):
code: str
message: str
class SkillFileCheckItemResponse(ResponseModel):
path: str
filename: str
extension: str
mime_type: str
size: int
errors: list[SkillFileCheckErrorResponse] = Field(default_factory=list)
class SkillFileCheckResponse(ResponseModel):
data: dict[str, SkillFileCheckItemResponse] = Field(default_factory=dict)
class SkillDetailResponse(SkillResponse):
files: list[SkillFileResponse] = Field(default_factory=list)
class SkillListResponse(ResponseModel):
data: list[SkillResponse] = Field(default_factory=list)
has_more: bool = False
limit: int = 20
page: int = 1
total: int = 0
class SkillTagResponse(ResponseModel):
tag: str
count: int
class SkillTagListResponse(ResponseModel):
data: list[SkillTagResponse] = Field(default_factory=list)
class SkillVersionResponse(ResponseModel):
id: str
skill_id: str
version_number: int
version_name: str
publish_note: str
hash_code: str
archive_size: int
published_by: str | None = None
published_by_name: str | None = None
is_latest: bool = False
created_at: int
class SkillVersionListResponse(ResponseModel):
data: list[SkillVersionResponse] = Field(default_factory=list)
class SkillVersionDetailResponse(SkillVersionResponse):
files: list[SkillFileResponse] = Field(default_factory=list)
class SkillVersionDeleteResponse(ResponseModel):
id: str
deleted: bool
latest_published_version_id: str | None = None
class SkillReferenceResponse(ResponseModel):
type: str
agent_id: str
agent_icon: str | None = None
agent_icon_background: str | None = None
agent_icon_type: str | None = None
app_id: str | None = None
name: str
display_name: str
workflow_id: str | None = None
workflow_name: str | None = None
workflow_icon: str | None = None
workflow_icon_background: str | None = None
workflow_icon_type: str | None = None
workflow_version: str | None = None
node_id: str | None = None
node_name: str | None = None
class SkillReferenceListResponse(ResponseModel):
data: list[SkillReferenceResponse] = Field(default_factory=list)
class SkillDeleteResponse(ResponseModel):
id: str
deleted: bool
class AgentSkillBindingItemResponse(ResponseModel):
id: str
priority: int
name: str
display_name: str
icon: str
description: str
tags: list[str] = Field(default_factory=list)
status: str
file_count: int
latest_published_version_id: str | None = None
latest_published_at: int | None = None
updated_at: int
class AgentSkillBindingsResponse(ResponseModel):
agent_id: str
skill_ids: list[str] = Field(default_factory=list)
data: list[AgentSkillBindingItemResponse] = Field(default_factory=list)
register_schema_models(
console_ns,
WorkspaceSkillsQuery,
SkillCreatePayload,
SkillAssistMessagePayload,
SkillMetadataPayload,
SkillDraftFileCheckPayload,
SkillDraftFileOperationPayload,
SkillDraftTreePayload,
SkillPublishPayload,
SkillRestorePayload,
SkillVersionUpdatePayload,
SkillDeletePayload,
SkillFileQuery,
AgentSkillBindingsPayload,
)
register_response_schema_models(
console_ns,
SkillResponse,
SkillFileResponse,
SkillFilePreviewResponse,
SkillFileUploadResponse,
SkillFileCheckErrorResponse,
SkillFileCheckItemResponse,
SkillFileCheckResponse,
SkillDetailResponse,
SkillListResponse,
SkillTagResponse,
SkillTagListResponse,
SkillVersionResponse,
SkillVersionListResponse,
SkillVersionDetailResponse,
SkillVersionDeleteResponse,
SkillReferenceResponse,
SkillReferenceListResponse,
SkillDeleteResponse,
AgentSkillBindingItemResponse,
AgentSkillBindingsResponse,
BinaryFileResponse,
)
def _error_response(exc: SkillManagementServiceError) -> tuple[dict[str, object], int]:
body: dict[str, object] = {"code": exc.code, "message": exc.message}
if exc.details:
body["details"] = exc.details
return body, exc.status_code
@console_ns.route("/workspaces/current/skills")
class WorkspaceSkillsApi(Resource):
@console_ns.doc(params=query_params_from_model(WorkspaceSkillsQuery))
@console_ns.response(200, "Workspace skills", console_ns.models[SkillListResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.SKILL_VIEW, resource_required=False)
@with_current_tenant_id
def get(self, current_tenant_id: str):
query_input: dict[str, object] = {
"keyword": request.args.get("keyword"),
"tag": request.args.getlist("tag"),
}
if "limit" in request.args:
query_input["limit"] = request.args.get("limit")
if "page" in request.args:
query_input["page"] = request.args.get("page")
query = WorkspaceSkillsQuery.model_validate(query_input)
result = SkillManagementService().list_skills(
tenant_id=current_tenant_id,
keyword=query.keyword,
page=query.page,
limit=query.limit,
tags=[tag for tag in query.tag if tag],
)
return dump_response(SkillListResponse, result)
@console_ns.expect(console_ns.models[SkillCreatePayload.__name__])
@console_ns.response(201, "Skill created", console_ns.models[SkillDetailResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@edit_permission_required
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.SKILL_EDIT, resource_required=False)
@with_current_user
@with_current_tenant_id
def post(self, current_tenant_id: str, current_user: Account):
try:
payload = SkillCreatePayload.model_validate(console_ns.payload or {})
result = SkillManagementService().create_skill(
tenant_id=current_tenant_id,
user_id=current_user.id,
payload=payload,
)
return dump_response(SkillDetailResponse, result), 201
except ValidationError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except ValueError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except SkillManagementServiceError as exc:
return _error_response(exc)
@console_ns.route("/workspaces/current/skills/files/upload")
class WorkspaceSkillFileUploadApi(Resource):
@console_ns.doc(consumes=["multipart/form-data"], params=_FILE_UPLOAD_PARAMS)
@console_ns.response(201, "Skill draft file uploaded", console_ns.models[SkillFileUploadResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@edit_permission_required
@with_current_user
@with_current_tenant_id
def post(self, current_tenant_id: str, current_user: Account):
if "file" not in request.files:
return {"code": "no_file_uploaded", "message": "no file uploaded"}, 400
file = request.files["file"]
if not file.filename:
return {"code": "filename_missing", "message": "filename is required"}, 400
try:
result = SkillManagementService().upload_file(
tenant_id=current_tenant_id,
user_id=current_user.id,
filename=file.filename,
content=file.stream.read(),
mime_type=file.mimetype,
)
return dump_response(SkillFileUploadResponse, result), 201
except SkillManagementServiceError as exc:
return _error_response(exc)
@console_ns.route("/workspaces/current/skills/tags")
class WorkspaceSkillTagsApi(Resource):
@console_ns.response(200, "Workspace Skill tags", console_ns.models[SkillTagListResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@with_current_tenant_id
def get(self, current_tenant_id: str):
result = SkillManagementService().list_tags(tenant_id=current_tenant_id)
return dump_response(SkillTagListResponse, result)
@console_ns.route("/workspaces/current/skills/import")
class WorkspaceSkillImportApi(Resource):
@console_ns.doc(description="Import a Skill zip package from multipart form field `file`.")
@console_ns.response(201, "Skill imported", console_ns.models[SkillDetailResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@edit_permission_required
@with_current_user
@with_current_tenant_id
def post(self, current_tenant_id: str, current_user: Account):
upload = request.files.get("file")
if upload is None:
return {"code": "invalid_request", "message": "file is required"}, 400
try:
payload = SkillImportPayload(content=upload.read(), filename=upload.filename or "skill.zip")
result = SkillManagementService().import_skill(
tenant_id=current_tenant_id,
user_id=current_user.id,
payload=payload,
)
return dump_response(SkillDetailResponse, result), 201
except (ValidationError, ValueError) as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except SkillManagementServiceError as exc:
return _error_response(exc)
@console_ns.route("/workspaces/current/skills/<string:skill_id>")
class WorkspaceSkillApi(Resource):
@console_ns.response(200, "Skill detail", console_ns.models[SkillDetailResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.SKILL_VIEW, resource_required=False)
@with_current_tenant_id
def get(self, current_tenant_id: str, skill_id: str):
try:
result = SkillManagementService().get_skill(tenant_id=current_tenant_id, skill_id=skill_id)
return dump_response(SkillDetailResponse, result)
except SkillManagementServiceError as exc:
return _error_response(exc)
@console_ns.expect(console_ns.models[SkillMetadataPayload.__name__])
@console_ns.response(200, "Skill updated", console_ns.models[SkillResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@edit_permission_required
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.SKILL_EDIT, resource_required=False)
@with_current_user
@with_current_tenant_id
def patch(self, current_tenant_id: str, current_user: Account, skill_id: str):
try:
payload = SkillMetadataPayload.model_validate(console_ns.payload or {})
result = SkillManagementService().update_metadata(
tenant_id=current_tenant_id,
user_id=current_user.id,
skill_id=skill_id,
payload=payload,
)
return dump_response(SkillResponse, result)
except ValidationError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except ValueError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except SkillManagementServiceError as exc:
return _error_response(exc)
@console_ns.expect(console_ns.models[SkillDeletePayload.__name__])
@console_ns.response(200, "Skill deleted", console_ns.models[SkillDeleteResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@edit_permission_required
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.SKILL_DELETE, resource_required=False)
@with_current_tenant_id
def delete(self, current_tenant_id: str, skill_id: str):
try:
payload = SkillDeletePayload.model_validate(console_ns.payload or {})
result = SkillManagementService().delete_skill(
tenant_id=current_tenant_id,
skill_id=skill_id,
confirmation_name=payload.confirmation_name,
)
return dump_response(SkillDeleteResponse, result)
except ValidationError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except SkillManagementServiceError as exc:
return _error_response(exc)
@console_ns.route("/workspaces/current/skills/<string:skill_id>/duplicate")
class WorkspaceSkillDuplicateApi(Resource):
@console_ns.response(201, "Skill duplicated", console_ns.models[SkillDetailResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@edit_permission_required
@with_current_user
@with_current_tenant_id
def post(self, current_tenant_id: str, current_user: Account, skill_id: str):
try:
result = SkillManagementService().duplicate_skill(
tenant_id=current_tenant_id,
user_id=current_user.id,
skill_id=skill_id,
)
return dump_response(SkillDetailResponse, result), 201
except SkillManagementServiceError as exc:
return _error_response(exc)
@console_ns.route("/workspaces/current/skills/<string:skill_id>/export")
class WorkspaceSkillExportApi(Resource):
@console_ns.response(200, "Published Skill zip archive")
@setup_required
@login_required
@account_initialization_required
@with_current_tenant_id
def get(self, current_tenant_id: str, skill_id: str):
try:
result = SkillManagementService().pull_published_archive(tenant_id=current_tenant_id, skill_id=skill_id)
return send_file(
io.BytesIO(result.payload),
mimetype=result.mime_type,
as_attachment=True,
download_name=result.filename,
)
except SkillManagementServiceError as exc:
return _error_response(exc)
@console_ns.route("/workspaces/current/skills/<string:skill_id>/assist/messages")
class WorkspaceSkillAssistMessageApi(Resource):
"""Stream read-only Skill Authoring suggestions from the default workspace model."""
@console_ns.expect(console_ns.models[SkillAssistMessagePayload.__name__])
@console_ns.response(200, "Skill Authoring assistant event stream")
@setup_required
@login_required
@account_initialization_required
@with_current_user
@with_current_tenant_id
def post(self, current_tenant_id: str, current_user: Account, skill_id: str):
try:
payload = SkillAssistMessagePayload.model_validate(console_ns.payload or {})
response = SkillManagementService().create_assistant_action_stream(
tenant_id=current_tenant_id,
skill_id=skill_id,
user_id=current_user.id,
message=payload.message,
attachments=payload.attachments,
history=payload.history,
model_payload=payload.model,
target_path=payload.target_path,
)
except ValidationError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except SkillManagementServiceError as exc:
return _error_response(exc)
return helper.compact_generate_response(response)
@console_ns.route("/workspaces/current/skills/<string:skill_id>/files/check")
class WorkspaceSkillFilesCheckApi(Resource):
@console_ns.expect(console_ns.models[SkillDraftFileCheckPayload.__name__])
@console_ns.response(200, "Draft files checked", console_ns.models[SkillFileCheckResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@edit_permission_required
@with_current_tenant_id
def post(self, current_tenant_id: str, skill_id: str):
try:
payload = SkillDraftFileCheckPayload.model_validate(console_ns.payload or {})
result = SkillManagementService().check_draft_files(
tenant_id=current_tenant_id,
skill_id=skill_id,
payload=payload,
)
return dump_response(SkillFileCheckResponse, result)
except ValidationError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except ValueError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except SkillManagementServiceError as exc:
return _error_response(exc)
@console_ns.route("/workspaces/current/skills/<string:skill_id>/files")
class WorkspaceSkillFilesApi(Resource):
@console_ns.expect(console_ns.models[SkillDraftFileOperationPayload.__name__])
@console_ns.response(200, "Draft file operation applied", console_ns.models[SkillDetailResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@edit_permission_required
@with_current_user
@with_current_tenant_id
def patch(self, current_tenant_id: str, current_user: Account, skill_id: str):
try:
payload = SkillDraftFileOperationPayload.model_validate(console_ns.payload or {})
result = SkillManagementService().apply_draft_file_operation(
tenant_id=current_tenant_id,
user_id=current_user.id,
skill_id=skill_id,
payload=payload,
)
return dump_response(SkillDetailResponse, result)
except ValidationError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except ValueError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except SkillManagementServiceError as exc:
return _error_response(exc)
@console_ns.expect(console_ns.models[SkillDraftTreePayload.__name__])
@console_ns.response(200, "Draft files replaced", console_ns.models[SkillDetailResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@edit_permission_required
@with_current_user
@with_current_tenant_id
def put(self, current_tenant_id: str, current_user: Account, skill_id: str):
try:
payload = SkillDraftTreePayload.model_validate(console_ns.payload or {})
result = SkillManagementService().replace_draft_tree(
tenant_id=current_tenant_id,
user_id=current_user.id,
skill_id=skill_id,
payload=payload,
)
return dump_response(SkillDetailResponse, result)
except ValidationError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except ValueError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except SkillManagementServiceError as exc:
return _error_response(exc)
@console_ns.route("/workspaces/current/skills/<string:skill_id>/files/preview")
class WorkspaceSkillFilePreviewApi(Resource):
@console_ns.doc(params=query_params_from_model(SkillFileQuery))
@console_ns.response(200, "Skill file text preview", console_ns.models[SkillFilePreviewResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@with_current_tenant_id
def get(self, current_tenant_id: str, skill_id: str):
try:
query = SkillFileQuery.model_validate(
{
"path": request.args.get("path"),
"version_id": request.args.get("version_id"),
}
)
result = SkillManagementService().preview_file(
tenant_id=current_tenant_id,
skill_id=skill_id,
path=query.path,
version_id=query.version_id,
)
return dump_response(SkillFilePreviewResponse, result)
except ValidationError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except ValueError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except SkillManagementServiceError as exc:
return _error_response(exc)
@console_ns.route("/workspaces/current/skills/<string:skill_id>/files/content")
class WorkspaceSkillFileContentApi(Resource):
@console_ns.doc(params={**query_params_from_model(SkillFileQuery), "download": "Return as an attachment when 1."})
@console_ns.response(200, "Skill file content", console_ns.models[BinaryFileResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@with_current_tenant_id
def get(self, current_tenant_id: str, skill_id: str):
try:
query = SkillFileQuery.model_validate(
{
"path": request.args.get("path"),
"version_id": request.args.get("version_id"),
}
)
result = SkillManagementService().pull_file(
tenant_id=current_tenant_id,
skill_id=skill_id,
path=query.path,
version_id=query.version_id,
)
return send_file(
io.BytesIO(result.payload),
mimetype=result.mime_type,
as_attachment=request.args.get("download") == "1",
download_name=result.filename,
)
except ValidationError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except ValueError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except SkillManagementServiceError as exc:
return _error_response(exc)
@console_ns.route("/workspaces/current/skills/<string:skill_id>/publish")
class WorkspaceSkillPublishApi(Resource):
@console_ns.expect(console_ns.models[SkillPublishPayload.__name__])
@console_ns.response(200, "Skill published", console_ns.models[SkillVersionResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@edit_permission_required
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.SKILL_PUBLISH, resource_required=False)
@with_current_user
@with_current_tenant_id
def post(self, current_tenant_id: str, current_user: Account, skill_id: str):
try:
payload = SkillPublishPayload.model_validate(console_ns.payload or {})
result = SkillManagementService().publish_skill(
tenant_id=current_tenant_id,
user_id=current_user.id,
skill_id=skill_id,
payload=payload,
)
return dump_response(SkillVersionResponse, result)
except ValidationError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except SkillManagementServiceError as exc:
return _error_response(exc)
@console_ns.route("/workspaces/current/skills/<string:skill_id>/restore")
class WorkspaceSkillRestoreApi(Resource):
@console_ns.expect(console_ns.models[SkillRestorePayload.__name__])
@console_ns.response(200, "Skill version restored to draft", console_ns.models[SkillDetailResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@edit_permission_required
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.SKILL_PUBLISH, resource_required=False)
@with_current_user
@with_current_tenant_id
def post(self, current_tenant_id: str, current_user: Account, skill_id: str):
try:
payload = SkillRestorePayload.model_validate(console_ns.payload or {})
result = SkillManagementService().restore_version(
tenant_id=current_tenant_id,
user_id=current_user.id,
skill_id=skill_id,
payload=payload,
)
return dump_response(SkillDetailResponse, result)
except ValidationError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except SkillManagementServiceError as exc:
return _error_response(exc)
@console_ns.route("/workspaces/current/skills/<string:skill_id>/references")
class WorkspaceSkillReferencesApi(Resource):
@console_ns.response(200, "Skill references", console_ns.models[SkillReferenceListResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@with_current_tenant_id
def get(self, current_tenant_id: str, skill_id: str):
try:
result = SkillManagementService().list_skill_references(tenant_id=current_tenant_id, skill_id=skill_id)
return dump_response(SkillReferenceListResponse, result)
except SkillManagementServiceError as exc:
return _error_response(exc)
@console_ns.route("/workspaces/current/skills/<string:skill_id>/versions")
class WorkspaceSkillVersionsApi(Resource):
@console_ns.response(200, "Skill versions", console_ns.models[SkillVersionListResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@with_current_tenant_id
def get(self, current_tenant_id: str, skill_id: str):
try:
result = SkillManagementService().list_versions(tenant_id=current_tenant_id, skill_id=skill_id)
return dump_response(SkillVersionListResponse, result)
except SkillManagementServiceError as exc:
return _error_response(exc)
@console_ns.route("/workspaces/current/skills/<string:skill_id>/versions/<string:version_id>")
class WorkspaceSkillVersionApi(Resource):
@console_ns.response(200, "Skill version detail", console_ns.models[SkillVersionDetailResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@with_current_tenant_id
def get(self, current_tenant_id: str, skill_id: str, version_id: str):
try:
result = SkillManagementService().get_version(
tenant_id=current_tenant_id,
skill_id=skill_id,
version_id=version_id,
)
return dump_response(SkillVersionDetailResponse, result)
except SkillManagementServiceError as exc:
return _error_response(exc)
@console_ns.expect(console_ns.models[SkillVersionUpdatePayload.__name__])
@console_ns.response(200, "Skill version updated", console_ns.models[SkillVersionResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@edit_permission_required
@with_current_tenant_id
def patch(self, current_tenant_id: str, skill_id: str, version_id: str):
try:
payload = SkillVersionUpdatePayload.model_validate(console_ns.payload or {})
result = SkillManagementService().update_version(
tenant_id=current_tenant_id,
skill_id=skill_id,
version_id=version_id,
payload=payload,
)
return dump_response(SkillVersionResponse, result)
except ValidationError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except SkillManagementServiceError as exc:
return _error_response(exc)
@console_ns.response(200, "Skill version deleted", console_ns.models[SkillVersionDeleteResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@edit_permission_required
@with_current_user
@with_current_tenant_id
def delete(self, current_tenant_id: str, current_user: Account, skill_id: str, version_id: str):
try:
result = SkillManagementService().delete_version(
tenant_id=current_tenant_id,
user_id=current_user.id,
skill_id=skill_id,
version_id=version_id,
)
return dump_response(SkillVersionDeleteResponse, result)
except SkillManagementServiceError as exc:
return _error_response(exc)
@console_ns.route("/workspaces/current/agents/<string:agent_id>/skills")
class WorkspaceAgentSkillBindingsApi(Resource):
@console_ns.response(200, "Agent Skill bindings", console_ns.models[AgentSkillBindingsResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@with_current_tenant_id
def get(self, current_tenant_id: str, agent_id: str):
result = SkillManagementService().list_agent_bindings(tenant_id=current_tenant_id, agent_id=agent_id)
return dump_response(AgentSkillBindingsResponse, result)
@console_ns.expect(console_ns.models[AgentSkillBindingsPayload.__name__])
@console_ns.response(200, "Agent Skill bindings replaced", console_ns.models[AgentSkillBindingsResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@edit_permission_required
@with_current_user
@with_current_tenant_id
def put(self, current_tenant_id: str, current_user: Account, agent_id: str):
try:
payload = AgentSkillBindingsPayload.model_validate(console_ns.payload or {})
result = SkillManagementService().replace_agent_bindings(
tenant_id=current_tenant_id,
user_id=current_user.id,
agent_id=agent_id,
skill_ids=payload.skill_ids,
)
return dump_response(AgentSkillBindingsResponse, result)
except ValidationError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except SkillManagementServiceError as exc:
return _error_response(exc)
__all__ = [
"WorkspaceAgentSkillBindingsApi",
"WorkspaceSkillApi",
"WorkspaceSkillDuplicateApi",
"WorkspaceSkillExportApi",
"WorkspaceSkillFilesApi",
"WorkspaceSkillImportApi",
"WorkspaceSkillPublishApi",
"WorkspaceSkillReferencesApi",
"WorkspaceSkillRestoreApi",
"WorkspaceSkillTagsApi",
"WorkspaceSkillVersionApi",
"WorkspaceSkillVersionsApi",
"WorkspaceSkillsApi",
]
+2
View File
@@ -24,6 +24,7 @@ from .knowledge import retrieval as _knowledge_retrieval
from .plugin import agent_config as _agent_config
from .plugin import agent_drive as _agent_drive
from .plugin import plugin as _plugin
from .plugin import skills as _skills
from .workspace import workspace as _workspace
api.add_namespace(inner_api_ns)
@@ -38,6 +39,7 @@ __all__ = [
"_mail",
"_plugin",
"_runtime_credentials",
"_skills",
"_workspace",
"api",
"bp",
@@ -0,0 +1,54 @@
"""Inner API for published workspace Skills.
These endpoints are called by trusted runtime services. They expose only
published Skill artifacts, never draft files or editable metadata.
"""
from __future__ import annotations
import io
from flask import request, send_file
from flask_restx import Resource
from pydantic import BaseModel, ValidationError
from controllers.console.wraps import setup_required
from controllers.inner_api import inner_api_ns
from controllers.inner_api.wraps import plugin_inner_api_only
from services.skill_management_service import SkillManagementService, SkillManagementServiceError
class _SkillTargetQuery(BaseModel):
tenant_id: str
def _target_query_from_request() -> _SkillTargetQuery:
return _SkillTargetQuery.model_validate({"tenant_id": request.args.get("tenant_id")})
def _error_response(exc: SkillManagementServiceError) -> tuple[dict[str, str], int]:
return {"code": exc.code, "message": exc.message}, exc.status_code
@inner_api_ns.route("/skills/<string:skill_id>/pull")
class PublishedSkillPullApi(Resource):
@setup_required
@plugin_inner_api_only
@inner_api_ns.doc("published_skill_pull")
def get(self, skill_id: str):
try:
query = _target_query_from_request()
result = SkillManagementService().pull_published_archive(tenant_id=query.tenant_id, skill_id=skill_id)
return send_file(
io.BytesIO(result.payload),
mimetype=result.mime_type,
as_attachment=True,
download_name=result.filename,
)
except ValidationError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except SkillManagementServiceError as exc:
return _error_response(exc)
__all__ = ["PublishedSkillPullApi"]
@@ -43,6 +43,7 @@ from core.workflow.nodes.agent_v2.runtime_request_builder import (
build_config_layer_config,
build_knowledge_layer_config,
build_shell_layer_config,
load_runtime_agent_skill_configs,
)
from models.agent_config_entities import AgentSoulConfig, AgentSoulToolsConfig
from models.provider_ids import ModelProviderID
@@ -127,14 +128,22 @@ class AgentAppRuntimeRequestBuilder:
"cli_tool_count": len(agent_soul.tools.cli_tools),
}
runtime_config_skills = load_runtime_agent_skill_configs(
tenant_id=context.dify_context.tenant_id,
agent_id=context.agent_id,
)
config_layer_config, config_warnings = build_config_layer_config(
agent_soul,
agent_id=context.agent_id,
config_version_id=context.agent_config_snapshot_id,
config_version_kind=context.agent_config_version_kind,
runtime_config_skills=runtime_config_skills,
)
append_runtime_warnings(metadata, config_warnings)
soul_prompt_resolver = build_config_aware_soul_mention_resolver(agent_soul)
soul_prompt_resolver = build_config_aware_soul_mention_resolver(
agent_soul,
runtime_config_skills=runtime_config_skills,
)
knowledge_config = build_knowledge_layer_config(agent_soul)
request = self._request_builder.build_for_agent_app(
+1 -1
View File
@@ -1545,7 +1545,7 @@ class ProviderManager:
except ValidationError:
return SystemConfiguration(enabled=False)
if plugin_provider_entity.installation_source != PluginInstallationSource.Marketplace:
if plugin_provider_entity.installation_source in (None, PluginInstallationSource.Package):
return SystemConfiguration(enabled=False)
from core.plugin.plugin_service import PluginService
+4
View File
@@ -62,6 +62,10 @@ class RBACPermission(StrEnum):
API_EXTENSION_MANAGE = "api_extension_manage"
CUSTOMIZATION_MANAGE = "customization_manage"
AGENT_MANAGE = "agent_manage"
SKILL_VIEW = "skill_view"
SKILL_EDIT = "skill_edit"
SKILL_PUBLISH = "skill_publish"
SKILL_DELETE = "skill_delete"
SNIPPETS_CREATE_AND_MODIFY = "snippets_create_and_modify"
SNIPPETS_MANAGE = "snippets_management"
@@ -37,6 +37,7 @@ from dify_agent.layers.shell import (
)
from dify_agent.protocol import CreateRunRequest, DeferredToolResultsPayload
from pydantic import BaseModel, ValidationError
from sqlalchemy.exc import OperationalError
from clients.agent_backend import (
AgentBackendModelConfig,
@@ -207,14 +208,22 @@ class WorkflowAgentRuntimeRequestBuilder:
"cli_tool_count": len(agent_soul.tools.cli_tools),
}
runtime_config_skills = load_runtime_agent_skill_configs(
tenant_id=context.dify_context.tenant_id,
agent_id=context.agent.id,
)
config_layer_config, config_warnings = build_config_layer_config(
agent_soul,
agent_id=context.agent.id,
config_version_id=context.snapshot.id,
config_version_kind="snapshot",
runtime_config_skills=runtime_config_skills,
)
append_runtime_warnings(metadata, config_warnings)
soul_prompt_resolver = build_config_aware_soul_mention_resolver(agent_soul)
soul_prompt_resolver = build_config_aware_soul_mention_resolver(
agent_soul,
runtime_config_skills=runtime_config_skills,
)
soul_prompt = expand_prompt_mentions(agent_soul.prompt.system_prompt, soul_prompt_resolver).strip()
knowledge_config = build_knowledge_layer_config(agent_soul)
@@ -878,11 +887,16 @@ def append_runtime_warnings(metadata: dict[str, Any], warnings: list[dict[str, s
existing.extend(warnings)
def build_config_aware_soul_mention_resolver(agent_soul: AgentSoulConfig):
def build_config_aware_soul_mention_resolver(
agent_soul: AgentSoulConfig,
*,
runtime_config_skills: Sequence[DifyConfigSkillConfig] = (),
):
"""Resolve config skill/file mentions and delegate the rest to Agent Soul."""
base_resolver = build_soul_mention_resolver(agent_soul)
skill_names = {item.name for item in agent_soul.config_skills if not item.is_missing}
skill_names.update(item.name for item in runtime_config_skills)
file_names = {item.name for item in agent_soul.config_files if not item.is_missing}
def _resolve(mention: object) -> str | None:
@@ -900,12 +914,34 @@ def build_config_aware_soul_mention_resolver(agent_soul: AgentSoulConfig):
return _resolve
def load_runtime_agent_skill_configs(*, tenant_id: str, agent_id: str) -> list[DifyConfigSkillConfig]:
"""Return workspace-bound Skills as prompt-safe runtime config skills."""
from services.skill_management_service import SkillManagementService
try:
runtime_skills = SkillManagementService().list_runtime_agent_skills(tenant_id=tenant_id, agent_id=agent_id)
except OperationalError as exc:
if "no such table: agent_skill_bindings" not in str(exc.orig):
raise
runtime_skills = []
return [
DifyConfigSkillConfig(
name=str(item["name"]),
description=str(item.get("description") or ""),
size=cast(int | None, item.get("size")),
mime_type=cast(str | None, item.get("mime_type")),
)
for item in runtime_skills
]
def build_config_layer_config(
agent_soul: AgentSoulConfig,
*,
agent_id: str | None = None,
config_version_id: str | None = None,
config_version_kind: Literal["snapshot", "draft", "build_draft"] = "snapshot",
runtime_config_skills: Sequence[DifyConfigSkillConfig] = (),
) -> tuple[DifyConfigLayerConfig, list[dict[str, str]]]:
"""Build the always-present Agent config layer from Agent Soul state.
@@ -922,8 +958,23 @@ def build_config_layer_config(
)
)
available_skills = [skill for skill in agent_soul.config_skills if not skill.is_missing]
skill_configs = [
DifyConfigSkillConfig(
name=skill.name,
description=skill.description,
size=skill.size,
mime_type=skill.mime_type,
)
for skill in available_skills
]
seen_skill_names = {skill.name for skill in skill_configs}
for skill in runtime_config_skills:
if skill.name in seen_skill_names:
continue
seen_skill_names.add(skill.name)
skill_configs.append(skill)
available_files = [file_ref for file_ref in agent_soul.config_files if not file_ref.is_missing]
skill_names = {skill.name for skill in available_skills}
skill_names = {skill.name for skill in skill_configs}
file_names = {file_ref.name for file_ref in available_files}
warnings: list[dict[str, str]] = [
{
@@ -960,15 +1011,7 @@ def build_config_layer_config(
kind=config_version_kind,
writable=config_version_kind == "build_draft",
),
skills=[
DifyConfigSkillConfig(
name=skill.name,
description=skill.description,
size=skill.size,
mime_type=skill.mime_type,
)
for skill in available_skills
],
skills=skill_configs,
files=[
DifyConfigFileConfig(
name=file_ref.name,
@@ -0,0 +1,114 @@
"""add workspace skill management
Revision ID: a4f8d2c9e1b0
Revises: e4708db55c1d
Create Date: 2026-07-09 12:00:00.000000
"""
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import mysql
from models.types import StringUUID
# revision identifiers, used by Alembic.
revision = "a4f8d2c9e1b0"
down_revision = "e4708db55c1d"
branch_labels = None
depends_on = None
def _uuid_column(name: str, *, nullable: bool = False) -> sa.Column:
return sa.Column(name, StringUUID(), nullable=nullable)
def _long_text() -> sa.types.TypeEngine:
return sa.Text().with_variant(mysql.LONGTEXT(), "mysql")
def upgrade() -> None:
op.create_table(
"skills",
_uuid_column("id"),
_uuid_column("tenant_id"),
sa.Column("name", sa.String(length=64), nullable=False),
sa.Column("display_name", sa.String(length=128), nullable=False),
sa.Column("icon", sa.String(length=16), nullable=False, server_default="📄"),
sa.Column("description", sa.String(length=1024), nullable=False, server_default=""),
sa.Column("name_manually_edited", sa.Boolean(), nullable=False, server_default=sa.false()),
sa.Column("visibility", sa.String(length=32), nullable=False, server_default="workspace"),
_uuid_column("latest_published_version_id", nullable=True),
_uuid_column("created_by", nullable=True),
_uuid_column("updated_by", nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.func.current_timestamp()),
sa.Column("updated_at", sa.DateTime(), nullable=False, server_default=sa.func.current_timestamp()),
sa.PrimaryKeyConstraint("id", name="skill_pkey"),
sa.UniqueConstraint("tenant_id", "name", name="skill_tenant_name_unique"),
)
op.create_index("skills_tenant_updated_at_idx", "skills", ["tenant_id", "updated_at"])
op.create_table(
"skill_draft_files",
_uuid_column("id"),
_uuid_column("skill_id"),
sa.Column("path", sa.String(length=512), nullable=False),
sa.Column("kind", sa.String(length=32), nullable=False),
sa.Column("storage", sa.String(length=32), nullable=True),
sa.Column("mime_type", sa.String(length=255), nullable=True),
sa.Column("content_text", _long_text(), nullable=True),
_uuid_column("tool_file_id", nullable=True),
sa.Column("size", sa.BigInteger(), nullable=True),
sa.Column("hash", sa.String(length=255), nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.func.current_timestamp()),
sa.Column("updated_at", sa.DateTime(), nullable=False, server_default=sa.func.current_timestamp()),
sa.PrimaryKeyConstraint("id", name="skill_draft_file_pkey"),
sa.UniqueConstraint("skill_id", "path", name="skill_draft_file_skill_path_unique"),
)
op.create_index("skill_draft_files_skill_path_idx", "skill_draft_files", ["skill_id", "path"])
op.create_table(
"skill_versions",
_uuid_column("id"),
_uuid_column("skill_id"),
sa.Column("version_number", sa.Integer(), nullable=False),
sa.Column("version_name", sa.String(length=128), nullable=False, server_default=""),
sa.Column("publish_note", sa.String(length=1024), nullable=False, server_default=""),
sa.Column("manifest", _long_text(), nullable=False),
_uuid_column("archive_tool_file_id"),
sa.Column("hash_code", sa.String(length=255), nullable=False),
sa.Column("archive_size", sa.BigInteger(), nullable=False),
_uuid_column("published_by", nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.func.current_timestamp()),
sa.Column("updated_at", sa.DateTime(), nullable=False, server_default=sa.func.current_timestamp()),
sa.PrimaryKeyConstraint("id", name="skill_version_pkey"),
sa.UniqueConstraint("skill_id", "version_number", name="skill_version_skill_number_unique"),
)
op.create_index("skill_versions_skill_created_at_idx", "skill_versions", ["skill_id", "created_at"])
op.create_table(
"agent_skill_bindings",
_uuid_column("id"),
_uuid_column("tenant_id"),
_uuid_column("agent_id"),
_uuid_column("skill_id"),
sa.Column("priority", sa.Integer(), nullable=False),
_uuid_column("created_by", nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.func.current_timestamp()),
sa.Column("updated_at", sa.DateTime(), nullable=False, server_default=sa.func.current_timestamp()),
sa.PrimaryKeyConstraint("id", name="agent_skill_binding_pkey"),
sa.UniqueConstraint("tenant_id", "agent_id", "skill_id", name="agent_skill_binding_unique"),
sa.UniqueConstraint("tenant_id", "agent_id", "priority", name="agent_skill_binding_priority_unique"),
)
op.create_index("agent_skill_bindings_skill_idx", "agent_skill_bindings", ["tenant_id", "skill_id"])
def downgrade() -> None:
op.drop_index("agent_skill_bindings_skill_idx", table_name="agent_skill_bindings")
op.drop_table("agent_skill_bindings")
op.drop_index("skill_versions_skill_created_at_idx", table_name="skill_versions")
op.drop_table("skill_versions")
op.drop_index("skill_draft_files_skill_path_idx", table_name="skill_draft_files")
op.drop_table("skill_draft_files")
op.drop_index("skills_tenant_updated_at_idx", table_name="skills")
op.drop_table("skills")
@@ -18,31 +18,109 @@ branch_labels = None
depends_on = None
def _is_pg(conn) -> bool:
return conn.dialect.name == "postgresql"
def _uuid_column(name: str, *, nullable: bool = False, primary_key: bool = False) -> sa.Column:
kwargs = {"nullable": nullable, "primary_key": primary_key}
if primary_key and _is_pg(op.get_bind()):
kwargs["server_default"] = sa.text("uuidv7()")
return sa.Column(name, models.types.StringUUID(), **kwargs)
def _has_table(table_name: str) -> bool:
return sa.inspect(op.get_bind()).has_table(table_name)
def _has_column(table_name: str, column_name: str) -> bool:
return any(
column["name"] == column_name for column in sa.inspect(op.get_bind()).get_columns(table_name)
)
def _has_unique_constraint(table_name: str, constraint_name: str) -> bool:
return any(
constraint["name"] == constraint_name
for constraint in sa.inspect(op.get_bind()).get_unique_constraints(table_name)
)
def upgrade():
if not _has_table("agent_debug_conversations"):
op.create_table(
"agent_debug_conversations",
_uuid_column("id", primary_key=True),
sa.Column("tenant_id", models.types.StringUUID(), nullable=False),
sa.Column("agent_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("conversation_id", models.types.StringUUID(), nullable=False),
sa.Column(
"draft_type",
sa.String(length=32),
nullable=False,
server_default=sa.text("'debug_build'"),
),
sa.Column("created_at", sa.DateTime(), server_default=sa.func.current_timestamp(), nullable=False),
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.current_timestamp(), nullable=False),
sa.PrimaryKeyConstraint("id", name=op.f("agent_debug_conversation_pkey")),
sa.UniqueConstraint(
"tenant_id",
"agent_id",
"account_id",
"draft_type",
name=op.f("agent_debug_conversation_agent_account_draft_type_unique"),
),
)
op.create_index(
"agent_debug_conversation_conversation_idx",
"agent_debug_conversations",
["conversation_id"],
)
op.create_index(
"agent_debug_conversation_account_idx",
"agent_debug_conversations",
["tenant_id", "account_id"],
)
return
# Existing pointers have always represented Build chat because the Agent
# detail API exposes them as ``debug_conversation_id`` for that surface.
op.add_column(
if not _has_column("agent_debug_conversations", "draft_type"):
op.add_column(
"agent_debug_conversations",
sa.Column(
"draft_type",
sa.String(length=32),
nullable=False,
server_default=sa.text("'debug_build'"),
),
)
if _has_unique_constraint(
"agent_debug_conversations",
sa.Column(
"draft_type",
sa.String(length=32),
nullable=False,
server_default=sa.text("'debug_build'"),
),
)
op.drop_constraint(
"agent_debug_conversation_agent_account_unique",
):
op.drop_constraint(
"agent_debug_conversation_agent_account_unique",
"agent_debug_conversations",
type_="unique",
)
if not _has_unique_constraint(
"agent_debug_conversations",
type_="unique",
)
op.create_unique_constraint(
"agent_debug_conversation_agent_account_draft_type_unique",
"agent_debug_conversations",
["tenant_id", "agent_id", "account_id", "draft_type"],
)
):
op.create_unique_constraint(
"agent_debug_conversation_agent_account_draft_type_unique",
"agent_debug_conversations",
["tenant_id", "agent_id", "account_id", "draft_type"],
)
def downgrade():
if not _has_table("agent_debug_conversations"):
return
debug_conversations = sa.table(
"agent_debug_conversations",
sa.column("tenant_id", models.types.StringUUID()),
@@ -64,14 +142,23 @@ def downgrade():
),
)
)
op.drop_constraint(
if _has_unique_constraint(
"agent_debug_conversations",
"agent_debug_conversation_agent_account_draft_type_unique",
):
op.drop_constraint(
"agent_debug_conversation_agent_account_draft_type_unique",
"agent_debug_conversations",
type_="unique",
)
if not _has_unique_constraint(
"agent_debug_conversations",
type_="unique",
)
op.create_unique_constraint(
"agent_debug_conversation_agent_account_unique",
"agent_debug_conversations",
["tenant_id", "agent_id", "account_id"],
)
op.drop_column("agent_debug_conversations", "draft_type")
):
op.create_unique_constraint(
"agent_debug_conversation_agent_account_unique",
"agent_debug_conversations",
["tenant_id", "agent_id", "account_id"],
)
if _has_column("agent_debug_conversations", "draft_type"):
op.drop_column("agent_debug_conversations", "draft_type")
@@ -0,0 +1,21 @@
"""merge skill and agent debug conversation heads
Revision ID: e9f4a1b2c3d5
Revises: a4f8d2c9e1b0, d2825e7b9c10
Create Date: 2026-07-23 15:00:00.000000
"""
# revision identifiers, used by Alembic.
revision = "e9f4a1b2c3d5"
down_revision = ("a4f8d2c9e1b0", "d2825e7b9c10")
branch_labels = None
depends_on = None
def upgrade():
pass
def downgrade():
pass
+7
View File
@@ -114,6 +114,7 @@ from .provider import (
TenantDefaultModel,
TenantPreferredModelProvider,
)
from .skill import AgentSkillBinding, Skill, SkillDraftFile, SkillFileKind, SkillFileStorage, SkillVersion
from .snippet import CustomizedSnippet, SnippetType
from .source import DataSourceApiKeyAuthBinding, DataSourceOauthBinding
from .task import CeleryTask, CeleryTaskSet
@@ -173,6 +174,7 @@ __all__ = [
"AgentIconType",
"AgentKind",
"AgentScope",
"AgentSkillBinding",
"AgentSource",
"AgentStatus",
"AgentWorkingResourceStatus",
@@ -250,6 +252,11 @@ __all__ = [
"RecommendedApp",
"SavedMessage",
"Site",
"Skill",
"SkillDraftFile",
"SkillFileKind",
"SkillFileStorage",
"SkillVersion",
"SnippetType",
"Tag",
"TagBinding",
+1
View File
@@ -249,6 +249,7 @@ class TagType(StrEnum):
KNOWLEDGE = "knowledge"
APP = "app"
SNIPPET = "snippet"
SKILL = "skill"
class DatasetMetadataType(StrEnum):
+1 -1
View File
@@ -2675,7 +2675,7 @@ class Tag(TypeBase):
sa.Index("tag_name_idx", "name"),
)
TAG_TYPE_LIST = ["knowledge", "app", "snippet"]
TAG_TYPE_LIST = ["knowledge", "app", "snippet", "skill"]
id: Mapped[str] = mapped_column(
StringUUID, insert_default=lambda: str(uuid4()), default_factory=lambda: str(uuid4()), init=False
+167
View File
@@ -0,0 +1,167 @@
"""Workspace-level Skill Management models.
These tables are the source of truth for reusable workspace Skills. Agent Soul
``config_skills`` and Agent Drive skill rows remain per-agent runtime/config
assets; they may consume a published Skill snapshot but do not own the Skill's
draft, metadata, version history, or Agent binding priority.
"""
from enum import StrEnum
import sqlalchemy as sa
from pydantic import BaseModel, ConfigDict
from sqlalchemy import Index, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column
from models.base import Base, DefaultFieldsMixin
from models.types import EnumText, JSONModelColumn, LongText, StringUUID
class SkillFileKind(StrEnum):
"""Draft file entry kind."""
FILE = "file"
DIRECTORY = "directory"
class SkillFileStorage(StrEnum):
"""How a draft file's content is stored."""
TEXT = "text"
TOOL_FILE = "tool_file"
class SkillVersionManifestFile(BaseModel):
"""One file entry captured in a published Skill snapshot manifest."""
path: str
mime_type: str | None = None
size: int
hash: str
model_config = ConfigDict(extra="forbid")
class SkillVersionManifest(BaseModel):
"""Published Skill snapshot file index."""
files: list[SkillVersionManifestFile]
name: str | None = None
display_name: str | None = None
description: str | None = None
model_config = ConfigDict(extra="forbid")
class Skill(DefaultFieldsMixin, Base):
"""Workspace-level reusable Skill metadata and draft status."""
__tablename__ = "skills"
__table_args__ = (
sa.PrimaryKeyConstraint("id", name="skill_pkey"),
UniqueConstraint("tenant_id", "name", name="skill_tenant_name_unique"),
Index("skills_tenant_updated_at_idx", "tenant_id", "updated_at"),
)
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
name: Mapped[str] = mapped_column(sa.String(64), nullable=False)
display_name: Mapped[str] = mapped_column(sa.String(128), nullable=False)
icon: Mapped[str] = mapped_column(sa.String(16), nullable=False, default="📄", server_default="📄")
description: Mapped[str] = mapped_column(sa.String(1024), nullable=False, default="", server_default="")
name_manually_edited: Mapped[bool] = mapped_column(
sa.Boolean,
nullable=False,
default=False,
server_default=sa.false(),
)
visibility: Mapped[str] = mapped_column(
sa.String(32),
nullable=False,
default="workspace",
server_default="workspace",
)
latest_published_version_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
created_by: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
updated_by: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
class SkillDraftFile(DefaultFieldsMixin, Base):
"""One draft file or directory in a workspace Skill."""
__tablename__ = "skill_draft_files"
__table_args__ = (
sa.PrimaryKeyConstraint("id", name="skill_draft_file_pkey"),
UniqueConstraint("skill_id", "path", name="skill_draft_file_skill_path_unique"),
Index("skill_draft_files_skill_path_idx", "skill_id", "path"),
)
skill_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
path: Mapped[str] = mapped_column(sa.String(512), nullable=False)
kind: Mapped[SkillFileKind] = mapped_column(EnumText(SkillFileKind, length=32), nullable=False)
storage: Mapped[SkillFileStorage | None] = mapped_column(EnumText(SkillFileStorage, length=32), nullable=True)
mime_type: Mapped[str | None] = mapped_column(sa.String(255), nullable=True)
content_text: Mapped[str | None] = mapped_column(LongText, nullable=True)
tool_file_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
size: Mapped[int | None] = mapped_column(sa.BigInteger, nullable=True)
hash: Mapped[str | None] = mapped_column(sa.String(255), nullable=True)
class SkillVersion(DefaultFieldsMixin, Base):
"""Immutable published Skill snapshot.
``hash_code`` uniquely identifies a published version for downstream
execution audit. It includes Skill identity, version number, and archive
content digest instead of being only the archive content hash.
"""
__tablename__ = "skill_versions"
__table_args__ = (
sa.PrimaryKeyConstraint("id", name="skill_version_pkey"),
UniqueConstraint("skill_id", "version_number", name="skill_version_skill_number_unique"),
Index("skill_versions_skill_created_at_idx", "skill_id", "created_at"),
)
skill_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
version_number: Mapped[int] = mapped_column(sa.Integer, nullable=False)
version_name: Mapped[str] = mapped_column(sa.String(128), nullable=False, default="", server_default="")
publish_note: Mapped[str] = mapped_column(sa.String(1024), nullable=False, default="", server_default="")
manifest: Mapped[SkillVersionManifest] = mapped_column(JSONModelColumn(SkillVersionManifest), nullable=False)
archive_tool_file_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
hash_code: Mapped[str] = mapped_column(sa.String(255), nullable=False)
archive_size: Mapped[int] = mapped_column(sa.BigInteger, nullable=False)
published_by: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
class AgentSkillBinding(DefaultFieldsMixin, Base):
"""Direct Agent-to-workspace-Skill binding.
``priority`` is retained as an internal ordering column for the current
schema constraints. Runtime Skill selection is Agent-driven and must not
treat it as a matching priority.
"""
__tablename__ = "agent_skill_bindings"
__table_args__ = (
sa.PrimaryKeyConstraint("id", name="agent_skill_binding_pkey"),
UniqueConstraint("tenant_id", "agent_id", "skill_id", name="agent_skill_binding_unique"),
UniqueConstraint("tenant_id", "agent_id", "priority", name="agent_skill_binding_priority_unique"),
Index("agent_skill_bindings_skill_idx", "tenant_id", "skill_id"),
)
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
agent_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
skill_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
priority: Mapped[int] = mapped_column(sa.Integer, nullable=False)
created_by: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
__all__ = [
"AgentSkillBinding",
"Skill",
"SkillDraftFile",
"SkillFileKind",
"SkillFileStorage",
"SkillVersion",
"SkillVersionManifest",
"SkillVersionManifestFile",
]
+801 -2
View File
@@ -9557,7 +9557,7 @@ Remove one or more tag bindings from a target.
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| keyword | query | Search keyword | No | string |
| type | query | Tag type filter | No | string, <br>**Available values:** "", "app", "knowledge", "snippet" |
| type | query | Tag type filter | No | string |
#### Responses
@@ -10086,6 +10086,38 @@ Get list of available agent providers
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [AgentProviderListResponse](#agentproviderlistresponse)<br> |
### [GET] /workspaces/current/agents/{agent_id}/skills
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| agent_id | path | | Yes | string |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Agent Skill bindings | **application/json**: [AgentSkillBindingsResponse](#agentskillbindingsresponse)<br> |
### [PUT] /workspaces/current/agents/{agent_id}/skills
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| agent_id | path | | Yes | string |
#### Request Body
| Required | Schema |
| -------- | ------ |
| Yes | **application/json**: [AgentSkillBindingsPayload](#agentskillbindingspayload)<br> |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Agent Skill bindings replaced | **application/json**: [AgentSkillBindingsResponse](#agentskillbindingsresponse)<br> |
### [GET] /workspaces/current/customized-snippets
**List customized snippets with pagination and search**
@@ -12017,6 +12049,360 @@ Returns permission flags that control workspace features like member invitations
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [WorkspaceAccessMatrix](#workspaceaccessmatrix)<br> |
### [GET] /workspaces/current/skills
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| keyword | query | Search keyword matching skill name or description. | No | string |
| limit | query | Number of items per page. | No | integer, <br>**Default:** 20 |
| page | query | Page number. | No | integer, <br>**Default:** 1 |
| tag | query | Skill tag filters. Repeat the parameter for multiple tags. | No | [ string ] |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Workspace skills | **application/json**: [SkillListResponse](#skilllistresponse)<br> |
### [POST] /workspaces/current/skills
#### Request Body
| Required | Schema |
| -------- | ------ |
| Yes | **application/json**: [SkillCreatePayload](#skillcreatepayload)<br> |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 201 | Skill created | **application/json**: [SkillDetailResponse](#skilldetailresponse)<br> |
### [POST] /workspaces/current/skills/files/upload
#### Request Body
| Required | Schema |
| -------- | ------ |
| Yes | **multipart/form-data**: { **"file"**: binary }<br> |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 201 | Skill draft file uploaded | **application/json**: [SkillFileUploadResponse](#skillfileuploadresponse)<br> |
### [POST] /workspaces/current/skills/import
Import a Skill zip package from multipart form field `file`.
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 201 | Skill imported | **application/json**: [SkillDetailResponse](#skilldetailresponse)<br> |
### [GET] /workspaces/current/skills/tags
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Workspace Skill tags | **application/json**: [SkillTagListResponse](#skilltaglistresponse)<br> |
### [DELETE] /workspaces/current/skills/{skill_id}
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| skill_id | path | | Yes | string |
#### Request Body
| Required | Schema |
| -------- | ------ |
| Yes | **application/json**: [SkillDeletePayload](#skilldeletepayload)<br> |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Skill deleted | **application/json**: [SkillDeleteResponse](#skilldeleteresponse)<br> |
### [GET] /workspaces/current/skills/{skill_id}
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| skill_id | path | | Yes | string |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Skill detail | **application/json**: [SkillDetailResponse](#skilldetailresponse)<br> |
### [PATCH] /workspaces/current/skills/{skill_id}
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| skill_id | path | | Yes | string |
#### Request Body
| Required | Schema |
| -------- | ------ |
| Yes | **application/json**: [SkillMetadataPayload](#skillmetadatapayload)<br> |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Skill updated | **application/json**: [SkillResponse](#skillresponse)<br> |
### [POST] /workspaces/current/skills/{skill_id}/assist/messages
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| skill_id | path | | Yes | string |
#### Request Body
| Required | Schema |
| -------- | ------ |
| Yes | **application/json**: [SkillAssistMessagePayload](#skillassistmessagepayload)<br> |
#### Responses
| Code | Description |
| ---- | ----------- |
| 200 | Skill Authoring assistant event stream |
### [POST] /workspaces/current/skills/{skill_id}/duplicate
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| skill_id | path | | Yes | string |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 201 | Skill duplicated | **application/json**: [SkillDetailResponse](#skilldetailresponse)<br> |
### [GET] /workspaces/current/skills/{skill_id}/export
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| skill_id | path | | Yes | string |
#### Responses
| Code | Description |
| ---- | ----------- |
| 200 | Published Skill zip archive |
### [PATCH] /workspaces/current/skills/{skill_id}/files
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| skill_id | path | | Yes | string |
#### Request Body
| Required | Schema |
| -------- | ------ |
| Yes | **application/json**: [SkillDraftFileOperationPayload](#skilldraftfileoperationpayload)<br> |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Draft file operation applied | **application/json**: [SkillDetailResponse](#skilldetailresponse)<br> |
### [PUT] /workspaces/current/skills/{skill_id}/files
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| skill_id | path | | Yes | string |
#### Request Body
| Required | Schema |
| -------- | ------ |
| Yes | **application/json**: [SkillDraftTreePayload](#skilldrafttreepayload)<br> |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Draft files replaced | **application/json**: [SkillDetailResponse](#skilldetailresponse)<br> |
### [POST] /workspaces/current/skills/{skill_id}/files/check
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| skill_id | path | | Yes | string |
#### Request Body
| Required | Schema |
| -------- | ------ |
| Yes | **application/json**: [SkillDraftFileCheckPayload](#skilldraftfilecheckpayload)<br> |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Draft files checked | **application/json**: [SkillFileCheckResponse](#skillfilecheckresponse)<br> |
### [GET] /workspaces/current/skills/{skill_id}/files/content
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| download | query | Return as an attachment when 1. | No | string |
| path | query | Skill file path relative to the Skill root. | Yes | string |
| version_id | query | Optional published version ID. Omit for current draft. | No | string |
| skill_id | path | | Yes | string |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Skill file content | **application/json**: [BinaryFileResponse](#binaryfileresponse)<br> |
### [GET] /workspaces/current/skills/{skill_id}/files/preview
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| path | query | Skill file path relative to the Skill root. | Yes | string |
| version_id | query | Optional published version ID. Omit for current draft. | No | string |
| skill_id | path | | Yes | string |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Skill file text preview | **application/json**: [SkillFilePreviewResponse](#skillfilepreviewresponse)<br> |
### [POST] /workspaces/current/skills/{skill_id}/publish
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| skill_id | path | | Yes | string |
#### Request Body
| Required | Schema |
| -------- | ------ |
| Yes | **application/json**: [SkillPublishPayload](#skillpublishpayload)<br> |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Skill published | **application/json**: [SkillVersionResponse](#skillversionresponse)<br> |
### [GET] /workspaces/current/skills/{skill_id}/references
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| skill_id | path | | Yes | string |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Skill references | **application/json**: [SkillReferenceListResponse](#skillreferencelistresponse)<br> |
### [POST] /workspaces/current/skills/{skill_id}/restore
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| skill_id | path | | Yes | string |
#### Request Body
| Required | Schema |
| -------- | ------ |
| Yes | **application/json**: [SkillRestorePayload](#skillrestorepayload)<br> |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Skill version restored to draft | **application/json**: [SkillDetailResponse](#skilldetailresponse)<br> |
### [GET] /workspaces/current/skills/{skill_id}/versions
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| skill_id | path | | Yes | string |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Skill versions | **application/json**: [SkillVersionListResponse](#skillversionlistresponse)<br> |
### [DELETE] /workspaces/current/skills/{skill_id}/versions/{version_id}
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| skill_id | path | | Yes | string |
| version_id | path | | Yes | string |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Skill version deleted | **application/json**: [SkillVersionDeleteResponse](#skillversiondeleteresponse)<br> |
### [GET] /workspaces/current/skills/{skill_id}/versions/{version_id}
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| skill_id | path | | Yes | string |
| version_id | path | | Yes | string |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Skill version detail | **application/json**: [SkillVersionDetailResponse](#skillversiondetailresponse)<br> |
### [PATCH] /workspaces/current/skills/{skill_id}/versions/{version_id}
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| skill_id | path | | Yes | string |
| version_id | path | | Yes | string |
#### Request Body
| Required | Schema |
| -------- | ------ |
| Yes | **application/json**: [SkillVersionUpdatePayload](#skillversionupdatepayload)<br> |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Skill version updated | **application/json**: [SkillVersionResponse](#skillversionresponse)<br> |
### [GET] /workspaces/current/summary
#### Responses
@@ -14760,6 +15146,37 @@ Visibility and lifecycle scope of an Agent record.
| ---- | ---- | ----------- | -------- |
| result | string | | Yes |
#### AgentSkillBindingItemResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| description | string | | Yes |
| display_name | string | | Yes |
| file_count | integer | | Yes |
| icon | string | | Yes |
| id | string | | Yes |
| latest_published_at | integer | | No |
| latest_published_version_id | string | | No |
| name | string | | Yes |
| priority | integer | | Yes |
| status | string | | Yes |
| tags | [ string ] | | No |
| updated_at | integer | | Yes |
#### AgentSkillBindingsPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| skill_ids | [ string ] | Ordered Skill IDs bound to the Agent. | No |
#### AgentSkillBindingsResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| agent_id | string | | Yes |
| data | [ [AgentSkillBindingItemResponse](#agentskillbindingitemresponse) ] | | No |
| skill_ids | [ string ] | | No |
#### AgentSkillRefConfig
| Name | Type | Description | Required |
@@ -21852,6 +22269,238 @@ Simple provider entity response.
| title | string | | Yes |
| use_icon_as_answer_icon | boolean | | Yes |
#### SkillAssistAttachmentPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| mime_type | string | | No |
| name | string | | Yes |
| size | integer | | No |
| tool_file_id | string | | Yes |
#### SkillAssistHistoryMessagePayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| content | string | | Yes |
| role | string, <br>**Available values:** "assistant", "user" | *Enum:* `"assistant"`, `"user"` | Yes |
| suggested_display_name | string | | No |
| suggested_name | string | | No |
#### SkillAssistMessagePayload
One user message and optional uploaded context for the Skill Authoring assistant.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| attachments | [ [SkillAssistAttachmentPayload](#skillassistattachmentpayload) ] | | No |
| history | [ [SkillAssistHistoryMessagePayload](#skillassisthistorymessagepayload) ] | | No |
| message | string | | Yes |
| model | [SkillAssistModelPayload](#skillassistmodelpayload) | | No |
| target_path | string | | No |
#### SkillAssistModelPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| model | string | | Yes |
| model_settings | object | | No |
| plugin_id | string | | No |
| provider | string | | Yes |
#### SkillCreatePayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| description | string | | No |
| display_name | string | | No |
| icon | string, <br>**Default:** 📄 | | No |
| name | string | | No |
| tags | [ string ] | | No |
#### SkillDeletePayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| confirmation_name | string | Required when deleting a referenced Skill. Must match the Skill display name. | No |
#### SkillDeleteResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| deleted | boolean | | Yes |
| id | string | | Yes |
#### SkillDetailResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| created_at | integer | | Yes |
| created_by | string | | No |
| created_by_name | string | | No |
| description | string | | Yes |
| display_name | string | | Yes |
| files | [ [SkillFileResponse](#skillfileresponse) ] | | No |
| icon | string | | Yes |
| id | string | | Yes |
| latest_published_at | integer | | No |
| latest_published_version_id | string | | No |
| latest_published_version_number | integer | | No |
| name | string | | Yes |
| name_manually_edited | boolean | | No |
| reference_count | integer | | No |
| tags | [ string ] | | No |
| updated_at | integer | | Yes |
| updated_by | string | | No |
| updated_by_name | string | | No |
| visibility | string | | Yes |
#### SkillDraftFileCheckItemPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| filename | string | | Yes |
| mime_type | string | | No |
| path | string | Target draft path. Defaults to filename. | No |
| size | integer | | Yes |
#### SkillDraftFileCheckPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| files | [ [SkillDraftFileCheckItemPayload](#skilldraftfilecheckitempayload) ] | | No |
#### SkillDraftFileOperation
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| SkillDraftFileOperation | string | | |
#### SkillDraftFileOperationPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| content | string | | No |
| expected_updated_at | integer | | No |
| hash | string | | No |
| mime_type | string | | No |
| operation | [SkillDraftFileOperation](#skilldraftfileoperation) | | Yes |
| path | string | | Yes |
| size | integer | | No |
| target_path | string | | No |
| tool_file_id | string | | No |
#### SkillDraftTreeItemPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| content | string | | No |
| hash | string | | No |
| kind | [SkillFileKind](#skillfilekind) | | No |
| mime_type | string | | No |
| path | string | | Yes |
| size | integer | | No |
| storage | [SkillFileStorage](#skillfilestorage) | | No |
| tool_file_id | string | | No |
#### SkillDraftTreePayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| expected_updated_at | integer | | No |
| files | [ [SkillDraftTreeItemPayload](#skilldrafttreeitempayload) ] | | No |
#### SkillFileCheckErrorResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| code | string | | Yes |
| message | string | | Yes |
#### SkillFileCheckItemResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| errors | [ [SkillFileCheckErrorResponse](#skillfilecheckerrorresponse) ] | | No |
| extension | string | | Yes |
| filename | string | | Yes |
| mime_type | string | | Yes |
| path | string | | Yes |
| size | integer | | Yes |
#### SkillFileCheckResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| data | object | | No |
#### SkillFileKind
Draft file entry kind.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| SkillFileKind | string | Draft file entry kind. | |
#### SkillFilePreviewResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| content | string | | Yes |
| hash | string | | Yes |
| mime_type | string | | Yes |
| path | string | | Yes |
| size | integer | | Yes |
#### SkillFileQuery
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| path | string | Skill file path relative to the Skill root. | Yes |
| version_id | string | Optional published version ID. Omit for current draft. | No |
#### SkillFileResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| content | string | | No |
| hash | string | | No |
| id | string | | No |
| kind | string | | Yes |
| mime_type | string | | No |
| path | string | | Yes |
| size | integer | | No |
| storage | string | | No |
| tool_file_id | string | | No |
#### SkillFileStorage
How a draft file's content is stored.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| SkillFileStorage | string | How a draft file's content is stored. | |
#### SkillFileUploadResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| hash | string | | Yes |
| id | string | | Yes |
| mime_type | string | | Yes |
| name | string | | Yes |
| size | integer | | Yes |
#### SkillListResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| data | [ [SkillResponse](#skillresponse) ] | | No |
| has_more | boolean | | No |
| limit | integer, <br>**Default:** 20 | | No |
| page | integer, <br>**Default:** 1 | | No |
| total | integer | | No |
#### SkillManifest
Validated metadata extracted from a Skill package.
@@ -21865,6 +22514,93 @@ Validated metadata extracted from a Skill package.
| name | string | | Yes |
| size | integer | | Yes |
#### SkillMetadataPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| display_name | string | | No |
| expected_updated_at | integer | | No |
| icon | string | | No |
| tags | [ string ] | | No |
#### SkillPublishPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| publish_note | string | | No |
| version_name | string | | No |
#### SkillReferenceListResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| data | [ [SkillReferenceResponse](#skillreferenceresponse) ] | | No |
#### SkillReferenceResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| agent_icon | string | | No |
| agent_icon_background | string | | No |
| agent_icon_type | string | | No |
| agent_id | string | | Yes |
| app_id | string | | No |
| display_name | string | | Yes |
| name | string | | Yes |
| node_id | string | | No |
| node_name | string | | No |
| type | string | | Yes |
| workflow_icon | string | | No |
| workflow_icon_background | string | | No |
| workflow_icon_type | string | | No |
| workflow_id | string | | No |
| workflow_name | string | | No |
| workflow_version | string | | No |
#### SkillResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| created_at | integer | | Yes |
| created_by | string | | No |
| created_by_name | string | | No |
| description | string | | Yes |
| display_name | string | | Yes |
| icon | string | | Yes |
| id | string | | Yes |
| latest_published_at | integer | | No |
| latest_published_version_id | string | | No |
| latest_published_version_number | integer | | No |
| name | string | | Yes |
| name_manually_edited | boolean | | No |
| reference_count | integer | | No |
| tags | [ string ] | | No |
| updated_at | integer | | Yes |
| updated_by | string | | No |
| updated_by_name | string | | No |
| visibility | string | | Yes |
#### SkillRestorePayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| publish_note | string | | No |
| version_id | string | | Yes |
| version_name | string | | No |
#### SkillTagListResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| data | [ [SkillTagResponse](#skilltagresponse) ] | | No |
#### SkillTagResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| count | integer | | Yes |
| tag | string | | Yes |
#### SkillToolInferenceResult
| Name | Type | Description | Required |
@@ -21873,6 +22609,60 @@ Validated metadata extracted from a Skill package.
| inferable | boolean | | Yes |
| reason | string | | No |
#### SkillVersionDeleteResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| deleted | boolean | | Yes |
| id | string | | Yes |
| latest_published_version_id | string | | No |
#### SkillVersionDetailResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| archive_size | integer | | Yes |
| created_at | integer | | Yes |
| files | [ [SkillFileResponse](#skillfileresponse) ] | | No |
| hash_code | string | | Yes |
| id | string | | Yes |
| is_latest | boolean | | No |
| publish_note | string | | Yes |
| published_by | string | | No |
| published_by_name | string | | No |
| skill_id | string | | Yes |
| version_name | string | | Yes |
| version_number | integer | | Yes |
#### SkillVersionListResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| data | [ [SkillVersionResponse](#skillversionresponse) ] | | No |
#### SkillVersionResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| archive_size | integer | | Yes |
| created_at | integer | | Yes |
| hash_code | string | | Yes |
| id | string | | Yes |
| is_latest | boolean | | No |
| publish_note | string | | Yes |
| published_by | string | | No |
| published_by_name | string | | No |
| skill_id | string | | Yes |
| version_name | string | | Yes |
| version_number | integer | | Yes |
#### SkillVersionUpdatePayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| publish_note | string | | No |
| version_name | string | | No |
#### SnippetDependencyCheckResponse
| Name | Type | Description | Required |
@@ -22347,7 +23137,7 @@ Non-sensitive bootstrap snapshot exposed before Console or Web authentication.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| keyword | string | Search keyword | No |
| type | string, <br>**Available values:** "", "app", "knowledge", "snippet" | Tag type filter<br>*Enum:* `""`, `"app"`, `"knowledge"`, `"snippet"` | No |
| type | [TagType](#tagtype)<br>string | Tag type filter | No |
#### TagListResponse
@@ -24543,6 +25333,15 @@ Workflow tool configuration
| ---- | ---- | ----------- | -------- |
| permission_keys | [ string ] | | No |
#### WorkspaceSkillsQuery
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| keyword | string | Search keyword matching skill name or description. | No |
| limit | integer, <br>**Default:** 20 | Number of items per page. | No |
| page | integer, <br>**Default:** 1 | Page number. | No |
| tag | [ string ] | Skill tag filters. Repeat the parameter for multiple tags. | No |
#### WorkspaceTenantResultResponse
| Name | Type | Description | Required |
+3 -1
View File
@@ -1614,7 +1614,9 @@ class TenantService:
return updated_accounts
@staticmethod
def iter_member_account_id_batches(tenant_id: str, batch_size: int, *, session: Session) -> Iterator[list[str]]:
def iter_member_account_id_batches(
tenant_id: str, batch_size: int, *, session: Session
) -> Iterator[list[str]]:
"""Yield workspace member account ids in bounded, ordered batches."""
offset = 0
while True:
+46 -20
View File
@@ -19,12 +19,11 @@ from __future__ import annotations
import hashlib
import io
import posixpath
import re
import zipfile
import zlib
import yaml
from pydantic import BaseModel
from pydantic import BaseModel, Field, ValidationError, field_validator
from configs import dify_config
@@ -34,7 +33,8 @@ _MAX_SKILL_MD_BYTES = 1 * 1024 * 1024
_MAX_ENTRIES = 5000
_ALLOWED_EXTENSIONS = (".zip", ".skill")
_SKILL_MD_NAME = "SKILL.md"
_HEADING_RE = re.compile(r"^\s*#\s+(.+?)\s*$", re.MULTILINE)
_SKILL_NAME_PATTERN = r"^[a-z0-9]+(?:-[a-z0-9]+)*$"
_MAX_SKILL_DESCRIPTION_LENGTH = 1024
class SkillPackageError(Exception):
@@ -54,13 +54,18 @@ class SkillPackageError(Exception):
class SkillManifest(BaseModel):
"""Validated metadata extracted from a Skill package."""
name: str
description: str
name: str = Field(min_length=1, max_length=64, pattern=_SKILL_NAME_PATTERN)
description: str = Field(min_length=1, max_length=_MAX_SKILL_DESCRIPTION_LENGTH)
entry_path: str # path of SKILL.md inside the archive
files: list[str] # all (safe) file paths inside the archive
size: int # total uncompressed bytes
hash: str # sha256 of the archive bytes
@field_validator("name", "description", mode="before")
@classmethod
def _strip_required_string(cls, value: object) -> object:
return value.strip() if isinstance(value, str) else value
class NormalizedSkillPackage(BaseModel):
"""Canonical skill package bytes and metadata ready to store in agent drive."""
@@ -109,14 +114,17 @@ class SkillPackageService:
normalized_size = sum(max(info.file_size, 0) for info in normalized_members.values())
name, description = self._parse_skill_md(skill_md)
manifest = SkillManifest(
name=name,
description=description,
entry_path=_SKILL_MD_NAME,
files=sorted(normalized_members),
size=normalized_size,
hash=hashlib.sha256(normalized_archive_bytes).hexdigest(),
)
try:
manifest = SkillManifest(
name=name,
description=description,
entry_path=_SKILL_MD_NAME,
files=sorted(normalized_members),
size=normalized_size,
hash=hashlib.sha256(normalized_archive_bytes).hexdigest(),
)
except ValidationError as exc:
raise self._manifest_validation_error(exc) from exc
return NormalizedSkillPackage(
manifest=manifest,
archive_bytes=normalized_archive_bytes,
@@ -124,6 +132,31 @@ class SkillPackageService:
strip_prefix=strip_prefix,
)
@staticmethod
def _manifest_validation_error(exc: ValidationError) -> SkillPackageError:
first_error = exc.errors()[0]
loc = first_error["loc"]
field = loc[0] if loc else "manifest"
error_type = first_error["type"]
if field == "name":
code = "missing_skill_name" if error_type == "string_too_short" else "invalid_skill_name"
message = (
"SKILL.md frontmatter name is required"
if code == "missing_skill_name"
else "SKILL.md frontmatter name must be lowercase letters, numbers, and hyphens only, "
"must not start or end with a hyphen, and must be at most 64 characters"
)
return SkillPackageError(code, message, status_code=400)
if field == "description":
code = "missing_skill_description" if error_type == "string_too_short" else "invalid_skill_description"
message = (
"SKILL.md frontmatter description is required"
if code == "missing_skill_description"
else f"SKILL.md frontmatter description must be at most {_MAX_SKILL_DESCRIPTION_LENGTH} characters"
)
return SkillPackageError(code, message, status_code=400)
return SkillPackageError("invalid_skill_manifest", "SKILL.md frontmatter is invalid", status_code=400)
def _open_archive(self, *, content: bytes, filename: str) -> zipfile.ZipFile:
self._check_extension(filename)
if not content:
@@ -282,13 +315,6 @@ class SkillPackageService:
frontmatter = cls._parse_frontmatter(content)
name = str(frontmatter.get("name") or "").strip()
description = str(frontmatter.get("description") or "").strip()
if not name:
heading = _HEADING_RE.search(content)
name = heading.group(1).strip() if heading else ""
if not name:
raise SkillPackageError(
"missing_skill_name", "SKILL.md must declare a name (frontmatter or top heading)", status_code=400
)
return name, description
@staticmethod
+196 -28
View File
@@ -37,6 +37,7 @@ from core.db.session_factory import session_factory as default_session_factory
from core.tools.signature import bind_file_uri, sign_tool_file_uri
from core.tools.tool_file_manager import ToolFileManager
from extensions.ext_storage import storage
from factories import file_factory
from models.agent import Agent, AgentConfigDraft, AgentConfigDraftType, AgentConfigSnapshot
from models.agent_config_entities import (
AgentConfigFileRefConfig,
@@ -51,6 +52,7 @@ from models.tools import ToolFile
from services.agent.config_skill_normalize_service import ConfigSkillNormalizeService
from services.agent.skill_package_service import SkillPackageError
from services.agent_drive_service import DriveFileRef
from services.skill_management_service import SkillManagementService, SkillManagementServiceError
class AgentConfigVersionKind(StrEnum):
@@ -103,6 +105,7 @@ class ConfigPushPayload(BaseModel):
@dataclass(slots=True)
class AgentConfigTarget:
tenant_id: str
agent_id: str
version_id: str
kind: AgentConfigVersionKind
@@ -129,17 +132,10 @@ class ConfigDownloadRequest:
class AgentConfigService:
"""Read and update Agent Soul-backed config assets for one version target.
The service owns the lifecycle of its database sessions. Callers may inject
a session creator for an alternate engine; production defaults to the
application-wide session factory.
"""
"""Read and update Agent Soul-backed config assets for one version target."""
PREVIEW_MAX_BYTES = 64 * 1024
_session_factory: Callable[[], Session]
def __init__(
self,
*,
@@ -147,8 +143,6 @@ class AgentConfigService:
skill_normalize_service: ConfigSkillNormalizeService | None = None,
session_factory: Callable[[], Session] | None = None,
) -> None:
"""Initialize external collaborators and the service-owned session creator."""
self._tool_files = tool_file_manager or ToolFileManager()
self._skill_normalizer = skill_normalize_service or ConfigSkillNormalizeService()
self._session_factory = session_factory or default_session_factory.create_session
@@ -172,6 +166,7 @@ class AgentConfigService:
user_id=user_id,
)
return AgentConfigTarget(
tenant_id=tenant_id,
agent_id=target.agent_id,
version_id=target.version_id,
kind=target.kind,
@@ -217,7 +212,7 @@ class AgentConfigService:
return {
"agent_id": target.agent_id,
"config_version": self._config_version_payload(target),
"items": [self._serialize_skill_item(skill) for skill in target.agent_soul.config_skills],
"items": self._skill_items_for_target(target, include_runtime_workspace_skills=False),
}
def list_files(
@@ -253,8 +248,6 @@ class AgentConfigService:
name: str,
user_id: str | None = None,
) -> ConfigDownloadRequest:
"""Authorize one Config reference and return origin-free download metadata."""
target = self.resolve_target(
tenant_id=tenant_id,
agent_id=agent_id,
@@ -263,12 +256,39 @@ class AgentConfigService:
user_id=user_id,
)
if kind == "skill":
skill = self._require_skill(target.agent_soul, name=name)
try:
skill = self._require_skill(target.agent_soul, name=name)
return self._resolve_download_request(
tenant_id=tenant_id,
file_kind=skill.file_kind,
file_id=self._available_skill_file_id(skill),
filename=f"{skill.name}.zip",
default_mime_type="application/zip",
missing_code="config_skill_not_found",
missing_message="config skill payload is missing",
)
except AgentConfigServiceError as exc:
if exc.code != "config_skill_not_found":
raise
runtime_skill = next(
(
item
for item in SkillManagementService().list_runtime_agent_skills(
tenant_id=tenant_id,
agent_id=agent_id,
)
if item["name"] == name
),
None,
)
if runtime_skill is None:
raise AgentConfigServiceError("config_skill_not_found", "config skill not found", status_code=404)
return self._resolve_download_request(
tenant_id=tenant_id,
file_kind=skill.file_kind,
file_id=self._available_skill_file_id(skill),
filename=f"{skill.name}.zip",
file_kind="tool_file",
file_id=str(runtime_skill["file_id"]),
filename=f"{runtime_skill['name']}.zip",
default_mime_type="application/zip",
missing_code="config_skill_not_found",
missing_message="config skill payload is missing",
@@ -285,6 +305,45 @@ class AgentConfigService:
missing_message="config file payload is missing",
)
def pull_skill(
self,
*,
tenant_id: str,
agent_id: str,
config_version_id: str,
config_version_kind: AgentConfigVersionKind,
name: str,
user_id: str | None = None,
) -> ConfigDownload:
target = self.resolve_target(
tenant_id=tenant_id,
agent_id=agent_id,
config_version_id=config_version_id,
config_version_kind=config_version_kind,
user_id=user_id,
)
try:
skill = self._require_skill(target.agent_soul, name=name)
file_id = self._available_skill_file_id(skill)
payload, mime_type = self._load_tool_file_bytes(tenant_id=tenant_id, file_id=file_id)
return ConfigDownload(
filename=f"{skill.name}.zip",
mime_type=mime_type or "application/zip",
payload=payload,
)
except AgentConfigServiceError as exc:
if exc.code != "config_skill_not_found":
raise
try:
result = SkillManagementService().pull_runtime_agent_skill(
tenant_id=tenant_id,
agent_id=agent_id,
name=name,
)
return ConfigDownload(filename=result.filename, mime_type=result.mime_type, payload=result.payload)
except SkillManagementServiceError as exc:
raise AgentConfigServiceError("config_skill_not_found", "config skill not found", status_code=404) from exc
def download_skill_url(
self,
*,
@@ -323,9 +382,45 @@ class AgentConfigService:
config_version_kind=config_version_kind,
user_id=user_id,
)
skill = self._require_skill(target.agent_soul, name=name)
file_id = self._available_skill_file_id(skill)
archive_bytes, _mime_type = self._load_tool_file_bytes(tenant_id=tenant_id, file_id=file_id)
try:
skill = self._require_skill(target.agent_soul, name=name)
file_id = self._available_skill_file_id(skill)
archive_bytes, _mime_type = self._load_tool_file_bytes(tenant_id=tenant_id, file_id=file_id)
skill_item = self._serialize_skill_item(skill)
except AgentConfigServiceError as exc:
if exc.code != "config_skill_not_found":
raise
try:
workspace_archive = SkillManagementService().pull_runtime_agent_skill(
tenant_id=tenant_id,
agent_id=agent_id,
name=name,
)
except SkillManagementServiceError as skill_exc:
raise AgentConfigServiceError(
"config_skill_not_found",
"config skill not found",
status_code=404,
) from skill_exc
archive_bytes = workspace_archive.payload
skill_item = next(
(
item
for item in SkillManagementService().list_runtime_agent_skills(
tenant_id=tenant_id,
agent_id=agent_id,
)
if item["name"] == name
),
{
"id": name,
"name": name,
"description": "",
"size": None,
"hash": None,
"mime_type": "application/zip",
},
)
try:
archive_items, skill_md = self._inspect_skill_archive(archive_bytes)
except (OSError, ValueError, zipfile.BadZipFile) as exc:
@@ -335,7 +430,7 @@ class AgentConfigService:
status_code=500,
) from exc
return {
**self._serialize_skill_item(skill),
**skill_item,
"source": "config_skill_zip",
"files": archive_items,
"skill_md": skill_md,
@@ -430,6 +525,34 @@ class AgentConfigService:
)
return member_path
def pull_file(
self,
*,
tenant_id: str,
agent_id: str,
config_version_id: str,
config_version_kind: AgentConfigVersionKind,
name: str,
user_id: str | None = None,
) -> ConfigDownload:
target = self.resolve_target(
tenant_id=tenant_id,
agent_id=agent_id,
config_version_id=config_version_id,
config_version_kind=config_version_kind,
user_id=user_id,
)
file_ref = self._require_file(target.agent_soul, name=name)
file_id = self._available_file_id(file_ref)
payload, filename, mime_type = self._load_file_ref_bytes(
tenant_id=tenant_id,
file_kind=file_ref.file_kind,
file_id=file_id,
)
return ConfigDownload(
filename=filename or file_ref.name, mime_type=mime_type or "application/octet-stream", payload=payload
)
def download_file_url(
self,
*,
@@ -852,6 +975,7 @@ class AgentConfigService:
status_code=404,
)
return AgentConfigTarget(
tenant_id=tenant_id,
agent_id=agent_id,
version_id=version.id,
kind=config_version_kind,
@@ -1147,7 +1271,7 @@ class AgentConfigService:
"agent_id": target.agent_id,
"config_version": AgentConfigService._config_version_payload(target),
"skills": {
"items": [AgentConfigService._serialize_skill_item(skill) for skill in target.agent_soul.config_skills]
"items": AgentConfigService._skill_items_for_target(target, include_runtime_workspace_skills=True)
},
"files": {
"items": [
@@ -1158,6 +1282,25 @@ class AgentConfigService:
"note": target.agent_soul.config_note,
}
@staticmethod
def _skill_items_for_target(
target: AgentConfigTarget, *, include_runtime_workspace_skills: bool
) -> list[dict[str, object]]:
items = [AgentConfigService._serialize_skill_item(skill) for skill in target.agent_soul.config_skills]
if not include_runtime_workspace_skills:
return items
seen_names = {str(item["name"]) for item in items}
for item in SkillManagementService().list_runtime_agent_skills(
tenant_id=target.tenant_id,
agent_id=target.agent_id,
):
if item["name"] in seen_names:
continue
seen_names.add(str(item["name"]))
items.append(item)
return items
@staticmethod
def _config_version_payload(target: AgentConfigTarget) -> dict[str, object]:
return {
@@ -1426,12 +1569,37 @@ class AgentConfigService:
query.append(("as_attachment", "true"))
uri = urllib.parse.urlunsplit(parsed._replace(query=urllib.parse.urlencode(query)))
return ConfigDownloadRequest(
filename=filename,
mime_type=mime_type,
size=size,
download_uri=uri,
)
return ConfigDownloadRequest(filename=filename, mime_type=mime_type, size=size, download_uri=uri)
@staticmethod
def _resolve_download_url(
*, tenant_id: str, file_kind: Literal["upload_file", "tool_file"], file_id: str
) -> str | None:
controller = DatabaseFileAccessController()
from core.app.workflow.file_runtime import DifyWorkflowFileRuntime
runtime = DifyWorkflowFileRuntime(file_access_controller=controller)
try:
if file_kind == "upload_file":
return runtime.resolve_upload_file_url(
upload_file_id=file_id,
for_external=True,
as_attachment=True,
)
file = file_factory.build_from_mapping(
mapping={"transfer_method": "tool_file", "tool_file_id": file_id},
tenant_id=tenant_id,
access_controller=controller,
)
url = runtime.resolve_file_url(file=file, for_external=True)
if not url:
return None
parsed = urllib.parse.urlsplit(url)
query = urllib.parse.parse_qsl(parsed.query, keep_blank_values=True)
query.append(("as_attachment", "true"))
return urllib.parse.urlunsplit(parsed._replace(query=urllib.parse.urlencode(query)))
except ValueError:
return None
__all__ = [
+9
View File
@@ -37,6 +37,7 @@ from models.agent import (
AgentWorkspaceBinding,
)
from models.model import App, AppMode, AppModelConfig, IconType, Site, load_annotation_reply_config
from models.skill import AgentSkillBinding
from models.tools import ApiToolProvider
from models.workflow import Workflow
from services.agent.errors import AgentAccessNotReadyError, AgentNameConflictError
@@ -999,6 +1000,14 @@ class AppService:
Agent.status == AgentStatus.ACTIVE,
)
).all()
agent_ids_to_unbind = [*workflow_agent_ids]
if backing_agent is not None:
agent_ids_to_unbind.append(backing_agent.id)
if agent_ids_to_unbind:
session.query(AgentSkillBinding).filter(
AgentSkillBinding.tenant_id == app.tenant_id,
AgentSkillBinding.agent_id.in_(agent_ids_to_unbind),
).delete(synchronize_session=False)
account_id = current_user.id if current_user else None
if backing_agent is not None:
now = naive_utc_now()
+14
View File
@@ -304,6 +304,10 @@ class MyPermissionsResponse(_RBACModel):
# Fallback permission snapshots for legacy Dify tenant roles when external RBAC is disabled.
# Keep these keys aligned with langgenius/rbac's built-in workspace roles and access policies.
_LEGACY_WORKSPACE_OWNER_KEYS: list[str] = [
"skill.view",
"skill.edit",
"skill.publish",
"skill.delete",
"workspace.member.manage",
"workspace.role.manage",
"data_source.manage",
@@ -334,6 +338,10 @@ _LEGACY_WORKSPACE_OWNER_KEYS: list[str] = [
]
_LEGACY_WORKSPACE_ADMIN_KEYS: list[str] = [
"skill.view",
"skill.edit",
"skill.publish",
"skill.delete",
"workspace.member.manage",
"workspace.role.manage",
"data_source.manage",
@@ -362,6 +370,10 @@ _LEGACY_WORKSPACE_ADMIN_KEYS: list[str] = [
]
_LEGACY_WORKSPACE_EDITOR_KEYS: list[str] = [
"skill.view",
"skill.edit",
"skill.publish",
"skill.delete",
"api_extension.manage",
"plugin.install",
"credential.use",
@@ -377,6 +389,7 @@ _LEGACY_WORKSPACE_EDITOR_KEYS: list[str] = [
]
_LEGACY_WORKSPACE_NORMAL_KEYS: list[str] = [
"skill.view",
"api_extension.manage",
"plugin.install",
"credential.use",
@@ -384,6 +397,7 @@ _LEGACY_WORKSPACE_NORMAL_KEYS: list[str] = [
]
_LEGACY_WORKSPACE_DATASET_OPERATOR_KEYS: list[str] = [
"skill.view",
"plugin.install",
"dataset.create_and_management",
"dataset.external.connect",
File diff suppressed because it is too large Load Diff
+9
View File
@@ -12,6 +12,7 @@ from werkzeug.exceptions import NotFound
from models.dataset import Dataset
from models.enums import TagType
from models.model import App, Tag, TagBinding
from models.skill import Skill
from models.snippet import CustomizedSnippet
type _TagTypeLike = TagType | str
@@ -282,5 +283,13 @@ class TagService:
)
if not snippet:
raise NotFound("Snippet not found")
elif type == "skill":
skill = session.scalar(
select(Skill)
.where(Skill.tenant_id == current_user.current_tenant_id, Skill.id == target_id)
.limit(1)
)
if not skill:
raise NotFound("Skill not found")
else:
raise NotFound("Invalid binding type")
File diff suppressed because it is too large Load Diff
@@ -39,6 +39,7 @@ from core.workflow.system_variables import build_system_variables
from graphon.entities import WorkflowStartReason
from graphon.enums import WorkflowExecutionStatus, WorkflowNodeExecutionStatus
from graphon.runtime import GraphRuntimeState, VariablePool
from libs.datetime_utils import to_utc_timestamp
from models.account import Account
from models.enums import CreatorUserRole
from models.human_input import HumanInputForm
@@ -679,7 +680,7 @@ class TestHitlServiceApi:
assert pause_resp.data.reasons[0]["TYPE"] == "human_input_required"
assert pause_resp.data.reasons[0]["form_id"] == "form-1"
assert pause_resp.data.reasons[0]["form_token"] == "token"
assert pause_resp.data.reasons[0]["expiration_time"] == int(expiration_time.timestamp())
assert pause_resp.data.reasons[0]["expiration_time"] == to_utc_timestamp(expiration_time)
assert isinstance(responses[0], HumanInputRequiredResponse)
hi_resp = responses[0]
@@ -690,7 +691,7 @@ class TestHitlServiceApi:
assert hi_resp.data.actions[0].id == "approve"
assert hi_resp.data.display_in_ui is True
assert hi_resp.data.form_token == "token"
assert hi_resp.data.expiration_time == int(expiration_time.timestamp())
assert hi_resp.data.expiration_time == to_utc_timestamp(expiration_time)
# Snapshot payload contract
@pytest.mark.parametrize("sqlite_session", [(HumanInputForm,)], indirect=True)
@@ -748,13 +749,13 @@ class TestHitlServiceApi:
]
assert events[2]["data"]["status"] == WorkflowNodeExecutionStatus.PAUSED.value
assert events[3]["data"]["form_token"] == "wtok"
assert events[3]["data"]["expiration_time"] == int(expiration_time.timestamp())
assert events[3]["data"]["expiration_time"] == to_utc_timestamp(expiration_time)
pause_data = events[-1]["data"]
assert pause_data["paused_nodes"] == ["node-1"]
assert pause_data["outputs"] == {"result": "value"}
assert pause_data["reasons"][0]["TYPE"] == "human_input_required"
assert pause_data["reasons"][0]["form_token"] == "wtok"
assert pause_data["reasons"][0]["expiration_time"] == int(expiration_time.timestamp())
assert pause_data["reasons"][0]["expiration_time"] == to_utc_timestamp(expiration_time)
assert pause_data["status"] == WorkflowExecutionStatus.PAUSED.value
assert pause_data["created_at"] == int(workflow_run.created_at.timestamp())
assert pause_data["elapsed_time"] == workflow_run.elapsed_time
@@ -7,6 +7,7 @@ from types import SimpleNamespace
from typing import Any
import pytest
from dify_agent.layers.config import DifyConfigSkillConfig
from dify_agent.layers.dify_core_tools import DifyCoreToolConfig, DifyCoreToolsLayerConfig
from dify_agent.layers.dify_plugin import DifyPluginToolConfig, DifyPluginToolsLayerConfig
from dify_agent.layers.execution_context import DifyExecutionContextLayerConfig
@@ -29,6 +30,14 @@ from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom
from models.agent_config_entities import AgentSoulConfig
@pytest.fixture(autouse=True)
def _no_runtime_agent_skills(monkeypatch: pytest.MonkeyPatch):
monkeypatch.setattr(
"core.app.apps.agent_app.runtime_request_builder.load_runtime_agent_skill_configs",
lambda **_kwargs: [],
)
def _exec_ctx() -> DifyExecutionContextLayerConfig:
return DifyExecutionContextLayerConfig(
tenant_id="tenant-1",
@@ -511,6 +520,33 @@ class TestAgentAppConfigLayer:
"mentioned_file_names": [],
}
def test_config_layer_includes_bound_workspace_skills(self, monkeypatch: pytest.MonkeyPatch):
monkeypatch.setattr(
"core.app.apps.agent_app.runtime_request_builder.load_runtime_agent_skill_configs",
lambda **_kwargs: [
DifyConfigSkillConfig(
name="workspace-skill",
description="Bound workspace skill.",
size=123,
mime_type="application/zip",
)
],
)
soul = _soul_with_model()
soul.prompt.system_prompt = "Use [§skill:workspace-skill:Workspace Skill§]."
builder = AgentAppRuntimeRequestBuilder(
credentials_provider=_FakeCredentialsProvider(),
dify_tools_builder=_NoToolsBuilder(), # type: ignore[arg-type]
)
result = builder.build(_ctx(soul))
config = next(layer for layer in result.request.composition.layers if layer.name == DIFY_CONFIG_LAYER_ID)
assert [skill.name for skill in config.config.skills] == ["workspace-skill"]
assert config.config.mentioned_skill_names == ["workspace-skill"]
prompt_layer = next(layer for layer in result.request.composition.layers if layer.name == "agent_soul_prompt")
assert prompt_layer.config.prefix == "Use workspace-skill."
@pytest.mark.parametrize(
("system_prompt", "expected_prefix"),
[
@@ -5,6 +5,7 @@ from typing import cast
import pytest
from agenton.compositor import CompositorSessionSnapshot
from dify_agent.layers.config import DifyConfigSkillConfig
from dify_agent.layers.dify_core_tools import DifyCoreToolConfig, DifyCoreToolsLayerConfig
from dify_agent.layers.dify_plugin import DifyPluginToolConfig, DifyPluginToolsLayerConfig
from dify_agent.protocol import DIFY_AGENT_HISTORY_LAYER_ID, DIFY_AGENT_MODEL_LAYER_ID
@@ -40,6 +41,14 @@ from models.agent_config_entities import (
)
@pytest.fixture(autouse=True)
def _no_runtime_agent_skills(monkeypatch: pytest.MonkeyPatch):
monkeypatch.setattr(
"core.workflow.nodes.agent_v2.runtime_request_builder.load_runtime_agent_skill_configs",
lambda **_kwargs: [],
)
class FakeCredentialsProvider:
def fetch(self, provider_name: str, model_name: str) -> dict[str, object]:
assert provider_name == "openai"
@@ -1422,6 +1431,30 @@ def test_build_config_layer_config_includes_soul_context_and_mentions():
assert warnings == []
def test_build_config_layer_config_includes_runtime_agent_skills():
from core.workflow.nodes.agent_v2.runtime_request_builder import build_config_layer_config
soul = AgentSoulConfig(
prompt={"system_prompt": "Use [§skill:workspace-skill:Workspace Skill§]."},
model=AgentSoulModelConfig(plugin_id="langgenius/openai", model_provider="openai", model="gpt-test"),
)
config, warnings = build_config_layer_config(
soul,
runtime_config_skills=[
DifyConfigSkillConfig(
name="workspace-skill",
description="Bound workspace skill.",
size=123,
mime_type="application/zip",
)
],
)
assert [skill.name for skill in config.skills] == ["workspace-skill"]
assert config.mentioned_skill_names == ["workspace-skill"]
assert warnings == []
def test_build_config_layer_config_returns_empty_config_for_empty_agent_soul():
from core.workflow.nodes.agent_v2.runtime_request_builder import build_config_layer_config
@@ -1505,6 +1538,33 @@ def test_workflow_run_request_contains_config_layer():
assert warnings == []
def test_workflow_run_request_includes_bound_workspace_skills(monkeypatch: pytest.MonkeyPatch):
monkeypatch.setattr(
"core.workflow.nodes.agent_v2.runtime_request_builder.load_runtime_agent_skill_configs",
lambda **_kwargs: [
DifyConfigSkillConfig(
name="workspace-skill",
description="Bound workspace skill.",
size=123,
mime_type="application/zip",
)
],
)
context = _context()
context.snapshot.config_snapshot = AgentSoulConfig(
prompt={"system_prompt": "Use [§skill:workspace-skill:Workspace Skill§]."},
model=AgentSoulModelConfig(plugin_id="langgenius/openai", model_provider="openai", model="gpt-test"),
)
result = WorkflowAgentRuntimeRequestBuilder(credentials_provider=FakeCredentialsProvider()).build(context)
config = next(layer for layer in result.request.composition.layers if layer.name == DIFY_CONFIG_LAYER_ID)
assert [skill.name for skill in config.config.skills] == ["workspace-skill"]
assert config.config.mentioned_skill_names == ["workspace-skill"]
soul_prompt = next(layer for layer in result.request.composition.layers if layer.name == "agent_soul_prompt")
assert soul_prompt.config.prefix == "Use workspace-skill."
def test_workflow_runtime_expands_config_mentions_in_agent_soul_prompt():
context = _context()
context.snapshot.config_snapshot = _soul_with_config_assets()
@@ -13,7 +13,7 @@ from services.agent import skill_package_service as skill_package_service_module
from services.agent.skill_package_service import NormalizedSkillPackage, SkillPackageError, SkillPackageService
_SKILL_MD = """---
name: PDF Toolkit
name: pdf-toolkit
description: Tools for working with PDF files.
---
@@ -43,7 +43,7 @@ def _archive_members(content: bytes) -> list[str]:
def test_valid_skill_normalizes_manifest():
manifest = _normalize({"SKILL.md": _SKILL_MD.encode(), "scripts/run.py": b"print('hi')\n"}).manifest
assert manifest.name == "PDF Toolkit"
assert manifest.name == "pdf-toolkit"
assert manifest.description == "Tools for working with PDF files."
assert manifest.entry_path == "SKILL.md"
assert set(manifest.files) == {"SKILL.md", "scripts/run.py"}
@@ -51,10 +51,10 @@ def test_valid_skill_normalizes_manifest():
assert len(manifest.hash) == 64
def test_name_falls_back_to_heading_without_frontmatter():
manifest = _normalize({"SKILL.md": b"# Heading Name\n\nbody"}).manifest
assert manifest.name == "Heading Name"
assert manifest.description == ""
def test_name_and_description_are_required_in_frontmatter():
with pytest.raises(SkillPackageError) as exc_info:
_normalize({"SKILL.md": b"# heading-name\n\nbody"})
assert exc_info.value.code == "missing_skill_name"
def test_shallowest_skill_md_preferred_during_normalization():
@@ -155,7 +155,18 @@ def test_validate_and_normalize_strips_deeper_selected_skill_root():
({"README.md": b"x"}, "skill.zip", "missing_skill_md"),
({"SKILL.md": _SKILL_MD.encode()}, "skill.tar", "unsupported_extension"),
({"SKILL.md": b""}, "skill.zip", "empty_skill_md"),
({"SKILL.md": b"no name here"}, "skill.zip", "missing_skill_name"),
({"SKILL.md": b"---\ndescription: valid\n---\n# no name here"}, "skill.zip", "missing_skill_name"),
({"SKILL.md": b"---\nname: pdf-toolkit\n---\n# no description"}, "skill.zip", "missing_skill_description"),
(
{"SKILL.md": b"---\nname: PDF Toolkit\ndescription: valid\n---\n# invalid name"},
"skill.zip",
"invalid_skill_name",
),
(
{"SKILL.md": f"---\nname: pdf-toolkit\ndescription: {'x' * 1025}\n---\n# long".encode()},
"skill.zip",
"invalid_skill_description",
),
({"SKILL.md": b"\xff\xfenot utf8"}, "skill.zip", "skill_md_not_utf8"),
],
)
@@ -224,10 +235,10 @@ def test_bad_frontmatter_yaml_rejected():
assert exc_info.value.code == "invalid_frontmatter"
def test_unterminated_frontmatter_falls_back_to_heading():
# leading '---' with no closing fence -> no frontmatter, use the heading
manifest = _normalize({"SKILL.md": b"---\n# Heading Wins\nbody"}).manifest
assert manifest.name == "Heading Wins"
def test_unterminated_frontmatter_rejected():
with pytest.raises(SkillPackageError) as exc_info:
_normalize({"SKILL.md": b"---\n# heading-wins\nbody"})
assert exc_info.value.code == "missing_skill_name"
def test_validate_and_normalize_rejects_files_outside_selected_skill_root():
@@ -20,7 +20,7 @@ _AGENT_ID = "22222222-2222-2222-2222-222222222222"
_USER_ID = "33333333-3333-3333-3333-333333333333"
_SKILL_MD = b"""---
name: PDF Toolkit
name: pdf-toolkit
description: Work with PDFs.
---
@@ -121,7 +121,7 @@ def test_standardize_creates_drive_owned_toolfiles_and_commits_archive_manifest(
assert skill_row.is_skill is True
assert skill_row.skill_metadata is not None
skill_metadata = DriveSkillMetadata.model_validate_json(skill_row.skill_metadata)
assert skill_metadata.name == "PDF Toolkit"
assert skill_metadata.name == "pdf-toolkit"
assert skill_metadata.manifest_files == ["SKILL.md", "scripts/run.py"]
assert archive_row.file_kind == AgentDriveFileKind.TOOL_FILE
assert archive_row.file_id == archive_tool_file.id
@@ -132,7 +132,7 @@ def test_standardize_creates_drive_owned_toolfiles_and_commits_archive_manifest(
# The returned upload response carries only the drive-derived fields the UI needs.
skill = result["skill"]
assert skill["path"] == "pdf-toolkit"
assert skill["name"] == "PDF Toolkit"
assert skill["name"] == "pdf-toolkit"
assert skill["archive_key"] == "pdf-toolkit/.DIFY-SKILL-FULL.zip"
assert skill["skill_md_key"] == "pdf-toolkit/SKILL.md"
assert result["manifest"]["entry_path"] == "SKILL.md"
@@ -40,6 +40,7 @@ from services.agent_config_service import (
ConfigPushPayload,
ConfigPushSkillItem,
)
from services.skill_management_service import SkillManagementServiceError
MODULE = "services.agent_config_service"
TENANT = "11111111-1111-1111-1111-111111111111"
@@ -138,6 +139,7 @@ def _target(
) -> AgentConfigTarget:
agent_soul = soul or _soul()
return AgentConfigTarget(
tenant_id=TENANT,
agent_id=AGENT,
version_id=version_id,
kind=kind,
@@ -755,6 +757,149 @@ def test_inspect_skill_maps_invalid_archives_to_service_errors(archive_bytes: by
assert exc_info.value.status_code == 500
def test_pull_skill_falls_back_to_workspace_runtime_skill() -> None:
service = AgentConfigService()
target = _target(kind=AgentConfigVersionKind.DRAFT, writable=False, soul=_soul(config_skills=[]))
workspace_archive = SimpleNamespace(
filename="workspace-skill.zip",
mime_type="application/zip",
payload=b"zip-bytes",
)
with (
patch.object(service, "resolve_target", return_value=target),
patch(f"{MODULE}.SkillManagementService") as skill_management_service,
):
skill_management_service.return_value.pull_runtime_agent_skill.return_value = workspace_archive
download = service.pull_skill(
tenant_id=TENANT,
agent_id=AGENT,
config_version_id="draft-1",
config_version_kind=AgentConfigVersionKind.DRAFT,
name="workspace-skill",
user_id=USER,
)
assert download.filename == "workspace-skill.zip"
assert download.mime_type == "application/zip"
assert download.payload == b"zip-bytes"
skill_management_service.return_value.pull_runtime_agent_skill.assert_called_once_with(
tenant_id=TENANT,
agent_id=AGENT,
name="workspace-skill",
)
def test_request_download_falls_back_to_workspace_runtime_skill() -> None:
service = AgentConfigService()
target = _target(kind=AgentConfigVersionKind.DRAFT, writable=False, soul=_soul(config_skills=[]))
expected = SimpleNamespace(
filename="workspace-skill.zip",
mime_type="application/zip",
size=123,
download_uri="/files/tools/workspace.zip?signature=1",
)
with (
patch.object(service, "resolve_target", return_value=target),
patch(f"{MODULE}.SkillManagementService") as skill_management_service,
patch.object(service, "_resolve_download_request", return_value=expected) as resolve_download_request,
):
skill_management_service.return_value.list_runtime_agent_skills.return_value = [
{
"name": "workspace-skill",
"file_id": "workspace-archive-id",
}
]
download = service.request_download(
tenant_id=TENANT,
agent_id=AGENT,
config_version_id="draft-1",
config_version_kind=AgentConfigVersionKind.DRAFT,
kind="skill",
name="workspace-skill",
user_id=USER,
)
assert download is expected
resolve_download_request.assert_called_once_with(
tenant_id=TENANT,
file_kind="tool_file",
file_id="workspace-archive-id",
filename="workspace-skill.zip",
default_mime_type="application/zip",
missing_code="config_skill_not_found",
missing_message="config skill payload is missing",
)
def test_pull_skill_maps_missing_workspace_runtime_skill_to_config_error() -> None:
service = AgentConfigService()
target = _target(kind=AgentConfigVersionKind.DRAFT, writable=False, soul=_soul(config_skills=[]))
with (
patch.object(service, "resolve_target", return_value=target),
patch(f"{MODULE}.SkillManagementService") as skill_management_service,
):
skill_management_service.return_value.pull_runtime_agent_skill.side_effect = SkillManagementServiceError(
"skill_not_found",
"skill not found",
status_code=404,
)
with pytest.raises(AgentConfigServiceError) as exc_info:
service.pull_skill(
tenant_id=TENANT,
agent_id=AGENT,
config_version_id="draft-1",
config_version_kind=AgentConfigVersionKind.DRAFT,
name="workspace-skill",
user_id=USER,
)
assert exc_info.value.code == "config_skill_not_found"
assert exc_info.value.status_code == 404
def test_inspect_skill_falls_back_to_workspace_runtime_skill() -> None:
service = AgentConfigService()
target = _target(kind=AgentConfigVersionKind.DRAFT, writable=False, soul=_soul(config_skills=[]))
archive = _zip_bytes(
{
"SKILL.md": b"---\nname: workspace-skill\ndescription: Workspace skill.\n---\n# Workspace",
"references/policy.md": b"Policy",
}
)
with (
patch.object(service, "resolve_target", return_value=target),
patch(f"{MODULE}.SkillManagementService") as skill_management_service,
):
skill_management_service.return_value.pull_runtime_agent_skill.return_value = SimpleNamespace(payload=archive)
skill_management_service.return_value.list_runtime_agent_skills.return_value = [
{
"id": "skill-1",
"name": "workspace-skill",
"description": "Workspace skill.",
"size": len(archive),
"hash": "hash",
"mime_type": "application/zip",
}
]
result = service.inspect_skill(
tenant_id=TENANT,
agent_id=AGENT,
config_version_id="draft-1",
config_version_kind=AgentConfigVersionKind.DRAFT,
name="workspace-skill",
user_id=USER,
)
assert result["id"] == "skill-1"
assert result["source"] == "config_skill_zip"
assert result["skill_md"]["text"] == "---\nname: workspace-skill\ndescription: Workspace skill.\n---\n# Workspace"
assert [item["path"] for item in result["files"]] == ["SKILL.md", "references", "references/policy.md"]
def test_manifest_uses_items_shape_without_download_urls() -> None:
target = _target(
kind=AgentConfigVersionKind.DRAFT,
@@ -766,7 +911,9 @@ def test_manifest_uses_items_shape_without_download_urls() -> None:
),
)
manifest = AgentConfigService._manifest_for_target(target)
with patch(f"{MODULE}.SkillManagementService") as skill_management_service:
skill_management_service.return_value.list_runtime_agent_skills.return_value = []
manifest = AgentConfigService._manifest_for_target(target)
assert manifest == {
"agent_id": AGENT,
@@ -823,7 +970,9 @@ def test_manifest_preserves_missing_config_assets_and_download_rejects_them(sqli
user_id=USER,
)
manifest = service._manifest_for_target(target)
with patch(f"{MODULE}.SkillManagementService") as skill_management_service:
skill_management_service.return_value.list_runtime_agent_skills.return_value = []
manifest = service._manifest_for_target(target)
assert manifest["skills"]["items"][0]["is_missing"] is True # type: ignore[index]
assert manifest["files"]["items"][0]["is_missing"] is True # type: ignore[index]
@@ -873,6 +1022,79 @@ def test_config_asset_refs_require_file_id_unless_marked_missing() -> None:
)
def test_manifest_appends_published_workspace_skills() -> None:
target = _target(
kind=AgentConfigVersionKind.DRAFT,
writable=False,
soul=_soul(
config_skills=[AgentConfigSkillRefConfig(name="alpha", description="Alpha skill", file_id="tool-file-1")]
),
)
with patch(f"{MODULE}.SkillManagementService") as skill_management_service:
skill_management_service.return_value.list_runtime_agent_skills.return_value = [
{
"id": "workspace-skill-id",
"name": "beta",
"file_id": "tool-file-2",
"description": "Beta workspace skill",
"size": 123,
"hash": "sha256:beta",
"mime_type": "application/zip",
},
{
"id": "duplicate",
"name": "alpha",
"file_id": "tool-file-ignored",
"description": "Duplicate workspace skill",
"size": 456,
"hash": "sha256:ignored",
"mime_type": "application/zip",
},
]
manifest = AgentConfigService._manifest_for_target(target)
assert [item["name"] for item in manifest["skills"]["items"]] == ["alpha", "beta"]
assert manifest["skills"]["items"][1]["file_id"] == "tool-file-2"
def test_list_skills_excludes_workspace_skill_bindings() -> None:
target = _target(
kind=AgentConfigVersionKind.DRAFT,
writable=False,
soul=_soul(
config_skills=[AgentConfigSkillRefConfig(name="alpha", description="Alpha skill", file_id="tool-file-1")]
),
)
service = AgentConfigService()
with (
patch.object(service, "resolve_target", return_value=target),
patch(f"{MODULE}.SkillManagementService") as skill_management_service,
):
skill_management_service.return_value.list_runtime_agent_skills.return_value = [
{
"id": "workspace-skill-id",
"name": "beta",
"file_id": "tool-file-2",
"description": "Beta workspace skill",
"size": 123,
"hash": "sha256:beta",
"mime_type": "application/zip",
}
]
result = service.list_skills(
tenant_id=target.tenant_id,
agent_id=target.agent_id,
config_version_id=target.version_id,
config_version_kind=target.kind,
user_id=None,
)
assert [item["name"] for item in result["items"]] == ["alpha"]
skill_management_service.return_value.list_runtime_agent_skills.assert_not_called()
def test_preview_skill_file_returns_text_preview() -> None:
service = AgentConfigService()
target = _target(
@@ -726,6 +726,7 @@ class TestAgentAppType:
assert backing_agent.archived_by == "account-2"
assert backing_agent.archived_at is not None
mock_db.session.delete.assert_called_once_with(app)
mock_db.session.query.return_value.filter.return_value.delete.assert_called_once_with(synchronize_session=False)
mock_workflow_retirement.assert_called_once_with(
tenant_id="tenant-1",
agent_ids=["workflow-agent-1", "workflow-agent-2"],
File diff suppressed because it is too large Load Diff
-24
View File
@@ -2907,14 +2907,6 @@
"count": 5
}
},
"web/app/components/header/account-setting/model-provider-page/model-parameter-modal/index.tsx": {
"jsx_a11y/click-events-have-key-events": {
"count": 1
},
"jsx_a11y/no-static-element-interactions": {
"count": 1
}
},
"web/app/components/header/account-setting/model-provider-page/model-parameter-modal/model-display.tsx": {
"typescript/no-explicit-any": {
"count": 1
@@ -3059,14 +3051,6 @@
"count": 1
}
},
"web/app/components/plugins/marketplace/plugin-type-switch.tsx": {
"jsx_a11y/click-events-have-key-events": {
"count": 1
},
"jsx_a11y/no-static-element-interactions": {
"count": 1
}
},
"web/app/components/plugins/plugin-auth/authorized/index.tsx": {
"no-restricted-imports": {
"count": 1
@@ -3855,14 +3839,6 @@
"count": 1
}
},
"web/app/components/workflow/nodes/_base/components/__tests__/node-handle.spec.tsx": {
"jsx_a11y/click-events-have-key-events": {
"count": 1
},
"jsx_a11y/no-static-element-interactions": {
"count": 1
}
},
"web/app/components/workflow/nodes/_base/components/add-variable-popup-with-position.tsx": {
"typescript/no-explicit-any": {
"count": 2
@@ -926,11 +926,15 @@ export const zSandboxListResponse = z.object({
* Validated metadata extracted from a Skill package.
*/
export const zSkillManifest = z.object({
description: z.string(),
description: z.string().min(1).max(1024),
entry_path: z.string(),
files: z.array(z.string()),
hash: z.string(),
name: z.string(),
name: z
.string()
.min(1)
.max(64)
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
size: z.int(),
})
@@ -1361,11 +1361,15 @@ export const zAgentLogMetaResponse = z.object({
* Validated metadata extracted from a Skill package.
*/
export const zSkillManifest = z.object({
description: z.string(),
description: z.string().min(1).max(1024),
entry_path: z.string(),
files: z.array(z.string()),
hash: z.string(),
name: z.string(),
name: z
.string()
.min(1)
.max(64)
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
size: z.int(),
})
@@ -20,7 +20,7 @@ export type TagBindingRemovePayload = {
type: TagType
}
export type TagType = 'app' | 'knowledge' | 'snippet'
export type TagType = 'app' | 'knowledge' | 'skill' | 'snippet'
export type PostTagBindingsData = {
body: TagBindingPayload
@@ -14,7 +14,7 @@ export const zSimpleResultResponse = z.object({
*
* Tag type
*/
export const zTagType = z.enum(['app', 'knowledge', 'snippet'])
export const zTagType = z.enum(['app', 'knowledge', 'skill', 'snippet'])
/**
* TagBindingPayload
@@ -22,14 +22,14 @@ export type TagUpdateRequestPayload = {
name: string
}
export type TagType = 'app' | 'knowledge' | 'snippet'
export type TagType = 'app' | 'knowledge' | 'skill' | 'snippet'
export type GetTagsData = {
body?: never
path?: never
query?: {
keyword?: string
type?: '' | 'app' | 'knowledge' | 'snippet'
type?: '' | 'app' | 'knowledge' | 'skill' | 'snippet'
}
url: '/tags'
}
@@ -29,7 +29,7 @@ export const zTagUpdateRequestPayload = z.object({
*
* Tag type
*/
export const zTagType = z.enum(['app', 'knowledge', 'snippet'])
export const zTagType = z.enum(['app', 'knowledge', 'skill', 'snippet'])
/**
* TagBasePayload
@@ -41,7 +41,7 @@ export const zTagBasePayload = z.object({
export const zGetTagsQuery = z.object({
keyword: z.string().optional(),
type: z.enum(['', 'app', 'knowledge', 'snippet']).optional().default(''),
type: z.enum(['', 'app', 'knowledge', 'skill', 'snippet']).optional().default(''),
})
/**
File diff suppressed because it is too large Load Diff
@@ -16,6 +16,16 @@ export type AgentProviderListResponse = Array<{
[key: string]: unknown
}>
export type AgentSkillBindingsResponse = {
agent_id: string
data?: Array<AgentSkillBindingItemResponse>
skill_ids?: Array<string>
}
export type AgentSkillBindingsPayload = {
skill_ids?: Array<string>
}
export type SnippetPaginationResponse = {
data: Array<SnippetListItemResponse>
has_more: boolean
@@ -642,6 +652,209 @@ export type WorkspaceAccessMatrix = {
pagination?: Pagination | null
}
export type SkillListResponse = {
data?: Array<SkillResponse>
has_more?: boolean
limit?: number
page?: number
total?: number
}
export type SkillCreatePayload = {
description?: string
display_name?: string | null
icon?: string
name?: string | null
tags?: Array<string>
}
export type SkillDetailResponse = {
created_at: number
created_by?: string | null
created_by_name?: string | null
description: string
display_name: string
files?: Array<SkillFileResponse>
icon: string
id: string
latest_published_at?: number | null
latest_published_version_id?: string | null
latest_published_version_number?: number | null
name: string
name_manually_edited?: boolean
reference_count?: number
tags?: Array<string>
updated_at: number
updated_by?: string | null
updated_by_name?: string | null
visibility: string
}
export type SkillFileUploadResponse = {
hash: string
id: string
mime_type: string
name: string
size: number
}
export type SkillTagListResponse = {
data?: Array<SkillTagResponse>
}
export type SkillDeletePayload = {
confirmation_name?: string | null
}
export type SkillDeleteResponse = {
deleted: boolean
id: string
}
export type SkillMetadataPayload = {
display_name?: string | null
expected_updated_at?: number | null
icon?: string | null
tags?: Array<string> | null
}
export type SkillResponse = {
created_at: number
created_by?: string | null
created_by_name?: string | null
description: string
display_name: string
icon: string
id: string
latest_published_at?: number | null
latest_published_version_id?: string | null
latest_published_version_number?: number | null
name: string
name_manually_edited?: boolean
reference_count?: number
tags?: Array<string>
updated_at: number
updated_by?: string | null
updated_by_name?: string | null
visibility: string
}
export type SkillAssistModelPayload = {
model: string
model_settings?: { [key: string]: unknown } | null
plugin_id?: string | null
provider: string
}
export type SkillAssistAttachmentPayload = {
mime_type?: string | null
name: string
size?: number | null
tool_file_id: string
}
export type SkillAssistMessagePayload = {
attachments?: Array<SkillAssistAttachmentPayload>
history?: Array<SkillAssistHistoryMessagePayload>
message: string
model?: SkillAssistModelPayload | null
target_path?: string | null
}
export type SkillDraftFileOperationPayload = {
content?: string | null
expected_updated_at?: number | null
hash?: string | null
mime_type?: string | null
operation: SkillDraftFileOperation
path: string
size?: number | null
target_path?: string | null
tool_file_id?: string | null
}
export type SkillDraftTreePayload = {
expected_updated_at?: number | null
files?: Array<SkillDraftTreeItemPayload>
}
export type SkillDraftFileCheckPayload = {
files?: Array<SkillDraftFileCheckItemPayload>
}
export type SkillFileCheckResponse = {
data?: {
[key: string]: SkillFileCheckItemResponse
}
}
export type SkillFilePreviewResponse = {
content: string
hash: string
mime_type: string
path: string
size: number
}
export type SkillPublishPayload = {
publish_note?: string
version_name?: string | null
}
export type SkillVersionResponse = {
archive_size: number
created_at: number
hash_code: string
id: string
is_latest?: boolean
publish_note: string
published_by?: string | null
published_by_name?: string | null
skill_id: string
version_name: string
version_number: number
}
export type SkillReferenceListResponse = {
data?: Array<SkillReferenceResponse>
}
export type SkillRestorePayload = {
publish_note?: string
version_id: string
version_name?: string | null
}
export type SkillVersionListResponse = {
data?: Array<SkillVersionResponse>
}
export type SkillVersionDeleteResponse = {
deleted: boolean
id: string
latest_published_version_id?: string | null
}
export type SkillVersionDetailResponse = {
archive_size: number
created_at: number
files?: Array<SkillFileResponse>
hash_code: string
id: string
is_latest?: boolean
publish_note: string
published_by?: string | null
published_by_name?: string | null
skill_id: string
version_name: string
version_number: number
}
export type SkillVersionUpdatePayload = {
publish_note?: string
version_name?: string | null
}
export type CurrentWorkspaceSummaryResponse = {
credits: number | null
id: string
@@ -1057,6 +1270,21 @@ export type TenantListItemResponse = {
status?: string | null
}
export type AgentSkillBindingItemResponse = {
description: string
display_name: string
file_count: number
icon: string
id: string
latest_published_at?: number | null
latest_published_version_id?: string | null
name: string
priority: number
status: string
tags?: Array<string>
updated_at: number
}
export type SnippetListItemResponse = {
author_name: string | null
created_at: number
@@ -1541,6 +1769,98 @@ export type AccessPolicyRole = {
role_tag?: string
}
export type SkillFileResponse = {
content?: string | null
hash?: string | null
id?: string | null
kind: string
mime_type?: string | null
path: string
size?: number | null
storage?: string | null
tool_file_id?: string | null
}
export type SkillTagResponse = {
count: number
tag: string
}
export type SkillAssistAttachmentPayload = {
mime_type?: string | null
name: string
size?: number | null
tool_file_id: string
}
export type SkillAssistHistoryMessagePayload = {
content: string
role: 'assistant' | 'user'
suggested_display_name?: string | null
suggested_name?: string | null
}
export type SkillAssistModelPayload = {
model: string
model_settings?: {
[key: string]: unknown
} | null
plugin_id?: string | null
provider: string
}
export type SkillDraftFileOperation =
| 'delete'
| 'mkdir'
| 'rename'
| 'upsert_text'
| 'upsert_tool_file'
export type SkillDraftTreeItemPayload = {
content?: string | null
hash?: string | null
kind?: SkillFileKind
mime_type?: string | null
path: string
size?: number | null
storage?: SkillFileStorage | null
tool_file_id?: string | null
}
export type SkillDraftFileCheckItemPayload = {
filename: string
mime_type?: string | null
path?: string | null
size: number
}
export type SkillFileCheckItemResponse = {
errors?: Array<SkillFileCheckErrorResponse>
extension: string
filename: string
mime_type: string
path: string
size: number
}
export type SkillReferenceResponse = {
agent_id: string
agent_icon?: string | null
agent_icon_background?: string | null
agent_icon_type?: string | null
app_id?: string | null
display_name: string
name: string
node_id?: string | null
node_name?: string | null
type: string
workflow_icon?: string | null
workflow_icon_background?: string | null
workflow_icon_type?: string | null
workflow_id?: string | null
workflow_name?: string | null
workflow_version?: string | null
}
export type TenantAccountRole = 'admin' | 'dataset_operator' | 'editor' | 'normal' | 'owner'
export type ToolLabel = {
@@ -2048,6 +2368,15 @@ export type PermissionCatalogItem = {
name: string
}
export type SkillFileKind = 'directory' | 'file'
export type SkillFileStorage = 'text' | 'tool_file'
export type SkillFileCheckErrorResponse = {
code: string
message: string
}
export type ToolParameter = {
auto_generate?: PluginParameterAutoGenerate | null
default?:
@@ -2529,6 +2858,38 @@ export type GetWorkspacesCurrentAgentProvidersResponses = {
export type GetWorkspacesCurrentAgentProvidersResponse =
GetWorkspacesCurrentAgentProvidersResponses[keyof GetWorkspacesCurrentAgentProvidersResponses]
export type GetWorkspacesCurrentAgentsByAgentIdSkillsData = {
body?: never
path: {
agent_id: string
}
query?: never
url: '/workspaces/current/agents/{agent_id}/skills'
}
export type GetWorkspacesCurrentAgentsByAgentIdSkillsResponses = {
200: AgentSkillBindingsResponse
}
export type GetWorkspacesCurrentAgentsByAgentIdSkillsResponse =
GetWorkspacesCurrentAgentsByAgentIdSkillsResponses[keyof GetWorkspacesCurrentAgentsByAgentIdSkillsResponses]
export type PutWorkspacesCurrentAgentsByAgentIdSkillsData = {
body: AgentSkillBindingsPayload
path: {
agent_id: string
}
query?: never
url: '/workspaces/current/agents/{agent_id}/skills'
}
export type PutWorkspacesCurrentAgentsByAgentIdSkillsResponses = {
200: AgentSkillBindingsResponse
}
export type PutWorkspacesCurrentAgentsByAgentIdSkillsResponse =
PutWorkspacesCurrentAgentsByAgentIdSkillsResponses[keyof PutWorkspacesCurrentAgentsByAgentIdSkillsResponses]
export type GetWorkspacesCurrentCustomizedSnippetsData = {
body?: never
path?: never
@@ -4728,6 +5089,385 @@ export type GetWorkspacesCurrentRbacWorkspaceDatasetsAccessPolicyResponses = {
export type GetWorkspacesCurrentRbacWorkspaceDatasetsAccessPolicyResponse =
GetWorkspacesCurrentRbacWorkspaceDatasetsAccessPolicyResponses[keyof GetWorkspacesCurrentRbacWorkspaceDatasetsAccessPolicyResponses]
export type GetWorkspacesCurrentSkillsData = {
body?: never
path?: never
query?: {
keyword?: string
limit?: number
page?: number
tag?: Array<string>
}
url: '/workspaces/current/skills'
}
export type GetWorkspacesCurrentSkillsResponses = {
200: SkillListResponse
}
export type GetWorkspacesCurrentSkillsResponse =
GetWorkspacesCurrentSkillsResponses[keyof GetWorkspacesCurrentSkillsResponses]
export type PostWorkspacesCurrentSkillsData = {
body: SkillCreatePayload
path?: never
query?: never
url: '/workspaces/current/skills'
}
export type PostWorkspacesCurrentSkillsResponses = {
201: SkillDetailResponse
}
export type PostWorkspacesCurrentSkillsResponse =
PostWorkspacesCurrentSkillsResponses[keyof PostWorkspacesCurrentSkillsResponses]
export type PostWorkspacesCurrentSkillsFilesUploadData = {
body: {
file: Blob | File
}
path?: never
query?: never
url: '/workspaces/current/skills/files/upload'
}
export type PostWorkspacesCurrentSkillsFilesUploadResponses = {
201: SkillFileUploadResponse
}
export type PostWorkspacesCurrentSkillsFilesUploadResponse =
PostWorkspacesCurrentSkillsFilesUploadResponses[keyof PostWorkspacesCurrentSkillsFilesUploadResponses]
export type PostWorkspacesCurrentSkillsImportData = {
body?: never
path?: never
query?: never
url: '/workspaces/current/skills/import'
}
export type PostWorkspacesCurrentSkillsImportResponses = {
201: SkillDetailResponse
}
export type PostWorkspacesCurrentSkillsImportResponse =
PostWorkspacesCurrentSkillsImportResponses[keyof PostWorkspacesCurrentSkillsImportResponses]
export type GetWorkspacesCurrentSkillsTagsData = {
body?: never
path?: never
query?: never
url: '/workspaces/current/skills/tags'
}
export type GetWorkspacesCurrentSkillsTagsResponses = {
200: SkillTagListResponse
}
export type GetWorkspacesCurrentSkillsTagsResponse =
GetWorkspacesCurrentSkillsTagsResponses[keyof GetWorkspacesCurrentSkillsTagsResponses]
export type DeleteWorkspacesCurrentSkillsBySkillIdData = {
body: SkillDeletePayload
path: {
skill_id: string
}
query?: never
url: '/workspaces/current/skills/{skill_id}'
}
export type DeleteWorkspacesCurrentSkillsBySkillIdResponses = {
200: SkillDeleteResponse
}
export type DeleteWorkspacesCurrentSkillsBySkillIdResponse =
DeleteWorkspacesCurrentSkillsBySkillIdResponses[keyof DeleteWorkspacesCurrentSkillsBySkillIdResponses]
export type GetWorkspacesCurrentSkillsBySkillIdData = {
body?: never
path: {
skill_id: string
}
query?: never
url: '/workspaces/current/skills/{skill_id}'
}
export type GetWorkspacesCurrentSkillsBySkillIdResponses = {
200: SkillDetailResponse
}
export type GetWorkspacesCurrentSkillsBySkillIdResponse =
GetWorkspacesCurrentSkillsBySkillIdResponses[keyof GetWorkspacesCurrentSkillsBySkillIdResponses]
export type PatchWorkspacesCurrentSkillsBySkillIdData = {
body: SkillMetadataPayload
path: {
skill_id: string
}
query?: never
url: '/workspaces/current/skills/{skill_id}'
}
export type PatchWorkspacesCurrentSkillsBySkillIdResponses = {
200: SkillResponse
}
export type PatchWorkspacesCurrentSkillsBySkillIdResponse =
PatchWorkspacesCurrentSkillsBySkillIdResponses[keyof PatchWorkspacesCurrentSkillsBySkillIdResponses]
export type PostWorkspacesCurrentSkillsBySkillIdAssistMessagesData = {
body: SkillAssistMessagePayload
path: {
skill_id: string
}
query?: never
url: '/workspaces/current/skills/{skill_id}/assist/messages'
}
export type PostWorkspacesCurrentSkillsBySkillIdAssistMessagesResponses = {
200: {
[key: string]: unknown
}
}
export type PostWorkspacesCurrentSkillsBySkillIdAssistMessagesResponse =
PostWorkspacesCurrentSkillsBySkillIdAssistMessagesResponses[keyof PostWorkspacesCurrentSkillsBySkillIdAssistMessagesResponses]
export type PostWorkspacesCurrentSkillsBySkillIdDuplicateData = {
body?: never
path: {
skill_id: string
}
query?: never
url: '/workspaces/current/skills/{skill_id}/duplicate'
}
export type PostWorkspacesCurrentSkillsBySkillIdDuplicateResponses = {
201: SkillDetailResponse
}
export type PostWorkspacesCurrentSkillsBySkillIdDuplicateResponse =
PostWorkspacesCurrentSkillsBySkillIdDuplicateResponses[keyof PostWorkspacesCurrentSkillsBySkillIdDuplicateResponses]
export type GetWorkspacesCurrentSkillsBySkillIdExportData = {
body?: never
path: {
skill_id: string
}
query?: never
url: '/workspaces/current/skills/{skill_id}/export'
}
export type GetWorkspacesCurrentSkillsBySkillIdExportResponses = {
200: {
[key: string]: unknown
}
}
export type GetWorkspacesCurrentSkillsBySkillIdExportResponse =
GetWorkspacesCurrentSkillsBySkillIdExportResponses[keyof GetWorkspacesCurrentSkillsBySkillIdExportResponses]
export type PatchWorkspacesCurrentSkillsBySkillIdFilesData = {
body: SkillDraftFileOperationPayload
path: {
skill_id: string
}
query?: never
url: '/workspaces/current/skills/{skill_id}/files'
}
export type PatchWorkspacesCurrentSkillsBySkillIdFilesResponses = {
200: SkillDetailResponse
}
export type PatchWorkspacesCurrentSkillsBySkillIdFilesResponse =
PatchWorkspacesCurrentSkillsBySkillIdFilesResponses[keyof PatchWorkspacesCurrentSkillsBySkillIdFilesResponses]
export type PutWorkspacesCurrentSkillsBySkillIdFilesData = {
body: SkillDraftTreePayload
path: {
skill_id: string
}
query?: never
url: '/workspaces/current/skills/{skill_id}/files'
}
export type PutWorkspacesCurrentSkillsBySkillIdFilesResponses = {
200: SkillDetailResponse
}
export type PutWorkspacesCurrentSkillsBySkillIdFilesResponse =
PutWorkspacesCurrentSkillsBySkillIdFilesResponses[keyof PutWorkspacesCurrentSkillsBySkillIdFilesResponses]
export type PostWorkspacesCurrentSkillsBySkillIdFilesCheckData = {
body: SkillDraftFileCheckPayload
path: {
skill_id: string
}
query?: never
url: '/workspaces/current/skills/{skill_id}/files/check'
}
export type PostWorkspacesCurrentSkillsBySkillIdFilesCheckResponses = {
200: SkillFileCheckResponse
}
export type PostWorkspacesCurrentSkillsBySkillIdFilesCheckResponse =
PostWorkspacesCurrentSkillsBySkillIdFilesCheckResponses[keyof PostWorkspacesCurrentSkillsBySkillIdFilesCheckResponses]
export type GetWorkspacesCurrentSkillsBySkillIdFilesContentData = {
body?: never
path: {
skill_id: string
}
query: {
download?: string
path: string
version_id?: string
}
url: '/workspaces/current/skills/{skill_id}/files/content'
}
export type GetWorkspacesCurrentSkillsBySkillIdFilesContentResponses = {
200: BinaryFileResponse
}
export type GetWorkspacesCurrentSkillsBySkillIdFilesContentResponse =
GetWorkspacesCurrentSkillsBySkillIdFilesContentResponses[keyof GetWorkspacesCurrentSkillsBySkillIdFilesContentResponses]
export type GetWorkspacesCurrentSkillsBySkillIdFilesPreviewData = {
body?: never
path: {
skill_id: string
}
query: {
path: string
version_id?: string
}
url: '/workspaces/current/skills/{skill_id}/files/preview'
}
export type GetWorkspacesCurrentSkillsBySkillIdFilesPreviewResponses = {
200: SkillFilePreviewResponse
}
export type GetWorkspacesCurrentSkillsBySkillIdFilesPreviewResponse =
GetWorkspacesCurrentSkillsBySkillIdFilesPreviewResponses[keyof GetWorkspacesCurrentSkillsBySkillIdFilesPreviewResponses]
export type PostWorkspacesCurrentSkillsBySkillIdPublishData = {
body: SkillPublishPayload
path: {
skill_id: string
}
query?: never
url: '/workspaces/current/skills/{skill_id}/publish'
}
export type PostWorkspacesCurrentSkillsBySkillIdPublishResponses = {
200: SkillVersionResponse
}
export type PostWorkspacesCurrentSkillsBySkillIdPublishResponse =
PostWorkspacesCurrentSkillsBySkillIdPublishResponses[keyof PostWorkspacesCurrentSkillsBySkillIdPublishResponses]
export type GetWorkspacesCurrentSkillsBySkillIdReferencesData = {
body?: never
path: {
skill_id: string
}
query?: never
url: '/workspaces/current/skills/{skill_id}/references'
}
export type GetWorkspacesCurrentSkillsBySkillIdReferencesResponses = {
200: SkillReferenceListResponse
}
export type GetWorkspacesCurrentSkillsBySkillIdReferencesResponse =
GetWorkspacesCurrentSkillsBySkillIdReferencesResponses[keyof GetWorkspacesCurrentSkillsBySkillIdReferencesResponses]
export type PostWorkspacesCurrentSkillsBySkillIdRestoreData = {
body: SkillRestorePayload
path: {
skill_id: string
}
query?: never
url: '/workspaces/current/skills/{skill_id}/restore'
}
export type PostWorkspacesCurrentSkillsBySkillIdRestoreResponses = {
200: SkillDetailResponse
}
export type PostWorkspacesCurrentSkillsBySkillIdRestoreResponse =
PostWorkspacesCurrentSkillsBySkillIdRestoreResponses[keyof PostWorkspacesCurrentSkillsBySkillIdRestoreResponses]
export type GetWorkspacesCurrentSkillsBySkillIdVersionsData = {
body?: never
path: {
skill_id: string
}
query?: never
url: '/workspaces/current/skills/{skill_id}/versions'
}
export type GetWorkspacesCurrentSkillsBySkillIdVersionsResponses = {
200: SkillVersionListResponse
}
export type GetWorkspacesCurrentSkillsBySkillIdVersionsResponse =
GetWorkspacesCurrentSkillsBySkillIdVersionsResponses[keyof GetWorkspacesCurrentSkillsBySkillIdVersionsResponses]
export type DeleteWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdData = {
body?: never
path: {
skill_id: string
version_id: string
}
query?: never
url: '/workspaces/current/skills/{skill_id}/versions/{version_id}'
}
export type DeleteWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponses = {
200: SkillVersionDeleteResponse
}
export type DeleteWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponse =
DeleteWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponses[keyof DeleteWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponses]
export type GetWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdData = {
body?: never
path: {
skill_id: string
version_id: string
}
query?: never
url: '/workspaces/current/skills/{skill_id}/versions/{version_id}'
}
export type GetWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponses = {
200: SkillVersionDetailResponse
}
export type GetWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponse =
GetWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponses[keyof GetWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponses]
export type PatchWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdData = {
body: SkillVersionUpdatePayload
path: {
skill_id: string
version_id: string
}
query?: never
url: '/workspaces/current/skills/{skill_id}/versions/{version_id}'
}
export type PatchWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponses = {
200: SkillVersionResponse
}
export type PatchWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponse =
PatchWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponses[keyof PatchWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponses]
export type GetWorkspacesCurrentSummaryData = {
body?: never
path?: never
@@ -12,6 +12,13 @@ export const zAgentProviderResponse = z.record(z.string(), z.unknown())
*/
export const zAgentProviderListResponse = z.array(z.record(z.string(), z.unknown()))
/**
* AgentSkillBindingsPayload
*/
export const zAgentSkillBindingsPayload = z.object({
skill_ids: z.array(z.string()).optional(),
})
/**
* SnippetImportPayload
*
@@ -458,6 +465,157 @@ export const zReplaceBindingsRequest = z.object({
role_ids: z.array(z.string()).optional(),
})
/**
* SkillCreatePayload
*/
export const zSkillCreatePayload = z.object({
description: z.string().optional().default(''),
display_name: z.string().nullish(),
icon: z.string().optional().default('📄'),
name: z.string().nullish(),
tags: z.array(z.string()).optional(),
})
/**
* SkillFileUploadResponse
*/
export const zSkillFileUploadResponse = z.object({
hash: z.string(),
id: z.string(),
mime_type: z.string(),
name: z.string(),
size: z.int(),
})
/**
* SkillDeletePayload
*/
export const zSkillDeletePayload = z.object({
confirmation_name: z.string().nullish(),
})
/**
* SkillDeleteResponse
*/
export const zSkillDeleteResponse = z.object({
deleted: z.boolean(),
id: z.string(),
})
/**
* SkillMetadataPayload
*/
export const zSkillMetadataPayload = z.object({
display_name: z.string().nullish(),
expected_updated_at: z.int().nullish(),
icon: z.string().nullish(),
tags: z.array(z.string()).nullish(),
})
/**
* SkillResponse
*/
export const zSkillResponse = z.object({
created_at: z.int(),
created_by: z.string().nullish(),
created_by_name: z.string().nullish(),
description: z.string(),
display_name: z.string(),
icon: z.string(),
id: z.string(),
latest_published_at: z.int().nullish(),
latest_published_version_id: z.string().nullish(),
latest_published_version_number: z.int().nullish(),
name: z.string(),
name_manually_edited: z.boolean().optional().default(false),
reference_count: z.int().optional().default(0),
tags: z.array(z.string()).optional(),
updated_at: z.int(),
updated_by: z.string().nullish(),
updated_by_name: z.string().nullish(),
visibility: z.string(),
})
/**
* SkillListResponse
*/
export const zSkillListResponse = z.object({
data: z.array(zSkillResponse).optional(),
has_more: z.boolean().optional().default(false),
limit: z.int().optional().default(20),
page: z.int().optional().default(1),
total: z.int().optional().default(0),
})
/**
* SkillFilePreviewResponse
*/
export const zSkillFilePreviewResponse = z.object({
content: z.string(),
hash: z.string(),
mime_type: z.string(),
path: z.string(),
size: z.int(),
})
/**
* SkillPublishPayload
*/
export const zSkillPublishPayload = z.object({
publish_note: z.string().max(1024).optional().default(''),
version_name: z.string().max(128).nullish(),
})
/**
* SkillVersionResponse
*/
export const zSkillVersionResponse = z.object({
archive_size: z.int(),
created_at: z.int(),
hash_code: z.string(),
id: z.string(),
is_latest: z.boolean().optional().default(false),
publish_note: z.string(),
published_by: z.string().nullish(),
published_by_name: z.string().nullish(),
skill_id: z.string(),
version_name: z.string(),
version_number: z.int(),
})
/**
* SkillRestorePayload
*/
export const zSkillRestorePayload = z.object({
publish_note: z.string().max(1024).optional().default(''),
version_id: z.string(),
version_name: z.string().max(128).nullish(),
})
/**
* SkillVersionListResponse
*/
export const zSkillVersionListResponse = z.object({
data: z.array(zSkillVersionResponse).optional(),
})
/**
* SkillVersionDeleteResponse
*/
export const zSkillVersionDeleteResponse = z.object({
deleted: z.boolean(),
id: z.string(),
latest_published_version_id: z.string().nullish(),
})
/**
* SkillVersionUpdatePayload
*/
export const zSkillVersionUpdatePayload = z.object({
publish_note: z.string().max(1024).optional().default(''),
version_name: z.string().max(128).nullish(),
})
/**
* ApiToolProviderDeletePayload
*/
@@ -643,6 +801,33 @@ export const zTenantListResponse = z.object({
workspaces: z.array(zTenantListItemResponse),
})
/**
* AgentSkillBindingItemResponse
*/
export const zAgentSkillBindingItemResponse = z.object({
description: z.string(),
display_name: z.string(),
file_count: z.int(),
icon: z.string(),
id: z.string(),
latest_published_at: z.int().nullish(),
latest_published_version_id: z.string().nullish(),
name: z.string(),
priority: z.int(),
status: z.string(),
tags: z.array(z.string()).optional(),
updated_at: z.int(),
})
/**
* AgentSkillBindingsResponse
*/
export const zAgentSkillBindingsResponse = z.object({
agent_id: z.string(),
data: z.array(zAgentSkillBindingItemResponse).optional(),
skill_ids: z.array(z.string()).optional(),
})
/**
* IconInfo
*
@@ -1231,6 +1416,194 @@ export const zWorkspaceAccessMatrix = z.object({
pagination: zPagination.nullish(),
})
/**
* SkillFileResponse
*/
export const zSkillFileResponse = z.object({
content: z.string().nullish(),
hash: z.string().nullish(),
id: z.string().nullish(),
kind: z.string(),
mime_type: z.string().nullish(),
path: z.string(),
size: z.int().nullish(),
storage: z.string().nullish(),
tool_file_id: z.string().nullish(),
})
/**
* SkillDetailResponse
*/
export const zSkillDetailResponse = z.object({
created_at: z.int(),
created_by: z.string().nullish(),
created_by_name: z.string().nullish(),
description: z.string(),
display_name: z.string(),
files: z.array(zSkillFileResponse).optional(),
icon: z.string(),
id: z.string(),
latest_published_at: z.int().nullish(),
latest_published_version_id: z.string().nullish(),
latest_published_version_number: z.int().nullish(),
name: z.string(),
name_manually_edited: z.boolean().optional().default(false),
reference_count: z.int().optional().default(0),
tags: z.array(z.string()).optional(),
updated_at: z.int(),
updated_by: z.string().nullish(),
updated_by_name: z.string().nullish(),
visibility: z.string(),
})
/**
* SkillVersionDetailResponse
*/
export const zSkillVersionDetailResponse = z.object({
archive_size: z.int(),
created_at: z.int(),
files: z.array(zSkillFileResponse).optional(),
hash_code: z.string(),
id: z.string(),
is_latest: z.boolean().optional().default(false),
publish_note: z.string(),
published_by: z.string().nullish(),
published_by_name: z.string().nullish(),
skill_id: z.string(),
version_name: z.string(),
version_number: z.int(),
})
/**
* SkillTagResponse
*/
export const zSkillTagResponse = z.object({
count: z.int(),
tag: z.string(),
})
/**
* SkillTagListResponse
*/
export const zSkillTagListResponse = z.object({
data: z.array(zSkillTagResponse).optional(),
})
/**
* SkillAssistAttachmentPayload
*/
export const zSkillAssistAttachmentPayload = z.object({
mime_type: z.string().min(1).max(255).nullish(),
name: z.string().min(1).max(255),
size: z.int().gte(0).nullish(),
tool_file_id: z.string().min(1),
})
/**
* SkillAssistHistoryMessagePayload
*/
export const zSkillAssistHistoryMessagePayload = z.object({
content: z.string().min(1).max(8000),
role: z.enum(['assistant', 'user']),
suggested_display_name: z.string().max(128).nullish(),
suggested_name: z.string().max(128).nullish(),
})
/**
* SkillAssistModelPayload
*/
export const zSkillAssistModelPayload = z.object({
model: z.string().min(1).max(255),
model_settings: z.record(z.string(), z.unknown()).nullish(),
plugin_id: z.string().min(1).max(255).nullish(),
provider: z.string().min(1).max(255),
})
/**
* SkillAssistMessagePayload
*
* One user message and optional uploaded context for the Skill Authoring assistant.
*/
export const zSkillAssistMessagePayload = z.object({
attachments: z.array(zSkillAssistAttachmentPayload).max(10).optional(),
history: z.array(zSkillAssistHistoryMessagePayload).max(20).optional(),
message: z.string().min(1).max(8000),
model: zSkillAssistModelPayload.nullish(),
target_path: z.string().nullish(),
})
/**
* SkillDraftFileOperation
*/
export const zSkillDraftFileOperation = z.enum([
'delete',
'mkdir',
'rename',
'upsert_text',
'upsert_tool_file',
])
/**
* SkillDraftFileOperationPayload
*/
export const zSkillDraftFileOperationPayload = z.object({
content: z.string().nullish(),
expected_updated_at: z.int().nullish(),
hash: z.string().nullish(),
mime_type: z.string().nullish(),
operation: zSkillDraftFileOperation,
path: z.string(),
size: z.int().gte(0).nullish(),
target_path: z.string().nullish(),
tool_file_id: z.string().nullish(),
})
/**
* SkillDraftFileCheckItemPayload
*/
export const zSkillDraftFileCheckItemPayload = z.object({
filename: z.string().min(1).max(255),
mime_type: z.string().max(255).nullish(),
path: z.string().nullish(),
size: z.int().gte(0),
})
/**
* SkillDraftFileCheckPayload
*/
export const zSkillDraftFileCheckPayload = z.object({
files: z.array(zSkillDraftFileCheckItemPayload).max(100).optional(),
})
/**
* SkillReferenceResponse
*/
export const zSkillReferenceResponse = z.object({
agent_id: z.string(),
agent_icon: z.string().nullish(),
agent_icon_background: z.string().nullish(),
agent_icon_type: z.string().nullish(),
app_id: z.string().nullish(),
display_name: z.string(),
name: z.string(),
node_id: z.string().nullish(),
node_name: z.string().nullish(),
type: z.string(),
workflow_icon: z.string().nullish(),
workflow_icon_background: z.string().nullish(),
workflow_icon_type: z.string().nullish(),
workflow_id: z.string().nullish(),
workflow_name: z.string().nullish(),
workflow_version: z.string().nullish(),
})
/**
* SkillReferenceListResponse
*/
export const zSkillReferenceListResponse = z.object({
data: z.array(zSkillReferenceResponse).optional(),
})
/**
* TenantAccountRole
*/
@@ -2193,6 +2566,69 @@ export const zPermissionCatalogResponse = z.object({
groups: z.array(zPermissionCatalogGroup).optional(),
})
/**
* SkillFileKind
*
* Draft file entry kind.
*/
export const zSkillFileKind = z.enum(['directory', 'file'])
/**
* SkillFileStorage
*
* How a draft file's content is stored.
*/
export const zSkillFileStorage = z.enum(['text', 'tool_file'])
/**
* SkillDraftTreeItemPayload
*/
export const zSkillDraftTreeItemPayload = z.object({
content: z.string().nullish(),
hash: z.string().nullish(),
kind: zSkillFileKind.optional().default('file'),
mime_type: z.string().nullish(),
path: z.string(),
size: z.int().gte(0).nullish(),
storage: zSkillFileStorage.nullish(),
tool_file_id: z.string().nullish(),
})
/**
* SkillDraftTreePayload
*/
export const zSkillDraftTreePayload = z.object({
expected_updated_at: z.int().nullish(),
files: z.array(zSkillDraftTreeItemPayload).optional(),
})
/**
* SkillFileCheckErrorResponse
*/
export const zSkillFileCheckErrorResponse = z.object({
code: z.string(),
message: z.string(),
})
/**
* SkillFileCheckItemResponse
*/
export const zSkillFileCheckItemResponse = z.object({
errors: z.array(zSkillFileCheckErrorResponse).optional(),
extension: z.string(),
filename: z.string(),
mime_type: z.string(),
path: z.string(),
size: z.int(),
})
/**
* SkillFileCheckResponse
*/
export const zSkillFileCheckResponse = z.object({
data: z.record(z.string(), zSkillFileCheckItemResponse).optional(),
})
/**
* Option
*/
@@ -3513,6 +3949,26 @@ export const zGetWorkspacesCurrentAgentProviderByProviderNameResponse = zAgentPr
*/
export const zGetWorkspacesCurrentAgentProvidersResponse = zAgentProviderListResponse
export const zGetWorkspacesCurrentAgentsByAgentIdSkillsPath = z.object({
agent_id: z.string(),
})
/**
* Agent Skill bindings
*/
export const zGetWorkspacesCurrentAgentsByAgentIdSkillsResponse = zAgentSkillBindingsResponse
export const zPutWorkspacesCurrentAgentsByAgentIdSkillsBody = zAgentSkillBindingsPayload
export const zPutWorkspacesCurrentAgentsByAgentIdSkillsPath = z.object({
agent_id: z.string(),
})
/**
* Agent Skill bindings replaced
*/
export const zPutWorkspacesCurrentAgentsByAgentIdSkillsResponse = zAgentSkillBindingsResponse
export const zGetWorkspacesCurrentCustomizedSnippetsQuery = z.object({
creators: z.array(z.string()).optional(),
is_published: z.boolean().optional(),
@@ -4808,6 +5264,245 @@ export const zGetWorkspacesCurrentRbacWorkspaceDatasetsAccessPoliciesByPolicyIdR
*/
export const zGetWorkspacesCurrentRbacWorkspaceDatasetsAccessPolicyResponse = zWorkspaceAccessMatrix
export const zGetWorkspacesCurrentSkillsQuery = z.object({
keyword: z.string().optional(),
limit: z.int().gte(1).lte(100).optional().default(20),
page: z.int().gte(1).lte(99999).optional().default(1),
tag: z.array(z.string()).optional(),
})
/**
* Workspace skills
*/
export const zGetWorkspacesCurrentSkillsResponse = zSkillListResponse
export const zPostWorkspacesCurrentSkillsBody = zSkillCreatePayload
/**
* Skill created
*/
export const zPostWorkspacesCurrentSkillsResponse = zSkillDetailResponse
export const zPostWorkspacesCurrentSkillsFilesUploadBody = z.object({
file: z.custom<Blob | File>((value) => value instanceof Blob || value instanceof File),
})
/**
* Skill draft file uploaded
*/
export const zPostWorkspacesCurrentSkillsFilesUploadResponse = zSkillFileUploadResponse
/**
* Skill imported
*/
export const zPostWorkspacesCurrentSkillsImportResponse = zSkillDetailResponse
/**
* Workspace Skill tags
*/
export const zGetWorkspacesCurrentSkillsTagsResponse = zSkillTagListResponse
export const zDeleteWorkspacesCurrentSkillsBySkillIdBody = zSkillDeletePayload
export const zDeleteWorkspacesCurrentSkillsBySkillIdPath = z.object({
skill_id: z.string(),
})
/**
* Skill deleted
*/
export const zDeleteWorkspacesCurrentSkillsBySkillIdResponse = zSkillDeleteResponse
export const zGetWorkspacesCurrentSkillsBySkillIdPath = z.object({
skill_id: z.string(),
})
/**
* Skill detail
*/
export const zGetWorkspacesCurrentSkillsBySkillIdResponse = zSkillDetailResponse
export const zPatchWorkspacesCurrentSkillsBySkillIdBody = zSkillMetadataPayload
export const zPatchWorkspacesCurrentSkillsBySkillIdPath = z.object({
skill_id: z.string(),
})
/**
* Skill updated
*/
export const zPatchWorkspacesCurrentSkillsBySkillIdResponse = zSkillResponse
export const zPostWorkspacesCurrentSkillsBySkillIdAssistMessagesBody = zSkillAssistMessagePayload
export const zPostWorkspacesCurrentSkillsBySkillIdAssistMessagesPath = z.object({
skill_id: z.string(),
})
/**
* Skill Authoring assistant event stream
*/
export const zPostWorkspacesCurrentSkillsBySkillIdAssistMessagesResponse = z.record(
z.string(),
z.unknown(),
)
export const zPostWorkspacesCurrentSkillsBySkillIdDuplicatePath = z.object({
skill_id: z.string(),
})
/**
* Skill duplicated
*/
export const zPostWorkspacesCurrentSkillsBySkillIdDuplicateResponse = zSkillDetailResponse
export const zGetWorkspacesCurrentSkillsBySkillIdExportPath = z.object({
skill_id: z.string(),
})
/**
* Published Skill zip archive
*/
export const zGetWorkspacesCurrentSkillsBySkillIdExportResponse = z.record(z.string(), z.unknown())
export const zPatchWorkspacesCurrentSkillsBySkillIdFilesBody = zSkillDraftFileOperationPayload
export const zPatchWorkspacesCurrentSkillsBySkillIdFilesPath = z.object({
skill_id: z.string(),
})
/**
* Draft file operation applied
*/
export const zPatchWorkspacesCurrentSkillsBySkillIdFilesResponse = zSkillDetailResponse
export const zPutWorkspacesCurrentSkillsBySkillIdFilesBody = zSkillDraftTreePayload
export const zPutWorkspacesCurrentSkillsBySkillIdFilesPath = z.object({
skill_id: z.string(),
})
/**
* Draft files replaced
*/
export const zPutWorkspacesCurrentSkillsBySkillIdFilesResponse = zSkillDetailResponse
export const zPostWorkspacesCurrentSkillsBySkillIdFilesCheckBody = zSkillDraftFileCheckPayload
export const zPostWorkspacesCurrentSkillsBySkillIdFilesCheckPath = z.object({
skill_id: z.string(),
})
/**
* Draft files checked
*/
export const zPostWorkspacesCurrentSkillsBySkillIdFilesCheckResponse = zSkillFileCheckResponse
export const zGetWorkspacesCurrentSkillsBySkillIdFilesContentPath = z.object({
skill_id: z.string(),
})
export const zGetWorkspacesCurrentSkillsBySkillIdFilesContentQuery = z.object({
download: z.string().optional(),
path: z.string(),
version_id: z.string().optional(),
})
/**
* Skill file content
*/
export const zGetWorkspacesCurrentSkillsBySkillIdFilesContentResponse = zBinaryFileResponse
export const zGetWorkspacesCurrentSkillsBySkillIdFilesPreviewPath = z.object({
skill_id: z.string(),
})
export const zGetWorkspacesCurrentSkillsBySkillIdFilesPreviewQuery = z.object({
path: z.string(),
version_id: z.string().optional(),
})
/**
* Skill file text preview
*/
export const zGetWorkspacesCurrentSkillsBySkillIdFilesPreviewResponse = zSkillFilePreviewResponse
export const zPostWorkspacesCurrentSkillsBySkillIdPublishBody = zSkillPublishPayload
export const zPostWorkspacesCurrentSkillsBySkillIdPublishPath = z.object({
skill_id: z.string(),
})
/**
* Skill published
*/
export const zPostWorkspacesCurrentSkillsBySkillIdPublishResponse = zSkillVersionResponse
export const zGetWorkspacesCurrentSkillsBySkillIdReferencesPath = z.object({
skill_id: z.string(),
})
/**
* Skill references
*/
export const zGetWorkspacesCurrentSkillsBySkillIdReferencesResponse = zSkillReferenceListResponse
export const zPostWorkspacesCurrentSkillsBySkillIdRestoreBody = zSkillRestorePayload
export const zPostWorkspacesCurrentSkillsBySkillIdRestorePath = z.object({
skill_id: z.string(),
})
/**
* Skill version restored to draft
*/
export const zPostWorkspacesCurrentSkillsBySkillIdRestoreResponse = zSkillDetailResponse
export const zGetWorkspacesCurrentSkillsBySkillIdVersionsPath = z.object({
skill_id: z.string(),
})
/**
* Skill versions
*/
export const zGetWorkspacesCurrentSkillsBySkillIdVersionsResponse = zSkillVersionListResponse
export const zDeleteWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdPath = z.object({
skill_id: z.string(),
version_id: z.string(),
})
/**
* Skill version deleted
*/
export const zDeleteWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponse =
zSkillVersionDeleteResponse
export const zGetWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdPath = z.object({
skill_id: z.string(),
version_id: z.string(),
})
/**
* Skill version detail
*/
export const zGetWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponse =
zSkillVersionDetailResponse
export const zPatchWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdBody =
zSkillVersionUpdatePayload
export const zPatchWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdPath = z.object({
skill_id: z.string(),
version_id: z.string(),
})
/**
* Skill version updated
*/
export const zPatchWorkspacesCurrentSkillsBySkillIdVersionsByVersionIdResponse =
zSkillVersionResponse
/**
* Success
*/
+18
View File
@@ -156,6 +156,21 @@ export type TemplateDetailResponse = {
export type DownloadPluginResponse = Blob
const bannerListContract = base
.route({
path: '/banners',
method: 'GET',
})
.input(
type<{
query: {
page: 'plugins'
language: string
}
}>(),
)
.output(type<unknown>())
const collectionsContract = base
.route({
path: '/collections',
@@ -229,6 +244,9 @@ const downloadPluginContract = base
.output(type<DownloadPluginResponse>())
export const marketplaceRouterContract = {
banners: {
list: bannerListContract,
},
collections: collectionsContract,
collectionPlugins: collectionPluginsContract,
searchAdvanced: searchAdvancedContract,
@@ -18,6 +18,7 @@ The current Flask-RESTX generator still emits these response entries under `appl
| service | GET | `/files/{file_id}/preview` | Original file MIME type, optionally attachment | `BinaryFileResponse` |
| console | GET | `/workspaces/current/plugin/icon` | Plugin asset MIME type | `BinaryFileResponse` |
| console | GET | `/workspaces/current/plugin/asset` | `application/octet-stream` | `BinaryFileResponse` |
| console | GET | `/workspaces/current/skills/{skill_id}/files/content` | Skill file MIME type, optionally attachment | `BinaryFileResponse` |
| console | GET | `/workspaces/current/tool-provider/builtin/{provider}/icon` | Tool icon MIME type | `BinaryFileResponse` |
| console | GET | `/workspaces/current/trigger-provider/{provider}/icon` | Trigger icon response | `BinaryFileResponse` |
| console | GET | `/workspaces/{tenant_id}/model-providers/{provider}/{icon_type}/{lang}` | Model provider icon MIME type | `BinaryFileResponse` |
@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.30314 1.54615C8.48087 1.50085 8.66708 1.49473 8.84742 1.52858C9.07685 1.57176 9.27914 1.6956 9.41968 1.77467L13.1768 3.88795C13.2961 3.955 13.4666 4.04398 13.6098 4.17701L13.6697 4.23691C13.7899 4.36839 13.8809 4.52463 13.9366 4.69394C14.0074 4.90941 13.9998 5.13856 13.9998 5.29485V9.55917C13.9998 9.70492 14.0069 9.91888 13.9444 10.1223C13.9076 10.2422 13.8527 10.3559 13.7823 10.4596L13.7068 10.5598C13.5702 10.7233 13.3869 10.8339 13.2647 10.9133L8.25171 14.1718C8.11671 14.2596 7.92272 14.396 7.69637 14.4537C7.51868 14.499 7.33292 14.5051 7.15275 14.4713C6.92315 14.4281 6.72034 14.3049 6.57984 14.2258L2.82268 12.1119C2.68658 12.0353 2.48283 11.9301 2.32984 11.7629C2.20953 11.6314 2.11852 11.475 2.06291 11.3059C1.99209 11.0903 1.99976 10.8606 1.99976 10.7044V6.44069C1.99976 6.29486 1.99262 6.08092 2.0551 5.87753L2.09807 5.7597C2.14656 5.64426 2.21216 5.53647 2.29273 5.44003L2.34611 5.38144C2.47424 5.24974 2.62775 5.15609 2.73479 5.08652L7.85979 1.75514C7.98194 1.67712 8.13342 1.58951 8.30314 1.54615ZM3.33309 10.7044C3.33309 10.7559 3.33334 10.7962 3.33374 10.8307C3.33392 10.8452 3.33411 10.8578 3.33439 10.8684C3.34356 10.8739 3.3543 10.8806 3.36695 10.8879C3.39682 10.9052 3.43208 10.9245 3.47697 10.9498L6.74064 12.7857V11.649L3.33309 9.73235V10.7044ZM8.07398 11.621V12.6972L12.5382 9.7955C12.5784 9.76933 12.6098 9.74883 12.6365 9.73105C12.6475 9.72368 12.6564 9.71639 12.6645 9.71087C12.6647 9.70125 12.6656 9.69004 12.6658 9.67701C12.6661 9.64494 12.6664 9.60721 12.6664 9.55917V8.636L8.07398 11.621ZM3.33309 8.2024L6.74064 10.1191V8.98235L3.33309 7.06568V8.2024ZM8.07398 8.95436V10.0299L12.6664 7.04485V5.96933L8.07398 8.95436ZM8.58374 2.87493L3.95288 5.8847L7.38192 7.81373L12.046 4.78183L8.76604 2.93613C8.71971 2.91007 8.68338 2.89008 8.6521 2.87298C8.63863 2.86561 8.62684 2.85931 8.61695 2.8541C8.60751 2.85987 8.59651 2.86685 8.58374 2.87493Z" fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

@@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path transform="translate(-5.75 -7.25)" d="M15.5 10.2912C18.875 10.6643 21.5 13.5256 21.5 17V23.75H8V17C8 13.5256 10.625 10.6643 14 10.2912V8H15.5V10.2912ZM14.75 20.75C16.821 20.75 18.5 19.071 18.5 17C18.5 14.9289 16.821 13.25 14.75 13.25C12.6789 13.25 11 14.9289 11 17C11 19.071 12.6789 20.75 14.75 20.75ZM14.75 19.25C13.5073 19.25 12.5 18.2427 12.5 17C12.5 15.7573 13.5073 14.75 14.75 14.75C15.9927 14.75 17 15.7573 17 17C17 18.2427 15.9927 19.25 14.75 19.25ZM14.75 17.75C15.1642 17.75 15.5 17.4142 15.5 17C15.5 16.5858 15.1642 16.25 14.75 16.25C14.3358 16.25 14 16.5858 14 17C14 17.4142 14.3358 17.75 14.75 17.75Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 746 B

@@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.87891 2.81952C13.2539 3.1926 15.8789 6.05391 15.8789 9.52832V16.2783H2.37891V9.52832C2.37891 6.05391 5.00394 3.1926 8.37891 2.81952V0.52832H9.87891V2.81952ZM14.3789 14.7783V9.52832C14.3789 6.62883 12.0284 4.27832 9.12891 4.27832C6.22941 4.27832 3.87891 6.62883 3.87891 9.52832V14.7783H14.3789ZM9.12891 13.2783C7.05784 13.2783 5.37891 11.5994 5.37891 9.52832C5.37891 7.45727 7.05784 5.77832 9.12891 5.77832C11.2 5.77832 12.8789 7.45727 12.8789 9.52832C12.8789 11.5994 11.2 13.2783 9.12891 13.2783ZM9.12891 11.7783C10.3716 11.7783 11.3789 10.771 11.3789 9.52832C11.3789 8.28565 10.3716 7.27832 9.12891 7.27832C7.88623 7.27832 6.87891 8.28565 6.87891 9.52832C6.87891 10.771 7.88623 11.7783 9.12891 11.7783ZM9.12891 10.2783C8.71468 10.2783 8.37891 9.94254 8.37891 9.52832C8.37891 9.1141 8.71468 8.77832 9.12891 8.77832C9.54313 8.77832 9.87891 9.1141 9.87891 9.52832C9.87891 9.94254 9.54313 10.2783 9.12891 10.2783Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path transform="translate(-5.75 -6.3)" d="M21.5 21.2999C21.5 21.7141 21.1642 22.0499 20.75 22.0499H8.75C8.33579 22.0499 8 21.7141 8 21.2999V13.4167C8 13.1852 8.10685 12.9667 8.28954 12.8247L14.2896 8.15798C14.5604 7.94734 14.9396 7.94734 15.2104 8.15798L21.2104 12.8247C21.3931 12.9667 21.5 13.1852 21.5 13.4167V21.2999ZM14 16.0499V20.5499H15.5V16.0499H14Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 486 B

@@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.75 14.2501H14.25V7.48367L9 3.40034L3.75 7.48367V14.2501H8.25V9.75006H9.75V14.2501ZM15.75 15.0001C15.75 15.4143 15.4142 15.7501 15 15.7501H3C2.58579 15.7501 2.25 15.4143 2.25 15.0001V7.11686C2.25 6.88542 2.35685 6.66694 2.53954 6.52485L8.53957 1.85818C8.8104 1.64753 9.1896 1.64753 9.46043 1.85818L15.4604 6.52485C15.6431 6.66694 15.75 6.88542 15.75 7.11686V15.0001Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 506 B

@@ -0,0 +1,7 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(-6.39355 -5.66187)" fill="currentColor">
<path d="M22.25 11.75C22.6642 11.75 23 12.0858 23 12.5V20C23 20.4142 22.6642 20.75 22.25 20.75H8.75C8.33579 20.75 8 20.4142 8 20V12.5C8 12.0858 8.33579 11.75 8.75 11.75H22.25Z"/>
<path d="M14 8C14.4142 8 14.75 8.33579 14.75 8.75V10.25H9.5V8.75C9.5 8.33579 9.83579 8 10.25 8H14Z"/>
<path d="M20.75 8C21.1642 8 21.5 8.33579 21.5 8.75V10.25H16.25V8.75C16.25 8.33579 16.5858 8 17 8H20.75Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 577 B

@@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.5 2.25C7.91421 2.25 8.25 2.58579 8.25 3V5.25H9.75V3C9.75 2.58579 10.0858 2.25 10.5 2.25H14.25C14.6642 2.25 15 2.58579 15 3V5.25H15.75C16.1642 5.25 16.5 5.58579 16.5 6V14.25C16.5 14.6642 16.1642 15 15.75 15H2.25C1.83579 15 1.5 14.6642 1.5 14.25V6C1.5 5.58579 1.83579 5.25 2.25 5.25H3V3C3 2.58579 3.33579 2.25 3.75 2.25H7.5ZM3 13.5H15V6.75H3V13.5ZM11.25 5.25H13.5V3.75H11.25V5.25ZM4.5 5.25H6.75V3.75H4.5V5.25Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 588 B

@@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path transform="translate(-6 -6.98706)" d="M21.5 20H10.25C9.83579 20 9.5 20.3358 9.5 20.75C9.5 21.1642 9.83579 21.5 10.25 21.5H21.5V23H10.25C9.00736 23 8 21.9927 8 20.75V9.5C8 8.67157 8.67157 8 9.5 8H21.5V20ZM17.75 13.25V11.75H11.75V13.25H17.75Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 375 B

@@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.75 13.5H4.5C4.08579 13.5 3.75 13.8358 3.75 14.25C3.75 14.6642 4.08579 15 4.5 15H15.75V16.5H4.5C3.25736 16.5 2.25 15.4927 2.25 14.25V3C2.25 2.17157 2.92157 1.5 3.75 1.5H15.75V13.5ZM3.75 12.0375C3.87117 12.0129 3.99658 12 4.125 12H14.25V3H3.75V12.0375ZM12 6.75H6V5.25H12V6.75Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 416 B

@@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path transform="translate(-7.25 -6.875)" d="M23.75 21.5V23H8.75V21.5H9.5V16.4317C8.59551 15.8263 8 14.7951 8 13.625C8 13.0046 8.16832 12.4072 8.47491 11.8981L10.509 8.375C10.6429 8.14295 10.8905 8 11.1585 8H21.3415C21.6094 8 21.8571 8.14295 21.991 8.375L24.0181 11.8863C24.3317 12.4072 24.5 13.0046 24.5 13.625C24.5 14.7951 23.9045 15.8263 23 16.4317V21.5H23.75ZM11.5915 9.5L9.76698 12.6599C9.59307 12.9488 9.5 13.2792 9.5 13.625C9.5 14.6605 10.3395 15.5 11.375 15.5C12.1482 15.5 12.8335 15.0277 13.1163 14.3221C13.3681 13.6942 14.257 13.6942 14.5087 14.3221C14.7915 15.0277 15.4767 15.5 16.25 15.5C17.0232 15.5 17.7085 15.0277 17.9914 14.3221C18.2431 13.6942 19.1319 13.6942 19.3836 14.3221C19.6665 15.0277 20.3518 15.5 21.125 15.5C22.1605 15.5 23 14.6605 23 13.625C23 13.2792 22.9069 12.9488 22.726 12.6481L20.9085 9.5H11.5915Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 959 B

@@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.75 9.93165V15H16.5V16.5H1.5V15H2.25V9.93165C1.34551 9.32625 0.75 8.29515 0.75 7.125C0.75 6.50464 0.918322 5.90716 1.22491 5.3981L3.25898 1.875C3.39295 1.64295 3.64054 1.5 3.90849 1.5H14.0915C14.3594 1.5 14.6071 1.64295 14.741 1.875L16.7681 5.38629C17.0817 5.90716 17.25 6.50464 17.25 7.125C17.25 8.29515 16.6545 9.32625 15.75 9.93165ZM14.25 10.4794C14.1269 10.493 14.0017 10.5 13.875 10.5C12.9307 10.5 12.0592 10.1085 11.4375 9.4599C10.8158 10.1085 9.94432 10.5 9 10.5C8.05568 10.5 7.18418 10.1085 6.5625 9.4599C5.94082 10.1085 5.06933 10.5 4.125 10.5C3.99825 10.5 3.87313 10.493 3.75 10.4794V15H14.25V10.4794ZM4.34149 3L2.51698 6.15991C2.34307 6.44882 2.25 6.77917 2.25 7.125C2.25 8.16052 3.08947 9 4.125 9C4.89822 9 5.5835 8.52772 5.86634 7.82212C6.11805 7.19417 7.00695 7.19417 7.25866 7.82212C7.54148 8.52772 8.22675 9 9 9C9.77325 9 10.4585 8.52772 10.7414 7.82212C10.9931 7.19417 11.8819 7.19417 12.1336 7.82212C12.4165 8.52772 13.1018 9 13.875 9C14.9105 9 15.75 8.16052 15.75 7.125C15.75 6.77917 15.6569 6.44882 15.476 6.1481L13.6585 3H4.34149Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path transform="translate(-6.2 -6.70433)" d="M8 18.6501C8 19.1406 8 19.3859 8.07199 19.6049C8.13567 19.7987 8.2398 19.9767 8.37747 20.1273C8.53307 20.2974 8.74686 20.4177 9.17444 20.6581L13.76 23.2375V21.0775L8 17.8819V18.6501ZM15.2 21.0775V23.2642L21.422 18.8858C21.78 18.6339 21.9591 18.5079 22.0885 18.3446C22.203 18.2001 22.2888 18.035 22.341 17.8581C22.4 17.6583 22.4 17.4394 22.4 17.0015V16.0375L15.2 21.0775ZM8 16.1582L13.76 19.2889V17.1175L8 13.9066V16.1582ZM15.2 17.1175V19.2775L22.4 14.2375V12.0775L15.2 17.1175ZM8.72 12.4375L14.48 15.6775L21.68 10.9975L17.1357 8.44137C16.6939 8.19286 16.473 8.0686 16.2398 8.02477C16.0335 7.98601 15.8213 7.99281 15.618 8.04468C15.388 8.10333 15.1755 8.24145 14.7505 8.51771L8.72 12.4375Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 863 B

@@ -0,0 +1,6 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.8922 4.70983L10.3274 2.14208C10.1065 2.01781 9.99601 1.95568 9.87944 1.93377C9.77627 1.91439 9.67021 1.91779 9.56848 1.94372C9.45356 1.97304 9.34729 2.04211 9.13475 2.18024L3.04371 6.13944C2.85218 6.26393 2.75642 6.32617 2.68704 6.4092C2.62562 6.4827 2.57949 6.56771 2.55133 6.65926C2.51953 6.76268 2.51953 6.8769 2.51953 7.10533V12.2864C2.51953 12.5317 2.51953 12.6544 2.55552 12.7639C2.58736 12.8607 2.63943 12.9498 2.70826 13.025C2.78607 13.1101 2.89296 13.1702 3.10675 13.2905L7.67163 15.8582C7.8926 15.9825 8.00305 16.0446 8.11962 16.0665C8.22279 16.0859 8.32885 16.0825 8.43059 16.0566C8.5455 16.0273 8.65177 15.9582 8.86424 15.8201L14.9553 11.8609C15.1468 11.7364 15.2426 11.6742 15.312 11.5911C15.3734 11.5176 15.4195 11.4326 15.4476 11.341C15.4795 11.2376 15.4795 11.1234 15.4795 10.895V5.71389C15.4795 5.4686 15.4795 5.34596 15.4435 5.23644C15.4116 5.13955 15.3596 5.05054 15.2907 4.97528C15.2129 4.89022 15.1061 4.8301 14.8922 4.70983Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="square" stroke-linejoin="round"/>
<path d="M15.1199 5.27417L8.27988 9.72024L2.87988 6.68271" stroke="currentColor" stroke-width="1.5" stroke-linecap="square" stroke-linejoin="round"/>
<path d="M15.4795 8.28003L8.27953 12.96L2.51953 9.72003" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.28027 15.48V9.71997" stroke="currentColor" stroke-width="1.5" stroke-linecap="square" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,7 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(-5.75 -6.5)" fill="currentColor">
<path d="M11.1062 16.269C10.1983 16.3989 9.50011 17.1812 9.5 18.125C9.50012 19.1604 10.3395 20 11.375 20H17.1304C17.4395 19.1265 18.2706 18.5 19.25 18.5C20.4926 18.5 21.5 19.5074 21.5 20.75C21.5 21.9926 20.4926 23 19.25 23C18.2706 23 17.4395 22.3735 17.1304 21.5H11.375C9.51111 21.5 8.00012 19.9889 8 18.125C8.00011 16.4242 9.25759 15.0177 10.8938 14.7837L11.1062 16.269Z"/>
<path d="M14.75 12.5C14.9307 12.5 15.0931 12.6111 15.158 12.7798L15.6802 14.1392C15.7564 14.3371 15.9129 14.4936 16.1108 14.5698L17.4702 15.092C17.6388 15.157 17.75 15.3193 17.75 15.5C17.75 15.6807 17.6388 15.843 17.4702 15.908L16.1108 16.4302C15.9129 16.5064 15.7564 16.6629 15.6802 16.8608L15.158 18.2202C15.0931 18.3888 14.9307 18.5 14.75 18.5C14.5693 18.5 14.4069 18.3888 14.342 18.2202L13.8198 16.8608C13.7436 16.6629 13.5871 16.5064 13.3892 16.4302L12.0298 15.908C11.8612 15.843 11.75 15.6807 11.75 15.5C11.75 15.3193 11.8612 15.157 12.0298 15.092L13.3892 14.5698C13.5871 14.4936 13.7436 14.3371 13.8198 14.1392L14.342 12.7798C14.4069 12.6111 14.5693 12.5 14.75 12.5Z"/>
<path d="M10.25 8C11.2294 8 12.0605 8.62649 12.3696 9.5H18.125C19.989 9.5 21.5 11.011 21.5 12.875C21.5 14.5759 20.2425 15.9815 18.6062 16.2156L18.3938 14.731C19.3017 14.6011 20 13.8189 20 12.875C20 11.8395 19.1605 11 18.125 11H12.3696C12.0605 11.8735 11.2294 12.5 10.25 12.5C9.00736 12.5 8 11.4926 8 10.25C8 9.00736 9.00736 8 10.25 8Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,5 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.3562 9.76904C4.44832 9.89892 3.75009 10.6811 3.75 11.625C3.75012 12.6604 4.58954 13.5 5.625 13.5H11.3804C11.6895 12.6265 12.5206 12 13.5 12C14.7426 12 15.75 13.0074 15.75 14.25C15.75 15.4926 14.7426 16.5 13.5 16.5C12.5206 16.5 11.6895 15.8735 11.3804 15H5.625C3.76111 15 2.25012 13.4889 2.25 11.625C2.25009 9.92417 3.50758 8.51773 5.1438 8.28369L5.3562 9.76904ZM13.5 13.5C13.0858 13.5 12.75 13.8358 12.75 14.25C12.75 14.6642 13.0858 15 13.5 15C13.9142 15 14.25 14.6642 14.25 14.25C14.25 13.8358 13.9142 13.5 13.5 13.5Z" fill="currentColor"/>
<path d="M9 6C9.18068 6 9.34308 6.11115 9.40796 6.27979L9.93018 7.63916C10.0064 7.83706 10.1629 7.99364 10.3608 8.06982L11.7202 8.59204C11.8888 8.65695 12 8.81929 12 9C12 9.18071 11.8888 9.34305 11.7202 9.40796L10.3608 9.93018C10.1629 10.0064 10.0064 10.1629 9.93018 10.3608L9.40796 11.7202C9.34308 11.8888 9.18068 12 9 12C8.81932 12 8.65692 11.8888 8.59204 11.7202L8.06982 10.3608C7.99363 10.1629 7.83707 10.0064 7.63916 9.93018L6.27979 9.40796C6.11119 9.34305 6 9.18071 6 9C6 8.81929 6.11119 8.65695 6.27979 8.59204L7.63916 8.06982C7.83707 7.99364 7.99363 7.83706 8.06982 7.63916L8.59204 6.27979C8.65692 6.11115 8.81932 6 9 6Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.5 1.5C5.47939 1.5 6.31054 2.12649 6.61963 3H12.375C14.239 3 15.75 4.51104 15.75 6.375C15.75 8.0759 14.4925 9.48151 12.8562 9.71558L12.6438 8.23096C13.5517 8.10109 14.25 7.31895 14.25 6.375C14.25 5.33946 13.4105 4.5 12.375 4.5H6.61963C6.31054 5.37351 5.47939 6 4.5 6C3.25736 6 2.25 4.99264 2.25 3.75C2.25 2.50736 3.25736 1.5 4.5 1.5ZM4.5 3C4.08579 3 3.75 3.33579 3.75 3.75C3.75 4.16421 4.08579 4.5 4.5 4.5C4.91421 4.5 5.25 4.16421 5.25 3.75C5.25 3.33579 4.91421 3 4.5 3Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -1,12 +1,15 @@
{
"prefix": "custom-vender",
"lastModified": 1782516559,
"lastModified": 1785319384,
"icons": {
"agent-v2-access-point": {
"body": "<g fill=\"none\"><path d=\"M7.5 11.25C7.91421 11.25 8.25 11.5858 8.25 12V14.25C8.25 14.6642 7.91421 15 7.5 15C7.08579 15 6.75 14.6642 6.75 14.25V12C6.75 11.5858 7.08579 11.25 7.5 11.25Z\" fill=\"currentColor\"/><path d=\"M2.19653 2.19653C2.48937 1.90372 2.96418 1.90382 3.25708 2.19653L8.03027 6.96973C8.09162 7.03108 8.13966 7.10082 8.17529 7.1748C8.19164 7.20869 8.20587 7.24378 8.21704 7.28027C8.24638 7.37633 8.25641 7.477 8.24634 7.57617C8.23743 7.66451 8.21216 7.74788 8.17529 7.82446C8.13963 7.89868 8.09176 7.96874 8.03027 8.03027L3.25708 12.8035C2.96419 13.096 2.48932 13.0962 2.19653 12.8035C1.90394 12.5107 1.90405 12.0358 2.19653 11.7429L5.68945 8.25H0.75C0.335786 8.25 0 7.91421 0 7.5C0 7.08579 0.335786 6.75 0.75 6.75H5.68945L2.19653 3.25708C1.90389 2.96423 1.90388 2.48937 2.19653 2.19653Z\" fill=\"currentColor\"/><path d=\"M10.1521 10.1521C10.445 9.85921 10.9198 9.85921 11.2126 10.1521L12.8035 11.7429C13.096 12.0358 13.0962 12.5107 12.8035 12.8035C12.5107 13.0962 12.0358 13.096 11.7429 12.8035L10.1521 11.2126C9.85921 10.9198 9.85922 10.445 10.1521 10.1521Z\" fill=\"currentColor\"/><path d=\"M14.25 6.75C14.6642 6.75 15 7.08579 15 7.5C15 7.91421 14.6642 8.25 14.25 8.25H12C11.5858 8.25 11.25 7.91421 11.25 7.5C11.25 7.08579 11.5858 6.75 12 6.75H14.25Z\" fill=\"currentColor\"/><path d=\"M11.7422 2.19653C12.035 1.90387 12.5098 1.90406 12.8027 2.19653C13.0956 2.4894 13.0955 2.96419 12.8027 3.25708L11.2119 4.8479C10.919 5.14079 10.4443 5.1408 10.1514 4.8479C9.85883 4.55497 9.85858 4.08013 10.1514 3.78735L11.7422 2.19653Z\" fill=\"currentColor\"/><path d=\"M7.5 0C7.91421 0 8.25 0.335786 8.25 0.75V3C8.25 3.41421 7.91421 3.75 7.5 3.75C7.08579 3.75 6.75 3.41421 6.75 3V0.75C6.75 0.335786 7.08579 0 7.5 0Z\" fill=\"currentColor\"/></g>",
"width": 15,
"height": 15
},
"agent-v2-building-blocks": {
"body": "<g fill=\"none\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M8.30314 1.54615C8.48087 1.50085 8.66708 1.49473 8.84742 1.52858C9.07685 1.57176 9.27914 1.6956 9.41968 1.77467L13.1768 3.88795C13.2961 3.955 13.4666 4.04398 13.6098 4.17701L13.6697 4.23691C13.7899 4.36839 13.8809 4.52463 13.9366 4.69394C14.0074 4.90941 13.9998 5.13856 13.9998 5.29485V9.55917C13.9998 9.70492 14.0069 9.91888 13.9444 10.1223C13.9076 10.2422 13.8527 10.3559 13.7823 10.4596L13.7068 10.5598C13.5702 10.7233 13.3869 10.8339 13.2647 10.9133L8.25171 14.1718C8.11671 14.2596 7.92272 14.396 7.69637 14.4537C7.51868 14.499 7.33292 14.5051 7.15275 14.4713C6.92315 14.4281 6.72034 14.3049 6.57984 14.2258L2.82268 12.1119C2.68658 12.0353 2.48283 11.9301 2.32984 11.7629C2.20953 11.6314 2.11852 11.475 2.06291 11.3059C1.99209 11.0903 1.99976 10.8606 1.99976 10.7044V6.44069C1.99976 6.29486 1.99262 6.08092 2.0551 5.87753L2.09807 5.7597C2.14656 5.64426 2.21216 5.53647 2.29273 5.44003L2.34611 5.38144C2.47424 5.24974 2.62775 5.15609 2.73479 5.08652L7.85979 1.75514C7.98194 1.67712 8.13342 1.58951 8.30314 1.54615ZM3.33309 10.7044C3.33309 10.7559 3.33334 10.7962 3.33374 10.8307C3.33392 10.8452 3.33411 10.8578 3.33439 10.8684C3.34356 10.8739 3.3543 10.8806 3.36695 10.8879C3.39682 10.9052 3.43208 10.9245 3.47697 10.9498L6.74064 12.7857V11.649L3.33309 9.73235V10.7044ZM8.07398 11.621V12.6972L12.5382 9.7955C12.5784 9.76933 12.6098 9.74883 12.6365 9.73105C12.6475 9.72368 12.6564 9.71639 12.6645 9.71087C12.6647 9.70125 12.6656 9.69004 12.6658 9.67701C12.6661 9.64494 12.6664 9.60721 12.6664 9.55917V8.636L8.07398 11.621ZM3.33309 8.2024L6.74064 10.1191V8.98235L3.33309 7.06568V8.2024ZM8.07398 8.95436V10.0299L12.6664 7.04485V5.96933L8.07398 8.95436ZM8.58374 2.87493L3.95288 5.8847L7.38192 7.81373L12.046 4.78183L8.76604 2.93613C8.71971 2.91007 8.68338 2.89008 8.6521 2.87298C8.63863 2.86561 8.62684 2.85931 8.61695 2.8541C8.60751 2.85987 8.59651 2.86685 8.58374 2.87493Z\" fill=\"currentColor\"/></g>"
},
"agent-v2-configure": {
"body": "<g fill=\"none\"><path d=\"M7.5 2.2912C10.875 2.66428 13.5 5.52559 13.5 9V15.75H0V9C0 5.52559 2.62504 2.66428 6 2.2912V0H7.5V2.2912ZM12 14.25V9C12 6.10051 9.6495 3.75 6.75 3.75C3.85051 3.75 1.5 6.10051 1.5 9V14.25H12ZM6.75 12.75C4.67893 12.75 3 11.071 3 9C3 6.92895 4.67893 5.25 6.75 5.25C8.82105 5.25 10.5 6.92895 10.5 9C10.5 11.071 8.82105 12.75 6.75 12.75ZM6.75 11.25C7.99268 11.25 9 10.2427 9 9C9 7.75732 7.99268 6.75 6.75 6.75C5.50732 6.75 4.5 7.75732 4.5 9C4.5 10.2427 5.50732 11.25 6.75 11.25ZM6.75 9.75C6.33578 9.75 6 9.41422 6 9C6 8.58578 6.33578 8.25 6.75 8.25C7.16422 8.25 7.5 8.58578 7.5 9C7.5 9.41422 7.16422 9.75 6.75 9.75Z\" fill=\"currentColor\"/></g>",
"width": 14
@@ -16,8 +19,7 @@
"width": 14
},
"agent-v2-configure-build": {
"body": "<g fill=\"none\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M9.66661 1.33333C9.8434 1.33335 10.013 1.40364 10.138 1.52865L11.4713 2.86198C11.5963 2.987 11.6666 3.15654 11.6666 3.33333C11.6666 3.51012 11.5963 3.67967 11.4713 3.80469L9.10932 6.16667L14.3046 11.362C14.5649 11.6223 14.5649 12.0443 14.3046 12.3047L12.3046 14.3047C12.0443 14.565 11.6223 14.565 11.3619 14.3047L6.16661 9.10938L4.4713 10.8047C4.3463 10.9297 4.17673 11 3.99995 11C3.82316 11 3.65361 10.9297 3.52859 10.8047L0.695261 7.97136C0.434913 7.71101 0.434913 7.289 0.695261 7.02865L6.19526 1.52865L6.24409 1.48438C6.36272 1.38718 6.51191 1.33333 6.66661 1.33333H9.66661ZM7.10932 8.16667L11.8333 12.8906L12.8906 11.8333L8.16661 7.10938L7.10932 8.16667ZM2.10932 7.5L3.99995 9.39063L10.0572 3.33333L9.39057 2.66667H6.94266L2.10932 7.5Z\" fill=\"currentColor\"/></g>",
"width": 16
"body": "<g fill=\"none\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M9.66661 1.33333C9.8434 1.33335 10.013 1.40364 10.138 1.52865L11.4713 2.86198C11.5963 2.987 11.6666 3.15654 11.6666 3.33333C11.6666 3.51012 11.5963 3.67967 11.4713 3.80469L9.10932 6.16667L14.3046 11.362C14.5649 11.6223 14.5649 12.0443 14.3046 12.3047L12.3046 14.3047C12.0443 14.565 11.6223 14.565 11.3619 14.3047L6.16661 9.10938L4.4713 10.8047C4.3463 10.9297 4.17673 11 3.99995 11C3.82316 11 3.65361 10.9297 3.52859 10.8047L0.695261 7.97136C0.434913 7.71101 0.434913 7.289 0.695261 7.02865L6.19526 1.52865L6.24409 1.48438C6.36272 1.38718 6.51191 1.33333 6.66661 1.33333H9.66661ZM7.10932 8.16667L11.8333 12.8906L12.8906 11.8333L8.16661 7.10938L7.10932 8.16667ZM2.10932 7.5L3.99995 9.39063L10.0572 3.33333L9.39057 2.66667H6.94266L2.10932 7.5Z\" fill=\"currentColor\"/></g>"
},
"agent-v2-configure-preview": {
"body": "<g fill=\"none\"><path d=\"M12.4756 4.75207L12.3112 5.12919C12.1909 5.40528 11.8091 5.40528 11.6887 5.12919L11.5244 4.75207C11.2314 4.07965 10.7037 3.54427 10.0451 3.25139L9.53867 3.02615C9.26487 2.90435 9.26487 2.50587 9.53867 2.38408L10.0168 2.17143C10.6923 1.87101 11.2295 1.31582 11.5174 0.620554L11.6862 0.21302C11.8039-0.0710068 12.1961-0.0710068 12.3137 0.21302L12.4825 0.620554C12.7705 1.31582 13.3077 1.87101 13.9832 2.17143L14.4613 2.38408C14.7351 2.50587 14.7351 2.90435 14.4613 3.02615L13.9549 3.25139C13.2963 3.54427 12.7686 4.07965 12.4756 4.75207ZM5.33333 1.33333H8V2.66667H5.33333C3.12419 2.66667 1.33333 4.45753 1.33333 6.66667C1.33333 9.07333 2.97472 10.6437 6.66667 12.3199V10.6667H8C10.2091 10.6667 12 8.8758 12 6.66667H13.3333C13.3333 9.6122 10.9455 12 8 12V14.3333C4.66667 13 0 11 0 6.66667C0 3.72115 2.38781 1.33333 5.33333 1.33333Z\" fill=\"currentColor\"/></g>",
@@ -25,8 +27,7 @@
"height": 15
},
"agent-v2-end-user-auth": {
"body": "<g fill=\"none\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M12 7.33325C13.1046 7.33325 14 8.22865 14 9.33325C14 10.1403 13.5218 10.8356 12.8333 11.1516V11.9999L12.3333 12.4999L12.8333 12.9511V13.6666L12 14.3333L11.1667 13.6666V11.1516C10.4782 10.8356 10 10.1403 10 9.33325C10 8.22865 10.8954 7.33325 12 7.33325ZM12 8.66659C11.6318 8.66659 11.3333 8.96505 11.3333 9.33325C11.3333 9.70145 11.6318 9.99992 12 9.99992C12.3682 9.99992 12.6667 9.70145 12.6667 9.33325C12.6667 8.96505 12.3682 8.66659 12 8.66659Z\" fill=\"currentColor\"/><path d=\"M8 7.99992C8.2545 7.99992 8.50382 8.01506 8.7474 8.04484L8.58594 9.36841C8.39687 9.34527 8.20127 9.33325 8 9.33325C5.8465 9.33325 4.25915 10.7274 3.78646 12.6666H10V13.9999H2.26758L2.33594 13.2708C2.61081 10.3473 4.82817 7.99992 8 7.99992Z\" fill=\"currentColor\"/><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M8 1.33325C9.65687 1.33325 11 2.6764 11 4.33325C11 5.99011 9.65687 7.33325 8 7.33325C6.34315 7.33325 5 5.99011 5 4.33325C5 2.6764 6.34315 1.33325 8 1.33325ZM8 2.66659C7.07953 2.66659 6.33333 3.41278 6.33333 4.33325C6.33333 5.25373 7.07953 5.99992 8 5.99992C8.92047 5.99992 9.66667 5.25373 9.66667 4.33325C9.66667 3.41278 8.92047 2.66659 8 2.66659Z\" fill=\"currentColor\"/></g>",
"width": 16
"body": "<g fill=\"none\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M12 7.33325C13.1046 7.33325 14 8.22865 14 9.33325C14 10.1403 13.5218 10.8356 12.8333 11.1516V11.9999L12.3333 12.4999L12.8333 12.9511V13.6666L12 14.3333L11.1667 13.6666V11.1516C10.4782 10.8356 10 10.1403 10 9.33325C10 8.22865 10.8954 7.33325 12 7.33325ZM12 8.66659C11.6318 8.66659 11.3333 8.96505 11.3333 9.33325C11.3333 9.70145 11.6318 9.99992 12 9.99992C12.3682 9.99992 12.6667 9.70145 12.6667 9.33325C12.6667 8.96505 12.3682 8.66659 12 8.66659Z\" fill=\"currentColor\"/><path d=\"M8 7.99992C8.2545 7.99992 8.50382 8.01506 8.7474 8.04484L8.58594 9.36841C8.39687 9.34527 8.20127 9.33325 8 9.33325C5.8465 9.33325 4.25915 10.7274 3.78646 12.6666H10V13.9999H2.26758L2.33594 13.2708C2.61081 10.3473 4.82817 7.99992 8 7.99992Z\" fill=\"currentColor\"/><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M8 1.33325C9.65687 1.33325 11 2.6764 11 4.33325C11 5.99011 9.65687 7.33325 8 7.33325C6.34315 7.33325 5 5.99011 5 4.33325C5 2.6764 6.34315 1.33325 8 1.33325ZM8 2.66659C7.07953 2.66659 6.33333 3.41278 6.33333 4.33325C6.33333 5.25373 7.07953 5.99992 8 5.99992C8.92047 5.99992 9.66667 5.25373 9.66667 4.33325C9.66667 3.41278 8.92047 2.66659 8 2.66659Z\" fill=\"currentColor\"/></g>"
},
"agent-v2-plan": {
"body": "<g fill=\"none\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M17 0C17.5523 0 18 0.447715 18 1V6C18 6.55228 17.5523 7 17 7H12C11.4477 7 11 6.55228 11 6V4.5H6.94629C5.92438 4.50039 5.56101 5.85276 6.44531 6.36523L12.5576 9.90332C15.2116 11.4402 14.1206 15.4996 11.0537 15.5H7V17C7 17.5523 6.55228 18 6 18H1C0.447715 18 0 17.5523 0 17V12C0 11.4477 0.447715 11 1 11H6C6.55228 11 7 11.4477 7 12V13.5H11.0537C12.0756 13.4996 12.4394 12.1472 11.5557 11.6348L5.44336 8.09668C2.789 6.55983 3.87917 2.50039 6.94629 2.5H11V1C11 0.447715 11.4477 0 12 0H17ZM2 16H5V13H2V16ZM13 5H16V2H13V5Z\" fill=\"currentColor\"/></g>",
@@ -35,8 +36,8 @@
},
"agent-v2-prompt-insert": {
"body": "<g fill=\"none\"><path d=\"M2.91669 1.16669C1.95019 1.16669 1.16669 1.95019 1.16669 2.91669V11.0834C1.16669 12.0499 1.95019 12.8334 2.91669 12.8334H11.0834C12.0499 12.8334 12.8334 12.0499 12.8334 11.0834V2.91669C12.8334 1.95019 12.0499 1.16669 11.0834 1.16669H2.91669ZM2.33335 2.91669C2.33335 2.59452 2.59452 2.33335 2.91669 2.33335H11.0834C11.4055 2.33335 11.6667 2.59452 11.6667 2.91669V11.0834C11.6667 11.4055 11.4055 11.6667 11.0834 11.6667H2.91669C2.59452 11.6667 2.33335 11.4055 2.33335 11.0834V2.91669ZM5.67188 10.5L9.67186 3.50002H8.32815L4.32817 10.5H5.67188Z\" fill=\"currentColor\"/></g>",
"height": 14,
"width": 14
"width": 14,
"height": 14
},
"agent-v2-robot-3": {
"body": "<g fill=\"none\"><path d=\"M6.25 6.875C6.82523 6.875 7.29167 7.34128 7.29167 7.91667V9.16667C7.29167 9.74205 6.82523 10.2083 6.25 10.2083C5.67477 10.2083 5.20833 9.74205 5.20833 9.16667V7.91667C5.20833 7.34128 5.67477 6.875 6.25 6.875Z\" fill=\"currentColor\"/><path d=\"M10.4167 6.875C10.992 6.875 11.4583 7.34135 11.4583 7.91667V9.16667C11.4583 9.74199 10.992 10.2083 10.4167 10.2083C9.84135 10.2083 9.375 9.74199 9.375 9.16667V7.91667C9.375 7.34135 9.84135 6.875 10.4167 6.875Z\" fill=\"currentColor\"/><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M8.33333 0C9.13875 0 9.79167 0.652918 9.79167 1.45833C9.79167 2.02329 9.46964 2.51173 8.99984 2.75391V3.33822C9.38912 3.34279 9.77995 3.35006 10.175 3.36263C11.6983 3.41112 12.7377 3.42425 13.6401 3.90951C14.375 4.30477 15.0255 4.97655 15.3971 5.72347C15.5468 6.02442 15.6427 6.33532 15.7056 6.66667H15.8333C16.2936 6.66667 16.6667 7.03976 16.6667 7.5V10C16.6667 10.4602 16.2936 10.8333 15.8333 10.8333H15.8285C15.8235 11.2254 15.813 11.5735 15.7869 11.8831C15.7386 12.4571 15.6361 12.9628 15.3971 13.4432C15.0254 14.1901 14.3749 14.8619 13.6401 15.2572C12.7377 15.7424 11.6982 15.7556 10.175 15.804C8.93336 15.8436 7.73328 15.8436 6.4917 15.804C4.96843 15.7556 3.92896 15.7424 3.02653 15.2572C2.29178 14.8619 1.64121 14.1902 1.26953 13.4432C1.03058 12.9628 0.928072 12.4571 0.87972 11.8831C0.853642 11.5735 0.843216 11.2254 0.838216 10.8333H0.833333C0.373096 10.8333 0 10.4602 0 10V7.5C0 7.03976 0.373096 6.66667 0.833333 6.66667H0.9611C1.02392 6.33532 1.11984 6.02442 1.26953 5.72347C1.64119 4.97649 2.29177 4.30475 3.02653 3.90951C3.92895 3.42425 4.96837 3.41112 6.4917 3.36263C6.88671 3.35006 7.27754 3.34279 7.66683 3.33822V2.75391C7.19703 2.51173 6.875 2.02329 6.875 1.45833C6.875 0.652918 7.52792 0 8.33333 0ZM10.1213 5.02848C8.91522 4.9901 7.75142 4.9901 6.54541 5.02848C4.85908 5.08217 4.29323 5.12091 3.81592 5.3776C3.38476 5.60954 2.98015 6.02734 2.76204 6.46566C2.65217 6.68652 2.57959 6.96168 2.54069 7.4235C2.50069 7.89854 2.5 8.50363 2.5 9.37825V9.78841C2.5 10.663 2.50069 11.2681 2.54069 11.7432C2.57959 12.205 2.65215 12.4801 2.76204 12.701C2.98015 13.1393 3.38475 13.5571 3.81592 13.7891C4.29321 14.0458 4.85904 14.0845 6.54541 14.1382C7.75141 14.1766 8.91523 14.1766 10.1213 14.1382C11.8075 14.0845 12.3734 14.0458 12.8507 13.7891C13.2819 13.5572 13.6865 13.1394 13.9046 12.701C14.0145 12.4801 14.0871 12.205 14.126 11.7432C14.166 11.2681 14.1667 10.663 14.1667 9.78841V9.37825C14.1667 8.50363 14.166 7.89854 14.126 7.4235C14.0871 6.96168 14.0145 6.68652 13.9046 6.46566C13.6865 6.02729 13.2819 5.60951 12.8507 5.3776C12.3734 5.12091 11.8075 5.08217 10.1213 5.02848Z\" fill=\"currentColor\"/></g>",
@@ -794,6 +795,16 @@
"width": 24,
"height": 24
},
"main-nav-agent": {
"body": "<g fill=\"none\"><path d=\"M9.87891 2.81952C13.2539 3.1926 15.8789 6.05391 15.8789 9.52832V16.2783H2.37891V9.52832C2.37891 6.05391 5.00394 3.1926 8.37891 2.81952V0.52832H9.87891V2.81952ZM14.3789 14.7783V9.52832C14.3789 6.62883 12.0284 4.27832 9.12891 4.27832C6.22941 4.27832 3.87891 6.62883 3.87891 9.52832V14.7783H14.3789ZM9.12891 13.2783C7.05784 13.2783 5.37891 11.5994 5.37891 9.52832C5.37891 7.45727 7.05784 5.77832 9.12891 5.77832C11.2 5.77832 12.8789 7.45727 12.8789 9.52832C12.8789 11.5994 11.2 13.2783 9.12891 13.2783ZM9.12891 11.7783C10.3716 11.7783 11.3789 10.771 11.3789 9.52832C11.3789 8.28565 10.3716 7.27832 9.12891 7.27832C7.88623 7.27832 6.87891 8.28565 6.87891 9.52832C6.87891 10.771 7.88623 11.7783 9.12891 11.7783ZM9.12891 10.2783C8.71468 10.2783 8.37891 9.94254 8.37891 9.52832C8.37891 9.1141 8.71468 8.77832 9.12891 8.77832C9.54313 8.77832 9.87891 9.1141 9.87891 9.52832C9.87891 9.94254 9.54313 10.2783 9.12891 10.2783Z\" fill=\"currentColor\"/></g>",
"width": 18,
"height": 18
},
"main-nav-agent-active": {
"body": "<g fill=\"none\"><path transform=\"translate(-5.75 -7.25)\" d=\"M15.5 10.2912C18.875 10.6643 21.5 13.5256 21.5 17V23.75H8V17C8 13.5256 10.625 10.6643 14 10.2912V8H15.5V10.2912ZM14.75 20.75C16.821 20.75 18.5 19.071 18.5 17C18.5 14.9289 16.821 13.25 14.75 13.25C12.6789 13.25 11 14.9289 11 17C11 19.071 12.6789 20.75 14.75 20.75ZM14.75 19.25C13.5073 19.25 12.5 18.2427 12.5 17C12.5 15.7573 13.5073 14.75 14.75 14.75C15.9927 14.75 17 15.7573 17 17C17 18.2427 15.9927 19.25 14.75 19.25ZM14.75 17.75C15.1642 17.75 15.5 17.4142 15.5 17C15.5 16.5858 15.1642 16.25 14.75 16.25C14.3358 16.25 14 16.5858 14 17C14 17.4142 14.3358 17.75 14.75 17.75Z\" fill=\"currentColor\"/></g>",
"width": 18,
"height": 18
},
"main-nav-app-home": {
"body": "<g fill=\"none\"><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\"/></g>",
"width": 16,
@@ -819,6 +830,16 @@
"width": 20,
"height": 20
},
"main-nav-home-v2": {
"body": "<g fill=\"none\"><path d=\"M9.75 14.2501H14.25V7.48367L9 3.40034L3.75 7.48367V14.2501H8.25V9.75006H9.75V14.2501ZM15.75 15.0001C15.75 15.4143 15.4142 15.7501 15 15.7501H3C2.58579 15.7501 2.25 15.4143 2.25 15.0001V7.11686C2.25 6.88542 2.35685 6.66694 2.53954 6.52485L8.53957 1.85818C8.8104 1.64753 9.1896 1.64753 9.46043 1.85818L15.4604 6.52485C15.6431 6.66694 15.75 6.88542 15.75 7.11686V15.0001Z\" fill=\"currentColor\"/></g>",
"width": 18,
"height": 18
},
"main-nav-home-v2-active": {
"body": "<g fill=\"none\"><path transform=\"translate(-5.75 -6.3)\" d=\"M21.5 21.2999C21.5 21.7141 21.1642 22.0499 20.75 22.0499H8.75C8.33579 22.0499 8 21.7141 8 21.2999V13.4167C8 13.1852 8.10685 12.9667 8.28954 12.8247L14.2896 8.15798C14.5604 7.94734 14.9396 7.94734 15.2104 8.15798L21.2104 12.8247C21.3931 12.9667 21.5 13.1852 21.5 13.4167V21.2999ZM14 16.0499V20.5499H15.5V16.0499H14Z\" fill=\"currentColor\"/></g>",
"width": 18,
"height": 18
},
"main-nav-integrations": {
"body": "<g fill=\"none\"><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\"/></g>",
"width": 20,
@@ -829,6 +850,16 @@
"width": 20,
"height": 20
},
"main-nav-integrations-v2": {
"body": "<g fill=\"none\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M7.5 2.25C7.91421 2.25 8.25 2.58579 8.25 3V5.25H9.75V3C9.75 2.58579 10.0858 2.25 10.5 2.25H14.25C14.6642 2.25 15 2.58579 15 3V5.25H15.75C16.1642 5.25 16.5 5.58579 16.5 6V14.25C16.5 14.6642 16.1642 15 15.75 15H2.25C1.83579 15 1.5 14.6642 1.5 14.25V6C1.5 5.58579 1.83579 5.25 2.25 5.25H3V3C3 2.58579 3.33579 2.25 3.75 2.25H7.5ZM3 13.5H15V6.75H3V13.5ZM11.25 5.25H13.5V3.75H11.25V5.25ZM4.5 5.25H6.75V3.75H4.5V5.25Z\" fill=\"currentColor\"/></g>",
"width": 18,
"height": 18
},
"main-nav-integrations-v2-active": {
"body": "<g fill=\"none\"><g transform=\"translate(-6.39355 -5.66187)\" fill=\"currentColor\"><path d=\"M22.25 11.75C22.6642 11.75 23 12.0858 23 12.5V20C23 20.4142 22.6642 20.75 22.25 20.75H8.75C8.33579 20.75 8 20.4142 8 20V12.5C8 12.0858 8.33579 11.75 8.75 11.75H22.25Z\"/><path d=\"M14 8C14.4142 8 14.75 8.33579 14.75 8.75V10.25H9.5V8.75C9.5 8.33579 9.83579 8 10.25 8H14Z\"/><path d=\"M20.75 8C21.1642 8 21.5 8.33579 21.5 8.75V10.25H16.25V8.75C16.25 8.33579 16.5858 8 17 8H20.75Z\"/></g></g>",
"width": 18,
"height": 18
},
"main-nav-knowledge": {
"body": "<g fill=\"none\"><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\"/></g>",
"width": 20,
@@ -839,6 +870,16 @@
"width": 20,
"height": 20
},
"main-nav-knowledge-v2": {
"body": "<g fill=\"none\"><path d=\"M15.75 13.5H4.5C4.08579 13.5 3.75 13.8358 3.75 14.25C3.75 14.6642 4.08579 15 4.5 15H15.75V16.5H4.5C3.25736 16.5 2.25 15.4927 2.25 14.25V3C2.25 2.17157 2.92157 1.5 3.75 1.5H15.75V13.5ZM3.75 12.0375C3.87117 12.0129 3.99658 12 4.125 12H14.25V3H3.75V12.0375ZM12 6.75H6V5.25H12V6.75Z\" fill=\"currentColor\"/></g>",
"width": 18,
"height": 18
},
"main-nav-knowledge-v2-active": {
"body": "<g fill=\"none\"><path transform=\"translate(-6 -6.98706)\" d=\"M21.5 20H10.25C9.83579 20 9.5 20.3358 9.5 20.75C9.5 21.1642 9.83579 21.5 10.25 21.5H21.5V23H10.25C9.00736 23 8 21.9927 8 20.75V9.5C8 8.67157 8.67157 8 9.5 8H21.5V20ZM17.75 13.25V11.75H11.75V13.25H17.75Z\" fill=\"currentColor\"/></g>",
"width": 18,
"height": 18
},
"main-nav-marketplace": {
"body": "<g fill=\"none\"><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\"/></g>",
"width": 20,
@@ -849,6 +890,16 @@
"width": 20,
"height": 20
},
"main-nav-marketplace-v2": {
"body": "<g fill=\"none\"><path d=\"M15.75 9.93165V15H16.5V16.5H1.5V15H2.25V9.93165C1.34551 9.32625 0.75 8.29515 0.75 7.125C0.75 6.50464 0.918322 5.90716 1.22491 5.3981L3.25898 1.875C3.39295 1.64295 3.64054 1.5 3.90849 1.5H14.0915C14.3594 1.5 14.6071 1.64295 14.741 1.875L16.7681 5.38629C17.0817 5.90716 17.25 6.50464 17.25 7.125C17.25 8.29515 16.6545 9.32625 15.75 9.93165ZM14.25 10.4794C14.1269 10.493 14.0017 10.5 13.875 10.5C12.9307 10.5 12.0592 10.1085 11.4375 9.4599C10.8158 10.1085 9.94432 10.5 9 10.5C8.05568 10.5 7.18418 10.1085 6.5625 9.4599C5.94082 10.1085 5.06933 10.5 4.125 10.5C3.99825 10.5 3.87313 10.493 3.75 10.4794V15H14.25V10.4794ZM4.34149 3L2.51698 6.15991C2.34307 6.44882 2.25 6.77917 2.25 7.125C2.25 8.16052 3.08947 9 4.125 9C4.89822 9 5.5835 8.52772 5.86634 7.82212C6.11805 7.19417 7.00695 7.19417 7.25866 7.82212C7.54148 8.52772 8.22675 9 9 9C9.77325 9 10.4585 8.52772 10.7414 7.82212C10.9931 7.19417 11.8819 7.19417 12.1336 7.82212C12.4165 8.52772 13.1018 9 13.875 9C14.9105 9 15.75 8.16052 15.75 7.125C15.75 6.77917 15.6569 6.44882 15.476 6.1481L13.6585 3H4.34149Z\" fill=\"currentColor\"/></g>",
"width": 18,
"height": 18
},
"main-nav-marketplace-v2-active": {
"body": "<g fill=\"none\"><path transform=\"translate(-7.25 -6.875)\" d=\"M23.75 21.5V23H8.75V21.5H9.5V16.4317C8.59551 15.8263 8 14.7951 8 13.625C8 13.0046 8.16832 12.4072 8.47491 11.8981L10.509 8.375C10.6429 8.14295 10.8905 8 11.1585 8H21.3415C21.6094 8 21.8571 8.14295 21.991 8.375L24.0181 11.8863C24.3317 12.4072 24.5 13.0046 24.5 13.625C24.5 14.7951 23.9045 15.8263 23 16.4317V21.5H23.75ZM11.5915 9.5L9.76698 12.6599C9.59307 12.9488 9.5 13.2792 9.5 13.625C9.5 14.6605 10.3395 15.5 11.375 15.5C12.1482 15.5 12.8335 15.0277 13.1163 14.3221C13.3681 13.6942 14.257 13.6942 14.5087 14.3221C14.7915 15.0277 15.4767 15.5 16.25 15.5C17.0232 15.5 17.7085 15.0277 17.9914 14.3221C18.2431 13.6942 19.1319 13.6942 19.3836 14.3221C19.6665 15.0277 20.3518 15.5 21.125 15.5C22.1605 15.5 23 14.6605 23 13.625C23 13.2792 22.9069 12.9488 22.726 12.6481L20.9085 9.5H11.5915Z\" fill=\"currentColor\"/></g>",
"width": 18,
"height": 18
},
"main-nav-quick-search": {
"body": "<g fill=\"none\"><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\"/></g>",
"width": 24,
@@ -864,6 +915,16 @@
"width": 18,
"height": 18
},
"main-nav-skill": {
"body": "<g fill=\"none\"><path d=\"M14.8922 4.70983L10.3274 2.14208C10.1065 2.01781 9.99601 1.95568 9.87944 1.93377C9.77627 1.91439 9.67021 1.91779 9.56848 1.94372C9.45356 1.97304 9.34729 2.04211 9.13475 2.18024L3.04371 6.13944C2.85218 6.26393 2.75642 6.32617 2.68704 6.4092C2.62562 6.4827 2.57949 6.56771 2.55133 6.65926C2.51953 6.76268 2.51953 6.8769 2.51953 7.10533V12.2864C2.51953 12.5317 2.51953 12.6544 2.55552 12.7639C2.58736 12.8607 2.63943 12.9498 2.70826 13.025C2.78607 13.1101 2.89296 13.1702 3.10675 13.2905L7.67163 15.8582C7.8926 15.9825 8.00305 16.0446 8.11962 16.0665C8.22279 16.0859 8.32885 16.0825 8.43059 16.0566C8.5455 16.0273 8.65177 15.9582 8.86424 15.8201L14.9553 11.8609C15.1468 11.7364 15.2426 11.6742 15.312 11.5911C15.3734 11.5176 15.4195 11.4326 15.4476 11.341C15.4795 11.2376 15.4795 11.1234 15.4795 10.895V5.71389C15.4795 5.4686 15.4795 5.34596 15.4435 5.23644C15.4116 5.13955 15.3596 5.05054 15.2907 4.97528C15.2129 4.89022 15.1061 4.8301 14.8922 4.70983Z\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"square\" stroke-linejoin=\"round\"/><path d=\"M15.1199 5.27417L8.27988 9.72024L2.87988 6.68271\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"square\" stroke-linejoin=\"round\"/><path d=\"M15.4795 8.28003L8.27953 12.96L2.51953 9.72003\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M8.28027 15.48V9.71997\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"square\" stroke-linejoin=\"round\"/></g>",
"width": 18,
"height": 18
},
"main-nav-skill-active": {
"body": "<g fill=\"none\"><path transform=\"translate(-6.2 -6.70433)\" d=\"M8 18.6501C8 19.1406 8 19.3859 8.07199 19.6049C8.13567 19.7987 8.2398 19.9767 8.37747 20.1273C8.53307 20.2974 8.74686 20.4177 9.17444 20.6581L13.76 23.2375V21.0775L8 17.8819V18.6501ZM15.2 21.0775V23.2642L21.422 18.8858C21.78 18.6339 21.9591 18.5079 22.0885 18.3446C22.203 18.2001 22.2888 18.035 22.341 17.8581C22.4 17.6583 22.4 17.4394 22.4 17.0015V16.0375L15.2 21.0775ZM8 16.1582L13.76 19.2889V17.1175L8 13.9066V16.1582ZM15.2 17.1175V19.2775L22.4 14.2375V12.0775L15.2 17.1175ZM8.72 12.4375L14.48 15.6775L21.68 10.9975L17.1357 8.44137C16.6939 8.19286 16.473 8.0686 16.2398 8.02477C16.0335 7.98601 15.8213 7.99281 15.618 8.04468C15.388 8.10333 15.1755 8.24145 14.7505 8.51771L8.72 12.4375Z\" fill=\"currentColor\"/></g>",
"width": 18,
"height": 18
},
"main-nav-studio": {
"body": "<g fill=\"none\"><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\"/></g>",
"width": 20,
@@ -874,6 +935,16 @@
"width": 20,
"height": 20
},
"main-nav-studio-v2": {
"body": "<g fill=\"none\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M5.3562 9.76904C4.44832 9.89892 3.75009 10.6811 3.75 11.625C3.75012 12.6604 4.58954 13.5 5.625 13.5H11.3804C11.6895 12.6265 12.5206 12 13.5 12C14.7426 12 15.75 13.0074 15.75 14.25C15.75 15.4926 14.7426 16.5 13.5 16.5C12.5206 16.5 11.6895 15.8735 11.3804 15H5.625C3.76111 15 2.25012 13.4889 2.25 11.625C2.25009 9.92417 3.50758 8.51773 5.1438 8.28369L5.3562 9.76904ZM13.5 13.5C13.0858 13.5 12.75 13.8358 12.75 14.25C12.75 14.6642 13.0858 15 13.5 15C13.9142 15 14.25 14.6642 14.25 14.25C14.25 13.8358 13.9142 13.5 13.5 13.5Z\" fill=\"currentColor\"/><path d=\"M9 6C9.18068 6 9.34308 6.11115 9.40796 6.27979L9.93018 7.63916C10.0064 7.83706 10.1629 7.99364 10.3608 8.06982L11.7202 8.59204C11.8888 8.65695 12 8.81929 12 9C12 9.18071 11.8888 9.34305 11.7202 9.40796L10.3608 9.93018C10.1629 10.0064 10.0064 10.1629 9.93018 10.3608L9.40796 11.7202C9.34308 11.8888 9.18068 12 9 12C8.81932 12 8.65692 11.8888 8.59204 11.7202L8.06982 10.3608C7.99363 10.1629 7.83707 10.0064 7.63916 9.93018L6.27979 9.40796C6.11119 9.34305 6 9.18071 6 9C6 8.81929 6.11119 8.65695 6.27979 8.59204L7.63916 8.06982C7.83707 7.99364 7.99363 7.83706 8.06982 7.63916L8.59204 6.27979C8.65692 6.11115 8.81932 6 9 6Z\" fill=\"currentColor\"/><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M4.5 1.5C5.47939 1.5 6.31054 2.12649 6.61963 3H12.375C14.239 3 15.75 4.51104 15.75 6.375C15.75 8.0759 14.4925 9.48151 12.8562 9.71558L12.6438 8.23096C13.5517 8.10109 14.25 7.31895 14.25 6.375C14.25 5.33946 13.4105 4.5 12.375 4.5H6.61963C6.31054 5.37351 5.47939 6 4.5 6C3.25736 6 2.25 4.99264 2.25 3.75C2.25 2.50736 3.25736 1.5 4.5 1.5ZM4.5 3C4.08579 3 3.75 3.33579 3.75 3.75C3.75 4.16421 4.08579 4.5 4.5 4.5C4.91421 4.5 5.25 4.16421 5.25 3.75C5.25 3.33579 4.91421 3 4.5 3Z\" fill=\"currentColor\"/></g>",
"width": 18,
"height": 18
},
"main-nav-studio-v2-active": {
"body": "<g fill=\"none\"><g transform=\"translate(-5.75 -6.5)\" fill=\"currentColor\"><path d=\"M11.1062 16.269C10.1983 16.3989 9.50011 17.1812 9.5 18.125C9.50012 19.1604 10.3395 20 11.375 20H17.1304C17.4395 19.1265 18.2706 18.5 19.25 18.5C20.4926 18.5 21.5 19.5074 21.5 20.75C21.5 21.9926 20.4926 23 19.25 23C18.2706 23 17.4395 22.3735 17.1304 21.5H11.375C9.51111 21.5 8.00012 19.9889 8 18.125C8.00011 16.4242 9.25759 15.0177 10.8938 14.7837L11.1062 16.269Z\"/><path d=\"M14.75 12.5C14.9307 12.5 15.0931 12.6111 15.158 12.7798L15.6802 14.1392C15.7564 14.3371 15.9129 14.4936 16.1108 14.5698L17.4702 15.092C17.6388 15.157 17.75 15.3193 17.75 15.5C17.75 15.6807 17.6388 15.843 17.4702 15.908L16.1108 16.4302C15.9129 16.5064 15.7564 16.6629 15.6802 16.8608L15.158 18.2202C15.0931 18.3888 14.9307 18.5 14.75 18.5C14.5693 18.5 14.4069 18.3888 14.342 18.2202L13.8198 16.8608C13.7436 16.6629 13.5871 16.5064 13.3892 16.4302L12.0298 15.908C11.8612 15.843 11.75 15.6807 11.75 15.5C11.75 15.3193 11.8612 15.157 12.0298 15.092L13.3892 14.5698C13.5871 14.4936 13.7436 14.3371 13.8198 14.1392L14.342 12.7798C14.4069 12.6111 14.5693 12.5 14.75 12.5Z\"/><path d=\"M10.25 8C11.2294 8 12.0605 8.62649 12.3696 9.5H18.125C19.989 9.5 21.5 11.011 21.5 12.875C21.5 14.5759 20.2425 15.9815 18.6062 16.2156L18.3938 14.731C19.3017 14.6011 20 13.8189 20 12.875C20 11.8395 19.1605 11 18.125 11H12.3696C12.0605 11.8735 11.2294 12.5 10.25 12.5C9.00736 12.5 8 11.4926 8 10.25C8 9.00736 9.00736 8 10.25 8Z\"/></g></g>",
"width": 18,
"height": 18
},
"main-nav-workspace-settings": {
"body": "<g fill=\"none\"><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></g>",
"width": 16,
@@ -1,7 +1,7 @@
{
"prefix": "custom-vender",
"name": "Dify Custom Vender",
"total": 331,
"total": 346,
"version": "0.0.0-private",
"author": {
"name": "LangGenius, Inc.",
@@ -14,11 +14,11 @@
},
"samples": [
"agent-v2-access-point",
"agent-v2-building-blocks",
"agent-v2-configure",
"agent-v2-configure-active",
"agent-v2-configure-build",
"agent-v2-configure-preview",
"agent-v2-end-user-auth"
"agent-v2-configure-preview"
],
"palette": false
}
+19 -8
View File
@@ -1,19 +1,30 @@
import type { SearchParams } from 'nuqs'
import Marketplace from '@/app/components/plugins/marketplace'
import AccountDropdown from '@/app/components/header/account-dropdown'
import { EmbeddedMarketplace } from '@/app/components/plugins/marketplace/embedded'
import { fetchPluginBanners } from '@/app/components/plugins/marketplace/home/banners'
import MarketplaceInstallPermissionProvider from '@/app/components/plugins/marketplace/marketplace-install-permission-provider'
import { getLocaleOnServer } from '@/i18n-config/server'
type MarketplacePageProps = {
searchParams?: Promise<SearchParams>
}
const MarketplacePage = async () => {
const initialBanners = await getLocaleOnServer()
.then(fetchPluginBanners)
.catch(() => undefined)
const MarketplacePage = ({ searchParams }: MarketplacePageProps) => {
return (
<div
id="marketplace-container"
className="flex h-full min-h-0 flex-col overflow-y-auto bg-background-default-subtle pr-1"
className="flex h-full min-h-0 flex-col overflow-y-auto bg-background-default"
>
<MarketplaceInstallPermissionProvider>
<Marketplace searchParams={searchParams} isMarketplacePlatform showInstallButton />
<EmbeddedMarketplace
initialBanners={initialBanners}
showInstallButton
variant="home"
homeHeaderActions={
<div className="p-0.5">
<AccountDropdown />
</div>
}
/>
</MarketplaceInstallPermissionProvider>
</div>
)
@@ -0,0 +1,5 @@
import SkillDetailPage from '@/features/skills/detail-page'
export default function Page() {
return <SkillDetailPage />
}
+5
View File
@@ -0,0 +1,5 @@
import SkillsPage from '@/features/skills/page'
export default function Page() {
return <SkillsPage />
}
+32 -11
View File
@@ -15,6 +15,7 @@ type CarouselProps = Readonly<{
opts?: CarouselOptions
plugins?: CarouselPlugin
orientation?: 'horizontal' | 'vertical'
overlay?: React.ReactNode
}>
type CarouselContextValue = {
@@ -49,7 +50,7 @@ type TCarousel = {
>
const Carousel: TCarousel = React.forwardRef(
({ orientation = 'horizontal', opts, plugins, className, children, ...props }, ref) => {
({ orientation = 'horizontal', opts, plugins, overlay, className, children, ...props }, ref) => {
const [carouselRef, api] = useEmblaCarousel(
{ ...opts, axis: orientation === 'horizontal' ? 'x' : 'y' },
plugins,
@@ -98,6 +99,35 @@ const Carousel: TCarousel = React.forwardRef(
canScrollNext,
}))
const carousel = overlay
? (
<div
className={cn('relative', className)}
role="region"
aria-roledescription="carousel"
{...props}
>
{overlay}
<div
ref={carouselRef}
className="overflow-hidden [border-radius:inherit]"
>
{children}
</div>
</div>
)
: (
<div
ref={carouselRef}
className={cn('relative overflow-hidden', className)}
role="region"
aria-roledescription="carousel"
{...props}
>
{children}
</div>
)
return (
<CarouselContext.Provider
value={{
@@ -112,16 +142,7 @@ const Carousel: TCarousel = React.forwardRef(
canScrollNext,
}}
>
<div
ref={carouselRef}
// onKeyDownCapture={handleKeyDown}
className={cn('relative overflow-hidden', className)}
role="region"
aria-roledescription="carousel"
{...props}
>
{children}
</div>
{carousel}
</CarouselContext.Provider>
)
},
@@ -91,9 +91,12 @@ describe('AgentRosterResponseContent', () => {
await user.click(processToggle)
expect(processToggle).toHaveAttribute('aria-expanded', 'true')
await waitFor(() => {
expect(screen.getByText('history answer')).toBeInTheDocument()
})
await waitFor(
() => {
expect(screen.getByText('history answer')).toBeInTheDocument()
},
{ timeout: 5000 },
)
expect(screen.queryByText('internal thought should not render')).not.toBeInTheDocument()
})
@@ -122,9 +125,12 @@ describe('AgentRosterResponseContent', () => {
render(<AgentRosterResponseContent item={item} />)
await user.click(screen.getByRole('button', { name: 'Thinking' }))
await waitFor(() => {
expect(screen.getByText('const answer = 42').tagName).toBe('CODE')
})
await waitFor(
() => {
expect(screen.getByText('const answer = 42').tagName).toBe('CODE')
},
{ timeout: 5000 },
)
})
it('should keep one collapsible thinking timeline while response parts interleave', async () => {
@@ -1,6 +1,17 @@
import { render, screen } from '@testing-library/react'
import { vi } from 'vitest'
import { Img } from '..'
vi.mock('@/app/components/base/image-gallery', () => ({
default: ({ srcs }: { srcs: string[] }) => (
<div data-testid="image-gallery">
{srcs.map((src) => (
<span key={src} data-testid="gallery-image" data-src={src} />
))}
</div>
),
}))
describe('Img', () => {
describe('Rendering', () => {
it('should render with the correct wrapper class', () => {
@@ -16,9 +27,9 @@ describe('Img', () => {
const gallery = screen.getByTestId('image-gallery')
expect(gallery).toBeInTheDocument()
const images = gallery.querySelectorAll('img')
const images = gallery.querySelectorAll('[data-testid="gallery-image"]')
expect(images).toHaveLength(1)
expect(images[0]).toHaveAttribute('src', 'https://example.com/image.png')
expect(images[0]).toHaveAttribute('data-src', 'https://example.com/image.png')
})
it('should pass src as single element array to ImageGallery', () => {
@@ -26,21 +37,21 @@ describe('Img', () => {
render(<Img src={testSrc} />)
const gallery = screen.getByTestId('image-gallery')
const images = gallery.querySelectorAll('img')
const images = gallery.querySelectorAll('[data-testid="gallery-image"]')
expect(images[0]).toHaveAttribute('src', testSrc)
expect(images[0]).toHaveAttribute('data-src', testSrc)
})
it('should render with different src values', () => {
const { rerender } = render(<Img src="https://example.com/first.png" />)
expect(screen.getByTestId('gallery-image')).toHaveAttribute(
'src',
'data-src',
'https://example.com/first.png',
)
rerender(<Img src="https://example.com/second.jpg" />)
expect(screen.getByTestId('gallery-image')).toHaveAttribute(
'src',
'data-src',
'https://example.com/second.jpg',
)
})
@@ -1,6 +1,10 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import ModelParameterModal from '../index'
const mocks = vi.hoisted(() => ({
setSettingsDestination: vi.fn(),
}))
let parameterRules: Array<Record<string, unknown>> | undefined = [
{
name: 'temperature',
@@ -41,6 +45,14 @@ let activeTextGenerationModelList: Array<Record<string, unknown>> = [
},
]
vi.mock('nuqs', () => ({
parseAsStringLiteral: () => ({
withDefault: () => ({}),
withOptions: () => ({}),
}),
useQueryState: () => [null, mocks.setSettingsDestination],
}))
vi.mock('@/context/provider-context', () => ({
useProviderContext: () => ({
isAPIKeySet: true,
@@ -94,16 +106,34 @@ vi.mock('../parameter-item', () => ({
vi.mock('../../model-selector', () => ({
default: ({
defaultModel,
modelList,
onConfigureEmptyState,
onHide,
onSelect,
}: {
onHide: () => void
defaultModel?: { provider: string; model: string }
modelList?: Array<{
provider: string
models: Array<{ model: string }>
}>
onConfigureEmptyState?: () => void
onHide?: () => void
onSelect: (value: { provider: string; model: string }) => void
}) => (
<div data-testid="model-selector">
<div
data-testid="model-selector"
data-default-provider={defaultModel?.provider ?? ''}
data-default-model={defaultModel?.model ?? ''}
data-provider-count={modelList?.length ?? 0}
>
<button onClick={() => onSelect({ provider: 'openai', model: 'gpt-4.1' })}>
Select GPT-4.1
</button>
<button onClick={() => onSelect({ provider: 'custom-provider', model: 'custom-model' })}>
Select custom model
</button>
<button onClick={onConfigureEmptyState}>configure-empty-model</button>
<button onClick={onHide}>hide</button>
</div>
),
@@ -235,6 +265,14 @@ describe('ModelParameterModal', () => {
expect(screen.getByRole('button', { name: /modelProvider\.modelSettings/i })).toBeDisabled()
})
it('opens provider settings from the model selector empty state', () => {
render(<ModelParameterModal {...defaultProps} provider="" modelId="" />)
fireEvent.click(screen.getByText('configure-empty-model'))
expect(mocks.setSettingsDestination).toHaveBeenCalledWith('provider')
})
it('should call onCompletionParamsChange when parameter changes and switch actions happen', () => {
render(<ModelParameterModal {...defaultProps} />)
openSettings()
@@ -296,6 +334,50 @@ describe('ModelParameterModal', () => {
})
})
it('uses the provided modelList to resolve selected model metadata', () => {
currentProvider = undefined
currentModel = undefined
activeTextGenerationModelList = []
render(
<ModelParameterModal
{...defaultProps}
provider="custom-provider"
modelId="custom-model"
modelList={
[
{
provider: 'custom-provider',
label: { en_US: 'Custom Provider' },
models: [
{
model: 'custom-model',
label: { en_US: 'Custom Model' },
model_properties: { mode: 'completion' },
features: ['tool-call'],
},
],
},
] as never
}
/>,
)
const selector = screen.getByTestId('model-selector')
expect(selector).toHaveAttribute('data-default-provider', 'custom-provider')
expect(selector).toHaveAttribute('data-default-model', 'custom-model')
expect(selector).toHaveAttribute('data-provider-count', '1')
fireEvent.click(screen.getByText('Select custom model'))
expect(defaultProps.setModel).toHaveBeenCalledWith({
provider: 'custom-provider',
modelId: 'custom-model',
mode: 'completion',
features: ['tool-call'],
})
})
it('should toggle debug mode when debug footer is clicked', () => {
render(<ModelParameterModal {...defaultProps} />)
openSettings()
@@ -5,10 +5,14 @@ import type { TriggerProps } from './types'
import type { Node, NodeOutPutVar } from '@/app/components/workflow/types'
import { cn } from '@langgenius/dify-ui/cn'
import { Popover, PopoverClose, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
import { useQueryState } from 'nuqs'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { ArrowNarrowLeft } from '@/app/components/base/icons/src/vender/line/arrows'
import Loading from '@/app/components/base/loading'
import {
settingsQueryParamName,
settingsQueryParser,
} from '@/app/components/header/account-setting/query-params'
import { PROVIDER_WITH_PRESET_TONE, STOP_PARAMETER_RULE } from '@/config'
import { useModelParameterRules } from '@/service/use-common'
import { useTextGenerationCurrentProviderAndModelAndModelList } from '../hooks'
@@ -69,6 +73,10 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
const { currentProvider, currentModel, activeTextGenerationModelList } =
useTextGenerationCurrentProviderAndModelAndModelList({ provider, model: modelId })
const selectableModelList = modelList ?? activeTextGenerationModelList
const [settingsDestination, setSettingsDestination] = useQueryState(
settingsQueryParamName,
settingsQueryParser,
)
const parameterRules: ModelParameterRule[] = useMemo(() => {
return parameterRulesData?.data || []
@@ -95,6 +103,10 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
})
}
const handleConfigureEmptyState = () => {
if (settingsDestination !== 'provider') void setSettingsDestination('provider')
}
const handleSwitch = (key: string, value: boolean, assignValue: ParameterValue) => {
if (!value) {
const newCompletionParams = { ...completionParams }
@@ -160,6 +172,7 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
isInWorkflow &&
'border border-workflow-block-parma-bg bg-workflow-block-parma-bg hover:bg-workflow-block-parma-bg',
)}
onConfigureEmptyState={handleConfigureEmptyState}
onSelect={handleChangeModel}
/>
</div>
@@ -250,15 +263,19 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
)}
</div>
{!hideDebugWithMultipleModel && (
<div
className="flex h-12.5 cursor-pointer items-center justify-between rounded-b-xl border-t border-t-divider-subtle px-4 system-sm-regular text-text-accent"
<button
type="button"
className="flex h-12.5 w-full cursor-pointer items-center justify-between rounded-b-xl border-t border-t-divider-subtle bg-transparent px-4 text-left system-sm-regular text-text-accent"
onClick={() => onDebugWithMultipleModelChange?.()}
>
{debugWithMultipleModel
? t(($) => $.debugAsSingleModel, { ns: 'appDebug' })
: t(($) => $.debugAsMultipleModel, { ns: 'appDebug' })}
<ArrowNarrowLeft className="size-3 rotate-180" />
</div>
<span
aria-hidden="true"
className="i-custom-vender-line-arrows-arrow-narrow-left size-3 rotate-180"
/>
</button>
)}
</PopoverContent>
</Popover>
@@ -624,22 +624,34 @@ describe('MainNav', () => {
expect(screen.getByRole('button', { name: 'common.account.account' })).not.toHaveTextContent(
Plan.team,
)
expect(screen.getByRole('link', { name: /common.mainNav.home/ })).toHaveAttribute('href', '/')
expect(screen.getByRole('link', { name: /common.menus.apps/ })).toHaveAttribute('href', '/apps')
expect(screen.getByRole('link', { name: /Agents/ })).toHaveAttribute('href', '/agents')
const homeLink = screen.getByRole('link', { name: /common.mainNav.home/ })
expect(homeLink).toHaveAttribute('href', '/')
expect(homeLink.querySelector('.i-custom-vender-main-nav-home-v2')).toBeInTheDocument()
const studioLink = screen.getByRole('link', { name: /common.menus.apps/ })
expect(studioLink).toHaveAttribute('href', '/apps')
expect(studioLink.querySelector('.i-custom-vender-main-nav-studio-v2')).toBeInTheDocument()
const agentsLink = screen.getByRole('link', { name: /Agents/ })
expect(agentsLink).toHaveAttribute('href', '/agents')
expect(screen.getByRole('link', { name: /Agents common.menus.status/ })).toBeInTheDocument()
expect(screen.getByRole('link', { name: /common.menus.datasets/ })).toHaveAttribute(
'href',
'/datasets',
)
expect(screen.getByRole('link', { name: /common.mainNav.integrations/ })).toHaveAttribute(
'href',
'/integrations/model-provider',
)
expect(screen.getByRole('link', { name: /common.mainNav.marketplace/ })).toHaveAttribute(
'href',
'/marketplace',
)
expect(agentsLink.querySelector('.i-custom-vender-main-nav-agent')).toBeInTheDocument()
const skillsLink = screen.getByRole('link', { name: /common.mainNav.skills/ })
expect(skillsLink).toHaveAttribute('href', '/skills')
expect(skillsLink.querySelector('.i-custom-vender-main-nav-skill')).toBeInTheDocument()
const knowledgeLink = screen.getByRole('link', { name: /common.menus.datasets/ })
expect(knowledgeLink).toHaveAttribute('href', '/datasets')
expect(
knowledgeLink.querySelector('.i-custom-vender-main-nav-knowledge-v2'),
).toBeInTheDocument()
const integrationsLink = screen.getByRole('link', { name: /common.mainNav.integrations/ })
expect(integrationsLink).toHaveAttribute('href', '/integrations/model-provider')
expect(
integrationsLink.querySelector('.i-custom-vender-main-nav-integrations-v2'),
).toBeInTheDocument()
const marketplaceLink = screen.getByRole('link', { name: /common.mainNav.marketplace/ })
expect(marketplaceLink).toHaveAttribute('href', '/marketplace')
expect(
marketplaceLink.querySelector('.i-custom-vender-main-nav-marketplace-v2'),
).toBeInTheDocument()
})
it('hides the roster entry when Agent v2 is disabled', () => {
@@ -648,6 +660,10 @@ describe('MainNav', () => {
renderMainNav()
expect(screen.queryByRole('link', { name: /Agents/ })).not.toBeInTheDocument()
expect(screen.getByRole('link', { name: /common.mainNav.skills/ })).toHaveAttribute(
'href',
'/skills',
)
})
it('hides the roster entry when the user lacks agent.manage', () => {
@@ -767,6 +783,7 @@ describe('MainNav', () => {
expect(screen.getByRole('link', { name: /common.mainNav.home/ })).toHaveAttribute('href', '/')
expect(screen.getByRole('link', { name: /common.menus.apps/ })).toHaveAttribute('href', '/apps')
expect(screen.queryByRole('link', { name: /Agents/ })).not.toBeInTheDocument()
expect(screen.queryByRole('link', { name: /common.mainNav.skills/ })).not.toBeInTheDocument()
expect(screen.getByRole('link', { name: /common.menus.datasets/ })).toHaveAttribute(
'href',
'/datasets',
@@ -196,6 +196,31 @@ describe('MainNavLayout', () => {
expect(screen.getByRole('main')).toHaveTextContent('new knowledge detail')
})
it('hides the global main nav on a skill detail route', () => {
;(usePathname as Mock).mockReturnValue('/skills/skill-1')
render(
<MainNavLayout>
<div>skill detail</div>
</MainNavLayout>,
)
expect(screen.queryByTestId('main-nav')).not.toBeInTheDocument()
expect(screen.getByRole('main')).toHaveTextContent('skill detail')
})
it('keeps the global main nav on the skills collection route', () => {
;(usePathname as Mock).mockReturnValue('/skills')
render(
<MainNavLayout>
<div>skills collection</div>
</MainNavLayout>,
)
expect(screen.getByTestId('main-nav')).toBeInTheDocument()
})
it.each([
'/datasets/create',
'/datasets/new/create',
@@ -6,7 +6,9 @@ import { cn } from '@langgenius/dify-ui/cn'
import Link from '@/next/link'
const NavIcon = ({ icon, className }: { icon: string; className?: string }) => (
<span aria-hidden className={cn(icon, 'h-5 w-5 shrink-0', className)} />
<span aria-hidden className={cn('flex size-5 shrink-0 items-center justify-center', className)}>
<span className={cn(icon, 'size-[18px]')} />
</span>
)
type MainNavLinkProps = {
@@ -30,7 +32,10 @@ const MainNavLink = ({ item, pathname, children }: MainNavLinkProps) => {
)}
>
<NavIcon icon={item.icon} className="group-aria-[current=page]:hidden" />
<NavIcon icon={item.activeIcon} className="hidden group-aria-[current=page]:block" />
<NavIcon
icon={item.activeIcon}
className="hidden drop-shadow-[0_0_4px_rgba(49,70,255,0.18)] group-aria-[current=page]:flex"
/>
<span className="min-w-0 truncate group-aria-[current=page]:text-shadow-[0px_0px_8px_var(--color-components-main-nav-glass-text-glow)]">
{item.label}
</span>
+34 -16
View File
@@ -32,6 +32,8 @@ export type DetailSidebarVisibilityOptions = Pick<
const VISIBLE_TO_ALL: MainNavRouteVisibility = () => true
const CAN_MANAGE_AGENTS: MainNavRouteVisibility = (options) => options.canManageAgents
const CAN_USE_APP_DEPLOY: MainNavRouteVisibility = (options) => options.canUseAppDeploy
const NOT_DATASET_OPERATOR: MainNavRouteVisibility = (options) =>
!options.isCurrentWorkspaceDatasetOperator
function isPathUnderRoute(pathname: string, route: string) {
return pathname === route || pathname.startsWith(`${route}/`)
@@ -43,8 +45,8 @@ export const MAIN_NAV_ROUTES = [
href: '/',
labelKey: 'mainNav.home',
active: (path: string) => path === '/' || path === '/explore/apps',
icon: 'i-custom-vender-main-nav-home',
activeIcon: 'i-custom-vender-main-nav-home-active',
icon: 'i-custom-vender-main-nav-home-v2',
activeIcon: 'i-custom-vender-main-nav-home-v2-active',
visibility: VISIBLE_TO_ALL,
},
{
@@ -55,8 +57,8 @@ export const MAIN_NAV_ROUTES = [
isPathUnderRoute(path, '/apps') ||
isPathUnderRoute(path, '/app') ||
isPathUnderRoute(path, '/snippets'),
icon: 'i-custom-vender-main-nav-studio',
activeIcon: 'i-custom-vender-main-nav-studio-active',
icon: 'i-custom-vender-main-nav-studio-v2',
activeIcon: 'i-custom-vender-main-nav-studio-v2-active',
visibility: VISIBLE_TO_ALL,
},
{
@@ -64,18 +66,27 @@ export const MAIN_NAV_ROUTES = [
href: '/agents',
label: 'Agents',
active: (path: string) => isPathUnderRoute(path, '/agents'),
icon: 'i-custom-vender-main-nav-roster',
activeIcon: 'i-custom-vender-main-nav-roster-active',
icon: 'i-custom-vender-main-nav-agent',
activeIcon: 'i-custom-vender-main-nav-agent-active',
visibility: CAN_MANAGE_AGENTS,
feature: 'agentV2',
},
{
key: 'skills',
href: '/skills',
labelKey: 'mainNav.skills',
active: (path: string) => isPathUnderRoute(path, '/skills'),
icon: 'i-custom-vender-main-nav-skill',
activeIcon: 'i-custom-vender-main-nav-skill-active',
visibility: NOT_DATASET_OPERATOR,
},
{
key: 'datasets',
href: '/datasets',
labelKey: 'menus.datasets',
active: (path: string) => isPathUnderRoute(path, '/datasets'),
icon: 'i-custom-vender-main-nav-knowledge',
activeIcon: 'i-custom-vender-main-nav-knowledge-active',
icon: 'i-custom-vender-main-nav-knowledge-v2',
activeIcon: 'i-custom-vender-main-nav-knowledge-v2-active',
visibility: VISIBLE_TO_ALL,
},
{
@@ -84,8 +95,8 @@ export const MAIN_NAV_ROUTES = [
labelKey: 'mainNav.integrations',
active: (path: string) =>
isPathUnderRoute(path, '/integrations') || isPathUnderRoute(path, '/tools'),
icon: 'i-custom-vender-main-nav-integrations',
activeIcon: 'i-custom-vender-main-nav-integrations-active',
icon: 'i-custom-vender-main-nav-integrations-v2',
activeIcon: 'i-custom-vender-main-nav-integrations-v2-active',
visibility: VISIBLE_TO_ALL,
},
{
@@ -94,8 +105,8 @@ export const MAIN_NAV_ROUTES = [
labelKey: 'mainNav.marketplace',
active: (path: string) =>
isPathUnderRoute(path, '/marketplace') || isPathUnderRoute(path, '/plugins'),
icon: 'i-custom-vender-main-nav-marketplace',
activeIcon: 'i-custom-vender-main-nav-marketplace-active',
icon: 'i-custom-vender-main-nav-marketplace-v2',
activeIcon: 'i-custom-vender-main-nav-marketplace-v2-active',
visibility: VISIBLE_TO_ALL,
feature: 'marketplace',
},
@@ -140,14 +151,21 @@ function isDatasetDetailPathname(pathname: string) {
return true
}
function isSkillDetailPathname(pathname: string) {
const [section, skillId] = pathname.split('/').filter(Boolean)
return section === 'skills' && !!skillId
}
export function shouldHideMainNavigation(pathname: string) {
const [section, namespace, knowledgeSpaceId] = pathname.split('/').filter(Boolean)
return (
section === 'datasets' &&
namespace === 'new' &&
!!knowledgeSpaceId &&
knowledgeSpaceId !== 'create'
(section === 'datasets' &&
namespace === 'new' &&
!!knowledgeSpaceId &&
knowledgeSpaceId !== 'create') ||
isSkillDetailPathname(pathname)
)
}
@@ -0,0 +1,116 @@
import type { ReactNode } from 'react'
import type { PluginBanner } from '../home/banners'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mockFetchPluginBanners = vi.fn()
vi.mock('@/context/i18n', () => ({
useLocale: () => 'zh-Hans',
}))
vi.mock('../home/banners', async (importOriginal) => {
const original = await importOriginal<typeof import('../home/banners')>()
return {
...original,
fetchPluginBanners: (...args: unknown[]) => mockFetchPluginBanners(...args),
}
})
vi.mock('../view', () => ({
MarketplaceView: ({
banners,
showInstallButton,
}: {
banners: PluginBanner[]
showInstallButton: boolean
}) => (
<div>
<p>Trending banners: {banners.length}</p>
<p>{showInstallButton ? 'Install enabled' : 'Install disabled'}</p>
</div>
),
}))
let queryClient: QueryClient
function Wrapper({ children }: { children: ReactNode }) {
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
}
describe('EmbeddedMarketplace', () => {
beforeEach(() => {
vi.clearAllMocks()
queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
gcTime: 0,
},
},
})
})
it('loads homepage banners on the client for the active locale', async () => {
mockFetchPluginBanners.mockResolvedValue([
{
id: 'banner-1',
title: 'Trending',
sort: 1,
language: 'zh-Hans',
style_type: 'blog',
content: {
blog_title: 'Dify update',
link: 'https://dify.ai/blog',
link_target_type: 'blog',
},
},
] satisfies PluginBanner[])
const { EmbeddedMarketplace } = await import('../embedded')
render(<EmbeddedMarketplace showInstallButton variant="home" />, { wrapper: Wrapper })
expect(await screen.findByText('Trending banners: 1')).toBeInTheDocument()
expect(screen.getByText('Install enabled')).toBeInTheDocument()
expect(mockFetchPluginBanners).toHaveBeenCalledWith('zh-Hans')
})
it('uses server-rendered homepage banners without requesting them again on hydration', async () => {
const initialBanners = [
{
id: 'banner-1',
title: 'Trending',
sort: 1,
language: 'zh-Hans',
style_type: 'blog',
content: {
blog_title: 'Dify update',
link: 'https://dify.ai/blog',
link_target_type: 'blog',
},
},
] satisfies PluginBanner[]
const { EmbeddedMarketplace } = await import('../embedded')
render(
<EmbeddedMarketplace initialBanners={initialBanners} showInstallButton variant="home" />,
{ wrapper: Wrapper },
)
expect(screen.getByText('Trending banners: 1')).toBeInTheDocument()
expect(mockFetchPluginBanners).not.toHaveBeenCalled()
})
it('does not request homepage banners for the default catalog variant', async () => {
const { EmbeddedMarketplace } = await import('../embedded')
render(<EmbeddedMarketplace variant="default" />, { wrapper: Wrapper })
expect(screen.getByText('Trending banners: 0')).toBeInTheDocument()
expect(mockFetchPluginBanners).not.toHaveBeenCalled()
})
})
@@ -1,10 +1,11 @@
import type { ReactNode } from 'react'
import type { ComponentProps, ReactNode } from 'react'
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Provider as JotaiProvider } from 'jotai'
import { describe, expect, it, vi } from 'vitest'
import { createNuqsTestWrapper } from '@/test/nuqs-testing'
import PluginTypeSwitch from '../plugin-type-switch'
import styles from '../plugin-type-switch.module.css'
vi.mock('#i18n', async () => {
const { withSelectorKey } = await import('@/test/i18n-mock')
@@ -13,7 +14,7 @@ vi.mock('#i18n', async () => {
}
})
const renderSwitch = (searchParams = '') => {
const renderSwitch = (searchParams = '', props?: ComponentProps<typeof PluginTypeSwitch>) => {
const { wrapper: NuqsWrapper, onUrlUpdate } = createNuqsTestWrapper({ searchParams })
const Wrapper = ({ children }: { children: ReactNode }) => (
<JotaiProvider>
@@ -21,7 +22,7 @@ const renderSwitch = (searchParams = '') => {
</JotaiProvider>
)
return { ...render(<PluginTypeSwitch />, { wrapper: Wrapper }), onUrlUpdate }
return { ...render(<PluginTypeSwitch {...props} />, { wrapper: Wrapper }), onUrlUpdate }
}
describe('PluginTypeSwitch', () => {
@@ -47,4 +48,28 @@ describe('PluginTypeSwitch', () => {
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
expect(onUrlUpdate.mock.calls.at(-1)?.[0].searchParams.get('category')).toBe('model')
})
it('exposes the selected category and updates the URL in the home variant', async () => {
const user = userEvent.setup()
const { onUrlUpdate } = renderSwitch('?category=all', { variant: 'home' })
const categoryGroup = screen.getByRole('group', { name: 'marketplace.allPlugins' })
expect(categoryGroup).toHaveClass('w-full', 'justify-start', 'gap-1')
const activeCategory = screen.getByRole('button', { name: 'category.all' })
const inactiveCategory = screen.getByRole('button', { name: 'category.models' })
expect(activeCategory).toHaveAttribute('aria-pressed', 'true')
expect(activeCategory).toHaveClass(styles.homeItem, styles.homeItemActive)
expect(inactiveCategory).toHaveClass(styles.homeItem)
expect(inactiveCategory).not.toHaveClass(styles.homeItemActive)
expect(screen.getByRole('button', { name: 'categorySingle.datasource' })).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'categorySingle.agent' })).toBeInTheDocument()
await user.click(screen.getByRole('button', { name: 'category.models' }))
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
const update = onUrlUpdate.mock.calls.at(-1)?.[0]
expect(update?.searchParams.get('category')).toBe('model')
expect(update?.options.scroll).toBe(false)
})
})
@@ -0,0 +1,65 @@
import type { ReactNode } from 'react'
import type { PluginBanner } from '../home/banners'
import { render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { mockFetchPluginBanners, mockGetLocaleOnServer } = vi.hoisted(() => ({
mockFetchPluginBanners: vi.fn(),
mockGetLocaleOnServer: vi.fn(),
}))
vi.mock('@/i18n-config/server', () => ({
getLocaleOnServer: mockGetLocaleOnServer,
}))
vi.mock('../home/banners', async (importOriginal) => {
const original = await importOriginal<typeof import('../home/banners')>()
return {
...original,
fetchPluginBanners: mockFetchPluginBanners,
}
})
vi.mock('../hydration-server', () => ({
HydrateQueryClient: ({ children }: { children: ReactNode }) => children,
}))
vi.mock('../view', () => ({
MarketplaceView: ({ banners }: { banners: PluginBanner[] }) => (
<p>Server banners: {banners.length}</p>
),
}))
describe('Marketplace server entry', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('prefetches localized homepage banners before rendering the standalone view', async () => {
mockGetLocaleOnServer.mockResolvedValue('en-US')
mockFetchPluginBanners.mockResolvedValue([
{
id: 'banner-1',
title: 'Trending',
sort: 1,
language: 'en-US',
style_type: 'blog',
content: {
blog_title: 'Dify update',
link: 'https://dify.ai/blog',
link_target_type: 'blog',
},
},
] satisfies PluginBanner[])
const { default: Marketplace } = await import('../index')
const element = await Marketplace({ variant: 'home' })
render(element)
expect(screen.getByText('Server banners: 1')).toBeInTheDocument()
expect(mockGetLocaleOnServer).toHaveBeenCalledOnce()
expect(mockFetchPluginBanners).toHaveBeenCalledWith('en-US')
})
})
@@ -0,0 +1,64 @@
import type { Plugin } from '@/app/components/plugins/types'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { ThemeProvider } from 'next-themes'
import { describe, expect, it, vi } from 'vitest'
import { PluginCategoryEnum } from '@/app/components/plugins/types'
import MarketplaceDetailDialog from '../index'
vi.mock('../../utils', () => ({
getPluginLinkInMarketplace: (
plugin: Plugin,
params: { language: string; source?: string; theme?: string; view: string },
) => `about:blank?plugin=${plugin.org}/${plugin.name}&language=${params.language}&source=${params.source}&theme=${params.theme}&view=${params.view}`,
}))
const plugin = {
type: 'plugin',
org: 'dify',
name: 'plugin-a',
plugin_id: 'plugin-a',
version: '1.0.0',
latest_version: '1.0.0',
latest_package_identifier: 'pkg',
icon: 'icon.png',
verified: true,
label: { 'en-US': 'Plugin A' },
brief: { 'en-US': 'Brief' },
description: { 'en-US': 'Description' },
introduction: 'Intro',
repository: 'https://github.com/dify/plugin-a',
category: PluginCategoryEnum.tool,
install_count: 42,
endpoint: { settings: [] },
tags: [],
badges: [],
verification: { authorized_category: 'community' },
from: 'marketplace',
} as Plugin
describe('MarketplaceDetailDialog', () => {
it('renders the marketplace detail route in modal mode and closes in place', async () => {
const user = userEvent.setup()
const onOpenChange = vi.fn()
render(
<ThemeProvider forcedTheme="dark">
<MarketplaceDetailDialog
open
plugin={plugin}
onOpenChange={onOpenChange}
/>
</ThemeProvider>,
)
const frame = screen.getByTitle('Plugin A · plugin.detailPanel.operation.detail')
expect(frame).toHaveAttribute(
'src',
'about:blank?plugin=dify/plugin-a&language=en-US&source=http://localhost:3000&theme=system&view=modal',
)
await user.click(screen.getByRole('button', { name: 'common.operation.close' }))
expect(onOpenChange).toHaveBeenCalledWith(false)
})
})
@@ -0,0 +1,102 @@
'use client'
import type { Plugin } from '@/app/components/plugins/types'
import { cn } from '@langgenius/dify-ui/cn'
import {
Dialog,
DialogCloseButton,
DialogContent,
DialogTitle,
} from '@langgenius/dify-ui/dialog'
import { useTheme } from 'next-themes'
import { useState } from 'react'
import { useLocale, useTranslation } from '#i18n'
import { getPluginLinkInMarketplace } from '../utils'
type MarketplaceDetailDialogProps = {
open: boolean
plugin: Plugin
onOpenChange: (open: boolean) => void
}
function MarketplaceDetailDialog({
open,
plugin,
onOpenChange,
}: MarketplaceDetailDialogProps) {
const { t } = useTranslation()
const locale = useLocale()
const { theme } = useTheme()
const [isLoading, setIsLoading] = useState(true)
const pluginLabel = plugin.label[locale] ?? plugin.label['en-US'] ?? plugin.name
const detailLabel = t(($) => $['detailPanel.operation.detail'], { ns: 'plugin' })
const detailURL = getPluginLinkInMarketplace(plugin, {
language: locale,
source: globalThis.location?.origin,
theme,
view: 'modal',
})
const handleOpenChange = (nextOpen: boolean) => {
if (!nextOpen)
setIsLoading(true)
onOpenChange(nextOpen)
}
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent
className="h-[min(800px,calc(100dvh-48px))] max-h-none! w-[min(1200px,calc(100vw-48px))] max-w-none! overflow-hidden! border-0 p-0 shadow-xl"
>
<DialogTitle className="sr-only">
{pluginLabel}
{' · '}
{detailLabel}
</DialogTitle>
<div
aria-hidden
className={cn(
'absolute inset-0 bg-background-default transition-opacity',
isLoading ? 'opacity-100' : 'pointer-events-none opacity-0',
)}
>
<div className="flex h-[52px] items-center px-6">
<div className="h-4 w-40 animate-pulse rounded-md bg-state-base-hover motion-reduce:animate-none" />
</div>
<div className="mx-auto flex w-full max-w-[1000px] gap-8 px-12 py-8">
<div className="flex flex-1 flex-col gap-4">
<div className="h-16 w-2/3 animate-pulse rounded-xl bg-state-base-hover motion-reduce:animate-none" />
<div className="h-4 w-full animate-pulse rounded-md bg-state-base-hover motion-reduce:animate-none" />
<div className="h-4 w-5/6 animate-pulse rounded-md bg-state-base-hover motion-reduce:animate-none" />
<div className="mt-8 h-72 w-full animate-pulse rounded-xl bg-state-base-hover motion-reduce:animate-none" />
</div>
<div className="hidden w-60 flex-col gap-4 lg:flex">
<div className="h-24 animate-pulse rounded-xl bg-state-base-hover motion-reduce:animate-none" />
<div className="h-52 animate-pulse rounded-xl bg-state-base-hover motion-reduce:animate-none" />
</div>
</div>
</div>
<iframe
className={cn(
'size-full border-0 bg-background-default transition-opacity',
isLoading ? 'opacity-0' : 'opacity-100',
)}
onLoad={() => setIsLoading(false)}
referrerPolicy="strict-origin-when-cross-origin"
src={detailURL}
title={`${pluginLabel} · ${detailLabel}`}
/>
<DialogCloseButton
aria-label={t(($) => $['operation.close'], { ns: 'common' })}
className="top-5 right-5 size-8 rounded-lg"
/>
<div
aria-hidden
className="pointer-events-none absolute inset-x-0 bottom-0 h-12 bg-linear-to-t from-background-default to-transparent"
/>
</DialogContent>
</Dialog>
)
}
export default MarketplaceDetailDialog
@@ -0,0 +1,40 @@
'use client'
import type { PluginBanner } from './home/banners'
import type { MarketplaceViewProps } from './view'
import { queryOptions, useQuery } from '@tanstack/react-query'
import { useLocale } from '@/context/i18n'
import { marketplaceQuery } from '@/service/client'
import { fetchPluginBanners } from './home/banners'
import { MarketplaceView } from './view'
const BANNER_STALE_TIME = 1000 * 60 * 5
export type EmbeddedMarketplaceProps = Omit<MarketplaceViewProps, 'banners'> & {
initialBanners?: PluginBanner[]
}
export function EmbeddedMarketplace({
initialBanners,
variant = 'default',
...props
}: EmbeddedMarketplaceProps) {
const locale = useLocale()
const input = {
query: {
page: 'plugins' as const,
language: locale,
},
}
const { data: banners = [] } = useQuery(
queryOptions({
queryKey: [...marketplaceQuery.banners.list.queryKey({ input }), locale],
queryFn: () => fetchPluginBanners(locale),
enabled: variant === 'home',
initialData: initialBanners,
staleTime: BANNER_STALE_TIME,
}),
)
return <MarketplaceView {...props} banners={banners} variant={variant} />
}
@@ -0,0 +1,12 @@
# Marketplace Catalog Home
The redesigned Marketplace catalog shell provides the shared header, hero, search, trending, tabs, and sticky category navigation used by the Plugins and Templates pages.
## Internal Modules
- `marketplace/list/list-wrapper`
- `marketplace/plugin-type-switch`
## External Modules
None.
@@ -0,0 +1,183 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import HomeCatalogNavigation from '../home-catalog-navigation'
import HomeCatalogTabs from '../home-catalog-tabs'
import { HomeStickyCatalogTabs, HomeStickyStateProvider } from '../home-sticky-state-provider'
import styles from '../home-sticky.module.css'
vi.mock('#i18n', async () => {
const { withSelectorKey } = await import('@/test/i18n-mock')
return {
useTranslation: () => ({
t: withSelectorKey((key: string, options?: { ns?: string }) =>
options?.ns ? `${options.ns}.${key}` : key,
),
}),
}
})
vi.mock('../../plugin-type-switch', () => ({
default: ({ className, variant }: { className?: string; variant?: string }) => (
<div data-testid="plugin-type-switch" className={className} data-variant={variant} />
),
}))
vi.mock('@/utils/var', () => ({
getMarketplaceUrl: (path: string) => `https://marketplace.dify.ai${path}?source=console`,
}))
describe('HomeCatalogNavigation', () => {
const renderNavigation = (isMarketplacePlatform: boolean) => {
return render(
<HomeStickyStateProvider>
<HomeStickyCatalogTabs>
<div data-testid="header-catalog-tabs" />
</HomeStickyCatalogTabs>
<HomeCatalogNavigation
catalogTabs={<HomeCatalogTabs isMarketplacePlatform={isMarketplacePlatform} />}
/>
</HomeStickyStateProvider>,
)
}
it('keeps template navigation inside the Marketplace platform', () => {
renderNavigation(true)
const navigationSection = screen.getByRole('region', { name: 'common.mainNav.marketplace' })
expect(navigationSection).toHaveClass(styles.catalogNavigation)
expect(navigationSection.firstElementChild).toHaveClass('w-full')
expect(navigationSection.firstElementChild).not.toHaveClass('mx-auto', 'max-w-[1200px]')
const activeTab = screen.getByRole('link', { name: 'plugin.marketplace.home.plugins' })
expect(activeTab).toHaveAttribute('aria-current', 'page')
expect(activeTab).toHaveAttribute('href', '/plugins')
expect(
screen.getByRole('link', { name: /plugin\.marketplace\.home\.templates/ }),
).toHaveAttribute('href', '/templates')
expect(screen.queryByText('plugin.marketplace.home.new')).not.toBeInTheDocument()
expect(screen.getByTestId('plugin-type-switch')).toHaveAttribute('data-variant', 'home')
})
it('keeps header tabs clickable', () => {
render(<HomeCatalogTabs isMarketplacePlatform />)
const pluginsTab = screen.getByRole('link', { name: 'plugin.marketplace.home.plugins' })
const templatesTab = screen.getByRole('link', { name: 'plugin.marketplace.home.templates' })
expect(pluginsTab).toHaveAttribute('href', '/plugins')
expect(pluginsTab).toHaveClass('cursor-pointer')
expect(templatesTab).toHaveAttribute('href', '/templates')
expect(templatesTab).toHaveClass('cursor-pointer')
})
it('marks Templates as active when rendering the Templates catalog', () => {
render(<HomeCatalogTabs activeTab="templates" isMarketplacePlatform />)
const pluginsTab = screen.getByRole('link', { name: 'plugin.marketplace.home.plugins' })
const templatesTab = screen.getByRole('link', { name: 'plugin.marketplace.home.templates' })
expect(pluginsTab).not.toHaveAttribute('aria-current')
expect(templatesTab).toHaveAttribute('aria-current', 'page')
expect(screen.queryByText('plugin.marketplace.home.new')).not.toBeInTheDocument()
})
it('renders a supplied catalog category navigation', () => {
render(
<HomeStickyStateProvider>
<HomeCatalogNavigation
catalogTabs={<HomeCatalogTabs activeTab="templates" isMarketplacePlatform />}
catalogCategories={<nav aria-label="Template categories">Template categories</nav>}
/>
</HomeStickyStateProvider>,
)
expect(screen.getByRole('navigation', { name: 'Template categories' })).toBeInTheDocument()
expect(screen.queryByTestId('plugin-type-switch')).not.toBeInTheDocument()
})
it('links Dify users to the hosted Marketplace templates page', () => {
renderNavigation(false)
expect(screen.getByRole('link', { name: 'plugin.marketplace.home.plugins' })).toHaveAttribute(
'href',
'https://marketplace.dify.ai/plugins?source=console',
)
expect(
screen.getByRole('link', { name: /plugin\.marketplace\.home\.templates/ }),
).toHaveAttribute('href', 'https://marketplace.dify.ai/templates?source=console')
})
it('shows the compact navigation and header tabs after reaching the sticky header', () => {
const scrollContainer = document.createElement('div')
scrollContainer.id = 'marketplace-container'
document.body.appendChild(scrollContainer)
renderNavigation(true)
const navigationSection = screen.getByRole('region', { name: 'common.mainNav.marketplace' })
const pinTrigger = navigationSection.previousElementSibling as HTMLElement
vi.spyOn(scrollContainer, 'getBoundingClientRect').mockReturnValue(new DOMRect(0, 0, 100, 100))
const triggerRect = vi
.spyOn(pinTrigger, 'getBoundingClientRect')
.mockReturnValue(new DOMRect(0, 49, 100, 100))
fireEvent.scroll(scrollContainer)
expect(screen.queryByTestId('header-catalog-tabs')).not.toBeInTheDocument()
triggerRect.mockReturnValue(new DOMRect(0, 48, 100, 100))
fireEvent.scroll(scrollContainer)
expect(navigationSection).toHaveClass(styles.catalogNavigationPinned)
expect(
screen.getByRole('navigation', { name: 'common.mainNav.marketplace' }).parentElement,
).toHaveClass(styles.catalogTabsPinned)
expect(screen.getByTestId('plugin-type-switch')).toHaveClass(styles.categoriesPinned)
expect(screen.getByTestId('header-catalog-tabs')).toBeInTheDocument()
triggerRect.mockReturnValue(new DOMRect(0, 49, 100, 100))
fireEvent.scroll(scrollContainer)
expect(navigationSection).not.toHaveClass(styles.catalogNavigationPinned)
expect(screen.queryByTestId('header-catalog-tabs')).not.toBeInTheDocument()
scrollContainer.remove()
})
it('keeps the pinned state when compact styling moves the sticky section', () => {
const scrollContainer = document.createElement('div')
scrollContainer.id = 'marketplace-container'
document.body.appendChild(scrollContainer)
renderNavigation(true)
const navigationSection = screen.getByRole('region', { name: 'common.mainNav.marketplace' })
const pinTrigger = navigationSection.previousElementSibling as HTMLElement
vi.spyOn(scrollContainer, 'getBoundingClientRect').mockReturnValue(new DOMRect(0, 0, 100, 100))
vi.spyOn(pinTrigger, 'getBoundingClientRect').mockReturnValue(new DOMRect(0, 47, 100, 0))
vi.spyOn(navigationSection, 'getBoundingClientRect').mockReturnValue(
new DOMRect(0, 49, 100, 60),
)
fireEvent.scroll(scrollContainer)
expect(navigationSection).toHaveClass(styles.catalogNavigationPinned)
expect(screen.getByTestId('header-catalog-tabs')).toBeInTheDocument()
scrollContainer.remove()
})
it('prevents scroll anchoring from reversing the sticky threshold', () => {
const scrollContainer = document.createElement('div')
scrollContainer.id = 'marketplace-container'
document.body.appendChild(scrollContainer)
const { unmount } = renderNavigation(true)
expect(scrollContainer.style.overflowAnchor).toBe('none')
unmount()
expect(scrollContainer.style.overflowAnchor).toBe('')
scrollContainer.remove()
})
})
@@ -0,0 +1,56 @@
import { render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import HomeHeader from '../home-header'
vi.mock('#i18n', async () => {
const { withSelectorKey } = await import('@/test/i18n-mock')
return {
useTranslation: () => ({
t: withSelectorKey((key: string) => key),
}),
}
})
vi.mock('@/context/i18n', () => ({
useDocLink: () => () => 'https://docs.dify.ai/en/home',
}))
vi.mock('../home-sticky-state-provider', () => ({
HomeStickyCatalogTabs: ({ children }: { children: React.ReactNode }) => children,
}))
describe('HomeHeader', () => {
it('links the Guide action to Dify documentation', () => {
render(<HomeHeader isMarketplacePlatform />)
const brandLink = screen.getByRole('link', { name: 'Dify Marketplace' })
const [lightLogo, darkLogo] = brandLink.querySelectorAll('img')
expect(lightLogo).toHaveAttribute('src', expect.stringContaining('dify-marketplace-logo.svg'))
expect(darkLogo).toHaveAttribute(
'src',
expect.stringContaining('dify-marketplace-logo-dark.svg'),
)
expect(lightLogo).toHaveAttribute('width', '141.761')
expect(lightLogo).toHaveAttribute('height', '16.386')
expect(darkLogo).toHaveAttribute('width', '141.761')
expect(darkLogo).toHaveAttribute('height', '16.386')
expect(screen.queryByText('mainNav.marketplace')).not.toBeInTheDocument()
const guideLink = screen.getByRole('link', { name: 'marketplace.home.guide' })
expect(guideLink).toHaveAttribute('href', 'https://docs.dify.ai/en/home')
expect(guideLink).toHaveAttribute('target', '_blank')
expect(guideLink).toHaveAttribute('rel', 'noopener noreferrer')
})
it('shows Templates as the active compact tab on the Templates catalog', () => {
render(<HomeHeader activeTab="templates" isMarketplacePlatform />)
expect(screen.getByRole('link', { name: 'marketplace.home.plugins' })).not.toHaveAttribute(
'aria-current',
)
expect(screen.getByRole('link', { name: 'marketplace.home.templates' })).toHaveAttribute(
'aria-current',
'page',
)
})
})
@@ -0,0 +1,28 @@
import { render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import HomeHero from '../home-hero'
vi.mock('#i18n', async () => {
const { withSelectorKey } = await import('@/test/i18n-mock')
return {
useTranslation: () => ({
t: withSelectorKey((key: string) => key),
}),
}
})
describe('HomeHero', () => {
it('renders catalog-specific copy when supplied', () => {
render(
<HomeHero
isMarketplacePlatform
title="Discover templates"
subtitle="Start faster with ready-to-use workflows."
/>,
)
expect(screen.getByRole('heading', { name: 'Discover templates' })).toBeInTheDocument()
expect(screen.getByText('Start faster with ready-to-use workflows.')).toBeInTheDocument()
expect(screen.queryByText('marketplace.home.heroTitle')).not.toBeInTheDocument()
})
})
@@ -0,0 +1,208 @@
import type { PluginBanner } from '../banners'
import { render, screen, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, expect, it, vi } from 'vitest'
import HomeTrending from '../home-trending'
vi.mock('#i18n', async () => {
const { withSelectorKey } = await import('@/test/i18n-mock')
return {
useTranslation: (namespace: string) => ({
t: withSelectorKey((key: string) => `${namespace}.${key}`),
}),
}
})
vi.mock('@/app/components/plugins/base/badges/partner', () => ({
default: () => <span data-testid="partner-badge" />,
}))
vi.mock('@/app/components/plugins/base/badges/verified', () => ({
default: () => <span data-testid="verified-badge" />,
}))
const banners: PluginBanner[] = [
{
id: 'recommend',
style_type: 'recommend',
title: 'Trending',
sort: 0,
language: 'en',
content: {
theme_type: 'hottest',
heading: 'Popular plugins',
description: 'Chosen from real usage.',
cards: [
{
item_type: 'plugin',
item_id: 'langgenius/dropbox',
display_name: 'Dropbox',
icon_url: '/api/v1/plugins/langgenius/dropbox/icon',
creator: 'langgenius',
badges: ['partner', 'verified'],
link: '/plugins/langgenius/dropbox',
card_position: 0,
},
{
item_type: 'plugin',
item_id: 'langgenius/zapier',
display_name: 'Zapier',
link: '/plugins/langgenius/zapier',
card_position: 1,
},
{
item_type: 'plugin',
item_id: 'langgenius/notion',
display_name: 'Notion',
link: '/plugins/langgenius/notion',
card_position: 2,
},
{
item_type: 'plugin',
item_id: 'langgenius/slack',
display_name: 'Slack',
link: '/plugins/langgenius/slack',
card_position: 3,
},
],
},
},
{
id: 'blog',
style_type: 'blog',
title: 'Dify Updates',
sort: 1,
language: 'en',
content: {
blog_title: 'Dify v1.9 new launch',
subtitle: 'New Agent node support',
description: 'Build agent workflows with the new Agent node.',
link: 'https://dify.ai/blog',
link_target_type: 'blog',
},
},
{
id: 'event',
style_type: 'event',
title: 'Duck Duck Go',
sort: 2,
language: 'en',
content: {
images: {
desktop: '/api/v1/banners/images/banners/duckduckgo.png',
mobile: '/api/v1/banners/images/banners/duckduckgo-mobile.png',
},
link: 'https://marketplace.dify.ai/plugin/langgenius/duckduckgo',
alt_text: 'DuckDuckGo plugin',
},
},
]
describe('HomeTrending', () => {
it('renders and switches between the three API-backed banner layouts', async () => {
const user = userEvent.setup()
render(<HomeTrending banners={banners} isMarketplacePlatform />)
expect(screen.getByRole('heading', { name: 'Popular plugins' })).toBeInTheDocument()
const recommendationSlide = screen.getByRole('group', { name: 'Trending' })
expect(
within(recommendationSlide)
.getAllByRole('link')
.map((link) => link.getAttribute('aria-label')),
).toEqual(['Dropbox', 'Zapier', 'Notion', 'Slack'])
await user.click(screen.getByRole('button', { name: 'Dify Updates' }))
expect(screen.getByRole('heading', { name: 'Dify v1.9 new launch' })).toBeInTheDocument()
expect(
screen.getByRole('link', {
name: 'Read more about Dify v1.9 new launch',
}),
).toHaveAttribute('href', 'https://dify.ai/blog')
await user.click(screen.getByRole('button', { name: 'Duck Duck Go' }))
expect(screen.getByRole('link', { name: 'DuckDuckGo plugin' })).toHaveAttribute(
'href',
'https://marketplace.dify.ai/plugin/langgenius/duckduckgo',
)
})
it('switches to the selected slide from the pagination with the keyboard', async () => {
const user = userEvent.setup()
render(<HomeTrending banners={banners} isMarketplacePlatform />)
const duckDuckGoButton = screen.getByRole('button', { name: 'Duck Duck Go' })
duckDuckGoButton.focus()
await user.keyboard('{Enter}')
expect(duckDuckGoButton).toHaveAttribute('aria-current', 'true')
expect(screen.getByRole('button', { name: 'Trending' })).not.toHaveAttribute('aria-current')
expect(screen.getByRole('group', { name: 'Duck Duck Go' })).toHaveAttribute(
'aria-hidden',
'false',
)
})
it('toggles the carousel between paused and playing states', async () => {
const user = userEvent.setup()
render(<HomeTrending banners={banners} isMarketplacePlatform />)
const pauseButton = screen.getByRole('button', {
name: 'plugin.marketplace.home.trendingPause',
})
pauseButton.focus()
await user.keyboard('{Enter}')
const playButton = screen.getByRole('button', {
name: 'plugin.marketplace.home.trendingPlay',
})
playButton.focus()
await user.keyboard(' ')
expect(
screen.getByRole('button', {
name: 'plugin.marketplace.home.trendingPause',
}),
).toBeInTheDocument()
})
it('starts with autoplay paused when reduced motion is enabled', () => {
const matchMedia = vi.spyOn(window, 'matchMedia').mockReturnValue({
matches: true,
media: '(prefers-reduced-motion: reduce)',
onchange: null,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
addListener: vi.fn(),
removeListener: vi.fn(),
dispatchEvent: vi.fn(),
})
render(<HomeTrending banners={banners} isMarketplacePlatform />)
expect(
screen.getByRole('button', {
name: 'plugin.marketplace.home.trendingPlay',
}),
).toBeInTheDocument()
matchMedia.mockRestore()
})
it('renders no carousel when the API returns no banners', () => {
render(<HomeTrending banners={[]} isMarketplacePlatform />)
expect(
screen.queryByRole('region', {
name: 'plugin.marketplace.home.trendingTitle',
}),
).not.toBeInTheDocument()
})
})
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

@@ -0,0 +1,200 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { marketplaceClient } from '@/service/client'
import { fetchPluginBanners } from './banners'
vi.mock('@/service/client', () => ({
marketplaceClient: {
banners: {
list: vi.fn(),
},
},
}))
const mockedListBanners = vi.mocked(marketplaceClient.banners.list)
describe('fetchPluginBanners', () => {
beforeEach(() => {
mockedListBanners.mockReset()
})
it('normalizes every public banner style in API sort order', async () => {
mockedListBanners.mockResolvedValue({
code: 0,
msg: 'success',
data: {
banners: [
{
id: 'event',
style_type: 'event',
title: 'Dify Event',
sort: 3,
language: 'en',
content: {
images: {
desktop: '/api/v1/banners/images/banners/event.png',
mobile: '/api/v1/banners/images/banners/event-mobile.png',
},
link: 'https://dify.ai/events',
alt_text: 'Dify Event',
activity_id: 'event-1',
},
},
{
id: 'recommend',
style_type: 'recommend',
title: 'Trending Now',
sort: 1,
language: 'en',
content: {
theme_type: 'hottest',
heading: 'Popular plugins',
description: 'Chosen from real usage.',
cards: [
{
item_type: 'plugin',
item_id: 'langgenius/fourth',
display_name: 'Fourth',
link: '/plugins/langgenius/fourth',
card_position: 3,
},
{
item_type: 'plugin',
item_id: 'langgenius/first',
display_name: 'First',
icon_url: '/api/v1/plugins/langgenius/first/icon',
creator: 'langgenius',
badges: ['verified', 'partner', 'unknown'],
link: '/plugins/langgenius/first',
card_position: 0,
},
{
item_type: 'plugin',
item_id: 'langgenius/third',
display_name: 'Third',
link: '/plugins/langgenius/third',
card_position: 2,
},
{
item_type: 'plugin',
item_id: 'langgenius/second',
display_name: 'Second',
link: '/plugins/langgenius/second',
card_position: 1,
},
],
},
},
{
id: 'ad',
style_type: 'ad',
title: 'Partner campaign',
sort: 4,
language: 'en',
content: {
images: {
desktop: '/api/v1/banners/images/banners/ad.webp',
},
link: 'https://example.com',
partner_id: 'partner-1',
campaign_id: 'campaign-1',
},
},
{
id: 'blog',
style_type: 'blog',
title: 'Dify Updates',
sort: 2,
language: 'en',
content: {
blog_title: 'Dify v1.9 new launch',
subtitle: 'New Agent node support',
description: 'Build agent workflows with the new Agent node.',
link: 'https://dify.ai/blog',
link_target_type: 'blog',
},
},
{
id: 'unsupported',
style_type: 'popup',
title: 'Unsupported',
sort: 0,
language: 'en',
content: {},
},
],
},
})
const banners = await fetchPluginBanners('en-US')
expect(mockedListBanners).toHaveBeenCalledWith({
query: {
page: 'plugins',
language: 'en-US',
},
})
expect(banners.map((banner) => banner.id)).toEqual(['recommend', 'blog', 'event', 'ad'])
const recommend = banners[0]
expect(recommend?.style_type).toBe('recommend')
if (recommend?.style_type === 'recommend') {
expect(recommend.content.cards.map((card) => card.display_name)).toEqual([
'First',
'Second',
'Third',
'Fourth',
])
expect(recommend.content.cards[0]).toMatchObject({
creator: 'langgenius',
badges: ['verified', 'partner'],
})
}
const event = banners[2]
expect(event?.style_type).toBe('event')
if (event?.style_type === 'event') {
expect(event.content.images).toEqual({
desktop: '/api/v1/banners/images/banners/event.png',
mobile: '/api/v1/banners/images/banners/event-mobile.png',
})
}
})
it('drops malformed banners and returns no placeholders for an empty response', async () => {
mockedListBanners
.mockResolvedValueOnce({
data: {
banners: [
{
id: 'empty-recommend',
style_type: 'recommend',
title: 'Empty',
sort: 0,
language: 'en',
content: {
theme_type: 'hottest',
cards: [],
},
},
{
id: 'event-without-desktop',
style_type: 'event',
title: 'Broken',
sort: 1,
language: 'en',
content: {
images: {
mobile: '/api/v1/banners/images/banners/mobile.png',
},
link: 'https://example.com',
},
},
],
},
})
.mockResolvedValueOnce('')
await expect(fetchPluginBanners('en-US')).resolves.toEqual([])
await expect(fetchPluginBanners('en-US')).resolves.toEqual([])
})
})
@@ -0,0 +1,282 @@
import { marketplaceClient } from '@/service/client'
const MAX_CARDS_PER_PAGE = 4
type BannerBase = {
id: string
title: string
sort: number
language: string
}
export type BannerRecommendCard = {
item_type: 'plugin' | 'template'
item_id: string
display_name: string
icon_url?: string
icon?: string
icon_background?: string
creator?: string
badges?: Array<'partner' | 'verified'>
link: string
card_position: number
}
export type BannerRecommend = BannerBase & {
style_type: 'recommend'
content: {
theme_type: 'newest' | 'hottest' | 'partner'
heading?: string
subheadings?: string[]
description?: string
cards: BannerRecommendCard[]
}
}
export type BannerBlog = BannerBase & {
style_type: 'blog'
content: {
blog_title: string
subtitle?: string
description?: string
link: string
link_target_type: 'blog' | 'github'
}
}
type BannerImageContent = {
images: {
desktop: string
tablet?: string
mobile?: string
}
link: string
alt_text?: string
activity_id?: string
}
export type BannerEvent = BannerBase & {
style_type: 'event'
content: BannerImageContent
}
export type BannerAd = BannerBase & {
style_type: 'ad'
content: BannerImageContent & {
partner_id?: string
campaign_id?: string
}
}
export type PluginBanner = BannerRecommend | BannerBlog | BannerEvent | BannerAd
const isRecord = (value: unknown): value is Record<string, unknown> => {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
const parseBannerBase = (value: Record<string, unknown>): BannerBase | null => {
if (
typeof value.id !== 'string' ||
!value.id ||
typeof value.title !== 'string' ||
!value.title ||
typeof value.sort !== 'number' ||
typeof value.language !== 'string' ||
!value.language
) {
return null
}
return {
id: value.id,
title: value.title,
sort: value.sort,
language: value.language,
}
}
const parseRecommendCard = (value: unknown): BannerRecommendCard | null => {
if (!isRecord(value)) return null
const itemType = value.item_type
const itemId = value.item_id
const displayName = value.display_name
if (
(itemType !== 'plugin' && itemType !== 'template') ||
typeof itemId !== 'string' ||
!itemId ||
typeof displayName !== 'string' ||
!displayName
) {
return null
}
const badges = Array.isArray(value.badges)
? value.badges.filter(
(badge): badge is 'partner' | 'verified' => badge === 'partner' || badge === 'verified',
)
: undefined
return {
item_type: itemType,
item_id: itemId,
display_name: displayName,
icon_url: typeof value.icon_url === 'string' ? value.icon_url : undefined,
icon: typeof value.icon === 'string' ? value.icon : undefined,
icon_background: typeof value.icon_background === 'string' ? value.icon_background : undefined,
creator: typeof value.creator === 'string' ? value.creator : undefined,
badges,
link: typeof value.link === 'string' ? value.link : '',
card_position: typeof value.card_position === 'number' ? value.card_position : 0,
}
}
const parseRecommendBanner = (
base: BannerBase,
content: Record<string, unknown>,
): BannerRecommend | null => {
const themeType = content.theme_type
if (themeType !== 'newest' && themeType !== 'hottest' && themeType !== 'partner') return null
const cards = Array.isArray(content.cards)
? content.cards
.map(parseRecommendCard)
.filter((card): card is BannerRecommendCard => Boolean(card))
.sort((a, b) => a.card_position - b.card_position)
.slice(0, MAX_CARDS_PER_PAGE)
: []
if (cards.length === 0) return null
const subheadings = Array.isArray(content.subheadings)
? content.subheadings.filter((item): item is string => typeof item === 'string')
: undefined
return {
...base,
style_type: 'recommend',
content: {
theme_type: themeType,
heading: typeof content.heading === 'string' ? content.heading : undefined,
subheadings,
description: typeof content.description === 'string' ? content.description : undefined,
cards,
},
}
}
const parseBlogBanner = (base: BannerBase, content: Record<string, unknown>): BannerBlog | null => {
const linkTargetType = content.link_target_type
if (
typeof content.blog_title !== 'string' ||
!content.blog_title ||
typeof content.link !== 'string' ||
!content.link ||
(linkTargetType !== 'blog' && linkTargetType !== 'github')
) {
return null
}
return {
...base,
style_type: 'blog',
content: {
blog_title: content.blog_title,
subtitle: typeof content.subtitle === 'string' ? content.subtitle : undefined,
description: typeof content.description === 'string' ? content.description : undefined,
link: content.link,
link_target_type: linkTargetType,
},
}
}
const parseImageBanner = (
base: BannerBase,
styleType: 'event' | 'ad',
content: Record<string, unknown>,
): BannerEvent | BannerAd | null => {
if (
!isRecord(content.images) ||
typeof content.images.desktop !== 'string' ||
!content.images.desktop ||
typeof content.link !== 'string' ||
!content.link
) {
return null
}
const imageContent: BannerImageContent = {
images: {
desktop: content.images.desktop,
tablet:
typeof content.images.tablet === 'string' && content.images.tablet
? content.images.tablet
: undefined,
mobile:
typeof content.images.mobile === 'string' && content.images.mobile
? content.images.mobile
: undefined,
},
link: content.link,
alt_text: typeof content.alt_text === 'string' ? content.alt_text : undefined,
activity_id: typeof content.activity_id === 'string' ? content.activity_id : undefined,
}
if (styleType === 'event') {
return {
...base,
style_type: 'event',
content: imageContent,
}
}
return {
...base,
style_type: 'ad',
content: {
...imageContent,
partner_id: typeof content.partner_id === 'string' ? content.partner_id : undefined,
campaign_id: typeof content.campaign_id === 'string' ? content.campaign_id : undefined,
},
}
}
const parsePluginBanner = (value: unknown): PluginBanner | null => {
if (!isRecord(value) || !isRecord(value.content)) return null
const base = parseBannerBase(value)
if (!base) return null
switch (value.style_type) {
case 'recommend':
return parseRecommendBanner(base, value.content)
case 'blog':
return parseBlogBanner(base, value.content)
case 'event':
case 'ad':
return parseImageBanner(base, value.style_type, value.content)
default:
return null
}
}
export const normalizePluginBanners = (response: unknown): PluginBanner[] => {
if (!isRecord(response) || !isRecord(response.data) || !Array.isArray(response.data.banners))
return []
return response.data.banners
.map(parsePluginBanner)
.filter((banner): banner is PluginBanner => Boolean(banner))
.sort((a, b) => a.sort - b.sort)
}
export const fetchPluginBanners = async (language: string): Promise<PluginBanner[]> => {
const response = await marketplaceClient.banners.list({
query: {
page: 'plugins',
language,
},
})
return normalizePluginBanners(response)
}
@@ -0,0 +1,83 @@
'use client'
import type { ReactNode } from 'react'
import { cn } from '@langgenius/dify-ui/cn'
import { useAtomValue, useSetAtom } from 'jotai'
import { useEffect, useRef } from 'react'
import { useTranslation } from '#i18n'
import PluginTypeSwitch from '../plugin-type-switch'
import { homeCatalogPinnedAtom } from './home-sticky-state'
import styles from './home-sticky.module.css'
type HomeCatalogNavigationProps = {
catalogCategories?: ReactNode
catalogTabs: ReactNode
}
const STICKY_TOP = 48
function HomeCatalogNavigation({ catalogCategories, catalogTabs }: HomeCatalogNavigationProps) {
const { t } = useTranslation()
const isPinned = useAtomValue(homeCatalogPinnedAtom)
const setIsPinned = useSetAtom(homeCatalogPinnedAtom)
const pinTriggerRef = useRef<HTMLSpanElement>(null)
useEffect(() => {
const scrollContainer = document.getElementById('marketplace-container')
if (!scrollContainer) return
const previousOverflowAnchor = scrollContainer.style.overflowAnchor
// The sticky section becomes shorter when its tabs move into the header.
// Prevent browser scroll anchoring from moving it back across the pin threshold.
scrollContainer.style.overflowAnchor = 'none'
const updatePinnedState = () => {
const pinTrigger = pinTriggerRef.current
if (!pinTrigger) return
const containerTop = scrollContainer.getBoundingClientRect().top
const triggerTop = pinTrigger.getBoundingClientRect().top
setIsPinned(triggerTop <= containerTop + STICKY_TOP)
}
updatePinnedState()
scrollContainer.addEventListener('scroll', updatePinnedState, { passive: true })
window.addEventListener('resize', updatePinnedState)
return () => {
scrollContainer.removeEventListener('scroll', updatePinnedState)
window.removeEventListener('resize', updatePinnedState)
scrollContainer.style.overflowAnchor = previousOverflowAnchor
}
}, [setIsPinned])
return (
<>
<span ref={pinTriggerRef} aria-hidden className={styles.catalogNavigationTrigger} />
<section
aria-label={t(($) => $['mainNav.marketplace'], { ns: 'common' })}
className={cn(
'w-full shrink-0 bg-background-default',
styles.catalogNavigation,
isPinned && styles.catalogNavigationPinned,
)}
>
<div className="w-full">
<div className={cn('-ml-2', isPinned && styles.catalogTabsPinned)}>{catalogTabs}</div>
{catalogCategories ? (
<div className={cn('mt-4', isPinned && styles.categoriesPinned)}>
{catalogCategories}
</div>
) : (
<PluginTypeSwitch
className={cn('mt-4', isPinned && styles.categoriesPinned)}
variant="home"
/>
)}
</div>
</section>
</>
)
}
export default HomeCatalogNavigation
@@ -0,0 +1,64 @@
import { cn } from '@langgenius/dify-ui/cn'
import { useTranslation } from '#i18n'
import Link from '@/next/link'
import { getMarketplaceUrl } from '@/utils/var'
export type HomeCatalogTab = 'plugins' | 'templates'
type HomeCatalogTabsProps = {
activeTab?: HomeCatalogTab
className?: string
isMarketplacePlatform: boolean
}
const HomeCatalogTabs = ({
activeTab = 'plugins',
className,
isMarketplacePlatform,
}: HomeCatalogTabsProps) => {
const { t } = useTranslation()
const pluginsHref = isMarketplacePlatform ? '/plugins' : getMarketplaceUrl('/plugins')
const templatesHref = isMarketplacePlatform ? '/templates' : getMarketplaceUrl('/templates')
const isPluginsActive = activeTab === 'plugins'
const isTemplatesActive = activeTab === 'templates'
const pluginsLabel = t(($) => $['marketplace.home.plugins'], { ns: 'plugin' })
const templatesLabel = t(($) => $['marketplace.home.templates'], { ns: 'plugin' })
return (
<nav
aria-label={t(($) => $['mainNav.marketplace'], { ns: 'common' })}
className={cn('flex h-8 items-center gap-1', className)}
>
<Link
href={pluginsHref}
aria-label={pluginsLabel}
aria-current={isPluginsActive ? 'page' : undefined}
className={cn(
'relative flex h-8 cursor-pointer items-start rounded-lg px-[9px] pt-2 outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid',
isPluginsActive ? 'body-sm-medium' : 'body-sm-regular',
isPluginsActive
? 'bg-state-base-active text-text-primary'
: 'text-text-tertiary hover:bg-state-base-hover',
)}
>
{pluginsLabel}
</Link>
<Link
href={templatesHref}
aria-label={templatesLabel}
aria-current={isTemplatesActive ? 'page' : undefined}
className={cn(
'relative flex h-8 cursor-pointer items-center rounded-[10px] p-2 outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid',
isTemplatesActive ? 'body-sm-medium' : 'body-sm-regular',
isTemplatesActive
? 'bg-state-base-active text-text-primary'
: 'text-text-tertiary hover:bg-state-base-hover',
)}
>
{templatesLabel}
</Link>
</nav>
)
}
export default HomeCatalogTabs
@@ -0,0 +1,87 @@
import type { HomeCatalogTab } from './home-catalog-tabs'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { useTranslation } from '#i18n'
import { useDocLink } from '@/context/i18n'
import Link from '@/next/link'
import MarketplaceLogoDark from '@/public/marketplace/dify-marketplace-logo-dark.svg'
import MarketplaceLogo from '@/public/marketplace/dify-marketplace-logo.svg'
import HomeCatalogTabs from './home-catalog-tabs'
import { HomeStickyCatalogTabs } from './home-sticky-state-provider'
import styles from './home-sticky.module.css'
type HomeHeaderProps = {
activeTab?: HomeCatalogTab
actions?: React.ReactNode
isMarketplacePlatform: boolean
}
function Guide() {
const docLink = useDocLink()
const { t } = useTranslation('plugin')
return (
<Link href={docLink()} target="_blank" rel="noopener noreferrer" className={styles.guide}>
<Button variant="ghost" size="large" className="min-w-[94px] gap-0.5 px-3 text-text-primary">
<span aria-hidden className="i-ri-map-2-line size-5" />
<span className="px-1 system-md-medium">{t(($) => $['marketplace.home.guide'])}</span>
</Button>
</Link>
)
}
const HomeHeader = ({ activeTab = 'plugins', actions, isMarketplacePlatform }: HomeHeaderProps) => {
return (
<header
className={cn(
'sticky top-0 z-50 flex w-full shrink-0 items-center gap-4 border-b border-divider-regular bg-background-default px-4 py-1.5 md:px-9',
styles.header,
)}
>
<div className="flex min-w-0 flex-1 items-center gap-4">
<Link
href="/"
aria-label="Dify Marketplace"
className="flex h-full w-[141.933px] shrink-0 items-center"
>
<img
alt=""
aria-hidden
className={cn(
'h-[16.386px] w-[141.761px] max-w-none shrink-0',
styles.marketplaceLogoLight,
)}
height="16.386"
src={MarketplaceLogo.src}
width="141.761"
/>
<img
alt=""
aria-hidden
className={cn(
'h-[16.386px] w-[141.761px] max-w-none shrink-0',
styles.marketplaceLogoDark,
)}
height="16.386"
src={MarketplaceLogoDark.src}
width="141.761"
/>
</Link>
<HomeStickyCatalogTabs>
<HomeCatalogTabs
activeTab={activeTab}
className={styles.headerCatalogTabs}
isMarketplacePlatform={isMarketplacePlatform}
/>
</HomeStickyCatalogTabs>
</div>
<div className="flex h-full min-w-0 flex-1 items-center justify-end gap-2.5">
<Guide />
{actions}
</div>
</header>
)
}
export default HomeHeader
@@ -0,0 +1,38 @@
import type { ReactNode } from 'react'
import { cn } from '@langgenius/dify-ui/cn'
import { useTranslation } from '#i18n'
type HomeHeroProps = {
isMarketplacePlatform: boolean
subtitle?: ReactNode
title?: ReactNode
}
const HomeHero = ({ isMarketplacePlatform, subtitle, title }: HomeHeroProps) => {
const { t } = useTranslation('plugin')
return (
<section
className={cn(
'relative flex shrink-0 justify-center bg-background-default px-4',
!isMarketplacePlatform && 'pt-6',
)}
>
<div className="relative flex h-[162px] w-full max-w-[726px] flex-col items-center pt-[41px]">
<div className="flex flex-col items-center gap-2 text-center">
<h1
className="text-[28px] leading-[1.2] font-medium tracking-[-0.56px] text-text-primary"
style={{ fontFamily: "var(--font-family-brand, 'Söhne', var(--font-sans))" }}
>
{title ?? t(($) => $['marketplace.home.heroTitle'])}
</h1>
<p className="text-[13px] leading-4 font-light tracking-[-0.065px] text-text-tertiary">
{subtitle ?? t(($) => $['marketplace.home.heroSubtitle'])}
</p>
</div>
</div>
</section>
)
}
export default HomeHero
@@ -0,0 +1,50 @@
'use client'
import type { ReactNode } from 'react'
import { cn } from '@langgenius/dify-ui/cn'
import { useEffect, useRef } from 'react'
import { useTranslation } from '#i18n'
import SearchBoxWrapper from '@/app/components/plugins/marketplace/search-box/search-box-wrapper'
import styles from './home-sticky.module.css'
const HomeSearch = ({ children }: { children?: ReactNode }) => {
const searchRef = useRef<HTMLDivElement>(null)
const { t } = useTranslation('plugin')
useEffect(() => {
const handleGlobalSearchShortcut = (event: KeyboardEvent) => {
if (event.key.toLowerCase() !== 'k' || (!event.metaKey && !event.ctrlKey)) return
event.preventDefault()
searchRef.current?.querySelector('input')?.focus()
}
document.addEventListener('keydown', handleGlobalSearchShortcut)
return () => document.removeEventListener('keydown', handleGlobalSearchShortcut)
}, [])
return (
<div
className={cn(
'pointer-events-none sticky z-[60] -mt-9 flex h-9 shrink-0 justify-center px-4',
styles.search,
)}
>
<div ref={searchRef} className="pointer-events-auto relative w-full max-w-[420px]">
{children ?? (
<SearchBoxWrapper
wrapperClassName="w-full max-w-none"
inputClassName="h-9 w-full rounded-[10px] bg-components-input-bg-normal [&>div]:px-2.5"
inputElementClassName="text-[14px] leading-5"
searchIconName="i-ri-search-line"
placeholder={t(($) => $['marketplace.home.searchPlaceholder'])}
showTags={false}
usedInMarketplace={false}
/>
)}
</div>
</div>
)
}
export default HomeSearch
@@ -0,0 +1,20 @@
'use client'
import type { ReactNode } from 'react'
import { useAtomValue } from 'jotai'
import { ScopeProvider } from 'jotai-scope'
import { homeCatalogPinnedAtom, homeStickyScopedAtoms } from './home-sticky-state'
export function HomeStickyStateProvider({ children }: { children: ReactNode }) {
return (
<ScopeProvider atoms={homeStickyScopedAtoms} name="MarketplaceHomeSticky">
{children}
</ScopeProvider>
)
}
export function HomeStickyCatalogTabs({ children }: { children: ReactNode }) {
const isCatalogPinned = useAtomValue(homeCatalogPinnedAtom)
return isCatalogPinned ? children : null
}
@@ -0,0 +1,5 @@
import { atom } from 'jotai'
export const homeCatalogPinnedAtom = atom(false)
export const homeStickyScopedAtoms = [homeCatalogPinnedAtom]
@@ -0,0 +1,73 @@
.header {
height: 48px;
}
.headerCatalogTabs {
display: flex;
}
.marketplaceLogoLight {
display: block;
}
.marketplaceLogoDark {
display: none;
}
:global(html[data-theme='dark']) .marketplaceLogoLight {
display: none;
}
:global(html[data-theme='dark']) .marketplaceLogoDark {
display: block;
}
.search {
top: 6px;
}
.catalogNavigationTrigger {
display: block;
width: 100%;
height: 0;
flex-shrink: 0;
pointer-events: none;
}
.catalogNavigation {
position: sticky;
top: 48px;
z-index: 40;
padding: 24px 36px 16px;
}
.catalogNavigationPinned {
border-bottom: 1px solid var(--color-divider-regular);
padding-top: 16px;
padding-bottom: 15px;
background-color: var(--color-background-default);
}
.catalogTabsPinned {
display: none;
}
.categoriesPinned {
margin-top: 0;
}
.catalogContent {
min-height: calc(100vh - 112px);
min-height: calc(100dvh - 112px);
}
@media (max-width: 767px) {
.guide {
display: none;
}
.search {
position: relative;
top: auto;
}
}
@@ -0,0 +1,108 @@
.wrapper {
padding-bottom: 30px;
}
.copy {
flex: none;
width: 36.9167%;
height: 200px;
}
.recommendVisual {
flex: 1;
min-width: 0;
container-type: inline-size;
}
.recommendCards {
display: flex;
justify-content: space-between;
gap: 12px;
overflow: hidden;
padding: 42px 36px;
}
.navigation {
top: 208px;
width: 100%;
}
.contentTrack {
transition: transform 400ms ease-out;
}
.card {
flex: 1 1 161px;
width: auto;
min-width: 161px;
max-width: 210px;
box-shadow: 0 8px 7.2px -6px rgb(0 0 0 / 19%);
scroll-snap-align: start;
}
@container (max-width: 751px) {
.recommendCards > .card:nth-child(n + 4) {
display: none;
}
}
@container (max-width: 578px) {
.recommendCards > .card:nth-child(n + 3) {
display: none;
}
}
@container (max-width: 405px) {
.recommendCards {
justify-content: flex-start;
overflow-x: auto;
scroll-snap-type: x proximity;
scrollbar-width: none;
overscroll-behavior-x: contain;
-webkit-overflow-scrolling: touch;
}
.recommendCards::-webkit-scrollbar {
display: none;
}
.recommendCards > .card:nth-child(n) {
display: flex;
flex: 0 0 161px;
}
}
.updatesArt {
width: 33.3333%;
max-width: 400px;
}
.updatesDescription {
display: -webkit-box;
max-height: 40px;
overflow: hidden;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
@media (prefers-reduced-motion: reduce) {
.contentTrack {
transition-duration: 0ms;
}
}
@media (min-width: 1232px) {
.marketplaceCopy {
width: 443px;
}
.updatesArt {
width: 400px;
}
}
@media (min-width: 1260px) {
.embeddedCopy {
width: 431px;
}
}
@@ -0,0 +1,609 @@
'use client'
import type { RefObject } from 'react'
import type {
BannerAd,
BannerBlog,
BannerEvent,
BannerRecommend,
BannerRecommendCard,
PluginBanner,
} from './banners'
import { cn } from '@langgenius/dify-ui/cn'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from '#i18n'
import Partner from '@/app/components/plugins/base/badges/partner'
import Verified from '@/app/components/plugins/base/badges/verified'
import { MARKETPLACE_API_PREFIX } from '@/config'
import Link from '@/next/link'
import background from './assets/background.jpg'
import difyUpdatesArt from './assets/dify-updates-art.png'
import styles from './home-trending.module.css'
const AUTOPLAY_DELAY = 5000
const PAGINATION_DOT_SIZE = 6
const PAGINATION_ACTIVE_WIDTH = 40
const PAGINATION_GAP = 8
const PAGINATION_STEP = PAGINATION_DOT_SIZE + PAGINATION_GAP
const PAGINATION_ACTIVE_SHIFT = PAGINATION_ACTIVE_WIDTH - PAGINATION_DOT_SIZE
const getPaginationItemOffset = (index: number, selectedIndex: number) =>
index * PAGINATION_STEP + (index > selectedIndex ? PAGINATION_ACTIVE_SHIFT : 0)
type AutoplayPauseReason = 'focus' | 'hover' | 'reduced-motion' | 'user' | 'visibility'
function TrendingCopy({
banner,
isMarketplacePlatform,
}: {
banner: BannerRecommend
isMarketplacePlatform: boolean
}) {
const { t } = useTranslation('plugin')
const heading = banner.content.heading || t(($) => $['marketplace.home.trendingTitle'])
const description =
banner.content.description ||
banner.content.subheadings?.join(' · ') ||
t(($) => $['marketplace.home.trendingDescription'])
return (
<div
className={cn(
styles.copy,
'flex min-w-0 flex-col items-start overflow-hidden p-5',
isMarketplacePlatform ? styles.marketplaceCopy : styles.embeddedCopy,
)}
>
<div className="flex w-full flex-col items-start gap-2 overflow-hidden">
<p className="shrink-0 rounded-sm bg-state-accent-hover-alt px-1.5 py-0.5 text-[10px] leading-3 font-semibold tracking-[-0.2px] text-text-accent">
{banner.title}
</p>
<h2 className="shrink-0 text-xl leading-6 font-semibold tracking-[-0.4px] text-text-primary">
{heading}
</h2>
<p className="w-full text-[13px] leading-5 font-normal tracking-[-0.065px] text-text-tertiary">
{description}
</p>
</div>
</div>
)
}
const getMarketplaceAssetURL = (path?: string) => {
if (!path) return ''
if (/^https?:\/\//.test(path) || path.startsWith('/_next/')) return path
try {
const apiURL = new URL(MARKETPLACE_API_PREFIX)
if (path.startsWith('/api/')) return `${apiURL.origin}${path}`
return `${MARKETPLACE_API_PREFIX.replace(/\/$/, '')}/${path.replace(/^\//, '')}`
} catch {
return path
}
}
const getLocalCardHref = (card: BannerRecommendCard) => {
if (card.item_type === 'plugin') {
const [organization, pluginName] = card.item_id.split('/')
if (organization && pluginName)
return `/plugin/${encodeURIComponent(organization)}/${encodeURIComponent(pluginName)}`
}
if (card.item_type === 'template') return `/templates?tid=${encodeURIComponent(card.item_id)}`
return '/'
}
const getCardHref = (card: BannerRecommendCard, isMarketplacePlatform: boolean) => {
if (!isMarketplacePlatform && card.link) return card.link
return getLocalCardHref(card)
}
const getCardCreator = (card: BannerRecommendCard) => {
if (card.creator) return card.creator
if (card.item_type !== 'plugin') return ''
return card.item_id.split('/')[0] || ''
}
function TrendingCard({
card,
isMarketplacePlatform,
}: {
card: BannerRecommendCard
isMarketplacePlatform: boolean
}) {
const { t } = useTranslation('plugin')
const iconURL = getMarketplaceAssetURL(card.icon_url)
const creator = getCardCreator(card)
const href = getCardHref(card, isMarketplacePlatform)
const opensInNewTab = !isMarketplacePlatform && /^https?:\/\//.test(href)
const isPartner = card.badges?.includes('partner')
const isVerified = card.badges?.includes('verified')
return (
<Link
href={href}
target={opensInNewTab ? '_blank' : undefined}
rel={opensInNewTab ? 'noopener noreferrer' : undefined}
aria-label={card.display_name}
className={cn(
styles.card,
'flex h-[116px] shrink-0 flex-col items-start justify-between overflow-hidden rounded-lg bg-background-default-dodge p-3.5 outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid',
)}
>
<div
className="flex size-10 shrink-0 items-center justify-center overflow-hidden rounded-[10px] border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge"
style={{
backgroundColor: !iconURL ? card.icon_background : undefined,
}}
>
{iconURL ? (
<img
src={iconURL}
width={40}
height={40}
alt=""
aria-hidden
className="size-full object-cover"
/>
) : card.icon ? (
<span className="text-xl leading-none">{card.icon}</span>
) : (
<span aria-hidden="true" className="i-ri-image-line size-5 text-text-quaternary" />
)}
</div>
<div className="flex w-full items-end gap-1">
<div className="flex min-w-0 flex-1 flex-col items-start gap-[3px]">
<div className="flex w-full min-w-0 items-center gap-[3px]">
<h3 className="min-w-0 truncate text-sm leading-[normal] font-medium text-text-primary">
{card.display_name}
</h3>
{(isPartner || isVerified) && (
<div className="flex shrink-0 items-start gap-[3.5px]">
{isPartner && (
<Partner className="size-3.5" text={t(($) => $['marketplace.partnerTip'])} />
)}
{isVerified && (
<Verified className="size-3.5" text={t(($) => $['marketplace.verifiedTip'])} />
)}
</div>
)}
</div>
{creator && (
<p className="w-full truncate text-xs leading-[normal] font-normal text-text-tertiary">
{t(($) => $['marketplace.home.trendingByCreator'], { creator })}
</p>
)}
</div>
<span className="shrink-0 rounded-full bg-background-section-burn px-1.5 py-[3px] text-[10px] leading-3 font-normal text-text-primary">
{t(($) => $['marketplace.home.trendingView'])}
</span>
</div>
</Link>
)
}
function TrendingRecommendationSlide({
banner,
isMarketplacePlatform,
}: {
banner: BannerRecommend
isMarketplacePlatform: boolean
}) {
return (
<div
className={cn(
styles.recommendSlide,
'flex h-[200px] w-full overflow-hidden rounded-2xl bg-background-body',
)}
>
<TrendingCopy banner={banner} isMarketplacePlatform={isMarketplacePlatform} />
<div
className={cn(
styles.recommendVisual,
'relative h-[200px] shrink-0 overflow-hidden rounded-xl bg-background-body',
)}
>
<img
src={background.src}
width={3840}
height={2160}
alt=""
aria-hidden
className="absolute top-[-173px] left-[-990px] h-[1201px] w-[2135px] max-w-none opacity-80"
/>
<div aria-hidden className="absolute inset-0 bg-text-accent mix-blend-color" />
<div className={cn(styles.recommendCards, 'relative z-10 h-full items-center')}>
{banner.content.cards.map((card) => (
<TrendingCard
key={`${card.item_type}:${card.item_id}`}
card={card}
isMarketplacePlatform={isMarketplacePlatform}
/>
))}
</div>
</div>
</div>
)
}
function BlogBannerSlide({ banner }: { banner: BannerBlog }) {
const opensInNewTab = /^https?:\/\//.test(banner.content.link)
return (
<div className="flex h-[200px] w-full overflow-hidden rounded-2xl bg-background-body">
<div className="flex min-w-0 flex-1 flex-col items-start overflow-hidden px-6 py-5">
<div className="flex min-h-0 w-full flex-1 flex-col items-start gap-2">
<p className="shrink-0 rounded-sm bg-state-success-hover-alt px-1.5 py-0.5 text-[10px] leading-3 font-semibold tracking-[-0.2px] text-text-success">
{banner.title}
</p>
<div className="flex min-h-0 w-full max-w-[800px] flex-1 flex-col items-start gap-3">
<h2 className="shrink-0 text-xl leading-6 font-semibold tracking-[-0.4px] text-text-primary">
{banner.content.blog_title}
</h2>
<div className="flex min-h-0 w-full flex-1 flex-col items-start gap-2">
{banner.content.subtitle && (
<p className="shrink-0 text-[15px] leading-[18px] font-normal tracking-[-0.3px] text-text-primary">
{banner.content.subtitle}
</p>
)}
{banner.content.description && (
<p className="min-h-0 w-full flex-1 overflow-hidden text-[13px] leading-5 font-normal tracking-[-0.065px] text-text-tertiary">
<span className={styles.updatesDescription}>{banner.content.description}</span>
</p>
)}
<Link
href={banner.content.link}
target={opensInNewTab ? '_blank' : undefined}
rel={opensInNewTab ? 'noopener noreferrer' : undefined}
aria-label={`Read more about ${banner.content.blog_title}`}
className="flex shrink-0 items-center gap-1 text-[13px] leading-[normal] font-medium text-text-accent underline decoration-[10%] underline-offset-2"
>
<span>Read more</span>
<span aria-hidden className="i-ri-arrow-right-s-line size-4" />
</Link>
</div>
</div>
</div>
</div>
<img
src={difyUpdatesArt.src}
width={400}
height={200}
alt=""
aria-hidden
className={cn(styles.updatesArt, 'h-[200px] shrink-0 object-cover')}
/>
</div>
)
}
function ImageBannerSlide({ banner }: { banner: BannerEvent | BannerAd }) {
const desktopImage = getMarketplaceAssetURL(banner.content.images.desktop)
const tabletImage = getMarketplaceAssetURL(banner.content.images.tablet)
const mobileImage = getMarketplaceAssetURL(banner.content.images.mobile)
return (
<Link
href={banner.content.link}
target="_blank"
rel="noopener noreferrer"
aria-label={banner.content.alt_text || banner.title}
className="block h-[200px] w-full overflow-hidden rounded-2xl outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid"
>
<picture className="block size-full">
{mobileImage && <source media="(max-width: 639px)" srcSet={mobileImage} />}
{tabletImage && <source media="(max-width: 1023px)" srcSet={tabletImage} />}
<img
src={desktopImage}
width={1200}
height={200}
alt=""
aria-hidden
className="size-full object-cover"
/>
</picture>
</Link>
)
}
function HomeBannerSlide({
banner,
isMarketplacePlatform,
}: {
banner: PluginBanner
isMarketplacePlatform: boolean
}) {
if (banner.style_type === 'blog') return <BlogBannerSlide banner={banner} />
if (banner.style_type === 'event' || banner.style_type === 'ad')
return <ImageBannerSlide banner={banner} />
return (
<TrendingRecommendationSlide banner={banner} isMarketplacePlatform={isMarketplacePlatform} />
)
}
function TrendingNavigation({
banners,
selectedIndex,
carouselRootRef,
onSelect,
onNext,
}: {
banners: PluginBanner[]
selectedIndex: number
carouselRootRef: RefObject<HTMLDivElement | null>
onSelect: (index: number) => void
onNext: () => void
}) {
const { t } = useTranslation('plugin')
const progressRef = useRef<HTMLSpanElement>(null)
const progressAnimationRef = useRef<Animation | null>(null)
const pauseReasonsRef = useRef(new Set<AutoplayPauseReason>())
const [isUserPaused, setIsUserPaused] = useState(false)
const [isReducedMotionPaused, setIsReducedMotionPaused] = useState(false)
const isExplicitlyPaused = isUserPaused || isReducedMotionPaused
const paginationWidth =
PAGINATION_ACTIVE_WIDTH + Math.max(0, banners.length - 1) * PAGINATION_STEP
const setPauseReason = useCallback((reason: AutoplayPauseReason, shouldPause: boolean) => {
if (shouldPause) pauseReasonsRef.current.add(reason)
else pauseReasonsRef.current.delete(reason)
const progressAnimation = progressAnimationRef.current
if (!progressAnimation) return
if (pauseReasonsRef.current.size > 0) progressAnimation.pause()
else progressAnimation.play()
}, [])
useEffect(() => {
const progressElement = progressRef.current
if (!progressElement?.animate) return
const progressAnimation = progressElement.animate(
[{ transform: 'scaleX(0)' }, { transform: 'scaleX(1)' }],
{
duration: AUTOPLAY_DELAY,
easing: 'linear',
fill: 'forwards',
},
)
progressAnimationRef.current = progressAnimation
if (pauseReasonsRef.current.size > 0) progressAnimation.pause()
progressAnimation.onfinish = onNext
return () => {
progressAnimation.onfinish = null
progressAnimation.cancel()
if (progressAnimationRef.current === progressAnimation) progressAnimationRef.current = null
}
}, [onNext, selectedIndex])
useEffect(() => {
const carouselRoot = carouselRootRef.current
if (!carouselRoot) return
const handleMouseEnter = () => setPauseReason('hover', true)
const handleMouseLeave = () => setPauseReason('hover', false)
const handleFocusIn = () => setPauseReason('focus', true)
const handleFocusOut = (event: FocusEvent) => {
if (carouselRoot.contains(event.relatedTarget as Node | null)) return
setPauseReason('focus', false)
}
const handleVisibilityChange = () =>
setPauseReason('visibility', document.visibilityState === 'hidden')
carouselRoot.addEventListener('mouseenter', handleMouseEnter)
carouselRoot.addEventListener('mouseleave', handleMouseLeave)
carouselRoot.addEventListener('focusin', handleFocusIn)
carouselRoot.addEventListener('focusout', handleFocusOut)
document.addEventListener('visibilitychange', handleVisibilityChange)
return () => {
carouselRoot.removeEventListener('mouseenter', handleMouseEnter)
carouselRoot.removeEventListener('mouseleave', handleMouseLeave)
carouselRoot.removeEventListener('focusin', handleFocusIn)
carouselRoot.removeEventListener('focusout', handleFocusOut)
document.removeEventListener('visibilitychange', handleVisibilityChange)
}
}, [carouselRootRef, setPauseReason])
useEffect(() => {
const reducedMotionQuery = window.matchMedia('(prefers-reduced-motion: reduce)')
const syncReducedMotion = () => {
// oxlint-disable-next-line eslint-react/set-state-in-effect -- This state mirrors an external media query.
setIsReducedMotionPaused(reducedMotionQuery.matches)
setPauseReason('reduced-motion', reducedMotionQuery.matches)
}
syncReducedMotion()
reducedMotionQuery.addEventListener('change', syncReducedMotion)
return () => reducedMotionQuery.removeEventListener('change', syncReducedMotion)
}, [setPauseReason])
const toggleAutoplay = () => {
if (isExplicitlyPaused) {
setIsUserPaused(false)
setIsReducedMotionPaused(false)
setPauseReason('user', false)
setPauseReason('reduced-motion', false)
return
}
setIsUserPaused(true)
setPauseReason('user', true)
}
return (
<div
role="group"
aria-label={t(($) => $['marketplace.home.trendingPaginationLabel'])}
className={cn(
styles.navigation,
'absolute right-0 z-10 flex h-[22px] items-center gap-2 px-5 py-2',
)}
>
<div className="relative h-1.5 shrink-0" style={{ width: paginationWidth }}>
<span
aria-hidden
className="pointer-events-none absolute top-0 left-0 z-1 flex h-1.5 w-10 items-center overflow-hidden rounded-full bg-state-base-handle transition-transform duration-500 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-transform motion-reduce:transition-none"
style={{
transform: `translate3d(${selectedIndex * PAGINATION_STEP}px, 0, 0)`,
}}
>
<span
key={selectedIndex}
ref={progressRef}
data-carousel-progress
className="h-full w-full rounded-full bg-text-accent"
style={{ transform: 'scaleX(0)', transformOrigin: 'left center' }}
/>
</span>
{banners.map((banner, index) => {
const isCurrent = index === selectedIndex
return (
<button
key={banner.id}
type="button"
aria-label={banner.title}
aria-current={isCurrent ? 'true' : undefined}
onClick={() => {
if (isCurrent) return
onSelect(index)
}}
onKeyDown={(event) => {
if (event.key !== 'Enter' && event.key !== ' ') return
event.preventDefault()
if (isCurrent) return
onSelect(index)
}}
className={cn(
'absolute top-0 left-0 z-2 h-1.5 overflow-hidden rounded-full outline-hidden transition-[transform,width,background-color] duration-500 ease-[cubic-bezier(0.22,1,0.36,1)] after:absolute after:-inset-2 hover:bg-state-base-handle-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid motion-reduce:transition-none',
isCurrent ? 'bg-transparent' : 'bg-state-base-handle',
)}
style={{
width: isCurrent ? PAGINATION_ACTIVE_WIDTH : PAGINATION_DOT_SIZE,
transform: `translate3d(${getPaginationItemOffset(index, selectedIndex)}px, 0, 0)`,
}}
/>
)
})}
</div>
<div className="min-w-0 flex-1" />
<button
type="button"
aria-label={t(
($) =>
$[
isExplicitlyPaused
? 'marketplace.home.trendingPlay'
: 'marketplace.home.trendingPause'
],
)}
onClick={toggleAutoplay}
onKeyDown={(event) => {
if (event.key !== 'Enter' && event.key !== ' ') return
event.preventDefault()
toggleAutoplay()
}}
className="flex size-4 shrink-0 items-center justify-center rounded-full bg-state-base-handle text-text-primary outline-hidden hover:bg-state-base-handle-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid"
>
{isExplicitlyPaused ? (
<span aria-hidden className="i-ri-play-large-fill size-2 opacity-30" />
) : (
<span aria-hidden className="i-ri-pause-large-fill size-2 opacity-30" />
)}
</button>
</div>
)
}
function HomeTrending({
banners,
isMarketplacePlatform,
}: {
banners: PluginBanner[]
isMarketplacePlatform: boolean
}) {
const { t } = useTranslation('plugin')
const carouselRootRef = useRef<HTMLDivElement>(null)
const [selectedIndex, setSelectedIndex] = useState(0)
const selectSlide = useCallback((index: number) => setSelectedIndex(index), [])
const selectNextSlide = useCallback(
() => setSelectedIndex((currentIndex) => (currentIndex + 1) % banners.length),
[banners.length],
)
if (banners.length === 0) return null
return (
<section
aria-label={t(($) => $['marketplace.home.trendingTitle'])}
className={cn(
'shrink-0 bg-background-default pb-6',
isMarketplacePlatform ? 'px-4 min-[1232px]:px-0' : 'px-4 md:px-9',
)}
>
<div
className={cn(
styles.wrapper,
'mx-auto w-full',
isMarketplacePlatform ? 'max-w-[1200px]' : 'max-w-[1188px]',
)}
>
<div
role="region"
aria-roledescription="carousel"
aria-label={t(($) => $['marketplace.home.trendingTitle'])}
className="relative h-[200px] w-full rounded-2xl"
>
<TrendingNavigation
banners={banners}
selectedIndex={selectedIndex}
carouselRootRef={carouselRootRef}
onSelect={selectSlide}
onNext={selectNextSlide}
/>
<div ref={carouselRootRef} className="h-full overflow-hidden rounded-2xl">
<div
aria-live="polite"
className={cn(styles.contentTrack, 'flex h-full')}
style={{ transform: `translate3d(-${selectedIndex * 100}%, 0, 0)` }}
>
{banners.map((banner, index) => {
const isActive = index === selectedIndex
return (
<div
key={banner.id}
role="group"
aria-roledescription="slide"
aria-label={banner.title}
aria-hidden={!isActive}
inert={!isActive}
className="h-full min-w-0 shrink-0 grow-0 basis-full"
>
<HomeBannerSlide
banner={banner}
isMarketplacePlatform={isMarketplacePlatform}
/>
</div>
)
})}
</div>
</div>
</div>
</div>
</section>
)
}
export default HomeTrending
@@ -0,0 +1,56 @@
import type { PluginBanner } from './banners'
import ListWrapper from '../list/list-wrapper'
import HomeCatalogNavigation from './home-catalog-navigation'
import HomeCatalogTabs from './home-catalog-tabs'
import HomeHeader from './home-header'
import HomeHero from './home-hero'
import HomeSearch from './home-search'
import { HomeStickyStateProvider } from './home-sticky-state-provider'
import styles from './home-sticky.module.css'
import HomeTrending from './home-trending'
type MarketplaceHomeProps = {
actions?: React.ReactNode
banners: PluginBanner[]
isMarketplacePlatform: boolean
linkToMarketplaceDetail: boolean
showInstallButton: boolean
}
const MarketplaceHome = ({
actions,
banners,
isMarketplacePlatform,
linkToMarketplaceDetail,
showInstallButton,
}: MarketplaceHomeProps) => {
return (
<HomeStickyStateProvider>
<div className="flex min-h-full w-full flex-col bg-background-default">
<HomeHeader actions={actions} isMarketplacePlatform={isMarketplacePlatform} />
<div className="relative flex w-full flex-col">
<HomeHero isMarketplacePlatform={isMarketplacePlatform} />
<HomeSearch />
{banners.length > 0 && (
<>
<div aria-hidden="true" className="h-12 shrink-0" />
<HomeTrending banners={banners} isMarketplacePlatform={isMarketplacePlatform} />
</>
)}
<HomeCatalogNavigation
catalogTabs={<HomeCatalogTabs isMarketplacePlatform={isMarketplacePlatform} />}
/>
<div className="contents [&>div]:bg-background-default!">
<ListWrapper
className={styles.catalogContent}
showInstallButton={showInstallButton}
linkToMarketplaceDetail={linkToMarketplaceDetail}
/>
</div>
</div>
</div>
</HomeStickyStateProvider>
)
}
export default MarketplaceHome

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