Compare commits

..
Author SHA1 Message Date
Jyong a220bf394f docs(test): record KnowledgeFS workflow v2 coverage
Document the executed Workflow Knowledge Retrieval v2 matrix, Linear issue mapping, fix verification, remaining environment blockers, and reusable DSL fixtures.
2026-08-11 11:51:17 -04:00
Jyong c8780dcb7c fix(workflow): stabilize KnowledgeFS retrieval access
Keep the workflow editor mounted after history updates, expose an independent Workflow Access control, and map KnowledgeFS admission failures to actionable localized errors. Add frontend and backend regression coverage for KF-BUG-020 and KF-BUG-021.
2026-08-11 11:51:17 -04:00
FFXN 4769070af0 Merge remote-tracking branch 'origin/deploy/konwledge' into deploy/konwledge
# Conflicts:
#	api/knowledge-fs-contract.lock.json
#	packages/contracts/generated/api/console/knowledge-fs/orpc.gen.ts
#	packages/contracts/generated/api/console/knowledge-fs/types.gen.ts
#	packages/contracts/generated/api/console/knowledge-fs/zod.gen.ts
2026-08-11 19:54:25 +08:00
FFXN 0853258f92 feat(knowledge-fs): add bulk logical document deletion support 2026-08-11 19:47:37 +08:00
FFXN 7f0efa20fa feat(knowledge-fs): add bulk logical document deletion support 2026-08-11 19:47:12 +08:00
Stephen Zhou 308795393a fix(knowledge-fs): refresh contract subtree lock 2026-08-11 18:24:28 +08:00
Jyong 4ce6a36b12 Merge remote-tracking branch 'origin/deploy/konwledge' into deploy/konwledge
# Conflicts:
#	web/features/new-rag/__tests__/create-knowledge-page.spec.tsx
#	web/i18n/de-DE/dataset.json
#	web/i18n/nl-NL/dataset.json
2026-08-11 06:12:00 -04:00
Jyong 14e5afa54d Update application components and supporting logic 2026-08-11 05:52:22 -04:00
Stephen Zhou 7ee9296bea fix(knowledge-fs): wire document download actions 2026-08-11 17:28:20 +08:00
Stephen Zhou fc1eaf3287 fix(knowledge-fs): reject empty file uploads (WTA-1929) 2026-08-11 16:43:10 +08:00
FFXN fd7854de7e Merge remote-tracking branch 'origin/deploy/konwledge' into deploy/konwledge 2026-08-11 16:29:46 +08:00
FFXN 0d4441e2cf feat(knowledge): support KnowledgeFS document downloads
- add single logical document download endpoint
- add batch ZIP download endpoint
- stream files from KnowledgeFS object storage
- validate batch size and deduplicate ZIP filenames
- add download service and facade unit tests
2026-08-11 16:29:23 +08:00
Stephen Zhou 8dabdb64ea fix(knowledge-fs): handle disabled reindex results 2026-08-11 16:10:20 +08:00
Stephen Zhou cb62c52255 feat(knowledge-fs): align document actions with design 2026-08-11 15:48:25 +08:00
Stephen Zhou 668b122632 style(knowledge-fs): format availability changes 2026-08-11 11:26:36 +08:00
Stephen Zhou c50c815e81 Merge branch 'deploy/konwledge' of github.com:langgenius/dify into deploy/konwledge 2026-08-11 11:24:20 +08:00
Stephen Zhou 429850531a fix(knowledge-fs): display source provider names 2026-08-11 11:23:48 +08:00
FFXN 14d53ff11e Merge remote-tracking branch 'origin/deploy/konwledge' into deploy/konwledge 2026-08-11 11:21:10 +08:00
FFXN 79348cfc12 test(migrate): add additional migration ID assertions for logical document availability 2026-08-11 11:20:34 +08:00
Stephen Zhou e4c7987cf0 fix(knowledge-fs): sync capability operation count 2026-08-11 11:19:02 +08:00
Stephen Zhou d9e25e40f6 fix(knowledge-fs): sync api migration expectations 2026-08-11 11:12:31 +08:00
FFXN 465d3c53cb Merge remote-tracking branch 'origin/deploy/konwledge' into deploy/konwledge 2026-08-11 11:06:53 +08:00
FFXN 17cc4e20e7 fix(migration): add logical document availability migration ID 2026-08-11 11:06:31 +08:00
Stephen Zhou dabd74ac44 fix(knowledge-fs): align availability schema migration 2026-08-11 11:04:00 +08:00
FFXN 5fed29ed57 Merge remote-tracking branch 'origin/deploy/konwledge' into deploy/konwledge 2026-08-11 10:57:34 +08:00
FFXN 703c4b5f83 Merge remote-tracking branch 'origin/deploy/konwledge' into deploy/konwledge 2026-08-11 10:50:55 +08:00
Stephen Zhou 43f76aec9a fix: reconcile remaining main merge regressions 2026-08-11 10:45:17 +08:00
FFXN 6b3957af9a feat(knowledge_fs): add APIs for managing logical document availability with bulk update support 2026-08-11 10:39:44 +08:00
FFXN c0b57f2f54 feat(knowledge_fs): add APIs for managing logical document availability with bulk update support 2026-08-11 10:39:28 +08:00
Stephen Zhou 3bcc648f07 fix: restore upstream behavior after main merge 2026-08-11 10:23:44 +08:00
Stephen Zhou 4120cb5eef Merge remote-tracking branch 'origin/main' into deploy/konwledge
# Conflicts:
#	api/controllers/common/wraps.py
#	api/tests/unit_tests/core/rag/docstore/test_dataset_docstore.py
#	api/tests/unit_tests/core/rag/indexing/processor/test_parent_child_index_processor.py
#	api/tests/unit_tests/services/agent/test_workflow_publish_service.py
2026-08-11 10:14:53 +08:00
FFXN 26544610ff feat(knowledge_fs): add document availability management with bulk update support 2026-08-11 10:01:33 +08:00
f785af3cec fix(docker): use dynamic DNS resolution for all nginx upstreams (#39155)
Co-authored-by: Cursor <[email protected]>
2026-08-11 00:48:59 +00:00
Tushar MotwaniandGitHub aa5e8a5eff chore: suppress pyrefly necessary conversions (#40444) 2026-08-10 16:41:34 +00:00
Stephen Zhou aea50cd647 fix(knowledge-fs): sync deployment env contract 2026-08-10 23:20:31 +08:00
Jyong fcbfb49ef7 fix(knowledge-fs): refresh secret scan allowlist 2026-08-10 11:07:13 -04:00
Jyong 28fd666fa6 feat(knowledge): stage document uploads before admission 2026-08-10 10:58:17 -04:00
Stephen Zhou 655e596106 refactor(web): share new RAG setup controls 2026-08-10 22:51:52 +08:00
Stephen Zhou fbde89eeb9 fix(knowledge-fs): align datasource credential type 2026-08-10 22:38:33 +08:00
yyhandGitHub 6a9efa8ffd fix(web): keep workflow ref updates out of render (#40428) 2026-08-10 14:38:04 +00:00
Stephen Zhou a2194300cc fix(knowledge-fs): stabilize initial source creation 2026-08-10 22:35:24 +08:00
Stephen Zhou e4e9271fbe feat(knowledge-fs): create spaces with initial sources 2026-08-10 22:08:01 +08:00
Stephen Zhou 62cebcf64d fix(web): discover source providers dynamically 2026-08-10 20:51:20 +08:00
林玮 (Jade Lin)andGitHub 4eb9a24997 refactor(workflow): align standalone node execution layers (#40447) 2026-08-10 12:37:34 +00:00
Stephen Zhou b2956fd544 fix(knowledge-fs): refresh contract subtree lock 2026-08-10 19:50:00 +08:00
yyhandGitHub b206e691ca fix(web): keep shared ref updates out of render (#40427) 2026-08-10 11:33:11 +00:00
Stephen Zhou 436cb0f544 tweaks 2026-08-10 19:30:03 +08:00
Stephen Zhou e1bbb85e3e fix(web): stop waiting for source indexing 2026-08-10 19:23:58 +08:00
Stephen Zhou 99b67d56e3 fix(knowledge-fs): scope deletion fences by source 2026-08-10 19:16:57 +08:00
yyhandGitHub 3f354c2b06 docs(web): clarify ref mutation ownership (#40426) 2026-08-10 11:04:18 +00:00
github-actions[bot]GitHubclaude[bot] <41898282+claude[bot]@users.noreply.github.com>yyh
8ae9d0ad8e chore(i18n): sync translations with en-US (#40435)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: yyh <[email protected]>
2026-08-10 11:00:10 +00:00
Stephen Zhou 9993b4f13d fix(web): connect installed website datasource providers 2026-08-10 18:36:19 +08:00
Jyong b630259d21 fix(web): wait for KnowledgeFS provisioning before upload 2026-08-10 06:08:36 -04:00
FFXN ec276ef415 Merge remote-tracking branch 'origin/deploy/konwledge' into deploy/konwledge 2026-08-10 18:01:22 +08:00
FFXN 810cfb6ea5 fix(index-projection-builders): add getManyByNodeIds method for efficient projection retrieval 2026-08-10 18:00:54 +08:00
FFXN 1989fe923a Merge remote-tracking branch 'origin/deploy/konwledge' into deploy/konwledge 2026-08-10 18:00:44 +08:00
Stephen Zhou 7af76cfa75 fix(web): create knowledge with initial source 2026-08-10 17:58:12 +08:00
JoelandGitHub 0f583cfe75 fix: widen agent orchestration chat on large displays (#40433) 2026-08-10 09:57:47 +00:00
FFXN 5008b98b05 fix(index-projection-builders): add getManyByNodeIds method for efficient projection retrieval 2026-08-10 17:56:33 +08:00
zyqzyqGitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>hjlarry
df250423d4 feat: add tool plugin param type date and date-picker (#36163)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: hjlarry <[email protected]>
2026-08-10 09:31:38 +00:00
Stephen Zhou 48ea979f5b refactor(web): share knowledge setup components 2026-08-10 17:14:52 +08:00
FFXN 37f357b4e8 Merge remote-tracking branch 'origin/deploy/konwledge' into deploy/konwledge 2026-08-10 17:03:27 +08:00
FFXN c537b130fe fix(index-projection-builders): optimize dense projection handling to reuse persisted projections 2026-08-10 17:02:42 +08:00
FFXN 8eaaef42de fix(index-projection-builders): optimize dense projection handling to reuse persisted projections 2026-08-10 17:02:30 +08:00
Asuka MinatoGitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
b8934b743c test: migrate core app sessions to SQLite (#40084)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-10 09:01:02 +00:00
Stephen Zhou 1db2f68b3d fix(web): align create knowledge flow with design 2026-08-10 17:00:17 +08:00
Byron.wangGitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
976d061f9b refactor(api): merge ping and version endpoints into system (#40058)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-10 08:50:40 +00:00
Asuka MinatoGitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
96655436f4 test: migrate RAG index sessions to SQLite (#40080)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-10 08:43:18 +00:00
非法操作andGitHub 648806bc9c refactor(api): extract explore banner query service (#40053) 2026-08-10 08:07:54 +00:00
FFXN 88ce098f55 fix(document-semantic-enrichment): update baseHeadRevision handling to use nonnegativeSafeIntegerColumn 2026-08-10 15:50:57 +08:00
FFXN aea65f1ce0 Merge remote-tracking branch 'origin/deploy/konwledge' into deploy/konwledge 2026-08-10 15:50:44 +08:00
FFXN c38d795db5 fix(document-semantic-enrichment): update baseHeadRevision handling to use nonnegativeSafeIntegerColumn 2026-08-10 15:49:25 +08:00
Stephen Zhou e21af924d8 fix(knowledge-fs): refresh contract subtree lock 2026-08-10 15:25:37 +08:00
Stephen Zhou 64cc650e78 fix(knowledge-fs): sync metadata test contracts 2026-08-10 15:17:35 +08:00
yyhandGitHub 64ebfe2235 fix(web): name the webapp password visibility action (#40225) 2026-08-10 07:12:24 +00:00
Jyong 1ac8c9e434 feat(knowledge-fs): add durable metadata management 2026-08-10 02:55:09 -04:00
yyhGitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
ca4d7ff1f2 fix(web): make suggested questions native buttons (#40200)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-10 06:47:19 +00:00
yyhandGitHub 2917d1d51c refactor(web): migrate webapp password fields to dify ui (#40223) 2026-08-10 06:44:28 +00:00
yyhandGitHub c79b9821fa fix(web): name endpoint card actions (#40260) 2026-08-10 06:44:10 +00:00
yyhGitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
6422ef2b57 fix(web): use button for workflow debug close (#40274)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-10 06:43:23 +00:00
yyhandGitHub 509ea9afc4 fix(web): name secret key delete action (#40297) 2026-08-10 06:43:02 +00:00
yyhandGitHub 1828cc6a10 fix(web): name MCP header delete actions (#40300) 2026-08-10 06:42:45 +00:00
yyhandGitHub 8d5ff67d05 fix(web): name external API card actions (#40304) 2026-08-10 06:42:36 +00:00
yyhandGitHub ec63a26511 fix(web): expose chat variable tips state (#40334) 2026-08-10 06:42:03 +00:00
yyhandGitHub c2b3b89445 docs(web): add Next.js agent rules (#40398) 2026-08-10 06:39:37 +00:00
FFXN 438dc96334 fix(product_remote_http): log upstream rejection details for specific error statuses 2026-08-10 14:34:30 +08:00
5967bd8e08 chore: tighten education verification and pause discount access (#40402)
Co-authored-by: Yansong Zhang <[email protected]>
2026-08-10 06:33:03 +00:00
非法操作andGitHub 7e7548cd18 fix(web): allow unlimited text input in trial apps (#40403) 2026-08-10 06:18:38 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1ba3aa718e chore(deps): bump pymdown-extensions from 11.0 to 11.0.1 in /dify-agent (#40183)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-10 06:16:06 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c7b1660650 chore(deps): bump gitpython from 3.1.57 to 3.1.58 in /api (#40184)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-10 06:14:40 +00:00
FFXN 9eff9783fc fix(gateway): enhance error handling to log detailed context information 2026-08-10 13:30:36 +08:00
FFXN 8b3fc51374 fix(gateway): enhance error handling to log detailed context information 2026-08-10 13:27:31 +08:00
yyhGitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
f5e1f1590f fix(web): use buttons for version filters (#40357)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-10 04:41:57 +00:00
yyhandGitHub 09d0cca5b9 fix(web): make tool category filters keyboard accessible (#40243) 2026-08-10 04:18:04 +00:00
yyhandGitHub d8736be194 fix(web): name MCP header fields (#40299) 2026-08-10 03:57:24 +00:00
Pranav AgarwalandGitHub 7f15f506f8 fix(api): end_date filter in feedback export excludes the entire end date (#40114) 2026-08-10 03:27:56 +00:00
chelsealongandGitHub 6aa3fa5193 fix: catch ValueError instead of bare except in account CLI commands (#40356) 2026-08-10 03:18:33 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c849be5e52 chore(deps): bump the github-actions-dependencies group with 2 updates (#40391)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-10 03:17:53 +00:00
Stephen Zhou efc21d5e48 fix(knowledge-fs): classify app binding coverage 2026-08-10 11:14:33 +08:00
yyhandGitHub b3b3088a17 fix(web): name external API panel close (#40309) 2026-08-10 03:13:36 +00:00
yyhandGitHub 72fad39913 fix(web): name external API modal close (#40313) 2026-08-10 03:13:35 +00:00
yyhandGitHub e12563a4b4 fix(web): name plugin item actions (#40261) 2026-08-10 03:13:33 +00:00
yyhandGitHub e8afed13cc fix(web): name plugin detail close action (#40264) 2026-08-10 03:13:33 +00:00
yyhandGitHub 7a72bb3571 fix(web): name OAuth redirect copy action (#40303) 2026-08-10 03:13:33 +00:00
yyhandGitHub 08f30717ad fix(web): make verification back actions keyboard accessible (#40196) 2026-08-10 03:13:31 +00:00
yyhandGitHub 2bdfbb9e61 fix(web): name generated result copy action (#40259) 2026-08-10 03:13:31 +00:00
yyhandGitHub 06e757a9e9 fix(web): name the snippet info menu trigger (#40251) 2026-08-10 03:13:31 +00:00
yyhandGitHub 7b84d4daca fix(web): use primary headings on password reset pages (#40193) 2026-08-10 03:13:31 +00:00
yyhandGitHub 5e0d705a7f fix(web): name the plugin debug trigger (#40252) 2026-08-10 03:13:31 +00:00
yyhandGitHub 30ca836e03 fix(web): make the citation toggle a button (#40204) 2026-08-10 03:12:25 +00:00
Stephen Zhou 3f5c035329 Merge remote-tracking branch 'origin/deploy/konwledge' into deploy/konwledge 2026-08-10 11:00:35 +08:00
Stephen Zhou f4e8788d29 fix(api): restore security guards after main merge 2026-08-10 11:00:06 +08:00
Asuka MinatoandGitHub b391dc56a0 chore: example to rm db.session (#40370) 2026-08-10 02:58:52 +00:00
yyhGitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
606fac6e1c fix(web): use button for publisher access (#40361)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-10 02:56:21 +00:00
yyhGitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
ed36ee5133 refactor(web): rely on preview dialog containment (#40363)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-10 02:56:20 +00:00
yyhandGitHub c35242731f fix(web): use button for provider tool item (#40362) 2026-08-10 02:56:20 +00:00
yyhGitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
d2d07d0345 fix(web): use buttons for plugin type switch (#40353)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-10 02:56:19 +00:00
yyhGitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
8b908d6150 fix(web): use buttons for pricing categories (#40360)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-10 02:56:18 +00:00
yyhandGitHub 1017a76b4c fix(web): name assigner add operation (#40337) 2026-08-10 02:56:18 +00:00
yyhGitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
9c5d76c299 refactor(web): rely on QR panel containment (#40352)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-10 02:56:18 +00:00
yyhandGitHub 83987b8eec fix(web): name prompt generator action (#40328) 2026-08-10 02:56:16 +00:00
yyhandGitHub cbd71699de fix(web): expose SVG preview toggle state (#40332) 2026-08-10 02:56:16 +00:00
yyhandGitHub 5d58a25e52 fix(web): name human input preview close (#40321) 2026-08-10 02:56:16 +00:00
yyhandGitHub 764d9b5fbe fix(web): name external retrieval close action (#40317) 2026-08-10 02:56:16 +00:00
yyhandGitHub 5ffea6d30e fix(web): name code generator action (#40324) 2026-08-10 02:56:16 +00:00
yyhandGitHub 2796210e18 fix(web): name agent log navigation menu (#40286) 2026-08-10 02:56:14 +00:00
yyhandGitHub 1b2c71ac75 fix(web): name variable inspect close actions (#40289) 2026-08-10 02:56:14 +00:00
yyhandGitHub cc4bf22d07 fix(web): expose workflow parallel toggle state (#40282) 2026-08-10 02:56:14 +00:00
yyhandGitHub 7295d9306f fix(web): name image previewer actions (#40283) 2026-08-10 02:56:14 +00:00
yyhandGitHub 2a66b49bc7 fix(web): name human input delete action (#40293) 2026-08-10 02:56:14 +00:00
yyhandGitHub c944a25585 fix(web): expose debug toolbar semantics (#40258) 2026-08-10 02:56:12 +00:00
yyhandGitHub 2067836851 fix(web): name saved item actions (#40256) 2026-08-10 02:56:12 +00:00
yyhandGitHub 6f95a071b5 fix(web): name snippet stop action (#40278) 2026-08-10 02:56:11 +00:00
yyhandGitHub a543fd3656 fix(web): name workflow version history action (#40276) 2026-08-10 02:56:11 +00:00
yyhandGitHub d94c4415af fix(web): name annotation reply actions (#40270) 2026-08-10 02:56:11 +00:00
yyhandGitHub abbed23c64 fix(web): make dataset sidebar menu items buttons (#40248) 2026-08-10 02:56:09 +00:00
yyhandGitHub 5de4e5b3c0 fix(web): name dataset configuration actions (#40254) 2026-08-10 02:56:09 +00:00
yyhandGitHub 1e17a55e79 fix(web): name annotation configuration action (#40255) 2026-08-10 02:56:09 +00:00
yyhandGitHub 465dab2eed refactor(web): keep copy id events on the button (#40247) 2026-08-10 02:56:09 +00:00
yyhandGitHub 532e0a77cc fix(web): name conversation menu triggers (#40253) 2026-08-10 02:56:09 +00:00
yyhandGitHub 47835e6b25 fix(web): expose the voice recording timer (#40205) 2026-08-10 02:56:07 +00:00
yyhandGitHub 11c537dc1e fix(web): restore webapp email code form submission (#40227) 2026-08-10 02:56:07 +00:00
yyhandGitHub e4420227b5 fix(web): make the scroll-to-node action a button (#40214) 2026-08-10 02:56:07 +00:00
yyhandGitHub f9e44fe86e fix(web): restore the signup email focus order (#40230) 2026-08-10 02:56:07 +00:00
yyhandGitHub b256e211dc fix(web): make the member selector keyboard accessible (#40209) 2026-08-10 02:56:07 +00:00
yyhGitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2a34c7a714 fix(web): make the human input expander a button (#40202)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-10 02:55:30 +00:00
yyhGitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
04132b51f0 fix(web): use button for chat variable panel close (#40333)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-10 02:53:49 +00:00
yyhandGitHub 908f6ada4e fix(web): restore webapp password form submission (#40222) 2026-08-10 02:53:43 +00:00
yyhandGitHub e7da213f93 fix(web): name workflow debug actions (#40273) 2026-08-10 02:53:37 +00:00
Jyong 93dcc67e63 Fix workflow binding sync session handling 2026-08-09 22:50:44 -04:00
Jyong a9c92f114f Implement the requested changes across the application 2026-08-09 22:40:48 -04:00
Stephen Zhou 3e59fba46a Merge remote-tracking branch 'origin/main' into deploy/konwledge
# Conflicts:
#	api/controllers/console/app/app.py
#	api/controllers/console/app/app_import.py
#	api/controllers/console/auth/activate.py
#	api/controllers/console/setup.py
#	api/tests/unit_tests/controllers/console/workspace/test_plugin.py
#	web/features/new-rag/document-list.tsx
#	web/features/new-rag/sources-page.tsx
#	web/features/new-rag/website-crawl-preview.tsx
2026-08-10 10:34:47 +08:00
yyhandGitHub e7f789f185 fix(web): hide citation measurement items from assistive tech (#40203) 2026-08-10 02:34:14 +00:00
yyhandGitHub f8d381e6c5 fix(web): name chat settings triggers (#40341) 2026-08-10 02:33:11 +00:00
yyhandGitHub c29425c0fc fix(web): name chat history controls (#40344) 2026-08-10 02:33:02 +00:00
yyhGitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
612f9ee669 fix(web): use button for tool detail toggle (#40349)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-10 02:32:52 +00:00
Souravrajvi0andGitHub 81a38df814 feat(api): cache remote recommended app template fetches (#40176) 2026-08-10 02:30:15 +00:00
yyhandGitHub e9eb130731 fix(web): use a button for unauthorized logout (#40198) 2026-08-10 02:29:46 +00:00
yyhandGitHub 4edaecc04d fix(web): use a button for the webapp fallback action (#40197) 2026-08-10 02:29:10 +00:00
yyhandGitHub fc8d0a0187 fix(web): use a primary heading on the install page (#40194) 2026-08-10 02:26:36 +00:00
yyhandGitHub 96960499ef fix(web): use primary headings on auth pages (#40192) 2026-08-10 02:26:10 +00:00
yyhandGitHub c2687c16b3 refactor(web): unify infinite scroll sentinels (#40188) 2026-08-10 02:22:07 +00:00
Asuka MinatoGitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
883944d289 test: Type test (#40371)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-10 02:13:17 +00:00
yyhandGitHub a7156a0702 fix(web): remove workflow output title tab stop (#40377) 2026-08-10 01:52:33 +00:00
yyhandGitHub dee9d27af8 fix(web): associate app deletion confirmation label (#40375) 2026-08-10 01:52:09 +00:00
yyhandGitHub e06f0ee315 fix(web): hide credential icons from assistive tech (#40376) 2026-08-10 01:51:42 +00:00
Jyong 1d990de7e7 Refactor application components and update related functionality 2026-08-09 12:19:56 -04:00
Jyong 78e9d41c83 feat(knowledge-fs): support image query retrieval 2026-08-09 09:45:43 -04:00
chelsealongGitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
7522ae14b2 fix(agent): preserve plugin-declared model parameters through Agent Soul (#40163)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-09 08:48:01 +00:00
LikalikaliGitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
e09eee8fd1 refactor: replace manual model_validate with @model_validate in datasets controllers (#40236)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-09 08:25:27 +00:00
LikalikaliGitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
b692ddd80c refactor: replace manual model_validate with @model_validate in workspace remaining controllers (#40239)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-09 08:23:51 +00:00
LikalikaliandGitHub 5ebb389d26 refactor: replace manual model_validate with @model_validate in rag_pipeline controllers (#40237) 2026-08-09 08:22:50 +00:00
LikalikaliandGitHub fff6f7cf2f refactor: replace manual model_validate with @model_validate in app workflow controllers (#40235) 2026-08-09 08:20:10 +00:00
LikalikaliGitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
516fd12b8d refactor: replace manual model_validate with @model_validate in app core controllers (#40234)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-09 08:18:02 +00:00
LikalikaliandGitHub 925ca49f21 refactor: replace manual model_validate with @model_validate in explore/snippets controllers (#40241) 2026-08-09 07:07:08 +00:00
LikalikaliandGitHub a1577fc48c refactor: replace manual model_validate with @model_validate in auth/billing/tag controllers (#40240) 2026-08-09 06:14:07 +00:00
LikalikaliandGitHub acda0341ed refactor: replace manual model_validate with @model_validate in workspace plugin/tool controllers (#40238) 2026-08-09 06:13:24 +00:00
LikalikaliandGitHub 98378cfe03 refactor: replace manual model_validate with @model_validate in agent controllers (#40233) 2026-08-09 06:11:33 +00:00
非法操作andGitHub 3f89a3c742 fix(workflow): fall back when default socket is unavailable (#40191) 2026-08-09 04:13:41 +00:00
medimediandGitHub e036075e22 chore: Remove redundant dataset context string conversions (#40189) 2026-08-08 18:26:52 +00:00
FFXN fec55e505d fix(web): add silent context to getSyncPolicy and updateSyncPolicy calls 2026-08-08 21:04:35 +08:00
FFXN 488bad3424 fix(knowledge_fs): enhance online-document connector to aggregate Dify content variable messages and support streaming 2026-08-08 20:38:59 +08:00
FFXN b70136f46e fix(knowledge_fs): enhance online-document connector to aggregate Dify content variable messages and support streaming 2026-08-08 20:38:29 +08:00
FFXN aea90b08d9 fix(knowledge_fs): add diagnostics for empty content and frame metadata logging 2026-08-08 20:16:05 +08:00
FFXN 42aecd649c fix(knowledge_fs): add diagnostics for empty content and frame metadata logging 2026-08-08 20:15:21 +08:00
Asuka MinatoandGitHub 5a84cde198 test: move RAG pipeline workflow coverage to unit tests (#38937) 2026-08-08 12:00:32 +00:00
Byron.wangandGitHub 4e18ab0ad4 refactor(api): decouple setup and spec endpoints (#40104) 2026-08-08 11:52:15 +00:00
FFXN a4ee85232f fix(knowledge_fs): update lock 2026-08-08 19:43:13 +08:00
FFXN a6bd93036b fix(knowledge_fs): implement initial Firecrawl source import for knowledge-space creation with enhanced error handling and diagnostics 2026-08-08 19:42:47 +08:00
FFXN 3b6f011c6c fix(knowledge_fs): update online-document connector to concatenate Dify text messages and support structured content envelopes 2026-08-08 19:38:12 +08:00
FFXN faef9378e2 fix(knowledge_fs): enhance document path generation with sibling item handling 2026-08-08 18:40:53 +08:00
FFXN f1c302826c fix(knowledge_fs): enhance document path generation with sibling item handling 2026-08-08 18:40:33 +08:00
FFXN e733ccf47b fix(knowledge_fs): update lock 2026-08-08 16:19:16 +08:00
FFXN 757c7227f8 fix(knowledge_fs): enhance error handling and logging for source compilation failures 2026-08-08 16:18:23 +08:00
chelsealongGitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2a742486c5 fix(agent-v2): accept composer UI ids on manual metadata filter conditions (#40171)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-08 06:55:27 +00:00
LikalikaliandGitHub d362bdbb4e refactor: replace manual model_validate with @model_validate decorator in app controllers (#40170) 2026-08-08 05:47:11 +00:00
LikalikaliandGitHub 60d6be5b13 chore: dep inject for model in explore workflow run (#40158) 2026-08-08 02:50:27 +00:00
LikalikaliGitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
cc1b55aaf7 chore: dep inject for model in explore apis (#40160)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-08 02:23:17 +00:00
yyhandGitHub d1fa17032e revert(web): use suspense boundary in home page (#40167) 2026-08-07 18:50:33 +00:00
盐粒 YanliGitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
916d7e1c96 refactor(dify-agent): download Binding files through Agent Stub (#40143)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-07 14:56:52 +00:00
Stephen Zhou 86bfb66990 fix(knowledge-fs): update contract lock 2026-08-07 21:44:56 +08:00
LikalikaliandGitHub c094117ca2 chore: dep inject for model in agent app sandbox upload (#40153) 2026-08-07 13:28:31 +00:00
Stephen Zhou c7ef31fbeb fix(knowledge_fs): recover source sync status after reload 2026-08-07 21:25:21 +08:00
LikalikaliandGitHub c6b1311aa3 chore: dep inject for model in workspace snippets, load balancing, and endpoint (#40161) 2026-08-07 13:22:02 +00:00
LikalikaliandGitHub 515c3d0c62 chore: dep inject for model in onboarding step by step tour (#40155) 2026-08-07 12:41:22 +00:00
LikalikaliandGitHub fe94ddaaeb chore: dep inject for model in workflow run archive download (#40157) 2026-08-07 12:40:44 +00:00
LikalikaliandGitHub 3e8c0fdcef chore: dep inject for model in website crawl api (#40156) 2026-08-07 12:40:12 +00:00
LikalikaliandGitHub 055462fe6e chore: dep inject for model in tags, mcp server, and data source auth (#40159) 2026-08-07 12:38:29 +00:00
LikalikaliandGitHub 06e229e8b2 chore: dep inject for model in api based extension (#40154) 2026-08-07 12:37:50 +00:00
FFXN 28da39f909 fix(knowledge_fs): add last error message handling and improve logging for failed source imports 2026-08-07 20:27:57 +08:00
FFXN 03fba953ec fix(knowledge_fs): add last error message handling and improve logging for failed source imports 2026-08-07 20:16:56 +08:00
FFXN c6edaf70b6 fix(knowledge_fs): enhance workflow handling in initial source import 2026-08-07 19:21:19 +08:00
LikalikaliandGitHub fa498ad7a2 chore: dep inject for model in oauth server (#40147) 2026-08-07 11:16:11 +00:00
LikalikaliandGitHub a279be91a8 chore: dep inject for model in datasets document and web human input … (#40151) 2026-08-07 11:15:43 +00:00
FFXN 96c5703486 fix(knowledge_fs): add source update handling and adjust sync policy versioning 2026-08-07 18:58:01 +08:00
盐粒 YanliGitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
12300f3833 feat(dify-agent): propagate run limit error types (#40116)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-07 10:37:22 +00:00
JoelandGitHub b439b6a800 feat: support accessibility checks in CI (#40137) 2026-08-07 10:28:44 +00:00
FFXN 11a47127cd fix(knowledge_fs): implement firecrawl credential handling and connection creation 2026-08-07 18:17:05 +08:00
github-actions[bot]GitHubclaude[bot] <41898282+claude[bot]@users.noreply.github.com>
7a7d8fe19e chore(i18n): sync translations with en-US (#40150)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-08-07 10:10:33 +00:00
JoelandGitHub 2635d1b3df fix: improve readability of DSL import warnings (#40148) 2026-08-07 10:01:09 +00:00
yyhandGitHub a952487789 chore(web): remove unreachable explore sidebar (#40136) 2026-08-07 09:29:44 +00:00
WH-2099andGitHub c9e6db5ef2 fix(api): scope temporary capabilities to owners (#39863) 2026-08-07 09:21:13 +00:00
FFXN 622db45bd5 Merge remote-tracking branch 'origin/deploy/konwledge' into deploy/konwledge 2026-08-07 17:20:02 +08:00
Stephen Zhou f41abcd881 fix(knowledge-fs): register crawl import operation 2026-08-07 17:19:07 +08:00
FFXN 59b9453592 fix(knowledge_fs): standardize payload field names in initial source tasks 2026-08-07 17:18:44 +08:00
Stephen Zhou ada481e836 fix(knowledge-fs): satisfy initial source type check 2026-08-07 17:10:42 +08:00
Stephen Zhou 4011cfbdbc style(knowledge-fs): format initial source task 2026-08-07 17:03:34 +08:00
LikalikaliandGitHub b31e66107c chore: dep inject for model in notification dismiss (#40146) 2026-08-07 09:02:49 +00:00
FFXN cbecb64fef fix(knowledge_fs): refactor crawl page import logic to handle missing pages 2026-08-07 16:55:05 +08:00
FFXN 6c47ee52eb Merge remote-tracking branch 'origin/deploy/konwledge' into deploy/konwledge 2026-08-07 16:46:50 +08:00
FFXN 347cd11771 fix(knowledge_fs): update tests for createSourceCrawlImportWorkflow operation 2026-08-07 16:46:15 +08:00
Stephen Zhou 4bef5c4667 test(web): align website crawl sync policy default 2026-08-07 16:45:58 +08:00
Stephen Zhou 74ec7c8635 Merge remote-tracking branch 'origin/deploy/konwledge' into deploy/konwledge 2026-08-07 16:44:42 +08:00
Stephen Zhou 3b9be7d03f fix(knowledge-fs): improve source sync and crawl preview 2026-08-07 16:44:08 +08:00
FFXN e91b96f0bf Merge remote-tracking branch 'origin/deploy/konwledge' into deploy/konwledge 2026-08-07 16:31:47 +08:00
FFXN cb49d83e9c feat(knowledge_fs): import selected Firecrawl pages during space creation
- include Firecrawl configuration and selected page URLs in space creation
- start a server-side crawl import after KnowledgeFS space provisioning
- require the second crawl to match the complete user selection
- keep the knowledge space valid when website content import fails
- reuse existing background task status and failure UI
- add KnowledgeFS crawl-import API and regression tests
2026-08-07 16:29:25 +08:00
JoelandGitHub 42d6589ce2 fix: show inline agent build draft change details (#40140) 2026-08-07 07:28:41 +00:00
f00d0329d4 perf(web): prefetch app links on intent (#40133)
Co-authored-by: 非法操作 <[email protected]>
2026-08-07 07:16:38 +00:00
yyhandGitHub af9d2d00a3 chore(web): add dedicated home loading boundary (#40135) 2026-08-07 07:16:25 +00:00
非法操作andGitHub ae8cb996c7 chore: use multi-arch plugin daemon image tag (#40139) 2026-08-07 07:01:06 +00:00
盐粒 YanliandGitHub adb89a8926 refactor(dify-agent): make config pulls direct and remove gRPC (#40044) 2026-08-07 06:54:07 +00:00
非法操作andGitHub 3a4920c595 chore: bump plugin daemon image (#40134) 2026-08-07 06:22:33 +00:00
Stephen Zhou 9b146706fe fix(knowledge-fs): update contract lock 2026-08-07 14:03:41 +08:00
Jyong 01074e631e Document image-query retrieval implementation plan 2026-08-07 01:50:07 -04:00
Stephen Zhou 5af14795af fix(knowledge-fs): update contract lock 2026-08-07 13:33:46 +08:00
yyhGitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
276c3b6e02 refactor(web): localize education status query (#40132)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-07 05:29:31 +00:00
MubelotixandGitHub 7ff5c85796 docs: Fixes Star History chart (#40115) 2026-08-07 05:23:22 +00:00
Jyong 64b7cd58b2 Merge remote-tracking branch 'origin/deploy/konwledge' into deploy/konwledge
# Conflicts:
#	api/knowledge-fs-contract.lock.json
2026-08-07 01:21:22 -04:00
Jyong a3683465d3 Fix KnowledgeFS document outline fidelity 2026-08-07 01:20:50 -04:00
yyhandGitHub b8330df96e refactor(web): simplify billing portal link (#40131) 2026-08-07 04:26:18 +00:00
yyhandGitHub 7422052759 fix(web): scope hidden outlines to focus-visible (#40130) 2026-08-07 04:00:19 +00:00
Xiyuan ChenGitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
ab0e131e87 refactor(api): move license expiry notice flag onto license detail (#40128)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-07 03:53:35 +00:00
Asuka MinatoGitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1375dc3864 test: migrate console app sessions to SQLite (#40081)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-07 03:46:27 +00:00
Asuka MinatoGitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
6530f2bcdb test: migrate document indexing task sessions to SQLite (#40092)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-07 03:45:56 +00:00
Asuka MinatoandGitHub 7039217c99 test: use sqlite sessions at generation boundaries (#40095) 2026-08-07 03:45:27 +00:00
yyhandGitHub 924d787e41 fix(web): enforce frame ancestors on protected routes (#40124)
Signed-off-by: yyh <[email protected]>
2026-08-07 03:11:30 +00:00
Stephen Zhou d2fbc37a1b fix(web): handle deleted Firecrawl credentials 2026-08-07 11:10:53 +08:00
baba36d47d chore: improve current workspace summary (#40121)
Co-authored-by: Jingyi-Dify <[email protected]>
2026-08-07 03:08:55 +00:00
Stephen Zhou fab913bce5 fix(web): add provider shortcuts for sources 2026-08-07 10:55:44 +08:00
Stephen Zhou 3de65e6ce4 fix(web): make document list responsive 2026-08-07 10:29:09 +08:00
Xiyuan ChenGitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
4b2c5ac4ae feat: make the console license expiry badge optional (#39972)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-07 02:28:58 +00:00
yyhandGitHub 73197e4354 refactor(web): modernize Studio app list data flow (#40109) 2026-08-07 02:23:38 +00:00
FFXN d127b97e36 Merge remote-tracking branch 'origin/deploy/konwledge' into deploy/konwledge 2026-08-07 10:14:11 +08:00
FFXN d754811d5f fix(web): add website crawl preview selection and import functionality 2026-08-07 10:12:01 +08:00
JingyiGitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
5dfd83e9ab feat: optimize integrations initial loading (#40118)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-07 02:06:25 +00:00
Stephen Zhou 8a5c248fbb test: restore sqlite conversation coverage 2026-08-07 10:04:39 +08:00
yyhandGitHub 6f2e4d72d3 test(e2e): align duplicate Agent publish state (#40108) 2026-08-07 01:56:05 +00:00
Stephen Zhou fde800e337 Merge remote-tracking branch 'origin/main' into deploy/konwledge
# Conflicts:
#	.github/dependabot.yml
#	.github/workflows/deploy-dev.yml
#	api/app_factory.py
#	api/core/indexing_runner.py
#	api/services/feature_service.py
#	api/tests/test_containers_integration_tests/controllers/console/auth/test_password_reset.py
#	api/tests/test_containers_integration_tests/services/test_feature_service.py
#	api/tests/unit_tests/controllers/console/auth/test_account_activation.py
#	api/tests/unit_tests/controllers/service_api/app/test_conversation.py
#	api/tests/unit_tests/controllers/web/test_site.py
#	api/tests/unit_tests/core/rag/indexing/test_indexing_runner.py
#	api/tests/unit_tests/core/tools/test_base_tool.py
#	api/tests/unit_tests/extensions/otel/test_retrieval_tracing.py
#	api/tests/unit_tests/extensions/test_ext_login.py
#	api/tests/unit_tests/services/test_feature_service_deployment_edition.py
#	api/tests/unit_tests/services/test_feature_service_knowledge_fs.py
#	api/tests/unit_tests/services/test_feature_service_licensed_seats.py
2026-08-07 09:55:10 +08:00
Byron.wangandGitHub b76a14d71d refactor(api): decouple console queries through application services (#40031) 2026-08-07 00:42:55 +00:00
Yunlu WenGitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>-LAN-
1983e842ca feat: support azure keyvault (#39933)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: -LAN- <[email protected]>
2026-08-07 00:42:13 +00:00
Stephen Zhou f9e4c43a48 fix(web): align background tasks with design 2026-08-06 21:54:17 +08:00
Stephen Zhou 307dd7a704 fix(web): align golden question dialog behavior 2026-08-06 20:56:50 +08:00
zyssyz123GitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>yyh
90d6046345 fix: gate Agent access until first publish (#40105)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: yyh <[email protected]>
2026-08-06 12:36:24 +00:00
Stephen Zhou 88a9ada626 fix(web): align quality table header typography 2026-08-06 20:30:50 +08:00
Stephen Zhou 6c5ee0084f fix(web): align bad cases empty state with design 2026-08-06 20:19:30 +08:00
Stephen Zhou 262fbd7512 fix(web): align quality empty state with design 2026-08-06 19:39:42 +08:00
Stephen Zhou 783a645de0 fix(knowledge-fs): update contract lock 2026-08-06 18:58:05 +08:00
盐粒 YanliandGitHub 05c7386013 fix(dify-agent): end SSE streams on terminal events (#40042) 2026-08-06 10:53:10 +00:00
盐粒 YanliGitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
4d0bc32dd3 fix(dify-agent): align agent and shell execution limits (#40043)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-06 10:53:03 +00:00
Stephen Zhou b88a8933ed fix(knowledge-fs): allow zero-revision logical document deletion 2026-08-06 18:49:11 +08:00
Stephen Zhou fd8e4a80cd fix(knowledge-fs): update contract lock 2026-08-06 18:34:51 +08:00
Stephen Zhou e61ead98d9 fix(knowledge-fs): update contract lock 2026-08-06 18:27:51 +08:00
Stephen Zhou 3176291f40 fix(web): align documents page with design 2026-08-06 18:27:23 +08:00
Jyong bcc328eeb5 Merge remote-tracking branch 'origin/deploy/konwledge' into deploy/konwledge 2026-08-06 06:10:25 -04:00
Jyong 13ddabf832 Update application functionality and related components 2026-08-06 06:10:09 -04:00
FFXN 6ac9b4e7fa fix(web): implement crawl preview page selection component 2026-08-06 18:09:01 +08:00
FFXN 50a6764eaa Merge remote-tracking branch 'origin/deploy/konwledge' into deploy/konwledge 2026-08-06 17:46:30 +08:00
FFXN e6fec870db fix: remove auto preview functionality from source and knowledge creation workflows 2026-08-06 17:43:36 +08:00
Stephen Zhou 743b534212 fix(web): align document upload with design 2026-08-06 17:40:56 +08:00
yyhandGitHub d6336c5d49 revert(web): remove redundant slash picker intent tracking (#40103) 2026-08-06 09:20:46 +00:00
Stephen Zhou 25fee85f0f fix(web): align empty documents state with design 2026-08-06 17:02:50 +08:00
yyhandGitHub 7073baa737 fix(ci): switch Playwright jobs to Azure Ubuntu mirror (#40100) 2026-08-06 09:01:37 +00:00
FFXN 0cf549e14a Merge remote-tracking branch 'origin/deploy/konwledge' into deploy/konwledge 2026-08-06 16:45:29 +08:00
FFXN 88d9f82941 fix(web): restore crawl preview during knowledge creation
- hand off newly created spaces to the existing add-source workflow
- automatically start website crawl previews from creation flows
- consume the preview flag to prevent duplicate crawls on refresh
- add a navigation fallback when popstate is not emitted
- wait for the persisted source draft before mounting the preview
- add regression coverage for preview handoff and navigation recovery
2026-08-06 16:45:18 +08:00
Stephen Zhou d96a7707a6 fix: restore failed retrieval trace history 2026-08-06 16:44:13 +08:00
Stephen Zhou 43f7fcae36 fix: show retrieval metrics in trace history 2026-08-06 16:30:07 +08:00
Asuka MinatoandGitHub 78129cf6c3 test: use SQLite sessions in datasource files (#39048) 2026-08-06 08:28:27 +00:00
Asuka MinatoGitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1a69435fff test: use sqlite3 session in test_external_dataset_service (#38696)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-06 08:27:37 +00:00
FFXN 48c8495d3e fix: truncate document section paths to the KnowledgeFS virtual path limit 2026-08-06 16:21:25 +08:00
yyhandGitHub 33b238343b chore: remove obsolete development docs (#40098) 2026-08-06 08:21:17 +00:00
github-actions[bot]GitHubclaude[bot] <41898282+claude[bot]@users.noreply.github.com>
4b8a2c7e6c chore(i18n): sync translations with en-US (#40096)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-08-06 08:14:10 +00:00
FFXN e6dc350cf2 fix: truncate document section paths to the KnowledgeFS virtual path limit 2026-08-06 16:05:24 +08:00
FFXN 5b05ebee28 Merge remote-tracking branch 'origin/deploy/konwledge' into deploy/konwledge 2026-08-06 15:52:08 +08:00
FFXN 60bf39efd5 fix: truncate document section paths to the KnowledgeFS virtual path limit 2026-08-06 15:50:39 +08:00
JoelandGitHub 06d8301a90 fix: report unavailable agent tools in publish and workflow checks (#40077) 2026-08-06 07:44:22 +00:00
Stephen Zhou 09cbdda519 fix(knowledge-fs): update contract lock 2026-08-06 15:41:10 +08:00
yyhandGitHub dc309ced59 chore(web): remove obsolete PWA assets (#40082) 2026-08-06 07:41:01 +00:00
Stephen Zhou 3ebbe0e4ae fix(web): hide tasks in empty documents state 2026-08-06 15:19:46 +08:00
Stephen Zhou cc10ca8198 fix: clean up profile migration publication references 2026-08-06 15:13:29 +08:00
cf88aef710 feat: support TiDB Vector full-text and hybrid search (#38112)
Co-authored-by: Crazywoola <[email protected]>
Co-authored-by: bkidy <[email protected]>
2026-08-06 07:08:42 +00:00
BoneNIGitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>crazywoola
0277b9eb30 feat: Add Lao (lo-LA) language support #39489 (#39474)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: crazywoola <[email protected]>
2026-08-06 07:00:37 +00:00
8fbbfbc243 fix(web): register audio listeners on real media events (#37841)
Co-authored-by: crazywoola <[email protected]>
2026-08-06 06:57:58 +00:00
Stephen Zhou f2255e0d27 fix(web): align retrieval failure state with design 2026-08-06 14:51:54 +08:00
FFXN b68289f24a fix: prevent source deletion failures caused by UUID and text type mismatch 2026-08-06 14:40:30 +08:00
wangxiaoleiGitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
5bceac05eb fix: get role and list role need filter billing permission key (#38218)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-06 06:37:52 +00:00
yyhandGitHub febcc4e054 chore: format GitHub metadata with Oxfmt (#40073) 2026-08-06 06:37:06 +00:00
Souravrajvi0andGitHub 36c990a0cf fix(web): only open slash variable picker when / is typed (#39755) (#39761) 2026-08-06 06:36:30 +00:00
yyhandGitHub 658d5e0dcf fix(e2e): align marketplace plugin download URL (#40075) 2026-08-06 06:32:16 +00:00
Stephen Zhou 18ae46f7a3 fix(web): align retrieval result annotations 2026-08-06 13:59:50 +08:00
yyhandGitHub cfef48c408 chore: upgrade workspace dependencies (#40069) 2026-08-06 05:56:30 +00:00
1973a2e96f feat(trace-aliyun): GenAI spans for LLM TTFT, agent ReAct, tools, and failed LLM nodes (#39339)
Co-authored-by: Cursor <[email protected]>
2026-08-06 05:51:28 +00:00
b105f21763 test: use SQLite sessions in core repositories (#39061)
Co-authored-by: Byron Wang <[email protected]>
2026-08-06 05:50:40 +00:00
Wu TianweiandGitHub e5a07e97a4 fix(web): improve markdown form rendering (#40070) 2026-08-06 05:49:24 +00:00
Stephen Zhou 670fa67ef9 fix(web): align new rag retrieval states with design 2026-08-06 13:29:29 +08:00
Asuka MinatoGitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
046df1260a test: use SQLite sessions in controllers console app (#39096)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-06 05:25:01 +00:00
Asuka MinatoandGitHub 9e95e1302e test: use SQLite sessions in commands (#39082) 2026-08-06 05:14:38 +00:00
Asuka MinatoandGitHub 59cbba3b37 test: use SQLite sessions in controllers console auth (#39060) 2026-08-06 05:12:35 +00:00
20b31de791 test: use SQLite sessions in core entities (#39056)
Co-authored-by: Byron Wang <[email protected]>
2026-08-06 05:10:04 +00:00
Stephen Zhou 654884817a fix(web): align new rag retrieval test with design 2026-08-06 12:53:57 +08:00
yyhandGitHub 5e036e5489 refactor(system-features): derive nullable SSO protocol (#40063) 2026-08-06 04:51:32 +00:00
Asuka MinatoandGitHub e6ede05fe9 test: use SQLite sessions in clean dataset (#39051) 2026-08-06 04:49:10 +00:00
Asuka MinatoGitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
7edd05100a test: move webhook service coverage to unit tests (#38945)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-06 04:48:27 +00:00
Asuka MinatoGitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
46024b6ad8 test: use SQLite sessions in core app apps (#39083)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-06 04:38:15 +00:00
Stephen Zhou f1ea6338c3 fix(web): autosave new rag settings 2026-08-06 12:37:15 +08:00
Asuka MinatoandGitHub 123f128ccc test: use real ORM models in app and API tests (#40020) 2026-08-06 04:36:58 +00:00
JoelandGitHub 6474059eb1 fix: hide log detail actions for legacy app logs (#40057) 2026-08-06 04:18:23 +00:00
Asuka MinatoandGitHub 479a5613a8 test: use real ORM models in dataset service tests (#40018) 2026-08-06 04:14:12 +00:00
d51900974f fix: offset legacy app error toasts below model controls (#40040)
Co-authored-by: yyh <[email protected]>
Co-authored-by: yyh <[email protected]>
2026-08-06 04:14:12 +00:00
Stephen Zhou d16b9c5e8b fix(web): align new rag overview with design 2026-08-06 12:09:04 +08:00
Asuka MinatoandGitHub f08cc41546 test: use SQLite sessions in services core (#39058) 2026-08-06 04:07:01 +00:00
Asuka MinatoandGitHub 2443df2e42 test: use real ORM models in workflow repository tests (#40019) 2026-08-06 04:06:04 +00:00
Stephen Zhou 1fe71ee5d3 chore: update 2026-08-06 11:59:52 +08:00
Asuka MinatoandGitHub 2e65ab9d1e test: use SQLite sessions in core tools (#39108) 2026-08-06 03:57:33 +00:00
hyoban be221ec89d fix(web): restore new rag datasets page 2026-08-06 11:34:29 +08:00
6add64a44d fix(api): allow account avatars across workspaces (#40055)
Co-authored-by: aiden <[email protected]>
2026-08-06 03:33:23 +00:00
非法操作GitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
8170a2a5f3 chore: more upload file size for paid user (#39967)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-06 03:23:16 +00:00
Stephen Zhou d144840384 Merge remote-tracking branch 'origin/deploy/konwledge' into deploy/konwledge 2026-08-06 11:02:30 +08:00
Stephen Zhou 8d2eac55d7 fix(agent): preserve workflow node execution logs 2026-08-06 11:01:54 +08:00
Stephen Zhou a7283bccb3 Merge remote-tracking branch 'origin/main' into deploy/konwledge 2026-08-06 10:46:50 +08:00
Stephen Zhou 24847366e1 Merge remote-tracking branch 'origin/main' into deploy/konwledge
# Conflicts:
#	api/app_factory.py
#	api/controllers/console/workspace/workspace.py
#	api/services/agent/observability_service.py
#	api/tests/unit_tests/core/rag/docstore/test_dataset_docstore.py
#	api/tests/unit_tests/core/rag/embedding/test_token_counter.py
#	api/tests/unit_tests/core/rag/indexing/test_indexing_runner.py
#	api/tests/unit_tests/services/agent/test_agent_observability_service.py
2026-08-06 10:46:15 +08:00
FFXN 095e9c3a31 fix: update subtreeTree hash in knowledge-fs-contract.lock.json 2026-08-06 10:36:03 +08:00
pineapple880066andGitHub 70051950ab fix: preserve UTF-8 byte size for text uploads (#40034) 2026-08-06 02:25:29 +00:00
f7881bb635 fix: make document retry batches consistent (#39978)
Co-authored-by: caosurong <[email protected]>
2026-08-06 02:01:14 +00:00
Escape0707GitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
b6b3e747f3 refactor(api): extract workspace member listing into layered service (#39974)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-06 01:08:39 +00:00
JingyiGitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
18f740402f fix(web): redirect legacy plugin fallbacks (#40051)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-05 23:27:08 +00:00
JingyiandGitHub 15a5317bd3 fix(web): refresh model provider plugins after updates (#40002) 2026-08-05 23:26:18 +00:00
盐粒 YanliandGitHub 1369a2cdfc fix(dify-agent): wait for E2B shellctl readiness (#39996) 2026-08-05 11:03:06 +00:00
FFXN f1e79a7ad6 feat: implement logging for durable deletion failures with job details 2026-08-05 18:57:12 +08:00
Asuka MinatoandGitHub ac0c909d11 test: use real ORM models in RAG indexing tests (#40017) 2026-08-05 10:03:23 +00:00
林玮 (Jade Lin)andGitHub 945503523b fix(web): improve debug plugin copy accessibility (#40035) 2026-08-05 10:00:37 +00:00
Asuka MinatoandGitHub fe7e2dc62f test: use sqlite3 session in test_agent_config_service (#38764) 2026-08-05 08:43:40 +00:00
Asuka MinatoandGitHub 3bb21a9a88 test: use sqlite3 session in test_credit_pool_service (#38694) 2026-08-05 08:43:27 +00:00
yyhandGitHub dc09384b1d refactor(web): derive system feature enums from schemas (#40030) 2026-08-05 08:32:54 +00:00
FFXN 3213741104 feat: add logging for upstream rejection details in KnowledgeFS requests 2026-08-05 15:55:28 +08:00
yyhandGitHub 890e5aa294 refactor(web): align query client ownership (#40024) 2026-08-05 07:02:09 +00:00
yyhandGitHub 0bf3416431 fix(web): remove misleading ARIA states (#40015) 2026-08-05 06:52:41 +00:00
yyhandGitHub 685e291e65 refactor(web): remove duplicate dataset title update (#40023) 2026-08-05 06:18:09 +00:00
Jyong ddd5e3c46b Merge remote-tracking branch 'origin/deploy/konwledge' into deploy/konwledge 2026-08-05 02:06:10 -04:00
Jyong 2204ccaed5 chore(knowledge-fs): refresh contract lock 2026-08-05 02:00:43 -04:00
Stephen Zhou a4756e953d feat(dataset): filter New RAG spaces by creator 2026-08-05 13:46:06 +08:00
yyhandGitHub c15dd6d994 refactor(web): simplify tag selector trigger (#40014) 2026-08-05 05:23:53 +00:00
JoelandGitHub 72b30ff313 chore: add copy action to workflow log errors (#40011) 2026-08-05 05:15:59 +00:00
Jyong aa58dfb598 Merge remote-tracking branch 'origin/deploy/konwledge' into deploy/konwledge 2026-08-05 00:16:10 -04:00
yyhandGitHub 7dd7b3dfa8 fix(ci): make web workflows reproducible (#40010) 2026-08-05 04:08:12 +00:00
yyhandGitHub d033124552 fix(ci): harden Playwright browser installs (#40009) 2026-08-05 03:57:08 +00:00
yyhandGitHub 2fd8f98f2d chore(deps): upgrade Base UI to 1.7.0 (#40006) 2026-08-05 03:55:46 +00:00
Jyong 140726a1ea merge: structured document section tree 2026-08-04 23:46:34 -04:00
Jyong 90e373199c fix(knowledge-fs): use structured document sections 2026-08-04 23:46:13 -04:00
Asuka MinatoandGitHub 7d6d173b01 test: use SQLite in Arize Phoenix traces (#38984) 2026-08-05 03:45:06 +00:00
yyhandGitHub 2dc6b08044 fix(web): preserve feedback state on submission failure (#40004) 2026-08-05 02:55:50 +00:00
896e5e20c0 refactor(api): extract workspace list into layered application service (#39822)
Co-authored-by: WH-2099 <[email protected]>
2026-08-05 02:36:16 +00:00
yyhandGitHub 143bc121da fix(e2e): configure sandbox-reachable API URL (#40005) 2026-08-05 02:28:16 +00:00
Stephen Zhou c0a5ec4e67 feat(dataset): preview local files before upload 2026-08-05 09:36:14 +08:00
zyssyz123GitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
4213e6b150 fix(agent): expose feedback in agent logs (#39994)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-04 14:11:13 +00:00
盐粒 YanliGitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
b462cb041d feat(agent): separate CLI file URL audiences (#39952)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-04 13:14:02 +00:00
Stephen Zhou a77f508b05 feat(dataset): show linked app counts in New RAG spaces 2026-08-04 20:49:52 +08:00
9d81f0da10 chore(api): remove unreachable match suppressions (#39949)
Co-authored-by: nightcityblade <[email protected]>
2026-08-04 11:56:37 +00:00
Stephen Zhou d2b58f8636 Merge remote-tracking branch 'origin/main' into deploy/konwledge
# Conflicts:
#	web/features/new-rag/new-knowledge-list.tsx
2026-08-04 19:36:24 +08:00
Stephen Zhou c758131fb9 feat(dataset): support New RAG document metadata 2026-08-04 19:33:19 +08:00
Asuka MinatoandGitHub 482ec5e712 test: use sqlite3 session in test_qa_index_processor (#38765) 2026-08-04 10:53:55 +00:00
Asuka MinatoandGitHub 36a4c98732 test: use sqlite3 session in test_if_else (#38753) 2026-08-04 10:53:42 +00:00
yyhandGitHub dec1cb3798 fix(ui): align button spacing with design (#39990) 2026-08-04 10:37:16 +00:00
Jyong 4fd80c8d59 Improve knowledge grep matching for document projections 2026-08-04 05:41:52 -04:00
林玮 (Jade Lin)andGitHub 3ada29bbe0 fix(api): surface system quota exhaustion without fallback (#39986) 2026-08-04 08:49:25 +00:00
非法操作andGitHub 8f817644e9 fix(workflow): avoid app context dependency in parallel iteration (#39983) 2026-08-04 07:47:49 +00:00
Jyong fcf87ec68c fix(knowledge-fs): read current published fs paths 2026-08-04 03:46:00 -04:00
Stephen Zhou 1ac9a6ba25 fix(dataset): align New RAG quality button spacing 2026-08-04 15:34:51 +08:00
Jyong 3d9feaba0b fix(knowledge-fs): classify capability reads from operations 2026-08-04 03:17:15 -04:00
JoelandGitHub 0271ca6e5f fix: align build and preview chat layouts (#39980) 2026-08-04 07:07:48 +00:00
Jyong c944ffe558 fix(knowledge-fs): honor capability grants in fs handlers 2026-08-04 02:56:03 -04:00
Stephen Zhou 28b3306867 Merge remote-tracking branch 'origin/main' into deploy/konwledge 2026-08-04 14:43:38 +08:00
-LAN-GitHubautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
aab31292d8 feat(workflow): support LLM environment variables (#39125)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-04 06:17:13 +00:00
-LAN-andGitHub 545bdecff2 chore(docker): move optional settings to service envs (#39970) 2026-08-04 05:28:05 +00:00
yyhandGitHub 7d92097526 fix(dify-ui): improve select and status dot accessibility (#39971) 2026-08-04 05:14:02 +00:00
Xiyuan ChenandGitHub edcd3fc2e3 fix(api): allow external knowledge base creation under enterprise RBAC (#39930) 2026-08-04 04:50:43 +00:00
yyhandGitHub be413650ba chore(web): upgrade Next.js to 16.3 (#39962) 2026-08-04 04:46:44 +00:00
Jyong 87d305da1d chore(knowledge-fs): refresh contract subtree lock 2026-08-04 00:26:36 -04:00
Jyong 505fe93d2c fix(knowledge-fs): update vulnerable transitive dependencies 2026-08-04 00:17:57 -04:00
Jyong e08568b3b5 ci(cli): build edge on knowledge deploy branch 2026-08-04 00:08:13 -04:00
-LAN-andGitHub 2d2cc7cab1 refactor(workflow): move AgentNode events out of graphon (#39845) 2026-08-04 03:54:40 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>-LAN-
2838957004 chore(deps): bump the vdb group with 9 updates (#39911)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: -LAN- <[email protected]>
2026-08-04 03:40:41 +00:00
f6852205d2 fix(vdb): bound the TiDB Cloud API timeouts (#39953)
Signed-off-by: Yufeng He <[email protected]>
Co-authored-by: -LAN- <[email protected]>
2026-08-04 03:17:05 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>-LAN-
9802329b91 chore(deps): bump aiohttp from 3.14.1 to 3.14.3 in /api (#39959)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: -LAN- <[email protected]>
2026-08-04 03:14:55 +00:00
Stephen Zhou a65845e5fa fix(dataset): synchronize New RAG translations 2026-08-03 23:22:30 +08:00
Stephen Zhou da92158f26 fix(knowledge-fs): refine Chinese document copy 2026-08-03 17:57:54 +08:00
Jyong e65905b4b3 Enrich research task progress with stage details 2026-08-03 05:38:52 -04:00
Jyong 17ccc0b995 Merge remote-tracking branch 'origin/deploy/konwledge' into deploy/konwledge 2026-08-03 05:04:06 -04:00
Jyong 3d99416631 Implement application updates 2026-08-03 05:01:00 -04:00
Stephen Zhou 53e9a62b6e fix(knowledge-fs): enable research citation navigation 2026-08-03 12:50:27 +08:00
Stephen Zhou 19ad4b6a72 fix(knowledge-fs): sync retrieval record selection with URL 2026-08-03 11:49:42 +08:00
Stephen Zhou 701f591c5e fix(knowledge-fs): sync document upload state with URL 2026-08-03 11:31:05 +08:00
Stephen Zhou ac0031b75d fix(knowledge-fs): restore discard dialog spacing 2026-08-03 11:18:25 +08:00
Stephen Zhou c7e4cb0c29 Merge remote-tracking branch 'origin/main' into deploy/konwledge
# Conflicts:
#	api/tests/unit_tests/extensions/test_ext_login.py
#	web/app/components/main-nav/__tests__/index.spec.tsx
2026-08-03 11:10:06 +08:00
Stephen Zhou d040afebfb fix(knowledge-fs): align new rag edge states 2026-08-03 11:06:21 +08:00
Jyong 1648d8ac6b fix(knowledge-fs): handle initial revisions and published evidence 2026-08-02 14:47:09 -04:00
Jyong afee5e28d2 Update knowledge filesystem contract subtree reference 2026-08-02 13:46:37 -04:00
Jyong 4a7195be12 Bound Dify embedding requests and preserve result ordering 2026-08-02 13:28:31 -04:00
Jyong 923507baeb Stream research task answer deltas through durable progress events 2026-08-02 12:45:51 -04:00
Jyong 6aacd46b03 test(knowledge-fs): cover openapi filesystem controller 2026-08-02 12:06:26 -04:00
Jyong d93902eaff fix(knowledge-fs): validate evidence match response 2026-08-02 11:56:25 -04:00
Jyong 4d5f845b8c Merge codex/golden-question-bulk-import into deploy/konwledge
# Conflicts:
#	api/knowledge-fs-contract.lock.json
#	api/services/knowledge_fs/data_facade.py
#	api/tests/unit_tests/services/test_knowledge_fs_data_facade.py
#	knowledge-fs/scripts/export-capability-v2-operations.test.mjs
#	packages/contracts/generated/api/console/knowledge-fs/orpc.gen.ts
2026-08-02 11:35:09 -04:00
Jyong c9cdc34abc feat(dataset): add golden question bulk import 2026-08-02 11:29:11 -04:00
Jyong 737f315b53 Merge deploy/konwledge into codex/knowledge-fs-difyctl 2026-08-02 11:24:06 -04:00
Stephen Zhou c91e48127a fix(dataset): align New RAG overview indexing state 2026-07-31 17:20:07 +08:00
Stephen Zhou 0eb582492f fix(knowledge-fs): update contract lock 2026-07-31 17:15:40 +08:00
Stephen Zhou 74ec0c78b9 test(knowledge-fs): update capability export count 2026-07-31 17:09:21 +08:00
Stephen Zhou abee50b90e fix(dataset): align New RAG activity filters 2026-07-31 17:00:59 +08:00
Stephen Zhou e891790ce0 fix(dataset): connect New RAG overview data 2026-07-31 16:23:50 +08:00
Jyong ce847f2b6f feat(cli): add KnowledgeFS filesystem commands 2026-07-31 04:11:14 -04:00
Stephen Zhou 26f95a3bca fix(dataset): align New RAG overview skeleton layout 2026-07-31 15:45:46 +08:00
Stephen Zhou 0950bf609d fix(dataset): polish New RAG overview states 2026-07-31 15:29:41 +08:00
Stephen Zhou 5b939067e1 Merge remote-tracking branch 'origin/main' into deploy/konwledge
# Conflicts:
#	api/tests/unit_tests/services/test_app_dsl_service.py
#	web/features/new-rag/__tests__/new-knowledge-list.spec.tsx
2026-07-31 14:28:03 +08:00
Stephen Zhou 465e7011e5 fix(dataset): align New RAG overview design 2026-07-31 12:05:53 +08:00
Stephen Zhou fe79fed793 fix(knowledge-fs): update contract lock 2026-07-31 11:55:29 +08:00
Stephen Zhou c0b8e901ba fix(dataset): make quality table responsive 2026-07-31 11:48:54 +08:00
Stephen Zhou 007b28e2ed fix(dataset): fill document detail height 2026-07-31 11:45:48 +08:00
Stephen Zhou 480deebf02 fix(knowledge-fs): harden research progress and scoring 2026-07-31 11:39:36 +08:00
Jyong f1045bc099 Retry provider errors in document compilation runtime 2026-07-30 23:35:31 -04:00
Stephen Zhou 8a7da3192f fix(dataset): align New RAG retrieval test design 2026-07-31 01:29:45 +08:00
Stephen Zhou bc312f249e fix(dataset): refresh quality after golden question save 2026-07-31 01:16:14 +08:00
Stephen Zhou a392199048 fix(dataset): align New RAG sidebar active state 2026-07-31 01:11:04 +08:00
Stephen Zhou 337c4f8f87 fix(knowledge-fs): update contract lock 2026-07-31 01:08:45 +08:00
Stephen Zhou c517805213 fix(dataset): remove redundant quality table scrollbar 2026-07-31 01:01:07 +08:00
Stephen Zhou 049208c095 fix(knowledge-fs): fence capability profile activation 2026-07-31 00:54:37 +08:00
Stephen Zhou c07f3d9373 fix(dataset): align New RAG document chunk presentation 2026-07-31 00:18:37 +08:00
Jyong f39b15f76d Resume document compilation from persisted checkpoints 2026-07-30 12:03:59 -04:00
Stephen Zhou cb02cdd545 fix(dataset): stabilize New RAG re-index task handling 2026-07-30 23:51:33 +08:00
Stephen Zhou 3190ea955a fix(dataset): auto-load New RAG document chunks 2026-07-30 22:06:25 +08:00
Stephen Zhou 9dca9e139f fix(knowledge-fs): update contract lock 2026-07-30 21:53:14 +08:00
Stephen Zhou 3adb119dd3 test(knowledge-fs): update capability export count 2026-07-30 21:46:43 +08:00
Stephen Zhou 13778d96a0 fix(dataset): wire New RAG document actions 2026-07-30 21:37:05 +08:00
Stephen Zhou 121a2e7b87 fix(knowledge-fs): update contract lock 2026-07-30 20:46:46 +08:00
Stephen Zhou 29602d261c Revert "fix(dataset): retry failed New RAG document processing"
This reverts commit 3bca4b3485.
2026-07-30 20:37:15 +08:00
Stephen Zhou bf5f916cd4 fix(knowledge-fs): improve semantic extraction resilience 2026-07-30 20:36:53 +08:00
Jyong 5eabe14982 Merge remote-tracking branch 'origin/deploy/konwledge' into deploy/konwledge 2026-07-30 07:37:23 -04:00
Jyong 6c59bdf724 Merge branch 'codex/research-semantic-llm-tree-search' into deploy/konwledge 2026-07-30 07:35:43 -04:00
Jyong 02e8867588 feat(knowledge-fs): add semantic LLM research retrieval 2026-07-30 07:35:31 -04:00
Stephen Zhou 3bca4b3485 fix(dataset): retry failed New RAG document processing 2026-07-30 18:26:04 +08:00
Stephen Zhou 8f19a9163a fix(dataset): center New RAG document action icon 2026-07-30 18:18:42 +08:00
Stephen Zhou 1a4ad7edb2 fix(dataset): center New RAG documents content 2026-07-30 18:08:17 +08:00
Stephen Zhou 178c301c5f fix(dataset): align New RAG settings edge states 2026-07-30 17:59:00 +08:00
Stephen Zhou 1e2ebf43f9 fix(knowledge-fs): update contract lock 2026-07-30 17:33:25 +08:00
Jyong c5597305e0 Merge remote-tracking branch 'origin/deploy/konwledge' into deploy/konwledge 2026-07-30 05:20:42 -04:00
Jyong 0e06266770 Aggregate knowledge-space overview queries across all members 2026-07-30 05:20:20 -04:00
Stephen Zhou 4ee61733ec fix(dataset): remove New RAG create chevron 2026-07-30 17:08:21 +08:00
Stephen Zhou dc959e0c0f fix(dataset): align New RAG settings with design 2026-07-30 17:04:54 +08:00
Stephen Zhou 7b8770d203 fix(knowledge-fs): update contract lock 2026-07-30 17:03:18 +08:00
Jyong 4b53ad8b9d Route knowledge graph ingestion through space-scoped reasoning models 2026-07-30 04:54:09 -04:00
Stephen Zhou 1b9db252bc fix(dataset): enable New RAG settings scrolling 2026-07-30 16:23:50 +08:00
Stephen Zhou 3caeb28f4a fix(dataset): align New RAG sources layout 2026-07-30 16:17:59 +08:00
Stephen Zhou 360668492f fix(dataset): align New RAG source icon 2026-07-30 16:03:14 +08:00
Jyong 4707a4226a chore: align KnowledgeFS contract lock after merge 2026-07-30 03:53:07 -04:00
Jyong ca0aca6fd7 Merge remote-tracking branch 'origin/deploy/konwledge' into deploy/konwledge 2026-07-30 03:43:36 -04:00
Stephen Zhou 9b6796df1c fix(dataset): align New RAG empty state with design 2026-07-30 15:25:10 +08:00
Jyong 1af4d746d3 merge: fix golden questions latency 2026-07-30 03:15:13 -04:00
Jyong a82290fe8f perf(knowledge-fs): reduce capability request overhead 2026-07-30 03:14:51 -04:00
Jyong fc377d4b8f chore: refresh KnowledgeFS contract lock 2026-07-30 02:52:31 -04:00
Jyong 0ec4940c9a fix research result partial refresh 2026-07-30 02:49:10 -04:00
Jyong acac966cdc fix duplicate background task polling 2026-07-30 02:34:24 -04:00
JyongandClaude Opus 4.8 79984e1c84 perf(knowledge-fs): eliminate graph and publication query hot spots
Fixes five scaling hot spots in the KnowledgeFS repository layer. All changes
preserve existing behavior and dual-dialect (Postgres/TiDB) parity; verified via
the full packages/api suite and by generating the emitted SQL for both dialects.

- Graph traversal (#2): restrict the relation_fanout CTE to relations whose
  subject is the start entity or one of its objects, so the ROW_NUMBER window no
  longer ranks every relation in the space. maxDepth is capped at 2, so the
  reachable-subject seed is a provable superset of the visited frontier; results
  are unchanged. Uses the existing graph_relations_subject_traversal_idx.
- Community materialization (#3): bound the per-entity traversal fan-out with a
  concurrency limiter instead of an unbounded Promise.all, preventing connection
  pool exhaustion. Identical queries and results.
- Profile-migration PageIndex promotion (#4): replace the up-to-100k per-row
  SELECT ... FOR UPDATE loop with one set-based UPDATE plus a NOT EXISTS
  completeness fence, keeping the safety bound and error code.
- Chunk snapshot publish (#5): batch the per-chunk INSERT loop into multi-row
  VALUES statements (<=1000 rows/batch, under the bind-parameter limit).
- Graph batch upsert read-back (#7): replace the per-row SELECT after each batch
  upsert with one indexed IN-list query per (space, generation) group, preserving
  the mismatch, duplicate, and did-not-persist fences.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-30 02:27:43 -04:00
Stephen Zhou b66c7c9a52 Remove KnowledgeFS full-chain validation record 2026-07-30 14:23:25 +08:00
Jyong e052b2e27f Support emoji icons and map Knowledge FS bad requests 2026-07-30 01:34:56 -04:00
Stephen Zhou 9bfe856f59 fix(dataset): make New RAG quality tables responsive 2026-07-30 11:30:15 +08:00
Stephen Zhou 321263af61 fix(dataset): align New RAG page layouts 2026-07-30 11:09:03 +08:00
Stephen Zhou 37b07b1709 fix(dataset): align New RAG empty states 2026-07-30 10:51:26 +08:00
Stephen Zhou 72a75fe3bb fix(dataset): align New RAG sources empty state 2026-07-30 10:13:45 +08:00
Jyong 5869016c42 Restore Research answer synthesis routing 2026-07-29 13:32:01 -04:00
Jyong e653935288 Normalize retrieval scores before fusion and ranking 2026-07-29 12:45:45 -04:00
Jyong 08e974ed48 chore: refresh KnowledgeFS contract lock 2026-07-29 11:52:00 -04:00
Stephen Zhou cf0d5afcef fix(dataset): stabilize overview loading state 2026-07-29 23:27:37 +08:00
Jyong 3a42945fcc Refresh retry base revision and preserve immutable candidates 2026-07-29 11:14:51 -04:00
Jyong b8a2f991c1 Keep interactive retrieval evidence-only and isolate Research synthesis 2026-07-29 11:14:51 -04:00
Jyong 5c359b710a Normalize Unstructured layout and enable coordinates 2026-07-29 11:14:51 -04:00
Stephen Zhou 4ffe1870d1 fix(dataset): synchronize New RAG translations 2026-07-29 22:52:23 +08:00
Stephen Zhou 2f892071d2 fix(dataset): align New RAG acceptance flows 2026-07-29 22:41:42 +08:00
Stephen Zhou effcefca34 fix(knowledge-fs): update contract lock 2026-07-29 20:45:34 +08:00
Stephen Zhou 617ba37988 lint 2026-07-29 20:29:28 +08:00
Stephen Zhou addd754eb3 Merge remote-tracking branch 'origin/deploy/konwledge' into deploy/konwledge 2026-07-29 20:27:52 +08:00
Stephen Zhou f2138ac15e Merge remote-tracking branch 'origin/deploy/konwledge' into deploy/konwledge 2026-07-29 20:26:19 +08:00
Stephen Zhou f9bd5ad923 Merge remote-tracking branch 'origin/main' into deploy/konwledge
# Conflicts:
#	api/tests/unit_tests/services/test_account_service.py
#	web/features/new-rag/add-source-page.tsx
#	web/features/new-rag/components/knowledge-space-card.tsx
#	web/features/new-rag/components/knowledge-view-switcher.tsx
#	web/features/new-rag/document-chunk-detail.tsx
#	web/features/new-rag/document-list.tsx
#	web/features/new-rag/knowledge-space-shell.tsx
#	web/features/new-rag/sources-page.tsx
2026-07-29 20:26:13 +08:00
Jyong dd1d24e341 Preserve Dify route identity across upstream model aliases 2026-07-29 07:27:19 -04:00
Jyong 7731f1bdc0 Harden Knowledge FS remote transport validation 2026-07-29 07:01:59 -04:00
Jyong 5270f78ff1 Merge remote-tracking branch 'origin/deploy/konwledge' into deploy/konwledge
# Conflicts:
#	api/knowledge-fs-contract.lock.json
2026-07-29 06:22:07 -04:00
Jyong 34f4e3b145 Update frontend components and application workflows 2026-07-29 06:18:05 -04:00
Stephen Zhou 360fa45b3e Merge remote-tracking branch 'origin/main' into deploy/konwledge 2026-07-29 17:42:53 +08:00
Stephen Zhou 9187eb61ac fix(knowledge-fs): update contract lock 2026-07-29 17:29:04 +08:00
Stephen Zhou 5c311d3eef feat(dataset): add New Knowledge overview 2026-07-29 17:20:40 +08:00
Jyong 1884f03350 Reduce KnowledgeFS task polling page size 2026-07-29 04:36:43 -04:00
Jyong 333c50f663 Increase preflight reasoning output token limit 2026-07-29 04:10:35 -04:00
Jyong 71e9755d6c Scope knowledge rate limiting to entry points 2026-07-29 03:09:42 -04:00
Jyong 6cfd1abb59 Refactor frontend architecture and simplify implementation 2026-07-29 02:30:27 -04:00
Jyong 5e7ea24340 fix(knowledge-fs): use declared startup probe table 2026-07-29 01:39:16 -04:00
Jyong 71d07d8770 fix(knowledge-fs): retry database startup recovery 2026-07-29 00:51:28 -04:00
Stephen Zhou 6dd0f85f67 feat(dataset): add New Knowledge space actions 2026-07-29 12:15:33 +08:00
Stephen Zhou 85fbeb644a chore: refresh KnowledgeFS contract lock 2026-07-29 11:35:43 +08:00
Stephen Zhou b8a4586b51 test(knowledge-fs): cover product validation branches 2026-07-29 11:29:04 +08:00
Stephen Zhou d51692a62d fix(api): restore agent app binding lookup 2026-07-29 11:29:01 +08:00
Stephen Zhou 6f6198f719 test: align password reset token assertion 2026-07-29 11:01:25 +08:00
Stephen Zhou cd86ffd6f8 Merge remote-tracking branch 'origin/main' into deploy/konwledge
# Conflicts:
#	api/controllers/console/app/completion.py
#	api/core/app/apps/agent_app/app_generator.py
#	api/services/agent/composer_service.py
#	api/services/agent/roster_service.py
#	api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py
#	api/tests/unit_tests/core/agent/test_publish_visibility.py
#	api/tests/unit_tests/core/app/apps/agent_app/test_resolve_agent.py
#	api/tests/unit_tests/core/workflow/nodes/agent_v2/test_binding_resolver.py
#	api/tests/unit_tests/services/agent/test_agent_services.py
2026-07-29 10:58:29 +08:00
Stephen Zhou 20a06fdb94 feat(dataset): add New RAG quality control 2026-07-29 10:49:52 +08:00
Stephen Zhou 4bb1165c9d chore: refresh KnowledgeFS contract lock 2026-07-28 23:50:28 +08:00
Stephen Zhou 3a7a5075de feat(dataset): add New RAG retrieval test 2026-07-28 23:34:06 +08:00
Stephen Zhou f5fa4d8213 feat(dataset): support deferred New RAG model setup 2026-07-28 23:19:25 +08:00
Jyong 51d36b5c9e test(knowledge-fs): cover archived workspace bootstrap 2026-07-28 09:25:21 -04:00
Jyong 2674190bb4 fix(knowledge-fs): align retrieval profile payload alias 2026-07-28 09:21:40 -04:00
Jyong ff07021c22 feat(knowledge-fs): automate greenfield workspace cutover 2026-07-28 09:12:50 -04:00
Stephen Zhou d2380dd951 feat(dataset): add New RAG knowledge settings 2026-07-28 20:56:00 +08:00
Jyong a7fb5513e4 chore: refresh KnowledgeFS contract lock 2026-07-28 06:44:29 -04:00
Jyong 76f67f096e fix(knowledge-fs): decode freeze bigint evidence 2026-07-28 06:34:02 -04:00
Jyong da9870b5e1 fix(knowledge-fs): isolate reconciliation grants 2026-07-28 06:15:17 -04:00
Jyong 79a681b88b fix(knowledge-fs): persist greenfield audit anchor 2026-07-28 06:08:21 -04:00
Jyong 0ed37b82d3 fix(knowledge-fs): support empty workspace cutover 2026-07-28 05:53:02 -04:00
Stephen Zhou fbb1043f58 fix(dataset): align New RAG permission behavior 2026-07-28 16:24:54 +08:00
Stephen Zhou 65b9dc09a2 fix(dataset): stabilize New RAG creation and indexing 2026-07-28 15:49:52 +08:00
Jyong 6eed85af89 Merge branch 'main' into deploy/konwledge
# Conflicts:
#	api/tests/test_containers_integration_tests/services/test_workspace_service.py
#	api/tests/unit_tests/controllers/web/test_web_login.py
#	api/tests/unit_tests/libs/test_helper.py
2026-07-28 03:25:30 -04:00
Jyong 4df8c6662f chore: refresh KnowledgeFS contract lock 2026-07-28 02:35:21 -04:00
Jyong d89a92dbad ci: exclude KnowledgeFS API coverage gate 2026-07-28 02:01:51 -04:00
Stephen Zhou 4ebee54ffd fix(ci): restore KnowledgeFS branch coverage gate 2026-07-28 13:22:00 +08:00
Jyong 1b6b811108 ci: wait for KnowledgeFS before deployment 2026-07-28 01:01:32 -04:00
Stephen Zhou ab0b2282ab fix(dataset): complete New RAG connected source flows 2026-07-28 12:58:18 +08:00
Jyong f02de24470 ci: gate Knowledge deployment on KnowledgeFS 2026-07-28 00:47:39 -04:00
Stephen Zhou 665515da40 chore: refresh KnowledgeFS contract lock 2026-07-28 12:01:14 +08:00
Stephen Zhou 68fb92c8d5 fix(ci): align KnowledgeFS integration tests 2026-07-28 11:54:55 +08:00
Stephen Zhou 7a20d54847 fix(ci): format KnowledgeFS data facade 2026-07-28 11:47:05 +08:00
Stephen Zhou 53e12ff376 fix(ci): format KnowledgeFS deletion capability test 2026-07-28 11:40:57 +08:00
Stephen Zhou 055143f67a fix(ci): align KnowledgeFS deployment env assertion 2026-07-28 11:36:03 +08:00
Stephen Zhou b4a8f7f180 fix(dataset): align New RAG document views 2026-07-27 22:56:59 +08:00
Stephen Zhou e5d4f37469 fix(dataset): restore New RAG document uploads 2026-07-27 21:46:16 +08:00
Stephen Zhou 556fc7a03a fix(dataset): align New RAG sidebar icons 2026-07-27 18:34:23 +08:00
Stephen Zhou cc37dfcb01 fix(dataset): align New RAG controls with design system 2026-07-27 18:19:11 +08:00
Stephen Zhou 3de75a3315 fix(dataset): stabilize New RAG KnowledgeFS flows 2026-07-27 16:18:51 +08:00
Stephen Zhou 28c2b6ee95 Merge remote-tracking branch 'origin/main' into deploy/konwledge
# Conflicts:
#	api/controllers/console/auth/login.py
#	api/controllers/console/feature.py
#	api/controllers/web/site.py
#	api/openapi/markdown/console-openapi.md
#	api/openapi/markdown/web-openapi.md
#	api/services/feature_service.py
#	api/tests/test_containers_integration_tests/services/test_account_service.py
#	api/tests/test_containers_integration_tests/services/test_feature_service.py
#	api/tests/unit_tests/controllers/console/auth/test_email_verification.py
#	api/tests/unit_tests/controllers/console/auth/test_login_logout.py
#	api/tests/unit_tests/controllers/web/test_site.py
#	api/tests/unit_tests/services/test_recommended_app_service.py
2026-07-27 13:16:01 +08:00
Stephen ZhouandGitHub 1cde846bcf fix(dataset): integrate New RAG with KnowledgeFS (#39621) 2026-07-27 12:52:44 +08:00
Stephen Zhou ce0dee9afb Merge branch 'main' into deploy/konwledge
# Conflicts:
#	api/controllers/console/knowledge_fs_proxy.py
#	api/dev/generate_knowledge_fs_contract.py
#	api/extensions/storage/aws_s3_storage.py
#	api/knowledge-fs-contract.lock.json
#	api/services/knowledge_fs_proxy.py
#	api/tests/unit_tests/controllers/console/test_knowledge_fs_proxy.py
#	api/tests/unit_tests/dev/test_generate_knowledge_fs_contract.py
#	api/tests/unit_tests/extensions/storage/test_aws_s3_storage.py
#	api/tests/unit_tests/services/test_knowledge_fs_proxy.py
2026-07-24 23:43:42 +08:00
Jyong 11ae998de0 ci: gate dev deploy on all image builds 2026-07-24 03:13:40 -04:00
JyongandGitHub 901923457c Update deploy-dev.yml 2026-07-24 00:54:50 -04:00
JyongandGitHub 1b28bacb46 Update deploy-dev.yml 2026-07-24 00:14:28 -04:00
Jyong 918989b98a test: restore KnowledgeFS API branch coverage 2026-07-24 00:01:36 -04:00
Jyong 372930ce71 Update knowledge-fs lock for bulk operations migration 2026-07-23 23:45:17 -04:00
JyongandGitHub 652dccd2c4 Update deploy-dev.yml 2026-07-23 23:35:59 -04:00
Jyong 90b6a7f012 Expand workflow features and update related components 2026-07-23 22:53:41 -04:00
Jyong 7efe5a4783 chore: refresh KnowledgeFS contract lock 2026-07-23 03:00:24 -04:00
Jyong 0300a4d881 fix: support Dify-managed KnowledgeFS deployment 2026-07-23 02:51:05 -04:00
Jyong 85c8d026a2 chore: refresh KnowledgeFS contract lock 2026-07-23 02:33:24 -04:00
Jyong c09d61d8bc ci: cover KnowledgeFS inner API controllers 2026-07-23 02:25:45 -04:00
Jyong f482bf0aa1 Refactor codebase and remove obsolete implementations 2026-07-23 01:53:40 -04:00
Jyong 2eb328104a fix: type Dify Agent core tool boundaries 2026-07-22 07:32:29 -04:00
Jyong 91a240c072 ci: register KnowledgeFS cleanup migrations 2026-07-22 07:24:07 -04:00
Jyong dd70011fd9 test: align RBAC expectations after billing boundary fix 2026-07-22 07:12:02 -04:00
Jyong 954be8443c fix: unblock KnowledgeFS secure image publishing 2026-07-22 07:01:02 -04:00
Jyong cae5553f1a build: publish KnowledgeFS from deploy branch 2026-07-22 06:39:35 -04:00
Jyong fc7fabe544 Merge branch 'codex/migrate-knowledge-fs' into deploy/konwledge
# Conflicts:
#	api/tests/unit_tests/services/enterprise/test_rbac_service.py
#	api/tests/unit_tests/services/test_agent_tool_inner_service.py
2026-07-22 06:16:29 -04:00
Jyong d74499c09d update github action 2026-07-22 06:07:12 -04:00
Jyong 29858d0db0 Expand application capabilities and refactor core workflows 2026-07-22 05:09:09 -04:00
Jyong 77732c7bb1 docs: decouple KnowledgeFS from Dify datasets 2026-07-20 06:02:06 -04:00
Jyong dde79fc9bf docs: add Dify KnowledgeFS integration plan 2026-07-20 05:33:56 -04:00
Jyong 4ee43b8afc chore: migrate knowledge-fs source tree
Import the committed KnowledgeFS snapshot dc4072ee302317145612087ce7440851dc329fd0 under knowledge-fs/ without its Git history, local IDE settings, or build artifacts.
2026-07-20 04:54:20 -04:00
4244 changed files with 749556 additions and 57951 deletions
@@ -4,6 +4,7 @@ Read this document when a change introduces Effects, navigation side effects, me
## Effects
- Keep render pure: do not read or write `ref.current` during render except for predictable null-guarded lazy initialization. Update interaction-owned refs in event handlers, synchronize external-system refs after commit, and use state or derivation for rendered values.
- Use Effects only to synchronize with a named external system such as a browser API, subscription, timer, analytics integration, non-React widget, or imperative DOM API.
- Do not use Effects to transform render state, handle user actions, copy query data, reset state from props, or fetch data owned by framework APIs or TanStack Query.
- Initialize query-backed forms with keyed remounts or surface-entry hydration instead of copying data through Effects.
+15
View File
@@ -0,0 +1,15 @@
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "npx -y [email protected]"
}
]
}
]
}
}
+1
View File
@@ -8,6 +8,7 @@
**/*.pyc
**/.mypy_cache
**/.ruff_cache
knowledge-fs/
.git
.github
*.md
+4 -4
View File
@@ -1,17 +1,17 @@
title: "General Discussion"
title: 'General Discussion'
body:
- type: checkboxes
attributes:
label: Self Checks
description: "To make sure we get to you in time, please check the following :)"
description: 'To make sure we get to you in time, please check the following :)'
options:
- label: I have searched for existing issues [search for existing issues](https://github.com/langgenius/dify/issues), including closed ones.
required: true
- label: I confirm that I am using English to submit this report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)).
required: true
- label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:)"
- label: '[FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:)'
required: true
- label: "Please do not modify this template :) and fill in all the required fields."
- label: 'Please do not modify this template :) and fill in all the required fields.'
required: true
- type: textarea
attributes:
+4 -4
View File
@@ -1,17 +1,17 @@
title: "Help"
title: 'Help'
body:
- type: checkboxes
attributes:
label: Self Checks
description: "To make sure we get to you in time, please check the following :)"
description: 'To make sure we get to you in time, please check the following :)'
options:
- label: I have searched for existing issues [search for existing issues](https://github.com/langgenius/dify/issues), including closed ones.
required: true
- label: I confirm that I am using English to submit this report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)).
required: true
- label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:)"
- label: '[FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:)'
required: true
- label: "Please do not modify this template :) and fill in all the required fields."
- label: 'Please do not modify this template :) and fill in all the required fields.'
required: true
- type: textarea
attributes:
+3 -3
View File
@@ -3,15 +3,15 @@ body:
- type: checkboxes
attributes:
label: Self Checks
description: "To make sure we get to you in time, please check the following :)"
description: 'To make sure we get to you in time, please check the following :)'
options:
- label: I have searched for existing issues [search for existing issues](https://github.com/langgenius/dify/issues), including closed ones.
required: true
- label: I confirm that I am using English to submit this report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)).
required: true
- label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:)"
- label: '[FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:)'
required: true
- label: "Please do not modify this template :) and fill in all the required fields."
- label: 'Please do not modify this template :) and fill in all the required fields.'
required: true
- type: textarea
attributes:
+3 -3
View File
@@ -1,4 +1,4 @@
name: "🕷️ Bug report"
name: '🕷️ Bug report'
description: Report errors or unexpected behavior
labels:
- bug
@@ -6,7 +6,7 @@ body:
- type: checkboxes
attributes:
label: Self Checks
description: "To make sure we get to you in time, please check the following :)"
description: 'To make sure we get to you in time, please check the following :)'
options:
- label: I have read the [Contributing Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md) and [Language Policy](https://github.com/langgenius/dify/issues/1542).
required: true
@@ -18,7 +18,7 @@ body:
required: true
- label: 【中文用户 & Non English User】请使用英语提交,否则会被关闭 :)
required: true
- label: "Please do not modify this template :) and fill in all the required fields."
- label: 'Please do not modify this template :) and fill in all the required fields.'
required: true
- type: input
+3 -3
View File
@@ -1,13 +1,13 @@
blank_issues_enabled: false
contact_links:
- name: "\U0001F510 Security Vulnerabilities"
url: "https://github.com/langgenius/dify/security/advisories/new"
url: 'https://github.com/langgenius/dify/security/advisories/new'
about: Report security vulnerabilities through GitHub Security Advisories to ensure responsible disclosure. 💡 Please do not report security vulnerabilities in public issues.
- name: "\U0001F4A1 Model Providers & Plugins"
url: "https://github.com/langgenius/dify-official-plugins/issues/new/choose"
url: 'https://github.com/langgenius/dify-official-plugins/issues/new/choose'
about: Report issues with official plugins or model providers, you will need to provide the plugin version and other relevant details.
- name: "\U0001F4AC Documentation Issues"
url: "https://github.com/langgenius/dify-docs/issues/new"
url: 'https://github.com/langgenius/dify-docs/issues/new'
about: Report issues with the documentation, such as typos, outdated information, or missing content. Please provide the specific section and details of the issue.
- name: "\U0001F4E7 Discussions"
url: https://github.com/langgenius/dify/discussions/categories/general
+3 -3
View File
@@ -1,4 +1,4 @@
name: "⭐ Feature or enhancement request"
name: '⭐ Feature or enhancement request'
description: Propose something new.
labels:
- enhancement
@@ -6,7 +6,7 @@ body:
- type: checkboxes
attributes:
label: Self Checks
description: "To make sure we get to you in time, please check the following :)"
description: 'To make sure we get to you in time, please check the following :)'
options:
- label: I have read the [Contributing Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md) and [Language Policy](https://github.com/langgenius/dify/issues/1542).
required: true
@@ -14,7 +14,7 @@ body:
required: true
- label: I confirm that I am using English to submit this report, otherwise it will be closed.
required: true
- label: "Please do not modify this template :) and fill in all the required fields."
- label: 'Please do not modify this template :) and fill in all the required fields.'
required: true
- type: textarea
attributes:
+7 -7
View File
@@ -1,11 +1,11 @@
name: "✨ Refactor or Chore"
name: '✨ Refactor or Chore'
description: Refactor existing code or perform maintenance chores to improve readability and reliability.
title: "[Refactor/Chore] "
title: '[Refactor/Chore] '
body:
- type: checkboxes
attributes:
label: Self Checks
description: "To make sure we get to you in time, please check the following :)"
description: 'To make sure we get to you in time, please check the following :)'
options:
- label: I have read the [Contributing Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md) and [Language Policy](https://github.com/langgenius/dify/issues/1542).
required: true
@@ -17,26 +17,26 @@ body:
required: true
- label: 【中文用户 & Non English User】请使用英语提交,否则会被关闭 :)
required: true
- label: "Please do not modify this template :) and fill in all the required fields."
- label: 'Please do not modify this template :) and fill in all the required fields.'
required: true
- type: textarea
id: description
attributes:
label: Description
placeholder: "Describe the refactor or chore you are proposing."
placeholder: 'Describe the refactor or chore you are proposing.'
validations:
required: true
- type: textarea
id: motivation
attributes:
label: Motivation
placeholder: "Explain why this refactor or chore is necessary."
placeholder: 'Explain why this refactor or chore is necessary.'
validations:
required: false
- type: textarea
id: additional-context
attributes:
label: Additional Context
placeholder: "Add any other context or screenshots about the request here."
placeholder: 'Add any other context or screenshots about the request here.'
validations:
required: false
+2 -2
View File
@@ -5,11 +5,11 @@ runs:
using: composite
steps:
- name: Setup pnpm
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
with:
run_install: false
- name: Setup Vite+
uses: voidzero-dev/setup-vp@ca1c46663915d6c1042ae23bd39ab85718bfb0fa # v1.10.0
uses: voidzero-dev/setup-vp@143f5f385f39b1b753ffed1a01ad443811855c8b # v1.16.1
with:
node-version-file: .nvmrc
cache: true
+173 -164
View File
@@ -1,223 +1,232 @@
version: 2
updates:
- package-ecosystem: "uv"
directory: "/api"
- package-ecosystem: 'npm'
directory: '/knowledge-fs'
open-pull-requests-limit: 10
schedule:
interval: "weekly"
interval: 'weekly'
groups:
knowledge-fs-dependencies:
patterns:
- '*'
- package-ecosystem: 'uv'
directory: '/api'
open-pull-requests-limit: 10
schedule:
interval: 'weekly'
groups:
flask:
patterns:
- "flask"
- "flask-*"
- "werkzeug"
- "gunicorn"
- 'flask'
- 'flask-*'
- 'werkzeug'
- 'gunicorn'
google:
patterns:
- "google-*"
- "googleapis-*"
- 'google-*'
- 'googleapis-*'
opentelemetry:
patterns:
- "opentelemetry-*"
- 'opentelemetry-*'
pydantic:
patterns:
- "pydantic"
- "pydantic-*"
- 'pydantic'
- 'pydantic-*'
llm:
patterns:
- "langfuse"
- "langsmith"
- "litellm"
- "mlflow*"
- "opik"
- "weave*"
- "arize*"
- "tiktoken"
- "transformers"
- 'langfuse'
- 'langsmith'
- 'litellm'
- 'mlflow*'
- 'opik'
- 'weave*'
- 'arize*'
- 'tiktoken'
- 'transformers'
database:
patterns:
- "sqlalchemy"
- "psycopg2*"
- "psycogreen"
- "redis*"
- "alembic*"
- 'sqlalchemy'
- 'psycopg2*'
- 'psycogreen'
- 'redis*'
- 'alembic*'
storage:
patterns:
- "boto3*"
- "botocore*"
- "azure-*"
- "bce-*"
- "cos-python-*"
- "esdk-obs-*"
- "google-cloud-storage"
- "opendal"
- "oss2"
- "supabase*"
- "tos*"
- 'boto3*'
- 'botocore*'
- 'azure-*'
- 'bce-*'
- 'cos-python-*'
- 'esdk-obs-*'
- 'google-cloud-storage'
- 'opendal'
- 'oss2'
- 'supabase*'
- 'tos*'
vdb:
patterns:
- "alibabacloud*"
- "chromadb"
- "clickhouse-*"
- "clickzetta-*"
- "couchbase"
- "elasticsearch"
- "opensearch-py"
- "oracledb"
- "pgvect*"
- "pymilvus"
- "pymochow"
- "pyobvector"
- "qdrant-client"
- "intersystems-*"
- "tablestore"
- "tcvectordb"
- "tidb-vector"
- "upstash-*"
- "volcengine-*"
- "weaviate-*"
- "xinference-*"
- "mo-vector"
- "mysql-connector-*"
- 'alibabacloud*'
- 'chromadb'
- 'clickhouse-*'
- 'clickzetta-*'
- 'couchbase'
- 'elasticsearch'
- 'opensearch-py'
- 'oracledb'
- 'pgvect*'
- 'pymilvus'
- 'pymochow'
- 'pyobvector'
- 'qdrant-client'
- 'intersystems-*'
- 'tablestore'
- 'tcvectordb'
- 'tidb-vector'
- 'upstash-*'
- 'volcengine-*'
- 'weaviate-*'
- 'xinference-*'
- 'mo-vector'
- 'mysql-connector-*'
dev:
patterns:
- "coverage"
- "dotenv-linter"
- "faker"
- "lxml-stubs"
- "basedpyright"
- "ruff"
- "pytest*"
- "types-*"
- "boto3-stubs"
- "hypothesis"
- "pandas-stubs"
- "scipy-stubs"
- "import-linter"
- "celery-types"
- "mypy*"
- "pyrefly"
- 'coverage'
- 'dotenv-linter'
- 'faker'
- 'lxml-stubs'
- 'basedpyright'
- 'ruff'
- 'pytest*'
- 'types-*'
- 'boto3-stubs'
- 'hypothesis'
- 'pandas-stubs'
- 'scipy-stubs'
- 'import-linter'
- 'celery-types'
- 'mypy*'
- 'pyrefly'
python-packages:
patterns:
- "*"
- package-ecosystem: "github-actions"
directory: "/"
- '*'
- package-ecosystem: 'github-actions'
directory: '/'
open-pull-requests-limit: 5
schedule:
interval: "weekly"
interval: 'weekly'
groups:
github-actions-dependencies:
patterns:
- "*"
- package-ecosystem: "uv"
directory: "/api"
target-branch: "lts/1.13.x"
- '*'
- package-ecosystem: 'uv'
directory: '/api'
target-branch: 'lts/1.13.x'
open-pull-requests-limit: 10
schedule:
interval: "weekly"
interval: 'weekly'
groups:
flask:
patterns:
- "flask"
- "flask-*"
- "werkzeug"
- "gunicorn"
- 'flask'
- 'flask-*'
- 'werkzeug'
- 'gunicorn'
google:
patterns:
- "google-*"
- "googleapis-*"
- 'google-*'
- 'googleapis-*'
opentelemetry:
patterns:
- "opentelemetry-*"
- 'opentelemetry-*'
pydantic:
patterns:
- "pydantic"
- "pydantic-*"
- 'pydantic'
- 'pydantic-*'
llm:
patterns:
- "langfuse"
- "langsmith"
- "litellm"
- "mlflow*"
- "opik"
- "weave*"
- "arize*"
- "tiktoken"
- "transformers"
- 'langfuse'
- 'langsmith'
- 'litellm'
- 'mlflow*'
- 'opik'
- 'weave*'
- 'arize*'
- 'tiktoken'
- 'transformers'
database:
patterns:
- "sqlalchemy"
- "psycopg2*"
- "psycogreen"
- "redis*"
- "alembic*"
- 'sqlalchemy'
- 'psycopg2*'
- 'psycogreen'
- 'redis*'
- 'alembic*'
storage:
patterns:
- "boto3*"
- "botocore*"
- "azure-*"
- "bce-*"
- "cos-python-*"
- "esdk-obs-*"
- "google-cloud-storage"
- "opendal"
- "oss2"
- "supabase*"
- "tos*"
- 'boto3*'
- 'botocore*'
- 'azure-*'
- 'bce-*'
- 'cos-python-*'
- 'esdk-obs-*'
- 'google-cloud-storage'
- 'opendal'
- 'oss2'
- 'supabase*'
- 'tos*'
vdb:
patterns:
- "alibabacloud*"
- "chromadb"
- "clickhouse-*"
- "clickzetta-*"
- "couchbase"
- "elasticsearch"
- "opensearch-py"
- "oracledb"
- "pgvect*"
- "pymilvus"
- "pymochow"
- "pyobvector"
- "qdrant-client"
- "intersystems-*"
- "tablestore"
- "tcvectordb"
- "tidb-vector"
- "upstash-*"
- "volcengine-*"
- "weaviate-*"
- "xinference-*"
- "mo-vector"
- "mysql-connector-*"
- 'alibabacloud*'
- 'chromadb'
- 'clickhouse-*'
- 'clickzetta-*'
- 'couchbase'
- 'elasticsearch'
- 'opensearch-py'
- 'oracledb'
- 'pgvect*'
- 'pymilvus'
- 'pymochow'
- 'pyobvector'
- 'qdrant-client'
- 'intersystems-*'
- 'tablestore'
- 'tcvectordb'
- 'tidb-vector'
- 'upstash-*'
- 'volcengine-*'
- 'weaviate-*'
- 'xinference-*'
- 'mo-vector'
- 'mysql-connector-*'
dev:
patterns:
- "coverage"
- "dotenv-linter"
- "faker"
- "lxml-stubs"
- "basedpyright"
- "ruff"
- "pytest*"
- "types-*"
- "boto3-stubs"
- "hypothesis"
- "pandas-stubs"
- "scipy-stubs"
- "import-linter"
- "celery-types"
- "mypy*"
- "pyrefly"
- 'coverage'
- 'dotenv-linter'
- 'faker'
- 'lxml-stubs'
- 'basedpyright'
- 'ruff'
- 'pytest*'
- 'types-*'
- 'boto3-stubs'
- 'hypothesis'
- 'pandas-stubs'
- 'scipy-stubs'
- 'import-linter'
- 'celery-types'
- 'mypy*'
- 'pyrefly'
python-packages:
patterns:
- "*"
- package-ecosystem: "github-actions"
directory: "/"
target-branch: "lts/1.13.x"
- '*'
- package-ecosystem: 'github-actions'
directory: '/'
target-branch: 'lts/1.13.x'
open-pull-requests-limit: 5
schedule:
interval: "weekly"
interval: 'weekly'
groups:
github-actions-dependencies:
patterns:
- "*"
- '*'
+1 -1
View File
@@ -1 +1 @@
failure-threshold: "error"
failure-threshold: 'error'
-1
View File
@@ -1,5 +1,4 @@
---
extends: default
rules:
+20 -20
View File
@@ -1,22 +1,22 @@
{
"Verbose": false,
"Debug": false,
"IgnoreDefaults": false,
"SpacesAfterTabs": false,
"NoColor": false,
"Exclude": [
"^web/public/vs/",
"^web/public/pdf.worker.min.mjs$",
"web/app/components/base/icons/src/vender/"
],
"AllowedContentTypes": [],
"PassedFiles": [],
"Disable": {
"EndOfLine": false,
"Indentation": false,
"IndentSize": true,
"InsertFinalNewline": false,
"TrimTrailingWhitespace": false,
"MaxLineLength": false
}
"Verbose": false,
"Debug": false,
"IgnoreDefaults": false,
"SpacesAfterTabs": false,
"NoColor": false,
"Exclude": [
"^web/public/vs/",
"^web/public/pdf.worker.min.mjs$",
"web/app/components/base/icons/src/vender/"
],
"AllowedContentTypes": [],
"PassedFiles": [],
"Disable": {
"EndOfLine": false,
"Indentation": false,
"IndentSize": true,
"InsertFinalNewline": false,
"TrimTrailingWhitespace": false,
"MaxLineLength": false
}
}
+2 -2
View File
@@ -12,8 +12,8 @@
## Screenshots
| Before | After |
|--------|-------|
| ... | ... |
| ------ | ----- |
| ... | ... |
## Checklist
+11 -12
View File
@@ -8,31 +8,31 @@ const headSha = process.env.HEAD_SHA || ''
const files = (process.env.CHANGED_FILES || '').split(/\s+/).filter(Boolean)
const outputPath = process.env.I18N_CHANGES_OUTPUT_PATH || '/tmp/i18n-changes.json'
const englishPath = fileStem => path.join(repoRoot, 'web', 'i18n', 'en-US', `${fileStem}.json`)
const englishPath = (fileStem) => path.join(repoRoot, 'web', 'i18n', 'en-US', `${fileStem}.json`)
const readCurrentJson = (fileStem) => {
const filePath = englishPath(fileStem)
if (!fs.existsSync(filePath))
return null
if (!fs.existsSync(filePath)) return null
return JSON.parse(fs.readFileSync(filePath, 'utf8'))
}
const readBaseJson = (fileStem) => {
if (!baseSha)
return null
if (!baseSha) return null
try {
const relativePath = `web/i18n/en-US/${fileStem}.json`
const content = execFileSync('git', ['show', `${baseSha}:${relativePath}`], { encoding: 'utf8' })
const content = execFileSync('git', ['show', `${baseSha}:${relativePath}`], {
encoding: 'utf8',
})
return JSON.parse(content)
}
catch {
} catch {
return null
}
}
const compareJson = (beforeValue, afterValue) => JSON.stringify(beforeValue) === JSON.stringify(afterValue)
const compareJson = (beforeValue, afterValue) =>
JSON.stringify(beforeValue) === JSON.stringify(afterValue)
const changes = {}
@@ -59,8 +59,7 @@ for (const fileStem of files) {
}
for (const key of Object.keys(beforeJson)) {
if (!(key in afterJson))
deleted.push(key)
if (!(key in afterJson)) deleted.push(key)
}
changes[fileStem] = {
@@ -78,5 +77,5 @@ fs.writeFileSync(
headSha,
files,
changes,
})
}),
)
+101
View File
@@ -0,0 +1,101 @@
name: Manual WCAG Accessibility Audit
run-name: WCAG ${{ inputs.level }} · ${{ inputs.page }}
on:
workflow_dispatch:
inputs:
page:
description: Page to scan
required: true
default: home
type: choice
options:
- home
- sign-in
- studio
- agents
- knowledge
- integrations
- all
level:
description: WCAG level to scan
required: true
default: aa
type: choice
options:
- aa
- a
permissions:
contents: read
concurrency:
group: accessibility-e2e-${{ github.ref }}-${{ inputs.page }}-${{ inputs.level }}
cancel-in-progress: true
jobs:
accessibility:
name: WCAG Level ${{ inputs.level == 'a' && 'A' || 'AA' }} · ${{ inputs.page }}
runs-on: depot-ubuntu-24.04-4
timeout-minutes: 120
defaults:
run:
shell: bash
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup web dependencies
uses: ./.github/actions/setup-web
- name: Setup UV and Python
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
enable-cache: true
python-version: '3.12'
cache-dependency-glob: |
api/uv.lock
- name: Install API dependencies
run: uv sync --project api --dev
- name: Install Chromium for accessibility E2E
timeout-minutes: 15
working-directory: ./e2e
run: vp run e2e:install:ci:chromium
- name: Run automated WCAG Level ${{ inputs.level == 'a' && 'A' || 'AA' }} checks
working-directory: ./e2e
env:
E2E_ADMIN_EMAIL: [email protected]
E2E_ADMIN_NAME: E2E Admin
E2E_ADMIN_PASSWORD: E2eAdmin12345
E2E_FORCE_WEB_BUILD: '1'
E2E_INIT_PASSWORD: E2eInit12345
WCAG_PAGE: ${{ inputs.page }}
run: |
if [[ "$WCAG_PAGE" == "all" ]]; then
vp run e2e:accessibility:${{ inputs.level }}
else
pnpm exec tsx ./scripts/run-cucumber.ts --full -- --tags "@axe and @wcag-${{ inputs.level }} and @wcag-page-$WCAG_PAGE"
fi
- name: Upload accessibility Cucumber report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: cucumber-report-accessibility-${{ inputs.level }}-${{ inputs.page }}
path: e2e/cucumber-report
retention-days: 7
- name: Upload accessibility E2E logs
if: ${{ !cancelled() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: e2e-logs-accessibility-${{ inputs.level }}-${{ inputs.page }}
path: e2e/.logs/*.log
include-hidden-files: true
retention-days: 7
+3 -3
View File
@@ -25,7 +25,7 @@ jobs:
strategy:
matrix:
python-version:
- "3.12"
- '3.12'
steps:
- name: Checkout code
@@ -84,7 +84,7 @@ jobs:
strategy:
matrix:
python-version:
- "3.12"
- '3.12'
steps:
- name: Checkout code
@@ -148,7 +148,7 @@ jobs:
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
enable-cache: true
python-version: "3.12"
python-version: '3.12'
cache-dependency-glob: api/uv.lock
- name: Install dependencies
+5 -6
View File
@@ -1,12 +1,12 @@
name: autofix.ci
on:
pull_request:
branches: ["main"]
branches: ['main']
merge_group:
branches: ["main"]
branches: ['main']
types: [checks_requested]
push:
branches: ["main"]
branches: ['main']
permissions:
contents: read
@@ -53,8 +53,7 @@ jobs:
oxlint-suppressions.json
eslint-suppressions.json
.vscode/**
.github/workflows/autofix.yml
.github/workflows/style.yml
.github/**
- name: Check api inputs
if: github.event_name != 'merge_group'
id: api-changes
@@ -86,7 +85,7 @@ jobs:
- if: github.event_name != 'merge_group'
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.11"
python-version: '3.11'
- if: github.event_name != 'merge_group'
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
+70 -70
View File
@@ -3,13 +3,13 @@ name: Build and Push API & Web
on:
push:
branches:
- "main"
- "deploy/**"
- "build/**"
- "release/e-*"
- "hotfix/**"
- 'main'
- 'deploy/**'
- 'build/**'
- 'release/e-*'
- 'hotfix/**'
tags:
- "*"
- '*'
concurrency:
group: build-push-${{ github.head_ref || github.run_id }}
@@ -33,60 +33,60 @@ jobs:
strategy:
matrix:
include:
- service_name: "build-api-amd64"
image_name_env: "DIFY_API_IMAGE_NAME"
artifact_context: "api"
build_context: "{{defaultContext}}"
file: "api/Dockerfile"
- service_name: 'build-api-amd64'
image_name_env: 'DIFY_API_IMAGE_NAME'
artifact_context: 'api'
build_context: '{{defaultContext}}'
file: 'api/Dockerfile'
platform: linux/amd64
runs_on: depot-ubuntu-24.04-4
- service_name: "build-api-arm64"
image_name_env: "DIFY_API_IMAGE_NAME"
artifact_context: "api"
build_context: "{{defaultContext}}"
file: "api/Dockerfile"
- service_name: 'build-api-arm64'
image_name_env: 'DIFY_API_IMAGE_NAME'
artifact_context: 'api'
build_context: '{{defaultContext}}'
file: 'api/Dockerfile'
platform: linux/arm64
runs_on: depot-ubuntu-24.04-4
- service_name: "build-web-amd64"
image_name_env: "DIFY_WEB_IMAGE_NAME"
artifact_context: "web"
build_context: "{{defaultContext}}"
file: "web/Dockerfile"
- service_name: 'build-web-amd64'
image_name_env: 'DIFY_WEB_IMAGE_NAME'
artifact_context: 'web'
build_context: '{{defaultContext}}'
file: 'web/Dockerfile'
platform: linux/amd64
runs_on: depot-ubuntu-24.04-4
- service_name: "build-web-arm64"
image_name_env: "DIFY_WEB_IMAGE_NAME"
artifact_context: "web"
build_context: "{{defaultContext}}"
file: "web/Dockerfile"
- service_name: 'build-web-arm64'
image_name_env: 'DIFY_WEB_IMAGE_NAME'
artifact_context: 'web'
build_context: '{{defaultContext}}'
file: 'web/Dockerfile'
platform: linux/arm64
runs_on: depot-ubuntu-24.04-4
- service_name: "build-agent-amd64"
image_name_env: "DIFY_AGENT_IMAGE_NAME"
artifact_context: "agent"
build_context: "{{defaultContext}}"
file: "dify-agent/Dockerfile"
- service_name: 'build-agent-amd64'
image_name_env: 'DIFY_AGENT_IMAGE_NAME'
artifact_context: 'agent'
build_context: '{{defaultContext}}'
file: 'dify-agent/Dockerfile'
platform: linux/amd64
runs_on: depot-ubuntu-24.04-4
- service_name: "build-agent-arm64"
image_name_env: "DIFY_AGENT_IMAGE_NAME"
artifact_context: "agent"
build_context: "{{defaultContext}}"
file: "dify-agent/Dockerfile"
- service_name: 'build-agent-arm64'
image_name_env: 'DIFY_AGENT_IMAGE_NAME'
artifact_context: 'agent'
build_context: '{{defaultContext}}'
file: 'dify-agent/Dockerfile'
platform: linux/arm64
runs_on: depot-ubuntu-24.04-4
- service_name: "build-agent-local-sandbox-amd64"
image_name_env: "DIFY_AGENT_LOCAL_SANDBOX_IMAGE_NAME"
artifact_context: "local-sandbox"
build_context: "{{defaultContext}}:dify-agent-runtime"
file: "docker/Dockerfile"
- service_name: 'build-agent-local-sandbox-amd64'
image_name_env: 'DIFY_AGENT_LOCAL_SANDBOX_IMAGE_NAME'
artifact_context: 'local-sandbox'
build_context: '{{defaultContext}}:dify-agent-runtime'
file: 'docker/Dockerfile'
platform: linux/amd64
runs_on: depot-ubuntu-24.04-4
- service_name: "build-agent-local-sandbox-arm64"
image_name_env: "DIFY_AGENT_LOCAL_SANDBOX_IMAGE_NAME"
artifact_context: "local-sandbox"
build_context: "{{defaultContext}}:dify-agent-runtime"
file: "docker/Dockerfile"
- service_name: 'build-agent-local-sandbox-arm64'
image_name_env: 'DIFY_AGENT_LOCAL_SANDBOX_IMAGE_NAME'
artifact_context: 'local-sandbox'
build_context: '{{defaultContext}}:dify-agent-runtime'
file: 'docker/Dockerfile'
platform: linux/arm64
runs_on: depot-ubuntu-24.04-4
@@ -146,18 +146,18 @@ jobs:
strategy:
matrix:
include:
- service_name: "validate-api-amd64"
build_context: "{{defaultContext}}"
file: "api/Dockerfile"
- service_name: "validate-web-amd64"
build_context: "{{defaultContext}}"
file: "web/Dockerfile"
- service_name: "validate-agent-amd64"
build_context: "{{defaultContext}}"
file: "dify-agent/Dockerfile"
- service_name: "validate-agent-local-sandbox-amd64"
build_context: "{{defaultContext}}:dify-agent-runtime"
file: "docker/Dockerfile"
- service_name: 'validate-api-amd64'
build_context: '{{defaultContext}}'
file: 'api/Dockerfile'
- service_name: 'validate-web-amd64'
build_context: '{{defaultContext}}'
file: 'web/Dockerfile'
- service_name: 'validate-agent-amd64'
build_context: '{{defaultContext}}'
file: 'dify-agent/Dockerfile'
- service_name: 'validate-agent-local-sandbox-amd64'
build_context: '{{defaultContext}}:dify-agent-runtime'
file: 'docker/Dockerfile'
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
@@ -178,18 +178,18 @@ jobs:
strategy:
matrix:
include:
- service_name: "merge-api-images"
image_name_env: "DIFY_API_IMAGE_NAME"
context: "api"
- service_name: "merge-web-images"
image_name_env: "DIFY_WEB_IMAGE_NAME"
context: "web"
- service_name: "merge-agent-images"
image_name_env: "DIFY_AGENT_IMAGE_NAME"
context: "agent"
- service_name: "merge-agent-local-sandbox-images"
image_name_env: "DIFY_AGENT_LOCAL_SANDBOX_IMAGE_NAME"
context: "local-sandbox"
- service_name: 'merge-api-images'
image_name_env: 'DIFY_API_IMAGE_NAME'
context: 'api'
- service_name: 'merge-web-images'
image_name_env: 'DIFY_WEB_IMAGE_NAME'
context: 'web'
- service_name: 'merge-agent-images'
image_name_env: 'DIFY_AGENT_IMAGE_NAME'
context: 'agent'
- service_name: 'merge-agent-local-sandbox-images'
image_name_env: 'DIFY_AGENT_LOCAL_SANDBOX_IMAGE_NAME'
context: 'local-sandbox'
steps:
- name: Download digests
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+114 -115
View File
@@ -4,19 +4,19 @@ on:
workflow_dispatch:
inputs:
cli_ref:
description: "Git ref (default: current branch)"
description: 'Git ref (default: current branch)'
type: string
required: false
edition:
description: "Dify edition"
description: 'Dify edition'
type: choice
required: false
default: ee
options: [ee, ce]
test_scope:
description: "smoke = [P0] only / full = all cases"
description: 'smoke = [P0] only / full = all cases'
type: choice
required: false
default: full
@@ -24,23 +24,23 @@ on:
# ── Suite on/off ────────────────────────────────────────────────────────
suite_framework_output_error:
description: "framework + output + error-handling suites"
description: 'framework + output + error-handling suites'
type: boolean
default: true
suite_discovery:
description: "discovery suite (get app / describe app)"
description: 'discovery suite (get app / describe app)'
type: boolean
default: true
suite_run:
description: "run suite (basic / streaming / conversation / file / hitl)"
description: 'run suite (basic / streaming / conversation / file / hitl)'
type: boolean
default: true
suite_auth:
description: "auth suite (login / status / whoami / use / devices / logout)"
description: 'auth suite (login / status / whoami / use / devices / logout)'
type: boolean
default: true
suite_agent:
description: "agent suite"
description: 'agent suite'
type: boolean
default: true
@@ -51,32 +51,31 @@ permissions:
# Each job reads DIFY_E2E_TOKEN + app IDs from the provision job outputs,
# so global-setup skips minting and finds existing apps in < 10 s.
env:
DIFY_E2E_NO_KEYRING: "1" # Linux CI has no keychain; skip probe
VITEST_RETRY: "2" # Retry flaky staging responses
DIFY_E2E_NO_KEYRING: '1' # Linux CI has no keychain; skip probe
VITEST_RETRY: '2' # Retry flaky staging responses
jobs:
# ════════════════════════════════════════════════════════════════════════════
# 0. PROVISION — mint token + import DSL fixtures (runs once, outputs IDs)
# ════════════════════════════════════════════════════════════════════════════
# ════════════════════════════════════════════════════════════════════════════
# 0. PROVISION — mint token + import DSL fixtures (runs once, outputs IDs)
# ════════════════════════════════════════════════════════════════════════════
provision:
name: "Provision: mint token + DSL apps"
name: 'Provision: mint token + DSL apps'
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
token: ${{ steps.out.outputs.DIFY_E2E_TOKEN }}
workspace_id: ${{ steps.out.outputs.DIFY_E2E_WORKSPACE_ID }}
workspace_name: ${{ steps.out.outputs.DIFY_E2E_WORKSPACE_NAME }}
ws2_id: ${{ steps.out.outputs.DIFY_E2E_WS2_ID }}
chat_app_id: ${{ steps.out.outputs.DIFY_E2E_CHAT_APP_ID }}
workflow_app_id: ${{ steps.out.outputs.DIFY_E2E_WORKFLOW_APP_ID }}
file_app_id: ${{ steps.out.outputs.DIFY_E2E_FILE_APP_ID }}
file_chat_app_id: ${{ steps.out.outputs.DIFY_E2E_FILE_CHAT_APP_ID }}
hitl_app_id: ${{ steps.out.outputs.DIFY_E2E_HITL_APP_ID }}
hitl_external_app_id: ${{ steps.out.outputs.DIFY_E2E_HITL_EXTERNAL_APP_ID }}
hitl_single_action_app_id: ${{ steps.out.outputs.DIFY_E2E_HITL_SINGLE_ACTION_APP_ID }}
hitl_multi_node_app_id: ${{ steps.out.outputs.DIFY_E2E_HITL_MULTI_NODE_APP_ID }}
ws2_app_id: ${{ steps.out.outputs.DIFY_E2E_WS2_APP_ID }}
token: ${{ steps.out.outputs.DIFY_E2E_TOKEN }}
workspace_id: ${{ steps.out.outputs.DIFY_E2E_WORKSPACE_ID }}
workspace_name: ${{ steps.out.outputs.DIFY_E2E_WORKSPACE_NAME }}
ws2_id: ${{ steps.out.outputs.DIFY_E2E_WS2_ID }}
chat_app_id: ${{ steps.out.outputs.DIFY_E2E_CHAT_APP_ID }}
workflow_app_id: ${{ steps.out.outputs.DIFY_E2E_WORKFLOW_APP_ID }}
file_app_id: ${{ steps.out.outputs.DIFY_E2E_FILE_APP_ID }}
file_chat_app_id: ${{ steps.out.outputs.DIFY_E2E_FILE_CHAT_APP_ID }}
hitl_app_id: ${{ steps.out.outputs.DIFY_E2E_HITL_APP_ID }}
hitl_external_app_id: ${{ steps.out.outputs.DIFY_E2E_HITL_EXTERNAL_APP_ID }}
hitl_single_action_app_id: ${{ steps.out.outputs.DIFY_E2E_HITL_SINGLE_ACTION_APP_ID }}
hitl_multi_node_app_id: ${{ steps.out.outputs.DIFY_E2E_HITL_MULTI_NODE_APP_ID }}
ws2_app_id: ${{ steps.out.outputs.DIFY_E2E_WS2_APP_ID }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
@@ -88,7 +87,7 @@ jobs:
with:
bun-version: latest
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
with:
package_json_field: packageManager
run_install: false
@@ -101,18 +100,18 @@ jobs:
id: out
working-directory: cli
env:
DIFY_E2E_HOST: ${{ secrets.DIFY_E2E_HOST }}
DIFY_E2E_EMAIL: ${{ secrets.DIFY_E2E_EMAIL }}
DIFY_E2E_HOST: ${{ secrets.DIFY_E2E_HOST }}
DIFY_E2E_EMAIL: ${{ secrets.DIFY_E2E_EMAIL }}
DIFY_E2E_PASSWORD: ${{ secrets.DIFY_E2E_PASSWORD }}
DIFY_E2E_TOKEN: ${{ secrets.DIFY_E2E_TOKEN }}
DIFY_E2E_EDITION: ${{ inputs.edition || 'ee' }}
DIFY_E2E_TOKEN: ${{ secrets.DIFY_E2E_TOKEN }}
DIFY_E2E_EDITION: ${{ inputs.edition || 'ee' }}
run: bun scripts/e2e-provision.ts
# ════════════════════════════════════════════════════════════════════════════
# 1-B. framework + output + error-handling (parallel with run/discovery)
# ════════════════════════════════════════════════════════════════════════════
# ════════════════════════════════════════════════════════════════════════════
# 1-B. framework + output + error-handling (parallel with run/discovery)
# ════════════════════════════════════════════════════════════════════════════
suite-framework-output-error:
name: "Suite: framework + output + error-handling"
name: 'Suite: framework + output + error-handling'
if: ${{ inputs.suite_framework_output_error != 'false' }}
needs: provision
runs-on: ubuntu-latest
@@ -131,23 +130,23 @@ jobs:
- uses: ./.github/actions/setup-web
- uses: oven-sh/setup-bun@v2
with: { bun-version: latest }
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
with: { package_json_field: packageManager, run_install: false }
- run: pnpm install --frozen-lockfile
- run: pnpm tree:gen
- name: Run framework + output + error-handling
env:
DIFY_E2E_HOST: ${{ secrets.DIFY_E2E_HOST }}
DIFY_E2E_EMAIL: ${{ secrets.DIFY_E2E_EMAIL }}
DIFY_E2E_PASSWORD: ${{ secrets.DIFY_E2E_PASSWORD }}
DIFY_E2E_EDITION: ${{ inputs.edition || 'ee' }}
DIFY_E2E_TOKEN: ${{ needs.provision.outputs.token }}
DIFY_E2E_WORKSPACE_ID: ${{ needs.provision.outputs.workspace_id }}
DIFY_E2E_HOST: ${{ secrets.DIFY_E2E_HOST }}
DIFY_E2E_EMAIL: ${{ secrets.DIFY_E2E_EMAIL }}
DIFY_E2E_PASSWORD: ${{ secrets.DIFY_E2E_PASSWORD }}
DIFY_E2E_EDITION: ${{ inputs.edition || 'ee' }}
DIFY_E2E_TOKEN: ${{ needs.provision.outputs.token }}
DIFY_E2E_WORKSPACE_ID: ${{ needs.provision.outputs.workspace_id }}
DIFY_E2E_WORKSPACE_NAME: ${{ needs.provision.outputs.workspace_name }}
DIFY_E2E_CHAT_APP_ID: ${{ needs.provision.outputs.chat_app_id }}
DIFY_E2E_CHAT_APP_ID: ${{ needs.provision.outputs.chat_app_id }}
DIFY_E2E_WORKFLOW_APP_ID: ${{ needs.provision.outputs.workflow_app_id }}
DIFY_E2E_INCLUDE: "test/e2e/suites/framework/**/*.e2e.ts,test/e2e/suites/output/**/*.e2e.ts,test/e2e/suites/error-handling/**/*.e2e.ts"
DIFY_E2E_INCLUDE: 'test/e2e/suites/framework/**/*.e2e.ts,test/e2e/suites/output/**/*.e2e.ts,test/e2e/suites/error-handling/**/*.e2e.ts'
run: |
if [ "${{ inputs.test_scope }}" = "smoke" ]; then
pnpm test:e2e -- -t "\[P0\]"
@@ -155,11 +154,11 @@ jobs:
pnpm test:e2e
fi
# ════════════════════════════════════════════════════════════════════════════
# 1-C. Discovery (parallel)
# ════════════════════════════════════════════════════════════════════════════
# ════════════════════════════════════════════════════════════════════════════
# 1-C. Discovery (parallel)
# ════════════════════════════════════════════════════════════════════════════
suite-discovery:
name: "Suite: discovery"
name: 'Suite: discovery'
if: ${{ inputs.suite_discovery != 'false' }}
needs: provision
runs-on: ubuntu-latest
@@ -178,24 +177,24 @@ jobs:
- uses: ./.github/actions/setup-web
- uses: oven-sh/setup-bun@v2
with: { bun-version: latest }
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
with: { package_json_field: packageManager, run_install: false }
- run: pnpm install --frozen-lockfile
- run: pnpm tree:gen
- name: Run discovery suite
env:
DIFY_E2E_HOST: ${{ secrets.DIFY_E2E_HOST }}
DIFY_E2E_EMAIL: ${{ secrets.DIFY_E2E_EMAIL }}
DIFY_E2E_PASSWORD: ${{ secrets.DIFY_E2E_PASSWORD }}
DIFY_E2E_EDITION: ${{ inputs.edition || 'ee' }}
DIFY_E2E_TOKEN: ${{ needs.provision.outputs.token }}
DIFY_E2E_WORKSPACE_ID: ${{ needs.provision.outputs.workspace_id }}
DIFY_E2E_HOST: ${{ secrets.DIFY_E2E_HOST }}
DIFY_E2E_EMAIL: ${{ secrets.DIFY_E2E_EMAIL }}
DIFY_E2E_PASSWORD: ${{ secrets.DIFY_E2E_PASSWORD }}
DIFY_E2E_EDITION: ${{ inputs.edition || 'ee' }}
DIFY_E2E_TOKEN: ${{ needs.provision.outputs.token }}
DIFY_E2E_WORKSPACE_ID: ${{ needs.provision.outputs.workspace_id }}
DIFY_E2E_WORKSPACE_NAME: ${{ needs.provision.outputs.workspace_name }}
DIFY_E2E_WS2_ID: ${{ needs.provision.outputs.ws2_id }}
DIFY_E2E_CHAT_APP_ID: ${{ needs.provision.outputs.chat_app_id }}
DIFY_E2E_WS2_ID: ${{ needs.provision.outputs.ws2_id }}
DIFY_E2E_CHAT_APP_ID: ${{ needs.provision.outputs.chat_app_id }}
DIFY_E2E_WORKFLOW_APP_ID: ${{ needs.provision.outputs.workflow_app_id }}
DIFY_E2E_INCLUDE: "test/e2e/suites/discovery/**/*.e2e.ts"
DIFY_E2E_INCLUDE: 'test/e2e/suites/discovery/**/*.e2e.ts'
run: |
if [ "${{ inputs.test_scope }}" = "smoke" ]; then
pnpm test:e2e -- -t "\[P0\]"
@@ -203,11 +202,11 @@ jobs:
pnpm test:e2e
fi
# ════════════════════════════════════════════════════════════════════════════
# 1-D. Run suite — 5 files in matrix (parallel)
# ════════════════════════════════════════════════════════════════════════════
# ════════════════════════════════════════════════════════════════════════════
# 1-D. Run suite — 5 files in matrix (parallel)
# ════════════════════════════════════════════════════════════════════════════
suite-run:
name: "Suite: run / ${{ matrix.name }}"
name: 'Suite: run / ${{ matrix.name }}'
if: ${{ inputs.suite_run != 'false' }}
needs: provision
runs-on: ubuntu-latest
@@ -241,30 +240,30 @@ jobs:
- uses: ./.github/actions/setup-web
- uses: oven-sh/setup-bun@v2
with: { bun-version: latest }
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
with: { package_json_field: packageManager, run_install: false }
- run: pnpm install --frozen-lockfile
- run: pnpm tree:gen
- name: "Run run/${{ matrix.name }}"
- name: 'Run run/${{ matrix.name }}'
env:
DIFY_E2E_HOST: ${{ secrets.DIFY_E2E_HOST }}
DIFY_E2E_EMAIL: ${{ secrets.DIFY_E2E_EMAIL }}
DIFY_E2E_PASSWORD: ${{ secrets.DIFY_E2E_PASSWORD }}
DIFY_E2E_EDITION: ${{ inputs.edition || 'ee' }}
DIFY_E2E_SSO_TOKEN: ${{ secrets.DIFY_E2E_SSO_TOKEN }}
DIFY_E2E_TOKEN: ${{ needs.provision.outputs.token }}
DIFY_E2E_WORKSPACE_ID: ${{ needs.provision.outputs.workspace_id }}
DIFY_E2E_WORKSPACE_NAME: ${{ needs.provision.outputs.workspace_name }}
DIFY_E2E_CHAT_APP_ID: ${{ needs.provision.outputs.chat_app_id }}
DIFY_E2E_WORKFLOW_APP_ID: ${{ needs.provision.outputs.workflow_app_id }}
DIFY_E2E_FILE_APP_ID: ${{ needs.provision.outputs.file_app_id }}
DIFY_E2E_FILE_CHAT_APP_ID: ${{ needs.provision.outputs.file_chat_app_id }}
DIFY_E2E_HITL_APP_ID: ${{ needs.provision.outputs.hitl_app_id }}
DIFY_E2E_HITL_EXTERNAL_APP_ID: ${{ needs.provision.outputs.hitl_external_app_id }}
DIFY_E2E_HOST: ${{ secrets.DIFY_E2E_HOST }}
DIFY_E2E_EMAIL: ${{ secrets.DIFY_E2E_EMAIL }}
DIFY_E2E_PASSWORD: ${{ secrets.DIFY_E2E_PASSWORD }}
DIFY_E2E_EDITION: ${{ inputs.edition || 'ee' }}
DIFY_E2E_SSO_TOKEN: ${{ secrets.DIFY_E2E_SSO_TOKEN }}
DIFY_E2E_TOKEN: ${{ needs.provision.outputs.token }}
DIFY_E2E_WORKSPACE_ID: ${{ needs.provision.outputs.workspace_id }}
DIFY_E2E_WORKSPACE_NAME: ${{ needs.provision.outputs.workspace_name }}
DIFY_E2E_CHAT_APP_ID: ${{ needs.provision.outputs.chat_app_id }}
DIFY_E2E_WORKFLOW_APP_ID: ${{ needs.provision.outputs.workflow_app_id }}
DIFY_E2E_FILE_APP_ID: ${{ needs.provision.outputs.file_app_id }}
DIFY_E2E_FILE_CHAT_APP_ID: ${{ needs.provision.outputs.file_chat_app_id }}
DIFY_E2E_HITL_APP_ID: ${{ needs.provision.outputs.hitl_app_id }}
DIFY_E2E_HITL_EXTERNAL_APP_ID: ${{ needs.provision.outputs.hitl_external_app_id }}
DIFY_E2E_HITL_SINGLE_ACTION_APP_ID: ${{ needs.provision.outputs.hitl_single_action_app_id }}
DIFY_E2E_HITL_MULTI_NODE_APP_ID: ${{ needs.provision.outputs.hitl_multi_node_app_id }}
DIFY_E2E_INCLUDE: "test/e2e/suites/run/${{ matrix.file }}"
DIFY_E2E_HITL_MULTI_NODE_APP_ID: ${{ needs.provision.outputs.hitl_multi_node_app_id }}
DIFY_E2E_INCLUDE: 'test/e2e/suites/run/${{ matrix.file }}'
run: |
if [ "${{ inputs.test_scope }}" = "smoke" ]; then
pnpm test:e2e -- -t "\[P0\]"
@@ -280,11 +279,11 @@ jobs:
path: cli/test-results/
retention-days: 3
# ════════════════════════════════════════════════════════════════════════════
# 1-E. auth/login + status + whoami (parallel, read-only, safe)
# ════════════════════════════════════════════════════════════════════════════
# ════════════════════════════════════════════════════════════════════════════
# 1-E. auth/login + status + whoami (parallel, read-only, safe)
# ════════════════════════════════════════════════════════════════════════════
suite-auth-safe:
name: "Suite: auth (login / status / whoami)"
name: 'Suite: auth (login / status / whoami)'
if: ${{ inputs.suite_auth != 'false' }}
needs: provision
runs-on: ubuntu-latest
@@ -303,22 +302,22 @@ jobs:
- uses: ./.github/actions/setup-web
- uses: oven-sh/setup-bun@v2
with: { bun-version: latest }
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
with: { package_json_field: packageManager, run_install: false }
- run: pnpm install --frozen-lockfile
- run: pnpm tree:gen
- name: Run auth/login + status + whoami
env:
DIFY_E2E_HOST: ${{ secrets.DIFY_E2E_HOST }}
DIFY_E2E_EMAIL: ${{ secrets.DIFY_E2E_EMAIL }}
DIFY_E2E_PASSWORD: ${{ secrets.DIFY_E2E_PASSWORD }}
DIFY_E2E_EDITION: ${{ inputs.edition || 'ee' }}
DIFY_E2E_TOKEN: ${{ needs.provision.outputs.token }}
DIFY_E2E_WORKSPACE_ID: ${{ needs.provision.outputs.workspace_id }}
DIFY_E2E_HOST: ${{ secrets.DIFY_E2E_HOST }}
DIFY_E2E_EMAIL: ${{ secrets.DIFY_E2E_EMAIL }}
DIFY_E2E_PASSWORD: ${{ secrets.DIFY_E2E_PASSWORD }}
DIFY_E2E_EDITION: ${{ inputs.edition || 'ee' }}
DIFY_E2E_TOKEN: ${{ needs.provision.outputs.token }}
DIFY_E2E_WORKSPACE_ID: ${{ needs.provision.outputs.workspace_id }}
DIFY_E2E_WORKSPACE_NAME: ${{ needs.provision.outputs.workspace_name }}
DIFY_E2E_WS2_ID: ${{ needs.provision.outputs.ws2_id }}
DIFY_E2E_INCLUDE: "test/e2e/suites/auth/login.e2e.ts,test/e2e/suites/auth/status.e2e.ts,test/e2e/suites/auth/whoami.e2e.ts"
DIFY_E2E_WS2_ID: ${{ needs.provision.outputs.ws2_id }}
DIFY_E2E_INCLUDE: 'test/e2e/suites/auth/login.e2e.ts,test/e2e/suites/auth/status.e2e.ts,test/e2e/suites/auth/whoami.e2e.ts'
run: |
if [ "${{ inputs.test_scope }}" = "smoke" ]; then
pnpm test:e2e -- -t "\[P0\]"
@@ -326,13 +325,13 @@ jobs:
pnpm test:e2e
fi
# ════════════════════════════════════════════════════════════════════════════
# 2. DESTRUCTIVE — auth/use + devices + logout + agent (serial, runs LAST)
# Must wait for ALL parallel suites to finish to avoid token revocation
# invalidating other in-flight requests.
# ════════════════════════════════════════════════════════════════════════════
# ════════════════════════════════════════════════════════════════════════════
# 2. DESTRUCTIVE — auth/use + devices + logout + agent (serial, runs LAST)
# Must wait for ALL parallel suites to finish to avoid token revocation
# invalidating other in-flight requests.
# ════════════════════════════════════════════════════════════════════════════
suite-last:
name: "Suite: auth-use + devices + logout + agent (last, serial)"
name: 'Suite: auth-use + devices + logout + agent (last, serial)'
# Runs when auth is selected; also runs after all parallel jobs finish
if: ${{ inputs.suite_auth != 'false' || inputs.suite_agent != 'false' }}
needs:
@@ -359,27 +358,27 @@ jobs:
- uses: ./.github/actions/setup-web
- uses: oven-sh/setup-bun@v2
with: { bun-version: latest }
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
with: { package_json_field: packageManager, run_install: false }
- run: pnpm install --frozen-lockfile
- run: pnpm tree:gen
- name: Run use / devices / logout / agent (serial)
env:
DIFY_E2E_HOST: ${{ secrets.DIFY_E2E_HOST }}
DIFY_E2E_EMAIL: ${{ secrets.DIFY_E2E_EMAIL }}
DIFY_E2E_PASSWORD: ${{ secrets.DIFY_E2E_PASSWORD }}
DIFY_E2E_EDITION: ${{ inputs.edition || 'ee' }}
DIFY_E2E_TOKEN: ${{ needs.provision.outputs.token }}
DIFY_E2E_WORKSPACE_ID: ${{ needs.provision.outputs.workspace_id }}
DIFY_E2E_WORKSPACE_NAME: ${{ needs.provision.outputs.workspace_name }}
DIFY_E2E_WS2_ID: ${{ needs.provision.outputs.ws2_id }}
DIFY_E2E_CHAT_APP_ID: ${{ needs.provision.outputs.chat_app_id }}
DIFY_E2E_WORKFLOW_APP_ID: ${{ needs.provision.outputs.workflow_app_id }}
DIFY_E2E_HITL_APP_ID: ${{ needs.provision.outputs.hitl_app_id }}
DIFY_E2E_HITL_EXTERNAL_APP_ID: ${{ needs.provision.outputs.hitl_external_app_id }}
DIFY_E2E_HOST: ${{ secrets.DIFY_E2E_HOST }}
DIFY_E2E_EMAIL: ${{ secrets.DIFY_E2E_EMAIL }}
DIFY_E2E_PASSWORD: ${{ secrets.DIFY_E2E_PASSWORD }}
DIFY_E2E_EDITION: ${{ inputs.edition || 'ee' }}
DIFY_E2E_TOKEN: ${{ needs.provision.outputs.token }}
DIFY_E2E_WORKSPACE_ID: ${{ needs.provision.outputs.workspace_id }}
DIFY_E2E_WORKSPACE_NAME: ${{ needs.provision.outputs.workspace_name }}
DIFY_E2E_WS2_ID: ${{ needs.provision.outputs.ws2_id }}
DIFY_E2E_CHAT_APP_ID: ${{ needs.provision.outputs.chat_app_id }}
DIFY_E2E_WORKFLOW_APP_ID: ${{ needs.provision.outputs.workflow_app_id }}
DIFY_E2E_HITL_APP_ID: ${{ needs.provision.outputs.hitl_app_id }}
DIFY_E2E_HITL_EXTERNAL_APP_ID: ${{ needs.provision.outputs.hitl_external_app_id }}
DIFY_E2E_HITL_SINGLE_ACTION_APP_ID: ${{ needs.provision.outputs.hitl_single_action_app_id }}
DIFY_E2E_HITL_MULTI_NODE_APP_ID: ${{ needs.provision.outputs.hitl_multi_node_app_id }}
DIFY_E2E_HITL_MULTI_NODE_APP_ID: ${{ needs.provision.outputs.hitl_multi_node_app_id }}
run: |
# Collect files in safe order: use → devices → logout (revokes last) → agent
FILES=()
+3 -2
View File
@@ -2,10 +2,11 @@ name: CLI Edge Publish
on:
push:
branches: [main]
branches: [main, deploy/konwledge]
paths:
- 'cli/**'
- 'packages/contracts/generated/api/openapi/**'
- '.github/workflows/cli-edge.yml'
workflow_dispatch:
concurrency:
@@ -36,7 +37,7 @@ jobs:
uses: ./.github/actions/setup-web
- name: Setup Bun
uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.2
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version-file: cli/.bun-version
+1 -1
View File
@@ -114,7 +114,7 @@ jobs:
run: node scripts/release-naming.mjs github-env >> "$GITHUB_ENV"
- name: Setup Bun
uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.2
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version-file: cli/.bun-version
+2 -2
View File
@@ -4,11 +4,11 @@ on:
workflow_dispatch:
inputs:
dify_version:
description: "Dify image tag to test against (e.g. 1.7.0)"
description: 'Dify image tag to test against (e.g. 1.7.0)'
type: string
required: true
cli_ref:
description: "Git ref to build the cli from (default: current branch)"
description: 'Git ref to build the cli from (default: current branch)'
type: string
required: false
+4 -4
View File
@@ -22,7 +22,7 @@ jobs:
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
enable-cache: true
python-version: "3.12"
python-version: '3.12'
cache-dependency-glob: api/uv.lock
- name: Install dependencies
@@ -40,7 +40,7 @@ jobs:
cp envs/middleware.env.example middleware.env
- name: Set up Middlewares
uses: hoverkraft-tech/compose-action@11beaa1c2dae4e8ed7b1665aa074723b6cecb0e4 # v3.0.0
uses: hoverkraft-tech/compose-action@ee6af68587292d72db67743171809c19787df4c9 # v3.1.0
with:
compose-file: |
docker/docker-compose.middleware.yaml
@@ -72,7 +72,7 @@ jobs:
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
enable-cache: true
python-version: "3.12"
python-version: '3.12'
cache-dependency-glob: api/uv.lock
- name: Install dependencies
@@ -94,7 +94,7 @@ jobs:
sed -i 's/DB_USERNAME=postgres/DB_USERNAME=mysql/' middleware.env
- name: Set up Middlewares
uses: hoverkraft-tech/compose-action@11beaa1c2dae4e8ed7b1665aa074723b6cecb0e4 # v3.0.0
uses: hoverkraft-tech/compose-action@ee6af68587292d72db67743171809c19787df4c9 # v3.1.0
with:
compose-file: |
docker/docker-compose.middleware.yaml
+2 -2
View File
@@ -5,9 +5,9 @@ permissions:
on:
workflow_run:
workflows: ["Build and Push API & Web"]
workflows: ['Build and Push API & Web']
branches:
- "deploy/agent"
- 'deploy/agent'
types:
- completed
+51 -4
View File
@@ -2,23 +2,70 @@ name: Deploy Dev
on:
workflow_run:
workflows: ["Build and Push API & Web"]
workflows: ['KnowledgeFS CI']
branches:
- "deploy/dev"
- 'deploy/konwledge'
types:
- completed
permissions: {}
jobs:
deploy:
runs-on: depot-ubuntu-24.04
permissions:
actions: read
contents: read
if: |
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.head_branch == 'deploy/dev'
github.event.workflow_run.head_branch == 'deploy/konwledge'
steps:
- name: Wait for API and Web image build
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
timeout-minutes: 35
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const workflowId = "build-push.yml";
const headBranch = context.payload.workflow_run.head_branch;
const headSha = context.payload.workflow_run.head_sha;
const deadline = Date.now() + 30 * 60 * 1000;
const pollIntervalMs = 15 * 1000;
while (Date.now() < deadline) {
const { data } = await github.rest.actions.listWorkflowRuns({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: workflowId,
branch: headBranch,
event: "push",
head_sha: headSha,
per_page: 10,
});
const run = data.workflow_runs[0];
if (!run) {
core.info(`Waiting for ${workflowId} to start for ${headSha}.`);
} else if (run.status !== "completed") {
core.info(`Waiting for ${run.html_url}; current status is ${run.status}.`);
} else if (run.conclusion !== "success") {
throw new Error(
`${workflowId} did not succeed for ${headSha}: ${run.conclusion} (${run.html_url})`,
);
} else {
core.info(`Both image workflows succeeded for ${headSha}: ${run.html_url}`);
return;
}
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
}
throw new Error(`Timed out waiting for ${workflowId} to succeed for ${headSha}.`);
- name: Deploy to server
uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1.2.5
with:
host: ${{ secrets.SSH_HOST }}
host: ${{ secrets.SSH_NEW_RAG_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
+2 -2
View File
@@ -5,9 +5,9 @@ permissions:
on:
workflow_run:
workflows: ["Build and Push API & Web"]
workflows: ['Build and Push API & Web']
branches:
- "deploy/enterprise"
- 'deploy/enterprise'
types:
- completed
+3 -3
View File
@@ -6,9 +6,9 @@ permissions:
on:
workflow_run:
workflows: ["Build and Push API & Web"]
workflows: ['Build and Push API & Web']
branches:
- "deploy/konwledge"
- 'deploy/konwledge'
types:
- completed
@@ -68,4 +68,4 @@ jobs:
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
${{ vars.SSH_SCRIPT || secrets.SSH_SCRIPT }}
${{ vars.SSH_KNOWLEDGE_SCRIPT || secrets.SSH_KNOWLEDGE_SCRIPT }}
+2 -2
View File
@@ -5,9 +5,9 @@ permissions:
on:
workflow_run:
workflows: ["Build and Push API & Web"]
workflows: ['Build and Push API & Web']
branches:
- "deploy/saas"
- 'deploy/saas'
types:
- completed
+28 -28
View File
@@ -3,7 +3,7 @@ name: Build docker image
on:
pull_request:
branches:
- "main"
- 'main'
paths:
- api/Dockerfile
- api/Dockerfile.dockerignore
@@ -33,36 +33,36 @@ jobs:
strategy:
matrix:
include:
- service_name: "api-amd64"
- service_name: 'api-amd64'
platform: linux/amd64
runs_on: depot-ubuntu-24.04-4
context: "{{defaultContext}}"
file: "api/Dockerfile"
- service_name: "api-arm64"
context: '{{defaultContext}}'
file: 'api/Dockerfile'
- service_name: 'api-arm64'
platform: linux/arm64
runs_on: depot-ubuntu-24.04-4
context: "{{defaultContext}}"
file: "api/Dockerfile"
- service_name: "web-amd64"
context: '{{defaultContext}}'
file: 'api/Dockerfile'
- service_name: 'web-amd64'
platform: linux/amd64
runs_on: depot-ubuntu-24.04-4
context: "{{defaultContext}}"
file: "web/Dockerfile"
- service_name: "web-arm64"
context: '{{defaultContext}}'
file: 'web/Dockerfile'
- service_name: 'web-arm64'
platform: linux/arm64
runs_on: depot-ubuntu-24.04-4
context: "{{defaultContext}}"
file: "web/Dockerfile"
- service_name: "local-sandbox-amd64"
context: '{{defaultContext}}'
file: 'web/Dockerfile'
- service_name: 'local-sandbox-amd64'
platform: linux/amd64
runs_on: depot-ubuntu-24.04-4
context: "{{defaultContext}}:dify-agent-runtime"
file: "docker/Dockerfile"
- service_name: "local-sandbox-arm64"
context: '{{defaultContext}}:dify-agent-runtime'
file: 'docker/Dockerfile'
- service_name: 'local-sandbox-arm64'
platform: linux/arm64
runs_on: depot-ubuntu-24.04-4
context: "{{defaultContext}}:dify-agent-runtime"
file: "docker/Dockerfile"
context: '{{defaultContext}}:dify-agent-runtime'
file: 'docker/Dockerfile'
steps:
- name: Set up Depot CLI
uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # v1.7.1
@@ -85,15 +85,15 @@ jobs:
strategy:
matrix:
include:
- service_name: "api-amd64"
context: "{{defaultContext}}"
file: "api/Dockerfile"
- service_name: "web-amd64"
context: "{{defaultContext}}"
file: "web/Dockerfile"
- service_name: "local-sandbox-amd64"
context: "{{defaultContext}}:dify-agent-runtime"
file: "docker/Dockerfile"
- service_name: 'api-amd64'
context: '{{defaultContext}}'
file: 'api/Dockerfile'
- service_name: 'web-amd64'
context: '{{defaultContext}}'
file: 'web/Dockerfile'
- service_name: 'local-sandbox-amd64'
context: '{{defaultContext}}:dify-agent-runtime'
file: 'docker/Dockerfile'
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
+514
View File
@@ -0,0 +1,514 @@
name: KnowledgeFS CI
on:
pull_request:
branches: ['main']
merge_group:
branches: ['main']
types: [checks_requested]
push:
branches: ['main', 'deploy/konwledge']
workflow_dispatch:
permissions:
contents: read
pull-requests: read
concurrency:
group: knowledge-fs-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
CI: true
DIFY_KNOWLEDGE_FS_API_IMAGE_NAME: >-
${{ vars.DIFY_KNOWLEDGE_FS_API_IMAGE_NAME || 'langgenius/dify-knowledge-fs-api' }}
jobs:
check-changes:
name: Check KnowledgeFS changes
runs-on: depot-ubuntu-24.04-4
outputs:
knowledge-fs: ${{ steps.changes.outputs.knowledge-fs }}
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
persist-credentials: false
- name: Detect KnowledgeFS changes
id: changes
uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
with:
filters: |
knowledge-fs:
- 'knowledge-fs/**'
- 'knowledge-fs/packages/api/src/dify-capability-v2.ts'
- 'knowledge-fs/packages/api/src/knowledge-space-routes.ts'
- 'knowledge-fs/packages/api/src/upload-session-routes.ts'
- 'knowledge-fs/scripts/export-capability-v2-operations.mjs'
- 'knowledge-fs/scripts/export-openapi.mjs'
- 'api/dev/generate_knowledge_fs_contract.py'
- 'api/dev/knowledge_fs_product_contract.py'
- 'api/knowledge-fs-contract.lock.json'
- 'api/knowledge-fs-product-operation-gaps.json'
- 'api/knowledge-fs-product-operations.json'
- 'api/**/knowledge_fs/**'
- 'api/**/*knowledge_fs*'
- 'api/**/*knowledge-fs*'
- 'api/.env.example'
- 'api/app_factory.py'
- 'api/commands/__init__.py'
- 'api/controllers/console/__init__.py'
- 'api/controllers/console/workspace/rbac.py'
- 'api/controllers/service_api/__init__.py'
- 'api/core/agent/base_agent_runner.py'
- 'api/core/app/apps/agent_app/runtime_request_builder.py'
- 'api/core/rbac/entities.py'
- 'api/core/tools/__base/tool_runtime.py'
- 'api/core/tools/builtin_tool/_position.yaml'
- 'api/core/workflow/node_runtime.py'
- 'api/core/workflow/nodes/agent_v2/runtime_request_builder.py'
- 'api/extensions/ext_celery.py'
- 'api/extensions/ext_commands.py'
- 'api/models/__init__.py'
- 'api/services/account_service.py'
- 'api/services/agent_tool_inner_service.py'
- 'api/services/enterprise/rbac_service.py'
- 'api/services/entities/agent_tool_inner.py'
- 'api/services/knowledge_fs/**'
- 'api/services/knowledge_fs_capability.py'
- 'api/tests/unit_tests/dev/test_generate_knowledge_fs_contract.py'
- 'api/tests/unit_tests/controllers/console/workspace/test_rbac.py'
- 'api/tests/unit_tests/core/agent/test_base_agent_runner.py'
- 'api/tests/unit_tests/core/app/apps/agent_app/test_runtime_request_builder.py'
- 'api/tests/unit_tests/core/workflow/nodes/agent_v2/test_runtime_request_builder.py'
- 'api/tests/unit_tests/core/workflow/nodes/tool/test_tool_node_runtime.py'
- 'api/tests/unit_tests/core/workflow/test_node_runtime.py'
- 'api/tests/unit_tests/services/enterprise/test_rbac_service.py'
- 'api/tests/unit_tests/services/test_account_service.py'
- 'api/tests/unit_tests/services/test_agent_tool_inner_service.py'
- 'api/tests/unit_tests/services/test_knowledge_fs_capability.py'
- 'api/tests/unit_tests/services/test_knowledge_fs_product_operations.py'
- 'api/pyproject.toml'
- 'api/uv.lock'
- 'dify-agent/src/dify_agent/layers/dify_core_tools/client.py'
- 'dify-agent/tests/local/dify_agent/layers/dify_core_tools/test_client.py'
- 'packages/contracts/generated/api/console/**'
- 'packages/contracts/generated/api/service/**'
- 'docker/.env.example'
- 'docker/README.md'
- 'docker/dify-env-sync.py'
- 'docker/dify-env-sync.sh'
- 'docker/docker-compose-template.yaml'
- 'docker/docker-compose.yaml'
- 'docker/envs/core-services/api.env.example'
- 'docker/envs/core-services/knowledge-fs.env.example'
- 'docker/generate_docker_compose'
- 'docs/design/knowledge-fs*'
- '.github/dependabot.yml'
- '.github/workflows/knowledge-fs-ci.yml'
build:
name: Build KnowledgeFS API production image
needs:
- check-changes
- quality
if: >-
(needs.check-changes.outputs.knowledge-fs == 'true' || github.event_name == 'workflow_dispatch') &&
needs.quality.result == 'success'
runs-on: depot-ubuntu-24.04-4
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Login to Docker Hub
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'push' &&
(github.ref == 'refs/heads/main' || github.ref == 'refs/heads/deploy/konwledge'))
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
username: ${{ secrets.DOCKERHUB_USER }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract KnowledgeFS image metadata
id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: ${{ env.DIFY_KNOWLEDGE_FS_API_IMAGE_NAME }}
tags: |
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
type=ref,event=branch
type=sha,format=long
- name: Build KnowledgeFS API image
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: ./knowledge-fs
file: ./knowledge-fs/apps/api/Dockerfile
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64
push: >-
${{ github.event_name == 'workflow_dispatch' ||
(github.event_name == 'push' &&
(github.ref == 'refs/heads/main' || github.ref == 'refs/heads/deploy/konwledge')) }}
tags: ${{ steps.meta.outputs.tags }}
quality:
name: Run KnowledgeFS quality and contract gates
needs: check-changes
if: needs.check-changes.outputs.knowledge-fs == 'true' || github.event_name == 'workflow_dispatch'
runs-on: depot-ubuntu-24.04-4
defaults:
run:
shell: bash
working-directory: ./knowledge-fs
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
package_json_file: knowledge-fs/package.json
run_install: false
- name: Setup Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
cache: pnpm
cache-dependency-path: knowledge-fs/pnpm-lock.yaml
- name: Install KnowledgeFS dependencies
run: pnpm install --frozen-lockfile
- name: Scan KnowledgeFS secrets
run: pnpm security:secrets
- name: Audit KnowledgeFS production dependencies
run: pnpm security:dependencies
- name: Run KnowledgeFS checks
run: pnpm check
- name: Build KnowledgeFS
run: pnpm build
- name: Lint KnowledgeFS
run: pnpm lint:backend
- name: Setup UV and Python
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
with:
enable-cache: true
python-version: '3.12'
cache-dependency-glob: |
api/uv.lock
dify-agent/uv.lock
- name: Verify Dify dependency lock
working-directory: .
run: uv lock --project api --check
- name: Install Dify contract dependencies
working-directory: .
run: uv sync --project api --locked --dev
- name: Collect Dify KnowledgeFS gate targets
working-directory: .
run: |
set -euo pipefail
target_dir="${RUNNER_TEMP:?}/knowledge-fs-ci-targets"
mkdir -p "$target_dir"
production_targets=()
add_production_target() {
local path="$1"
if [[ ! -f "$path" ]]; then
echo "required Dify KnowledgeFS production target is missing: $path" >&2
exit 1
fi
production_targets+=("$path")
}
while IFS= read -r -d '' path; do
if [[ "$path" == *knowledge_fs* ]]; then
add_production_target "$path"
fi
done < <(
find api \
\( -path 'api/.venv' -o -path 'api/tests' -o -path 'api/storage' \) -prune \
-o -type f -name '*.py' -print0
)
production_touchpoints=(
api/app_factory.py
api/commands/__init__.py
api/controllers/console/__init__.py
api/controllers/console/workspace/rbac.py
api/controllers/service_api/__init__.py
api/core/agent/base_agent_runner.py
api/core/app/apps/agent_app/runtime_request_builder.py
api/core/rbac/entities.py
api/core/tools/__base/tool_runtime.py
api/core/workflow/node_runtime.py
api/core/workflow/nodes/agent_v2/runtime_request_builder.py
api/extensions/ext_celery.py
api/extensions/ext_commands.py
api/models/__init__.py
api/services/account_service.py
api/services/agent_tool_inner_service.py
api/services/enterprise/rbac_service.py
api/services/entities/agent_tool_inner.py
)
for path in "${production_touchpoints[@]}"; do
add_production_target "$path"
done
printf '%s\0' "${production_touchpoints[@]}" > "$target_dir/glue-files"
if ((${#production_targets[@]} == 0)); then
echo "Dify KnowledgeFS production target set is empty" >&2
exit 1
fi
printf '%s\0' "${production_targets[@]}" > "$target_dir/production-files"
test_targets=()
add_test_target() {
local path="$1"
if [[ ! -f "$path" ]]; then
echo "required Dify KnowledgeFS unit test is missing: $path" >&2
exit 1
fi
test_targets+=("$path")
}
while IFS= read -r -d '' path; do
if [[ "$path" == *knowledge_fs* ]]; then
add_test_target "$path"
fi
done < <(find api/tests/unit_tests -type f -name '*.py' -print0)
test_touchpoints=(
api/tests/unit_tests/controllers/console/workspace/test_rbac.py
api/tests/unit_tests/core/agent/test_base_agent_runner.py
api/tests/unit_tests/core/app/apps/agent_app/test_runtime_request_builder.py
api/tests/unit_tests/core/workflow/nodes/agent_v2/test_runtime_request_builder.py
api/tests/unit_tests/core/workflow/nodes/tool/test_tool_node_runtime.py
api/tests/unit_tests/core/workflow/test_node_runtime.py
api/tests/unit_tests/services/enterprise/test_rbac_service.py
api/tests/unit_tests/services/test_account_service.py
api/tests/unit_tests/services/test_agent_tool_inner_service.py
)
for path in "${test_touchpoints[@]}"; do
add_test_target "$path"
done
required_test_scopes=(
/commands/
/configs/
/controllers/
/core/agent/
/core/app/apps/agent_app/
/core/tools/builtin_tool/providers/knowledge_fs/
/core/workflow/
/dev/
/extensions/
/migrations/
/models/
/repositories/
/services/
/tasks/
)
for required_scope in "${required_test_scopes[@]}"; do
scope_found=false
for path in "${test_targets[@]}"; do
if [[ "$path" == *"$required_scope"* ]]; then
scope_found=true
break
fi
done
if [[ "$scope_found" != true ]]; then
echo "required Dify KnowledgeFS test scope is empty: $required_scope" >&2
exit 1
fi
done
if ((${#test_targets[@]} == 0)); then
echo "Dify KnowledgeFS unit test target set is empty" >&2
exit 1
fi
printf '%s\0' "${test_targets[@]}" > "$target_dir/unit-test-files"
- name: Lint Dify KnowledgeFS integration
working-directory: .
run: |
set -euo pipefail
targets=()
while IFS= read -r -d '' path; do
targets+=("$path")
done < "${RUNNER_TEMP:?}/knowledge-fs-ci-targets/production-files"
if ((${#targets[@]} == 0)); then
echo "Dify KnowledgeFS production target manifest is empty" >&2
exit 1
fi
uv run --project api --dev ruff format --check "${targets[@]}"
uv run --project api --dev ruff check "${targets[@]}"
- name: Type-check Dify KnowledgeFS integration
working-directory: .
run: |
set -euo pipefail
targets=()
while IFS= read -r -d '' path; do
targets+=("$path")
done < "${RUNNER_TEMP:?}/knowledge-fs-ci-targets/production-files"
if ((${#targets[@]} == 0)); then
echo "Dify KnowledgeFS production target manifest is empty" >&2
exit 1
fi
PYREFLY_OUTPUT_FORMAT=github ./dev/pyrefly-check-local "${targets[@]}"
mypy_targets=()
for path in "${targets[@]}"; do
if [[ "$path" != api/migrations/* ]]; then
mypy_targets+=("${path#api/}")
fi
done
if ((${#mypy_targets[@]} == 0)); then
echo "Dify KnowledgeFS Mypy target set is empty" >&2
exit 1
fi
uv run --directory api --dev mypy \
--explicit-package-bases \
--exclude-gitignore \
--exclude '(^|/)conftest\.py$' \
--exclude 'tests/' \
--exclude 'migrations/' \
--check-untyped-defs \
--disable-error-code=import-untyped \
"${mypy_targets[@]}"
- name: Test Dify KnowledgeFS unit surface
working-directory: .
env:
COVERAGE_FILE: ${{ runner.temp }}/dify-knowledge-fs.coverage
run: |
set -euo pipefail
targets=()
while IFS= read -r -d '' path; do
targets+=("$path")
done < "${RUNNER_TEMP:?}/knowledge-fs-ci-targets/unit-test-files"
if ((${#targets[@]} == 0)); then
echo "Dify KnowledgeFS unit test target manifest is empty" >&2
exit 1
fi
uv run --project api --dev coverage run --branch --source=api -m pytest "${targets[@]}" --no-cov -q
- name: Enforce Dify KnowledgeFS focused coverage
working-directory: .
env:
COVERAGE_FILE: ${{ runner.temp }}/dify-knowledge-fs.coverage
KNOWLEDGE_FS_COVERAGE_BASE: >-
${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha || github.event.before || '' }}
run: |
set -euo pipefail
report="${RUNNER_TEMP:?}/dify-knowledge-fs-coverage.json"
uv run --project api --dev coverage json --show-contexts -o "$report"
uv run --project api --dev python api/dev/check_knowledge_fs_coverage.py \
--coverage-json "$report" \
--glue-manifest "${RUNNER_TEMP:?}/knowledge-fs-ci-targets/glue-files" \
--base "$KNOWLEDGE_FS_COVERAGE_BASE" \
--minimum 90 \
--glue-minimum 90
- name: Verify Dify KnowledgeFS contract
working-directory: .
run: uv run --project api python api/dev/generate_knowledge_fs_contract.py --check
- name: Verify Dify Agent dependency lock
working-directory: .
run: uv lock --project dify-agent --check
- name: Install Dify Agent gate dependencies
working-directory: .
run: uv sync --project dify-agent --locked --dev
- name: Lint Dify Agent KnowledgeFS integration
working-directory: ./dify-agent
run: |
uv run --project . --dev ruff format --check \
src/dify_agent/layers/dify_core_tools/client.py \
tests/local/dify_agent/layers/dify_core_tools/test_client.py
uv run --project . --dev ruff check \
src/dify_agent/layers/dify_core_tools/client.py \
tests/local/dify_agent/layers/dify_core_tools/test_client.py
- name: Type-check Dify Agent KnowledgeFS integration
working-directory: ./dify-agent
run: >-
uv run --project . --dev basedpyright --level error
src/dify_agent/layers/dify_core_tools/client.py
tests/local/dify_agent/layers/dify_core_tools/test_client.py
- name: Test Dify Agent KnowledgeFS integration
working-directory: ./dify-agent
run: >-
uv run --project . --dev python -m pytest
tests/local/dify_agent/layers/dify_core_tools/test_client.py
-q
skip:
name: Skip KnowledgeFS quality and contract gates
needs: check-changes
if: needs.check-changes.outputs.knowledge-fs != 'true' && github.event_name != 'workflow_dispatch'
runs-on: depot-ubuntu-24.04-4
steps:
- name: Report skipped KnowledgeFS checks
run: echo "No KnowledgeFS-related changes detected; skipping KnowledgeFS checks."
final:
name: KnowledgeFS CI
if: ${{ always() }}
needs:
- check-changes
- build
- quality
- skip
runs-on: depot-ubuntu-24.04-4
steps:
- name: Finalize KnowledgeFS CI status
env:
EVENT_NAME: ${{ github.event_name }}
BUILD_RESULT: ${{ needs.build.result }}
KNOWLEDGE_FS_CHANGED: ${{ needs.check-changes.outputs.knowledge-fs }}
QUALITY_RESULT: ${{ needs.quality.result }}
SKIP_RESULT: ${{ needs.skip.result }}
run: |
if [[ "$EVENT_NAME" == 'workflow_dispatch' || "$KNOWLEDGE_FS_CHANGED" == 'true' ]]; then
if [[ "$BUILD_RESULT" == 'success' && "$QUALITY_RESULT" == 'success' ]]; then
echo "KnowledgeFS build and checks ran successfully."
exit 0
fi
echo "KnowledgeFS build or checks failed: build=$BUILD_RESULT quality=$QUALITY_RESULT" >&2
exit 1
fi
if [[ "$SKIP_RESULT" == 'success' ]]; then
echo "KnowledgeFS checks were skipped because no related files changed."
exit 0
fi
echo "KnowledgeFS change detection or skip reporting failed with result: $SKIP_RESULT" >&2
exit 1
+1 -1
View File
@@ -1,4 +1,4 @@
name: "Pull Request Labeler"
name: 'Pull Request Labeler'
on:
pull_request_target:
+5 -4
View File
@@ -2,9 +2,9 @@ name: Main CI Pipeline
on:
pull_request:
branches: ["main"]
branches: ['main']
merge_group:
branches: ["main"]
branches: ['main']
types: [checks_requested]
permissions:
@@ -15,7 +15,7 @@ permissions:
statuses: write
concurrency:
group: main-ci-${{ github.head_ref || github.run_id }}
group: main-ci-${{ github.event.pull_request.number || github.event.merge_group.head_sha || github.run_id }}
cancel-in-progress: true
jobs:
@@ -48,7 +48,7 @@ jobs:
sandbox-runtime-changed: ${{ steps.changes.outputs.sandbox-runtime }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
- uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3
id: changes
with:
filters: |
@@ -89,6 +89,7 @@ jobs:
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- '.nvmrc'
- '.github/workflows/main-ci.yml'
- '.github/workflows/web-tests.yml'
- '.github/actions/setup-web/**'
e2e:
+2 -2
View File
@@ -2,7 +2,7 @@ name: Post-Merge Checks
on:
push:
branches: ["main"]
branches: ['main']
permissions:
contents: read
@@ -19,7 +19,7 @@ jobs:
external-e2e-changed: ${{ steps.changes.outputs.external_e2e }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
- uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3
id: changes
with:
filters: |
+1 -1
View File
@@ -57,7 +57,7 @@ jobs:
cache-dependency-path: dify-agent-runtime/go.sum
- name: Run golangci-lint
uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v6.5.0
uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0
with:
working-directory: dify-agent-runtime
version: latest
+1 -1
View File
@@ -8,7 +8,7 @@ on:
- reopened
- synchronize
merge_group:
branches: ["main"]
branches: ['main']
types: [checks_requested]
jobs:
+2 -3
View File
@@ -11,7 +11,6 @@ on:
jobs:
stale:
runs-on: depot-ubuntu-24.04
permissions:
issues: write
@@ -23,8 +22,8 @@ jobs:
days-before-issue-stale: 15
days-before-issue-close: 3
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: "Closed due to inactivity. If you have any questions, you can reopen it."
stale-pr-message: "Closed due to inactivity. If you have any questions, you can reopen it."
stale-issue-message: 'Closed due to inactivity. If you have any questions, you can reopen it.'
stale-pr-message: 'Closed due to inactivity. If you have any questions, you can reopen it.'
stale-issue-label: 'no-issue-activity'
stale-pr-label: 'no-pr-activity'
any-of-labels: '🌚 invalid,🙋‍♂️ question,wont-fix,no-issue-activity,no-pr-activity,💪 enhancement,🤔 cant-reproduce,🙏 help wanted'
+7 -8
View File
@@ -7,10 +7,6 @@ on:
required: true
type: string
concurrency:
group: style-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions:
checks: write
statuses: write
@@ -48,7 +44,7 @@ jobs:
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
enable-cache: false
python-version: "3.12"
python-version: '3.12'
cache-dependency-glob: api/uv.lock
- name: Install dependencies
@@ -109,6 +105,8 @@ jobs:
package.json
pnpm-lock.yaml
pnpm-workspace.yaml
knip.config.ts
scripts/check-web-production-unused-after-knip-fix.mjs
.nvmrc
.github/workflows/style.yml
.github/actions/setup-web/**
@@ -125,14 +123,17 @@ jobs:
- name: Web dead code check
if: steps.changed-files.outputs.any_changed == 'true'
working-directory: .
run: vp run knip
- name: Web dead code check production
if: steps.changed-files.outputs.any_changed == 'true'
working-directory: .
run: vp run knip:production
- name: Web production unused declarations check
if: steps.changed-files.outputs.any_changed == 'true'
working-directory: .
run: vp run knip:production-unused-check
ts-common-style:
@@ -168,9 +169,7 @@ jobs:
oxlint-suppressions.json
eslint-suppressions.json
.vscode/**
.github/workflows/autofix.yml
.github/workflows/style.yml
.github/actions/setup-web/**
.github/**
- name: Setup web environment
if: steps.changed-files.outputs.any_changed == 'true'
+1 -1
View File
@@ -158,7 +158,7 @@ jobs:
- name: Run Claude Code for Translation Sync
if: steps.context.outputs.CHANGED_FILES != ''
uses: anthropics/claude-code-action@be7b93b1907a4abad570368f3c74b6fe3807510b # v1.0.183
uses: anthropics/claude-code-action@1623c36729ac1cd5895198cded705a287de7db79 # v1.0.187
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ secrets.GITHUB_TOKEN }}
+10 -10
View File
@@ -20,7 +20,7 @@ jobs:
strategy:
matrix:
python-version:
- "3.12"
- '3.12'
steps:
- name: Checkout code
@@ -48,16 +48,16 @@ jobs:
- name: Install dependencies
run: uv sync --project api --dev
# - name: Set up Vector Store (TiDB)
# uses: hoverkraft-tech/[email protected]
# with:
# compose-file: docker/tidb/docker-compose.yaml
# services: |
# tidb
# tiflash
# - name: Set up Vector Store (TiDB)
# uses: hoverkraft-tech/[email protected]
# with:
# compose-file: docker/tidb/docker-compose.yaml
# services: |
# tidb
# tiflash
# - name: Check VDB Ready (TiDB)
# run: uv run --project api python api/providers/vdb/tidb-vector/tests/integration_tests/check_tiflash_ready.py
# - name: Check VDB Ready (TiDB)
# run: uv run --project api python api/providers/vdb/tidb-vector/tests/integration_tests/check_tiflash_ready.py
- name: Test Vector Stores
run: |
+10 -10
View File
@@ -17,7 +17,7 @@ jobs:
strategy:
matrix:
python-version:
- "3.12"
- '3.12'
steps:
- name: Checkout code
@@ -45,16 +45,16 @@ jobs:
- name: Install dependencies
run: uv sync --project api --dev
# - name: Set up Vector Store (TiDB)
# uses: hoverkraft-tech/[email protected]
# with:
# compose-file: docker/tidb/docker-compose.yaml
# services: |
# tidb
# tiflash
# - name: Set up Vector Store (TiDB)
# uses: hoverkraft-tech/[email protected]
# with:
# compose-file: docker/tidb/docker-compose.yaml
# services: |
# tidb
# tiflash
# - name: Check VDB Ready (TiDB)
# run: uv run --project api python api/providers/vdb/tidb-vector/tests/integration_tests/check_tiflash_ready.py
# - name: Check VDB Ready (TiDB)
# run: uv run --project api python api/providers/vdb/tidb-vector/tests/integration_tests/check_tiflash_ready.py
- name: Test Vector Stores
run: |
+18 -9
View File
@@ -11,10 +11,6 @@ on:
permissions:
contents: read
concurrency:
group: web-e2e-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
name: Web Full-Stack E2E
@@ -30,6 +26,11 @@ jobs:
with:
persist-credentials: false
- name: Switch Ubuntu mirror
run: |
sudo sed -i 's|http://us-east-1.ec2.archive.ubuntu.com/ubuntu|http://azure.archive.ubuntu.com/ubuntu|g' /etc/apt/sources.list.d/ubuntu.sources
sudo apt-get update
- name: Setup web dependencies
uses: ./.github/actions/setup-web
@@ -37,7 +38,7 @@ jobs:
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
enable-cache: true
python-version: "3.12"
python-version: '3.12'
cache-dependency-glob: |
api/uv.lock
dify-agent/uv.lock
@@ -50,9 +51,17 @@ jobs:
working-directory: ./e2e
run: vp run test:unit
- name: Install Playwright browser
- name: Install Playwright browsers for core E2E
if: ${{ !inputs.run-external-runtime }}
timeout-minutes: 15
working-directory: ./e2e
run: vp run e2e:install
run: vp run e2e:install:ci
- name: Install Chromium for external runtime E2E
if: ${{ inputs.run-external-runtime }}
timeout-minutes: 15
working-directory: ./e2e
run: vp run e2e:install:ci:chromium
- name: Run isolated source-api and built-web Cucumber E2E tests
if: ${{ !inputs.run-external-runtime }}
@@ -61,7 +70,7 @@ jobs:
E2E_ADMIN_EMAIL: [email protected]
E2E_ADMIN_NAME: E2E Admin
E2E_ADMIN_PASSWORD: E2eAdmin12345
E2E_FORCE_WEB_BUILD: "1"
E2E_FORCE_WEB_BUILD: '1'
E2E_INIT_PASSWORD: E2eInit12345
run: vp run e2e:full
@@ -121,7 +130,7 @@ jobs:
E2E_AGENT_DECISION_MODEL_NAME: ${{ vars.E2E_AGENT_DECISION_MODEL_NAME || 'gpt-5.5' }}
E2E_AGENT_DECISION_MODEL_PROVIDER: ${{ vars.E2E_AGENT_DECISION_MODEL_PROVIDER || 'openai' }}
E2E_AGENT_DECISION_MODEL_TYPE: ${{ vars.E2E_AGENT_DECISION_MODEL_TYPE || 'llm' }}
E2E_FORCE_WEB_BUILD: "1"
E2E_FORCE_WEB_BUILD: '1'
E2E_INIT_PASSWORD: E2eInit12345
E2E_MARKETPLACE_API_URL: ${{ vars.E2E_MARKETPLACE_API_URL }}
E2E_MARKETPLACE_PLUGIN_IDS: ${{ vars.E2E_MARKETPLACE_PLUGIN_IDS }}
+16 -6
View File
@@ -9,10 +9,6 @@ on:
permissions:
contents: read
concurrency:
group: web-tests-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
test:
name: Web Tests (${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
@@ -91,6 +87,7 @@ jobs:
dify-ui-test:
name: dify-ui Tests
runs-on: depot-ubuntu-24.04-4
timeout-minutes: 20
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
defaults:
@@ -104,11 +101,17 @@ jobs:
with:
persist-credentials: false
- name: Switch Ubuntu mirror
run: |
sudo sed -i 's|http://us-east-1.ec2.archive.ubuntu.com/ubuntu|http://azure.archive.ubuntu.com/ubuntu|g' /etc/apt/sources.list.d/ubuntu.sources
sudo apt-get update
- name: Setup web environment
uses: ./.github/actions/setup-web
- name: Install Chromium for Browser Mode
run: vp exec playwright install --with-deps chromium
timeout-minutes: 15
run: vp exec playwright install --with-deps --only-shell chromium
- name: Run dify-ui tests
run: vp test run --project unit --coverage --silent=passed-only
@@ -125,6 +128,7 @@ jobs:
dify-ui-storybook-test:
name: dify-ui Storybook Tests
runs-on: depot-ubuntu-24.04-4
timeout-minutes: 20
defaults:
run:
shell: bash
@@ -136,11 +140,17 @@ jobs:
with:
persist-credentials: false
- name: Switch Ubuntu mirror
run: |
sudo sed -i 's|http://us-east-1.ec2.archive.ubuntu.com/ubuntu|http://azure.archive.ubuntu.com/ubuntu|g' /etc/apt/sources.list.d/ubuntu.sources
sudo apt-get update
- name: Setup web environment
uses: ./.github/actions/setup-web
- name: Install Chromium for Browser Mode
run: vp exec playwright install --with-deps chromium
timeout-minutes: 15
run: vp exec playwright install --with-deps --only-shell chromium
- name: Run dify-ui Storybook tests
run: vp run test:storybook
+8
View File
@@ -30,6 +30,11 @@ share/python-wheels/
*.egg
MANIFEST
# KnowledgeFS is an independently rooted TypeScript workspace. Its admin `lib`
# directory contains source files rather than Python build output.
!/knowledge-fs/apps/admin/lib/
!/knowledge-fs/apps/admin/lib/**
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
@@ -140,6 +145,9 @@ dmypy.json
pyrightconfig.json
!api/pyrightconfig.json
# import-linter
.import_linter_cache/
# Pyre type checker
.pyre/
.idea/'
@@ -0,0 +1,30 @@
# KnowledgeFS metadata catalog
## What changed
- Added a durable, per-knowledge-space metadata field catalog with `string`, `number`, and `time` field types.
- Added document-to-field bindings so field usage counts no longer require scanning every document.
- Added metadata field list, create, rename, and delete APIs in KnowledgeFS and the Dify console proxy.
- Made document metadata writes validate field definitions and update the document, bindings, and active retrieval asset in one database transaction.
- Made field rename and delete cascade to bound document values and active retrieval metadata.
- Updated the KnowledgeFS document UI to read and mutate the field catalog directly instead of deriving it from paginated document data.
- Included nested `userMetadata` in retrieval metadata filtering.
## Data migration
Migration `0040_knowledge_space_metadata` creates the catalog and binding tables and backfills consistently typed custom metadata already stored on logical documents. Reserved system keys and inconsistently typed values are left unbound rather than inventing an unsafe catalog type.
## Compatibility
- Existing document metadata remains readable.
- A custom field must exist in the space catalog before a new value can be assigned.
- Catalog mutations use row-version compare-and-swap semantics.
- No default value is written to every document when a field is created.
## Verification
- Database schema and migration suites.
- Metadata repository, handler, logical-document lifecycle, and retrieval candidate suites.
- Dify facade, product operation, and controller delegation suites.
- KnowledgeFS TypeScript type checking and Biome checks.
- Frontend metadata page tests, Vite+ checks, and TypeScript type checking.
+1
View File
@@ -6,3 +6,4 @@ Dify is an open-source platform for building LLM applications, agentic workflows
- Run backend commands through `uv run --project api <command>`.
- Backend integration tests are CI-only and are not expected to run locally.
- Keep `docker/.env.example` limited to variables required for a default Docker Compose deployment to start. Put optional and provider-specific settings in the matching `docker/envs/*.env.example` file; `docker/.env` overrides those service-specific env files.
+4 -5
View File
@@ -209,12 +209,11 @@ At the same time, please consider supporting Dify by sharing it on social media
## Star History
<!-- GitHub token name: star-history -->
<a href="https://www.star-history.com/?repos=langgenius%2Fdify&type=date&legend=top-left">
<a href="https://star-history.dera.page/#langgenius/dify&type=date&legend=top-left">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=langgenius/dify&type=date&theme=dark&legend=top-left&sealed_token=p-SWD-UXZEDc5a2d0EMfgMmyCwVyRlSof0Qox68v7k4PvQPKRlCx0jDIlNztw7mbA6DEn96R50DojO9pCi5LQUlDBAoIhRswt8-GuC8K3rZQ3naJUXbuHqR_oItIW2F0NNM-7npevzw5SXp7L5mwixgqJncAvAGzuGq5zmhQfiDRSW_Jd6y8TQCZMJbt" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=langgenius/dify&type=date&legend=top-left&sealed_token=p-SWD-UXZEDc5a2d0EMfgMmyCwVyRlSof0Qox68v7k4PvQPKRlCx0jDIlNztw7mbA6DEn96R50DojO9pCi5LQUlDBAoIhRswt8-GuC8K3rZQ3naJUXbuHqR_oItIW2F0NNM-7npevzw5SXp7L5mwixgqJncAvAGzuGq5zmhQfiDRSW_Jd6y8TQCZMJbt" />
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=langgenius/dify&type=date&legend=top-left&sealed_token=p-SWD-UXZEDc5a2d0EMfgMmyCwVyRlSof0Qox68v7k4PvQPKRlCx0jDIlNztw7mbA6DEn96R50DojO9pCi5LQUlDBAoIhRswt8-GuC8K3rZQ3naJUXbuHqR_oItIW2F0NNM-7npevzw5SXp7L5mwixgqJncAvAGzuGq5zmhQfiDRSW_Jd6y8TQCZMJbt" />
<source media="(prefers-color-scheme: dark)" srcset="https://star-history.dera.page/svg?repos=langgenius/dify&type=date&theme=dark&legend=top-left" />
<source media="(prefers-color-scheme: light)" srcset="https://star-history.dera.page/svg?repos=langgenius/dify&type=date&legend=top-left" />
<img alt="Star History Chart" src="https://star-history.dera.page/svg?repos=langgenius/dify&type=date&legend=top-left" />
</picture>
</a>
+36 -3
View File
@@ -39,6 +39,9 @@ ENABLE_COLLABORATION_MODE=true
# Learn app feature toggle
ENABLE_LEARN_APP=true
# Show the license expiry countdown badge in the console (enterprise only)
ENABLE_LICENSE_EXPIRY_NOTICE=true
# Access token expiration time in minutes
ACCESS_TOKEN_EXPIRE_MINUTES=60
@@ -117,6 +120,19 @@ SQLALCHEMY_POOL_RESET_ON_RETURN=rollback
# storage type: opendal, s3, aliyun-oss, azure-blob, baidu-obs, google-storage, huawei-obs, oci-storage, tencent-cos, volcengine-tos, supabase
STORAGE_TYPE=opendal
# Key provider configuration, used to encrypt/decrypt tenant credentials (LLM/tool provider secrets)
# key provider type: local, azure-keyvault
KEY_PROVIDER_TYPE=local
# Azure Key Vault configuration, required when KEY_PROVIDER_TYPE=azure-keyvault
# authentication uses DefaultAzureCredential (managed identity, environment variables, or Azure CLI login)
AZURE_KEYVAULT_VAULT_URL=https://<your-vault-name>.vault.azure.net
AZURE_KEYVAULT_KEY_SIZE=2048
# optional: auto-rotate each tenant's key every N days. Leave empty to manage rotation manually.
# rotation is safe because old key versions are never given an expiry and stay usable forever --
# do NOT set a rotation policy on this key in the Azure portal that expires old versions.
AZURE_KEYVAULT_ROTATION_INTERVAL_DAYS=
# Apache OpenDAL storage configuration, refer to https://github.com/apache/opendal
OPENDAL_SCHEME=fs
OPENDAL_FS_ROOT=storage
@@ -326,6 +342,7 @@ TIDB_VECTOR_PORT=4000
TIDB_VECTOR_USER=xxx.root
TIDB_VECTOR_PASSWORD=xxxxxx
TIDB_VECTOR_DATABASE=dify
TIDB_VECTOR_ENABLE_FULLTEXT_SEARCH=false
# Tidb on qdrant configuration
TIDB_ON_QDRANT_URL=http://127.0.0.1
@@ -333,6 +350,7 @@ TIDB_ON_QDRANT_API_KEY=dify
TIDB_ON_QDRANT_CLIENT_TIMEOUT=20
TIDB_ON_QDRANT_GRPC_ENABLED=false
TIDB_ON_QDRANT_GRPC_PORT=6334
TIDB_ON_QDRANT_ESTIMATED_STORAGE_LIMITS_MB=sandbox:60,professional:6400,team:25600
TIDB_PUBLIC_KEY=dify
TIDB_PRIVATE_KEY=dify
TIDB_API_URL=http://127.0.0.1
@@ -432,6 +450,7 @@ OPENGAUSS_MAX_CONNECTION=5
# Upload configuration
UPLOAD_FILE_SIZE_LIMIT=15
KNOWLEDGE_UPLOAD_FILE_SIZE_LIMIT_FOR_PAID_PLAN=15
UPLOAD_FILE_BATCH_LIMIT=5
UPLOAD_IMAGE_FILE_SIZE_LIMIT=10
UPLOAD_VIDEO_FILE_SIZE_LIMIT=100
@@ -687,11 +706,25 @@ AGENT_BACKEND_RUN_TIMEOUT_SECONDS=1200
# KnowledgeFS (Dataset 2.0)
KNOWLEDGE_FS_ENABLED=false
# Production deployments require HTTPS; plain HTTP is limited to non-production or loopback.
KNOWLEDGE_FS_BASE_URL=
# Shared with KnowledgeFS; use at least 32 random characters.
KNOWLEDGE_FS_JWT_SECRET=
KNOWLEDGE_FS_SSE_READ_TIMEOUT_SECONDS=300
KNOWLEDGE_FS_DIRECT_ORIGIN=
KNOWLEDGE_FS_LIFECYCLE_WORKER_ENABLED=false
KNOWLEDGE_FS_INTEGRATED_PROVISION_READY=false
KNOWLEDGE_FS_LEGACY_ACL_FREEZE_READY=false
KNOWLEDGE_FS_LIFECYCLE_POLL_INTERVAL_SECONDS=15
KNOWLEDGE_FS_LIFECYCLE_LEASE_SECONDS=60
KNOWLEDGE_FS_LIFECYCLE_BATCH_SIZE=25
KNOWLEDGE_FS_CAPABILITY_V2_ENABLED=false
KNOWLEDGE_FS_CAPABILITY_V2_SIGNING_KID=
KNOWLEDGE_FS_CAPABILITY_V2_PRIVATE_KEY_PEM=
KNOWLEDGE_FS_CAPABILITY_V2_PREVIOUS_PUBLIC_JWKS=
KNOWLEDGE_FS_CAPABILITY_V2_ISSUER=dify-control-plane
KNOWLEDGE_FS_CAPABILITY_V2_AUDIENCE=knowledge-fs
KNOWLEDGE_FS_CAPABILITY_V2_MAX_TTL_SECONDS=60
KNOWLEDGE_FS_TIMEOUT_SECONDS=10
KNOWLEDGE_FS_JWKS_CACHE_MAX_AGE_SECONDS=300
KNOWLEDGE_FS_PRODUCT_MAX_RESPONSE_BYTES=4194304
# Marketplace configuration
MARKETPLACE_ENABLED=true
+127
View File
@@ -8,7 +8,134 @@ root_packages =
extensions
factories
libs
machinery
models
repositories
tasks
services
include_external_packages = True
[importlinter:contract:no-direct-rsa-imports]
# Note: `libs` itself is deliberately excluded from source_modules -- import-linter's
# `forbidden` contract cannot check a package against its own descendant (libs.rsa lives
# inside libs). Sibling modules under libs/ importing libs.rsa are not covered by this
# contract; the realistic risk this guards against is application/service code reaching
# past the key provider abstraction, which is fully covered below.
name = Only the key provider abstraction may import libs.rsa directly
type = forbidden
source_modules =
core
constants
context
configs
controllers
extensions
factories
models
tasks
services
forbidden_modules =
libs.rsa
allow_indirect_imports = True
[importlinter:contract:machinery-framework-boundary]
name = API machinery is framework neutral
type = forbidden
source_modules =
machinery
forbidden_modules =
controllers
extensions
flask
models
sqlalchemy
werkzeug
[importlinter:contract:workspace-query-service-boundary]
name = Workspace query application service is framework and persistence neutral
type = forbidden
source_modules =
services.workspace_query_service
forbidden_modules =
controllers
extensions
flask
models
repositories
sqlalchemy
werkzeug
[importlinter:contract:explore-banner-query-service-boundary]
name = Explore banner query application service is framework and persistence neutral
type = forbidden
source_modules =
services.explore_banner_query_service
forbidden_modules =
controllers
extensions
flask
models
repositories
sqlalchemy
werkzeug
[importlinter:contract:feature-query-service-boundary]
name = Feature query application service is framework and persistence neutral
type = forbidden
source_modules =
services.feature_query_service
forbidden_modules =
controllers
extensions
flask
models
repositories
sqlalchemy
werkzeug
[importlinter:contract:workspace-member-query-service-boundary]
name = Workspace member query application service is framework and persistence neutral
type = forbidden
source_modules =
services.workspace_member_query_service
forbidden_modules =
configs
controllers
extensions
flask
models
repositories
services.enterprise
services.workspace_member_role_resolver
sqlalchemy
werkzeug
[importlinter:contract:setup-service-boundary]
name = Setup application service is framework and persistence neutral
type = forbidden
source_modules =
services.setup_service
forbidden_modules =
configs
controllers
extensions
flask
models
repositories
sqlalchemy
werkzeug
[importlinter:contract:schema-definition-service-boundary]
name = Schema definition application service is framework and persistence neutral
type = forbidden
source_modules =
services.schema_definition_service
forbidden_modules =
configs
controllers
extensions
flask
models
repositories
sqlalchemy
werkzeug
+3 -1
View File
@@ -50,7 +50,9 @@ The scripts resolve paths relative to their location, so you can run them from a
./dev/start-worker
```
1. Optional: start Celery Beat (scheduled tasks).
1. Start Celery Beat when scheduled tasks are needed. This is required when
`KNOWLEDGE_FS_LIFECYCLE_WORKER_ENABLED=true` so provisioning and deletion
outbox commands are dispatched to the lifecycle worker.
```bash
./dev/start-beat
+7 -1
View File
@@ -16,7 +16,7 @@ from core.logging.context import init_request_context
from dify_app import DifyApp
from extensions.ext_socketio import sio
from services.enterprise.enterprise_service import EnterpriseService
from services.feature_service import LicenseStatus
from services.entities.feature_entities import LicenseStatus
logger = logging.getLogger(__name__)
@@ -172,6 +172,7 @@ def initialize_extensions(app: DifyApp):
from context.flask_app_context import init_flask_context
from extensions import (
ext_app_metrics,
ext_application_services,
ext_blueprints,
ext_celery,
ext_code_based_extension,
@@ -183,6 +184,8 @@ def initialize_extensions(app: DifyApp):
ext_forward_refs,
ext_hosting_provider,
ext_import_modules,
ext_key_provider,
ext_knowledge_fs_observability,
ext_logging,
ext_login,
ext_logstore,
@@ -218,6 +221,7 @@ def initialize_extensions(app: DifyApp):
ext_migrate,
ext_redis,
ext_storage,
ext_key_provider, # Initialize after storage, since RSAKeyProvider reads private keys from it
ext_set_secretkey,
ext_logstore, # Initialize logstore after storage, before celery
ext_celery,
@@ -233,6 +237,8 @@ def initialize_extensions(app: DifyApp):
ext_enterprise_telemetry,
ext_request_logging,
ext_session_factory,
ext_knowledge_fs_observability,
ext_application_services,
ext_oauth_bearer,
]
for ext in extensions:
+5
View File
@@ -10,6 +10,8 @@ from __future__ import annotations
from typing import Any
from dify_agent.protocol import RunFailureType
class AgentBackendError(Exception):
"""Base error for API-side Agent backend integration failures."""
@@ -54,6 +56,7 @@ class AgentBackendRunFailedError(AgentBackendError):
run_id: str
detail: Any
error_type: RunFailureType | None
reason: str | None
source_event_id: str | None
@@ -63,11 +66,13 @@ class AgentBackendRunFailedError(AgentBackendError):
detail: Any,
*,
message: str | None = None,
error_type: RunFailureType | None = None,
reason: str | None = None,
source_event_id: str | None = None,
) -> None:
self.run_id = run_id
self.detail = detail
self.error_type = error_type
self.reason = reason
self.source_event_id = source_event_id
display_message = message or f"Agent backend run failed: {run_id}"
@@ -22,6 +22,7 @@ from dify_agent.protocol import (
RunCancelledEvent,
RunEvent,
RunFailedEvent,
RunFailureType,
RunStartedEvent,
RunSucceededEvent,
)
@@ -96,6 +97,7 @@ class AgentBackendRunFailedInternalEvent(AgentBackendInternalEventBase):
type: Literal[AgentBackendInternalEventType.RUN_FAILED] = AgentBackendInternalEventType.RUN_FAILED
error: str
error_type: RunFailureType | None = None
reason: str | None = None
@@ -180,6 +182,7 @@ class AgentBackendRunEventAdapter:
run_id=event.run_id,
source_event_id=event.id,
error=event.data.error,
error_type=event.data.error_type,
reason=event.data.reason,
)
]
+16 -2
View File
@@ -159,8 +159,12 @@ class AgentBackendModelConfig(BaseModel):
# ``DifyPluginLLMLayerConfig.model_settings`` is pydantic_ai's ``ModelSettings``
# TypedDict (closed: unknown keys are rejected, explicit ``None`` values fail the
# per-field type checks). Agent Soul model settings carry a wider, nullable shape
# (``stop`` / ``response_format`` plus null-padded fields), so the layer config
# only receives the keys the runtime contract accepts.
# (``stop`` / ``response_format`` plus null-padded fields, plus arbitrary
# plugin-declared parameters such as Qwen's ``enable_thinking``), so the layer
# config only receives the keys the runtime contract accepts directly; anything
# else is forwarded through ``extra_body``, the TypedDict's own escape hatch for
# provider-specific parameters (see
# ``dify_agent.adapters.llm.model._map_model_settings_to_parameters``).
_AGENT_MODEL_SETTINGS_PASSTHROUGH_KEYS = (
"temperature",
"top_p",
@@ -168,6 +172,7 @@ _AGENT_MODEL_SETTINGS_PASSTHROUGH_KEYS = (
"frequency_penalty",
"max_tokens",
)
_AGENT_MODEL_SETTINGS_KNOWN_KEYS = frozenset({*_AGENT_MODEL_SETTINGS_PASSTHROUGH_KEYS, "stop", "response_format"})
def _agent_model_settings(settings: Mapping[str, JsonValue]) -> dict[str, JsonValue] | None:
@@ -177,6 +182,15 @@ def _agent_model_settings(settings: Mapping[str, JsonValue]) -> dict[str, JsonVa
stop = settings.get("stop")
if isinstance(stop, list) and stop:
sanitized["stop_sequences"] = stop
extra_body: dict[str, JsonValue] = {
key: value
for key, value in settings.items()
if key not in _AGENT_MODEL_SETTINGS_KNOWN_KEYS and value is not None
}
if extra_body:
sanitized["extra_body"] = extra_body
return sanitized or None
+2
View File
@@ -10,6 +10,7 @@ from .data_migration import (
import_migration_data,
migration_data_wizard,
)
from .knowledge_fs import knowledge_fs_control_space
from .plugin import (
backfill_plugin_auto_upgrade,
extract_plugins,
@@ -75,6 +76,7 @@ __all__ = [
"import_migration_data",
"install_plugins",
"install_rag_pipeline_plugins",
"knowledge_fs_control_space",
"legacy_model_types",
"migrate_annotation_vector_database",
"migrate_data_for_plugin",
+2 -2
View File
@@ -33,7 +33,7 @@ def reset_password(email, new_password, password_confirm):
try:
valid_password(new_password)
except:
except ValueError:
click.echo(click.style(f"Invalid password. Must match {password_pattern}", fg="red"))
return
@@ -75,7 +75,7 @@ def reset_email(email, new_email, email_confirm):
try:
email_validate(normalized_new_email)
except:
except ValueError:
click.echo(click.style(f"Invalid email: {new_email}", fg="red"))
return
+621
View File
@@ -0,0 +1,621 @@
"""Operator commands for the independent KnowledgeFS control-plane."""
from __future__ import annotations
import json
from collections.abc import Callable
from datetime import datetime
from functools import partial
from pathlib import Path
import click
from pydantic import BaseModel, ValidationError
from core.db.session_factory import session_factory
from services.knowledge_fs.cleanup import (
CleanupApprovalInput,
CleanupCompletionEvidenceInput,
CleanupReadinessEvidenceInput,
CleanupStartInput,
KnowledgeFSCleanupError,
KnowledgeFSCleanupService,
)
from services.knowledge_fs.control_space_commands import KnowledgeFSControlSpaceCommandService
from services.knowledge_fs.control_space_lifecycle import KnowledgeFSControlSpaceLifecycleError
from services.knowledge_fs.control_space_management import (
KnowledgeFSControlSpaceManagementService,
KnowledgeFSControlSpaceRegistration,
)
from services.knowledge_fs.cutover import (
CutoverSmokeResultsInput,
FinalDeltaInput,
KnowledgeFSCutoverError,
KnowledgeFSWorkspaceCutoverService,
LegacyDependencyInput,
QuarantineResolutionInput,
ShadowAuthorizationObservationInput,
ShadowCompletionInput,
WorkspaceInventoryInput,
)
from services.knowledge_fs.greenfield_initializer import KnowledgeFSWorkspaceGreenfieldInitializer
from services.knowledge_fs.orphan_reconciler import KnowledgeFSOrphanReconciler
from services.knowledge_fs.remote_registry import get_knowledge_fs_lifecycle_remote
@click.group("knowledge-fs-control-space")
def knowledge_fs_control_space() -> None:
"""Inspect and repair Dify-owned KnowledgeFS control-space state."""
@knowledge_fs_control_space.command("dry-run")
@click.option("--tenant-id", default=None)
def dry_run(tenant_id: str | None) -> None:
report = _management_service().dry_run(tenant_id=tenant_id)
click.echo(json.dumps(report._asdict(), sort_keys=True))
@knowledge_fs_control_space.command("inventory")
@click.option("--input", "input_path", type=click.Path(path_type=Path, exists=True, dir_okay=False), required=True)
@click.option("--apply", is_flag=True, default=False, help="Create ledgers; omitted means read-only inventory.")
def inventory(input_path: Path, apply: bool) -> None:
"""Validate strict Workspace inventory JSONL and optionally create ledgers."""
service = _cutover_service()
for payload in _read_jsonl(input_path, WorkspaceInventoryInput):
report = _operator_call(partial(service.inventory, payload, apply=apply))
_echo_json(report._asdict())
@knowledge_fs_control_space.command("register")
@click.option("--tenant-id", required=True)
@click.option("--owner-account-id", required=True)
@click.option("--provisioning-key", required=True)
@click.option("--knowledge-space-id", required=True)
@click.option("--knowledge-space-revision", type=click.IntRange(min=0), required=True)
def register(
tenant_id: str,
owner_account_id: str,
provisioning_key: str,
knowledge_space_id: str,
knowledge_space_revision: int,
) -> None:
control_space, replayed = _management_service().register(
KnowledgeFSControlSpaceRegistration(
tenant_id,
owner_account_id,
provisioning_key,
knowledge_space_id,
knowledge_space_revision,
)
)
click.echo(json.dumps({"control_space_id": control_space.id, "replayed": replayed}, sort_keys=True))
@knowledge_fs_control_space.command("backfill")
@click.option("--input", "input_path", type=click.Path(path_type=Path, exists=True, dir_okay=False), required=True)
@click.option("--apply", is_flag=True, default=False, help="Persist registrations; omitted means dry-run.")
def backfill(input_path: Path, apply: bool) -> None:
"""Backfill strict Workspace inventory JSONL; dry-run unless --apply is explicit."""
service = _cutover_service()
for payload in _read_jsonl(input_path, WorkspaceInventoryInput):
report = _operator_call(partial(service.backfill, payload, apply=apply))
_echo_json(report._asdict())
@knowledge_fs_control_space.command("quarantine-resolve")
@click.option("--input", "input_path", type=click.Path(path_type=Path, exists=True, dir_okay=False), required=True)
@click.option("--apply", is_flag=True, default=False, help="Persist resolutions; omitted means dry-run.")
def quarantine_resolve(input_path: Path, apply: bool) -> None:
"""Resolve strict tenant-scoped quarantine JSONL with immutable operator evidence."""
service = _cutover_service()
for payload in _read_jsonl(input_path, QuarantineResolutionInput):
report = _operator_call(partial(service.resolve_quarantine, payload, apply=apply))
_echo_json(report._asdict())
@knowledge_fs_control_space.command("shadow-start")
@click.option("--tenant-id", required=True)
@click.option("--expected-cas-version", type=click.IntRange(min=0), required=True)
@click.option("--at", "started_at", default=None, help="Optional explicit timezone-aware shadow start.")
def shadow_start(tenant_id: str, expected_cas_version: int, started_at: str | None) -> None:
service = _cutover_service()
_operator_call(
lambda: service.begin_shadow(
tenant_id=tenant_id,
expected_cas_version=expected_cas_version,
started_at=_parse_timestamp(started_at) if started_at is not None else None,
)
)
_echo_json(service.status(tenant_id=tenant_id))
@knowledge_fs_control_space.command("shadow-report")
@click.option("--input", "input_path", type=click.Path(path_type=Path, exists=True, dir_okay=False), required=True)
@click.option("--apply", is_flag=True, default=False, help="Persist observations; omitted means dry-run.")
def shadow_report(input_path: Path, apply: bool) -> None:
observations = _read_jsonl(input_path, ShadowAuthorizationObservationInput)
report = _operator_call(lambda: _cutover_service().record_shadow_report(observations, apply=apply))
_echo_json(report._asdict())
@knowledge_fs_control_space.command("shadow-complete")
@click.option("--input", "input_path", type=click.Path(path_type=Path, exists=True, dir_okay=False), required=True)
@click.option("--apply", is_flag=True, default=False, help="Persist completion; omitted means dry-run.")
def shadow_complete(input_path: Path, apply: bool) -> None:
payload = _read_one_jsonl(input_path, ShadowCompletionInput)
report = _operator_call(lambda: _cutover_service().complete_shadow(payload, apply=apply))
_echo_json(report._asdict())
@knowledge_fs_control_space.command("issue-approve")
@click.option("--tenant-id", required=True)
@click.option("--issue-key", required=True)
@click.option("--account-id", required=True)
@click.option("--at", "approved_at", required=True)
def issue_approve(tenant_id: str, issue_key: str, account_id: str, approved_at: str) -> None:
_operator_call(
lambda: _cutover_service().approve_issue_fail_closed(
tenant_id=tenant_id,
issue_key=issue_key,
account_id=account_id,
approved_at=_parse_timestamp(approved_at),
)
)
_echo_json(_cutover_service().status(tenant_id=tenant_id))
@knowledge_fs_control_space.command("issue-resolve")
@click.option("--tenant-id", required=True)
@click.option("--issue-key", required=True)
@click.option("--account-id", required=True)
@click.option("--at", "resolved_at", required=True)
def issue_resolve(tenant_id: str, issue_key: str, account_id: str, resolved_at: str) -> None:
_operator_call(
lambda: _cutover_service().resolve_issue(
tenant_id=tenant_id,
issue_key=issue_key,
account_id=account_id,
resolved_at=_parse_timestamp(resolved_at),
)
)
_echo_json(_cutover_service().status(tenant_id=tenant_id))
@knowledge_fs_control_space.command("shadow-approve")
@click.option("--tenant-id", required=True)
@click.option("--diff-key", required=True)
@click.option("--account-id", required=True)
@click.option("--at", "approved_at", required=True)
def shadow_approve(tenant_id: str, diff_key: str, account_id: str, approved_at: str) -> None:
_operator_call(
lambda: _cutover_service().approve_shadow_diff(
tenant_id=tenant_id,
diff_key=diff_key,
account_id=account_id,
approved_at=_parse_timestamp(approved_at),
)
)
_echo_json(_cutover_service().status(tenant_id=tenant_id))
@knowledge_fs_control_space.command("shadow-resolve")
@click.option("--tenant-id", required=True)
@click.option("--diff-key", required=True)
@click.option("--account-id", required=True)
@click.option("--at", "resolved_at", required=True)
def shadow_resolve(tenant_id: str, diff_key: str, account_id: str, resolved_at: str) -> None:
_operator_call(
lambda: _cutover_service().resolve_shadow_diff(
tenant_id=tenant_id,
diff_key=diff_key,
account_id=account_id,
resolved_at=_parse_timestamp(resolved_at),
)
)
_echo_json(_cutover_service().status(tenant_id=tenant_id))
@knowledge_fs_control_space.command("legacy-dashboard")
@click.option("--tenant-id", required=True)
@click.option("--input", "input_path", type=click.Path(path_type=Path, exists=True, dir_okay=False), required=True)
@click.option("--checked-at", required=True)
@click.option("--expected-cas-version", type=click.IntRange(min=0), default=None)
@click.option("--apply", is_flag=True, default=False, help="Persist gate evidence; omitted means read-only report.")
def legacy_dashboard(
tenant_id: str,
input_path: Path,
checked_at: str,
expected_cas_version: int | None,
apply: bool,
) -> None:
dependencies = _read_jsonl(input_path, LegacyDependencyInput, allow_empty=True)
report = _operator_call(
lambda: _cutover_service().legacy_dependency_dashboard(
tenant_id=tenant_id,
dependencies=dependencies,
expected_cas_version=expected_cas_version,
checked_at=_parse_timestamp(checked_at),
apply=apply,
)
)
_echo_json(report._asdict())
@knowledge_fs_control_space.command("legacy-check")
@click.option("--tenant-id", required=True)
def legacy_check(tenant_id: str) -> None:
status_report = _operator_call(lambda: _cutover_service().status(tenant_id=tenant_id))
passed = (
bool(status_report["legacy_dependency_ready"])
and status_report["open_issues"] == 0
and status_report["unresolved_cutover_quarantine"] == 0
)
_echo_json({"tenant_id": tenant_id, "passed": passed, "status": status_report})
if not passed:
raise click.exceptions.Exit(1)
@knowledge_fs_control_space.command("freeze")
@click.option("--tenant-id", required=True)
@click.option("--expected-cas-version", type=click.IntRange(min=0), required=True)
@click.option("--at", "freeze_at", required=True)
def freeze(tenant_id: str, expected_cas_version: int, freeze_at: str) -> None:
service = _cutover_service()
_operator_call(
lambda: service.freeze(
tenant_id=tenant_id,
expected_cas_version=expected_cas_version,
freeze_at=_parse_timestamp(freeze_at),
)
)
_echo_json(service.status(tenant_id=tenant_id))
@knowledge_fs_control_space.command("final-delta")
@click.option("--input", "input_path", type=click.Path(path_type=Path, exists=True, dir_okay=False), required=True)
def final_delta(input_path: Path) -> None:
payload = _read_one_jsonl(input_path, FinalDeltaInput)
service = _cutover_service()
_operator_call(lambda: service.apply_final_delta(payload))
_echo_json(service.status(tenant_id=str(payload.tenant_id)))
@knowledge_fs_control_space.command("cutover")
@click.option("--tenant-id", required=True)
@click.option("--expected-cas-version", type=click.IntRange(min=0), required=True)
@click.option("--at", "cutover_at", required=True)
@click.option("--rollback-cutoff-at", required=True)
def cutover(tenant_id: str, expected_cas_version: int, cutover_at: str, rollback_cutoff_at: str) -> None:
service = _cutover_service()
_operator_call(
lambda: service.cutover(
tenant_id=tenant_id,
expected_cas_version=expected_cas_version,
cutover_at=_parse_timestamp(cutover_at),
rollback_cutoff_at=_parse_timestamp(rollback_cutoff_at),
)
)
_echo_json(service.status(tenant_id=tenant_id))
@knowledge_fs_control_space.command("smoke")
@click.option("--tenant-id", required=True)
@click.option("--expected-cas-version", type=click.IntRange(min=0), required=True)
@click.option("--input", "input_path", type=click.Path(path_type=Path, exists=True, dir_okay=False), required=True)
def smoke(tenant_id: str, expected_cas_version: int, input_path: Path) -> None:
results = _read_one_jsonl(input_path, CutoverSmokeResultsInput)
service = _cutover_service()
_operator_call(
lambda: service.record_smoke_results(
tenant_id=tenant_id,
expected_cas_version=expected_cas_version,
results=results,
)
)
_echo_json(service.status(tenant_id=tenant_id))
@knowledge_fs_control_space.command("observe")
@click.option("--tenant-id", required=True)
@click.option("--expected-cas-version", type=click.IntRange(min=0), required=True)
@click.option("--started-at", default=None)
@click.option("--window-ends-at", default=None)
@click.option("--maximum-task-expires-at", default=None)
@click.option("--observed-at", default=None)
def observe(
tenant_id: str,
expected_cas_version: int,
started_at: str | None,
window_ends_at: str | None,
maximum_task_expires_at: str | None,
observed_at: str | None,
) -> None:
service = _cutover_service()
if observed_at is not None:
if any(value is not None for value in (started_at, window_ends_at, maximum_task_expires_at)):
raise click.UsageError("--observed-at cannot be combined with observation start options")
_operator_call(
lambda: service.complete_observation(
tenant_id=tenant_id,
expected_cas_version=expected_cas_version,
observed_at=_parse_timestamp(observed_at),
)
)
else:
if started_at is None or window_ends_at is None or maximum_task_expires_at is None:
raise click.UsageError(
"observation start requires --started-at, --window-ends-at, and --maximum-task-expires-at"
)
_operator_call(
lambda: service.begin_observation(
tenant_id=tenant_id,
expected_cas_version=expected_cas_version,
started_at=_parse_timestamp(started_at),
window_ends_at=_parse_timestamp(window_ends_at),
maximum_task_expires_at=_parse_timestamp(maximum_task_expires_at),
)
)
_echo_json(service.status(tenant_id=tenant_id))
@knowledge_fs_control_space.command("rollback")
@click.option("--tenant-id", required=True)
@click.option("--expected-cas-version", type=click.IntRange(min=0), required=True)
@click.option("--at", "rolled_back_at", required=True)
def rollback(tenant_id: str, expected_cas_version: int, rolled_back_at: str) -> None:
service = _cutover_service()
_operator_call(
lambda: service.rollback(
tenant_id=tenant_id,
expected_cas_version=expected_cas_version,
rolled_back_at=_parse_timestamp(rolled_back_at),
)
)
_echo_json(service.status(tenant_id=tenant_id))
@knowledge_fs_control_space.command("status")
@click.option("--tenant-id", required=True)
def status(tenant_id: str) -> None:
_echo_json(_operator_call(lambda: _cutover_service().status(tenant_id=tenant_id)))
@knowledge_fs_control_space.command("greenfield-initialize")
@click.option("--tenant-id", required=True)
def greenfield_initialize(tenant_id: str) -> None:
"""Idempotently initialize one Workspace that has no KnowledgeFS state."""
_operator_call(lambda: _greenfield_initializer().ensure_initialized(tenant_id=tenant_id))
_echo_json(_operator_call(lambda: _cutover_service().status(tenant_id=tenant_id)))
@knowledge_fs_control_space.command("cleanup-request")
@click.option("--input", "input_path", type=click.Path(path_type=Path, exists=True, dir_okay=False), required=True)
@click.option("--apply", is_flag=True, default=False, help="Persist readiness evidence; omitted means dry-run.")
def cleanup_request(input_path: Path, apply: bool) -> None:
payload = _read_one_jsonl(input_path, CleanupReadinessEvidenceInput)
report = _cleanup_call(lambda: _cleanup_service().request_cleanup(payload, apply=apply))
_echo_json(report._asdict())
@knowledge_fs_control_space.command("cleanup-approve")
@click.option("--input", "input_path", type=click.Path(path_type=Path, exists=True, dir_okay=False), required=True)
@click.option("--apply", is_flag=True, default=False, help="Persist four-eyes approval; omitted means dry-run.")
def cleanup_approve(input_path: Path, apply: bool) -> None:
payload = _read_one_jsonl(input_path, CleanupApprovalInput)
report = _cleanup_call(lambda: _cleanup_service().approve_cleanup(payload, apply=apply))
_echo_json(report._asdict())
@knowledge_fs_control_space.command("cleanup-start")
@click.option("--input", "input_path", type=click.Path(path_type=Path, exists=True, dir_okay=False), required=True)
@click.option("--apply", is_flag=True, default=False, help="Persist the irreversible fence; never runs deletion.")
@click.option(
"--acknowledge-irreversible",
is_flag=True,
default=False,
help="Required with --apply; confirms rollback will be permanently closed.",
)
def cleanup_start(input_path: Path, apply: bool, acknowledge_irreversible: bool) -> None:
payload = _read_one_jsonl(input_path, CleanupStartInput)
if apply and not acknowledge_irreversible:
raise click.UsageError("--apply requires --acknowledge-irreversible")
report = _cleanup_call(lambda: _cleanup_service().start_cleanup(payload, apply=apply))
_echo_json(report._asdict())
@knowledge_fs_control_space.command("cleanup-complete")
@click.option("--input", "input_path", type=click.Path(path_type=Path, exists=True, dir_okay=False), required=True)
@click.option("--apply", is_flag=True, default=False, help="Persist externally verified cleanup completion.")
@click.option(
"--acknowledge-executed",
is_flag=True,
default=False,
help="Required with --apply; confirms the reviewed destructive bundle already executed.",
)
def cleanup_complete(input_path: Path, apply: bool, acknowledge_executed: bool) -> None:
payload = _read_one_jsonl(input_path, CleanupCompletionEvidenceInput)
if apply and not acknowledge_executed:
raise click.UsageError("--apply requires --acknowledge-executed")
report = _cleanup_call(lambda: _cleanup_service().complete_cleanup(payload, apply=apply))
_echo_json(report._asdict())
@knowledge_fs_control_space.command("cleanup-status")
@click.option("--tenant-id", required=True)
@click.option("--request-id", required=True)
def cleanup_status(tenant_id: str, request_id: str) -> None:
_echo_json(_cleanup_call(lambda: _cleanup_service().status(tenant_id=tenant_id, request_id=request_id)))
@knowledge_fs_control_space.command("repair")
@click.option("--tenant-id", required=True)
@click.option("--control-space-id", required=True)
@click.option("--expected-resource-version", type=click.IntRange(min=0), required=True)
@click.option("--knowledge-space-id", required=True)
@click.option("--knowledge-space-revision", type=click.IntRange(min=0), required=True)
def repair(
tenant_id: str,
control_space_id: str,
expected_resource_version: int,
knowledge_space_id: str,
knowledge_space_revision: int,
) -> None:
control_space = _management_service().repair_registration(
tenant_id=tenant_id,
control_space_id=control_space_id,
expected_resource_version=expected_resource_version,
knowledge_space_id=knowledge_space_id,
knowledge_space_revision=knowledge_space_revision,
)
click.echo(json.dumps({"control_space_id": control_space.id, "state": control_space.state.value}, sort_keys=True))
@knowledge_fs_control_space.command("orphan-report")
@click.option("--limit", type=click.IntRange(min=1, max=10_000), default=500, show_default=True)
def orphan_report(limit: int) -> None:
report = KnowledgeFSOrphanReconciler(
session_factory.get_session_maker(),
get_knowledge_fs_lifecycle_remote(),
).reconcile(limit=limit, apply_repairs=False)
click.echo(json.dumps(report._asdict(), sort_keys=True))
@knowledge_fs_control_space.command("workspace-delete-request")
@click.option("--tenant-id", required=True)
@click.option("--apply", is_flag=True, default=False, help="Persist durable deletion intents; omitted means dry-run.")
def workspace_delete_request(tenant_id: str, apply: bool) -> None:
"""Route every KnowledgeFS Space through the canonical lifecycle deletion path."""
if not apply:
report = _management_service().dry_run(tenant_id=tenant_id)
_echo_json(
{
"apply": False,
"by_state": report.by_state,
"tenant_id": tenant_id,
"total": report.total,
}
)
return
results = _lifecycle_call(lambda: _lifecycle_service().request_workspace_cleanup(tenant_id=tenant_id))
_echo_json(
{
"apply": True,
"control_space_ids": [result.control_space.id for result in results],
"operation_ids": [result.outbox.operation_id for result in results if result.outbox is not None],
"tenant_id": tenant_id,
}
)
@knowledge_fs_control_space.command("workspace-delete-finalize")
@click.option("--tenant-id", required=True)
@click.option("--apply", is_flag=True, default=False, help="Purge terminal local control-plane rows.")
@click.option(
"--acknowledge-control-plane-purge",
is_flag=True,
default=False,
help="Required with --apply after every remote Space has reached deleted.",
)
def workspace_delete_finalize(tenant_id: str, apply: bool, acknowledge_control_plane_purge: bool) -> None:
"""Release the Workspace FK only after all remote deletions are terminal."""
service = _lifecycle_service()
if not apply:
_lifecycle_call(lambda: service.assert_workspace_deletion_allowed(tenant_id=tenant_id))
_echo_json({"apply": False, "ready": True, "tenant_id": tenant_id})
return
if not acknowledge_control_plane_purge:
raise click.UsageError("--apply requires --acknowledge-control-plane-purge")
deleted = _lifecycle_call(lambda: service.finalize_workspace_deletion(tenant_id=tenant_id))
_echo_json({"apply": True, "purged_control_spaces": deleted, "tenant_id": tenant_id})
def _read_jsonl[InputT: BaseModel](
input_path: Path, input_type: type[InputT], *, allow_empty: bool = False
) -> tuple[InputT, ...]:
records: list[InputT] = []
for line_number, line in enumerate(input_path.read_text(encoding="utf-8").splitlines(), start=1):
if not line.strip():
continue
try:
records.append(input_type.model_validate_json(line))
except ValidationError as exc:
raise click.ClickException(f"invalid strict JSONL at line {line_number}: {exc}") from exc
if not records and not allow_empty:
raise click.ClickException("strict JSONL input must contain at least one record")
return tuple(records)
def _read_one_jsonl[InputT: BaseModel](input_path: Path, input_type: type[InputT]) -> InputT:
records = _read_jsonl(input_path, input_type)
if len(records) != 1:
raise click.ClickException("this command requires exactly one JSONL record")
return records[0]
def _parse_timestamp(value: str) -> datetime:
try:
parsed = datetime.fromisoformat(value)
except ValueError as exc:
raise click.ClickException(f"invalid ISO-8601 timestamp: {value}") from exc
if parsed.tzinfo is None:
raise click.ClickException("operator timestamps must include an explicit timezone")
return parsed
def _operator_call[ResultT](operation: Callable[[], ResultT]) -> ResultT:
try:
return operation()
except KnowledgeFSCutoverError as exc:
raise click.ClickException(str(exc)) from exc
def _cleanup_call[ResultT](operation: Callable[[], ResultT]) -> ResultT:
try:
return operation()
except KnowledgeFSCleanupError as exc:
raise click.ClickException(str(exc)) from exc
def _lifecycle_call[ResultT](operation: Callable[[], ResultT]) -> ResultT:
try:
return operation()
except KnowledgeFSControlSpaceLifecycleError as exc:
raise click.ClickException(str(exc)) from exc
def _echo_json(payload: object) -> None:
click.echo(json.dumps(payload, default=str, sort_keys=True))
def _management_service() -> KnowledgeFSControlSpaceManagementService:
return KnowledgeFSControlSpaceManagementService(session_factory.get_session_maker())
def _cutover_service() -> KnowledgeFSWorkspaceCutoverService:
return KnowledgeFSWorkspaceCutoverService(
session_factory.get_session_maker(),
remote_factory=get_knowledge_fs_lifecycle_remote,
)
def _greenfield_initializer() -> KnowledgeFSWorkspaceGreenfieldInitializer:
return KnowledgeFSWorkspaceGreenfieldInitializer(
session_factory.get_session_maker(),
cutover=_cutover_service(),
)
def _cleanup_service() -> KnowledgeFSCleanupService:
return KnowledgeFSCleanupService(session_factory.get_session_maker())
def _lifecycle_service() -> KnowledgeFSControlSpaceCommandService:
return KnowledgeFSControlSpaceCommandService(session_factory.get_session_maker())
__all__ = ["knowledge_fs_control_space"]
+2 -3
View File
@@ -6,12 +6,11 @@ from sqlalchemy import delete, select, update
from sqlalchemy.orm import sessionmaker
from configs import dify_config
from enums.deployment_edition import DeploymentEdition
from events.app_event import app_was_created
from extensions.ext_database import db
from extensions.ext_redis import redis_client
from libs.db_migration_lock import DbMigrationAutoRenewLock
from libs.rsa import generate_key_pair
from libs.key_providers import generate_key_pair
from models import Tenant
from models.model import App, AppMode, Conversation
from models.provider import Provider, ProviderModel
@@ -43,7 +42,7 @@ def reset_encrypt_key_pair():
After the reset, all LLM credentials will become invalid, requiring re-entry.
Only support SELF_HOSTED mode.
"""
if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD:
if dify_config.EDITION != "SELF_HOSTED":
click.echo(click.style("This command is only for SELF_HOSTED installations.", fg="red"))
return
with sessionmaker(db.engine, expire_on_commit=False).begin() as session:
+6
View File
@@ -25,6 +25,12 @@ class EnterpriseFeatureConfig(BaseSettings):
default=False,
)
ENABLE_LICENSE_EXPIRY_NOTICE: bool = Field(
description="Show the license expiry countdown badge in the console when the license is expiring. "
"Disable to hide the badge; license status and all enforcement remain unaffected.",
default=True,
)
ENTERPRISE_REQUEST_TIMEOUT: int = Field(
ge=1, description="Maximum timeout in seconds for enterprise requests", default=5
)
+76 -20
View File
@@ -1,30 +1,63 @@
"""Configuration for the optional KnowledgeFS Console bridge."""
"""Configuration for the optional KnowledgeFS control-plane integration."""
from ipaddress import ip_address
from urllib.parse import urlsplit
from pydantic import Field, PositiveFloat, SecretStr, field_validator, model_validator
from pydantic import Field, PositiveFloat, PositiveInt, SecretStr, field_validator, model_validator
from pydantic_settings import BaseSettings
class KnowledgeFSConfig(BaseSettings):
"""Server-only settings for the KnowledgeFS production connection."""
"""Server-only KnowledgeFS connection and rollout settings."""
KNOWLEDGE_FS_ENABLED: bool = Field(
default=False,
description="Enable the private KnowledgeFS Console bridge.",
description="Enable the KnowledgeFS control-plane product routes.",
)
KNOWLEDGE_FS_LIFECYCLE_WORKER_ENABLED: bool = Field(
default=False,
description="Enable delivery of durable KnowledgeFS lifecycle commands after every rollout gate is ready.",
)
KNOWLEDGE_FS_INTEGRATED_PROVISION_READY: bool = Field(
default=False,
description="Confirm that the Capability-v2 integrated provision route is deployed and verified.",
)
KNOWLEDGE_FS_LEGACY_ACL_FREEZE_READY: bool = Field(
default=False,
description="Confirm that legacy KFS ACL mutation is frozen for integrated mode.",
)
KNOWLEDGE_FS_LIFECYCLE_POLL_INTERVAL_SECONDS: PositiveInt = Field(default=15, le=300)
KNOWLEDGE_FS_LIFECYCLE_LEASE_SECONDS: PositiveInt = Field(default=60, le=600)
KNOWLEDGE_FS_LIFECYCLE_BATCH_SIZE: PositiveInt = Field(default=25, le=1_000)
KNOWLEDGE_FS_BASE_URL: str | None = Field(default=None, description="KnowledgeFS gateway base URL.")
KNOWLEDGE_FS_JWT_SECRET: SecretStr | None = Field(
default=None,
min_length=32,
description="Shared secret used to sign short-lived KnowledgeFS service JWTs.",
KNOWLEDGE_FS_CAPABILITY_V2_ENABLED: bool = Field(
default=False,
description="Prepare resource-scoped Capability v2 issuance; disabled until rollout approval.",
)
KNOWLEDGE_FS_SSE_READ_TIMEOUT_SECONDS: PositiveFloat = Field(default=300.0, le=3600.0, allow_inf_nan=False)
KNOWLEDGE_FS_CAPABILITY_V2_SIGNING_KID: str | None = Field(
default=None,
description="Identifier for the current asymmetric Capability v2 signing key.",
)
KNOWLEDGE_FS_CAPABILITY_V2_PRIVATE_KEY_PEM: SecretStr | None = Field(
default=None,
description="Server-only PEM for the current Capability v2 RSA signing key.",
)
KNOWLEDGE_FS_CAPABILITY_V2_PREVIOUS_PUBLIC_JWKS: str | None = Field(
default=None,
description="Optional public-only JWKS JSON retained during key rotation overlap.",
)
KNOWLEDGE_FS_CAPABILITY_V2_ISSUER: str = Field(default="dify-control-plane", min_length=1)
KNOWLEDGE_FS_CAPABILITY_V2_AUDIENCE: str = Field(default="knowledge-fs", min_length=1)
KNOWLEDGE_FS_CAPABILITY_V2_MAX_TTL_SECONDS: PositiveInt = Field(default=60, le=60)
KNOWLEDGE_FS_JWKS_CACHE_MAX_AGE_SECONDS: PositiveInt = Field(default=300, le=86_400)
KNOWLEDGE_FS_PRODUCT_MAX_RESPONSE_BYTES: PositiveInt = Field(default=4 * 1024 * 1024, le=16 * 1024 * 1024)
KNOWLEDGE_FS_TIMEOUT_SECONDS: PositiveFloat = Field(default=10.0, le=60.0, allow_inf_nan=False)
@field_validator(
"KNOWLEDGE_FS_BASE_URL",
"KNOWLEDGE_FS_JWT_SECRET",
"KNOWLEDGE_FS_CAPABILITY_V2_SIGNING_KID",
"KNOWLEDGE_FS_CAPABILITY_V2_PRIVATE_KEY_PEM",
"KNOWLEDGE_FS_CAPABILITY_V2_PREVIOUS_PUBLIC_JWKS",
mode="before",
)
@classmethod
@@ -40,25 +73,48 @@ class KnowledgeFSConfig(BaseSettings):
@field_validator("KNOWLEDGE_FS_BASE_URL")
@classmethod
def validate_base_url(cls, value: str | None) -> str | None:
return cls._validate_origin(value, name="KNOWLEDGE_FS_BASE_URL")
@classmethod
def _validate_origin(cls, value: str | None, *, name: str) -> str | None:
if value is None:
return None
parsed = urlsplit(value)
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
raise ValueError("KNOWLEDGE_FS_BASE_URL must be an absolute HTTP(S) URL")
raise ValueError(f"{name} must be an absolute HTTP(S) URL")
try:
_ = parsed.port
except ValueError as exc:
raise ValueError("KNOWLEDGE_FS_BASE_URL must include a valid port") from exc
if parsed.username or parsed.password or parsed.query or parsed.fragment:
raise ValueError("KNOWLEDGE_FS_BASE_URL must not include credentials, query, or fragment")
raise ValueError(f"{name} must include a valid port") from exc
if parsed.username or parsed.password or parsed.query or parsed.fragment or parsed.path not in {"", "/"}:
raise ValueError(f"{name} must be an origin without credentials, path, query, or fragment")
return value.rstrip("/")
@model_validator(mode="after")
def validate_enabled_connection(self) -> "KnowledgeFSConfig":
if not self.KNOWLEDGE_FS_ENABLED:
return self
if bool(self.KNOWLEDGE_FS_BASE_URL) != bool(self.KNOWLEDGE_FS_JWT_SECRET):
raise ValueError("KNOWLEDGE_FS_BASE_URL and KNOWLEDGE_FS_JWT_SECRET must be configured together")
if not self.KNOWLEDGE_FS_BASE_URL:
raise ValueError("KnowledgeFS connection settings are required when the integration is enabled")
if str(getattr(self, "DEPLOY_ENV", "")).strip().upper() == "PRODUCTION":
if self.KNOWLEDGE_FS_BASE_URL and not self._is_secure_or_loopback_origin(self.KNOWLEDGE_FS_BASE_URL):
raise ValueError("KNOWLEDGE_FS_BASE_URL must use HTTPS in production unless it targets loopback")
if self.KNOWLEDGE_FS_ENABLED:
if not self.KNOWLEDGE_FS_BASE_URL:
raise ValueError("KnowledgeFS base URL is required when the integration is enabled")
if not self.KNOWLEDGE_FS_CAPABILITY_V2_ENABLED:
raise ValueError("KnowledgeFS product routes require Capability v2 when enabled")
if self.KNOWLEDGE_FS_CAPABILITY_V2_ENABLED and not (
self.KNOWLEDGE_FS_CAPABILITY_V2_SIGNING_KID and self.KNOWLEDGE_FS_CAPABILITY_V2_PRIVATE_KEY_PEM
):
raise ValueError("Capability v2 signing kid and private key are required when issuance is enabled")
return self
@staticmethod
def _is_secure_or_loopback_origin(value: str) -> bool:
parsed = urlsplit(value)
if parsed.scheme == "https":
return True
hostname = (parsed.hostname or "").rstrip(".").lower()
if hostname == "localhost":
return True
try:
return ip_address(hostname).is_loopback
except ValueError:
return False
+5
View File
@@ -450,6 +450,11 @@ class FileUploadConfig(BaseSettings):
default=15,
)
KNOWLEDGE_UPLOAD_FILE_SIZE_LIMIT_FOR_PAID_PLAN: NonNegativeInt = Field(
description="Maximum allowed file size for knowledge uploads on paid cloud plans in megabytes",
default=15,
)
UPLOAD_FILE_BATCH_LIMIT: NonNegativeInt = Field(
description="Maximum number of files allowed in a single upload batch",
default=5,
@@ -451,6 +451,12 @@ class HostedFetchAppTemplateConfig(BaseSettings):
default="https://tmpl.dify.ai",
)
HOSTED_FETCH_APP_TEMPLATES_CACHE_TTL: int = Field(
description="TTL in seconds for caching remote app template HTTP responses. 0 disables caching.",
default=600,
ge=0,
)
class HostedFetchPipelineTemplateConfig(BaseSettings):
"""
+20
View File
@@ -7,6 +7,7 @@ from pydantic_settings import BaseSettings
from .cache.redis_config import RedisConfig
from .cache.redis_pubsub_config import RedisPubSubConfig
from .key_provider.azure_keyvault_config import AzureKeyVaultConfig
from .storage.aliyun_oss_storage_config import AliyunOSSStorageConfig
from .storage.amazon_s3_storage_config import S3StorageConfig
from .storage.azure_blob_storage_config import AzureBlobStorageConfig
@@ -83,6 +84,22 @@ class StorageConfig(BaseSettings):
)
_VALID_KEY_PROVIDER_TYPE = Literal[
"local",
"azure-keyvault",
]
class KeyProviderConfig(BaseSettings):
KEY_PROVIDER_TYPE: _VALID_KEY_PROVIDER_TYPE = Field(
description="Key provider used to encrypt/decrypt tenant credentials (LLM/tool provider secrets)."
" Options: 'local' (per-tenant RSA key pair, private key kept in the STORAGE_TYPE backend),"
" 'azure-keyvault' (per-tenant RSA key kept in Azure Key Vault, private key never leaves the vault)."
" Default is 'local'.",
default=cast(_VALID_KEY_PROVIDER_TYPE, "local"),
)
class VectorStoreConfig(BaseSettings):
VECTOR_STORE: str | None = Field(
description="Type of vector store to use for efficient similarity search."
@@ -359,6 +376,9 @@ class MiddlewareConfig(
KeywordStoreConfig,
RedisConfig,
RedisPubSubConfig,
# configs of the tenant credential encryption key provider
KeyProviderConfig,
AzureKeyVaultConfig,
# configs of storage and storage providers
StorageConfig,
AliyunOSSStorageConfig,
@@ -0,0 +1,38 @@
from pydantic import Field, field_validator
from pydantic_settings import BaseSettings
class AzureKeyVaultConfig(BaseSettings):
"""
Configuration settings for Azure Key Vault, used as a tenant credential encryption key provider
"""
AZURE_KEYVAULT_VAULT_URL: str | None = Field(
description="URL of the Azure Key Vault instance (e.g., 'https://<your-vault-name>.vault.azure.net')."
" Required when KEY_PROVIDER_TYPE is set to 'azure-keyvault'. Authentication uses"
" DefaultAzureCredential (managed identity, environment variables, or Azure CLI login).",
default=None,
)
AZURE_KEYVAULT_KEY_SIZE: int = Field(
description="RSA key size (in bits) used when Dify provisions a new per-tenant key in Azure Key Vault.",
default=2048,
)
AZURE_KEYVAULT_ROTATION_INTERVAL_DAYS: int | None = Field(
description="If set, Dify configures each newly-created per-tenant Key Vault key to auto-rotate every"
" N days (using a 'time after create' trigger, with no expiry set on generated versions)."
" Old key versions are kept forever and remain usable for decrypting credentials encrypted"
" before the rotation, so rotation requires no manual re-encryption. Leave unset (default) to"
" not configure a rotation policy and manage rotation manually in Azure. Must be at least 7 days.",
default=None,
ge=7,
)
@field_validator("AZURE_KEYVAULT_ROTATION_INTERVAL_DAYS", mode="before")
@classmethod
def _empty_string_to_none_for_rotation_interval(cls, v):
"""Allow empty string in env/.env (e.g. an unfilled template value) to mean 'unset'."""
if isinstance(v, str) and v.strip() == "":
return None
return v
@@ -32,6 +32,11 @@ class TidbOnQdrantConfig(BaseSettings):
default=6334,
)
TIDB_ON_QDRANT_ESTIMATED_STORAGE_LIMITS_MB: str = Field(
description="Cloud pre-write thresholds for projected TiDB vector storage usage, in plan:MB pairs.",
default="sandbox:60,professional:6400,team:25600",
)
TIDB_PUBLIC_KEY: str | None = Field(
description="Tidb account public key",
default=None,
@@ -31,3 +31,8 @@ class TiDBVectorConfig(BaseSettings):
description="Name of the TiDB Vector database to connect to",
default=None,
)
TIDB_VECTOR_ENABLE_FULLTEXT_SEARCH: bool = Field(
description="Enable TiDB Vector full-text and hybrid search features",
default=False,
)
+2
View File
@@ -77,3 +77,5 @@ COOKIE_NAME_PASSPORT = "passport"
HEADER_NAME_CSRF_TOKEN = "X-CSRF-Token"
HEADER_NAME_APP_CODE = "X-App-Code"
HEADER_NAME_PASSPORT = "X-App-Passport"
HEADER_NAME_IDEMPOTENCY_KEY = "Idempotency-Key"
HEADER_NAME_REQUEST_ID = "X-Request-ID"
+3 -2
View File
@@ -88,8 +88,9 @@ default_app_templates: Mapping[AppMode, Mapping] = {
AppMode.AGENT: {
"app": {
"mode": AppMode.AGENT,
"enable_site": True,
"enable_api": True,
# Public access is enabled atomically by the first successful publish.
"enable_site": False,
"enable_api": False,
},
},
}
@@ -3,6 +3,7 @@ from typing import Any
from sqlalchemy import select
from sqlalchemy.orm import Session
from core.agent.publish_visibility import agent_has_workflow_callable_active_snapshot
from core.app.apps.agent_app.app_feature_projection import merge_agent_app_features
from core.app.apps.agent_app.app_variable_projection import agent_app_variables_to_user_input_form
from core.app.apps.agent_app.errors import AgentAppGeneratorError, AgentAppNotPublishedError
@@ -34,9 +35,7 @@ def get_published_agent_app_feature_dict_and_user_input_form(
)
if agent is None:
raise AgentAppGeneratorError("Agent App has no bound Agent")
# active_config_is_published means the draft has no unpublished edits; the public app
# can still read parameters from the active snapshot while a newer draft is pending.
if not agent.active_config_snapshot_id:
if not agent_has_workflow_callable_active_snapshot(session=session, agent=agent):
raise AgentAppNotPublishedError("Agent has not been published")
snapshot = session.scalar(
+28 -9
View File
@@ -234,15 +234,9 @@ def _query_param_from_property(
items = param_schema.get("items")
if isinstance(items, Mapping):
item_schema = _resolve_schema_ref(items, definitions)
item_type = item_schema.get("type")
if isinstance(item_type, str):
param_doc["items"] = {"type": item_type}
item_enum = item_schema.get("enum")
if isinstance(item_enum, list):
param_doc.setdefault("items", {})["enum"] = item_enum
item_format = item_schema.get("format")
if isinstance(item_format, str):
param_doc.setdefault("items", {})["format"] = item_format
item_doc = _query_param_item_schema(item_schema)
if item_doc:
param_doc["items"] = item_doc
enum = param_schema.get("enum")
if isinstance(enum, list):
@@ -303,6 +297,31 @@ def _query_param_from_property(
return param_doc
def _query_param_item_schema(item_schema: Mapping[str, Any]) -> dict[str, object]:
item_doc: dict[str, object] = {}
for key in ("type", "format", "pattern"):
value = item_schema.get(key)
if isinstance(value, str):
item_doc[key] = value
enum = item_schema.get("enum")
if isinstance(enum, list):
item_doc["enum"] = enum
for key in ("minLength", "maxLength"):
value = item_schema.get(key)
if isinstance(value, int):
item_doc[key] = value
for key in ("minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum", "multipleOf"):
value = item_schema.get(key)
if isinstance(value, int | float):
item_doc[key] = value
return item_doc
def _resolve_schema_ref(property_schema: Mapping[str, Any], definitions: Mapping[str, Any]) -> Mapping[str, Any]:
ref = property_schema.get("$ref")
if not isinstance(ref, str):
+3 -3
View File
@@ -152,7 +152,7 @@ def _extract_resource_id(
if resource_type == RBACResourceScope.APP:
app_id = matched_args.get("app_id")
if app_id:
return str(app_id)
return str(app_id) # pyrefly: ignore[unnecessary-type-conversion]
agent_id = matched_args.get("agent_id")
if agent_id:
@@ -161,7 +161,7 @@ def _extract_resource_id(
resource_id = matched_args.get("resource_id")
if resource_id:
return str(resource_id)
return str(resource_id) # pyrefly: ignore[unnecessary-type-conversion]
raise ValueError("Missing app_id in request path")
if resource_type == RBACResourceScope.DATASET:
@@ -174,6 +174,6 @@ def _extract_resource_id(
dataset = db.session.scalar(select(Dataset).where(Dataset.pipeline_id == str(pipeline_id)))
if not dataset:
raise NotFound("Dataset not found for pipeline")
return str(dataset.id)
return str(dataset.id) # pyrefly: ignore[unnecessary-type-conversion]
raise ValueError("Missing dataset_id or pipeline_id in request path")
raise ValueError(f"Unknown resource_type: {resource_type}")
+4 -6
View File
@@ -38,13 +38,11 @@ from . import (
feature,
human_input_form,
init_validate,
knowledge_fs_proxy,
notification,
onboarding,
ping,
setup,
spec,
version,
system,
workflow_run_archive,
)
from .agent import composer as agent_composer
@@ -127,6 +125,7 @@ from .explore import (
saved_message,
trial,
)
from .knowledge_fs import resources as knowledge_fs_resources
from .snippets import snippet_workflow, snippet_workflow_draft_variable
from .socketio import workflow as socketio_workflow
@@ -197,7 +196,7 @@ __all__ = [
"human_input_form",
"init_validate",
"installed_app",
"knowledge_fs_proxy",
"knowledge_fs_resources",
"load_balancing_config",
"login",
"mcp_server",
@@ -213,7 +212,6 @@ __all__ = [
"onboarding",
"ops_trace",
"parameter",
"ping",
"plugin",
"rag_pipeline",
"rag_pipeline_datasets",
@@ -231,11 +229,11 @@ __all__ = [
"socketio_workflow",
"spec",
"statistic",
"system",
"tags",
"tool_providers",
"trial",
"trigger_providers",
"version",
"website",
"workflow",
"workflow_app_log",
+118 -54
View File
@@ -1,6 +1,5 @@
from uuid import UUID
from flask import request
from flask_restx import Resource
from sqlalchemy.orm import Session
from werkzeug.exceptions import NotFound
@@ -14,6 +13,7 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -64,8 +64,16 @@ class WorkflowAgentComposerApi(Resource):
@with_current_tenant_id
@with_session
@get_app_model(mode=[AppMode.WORKFLOW, AppMode.ADVANCED_CHAT])
def get(self, session: Session, tenant_id: str, account_id: str, app_model: App, node_id: str):
query = WorkflowAgentComposerQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(WorkflowAgentComposerQuery)
def get(
self,
req_data: WorkflowAgentComposerQuery,
session: Session,
tenant_id: str,
account_id: str,
app_model: App,
node_id: str,
):
return dump_response(
WorkflowAgentComposerResponse,
AgentComposerService.load_workflow_composer(
@@ -74,7 +82,7 @@ class WorkflowAgentComposerApi(Resource):
app_id=app_model.id,
node_id=node_id,
account_id=account_id,
snapshot_id=query.snapshot_id,
snapshot_id=req_data.snapshot_id,
),
)
@@ -91,8 +99,16 @@ class WorkflowAgentComposerApi(Resource):
@with_current_tenant_id
@with_session
@get_app_model(mode=[AppMode.WORKFLOW, AppMode.ADVANCED_CHAT])
def put(self, session: Session, tenant_id: str, account_id: str, app_model: App, node_id: str):
payload = ComposerSavePayload.model_validate(console_ns.payload or {})
@model_validate(ComposerSavePayload)
def put(
self,
req_data: ComposerSavePayload,
session: Session,
tenant_id: str,
account_id: str,
app_model: App,
node_id: str,
):
return dump_response(
WorkflowAgentComposerResponse,
AgentComposerService.save_workflow_composer(
@@ -101,7 +117,7 @@ class WorkflowAgentComposerApi(Resource):
app_id=app_model.id,
node_id=node_id,
account_id=account_id,
payload=payload,
payload=req_data,
),
)
@@ -123,8 +139,16 @@ class WorkflowAgentComposerCopyFromRosterApi(Resource):
@with_current_tenant_id
@with_session
@get_app_model(mode=[AppMode.WORKFLOW, AppMode.ADVANCED_CHAT])
def post(self, session: Session, tenant_id: str, account_id: str, app_model: App, node_id: str):
payload = WorkflowComposerCopyFromRosterPayload.model_validate(console_ns.payload or {})
@model_validate(WorkflowComposerCopyFromRosterPayload)
def post(
self,
req_data: WorkflowComposerCopyFromRosterPayload,
session: Session,
tenant_id: str,
account_id: str,
app_model: App,
node_id: str,
):
return dump_response(
WorkflowAgentComposerResponse,
AgentComposerService.copy_workflow_composer_from_roster(
@@ -133,9 +157,9 @@ class WorkflowAgentComposerCopyFromRosterApi(Resource):
app_id=app_model.id,
node_id=node_id,
account_id=account_id,
source_agent_id=payload.source_agent_id,
source_snapshot_id=payload.source_snapshot_id,
idempotency_key=payload.idempotency_key,
source_agent_id=req_data.source_agent_id,
source_snapshot_id=req_data.source_snapshot_id,
idempotency_key=req_data.idempotency_key,
),
)
@@ -152,16 +176,16 @@ class WorkflowAgentComposerValidateApi(Resource):
@with_current_tenant_id
@with_session(write=False)
@get_app_model(mode=[AppMode.WORKFLOW, AppMode.ADVANCED_CHAT])
def post(self, session: Session, tenant_id: str, app_model: App, node_id: str):
payload = ComposerSavePayload.model_validate(console_ns.payload or {})
ComposerConfigValidator.validate_publish_payload(payload)
@model_validate(ComposerSavePayload)
def post(self, req_data: ComposerSavePayload, session: Session, tenant_id: str, app_model: App, node_id: str):
ComposerConfigValidator.validate_publish_payload(req_data)
AgentComposerService.validate_knowledge_datasets(
session=session, tenant_id=tenant_id, agent_soul=payload.agent_soul
session=session, tenant_id=tenant_id, agent_soul=req_data.agent_soul
)
findings = AgentComposerService.collect_validation_findings(
session=session,
tenant_id=tenant_id,
payload=payload,
payload=req_data,
agent_id=AgentComposerService.resolve_workflow_node_agent_id(
session=session, tenant_id=tenant_id, app_id=app_model.id, node_id=node_id
),
@@ -204,9 +228,9 @@ class WorkflowAgentComposerImpactApi(Resource):
@with_current_tenant_id
@with_session(write=False)
@get_app_model(mode=[AppMode.WORKFLOW, AppMode.ADVANCED_CHAT])
def post(self, session: Session, tenant_id: str, app_model: App, node_id: str):
payload = ComposerSavePayload.model_validate(console_ns.payload or {})
current_snapshot_id = payload.binding.current_snapshot_id if payload.binding else None
@model_validate(ComposerSavePayload)
def post(self, req_data: ComposerSavePayload, session: Session, tenant_id: str, app_model: App, node_id: str):
current_snapshot_id = req_data.binding.current_snapshot_id if req_data.binding else None
if not current_snapshot_id:
return dump_response(
AgentComposerImpactResponse, {"current_snapshot_id": None, "workflow_node_count": 0, "bindings": []}
@@ -235,8 +259,16 @@ class WorkflowAgentComposerSaveToRosterApi(Resource):
@with_current_tenant_id
@with_session
@get_app_model(mode=[AppMode.WORKFLOW, AppMode.ADVANCED_CHAT])
def post(self, session: Session, tenant_id: str, account_id: str, app_model: App, node_id: str):
payload = ComposerSavePayload.model_validate(console_ns.payload or {})
@model_validate(ComposerSavePayload)
def post(
self,
req_data: ComposerSavePayload,
session: Session,
tenant_id: str,
account_id: str,
app_model: App,
node_id: str,
):
return dump_response(
WorkflowAgentComposerResponse,
AgentComposerService.save_workflow_composer(
@@ -245,14 +277,14 @@ class WorkflowAgentComposerSaveToRosterApi(Resource):
app_id=app_model.id,
node_id=node_id,
account_id=account_id,
payload=payload,
payload=req_data,
),
)
def _require_snippet_app_id(*, session: Session, tenant_id: str, snippet_id: UUID) -> str:
snippet = SnippetService(session=session).get_snippet_by_id(
snippet_id=str(snippet_id),
snippet_id=str(snippet_id), # pyrefly: ignore[unnecessary-type-conversion]
tenant_id=tenant_id,
)
if snippet is None:
@@ -270,8 +302,16 @@ class SnippetAgentComposerApi(Resource):
@with_current_user_id
@with_current_tenant_id
@with_session
def get(self, session: Session, tenant_id: str, account_id: str, snippet_id: UUID, node_id: str):
query = WorkflowAgentComposerQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(WorkflowAgentComposerQuery)
def get(
self,
req_data: WorkflowAgentComposerQuery,
session: Session,
tenant_id: str,
account_id: str,
snippet_id: UUID,
node_id: str,
):
return dump_response(
WorkflowAgentComposerResponse,
AgentComposerService.load_workflow_composer(
@@ -280,7 +320,7 @@ class SnippetAgentComposerApi(Resource):
app_id=_require_snippet_app_id(session=session, tenant_id=tenant_id, snippet_id=snippet_id),
node_id=node_id,
account_id=account_id,
snapshot_id=query.snapshot_id,
snapshot_id=req_data.snapshot_id,
),
)
@@ -296,8 +336,16 @@ class SnippetAgentComposerApi(Resource):
@with_current_user_id
@with_current_tenant_id
@with_session
def put(self, session: Session, tenant_id: str, account_id: str, snippet_id: UUID, node_id: str):
payload = ComposerSavePayload.model_validate(console_ns.payload or {})
@model_validate(ComposerSavePayload)
def put(
self,
req_data: ComposerSavePayload,
session: Session,
tenant_id: str,
account_id: str,
snippet_id: UUID,
node_id: str,
):
return dump_response(
WorkflowAgentComposerResponse,
AgentComposerService.save_workflow_composer(
@@ -306,7 +354,7 @@ class SnippetAgentComposerApi(Resource):
app_id=_require_snippet_app_id(session=session, tenant_id=tenant_id, snippet_id=snippet_id),
node_id=node_id,
account_id=account_id,
payload=payload,
payload=req_data,
),
)
@@ -327,8 +375,16 @@ class SnippetAgentComposerCopyFromRosterApi(Resource):
@with_current_user_id
@with_current_tenant_id
@with_session
def post(self, session: Session, tenant_id: str, account_id: str, snippet_id: UUID, node_id: str):
payload = WorkflowComposerCopyFromRosterPayload.model_validate(console_ns.payload or {})
@model_validate(WorkflowComposerCopyFromRosterPayload)
def post(
self,
req_data: WorkflowComposerCopyFromRosterPayload,
session: Session,
tenant_id: str,
account_id: str,
snippet_id: UUID,
node_id: str,
):
return dump_response(
WorkflowAgentComposerResponse,
AgentComposerService.copy_workflow_composer_from_roster(
@@ -337,9 +393,9 @@ class SnippetAgentComposerCopyFromRosterApi(Resource):
app_id=_require_snippet_app_id(session=session, tenant_id=tenant_id, snippet_id=snippet_id),
node_id=node_id,
account_id=account_id,
source_agent_id=payload.source_agent_id,
source_snapshot_id=payload.source_snapshot_id,
idempotency_key=payload.idempotency_key,
source_agent_id=req_data.source_agent_id,
source_snapshot_id=req_data.source_snapshot_id,
idempotency_key=req_data.idempotency_key,
),
)
@@ -355,17 +411,17 @@ class SnippetAgentComposerValidateApi(Resource):
@account_initialization_required
@with_current_tenant_id
@with_session(write=False)
def post(self, session: Session, tenant_id: str, snippet_id: UUID, node_id: str):
@model_validate(ComposerSavePayload)
def post(self, req_data: ComposerSavePayload, session: Session, tenant_id: str, snippet_id: UUID, node_id: str):
app_id = _require_snippet_app_id(session=session, tenant_id=tenant_id, snippet_id=snippet_id)
payload = ComposerSavePayload.model_validate(console_ns.payload or {})
ComposerConfigValidator.validate_publish_payload(payload)
ComposerConfigValidator.validate_publish_payload(req_data)
AgentComposerService.validate_knowledge_datasets(
session=session, tenant_id=tenant_id, agent_soul=payload.agent_soul
session=session, tenant_id=tenant_id, agent_soul=req_data.agent_soul
)
findings = AgentComposerService.collect_validation_findings(
session=session,
tenant_id=tenant_id,
payload=payload,
payload=req_data,
agent_id=AgentComposerService.resolve_workflow_node_agent_id(
session=session,
tenant_id=tenant_id,
@@ -409,10 +465,10 @@ class SnippetAgentComposerImpactApi(Resource):
@account_initialization_required
@with_current_tenant_id
@with_session(write=False)
def post(self, session: Session, tenant_id: str, snippet_id: UUID, node_id: str):
@model_validate(ComposerSavePayload)
def post(self, req_data: ComposerSavePayload, session: Session, tenant_id: str, snippet_id: UUID, node_id: str):
_require_snippet_app_id(session=session, tenant_id=tenant_id, snippet_id=snippet_id)
payload = ComposerSavePayload.model_validate(console_ns.payload or {})
current_snapshot_id = payload.binding.current_snapshot_id if payload.binding else None
current_snapshot_id = req_data.binding.current_snapshot_id if req_data.binding else None
if not current_snapshot_id:
return dump_response(
AgentComposerImpactResponse, {"current_snapshot_id": None, "workflow_node_count": 0, "bindings": []}
@@ -444,8 +500,16 @@ class SnippetAgentComposerSaveToRosterApi(Resource):
@with_current_user_id
@with_current_tenant_id
@with_session
def post(self, session: Session, tenant_id: str, account_id: str, snippet_id: UUID, node_id: str):
payload = ComposerSavePayload.model_validate(console_ns.payload or {})
@model_validate(ComposerSavePayload)
def post(
self,
req_data: ComposerSavePayload,
session: Session,
tenant_id: str,
account_id: str,
snippet_id: UUID,
node_id: str,
):
return dump_response(
WorkflowAgentComposerResponse,
AgentComposerService.save_workflow_composer(
@@ -454,7 +518,7 @@ class SnippetAgentComposerSaveToRosterApi(Resource):
app_id=_require_snippet_app_id(session=session, tenant_id=tenant_id, snippet_id=snippet_id),
node_id=node_id,
account_id=account_id,
payload=payload,
payload=req_data,
),
)
@@ -484,8 +548,8 @@ class AgentComposerApi(Resource):
@with_current_user_id
@with_current_tenant_id
@with_session
def put(self, session: Session, tenant_id: str, account_id: str, agent_id: UUID):
payload = ComposerSavePayload.model_validate(console_ns.payload or {})
@model_validate(ComposerSavePayload)
def put(self, req_data: ComposerSavePayload, session: Session, tenant_id: str, account_id: str, agent_id: UUID):
return dump_response(
AgentAppComposerResponse,
AgentComposerService.save_agent_composer(
@@ -493,7 +557,7 @@ class AgentComposerApi(Resource):
tenant_id=tenant_id,
agent_id=str(agent_id),
account_id=account_id,
payload=payload,
payload=req_data,
),
)
@@ -509,17 +573,17 @@ class AgentComposerValidateApi(Resource):
@account_initialization_required
@with_current_tenant_id
@with_session
def post(self, session: Session, tenant_id: str, agent_id: UUID):
@model_validate(ComposerSavePayload)
def post(self, req_data: ComposerSavePayload, session: Session, tenant_id: str, agent_id: UUID):
AgentComposerService.load_agent_composer(session=session, tenant_id=tenant_id, agent_id=str(agent_id))
payload = ComposerSavePayload.model_validate(console_ns.payload or {})
ComposerConfigValidator.validate_publish_payload(payload)
ComposerConfigValidator.validate_publish_payload(req_data)
AgentComposerService.validate_knowledge_datasets(
session=session, tenant_id=tenant_id, agent_soul=payload.agent_soul
session=session, tenant_id=tenant_id, agent_soul=req_data.agent_soul
)
findings = AgentComposerService.collect_validation_findings(
session=session,
tenant_id=tenant_id,
payload=payload,
payload=req_data,
agent_id=str(agent_id),
)
return dump_response(AgentComposerValidateResponse, {"result": "success", "errors": [], **findings})
+107 -50
View File
@@ -38,11 +38,13 @@ from controllers.console.wraps import (
edit_permission_required,
enterprise_license_required,
is_admin_or_owner_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
with_current_user,
)
from core.agent.publish_visibility import agent_has_workflow_callable_active_snapshot
from fields.agent_fields import (
AgentConfigDraftSummaryResponse,
AgentConfigSnapshotDetailResponse,
@@ -139,6 +141,7 @@ class AgentApiStatusPayload(BaseModel):
class AgentApiAccessResponse(BaseModel):
access_ready: bool
enabled: bool
service_api_base_url: str
streaming_only: bool = True
@@ -257,6 +260,7 @@ class AgentAppDetailWithSite(GenericAppDetailWithSite):
debug_conversation_has_messages: bool = False
debug_conversation_message_count: int = 0
role: str | None = None
access_ready: bool = False
class AgentDebugConversationRefreshResponse(BaseModel):
@@ -400,6 +404,7 @@ def _serialize_agent_app_detail(
payload["debug_conversation_has_messages"] = message_count > 0
payload["debug_conversation_message_count"] = message_count
payload["role"] = agent.role or ""
payload["access_ready"] = agent_has_workflow_callable_active_snapshot(session=session, agent=agent)
return payload
@@ -491,10 +496,20 @@ def _agent_api_key_count(session: Session, app_id: str) -> int:
)
def _agent_app_access_ready(session: Session, app_model: App) -> bool:
agent = _agent_roster_service(session).get_app_backing_agent(
tenant_id=app_model.tenant_id,
app_id=str(app_model.id),
)
return bool(agent and agent_has_workflow_callable_active_snapshot(session=session, agent=agent))
def _serialize_agent_api_access(session: Session, app_model: App) -> dict:
base_url = app_model.api_base_url
access_ready = _agent_app_access_ready(session, app_model)
response = AgentApiAccessResponse(
enabled=bool(app_model.enable_api),
access_ready=access_ready,
enabled=bool(app_model.enable_api and access_ready),
service_api_base_url=base_url,
chat_endpoint=f"{base_url}/chat-messages",
stop_endpoint=f"{base_url}/chat-messages/{{task_id}}/stop",
@@ -593,16 +608,16 @@ class AgentAppListApi(Resource):
@with_current_user
@with_current_tenant_id
@with_session
def post(self, session: Session, current_tenant_id: str, current_user: Account):
args = AgentAppCreatePayload.model_validate(console_ns.payload)
@model_validate(AgentAppCreatePayload)
def post(self, req_data: AgentAppCreatePayload, session: Session, current_tenant_id: str, current_user: Account):
params = CreateAppParams(
name=args.name,
description=args.description,
name=req_data.name,
description=req_data.description,
mode="agent",
agent_role=args.role or "",
icon_type=args.icon_type,
icon=args.icon,
icon_background=args.icon_background,
agent_role=req_data.role or "",
icon_type=req_data.icon_type,
icon=req_data.icon,
icon_background=req_data.icon_background,
)
app = AppService().create_app(current_tenant_id, params, current_user, session=session)
@@ -635,18 +650,25 @@ class AgentAppApi(Resource):
@with_current_user
@with_current_tenant_id
@with_session
def put(self, session: Session, tenant_id: str, current_user: Account, agent_id: UUID):
@model_validate(AgentAppUpdatePayload)
def put(
self,
req_data: AgentAppUpdatePayload,
session: Session,
tenant_id: str,
current_user: Account,
agent_id: UUID,
):
app_model = _resolve_agent_app_model(session, tenant_id=tenant_id, agent_id=agent_id)
args = AgentAppUpdatePayload.model_validate(console_ns.payload)
args_dict: AppService.ArgsDict = {
"name": args.name,
"description": args.description or "",
"icon_type": args.icon_type,
"icon": args.icon or "",
"icon_background": args.icon_background or "",
"use_icon_as_answer_icon": args.use_icon_as_answer_icon or False,
"max_active_requests": args.max_active_requests or 0,
"role": args.role,
"name": req_data.name,
"description": req_data.description or "",
"icon_type": req_data.icon_type,
"icon": req_data.icon or "",
"icon_background": req_data.icon_background or "",
"use_icon_as_answer_icon": req_data.use_icon_as_answer_icon or False,
"max_active_requests": req_data.max_active_requests or 0,
"role": req_data.role,
}
updated = AppService().update_app(app_model, args_dict, session=session)
return _serialize_agent_app_detail(session, updated, current_user=current_user)
@@ -707,14 +729,21 @@ class AgentPublishApi(Resource):
@with_current_user
@with_current_tenant_id
@with_session
def post(self, session: Session, tenant_id: str, current_user: Account, agent_id: UUID):
args = AgentPublishPayload.model_validate(console_ns.payload or {})
@model_validate(AgentPublishPayload)
def post(
self,
req_data: AgentPublishPayload,
session: Session,
tenant_id: str,
current_user: Account,
agent_id: UUID,
):
return AgentComposerService.publish_agent_app_draft(
session=session,
tenant_id=tenant_id,
agent_id=str(agent_id),
account_id=current_user.id,
version_note=args.version_note,
version_note=req_data.version_note,
)
@@ -730,14 +759,21 @@ class AgentBuildDraftCheckoutApi(Resource):
@with_current_user
@with_current_tenant_id
@with_session(write=False)
def post(self, session: Session, tenant_id: str, current_user: Account, agent_id: UUID):
args = AgentBuildDraftCheckoutPayload.model_validate(console_ns.payload or {})
@model_validate(AgentBuildDraftCheckoutPayload)
def post(
self,
req_data: AgentBuildDraftCheckoutPayload,
session: Session,
tenant_id: str,
current_user: Account,
agent_id: UUID,
):
return AgentComposerService.checkout_agent_app_build_draft(
session=session,
tenant_id=tenant_id,
agent_id=str(agent_id),
account_id=current_user.id,
force=args.force,
force=req_data.force,
)
@@ -769,14 +805,21 @@ class AgentBuildDraftApi(Resource):
@with_current_user
@with_current_tenant_id
@with_session
def put(self, session: Session, tenant_id: str, current_user: Account, agent_id: UUID):
payload = ComposerSavePayload.model_validate(console_ns.payload or {})
@model_validate(ComposerSavePayload)
def put(
self,
req_data: ComposerSavePayload,
session: Session,
tenant_id: str,
current_user: Account,
agent_id: UUID,
):
return AgentComposerService.save_agent_app_build_draft(
session=session,
tenant_id=tenant_id,
agent_id=str(agent_id),
account_id=current_user.id,
payload=payload,
payload=req_data,
)
@console_ns.response(200, "Agent build draft discarded", console_ns.models[AgentSimpleResultResponse.__name__])
@@ -830,18 +873,25 @@ class AgentAppCopyApi(Resource):
@with_current_user
@with_current_tenant_id
@with_session
def post(self, session: Session, tenant_id: str, current_user: Account, agent_id: UUID):
args = AgentAppCopyPayload.model_validate(console_ns.payload or {})
@model_validate(AgentAppCopyPayload)
def post(
self,
req_data: AgentAppCopyPayload,
session: Session,
tenant_id: str,
current_user: Account,
agent_id: UUID,
):
copied_app = _agent_roster_service(session).duplicate_agent_app(
tenant_id=tenant_id,
agent_id=str(agent_id),
account=current_user,
name=args.name,
description=args.description,
role=args.role,
icon_type=args.icon_type,
icon=args.icon,
icon_background=args.icon_background,
name=req_data.name,
description=req_data.description,
role=req_data.role,
icon_type=req_data.icon_type,
icon=req_data.icon,
icon_background=req_data.icon_background,
)
return _serialize_agent_app_detail(session, copied_app, current_user=current_user), 201
@@ -873,10 +923,10 @@ class AgentApiStatusApi(Resource):
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_RELEASE_AND_VERSION)
@with_current_tenant_id
@with_session
def post(self, session: Session, tenant_id: str, agent_id: UUID):
@model_validate(AgentApiStatusPayload)
def post(self, req_data: AgentApiStatusPayload, session: Session, tenant_id: str, agent_id: UUID):
app_model = _resolve_agent_app_model(session, tenant_id=tenant_id, agent_id=agent_id)
args = AgentApiStatusPayload.model_validate(console_ns.payload)
app_model = AppService().update_app_api_status(app_model, args.enable_api, session=session)
app_model = AppService().update_app_api_status(app_model, req_data.enable_api, session=session)
return _serialize_agent_api_access(session, app_model)
@@ -944,16 +994,16 @@ class AgentInviteOptionsApi(Resource):
@account_initialization_required
@with_current_tenant_id
@with_session(write=False)
def get(self, session: Session, tenant_id: str):
query = AgentInviteOptionsQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(AgentInviteOptionsQuery)
def get(self, req_data: AgentInviteOptionsQuery, session: Session, tenant_id: str):
return dump_response(
AgentInviteOptionsResponse,
_agent_roster_service(session).list_invite_options(
tenant_id=tenant_id,
page=query.page,
limit=query.limit,
keyword=query.keyword,
app_id=query.app_id,
page=req_data.page,
limit=req_data.limit,
keyword=req_data.keyword,
app_id=req_data.app_id,
),
)
@@ -1019,7 +1069,7 @@ class AgentLogMessagesApi(Resource):
payload = _agent_observability_service(session).list_log_messages(
app=app_model,
agent_id=str(agent_id),
conversation_id=str(conversation_id),
conversation_id=str(conversation_id), # pyrefly: ignore[unnecessary-type-conversion]
params=AgentLogQueryParams(
page=query.page,
limit=query.limit,
@@ -1068,16 +1118,23 @@ class AgentStatisticsSummaryApi(Resource):
@with_current_user
@with_current_tenant_id
@with_session(write=False)
def get(self, session: Session, tenant_id: str, current_user: Account, agent_id: UUID):
@model_validate(AgentStatisticsQuery)
def get(
self,
req_data: AgentStatisticsQuery,
session: Session,
tenant_id: str,
current_user: Account,
agent_id: UUID,
):
app_model = _resolve_agent_runtime_app_model(session, tenant_id=tenant_id, agent_id=agent_id)
query = AgentStatisticsQuery.model_validate(request.args.to_dict(flat=True))
timezone = current_user.timezone or "UTC"
start, end = _parse_observability_time_range(query.start, query.end, current_user)
start, end = _parse_observability_time_range(req_data.start, req_data.end, current_user)
try:
payload = _agent_observability_service(session).get_statistics_summary(
app=app_model,
agent_id=str(agent_id),
params=AgentStatisticsQueryParams(source=query.source, start=start, end=end, timezone=timezone),
params=AgentStatisticsQueryParams(source=req_data.source, start=start, end=end, timezone=timezone),
)
except ValueError as exc:
abort(400, description=str(exc))
+10 -7
View File
@@ -21,6 +21,7 @@ from models.dataset import Dataset
from models.enums import ApiTokenType
from models.model import ApiToken, App
from services.api_token_service import ApiTokenCache
from services.app_service import AppService
from . import console_ns
from .wraps import (
@@ -103,7 +104,9 @@ class BaseApiKeyListResource(Resource):
def _create_api_key(self, resource_id: str, current_tenant_id: str, *, session: Session) -> ApiToken:
assert self.resource_id_field is not None, "resource_id_field must be set"
_get_resource(resource_id, current_tenant_id, self.resource_model, session=session)
resource = _get_resource(resource_id, current_tenant_id, self.resource_model, session=session)
if isinstance(resource, App):
AppService.ensure_agent_app_access_ready(resource, session=session)
current_key_count: int = (
session.scalar(
select(func.count(ApiToken.id)).where(
@@ -201,7 +204,7 @@ class AppApiKeyListResource(BaseApiKeyListResource):
"""Get all API keys for an app"""
return dump_response(
ApiKeyList,
self._get_api_key_list(str(resource_id), current_tenant_id, session=session),
self._get_api_key_list(str(resource_id), current_tenant_id, session=session), # pyrefly: ignore[unnecessary-type-conversion]
)
@console_ns.doc("create_app_api_key")
@@ -218,7 +221,7 @@ class AppApiKeyListResource(BaseApiKeyListResource):
"""Create a new API key for an app"""
return dump_response(
ApiKeyItem,
self._create_api_key(str(resource_id), current_tenant_id, session=session),
self._create_api_key(str(resource_id), current_tenant_id, session=session), # pyrefly: ignore[unnecessary-type-conversion]
), 201
resource_type = ApiTokenType.APP
@@ -248,7 +251,7 @@ class AppApiKeyResource(BaseApiKeyResource):
) -> tuple[str, int]:
"""Delete an API key for an app"""
self._delete_api_key(
str(resource_id),
str(resource_id), # pyrefly: ignore[unnecessary-type-conversion]
str(api_key_id),
current_tenant_id,
current_user,
@@ -273,7 +276,7 @@ class DatasetApiKeyListResource(BaseApiKeyListResource):
"""Get all API keys for a dataset"""
return dump_response(
ApiKeyList,
self._get_api_key_list(str(resource_id), current_tenant_id, session=session),
self._get_api_key_list(str(resource_id), current_tenant_id, session=session), # pyrefly: ignore[unnecessary-type-conversion]
)
@console_ns.doc("create_dataset_api_key")
@@ -289,7 +292,7 @@ class DatasetApiKeyListResource(BaseApiKeyListResource):
"""Create a new API key for a dataset"""
return dump_response(
ApiKeyItem,
self._create_api_key(str(resource_id), current_tenant_id, session=session),
self._create_api_key(str(resource_id), current_tenant_id, session=session), # pyrefly: ignore[unnecessary-type-conversion]
), 201
resource_type = ApiTokenType.DATASET
@@ -318,7 +321,7 @@ class DatasetApiKeyResource(BaseApiKeyResource):
) -> tuple[str, int]:
"""Delete an API key for a dataset"""
self._delete_api_key(
str(resource_id),
str(resource_id), # pyrefly: ignore[unnecessary-type-conversion]
str(api_key_id),
current_tenant_id,
current_user,
@@ -1,6 +1,5 @@
from typing import Any
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field
@@ -10,7 +9,7 @@ from controllers.common.schema import (
register_response_schema_models,
)
from controllers.console import console_ns
from controllers.console.wraps import account_initialization_required, setup_required
from controllers.console.wraps import account_initialization_required, model_validate, setup_required
from fields.base import ResponseModel
from libs.login import login_required
from services.advanced_prompt_template_service import AdvancedPromptTemplateArgs, AdvancedPromptTemplateService
@@ -49,12 +48,12 @@ class AdvancedPromptTemplateList(Resource):
@setup_required
@login_required
@account_initialization_required
def get(self):
args = AdvancedPromptTemplateQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(AdvancedPromptTemplateQuery)
def get(self, req_data: AdvancedPromptTemplateQuery):
prompt_args: AdvancedPromptTemplateArgs = {
"app_mode": args.app_mode,
"model_mode": args.model_mode,
"model_name": args.model_name,
"has_context": args.has_context,
"app_mode": req_data.app_mode,
"model_mode": req_data.model_mode,
"model_name": req_data.model_name,
"has_context": req_data.has_context,
}
return AdvancedPromptTemplateService.get_prompt(prompt_args)
+5 -4
View File
@@ -21,6 +21,7 @@ from controllers.console.wraps import (
RBACPermission,
RBACResourceScope,
account_initialization_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -211,12 +212,12 @@ def _upload_skill_for_app(*, session: Session, current_user: Account, app_model:
def _commit_drive_file_for_app(*, session: Session, current_user: Account, app_model: App, allow_node_id: bool = True):
payload = AgentDriveFilePayload.model_validate(console_ns.payload or {})
query = query_params_from_request(AgentDriveMutationQuery)
node_id = query.node_id if allow_node_id else None
agent_id = _resolve_agent_id(session, app_model, node_id)
if not agent_id:
return _agent_not_bound()
payload = AgentDriveFilePayload.model_validate(console_ns.payload or {})
upload_file = session.scalar(
select(UploadFile).where(
@@ -341,11 +342,11 @@ class AgentLogApi(Resource):
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
@with_session(write=False)
@get_app_model(mode=[AppMode.AGENT_CHAT])
def get(self, session: Session, app_model: App):
@model_validate(AgentLogQuery)
def get(self, req_data: AgentLogQuery, session: Session, app_model: App):
"""Get agent logs"""
args = AgentLogQuery.model_validate(request.args.to_dict(flat=True))
return AgentService.get_agent_logs(app_model, args.conversation_id, args.message_id, session)
return AgentService.get_agent_logs(app_model, req_data.conversation_id, req_data.message_id, session)
@console_ns.route("/agent/<uuid:agent_id>/skills/upload")
@@ -25,6 +25,7 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -87,14 +88,21 @@ class AgentAppFeatureConfigResource(Resource):
@with_current_user
@with_current_tenant_id
@with_session
def post(self, session: Session, tenant_id: str, current_user: Account, agent_id: UUID):
@model_validate(AgentAppFeaturesPayload)
def post(
self,
req_data: AgentAppFeaturesPayload,
session: Session,
tenant_id: str,
current_user: Account,
agent_id: UUID,
):
app_model = resolve_agent_runtime_app_model(session=session, tenant_id=tenant_id, agent_id=agent_id)
args = AgentAppFeaturesPayload.model_validate(console_ns.payload or {})
new_app_model_config = AgentAppFeatureConfigService.update_features(
app_model=app_model,
account=current_user,
config=args.model_dump(exclude_none=True),
config=req_data.model_dump(exclude_none=True),
session=session,
)
@@ -1,8 +1,8 @@
"""Console routes for Agent App and workflow Agent sandbox file access.
The API accepts product-facing Conversation, Build Draft, or Workflow Node
Execution locators and proxies list/read/upload to the agent backend's
``/sandbox`` contract.
Execution locators and proxies list/read/download to the agent backend's
``/execution-bindings/files`` contract.
"""
from __future__ import annotations
@@ -11,10 +11,8 @@ from typing import Literal
from uuid import UUID
from dify_agent.client import DifyAgentClientError, DifyAgentHTTPError, DifyAgentTimeoutError
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
from controllers.common.schema import (
query_params_from_model,
@@ -22,12 +20,12 @@ from controllers.common.schema import (
register_response_schema_models,
register_schema_models,
)
from controllers.common.session import with_session
from controllers.console import console_ns
from controllers.console.agent.app_helpers import resolve_agent_runtime_app_model
from controllers.console.app.error import AppNotFoundError
from controllers.console.app.wraps import get_app_model
from controllers.console.wraps import (
account_initialization_required,
model_validate,
setup_required,
with_current_tenant_id,
with_current_user,
@@ -43,11 +41,17 @@ from services.agent_app_sandbox_service import (
WorkflowAgentSandboxService,
)
_BINDING_PATH_DESCRIPTION = (
"Binding path: relative paths start in Workspace; exact `~` and paths beginning with `~/` start in Home; "
"`~user` is an ordinary relative path from Workspace; absolute paths remain absolute; `..` and paths outside "
"Workspace are governed by backend isolation, not a Workspace-root restriction"
)
class AgentSandboxListQuery(BaseModel):
caller_type: Literal["conversation", "build_draft"]
caller_id: str = Field(min_length=1, description="Agent App caller ID")
path: str = Field(default=".", description="Directory path relative to the sandbox workspace")
path: str = Field(default=".", description=_BINDING_PATH_DESCRIPTION)
class AgentSandboxInfoQuery(BaseModel):
@@ -58,28 +62,28 @@ class AgentSandboxInfoQuery(BaseModel):
class AgentSandboxFileQuery(BaseModel):
caller_type: Literal["conversation", "build_draft"]
caller_id: str = Field(min_length=1, description="Agent App caller ID")
path: str = Field(min_length=1, description="File path relative to the sandbox workspace")
path: str = Field(min_length=1, description=_BINDING_PATH_DESCRIPTION)
class AgentSandboxUploadPayload(BaseModel):
class AgentSandboxDownloadPayload(BaseModel):
caller_type: Literal["conversation", "build_draft"]
caller_id: str = Field(min_length=1, description="Agent App caller ID")
path: str = Field(min_length=1, description="File path relative to the sandbox workspace")
path: str = Field(min_length=1, description=_BINDING_PATH_DESCRIPTION)
class WorkflowAgentSandboxListQuery(BaseModel):
node_execution_id: str = Field(min_length=1, description="Workflow node execution ID")
path: str = Field(default=".", description="Directory path relative to the sandbox workspace")
path: str = Field(default=".", description=_BINDING_PATH_DESCRIPTION)
class WorkflowAgentSandboxFileQuery(BaseModel):
node_execution_id: str = Field(min_length=1, description="Workflow node execution ID")
path: str = Field(min_length=1, description="File path relative to the sandbox workspace")
path: str = Field(min_length=1, description=_BINDING_PATH_DESCRIPTION)
class WorkflowAgentSandboxUploadPayload(BaseModel):
class WorkflowAgentSandboxDownloadPayload(BaseModel):
node_execution_id: str = Field(min_length=1, description="Workflow node execution ID")
path: str = Field(min_length=1, description="File path relative to the sandbox workspace")
path: str = Field(min_length=1, description=_BINDING_PATH_DESCRIPTION)
class SandboxFileEntryResponse(ResponseModel):
@@ -107,21 +111,21 @@ class SandboxReadResponse(ResponseModel):
text: str | None = None
class SandboxUploadResponse(ResponseModel):
class SandboxDownloadResponse(ResponseModel):
url: str
register_schema_models(
console_ns,
AgentSandboxUploadPayload,
WorkflowAgentSandboxUploadPayload,
AgentSandboxDownloadPayload,
WorkflowAgentSandboxDownloadPayload,
)
register_response_schema_models(
console_ns,
SandboxInfoResponse,
SandboxListResponse,
SandboxReadResponse,
SandboxUploadResponse,
SandboxDownloadResponse,
)
@@ -152,14 +156,14 @@ class AgentAppSandboxInfoResource(Resource):
@account_initialization_required
@with_current_tenant_id
@with_current_user
@with_session(write=False)
def get(self, session: Session, current_user: Account, tenant_id: str, agent_id: UUID):
app_model = resolve_agent_runtime_app_model(session=session, tenant_id=tenant_id, agent_id=agent_id)
def get(self, current_user: Account, tenant_id: str, agent_id: UUID):
service = AgentAppSandboxService()
app_id = service.resolve_app_id(tenant_id=tenant_id, agent_id=str(agent_id))
query = query_params_from_request(AgentSandboxInfoQuery)
try:
result = AgentAppSandboxService().get_info(
result = service.get_info(
tenant_id=tenant_id,
app_id=app_model.id,
app_id=app_id,
agent_id=str(agent_id),
caller_type=query.caller_type,
caller_id=query.caller_id,
@@ -181,14 +185,14 @@ class AgentAppSandboxListResource(Resource):
@account_initialization_required
@with_current_tenant_id
@with_current_user
@with_session(write=False)
def get(self, session: Session, current_user: Account, tenant_id: str, agent_id: UUID):
app_model = resolve_agent_runtime_app_model(session=session, tenant_id=tenant_id, agent_id=agent_id)
def get(self, current_user: Account, tenant_id: str, agent_id: UUID):
service = AgentAppSandboxService()
app_id = service.resolve_app_id(tenant_id=tenant_id, agent_id=str(agent_id))
query = query_params_from_request(AgentSandboxListQuery)
try:
result = AgentAppSandboxService().list_files(
result = service.list_files(
tenant_id=tenant_id,
app_id=app_model.id,
app_id=app_id,
agent_id=str(agent_id),
caller_type=query.caller_type,
caller_id=query.caller_id,
@@ -211,14 +215,14 @@ class AgentAppSandboxReadResource(Resource):
@account_initialization_required
@with_current_tenant_id
@with_current_user
@with_session(write=False)
def get(self, session: Session, current_user: Account, tenant_id: str, agent_id: UUID):
app_model = resolve_agent_runtime_app_model(session=session, tenant_id=tenant_id, agent_id=agent_id)
def get(self, current_user: Account, tenant_id: str, agent_id: UUID):
service = AgentAppSandboxService()
app_id = service.resolve_app_id(tenant_id=tenant_id, agent_id=str(agent_id))
query = query_params_from_request(AgentSandboxFileQuery)
try:
result = AgentAppSandboxService().read_file(
result = service.read_file(
tenant_id=tenant_id,
app_id=app_model.id,
app_id=app_id,
agent_id=str(agent_id),
caller_type=query.caller_type,
caller_id=query.caller_id,
@@ -230,30 +234,36 @@ class AgentAppSandboxReadResource(Resource):
return result.model_dump()
@console_ns.route("/agent/<uuid:agent_id>/sandbox/files/upload")
class AgentAppSandboxUploadResource(Resource):
@console_ns.doc("upload_agent_app_sandbox_file")
@console_ns.doc(description="Upload one Agent App sandbox file and return a signed download URL")
@console_ns.expect(console_ns.models[AgentSandboxUploadPayload.__name__])
@console_ns.response(200, "Uploaded", console_ns.models[SandboxUploadResponse.__name__])
@console_ns.route("/agent/<uuid:agent_id>/sandbox/files/download")
class AgentAppSandboxDownloadResource(Resource):
@console_ns.doc("download_agent_app_sandbox_file")
@console_ns.doc(description="Create a ToolFile from one Agent App Binding file and return its download URL")
@console_ns.expect(console_ns.models[AgentSandboxDownloadPayload.__name__])
@console_ns.response(200, "Download URL returned", console_ns.models[SandboxDownloadResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@with_current_tenant_id
@with_current_user
@with_session(write=False)
def post(self, session: Session, current_user: Account, tenant_id: str, agent_id: UUID):
app_model = resolve_agent_runtime_app_model(session=session, tenant_id=tenant_id, agent_id=agent_id)
payload = AgentSandboxUploadPayload.model_validate(request.get_json(silent=True) or {})
@model_validate(AgentSandboxDownloadPayload)
def post(
self,
req_data: AgentSandboxDownloadPayload,
current_user: Account,
tenant_id: str,
agent_id: UUID,
):
service = AgentAppSandboxService()
app_id = service.resolve_app_id(tenant_id=tenant_id, agent_id=str(agent_id))
try:
result = AgentAppSandboxService().upload_file(
result = service.download_file(
tenant_id=tenant_id,
app_id=app_model.id,
app_id=app_id,
agent_id=str(agent_id),
caller_type=payload.caller_type,
caller_id=payload.caller_id,
caller_type=req_data.caller_type,
caller_id=req_data.caller_id,
account_id=current_user.id,
path=payload.path,
path=req_data.path,
)
except Exception as exc:
return _handle(exc)
@@ -333,29 +343,41 @@ class WorkflowAgentSandboxReadResource(Resource):
@console_ns.route(
"/apps/<uuid:app_id>/workflow-runs/<uuid:workflow_run_id>/agent-nodes/<string:node_id>/sandbox/files/upload"
"/apps/<uuid:app_id>/workflow-runs/<uuid:workflow_run_id>/agent-nodes/<string:node_id>/sandbox/files/download"
)
class WorkflowAgentSandboxUploadResource(Resource):
@console_ns.doc("upload_workflow_agent_sandbox_file")
@console_ns.doc(description="Upload one workflow Agent sandbox file and return a signed download URL")
@console_ns.expect(console_ns.models[WorkflowAgentSandboxUploadPayload.__name__])
@console_ns.response(200, "Uploaded", console_ns.models[SandboxUploadResponse.__name__])
class WorkflowAgentSandboxDownloadResource(Resource):
@console_ns.doc("download_workflow_agent_sandbox_file")
@console_ns.doc(description="Create a ToolFile from one workflow Agent Binding file and return its download URL")
@console_ns.expect(console_ns.models[WorkflowAgentSandboxDownloadPayload.__name__])
@console_ns.response(200, "Download URL returned", console_ns.models[SandboxDownloadResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
@with_current_user
@with_current_tenant_id
def post(self, tenant_id: str, app_model: App, workflow_run_id: UUID, node_id: str):
payload = WorkflowAgentSandboxUploadPayload.model_validate(request.get_json(silent=True) or {})
@model_validate(WorkflowAgentSandboxDownloadPayload)
def post(
self,
req_data: WorkflowAgentSandboxDownloadPayload,
tenant_id: str,
current_user: Account,
app_id: UUID,
workflow_run_id: UUID,
node_id: str,
):
service = WorkflowAgentSandboxService()
resolved_app_id = service.resolve_app_id(tenant_id=tenant_id, app_id=str(app_id))
if resolved_app_id is None:
raise AppNotFoundError()
try:
result = WorkflowAgentSandboxService().upload_file(
result = service.download_file(
tenant_id=tenant_id,
app_id=app_model.id,
app_id=resolved_app_id,
workflow_run_id=str(workflow_run_id),
node_id=node_id,
node_execution_id=payload.node_execution_id,
path=payload.path,
session=db.session(),
node_execution_id=req_data.node_execution_id,
account_id=current_user.id,
path=req_data.path,
)
except Exception as exc:
return _handle(exc)
@@ -30,6 +30,7 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -810,14 +811,21 @@ class AgentConfigFilesByAgentApi(Resource):
@with_current_user
@with_current_tenant_id
@with_session
def post(self, session: Session, tenant_id: str, current_user: Account, agent_id: UUID):
payload = AgentConfigFileUploadPayload.model_validate(console_ns.payload or {})
@model_validate(AgentConfigFileUploadPayload)
def post(
self,
req_data: AgentConfigFileUploadPayload,
session: Session,
tenant_id: str,
current_user: Account,
agent_id: UUID,
):
return _with_agent_route_target(
session=session,
tenant_id=tenant_id,
agent_id=agent_id,
current_user=current_user,
action=lambda target: _file_upload_response(target, payload),
action=lambda target: _file_upload_response(target, req_data),
)
@@ -849,13 +857,13 @@ class AgentConfigFilesApi(Resource):
@with_current_user
@with_session
@get_app_model(mode=_WORKFLOW_APP_MODES)
def post(self, session: Session, current_user: Account, app_model: App):
payload = AgentConfigFileUploadPayload.model_validate(console_ns.payload or {})
@model_validate(AgentConfigFileUploadPayload)
def post(self, req_data: AgentConfigFileUploadPayload, session: Session, current_user: Account, app_model: App):
return _with_app_route_target(
session=session,
app_model=app_model,
current_user=current_user,
action=lambda target: _file_upload_response(target, payload),
action=lambda target: _file_upload_response(target, req_data),
)
+36 -30
View File
@@ -20,6 +20,7 @@ from controllers.console.wraps import (
annotation_import_rate_limit,
cloud_edition_billing_resource_check,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
)
@@ -180,14 +181,14 @@ class AnnotationReplyActionApi(Resource):
@cloud_edition_billing_resource_check("annotation")
@edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
def post(self, app_id: UUID, action: Literal["enable", "disable"]):
args = AnnotationReplyPayload.model_validate(console_ns.payload)
@model_validate(AnnotationReplyPayload)
def post(self, req_data: AnnotationReplyPayload, app_id: UUID, action: Literal["enable", "disable"]):
match action:
case "enable":
enable_args: EnableAnnotationArgs = {
"score_threshold": args.score_threshold,
"embedding_provider_name": args.embedding_provider_name,
"embedding_model_name": args.embedding_model_name,
"score_threshold": req_data.score_threshold,
"embedding_provider_name": req_data.embedding_provider_name,
"embedding_model_name": req_data.embedding_model_name,
}
result = AppAnnotationService.enable_app_annotation(enable_args, str(app_id))
case "disable":
@@ -231,12 +232,17 @@ class AppAnnotationSettingUpdateApi(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
@with_session
def post(self, session: Session, app_id: UUID, annotation_setting_id: UUID):
@model_validate(AnnotationSettingUpdatePayload)
def post(
self,
req_data: AnnotationSettingUpdatePayload,
session: Session,
app_id: UUID,
annotation_setting_id: UUID,
):
annotation_setting_id_str = str(annotation_setting_id)
args = AnnotationSettingUpdatePayload.model_validate(console_ns.payload)
setting_args: UpdateAnnotationSettingArgs = {"score_threshold": args.score_threshold}
setting_args: UpdateAnnotationSettingArgs = {"score_threshold": req_data.score_threshold}
result = AppAnnotationService.update_app_annotation_setting(
str(app_id), annotation_setting_id_str, setting_args, session
)
@@ -290,11 +296,11 @@ class AnnotationApi(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
@with_session(write=False)
def get(self, session: Session, app_id: UUID):
args = AnnotationListQuery.model_validate(request.args.to_dict(flat=True))
page = args.page
limit = args.limit
keyword = args.keyword
@model_validate(AnnotationListQuery)
def get(self, req_data: AnnotationListQuery, session: Session, app_id: UUID):
page = req_data.page
limit = req_data.limit
keyword = req_data.keyword
annotation_list, total = AppAnnotationService.get_annotation_list_by_app_id(
str(app_id), page, limit, keyword, session
@@ -317,17 +323,17 @@ class AnnotationApi(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
@with_session
def post(self, session: Session, app_id: UUID):
args = CreateAnnotationPayload.model_validate(console_ns.payload)
@model_validate(CreateAnnotationPayload)
def post(self, req_data: CreateAnnotationPayload, session: Session, app_id: UUID):
upsert_args: UpsertAnnotationArgs = {}
if args.answer is not None:
upsert_args["answer"] = args.answer
if args.content is not None:
upsert_args["content"] = args.content
if args.message_id is not None:
upsert_args["message_id"] = args.message_id
if args.question is not None:
upsert_args["question"] = args.question
if req_data.answer is not None:
upsert_args["answer"] = req_data.answer
if req_data.content is not None:
upsert_args["content"] = req_data.content
if req_data.message_id is not None:
upsert_args["message_id"] = req_data.message_id
if req_data.question is not None:
upsert_args["question"] = req_data.question
annotation = AppAnnotationService.up_insert_app_annotation_from_message(upsert_args, str(app_id), session)
return dump_response(Annotation, annotation), 201
@@ -407,13 +413,13 @@ class AnnotationUpdateDeleteApi(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
@with_session
def post(self, session: Session, app_id: UUID, annotation_id: UUID):
args = UpdateAnnotationPayload.model_validate(console_ns.payload)
@model_validate(UpdateAnnotationPayload)
def post(self, req_data: UpdateAnnotationPayload, session: Session, app_id: UUID, annotation_id: UUID):
update_args: UpdateAnnotationArgs = {}
if args.answer is not None:
update_args["answer"] = args.answer
if args.question is not None:
update_args["question"] = args.question
if req_data.answer is not None:
update_args["answer"] = req_data.answer
if req_data.question is not None:
update_args["question"] = req_data.question
app_ref = _get_app_ref(session, str(app_id))
annotation_ref = AppRefService.create_annotation_ref(app_ref, str(annotation_id))
annotation = AppAnnotationService.update_app_annotation_directly(update_args, annotation_ref, session)
+47 -47
View File
@@ -4,7 +4,6 @@ from collections.abc import Sequence
from datetime import datetime
from typing import Any, Literal
from flask import request
from flask_restx import Resource
from pydantic import AliasChoices, BaseModel, Field, ValidationInfo, computed_field, field_validator, model_validator
from sqlalchemy import select
@@ -33,6 +32,7 @@ from controllers.console.wraps import (
edit_permission_required,
enterprise_license_required,
is_admin_or_owner_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -697,16 +697,16 @@ class AppListApi(Resource):
@with_current_user
@with_current_tenant_id
@with_session
def post(self, session: Session, current_tenant_id: str, current_user: Account):
@model_validate(CreateAppPayload)
def post(self, req_data: CreateAppPayload, session: Session, current_tenant_id: str, current_user: Account):
"""Create app"""
args = CreateAppPayload.model_validate(console_ns.payload)
params = CreateAppParams(
name=args.name,
description=args.description,
mode=args.mode,
icon_type=args.icon_type,
icon=args.icon,
icon_background=args.icon_background,
name=req_data.name,
description=req_data.description,
mode=req_data.mode,
icon_type=req_data.icon_type,
icon=req_data.icon,
icon_background=req_data.icon_background,
)
app_service = AppService()
@@ -908,20 +908,20 @@ class AppApi(Resource):
@agent_manage_required_for_agent_app
@with_session
@get_app_model(mode=None)
def put(self, session: Session, app_model: App):
@model_validate(UpdateAppPayload)
def put(self, req_data: UpdateAppPayload, session: Session, app_model: App):
"""Update app"""
args = UpdateAppPayload.model_validate(console_ns.payload)
app_service = AppService()
args_dict: AppService.ArgsDict = {
"name": args.name,
"description": args.description or "",
"icon_type": args.icon_type,
"icon": args.icon or "",
"icon_background": args.icon_background or "",
"use_icon_as_answer_icon": args.use_icon_as_answer_icon or False,
"max_active_requests": args.max_active_requests or 0,
"name": req_data.name,
"description": req_data.description or "",
"icon_type": req_data.icon_type,
"icon": req_data.icon or "",
"icon_background": req_data.icon_background or "",
"use_icon_as_answer_icon": req_data.use_icon_as_answer_icon or False,
"max_active_requests": req_data.max_active_requests or 0,
}
app_model = app_service.update_app(app_model, args_dict, session=session)
return AppDetailWithSite.model_validate(
@@ -969,10 +969,10 @@ class AppCopyApi(Resource):
@with_current_user
@with_current_tenant_id
@get_app_model(mode=None)
def post(self, current_tenant_id: str, current_user: Account, app_model: App):
@model_validate(CopyAppPayload)
def post(self, req_data: CopyAppPayload, current_tenant_id: str, current_user: Account, app_model: App):
"""Copy app"""
# The role of the current user in the ta table must be admin, owner, or editor
args = CopyAppPayload.model_validate(console_ns.payload or {})
with Session(db.engine, expire_on_commit=False) as session:
import_service = AppDslService(session)
@@ -982,11 +982,11 @@ class AppCopyApi(Resource):
account=current_user,
import_mode=ImportMode.YAML_CONTENT,
yaml_content=yaml_content,
name=args.name,
description=args.description,
icon_type=args.icon_type,
icon=args.icon,
icon_background=args.icon_background,
name=req_data.name,
description=req_data.description,
icon_type=req_data.icon_type,
icon=req_data.icon,
icon_background=req_data.icon_background,
)
except NoPermissionError as e:
raise Forbidden(str(e))
@@ -1045,16 +1045,16 @@ class AppExportApi(Resource):
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_IMPORT_EXPORT_DSL)
@agent_manage_required_for_agent_app
@get_app_model
def get(self, app_model: App):
@model_validate(AppExportQuery)
def get(self, req_data: AppExportQuery, app_model: App):
"""Export app"""
args = AppExportQuery.model_validate(request.args.to_dict(flat=True))
response = AppExportResponse(
data=AppDslService.export_dsl(
app_model=app_model,
session=db.session(),
include_secret=args.include_secret,
workflow_id=args.workflow_id,
include_secret=req_data.include_secret,
workflow_id=req_data.workflow_id,
)
)
return response.model_dump(mode="json")
@@ -1102,11 +1102,11 @@ class AppNameApi(Resource):
@agent_manage_required_for_agent_app
@with_session
@get_app_model(mode=None)
def post(self, session: Session, app_model: App):
args = AppNamePayload.model_validate(console_ns.payload)
@model_validate(AppNamePayload)
def post(self, req_data: AppNamePayload, session: Session, app_model: App):
app_service = AppService()
app_model = app_service.update_app_name(app_model, args.name, session=session)
app_model = app_service.update_app_name(app_model, req_data.name, session=session)
return AppDetail.model_validate(
app_model,
from_attributes=True,
@@ -1130,15 +1130,15 @@ class AppIconApi(Resource):
@agent_manage_required_for_agent_app
@with_session
@get_app_model(mode=None)
def post(self, session: Session, app_model: App):
args = AppIconPayload.model_validate(console_ns.payload or {})
@model_validate(AppIconPayload)
def post(self, req_data: AppIconPayload, session: Session, app_model: App):
app_service = AppService()
app_model = app_service.update_app_icon(
app_model,
args.icon or "",
args.icon_background or "",
args.icon_type,
req_data.icon or "",
req_data.icon_background or "",
req_data.icon_type,
session=session,
)
return AppDetail.model_validate(
@@ -1164,11 +1164,11 @@ class AppSiteStatus(Resource):
@agent_manage_required_for_agent_app
@with_session
@get_app_model(mode=None)
def post(self, session: Session, app_model: App):
args = AppSiteStatusPayload.model_validate(console_ns.payload)
@model_validate(AppSiteStatusPayload)
def post(self, req_data: AppSiteStatusPayload, session: Session, app_model: App):
app_service = AppService()
app_model = app_service.update_app_site_status(app_model, args.enable_site, session=session)
app_model = app_service.update_app_site_status(app_model, req_data.enable_site, session=session)
return AppDetail.model_validate(
app_model,
from_attributes=True,
@@ -1192,11 +1192,11 @@ class AppApiStatus(Resource):
@agent_manage_required_for_agent_app
@with_session
@get_app_model(mode=None)
def post(self, session: Session, app_model: App):
args = AppApiStatusPayload.model_validate(console_ns.payload)
@model_validate(AppApiStatusPayload)
def post(self, req_data: AppApiStatusPayload, session: Session, app_model: App):
app_service = AppService()
app_model = app_service.update_app_api_status(app_model, args.enable_api, session=session)
app_model = app_service.update_app_api_status(app_model, req_data.enable_api, session=session)
return AppDetail.model_validate(
app_model,
from_attributes=True,
@@ -1242,14 +1242,14 @@ class AppTraceApi(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TRACING_CONFIG)
@get_app_model
def post(self, app_model: App):
@model_validate(AppTracePayload)
def post(self, req_data: AppTracePayload, app_model: App):
# add app trace
args = AppTracePayload.model_validate(console_ns.payload)
OpsTraceManager.update_app_tracing_config(
app_id=app_model.id,
enabled=args.enabled,
tracing_provider=args.tracing_provider,
enabled=req_data.enabled,
tracing_provider=req_data.tracing_provider,
)
return SimpleResultResponse(result="success").model_dump(mode="json")
+14 -16
View File
@@ -12,6 +12,7 @@ from controllers.console.wraps import (
account_initialization_required,
cloud_edition_billing_resource_check,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_user,
@@ -83,8 +84,8 @@ class AppImportApi(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_IMPORT_EXPORT_DSL, resource_required=False)
@with_current_user
def post(self, current_user: Account | None = None):
args = AppImportPayload.model_validate(console_ns.payload)
@model_validate(AppImportPayload)
def post(self, req_data: AppImportPayload, current_user: Account | None = None):
current_user = current_user if current_user is not None else _current_user_and_tenant_id(None)[0]
# AppDslService performs internal commits for some creation paths, so use a plain
@@ -96,15 +97,15 @@ class AppImportApi(Resource):
try:
result = import_service.import_app(
account=account,
import_mode=args.mode,
yaml_content=args.yaml_content,
yaml_url=args.yaml_url,
name=args.name,
description=args.description,
icon_type=args.icon_type,
icon=args.icon,
icon_background=args.icon_background,
app_id=args.app_id,
import_mode=req_data.mode,
yaml_content=req_data.yaml_content,
yaml_url=req_data.yaml_url,
name=req_data.name,
description=req_data.description,
icon_type=req_data.icon_type,
icon=req_data.icon,
icon_background=req_data.icon_background,
app_id=req_data.app_id,
)
except NoPermissionError as e:
raise Forbidden(str(e))
@@ -113,7 +114,7 @@ class AppImportApi(Resource):
else:
session.commit()
is_created_app = args.app_id is None and result.status in {
is_created_app = req_data.app_id is None and result.status in {
ImportStatus.COMPLETED,
ImportStatus.COMPLETED_WITH_WARNINGS,
}
@@ -162,10 +163,7 @@ class AppImportConfirmApi(Resource):
import_service = AppDslService(session)
# Confirm import
account = current_user
try:
result = import_service.confirm_import(import_id=import_id, account=account)
except NoPermissionError as e:
raise Forbidden(str(e))
result = import_service.confirm_import(import_id=import_id, account=account)
if result.status == ImportStatus.FAILED:
session.rollback()
else:
+10 -12
View File
@@ -31,6 +31,7 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -276,17 +277,15 @@ class ChatMessageTextApi(Resource):
@login_required
@account_initialization_required
@get_app_model
def post(self, app_model: App):
@model_validate(TextToSpeechPayload)
def post(self, req_data: TextToSpeechPayload, app_model: App):
try:
payload_data = dict(console_ns.payload or {})
payload_data.setdefault("text", "")
payload = TextToSpeechPayload.model_validate(payload_data)
message_ref = None
if payload.message_id:
if req_data.message_id:
app_ref = AppRefService.create_app_ref(app_model)
message_ref = AppRefService.create_message_ref(
app_ref,
payload.message_id,
req_data.message_id,
account_id=current_user.id,
)
@@ -294,8 +293,8 @@ class ChatMessageTextApi(Resource):
return AudioService.transcript_tts(
app_model=app_model,
session=db.session(),
text=payload.text,
voice=payload.voice,
text=req_data.text,
voice=req_data.voice,
message_ref=message_ref,
is_draft=True,
)
@@ -341,13 +340,12 @@ class TextModesApi(Resource):
@account_initialization_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
@get_app_model
def get(self, app_model: App):
@model_validate(TextToSpeechVoiceQuery)
def get(self, req_data: TextToSpeechVoiceQuery, app_model: App):
try:
args = TextToSpeechVoiceQuery.model_validate(request.args.to_dict(flat=True))
response = AudioService.transcript_tts_voices(
tenant_id=app_model.tenant_id,
language=args.language,
language=req_data.language,
)
return dump_response(TextToSpeechVoiceListResponse, response)
+5 -4
View File
@@ -29,6 +29,7 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -160,11 +161,11 @@ class CompletionMessageApi(Resource):
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN)
@with_session
@get_app_model(mode=AppMode.COMPLETION)
def post(self, session: Session, current_user: Account, app_model: App):
args_model = CompletionMessagePayload.model_validate(console_ns.payload)
args = args_model.model_dump(exclude_none=True, by_alias=True)
@model_validate(CompletionMessagePayload)
def post(self, req_data: CompletionMessagePayload, session: Session, current_user: Account, app_model: App):
args = req_data.model_dump(exclude_none=True, by_alias=True)
streaming = args_model.response_mode != "blocking"
streaming = req_data.response_mode != "blocking"
args["auto_generate_name"] = False
try:
+20 -19
View File
@@ -2,7 +2,7 @@ from typing import Literal
from uuid import UUID
import sqlalchemy as sa
from flask import abort, request
from flask import abort
from flask_restx import Resource
from pydantic import BaseModel, Field, field_validator
from sqlalchemy import func, or_
@@ -18,6 +18,7 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_user,
@@ -108,17 +109,17 @@ class CompletionConversationApi(Resource):
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
@with_session(write=False)
@get_app_model(mode=AppMode.COMPLETION)
def get(self, session: Session, current_user: Account, app_model: App):
args = CompletionConversationQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(CompletionConversationQuery)
def get(self, req_data: CompletionConversationQuery, session: Session, current_user: Account, app_model: App):
query = sa.select(Conversation).where(
Conversation.app_id == app_model.id, Conversation.mode == "completion", Conversation.is_deleted.is_(False)
)
if args.keyword:
if req_data.keyword:
from libs.helper import escape_like_pattern
escaped_keyword = escape_like_pattern(args.keyword)
escaped_keyword = escape_like_pattern(req_data.keyword)
query = query.join(Message, Message.conversation_id == Conversation.id).where(
or_(
Message.query.ilike(f"%{escaped_keyword}%", escape="\\"),
@@ -130,7 +131,7 @@ class CompletionConversationApi(Resource):
assert account.timezone is not None
try:
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
@@ -142,7 +143,7 @@ class CompletionConversationApi(Resource):
query = query.where(Conversation.created_at < end_datetime_utc)
# FIXME, the type ignore in this file
if args.annotation_status == "annotated":
if req_data.annotation_status == "annotated":
query = (
query.options(selectinload(Conversation.message_annotations)) # type: ignore[arg-type]
.join( # type: ignore
@@ -150,7 +151,7 @@ class CompletionConversationApi(Resource):
)
.group_by(Conversation.id)
)
elif args.annotation_status == "not_annotated":
elif req_data.annotation_status == "not_annotated":
query = (
query.outerjoin(MessageAnnotation, MessageAnnotation.conversation_id == Conversation.id)
.group_by(Conversation.id)
@@ -159,7 +160,7 @@ class CompletionConversationApi(Resource):
query = query.order_by(Conversation.created_at.desc())
conversations = paginate_query(query, session=session, page=args.page, per_page=args.limit)
conversations = paginate_query(query, session=session, page=req_data.page, per_page=req_data.limit)
return dump_response(
ConversationPaginationResponse,
@@ -238,8 +239,8 @@ class ChatConversationApi(Resource):
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
@with_session(write=False)
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT])
def get(self, session: Session, current_user: Account, app_model: App):
args = ChatConversationQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(ChatConversationQuery)
def get(self, req_data: ChatConversationQuery, session: Session, current_user: Account, app_model: App):
subquery = (
sa.select(Conversation.id.label("conversation_id"), EndUser.session_id.label("from_end_user_session_id"))
@@ -249,10 +250,10 @@ class ChatConversationApi(Resource):
query = sa.select(Conversation).where(Conversation.app_id == app_model.id, Conversation.is_deleted.is_(False))
if args.keyword:
if req_data.keyword:
from libs.helper import escape_like_pattern
escaped_keyword = escape_like_pattern(args.keyword)
escaped_keyword = escape_like_pattern(req_data.keyword)
keyword_filter = f"%{escaped_keyword}%"
query = (
query.join(
@@ -276,12 +277,12 @@ class ChatConversationApi(Resource):
assert account.timezone is not None
try:
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
if start_datetime_utc:
match args.sort_by:
match req_data.sort_by:
case "updated_at" | "-updated_at":
query = query.where(Conversation.updated_at >= start_datetime_utc)
case "created_at" | "-created_at" | _:
@@ -289,13 +290,13 @@ class ChatConversationApi(Resource):
if end_datetime_utc:
end_datetime_utc = end_datetime_utc.replace(second=59)
match args.sort_by:
match req_data.sort_by:
case "updated_at" | "-updated_at":
query = query.where(Conversation.updated_at <= end_datetime_utc)
case "created_at" | "-created_at" | _:
query = query.where(Conversation.created_at <= end_datetime_utc)
match args.annotation_status:
match req_data.annotation_status:
case "annotated":
query = (
query.options(selectinload(Conversation.message_annotations)) # type: ignore[arg-type]
@@ -316,7 +317,7 @@ class ChatConversationApi(Resource):
if app_model.mode == AppMode.ADVANCED_CHAT:
query = query.where(Conversation.invoke_from != InvokeFrom.DEBUGGER)
match args.sort_by:
match req_data.sort_by:
case "created_at":
query = query.order_by(Conversation.created_at.asc())
case "-created_at":
@@ -328,7 +329,7 @@ class ChatConversationApi(Resource):
case _:
query = query.order_by(Conversation.created_at.desc())
conversations = paginate_query(query, session=session, page=args.page, per_page=args.limit)
conversations = paginate_query(query, session=session, page=req_data.page, per_page=req_data.limit)
return dump_response(
ConversationWithSummaryPaginationResponse,
@@ -3,7 +3,6 @@ from __future__ import annotations
from datetime import datetime
from typing import Any
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field, field_validator
from sqlalchemy import select
@@ -16,6 +15,7 @@ from controllers.console.wraps import (
RBACPermission,
RBACResourceScope,
account_initialization_required,
model_validate,
rbac_permission_required,
setup_required,
)
@@ -101,15 +101,15 @@ class ConversationVariablesApi(Resource):
@account_initialization_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT)
@get_app_model(mode=AppMode.ADVANCED_CHAT)
def get(self, app_model: App):
args = ConversationVariablesQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(ConversationVariablesQuery)
def get(self, req_data: ConversationVariablesQuery, app_model: App):
stmt = (
select(ConversationVariable)
.where(ConversationVariable.app_id == app_model.id)
.order_by(ConversationVariable.created_at)
)
stmt = stmt.where(ConversationVariable.conversation_id == args.conversation_id)
stmt = stmt.where(ConversationVariable.conversation_id == req_data.conversation_id)
# NOTE: This is a temporary solution to avoid performance issues.
page = 1
+72 -65
View File
@@ -17,7 +17,12 @@ from controllers.console.app.error import (
ProviderQuotaExceededError,
)
from controllers.console.app.wraps import with_session
from controllers.console.wraps import account_initialization_required, setup_required, with_current_tenant_id
from controllers.console.wraps import (
account_initialization_required,
model_validate,
setup_required,
with_current_tenant_id,
)
from core.app.app_config.entities import ModelConfig
from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError
from core.helper.code_executor.code_node_provider import CodeNodeProvider
@@ -238,11 +243,11 @@ class RuleGenerateApi(Resource):
@login_required
@account_initialization_required
@with_current_tenant_id
def post(self, current_tenant_id: str):
args = RuleGeneratePayload.model_validate(console_ns.payload)
@model_validate(RuleGeneratePayload)
def post(self, req_data: RuleGeneratePayload, current_tenant_id: str):
try:
rules = LLMGenerator.generate_rule_config(tenant_id=current_tenant_id, args=args)
rules = LLMGenerator.generate_rule_config(tenant_id=current_tenant_id, args=req_data)
except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description)
except QuotaExceededError:
@@ -267,13 +272,13 @@ class RuleCodeGenerateApi(Resource):
@login_required
@account_initialization_required
@with_current_tenant_id
def post(self, current_tenant_id: str):
args = RuleCodeGeneratePayload.model_validate(console_ns.payload)
@model_validate(RuleCodeGeneratePayload)
def post(self, req_data: RuleCodeGeneratePayload, current_tenant_id: str):
try:
code_result = LLMGenerator.generate_code(
tenant_id=current_tenant_id,
args=args,
args=req_data,
)
except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description)
@@ -299,13 +304,13 @@ class RuleStructuredOutputGenerateApi(Resource):
@login_required
@account_initialization_required
@with_current_tenant_id
def post(self, current_tenant_id: str):
args = RuleStructuredOutputPayload.model_validate(console_ns.payload)
@model_validate(RuleStructuredOutputPayload)
def post(self, req_data: RuleStructuredOutputPayload, current_tenant_id: str):
try:
structured_output = LLMGenerator.generate_structured_output(
tenant_id=current_tenant_id,
args=args,
args=req_data,
)
except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description)
@@ -332,36 +337,36 @@ class InstructionGenerateApi(Resource):
@account_initialization_required
@with_current_tenant_id
@with_session(write=False)
def post(self, session: Session, current_tenant_id: str):
args = InstructionGeneratePayload.model_validate(console_ns.payload)
@model_validate(InstructionGeneratePayload)
def post(self, req_data: InstructionGeneratePayload, session: Session, current_tenant_id: str):
providers: list[type[CodeNodeProvider]] = [Python3CodeProvider, JavascriptCodeProvider]
code_provider: type[CodeNodeProvider] | None = next(
(p for p in providers if p.is_accept_language(args.language)), None
(p for p in providers if p.is_accept_language(req_data.language)), None
)
code_template = code_provider.get_default_code() if code_provider else ""
try:
# Generate from nothing for a workflow node
if (args.current in (code_template, "")) and args.node_id != "":
if (req_data.current in (code_template, "")) and req_data.node_id != "":
app = session.scalar(
select(App).where(App.id == args.flow_id, App.tenant_id == current_tenant_id).limit(1)
select(App).where(App.id == req_data.flow_id, App.tenant_id == current_tenant_id).limit(1)
)
if not app:
return {"error": f"app {args.flow_id} not found"}, 400
return {"error": f"app {req_data.flow_id} not found"}, 400
workflow = WorkflowService().get_draft_workflow(app_model=app, session=session)
if not workflow:
return {"error": f"workflow {args.flow_id} not found"}, 400
return {"error": f"workflow {req_data.flow_id} not found"}, 400
nodes: Sequence = workflow.graph_dict["nodes"]
node = [node for node in nodes if node["id"] == args.node_id]
node = [node for node in nodes if node["id"] == req_data.node_id]
if len(node) == 0:
return {"error": f"node {args.node_id} not found"}, 400
return {"error": f"node {req_data.node_id} not found"}, 400
node_type = node[0]["data"]["type"]
match node_type:
case "llm":
return LLMGenerator.generate_rule_config(
current_tenant_id,
args=RuleGeneratePayload(
instruction=args.instruction,
model_config=args.model_config_data,
instruction=req_data.instruction,
model_config=req_data.model_config_data,
no_variable=True,
),
)
@@ -369,8 +374,8 @@ class InstructionGenerateApi(Resource):
return LLMGenerator.generate_rule_config(
current_tenant_id,
args=RuleGeneratePayload(
instruction=args.instruction,
model_config=args.model_config_data,
instruction=req_data.instruction,
model_config=req_data.model_config_data,
no_variable=True,
),
)
@@ -378,31 +383,31 @@ class InstructionGenerateApi(Resource):
return LLMGenerator.generate_code(
tenant_id=current_tenant_id,
args=RuleCodeGeneratePayload(
instruction=args.instruction,
model_config=args.model_config_data,
code_language=args.language,
instruction=req_data.instruction,
model_config=req_data.model_config_data,
code_language=req_data.language,
),
)
case _:
return {"error": f"invalid node type: {node_type}"}
if args.node_id == "" and args.current != "": # For legacy app without a workflow
if req_data.node_id == "" and req_data.current != "": # For legacy app without a workflow
return LLMGenerator.instruction_modify_legacy(
tenant_id=current_tenant_id,
flow_id=args.flow_id,
current=args.current,
instruction=args.instruction,
model_config=args.model_config_data,
ideal_output=args.ideal_output,
flow_id=req_data.flow_id,
current=req_data.current,
instruction=req_data.instruction,
model_config=req_data.model_config_data,
ideal_output=req_data.ideal_output,
)
if args.node_id != "" and args.current != "": # For workflow node
if req_data.node_id != "" and req_data.current != "": # For workflow node
return LLMGenerator.instruction_modify_workflow(
tenant_id=current_tenant_id,
flow_id=args.flow_id,
node_id=args.node_id,
current=args.current,
instruction=args.instruction,
model_config=args.model_config_data,
ideal_output=args.ideal_output,
flow_id=req_data.flow_id,
node_id=req_data.node_id,
current=req_data.current,
instruction=req_data.instruction,
model_config=req_data.model_config_data,
ideal_output=req_data.ideal_output,
workflow_service=WorkflowService(),
)
return {"error": "incompatible parameters"}, 400
@@ -426,9 +431,9 @@ class InstructionGenerationTemplateApi(Resource):
@setup_required
@login_required
@account_initialization_required
def post(self):
args = InstructionTemplatePayload.model_validate(console_ns.payload)
match args.type:
@model_validate(InstructionTemplatePayload)
def post(self, req_data: InstructionTemplatePayload):
match req_data.type:
case "prompt":
from core.llm_generator.prompts import INSTRUCTION_GENERATE_TEMPLATE_PROMPT
@@ -438,7 +443,7 @@ class InstructionGenerationTemplateApi(Resource):
return {"data": INSTRUCTION_GENERATE_TEMPLATE_CODE}
case _:
raise ValueError(f"Invalid type: {args.type}")
raise ValueError(f"Invalid type: {req_data.type}")
def _workflow_instruction_guard(args: WorkflowGeneratePayload) -> tuple[dict, int] | None:
@@ -492,24 +497,24 @@ class WorkflowGenerateApi(Resource):
@login_required
@account_initialization_required
@with_current_tenant_id
def post(self, current_tenant_id: str):
args = WorkflowGeneratePayload.model_validate(console_ns.payload)
@model_validate(WorkflowGeneratePayload)
def post(self, req_data: WorkflowGeneratePayload, current_tenant_id: str):
# Reject empty / over-length instructions at the boundary (shared with
# the streaming endpoint) before spending a planner+builder roundtrip.
guard = _workflow_instruction_guard(args)
guard = _workflow_instruction_guard(req_data)
if guard is not None:
return guard
try:
result = WorkflowGeneratorService.generate_workflow_graph(
tenant_id=current_tenant_id,
mode=args.mode,
instruction=args.instruction,
model_config=args.model_config_data,
ideal_output=args.ideal_output,
current_graph=args.current_graph.model_dump(by_alias=True, exclude_none=True)
if args.current_graph
mode=req_data.mode,
instruction=req_data.instruction,
model_config=req_data.model_config_data,
ideal_output=req_data.ideal_output,
current_graph=req_data.current_graph.model_dump(by_alias=True, exclude_none=True)
if req_data.current_graph
else None,
)
except ProviderTokenNotInitError as ex:
@@ -547,13 +552,13 @@ class WorkflowInstructionSuggestionsApi(Resource):
@login_required
@account_initialization_required
@with_current_tenant_id
def post(self, current_tenant_id: str):
args = WorkflowInstructionSuggestionsPayload.model_validate(console_ns.payload)
@model_validate(WorkflowInstructionSuggestionsPayload)
def post(self, req_data: WorkflowInstructionSuggestionsPayload, current_tenant_id: str):
suggestions = LLMGenerator.generate_workflow_instruction_suggestions(
tenant_id=current_tenant_id,
mode=args.mode,
language=args.language,
count=args.count,
mode=req_data.mode,
language=req_data.language,
count=req_data.count,
)
return dump_response(WorkflowInstructionSuggestionsResponse, {"suggestions": suggestions})
@@ -583,12 +588,12 @@ class WorkflowGenerateStreamApi(Resource):
@login_required
@account_initialization_required
@with_current_tenant_id
def post(self, current_tenant_id: str):
args = WorkflowGeneratePayload.model_validate(console_ns.payload)
@model_validate(WorkflowGeneratePayload)
def post(self, req_data: WorkflowGeneratePayload, current_tenant_id: str):
# Same boundary guards as the blocking endpoint — return a normal 400
# JSON for these BEFORE opening the stream.
guard = _workflow_instruction_guard(args)
guard = _workflow_instruction_guard(req_data)
if guard is not None:
return guard
@@ -596,12 +601,14 @@ class WorkflowGenerateStreamApi(Resource):
try:
for event_name, payload in WorkflowGeneratorService.generate_workflow_graph_stream(
tenant_id=current_tenant_id,
mode=args.mode,
instruction=args.instruction,
model_config=args.model_config_data,
ideal_output=args.ideal_output,
mode=req_data.mode,
instruction=req_data.instruction,
model_config=req_data.model_config_data,
ideal_output=req_data.ideal_output,
current_graph=(
args.current_graph.model_dump(by_alias=True, exclude_none=True) if args.current_graph else None
req_data.current_graph.model_dump(by_alias=True, exclude_none=True)
if req_data.current_graph
else None
),
):
body = {"event": event_name, **payload}
+12 -12
View File
@@ -15,6 +15,7 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -109,17 +110,16 @@ class AppMCPServerController(Resource):
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
@with_current_tenant_id
@get_app_model
def post(self, current_tenant_id: str, app_model: App):
payload = MCPServerCreatePayload.model_validate(console_ns.payload or {})
description = payload.description
@model_validate(MCPServerCreatePayload)
def post(self, req_data: MCPServerCreatePayload, current_tenant_id: str, app_model: App):
description = req_data.description
if not description:
description = app_model.description or ""
server = AppMCPServer(
name=app_model.name,
description=description,
parameters=json.dumps(payload.parameters, ensure_ascii=False),
parameters=json.dumps(req_data.parameters, ensure_ascii=False),
status=AppMCPServerStatus.ACTIVE,
app_id=app_model.id,
tenant_id=current_tenant_id,
@@ -144,10 +144,10 @@ class AppMCPServerController(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
@get_app_model
def put(self, app_model: App):
payload = MCPServerUpdatePayload.model_validate(console_ns.payload or {})
@model_validate(MCPServerUpdatePayload)
def put(self, req_data: MCPServerUpdatePayload, app_model: App):
app_ref = AppRefService.create_app_ref(app_model)
server_ref = AppRefService.create_mcp_server_ref(app_ref, payload.id)
server_ref = AppRefService.create_mcp_server_ref(app_ref, req_data.id)
server = db.session.scalar(
select(AppMCPServer)
.where(
@@ -160,7 +160,7 @@ class AppMCPServerController(Resource):
if not server:
raise NotFound()
description = payload.description
description = req_data.description
if description is None or not description:
server.description = app_model.description or ""
else:
@@ -168,10 +168,10 @@ class AppMCPServerController(Resource):
server.name = app_model.name
server.parameters = json.dumps(payload.parameters, ensure_ascii=False)
if payload.status:
server.parameters = json.dumps(req_data.parameters, ensure_ascii=False)
if req_data.status:
try:
server.status = AppMCPServerStatus(payload.status)
server.status = AppMCPServerStatus(req_data.status)
except ValueError:
raise ValueError("Invalid status")
db.session.commit()
+9 -8
View File
@@ -28,6 +28,7 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -320,8 +321,8 @@ class MessageFeedbackExportApi(Resource):
@account_initialization_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
@get_app_model
def get(self, app_model: App):
args = FeedbackExportQuery.model_validate(request.args.to_dict())
@model_validate(FeedbackExportQuery)
def get(self, req_data: FeedbackExportQuery, app_model: App):
# Import the service function
from services.feedback_service import FeedbackService
@@ -330,12 +331,12 @@ class MessageFeedbackExportApi(Resource):
export_data = FeedbackService.export_feedbacks(
app_model.id,
session=db.session(),
from_source=args.from_source,
rating=args.rating,
has_comment=args.has_comment,
start_date=args.start_date,
end_date=args.end_date,
format_type=args.format,
from_source=req_data.from_source,
rating=req_data.rating,
has_comment=req_data.has_comment,
start_date=req_data.start_date,
end_date=req_data.end_date,
format_type=req_data.format,
)
return export_data
+15 -19
View File
@@ -1,6 +1,5 @@
from typing import Any
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field
from werkzeug.exceptions import BadRequest
@@ -14,6 +13,7 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
)
@@ -74,12 +74,11 @@ class TraceAppConfigApi(Resource):
@account_initialization_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TRACING_CONFIG)
@get_app_model
def get(self, app_model: App):
args = TraceProviderQuery.model_validate(request.args.to_dict(flat=True)) # type: ignore
@model_validate(TraceProviderQuery)
def get(self, req_data: TraceProviderQuery, app_model: App):
try:
trace_config = OpsService.get_tracing_app_config(
app_id=app_model.id, tracing_provider=args.tracing_provider, session=db.session()
app_id=app_model.id, tracing_provider=req_data.tracing_provider, session=db.session()
)
if not trace_config:
return {"has_not_configured": True}
@@ -104,15 +103,14 @@ class TraceAppConfigApi(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TRACING_CONFIG)
@get_app_model
def post(self, app_model: App):
@model_validate(TraceConfigPayload)
def post(self, req_data: TraceConfigPayload, app_model: App):
"""Create a new trace app configuration"""
args = TraceConfigPayload.model_validate(console_ns.payload)
try:
result = OpsService.create_tracing_app_config(
app_id=app_model.id,
tracing_provider=args.tracing_provider,
tracing_config=args.tracing_config,
tracing_provider=req_data.tracing_provider,
tracing_config=req_data.tracing_config,
session=db.session(),
)
if not result:
@@ -140,15 +138,14 @@ class TraceAppConfigApi(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TRACING_CONFIG)
@get_app_model
def patch(self, app_model: App):
@model_validate(TraceConfigPayload)
def patch(self, req_data: TraceConfigPayload, app_model: App):
"""Update an existing trace app configuration"""
args = TraceConfigPayload.model_validate(console_ns.payload)
try:
result = OpsService.update_tracing_app_config(
app_id=app_model.id,
tracing_provider=args.tracing_provider,
tracing_config=args.tracing_config,
tracing_provider=req_data.tracing_provider,
tracing_config=req_data.tracing_config,
session=db.session(),
)
if not result:
@@ -170,13 +167,12 @@ class TraceAppConfigApi(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TRACING_CONFIG)
@get_app_model
def delete(self, app_model: App):
@model_validate(TraceProviderQuery)
def delete(self, req_data: TraceProviderQuery, app_model: App):
"""Delete an existing trace app configuration"""
args = TraceProviderQuery.model_validate(request.args.to_dict(flat=True))
try:
result = OpsService.delete_tracing_app_config(
app_id=app_model.id, tracing_provider=args.tracing_provider, session=db.session()
app_id=app_model.id, tracing_provider=req_data.tracing_provider, session=db.session()
)
if not result:
raise TracingConfigNotExist()
+5 -6
View File
@@ -10,13 +10,14 @@ from constants.languages import supported_language
from controllers.common.schema import register_schema_models
from controllers.common.session import with_session
from controllers.console import console_ns
from controllers.console.app.wraps import agent_manage_required_for_agent_app, get_app_model
from controllers.console.app.wraps import get_app_model
from controllers.console.wraps import (
RBACPermission,
RBACResourceScope,
account_initialization_required,
edit_permission_required,
is_admin_or_owner_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_user,
@@ -93,13 +94,12 @@ class AppSite(Resource):
@login_required
@edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_RELEASE_AND_VERSION)
@agent_manage_required_for_agent_app
@account_initialization_required
@with_current_user
@with_session
@get_app_model
def post(self, session: Session, current_user: Account, app_model: App):
args = AppSiteUpdatePayload.model_validate(console_ns.payload or {})
@model_validate(AppSiteUpdatePayload)
def post(self, req_data: AppSiteUpdatePayload, session: Session, current_user: Account, app_model: App):
site = session.scalar(select(Site).where(Site.app_id == app_model.id).limit(1))
if not site:
raise NotFound
@@ -123,7 +123,7 @@ class AppSite(Resource):
"show_workflow_steps",
"use_icon_as_answer_icon",
]:
value = getattr(args, attr_name)
value = getattr(req_data, attr_name)
if value is not None:
setattr(site, attr_name, value)
@@ -146,7 +146,6 @@ class AppSiteAccessTokenReset(Resource):
@login_required
@is_admin_or_owner_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_RELEASE_AND_VERSION)
@agent_manage_required_for_agent_app
@account_initialization_required
@with_current_user
@with_session
+26 -25
View File
@@ -1,7 +1,7 @@
from decimal import Decimal
import sqlalchemy as sa
from flask import abort, request
from flask import abort
from flask_restx import Resource
from pydantic import BaseModel, Field, field_validator
@@ -12,6 +12,7 @@ from controllers.console.wraps import (
RBACPermission,
RBACResourceScope,
account_initialization_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_user,
@@ -157,8 +158,8 @@ class DailyMessageStatistic(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
@get_app_model
def get(self, account: Account, app_model: App):
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(StatisticTimeRangeQuery)
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
converted_created_at = convert_datetime_to_date("created_at")
sql_query = f"""SELECT
@@ -177,7 +178,7 @@ WHERE
}
try:
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
@@ -217,8 +218,8 @@ class DailyConversationStatistic(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
@get_app_model
def get(self, account: Account, app_model: App):
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(StatisticTimeRangeQuery)
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
converted_created_at = convert_datetime_to_date("created_at")
sql_query = f"""SELECT
@@ -237,7 +238,7 @@ WHERE
}
try:
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
@@ -276,8 +277,8 @@ class DailyTerminalsStatistic(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
@get_app_model
def get(self, account: Account, app_model: App):
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(StatisticTimeRangeQuery)
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
converted_created_at = convert_datetime_to_date("created_at")
sql_query = f"""SELECT
@@ -296,7 +297,7 @@ WHERE
}
try:
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
@@ -336,8 +337,8 @@ class DailyTokenCostStatistic(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
@get_app_model
def get(self, account: Account, app_model: App):
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(StatisticTimeRangeQuery)
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
converted_created_at = convert_datetime_to_date("created_at")
sql_query = f"""SELECT
@@ -357,7 +358,7 @@ WHERE
}
try:
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
@@ -399,8 +400,8 @@ class AverageSessionInteractionStatistic(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT])
def get(self, account: Account, app_model: App):
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(StatisticTimeRangeQuery)
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
converted_created_at = convert_datetime_to_date("c.created_at")
sql_query = f"""SELECT
@@ -427,7 +428,7 @@ FROM
}
try:
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
@@ -478,8 +479,8 @@ class UserSatisfactionRateStatistic(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
@get_app_model
def get(self, account: Account, app_model: App):
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(StatisticTimeRangeQuery)
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
converted_created_at = convert_datetime_to_date("m.created_at")
sql_query = f"""SELECT
@@ -502,7 +503,7 @@ WHERE
}
try:
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
@@ -547,8 +548,8 @@ class AverageResponseTimeStatistic(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
@get_app_model(mode=AppMode.COMPLETION)
def get(self, account: Account, app_model: App):
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(StatisticTimeRangeQuery)
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
converted_created_at = convert_datetime_to_date("created_at")
sql_query = f"""SELECT
@@ -567,7 +568,7 @@ WHERE
}
try:
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
@@ -607,8 +608,8 @@ class TokensPerSecondStatistic(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
@get_app_model
def get(self, account: Account, app_model: App):
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(StatisticTimeRangeQuery)
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
converted_created_at = convert_datetime_to_date("created_at")
sql_query = f"""SELECT
@@ -630,7 +631,7 @@ WHERE
}
try:
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
+72 -19
View File
@@ -2,11 +2,20 @@ import json
import logging
from collections.abc import Sequence
from datetime import datetime
from typing import Any, NotRequired, TypedDict
from typing import Any, NotRequired, Self, TypedDict
from flask import abort, request
from flask_restx import Resource, fields
from pydantic import AliasChoices, BaseModel, ConfigDict, Field, RootModel, ValidationError, field_validator
from pydantic import (
AliasChoices,
BaseModel,
ConfigDict,
Field,
RootModel,
ValidationError,
field_validator,
model_validator,
)
from sqlalchemy.orm import Session, sessionmaker
from werkzeug.exceptions import BadRequest, Forbidden, InternalServerError, NotFound
@@ -54,6 +63,10 @@ from core.trigger.debug.event_selectors import (
create_event_poller,
select_trigger_debug_events,
)
from core.workflow.llm_environment_variable import (
LLM_ENVIRONMENT_VARIABLE_VALUE_TYPE,
LLMEnvironmentVariable,
)
from extensions.ext_database import db
from extensions.ext_redis import redis_client
from factories import file_factory, variable_factory
@@ -103,14 +116,35 @@ class EnvironmentVariableResponseDict(TypedDict):
description: NotRequired[str | None]
class SyncEnvironmentVariablePatchPayload(BaseModel):
environment_variables: list[dict[str, Any]] = Field(default_factory=list)
deleted_environment_variable_ids: list[str] = Field(default_factory=list)
@model_validator(mode="after")
def validate_patch(self) -> Self:
"""Require stable, disjoint IDs so the service can merge the patch deterministically."""
upsert_ids = [variable.get("id") for variable in self.environment_variables]
if any(not isinstance(variable_id, str) or not variable_id for variable_id in upsert_ids):
raise ValueError("patched environment variables require an id")
if len(set(upsert_ids)) != len(upsert_ids):
raise ValueError("patched environment variable ids must be unique")
if any(not variable_id for variable_id in self.deleted_environment_variable_ids):
raise ValueError("deleted environment variable ids must not be empty")
if len(set(self.deleted_environment_variable_ids)) != len(self.deleted_environment_variable_ids):
raise ValueError("deleted environment variable ids must be unique")
if set(upsert_ids).intersection(self.deleted_environment_variable_ids):
raise ValueError("an environment variable cannot be upserted and deleted in the same patch")
return self
class SyncDraftWorkflowPayload(BaseModel):
model_config = ConfigDict(extra="forbid")
graph: dict[str, Any]
features: dict[str, Any]
hash: str | None = None
is_collaborative: bool = Field(default=False, alias="_is_collaborative")
environment_variables: list[dict[str, Any]] = Field(
default_factory=list,
)
environment_variable_patch: SyncEnvironmentVariablePatchPayload | None = None
conversation_variables: list[dict[str, Any]] = Field(
default_factory=list,
)
@@ -483,6 +517,15 @@ def _parse_file(workflow: Workflow, files: list[dict] | None = None) -> Sequence
def _serialize_environment_variable(value: Any) -> EnvironmentVariableResponseDict | Any:
match value:
case LLMEnvironmentVariable():
return {
"id": value.id,
"name": value.name,
"value": value.value,
"value_type": LLM_ENVIRONMENT_VARIABLE_VALUE_TYPE,
"description": value.description,
}
case SecretVariable():
return {
"id": value.id,
@@ -507,6 +550,8 @@ def _serialize_environment_variable(value: Any) -> EnvironmentVariableResponseDi
raise TypeError(
f"unexpected type for value_type field, value={value_type_str}, type={type(value_type_str)}"
)
if value_type_str == LLM_ENVIRONMENT_VARIABLE_VALUE_TYPE:
return value
value_type = SegmentType(value_type_str).exposed_type()
if value_type not in ENVIRONMENT_VARIABLE_SUPPORTED_TYPES:
raise ValueError(f"Unsupported environment variable value type: {value_type}")
@@ -590,30 +635,38 @@ class DraftWorkflowApi(Resource):
return {"message": "Invalid JSON data"}, 400
else:
abort(415)
args = args_model.model_dump()
workflow_service = WorkflowService()
try:
environment_variables_list = Workflow.normalize_environment_variable_mappings(
args.get("environment_variables") or [],
)
environment_variables = [
variable_factory.build_environment_variable_from_mapping(obj) for obj in environment_variables_list
]
conversation_variables_list = args.get("conversation_variables") or []
environment_variable_patch = args_model.environment_variable_patch
environment_variable_upserts: list[VariableBase] | None = None
deleted_environment_variable_ids: list[str] = []
if environment_variable_patch is not None:
environment_variable_upsert_mappings = Workflow.normalize_environment_variable_mappings(
environment_variable_patch.environment_variables,
)
environment_variable_upserts = [
variable_factory.build_environment_variable_from_mapping(obj)
for obj in environment_variable_upsert_mappings
]
deleted_environment_variable_ids = environment_variable_patch.deleted_environment_variable_ids
conversation_variables = [
variable_factory.build_conversation_variable_from_mapping(obj) for obj in conversation_variables_list
variable_factory.build_conversation_variable_from_mapping(obj)
for obj in args_model.conversation_variables
]
workflow = workflow_service.sync_draft_workflow(
app_model=app_model,
graph=args["graph"],
features=args["features"],
unique_hash=args.get("hash"),
graph=args_model.graph,
features=args_model.features,
unique_hash=args_model.hash,
account=current_user,
environment_variables=environment_variables,
environment_variables=[],
conversation_variables=conversation_variables,
session=db.session(),
graph_only=args["is_collaborative"],
environment_variable_upserts=environment_variable_upserts,
deleted_environment_variable_ids=deleted_environment_variable_ids,
preserve_environment_variables=True,
graph_only=args_model.is_collaborative,
)
except WorkflowHashNotEqualError:
raise DraftWorkflowNotSync()
+16 -16
View File
@@ -2,7 +2,6 @@ from datetime import datetime
from typing import Any
from dateutil.parser import isoparse
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field, field_validator
from sqlalchemy.orm import sessionmaker
@@ -14,6 +13,7 @@ from controllers.console.wraps import (
RBACPermission,
RBACResourceScope,
account_initialization_required,
model_validate,
rbac_permission_required,
setup_required,
)
@@ -183,11 +183,11 @@ class WorkflowAppLogApi(Resource):
@account_initialization_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_LOG_AND_ANNOTATION)
@get_app_model(mode=[AppMode.WORKFLOW])
def get(self, app_model: App):
@model_validate(WorkflowAppLogQuery)
def get(self, req_data: WorkflowAppLogQuery, app_model: App):
"""
Get workflow app logs
"""
args = WorkflowAppLogQuery.model_validate(request.args.to_dict(flat=True))
# get paginate workflow app logs
workflow_app_service = WorkflowAppService()
@@ -195,15 +195,15 @@ class WorkflowAppLogApi(Resource):
workflow_app_log_pagination = workflow_app_service.get_paginate_workflow_app_logs(
session=session,
app_model=app_model,
keyword=args.keyword,
status=args.status,
created_at_before=args.created_at__before,
created_at_after=args.created_at__after,
page=args.page,
limit=args.limit,
detail=args.detail,
created_by_end_user_session_id=args.created_by_end_user_session_id,
created_by_account=args.created_by_account,
keyword=req_data.keyword,
status=req_data.status,
created_at_before=req_data.created_at__before,
created_at_after=req_data.created_at__after,
page=req_data.page,
limit=req_data.limit,
detail=req_data.detail,
created_by_end_user_session_id=req_data.created_by_end_user_session_id,
created_by_account=req_data.created_by_account,
)
return WorkflowAppLogPaginationResponse.model_validate(
@@ -227,19 +227,19 @@ class WorkflowArchivedLogApi(Resource):
@account_initialization_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_LOG_AND_ANNOTATION)
@get_app_model(mode=[AppMode.WORKFLOW])
def get(self, app_model: App):
@model_validate(WorkflowAppLogQuery)
def get(self, req_data: WorkflowAppLogQuery, app_model: App):
"""
Get workflow archived logs
"""
args = WorkflowAppLogQuery.model_validate(request.args.to_dict(flat=True))
workflow_app_service = WorkflowAppService()
with sessionmaker(db.engine, expire_on_commit=False).begin() as session:
workflow_app_log_pagination = workflow_app_service.get_paginate_workflow_archive_logs(
session=session,
app_model=app_model,
page=args.page,
limit=args.limit,
page=req_data.page,
limit=req_data.limit,
)
return WorkflowArchivedLogPaginationResponse.model_validate(
+49 -22
View File
@@ -10,6 +10,7 @@ from controllers.console.app.wraps import get_app_model
from controllers.console.wraps import (
account_initialization_required,
edit_permission_required,
model_validate,
setup_required,
with_current_tenant_id,
with_current_user,
@@ -239,18 +240,24 @@ class WorkflowCommentListApi(Resource):
@with_current_user
@with_current_tenant_id
@get_app_model()
def post(self, current_tenant_id: str, current_user: Account, app_model: App):
@model_validate(WorkflowCommentCreatePayload)
def post(
self,
req_data: WorkflowCommentCreatePayload,
current_tenant_id: str,
current_user: Account,
app_model: App,
):
"""Create a new workflow comment."""
payload = WorkflowCommentCreatePayload.model_validate(console_ns.payload or {})
result = WorkflowCommentService.create_comment(
tenant_id=current_tenant_id,
app_id=app_model.id,
created_by=current_user.id,
content=payload.content,
position_x=payload.position_x,
position_y=payload.position_y,
mentioned_user_ids=payload.mentioned_user_ids,
content=req_data.content,
position_x=req_data.position_x,
position_y=req_data.position_y,
mentioned_user_ids=req_data.mentioned_user_ids,
)
return dump_response(WorkflowCommentCreate, result), 201
@@ -289,19 +296,26 @@ class WorkflowCommentDetailApi(Resource):
@with_current_user
@with_current_tenant_id
@get_app_model()
def put(self, current_tenant_id: str, current_user: Account, app_model: App, comment_id: str):
@model_validate(WorkflowCommentUpdatePayload)
def put(
self,
req_data: WorkflowCommentUpdatePayload,
current_tenant_id: str,
current_user: Account,
app_model: App,
comment_id: str,
):
"""Update a workflow comment."""
payload = WorkflowCommentUpdatePayload.model_validate(console_ns.payload or {})
result = WorkflowCommentService.update_comment(
tenant_id=current_tenant_id,
app_id=app_model.id,
comment_id=comment_id,
user_id=current_user.id,
content=payload.content,
position_x=payload.position_x,
position_y=payload.position_y,
mentioned_user_ids=payload.mentioned_user_ids,
content=req_data.content,
position_x=req_data.position_x,
position_y=req_data.position_y,
mentioned_user_ids=req_data.mentioned_user_ids,
)
return dump_response(WorkflowCommentUpdate, result)
@@ -372,20 +386,26 @@ class WorkflowCommentReplyApi(Resource):
@with_current_user
@with_current_tenant_id
@get_app_model()
def post(self, current_tenant_id: str, current_user: Account, app_model: App, comment_id: str):
@model_validate(WorkflowCommentReplyPayload)
def post(
self,
req_data: WorkflowCommentReplyPayload,
current_tenant_id: str,
current_user: Account,
app_model: App,
comment_id: str,
):
"""Add a reply to a workflow comment."""
# Validate comment access first
WorkflowCommentService.validate_comment_access(
comment_id=comment_id, tenant_id=current_tenant_id, app_id=app_model.id
)
payload = WorkflowCommentReplyPayload.model_validate(console_ns.payload or {})
result = WorkflowCommentService.create_reply(
comment_id=comment_id,
content=payload.content,
content=req_data.content,
created_by=current_user.id,
mentioned_user_ids=payload.mentioned_user_ids,
mentioned_user_ids=req_data.mentioned_user_ids,
)
return dump_response(WorkflowCommentReplyCreate, result), 201
@@ -407,23 +427,30 @@ class WorkflowCommentReplyDetailApi(Resource):
@with_current_user
@with_current_tenant_id
@get_app_model()
def put(self, current_tenant_id: str, current_user: Account, app_model: App, comment_id: str, reply_id: str):
@model_validate(WorkflowCommentReplyPayload)
def put(
self,
req_data: WorkflowCommentReplyPayload,
current_tenant_id: str,
current_user: Account,
app_model: App,
comment_id: str,
reply_id: str,
):
"""Update a comment reply."""
# Validate comment access first
WorkflowCommentService.validate_comment_access(
comment_id=comment_id, tenant_id=current_tenant_id, app_id=app_model.id
)
payload = WorkflowCommentReplyPayload.model_validate(console_ns.payload or {})
reply = WorkflowCommentService.update_reply(
tenant_id=current_tenant_id,
app_id=app_model.id,
comment_id=comment_id,
reply_id=reply_id,
user_id=current_user.id,
content=payload.content,
mentioned_user_ids=payload.mentioned_user_ids,
content=req_data.content,
mentioned_user_ids=req_data.mentioned_user_ids,
)
return dump_response(WorkflowCommentReplyUpdate, reply)
@@ -1,12 +1,12 @@
import logging
from collections.abc import Callable
from functools import wraps
from typing import Any, Concatenate, TypedDict, override
from typing import Any, Concatenate, Self, TypedDict, override
from uuid import UUID
from flask import Response, request
from flask import Response
from flask_restx import Resource, fields, marshal, marshal_with
from pydantic import BaseModel, ConfigDict, Field
from pydantic import BaseModel, ConfigDict, Field, model_validator
from sqlalchemy.orm import sessionmaker
from controllers.common.errors import InvalidArgumentError, NotFoundError
@@ -22,11 +22,13 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_user,
)
from core.app.file_access import DatabaseFileAccessController
from core.workflow.llm_environment_variable import environment_variable_value_type
from core.workflow.variable_prefixes import CONVERSATION_VARIABLE_NODE_ID, SYSTEM_VARIABLE_NODE_ID
from extensions.ext_database import db
from factories import variable_factory
@@ -109,6 +111,35 @@ class EnvironmentVariableUpdatePayload(BaseModel):
...,
description="Environment variables for the draft workflow",
)
patch: bool = Field(
default=False,
description="Treat environment_variables as per-ID upserts instead of replacing the full collection",
)
deleted_environment_variable_ids: list[str] = Field(
default_factory=list,
description="Environment variable IDs to delete when patch is true",
)
@model_validator(mode="after")
def validate_patch(self) -> Self:
"""Validate the per-variable patch contract without changing legacy replacement requests."""
if not self.patch:
if self.deleted_environment_variable_ids:
raise ValueError("deleted_environment_variable_ids requires patch=true")
return self
upsert_ids = [variable.id for variable in self.environment_variables]
if any(not variable_id for variable_id in upsert_ids):
raise ValueError("patched environment variables require an id")
if len(set(upsert_ids)) != len(upsert_ids):
raise ValueError("patched environment variable ids must be unique")
if any(not variable_id for variable_id in self.deleted_environment_variable_ids):
raise ValueError("deleted environment variable ids must not be empty")
if len(set(self.deleted_environment_variable_ids)) != len(self.deleted_environment_variable_ids):
raise ValueError("deleted environment variable ids must be unique")
if set(upsert_ids).intersection(self.deleted_environment_variable_ids):
raise ValueError("an environment variable cannot be upserted and deleted in the same patch")
return self
class EnvironmentVariableItemResponse(ResponseModel):
@@ -329,11 +360,11 @@ class WorkflowVariableCollectionApi(Resource):
@_api_prerequisite
@marshal_with(workflow_draft_variable_list_without_value_model)
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
def get(self, current_user: Account, app_model: App):
@model_validate(WorkflowDraftVariableListQuery)
def get(self, req_data: WorkflowDraftVariableListQuery, current_user: Account, app_model: App):
"""
Get draft workflow
"""
args = WorkflowDraftVariableListQuery.model_validate(request.args.to_dict(flat=True))
# fetch draft workflow by app_model
workflow_service = WorkflowService()
@@ -348,8 +379,8 @@ class WorkflowVariableCollectionApi(Resource):
)
workflow_vars = draft_var_srv.list_variables_without_values(
app_id=app_model.id,
page=args.page,
limit=args.limit,
page=req_data.page,
limit=req_data.limit,
user_id=current_user.id,
)
@@ -449,7 +480,14 @@ class VariableApi(Resource):
@console_ns.response(404, "Variable not found")
@_api_prerequisite
@marshal_with(workflow_draft_variable_model)
def patch(self, current_user: Account, app_model: App, variable_id: UUID):
@model_validate(WorkflowDraftVariableUpdatePayload)
def patch(
self,
req_data: WorkflowDraftVariableUpdatePayload,
current_user: Account,
app_model: App,
variable_id: UUID,
):
# Request payload for file types:
#
# Local File:
@@ -474,7 +512,6 @@ class VariableApi(Resource):
draft_var_srv = WorkflowDraftVariableService(
session=db.session(),
)
args_model = WorkflowDraftVariableUpdatePayload.model_validate(console_ns.payload or {})
variable_id_str = str(variable_id)
variable = ensure_variable_access(
@@ -484,8 +521,8 @@ class VariableApi(Resource):
current_user_id=current_user.id,
)
new_name = args_model.name
raw_value = args_model.value
new_name = req_data.name
raw_value = req_data.value
if new_name is None and raw_value is None:
return variable
@@ -630,13 +667,13 @@ class ConversationVariableCollectionApi(Resource):
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
@with_current_user
@get_app_model(mode=AppMode.ADVANCED_CHAT)
def post(self, current_user: Account, app_model: App):
payload = ConversationVariableUpdatePayload.model_validate(console_ns.payload or {})
@model_validate(ConversationVariableUpdatePayload)
def post(self, req_data: ConversationVariableUpdatePayload, current_user: Account, app_model: App):
workflow_service = WorkflowService()
conversation_variables_list = [
variable.model_dump(mode="json", exclude_unset=True) for variable in payload.conversation_variables
variable.model_dump(mode="json", exclude_unset=True) for variable in req_data.conversation_variables
]
conversation_variables = [
variable_factory.build_conversation_variable_from_mapping(obj) for obj in conversation_variables_list
@@ -698,7 +735,7 @@ class EnvironmentVariableCollectionApi(Resource):
"name": v.name,
"description": v.description,
"selector": v.selector,
"value_type": str(v.value_type.exposed_type()),
"value_type": environment_variable_value_type(v),
"value": v.value,
# Do not track edited for env vars.
"edited": False,
@@ -725,23 +762,32 @@ class EnvironmentVariableCollectionApi(Resource):
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
@with_current_user
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
def post(self, current_user: Account, app_model: App):
payload = EnvironmentVariableUpdatePayload.model_validate(console_ns.payload or {})
@model_validate(EnvironmentVariableUpdatePayload)
def post(self, req_data: EnvironmentVariableUpdatePayload, current_user: Account, app_model: App):
workflow_service = WorkflowService()
environment_variables_list = [
variable.model_dump(mode="json", exclude_unset=True) for variable in payload.environment_variables
variable.model_dump(mode="json", exclude_unset=True) for variable in req_data.environment_variables
]
environment_variables = [
variable_factory.build_environment_variable_from_mapping(obj) for obj in environment_variables_list
]
workflow_service.update_draft_workflow_environment_variables(
app_model=app_model,
account=current_user,
environment_variables=environment_variables,
session=db.session(),
)
if req_data.patch:
workflow_service.patch_draft_workflow_environment_variables(
app_model=app_model,
account=current_user,
environment_variables=environment_variables,
deleted_environment_variable_ids=req_data.deleted_environment_variable_ids,
session=db.session(),
)
else:
workflow_service.update_draft_workflow_environment_variables(
app_model=app_model,
account=current_user,
environment_variables=environment_variables,
session=db.session(),
)
return {"result": "success"}
+29 -29
View File
@@ -2,7 +2,6 @@ from datetime import UTC, datetime, timedelta
from typing import Literal
from uuid import UUID
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field, field_validator
from sqlalchemy import select
@@ -17,6 +16,7 @@ from controllers.console.wraps import (
RBACPermission,
RBACResourceScope,
account_initialization_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -160,21 +160,21 @@ class AdvancedChatAppWorkflowRunListApi(Resource):
@account_initialization_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT)
@get_app_model(mode=[AppMode.ADVANCED_CHAT])
def get(self, app_model: App):
@model_validate(WorkflowRunListQuery)
def get(self, req_data: WorkflowRunListQuery, app_model: App):
"""
Get advanced chat app workflow run list
"""
args_model = WorkflowRunListQuery.model_validate(request.args.to_dict(flat=True))
args: WorkflowRunListArgs = {"limit": args_model.limit}
if args_model.last_id is not None:
args["last_id"] = args_model.last_id
if args_model.status is not None:
args["status"] = args_model.status
args: WorkflowRunListArgs = {"limit": req_data.limit}
if req_data.last_id is not None:
args["last_id"] = req_data.last_id
if req_data.status is not None:
args["status"] = req_data.status
# Default to DEBUGGING if not specified
triggered_from = (
WorkflowRunTriggeredFrom(args_model.triggered_from)
if args_model.triggered_from
WorkflowRunTriggeredFrom(req_data.triggered_from)
if req_data.triggered_from
else WorkflowRunTriggeredFrom.DEBUGGING
)
@@ -258,17 +258,17 @@ class AdvancedChatAppWorkflowRunCountApi(Resource):
@account_initialization_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT)
@get_app_model(mode=[AppMode.ADVANCED_CHAT])
def get(self, app_model: App):
@model_validate(WorkflowRunCountQuery)
def get(self, req_data: WorkflowRunCountQuery, app_model: App):
"""
Get advanced chat workflow runs count statistics
"""
args_model = WorkflowRunCountQuery.model_validate(request.args.to_dict(flat=True))
args = args_model.model_dump(exclude_none=True)
args = req_data.model_dump(exclude_none=True)
# Default to DEBUGGING if not specified
triggered_from = (
WorkflowRunTriggeredFrom(args_model.triggered_from)
if args_model.triggered_from
WorkflowRunTriggeredFrom(req_data.triggered_from)
if req_data.triggered_from
else WorkflowRunTriggeredFrom.DEBUGGING
)
@@ -299,21 +299,21 @@ class WorkflowRunListApi(Resource):
@account_initialization_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT)
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
def get(self, app_model: App):
@model_validate(WorkflowRunListQuery)
def get(self, req_data: WorkflowRunListQuery, app_model: App):
"""
Get workflow run list
"""
args_model = WorkflowRunListQuery.model_validate(request.args.to_dict(flat=True))
args: WorkflowRunListArgs = {"limit": args_model.limit}
if args_model.last_id is not None:
args["last_id"] = args_model.last_id
if args_model.status is not None:
args["status"] = args_model.status
args: WorkflowRunListArgs = {"limit": req_data.limit}
if req_data.last_id is not None:
args["last_id"] = req_data.last_id
if req_data.status is not None:
args["status"] = req_data.status
# Default to DEBUGGING for workflow if not specified (backward compatibility)
triggered_from = (
WorkflowRunTriggeredFrom(args_model.triggered_from)
if args_model.triggered_from
WorkflowRunTriggeredFrom(req_data.triggered_from)
if req_data.triggered_from
else WorkflowRunTriggeredFrom.DEBUGGING
)
@@ -341,17 +341,17 @@ class WorkflowRunCountApi(Resource):
@account_initialization_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT)
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
def get(self, app_model: App):
@model_validate(WorkflowRunCountQuery)
def get(self, req_data: WorkflowRunCountQuery, app_model: App):
"""
Get workflow runs count statistics
"""
args_model = WorkflowRunCountQuery.model_validate(request.args.to_dict(flat=True))
args = args_model.model_dump(exclude_none=True)
args = req_data.model_dump(exclude_none=True)
# Default to DEBUGGING for workflow if not specified (backward compatibility)
triggered_from = (
WorkflowRunTriggeredFrom(args_model.triggered_from)
if args_model.triggered_from
WorkflowRunTriggeredFrom(req_data.triggered_from)
if req_data.triggered_from
else WorkflowRunTriggeredFrom.DEBUGGING
)

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