From be8f265e43757ef5f70995d75112f9b03b69852b Mon Sep 17 00:00:00 2001 From: wangxiaolei Date: Thu, 5 Feb 2026 17:32:33 +0800 Subject: [PATCH 01/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 02/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 03/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 04/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 05/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 06/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 07/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 08/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 09/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: From e988266f5368d8b333702874811d6f5208f4778a Mon Sep 17 00:00:00 2001 From: QuantumGhost Date: Fri, 6 Feb 2026 14:15:32 +0800 Subject: [PATCH 10/10] chore: update HITL auto deploy workflow (#32040) --- .github/workflows/deploy-hitl.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/deploy-hitl.yml b/.github/workflows/deploy-hitl.yml index 7d5f0a22e7..6a4702d2da 100644 --- a/.github/workflows/deploy-hitl.yml +++ b/.github/workflows/deploy-hitl.yml @@ -4,8 +4,7 @@ on: workflow_run: workflows: ["Build and Push API & Web"] branches: - - "feat/hitl-frontend" - - "feat/hitl-backend" + - "feat/hitl" types: - completed @@ -14,10 +13,7 @@ jobs: runs-on: ubuntu-latest if: | github.event.workflow_run.conclusion == 'success' && - ( - github.event.workflow_run.head_branch == 'feat/hitl-frontend' || - github.event.workflow_run.head_branch == 'feat/hitl-backend' - ) + github.event.workflow_run.head_branch == 'feat/hitl' steps: - name: Deploy to server uses: appleboy/ssh-action@v1