From 9e54f086dc4bafc0e0e68f554daa4e5b04fb2c33 Mon Sep 17 00:00:00 2001 From: lif <1835304752@qq.com> Date: Thu, 5 Feb 2026 15:42:18 +0800 Subject: [PATCH 01/10] fix(web): add rewrite rule to fix Serwist precaching 404 errors (#31770) Signed-off-by: majiayu000 <1835304752@qq.com> Co-authored-by: Stephen Zhou <38493346+hyoban@users.noreply.github.com> --- web/app/serwist/[path]/route.ts | 5 +- web/app/sw.ts | 90 ++++------------ web/next.config.ts | 2 +- web/package.json | 7 +- web/pnpm-lock.yaml | 181 ++++++++++++++++++-------------- 5 files changed, 129 insertions(+), 156 deletions(-) diff --git a/web/app/serwist/[path]/route.ts b/web/app/serwist/[path]/route.ts index ad371756b5..beca2cd412 100644 --- a/web/app/serwist/[path]/route.ts +++ b/web/app/serwist/[path]/route.ts @@ -1,14 +1,11 @@ -import { spawnSync } from 'node:child_process' -import { randomUUID } from 'node:crypto' import { createSerwistRoute } from '@serwist/turbopack' const basePath = process.env.NEXT_PUBLIC_BASE_PATH || '' -const revision = spawnSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf-8' }).stdout?.trim() || randomUUID() export const { dynamic, dynamicParams, revalidate, generateStaticParams, GET } = createSerwistRoute({ - additionalPrecacheEntries: [{ url: `${basePath}/_offline.html`, revision }], swSrc: 'app/sw.ts', nextConfig: { basePath, }, + useNativeEsbuild: true, }) diff --git a/web/app/sw.ts b/web/app/sw.ts index 99ef2c3544..f4011ee224 100644 --- a/web/app/sw.ts +++ b/web/app/sw.ts @@ -3,7 +3,9 @@ /// import type { PrecacheEntry, SerwistGlobalConfig } from 'serwist' -import { CacheableResponsePlugin, CacheFirst, ExpirationPlugin, NetworkFirst, Serwist, StaleWhileRevalidate } from 'serwist' +import { defaultCache } from '@serwist/turbopack/worker' +import { Serwist } from 'serwist' +import { withLeadingSlash } from 'ufo' declare global { // eslint-disable-next-line ts/consistent-type-definitions @@ -18,78 +20,30 @@ const scopePathname = new URL(self.registration.scope).pathname const basePath = scopePathname.replace(/\/serwist\/$/, '').replace(/\/$/, '') const offlineUrl = `${basePath}/_offline.html` +const normalizeManifestUrl = (url: string): string => { + if (url.startsWith('/serwist/')) + return url.replace(/^\/serwist\//, '/') + + return withLeadingSlash(url) +} + +const manifest = self.__SW_MANIFEST?.map((entry) => { + if (typeof entry === 'string') + return normalizeManifestUrl(entry) + + return { + ...entry, + url: normalizeManifestUrl(entry.url), + } +}) + const serwist = new Serwist({ - precacheEntries: self.__SW_MANIFEST, + precacheEntries: manifest, skipWaiting: true, disableDevLogs: true, clientsClaim: true, navigationPreload: true, - runtimeCaching: [ - { - matcher: ({ url }) => url.origin === 'https://fonts.googleapis.com', - handler: new CacheFirst({ - cacheName: 'google-fonts', - plugins: [ - new CacheableResponsePlugin({ statuses: [0, 200] }), - new ExpirationPlugin({ - maxEntries: 4, - maxAgeSeconds: 365 * 24 * 60 * 60, - }), - ], - }), - }, - { - matcher: ({ url }) => url.origin === 'https://fonts.gstatic.com', - handler: new CacheFirst({ - cacheName: 'google-fonts-webfonts', - plugins: [ - new CacheableResponsePlugin({ statuses: [0, 200] }), - new ExpirationPlugin({ - maxEntries: 4, - maxAgeSeconds: 365 * 24 * 60 * 60, - }), - ], - }), - }, - { - matcher: ({ request }) => request.destination === 'image', - handler: new CacheFirst({ - cacheName: 'images', - plugins: [ - new CacheableResponsePlugin({ statuses: [0, 200] }), - new ExpirationPlugin({ - maxEntries: 64, - maxAgeSeconds: 30 * 24 * 60 * 60, - }), - ], - }), - }, - { - matcher: ({ request }) => request.destination === 'script' || request.destination === 'style', - handler: new StaleWhileRevalidate({ - cacheName: 'static-resources', - plugins: [ - new ExpirationPlugin({ - maxEntries: 32, - maxAgeSeconds: 24 * 60 * 60, - }), - ], - }), - }, - { - matcher: ({ url, sameOrigin }) => sameOrigin && url.pathname.startsWith('/api/'), - handler: new NetworkFirst({ - cacheName: 'api-cache', - networkTimeoutSeconds: 10, - plugins: [ - new ExpirationPlugin({ - maxEntries: 16, - maxAgeSeconds: 60 * 60, - }), - ], - }), - }, - ], + runtimeCaching: defaultCache, fallbacks: { entries: [ { diff --git a/web/next.config.ts b/web/next.config.ts index 05f4158ac8..0bbdbaf32c 100644 --- a/web/next.config.ts +++ b/web/next.config.ts @@ -29,7 +29,7 @@ const remoteImageURLs = ([hasSetWebPrefix ? new URL(`${process.env.NEXT_PUBLIC_W const nextConfig: NextConfig = { basePath: process.env.NEXT_PUBLIC_BASE_PATH || '', - serverExternalPackages: ['esbuild-wasm'], + serverExternalPackages: ['esbuild'], transpilePackages: ['echarts', 'zrender'], turbopack: { rules: codeInspectorPlugin({ diff --git a/web/package.json b/web/package.json index b37a46681f..c85de48b5d 100644 --- a/web/package.json +++ b/web/package.json @@ -156,6 +156,7 @@ "string-ts": "2.3.1", "tailwind-merge": "2.6.0", "tldts": "7.0.17", + "ufo": "1.6.3", "use-context-selector": "2.0.0", "uuid": "10.0.0", "zod": "3.25.76", @@ -172,7 +173,7 @@ "@next/eslint-plugin-next": "16.1.6", "@next/mdx": "16.1.5", "@rgrove/parse-xml": "4.2.0", - "@serwist/turbopack": "9.5.0", + "@serwist/turbopack": "9.5.4", "@storybook/addon-docs": "10.2.0", "@storybook/addon-links": "10.2.0", "@storybook/addon-onboarding": "10.2.0", @@ -210,7 +211,7 @@ "autoprefixer": "10.4.21", "code-inspector-plugin": "1.3.6", "cross-env": "10.1.0", - "esbuild-wasm": "0.27.2", + "esbuild": "0.27.2", "eslint": "9.39.2", "eslint-plugin-react-hooks": "7.0.1", "eslint-plugin-react-refresh": "0.4.26", @@ -226,7 +227,7 @@ "postcss": "8.5.6", "react-scan": "0.4.3", "sass": "1.93.2", - "serwist": "9.5.0", + "serwist": "9.5.4", "storybook": "10.2.0", "tailwindcss": "3.4.18", "tsx": "4.21.0", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 5266b4ac6f..d7ba2570d4 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -347,6 +347,9 @@ importers: tldts: specifier: 7.0.17 version: 7.0.17 + ufo: + specifier: 1.6.3 + version: 1.6.3 use-context-selector: specifier: 2.0.0 version: 2.0.0(react@19.2.4)(scheduler@0.27.0) @@ -391,8 +394,8 @@ importers: specifier: 4.2.0 version: 4.2.0 '@serwist/turbopack': - specifier: 9.5.0 - version: 9.5.0(@swc/helpers@0.5.18)(esbuild-wasm@0.27.2)(next@16.1.5(@babel/core@7.28.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.93.2))(react@19.2.4)(typescript@5.9.3) + specifier: 9.5.4 + version: 9.5.4(@swc/helpers@0.5.18)(esbuild-wasm@0.27.2)(esbuild@0.27.2)(next@16.1.5(@babel/core@7.28.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.93.2))(react@19.2.4)(typescript@5.9.3) '@storybook/addon-docs': specifier: 10.2.0 version: 10.2.0(@types/react@19.2.9)(esbuild@0.27.2)(rollup@4.56.0)(storybook@10.2.0(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@18.15.0)(jiti@1.21.7)(sass@1.93.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.104.1(esbuild@0.27.2)(uglify-js@3.19.3)) @@ -504,7 +507,7 @@ importers: cross-env: specifier: 10.1.0 version: 10.1.0 - esbuild-wasm: + esbuild: specifier: 0.27.2 version: 0.27.2 eslint: @@ -553,8 +556,8 @@ importers: specifier: 1.93.2 version: 1.93.2 serwist: - specifier: 9.5.0 - version: 9.5.0(typescript@5.9.3) + specifier: 9.5.4 + version: 9.5.4(browserslist@4.28.1)(typescript@5.9.3) storybook: specifier: 10.2.0 version: 10.2.0(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -2542,8 +2545,8 @@ packages: peerDependencies: react: ^16.14.0 || 17.x || 18.x || 19.x - '@serwist/build@9.5.0': - resolution: {integrity: sha512-8D330WwYjBI5MadyVOphwUqJLMNQK76KWBoDykIPrbtt0C3uGFPxG4XNZCFXBkRG3O1QLedv9BWqH27SeqhcLg==} + '@serwist/build@9.5.4': + resolution: {integrity: sha512-FTiNsNb3luKsLIxjKCvkPiqFZSbx7yVNOFGSUhp4lyfzgnelT1M3/lMC88kLiak90emkuFjSkQgwa6OnyhMZlQ==} engines: {node: '>=18.0.0'} peerDependencies: typescript: '>=5.0.0' @@ -2551,23 +2554,33 @@ packages: typescript: optional: true - '@serwist/turbopack@9.5.0': - resolution: {integrity: sha512-MPxDapkN6LPG25I8LgOxQHc2ifIWK8WY+4pOdAbAFmN4FvsgLwYJ/W4531lsRWu08sq5IZ9JlRtt6KLNm5DHSQ==} + '@serwist/turbopack@9.5.4': + resolution: {integrity: sha512-HerOIc2z3LWbFVq/gXK44I99KdF+x0uBI7cPHb+Q3q0WpF50d/i5fV5pZZXCf3LCqtc9oH0VlY6FWDcjWjHI8g==} engines: {node: '>=18.0.0'} peerDependencies: + esbuild: 0.27.2 esbuild-wasm: '>=0.25.0 <1.0.0' next: '>=14.0.0' react: '>=18.0.0' typescript: '>=5.0.0' peerDependenciesMeta: + esbuild: + optional: true + esbuild-wasm: + optional: true typescript: optional: true - '@serwist/utils@9.5.0': - resolution: {integrity: sha512-DBzmJgL63/VYcQ/TpSYXW1FJKRILesOxytu+1MHY0vW2WFhW7pYkLgVuHqksP5K+3ypt54M3+2QNDDMixrnutQ==} + '@serwist/utils@9.5.4': + resolution: {integrity: sha512-uyriGQF1qjNEHXXfsd8XJ5kfK3/MezEaUw//XdHjZeJ0LvLamrgnLJGQQoyJqUfEPCiJ4jJwc4uYMB9LjLiHxA==} + peerDependencies: + browserslist: '>=4' + peerDependenciesMeta: + browserslist: + optional: true - '@serwist/window@9.5.0': - resolution: {integrity: sha512-WqmEZjJ+u841sbUJh2LAbtPNrz8mU/wCTo9sEVqsMOk+EM5oBz5FRpF3kDzx1cF5rTIfXer1df0D354lIdFw1Q==} + '@serwist/window@9.5.4': + resolution: {integrity: sha512-52t2G+TgiWDdRwGG0ArU28uy6/oQYICQfNLHs4ywybyS6mHy3BxHFl+JjB5vhg8znIG1LMpGvOmS5b7AuPVYDw==} peerDependencies: typescript: '>=5.0.0' peerDependenciesMeta: @@ -2719,68 +2732,68 @@ packages: '@svgdotjs/svg.js@3.2.5': resolution: {integrity: sha512-/VNHWYhNu+BS7ktbYoVGrCmsXDh+chFMaONMwGNdIBcFHrWqk2jY8fNyr3DLdtQUIalvkPfM554ZSFa3dm3nxQ==} - '@swc/core-darwin-arm64@1.15.8': - resolution: {integrity: sha512-M9cK5GwyWWRkRGwwCbREuj6r8jKdES/haCZ3Xckgkl8MUQJZA3XB7IXXK1IXRNeLjg6m7cnoMICpXv1v1hlJOg==} + '@swc/core-darwin-arm64@1.15.11': + resolution: {integrity: sha512-QoIupRWVH8AF1TgxYyeA5nS18dtqMuxNwchjBIwJo3RdwLEFiJq6onOx9JAxHtuPwUkIVuU2Xbp+jCJ7Vzmgtg==} engines: {node: '>=10'} cpu: [arm64] os: [darwin] - '@swc/core-darwin-x64@1.15.8': - resolution: {integrity: sha512-j47DasuOvXl80sKJHSi2X25l44CMc3VDhlJwA7oewC1nV1VsSzwX+KOwE5tLnfORvVJJyeiXgJORNYg4jeIjYQ==} + '@swc/core-darwin-x64@1.15.11': + resolution: {integrity: sha512-S52Gu1QtPSfBYDiejlcfp9GlN+NjTZBRRNsz8PNwBgSE626/FUf2PcllVUix7jqkoMC+t0rS8t+2/aSWlMuQtA==} engines: {node: '>=10'} cpu: [x64] os: [darwin] - '@swc/core-linux-arm-gnueabihf@1.15.8': - resolution: {integrity: sha512-siAzDENu2rUbwr9+fayWa26r5A9fol1iORG53HWxQL1J8ym4k7xt9eME0dMPXlYZDytK5r9sW8zEA10F2U3Xwg==} + '@swc/core-linux-arm-gnueabihf@1.15.11': + resolution: {integrity: sha512-lXJs8oXo6Z4yCpimpQ8vPeCjkgoHu5NoMvmJZ8qxDyU99KVdg6KwU9H79vzrmB+HfH+dCZ7JGMqMF//f8Cfvdg==} engines: {node: '>=10'} cpu: [arm] os: [linux] - '@swc/core-linux-arm64-gnu@1.15.8': - resolution: {integrity: sha512-o+1y5u6k2FfPYbTRUPvurwzNt5qd0NTumCTFscCNuBksycloXY16J8L+SMW5QRX59n4Hp9EmFa3vpvNHRVv1+Q==} + '@swc/core-linux-arm64-gnu@1.15.11': + resolution: {integrity: sha512-chRsz1K52/vj8Mfq/QOugVphlKPWlMh10V99qfH41hbGvwAU6xSPd681upO4bKiOr9+mRIZZW+EfJqY42ZzRyA==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-arm64-musl@1.15.8': - resolution: {integrity: sha512-koiCqL09EwOP1S2RShCI7NbsQuG6r2brTqUYE7pV7kZm9O17wZ0LSz22m6gVibpwEnw8jI3IE1yYsQTVpluALw==} + '@swc/core-linux-arm64-musl@1.15.11': + resolution: {integrity: sha512-PYftgsTaGnfDK4m6/dty9ryK1FbLk+LosDJ/RJR2nkXGc8rd+WenXIlvHjWULiBVnS1RsjHHOXmTS4nDhe0v0w==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-x64-gnu@1.15.8': - resolution: {integrity: sha512-4p6lOMU3bC+Vd5ARtKJ/FxpIC5G8v3XLoPEZ5s7mLR8h7411HWC/LmTXDHcrSXRC55zvAVia1eldy6zDLz8iFQ==} + '@swc/core-linux-x64-gnu@1.15.11': + resolution: {integrity: sha512-DKtnJKIHiZdARyTKiX7zdRjiDS1KihkQWatQiCHMv+zc2sfwb4Glrodx2VLOX4rsa92NLR0Sw8WLcPEMFY1szQ==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-linux-x64-musl@1.15.8': - resolution: {integrity: sha512-z3XBnbrZAL+6xDGAhJoN4lOueIxC/8rGrJ9tg+fEaeqLEuAtHSW2QHDHxDwkxZMjuF/pZ6MUTjHjbp8wLbuRLA==} + '@swc/core-linux-x64-musl@1.15.11': + resolution: {integrity: sha512-mUjjntHj4+8WBaiDe5UwRNHuEzLjIWBTSGTw0JT9+C9/Yyuh4KQqlcEQ3ro6GkHmBGXBFpGIj/o5VMyRWfVfWw==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-win32-arm64-msvc@1.15.8': - resolution: {integrity: sha512-djQPJ9Rh9vP8GTS/Df3hcc6XP6xnG5c8qsngWId/BLA9oX6C7UzCPAn74BG/wGb9a6j4w3RINuoaieJB3t+7iQ==} + '@swc/core-win32-arm64-msvc@1.15.11': + resolution: {integrity: sha512-ZkNNG5zL49YpaFzfl6fskNOSxtcZ5uOYmWBkY4wVAvgbSAQzLRVBp+xArGWh2oXlY/WgL99zQSGTv7RI5E6nzA==} engines: {node: '>=10'} cpu: [arm64] os: [win32] - '@swc/core-win32-ia32-msvc@1.15.8': - resolution: {integrity: sha512-/wfAgxORg2VBaUoFdytcVBVCgf1isWZIEXB9MZEUty4wwK93M/PxAkjifOho9RN3WrM3inPLabICRCEgdHpKKQ==} + '@swc/core-win32-ia32-msvc@1.15.11': + resolution: {integrity: sha512-6XnzORkZCQzvTQ6cPrU7iaT9+i145oLwnin8JrfsLG41wl26+5cNQ2XV3zcbrnFEV6esjOceom9YO1w9mGJByw==} engines: {node: '>=10'} cpu: [ia32] os: [win32] - '@swc/core-win32-x64-msvc@1.15.8': - resolution: {integrity: sha512-GpMePrh9Sl4d61o4KAHOOv5is5+zt6BEXCOCgs/H0FLGeii7j9bWDE8ExvKFy2GRRZVNR1ugsnzaGWHKM6kuzA==} + '@swc/core-win32-x64-msvc@1.15.11': + resolution: {integrity: sha512-IQ2n6af7XKLL6P1gIeZACskSxK8jWtoKpJWLZmdXTDj1MGzktUy4i+FvpdtxFmJWNavRWH1VmTr6kAubRDHeKw==} engines: {node: '>=10'} cpu: [x64] os: [win32] - '@swc/core@1.15.8': - resolution: {integrity: sha512-T8keoJjXaSUoVBCIjgL6wAnhADIb09GOELzKg10CjNg+vLX48P93SME6jTfte9MZIm5m+Il57H3rTSk/0kzDUw==} + '@swc/core@1.15.11': + resolution: {integrity: sha512-iLmLTodbYxU39HhMPaMUooPwO/zqJWvsqkrXv1ZI38rMb048p6N7qtAtTp37sw9NzSrvH6oli8EdDygo09IZ/w==} engines: {node: '>=10'} peerDependencies: '@swc/helpers': '>=0.5.17' @@ -4877,11 +4890,13 @@ packages: glob@10.5.0: resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@11.1.0: resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} engines: {node: 20 || >=22} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true globals@14.0.0: @@ -6601,8 +6616,8 @@ packages: server-only@0.0.1: resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} - serwist@9.5.0: - resolution: {integrity: sha512-wjrsPWHI5ZM20jIsVKZGN/uAdS2aKOgmIOE4dqUaFhK6SVIzgoJZjTnZ3v29T+NmneuD753jlhGui9eYypsj0A==} + serwist@9.5.4: + resolution: {integrity: sha512-uTHBzpIeA6rE3oyRt392MbtNQDs2JVZelKD1KkT18UkhX6HRwCeassoI1Nd1h52DqYqa7ZfBeldJ4awy+PYrnQ==} peerDependencies: typescript: '>=5.0.0' peerDependenciesMeta: @@ -7480,9 +7495,6 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} - zod@4.3.5: - resolution: {integrity: sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==} - zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} @@ -9603,43 +9615,51 @@ snapshots: hoist-non-react-statics: 3.3.2 react: 19.2.4 - '@serwist/build@9.5.0(typescript@5.9.3)': + '@serwist/build@9.5.4(browserslist@4.28.1)(typescript@5.9.3)': dependencies: - '@serwist/utils': 9.5.0 + '@serwist/utils': 9.5.4(browserslist@4.28.1) common-tags: 1.8.2 glob: 10.5.0 pretty-bytes: 6.1.1 source-map: 0.8.0-beta.0 - zod: 4.3.5 + zod: 4.3.6 optionalDependencies: typescript: 5.9.3 + transitivePeerDependencies: + - browserslist - '@serwist/turbopack@9.5.0(@swc/helpers@0.5.18)(esbuild-wasm@0.27.2)(next@16.1.5(@babel/core@7.28.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.93.2))(react@19.2.4)(typescript@5.9.3)': + '@serwist/turbopack@9.5.4(@swc/helpers@0.5.18)(esbuild-wasm@0.27.2)(esbuild@0.27.2)(next@16.1.5(@babel/core@7.28.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.93.2))(react@19.2.4)(typescript@5.9.3)': dependencies: - '@serwist/build': 9.5.0(typescript@5.9.3) - '@serwist/utils': 9.5.0 - '@serwist/window': 9.5.0(typescript@5.9.3) - '@swc/core': 1.15.8(@swc/helpers@0.5.18) - esbuild-wasm: 0.27.2 + '@serwist/build': 9.5.4(browserslist@4.28.1)(typescript@5.9.3) + '@serwist/utils': 9.5.4(browserslist@4.28.1) + '@serwist/window': 9.5.4(browserslist@4.28.1)(typescript@5.9.3) + '@swc/core': 1.15.11(@swc/helpers@0.5.18) + browserslist: 4.28.1 kolorist: 1.8.0 next: 16.1.5(@babel/core@7.28.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.93.2) react: 19.2.4 semver: 7.7.3 - serwist: 9.5.0(typescript@5.9.3) - zod: 4.3.5 + serwist: 9.5.4(browserslist@4.28.1)(typescript@5.9.3) + zod: 4.3.6 optionalDependencies: + esbuild: 0.27.2 + esbuild-wasm: 0.27.2 typescript: 5.9.3 transitivePeerDependencies: - '@swc/helpers' - '@serwist/utils@9.5.0': {} + '@serwist/utils@9.5.4(browserslist@4.28.1)': + optionalDependencies: + browserslist: 4.28.1 - '@serwist/window@9.5.0(typescript@5.9.3)': + '@serwist/window@9.5.4(browserslist@4.28.1)(typescript@5.9.3)': dependencies: '@types/trusted-types': 2.0.7 - serwist: 9.5.0(typescript@5.9.3) + serwist: 9.5.4(browserslist@4.28.1)(typescript@5.9.3) optionalDependencies: typescript: 5.9.3 + transitivePeerDependencies: + - browserslist '@sindresorhus/base62@1.0.0': {} @@ -9821,51 +9841,51 @@ snapshots: '@svgdotjs/svg.js@3.2.5': {} - '@swc/core-darwin-arm64@1.15.8': + '@swc/core-darwin-arm64@1.15.11': optional: true - '@swc/core-darwin-x64@1.15.8': + '@swc/core-darwin-x64@1.15.11': optional: true - '@swc/core-linux-arm-gnueabihf@1.15.8': + '@swc/core-linux-arm-gnueabihf@1.15.11': optional: true - '@swc/core-linux-arm64-gnu@1.15.8': + '@swc/core-linux-arm64-gnu@1.15.11': optional: true - '@swc/core-linux-arm64-musl@1.15.8': + '@swc/core-linux-arm64-musl@1.15.11': optional: true - '@swc/core-linux-x64-gnu@1.15.8': + '@swc/core-linux-x64-gnu@1.15.11': optional: true - '@swc/core-linux-x64-musl@1.15.8': + '@swc/core-linux-x64-musl@1.15.11': optional: true - '@swc/core-win32-arm64-msvc@1.15.8': + '@swc/core-win32-arm64-msvc@1.15.11': optional: true - '@swc/core-win32-ia32-msvc@1.15.8': + '@swc/core-win32-ia32-msvc@1.15.11': optional: true - '@swc/core-win32-x64-msvc@1.15.8': + '@swc/core-win32-x64-msvc@1.15.11': optional: true - '@swc/core@1.15.8(@swc/helpers@0.5.18)': + '@swc/core@1.15.11(@swc/helpers@0.5.18)': dependencies: '@swc/counter': 0.1.3 '@swc/types': 0.1.25 optionalDependencies: - '@swc/core-darwin-arm64': 1.15.8 - '@swc/core-darwin-x64': 1.15.8 - '@swc/core-linux-arm-gnueabihf': 1.15.8 - '@swc/core-linux-arm64-gnu': 1.15.8 - '@swc/core-linux-arm64-musl': 1.15.8 - '@swc/core-linux-x64-gnu': 1.15.8 - '@swc/core-linux-x64-musl': 1.15.8 - '@swc/core-win32-arm64-msvc': 1.15.8 - '@swc/core-win32-ia32-msvc': 1.15.8 - '@swc/core-win32-x64-msvc': 1.15.8 + '@swc/core-darwin-arm64': 1.15.11 + '@swc/core-darwin-x64': 1.15.11 + '@swc/core-linux-arm-gnueabihf': 1.15.11 + '@swc/core-linux-arm64-gnu': 1.15.11 + '@swc/core-linux-arm64-musl': 1.15.11 + '@swc/core-linux-x64-gnu': 1.15.11 + '@swc/core-linux-x64-musl': 1.15.11 + '@swc/core-win32-arm64-msvc': 1.15.11 + '@swc/core-win32-ia32-msvc': 1.15.11 + '@swc/core-win32-x64-msvc': 1.15.11 '@swc/helpers': 0.5.18 '@swc/counter@0.1.3': {} @@ -11653,7 +11673,8 @@ snapshots: esast-util-from-estree: 2.0.0 vfile-message: 4.0.3 - esbuild-wasm@0.27.2: {} + esbuild-wasm@0.27.2: + optional: true esbuild@0.27.2: optionalDependencies: @@ -14563,12 +14584,14 @@ snapshots: server-only@0.0.1: {} - serwist@9.5.0(typescript@5.9.3): + serwist@9.5.4(browserslist@4.28.1)(typescript@5.9.3): dependencies: - '@serwist/utils': 9.5.0 + '@serwist/utils': 9.5.4(browserslist@4.28.1) idb: 8.0.3 optionalDependencies: typescript: 5.9.3 + transitivePeerDependencies: + - browserslist sharp@0.33.5: dependencies: @@ -15487,8 +15510,6 @@ snapshots: zod@3.25.76: {} - zod@4.3.5: {} - zod@4.3.6: {} zrender@5.6.1: From be8f265e43757ef5f70995d75112f9b03b69852b Mon Sep 17 00:00:00 2001 From: wangxiaolei Date: Thu, 5 Feb 2026 17:32:33 +0800 Subject: [PATCH 02/10] fix: fix uuid_generate_v4 only used in postgresql (#31304) --- ...12_25_1039-7df29de0f6be_add_credit_pool.py | 39 ++++++++++++++----- api/models/model.py | 4 +- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/api/migrations/versions/2025_12_25_1039-7df29de0f6be_add_credit_pool.py b/api/migrations/versions/2025_12_25_1039-7df29de0f6be_add_credit_pool.py index e89fcee7e5..6a9bfd2be0 100644 --- a/api/migrations/versions/2025_12_25_1039-7df29de0f6be_add_credit_pool.py +++ b/api/migrations/versions/2025_12_25_1039-7df29de0f6be_add_credit_pool.py @@ -10,6 +10,10 @@ import models as models import sqlalchemy as sa from sqlalchemy.dialects import postgresql + +def _is_pg(conn): + return conn.dialect.name == "postgresql" + # revision identifiers, used by Alembic. revision = '7df29de0f6be' down_revision = '03ea244985ce' @@ -19,16 +23,31 @@ depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('tenant_credit_pools', - sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False), - sa.Column('tenant_id', models.types.StringUUID(), nullable=False), - sa.Column('pool_type', sa.String(length=40), server_default='trial', nullable=False), - sa.Column('quota_limit', sa.BigInteger(), nullable=False), - sa.Column('quota_used', sa.BigInteger(), nullable=False), - sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), - sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), - sa.PrimaryKeyConstraint('id', name='tenant_credit_pool_pkey') - ) + conn = op.get_bind() + + if _is_pg(conn): + op.create_table('tenant_credit_pools', + sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False), + sa.Column('tenant_id', models.types.StringUUID(), nullable=False), + sa.Column('pool_type', sa.String(length=40), server_default='trial', nullable=False), + sa.Column('quota_limit', sa.BigInteger(), nullable=False), + sa.Column('quota_used', sa.BigInteger(), nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), + sa.PrimaryKeyConstraint('id', name='tenant_credit_pool_pkey') + ) + else: + # For MySQL and other databases, UUID should be generated at application level + op.create_table('tenant_credit_pools', + sa.Column('id', models.types.StringUUID(), nullable=False), + sa.Column('tenant_id', models.types.StringUUID(), nullable=False), + sa.Column('pool_type', sa.String(length=40), server_default='trial', nullable=False), + sa.Column('quota_limit', sa.BigInteger(), nullable=False), + sa.Column('quota_used', sa.BigInteger(), nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.func.current_timestamp(), nullable=False), + sa.Column('updated_at', sa.DateTime(), server_default=sa.func.current_timestamp(), nullable=False), + sa.PrimaryKeyConstraint('id', name='tenant_credit_pool_pkey') + ) with op.batch_alter_table('tenant_credit_pools', schema=None) as batch_op: batch_op.create_index('tenant_credit_pool_pool_type_idx', ['pool_type'], unique=False) batch_op.create_index('tenant_credit_pool_tenant_id_idx', ['tenant_id'], unique=False) diff --git a/api/models/model.py b/api/models/model.py index c1c6e04ce9..5a274c29cd 100644 --- a/api/models/model.py +++ b/api/models/model.py @@ -2166,7 +2166,9 @@ class TenantCreditPool(TypeBase): sa.Index("tenant_credit_pool_pool_type_idx", "pool_type"), ) - id: Mapped[str] = mapped_column(StringUUID, primary_key=True, server_default=text("uuid_generate_v4()"), init=False) + id: Mapped[str] = mapped_column( + StringUUID, insert_default=lambda: str(uuid4()), default_factory=lambda: str(uuid4()), init=False + ) tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) pool_type: Mapped[str] = mapped_column(String(40), nullable=False, default="trial", server_default="trial") quota_limit: Mapped[int] = mapped_column(BigInteger, nullable=False, default=0) From 7202a24bcf1a9aa02e9d5f5e419ff44e7d7d99a4 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Thu, 5 Feb 2026 17:36:08 +0800 Subject: [PATCH 03/10] chore: migrate to eslint-better-tailwind (#31969) --- .github/workflows/autofix.yml | 23 - .../components/base/effect/index.stories.tsx | 5 +- .../website-crawl/base/error-message.tsx | 1 - web/docs/lint.md | 5 + web/eslint-suppressions.json | 700 +++++++++++++++++- web/eslint.config.mjs | 44 +- web/package.json | 14 +- web/pnpm-lock.yaml | 359 +++++---- web/utils/classnames.spec.ts | 157 ---- 9 files changed, 941 insertions(+), 367 deletions(-) delete mode 100644 web/utils/classnames.spec.ts diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml index 4a8c61e7d2..4571fd1cd1 100644 --- a/.github/workflows/autofix.yml +++ b/.github/workflows/autofix.yml @@ -79,29 +79,6 @@ jobs: find . -name "*.py" -type f -exec sed -i.bak -E 's/"([^"]+)" \| None/Optional["\1"]/g; s/'"'"'([^'"'"']+)'"'"' \| None/Optional['"'"'\1'"'"']/g' {} \; find . -name "*.py.bak" -type f -delete - - name: Install pnpm - uses: pnpm/action-setup@v4 - with: - package_json_file: web/package.json - run_install: false - - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: 24 - cache: pnpm - cache-dependency-path: ./web/pnpm-lock.yaml - - - name: Install web dependencies - run: | - cd web - pnpm install --frozen-lockfile - - - name: ESLint autofix - run: | - cd web - pnpm lint:fix || true - # mdformat breaks YAML front matter in markdown files. Add --exclude for directories containing YAML front matter. - name: mdformat run: | diff --git a/web/app/components/base/effect/index.stories.tsx b/web/app/components/base/effect/index.stories.tsx index 36a0e668cf..8452e9aefe 100644 --- a/web/app/components/base/effect/index.stories.tsx +++ b/web/app/components/base/effect/index.stories.tsx @@ -1,4 +1,3 @@ -/* eslint-disable tailwindcss/classnames-order */ import type { Meta, StoryObj } from '@storybook/nextjs-vite' import Effect from '.' @@ -29,8 +28,8 @@ type Story = StoryObj export const Playground: Story = { render: () => (
- - + +
Accent glow
diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/base/error-message.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/base/error-message.tsx index f0a1fb64a9..9bc97d9970 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/base/error-message.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/base/error-message.tsx @@ -14,7 +14,6 @@ const ErrorMessage = ({ errorMsg, }: ErrorMessageProps) => { return ( - // eslint-disable-next-line tailwindcss/migration-from-tailwind-2
=16.0.0} - '@eslint-react/ast@2.8.1': - resolution: {integrity: sha512-4D442lxeFvvd9PMvBbA621rfz/Ne8Kod8RW0/FLKO0vx+IOxm74pP6be1uU56rqL9TvoIHxjclBjfgXplEF+Yw==} + '@eslint-react/ast@2.9.4': + resolution: {integrity: sha512-WI9iq5ePTlcWo0xhSs4wxLUC6u4QuBmQkKeSiXexkEO8C2p8QE7ECNIXhRVkYs3p3AKH5xTez9V8C/CBIGxeXA==} engines: {node: '>=20.19.0'} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@eslint-react/core@2.8.1': - resolution: {integrity: sha512-zF73p8blyuX+zrfgyTtpKesichYzK+G54TEjFWtzagWIbnqQjtVscebL/eGep72oWzAOd5B04ACBvJ2hW4fp5g==} + '@eslint-react/core@2.9.4': + resolution: {integrity: sha512-Ob+Dip1vyR9ch9XL7LUAsGXc0UUf9Kuzn9BEiwOLT7l+cF91ieKeCvIzNPp0LmTuanPfQweJ9iDT9i295SqBZA==} engines: {node: '>=20.19.0'} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@eslint-react/eff@2.8.1': - resolution: {integrity: sha512-ZASOs8oTZJSiu1giue7V87GEKQvlKLfGfLppal6Rl+aKnfIEz+vartmjpH12pkFQZ9ESRyHzYbU533S6pEDoNg==} + '@eslint-react/eff@2.9.4': + resolution: {integrity: sha512-7AOmozmfa0HgXY9O+J+iX3ciZfViz+W+jhRe2y0YqqkDR7PwV2huzhk/Bxq6sRzzf2uFHqoh/AQNZUhRJ3A05A==} engines: {node: '>=20.19.0'} - '@eslint-react/eslint-plugin@2.8.1': - resolution: {integrity: sha512-ob+SSDnTPnA5dhiWWJLfyHRLEzWnjilCsohgo5s9PPKF5b5bjxG+c/rwqhQwT3M9Ey83mGNdkrLzt00SOfr4pw==} + '@eslint-react/eslint-plugin@2.9.4': + resolution: {integrity: sha512-B1LOEUBuT4L7EmY3E9F7+K8Jdr9nAzx66USz4uWEtg8ZMn82E2O5TzOBPw6eeL0O9BoyLBoslZotXNQVazR2dA==} engines: {node: '>=20.19.0'} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@eslint-react/shared@2.8.1': - resolution: {integrity: sha512-NDmJBiMiPDXR6qeZzYOtiILHxWjYwBHxquQ/bMQkWcWK+1qF5LeD8UTRcWtBpZoMPi3sNBWwR3k2Sc5HWZpJ7g==} + '@eslint-react/shared@2.9.4': + resolution: {integrity: sha512-PU7C4JzDZ6OffAWD+HwJdvzGSho25UPYJRyb4wZ/pDaI8QPTDj8AtKWKK69SEOQl2ic89ht1upjQX+jrXhN15w==} engines: {node: '>=20.19.0'} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@eslint-react/var@2.8.1': - resolution: {integrity: sha512-iHIdEBz6kgW4dEFdhEjpy9SEQ6+d4RYg+WBzHg5J5ktT2xSQFi77Dq6Wtemik6QvvAPnYLRseQxgW+m+1rQlfA==} + '@eslint-react/var@2.9.4': + resolution: {integrity: sha512-Qiih6hT+D2vZmCbAGUooReKlqXjtb/g3SzYj2zNlci6YcWxsQB/pqhR0ayU2AOdW6U9YdeCCfPIwBBQ4AEpyBA==} engines: {node: '>=20.19.0'} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -1208,6 +1208,10 @@ packages: resolution: {integrity: sha512-r18fEAj9uCk+VjzGt2thsbOmychS+4kxI14spVNibUO2vqKX7obOG+ymZljAwuPZl+S3clPGwCwTDtrdqTiY6Q==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} + '@eslint/css-tree@3.6.8': + resolution: {integrity: sha512-s0f40zY7dlMp8i0Jf0u6l/aSswS0WRAgkhgETgiCJRcxIWb4S/Sp9uScKHWbkM3BnoFLbJbmOYk5AZUDFVxaLA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + '@eslint/eslintrc@3.3.3': resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2867,10 +2871,14 @@ packages: peerDependencies: solid-js: 1.9.11 - '@tanstack/eslint-plugin-query@5.91.3': - resolution: {integrity: sha512-5GMGZMYFK9dOvjpdedjJs4hU40EdPuO2AjzObQzP7eOSsikunCfrXaU3oNGXSsvoU9ve1Z1xQZZuDyPi0C1M7Q==} + '@tanstack/eslint-plugin-query@5.91.4': + resolution: {integrity: sha512-8a+GAeR7oxJ5laNyYBQ6miPK09Hi18o5Oie/jx8zioXODv/AUFLZQecKabPdpQSLmuDXEBPKFh+W5DKbWlahjQ==} peerDependencies: eslint: ^8.57.0 || ^9.0.0 + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true '@tanstack/form-core@1.24.3': resolution: {integrity: sha512-e+HzSD49NWr4aIqJWtPPzmi+/phBJAP3nSPN8dvxwmJWqAxuB/cH138EcmCFf3+oA7j3BXvwvTY0I+8UweGPjQ==} @@ -3378,6 +3386,11 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@valibot/to-json-schema@1.5.0': + resolution: {integrity: sha512-GE7DmSr1C2UCWPiV0upRH6mv0cCPsqYGs819fb6srCS1tWhyXrkGGe+zxUiwzn/L1BOfADH4sNjY/YHCuP8phQ==} + peerDependencies: + valibot: ^1.2.0 + '@vitejs/plugin-react@5.1.2': resolution: {integrity: sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -4443,6 +4456,19 @@ packages: peerDependencies: eslint: '*' + eslint-plugin-better-tailwindcss@4.1.1: + resolution: {integrity: sha512-ctw461TGJi8iM0P01mNVjSW7jeUAdyUgmrrd59np5/VxqX50nayMbwKZkfmjWpP1PWOqlh4CSMOH/WW6ICWmJw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 + oxlint: ^1.35.0 + tailwindcss: ^3.3.0 || ^4.1.17 + peerDependenciesMeta: + eslint: + optional: true + oxlint: + optional: true + eslint-plugin-command@3.4.0: resolution: {integrity: sha512-EW4eg/a7TKEhG0s5IEti72kh3YOTlnhfFNuctq5WnB1fst37/IHTd5OkD+vnlRf3opTvUcSRihAateP6bT5ZcA==} peerDependencies: @@ -4493,15 +4519,15 @@ packages: peerDependencies: eslint: ^9.0.0 - eslint-plugin-react-dom@2.8.1: - resolution: {integrity: sha512-VAVs3cp/0XTxdjTeLePtZVadj+om+N1VNVy7hyzSPACfh5ncAicC0zOIc5MB15KUWCj8PoG/ZnVny0YqeubgRg==} + eslint-plugin-react-dom@2.9.4: + resolution: {integrity: sha512-lRa3iN082cX3HRKdbKSESmlj+z4zMR10DughwagV7h+IOd3O07UGnYQhenH08GMSyLy1f2D6QJmKBLGbx2p20g==} engines: {node: '>=20.19.0'} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - eslint-plugin-react-hooks-extra@2.8.1: - resolution: {integrity: sha512-YeZLGzcib6UxlY7Gf+3zz8Mfl7u+OoVj3MukGaTuU6zkm1XQMI8/k4o16bKHuWtUauhn7Udl1bLAWfLgQM5UFw==} + eslint-plugin-react-hooks-extra@2.9.4: + resolution: {integrity: sha512-8hQArFHpXubT+i++8TwIL24vQ5b/ZcnVT3EFOSvy1TdBZw8NqrcFNBVqywQ6YUWX0utuPiTQgeJB0qnBF7gx4g==} engines: {node: '>=20.19.0'} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -4513,27 +4539,34 @@ packages: peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 - eslint-plugin-react-naming-convention@2.8.1: - resolution: {integrity: sha512-fVj+hSzIe2I6HyPTf1nccMBXq72c4jbM3gk0T+szo/wewEF8/LgenjfquJoxHPpheb1fujFgdlo5HBhsilAX7Q==} + eslint-plugin-react-naming-convention@2.9.4: + resolution: {integrity: sha512-Ow9ikJ49tDjeTaO2wfUYlSlVBsbG8AZVqoVFu4HH69FZe6I5LEdjZf/gdXnN2W+/JAy7Ru5vYQ8H8LU3tTZERg==} engines: {node: '>=20.19.0'} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - eslint-plugin-react-refresh@0.4.26: - resolution: {integrity: sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==} + eslint-plugin-react-refresh@0.5.0: + resolution: {integrity: sha512-ZYvmh7VfVgqR/7wR71I3Zl6hK/C5CcxdWYKZSpHawS5JCNgE4efhQWg/+/WPpgGAp9Ngp/rRZYyaIwmPQBq/lA==} peerDependencies: - eslint: '>=8.40' + eslint: '>=9' - eslint-plugin-react-web-api@2.8.1: - resolution: {integrity: sha512-NYsZKW1aJZ2XZuYTPzbwYLShvGcuXKRV/5TW61VO56gik/btil4Snt5UtyxshHbvT/zXx/Z+QsHul51/XM4/Qw==} + eslint-plugin-react-rsc@2.9.4: + resolution: {integrity: sha512-RwBYSLkcGXQV6SQYABdHLrafUmpfdPBYsAa/kvg6smqEn+/vPKSk0I+uAuzkmiw4y4KXW94Q9rlIdJlzOMdJfQ==} engines: {node: '>=20.19.0'} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - eslint-plugin-react-x@2.8.1: - resolution: {integrity: sha512-4IpCMrsb63AVEa9diOApIm+T3wUGIzK+EB5vyYocO31YYPJ16+R7Fh4lV3S3fOuX1+aQ+Ad4SE0cYuZ2pF2Tlg==} + eslint-plugin-react-web-api@2.9.4: + resolution: {integrity: sha512-/k++qhGoYtMNZrsQT+M08fCGi/VurL1fE/LNiz2fMwOIU7KjXD9N0kGWPFdIAISnYXGzOg53O5WW/mnNR78emQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + eslint-plugin-react-x@2.9.4: + resolution: {integrity: sha512-a078MHeM/FdjRu3KJsFX+PCHewZyC77EjAO7QstL/vvwjsFae3PCWMZ8Q4b+mzUsT4FkFxi5mEW43ZHksPWDFw==} engines: {node: '>=20.19.0'} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -4550,17 +4583,11 @@ packages: peerDependencies: eslint: ^8.0.0 || ^9.0.0 - eslint-plugin-storybook@10.2.1: - resolution: {integrity: sha512-5+V+dlzTuZfNKUD8hPbLvCVtggcWfI2lDGTpiq0AENrHeAgcztj17wwDva96lbg/sAG20uX71l8HQo3s/GmpHw==} + eslint-plugin-storybook@10.2.6: + resolution: {integrity: sha512-Ykf0hDS97oJlQel21WG+SYtGnzFkkSfifupJ92NQtMMSMLXsWm4P0x8ZQqu9/EQa+dUkGoj9EWyNmmbB/54uhA==} peerDependencies: eslint: '>=8' - storybook: ^10.2.1 - - eslint-plugin-tailwindcss@3.18.2: - resolution: {integrity: sha512-QbkMLDC/OkkjFQ1iz/5jkMdHfiMu/uwujUHLAJK5iwNHD8RTxVTlsUezE0toTZ6VhybNBsk+gYGPDq2agfeRNA==} - engines: {node: '>=18.12.0'} - peerDependencies: - tailwindcss: ^3.4.0 + storybook: ^10.2.6 eslint-plugin-toml@1.0.3: resolution: {integrity: sha512-GlCBX+R313RvFY2Tj0ZmvzCEv8FDp1z2itvTFTV4bW/Bkbl3xEp9inWNsRWH3SiDUlxo8Pew31ILEp/3J0WxaA==} @@ -5585,6 +5612,9 @@ packages: mdn-data@2.12.2: resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + mdn-data@2.23.0: + resolution: {integrity: sha512-786vq1+4079JSeu2XdcDjrhi/Ry7BWtjDl9WtGPWLiIHb2T66GvIVflZTBoSNZ5JqTtJGYEVMuFA/lbQlMOyDQ==} + memoize-one@5.2.1: resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} @@ -6850,11 +6880,15 @@ packages: resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} engines: {node: '>=20'} - tailwind-merge@2.6.0: - resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==} + tailwind-csstree@0.1.4: + resolution: {integrity: sha512-FzD187HuFIZEyeR7Xy6sJbJll2d4SybS90satC8SKIuaNRC05CxMvdzN7BUsfDQffcnabckRM5OIcfArjsZ0mg==} + engines: {node: '>=18.18'} - tailwindcss@3.4.18: - resolution: {integrity: sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==} + tailwind-merge@2.6.1: + resolution: {integrity: sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ==} + + tailwindcss@3.4.19: + resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==} engines: {node: '>=14.0.0'} hasBin: true @@ -7003,6 +7037,10 @@ packages: typescript: optional: true + tsconfig-paths-webpack-plugin@4.2.0: + resolution: {integrity: sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==} + engines: {node: '>=10.13.0'} + tsconfig-paths@4.2.0: resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} engines: {node: '>=6'} @@ -7171,6 +7209,14 @@ packages: resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} hasBin: true + valibot@1.2.0: + resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + vfile-location@5.0.3: resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} @@ -7694,7 +7740,7 @@ snapshots: idb: 8.0.3 tslib: 2.8.1 - '@antfu/eslint-config@7.2.0(@eslint-react/eslint-plugin@2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(@next/eslint-plugin-next@16.1.6)(@vue/compiler-sfc@3.5.27)(eslint-plugin-react-hooks@7.0.1(eslint@9.39.2(jiti@1.21.7)))(eslint-plugin-react-refresh@0.4.26(eslint@9.39.2(jiti@1.21.7)))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)(vitest@4.0.17)': + '@antfu/eslint-config@7.2.0(@eslint-react/eslint-plugin@2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(@next/eslint-plugin-next@16.1.6)(@vue/compiler-sfc@3.5.27)(eslint-plugin-react-hooks@7.0.1(eslint@9.39.2(jiti@1.21.7)))(eslint-plugin-react-refresh@0.5.0(eslint@9.39.2(jiti@1.21.7)))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)(vitest@4.0.17)': dependencies: '@antfu/install-pkg': 1.1.0 '@clack/prompts': 0.11.0 @@ -7734,10 +7780,10 @@ snapshots: vue-eslint-parser: 10.2.0(eslint@9.39.2(jiti@1.21.7)) yaml-eslint-parser: 2.0.0 optionalDependencies: - '@eslint-react/eslint-plugin': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@eslint-react/eslint-plugin': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) '@next/eslint-plugin-next': 16.1.6 eslint-plugin-react-hooks: 7.0.1(eslint@9.39.2(jiti@1.21.7)) - eslint-plugin-react-refresh: 0.4.26(eslint@9.39.2(jiti@1.21.7)) + eslint-plugin-react-refresh: 0.5.0(eslint@9.39.2(jiti@1.21.7)) transitivePeerDependencies: - '@eslint/json' - '@vue/compiler-sfc' @@ -8137,9 +8183,9 @@ snapshots: '@eslint-community/regexpp@4.12.2': {} - '@eslint-react/ast@2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': + '@eslint-react/ast@2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': dependencies: - '@eslint-react/eff': 2.8.1 + '@eslint-react/eff': 2.9.4 '@typescript-eslint/types': 8.54.0 '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) @@ -8149,12 +8195,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint-react/core@2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': + '@eslint-react/core@2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': dependencies: - '@eslint-react/ast': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) - '@eslint-react/eff': 2.8.1 - '@eslint-react/shared': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) - '@eslint-react/var': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@eslint-react/ast': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@eslint-react/eff': 2.9.4 + '@eslint-react/shared': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@eslint-react/var': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.54.0 '@typescript-eslint/types': 8.54.0 '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) @@ -8164,30 +8210,31 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint-react/eff@2.8.1': {} + '@eslint-react/eff@2.9.4': {} - '@eslint-react/eslint-plugin@2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': + '@eslint-react/eslint-plugin@2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': dependencies: - '@eslint-react/eff': 2.8.1 - '@eslint-react/shared': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@eslint-react/eff': 2.9.4 + '@eslint-react/shared': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.54.0 '@typescript-eslint/type-utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/types': 8.54.0 '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) eslint: 9.39.2(jiti@1.21.7) - eslint-plugin-react-dom: 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) - eslint-plugin-react-hooks-extra: 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) - eslint-plugin-react-naming-convention: 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) - eslint-plugin-react-web-api: 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) - eslint-plugin-react-x: 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + eslint-plugin-react-dom: 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + eslint-plugin-react-hooks-extra: 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + eslint-plugin-react-naming-convention: 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + eslint-plugin-react-rsc: 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + eslint-plugin-react-web-api: 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + eslint-plugin-react-x: 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@eslint-react/shared@2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': + '@eslint-react/shared@2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': dependencies: - '@eslint-react/eff': 2.8.1 + '@eslint-react/eff': 2.9.4 '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) eslint: 9.39.2(jiti@1.21.7) ts-pattern: 5.9.0 @@ -8196,11 +8243,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint-react/var@2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': + '@eslint-react/var@2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': dependencies: - '@eslint-react/ast': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) - '@eslint-react/eff': 2.8.1 - '@eslint-react/shared': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@eslint-react/ast': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@eslint-react/eff': 2.9.4 + '@eslint-react/shared': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.54.0 '@typescript-eslint/types': 8.54.0 '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) @@ -8258,6 +8305,11 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 + '@eslint/css-tree@3.6.8': + dependencies: + mdn-data: 2.23.0 + source-map-js: 1.2.1 + '@eslint/eslintrc@3.3.3': dependencies: ajv: 6.12.6 @@ -9902,10 +9954,10 @@ snapshots: dependencies: '@swc/counter': 0.1.3 - '@tailwindcss/typography@0.5.19(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.2))': + '@tailwindcss/typography@0.5.19(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2))': dependencies: postcss-selector-parser: 6.0.10 - tailwindcss: 3.4.18(tsx@4.21.0)(yaml@2.8.2) + tailwindcss: 3.4.19(tsx@4.21.0)(yaml@2.8.2) '@tanstack/devtools-client@0.0.5': dependencies: @@ -9957,13 +10009,14 @@ snapshots: - csstype - utf-8-validate - '@tanstack/eslint-plugin-query@5.91.3(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': + '@tanstack/eslint-plugin-query@5.91.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': dependencies: - '@typescript-eslint/utils': 8.53.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) eslint: 9.39.2(jiti@1.21.7) + optionalDependencies: + typescript: 5.9.3 transitivePeerDependencies: - supports-color - - typescript '@tanstack/form-core@1.24.3': dependencies: @@ -10593,6 +10646,10 @@ snapshots: '@ungap/structured-clone@1.3.0': {} + '@valibot/to-json-schema@1.5.0(valibot@1.2.0(typescript@5.9.3))': + dependencies: + valibot: 1.2.0(typescript@5.9.3) + '@vitejs/plugin-react@5.1.2(vite@7.3.1(@types/node@18.15.0)(jiti@1.21.7)(sass@1.93.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@babel/core': 7.28.6 @@ -11747,6 +11804,22 @@ snapshots: dependencies: eslint: 9.39.2(jiti@1.21.7) + eslint-plugin-better-tailwindcss@4.1.1(eslint@9.39.2(jiti@1.21.7))(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2))(typescript@5.9.3): + dependencies: + '@eslint/css-tree': 3.6.8 + '@valibot/to-json-schema': 1.5.0(valibot@1.2.0(typescript@5.9.3)) + enhanced-resolve: 5.18.4 + jiti: 2.6.1 + synckit: 0.11.12 + tailwind-csstree: 0.1.4 + tailwindcss: 3.4.19(tsx@4.21.0)(yaml@2.8.2) + tsconfig-paths-webpack-plugin: 4.2.0 + valibot: 1.2.0(typescript@5.9.3) + optionalDependencies: + eslint: 9.39.2(jiti@1.21.7) + transitivePeerDependencies: + - typescript + eslint-plugin-command@3.4.0(eslint@9.39.2(jiti@1.21.7)): dependencies: '@es-joy/jsdoccomment': 0.78.0 @@ -11835,37 +11908,35 @@ snapshots: yaml: 2.8.2 yaml-eslint-parser: 2.0.0 - eslint-plugin-react-dom@2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3): + eslint-plugin-react-dom@2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3): dependencies: - '@eslint-react/ast': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) - '@eslint-react/core': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) - '@eslint-react/eff': 2.8.1 - '@eslint-react/shared': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) - '@eslint-react/var': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@eslint-react/ast': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@eslint-react/core': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@eslint-react/eff': 2.9.4 + '@eslint-react/shared': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@eslint-react/var': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.54.0 '@typescript-eslint/types': 8.54.0 '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) compare-versions: 6.1.1 eslint: 9.39.2(jiti@1.21.7) - string-ts: 2.3.1 ts-pattern: 5.9.0 typescript: 5.9.3 transitivePeerDependencies: - supports-color - eslint-plugin-react-hooks-extra@2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3): + eslint-plugin-react-hooks-extra@2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3): dependencies: - '@eslint-react/ast': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) - '@eslint-react/core': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) - '@eslint-react/eff': 2.8.1 - '@eslint-react/shared': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) - '@eslint-react/var': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@eslint-react/ast': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@eslint-react/core': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@eslint-react/eff': 2.9.4 + '@eslint-react/shared': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@eslint-react/var': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.54.0 '@typescript-eslint/type-utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/types': 8.54.0 '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) eslint: 9.39.2(jiti@1.21.7) - string-ts: 2.3.1 ts-pattern: 5.9.0 typescript: 5.9.3 transitivePeerDependencies: @@ -11882,13 +11953,13 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-react-naming-convention@2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3): + eslint-plugin-react-naming-convention@2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3): dependencies: - '@eslint-react/ast': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) - '@eslint-react/core': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) - '@eslint-react/eff': 2.8.1 - '@eslint-react/shared': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) - '@eslint-react/var': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@eslint-react/ast': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@eslint-react/core': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@eslint-react/eff': 2.9.4 + '@eslint-react/shared': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@eslint-react/var': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.54.0 '@typescript-eslint/type-utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/types': 8.54.0 @@ -11901,35 +11972,47 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-react-refresh@0.4.26(eslint@9.39.2(jiti@1.21.7)): + eslint-plugin-react-refresh@0.5.0(eslint@9.39.2(jiti@1.21.7)): dependencies: eslint: 9.39.2(jiti@1.21.7) - eslint-plugin-react-web-api@2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3): + eslint-plugin-react-rsc@2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3): dependencies: - '@eslint-react/ast': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) - '@eslint-react/core': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) - '@eslint-react/eff': 2.8.1 - '@eslint-react/shared': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) - '@eslint-react/var': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.54.0 + '@eslint-react/ast': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@eslint-react/shared': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@eslint-react/var': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/types': 8.54.0 '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) - birecord: 0.1.1 eslint: 9.39.2(jiti@1.21.7) - string-ts: 2.3.1 ts-pattern: 5.9.0 typescript: 5.9.3 transitivePeerDependencies: - supports-color - eslint-plugin-react-x@2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3): + eslint-plugin-react-web-api@2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3): dependencies: - '@eslint-react/ast': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) - '@eslint-react/core': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) - '@eslint-react/eff': 2.8.1 - '@eslint-react/shared': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) - '@eslint-react/var': 2.8.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@eslint-react/ast': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@eslint-react/core': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@eslint-react/eff': 2.9.4 + '@eslint-react/shared': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@eslint-react/var': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.54.0 + '@typescript-eslint/types': 8.54.0 + '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + birecord: 0.1.1 + eslint: 9.39.2(jiti@1.21.7) + ts-pattern: 5.9.0 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + eslint-plugin-react-x@2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3): + dependencies: + '@eslint-react/ast': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@eslint-react/core': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@eslint-react/eff': 2.9.4 + '@eslint-react/shared': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@eslint-react/var': 2.9.4(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.54.0 '@typescript-eslint/type-utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/types': 8.54.0 @@ -11937,7 +12020,6 @@ snapshots: compare-versions: 6.1.1 eslint: 9.39.2(jiti@1.21.7) is-immutable-type: 5.0.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) - string-ts: 2.3.1 ts-api-utils: 2.4.0(typescript@5.9.3) ts-pattern: 5.9.0 typescript: 5.9.3 @@ -11969,21 +12051,15 @@ snapshots: semver: 7.7.3 typescript: 5.9.3 - eslint-plugin-storybook@10.2.1(eslint@9.39.2(jiti@1.21.7))(storybook@10.2.0(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3): + eslint-plugin-storybook@10.2.6(eslint@9.39.2(jiti@1.21.7))(storybook@10.2.0(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3): dependencies: - '@typescript-eslint/utils': 8.53.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) eslint: 9.39.2(jiti@1.21.7) storybook: 10.2.0(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) transitivePeerDependencies: - supports-color - typescript - eslint-plugin-tailwindcss@3.18.2(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.2)): - dependencies: - fast-glob: 3.3.3 - postcss: 8.5.6 - tailwindcss: 3.4.18(tsx@4.21.0)(yaml@2.8.2) - eslint-plugin-toml@1.0.3(eslint@9.39.2(jiti@1.21.7)): dependencies: '@eslint/core': 1.0.1 @@ -13250,6 +13326,8 @@ snapshots: mdn-data@2.12.2: {} + mdn-data@2.23.0: {} + memoize-one@5.2.1: {} merge-stream@2.0.0: {} @@ -14863,9 +14941,11 @@ snapshots: tagged-tag@1.0.0: {} - tailwind-merge@2.6.0: {} + tailwind-csstree@0.1.4: {} - tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.2): + tailwind-merge@2.6.1: {} + + tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -15020,6 +15100,13 @@ snapshots: optionalDependencies: typescript: 5.9.3 + tsconfig-paths-webpack-plugin@4.2.0: + dependencies: + chalk: 4.1.2 + enhanced-resolve: 5.18.4 + tapable: 2.3.0 + tsconfig-paths: 4.2.0 + tsconfig-paths@4.2.0: dependencies: json5: 2.2.3 @@ -15185,6 +15272,10 @@ snapshots: uuid@11.1.0: {} + valibot@1.2.0(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 + vfile-location@5.0.3: dependencies: '@types/unist': 3.0.3 diff --git a/web/utils/classnames.spec.ts b/web/utils/classnames.spec.ts deleted file mode 100644 index 1b8f487856..0000000000 --- a/web/utils/classnames.spec.ts +++ /dev/null @@ -1,157 +0,0 @@ -/** - * Test suite for the classnames utility function - * This utility combines the classnames library with tailwind-merge - * to handle conditional CSS classes and merge conflicting Tailwind classes - */ -import { cn } from './classnames' - -describe('classnames', () => { - /** - * Tests basic classnames library features: - * - String concatenation - * - Array handling - * - Falsy value filtering - * - Object-based conditional classes - */ - it('classnames libs feature', () => { - expect(cn('foo')).toBe('foo') - expect(cn('foo', 'bar')).toBe('foo bar') - expect(cn(['foo', 'bar'])).toBe('foo bar') - - expect(cn(undefined)).toBe('') - expect(cn(null)).toBe('') - expect(cn(false)).toBe('') - - expect(cn({ - foo: true, - bar: false, - baz: true, - })).toBe('foo baz') - }) - - /** - * Tests tailwind-merge functionality: - * - Conflicting class resolution (last one wins) - * - Modifier handling (hover, focus, etc.) - * - Important prefix (!) - * - Custom color classes - * - Arbitrary values - */ - it('tailwind-merge', () => { - /* eslint-disable tailwindcss/classnames-order */ - expect(cn('p-0')).toBe('p-0') - expect(cn('text-right text-center text-left')).toBe('text-left') - expect(cn('pl-4 p-8')).toBe('p-8') - expect(cn('m-[2px] m-[4px]')).toBe('m-[4px]') - expect(cn('m-1 m-[4px]')).toBe('m-[4px]') - expect(cn('overflow-x-auto hover:overflow-x-hidden overflow-x-scroll')).toBe( - 'hover:overflow-x-hidden overflow-x-scroll', - ) - expect(cn('h-10 h-min')).toBe('h-min') - expect(cn('bg-grey-5 bg-hotpink')).toBe('bg-hotpink') - - expect(cn('hover:block hover:inline')).toBe('hover:inline') - - expect(cn('font-medium !font-bold')).toBe('font-medium !font-bold') - expect(cn('!font-medium !font-bold')).toBe('!font-bold') - - expect(cn('text-gray-100 text-primary-200')).toBe('text-primary-200') - expect(cn('text-some-unknown-color text-components-input-bg-disabled text-primary-200')).toBe('text-primary-200') - expect(cn('bg-some-unknown-color bg-components-input-bg-disabled bg-primary-200')).toBe('bg-primary-200') - - expect(cn('border-t border-white/10')).toBe('border-t border-white/10') - expect(cn('border-t border-white')).toBe('border-t border-white') - expect(cn('text-3.5xl text-black')).toBe('text-3.5xl text-black') - }) - - /** - * Tests the integration of classnames and tailwind-merge: - * - Object-based conditional classes with Tailwind conflict resolution - */ - it('classnames combined with tailwind-merge', () => { - expect(cn('text-right', { - 'text-center': true, - })).toBe('text-center') - - expect(cn('text-right', { - 'text-center': false, - })).toBe('text-right') - }) - - /** - * Tests handling of multiple mixed argument types: - * - Strings, arrays, and objects in a single call - * - Tailwind merge working across different argument types - */ - it('multiple mixed argument types', () => { - expect(cn('foo', ['bar', 'baz'], { qux: true, quux: false })).toBe('foo bar baz qux') - expect(cn('p-4', ['p-2', 'm-4'], { 'text-left': true, 'text-right': true })).toBe('p-2 m-4 text-right') - }) - - /** - * Tests nested array handling: - * - Deep array flattening - * - Tailwind merge with nested structures - */ - it('nested arrays', () => { - expect(cn(['foo', ['bar', 'baz']])).toBe('foo bar baz') - expect(cn(['p-4', ['p-2', 'text-center']])).toBe('p-2 text-center') - }) - - /** - * Tests empty input handling: - * - Empty strings, arrays, and objects - * - Mixed empty and non-empty values - */ - it('empty inputs', () => { - expect(cn('')).toBe('') - expect(cn([])).toBe('') - expect(cn({})).toBe('') - expect(cn('', [], {})).toBe('') - expect(cn('foo', '', 'bar')).toBe('foo bar') - }) - - /** - * Tests number input handling: - * - Truthy numbers converted to strings - * - Zero treated as falsy - */ - it('numbers as inputs', () => { - expect(cn(1)).toBe('1') - expect(cn(0)).toBe('') - expect(cn('foo', 1, 'bar')).toBe('foo 1 bar') - }) - - /** - * Tests multiple object arguments: - * - Object merging - * - Tailwind conflict resolution across objects - */ - it('multiple objects', () => { - expect(cn({ foo: true }, { bar: true })).toBe('foo bar') - expect(cn({ foo: true, bar: false }, { bar: true, baz: true })).toBe('foo bar baz') - expect(cn({ 'p-4': true }, { 'p-2': true })).toBe('p-2') - }) - - /** - * Tests complex edge cases: - * - Mixed falsy values - * - Nested arrays with falsy values - * - Multiple conflicting Tailwind classes - */ - it('complex edge cases', () => { - expect(cn('foo', null, undefined, false, 'bar', 0, 1, '')).toBe('foo bar 1') - expect(cn(['foo', null, ['bar', undefined, 'baz']])).toBe('foo bar baz') - expect(cn('text-sm', { 'text-lg': false, 'text-xl': true }, 'text-2xl')).toBe('text-2xl') - }) - - /** - * Tests important (!) modifier behavior: - * - Important modifiers in objects - * - Conflict resolution with important prefix - */ - it('important modifier with objects', () => { - expect(cn({ '!font-medium': true }, { '!font-bold': true })).toBe('!font-bold') - expect(cn('font-normal', { '!font-bold': true })).toBe('font-normal !font-bold') - }) -}) From e04f2a0786df994168618abe1832a8ae921f6227 Mon Sep 17 00:00:00 2001 From: Stream Date: Thu, 5 Feb 2026 18:58:17 +0800 Subject: [PATCH 04/10] feat: use static manifest for pre-caching all plugin manifests before checking updates (#31942) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Junyan Qin Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- api/core/helper/marketplace.py | 54 +++++++----- api/core/plugin/entities/marketplace.py | 14 +++- api/schedule/check_upgradable_plugin_task.py | 24 ++++++ ...ss_tenant_plugin_autoupgrade_check_task.py | 83 +++++-------------- 4 files changed, 88 insertions(+), 87 deletions(-) diff --git a/api/core/helper/marketplace.py b/api/core/helper/marketplace.py index 25dc4ba9ed..d7b6e82062 100644 --- a/api/core/helper/marketplace.py +++ b/api/core/helper/marketplace.py @@ -6,7 +6,8 @@ from yarl import URL from configs import dify_config from core.helper.download import download_with_size_limit -from core.plugin.entities.marketplace import MarketplacePluginDeclaration +from core.plugin.entities.marketplace import MarketplacePluginDeclaration, MarketplacePluginSnapshot +from extensions.ext_redis import redis_client marketplace_api_url = URL(str(dify_config.MARKETPLACE_API_URL)) logger = logging.getLogger(__name__) @@ -43,28 +44,37 @@ def batch_fetch_plugin_by_ids(plugin_ids: list[str]) -> list[dict]: return data.get("data", {}).get("plugins", []) -def batch_fetch_plugin_manifests_ignore_deserialization_error( - plugin_ids: list[str], -) -> Sequence[MarketplacePluginDeclaration]: - if len(plugin_ids) == 0: - return [] - - url = str(marketplace_api_url / "api/v1/plugins/batch") - response = httpx.post(url, json={"plugin_ids": plugin_ids}, headers={"X-Dify-Version": dify_config.project.version}) - response.raise_for_status() - result: list[MarketplacePluginDeclaration] = [] - for plugin in response.json()["data"]["plugins"]: - try: - result.append(MarketplacePluginDeclaration.model_validate(plugin)) - except Exception: - logger.exception( - "Failed to deserialize marketplace plugin manifest for %s", plugin.get("plugin_id", "unknown") - ) - - return result - - def record_install_plugin_event(plugin_unique_identifier: str): url = str(marketplace_api_url / "api/v1/stats/plugins/install_count") response = httpx.post(url, json={"unique_identifier": plugin_unique_identifier}) response.raise_for_status() + + +def fetch_global_plugin_manifest(cache_key_prefix: str, cache_ttl: int) -> None: + """ + Fetch all plugin manifests from marketplace and cache them in Redis. + This should be called once per check cycle to populate the instance-level cache. + + Args: + cache_key_prefix: Redis key prefix for caching plugin manifests + cache_ttl: Cache TTL in seconds + + Raises: + httpx.HTTPError: If the HTTP request fails + Exception: If any other error occurs during fetching or caching + """ + url = str(marketplace_api_url / "api/v1/dist/plugins/manifest.json") + response = httpx.get(url, headers={"X-Dify-Version": dify_config.project.version}, timeout=30) + response.raise_for_status() + + raw_json = response.json() + plugins_data = raw_json.get("plugins", []) + + # Parse and cache all plugin snapshots + for plugin_data in plugins_data: + plugin_snapshot = MarketplacePluginSnapshot.model_validate(plugin_data) + redis_client.setex( + name=f"{cache_key_prefix}{plugin_snapshot.plugin_id}", + time=cache_ttl, + value=plugin_snapshot.model_dump_json(), + ) diff --git a/api/core/plugin/entities/marketplace.py b/api/core/plugin/entities/marketplace.py index e0762619e6..cf1f7ff0dd 100644 --- a/api/core/plugin/entities/marketplace.py +++ b/api/core/plugin/entities/marketplace.py @@ -1,4 +1,4 @@ -from pydantic import BaseModel, Field, model_validator +from pydantic import BaseModel, Field, computed_field, model_validator from core.model_runtime.entities.provider_entities import ProviderEntity from core.plugin.entities.endpoint import EndpointProviderDeclaration @@ -48,3 +48,15 @@ class MarketplacePluginDeclaration(BaseModel): if "tool" in data and not data["tool"]: del data["tool"] return data + + +class MarketplacePluginSnapshot(BaseModel): + org: str + name: str + latest_version: str + latest_package_identifier: str + latest_package_url: str + + @computed_field + def plugin_id(self) -> str: + return f"{self.org}/{self.name}" diff --git a/api/schedule/check_upgradable_plugin_task.py b/api/schedule/check_upgradable_plugin_task.py index e91ce07be3..13d2f24ca0 100644 --- a/api/schedule/check_upgradable_plugin_task.py +++ b/api/schedule/check_upgradable_plugin_task.py @@ -1,16 +1,24 @@ +import logging import math import time import click import app +from core.helper.marketplace import fetch_global_plugin_manifest from extensions.ext_database import db from models.account import TenantPluginAutoUpgradeStrategy from tasks import process_tenant_plugin_autoupgrade_check_task as check_task +logger = logging.getLogger(__name__) + AUTO_UPGRADE_MINIMAL_CHECKING_INTERVAL = 15 * 60 # 15 minutes MAX_CONCURRENT_CHECK_TASKS = 20 +# Import cache constants from the task module +CACHE_REDIS_KEY_PREFIX = check_task.CACHE_REDIS_KEY_PREFIX +CACHE_REDIS_TTL = check_task.CACHE_REDIS_TTL + @app.celery.task(queue="plugin") def check_upgradable_plugin_task(): @@ -40,6 +48,22 @@ def check_upgradable_plugin_task(): ) # make sure all strategies are checked in this interval batch_interval_time = (AUTO_UPGRADE_MINIMAL_CHECKING_INTERVAL / batch_chunk_count) if batch_chunk_count > 0 else 0 + if total_strategies == 0: + click.echo(click.style("no strategies to process, skipping plugin manifest fetch.", fg="green")) + return + + # Fetch and cache all plugin manifests before processing tenants + # This reduces load on marketplace from 300k requests to 1 request per check cycle + logger.info("fetching global plugin manifest from marketplace") + try: + fetch_global_plugin_manifest(CACHE_REDIS_KEY_PREFIX, CACHE_REDIS_TTL) + logger.info("successfully fetched and cached global plugin manifest") + except Exception as e: + logger.exception("failed to fetch global plugin manifest") + click.echo(click.style(f"failed to fetch global plugin manifest: {e}", fg="red")) + click.echo(click.style("skipping plugin upgrade check for this cycle", fg="yellow")) + return + for i in range(0, total_strategies, MAX_CONCURRENT_CHECK_TASKS): batch_strategies = strategies[i : i + MAX_CONCURRENT_CHECK_TASKS] for strategy in batch_strategies: diff --git a/api/tasks/process_tenant_plugin_autoupgrade_check_task.py b/api/tasks/process_tenant_plugin_autoupgrade_check_task.py index b5e6508006..6ad04aab0d 100644 --- a/api/tasks/process_tenant_plugin_autoupgrade_check_task.py +++ b/api/tasks/process_tenant_plugin_autoupgrade_check_task.py @@ -6,8 +6,8 @@ import typing import click from celery import shared_task -from core.helper import marketplace -from core.helper.marketplace import MarketplacePluginDeclaration +from core.helper.marketplace import record_install_plugin_event +from core.plugin.entities.marketplace import MarketplacePluginSnapshot from core.plugin.entities.plugin import PluginInstallationSource from core.plugin.impl.plugin import PluginInstaller from extensions.ext_redis import redis_client @@ -16,7 +16,7 @@ from models.account import TenantPluginAutoUpgradeStrategy logger = logging.getLogger(__name__) RETRY_TIMES_OF_ONE_PLUGIN_IN_ONE_TENANT = 3 -CACHE_REDIS_KEY_PREFIX = "plugin_autoupgrade_check_task:cached_plugin_manifests:" +CACHE_REDIS_KEY_PREFIX = "plugin_autoupgrade_check_task:cached_plugin_snapshot:" CACHE_REDIS_TTL = 60 * 60 # 1 hour @@ -25,11 +25,11 @@ def _get_redis_cache_key(plugin_id: str) -> str: return f"{CACHE_REDIS_KEY_PREFIX}{plugin_id}" -def _get_cached_manifest(plugin_id: str) -> typing.Union[MarketplacePluginDeclaration, None, bool]: +def _get_cached_manifest(plugin_id: str) -> typing.Union[MarketplacePluginSnapshot, None, bool]: """ Get cached plugin manifest from Redis. Returns: - - MarketplacePluginDeclaration: if found in cache + - MarketplacePluginSnapshot: if found in cache - None: if cached as not found (marketplace returned no result) - False: if not in cache at all """ @@ -43,76 +43,31 @@ def _get_cached_manifest(plugin_id: str) -> typing.Union[MarketplacePluginDeclar if cached_json is None: return None - return MarketplacePluginDeclaration.model_validate(cached_json) + return MarketplacePluginSnapshot.model_validate(cached_json) except Exception: logger.exception("Failed to get cached manifest for plugin %s", plugin_id) return False -def _set_cached_manifest(plugin_id: str, manifest: typing.Union[MarketplacePluginDeclaration, None]) -> None: - """ - Cache plugin manifest in Redis. - Args: - plugin_id: The plugin ID - manifest: The manifest to cache, or None if not found in marketplace - """ - try: - key = _get_redis_cache_key(plugin_id) - if manifest is None: - # Cache the fact that this plugin was not found - redis_client.setex(key, CACHE_REDIS_TTL, json.dumps(None)) - else: - # Cache the manifest data - redis_client.setex(key, CACHE_REDIS_TTL, manifest.model_dump_json()) - except Exception: - # If Redis fails, continue without caching - # traceback.print_exc() - logger.exception("Failed to set cached manifest for plugin %s", plugin_id) - - def marketplace_batch_fetch_plugin_manifests( plugin_ids_plain_list: list[str], -) -> list[MarketplacePluginDeclaration]: - """Fetch plugin manifests with Redis caching support.""" - cached_manifests: dict[str, typing.Union[MarketplacePluginDeclaration, None]] = {} - not_cached_plugin_ids: list[str] = [] +) -> list[MarketplacePluginSnapshot]: + """ + Fetch plugin manifests from Redis cache only. + This function assumes fetch_global_plugin_manifest() has been called + to pre-populate the cache with all marketplace plugins. + """ + result: list[MarketplacePluginSnapshot] = [] # Check Redis cache for each plugin for plugin_id in plugin_ids_plain_list: cached_result = _get_cached_manifest(plugin_id) - if cached_result is False: - # Not in cache, need to fetch - not_cached_plugin_ids.append(plugin_id) - else: - # Either found manifest or cached as None (not found in marketplace) - # At this point, cached_result is either MarketplacePluginDeclaration or None - if isinstance(cached_result, bool): - # This should never happen due to the if condition above, but for type safety - continue - cached_manifests[plugin_id] = cached_result + if not isinstance(cached_result, MarketplacePluginSnapshot): + # cached_result is False (not in cache) or None (cached as not found) + logger.warning("plugin %s not found in cache, skipping", plugin_id) + continue - # Fetch uncached plugins from marketplace - if not_cached_plugin_ids: - manifests = marketplace.batch_fetch_plugin_manifests_ignore_deserialization_error(not_cached_plugin_ids) - - # Cache the fetched manifests - for manifest in manifests: - cached_manifests[manifest.plugin_id] = manifest - _set_cached_manifest(manifest.plugin_id, manifest) - - # Cache plugins that were not found in marketplace - fetched_plugin_ids = {manifest.plugin_id for manifest in manifests} - for plugin_id in not_cached_plugin_ids: - if plugin_id not in fetched_plugin_ids: - cached_manifests[plugin_id] = None - _set_cached_manifest(plugin_id, None) - - # Build result list from cached manifests - result: list[MarketplacePluginDeclaration] = [] - for plugin_id in plugin_ids_plain_list: - cached_manifest: typing.Union[MarketplacePluginDeclaration, None] = cached_manifests.get(plugin_id) - if cached_manifest is not None: - result.append(cached_manifest) + result.append(cached_result) return result @@ -211,7 +166,7 @@ def process_tenant_plugin_autoupgrade_check_task( # execute upgrade new_unique_identifier = manifest.latest_package_identifier - marketplace.record_install_plugin_event(new_unique_identifier) + record_install_plugin_event(new_unique_identifier) click.echo( click.style( f"Upgrade plugin: {original_unique_identifier} -> {new_unique_identifier}", From cb970e54dae86f8d288a69313aeb064f25095423 Mon Sep 17 00:00:00 2001 From: QuantumGhost Date: Thu, 5 Feb 2026 19:05:09 +0800 Subject: [PATCH 05/10] perf(api): Optimize the response time of AppListApi endpoint (#31999) --- api/controllers/console/app/app.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/api/controllers/console/app/app.py b/api/controllers/console/app/app.py index 8c371da596..91034f2d87 100644 --- a/api/controllers/console/app/app.py +++ b/api/controllers/console/app/app.py @@ -1,3 +1,4 @@ +import logging import uuid from datetime import datetime from typing import Any, Literal, TypeAlias @@ -54,6 +55,8 @@ ALLOW_CREATE_APP_MODES = ["chat", "agent-chat", "advanced-chat", "workflow", "co register_enum_models(console_ns, IconType) +_logger = logging.getLogger(__name__) + class AppListQuery(BaseModel): page: int = Field(default=1, ge=1, le=99999, description="Page number (1-99999)") @@ -499,6 +502,7 @@ class AppListApi(Resource): select(Workflow).where( Workflow.version == Workflow.VERSION_DRAFT, Workflow.app_id.in_(workflow_capable_app_ids), + Workflow.tenant_id == current_tenant_id, ) ) .scalars() @@ -510,12 +514,14 @@ class AppListApi(Resource): NodeType.TRIGGER_PLUGIN, } for workflow in draft_workflows: + node_id = None try: - for _, node_data in workflow.walk_nodes(): + for node_id, node_data in workflow.walk_nodes(): if node_data.get("type") in trigger_node_types: draft_trigger_app_ids.add(str(workflow.app_id)) break except Exception: + _logger.exception("error while walking nodes, workflow_id=%s, node_id=%s", workflow.id, node_id) continue for app in app_pagination.items: From 095b3ee234ddbfdfb507267bb21b0cc4bb83255b Mon Sep 17 00:00:00 2001 From: 99 Date: Thu, 5 Feb 2026 21:44:31 +0800 Subject: [PATCH 06/10] chore: Remove redundant double space in variable type description (core/variables/variables.py) (#32002) --- api/core/variables/variables.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/core/variables/variables.py b/api/core/variables/variables.py index a19c53918d..338d81df78 100644 --- a/api/core/variables/variables.py +++ b/api/core/variables/variables.py @@ -112,7 +112,7 @@ class ArrayBooleanVariable(ArrayBooleanSegment, ArrayVariable): class RAGPipelineVariable(BaseModel): belong_to_node_id: str = Field(description="belong to which node id, shared means public") - type: str = Field(description="variable type, text-input, paragraph, select, number, file, file-list") + type: str = Field(description="variable type, text-input, paragraph, select, number, file, file-list") label: str = Field(description="label") description: str | None = Field(description="description", default="") variable: str = Field(description="variable key", default="") From 45164ce33e4608df77c7fb0ea8344bd45f9f48af Mon Sep 17 00:00:00 2001 From: 99 Date: Fri, 6 Feb 2026 10:37:26 +0800 Subject: [PATCH 07/10] refactor: strip external imports in workflow template transform (#32017) --- api/.importlinter | 1 - api/core/app/workflow/node_factory.py | 5 +++++ .../template_transform/template_transform_node.py | 13 +++++++++---- .../template_transform_node_spec.py | 2 +- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/api/.importlinter b/api/.importlinter index 9dad254560..2a6bb66a95 100644 --- a/api/.importlinter +++ b/api/.importlinter @@ -136,7 +136,6 @@ ignore_imports = core.workflow.nodes.llm.llm_utils -> models.provider core.workflow.nodes.llm.llm_utils -> services.credit_pool_service core.workflow.nodes.llm.node -> core.tools.signature - core.workflow.nodes.template_transform.template_transform_node -> configs core.workflow.nodes.tool.tool_node -> core.callback_handler.workflow_tool_callback_handler core.workflow.nodes.tool.tool_node -> core.tools.tool_engine core.workflow.nodes.tool.tool_node -> core.tools.tool_manager diff --git a/api/core/app/workflow/node_factory.py b/api/core/app/workflow/node_factory.py index a5773bbef8..6717be3ae6 100644 --- a/api/core/app/workflow/node_factory.py +++ b/api/core/app/workflow/node_factory.py @@ -47,6 +47,7 @@ class DifyNodeFactory(NodeFactory): code_providers: Sequence[type[CodeNodeProvider]] | None = None, code_limits: CodeNodeLimits | None = None, template_renderer: Jinja2TemplateRenderer | None = None, + template_transform_max_output_length: int | None = None, http_request_http_client: HttpClientProtocol | None = None, http_request_tool_file_manager_factory: Callable[[], ToolFileManager] = ToolFileManager, http_request_file_manager: FileManagerProtocol | None = None, @@ -68,6 +69,9 @@ class DifyNodeFactory(NodeFactory): max_object_array_length=dify_config.CODE_MAX_OBJECT_ARRAY_LENGTH, ) self._template_renderer = template_renderer or CodeExecutorJinja2TemplateRenderer() + self._template_transform_max_output_length = ( + template_transform_max_output_length or dify_config.TEMPLATE_TRANSFORM_MAX_LENGTH + ) self._http_request_http_client = http_request_http_client or ssrf_proxy self._http_request_tool_file_manager_factory = http_request_tool_file_manager_factory self._http_request_file_manager = http_request_file_manager or file_manager @@ -122,6 +126,7 @@ class DifyNodeFactory(NodeFactory): graph_init_params=self.graph_init_params, graph_runtime_state=self.graph_runtime_state, template_renderer=self._template_renderer, + max_output_length=self._template_transform_max_output_length, ) if node_type == NodeType.HTTP_REQUEST: diff --git a/api/core/workflow/nodes/template_transform/template_transform_node.py b/api/core/workflow/nodes/template_transform/template_transform_node.py index f7e0bccccf..3dc8afd9be 100644 --- a/api/core/workflow/nodes/template_transform/template_transform_node.py +++ b/api/core/workflow/nodes/template_transform/template_transform_node.py @@ -1,7 +1,6 @@ from collections.abc import Mapping, Sequence from typing import TYPE_CHECKING, Any -from configs import dify_config from core.workflow.enums import NodeType, WorkflowNodeExecutionStatus from core.workflow.node_events import NodeRunResult from core.workflow.nodes.base.node import Node @@ -16,12 +15,13 @@ if TYPE_CHECKING: from core.workflow.entities import GraphInitParams from core.workflow.runtime import GraphRuntimeState -MAX_TEMPLATE_TRANSFORM_OUTPUT_LENGTH = dify_config.TEMPLATE_TRANSFORM_MAX_LENGTH +DEFAULT_TEMPLATE_TRANSFORM_MAX_OUTPUT_LENGTH = 400_000 class TemplateTransformNode(Node[TemplateTransformNodeData]): node_type = NodeType.TEMPLATE_TRANSFORM _template_renderer: Jinja2TemplateRenderer + _max_output_length: int def __init__( self, @@ -31,6 +31,7 @@ class TemplateTransformNode(Node[TemplateTransformNodeData]): graph_runtime_state: "GraphRuntimeState", *, template_renderer: Jinja2TemplateRenderer | None = None, + max_output_length: int | None = None, ) -> None: super().__init__( id=id, @@ -40,6 +41,10 @@ class TemplateTransformNode(Node[TemplateTransformNodeData]): ) self._template_renderer = template_renderer or CodeExecutorJinja2TemplateRenderer() + if max_output_length is not None and max_output_length <= 0: + raise ValueError("max_output_length must be a positive integer") + self._max_output_length = max_output_length or DEFAULT_TEMPLATE_TRANSFORM_MAX_OUTPUT_LENGTH + @classmethod def get_default_config(cls, filters: Mapping[str, object] | None = None) -> Mapping[str, object]: """ @@ -69,11 +74,11 @@ class TemplateTransformNode(Node[TemplateTransformNodeData]): except TemplateRenderError as e: return NodeRunResult(inputs=variables, status=WorkflowNodeExecutionStatus.FAILED, error=str(e)) - if len(rendered) > MAX_TEMPLATE_TRANSFORM_OUTPUT_LENGTH: + if len(rendered) > self._max_output_length: return NodeRunResult( inputs=variables, status=WorkflowNodeExecutionStatus.FAILED, - error=f"Output length exceeds {MAX_TEMPLATE_TRANSFORM_OUTPUT_LENGTH} characters", + error=f"Output length exceeds {self._max_output_length} characters", ) return NodeRunResult( diff --git a/api/tests/unit_tests/core/workflow/nodes/template_transform/template_transform_node_spec.py b/api/tests/unit_tests/core/workflow/nodes/template_transform/template_transform_node_spec.py index 66d6c3c56b..61bdcbd250 100644 --- a/api/tests/unit_tests/core/workflow/nodes/template_transform/template_transform_node_spec.py +++ b/api/tests/unit_tests/core/workflow/nodes/template_transform/template_transform_node_spec.py @@ -217,7 +217,6 @@ class TestTemplateTransformNode: @patch( "core.workflow.nodes.template_transform.template_transform_node.CodeExecutorJinja2TemplateRenderer.render_template" ) - @patch("core.workflow.nodes.template_transform.template_transform_node.MAX_TEMPLATE_TRANSFORM_OUTPUT_LENGTH", 10) def test_run_output_length_exceeds_limit( self, mock_execute, basic_node_data, mock_graph, mock_graph_runtime_state, graph_init_params ): @@ -231,6 +230,7 @@ class TestTemplateTransformNode: graph_init_params=graph_init_params, graph=mock_graph, graph_runtime_state=mock_graph_runtime_state, + max_output_length=10, ) result = node._run() From 59a9cbbf78c3bbe20d15108c5d68066132369ed4 Mon Sep 17 00:00:00 2001 From: Ryan Date: Fri, 6 Feb 2026 10:46:50 +0800 Subject: [PATCH 08/10] chore: remove .codex/skills directory (#32022) Co-authored-by: Longwei Liu --- .codex/skills/component-refactoring | 1 - .codex/skills/frontend-code-review | 1 - .codex/skills/frontend-testing | 1 - .codex/skills/orpc-contract-first | 1 - 4 files changed, 4 deletions(-) delete mode 120000 .codex/skills/component-refactoring delete mode 120000 .codex/skills/frontend-code-review delete mode 120000 .codex/skills/frontend-testing delete mode 120000 .codex/skills/orpc-contract-first diff --git a/.codex/skills/component-refactoring b/.codex/skills/component-refactoring deleted file mode 120000 index 53ae67e2f2..0000000000 --- a/.codex/skills/component-refactoring +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/component-refactoring \ No newline at end of file diff --git a/.codex/skills/frontend-code-review b/.codex/skills/frontend-code-review deleted file mode 120000 index 55654ffbd7..0000000000 --- a/.codex/skills/frontend-code-review +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/frontend-code-review \ No newline at end of file diff --git a/.codex/skills/frontend-testing b/.codex/skills/frontend-testing deleted file mode 120000 index 092cec7745..0000000000 --- a/.codex/skills/frontend-testing +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/frontend-testing \ No newline at end of file diff --git a/.codex/skills/orpc-contract-first b/.codex/skills/orpc-contract-first deleted file mode 120000 index da47b335c7..0000000000 --- a/.codex/skills/orpc-contract-first +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/orpc-contract-first \ No newline at end of file From b24e6edada70f1711b745787919e5b113dd047ad Mon Sep 17 00:00:00 2001 From: wangxiaolei Date: Fri, 6 Feb 2026 11:24:39 +0800 Subject: [PATCH 09/10] fix: fix agent node tool type is not right (#32008) Infer real tool type via querying relevant database tables. The root cause for incorrect `type` field is still not clear. --- api/.importlinter | 2 + api/core/workflow/nodes/agent/agent_node.py | 42 +++- .../core/workflow/nodes/agent/__init__.py | 0 .../workflow/nodes/agent/test_agent_node.py | 197 ++++++++++++++++++ 4 files changed, 239 insertions(+), 2 deletions(-) create mode 100644 api/tests/unit_tests/core/workflow/nodes/agent/__init__.py create mode 100644 api/tests/unit_tests/core/workflow/nodes/agent/test_agent_node.py diff --git a/api/.importlinter b/api/.importlinter index 2a6bb66a95..fb66df7334 100644 --- a/api/.importlinter +++ b/api/.importlinter @@ -102,6 +102,8 @@ forbidden_modules = core.trigger core.variables ignore_imports = + core.workflow.nodes.agent.agent_node -> core.db.session_factory + core.workflow.nodes.agent.agent_node -> models.tools core.workflow.nodes.loop.loop_node -> core.app.workflow.node_factory core.workflow.graph_engine.command_channels.redis_channel -> extensions.ext_redis core.workflow.workflow_entry -> core.app.workflow.layers.observability diff --git a/api/core/workflow/nodes/agent/agent_node.py b/api/core/workflow/nodes/agent/agent_node.py index e195aebe6d..e64a83034c 100644 --- a/api/core/workflow/nodes/agent/agent_node.py +++ b/api/core/workflow/nodes/agent/agent_node.py @@ -2,7 +2,7 @@ from __future__ import annotations import json from collections.abc import Generator, Mapping, Sequence -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any, Union, cast from packaging.version import Version from pydantic import ValidationError @@ -11,6 +11,7 @@ from sqlalchemy.orm import Session from core.agent.entities import AgentToolEntity from core.agent.plugin_entities import AgentStrategyParameter +from core.db.session_factory import session_factory from core.file import File, FileTransferMethod from core.memory.token_buffer_memory import TokenBufferMemory from core.model_manager import ModelInstance, ModelManager @@ -49,6 +50,12 @@ from factories import file_factory from factories.agent_factory import get_plugin_agent_strategy from models import ToolFile from models.model import Conversation +from models.tools import ( + ApiToolProvider, + BuiltinToolProvider, + MCPToolProvider, + WorkflowToolProvider, +) from services.tools.builtin_tools_manage_service import BuiltinToolManageService from .exc import ( @@ -259,7 +266,7 @@ class AgentNode(Node[AgentNodeData]): value = cast(list[dict[str, Any]], value) tool_value = [] for tool in value: - provider_type = ToolProviderType(tool.get("type", ToolProviderType.BUILT_IN)) + provider_type = self._infer_tool_provider_type(tool, self.tenant_id) setting_params = tool.get("settings", {}) parameters = tool.get("parameters", {}) manual_input_params = [key for key, value in parameters.items() if value is not None] @@ -748,3 +755,34 @@ class AgentNode(Node[AgentNodeData]): llm_usage=llm_usage, ) ) + + @staticmethod + def _infer_tool_provider_type(tool_config: dict[str, Any], tenant_id: str) -> ToolProviderType: + provider_type_str = tool_config.get("type") + if provider_type_str: + return ToolProviderType(provider_type_str) + + provider_id = tool_config.get("provider_name") + if not provider_id: + return ToolProviderType.BUILT_IN + + with session_factory.create_session() as session: + provider_map: dict[ + type[Union[WorkflowToolProvider, MCPToolProvider, ApiToolProvider, BuiltinToolProvider]], + ToolProviderType, + ] = { + WorkflowToolProvider: ToolProviderType.WORKFLOW, + MCPToolProvider: ToolProviderType.MCP, + ApiToolProvider: ToolProviderType.API, + BuiltinToolProvider: ToolProviderType.BUILT_IN, + } + + for provider_model, provider_type in provider_map.items(): + stmt = select(provider_model).where( + provider_model.id == provider_id, + provider_model.tenant_id == tenant_id, + ) + if session.scalar(stmt): + return provider_type + + raise AgentNodeError(f"Tool provider with ID '{provider_id}' not found.") diff --git a/api/tests/unit_tests/core/workflow/nodes/agent/__init__.py b/api/tests/unit_tests/core/workflow/nodes/agent/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/tests/unit_tests/core/workflow/nodes/agent/test_agent_node.py b/api/tests/unit_tests/core/workflow/nodes/agent/test_agent_node.py new file mode 100644 index 0000000000..a95892d0b6 --- /dev/null +++ b/api/tests/unit_tests/core/workflow/nodes/agent/test_agent_node.py @@ -0,0 +1,197 @@ +from unittest.mock import MagicMock, patch + +import pytest + +from core.tools.entities.tool_entities import ToolProviderType +from core.workflow.nodes.agent.agent_node import AgentNode + + +class TestInferToolProviderType: + """Test cases for AgentNode._infer_tool_provider_type method.""" + + def test_infer_type_from_config_workflow(self): + """Test inferring workflow provider type from config.""" + tool_config = { + "type": "workflow", + "provider_name": "workflow-provider-id", + } + tenant_id = "test-tenant" + + result = AgentNode._infer_tool_provider_type(tool_config, tenant_id) + + assert result == ToolProviderType.WORKFLOW + + def test_infer_type_from_config_builtin(self): + """Test inferring builtin provider type from config.""" + tool_config = { + "type": "builtin", + "provider_name": "builtin-provider-id", + } + tenant_id = "test-tenant" + + result = AgentNode._infer_tool_provider_type(tool_config, tenant_id) + + assert result == ToolProviderType.BUILT_IN + + def test_infer_type_from_config_api(self): + """Test inferring API provider type from config.""" + tool_config = { + "type": "api", + "provider_name": "api-provider-id", + } + tenant_id = "test-tenant" + + result = AgentNode._infer_tool_provider_type(tool_config, tenant_id) + + assert result == ToolProviderType.API + + def test_infer_type_from_config_mcp(self): + """Test inferring MCP provider type from config.""" + tool_config = { + "type": "mcp", + "provider_name": "mcp-provider-id", + } + tenant_id = "test-tenant" + + result = AgentNode._infer_tool_provider_type(tool_config, tenant_id) + + assert result == ToolProviderType.MCP + + def test_infer_type_invalid_config_value_raises_error(self): + """Test that invalid type value in config raises ValueError.""" + tool_config = { + "type": "invalid-type", + "provider_name": "workflow-provider-id", + } + tenant_id = "test-tenant" + + with pytest.raises(ValueError): + AgentNode._infer_tool_provider_type(tool_config, tenant_id) + + def test_infer_workflow_type_from_database(self): + """Test inferring workflow provider type from database.""" + tool_config = { + "provider_name": "workflow-provider-id", + } + tenant_id = "test-tenant" + + with patch("core.db.session_factory.session_factory.create_session") as mock_create_session: + mock_session = MagicMock() + mock_create_session.return_value.__enter__.return_value = mock_session + + # First query (WorkflowToolProvider) returns a result + mock_session.scalar.return_value = True + + result = AgentNode._infer_tool_provider_type(tool_config, tenant_id) + + assert result == ToolProviderType.WORKFLOW + # Should only query once (after finding WorkflowToolProvider) + assert mock_session.scalar.call_count == 1 + + def test_infer_mcp_type_from_database(self): + """Test inferring MCP provider type from database.""" + tool_config = { + "provider_name": "mcp-provider-id", + } + tenant_id = "test-tenant" + + with patch("core.db.session_factory.session_factory.create_session") as mock_create_session: + mock_session = MagicMock() + mock_create_session.return_value.__enter__.return_value = mock_session + + # First query (WorkflowToolProvider) returns None + # Second query (MCPToolProvider) returns a result + mock_session.scalar.side_effect = [None, True] + + result = AgentNode._infer_tool_provider_type(tool_config, tenant_id) + + assert result == ToolProviderType.MCP + assert mock_session.scalar.call_count == 2 + + def test_infer_api_type_from_database(self): + """Test inferring API provider type from database.""" + tool_config = { + "provider_name": "api-provider-id", + } + tenant_id = "test-tenant" + + with patch("core.db.session_factory.session_factory.create_session") as mock_create_session: + mock_session = MagicMock() + mock_create_session.return_value.__enter__.return_value = mock_session + + # First query (WorkflowToolProvider) returns None + # Second query (MCPToolProvider) returns None + # Third query (ApiToolProvider) returns a result + mock_session.scalar.side_effect = [None, None, True] + + result = AgentNode._infer_tool_provider_type(tool_config, tenant_id) + + assert result == ToolProviderType.API + assert mock_session.scalar.call_count == 3 + + def test_infer_builtin_type_from_database(self): + """Test inferring builtin provider type from database.""" + tool_config = { + "provider_name": "builtin-provider-id", + } + tenant_id = "test-tenant" + + with patch("core.db.session_factory.session_factory.create_session") as mock_create_session: + mock_session = MagicMock() + mock_create_session.return_value.__enter__.return_value = mock_session + + # First three queries return None + # Fourth query (BuiltinToolProvider) returns a result + mock_session.scalar.side_effect = [None, None, None, True] + + result = AgentNode._infer_tool_provider_type(tool_config, tenant_id) + + assert result == ToolProviderType.BUILT_IN + assert mock_session.scalar.call_count == 4 + + def test_infer_type_default_when_not_found(self): + """Test raising AgentNodeError when provider is not found in database.""" + tool_config = { + "provider_name": "unknown-provider-id", + } + tenant_id = "test-tenant" + + with patch("core.db.session_factory.session_factory.create_session") as mock_create_session: + mock_session = MagicMock() + mock_create_session.return_value.__enter__.return_value = mock_session + + # All queries return None + mock_session.scalar.return_value = None + + # Current implementation raises AgentNodeError when provider not found + from core.workflow.nodes.agent.exc import AgentNodeError + + with pytest.raises(AgentNodeError, match="Tool provider with ID 'unknown-provider-id' not found"): + AgentNode._infer_tool_provider_type(tool_config, tenant_id) + + def test_infer_type_default_when_no_provider_name(self): + """Test defaulting to BUILT_IN when provider_name is missing.""" + tool_config = {} + tenant_id = "test-tenant" + + result = AgentNode._infer_tool_provider_type(tool_config, tenant_id) + + assert result == ToolProviderType.BUILT_IN + + def test_infer_type_database_exception_propagates(self): + """Test that database exception propagates (current implementation doesn't catch it).""" + tool_config = { + "provider_name": "provider-id", + } + tenant_id = "test-tenant" + + with patch("core.db.session_factory.session_factory.create_session") as mock_create_session: + mock_session = MagicMock() + mock_create_session.return_value.__enter__.return_value = mock_session + + # Database query raises exception + mock_session.scalar.side_effect = Exception("Database error") + + # Current implementation doesn't catch exceptions, so it propagates + with pytest.raises(Exception, match="Database error"): + AgentNode._infer_tool_provider_type(tool_config, tenant_id) From d9530f7bb75c324ec17fae870d752209e1e86194 Mon Sep 17 00:00:00 2001 From: longbingljw Date: Fri, 6 Feb 2026 13:01:31 +0900 Subject: [PATCH 10/10] fix: make `flask upgrade-db` fail on error (#32024) --- api/commands.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/commands.py b/api/commands.py index c4f2c9edbb..93855bc3b8 100644 --- a/api/commands.py +++ b/api/commands.py @@ -739,8 +739,10 @@ def upgrade_db(): click.echo(click.style("Database migration successful!", fg="green")) - except Exception: + except Exception as e: logger.exception("Failed to execute database migration") + click.echo(click.style(f"Database migration failed: {e}", fg="red")) + raise SystemExit(1) finally: lock.release() else: